@bongos/core 1.25.15 → 1.25.17

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 (66) hide show
  1. package/.bongos-core.json +128 -58
  2. package/.claude/skills/goal-create/SKILL.md +1 -0
  3. package/.claude/skills/planning-session/SKILL.md +5 -4
  4. package/bin/bongos.js +1 -0
  5. package/clients/bongos-client/README.md +1 -1
  6. package/clients/bongos-client/bongos-client.global.js +14 -0
  7. package/clients/bongos-client/index.cjs +14 -0
  8. package/clients/bongos-client/index.d.ts +22 -2
  9. package/clients/bongos-client/index.mjs +14 -0
  10. package/docs/adr/0406-every-project-gets-a-sandbox-that-follows-main-and-live-moves-only-when-a-person-promotes.md +8 -0
  11. package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +1 -0
  12. package/docs/api/openapi.json +415 -3
  13. package/docs/api-reference.md +13 -2
  14. package/docs/copy-inventory.md +120 -109
  15. package/docs/copy-registry.json +275 -165
  16. package/docs/module-api-changelog.md +4 -2
  17. package/docs/page-inventory.json +2 -0
  18. package/docs/page-readings.json +107 -107
  19. package/docs/project-context.template.md +1 -0
  20. package/modules/government/decision-rules.js +1 -0
  21. package/modules/hall-ui/public/dom-utils.js +5 -0
  22. package/modules/hall-ui/public/genesis-home.js +23 -8
  23. package/modules/hall-ui/public/setup-gate.js +1 -1
  24. package/modules/hall-ui/public/style.css +21 -1
  25. package/modules/lifecycle/first-read-db.js +152 -0
  26. package/modules/lifecycle/first-read.js +265 -0
  27. package/modules/lifecycle/kickoff-checklist.js +29 -6
  28. package/modules/lifecycle/kickoff-seed.js +27 -4
  29. package/modules/lifecycle/migrations/lifecycle_022_repo_maps.sql +44 -0
  30. package/modules/lifecycle/module.json +2 -1
  31. package/modules/lifecycle/routes/first-read.js +79 -0
  32. package/modules/onboarding/genesis-home.js +11 -2
  33. package/modules/onboarding/genesis-work.js +28 -5
  34. package/modules/provisioning/pollers/app-liveness.js +23 -1
  35. package/modules/provisioning/render-sandbox.js +113 -0
  36. package/modules/provisioning/render-standup.js +3 -1
  37. package/modules/provisioning/routes/provisioning.js +4 -1
  38. package/modules/provisioning/routes/render-standup.js +49 -1
  39. package/modules/public-landing/public/assets/guide-scene.css +245 -0
  40. package/modules/public-landing/public/assets/guide-scene.js +566 -0
  41. package/modules/public-landing/public/index.html +62 -2
  42. package/modules/public-landing/public/projects.html +101 -4
  43. package/package-lock.json +2 -2
  44. package/package.json +1 -1
  45. package/release-notes.json +23 -1
  46. package/scripts/bongos/first-read.js +116 -0
  47. package/scripts/bongos/genesis.js +2 -0
  48. package/scripts/bongos/government-walk.js +4 -4
  49. package/scripts/bongos/provision-render-sandbox.js +171 -0
  50. package/scripts/bongos/provision-render.js +37 -2
  51. package/scripts/bongos/provision-teardown.js +1 -1
  52. package/scripts/bongos/render-api.js +33 -3
  53. package/scripts/bongos/render-payload.js +39 -1
  54. package/scripts/gds/first-read.js +7 -0
  55. package/scripts/gds/provision-render-sandbox.js +6 -0
  56. package/src/module-api.js +1 -1
  57. package/tests/first_read.mjs +303 -0
  58. package/tests/genesis_home.mjs +70 -2
  59. package/tests/genesis_work.mjs +49 -0
  60. package/tests/government_act_alone_buildings.mjs +2 -0
  61. package/tests/landing_guide_scene.mjs +504 -0
  62. package/tests/provisioning_app_status.mjs +3 -2
  63. package/tests/provisioning_render_route.mjs +1 -1
  64. package/tests/provisioning_teardown_intent.mjs +50 -0
  65. package/tests/render_api.mjs +5 -2
  66. package/tests/render_sandbox.mjs +447 -0
@@ -43,6 +43,9 @@
43
43
  then styles new markup with an old sheet). -->
44
44
  <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
