@bongos/core 1.21.102 → 1.21.103
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 +27 -22
- package/.claude/skills/planning-session/SKILL.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/recipes/core-release-pipeline.md +14 -7
- package/modules/lifecycle/merge-reach.js +56 -0
- package/modules/lifecycle/ship-card.js +14 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +14 -0
- package/scripts/gds/deploy-divergence.js +92 -26
- package/scripts/gds/provision-repo.js +7 -2
- package/scripts/gds/ship-finish.js +17 -8
- package/src/module-api.js +1 -1
- package/tests/core_upgrade_runner.mjs +8 -0
- package/tests/deploy_divergence_line.mjs +85 -6
- package/tests/provision.mjs +15 -6
- package/tests/ship_card.mjs +18 -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.103",
|
|
6
|
+
"core_contract": "1.21.103",
|
|
7
|
+
"source_commit": "56f5f9df610b5d9079d28702989bf24a9a462db9",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-09T13:
|
|
9
|
+
"built_at": "2026-10-09T13:27:17.718Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 592,
|
|
13
13
|
"agent_docs_stubbed": 28,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 3020,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3641,
|
|
20
|
+
"tree_sha256": "3e52cfca3b8385e564d956ceaae717fbedb093d4db368a3c960d3f6db8c6139a",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
{
|
|
208
208
|
"path": ".claude/skills/planning-session/SKILL.md",
|
|
209
209
|
"mode": "0000644",
|
|
210
|
-
"sha256": "
|
|
210
|
+
"sha256": "0447f0526aaf9735f47cb52fe7423914f8aba4cfbeb0f9b56fa49e0d4e5073fd"
|
|
211
211
|
},
|
|
212
212
|
{
|
|
213
213
|
"path": ".claude/skills/priority-session/SKILL.md",
|
|
@@ -2872,7 +2872,7 @@
|
|
|
2872
2872
|
{
|
|
2873
2873
|
"path": "docs/module-api-changelog.md",
|
|
2874
2874
|
"mode": "0000644",
|
|
2875
|
-
"sha256": "
|
|
2875
|
+
"sha256": "53e81515be4483fdb9a4d361335cbe9bac0efb82bd28946cf6b70d0ca28b0b3e"
|
|
2876
2876
|
},
|
|
2877
2877
|
{
|
|
2878
2878
|
"path": "docs/modules-contract.md",
|
|
@@ -2997,7 +2997,7 @@
|
|
|
2997
2997
|
{
|
|
2998
2998
|
"path": "docs/recipes/core-release-pipeline.md",
|
|
2999
2999
|
"mode": "0000644",
|
|
3000
|
-
"sha256": "
|
|
3000
|
+
"sha256": "00d1879609d950f379acdf411e8c91acc6da0b08838c49a338257433af24ceb8"
|
|
3001
3001
|
},
|
|
3002
3002
|
{
|
|
3003
3003
|
"path": "docs/recipes/failed-ship-recovery-proof.md",
|
|
@@ -7109,6 +7109,11 @@
|
|
|
7109
7109
|
"mode": "0000644",
|
|
7110
7110
|
"sha256": "16667b27f462627277da092dd0cf8905cf7144f1a959fdaf3174a9fb41a91da4"
|
|
7111
7111
|
},
|
|
7112
|
+
{
|
|
7113
|
+
"path": "modules/lifecycle/merge-reach.js",
|
|
7114
|
+
"mode": "0000644",
|
|
7115
|
+
"sha256": "f800bcd7e9f78cd680a06fa3242e70deed97c3399a435e783bec7288a5708cb8"
|
|
7116
|
+
},
|
|
7112
7117
|
{
|
|
7113
7118
|
"path": "modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql",
|
|
7114
7119
|
"mode": "0000644",
|
|
@@ -7427,7 +7432,7 @@
|
|
|
7427
7432
|
{
|
|
7428
7433
|
"path": "modules/lifecycle/ship-card.js",
|
|
7429
7434
|
"mode": "0000644",
|
|
7430
|
-
"sha256": "
|
|
7435
|
+
"sha256": "db49d616bad9ae851bb86f3729252290827db46cd6a6f3c1f86c60c9b9d2691a"
|
|
7431
7436
|
},
|
|
7432
7437
|
{
|
|
7433
7438
|
"path": "modules/lifecycle/ship-files.js",
|
|
@@ -10132,12 +10137,12 @@
|
|
|
10132
10137
|
{
|
|
10133
10138
|
"path": "package-lock.json",
|
|
10134
10139
|
"mode": "0000644",
|
|
10135
|
-
"sha256": "
|
|
10140
|
+
"sha256": "bc23786ce9a171a96d049bc9568d8a73e513c0aa66783cedae0e51d4b16b6d93"
|
|
10136
10141
|
},
|
|
10137
10142
|
{
|
|
10138
10143
|
"path": "package.json",
|
|
10139
10144
|
"mode": "0000644",
|
|
10140
|
-
"sha256": "
|
|
10145
|
+
"sha256": "923e849fd1ededbb5563d4c81dc9013c308a99a53f9d8595724fa8b35e831914"
|
|
10141
10146
|
},
|
|
10142
10147
|
{
|
|
10143
10148
|
"path": "public-docs/index.html",
|
|
@@ -10157,7 +10162,7 @@
|
|
|
10157
10162
|
{
|
|
10158
10163
|
"path": "release-notes.json",
|
|
10159
10164
|
"mode": "0000644",
|
|
10160
|
-
"sha256": "
|
|
10165
|
+
"sha256": "5f152e713ca6f702119fe887e076a370e72ad380cd3c880ef56683202f3b573f"
|
|
10161
10166
|
},
|
|
10162
10167
|
{
|
|
10163
10168
|
"path": "scripts/cron/style_guide_audit.sh",
|
|
@@ -10562,7 +10567,7 @@
|
|
|
10562
10567
|
{
|
|
10563
10568
|
"path": "scripts/gds/deploy-divergence.js",
|
|
10564
10569
|
"mode": "0000644",
|
|
10565
|
-
"sha256": "
|
|
10570
|
+
"sha256": "0fd4f72d74f1f39ebbd6267f485a24705a0c20706a2475507c895e726b6f2287"
|
|
10566
10571
|
},
|
|
10567
10572
|
{
|
|
10568
10573
|
"path": "scripts/gds/deploy-main-audit-gate.sh",
|
|
@@ -11212,7 +11217,7 @@
|
|
|
11212
11217
|
{
|
|
11213
11218
|
"path": "scripts/gds/provision-repo.js",
|
|
11214
11219
|
"mode": "0000644",
|
|
11215
|
-
"sha256": "
|
|
11220
|
+
"sha256": "a52e17d4341c7498d9fc204bfaa7447305a19924e8e05f0fde10583eb1f0a6da"
|
|
11216
11221
|
},
|
|
11217
11222
|
{
|
|
11218
11223
|
"path": "scripts/gds/provision-teardown.js",
|
|
@@ -11632,7 +11637,7 @@
|
|
|
11632
11637
|
{
|
|
11633
11638
|
"path": "scripts/gds/ship-finish.js",
|
|
11634
11639
|
"mode": "0000644",
|
|
11635
|
-
"sha256": "
|
|
11640
|
+
"sha256": "03bd3bacc00dfb644f25e22bf90a2864700cb570dcdf051e6c9fb9ca3edfb11a"
|
|
11636
11641
|
},
|
|
11637
11642
|
{
|
|
11638
11643
|
"path": "scripts/gds/ship-flow.js",
|
|
@@ -12432,7 +12437,7 @@
|
|
|
12432
12437
|
{
|
|
12433
12438
|
"path": "src/module-api.js",
|
|
12434
12439
|
"mode": "0000644",
|
|
12435
|
-
"sha256": "
|
|
12440
|
+
"sha256": "0228b28afc1fe4d1cd2a3e59ceeb9411a5ac0477bbdf95baf8a8de2100c2863a"
|
|
12436
12441
|
},
|
|
12437
12442
|
{
|
|
12438
12443
|
"path": "src/module-loader/catalog.js",
|
|
@@ -13437,7 +13442,7 @@
|
|
|
13437
13442
|
{
|
|
13438
13443
|
"path": "tests/core_upgrade_runner.mjs",
|
|
13439
13444
|
"mode": "0000644",
|
|
13440
|
-
"sha256": "
|
|
13445
|
+
"sha256": "33507dcd145bca157be08b9c8b55ad7d2c3ebae07ab8c94af7af9e235ac60769"
|
|
13441
13446
|
},
|
|
13442
13447
|
{
|
|
13443
13448
|
"path": "tests/cosmos_token_contrast.mjs",
|
|
@@ -13647,7 +13652,7 @@
|
|
|
13647
13652
|
{
|
|
13648
13653
|
"path": "tests/deploy_divergence_line.mjs",
|
|
13649
13654
|
"mode": "0000644",
|
|
13650
|
-
"sha256": "
|
|
13655
|
+
"sha256": "0eb792d18f705ba4990936822195d931802bad7f49b951633173446d5b7f2cba"
|
|
13651
13656
|
},
|
|
13652
13657
|
{
|
|
13653
13658
|
"path": "tests/deploy_instance.mjs",
|
|
@@ -16517,7 +16522,7 @@
|
|
|
16517
16522
|
{
|
|
16518
16523
|
"path": "tests/provision.mjs",
|
|
16519
16524
|
"mode": "0000644",
|
|
16520
|
-
"sha256": "
|
|
16525
|
+
"sha256": "71ea3289a3007e08249cfb309272208c6ec740d7345d8fbf9638e81b6ece5010"
|
|
16521
16526
|
},
|
|
16522
16527
|
{
|
|
16523
16528
|
"path": "tests/provision_account_identity.mjs",
|
|
@@ -17337,7 +17342,7 @@
|
|
|
17337
17342
|
{
|
|
17338
17343
|
"path": "tests/ship_card.mjs",
|
|
17339
17344
|
"mode": "0000644",
|
|
17340
|
-
"sha256": "
|
|
17345
|
+
"sha256": "e79fdf44d85523853ebbcefe8fcdae1806f258e9c546d8f086a46a806016f085"
|
|
17341
17346
|
},
|
|
17342
17347
|
{
|
|
17343
17348
|
"path": "tests/ship_card_injection.mjs",
|
|
@@ -151,7 +151,7 @@ For the version being planned, **group the agreed criteria into goals**. For eac
|
|
|
151
151
|
- **`scope_modules`** — the module **keys** (NOT file paths) whose files this goal's members may touch. **This is the wall, and it is set here and only here — never self-granted.** Keep it tight: the modules the goal's work actually needs. Widening later goes through an explicit request a trusted rank approves (`POST /goals/:id/scope`; Archon for a `protected` module) — BV1.R64.
|
|
152
152
|
> **The valid key list is the source of truth in [`src/bongos/module-scope-map.js`](../../../src/bongos/module-scope-map.js) — read it, don't trust this list.** It exports `moduleKeys()` (all keys), `protectedModules()` / `isProtectedModule(key)` (which are PROTECTED), and `scopeForModule(key)` (a key's globs + `protected` flag). Each key maps to path-globs; `protected` is **derived** (the key's globs overlap the trust/ship-pipeline/migration/authz core), and a goal scoped to any protected key is **Archon-only** to create. The current roster (run `node -e "const m=require('./src/bongos/module-scope-map');console.log(m.moduleKeys().map(k=>k+(m.isProtectedModule(k)?' 🔒':'')).join('\n'))"` to print it live):
|
|
153
153
|
> - **No roster is written down here on purpose.** The previous snapshot went stale within a release (it still omitted a key that had been added), which is exactly the failure a "read it live" rule prevents. Run the one-liner above; `protected` is derived from glob overlap, so it changes when the globs change.
|
|
154
|
-
> - **That file is the SOURCE. The RUNNING SERVER is the GATE.** `POST /goals` validates `scope_modules` against the roster the LIVE server loaded — not the one in your checkout — so a key added since the last deploy is valid here and refused on the wire.
|
|
154
|
+
> - **That file is the SOURCE. The RUNNING SERVER is the GATE.** `POST /goals` validates `scope_modules` against the roster the LIVE server loaded — not the one in your checkout — so a key added since the last deploy is valid here and refused on the wire. The site picks up a merge only on its next redeploy ([who sees a merge](../../../docs/recipes/core-release-pipeline.md#who-sees-a-merge-and-when)), and this is not a rare race: the module roster grows precisely during the versions when planning sessions run. On 2026-08-16 goal 1000069 was created with `government` in scope and came back `unknown: ["government"]` — the key was on main; the site was a release behind. **The SessionStart deploy-state line already tells you whether you are exposed.** If it says the site is behind main, treat any recently-added key as suspect before you compose the goal, not after.
|
|
155
155
|
> - **The refusal is not a dead end — it hands you the answer.** `unknown_module` comes back with `valid: [...]`: the LIVE roster, from the server that just refused you. One rejected call tells you exactly what will be accepted, so read it and re-point rather than trying keys one at a time. (`tests/goal_unknown_module_hint.mjs` pins that the refusal keeps carrying it, because this instruction is only true while it does.)
|
|
156
156
|
> - **Prefer a deployed alias where one exists, and write the re-point down.** Goal 1000069 was walled to the deprecated `governance` alias, which ADR 0174 deliberately kept carrying BOTH sets of globs through the transition — so the wall was identical, not weaker — and the goal description recorded that `scope_modules` should be re-pointed once the newer release went live. A goal walled to an alias with no note is a goal nobody re-points.
|
|
157
157
|
- **No covering key? → file a module-map prep task FIRST.** Every `scope_modules` entry must be a real key from `moduleKeys()`. If a goal's planned work touches file paths that **no existing key covers** (e.g. a brand-new module dir, or code still fused under `src/bongos/` that isn't yet broken out as its own key), the scope wall cannot place that work. Do **not** invent a key. Instead, **add a prep task to this version** — `kind='refactor'`, scoped to `kernel` (the module that owns `module-scope-map.js`) — to extend `MODULE_GLOBS` so a key covers those paths, and make the dependent goal's tasks depend on it. Adding/editing a key is itself a `kernel`-scoped (Archon-gated) change, so flag it for Archon sign-off in this session.
|
|
@@ -3041,5 +3041,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3041
3041
|
landed since 1.21.100 with no explicit bump. run 37933684016. (task 1002620)
|
|
3042
3042
|
1.21.102 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3043
3043
|
landed since 1.21.101 with no explicit bump. run 37935415352. (task 1002620)
|
|
3044
|
+
1.21.103 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3045
|
+
landed since 1.21.102 with no explicit bump. run 37936846665. (task 1002620)
|
|
3044
3046
|
---------------------------------------------------------------------------
|
|
3045
3047
|
```
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# Recipe — the core release pipeline: how a change to Cloud Bongos reaches a live instance
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
## Who sees a merge, and when
|
|
4
|
+
|
|
5
|
+
| Who | Gets a merged change | How to check |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| **cloudbongos.com** — the projects hub and the builders hall | on **every merge**, within minutes: it redeploys from `main` | its `/version`; the ship card's **Deploy** row reads it: *"live at `<sha>` (includes this merge \| not yet)"* |
|
|
8
|
+
| **Projects hosted on the platform** | only when a **published core release** carries it (auto-published per merge, [ADR 0161](../adr/0161-publish-on-merge.md)) **and that project's pin moves** — a patch-channel subscription or a core-upgrade | that project's `/version` `coreVersion`; the ship card's **Projects** row states the rule |
|
|
9
|
+
|
|
10
|
+
So a shipped task is live on the hub minutes after its merge, and on a hosted project only once that project's pin has moved — say which one you mean. The wording has one source, [`modules/lifecycle/merge-reach.js`](../../modules/lifecycle/merge-reach.js); the ship card, `deploy-divergence.js` and the SessionStart line all print it (task 1004682). `/recall who sees a merge` finds the same rule as learning 1000291.
|
|
11
|
+
|
|
12
|
+
> **The map.** Three sibling recipes each cover one leg — [packaging-the-core](packaging-the-core.md) (build the artifact), [private-npm-distribution](private-npm-distribution.md) (publish it), [upgrading-the-core](upgrading-the-core.md) (consume it). This file is the thing that connects them.
|
|
6
13
|
>
|
|
7
|
-
>
|
|
14
|
+
> **History, not the current rule.** Under [ADR 0137](../adr/0137-upstream-publish-policy-manual-owner-gated.md) (manual, owner-gated publish) a merge reached no instance at all, and this file opened with "Merging to `main` ships nothing." [ADR 0161](../adr/0161-publish-on-merge.md) superseded that. Sessions that kept repeating the old line told the owner that live work was not live. The gates below still exist; for patches they now run unattended.
|
|
8
15
|
|
|
9
16
|
> ## Working on a hosted project? Stop here — nothing in this file is yours to run.
|
|
10
17
|
>
|
|
@@ -70,7 +77,7 @@ The ordinary claim→ship loop: `/builder-claim N` → build → `/builder-ship`
|
|
|
70
77
|
|
|
71
78
|
Landing is CI-gated (tasks 1002610/1002611/1002619, ADR 0159): every PR runs the **`unit`** check — the DB-free suite plus the fitness gates, via `npm test` — and branch protection **requires** it, so a red suite blocks the merge. PRs touching the hard-floor safety surfaces (`CODEOWNERS` (instance-side)) additionally need the owner's review. The owner kill-switch is `node scripts/gds/gating.js on|off|status` — reversible by construction, snapshot-restored.
|
|
72
79
|
|
|
73
|
-
**Stop here and
|
|
80
|
+
**Stop here and the hub has it within minutes; no hosted project does.** This is the gate that feels like the finish line for every project, and is one only for the hub (see [Who sees a merge](#who-sees-a-merge-and-when)).
|
|
74
81
|
|
|
75
82
|
## Gate 2 — cut a release
|
|
76
83
|
|
|
@@ -334,8 +341,8 @@ node scripts/gds/deploy-divergence.js --url https://<instance-host>
|
|
|
334
341
|
|
|
335
342
|
It reports the two gaps **separately**, because they have different fixes:
|
|
336
343
|
|
|
337
|
-
- **UNRELEASED** — commits on `main` past the last `CORE_VERSION` bump. *No
|
|
338
|
-
- **UNDEPLOYED** — the live instance's `coreVersion` is behind `main`'s. A release exists
|
|
344
|
+
- **UNRELEASED** — commits on `main` past the last `CORE_VERSION` bump. *No hosted project can take these at any pin* — no published version contains them yet. The publish lane (ADR 0161) normally carries a merge within minutes; a gap that stays means the lane is disarmed or red.
|
|
345
|
+
- **UNDEPLOYED** — the live instance's `coreVersion` is behind `main`'s. A release exists it has not picked up: on the hub, its redeploy is late or stuck; on a hosted project, its pin has not moved (gate 3).
|
|
339
346
|
|
|
340
347
|
Exit codes: `0` aligned, `1` diverged, `2` undetermined.
|
|
341
348
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// modules/lifecycle/merge-reach.js — who sees a merge, and when. The ONE source of
|
|
2
|
+
// the sentence (task 1004682).
|
|
3
|
+
//
|
|
4
|
+
// Two audiences, two rules:
|
|
5
|
+
// 1. The hub this checkout ships to (cloudbongos.com: the projects hub and the
|
|
6
|
+
// builders hall) redeploys from main on every merge, within minutes.
|
|
7
|
+
// 2. Projects hosted on the platform get a change only when a published core
|
|
8
|
+
// release carries it (auto-published per merge, ADR 0161) AND their pin moves
|
|
9
|
+
// (a patch-channel subscription or a core-upgrade).
|
|
10
|
+
//
|
|
11
|
+
// Before this, deploy-divergence.js printed "Merging to main deploys nothing" on
|
|
12
|
+
// every ship — true under ADR 0137, false since ADR 0161 — and sessions repeated
|
|
13
|
+
// it to the owner about work that was in fact live. The ship card, the divergence
|
|
14
|
+
// report and the SessionStart line all read their wording from here, so the three
|
|
15
|
+
// cannot drift apart again.
|
|
16
|
+
//
|
|
17
|
+
// PURE and dependency-free: callers pass an already-sanitised reading (host, short
|
|
18
|
+
// sha, inclusion), never a raw network response — some of this text is injected
|
|
19
|
+
// into an agent's SessionStart context.
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const HOSTED_LINE = 'Hosted projects: only when a published core release carries the change and their pin moves (patch-channel subscription or a core-upgrade).';
|
|
24
|
+
|
|
25
|
+
function hubRule(host) {
|
|
26
|
+
return `${host || 'The hub'} redeploys from main on every merge, within minutes`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// includes: true (the live build carries this merge), false (not yet), null (cannot tell).
|
|
30
|
+
function hubLine({ host = null, liveShort = null, includes = null } = {}) {
|
|
31
|
+
if (!liveShort) return `${hubRule(host)}.`;
|
|
32
|
+
const state = includes === true ? 'includes this merge'
|
|
33
|
+
: includes === false ? 'not yet'
|
|
34
|
+
: 'cannot tell yet whether it includes this merge';
|
|
35
|
+
return `${host || 'The hub'}: live at ${liveShort} (${state}) — it redeploys from main on every merge, within minutes.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Re-bound a reading that may have travelled (a land-watch breadcrumb file) before
|
|
39
|
+
// it is printed: host-shaped, hex, boolean — anything else is dropped. A reading with
|
|
40
|
+
// no usable host is null, so the caller keeps its own fallback.
|
|
41
|
+
function boundReading(r) {
|
|
42
|
+
const host = r && typeof r.host === 'string' && /^[A-Za-z0-9.:_-]{1,120}$/.test(r.host) ? r.host : null;
|
|
43
|
+
if (!host) return null;
|
|
44
|
+
return {
|
|
45
|
+
host,
|
|
46
|
+
liveShort: typeof r.liveShort === 'string' && /^[0-9a-f]{4,40}$/i.test(r.liveShort) ? r.liveShort : null,
|
|
47
|
+
includes: typeof r.includes === 'boolean' ? r.includes : null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The two-line wording, in order: [hub, hosted projects].
|
|
52
|
+
function whoSeesAMerge(reading) {
|
|
53
|
+
return [hubLine(reading || {}), HOSTED_LINE];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { HOSTED_LINE, hubRule, whoSeesAMerge, boundReading };
|
|
@@ -23,6 +23,7 @@ const fs = require('node:fs');
|
|
|
23
23
|
const os = require('node:os');
|
|
24
24
|
const path = require('node:path');
|
|
25
25
|
const { branding } = require('../../src/module-api');
|
|
26
|
+
const { whoSeesAMerge, boundReading } = require('./merge-reach.js');
|
|
26
27
|
|
|
27
28
|
// tone -> the widget's banner color family. 'neutral' has no semantic CSS var, so
|
|
28
29
|
// every tone resolves through this map (never `--color-background-${tone}`).
|
|
@@ -445,9 +446,20 @@ function buildCardModel(input = {}) {
|
|
|
445
446
|
// Deploy
|
|
446
447
|
let deployText;
|
|
447
448
|
let deployCommit = null;
|
|
449
|
+
let hostedText = null;
|
|
448
450
|
if (stage === 'shipped') {
|
|
449
451
|
deployCommit = input.deploy && input.deploy.commit ? String(input.deploy.commit) : null;
|
|
450
|
-
|
|
452
|
+
const reach = boundReading(input.deploy && input.deploy.reach);
|
|
453
|
+
if (reach) {
|
|
454
|
+
// task 1004682: who sees this merge, in the one shared wording
|
|
455
|
+
// (merge-reach.js) — the hub's live build as read, then the hosted rule.
|
|
456
|
+
// A bare "live · <sha>" read as "every user has it", which is only true
|
|
457
|
+
// of the hub. The reading can come back from a breadcrumb file, so
|
|
458
|
+
// boundReading re-bounds it (host-shaped, hex, boolean) before printing.
|
|
459
|
+
[deployText, hostedText] = whoSeesAMerge(reach);
|
|
460
|
+
} else {
|
|
461
|
+
deployText = deployCommit ? `live · ${deployCommit}` : 'live';
|
|
462
|
+
}
|
|
451
463
|
} else if (stage === 'confirmed') {
|
|
452
464
|
// task 1002541: this line used to be unconditional, driven by the STATUS
|
|
453
465
|
// alone — so a merge that bailed still reported "landing automatically".
|
|
@@ -471,6 +483,7 @@ function buildCardModel(input = {}) {
|
|
|
471
483
|
|
|
472
484
|
addRow('Value', value); addRow('Changed', changed); addRow('Grade', gradeText);
|
|
473
485
|
addRow('Credits', creditsText); addRow('Usage', usageText); addRow('Deploy', deployText);
|
|
486
|
+
addRow('Projects', hostedText);
|
|
474
487
|
// task 1003150: the session-boundary nudge. Not a row — it qualifies the
|
|
475
488
|
// Next-> action rather than reporting a fact about the task, so both renderers
|
|
476
489
|
// hang it off the action. Usually null.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.103",
|
|
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.103",
|
|
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.103",
|
|
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
|
@@ -9450,5 +9450,19 @@
|
|
|
9450
9450
|
"id": "1004588",
|
|
9451
9451
|
"text": "The guilds test now sets up everything it needs on a brand-new database, so it runs all the way through instead of stopping early. It also checks, without needing a database, that its setup keeps up with the tables the guild c"
|
|
9452
9452
|
}
|
|
9453
|
+
],
|
|
9454
|
+
"1.21.103": [
|
|
9455
|
+
{
|
|
9456
|
+
"id": "1004663",
|
|
9457
|
+
"text": "Two test suites that always failed on Windows machines now pass there, so a builder on Windows can trust a red result again. Two checks that had been passing without really checking anything now check properly."
|
|
9458
|
+
},
|
|
9459
|
+
{
|
|
9460
|
+
"id": "1004682",
|
|
9461
|
+
"text": "Ship reports now say plainly who sees a merge. The old line \"Merging to main deploys nothing\" is gone: it stopped being true when every merge began publishing automatically. A shipped card now reads the live site and says, for"
|
|
9462
|
+
},
|
|
9463
|
+
{
|
|
9464
|
+
"id": "1004684",
|
|
9465
|
+
"text": "New house rule, Slop Bongo: when an AI realises it was wrong about something, it now says 'Bongo Slop Detected, Filing Bug' in chat and files a bug against the doc or message that misled it, so the same mistake stops repeating"
|
|
9466
|
+
}
|
|
9453
9467
|
]
|
|
9454
9468
|
}
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Compare what a live instance is SERVING against what is on main.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
4
|
+
// WHO SEES A MERGE (task 1004682; the wording lives in modules/lifecycle/merge-reach.js):
|
|
5
|
+
// - The hub this checkout ships to (cloudbongos.com: the projects hub and the
|
|
6
|
+
// builders hall) redeploys from main on every merge, within minutes.
|
|
7
|
+
// - Projects hosted on the platform get a change only when a published core
|
|
8
|
+
// release carries it (auto-published per merge, ADR 0161) and their pin moves
|
|
9
|
+
// (patch-channel subscription or a core-upgrade).
|
|
10
|
+
// This script used to print "Merging to main deploys nothing" — the ADR 0137 rule
|
|
11
|
+
// (manual, owner-gated publish), false since ADR 0161 — and sessions repeated it
|
|
12
|
+
// to the owner about work that was live. It now READS the hub's /version and says
|
|
13
|
+
// whether the live build includes main's tip, instead of a blanket claim.
|
|
14
|
+
//
|
|
15
|
+
// Why it exists at all: between 2026-07-24 and 2026-08-03 a hundred and
|
|
16
|
+
// seventy-one commits — including a whole governance permission system — sat on
|
|
17
|
+
// main while cloudbongos.com served 1.18.17, every layer reported success, and
|
|
18
|
+
// nobody noticed for ten days (blocker 1000097 / task 1002521). This script is
|
|
19
|
+
// the comparison that was missing.
|
|
12
20
|
//
|
|
13
21
|
// Three independent gaps, all reported:
|
|
14
|
-
// 1. UNRELEASED — commits on main since the last CORE_VERSION bump. No
|
|
15
|
-
//
|
|
22
|
+
// 1. UNRELEASED — commits on main since the last CORE_VERSION bump. No hosted
|
|
23
|
+
// project can take these at any pin, because no published version carries them.
|
|
16
24
|
// 2. UNDEPLOYED — the live instance's coreVersion is behind the CORE_VERSION
|
|
17
|
-
// on main. A release exists
|
|
25
|
+
// on main. A release exists that it has not picked up: on the hub, its
|
|
26
|
+
// redeploy is late or stuck; on a hosted project, its pin has not moved.
|
|
18
27
|
// 3. SCHEMA BEHIND — live's /version reports schemaPending > 0: its database
|
|
19
28
|
// lacks migrations its running code ships (the 2026-07-04 502 outage
|
|
20
29
|
// shape; task 1002002). An instance too old to report it is not flagged.
|
|
@@ -35,6 +44,7 @@ const path = require('node:path');
|
|
|
35
44
|
const { execSync } = require('node:child_process');
|
|
36
45
|
|
|
37
46
|
const { API_BASE, arg, hasFlag } = require('./cli-lib');
|
|
47
|
+
const { whoSeesAMerge, hubRule, HOSTED_LINE } = require('../../modules/lifecycle/merge-reach.js');
|
|
38
48
|
|
|
39
49
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
40
50
|
const MODULE_API = path.join(REPO_ROOT, 'src', 'module-api.js');
|
|
@@ -99,6 +109,51 @@ function unreleasedSince(mainRef) {
|
|
|
99
109
|
};
|
|
100
110
|
}
|
|
101
111
|
|
|
112
|
+
// Does the live build include commit `sha`? true / false, or null when this
|
|
113
|
+
// checkout cannot tell (task 1004682). Two ways to know, tried in order:
|
|
114
|
+
// 1. The /version commit is one this repo has (a box that runs this checkout
|
|
115
|
+
// directly) — plain ancestry.
|
|
116
|
+
// 2. Otherwise the commit is the hub's OWN repo (it consumes the core as a
|
|
117
|
+
// published package — measured 2026-10-09: cloudbongos.com reports a commit
|
|
118
|
+
// core main does not contain, at the coreVersion main declares). Then: find the
|
|
119
|
+
// first CORE_VERSION bump on main at or after `sha` — the release that carries
|
|
120
|
+
// it — and compare the live coreVersion against it. No bump yet ⇒ not yet.
|
|
121
|
+
// Both inputs reach git, so both are hex-or-nothing and the ref is a fixed name.
|
|
122
|
+
function liveIncludes(live, sha, mainRef = resolveMainRef()) {
|
|
123
|
+
const want = typeof sha === 'string' && /^[0-9a-f]{7,40}$/i.test(sha) ? sha.toLowerCase() : null;
|
|
124
|
+
if (!live || !want || !/^(origin\/main|HEAD)$/.test(mainRef)) return null;
|
|
125
|
+
const liveCommit = typeof live.commit === 'string' && /^[0-9a-f]{40}$/i.test(live.commit) ? live.commit : null;
|
|
126
|
+
if (liveCommit && git(`cat-file -e ${liveCommit}`) !== null) {
|
|
127
|
+
return git(`merge-base --is-ancestor ${want} ${liveCommit}`) !== null;
|
|
128
|
+
}
|
|
129
|
+
if (!isPlausibleVersion(live.coreVersion)) return null;
|
|
130
|
+
if (git(`merge-base --is-ancestor ${want} ${mainRef}`) === null) return null; // not on main here yet
|
|
131
|
+
// `sha` itself may be the bump (a hand-cut release); otherwise the first bump after it.
|
|
132
|
+
const selfBump = git(`log -1 --format=%H -G"^const CORE_VERSION = " ${want} -- src/module-api.js`, '');
|
|
133
|
+
const after = git(`log --ancestry-path --reverse --format=%H -G"^const CORE_VERSION = " ${want}..${mainRef} -- src/module-api.js`, '');
|
|
134
|
+
const carrier = selfBump && selfBump.startsWith(want) ? selfBump : (after ? after.split('\n')[0].trim() : null);
|
|
135
|
+
if (!carrier) return false; // on main, but no release carries it yet
|
|
136
|
+
const carried = coreVersionAt(carrier);
|
|
137
|
+
return carried ? cmpSemver(live.coreVersion, carried) >= 0 : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The sanitised reading the merge-reach sentence is built from. `live` may be a
|
|
141
|
+
// raw /version body (ship-finish passes one), so it goes through the same filters
|
|
142
|
+
// fetchLive applies before anything is interpolated.
|
|
143
|
+
// Prefer origin/main — the released trunk — but fall back to HEAD in a checkout with
|
|
144
|
+
// no remote (a box, a tarball). Shared by check() and hubReading() so both agree.
|
|
145
|
+
function resolveMainRef() {
|
|
146
|
+
return git('rev-parse --verify --quiet origin/main') ? 'origin/main' : 'HEAD';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function hubReading({ target, live, sha, mainRef = resolveMainRef() } = {}) {
|
|
150
|
+
return {
|
|
151
|
+
host: safeHost(target || API_BASE),
|
|
152
|
+
liveShort: live ? safeCommitShort(live.commitShort) || safeCommitShort(typeof live.commit === 'string' ? live.commit.slice(0, 8) : null) : null,
|
|
153
|
+
includes: live ? liveIncludes(live, sha, mainRef) : null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
102
157
|
function cmpSemver(a, b) {
|
|
103
158
|
const pa = String(a).split('.').map(Number);
|
|
104
159
|
const pb = String(b).split('.').map(Number);
|
|
@@ -162,6 +217,8 @@ async function fetchLive(base, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
162
217
|
url,
|
|
163
218
|
coreVersion: String(body.coreVersion),
|
|
164
219
|
commitShort: safeCommitShort(body.commitShort),
|
|
220
|
+
// Full sha, hex-or-nothing: only ever handed to git (liveIncludes), never printed.
|
|
221
|
+
commit: typeof body.commit === 'string' && /^[0-9a-f]{40}$/i.test(body.commit) ? body.commit : null,
|
|
165
222
|
schemaPending: safeCount(body.schemaPending),
|
|
166
223
|
};
|
|
167
224
|
} catch (e) {
|
|
@@ -193,7 +250,9 @@ function subscriptionHealth(dir = REPO_ROOT) {
|
|
|
193
250
|
}
|
|
194
251
|
}
|
|
195
252
|
|
|
196
|
-
|
|
253
|
+
// withReach: compute the who-sees-a-merge reading (a few git calls). The SessionStart
|
|
254
|
+
// --line never prints it, so it passes false and pays nothing for it.
|
|
255
|
+
async function check({ base, timeoutMs = DEFAULT_TIMEOUT_MS, withReach = true } = {}) {
|
|
197
256
|
const target = base || arg('--url') || API_BASE;
|
|
198
257
|
const local = coreVersionAt(null);
|
|
199
258
|
if (!local) return { ok: false, undetermined: true, reason: 'could not read CORE_VERSION from src/module-api.js', target };
|
|
@@ -201,7 +260,7 @@ async function check({ base, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
|
201
260
|
// Prefer origin/main — the released trunk — but fall back to HEAD in a
|
|
202
261
|
// checkout with no remote (a box, a tarball) so the check still says
|
|
203
262
|
// something useful instead of bailing.
|
|
204
|
-
const mainRef =
|
|
263
|
+
const mainRef = resolveMainRef();
|
|
205
264
|
const onMain = coreVersionAt(mainRef) || local;
|
|
206
265
|
const unreleased = unreleasedSince(mainRef);
|
|
207
266
|
const live = await fetchLive(target, timeoutMs);
|
|
@@ -214,10 +273,12 @@ async function check({ base, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
|
214
273
|
const ahead = cmpSemver(live.coreVersion, onMain) > 0;
|
|
215
274
|
const hasUnreleased = !!(unreleased && unreleased.count > 0);
|
|
216
275
|
const schemaBehind = live.schemaPending > 0;
|
|
276
|
+
// "this merge", read from here, is main's tip: ship-finish runs this right after a land.
|
|
277
|
+
const reach = withReach ? hubReading({ target, live, sha: git(`rev-parse ${mainRef}`), mainRef }) : null;
|
|
217
278
|
|
|
218
279
|
return {
|
|
219
280
|
ok: !behind && !hasUnreleased && !schemaBehind,
|
|
220
|
-
target, local, onMain, mainRef, unreleased, live, behind, ahead, hasUnreleased, schemaBehind, subscriptions,
|
|
281
|
+
target, local, onMain, mainRef, unreleased, live, behind, ahead, hasUnreleased, schemaBehind, subscriptions, reach,
|
|
221
282
|
};
|
|
222
283
|
}
|
|
223
284
|
|
|
@@ -243,9 +304,16 @@ function report(r, { quiet = false } = {}) {
|
|
|
243
304
|
}
|
|
244
305
|
say('');
|
|
245
306
|
|
|
307
|
+
// Who sees a merge — the same two lines the ship card prints (merge-reach.js).
|
|
308
|
+
const reachBlock = () => {
|
|
309
|
+
say('');
|
|
310
|
+
for (const l of whoSeesAMerge(r.reach || { host: safeHost(r.target) })) say(` ${l}`);
|
|
311
|
+
say('');
|
|
312
|
+
};
|
|
313
|
+
|
|
246
314
|
if (r.ok) {
|
|
247
315
|
say(' ✓ aligned — live matches main, and main carries no unreleased commits.');
|
|
248
|
-
|
|
316
|
+
reachBlock();
|
|
249
317
|
return;
|
|
250
318
|
}
|
|
251
319
|
|
|
@@ -257,13 +325,13 @@ function report(r, { quiet = false } = {}) {
|
|
|
257
325
|
const merges = u.countWithMerges != null && u.countWithMerges !== u.count
|
|
258
326
|
? ` (${u.countWithMerges} counting merges)` : '';
|
|
259
327
|
say(` ⚠ UNRELEASED — ${u.count} work commit${u.count === 1 ? '' : 's'}${merges} on ${r.mainRef} since the last CORE_VERSION bump.`);
|
|
260
|
-
say(' No
|
|
261
|
-
say(
|
|
328
|
+
say(' No hosted project can take these at ANY pin: no published version contains them yet.');
|
|
329
|
+
say(' The publish lane (ADR 0161) normally carries a merge within minutes; a gap that stays means it is disarmed or red.');
|
|
262
330
|
}
|
|
263
331
|
if (r.behind) {
|
|
264
332
|
say(` ⚠ UNDEPLOYED — live is ${r.live.coreVersion}, main declares ${r.onMain}.`);
|
|
265
|
-
say(' A release exists that this instance has not
|
|
266
|
-
say('
|
|
333
|
+
say(' A release exists that this instance has not picked up. On the hub, its redeploy is late or stuck;');
|
|
334
|
+
say(' on a hosted project, its pin has not moved (see docs/recipes/upgrading-the-core.md).');
|
|
267
335
|
}
|
|
268
336
|
if (r.schemaBehind) {
|
|
269
337
|
const n = r.live.schemaPending;
|
|
@@ -281,10 +349,7 @@ function report(r, { quiet = false } = {}) {
|
|
|
281
349
|
say(` ${r.subscriptions.level === 'alarm' ? '⚠ AUTO-UPGRADE' : 'note: auto-upgrade'} — ${r.subscriptions.message}`);
|
|
282
350
|
if (r.behind) say(' So the gap above will NOT close by itself.');
|
|
283
351
|
}
|
|
284
|
-
|
|
285
|
-
say(' Merging to main deploys nothing by design. "shipped" means "merged" —');
|
|
286
|
-
say(' it does not mean users can see it.');
|
|
287
|
-
say('');
|
|
352
|
+
reachBlock();
|
|
288
353
|
}
|
|
289
354
|
|
|
290
355
|
// ONE line, for injecting into an agent's SessionStart context (task 1002713).
|
|
@@ -324,7 +389,8 @@ function line(r) {
|
|
|
324
389
|
if (!parts.length) return null;
|
|
325
390
|
|
|
326
391
|
const advice = [];
|
|
327
|
-
|
|
392
|
+
// Fixed vocabulary + the bounded host only (this line is injected context).
|
|
393
|
+
if (versionParts) advice.push(`${hubRule(host)}; ${HOSTED_LINE.charAt(0).toLowerCase()}${HOSTED_LINE.slice(1)} Say which one you mean before calling work live.`);
|
|
328
394
|
if (pending) advice.push('Run migrate against that instance DB (PGDATABASE set) before anything else.');
|
|
329
395
|
return `Deploy state (read from ${host}/version): ${parts.join('; ')}. ${advice.join(' ')}`;
|
|
330
396
|
}
|
|
@@ -350,7 +416,7 @@ async function main() {
|
|
|
350
416
|
// seconds apart asked this same question four times and all four timed out, which
|
|
351
417
|
// emits NO line — and silence from a truth-teller reads as "nothing to report".
|
|
352
418
|
const { cached } = require('./probe-cache.js');
|
|
353
|
-
const { value: one } = await cached('deploy-divergence-line', async () => line(await check({ timeoutMs: ms })));
|
|
419
|
+
const { value: one } = await cached('deploy-divergence-line', async () => line(await check({ timeoutMs: ms, withReach: false })));
|
|
354
420
|
if (one) console.log(one);
|
|
355
421
|
} catch { /* offline, no git, unreadable module-api — all silence */ }
|
|
356
422
|
process.exitCode = 0;
|
|
@@ -377,4 +443,4 @@ if (require.main === module) {
|
|
|
377
443
|
});
|
|
378
444
|
}
|
|
379
445
|
|
|
380
|
-
module.exports = { check, report, line, coreVersionAt, unreleasedSince, cmpSemver, subscriptionHealth };
|
|
446
|
+
module.exports = { check, report, line, coreVersionAt, unreleasedSince, cmpSemver, subscriptionHealth, liveIncludes, hubReading };
|
|
@@ -147,9 +147,14 @@ function standaloneRoot(inst, { base = CONFIG.standaloneBase } = {}) { return pa
|
|
|
147
147
|
// This is a string question, not a filesystem one: the vendored layout is the only
|
|
148
148
|
// way a core ends up under `node_modules/`, so the suffix IS the answer. Keeping it
|
|
149
149
|
// pure means the control-plane path is testable without a box.
|
|
150
|
+
// Either separator counts as a segment boundary (task 1004663): a suffix built from
|
|
151
|
+
// path.sep alone never matched a POSIX-shaped path on Windows, so the suite went red
|
|
152
|
+
// there while the Linux box (the only place this runs for real) stayed green.
|
|
150
153
|
function selfInstanceRoot(repoRoot = REPO_ROOT) {
|
|
151
|
-
const
|
|
152
|
-
|
|
154
|
+
const segs = CORE_PKG_DIR.split('/');
|
|
155
|
+
const tail = repoRoot.split(/[\\/]/).slice(-(segs.length + 1));
|
|
156
|
+
const isVendored = tail.length === segs.length + 1 && segs.every((s, i) => tail[i + 1] === s);
|
|
157
|
+
return isVendored ? repoRoot.slice(0, -(segs.join('/').length + 1)) : repoRoot;
|
|
153
158
|
}
|
|
154
159
|
// Sync the instance's own repo (host content + the core pin). Mirrors deploy-instance.sh
|
|
155
160
|
// step 1 — assumes the repo is already cloned at standaloneRoot (initial clone is a
|
|
@@ -16,7 +16,7 @@ const { syncMemoryOnShip } = require('./ship-memory-sync.js');
|
|
|
16
16
|
const { uploadSessionOnShip } = require('./ship-session-upload.js');
|
|
17
17
|
const { execFileSync } = require('node:child_process');
|
|
18
18
|
const { gitOk } = require('./ship-git.js');
|
|
19
|
-
const { fetchDeployedVersion } = require('./ship-deploy-target.js');
|
|
19
|
+
const { fetchDeployedVersion, resolveInstanceBase } = require('./ship-deploy-target.js');
|
|
20
20
|
const { landBailed, reconciledCardStatus } = require('./ship-land.js');
|
|
21
21
|
const { autoMerge, publishHeld } = require('./ship-merge.js');
|
|
22
22
|
const tweakHold = require('../../modules/lifecycle/page-tweak-hold.js');
|
|
@@ -282,9 +282,17 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage, land
|
|
|
282
282
|
|
|
283
283
|
// Deployed commit (shipped only): prefer prod /version, fall back to short HEAD.
|
|
284
284
|
let commit = null;
|
|
285
|
+
let reach = null;
|
|
285
286
|
if (postGradeStatus === 'shipped') {
|
|
286
287
|
const v = fetchDeployedVersion();
|
|
287
288
|
commit = (v && v.commit) ? String(v.commit).slice(0, 7) : (gitOk('git rev-parse --short HEAD') || null);
|
|
289
|
+
// task 1004682: say WHO sees this merge — the hub's live build (read, not
|
|
290
|
+
// assumed) and the hosted-projects rule — in the merge-reach wording.
|
|
291
|
+
try {
|
|
292
|
+
reach = require('./deploy-divergence').hubReading({
|
|
293
|
+
target: resolveInstanceBase(), live: v, sha: gitOk('git rev-parse HEAD'),
|
|
294
|
+
});
|
|
295
|
+
} catch (_) { /* the card falls back to the bare commit */ }
|
|
288
296
|
}
|
|
289
297
|
|
|
290
298
|
// Running drachmae total + the builder's card-richness level (task 1279's
|
|
@@ -342,7 +350,7 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage, land
|
|
|
342
350
|
grade: card.grade || null,
|
|
343
351
|
credits: { awarded: card.creditsAwarded, total, perTaskCredit: rewardPolicy(getServerReward()).perTaskCredit },
|
|
344
352
|
usage: { tokens, costUsd, newTokens, cachedTokens },
|
|
345
|
-
deploy: { commit },
|
|
353
|
+
deploy: { commit, reach },
|
|
346
354
|
// task 1002541: absent (the default) ⇒ the card behaves exactly as before,
|
|
347
355
|
// which keeps every other caller — resumeMerge, the claim card, and
|
|
348
356
|
// land-watch replaying breadcrumbs written by older versions — unchanged.
|
|
@@ -376,12 +384,13 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage, land
|
|
|
376
384
|
/* a summary card must never fail a ship */
|
|
377
385
|
}
|
|
378
386
|
|
|
379
|
-
// task 1002521: the card above says SHIPPED, which means "merged"
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
// ten days while every layer reported success.
|
|
384
|
-
// task is still shipped, and an offline builder
|
|
387
|
+
// task 1002521: the card above says SHIPPED, which means "merged". Its Deploy
|
|
388
|
+
// row says who sees that (task 1004682: the hub redeploys on every merge;
|
|
389
|
+
// hosted projects when a release carries it and their pin moves). This adds
|
|
390
|
+
// the full divergence report when live and main disagree — blocker 1000097:
|
|
391
|
+
// 171 commits sat unreleased for ten days while every layer reported success.
|
|
392
|
+
// Warn, never block — a shipped task is still shipped, and an offline builder
|
|
393
|
+
// must not be punished for it.
|
|
385
394
|
try {
|
|
386
395
|
const { check, report } = require('./deploy-divergence');
|
|
387
396
|
const r = await check();
|
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.103'; // 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');
|
|
@@ -657,6 +657,14 @@ await t('a path that merely CONTAINS the package name is not mistaken for one',
|
|
|
657
657
|
assert.equal(RP.selfInstanceRoot('/srv/node_modules/@bongos/core/sub'), '/srv/node_modules/@bongos/core/sub');
|
|
658
658
|
});
|
|
659
659
|
|
|
660
|
+
await t('either path separator is a segment boundary, on any host OS (task 1004663)', () => {
|
|
661
|
+
// The suffix used to be built from path.sep alone, so the POSIX-shaped fixture
|
|
662
|
+
// above failed on Windows. Pin the other shape too, so Linux CI covers it.
|
|
663
|
+
assert.equal(RP.selfInstanceRoot('C:\\srv\\inst\\node_modules\\@bongos\\core'), 'C:\\srv\\inst');
|
|
664
|
+
assert.equal(RP.selfInstanceRoot('C:\\srv\\node_modules\\@bongos\\core-tools'), 'C:\\srv\\node_modules\\@bongos\\core-tools');
|
|
665
|
+
assert.equal(RP.selfInstanceRoot('/srv/other_modules/@bongos/core'), '/srv/other_modules/@bongos/core');
|
|
666
|
+
});
|
|
667
|
+
|
|
660
668
|
await t('the runner reads the installed version from the INSTANCE, not from the core package', async () => {
|
|
661
669
|
// The defect this replaces: readInstalledCoreVersion was handed REPO_ROOT, which in
|
|
662
670
|
// the vendored shape holds no node_modules/@bongos/core at all — so it answered null
|
|
@@ -71,10 +71,16 @@ test('a behind instance names both versions and the host', () => {
|
|
|
71
71
|
assert.equal(s.split('\n').length, 1, 'must be ONE line — it is injected context');
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
test('the line
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
test('the line says who sees a merge, not a blanket "deploys nothing"', () => {
|
|
75
|
+
// task 1004682: the old advice ("Merging to main deploys nothing") was the ADR 0137
|
|
76
|
+
// rule, false since ADR 0161, and sessions repeated it to the owner about live work.
|
|
77
|
+
// The invariant now: name BOTH audiences — the hub redeploys on every merge, hosted
|
|
78
|
+
// projects only when a release carries the change and their pin moves.
|
|
79
|
+
const s = line(behind);
|
|
80
|
+
assert.doesNotMatch(s, /deploys nothing/);
|
|
81
|
+
assert.match(s, /cloudbongos[.]com redeploys from main on every merge/);
|
|
82
|
+
assert.match(s, /hosted projects: only when a published core release carries the change and their pin moves/);
|
|
83
|
+
assert.match(s, /Say which one you mean before calling work live/);
|
|
78
84
|
});
|
|
79
85
|
|
|
80
86
|
test('live AHEAD of main is reported, not treated as fine', () => {
|
|
@@ -273,14 +279,14 @@ test('a database missing migrations is named on the one line, with its own advic
|
|
|
273
279
|
const s = line(schemaOnly);
|
|
274
280
|
assert.match(s, /MISSING 3 migrations the running code ships/);
|
|
275
281
|
assert.match(s, /Run migrate against that instance DB/);
|
|
276
|
-
assert.doesNotMatch(s, /
|
|
282
|
+
assert.doesNotMatch(s, /redeploys from main/, 'the version advice is for version gaps only');
|
|
277
283
|
assert.equal(s.split('\n').length, 1);
|
|
278
284
|
assert.match(line({ ...schemaOnly, live: { ...schemaOnly.live, schemaPending: 1 } }), /MISSING 1 migration the/);
|
|
279
285
|
|
|
280
286
|
const both = line({ ...schemaOnly, behind: true, live: { version: '1.0.0', coreVersion: '1.19.56', schemaPending: 2 } });
|
|
281
287
|
assert.match(both, /1\.19\.56/);
|
|
282
288
|
assert.match(both, /MISSING 2 migrations/);
|
|
283
|
-
assert.match(both, /
|
|
289
|
+
assert.match(both, /redeploys from main on every merge/);
|
|
284
290
|
});
|
|
285
291
|
|
|
286
292
|
test('a non-integer schemaPending never reaches the line', () => {
|
|
@@ -316,3 +322,76 @@ test('check() reads schemaPending off /version, and only a bounded integer count
|
|
|
316
322
|
assert.equal(bad.live.schemaPending, null);
|
|
317
323
|
assert.equal(bad.schemaBehind, false);
|
|
318
324
|
});
|
|
325
|
+
|
|
326
|
+
// ---- who sees a merge (task 1004682) ----------------------------------------
|
|
327
|
+
// liveIncludes answers "does the live build carry this commit?" two ways: plain
|
|
328
|
+
// ancestry when the /version commit is one this repo has, else by the first
|
|
329
|
+
// CORE_VERSION bump at or after the commit (the hub runs its own repo and reports
|
|
330
|
+
// its own commit). Pinned against this repo's real history, on HEAD so a checkout
|
|
331
|
+
// without origin/main still runs it (CI checks out fetch-depth 0).
|
|
332
|
+
const { liveIncludes, hubReading, coreVersionAt } = require('../scripts/gds/deploy-divergence.js');
|
|
333
|
+
const gitOut = (args) => spawnSync('git', args, { cwd: ROOT, encoding: 'utf8' }).stdout.trim();
|
|
334
|
+
const bumps = gitOut(['log', 'HEAD', '-3', '--format=%H', '-G^const CORE_VERSION = ', '--', 'src/module-api.js']).split('\n').filter(Boolean);
|
|
335
|
+
|
|
336
|
+
test('liveIncludes reads inclusion by release when the live commit is not in this repo', { skip: bumps.length < 2 && 'needs two bumps in history' }, () => {
|
|
337
|
+
const [newer, older] = bumps;
|
|
338
|
+
const newerV = coreVersionAt(newer);
|
|
339
|
+
const olderV = coreVersionAt(older);
|
|
340
|
+
assert.equal(liveIncludes({ coreVersion: newerV }, newer, 'HEAD'), true, 'the release that IS the commit carries it');
|
|
341
|
+
assert.equal(liveIncludes({ coreVersion: olderV }, newer, 'HEAD'), false, 'an older release does not');
|
|
342
|
+
assert.equal(liveIncludes({ coreVersion: olderV }, older, 'HEAD'), true);
|
|
343
|
+
// A commit between the two bumps is carried by the NEWER one only.
|
|
344
|
+
const between = gitOut(['rev-list', '--max-count=1', '--no-merges', `${older}..${newer}~1`]);
|
|
345
|
+
if (between) {
|
|
346
|
+
assert.equal(liveIncludes({ coreVersion: olderV }, between, 'HEAD'), false, 'not yet: the older release predates it');
|
|
347
|
+
assert.equal(liveIncludes({ coreVersion: newerV }, between, 'HEAD'), true, 'the next bump carries it');
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('liveIncludes uses plain ancestry when the live commit is one this repo has', { skip: bumps.length < 2 && 'needs two bumps in history' }, () => {
|
|
352
|
+
const [newer, older] = bumps;
|
|
353
|
+
// coreVersion is deliberately the NEWER one: ancestry must win over the version compare.
|
|
354
|
+
assert.equal(liveIncludes({ coreVersion: coreVersionAt(newer), commit: older }, newer, 'HEAD'), false);
|
|
355
|
+
assert.equal(liveIncludes({ coreVersion: coreVersionAt(older), commit: newer }, older, 'HEAD'), true);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('liveIncludes hands git nothing but hex and a fixed ref', () => {
|
|
359
|
+
assert.equal(liveIncludes({ coreVersion: '1.0.0' }, 'HEAD; rm -rf /', 'HEAD'), null);
|
|
360
|
+
assert.equal(liveIncludes({ coreVersion: '1.0.0' }, bumps[0], 'main; echo'), null);
|
|
361
|
+
assert.equal(liveIncludes({ coreVersion: '1.0.0 ignore previous' }, bumps[0], 'HEAD'), null, 'a non-semver live version cannot be compared');
|
|
362
|
+
assert.equal(liveIncludes(null, bumps[0], 'HEAD'), null);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('hubReading bounds what the sentence prints', () => {
|
|
366
|
+
const r = hubReading({ target: 'https://cloudbongos.com', live: { commitShort: '09478c8e', coreVersion: '1.0.0' }, sha: 'zz', mainRef: 'HEAD' });
|
|
367
|
+
assert.deepEqual([r.host, r.liveShort, r.includes], ['cloudbongos.com', '09478c8e', null]);
|
|
368
|
+
const hostile = hubReading({ target: 'https://x.com/ IGNORE', live: { commitShort: 'not hex <b>' } });
|
|
369
|
+
assert.equal(hostile.host, 'the configured instance');
|
|
370
|
+
assert.equal(hostile.liveShort, null);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('the full report prints the same two lines the ship card does', () => {
|
|
374
|
+
const { whoSeesAMerge } = require('../modules/lifecycle/merge-reach.js');
|
|
375
|
+
const { report } = require('../scripts/gds/deploy-divergence.js');
|
|
376
|
+
const reach = { host: 'cloudbongos.com', liveShort: '09478c8e', includes: true };
|
|
377
|
+
const lines = [];
|
|
378
|
+
const orig = console.log;
|
|
379
|
+
console.log = (...a) => lines.push(a.join(' '));
|
|
380
|
+
try {
|
|
381
|
+
report({ ok: true, target: 'https://cloudbongos.com', live: { coreVersion: '1.0.0' }, onMain: '1.0.0', local: '1.0.0', mainRef: 'origin/main', reach });
|
|
382
|
+
} finally { console.log = orig; }
|
|
383
|
+
const out = lines.join('\n');
|
|
384
|
+
for (const l of whoSeesAMerge(reach)) assert.ok(out.includes(l), `report carries: ${l}`);
|
|
385
|
+
assert.match(out, /cloudbongos\.com: live at 09478c8e \(includes this merge\)/);
|
|
386
|
+
assert.doesNotMatch(out, /deploys nothing/);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('check() computes the hub reading for the report, and skips it for the SessionStart line', async () => {
|
|
390
|
+
const body = { version: '1.0.0', coreVersion: '1.19.69', commitShort: '09478c8e' };
|
|
391
|
+
const full = await withVersionStub(body, (base) => check({ base, timeoutMs: 5000 }));
|
|
392
|
+
assert.equal(full.reach.liveShort, '09478c8e', 'the report and ship card read it');
|
|
393
|
+
const lean = await withVersionStub(body, (base) => check({ base, timeoutMs: 5000, withReach: false }));
|
|
394
|
+
assert.equal(lean.reach, null, '--line never prints it, so it pays no git calls for it');
|
|
395
|
+
const src = fs.readFileSync(SCRIPT, 'utf8');
|
|
396
|
+
assert.match(src, /line\(await check\(\{ timeoutMs: ms, withReach: false \}\)\)/, 'the --line path opts out');
|
|
397
|
+
});
|
package/tests/provision.mjs
CHANGED
|
@@ -1357,23 +1357,26 @@ async function adoptWithPack(packText, mode = 'adopt') {
|
|
|
1357
1357
|
}
|
|
1358
1358
|
const U2 = require('../scripts/gds/provision-units.js');
|
|
1359
1359
|
|
|
1360
|
+
// The write path comes from path.join, so it carries `\` on Windows: every match below
|
|
1361
|
+
// takes either separator. Slash-only, the positive case failed there and the two
|
|
1362
|
+
// "left alone" cases passed vacuously (task 1004663).
|
|
1360
1363
|
await ta('scaffoldStandaloneRepo(adopt): an owner pack on another envPrefix is corrected IN the adopt commit (task 1004573)', async () => {
|
|
1361
1364
|
const owner = '{\n "identity": { "productName": "mercury" },\n "envPrefix": "CLOUDBONGOS",\n "configDir": "cloudbongos"\n}\n';
|
|
1362
1365
|
const { seq, writes } = await adoptWithPack(owner);
|
|
1363
|
-
const w = writes.find((x) => /config
|
|
1366
|
+
const w = writes.find((x) => /config[\\/]branding\.json$/.test(x.p));
|
|
1364
1367
|
assert.ok(w, 'the pack was rewritten');
|
|
1365
1368
|
assert.equal(JSON.parse(w.body).envPrefix, U2.provisionedEnvPrefix(), 'to the prefix web.env is written under');
|
|
1366
1369
|
assert.equal(w.body, owner.replace('"envPrefix": "CLOUDBONGOS"', `"envPrefix": "${U2.provisionedEnvPrefix()}"`), 'and NOTHING else in the owner\'s file moved');
|
|
1367
|
-
const wrote = seq.findIndex((s) => /^write .*config
|
|
1370
|
+
const wrote = seq.findIndex((s) => /^write .*config[\\/]branding\.json$/.test(s));
|
|
1368
1371
|
const added = seq.findIndex((s) => /git -C \S+ add -A/.test(s));
|
|
1369
1372
|
assert.ok(wrote >= 0 && added > wrote, 'written before `git add -A`, so it rides the adopt commit to the owner\'s repo');
|
|
1370
1373
|
});
|
|
1371
1374
|
|
|
1372
1375
|
await ta('scaffoldStandaloneRepo: a pack that already agrees, and a greenfield scaffold, are left alone (task 1004573)', async () => {
|
|
1373
1376
|
const agree = await adoptWithPack(JSON.stringify({ envPrefix: U2.provisionedEnvPrefix() }));
|
|
1374
|
-
assert.ok(!agree.writes.some((x) => /config
|
|
1377
|
+
assert.ok(!agree.writes.some((x) => /config[\\/]branding\.json$/.test(x.p)), 'agreeing pack untouched');
|
|
1375
1378
|
const green = await adoptWithPack(JSON.stringify({ envPrefix: 'CLOUDBONGOS' }), 'greenfield');
|
|
1376
|
-
assert.ok(!green.writes.some((x) => /config
|
|
1379
|
+
assert.ok(!green.writes.some((x) => /config[\\/]branding\.json$/.test(x.p)), 'greenfield owns its config through init; no rewrite here');
|
|
1377
1380
|
});
|
|
1378
1381
|
|
|
1379
1382
|
await ta('normalizePackEnvPrefix: rewrites only the value, refuses what it cannot rewrite safely (task 1004573)', async () => {
|
|
@@ -3499,10 +3502,16 @@ await ta('provisionInstance: the password IS written to web.env, which makeWrite
|
|
|
3499
3502
|
// Driven through the REAL cp.execSync against a real failing shell, because the thing
|
|
3500
3503
|
// being relied on is that Node populates e.stdout/e.stderr on a throw. A fake that
|
|
3501
3504
|
// simply returns what the fix expects would prove nothing about that.
|
|
3505
|
+
//
|
|
3506
|
+
// The failing child is `node -e`, not `sh -c '…'` (task 1004663): on Windows execSync's
|
|
3507
|
+
// shell is cmd.exe, which does not honour single quotes, so it read the inner `1>&2` as
|
|
3508
|
+
// ITS redirect and sent the whole child's stdout to stderr — the stdout case failed
|
|
3509
|
+
// there, and the secret case's `stdout === ''` passed for that wrong reason.
|
|
3510
|
+
const failingChild = (out, err) => `"${process.execPath}" -e "process.stdout.write('${out}\\n');process.stderr.write('${err}\\n');process.exit(1)"`;
|
|
3502
3511
|
t('makeExec(allowFail): a soft failure carries the command output back to the caller', () => {
|
|
3503
3512
|
const logs = [];
|
|
3504
3513
|
const run = P.makeExec({ apply: true, log: (m) => logs.push(m) });
|
|
3505
|
-
const r = run(
|
|
3514
|
+
const r = run(failingChild('✖ already on 1.19.927', 'sudo: unknown user bongos-test4'), { allowFail: true });
|
|
3506
3515
|
assert.equal(r.softFailed, true, 'still a soft failure');
|
|
3507
3516
|
assert.equal(r.ok, true, 'and still ok:true — the caller decides, not the runner');
|
|
3508
3517
|
assert.match(r.stdout, /already on 1\.19\.927/, 'stdout survives');
|
|
@@ -3515,7 +3524,7 @@ t('makeExec(allowFail): a SECRET-BEARING step still withholds its output', () =>
|
|
|
3515
3524
|
// around that control on the return value instead of the log.
|
|
3516
3525
|
const logs = [];
|
|
3517
3526
|
const run = P.makeExec({ apply: true, log: (m) => logs.push(m) });
|
|
3518
|
-
const r = run(
|
|
3527
|
+
const r = run(failingChild('token=abcd1234', 'token=abcd1234'), { allowFail: true, shown: 'npm publish (token redacted)' });
|
|
3519
3528
|
assert.equal(r.softFailed, true);
|
|
3520
3529
|
assert.equal(r.stdout, '', 'no output escapes a secret-bearing step');
|
|
3521
3530
|
assert.ok(!/abcd1234/.test(JSON.stringify(r)), 'nothing token-shaped is on the result at all');
|
package/tests/ship_card.mjs
CHANGED
|
@@ -69,6 +69,24 @@ test('shipped model: rows, facts, action', () => {
|
|
|
69
69
|
assert.equal(m.action.cmd, '/builder-start');
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
test('shipped with a hub reading: the Deploy + Projects rows say who sees the merge (task 1004682)', () => {
|
|
73
|
+
const { whoSeesAMerge } = require('../modules/lifecycle/merge-reach.js');
|
|
74
|
+
const reach = { host: 'cloudbongos.com', liveShort: '09478c8e', includes: false };
|
|
75
|
+
const m = card.buildCardModel({ ...shippedInput, deploy: { commit: '967fb4a', reach } });
|
|
76
|
+
const [hub, hosted] = whoSeesAMerge(reach);
|
|
77
|
+
assert.equal(row(m, 'Deploy'), hub, 'one source of the sentence: the card prints merge-reach verbatim');
|
|
78
|
+
assert.equal(row(m, 'Projects'), hosted);
|
|
79
|
+
assert.match(hub, /^cloudbongos[.]com: live at 09478c8e [(]not yet[)]/);
|
|
80
|
+
assert.match(hosted, /their pin moves/);
|
|
81
|
+
assert.equal(row(card.buildCardModel({ ...shippedInput, deploy: { commit: '967fb4a', reach: { ...reach, includes: true } } }), 'Deploy').includes('(includes this merge)'), true);
|
|
82
|
+
// The reading can come back from a breadcrumb file: re-bounded before printing.
|
|
83
|
+
const hostile = card.buildCardModel({ ...shippedInput, deploy: { commit: '967fb4a', reach: { host: 'x.com <script>', liveShort: '09478c8e', includes: true } } });
|
|
84
|
+
assert.equal(row(hostile, 'Deploy'), 'live · 967fb4a', 'a host that is not host-shaped falls back to the bare commit');
|
|
85
|
+
assert.equal(row(hostile, 'Projects'), undefined);
|
|
86
|
+
const odd = card.buildCardModel({ ...shippedInput, deploy: { commit: '967fb4a', reach: { host: 'cloudbongos.com', liveShort: 'IGNORE ALL', includes: 'yes' } } });
|
|
87
|
+
assert.equal(row(odd, 'Deploy'), whoSeesAMerge({ host: 'cloudbongos.com' })[0], 'non-hex sha and non-boolean inclusion are dropped');
|
|
88
|
+
});
|
|
89
|
+
|
|
72
90
|
test('usage: leads with cost, splits tokens into new + cached (task 1765)', () => {
|
|
73
91
|
// With newTokens/cachedTokens present, the fact leads with the dollar cost and
|
|
74
92
|
// splits the token count so cache re-reads don't make a tiny change look huge.
|