@bongos/core 1.19.315 → 1.19.317

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.bongos-core.json +104 -29
  2. package/.claude/skills/merge-mode/SKILL.md +16 -0
  3. package/.claude/skills/planning-session/SKILL.md +10 -0
  4. package/modules/lifecycle/db-analytics.js +719 -0
  5. package/modules/lifecycle/db-claim-reads.js +165 -0
  6. package/modules/lifecycle/db-claims.js +934 -0
  7. package/modules/lifecycle/db-deps-criteria.js +441 -0
  8. package/modules/lifecycle/db-goals.js +763 -0
  9. package/modules/lifecycle/db-grade.js +233 -0
  10. package/modules/lifecycle/db-overrides.js +181 -0
  11. package/modules/lifecycle/db-rank-authz.js +446 -0
  12. package/modules/lifecycle/db-shared.js +115 -0
  13. package/modules/lifecycle/db-ship.js +994 -0
  14. package/modules/lifecycle/db-tasks.js +810 -0
  15. package/modules/lifecycle/db-versions.js +118 -0
  16. package/modules/lifecycle/db.js +167 -5564
  17. package/package-lock.json +2 -2
  18. package/package.json +1 -1
  19. package/scripts/gds/coreB-carve-proof.sh +18 -9
  20. package/scripts/gds/fitness.js +35 -27
  21. package/scripts/gds/identity-strip.js +75 -0
  22. package/scripts/gds/preflight.js +25 -2
  23. package/scripts/gds/provision.js +7 -3
  24. package/scripts/gds/skill-preflight.js +336 -0
  25. package/src/module-api.js +5 -1
  26. package/tests/auto_satisfy_criteria.mjs +3 -2
  27. package/tests/dependencies_migration.mjs +3 -2
  28. package/tests/fitness.mjs +43 -0
  29. package/tests/goal_abuse_matrix.mjs +3 -2
  30. package/tests/goal_advisory.mjs +3 -2
  31. package/tests/government_claim_eligibility.mjs +3 -1
  32. package/tests/government_rank_sync.mjs +9 -2
  33. package/tests/helpers.mjs +19 -0
  34. package/tests/migration_allocation.mjs +3 -1
  35. package/tests/publish_reconciler.mjs +3 -1
  36. package/tests/rank_gate.mjs +3 -1
  37. package/tests/reward_gate.mjs +3 -1
  38. package/tests/skill_preflight.mjs +251 -0
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.315",
3
+ "version": "1.19.317",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.315",
9
+ "version": "1.19.317",
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.315",
3
+ "version": "1.19.317",
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",
@@ -50,6 +50,13 @@
50
50
  # FULL_SUITE=1 bash scripts/gds/coreB-carve-proof.sh # also run the full CI gate
51
51
  # SKIP_SUITE=1 bash scripts/gds/coreB-carve-proof.sh # legs 1-4 only (fast)
52
52
 
53
+ # task 1003204 (goal 1000079): the lifecycle data layer is no longer one file —
54
+ # modules/lifecycle/db.js is a re-export facade over twelve db-*.js units. Every
55
+ # proof below that greps "the lifecycle db" must scan the FAMILY, or it silently
56
+ # stops covering code that merely moved between units (and can even pass off a
57
+ # doc-comment in the facade, which is exactly what happened before this fix).
58
+ LIFECYCLE_DB="modules/lifecycle/db.js $(ls modules/lifecycle/db-*.js 2>/dev/null | tr '\n' ' ')"
59
+
53
60
  set -uo pipefail
54
61
  cd "$(dirname "$0")/../.." # repo root (consistent with portability-smoke-*.sh)
55
62
 
@@ -191,10 +198,12 @@ grep -q "registerProvider('reward'" modules/economy/routes/reward.js 2>/dev/null
191
198
  && ok "economy registers the 'reward' provider at boot (modules/economy/routes/reward.js)" \