45
45
  <link rel="stylesheet" href="/assets/world.css">
46
+ <!-- the start-a-project guide, as a scene in the game (task 1005275): drawn only when the
47
+ front-door-guide module is on and its server can talk -->
48
+ <link rel="stylesheet" href="/assets/guide-scene.css">
46
49
  <style>
47
50
  /* ── PAGE-LOCAL SHEET ────────────────────────────────────────────────────
48
51
  The composition: how this page arranges the world's sculpture, and nothing
@@ -574,6 +577,12 @@ body{
574
577
  --orb-x:4vw;
575
578
  --orb-y:-6vh;
576
579
  --doors-cx:0;--doors-cy:0;
580
+ /* the start-a-project guide (assets/guide-scene.css, task 1005275): one column, the
581
+ frame the whole screen, the plan over the talk from its Plan button */
582
+ --gd-pad:0;--gd-frame-w:100%;--gd-frame-h:100%;--gd-cols:minmax(0,1fr);--gd-side:0px;
583
+ --gd-gutter:var(--ds-space-gutter-narrow);--gd-face:56px;--gd-face-img:48px;
584
+ --gd-planbtn:inline-flex;--gd-planx:inline-grid;--gd-go-w:100%;
585
+ --gd-plan-show:none;--gd-plan-pos:absolute;--gd-plan-edge:0px;--gd-talk-under-plan:hidden;
577
586
  }
578
587
  :root[data-mode="dark"] body{
579
588
  --layer-pos:relative;
@@ -779,7 +788,7 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
779
788
  <div class="doors">
780
789
  <div class="doorbar">
781
790
  <a class="door" href="/projects">Find a project to join</a>
782
- <a class="door door--go" href="/projects?view=new">Start a project</a>
791
+ <a class="door door--go" href="/projects?view=new" data-guide-door>Start a project</a>
783
792
  </div>
784
793
  </div>
785
794
  </div>
@@ -945,7 +954,7 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
945
954
  <div class="wrap">
946
955
  <div class="closemark" aria-hidden="true"></div>
947
956
  <div class="doorbar">
948
- <a class="door door--go" href="/projects?view=new">Start a project</a>
957
+ <a class="door door--go" href="/projects?view=new" data-guide-door>Start a project</a>
949
958
  <a class="door" href="/projects">Find a project</a>
950
959
  </div>
951
960
  <div class="cli" data-cli hidden>
@@ -957,6 +966,53 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
957
966
  </section>
958
967
  </main>
959
968
 
969
+ <!-- THE GUIDE (task 1005275): the start-a-project conversation as a scene in the game.
970
+ Hidden, and never opened, unless the front-door-guide module is on and its status read
971
+ says it can talk (assets/guide-scene.js); with it off the doors above are plain links. -->
972
+ <div class="gd" id="guide" role="dialog" aria-modal="true" aria-labelledby="gdTitle" hidden>
973
+ <div class="gd__frame">
974
+ <section class="gd__talk" aria-labelledby="gdTitle">
975
+ <div class="gd__head">
976
+ <h2 class="gd__title" id="gdTitle">Start a project</h2>
977
+ <button type="button" class="gd__x" data-gd="close" aria-label="Close"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 5l10 10M15 5L5 15"/></svg></button>
978
+ </div>
979
+ <div class="gd__who-row">
980
+ <span class="gd__face"><img src="/modules/hall-ui/public/characters/patron.png" alt="" width="64" height="64"></span>
981
+ <div class="gd__meta">
982
+ <p class="gd__name">Your guide</p>
983
+ <p class="gd__left" data-gd="left" aria-live="polite" hidden></p>
984
+ <span class="gd__segs" data-gd="segs" aria-hidden="true" hidden></span>
985
+ </div>
986
+ <button type="button" class="gd__planbtn" data-gd="plan-toggle" aria-expanded="false" aria-controls="gdPlan">Plan</button>
987
+ </div>
988
+ <ol class="gd__log" data-gd="log" aria-live="polite" aria-label="Conversation"></ol>
989
+ <p class="gd__err" data-gd="err" role="alert" hidden></p>
990
+ <form class="gd__say" data-gd="form" hidden>
991
+ <label class="vh" for="gdText">Your message</label>
992
+ <textarea class="gd__text" id="gdText" data-gd="text" rows="2" maxlength="1000" placeholder="What do you want to build?"></textarea>
993
+ <input class="gd__hp" type="text" name="website" data-gd="website" tabindex="-1" autocomplete="off" aria-hidden="true">
994
+ <button type="submit" class="gd__send" data-gd="send">Send</button>
995
+ <div class="gd__sayfoot">
996
+ <button type="button" class="gd__more" data-gd="make-plan" hidden>Make my plan</button>
997
+ <a class="gd__skip" data-gd="skip" href="/projects?view=new">Skip to sign up</a>
998
+ </div>
999
+ </form>
1000
+ <div class="gd__end" data-gd="end" hidden>
1001
+ <button type="button" class="gd__more" data-gd="plan-again" hidden>Make my plan</button>
1002
+ <a class="gd__go" data-gd="go" href="/projects?view=new">Sign up and get started</a>
1003
+ </div>
1004
+ </section>
1005
+ <aside class="gd__plan" id="gdPlan" aria-labelledby="gdPlanH">
1006
+ <div class="gd__planhead">
1007
+ <h3 class="gd__planh" id="gdPlanH">Your plan</h3>
1008
+ <button type="button" class="gd__x" data-gd="plan-close" aria-label="Back to the chat"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 5l10 10M15 5L5 15"/></svg></button>
1009
+ </div>
1010
+ <div class="gd__planbody" data-gd="plan-body"><p class="gd__planwait">When the chat ends, your guide writes your plan here.</p></div>
1011
+ <p class="gd__saved" data-gd="saved" hidden></p>
1012
+ </aside>
1013
+ </div>
1014
+ </div>
1015
+
960
1016
  <footer>
961
1017
  <div class="footrow">
962
1018
  <p><!--apex-only:start-->A non-profit, capture-resistant commons. <!--apex-only:end-->Built by agents, on Cloud&nbsp;Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).</p>
