@bongos/core 1.25.18 → 1.25.20
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 +113 -83
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -1
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +1 -1
- package/docs/api/openapi.json +81 -2
- package/docs/api-reference.md +7 -5
- package/docs/copy-inventory.md +219 -216
- package/docs/copy-registry.json +273 -246
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +348 -347
- package/docs/project-context.template.md +1 -1
- package/modules/front-door-guide/handoff.js +120 -0
- package/modules/front-door-guide/module.json +1 -1
- package/modules/front-door-guide/routes/front-door-guide-plan.js +24 -0
- package/modules/hall-ui/public/ai-studio.html +1 -1
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/genesis-home.js +51 -7
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/home-city-draw.js +14 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/market-watch-item.html +1 -1
- package/modules/hall-ui/public/market-watch-rules.html +1 -1
- package/modules/hall-ui/public/market-watch.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/money.html +1 -1
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +32 -20
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +9 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/lifecycle/home-city.js +17 -3
- package/modules/onboarding/founding-mode.js +45 -1
- package/modules/onboarding/genesis-home.js +70 -7
- package/modules/onboarding/genesis-work.js +68 -0
- package/modules/onboarding/guide-handoff.js +109 -0
- package/modules/onboarding/routes/genesis-work.js +11 -4
- package/modules/provisioning/guide-handoff.js +65 -0
- package/modules/provisioning/provisioning.js +2 -2
- package/modules/provisioning/routes/provisioning.js +4 -2
- package/modules/public-landing/public/projects.html +79 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/bongos/genesis.js +8 -1
- package/src/bongos/founding-page-gate.js +67 -0
- package/src/bongos/serve-internal.js +3 -0
- package/src/branding.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/founding_mode.mjs +84 -8
- package/tests/founding_page_gate.mjs +105 -0
- package/tests/front_door_guide_existing.mjs +1 -0
- package/tests/genesis_home.mjs +105 -1
- package/tests/genesis_work.mjs +29 -0
- package/tests/genesis_work_routes.mjs +9 -0
- package/tests/guide_handoff.mjs +322 -0
- package/tests/home_city.mjs +52 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/provision_settings_apply.mjs +2 -1
- package/tests/provisioning_settings_env.mjs +2 -2
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/onboarding/guide-handoff.js — THE GUIDE'S ANSWERS, ARRIVED (task 1005274, goal
|
|
4
|
+
// 1000106). The founder talked with the start-a-project guide before signing up; the hub
|
|
5
|
+
// stamped what came out of it on the new project (modules/provisioning/guide-handoff.js),
|
|
6
|
+
// and it reached this instance as one web.env line, mapped by src/branding.js onto the
|
|
7
|
+
// server-only founding.guideHandoff (base64url JSON { scope, markdown }).
|
|
8
|
+
//
|
|
9
|
+
// Here it lands, once, in the two places a later reader already looks:
|
|
10
|
+
// * The Genesis scope interview: each carried answer is written as ORIGIN carried_over
|
|
11
|
+
// through genesis-work's own if-empty write, so it never replaces anything the founder
|
|
12
|
+
// already typed, and the interview shows it as carried over and editable (any edit by
|
|
13
|
+
// the founder makes it theirs). Only the questions the hub's explicit map answered are
|
|
14
|
+
// written; the rest stay empty.
|
|
15
|
+
// * The hand-off file: kept in project_settings (HANDOFF_KEY) and served at the top of
|
|
16
|
+
// GET /founding/context, the read `npx bongos genesis context` prints and the project
|
|
17
|
+
// context scaffold (docs/project-context.template.md) tells every session to run first.
|
|
18
|
+
// So the founder's own AI meets it in its first session without being told where.
|
|
19
|
+
//
|
|
20
|
+
// WHEN: lazily, on the first genesis read (routes/genesis-work.js), not at boot — the
|
|
21
|
+
// founding-birth.js reasoning: by then the database is up and the founder is seated. NOT
|
|
22
|
+
// tied to genesis being switched on: a paused project still keeps its founder's words.
|
|
23
|
+
// ONCE: the hand-off file's presence is the done-mark, written LAST, so a run that fails
|
|
24
|
+
// half-way is retried and the if-empty writes make the retry harmless. No marker (a project
|
|
25
|
+
// whose founder never used the guide, or one made before this shipped): resolves without
|
|
26
|
+
// touching the database.
|
|
27
|
+
|
|
28
|
+
const HANDOFF_KEY = 'founding.guide_handoff';
|
|
29
|
+
|
|
30
|
+
// handoffMarker(founding) — PURE. founding.guideHandoff → { scope, markdown } or null.
|
|
31
|
+
function handoffMarker(founding) {
|
|
32
|
+
const raw = founding && typeof founding === 'object' ? founding.guideHandoff : null;
|
|
33
|
+
if (typeof raw !== 'string' || !raw.trim()) return null;
|
|
34
|
+
try {
|
|
35
|
+
const h = JSON.parse(Buffer.from(raw.trim(), 'base64url').toString('utf8'));
|
|
36
|
+
if (!h || typeof h.markdown !== 'string' || !h.markdown.trim()) return null;
|
|
37
|
+
const scope = h.scope && typeof h.scope === 'object' && !Array.isArray(h.scope) ? h.scope : {};
|
|
38
|
+
return { scope, markdown: h.markdown };
|
|
39
|
+
} catch (_) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function markerFrom(deps) {
|
|
45
|
+
if (Object.prototype.hasOwnProperty.call(deps, 'marker')) return deps.marker;
|
|
46
|
+
try { return handoffMarker(require('../../src/module-api').branding().founding); } catch (_) { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const settingsOf = (deps) => ({
|
|
50
|
+
get: deps.getSetting || require('../../src/module-api').projectSettings.get,
|
|
51
|
+
set: deps.setSetting || require('../../src/module-api').projectSettings.set,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// carryOver(deps) → what it did: { ran:false, reason } or { ran:true, carried:[ids], kept:[ids] }.
|
|
55
|
+
// carried questions now holding the guide's words
|
|
56
|
+
// kept questions the founder had already answered, left alone
|
|
57
|
+
async function carryOver(deps = {}) {
|
|
58
|
+
const marker = markerFrom(deps);
|
|
59
|
+
if (!marker) return { ran: false, reason: 'no_handoff' };
|
|
60
|
+
const { get, set } = settingsOf(deps);
|
|
61
|
+
const done = await get(HANDOFF_KEY);
|
|
62
|
+
if (done && done.value) return { ran: false, reason: 'already' };
|
|
63
|
+
const work = require('./genesis-work');
|
|
64
|
+
const carried = [];
|
|
65
|
+
const kept = [];
|
|
66
|
+
for (const [question, text] of Object.entries(marker.scope)) {
|
|
67
|
+
if (typeof text !== 'string' || !text.trim()) continue;
|
|
68
|
+
const r = await work.setAnswer('scope', question, { state: 'answered', text, origin: 'carried_over' }, null, deps, { ifEmpty: true });
|
|
69
|
+
if (r.ok === false) continue; // a question this core no longer asks: skipped, never invented
|
|
70
|
+
(r.changed ? carried : kept).push(question);
|
|
71
|
+
}
|
|
72
|
+
await set(HANDOFF_KEY, marker.markdown, null);
|
|
73
|
+
return { ran: true, carried, kept };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ensureHandoff(deps) — the once-per-process door the reads call. Never throws: a hand-off
|
|
77
|
+
// hiccup must never fail the read that triggered it; a failed run is retried next read.
|
|
78
|
+
let once = null;
|
|
79
|
+
function ensureHandoff(deps = {}) {
|
|
80
|
+
if (deps.fresh) return carryOver(deps).catch(() => ({ ran: false, reason: 'error' }));
|
|
81
|
+
if (once) return once;
|
|
82
|
+
const p = carryOver(deps).then((r) => r, (err) => {
|
|
83
|
+
once = null;
|
|
84
|
+
try { require('../../src/module-api').logger('onboarding').error('[onboarding] guide hand-off failed', err); } catch (_) { /* no logger */ }
|
|
85
|
+
return { ran: false, reason: 'error' };
|
|
86
|
+
});
|
|
87
|
+
once = p;
|
|
88
|
+
return p;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// readHandoff(deps) → the hand-off file's markdown, or null. No marker, no read: a project
|
|
92
|
+
// whose founder never used the guide never touches the database for it.
|
|
93
|
+
async function readHandoff(deps = {}) {
|
|
94
|
+
if (!markerFrom(deps)) return null;
|
|
95
|
+
const row = await settingsOf(deps).get(HANDOFF_KEY);
|
|
96
|
+
return row && typeof row.value === 'string' && row.value.trim() ? row.value : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// withHandoff(context, markdown) — PURE. The planning read with the hand-off file first:
|
|
100
|
+
// it is where the project started, and the founder's own answers below it are their
|
|
101
|
+
// latest word.
|
|
102
|
+
function withHandoff(context, markdown) {
|
|
103
|
+
if (!markdown) return { ...context, handoff: null };
|
|
104
|
+
const said = context.markdown ? `\n---\n\n${context.markdown}` : '';
|
|
105
|
+
return { ...context, handoff: markdown, markdown: `${markdown.trimEnd()}\n${said}` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
module.exports = { HANDOFF_KEY, handoffMarker, carryOver, ensureHandoff, readHandoff, withHandoff };
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
const api = require('../../../src/module-api');
|
|
14
14
|
const work = require('../genesis-work');
|
|
15
|
+
const handoff = require('../guide-handoff'); // the start-a-project guide's answers, carried in once (task 1005274)
|
|
15
16
|
|
|
16
17
|
const REFUSALS = {
|
|
17
18
|
unknown_stage: 404, unknown_question: 404, bad_door: 400, bad_state: 400, bad_origin: 400,
|
|
@@ -37,10 +38,16 @@ module.exports = function mountGenesisWorkRoutes(router, deps = {}) {
|
|
|
37
38
|
const stage = (req) => String(req.params.stage);
|
|
38
39
|
const q = (req) => String(req.params.question);
|
|
39
40
|
|
|
40
|
-
// The read path for every door and for AI sessions planning a goal or task.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
// The read path for every door and for AI sessions planning a goal or task. Each first
|
|
42
|
+
// lands the guide's hand-off if one is waiting (../guide-handoff.js), so the scope
|
|
43
|
+
// interview opens with the carried-over answers and the context leads with the file.
|
|
44
|
+
const arrived = (fn) => async (req) => { await handoff.ensureHandoff(deps); return fn(req); };
|
|
45
|
+
router.get('/founding/work', api.requireBuilder, handle(arrived(async () => ({ stages: await work.readAll(deps) }))));
|
|
46
|
+
router.get('/founding/context', api.requireBuilder, handle(arrived(async () => handoff.withHandoff(await work.readContext(deps), await handoff.readHandoff(deps)))));
|
|
47
|
+
router.get('/founding/work/:stage', api.requireBuilder, handle(arrived((req) => work.readStage(stage(req), deps))));
|
|
48
|
+
// The stage's document (task 1005115): its record read as one named page, editable
|
|
49
|
+
// through the writes below at any time, without reopening the stage.
|
|
50
|
+
router.get('/founding/work/:stage/document', api.requireBuilder, handle(arrived((req) => work.readDocument(stage(req), deps))));
|
|
44
51
|
router.get('/founding/work/:stage/assist', api.requireBuilder, handle(async (req) => {
|
|
45
52
|
const prompt = work.assistPrompt(stage(req));
|
|
46
53
|
return prompt ? { prompt } : { ok: false, reason: 'unknown_stage' };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/provisioning/guide-handoff.js — THE GUIDE'S ANSWERS RIDE INTO THE NEW PROJECT
|
|
4
|
+
// (task 1005274, goal 1000106). A founder who talked with the start-a-project guide
|
|
5
|
+
// (modules/front-door-guide) should never be asked the same thing twice. The creation
|
|
6
|
+
// page pre-fills itself from the plan; this file carries the rest across to the new
|
|
7
|
+
// project, whose database this process cannot reach — the birth marker's path exactly
|
|
8
|
+
// (birth.js, modules/onboarding/founding-birth.js):
|
|
9
|
+
//
|
|
10
|
+
// 1. The create request says `from_guide: true` (the page sends it only when it
|
|
11
|
+
// pre-filled from a plan). The route asks the guide, through the
|
|
12
|
+
// `front-door-guide.handoff` port, for the CALLER's own newest attached plan — never a
|
|
13
|
+
// plan named by the request, so nobody can carry someone else's words in.
|
|
14
|
+
// 2. stampGuideHandoff writes the reserved write-once settings key `guide_handoff`
|
|
15
|
+
// ({ scope, markdown }) on the row this request JUST made.
|
|
16
|
+
// 3. The companion GUIDE_HANDOFF carries it into the instance's web.env as one
|
|
17
|
+
// base64url line; src/branding.js maps it onto the server-only founding.guideHandoff,
|
|
18
|
+
// and modules/onboarding/guide-handoff.js writes it once: the scope answers as
|
|
19
|
+
// carried over (never over the founder's own) and the hand-off file.
|
|
20
|
+
//
|
|
21
|
+
// No guide, no flag, no port (an instance without the guide) or no plan: nothing is
|
|
22
|
+
// stamped and the companion goes out empty, so a visitor who never used the guide sees
|
|
23
|
+
// exactly what they saw before.
|
|
24
|
+
|
|
25
|
+
const KEY = 'guide_handoff';
|
|
26
|
+
const MAX_BYTES = 24000; // the stored JSON; handoff.js already bounds the markdown
|
|
27
|
+
|
|
28
|
+
const storedOf = (row) => (row && row.settings && typeof row.settings === 'object' && !Array.isArray(row.settings) ? row.settings : {});
|
|
29
|
+
|
|
30
|
+
// cleanHandoff(h) — PURE. The port's answer reduced to exactly { scope, markdown }, or null.
|
|
31
|
+
function cleanHandoff(h) {
|
|
32
|
+
if (!h || typeof h !== 'object' || typeof h.markdown !== 'string' || !h.markdown.trim()) return null;
|
|
33
|
+
const scope = {};
|
|
34
|
+
if (h.scope && typeof h.scope === 'object') {
|
|
35
|
+
for (const [k, v] of Object.entries(h.scope)) if (/^[a-z_]{1,40}$/.test(k) && typeof v === 'string' && v.trim()) scope[k] = v;
|
|
36
|
+
}
|
|
37
|
+
const out = { scope, markdown: h.markdown };
|
|
38
|
+
return Buffer.byteLength(JSON.stringify(out)) <= MAX_BYTES ? out : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// guideHandoffEnv(row) — PURE. The companion line's value: base64url JSON, or '' for none.
|
|
42
|
+
function guideHandoffEnv(row) {
|
|
43
|
+
const h = cleanHandoff(storedOf(row)[KEY]);
|
|
44
|
+
return h ? Buffer.from(JSON.stringify(h), 'utf8').toString('base64url') : '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// stampGuideHandoff(db, instanceId, builderId, { name, resolve }) — the create route's
|
|
48
|
+
// call, on the row it just made. Returns true when a hand-off was stamped. Write-once.
|
|
49
|
+
async function stampGuideHandoff(db, instanceId, builderId, { name = '', resolve } = {}) {
|
|
50
|
+
const port = (resolve || require('../../src/module-api').resolveOptional)('front-door-guide.handoff');
|
|
51
|
+
if (typeof port !== 'function') return false;
|
|
52
|
+
const h = cleanHandoff(await port(builderId, { name }));
|
|
53
|
+
if (!h) return false;
|
|
54
|
+
await db.query(
|
|
55
|
+
`UPDATE provisioning_instances
|
|
56
|
+
SET settings = COALESCE(settings, '{}'::jsonb) || jsonb_build_object($2::text, $3::jsonb)
|
|
57
|
+
WHERE id = $1 AND NOT (COALESCE(settings, '{}'::jsonb) ? $2::text)`,
|
|
58
|
+
[instanceId, KEY, JSON.stringify(h)],
|
|
59
|
+
);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const GUIDE_COMPANION_ENV = Object.freeze({ GUIDE_HANDOFF: guideHandoffEnv });
|
|
64
|
+
|
|
65
|
+
module.exports = { KEY, GUIDE_COMPANION_ENV, stampGuideHandoff };
|
|
@@ -25,7 +25,7 @@ const { normalizeModuleSelection, effectiveModules } = require('./starter-bundle
|
|
|
25
25
|
const { PLANET_PHYSICS_QUESTIONS, canonicalAnswer } = require('./planet-physics');
|
|
26
26
|
const { softLimitsFor } = require('./soft-limits'); const { demoView } = require('./demo'); const { isTestProject, TEST_COMPANION_ENV } = require('./test-project'); // the demo marker's owner view (task 1004420); the test mark (task 1005036)
|
|
27
27
|
const { dnsResolves, recordDnsResolved, lookupAnswers } = require('./dns-resolves'); // has the platform's lookup seen the address answer (task 1004505)
|
|
28
|
-
const { LOOK_SETTINGS_VOCAB, LOOK_DEFAULTS, LOOK_COMPANION_ENV } = require('./look'); const { BIRTH_COMPANION_ENV } = require('./birth'); // born into genesis (task 1004506)
|
|
28
|
+
const { LOOK_SETTINGS_VOCAB, LOOK_DEFAULTS, LOOK_COMPANION_ENV } = require('./look'); const { BIRTH_COMPANION_ENV } = require('./birth'); const { GUIDE_COMPANION_ENV } = require('./guide-handoff'); // born into genesis (task 1004506); the guide's hand-off (task 1005274)
|
|
29
29
|
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
// Constants (mirror the CHECK constraints in provisioning_001_tables.sql)
|
|
@@ -513,7 +513,7 @@ function settingsEnvVars(row, prefix) {
|
|
|
513
513
|
// cutoff and every unresolved review counts, which is the right answer for a
|
|
514
514
|
// project that has been strict since it stood up.
|
|
515
515
|
const SETTINGS_COMPANION_ENV = Object.freeze({
|
|
516
|
-
ARTIST_GATE_SINCE: (row) => artistGateStrictSince(row) || '', ...LOOK_COMPANION_ENV, ...BIRTH_COMPANION_ENV, ...TEST_COMPANION_ENV, // + accent, logo (look.js); birth, demo box (birth.js); test mark (test-project.js)
|
|
516
|
+
ARTIST_GATE_SINCE: (row) => artistGateStrictSince(row) || '', ...LOOK_COMPANION_ENV, ...BIRTH_COMPANION_ENV, ...TEST_COMPANION_ENV, ...GUIDE_COMPANION_ENV, // + accent, logo (look.js); birth, demo box (birth.js); test mark (test-project.js); the guide's hand-off (guide-handoff.js)
|
|
517
517
|
});
|
|
518
518
|
|
|
519
519
|
// The web.env block the runner writes at STANDUP (task 1003139): a comment that
|
|
@@ -65,7 +65,7 @@ const catalogBridge = require('../catalog-bridge');
|
|
|
65
65
|
const capacity = require('../capacity'); // the box's slot ceiling (ADR 0285, task 1003927)
|
|
66
66
|
const { callbackPage } = require('./callback-page'); const githubSetup = require('../github-setup');
|
|
67
67
|
// the LOUD body validators (task 1003504) — lifted out when this file hit the size ratchet
|
|
68
|
-
const { badProjectDetail, badModuleSelection, badProjectType, screenedOut } = require('./body-validators'); const demo = require('../demo'); const birth = require('../birth'); const testProject = require('../test-project');
|
|
68
|
+
const { badProjectDetail, badModuleSelection, badProjectType, screenedOut } = require('./body-validators'); const demo = require('../demo'); const birth = require('../birth'); const testProject = require('../test-project'); const guideHandoff = require('../guide-handoff');
|
|
69
69
|
const { failFrom } = require('../public-refusal'); // every catch: a declared refusal reaches the owner, anything else stays opaque (task 1004126)
|
|
70
70
|
const { settingsPush, failPushConflict } = require('../settings-push'); // a settings save's push + receipt, shared with routes/look.js (task 1004421)
|
|
71
71
|
const { requireProjectOwner, projectActor, isProjectOwner } = require('../route-helpers'); // the owner-or-archon gate (task 1004159, ADR 0328)
|
|
@@ -362,7 +362,7 @@ module.exports = function provisioningRoutes() {
|
|
|
362
362
|
// exactly the set the panel preselected. An explicit [] is the different
|
|
363
363
|
// answer "none of them". maxItems is a sanity ceiling well above a catalog
|
|
364
364
|
// that is three modules wide today.
|
|
365
|
-
modules: { type: 'array', itemsType: 'string', maxItems: 50 }, terms_acknowledged: { type: 'boolean' }, demo: { type: 'object' }, ship_mode: { type: 'string', enum: ['auto', 'manual'] }, genesis: { type: 'boolean' }, test: { type: 'boolean' }, // the wizard's terms tick (task 1004417, ../terms-ack.js); the demo route's { people, hours_each } (task 1004420, ../demo.js)
|
|
365
|
+
modules: { type: 'array', itemsType: 'string', maxItems: 50 }, terms_acknowledged: { type: 'boolean' }, demo: { type: 'object' }, ship_mode: { type: 'string', enum: ['auto', 'manual'] }, genesis: { type: 'boolean' }, test: { type: 'boolean' }, from_guide: { type: 'boolean' }, // from_guide: the page pre-filled from the guide's plan (task 1005274, ../guide-handoff.js); the wizard's terms tick (task 1004417, ../terms-ack.js); the demo route's { people, hours_each } (task 1004420, ../demo.js)
|
|
366
366
|
})) return;
|
|
367
367
|
const body = req.body || {};
|
|
368
368
|
const detailFault = badProjectDetail(body.detail);
|
|
@@ -485,6 +485,8 @@ module.exports = function provisioningRoutes() {
|
|
|
485
485
|
if (created && testAsk.test) await testProject.stampTestProject(pool, instance.id).catch((err) => log.error('[provisioning] test mark not stamped', err));
|
|
486
486
|
// The wizard's ship mode (task 1004629) rides beside it; a miss means the project asks at its first ship.
|
|
487
487
|
if (created) await birth.stampShipMode(pool, instance.id, body.ship_mode).catch((err) => log.error('[provisioning] ship mode not stamped', err));
|
|
488
|
+
// The guide's answers (task 1005274): the CALLER's own attached plan, never one the request names; a miss means the project opens unprefilled.
|
|
489
|
+
if (created && body.from_guide === true) await guideHandoff.stampGuideHandoff(pool, instance.id, req.builder.id, { name: slug }).catch((err) => log.error('[provisioning] guide hand-off not stamped', err));
|
|
488
490
|
const demoRow = created && demoAsk.demo ? await demo.startDemoProject(pool, provisioning, instance.id, demoAsk.demo) : null;
|
|
489
491
|
// Enqueue the provision intent (idempotent: an already-open intent is returned)
|
|
490
492
|
// — but ONLY when the instance actually needs standing up. Blindly enqueueing on
|
|
@@ -1579,6 +1579,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1579
1579
|
<h2 id="p1-h"><span class="n">01</span>Your project</h2>
|
|
1580
1580
|
<!-- auto-fill: the answers this account gave on its most recent project -->
|
|
1581
1581
|
<p class="fillOffer" id="physFillOffer" hidden><button type="button" class="linklike" id="physFromAccount">Fill these in from my last project</button></p>
|
|
1582
|
+
<!-- the guide's answers (task 1005274): shown only when this page filled itself in from the plan -->
|
|
1583
|
+
<p class="fillOffer hint" id="guideFill" hidden>Filled in from your talk with the guide. Change anything you like. <a id="guideFileLink" href="#" download="PROJECT-HANDOFF.md">Download the plan for your AI</a></p>
|
|
1582
1584
|
<!-- the demo route (task 1004420, BV2.PS09): opened in place at the top of part
|
|
1583
1585
|
01 by the small link under the heading; the rest of the page is unchanged
|
|
1584
1586
|
and marked "demo" (D9). The fit check is a copy of modules/provisioning/demo.js
|
|
@@ -5213,6 +5215,77 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5213
5215
|
});
|
|
5214
5216
|
paintTeam(state.teamShape || null);
|
|
5215
5217
|
|
|
5218
|
+
/* ── the guide's answers (task 1005274) ──
|
|
5219
|
+
A founder who talked with the start-a-project guide (modules/front-door-guide) is
|
|
5220
|
+
never asked the same thing twice. This browser holds their chat in localStorage
|
|
5221
|
+
(the landing's guide-scene.js writes it); once they are signed in, the chat moves
|
|
5222
|
+
onto their account (POST …/attach) and its plan fills in what it covers, and ONLY
|
|
5223
|
+
what is still unanswered: the kind, the sentence, the extras. The name stays the
|
|
5224
|
+
founder's (the plan has none). Every field stays the ordinary control it always
|
|
5225
|
+
was, so each answer is editable. The create then says from_guide, and the hub
|
|
5226
|
+
carries the plan's scope answers and the hand-off file into the new project
|
|
5227
|
+
(modules/provisioning/guide-handoff.js). No chat in this browser: nothing is read
|
|
5228
|
+
and nothing changes. */
|
|
5229
|
+
var GUIDE_KEY = 'cb-front-door-guide';
|
|
5230
|
+
var guideAsked = false;
|
|
5231
|
+
function guideChat() {
|
|
5232
|
+
try {
|
|
5233
|
+
if (!(window.__MODULES__ && window.__MODULES__.enabled && window.__MODULES__.enabled['front-door-guide'])) return null;
|
|
5234
|
+
var g = JSON.parse(localStorage.getItem(GUIDE_KEY) || 'null');
|
|
5235
|
+
return g && g.id && g.token ? g : null;
|
|
5236
|
+
} catch (e) { return null; }
|
|
5237
|
+
}
|
|
5238
|
+
/* applyGuidePlan(plan) — fills the unanswered fields from a plan; returns what it filled. */
|
|
5239
|
+
function applyGuidePlan(plan) {
|
|
5240
|
+
var filled = [];
|
|
5241
|
+
if (!plan || typeof plan !== 'object') return filled;
|
|
5242
|
+
if (!state.projType && plan.project_type && root.querySelector('#typeCards .tpill[data-type="' + plan.project_type + '"]')) {
|
|
5243
|
+
state.projType = plan.project_type; filled.push('type');
|
|
5244
|
+
}
|
|
5245
|
+
if (!String(state.description || '').trim() && typeof plan.summary === 'string' && plan.summary.trim()) {
|
|
5246
|
+
state.description = plan.summary.trim().slice(0, 600); filled.push('description');
|
|
5247
|
+
}
|
|
5248
|
+
if (state.modules === null && Array.isArray(plan.modules) && plan.modules.length) {
|
|
5249
|
+
state.modules = plan.modules.map(function (m) { return m && m.key; }).filter(function (k) { return typeof k === 'string' && k; });
|
|
5250
|
+
filled.push('modules');
|
|
5251
|
+
}
|
|
5252
|
+
return filled;
|
|
5253
|
+
}
|
|
5254
|
+
function paintGuideFill() {
|
|
5255
|
+
var note = $('guideFill');
|
|
5256
|
+
if (!note) return;
|
|
5257
|
+
note.hidden = !state.fromGuide;
|
|
5258
|
+
$('guideFileLink').href = API + '/front-door-guide/mine/handoff.md?name=' + encodeURIComponent(String(state.name || '').trim());
|
|
5259
|
+
}
|
|
5260
|
+
function guidePrefill() {
|
|
5261
|
+
if (guideAsked || !me) return;
|
|
5262
|
+
var g = guideChat();
|
|
5263
|
+
if (!g) return;
|
|
5264
|
+
guideAsked = true;
|
|
5265
|
+
fetch(API + '/front-door-guide/conversations/' + encodeURIComponent(g.id) + '/attach', {
|
|
5266
|
+
method: 'POST', credentials: 'same-origin',
|
|
5267
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5268
|
+
body: JSON.stringify({ token: g.token })
|
|
5269
|
+
}).catch(function () { return null; }).then(function () {
|
|
5270
|
+
return fetch(API + '/front-door-guide/mine', { credentials: 'same-origin' });
|
|
5271
|
+
}).then(function (res) {
|
|
5272
|
+
return res && res.ok ? res.json() : null;
|
|
5273
|
+
}).then(function (data) {
|
|
5274
|
+
var plan = data && data.conversation && data.conversation.plan;
|
|
5275
|
+
if (!plan) return;
|
|
5276
|
+
applyGuidePlan(plan);
|
|
5277
|
+
state.fromGuide = true;
|
|
5278
|
+
saveDraft();
|
|
5279
|
+
paintType(state.projType || null);
|
|
5280
|
+
$('f-desc').value = state.description || '';
|
|
5281
|
+
renderModules();
|
|
5282
|
+
paintSetup();
|
|
5283
|
+
paintCreate();
|
|
5284
|
+
paintGuideFill();
|
|
5285
|
+
}).catch(function () { /* the guide is a convenience: the page works without it */ });
|
|
5286
|
+
}
|
|
5287
|
+
paintGuideFill();
|
|
5288
|
+
|
|
5216
5289
|
/* ── the module picker (task 1002339, BV1.R20) ──
|
|
5217
5290
|
ONE public read gives the panel both halves it needs: `optional` is the
|
|
5218
5291
|
catalog it renders, `bundles` is what each project type preselects
|
|
@@ -6933,6 +7006,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6933
7006
|
has commits rejects as unrelated history. state.mode already tracked
|
|
6934
7007
|
greenfield|adopt for the UI — it has to reach the server too. */
|
|
6935
7008
|
onboard_mode: state.mode === 'adopt' ? 'adopt' : 'greenfield',
|
|
7009
|
+
/* the page filled itself in from the guide's plan (task 1005274): the hub
|
|
7010
|
+
carries the scope answers and the hand-off file into the new project */
|
|
7011
|
+
from_guide: state.fromGuide ? true : undefined,
|
|
6936
7012
|
/* the ship row (task 1004629): becomes the new project's own ship mode */
|
|
6937
7013
|
ship_mode: shipModeChoice()
|
|
6938
7014
|
})
|
|
@@ -6963,6 +7039,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6963
7039
|
demo_invites: state.demo && state.demo.invites && state.demo.invites.length ? state.demo.invites.slice() : undefined
|
|
6964
7040
|
}));
|
|
6965
7041
|
sessionStorage.removeItem(DRAFT_KEY);
|
|
7042
|
+
/* the guide's chat has done its job: a second project must not fill in from it */
|
|
7043
|
+
if (state.fromGuide) localStorage.removeItem(GUIDE_KEY);
|
|
6966
7044
|
} catch (e) {}
|
|
6967
7045
|
createBusy = false;
|
|
6968
7046
|
renderDone();
|
|
@@ -8568,6 +8646,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
8568
8646
|
$('f-owner').value = state.owner;
|
|
8569
8647
|
saveDraft();
|
|
8570
8648
|
}
|
|
8649
|
+
guidePrefill();
|
|
8571
8650
|
if (currentView === 'new') enter(false);
|
|
8572
8651
|
}
|
|
8573
8652
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.20",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.25.
|
|
9
|
+
"version": "1.25.20",
|
|
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.
|
|
3
|
+
"version": "1.25.20",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -11852,5 +11852,21 @@
|
|
|
11852
11852
|
"id": "1004571",
|
|
11853
11853
|
"text": "Inside a project's hall, the options menu now shows where the project's app runs (no platform yet, or Render and its progress and address), and gives the owner a one-click way to connect Render."
|
|
11854
11854
|
}
|
|
11855
|
+
],
|
|
11856
|
+
"1.25.19": [
|
|
11857
|
+
{
|
|
11858
|
+
"id": "1005115",
|
|
11859
|
+
"text": "A solo founder's Genesis now moves to the next stage by itself when a stage's work is finished, and every stage leaves a named document (the scope, the strategy...) they can come back and edit any time."
|
|
11860
|
+
}
|
|
11861
|
+
],
|
|
11862
|
+
"1.25.20": [
|
|
11863
|
+
{
|
|
11864
|
+
"id": "1005112",
|
|
11865
|
+
"text": "While a new project is being set up, the menu now shows only the rooms that are open, a room that just opened is highlighted, a closed room can't be reached by its address, and the later setup stages say what finishing them ea"
|
|
11866
|
+
},
|
|
11867
|
+
{
|
|
11868
|
+
"id": "1005274",
|
|
11869
|
+
"text": "A founder who planned their project with the guide now finds the project form and the first Genesis question already filled in, plus a plan file for their own AI."
|
|
11870
|
+
}
|
|
11855
11871
|
]
|
|
11856
11872
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Usage:
|
|
13
13
|
// npx bongos genesis show [stage] every stage, or one, with its answers
|
|
14
|
+
// npx bongos genesis document <stage> the stage's document (task 1005115): what it left behind, editable any time
|
|
14
15
|
// npx bongos genesis context what the founder said, as markdown (for planning sessions)
|
|
15
16
|
// npx bongos genesis assist <stage> the brief for the founder's AI session (the assisted door)
|
|
16
17
|
// npx bongos genesis answer <stage> <question> "<words>" [--origin founder|assisted]
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
const fs = require('node:fs');
|
|
29
30
|
const { apiCall, requireSession, errCode, cliExit } = require('./cli-lib');
|
|
30
31
|
|
|
31
|
-
const USAGE = 'usage: npx bongos genesis show [stage] | context | assist <stage> | answer <stage> <question> "<words>" [--origin founder|assisted] | answer <stage> <question> --not-sure|--skip ["why"] | clear <stage> <question> | history <stage> <question> | door <stage> guided|open|assisted | page <stage> "<words>"|--file <path>|--remove | link <stage> <url>|--remove';
|
|
32
|
+
const USAGE = 'usage: npx bongos genesis show [stage] | context | document <stage> | assist <stage> | answer <stage> <question> "<words>" [--origin founder|assisted] | answer <stage> <question> --not-sure|--skip ["why"] | clear <stage> <question> | history <stage> <question> | door <stage> guided|open|assisted | page <stage> "<words>"|--file <path>|--remove | link <stage> <url>|--remove';
|
|
32
33
|
|
|
33
34
|
const STATE_LABEL = { answered: '', not_sure: '[not sure yet] ', skipped: '[skipped] ' };
|
|
34
35
|
const ORIGIN_LABEL = { assisted: ' (with AI help)', carried_over: ' (carried over — confirm or edit)' };
|
|
@@ -130,6 +131,12 @@ async function main(argv) {
|
|
|
130
131
|
return 0;
|
|
131
132
|
}
|
|
132
133
|
if (!stage) { console.error(USAGE); return 2; }
|
|
134
|
+
if (cmd === 'document') {
|
|
135
|
+
const d = await call('GET', `/founding/work/${enc(stage)}/document`, null, 'read the document');
|
|
136
|
+
if (!d) return 1;
|
|
137
|
+
console.log(d.document.markdown || `${d.document.title}: nothing written yet.`);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
133
140
|
if (cmd === 'assist') {
|
|
134
141
|
const d = await call('GET', `/founding/work/${enc(stage)}/assist`, null, 'build the brief');
|
|
135
142
|
if (!d) return 1;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bongos/founding-page-gate.js — A SLEEPING ROOM IS NOT REACHABLE BY URL (task 1005112).
|
|
4
|
+
//
|
|
5
|
+
// While a project is being founded, the hall's sidebar holds only the rooms its genesis
|
|
6
|
+
// stages have opened. Hiding a room and refusing its page are ONE change: a room nobody
|
|
7
|
+
// can see but anyone can type the address of would be a wall the product only claims to
|
|
8
|
+
// have. So this is the page half. It asks the server's own answer, the onboarding port's
|
|
9
|
+
// foundingModeFor() (the same read /me carries as founding_mode.asleep, keyed by the
|
|
10
|
+
// sidebar's room ids), and never a browser's opinion (ADR 0016).
|
|
11
|
+
//
|
|
12
|
+
// A sleeping room's page answers with a redirect home carrying ?asleep=<room>&opens=<stage>,
|
|
13
|
+
// where the shell says what the room is and which stage opens it, instead of a dead end.
|
|
14
|
+
//
|
|
15
|
+
// What it never touches: a project that is not founding (foundingModeFor answers null,
|
|
16
|
+
// and the page is served exactly as before), a room that is awake, the Primer and the
|
|
17
|
+
// Board Room (never asleep), and every page that is not one of the rooms below. An
|
|
18
|
+
// unreadable founding state is null too, so a failure opens the page, never shuts it:
|
|
19
|
+
// founding-mode.js fails closed to OFF for the same reason (a hall wrongly asleep would
|
|
20
|
+
// hijack it). Rooms are keyed by the sidebar id, and each room's page slug IS that id.
|
|
21
|
+
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const seams = require('../module-seams');
|
|
24
|
+
|
|
25
|
+
// The rooms a genesis stage can put to sleep (founding-mode.js WAKES + WAKES_WHEN_ALIVE),
|
|
26
|
+
// by page slug. tests/founding_page_gate.mjs holds this list to that map.
|
|
27
|
+
const ROOM_PAGE_RE = /^\/builders\/(work|goals|goal-map|collab|roster)(?:\.html)?\/?$/;
|
|
28
|
+
|
|
29
|
+
// asleepRoomFor(room, founding) — PURE. The room id when that room sleeps under this
|
|
30
|
+
// founding answer, else null.
|
|
31
|
+
function asleepRoomFor(room, founding) {
|
|
32
|
+
if (!founding || founding.active !== true || !founding.asleep || typeof founding.asleep !== 'object') return null;
|
|
33
|
+
return Object.prototype.hasOwnProperty.call(founding.asleep, room) ? room : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// readFounding() — the server's answer, or null on any failure (fail open: OFF).
|
|
37
|
+
async function readFounding(resolve = seams.resolveOptional) {
|
|
38
|
+
try {
|
|
39
|
+
const onboarding = resolve('onboarding');
|
|
40
|
+
if (!onboarding || typeof onboarding.foundingModeFor !== 'function') return null;
|
|
41
|
+
return await onboarding.foundingModeFor();
|
|
42
|
+
} catch (_) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// foundingPageGate({ skip, isBuildersHost, read }) — the Express middleware. `skip(req)`
|
|
48
|
+
// is the caller's module-surface test; `read` is injectable for tests. The founding read
|
|
49
|
+
// is made only for one of the five room pages, so every other request pays nothing.
|
|
50
|
+
function foundingPageGate({ skip = () => false, isBuildersHost = () => false, read = readFounding } = {}) {
|
|
51
|
+
return async function gate(req, res, next) {
|
|
52
|
+
if (skip(req) || !String(req.path || '').startsWith('/builders')) return next();
|
|
53
|
+
const m = ROOM_PAGE_RE.exec(path.posix.normalize(req.path));
|
|
54
|
+
if (!m) return next();
|
|
55
|
+
let founding = null;
|
|
56
|
+
try { founding = await read(); } catch (_) { founding = null; } // fail open: OFF
|
|
57
|
+
const room = asleepRoomFor(m[1], founding);
|
|
58
|
+
if (!room) return next();
|
|
59
|
+
const opens = String(founding.asleep[room] || '');
|
|
60
|
+
res.set('Cache-Control', 'no-store');
|
|
61
|
+
return res.redirect(302, `${isBuildersHost(req) ? '' : '/builders'}/?asleep=${encodeURIComponent(room)}`
|
|
62
|
+
+ (/^[a-z]{1,20}$/.test(opens) ? `&opens=${opens}` : ''));
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// One export: the pure parts are proven through it (tests/founding_page_gate.mjs).
|
|
67
|
+
module.exports = { foundingPageGate };
|
|
@@ -50,6 +50,7 @@ const { linkPreviewTags } = require('../link-preview');
|
|
|
50
50
|
const { prelaunchGate } = require('./prelaunch-gate');
|
|
51
51
|
const { platformVisibilityGate } = require('./platform-visibility-gate');
|
|
52
52
|
const { schemaPending } = require('./schema-lag');
|
|
53
|
+
const { foundingPageGate } = require('./founding-page-gate');
|
|
53
54
|
const { clientModules, isModuleEnabled } = require('../modules');
|
|
54
55
|
const { hallWidgetScripts, moduleWebSurfaces } = require('../module-loader/loader');
|
|
55
56
|
const { resolveCoreRoot, resolveDocsRoot, resolveInstanceRoot, resolveEnv } = require('../instance-config');
|
|
@@ -1353,6 +1354,8 @@ function mountInternalSurfaces(app) {
|
|
|
1353
1354
|
}
|
|
1354
1355
|
next();
|
|
1355
1356
|
});
|
|
1357
|
+
// task 1005112: a founding project's sleeping room is refused by URL, after sign-in.
|
|
1358
|
+
app.use(foundingPageGate({ skip: isModuleSurfaceHost, isBuildersHost }));
|
|
1356
1359
|
|
|
1357
1360
|
// task 1965: the read-only Task Board (/board, task 751) was consolidated into
|
|
1358
1361
|
// the Work page (/work) — the hall keeps one task-listing surface. 301 old
|
package/src/branding.js
CHANGED
|
@@ -112,6 +112,10 @@ const ENV_OVERRIDES = [
|
|
|
112
112
|
// The ship mode the founder picked in the wizard (task 1004629), empty when none was.
|
|
113
113
|
// resolveShipMode reads it only while project_settings['ship.mode'] is unset.
|
|
114
114
|
['SHIP_MODE', ['founding', 'shipMode']],
|
|
115
|
+
// The start-a-project guide's hand-off (task 1005274, modules/provisioning/guide-handoff.js): base64url
|
|
116
|
+
// JSON { scope, markdown }, empty when the founder never used the guide. modules/onboarding/guide-handoff.js
|
|
117
|
+
// writes it once: the scope answers as carried over, and the hand-off file. SERVER-ONLY, like the birth.
|
|
118
|
+
['GUIDE_HANDOFF', ['founding', 'guideHandoff']],
|
|
115
119
|
];
|
|
116
120
|
|
|
117
121
|
const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.25.
|
|
84
|
+
const CORE_VERSION = '1.25.20'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|