@bongos/core 1.17.0 → 1.17.1

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.
Files changed (90) hide show
  1. package/.bongos-core.json +154 -83
  2. package/CLAUDE.md +11 -238
  3. package/bin/bongos.js +1 -0
  4. package/clients/bongos-client/README.md +4 -4
  5. package/clients/bongos-client/bongos-client.global.js +14 -1
  6. package/clients/bongos-client/index.cjs +14 -1
  7. package/clients/bongos-client/index.d.ts +19 -2
  8. package/clients/bongos-client/index.mjs +11 -1
  9. package/clients/bongos-client/package.json +1 -1
  10. package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
  11. package/docs/adr/0134-private-first-npm-distribution.md +20 -6
  12. package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
  13. package/docs/adr/README.md +5 -4
  14. package/docs/api/openapi.json +315 -6
  15. package/docs/api-reference.md +10 -5
  16. package/docs/architecture.md +1 -1
  17. package/docs/file-map.md +2 -2
  18. package/docs/modules-contract.md +2 -0
  19. package/docs/recipes/instance-deploy.md +4 -4
  20. package/docs/recipes/instance-first-run.md +51 -0
  21. package/docs/recipes/packaging-the-core.md +5 -5
  22. package/docs/recipes/private-npm-distribution.md +22 -12
  23. package/docs/recipes/self-host.md +1 -1
  24. package/docs/recipes/upgrading-the-core.md +8 -8
  25. package/migrations/core_191_module_submissions.sql +45 -0
  26. package/modules/autonomy/CLAUDE.md +11 -45
  27. package/modules/builder-settings/CLAUDE.md +11 -54
  28. package/modules/dev-box/CLAUDE.md +11 -34
  29. package/modules/discord/CLAUDE.md +11 -46
  30. package/modules/economy/CLAUDE.md +11 -44
  31. package/modules/grading/CLAUDE.md +11 -38
  32. package/modules/hall-ui/CLAUDE.md +11 -57
  33. package/modules/hall-ui/public/goals-page.js +227 -22
  34. package/modules/hall-ui/public/goals.html +16 -0
  35. package/modules/hall-ui/public/new-project-wizard.js +5 -4
  36. package/modules/ideas/CLAUDE.md +11 -33
  37. package/modules/lifecycle/CLAUDE.md +11 -59
  38. package/modules/lifecycle/routes/goals.js +80 -0
  39. package/modules/memory/CLAUDE.md +11 -36
  40. package/modules/onboarding/CLAUDE.md +11 -51
  41. package/modules/security/CLAUDE.md +11 -40
  42. package/modules/sessions/CLAUDE.md +11 -35
  43. package/modules/status-ui/CLAUDE.md +11 -43
  44. package/package-lock.json +2 -2
  45. package/package.json +1 -1
  46. package/public-landing/create.html +58 -12
  47. package/public-landing/index.html +40 -21
  48. package/scripts/gds/CLAUDE.md +11 -140
  49. package/scripts/gds/claude-materialize.js +151 -18
  50. package/scripts/gds/claude-md-template.js +84 -0
  51. package/scripts/gds/cli-lib.js +112 -2
  52. package/scripts/gds/gen-api-client.js +10 -10
  53. package/scripts/gds/init.js +53 -13
  54. package/scripts/gds/migration-namespace.js +1 -1
  55. package/scripts/gds/module.js +118 -4
  56. package/scripts/gds/npmrc.js +93 -0
  57. package/scripts/gds/onboard.js +13 -24
  58. package/scripts/gds/package-core.js +56 -14
  59. package/scripts/gds/provision.js +102 -17
  60. package/scripts/gds/seed-adopt-tasks.js +1 -1
  61. package/scripts/gds/status.js +6 -1
  62. package/scripts/gds/task.js +166 -0
  63. package/scripts/gds/upgrade.js +58 -18
  64. package/src/bongos/CLAUDE.md +11 -60
  65. package/src/bongos/auth.js +11 -0
  66. package/src/bongos/module-submissions.js +56 -0
  67. package/src/bongos/route-rank-check.js +6 -0
  68. package/src/bongos/routes/CLAUDE.md +11 -39
  69. package/src/bongos/routes/auth.js +6 -1
  70. package/src/bongos/routes/modules.js +123 -21
  71. package/src/instance-config.js +1 -1
  72. package/src/module-api.js +13 -1
  73. package/tests/claude_materialize.mjs +105 -0
  74. package/tests/device_flow_start.mjs +73 -0
  75. package/tests/first_run_engine_check.mjs +79 -0
  76. package/tests/goal_membership_requests.mjs +33 -3
  77. package/tests/hook_user_agents.mjs +62 -0
  78. package/tests/init.mjs +59 -6
  79. package/tests/instance_mismatch_guard.mjs +101 -0
  80. package/tests/module_cli.mjs +130 -2
  81. package/tests/module_submissions.mjs +135 -0
  82. package/tests/npmrc.mjs +105 -0
  83. package/tests/onboard.mjs +49 -0
  84. package/tests/package_core.mjs +68 -22
  85. package/tests/platform_boot.mjs +5 -2
  86. package/tests/provision.mjs +107 -8
  87. package/tests/provisioning_github_app_routes.mjs +29 -2
  88. package/tests/served_paths_shipped.mjs +2 -2
  89. package/tests/task_cli.mjs +99 -0
  90. package/tests/upgrade.mjs +89 -25
@@ -1,42 +1,15 @@
1
- # `grading` module — the grade-a-diff adversarial panel + the `grade` kernel port
1
+ # @bongos/core — installed platform dependency (not your project's CLAUDE.md)
2
2
 
3
- > The **second CORE-domain** carved into a module (BV1.R73, [ADR 0091](../../docs/adr/<redacted>.md) §1+§4) — after memory (R72). It is the carve that proves ADR 0091 §4's load-bearing pattern: **keep the lifecycle state machine whole, extract the capability it invokes behind a kernel seam.** The lifecycle no longer imports the grader directly; it resolves a `grade` PORT this module registers.
3
+ This file is a placeholder shipped inside the **@bongos/core** package. The core's own
4
+ internal agent notes are intentionally omitted from the published artifact so they cannot
5
+ load into your instance's context.
4
6
 
5
- Owns the **quality gate**: the v4 adversarial worker panel (Narc / Quality / Hacker / Efficiency, unanimous-PASS), the deterministic pre-passes (route-rank + permission-path, run as panel inputs), and the scoring **rubric**. `default: true` in `module.json` — every instance grades its own ships, so it mounts unconditionally unless explicitly disabled.
7
+ **It does not govern this repository.** Your own root `CLAUDE.md`, your `.claude/`
8
+ directory, and the live Bongos database are the source of truth for how work happens here
9
+ and who may do what.
6
10
 