@@ -1035,6 +1091,8 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
1035
1091
  json(API + '/me').then(function (data) {
1036
1092
  var me = data && (data.builder || data.me);
1037
1093
  if (!me || !me.github_login) return;
1094
+ /* the start-a-project guide is for a visitor who has not signed up (task 1005275) */
1095
+ document.querySelectorAll('[data-guide-door]').forEach(function (d) { d.removeAttribute('data-guide-door'); });
1038
1096
  var slot = document.getElementById('who');
1039
1097
  if (!slot) return;
1040
1098
  slot.textContent = '';
@@ -1302,6 +1360,8 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
1302
1360
  });
1303
1361
  })();
1304
1362
  </script>
1363
+ <!-- the start-a-project guide (task 1005275) -->
1364
+ <script src="/assets/guide-scene.js"></script>
1305
1365
  <!-- the Artist review button: signed-in builders only (task 1004499) -->
1306
1366
  <script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
1307
1367
  </body>
@@ -2250,7 +2250,13 @@ summary{min-height:24px;padding:3px 0;}
2250
2250
  wires. null when there is nothing to show (no platform chosen). */
2251
2251
  function appStatusView(data) {
2252
2252
  var app = data && data.app;
2253
- if (!app) return null;
2253
+ if (!app) {
2254
+ /* no app on Render yet: the sandbox says plainly why it has none (task 1005289) */
2255
+ if (!data || !data.sandbox || data.sandbox.state !== 'none') return null;
2256
+ var nv = { root: appEl('div', 'appStat') };
2257
+ sandboxView(data.sandbox, nv);
2258
+ return nv;
2259
+ }
2254
2260
  var v = { root: appEl('div', 'appStat') };
2255
2261
  if (app.state === 'disconnected') {
2256
2262
  v.root.appendChild(appPara(['This project is no longer connected to Render. Its services there are still yours; remove them in Render if you want to stop paying.']));
@@ -2304,6 +2310,7 @@ summary{min-height:24px;padding:3px 0;}
2304
2310
  }
2305
2311
  }
2306
2312
  if (app.links) v.root.appendChild(appPara([appLink(app.links.service, 'Open it on Render')]));
2313
+ sandboxView(data.sandbox, v);
2307
2314
  var vars = (data.env_vars || []).filter(function (x) { return !x.set_for_you; });
2308
2315
  if (vars.length) {
2309
2316
  v.root.appendChild(appEl('h3', null, 'Still to set on Render'));
@@ -2320,7 +2327,59 @@ summary{min-height:24px;padding:3px 0;}
2320
2327
  }
2321
2328
  return v;
2322
2329
  }
