@jhizzard/termdeck 1.0.0 → 1.0.2

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.
@@ -20,7 +20,9 @@
20
20
  // 4. Apply rumen migration 001 via pg
21
21
  // 5. supabase functions deploy rumen-tick AND graph-inference (Sprint 43 T3)
22
22
  // from a single staging dir with multi-function supabase/config.toml
23
- // 6. supabase secrets set DATABASE_URL=... ANTHROPIC_API_KEY=... [OPENAI_API_KEY=...]
23
+ // 6. supabase secrets set, ONE call per key (DATABASE_URL, ANTHROPIC_API_KEY,
24
+ // [OPENAI_API_KEY], [GRAPH_LLM_CLASSIFY=1]) — per-secret to avoid the
25
+ // v2.90.0 multi-arg drop documented in INSTALLER-PITFALLS.md Class J.
24
26
  // 7. Test rumen-tick with a manual POST (graph-inference is cron-only)
25
27
  // 8. Apply pg_cron schedule migrations 002 (rumen-tick) AND 003 (graph-inference)
26
28
  // with project ref substituted
@@ -38,7 +40,8 @@ const {
38
40
  migrations,
39
41
  migrationTemplating,
40
42
  pgRunner,
41
- preconditions
43
+ preconditions,
44
+ auditUpgrade: auditUpgradeMod
42
45
  } = require(SETUP_DIR);
43
46
 
44
47
  const {
@@ -303,13 +306,84 @@ async function applyRumenTables(secrets, dryRun) {
303
306
  }
304
307
  }
305
308
 
309
+ // Sprint 51.5 T1 — schema-introspection audit-upgrade. Probes for missing
310
+ // mnestra schema artifacts AND missing rumen cron schedules. Runs before
311
+ // the existing init-rumen flow so the user sees what's about to be applied
312
+ // up front. Idempotent: a re-run on an up-to-date project reports
313
+ // "install up to date" and applies nothing.
314
+ //
315
+ // Brad's 2026-05-02 jizzard-brain report (INSTALLER-PITFALLS.md ledger #13)
316
+ // is the originating motivation: he upgraded npm packages but his database
317
+ // stayed frozen at first-kickstart because no installer code path diffed an
318
+ // existing install against the bundled migration set. After v1.0.1 ships,
319
+ // `npm install -g @jhizzard/termdeck@1.0.1 && termdeck init --rumen` will
320
+ // surface and apply every missing artifact in one pass.
321
+ //
322
+ // Errors are surfaced inline but do NOT abort the wizard — a single
323
+ // failing probe (e.g., pg_cron not enabled when probing for cron.job)
324
+ // shouldn't block the rest of the audit or the rest of the init flow.
325
+ async function runRumenAudit(projectRef, secrets, dryRun) {
326
+ step('Audit-upgrade: probing for missing schema + cron artifacts...');
327
+ if (dryRun) { ok('(dry-run)'); return true; }
328
+ let client;
329
+ try {
330
+ client = await pgRunner.connect(secrets.DATABASE_URL);
331
+ } catch (err) {
332
+ fail(err.message);
333
+ return false;
334
+ }
335
+ try {
336
+ const result = await auditUpgradeMod.auditUpgrade({
337
+ pgClient: client,
338
+ projectRef
339
+ });
340
+ if (result.applied.length === 0 && result.errors.length === 0 && result.skipped.length === 0) {
341
+ ok(`(install up to date — ${result.probed.length} probes all present)`);
342
+ return true;
343
+ }
344
+ ok(`(probed ${result.probed.length}, applied ${result.applied.length}, skipped ${result.skipped.length})`);
345
+ for (const name of result.applied) {
346
+ process.stdout.write(` ✓ applied ${name}\n`);
347
+ }
348
+ // Sprint 51.6 T3 — Bug D: surface skipped[] entries (functionSource
349
+ // probes that detected drift but can't auto-redeploy from audit). The
350
+ // wizard will redeploy below in the deployFunctions step, which fixes
351
+ // the drift; this print just makes the diagnosis visible.
352
+ for (const s of result.skipped) {
353
+ process.stdout.write(` ⊘ skipped ${s.name}: ${s.reason}\n`);
354
+ }
355
+ for (const e of result.errors) {
356
+ process.stdout.write(` ! ${e.name}: ${e.error}\n`);
357
+ }
358
+ return true;
359
+ } catch (err) {
360
+ fail(err.message);
361
+ return true; // non-blocking
362
+ } finally {
363
+ try { await client.end(); } catch (_err) { /* ignore */ }
364
+ }
365
+ }
366
+
306
367
  // Sprint 43 T3: rumen-tick is the only function with a `__RUMEN_VERSION__`
