@bongos/core 1.19.208 → 1.19.210

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.208",
6
- "core_contract": "1.19.208",
7
- "source_commit": "6dabd2e84af8785ac779f8a6801b951d80ecad3b",
5
+ "core_version": "1.19.210",
6
+ "core_contract": "1.19.210",
7
+ "source_commit": "00146d83dff6ca94febcce329b5677b8d7dcb25c",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-17T20:55:34.065Z",
9
+ "built_at": "2026-08-17T22:55:17.955Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 310,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1299,
14
+ "functional_verbatim": 1300,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1630,
20
- "tree_sha256": "63df68d9f034ba54b42011a4bfc310a811d2c53cb550a586bf40d5d35228a258",
19
+ "file_count": 1631,
20
+ "tree_sha256": "101f3cedf1222f6a1a14cc53957baf132e2586e9061e196dcff4550d43033a10",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1367,12 +1367,12 @@
1367
1367
  {
1368
1368
  "path": "docs/api-reference.md",
1369
1369
  "mode": "0000644",
1370
- "sha256": "52554cf7c8e5355c59594c872f30975581eb6c1b9a5bcb63efa1a1a19f2d3877"
1370
+ "sha256": "7b61c830bb85b66e1d6d5dad672fd4b90166e01d07be0f5db7677fc38cc24817"
1371
1371
  },
1372
1372
  {
1373
1373
  "path": "docs/api/openapi.json",
1374
1374
  "mode": "0000644",
1375
- "sha256": "825af2c499836301a34cff79192e41556cc863fe9a5378d9e5134c862769c2fa"
1375
+ "sha256": "260f4413742edbb61d7695a1912ecae67383c9787a7f6f40211c5421668da480"
1376
1376
  },