7
- ## The boundary (the one rule)
11
+ You are seeing this only because an agent opened a file under `node_modules/@bongos/core/`.
12
+ Nothing in here needs to be read to use the platform — run `bongos help` for commands.
8
13
 
9
- This module reaches core **only** through the published doorway `../../src/module-api.js` (`coreVersion ^1.6.0` — it needs the R73 doorway additions: `routeRankCheck`, `permissionPathCheck`, `llmCache`). It must **never** `require()` a core internal directly, and never another module. Core, in turn, must never `require()` a file in here — it reaches grading only through the `grade` port (below). Both directions are enforced by the BV1.R44/R45 fitness checks; R70's `kernel-imports-no-domain` check keeps the kernel from reaching back in.
10
-
11
- ## The `grade` kernel PORT (ADR 0091 §4 — the headline of this carve)
12
-
13
- - **Provides — port `grade`** → the grader module surface. Registered in `routes/grade.js`'s factory (the loader runs it at boot, and again whenever a test rebuilds the GDS router — `hasProvider`-guarded so re-instantiation never trips the single-provider duplicate guard).
14
- - **Core consumes it WITHOUT importing this module:**
15
- - `routes/tasks.js` (the LIFECYCLE ship path, `POST /tasks/:id/grade`) resolves it via `seams.resolveOptional('grade')` and calls `grade.validateAndScore(...)` then `grade.serverSideGuards(...)`. A grading-disabled instance gets a clean `503 grading_unavailable` instead of a thrown `resolve()`.
16
- - `routes/public.js` (`GET /public/grades`) resolves it the same way and reads `grade.RUBRIC.{threshold,version}`, falling back to `null` when grading is off.
17
- - This replaced the former `const grader = require('../grader')` core→domain import in both files — the edge ADR 0091 §4 set out to sever.
18
-
19
- Why a route factory for a module with **no HTTP routes**: `mountModuleRoutes` is the only loader hook that runs both in the live server and when a test rebuilds the GDS router, so it's where the port registration belongs. The factory returns an empty `express.Router()` purely as the mount vehicle.
20
-
21
- ## Files
22
-
23
- | File | What |
24
- |---|---|
25
- | `grader.js` | the quality gate (~86 KB): v4 adversarial panel + `validateAndScore`/`serverSideGuards`/`gradeSubagent`/`RUBRIC`. The deterministic pre-passes reach `routeRankCheck`/`permissionPathCheck` through the doorway; `runSubagentCached` wraps spawns in the doorway's `llmCache`. |
26
- | `grader-workers/` | the four worker modules (`narc`/`quality`/`hacker`/`efficiency`) + shared kit (`fence`, `worker-prompt`, `worker-verdict`, `rubric`, `extract-json`, `index`). All intra-module relative requires — unchanged by the move. |
27
- | `grader-rubric.json` | the single-source scoring rubric (thresholds, per-worker config). Read by `grader.js` + `grader-workers/rubric.js` via `__dirname`-relative paths — survives the move. |
28
- | `routes/grade.js` | registers the `grade` port (above); no HTTP endpoint. |
29
-
30
- ## NOT in this module (deliberately left in core)
31
-
32
- - **`cascade-dispatch.js`** — stays in `src/bongos/`. It is dormant System 3 (model-tier dispatch, `OTB_AUTONOMY_ENABLED`-gated; nothing calls it but its mock unit test), and its real dependencies are core `task-classifier` (lifecycle) + `skill-prefs` (builder-settings), **not** the grader (the grader is an injected `runner`, never imported). Moving it would force two gratuitous doorway domain-leaks (`classifyTaskKind`, `skill-prefs.getForBuilder`) for capabilities grading never uses — the "ceremony with no boundary benefit" ADR 0091 §Alternatives rejects. It stays a core lib (scoped to `grading` in `module-scope-map.js`) until autonomy/dispatch is carved (tranche 2). The grading scope-map glob lists it as a core file, not part of this dir.
33
- - **`scripts/gds/grade-smoke.js`** + the other grading-owned CLIs — a command travels with its domain but stays under `scripts/gds/` (the memory-carve precedent). They `require('../../modules/grading/grader')`.
34
- - **`llm-cache.js`** — shared LLM-cost infra core also uses (`routes/llm-cache.js`, the cost scripts); it stays core and is exposed to this module on the doorway as `api.llmCache` (a tracked R70 leak).
35
-
36
- ## Tables
37
-
38
- None owned. The grade-attempt rows (`grades`, `grade_attempts`, …) are written by core `db.applyGrade` on the lifecycle side; grading only computes the verdict the route then persists. No `modules/grading/migrations/` yet.
39
-
40
- ## Tests
41
-
42
- `tests/grader.mjs`, `tests/grader_panel.mjs`, `tests/grader_prompt_fencing.mjs`, `tests/grade_server_authoritative.mjs`, `tests/grade_unavailable_display.mjs` import from `modules/grading/…`. `tests/cascade-dispatch.mjs` still imports `src/bongos/cascade-dispatch.js` (it stayed core). `scripts/gds/grade-smoke.js` is the live panel smoke.
14
+ _To work ON the platform core itself, use the standalone @bongos/core source repository,
15
+ not this vendored copy._
@@ -1,61 +1,15 @@
1
- # `hall-ui` module — the authenticated builders' hall
1
+ # @bongos/core — installed platform dependency (not your project's CLAUDE.md)
2
2
 
3
- > A CORE-domain carve (BV1.R85, [ADR 0093](../../docs/adr/<redacted>.md) §1) — the **second WEB-SURFACE module** ([02-module-map §6](../../docs/design/modular-architecture/02-module-map.md): "a web surface is first-class"), after status-UI (BV1.R84), and the **largest** (~13k LOC). `default: true` — a Cloud Bongos instance ships its hall like it ships its status page, so it mounts unconditionally unless an instance disables it.
3
+ This file is a placeholder shipped inside the **@bongos/core** package. The core's own
4
+ internal agent notes are intentionally omitted from the published artifact so they cannot
5
+ load into your instance's context.
4
6
 
5
- Owns the authenticated builders' hall served at **`builders.<apex>`** (`builders.example.com`) — the roster, the Work Board, standing/karma, the leaderboard, achievements, the citizen's primer, the system maps, the Repo Atlas, Settings, the dev-box pair page, and the Archon-only watch/harbor/gate pages. A no-framework single-page surface (`builders.js` + per-page `*.js`) that polls `/api/gds/*` and the read-only `/api/gds/public/*` surface (live updates ride the SSE channel, `routes/live.js`).
7
+ **It does not govern this repository.** Your own root `CLAUDE.md`, your `.claude/`
8
+ directory, and the live Bongos database are the source of truth for how work happens here
9
+ and who may do what.
6
10
 