192
199
  || bad "economy does not registerProvider('reward')"
193
200
  # the lifecycle db slice carved to modules/lifecycle/db.js at R86 — it resolves the reward port there.
194
- if grep -q "resolveOptional('reward')" modules/lifecycle/db.js 2>/dev/null; then
195
- ok "the lifecycle resolves economy via the kernel port (seams.resolveOptional('reward') in modules/lifecycle/db.js)"
201
+ # task 1003204 split that slice into db.js + db-*.js, so scan the whole family: the
202
+ # port call now lives in the unit that owns the transaction, not in the facade.
203
+ if grep -q "resolveOptional('reward')" $LIFECYCLE_DB 2>/dev/null; then
204
+ ok "the lifecycle resolves economy via the kernel port (seams.resolveOptional('reward') in the lifecycle db family)"
196
205
  else
197
- bad "the lifecycle does not resolve 'reward' via the seam in modules/lifecycle/db.js"
206
+ bad "the lifecycle does not resolve 'reward' via the seam in the modules/lifecycle db family"
198
207
  fi
199
208
  # no core file requires the moved economy files directly (llm-pricing stays core).
200
209
  if grep -rnE "require\(['\"][^'\"]*/(cost|streak|achievements|cost-summary-shape)['\"]\)" src/ server.js 2>/dev/null | grep -v node_modules | grep -v module-api | grep -v 'modules/economy' | grep -qv llm-pricing; then
@@ -352,11 +361,11 @@ grep -q "registerProvider('onboarding'" modules/onboarding/routes/onboarding.js
352
361
  || bad "onboarding does not registerProvider('onboarding')"
353
362
  # the lifecycle's claim/ship carved to modules/lifecycle/db.js at R86 (it ticks the
354
363
  # onboarding stage there); core auth.js (the auth callbacks) stays and resolves it too.
355
- if grep -q "resolveOptional('onboarding')" modules/lifecycle/db.js 2>/dev/null \
364
+ if grep -q "resolveOptional('onboarding')" $LIFECYCLE_DB 2>/dev/null \
356
365
  && grep -q "resolveOptional('onboarding')" src/bongos/auth.js 2>/dev/null; then
357
- ok "the lifecycle + core resolve onboarding via the kernel port (resolveOptional('onboarding') in modules/lifecycle/db.js + core auth.js)"
366
+ ok "the lifecycle + core resolve onboarding via the kernel port (resolveOptional('onboarding') in the lifecycle db family + core auth.js)"
358
367
  else
359
- bad "the lifecycle or core does not resolve 'onboarding' via the seam (modules/lifecycle/db.js + auth.js)"
368
+ bad "the lifecycle or core does not resolve 'onboarding' via the seam (lifecycle db family + auth.js)"
360
369
  fi
361
370
  # no core file requires a moved onboarding domain file directly. The match is
362
371
  # scoped to the moved domain basenames; the onboarding CLIs (setup/onboarding-
@@ -590,10 +599,10 @@ if grep -qE "^async function (claimTask|shipTask|createTask|resolveClaim)\b" src
590
599
  else
591
600
  ok "the lifecycle state-machine SQL left src/bongos/db.js — the residue is the kernel identity/auth/twin slice (ADR 0093 §3)"
592
601
  fi
593
- if grep -qE "async function (claimTask|shipTask|createTask)\b" modules/lifecycle/db.js 2>/dev/null; then
594
- ok "the lifecycle state-machine SQL now lives in modules/lifecycle/db.js (claim/ship/createTask/…)"
602
+ if grep -qE "async function (claimTask|shipTask|createTask)\b" $LIFECYCLE_DB 2>/dev/null; then
603
+ ok "the lifecycle state-machine SQL now lives in the modules/lifecycle db family (claim/ship/createTask/…)"
595
604
  else
