@bongos/core 1.21.128 → 1.21.130
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 +44 -19
- package/bin/bongos.js +1 -0
- package/docs/architecture.md +12 -0
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/migrations/core_276_module_release_channels.sql +73 -0
- package/modules/onboarding/howto-guide.js +6 -0
- package/modules/onboarding/howto-moments.js +118 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/gds/claude-md-template.js +1 -0
- package/scripts/gds/howto.js +145 -0
- package/src/module-api.js +1 -1
- package/tests/conductor_main_e2e.mjs +22 -8
- package/tests/howto_guide.mjs +12 -0
- package/tests/howto_moments.mjs +183 -0
- package/tests/module_release_channels.mjs +58 -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.130",
|
|
6
|
+
"core_contract": "1.21.130",
|
|
7
|
+
"source_commit": "d9a1489516436c9e34730e3c028f4465cfd62474",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-
|
|
9
|
+
"built_at": "2026-10-15T17:14:20.940Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 606,
|
|
13
13
|
"agent_docs_stubbed": 28,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 3146,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3781,
|
|
20
|
+
"tree_sha256": "309831afd3ba5a2aed6ebe90ff918936d2c3fd2ea36d1fb2a90e41b7187cb474",
|
|
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": "656e91004fc2236dfd121dd3f46848ba5a51331c2eb17095398eb30a5255b7ef"
|
|
316
316
|
},
|
|
317
317
|
{
|
|
318
318
|
"path": "clients/bongos-client/README.md",
|
|
@@ -2387,7 +2387,7 @@
|
|
|
2387
2387
|
{
|
|
2388
2388
|
"path": "docs/architecture.md",
|
|
2389
2389
|
"mode": "0000644",
|
|
2390
|
-
"sha256": "
|
|
2390
|
+
"sha256": "fca9878d8501b33c0258045f61365a90e60e148c2b730b00a83a6f4f89b79b8d"
|
|
2391
2391
|
},
|
|
2392
2392
|
{
|
|
2393
2393
|
"path": "docs/branding-contract.md",
|
|
@@ -2912,7 +2912,7 @@
|
|
|
2912
2912
|
{
|
|
2913
2913
|
"path": "docs/file-map.md",
|
|
2914
2914
|
"mode": "0000644",
|
|
2915
|
-
"sha256": "
|
|
2915
|
+
"sha256": "18a6694f80109250c168809eb753e4d4f8c7c62d5eed96b9bee217d89189decf"
|
|
2916
2916
|
},
|
|
2917
2917
|
{
|
|
2918
2918
|
"path": "docs/handoff-template.md",
|
|
@@ -2922,7 +2922,7 @@
|
|
|
2922
2922
|
{
|
|
2923
2923
|
"path": "docs/module-api-changelog.md",
|
|
2924
2924
|
"mode": "0000644",
|
|
2925
|
-
"sha256": "
|
|
2925
|
+
"sha256": "a8659f105f89cd130c2772293c07aad2e7071ef6adc4c094e65fad8ac205a2ad"
|
|
2926
2926
|
},
|
|
2927
2927
|
{
|
|
2928
2928
|
"path": "docs/modules-contract.md",
|
|
@@ -4154,6 +4154,11 @@
|
|
|
4154
4154
|
"mode": "0000644",
|
|
4155
4155
|
"sha256": "75381af0ac96645f2f8fe254f609c75d2fa83bc2fc1f13887c23165fc8eed58e"
|
|
4156
4156
|
},
|
|
4157
|
+
{
|
|
4158
|
+
"path": "migrations/core_276_module_release_channels.sql",
|
|
4159
|
+
"mode": "0000644",
|
|
4160
|
+
"sha256": "550884fa117e707b65f5c56faca0e4e3850905db94d3f275cad5d520aa68db03"
|
|
4161
|
+
},
|
|
4157
4162
|
{
|
|
4158
4163
|
"path": "modules/agents/CLAUDE.md",
|
|
4159
4164
|
"mode": "0000644",
|
|
@@ -7932,7 +7937,12 @@
|
|
|
7932
7937
|
{
|
|
7933
7938
|
"path": "modules/onboarding/howto-guide.js",
|
|
7934
7939
|
"mode": "0000644",
|
|
7935
|
-
"sha256": "
|
|
7940
|
+
"sha256": "7b352dda65b04f44e48feb19fc3a959d9a122411c22f2830b9cc967d1a0f6852"
|
|
7941
|
+
},
|
|
7942
|
+
{
|
|
7943
|
+
"path": "modules/onboarding/howto-moments.js",
|
|
7944
|
+
"mode": "0000644",
|
|
7945
|
+
"sha256": "6950e77759f8e574187e7e77871e28492116c3c5ad17d3cd2cebd3a018c0ad6c"
|
|
7936
7946
|
},
|
|
7937
7947
|
{
|
|
7938
7948
|
"path": "modules/onboarding/howto-state.js",
|
|
@@ -10492,12 +10502,12 @@
|
|
|
10492
10502
|
{
|
|
10493
10503
|
"path": "package-lock.json",
|
|
10494
10504
|
"mode": "0000644",
|
|
10495
|
-
"sha256": "
|
|
10505
|
+
"sha256": "1556b57526d416a0156c23915ba0aaf080eef4732031261f80c7a2c891f49b9d"
|
|
10496
10506
|
},
|
|
10497
10507
|
{
|
|
10498
10508
|
"path": "package.json",
|
|
10499
10509
|
"mode": "0000644",
|
|
10500
|
-
"sha256": "
|
|
10510
|
+
"sha256": "5db6c25e29a3abec630266149723a99b09a249bcc375bca40e39bcc8eb5a0d2b"
|
|
10501
10511
|
},
|
|
10502
10512
|
{
|
|
10503
10513
|
"path": "public-docs/index.html",
|
|
@@ -10517,7 +10527,7 @@
|
|
|
10517
10527
|
{
|
|
10518
10528
|
"path": "release-notes.json",
|
|
10519
10529
|
"mode": "0000644",
|
|
10520
|
-
"sha256": "
|
|
10530
|
+
"sha256": "52d4fa7c27b8a7cfe9ead4896e254d332b1abae140b60beb8c848efa1e16fa2f"
|
|
10521
10531
|
},
|
|
10522
10532
|
{
|
|
10523
10533
|
"path": "scripts/cron/style_guide_audit.sh",
|
|
@@ -10842,7 +10852,7 @@
|
|
|
10842
10852
|
{
|
|
10843
10853
|
"path": "scripts/gds/claude-md-template.js",
|
|
10844
10854
|
"mode": "0000644",
|
|
10845
|
-
"sha256": "
|
|
10855
|
+
"sha256": "ebfbfe3e5dbd865b5699d65fd7f3fc345f992c92f84b41db716295c40553dacb"
|
|
10846
10856
|
},
|
|
10847
10857
|
{
|
|
10848
10858
|
"path": "scripts/gds/cleanup-orphans.js",
|
|
@@ -11249,6 +11259,11 @@
|
|
|
11249
11259
|
"mode": "0000644",
|
|
11250
11260
|
"sha256": "b7c61201d165b8c2b7e51b3a057153c3c7238bfc8bd55412acaf0ba87acd367d"
|
|
11251
11261
|
},
|
|
11262
|
+
{
|
|
11263
|
+
"path": "scripts/gds/howto.js",
|
|
11264
|
+
"mode": "0000644",
|
|
11265
|
+
"sha256": "06c77dee345d93604e5d1bde431476399e858556897114bae368ed513d3a944c"
|
|
11266
|
+
},
|
|
11252
11267
|
{
|
|
11253
11268
|
"path": "scripts/gds/http-api-client.js",
|
|
11254
11269
|
"mode": "0000644",
|
|
@@ -12867,7 +12882,7 @@
|
|
|
12867
12882
|
{
|
|
12868
12883
|
"path": "src/module-api.js",
|
|
12869
12884
|
"mode": "0000644",
|
|
12870
|
-
"sha256": "
|
|
12885
|
+
"sha256": "7c374bdf850a487a15f60f8ddd21aec96c49f644a50b396d1567a1e227bf4e0c"
|
|
12871
12886
|
},
|
|
12872
12887
|
{
|
|
12873
12888
|
"path": "src/module-loader/catalog.js",
|
|
@@ -13782,7 +13797,7 @@
|
|
|
13782
13797
|
{
|
|
13783
13798
|
"path": "tests/conductor_main_e2e.mjs",
|
|
13784
13799
|
"mode": "0000644",
|
|
13785
|
-
"sha256": "
|
|
13800
|
+
"sha256": "5d825f686bbe5351def658bf6e2f011e3ca351312f04009500ee65c8a195914d"
|
|
13786
13801
|
},
|
|
13787
13802
|
{
|
|
13788
13803
|
"path": "tests/confirm_attribution.mjs",
|
|
@@ -15647,7 +15662,12 @@
|
|
|
15647
15662
|
{
|
|
15648
15663
|
"path": "tests/howto_guide.mjs",
|
|
15649
15664
|
"mode": "0000644",
|
|
15650
|
-
"sha256": "
|
|
15665
|
+
"sha256": "72b439c6189e78bed945ec7e16da9cfeb32e34c9f3a4417f024c32dec07a73cf"
|
|
15666
|
+
},
|
|
15667
|
+
{
|
|
15668
|
+
"path": "tests/howto_moments.mjs",
|
|
15669
|
+
"mode": "0000644",
|
|
15670
|
+
"sha256": "fc668c29944ddbbc629e9ee4e6c30dc1cffbc88d12f478a1b95a4585e360a185"
|
|
15651
15671
|
},
|
|
15652
15672
|
{
|
|
15653
15673
|
"path": "tests/howto_state.mjs",
|
|
@@ -16474,6 +16494,11 @@
|
|
|
16474
16494
|
"mode": "0000644",
|
|
16475
16495
|
"sha256": "4f5a0c6e24d3883a156798f17c485183cf73be2cf3a0e0c610d5014a142909e5"
|
|
16476
16496
|
},
|
|
16497
|
+
{
|
|
16498
|
+
"path": "tests/module_release_channels.mjs",
|
|
16499
|
+
"mode": "0000644",
|
|
16500
|
+
"sha256": "8607f9b60a6094296b005def559eb3cfa6861add5bfa473d9264a3dc4025bf44"
|
|
16501
|
+
},
|
|
16477
16502
|
{
|
|
16478
16503
|
"path": "tests/module_route_rank.mjs",
|
|
16479
16504
|
"mode": "0000644",
|
package/bin/bongos.js
CHANGED
|
@@ -82,6 +82,7 @@ const VERBS = {
|
|
|
82
82
|
status: { script: 'status.js', group: 'work', summary: 'Version / criterion progress (bongos status C8)', selfHelp: true },
|
|
83
83
|
recall: { script: 'recall.js', group: 'work', summary: 'Search the docs + project knowledge (bongos recall "x")' },
|
|
84
84
|
cost: { script: 'cost.js', group: 'work', summary: 'Log a cost entry' },
|
|
85
|
+
howto: { script: 'howto.js', group: 'work', summary: 'The how-to guide: show the unread steps for a moment (bongos howto moment first_open)' },
|
|
85
86
|
|
|
86
87
|
login: { script: 'login.js', group: 'account', summary: 'Sign in to an instance (bongos login <url> — browser click, no paste)' },
|
|
87
88
|
setup: { script: 'setup.js', group: 'account', summary: 'Full builder setup on this instance (art key, disciplines, consent)' },
|
package/docs/architecture.md
CHANGED
|
@@ -251,6 +251,18 @@ store_module_prices(id, module_key, price_cents, price_credits, set_by, set_at)
|
|
|
251
251
|
-- in that currency, at least one must be set.
|
|
252
252
|
```
|
|
253
253
|
|
|
254
|
+
Module release channels (migration `core_276`, task 1003815, [ADR 0344](adr/0344-a-tester-is-a-project-that-opts-in-once.md) — which channel a published version is on; the data only, the tester-first publish, promotion gate and channel-aware install are their own tasks):
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
store_release_channels(channel PK, description, serves_installs, sort_order, created_at)
|
|
258
|
+
-- rows, not a CHECK: tester + general serve installs, withdrawn never does;
|
|
259
|
+
-- a new channel is an INSERT, not a schema change.
|
|
260
|
+
store_module_version_channels(id, version_id FK store_module_versions, channel FK,
|
|
261
|
+
reason short code, set_by builder | NULL (system), set_at)
|
|
262
|
+
-- history; newest row per version is current. Pre-existing versions were
|
|
263
|
+
-- backfilled to general ('predates_staged_rollout').
|
|
264
|
+
```
|
|
265
|
+
|
|
254
266
|
Module entitlements (migration `core_259`, task 1003813 — who holds which module, at which version; the record install, update and the telemetry signals consult; it records ownership, never payment — area 8 owns money):
|
|
255
267
|
|
|
256
268
|
```
|
package/docs/file-map.md
CHANGED
|
@@ -169,6 +169,7 @@
|
|
|
169
169
|
│ ├── linkify-refs.js ← rewrite bare `#NNN` task refs across ALL docs → status-dashboard permalinks (stops GitHub's PR/issue autolinker mis-linking them); conservative classifier, token-free id list via /public/ref-ids; --apply / --check (CI gate: tests/linkify_check.mjs, in the required `unit` check) / --dry-run default ([#604](https://example.com/builders#/task/604), [#653](https://example.com/builders#/task/653)).
|
|
170
170
|
│ ├── release.js ← resolve claim as abandoned (`/builder-release`)
|
|
171
171
|
│ ├── cost.js ← log a cost entry (`/builder-cost`)
|
|
172
|
+
│ ├── howto.js ← the how-to guide for AI tools without hooks: `bongos howto moment <id>` prints + marks read the person's unread steps for a moment (task 1004635); `show` / `next` / `skip` / `reset` reopen the guide, continue it, turn it off or start it over (task 1004636)
|
|
172
173
|
│ ├── me.js ← current builder profile + leaderboard
|
|
173
174
|
│ ├── goal.js ← goal board CLI: `list` (rollup per goal) + `show <id>` (criteria, tasks, waits-on, next claimable)
|
|
174
175
|
│ ├── seed-v3-tasks.js ← V3 Planning Session #1: 80 net-new GDS-V3 tasks (V3.R01–V3.R89) + 10 idea_inbox promotions; idempotent on source_ref
|
|
@@ -472,7 +473,7 @@ To install on a new machine, symlink each routine's directory under `~/.claude/s
|
|
|
472
473
|
├── rank-change-greeting.js ← promotion/demotion greeting (V3.R28 #249); spawned by session-start.js, also runnable standalone
|
|
473
474
|
├── pull-memory.js ← memory pull-on-session-start (V3.R56 #275); spawned by session-start.js (kill-switches: OTB_MEMORY_PULL_OFF / OTB_SUBAGENT)
|
|
474
475
|
├── recall-hints.js ← SessionStart: when the session opens holding a claim, surface the top project-knowledge hits for that task via POST /api/bongos/search (#963/V4.R25, ADR 0060)
|
|
475
|
-
├── conductor.js ← UserPromptSubmit: MAS Conductor advisory + dispatch directive (#427/#439); self-gates on OTB_CONDUCTOR_OFF / OTB_SUBAGENT
|
|
476
|
+
├── conductor.js ← UserPromptSubmit: MAS Conductor advisory + dispatch directive (#427/#439); self-gates on OTB_CONDUCTOR_OFF / OTB_SUBAGENT; also delivers the how-to step for the moment and marks it read (task 1004635)
|
|
476
477
|
├── session-cards.js ← UserPromptSubmit: deterministic delivery of READ-ONLY in-session cards (builder-start, status) — pre-renders the card + injects a one-step render directive (task 1288)
|
|
477
478
|
├── lifecycle-card.js ← PostToolUse (Bash|PowerShell): deterministic delivery of MUTATING lifecycle cards (claim/ship/release) — the write-side mirror of session-cards.js (task 1288)
|
|
478
479
|
├── stuck-detector.js ← PostToolUse + PostToolUseFailure (Bash|PowerShell): the third time one session hits the same wall — the same `[otb-plain]` card for the same task id, or the same red check — it injects ONE instruction: stop retrying, diagnose (path named), or /ask-for-help. Never blocks; silent with no card; kill-switch BONGOS_STUCK_DETECTOR_OFF=1 (task 1003990)
|
|
@@ -3093,5 +3093,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3093
3093
|
landed since 1.21.126 with no explicit bump. run 38764985266. (task 1002620)
|
|
3094
3094
|
1.21.128 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3095
3095
|
landed since 1.21.127 with no explicit bump. run 38770118067. (task 1002620)
|
|
3096
|
+
1.21.129 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3097
|
+
landed since 1.21.128 with no explicit bump. run 38773346540. (task 1002620)
|
|
3098
|
+
1.21.130 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3099
|
+
landed since 1.21.129 with no explicit bump. run 38775541138. (task 1002620)
|
|
3096
3100
|
---------------------------------------------------------------------------
|
|
3097
3101
|
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
-- core_276_module_release_channels.sql — which release channel each published
|
|
2
|
+
-- module version sits on: tester first, then general (ADR 0344).
|
|
3
|
+
--
|
|
4
|
+
-- task 1003815 (goal 1000091, working area 5). ADR 0344 D2/D3: every new version
|
|
5
|
+
-- goes to the tester channel first, and moves to general after seven days or on
|
|
6
|
+
-- evidence. This is the DATA ONLY. Putting a new version on tester (task 1003803),
|
|
7
|
+
-- the promotion gate (task 1003804) and install/update honouring the channel
|
|
8
|
+
-- (task 1003805) are their own tasks and read/write these tables.
|
|
9
|
+
--
|
|
10
|
+
-- store_release_channels the channels that exist, as ROWS, not a CHECK
|
|
11
|
+
-- or a boolean. Two serve installs today
|
|
12
|
+
-- (tester, general); 'withdrawn' is the third,
|
|
13
|
+
-- for a version taken back. A fourth is an
|
|
14
|
+
-- INSERT here, never a schema change — the
|
|
15
|
+
-- retrofit the task warns about.
|
|
16
|
+
-- `serves_installs` says whether a project can
|
|
17
|
+
-- be handed a version on that channel; a
|
|
18
|
+
-- withdrawn version is never handed out, but a
|
|
19
|
+
-- copy already installed keeps working (ADR
|
|
20
|
+
-- 0338 D2 — nothing here can reach it).
|
|
21
|
+
-- store_module_version_channels the HISTORY of a version's channel. A channel
|
|
22
|
+
-- is a property of a version, not of a module,
|
|
23
|
+
-- and store_module_versions is immutable, so a
|
|
24
|
+
-- move is a new row and the CURRENT channel is
|
|
25
|
+
-- the newest row per version. The history is
|
|
26
|
+
-- what lets the promotion gate answer "how long
|
|
27
|
+
-- has this been on tester" and who moved it.
|
|
28
|
+
-- `set_by` is NULL when the system moved it
|
|
29
|
+
-- (the seven-day promotion); `reason` is a
|
|
30
|
+
-- short code, never free text.
|
|
31
|
+
--
|
|
32
|
+
-- Versions published before this migration were never on a tester channel, so
|
|
33
|
+
-- each gets one 'general' row with reason 'predates_staged_rollout' — every
|
|
34
|
+
-- version then has a current channel, and nothing already offered to everyone is
|
|
35
|
+
-- taken away from them.
|
|
36
|
+
--
|
|
37
|
+
-- Additive only (two new tables, rows inserted). Idempotent: safe to re-run.
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS store_release_channels (
|
|
40
|
+
channel text PRIMARY KEY
|
|
41
|
+
CHECK (channel ~ '^[a-z][a-z0-9_]{0,31}$'),
|
|
42
|
+
description text NOT NULL,
|
|
43
|
+
serves_installs boolean NOT NULL,
|
|
44
|
+
sort_order integer NOT NULL,
|
|
45
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
INSERT INTO store_release_channels (channel, description, serves_installs, sort_order) VALUES
|
|
49
|
+
('tester', 'New versions land here first; only projects that opted in to testing get them (ADR 0344 D1).', true, 10),
|
|
50
|
+
('general', 'Offered to every project that has the module.', true, 20),
|
|
51
|
+
('withdrawn', 'Taken back: never handed out again; copies already installed keep working (ADR 0338 D2).', false, 90)
|
|
52
|
+
ON CONFLICT (channel) DO NOTHING;
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS store_module_version_channels (
|
|
55
|
+
id bigserial PRIMARY KEY,
|
|
56
|
+
version_id bigint NOT NULL REFERENCES store_module_versions(id),
|
|
57
|
+
channel text NOT NULL REFERENCES store_release_channels(channel),
|
|
58
|
+
reason text NOT NULL CHECK (reason ~ '^[a-z][a-z0-9_]{0,39}$'),
|
|
59
|
+
set_by bigint REFERENCES builders(id),
|
|
60
|
+
set_at timestamptz NOT NULL DEFAULT now()
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
-- The current channel is the newest row per version; this index answers it.
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_store_module_version_channels_current
|
|
65
|
+
ON store_module_version_channels (version_id, set_at DESC, id DESC);
|
|
66
|
+
|
|
67
|
+
INSERT INTO store_module_version_channels (version_id, channel, reason)
|
|
68
|
+
SELECT v.id, 'general', 'predates_staged_rollout'
|
|
69
|
+
FROM store_module_versions v
|
|
70
|
+
WHERE NOT EXISTS (SELECT 1 FROM store_module_version_channels c WHERE c.version_id = v.id);
|
|
71
|
+
|
|
72
|
+
INSERT INTO schema_migrations (version) VALUES ('core_276_module_release_channels')
|
|
73
|
+
ON CONFLICT (version) DO NOTHING;
|
|
@@ -67,6 +67,12 @@ const STEPS = Object.freeze([
|
|
|
67
67
|
when: 'When you want to see everything at a glance instead of asking.',
|
|
68
68
|
try: 'Ask: "Open the hall for this project" or "What is the hall link?"',
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
id: 'reopen_the_guide', moment: 'first_open', title: 'Come back to this guide',
|
|
72
|
+
what: 'These tips show up once each, at the moment they help. You can look at the whole list, pick up the next tip, or turn the tips off.',
|
|
73
|
+
when: 'Whenever you want a refresher, or the tips get in the way.',
|
|
74
|
+
try: 'Say: "Show me the how-to", "What\'s next in the tutorial?" or "Skip the tutorial."',
|
|
75
|
+
},
|
|
70
76
|
// setup
|
|
71
77
|
{
|
|
72
78
|
id: 'sign_in', moment: 'setup', title: 'Sign in once',
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// modules/onboarding/howto-moments.js — which how-to step to show right now, and the words that ask the AI to show it (task 1004635)
|
|
2
|
+
//
|
|
3
|
+
// WHY. The how-to guide (howto-guide.js) is only useful if a step reaches the person
|
|
4
|
+
// at the moment it applies — the first prompt in a new project, the first claim, the
|
|
5
|
+
// first ship — and never again once read (howto-state.js keeps the read marks). This
|
|
6
|
+
// file is the one place that decides WHICH step that is, so every delivery path gives
|
|
7
|
+
// the same answer:
|
|
8
|
+
// • Claude Code: the Conductor hook (.claude/hooks/conductor.js) reads the signals it
|
|
9
|
+
// already has (the prompt, the active claims, whether this is the session's first
|
|
10
|
+
// prompt), injects the step, and marks it read;
|
|
11
|
+
// • any other AI tool: the instance CLAUDE.md tells it to run
|
|
12
|
+
// `npx bongos howto moment <id>` (scripts/gds/howto.js), which prints the step and
|
|
13
|
+
// marks it read the same way.
|
|
14
|
+
//
|
|
15
|
+
// Short by design: at most MAX_STEPS steps, from ONE moment, per prompt. A read (or
|
|
16
|
+
// skipped) step is never picked again.
|
|
17
|
+
//
|
|
18
|
+
// Pure: signals and read ids in, steps and text out. No network, no files.
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const guide = require('./howto-guide');
|
|
23
|
+
|
|
24
|
+
const MAX_STEPS = 2;
|
|
25
|
+
|
|
26
|
+
// Prompt words that open a moment. Vocabulary-gated so ordinary prompts stay quiet.
|
|
27
|
+
const SHIP_RE = /\/builder-ship\b|\bship (?:it|this)\b|\bi'?m done\b|\bmark (?:this |it )?complete\b/i;
|
|
28
|
+
const STUCK_RE = /\bstuck\b|\bconfus(?:ed|ing)\b|\bi'?m lost\b|\bnot working\b|\bdoesn'?t work\b|\bwhat went wrong\b/i;
|
|
29
|
+
const FIND_WORK_RE = /\bwhat (?:can|should) i (?:work on|do)\b|\bwhat'?s next\b|\/builder-start\b|\bwhat'?s claimable\b/i;
|
|
30
|
+
const GOAL_RE = /\bgoal\b|\/goal-create\b/i;
|
|
31
|
+
const TASK_RE = /\b(?:make|create|file|new|add) (?:a |another )?task\b|\bsave (?:an |this )?idea\b/i;
|
|
32
|
+
const INVITE_RE = /\binvit(?:e|ing)\b|\bteammates?\b|\bco-?workers?\b|\bcolleagues?\b/i;
|
|
33
|
+
const SETTINGS_RE = /\bsettings?\b|\bship[- ]mode\b|\bshorter answers?\b|\btoo (?:long|technical)\b/i;
|
|
34
|
+
const SETUP_RE = /\bset ?up\b|\bsign(?:ed)? ?in\b|\blog ?in\b|\/builder-setup\b/i;
|
|
35
|
+
|
|
36
|
+
// A person asking for the guide itself (task 1004636): "show me the how-to", "what's
|
|
37
|
+
// next in the tutorial", "skip the tutorial", "start the tutorial over". Checked in
|
|
38
|
+
// this order, so "stop showing me the tutorial" is a skip, not a show.
|
|
39
|
+
// "guide" and "tips" only with a determiner straight before them, so "the style guide"
|
|
40
|
+
// or "tips for the header" are not read as asking for this guide.
|
|
41
|
+
const GUIDE = String.raw`(?:how-?to|tutorial|(?:the|this|your|these|those) (?:guide|tips))`;
|
|
42
|
+
const REQUESTS = [
|
|
43
|
+
['skip', new RegExp(String.raw`\b(?:skip|stop|turn off|hide|dismiss|no more)\b.{0,24}\b${GUIDE}\b|\b${GUIDE}\b.{0,12}\b(?:off|away)\b`, 'i')],
|
|
44
|
+
['reset', new RegExp(String.raw`\b(?:restart|start over|reset|bring back)\b.{0,24}\b${GUIDE}\b|\b${GUIDE}\b.{0,12}\bover\b`, 'i')],
|
|
45
|
+
['next', new RegExp(String.raw`\bnext\b.{0,24}\b${GUIDE}\b|\b(?:continue|resume)\b.{0,12}\b${GUIDE}\b`, 'i')],
|
|
46
|
+
['show', new RegExp(String.raw`\b(?:show|open|reopen|see|view)\b.{0,24}\b${GUIDE}\b|\b${GUIDE}\b.{0,12}\bprogress\b`, 'i')],
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// 'show' | 'next' | 'skip' | 'reset' when the prompt asks for the guide itself, else null.
|
|
50
|
+
function guideRequest(prompt) {
|
|
51
|
+
const p = typeof prompt === 'string' ? prompt : '';
|
|
52
|
+
for (const [kind, re] of REQUESTS) if (re.test(p)) return kind;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// What the AI is told to do for one of those requests: run the matching command and
|
|
57
|
+
// pass its output on. The command changes the stored read state itself.
|
|
58
|
+
function renderRequestForAi(kind) {
|
|
59
|
+
const said = { show: 'to see the how-to', next: 'for the next how-to step', skip: 'to skip the how-to', reset: 'to start the how-to over' }[kind];
|
|
60
|
+
return `The person asked ${said}: run \`npx bongos howto ${kind}\` and pass on what it prints in plain words.`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The moments open right now, most urgent first. Signals:
|
|
64
|
+
// firstPrompt true on the first prompt of a session
|
|
65
|
+
// prompt the person's words
|
|
66
|
+
// claims null (unknown) | [] (none) | [...] active claims
|
|
67
|
+
// read iterable of step ids already read or skipped
|
|
68
|
+
function openMoments({ firstPrompt = false, prompt = '', claims = null, read = [] } = {}) {
|
|
69
|
+
const p = typeof prompt === 'string' ? prompt : '';
|
|
70
|
+
const known = Array.isArray(claims);
|
|
71
|
+
const hasClaim = known && claims.length > 0;
|
|
72
|
+
const done = new Set(read || []);
|
|
73
|
+
const out = [];
|
|
74
|
+
if (STUCK_RE.test(p)) out.push('stuck');
|
|
75
|
+
if (hasClaim && SHIP_RE.test(p)) out.push('first_ship');
|
|
76
|
+
if (hasClaim) out.push('first_claim');
|
|
77
|
+
if (firstPrompt) out.push('first_open');
|
|
78
|
+
// After a ship: no claim any more, and the shipping walkthrough was already shown.
|
|
79
|
+
if (known && !hasClaim && guide.stepsForMoment('first_ship').every((s) => done.has(s.id))) out.push('after_ship');
|
|
80
|
+
if (SETUP_RE.test(p)) out.push('setup');
|
|
81
|
+
if (GOAL_RE.test(p)) out.push('goal');
|
|
82
|
+
if (TASK_RE.test(p)) out.push('task');
|
|
83
|
+
if (FIND_WORK_RE.test(p) || (firstPrompt && known && !hasClaim)) out.push('find_work');
|
|
84
|
+
if (INVITE_RE.test(p)) out.push('invite');
|
|
85
|
+
if (SETTINGS_RE.test(p)) out.push('settings');
|
|
86
|
+
return [...new Set(out)];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The unread steps of ONE moment, in guide order, at most `max`. Unknown moment → [].
|
|
90
|
+
function unreadForMoment(momentId, read = [], { max = MAX_STEPS } = {}) {
|
|
91
|
+
const done = new Set(read || []);
|
|
92
|
+
return guide.stepsForMoment(momentId).filter((s) => !done.has(s.id)).slice(0, max);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The first open moment that still has unread steps → { moment, steps }, or null.
|
|
96
|
+
function pickSteps(signals = {}, { max = MAX_STEPS } = {}) {
|
|
97
|
+
for (const id of openMoments(signals)) {
|
|
98
|
+
const steps = unreadForMoment(id, signals.read, { max });
|
|
99
|
+
if (steps.length) return { moment: guide.MOMENTS.find((m) => m.id === id), steps };
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The words handed to the AI: what to tell the person, briefly, once.
|
|
105
|
+
function renderForAi({ moment, steps }) {
|
|
106
|
+
const parts = steps.map((s) => `"${s.title}": ${s.what} Try: ${s.try}`);
|
|
107
|
+
return `How-to (${moment.title.toLowerCase()}): the person has not seen this yet. `
|
|
108
|
+
+ 'At the end of your reply, in two or three plain sentences, tell them: '
|
|
109
|
+
+ parts.join(' Then: ')
|
|
110
|
+
+ ' Say it once and keep it short; it is now marked read and will not come back.';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The same steps for a person reading a terminal (`npx bongos howto moment <id>`).
|
|
114
|
+
function renderForPerson({ moment, steps }) {
|
|
115
|
+
return [`${moment.title}:`, ...steps.map((s) => `- ${s.title}: ${s.what}\n Try: ${s.try}`)].join('\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { MAX_STEPS, guideRequest, renderRequestForAi, openMoments, unreadForMoment, pickSteps, renderForAi, renderForPerson };
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.130",
|
|
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.130",
|
|
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.130",
|
|
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
|
@@ -9766,5 +9766,21 @@
|
|
|
9766
9766
|
"id": "1004634",
|
|
9767
9767
|
"text": "Bongos now remembers, for each person, which how-to steps they have already seen, so the AI never shows the same tip twice, on any computer."
|
|
9768
9768
|
}
|
|
9769
|
+
],
|
|
9770
|
+
"1.21.129": [
|
|
9771
|
+
{
|
|
9772
|
+
"id": "1004635",
|
|
9773
|
+
"text": "When someone is new to Bongos, the AI now gives them the right short tip at the right moment (first time opening the project, first task claimed, first ship) and never repeats a tip once seen. Other AI tools get the same tips"
|
|
9774
|
+
},
|
|
9775
|
+
{
|
|
9776
|
+
"id": "1003815",
|
|
9777
|
+
"text": "The module store can now record which release channel each module version is on — tester first, then everyone — with room for more, like taking a version back. Nothing changes for users yet; the next tasks use it."
|
|
9778
|
+
}
|
|
9779
|
+
],
|
|
9780
|
+
"1.21.130": [
|
|
9781
|
+
{
|
|
9782
|
+
"id": "1004636",
|
|
9783
|
+
"text": "People can now say 'show me the how-to', 'what's next in the tutorial' or 'skip the tutorial' and the AI does it: shows the whole guide with what they have seen, gives the next tip, or turns the tips off. 'Start the tutorial o"
|
|
9784
|
+
}
|
|
9769
9785
|
]
|
|
9770
9786
|
}
|
|
@@ -79,6 +79,7 @@ ${ident}
|
|
|
79
79
|
- **\`npx bongos ship\`** runs the smoke + grade, then merges and deploys; **\`npx bongos release <id>\`** drops a claim with no credit. Ship or release before you sign off.
|
|
80
80
|
- **The project's ship mode says who starts a ship** — \`npx bongos ship-mode\` prints it. In **Auto** (the default) you run \`npx bongos ship\` yourself when the task is done; never hand the person commands to run. In **Manual** ask the person first, and ship only after they say yes.
|
|
81
81
|
- **Dependencies gate everything** — a task becomes claimable only once every task it depends on has shipped.
|
|
82
|
+
- **Walk new people through the how-to, once.** The first time the person meets one of these moments, run the matching command and pass its few lines on in plain words at the end of your reply: the first prompt of a session — \`npx bongos howto moment first_open\`; their first claim — \`npx bongos howto moment first_claim\`; finishing their first task — \`npx bongos howto moment first_ship\`. It prints nothing once a step is read, so it never repeats (\`npx bongos howto moments\` lists the rest). When they ask to see the how-to, for the next step, or to skip it, run \`npx bongos howto show\`, \`npx bongos howto next\` or \`npx bongos howto skip\`. Claude Code is handed these steps automatically.
|
|
82
83
|
- **Code you write or touch carries a doc-comment** — each module a header, each public function/class a line or two, saying *what* it does and *why* it exists (never how), in the words someone would search for. Agents find code by searching text, so meaning written into the source is what makes it findable.
|
|
83
84
|
|
|
84
85
|
## Where things live
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/howto.js — the in-session how-to guide from a terminal, for any AI tool (task 1004635)
|
|
3
|
+
//
|
|
4
|
+
// WHY. Claude Code gets the right how-to step injected by the Conductor hook at the
|
|
5
|
+
// moment it applies. Other AI tools (Codex, Cursor, Gemini…) have no such hook, so the
|
|
6
|
+
// instance CLAUDE.md / AGENTS.md tells them to run this at the same moments instead. It
|
|
7
|
+
// prints the person's unread steps for that moment — nothing at all once they are read —
|
|
8
|
+
// and marks them read in Bongos, so no tool shows the same step twice.
|
|
9
|
+
//
|
|
10
|
+
// It is also how a person reopens the guide in plain words (task 1004636): "show me the
|
|
11
|
+
// how-to" → `show` (every step with its read mark and the progress count), "what's next
|
|
12
|
+
// in the tutorial" → `next` (the first unread step, then marked read), "skip the
|
|
13
|
+
// tutorial" → `skip` (every step marked skipped, so no moment shows one again), and
|
|
14
|
+
// "start the tutorial over" → `reset`. The Conductor hook maps those words to these
|
|
15
|
+
// commands for Claude Code; the instance CLAUDE.md names them for other tools.
|
|
16
|
+
//
|
|
17
|
+
// Usage:
|
|
18
|
+
// node scripts/gds/howto.js show the whole guide with read/unread marks + progress
|
|
19
|
+
// node scripts/gds/howto.js next print + mark read the next unread step
|
|
20
|
+
// node scripts/gds/howto.js skip mark every step skipped (the guide goes quiet)
|
|
21
|
+
// node scripts/gds/howto.js reset forget every mark, so the guide starts over
|
|
22
|
+
// node scripts/gds/howto.js moment <moment-id> print + mark read the unread steps of a moment
|
|
23
|
+
// node scripts/gds/howto.js moments list the moment ids and when each applies
|
|
24
|
+
//
|
|
25
|
+
// Also `npx bongos howto …`.
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const { apiCall, requireSession, errCode, cliExit } = require('./cli-lib');
|
|
29
|
+
const guide = require('../../modules/onboarding/howto-guide');
|
|
30
|
+
const moments = require('../../modules/onboarding/howto-moments');
|
|
31
|
+
|
|
32
|
+
const USAGE = 'usage: node scripts/gds/howto.js show | next | skip | reset | moment <moment-id> | moments';
|
|
33
|
+
|
|
34
|
+
// The step ids this person has already read or skipped, from GET /me/howto's answer.
|
|
35
|
+
function readIds(state) {
|
|
36
|
+
return ((state && state.steps) || []).filter((s) => s.state !== 'unread').map((s) => s.id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MARK = { read: '[x]', skipped: '[-]', unread: '[ ]' };
|
|
40
|
+
|
|
41
|
+
// Pure: the whole guide as the person sees it, from GET /me/howto's answer.
|
|
42
|
+
function formatProgress(state) {
|
|
43
|
+
const byId = new Map(((state && state.steps) || []).map((s) => [s.id, s.state]));
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const m of guide.MOMENTS) {
|
|
46
|
+
out.push(`${m.title}`);
|
|
47
|
+
for (const s of guide.stepsForMoment(m.id)) out.push(` ${MARK[byId.get(s.id) || 'unread']} ${s.title}`);
|
|
48
|
+
}
|
|
49
|
+
const total = guide.STEPS.length;
|
|
50
|
+
const read = (state && state.read) || 0;
|
|
51
|
+
const skipped = (state && state.skipped) || 0;
|
|
52
|
+
const next = guide.nextUnread(readIds(state));
|
|
53
|
+
out.push('', `Progress: ${read} of ${total} read${skipped ? `, ${skipped} skipped` : ''}.`);
|
|
54
|
+
out.push(next ? `Next: ${next.title} — say "what's next in the tutorial".` : 'All done. Say "start the tutorial over" to see it again.');
|
|
55
|
+
return out.join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function readState() {
|
|
59
|
+
await requireSession();
|
|
60
|
+
const r = await apiCall('GET', '/api/bongos/me/howto');
|
|
61
|
+
if (!r.ok) console.error(`Could not read the how-to progress (${r.status}${errCode(r.data) ? `: ${errCode(r.data)}` : ''}).`);
|
|
62
|
+
return r.ok ? r.data : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function write(route, body, what) {
|
|
66
|
+
const w = await apiCall('POST', `/api/bongos/me/howto/${route}`, body);
|
|
67
|
+
if (!w.ok) console.error(`Could not ${what} (${w.status}${errCode(w.data) ? `: ${errCode(w.data)}` : ''}).`);
|
|
68
|
+
return w.ok;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function show() {
|
|
72
|
+
const state = await readState();
|
|
73
|
+
if (!state) return 1;
|
|
74
|
+
console.log(formatProgress(state));
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function next() {
|
|
79
|
+
const state = await readState();
|
|
80
|
+
if (!state) return 1;
|
|
81
|
+
const step = guide.nextUnread(readIds(state));
|
|
82
|
+
if (!step) { console.log('You have seen every step. Say "start the tutorial over" to see them again.'); return 0; }
|
|
83
|
+
console.log(moments.renderForPerson({ moment: guide.MOMENTS.find((m) => m.id === step.moment), steps: [step] }));
|
|
84
|
+
if (!(await write('read', { steps: [step.id] }, 'mark it read'))) return 1;
|
|
85
|
+
const left = guide.STEPS.length - readIds(state).length - 1;
|
|
86
|
+
console.log(left > 0 ? `(${left} more to go — say "what's next in the tutorial" for the next one.)` : '(That was the last one.)');
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function skip() {
|
|
91
|
+
await requireSession();
|
|
92
|
+
if (!(await write('read', { all: true, how: 'skipped' }, 'skip the guide'))) return 1;
|
|
93
|
+
console.log('Skipped. The how-to will not show any more tips. Say "show me the how-to" any time to look at it, or "start the tutorial over" to bring the tips back.');
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function reset() {
|
|
98
|
+
await requireSession();
|
|
99
|
+
if (!(await write('reset', {}, 'start the guide over'))) return 1;
|
|
100
|
+
console.log('Done. The how-to starts over: each tip will show again at its moment.');
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function showMoment(id) {
|
|
105
|
+
if (!guide.MOMENT_IDS.has(id)) {
|
|
106
|
+
console.error(`unknown moment "${id}" — one of: ${[...guide.MOMENT_IDS].join(', ')}`);
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
await requireSession();
|
|
110
|
+
const r = await apiCall('GET', '/api/bongos/me/howto');
|
|
111
|
+
if (!r.ok) {
|
|
112
|
+
console.error(`Could not read the how-to progress (${r.status}${errCode(r.data) ? `: ${errCode(r.data)}` : ''}).`);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
const steps = moments.unreadForMoment(id, readIds(r.data));
|
|
116
|
+
if (!steps.length) return 0; // all read: say nothing, so it never repeats
|
|
117
|
+
console.log(moments.renderForPerson({ moment: guide.MOMENTS.find((m) => m.id === id), steps }));
|
|
118
|
+
const w = await apiCall('POST', '/api/bongos/me/howto/read', { steps: steps.map((s) => s.id) });
|
|
119
|
+
if (!w.ok) console.error(`(could not mark these read: ${w.status}${errCode(w.data) ? ` ${errCode(w.data)}` : ''})`);
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function main(argv) {
|
|
124
|
+
const [cmd, arg] = argv;
|
|
125
|
+
if (!cmd || cmd === '--help' || cmd === '-h') { console.log(USAGE); return cmd ? 0 : 2; }
|
|
126
|
+
if (cmd === 'moments') {
|
|
127
|
+
for (const m of guide.MOMENTS) console.log(`${m.id.padEnd(12)} ${m.trigger}`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
if (cmd === 'moment' && arg) return showMoment(String(arg).trim());
|
|
131
|
+
if (cmd === 'show') return show();
|
|
132
|
+
if (cmd === 'next') return next();
|
|
133
|
+
if (cmd === 'skip') return skip();
|
|
134
|
+
if (cmd === 'reset') return reset();
|
|
135
|
+
console.error(USAGE);
|
|
136
|
+
return 2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (require.main === module) {
|
|
140
|
+
main(process.argv.slice(2))
|
|
141
|
+
.then((code) => cliExit(code))
|
|
142
|
+
.catch((err) => { console.error('fatal:', err && err.message ? err.message : err); cliExit(1); });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { main, readIds, formatProgress };
|
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.130'; // 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');
|
|
@@ -49,7 +49,7 @@ function mePayload({ discipline = 'engineer' } = {}) {
|
|
|
49
49
|
// Run the hook once in a throwaway HOME whose session points at `base`.
|
|
50
50
|
// `worktree` names a git repo created under the fake home for the hook to stand
|
|
51
51
|
// in — its basename is what binds a claim to "this" tree (task 1002992).
|
|
52
|
-
async function runHook(me, { worktree = null } = {}) {
|
|
52
|
+
async function runHook(me, { worktree = null, prompts = ['hello'] } = {}) {
|
|
53
53
|
const server = http.createServer((req, res) => {
|
|
54
54
|
if (req.url.startsWith('/api/bongos/me')) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(me)); return; }
|
|
55
55
|
res.writeHead(404); res.end('{}');
|
|
@@ -70,14 +70,22 @@ async function runHook(me, { worktree = null } = {}) {
|
|
|
70
70
|
fs.mkdirSync(cwd, { recursive: true });
|
|
71
71
|
execFileSync('git', ['init', '-q'], { cwd, stdio: 'ignore' });
|
|
72
72
|
}
|
|
73
|
+
// Several prompts run one after another in ONE session, sharing the hook's cache —
|
|
74
|
+
// what the per-session repeat filter sees (task 1004636).
|
|
75
|
+
const sessionId = 'e2e-' + Date.now();
|
|
73
76
|
try {
|
|
74
|
-
const
|
|
75
|
-
let
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
const outs = [];
|
|
78
|
+
let err = ''; let code = 0;
|
|
79
|
+
for (const prompt of prompts) {
|
|
80
|
+
const child = spawn(process.execPath, [HOOK], { env, cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
81
|
+
let out = '';
|
|
82
|
+
child.stdout.on('data', (d) => { out += d; });
|
|
83
|
+
child.stderr.on('data', (d) => { err += d; });
|
|
84
|
+
child.stdin.end(JSON.stringify({ session_id: sessionId, prompt, cwd }));
|
|
85
|
+
code = await new Promise((r) => child.on('close', r));
|
|
86
|
+
outs.push(out);
|
|
87
|
+
}
|
|
88
|
+
return { out: outs[outs.length - 1], outs, err, code };
|
|
81
89
|
} finally {
|
|
82
90
|
await new Promise((r) => server.close(r));
|
|
83
91
|
fs.rmSync(home, { recursive: true, force: true });
|
|
@@ -149,4 +157,10 @@ await test('a project with no laws sends no laws block', async () => {
|
|
|
149
157
|
assert.ok(r.out.includes(INTERACTION), 'and the rest of the hook still runs');
|
|
150
158
|
});
|
|
151
159
|
|
|
160
|
+
await test('asking for the next how-to step twice in one session reaches the AI both times (task 1004636)', async () => {
|
|
161
|
+
const r = await runHook(mePayload(), { prompts: ["what's next in the tutorial", "what's next in the tutorial"] });
|
|
162
|
+
assert.equal(r.outs.length, 2);
|
|
163
|
+
for (const [i, out] of r.outs.entries()) assert.ok(out.includes('npx bongos howto next'), `ask ${i + 1} was swallowed; stdout: ${JSON.stringify(out.slice(0, 200))}`);
|
|
164
|
+
});
|
|
165
|
+
|
|
152
166
|
summary();
|
package/tests/howto_guide.mjs
CHANGED
|
@@ -66,6 +66,18 @@ test('steps are short and in plain language (no jargon)', () => {
|
|
|
66
66
|
assert.deepEqual(hits, []);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
test('every `npx bongos <verb>` a step names is a real bongos verb', () => {
|
|
70
|
+
const { VERBS } = require('../bin/bongos.js');
|
|
71
|
+
let seen = 0;
|
|
72
|
+
for (const s of guide.STEPS) {
|
|
73
|
+
for (const m of s.try.matchAll(/npx bongos ([a-z][a-z-]*)/g)) {
|
|
74
|
+
seen++;
|
|
75
|
+
assert.ok(Object.prototype.hasOwnProperty.call(VERBS, m[1]), `${s.id}: "npx bongos ${m[1]}" is not a verb in bin/bongos.js`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
assert.ok(seen > 0, 'the guide names no bongos verbs — has the check stopped matching?');
|
|
79
|
+
});
|
|
80
|
+
|
|
69
81
|
test('every slash command a step names is a core skill an instance gets', async () => {
|
|
70
82
|
const fs = await import('node:fs');
|
|
71
83
|
const { isPublishable } = require('../scripts/gds/publish-manifest.js');
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// tests/howto_moments.mjs — the right unread how-to step at the right moment, never twice (task 1004635)
|
|
2
|
+
//
|
|
3
|
+
// The done-when: on a fresh project the first session is walked through the starting
|
|
4
|
+
// step, the first ship triggers the shipping walkthrough, and neither repeats once read.
|
|
5
|
+
// Held here at three layers: the pure picker (modules/onboarding/howto-moments.js), the
|
|
6
|
+
// Conductor advisory built from it (.claude/hooks/conductor.js howtoAdvisory), and the
|
|
7
|
+
// non-Claude path (the `npx bongos howto` verb + the instance CLAUDE.md lines naming it).
|
|
8
|
+
//
|
|
9
|
+
// Run: node tests/howto_moments.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 m = require('../modules/onboarding/howto-moments.js');
|
|
17
|
+
const { howtoAdvisory, evaluateTriggers } = require('../.claude/hooks/conductor.js');
|
|
18
|
+
const { VERBS } = require('../bin/bongos.js');
|
|
19
|
+
const { renderInstanceClaudeMd } = require('../scripts/gds/claude-md-template.js');
|
|
20
|
+
const { readIds, formatProgress } = require('../scripts/gds/howto.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.message}`); }
|
|
27
|
+
}
|
|
28
|
+
const ids = (pick) => (pick ? pick.steps.map((s) => s.id) : []);
|
|
29
|
+
|
|
30
|
+
console.log('the picker:');
|
|
31
|
+
|
|
32
|
+
test('a fresh project: the first prompt walks through what Bongos is and where to start', () => {
|
|
33
|
+
const pick = m.pickSteps({ firstPrompt: true, prompt: 'hello', claims: [], read: [] });
|
|
34
|
+
assert.equal(pick.moment.id, 'first_open');
|
|
35
|
+
assert.deepEqual(ids(pick), ['welcome', 'where_to_start']);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('once read, the starting step never comes back — the next session moves on', () => {
|
|
39
|
+
const pick = m.pickSteps({ firstPrompt: true, prompt: 'hello', claims: [], read: ['welcome', 'where_to_start'] });
|
|
40
|
+
assert.ok(!ids(pick).includes('where_to_start'));
|
|
41
|
+
assert.ok(!ids(pick).includes('welcome'));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('an ordinary later prompt with nothing claimed says nothing', () => {
|
|
45
|
+
assert.equal(m.pickSteps({ firstPrompt: false, prompt: 'rename the button', claims: [], read: [] }), null);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('the first ship triggers the shipping walkthrough, ahead of the claim steps', () => {
|
|
49
|
+
const pick = m.pickSteps({ prompt: 'ship it', claims: [{ task_id: 1 }], read: [] });
|
|
50
|
+
assert.equal(pick.moment.id, 'first_ship');
|
|
51
|
+
assert.deepEqual(ids(pick), ['what_ship_does', 'review_findings']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('the shipping walkthrough does not repeat once read', () => {
|
|
55
|
+
const read = ['what_ship_does', 'review_findings', 'claim_a_task', 'give_a_task_back', 'preview_before_ship'];
|
|
56
|
+
assert.equal(m.pickSteps({ prompt: 'ship it', claims: [{ task_id: 1 }], read }), null);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('holding a claim opens the first-claim steps', () => {
|
|
60
|
+
assert.equal(m.pickSteps({ prompt: 'go on', claims: [{ task_id: 1 }], read: [] }).moment.id, 'first_claim');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('after a ship (no claim, shipping walkthrough read) comes what happens next', () => {
|
|
64
|
+
const pick = m.pickSteps({ prompt: 'ok', claims: [], read: ['what_ship_does', 'review_findings'] });
|
|
65
|
+
assert.equal(pick.moment.id, 'after_ship');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('unknown claim state never guesses a claim moment', () => {
|
|
69
|
+
assert.equal(m.pickSteps({ prompt: 'ship it', claims: null, read: [] }), null);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('short: never more than two steps, all from one moment', () => {
|
|
73
|
+
for (const firstPrompt of [true, false]) {
|
|
74
|
+
for (const prompt of ['ship it', 'I am stuck', 'make a goal', 'invite my teammate', 'what can I work on?']) {
|
|
75
|
+
const pick = m.pickSteps({ firstPrompt, prompt, claims: [{ task_id: 1 }], read: [] });
|
|
76
|
+
if (!pick) continue;
|
|
77
|
+
assert.ok(pick.steps.length <= m.MAX_STEPS);
|
|
78
|
+
assert.ok(pick.steps.every((s) => s.moment === pick.moment.id));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('every moment the picker can open is a moment the guide defines', () => {
|
|
84
|
+
const opened = new Set();
|
|
85
|
+
for (const prompt of ['ship it', 'stuck', 'set up', 'goal', 'make a task', "what's next", 'invite', 'settings']) {
|
|
86
|
+
for (const claims of [[], [{}], null]) {
|
|
87
|
+
for (const id of m.openMoments({ firstPrompt: true, prompt, claims, read: [] })) opened.add(id);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const id of opened) assert.ok(guide.MOMENT_IDS.has(id), id);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('the AI is told to say it once, briefly, and that it is marked read', () => {
|
|
94
|
+
const text = m.renderForAi(m.pickSteps({ firstPrompt: true, claims: [], read: [] }));
|
|
95
|
+
assert.match(text, /Where to start/);
|
|
96
|
+
assert.match(text, /once/);
|
|
97
|
+
assert.match(text, /marked read/);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
console.log('\nasking for the guide itself (task 1004636):');
|
|
101
|
+
|
|
102
|
+
test('the three plain-word requests, and starting over, are recognised', () => {
|
|
103
|
+
const cases = {
|
|
104
|
+
'show me the how-to': 'show',
|
|
105
|
+
'Can I see my tutorial progress?': 'show',
|
|
106
|
+
"what's next in the tutorial": 'next',
|
|
107
|
+
'continue the tutorial': 'next',
|
|
108
|
+
'skip the tutorial': 'skip',
|
|
109
|
+
'please stop showing me the tutorial': 'skip',
|
|
110
|
+
'turn the tips off': 'skip',
|
|
111
|
+
'start the tutorial over': 'reset',
|
|
112
|
+
};
|
|
113
|
+
for (const [prompt, kind] of Object.entries(cases)) assert.equal(m.guideRequest(prompt), kind, prompt);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('ordinary prompts are not guide requests', () => {
|
|
117
|
+
for (const p of ['show me the login page', 'what is next on the roadmap', 'skip the failing test', 'ship it', 'show me the style guide', 'add tips for the header']) {
|
|
118
|
+
assert.equal(m.guideRequest(p), null, p);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('each request tells the AI the command that changes the stored state', () => {
|
|
123
|
+
for (const kind of ['show', 'next', 'skip', 'reset']) assert.match(m.renderRequestForAi(kind), new RegExp(`npx bongos howto ${kind}`));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('the full guide shows read, skipped and unread marks, the progress, and what is next', () => {
|
|
127
|
+
const out = formatProgress({ steps: [{ id: 'welcome', state: 'read' }, { id: 'the_hall', state: 'skipped' }], read: 1, skipped: 1 });
|
|
128
|
+
assert.match(out, /\[x\] What Bongos is/);
|
|
129
|
+
assert.match(out, /\[-\] The hall/);
|
|
130
|
+
assert.match(out, /\[ \] Where to start/);
|
|
131
|
+
assert.match(out, new RegExp(`Progress: 1 of ${guide.STEPS.length} read, 1 skipped`));
|
|
132
|
+
assert.match(out, /Next: Where to start/);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('with every step done, the guide says so and how to start over', () => {
|
|
136
|
+
const out = formatProgress({ steps: guide.STEPS.map((s) => ({ id: s.id, state: 'read' })), read: guide.STEPS.length });
|
|
137
|
+
assert.match(out, /All done/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log('\nthe Conductor:');
|
|
141
|
+
|
|
142
|
+
test('a guide request becomes the advisory even with no read state, and replaces the moment tip', () => {
|
|
143
|
+
const a = howtoAdvisory({ howto: { read: [] }, prompt: 'skip the tutorial', firstPrompt: true, activeClaims: [] });
|
|
144
|
+
assert.equal(a.id, 'howto-request:skip');
|
|
145
|
+
assert.equal(a.howtoSteps, undefined, 'a request marks nothing itself — the command does');
|
|
146
|
+
assert.equal(howtoAdvisory({ howto: null, prompt: 'show me the how-to' }).id, 'howto-request:show');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('no read state (offline, signed out, no onboarding module) → no how-to advisory', () => {
|
|
150
|
+
assert.equal(howtoAdvisory({ howto: null, firstPrompt: true, activeClaims: [] }), null);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('the advisory names the steps it shows, so main() can mark exactly those read', () => {
|
|
154
|
+
const a = howtoAdvisory({ howto: { read: [] }, firstPrompt: true, activeClaims: [] });
|
|
155
|
+
assert.deepEqual(a.howtoSteps, ['welcome', 'where_to_start']);
|
|
156
|
+
assert.equal(a.id, 'howto:welcome,where_to_start');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('evaluateTriggers carries the how-to advisory on a first prompt, and not once read', () => {
|
|
160
|
+
const first = evaluateTriggers({ prompt: 'hi', activeClaims: [], howto: { read: [] }, firstPrompt: true });
|
|
161
|
+
assert.ok(first.some((a) => a.howtoSteps));
|
|
162
|
+
const all = guide.STEPS.map((s) => s.id);
|
|
163
|
+
const later = evaluateTriggers({ prompt: 'ship it', activeClaims: [{ task_id: 1 }], howto: { read: all }, firstPrompt: true });
|
|
164
|
+
assert.ok(!later.some((a) => a.howtoSteps));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
console.log('\nother AI tools:');
|
|
168
|
+
|
|
169
|
+
test('`bongos howto` is a verb, run by scripts/gds/howto.js', () => {
|
|
170
|
+
assert.equal(VERBS.howto.script, 'howto.js');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('the instance CLAUDE.md names the three moment commands', () => {
|
|
174
|
+
const md = renderInstanceClaudeMd({});
|
|
175
|
+
for (const id of ['first_open', 'first_claim', 'first_ship']) assert.ok(md.includes(`npx bongos howto moment ${id}`), id);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('the CLI counts read and skipped steps as done', () => {
|
|
179
|
+
assert.deepEqual(readIds({ steps: [{ id: 'a', state: 'read' }, { id: 'b', state: 'skipped' }, { id: 'c', state: 'unread' }] }), ['a', 'b']);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
183
|
+
if (failed) process.exitCode = 1;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// tests/module_release_channels.mjs — the release-channel schema a module version
|
|
2
|
+
// sits on (task 1003815, ADR 0344; migration core_276). No database: the migration
|
|
3
|
+
// is read as text and its shape pinned. The integration lane applies it to a real
|
|
4
|
+
// Postgres with every other migration.
|
|
5
|
+
//
|
|
6
|
+
// Run: node tests/module_release_channels.mjs
|
|
7
|
+
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import { strict as assert } from 'node:assert';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
const SQL = fs.readFileSync(path.join(ROOT, 'migrations', 'core_276_module_release_channels.sql'), 'utf8');
|
|
19
|
+
const DDL = SQL.replace(/--.*$/gm, '');
|
|
20
|
+
|
|
21
|
+
test('core_276 is additive and idempotent', () => {
|
|
22
|
+
// the project's own migration linter, so this tracks its unsafe-pattern list as it grows
|
|
23
|
+
const lint = require('../scripts/gds/lint-migrations.js');
|
|
24
|
+
assert.deepEqual(lint.scanMigrationSql(SQL).unsafe || [], [], 'additive only');
|
|
25
|
+
for (const m of DDL.matchAll(/CREATE (TABLE|INDEX)\s+(?!IF NOT EXISTS\b)(\w+)/g)) {
|
|
26
|
+
assert.fail(`CREATE ${m[1]} ${m[2]} without IF NOT EXISTS is not safe to re-run`);
|
|
27
|
+
}
|
|
28
|
+
assert.match(DDL, /ON CONFLICT \(channel\) DO NOTHING/, 're-running does not duplicate the channel rows');
|
|
29
|
+
assert.match(DDL, /WHERE NOT EXISTS \(SELECT 1 FROM store_module_version_channels c WHERE c\.version_id = v\.id\)/,
|
|
30
|
+
're-running does not add a second backfill row');
|
|
31
|
+
assert.match(SQL, /INSERT INTO schema_migrations \(version\) VALUES \('core_276_module_release_channels'\)/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('channels are rows, so a fourth needs no schema change; tester, general and withdrawn ship', () => {
|
|
35
|
+
assert.match(DDL, /CREATE TABLE IF NOT EXISTS store_release_channels \(\s*channel\s+text PRIMARY KEY/);
|
|
36
|
+
const seeded = [...DDL.matchAll(/\('(\w+)',\s+'[^']*',\s+(true|false),\s+\d+\)/g)].map((m) => [m[1], m[2]]);
|
|
37
|
+
assert.deepEqual(seeded, [['tester', 'true'], ['general', 'true'], ['withdrawn', 'false']],
|
|
38
|
+
'two channels serve installs; withdrawn never does');
|
|
39
|
+
assert.doesNotMatch(DDL, /channel\s+text[^,\n]*CHECK \(channel IN/, 'no closed list of channels in a CHECK');
|
|
40
|
+
assert.doesNotMatch(DDL, /\bis_tester\b|\btester\s+boolean/, 'not a boolean');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('a channel belongs to a version, as history: the newest row is current', () => {
|
|
44
|
+
const t = DDL.match(/CREATE TABLE IF NOT EXISTS store_module_version_channels \(([\s\S]*?)\n\);/);
|
|
45
|
+
assert.ok(t, 'the history table exists');
|
|
46
|
+
const body = t[1];
|
|
47
|
+
assert.match(body, /version_id\s+bigint NOT NULL REFERENCES store_module_versions\(id\)/, 'keyed by version, not module');
|
|
48
|
+
assert.doesNotMatch(body, /\bmodule_key\b/, 'never by module');
|
|
49
|
+
assert.match(body, /channel\s+text NOT NULL REFERENCES store_release_channels\(channel\)/);
|
|
50
|
+
assert.match(body, /reason\s+text NOT NULL CHECK \(reason ~ '\^\[a-z\]\[a-z0-9_\]\{0,39\}\$'\)/, 'a short code, never free text');
|
|
51
|
+
assert.match(body, /set_by\s+bigint REFERENCES builders\(id\),/, 'nullable: the system can move a version');
|
|
52
|
+
assert.doesNotMatch(body, /UNIQUE \(version_id\)/, 'a version can move more than once');
|
|
53
|
+
assert.match(DDL, /ON store_module_version_channels \(version_id, set_at DESC, id DESC\)/, 'current-channel lookup is indexed');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('versions published before staged rollout stay offered to everyone', () => {
|
|
57
|
+
assert.match(DDL, /SELECT v\.id, 'general', 'predates_staged_rollout'\s+FROM store_module_versions v/);
|
|
58
|
+
});
|