307
368
  // placeholder (its `npm:@jhizzard/rumen@<ver>` import is rewritten at deploy
308
369
  // time). graph-inference pins its own deps (`npm:postgres@3.4.4`) and is
309
370
  // copied verbatim. If a future function adds a placeholder, list it here.
310
371
  const FUNCTIONS_WITH_VERSION_PLACEHOLDER = new Set(['rumen-tick']);
311
372
 
312
- function deployFunctions(rumenVersion, dryRun) {
373
+ // Sprint 51.6 T3 — `projectRef` is required and passed explicitly to every
374
+ // `supabase functions deploy` invocation as `--project-ref <ref>`. Brad's
375
+ // 2026-05-03 jizzard-brain install hit Bug C: `supabase link --project-ref`
376
+ // runs successfully (audit-upgrade probes confirm the link is live), but a
377
+ // few subprocess calls later `supabase functions deploy` errors with
378
+ // `Cannot find project ref. Have you run supabase link?` because the link
379
+ // state persists per-cwd in supabase/config.toml and the staged-functions
380
+ // directory has none. Threading --project-ref through dodges link-state
381
+ // coupling entirely. The flag is supported by supabase CLI v1.x and v2.x.
382
+ function deployFunctions(rumenVersion, projectRef, dryRun) {
383
+ if (!projectRef || typeof projectRef !== 'string') {
384
+ fail('deployFunctions: projectRef is required (Sprint 51.6 T3 — explicit --project-ref to dodge subprocess link-state isolation)');
385
+ return false;
386
+ }
313
387
  const fnNames = migrations.listRumenFunctions();
314
388
  if (fnNames.length === 0) {
315
389
  fail('no Rumen Edge Function source found in bundled setup or @jhizzard/rumen package');
@@ -336,10 +410,14 @@ function deployFunctions(rumenVersion, dryRun) {
336
410
  ok();
337
411
 
338
412
  for (const name of fnNames) {
339
- step(`Running: supabase functions deploy ${name} --no-verify-jwt...`);
340
- const r = runShell('supabase', ['functions', 'deploy', name, '--no-verify-jwt'], {
341
- cwd: stage
342
- });
413
+ // Sprint 51.6 T3 `--project-ref <ref>` explicit, dodging supabase
414
+ // link-state subprocess isolation (Bug C, Brad's 2026-05-03 install).
415
+ step(`Running: supabase functions deploy ${name} --project-ref ${projectRef} --no-verify-jwt...`);
416
+ const r = runShell('supabase', [
417
+ 'functions', 'deploy', name,
418
+ '--project-ref', projectRef,
419
+ '--no-verify-jwt',
420
+ ], { cwd: stage });
343
421
  if (!r.ok) {
344
422
  fail(`deploy of ${name} failed (exit ${r.code})`);
345
423
  return false;
@@ -400,29 +478,238 @@ ${fnBlocks}`;
400
478
  return stage;
401
479
  }
402
480
 
403
- function setFunctionSecrets(secrets, dryRun) {
481
+ // Sprint 51.5 T3: per-secret CLI loop. Pre-Sprint-51.5 this issued a single
482
+ // `supabase secrets set KEY1=VAL1 KEY2=VAL2 ...` call with all keys as
483
+ // positional args. Brad's 2026-05-03 4-project install pass observed
484
+ // supabase CLI v2.90.0 silently dropping some args from a multi-arg call —
485
+ // even materializing stray entries from misparsed argv (his email landed as
486
+ // a secret name). Documented as INSTALLER-PITFALLS.md Class J. The
487
+ // deterministic fix is one CLI invocation per secret, exit code checked per
488
+ // call, stderr surfaced with the failing key name. Order is preserved for
489
+ // log readability (DATABASE_URL → ANTHROPIC_API_KEY → optional → optional).
490
+ function setFunctionSecrets(secrets, dryRun, opts = {}) {
491
+ const orderedKeys = ['DATABASE_URL', 'ANTHROPIC_API_KEY'];
492
+ if (secrets.OPENAI_API_KEY) orderedKeys.push('OPENAI_API_KEY');
493
+ if (secrets.GRAPH_LLM_CLASSIFY === '1') orderedKeys.push('GRAPH_LLM_CLASSIFY');
494
+
404
495
  const haveOpenAI = Boolean(secrets.OPENAI_API_KEY);
405
- const label = haveOpenAI
406
- ? 'DATABASE_URL, ANTHROPIC_API_KEY, OPENAI_API_KEY'
407
- : 'DATABASE_URL, ANTHROPIC_API_KEY';
408
- step(`Setting function secrets (${label})...`);
496
+ step(`Setting function secrets per-call (${orderedKeys.join(', ')})...`);
409
497
  if (dryRun) { ok('(dry-run)'); return true; }
410
- const args = [
411
- 'secrets', 'set',
412
- `DATABASE_URL=${secrets.DATABASE_URL}`,
413
- `ANTHROPIC_API_KEY=${secrets.ANTHROPIC_API_KEY}`
498
+
499
+ // Test surface: opts.runner is an optional injected function that mimics
500
+ // runShellCaptured((bin, args) => { ok, code, stdout, stderr }). Production
501
+ // path passes nothing and shells out to the real supabase CLI.
502
+ const runner = (typeof opts.runner === 'function') ? opts.runner : runShellCaptured;
503
+
504
+ for (const key of orderedKeys) {
505
+ const value = secrets[key];
506
+ if (value === undefined || value === null || value === '') {
507
+ fail(`secret ${key} missing from in-memory secrets map — wizard wiring bug`);
508
+ return false;
509
+ }
510
+ const r = runner('supabase', ['secrets', 'set', `${key}=${value}`]);
511
+ if (!r || !r.ok) {
512
+ fail(`supabase secrets set ${key} failed (exit ${r ? r.code : 'no-result'})`);
513
+ if (r && r.stderr) process.stderr.write(r.stderr + '\n');
514
+ return false;
515
+ }
516
+ }
517
+ const llmTag = secrets.GRAPH_LLM_CLASSIFY === '1' ? ', graph LLM classify on' : '';
518
+ ok(`${haveOpenAI ? '(hybrid mode' : '(keyword-only mode — OPENAI_API_KEY not set'}${llmTag})`);
519
+ return true;
520
+ }
521
+
522
+ // Sprint 51.5 T3: Build a Supabase SQL-Editor deeplink that pre-fills the
523
+ // vault.create_secret() call for one secret. Used as the fallback when
524
+ // auto-apply via pgRunner can't write to vault.secrets (permission denied,
525
+ // missing extension, etc.) AND as the manual-fix surface in any wizard text
526
+ // that previously instructed users to "click Vault in the dashboard" — the
527
+ // Vault dashboard panel was quietly removed/relocated in current Supabase
528
+ // UIs (Brad 2026-05-03 takeaway #2; INSTALLER-PITFALLS.md Class B).
529
+ //
530
+ // vault.create_secret signature is `(secret text, name text [, description text])`
531
+ // — value-then-name. Both arguments are escaped as Postgres string literals
532
+ // (single-quote doubling). The full URL is roughly:
533
+ // https://supabase.com/dashboard/project/<ref>/sql/new?content=<encoded SQL>
534
+ // Click → SQL Editor opens with the call pre-filled → user clicks Run.
535
+ function vaultSqlEditorUrl(projectRef, secretName, secretValue) {
536
+ if (!projectRef || typeof projectRef !== 'string') {
537
+ throw new Error('vaultSqlEditorUrl: projectRef is required');
538
+ }
539
+ const value = String(secretValue == null ? '' : secretValue).replace(/'/g, "''");
540
+ const name = String(secretName == null ? '' : secretName).replace(/'/g, "''");
541
+ const sql = `select vault.create_secret('${value}', '${name}');`;
542
+ return `https://supabase.com/dashboard/project/${projectRef}/sql/new?content=${encodeURIComponent(sql)}`;
543
+ }
544
+
545
+ // Sprint 51.5 T3: Ensure the two Vault secrets the cron schedules need are
546
+ // present, auto-creating them via the user's pg connection when possible.
547
+ //
548
+ // Required:
549
+ // - rumen_service_role_key (used by 002_pg_cron_schedule.sql)
550
+ // - graph_inference_service_role_key (used by 003_graph_inference_schedule.sql)
551
+ //
552
+ // Both keys hold the same value (`secrets.SUPABASE_SERVICE_ROLE_KEY`). Brad's
553
+ // 2026-05-02 recovery on jizzard-brain literally cloned rumen → graph_inference
554
+ // in vault.
555
+ //
556
+ // Strategy:
557
+ // 1. Open a pg connection to DATABASE_URL (same path as applyRumenTables).
558
+ // 2. Probe vault.secrets for both names. (Reads vault.secrets, NOT
559
+ // vault.decrypted_secrets — we don't need the decrypted value, just
560
+ // presence.)
561
+ // 3. For any missing name, call `vault.create_secret($value, $name)`.
562
+ // 4. On per-secret failure (permission denied, etc.), surface a SQL-Editor
563
+ // deeplink the user can click; do not fail the wizard hard — the
564
+ // preconditions audit will catch a still-missing rumen_service_role_key
565
+ // with its own hint, and the user has the actionable URL in front of
566
+ // them either way.
567
+ //
568
+ // Returns `{ ok, created: [...], deeplinks: [{ name, url, error }] }`.
569
+ async function ensureVaultSecrets({ projectRef, secrets, dryRun, _pgClient }) {
570
+ const required = [
571
+ { name: 'rumen_service_role_key', value: secrets.SUPABASE_SERVICE_ROLE_KEY },
572
+ { name: 'graph_inference_service_role_key', value: secrets.SUPABASE_SERVICE_ROLE_KEY }
414
573
  ];
415
- if (haveOpenAI) {
416
- args.push(`OPENAI_API_KEY=${secrets.OPENAI_API_KEY}`);
574
+
575
+ step('Ensuring Vault secrets (rumen_service_role_key, graph_inference_service_role_key)...');
576
+ if (dryRun) { ok('(dry-run)'); return { ok: true, created: [], deeplinks: [] }; }
577
+
578
+ if (!secrets.SUPABASE_SERVICE_ROLE_KEY) {
579
+ fail('SUPABASE_SERVICE_ROLE_KEY missing from in-memory secrets — preflight should have rejected');
580
+ return { ok: false, created: [], deeplinks: [], error: 'service-role-key-missing' };
417
581
  }
418
- const r = runShellCaptured('supabase', args);
419
- if (!r.ok) {
420
- fail(`secrets set failed (exit ${r.code})`);
421
- if (r.stderr) process.stderr.write(r.stderr + '\n');
422
- return false;
582
+
583
+ let client = _pgClient;
584
+ let ownsClient = false;
585
+ if (!client) {
586
+ try {
587
+ client = await pgRunner.connect(secrets.DATABASE_URL);
588
+ ownsClient = true;
589
+ } catch (err) {
590
+ fail(err.message);
591
+ return { ok: false, created: [], deeplinks: [], error: 'pg-connect-failed' };
592
+ }
423
593
  }
424
- ok(haveOpenAI ? '(hybrid mode)' : '(keyword-only mode — OPENAI_API_KEY not set)');
425
- return true;
594
+
595
+ let existing = new Set();
596
+ try {
597
+ const probe = await client.query(
598
+ 'SELECT name FROM vault.secrets WHERE name = ANY($1::text[])',
599
+ [required.map((x) => x.name)]
600
+ );
601
+ existing = new Set((probe.rows || []).map((r) => r.name));
602
+ } catch (err) {
603
+ fail(`vault.secrets probe failed: ${err.message}`);
604
+ if (ownsClient) { try { await client.end(); } catch (_e) { /* ignore */ } }
605
+ // Emit deeplinks for both — user can click through manually.
606
+ const deeplinks = required.map(({ name, value }) => ({
607
+ name,
608
+ url: vaultSqlEditorUrl(projectRef, name, value),
609
+ error: err.message
610
+ }));
611
+ printVaultDeeplinks(deeplinks);
612
+ return { ok: false, created: [], deeplinks, error: 'vault-probe-failed' };
613
+ }
614
+
615
+ const missing = required.filter((x) => !existing.has(x.name));
616
+ if (missing.length === 0) {
617
+ if (ownsClient) { try { await client.end(); } catch (_e) { /* ignore */ } }
618
+ ok('(both already present)');
619
+ return { ok: true, created: [], deeplinks: [] };
620
+ }
621
+
622
+ const created = [];
623
+ const deeplinks = [];
624
+ for (const { name, value } of missing) {
625
+ try {
626
+ await client.query('SELECT vault.create_secret($1, $2)', [value, name]);
627
+ created.push(name);
628
+ } catch (err) {
629
+ deeplinks.push({
630
+ name,
631
+ url: vaultSqlEditorUrl(projectRef, name, value),
632
+ error: err && err.message ? err.message : String(err)
633
+ });
634
+ }
635
+ }
636
+ if (ownsClient) { try { await client.end(); } catch (_e) { /* ignore */ } }
637
+
638
+ if (deeplinks.length > 0) {
639
+ fail(`auto-created ${created.length} of ${missing.length}; ${deeplinks.length} need a manual SQL Editor click`);
640
+ printVaultDeeplinks(deeplinks);
641
+ return { ok: false, created, deeplinks };
642
+ }
643
+
644
+ ok(`(created ${created.length}: ${created.map((n) => n).join(', ')})`);
645
+ return { ok: true, created, deeplinks: [] };
646
+ }
647
+
648
+ function printVaultDeeplinks(deeplinks) {
649
+ process.stderr.write(
650
+ '\nThe Supabase Vault dashboard panel has been removed in current Supabase UIs.\n' +
651
+ 'Open each link below and click Run in SQL Editor to create the secret:\n\n'
652
+ );
653
+ for (const d of deeplinks) {
654
+ process.stderr.write(` ${d.name}\n ${d.url}\n`);
655
+ if (d.error) process.stderr.write(` (auto-apply error: ${d.error})\n`);
656
+ process.stderr.write('\n');
657
+ }
658
+ }
659
+
660
+ // Sprint 51.5 T3: Install-time prompt for the GRAPH_LLM_CLASSIFY toggle.
661
+ //
662
+ // graph-inference (the daily cron Edge Function) defaults every new edge to
663
+ // `relates_to` unless GRAPH_LLM_CLASSIFY=1 AND ANTHROPIC_API_KEY are set as
664
+ // Edge Function secrets. Pre-Sprint-51.5, no install path covered this — the
665
+ // wizard set ANTHROPIC_API_KEY (already required) but never set
666
+ // GRAPH_LLM_CLASSIFY, leaving the LLM classifier off by default. This is
667
+ // INSTALLER-PITFALLS.md Class F (default-vs-runtime asymmetry — Joshua may
668
+ // or may not have set the flag manually; new installs definitely don't).
669
+ //
670
+ // Side-effect: mutates `secrets.GRAPH_LLM_CLASSIFY` to '1' on Y-path. The
671
+ // per-secret loop in setFunctionSecrets reads that key and pushes it into
672
+ // the supabase secrets set sequence. On N-path the key is left undefined
673
+ // and the loop skips it; the wizard prints the manual flip command so the
674
+ // user can opt in later without re-running the whole wizard.
675
+ //
676
+ // --yes accepts the default (Y). --dry-run reports the plan and assumes Y.
677
+ async function promptGraphLlmClassify({ secrets, flags }) {
678
+ const explainer =
679
+ '\nGraph edge classification\n' +
680
+ '─────────────────────────\n' +
681
+ 'When enabled, the daily graph-inference cron uses Claude Haiku 4.5 to label\n' +
682
+ 'each new edge with a relationship type (supersedes, contradicts, elaborates,\n' +
683
+ 'caused_by, blocks, inspired_by, cross_project_link, relates_to).\n' +
684
+ '\n' +
685
+ 'Cost: ~$0.003 per 1k edges classified (a typical project sees a few hundred\n' +
686
+ 'new edges per day). Disabled = every edge is typed "relates_to".\n\n';
687
+
688
+ if (flags.dryRun) {
689
+ process.stdout.write(explainer);
690
+ process.stdout.write('? Enable AI-classified graph edges? [Y/n] (dry-run, defaulting Y)\n');
691
+ secrets.GRAPH_LLM_CLASSIFY = '1';
692
+ return { enabled: true, source: 'dry-run' };
693
+ }
694
+ if (flags.yes) {
695
+ process.stdout.write(explainer);
696
+ process.stdout.write('? Enable AI-classified graph edges? [Y/n] (--yes, defaulting Y)\n');
697
+ secrets.GRAPH_LLM_CLASSIFY = '1';
698
+ return { enabled: true, source: '--yes' };
699
+ }
700
+
701
+ process.stdout.write(explainer);
702
+ const yes = await prompts.confirm('? Enable AI-classified graph edges?', { defaultYes: true });
703
+ if (yes) {
704
+ secrets.GRAPH_LLM_CLASSIFY = '1';
705
+ process.stdout.write(' → Will set GRAPH_LLM_CLASSIFY=1 + ANTHROPIC_API_KEY in Edge Function secrets.\n\n');
706
+ return { enabled: true, source: 'prompt' };
707
+ }
708
+ process.stdout.write(
709
+ ' → GRAPH_LLM_CLASSIFY left unset. Edges will default to "relates_to".\n' +
710
+ ' To enable later: supabase secrets set GRAPH_LLM_CLASSIFY=1\n\n'
711
+ );
712
+ return { enabled: false, source: 'prompt' };
426
713
  }
427
714
 
428
715
  async function testFunction(projectRef, secrets, dryRun) {
@@ -601,13 +888,22 @@ function wireAccessTokenInMcpJson({ token, mcpJsonPath, _testFs } = {}) {
601
888
  return { status: 'updated', path: targetPath };
602
889
  }
603
890
 
604
- function printNextSteps(projectRef) {
891
+ function printNextSteps(projectRef, vaultResult, llmResult) {
605
892
  const rumenTickUrl = `https://${projectRef}.supabase.co/functions/v1/rumen-tick`;
606
893
  const graphInferenceUrl = `https://${projectRef}.supabase.co/functions/v1/graph-inference`;
607
894
  const now = new Date();
608
895
  // Round up to the next 15-minute mark so the hint is accurate.
609
896
  const next = new Date(now.getTime());
610
897
  next.setUTCMinutes(Math.ceil((now.getUTCMinutes() + 1) / 15) * 15, 0, 0);
898
+
899
+ const vaultLine = (vaultResult && vaultResult.ok)
900
+ ? ' Vault secrets: rumen_service_role_key + graph_inference_service_role_key in place.'
901
+ : ' Vault secrets: open the SQL Editor URLs above and click Run for any deeplinks shown.';
902
+
903
+ const llmLine = llmResult && llmResult.enabled
904
+ ? ' Graph edges: classified by Claude Haiku 4.5 (GRAPH_LLM_CLASSIFY=1).'
905
+ : ' Graph edges: untyped (relates_to). To enable: supabase secrets set GRAPH_LLM_CLASSIFY=1';
906
+
611
907
  process.stdout.write(`
612
908
  Rumen is deployed.
613
909
 
@@ -618,13 +914,12 @@ Edge Functions:
618
914
  ${graphInferenceUrl}
619
915
 
620
916
  Next steps:
621
- 1. Monitor rumen jobs: psql "$DATABASE_URL" -c "SELECT * FROM rumen_jobs ORDER BY started_at DESC LIMIT 5"
622
- 2. Store service_role keys in Supabase Vault — required for both cron schedules:
623
- rumen_service_role_key (used by 002_pg_cron_schedule.sql)
624
- graph_inference_service_role_key (used by 003_graph_inference_schedule.sql)
625
- 3. Rumen insights flow back into Mnestra's memory_items via rumen_insights.
626
- 4. graph-inference fills memory_relationships edges nightly (cosine similarity ≥ 0.85).
627
- 5. TermDeck's Flashback will surface cross-project patterns automatically.
917
+ ${vaultLine}
918
+ ${llmLine}
919
+ Monitor rumen jobs: psql "$DATABASE_URL" -c "SELECT * FROM rumen_jobs ORDER BY started_at DESC LIMIT 5"
920
+ Rumen insights flow back into Mnestra's memory_items via rumen_insights.
921
+ graph-inference fills memory_relationships edges nightly (cosine similarity 0.85).
922
+ TermDeck's Flashback will surface cross-project patterns automatically.
628
923
  `);
629
924
  }
630
925
 
@@ -658,6 +953,22 @@ async function main(argv) {
658
953
  }
659
954
  }
660
955
 
956
+ // Sprint 51.5 T3: ensure both Vault secrets are present BEFORE the
957
+ // precondition audit (which checks vault.decrypted_secrets for
958
+ // rumen_service_role_key). Auto-applies via pgRunner; on permission
959
+ // failure prints SQL-Editor deeplinks and lets the audit's own hint
960
+ // catch the still-missing secret. The Vault dashboard panel was quietly
961
+ // removed in current Supabase UIs (Brad 2026-05-03 takeaway #2;
962
+ // INSTALLER-PITFALLS.md Class B), which is why we no longer instruct
963
+ // users to "click Vault."
964
+ let vaultResult = { ok: true, created: [], deeplinks: [] };
965
+ if (!flags.dryRun) {
966
+ vaultResult = await ensureVaultSecrets({ projectRef, secrets, dryRun: false });
967
+ // Continue regardless — preconditions audit will catch a still-missing
968
+ // secret with its own hint, and the user already has the deeplinks if
969
+ // any auto-apply failed.
970
+ }
971
+
661
972
  // v0.6.9: front-loaded precondition audit. Runs BEFORE link so we don't
662
973
  // create state (function deploy, function secrets, schedule SQL) that the
663
974
  // user would have to manually clean up if a precondition is missing. Every
@@ -691,6 +1002,14 @@ async function main(argv) {
691
1002
  // already-set) are silent — they're all expected paths.
692
1003
  }
693
1004
 
1005
+ // Sprint 51.5 T1 — audit-upgrade BEFORE the rest of the flow. Surfaces
1006
+ // and applies any drift between the bundled artifact set and what's
1007
+ // actually live on the user's project. Non-blocking on failure (probe
1008
+ // errors get logged inline; main flow continues).
1009
+ if (!flags.dryRun) {
1010
+ await runRumenAudit(projectRef, secrets, flags.dryRun);
1011
+ }
1012
+
694
1013
  if (!(await applyRumenTables(secrets, flags.dryRun))) return 5;
695
1014
 
696
1015
  step('Resolving @jhizzard/rumen version from npm registry...');
@@ -702,7 +1021,13 @@ async function main(argv) {
702
1021
  process.stderr.write(` ! falling back to pinned FALLBACK_RUMEN_VERSION=${FALLBACK_RUMEN_VERSION}\n`);
703
1022
  }
704
1023
 
705
- if (!deployFunctions(resolved.version, flags.dryRun)) return 6;
1024
+ if (!deployFunctions(resolved.version, projectRef, flags.dryRun)) return 6;
1025
+
1026
+ // Sprint 51.5 T3: install-time prompt for AI edge classification. Sets
1027
+ // secrets.GRAPH_LLM_CLASSIFY in-memory; the per-secret loop below picks
1028
+ // it up. On --yes / --dry-run defaults to enabled (Y).
1029
+ const llmResult = await promptGraphLlmClassify({ secrets, flags });
1030
+
706
1031
  if (!setFunctionSecrets(secrets, flags.dryRun)) return 7;
707
1032
  if (!(await testFunction(projectRef, secrets, flags.dryRun))) return 8;
708
1033
  if (!flags.skipSchedule) {
@@ -720,7 +1045,7 @@ async function main(argv) {
720
1045
  process.stdout.write('→ Skipping pg_cron schedule (per --skip-schedule) ✓\n');
721
1046
  }
722
1047
 
723
- printNextSteps(projectRef);
1048
+ printNextSteps(projectRef, vaultResult, llmResult);
724
1049
  return 0;
725
1050
  }
726
1051
 
@@ -741,3 +1066,13 @@ module.exports._wireAccessTokenInMcpJson = wireAccessTokenInMcpJson;
741
1066
  // Sprint 43 T3: stage helper exposed so init-rumen-deploy.test.js can pin
742
1067
  // the multi-function staging contract without shelling out to `supabase`.
743
1068
  module.exports._stageRumenFunctions = stageRumenFunctions;
1069
+ // Sprint 51.6 T3 — exported for tests/init-rumen-project-ref.test.js so the
1070
+ // --project-ref invariant can be asserted without spawning a real shell.
1071
+ module.exports._deployFunctions = deployFunctions;
1072
+ // Sprint 51.5 T3: per-secret CLI loop, Vault SQL-Editor URL builder, and
1073
+ // Vault-secret ensure helper exposed for tests/init-rumen-secrets-per-call,
1074
+ // init-rumen-graph-llm, and init-rumen-vault-deeplinks.
1075
+ module.exports._setFunctionSecrets = setFunctionSecrets;
1076
+ module.exports._vaultSqlEditorUrl = vaultSqlEditorUrl;
1077
+ module.exports._ensureVaultSecrets = ensureVaultSecrets;
1078
+ module.exports._promptGraphLlmClassify = promptGraphLlmClassify;