@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.
- package/.bongos-core.json +128 -58
- package/.claude/skills/goal-create/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +5 -4
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +14 -0
- package/clients/bongos-client/index.cjs +14 -0
- package/clients/bongos-client/index.d.ts +22 -2
- package/clients/bongos-client/index.mjs +14 -0
- package/docs/adr/0406-every-project-gets-a-sandbox-that-follows-main-and-live-moves-only-when-a-person-promotes.md +8 -0
- package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +1 -0
- package/docs/api/openapi.json +415 -3
- package/docs/api-reference.md +13 -2
- package/docs/copy-inventory.md +120 -109
- package/docs/copy-registry.json +275 -165
- package/docs/module-api-changelog.md +4 -2
- package/docs/page-inventory.json +2 -0
- package/docs/page-readings.json +107 -107
- package/docs/project-context.template.md +1 -0
- package/modules/government/decision-rules.js +1 -0
- package/modules/hall-ui/public/dom-utils.js +5 -0
- package/modules/hall-ui/public/genesis-home.js +23 -8
- package/modules/hall-ui/public/setup-gate.js +1 -1
- package/modules/hall-ui/public/style.css +21 -1
- package/modules/lifecycle/first-read-db.js +152 -0
- package/modules/lifecycle/first-read.js +265 -0
- package/modules/lifecycle/kickoff-checklist.js +29 -6
- package/modules/lifecycle/kickoff-seed.js +27 -4
- package/modules/lifecycle/migrations/lifecycle_022_repo_maps.sql +44 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/first-read.js +79 -0
- package/modules/onboarding/genesis-home.js +11 -2
- package/modules/onboarding/genesis-work.js +28 -5
- package/modules/provisioning/pollers/app-liveness.js +23 -1
- package/modules/provisioning/render-sandbox.js +113 -0
- package/modules/provisioning/render-standup.js +3 -1
- package/modules/provisioning/routes/provisioning.js +4 -1
- package/modules/provisioning/routes/render-standup.js +49 -1
- package/modules/public-landing/public/assets/guide-scene.css +245 -0
- package/modules/public-landing/public/assets/guide-scene.js +566 -0
- package/modules/public-landing/public/index.html +62 -2
- package/modules/public-landing/public/projects.html +101 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +23 -1
- package/scripts/bongos/first-read.js +116 -0
- package/scripts/bongos/genesis.js +2 -0
- package/scripts/bongos/government-walk.js +4 -4
- package/scripts/bongos/provision-render-sandbox.js +171 -0
- package/scripts/bongos/provision-render.js +37 -2
- package/scripts/bongos/provision-teardown.js +1 -1
- package/scripts/bongos/render-api.js +33 -3
- package/scripts/bongos/render-payload.js +39 -1
- package/scripts/gds/first-read.js +7 -0
- package/scripts/gds/provision-render-sandbox.js +6 -0
- package/src/module-api.js +1 -1
- package/tests/first_read.mjs +303 -0
- package/tests/genesis_home.mjs +70 -2
- package/tests/genesis_work.mjs +49 -0
- package/tests/government_act_alone_buildings.mjs +2 -0
- package/tests/landing_guide_scene.mjs +504 -0
- package/tests/provisioning_app_status.mjs +3 -2
- package/tests/provisioning_render_route.mjs +1 -1
- package/tests/provisioning_teardown_intent.mjs +50 -0
- package/tests/render_api.mjs +5 -2
- package/tests/render_sandbox.mjs +447 -0
|
@@ -94,6 +94,7 @@ const AREAS = Object.freeze([
|
|
|
94
94
|
'POST /goals/reorder',
|
|
95
95
|
'POST /work-categories',
|
|
96
96
|
'PUT /versions/focus',
|
|
97
|
+
'POST /first-read/maps/:id/accept', // task 1005280: accepting a repository map files goals
|
|
97
98
|
]) }),
|
|
98
99
|
Object.freeze({ key: 'automation', name: 'Automation', gate: 'autonomy.fence.manage',
|
|
99
100
|
today: 'whoever may switch unattended building on and off', acts: Object.freeze([
|
|
@@ -529,6 +529,10 @@
|
|
|
529
529
|
} catch (err) { return null; }
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
+
// The hall's one completion tick (24x24, stroked, currentColor): the setup flow's step
|
|
533
|
+
// marks and the Genesis checklist both draw it, so a done thing looks done the same way.
|
|
534
|
+
const CHECK_SVG = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
535
|
+
|
|
532
536
|
const OTB = {
|
|
533
537
|
escapeHtml, sleep, $, fmtNum, num, fmtInt, fmtCompact, fmtDate, rankLabel, toast, moduleOn, disciplines,
|
|
534
538
|
plainMessage, toastKind, TOAST_MS, // task 1005086 — one toast, a duration per kind, no server address
|
|
@@ -542,6 +546,7 @@
|
|
|
542
546
|
splitCriterion,
|
|
543
547
|
categoryChip, // task 1003276 — shared by the Goals page, the Tasks board and the task page
|
|
544
548
|
hubOrigin, // task 1003485 — the hub account page's origin, for Settings and the profile nudge
|
|
549
|
+
CHECK_SVG, // task 1005110 — the one tick, setup-gate.js and genesis-home.js
|
|
545
550
|
};
|
|
546
551
|
if (typeof window !== 'undefined') window.OTB = OTB;
|
|
547
552
|
// Node: the hall libs that also load under require() (hall-kit.js, task-lib.js, …)
|
|
@@ -190,9 +190,10 @@
|
|
|
190
190
|
|
|
191
191
|
// task 1005109: Copy prompt on each task still to do, so the founder pastes it into
|
|
192
192
|
// Claude without leaving Genesis. The Tasks board's own button (work.js), and only that
|
|
193
|
-
// one verb: the owner declined a Claim button here.
|
|
193
|
+
// one verb: the owner declined a Claim button here. Only an OPEN task offers it: a done
|
|
194
|
+
// one has nothing left, and a locked one cannot be claimed yet (task 1005110).
|
|
194
195
|
function copyButtonHtml(t) {
|
|
195
|
-
if (t.
|
|
196
|
+
if (t.state !== 'open') return '';
|
|
196
197
|
return '<button class="task__copy" type="button" data-genesis-copy="' + esc(t.id) + '"'
|
|
197
198
|
+ ' title="Copy the task prompt to your clipboard" aria-label="Copy prompt for task ' + esc(t.id) + '">'
|
|
198
199
|
+ '<span class="task__copy-icon" aria-hidden="true">📋</span></button>';
|
|
@@ -212,16 +213,30 @@
|
|
|
212
213
|
return lib.copyTaskPrompt(t, btn, toast);
|
|
213
214
|
}
|
|
214
215
|
|
|
216
|
+
// task 1005110: one checklist row in one of three states, as the server decided them
|
|
217
|
+
// (shapeGenesis taskState): done (a filled circle with the tick), open (an empty
|
|
218
|
+
// circle, its status and Copy prompt) or locked (a dashed circle and what it waits on,
|
|
219
|
+
// no Copy prompt: the claim would be refused).
|
|
220
|
+
function taskRowHtml(t) {
|
|
221
|
+
var state = t.state === 'done' || t.state === 'locked' ? t.state : 'open';
|
|
222
|
+
var check = window.OTB.CHECK_SVG || '';
|
|
223
|
+
var mark = '<span class="genesis__mark" aria-hidden="true">' + (state === 'done' ? check : '') + '</span>';
|
|
224
|
+
var said = { done: 'Done', open: 'To do', locked: 'Locked' }[state];
|
|
225
|
+
var after = (t.waits_on || []).map(function (w) { return w.title; }).filter(Boolean);
|
|
226
|
+
var tail = state === 'locked'
|
|
227
|
+
? '<span class="genesis__task-wait">' + esc(after.length ? 'after ' + after.join(', ') : 'after an earlier task') + '</span>'
|
|
228
|
+
: window.OTB.statusPill(t.status, { small: true, extraClass: 'genesis__task-st' }) + copyButtonHtml(t);
|
|
229
|
+
return '<li class="genesis__task is-' + state + '">' + mark
|
|
230
|
+
+ '<span class="sr-only">' + said + ': </span>'
|
|
231
|
+
+ '<a class="genesis__task-t" href="/task/' + encodeURIComponent(t.id) + '">' + esc(t.title) + '</a>'
|
|
232
|
+
+ tail + '</li>';
|
|
233
|
+
}
|
|
234
|
+
|
|
215
235
|
function nowHtml(g, viewerId) {
|
|
216
236
|
var s = g.stages.find(function (x) { return x.state === 'now'; });
|
|
217
237
|
if (!s) return '';
|
|
218
238
|
var tasks = s.tasks.length
|
|
219
|
-
? '<ul class="genesis__tasks">' + s.tasks.map(
|
|
220
|
-
return '<li class="genesis__task' + (t.finished ? ' is-finished' : '') + '">'
|
|
221
|
-
+ '<a class="genesis__task-t" href="/task/' + encodeURIComponent(t.id) + '">' + esc(t.title) + '</a>'
|
|
222
|
-
+ window.OTB.statusPill(t.status, { small: true, extraClass: 'genesis__task-st' })
|
|
223
|
-
+ copyButtonHtml(t) + '</li>';
|
|
224
|
-
}).join('') + '</ul>'
|
|
239
|
+
? '<ul class="genesis__tasks">' + s.tasks.map(taskRowHtml).join('') + '</ul>'
|
|
225
240
|
: '<p class="genesis__note">No tasks for this stage yet.</p>';
|
|
226
241
|
return '<section class="genesis__now" aria-labelledby="genesis-now-h">'
|
|
227
242
|
+ '<h3 id="genesis-now-h" class="genesis__now-h">This stage’s work · ' + esc(s.label) + '</h3>'
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
function esc(s) { return window.OTB.escapeHtml(s); }
|
|
22
22
|
|
|
23
|
-
var CHECK =
|
|
23
|
+
var CHECK = window.OTB.CHECK_SVG; // the hall's one tick (dom-utils.js), shared with the Genesis checklist
|
|
24
24
|
|
|
25
25
|
function stepDetail(step) {
|
|
26
26
|
if (step.id === 'cli_setup') {
|
|
@@ -1054,7 +1054,27 @@ html[data-theme="dark"] .gplate__light { display: none; }
|
|
|
1054
1054
|
.genesis__stage.is-later { opacity: 0.6; }
|
|
1055
1055
|
.genesis__now-h { margin: 0 0 4px; font-size: 15px; font-weight: 500; }
|
|
1056
1056
|
.genesis__task-t { flex: 1; min-width: 0; padding: 3px 0; }
|
|
1057
|
-
|
|
1057
|
+
/* task 1005110: the stage's work reads as a checklist. Done is a filled circle with the
|
|
1058
|
+
hall's tick (the setup flow's mark), open an empty circle, locked a dashed one with
|
|
1059
|
+
what it waits on. Never struck through: a finished task was earned, not cancelled. */
|
|
1060
|
+
.genesis__mark {
|
|
1061
|
+
flex: none;
|
|
1062
|
+
display: inline-flex;
|
|
1063
|
+
align-items: center;
|
|
1064
|
+
justify-content: center;
|
|
1065
|
+
width: 24px;
|
|
1066
|
+
height: 24px;
|
|
1067
|
+
border-radius: var(--ds-radius-pill); /* round on purpose: the owner asked for a check filling a circle */
|
|
1068
|
+
border: 1.5px solid var(--rule);
|
|
1069
|
+
color: var(--ok);
|
|
1070
|
+
}
|
|
1071
|
+
.genesis__mark svg { width: 14px; height: 14px; }
|
|
1072
|
+
.genesis__task.is-done .genesis__mark { border-color: transparent; background: var(--ok-weak); }
|
|
1073
|
+
.genesis__task.is-done .genesis__task-t { color: var(--ink-soft); }
|
|
1074
|
+
.genesis__task.is-open .genesis__mark { border-color: var(--accent); }
|
|
1075
|
+
.genesis__task.is-locked .genesis__mark { border-style: dashed; }
|
|
1076
|
+
.genesis__task.is-locked .genesis__task-t { color: var(--ink-faint); }
|
|
1077
|
+
.genesis__task-wait { flex: none; max-width: 40%; font-size: var(--ds-text-xs); color: var(--ink-soft); text-align: right; }
|
|
1058
1078
|
.genesis__alive { display: grid; justify-items: center; text-align: center; gap: 10px; padding: 12px 0; }
|
|
1059
1079
|
.genesis__alive-plates { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
|
|
1060
1080
|
.genesis__alive-h { margin: 6px 0 0; font-size: 28px; font-weight: 400; }
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// modules/lifecycle/first-read-db.js — the store behind the first read-through (task
|
|
2
|
+
// 1005280): keep a submitted repository map, show it for review with what is already
|
|
3
|
+
// filed, and file the owner's accepted items as DRAFT goals and tasks.
|
|
4
|
+
//
|
|
5
|
+
// The rules live in first-read.js (pure); this file only runs them against Postgres.
|
|
6
|
+
// Two properties an edit must keep:
|
|
7
|
+
// - SUBMIT FILES NOTHING. A map is stored `pending`; only acceptMap creates goals or
|
|
8
|
+
// tasks, and only for the refs the owner named (or 'all').
|
|
9
|
+
// - A RE-RUN NEVER DUPLICATES. Every filed ref is a row in lifecycle_repo_map_filed
|
|
10
|
+
// (PRIMARY KEY), read under an advisory lock inside the accept transaction, so two
|
|
11
|
+
// accepts — or a second map naming the same item — file it once.
|
|
12
|
+
// Nothing here calls an AI: the map arrives already written by the owner's own session.
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const api = require('../../src/module-api');
|
|
17
|
+
const db = require('./db');
|
|
18
|
+
const fr = require('./first-read');
|
|
19
|
+
const { FIRST_VERSION_SQL } = require('./kickoff-seed');
|
|
20
|
+
|
|
21
|
+
const TASK_SOURCE = 'repo-map';
|
|
22
|
+
const ACCEPT_LOCK = "pg_advisory_xact_lock(hashtext('first-read/accept'))";
|
|
23
|
+
|
|
24
|
+
const poolOf = (deps) => deps.pool || api.pool;
|
|
25
|
+
|
|
26
|
+
// submitMap({ raw, builderId }) → { ok: true, map_id, items } | { ok: false, errors }
|
|
27
|
+
// Validates, then stores the NORMALISED map (unknown sections kept). Files nothing.
|
|
28
|
+
async function submitMap({ raw, builderId = null }, deps = {}) {
|
|
29
|
+
const v = fr.validateRepoMap(raw);
|
|
30
|
+
if (!v.ok) return v;
|
|
31
|
+
const { rows: [row] } = await poolOf(deps).query(
|
|
32
|
+
`INSERT INTO lifecycle_repo_maps (schema, map, submitted_by) VALUES ($1, $2::jsonb, $3) RETURNING id, status, submitted_at`,
|
|
33
|
+
[fr.SCHEMA, JSON.stringify(v.map), builderId],
|
|
34
|
+
);
|
|
35
|
+
return { ok: true, map_id: String(row.id), status: row.status, items: v.items };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// filedRefs(refs) → Map(ref → { kind, entity_id }) for the refs already filed.
|
|
39
|
+
async function filedRefs(refs, exec) {
|
|
40
|
+
if (!refs.length) return new Map();
|
|
41
|
+
const { rows } = await exec.query(
|
|
42
|
+
'SELECT ref, kind, entity_id FROM lifecycle_repo_map_filed WHERE ref = ANY($1::text[])', [refs],
|
|
43
|
+
);
|
|
44
|
+
return new Map(rows.map((r) => [r.ref, { kind: r.kind, entity_id: String(r.entity_id) }]));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// reviewMap(id | 'latest') → { map_id, status, summary, items: [{ ...item, filed }], extra } | null
|
|
48
|
+
// What the owner reads before deciding: every item with whether it is already in Bongos.
|
|
49
|
+
async function reviewMap(id, deps = {}) {
|
|
50
|
+
const pool = poolOf(deps);
|
|
51
|
+
const { rows: [row] } = id === 'latest'
|
|
52
|
+
? await pool.query('SELECT id, status, map, submitted_at FROM lifecycle_repo_maps ORDER BY submitted_at DESC, id DESC LIMIT 1')
|
|
53
|
+
: await pool.query('SELECT id, status, map, submitted_at FROM lifecycle_repo_maps WHERE id = $1', [id]);
|
|
54
|
+
if (!row) return null;
|
|
55
|
+
const v = fr.validateRepoMap(row.map);
|
|
56
|
+
const items = v.ok ? v.items : [];
|
|
57
|
+
const filed = await filedRefs(items.map((i) => i.ref), pool);
|
|
58
|
+
const extra = {};
|
|
59
|
+
for (const [k, val] of Object.entries(row.map || {})) if (!['schema', 'summary', 'goals', 'next'].includes(k)) extra[k] = val;
|
|
60
|
+
return {
|
|
61
|
+
map_id: String(row.id), status: row.status, submitted_at: row.submitted_at,
|
|
62
|
+
summary: (row.map && row.map.summary) || '',
|
|
63
|
+
items: items.map((i) => ({ ...i, filed: filed.has(i.ref) ? filed.get(i.ref) : null })),
|
|
64
|
+
extra,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// acceptMap({ mapId, select, builderId }) → { ok, filed: { goals, tasks }, skipped } | { ok: false, code, status, ... }
|
|
69
|
+
// ONE transaction: lock the map, lock the accept path, plan against what is already
|
|
70
|
+
// filed, create the drafts, remember their refs, mark the map accepted. A refusal files
|
|
71
|
+
// nothing. Goals are created open; tasks at `backlog` (drafts nobody can claim yet).
|
|
72
|
+
async function acceptMap({ mapId, select, builderId = null }, deps = {}) {
|
|
73
|
+
if (select !== 'all' && (!Array.isArray(select) || select.length === 0)) {
|
|
74
|
+
return { ok: false, code: 'nothing_selected', status: 400 };
|
|
75
|
+
}
|
|
76
|
+
const createGoal = deps.createGoal || ((row, d) => db.createGoal(row, d));
|
|
77
|
+
const createTask = deps.createTask || ((row, d) => db.createTask(row, d));
|
|
78
|
+
const client = await poolOf(deps).connect();
|
|
79
|
+
try {
|
|
80
|
+
await client.query('BEGIN');
|
|
81
|
+
const { rows: [row] } = await client.query('SELECT id, status, map FROM lifecycle_repo_maps WHERE id = $1 FOR UPDATE', [mapId]);
|
|
82
|
+
if (!row) { await client.query('ROLLBACK'); return { ok: false, code: 'not_found', status: 404 }; }
|
|
83
|
+
if (row.status === 'rejected') { await client.query('ROLLBACK'); return { ok: false, code: 'map_rejected', status: 409 }; }
|
|
84
|
+
await client.query(`SELECT ${ACCEPT_LOCK}`);
|
|
85
|
+
const v = fr.validateRepoMap(row.map);
|
|
86
|
+
if (!v.ok) { await client.query('ROLLBACK'); return { ok: false, code: 'invalid_map', status: 409, errors: v.errors }; }
|
|
87
|
+
const goalRefs = [...new Set([...v.items.map((i) => i.ref), ...v.items.map((i) => i.goal_ref).filter(Boolean)])];
|
|
88
|
+
const already = await filedRefs(goalRefs, client);
|
|
89
|
+
const plan = fr.planAcceptance({ items: v.items, mapId: String(row.id), select, filedRefs: new Set(already.keys()) });
|
|
90
|
+
if (plan.unknown.length) { await client.query('ROLLBACK'); return { ok: false, code: 'unknown_items', status: 400, unknown: plan.unknown }; }
|
|
91
|
+
|
|
92
|
+
let versionId = null;
|
|
93
|
+
if (plan.goals.length || plan.tasks.length) {
|
|
94
|
+
const { rows: versions } = await client.query(FIRST_VERSION_SQL);
|
|
95
|
+
if (!versions.length) { await client.query('ROLLBACK'); return { ok: false, code: 'no_version', status: 409 }; }
|
|
96
|
+
versionId = versions[0].id;
|
|
97
|
+
}
|
|
98
|
+
// The filed refs are written in ONE statement after the creates, not one per item.
|
|
99
|
+
const remembered = [];
|
|
100
|
+
const remember = (ref, kind, id) => { remembered.push([ref, kind, String(id)]); };
|
|
101
|
+
const goalIds = new Map([...already].filter(([, f]) => f.kind === 'goal').map(([ref, f]) => [ref, f.entity_id]));
|
|
102
|
+
const filed = { goals: [], tasks: [] };
|
|
103
|
+
for (const g of plan.goals) {
|
|
104
|
+
const goal = await createGoal({ versionId, title: g.title, description: g.description, createdBy: builderId }, { client });
|
|
105
|
+
goalIds.set(g.ref, String(goal.id));
|
|
106
|
+
remember(g.ref, 'goal', goal.id);
|
|
107
|
+
filed.goals.push({ ref: g.ref, id: String(goal.id), title: g.title });
|
|
108
|
+
}
|
|
109
|
+
for (const t of plan.tasks) {
|
|
110
|
+
const task = await createTask({
|
|
111
|
+
versionId, title: t.title, description: t.description, status: 'backlog', kind: t.kind,
|
|
112
|
+
discipline: 'artisan', source: TASK_SOURCE, sourceRef: t.ref, goalId: goalIds.get(t.goalRef), createdBy: builderId,
|
|
113
|
+
}, { client });
|
|
114
|
+
remember(t.ref, 'task', task.id);
|
|
115
|
+
filed.tasks.push({ ref: t.ref, id: String(task.id), title: t.title, goal_id: goalIds.get(t.goalRef) });
|
|
116
|
+
}
|
|
117
|
+
if (remembered.length) {
|
|
118
|
+
await client.query(
|
|
119
|
+
`INSERT INTO lifecycle_repo_map_filed (ref, kind, entity_id, map_id, filed_by)
|
|
120
|
+
SELECT r, k, e, $4, $5 FROM unnest($1::text[], $2::text[], $3::bigint[]) AS u(r, k, e)`,
|
|
121
|
+
[remembered.map((x) => x[0]), remembered.map((x) => x[1]), remembered.map((x) => x[2]), row.id, builderId],
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
await client.query(
|
|
125
|
+
`UPDATE lifecycle_repo_maps SET status = 'accepted', reviewed_by = $2, reviewed_at = now() WHERE id = $1`,
|
|
126
|
+
[row.id, builderId],
|
|
127
|
+
);
|
|
128
|
+
await client.query('COMMIT');
|
|
129
|
+
return { ok: true, map_id: String(row.id), filed, skipped: plan.skipped };
|
|
130
|
+
} catch (e) {
|
|
131
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
132
|
+
throw e;
|
|
133
|
+
} finally {
|
|
134
|
+
client.release();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// rejectMap({ mapId, builderId }) → { ok } | { ok: false, code, status }. Files nothing.
|
|
139
|
+
// Only a pending map can be rejected: an accepted one already has drafts the owner said yes to.
|
|
140
|
+
async function rejectMap({ mapId, builderId = null }, deps = {}) {
|
|
141
|
+
const { rows } = await poolOf(deps).query(
|
|
142
|
+
`UPDATE lifecycle_repo_maps SET status = 'rejected', reviewed_by = $2, reviewed_at = now()
|
|
143
|
+
WHERE id = $1 AND status = 'pending' RETURNING id`,
|
|
144
|
+
[mapId, builderId],
|
|
145
|
+
);
|
|
146
|
+
if (rows.length) return { ok: true, map_id: String(rows[0].id) };
|
|
147
|
+
const { rows: [row] } = await poolOf(deps).query('SELECT status FROM lifecycle_repo_maps WHERE id = $1', [mapId]);
|
|
148
|
+
if (!row) return { ok: false, code: 'not_found', status: 404 };
|
|
149
|
+
return { ok: false, code: 'not_pending', status: 409, current: row.status };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { TASK_SOURCE, submitMap, reviewMap, acceptMap, rejectMap };
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// modules/lifecycle/first-read.js — the first run-through of a project that arrives with
|
|
2
|
+
// code: the prompt the owner's OWN AI session runs, the repository map it writes back,
|
|
3
|
+
// the map's validation, and the plan for filing the owner's accepted items (task 1005280,
|
|
4
|
+
// goal 1000106 "Project creation").
|
|
5
|
+
//
|
|
6
|
+
// THE RULE THIS SERVES. The platform never reads a person's repository on its own AI
|
|
7
|
+
// bill. So Bongos does not read it at all: it hands the owner a prompt, their own AI
|
|
8
|
+
// session reads the repository on their own subscription, on their own computer, and
|
|
9
|
+
// sends back a MAP — goals already in progress, the tasks under each, and what needs
|
|
10
|
+
// doing next given errors or mishaps found in the code, each with a short reason and
|
|
11
|
+
// where it was seen. Nothing here calls an AI, and the map's length caps keep it a
|
|
12
|
+
// summary, never a copy of the code.
|
|
13
|
+
//
|
|
14
|
+
// NOTHING IS FILED WITHOUT THE OWNER'S YES. A submitted map waits as `pending`. Only an
|
|
15
|
+
// explicit accept (all, or named items) files anything, and only as drafts: goals open
|
|
16
|
+
// with their tasks in `backlog`, so nothing is claimable until someone promotes it.
|
|
17
|
+
//
|
|
18
|
+
// RE-RUNS DO NOT DUPLICATE. Each item's `ref` is derived from its title (never its
|
|
19
|
+
// position), and every filed ref is remembered (lifecycle_repo_map_filed), so a second
|
|
20
|
+
// map that names the same goal or task again plans to file nothing for it.
|
|
21
|
+
//
|
|
22
|
+
// ROOM TO GROW. Unknown top-level keys are kept, not refused — task 1005281 adds a
|
|
23
|
+
// `recommended_modules` section the same way, with no change to this version's shape.
|
|
24
|
+
//
|
|
25
|
+
// Pure — no I/O. The DB half is first-read-db.js; the HTTP surface routes/first-read.js;
|
|
26
|
+
// the CLI scripts/bongos/first-read.js; the Genesis step that hands out the prompt is
|
|
27
|
+
// GENESIS_EXISTING_FIRST in kickoff-checklist.js.
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const SCHEMA = 'bongos.repo-map/1';
|
|
32
|
+
const MAP_FILE = 'bongos-repo-map.json';
|
|
33
|
+
|
|
34
|
+
// Caps. Generous for a real project, small enough that a map cannot carry the code.
|
|
35
|
+
const LIMITS = Object.freeze({
|
|
36
|
+
SUMMARY: 1200,
|
|
37
|
+
TITLE: 160,
|
|
38
|
+
WHY: 500,
|
|
39
|
+
SEEN_AT: 10,
|
|
40
|
+
SEEN_AT_CHARS: 300,
|
|
41
|
+
GOALS: 20,
|
|
42
|
+
TASKS_PER_GOAL: 30,
|
|
43
|
+
NEXT: 50,
|
|
44
|
+
BYTES: 60000,
|
|
45
|
+
});
|
|
46
|
+
const TASK_STATES = ['done', 'in_progress', 'not_started'];
|
|
47
|
+
const NEXT_KINDS = ['error', 'mishap', 'gap'];
|
|
48
|
+
// Top-level keys this version reads. Anything else is kept as-is for a later reader.
|
|
49
|
+
const KNOWN_KEYS = ['schema', 'summary', 'goals', 'next'];
|
|
50
|
+
const UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype'];
|
|
51
|
+
// The goal a `next` item lands under when it names no goal of the map.
|
|
52
|
+
const NEXT_GOAL = Object.freeze({
|
|
53
|
+
ref: 'goal:fix-what-the-first-read-found',
|
|
54
|
+
title: 'Fix what the first read-through found',
|
|
55
|
+
why: 'Errors, mishaps and gaps the first read-through of the repository found, not tied to one goal.',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// slug(text) → a stable lower-case key: the same title always gives the same ref.
|
|
59
|
+
function slug(text) {
|
|
60
|
+
return String(text || '').toLowerCase().normalize('NFKD').replace(/[̀-ͯ]/g, '')
|
|
61
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The refs, in one place, so the plan and the review agree.
|
|
65
|
+
const goalRef = (title) => `goal:${slug(title)}`;
|
|
66
|
+
const taskRef = (goalTitle, title) => `task:${slug(goalTitle)}:${slug(title)}`;
|
|
67
|
+
const nextRef = (title) => `next:${slug(title)}`;
|
|
68
|
+
|
|
69
|
+
// ── the prompt ───────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
// An example map in the defined shape — printed in the prompt so the session copies it.
|
|
72
|
+
function exampleMap() {
|
|
73
|
+
return {
|
|
74
|
+
schema: SCHEMA,
|
|
75
|
+
summary: 'A booking site for a dog-walking business. Sign-up and the booking calendar work; payments are half built.',
|
|
76
|
+
goals: [{
|
|
77
|
+
title: 'Take payments for bookings',
|
|
78
|
+
why: 'A payments folder and a half-finished checkout page exist, and the last ten commits are about it.',
|
|
79
|
+
seen_at: ['src/payments/', 'src/pages/checkout.js'],
|
|
80
|
+
tasks: [
|
|
81
|
+
{ title: 'Finish the checkout page', status: 'in_progress', why: 'The page renders but the pay button does nothing.', seen_at: ['src/pages/checkout.js:88'] },
|
|
82
|
+
{ title: 'Send a receipt by email', status: 'not_started', why: 'A TODO says receipts are still to do.', seen_at: ['src/payments/charge.js:12'] },
|
|
83
|
+
],
|
|
84
|
+
}],
|
|
85
|
+
next: [
|
|
86
|
+
{ title: 'Fix the failing booking test', kind: 'error', goal: '', why: 'The test command fails: the calendar test expects Monday-first weeks.', seen_at: ['tests/calendar.test.js:40'] },
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// firstReadPrompt({ cli }) → the full text the owner hands their own AI session. It is
|
|
92
|
+
// also the Genesis step's description, so the hall's copy-prompt carries all of it.
|
|
93
|
+
function firstReadPrompt({ cli = 'npx bongos' } = {}) {
|
|
94
|
+
return [
|
|
95
|
+
'First read-through of a project that already exists. You are the owner\'s own AI session, working on their computer, on their subscription. Bongos itself never reads this repository.',
|
|
96
|
+
'',
|
|
97
|
+
'1. Read the repository here yourself: the README and any docs or notes, the to-do comments, the recent git history and open branches, and the code. If the project has a test or build command, run it once to see what fails. Do not upload, paste or send any source code anywhere.',
|
|
98
|
+
'2. Write what you found as a map, in a file named ' + MAP_FILE + ' at the top of the repository (do not commit it):',
|
|
99
|
+
' - "goals": the goals already in progress, each with the tasks under it (status done, in_progress or not_started).',
|
|
100
|
+
' - "next": what needs doing next because of errors or mishaps you found in the code (kind error, mishap or gap). Name the goal it belongs to in "goal", or leave it empty.',
|
|
101
|
+
' - Every goal, task and next item has a short "why" (one or two sentences, in plain words) and "seen_at" (where you saw it: file paths, with a line number when it helps). Reasons and places only, never copied code.',
|
|
102
|
+
' Use exactly this shape:',
|
|
103
|
+
JSON.stringify(exampleMap(), null, 2).split('\n').map((l) => ' ' + l).join('\n'),
|
|
104
|
+
`3. Check the file: ${cli} first-read check ${MAP_FILE} — fix anything it lists and check again.`,
|
|
105
|
+
`4. Send it to Bongos: ${cli} first-read submit ${MAP_FILE} — this files nothing yet; it prints the map's number and every item's ref.`,
|
|
106
|
+
'5. Walk the owner through the map in plain words, goal by goal. Ask which items to accept: all of them, some of them, or none.',
|
|
107
|
+
`6. Only after the owner says yes, accept exactly what they chose: ${cli} first-read accept <map number> --all, or --items <ref>,<ref>. If they want none of it: ${cli} first-read reject <map number>. Never accept on your own. Accepted items become draft goals and tasks; nothing can be worked on until someone promotes it.`,
|
|
108
|
+
'Running the read-through again later is safe: anything already accepted is not filed twice.',
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── validation ───────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
function str(v) { return typeof v === 'string' ? v.trim() : ''; }
|
|
115
|
+
|
|
116
|
+
// Check a text field; push an error and return '' when it is missing or too long.
|
|
117
|
+
function text(errors, where, v, max, { required = true } = {}) {
|
|
118
|
+
const s = str(v);
|
|
119
|
+
if (v != null && typeof v !== 'string') { errors.push(`${where} must be text`); return ''; }
|
|
120
|
+
if (!s) { if (required) errors.push(`${where} is missing`); return ''; }
|
|
121
|
+
if (s.length > max) { errors.push(`${where} is longer than ${max} characters — keep it short`); return ''; }
|
|
122
|
+
return s;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function seenAt(errors, where, v) {
|
|
126
|
+
if (!Array.isArray(v) || v.length === 0) { errors.push(`${where}.seen_at needs at least one place (a file path)`); return []; }
|
|
127
|
+
if (v.length > LIMITS.SEEN_AT) { errors.push(`${where}.seen_at lists more than ${LIMITS.SEEN_AT} places`); return []; }
|
|
128
|
+
const out = [];
|
|
129
|
+
v.forEach((p, i) => { const s = text(errors, `${where}.seen_at[${i}]`, p, LIMITS.SEEN_AT_CHARS); if (s) out.push(s); });
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// validateRepoMap(raw) → { ok: true, map, items } | { ok: false, errors: [string] }
|
|
134
|
+
// `map` is the normalised map (unknown top-level keys kept verbatim); `items` is the flat
|
|
135
|
+
// review list: [{ ref, type: 'goal'|'task'|'next', title, why, seen_at, status?, kind?,
|
|
136
|
+
// goal_ref }]. Every error names the field it is about, so a session can fix and retry.
|
|
137
|
+
function validateRepoMap(raw) {
|
|
138
|
+
const errors = [];
|
|
139
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, errors: ['the map must be a JSON object'] };
|
|
140
|
+
if (raw.schema !== SCHEMA) errors.push(`schema must be "${SCHEMA}"`);
|
|
141
|
+
// The server takes request bodies up to 64 KB; a map that size is a summary no longer.
|
|
142
|
+
if (JSON.stringify(raw).length > LIMITS.BYTES) errors.push(`the map is larger than ${LIMITS.BYTES} characters — shorten the reasons or list fewer items`);
|
|
143
|
+
const summary = text(errors, 'summary', raw.summary, LIMITS.SUMMARY);
|
|
144
|
+
const goalsIn = raw.goals == null ? [] : raw.goals;
|
|
145
|
+
const nextIn = raw.next == null ? [] : raw.next;
|
|
146
|
+
if (!Array.isArray(goalsIn)) errors.push('goals must be a list');
|
|
147
|
+
if (!Array.isArray(nextIn)) errors.push('next must be a list');
|
|
148
|
+
if (errors.length) return { ok: false, errors };
|
|
149
|
+
if (goalsIn.length > LIMITS.GOALS) errors.push(`goals lists more than ${LIMITS.GOALS}`);
|
|
150
|
+
if (nextIn.length > LIMITS.NEXT) errors.push(`next lists more than ${LIMITS.NEXT}`);
|
|
151
|
+
if (!goalsIn.length && !nextIn.length) errors.push('the map has no goals and nothing next — list at least one');
|
|
152
|
+
|
|
153
|
+
const items = [];
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
const claimRef = (ref, where) => {
|
|
156
|
+
if (ref.endsWith(':') || ref.endsWith('-') || /:$|::/.test(ref)) { errors.push(`${where}.title needs letters or numbers`); return false; }
|
|
157
|
+
if (seen.has(ref)) { errors.push(`${where}.title repeats another item's title — make it distinct`); return false; }
|
|
158
|
+
seen.add(ref); return true;
|
|
159
|
+
};
|
|
160
|
+
const goals = [];
|
|
161
|
+
goalsIn.slice(0, LIMITS.GOALS).forEach((g, gi) => {
|
|
162
|
+
const w = `goals[${gi}]`;
|
|
163
|
+
if (!g || typeof g !== 'object' || Array.isArray(g)) { errors.push(`${w} must be an object`); return; }
|
|
164
|
+
const title = text(errors, `${w}.title`, g.title, LIMITS.TITLE);
|
|
165
|
+
const why = text(errors, `${w}.why`, g.why, LIMITS.WHY);
|
|
166
|
+
const where = seenAt(errors, w, g.seen_at);
|
|
167
|
+
const tasksIn = g.tasks == null ? [] : g.tasks;
|
|
168
|
+
if (!Array.isArray(tasksIn)) { errors.push(`${w}.tasks must be a list`); return; }
|
|
169
|
+
if (tasksIn.length > LIMITS.TASKS_PER_GOAL) errors.push(`${w}.tasks lists more than ${LIMITS.TASKS_PER_GOAL}`);
|
|
170
|
+
const ref = title ? goalRef(title) : null;
|
|
171
|
+
if (ref && claimRef(ref, w)) items.push({ ref, type: 'goal', title, why, seen_at: where, goal_ref: null });
|
|
172
|
+
const tasks = [];
|
|
173
|
+
tasksIn.slice(0, LIMITS.TASKS_PER_GOAL).forEach((t, ti) => {
|
|
174
|
+
const tw = `${w}.tasks[${ti}]`;
|
|
175
|
+
if (!t || typeof t !== 'object' || Array.isArray(t)) { errors.push(`${tw} must be an object`); return; }
|
|
176
|
+
const tt = text(errors, `${tw}.title`, t.title, LIMITS.TITLE);
|
|
177
|
+
const twhy = text(errors, `${tw}.why`, t.why, LIMITS.WHY);
|
|
178
|
+
const tat = seenAt(errors, tw, t.seen_at);
|
|
179
|
+
const status = t.status == null || t.status === '' ? 'not_started' : t.status;
|
|
180
|
+
if (!TASK_STATES.includes(status)) errors.push(`${tw}.status must be one of ${TASK_STATES.join(', ')}`);
|
|
181
|
+
const tref = title && tt ? taskRef(title, tt) : null;
|
|
182
|
+
if (tref && claimRef(tref, tw)) items.push({ ref: tref, type: 'task', title: tt, why: twhy, seen_at: tat, status, goal_ref: ref });
|
|
183
|
+
tasks.push({ title: tt, status, why: twhy, seen_at: tat });
|
|
184
|
+
});
|
|
185
|
+
goals.push({ title, why, seen_at: where, tasks });
|
|
186
|
+
});
|
|
187
|
+
const goalTitles = new Map(goals.map((g) => [slug(g.title), g.title]));
|
|
188
|
+
const next = [];
|
|
189
|
+
nextIn.slice(0, LIMITS.NEXT).forEach((n, ni) => {
|
|
190
|
+
const w = `next[${ni}]`;
|
|
191
|
+
if (!n || typeof n !== 'object' || Array.isArray(n)) { errors.push(`${w} must be an object`); return; }
|
|
192
|
+
const title = text(errors, `${w}.title`, n.title, LIMITS.TITLE);
|
|
193
|
+
const why = text(errors, `${w}.why`, n.why, LIMITS.WHY);
|
|
194
|
+
const where = seenAt(errors, w, n.seen_at);
|
|
195
|
+
const kind = n.kind == null || n.kind === '' ? 'error' : n.kind;
|
|
196
|
+
if (!NEXT_KINDS.includes(kind)) errors.push(`${w}.kind must be one of ${NEXT_KINDS.join(', ')}`);
|
|
197
|
+
const goal = text(errors, `${w}.goal`, n.goal, LIMITS.TITLE, { required: false });
|
|
198
|
+
if (goal && !goalTitles.has(slug(goal))) errors.push(`${w}.goal names "${goal}", which is not a goal in this map — use a goal's exact title or leave it empty`);
|
|
199
|
+
const ref = title ? nextRef(title) : null;
|
|
200
|
+
const gref = goal && goalTitles.has(slug(goal)) ? goalRef(goal) : NEXT_GOAL.ref;
|
|
201
|
+
if (ref && claimRef(ref, w)) items.push({ ref, type: 'next', title, why, seen_at: where, kind, goal_ref: gref });
|
|
202
|
+
next.push({ title, kind, goal, why, seen_at: where });
|
|
203
|
+
});
|
|
204
|
+
if (errors.length) return { ok: false, errors };
|
|
205
|
+
|
|
206
|
+
const map = { schema: SCHEMA, summary, goals, next };
|
|
207
|
+
// Forward-compatible: a later section (task 1005281's recommended_modules) rides along.
|
|
208
|
+
// Prototype-shaped keys are never copied (CWE-1321): a later section is plain data.
|
|
209
|
+
for (const [k, v] of Object.entries(raw)) if (!KNOWN_KEYS.includes(k) && !UNSAFE_KEYS.includes(k)) map[k] = v;
|
|
210
|
+
return { ok: true, map, items };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── acceptance ───────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
function describe(item, mapId) {
|
|
216
|
+
const lines = [item.why, '', `Seen at: ${item.seen_at.join(', ')}`];
|
|
217
|
+
if (item.status && item.status !== 'not_started') lines.push(`Status when read: ${item.status.replace('_', ' ')}`);
|
|
218
|
+
lines.push('', `From the first read-through of the repository (map ${mapId}, ${item.ref}). A draft: promote it when it is right.`);
|
|
219
|
+
return lines.join('\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// planAcceptance({ items, mapId, select, filedRefs }) → { goals, tasks, skipped, unknown }
|
|
223
|
+
// select 'all' | [ref] — what the owner said yes to. Nothing else is planned.
|
|
224
|
+
// filedRefs Set of refs already filed by an earlier accept (any map) — never filed again.
|
|
225
|
+
// A chosen task or next item brings its goal along (a task needs a home); a goal that is
|
|
226
|
+
// already filed is reused by ref. A task whose status is `done` is history, not work:
|
|
227
|
+
// it is shown in review but never filed.
|
|
228
|
+
function planAcceptance({ items, mapId, select, filedRefs = new Set() }) {
|
|
229
|
+
const byRef = new Map(items.map((i) => [i.ref, i]));
|
|
230
|
+
const wanted = select === 'all' ? items.map((i) => i.ref) : [...new Set(Array.isArray(select) ? select : [])];
|
|
231
|
+
const unknown = wanted.filter((r) => !byRef.has(r));
|
|
232
|
+
const goals = new Map();
|
|
233
|
+
const tasks = [];
|
|
234
|
+
const skipped = [];
|
|
235
|
+
const needGoal = (ref) => {
|
|
236
|
+
if (!ref || filedRefs.has(ref) || goals.has(ref)) return;
|
|
237
|
+
if (ref === NEXT_GOAL.ref) {
|
|
238
|
+
goals.set(ref, { ref, title: NEXT_GOAL.title, description: NEXT_GOAL.why });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const g = byRef.get(ref);
|
|
242
|
+
if (g) goals.set(ref, { ref, title: g.title, description: describe(g, mapId) });
|
|
243
|
+
};
|
|
244
|
+
for (const ref of wanted) {
|
|
245
|
+
const it = byRef.get(ref);
|
|
246
|
+
if (!it) continue;
|
|
247
|
+
if (filedRefs.has(ref)) { skipped.push({ ref, reason: 'already_filed' }); continue; }
|
|
248
|
+
if (it.type === 'goal') { needGoal(ref); continue; }
|
|
249
|
+
if (it.type === 'task' && it.status === 'done') { skipped.push({ ref, reason: 'already_done' }); continue; }
|
|
250
|
+
needGoal(it.goal_ref);
|
|
251
|
+
tasks.push({
|
|
252
|
+
ref,
|
|
253
|
+
goalRef: it.goal_ref,
|
|
254
|
+
title: it.title,
|
|
255
|
+
description: describe(it, mapId),
|
|
256
|
+
kind: it.type === 'next' && it.kind !== 'gap' ? 'bug' : 'feature',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return { goals: [...goals.values()], tasks, skipped, unknown };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = {
|
|
263
|
+
SCHEMA, MAP_FILE, LIMITS, NEXT_GOAL,
|
|
264
|
+
exampleMap, firstReadPrompt, validateRepoMap, planAcceptance,
|
|
265
|
+
};
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// filers read the same steps and the copy cannot drift. It lives with the lifecycle
|
|
10
10
|
// module because tasks are its domain and the kernel doorway must not grow a domain
|
|
11
11
|
// import (ADR 0091 §1); init is a core CLI and, like abandon.js or audit-deps.js,
|
|
12
|
-
// may require module code directly. Pure —
|
|
12
|
+
// may require module code directly. Pure — its one import, first-read.js (the
|
|
13
|
+
// first read-through prompt, task 1005280), is pure too.
|
|
13
14
|
//
|
|
14
15
|
// Each step becomes a real, claimable task so it is tracked, never silently skipped
|
|
15
16
|
// (grounded in the onboarding/deploy surfaces; the task 1204 mapping pass). Per-module
|
|
@@ -17,6 +18,8 @@
|
|
|
17
18
|
|
|
18
19
|
'use strict';
|
|
19
20
|
|
|
21
|
+
const { firstReadPrompt } = require('./first-read');
|
|
22
|
+
|
|
20
23
|
// The one kickoff step the provisioning module AUTOMATES (ADR 0111 §3): standing up
|
|
21
24
|
// production hosting/TLS/DNS/managed-DB. Kept as a named builder so init can either
|
|
22
25
|
// FILE it (no provisioning requested, or the request couldn't be enqueued) or OMIT
|
|
@@ -144,7 +147,7 @@ function kickoffChecklist(spec = {}, { provisioned = false, firstSignIn = false
|
|
|
144
147
|
const GENESIS_STEPS = Object.freeze({
|
|
145
148
|
scope: [
|
|
146
149
|
{ title: 'Write the project scope: what we are making, for whom, and what is out of scope',
|
|
147
|
-
description: 'Genesis stage 1 (scope).
|
|
150
|
+
description: 'Genesis stage 1 (scope). The project interview: what you are creating and why, who it is for and who you want building it with you, what success looks like in two years, and what everyone else in this area is getting wrong. Every future goal and task is planned from your answers, and "not sure yet" is a fine answer you can come back to. You are the author and you choose how to work: be guided through the questions, write it yourself with the questions beside you, or have your own AI session walk you through them (it asks and pushes back; you decide) — switch any time, nothing is lost. The one-page scope is then drafted from your answers. Run `npx bongos genesis show scope` to see the questions and your answers, or `npx bongos genesis assist scope` for the brief to give your AI session. When it is written, Goals and the Roadmap wake.' },
|
|
148
151
|
],
|
|
149
152
|
strategize: [
|
|
150
153
|
{ title: 'Write out the strategy, or link the one you already have',
|
|
@@ -163,14 +166,30 @@ const GENESIS_STEPS = Object.freeze({
|
|
|
163
166
|
description: 'Genesis stage 5 (legislation). Who has a say, and the rule a decision passes by. When it is done, Ranks wake, and the project can come alive.' },
|
|
164
167
|
],
|
|
165
168
|
});
|
|
169
|
+
// task 1005280: the first read-through. The step's description IS the prompt the owner
|
|
170
|
+
// copies into their own AI session (the hall's copy-prompt hands over a task's
|
|
171
|
+
// description), so the repository is read on the owner's subscription, never the
|
|
172
|
+
// platform's, and comes back as a map the owner accepts or rejects (first-read.js).
|
|
166
173
|
const GENESIS_EXISTING_FIRST = {
|
|
167
|
-
title: 'Gather what already exists:
|
|
168
|
-
description: 'Genesis stage 1 (scope), for a project that already exists. Before writing anything new,
|
|
174
|
+
title: 'Gather what already exists: have your own AI read the repository and map it',
|
|
175
|
+
description: 'Genesis stage 1 (scope), for a project that already exists. Before writing anything new, map what is already there, so the scope is written from it rather than beside it. ' +
|
|
176
|
+
'Copy this task\'s prompt into your own AI session, opened in the project\'s folder on your computer; it reads the code there and sends back a map for you to accept.\n\n' +
|
|
177
|
+
firstReadPrompt(),
|
|
169
178
|
};
|
|
170
179
|
|
|
171
|
-
//
|
|
180
|
+
// GENESIS_AFTER — "this step follows that one", by ref (task 1005110). A step listed
|
|
181
|
+
// here is filed with a real dependency edge on the step it names, so it cannot be
|
|
182
|
+
// claimed (DEPS_NOT_SHIPPED) and the genesis home draws it locked until that one
|
|
183
|
+
// ships. Only where the order is real: a stage with no entry here is all open at once.
|
|
184
|
+
// Today that is brownfield scope alone — the scope is written FROM what was gathered.
|
|
185
|
+
const GENESIS_AFTER = Object.freeze({
|
|
186
|
+
'genesis:scope': 'genesis:scope:existing',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// genesisChecklist({ existing }) → { stageKey: [{ title, description, kind, discipline, ref, after }] }
|
|
172
190
|
// in genesis order. `ref` is the stable per-task key the seeder dedups on, so filing
|
|
173
|
-
// twice files nothing new.
|
|
191
|
+
// twice files nothing new. `after` is the ref of the step this one waits for, set only
|
|
192
|
+
// when that step is in the same list (GENESIS_AFTER), else null. Pure — no I/O.
|
|
174
193
|
function genesisChecklist({ existing = false } = {}) {
|
|
175
194
|
const out = {};
|
|
176
195
|
for (const [stage, steps] of Object.entries(GENESIS_STEPS)) {
|
|
@@ -181,6 +200,10 @@ function genesisChecklist({ existing = false } = {}) {
|
|
|
181
200
|
...t, kind: 'decision', discipline: 'artist', ref: t === GENESIS_EXISTING_FIRST ? 'genesis:scope:existing' : `genesis:${stage}`,
|
|
182
201
|
}));
|
|
183
202
|
}
|
|
203
|
+
for (const list of Object.values(out)) {
|
|
204
|
+
const refs = new Set(list.map((t) => t.ref));
|
|
205
|
+
for (const t of list) t.after = refs.has(GENESIS_AFTER[t.ref]) ? GENESIS_AFTER[t.ref] : null;
|
|
206
|
+
}
|
|
184
207
|
return out;
|
|
185
208
|
}
|
|
186
209
|
|