2323
- function appTerminal(data) { var a = data && data.app; return !a || a.state === 'disconnected' || !!a.error || a.stage === 'live'; }
2330
+ /* ── the project's sandbox (task 1005289, ADR 0406) ──
2331
+ A free copy of the app on the owner's Render account that takes every finished change
2332
+ first; the live app moves only when a person promotes. Same server-shaped answer as
2333
+ the app's (`data.sandbox`), drawn beside it, node by node. "Add a sandbox" asks for the
2334
+ key once, like every Render action, and never keeps it. */
2335
+ var mSandboxOn = {}; /* project id → has a sandbox to remove, so the teardown asks for the key only then */
2336
+ /* the stage word is the app's own (APP_STAGES), so one page never names one stage two ways */
2337
+ function stageWord(stage) {
2338
+ var s = stage === 'quiet' ? 'building' : stage;
2339
+ for (var i = 0; i < APP_STAGES.length; i++) if (APP_STAGES[i][0] === s) return APP_STAGES[i][1];
2340
+ return APP_STAGES[0][1];
2341
+ }
2342
+ function sandboxView(sb, v) {
2343
+ if (!sb) return;
2344
+ var box = appEl('div', 'appSandbox');
2345
+ box.appendChild(appEl('h3', null, 'Sandbox'));
2346
+ box.appendChild(appPara(['A free test copy of your app. Every finished change reaches it first; your live app only changes when you promote it.'], 'finePrint'));
2347
+ if (sb.state === 'none' || sb.state === 'requested' || sb.state === 'left') {
2348
+ box.appendChild(appPara([String(sb.error || sb.message || '')], sb.state === 'left' ? 'err' : null));
2349
+ } else if (sb.error) {
2350
+ box.appendChild(appPara([String(sb.error)], 'err'));
2351
+ } else {
2352
+ var line = [stageWord(sb.stage)];
2353
+ if (sb.url) line.push(' at ', appLink(sb.url, String(sb.url).replace(/^https:\/\//, '')));
2354
+ line.push('. It sleeps when nobody visits and takes about a minute to wake.');
2355
+ box.appendChild(appPara(line));
2356
+ if (sb.links) box.appendChild(appPara([appLink(sb.links.service, 'Open the sandbox on Render')]));
2357
+ }
2358
+ if (sb.database && sb.database.note) box.appendChild(appPara([String(sb.database.note)], 'finePrint'));
2359
+ if (sb.can_add) {
2360
+ v.sbBtn = appEl('button', 'btn ghost small appSbBtn', sb.state === 'none' ? 'Add a sandbox' : 'Try the sandbox again');
2361
+ v.sbBtn.type = 'button';
2362
+ box.appendChild(v.sbBtn);
2363
+ v.sbForm = appEl('div', 'appRetry appSbForm');
2364
+ v.sbForm.hidden = true;
2365
+ var lab = appEl('label', 'wizField');
2366
+ lab.appendChild(appEl('span', null, 'Render API key'));
2367
+ v.sbKey = appEl('input', 'appRetryKey');
2368
+ v.sbKey.type = 'password'; v.sbKey.autocomplete = 'off'; v.sbKey.spellcheck = false; v.sbKey.maxLength = 200; v.sbKey.placeholder = 'rnd_…';
2369
+ lab.appendChild(v.sbKey);
2370
+ v.sbForm.appendChild(lab);
2371
+ v.sbSend = appEl('button', 'btn small', 'Add the sandbox');
2372
+ v.sbSend.type = 'button';
2373
+ v.sbForm.appendChild(v.sbSend);
2374
+ v.sbHint = appEl('div', 'hint appRetryHint', 'Free on Render. Your key is used once and thrown away.');
2375
+ v.sbHint.setAttribute('role', 'status');
2376
+ v.sbForm.appendChild(v.sbHint);
2377
+ box.appendChild(v.sbForm);
2378
+ }
2379
+ v.root.appendChild(box);
2380
+ }
2381
+ function sandboxSettled(sb) { return !sb || ['none', 'left', 'removed'].indexOf(sb.state) >= 0 || !!sb.error || sb.stage === 'live' || sb.stage === 'quiet'; }
2382
+ function appTerminal(data) { var a = data && data.app; return (!a || a.state === 'disconnected' || !!a.error || a.stage === 'live') && sandboxSettled(data && data.sandbox); }
2324
2383
  function fetchAppStatus(id) {
2325
2384
  return fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + '/render', { credentials: 'same-origin' })
2326
2385
  .then(function (res) { return res.ok ? res.json() : null; })
@@ -2337,6 +2396,8 @@ summary{min-height:24px;padding:3px 0;}
2337
2396
  function paint(data) {
2338
2397
  if (stopped || !alive()) return;
2339
2398
  last = data;
2399
+ var sbNow = data && data.sandbox;
2400
+ mSandboxOn[String(id)] = !!(sbNow && (sbNow.links || sbNow.url));
2340
2401
  /* never repaint over a Retry the owner has opened — it holds a key they are typing */
2341
2402
  /* …whether this watch opened it or an earlier one on the same box did (the project
2342
2403
  page starts a fresh watch on every reload of the project) */
@@ -2352,6 +2413,7 @@ summary{min-height:24px;padding:3px 0;}
2352
2413
  }
2353
2414
  function tick() { fetchAppStatus(id).then(paint); }
2354
2415
  function wire(v) {
2416
+ if (v.sbBtn) wireSandbox(v);
2355
2417
  if (!v.retryBtn) return;
2356
2418
  v.retryBtn.addEventListener('click', function () {
2357
2419
  v.retryForm.hidden = false; v.retryBtn.hidden = true; v.keyEl.focus();
@@ -2381,6 +2443,30 @@ summary{min-height:24px;padding:3px 0;}
2381
2443
  });
2382
2444
  });
2383
2445
  }
2446
+ function wireSandbox(v) {
2447
+ v.sbBtn.addEventListener('click', function () { v.sbForm.hidden = false; v.sbBtn.hidden = true; v.sbKey.focus(); });
2448
+ v.sbSend.addEventListener('click', function () {
2449
+ var key = String(v.sbKey.value || '').trim();
2450
+ if (!/^rnd_[A-Za-z0-9]{16,128}$/.test(key)) {
2451
+ v.sbHint.textContent = 'That doesn’t look like a Render API key. It starts with “rnd_”.'; v.sbHint.className = 'hint appRetryHint err'; return;
2452
+ }
2453
+ var body = JSON.stringify({ key: key });
2454
+ v.sbKey.value = ''; key = '';
2455
+ v.sbSend.disabled = true; v.sbHint.textContent = 'Handing it to Render…'; v.sbHint.className = 'hint appRetryHint';
2456
+ fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + '/render/sandbox', {
2457
+ method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: body
2458
+ }).then(function (res) {
2459
+ body = '';
2460
+ return res.json().catch(function () { return {}; }).then(function (d) {
2461
+ if (!res.ok) throw new Error((d.error && d.error.message) || 'The sandbox could not be queued.');
2462
+ v.sbForm.hidden = true; view = null; clearTimeout(t); ticks = 0; tick();
2463
+ });
2464
+ }).catch(function (err) {
2465
+ body = '';
2466
+ v.sbSend.disabled = false; v.sbHint.textContent = err.message; v.sbHint.className = 'hint appRetryHint err';
2467
+ });
2468
+ });
2469
+ }
2384
2470
  tick();
