@bongos/core 1.19.686 → 1.19.688
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 +14 -14
- package/docs/module-api-changelog.md +4 -0
- package/modules/lifecycle/routes/claims.js +27 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/cascade-dispatch.js +24 -3
- package/src/module-api.js +1 -1
- package/tests/api_path_404.mjs +51 -2
- package/tests/cascade-dispatch.mjs +78 -1
- package/tests/sound_player.mjs +54 -21
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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": "
|
|
5
|
+
"core_version": "1.19.688",
|
|
6
|
+
"core_contract": "1.19.688",
|
|
7
|
+
"source_commit": "51aa693c35073ab1ce7daa457af2e0e4d7fec69b",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-12T23:37:57.983Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 477,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2626,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "816e4e976c2a1c41669cd3780f49542b0e1cfd53c429b6eed76083d4a5d9ca91",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2802,7 +2802,7 @@
|
|
|
2802
2802
|
{
|
|
2803
2803
|
"path": "docs/module-api-changelog.md",
|
|
2804
2804
|
"mode": "0000644",
|
|
2805
|
-
"sha256": "
|
|
2805
|
+
"sha256": "ad6fde9308e83470ab3ec5edac298cbf8779592deeb3d3303a3059fdde18c84b"
|
|
2806
2806
|
},
|
|
2807
2807
|
{
|
|
2808
2808
|
"path": "docs/modules-contract.md",
|
|
@@ -5872,7 +5872,7 @@
|
|
|
5872
5872
|
{
|
|
5873
5873
|
"path": "modules/lifecycle/routes/claims.js",
|
|
5874
5874
|
"mode": "0000644",
|
|
5875
|
-
"sha256": "
|
|
5875
|
+
"sha256": "9d4625ddaaa79e8496e05980153aca1a171d79aad3a32f1711be60be3be05955"
|
|
5876
5876
|
},
|
|
5877
5877
|
{
|
|
5878
5878
|
"path": "modules/lifecycle/routes/closeness.js",
|
|
@@ -7782,12 +7782,12 @@
|
|
|
7782
7782
|
{
|
|
7783
7783
|
"path": "package-lock.json",
|
|
7784
7784
|
"mode": "0000644",
|
|
7785
|
-
"sha256": "
|
|
7785
|
+
"sha256": "9d60af5f5140c304e6e4246a248d24756f4aed5c467d8939957243e41104e1e4"
|
|
7786
7786
|
},
|
|
7787
7787
|
{
|
|
7788
7788
|
"path": "package.json",
|
|
7789
7789
|
"mode": "0000644",
|
|
7790
|
-
"sha256": "
|
|
7790
|
+
"sha256": "84191b0fabaca6bd99dc874989f2c2a74e6574e9e7ee1b74345d05fbbc50650e"
|
|
7791
7791
|
},
|
|
7792
7792
|
{
|
|
7793
7793
|
"path": "public-docs/index.html",
|
|
@@ -9287,7 +9287,7 @@
|
|
|
9287
9287
|
{
|
|
9288
9288
|
"path": "src/bongos/cascade-dispatch.js",
|
|
9289
9289
|
"mode": "0000644",
|
|
9290
|
-
"sha256": "
|
|
9290
|
+
"sha256": "7290d5d15b4c8283ebb17e28b17739b9dc5de4da3e916e9491248ba8acc24619"
|
|
9291
9291
|
},
|
|
9292
9292
|
{
|
|
9293
9293
|
"path": "src/bongos/db-kernel.js",
|
|
@@ -9547,7 +9547,7 @@
|
|
|
9547
9547
|
{
|
|
9548
9548
|
"path": "src/module-api.js",
|
|
9549
9549
|
"mode": "0000644",
|
|
9550
|
-
"sha256": "
|
|
9550
|
+
"sha256": "0cf41cdf014bc501350140b91d3cffe8c42cdb144375e53e8efdcc67c3f5ebb2"
|
|
9551
9551
|
},
|
|
9552
9552
|
{
|
|
9553
9553
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9702,7 +9702,7 @@
|
|
|
9702
9702
|
{
|
|
9703
9703
|
"path": "tests/api_path_404.mjs",
|
|
9704
9704
|
"mode": "0000644",
|
|
9705
|
-
"sha256": "
|
|
9705
|
+
"sha256": "82ca3700f6dd9d0aa88b39a0d6c45a03c20628201508b619e36a6d77ce0215ce"
|
|
9706
9706
|
},
|
|
9707
9707
|
{
|
|
9708
9708
|
"path": "tests/app_pair.mjs",
|
|
@@ -10072,7 +10072,7 @@
|
|
|
10072
10072
|
{
|
|
10073
10073
|
"path": "tests/cascade-dispatch.mjs",
|
|
10074
10074
|
"mode": "0000644",
|
|
10075
|
-
"sha256": "
|
|
10075
|
+
"sha256": "5a537d6c4a3bcde720b95420fb41224f5169ae578d56b8d80f55043ddaa8113e"
|
|
10076
10076
|
},
|
|
10077
10077
|
{
|
|
10078
10078
|
"path": "tests/catalog_only_client.mjs",
|
|
@@ -12757,7 +12757,7 @@
|
|
|
12757
12757
|
{
|
|
12758
12758
|
"path": "tests/sound_player.mjs",
|
|
12759
12759
|
"mode": "0000644",
|
|
12760
|
-
"sha256": "
|
|
12760
|
+
"sha256": "8d65bc64902312731e6b194af5be6336bcb59882adb930299cf43cf083977ca1"
|
|
12761
12761
|
},
|
|
12762
12762
|
{
|
|
12763
12763
|
"path": "tests/sound_prefs.mjs",
|
|
@@ -1831,5 +1831,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1831
1831
|
landed since 1.19.684 with no explicit bump. run 34719298791. (task 1002620)
|
|
1832
1832
|
1.19.686 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1833
1833
|
landed since 1.19.685 with no explicit bump. run 34721104118. (task 1002620)
|
|
1834
|
+
1.19.687 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1835
|
+
landed since 1.19.686 with no explicit bump. run 34724811506. (task 1002620)
|
|
1836
|
+
1.19.688 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1837
|
+
landed since 1.19.687 with no explicit bump. run 34725902871. (task 1002620)
|
|
1834
1838
|
---------------------------------------------------------------------------
|
|
1835
1839
|
```
|
|
@@ -68,6 +68,28 @@ function taskNotReadyHint(taskId, currentStatus) {
|
|
|
68
68
|
: `Task #${idNum} is '${currentStatus}', not 'ready' — only 'ready' tasks are claimable (promote applies to 'backlog': POST /api/bongos/tasks/${idNum}/promote).`;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// task 1003768: the way-forward hint on a REBASE_REQUIRED 409 (ADR 0120 part 3's
|
|
72
|
+
// owed-rebase claim gate). Pure + exported so the wording is pinned by a test,
|
|
73
|
+
// the same contract taskNotReadyHint above carries.
|
|
74
|
+
//
|
|
75
|
+
// WHY THE MENU CHANGED. The old wording offered "(rebase its branch + re-ship, or
|
|
76
|
+
// /merge-mode)". /merge-mode cannot run on a builder machine with no GitHub push
|
|
77
|
+
// credential — and that is the DOCUMENTED DEFAULT posture, not a misconfiguration
|
|
78
|
+
// (ADR 0031 §6 / ADR 0055: landing is server-mediated, /builder-ship uploads a
|
|
79
|
+
// bundle and the SERVER pushes). Meanwhile /strand-fix — whose own frontmatter
|
|
80
|
+
// says it is "the fix for a claim refused 409 REBASE_REQUIRED" — went unmentioned.
|
|
81
|
+
// So the one exit that works everywhere was missing and the first one offered
|
|
82
|
+
// fails on auth for most readers. The gate was always clearable; the MENU was
|
|
83
|
+
// wrong, and this hint is the menu.
|
|
84
|
+
//
|
|
85
|
+
// Ordering is the fix: the universally-available command leads, /merge-mode is
|
|
86
|
+
// marked as needing a push credential rather than dropped (it is still the right
|
|
87
|
+
// tool on a credentialed machine), and /builder-release stays as the give-up exit.
|
|
88
|
+
function rebaseRequiredHint(rebaseTasks) {
|
|
89
|
+
const ids = (rebaseTasks || []).map((t) => `task ${t.id}`);
|
|
90
|
+
return `Your queue is gated: ${ids.length} confirmed task(s) await your rebase (${ids.join(', ') || 'unknown'}). Clear it with /strand-fix N (works on any machine), or rebase the branch and re-ship — /merge-mode only if this machine has a GitHub push credential. To give up on one instead, /builder-release it. If CI is simply backed up, waiting is correct: a task already landed clears the gate on its own.`;
|
|
91
|
+
}
|
|
92
|
+
|
|
71
93
|
// task 1642 (+ the worktree-keying fix) — the pure one-active-claim decision.
|
|
72
94
|
// Working two tasks from one WORKING TREE means every commit lands on the single
|
|
73
95
|
// branch that tree owns; /builder-ship publishes the whole branch, so shipping one
|
|
@@ -343,8 +365,7 @@ module.exports = function buildClaimsRouter() {
|
|
|
343
365
|
// ADR 0120 part 3: name the offending task(s) + the two ways to clear the
|
|
344
366
|
// gate, so the builder isn't left guessing why their claim was refused.
|
|
345
367
|
if (err.code === 'REBASE_REQUIRED') {
|
|
346
|
-
|
|
347
|
-
err.hint = `Your queue is gated: ${ids.length} confirmed task(s) await your rebase (${ids.join(', ') || 'unknown'}). Land each (rebase its branch + re-ship, or /merge-mode) — or /builder-release it — then claim new work.`;
|
|
368
|
+
err.hint = rebaseRequiredHint(err.rebaseTasks);
|
|
348
369
|
}
|
|
349
370
|
// task 1002650: name the blocker(s) and the way forward — the same rule
|
|
350
371
|
// the claimable feed enforces, spoken at the gate instead of a bare 409.
|
|
@@ -645,6 +666,10 @@ module.exports = function buildClaimsRouter() {
|
|
|
645
666
|
module.exports.resolveCreatorSessionId = resolveCreatorSessionId;
|
|
646
667
|
// V4.R14 (#952): the TASK_NOT_READY 409 hint — exported so its wording is pinned.
|
|
647
668
|
module.exports.taskNotReadyHint = taskNotReadyHint;
|
|
669
|
+
// task 1003768: the REBASE_REQUIRED 409 hint — exported so its wording is pinned
|
|
670
|
+
// (tests/claim_gate_rebase.mjs). A remediation menu that names an impossible
|
|
671
|
+
// command is how a builder loses twenty minutes at a gate they met with no context.
|
|
672
|
+
module.exports.rebaseRequiredHint = rebaseRequiredHint;
|
|
648
673
|
// task 1642: the pure session-scoped one-active-claim decision — exported for
|
|
649
674
|
// tests/one_active_claim_per_session.mjs (DB-free) and reuse.
|
|
650
675
|
module.exports.shouldRefuseSessionClaim = shouldRefuseSessionClaim;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.688",
|
|
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.688",
|
|
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.688",
|
|
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",
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// exceeding the per-builder ceiling. A mechanical task drafts on haiku; a
|
|
4
4
|
// bug/refactor/feature drafts on sonnet; escalation to a higher rung happens only
|
|
5
5
|
// when the cheaper run (a) fails its expected schema or (b) self-reports low
|
|
6
|
-
// confidence — capped at
|
|
6
|
+
// confidence — capped at one full climb of the ladder (MAX_ESCALATIONS, derived
|
|
7
|
+
// from LADDER so the cap can never sit below the ceiling a builder is allowed to
|
|
8
|
+
// pick; task 1003664). The cascade operates UNDER the builder's
|
|
7
9
|
// configured model ceiling (skill-prefs.getForBuilder, migration 037) — it can
|
|
8
10
|
// downshift below it, never above.
|
|
9
11
|
//
|
|
@@ -25,14 +27,33 @@
|
|
|
25
27
|
const seams = require('../module-seams');
|
|
26
28
|
|
|
27
29
|
// Cheapest → dearest. Indices are "rungs"; the cascade only ever moves UP.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
// `fable` tops the ladder: it is this instance's top-model default and prices
|
|
31
|
+
// ABOVE opus ($10/$50 vs $5/$25 — llm-pricing.js PRICES), and the allowed-tier
|
|
32
|
+
// list the settings UI writes agrees (modules/builder-settings/model-alloc-prefs.js
|
|
33
|
+
// ALLOWED_TIERS). Without the rung, `claude-fable-5` fell through rungIndex's
|
|
34
|
+
// unknown-model default and a fable CEILING silently clamped the cascade to
|
|
35
|
+
// sonnet — one rung BELOW opus, the opposite of what that ceiling means
|
|
36
|
+
// (task 1003664).
|
|
37
|
+
const LADDER = ['haiku', 'sonnet', 'opus', 'fable'];
|
|
38
|
+
|
|
39
|
+
// Enough escalations to climb from the CHEAPEST rung to the dearest — the
|
|
40
|
+
// invariant the hardcoded `2` quietly satisfied while the ladder was 3 rungs
|
|
41
|
+
// (haiku→opus is 2 steps) and stopped satisfying the moment fable was added: a
|
|
42
|
+
// 'chore' starting on haiku with a fable ceiling would have run out of
|
|
43
|
+
// escalations at opus and stopped one rung short, which is this task's own bug
|
|
44
|
+
// wearing a different start rung. Derived, so a fifth rung cannot reintroduce it.
|
|
45
|
+
// Per-call `maxEscalations` is unchanged and still overrides this.
|
|
46
|
+
const MAX_ESCALATIONS = LADDER.length - 1;
|
|
30
47
|
|
|
31
48
|
function rungIndex(model) {
|
|
32
49
|
const m = String(model || '').toLowerCase();
|
|
33
50
|
if (m.includes('haiku')) return 0;
|
|
34
51
|
if (m.includes('sonnet')) return 1;
|
|
35
52
|
if (m.includes('opus')) return 2;
|
|
53
|
+
// fable and mythos are ONE family at one price — the same pairing llm-pricing's
|
|
54
|
+
// familyFor() and grader-model's MODEL_FAMILY_TOKENS already make, so mythos
|
|
55
|
+
// must not fall through to the unknown default either.
|
|
56
|
+
if (m.includes('fable') || m.includes('mythos')) return 3;
|
|
36
57
|
return 1; // unknown / 'script' / null → middle (sonnet); callers pass a real ceiling
|
|
37
58
|
}
|
|
38
59
|
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.688'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/api_path_404.mjs
CHANGED
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
// hint, never the HTML island page (which JSON.parse-faulted on the client).
|
|
7
7
|
// 2. A TASK_NOT_READY 409 names the promote route instead of leaving the caller
|
|
8
8
|
// to spelunk for it (#506).
|
|
9
|
+
// 3. A REBASE_REQUIRED 409 offers exits the reader's machine can actually take
|
|
10
|
+
// (task 1003768) — it used to lead with /merge-mode, which needs a GitHub push
|
|
11
|
+
// credential most builder machines deliberately lack.
|
|
9
12
|
//
|
|
10
|
-
//
|
|
13
|
+
// All three are pinned via the pure exported helpers (no server, no DB).
|
|
11
14
|
//
|
|
12
15
|
// Run: node tests/api_path_404.mjs
|
|
13
16
|
|
|
@@ -16,7 +19,7 @@ import { createRequire } from 'node:module';
|
|
|
16
19
|
|
|
17
20
|
const require = createRequire(import.meta.url);
|
|
18
21
|
const { isApiShapedPath, apiPathHint, apiNotFoundBody } = require('../src/bongos/api-path-404.js');
|
|
19
|
-
const { taskNotReadyHint } = require('../modules/lifecycle/routes/claims.js');
|
|
22
|
+
const { taskNotReadyHint, rebaseRequiredHint } = require('../modules/lifecycle/routes/claims.js');
|
|
20
23
|
|
|
21
24
|
let passed = 0;
|
|
22
25
|
let failed = 0;
|
|
@@ -95,5 +98,51 @@ t('taskNotReadyHint: a non-backlog status is explained + still names promote', (
|
|
|
95
98
|
assert.match(h, /POST \/api\/bongos\/tasks\/953\/promote/);
|
|
96
99
|
});
|
|
97
100
|
|
|
101
|
+
// ---- rebaseRequiredHint (the 409) ------------------------------------------
|
|
102
|
+
// task 1003768. This gate fires at SESSION START and blocks every new claim, so a
|
|
103
|
+
// builder meets it before they have any context — which makes each command it names
|
|
104
|
+
// load-bearing. Pinned here rather than in tests/claim_gate_rebase.mjs because that
|
|
105
|
+
// suite needs Postgres and sits in the INTEGRATION set, so a pin there would never
|
|
106
|
+
// run in the gate that guards main.
|
|
107
|
+
t('rebaseRequiredHint: leads with /strand-fix, the exit that works on any machine', () => {
|
|
108
|
+
const h = rebaseRequiredHint([{ id: 1003529 }]);
|
|
109
|
+
const strand = h.indexOf('/strand-fix');
|
|
110
|
+
const mergeMode = h.indexOf('/merge-mode');
|
|
111
|
+
// Both indices are asserted present before they are compared: indexOf returns -1
|
|
112
|
+
// for a missing needle, and -1 < anything, so a bare `strand < mergeMode` would
|
|
113
|
+
// report PASS on a hint that had dropped /strand-fix entirely.
|
|
114
|
+
assert.ok(strand >= 0, 'the hint must name /strand-fix');
|
|
115
|
+
assert.ok(mergeMode >= 0, 'this pin assumes /merge-mode is still mentioned; update it if that changes');
|
|
116
|
+
assert.ok(strand < mergeMode, 'the universally-available command must come before the credentialed-machine one');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
t('rebaseRequiredHint: never offers /merge-mode unconditionally', () => {
|
|
120
|
+
const h = rebaseRequiredHint([{ id: 1003529 }]);
|
|
121
|
+
// Landing is server-mediated and most builder machines hold no push credential
|
|
122
|
+
// BY DESIGN (ADR 0031 §6 / ADR 0055) — naming /merge-mode without that condition
|
|
123
|
+
// sends the reader to a command that fails on auth.
|
|
124
|
+
assert.match(h, /push credential/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
t('rebaseRequiredHint: keeps /builder-release as the give-up exit', () => {
|
|
128
|
+
assert.match(rebaseRequiredHint([{ id: 7 }]), /\/builder-release/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
t('rebaseRequiredHint: names every gating task, and degrades to "unknown" on none', () => {
|
|
132
|
+
const h = rebaseRequiredHint([{ id: 11 }, { id: 22 }]);
|
|
133
|
+
assert.match(h, /task 11, task 22/);
|
|
134
|
+
assert.match(h, /2 confirmed task\(s\)/);
|
|
135
|
+
// A gate that cannot name its cause is the failure mode this hint exists to avoid.
|
|
136
|
+
assert.match(rebaseRequiredHint([]), /unknown/);
|
|
137
|
+
assert.match(rebaseRequiredHint(undefined), /unknown/, 'a missing list must not throw');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
t('rebaseRequiredHint: says waiting is correct when CI is merely backed up', () => {
|
|
141
|
+
// learning 1000209: an exhausted Actions budget flags every confirmed task
|
|
142
|
+
// REBASE_REQUIRED, and there the right move is to WAIT — re-shipping or releasing
|
|
143
|
+
// makes it worse. Cheap to say in the menu; not cheaply detectable server-side.
|
|
144
|
+
assert.match(rebaseRequiredHint([{ id: 7 }]), /waiting is correct/);
|
|
145
|
+
});
|
|
146
|
+
|
|
98
147
|
console.log(`\napi_path_404: ${passed} passed, ${failed} failed`);
|
|
99
148
|
process.exit(failed ? 1 : 0);
|
|
@@ -56,7 +56,7 @@ test('ceiling is NEVER exceeded — schema keeps failing but stops at sonnet cei
|
|
|
56
56
|
assert.equal(r.ceiling, 'sonnet');
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
test('
|
|
59
|
+
test('a full climb from the cheapest rung reaches the ceiling (haiku → sonnet → opus)', async () => {
|
|
60
60
|
const neverValid = () => false;
|
|
61
61
|
const runner = mockRunner({});
|
|
62
62
|
const r = await cascade.dispatch({ prompt: 'p', taskKind: 'chore', ceiling: 'opus', expectSchema: neverValid, runner });
|
|
@@ -64,6 +64,19 @@ test('escalation capped at 2 (haiku → sonnet → opus, then stop)', async () =
|
|
|
64
64
|
assert.equal(r.escalations, 2);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
// This one used to be the test above, named for the CAP — but with a 3-rung
|
|
68
|
+
// ladder the cap and the opus ceiling stopped the climb at the same rung, so it
|
|
69
|
+
// could not tell which one had acted. Drive the cap explicitly instead.
|
|
70
|
+
test('the escalation cap stops a climb short of the ceiling when it is set below it', async () => {
|
|
71
|
+
const neverValid = () => false;
|
|
72
|
+
const runner = mockRunner({});
|
|
73
|
+
const r = await cascade.dispatch({
|
|
74
|
+
prompt: 'p', taskKind: 'chore', ceiling: 'fable', expectSchema: neverValid, runner, maxEscalations: 1,
|
|
75
|
+
});
|
|
76
|
+
assert.deepEqual(runner.calls, ['haiku', 'sonnet'], 'one escalation, then stop — well below the fable ceiling');
|
|
77
|
+
assert.equal(r.escalations, 1);
|
|
78
|
+
});
|
|
79
|
+
|
|
67
80
|
test('self-reported low confidence triggers escalation', async () => {
|
|
68
81
|
const runner = mockRunner({ sonnet: LOWCONF, opus: GOOD });
|
|
69
82
|
const r = await cascade.dispatch({ prompt: 'p', taskKind: 'feature', ceiling: 'opus', runner });
|
|
@@ -88,6 +101,70 @@ test('dispatch requires an injected runner', async () => {
|
|
|
88
101
|
await assert.rejects(() => cascade.dispatch({ prompt: 'p', taskKind: 'bug', ceiling: 'opus' }), /requires an injected runner/);
|
|
89
102
|
});
|
|
90
103
|
|
|
104
|
+
// --- the fable rung (task 1003664) ------------------------------------------
|
|
105
|
+
// A ceiling naming a model rungIndex does not recognise falls through to the
|
|
106
|
+
// unknown default (1 = sonnet). For a ceiling that is meant to be the HIGHEST
|
|
107
|
+
// rung that silently clamps the cascade BELOW opus — a downgrade dressed as a
|
|
108
|
+
// ceiling, and invisible because nothing errors.
|
|
109
|
+
|
|
110
|
+
test('a real fable model id resolves to the top rung, not the unknown default', async () => {
|
|
111
|
+
assert.equal(cascade.rungIndex('claude-fable-5'), 3, 'this is the bug: a fable id must not read as sonnet');
|
|
112
|
+
assert.equal(cascade.rungIndex('claude-fable-5-1'), 3);
|
|
113
|
+
assert.equal(cascade.rungIndex('fable'), 3);
|
|
114
|
+
// mythos is the same family at the same price everywhere else in the codebase.
|
|
115
|
+
assert.equal(cascade.rungIndex('claude-mythos-5-1'), 3);
|
|
116
|
+
assert.equal(cascade.rungIndex('mythos'), 3);
|
|
117
|
+
// the rung is above opus, which is the whole point
|
|
118
|
+
assert.ok(cascade.rungIndex('claude-fable-5') > cascade.rungIndex('claude-opus-5'));
|
|
119
|
+
assert.equal(cascade.LADDER[3], 'fable');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('a fable ceiling escalates PAST opus instead of stopping at sonnet', async () => {
|
|
123
|
+
const neverValid = () => false;
|
|
124
|
+
const runner = mockRunner({});
|
|
125
|
+
const r = await cascade.dispatch({
|
|
126
|
+
prompt: 'p', taskKind: 'feature', ceiling: 'claude-fable-5', expectSchema: neverValid, runner,
|
|
127
|
+
});
|
|
128
|
+
assert.deepEqual(runner.calls, ['sonnet', 'opus', 'fable'], 'a fable ceiling must reach fable');
|
|
129
|
+
assert.equal(r.model, 'fable');
|
|
130
|
+
assert.equal(r.ceiling, 'fable');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// The haiku-start case, which the sonnet-start test above cannot see. A 4-rung
|
|
134
|
+
// ladder needs 3 escalations to climb end-to-end; while MAX_ESCALATIONS was the
|
|
135
|
+
// hardcoded 2 that a 3-rung ladder happened to satisfy, this stopped at opus —
|
|
136
|
+
// the same "ceiling that doesn't reach its ceiling" bug, one start rung over.
|
|
137
|
+
test('a chore with a fable ceiling climbs the WHOLE ladder, not just two rungs', async () => {
|
|
138
|
+
const neverValid = () => false;
|
|
139
|
+
const runner = mockRunner({});
|
|
140
|
+
const r = await cascade.dispatch({
|
|
141
|
+
prompt: 'p', taskKind: 'chore', ceiling: 'claude-fable-5', expectSchema: neverValid, runner,
|
|
142
|
+
});
|
|
143
|
+
assert.deepEqual(runner.calls, ['haiku', 'sonnet', 'opus', 'fable'], 'the cap must not sit below the ceiling');
|
|
144
|
+
assert.equal(r.model, 'fable');
|
|
145
|
+
assert.equal(r.escalations, 3);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('the default cap always allows one full climb, whatever the ladder length', async () => {
|
|
149
|
+
assert.equal(cascade.MAX_ESCALATIONS, cascade.LADDER.length - 1, 'derive it — a fifth rung must not reintroduce the bug');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('an opus ceiling still never reaches fable (the new rung is not a new floor)', async () => {
|
|
153
|
+
const neverValid = () => false;
|
|
154
|
+
const runner = mockRunner({});
|
|
155
|
+
const r = await cascade.dispatch({
|
|
156
|
+
prompt: 'p', taskKind: 'feature', ceiling: 'claude-opus-5', expectSchema: neverValid, runner,
|
|
157
|
+
});
|
|
158
|
+
assert.ok(!runner.calls.includes('fable'), 'fable must never be called above an opus ceiling');
|
|
159
|
+
assert.equal(r.model, 'opus');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('an unknown model id still falls back to the safe middle', async () => {
|
|
163
|
+
assert.equal(cascade.rungIndex('script'), 1, 'the documented fallback is unchanged');
|
|
164
|
+
assert.equal(cascade.rungIndex(null), 1);
|
|
165
|
+
assert.equal(cascade.rungIndex(''), 1);
|
|
166
|
+
});
|
|
167
|
+
|
|
91
168
|
// settle async tests then report
|
|
92
169
|
setTimeout(() => {
|
|
93
170
|
console.log(`\ncascade-dispatch: ${passed} passed, ${failed} failed`);
|
package/tests/sound_player.mjs
CHANGED
|
@@ -355,29 +355,62 @@ test('playerFor never throws and returns null or a [cmd, args] pair', () => {
|
|
|
355
355
|
// right for the DEFAULT call; it is wrong the moment a caller names a file.
|
|
356
356
|
// ---------------------------------------------------------------------------
|
|
357
357
|
|
|
358
|
+
// Runs in a CHILD with a sandboxed home, for the reason task 1003760 wrote up at
|
|
359
|
+
// length in tests/cli_sessions.mjs: both prefs paths derive from os.homedir(), so
|
|
360
|
+
// in-process this case reads the BUILDER'S OWN settings.
|
|
361
|
+
//
|
|
362
|
+
// That is what made it red (task 1003628). The old guard skipped only when the
|
|
363
|
+
// LEGACY file existed, and then planted one — but it never checked the CANONICAL
|
|
364
|
+
// file, and readPrefs consults legacy only when the canonical read THROWS. On any
|
|
365
|
+
// machine with saved sound settings the canonical file parses, volume 100 comes
|
|
366
|
+
// back, and the final assertion failed 100 !== 11. On a bare CI runner neither file
|
|
367
|
+
// exists, the plant works, and it passed: green where nobody looks, red where
|
|
368
|
+
// everybody does — the exact phrase this file's own comment above uses for the
|
|
369
|
+
// product bug task 1003293 fixed. The product is correct; the test was reading the
|
|
370
|
+
// developer's machine. Sandboxing is what makes the claim testable at all, rather
|
|
371
|
+
// than skipped on every machine that has the state it cares about.
|
|
372
|
+
//
|
|
373
|
+
// Both layers of the 1003760 contract apply: override every variable a platform
|
|
374
|
+
// might read for the home, and FAIL CLOSED in the child if os.homedir() did not
|
|
375
|
+
// land in the sandbox — a silent escape here would plant a prefs file in the
|
|
376
|
+
// builder's real config dir.
|
|
377
|
+
function sandboxEnv(home) {
|
|
378
|
+
const { root } = path.parse(home);
|
|
379
|
+
return {
|
|
380
|
+
...process.env,
|
|
381
|
+
HOME: home,
|
|
382
|
+
USERPROFILE: home, // Windows: what os.homedir() actually reads
|
|
383
|
+
HOMEDRIVE: root.replace(/[\\/]+$/, ''), // legacy Windows fallback pair
|
|
384
|
+
HOMEPATH: home.slice(root.length - 1) || '\\',
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
358
388
|
test('an explicit missing path fails open — it never substitutes another file', () => {
|
|
359
|
-
|
|
360
|
-
// and prove an explicit path still ignores it. Skipped if the real file exists,
|
|
361
|
-
// because this test must never clobber a builder's own settings.
|
|
362
|
-
const legacy = player.LEGACY_PREFS_PATH;
|
|
363
|
-
if (!legacy || fs.existsSync(legacy)) {
|
|
364
|
-
// Nothing to plant, but the invariant is still checkable: an absent explicit
|
|
365
|
-
// path must yield the fail-open default, whatever the legacy file holds.
|
|
366
|
-
const p = player.readPrefs(path.join(os.tmpdir(), `otb-absent-${process.pid}.json`));
|
|
367
|
-
assert.equal(p.volume, 100);
|
|
368
|
-
assert.equal(p.enabled('ship'), true);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
fs.mkdirSync(path.dirname(legacy), { recursive: true });
|
|
372
|
-
fs.writeFileSync(legacy, JSON.stringify({ volume: 11, ship: false }));
|
|
389
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-sound-home-'));
|
|
373
390
|
try {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
391
|
+
const code = `const R=${JSON.stringify(ROOT)};
|
|
392
|
+
const SANDBOX=${JSON.stringify(home)};
|
|
393
|
+
const os=require('node:os'), fs=require('node:fs'), path=require('node:path');
|
|
394
|
+
const assert=require('node:assert/strict');
|
|
395
|
+
if (os.homedir() !== SANDBOX) {
|
|
396
|
+
console.error('ERR sandbox-escape: os.homedir()=' + os.homedir() + ' but the sandbox is ' + SANDBOX
|
|
397
|
+
+ ' — this platform does not take the home override, and planting prefs here would write to the real config dir.');
|
|
398
|
+
process.exit(2);
|
|
399
|
+
}
|
|
400
|
+
const player = require(R + '/.claude/sounds/play.js');
|
|
401
|
+
assert.ok(!fs.existsSync(player.PREFS_PATH), 'the sandbox must start with no canonical prefs file');
|
|
402
|
+
fs.mkdirSync(path.dirname(player.LEGACY_PREFS_PATH), { recursive: true });
|
|
403
|
+
fs.writeFileSync(player.LEGACY_PREFS_PATH, JSON.stringify({ volume: 11, ship: false }));
|
|
404
|
+
const p = player.readPrefs(path.join(os.tmpdir(), 'otb-absent-' + process.pid + '.json'));
|
|
405
|
+
assert.equal(p.volume, 100, 'an explicit path must not inherit the legacy file volume');
|
|
406
|
+
assert.equal(p.enabled('ship'), true, 'an explicit path must not inherit legacy mutes');
|
|
407
|
+
// …while the DEFAULT call still honours it — the fallback is not removed.
|
|
408
|
+
const d = player.readPrefs();
|
|
409
|
+
assert.equal(d.volume, 11, 'the default path must still fall back to the legacy file');
|
|
410
|
+
console.log('OK');`;
|
|
411
|
+
const r = execFileSync(process.execPath, ['-e', code], { encoding: 'utf8', env: sandboxEnv(home) });
|
|
412
|
+
assert.match(r, /OK/, 'the sandboxed child must reach its final assertion');
|
|
380
413
|
} finally {
|
|
381
|
-
fs.
|
|
414
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
382
415
|
}
|
|
383
416
|
});
|