@bongos/core 1.21.166 → 1.21.168
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 +153 -73
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +13 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/api/openapi.json +306 -3
- package/docs/api-reference.md +7 -3
- package/docs/copy-inventory.md +21 -20
- package/docs/copy-registry.json +34 -25
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +3 -0
- package/docs/page-readings.json +445 -445
- package/modules/autonomy/governor-docket.js +10 -5
- package/modules/builder-settings/builder-needs.js +29 -3
- package/modules/copy-desk/guarded-lines.js +89 -0
- package/modules/copy-desk/guarded-lines.json +14 -0
- package/modules/copy-desk/module.json +2 -2
- package/modules/copy-desk/page-status.js +77 -7
- package/modules/copy-desk/pages.js +4 -0
- package/modules/copy-desk/pollers/stuck-page-digest.js +34 -0
- package/modules/copy-desk/routes/copy-desk.js +8 -0
- package/modules/copy-desk/stuck-tweaks.js +79 -0
- package/modules/copy-desk/tests/copy_no_cms.mjs +6 -1
- package/modules/copy-desk/tests/copy_page_status.mjs +2 -2
- package/modules/government/asks-db.js +38 -2
- package/modules/government/catalog.js +1 -1
- package/modules/government/docket.js +20 -2
- package/modules/government/migrations/government_032_ask_flags.sql +35 -0
- package/modules/government/routes/government.js +43 -1
- package/modules/hall-ui/public/approval-queue-lib.js +4 -0
- package/modules/hall-ui/public/approval-queue.js +1 -0
- package/modules/hall-ui/public/brand-apply.js +3 -0
- package/modules/hall-ui/public/city-decide.js +642 -0
- package/modules/hall-ui/public/city-draw.js +14 -3
- package/modules/hall-ui/public/city-switchboard.js +141 -0
- package/modules/hall-ui/public/city.css +176 -10
- package/modules/hall-ui/public/city.html +8 -3
- package/modules/hall-ui/public/city.js +185 -38
- package/modules/hall-ui/public/sky-data.js +4 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +21 -2
- package/modules/hall-ui/public/tweak-editor.css +2 -0
- package/modules/hall-ui/public/tweak-editor.js +3 -1
- package/modules/lifecycle/db-claims.js +20 -5
- package/modules/lifecycle/rebase-gate.js +78 -0
- package/modules/lifecycle/routes/claims.js +10 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +28 -0
- package/scripts/gds/ask.js +68 -1
- package/scripts/gds/claim.js +6 -1
- package/scripts/gds/copy-apply.js +2 -0
- package/src/bongos/module-promotion.js +219 -0
- package/src/bongos/route-rank-check.js +7 -0
- package/src/bongos/routes/me.js +11 -0
- package/src/bongos/routes/modules.js +46 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +5 -0
- package/tests/claim_gate_ci_unblock.mjs +4 -0
- package/tests/claim_gate_rebase.mjs +56 -3
- package/tests/copy_desk_page_editor.mjs +1 -1
- package/tests/copy_desk_page_submit.mjs +42 -0
- package/tests/copy_guarded_lines.mjs +54 -0
- package/tests/copy_stuck_page_tweaks.mjs +118 -0
- package/tests/government_ask_flags.mjs +80 -0
- package/tests/government_seed.mjs +1 -0
- package/tests/government_treasury.mjs +1 -0
- package/tests/governor_docket.mjs +1 -1
- package/tests/hall_approval_queue.mjs +15 -0
- package/tests/hall_city.mjs +5 -4
- package/tests/hall_city_living.mjs +118 -12
- package/tests/hall_city_rewording.mjs +51 -0
- package/tests/hall_city_switchboard.mjs +63 -0
- package/tests/hall_gov_real_site.mjs +2 -2
- package/tests/hall_sky.mjs +18 -0
- package/tests/hall_street_deck.mjs +6 -4
- package/tests/hall_street_depth.mjs +1 -1
- package/tests/hall_street_desk.mjs +5 -3
- package/tests/hall_tweak_editor.mjs +26 -0
- package/tests/module_promotion.mjs +290 -0
- package/tests/rebase_gate_set_aside.mjs +73 -0
- package/tests/reward_mode_surfacing.mjs +12 -0
|
@@ -242,7 +242,13 @@
|
|
|
242
242
|
ring.appendChild(arc);
|
|
243
243
|
head.appendChild(ring);
|
|
244
244
|
}
|
|
245
|
-
|
|
245
|
+
// the switchboard's marker: a small toggle, on or off
|
|
246
|
+
if (pin.state === 'switch') {
|
|
247
|
+
el.className += pin.on ? ' city-pin--on' : ' city-pin--off';
|
|
248
|
+
var tg = span('city-pin__toggle');
|
|
249
|
+
tg.appendChild(span('city-pin__toggle-knob'));
|
|
250
|
+
head.appendChild(tg);
|
|
251
|
+
} else if (pin.state !== 'settled') {
|
|
246
252
|
var svg = document.createElementNS(SVG_NS, 'svg');
|
|
247
253
|
svg.setAttribute('width', '14');
|
|
248
254
|
svg.setAttribute('height', '14');
|
|
@@ -433,7 +439,7 @@
|
|
|
433
439
|
// route the deck takes (GovDeck.sendAnswer: the desk row's own request); then
|
|
434
440
|
// two doors, "decide all N in a row" to the deck and "go inside" to the
|
|
435
441
|
// building on the street. Without the deck's scripts it is the row and doors.
|
|
436
|
-
var here = { done: {}, busy: false, said: '', room: null, roomAsked: false, cv: null };
|
|
442
|
+
var here = { done: {}, busy: false, said: '', room: null, roomAsked: false, cv: null, at: 0, n: 0 };
|
|
437
443
|
function civicOf(key) { return model && model.civic ? model.civic.filter(function (c) { return c.key === key; })[0] || null : null; }
|
|
438
444
|
// The building's button: the keyboard's way in, and where the card is placed beside.
|
|
439
445
|
function hitFor(key) {
|
|
@@ -449,14 +455,16 @@
|
|
|
449
455
|
function openCard(key, from) {
|
|
450
456
|
var was = openKey;
|
|
451
457
|
openKey = key;
|
|
452
|
-
if (was !== key) { here.said = ''; here.cv = null; }
|
|
458
|
+
if (was !== key) { here.said = ''; here.cv = null; here.at = 0; }
|
|
453
459
|
if (was && was !== key) showPlate(was, false);
|
|
454
460
|
showPlate(key, true);
|
|
455
461
|
var el = renderCard(key);
|
|
456
462
|
if (!el) return;
|
|
457
463
|
place(el, from || hitFor(key));
|
|
458
|
-
|
|
459
|
-
|
|
464
|
+
// the first thing to act on takes focus, without scrolling the card: it always opens at its top
|
|
465
|
+
var first = el.querySelector ? el.querySelector('.ask__opt, .swb__toggle, .city-card__go, .city-card__arrow') : null;
|
|
466
|
+
if (first && first.focus) { try { first.focus({ preventScroll: true }); } catch (_) { first.focus(); } }
|
|
467
|
+
el.scrollTop = 0;
|
|
460
468
|
}
|
|
461
469
|
function closeCard() {
|
|
462
470
|
var el = $('cityCard');
|
|
@@ -473,9 +481,15 @@
|
|
|
473
481
|
function firstAsk(key) {
|
|
474
482
|
var K = window.GovDeck;
|
|
475
483
|
if (!K || !K.drawAsk || !data.living || !data.docket) return null;
|
|
476
|
-
|
|
484
|
+
// what is out for a rewrite leaves the queue until its new words land (city-decide.js)
|
|
485
|
+
var G = window.GovCityDecide;
|
|
486
|
+
var done = G && G.withoutReworded ? G.withoutReworded(here.done, data.docket) : here.done;
|
|
487
|
+
var q = K.queueOf(data.living, { done: done, delegated: {}, skipped: [], extra: [] }, key);
|
|
488
|
+
here.n = q.length;
|
|
489
|
+
here.yours = q.filter(function (x) { return x.needs_you; }).length;
|
|
477
490
|
if (!q.length) return null;
|
|
478
|
-
|
|
491
|
+
if (here.at >= q.length) here.at = 0;
|
|
492
|
+
var cv = K.cardView(q[here.at], { docket: data.docket, room: here.room, constitution: data.raw.constitution, goals: (data.goals || []).filter(function (g) { return g && g.status === 'open'; }), now: Date.now(), viewerId: data.docket.viewer ? String(data.docket.viewer.id) : null });
|
|
479
493
|
// a sitting's ballot is read once, when a vote's card first needs it
|
|
480
494
|
if (cv && cv.card && cv.card.loading && !here.roomAsked) {
|
|
481
495
|
here.roomAsked = true;
|
|
@@ -497,46 +511,157 @@
|
|
|
497
511
|
el.className = 'city-card gov-glass';
|
|
498
512
|
el.setAttribute('data-building', key);
|
|
499
513
|
el.setAttribute('aria-label', card.name);
|
|
500
|
-
|
|
514
|
+
// a building that waits on you wears the project's colour and glows it
|
|
515
|
+
if (card.on_you) el.className += ' city-card--you';
|
|
516
|
+
// the head: its name, and "Total N / Yours M" top right
|
|
501
517
|
var top = node('div', 'city-card__top');
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
close.setAttribute('type', 'button');
|
|
505
|
-
close.setAttribute('aria-label', 'Close');
|
|
506
|
-
close.setAttribute('aria-keyshortcuts', 'Escape');
|
|
507
|
-
close.appendChild(node('kbd', null, 'esc'));
|
|
508
|
-
close.addEventListener('click', closeCard);
|
|
509
|
-
top.appendChild(close);
|
|
510
|
-
head.appendChild(top);
|
|
511
|
-
// who holds it, one fact, and what waits there (salmon only while it is yours)
|
|
518
|
+
var name = node('p', 'city-card__name', card.name);
|
|
519
|
+
// who holds it is a hover hint now, not a line of its own
|
|
512
520
|
var hold = K && K.holdWords ? K.holdWords(data.living, key) : card.plate ? (card.lead ? 'Led by ' + card.plate + '.' : 'The owner holds it until a Lead is seated.') : '';
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
var
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
521
|
+
if (hold) name.setAttribute('title', hold);
|
|
522
|
+
top.appendChild(name);
|
|
523
|
+
var total = card.open != null ? card.open : (card.on_you || 0) + (card.on_others || 0);
|
|
524
|
+
var mine = card.on_you || 0;
|
|
525
|
+
// what is still open after answers given here, as the decide view counts it
|
|
526
|
+
var cv0 = key !== 'automation' && window.GovDeck && data.living ? firstAsk(key) : null;
|
|
527
|
+
if (window.GovDeck && data.living && key !== 'automation') { total = here.n; mine = here.yours; }
|
|
528
|
+
var counts = node('span', 'city-card__counts', 'Total ' + total + ' / ');
|
|
529
|
+
counts.appendChild(node('b', mine ? 'city-card__count--you' : null, 'Yours ' + mine));
|
|
530
|
+
var RW = window.GovCityDecide;
|
|
531
|
+
var rw = RW && RW.rewordingCount ? RW.rewordingCount(data.docket, key) : 0;
|
|
532
|
+
if (rw) counts.appendChild(node('span', 'city-card__rewording', 'Rewording ' + rw));
|
|
533
|
+
// a switchboard has no queue to count
|
|
534
|
+
if (key !== 'automation' || !window.GovCitySwitchboard) top.appendChild(counts);
|
|
535
|
+
el.appendChild(top);
|
|
536
|
+
// Automation is a switchboard, not a queue (city-switchboard.js)
|
|
537
|
+
var SW = key === 'automation' ? window.GovCitySwitchboard : null;
|
|
538
|
+
var cv = SW ? null : cv0;
|
|
520
539
|
here.cv = cv;
|
|
521
|
-
if (
|
|
540
|
+
if (SW) {
|
|
541
|
+
el.className += ' city-card--board';
|
|
542
|
+
SW.draw(document, el, data.docket, { now: Date.now(), viewerId: data.docket && data.docket.viewer ? String(data.docket.viewer.id) : null, goals: (data.goals || []).filter(function (g) { return g && g.status === 'open'; }) }, {
|
|
543
|
+
busy: here.busy,
|
|
544
|
+
send: function (req) { here.busy = true; return send(req); },
|
|
545
|
+
done: function (toast, ok) {
|
|
546
|
+
here.busy = false;
|
|
547
|
+
here.said = toast;
|
|
548
|
+
var flashIt = function () { var f = $('cityCard'); if (ok && window.GovCityDecide && window.GovCityDecide.flash && f) window.GovCityDecide.flash(f, 'Switched'); };
|
|
549
|
+
if (ok) load().then(flashIt); else renderCard(key);
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
// the glow says it is yours: the 'only you can answer' line is left off
|
|
554
|
+
if (cv) {
|
|
555
|
+
// answerable in place: the desk row's acts, chips and typed fields are the card's own (city-decide.js)
|
|
556
|
+
var G = window.GovCityDecide;
|
|
557
|
+
var drawn = G && G.drawCard ? G.drawCard(document, K, cv, { pick: function (op) { answer(key, cv, op); }, said: function (t) { here.said = t; renderCard(key); }, reworded: function (t) { here.said = t; if (window.GovFeeds && window.GovFeeds.changed) window.GovFeeds.changed(); draw(painted || effectiveTheme()); }, send: send, docket: function () { return data.docket; } }, { inner: true, busy: here.busy, cls: 'city-ask' }) : { node: K.drawAsk(document, quiet(cv), { pick: function (op) { answer(key, cv, op); } }, { inner: true, busy: here.busy, cls: 'city-ask' }), cv: cv };
|
|
558
|
+
here.cv = drawn.cv;
|
|
559
|
+
el.appendChild(drawn.node);
|
|
560
|
+
// which of this building's items this is; Tab and the up / down arrows step through them
|
|
561
|
+
if (here.n > 1) {
|
|
562
|
+
var step = node('div', 'city-card__step');
|
|
563
|
+
var up = node('button', 'city-card__stepbtn', '↑');
|
|
564
|
+
up.setAttribute('type', 'button');
|
|
565
|
+
up.setAttribute('aria-label', 'Previous item');
|
|
566
|
+
up.setAttribute('aria-keyshortcuts', 'Shift+Tab ArrowUp');
|
|
567
|
+
up.addEventListener('click', function () { stepItem(-1); });
|
|
568
|
+
var down = node('button', 'city-card__stepbtn', '↓');
|
|
569
|
+
down.setAttribute('type', 'button');
|
|
570
|
+
down.setAttribute('aria-label', 'Next item');
|
|
571
|
+
down.setAttribute('aria-keyshortcuts', 'Tab ArrowDown');
|
|
572
|
+
down.addEventListener('click', function () { stepItem(1); });
|
|
573
|
+
step.appendChild(up);
|
|
574
|
+
step.appendChild(node('span', 'city-card__stepn', (here.at + 1) + ' of ' + here.n));
|
|
575
|
+
step.appendChild(down);
|
|
576
|
+
step.appendChild(node('span', 'city-card__hint', 'Tab · ↑ ↓ items ← → buildings'));
|
|
577
|
+
el.appendChild(step);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
else if (!SW) el.appendChild(node('p', 'city-card__f', 'Nothing waits here.'));
|
|
522
581
|
if (here.said) {
|
|
523
582
|
var said = node('p', 'city-card__said', here.said);
|
|
524
583
|
said.setAttribute('role', 'status');
|
|
525
584
|
el.appendChild(said);
|
|
526
585
|
}
|
|
586
|
+
// the foot: flip between buildings, decide them all, and a small esc
|
|
527
587
|
var acts = node('div', 'city-card__acts');
|
|
528
|
-
var
|
|
588
|
+
var prev = node('button', 'city-card__arrow', '‹');
|
|
589
|
+
prev.setAttribute('type', 'button');
|
|
590
|
+
prev.setAttribute('aria-label', 'Previous building');
|
|
591
|
+
prev.setAttribute('aria-keyshortcuts', 'ArrowLeft');
|
|
592
|
+
prev.addEventListener('click', function () { flipCard(-1); });
|
|
593
|
+
var next = node('button', 'city-card__arrow', '›');
|
|
594
|
+
next.setAttribute('type', 'button');
|
|
595
|
+
next.setAttribute('aria-label', 'Next building');
|
|
596
|
+
next.setAttribute('aria-keyshortcuts', 'ArrowRight');
|
|
597
|
+
next.addEventListener('click', function () { flipCard(1); });
|
|
598
|
+
acts.appendChild(prev);
|
|
599
|
+
acts.appendChild(next);
|
|
600
|
+
// what is still open here, after answers and rewrites (firstAsk counted it)
|
|
601
|
+
var n = cv ? here.n : (card.on_you || card.on_others);
|
|
529
602
|
if (n) {
|
|
530
|
-
var go = node('
|
|
531
|
-
go.setAttribute('
|
|
603
|
+
var go = node('button', 'city-card__go', n === 1 ? 'decide it' : 'decide all ' + n);
|
|
604
|
+
go.setAttribute('type', 'button');
|
|
605
|
+
go.addEventListener('click', function () { openDecide(key); });
|
|
532
606
|
acts.appendChild(go);
|
|
533
607
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
608
|
+
acts.appendChild(node('span', 'city-card__gap'));
|
|
609
|
+
var close = node('button', 'city-card__close');
|
|
610
|
+
close.setAttribute('type', 'button');
|
|
611
|
+
close.setAttribute('aria-label', 'Close');
|
|
612
|
+
close.setAttribute('aria-keyshortcuts', 'Escape');
|
|
613
|
+
close.appendChild(node('kbd', null, 'esc'));
|
|
614
|
+
close.addEventListener('click', closeCard);
|
|
615
|
+
acts.appendChild(close);
|
|
537
616
|
el.appendChild(acts);
|
|
617
|
+
// a pick waiting on its confirm survives a redraw of the same card, never a move to another
|
|
618
|
+
var G2 = window.GovCityDecide;
|
|
619
|
+
if (el.__armed && (!here.cv || here.cv.id !== el.__armed.id)) el.__armed = null;
|
|
620
|
+
else if (G2 && G2.paintArmed) G2.paintArmed(el);
|
|
538
621
|
return el;
|
|
539
622
|
}
|
|
623
|
+
function quiet(cv) { var f = cv.foot || {}; var c = {}; for (var k in cv) c[k] = cv[k]; c.foot = { you: '', rest: f.rest || '' }; return c; }
|
|
624
|
+
// Tab / ↑ ↓ step through this building's items (wrapping)
|
|
625
|
+
function stepItem(d) {
|
|
626
|
+
if (!openKey || here.n < 2) return;
|
|
627
|
+
here.at = (here.at + d + here.n) % here.n;
|
|
628
|
+
here.said = '';
|
|
629
|
+
var el = renderCard(openKey);
|
|
630
|
+
var first = el && el.querySelector ? el.querySelector('.ask__opt, .ask-field__input') : null;
|
|
631
|
+
if (first && first.focus) first.focus();
|
|
632
|
+
}
|
|
633
|
+
// ← / → walk the buildings that have something open, else all of them
|
|
634
|
+
function flipCard(d) {
|
|
635
|
+
if (!model || !model.civic || !openKey) return;
|
|
636
|
+
var keys = model.civic.filter(function (c) { return c.key === 'automation' || (c.card && (c.card.open || c.card.on_you || c.card.on_others)); }).map(function (c) { return c.key; });
|
|
637
|
+
if (keys.indexOf(openKey) < 0) keys = model.civic.map(function (c) { return c.key; });
|
|
638
|
+
var i = keys.indexOf(openKey);
|
|
639
|
+
var to = keys[(i + d + keys.length) % keys.length];
|
|
640
|
+
if (to && to !== openKey) openCard(to);
|
|
641
|
+
}
|
|
642
|
+
// the decide view (city-decide.js): one by one or a table, over the city
|
|
643
|
+
var decide = null;
|
|
644
|
+
// how: 'one' (default) or 'table'
|
|
645
|
+
function openDecide(scope, how) {
|
|
646
|
+
if (!window.GovCityDecide || !window.GovDeck) { window.location.href = City.STREET_HREF + '#deck' + (scope ? '=' + scope : ''); return; }
|
|
647
|
+
if (!decide) decide = window.GovCityDecide.make({
|
|
648
|
+
data: function () { return data; },
|
|
649
|
+
done: here.done,
|
|
650
|
+
send: send,
|
|
651
|
+
changed: function () { if (window.GovFeeds && window.GovFeeds.changed) window.GovFeeds.changed(); draw(painted || effectiveTheme()); },
|
|
652
|
+
root: function () { return $('hall-main'); },
|
|
653
|
+
room: function () { return here.room; },
|
|
654
|
+
});
|
|
655
|
+
if (openKey) closeCard();
|
|
656
|
+
if (!here.roomAsked) {
|
|
657
|
+
here.roomAsked = true;
|
|
658
|
+
getJson('/api/bongos/government/board/items').then(function (r) {
|
|
659
|
+
here.room = r && (Array.isArray(r.open) || Array.isArray(r.closed)) ? r : { open: [], closed: [] };
|
|
660
|
+
if (decide && decide.isOpen()) decide.render();
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
decide.open(scope || null, how);
|
|
664
|
+
}
|
|
540
665
|
// One answer, through the deck's own sender: the same request its desk row sends.
|
|
541
666
|
function send(req) {
|
|
542
667
|
return fetch(req.url, { method: req.method, credentials: 'same-origin', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify(req.body || {}) })
|
|
@@ -560,8 +685,12 @@
|
|
|
560
685
|
// every count on the page leaves out what was answered here (gov-feeds.js)
|
|
561
686
|
if (window.GovFeeds && window.GovFeeds.changed) window.GovFeeds.changed();
|
|
562
687
|
}
|
|
563
|
-
|
|
688
|
+
// a decision lands with a beat you can see: the card pulses and says so (city-decide.js flash)
|
|
689
|
+
var G = window.GovCityDecide;
|
|
690
|
+
var cheer = function () { var f = $('cityCard'); if (res.ok && G && G.flash && f) G.flash(f, 'Decided'); };
|
|
691
|
+
if (res.ok && res.stored) return load().then(function () { cheer(); return true; });
|
|
564
692
|
if (openKey) { draw(painted || effectiveTheme()); var f = $('cityCard'); var first = f && f.querySelector ? f.querySelector('.ask__opt, .city-card__go') : null; if (first && first.focus) first.focus(); }
|
|
693
|
+
cheer();
|
|
565
694
|
return res.ok;
|
|
566
695
|
});
|
|
567
696
|
}
|
|
@@ -569,10 +698,19 @@
|
|
|
569
698
|
function cardKey(e) {
|
|
570
699
|
if (!e) return;
|
|
571
700
|
if (e.key === 'Escape') { closeCard(); return; }
|
|
572
|
-
if (
|
|
701
|
+
if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && !e.ctrlKey && !e.metaKey && !e.altKey && !(e.target && /^(INPUT|TEXTAREA)$/.test(String(e.target.tagName)))) { if (e.preventDefault) e.preventDefault(); flipCard(e.key === 'ArrowLeft' ? -1 : 1); return; }
|
|
702
|
+
var inText = e.target && /^(INPUT|TEXTAREA)$/.test(String(e.target.tagName));
|
|
703
|
+
// Tab is the next thing that needs you in this building, never the next button
|
|
704
|
+
if (e.key === 'Tab' && openKey && !inText) { if (e.preventDefault) e.preventDefault(); stepItem(e.shiftKey ? -1 : 1); return; }
|
|
705
|
+
if (inText) return;
|
|
706
|
+
if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && openKey) { if (e.preventDefault) e.preventDefault(); stepItem(e.key === 'ArrowUp' ? -1 : 1); return; }
|
|
707
|
+
if (!here.cv || !openKey || e.ctrlKey || e.metaKey || e.altKey || !/^[1-9]$/.test(String(e.key))) return;
|
|
573
708
|
var op = here.cv.options[Number(e.key) - 1];
|
|
574
709
|
if (!op) return;
|
|
575
710
|
if (e.preventDefault) e.preventDefault();
|
|
711
|
+
// the first press picks that answer, the same number again confirms it
|
|
712
|
+
var G = window.GovCityDecide;
|
|
713
|
+
if (G && G.armKey && !G.armKey($('cityCard'), here.cv, Number(e.key))) return;
|
|
576
714
|
answer(openKey, here.cv, op);
|
|
577
715
|
}
|
|
578
716
|
// Beside the building on a wide stage, over a light hush with the building
|
|
@@ -603,15 +741,24 @@
|
|
|
603
741
|
|
|
604
742
|
function boot() {
|
|
605
743
|
var start = $('cityStart');
|
|
606
|
-
|
|
744
|
+
// the big button opens the decide view in place; the street is archived
|
|
745
|
+
if (start) start.setAttribute('href', '#decide');
|
|
746
|
+
if (start) start.addEventListener('click', function (e) { if (window.GovCityDecide && window.GovDeck) { if (e.preventDefault) e.preventDefault(); openDecide(null); } });
|
|
747
|
+
window.addEventListener('hashchange', function () { var h = window.location && window.location.hash; if (/^#decide/.test(h || '')) openDecide(h.split('=')[1] || null); });
|
|
607
748
|
var btn = $('cityMode');
|
|
608
749
|
if (btn) btn.addEventListener('click', toggleTheme);
|
|
609
750
|
if (media && media.addEventListener) media.addEventListener('change', function () { if (!storedTheme()) draw(applyTheme()); });
|
|
610
751
|
window.addEventListener('resize', fit);
|
|
611
752
|
var card = $('cityCard');
|
|
753
|
+
// the card's keys work wherever focus is while it is open (Tab never walks its buttons)
|
|
612
754
|
if (card) card.addEventListener('keydown', cardKey);
|
|
755
|
+
if (document.addEventListener) document.addEventListener('keydown', function (e) {
|
|
756
|
+
var t = e && e.target;
|
|
757
|
+
if (!openKey || e.defaultPrevented || (decide && decide.isOpen()) || (card && t && card.contains && card.contains(t))) return;
|
|
758
|
+
cardKey(e);
|
|
759
|
+
});
|
|
613
760
|
// the page's counts leave out what the card answered (gov-feeds.js)
|
|
614
|
-
if (window.GovFeeds && typeof window.GovFeeds.useSession === 'function') window.GovFeeds.useSession(function () { return { answered: Object.keys(here.done), extra: [] }; });
|
|
761
|
+
if (window.GovFeeds && typeof window.GovFeeds.useSession === 'function') window.GovFeeds.useSession(function () { var G = window.GovCityDecide; return { answered: Object.keys(G && G.withoutReworded ? G.withoutReworded(here.done, data.docket) : here.done), extra: [] }; });
|
|
615
762
|
var frame = $('cityFrame');
|
|
616
763
|
// a click anywhere on the city but a building (its faces, its pin, its plate) closes the card
|
|
617
764
|
if (frame) frame.addEventListener('click', function (e) {
|
|
@@ -623,9 +770,9 @@
|
|
|
623
770
|
painted = applyTheme();
|
|
624
771
|
draw(painted);
|
|
625
772
|
watchShellTheme();
|
|
626
|
-
return load();
|
|
773
|
+
return load().then(function (r) { var h = window.location && window.location.hash; if (/^#decide/.test(h || '')) openDecide(h.split('=')[1] || null); return r; });
|
|
627
774
|
}
|
|
628
775
|
|
|
629
|
-
window.GovCityPage = { boot: boot, draw: draw, load: load, fit: fit, openCard: openCard, closeCard: closeCard, FEEDS: FEEDS, LIVING_FEEDS: LIVING_FEEDS, THEME_KEY: THEME_KEY };
|
|
776
|
+
window.GovCityPage = { boot: boot, draw: draw, load: load, fit: fit, openCard: openCard, closeCard: closeCard, openDecide: function (scope, how) { openDecide(scope, how); return decide; }, FEEDS: FEEDS, LIVING_FEEDS: LIVING_FEEDS, THEME_KEY: THEME_KEY };
|
|
630
777
|
window.GovCityPage.ready = boot();
|
|
631
778
|
}());
|
|
@@ -147,7 +147,10 @@
|
|
|
147
147
|
id: String(g.goal_id), title: g.title || 'A private goal', subtitle: g.subtitle || null, version: g.version_id, status: g.status,
|
|
148
148
|
taskTotal: Number(g.task_total) || 0, taskDone: Number(g.task_done) || 0, createdAt: g.created_at || null,
|
|
149
149
|
crit: (g.criteria || []).map((c) => ({ id: String(c.id), text: c.criterion_md || '', satisfied: c.satisfied === true, tasks: (c.task_ids || []).map(String), pendingReview: c.pending_review === true, uat: c.uat_state || null, uatLabel: uatLabel(c.uat_state), backendOnly: c.backend_only === true })),
|
|
150
|
-
|
|
150
|
+
// task 1004782 — abandoned (or merged-away) work carries no credits: it is listed, at zero, and never adds to a
|
|
151
|
+
// well's mass or a ring. A merged-away task is always abandoned (tasks.merged_into_task_id is CHECK-constrained to
|
|
152
|
+
// abandoned rows), so the status test covers both; /sky does not send the pointer.
|
|
153
|
+
tasks: (g.tasks || []).map((t) => ({ id: String(t.id), title: t.title || 'a task', status: t.status, credits: t.status === 'abandoned' || t.credits_reward == null ? 0 : Number(t.credits_reward), createdAt: t.created_on || null, sourceRef: t.idea_id != null ? String(t.idea_id) : null })),
|
|
151
154
|
};
|
|
152
155
|
}
|
|
153
156
|
const taskRow = (t) => ({ id: String(t.task_id ?? t.id), title: t.title || 'a task', status: t.status, goalId: t.goal_id != null ? String(t.goal_id) : null, origin: t.origin || null });
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
'use strict';
|
|
26
26
|
|
|
27
27
|
// The ADR's words (D4), and the read's own `notes` wins when it carries them.
|
|
28
|
-
const NOTES = Object.freeze({ shared: 'changes this on every page', engineer: 'goes to an engineer' });
|
|
28
|
+
const NOTES = Object.freeze({ shared: 'changes this on every page', engineer: 'goes to an engineer', guarded: 'must still say' });
|
|
29
29
|
|
|
30
30
|
function sectionsOf(lines) {
|
|
31
31
|
const out = [];
|
|
@@ -43,12 +43,31 @@
|
|
|
43
43
|
return typeof value === 'string' && value !== line.text;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// task 1004776: the phrases a guarded line's new text no longer contains (the
|
|
47
|
+
// server's guarded-lines.js rule, case-insensitive), so the warning shows while
|
|
48
|
+
// the artist types, not only at submit.
|
|
49
|
+
function droppedPhrases(line, value) {
|
|
50
|
+
const t = String(value == null ? '' : value).toLowerCase();
|
|
51
|
+
return (line.must_contain || []).filter((p) => !t.includes(String(p).toLowerCase()));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The submit's warning (the server's `warnings`), as plain sentences; the
|
|
55
|
+
// editor escapes them. Empty when nothing was warned.
|
|
56
|
+
function submitWarningHtml(warnings) {
|
|
57
|
+
return (warnings || []).map((w) => `A line you changed no longer says what it must: ${w.must_still_say} It was sent anyway; the person approving it will see this.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
// The mock's rule: a shared or engineer line always shows its own note, and
|
|
47
61
|
// any other changed line shows the words it replaced. An emptied line says it
|
|
48
62
|
// will be removed first, since that is what sending it now does.
|
|
49
63
|
function noteFor(line, value, notes) {
|
|
50
64
|
const n = notes || NOTES;
|
|
51
65
|
if (isRemoved(line, value)) return { kind: 'removed', text: `to be removed · goes to an engineer · was: ${line.text}` };
|
|
66
|
+
// task 1004776: a line that must keep its meaning says what it must still say.
|
|
67
|
+
if (line.note === 'guarded' && line.must_still_say) {
|
|
68
|
+
const drops = isChanged(line, value) && droppedPhrases(line, value).length;
|
|
69
|
+
return { kind: 'guarded', text: `${drops ? 'this no longer says what it must · ' : ''}${n.guarded || 'must still say'}: ${line.must_still_say}` };
|
|
70
|
+
}
|
|
52
71
|
if (line.note === 'shared') return { kind: 'shared', text: n.shared || NOTES.shared };
|
|
53
72
|
if (line.note === 'engineer') return { kind: 'engineer', text: n.engineer || NOTES.engineer };
|
|
54
73
|
if (isChanged(line, value)) return { kind: 'was', text: `was: ${line.text}` };
|
|
@@ -260,7 +279,7 @@
|
|
|
260
279
|
return null;
|
|
261
280
|
}
|
|
262
281
|
|
|
263
|
-
const api = { OPEN_WORDS, openChoice, clampSplit, splitFromPointer, splitFromKey, canFrame, FRAME_SIBLINGS, splitWindowFeatures, HINT, nextLineKey, confirmedCount, hintText, confirmStoreKey, parseConfirmed, NOTES, sectionsOf, isChanged, isRemoved, noteFor, draftBody, changedCount, counterText, initialValues, rowsFor, liveUrl, uploadedLabel, uploadRefusal, displayTitle, pageIdFrom };
|
|
282
|
+
const api = { OPEN_WORDS, openChoice, clampSplit, splitFromPointer, splitFromKey, canFrame, FRAME_SIBLINGS, splitWindowFeatures, HINT, nextLineKey, confirmedCount, hintText, confirmStoreKey, parseConfirmed, NOTES, sectionsOf, isChanged, isRemoved, droppedPhrases, noteFor, draftBody, submitWarningHtml, changedCount, counterText, initialValues, rowsFor, liveUrl, uploadedLabel, uploadRefusal, displayTitle, pageIdFrom };
|
|
264
283
|
if (typeof window !== 'undefined') window.OTBTweakEditor = api;
|
|
265
284
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
266
285
|
})();
|
|
@@ -412,6 +412,8 @@ html[data-fullscreen="on"] .tw-frame { left: 0; }
|
|
|
412
412
|
.tw-note { padding-top: 9px; font-size: 12px; line-height: 1.4; color: var(--tw-paper-faint); overflow-wrap: anywhere; }
|
|
413
413
|
.tw-note--shared { color: var(--tw-shared); }
|
|
414
414
|
.tw-note--engineer { color: var(--tw-engineer); }
|
|
415
|
+
/* task 1004776: a line that must keep its meaning. */
|
|
416
|
+
.tw-note--guarded { color: var(--tw-engineer); font-weight: 600; }
|
|
415
417
|
.tw-sr {
|
|
416
418
|
position: absolute;
|
|
417
419
|
width: 1px;
|
|
@@ -525,7 +525,9 @@
|
|
|
525
525
|
}
|
|
526
526
|
st.submitted = true;
|
|
527
527
|
st.writable = false;
|
|
528
|
-
|
|
528
|
+
// task 1004776: a guarded line that dropped its promise is warned, never refused.
|
|
529
|
+
const warned = lib.submitWarningHtml(r.data && r.data.warnings);
|
|
530
|
+
notice(warned.length ? warned.map((s) => `<p>${escapeHtml(s)}</p>`).join('') : '');
|
|
529
531
|
document.querySelectorAll('textarea.tw-line').forEach((ta) => { ta.readOnly = true; });
|
|
530
532
|
document.querySelectorAll('.tw-line--holes').forEach((box) => { box.setAttribute('contenteditable', 'false'); });
|
|
531
533
|
document.querySelectorAll('.tw-ctl').forEach((c) => { c.hidden = true; });
|
|
@@ -36,6 +36,7 @@ const { siblingOverlaps, RESOLVED_STATUSES } = require('./goal-conflicts.js');
|
|
|
36
36
|
const { claimGate, claimPermissionsFor, disambiguateWorktreeName, foldWorktreeName } = require('./db-rank-authz.js');
|
|
37
37
|
const { sealSessionLog } = require('./db-shared.js');
|
|
38
38
|
const { gatingRebaseTasksForBuilder, shouldRefuseClaimResolve } = require('./db-ship.js');
|
|
39
|
+
const { splitRebaseGate, rebaseRefusalTasks } = require('./rebase-gate.js');
|
|
39
40
|
const { REWARD_SUGGESTION_CAP, rewardGateAssignment } = require('./db-tasks.js');
|
|
40
41
|
const { autoPickupRefusal } = require('./autopickup.js');
|
|
41
42
|
|
|
@@ -135,13 +136,16 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
|
|
|
135
136
|
// ship that clears the last flag.
|
|
136
137
|
// The GATING subset, not the surfacing one: a strand the builder cannot clear
|
|
137
138
|
// stops blocking after a grace, while still being reported (task 1003294).
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
// task 1004788: split by the shared predicate (rebase-gate.js) — a strand that
|
|
140
|
+
// waits on something outside the builder never gates, and fixable ones gate
|
|
141
|
+
// only once REBASE_GATE_LIMIT of them pile up.
|
|
142
|
+
const rebaseSplit = splitRebaseGate(await gatingRebaseTasksForBuilder(builderId, client));
|
|
143
|
+
if (rebaseSplit.gates && !isRebaseGateExempt(
|
|
140
144
|
(await client.query(`SELECT kind, priority FROM tasks WHERE id = $1`, [taskId])).rows[0],
|
|
141
145
|
)) {
|
|
142
146
|
throw {
|
|
143
147
|
code: 'REBASE_REQUIRED',
|
|
144
|
-
rebaseTasks:
|
|
148
|
+
rebaseTasks: rebaseRefusalTasks(rebaseSplit),
|
|
145
149
|
};
|
|
146
150
|
}
|
|
147
151
|
|
|
@@ -539,11 +543,19 @@ async function claimTasksBatch({ taskIds, builderId, worktreePrefix, creatorSess
|
|
|
539
543
|
|
|
540
544
|
// Lock every requested task row up front (sorted to avoid deadlocks).
|
|
541
545
|
const { rows: taskRows } = await client.query(
|
|
542
|
-
`SELECT id, status, touches, newcomer_friendly, requires_rank, needs_migration, module_key, goal_id FROM tasks WHERE id = ANY($1::bigint[]) ORDER BY id FOR UPDATE`,
|
|
546
|
+
`SELECT id, status, touches, newcomer_friendly, requires_rank, needs_migration, module_key, goal_id, kind, priority FROM tasks WHERE id = ANY($1::bigint[]) ORDER BY id FOR UPDATE`,
|
|
543
547
|
[uniqIds]
|
|
544
548
|
);
|
|
545
549
|
|
|
546
550
|
const failures = [];
|
|
551
|
+
// task 1004788: the REBASE_REQUIRED gate, by the SAME predicate as claimTask
|
|
552
|
+
// (rebase-gate.js). Before this the batch path skipped the gate entirely.
|
|
553
|
+
const batchRebaseSplit = splitRebaseGate(await gatingRebaseTasksForBuilder(builderId, client));
|
|
554
|
+
if (batchRebaseSplit.gates) {
|
|
555
|
+
for (const t of taskRows) {
|
|
556
|
+
if (!isRebaseGateExempt(t)) failures.push({ task_id: t.id, reason: 'REBASE_REQUIRED', rebaseTasks: rebaseRefusalTasks(batchRebaseSplit) });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
547
559
|
// node-pg returns bigint/bigserial columns (tasks.id) as JS STRINGS, not
|
|
548
560
|
// numbers (learning #94) — Number() them before comparing against uniqIds
|
|
549
561
|
// (which are already numbers). Without this, foundIds.has(requestedId)
|
|
@@ -939,10 +951,12 @@ async function validateClaimBatch({ taskIds, builderId }) {
|
|
|
939
951
|
const builderRank = builderRows[0] ? builderRows[0].rank : null;
|
|
940
952
|
|
|
941
953
|
const { rows: taskRows } = await pool.query(
|
|
942
|
-
`SELECT id, status, touches, newcomer_friendly, requires_rank, module_key, goal_id
|
|
954
|
+
`SELECT id, status, touches, newcomer_friendly, requires_rank, module_key, goal_id, kind, priority
|
|
943
955
|
FROM tasks WHERE id = ANY($1::bigint[])`,
|
|
944
956
|
[uniqIds]
|
|
945
957
|
);
|
|
958
|
+
// task 1004788: the dry run asks the same rebase-gate question as the real paths.
|
|
959
|
+
const dryRebaseSplit = splitRebaseGate(await gatingRebaseTasksForBuilder(builderId, pool));
|
|
946
960
|
// node-pg returns tasks.id (bigserial) as a STRING (learning #94) — Number()
|
|
947
961
|
// it so lookups below (byId.get(id), id from uniqIds) actually hit.
|
|
948
962
|
const byId = new Map(taskRows.map((r) => [Number(r.id), r]));
|
|
@@ -968,6 +982,7 @@ async function validateClaimBatch({ taskIds, builderId }) {
|
|
|
968
982
|
if (builderInactive) push(id, { reason: 'BUILDER_INACTIVE' });
|
|
969
983
|
const t = byId.get(id);
|
|
970
984
|
if (!t) { push(id, { reason: 'TASK_NOT_FOUND' }); continue; }
|
|
985
|
+
if (dryRebaseSplit.gates && !isRebaseGateExempt(t)) push(id, { reason: 'REBASE_REQUIRED', rebaseTasks: rebaseRefusalTasks(dryRebaseSplit) });
|
|
971
986
|
if (t.status !== 'ready') push(id, { reason: 'TASK_NOT_READY', currentStatus: t.status });
|
|
972
987
|
// task 1003196: same composed decision as claimTask and claimTasksBatch, and
|
|
973
988
|
// the same deliberate xenos-only queue short-circuit the batch path carries.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// rebase-gate.js — the ONE predicate behind the REBASE_REQUIRED claim gate, shared
|
|
4
|
+
// by all three claim surfaces (claimTask, claimTasksBatch, validateClaimBatch; task
|
|
5
|
+
// 1004788, ADR 0120 part 3).
|
|
6
|
+
//
|
|
7
|
+
// WHY. The gate refused EVERY new claim while the builder had ANY confirmed task
|
|
8
|
+
// flagged needs_rebase. On 2026-10-23 one task parked on a server-side guard
|
|
9
|
+
// (strand:branch_modifies_executed_code, which no action of its builder clears)
|
|
10
|
+
// plus two peer-session strands froze a six-task build chain for over an hour.
|
|
11
|
+
// Owner: "if i send something for lars to do it shouldnt shut down all other
|
|
12
|
+
// merges and ships" — and, refining it, such a task is SET ASIDE: out of the claim
|
|
13
|
+
// gate, marked as waiting with what it waits on, and still swept, so it re-enters
|
|
14
|
+
// the land on its own.
|
|
15
|
+
//
|
|
16
|
+
// THE RULE. Each owed rebase (gatingRebaseTasksForBuilder's rows) is split:
|
|
17
|
+
// • WAITING — stuck on something outside the builder. Its reason names a
|
|
18
|
+
// server-side guard (branch_modifies_executed_code: the conflict resolver will
|
|
19
|
+
// not run code the branch changes; a person or a moved main lands it) or an
|
|
20
|
+
// explicit hold (a `waiting:<what>` reason). It never gates. It stays flagged,
|
|
21
|
+
// reported, and swept by the publish-reconciler every pass, so the moment the
|
|
22
|
+
// thing it waits on clears (main moves so it merges clean, the branch is
|
|
23
|
+
// brought up to date, a person lands it) the ordinary sweep lands it and ship
|
|
24
|
+
// clears the flag. Set aside, never dropped.
|
|
25
|
+
// • FIXABLE — everything else (an unresolvable hand-written conflict, a red
|
|
26
|
+
// check, an unpushed branch). These are the builder's to fix, and the gate
|
|
27
|
+
// keeps its purpose for them — but only once they pile up: it refuses at
|
|
28
|
+
// REBASE_GATE_LIMIT fixable strands, not on the first one.
|
|
29
|
+
//
|
|
30
|
+
// Pure: no DB. The rows come from db-ship.gatingRebaseTasksForBuilder, which keeps
|
|
31
|
+
// its own unprovable-strand grace (task 1003294).
|
|
32
|
+
|
|
33
|
+
// Fixable strands at which new claims are refused. Read per call so an operator
|
|
34
|
+
// (or a test) can move it with REBASE_GATE_LIMIT without a restart.
|
|
35
|
+
const REBASE_GATE_LIMIT = 3;
|
|
36
|
+
function gateLimit() { return Math.max(1, Number(process.env.REBASE_GATE_LIMIT) || REBASE_GATE_LIMIT); }
|
|
37
|
+
|
|
38
|
+
// Reasons (as stored: `strand:<reason>[:files]`) that wait on something outside
|
|
39
|
+
// the builder, with the words the refusal and the CLI say for each.
|
|
40
|
+
const WAITING_REASONS = Object.freeze({
|
|
41
|
+
branch_modifies_executed_code: 'a server-side guard: the merge resolver will not run code this branch changes, so it lands once main moves or a person lands it',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// waitsOn(reason) -> null (the builder's to fix) | string (what it waits on).
|
|
45
|
+
function waitsOn(reason) {
|
|
46
|
+
const r = String(reason == null ? '' : reason);
|
|
47
|
+
const m = /^waiting:(.+)$/.exec(r);
|
|
48
|
+
if (m) return m[1].trim() || 'something outside the builder';
|
|
49
|
+
const key = r.replace(/^strand:/, '').split(':')[0];
|
|
50
|
+
return Object.prototype.hasOwnProperty.call(WAITING_REASONS, key) ? WAITING_REASONS[key] : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// splitRebaseGate(rows, { limit }) -> { gates, gating, waiting, fixable }
|
|
54
|
+
// gating — the rows that refuse (all fixable rows once there are `limit` of them; else [])
|
|
55
|
+
// waiting — set-aside rows, each with waits_on
|
|
56
|
+
// fixable — every fixable row, gating or not
|
|
57
|
+
function splitRebaseGate(rows, { limit = gateLimit() } = {}) {
|
|
58
|
+
const waiting = [];
|
|
59
|
+
const fixable = [];
|
|
60
|
+
for (const t of rows || []) {
|
|
61
|
+
const w = waitsOn(t && t.needs_rebase_reason);
|
|
62
|
+
if (w) waiting.push({ ...t, waits_on: w });
|
|
63
|
+
else fixable.push(t);
|
|
64
|
+
}
|
|
65
|
+
const gates = fixable.length >= limit;
|
|
66
|
+
return { gates, gating: gates ? fixable : [], waiting, fixable };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The refusal's task list: gating tasks as before, plus the waiting ones marked so
|
|
70
|
+
// the hint and the CLI can say "waiting, not on you".
|
|
71
|
+
function rebaseRefusalTasks(split) {
|
|
72
|
+
return [
|
|
73
|
+
...split.gating.map((t) => ({ id: t.id, title: t.title, reason: t.needs_rebase_reason })),
|
|
74
|
+
...split.waiting.map((t) => ({ id: t.id, title: t.title, reason: t.needs_rebase_reason, waiting: true, waits_on: t.waits_on })),
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { REBASE_GATE_LIMIT, WAITING_REASONS, waitsOn, splitRebaseGate, rebaseRefusalTasks };
|
|
@@ -97,9 +97,17 @@ function taskNotReadyHint(taskId, currentStatus) {
|
|
|
97
97
|
// up-to-date branch and stays a fast-forward, which is why the recovery text in
|
|
98
98
|
// ship-land.js already names merge and explicitly says "Do NOT reset or
|
|
99
99
|
// re-rebase". This is the same advice, given BEFORE the deadlock instead of after.
|
|
100
|
+
// task 1004788: only the FIXABLE tasks gate (rebase-gate.js); a task set aside
|
|
101
|
+
// because it waits on something outside the builder is listed after them as
|
|
102
|
+
// "waiting, not on you" so nobody chases it.
|
|
100
103
|
function rebaseRequiredHint(rebaseTasks) {
|
|
101
|
-
const
|
|
102
|
-
|
|
104
|
+
const all = rebaseTasks || [];
|
|
105
|
+
const ids = all.filter((t) => !t.waiting).map((t) => `task ${t.id}`);
|
|
106
|
+
const waiting = all.filter((t) => t.waiting);
|
|
107
|
+
const aside = waiting.length
|
|
108
|
+
? ` Waiting, not on you (set aside; they land on their own when what they wait on clears): ${waiting.map((t) => `task ${t.id} (${t.waits_on || 'outside you'})`).join('; ')}.`
|
|
109
|
+
: '';
|
|
110
|
+
return `Your queue is gated: ${ids.length} confirmed task(s) of yours are waiting to land and need you (${ids.join(', ') || 'unknown'}).${aside} Clear it with /strand-fix N (works on any machine), or bring the branch up to date with \`git merge origin/main\` and re-ship — MERGE, never rebase: server-mediated publish is append-only, and rewriting a published branch makes it un-pushable (task 1002788). /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. If main's CI is red for a reason outside your branch, the fix is still claimable: a P1 kind=infra task passes this gate (task 1004370).`;
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
// task 1642 (+ the worktree-keying fix) — the pure one-active-claim decision.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.168",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.168",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.168",
|
|
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",
|