2385
2471
  return function stop() { stopped = true; clearTimeout(t); };
2386
2472
  }
@@ -11095,7 +11181,12 @@ summary{min-height:24px;padding:3px 0;}
11095
11181
  '<input type="text" id="mDelName" autocomplete="off" spellcheck="false" maxlength="40" ' +
11096
11182
  'aria-label="Type the project name to confirm" placeholder="' + esc(inst.slug) + '">' +
11097
11183
  '<button type="button" class="btn small danger" data-del-yes disabled>Yes, tear it down</button>' +
11098
- '<button type="button" class="linklike" data-del-no>Keep it</button></div>');
11184
+ '<button type="button" class="linklike" data-del-no>Keep it</button></div>' +
11185
+ (mSandboxOn[String(inst.id)]
11186
+ ? '<label class="wizField"><span>Render API key, to remove its sandbox too (optional)</span>' +
11187
+ '<input type="password" id="mDelRenderKey" autocomplete="off" spellcheck="false" maxlength="200" placeholder="rnd_…"></label>' +
11188
+ '<p class="finePrint">Only the free sandbox is removed. Your live app on Render is left as it is. The key is used once and thrown away; without it the sandbox stays on your Render account for you to remove.</p>'
11189
+ : ''));
11099
11190
  var field = $('mDelName');
11100
11191
  if (field) field.focus();
11101
11192
  }