7
- ## The shape: a static surface, but with DEDICATED serving (the difference from status-UI)
11
+ You are seeing this only because an agent opened a file under `node_modules/@bongos/core/`.
12
+ Nothing in here needs to be read to use the platform — run `bongos help` for commands.
8
13
 
9
- Like status-UI this module has **no `.js` backend, no `db.js` slice, no kernel port, and no routes**. It is purely:
10
- - `public/` — the static hall (HTML/CSS/JS/cursors/chime.wav), moved verbatim from the old repo-root `public-builders/`.
11
- - `module.json` — declares one **`contributes.webSurfaces`** entry `{ host: "builders.", dir: "public" }`.
12
-
13
- **But the hall is NOT served by the generic web-surface loop that serves status-UI.** The status page is a plain static surface; the hall has far richer serving that MUST be byte-identical:
14
- - the apex `/builders/*` tree + the `builders.<apex>` subdomain **rewrite shim** (#726),
15
- - the **canonical-URL redirects** (#744: `/builders/ranks` → `/ranks` on the subdomain; #739: apex `/builders/*` → `builders.<apex>`),
16
- - the **citizen/Archon page gates** (#488/#770: primer, diagrams, work, pair, settings, atlas are sign-in-gated; watch, harbor, gate are Archon-only — the real curl-proof access control),
17
- - per-page **asset-stamping** (#521 content-hash `?v=`),
18
- - the **module hall-widget injection** (BV1.R75 — `injectModuleHallWidgets`),
19
- - the diagram PNG + `primer.md` static mounts.
20
-
21
- So the manifest `webSurfaces` declaration is the **discovery + scope-map + `window.__MODULES__` record**; the bytes are served by the hall's dedicated block in `serve-internal.js`.
22
-
23
- ## The serving seam (how the hall keeps its dedicated block + is still a module)
24
-
25
- `src/bongos/serve-internal.js` resolves `loader.moduleWebSurfaces({ isEnabled })` — the same seam status-UI uses — but **PARTITIONS the hall out** of the generic static loop on the builders-host predicate (`isBuildersHost`), so:
26
- - the **generic loop** owns every web surface EXCEPT the hall (status-UI today),
27
- - the hall's **dedicated block** owns the builders host and reads from `BUILDERS_DIR = modules/hall-ui/public/`.
28
-
29
- The filter is on the **host** (`isBuildersHost`), not the `"hall-ui"` key, so the kernel still names no module key ([ADR 0083](../../docs/adr/<redacted>.md) §Decision #4). `isModuleSurfaceHost` (which the dedicated `/builders` guards use to skip the GENERIC status surface) therefore deliberately EXCLUDES the builders host — the dedicated block is what serves it, so it must not self-skip.
30
-
31
- ## The boundary
32
-
33
- This module reaches **nothing** in core (no doorway require — it has no JS the server requires; `builders.js` etc. are browser code). Core, in turn, must never `require()` a file in here — `serve-internal.js` resolves the surface through `loader.moduleWebSurfaces`/`BUILDERS_DIR`, never an import of `modules/hall-ui/*`. Both directions are enforced by the BV1.R44/R45 fitness checks.
34
-
35
- ## The data it renders STAYS shared core (the judgment call)
36
-
37
- The `/api/gds/me`, `/api/gds/public/*`, and `/api/gds/leaderboard` read endpoints the hall polls **did not move** — they live in `src/bongos/routes/{me,public,builders}.js` and are consumed by the status surface and the off-box mirror too, not just this hall. A read shared by multiple surfaces is correctly core/shared, not hall-exclusive (the same call status-UI / economy / ideas made). So the carve moves the *surface*, not the reads it happens to render.
38
-
39
- ## The hall-widget registry (BV1.R75)
40
-
41
- The hall's `<main>` is filled from a **registry** (`public/dom-utils.js` `contributeHallWidget`; the core set in `public/hall-widgets.js`; a module's own client script otherwise). No widget may be hardcoded as a literal `<section class="scroll">` in `index.html` — a fitness check (`scripts/gds/fitness.js`, `HALL_INDEX` now points at `modules/hall-ui/public/index.html`) reds the build if one creeps back. Module-contributed hall widgets are injected via `serve-internal.js`'s `injectModuleHallWidgets` (the `hallWidgetScripts` loader seam), served at `/modules/<key>/public/hall-widget.js`. (Known pre-existing limitation, idea #465: that asset path 404s on the builders *subdomain* because the rewrite shim shadows it — injection works on both hosts; serving works on the apex. No module ships a hall-widget today so it's a live no-op.)
42
-
43
- ## Files & tooling
44
-
45
- | Path | What |
46
- |---|---|
47
- | `public/index.html` · `public/builders.js` · `public/style.css` | the hall shell + core script + stylesheet (no build step, no framework) |
48
- | `public/{work,roadmap,watch,harbor,gate,pair,settings,ranks,sessions,primer,diagrams,atlas,not-ready}.{html,js}` | the per-page surfaces |
49
- | `public/task.html` + `public/task.js` | the task-detail page (`/task/:id`, task 751) — a real, shareable page that replaced the old `#/task/N` overlay. Irregular routing: `serve-internal.js`'s `TASK_DETAIL_PAGE_RE` resolves the two-segment `/builders/task/:id` path to this one file (every other page is a single-segment slug). (The old read-only Task Board twin `board.html`/`board.js` was removed in task 1965 — `/board` now 301s to `/work`.) |
50
- | `public/goals.html` + `public/goals-page.js` | the Goals page (irregular basename — `goals.js` is the index's Goals hall-widget) |
51
- | `public/dom-utils.js` · `public/hall-widgets.js` | the hall-widget + settings-panel registries (browser IIFEs) |
52
- | `public/instances-panel.js` | the "Instances" hall widget (provisioning module, [ADR 0111](../../docs/adr/<redacted>.md) §1, task 1947) — self-registers **gated on `module:'provisioning'`** (`contributeHallWidget` drops it when the module is off); a self-fetching widget (the `goals.js` pattern) that reads the P1 `/provisioning/*` routes and renders own instances always + the Archon fleet + cost-ledger total when `/me` is `archon`. Read/state only. |
53
- | `public/new-project-wizard.js` | the "Start a project" hall widget (provisioning module, [ADR 0111](../../docs/adr/<redacted>.md) / goal 35, task 1983) — the **CREATE** flow the instances-panel deferred: the most accessible form of the canonical onboarding runbook (the `/new-project` skill task 1981 + `bongos onboard` CLI task 1982). Same `module:'provisioning'` gating; a form that files a **standalone** provisioning request (`POST /provisioning/instances` — the web tier only ENQUEUES, trust boundary intact) then renders the irreducibly-human steps UI-first (create repo, OAuth app with the pinned apex callback, first sign-in) + points at the Instances panel for status. |
54
- | `public/cursors/` · `public/chime.wav` | bronze cursors + the volume-slider preview chime |
55
- | `module.json` | `contributes.webSurfaces: [{ host: "builders.", dir: "public" }]`; no port, no routes |
56
-
57
- Generators that WRITE into this surface live under `scripts/` and were re-pointed at `modules/hall-ui/public/`: `scripts/gds/gen-atlas.js` (the `atlas.json` Repo Atlas data, gitignored — regenerated on deploy), `scripts/art/gen-cursors.py` (cursors), `.claude/sounds/generate.js` (the chime mirror).
58
-
59
- ## Tests
60
-
61
- The loader seam is covered in `tests/module_loader.mjs` (`moduleWebSurfaces` + the live hall-ui surface); the manifest `webSurfaces` validation in `tests/module_manifest.mjs`; the carve discovery-key membership in `tests/fitness.mjs` / `tests/module_loader.mjs` / `tests/modules.mjs`; the browser settings-panel seam in `tests/module_contributions.mjs` (reads `public/dom-utils.js`); the Repo Atlas renderer contract in `tests/atlas.mjs`; and the carve proof in `scripts/gds/coreB-carve-proof.sh` (leg 4i).
14
+ _To work ON the platform core itself, use the standalone @bongos/core source repository,
15
+ not this vendored copy._
@@ -94,6 +94,13 @@
94
94
  todo: { page: 1, perPage: loadPerPageFor(WORK_PER_PAGE_KEY.todo) },
95
95
  done: { page: 1, perPage: loadPerPageFor(WORK_PER_PAGE_KEY.done) },
96
96
  },
97
+ // task 1953 — private goals: my own pending join-request on the OPEN goal (row
98
+ // from GET /goals/inbox's myPendingRequests, filtered to this goal), or null.
99
+ myPendingRequest: null,
100
+ // task 1953 — the open goal's own pending invite/join-request queue (manager
101
+ // view, GET /goals/:id/requests), or null while loading / on error / not a
102
+ // manager. Feature-detected: an older server 404s and the panel just stays hidden.
103
+ pendingRequests: null,
97
104
  };
98
105
 
99
106
  async function getJSON(path) {
@@ -112,6 +119,13 @@
112
119
  if (!res.ok) { const e = new Error(`${path} → ${res.status}`); e.status = res.status; e.data = data; throw e; }
113
120
  return data;
114
121
  }
122
+ // task 1953 — the visibility toggle is the only PATCH this page makes.
123
+ async function patchJSON(path, body) {
124
+ const res = await api.request('PATCH', path, { body: body || {} });
125
+ const data = res.data || {};
126
+ if (!res.ok) { const e = new Error(`${path} → ${res.status}`); e.status = res.status; e.data = data; throw e; }
127
+ return data;
128
+ }
115
129
 
116
130
  const curLabel = () => window.OTBBranding?.currencyLabel?.() ?? 'credits';
117
131
  // task 1957 — a task's drachmae is an ESTIMATE until it SETTLES (confirmed/
@@ -857,38 +871,74 @@
857
871
  const depsSpan = deps.length
858
872
  ? `<span class="goal-deps${(g.status === 'open' && unmet.length) ? ' goal-deps--blocked' : ''}" title="This goal is blocked until each prerequisite goal is achieved">${(g.status === 'open' && unmet.length) ? '🔒 ' : ''}depends on: ${deps.map((dep) => `${escapeHtml(dep.title || ('goal ' + dep.id))} ${dep.satisfied ? '✓' : '○'}`).join(', ')}</span>`
859
873
  : '';
860
- meta.innerHTML = `<span>${escapeHtml(g.version_id || '')}</span> ${statusBadge(g.status)} ${scope} ${depsSpan}`;
874
+ // task 1953 — a private goal is join-GATED, not hidden, so the lock badge is
875
+ // informational for every viewer (the toggle that flips it lives in Actions,
876
+ // manager-only).
877
+ const lockSpan = g.visibility === 'private'
878
+ ? '<span class="goal-badge goal-badge--locked" title="Private — join is by invitation or an approved request">🔒 Private</span>' : '';
879
+ meta.innerHTML = `<span>${escapeHtml(g.version_id || '')}</span> ${statusBadge(g.status)} ${lockSpan} ${scope} ${depsSpan}`;
861
880
  }
862
881
 
863
- const actions = $('#goal-detail-actions');
864
- if (actions) {
865
- actions.innerHTML = g.status === 'open'
866
- ? (joined
867
- ? '<button class="btn-ghost" id="goal-leave-btn" type="button">Leave goal</button>'
868
- : '<button class="landing__btn" id="goal-join-btn" type="button">Join goal</button>')
869
- : '';
870
- $('#goal-join-btn')?.addEventListener('click', () => joinLeave(g.id, 'join'));
871
- $('#goal-leave-btn')?.addEventListener('click', () => joinLeave(g.id, 'leave'));
872
- }
882
+ // task 1762: the goal's own owner/manager (or an Archon) can invite/manage
883
+ // without an Archon in the loop — mirrors the server's authorizeGoalManagement
884
+ // gate. A protected-scope goal stays Archon-only on the SERVER (goals.js);
885
+ // this client gate deliberately doesn't re-derive the protected-module list
886
+ // (that roster lives server-side only, module-scope-map.js) — a non-Archon
887
+ // owner/manager sees the control on a protected-scope goal and gets a clear
888
+ // toast if the server 403s (see inviteMember's error handling below).
889
+ const isOwner = sameId(g.created_by, selfId());
890
+ const canManage = g.status === 'open' && (isArchon() || isOwner || iAmLead);
891
+ renderActions(g, { joined, canManage });
873
892
 
874
893
  // task 1828 — Completion + Members each render into their band column AND
875
894
  // their dedicated full view (capped in the band, "View all →" into the view).
876
895
  renderCompletion();
877
- // task 1762: the goal's own owner/manager (or an Archon) can invite without
878
- // an Archon in the loop — mirrors the server's authorizeGoalManagement gate.
879
- // A protected-scope goal stays Archon-only on the SERVER (goals.js); this
880
- // client gate deliberately doesn't re-derive the protected-module list (that
881
- // roster lives server-side only, module-scope-map.js) — a non-Archon owner/
882
- // manager sees the control on a protected-scope goal and gets a clear toast
883
- // if the server 403s (see inviteMember's error handling below).
884
- const isOwner = sameId(g.created_by, selfId());
885
- const canInvite = g.status === 'open' && (isArchon() || isOwner || iAmLead);
886
- renderInviteBox(g.id, canInvite, members);
896
+ renderInviteBox(g.id, canManage, members);
887
897
  renderMembers();
888
898
 
889
899
  loadGoalTasks(g.id);
900
+ // task 1953 — my own pending request (any viewer) + the goal's pending queue
901
+ // (managers only); async, re-renders Actions + the pending panel on arrival.
902
+ loadGoalMembership(g.id, canManage);
890
903
  void sat; void crit; // now read inside renderCompletion()
891
- void iAmLead; // lead-only resolution actions arrive with tasks 1735/1736
904
+ }
905
+
906
+ // task 1953 — the goal-detail Actions row: Leave/Join (unchanged), a manager's
907
+ // Private/Public toggle (PATCH /goals/:id/visibility), and — for a non-member
908
+ // viewer of a PRIVATE goal — Request to join / Requested (withdraw) in place of
909
+ // Join (private goals are join-GATED: POST /goals/:id/join 409s goal_private for
910
+ // a non-Archon non-member). An Archon keeps the plain Join button: the server
911
+ // lets them self-join any goal regardless of visibility (POST /goals/:id/join),
912
+ // so "request" would be a pointless extra step for them.
913
+ function renderActions(g, { joined, canManage }) {
914
+ const actions = $('#goal-detail-actions');
915
+ if (!actions) return;
916
+ const parts = [];
917
+ if (canManage) {
918
+ const nextVisibility = g.visibility === 'private' ? 'public' : 'private';
919
+ const label = g.visibility === 'private' ? '🔒 Private — make public' : 'Make private';
920
+ parts.push(`<button class="btn-ghost" id="goal-visibility-btn" type="button" data-next-visibility="${escapeHtml(nextVisibility)}">${label}</button>`);
921
+ }
922
+ if (g.status === 'open') {
923
+ if (joined) {
924
+ parts.push('<button class="btn-ghost" id="goal-leave-btn" type="button">Leave goal</button>');
925
+ } else if (g.visibility === 'private' && !isArchon()) {
926
+ if (state.myPendingRequest) {
927
+ parts.push('<span class="role-pill" title="Awaiting the owner/manager\'s approval">Requested</span>');
928
+ parts.push('<button class="btn-ghost" id="goal-withdraw-btn" type="button">Withdraw</button>');
929
+ } else {
930
+ parts.push('<button class="btn-ghost" id="goal-request-btn" type="button">🔒 Request to join</button>');
931
+ }
932
+ } else {
933
+ parts.push('<button class="landing__btn" id="goal-join-btn" type="button">Join goal</button>');
934
+ }
935
+ }
936
+ actions.innerHTML = parts.join(' ');
937
+ $('#goal-join-btn')?.addEventListener('click', () => joinLeave(g.id, 'join'));
938
+ $('#goal-leave-btn')?.addEventListener('click', () => joinLeave(g.id, 'leave'));
939
+ $('#goal-visibility-btn')?.addEventListener('click', (e) => toggleVisibility(g.id, e.currentTarget.dataset.nextVisibility));
940
+ $('#goal-request-btn')?.addEventListener('click', () => requestToJoin(g.id));
941
+ $('#goal-withdraw-btn')?.addEventListener('click', () => withdrawRequest(g.id, state.myPendingRequest && state.myPendingRequest.id));
892
942
  }
