@bongos/core 1.19.1044 → 1.19.1046
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 +106 -121
- package/docs/adr/0337-the-hub-account-is-the-person-a-hall-account-is-their-seat-in-one-project.md +14 -2
- package/docs/adr/README.md +1 -1
- package/docs/copy-inventory.md +142 -158
- package/docs/copy-registry.json +173 -360
- package/docs/file-map.md +2 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/windows-builders.md +1 -1
- 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/claim-action.js +250 -0
- package/modules/hall-ui/public/collab.css +1 -1
- package/modules/hall-ui/public/collab.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 +28 -44
- package/modules/hall-ui/public/goals-render.js +7 -28
- package/modules/hall-ui/public/goals.html +6 -3
- package/modules/hall-ui/public/government.html +1 -1
- 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/{people.css → roster.css} +7 -11
- package/modules/hall-ui/public/roster.html +5 -5
- package/modules/hall-ui/public/roster.js +6 -7
- package/modules/hall-ui/public/roster.states.json +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/settings.js +2 -2
- package/modules/hall-ui/public/shell.js +3 -10
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +2 -9
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +5 -2
- package/modules/hall-ui/public/work.js +35 -123
- package/modules/hall-ui/records/builders-roster.md +1 -1
- package/modules/hall-ui/records/roster-and-ledger.md +2 -2
- package/modules/platform-identity/account-visibility.js +3 -3
- package/modules/platform-identity/platform-identity.js +2 -2
- package/modules/platform-identity/routes/profile.js +1 -1
- package/modules/platform-identity/routes/sso.js +4 -3
- package/modules/platform-identity/scouting-authz.js +2 -2
- package/modules/platform-identity/tests/platform-identity.mjs +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/auth.js +9 -24
- package/src/bongos/serve-internal.js +13 -19
- package/src/module-api.js +1 -1
- package/tests/account_privacy_flags.mjs +1 -1
- package/tests/auth_resolve_failure.mjs +6 -6
- package/tests/claim_action.mjs +417 -0
- package/tests/core_upgrade_owner_door.mjs +1 -1
- package/tests/edit_hints.mjs +8 -8
- package/tests/effective_visibility_predicate.mjs +1 -1
- package/tests/goal_category_ui.mjs +3 -0
- package/tests/goal_criterion_confirm_ui.mjs +3 -0
- package/tests/goal_forest_page.mjs +3 -0
- package/tests/goal_invite_consent.mjs +3 -1
- package/tests/goal_mine_lens.mjs +3 -0
- package/tests/government_parity.mjs +1 -1
- package/tests/hall_builders_page_pass.mjs +2 -2
- package/tests/hall_mine_lens.mjs +3 -0
- package/tests/hall_nav.mjs +8 -8
- package/tests/hall_page_gate_map.mjs +14 -11
- package/tests/hall_roster_world.mjs +47 -78
- package/tests/module_contributions.mjs +0 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/{scouting_page_gate.mjs → page_gate_second_door.mjs} +34 -29
- package/tests/profile_rollup_consent.mjs +4 -4
- package/tests/profile_route.mjs +3 -26
- package/tests/scouting_owner_access_and_reach.mjs +4 -8
- package/modules/hall-ui/public/people.html +0 -79
- package/modules/hall-ui/public/people.js +0 -321
- package/modules/hall-ui/public/people.states.json +0 -89
- package/tests/people_directory_clip.mjs +0 -269
- package/tests/people_name_only.mjs +0 -263
package/docs/file-map.md
CHANGED
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
│ ├── sessions/ ← CORE-DOMAIN carve (BV1.R82, default-on): the session_records corpus + data plane (upload, cross-builder search/deep-dive), the BFG read-the-room scorer, session-pulse/stale-claim helpers. Registers no port — reached via HTTP POST to its own /sessions route; resolves the economy `reward` port for the cost-plus token reward. CLAUDE.md, module.json, routes/sessions.js, bfg.js, pulse.js.
|
|
135
135
|
│ ├── autonomy/ ← CORE-DOMAIN carve (BV1.R83, default-on): the MAS autonomy surfaces — the $0 deterministic autonomy gate (System 3, inert until armed), the conductor/architect CLIs, and the in-process routine timers (incl. the weekly peer-vote-tally poller). Registers no port; resolves the lifecycle `tallyPeerVotes` doorway. CLAUDE.md, module.json, routes/autonomy.js, autonomy.js.
|
|
136
136
|
│ ├── status-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R84, default-on): the public status dashboard at status.<apex>, moved from public-status/. NO routes/db/port — declares contributes.webSurfaces:[{host:"status.",dir:"public"}], served by the loader web-surface seam (moduleWebSurfaces → serve-internal). CLAUDE.md, module.json, public/{index.html,status.js,style.css,cursors/,snapshots/}.
|
|
137
|
-
│ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, records/ (one file per page family — what each family's redesign settled; CLAUDE.md points at them, and a new record is a new file there so concurrent hall tasks stop colliding on one insertion point, task 1003499), public/{index.html,builders.js,style.css,work,watch,gate,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Four ARCHETYPE sheets sit beside the per-page ones: `record.css` (the task + idea records, task 1003305), `room.css` (the READING ROOMS — the primer + the diagrams index, task 1003306: the reading measure and rhythm, the table of contents, the progress hairline and the framed figure diagram-viewer.js upgrades on both pages), `oversight.css` (the OVERSIGHT family — government, watch, gate, task 1003469 (harbor was its fourth until task 1003890 removed the dev box): the row, the figure band inside a panel, the group head, the inline form, the grant toggle and the shared confirm modal; named oversight because the vocabulary wall bans "governance" on the government surface) and `panel.css` (the PANEL family — settings, profile, task 1003308 (pair was its third until task 1003890 removed the dev box): the row, the switch, the field with its label above, the chip control, the four voices and the sign-in gate; the hall's my-projects page was DELETED in the same task — "my projects" is the hub's user-level page, and the hall keeps no link or redirect; the family's fourth page `not-ready.html` was deleted by task 1003450, along with the emptied `requireNonXenosPage` gate that was the only thing redirecting to it). A page sheet then holds only what one page has and the others do not — `primer.css` the acknowledge gate, `diagrams.css` the legend strip, `gate.css` the hold reasons and file chips, `watch.css` the severities, the report reader and the grade trend, `settings.css` the rail layout, the group head, the token ladder and the voices grid, `profile.css` the identity band and the shelf, `
|
|
137
|
+
│ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, records/ (one file per page family — what each family's redesign settled; CLAUDE.md points at them, and a new record is a new file there so concurrent hall tasks stop colliding on one insertion point, task 1003499), public/{index.html,builders.js,style.css,work,watch,gate,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Four ARCHETYPE sheets sit beside the per-page ones: `record.css` (the task + idea records, task 1003305), `room.css` (the READING ROOMS — the primer + the diagrams index, task 1003306: the reading measure and rhythm, the table of contents, the progress hairline and the framed figure diagram-viewer.js upgrades on both pages), `oversight.css` (the OVERSIGHT family — government, watch, gate, task 1003469 (harbor was its fourth until task 1003890 removed the dev box): the row, the figure band inside a panel, the group head, the inline form, the grant toggle and the shared confirm modal; named oversight because the vocabulary wall bans "governance" on the government surface) and `panel.css` (the PANEL family — settings, profile, task 1003308 (pair was its third until task 1003890 removed the dev box): the row, the switch, the field with its label above, the chip control, the four voices and the sign-in gate; the hall's my-projects page was DELETED in the same task — "my projects" is the hub's user-level page, and the hall keeps no link or redirect; the family's fourth page `not-ready.html` was deleted by task 1003450, along with the emptied `requireNonXenosPage` gate that was the only thing redirecting to it). A page sheet then holds only what one page has and the others do not — `primer.css` the acknowledge gate, `diagrams.css` the legend strip, `gate.css` the hold reasons and file chips, `watch.css` the severities, the report reader and the grade trend, `settings.css` the rail layout, the group head, the token ladder and the voices grid, `profile.css` the identity band and the shelf, `roster.css` the Builders roster's identity cell (was `people.css`, shared with the cross-project `/people` directory until task 1003444 deleted that page — `/people` and its legacy door `/scouting` now 302 to `/roster`), `thinking.css` THE SKY's one sheet (task 1004231 / sky part 3 — `thinking.html` at `/thinking` is the prototype the owner adopted as the design of record, `docs/design/mocks/sky/`, built over `GET /sky` + `GET /sky/mine`; its script is the seven `sky-*.js` files on one shared `window.OTBSky`; it replaced THE ROOM of task 1004098, which survives as the list drawer, and the old `/sky` page, whose URL 301s here; `idea-objects.js` beside it is the shared five-object vocabulary).
|
|
138
138
|
│ ├── lifecycle/ ← CORE-DOMAIN carve (BV1.R86, default-on): the build state machine — tasks · claims · versions · done-when · goals · dependencies · claim→ship→grade→credit · github-push + merge-lock + conflict-resolve + publish-reconciler. The LAST, most-coupled carve; registers the `lifecycle` kernel port (createTask, classifyTaskKind, tallyPeerVotes, work-tracking reads). CLAUDE.md, module.json, routes/{tasks,claims,versions,done-when,goals,dependencies,gate-approvals,analytics}.js, lifecycle.js, github-push.js, merge-lock.js, conflict-resolve.js, publish-reconciler.js, task-visuals.js + task-visual-db.js + routes/visuals.js (the optional ship-time visual, task 1003109 — the route file is NOT named task-*.js on purpose; see the module CLAUDE.md).
|
|
139
139
|
│ ├── copy-desk/ ← CORE-DOMAIN module (task 1003113 / R02 of goal 1000074, default-on): the COPY half of the design contract — ADR 0081 gave design TOKENS a repo-owned source of truth, text had none. Owns the FLAG (anyone in any role marks a string or a whole surface as needing an artist's attention, with a reason required in BOTH the route and a CHECK constraint) and the QUEUE an artist reads it from. READS R01's committed registry (docs/copy-registry.json, generated by scripts/gds/copy-inventory.js) and never writes it. NO LIVE CMS is the hard non-goal: no column here could hold replacement text, and copy_no_cms.mjs pins that. Key is `copy-desk`, not `copy` — the branding contract already owns a `copy` block, so the bare word reds the "core carries no module code" fitness check. R03 (task 1003114, ADR 0233) added the PROPOSAL: an artist writes the replacement wording and it becomes a Bongos task carrying a fenced copy-proposal patch — no row, NO second migration (a proposals table is the forbidden column with extra steps), and scripts/gds/copy-apply.js lands it as a real diff under a claim. CLAUDE.md, module.json, registry.js, flags.js, proposals.js, routes/copy-desk.js, migrations/copy_desk_001_flags.sql, tests/{copy_flags,copy_queue,copy_no_cms,copy_proposals}.mjs. Its hall surface lives with hall-ui (public/copy-desk.{html,js,css}), per the one-key-per-web-surface precedent.
|
|
140
140
|
│ ├── npm-release/ ← the deploy page for a project that publishes an npm package from GitHub (task 1002622, default-OFF — on only in the Bongos hall, which publishes @bongos/core). "Where the work is": every task finished in the last 14 days, placed in the stage it has actually reached (not merged · merged, no version yet · published, not running here · running here, not released · released · in no version), plus the version history — each version's tasks. The version↔task join reads the release-notes.json the package itself carries (scripts/gds/release-notes.js writes it at pack time), streamed out of the NEWEST version's npm tarball in-process (no child process, no sync call) when the running core's own copy stops short. module.json, work.js (the reading + the stage placement — the packument, tarball stream and tar reader it uses are the core's src/bongos/package-registry.js, reached through the doorway's readPackageRegistry since task 1004296), routes/work.js (GET /npm-release/work, gated core.pin.move like the page), public/work.{js,css} (the two sections /deploy lends it: #nr-work, #nr-versions). Task 1004301 added the same line on a task's own page: routes/task-where.js (GET /npm-release/task/:id, any signed-in builder — it shows nothing not already public) + public/task-widget.js, injected into task.html by the loader's uiSections seam (hallWidgetScripts file task-widget.js) to fill #task-contrib on otb:task-shown. Tests sit at the repo root — tests/npm_release_{work,page}.mjs — because a default-off module's own tests/ is skipped by the unit runner.
|
|
@@ -377,8 +377,7 @@ tests/
|
|
|
377
377
|
├── canonical_profile_url.mjs ← (task 1002804, goal 1000045 criterion C1, ADR 0214): the P08 proof — the canonical profile URL is resolved in ONE place, `handle` reaches `GET /builders/:id/profile` under the EXISTENCE predicate (so a hidden builder keeps their address), absent/provisional/private are BYTE-IDENTICAL so the numeric-id redirect cannot become an existence oracle, and the widened response still leaks no private column. Real core router over a WHERE-derived fake pool on a real socket; the real browser script in a vm above a stub DOM, run twice — as shipped (every surface dark, because `/u/<handle>` still 404s) and with the liveness constant flipped, so the three live behaviours are proved rather than deferred. Asserts its own DB-free lane membership. DB-free `unit` lane.
|
|
378
378
|
├── application_lifecycle.mjs ← (task 1002284, goal 1000045, ADR 0201 D1/D3.2/D4 + ADR 0208): the application lifecycle EXECUTED — apply, list-pending, approve, decline, RESCIND (`invited → dismissed`), idempotency on the state already held, and the two things that must never happen: a builder without `access_request.review` moving the queue, and a VOUCH buying admission. Real router, REAL `requirePermission` resolving against the real government catalog's rank seed, and the REAL `hasInvitedAccessRequest` from `src/bongos/db-kernel.js` pointed at the same in-memory `access_requests` table (a stubbed `./pool`), whose filter is built from each statement's own WHERE clause and which refuses a predicate it cannot read. The no-authority proof seeds a vouched row and asserts the approve path and the admission read are byte-identical to the unvouched twin, and that no statement on either path names `applicant_github_id`/`vouched_at`. DB-free `unit` lane.
|
|
379
379
|
├── application_echo.mjs ← (task 1002284, goal 1000045, ADR 0201 D1.3/D1.4): the hub's STATUSLESS echo of an application. Part A pins the domain functions — the read binds `APPLICATION_ECHO_WINDOW_DAYS` itself (one 30, not three), the write is an upsert on the pair, neither statement mentions a status, and a non-numeric principal is refused before any query (ADR 0171 D2). Part B runs the real `POST /my-projects/join` and `GET /my-projects` over an in-memory `platform_identity_application_echoes` table with the relay's answer scripted: only an accepted application leaves a trace — a standing membership, either door refusal, a members-only project and an unreachable project leave none — and best-effort is asserted in both directions (a failed echo write still reports the filed application; a failed echo read still lands the memberships listing). DB-free `unit` lane.
|
|
380
|
-
├── scouting_owner_access_and_reach.mjs ← (task 1002290, goal 1000045 C5, ADR 0210): the R14 proof — WHO may read the cross-project recruiting directory, and WHO appears in it. A project owner (an instance-asserted, hub-witnessed `membership_kind='owner'`) reads `GET /scouting` with no rank at all; a builder who is neither owner nor `project.curate` holder gets the unchanged 403; an un-witnessed owner claim is refused; a broken ownership read falls closed onto the permission gate rather than admitting or 500ing. And the recruiting opt-out finally bites: a public, unhidden, active account that opted out is absent from `/scouting
|
|
381
|
-
├── scouting_page_gate.mjs ← (task 1002290, goal 1000045 C5, ADR 0210 D3): the OTHER half of R14 — the `/builders/people` SHELL must admit exactly the two audiences `GET /scouting` admits. A project owner holding no permission reaches it; everyone the route refuses gets the byte-identical `?denied=project.curate` redirect; and each of six ways the `alsoAdmits` door can fail (module absent, function absent, throw, reject, non-boolean, prototype-only) fails CLOSED. Also pins that the other four page gates never consult the seam at all, and that exactly ONE gate declares a second door. Its own file because the route proof's ESM imports pull `auth.js` in before a session double could be installed — this one installs it first, the `government_page_gates.mjs` pattern. DB-free `unit` lane.
|
|
380
|
+
├── scouting_owner_access_and_reach.mjs ← (task 1002290, goal 1000045 C5, ADR 0210): the R14 proof — WHO may read the cross-project recruiting directory, and WHO appears in it. A project owner (an instance-asserted, hub-witnessed `membership_kind='owner'`) reads `GET /scouting` with no rank at all; a builder who is neither owner nor `project.curate` holder gets the unchanged 403; an un-witnessed owner claim is refused; a broken ownership read falls closed onto the permission gate rather than admitting or 500ing. And the recruiting opt-out finally bites: a public, unhidden, active account that opted out is absent from `/scouting`, returns when it opts back in, and the opt-out grants and denies no permission — an opted-out owner still reads the directory while being absent from their own copy of it. Since task 1002291 (R15, ADR 0334) it also proves the directory's NAME-ONLY tier: a hidden, reachable builder is listed by login, name and avatar alone, never ranked, and none of their record reaches the body. Fake pool filtered by each statement's own WHERE clause, real route, real `requirePermission` over the real government catalog, real HTTP. Mutation-checked five ways. DB-free `unit` lane.
|
|
382
381
|
├── terms_acceptance.mjs ← (task 1002913, goal 1000045, ADR 0213): the platform T&C acceptance record. Proves the four properties that make it evidence rather than a claim — the /terms page carries the cross-project consent ADR 0141 §4 deferred to it and it is apex-only; acceptance is written ONLY by the hub's own /sso/authorize (driven over the real router with an attacker-controlled query string, so a forged timestamp/version/activate flag reaches nothing); it is written ONCE (the fake pool interprets the upsert's own column assignments, so COALESCE is executed, not string-matched, and an assignment shape it cannot read throws instead of reading as "unchanged"); and consent is two columns in the shared predicate, so an account the hub witnessed before the terms existed publishes nothing. Plus the projection rule — every read whose row a JS twin judges must select both consent columns, or the twin denies everyone silently. DB-free `unit` lane, asserts its own lane membership.
|
|
383
382
|
├── applicant_profile_boundary.mjs ← (task 1002972, goal 1000045 C4, privacy spec D7, ADR 0241): THE D7 BOUNDARY proof for the applicant view a reviewer sees. Whitelists read from the MODULES’ own exports — the private branch against `recruiter-sliver`’s `SLIVER_KEYS`/`SLIVER_KEYS_HIDDEN`, the public branch against the public-page payload’s key list — so a third shape (the “new disclosure class” D7 forbids) reds this file. Pins THE CONSENT SWAP in both directions on the same account: a private builder who never opted into recruiting gets an applicant view and gets `null` from `getRecruiterSliver`, so a refactor routing the port through the recruiter sliver cannot pass quietly. `hide_stats` beats the application in both branches; provisional and terms-never-accepted disclose nothing; planted sentinels asserted absent from the whole body with the avatar-embedded `github_id` pinned as the one honest exception. Plus the federated gate (ADR 0205): no echo → 404 and NO account row read (asserted on the statement log), the witness matched against the AUTHENTICATED client, an out-of-window echo stops working, and `Cache-Control: no-store`. Real router + real HTTP over in-memory tables filtered by each statement’s own WHERE clause. DB-free `unit` lane, asserted in the file itself.
|
|
384
383
|
├── recruiter_sliver_boundary.mjs ← (task 1002968, goal 1000045 C2/C5, privacy spec D3, ADR 0215): THE PRE-ACCEPT BOUNDARY proof for the D3 recruiter sliver. A WHITELIST over the module's own exported key list (a sixth key fails whether or not this file was told about it), planted improbable sentinels asserted absent from the WHOLE response body with two controls against vacuity, the ceiling assertion (the handle a recruiter is handed 404s at `GET /profiles/:handle`, with a 200 control) and the gate (both recruiting routes admit the same two audiences and refuse byte-identically). Real router + real HTTP over an in-memory table filtered by each statement's own WHERE clause. DB-free `unit` lane, asserted in the file itself.
|
|
@@ -2575,5 +2575,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2575
2575
|
landed since 1.19.1042 with no explicit bump. run 36258640780. (task 1002620)
|
|
2576
2576
|
1.19.1044 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2577
2577
|
landed since 1.19.1043 with no explicit bump. run 36260641494. (task 1002620)
|
|
2578
|
+
1.19.1045 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2579
|
+
landed since 1.19.1044 with no explicit bump. run 36263855490. (task 1002620)
|
|
2580
|
+
1.19.1046 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2581
|
+
landed since 1.19.1045 with no explicit bump. run 36265788618. (task 1002620)
|
|
2578
2582
|
---------------------------------------------------------------------------
|
|
2579
2583
|
```
|
|
@@ -212,7 +212,7 @@ caught a real bug during task 1003548, so read its assertion before blaming the
|
|
|
212
212
|
|
|
213
213
|
## CRLF: a multi-line find silently misses, and a whole-file diff is usually line endings
|
|
214
214
|
|
|
215
|
-
- Several hall files (`
|
|
215
|
+
- Several hall files (`roster.css`, `roster.html`, `style.css`, the privacy spec) and every `.claude/skills/*/SKILL.md` are CRLF; `scripts/gds/*.js` and the tests are LF. An exact-string patch must detect the file's EOL before joining lines (`text.includes('\r\n') ? '\r\n' : '\n'`), or the find matches nothing and the edit is a silent no-op (session log 2026-08-24).
|
|
216
216
|
- A `git diff --stat` that touches every line of a file you barely edited is an EOL flip — check with `git diff --stat --ignore-cr-at-eol`, and restore the original EOL before committing.
|
|
217
217
|
- **`copy-inventory.js --check` no longer reds on line endings alone** (task 1003619). It used to: the generator emits LF and the check byte-compared that against a CRLF worktree file, so `/ship-check` showed one permanent red line on every Windows checkout while the committed content was already correct — and a builder who "fixed" it committed a whole-file CRLF diff. `docs/copy-inventory.md` is now pinned `text eol=lf` in `.gitattributes` (its `.json` sibling always was, via `*.json`), and the check compares EOL-blind so an already-CRLF worktree passes without a re-clone. If it reports STALE now, the content really is stale — regenerate, don't reach for `--ignore-cr-at-eol`. Since task 1003981 `*.md` IS pinned repo-wide (`*.md text eol=lf`), so a fresh clone gets LF markdown everywhere and a markdown reader no longer has to be CRLF-aware to be correct. An EXISTING Windows checkout keeps its CRLF copies until git rewrites them — the clean filter still normalises to LF on add, so `git status` stays quiet and nothing is broken; to convert in place, `git rm --cached -r . && git reset --hard`.
|
|
218
218
|
- **A freshness check compares CONTENT, so route it through `sameContent()`** (`scripts/gds/gen-freshness.js`) rather than `===` on file bytes. A pin is the wrong lever on its own: regenerating never clears a byte-wise red, because the write lands LF and git converts it back on the next checkout. `tests/gen_freshness.mjs` enforces this for the four gated generators; `tests/api_client.mjs` was outside that guard and reported the client STALE forever on Windows until task 1003529 (`index.d.ts` and `README.md` are the two generated client files with no `eol=lf` pin).
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
|
|
92
92
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
93
93
|
|
|
94
|
-
<script src="/builders/shell.js?v=2026-09-26-
|
|
94
|
+
<script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
|
|
95
95
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
96
96
|
<script src="/builders/palette.js" defer></script>
|
|
97
97
|
<script src="/builders/atlas.js?v=2026-09-02-index" defer></script>
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
52
52
|
chrome exists before the page script runs. -->
|
|
53
|
-
<script src="/builders/shell.js?v=2026-09-26-
|
|
53
|
+
<script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
|
|
54
54
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
55
55
|
<script src="/builders/palette.js" defer></script>
|
|
56
56
|
<!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
|
|
61
61
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
62
62
|
|
|
63
|
-
<script src="/builders/shell.js?v=2026-09-26-
|
|
63
|
+
<script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
|
|
64
64
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
65
65
|
<script src="/builders/palette.js" defer></script>
|
|
66
66
|
<script src="/builders/dom-utils.js?v=2026-06-16-board"></script>
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// modules/hall-ui/public/claim-action.js — the hall's ONE claim/release action
|
|
2
|
+
// (task 1003458, from the board's own copy).
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS. /work owned the only working claim path in the hall, and the
|
|
5
|
+
// moment a second surface wanted a claim button it got one by copy-paste:
|
|
6
|
+
// goals-page.js's own comments still name the line ranges it lifted ("ported
|
|
7
|
+
// from work.js renderClaimable, work.js:345-394"). The copies then drifted, the
|
|
8
|
+
// way task-lib.js's header says they always do — by the time this was written
|
|
9
|
+
// the SAME refusal read "Someone else claimed that task first. The board will
|
|
10
|
+
// refresh." on /work and "… Reloading." on /goals, and the goals copy had
|
|
11
|
+
// quietly dropped the "Wait for it to ship." advice from TOUCHES_CONFLICT and
|
|
12
|
+
// "Pick another for now." from DEPS_NOT_SHIPPED. Neither copy had a single test.
|
|
13
|
+
//
|
|
14
|
+
// AND THE DRIFT HID A BIGGER HOLE. Both copies handled 8 of the server's refusal
|
|
15
|
+
// codes. modules/lifecycle/CLAUDE.md documents the claim gate's refusal chain in
|
|
16
|
+
// order, and REBASE_REQUIRED (#2), TASK_NOT_FOUND (#3), OPEN_BLOCKER (#5),
|
|
17
|
+
// GOAL_MEMBERSHIP_REQUIRED (#8) and MODULE_CONFLICT (#10) were in neither — so a
|
|
18
|
+
// builder who hit any of them, including the two most ordinary ones, got "The
|
|
19
|
+
// board could not complete that (409). Try again." tests/claim_action.mjs now
|
|
20
|
+
// reads db-claims.js and FAILS if the server grows a refusal this file has no
|
|
21
|
+
// sentence for, which is what makes "a new surface needs no new refusal
|
|
22
|
+
// handling" true rather than aspirational.
|
|
23
|
+
//
|
|
24
|
+
// Contract, mirrored from task-lib.js so load order only needs "before the page
|
|
25
|
+
// script": dependency-free (its own envelope readers, no window.OTB use), no
|
|
26
|
+
// eval-time DOM, everything hangs off one global, Node-require-safe so it can be
|
|
27
|
+
// unit-tested. The ACTION half takes its I/O injected — each board keeps its own
|
|
28
|
+
// postJSON (auth posture), toast and refresh — so this file never touches the
|
|
29
|
+
// network or the DOM itself.
|
|
30
|
+
//
|
|
31
|
+
// NOT here: task.js's friendlyActionError. That maps the LIFECYCLE refusals of
|
|
32
|
+
// POST /tasks/:id/ship and /confirm (task_not_confirmed, task_not_completed,
|
|
33
|
+
// verify_attestation_required) — a different route, a disjoint code set. Folding
|
|
34
|
+
// the two would make one switch that is wrong about half its cases.
|
|
35
|
+
(function () {
|
|
36
|
+
'use strict';
|
|
37
|
+
|
|
38
|
+
// ADR 0116's envelope: { error: { code, message, details } }. Both readers fall
|
|
39
|
+
// back to the flat legacy shape, because a refusal that renders "[object
|
|
40
|
+
// Object]" is the defect task 1002462 was about and it must not come back.
|
|
41
|
+
function errCode(data) {
|
|
42
|
+
const e = data && data.error;
|
|
43
|
+
return (e && typeof e === 'object') ? e.code : e;
|
|
44
|
+
}
|
|
45
|
+
function errDetails(data) {
|
|
46
|
+
const e = data && data.error;
|
|
47
|
+
return (e && typeof e === 'object' && e.details) ? e.details : (data || {});
|
|
48
|
+
}
|
|
49
|
+
function errMessage(data) {
|
|
50
|
+
const e = data && data.error;
|
|
51
|
+
if (e && typeof e === 'object' && e.message) return e.message;
|
|
52
|
+
return (data && data.message) || '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Every code POST /claims, /claims/batch and /claims/:id/resolve can refuse
|
|
56
|
+
// with, in the order modules/lifecycle/CLAUDE.md documents the gate checking
|
|
57
|
+
// them. Keep this list in that order: it is the map a reader compares against.
|
|
58
|
+
function friendlyClaimError(status, data) {
|
|
59
|
+
const det = errDetails(data);
|
|
60
|
+
switch (errCode(data)) {
|
|
61
|
+
// 1. the builder, before anything about the task
|
|
62
|
+
case 'BUILDER_INACTIVE':
|
|
63
|
+
return 'Your account is inactive. Contact an Archon.';
|
|
64
|
+
// 2. ADR 0120 part 3 — one stale claim blocks every NEW claim, so the
|
|
65
|
+
// sentence has to name the way out or the builder just retries forever.
|
|
66
|
+
// The server sends its own hint; prefer it, since it names the task.
|
|
67
|
+
case 'REBASE_REQUIRED':
|
|
68
|
+
return errMessage(data)
|
|
69
|
+
|| 'A task you hold needs rebasing before you can claim anything new. Run /strand-fix on it.';
|
|
70
|
+
// 3. the board is showing something the server no longer has
|
|
71
|
+
case 'TASK_NOT_FOUND':
|
|
72
|
+
return 'That task no longer exists. The board will refresh.';
|
|
73
|
+
case 'TASK_NOT_READY':
|
|
74
|
+
return 'That task is no longer ready to be claimed.';
|
|
75
|
+
// 5. task 1002650 — the feed filters these out, but a direct claim can
|
|
76
|
+
// still land on one, and "could not complete that" tells the builder
|
|
77
|
+
// nothing about a blocker they could go and read.
|
|
78
|
+
case 'OPEN_BLOCKER':
|
|
79
|
+
return 'That task is held by an open blocker. See /blockers for what it is waiting on.';
|
|
80
|
+
// 6 + 7 are two DIFFERENT axes (the permission queue vs the task's own
|
|
81
|
+
// requires_rank floor) and lifecycle/CLAUDE.md says never to collapse
|
|
82
|
+
// them — but from the builder's chair the remedy is identical, so they
|
|
83
|
+
// share one sentence rather than one case.
|
|
84
|
+
case 'rank_too_low_for_task':
|
|
85
|
+
case 'XENOS_RESTRICTED':
|
|
86
|
+
case 'INSUFFICIENT_RANK':
|
|
87
|
+
return 'Your rank is not yet high enough for that task.';
|
|
88
|
+
// 8. task 1003137. The refusal deliberately carries no title or scope for
|
|
89
|
+
// a private goal, so this must not invent one either.
|
|
90
|
+
case 'GOAL_MEMBERSHIP_REQUIRED':
|
|
91
|
+
return errMessage(data) || 'That task belongs to a goal you are not a member of.';
|
|
92
|
+
case 'DEPS_NOT_SHIPPED':
|
|
93
|
+
return 'That task waits on unshipped dependencies. Pick another for now.';
|
|
94
|
+
// 10. the module-level sibling of TOUCHES_CONFLICT, and a REFUSAL where
|
|
95
|
+
// TOUCHES_CONFLICT is only advisory.
|
|
96
|
+
case 'MODULE_CONFLICT':
|
|
97
|
+
return det.module_key
|
|
98
|
+
? `That task shares module "${det.module_key}" with an active claim. Wait for it to ship.`
|
|
99
|
+
: 'That task shares a module with an active claim. Wait for it to ship.';
|
|
100
|
+
// 11. advisory since task 880 — it only refuses on the enforcing path.
|
|
101
|
+
case 'TOUCHES_CONFLICT': {
|
|
102
|
+
const titles = Array.isArray(det.conflicts) ? det.conflicts.map((c) => c.title).filter(Boolean) : [];
|
|
103
|
+
return titles.length
|
|
104
|
+
? `That task overlaps an active claim: ${titles.join('; ')}. Wait for it to ship.`
|
|
105
|
+
: 'That task overlaps another active claim. Wait for it to ship.';
|
|
106
|
+
}
|
|
107
|
+
// 12. the 23505 backstop: anything unrecognised fails CLOSED onto this.
|
|
108
|
+
case 'ALREADY_CLAIMED':
|
|
109
|
+
return 'Someone else claimed that task first. The board will refresh.';
|
|
110
|
+
// route-layer gates, chained on POST /claims
|
|
111
|
+
case 'session_already_has_active_claim':
|
|
112
|
+
case 'session_already_has_bound_claim':
|
|
113
|
+
return 'This session already holds a claim. Ship or release it first.';
|
|
114
|
+
case 'newcomer_must_ship_first':
|
|
115
|
+
return det.what_to_do || 'Ship your current claim before opening another.';
|
|
116
|
+
// gateOneActiveClaimPerSession fails CLOSED: if it cannot read what this
|
|
117
|
+
// session holds it refuses rather than risk a second claim. Say that it is
|
|
118
|
+
// transient, or the builder reads it as a permission problem.
|
|
119
|
+
case 'session_claim_check_failed':
|
|
120
|
+
return 'Could not check what this session already holds. Try again in a moment.';
|
|
121
|
+
// release-side
|
|
122
|
+
case 'NOT_YOUR_CLAIM':
|
|
123
|
+
return 'That claim is not yours to release.';
|
|
124
|
+
case 'ALREADY_RESOLVED':
|
|
125
|
+
return 'That claim was already resolved. The board will refresh.';
|
|
126
|
+
default:
|
|
127
|
+
return `The board could not complete that (${status}). Try again.`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The action itself: the CALL, the refusal, and the refresh — the half the
|
|
132
|
+
// boards used to hand-roll four times each. `postJSON` must reject with
|
|
133
|
+
// { status, data } on a non-2xx (both boards' helpers already do) and may
|
|
134
|
+
// throw the sentinel 'redirecting to auth', which every caller swallows
|
|
135
|
+
// rather than toasting.
|
|
136
|
+
//
|
|
137
|
+
// What stays with the CALLER: its own button text, its re-entrancy guard, its
|
|
138
|
+
// confirm() wording, and its selection state. Those are page furniture and
|
|
139
|
+
// differ per surface; the network call and what a refusal MEANS do not. Each
|
|
140
|
+
// method answers { ok } (plus { redirected } on the auth sentinel) so a caller
|
|
141
|
+
// can re-enable its own button without re-reading the error.
|
|
142
|
+
//
|
|
143
|
+
// `refresh` is awaited after a success AND after a 409, because a 409 always
|
|
144
|
+
// means the board is showing something stale.
|
|
145
|
+
//
|
|
146
|
+
// `describeBatchReason` maps ONE item of a batch refusal (task-lib.js's
|
|
147
|
+
// friendlyBatchReason). Injected rather than imported to keep this file
|
|
148
|
+
// dependency-free; a caller that omits it gets the raw reason, never a crash.
|
|
149
|
+
function makeClaimAction({ postJSON, toast, refresh, API = '/api/bongos', describeBatchReason } = {}) {
|
|
150
|
+
const reason = describeBatchReason || ((f) => (f && f.reason) || 'not claimable');
|
|
151
|
+
const reload = async () => { if (refresh) await refresh(); };
|
|
152
|
+
const quiet = async () => { try { await reload(); } catch (_) { /* the toast already told them */ } };
|
|
153
|
+
|
|
154
|
+
// The batch endpoints are ALL-OR-NOTHING: a 409 batch_failed means nothing
|
|
155
|
+
// was claimed, so the toast has to name which tasks stopped it or the
|
|
156
|
+
// builder cannot tell what to deselect. task 1002462: this used to compare
|
|
157
|
+
// `err.data.error` (an object) to a string, so every batch refusal fell
|
|
158
|
+
// through to the generic line and the per-task reasons were dropped.
|
|
159
|
+
function batchRefusal(err) {
|
|
160
|
+
if (!(err && err.status === 409 && errCode(err.data) === 'batch_failed')) return null;
|
|
161
|
+
const failures = errDetails(err.data).failures;
|
|
162
|
+
const lines = Array.isArray(failures) ? failures.map((f) => `#${f.task_id} (${reason(f)})`) : [];
|
|
163
|
+
return lines.length
|
|
164
|
+
? `The batch could not be claimed — ${lines.join('; ')}.`
|
|
165
|
+
: 'The batch could not be claimed. It was left untouched (all-or-nothing).';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function run(fn, okMsg) {
|
|
169
|
+
try {
|
|
170
|
+
const out = await fn();
|
|
171
|
+
if (okMsg) toast(typeof okMsg === 'function' ? okMsg(out) : okMsg);
|
|
172
|
+
await reload();
|
|
173
|
+
return { ok: true, data: out };
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (err && err.message === 'redirecting to auth') return { ok: false, redirected: true };
|
|
176
|
+
const batch = batchRefusal(err);
|
|
177
|
+
toast(batch || friendlyClaimError(err && err.status, err && err.data));
|
|
178
|
+
if (batch || (err && err.status === 409)) await quiet();
|
|
179
|
+
return { ok: false, error: err };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// bind_session:false — a web claim has no working session, so leave it
|
|
184
|
+
// unbound (claims.js resolveCreatorSessionId) and the builder can ship it
|
|
185
|
+
// later from any of their own terminals.
|
|
186
|
+
const claimOne = (taskId) => postJSON(`${API}/claims`, { task_id: Number(taskId), bind_session: false });
|
|
187
|
+
// No DELETE /claims/:id exists — release is resolve(outcome=abandoned), the
|
|
188
|
+
// same path /builder-release uses, scoped server-side to the caller's own
|
|
189
|
+
// claim (NOT_YOUR_CLAIM).
|
|
190
|
+
const releaseOne = (claimId) => postJSON(`${API}/claims/${encodeURIComponent(claimId)}/resolve`, { outcome: 'abandoned' });
|
|
191
|
+
|
|
192
|
+
// THE DRY RUN. /claims/batch/validate asks the server what a bulk claim
|
|
193
|
+
// WOULD do, so a refusal can be shown before the click rather than after it.
|
|
194
|
+
// Best-effort by design: on any failure it answers "not parallel-safe, know
|
|
195
|
+
// nothing", because the caller uses this to decide whether to offer a
|
|
196
|
+
// parallel prompt, and copying a prompt for tasks nobody verified is worse
|
|
197
|
+
// than offering nothing. It therefore never throws and never toasts — an
|
|
198
|
+
// unavailable pre-check is not a refusal to report.
|
|
199
|
+
async function validateMany(ids) {
|
|
200
|
+
try {
|
|
201
|
+
const result = await postJSON(`${API}/claims/batch/validate`, { task_ids: ids });
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
parallelSafe: !!result.parallel_safe,
|
|
205
|
+
byId: new Map((result.results || []).map((r) => [String(r.task_id), r])),
|
|
206
|
+
};
|
|
207
|
+
} catch (_) {
|
|
208
|
+
return { ok: false, parallelSafe: false, byId: new Map() };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
validateMany,
|
|
214
|
+
claim: (taskId) => run(() => claimOne(taskId), `You claimed #${taskId}. Open a terminal to start work.`),
|
|
215
|
+
release: (claimId, taskId) => run(() => releaseOne(claimId), `Released #${taskId}. It returns to the queue.`),
|
|
216
|
+
|
|
217
|
+
// One id takes the SINGLE endpoint, not a batch of one: the batch route is
|
|
218
|
+
// all-or-nothing and reports per-item reasons, which is the wrong shape
|
|
219
|
+
// (and the wrong sentence) for a selection of one.
|
|
220
|
+
claimMany: (ids) => run(
|
|
221
|
+
() => (ids.length === 1 ? claimOne(ids[0]) : postJSON(`${API}/claims/batch`, { task_ids: ids, bind_session: false })),
|
|
222
|
+
ids.length === 1
|
|
223
|
+
? `You claimed #${ids[0]}. Open a terminal to start work.`
|
|
224
|
+
: `Claimed ${ids.length} tasks. Open a terminal to start work.`
|
|
225
|
+
),
|
|
226
|
+
|
|
227
|
+
// entries: [claimId, taskId] pairs. The batch release is NOT all-or-nothing
|
|
228
|
+
// — it answers released[] + failures[] — so its sentence counts rather
|
|
229
|
+
// than lists.
|
|
230
|
+
releaseMany: (entries) => run(
|
|
231
|
+
() => (entries.length === 1
|
|
232
|
+
? releaseOne(entries[0][0])
|
|
233
|
+
: postJSON(`${API}/claims/batch/release`, { claim_ids: entries.map(([cid]) => Number(cid)) })),
|
|
234
|
+
entries.length === 1
|
|
235
|
+
? `Released #${entries[0][1]}. It returns to the queue.`
|
|
236
|
+
: (result) => {
|
|
237
|
+
const released = Array.isArray(result && result.released) ? result.released.length : 0;
|
|
238
|
+
const failed = Array.isArray(result && result.failures) ? result.failures.length : 0;
|
|
239
|
+
return failed
|
|
240
|
+
? `Released ${released} of ${entries.length} — ${failed} could not be released.`
|
|
241
|
+
: `Released ${released} tasks. They return to the queue.`;
|
|
242
|
+
}
|
|
243
|
+
),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const api = { errCode, errDetails, errMessage, friendlyClaimError, makeClaimAction };
|
|
248
|
+
if (typeof window !== 'undefined') window.OTBClaimAction = api;
|
|
249
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
250
|
+
})();
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
|
|
109
109
|
/* ---- the face -----------------------------------------------------------------
|
|
110
110
|
A row leads with WHO: a 36px disc, the picture when the server sent one, the
|
|
111
|
-
initial on the glass when it did not (
|
|
111
|
+
initial on the glass when it did not (roster.css's .dir-avatar, at the row's
|
|
112
112
|
size). A craft ask wears the craft's letter in the label face, so it never
|
|
113
113
|
passes for a person. Decorative — the name beside it is the door. */
|
|
114
114
|
.cb-face {
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
|
|
149
149
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
150
150
|
|
|
151
|
-
<script src="/builders/shell.js?v=2026-09-26-
|
|
151
|
+
<script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
|
|
152
152
|
<script src="/builders/palette.js" defer></script>
|
|
153
153
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
154
154
|
<script src="/builders/collab-lib.js?v=2026-09-24-archive"></script>
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
|
|
178
178
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
179
179
|
|
|
180
|
-
<script src="/builders/shell.js?v=2026-09-26-
|
|
180
|
+
<script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
|
|
181
181
|
<script src="/builders/palette.js" defer></script>
|
|
182
182
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
183
183
|
<script src="/builders/hall-kit.js"></script>
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
|
|
70
70
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
71
71
|
|
|
72
|
-
<script src="/builders/shell.js?v=2026-09-26-
|
|
72
|
+
<script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
|
|
73
73
|
<script src="/builders/palette.js" defer></script>
|
|
74
74
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
75
75
|
<script src="/builders/hall-kit.js"></script>
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
|
|
98
98
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
99
99
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
100
|
-
<script src="/builders/shell.js?v=2026-09-26-
|
|
100
|
+
<script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
|
|
101
101
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
102
102
|
<script src="/builders/palette.js" defer></script>
|
|
103
103
|
<script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" defer></script>
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
|
|
99
99
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
100
100
|
|
|
101
|
-
<script src="/builders/shell.js?v=2026-09-26-
|
|
101
|
+
<script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
|
|
102
102
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
103
103
|
<script src="/builders/palette.js" defer></script>
|
|
104
104
|
<script src="/builders/md-reader.js?v=2026-09-02-mdreader-2"></script>
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
|
|
55
55
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
56
56
|
|
|
57
|
-
<script src="/builders/shell.js?v=2026-09-26-
|
|
57
|
+
<script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
|
|
58
58
|
<script src="/builders/palette.js" defer></script>
|
|
59
59
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
60
60
|
<script src="/builders/hall-kit.js"></script>
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
<div id="gate-modal-root"></div>
|
|
77
77
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
78
78
|
|
|
79
|
-
<script src="/builders/shell.js?v=2026-09-26-
|
|
79
|
+
<script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
|
|
80
80
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
81
81
|
<script src="/builders/palette.js" defer></script>
|
|
82
82
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
(function () {
|
|
24
24
|
'use strict';
|
|
25
25
|
const { escapeHtml, $, fmtNum, fmtDate, toast, rankLabel } = window.OTB;
|
|
26
|
+
// task 1003458 — the hall's one claim/release action.
|
|
27
|
+
const { makeClaimAction } = window.OTBClaimAction;
|
|
26
28
|
// Shared task-domain helpers (task 1002463). These lived in two copies here and
|
|
27
29
|
// in the other board and had already drifted — goals-page.js had silently lost
|
|
28
30
|
// friendlyBatchReason's MODULE_CONFLICT case. Destructured so every call site
|
|
@@ -110,6 +112,18 @@
|
|
|
110
112
|
if (!res.ok) { const e = new Error(`${path} → ${res.status}`); e.status = res.status; e.data = data; throw e; }
|
|
111
113
|
return data;
|
|
112
114
|
}
|
|
115
|
+
|
|
116
|
+
// The hall's ONE claim/release action (task 1003458). This page's three
|
|
117
|
+
// claim/release functions used to be hand-copied from work.js — their own
|
|
118
|
+
// comments still say so — and the copies drifted. They now keep only what is
|
|
119
|
+
// genuinely this page (button text, the pending guard, the confirm wording,
|
|
120
|
+
// the goal selection) and hand the call, the refusal and the refresh here.
|
|
121
|
+
// Refreshing the open goal is this surface's equivalent of the board's
|
|
122
|
+
// refreshLive; with no goal open there is no task list to reload.
|
|
123
|
+
const claimAction = makeClaimAction({
|
|
124
|
+
postJSON, toast, API, describeBatchReason: friendlyBatchReason,
|
|
125
|
+
refresh: async () => { if (state.current != null) await loadGoalTasks(state.current); },
|
|
126
|
+
});
|
|
113
127
|
// task 1953 — the visibility toggle is the only PATCH this page makes.
|
|
114
128
|
async function patchJSON(path, body) {
|
|
115
129
|
const res = await api.request('PATCH', path, { body: body || {} });
|
|
@@ -419,35 +433,17 @@
|
|
|
419
433
|
const btn = $('#goal-bulk-claim-btn');
|
|
420
434
|
if (btn) { btn.disabled = true; btn.textContent = 'Claiming…'; }
|
|
421
435
|
try {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
toast(`You claimed #${ids[0]}. Open a terminal to start work.`);
|
|
425
|
-
} else {
|
|
426
|
-
await postJSON(`${API}/claims/batch`, { task_ids: ids, bind_session: false });
|
|
427
|
-
toast(`Claimed ${ids.length} tasks. Open a terminal to start work.`);
|
|
428
|
-
}
|
|
429
|
-
goalSel.claim.clear();
|
|
430
|
-
} catch (err) {
|
|
431
|
-
if (err && err.message === 'redirecting to auth') return;
|
|
432
|
-
// claims/batch is all-or-nothing: a 409 batch_failed means NOTHING was
|
|
433
|
-
// claimed. Read failures from the ADR 0116 envelope (error.details.failures)
|
|
434
|
-
// or the flat legacy shape, via the same normalizers friendlyClaimError uses.
|
|
435
|
-
const det = errDetails(err.data);
|
|
436
|
-
if (err.status === 409 && errCode(err.data) === 'batch_failed') {
|
|
437
|
-
// One read covers both envelopes: errDetails returns error.details on the ADR
|
|
438
|
-
// 0116 shape and the raw body on the flat legacy one, so a second
|
|
439
|
-
// `err.data.failures` fallback here is unreachable (task 1003657).
|
|
440
|
-
const failures = Array.isArray(det.failures) ? det.failures : [];
|
|
441
|
-
const lines = failures.map((f) => `#${f.task_id} (${friendlyBatchReason(f)})`);
|
|
442
|
-
toast(lines.length
|
|
443
|
-
? `The batch could not be claimed — ${lines.join('; ')}.`
|
|
444
|
-
: 'The batch could not be claimed. It was left untouched (all-or-nothing).');
|
|
445
|
-
} else {
|
|
446
|
-
toast(friendlyClaimError(err.status, err.data));
|
|
447
|
-
}
|
|
436
|
+
const out = await claimAction.claimMany(ids);
|
|
437
|
+
if (out.ok) goalSel.claim.clear();
|
|
448
438
|
} finally {
|
|
449
|
-
|
|
450
|
-
|
|
439
|
+
// The shared action already reloaded the open goal's task list, on the
|
|
440
|
+
// success path AND the 409 one — re-fetching here would make every bulk
|
|
441
|
+
// claim on this page fetch and re-render twice. work.js's doBulkClaim is
|
|
442
|
+
// the right shape: redraw the toolbar, nothing else. It has to happen on
|
|
443
|
+
// every path, because the button was disabled above and the paths where
|
|
444
|
+
// no reload runs (the auth redirect, a refresh that failed quietly) have
|
|
445
|
+
// nothing else to re-enable it.
|
|
446
|
+
renderGoalBulkToolbar();
|
|
451
447
|
}
|
|
452
448
|
}
|
|
453
449
|
|
|
@@ -517,15 +513,8 @@
|
|
|
517
513
|
pendingClaims.add(key);
|
|
518
514
|
if (btn) { btn.disabled = true; btn.textContent = 'Claiming…'; }
|
|
519
515
|
try {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
await postJSON(`${API}/claims`, { task_id: Number(taskId), bind_session: false });
|
|
523
|
-
toast(`You claimed #${taskId}. Open a terminal to start work.`);
|
|
524
|
-
if (state.current != null) await loadGoalTasks(state.current);
|
|
525
|
-
} catch (err) {
|
|
526
|
-
if (err && err.message === 'redirecting to auth') return;
|
|
527
|
-
toast(friendlyClaimError(err.status, err.data));
|
|
528
|
-
if (btn) { btn.disabled = false; btn.textContent = 'Claim'; }
|
|
516
|
+
const out = await claimAction.claim(taskId);
|
|
517
|
+
if (!out.ok && !out.redirected && btn) { btn.disabled = false; btn.textContent = 'Claim'; }
|
|
529
518
|
} finally {
|
|
530
519
|
pendingClaims.delete(key);
|
|
531
520
|
}
|
|
@@ -542,13 +531,8 @@
|
|
|
542
531
|
pendingClaims.add(key);
|
|
543
532
|
if (btn) { btn.disabled = true; btn.textContent = 'Releasing…'; }
|
|
544
533
|
try {
|
|
545
|
-
await
|
|
546
|
-
|
|
547
|
-
if (state.current != null) await loadGoalTasks(state.current);
|
|
548
|
-
} catch (err) {
|
|
549
|
-
if (err && err.message === 'redirecting to auth') return;
|
|
550
|
-
toast(friendlyClaimError(err.status, err.data));
|
|
551
|
-
if (btn) { btn.disabled = false; btn.textContent = 'Release'; }
|
|
534
|
+
const out = await claimAction.release(claimId, taskId);
|
|
535
|
+
if (!out.ok && !out.redirected && btn) { btn.disabled = false; btn.textContent = 'Release'; }
|
|
552
536
|
} finally {
|
|
553
537
|
pendingClaims.delete(key);
|
|
554
538
|
}
|
|
@@ -557,34 +557,13 @@
|
|
|
557
557
|
const shown = paginate(list, pagers.todo);
|
|
558
558
|
listEl.innerHTML = shown.map(goalTaskCardHtml).join('');
|
|
559
559
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
function friendlyClaimError(status, data) {
|
|
569
|
-
const det = errDetails(data);
|
|
570
|
-
switch (errCode(data)) {
|
|
571
|
-
case 'ALREADY_CLAIMED': return 'Someone else claimed that task first. Reloading.';
|
|
572
|
-
case 'TASK_NOT_READY': return 'That task is no longer ready to be claimed.';
|
|
573
|
-
case 'TOUCHES_CONFLICT': {
|
|
574
|
-
const titles = Array.isArray(det.conflicts) ? det.conflicts.map((c) => c.title).filter(Boolean) : [];
|
|
575
|
-
return titles.length ? `That task overlaps an active claim: ${titles.join('; ')}.` : 'That task overlaps another active claim.';
|
|
576
|
-
}
|
|
577
|
-
case 'DEPS_NOT_SHIPPED': return 'That task waits on unshipped dependencies.';
|
|
578
|
-
case 'rank_too_low_for_task':
|
|
579
|
-
case 'XENOS_RESTRICTED':
|
|
580
|
-
case 'INSUFFICIENT_RANK': return 'Your rank is not yet high enough for that task.';
|
|
581
|
-
case 'newcomer_must_ship_first': return det.what_to_do || 'Ship your current claim before opening another.';
|
|
582
|
-
case 'NOT_YOUR_CLAIM': return 'That claim is not yours to release.';
|
|
583
|
-
case 'ALREADY_RESOLVED': return 'That claim was already resolved. Reloading.';
|
|
584
|
-
case 'BUILDER_INACTIVE': return 'Your account is inactive. Contact an Archon.';
|
|
585
|
-
default: return `The board could not do that (${status}). Try again.`;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
560
|
+
// The hall's ONE claim/release refusal copy + envelope readers (task 1003458).
|
|
561
|
+
// This file used to carry its own errCode/errDetails and a friendlyClaimError
|
|
562
|
+
// ported from work.js — and the two had drifted: the same refusal said
|
|
563
|
+
// "Reloading." here and "The board will refresh." there, and this copy had
|
|
564
|
+
// lost the "Wait for it to ship." / "Pick another for now." advice entirely.
|
|
565
|
+
// One definition now, in claim-action.js, complete against the server's chain.
|
|
566
|
+
const { errCode, errDetails, friendlyClaimError } = window.OTBClaimAction;
|
|
588
567
|
function conflictHtml(c) {
|
|
589
568
|
const sev = c.severity
|
|
590
569
|
? `<span class="conflict__sev conflict__sev--${escapeHtml(c.severity)}">${escapeHtml(c.severity)}</span>`
|