@@ -11126,11 +11217,17 @@ summary{min-height:24px;padding:3px 0;}
11126
11217
  /* the typed name is this page's own gate, never the server's — so a click
11127
11218
  on a button that only LOOKS enabled cannot act */
11128
11219
  if (!inst || !delTypedMatches()) return;
11220
+ var rkField = $('mDelRenderKey');
11221
+ var rk = rkField ? String(rkField.value || '').trim() : '';
11222
+ var headers = { 'Content-Type': 'application/json' };
11223
+ if (/^rnd_[A-Za-z0-9]{16,128}$/.test(rk)) headers['x-render-key'] = rk;
11224
+ rk = '';
11129
11225
  mDelAsking = null;
11130
11226
  delMsg('<p class="finePrint">Asking…</p>');
11131
11227
  fetch(API + '/provisioning/instances/' + encodeURIComponent(inst.id) + '/teardown', {
11132
- method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: '{}'
11228
+ method: 'POST', credentials: 'same-origin', headers: headers, body: '{}'
11133
11229
  }).then(function (res) {
11230
+ delete headers['x-render-key'];
11134
11231
  return jsonOrEmpty(res).then(function (data) {
11135
11232
  if (res.ok) {
11136
11233
  var note = esc(data.message || 'Teardown requested.');
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.25.15",
3
+ "version": "1.25.17",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.25.15",
9
+ "version": "1.25.17",
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.25.15",
3
+ "version": "1.25.17",
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",
@@ -11807,10 +11807,32 @@
11807
11807
  "text": "The project guide now asks whether you are starting fresh or already have something, and for an existing project plans from your own description, without ever reading your files or code."
11808
11808
  }
11809
11809
  ],
11810
- "1.25.15": [
11810
+ "1.25.16": [
11811
+ {
11812
+ "id": "1005110",
11813
+ "text": "Genesis setup now shows each stage's work as a checklist that fills in as tasks ship, and a step that must wait for the one before it is clearly locked until then."
11814
+ },
11815
+ {
11816
+ "id": "1005114",
11817
+ "text": "The project's first setup step now asks the founder real questions — what they are making and why, who it is for, what success looks like in two years, and what others in the area get wrong. Answers are saved as answers, 'not"
11818
+ },
11819
+ {
11820
+ "id": "1005275",
11821
+ "text": "Visitors pressing Start a project can now talk with a game-style guide about their idea, see a plan form and their messages left, then sign up; when the guide is off it works exactly as before."
11822
+ },
11811
11823
  {
11812
11824
  "id": "1005116",
11813
11825
  "text": "Founders now write their own strategy. Each setup stage can be done three ways: answer guided questions, write it freely, or get help from your own AI — and you can switch at any time without losing anything. Your answers are"
11814
11826
  }
11827
+ ],
11828
+ "1.25.17": [
11829
+ {
11830
+ "id": "1005289",
11831
+ "text": "Every project on Render now gets a free test copy of its app that takes each finished change first, shown beside the live site, added in one click for existing projects and removed when the project is taken down."
11832
+ },
11833
+ {
11834
+ "id": "1005280",
11835
+ "text": "Owners bringing in an existing project get a prompt for their own AI to read the code and map goals, tasks and what's broken; they then choose what Bongos files as drafts."
11836
+ }
11815
11837
  ]
11816
11838
  }
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ // scripts/bongos/first-read.js — `bongos first-read`: the command the owner's own AI
3
+ // session runs during the first read-through of a project that already exists (task
4
+ // 1005280, goal 1000106).
5
+ //
6
+ // The session reads the repository on the owner's computer and subscription, writes a
7
+ // map, and uses this to hand it to Bongos. Nothing here sends repository content: only
8
+ // the map file the session wrote, which `check` validates locally first with the same
9
+ // rules the server applies (modules/lifecycle/first-read.js).
10
+ //
11
+ // bongos first-read prompt print the prompt and the map's shape
12
+ // bongos first-read check <map.json> validate locally (no network)
13
+ // bongos first-read submit <map.json> send it for review — files nothing
14
+ // bongos first-read show [<map id>] the newest map (or one), with what is filed
15
+ // bongos first-read accept <map id> --all | --items <ref>,<ref> ONLY with the owner's yes
16
+ // bongos first-read reject <map id> the owner's no — files nothing
17
+
18
+ 'use strict';
19
+
20
+ const fs = require('node:fs');
21
+ const fr = require('../../modules/lifecycle/first-read');
22
+
23
+ function readMap(file) {
24
+ if (!file) { console.error(`usage: bongos first-read check|submit <${fr.MAP_FILE}>`); process.exit(2); }
25
+ let text;
26
+ try { text = fs.readFileSync(file, 'utf8').replace(/^/, ''); } catch (e) { console.error(`Cannot read ${file}: ${e.message}`); process.exit(2); }
27
+ try { return JSON.parse(text); } catch (e) { console.error(`${file} is not valid JSON: ${e.message}`); process.exit(1); }
28
+ }
29
+
30
+ function printErrors(errors) {
31
+ console.error('The map needs fixing:');
32
+ for (const e of errors) console.error(` - ${e}`);
33
+ }
34
+
35
+ function printItems(items) {
36
+ for (const i of items) {
37
+ const mark = i.filed ? ' [already in Bongos]' : '';
38
+ const indent = i.type === 'goal' ? '' : ' ';
39
+ console.log(`${indent}${i.type.padEnd(4)} ${i.title}${mark}\n${indent} ref: ${i.ref}`);
40
+ }
41
+ }
42
+
43
+ // A refusal prints and returns null; the caller then ends with cliExit — never a hard
44
+ // process.exit after a request (the Windows abort in scripts/bongos/CLAUDE.md).
45
+ async function call(method, path, body) {
46
+ const { apiCall } = require('./cli-lib');
47
+ const res = await apiCall(method, path, body);
48
+ if (!res.ok) {
49
+ const d = res.data || {};
50
+ console.error(`Bongos refused (${res.status}): ${d.error || d.code || 'error'}`);
51
+ if (Array.isArray(d.errors)) printErrors(d.errors);
52
+ if (d.unknown) console.error(` unknown refs: ${d.unknown.join(', ')}`);
53
+ if (d.hint || d.message) console.error(` ${d.hint || d.message}`);
54
+ return null;
55
+ }
56
+ return res.data;
57
+ }
58
+
59
+ const fail = () => require('./cli-lib').cliExit(1);
60
+ const flag = (name) => { const i = process.argv.indexOf(name); return i === -1 ? null : (process.argv[i + 1] || ''); };
61
+
62
+ (async () => {
63
+ const [verb, arg] = process.argv.slice(2);
64
+ switch (verb) {
65
+ case 'prompt':
66
+ console.log(fr.firstReadPrompt());
67
+ return;
68
+ case 'check': {
69
+ const v = fr.validateRepoMap(readMap(arg));
70
+ if (!v.ok) { printErrors(v.errors); process.exit(1); }
71
+ console.log(`The map is in the right shape: ${v.items.length} items.`);
72
+ printItems(v.items);
73
+ return;
74
+ }
75
+ case 'submit': {
76
+ const raw = readMap(arg);
77
+ const v = fr.validateRepoMap(raw);
78
+ if (!v.ok) { printErrors(v.errors); process.exit(1); }
79
+ const out = await call('POST', '/api/bongos/first-read/maps', { map: raw });
80
+ if (!out) return fail();
81
+ console.log(`Sent as map ${out.map_id}. Nothing is filed yet.`);
82
+ printItems(out.items);
83
+ console.log(`\nWalk the owner through it. Only with their yes:\n bongos first-read accept ${out.map_id} --all\n bongos first-read accept ${out.map_id} --items <ref>,<ref>\nor, if they want none of it:\n bongos first-read reject ${out.map_id}`);
84
+ return;
85
+ }
86
+ case 'show': {
87
+ const out = await call('GET', `/api/bongos/first-read/maps/${arg || 'latest'}`);
88
+ if (!out) return fail();
89
+ console.log(`Map ${out.map_id} (${out.status})\n${out.summary}\n`);
90
+ printItems(out.items);
91
+ return;
92
+ }
93
+ case 'accept': {
94
+ if (!arg) { console.error('usage: bongos first-read accept <map id> --all | --items <ref>,<ref>'); process.exit(2); }
95
+ const list = flag('--items');
96
+ const items = process.argv.includes('--all') ? 'all' : (list ? list.split(',').map((s) => s.trim()).filter(Boolean) : null);
97
+ if (!items || (Array.isArray(items) && !items.length)) { console.error('Say what the owner accepted: --all, or --items <ref>,<ref>.'); process.exit(2); }
98
+ const out = await call('POST', `/api/bongos/first-read/maps/${arg}/accept`, items === 'all' ? { all: true } : { items });
99
+ if (!out) return fail();
100
+ console.log(`Filed as drafts: ${out.filed.goals.length} goals, ${out.filed.tasks.length} tasks (tasks wait in backlog until promoted).`);
101
+ for (const g of out.filed.goals) console.log(` goal ${g.id} ${g.title}`);
102
+ for (const t of out.filed.tasks) console.log(` task ${t.id} ${t.title}`);
103
+ for (const s of out.skipped) console.log(` skipped ${s.ref} (${s.reason.replace('_', ' ')})`);
104
+ return;
105
+ }
106
+ case 'reject': {
107
+ if (!arg) { console.error('usage: bongos first-read reject <map id>'); process.exit(2); }
108
+ if (!(await call('POST', `/api/bongos/first-read/maps/${arg}/reject`, {}))) return fail();
109
+ console.log(`Map ${arg} rejected. Nothing was filed.`);
110
+ return;
111
+ }
112
+ default:
113
+ console.log('usage: bongos first-read prompt | check <file> | submit <file> | show [id] | accept <id> --all|--items a,b | reject <id>');
114
+ process.exit(verb ? 2 : 0);
115
+ }
116
+ })().catch((e) => { console.error(e.message || e); process.exitCode = 1; });
@@ -37,7 +37,9 @@ const ORIGIN_LABEL = { assisted: ' (with AI help)', carried_over: ' (carried ove
37
37
  function formatStage(s) {
38
38
  const out = [`${s.stage[0].toUpperCase()}${s.stage.slice(1)} — working ${s.door}` +
39
39
  (s.adopted ? ` · ${s.counts.answered} answered, ${s.counts.not_sure} not sure, ${s.counts.skipped} skipped, ${s.counts.empty} empty` : '')];
40
+ let group = null;
40
41
  for (const q of s.questions) {
42
+ if (q.group && q.group !== group) { group = q.group; out.push(` — ${group} —`); }
41
43
  out.push(` ${q.id}: ${q.ask}`);
42
44
  if (!q.answer) { out.push(' (empty)'); continue; }
43
45
  const a = q.answer;
@@ -73,10 +73,8 @@ const ACTIVE_IDS = BUILDERS.filter((b) => b.status === 'active').map((b) => b.id
73
73
  const FOUNDER = '42';
74
74
  const COUNCIL = ['43', '44'];
75
75
 
76
- // Each bound route as a request that, as today, stops in its handler's own first
77
- // check (a bad id or body) before any database write — the same requests as
78
- // tests/government_act_alone_buildings.mjs, with People's two carrying a real
79
- // custom rank and builder of this project.
76
+ // Each bound route as a request that stops in its handler's first check (bad id or body),
77
+ // before any DB write — as tests/government_act_alone_buildings.mjs; People's two carry a real rank + builder.
80
78
  const BAD = 'x';
81
79
  const REQUESTS = Object.freeze({
82
80
  'PATCH /project-settings/rot-days': ['PATCH', '/project-settings/rot-days', { days: 'soon' }],
@@ -98,6 +96,7 @@ const REQUESTS = Object.freeze({
98
96
  'POST /goals/reorder': ['POST', '/goals/reorder', {}],
99
97
  'POST /work-categories': ['POST', '/work-categories', {}],
100
98
  'PUT /versions/focus': ['PUT', '/versions/focus', { version_id: ' ' }],
99
+ 'POST /first-read/maps/:id/accept': ['POST', `/first-read/maps/${BAD}/accept`, { all: true }],
101
100
  'POST /autonomy/fence': ['POST', '/autonomy/fence', { enabled: 'yes' }],
102
101
  'POST /autonomy/fence/goals': ['POST', '/autonomy/fence/goals', { goal_id: 0 }],
103
102
  'DELETE /autonomy/fence/goals/:goalId': ['DELETE', `/autonomy/fence/goals/${BAD}`],
@@ -141,6 +140,7 @@ const ROUTERS = Object.freeze([
141
140
  'modules/lifecycle/routes/goals.js',
142
141
  'modules/lifecycle/routes/versions.js',
143
142
  'modules/lifecycle/routes/project-settings.js',
143
+ 'modules/lifecycle/routes/first-read.js',
144
144
  'modules/render-deploy/routes/act.js',
145
145
  'modules/npm-release/routes/release.js',
146
146
  'modules/provisioning/routes/core-upgrade.js',