893
943
 
894
944
  // task 1828 — the active-claim feed (all builders' in-flight claims) powers the
@@ -1056,12 +1106,153 @@
1056
1106
  } catch (err) {
1057
1107
  const code = err?.data?.error;
1058
1108
  if (code === 'archon_approval_required') toast('This goal is Archon-mediated — an Archon must add you.');
1109
+ else if (code === 'goal_private') toast('This goal is private — request to join instead.');
1059
1110
  else if (code === 'not_a_member') toast('You are not a member of this goal.');
1060
1111
  else if (code === 'goal_not_open') toast('This goal is not open.');
1061
1112
  else toast(`Could not ${action} just now.`);
1062
1113
  }
1063
1114
  }
1064
1115
 
1116
+ // task 1953 — a manager (or Archon) flips a goal public|private.
1117
+ async function toggleVisibility(goalId, visibility) {
1118
+ try {
1119
+ await patchJSON(`${API}/goals/${encodeURIComponent(goalId)}/visibility`, { visibility });
1120
+ toast(visibility === 'private' ? 'Goal is now private.' : 'Goal is now public.');
1121
+ await loadDetail(goalId, { force: true });
1122
+ renderDetail(goalId);
1123
+ renderList();
1124
+ } catch (err) {
1125
+ const code = err?.data?.error;
1126
+ if (code === 'not_owner_or_manager') toast('Only this goal\'s owner or a manager (or an Archon) may do this.');
1127
+ else if (code === 'goal_not_open') toast('Only an open goal can be managed.');
1128
+ else toast('Could not update visibility just now.');
1129
+ }
1130
+ }
1131
+
1132
+ // task 1953 — a non-member asks to join a private (or protected) goal.
1133
+ // renderDetail (below) re-derives canManage/joined and re-triggers
1134
+ // loadGoalMembership, so a plain reload-and-rerender is enough here — same
1135
+ // shape as joinLeave/inviteMember.
1136
+ async function requestToJoin(goalId) {
1137
+ try {
1138
+ await postJSON(`${API}/goals/${encodeURIComponent(goalId)}/requests`, {});
1139
+ toast('Request sent — the owner or a manager will review it.');
1140
+ await loadDetail(goalId, { force: true });
1141
+ renderDetail(goalId);
1142
+ } catch (err) {
1143
+ const code = err?.data?.error;
1144
+ if (code === 'request_pending') toast('You already have a pending request or invitation for this goal.');
1145
+ else if (code === 'already_a_member') toast('You are already a member of this goal.');
1146
+ else if (code === 'goal_is_open') toast('This goal is open — joining directly instead.');
1147
+ else toast('Could not send the request just now.');
1148
+ }
1149
+ }
1150
+
1151
+ // task 1953 — withdraw MY OWN pending join-request (self-served, no manager needed).
1152
+ async function withdrawRequest(goalId, reqId) {
1153
+ if (reqId == null) return;
1154
+ try {
1155
+ await postJSON(`${API}/goals/${encodeURIComponent(goalId)}/requests/${encodeURIComponent(reqId)}/withdraw`, {});
1156
+ toast('Request withdrawn.');
1157
+ await loadDetail(goalId, { force: true });
1158
+ renderDetail(goalId);
1159
+ } catch (err) {
1160
+ toast((err && err.data && err.data.message) || 'Could not withdraw that request just now.');
1161
+ }
1162
+ }
1163
+
1164
+ // task 1953 — a manager approves/denies a pending join-request from the goal's
1165
+ // own pending panel (mirrors the home-inbox action, POST …/requests/:reqId/respond).
1166
+ const pendingActionsPending = new Set();
1167
+ async function respondToJoinRequest(goalId, reqId, action, btn) {
1168
+ const key = `respond:${reqId}`;
1169
+ if (pendingActionsPending.has(key)) return;
1170
+ pendingActionsPending.add(key);
1171
+ if (btn) btn.disabled = true;
1172
+ try {
1173
+ await postJSON(`${API}/goals/${encodeURIComponent(goalId)}/requests/${encodeURIComponent(reqId)}/respond`, { action });
1174
+ toast(action === 'approve' ? 'Approved — they’re now a member.' : 'Request denied.');
1175
+ await loadDetail(goalId, { force: true });
1176
+ renderDetail(goalId);
1177
+ renderList();
1178
+ } catch (err) {
1179
+ toast((err && err.data && err.data.message) || 'Could not resolve that request just now.');
1180
+ if (btn) btn.disabled = false;
1181
+ } finally {
1182
+ pendingActionsPending.delete(key);
1183
+ }
1184
+ }
1185
+
1186
+ // task 1953 — every pending row on THIS goal, rendered for a manager: join
1187
+ // requests get Approve/Deny; outstanding invites are informational (only the
1188
+ // invitee can accept/decline theirs — the server has no manager-cancel route).
1189
+ function pendingRequestWho(r) {
1190
+ return r.display_name || (r.github_login ? `@${r.github_login}` : `builder #${r.builder_id}`);
1191
+ }
1192
+ function pendingRequestsHtml(rows) {
1193
+ const requests = rows.filter((r) => r.kind === 'request');
1194
+ const invites = rows.filter((r) => r.kind === 'invite');
1195
+ const requestLi = (r) => `<li class="pending-row">
1196
+ <span class="pending-row__who">${escapeHtml(pendingRequestWho(r))}</span>
1197
+ ${r.note ? `<span class="pending-row__note">“${escapeHtml(r.note)}”</span>` : ''}
1198
+ <span class="pending-row__actions">
1199
+ <button class="btn-ghost" type="button" data-approve-req="${escapeHtml(String(r.id))}">Approve</button>
1200
+ <button class="btn-ghost" type="button" data-deny-req="${escapeHtml(String(r.id))}">Deny</button>
1201
+ </span>
1202
+ </li>`;
1203
+ const inviteLi = (r) => `<li class="pending-row">
1204
+ <span class="pending-row__who">${escapeHtml(pendingRequestWho(r))}</span>
1205
+ <span class="pending-row__note">invited — awaiting their response</span>
1206
+ </li>`;
1207
+ const parts = [];
1208
+ if (requests.length) parts.push(`<div class="pending-group"><h3 class="pending-group__h">Join requests<span class="goal-count">${requests.length}</span></h3><ul class="pending-list">${requests.map(requestLi).join('')}</ul></div>`);
1209
+ if (invites.length) parts.push(`<div class="pending-group"><h3 class="pending-group__h">Pending invitations<span class="goal-count">${invites.length}</span></h3><ul class="pending-list">${invites.map(inviteLi).join('')}</ul></div>`);
1210
+ return parts.join('');
1211
+ }
1212
+ function renderPendingRequests() {
1213
+ const box = $('#goal-pending');
1214
+ if (!box) return;
1215
+ const rows = state.pendingRequests;
1216
+ if (!rows || !rows.length) { box.hidden = true; box.innerHTML = ''; return; }
1217
+ box.hidden = false;
1218
+ box.innerHTML = pendingRequestsHtml(rows);
1219
+ }
1220
+
1221
+ // task 1953 — async membership-queue load, kicked off (fire-and-forget) from
1222
+ // renderDetail like loadGoalTasks: my own pending request on this goal (any
1223
+ // viewer, from GET /goals/inbox's myPendingRequests) + — for a manager — the
1224
+ // goal's own pending queue (GET /goals/:id/requests, task 1953's new route).
1225
+ // Feature-detected the way the conflicts panel 404-detects the server-side
1226
+ // detector: an older server (or a non-manager 403) just hides the panel.
1227
+ async function loadGoalMembership(goalId, canManage) {
1228
+ try {
1229
+ const inbox = await getJSON(`${API}/goals/inbox`);
1230
+ if (!sameId(state.current, goalId)) return;
1231
+ state.myPendingRequest = (inbox.myPendingRequests || []).find((r) => sameId(r.goal_id, goalId)) || null;
1232
+ } catch (err) {
1233
+ if (err && err.message === 'redirecting to auth') return;
1234
+ if (!sameId(state.current, goalId)) return;
1235
+ state.myPendingRequest = null;
1236
+ }
1237
+ if (canManage) {
1238
+ try {
1239
+ const r = await getJSON(`${API}/goals/${encodeURIComponent(goalId)}/requests`);
1240
+ if (!sameId(state.current, goalId)) return;
1241
+ state.pendingRequests = r.requests || [];
1242
+ } catch (err) {
1243
+ if (err && err.message === 'redirecting to auth') return;
1244
+ if (!sameId(state.current, goalId)) return;
1245
+ state.pendingRequests = null;
1246
+ }
1247
+ } else {
1248
+ state.pendingRequests = null;
1249
+ }
1250
+ if (!sameId(state.current, goalId)) return;
1251
+ renderPendingRequests();
1252
+ const d = currentDetail();
1253
+ if (d) renderActions(d.goal, { joined: (d.members || []).some((m) => sameId(m.builder_id, selfId())), canManage });
1254
+ }
1255
+
1065
1256
  /* ---------------- data + routing ---------------- */