1377
1377
  {
1378
1378
  "path": "docs/architecture.md",
@@ -3982,7 +3982,7 @@
3982
3982
  {
3983
3983
  "path": "modules/provisioning/routes/provisioning.js",
3984
3984
  "mode": "0000644",
3985
- "sha256": "78c89017e17dfba57ade2330fce293e6422aab316e9311f8215ff3eb9fdc3fe5"
3985
+ "sha256": "a4419bdd0693d0e1168051bd640721b615bf400b25f26478d786b9aac908ab5e"
3986
3986
  },
3987
3987
  {
3988
3988
  "path": "modules/provisioning/tests/liveness.mjs",
@@ -4282,12 +4282,12 @@
4282
4282
  {
4283
4283
  "path": "package-lock.json",
4284
4284
  "mode": "0000644",
4285
- "sha256": "df481ba4f5057938d85fadf4e5551602cb50c68f78358dc110d03ebfb99ff278"
4285
+ "sha256": "4cfce69a22e52a9c0a89cd376458a32616167c2c6ca387f9cfc376212fd630db"
4286
4286
  },
4287
4287
  {
4288
4288
  "path": "package.json",
4289
4289
  "mode": "0000644",
4290
- "sha256": "5b6943de459044d4545437bd4941abadbdefcedc0063e291f72205e46956beb3"
4290
+ "sha256": "6c5ee2445f8c94f3ccbcee5c2387ac2029e34607601ff6146c6a932dc6282d40"
4291
4291
  },
4292
4292
  {
4293
4293
  "path": "public-docs/index.html",
@@ -4527,7 +4527,7 @@
4527
4527
  {
4528
4528
  "path": "public-landing/assets/cosmos.css",
4529
4529
  "mode": "0000644",
4530
- "sha256": "611050e8599be5791e89f9711b76ef6efc5b1e8bd703e2badc6a6418a9ed432b"
4530
+ "sha256": "bb8a74eaadebf0adf8e59dd03cab87f9c3cbd7faf21d1d6a5d47b460c032c28b"
4531
4531
  },
4532
4532
  {
4533
4533
  "path": "public-landing/assets/verro-medallion.png",
@@ -4547,7 +4547,7 @@
4547
4547
  {
4548
4548
  "path": "public-landing/projects.html",
4549
4549
  "mode": "0000644",
4550
- "sha256": "20dd4fcb471c8d5a4dced2788ca6fffecaba241207bc0087d918d15b60bcc1a1"
4550
+ "sha256": "e19cbe7db15331509cb1f09c5e8cbcf0379bb55ddb5bc405d4ebf386186f016c"
4551
4551
  },
4552
4552
  {
4553
4553
  "path": "public-landing/world/assets/clouds/cloud-island-large.png",
@@ -5339,6 +5339,11 @@
5339
5339
  "mode": "0000644",
5340
5340
  "sha256": "c5393ea0633f1fd60b1f6a1e86f7f86be489b74c56d2a0e947f8b262eaeab2cb"
5341
5341
  },
5342
+ {
5343
+ "path": "scripts/gds/seed-bongos-v1-idea-routing-tasks.js",
5344
+ "mode": "0000644",
5345
+ "sha256": "1756751e8c32d9667c82173ac7ba78f98b8416aa946c747036502c5853cbf77f"
5346
+ },
5342
5347
  {
5343
5348
  "path": "scripts/gds/seed-bongos-v1-seo-tasks.js",
5344
5349
  "mode": "0000644",
@@ -5897,7 +5902,7 @@
5897
5902
  {
5898
5903
  "path": "src/module-api.js",
5899
5904
  "mode": "0000644",
5900
- "sha256": "d20ec520ccfe8d1ac72910455f97f728feaf179b265f79ebf6f9a2ca21f1e424"
5905
+ "sha256": "8f9beea649114a7133ef56008878f45197bb97c29bdddf6e5c6c5d786ca30981"
5901
5906
  },
5902
5907
  {
5903
5908
  "path": "src/module-loader/catalog.js",
@@ -7517,7 +7522,7 @@
7517
7522
  {
7518
7523
  "path": "tests/projects_hub.mjs",
7519
7524
  "mode": "0000644",
7520
- "sha256": "5a3f724b4e84324d7560254517612b45fd4e4e31e57ebbb0b35a4021d712df6b"
7525
+ "sha256": "3c31493c1b2a804b52f34310dc31f10b3bf67387ed9266c06fd7295aac2f0d5b"
7521
7526
  },
7522
7527
  {
7523
7528
  "path": "tests/provision.mjs",
@@ -7547,7 +7552,7 @@
7547
7552
  {
7548
7553
  "path": "tests/provisioning_settings.mjs",
7549
7554
  "mode": "0000644",
7550
- "sha256": "bb800d2e15045efbac4fadbc62996054d33e163eb86e6a5e09087d409cc7fbfa"
7555
+ "sha256": "a525fdcfa2fde8edd2be3198ca7213b9b6bc43a12f748a718a570b7f993972b6"
7551
7556
  },
7552
7557
  {
7553
7558
  "path": "tests/provisioning_slug_available.mjs",
@@ -7567,7 +7572,7 @@
7567
7572
  {
7568
7573
  "path": "tests/public_landing_projects.mjs",
7569
7574
  "mode": "0000644",
7570
- "sha256": "ea3fc3b311115c765e7734e11cc33750a5ffde9f492d4635460b04cfc2108ad3"
7575
+ "sha256": "d3e132bd85382b1a9ff4fa2d582a5fc72dabc91b2dc50828b5f622f6c112b2f1"
7571
7576
  },
7572
7577
  {
7573
7578
  "path": "tests/public_mirror_export.mjs",
@@ -8725,7 +8725,7 @@
8725
8725
  "provisioning"
8726
8726
  ],
8727
8727
  "summary": "GET /provisioning/instances/:id",
8728
- "description": "GET /provisioning/instances/:id — read one of the caller's own instances. rank: any authenticated builder (own resource). 404 when the id isn't the caller's (own-scoped; does not leak another owner's instance existence).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
8728
+ "description": "GET /provisioning/instances/:id — read one instance (the manage page's read). rank: any authenticated builder (own resource; archon may read any — task 1003041). 404 otherwise — does not leak another owner's instance existence.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
8729
8729
  "x-rank": "any-builder",
8730
8730
  "x-source": "modules/provisioning/routes/provisioning.js",
8731
8731
  "parameters": [
@@ -380,7 +380,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
380
380
  | GET | `/api/bongos/provisioning/health` | `public` | — | GET /provisioning/health — liveness + the core version this module mounted against. |
381
381
  | GET | `/api/bongos/provisioning/instances` | `any-builder` | — | GET /provisioning/instances — the caller's own instances + each open intent. |
382
382
  | POST | `/api/bongos/provisioning/instances` | `any-builder` | `slug`, `target_ref`, `hosting_shape`, `domain`, `no_address`, `tier`, `onboard_mode`, `type` | POST /provisioning/instances — request a new instance. |
383
- | GET | `/api/bongos/provisioning/instances/:id` | `any-builder` | — | GET /provisioning/instances/:id — read one of the caller's own instances. |
383
+ | GET | `/api/bongos/provisioning/instances/:id` | `any-builder` | — | GET /provisioning/instances/:id — read one instance (the manage page's read). |
384
384
  | POST | `/api/bongos/provisioning/instances/:id/domain` | `any-builder` | `domain` | POST /provisioning/instances/:id/domain — give the caller's OWN instance its address (task 1002692, gap 5: no path existed to add a domai… |
385
385
  | POST | `/api/bongos/provisioning/instances/:id/force-teardown` | `metic+archon` | — | POST /provisioning/instances/:id/force-teardown — Archon tears down ANY instance. |
386
386
  | POST | `/api/bongos/provisioning/instances/:id/github-app` | `any-builder` | `account` | POST /provisioning/instances/:id/github-app — mint a GitHub-App-Manifest onboarding link for the caller's OWN instance (task 2080, ADR 01… |
@@ -569,15 +569,25 @@ module.exports = function provisioningRoutes() {
569
569
  }
570
570
  });
571
571
 
572
- // GET /provisioning/instances/:id — read one of the caller's own instances.
573
- // rank: any authenticated builder (own resource). 404 when the id isn't the
574
- // caller's (own-scoped; does not leak another owner's instance existence).
572
+ // Own-scoped with an Archon override: an owner manages their own project; an
573
+ // Archon may act on any (fleet support). 404 for everyone else — no existence
574
+ // leak. Guards the per-id read (the manage page, task 1003041) and both
575
+ // settings routes (task 1003040).
576
+ function instanceAccessDenied(inst, builder) {
577
+ if (!inst) return true;
578
+ if (builder.rank === 'archon') return false;
579
+ return String(inst.owner_builder_id) !== String(builder.id);
580
+ }
581
+
582
+ // GET /provisioning/instances/:id — read one instance (the manage page's read).
583
+ // rank: any authenticated builder (own resource; archon may read any — task
584
+ // 1003041). 404 otherwise — does not leak another owner's instance existence.
575
585
  router.get('/provisioning/instances/:id', auth.requireBuilder, async (req, res) => {
576
586
  const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
577
587
  if (!id) return;
578
588
  try {
579
589
  const inst = await provisioning.getInstanceById(pool, id);
580
- if (!inst || String(inst.owner_builder_id) !== String(req.builder.id)) {
590
+ if (instanceAccessDenied(inst, req.builder)) {
581
591
  return res.fail('instance_not_found', 404);
582
592
  }
583
593
  const open = await provisioning.getOpenIntent(pool, id);
@@ -594,14 +604,8 @@ module.exports = function provisioningRoutes() {
594
604
  // ── Per-project settings (task 1003040, goal 1000058) ─────────────────────
595
605
  // The foundation the manage-page wave hangs off: effective settings are the
596
606
  // DEFAULTS (today's behavior — public surfaces, apply-to-join) overlaid with
597
- // the instance's stored keys. Own-scoped with an Archon override: the owner
598
- // manages their own project; an Archon may act on any (fleet support). 404
599
- // for everyone else — no existence leak (the GET-by-id precedent).
600
- function settingsAccessDenied(inst, builder) {
601
- if (!inst) return true;
602
- if (builder.rank === 'archon') return false;
603
- return String(inst.owner_builder_id) !== String(builder.id);
604
- }
607
+ // the instance's stored keys. Access: instanceAccessDenied above (own-scoped,
608
+ // Archon override, 404 otherwise — no existence leak).
605
609
 
606
610
  // GET /provisioning/instances/:id/settings — read a project's effective
607
611
  // settings + the allowed values (so a form can render its choices).
@@ -611,7 +615,7 @@ module.exports = function provisioningRoutes() {
611
615
  if (!id) return;
612
616
  try {
613
617
  const inst = await provisioning.getInstanceById(pool, id);
614
- if (settingsAccessDenied(inst, req.builder)) return res.fail('instance_not_found', 404);
618
+ if (instanceAccessDenied(inst, req.builder)) return res.fail('instance_not_found', 404);
615
619
  res.json({ ok: true, settings: provisioning.effectiveSettings(inst), options: provisioning.SETTINGS_VOCAB });
616
620
  } catch (err) {
617
621
  console.error('[provisioning] GET /provisioning/instances/:id/settings', err);
@@ -640,7 +644,7 @@ module.exports = function provisioningRoutes() {
640
644
  }
641
645
  try {
642
646
  const inst = await provisioning.getInstanceById(pool, id);
643
- if (settingsAccessDenied(inst, req.builder)) return res.fail('instance_not_found', 404);
647
+ if (instanceAccessDenied(inst, req.builder)) return res.fail('instance_not_found', 404);
644
648
  const updated = await provisioning.updateInstanceSettings(pool, id, patch);
645
649
  await provisioning.recordEvent(pool, {
646
650
  instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'settings',
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.208",
3
+ "version": "1.19.210",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.208",
9
+ "version": "1.19.210",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.208",
3
+ "version": "1.19.210",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -299,8 +299,6 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
299
299
  card is there for — and the ghosted .orb stays the graphic cue. */
300
300
  .proj.gone{border-color:color-mix(in srgb,var(--line) 55%,var(--bg-card));}
301
301
  .proj.gone .projName{color:var(--ink-soft);}
302
- /* click-to-manage landing glow: the sky's owned orb hands focus to this card */
303
- .proj.anchored{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 30%,transparent);}
304
302
  .projBody{flex:1;min-width:0;}
305
303
  .projHead{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
306
304
  .projName{font-family:var(--font-display);font-weight:700;font-size:19px;letter-spacing:-0.3px;}
@@ -319,6 +317,11 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
319
317
  .projMsg.ok{color:var(--ok);}
320
318
  .projMsg.err{color:var(--err);}
321
319
  .projMsg a{color:var(--link-ink);font-weight:700;text-decoration:none;}
320
+ /* the manage view (task 1003041): its back link, meta repo link and the
321
+ honest empty-state links speak the same link ink as every other surface */
322
+ .projMeta a{color:var(--link-ink);font-weight:700;text-decoration:none;}
323
+ .backRow a{color:var(--link-ink);font-weight:700;text-decoration:none;}
324
+ .empty p a{color:var(--link-ink);font-weight:700;text-decoration:none;}
322
325
  .projMsg a:hover{text-decoration:underline;}
323
326
  .projMsg details{margin-top:8px;}
324
327
  .projMsg summary{cursor:pointer;font-size:12.5px;font-weight:700;color:var(--ink-faint);}
@@ -43,6 +43,12 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
43
43
  .viewWrap{padding-bottom:40px;}
44
44
  .card{margin-bottom:26px;}
45
45
 
46
+ /* ── the manage view (task 1003041): container rhythm only — every component
47
+ it draws (card, chip, addr, empty) is cosmos vocabulary ── */
48
+ .backRow{margin:14px 0;font-weight:700;}
49
+ #view-manage .empty{text-align:left;}
50
+ #view-manage .empty p{margin-bottom:0;}
51
+
46
52
  /* The bongo pair on the New-project view. Component-scoped custom properties,
47
53
  never :root (C1). The drum head is SKIN — using --bg-card would turn it black
48
54
  in dark mode — and --drum-shadow is ink in light but PLAIN BLACK in dark,
@@ -557,6 +563,74 @@ h1[tabindex="-1"]:focus{outline:none;}
557
563
  </div>
558
564
  </section>
559
565
 
566
+ <!-- ══════════════════ VIEW · MANAGE ═══════════════════════════════════════
567
+ One owned project's management home (task 1003041, goal-1000058 wave).
568
+ Deep link: ?view=manage&id=N — goToManage() routes the map's owned
569
+ orbs/rows and the mine cards here. The API is the wall: the per-id read
570
+ is own-scoped (owner, or an archon for fleet support) and answers 404
571
+ for anyone else, so a stranger's deep link renders the same clean
572
+ "no project here" as a wrong id — never a permission hint. The settings
573
+ sections are the homes the sibling manage-wave tasks fill in; until each
574
+ ships, its section says so honestly instead of faking a control. -->
575
+ <section id="view-manage" hidden>
576
+ <div class="wrap viewWrap">
577
+ <p class="backRow"><a href="?view=mine" data-nav-to="mine">← Your projects</a></p>
578
+
579
+ <!-- signed out: the same identity-only door the mine view uses (ADR 0143) -->
580
+ <section id="mGate" class="card" hidden>
581
+ <p class="lede">This is a project’s management page — it answers only to the person who owns it. Sign in with GitHub to open yours.</p>
582
+ <a class="btn" id="mSignin" href="/api/bongos/auth/web/start?return=/projects%3Fview%3Dmine">
583
+ <svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
584
+ Sign in with GitHub
585
+ </a>
586
+ </section>
587
+
588
+ <!-- 404: one clean state for "not yours" and "doesn't exist" — the API
589
+ answers both identically on purpose, and so does this copy -->
590
+ <section id="mMissing" class="card" hidden>
591
+ <p class="lede">No project here. It may have been removed, or the link may be wrong.</p>
592
+ <a class="btn small ghost" href="?view=mine" data-nav-to="mine">Back to your projects</a>
593
+ </section>
594
+
595
+ <div class="alert" id="mAlert" role="alert"></div>
596
+ <div class="loading" id="mLoading">Reading the project…</div>
597
+
598
+ <section id="mMain" hidden>
599
+ <div class="hero" style="padding-left:0;padding-right:0">
600
+ <h1>Manage</h1>
601
+ <p class="lede">One project, up close — its address, its repository, and what the platform last <b>verified</b> about it, with a timestamp. The controls land here piece by piece; each section says what it can do today.</p>
602
+ </div>
603
+
604
+ <div class="card">
605
+ <div class="projHead"><span class="projName" id="mName">…</span><span id="mChip"></span></div>
606
+ <div class="projMeta" id="mMeta" hidden></div>
607
+ <div class="addr" id="mAddr" hidden></div>
608
+ <p class="finePrint" id="mCreated" hidden></p>
609
+ </div>
610
+
611
+ <div class="card" id="mVisibility">
612
+ <h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">Who can see it</h2>
613
+ <div class="empty"><p>Choosing who can open this project’s pages — everyone, or only people signed in to it — isn’t wired up here yet. It lands in one of the next updates; until then every project keeps the behavior it already has.</p></div>
614
+ </div>
615
+
616
+ <div class="card" id="mJoinability">
617
+ <h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">How people join</h2>
618
+ <div class="empty"><p>Choosing how new people join — walk right in, apply first, or invite only — isn’t wired up here yet. It lands in one of the next updates.</p></div>
619
+ </div>
620
+
621
+ <div class="card" id="mRepoVisibility">
622
+ <h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">Repository visibility</h2>
623
+ <div class="empty"><p>Making the project’s code repository public or private from here isn’t wired up yet. Until it lands, GitHub’s own settings page is the place to change that.</p></div>
624
+ </div>
625
+
626
+ <div class="card" id="mDelete">
627
+ <h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">Delete the project</h2>
628
+ <div class="empty"><p>Deleting a project from this page isn’t wired up yet. A running project can still be torn down from its card under <a href="?view=mine" data-nav-to="mine">Your projects</a> — that stops the server and keeps your repository.</p></div>
629
+ </div>
630
+ </section>
631
+ </div>
632
+ </section>
633
+
560
634
  <!-- ══════════════════ VIEW · NEW PROJECT ══════════════════════════════════
561
635
  The whole create wizard, absorbed from /create (task 1002763; direction
562
636
  §4 — that route now 302s here). Seven panels behind the five-beat rail; the
@@ -912,12 +986,23 @@ h1[tabindex="-1"]:focus{outline:none;}
912
986
  pointed at /projects?view=new) actually arrive somewhere.
913
987
  ══════════════════════════════════════════════════════════════════════════ */
914
988
 
915
- var VIEWS = { map: 1, mine: 1, new: 1 };
989
+ var VIEWS = { map: 1, mine: 1, new: 1, manage: 1 };
916
990
  var currentView = 'map';
991
+ /* the manage view's target project (task 1003041) — set by goToManage() and
992
+ the ?view=manage&id=N deep link, read by loadManage() */
993
+ var manageId = null;
994
+ var manageInst = null;
917
995
 
918
996
  function viewFromUrl() {
919
997
  var v;
920
998
  try { v = new URLSearchParams(location.search).get('view'); } catch (e) { v = null; }
999
+ if (v === 'manage') {
1000
+ /* per-project: no id names no project — the list is the honest landing */
1001
+ var id = null;
1002
+ try { id = new URLSearchParams(location.search).get('id'); } catch (e) {}
1003
+ if (id) { manageId = String(id); return 'manage'; }
1004
+ return 'mine';
1005
+ }
921
1006
  return VIEWS[v] ? v : 'map';
922
1007
  }
923
1008
 
@@ -930,14 +1015,20 @@ h1[tabindex="-1"]:focus{outline:none;}
930
1015
  if (a.dataset.nav !== 'home' && a.dataset.nav === v) a.setAttribute('aria-current', 'page');
931
1016
  else a.removeAttribute('aria-current');
932
1017
  });
933
- try { history.replaceState(null, '', '?view=' + v); } catch (e) {}
1018
+ /* the manage view's URL names its project — the deep-link contract grew
1019
+ an id in task 1003041, so a manage tab is shareable and reload-safe */
1020
+ var idPart = (v === 'manage' && manageId) ? '&id=' + encodeURIComponent(manageId) : '';
1021
+ try { history.replaceState(null, '', '?view=' + v + idPart); } catch (e) {}
934
1022
  /* the shell's sign-in door returns the visitor to the view they left, and
935
1023
  carries the repo scope when that view is the wizard (ADR 0143: the
936
1024
  create path asks once up front instead of a second consent at Home) */
937
- $('signinTop').href = '/api/bongos/auth/web/start?return=/projects%3Fview%3D' + v + (v === 'new' ? '&repos=1' : '');
1025
+ $('signinTop').href = '/api/bongos/auth/web/start?return=/projects%3Fview%3D' + v +
1026
+ idPart.replace('&', '%26').replace('=', '%3D') + (v === 'new' ? '&repos=1' : '');
938
1027
  /* the mine view is the only one that polls; it re-reads on entry so a
939
1028
  card that changed while you were on the map is not stale */
940
1029
  if (v === 'mine' && me) refreshList();
1030
+ /* manage re-reads its project on entry — no timer lives here (C8) */
1031
+ if (v === 'manage') loadManage();
941
1032
  /* owned chips refresh on re-entry, never on a timer — C8 keeps the map quiet */
942
1033
  if (v === 'map' && me) loadOwnedInstances().then(applyMapFilter);
943
1034
  /* the living sky runs only on stage (C8) — start and stop with the view;
@@ -1169,11 +1260,11 @@ h1[tabindex="-1"]:focus{outline:none;}
1169
1260
  var href = safeHttpUrl(p.origin);
1170
1261
  if (inst) {
1171
1262
  /* an OWNED entry — catalog-annotated or owned-only, one code path.
1172
- Its job is MANAGE (direction §5): same tab, anchored on that
1173
- project. The public-site door still exists on the manage card; a
1174
- middle-click lands a fresh tab on the manage view, which is this
1175
- orb's honest destination. */
1176
- a.href = '?view=mine';
1263
+ Its job is MANAGE (direction §5): a plain click opens that
1264
+ project's manage view (task 1003041), and the href IS that view's
1265
+ deep link, so a middle/ctrl-click lands a fresh tab on the same
1266
+ honest destination. The public-site door stays on the manage page. */
1267
+ a.href = '?view=manage&id=' + encodeURIComponent(String(inst.id));
1177
1268
  a.setAttribute('data-manage', String(inst.id));
1178
1269
  } else {
1179
1270
  /* The href STAYS, and stays honest, even though a plain click now opens
@@ -1441,16 +1532,14 @@ h1[tabindex="-1"]:focus{outline:none;}
1441
1532
  if (p) openCard(p, relOf(p), a);
1442
1533
  });
1443
1534
 
1444
- /* ── Click-to-manage (task 1002744): an owned orb opens YOUR side of the hub,
1445
- anchored on that project's card. The anchor is deferred — setView('mine')
1446
- triggers an async refreshList(), so the target `[data-inst]` node may not
1447
- exist yet; render() and refreshList() retry tryAnchor() once it does. ── */
1448
- var ANCHOR_GLOW_MS = 2200;
1449
- var pendingAnchor = null;
1535
+ /* ── Click-to-manage (task 1002744 → 1003041): an owned orb, row or card
1536
+ opens that project's own manage view — the per-project page the
1537
+ manage-wave tasks fill in. It replaced the scroll-anchor-into-the-list
1538
+ this function used to do. ── */
1450
1539
  function goToManage(instId) {
1451
- pendingAnchor = String(instId || '');
1452
- setView('mine');
1453
- tryAnchor(); /* the list may already be rendered from an earlier visit */
1540
+ manageId = String(instId || '');
1541
+ manageInst = null;
1542
+ setView('manage');
1454
1543
  }
1455
1544
  /* Card-shell lookup by instance id. CSS.escape, not esc(): the data-inst
1456
1545
  attribute holds the DECODED value, so an HTML-escaped id would never match
@@ -1459,18 +1548,6 @@ h1[tabindex="-1"]:focus{outline:none;}
1459
1548
  function liFor(id) {
1460
1549
  return document.querySelector('li[data-inst="' + CSS.escape(String(id == null ? '' : id)) + '"]');
1461
1550
  }
1462
- function tryAnchor() {
1463
- if (!pendingAnchor) return;
1464
- var li = liFor(pendingAnchor);
1465
- if (!li) return;
1466
- pendingAnchor = null;
1467
- /* a search or state filter that hides the target would turn the anchor
1468
- into a silent no-op — clear them the same way the visitor would */
1469
- if (li.hidden) $('mineClear').click();
1470
- li.scrollIntoView({ block: 'center' });
1471
- li.classList.add('anchored');
1472
- setTimeout(function () { li.classList.remove('anchored'); }, ANCHOR_GLOW_MS);
1473
- }
1474
1551
 
1475
1552
  $('mapRows').addEventListener('click', function (ev) {
1476
1553
  /* the row's own "Open →" keeps its job — the card is for the rest of the row */
@@ -3630,7 +3707,9 @@ h1[tabindex="-1"]:focus{outline:none;}
3630
3707
  }
3631
3708
  var lm = lastMsg[inst.id];
3632
3709
  body += '<div class="projMsg' + (lm && lm.cls ? ' ' + lm.cls : '') + '" id="msg-' + iid + '">' + (lm ? lm.text : '') + '</div>';
3633
- var acts = [];
3710
+ /* every owned card opens its manage view (task 1003041) — a real link, so
3711
+ middle/ctrl-click works; the delegated handler upgrades a plain click */
3712
+ var acts = ['<a class="btn small ghost" data-manage="' + iid + '" href="?view=manage&id=' + iid + '">Manage</a>'];
3634
3713
  if (canRetry(inst)) acts.push('<button type="button" class="btn small" data-retry="' + iid + '">Try again</button>');
3635
3714
  if (canRevive(inst)) acts.push('<button type="button" class="btn small" data-revive="' + iid + '">Bring it back</button>');
3636
3715
  if (canTeardown(inst)) acts.push('<button type="button" class="btn small danger" data-teardown="' + iid + '">Tear it down</button>');
@@ -3663,7 +3742,6 @@ h1[tabindex="-1"]:focus{outline:none;}
3663
3742
  });
3664
3743
  recountMine();
3665
3744
  applyMine();
3666
- tryAnchor(); /* a click-to-manage jump may be waiting on this render */
3667
3745
  }
3668
3746
 
3669
3747
  function msg(id, text, cls) {
@@ -3753,7 +3831,6 @@ h1[tabindex="-1"]:focus{outline:none;}
3753
3831
  var sameSet = instances.length === known && instances.every(function (i) { return byId[i.id]; });
3754
3832
  if (!sameSet) { render(instances); return; }
3755
3833
  instances.forEach(function (inst) { takeUpdate(inst.id, inst); });
3756
- tryAnchor(); /* same-set refresh renders no shells — retry here too */
3757
3834
  })
3758
3835
  .catch(function () { /* transient — next tick will try again */ });
3759
3836
  }
@@ -3767,6 +3844,8 @@ h1[tabindex="-1"]:focus{outline:none;}
3767
3844
  /* returning to a MAP tab re-reads the owned overlay once — event-driven,
3768
3845
  like the mine view's entry refresh, not a timer */
3769
3846
  if (!document.hidden && me && currentView === 'map') loadOwnedInstances().then(applyMapFilter);
3847
+ /* returning to a MANAGE tab re-reads its one project the same way */
3848
+ if (!document.hidden && currentView === 'manage' && manageId) loadManage(true);
3770
3849
  });
3771
3850
 
3772
3851
  /* C6 freshness without C8's cost: once a minute the visible MAP refreshes
@@ -3793,6 +3872,93 @@ h1[tabindex="-1"]:focus{outline:none;}
3793
3872
  }
3794
3873
  setInterval(mapChipTick, REFRESH_MS);
3795
3874
 
3875
+ /* ── the manage view (task 1003041): one owned project's management home.
3876
+ The API is the wall — the per-id read is own-scoped with an archon
3877
+ override and 404s for anyone else — so this code only renders what it
3878
+ answers. The data re-reads on entry and tab return, never on a fetch
3879
+ timer (C8); the once-a-minute tick below re-renders from data already
3880
+ in hand so "checked Xm ago" keeps telling the truth (C6). ── */
3881
+
3882
+ function manageSignin() {
3883
+ return API + '/auth/web/start?return=' +
3884
+ encodeURIComponent('/projects?view=manage&id=' + manageId);
3885
+ }
3886
+
3887
+ function renderManage(inst) {
3888
+ manageInst = inst;
3889
+ $('mLoading').hidden = true;
3890
+ $('mGate').hidden = true;
3891
+ $('mMissing').hidden = true;
3892
+ $('mMain').hidden = false;
3893
+ $('mName').textContent = inst.slug || '';
3894
+ /* the ONE chip site of this view — chipFor() or nothing, so the manage
3895
+ page can never tell a different liveness story than the card (C6) */
3896
+ $('mChip').innerHTML = chipFor(inst);
3897
+ var meta = [];
3898
+ if (inst.target_ref) {
3899
+ meta.push('repo <a href="https://github.com/' + esc(inst.target_ref) +
3900
+ '" target="_blank" rel="noopener"><code>' + esc(inst.target_ref) + '</code></a>');
3901
+ }
3902
+ if (inst.hosting_shape) meta.push(esc(SHAPE_LABEL[inst.hosting_shape] || inst.hosting_shape));
3903
+ if (inst.onboard_mode === 'adopt') meta.push('adopted repo');
3904
+ $('mMeta').innerHTML = meta.join('<span aria-hidden="true">·</span>');
3905
+ $('mMeta').hidden = !meta.length;
3906
+ var addr = addressRow(inst);
3907
+ $('mAddr').innerHTML = addr;
3908
+ $('mAddr').hidden = !addr;
3909
+ /* a fact with no answer hides — never a dash (the card rule) */
3910
+ $('mCreated').textContent = inst.created_at ? 'Created ' + listedDate(inst.created_at) : '';
3911
+ $('mCreated').hidden = !inst.created_at;
3912
+ }
3913
+
3914
+ function loadManage(quiet) {
3915
+ if (!manageId) return;
3916
+ $('mAlert').classList.remove('on');
3917
+ $('mSignin').href = manageSignin();
3918
+ if (!quiet) {
3919
+ $('mMain').hidden = true;
3920
+ $('mGate').hidden = true;
3921
+ $('mMissing').hidden = true;
3922
+ $('mLoading').hidden = false;
3923
+ }
3924
+ var asked = manageId; /* a stale answer must never paint a newer project */
3925
+ fetch(API + '/provisioning/instances/' + encodeURIComponent(manageId), { credentials: 'same-origin' })
3926
+ .then(function (res) {
3927
+ if (asked !== manageId) throw new Error('stale');
3928
+ if (res.status === 401) {
3929
+ $('mLoading').hidden = true; $('mMain').hidden = true; $('mGate').hidden = false;
3930
+ throw new Error('handled');
3931
+ }
3932
+ /* 404 is ONE clean answer for "not yours" and "doesn't exist" — the
3933
+ API refuses both identically, so this page cannot leak existence */
3934
+ if (res.status === 404) {
3935
+ $('mLoading').hidden = true; $('mMain').hidden = true; $('mMissing').hidden = false;
3936
+ throw new Error('handled');
3937
+ }
3938
+ if (!res.ok) throw new Error('soft');
3939
+ return res.json();
3940
+ })
3941
+ .then(function (data) {
3942
+ var inst = data.instance || {};
3943
+ inst.open_intent = data.open_intent || null;
3944
+ renderManage(inst);
3945
+ })
3946
+ .catch(function (err) {
3947
+ var m = String(err && err.message);
3948
+ if (m === 'handled' || m === 'stale') return;
3949
+ $('mLoading').hidden = true;
3950
+ var a = $('mAlert');
3951
+ a.textContent = 'Couldn’t read this project right now — refresh in a moment.';
3952
+ a.classList.add('on');
3953
+ });
3954
+ }
3955
+
3956
+ function manageChipTick() {
3957
+ if (document.hidden || currentView !== 'manage' || !manageInst) return;
3958
+ renderManage(manageInst); /* zero requests — re-dates "checked Xm ago" */
3959
+ }
3960
+ setInterval(manageChipTick, REFRESH_MS);
3961
+
3796
3962
  /* ── teardown: ask, confirm (the truth, itemized — direction §5.4), then
3797
3963
  SAY exactly what the server said ── */
3798
3964
 
@@ -3923,6 +4089,14 @@ h1[tabindex="-1"]:focus{outline:none;}
3923
4089
 
3924
4090
  /* one delegated listener for all card actions */
3925
4091
  $('projectsList').addEventListener('click', function (ev) {
4092
+ /* the Manage link is a real anchor — a modified click keeps its browser
4093
+ meaning, a plain one stays in-page (the orb-click contract) */
4094
+ var m = ev.target.closest && ev.target.closest('a[data-manage]');
4095
+ if (m) {
4096
+ if (!isPlainClick(ev)) return;
4097
+ ev.preventDefault();
4098
+ return goToManage(m.getAttribute('data-manage'));
4099
+ }
3926
4100
  var t = ev.target.closest('button');
3927
4101
  if (!t) return;
3928
4102
  if (t.hasAttribute('data-teardown')) return askTeardown(t.getAttribute('data-teardown'));
@@ -0,0 +1,121 @@
1
+ // Seed the tasks for goal 1000071 "Idea routing — work that has a home lands in it" (BONGOS-V1).
2
+ // Source of truth: docs/specs/bongos-v1-idea-routing.md (status: final).
3
+ // Idempotent on (source, source_ref). Re-running resolves to the existing rows.
4
+ //
5
+ // Run: node scripts/gds/seed-bongos-v1-idea-routing-tasks.js
6
+ const os = require('os');
7
+ const sess = require(os.homedir() + '/.config/cloudbongos/gds-session.json');
8
+ const BASE = sess.api_base.replace(/\/$/, '');
9
+ const TOKEN = sess.token;
10
+ const VER = 'BONGOS-V1';
11
+ const GOAL = 1000071;
12
+ const SRC = 'planning-session';
13
+ const META_TASK = 1003059; // this planning session's meta task — moved into the goal
14
+ const DISCORD_BUGS_IDEA = 1000771; // subsumed by R11; marked promoted against it
15
+ const H = { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' };
16
+
17
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
18
+ async function api(method, path, body, _tries = 0) {
19
+ await sleep(400); // base throttle to stay under the per-IP rate limit
20
+ const res = await fetch(`${BASE}/api/gds${path}`, {
21
+ method, headers: H, body: body ? JSON.stringify(body) : undefined,
22
+ });
23
+ const text = await res.text();
24
+ let json; try { json = JSON.parse(text); } catch { json = { _raw: text.slice(0, 300) }; }
25
+ if (res.status === 429 && _tries < 8) {
26
+ const wait = ((json.error && json.error.details && json.error.details.retry_after_seconds) || 30) + 1;
27
+ console.log(` …rate-limited, waiting ${wait}s`);
28
+ await sleep(wait * 1000);
29
+ return api(method, path, body, _tries + 1);
30
+ }
31
+ return { status: res.status, ok: res.ok, json };
32
+ }
33
+
34
+ // Landing matrix (interview-decided): bugs hard-land (Metic+ filer → ready; sub-Metic → backlog);
35
+ // every other goal-bound kind soft-lands (backlog, one Metic+ promote away); homeless → inbox.
36
+ //
37
+ // crit uses the goal's criterion SLUGS via CRIT — positional "Cn" tokens resolve against the
38
+ // whole VERSION's criterion order (and even matched an old criterion literally slugged "C1"),
39
+ // which mislinked the first seed run; slugs are position-independent.
40
+ const CRIT = {
41
+ C1: 'bug-lands-in-its-goal', C2: 'inbox-holds-only-homeless', C3: 'no-route-loses-the-author',
42
+ C4: 'filing-cannot-outrun-rank', C5: 'system-offers-the-home', C6: 'triage-shrinks-to-judgment',
43
+ };
44
+ const TASKS = [
45
+ // T1 — zero-dep layer — P5
46
+ { ref:'R02', kind:'infra', disc:'engineer', prio:5, est:90, crit:['C1','C3'], deps:[], title:'R02 · Schema: idea_inbox.suggested_goal_id + lineage origin \'route\'', desc:'Core migration: nullable suggested_goal_id FK on idea_inbox (routing hint for soft-landed/Discord/homeless ideas). Module migration (ideas_*): extend ideas_task_lineage.origin CHECK with \'route\'. See docs/specs/bongos-v1-idea-routing.md.' },
47
+ { ref:'R03', kind:'bug', disc:'engineer', prio:5, est:90, crit:['C1'], deps:[], title:'R03 · Catch-all fallback never resolves to a non-building version', desc:'generalGoalIdForVersion (modules/lifecycle/db.js:1343) + the null-goal fallback (:1414): a task create must never land in a shipped/frozen version\'s catch-all goal (the 1000771 failure shape). Routed creates with no goal go to the inbox instead. Done-when: test proves a create against a shipped version cannot resolve a catch-all.' },
48
+ { ref:'R07', kind:'feature', disc:'engineer', prio:5, est:120, crit:['C5'], deps:[], title:'R07 · Capture-time goal suggester (advisory consulted BEFORE creation)', desc:'Expose candidate goals for a draft capture — reuse buildGoalAdvisory\'s candidate logic (modules/lifecycle/goal-advisory.js:89) as a pre-creation read (e.g. GET /inbox/goal-suggestions?title=&kind=&version=). Feeds the hall picker (R08), CLI (R09), and Discord suggestion (R11). Done-when: candidates returned for a draft title/kind without creating anything.' },
49
+ { ref:'R10', kind:'feature', disc:'ui', prio:5, est:120, crit:['C5'], deps:[], title:'R10 · Hall move-a-task-to-goal affordance', desc:'PATCH /tasks/:id {goal_id} (task 1763) exists but has no hall UI. Add move-to-goal on the task overlay (Metic+, task.edit) so a wrong routing pick is one reassignment, never a re-file. Done-when: a Metic+ moves a task between same-version goals from the hall.' },
50
+ { ref:'R14', kind:'decision', disc:'engineer', prio:5, est:60, crit:['C2'], deps:[], title:'R14 · ADR: idea routing — capture-time promotion, the landing matrix, the homeless-only inbox', desc:'Write the ADR recording: routing mechanism is capture-time promotion (never inbox bypass — the Discord #bugs lesson), the landing matrix (bugs hard-land by rank; other kinds soft-land; Full Ideas/criterion-proposals/decisions never auto-route), and the inbox\'s new identity (holds only work without a home). Source: docs/specs/bongos-v1-idea-routing.md interview decisions.' },
51
+
52
+ // T2 — the core mechanism — P5
53
+ { ref:'R01', kind:'feature', disc:'engineer', prio:5, est:240, crit:['C1','C3'], deps:['R02'], title:'R01 · Capture-time routing core: POST /inbox accepts goal_id and converts in one transaction', desc:'Extend inbox.captureIdea/route: when goal_id present and the landing matrix says route, one transaction writes the idea row (status=promoted, promoted_to_task_id), the task (in the goal, matrix-decided status), and ideas_task_lineage (origin=route). Validations: goal open + belongs to a building version; Full-grade and criterion-proposal/decision kinds never auto-route; goal-less capture behaves exactly as today. Deps R02.' },
54
+
55
+ // T3 — enforcement + surfaces — P4
56
+ { ref:'R05', kind:'feature', disc:'engineer', prio:4, est:150, crit:['C4'], deps:['R01'], title:'R05 · Rank-aware landing: matrix enforcement server-side', desc:'Metic+ filer\'s bug → status=ready; sub-Metic filer / any non-bug kind → backlog (awaiting any Metic+ promote via the existing task.promote, surfaced in the Needs-your-attention slice). Auto-derived requires_rank floors (ADR 0084) and the goal scope wall run on routed tasks exactly as on authored ones. Deps R01.' },
57
+ { ref:'R11', kind:'feature', disc:'engineer', prio:4, est:180, crit:['C1','C3'], deps:['R01'], title:'R11 · Discord #bugs retired onto the routing path', desc:'Replace discord-bugs.js\'s direct createTask bypass (hardcoded GDS-V3 → goal 1000002 catch-all; destroys attribution) with capture-time routing: idea row + classifier-suggested goal (suggested_goal_id), soft-landing into the pending queue for a one-click Metic+ route. Unlinked authors still yield an attributable idea row (captured_by null). Closes inbox idea 1000771. Deps R01.' },
58
+ { ref:'R12', kind:'feature', disc:'engineer', prio:4, est:120, crit:['C5'], deps:['R02'], title:'R12 · Discord #ideas carries routing hints', desc:'The #ideas inbound (discord-inbound.js) files with no suggested_version at all today. Add: default suggested_version (most-recent building), classifier-suggested suggested_goal_id. Deps R02.' },
59
+ { ref:'R08', kind:'feature', disc:'ui', prio:4, est:240, crit:['C1','C5'], deps:['R01','R07'], title:'R08 · Hall filing surface: file a bug/idea with a goal picker', desc:'The hall has NO capture surface today (triage only). Add file-a-bug/idea: title, body, kind, goal picker fed by the suggester (R07); submits to POST /inbox with goal_id so goal-bound work routes at filing time. Grade capture untouched. Deps R01,R07.' },
60
+ { ref:'R09', kind:'feature', disc:'engineer', prio:4, est:120, crit:['C1','C5'], deps:['R01','R07'], title:'R09 · CLI capture routing: --goal, suggester prompt, active-claim default', desc:'capture.js gains --goal; when the builder holds a claim the suggested goal defaults to the claim\'s goal (interview decision 4); suggester candidates offered otherwise. Deps R01,R07.' },
61
+ { ref:'R13', kind:'feature', disc:'engineer', prio:4, est:120, crit:['C2','C6'], deps:['R07'], title:'R13 · Promote path passes goal_id; /idea-triage goes homeless-only', desc:'task-detail.js (:320) and triage.js (:160) finally send goal_id (suggester-fed default) so even triaged promotions land in a real goal; the /idea-triage skill doc is updated to the homeless-only contract. Deps R07.' },
62
+
63
+ // T4 — proofs — P3
64
+ { ref:'R04', kind:'verify', disc:'engineer', prio:3, est:90, crit:['C3'], deps:['R01'], title:'R04 · PROOF: attribution survives the route end-to-end', desc:'A routed bug\'s task ships; sourceIdeaForTask returns the filer; ideas_task_lineage (origin=route) + captured_by intact — the trail goal 1000067\'s ideator credit will pay against (test). Deps R01.' },
65
+ { ref:'R06', kind:'verify', disc:'engineer', prio:3, est:120, crit:['C4'], deps:['R05'], title:'R06 · PROOF: abuse matrix — filing cannot escalate', desc:'A Xenos mass-filing bugs never lands claimable work, never reaches protected scope, rate limits hold; a sub-Metic cannot reach ready via any capture vector (test suite). Deps R05.' },
66
+
67
+ // T5 — the measured close — P2
68
+ { ref:'R15', kind:'verify', disc:'engineer', prio:2, est:120, crit:['C2','C6'], deps:['R01','R05','R13'], title:'R15 · PROOF: reclassification sweep of the open inbox', desc:'Walk the open idea_inbox (~50 rows at planning time; 14 bugs) under the new rules: every goal-bound bug homed in a real goal; residual inbox is homeless-only; before/after counts recorded in this task\'s notes as the goal\'s measured evidence. Deps R01,R05,R13.' },
69
+ ];
70
+
71
+ const ROOTS = ['R02', 'R03', 'R07', 'R10', 'R14']; // zero-dep, non-spike → open for claiming
72
+
73
+ (async () => {
74
+ const idByRef = {};
75
+ let created = 0, existing = 0;
76
+ console.log(`Seeding ${TASKS.length} tasks under goal ${GOAL} (${VER})…`);
77
+ for (const t of TASKS) {
78
+ const r = await api('POST', '/tasks', {
79
+ version_id: VER, goal_id: GOAL, title: t.title, description: t.desc,
80
+ kind: t.kind, discipline: t.disc, priority: t.prio, est_minutes: t.est,
81
+ criterion_ids: t.crit.map((c) => CRIT[c]), source: SRC, source_ref: `bongos-v1-idea-routing-${t.ref}`,
82
+ });
83
+ if (!r.ok) { console.log(` ${t.ref}: FAILED ${r.status} ${JSON.stringify(r.json)}`); continue; }
84
+ const task = r.json.task || r.json;
85
+ idByRef[t.ref] = task.id;
86
+ if (r.json.idempotent || r.json.existing) existing++; else created++;
87
+ console.log(` ${t.ref} → task ${task.id} (${t.kind}/${t.disc}, P${t.prio}, [${t.crit.join(',')}])`);
88
+ }
89
+
90
+ console.log('Wiring dependencies…');
91
+ let edges = 0;
92
+ for (const t of TASKS) {
93
+ if (!t.deps.length) continue;
94
+ const id = idByRef[t.ref];
95
+ const depIds = t.deps.map((d) => idByRef[d]).filter(Boolean);
96
+ if (!id || depIds.length !== t.deps.length) { console.log(` ${t.ref}: dep resolve gap, skipping`); continue; }
97
+ const r = await api('POST', `/tasks/${id}/dependencies`, { depends_on_task_ids: depIds });
98
+ if (!r.ok) { console.log(` ${t.ref}: dep FAILED ${r.status} ${JSON.stringify(r.json)}`); continue; }
99
+ edges += (r.json.added || depIds.length);
100
+ console.log(` ${t.ref} depends on ${t.deps.join(',')}`);
101
+ }
102
+
103
+ console.log('Promoting the zero-dep layer to ready…');
104
+ for (const ref of ROOTS) {
105
+ const id = idByRef[ref];
106
+ if (!id) continue;
107
+ const r = await api('POST', `/tasks/${id}/promote`, {});
108
+ console.log(` ${ref} (task ${id}): ${r.ok ? 'ready' : 'promote note ' + JSON.stringify(r.json).slice(0, 120)}`);
109
+ }
110
+
111
+ console.log('Linking provenance…');
112
+ if (idByRef.R11) {
113
+ const r = await api('PATCH', `/inbox/${DISCORD_BUGS_IDEA}`, { status: 'promoted', promoted_to_task_id: Number(idByRef.R11) });
114
+ console.log(` idea ${DISCORD_BUGS_IDEA} → ${r.ok ? `promoted against R11 (task ${idByRef.R11})` : 'PATCH note ' + JSON.stringify(r.json).slice(0, 160)}`);
115
+ }
116
+ const meta = await api('PATCH', `/tasks/${META_TASK}`, { goal_id: GOAL });
117
+ console.log(` meta task ${META_TASK} → ${meta.ok ? `goal ${GOAL}` : 'PATCH note ' + JSON.stringify(meta.json).slice(0, 160)}`);
118
+
119
+ console.log(`\nDONE. created=${created} existing=${existing} depEdges=${edges}. Roots opened: ${ROOTS.join(', ')}.`);
120
+ console.log('ref→id:', JSON.stringify(idByRef));
121
+ })();
package/src/module-api.js CHANGED
@@ -843,8 +843,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
843
843
  // landed since 1.19.206 with no explicit bump. run 32065995444. (task 1002620)
844
844
  // 1.19.208 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
845
845
  // landed since 1.19.207 with no explicit bump. run 32068332007. (task 1002620)
846
+ // 1.19.209 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
847
+ // landed since 1.19.208 with no explicit bump. run 32072227661. (task 1002620)
848
+ // 1.19.210 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
849
+ // landed since 1.19.209 with no explicit bump. run 32078135414. (task 1002620)
846
850
  // ---------------------------------------------------------------------------
847
- const CORE_VERSION = '1.19.208'; // CI auto-patch carrier (ADR 0161); see changelog above
851
+ const CORE_VERSION = '1.19.210'; // CI auto-patch carrier (ADR 0161); see changelog above
848
852
 
849
853
  // A namespaced logger so a module's log lines are attributable + consistent.
850
854
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -51,7 +51,11 @@ test('the nav carries Home plus the three views, and Home is not a view', () =>
51
51
 
52
52
  test('?view= is the deep-link contract, read on boot and written with replaceState', () => {
53
53
  assert.match(SCRIPT, /new URLSearchParams\(location\.search\)\.get\('view'\)/);
54
- assert.match(SCRIPT, /history\.replaceState\(null, '', '\?view=' \+ v\)/);
54
+ assert.match(SCRIPT, /history\.replaceState\(null, '', '\?view=' \+ v \+ idPart\)/);
55
+ // the manage view is per-project, so its deep link carries the id too
56
+ // (task 1003041) — and the id is encoded, never raw-interpolated
57
+ assert.match(SCRIPT, /'&id=' \+ encodeURIComponent\(manageId\)/,
58
+ 'the URL writer must carry the manage id, encoded');
55
59
  // the landing page's CTAs have always pointed here — this is the bug they hit
56
60
  const landing = fs.readFileSync(path.join(ROOT, 'public-landing', 'index.html'), 'utf8');
57
61
  assert.match(landing, /href="\/projects\?view=new"/,
@@ -753,7 +757,8 @@ test('owned truth reaches the TABLE too — same list, same chip, same manage jo
753
757
  });
754
758
 
755
759
  test('an owned orb\'s job is manage, not the public site', () => {
756
- assert.match(SCRIPT, /'\?view=mine'/, 'the owned href is the manage deep-link, real and middle-clickable');
760
+ assert.match(SCRIPT, /'\?view=manage&id=' \+ encodeURIComponent\(String\(inst\.id\)\)/,
761
+ 'the owned href is the manage deep-link, real and middle-clickable');
757
762
  assert.match(SCRIPT, /hasAttribute\('data-manage'\)\) return goToManage/,
758
763
  'a plain click on an owned orb routes to manage before the card path');
759
764
  const orbs = (SCRIPT.match(/function renderOrbs\(([\s\S]*?)\n \}/) || [, ''])[1];
@@ -794,13 +799,15 @@ test('the orb chip is the manage list\'s truth, never a second story (C6)', () =
794
799
  'no instance chip may be literal-classed live — chipMarkup() decides from the overlay');
795
800
  });
796
801
 
797
- test('click-to-manage anchors the project and defers until the card exists', () => {
798
- assert.match(SCRIPT, /function goToManage/);
799
- assert.match(SCRIPT, /pendingAnchor/);
800
- assert.match(SCRIPT, /scrollIntoView/);
801
- const fn = (SCRIPT.match(/function tryAnchor\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
802
- assert.ok(fn, 'tryAnchor() must be extractable');
803
- assert.match(fn, /mineClear/, 'a filter that hides the anchored card is cleared, not silently no-op\'d');
802
+ test('click-to-manage opens the per-project manage view (task 1003041)', () => {
803
+ const fn = (SCRIPT.match(/function goToManage\(instId\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
804
+ assert.ok(fn, 'goToManage() must be extractable');
805
+ assert.match(fn, /manageId = String\(instId/, 'the target project rides the module state');
806
+ assert.match(fn, /setView\('manage'\)/, 'the destination is the manage view, not a scroll anchor');
807
+ // the scroll-anchor era ended with its caller — a half-removed pendingAnchor
808
+ // would silently no-op every manage click
809
+ assert.doesNotMatch(SCRIPT, /pendingAnchor/, 'the anchor machinery must be fully gone');
810
+ assert.doesNotMatch(COSMOS, /\.proj\.anchored/, 'and so must its landing-glow rule');
804
811
  });
805
812
 
806
813
  test('the map does not poll for owned truth — it re-reads on entry (C8)', () => {
@@ -887,3 +894,86 @@ test('zoom heads for the cursor and survives line-mode wheels', () => {
887
894
  assert.ok(COSMOS.includes('transform-origin:0 0'),
888
895
  'the camera algebra assumes the layer scales about its origin');
889
896
  });
897
+
898
+ /* ── The manage view (task 1003041, goal-1000058 wave) ──────────────────────
899
+ One owned project's management home: ?view=manage&id=N. The identity block
900
+ renders the own-scoped per-id read (with the verification overlay); the
901
+ settings sections are the homes the sibling manage-wave tasks fill in, each
902
+ an honest not-wired-yet until its task ships. The API is the wall: 404 for
903
+ strangers (owner or archon only), so the page renders one clean not-found
904
+ and never a permission hint. ── */
905
+
906
+ test('the manage view ships as real markup with the four settings homes (C7)', () => {
907
+ assert.match(HUB, /id="view-manage"/);
908
+ for (const id of ['mGate', 'mSignin', 'mMissing', 'mAlert', 'mLoading', 'mMain',
909
+ 'mName', 'mChip', 'mMeta', 'mAddr', 'mCreated',
910
+ 'mVisibility', 'mJoinability', 'mRepoVisibility', 'mDelete']) {
911
+ assert.match(HUB, new RegExp(`id="${id}"`), `${id} must be real markup`);
912
+ }
913
+ // and the not-yet copy a crawler reads is in the document, not a template string
914
+ assert.match(HUB, /isn’t wired up/, 'the honest empty states live in the markup');
915
+ });
916
+
917
+ test('the manage view balances its own tree, like the wizard', () => {
918
+ const start = HUB.indexOf('<section id="view-manage"');
919
+ const end = HUB.indexOf('<section id="view-new"');
920
+ assert.ok(start !== -1 && end > start, 'need the view-manage span');
921
+ const manage = HUB.slice(start, end);
922
+ assert.equal((manage.match(/<div\b/g) || []).length, (manage.match(/<\/div>/g) || []).length,
923
+ 'every div opened in #view-manage closes in #view-manage');
924
+ assert.equal((manage.match(/<section\b/g) || []).length, (manage.match(/<\/section>/g) || []).length,
925
+ 'every section opened in #view-manage closes in #view-manage');
926
+ });
927
+
928
+ test('?view=manage&id=N is the deep link — read on boot, and no id means the list', () => {
929
+ assert.match(SCRIPT, /if \(v === 'manage'\)/, 'viewFromUrl must know the manage view');
930
+ assert.match(SCRIPT, /\.get\('id'\)/, 'and read the project id off the URL');
931
+ assert.match(SCRIPT, /manageId = String\(id\); return 'manage';/,
932
+ 'an id lands on the manage view');
933
+ assert.match(SCRIPT, /return 'mine';/,
934
+ 'a bare ?view=manage names no project — the list is the honest landing');
935
+ });
936
+
937
+ test('a stranger\'s deep link renders one clean not-found — never a permission hint', () => {
938
+ const missing = (HUB.match(/<section id="mMissing"[\s\S]*?<\/section>/) || [''])[0];
939
+ assert.ok(missing, 'the not-found state must be extractable');
940
+ assert.doesNotMatch(missing, /permission|allowed|access|owner/i,
941
+ 'the 404 copy must not hint that the project exists behind a wall');
942
+ assert.match(SCRIPT, /res\.status === 404[\s\S]{0,220}mMissing/,
943
+ 'a 404 lands on the clean not-found state');
944
+ assert.match(SCRIPT, /res\.status === 401[\s\S]{0,220}mGate/,
945
+ 'a 401 stays a sign-in gate, distinct from not-found');
946
+ });
947
+
948
+ test('the manage chip defers to chipFor — one site, no second story (C6)', () => {
949
+ const fn = (SCRIPT.match(/function renderManage\(inst\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
950
+ assert.ok(fn, 'renderManage() must be extractable');
951
+ assert.equal((fn.match(/chipFor\(inst\)/g) || []).length, 1,
952
+ 'renderManage must have exactly one chip call site');
953
+ assert.match(fn, /addressRow\(inst\)/, 'the address sentence is the shared, verified one');
954
+ assert.match(fn, /SHAPE_LABEL/, 'the hosting shape speaks the card\'s vocabulary');
955
+ });
956
+
957
+ test('the manage view spends no timer requests — refresh is entry and tab return (C8)', () => {
958
+ const tick = (SCRIPT.match(/function manageChipTick\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
959
+ assert.ok(tick, 'manageChipTick() must exist');
960
+ assert.match(tick, /currentView !== 'manage'/, 'the tick is view-guarded');
961
+ assert.match(tick, /document\.hidden/, 'and does nothing in a hidden tab');
962
+ assert.doesNotMatch(tick, /fetch\(/, 'zero requests — recency re-renders from data in hand');
963
+ assert.match(SCRIPT, /if \(v === 'manage'\) loadManage\(\);/, 'entry re-reads once');
964
+ assert.match(SCRIPT, /currentView === 'manage' && manageId\) loadManage\(true\)/,
965
+ 'tab return re-reads once, quietly');
966
+ });
967
+
968
+ test('the repo link is built on github.com and escaped — never a raw href', () => {
969
+ assert.match(SCRIPT, /https:\/\/github\.com\/' \+ esc\(inst\.target_ref\)/,
970
+ 'the identity block links the repo through the page escaper on a fixed origin');
971
+ });
972
+
973
+ test('every owned card carries its Manage door, and a modified click keeps its meaning', () => {
974
+ assert.match(SCRIPT, /data-manage="' \+ iid \+ '" href="\?view=manage&id=' \+ iid/,
975
+ 'the card affordance is a real deep-link anchor');
976
+ assert.match(SCRIPT, /closest\('a\[data-manage\]'\)/, 'the delegated handler upgrades it');
977
+ assert.match(SCRIPT, /if \(!isPlainClick\(ev\)\) return;/,
978
+ 'ctrl/middle-click falls through to the browser');
979
+ });
@@ -97,7 +97,36 @@ function reset({ inst, as } = {}) {
97
97
  events = [];
98
98
  }
99
99
 
100
- console.log('GET /provisioning/instances/:id/settings:');
100
+ console.log('GET /provisioning/instances/:id (the manage page\'s read — task 1003041):');
101
+
102
+ await ta('owner reads their own instance', async () => {
103
+ reset();
104
+ const r = await get('/provisioning/instances/18');
105
+ assert.equal(r.status, 200, r.raw);
106
+ assert.equal(r.body.instance.slug, 'spike');
107
+ });
108
+
109
+ await ta('ANOTHER builder\'s GET → 404 (no existence leak)', async () => {
110
+ reset({ as: { id: 8, github_login: 'stranger', rank: 'thetes' } });
111
+ const r = await get('/provisioning/instances/18');
112
+ assert.equal(r.status, 404, r.raw);
113
+ assert.equal(r.body.error.code, 'instance_not_found');
114
+ });
115
+
116
+ await ta('an Archon may read ANY instance — the manage-page override (task 1003041)', async () => {
117
+ reset({ as: { id: 999, github_login: 'archon', rank: 'archon' } });
118
+ const r = await get('/provisioning/instances/18');
119
+ assert.equal(r.status, 200, r.raw);
120
+ assert.equal(r.body.instance.slug, 'spike');
121
+ });
122
+
123
+ await ta('unknown instance id → 404 for everyone, archon included', async () => {
124
+ reset({ inst: null, as: { id: 999, github_login: 'archon', rank: 'archon' } });
125
+ const r = await get('/provisioning/instances/999');
126
+ assert.equal(r.status, 404, r.raw);
127
+ });
128
+
129
+ console.log('\nGET /provisioning/instances/:id/settings:');
101
130
 
102
131
  await ta('owner reads DEFAULTS (public / apply) on a row with empty settings — today\'s behavior', async () => {
103
132
  reset();
@@ -107,6 +107,20 @@ test('a domainless instance gets an honest no-address state', async () => {
107
107
  assert.match(body, /[Nn]o address yet/);
108
108
  });
109
109
 
110
+ test('the manage view is served: identity block + honest not-yet sections (task 1003041)', async () => {
111
+ const { body } = await request('/projects', 'game.test.local');
112
+ assert.match(body, /id="view-manage"/);
113
+ // the four settings homes the sibling manage-wave tasks fill in
114
+ for (const id of ['mVisibility', 'mJoinability', 'mRepoVisibility', 'mDelete']) {
115
+ assert.match(body, new RegExp(`id="${id}"`), `${id} must be served`);
116
+ }
117
+ assert.match(body, /isn’t wired up/, 'each unshipped section says so honestly');
118
+ // the not-found state is one clean answer — never a permission hint
119
+ const missing = (body.match(/<section id="mMissing"[\s\S]*?<\/section>/) || [''])[0];
120
+ assert.ok(missing, 'the not-found state must be served');
121
+ assert.doesNotMatch(missing, /permission|allowed|access|owner/i);
122
+ });
123
+
110
124
  test('the empty state points a new owner at the wizard view — never at a hall', async () => {
111
125
  const { body } = await request('/projects', 'game.test.local');
112
126
  // /create was absorbed into ?view=new (task 1002763); the CTA moves in-page