596
- bad "the lifecycle state-machine SQL not found in modules/lifecycle/db.js"
605
+ bad "the lifecycle state-machine SQL not found in the modules/lifecycle db family"
597
606
  fi
598
607
  # lifecycle registers the `lifecycle` port; core read routes + cascade-dispatch resolve it.
599
608
  grep -q "registerProvider('lifecycle'" modules/lifecycle/routes/lifecycle.js 2>/dev/null \
@@ -971,8 +971,12 @@ const PROMPT_IDENTITY_LITERALS = [
971
971
  // 1002201 sweep replaced that literal everywhere else. The straggler surfaced as a
972
972
  // 401 that locked a builder out of re-authenticating — a valid token verified
973
973
  // against the wrong host — and nothing in CI could see it (idea 1000564).
974
- const IDENTITY_SCAN_ROOTS = ['src/bongos', 'modules', 'scripts/gds'];
975
- const IDENTITY_SCAN_EXTRA_FILES = ['scripts/gds/architect-audit.js'];
974
+ // Widened scripts/gds → ALL of scripts/ at task 1002756: migrate.sh's hardcoded
975
+ // founder DB default (task 1002755) survived every gate because the detector never
976
+ // saw the file — a gate that cannot see the file class the bug lives in is worse
977
+ // than no gate, because it is trusted.
978
+ const IDENTITY_SCAN_ROOTS = ['src/bongos', 'modules', 'scripts'];
979
+ const IDENTITY_SCAN_EXTRA_FILES = [];
976
980
  // PERMANENT, COUNTED carve-outs for the scripts/gds root — {file: allowed hits}.
977
981
  //
978
982
  // Widening the root (task 1002427) surfaced 23 files. Task 1002428 swept them:
@@ -1010,7 +1014,15 @@ const IDENTITY_SCAN_EXTRA_FILES = ['scripts/gds/architect-audit.js'];
1010
1014
  // units on that box invoke these by path. Re-pointing an instance's backups is
1011
1015
  // deployment work, not a branding read. Their COUNT is still frozen here, so a
1012
1016
  // NEW literal in any of them fails exactly like one in an unlisted file.
1017
+ // * fitness.js / package-core.js — THE DETECTORS THEMSELVES: Check 13's regexes
1018
+ // and package-core's PRIOR_INSTANCE_FLOOR name the brand because hunting it is
1019
+ // their job. * seed-v3-tasks.js — the seeded-task-prose rationale above. All
1020
+ // three passed pre-1002756 only by the old stripper's accidents (phantom
1021
+ // blocks + regex-blindness); now they are seen and counted.
1013
1022
  const IDENTITY_SCAN_ALLOWED = {
1023
+ 'scripts/gds/fitness.js': 2,
1024
+ 'scripts/gds/package-core.js': 1,
1025
+ 'scripts/gds/seed-v3-tasks.js': 14,
1014
1026
  'scripts/gds/audit-replay.sh': 1,
1015
1027
  'scripts/gds/db-backup-nightly.sh': 1,
1016
1028
  'scripts/gds/db-backup-offsite.sh': 2,
@@ -1041,28 +1053,10 @@ const IDENTITY_SCAN_EXCLUDE = ['modules/game/'];
1041
1053
  // Directories with no authored source to gate.
1042
1054
  const IDENTITY_SCAN_SKIP_DIRS = new Set(['node_modules', '.git', 'generated', 'iterations', 'references']);
1043
1055
 
1044
- // Strip comments so a rationale comment naming the old brand never trips the scan.
1045
- // The JS // strip is `://`-safe (keeps the char before `//` unless it's `:`), so a
1046
- // hardcoded "https://amazonprimea.com" URL is NOT truncated and still gets caught.
1047
- function stripCommentsForIdentity(text, ext) {
1048
- let t = String(text == null ? '' : text);
1049
- if (ext === '.html') return t.replace(/<!--[\s\S]*?-->/g, '');
1050
- // Shell has no block comments, so it returns BEFORE the /* */ strip below (a
1051
- // `"$TMP"/*` glob would otherwise eat real code). Only FULL-LINE `#` comments
1052
- // (and the shebang) are stripped: cutting from an inline `#` would truncate
1053
- // `${v#p}` / `$#` lines and could hide a literal sitting after them.
1054
- if (ext === '.sh') return t.split(/\r?\n/).map((l) => (/^\s*#/.test(l) ? '' : l)).join('\n');
1055
- t = t.replace(/\/\*[\s\S]*?\*\//g, ''); // block comments (js + css)
1056
- // Split on /\r?\n/, NOT '\n' (task 1002428). On a CRLF checkout the old split
1057
- // left a trailing \r on every line, and since `.` does not match \r the `.*$`
1058
- // in the line-comment pattern could never reach end-of-string — so NO line
1059
- // comment was stripped and every rationale comment naming the old brand was
1060
- // scanned as if it were code. That silently broke this function's whole
1061
- // contract ("a rationale comment naming the old brand never trips the scan")
1062
- // on Windows, and made the check disagree with Linux CI on the same tree.
1063
- if (ext === '.js') t = t.split(/\r?\n/).map((l) => l.replace(/(^|[^:])\/\/.*$/, '$1')).join('\n');
1064
- return t;
1065
- }
1056
+ // Comment stripping lives in identity-strip.js (task 1002756: extracted + replaced
1057
+ // the two-regex JS strip, which a `/*` inside a `//` comment could OPEN — it
1058
+ // silently hid ~330 provision.js lines from this gate). Re-exported for the tests.
1059
+ const { stripCommentsForIdentity } = require('./identity-strip');
1066
1060
 
1067
1061
  // Pure: scan one file's comment-stripped source for a hardcoded host-brand literal.
1068
1062
  // Returns violation strings. Exported for the "actually catches a re-hardcode" test.
@@ -1153,7 +1147,7 @@ function checkKernelPromptsNoHardcodedIdentity() {
1153
1147
  hardFail: violations.length > 0,
1154
1148
  violations,
1155
1149
  warnings: [],
1156
- note: `${scanned} portable source file(s) scanned (.js/.html/.css/.sh under src/bongos/** + modules/** + scripts/gds/** + kernel-prompt CLIs, minus the instance-1 product + generated dirs) — none may hardcode the host brand; identity comes from branding()/window.__BRANDING__ (ADR 0062 §3/§5). ${ratcheted} scripts/gds file(s) hold documented permanent carve-outs (real DNS zones, real doc filenames, operator runbook text, historical seeded prose) capped at a count they may not exceed — task 1002428.`,
1150
+ note: `${scanned} portable source file(s) scanned (.js/.html/.css/.sh under src/bongos/** + modules/** + scripts/** (whole root, task 1002756), minus the instance-1 product + generated dirs) — none may hardcode the host brand; identity comes from branding()/window.__BRANDING__ (ADR 0062 §3/§5). ${ratcheted} scripts/gds file(s) hold documented permanent carve-outs (real DNS zones, real doc filenames, operator runbook text, historical seeded prose) capped at a count they may not exceed — task 1002428.`,
1157
1151
  };
1158
1152
  }
1159
1153
 
@@ -1655,7 +1649,6 @@ function checkWriteRoutesValidated() {
1655
1649
  };
1656
1650
  }
1657
1651
 
1658
- // ---- runner ----------------------------------------------------------------
1659
1652
 
1660
1653
  // ── Check 19 — the ADR 0174 vocabulary wall (goal 1000068 R07, task 1003008) ──
1661
1654
  //
@@ -1799,8 +1792,22 @@ const GOAL_VOCAB_SURFACE = [
1799
1792
  // allowlist longer than the rule; scanning the LINES THAT NAME THE TABLE catches
1800
1793
  // the only thing that can regress here — a membership query reaching for the old
1801
1794
  // column — with no false positives to excuse.
1795
+ // task 1003204 (goal 1000079) split that 5k-line file into db.js (a re-export
1796
+ // facade) + twelve db-*.js units, so the membership SQL is no longer in db.js at
1797
+ // all. Enumerate the family from disk rather than naming one file: a line-scoped
1798
+ // scan costs nothing on the units that hold no membership SQL, and a future
1799
+ // re-carve cannot silently disarm the rule by moving the queries again.
1800
+ const LIFECYCLE_DB_FILES = (() => {
1801
+ const dir = path.join(CORE_ROOT, 'modules', 'lifecycle');
1802
+ try {
1803
+ return fs.readdirSync(dir)
1804
+ .filter((f) => f === 'db.js' || (f.startsWith('db-') && f.endsWith('.js')))
1805
+ .sort()
1806
+ .map((f) => `modules/lifecycle/${f}`);
1807
+ } catch { return ['modules/lifecycle/db.js']; }
1808
+ })();
1802
1809
  const GOAL_VOCAB_LINE_SCOPED = [
1803
- { file: 'modules/lifecycle/db.js', mustMention: /goal_members/ },
1810
+ ...LIFECYCLE_DB_FILES.map((file) => ({ file, mustMention: /goal_members/ })),
1804
1811
  // sso.js is the federation surface: only the lines that name the membership table
1805
1812
  // or its upsert are in scope, so the file's other server-to-server prose stays out.
1806
1813
  { file: 'modules/platform-identity/routes/sso.js', mustMention: /project_memberships|upsertMembership/ },
@@ -1899,6 +1906,7 @@ const CHECKS = [
1899
1906
  checkWriteRoutesValidated, // Check 18 — BV1 R12 / task 1999: every body-reading write route validates (ADR 0118)
1900
1907
  require('./fitness-ratchets.js').checkQualityRatchets, // Check 20 — task 1003129: quality budgets only tighten (mechanics + knip CI step live in fitness-ratchets.js)
1901
1908
  require('./doc-cli-guard.js').checkDocNamesRealCli, // Check 22 — task 1003165 / G01: a doc that names a CLI invocation must match the real CLI
1909
+ require('./skill-preflight.js').checkOpsSkillsDeclareMachine, // Check 23 — task 1003167 / G03: an ops skill declares the machine it needs
1902
1910
  require('./control-manifest.js').checkControlsExist, // Check 21 — task 1003131: goal-1000076 controls assert their own existence (the ADR 0022 assurance-of-assurance)
1903
1911
  ];
1904
1912
 
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+ // identity-strip.js — comment stripping for fitness.js Check 13 (the hardcoded-
3
+ // identity gate), extracted in task 1002756. Strips comments so a rationale
4
+ // comment naming the old brand never trips the scan, WITHOUT ever letting a
5
+ // comment marker inside another construct change state.
6
+ //
7
+ // Per extension:
8
+ // .html — <!-- --> blocks removed.
9
+ // .sh — FULL-LINE `#` comments (and the shebang) only: cutting from an inline
10
+ // `#` would truncate `${v#p}` / `$#` lines and could hide a literal
11
+ // sitting after them. Shell has no block comments, so no /* */ pass (a
12
+ // `"$TMP"/*` glob would otherwise eat real code).
13
+ // .css — /* */ blocks removed (css has only block comments).
14
+ // .js — a single character-level pass (task 1002756). The old two-regex
15
+ // approach (block strip THEN line strip) could be OPENED by a `/*`
16
+ // sitting inside a `//` line comment — provision.js's
17
+ // `// … import <dir>/*.caddy` paired with a real `*/` 330 lines later
18
+ // and silently deleted everything between from the scanned text,
19
+ // hiding a genuine hardcoded literal in that window. The scanner
20
+ // tracks string state (' " ` — literals INSIDE strings are what the
21
+ // gate exists to catch, so string bodies are PRESERVED), comment
22
+ // state, and regex-literal state (a `//` or `/*` inside /…/ is not a
23
+ // comment; regex bodies are preserved). CRLF-safe by construction
24
+ // (task 1002428): newlines pass through untouched, and block-comment
25
+ // newlines are kept so reported line numbers hold.
26
+ const KEYWORDS_BEFORE_REGEX = new Set(['return', 'typeof', 'case', 'in', 'of', 'instanceof', 'new', 'delete', 'void', 'do', 'else', 'yield', 'await']);
27
+
28
+ function stripCommentsForIdentity(text, ext) {
29
+ const t = String(text == null ? '' : text);
30
+ if (ext === '.html') return t.replace(/<!--[\s\S]*?-->/g, '');
31
+ if (ext === '.sh') return t.split(/\r?\n/).map((l) => (/^\s*#/.test(l) ? '' : l)).join('\n');
32
+ if (ext === '.css') return t.replace(/\/\*[\s\S]*?\*\//g, '');
33
+ let out = '';
34
+ const n = t.length;
35
+ let i = 0;
36
+ const regexCanFollow = () => {
37
+ const trimmed = out.replace(/[ \t]+$/, '');
38
+ if (trimmed === '' || /[\n(,=:[!&|?{};+\-*%~^<>]$/.test(trimmed)) return true;
39
+ const word = /[A-Za-z_$][\w$]*$/.exec(trimmed);
40
+ return !!(word && KEYWORDS_BEFORE_REGEX.has(word[0]));
41
+ };
42
+ while (i < n) {
43
+ const c = t[i], d = t[i + 1];
44
+ if (c === '/' && d === '/') { while (i < n && t[i] !== '\n') i++; continue; } // line comment
45
+ if (c === '/' && d === '*') { // block comment — keep newlines so line numbers hold
46
+ i += 2;
47
+ while (i < n && !(t[i] === '*' && t[i + 1] === '/')) { if (t[i] === '\n') out += '\n'; i++; }
48
+ i += 2; continue;
49
+ }
50
+ if (c === '\'' || c === '"' || c === '`') { // string — body PRESERVED (leaks live here)
51
+ const q = c; out += c; i++;
52
+ while (i < n && t[i] !== q) {
53
+ if (t[i] === '\\') { out += t[i] + (t[i + 1] || ''); i += 2; continue; }
54
+ out += t[i]; i++;
55
+ }
56
+ out += q; i++; continue;
57
+ }
58
+ if (c === '/' && regexCanFollow()) { // regex literal — body preserved, markers inert
59
+ out += '/'; i++;
60
+ let inClass = false;
61
+ while (i < n && (inClass || t[i] !== '/') && t[i] !== '\n') {
62
+ if (t[i] === '\\') { out += t[i] + (t[i + 1] || ''); i += 2; continue; }
63
+ if (t[i] === '[') inClass = true;
64
+ else if (t[i] === ']') inClass = false;
65
+ out += t[i]; i++;
66
+ }
67
+ if (t[i] === '/') { out += '/'; i++; }
68
+ continue;
69
+ }
70
+ out += c; i++;
71
+ }
72
+ return out;
73
+ }
74
+
75
+ module.exports = { stripCommentsForIdentity };
@@ -40,11 +40,34 @@ const HEADLESS_AUTH_ENV = ['CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'];
40
40
  // caller — /builder-start runs on every session open — can reuse THIS exact
41
41
  // finding without paying for the `claude --version` spawn the full
42
42
  // preflightChecks() does. Pure: injectable fs + repoRoot.
43
+ //
44
+ // WORKTREES (task 1003167 / idea 1000788): this used to test `repoRoot/node_modules`
45
+ // ONLY, and a git worktree never has its own. But node's module resolution walks UP
46
+ // from the requiring file, so every require in a worktree resolves against the real
47
+ // repo root and works fine — meaning the check fired on essentially every claim
48
+ // (worktree-per-claim is the project default), telling the builder to burn an
49
+ // `npm ci` they did not need. A preflight that cries wolf is worse than none: it
50
+ // trains people to read past the one time it is right. So resolve the way node
51
+ // itself does — walk up — and only then call it missing.
43
52
  function depsFinding({ repoRoot = REPO_ROOT, fs = nodeFs } = {}) {
44
53
  let hasDeps = false;
45
- try { hasDeps = fs.existsSync(nodePath.join(repoRoot, 'node_modules')); } catch (_) { /* treat as missing */ }
54
+ let foundAt = null;
55
+ try {
56
+ let dir = nodePath.resolve(repoRoot);
57
+ for (;;) {
58
+ if (fs.existsSync(nodePath.join(dir, 'node_modules'))) { hasDeps = true; foundAt = dir; break; }
59
+ const up = nodePath.dirname(dir);
60
+ if (up === dir) break; // hit the filesystem root
61
+ dir = up;
62
+ }
63
+ } catch (_) { /* treat as missing */ }
46
64
  if (hasDeps) {
47
- return { level: 'ok', title: 'Dependencies', detail: 'node_modules present.' };
65
+ const here = nodePath.resolve(repoRoot) === foundAt;
66
+ return {
67
+ level: 'ok',
68
+ title: 'Dependencies',
69
+ detail: here ? 'node_modules present.' : `node_modules resolved from a parent (${foundAt}) — normal in a git worktree; requires resolve fine.`,
70
+ };
48
71
  }
49
72
  return {
50
73
  level: 'warn',
@@ -158,9 +158,11 @@ const CONFIG = {
158
158
  // on the control plane, where the token + the packaging tools live. UNSET
159
159
  // (default) = every step runs locally, byte-identical to today.
160
160
  remoteHost: process.env.PROVISION_REMOTE_HOST || null,
161
- // The co-hosting box's checkout dir — the cwd for the remote migrate (its relative
162
- // ./scripts/migrate.sh). Defaults to that box's shared checkout.
163
- remoteCwd: process.env.PROVISION_REMOTE_CHECKOUT || '/home/lars/amazonprimea',
161
+ // The co-hosting box's checkout dir — the remote migrate's cwd. NO baked-in default
162
+ // (task 1002756: the founder-pinned path was an identity leak, and a silent wrong
163
+ // cwd aims mutating SSH steps at a dir that may not exist); remote mode requires it
164
+ // explicitly (guarded at the makeExec call), local mode never reads it.
165
+ remoteCwd: process.env.PROVISION_REMOTE_CHECKOUT || null,
164
166
  // ssh options for the control-plane → co-hosting hop. BatchMode so a missing key
165
167
  // fails fast (never hangs on a prompt); accept-new pins the host key on first
166
168
  // connect (the operator can pre-seed known_hosts for strict pinning).
@@ -2441,6 +2443,8 @@ async function main() {
2441
2443
  // local) for the box-mutating steps; controlExec is ALWAYS local — the token
2442
2444
  // step (Cloudflare DNS hook) + the standalone repo scaffold run on the control
2443
2445
  // plane where the token + packaging tools live (task 2065, ADR 0130).
2446
+ // Remote mode needs an explicit checkout dir (task 1002756 — no founder default).
2447
+ if (CONFIG.remoteHost && !CONFIG.remoteCwd) { console.error('PROVISION_REMOTE_HOST is set but PROVISION_REMOTE_CHECKOUT is not — name the co-hosting box\'s checkout dir (the old hardcoded default was removed, task 1002756).'); process.exit(2); }
2444
2448
  const exec = makeExec({ apply, remoteHost: CONFIG.remoteHost, remoteCwd: CONFIG.remoteCwd, sshOpts: CONFIG.sshOpts });
2445
2449
  const controlExec = makeExec({ apply });
2446
2450
  const deps = {