1066
1257
 
1067
1258
  async function loadDetail(goalId, { force = false } = {}) {
@@ -1084,6 +1275,12 @@
1084
1275
  state.claimsByTask = new Map();
1085
1276
  state.conflictPairs = null;
1086
1277
  state.conflictSource = null;
1278
+ // task 1953 — a freshly opened goal starts with no known membership-queue
1279
+ // state; loadGoalMembership (kicked off from renderDetail) fills it in.
1280
+ state.myPendingRequest = null;
1281
+ state.pendingRequests = null;
1282
+ const pending = $('#goal-pending');
1283
+ if (pending) { pending.hidden = true; pending.innerHTML = ''; }
1087
1284
  setWorkTab('todo');
1088
1285
  ['#goal-todo-count', '#goal-done-count', '#goal-conflicts-count', '#goal-conflicts-view-count',
1089
1286
  '#goal-members-count', '#goal-members-view-count', '#goal-completion-count'].forEach((sel) => {
@@ -1290,6 +1487,14 @@
1290
1487
  if (row && row.dataset.taskId && window.OTBTaskDetail) window.OTBTaskDetail.open(row.dataset.taskId);
1291
1488
  });
1292
1489
  });
1490
+ // task 1953 — Approve/Deny on the goal's own pending-requests panel.
1491
+ $('#goal-pending')?.addEventListener('click', (e) => {
1492
+ if (state.current == null) return;
1493
+ const approveBtn = e.target.closest('[data-approve-req]');
1494
+ if (approveBtn) { respondToJoinRequest(state.current, approveBtn.dataset.approveReq, 'approve', approveBtn); return; }
1495
+ const denyBtn = e.target.closest('[data-deny-req]');
1496
+ if (denyBtn) { respondToJoinRequest(state.current, denyBtn.dataset.denyReq, 'deny', denyBtn); return; }
1497
+ });
1293
1498
  window.addEventListener('hashchange', route);
