@bongos/core 1.19.276 → 1.19.277
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
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": "
|
|
5
|
+
"core_version": "1.19.277",
|
|
6
|
+
"core_contract": "1.19.277",
|
|
7
|
+
"source_commit": "35b30c3a1c2a68136a144277b48bb750148995a4",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-21T16:28:45.099Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 318,
|
|
13
13
|
"agent_docs_stubbed": 22,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1376,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1716,
|
|
20
|
+
"tree_sha256": "42012dded2c6108d84ebdea4781e6cf78523e2f6c93cf2eebbc16e032b8a33b2",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
{
|
|
88
88
|
"path": ".claude/skills/builder-sequence/SKILL.md",
|
|
89
89
|
"mode": "0000644",
|
|
90
|
-
"sha256": "
|
|
90
|
+
"sha256": "838648912f0f058c58c42d943282346e2c35a90aea507b0ec136e9b0fbda50b0"
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
"path": ".claude/skills/builder-setup/SKILL.md",
|
|
@@ -3872,7 +3872,7 @@
|
|
|
3872
3872
|
{
|
|
3873
3873
|
"path": "modules/lifecycle/ship-card.js",
|
|
3874
3874
|
"mode": "0000644",
|
|
3875
|
-
"sha256": "
|
|
3875
|
+
"sha256": "c08ee0e89f67a2db7ea049353a12bdd6e5d97c6a7ec9039d19bdd797f66c112e"
|
|
3876
3876
|
},
|
|
3877
3877
|
{
|
|
3878
3878
|
"path": "modules/lifecycle/ship-preflight.js",
|
|
@@ -4487,12 +4487,12 @@
|
|
|
4487
4487
|
{
|
|
4488
4488
|
"path": "package-lock.json",
|
|
4489
4489
|
"mode": "0000644",
|
|
4490
|
-
"sha256": "
|
|
4490
|
+
"sha256": "296343fcb5f99b8dc4fb410e8b7c983d8c27da94bb23a64ffae6e246b87634d1"
|
|
4491
4491
|
},
|
|
4492
4492
|
{
|
|
4493
4493
|
"path": "package.json",
|
|
4494
4494
|
"mode": "0000644",
|
|
4495
|
-
"sha256": "
|
|
4495
|
+
"sha256": "0fa3267232afe7bfab282670e735d0cdd313d21614128c7666a10368f302ae2e"
|
|
4496
4496
|
},
|
|
4497
4497
|
{
|
|
4498
4498
|
"path": "public-docs/index.html",
|
|
@@ -6137,7 +6137,7 @@
|
|
|
6137
6137
|
{
|
|
6138
6138
|
"path": "src/module-api.js",
|
|
6139
6139
|
"mode": "0000644",
|
|
6140
|
-
"sha256": "
|
|
6140
|
+
"sha256": "3068c942e43140df9169a36c39acb9b3daffb182d2a7f33e89180b3926abfd37"
|
|
6141
6141
|
},
|
|
6142
6142
|
{
|
|
6143
6143
|
"path": "src/module-loader/catalog.js",
|
|
@@ -8194,6 +8194,11 @@
|
|
|
8194
8194
|
"mode": "0000644",
|
|
8195
8195
|
"sha256": "e76a3baf5fb2cdee08ffe8d80c55630b5ab4f708046f3c63c07c480926ccda31"
|
|
8196
8196
|
},
|
|
8197
|
+
{
|
|
8198
|
+
"path": "tests/session_boundary_handoff.mjs",
|
|
8199
|
+
"mode": "0000644",
|
|
8200
|
+
"sha256": "8789eb59b2c3984500b4b2eb24d1e812e0ce6c970fc6fc940c06a6daf2613ec2"
|
|
8201
|
+
},
|
|
8197
8202
|
{
|
|
8198
8203
|
"path": "tests/session_caps.mjs",
|
|
8199
8204
|
"mode": "0000644",
|
|
@@ -35,7 +35,20 @@ This skill owns the **loop and the halt discipline**. It does not re-derive how
|
|
|
35
35
|
|
|
36
36
|
5. **Ship it** — `/builder-ship`. The task must reach `shipped`, not stall at `completed` or `confirmed`. A ship that does not land has not unblocked anything, so the chain cannot advance; treat it as a halt (see `ship_failed` below).
|
|
37
37
|
|
|
38
|
-
6. **
|
|
38
|
+
6. **Hand the chain off before the next link — the chain gets slower otherwise.** This is the step that makes a long run stay fast, and it is easy to skip because nothing breaks without it.
|
|
39
|
+
|
|
40
|
+
Every turn re-reads the whole conversation, so a chain worked in one window pays more per turn the further down it goes. Measured on a three-task run (2026-08-21): 11.3M cached tokens on the first task, 36.1M on the second, 78.1M on the third. The third task was not twice the work of the second — it re-processed twice the context.
|
|
41
|
+
|
|
42
|
+
A shipped task is the moment almost all of that context turns into dead weight: superseded diffs, resolved test failures, closed grade rounds. **The chain's state is already durable** — dependencies and claims live in the GDS, and `--next` re-derives the plan from them — so a fresh window loses nothing that matters.
|
|
43
|
+
|
|
44
|
+
So, when the ship card shows a **heavy session** (it says so itself: `⚠ heavy session (NNM cached) — run /session-handoff…`), or when you have landed **three or more links** in one window:
|
|
45
|
+
|
|
46
|
+
- Run `/session-handoff`, scoped to the REMAINDER of the chain — the next task id, the chain's tail, and any decision from the links already landed that the rest depends on. Do not summarise the finished work in detail; it is shipped, and the GDS is its record.
|
|
47
|
+
- Hand the user that prompt and say plainly that the chain continues in a fresh session. Then **stop.** Do not claim the next task in this window.
|
|
48
|
+
|
|
49
|
+
`/session-handoff` composes text and changes nothing, so this is safe at any point — and it cannot be automated away: a hook cannot clear a conversation, which is exactly why this step is the runner's job.
|
|
50
|
+
|
|
51
|
+
7. **Otherwise, go back to step 2.** The ship you just landed is what promotes the next task to `ready`.
|
|
39
52
|
|
|
40
53
|
Between passes, report progress in one line — which task landed, what is next, how much of the chain remains. The user is following a long run; they should never have to ask where it is.
|
|
41
54
|
|
|
@@ -73,6 +86,8 @@ node scripts/gds/sequence.js --check <task-id>
|
|
|
73
86
|
|
|
74
87
|
## What NOT to do
|
|
75
88
|
|
|
89
|
+
- **Do not run a long chain to exhaustion in one window** to avoid the handoff in step 6. It feels like progress and it is the single biggest reason a long run drags: the last link can cost several times the first for identical work. Handing off is not an interruption of the chain; it is how the chain stays cheap.
|
|
90
|
+
|
|
76
91
|
- **Do not skip a halted task and continue down the chain.** The owner chose stop-and-report precisely so a blocked predecessor does not silently strand its dependents.
|
|
77
92
|
- **Do not batch-claim.** See "The one rule".
|
|
78
93
|
- **Do not widen a task's `touches[]`** to make the chain flow. File a new task instead (`node scripts/gds/capture.js`).
|
|
@@ -174,6 +174,59 @@ function nextActionFor(stage, { taskId, discipline, sandboxUrl } = {}) {
|
|
|
174
174
|
return { kind: 'prompt', label: 'Continue', cmd: '/builder-start' };
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// ---- the session boundary (task 1003150) -----------------------------------
|
|
178
|
+
//
|
|
179
|
+
// A shipped task is the one moment the system KNOWS a session's work is finished,
|
|
180
|
+
// and it is the only moment when almost everything in that session's context
|
|
181
|
+
// becomes dead weight: superseded diffs, resolved test failures, closed grade
|
|
182
|
+
// rounds. Carrying it into the next task costs real time, because every turn
|
|
183
|
+
// re-reads the whole conversation.
|
|
184
|
+
//
|
|
185
|
+
// MEASURED, session of 2026-08-21 — three tasks shipped in one window:
|
|
186
|
+
// task 1003148 262K new + 11.3M cached
|
|
187
|
+
// task 1003065 582K new + 36.1M cached
|
|
188
|
+
// task 1003066 891K new + 78.1M cached
|
|
189
|
+
// The third was not twice the work of the second; it re-processed twice the
|
|
190
|
+
// context. The builder's own report was that everything felt slow, and this was
|
|
191
|
+
// the largest single contributor.
|
|
192
|
+
//
|
|
193
|
+
// WHY THIS LIVES ON THE CARD AND NOT IN A HOOK. A hook cannot clear a
|
|
194
|
+
// conversation, and /session-handoff is read-only by its own spec ("composes text
|
|
195
|
+
// and changes no state") — it has to be invoked. The harness does auto-compact on
|
|
196
|
+
// context PRESSURE, but that fires on size rather than meaning: as likely to
|
|
197
|
+
// compact mid-task, where the detail is load-bearing, as after a ship, where it is
|
|
198
|
+
// not. The ship card is the only component that is already running, already at the
|
|
199
|
+
// boundary, and already printing to the builder — so the nudge costs one line and
|
|
200
|
+
// no new machinery.
|
|
201
|
+
//
|
|
202
|
+
// THE THRESHOLD is deliberately generous. The advice is only useful if it is rare:
|
|
203
|
+
// a line that prints on every ship is wallpaper (the deploy-divergence footer is
|
|
204
|
+
// the cautionary tale — 21 core versions of identical warning that nobody acted
|
|
205
|
+
// on). 25M cached sits between the 11.3M session that felt fine and the 36.1M one
|
|
206
|
+
// that had begun to drag, so a short session never sees it.
|
|
207
|
+
const HEAVY_SESSION_CACHED_TOKENS = 25_000_000;
|
|
208
|
+
|
|
209
|
+
// handoffAdvice — PURE. null when there is nothing to say (the overwhelming
|
|
210
|
+
// majority of ships), else the one-line nudge. Exported so every branch is pinned
|
|
211
|
+
// by a DB-free unit test.
|
|
212
|
+
//
|
|
213
|
+
// Gated on a TERMINAL stage, not merely a big number: mid-task the context is what
|
|
214
|
+
// the work is standing on, and telling someone to reset in the middle of a claim is
|
|
215
|
+
// actively wrong. 'shipped' and 'confirmed' are the two stages where the task is
|
|
216
|
+
// out of the builder's hands.
|
|
217
|
+
function handoffAdvice(input) {
|
|
218
|
+
// Normalise BEFORE destructuring. A default parameter only fires on `undefined`,
|
|
219
|
+
// so an explicit `null` — what a caller spreading an absent object hands over —
|
|
220
|
+
// would throw. This runs at the very end of a successful ship, so a crash here
|
|
221
|
+
// would turn a landed task into a traceback. (Same trap as decideLanding in
|
|
222
|
+
// modules/ideas/routing.js, task 1003065 — it caught me twice in one session.)
|
|
223
|
+
const { stage, cachedTokens } = input || {};
|
|
224
|
+
if (stage !== 'shipped' && stage !== 'confirmed') return null;
|
|
225
|
+
const cached = Number(cachedTokens);
|
|
226
|
+
if (!Number.isFinite(cached) || cached < HEAVY_SESSION_CACHED_TOKENS) return null;
|
|
227
|
+
return `heavy session (${fmtTokens(cached)} cached) — run /session-handoff, then start the next task in a fresh session`;
|
|
228
|
+
}
|
|
229
|
+
|
|
177
230
|
function safeBrand() {
|
|
178
231
|
try {
|
|
179
232
|
const b = branding();
|
|
@@ -219,6 +272,7 @@ function buildCardModel(input = {}) {
|
|
|
219
272
|
|
|
220
273
|
const rows = [];
|
|
221
274
|
const facts = [];
|
|
275
|
+
let handoff = null; // task 1003150 — the session-boundary nudge; null on all but a heavy ship
|
|
222
276
|
const addRow = (k, v) => { if (v != null && v !== '') rows.push({ k, v }); };
|
|
223
277
|
const addFact = (label, value, opts = {}) => { if (value != null && value !== '') facts.push({ label, value, ...opts }); };
|
|
224
278
|
|
|
@@ -319,6 +373,10 @@ function buildCardModel(input = {}) {
|
|
|
319
373
|
|
|
320
374
|
addRow('Value', value); addRow('Changed', changed); addRow('Grade', gradeText);
|
|
321
375
|
addRow('Credits', creditsText); addRow('Usage', usageText); addRow('Deploy', deployText);
|
|
376
|
+
// task 1003150: the session-boundary nudge. Not a row — it qualifies the
|
|
377
|
+
// Next-> action rather than reporting a fact about the task, so both renderers
|
|
378
|
+
// hang it off the action. Usually null.
|
|
379
|
+
handoff = handoffAdvice({ stage, cachedTokens: usage.cachedTokens });
|
|
322
380
|
|
|
323
381
|
addFact('', changed);
|
|
324
382
|
if (gradeText) addFact('grade', gradeText, { cls: gradeCls });
|
|
@@ -345,7 +403,7 @@ function buildCardModel(input = {}) {
|
|
|
345
403
|
notes: input.notes || '',
|
|
346
404
|
description: input.description || '',
|
|
347
405
|
issues,
|
|
348
|
-
rows, facts, action,
|
|
406
|
+
rows, facts, action, handoff,
|
|
349
407
|
};
|
|
350
408
|
}
|
|
351
409
|
|
|
@@ -403,6 +461,9 @@ function renderMonospaceCard(model) {
|
|
|
403
461
|
for (const r of m.rows) kv(r.k, r.v);
|
|
404
462
|
out.push(bar);
|
|
405
463
|
kv('Next →', m.action.kind === 'link' ? (m.action.url || '(sandbox)') : m.action.cmd);
|
|
464
|
+
// task 1003150: aligned under the Next action it qualifies, not as a row — this
|
|
465
|
+
// is guidance about the SESSION, not a fact about the task.
|
|
466
|
+
if (m.handoff) for (const l of wrap(`⚠ ${m.handoff}`, MONO_W - 9)) out.push(' ' + ' '.repeat(9) + l);
|
|
406
467
|
return out.join('\n');
|
|
407
468
|
}
|
|
408
469
|
|
|
@@ -459,6 +520,12 @@ function renderCardHtml(model) {
|
|
|
459
520
|
actionHtml = `<button class="cc-link cc-next" onclick="sendPrompt('${cmd}')">${esc(m.action.label)} ↗</button>`;
|
|
460
521
|
}
|
|
461
522
|
|
|
523
|
+
// task 1003150: the boundary nudge rides beside the action, muted — it is
|
|
524
|
+
// advice, not an alarm, and it must not outshout the Next button.
|
|
525
|
+
const handoffHtml = m.handoff
|
|
526
|
+
? `<span class="cc-sep">·</span><span style="color:var(--color-text-tertiary);">${esc(m.handoff)}</span>`
|
|
527
|
+
: '';
|
|
528
|
+
|
|
462
529
|
const factsSr = m.facts.map((f) => `${f.label ? f.label + ' ' : ''}${f.value}`).join('. ');
|
|
463
530
|
const srText = `${m.cfg.headline(m.taskId)}. ${factsSr}. A collapsible summary expands on click.`;
|
|
464
531
|
|
|
@@ -490,7 +557,7 @@ function renderCardHtml(model) {
|
|
|
490
557
|
${actionHtml}
|
|
491
558
|
</div>
|
|
492
559
|
<div style="display:flex;align-items:baseline;gap:10px;">
|
|
493
|
-
<div style="font-size:13px;color:var(--color-text-secondary);line-height:1.7;flex:1;min-width:0;">${facts}</div>
|
|
560
|
+
<div style="font-size:13px;color:var(--color-text-secondary);line-height:1.7;flex:1;min-width:0;">${facts}${handoffHtml}</div>
|
|
494
561
|
<button class="cc-link cc-sum" aria-label="Toggle task summary" onclick="var s=this.parentElement.nextElementSibling;var o=getComputedStyle(s).display==='none';s.style.display=o?'block':'none';this.querySelector('i').style.transform=o?'rotate(90deg)':'';">Summary <span class="cc-chev"><i class="ti ti-chevron-right" aria-hidden="true"></i></span></button>
|
|
495
562
|
</div>
|
|
496
563
|
<div style="display:none;margin-top:0.6rem;padding-top:0.55rem;border-top:0.5px solid var(--color-border-tertiary);font-size:13px;line-height:1.6;">${summaryBody}</div>
|
|
@@ -538,5 +605,7 @@ module.exports = {
|
|
|
538
605
|
STAGES, TONE, stageFor, buildCardModel, renderMonospaceCard, renderCardHtml, emitCard,
|
|
539
606
|
// exported for unit tests:
|
|
540
607
|
fmtTokens, fmtUsd, fmtInt, fmtMins, nextActionFor, esc, wrap, changedText,
|
|
608
|
+
// task 1003150 — the session-boundary nudge, pure so every branch is pinned.
|
|
609
|
+
handoffAdvice, HEAVY_SESSION_CACHED_TOKENS,
|
|
541
610
|
PIPELINE, FORMS, trailSteps,
|
|
542
611
|
};
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.277",
|
|
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.277",
|
|
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.277",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -979,8 +979,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
979
979
|
// landed since 1.19.274 with no explicit bump. run 32492414871. (task 1002620)
|
|
980
980
|
// 1.19.276 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
981
981
|
// landed since 1.19.275 with no explicit bump. run 32499257959. (task 1002620)
|
|
982
|
+
// 1.19.277 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
983
|
+
// landed since 1.19.276 with no explicit bump. run 32503053532. (task 1002620)
|
|
982
984
|
// ---------------------------------------------------------------------------
|
|
983
|
-
const CORE_VERSION = '1.19.
|
|
985
|
+
const CORE_VERSION = '1.19.277'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
984
986
|
|
|
985
987
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
986
988
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// tests/session_boundary_handoff.mjs
|
|
2
|
+
//
|
|
3
|
+
// THE SESSION BOUNDARY — task 1003150 (goal 1000053 "Session efficiency").
|
|
4
|
+
//
|
|
5
|
+
// The mirror of that goal's session-START series (1002435-1002438): those
|
|
6
|
+
// front-load what a session can know at open; this stops a session dragging what
|
|
7
|
+
// it no longer needs at close. A shipped task is the one moment the system KNOWS
|
|
8
|
+
// the work is finished, and the moment almost all of that context becomes dead
|
|
9
|
+
// weight.
|
|
10
|
+
//
|
|
11
|
+
// TWO HALVES, both pinned here:
|
|
12
|
+
// 1. ship-card.js handoffAdvice — PURE, so every branch is a real assertion with
|
|
13
|
+
// no card, no DB, no session.
|
|
14
|
+
// 2. .claude/skills/builder-sequence/SKILL.md — a DOC-CONTRACT check. The skill
|
|
15
|
+
// is the only place the between-links handoff can live (a hook cannot clear a
|
|
16
|
+
// conversation), and a prose step with nothing asserting it is exactly the
|
|
17
|
+
// kind of instruction that gets tidied away in a later edit. This is the same
|
|
18
|
+
// posture as the fitness doc checks: assert the RULE is stated, not its wording.
|
|
19
|
+
//
|
|
20
|
+
// WHY A THRESHOLD TEST AT ALL. The failure mode here is not a crash, it is
|
|
21
|
+
// wallpaper: advice that prints on every ship stops being read (the
|
|
22
|
+
// deploy-divergence footer is the in-repo cautionary tale — 21 core versions of
|
|
23
|
+
// identical warning that nobody acted on). So "stays silent on a normal ship" is
|
|
24
|
+
// the assertion that matters most, and it is first.
|
|
25
|
+
//
|
|
26
|
+
// Run: node tests/session_boundary_handoff.mjs
|
|
27
|
+
|
|
28
|
+
import { strict as assert } from 'node:assert';
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
import { makeRunner } from './helpers.mjs';
|
|
34
|
+
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
37
|
+
const card = require('../modules/lifecycle/ship-card.js');
|
|
38
|
+
const { handoffAdvice, HEAVY_SESSION_CACHED_TOKENS, buildCardModel, renderMonospaceCard, renderCardHtml } = card;
|
|
39
|
+
|
|
40
|
+
const { test, summary } = makeRunner();
|
|
41
|
+
|
|
42
|
+
// The three real sessions the threshold was calibrated against (2026-08-21).
|
|
43
|
+
const MEASURED = { light: 11.3e6, dragging: 36.1e6, heavy: 78.1e6 };
|
|
44
|
+
|
|
45
|
+
// =========================================================================
|
|
46
|
+
// 1. handoffAdvice — the decision
|
|
47
|
+
// =========================================================================
|
|
48
|
+
|
|
49
|
+
await test('SILENT on an ordinary ship — the whole design rests on this', () => {
|
|
50
|
+
// Advice that fires every time is wallpaper. A short session must never see it.
|
|
51
|
+
for (const cached of [0, 1, 1e6, MEASURED.light, HEAVY_SESSION_CACHED_TOKENS - 1]) {
|
|
52
|
+
assert.equal(handoffAdvice({ stage: 'shipped', cachedTokens: cached }), null,
|
|
53
|
+
`${cached} cached must stay silent`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await test('SPEAKS once the session is genuinely heavy', () => {
|
|
58
|
+
for (const cached of [HEAVY_SESSION_CACHED_TOKENS, MEASURED.dragging, MEASURED.heavy]) {
|
|
59
|
+
const advice = handoffAdvice({ stage: 'shipped', cachedTokens: cached });
|
|
60
|
+
assert.ok(advice, `${cached} cached should advise`);
|
|
61
|
+
assert.match(advice, /session-handoff/, 'and names the command that does something about it');
|
|
62
|
+
assert.match(advice, /fresh session/, 'and says what to do with it');
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await test('the advice quotes the ACTUAL figure, so it is arguable', () => {
|
|
67
|
+
// A builder who disagrees can check the number rather than take it on faith.
|
|
68
|
+
assert.match(handoffAdvice({ stage: 'shipped', cachedTokens: MEASURED.heavy }), /78\.1M cached/);
|
|
69
|
+
assert.match(handoffAdvice({ stage: 'shipped', cachedTokens: MEASURED.dragging }), /36\.1M cached/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await test('NEVER mid-task — resetting inside a claim is actively wrong', () => {
|
|
73
|
+
// Mid-task the context is what the work is standing on. Only a stage where the
|
|
74
|
+
// task has left the builder's hands may advise a reset.
|
|
75
|
+
for (const stage of ['claimed', 'review', 'built', 'completed', 'failed']) {
|
|
76
|
+
assert.equal(handoffAdvice({ stage, cachedTokens: MEASURED.heavy }), null,
|
|
77
|
+
`stage '${stage}' must not advise a reset`);
|
|
78
|
+
}
|
|
79
|
+
// ...and the two that may.
|
|
80
|
+
assert.ok(handoffAdvice({ stage: 'shipped', cachedTokens: MEASURED.heavy }));
|
|
81
|
+
assert.ok(handoffAdvice({ stage: 'confirmed', cachedTokens: MEASURED.heavy }),
|
|
82
|
+
'confirmed counts: the task is out of the builder\'s hands even though it has not landed');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await test('hostile or absent input is silence, never a throw', () => {
|
|
86
|
+
// This runs inside the ship card. A card that CRASHES at the end of a
|
|
87
|
+
// successful ship would turn a landed task into a scary traceback.
|
|
88
|
+
for (const args of [undefined, {}, null, { stage: 'shipped' }, { stage: 'shipped', cachedTokens: null },
|
|
89
|
+
{ stage: 'shipped', cachedTokens: 'lots' }, { stage: 'shipped', cachedTokens: NaN },
|
|
90
|
+
{ stage: 'shipped', cachedTokens: -1 }, { stage: null, cachedTokens: 1e9 }]) {
|
|
91
|
+
assert.doesNotThrow(() => handoffAdvice(args), `${JSON.stringify(args)} must not throw`);
|
|
92
|
+
assert.equal(handoffAdvice(args), null, `${JSON.stringify(args)} ⇒ silence`);
|
|
93
|
+
}
|
|
94
|
+
assert.equal(handoffAdvice(null), null, 'explicit null ⇒ silence, not a crash');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await test('the threshold sits between the session that felt fine and the one that dragged', () => {
|
|
98
|
+
// Documents the calibration as an assertion: if someone retunes the constant,
|
|
99
|
+
// this says what the number was chosen to separate.
|
|
100
|
+
assert.ok(HEAVY_SESSION_CACHED_TOKENS > MEASURED.light,
|
|
101
|
+
'the 11.3M session was fine and must stay quiet');
|
|
102
|
+
assert.ok(HEAVY_SESSION_CACHED_TOKENS < MEASURED.dragging,
|
|
103
|
+
'the 36.1M session had begun to drag and should be caught');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// =========================================================================
|
|
107
|
+
// 2. The card — the advice has to actually reach the builder
|
|
108
|
+
// =========================================================================
|
|
109
|
+
|
|
110
|
+
await test('a heavy ship card carries the nudge, in BOTH renderers', () => {
|
|
111
|
+
const model = buildCardModel({
|
|
112
|
+
stage: 'shipped', taskId: 1003150, title: 'A task',
|
|
113
|
+
usage: { cachedTokens: MEASURED.heavy, newTokens: 891e3, costUsd: 49.81 },
|
|
114
|
+
});
|
|
115
|
+
assert.ok(model.handoff, 'it is on the model');
|
|
116
|
+
const mono = renderMonospaceCard(model);
|
|
117
|
+
assert.match(mono, /heavy session/, 'terminal card shows it');
|
|
118
|
+
assert.match(mono, /Next →/, 'and it sits with the Next action it qualifies');
|
|
119
|
+
// Position matters: the nudge must come AFTER Next, as a qualifier, not above it.
|
|
120
|
+
assert.ok(mono.indexOf('Next →') < mono.indexOf('heavy session'), 'below the action, not shouting over it');
|
|
121
|
+
assert.match(renderCardHtml(model), /heavy session/, 'in-feed card shows it too');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await test('a normal ship card is BYTE-IDENTICAL to before the nudge existed', () => {
|
|
125
|
+
// The regression that matters: every ordinary ship must look exactly as it did.
|
|
126
|
+
const usage = { cachedTokens: MEASURED.light, newTokens: 262e3, costUsd: 8.06 };
|
|
127
|
+
const model = buildCardModel({ stage: 'shipped', taskId: 1003148, title: 'A task', usage });
|
|
128
|
+
assert.equal(model.handoff, null);
|
|
129
|
+
const mono = renderMonospaceCard(model);
|
|
130
|
+
assert.doesNotMatch(mono, /heavy session/);
|
|
131
|
+
assert.doesNotMatch(mono, /session-handoff/);
|
|
132
|
+
assert.doesNotMatch(renderCardHtml(model), /heavy session/);
|
|
133
|
+
// A card with no usage data at all (old breadcrumbs) must also stay clean.
|
|
134
|
+
const bare = buildCardModel({ stage: 'shipped', taskId: 1, title: 'A task' });
|
|
135
|
+
assert.equal(bare.handoff, null);
|
|
136
|
+
assert.doesNotMatch(renderMonospaceCard(bare), /heavy session/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await test('the nudge is not a Usage/Deploy ROW — it is guidance, not a task fact', () => {
|
|
140
|
+
const model = buildCardModel({
|
|
141
|
+
stage: 'shipped', taskId: 1, title: 'A task',
|
|
142
|
+
usage: { cachedTokens: MEASURED.heavy },
|
|
143
|
+
});
|
|
144
|
+
assert.equal(model.rows.some((r) => /heavy session/.test(String(r.v))), false,
|
|
145
|
+
'rows report facts about the TASK; this is advice about the SESSION');
|
|
146
|
+
assert.equal(model.facts.some((f) => /heavy session/.test(String(f.value))), false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// =========================================================================
|
|
150
|
+
// 3. /builder-sequence — the doc contract
|
|
151
|
+
//
|
|
152
|
+
// A hook cannot clear a conversation and /session-handoff is read-only, so the
|
|
153
|
+
// runner skill is the ONLY place the between-links handoff can live. That makes
|
|
154
|
+
// its prose load-bearing, and load-bearing prose needs a test.
|
|
155
|
+
// =========================================================================
|
|
156
|
+
|
|
157
|
+
const SEQ = fs.readFileSync(path.join(ROOT, '.claude/skills/builder-sequence/SKILL.md'), 'utf8');
|
|
158
|
+
|
|
159
|
+
await test('the sequence runner states the between-links handoff step', () => {
|
|
160
|
+
assert.match(SEQ, /\/session-handoff/, 'it names the command');
|
|
161
|
+
assert.match(SEQ, /fresh session/i, 'and that the chain continues in a fresh one');
|
|
162
|
+
assert.match(SEQ, /heavy session/i, 'and the ship-card signal it keys off');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await test('...and tells the runner to STOP rather than claim the next link', () => {
|
|
166
|
+
// The failure mode: a runner that emits the prompt and then helpfully carries on
|
|
167
|
+
// in the same window has done the paperwork and none of the work.
|
|
168
|
+
assert.match(SEQ, /do not claim the next task in this window/i,
|
|
169
|
+
'the halt must be explicit — emitting a prompt and continuing anyway is the trap');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await test('...and says WHY, with the measurement, not just what', () => {
|
|
173
|
+
// An unexplained instruction is the first thing dropped in a later edit.
|
|
174
|
+
assert.match(SEQ, /re-?read/i, 'names the mechanism (context re-read per turn)');
|
|
175
|
+
assert.match(SEQ, /11\.3M|36\.1M|78\.1M/, 'carries the measured figures');
|
|
176
|
+
assert.match(SEQ, /durable|GDS/, 'and why a fresh window is safe: the chain state is durable');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await test('the runner is warned against running a chain to exhaustion', () => {
|
|
180
|
+
assert.match(SEQ, /What NOT to do/, 'the section exists');
|
|
181
|
+
assert.match(SEQ, /exhaustion|one window/i, 'and names the anti-pattern this step prevents');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
summary('session_boundary_handoff');
|