@jhizzard/termdeck 0.18.0 → 1.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhizzard/termdeck",
3
- "version": "0.18.0",
3
+ "version": "1.0.1",
4
4
  "description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
5
5
  "bin": {
6
6
  "termdeck": "./packages/cli/src/index.js"
@@ -45,7 +45,8 @@ const {
45
45
  supabaseUrl: urlHelper,
46
46
  migrations,
47
47
  pgRunner,
48
- preconditions
48
+ preconditions,
49
+ auditUpgrade: auditUpgradeMod
49
50
  } = require(SETUP_DIR);
50
51
 
51
52
  const HELP = [
@@ -339,6 +340,43 @@ async function applyMigrations(client, dryRun) {
339
340
  }
340
341
  }
341
342
 
343
+ // Sprint 51.5 T1 — schema-introspection audit-upgrade. Runs AFTER the
344
+ // fresh-install applyMigrations() loop completes, but reports separately.
345
+ // On a Sprint-37-era project that pre-dated several mnestra migrations,
346
+ // applyMigrations now has the full bundled set (Sprint 51.5 synced 013-015
347
+ // from canonical engram), so this audit is mostly a belt-and-suspenders
348
+ // confirmation. It still surfaces drift if, e.g., the user manually
349
+ // dropped a column post-install. Mnestra-kind only — rumen cron probes
350
+ // reference vault secrets the user hasn't set up yet at this point. Run
351
+ // init-rumen for the rumen-side audit.
352
+ async function runMnestraAudit(client, projectRef, dryRun) {
353
+ step('Audit-upgrade: probing for missing mnestra schema artifacts...');
354
+ if (dryRun) { ok('(dry-run)'); return; }
355
+ const probes = auditUpgradeMod.PROBES.filter((p) => p.kind === 'mnestra');
356
+ let result;
357
+ try {
358
+ result = await auditUpgradeMod.auditUpgrade({
359
+ pgClient: client,
360
+ projectRef,
361
+ probes
362
+ });
363
+ } catch (err) {
364
+ fail(err.message);
365
+ return;
366
+ }
367
+ if (result.applied.length === 0 && result.errors.length === 0) {
368
+ ok(`(install up to date — ${result.probed.length} probes all present)`);
369
+ return;
370
+ }
371
+ ok(`(probed ${result.probed.length}, applied ${result.applied.length})`);
372
+ for (const name of result.applied) {
373
+ process.stdout.write(` ✓ applied ${name}\n`);
374
+ }
375
+ for (const e of result.errors) {
376
+ process.stdout.write(` ! ${e.name}: ${e.error}\n`);
377
+ }
378
+ }
379
+
342
380
  async function checkExistingStore(client) {
343
381
  step('Checking for existing memory_items table...');
344
382
  try {
@@ -538,6 +576,7 @@ async function main(argv) {
538
576
  try {
539
577
  await checkExistingStore(client);
540
578
  await applyMigrations(client, false);
579
+ await runMnestraAudit(client, inputs.projectUrl.projectRef, false);
541
580
  writeYamlConfig(false);
542
581
  // v0.6.9: post-write outcome verification. Confirms each migration's
543
582
  // expected schema bits actually landed — including memory_items.
@@ -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,6 +306,57 @@ 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) {
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})`);
345
+ for (const name of result.applied) {
346
+ process.stdout.write(` ✓ applied ${name}\n`);
347
+ }
348
+ for (const e of result.errors) {
349
+ process.stdout.write(` ! ${e.name}: ${e.error}\n`);
350
+ }
351
+ return true;
352
+ } catch (err) {
353
+ fail(err.message);
354
+ return true; // non-blocking
355
+ } finally {
356
+ try { await client.end(); } catch (_err) { /* ignore */ }
357
+ }
358
+ }
359
+
306
360
  // Sprint 43 T3: rumen-tick is the only function with a `__RUMEN_VERSION__`
307
361
  // placeholder (its `npm:@jhizzard/rumen@<ver>` import is rewritten at deploy
308
362
  // time). graph-inference pins its own deps (`npm:postgres@3.4.4`) and is
@@ -400,29 +454,238 @@ ${fnBlocks}`;
400
454
  return stage;
401
455
  }
402
456
 
403
- function setFunctionSecrets(secrets, dryRun) {
457
+ // Sprint 51.5 T3: per-secret CLI loop. Pre-Sprint-51.5 this issued a single
458
+ // `supabase secrets set KEY1=VAL1 KEY2=VAL2 ...` call with all keys as
459
+ // positional args. Brad's 2026-05-03 4-project install pass observed
460
+ // supabase CLI v2.90.0 silently dropping some args from a multi-arg call —
461
+ // even materializing stray entries from misparsed argv (his email landed as
462
+ // a secret name). Documented as INSTALLER-PITFALLS.md Class J. The
463
+ // deterministic fix is one CLI invocation per secret, exit code checked per
464
+ // call, stderr surfaced with the failing key name. Order is preserved for
465
+ // log readability (DATABASE_URL → ANTHROPIC_API_KEY → optional → optional).
466
+ function setFunctionSecrets(secrets, dryRun, opts = {}) {
467
+ const orderedKeys = ['DATABASE_URL', 'ANTHROPIC_API_KEY'];
468
+ if (secrets.OPENAI_API_KEY) orderedKeys.push('OPENAI_API_KEY');
469
+ if (secrets.GRAPH_LLM_CLASSIFY === '1') orderedKeys.push('GRAPH_LLM_CLASSIFY');
470
+
404
471
  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})...`);
472
+ step(`Setting function secrets per-call (${orderedKeys.join(', ')})...`);
409
473
  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}`
474
+
475
+ // Test surface: opts.runner is an optional injected function that mimics
476
+ // runShellCaptured((bin, args) => { ok, code, stdout, stderr }). Production
477
+ // path passes nothing and shells out to the real supabase CLI.
478
+ const runner = (typeof opts.runner === 'function') ? opts.runner : runShellCaptured;
479
+
480
+ for (const key of orderedKeys) {
481
+ const value = secrets[key];
482
+ if (value === undefined || value === null || value === '') {
483
+ fail(`secret ${key} missing from in-memory secrets map — wizard wiring bug`);
484
+ return false;
485
+ }
486
+ const r = runner('supabase', ['secrets', 'set', `${key}=${value}`]);
487
+ if (!r || !r.ok) {
488
+ fail(`supabase secrets set ${key} failed (exit ${r ? r.code : 'no-result'})`);
489
+ if (r && r.stderr) process.stderr.write(r.stderr + '\n');
490
+ return false;
491
+ }
492
+ }
493
+ const llmTag = secrets.GRAPH_LLM_CLASSIFY === '1' ? ', graph LLM classify on' : '';
494
+ ok(`${haveOpenAI ? '(hybrid mode' : '(keyword-only mode — OPENAI_API_KEY not set'}${llmTag})`);
495
+ return true;
496
+ }
497
+
498
+ // Sprint 51.5 T3: Build a Supabase SQL-Editor deeplink that pre-fills the
499
+ // vault.create_secret() call for one secret. Used as the fallback when
500
+ // auto-apply via pgRunner can't write to vault.secrets (permission denied,
501
+ // missing extension, etc.) AND as the manual-fix surface in any wizard text
502
+ // that previously instructed users to "click Vault in the dashboard" — the
503
+ // Vault dashboard panel was quietly removed/relocated in current Supabase
504
+ // UIs (Brad 2026-05-03 takeaway #2; INSTALLER-PITFALLS.md Class B).
505
+ //
506
+ // vault.create_secret signature is `(secret text, name text [, description text])`
507
+ // — value-then-name. Both arguments are escaped as Postgres string literals
508
+ // (single-quote doubling). The full URL is roughly:
509
+ // https://supabase.com/dashboard/project/<ref>/sql/new?content=<encoded SQL>
510
+ // Click → SQL Editor opens with the call pre-filled → user clicks Run.
511
+ function vaultSqlEditorUrl(projectRef, secretName, secretValue) {
512
+ if (!projectRef || typeof projectRef !== 'string') {
513
+ throw new Error('vaultSqlEditorUrl: projectRef is required');
514
+ }
515
+ const value = String(secretValue == null ? '' : secretValue).replace(/'/g, "''");
516
+ const name = String(secretName == null ? '' : secretName).replace(/'/g, "''");
517
+ const sql = `select vault.create_secret('${value}', '${name}');`;
518
+ return `https://supabase.com/dashboard/project/${projectRef}/sql/new?content=${encodeURIComponent(sql)}`;
519
+ }
520
+
521
+ // Sprint 51.5 T3: Ensure the two Vault secrets the cron schedules need are
522
+ // present, auto-creating them via the user's pg connection when possible.
523
+ //
524
+ // Required:
525
+ // - rumen_service_role_key (used by 002_pg_cron_schedule.sql)
526
+ // - graph_inference_service_role_key (used by 003_graph_inference_schedule.sql)
527
+ //
528
+ // Both keys hold the same value (`secrets.SUPABASE_SERVICE_ROLE_KEY`). Brad's
529
+ // 2026-05-02 recovery on jizzard-brain literally cloned rumen → graph_inference
530
+ // in vault.
531
+ //
532
+ // Strategy:
533
+ // 1. Open a pg connection to DATABASE_URL (same path as applyRumenTables).
534
+ // 2. Probe vault.secrets for both names. (Reads vault.secrets, NOT
535
+ // vault.decrypted_secrets — we don't need the decrypted value, just
536
+ // presence.)
537
+ // 3. For any missing name, call `vault.create_secret($value, $name)`.
538
+ // 4. On per-secret failure (permission denied, etc.), surface a SQL-Editor
539
+ // deeplink the user can click; do not fail the wizard hard — the
540
+ // preconditions audit will catch a still-missing rumen_service_role_key
541
+ // with its own hint, and the user has the actionable URL in front of
542
+ // them either way.
543
+ //
544
+ // Returns `{ ok, created: [...], deeplinks: [{ name, url, error }] }`.
545
+ async function ensureVaultSecrets({ projectRef, secrets, dryRun, _pgClient }) {
546
+ const required = [
547
+ { name: 'rumen_service_role_key', value: secrets.SUPABASE_SERVICE_ROLE_KEY },
548
+ { name: 'graph_inference_service_role_key', value: secrets.SUPABASE_SERVICE_ROLE_KEY }
414
549
  ];
415
- if (haveOpenAI) {
416
- args.push(`OPENAI_API_KEY=${secrets.OPENAI_API_KEY}`);
550
+
551
+ step('Ensuring Vault secrets (rumen_service_role_key, graph_inference_service_role_key)...');
552
+ if (dryRun) { ok('(dry-run)'); return { ok: true, created: [], deeplinks: [] }; }
553
+
554
+ if (!secrets.SUPABASE_SERVICE_ROLE_KEY) {
555
+ fail('SUPABASE_SERVICE_ROLE_KEY missing from in-memory secrets — preflight should have rejected');
556
+ return { ok: false, created: [], deeplinks: [], error: 'service-role-key-missing' };
417
557
  }
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;
558
+
559
+ let client = _pgClient;
560
+ let ownsClient = false;
561
+ if (!client) {
562
+ try {
563
+ client = await pgRunner.connect(secrets.DATABASE_URL);
564
+ ownsClient = true;
565
+ } catch (err) {
566
+ fail(err.message);
567
+ return { ok: false, created: [], deeplinks: [], error: 'pg-connect-failed' };
568
+ }
569
+ }
570
+
571
+ let existing = new Set();
572
+ try {
573
+ const probe = await client.query(
574
+ 'SELECT name FROM vault.secrets WHERE name = ANY($1::text[])',
575
+ [required.map((x) => x.name)]
576
+ );
577
+ existing = new Set((probe.rows || []).map((r) => r.name));
578
+ } catch (err) {
579
+ fail(`vault.secrets probe failed: ${err.message}`);
580
+ if (ownsClient) { try { await client.end(); } catch (_e) { /* ignore */ } }
581
+ // Emit deeplinks for both — user can click through manually.
582
+ const deeplinks = required.map(({ name, value }) => ({
583
+ name,
584
+ url: vaultSqlEditorUrl(projectRef, name, value),
585
+ error: err.message
586
+ }));
587
+ printVaultDeeplinks(deeplinks);
588
+ return { ok: false, created: [], deeplinks, error: 'vault-probe-failed' };
589
+ }
590
+
591
+ const missing = required.filter((x) => !existing.has(x.name));
592
+ if (missing.length === 0) {
593
+ if (ownsClient) { try { await client.end(); } catch (_e) { /* ignore */ } }
594
+ ok('(both already present)');
595
+ return { ok: true, created: [], deeplinks: [] };
596
+ }
597
+
598
+ const created = [];
599
+ const deeplinks = [];
600
+ for (const { name, value } of missing) {
601
+ try {
602
+ await client.query('SELECT vault.create_secret($1, $2)', [value, name]);
603
+ created.push(name);
604
+ } catch (err) {
605
+ deeplinks.push({
606
+ name,
607
+ url: vaultSqlEditorUrl(projectRef, name, value),
608
+ error: err && err.message ? err.message : String(err)
609
+ });
610
+ }
611
+ }
612
+ if (ownsClient) { try { await client.end(); } catch (_e) { /* ignore */ } }
613
+
614
+ if (deeplinks.length > 0) {
615
+ fail(`auto-created ${created.length} of ${missing.length}; ${deeplinks.length} need a manual SQL Editor click`);
616
+ printVaultDeeplinks(deeplinks);
617
+ return { ok: false, created, deeplinks };
618
+ }
619
+
620
+ ok(`(created ${created.length}: ${created.map((n) => n).join(', ')})`);
621
+ return { ok: true, created, deeplinks: [] };
622
+ }
623
+
624
+ function printVaultDeeplinks(deeplinks) {
625
+ process.stderr.write(
626
+ '\nThe Supabase Vault dashboard panel has been removed in current Supabase UIs.\n' +
627
+ 'Open each link below and click Run in SQL Editor to create the secret:\n\n'
628
+ );
629
+ for (const d of deeplinks) {
630
+ process.stderr.write(` ${d.name}\n ${d.url}\n`);
631
+ if (d.error) process.stderr.write(` (auto-apply error: ${d.error})\n`);
632
+ process.stderr.write('\n');
423
633
  }
424
- ok(haveOpenAI ? '(hybrid mode)' : '(keyword-only mode — OPENAI_API_KEY not set)');
425
- return true;
634
+ }
635
+
636
+ // Sprint 51.5 T3: Install-time prompt for the GRAPH_LLM_CLASSIFY toggle.
637
+ //
638
+ // graph-inference (the daily cron Edge Function) defaults every new edge to
639
+ // `relates_to` unless GRAPH_LLM_CLASSIFY=1 AND ANTHROPIC_API_KEY are set as
640
+ // Edge Function secrets. Pre-Sprint-51.5, no install path covered this — the
641
+ // wizard set ANTHROPIC_API_KEY (already required) but never set
642
+ // GRAPH_LLM_CLASSIFY, leaving the LLM classifier off by default. This is
643
+ // INSTALLER-PITFALLS.md Class F (default-vs-runtime asymmetry — Joshua may
644
+ // or may not have set the flag manually; new installs definitely don't).
645
+ //
646
+ // Side-effect: mutates `secrets.GRAPH_LLM_CLASSIFY` to '1' on Y-path. The
647
+ // per-secret loop in setFunctionSecrets reads that key and pushes it into
648
+ // the supabase secrets set sequence. On N-path the key is left undefined
649
+ // and the loop skips it; the wizard prints the manual flip command so the
650
+ // user can opt in later without re-running the whole wizard.
651
+ //
652
+ // --yes accepts the default (Y). --dry-run reports the plan and assumes Y.
653
+ async function promptGraphLlmClassify({ secrets, flags }) {
654
+ const explainer =
655
+ '\nGraph edge classification\n' +
656
+ '─────────────────────────\n' +
657
+ 'When enabled, the daily graph-inference cron uses Claude Haiku 4.5 to label\n' +
658
+ 'each new edge with a relationship type (supersedes, contradicts, elaborates,\n' +
659
+ 'caused_by, blocks, inspired_by, cross_project_link, relates_to).\n' +
660
+ '\n' +
661
+ 'Cost: ~$0.003 per 1k edges classified (a typical project sees a few hundred\n' +
662
+ 'new edges per day). Disabled = every edge is typed "relates_to".\n\n';
663
+
664
+ if (flags.dryRun) {
665
+ process.stdout.write(explainer);
666
+ process.stdout.write('? Enable AI-classified graph edges? [Y/n] (dry-run, defaulting Y)\n');
667
+ secrets.GRAPH_LLM_CLASSIFY = '1';
668
+ return { enabled: true, source: 'dry-run' };
669
+ }
670
+ if (flags.yes) {
671
+ process.stdout.write(explainer);
672
+ process.stdout.write('? Enable AI-classified graph edges? [Y/n] (--yes, defaulting Y)\n');
673
+ secrets.GRAPH_LLM_CLASSIFY = '1';
674
+ return { enabled: true, source: '--yes' };
675
+ }
676
+
677
+ process.stdout.write(explainer);
678
+ const yes = await prompts.confirm('? Enable AI-classified graph edges?', { defaultYes: true });
679
+ if (yes) {
680
+ secrets.GRAPH_LLM_CLASSIFY = '1';
681
+ process.stdout.write(' → Will set GRAPH_LLM_CLASSIFY=1 + ANTHROPIC_API_KEY in Edge Function secrets.\n\n');
682
+ return { enabled: true, source: 'prompt' };
683
+ }
684
+ process.stdout.write(
685
+ ' → GRAPH_LLM_CLASSIFY left unset. Edges will default to "relates_to".\n' +
686
+ ' To enable later: supabase secrets set GRAPH_LLM_CLASSIFY=1\n\n'
687
+ );
688
+ return { enabled: false, source: 'prompt' };
426
689
  }
427
690
 
428
691
  async function testFunction(projectRef, secrets, dryRun) {
@@ -601,13 +864,22 @@ function wireAccessTokenInMcpJson({ token, mcpJsonPath, _testFs } = {}) {
601
864
  return { status: 'updated', path: targetPath };
602
865
  }
603
866
 
604
- function printNextSteps(projectRef) {
867
+ function printNextSteps(projectRef, vaultResult, llmResult) {
605
868
  const rumenTickUrl = `https://${projectRef}.supabase.co/functions/v1/rumen-tick`;
606
869
  const graphInferenceUrl = `https://${projectRef}.supabase.co/functions/v1/graph-inference`;
607
870
  const now = new Date();
608
871
  // Round up to the next 15-minute mark so the hint is accurate.
609
872
  const next = new Date(now.getTime());
610
873
  next.setUTCMinutes(Math.ceil((now.getUTCMinutes() + 1) / 15) * 15, 0, 0);
874
+
875
+ const vaultLine = (vaultResult && vaultResult.ok)
876
+ ? ' Vault secrets: rumen_service_role_key + graph_inference_service_role_key in place.'
877
+ : ' Vault secrets: open the SQL Editor URLs above and click Run for any deeplinks shown.';
878
+
879
+ const llmLine = llmResult && llmResult.enabled
880
+ ? ' Graph edges: classified by Claude Haiku 4.5 (GRAPH_LLM_CLASSIFY=1).'
881
+ : ' Graph edges: untyped (relates_to). To enable: supabase secrets set GRAPH_LLM_CLASSIFY=1';
882
+
611
883
  process.stdout.write(`
612
884
  Rumen is deployed.
613
885
 
@@ -618,13 +890,12 @@ Edge Functions:
618
890
  ${graphInferenceUrl}
619
891
 
620
892
  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.
893
+ ${vaultLine}
894
+ ${llmLine}
895
+ Monitor rumen jobs: psql "$DATABASE_URL" -c "SELECT * FROM rumen_jobs ORDER BY started_at DESC LIMIT 5"
896
+ Rumen insights flow back into Mnestra's memory_items via rumen_insights.
897
+ graph-inference fills memory_relationships edges nightly (cosine similarity 0.85).
898
+ TermDeck's Flashback will surface cross-project patterns automatically.
628
899
  `);
629
900
  }
630
901
 
@@ -658,6 +929,22 @@ async function main(argv) {
658
929
  }
659
930
  }
660
931
 
932
+ // Sprint 51.5 T3: ensure both Vault secrets are present BEFORE the
933
+ // precondition audit (which checks vault.decrypted_secrets for
934
+ // rumen_service_role_key). Auto-applies via pgRunner; on permission
935
+ // failure prints SQL-Editor deeplinks and lets the audit's own hint
936
+ // catch the still-missing secret. The Vault dashboard panel was quietly
937
+ // removed in current Supabase UIs (Brad 2026-05-03 takeaway #2;
938
+ // INSTALLER-PITFALLS.md Class B), which is why we no longer instruct
939
+ // users to "click Vault."
940
+ let vaultResult = { ok: true, created: [], deeplinks: [] };
941
+ if (!flags.dryRun) {
942
+ vaultResult = await ensureVaultSecrets({ projectRef, secrets, dryRun: false });
943
+ // Continue regardless — preconditions audit will catch a still-missing
944
+ // secret with its own hint, and the user already has the deeplinks if
945
+ // any auto-apply failed.
946
+ }
947
+
661
948
  // v0.6.9: front-loaded precondition audit. Runs BEFORE link so we don't
662
949
  // create state (function deploy, function secrets, schedule SQL) that the
663
950
  // user would have to manually clean up if a precondition is missing. Every
@@ -691,6 +978,14 @@ async function main(argv) {
691
978
  // already-set) are silent — they're all expected paths.
692
979
  }
693
980
 
981
+ // Sprint 51.5 T1 — audit-upgrade BEFORE the rest of the flow. Surfaces
982
+ // and applies any drift between the bundled artifact set and what's
983
+ // actually live on the user's project. Non-blocking on failure (probe
984
+ // errors get logged inline; main flow continues).
985
+ if (!flags.dryRun) {
986
+ await runRumenAudit(projectRef, secrets, flags.dryRun);
987
+ }
988
+
694
989
  if (!(await applyRumenTables(secrets, flags.dryRun))) return 5;
695
990
 
696
991
  step('Resolving @jhizzard/rumen version from npm registry...');
@@ -703,6 +998,12 @@ async function main(argv) {
703
998
  }
704
999
 
705
1000
  if (!deployFunctions(resolved.version, flags.dryRun)) return 6;
1001
+
1002
+ // Sprint 51.5 T3: install-time prompt for AI edge classification. Sets
1003
+ // secrets.GRAPH_LLM_CLASSIFY in-memory; the per-secret loop below picks
1004
+ // it up. On --yes / --dry-run defaults to enabled (Y).
1005
+ const llmResult = await promptGraphLlmClassify({ secrets, flags });
1006
+
706
1007
  if (!setFunctionSecrets(secrets, flags.dryRun)) return 7;
707
1008
  if (!(await testFunction(projectRef, secrets, flags.dryRun))) return 8;
708
1009
  if (!flags.skipSchedule) {
@@ -720,7 +1021,7 @@ async function main(argv) {
720
1021
  process.stdout.write('→ Skipping pg_cron schedule (per --skip-schedule) ✓\n');
721
1022
  }
722
1023
 
723
- printNextSteps(projectRef);
1024
+ printNextSteps(projectRef, vaultResult, llmResult);
724
1025
  return 0;
725
1026
  }
726
1027
 
@@ -741,3 +1042,10 @@ module.exports._wireAccessTokenInMcpJson = wireAccessTokenInMcpJson;
741
1042
  // Sprint 43 T3: stage helper exposed so init-rumen-deploy.test.js can pin
742
1043
  // the multi-function staging contract without shelling out to `supabase`.
743
1044
  module.exports._stageRumenFunctions = stageRumenFunctions;
1045
+ // Sprint 51.5 T3: per-secret CLI loop, Vault SQL-Editor URL builder, and
1046
+ // Vault-secret ensure helper exposed for tests/init-rumen-secrets-per-call,
1047
+ // init-rumen-graph-llm, and init-rumen-vault-deeplinks.
1048
+ module.exports._setFunctionSecrets = setFunctionSecrets;
1049
+ module.exports._vaultSqlEditorUrl = vaultSqlEditorUrl;
1050
+ module.exports._ensureVaultSecrets = ensureVaultSecrets;
1051
+ module.exports._promptGraphLlmClassify = promptGraphLlmClassify;