@bongos/core 1.19.2 → 1.19.4
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 +50 -20
- package/bin/bongos.js +33 -7
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +1 -1
- package/docs/adr/0161-publish-on-merge.md +34 -0
- package/docs/adr/README.md +1 -0
- package/docs/recipes/core-release-pipeline.md +6 -4
- package/docs/recipes/private-npm-distribution.md +4 -2
- package/migrations/core_205_versions_track_default.sql +28 -0
- package/modules/lifecycle/routes/tasks.js +83 -53
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/bump-version.js +76 -0
- package/scripts/gds/init.js +7 -0
- package/src/module-api.js +13 -1
- package/tests/bongos_cli.mjs +91 -0
- package/tests/bump_version.mjs +71 -0
- package/tests/init.mjs +19 -0
- package/tests/patch_gate_ordering.mjs +102 -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.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
8
|
-
"source_ref": "
|
|
9
|
-
"built_at": "2026-08-
|
|
5
|
+
"core_version": "1.19.4",
|
|
6
|
+
"core_contract": "1.19.4",
|
|
7
|
+
"source_commit": "9064a9274ab0a846896a2474abaea9b3c801db4f",
|
|
8
|
+
"source_ref": "origin/main",
|
|
9
|
+
"built_at": "2026-08-08T20:06:49.516Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 284,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1202,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1507,
|
|
20
|
+
"tree_sha256": "4857f4b5f49e21f2a49f5c23537e5e489dc073ef08769ed60c1a38c0678b9759",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -327,7 +327,7 @@
|
|
|
327
327
|
{
|
|
328
328
|
"path": "bin/bongos.js",
|
|
329
329
|
"mode": "0000644",
|
|
330
|
-
"sha256": "
|
|
330
|
+
"sha256": "b976a94f0879fd4fe0cdc1eb82209b446cdc7a39186ea1300f4b535bec8f1d00"
|
|
331
331
|
},
|
|
332
332
|
{
|
|
333
333
|
"path": "clients/bongos-client/README.md",
|
|
@@ -1142,7 +1142,7 @@
|
|
|
1142
1142
|
{
|
|
1143
1143
|
"path": "docs/adr/0137-upstream-publish-policy-manual-owner-gated.md",
|
|
1144
1144
|
"mode": "0000644",
|
|
1145
|
-
"sha256": "
|
|
1145
|
+
"sha256": "f56c0106d45ac95b6c506680db609859fac109c7b63b52ae8761440d8144871e"
|
|
1146
1146
|
},
|
|
1147
1147
|
{
|
|
1148
1148
|
"path": "docs/adr/0138-bundled-module-deps-declared-per-module.md",
|
|
@@ -1259,10 +1259,15 @@
|
|
|
1259
1259
|
"mode": "0000644",
|
|
1260
1260
|
"sha256": "3a23a8e64dc258397aea1ecff0c4aa75bf05067be9e1c96c678bf95d66c0c0fb"
|
|
1261
1261
|
},
|
|
1262
|
+
{
|
|
1263
|
+
"path": "docs/adr/0161-publish-on-merge.md",
|
|
1264
|
+
"mode": "0000644",
|
|
1265
|
+
"sha256": "056633fe7521143b0e8ba89297426ffd7253deb9ae5aa2fad65c3dfbffddc44f"
|
|
1266
|
+
},
|
|
1262
1267
|
{
|
|
1263
1268
|
"path": "docs/adr/README.md",
|
|
1264
1269
|
"mode": "0000644",
|
|
1265
|
-
"sha256": "
|
|
1270
|
+
"sha256": "165327e0c447cf9e549ba9389bc7eed6406a64631a9e2d714f7eb3ee80af8be0"
|
|
1266
1271
|
},
|
|
1267
1272
|
{
|
|
1268
1273
|
"path": "docs/api-reference.md",
|
|
@@ -1447,7 +1452,7 @@
|
|
|
1447
1452
|
{
|
|
1448
1453
|
"path": "docs/recipes/core-release-pipeline.md",
|
|
1449
1454
|
"mode": "0000644",
|
|
1450
|
-
"sha256": "
|
|
1455
|
+
"sha256": "c544204fb13188ac17bdb8841866fcd61df2cd8a9a033f73bbca04b4cd1202e2"
|
|
1451
1456
|
},
|
|
1452
1457
|
{
|
|
1453
1458
|
"path": "docs/recipes/devbox-app-release.md",
|
|
@@ -1532,7 +1537,7 @@
|
|
|
1532
1537
|
{
|
|
1533
1538
|
"path": "docs/recipes/private-npm-distribution.md",
|
|
1534
1539
|
"mode": "0000644",
|
|
1535
|
-
"sha256": "
|
|
1540
|
+
"sha256": "1aafe7dafea94084c381e7ea1e91fad533ca379f9560da6f9f9f4a2cf4db4fc5"
|
|
1536
1541
|
},
|
|
1537
1542
|
{
|
|
1538
1543
|
"path": "docs/recipes/search-index-ingestion.md",
|
|
@@ -2179,6 +2184,11 @@
|
|
|
2179
2184
|
"mode": "0000644",
|
|
2180
2185
|
"sha256": "614def05feaeefe06de81de73523bbc42da4ffd095a8a71ad4f80af7bf61d878"
|
|
2181
2186
|
},
|
|
2187
|
+
{
|
|
2188
|
+
"path": "migrations/core_205_versions_track_default.sql",
|
|
2189
|
+
"mode": "0000644",
|
|
2190
|
+
"sha256": "cfc4208327550710a64e44a401f552d78ab28814f033627f5fccc174f1c15b9d"
|
|
2191
|
+
},
|
|
2182
2192
|
{
|
|
2183
2193
|
"path": "modules/autonomy/CLAUDE.md",
|
|
2184
2194
|
"mode": "0000644",
|
|
@@ -3442,7 +3452,7 @@
|
|
|
3442
3452
|
{
|
|
3443
3453
|
"path": "modules/lifecycle/routes/tasks.js",
|
|
3444
3454
|
"mode": "0000644",
|
|
3445
|
-
"sha256": "
|
|
3455
|
+
"sha256": "14320bb5a3257a422854c41362f010042ed390bdd7efcf1b4959ed72f0ee1f9f"
|
|
3446
3456
|
},
|
|
3447
3457
|
{
|
|
3448
3458
|
"path": "modules/lifecycle/routes/versions.js",
|
|
@@ -3987,12 +3997,12 @@
|
|
|
3987
3997
|
{
|
|
3988
3998
|
"path": "package-lock.json",
|
|
3989
3999
|
"mode": "0000644",
|
|
3990
|
-
"sha256": "
|
|
4000
|
+
"sha256": "70ab73355f797cae5b2c277c408cd72a5c9d20d553c465094000ad529d176305"
|
|
3991
4001
|
},
|
|
3992
4002
|
{
|
|
3993
4003
|
"path": "package.json",
|
|
3994
4004
|
"mode": "0000644",
|
|
3995
|
-
"sha256": "
|
|
4005
|
+
"sha256": "6ce4caca6f095f694494c7eca0c39de6f522b4a4ba1ee7ecfb8d9013491907cb"
|
|
3996
4006
|
},
|
|
3997
4007
|
{
|
|
3998
4008
|
"path": "public-docs/index.html",
|
|
@@ -4489,6 +4499,11 @@
|
|
|
4489
4499
|
"mode": "0000644",
|
|
4490
4500
|
"sha256": "6a696f9291da2d071d8a8aa0f92bf0be08445569a73196f9204f696bf6cb6cb5"
|
|
4491
4501
|
},
|
|
4502
|
+
{
|
|
4503
|
+
"path": "scripts/gds/bump-version.js",
|
|
4504
|
+
"mode": "0000644",
|
|
4505
|
+
"sha256": "91664a0a1338ded41b780179c16040e2c686f6ab92304891cb1bc6ef3ca805b6"
|
|
4506
|
+
},
|
|
4492
4507
|
{
|
|
4493
4508
|
"path": "scripts/gds/capture.js",
|
|
4494
4509
|
"mode": "0000644",
|
|
@@ -4767,7 +4782,7 @@
|
|
|
4767
4782
|
{
|
|
4768
4783
|
"path": "scripts/gds/init.js",
|
|
4769
4784
|
"mode": "0000644",
|
|
4770
|
-
"sha256": "
|
|
4785
|
+
"sha256": "a58a741f11ea7d34396dcb9e242825bf88326ca2709b7c726a91b06f60f9c24c"
|
|
4771
4786
|
},
|
|
4772
4787
|
{
|
|
4773
4788
|
"path": "scripts/gds/install-git-hooks.js",
|
|
@@ -5607,7 +5622,7 @@
|
|
|
5607
5622
|
{
|
|
5608
5623
|
"path": "src/module-api.js",
|
|
5609
5624
|
"mode": "0000644",
|
|
5610
|
-
"sha256": "
|
|
5625
|
+
"sha256": "49ff422c2caafd8a1a78b6be322bc89cfb82ffce77b37bbd2c68919e3dc432eb"
|
|
5611
5626
|
},
|
|
5612
5627
|
{
|
|
5613
5628
|
"path": "src/module-loader/loader.js",
|
|
@@ -5859,6 +5874,11 @@
|
|
|
5859
5874
|
"mode": "0000644",
|
|
5860
5875
|
"sha256": "76380b52738c3ca2d452742f0b4316de765c39d68bc101ce6a90ea9d115842ef"
|
|
5861
5876
|
},
|
|
5877
|
+
{
|
|
5878
|
+
"path": "tests/bongos_cli.mjs",
|
|
5879
|
+
"mode": "0000644",
|
|
5880
|
+
"sha256": "e017ee497f5aaf041cf18d8bc2b12faed4e3cd71418e14d55eee630ebb4282d3"
|
|
5881
|
+
},
|
|
5862
5882
|
{
|
|
5863
5883
|
"path": "tests/bongos_dispatch.mjs",
|
|
5864
5884
|
"mode": "0000644",
|
|
@@ -5974,6 +5994,11 @@
|
|
|
5974
5994
|
"mode": "0000644",
|
|
5975
5995
|
"sha256": "e3d8c5ac45d59f5d566547e2c406cf44ffa60f1b243afcb222b09eee68f5ee0d"
|
|
5976
5996
|
},
|
|
5997
|
+
{
|
|
5998
|
+
"path": "tests/bump_version.mjs",
|
|
5999
|
+
"mode": "0000644",
|
|
6000
|
+
"sha256": "d8493f101a0009dd550c1ccf126e5ceb6433013257aa4f307ca68b00788dc77e"
|
|
6001
|
+
},
|
|
5977
6002
|
{
|
|
5978
6003
|
"path": "tests/cap_newcomer_tasks.mjs",
|
|
5979
6004
|
"mode": "0000644",
|
|
@@ -6642,7 +6667,7 @@
|
|
|
6642
6667
|
{
|
|
6643
6668
|
"path": "tests/init.mjs",
|
|
6644
6669
|
"mode": "0000644",
|
|
6645
|
-
"sha256": "
|
|
6670
|
+
"sha256": "ed99a7b7d925d9f202ab90f861cc4b761c0e58bb7eb79b189a6366591faeb388"
|
|
6646
6671
|
},
|
|
6647
6672
|
{
|
|
6648
6673
|
"path": "tests/instance_config.mjs",
|
|
@@ -6974,6 +6999,11 @@
|
|
|
6974
6999
|
"mode": "0000644",
|
|
6975
7000
|
"sha256": "fd69c44ebf3d246ecedf527275448389cca809e231d1c6cb34e4c12bfdc38111"
|
|
6976
7001
|
},
|
|
7002
|
+
{
|
|
7003
|
+
"path": "tests/patch_gate_ordering.mjs",
|
|
7004
|
+
"mode": "0000644",
|
|
7005
|
+
"sha256": "4472cc00b1bc62c47a2f3121d0b5ec08f0b5b3bca6cebc3aaef179093eccffe8"
|
|
7006
|
+
},
|
|
6977
7007
|
{
|
|
6978
7008
|
"path": "tests/path_match.mjs",
|
|
6979
7009
|
"mode": "0000644",
|
package/bin/bongos.js
CHANGED
|
@@ -62,18 +62,22 @@ let VERSION = '0.0.0';
|
|
|
62
62
|
try { VERSION = require('../config/bongos-cli.json').version || VERSION; } catch { /* best-effort */ }
|
|
63
63
|
if (VERSION === '0.0.0') { try { VERSION = require('../package.json').version || VERSION; } catch { /* best-effort */ } }
|
|
64
64
|
|
|
65
|
-
// verb -> { script, prefixArgs?, group, summary }
|
|
65
|
+
// verb -> { script, prefixArgs?, group, summary, selfHelp? }
|
|
66
66
|
// `script` a file under scripts/gds/ (or api.js for actions with no
|
|
67
67
|
// dedicated builder script yet).
|
|
68
68
|
// `prefixArgs` inserted before the user's args — used to drive the generic
|
|
69
69
|
// api.js caller (e.g. waking your box is just POST /box/ensure).
|
|
70
|
+
// `selfHelp` the script prints its own (richer) usage on --help/-h, so the
|
|
71
|
+
// dispatcher forwards the flag instead of answering it (task
|
|
72
|
+
// 1002625). Only set it after verifying the script really does —
|
|
73
|
+
// the default (intercept here) is the one that can never block.
|
|
70
74
|
const VERBS = {
|
|
71
75
|
start: { script: 'start.js', group: 'work', summary: 'List tasks you can claim right now' },
|
|
72
|
-
task: { script: 'task.js', group: 'work', summary: 'Create or show a task (bongos task new --title … --description …)' },
|
|
76
|
+
task: { script: 'task.js', group: 'work', summary: 'Create or show a task (bongos task new --title … --description …)', selfHelp: true },
|
|
73
77
|
claim: { script: 'claim.js', group: 'work', summary: 'Claim a task by id (bongos claim 1246)' },
|
|
74
78
|
ship: { script: 'ship.js', group: 'work', summary: 'Grade, merge, and deploy your claimed task' },
|
|
75
79
|
release: { script: 'release.js', group: 'work', summary: 'Cancel a claim — task returns to ready (no credits)' },
|
|
76
|
-
status: { script: 'status.js', group: 'work', summary: 'Version / criterion progress (bongos status C8)' },
|
|
80
|
+
status: { script: 'status.js', group: 'work', summary: 'Version / criterion progress (bongos status C8)', selfHelp: true },
|
|
77
81
|
recall: { script: 'recall.js', group: 'work', summary: 'Search the docs + project knowledge (bongos recall "x")' },
|
|
78
82
|
cost: { script: 'cost.js', group: 'work', summary: 'Log a cost entry' },
|
|
79
83
|
|
|
@@ -86,10 +90,10 @@ const VERBS = {
|
|
|
86
90
|
code: { script: 'code.js', group: 'account', summary: 'Open VS Code on your dev box over Remote-SSH (--local for a local clone)' },
|
|
87
91
|
|
|
88
92
|
init: { script: 'init.js', group: 'instance', summary: 'Scaffold a new instance (config + first version + kickoff tasks)' },
|
|
89
|
-
dev: { script: 'dev.js', group: 'instance', summary: 'Run THIS instance locally in one command (Postgres + migrate + server + sign-in)' },
|
|
93
|
+
dev: { script: 'dev.js', group: 'instance', summary: 'Run THIS instance locally in one command (Postgres + migrate + server + sign-in)', selfHelp: true },
|
|
90
94
|
onboard: { script: 'onboard.js', group: 'instance', summary: 'Guided zero-to-live walkthrough: init → scaffold → provision → OAuth → verify → brand' },
|
|
91
|
-
module: { script: 'module.js', group: 'instance', summary: 'Scaffold a new module (bongos module new <key>)' },
|
|
92
|
-
upgrade: { script: 'upgrade.js', group: 'instance', summary: 'Pull the core to a new version (bump pin→install→migrate→materialize→restart); bare = module-compat pre-check' },
|
|
95
|
+
module: { script: 'module.js', group: 'instance', summary: 'Scaffold a new module (bongos module new <key>)', selfHelp: true },
|
|
96
|
+
upgrade: { script: 'upgrade.js', group: 'instance', summary: 'Pull the core to a new version (bump pin→install→migrate→materialize→restart); bare = module-compat pre-check', selfHelp: true },
|
|
93
97
|
'package-core': { script: 'package-core.js', group: 'instance', summary: 'Package the core as a versioned, installable artifact (+ pin manifest)' },
|
|
94
98
|
exec: { script: null, group: 'instance', summary: 'Run a core script inside the package (bongos exec scripts/gds/<x>.js [args])' },
|
|
95
99
|
};
|
|
@@ -134,6 +138,17 @@ function helpText() {
|
|
|
134
138
|
return lines.join('\n');
|
|
135
139
|
}
|
|
136
140
|
|
|
141
|
+
// One verb's help card — what `bongos <verb> --help` prints for verbs whose
|
|
142
|
+
// script has no usage printer of its own (see selfHelp above).
|
|
143
|
+
function verbHelpText(verb, def) {
|
|
144
|
+
return [
|
|
145
|
+
`bongos ${verb} — ${def.summary}`,
|
|
146
|
+
'',
|
|
147
|
+
`Forwards to scripts/gds/${def.script}; extra arguments are passed through.`,
|
|
148
|
+
'Run `bongos help` to see every command.',
|
|
149
|
+
].join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
137
152
|
// Returns the process exit code (does not call process.exit, so it's testable).
|
|
138
153
|
function main(argv) {
|
|
139
154
|
const args = argv.slice(2);
|
|
@@ -154,6 +169,17 @@ function main(argv) {
|
|
|
154
169
|
return 2;
|
|
155
170
|
}
|
|
156
171
|
|
|
172
|
+
// `bongos <verb> --help` (task 1002625): never spawn a script that doesn't
|
|
173
|
+
// recognise the flag — init.js, for one, treated `--help` as the start of its
|
|
174
|
+
// interactive interview and BLOCKED on "Project name?" (hostile in a terminal,
|
|
175
|
+
// fatal in CI). Verbs marked selfHelp print their own richer usage, so the
|
|
176
|
+
// flag is forwarded to them; `exec` always forwards (the flag belongs to the
|
|
177
|
+
// child script the user named).
|
|
178
|
+
if (verb !== 'exec' && !def.selfHelp && args.slice(1).some((a) => a === '--help' || a === '-h')) {
|
|
179
|
+
process.stdout.write(verbHelpText(verb, def) + '\n');
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
157
183
|
// `bongos exec <relpath> [args]` — run a core script by its package-relative path (see above).
|
|
158
184
|
if (verb === 'exec') {
|
|
159
185
|
if (COMPILED) {
|
|
@@ -221,4 +247,4 @@ if (require.main === module) {
|
|
|
221
247
|
if (typeof code === 'number') process.exit(code);
|
|
222
248
|
}
|
|
223
249
|
|
|
224
|
-
module.exports = { VERBS, GROUPS, helpText, main, VERSION, GDS_DIR, COMPILED, BUNDLED_RUNNERS, resolveExecTarget };
|
|
250
|
+
module.exports = { VERBS, GROUPS, helpText, verbHelpText, main, VERSION, GDS_DIR, COMPILED, BUNDLED_RUNNERS, resolveExecTarget };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0137 — Upstream publish policy: `@bongos/core` patches are published manually (owner-gated), not auto-published on merge
|
|
2
2
|
|
|
3
|
-
- **Status:**
|
|
3
|
+
- **Status:** Superseded by [ADR 0161](<redacted>.md) (2026-08-08) — after two measured stale-production incidents (171 commits/10 days, 104 commits/5 days), the owner chose publish-on-merge; the "When to revisit" clause below fired.
|
|
4
4
|
- **Date:** 2026-07-10
|
|
5
5
|
- **Deciders:** example-owner (owner/Archon), Claude. Presented the manual-vs-auto fork; the owner chose to keep publishing a manual, owner-triggered step.
|
|
6
6
|
- **Task:** [#2158](https://example.com/builders#/task/2158) (BONGOS-V1, goal 26) — the **supply** half split out of [ADR 0136](<redacted>.md) §5 / [#2150](https://example.com/builders#/task/2150).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# 0161 — Publish on merge: every merge to core `main` auto-publishes `@bongos/core`
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-08-08
|
|
5
|
+
- **Deciders:** the owner (Archon), Claude. The owner chose this directly after reviewing the pipeline end-to-end: "any merge can publish, we can add in more complexity later."
|
|
6
|
+
- **Task:** task 1002620 (BONGOS-V1, goal 1000024 — build-pipeline integrity)
|
|
7
|
+
- **Supersedes:** [ADR 0137](<redacted>.md) (manual owner-gated publish)
|
|
8
|
+
- **Builds on:** [ADR 0134](<redacted>.md) (private-first npm), [ADR 0136](<redacted>.md) (channel policy), [ADR 0159](<redacted>.md) (the required `unit` check)
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
ADR 0137 kept publishing a manual, owner-gated step: no npm write token at rest, the owner runs `package-core.js --publish --live` when they decide to cut a release. Its own "When to revisit" clause said to re-evaluate when the manual step becomes a real bottleneck.
|
|
13
|
+
|
|
14
|
+
It became one twice in three weeks, measured:
|
|
15
|
+
|
|
16
|
+
- **171 commits / 10 days** unreleased (blocker 1000097) — an entire governance permission system sat on `main` while the live platform served the previous version and every pipeline layer reported success.
|
|
17
|
+
- **104 commits / 5 days** unreleased immediately after — including a `CORE_VERSION` bump (`1.19.0`) that was landed but never packaged, so the version number existed while no artifact carried it.
|
|
18
|
+
|
|
19
|
+
The consumption side (ADR 0136's subscription) was never the problem; supply was. The owner reviewed the full pipeline on 2026-08-08 and reversed 0137's trade-off explicitly.
|
|
20
|
+
|
|
21
|
+
## Decision
|
|
22
|
+
|
|
23
|
+
1. **Every merge to `main` publishes.** A `publish` workflow fires on the `unit` check succeeding on a main push (never on a red suite), builds via `package-core` — the fail-closed no-leak gate and `--self-verify` pin check run inside the build — publishes (restricted), and tags `core-v<version>`.
|
|
24
|
+
2. **CI bumps PATCH only.** When a merge lands without a human bump, `scripts/gds/bump-version.js` bumps the patch and writes the changelog line (shape-preserving; pinned by `tests/bump_version.mjs`). A **minor or major stays a deliberate in-task human edit** — it is a statement about the doorway surface, and it decides what patch-channel subscribers receive unattended (ADR 0136). CI is not allowed to make that call.
|
|
25
|
+
3. **Token custody reverses, knowingly.** A standing npm **write** Automation token (`NPM_TOKEN`) and a fine-grained **contents:write** PAT (`RELEASE_PUSH_TOKEN`, for the bump commit — the default `GITHUB_TOKEN` cannot push past the required `unit` check) live as GitHub Actions secrets. This is exactly the surface 0137 declined; the owner accepted it with the incidents as evidence. Mitigations: the lane is **default-inert** behind the repo variable `PUBLISH_ON_MERGE=1` (an owner kill-switch symmetrical to `gating.js`), packages publish `restricted`, and the no-leak gate structurally cannot upload a leaking artifact.
|
|
26
|
+
4. **Idempotent by version-compare.** The bump commit re-triggers `unit` → `publish`, which then finds HEAD's version already on npm and no-ops. npm's duplicate-version rejection is the backstop; concurrent merges queue behind a no-cancel concurrency group.
|
|
27
|
+
|
|
28
|
+
## Consequences
|
|
29
|
+
|
|
30
|
+
- A merged change is on npm within minutes, unattended. Combined with the subscription (ADR 0136), patch-channel instances take it on their next sweep — the full merge→live path has no human step for patches.
|
|
31
|
+
- The bump commit is authored by `github-actions[bot]` directly on `main` (admin-exempt push, `enforce_admins: false`). It re-runs `unit` as a safety net.
|
|
32
|
+
- `deploy-divergence.js`'s UNRELEASED gap should now hover near zero; a growing gap means the lane is disarmed or red — both loud.
|
|
33
|
+
- One-time owner arming step (filed as a blocker): create the two secrets, set `PUBLISH_ON_MERGE=1`.
|
|
34
|
+
- Revisit toward *more* ceremony (environments, provenance attestation, trusted publishing/OIDC to npm — which would retire the standing token) when the fleet grows; revisit toward *less* (auto-minor) never without a human surface-diff check.
|
package/docs/adr/README.md
CHANGED
|
@@ -232,5 +232,6 @@ This keeps the decision history honest and traceable.
|
|
|
232
232
|
| 0159 | [**Merge-gate reconciliation: required checks match reality, CODEOWNERS becomes real, protection stays the owner's switch** ([task 1002611](https://cloudbongos.com/builders#/task/1002611)). Findings D-12 + G-5 from the choke-point map: `gating.js` hardcoded three required status checks (`unit`, `trufflehog`, `gate-review`) of which two have never had a workflow — running `gating.js on` would have required contexts nothing can report, blocking every merge indefinitely instead of gating them — and CODEOWNERS was described as a live control by ADR 0042, `protected-surfaces.json`, `publish-manifest.js` and gating.js itself while not existing anywhere in the tree. Now that the `unit` lane exists (task 1002610), `DEFAULT_PROTECTION` requires ONLY `[unit]`; the future contexts are fixed as `secret-scan` + `gate-review` (the `CORE_EXPECTED_CHECKS` names — a context is named for what it asserts, not the tool asserting it) and return when their ADR 0058 workflows ship; CODEOWNERS becomes a real root file scoped to `gate-review.js`'s `HARD_FLOOR_GLOBS` + gating.js (inert until protection is on); branch protection itself stays OFF — flipping it remains the owner's explicit act, this only makes the switch safe. Rider: the unit lane is hardened so `unit` is actually requirable — `fetch-depth: 0` (depth-1 broke `tests/main_audit.mjs` on the lane's first run), two clean-env-red tests parked in `KNOWN_FAILING` with diagnoses + exit tracked in [task 1002617](https://cloudbongos.com/builders#/task/1002617), and `publish_manifest` (the public-mirror boundary test) fixed in-claim rather than parked — its ≥7-host-tests non-vacuity floor went stale when ADR 0107 moved host tests into `modules/<key>/tests/`, replaced by a regex specimen self-test. Rejected: deleting `GATE_WORKFLOWS`' forward-looking names (the switch handles absence gracefully; deleting them un-covers the workflows the moment they ship) and watering down the four CODEOWNERS descriptions instead of creating the one file.](<redacted>.md) | governance / build pipeline |
|
|
233
233
|
|
|
234
234
|
| 0160 | [**The authoritative main audit gets a repo-verifiable caller** ([task 1002612](https://cloudbongos.com/builders#/task/1002612)). Findings G-3 + G-8, honest limit S-1: `main-audit.js` — the post-push layer [ADR 0043](<redacted>.md) designates authoritative — had two declared invokers that never existed in this git history, and `audit-authorship.sh` (the no-hardcoded-Archon-identity enforcer) had no caller either and turned out red with 340 pre-existing hits on untouched main (idea 1000653). Now a `main-audit` workflow audits every push to `main` over exactly the pushed `before..after` range (clamped-window fallback); the roster comes from the public leaderboard when no session exists (server-authoritative, login-only keying, an absent builder resolves as unresolved — never falsely trusted); new `--require-roster` makes an empty roster a loud DID-NOT-RUN exit 2 (an audit that cannot fire a blocker must not exit green); the lane runs non-strict per the audit's own zero-false-positive calibration; and `audit-authorship.sh` rides as an advisory job until the 1000653 triage. Detective lane, not a merge gate — deliberately outside the required PR contexts. Rider: the known-failing ratchet caught task 1002611's publish_manifest fix exactly as designed — entry removed, ceiling ratcheted 3 → 2, remaining entries re-owned to [task 1002617](https://cloudbongos.com/builders#/task/1002617), idea 1000542 superseded. Rejected: a GDS token secret (silent-fail dependency the devcontainer lane warned about), `--strict` in CI (permanent red rots into wallpaper), enforcing authorship before triage.](<redacted>.md) | governance / build pipeline |
|
|
235
|
+
| 0161 | [**Publish on merge: every merge to core `main` auto-publishes `@bongos/core`** (task 1002620). Supersedes ADR 0137's manual owner-gated publish after its own "when to revisit" clause fired twice in three weeks: 171 commits/10 days, then 104 commits/5 days, sat on `main` reachable by no pin while every layer reported success. A `publish` workflow rides behind the required `unit` check (`workflow_run`, success-gated): when HEAD's `CORE_VERSION` is already on npm, CI bumps a PATCH via the shape-preserving `bump-version.js` (+ changelog line; pinned by `tests/bump_version.mjs`) and pushes the carrier commit; `package-core --self-verify --publish --live` builds through the fail-closed no-leak gate; the release is tagged `core-v<v>`. A MINOR/MAJOR stays a human in-task edit — it decides what patch-channel subscribers receive unattended (ADR 0136), a call CI must not make. Token custody knowingly reverses (owner-accepted): `NPM_TOKEN` write + `RELEASE_PUSH_TOKEN` PAT as Actions secrets, mitigated by the lane shipping INERT behind the `PUBLISH_ON_MERGE=1` repo variable — an owner kill-switch symmetrical to `gating.js`. Idempotent by version-compare; npm duplicate rejection is the backstop; queued no-cancel concurrency collapses merge races.](<redacted>.md) | build pipeline / distribution |
|
|
235
236
|
|
|
236
237
|
> ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
|
|
@@ -97,16 +97,18 @@ node scripts/gds/package-core.js --ref main --self-verify
|
|
|
97
97
|
|
|
98
98
|
> ⚠️ **Packaging a mid-flight `main` produces a stale version number.** If `CORE_VERSION` at your ref is still the previous value, your tarball is stamped with a version the fleet may already have. `bongos upgrade` refuses only the **equal** case (`already on <v>`, and `--force` bypasses even that) — it has **no downgrade guard**, so a target *below* what is installed proceeds and silently installs older code. Confirm the bump is actually in your ref before packaging.
|
|
99
99
|
|
|
100
|
-
### 2c. Publish —
|
|
100
|
+
### 2c. Publish — automatic on merge
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
**Every merge to `main` publishes** ([ADR 0161](../adr/<redacted>.md), superseding ADR 0137's manual gate): once the required `unit` check succeeds on a main push, the `publish` workflow auto-bumps a **patch** if nobody bumped (via `scripts/gds/bump-version.js`, changelog line included), builds through the fail-closed no-leak gate with `--self-verify`, publishes, and tags `core-v<version>`. A **minor/major is still a human in-task bump** — CI never makes that call, because it decides what patch-channel subscribers receive unattended.
|
|
103
|
+
|
|
104
|
+
The lane is armed by the owner via the `PUBLISH_ON_MERGE=1` repo variable + two secrets (`NPM_TOKEN`, `RELEASE_PUSH_TOKEN`); while disarmed it no-ops loudly. Manual fallback, same command CI runs:
|
|
103
105
|
|
|
104
106
|
```bash
|
|
105
107
|
node scripts/gds/package-core.js --publish # DRY-RUN — runs the no-leak gate, previews
|
|
106
108
|
node scripts/gds/package-core.js --publish --live # permanent
|
|
107
109
|
```
|
|
108
110
|
|
|
109
|
-
|
|
111
|
+
**A publish is not what moves a vendored-tarball instance** — see gate 3. Chasing an npm publish to move a `file:`-pinned instance is a documented dead end.
|
|
110
112
|
|
|
111
113
|
## Gate 3 — move the instance's pin
|
|
112
114
|
|
|
@@ -162,7 +164,7 @@ Exit codes: `0` aligned, `1` diverged, `2` undetermined.
|
|
|
162
164
|
| Land a change on `main` | 1 | any builder holding a claim, behind the required `unit` check (protected paths are Metic+; hard-floor paths need owner review per [`CODEOWNERS`](../../CODEOWNERS); [canonical-permissions](../canonical-permissions.md)) |
|
|
163
165
|
| Bump `CORE_VERSION` | 2a | a builder, inside a claimed task |
|
|
164
166
|
| Build the artifact | 2b | any builder — `package-core` is tokenless and network-free |
|
|
165
|
-
| Publish to npm | 2c | **
|
|
167
|
+
| Publish to npm | 2c | **automatic on merge** once the owner arms the lane ([ADR 0161](../adr/<redacted>.md)); the manual `--publish --live` fallback stays owner-only |
|
|
166
168
|
| Move an instance's pin | 3 | whoever operates that instance |
|
|
167
169
|
|
|
168
170
|
Automating gate 2c (a tag-triggered CI publish) is deliberately deferred until a real fleet makes the manual step a bottleneck — see [ADR 0137](../adr/<redacted>.md) "When to revisit".
|
|
@@ -49,9 +49,11 @@ npm pack ./clients/bongos-client --dry-run # token-free content preview
|
|
|
49
49
|
npm publish ./clients/bongos-client # real (publishConfig makes it restricted; needs npm login)
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
## Cutting a core release (the policy — [ADR
|
|
52
|
+
## Cutting a core release (the policy — [ADR 0161](../adr/<redacted>.md), superseding ADR 0137)
|
|
53
53
|
|
|
54
|
-
Publishing is
|
|
54
|
+
**Publishing is automatic on merge** as of 2026-08-08: the `publish` workflow rides behind the required `unit` check, auto-bumps a patch when nobody bumped, builds through the fail-closed no-leak gate, publishes, and tags — see the workflow header (`.github/workflows/publish.yml`) and ADR 0161. It ships inert until the owner arms it (repo variable `PUBLISH_ON_MERGE=1` + secrets `NPM_TOKEN`, `RELEASE_PUSH_TOKEN`). A **minor/major bump stays a human in-task edit**. The update subscription ([ADR 0136](../adr/<redacted>.md)) consumes whatever is published, on its next sweep.
|
|
55
|
+
|
|
56
|
+
The steps below remain as the **manual fallback** (lane disarmed, npm outage, or a deliberate out-of-band release):
|
|
55
57
|
|
|
56
58
|
1. **Bump `CORE_VERSION`** in [`src/module-api.js`](../../src/module-api.js) (semver; a **patch** for a fix, **minor** for additive surface — a **major** is a deliberate, human-only event) and add a one-line changelog entry in the same file. This bump is a normal claimed task, not a release-tool side effect — the version's provenance stays in the git history + changelog.
|
|
57
59
|
2. **Publish** (owner, from a clean checkout of the released commit):
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- core_205_versions_track_default.sql — task 1002627 (decision follow-up to 1002623).
|
|
2
|
+
--
|
|
3
|
+
-- versions.track is NOT NULL + CHECK ('product'|'internal') and had NO default
|
|
4
|
+
-- (003_pms.sql). Task 1002623 fixed the standup-blocking consequence in code —
|
|
5
|
+
-- createVersion + POST /versions now always supply track — but the column
|
|
6
|
+
-- itself still refused any insert that omitted it.
|
|
7
|
+
--
|
|
8
|
+
-- DECISION (recorded here + in the task): add DEFAULT 'product'. The failure
|
|
9
|
+
-- this prevents — a fresh instance that cannot seed its first version, or any
|
|
10
|
+
-- future insert path 500ing — is strictly worse than the failure it introduces
|
|
11
|
+
-- (a forgotten track silently classifying as 'product': cosmetic, one UPDATE to
|
|
12
|
+
-- fix). 'product' matches the code default: an instance's own version is
|
|
13
|
+
-- product work; 'internal' is the platform's opt-in track.
|
|
14
|
+
--
|
|
15
|
+
-- Core migration (not modules/lifecycle/) because versions is a core table —
|
|
16
|
+
-- a module migration may only touch its own namespaced tables (ADR 0083 §5).
|
|
17
|
+
--
|
|
18
|
+
-- Forward-safe: adds a default only — old code inserting an explicit track is
|
|
19
|
+
-- unaffected, and no existing rows change.
|
|
20
|
+
|
|
21
|
+
BEGIN;
|
|
22
|
+
|
|
23
|
+
ALTER TABLE versions ALTER COLUMN track SET DEFAULT 'product';
|
|
24
|
+
|
|
25
|
+
INSERT INTO schema_migrations (version) VALUES ('core_205_versions_track_default')
|
|
26
|
+
ON CONFLICT (version) DO NOTHING;
|
|
27
|
+
|
|
28
|
+
COMMIT;
|
|
@@ -797,18 +797,20 @@ module.exports = function buildTasksRouter() {
|
|
|
797
797
|
if (!hasParent && !hasKind && !hasDiscipline && !hasNewcomerOrXenos && !hasParallelSafe && !hasSecuritySensitive && !hasAutomationTag && !hasRequiresRank && !hasModuleKey && !hasGoalId) {
|
|
798
798
|
return res.fail('no_supported_fields', { status: 400, message: 'PATCH /tasks/:id supports parent_task_id, kind, discipline, parallel_safe, security_sensitive, automation_tag, requires_rank, module_key, goal_id, and newcomer_friendly (alias: xenos_claimable). ' + STATUS_HINT });
|
|
799
799
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
800
|
+
// ---- task 1002609 (G-22): settle EVERY gate and validation before the ----
|
|
801
|
+
// ---- FIRST write. The previous shape validated-and-wrote per field in ----
|
|
802
|
+
// sequence, so a body mixing a valid early field with a refused later one
|
|
803
|
+
// committed the early write and then 403/400'd — state changed on a request
|
|
804
|
+
// the caller was told was refused (a Metic sending { parallel_safe,
|
|
805
|
+
// security_sensitive } had parallel_safe committed before rank_forbidden).
|
|
806
|
+
// Authorization, type/enum checks, and the goal existence/version reads all
|
|
807
|
+
// run here; writes begin only after the last validation `return`.
|
|
808
|
+
// (Documented residual: parent_task_id validates inside hierarchy.attachParent
|
|
809
|
+
// at the end — its checks are atomic with its OWN write, but a refusal there
|
|
810
|
+
// still lands after the field writes below. No authorization gate is
|
|
811
|
+
// involved in that path; the handler's only rank gate is hoisted here.)
|
|
812
|
+
if (hasParallelSafe && body.parallel_safe !== null && typeof body.parallel_safe !== 'boolean') {
|
|
813
|
+
return res.fail('bad_parallel_safe', { status: 400, message: 'parallel_safe must be true, false, or null' });
|
|
812
814
|
}
|
|
813
815
|
if (hasSecuritySensitive) {
|
|
814
816
|
// task 990: explicit Archon-only-broadcast flag. Boolean only (no null) —
|
|
@@ -826,41 +828,19 @@ module.exports = function buildTasksRouter() {
|
|
|
826
828
|
if (typeof body.security_sensitive !== 'boolean') {
|
|
827
829
|
return res.fail('bad_security_sensitive', { status: 400, message: 'security_sensitive must be a boolean' });
|
|
828
830
|
}
|
|
829
|
-
try {
|
|
830
|
-
updated = await db.updateTaskSecuritySensitive(id, body.security_sensitive);
|
|
831
|
-
if (!updated) return res.fail('task_not_found', 404);
|
|
832
|
-
} catch (err) {
|
|
833
|
-
console.error('[gds] PATCH /tasks/:id security_sensitive', err);
|
|
834
|
-
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
835
|
-
}
|
|
836
831
|
}
|
|
832
|
+
const VALID_AUTOMATION_TAGS = new Set(['overnight-safe', 'live-only', 'needs-input']);
|
|
837
833
|
if (hasAutomationTag) {
|
|
838
834
|
const tag = body.automation_tag;
|
|
839
|
-
const VALID_AUTOMATION_TAGS = new Set(['overnight-safe', 'live-only', 'needs-input']);
|
|
840
835
|
if (tag !== null && (typeof tag !== 'string' || !VALID_AUTOMATION_TAGS.has(tag))) {
|
|
841
836
|
return res.fail('bad_automation_tag', { status: 400, message: 'automation_tag must be one of overnight-safe | live-only | needs-input, or null to clear', details: { valid: Array.from(VALID_AUTOMATION_TAGS) } });
|
|
842
837
|
}
|
|
843
|
-
try {
|
|
844
|
-
updated = await db.updateTaskAutomationTag(id, tag);
|
|
845
|
-
if (!updated) return res.fail('task_not_found', 404);
|
|
846
|
-
} catch (err) {
|
|
847
|
-
console.error('[gds] PATCH /tasks/:id automation_tag', err);
|
|
848
|
-
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
849
|
-
}
|
|
850
838
|
}
|
|
851
839
|
if (hasModuleKey) {
|
|
852
840
|
// BV1.R53 (task 1422): assign or clear the module affinity label. null clears.
|
|
853
|
-
|
|
854
|
-
if (key !== null && !isModuleKey(key)) {
|
|
841
|
+
if (body.module_key !== null && !isModuleKey(body.module_key)) {
|
|
855
842
|
return res.fail('bad_module_key', { status: 400, message: `module_key must be one of the known module keys, or null to clear. Valid keys: ${moduleKeys().join(', ')}.`, details: { valid: moduleKeys() } });
|
|
856
843
|
}
|
|
857
|
-
try {
|
|
858
|
-
updated = await db.updateTaskModuleKey(id, key ?? null);
|
|
859
|
-
if (!updated) return res.fail('task_not_found', 404);
|
|
860
|
-
} catch (err) {
|
|
861
|
-
console.error('[gds] PATCH /tasks/:id module_key', err);
|
|
862
|
-
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
863
|
-
}
|
|
864
844
|
}
|
|
865
845
|
if (hasGoalId) {
|
|
866
846
|
// task 1763: (re)assign the task's goal — the documented "Archon/Metic
|
|
@@ -882,8 +862,6 @@ module.exports = function buildTasksRouter() {
|
|
|
882
862
|
if (String(goal.version_id) !== String(task.version_id)) {
|
|
883
863
|
return res.fail('goal_version_mismatch', { status: 409, message: `Goal #${goalId} is in version '${goal.version_id}' but task #${id} is in '${task.version_id}'. A task can only join a goal in its own version.` });
|
|
884
864
|
}
|
|
885
|
-
updated = await db.updateTaskGoal(id, goalId);
|
|
886
|
-
if (!updated) return res.fail('task_not_found', 404);
|
|
887
865
|
} catch (err) {
|
|
888
866
|
console.error('[gds] PATCH /tasks/:id goal_id', err);
|
|
889
867
|
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
@@ -893,13 +871,77 @@ module.exports = function buildTasksRouter() {
|
|
|
893
871
|
// task 1498 / ADR 0084: the Archon override of the per-task rank floor.
|
|
894
872
|
// Validate the enum here; db.updateTaskRequiresRank clamps the value UP to
|
|
895
873
|
// the auto-derived floor (touches + security_sensitive), so a PATCH can
|
|
896
|
-
// only RAISE the gate — never open a sensitive task to a lower rank.
|
|
897
|
-
// AFTER the security_sensitive block above, so a same-request flag flip is
|
|
898
|
-
// already reflected in the derived floor.
|
|
874
|
+
// only RAISE the gate — never open a sensitive task to a lower rank.
|
|
899
875
|
const LIVE_RANKS = new Set(db.LIVE_RANK_LADDER);
|
|
900
876
|
if (typeof body.requires_rank !== 'string' || !LIVE_RANKS.has(body.requires_rank)) {
|
|
901
877
|
return res.fail('bad_requires_rank', { status: 400, message: `requires_rank must be one of ${db.LIVE_RANK_LADDER.join(' | ')}. An override can only raise the floor; it is clamped up to the auto-derived minimum.`, details: { valid: db.LIVE_RANK_LADDER } });
|
|
902
878
|
}
|
|
879
|
+
}
|
|
880
|
+
if (hasKind && !VALID_KINDS.has(body.kind)) {
|
|
881
|
+
return badKind(res);
|
|
882
|
+
}
|
|
883
|
+
if (hasDiscipline && !VALID_DISCIPLINES.has(body.discipline)) {
|
|
884
|
+
return res.fail('bad_discipline', 400, { valid: Array.from(VALID_DISCIPLINES) });
|
|
885
|
+
}
|
|
886
|
+
if (hasNewcomerOrXenos) {
|
|
887
|
+
// V3.R15 #239: prefer newcomer_friendly (canonical); fall back to
|
|
888
|
+
// xenos_claimable (pre-rename alias). Both map to the same DB column.
|
|
889
|
+
const incoming = hasNewcomerFriendly ? body.newcomer_friendly : body.xenos_claimable;
|
|
890
|
+
if (typeof incoming !== 'boolean') {
|
|
891
|
+
const field = hasNewcomerFriendly ? 'newcomer_friendly' : 'xenos_claimable';
|
|
892
|
+
return res.fail(`bad_${field}`, { status: 400, message: `${field} must be a boolean` });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// ---- all gates passed: apply the writes. Order preserved from the old ----
|
|
896
|
+
// shape — requires_rank still runs AFTER security_sensitive so a
|
|
897
|
+
// same-request flag flip is already reflected in the derived floor.
|
|
898
|
+
let updated = null;
|
|
899
|
+
if (hasParallelSafe) {
|
|
900
|
+
try {
|
|
901
|
+
updated = await db.updateTaskParallelSafe(id, body.parallel_safe);
|
|
902
|
+
if (!updated) return res.fail('task_not_found', 404);
|
|
903
|
+
} catch (err) {
|
|
904
|
+
console.error('[gds] PATCH /tasks/:id parallel_safe', err);
|
|
905
|
+
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (hasSecuritySensitive) {
|
|
909
|
+
try {
|
|
910
|
+
updated = await db.updateTaskSecuritySensitive(id, body.security_sensitive);
|
|
911
|
+
if (!updated) return res.fail('task_not_found', 404);
|
|
912
|
+
} catch (err) {
|
|
913
|
+
console.error('[gds] PATCH /tasks/:id security_sensitive', err);
|
|
914
|
+
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (hasAutomationTag) {
|
|
918
|
+
try {
|
|
919
|
+
updated = await db.updateTaskAutomationTag(id, body.automation_tag);
|
|
920
|
+
if (!updated) return res.fail('task_not_found', 404);
|
|
921
|
+
} catch (err) {
|
|
922
|
+
console.error('[gds] PATCH /tasks/:id automation_tag', err);
|
|
923
|
+
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (hasModuleKey) {
|
|
927
|
+
try {
|
|
928
|
+
updated = await db.updateTaskModuleKey(id, body.module_key ?? null);
|
|
929
|
+
if (!updated) return res.fail('task_not_found', 404);
|
|
930
|
+
} catch (err) {
|
|
931
|
+
console.error('[gds] PATCH /tasks/:id module_key', err);
|
|
932
|
+
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (hasGoalId) {
|
|
936
|
+
try {
|
|
937
|
+
updated = await db.updateTaskGoal(id, body.goal_id);
|
|
938
|
+
if (!updated) return res.fail('task_not_found', 404);
|
|
939
|
+
} catch (err) {
|
|
940
|
+
console.error('[gds] PATCH /tasks/:id goal_id', err);
|
|
941
|
+
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (hasRequiresRank) {
|
|
903
945
|
try {
|
|
904
946
|
updated = await db.updateTaskRequiresRank(id, body.requires_rank);
|
|
905
947
|
if (!updated) return res.fail('task_not_found', 404);
|
|
@@ -909,9 +951,6 @@ module.exports = function buildTasksRouter() {
|
|
|
909
951
|
}
|
|
910
952
|
}
|
|
911
953
|
if (hasKind) {
|
|
912
|
-
if (!VALID_KINDS.has(body.kind)) {
|
|
913
|
-
return badKind(res);
|
|
914
|
-
}
|
|
915
954
|
try {
|
|
916
955
|
updated = await db.updateTaskKind(id, body.kind);
|
|
917
956
|
if (!updated) return res.fail('task_not_found', 404);
|
|
@@ -921,9 +960,6 @@ module.exports = function buildTasksRouter() {
|
|
|
921
960
|
}
|
|
922
961
|
}
|
|
923
962
|
if (hasDiscipline) {
|
|
924
|
-
if (!VALID_DISCIPLINES.has(body.discipline)) {
|
|
925
|
-
return res.fail('bad_discipline', 400, { valid: Array.from(VALID_DISCIPLINES) });
|
|
926
|
-
}
|
|
927
963
|
try {
|
|
928
964
|
updated = await db.updateTaskDiscipline(id, body.discipline);
|
|
929
965
|
if (!updated) return res.fail('task_not_found', 404);
|
|
@@ -933,13 +969,7 @@ module.exports = function buildTasksRouter() {
|
|
|
933
969
|
}
|
|
934
970
|
}
|
|
935
971
|
if (hasNewcomerOrXenos) {
|
|
936
|
-
// V3.R15 #239: prefer newcomer_friendly (canonical); fall back to
|
|
937
|
-
// xenos_claimable (pre-rename alias). Both map to the same DB column.
|
|
938
972
|
const incoming = hasNewcomerFriendly ? body.newcomer_friendly : body.xenos_claimable;
|
|
939
|
-
if (typeof incoming !== 'boolean') {
|
|
940
|
-
const field = hasNewcomerFriendly ? 'newcomer_friendly' : 'xenos_claimable';
|
|
941
|
-
return res.fail(`bad_${field}`, { status: 400, message: `${field} must be a boolean` });
|
|
942
|
-
}
|
|
943
973
|
try {
|
|
944
974
|
updated = await db.updateTaskNewcomerFriendly(id, incoming);
|
|
945
975
|
if (!updated) return res.fail('task_not_found', 404);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.4",
|
|
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.19.
|
|
3
|
+
"version": "1.19.4",
|
|
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",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/bump-version.js — the mechanical CORE_VERSION patch bump for the
|
|
3
|
+
// publish-on-merge lane (ADR 0161, task 1002620).
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS EXISTS. ADR 0161 makes every merge to main publishable: when a merge
|
|
6
|
+
// lands without a human version bump, CI bumps a PATCH itself so the release
|
|
7
|
+
// has a unique version. The bump edits src/module-api.js in the two places the
|
|
8
|
+
// repo's version discipline lives — the `const CORE_VERSION` literal and the
|
|
9
|
+
// changelog comment block above it — because a bare literal change with no
|
|
10
|
+
// changelog line is exactly the drift the discipline exists to prevent.
|
|
11
|
+
//
|
|
12
|
+
// POLICY (deliberately narrow): this script only ever bumps PATCH. A MINOR or
|
|
13
|
+
// MAJOR is a statement about the doorway surface that only a human (or a far
|
|
14
|
+
// smarter check) can make — it stays an in-task hand edit, exactly as before.
|
|
15
|
+
//
|
|
16
|
+
// SHAPE-PRESERVING: deploy-divergence.js finds bump commits with
|
|
17
|
+
// `git log -G"^const CORE_VERSION = "` and reads the literal with a regex; the
|
|
18
|
+
// rewrite here keeps that exact line shape. tests/bump_version.mjs pins this.
|
|
19
|
+
//
|
|
20
|
+
// Usage:
|
|
21
|
+
// node scripts/gds/bump-version.js # dry-run: print the plan
|
|
22
|
+
// node scripts/gds/bump-version.js --apply # rewrite src/module-api.js
|
|
23
|
+
// node scripts/gds/bump-version.js --apply --note "run 123456" # extra provenance
|
|
24
|
+
// stdout (both modes): one JSON line { from, to, applied } for CI to capture.
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const fs = require('node:fs');
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
|
|
31
|
+
const TARGET = path.join(__dirname, '..', '..', 'src', 'module-api.js');
|
|
32
|
+
const CONST_RE = /^const CORE_VERSION = '(\d+)\.(\d+)\.(\d+)';.*$/m;
|
|
33
|
+
// The changelog block's closing divider sits immediately above the const line.
|
|
34
|
+
const DIVIDER = '// ---------------------------------------------------------------------------';
|
|
35
|
+
|
|
36
|
+
// Pure transform: (source text, optional note) -> { from, to, text }.
|
|
37
|
+
// Throws with a precise message on any shape it does not recognize — a silent
|
|
38
|
+
// mis-edit of the version literal is the worst possible failure mode here.
|
|
39
|
+
function bumpPatch(src, { note = '' } = {}) {
|
|
40
|
+
const m = src.match(CONST_RE);
|
|
41
|
+
if (!m) throw new Error(`bump-version: could not find the CORE_VERSION literal in ${TARGET} (expected: const CORE_VERSION = 'x.y.z';)`);
|
|
42
|
+
const [major, minor, patch] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
43
|
+
const from = `${major}.${minor}.${patch}`;
|
|
44
|
+
const to = `${major}.${minor}.${patch + 1}`;
|
|
45
|
+
|
|
46
|
+
// 1. The changelog entry — inserted just above the divider that closes the
|
|
47
|
+
// changelog block, matching the existing two-line entry style.
|
|
48
|
+
const constLineStart = src.search(CONST_RE);
|
|
49
|
+
const dividerStart = src.lastIndexOf(DIVIDER, constLineStart);
|
|
50
|
+
if (dividerStart === -1) throw new Error('bump-version: could not find the changelog closing divider above CORE_VERSION');
|
|
51
|
+
const noteSuffix = note ? ` ${note}.` : '';
|
|
52
|
+
const entry =
|
|
53
|
+
`// ${to} — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges\n` +
|
|
54
|
+
`// landed since ${from} with no explicit bump.${noteSuffix} (task 1002620)\n`;
|
|
55
|
+
|
|
56
|
+
// 2. The literal — same line shape deploy-divergence greps for.
|
|
57
|
+
const newConstLine = `const CORE_VERSION = '${to}'; // CI auto-patch carrier (ADR 0161); see changelog above`;
|
|
58
|
+
|
|
59
|
+
const text = src.slice(0, dividerStart) + entry + src.slice(dividerStart).replace(CONST_RE, newConstLine);
|
|
60
|
+
return { from, to, text };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function main() {
|
|
64
|
+
const apply = process.argv.includes('--apply');
|
|
65
|
+
const noteIdx = process.argv.indexOf('--note');
|
|
66
|
+
const note = noteIdx !== -1 ? (process.argv[noteIdx + 1] || '') : '';
|
|
67
|
+
|
|
68
|
+
const src = fs.readFileSync(TARGET, 'utf8');
|
|
69
|
+
const { from, to, text } = bumpPatch(src, { note });
|
|
70
|
+
if (apply) fs.writeFileSync(TARGET, text);
|
|
71
|
+
else console.error(`bump-version: would bump ${from} → ${to} (dry-run; pass --apply to write)`);
|
|
72
|
+
console.log(JSON.stringify({ from, to, applied: apply }));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { bumpPatch, CONST_RE };
|
|
76
|
+
if (require.main === module) main();
|
package/scripts/gds/init.js
CHANGED
|
@@ -258,6 +258,13 @@ function buildBrandingConfig(spec) {
|
|
|
258
258
|
firstAdmin: spec.firstAdmin,
|
|
259
259
|
currency: { label: (spec.currency && spec.currency.label) || 'credits', symbol: (spec.currency && spec.currency.symbol) || '' },
|
|
260
260
|
envPrefix: spec.envPrefix || 'CLOUDBONGOS',
|
|
261
|
+
// Every instance gets its OWN session/config dir (task 1002626). Without
|
|
262
|
+
// this, instance-config.js falls back to the shared 'cloudbongos' dir, so
|
|
263
|
+
// every un-branded instance on a machine reads/writes the SAME
|
|
264
|
+
// ~/.config/cloudbongos/gds-session.json — signing in to one silently
|
|
265
|
+
// evicts the CLI session of every other, including the hub. Same slug
|
|
266
|
+
// family as the package name + provisioning slug.
|
|
267
|
+
configDir: spec.configDir ? String(spec.configDir).trim() : slugify(productName, 'instance'),
|
|
261
268
|
};
|
|
262
269
|
if (spec.tagline) branding.copy = { tagline: spec.tagline };
|
|
263
270
|
return branding;
|
package/src/module-api.js
CHANGED
|
@@ -423,8 +423,20 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
423
423
|
// and the Metic-floor string sweep (1002604). The doorway surface is
|
|
424
424
|
// unchanged (an optional request field, no export change) → PATCH.
|
|
425
425
|
// (task 1002624)
|
|
426
|
+
// 1.19.3 — CLI/init ergonomics carrier: `bongos <verb> --help` is answered by
|
|
427
|
+
// the dispatcher instead of blocking on init's interview (1002625),
|
|
428
|
+
// `bongos init` defaults branding configDir from the product slug so
|
|
429
|
+
// instances stop sharing ~/.config/cloudbongos and evicting each
|
|
430
|
+
// other's CLI sessions (1002626), versions.track gains DB DEFAULT
|
|
431
|
+
// 'product' via core_205 so no insert path can reproduce the
|
|
432
|
+
// fresh-instance standup blocker (1002627). Also sweeps PATCH
|
|
433
|
+
// /tasks/:id gate atomicity (1002609) and the publish-on-merge lane
|
|
434
|
+
// (1002620, ADR 0161). git diff 1.19.2..main on this file was empty —
|
|
435
|
+
// the doorway surface is byte-identical → PATCH. (task 1002631)
|
|
436
|
+
// 1.19.4 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
437
|
+
// landed since 1.19.3 with no explicit bump. run 31275959473. (task 1002620)
|
|
426
438
|
// ---------------------------------------------------------------------------
|
|
427
|
-
const CORE_VERSION = '1.19.
|
|
439
|
+
const CORE_VERSION = '1.19.4'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
428
440
|
|
|
429
441
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
430
442
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// tests/bongos_cli.mjs
|
|
2
|
+
//
|
|
3
|
+
// The `bongos` dispatcher's --help contract (task 1002625). The regression that
|
|
4
|
+
// motivates this file: `bongos init --help` used to FORWARD the flag to init.js,
|
|
5
|
+
// which doesn't recognise it and dropped into its interactive interview —
|
|
6
|
+
// printing "Project name?" and blocking forever (fatal in CI). The dispatcher
|
|
7
|
+
// now answers --help/-h itself for every verb whose script has no usage printer
|
|
8
|
+
// of its own; the five that do (selfHelp) keep their richer output, and `exec`
|
|
9
|
+
// always forwards (the flag belongs to the child script).
|
|
10
|
+
//
|
|
11
|
+
// Run: node tests/bongos_cli.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const cli = require('../bin/bongos.js');
|
|
21
|
+
|
|
22
|
+
const BIN = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'bongos.js');
|
|
23
|
+
|
|
24
|
+
let passed = 0;
|
|
25
|
+
let failed = 0;
|
|
26
|
+
function test(name, fn) {
|
|
27
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
28
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Every spawn gets a hard timeout — the exact failure this file guards against
|
|
32
|
+
// is a verb BLOCKING on interactive input, and a hung test is that bug escaping.
|
|
33
|
+
const run = (...args) => spawnSync(process.execPath, [BIN, ...args], {
|
|
34
|
+
encoding: 'utf8', timeout: 30_000, input: '',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ---- pure: the help card + the selfHelp roster ----
|
|
38
|
+
|
|
39
|
+
test('verbHelpText: names the verb, its summary, and the full-help pointer', () => {
|
|
40
|
+
const out = cli.verbHelpText('claim', cli.VERBS.claim);
|
|
41
|
+
assert.match(out, /^bongos claim — /);
|
|
42
|
+
assert.match(out, /Claim a task by id/);
|
|
43
|
+
assert.match(out, /bongos help/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('VERBS: selfHelp is only claimed by scripts that really print their own usage', () => {
|
|
47
|
+
// Verified by reading each script (they all branch on --help/-h to a usage()
|
|
48
|
+
// printer). If a verb is added here, verify its script first — a wrong
|
|
49
|
+
// selfHelp:true recreates the blocking-interview bug for that verb.
|
|
50
|
+
const selfHelp = Object.entries(cli.VERBS).filter(([, d]) => d.selfHelp).map(([v]) => v).sort();
|
|
51
|
+
assert.deepEqual(selfHelp, ['dev', 'module', 'status', 'task', 'upgrade']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ---- spawned: the dispatcher answers --help without touching the script ----
|
|
55
|
+
|
|
56
|
+
test('bongos --help (first arg) prints the dispatcher help, exit 0', () => {
|
|
57
|
+
const r = run('--help');
|
|
58
|
+
assert.equal(r.status, 0, r.stderr);
|
|
59
|
+
assert.match(r.stdout, /bongos — the Cloud Bongos CLI/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('bongos init --help answers instantly — no interview, no blocking (the 1002625 regression)', () => {
|
|
63
|
+
const r = run('init', '--help');
|
|
64
|
+
assert.equal(r.signal, null, 'must not die on the spawn timeout — that means it blocked on input');
|
|
65
|
+
assert.equal(r.status, 0, r.stderr);
|
|
66
|
+
assert.match(r.stdout, /^bongos init — /);
|
|
67
|
+
assert.ok(!/Project name\?/.test(r.stdout + r.stderr), 'the interactive interview must not start');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('bongos claim -h is answered by the dispatcher too (short flag, non-selfHelp verb)', () => {
|
|
71
|
+
const r = run('claim', '-h');
|
|
72
|
+
assert.equal(r.status, 0, r.stderr);
|
|
73
|
+
assert.match(r.stdout, /^bongos claim — /);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('bongos task --help forwards to task.js (selfHelp) and gets its richer usage', () => {
|
|
77
|
+
const r = run('task', '--help');
|
|
78
|
+
assert.equal(r.status, 0, r.stderr);
|
|
79
|
+
assert.ok(!/^bongos task — .*\n\nForwards to/.test(r.stdout), 'must NOT be the dispatcher card');
|
|
80
|
+
assert.match(r.stdout + r.stderr, /new|usage/i, 'task.js prints its own usage');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('bongos exec still refuses a missing path with exit 2 (--help interception must not swallow exec)', () => {
|
|
84
|
+
const r = run('exec');
|
|
85
|
+
assert.equal(r.status, 2);
|
|
86
|
+
assert.match(r.stderr, /package-relative script path/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(`bongos CLI tests: ${passed} passed, ${failed} failed`);
|
|
91
|
+
if (failed > 0) process.exit(1);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// tests/bump_version.mjs — the CI auto-patch bump is shape-preserving (task 1002620).
|
|
2
|
+
//
|
|
3
|
+
// bump-version.js rewrites the single most load-bearing literal in the repo.
|
|
4
|
+
// These tests pin the three properties that must survive any refactor:
|
|
5
|
+
// 1. the bumped literal keeps the exact `^const CORE_VERSION = '…';` shape
|
|
6
|
+
// deploy-divergence.js greps and regex-reads;
|
|
7
|
+
// 2. a changelog entry lands inside the comment block, not after the const;
|
|
8
|
+
// 3. unrecognizable input throws instead of silently mis-editing.
|
|
9
|
+
|
|
10
|
+
import test from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const { bumpPatch, CONST_RE } = require(path.join(ROOT, 'scripts', 'gds', 'bump-version.js'));
|
|
20
|
+
|
|
21
|
+
const SAMPLE = [
|
|
22
|
+
'// prologue',
|
|
23
|
+
'// 1.19.0 — some entry. (task 1)',
|
|
24
|
+
'// 1.19.1 — another entry. (task 2)',
|
|
25
|
+
'// ---------------------------------------------------------------------------',
|
|
26
|
+
"const CORE_VERSION = '1.19.1'; // carrier; see changelog above",
|
|
27
|
+
'',
|
|
28
|
+
'module.exports = { CORE_VERSION };',
|
|
29
|
+
'',
|
|
30
|
+
].join('\n');
|
|
31
|
+
|
|
32
|
+
test('bumps patch and keeps the exact const line shape', () => {
|
|
33
|
+
const { from, to, text } = bumpPatch(SAMPLE);
|
|
34
|
+
assert.equal(from, '1.19.1');
|
|
35
|
+
assert.equal(to, '1.19.2');
|
|
36
|
+
const m = text.match(CONST_RE);
|
|
37
|
+
assert.ok(m, 'bumped text must still match the canonical const regex');
|
|
38
|
+
assert.equal(m[0].startsWith("const CORE_VERSION = '1.19.2';"), true);
|
|
39
|
+
// exactly one const line survives
|
|
40
|
+
assert.equal([...text.matchAll(/^const CORE_VERSION = /gm)].length, 1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('inserts the changelog entry above the divider, mentioning both versions', () => {
|
|
44
|
+
const { text } = bumpPatch(SAMPLE, { note: 'run 42' });
|
|
45
|
+
const dividerAt = text.indexOf('// ------');
|
|
46
|
+
const entryAt = text.indexOf('1.19.2 — CI auto-patch');
|
|
47
|
+
assert.ok(entryAt !== -1, 'entry exists');
|
|
48
|
+
assert.ok(entryAt < dividerAt, 'entry sits inside the changelog block, above the divider');
|
|
49
|
+
assert.match(text, /landed since 1\.19\.1 with no explicit bump\. run 42\./);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('a 9→10 patch rollover carries, not truncates', () => {
|
|
53
|
+
const src = SAMPLE.replace(/1\.19\.1/g, '1.19.9');
|
|
54
|
+
const { to } = bumpPatch(src);
|
|
55
|
+
assert.equal(to, '1.19.10');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('unrecognizable input throws loudly instead of mis-editing', () => {
|
|
59
|
+
assert.throws(() => bumpPatch('nothing here'), /could not find the CORE_VERSION literal/);
|
|
60
|
+
assert.throws(
|
|
61
|
+
() => bumpPatch("const CORE_VERSION = '1.2.3'; // no divider above"),
|
|
62
|
+
/changelog closing divider/
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('the REAL src/module-api.js is bumpable right now (shape not drifted)', () => {
|
|
67
|
+
const real = fs.readFileSync(path.join(ROOT, 'src', 'module-api.js'), 'utf8');
|
|
68
|
+
const { from, to, text } = bumpPatch(real);
|
|
69
|
+
assert.match(from, /^\d+\.\d+\.\d+$/);
|
|
70
|
+
assert.ok(text.includes(`const CORE_VERSION = '${to}';`));
|
|
71
|
+
});
|
package/tests/init.mjs
CHANGED
|
@@ -62,6 +62,25 @@ test('buildBrandingConfig: no tagline → no copy block', () => {
|
|
|
62
62
|
assert.equal(init.buildBrandingConfig(spec).copy, undefined);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
// configDir must ALWAYS be set per-instance (task 1002626): the neutral pack's
|
|
66
|
+
// fallback is the shared 'cloudbongos' dir, so an instance that omits it shares
|
|
67
|
+
// ~/.config/cloudbongos/gds-session.json with every other instance on the
|
|
68
|
+
// machine — signing in to one evicts the others' CLI sessions (incl. the hub).
|
|
69
|
+
test('buildBrandingConfig: configDir defaults to the product slug, never the shared fallback', () => {
|
|
70
|
+
const b = init.buildBrandingConfig(fullSpec());
|
|
71
|
+
assert.equal(b.configDir, 'cloud-bongos'); // slug of 'Cloud Bongos'
|
|
72
|
+
const hermes = init.buildBrandingConfig(init.normalizeSpec({
|
|
73
|
+
identity: { productName: 'Hermeslines Marketing' }, repo: { owner: 'o', name: 'n' }, firstAdmin: 'x',
|
|
74
|
+
}));
|
|
75
|
+
assert.equal(hermes.configDir, 'hermeslines-marketing');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('buildBrandingConfig: an explicit spec.configDir wins over the slug', () => {
|
|
79
|
+
const spec = fullSpec();
|
|
80
|
+
spec.configDir = ' custom-dir ';
|
|
81
|
+
assert.equal(init.buildBrandingConfig(spec).configDir, 'custom-dir');
|
|
82
|
+
});
|
|
83
|
+
|
|
65
84
|
// ---- buildModulesConfig ----
|
|
66
85
|
test('buildModulesConfig: every known module present, enabled ones true', () => {
|
|
67
86
|
const m = init.buildModulesConfig(fullSpec()).modules;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// tests/patch_gate_ordering.mjs — PATCH /tasks/:id settles gates before writes
|
|
2
|
+
// (task 1002609, choke-point finding G-22).
|
|
3
|
+
//
|
|
4
|
+
// The bug this pins: the handler used to validate-and-write per field in
|
|
5
|
+
// sequence, so a body mixing a valid early field with a refused later one
|
|
6
|
+
// committed the early write and THEN 403/400'd — a Metic sending
|
|
7
|
+
// { parallel_safe: false, security_sensitive: true } had parallel_safe
|
|
8
|
+
// committed before rank_forbidden. Authorization must be settled before any
|
|
9
|
+
// field is written, and the same holds for every validation refusal.
|
|
10
|
+
//
|
|
11
|
+
// Pinned as source-shape assertions on the PATCH handler slice (the
|
|
12
|
+
// goal_advisory.mjs pattern): every refusal code must appear BEFORE the first
|
|
13
|
+
// db.updateTask* write, and the checker itself is proven non-vacuous against a
|
|
14
|
+
// synthetic counter-example shaped like the old bug.
|
|
15
|
+
//
|
|
16
|
+
// Run: node --test tests/patch_gate_ordering.mjs
|
|
17
|
+
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
|
|
22
|
+
const src = readFileSync(new URL('../modules/lifecycle/routes/tasks.js', import.meta.url), 'utf8');
|
|
23
|
+
|
|
24
|
+
// The PATCH handler slice: router.patch('/tasks/:id' … up to the next route.
|
|
25
|
+
function patchSlice(source) {
|
|
26
|
+
const start = source.indexOf("router.patch('/tasks/:id'");
|
|
27
|
+
assert.ok(start !== -1, 'PATCH /tasks/:id handler not found');
|
|
28
|
+
const end = source.indexOf('router.post(', start);
|
|
29
|
+
assert.ok(end !== -1, 'no route follows the PATCH handler');
|
|
30
|
+
return source.slice(start, end);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// All refusal codes the validation phase can emit, and the first write.
|
|
34
|
+
const REFUSALS = [
|
|
35
|
+
"'rank_forbidden'",
|
|
36
|
+
"'bad_parallel_safe'",
|
|
37
|
+
"'bad_security_sensitive'",
|
|
38
|
+
"'bad_automation_tag'",
|
|
39
|
+
"'bad_module_key'",
|
|
40
|
+
"'bad_goal_id'",
|
|
41
|
+
"'goal_not_found'",
|
|
42
|
+
"'goal_version_mismatch'",
|
|
43
|
+
"'bad_requires_rank'",
|
|
44
|
+
'badKind(res)',
|
|
45
|
+
"'bad_discipline'",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function firstWriteIndex(slice) {
|
|
49
|
+
const m = slice.match(/await db\.updateTask\w+\(/);
|
|
50
|
+
return m ? m.index : -1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function refusalsBeforeFirstWrite(slice) {
|
|
54
|
+
const writeAt = firstWriteIndex(slice);
|
|
55
|
+
if (writeAt === -1) return { ok: false, offenders: ['no db.updateTask* write found'] };
|
|
56
|
+
const offenders = REFUSALS.filter((code) => {
|
|
57
|
+
const at = slice.indexOf(code);
|
|
58
|
+
return at === -1 || at > writeAt;
|
|
59
|
+
});
|
|
60
|
+
return { ok: offenders.length === 0, offenders };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
test('every gate and validation refusal precedes the first db write (G-22)', () => {
|
|
64
|
+
const slice = patchSlice(src);
|
|
65
|
+
const r = refusalsBeforeFirstWrite(slice);
|
|
66
|
+
assert.ok(r.ok, `refusal codes missing or after the first write: ${r.offenders.join(', ')}`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('the Archon gate on security_sensitive specifically precedes the parallel_safe write', () => {
|
|
70
|
+
const slice = patchSlice(src);
|
|
71
|
+
const gate = slice.indexOf("'rank_forbidden'");
|
|
72
|
+
const write = slice.indexOf('await db.updateTaskParallelSafe(');
|
|
73
|
+
assert.ok(gate !== -1 && write !== -1, 'gate or write not found');
|
|
74
|
+
assert.ok(gate < write, 'rank_forbidden must fire before parallel_safe commits — the exact G-22 case');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('write order keeps requires_rank after security_sensitive (derived-floor dependency)', () => {
|
|
78
|
+
const slice = patchSlice(src);
|
|
79
|
+
const ss = slice.indexOf('await db.updateTaskSecuritySensitive(');
|
|
80
|
+
const rr = slice.indexOf('await db.updateTaskRequiresRank(');
|
|
81
|
+
assert.ok(ss !== -1 && rr !== -1, 'expected both writes in the handler');
|
|
82
|
+
assert.ok(ss < rr, 'requires_rank clamps against the derived floor, so its write must follow the security_sensitive write');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('the ordering checker actually CATCHES the old write-then-refuse shape (not a no-op)', () => {
|
|
86
|
+
const oldShape = `router.patch('/tasks/:id', async (req, res) => {
|
|
87
|
+
let updated = null;
|
|
88
|
+
if (hasParallelSafe) {
|
|
89
|
+
if (bad) return res.fail('bad_parallel_safe', 400);
|
|
90
|
+
updated = await db.updateTaskParallelSafe(id, body.parallel_safe);
|
|
91
|
+
}
|
|
92
|
+
if (hasSecuritySensitive) {
|
|
93
|
+
if (!archon) return res.fail('rank_forbidden', 403);
|
|
94
|
+
if (bad) return res.fail('bad_security_sensitive', 400);
|
|
95
|
+
updated = await db.updateTaskSecuritySensitive(id, v);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
router.post('/tasks/:id/promote', () => {});`;
|
|
99
|
+
const r = refusalsBeforeFirstWrite(patchSlice(oldShape));
|
|
100
|
+
assert.equal(r.ok, false, 'the checker must flag a rank gate that follows a write');
|
|
101
|
+
assert.ok(r.offenders.includes("'rank_forbidden'"), 'rank_forbidden after the first write must be the offender');
|
|
102
|
+
});
|