@bongos/core 1.19.994 → 1.19.995

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.994",
6
- "core_contract": "1.19.994",
7
- "source_commit": "2d0c99af93af0f1d1728f6fce1dbce057f209d4c",
5
+ "core_version": "1.19.995",
6
+ "core_contract": "1.19.995",
7
+ "source_commit": "5289c5d78570e086cd6f6d7ee4c154883c7adb78",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-23T22:40:55.441Z",
9
+ "built_at": "2026-09-23T23:25:13.365Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 519,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2404,
14
+ "functional_verbatim": 2405,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2949,
20
- "tree_sha256": "4b408d832b68004c07a3d8ad8143cc76a7a5c40c651ba47878f8ae27230568c6",
19
+ "file_count": 2950,
20
+ "tree_sha256": "ca075a6b55f5c4d672bea732dad07148b251ceceabcea4a3823702e708fc28c7",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2717,7 +2717,7 @@
2717
2717
  {
2718
2718
  "path": "docs/module-api-changelog.md",
2719
2719
  "mode": "0000644",
2720
- "sha256": "aff2b4b93d88dd46eb74dacd0d51c4a959d1a860b9fda1646f3bd2c842780885"
2720
+ "sha256": "42c93e21972b2c4cf9e69574fbd72957a80d03ce51f5a86e6d28579981de11df"
2721
2721
  },