1294
1499
  wireDragReorder();
1295
1500
 
@@ -150,6 +150,18 @@
150
150
  .goal-invite__form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
151
151
  .goal-invite__form select#goal-invite-builder { flex: 1; min-width: 180px; }
152
152
 
153
+ /* task 1953 — private goals: the lock badge (meta line) + the goal's own
154
+ pending invite/join-request queue (manager view, next to the invite box). */
155
+ .goal-badge--locked { background: var(--warn-weak); color: var(--warn); }
156
+ .goal-pending { margin: 4px 0 10px; display: flex; flex-direction: column; gap: 10px; }
157
+ .pending-group__h { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.02em; color: var(--ink-faint); margin: 0 0 4px; display: flex; align-items: center; gap: 6px; }
158
+ .pending-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
159
+ .pending-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--rule-soft); font-size: 13px; flex-wrap: wrap; }
160
+ .pending-row:last-child { border-bottom: none; }
161
+ .pending-row__who { font-weight: 600; }
162
+ .pending-row__note { color: var(--ink-faint); font-size: 12px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
163
+ .pending-row__actions { display: flex; gap: 6px; margin-left: auto; }
164
+
153
165
  .conflict { border: 1px solid color-mix(in srgb, var(--warn) 30%, var(--border)); background: var(--warn-weak); border-radius: var(--radius-sm); padding: 10px 14px; font-size: 13px; margin: 10px 0 0; }
154
166
  .conflict__pair { font-weight: 600; }
155
167
  .conflict__paths { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 11.5px; color: var(--ink-soft); margin-top: 4px; word-break: break-all; }
@@ -246,6 +258,10 @@
246
258
  <h2 class="scroll__h">Members<span class="goal-count" id="goal-members-count" hidden></span></h2>
247
259
  <p class="scroll__lede" id="goal-members-lede"></p>
248
260
  <div class="goal-invite" id="goal-invite" hidden></div>
261
+ <!-- task 1953 — the goal's own pending invite/join-request queue
262
+ (manager view; feature-detected, hidden on an older server or
263
+ when the viewer isn't owner/manager/Archon). -->
264
+ <div class="goal-pending" id="goal-pending" hidden></div>
249
265
  <ul class="member-list" id="goal-members"></ul>
250
266
  <div class="goal-col__foot" id="goal-members-foot" hidden></div>
251
267
  </div>
@@ -74,11 +74,11 @@
74
74
  const oa = domain ? oauthAppValues(domain) : null;
75
75
  const dnsNote = domain ? '' : '<li>Add a domain you own, then re-run to wire the address (a domain is needed before sign-in works).</li>';
76
76
  const repoStep = adopt
77
- ? `<li><strong>Layer onto your existing repo</strong> — in your repo run <code>bongos init --adopt</code>. It shows a conflict pre-flight (migrations, CI, protected paths) and writes <em>nothing</em> until you accept; it never overwrites your files, and it imports your existing backlog (GitHub issues / a TODO/ROADMAP file). Commit + push.</li>`
78
- : '<li><strong>Create the empty GitHub repo</strong> — github.com → New repository, leave it empty.</li>';
77
+ ? `<li data-onboard-step="github-repo"><strong>Layer onto your existing repo</strong> — in your repo run <code>bongos init --adopt</code>. It shows a conflict pre-flight (migrations, CI, protected paths) and writes <em>nothing</em> until you accept; it never overwrites your files, and it imports your existing backlog (GitHub issues / a TODO/ROADMAP file). Commit + push.</li>`
78
+ : '<li data-onboard-step="github-repo"><strong>Create the empty GitHub repo</strong> — github.com → New repository, leave it empty.</li>';
79
79
  // The one-click GitHub sign-in step + a collapsible manual fallback (only when a
80
80
  // domain is set — the App needs a real callback host).
81
- const signInStep = oa ? `<li><strong>Set up GitHub sign-in — one click</strong>. We’ll open GitHub with a pre-filled app; approve it, and we place the credentials on your instance automatically (no secret to copy).
81
+ const signInStep = oa ? `<li data-onboard-step="oauth-app"><strong>Set up GitHub sign-in — one click</strong>. We’ll open GitHub with a pre-filled app; approve it, and we place the credentials on your instance automatically (no secret to copy).
82
82
  <div class="np-signin-actions">
83
83
  <button id="np-github-app" type="button" class="np-create-btn" data-instance="${escapeHtml(String(instanceId))}">Set up GitHub sign-in</button>
84
84
  <span id="np-github-app-msg" class="ledger__note"></span>
@@ -88,6 +88,7 @@
88
88
  <li><strong>Create the GitHub OAuth app</strong> — GitHub → <em>your account's</em> Settings → Developer settings → OAuth Apps → New OAuth App.
89
89
  <div class="np-kv"><span>Homepage URL</span><code>${escapeHtml(oa.homepageUrl)}</code></div>
90
90
  <div class="np-kv"><span>Authorization callback URL</span><code>${escapeHtml(oa.callbackUrl)}</code></div>
91
+ <div class="np-kv"><span>Enable Device Flow</span><code>✅ required — the terminal login uses it (off by default)</code></div>
91
92
  </li>
92
93
  <li><strong>Place the secret — verified</strong> (never hand-edit the env file): on the control plane run
93
94
  <code>bongos … oauth-secret place ${escapeHtml(slug)} --client-id &lt;id&gt; --client-secret &lt;secret&gt; --apply</code>.</li>
@@ -101,7 +102,7 @@
101
102
  ${repoStep}
102
103
  ${dnsNote}
103
104
  ${signInStep}
104
- ${oa ? `<li><strong>First sign-in</strong> — once the instance is live, open <code>${escapeHtml(oa.homepageUrl)}</code> and sign in with GitHub; that makes you the owner (Archon).</li>
105
+ ${oa ? `<li data-onboard-step="first-sign-in"><strong>First sign-in</strong> — once the instance is live, open <code>${escapeHtml(oa.homepageUrl)}</code> and sign in with GitHub; that makes you the owner (Archon).</li>
105
106
  <li><strong>Verify</strong> — <code>${escapeHtml(oa.homepageUrl)}/healthz</code> should report <code>auth_configured: true</code>.</li>` : ''}
106
107
  </ol>
107
108
  <p class="ledger__note">This is the same sequence the <code>/new-project</code> assistant and <code>bongos onboard</code> use.</p>
@@ -1,37 +1,15 @@
1
- # `ideas` module — the idea inbox + blockers + the `ideas.capture` kernel port
1
+ # @bongos/core — installed platform dependency (not your project's CLAUDE.md)
2
2
 
3
- > The **fourth CORE-domain** carved into a module (BV1.R78, [ADR 0093](../../docs/adr/<redacted>.md) §1) — after memory (R72), grading (R73), and economy (R77). A clean, self-contained slice: its SQL already lived in dedicated `inbox.js`/`blockers.js` files (not the `db.js` monolith), so the carve is a move + a doorway re-point, OTB byte-identical. `default: true` in `module.json` — every instance captures ideas and tracks what's stuck on its owner, so it mounts unconditionally unless explicitly disabled.
3
+ This file is a placeholder shipped inside the **@bongos/core** package. The core's own
4
+ internal agent notes are intentionally omitted from the published artifact so they cannot
5
+ load into your instance's context.
4
6
 
5
- Owns the **methodology intake surfaces** that feed the backlog: the `idea_inbox` (free-form capture → triage → promote/discard/merge, plus the Metic-proposes / Archon-ratifies `criterion-proposal` flow that writes `done_when_criteria`), and the `blockers` queue (a thing stuck on a human; resolving one auto-promotes its linked `blocked` tasks via a DB trigger).
7
+ **It does not govern this repository.** Your own root `CLAUDE.md`, your `.claude/`
8
+ directory, and the live Bongos database are the source of truth for how work happens here
9
+ and who may do what.
6
10
 
7
- ## The boundary (the one rule)
11
+ You are seeing this only because an agent opened a file under `node_modules/@bongos/core/`.
12
+ Nothing in here needs to be read to use the platform — run `bongos help` for commands.
8
13
 
9
- This module reaches core **only** through the published doorway `../../src/module-api.js` (`coreVersion ^1.8.0` — it needs the R78 doorway addition `parseId`, the route-param validator alongside `validateOrRespond`/`LIMITS`). It must **never** `require()` a core internal directly, and never another module. Core, in turn, must never `require()` a file in here — it reaches ideas only through the `ideas.capture` port (below). Both directions are enforced by the BV1.R44/R45 fitness checks.
10
-
11
- ## The `ideas.capture` kernel PORT (ADR 0093 §5 — the headline of this carve)
12
-
13
- - **Provides — port `ideas.capture`** → `inbox.captureIdea`. Registered in `routes/inbox.js`'s factory (the loader runs it at boot, and again whenever a test rebuilds the GDS router — `hasProvider`-guarded so re-instantiation never trips the single-provider duplicate guard). Mirrors `modules/economy/routes/reward.js` (a capability registered from a route factory).
14
- - **It retires the `captureIdea` doorway leak.** ADR 0091 §3 parked `captureIdea` on `src/module-api.js` as a temporary "domain-leak-pending-carve" because the ideas domain wasn't a module yet. This carve deletes that re-export; the discord `#ideas` inbound (`modules/discord/discord-inbound.js`, the **sole** consumer) now resolves `ideas.capture` at call time instead of importing the doorway helper. This is the codebase's **first module→module port** (every prior port is consumed by core); discord declares it in `consumes`.
15
- - **Why a verb-noun port, not a doorway re-export:** the same reason `grade`/`reward` are ports — a module capability another module invokes goes behind the kernel switchboard, never a direct import (ADR 0091 §4).
16
-
17
- ## Files
18
-
19
- | File | What |
20
- |---|---|
21
- | `inbox.js` | the `idea_inbox` domain: `captureIdea` (the port provider) + the keyword `classify` fallback, `listOpenIdeas`/`getIdea`/`countOpenIdeas` (with the V3.R70 skill-friction rank-visibility gate), `updateIdea`, and the `ratifyCriterionProposal` transaction (criterion-proposal → `done_when_criteria` + `karma_log` + `criterion_proposal_audit`, all in one `pool.connect()` tx). Reaches the shared pool through the doorway. |
22
- | `blockers.js` | the `blockers` domain: `listOpenBlockers`/`getBlocker`/`createBlocker`/`resolveBlocker`/`linkBlockerToTask`. `resolveBlocker` relies on the DB trigger on `blockers.status` to auto-promote linked `blocked` tasks. |
23
- | `routes/inbox.js` | `POST/GET /inbox`, `GET /inbox/:id`, `PATCH /inbox/:id` (Metic+ triage), `POST /inbox/:id/ratify` (Archon). Its factory **registers the `ideas.capture` port**. |
24
- | `routes/blockers.js` | `GET /blockers` (**public** — the status page reads it), `GET /blockers/:id`, `POST /blockers` (any builder files), `POST /blockers/:id/{resolve,link}` (Metic+). |
25
-
26
- ## NOT in this module (deliberately left in core)
27
-
28
- - **The public idea/blocker projections** — `routes/public.js` (`projectPublicIdea`/`projectPublicBlocker`) reads `idea_inbox`/`blockers` with its **own** inline `pool.query` SELECTs (existence/ref checks for the hall's `#NNN` deep-link resolver + the public sitemap). Core querying a shared table with its own SQL is allowed; it does **not** import this module, so there is no vendored twin to keep in sync — the boundary is about code imports, not table access.
29
- - **The ideas/blockers CLIs** — `scripts/gds/{capture,triage,render-ideas}.js` travel with the domain but stay under `scripts/`; they now `require('../../modules/ideas/inbox')` (the economy/memory CLI precedent — `scripts/gds/autonomy-gate.js` does the same for its idea-inbox read).
30
-
31
- ## Tables
32
-
33
- None owned. `idea_inbox`, `blockers`, `task_blockers`, `criterion_proposal_audit` (+ the `done_when_criteria`/`karma_log` rows the ratify tx touches) were created before the module system and stay in core `migrations/` (the `schema_migrations` stem-key makes a later relocation a no-op). The module owns the *queries*; any **new** ideas migration belongs in `modules/ideas/migrations/` namespaced `ideas_*` (the additive rule).
34
-
35
- ## Tests
36
-
37
- `tests/criterion_proposal.mjs` imports `modules/ideas/inbox` (the ratify transaction). `tests/discord_inbound.mjs` exercises the consumer (it injects a stub `captureIdea` via dependency injection, so it never hits the real port). `tests/route_validation.mjs` exercises the inbox/blocker write-schemas via the dependency-free `validate()` helper (it imports `_helpers`, not these routes). The doorway-surface contract (`tests/module_api.mjs`) asserts `captureIdea` is **gone** and `parseId` is present. See ADR 0093.
14
+ _To work ON the platform core itself, use the standalone @bongos/core source repository,
15
+ not this vendored copy._