@bongos/core 1.20.46 → 1.20.48
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 +103 -68
- package/docs/copy-inventory.md +230 -225
- package/docs/copy-registry.json +371 -286
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +7 -4
- package/docs/page-readings.json +248 -249
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/bulk-toolbar.js +291 -0
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goals-page.js +29 -44
- package/modules/hall-ui/public/goals-render.js +33 -21
- package/modules/hall-ui/public/goals.html +13 -17
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/hall-render.js +3 -0
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +82 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +110 -3
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work-founding.states.json +39 -0
- package/modules/hall-ui/public/work.css +4 -66
- package/modules/hall-ui/public/work.html +3 -2
- package/modules/hall-ui/public/work.js +17 -236
- package/modules/hall-ui/records/founding-mode.md +31 -0
- package/modules/onboarding/founding-mode.js +185 -0
- package/modules/onboarding/port.js +7 -0
- package/modules/ui-design/kit/fixtures/me-founding.json +24 -0
- package/modules/ui-design/kit/serve.js +3 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/routes/me.js +18 -1
- package/src/module-api.js +1 -1
- package/tests/bulk_toolbar.mjs +202 -0
- package/tests/claim_action.mjs +13 -4
- package/tests/founding_mode.mjs +294 -0
- package/tests/goal_category_ui.mjs +1 -0
- package/tests/goal_criterion_confirm_ui.mjs +2 -0
- package/tests/goal_forest_page.mjs +1 -0
- package/tests/goal_invite_consent.mjs +1 -1
- package/tests/goal_mine_lens.mjs +1 -0
- package/tests/hall_board_world.mjs +1 -1
- package/tests/hall_mine_lens.mjs +1 -0
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
verification state, which DESIGN.md §Reuse names as the one sanctioned
|
|
15
15
|
exception to the Fifteen Rule.
|
|
16
16
|
|
|
17
|
-
goals.html LOADS THIS SHEET
|
|
17
|
+
goals.html NO LONGER LOADS THIS SHEET (task 1003141): what the goal board's
|
|
18
|
+
cards borrowed from it — .claim-count-line, .wip__release, .wip__select — now
|
|
19
|
+
lives in style.css, so this sheet is the board's alone. */
|
|
18
20
|
|
|
19
21
|
/* `.page-head--board` MOVED to style.css (task 1003305) when the ideas composer
|
|
20
22
|
became its third consumer: a modifier on the shell's own .page-head is shell
|
|
@@ -35,17 +37,6 @@
|
|
|
35
37
|
.wip-builder { color: var(--ink); font-weight: 500; }
|
|
36
38
|
.wip-builder small { color: var(--ink-soft); font-weight: 400; }
|
|
37
39
|
|
|
38
|
-
/* Filtered-count line under the filter bar — the label voice, with the number
|
|
39
|
-
itself stepping up to ink so the count reads before the words. */
|
|
40
|
-
.claim-count-line {
|
|
41
|
-
margin: 0 0 12px;
|
|
42
|
-
font-family: var(--label-face);
|
|
43
|
-
font-size: var(--label-size);
|
|
44
|
-
letter-spacing: var(--label-space);
|
|
45
|
-
text-transform: var(--label-case);
|
|
46
|
-
color: var(--ink-faint);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
40
|
.backlog-count {
|
|
50
41
|
font-family: var(--font-mono);
|
|
51
42
|
font-size: 11px;
|
|
@@ -67,27 +58,6 @@
|
|
|
67
58
|
|
|
68
59
|
/* In-flight table: release action on the viewer's own claims. */
|
|
69
60
|
.wip__action { text-align: right; white-space: nowrap; }
|
|
70
|
-
.wip__release {
|
|
71
|
-
display: inline-flex;
|
|
72
|
-
align-items: center;
|
|
73
|
-
justify-content: center;
|
|
74
|
-
min-height: 30px;
|
|
75
|
-
padding: 0 14px;
|
|
76
|
-
background: none;
|
|
77
|
-
border: var(--line) solid var(--rule);
|
|
78
|
-
color: var(--ink-soft);
|
|
79
|
-
border-radius: var(--radius-pill);
|
|
80
|
-
font-size: 13px;
|
|
81
|
-
text-transform: lowercase;
|
|
82
|
-
cursor: pointer;
|
|
83
|
-
transition: background 150ms, color 150ms, border-color 150ms;
|
|
84
|
-
}
|
|
85
|
-
.wip__release:hover:not(:disabled) {
|
|
86
|
-
background: var(--danger-weak);
|
|
87
|
-
color: var(--danger);
|
|
88
|
-
border-color: color-mix(in srgb, var(--danger) 40%, transparent);
|
|
89
|
-
}
|
|
90
|
-
.wip__release:disabled { opacity: 0.55; cursor: default; }
|
|
91
61
|
.wip__mine-none { color: var(--ink-faint); }
|
|
92
62
|
.wip__row--mine { background: var(--accent-tint); }
|
|
93
63
|
|
|
@@ -121,7 +91,7 @@
|
|
|
121
91
|
.history__when { color: var(--ink-soft); font-size: 12.5px; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
122
92
|
|
|
123
93
|
|
|
124
|
-
/* ---- task 1759: multi-select
|
|
94
|
+
/* ---- task 1759: multi-select (the toolbar AND the checkbox are in style.css) ----
|
|
125
95
|
Re-cut for the 24px floor and the world (task 1003304). The floating dark
|
|
126
96
|
pill is gone: it painted an INVERTED surface (background: var(--ink)) with
|
|
127
97
|
three rgba(255,255,255,…) literals on top, which was correct only for as long
|
|
@@ -130,38 +100,6 @@
|
|
|
130
100
|
content column instead, inset by the rail, so it never has to invert. */
|
|
131
101
|
|
|
132
102
|
.wip__select-cell { width: 1%; text-align: center; }
|
|
133
|
-
.wip__select {
|
|
134
|
-
appearance: none;
|
|
135
|
-
-webkit-appearance: none;
|
|
136
|
-
position: relative;
|
|
137
|
-
width: 24px;
|
|
138
|
-
height: 24px;
|
|
139
|
-
margin: 0;
|
|
140
|
-
cursor: pointer;
|
|
141
|
-
}
|
|
142
|
-
.wip__select::before {
|
|
143
|
-
content: "";
|
|
144
|
-
position: absolute;
|
|
145
|
-
left: 5px;
|
|
146
|
-
top: 5px;
|
|
147
|
-
width: 14px;
|
|
148
|
-
height: 14px;
|
|
149
|
-
border: var(--line) solid var(--rule);
|
|
150
|
-
border-radius: 3px;
|
|
151
|
-
}
|
|
152
|
-
.wip__select:checked::before { background: var(--accent); border-color: var(--accent); }
|
|
153
|
-
.wip__select:checked::after {
|
|
154
|
-
content: "";
|
|
155
|
-
position: absolute;
|
|
156
|
-
left: 9px;
|
|
157
|
-
top: 8px;
|
|
158
|
-
width: 4px;
|
|
159
|
-
height: 8px;
|
|
160
|
-
border: solid var(--accent-on);
|
|
161
|
-
border-width: 0 1.5px 1.5px 0;
|
|
162
|
-
transform: rotate(42deg);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
103
|
/* ---- "Only mine" lens (the board's tab-wide filter; goal board's To-do bar
|
|
166
104
|
borrows the same class) --------------------------------------------------
|
|
167
105
|
Deliberately QUIETER than the filter bar below it: a checkbox in the label
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
|
|
172
172
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
173
173
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
174
|
-
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
174
|
+
<script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
|
|
175
175
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
176
176
|
<script src="/builders/palette.js" defer></script>
|
|
177
177
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
|
|
@@ -189,7 +189,8 @@
|
|
|
189
189
|
<script src="/builders/work-summary.js" defer></script>
|
|
190
190
|
<script src="/builders/task-detail.js?v=2026-07-03-taskdetail" defer></script>
|
|
191
191
|
<script src="/builders/brand-apply.js?v=2026-06-19-r56" defer></script>
|
|
192
|
-
<script src="/builders/
|
|
192
|
+
<script src="/builders/bulk-toolbar.js" defer></script>
|
|
193
|
+
<script src="/builders/work.js?v=2026-09-30-bulktoolbar" defer></script>
|
|
193
194
|
<script src="/builders/copy-on-click.js?v=2026-06-01-copy-1" defer></script>
|
|
194
195
|
</body>
|
|
195
196
|
</html>
|
|
@@ -187,9 +187,9 @@
|
|
|
187
187
|
// The hall's ONE claim/release action (task 1003458). Everything this board
|
|
188
188
|
// used to hand-roll four times — the POST, the refusal sentence, the
|
|
189
189
|
// stale-view refresh, the all-or-nothing batch report — lives in
|
|
190
|
-
// claim-action.js now; doClaim/doRelease
|
|
191
|
-
//
|
|
192
|
-
//
|
|
190
|
+
// claim-action.js now; doClaim/doRelease below keep only what is
|
|
191
|
+
// genuinely this page: button text and the re-entrancy guard (the bulk
|
|
192
|
+
// toolbar is bulk-toolbar.js, built from this same action). friendlyBatchReason is injected rather
|
|
193
193
|
// than imported there, so claim-action.js stays dependency-free.
|
|
194
194
|
// refreshLive is a hoisted declaration; toast and API are already bound above.
|
|
195
195
|
const claimAction = makeClaimAction({
|
|
@@ -291,18 +291,6 @@
|
|
|
291
291
|
// String(claim_id) -> task_id for the viewer's OWN in-flight claims (release
|
|
292
292
|
// is only ever offered on those rows to begin with — see renderInFlight).
|
|
293
293
|
const selection = { claim: new Set(), release: new Map() };
|
|
294
|
-
// Last server dry-run (POST /claims/batch/validate) result, keyed by the
|
|
295
|
-
// sorted selected-id set so an unrelated re-render doesn't refire it.
|
|
296
|
-
const validateCache = { key: '', parallelSafe: null, byId: new Map() };
|
|
297
|
-
let validateDebounce = null;
|
|
298
|
-
// Per-task dependency edges for the "can these be sequenced?" cycle check —
|
|
299
|
-
// fetched lazily via GET /tasks/:id (task-detail.js's call) since the
|
|
300
|
-
// claimable feed doesn't carry dependencies[] per row. Stable for a ready
|
|
301
|
-
// task within one page session, so caching by id is safe.
|
|
302
|
-
const depsCache = new Map(); // task_id -> [dependency task_id, ...]
|
|
303
|
-
const sequentialCache = { key: '', order: null };
|
|
304
|
-
let sequentialDebounce = null;
|
|
305
|
-
|
|
306
294
|
// task 1756: two tasks can't run in parallel if their touches[] overlap — exact
|
|
307
295
|
// path match or a directory-prefix either way (mirrors the server's touchesOverlap
|
|
308
296
|
// / the goals-page conflict estimate). Empty touches never conflict.
|
|
@@ -700,207 +688,18 @@
|
|
|
700
688
|
}
|
|
701
689
|
}
|
|
702
690
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
validateCache.key = key;
|
|
716
|
-
validateCache.parallelSafe = out.parallelSafe;
|
|
717
|
-
validateCache.byId = out.byId;
|
|
718
|
-
renderBulkToolbar();
|
|
719
|
-
}, 350);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
async function fetchDeps(taskId) {
|
|
723
|
-
if (depsCache.has(taskId)) return depsCache.get(taskId);
|
|
724
|
-
let deps = [];
|
|
725
|
-
try {
|
|
726
|
-
const d = await getJSON(`${API}/tasks/${encodeURIComponent(taskId)}`, { softAuth: true });
|
|
727
|
-
if (d && d.task && Array.isArray(d.task.dependencies)) {
|
|
728
|
-
deps = d.task.dependencies.map((x) => Number(x.id)).filter((x) => Number.isFinite(x));
|
|
729
|
-
}
|
|
730
|
-
} catch (_) { /* treat as no known dependencies */ }
|
|
731
|
-
depsCache.set(taskId, deps);
|
|
732
|
-
return deps;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// Kahn's algorithm restricted to edges BETWEEN the selected ids (an
|
|
736
|
-
// out-of-selection dependency doesn't block sequencing the selection
|
|
737
|
-
// itself). Returns the topological order, or null if the restricted graph
|
|
738
|
-
// has a cycle (can't be sequenced).
|
|
739
|
-
function topoOrder(ids, edgesByTask) {
|
|
740
|
-
const remaining = new Set(ids);
|
|
741
|
-
const order = [];
|
|
742
|
-
let progressed = true;
|
|
743
|
-
while (remaining.size && progressed) {
|
|
744
|
-
progressed = false;
|
|
745
|
-
for (const id of Array.from(remaining)) {
|
|
746
|
-
const deps = (edgesByTask.get(id) || []).filter((d) => remaining.has(d));
|
|
747
|
-
if (deps.length === 0) {
|
|
748
|
-
order.push(id);
|
|
749
|
-
remaining.delete(id);
|
|
750
|
-
progressed = true;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
return remaining.size ? null : order;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
function scheduleSequentialCheck(key, ids) {
|
|
758
|
-
if (sequentialDebounce) clearTimeout(sequentialDebounce);
|
|
759
|
-
sequentialDebounce = setTimeout(async () => {
|
|
760
|
-
const edgesByTask = new Map();
|
|
761
|
-
await Promise.all(ids.map(async (id) => { edgesByTask.set(id, await fetchDeps(id)); }));
|
|
762
|
-
sequentialCache.key = key;
|
|
763
|
-
sequentialCache.order = topoOrder(ids, edgesByTask);
|
|
764
|
-
renderBulkToolbar();
|
|
765
|
-
}, 350);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
function renderBulkToolbar() {
|
|
769
|
-
const bar = $('#bulk-toolbar');
|
|
770
|
-
if (!bar) return;
|
|
771
|
-
if (selectionTotal() === 0) { bar.hidden = true; return; }
|
|
772
|
-
bar.hidden = false;
|
|
773
|
-
|
|
774
|
-
const countEl = $('#bulk-count');
|
|
775
|
-
if (countEl) {
|
|
776
|
-
const parts = [];
|
|
777
|
-
if (selection.claim.size) parts.push(`${selection.claim.size} to claim`);
|
|
778
|
-
if (selection.release.size) parts.push(`${selection.release.size} to release`);
|
|
779
|
-
countEl.textContent = parts.join(' · ');
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
const claimBtn = $('#bulk-claim-btn');
|
|
783
|
-
if (claimBtn) {
|
|
784
|
-
claimBtn.disabled = selection.claim.size === 0;
|
|
785
|
-
claimBtn.textContent = selection.claim.size > 1 ? `Bulk claim (${selection.claim.size})` : 'Bulk claim';
|
|
786
|
-
}
|
|
787
|
-
const releaseBtn = $('#bulk-release-btn');
|
|
788
|
-
if (releaseBtn) {
|
|
789
|
-
releaseBtn.disabled = selection.release.size === 0;
|
|
790
|
-
releaseBtn.textContent = selection.release.size > 1 ? `Bulk release (${selection.release.size})` : 'Bulk release';
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
const parallelBtn = $('#bulk-parallel-btn');
|
|
794
|
-
if (parallelBtn) {
|
|
795
|
-
if (selection.claim.size < 2) {
|
|
796
|
-
parallelBtn.disabled = true;
|
|
797
|
-
parallelBtn.title = 'Select at least 2 waiting tasks to copy a parallel-session prompt.';
|
|
798
|
-
} else {
|
|
799
|
-
const ids = selectedClaimIds();
|
|
800
|
-
const key = selectionKey(ids);
|
|
801
|
-
if (validateCache.key === key && validateCache.parallelSafe != null) {
|
|
802
|
-
parallelBtn.disabled = !validateCache.parallelSafe;
|
|
803
|
-
if (!validateCache.parallelSafe) {
|
|
804
|
-
const bad = [...validateCache.byId.values()].filter((r) => !r.valid);
|
|
805
|
-
const detail = bad.map((r) => `#${r.task_id}: ${(r.reasons || []).map(friendlyBatchReason).join(', ')}`).join(' · ');
|
|
806
|
-
parallelBtn.title = detail || 'Selected tasks conflict and cannot run in parallel.';
|
|
807
|
-
} else {
|
|
808
|
-
parallelBtn.title = '';
|
|
809
|
-
}
|
|
810
|
-
} else {
|
|
811
|
-
parallelBtn.disabled = true;
|
|
812
|
-
parallelBtn.title = 'Checking for conflicts…';
|
|
813
|
-
scheduleValidate(key, ids);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
const sequentialBtn = $('#bulk-sequential-btn');
|
|
819
|
-
if (sequentialBtn) {
|
|
820
|
-
if (selection.claim.size < 2) {
|
|
821
|
-
sequentialBtn.disabled = true;
|
|
822
|
-
sequentialBtn.title = 'Select at least 2 waiting tasks to copy a sequential prompt.';
|
|
823
|
-
} else {
|
|
824
|
-
const ids = selectedClaimIds();
|
|
825
|
-
const key = selectionKey(ids);
|
|
826
|
-
if (sequentialCache.key === key) {
|
|
827
|
-
sequentialBtn.disabled = sequentialCache.order == null;
|
|
828
|
-
sequentialBtn.title = sequentialCache.order == null
|
|
829
|
-
? 'These tasks depend on each other in a cycle and cannot be ordered.'
|
|
830
|
-
: '';
|
|
831
|
-
} else {
|
|
832
|
-
sequentialBtn.disabled = true;
|
|
833
|
-
sequentialBtn.title = 'Checking dependency order…';
|
|
834
|
-
scheduleSequentialCheck(key, ids);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function clearSelection() {
|
|
841
|
-
selection.claim.clear();
|
|
842
|
-
selection.release.clear();
|
|
843
|
-
renderClaimable();
|
|
844
|
-
renderInFlight();
|
|
845
|
-
renderBulkToolbar();
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function buildBulkParallelPrompt(tasks) {
|
|
849
|
-
const hallBase = location.hostname.startsWith('builders.') ? location.origin : `${location.origin}/builders`;
|
|
850
|
-
const lines = [
|
|
851
|
-
`I want to work on ${tasks.length} Bongos tasks IN PARALLEL — open ${tasks.length} separate Claude Code sessions, each in its own worktree, one per task below, so they can run at the same time without touching each other's files.`,
|
|
852
|
-
'',
|
|
853
|
-
];
|
|
854
|
-
tasks.forEach((t, i) => {
|
|
855
|
-
lines.push(`Session ${i + 1} — task ${t.id} (${t.title}) — ${hallBase}#/task/${t.id}`);
|
|
856
|
-
lines.push(`Claim it with /builder-claim ${t.id}, follow its acceptance criteria, stay within touches[], ship via /builder-ship when done.`);
|
|
857
|
-
lines.push('');
|
|
858
|
-
});
|
|
859
|
-
return lines.join('\n').trim();
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
function buildBulkSequentialPrompt(tasks) {
|
|
863
|
-
const hallBase = location.hostname.startsWith('builders.') ? location.origin : `${location.origin}/builders`;
|
|
864
|
-
const lines = [
|
|
865
|
-
`I want to work on ${tasks.length} Bongos tasks IN ORDER, one after another in this same session — claim, build, and ship each before moving to the next.`,
|
|
866
|
-
'',
|
|
867
|
-
];
|
|
868
|
-
tasks.forEach((t, i) => {
|
|
869
|
-
lines.push(`${i + 1}. Task ${t.id} (${t.title}) — ${hallBase}#/task/${t.id}`);
|
|
870
|
-
});
|
|
871
|
-
lines.push('');
|
|
872
|
-
lines.push('For each: /builder-claim <id>, follow its acceptance criteria, stay within touches[], /builder-ship <id> before starting the next.');
|
|
873
|
-
return lines.join('\n');
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
async function doBulkClaim() {
|
|
877
|
-
const ids = selectedClaimIds();
|
|
878
|
-
if (!ids.length) return;
|
|
879
|
-
const btn = $('#bulk-claim-btn');
|
|
880
|
-
if (btn) { btn.disabled = true; btn.textContent = 'Claiming…'; }
|
|
881
|
-
try {
|
|
882
|
-
const out = await claimAction.claimMany(ids);
|
|
883
|
-
if (out.ok) selection.claim.clear();
|
|
884
|
-
} finally {
|
|
885
|
-
renderBulkToolbar();
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
async function doBulkRelease() {
|
|
890
|
-
const entries = [...selection.release.entries()]; // [claimId, taskId]
|
|
891
|
-
if (!entries.length) return;
|
|
892
|
-
const label = entries.length === 1 ? `#${entries[0][1]}` : `${entries.length} tasks`;
|
|
893
|
-
const pronoun = entries.length === 1 ? 'It returns' : 'They return';
|
|
894
|
-
if (!window.confirm(`Release ${label}? ${pronoun} to the queue and no ${curLabel()} are earned.`)) return;
|
|
895
|
-
const btn = $('#bulk-release-btn');
|
|
896
|
-
if (btn) { btn.disabled = true; btn.textContent = 'Releasing…'; }
|
|
897
|
-
try {
|
|
898
|
-
const out = await claimAction.releaseMany(entries);
|
|
899
|
-
if (out.ok) selection.release.clear();
|
|
900
|
-
} finally {
|
|
901
|
-
renderBulkToolbar();
|
|
902
|
-
}
|
|
903
|
-
}
|
|
691
|
+
// The toolbar itself (render, bulk claim/release, the two prompts) is the
|
|
692
|
+
// hall's shared one — bulk-toolbar.js. This page keeps the selection and how
|
|
693
|
+
// Clear repaints its own cards.
|
|
694
|
+
const bulk = window.OTBBulkToolbar.makeBulkToolbar({
|
|
695
|
+
ids: { bar: 'bulk-toolbar', count: 'bulk-count', claim: 'bulk-claim-btn', release: 'bulk-release-btn', parallel: 'bulk-parallel-btn', sequential: 'bulk-sequential-btn', clear: 'bulk-clear-btn' },
|
|
696
|
+
selection,
|
|
697
|
+
findTask: (id) => taskState.all.find((t) => Number(t.id) === id),
|
|
698
|
+
claimAction, getJSON, API, copyText, toast, curLabel, describeReason: friendlyBatchReason,
|
|
699
|
+
onClear: () => { renderClaimable(); renderInFlight(); },
|
|
700
|
+
});
|
|
701
|
+
function renderBulkToolbar() { bulk.render(); }
|
|
702
|
+
function clearSelection() { bulk.clearSelection(); }
|
|
904
703
|
|
|
905
704
|
async function doClaim(taskId, btn) {
|
|
906
705
|
const key = `claim:${taskId}`;
|
|
@@ -1373,26 +1172,8 @@
|
|
|
1373
1172
|
doRelease(rel.dataset.claimId, rel.dataset.taskId, rel);
|
|
1374
1173
|
});
|
|
1375
1174
|
|
|
1376
|
-
// task 1759: the bottom bulk-action toolbar.
|
|
1377
|
-
|
|
1378
|
-
$('#bulk-release-btn')?.addEventListener('click', doBulkRelease);
|
|
1379
|
-
$('#bulk-clear-btn')?.addEventListener('click', clearSelection);
|
|
1380
|
-
$('#bulk-parallel-btn')?.addEventListener('click', async () => {
|
|
1381
|
-
const ids = selectedClaimIds();
|
|
1382
|
-
const tasks = ids.map((id) => taskState.all.find((t) => Number(t.id) === id)).filter(Boolean);
|
|
1383
|
-
if (tasks.length < 2) return;
|
|
1384
|
-
const ok = await copyText(buildBulkParallelPrompt(tasks));
|
|
1385
|
-
toast(ok ? `Parallel-session prompt for ${tasks.length} tasks copied.` : 'Could not copy to the clipboard.');
|
|
1386
|
-
});
|
|
1387
|
-
$('#bulk-sequential-btn')?.addEventListener('click', async () => {
|
|
1388
|
-
const ids = selectedClaimIds();
|
|
1389
|
-
const key = selectionKey(ids);
|
|
1390
|
-
if (sequentialCache.key !== key || sequentialCache.order == null) return;
|
|
1391
|
-
const tasks = sequentialCache.order.map((id) => taskState.all.find((t) => Number(t.id) === id)).filter(Boolean);
|
|
1392
|
-
if (tasks.length < 2) return;
|
|
1393
|
-
const ok = await copyText(buildBulkSequentialPrompt(tasks));
|
|
1394
|
-
toast(ok ? `Sequential prompt for ${tasks.length} tasks copied.` : 'Could not copy to the clipboard.');
|
|
1395
|
-
});
|
|
1175
|
+
// task 1759: the bottom bulk-action toolbar (shared: bulk-toolbar.js).
|
|
1176
|
+
bulk.wire();
|
|
1396
1177
|
}
|
|
1397
1178
|
|
|
1398
1179
|
document.addEventListener('DOMContentLoaded', () => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Project founding mode — the band and the asleep rooms, worn by the existing hall (task 1004422)
|
|
2
|
+
|
|
3
|
+
**BV2.PS03 (goal 1000121).**
|
|
4
|
+
- Design of record: [`docs/design/project-startup-direction.md`](../../../docs/design/project-startup-direction.md), part 2 of the genesis table.
|
|
5
|
+
- Spec: [`docs/specs/<redacted>.md`](../../../docs/specs/<redacted>.md).
|
|
6
|
+
|
|
7
|
+
## The rule this record exists to keep
|
|
8
|
+
|
|
9
|
+
The owner's blend rule: **genesis is a MODE the existing hall wears, never a second hall.** Founding mode follows the social-mode precedent exactly: one attribute on the document, `html[data-founding="on"]`, read from `/me`. It adds two things in this task:
|
|
10
|
+
|
|
11
|
+
- **the founding band**, a strip created as the first child of `.app-body`. It sits above the sticky topbar, which is kept as it is.
|
|
12
|
+
- **the asleep state on today's nav items**: dimmed, with an "at <stage>" or "when alive" tag that reads in the open rail.
|
|
13
|
+
|
|
14
|
+
Nothing else changes: no new page, no new route, no new nav item. **A project that is not founding makes `applyFounding` return before it touches a single node**, and `tests/founding_mode.mjs` proves that with a proxy that records every DOM call. Switching the mode off REMOVES the band and every class and tag, so the hall is restored exactly; it does not merely hide them. It is cosmetic: an asleep room still opens, and nothing is enforced.
|
|
15
|
+
|
|
16
|
+
## Where the answer comes from
|
|
17
|
+
|
|
18
|
+
- `modules/onboarding/founding-mode.js` holds three `project_settings` keys (`founding.active`, `founding.stage`, `founding.started_at`) and computes which rooms sleep.
|
|
19
|
+
- `asleep` is keyed by `shell.js` NAV ids, so the client never re-derives it. A test holds the two maps together.
|
|
20
|
+
- It **fails closed to OFF**: an absent, unreadable or unknown stage reads as not founding. A hall wrongly stuck asleep would be the hijack; a hall wrongly awake only loses a banner.
|
|
21
|
+
- `/me.founding_mode` carries the answer. Both `applyAccess` callers pass it (`shell.js` `probeMe` and `hall-render.js`).
|
|
22
|
+
|
|
23
|
+
## Rendered, and the phone rule
|
|
24
|
+
|
|
25
|
+
work-founding.states.json renders an ordinary hall page with a founding /me (kit fixture me-founding.json, the real shaper's output). The kit's /me honours stub.fixtures for this. At 390 and 320 the band wraps, the sentence steps aside and the stage and link stay visible, because the first render measured the band widening the page to 483px.
|
|
26
|
+
|
|
27
|
+
## What is deliberately not here
|
|
28
|
+
|
|
29
|
+
- The genesis home, the stage tasks and the "what's this?" destination belong to PS08 (task 1004423). The band links home until then.
|
|
30
|
+
- The founding mark belongs to PS10 (task 1004424).
|
|
31
|
+
- Starting founding on a new project and moving its stage is `setFoundingMode`, on the onboarding port, and PS08 drives it. Nothing in this task writes it.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/onboarding/founding-mode.js — PROJECT FOUNDING MODE: is this project still
|
|
4
|
+
// being founded, and which stage of genesis is it on? (task 1004422 / BV2.PS03, goal
|
|
5
|
+
// 1000121; design of record docs/design/project-startup-direction.md.)
|
|
6
|
+
//
|
|
7
|
+
// THE BLEND RULE THIS FILE SERVES (owner, 2026-09-30). Genesis is a MODE the existing
|
|
8
|
+
// builders hall wears, never a second hall. So this file answers ONE question for the
|
|
9
|
+
// hall shell — founding or not, and at which stage — and the shell stamps the answer on
|
|
10
|
+
// the document the way it stamps social mode (`html[data-social]`). Nothing here renders
|
|
11
|
+
// anything, and a project that is not founding gets `null`, which the shell reads as OFF:
|
|
12
|
+
// the hall then renders exactly as it does today.
|
|
13
|
+
//
|
|
14
|
+
// WHY THE ONBOARDING MODULE. Founding is the FOUNDER-side counterpart of the joiner
|
|
15
|
+
// state machine this module already owns (onboarding-state.js graduates a newcomer; this
|
|
16
|
+
// graduates a project). It lives in project_settings (migration core_231) through the
|
|
17
|
+
// doorway's one accessor, like mingle.enabled, rather than a table of its own — one row
|
|
18
|
+
// per project is the whole state, and "do not fork a second settings store" is that
|
|
19
|
+
// accessor's own rule.
|
|
20
|
+
//
|
|
21
|
+
// FAIL POSTURE: CLOSED, i.e. OFF. An unreadable, absent, or malformed state is NOT
|
|
22
|
+
// founding. The failure this guards against is the owner's no-hijack condition: a hall
|
|
23
|
+
// wrongly stuck in founding mode would dim rooms that are really open. A hall wrongly
|
|
24
|
+
// NOT in founding mode only loses a banner. So every doubtful read answers null.
|
|
25
|
+
|
|
26
|
+
// The genesis stages, in order. 'decide' is the pre-stage the founder plans in; the
|
|
27
|
+
// five after it are the stages proper (spec C4). The order IS the data: a room wakes
|
|
28
|
+
// when the stage that wakes it has been left behind.
|
|
29
|
+
const STAGES = Object.freeze(['decide', 'scope', 'strategize', 'charter', 'legalize', 'legislation']);
|
|
30
|
+
|
|
31
|
+
// Which hall rooms each finished stage wakes (the direction doc's "How rooms wake"
|
|
32
|
+
// table). Keyed by the shell's NAV item ids, so the client applies it without a second
|
|
33
|
+
// mapping of its own — a surface that re-derived this could disagree with the server.
|
|
34
|
+
// 'community' wakes only when the planet is ALIVE, which is when founding mode switches
|
|
35
|
+
// off entirely, so during founding it is always asleep.
|
|
36
|
+
const WAKES = Object.freeze({
|
|
37
|
+
scope: Object.freeze(['roadmap', 'goals', 'goal-map']),
|
|
38
|
+
strategize: Object.freeze(['work']),
|
|
39
|
+
charter: Object.freeze(['board-room']),
|
|
40
|
+
legalize: Object.freeze(['economy']),
|
|
41
|
+
legislation: Object.freeze(['ranks']),
|
|
42
|
+
});
|
|
43
|
+
const WAKES_WHEN_ALIVE = Object.freeze(['collab', 'roster']);
|
|
44
|
+
|
|
45
|
+
// The project_settings keys. Namespaced 'founding.' like 'mingle.' and 'attention.'.
|
|
46
|
+
const KEYS = Object.freeze({
|
|
47
|
+
ACTIVE: 'founding.active',
|
|
48
|
+
STAGE: 'founding.stage',
|
|
49
|
+
STARTED_AT: 'founding.started_at',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// The only 'on' spelling accepted, mirroring mingle's consent-gate parse: an explicit,
|
|
53
|
+
// recognised yes. Anything else is off.
|
|
54
|
+
function parseActive(raw) {
|
|
55
|
+
if (raw == null) return false;
|
|
56
|
+
return ['true', '1', 'on', 'yes'].includes(String(raw).trim().toLowerCase());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// asleepRooms(stage) — PURE. For a known stage, { navId: wakingStage } for every room
|
|
60
|
+
// still asleep. A room is awake once its waking stage is BEHIND the current one.
|
|
61
|
+
function asleepRooms(stage) {
|
|
62
|
+
const at = STAGES.indexOf(stage);
|
|
63
|
+
if (at < 0) return null;
|
|
64
|
+
const asleep = {};
|
|
65
|
+
for (const [wakingStage, ids] of Object.entries(WAKES)) {
|
|
66
|
+
if (STAGES.indexOf(wakingStage) >= at) for (const id of ids) asleep[id] = wakingStage;
|
|
67
|
+
}
|
|
68
|
+
for (const id of WAKES_WHEN_ALIVE) asleep[id] = 'alive';
|
|
69
|
+
return asleep;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// dayOf(startedAt, now) — PURE. Day 1 is the founding day. null when unreadable, so a
|
|
73
|
+
// bad timestamp drops the day from the band rather than printing "day NaN".
|
|
74
|
+
function dayOf(startedAt, now = new Date()) {
|
|
75
|
+
if (!startedAt) return null;
|
|
76
|
+
const t = new Date(startedAt).getTime();
|
|
77
|
+
if (!Number.isFinite(t)) return null;
|
|
78
|
+
const days = Math.floor((now.getTime() - t) / 86400000) + 1;
|
|
79
|
+
return days >= 1 ? days : 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// shapeFoundingMode(rows, now) — PURE. rows = { active, stage, startedAt } raw strings
|
|
83
|
+
// (or nulls). Returns the /me payload, or null for "not founding" in any form.
|
|
84
|
+
function shapeFoundingMode(rows, now = new Date()) {
|
|
85
|
+
if (!rows || !parseActive(rows.active)) return null;
|
|
86
|
+
const stage = rows.stage == null ? 'decide' : String(rows.stage).trim().toLowerCase();
|
|
87
|
+
const asleep = asleepRooms(stage);
|
|
88
|
+
if (!asleep) return null; // an unknown stage is not a stage — fail closed (OFF)
|
|
89
|
+
const index = STAGES.indexOf(stage);
|
|
90
|
+
return {
|
|
91
|
+
active: true,
|
|
92
|
+
stage,
|
|
93
|
+
// 0 while deciding, then 1..5 through the stages proper.
|
|
94
|
+
stage_number: index,
|
|
95
|
+
stage_count: STAGES.length - 1,
|
|
96
|
+
day: dayOf(rows.startedAt, now),
|
|
97
|
+
asleep,
|
|
98
|
+
// Who closing the current stage is waiting on (spec D7) — display names, at most
|
|
99
|
+
// five, from the government module's genesis-stage sitting (task 1004429). null
|
|
100
|
+
// when the stage is not sitting, nobody is left to vote, or the read is missing.
|
|
101
|
+
waiting_on: Array.isArray(rows.waitingOn) && rows.waitingOn.length
|
|
102
|
+
? rows.waitingOn.slice(0, 5).map((n) => String(n))
|
|
103
|
+
: null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// foundingModeFor(deps) — the /me read. Resolves the three rows through the doorway's
|
|
108
|
+
// project-settings accessor (injectable for tests). Never throws: any read failure is
|
|
109
|
+
// "not founding".
|
|
110
|
+
// Three primary-key reads in parallel: the doorway's project-settings accessor is
|
|
111
|
+
// single-key by design (one accessor, one table), and a founding project is a short
|
|
112
|
+
// window of a project's life, so a batched read is not worth a second accessor.
|
|
113
|
+
async function foundingModeFor(deps = {}) {
|
|
114
|
+
const get = deps.getSetting || require('../../src/module-api').projectSettings.get;
|
|
115
|
+
let shaped;
|
|
116
|
+
try {
|
|
117
|
+
const [active, stage, startedAt] = await Promise.all([
|
|
118
|
+
get(KEYS.ACTIVE), get(KEYS.STAGE), get(KEYS.STARTED_AT),
|
|
119
|
+
]);
|
|
120
|
+
shaped = shapeFoundingMode({
|
|
121
|
+
active: active && active.value,
|
|
122
|
+
stage: stage && stage.value,
|
|
123
|
+
startedAt: startedAt && startedAt.value,
|
|
124
|
+
}, deps.now || new Date());
|
|
125
|
+
} catch (_) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (!shaped) return null;
|
|
129
|
+
// The waiting-on names come from the government module's genesis-stage read
|
|
130
|
+
// (genesisStageStatus on its port, task 1004429: one entry per stage, each with
|
|
131
|
+
// a state and, while sitting, the members still to vote under the item's own
|
|
132
|
+
// snapshot). Optional and fail-open: a missing port or a failed read leaves the
|
|
133
|
+
// band without the names, never without the mode.
|
|
134
|
+
try {
|
|
135
|
+
const waitingOnFor = deps.waitingOnFor || waitingOnFromGovernment(
|
|
136
|
+
deps.government !== undefined ? deps.government : require('../../src/module-api').resolveOptional('government'),
|
|
137
|
+
);
|
|
138
|
+
if (waitingOnFor) {
|
|
139
|
+
const names = await waitingOnFor(shaped.stage);
|
|
140
|
+
if (Array.isArray(names) && names.length) shaped.waiting_on = names.slice(0, 5).map((n) => String(n));
|
|
141
|
+
}
|
|
142
|
+
} catch (_) { /* fail open: the band shows the stage without the names */ }
|
|
143
|
+
return shaped;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// waitingOnFromGovernment(gov) — a stage -> [display name] reader over the government
|
|
147
|
+
// port's genesisStageStatus(), or null when the port does not offer it. Only a stage
|
|
148
|
+
// that is SITTING waits on anyone; not_put, passed and returned wait on nobody.
|
|
149
|
+
function waitingOnFromGovernment(gov) {
|
|
150
|
+
if (!gov || typeof gov.genesisStageStatus !== 'function') return null;
|
|
151
|
+
return async (stage) => {
|
|
152
|
+
const status = await gov.genesisStageStatus();
|
|
153
|
+
const row = status && Array.isArray(status.stages) ? status.stages.find((s) => s.key === stage) : null;
|
|
154
|
+
if (!row || row.state !== 'sitting' || !Array.isArray(row.waiting_on)) return [];
|
|
155
|
+
return row.waiting_on.map((m) => m.display_name || m.github_login).filter(Boolean);
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// setFoundingMode({ active, stage }, builderId, deps) — the write the genesis home
|
|
160
|
+
// (task 1004423) drives: start founding on a new project, move the stage, and switch
|
|
161
|
+
// founding off when the planet comes alive. Refuses an unknown stage rather than storing
|
|
162
|
+
// a value the read would then treat as OFF. Starting stamps started_at once.
|
|
163
|
+
async function setFoundingMode({ active, stage } = {}, builderId = null, deps = {}) {
|
|
164
|
+
const settings = require('../../src/module-api').projectSettings;
|
|
165
|
+
const get = deps.getSetting || settings.get;
|
|
166
|
+
const set = deps.setSetting || settings.set;
|
|
167
|
+
if (stage != null && !STAGES.includes(stage)) {
|
|
168
|
+
const err = new Error(`unknown founding stage '${stage}'`);
|
|
169
|
+
err.code = 'bad_founding_stage';
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
if (active === true) {
|
|
173
|
+
const started = await get(KEYS.STARTED_AT);
|
|
174
|
+
if (!started || !started.value) await set(KEYS.STARTED_AT, (deps.now || new Date()).toISOString(), builderId);
|
|
175
|
+
}
|
|
176
|
+
if (typeof active === 'boolean') await set(KEYS.ACTIVE, active ? 'true' : 'false', builderId);
|
|
177
|
+
if (stage != null) await set(KEYS.STAGE, stage, builderId);
|
|
178
|
+
return foundingModeFor({ getSetting: get, now: deps.now });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
STAGES, WAKES, WAKES_WHEN_ALIVE, KEYS,
|
|
183
|
+
parseActive, asleepRooms, dayOf, shapeFoundingMode, waitingOnFromGovernment,
|
|
184
|
+
foundingModeFor, setFoundingMode,
|
|
185
|
+
};
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
const onboardingState = require('./onboarding-state');
|
|
42
42
|
const newcomerRestock = require('./newcomer-restock');
|
|
43
|
+
const foundingMode = require('./founding-mode');
|
|
43
44
|
|
|
44
45
|
module.exports = {
|
|
45
46
|
// --- the newcomer onboarding state machine (onboarding-state.js) ---------
|
|
@@ -53,6 +54,12 @@ module.exports = {
|
|
|
53
54
|
markStage: (...a) => onboardingState.markStage(...a),
|
|
54
55
|
broadcastGraduation: (...a) => onboardingState.broadcastGraduation(...a),
|
|
55
56
|
|
|
57
|
+
// --- project founding mode (founding-mode.js, task 1004422) ---------------
|
|
58
|
+
// me.js reads it for the hall shell (null = not founding = the hall as today);
|
|
59
|
+
// the genesis home (task 1004423) drives the write.
|
|
60
|
+
foundingModeFor: (...a) => foundingMode.foundingModeFor(...a),
|
|
61
|
+
setFoundingMode: (...a) => foundingMode.setFoundingMode(...a),
|
|
62
|
+
|
|
56
63
|
// --- the evergreen newcomer-chore restock (newcomer-restock.js) ----------
|
|
57
64
|
// restock the just-claimed chore's shelf (claims.js / tasks.js); the Archon
|
|
58
65
|
// backstop tops every chore up (tasks.js). SOURCE_TAG is the marker the claim
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"builder": {
|
|
3
|
+
"id": "1",
|
|
4
|
+
"github_login": "builder",
|
|
5
|
+
"display_name": "A Builder",
|
|
6
|
+
"avatar_url": "/__kit/avatar.svg",
|
|
7
|
+
"rank": "archon"
|
|
8
|
+
},
|
|
9
|
+
"founding_mode": {
|
|
10
|
+
"active": true,
|
|
11
|
+
"stage": "strategize",
|
|
12
|
+
"stage_number": 2,
|
|
13
|
+
"stage_count": 5,
|
|
14
|
+
"day": 9,
|
|
15
|
+
"asleep": {
|
|
16
|
+
"work": "strategize",
|
|
17
|
+
"board-room": "charter",
|
|
18
|
+
"economy": "legalize",
|
|
19
|
+
"ranks": "legislation",
|
|
20
|
+
"collab": "alive",
|
|
21
|
+
"roster": "alive"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|