2722
2722
  {
2723
2723
  "path": "docs/modules-contract.md",
@@ -5502,7 +5502,7 @@
5502
5502
  {
5503
5503
  "path": "modules/hall-ui/public/work.css",
5504
5504
  "mode": "0000644",
5505
- "sha256": "68399594a00fdffaf9833d3105d8c9e2f5872642b0f49868edae6cc526fa9021"
5505
+ "sha256": "12b5c9aefa39b8922a5a5a8585a107779bc15678b120cf00239584e28959c604"
5506
5506
  },
5507
5507
  {
5508
5508
  "path": "modules/hall-ui/public/work.html",
@@ -5512,7 +5512,7 @@
5512
5512
  {
5513
5513
  "path": "modules/hall-ui/public/work.js",
5514
5514
  "mode": "0000644",
5515
- "sha256": "ccf5a2bad033e97bf0a3ed1105beac415df9191afd9d5778185ef6da5caee3a9"
5515
+ "sha256": "b645f234ad549dc245a3a4f066b841ddfa6888a619db8ce3816f8e91b3af7a17"
5516
5516
  },
5517
5517
  {
5518
5518
  "path": "modules/hall-ui/public/work.states.json",
@@ -8182,12 +8182,12 @@
8182
8182
  {
8183
8183
  "path": "package-lock.json",
8184
8184
  "mode": "0000644",
8185
- "sha256": "886b77d3e78846e2ac7160ee992c62370feca07cedeac66ae1e71c0f4723f5a7"
8185
+ "sha256": "1b91fb64b2d18219b4433b0ef6589de4488e9d7d78db9a1af45268a1f2a51944"
8186
8186
  },
8187
8187
  {
8188
8188
  "path": "package.json",
8189
8189
  "mode": "0000644",
8190
- "sha256": "c6b01ed3391c36e6a05936826c2926683568412b7d3b15d50deeeb5e970dce15"
8190
+ "sha256": "6b994df4ca85e51d032ee640a4aa4b6dfb57d82581a840eb5de5f05a0b831ee3"
8191
8191
  },
8192
8192
  {
8193
8193
  "path": "public-docs/index.html",
@@ -10232,7 +10232,7 @@
10232
10232
  {
10233
10233
  "path": "src/module-api.js",
10234
10234
  "mode": "0000644",
10235
- "sha256": "bc208fde6a6f87de913925e670e21c54db09f8797b7e0a0f773b5d83d2fe200f"
10235
+ "sha256": "34e569846f695b7f3b69061619a97ab92cda5a4262a67f6c016f31edaf984714"
10236
10236
  },
10237
10237
  {
10238
10238
  "path": "src/module-loader/catalog.js",
@@ -14592,7 +14592,7 @@
14592
14592
  {
14593
14593
  "path": "tests/update_subscription_engine.mjs",
14594
14594
  "mode": "0000644",
14595
- "sha256": "a083174381f10d1cd19795b43580a3113c4f758e4b98da1f7c8e389429cb6a26"
14595
+ "sha256": "636c22e03dfa436c4cb30b59bdeb2d5085cb38e027b32dfc77cc000e4fd6569c"
14596
14596
  },
14597
14597
  {
14598
14598
  "path": "tests/upgrade.mjs",
@@ -14729,6 +14729,11 @@
14729
14729
  "mode": "0000644",
14730
14730
  "sha256": "2dc90311d8a4f31b543ed7ef711b4d3fc8b387a241bf0bbe797ab360107eac38"
14731
14731
  },
14732
+ {
14733
+ "path": "tests/work_title_wrap.mjs",
14734
+ "mode": "0000644",
14735
+ "sha256": "fb42c5fd94bcbd0c968ede7da4026604ff56eed2974be81b370fde08d9fea1df"
14736
+ },
14732
14737
  {
14733
14738
  "path": "tests/workflow_exec_paths.mjs",
14734
14739
  "mode": "0000644",
@@ -2462,5 +2462,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2462
2462
  landed since 1.19.992 with no explicit bump. run 35920872835. (task 1002620)
2463
2463
  1.19.994 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2464
2464
  landed since 1.19.993 with no explicit bump. run 35929622479. (task 1002620)
2465
+ 1.19.995 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2466
+ landed since 1.19.994 with no explicit bump. run 35933457545. (task 1002620)
2465
2467
  ---------------------------------------------------------------------------
2466
2468
  ```
@@ -109,7 +109,9 @@
109
109
  color: var(--ink-faint);
110
110
  font-variant-numeric: tabular-nums; min-width: 3.2em;
111
111
  }
112
- .history__title { flex: 1 1 auto; color: var(--ink); min-width: 0; }
112
+ /* work.js breaks a title after each "/" (a slash-joined run ran 44px past its column
113
+ at 390, task 1004209); `anywhere` is the backstop for a run with no slash at all. */
114
+ .history__title { flex: 1 1 auto; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
113
115
  /* task 1003828 — the goal a backlog row belongs to, on its own line under the
114
116
  title. The SUB-LINE voice (the .scroll__lede tone), not a tag: a tag would read
115
117
  as one more equal chip beside version/discipline/rank, and where a task LIVES
@@ -1050,7 +1050,7 @@
1050
1050
  return `
1051
1051
  <div class="history__row">
1052
1052
  <span class="history__id">#${escapeHtml(String(t.id))}</span>
1053
- <span class="history__title">${escapeHtml(t.title || '(untitled)')} ${ver}</span>
1053
+ <span class="history__title">${escapeHtml(t.title || '(untitled)').replace(/\//g, '/<wbr>')} ${ver}</span>
1054
1054
  <span class="history__when" title="${escapeHtml(fmtDate(t.shipped_at))}">${escapeHtml(when)}</span>
1055
1055
  </div>
1056
1056
  `;
@@ -1115,7 +1115,7 @@
1115
1115
  return `
1116
1116
  <div class="history__row" data-task-id="${escapeHtml(String(t.id))}" title="Open task #${escapeHtml(String(t.id))}">
1117
1117
  <span class="history__id">#${escapeHtml(String(t.id))}</span>
1118
- <span class="history__title">${escapeHtml(t.title || '(untitled)')} ${ver} ${disciplineTag} ${rankTagHtml(t.requires_rank)}<br>${goalTag}</span>
1118
+ <span class="history__title">${escapeHtml(t.title || '(untitled)').replace(/\//g, '/<wbr>')} ${ver} ${disciplineTag} ${rankTagHtml(t.requires_rank)}<br>${goalTag}</span>
1119
1119
  <span class="history__when">${window.OTB.kindIsUnset(t.kind) ? '' : escapeHtml(t.kind)}</span>
1120
1120
  </div>`;
1121
1121
  }).join('');
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.994",
3
+ "version": "1.19.995",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.994",
9
+ "version": "1.19.995",
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.994",
3
+ "version": "1.19.995",
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
@@ -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.994'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.995'; // 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');
@@ -35,6 +35,7 @@
35
35
  import { test } from 'node:test';
36
36
  import assert from 'node:assert/strict';
37
37
  import { createRequire } from 'node:module';
38
+ import { spawnSync } from 'node:child_process';
38
39
  import { mkdtempSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
39
40
  import { tmpdir } from 'node:os';
40
41
  import path from 'node:path';
@@ -475,3 +476,293 @@ test('both commitPin successes report committed:true', () => {
475
476
  assert.equal(S.commitPin({ dir: '/srv/demo', message: 'm' }, fakeGit({ dirty: [] }).run, () => {}).committed, undefined,
476
477
  'an already-clean tree committed nothing and must not be counted as a heal');
477
478
  });
479
+
480
+ // ---------------------------------------------------------------------------
481
+ // 10. the THIRD bucket — what the lane itself generated (task 1003968)
482
+ //
483
+ // pinState sorted a dirty worktree two ways: "the pin" (ours to commit) and
484
+ // "everything else" (a human's work — halt). An upgrade's OWN output is neither,
485
+ // and every bump produces it: materializeClaude rewrites `.claude/` and
486
+ // regenerateApiArtifacts rewrites the OpenAPI spec + typed client. So on any run
487
+ // that did not commit that output, the lane halted on its own exhaust:
488
+ //
489
+ // • pinMode `leave-dirty` — which commits NOTHING by definition — reached
490
+ // healDecision's `other`-first check on every sweep after its first upgrade
491
+ // and returned `halt` for a mode whose entire premise is that the tree may
492
+ // stay dirty. The `by-design` branch beneath it was unreachable.
493
+ // • the direct engine on a core too old for --pin-no-push, where pinFlagsFor
494
+ // correctly withholds every flag and leaves the same output behind.
495
+ //
496
+ // The fix is a provenance answer, not a wider guard: laneGeneratedPaths asks the
497
+ // instance's OWN core what this upgrade rewrites, pinState sorts by it, and
498
+ // commitPin stages that bucket while its refusal stays keyed on everything else.
499
+ // ---------------------------------------------------------------------------
500
+
501
+ // A resolveFrom/load pair standing in for the target instance's core, so the probe is
502
+ // exercised without a second core on disk.
503
+ function fakeCore({ api, willRewrite, upgradeThrows = false, materializeThrows = false } = {}) {
504
+ const seen = [];
505
+ const resolveFrom = (dir, rel) => {
506
+ if (rel === 'scripts/gds/upgrade.js' && api === null) return null;
507
+ if (rel === 'scripts/gds/claude-materialize.js' && willRewrite === null) return null;
508
+ return path.join(dir, 'node_modules', '@bongos', 'core', rel);
509
+ };
510
+ const load = (p) => {
511
+ if (p.endsWith('upgrade.js')) {
512
+ if (upgradeThrows) throw new Error('an older core cannot be loaded here');
513
+ return { API_ARTIFACT_FILES: api || {} };
514
+ }
515
+ if (materializeThrows) throw new Error('materialize blew up');
516
+ return { materializeWillRewrite: willRewrite === undefined ? undefined : (args) => { seen.push(args); return willRewrite; } };
517
+ };
518
+ return { resolveFrom, load, seen };
519
+ }
520
+
521
+ test('laneGeneratedPaths unions the API artifacts with what materialize WOULD rewrite', () => {
522
+ const core = fakeCore({
523
+ api: { 'gen-api-docs.js': ['docs/api/openapi.json'], 'gen-api-client.js': ['clients/bongos-client/index.mjs'] },
524
+ willRewrite: ['.claude/skills/builder-ship/SKILL.md', '.claude/settings.json'],
525
+ });
526
+ assert.deepEqual(S.laneGeneratedPaths('/srv/demo', core).sort(), [
527
+ '.claude/settings.json',
528
+ '.claude/skills/builder-ship/SKILL.md',
529
+ 'clients/bongos-client/index.mjs',
530
+ 'docs/api/openapi.json',
531
+ ]);
532
+ });
533
+
534
+ test('laneGeneratedPaths derives coreRoot from where the module RESOLVED', () => {
535
+ // Not from a package-name guess: an instance may carry the core under any of its
536
+ // published names, and materializeWillRewrite's own default only knows the first.
537
+ const core = fakeCore({ api: {}, willRewrite: [] });
538
+ S.laneGeneratedPaths('/srv/demo', core);
539
+ assert.equal(core.seen.length, 1);
540
+ assert.equal(core.seen[0].instanceDir, '/srv/demo');
541
+ assert.equal(path.resolve(core.seen[0].coreRoot), path.resolve('/srv/demo/node_modules/@bongos/core'));
542
+ });
543
+
544
+ test('laneGeneratedPaths is FAIL-SOFT in every direction — an empty set, never a guessed one', () => {
545
+ // The direction matters. An instance this cannot interrogate keeps the OLD, stricter guard
546
+ // (still wedged, and visibly so), rather than gaining a looser one that commits a stranger's work.
547
+ assert.deepEqual(S.laneGeneratedPaths('/srv/demo', fakeCore({ api: null, willRewrite: null })), []);
548
+ assert.deepEqual(S.laneGeneratedPaths('/srv/demo', fakeCore({ api: {}, willRewrite: undefined })), []);
549
+ assert.deepEqual(S.laneGeneratedPaths('/srv/demo', fakeCore({ upgradeThrows: true, materializeThrows: true })), []);
550
+ // A core that exports API_ARTIFACT_FILES but whose materialize throws still contributes the
551
+ // half it could answer for.
552
+ assert.deepEqual(
553
+ S.laneGeneratedPaths('/srv/demo', fakeCore({ api: { g: ['docs/api/openapi.json'] }, materializeThrows: true })),
554
+ ['docs/api/openapi.json'],
555
+ );
556
+ });
557
+
558
+ test('the core still exports the two things the probe reads', () => {
559
+ // laneGeneratedPaths reads them BY NAME off a module it resolves at runtime, so a rename in the
560
+ // core degrades it silently to the empty set — the pre-fix wedge, back again and invisible.
561
+ // This is the cross-file contract, pinned here the way GO_LIVE_REQUIRED is.
562
+ const up = require('../scripts/gds/upgrade.js');
563
+ const mat = require('../scripts/gds/claude-materialize.js');
564
+ assert.equal(typeof up.API_ARTIFACT_FILES, 'object', 'upgrade.js no longer exports API_ARTIFACT_FILES');
565
+ assert.ok(Object.values(up.API_ARTIFACT_FILES).flat().includes('docs/api/openapi.json'));
566
+ assert.equal(typeof mat.materializeWillRewrite, 'function', 'claude-materialize.js no longer exports materializeWillRewrite');
567
+ });
568
+
569
+ test('pinState sorts the lane\'s own output into its own bucket, and nothing else with it', () => {
570
+ const g = fakeGit({ dirty: ['package.json', '.claude/settings.json', 'config/branding.json'] });
571
+ const st = S.pinState('/srv/demo', g.run, ['.claude/settings.json']);
572
+ assert.deepEqual(st.pin.map((e) => e.file), ['package.json']);
573
+ assert.deepEqual(st.generated.map((e) => e.file), ['.claude/settings.json']);
574
+ assert.deepEqual(st.other.map((e) => e.file), ['config/branding.json'], 'an owner edit is still a human\'s work');
575
+ });
576
+
577
+ test('pinState called with no generated set behaves exactly as it always did', () => {
578
+ // Every pre-existing caller passes two arguments; the third bucket must be opt-in.
579
+ const g = fakeGit({ dirty: ['package.json', '.claude/settings.json'] });
580
+ const st = S.pinState('/srv/demo', g.run);
581
+ assert.deepEqual(st.generated, []);
582
+ assert.deepEqual(st.other.map((e) => e.file), ['.claude/settings.json']);
583
+ });
584
+
585
+ test('healDecision: leave-dirty finally reaches by-design, because the dirt is the lane\'s own', () => {
586
+ const pre = S.pinState('/srv/demo', fakeGit({ dirty: ['package.json', '.claude/settings.json'] }).run, ['.claude/settings.json']);
587
+ assert.deepEqual(S.healDecision(pre, 'leave-dirty'), { action: 'by-design' });
588
+ assert.deepEqual(S.healDecision(pre, 'local-commit'), { action: 'commit', push: false });
589
+ assert.deepEqual(S.healDecision(pre, 'push'), { action: 'commit', push: true });
590
+ });
591
+
592
+ test('healDecision: an owner\'s edit still halts EVERY mode, leave-dirty included', () => {
593
+ // leave-dirty licenses the LANE's dirt, not a stranger's. That is why the order inside
594
+ // healDecision stays dirt-before-mode rather than being flipped.
595
+ const pre = S.pinState('/srv/demo', fakeGit({ dirty: ['package.json', '.claude/settings.json', 'src/thing.js'] }).run, ['.claude/settings.json']);
596
+ for (const mode of S.PIN_MODES) assert.deepEqual(S.healDecision(pre, mode), { action: 'halt' }, `mode ${mode}`);
597
+ });
598
+
599
+ test('preflightFlagsFor: only leave-dirty gets --force, and it gets nothing else', () => {
600
+ assert.deepEqual(S.preflightFlagsFor('leave-dirty'), ['--force']);
601
+ assert.deepEqual(S.preflightFlagsFor('local-commit'), []);
602
+ assert.deepEqual(S.preflightFlagsFor('push'), []);
603
+ assert.deepEqual(S.preflightFlagsFor(undefined), []);
604
+ });
605
+
606
+ test('preflightFlagsFor has the precedent it claims — go-live.js really does send --force', () => {
607
+ const body = readFileSync(path.join(ROOT, 'scripts', 'gds', 'go-live.js'), 'utf8')
608
+ .split('function upgradeCommand(')[1].split('\n}')[0];
609
+ assert.match(body, /'--force'/, 'go-live upgradeCommand() no longer sends --force — re-derive preflightFlagsFor from it');
610
+ });
611
+
612
+ test('the direct-upgrade fallback asks preflightFlagsFor rather than deciding inline', () => {
613
+ const src = readFileSync(path.join(ROOT, '.claude', 'scheduled-tasks', 'core-update-subscription', 'subscribe.js'), 'utf8');
614
+ assert.match(src, /upArgs\.push\(\.\.\.preflightFlagsFor\(pinMode\)\)/,
615
+ 'the clean-tree policy is a pin-mode decision and must go through the named function');
616
+ });
617
+
618
+ test('every commitPin call site in the lane also decides what the upgrade generated', () => {
619
+ // The sibling of the push assertion in section 8, and for the same reason: a site that inherits
620
+ // the `generated = []` default silently reverts to the two-bucket guard that wedged the lane.
621
+ const src = readFileSync(path.join(ROOT, '.claude', 'scheduled-tasks', 'core-update-subscription', 'subscribe.js'), 'utf8');
622
+ const calls = [];
623
+ const re = /commitPin\(\{/g;
624
+ let m;
625
+ while ((m = re.exec(src)) !== null) {
626
+ if (src.slice(Math.max(0, m.index - 9), m.index) === 'function ') continue;
627
+ let depth = 0, end = m.index;
628
+ for (let i = src.indexOf('{', m.index); i < src.length; i++) {
629
+ if (src[i] === '{') depth++;
630
+ else if (src[i] === '}' && --depth === 0) { end = i + 1; break; }
631
+ }
632
+ calls.push(src.slice(m.index, end));
633
+ }
634
+ assert.ok(calls.length >= 3, `expected the three known pin-writing call sites, found ${calls.length}`);
635
+ for (const c of calls) assert.match(c, /generated(: generatedAfter)?,/, `a commitPin call site does not pass the generated set:\n${c}`);
636
+ });
637
+
638
+ // ---------------------------------------------------------------------------
639
+ // 11. real git — the wedge, and the un-wedge
640
+ //
641
+ // The cases above pin each decision on its own; these drive the same three functions
642
+ // against a real repository, because the failure was never inside one decision. It was
643
+ // that all three agreed on a sorting which did not describe what an upgrade leaves
644
+ // behind — and a hand-written `git status` cannot show that.
645
+ // ---------------------------------------------------------------------------
646
+
647
+ function realGit(dir, ...args) { return spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' }); }
648
+ function put(dir, rel, body) {
649
+ const p = path.join(dir, rel);
650
+ mkdirSync(path.dirname(p), { recursive: true });
651
+ writeFileSync(p, body);
652
+ }
653
+
654
+ // A provisioned co-tenant mid-lane: scaffolded + committed, then ONE upgrade applied that moved
655
+ // the pin, re-materialized `.claude/` and regenerated the API artifacts — committing none of it,
656
+ // which is exactly what pinMode leave-dirty asks for.
657
+ function coTenantAfterOneUpgrade() {
658
+ const dir = mkdtempSync(path.join(tmpdir(), 'lane-cotenant-'));
659
+ realGit(dir, 'init', '-q');
660
+ realGit(dir, 'config', 'user.email', 'lane-test@example.invalid');
661
+ realGit(dir, 'config', 'user.name', 'lane test');
662
+ realGit(dir, 'config', 'core.autocrlf', 'false');
663
+ put(dir, 'package.json', JSON.stringify({ dependencies: { '@bongos/core': '1.19.737' } }));
664
+ put(dir, 'package-lock.json', 'lock 1.19.737\n');
665
+ put(dir, '.claude/settings.json', '{"from":"1.19.737"}\n');
666
+ put(dir, '.claude/skills/builder-ship/SKILL.md', '# builder-ship (1.19.737)\n');
667
+ put(dir, 'docs/api/openapi.json', '{"core":"1.19.737"}\n');
668
+ put(dir, 'config/branding.json', '{"identity":{}}\n');
669
+ realGit(dir, 'add', '-A');
670
+ realGit(dir, 'commit', '-q', '-m', 'scaffold');
671
+ // …then one upgrade, with nothing committed after it.
672
+ put(dir, 'package.json', JSON.stringify({ dependencies: { '@bongos/core': '1.19.785' } }));
673
+ put(dir, 'package-lock.json', 'lock 1.19.785\n');
674
+ put(dir, '.claude/settings.json', '{"from":"1.19.785"}\n');
675
+ put(dir, '.claude/skills/builder-ship/SKILL.md', '# builder-ship (1.19.785)\n');
676
+ put(dir, '.claude/skills/new-in-785/SKILL.md', '# a skill this core added\n'); // untracked
677
+ put(dir, 'docs/api/openapi.json', '{"core":"1.19.785"}\n');
678
+ return dir;
679
+ }
680
+
681
+ // What laneGeneratedPaths answers for that upgrade.
682
+ const GENERATED = [
683
+ '.claude/settings.json', '.claude/skills/builder-ship/SKILL.md',
684
+ '.claude/skills/new-in-785/SKILL.md', 'docs/api/openapi.json',
685
+ ];
686
+
687
+ test('real git: the WEDGE — under the old two-way sorting every mode halts, leave-dirty included', () => {
688
+ const dir = coTenantAfterOneUpgrade();
689
+ const pre = S.pinState(dir, spawnSync); // no generated set = the pre-fix behaviour
690
+ assert.ok(pre.other.length, 'the upgrade\'s own output landed in the human-work bucket');
691
+ for (const mode of S.PIN_MODES) {
692
+ assert.deepEqual(S.healDecision(pre, mode), { action: 'halt' }, `mode ${mode} halted here, every sweep, forever`);
693
+ }
694
+ });
695
+
696
+ test('real git: the UN-WEDGE — leave-dirty takes a second sweep with no human hands', () => {
697
+ const dir = coTenantAfterOneUpgrade();
698
+ const pre = S.pinState(dir, spawnSync, GENERATED);
699
+ assert.deepEqual(pre.other, [], 'nothing dirty here is a human\'s work');
700
+ assert.deepEqual(S.healDecision(pre, 'leave-dirty'), { action: 'by-design' });
701
+ // …and the sweep then hands upgrade.js the flag that lets the bump past its own clean-tree
702
+ // pre-flight, which the deliberately-dirty pin would otherwise refuse.
703
+ assert.deepEqual(S.preflightFlagsFor('leave-dirty'), ['--force']);
704
+ });
705
+
706
+ test('real git: local-commit heals the lane\'s own leftovers, tracked and untracked', () => {
707
+ const dir = coTenantAfterOneUpgrade();
708
+ const pre = S.pinState(dir, spawnSync, GENERATED);
709
+ assert.deepEqual(S.healDecision(pre, 'local-commit'), { action: 'commit', push: false });
710
+ const r = S.commitPin({ dir, message: 'heal', push: false, generated: GENERATED }, spawnSync, () => {});
711
+ assert.equal(r.ok, true, r.reason);
712
+ assert.equal(r.action, 'committed-local');
713
+ assert.equal(realGit(dir, 'status', '--porcelain').stdout, '', 'the next sweep finds a clean tree');
714
+ const committed = realGit(dir, 'show', '--name-only', '--format=', 'HEAD').stdout;
715
+ for (const f of ['package.json', 'package-lock.json', ...GENERATED]) {
716
+ assert.ok(committed.includes(f), `${f} rode the pin commit`);
717
+ }
718
+ });
719
+
720
+ test('real git: an OWNER\'s edit is still never swept into the lane\'s commit', () => {
721
+ const dir = coTenantAfterOneUpgrade();
722
+ put(dir, 'config/branding.json', '{"the owner":"was here"}\n'); // tracked
723
+ put(dir, '.claude/skills/my-own-skill/SKILL.md', 'hand-written\n'); // untracked
724
+ const st = S.pinState(dir, spawnSync, GENERATED);
725
+ assert.deepEqual(st.other.map((e) => e.file).sort(), ['.claude/skills/my-own-skill/SKILL.md', 'config/branding.json']);
726
+ const r = S.commitPin({ dir, message: 'heal', push: false, generated: GENERATED }, spawnSync, () => {});
727
+ assert.equal(r.ok, false);
728
+ assert.equal(r.action, 'refused');
729
+ assert.equal(realGit(dir, 'rev-list', '--count', 'HEAD').stdout.trim(), '1', 'nothing was committed at all');
730
+ assert.match(realGit(dir, 'status', '--porcelain').stdout, /config\/branding\.json/, 'and the edit is still theirs');
731
+ });
732
+
733
+ test('real git: a tree holding ONLY the lane\'s own output still commits — the pin need not be dirty', () => {
734
+ // The shape a rollback leaves behind: the pin restored to what is committed, the regenerated
735
+ // artifacts not. Answering `already-clean` here would strand them and wedge the next sweep.
736
+ const dir = coTenantAfterOneUpgrade();
737
+ realGit(dir, 'checkout', '--', 'package.json', 'package-lock.json');
738
+ const st = S.pinState(dir, spawnSync, GENERATED);
739
+ assert.deepEqual(st.pin, []);
740
+ assert.ok(st.generated.length);
741
+ const r = S.commitPin({ dir, message: 'heal', push: false, generated: GENERATED }, spawnSync, () => {});
742
+ assert.equal(r.ok, true, r.reason);
743
+ assert.equal(r.committed, true);
744
+ assert.equal(realGit(dir, 'status', '--porcelain').stdout, '');
745
+ });
746
+
747
+ test('real git: a genuinely clean tree is still a no-op', () => {
748
+ const dir = coTenantAfterOneUpgrade();
749
+ realGit(dir, 'add', '-A'); realGit(dir, 'commit', '-q', '-m', 'by hand');
750
+ const r = S.commitPin({ dir, message: 'heal', push: false, generated: GENERATED }, spawnSync, () => {});
751
+ assert.equal(r.action, 'already-clean');
752
+ assert.equal(r.committed, undefined);
753
+ });
754
+
755
+ test('the post-upgrade net re-probes, rather than reusing the set from before the bump', () => {
756
+ // A skill the NEW core adds is absent from the pre-upgrade answer, so reusing it would drop that
757
+ // file into `other` and wedge the lane one release later — this defect again, wearing the next
758
+ // version number. The heal, which runs BEFORE the bump, must keep using the pre-bump set.
759
+ const src = readFileSync(path.join(ROOT, '.claude', 'scheduled-tasks', 'core-update-subscription', 'subscribe.js'), 'utf8');
760
+ const afterSpawn = src.split('const generatedAfter = laneGeneratedPaths(inst.dir);')[1];
761
+ assert.ok(afterSpawn, 'the post-upgrade re-probe is gone');
762
+ assert.equal((afterSpawn.match(/generated: generatedAfter,/g) || []).length, 2,
763
+ 'both post-upgrade commitPin sites (the rollback tidy and the durability net) must use the re-probed set');
764
+ assert.match(afterSpawn, /pinState\(inst\.dir, spawnSync, generatedAfter\)/);
765
+ const beforeSpawn = src.split('const generatedAfter = laneGeneratedPaths(inst.dir);')[0];
766
+ assert.match(beforeSpawn, /const pre = pinState\(inst\.dir, spawnSync, generated\);/,
767
+ 'the heal runs before the bump, so it reads the core installed THEN');
768
+ });
@@ -0,0 +1,38 @@
1
+ // tests/work_title_wrap.mjs — a Work board title wraps inside its column (task 1004209).
2
+ //
3
+ // At 390 the backlog title "Watch redesign: … People/Operations/Security tabs" ran
4
+ // 44px past its column: a slash-joined run offers the browser no break point. The
5
+ // rendered check (scripts/gds/render-check.js) is what measures it; this pins the
6
+ // two halves of the fix in source so neither is dropped by an unrelated edit.
7
+ //
8
+ // Run: node tests/work_title_wrap.mjs
9
+ import assert from 'node:assert/strict';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { makeRunner } from './helpers.mjs';
14
+
15
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
16
+ const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
17
+ const WORK_SRC = read('modules/hall-ui/public/work.js');
18
+ const WORK_CSS = read('modules/hall-ui/public/work.css');
19
+
20
+ const { test, summary } = makeRunner();
21
+
22
+ await test('every history title breaks after a slash, and only after escaping', () => {
23
+ const sites = WORK_SRC.match(/<span class="history__title">[^\n]*/g) || [];
24
+ assert.equal(sites.length, 2, 'the backlog and completed rows both render a history title');
25
+ for (const s of sites) {
26
+ assert.ok(s.includes("escapeHtml(t.title || '(untitled)').replace(/\\//g, '/<wbr>')"),
27
+ `the <wbr> goes in AFTER escapeHtml, so a title cannot inject markup: ${s.trim()}`);
28
+ }
29
+ });
30
+
31
+ await test('the break-anything backstop stays on .history__title', () => {
32
+ const rule = WORK_CSS.match(/^\.history__title \{[^}]*\}/m);
33
+ assert.ok(rule, '.history__title rule present');
34
+ assert.match(rule[0], /overflow-wrap:\s*anywhere/, 'a run with no slash still wraps instead of running out of the column');
35
+ assert.match(rule[0], /min-width:\s*0/, 'a flex item needs min-width: 0 to shrink below its longest word');
36
+ });
37
+
38
+ summary();