@bongos/core 1.19.376 → 1.19.377

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.376",
6
- "core_contract": "1.19.376",
7
- "source_commit": "a431d7ef6e3018eeb8b16fbf2616b7b99ec4d5d5",
5
+ "core_version": "1.19.377",
6
+ "core_contract": "1.19.377",
7
+ "source_commit": "90c7b43a6f0265aaf5d5c117cbe2721c18bd6c35",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-25T23:21:03.279Z",
9
+ "built_at": "2026-08-26T23:56:55.300Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 335,
13
13
  "agent_docs_stubbed": 22,
14
- "functional_verbatim": 1557,
14
+ "functional_verbatim": 1565,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1914,
20
- "tree_sha256": "6c7b5981caac40dce57dfecbee01587f18c3b4e5b4cb8fa65ebde70c9869ef53",
19
+ "file_count": 1922,
20
+ "tree_sha256": "ef3ea529acb199c38d7dc4bb8bc657b694727f8e8ffaa8fdf2af230913416726",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -4154,6 +4154,16 @@
4154
4154
  "mode": "0000644",
4155
4155
  "sha256": "0027e5e8a3b718a16dcae5ad4387b4773525d429944e712228fbd38a5335e23a"
4156
4156
  },
4157
+ {
4158
+ "path": "modules/lifecycle/kickoff-checklist.js",
4159
+ "mode": "0000644",
4160
+ "sha256": "829dd13fb87033dba1db8d3003a0359e1be8bb97198df419edb2617558bfd984"
4161
+ },
4162
+ {
4163
+ "path": "modules/lifecycle/kickoff-seed.js",
4164
+ "mode": "0000644",
4165
+ "sha256": "f26a507ea55169df9f4841edb2347610f3dea9b7eac729c034c038a0edd1c7af"
4166
+ },
4157
4167
  {
4158
4168
  "path": "modules/lifecycle/lifecycle.js",
4159
4169
  "mode": "0000644",
@@ -4187,7 +4197,7 @@
4187
4197
  {
4188
4198
  "path": "modules/lifecycle/module.json",
4189
4199
  "mode": "0000644",
4190
- "sha256": "30653b4064be8933894679ac4e272711edcc629e151d87017e2b2e45be96d78f"
4200
+ "sha256": "401e4121a3f335ae0ae532b6b7ccff63e668b3a53520a0ff3f0d1a4223b02020"
4191
4201
  },
4192
4202
  {
4193
4203
  "path": "modules/lifecycle/pollers/publish-reconciler.js",
@@ -4244,6 +4254,11 @@
4244
4254
  "mode": "0000644",
4245
4255
  "sha256": "0e9430101ce99d0a84f1893d13bcf1434af13325c2ce531e693b6bce42cb9385"
4246
4256
  },
4257
+ {
4258
+ "path": "modules/lifecycle/routes/kickoff-seed.js",
4259
+ "mode": "0000644",
4260
+ "sha256": "f5090caaf0d0f32feff5bf4263e65ecf831ad6fd10ae5b053788c987e1a86399"
4261
+ },
4247
4262
  {
4248
4263
  "path": "modules/lifecycle/routes/lifecycle.js",
4249
4264
  "mode": "0000644",
@@ -4927,12 +4942,12 @@
4927
4942
  {
4928
4943
  "path": "package-lock.json",
4929
4944
  "mode": "0000644",
4930
- "sha256": "0cdd9747593cc2fb83327041cf2d869234271cf7e3dcc7ecfb626c43ed262155"
4945
+ "sha256": "e5e8b0e3c6879a8636f96f6bdce1c4fd8d9b06521e431bd6051bb2be2ea96518"
4931
4946
  },
4932
4947
  {
4933
4948
  "path": "package.json",
4934
4949
  "mode": "0000644",
4935
- "sha256": "00ff5e9289bc25cc0e6d1a7226259b52b906eaf887c9dd417067d41193877d7c"
4950
+ "sha256": "3057a201c65b95c06512dfbf5642b4d3de10c513382e771b478f0eb43dad3ff8"
4936
4951
  },
4937
4952
  {
4938
4953
  "path": "public-docs/index.html",
@@ -5627,7 +5642,7 @@
5627
5642
  {
5628
5643
  "path": "scripts/gds/fitness.js",
5629
5644
  "mode": "0000644",
5630
- "sha256": "95f116e70ba6d6e890705ff862a5c5011002c42a58ba60553c50ec2d656dcf3b"
5645
+ "sha256": "4dec7f6bee219ca3c11dbf5b9c3380de286264a66830fd8e2d37018a989eb83e"
5631
5646
  },
5632
5647
  {
5633
5648
  "path": "scripts/gds/gate-review.js",
@@ -5747,7 +5762,7 @@
5747
5762
  {
5748
5763
  "path": "scripts/gds/init.js",
5749
5764
  "mode": "0000644",
5750
- "sha256": "bde86f3f65cdd4a1ad0e143c58f6517328dd7cdee7c1194c0fa614aaf9df56f6"
5765
+ "sha256": "280197b62032374c11824c4f97ec713725e28e2b57cfd6c8aa42d493b02c4ed7"
5751
5766
  },
5752
5767
  {
5753
5768
  "path": "scripts/gds/install-git-hooks.js",
@@ -6399,6 +6414,11 @@
6399
6414
  "mode": "0000644",
6400
6415
  "sha256": "88b9b721b3911957b13aa5e8023e6d41534e34afd4bbc70f40435efc52e2017b"
6401
6416
  },
6417
+ {
6418
+ "path": "scripts/gds/test-path-guard.js",
6419
+ "mode": "0000644",
6420
+ "sha256": "d3e542cf977f9aadad8f856defa6d809f3604e184cd261fbee7738aa1da736ea"
6421
+ },
6402
6422
  {
6403
6423
  "path": "scripts/gds/triage.js",
6404
6424
  "mode": "0000644",
@@ -6542,7 +6562,7 @@
6542
6562
  {
6543
6563
  "path": "src/bongos/auth-admission.js",
6544
6564
  "mode": "0000644",
6545
- "sha256": "0d2e4aaf4b99ac4b1122c182f279ae622f53d8896baa31d796c5bb0f4ef6532c"
6565
+ "sha256": "9978b10e2e675b6f7c1211dd74c8cf302395b2f47062eba5df376203ee77d341"
6546
6566
  },
6547
6567
  {
6548
6568
  "path": "src/bongos/auth-config.js",
@@ -6817,7 +6837,7 @@
6817
6837
  {
6818
6838
  "path": "src/module-api.js",
6819
6839
  "mode": "0000644",
6820
- "sha256": "94c29ffd7acffe072f72060e6bf7deb322031a3521e935f77f4069cf7da1a699"
6840
+ "sha256": "5287468c8e1a2ba691e02ff2150bcb425b00db6c0ce676b063d32cb20ef640c5"
6821
6841
  },
6822
6842
  {
6823
6843
  "path": "src/module-loader/catalog.js",
@@ -7694,6 +7714,11 @@
7694
7714
  "mode": "0000644",
7695
7715
  "sha256": "5466ddf0b7e0298206d46da7b84cfd3826e0f549395088bf5ed91397d15f1746"
7696
7716
  },
7717
+ {
7718
+ "path": "tests/first_admin_kickoff_seed.mjs",
7719
+ "mode": "0000644",
7720
+ "sha256": "8006f4696e6bae21b84c9e8ec25cb7a7d34c5899f187d2ac59b2a6dcbb6f92d1"
7721
+ },
7697
7722
  {
7698
7723
  "path": "tests/first_run_engine_check.mjs",
7699
7724
  "mode": "0000644",
@@ -8374,6 +8399,16 @@
8374
8399
  "mode": "0000644",
8375
8400
  "sha256": "db9ad86df4c9593db89f91ef2306c30eb33ae56bebe5b210a4b3a1912f67d42c"
8376
8401
  },
8402
+ {
8403
+ "path": "tests/kickoff_checklist.mjs",
8404
+ "mode": "0000644",
8405
+ "sha256": "d0849053675cb4705fe6774b1a8ac6b3edb5b43a758775446fad257be6cb9ba3"
8406
+ },
8407
+ {
8408
+ "path": "tests/kickoff_seed.mjs",
8409
+ "mode": "0000644",
8410
+ "sha256": "416d413757a13fbddb5841ea8a2c503077d7e6067bc1c58ce1f4d416b8d8487f"
8411
+ },
8377
8412
  {
8378
8413
  "path": "tests/known_failing_ratchet.mjs",
8379
8414
  "mode": "0000644",
@@ -8832,7 +8867,7 @@
8832
8867
  {
8833
8868
  "path": "tests/provisioning_onboard_mode.mjs",
8834
8869
  "mode": "0000644",
8835
- "sha256": "7749df454ac3cca7c7e5ea1acc94ead15d1c9f87b38fe2028a1c93c8adf31bcf"
8870
+ "sha256": "97d04ee39cc06b720d4a5b42f3e3cc614f64d9462b27471a1af7598b5e2929be"
8836
8871
  },
8837
8872
  {
8838
8873
  "path": "tests/provisioning_settings.mjs",
@@ -9057,7 +9092,7 @@
9057
9092
  {
9058
9093
  "path": "tests/search_ingest.mjs",
9059
9094
  "mode": "0000644",
9060
- "sha256": "504a855803d88468667663f663656dfd8e25726b90a412e34bcf55da0ad57d5e"
9095
+ "sha256": "8d685a46ab9f4cf9aaa71b9ee78774d95c16daa27c15a229b8628a97688b9a55"
9061
9096
  },
9062
9097
  {
9063
9098
  "path": "tests/search_ingest_db.mjs",
@@ -9192,7 +9227,7 @@
9192
9227
  {
9193
9228
  "path": "tests/setup_smoke.mjs",
9194
9229
  "mode": "0000644",
9195
- "sha256": "919541c9444b7dc60f33a1e56b2f7defaf07ea1da5fb4b0c9760ca1bb9e7f1e0"
9230
+ "sha256": "c55f6c506758d973b7cbe750a873e8c38d02006e1495f027fedb0ebfc84c7186"
9196
9231
  },
9197
9232
  {
9198
9233
  "path": "tests/ship_api_client_pathspec.mjs",
@@ -9459,6 +9494,11 @@
9459
9494
  "mode": "0000644",
9460
9495
  "sha256": "3ff18b286a68b244a272b3f2606ae33b298622347aae97305152f235ca57b330"
9461
9496
  },
9497
+ {
9498
+ "path": "tests/test_path_guard.mjs",
9499
+ "mode": "0000644",
9500
+ "sha256": "bbb0b063bffc6e394114bc4895da5006d8797a0317ed38f1c7f9b989841d5eab"
9501
+ },
9462
9502
  {
9463
9503
  "path": "tests/tool-scan-fixtures/README.md",
9464
9504
  "mode": "0000644",
@@ -0,0 +1,138 @@
1
+ // modules/lifecycle/kickoff-checklist.js — the kickoff checklist: the launch steps a fresh
2
+ // instance needs that the platform CANNOT do for its owner, as task specs (task 1003239).
3
+ //
4
+ // ONE list, two filers. `bongos init` files it at scaffold time on the self-host path
5
+ // (scripts/gds/init.js kickoffTasks); the lifecycle module files it at the founder's
6
+ // FIRST SIGN-IN on the control-plane path (modules/lifecycle/kickoff-seed.js), where
7
+ // the scaffold ran `init --no-seed` and no Archon existed to file anything — walk
8
+ // finding W4: the founder reached an empty board. Moved here from init.js so both
9
+ // filers read the same steps and the copy cannot drift. It lives with the lifecycle
10
+ // module because tasks are its domain and the kernel doorway must not grow a domain
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 — imports nothing.
13
+ //
14
+ // Each step becomes a real, claimable task so it is tracked, never silently skipped
15
+ // (grounded in the onboarding/deploy surfaces; the task 1204 mapping pass). Per-module
16
+ // prerequisites are appended only for the modules the owner enabled.
17
+
18
+ 'use strict';
19
+
20
+ // The one kickoff step the provisioning module AUTOMATES (ADR 0111 §3): standing up
21
+ // production hosting/TLS/DNS/managed-DB. Kept as a named builder so init can either
22
+ // FILE it (no provisioning requested, or the request couldn't be enqueued) or OMIT
23
+ // it (a request went in — the control-plane runner covers this leg).
24
+ function productionTopologyKickoff() {
25
+ return {
26
+ title: 'Production topology: TLS, real origins, managed Postgres',
27
+ kind: 'infra',
28
+ description: 'For a real (non-localhost) launch: front the instance with TLS (Caddy/Cloudflare/nginx), point ' +
29
+ 'publicOrigin/buildersOrigin/statusOrigin at the real domain (the OAuth callback depends on this), and move off the bundled container DB volume to managed/backed-up Postgres.',
30
+ };
31
+ }
32
+
33
+ // The admission step as the SELF-HOST owner meets it: an env flag on their own box.
34
+ const ADMISSION_SELF_HOST = {
35
+ title: 'Set the admission posture and invite the first builders',
36
+ kind: 'infra',
37
+ description: 'Enrollment is invite-only by default. Beyond you, no one can join until you approve an access request ' +
38
+ 'or set <ENVPREFIX>_OPEN_XENOS_ENROLLMENT=1. Decide who can join and invite/approve the first builders.',
39
+ };
40
+
41
+ // The same decision as a HUB-PROVISIONED founder meets it: the door is the project's
42
+ // "How people join" setting on the hub (task 1003044), never an env var they cannot
43
+ // reach; invites are the Watch page's "Invite by GitHub username". Names only surfaces
44
+ // that exist — a blank board is better than untrue copy (task 1003239).
45
+ const ADMISSION_AT_FIRST_SIGN_IN = {
46
+ title: 'Decide how people join, and invite the first builders',
47
+ kind: 'infra',
48
+ description: 'Unless you opened the door when you created the project, nobody beyond you can join until you invite them. ' +
49
+ 'Set "How people join" (open / apply to join / invite-only) from your project\'s manage page on the hub, ' +
50
+ 'and invite the first builders by GitHub username from your project\'s Watch page.',
51
+ };
52
+
53
+ const MERGE_GATE = {
54
+ title: 'Arm the merge gate before a second builder joins (ci deploy mode)',
55
+ kind: 'infra',
56
+ description: 'For a shared instance on ci deploy mode, enable branch protection on main + required checks ' +
57
+ '(unit / secret-scan / gate-review), so untrusted builders cannot push straight to production. No review requirement — gates are CI checks, not human approvals (ADR 0162). Skippable for a solo/local instance.',
58
+ };
59
+
60
+ const INSTANCE_LOOK = {
61
+ title: 'Fill the instance look with your own LLM',
62
+ // 'ideator' is a DISCIPLINE, not a kind — tasks_kind_check rejects it as a
63
+ // kind and the task silently vanished from the checklist (the one non-infra
64
+ // kickoff step). Deciding the instance look is a decision by an ideator.
65
+ kind: 'decision',
66
+ discipline: 'ideator',
67
+ description: 'init wrote a lean config/branding.json and left the LOOK to you (a locked project rule: brand with your ' +
68
+ 'own LLM, not a generator). Hand docs/branding-fill-prompt.md to your AI to design a palette/fonts/wordmark, or keep the vanilla look.',
69
+ };
70
+
71
+ // Steps that are already TRUE the moment a hub-provisioned founder signs in: auth
72
+ // works (they just used it), the sign-in is this one, and the scaffold bound the repo
73
+ // and chose the deploy mode. Filed on the self-host path only.
74
+ function selfHostOnlySteps(spec, origin) {
75
+ return [
76
+ {
77
+ title: 'Register a GitHub OAuth app and set its credentials',
78
+ kind: 'infra',
79
+ description: 'Auth is the hard prerequisite for every sign-in — without it the server returns 503 ' +
80
+ 'auth_not_configured and nobody (including you) can log in. Create a GitHub OAuth app, set ' +
81
+ `<ENVPREFIX>_GITHUB_CLIENT_ID/SECRET, tick "Enable Device Flow" (a separate save), and set the callback URL to ${origin}/api/gds/auth/web/callback.`,
82
+ },
83
+ {
84
+ title: `Sign in once as ${spec.firstAdmin} to seat the founding owner`,
85
+ kind: 'infra',
86
+ description: `A fresh database has no owner. Signing in as the configured firstAdmin (${spec.firstAdmin}) ` +
87
+ 'the first time auto-seats that account as the founding Archon (owner). Config alone does nothing — you must actually sign in.',
88
+ },
89
+ {
90
+ title: 'Confirm the GitHub repo binding and deploy mode',
91
+ kind: 'infra',
92
+ description: `This instance ships work to ${spec.repo.owner}/${spec.repo.name}. Confirm the git remote / repo binding ` +
93
+ 'and pick deploy mode in config/deploy.json (ci = server-mediated PR auto-merge, or laptop). A wrong/absent repo binding lets a task grade but never merge.',
94
+ },
95
+ ];
96
+ }
97
+
98
+ // Per-module prerequisites — a key the module cannot run without.
99
+ function moduleKeySteps(spec) {
100
+ const enabled = new Set(Array.isArray(spec.modules) ? spec.modules : []);
101
+ const tasks = [];
102
+ if (enabled.has('art-pipeline')) {
103
+ tasks.push({ title: 'Provide a Google Gemini API key for the art pipeline', kind: 'infra',
104
+ description: 'The art-pipeline module needs a Google AI Studio (Gemini) key (GOOGLE_AI_STUDIO_KEY, a shared key, or per-builder keys) or it cannot generate art.' });
105
+ }
106
+ if (enabled.has('discord')) {
107
+ tasks.push({ title: 'Provide Discord bot token + webhooks', kind: 'infra',
108
+ description: 'The discord module needs a bot token + webhook URLs + DISCORD_BOT_ENABLED=true. Note: outbound/link surfaces fire on credentials even if the module flag is later turned off.' });
109
+ }
110
+ if (enabled.has('dev-box')) {
111
+ tasks.push({ title: 'Provide a DigitalOcean API token for dev boxes', kind: 'infra',
112
+ description: 'The dev-box module needs a DigitalOcean API token (and optionally Cloudflare) on the operator control plane before any builder box can be provisioned.' });
113
+ }
114
+ return tasks;
115
+ }
116
+
117
+ // kickoffChecklist(spec, opts) → task specs ({ title, kind, description[, discipline] }).
118
+ // provisioned — the owner opted into provisioning at init: the control-plane runner
119
+ // covers production topology, so that step is omitted (ADR 0111 §3).
120
+ // firstSignIn — filed the moment the founder of a hub-provisioned project is seated:
121
+ // only the steps still true after the standup, with the admission door
122
+ // described as the hub setting it now is.
123
+ // Pure — no I/O; the caller files them.
124
+ function kickoffChecklist(spec = {}, { provisioned = false, firstSignIn = false } = {}) {
125
+ const origin = (spec.domains && spec.domains.publicOrigin) || 'http://localhost:3000';
126
+ const tasks = firstSignIn ? [] : selfHostOnlySteps(spec, origin);
127
+ tasks.push(firstSignIn ? ADMISSION_AT_FIRST_SIGN_IN : ADMISSION_SELF_HOST);
128
+ tasks.push(MERGE_GATE);
129
+ // Production topology is either AUTOMATED (the owner opted into provisioning — the
130
+ // control-plane runner stands up TLS/DNS/managed-DB) or a tracked manual step.
131
+ // Keep it in its infra slot, before the look step, when it's still manual.
132
+ if (!provisioned && !firstSignIn) tasks.push(productionTopologyKickoff());
133
+ tasks.push(INSTANCE_LOOK);
134
+ tasks.push(...moduleKeySteps(spec));
135
+ return tasks.map((t) => ({ ...t }));
136
+ }
137
+
138
+ module.exports = { kickoffChecklist, productionTopologyKickoff };
@@ -0,0 +1,69 @@
1
+ // modules/lifecycle/kickoff-seed.js — file the kickoff checklist at the founder's first
2
+ // sign-in (task 1003239; behind the `lifecycle.kickoffSeed` port).
3
+ //
4
+ // WHY HERE, WHY THEN. `bongos init` files the kickoff checklist as tasks — but the
5
+ // control-plane scaffold runs `init --no-seed` (scripts/gds/provision-repo.js), and a
6
+ // hub-provisioned project has no Archon until its founder signs in, so nothing could
7
+ // be filed earlier: the founder reached an EMPTY board (walk finding W4). The kernel's
8
+ // first-admin seating (src/bongos/auth-admission.js) resolves this port the moment the
9
+ // owner is seated — the first moment the checklist CAN be filed.
10
+ //
11
+ // IDEMPOTENT BY EMPTINESS, not by path. It seeds only when the tasks table is empty:
12
+ // the self-host path already filed its checklist before anyone signed in, so it is
13
+ // skipped there with no "was I provisioned" marker to keep true. The caller treats a
14
+ // failure as best-effort — a seed hiccup must never fail the sign-in that just seated
15
+ // the owner.
16
+ //
17
+ // THE BOUNDARY (ADR 0093 §1): core is reached only through ../../src/module-api.js.
18
+
19
+ 'use strict';
20
+
21
+ const api = require('../../src/module-api');
22
+ const db = require('./db');
23
+ const { kickoffChecklist } = require('./kickoff-checklist');
24
+
25
+ // The spec the checklist reads, from the instance's own branding pack. `modules` is
26
+ // empty on purpose: a hub-provisioned project is scaffolded with every optional module
27
+ // off, and its founder cannot have enabled one before their first sign-in.
28
+ function specFromBranding(b = api.branding()) {
29
+ return {
30
+ firstAdmin: b.firstAdmin,
31
+ repo: b.repo || {},
32
+ domains: { publicOrigin: b.domains && b.domains.publicOrigin },
33
+ modules: [],
34
+ };
35
+ }
36
+
37
+ // The version the checklist lands under: the building one, else the first by id. The
38
+ // scaffold seeds exactly one (provision-repo.js seedFirstVersionCmd).
39
+ const FIRST_VERSION_SQL = `SELECT id FROM versions ORDER BY (status = 'building') DESC, id ASC LIMIT 1`;
40
+
41
+ // seedKickoffIfEmpty({ builderId }, deps) → { seeded, versionId } | { seeded: 0, skipped }.
42
+ // deps.pool / deps.createTask / deps.spec are test seams; production reads the doorway.
43
+ async function seedKickoffIfEmpty({ builderId = null } = {}, deps = {}) {
44
+ const pool = deps.pool || api.pool;
45
+ const insert = deps.createTask || ((row) => db.createTask(row));
46
+ const { rows: [{ n }] } = await pool.query('SELECT count(*)::int AS n FROM tasks');
47
+ if (n > 0) return { seeded: 0, skipped: 'tasks_exist' };
48
+ const { rows: versions } = await pool.query(FIRST_VERSION_SQL);
49
+ if (!versions.length) return { seeded: 0, skipped: 'no_version' };
50
+ const versionId = versions[0].id;
51
+ const steps = kickoffChecklist(deps.spec || specFromBranding(), { firstSignIn: true });
52
+ let seeded = 0;
53
+ for (const t of steps) {
54
+ await insert({
55
+ versionId,
56
+ title: t.title,
57
+ description: t.description || '',
58
+ status: 'ready',
59
+ kind: t.kind || 'infra',
60
+ discipline: t.discipline || 'engineer',
61
+ source: 'first-sign-in',
62
+ createdBy: builderId,
63
+ });
64
+ seeded += 1;
65
+ }
66
+ return { seeded, versionId };
67
+ }
68
+
69
+ module.exports = { seedKickoffIfEmpty, specFromBranding, FIRST_VERSION_SQL };
@@ -21,14 +21,16 @@
21
21
  "help-requests",
22
22
  "recommendations",
23
23
  "closeness",
24
- "visuals"
24
+ "visuals",
25
+ "kickoff-seed"
25
26
  ],
26
27
  "pollers": [
27
28
  "publish-reconciler"
28
29
  ]
29
30
  },
30
31
  "provides": [
31
- "lifecycle"
32
+ "lifecycle",
33
+ "lifecycle.kickoffSeed"
32
34
  ],
33
35
  "consumes": []
34
36
  }
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ // modules/lifecycle/routes/kickoff-seed.js — registers the `lifecycle.kickoffSeed` port
4
+ // (task 1003239): the founder's first board, filed the moment the kernel seats them.
5
+ //
6
+ // Resolved by ONE caller — the first-admin bootstrap in src/bongos/auth-admission.js —
7
+ // through resolveOptional, so an instance without this module signs in exactly as
8
+ // before. A route factory for a routeless capability, for the reason routes/lifecycle.js
9
+ // and routes/rank-sync.js give: mountModuleRoutes is the boot hook that runs in the live
10
+ // server AND whenever a test rebuilds the router. The router stays EMPTY — seeding has
11
+ // no HTTP surface, and an endpoint here would let anyone re-file a founder's checklist.
12
+
13
+ const express = require('express');
14
+ const api = require('../../../src/module-api');
15
+ const seed = require('../kickoff-seed');
16
+
17
+ // Delegates at call time (never spreads) so a stubbed seed still bites.
18
+ const kickoffSeedPort = {
19
+ seedIfEmpty: (...a) => seed.seedKickoffIfEmpty(...a),
20
+ };
21
+
22
+ module.exports = function buildLifecycleKickoffSeedRouter() {
23
+ if (!api.hasProvider('lifecycle.kickoffSeed')) {
24
+ api.registerProvider('lifecycle.kickoffSeed', kickoffSeedPort);
25
+ }
26
+ return express.Router();
27
+ };
28
+
29
+ module.exports.kickoffSeedPort = kickoffSeedPort;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.376",
3
+ "version": "1.19.377",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.376",
9
+ "version": "1.19.377",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.376",
3
+ "version": "1.19.377",
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",
@@ -1308,6 +1308,7 @@ const CHECKS = [
1308
1308
  require('./routine-schedule.js').checkRoutineCadencesScheduled, // Check 25 — task 1003209: a routine that declares a cadence must have a scheduler firing it AT that period (ADR 0188)
1309
1309
  require('./baseline-staleness.js').checkBaselineFloors, // Check 26 — task 1003210: a shrink-only baseline that never shrinks is unmanaged debt; a floor exempting one must say why
1310
1310
  require('./client-baseurl-guard.js').checkClientBaseUrl,
1311
+ require('./test-path-guard.js').checkTestsUseFileUrlToPath, // Check 30 — task 1002889: a test builds its repo root with fileURLToPath, never URL.pathname (a %20 or a Windows drive slash makes require() throw at load; CI's spaceless checkout can't see it)
1311
1312
  // Check 29 — task 1003275 / ADR 0189: a work category ORIENTS a human and steers
1312
1313
  // Claude; it authorises nothing. The owner said so twice, and the goal's own prose
1313
1314
  // predicts the failure: a second thing that looks like a hierarchy gets mistaken
@@ -218,18 +218,11 @@ function provisioningRequest(spec) {
218
218
  return body;
219
219
  }
220
220
 
221
- // The one kickoff step the provisioning module AUTOMATES (ADR 0111 §3): standing up
222
- // production hosting/TLS/DNS/managed-DB. Kept as a named builder so init can either
223
- // FILE it (no provisioning requested, or the request couldn't be enqueued) or OMIT
224
- // it (a request went in — the control-plane runner covers this leg).
225
- function productionTopologyKickoff() {
226
- return {
227
- title: 'Production topology: TLS, real origins, managed Postgres',
228
- kind: 'infra',
229
- description: 'For a real (non-localhost) launch: front the instance with TLS (Caddy/Cloudflare/nginx), point ' +
230
- 'publicOrigin/buildersOrigin/statusOrigin at the real domain (the OAuth callback depends on this), and move off the bundled container DB volume to managed/backed-up Postgres.',
231
- };
232
- }
221
+ // The kickoff checklist itself (and the production-topology step the provisioning
222
+ // module can automate) now live in modules/lifecycle/kickoff-checklist.js — ONE list shared
223
+ // with the lifecycle module, which files the same steps at the founder's first
224
+ // sign-in on the control-plane path (task 1003239). init keeps its names.
225
+ const { kickoffChecklist, productionTopologyKickoff } = require('../../modules/lifecycle/kickoff-checklist');
233
226
 
234
227
  // The lean instance branding.json: ONLY the fields the owner supplied. Everything
235
228
  // else (theme, copy, rank labels, box subdomain pattern) resolves from
@@ -325,74 +318,11 @@ function mergeCoreDependency(pkg, { corePkg = CORE_PKG, coreRange, name = 'cloud
325
318
  return { pkg: next, action: 'added' };
326
319
  }
327
320
 
328
- // The kickoff checklist, as tasks. These are the steps init CANNOT do for you —
329
- // the human-only setup a fresh instance needs to actually launch (grounded in
330
- // the onboarding/deploy surfaces; see the task 1204 mapping pass). Each becomes
331
- // a real, claimable task so it's tracked, never silently skipped. Per-module
332
- // prerequisites are appended only for the modules the owner enabled.
321
+ // The kickoff checklist, as tasks — the steps init CANNOT do for you (the shared
322
+ // list, self-host shape). Production topology is omitted when the owner opted into
323
+ // provisioning: the control-plane runner covers that leg (ADR 0111 §3).
333
324
  function kickoffTasks(spec) {
334
- const origin = (spec.domains && spec.domains.publicOrigin) || 'http://localhost:3000';
335
- const tasks = [
336
- {
337
- title: 'Register a GitHub OAuth app and set its credentials',
338
- kind: 'infra',
339
- description: 'Auth is the hard prerequisite for every sign-in — without it the server returns 503 ' +
340
- 'auth_not_configured and nobody (including you) can log in. Create a GitHub OAuth app, set ' +
341
- `<ENVPREFIX>_GITHUB_CLIENT_ID/SECRET, tick "Enable Device Flow" (a separate save), and set the callback URL to ${origin}/api/gds/auth/web/callback.`,
342
- },
343
- {
344
- title: `Sign in once as ${spec.firstAdmin} to seat the founding owner`,
345
- kind: 'infra',
346
- description: `A fresh database has no owner. Signing in as the configured firstAdmin (${spec.firstAdmin}) ` +
347
- 'the first time auto-seats that account as the founding Archon (owner). Config alone does nothing — you must actually sign in.',
348
- },
349
- {
350
- title: 'Confirm the GitHub repo binding and deploy mode',
351
- kind: 'infra',
352
- description: `This instance ships work to ${spec.repo.owner}/${spec.repo.name}. Confirm the git remote / repo binding ` +
353
- 'and pick deploy mode in config/deploy.json (ci = server-mediated PR auto-merge, or laptop). A wrong/absent repo binding lets a task grade but never merge.',
354
- },
355
- {
356
- title: 'Set the admission posture and invite the first builders',
357
- kind: 'infra',
358
- description: 'Enrollment is invite-only by default. Beyond you, no one can join until you approve an access request ' +
359
- 'or set <ENVPREFIX>_OPEN_XENOS_ENROLLMENT=1. Decide who can join and invite/approve the first builders.',
360
- },
361
- {
362
- title: 'Arm the merge gate before a second builder joins (ci deploy mode)',
363
- kind: 'infra',
364
- description: 'For a shared instance on ci deploy mode, enable branch protection on main + required checks ' +
365
- '(unit / secret-scan / gate-review), so untrusted builders cannot push straight to production. No review requirement — gates are CI checks, not human approvals (ADR 0162). Skippable for a solo/local instance.',
366
- },
367
- ];
368
- // Production topology is either AUTOMATED (the owner opted into provisioning — the
369
- // control-plane runner stands up TLS/DNS/managed-DB) or a tracked manual step.
370
- // Keep it in its infra slot, before the look step, when it's still manual. (ADR 0111 §3)
371
- if (!provisioningRequest(spec)) tasks.push(productionTopologyKickoff());
372
- tasks.push({
373
- title: 'Fill the instance look with your own LLM',
374
- // 'ideator' is a DISCIPLINE, not a kind — tasks_kind_check rejects it as a
375
- // kind and the task silently vanished from the checklist (the one non-infra
376
- // kickoff step). Deciding the instance look is a decision by an ideator.
377
- kind: 'decision',
378
- discipline: 'ideator',
379
- description: 'init wrote a lean config/branding.json and left the LOOK to you (a locked project rule: brand with your ' +
380
- 'own LLM, not a generator). Hand docs/branding-fill-prompt.md to your AI to design a palette/fonts/wordmark, or keep the vanilla look.',
381
- });
382
- const enabled = new Set(Array.isArray(spec.modules) ? spec.modules : []);
383
- if (enabled.has('art-pipeline')) {
384
- tasks.push({ title: 'Provide a Google Gemini API key for the art pipeline', kind: 'infra',
385
- description: 'The art-pipeline module needs a Google AI Studio (Gemini) key (GOOGLE_AI_STUDIO_KEY, a shared key, or per-builder keys) or it cannot generate art.' });
386
- }
387
- if (enabled.has('discord')) {
388
- tasks.push({ title: 'Provide Discord bot token + webhooks', kind: 'infra',
389
- description: 'The discord module needs a bot token + webhook URLs + DISCORD_BOT_ENABLED=true. Note: outbound/link surfaces fire on credentials even if the module flag is later turned off.' });
390
- }
391
- if (enabled.has('dev-box')) {
392
- tasks.push({ title: 'Provide a DigitalOcean API token for dev boxes', kind: 'infra',
393
- description: 'The dev-box module needs a DigitalOcean API token (and optionally Cloudflare) on the operator control plane before any builder box can be provisioned.' });
394
- }
395
- return tasks;
325
+ return kickoffChecklist(spec, { provisioned: !!provisioningRequest(spec) });
396
326
  }
397
327
 
398
328
  // Normalize a raw spec (from --from or the interview) to canonical shape +
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/test-path-guard.js — a test must not build a filesystem path from
3
+ // `URL.pathname` (task 1002889).
4
+ //
5
+ // THE BUG THIS EXISTS TO STOP. `new URL('..', import.meta.url).pathname` is a URL
6
+ // path, not a filesystem path: percent-encoding survives (a space stays `%20`) and
7
+ // on Windows it carries a leading slash before the drive letter. Handed to
8
+ // require() or fs it resolves nowhere — four tests threw MODULE_NOT_FOUND at load
9
+ // on the owner's checkout, whose path has a space in it.
10
+ //
11
+ // WHY A FITNESS CHECK AND NOT A TEST. CI checks out to a path with no space, so the
12
+ // broken form passes there and the suite reads green while being unrunnable on a
13
+ // Windows dev machine. CI can never catch the regression on its own; a static scan
14
+ // at a different altitude can.
15
+ //
16
+ // THE FIX IT POINTS AT. `fileURLToPath(new URL('..', import.meta.url))` from
17
+ // node:url (tests/fitness.mjs), or a plain relative require through
18
+ // createRequire(import.meta.url) (tests/provisioning_teardown_intent.mjs).
19
+
20
+ 'use strict';
21
+
22
+ const fs = require('node:fs');
23
+ const path = require('node:path');
24
+ const { execFileSync } = require('node:child_process');
25
+
26
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
27
+ const NAME = 'a test builds its repo root with fileURLToPath, never URL.pathname (task 1002889)';
28
+
29
+ // `import.meta.url).pathname` — the URL-path-as-filesystem-path defect, in both its
30
+ // forms: new URL('..', import.meta.url).pathname and new URL(import.meta.url).pathname.
31
+ const PATHNAME_ROOT = /import\.meta\.url\s*\)\s*\.pathname\b/;
32
+
33
+ // Every tracked test file. Only tests use import.meta.url this way; source is CJS.
34
+ function trackedTestFiles() {
35
+ try {
36
+ const out = execFileSync('git', ['ls-files', 'tests/*.mjs', 'tests/*.js', 'tests/**/*.mjs', 'tests/**/*.js'],
37
+ { cwd: REPO_ROOT, encoding: 'utf8' });
38
+ return [...new Set(out.split('\n').map((s) => s.trim()).filter(Boolean))];
39
+ } catch {
40
+ return [];
41
+ }
42
+ }
43
+
44
+ function scanTests(files, read) {
45
+ const violations = [];
46
+ let scanned = 0;
47
+ for (const rel of files) {
48
+ const src = read(rel);
49
+ if (src == null) continue;
50
+ scanned += 1;
51
+ const lines = src.split('\n');
52
+ for (let i = 0; i < lines.length; i += 1) {
53
+ if (!PATHNAME_ROOT.test(lines[i])) continue;
54
+ violations.push(
55
+ `${rel}:${i + 1} builds a filesystem path from URL.pathname — a space becomes %20 and a Windows `
56
+ + 'drive gains a leading slash, so require()/fs throw MODULE_NOT_FOUND at load. Use '
57
+ + "fileURLToPath(new URL('..', import.meta.url)) from node:url, or a relative require via "
58
+ + 'createRequire(import.meta.url).',
59
+ );
60
+ }
61
+ }
62
+ return { violations, scanned };
63
+ }
64
+
65
+ function checkTestsUseFileUrlToPath({ files, read } = {}) {
66
+ const list = files || trackedTestFiles();
67
+ if (!list.length) {
68
+ return {
69
+ name: NAME, ok: false, hardFail: true,
70
+ violations: ['scan defect — enumerated 0 tracked test files; a broken enumeration, not a clean result.'],
71
+ warnings: [], note: 'static scan of tests/ for a URL.pathname used as a filesystem path.',
72
+ };
73
+ }
74
+ const reader = read || ((rel) => {
75
+ try { return fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8'); } catch { return null; }
76
+ });
77
+ const { violations, scanned } = scanTests(list, reader);
78
+ return {
79
+ name: NAME,
80
+ ok: violations.length === 0,
81
+ hardFail: violations.length > 0,
82
+ violations,
83
+ warnings: [],
84
+ note: `${scanned} test file(s) scanned: none may hand \`import.meta.url).pathname\` to require()/fs — `
85
+ + 'it keeps %20 and a Windows drive slash, so the file is unrunnable on a checkout with a space '
86
+ + 'in its path while CI (no space) stays green. fileURLToPath() or a relative require is the form.',
87
+ };
88
+ }
89
+
90
+ function main() {
91
+ const r = checkTestsUseFileUrlToPath();
92
+ for (const v of r.violations) console.error(`✗ ${v}`);
93
+ console.log(`${r.hardFail ? 'FAIL' : 'PASS'} ${r.note}`);
94
+ process.exit(r.hardFail ? 1 : 0);
95
+ }
96
+
97
+ if (require.main === module) main();
98
+
99
+ module.exports = { checkTestsUseFileUrlToPath, scanTests, trackedTestFiles, PATHNAME_ROOT };
@@ -261,6 +261,7 @@ async function maybeSeatFirstAdmin(builder) {
261
261
  const seated = await seatFirstAdminIfUnclaimed(builder.id, builder.github_login);
262
262
  if (seated) {
263
263
  log.info(`[auth] first-admin bootstrap: seated ${builder.github_login} as Archon (configured firstAdmin, no prior Archon).`);
264
+ await seedKickoffChecklist(builder.id);
264
265
  return { ...builder, rank: 'archon' };
265
266
  }
266
267
  } catch (err) {
@@ -269,6 +270,27 @@ async function maybeSeatFirstAdmin(builder) {
269
270
  return builder;
270
271
  }
271
272
 
273
+ // seedKickoffChecklist — the founder's first board (task 1003239). A hub-provisioned
274
+ // project reaches its first sign-in with an EMPTY tasks table (the scaffold ran init
275
+ // --no-seed; no Archon existed to file anything), so the moment the founder is seated
276
+ // is the first moment the kickoff checklist CAN be filed. Resolved through the
277
+ // `lifecycle.kickoffSeed` port — this is a KERNEL_FILE and may not require a module
278
+ // (ADR 0083 / ADR 0091 §1); the port itself skips a board that already has tasks.
279
+ // BEST-EFFORT: a seed hiccup must never fail the sign-in that just seated the owner.
280
+ // deps.port is a test seam; production resolves the seam registry.
281
+ async function seedKickoffChecklist(builderId, deps = {}) {
282
+ const port = deps.port !== undefined ? deps.port : seams.resolveOptional('lifecycle.kickoffSeed');
283
+ if (!port) return null;
284
+ try {
285
+ const r = await port.seedIfEmpty({ builderId });
286
+ if (r && r.seeded) log.info(`[auth] first-admin bootstrap: filed ${r.seeded} kickoff task(s) under ${r.versionId} — the founder's first board.`);
287
+ return r;
288
+ } catch (err) {
289
+ log.error(`[auth] first-admin bootstrap: kickoff seed failed (non-fatal): ${err.message}`);
290
+ return null;
291
+ }
292
+ }
293
+
272
294
  // safeSyncRankRole — give a builder the seeded rank-role matching their rank at
273
295
  // REGISTRATION (BV1.R95b, ADR 0151). governance_002's builder→role backfill is
274
296
  // run-once, so before this every builder admitted after that migration had no role
@@ -303,6 +325,7 @@ module.exports = {
303
325
  firstTimeAdmissionBlocked,
304
326
  joinabilityMode,
305
327
  maybeSeatFirstAdmin,
328
+ seedKickoffChecklist,
306
329
  membershipKind,
307
330
  openEnrollmentEnabled,
308
331
  reportActivityRollup,
package/src/module-api.js CHANGED
@@ -1192,8 +1192,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1192
1192
  // landed since 1.19.374 with no explicit bump. run 32909701598. (task 1002620)
1193
1193
  // 1.19.376 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1194
1194
  // landed since 1.19.375 with no explicit bump. run 32910331206. (task 1002620)
1195
+ // 1.19.377 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1196
+ // landed since 1.19.376 with no explicit bump. run 33025011131. (task 1002620)
1195
1197
  // ---------------------------------------------------------------------------
1196
- const CORE_VERSION = '1.19.376'; // CI auto-patch carrier (ADR 0161); see changelog above
1198
+ const CORE_VERSION = '1.19.377'; // CI auto-patch carrier (ADR 0161); see changelog above
1197
1199
 
1198
1200
  // A namespaced logger so a module's log lines are attributable + consistent.
1199
1201
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,56 @@
1
+ // tests/first_admin_kickoff_seed.mjs — the kernel side of task 1003239: the first-admin
2
+ // bootstrap files the founder's first board through the `lifecycle.kickoffSeed` port,
3
+ // and NOTHING about that can fail the sign-in that just seated the owner.
4
+ //
5
+ // DB-free: the helper is exercised with an injected port (deps.port), the same seam the
6
+ // production call resolves from the registry. Covers the three shapes the port can take
7
+ // at runtime — absent (an instance without the lifecycle module), throwing, seeding.
8
+ //
9
+ // Run: node --test tests/first_admin_kickoff_seed.mjs
10
+
11
+ import assert from 'node:assert/strict';
12
+ import { test } from 'node:test';
13
+ import { createRequire } from 'node:module';
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const { seedKickoffChecklist } = require('../src/bongos/auth-admission.js');
17
+ const seams = require('../src/module-seams.js');
18
+
19
+ test('no port registered → a quiet null, never a throw (an instance without lifecycle signs in as before)', async () => {
20
+ assert.equal(seams.hasProvider('lifecycle.kickoffSeed'), false, 'nothing in-process registered the port');
21
+ assert.equal(await seedKickoffChecklist(1), null);
22
+ });
23
+
24
+ test('an absent port passed explicitly is the same quiet null', async () => {
25
+ assert.equal(await seedKickoffChecklist(1, { port: null }), null);
26
+ });
27
+
28
+ test('a throwing port is swallowed — best-effort, the seat stands', async () => {
29
+ const port = { seedIfEmpty: async () => { throw new Error('db went away'); } };
30
+ assert.equal(await seedKickoffChecklist(1, { port }), null);
31
+ });
32
+
33
+ test('a seeding port is handed the seated builder id and its result flows back', async () => {
34
+ const calls = [];
35
+ const port = { seedIfEmpty: async (arg) => { calls.push(arg); return { seeded: 3, versionId: 'V1' }; } };
36
+ const r = await seedKickoffChecklist(42, { port });
37
+ assert.deepEqual(calls, [{ builderId: 42 }]);
38
+ assert.deepEqual(r, { seeded: 3, versionId: 'V1' });
39
+ });
40
+
41
+ test('a skipping port (board already populated) flows back untouched', async () => {
42
+ const port = { seedIfEmpty: async () => ({ seeded: 0, skipped: 'tasks_exist' }) };
43
+ assert.deepEqual(await seedKickoffChecklist(42, { port }), { seeded: 0, skipped: 'tasks_exist' });
44
+ });
45
+
46
+ test('production resolves the port from the seam registry', async () => {
47
+ const calls = [];
48
+ seams.registerProvider('lifecycle.kickoffSeed', { seedIfEmpty: async (arg) => { calls.push(arg); return { seeded: 1, versionId: 'V1' }; } });
49
+ try {
50
+ const r = await seedKickoffChecklist(9);
51
+ assert.deepEqual(calls, [{ builderId: 9 }]);
52
+ assert.equal(r.seeded, 1);
53
+ } finally {
54
+ seams._reset();
55
+ }
56
+ });
@@ -0,0 +1,107 @@
1
+ // tests/kickoff_checklist.mjs — the shared kickoff checklist (task 1003239).
2
+ //
3
+ // One list, two filers: `bongos init` at scaffold time (self-host) and the lifecycle
4
+ // module at the founder's first sign-in (hub-provisioned, where init ran --no-seed).
5
+ // The first-sign-in shape must contain only what is still TRUE after a hub standup,
6
+ // and must never name a surface that does not exist — the task's own rule: a blank
7
+ // board beats untrue copy.
8
+ //
9
+ // Run: node --test tests/kickoff_checklist.mjs
10
+
11
+ import assert from 'node:assert/strict';
12
+ import { test } from 'node:test';
13
+ import { createRequire } from 'node:module';
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const { kickoffChecklist, productionTopologyKickoff } = require('../modules/lifecycle/kickoff-checklist.js');
17
+ const { VALID_KINDS, VALID_DISCIPLINES } = require('../modules/lifecycle/task-classifier.js');
18
+
19
+ const spec = () => ({
20
+ firstAdmin: 'founder', repo: { owner: 'o', name: 'n' },
21
+ domains: { publicOrigin: 'https://p.example' }, modules: [],
22
+ });
23
+
24
+ test('default (self-host): the seven owner-manual steps, topology included', () => {
25
+ const t = kickoffChecklist(spec());
26
+ assert.equal(t.length, 7);
27
+ assert.ok(t.some((x) => /OAuth/i.test(x.title)), 'OAuth app step');
28
+ assert.ok(t.some((x) => /founding owner/i.test(x.title)), 'first sign-in step');
29
+ assert.ok(t.some((x) => /repo binding/i.test(x.title)), 'repo binding step');
30
+ assert.ok(t.some((x) => /Production topology/i.test(x.title)), 'topology step');
31
+ assert.ok(t.every((x) => x.title && x.description), 'every step has a title + description');
32
+ });
33
+
34
+ test('provisioned: only the topology step is omitted', () => {
35
+ const t = kickoffChecklist(spec(), { provisioned: true });
36
+ assert.equal(t.length, 6);
37
+ assert.ok(!t.some((x) => /Production topology/i.test(x.title)));
38
+ assert.ok(t.some((x) => /OAuth/i.test(x.title)), 'the self-host owner still registers their OAuth app');
39
+ });
40
+
41
+ test('firstSignIn: only what is still true after a hub standup', () => {
42
+ const t = kickoffChecklist(spec(), { firstSignIn: true });
43
+ assert.deepEqual(
44
+ t.map((x) => x.title),
45
+ [
46
+ 'Decide how people join, and invite the first builders',
47
+ 'Arm the merge gate before a second builder joins (ci deploy mode)',
48
+ 'Fill the instance look with your own LLM',
49
+ ],
50
+ );
51
+ for (const x of t) {
52
+ assert.ok(!/OAuth|founding owner|repo binding|topology/i.test(x.title + x.description),
53
+ `"${x.title}" names a step the standup already did`);
54
+ }
55
+ });
56
+
57
+ test('firstSignIn: the admission step points at surfaces that exist, never an env var', () => {
58
+ const join = kickoffChecklist(spec(), { firstSignIn: true })[0];
59
+ assert.ok(!/ENVPREFIX|OPEN_XENOS_ENROLLMENT|config\//.test(join.description), 'no env var or config file a hub founder cannot reach');
60
+ assert.match(join.description, /How people join/, 'the hub setting (task 1003044)');
61
+ assert.match(join.description, /manage page/, 'where the setting lives');
62
+ assert.match(join.description, /Watch page/, 'where invites live');
63
+ assert.match(join.description, /GitHub username/, 'how an invite is addressed');
64
+ });
65
+
66
+ test('firstSignIn: per-module keys still follow the enabled modules', () => {
67
+ const t = kickoffChecklist({ ...spec(), modules: ['art-pipeline', 'discord'] }, { firstSignIn: true });
68
+ assert.equal(t.length, 5);
69
+ assert.ok(t.some((x) => /Gemini/i.test(x.title)));
70
+ assert.ok(t.some((x) => /Discord/i.test(x.title)));
71
+ });
72
+
73
+ test('firstSignIn: never reads spec.repo or spec.firstAdmin (the steps that need them are omitted)', () => {
74
+ assert.doesNotThrow(() => kickoffChecklist({}, { firstSignIn: true }));
75
+ assert.equal(kickoffChecklist({}, { firstSignIn: true }).length, 3);
76
+ });
77
+
78
+ test('every variant satisfies the DB CHECK vocabulary (tasks_kind_check / tasks_discipline_check)', () => {
79
+ const full = { ...spec(), modules: ['art-pipeline', 'discord', 'dev-box'] };
80
+ const all = [
81
+ ...kickoffChecklist(full),
82
+ ...kickoffChecklist(full, { provisioned: true }),
83
+ ...kickoffChecklist(full, { firstSignIn: true }),
84
+ productionTopologyKickoff(),
85
+ ];
86
+ for (const t of all) {
87
+ assert.ok(VALID_KINDS.has(t.kind), `"${t.title}": kind '${t.kind}' violates tasks_kind_check`);
88
+ if (t.discipline !== undefined) {
89
+ assert.ok(VALID_DISCIPLINES.has(t.discipline), `"${t.title}": discipline '${t.discipline}' violates tasks_discipline_check`);
90
+ }
91
+ }
92
+ });
93
+
94
+ test('the look step is a decision BY an ideator in every variant', () => {
95
+ for (const opts of [{}, { provisioned: true }, { firstSignIn: true }]) {
96
+ const look = kickoffChecklist(spec(), opts).find((t) => /instance look/i.test(t.title));
97
+ assert.ok(look, `look step present for ${JSON.stringify(opts)}`);
98
+ assert.equal(look.kind, 'decision');
99
+ assert.equal(look.discipline, 'ideator');
100
+ }
101
+ });
102
+
103
+ test('returns fresh objects — a filer mutating one cannot poison the next call', () => {
104
+ const a = kickoffChecklist(spec(), { firstSignIn: true });
105
+ a[0].title = 'mutated';
106
+ assert.notEqual(kickoffChecklist(spec(), { firstSignIn: true })[0].title, 'mutated');
107
+ });
@@ -0,0 +1,93 @@
1
+ // tests/kickoff_seed.mjs — the lifecycle module files the kickoff checklist at the
2
+ // founder's first sign-in, and only onto an EMPTY board (task 1003239).
3
+ //
4
+ // The seed is idempotent by emptiness, not by path: the self-host path already filed
5
+ // its checklist before anyone signed in, so it must be skipped there without any
6
+ // "was I provisioned" marker. Every case runs against an in-memory pool + insert —
7
+ // no database.
8
+ //
9
+ // Run: node --test tests/kickoff_seed.mjs
10
+
11
+ import assert from 'node:assert/strict';
12
+ import { test } from 'node:test';
13
+ import { createRequire } from 'node:module';
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const { seedKickoffIfEmpty, specFromBranding } = require('../modules/lifecycle/kickoff-seed.js');
17
+ const buildRouter = require('../modules/lifecycle/routes/kickoff-seed.js');
18
+ const api = require('../src/module-api.js');
19
+
20
+ const spec = { firstAdmin: 'founder', repo: { owner: 'o', name: 'n' }, domains: { publicOrigin: 'https://p.example' }, modules: [] };
21
+
22
+ function fakePool({ taskCount, versions }) {
23
+ const queries = [];
24
+ return {
25
+ queries,
26
+ query: async (sql) => {
27
+ queries.push(sql);
28
+ if (/FROM tasks/.test(sql)) return { rows: [{ n: taskCount }] };
29
+ if (/FROM versions/.test(sql)) return { rows: versions };
30
+ throw new Error(`unexpected query: ${sql}`);
31
+ },
32
+ };
33
+ }
34
+
35
+ test('an empty board gets the first-sign-in checklist, as ready tasks attributed to the founder', async () => {
36
+ const inserted = [];
37
+ const pool = fakePool({ taskCount: 0, versions: [{ id: 'V1' }] });
38
+ const r = await seedKickoffIfEmpty({ builderId: 7 }, { pool, spec, createTask: async (row) => { inserted.push(row); return row; } });
39
+ assert.deepEqual(r, { seeded: 3, versionId: 'V1' });
40
+ assert.equal(inserted.length, 3);
41
+ for (const row of inserted) {
42
+ assert.equal(row.versionId, 'V1');
43
+ assert.equal(row.status, 'ready', 'claimable from the first board, not backlog');
44
+ assert.equal(row.source, 'first-sign-in');
45
+ assert.equal(row.createdBy, 7);
46
+ assert.ok(row.title && row.description);
47
+ }
48
+ assert.match(inserted[0].title, /how people join/i);
49
+ });
50
+
51
+ test('a board that already has tasks is left alone (the self-host path seeded at init)', async () => {
52
+ const inserted = [];
53
+ const pool = fakePool({ taskCount: 12, versions: [{ id: 'V1' }] });
54
+ const r = await seedKickoffIfEmpty({ builderId: 7 }, { pool, spec, createTask: async (row) => inserted.push(row) });
55
+ assert.deepEqual(r, { seeded: 0, skipped: 'tasks_exist' });
56
+ assert.equal(inserted.length, 0);
57
+ assert.ok(!pool.queries.some((q) => /FROM versions/.test(q)), 'never even looks for a version');
58
+ });
59
+
60
+ test('no version to file under → skipped, nothing inserted', async () => {
61
+ const inserted = [];
62
+ const pool = fakePool({ taskCount: 0, versions: [] });
63
+ const r = await seedKickoffIfEmpty({ builderId: 7 }, { pool, spec, createTask: async (row) => inserted.push(row) });
64
+ assert.deepEqual(r, { seeded: 0, skipped: 'no_version' });
65
+ assert.equal(inserted.length, 0);
66
+ });
67
+
68
+ test('the version query prefers the building one, then the first by id', async () => {
69
+ const pool = fakePool({ taskCount: 0, versions: [{ id: 'V1' }] });
70
+ await seedKickoffIfEmpty({ builderId: 1 }, { pool, spec, createTask: async () => {} });
71
+ const q = pool.queries.find((s) => /FROM versions/.test(s));
72
+ assert.match(q, /status = 'building'/);
73
+ assert.match(q, /LIMIT 1/);
74
+ });
75
+
76
+ test('specFromBranding reads the instance pack and keeps modules empty', () => {
77
+ const s = specFromBranding({ firstAdmin: 'f', repo: { owner: 'a', name: 'b' }, domains: { publicOrigin: 'https://x' } });
78
+ assert.deepEqual(s, { firstAdmin: 'f', repo: { owner: 'a', name: 'b' }, domains: { publicOrigin: 'https://x' }, modules: [] });
79
+ assert.deepEqual(specFromBranding({}).repo, {});
80
+ });
81
+
82
+ test('the route factory registers the port once, returns an empty router, and the port delegates', async () => {
83
+ const before = api.hasProvider('lifecycle.kickoffSeed');
84
+ const router = buildRouter();
85
+ buildRouter(); // a rebuild must not trip the single-provider guard
86
+ assert.ok(api.hasProvider('lifecycle.kickoffSeed'));
87
+ assert.equal(typeof router, 'function');
88
+ assert.equal(router.stack.length, 0, 'seeding has no HTTP surface');
89
+ const port = api.resolveOptional('lifecycle.kickoffSeed');
90
+ const r = await port.seedIfEmpty({ builderId: 3 }, { pool: fakePool({ taskCount: 1, versions: [] }), spec });
91
+ assert.deepEqual(r, { seeded: 0, skipped: 'tasks_exist' });
92
+ assert.equal(before, false, 'this test registered it, nothing else in-process had');
93
+ });
@@ -21,9 +21,8 @@ import { createRequire } from 'node:module';
21
21
  import http from 'node:http';
22
22
 
23
23
  const require = createRequire(import.meta.url);
24
- const ROOT = new URL('..', import.meta.url).pathname;
25
24
 
26
- const provisioning = require(`${ROOT}/modules/provisioning/provisioning.js`);
25
+ const provisioning = require('../modules/provisioning/provisioning.js');
27
26
 
28
27
  let passed = 0, failed = 0;
29
28
  function t(name, fn) {
@@ -99,11 +98,11 @@ provisioning.enqueueIntent = async () => ({ intent: { id: 1, state: 'pending' },
99
98
  provisioning.recordEvent = async () => {};
100
99
  provisioning.getInstanceBySlug = async () => null;
101
100
 
102
- const api = require(`${ROOT}/src/module-api.js`);
101
+ const api = require('../src/module-api.js');
103
102
  api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
104
103
 
105
104
  const express = require('express');
106
- const provisioningRoutes = require(`${ROOT}/modules/provisioning/routes/provisioning.js`);
105
+ const provisioningRoutes = require('../modules/provisioning/routes/provisioning.js');
107
106
  const app = express();
108
107
  app.use(express.json());
109
108
  app.use((_req, res, next) => {
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { strict as assert } from 'node:assert';
13
13
  import { createRequire } from 'node:module';
14
+ import { fileURLToPath } from 'node:url';
14
15
 
15
16
  const require = createRequire(import.meta.url);
16
17
  const ingest = require('../scripts/gds/search-ingest-md.js');
@@ -61,7 +62,7 @@ test('SENSITIVE_DOC_GLOBS is the curated source of truth (subtrees end with /)',
61
62
 
62
63
  test('collectMarkdown finds the repo corpus but excludes node_modules/.git/worktrees/public', () => {
63
64
  const path = require('node:path');
64
- const repoRoot = path.resolve(new URL('..', import.meta.url).pathname);
65
+ const repoRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
65
66
  const real = collectMarkdown(repoRoot, repoRoot);
66
67
  // Assert on paths RELATIVE to the repo root — the suite may run from inside a
67
68
  // git worktree (…/.claude/worktrees/<name>/…), so the ABSOLUTE paths would all
@@ -77,7 +78,7 @@ test('collectMarkdown finds the repo corpus but excludes node_modules/.git/workt
77
78
 
78
79
  test('chunkFile yields well-formed rows (source_kind doc, hash, rank, sequential index)', () => {
79
80
  const path = require('node:path');
80
- const repoRoot = path.resolve(new URL('..', import.meta.url).pathname);
81
+ const repoRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
81
82
  const rows = chunkFile(path.join(repoRoot, 'docs', 'adr', '0016-trust-boundary-server-enforced-permissions.md'));
82
83
  assert.ok(rows.length > 0);
83
84
  rows.forEach((r, i) => {
@@ -22,8 +22,9 @@ import { spawnSync } from 'node:child_process';
22
22
  import fs from 'node:fs';
23
23
  import os from 'node:os';
24
24
  import path from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
25
26
 
26
- const REPO_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
27
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
27
28
  const SETUP_JS = path.join(REPO_ROOT, 'scripts/gds/setup.js');
28
29
 
29
30
  const tests = [];
@@ -36,7 +37,8 @@ function makeFreshHome() {
36
37
 
37
38
  function runSetup(args, env = {}) {
38
39
  return spawnSync('node', [SETUP_JS, ...args], {
39
- env: { ...process.env, ...env },
40
+ // os.homedir() reads USERPROFILE on Windows, so a HOME override alone leaks the real home there.
41
+ env: { ...process.env, ...env, ...(env.HOME ? { USERPROFILE: env.HOME } : {}) },
40
42
  encoding: 'utf8',
41
43
  timeout: 15000,
42
44
  });
@@ -0,0 +1,126 @@
1
+ // tests/test_path_guard.mjs — the guard that keeps a test runnable on a checkout
2
+ // whose path has a space in it (task 1002889).
3
+ //
4
+ // Four tests built their repo root from URL.pathname and threw MODULE_NOT_FOUND at
5
+ // load on the owner's Windows checkout, while CI (no space in its path) stayed
6
+ // green. The guard is tested on synthetic sources, not only the live tree: a scan
7
+ // that passes because the tree is clean today proves nothing about what it REFUSES.
8
+ //
9
+ // The defective form is spelled by concatenation below so this file never trips the
10
+ // guard it tests.
11
+ //
12
+ // Of the four files the task named, tests/provisioning_github_app_routes.mjs had
13
+ // already been converted before this landed (915d273b, task 1003236 — fileURLToPath
14
+ // at its line 17), so it is absent from this change on purpose; the live-tree case
15
+ // at the bottom is what proves all four are clean.
16
+ //
17
+ // Run: node tests/test_path_guard.mjs
18
+
19
+ import { strict as assert } from 'node:assert';
20
+ import { createRequire } from 'node:module';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const { checkTestsUseFileUrlToPath, scanTests } = require('../scripts/gds/test-path-guard.js');
24
+
25
+ let passed = 0;
26
+ let failed = 0;
27
+ function test(name, fn) {
28
+ try { fn(); passed++; console.log(` ok ${name}`); }
29
+ catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
30
+ }
31
+
32
+ const from = (map) => ({
33
+ files: Object.keys(map),
34
+ read: (rel) => (rel in map ? map[rel] : null),
35
+ });
36
+
37
+ const BAD_TAIL = 'import.meta.url)' + '.pathname';
38
+
39
+ // --- THE DEFECT ------------------------------------------------------------
40
+
41
+ test('the shipped defect is refused, with its line and the fix', () => {
42
+ const { violations } = scanTests(...Object.values(from({
43
+ 'tests/bad.mjs':
44
+ "import { createRequire } from 'node:module';\n"
45
+ + `const ROOT = new URL('..', ${BAD_TAIL};\n`
46
+ + 'const mod = require(`${ROOT}/modules/x.js`);\n',
47
+ })));
48
+ assert.equal(violations.length, 1);
49
+ assert.match(violations[0], /bad\.mjs:2/, 'names the file and line');
50
+ assert.match(violations[0], /%20/, 'says WHY it breaks');
51
+ assert.match(violations[0], /fileURLToPath/, 'and what to write instead');
52
+ });
53
+
54
+ test('the dirname form is the same defect', () => {
55
+ const { violations } = scanTests(...Object.values(from({
56
+ 'tests/smoke.mjs': `const REPO_ROOT = path.resolve(path.dirname(new URL(${BAD_TAIL}), '..');\n`,
57
+ })));
58
+ assert.equal(violations.length, 1);
59
+ });
60
+
61
+ test('every site in a file is reported, not just the first', () => {
62
+ const { violations } = scanTests(...Object.values(from({
63
+ 'tests/two.mjs':
64
+ `const a = path.resolve(new URL('..', ${BAD_TAIL});\n`
65
+ + 'x();\n'
66
+ + `const b = path.resolve(new URL('..', ${BAD_TAIL});\n`,
67
+ })));
68
+ assert.equal(violations.length, 2);
69
+ assert.match(violations[1], /two\.mjs:3/);
70
+ });
71
+
72
+ // --- WHAT IT MUST NOT FLAG -------------------------------------------------
73
+
74
+ test('fileURLToPath and a relative createRequire are the correct forms — never flagged', () => {
75
+ const { violations } = scanTests(...Object.values(from({
76
+ 'tests/good_a.mjs':
77
+ "import { fileURLToPath } from 'node:url';\n"
78
+ + "const ROOT = fileURLToPath(new URL('..', import.meta.url));\n",
79
+ 'tests/good_b.mjs':
80
+ "const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');\n",
81
+ 'tests/good_c.mjs':
82
+ "const require = createRequire(import.meta.url);\n"
83
+ + "const mod = require('../modules/x.js');\n",
84
+ })));
85
+ assert.deepEqual(violations, []);
86
+ });
87
+
88
+ test('a request URL parsed for its pathname is not the defect', () => {
89
+ // http tests routinely do this; the guard is about import.meta.url only.
90
+ const { violations } = scanTests(...Object.values(from({
91
+ 'tests/routes.mjs': "const { pathname } = new URL(req.url, 'http://x');\nconst p = new URL(req.url, base).pathname;\n",
92
+ })));
93
+ assert.deepEqual(violations, []);
94
+ });
95
+
96
+ // --- THE SCAN MUST NOT SILENTLY SUCCEED ------------------------------------
97
+
98
+ test('an empty enumeration is a hard FAIL, not a clean pass', () => {
99
+ const r = checkTestsUseFileUrlToPath({ files: [], read: () => null });
100
+ assert.equal(r.hardFail, true);
101
+ assert.match(r.violations[0], /scan defect/);
102
+ });
103
+
104
+ test('an unreadable file is skipped without being counted as scanned', () => {
105
+ const { scanned } = scanTests(['gone.mjs'], () => null);
106
+ assert.equal(scanned, 0);
107
+ });
108
+
109
+ test('the check reports hardFail so CI actually stops', () => {
110
+ const r = checkTestsUseFileUrlToPath(from({
111
+ 'tests/bad.mjs': `const ROOT = new URL('..', ${BAD_TAIL};\n`,
112
+ }));
113
+ assert.equal(r.ok, false);
114
+ assert.equal(r.hardFail, true);
115
+ });
116
+
117
+ // --- THE LIVE TREE ---------------------------------------------------------
118
+
119
+ test('the tracked tests/ tree is clean (the four sites of task 1002889 are fixed)', () => {
120
+ const r = checkTestsUseFileUrlToPath();
121
+ assert.equal(r.hardFail, false, r.violations.join('\n'));
122
+ assert.ok(/^\d+ test file\(s\) scanned/.test(r.note) && !r.note.startsWith('0 '), r.note);
123
+ });
124
+
125
+ console.log(`\ntest_path_guard: ${passed} passed, ${failed} failed`);
126
+ process.exit(failed ? 1 : 0);