@sonnechasser/ntrp 0.3.2 → 0.3.4

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.
@@ -62,6 +62,10 @@ var init_context = __esm({
62
62
  });
63
63
 
64
64
  // src/output/formatters.ts
65
+ function formatDollarImpact(value, label) {
66
+ if (value != null && value > 0) return `${formatDollarValue(value)} ${label ?? ""}`.trim();
67
+ return "N/A";
68
+ }
65
69
  function formatScore(score) {
66
70
  return `${Math.round(score)}`;
67
71
  }
@@ -685,20 +689,20 @@ function isClosedConnectionError(err) {
685
689
  function closeConnection(c) {
686
690
  const close2 = c.close;
687
691
  if (typeof close2 !== "function") return Promise.resolve();
688
- return new Promise((resolve7) => {
692
+ return new Promise((resolve8) => {
689
693
  try {
690
- close2.call(c, () => resolve7());
694
+ close2.call(c, () => resolve8());
691
695
  } catch {
692
- resolve7();
696
+ resolve8();
693
697
  }
694
698
  });
695
699
  }
696
700
  function isConnectionAlive(c) {
697
- return new Promise((resolve7) => {
701
+ return new Promise((resolve8) => {
698
702
  try {
699
- c.all("SELECT 1", (err) => resolve7(!err));
703
+ c.all("SELECT 1", (err) => resolve8(!err));
700
704
  } catch {
701
- resolve7(false);
705
+ resolve8(false);
702
706
  }
703
707
  });
704
708
  }
@@ -712,8 +716,8 @@ async function discardConnection() {
712
716
  await closeConnection(currentConn).catch(() => void 0);
713
717
  }
714
718
  if (currentDb) {
715
- await new Promise((resolve7) => {
716
- currentDb.close(() => resolve7());
719
+ await new Promise((resolve8) => {
720
+ currentDb.close(() => resolve8());
717
721
  }).catch(() => void 0);
718
722
  }
719
723
  }
@@ -728,10 +732,10 @@ async function withReconnect(op) {
728
732
  }
729
733
  async function execAllOnce(sql, params) {
730
734
  const c = await getConnection();
731
- return new Promise((resolve7, reject) => {
735
+ return new Promise((resolve8, reject) => {
732
736
  const cb = (err, rows) => {
733
737
  if (err) reject(err);
734
- else resolve7(rows ?? []);
738
+ else resolve8(rows ?? []);
735
739
  };
736
740
  if (params.length > 0) {
737
741
  const stmt = c.prepare(sql);
@@ -746,18 +750,18 @@ async function execAllOnce(sql, params) {
746
750
  }
747
751
  async function runOnce(sql, params = []) {
748
752
  const c = await getConnection();
749
- return new Promise((resolve7, reject) => {
753
+ return new Promise((resolve8, reject) => {
750
754
  if (params.length > 0) {
751
755
  const stmt = c.prepare(sql);
752
756
  stmt.run(...params, (err) => {
753
757
  stmt.finalize();
754
758
  if (err) reject(err);
755
- else resolve7();
759
+ else resolve8();
756
760
  });
757
761
  } else {
758
762
  c.run(sql, (err) => {
759
763
  if (err) reject(err);
760
- else resolve7();
764
+ else resolve8();
761
765
  });
762
766
  }
763
767
  });
@@ -2381,20 +2385,12 @@ function bold(text) {
2381
2385
  }
2382
2386
  function badge(label, tone = "muted") {
2383
2387
  const normalized = ` ${label.toUpperCase()} `;
2384
- switch (tone) {
2385
- case "success":
2386
- return chalk.hex(TOKENS.success)(normalized);
2387
- case "warning":
2388
- return chalk.hex(TOKENS.warning)(normalized);
2389
- case "error":
2390
- return chalk.hex(TOKENS.error)(normalized);
2391
- case "info":
2392
- return chalk.hex(TOKENS.info)(normalized);
2393
- case "accent":
2394
- return chalk.hex(TOKENS.accent)(normalized);
2395
- case "muted":
2396
- return chalk.dim(normalized);
2388
+ if (tone === "muted") return chalk.dim(normalized);
2389
+ const color = BADGE_TONE_COLORS[tone];
2390
+ if (chalk.level >= 2) {
2391
+ return chalk.bgHex(color).hex(BADGE_TEXT).bold(normalized);
2397
2392
  }
2393
+ return chalk.hex(color)(normalized);
2398
2394
  }
2399
2395
  function sectionHeading(label) {
2400
2396
  return `${paint("accent", "\u25B8")} ${paint("accent", bold(label))}`;
@@ -2403,9 +2399,27 @@ function actionHint(label, command, detail) {
2403
2399
  const suffix = detail ? chalk.dim(` ${detail}`) : "";
2404
2400
  return `${chalk.dim(label)} ${paint("accent", command)}${suffix}`;
2405
2401
  }
2402
+ function statusDot(status) {
2403
+ if (status === "neutral") return chalk.dim("\u25CB");
2404
+ return chalk.hex(STATUS[status])("\u25CF");
2405
+ }
2406
+ function statusPaint(status) {
2407
+ if (status === "neutral") return chalk.dim;
2408
+ return chalk.hex(STATUS[status]);
2409
+ }
2410
+ function severityPaint(severity) {
2411
+ switch (severity) {
2412
+ case "critical":
2413
+ return chalk.hex(STATUS.red);
2414
+ case "warning":
2415
+ return chalk.hex(STATUS.yellow);
2416
+ default:
2417
+ return chalk.hex(TOKENS.info);
2418
+ }
2419
+ }
2406
2420
  function scoreBar(score, status, width = 14) {
2407
2421
  const filled = Math.round(score / 100 * width);
2408
- const color = chalk.hex(STATUS_COLORS[status]);
2422
+ const color = chalk.hex(STATUS[status]);
2409
2423
  let filledPart = "";
2410
2424
  for (let i = 0; i < filled; i++) {
2411
2425
  filledPart += i % 2 === 0 ? "\u2588" : "\u2593";
@@ -2413,15 +2427,16 @@ function scoreBar(score, status, width = 14) {
2413
2427
  const emptyPart = "\u2591".repeat(width - filled);
2414
2428
  return color(filledPart) + chalk.dim(emptyPart);
2415
2429
  }
2416
- var STATUS_COLORS, TOKENS;
2430
+ var STATUS, TOKENS, BADGE_TONE_COLORS, BADGE_TEXT;
2417
2431
  var init_theme = __esm({
2418
2432
  "src/ui/theme.ts"() {
2419
2433
  "use strict";
2420
2434
  init_formatters();
2421
- STATUS_COLORS = {
2435
+ STATUS = {
2422
2436
  green: "#22c55e",
2423
2437
  yellow: "#eab308",
2424
- red: "#ef4444"
2438
+ red: "#ef4444",
2439
+ neutral: "#64748b"
2425
2440
  };
2426
2441
  TOKENS = {
2427
2442
  accent: "#14b8a6",
@@ -2430,11 +2445,20 @@ var init_theme = __esm({
2430
2445
  borderMuted: "#1e293b",
2431
2446
  dim: "#64748b",
2432
2447
  text: "#e2e8f0",
2433
- error: "#ef4444",
2434
- warning: "#eab308",
2435
- success: "#22c55e",
2436
- info: "#3b82f6"
2448
+ info: "#3b82f6",
2449
+ ...STATUS,
2450
+ success: STATUS.green,
2451
+ warning: STATUS.yellow,
2452
+ error: STATUS.red
2453
+ };
2454
+ BADGE_TONE_COLORS = {
2455
+ success: TOKENS.success,
2456
+ warning: TOKENS.warning,
2457
+ error: TOKENS.error,
2458
+ info: TOKENS.info,
2459
+ accent: TOKENS.accent
2437
2460
  };
2461
+ BADGE_TEXT = "#0f172a";
2438
2462
  }
2439
2463
  });
2440
2464
 
@@ -3009,13 +3033,22 @@ function printTimeBankCelebration(milestone, totalMinutes) {
3009
3033
  seed: rotationSeed(state2) + 1
3010
3034
  });
3011
3035
  console.log();
3012
- console.log(" " + paint("accent", `\u2726 ${milestone.title}`) + chalk2.dim(` \u2014 ${formatHoursLabel(totalHours)} saved`));
3013
- console.log(" " + chalk2.dim(milestone.message));
3014
- if (perspective) {
3015
- console.log(" " + chalk2.dim.italic(formatPerspectiveLine(perspective, totalHours)));
3036
+ const head = paint("accent", `\u2726 ${milestone.title}`) + chalk2.dim(` \u2014 ${formatHoursLabel(totalHours)} saved`);
3037
+ const tail = perspective ? chalk2.dim(" \xB7 ") + chalk2.dim.italic(formatPerspectiveLine(perspective, totalHours)) : "";
3038
+ console.log(" " + head + tail);
3039
+ const message = stripLeadingTitle(milestone.message, milestone.title);
3040
+ if (message) {
3041
+ console.log(" " + chalk2.dim(message));
3016
3042
  }
3017
3043
  console.log();
3018
3044
  }
3045
+ function stripLeadingTitle(message, title) {
3046
+ const trimmed = message.trim();
3047
+ if (trimmed.toLowerCase().startsWith(title.toLowerCase())) {
3048
+ return trimmed.slice(title.length).replace(/^[.!,:;\s—–-]+/, "").trim();
3049
+ }
3050
+ return trimmed;
3051
+ }
3019
3052
  function formatHoursLabel(hours) {
3020
3053
  if (hours < 1) return `${Math.round(hours * 60)}m`;
3021
3054
  if (hours < 10) return `${hours.toFixed(1)}h`;
@@ -4383,6 +4416,14 @@ function resolveEffectiveTier(ctx, surface) {
4383
4416
  function resolveEffectiveModelOverride(ctx) {
4384
4417
  return getSessionModelOverride(ctx) ?? loadLlmConfig().modelOverride;
4385
4418
  }
4419
+ function resolveModelForActive(ctx, surface) {
4420
+ const provider = resolveActiveProvider(ctx);
4421
+ const tier = resolveEffectiveTier(ctx, surface);
4422
+ const override = resolveEffectiveModelOverride(ctx);
4423
+ const providerOverride = overrideForProvider(override, provider, provider);
4424
+ const modelId = resolveModelSafe(provider, tier, providerOverride);
4425
+ return { provider, tier, modelId };
4426
+ }
4386
4427
  function resolveProviderOrder(ctx) {
4387
4428
  const active = resolveActiveProvider(ctx);
4388
4429
  const order = [active];
@@ -4396,6 +4437,10 @@ function resolveProviderOrder(ctx) {
4396
4437
  }
4397
4438
  return order;
4398
4439
  }
4440
+ function formatActiveStackShort(ctx, surface = "agentic_investigation") {
4441
+ const { provider, tier } = resolveModelForActive(ctx, surface);
4442
+ return `${provider} \xB7 ${tier}`;
4443
+ }
4399
4444
  var init_session_state = __esm({
4400
4445
  "src/ai/llm/session-state.ts"() {
4401
4446
  "use strict";
@@ -6297,7 +6342,7 @@ async function distillSessionFactsWithTimeout(ctx, sessionId, timeoutMs = DISTIL
6297
6342
  });
6298
6343
  const raced = await Promise.race([
6299
6344
  work,
6300
- new Promise((resolve7) => setTimeout(() => resolve7(-1), timeoutMs))
6345
+ new Promise((resolve8) => setTimeout(() => resolve8(-1), timeoutMs))
6301
6346
  ]);
6302
6347
  if (raced >= 0) return { count: raced, background };
6303
6348
  if (settled) return { count: await background, background };
@@ -6383,7 +6428,9 @@ function isSessionStale(s) {
6383
6428
  return Date.now() - s.mtime > STALE_SESSION_MS;
6384
6429
  }
6385
6430
  function isAnalysisReady(ctx) {
6386
- if (ctx.stage !== "analyzed" || ctx.analysis.completed.length === 0) return false;
6431
+ if (ctx.stage !== "analyzed" && ctx.stage !== "delivered" || ctx.analysis.completed.length === 0) {
6432
+ return false;
6433
+ }
6387
6434
  if (!ctx.dataset) return false;
6388
6435
  const counts = ctx.dataset.counts ?? {};
6389
6436
  return Object.values(counts).some((n) => n > 0);
@@ -6470,6 +6517,7 @@ function buildSessionFileSnapshot(ctx) {
6470
6517
  if (ctx.attachments && ctx.attachments.length > 0) file.attachments = ctx.attachments;
6471
6518
  if (ctx.llm && Object.keys(ctx.llm).length > 0) file.llm = ctx.llm;
6472
6519
  if (ctx.strategistState) file.strategist = ctx.strategistState;
6520
+ if (ctx.pendingAsk) file.pending_ask = ctx.pendingAsk;
6473
6521
  return file;
6474
6522
  }
6475
6523
  function defaultSessionAnalysis(primary = "gtm_health") {
@@ -6812,6 +6860,9 @@ async function finalizeSession(ctx, stage) {
6812
6860
  if (ctx.strategistState) {
6813
6861
  file.strategist = ctx.strategistState;
6814
6862
  }
6863
+ if (ctx.pendingAsk) {
6864
+ file.pending_ask = ctx.pendingAsk;
6865
+ }
6815
6866
  try {
6816
6867
  writeFileSync11(ctx.sessionFile, JSON.stringify(file, null, 2) + "\n");
6817
6868
  } catch {
@@ -6880,10 +6931,12 @@ function resetContextForSwitch(ctx, opts) {
6880
6931
  ctx.attachments = opts.attachments ?? [];
6881
6932
  ctx.llm = opts.llm;
6882
6933
  ctx.strategistState = opts.strategistState;
6934
+ ctx.pendingAsk = opts.pendingAsk;
6883
6935
  ctx.gapAudit = void 0;
6884
6936
  ctx.deliverIntent = false;
6885
6937
  ctx.computeInProgress = false;
6886
6938
  ctx.wizardDepth = 0;
6939
+ ctx.welcomeLogoShown = false;
6887
6940
  ctx.snapshot = { computeResult: null, divergences: [] };
6888
6941
  rebindSessionTranscript(ctx);
6889
6942
  }
@@ -9985,7 +10038,42 @@ var init_explore_mode = __esm({
9985
10038
  }
9986
10039
  });
9987
10040
 
10041
+ // src/conversation/recommended-action.ts
10042
+ function resolveRecommendedAction(ctx) {
10043
+ const phase = resolveConversationPhase(ctx);
10044
+ switch (phase) {
10045
+ case "explore":
10046
+ if (ctx.stage === "delivered") return { submit: "/end", hint: "home" };
10047
+ return canUseReplAi(ctx) ? null : { submit: "/connect", hint: "/connect" };
10048
+ case "awaiting_data":
10049
+ if (ctx.gapAudit?.can_compute) return { submit: "go ahead", hint: "go ahead" };
10050
+ if (!sessionHasData(ctx)) return { submit: "use demo data", hint: "use demo data" };
10051
+ return null;
10052
+ case "scope":
10053
+ return { submit: "yes", hint: "yes" };
10054
+ case "strategize":
10055
+ return ctx.strategistState?.step === "objective_confirm" ? { submit: "yes", hint: "yes" } : null;
10056
+ default:
10057
+ return null;
10058
+ }
10059
+ }
10060
+ var init_recommended_action = __esm({
10061
+ "src/conversation/recommended-action.ts"() {
10062
+ "use strict";
10063
+ init_repl_api();
10064
+ init_phase();
10065
+ }
10066
+ });
10067
+
9988
10068
  // src/conversation/phase.ts
10069
+ var phase_exports = {};
10070
+ __export(phase_exports, {
10071
+ buildConversationPrompt: () => buildConversationPrompt,
10072
+ formatPhaseLabel: () => formatPhaseLabel,
10073
+ getConversationPhaseBlock: () => getConversationPhaseBlock,
10074
+ resolveConversationPhase: () => resolveConversationPhase,
10075
+ sessionHasData: () => sessionHasData
10076
+ });
9989
10077
  import chalk3 from "chalk";
9990
10078
  function sessionHasData(ctx) {
9991
10079
  const counts = ctx.dataset?.counts ?? {};
@@ -9994,7 +10082,7 @@ function sessionHasData(ctx) {
9994
10082
  function resolveConversationPhase(ctx) {
9995
10083
  if (ctx.deliverIntent) return "deliver";
9996
10084
  if (ctx.computeInProgress) return "compute";
9997
- if (ctx.strategistState && ctx.strategistState.step !== "awaiting_analysis") {
10085
+ if (ctx.strategistState && ctx.strategistState.step !== "awaiting_analysis" && ctx.strategistState.step !== "awaiting_connect") {
9998
10086
  return "strategize";
9999
10087
  }
10000
10088
  if (isAnalysisReady(ctx)) return "explore";
@@ -10016,6 +10104,25 @@ function formatPhaseLabel(phase) {
10016
10104
  return phase.replace(/_/g, " ");
10017
10105
  }
10018
10106
  }
10107
+ function buildConversationPrompt(ctx) {
10108
+ const phase = resolveConversationPhase(ctx);
10109
+ const label = PROMPT_LABELS[phase];
10110
+ const scope = ctx.sessionName ? ` ${ctx.sessionName}` : "";
10111
+ if (phase === "orient") {
10112
+ return paint("accent", `${label} `);
10113
+ }
10114
+ const action = resolveRecommendedAction(ctx);
10115
+ if (phase === "explore") {
10116
+ const mode = defaultExploreResponseMode(ctx, Math.floor(ctx.messages.length / 2));
10117
+ const modeTag = mode === "brief" ? "brief" : "deep";
10118
+ const stack = canUseReplAi(ctx) ? formatActiveStackShort(ctx) : "no engine";
10119
+ const strategyWait = ctx.strategistState?.step === "awaiting_connect" ? chalk3.dim(" \xB7 strategy after /connect") : "";
10120
+ const enterHint2 = action ? chalk3.dim(` \xB7 \u23CE ${action.hint}`) : "";
10121
+ return paint("accent", `ask${scope} \u203A `) + chalk3.dim(`${modeTag} \xB7 ${stack}`) + strategyWait + enterHint2 + " ";
10122
+ }
10123
+ const enterHint = action ? chalk3.dim(`\u23CE ${action.hint} `) : "";
10124
+ return paint("accent", `${label.replace(" \u203A", "")}${scope} \u203A `) + enterHint;
10125
+ }
10019
10126
  function getConversationPhaseBlock(ctx) {
10020
10127
  const phase = resolveConversationPhase(ctx);
10021
10128
  const lines = [`Conversation phase: ${formatPhaseLabel(phase)}`];
@@ -10035,6 +10142,7 @@ function getConversationPhaseBlock(ctx) {
10035
10142
  }
10036
10143
  return lines.join("\n");
10037
10144
  }
10145
+ var PROMPT_LABELS;
10038
10146
  var init_phase = __esm({
10039
10147
  "src/conversation/phase.ts"() {
10040
10148
  "use strict";
@@ -10043,6 +10151,16 @@ var init_phase = __esm({
10043
10151
  init_explore_mode();
10044
10152
  init_session_state();
10045
10153
  init_theme();
10154
+ init_recommended_action();
10155
+ PROMPT_LABELS = {
10156
+ orient: "\u203A",
10157
+ scope: "scope \u203A",
10158
+ awaiting_data: "data \u203A",
10159
+ compute: "\u2026",
10160
+ explore: "ask \u203A",
10161
+ strategize: "strategy \u203A",
10162
+ deliver: "ship \u203A"
10163
+ };
10046
10164
  }
10047
10165
  });
10048
10166
 
@@ -10284,8 +10402,13 @@ var init_repl_globals = __esm({
10284
10402
  });
10285
10403
 
10286
10404
  // src/cli/prompts.ts
10405
+ var prompts_exports = {};
10406
+ __export(prompts_exports, {
10407
+ createPromptSession: () => createPromptSession
10408
+ });
10287
10409
  import { createInterface } from "readline/promises";
10288
10410
  import { clearLine, cursorTo } from "readline";
10411
+ import { StringDecoder } from "string_decoder";
10289
10412
  import chalk4 from "chalk";
10290
10413
  function marker() {
10291
10414
  return paint("accent", "ntrp \u203A ");
@@ -10390,16 +10513,24 @@ function createPromptSession(existing, ctx) {
10390
10513
  replRl.line = "";
10391
10514
  replRl.cursor = 0;
10392
10515
  }
10516
+ const wasRaw = stdin.isRaw === true;
10393
10517
  if (stdin.isTTY) stdin.setRawMode(true);
10394
10518
  rl.pause();
10519
+ const keypressListeners = stdin.rawListeners("keypress");
10520
+ for (const listener of keypressListeners) {
10521
+ stdin.removeListener("keypress", listener);
10522
+ }
10395
10523
  process.stdout.write("\n" + prompt);
10396
10524
  try {
10397
- return await new Promise((resolve7, reject) => {
10525
+ return await new Promise((resolve8, reject) => {
10398
10526
  let value = "";
10399
10527
  let settled = false;
10400
10528
  const cleanup = () => {
10401
10529
  stdin.off("data", onData);
10402
- if (stdin.isTTY && stdin.isRaw) stdin.setRawMode(false);
10530
+ for (const listener of keypressListeners) {
10531
+ stdin.addListener("keypress", listener);
10532
+ }
10533
+ if (stdin.isTTY) stdin.setRawMode(wasRaw);
10403
10534
  clearLine(process.stdout, 0);
10404
10535
  cursorTo(process.stdout, 0);
10405
10536
  rl.resume();
@@ -10418,16 +10549,16 @@ function createPromptSession(existing, ctx) {
10418
10549
  }
10419
10550
  };
10420
10551
  stdin.resume();
10421
- stdin.setEncoding("utf8");
10552
+ const decoder = new StringDecoder("utf8");
10422
10553
  const onData = (chunk) => {
10423
- const cleaned = stripTerminalArtifacts(chunk);
10554
+ const cleaned = stripTerminalArtifacts(typeof chunk === "string" ? chunk : decoder.write(chunk));
10424
10555
  for (const char of cleaned) {
10425
10556
  if (char === "\r" || char === "\n") {
10426
10557
  finish(() => {
10427
10558
  process.stdout.write("\n");
10428
10559
  const trimmed = stripTerminalArtifacts(value).trim();
10429
10560
  assertNotGlobalReplCommand(trimmed);
10430
- resolve7(trimmed);
10561
+ resolve8(trimmed);
10431
10562
  });
10432
10563
  return;
10433
10564
  }
@@ -10443,7 +10574,7 @@ function createPromptSession(existing, ctx) {
10443
10574
  process.stdout.write("\n");
10444
10575
  const trimmed = stripTerminalArtifacts(value).trim();
10445
10576
  assertNotGlobalReplCommand(trimmed);
10446
- resolve7(trimmed);
10577
+ resolve8(trimmed);
10447
10578
  });
10448
10579
  return;
10449
10580
  }
@@ -10470,6 +10601,7 @@ function createPromptSession(existing, ctx) {
10470
10601
  for (; ; ) {
10471
10602
  const value = await readMaskedLine(secretPromptLine(question), maskChar);
10472
10603
  if (!value) {
10604
+ if (opts.allowEmpty) return "";
10473
10605
  console.log(" " + chalk4.red("This one is required."));
10474
10606
  continue;
10475
10607
  }
@@ -10512,29 +10644,43 @@ var init_prompts = __esm({
10512
10644
 
10513
10645
  // src/conversation/gap-card.ts
10514
10646
  import chalk5 from "chalk";
10515
- function printGapCard(audit) {
10647
+ function printGapCard(audit, opts = {}) {
10516
10648
  console.log();
10517
- console.log(" " + chalk5.bold("Data check"));
10518
- if (audit.satisfied.length > 0) {
10519
- for (const item of audit.satisfied) {
10520
- console.log(" " + chalk5.green("\u2713") + " " + chalk5.dim(`${item.label}: ${item.detail}`));
10649
+ if (opts.skipSatisfied) {
10650
+ if (audit.missing.length > 0) {
10651
+ console.log(" " + chalk5.bold("Data check"));
10521
10652
  }
10653
+ } else if (audit.satisfied.length > 0) {
10654
+ const bits = audit.satisfied.map((item) => item.detail);
10655
+ console.log(
10656
+ " " + chalk5.green("\u2713") + " " + chalk5.bold("Data check") + chalk5.dim(" \u2014 " + bits.join(" \xB7 "))
10657
+ );
10658
+ } else {
10659
+ console.log(" " + chalk5.bold("Data check"));
10522
10660
  }
10523
10661
  for (const item of audit.missing) {
10524
10662
  console.log(" " + chalk5.red("\u2717") + " " + item.label + chalk5.dim(` \u2014 ${item.why}`));
10525
10663
  console.log(" " + chalk5.dim(item.suggestion));
10526
10664
  }
10527
- for (const item of audit.optional) {
10528
- console.log(" " + chalk5.yellow("~") + " " + chalk5.dim(`${item.label}: ${item.detail}`));
10665
+ if (audit.optional.length > 0) {
10666
+ const heads = audit.optional.map((item) => item.detail.split(" \u2014 ")[0] ?? item.detail);
10667
+ const joined = heads.join(" \xB7 ");
10668
+ if (joined.length <= 100) {
10669
+ console.log(" " + chalk5.yellow("~") + " " + chalk5.dim(joined));
10670
+ } else {
10671
+ for (const item of audit.optional) {
10672
+ console.log(" " + chalk5.yellow("~") + " " + chalk5.dim(`${item.label}: ${item.detail}`));
10673
+ }
10674
+ }
10529
10675
  }
10530
10676
  console.log();
10531
10677
  if (audit.can_compute) {
10532
10678
  console.log(
10533
- " " + chalk5.dim("Ready to compute \u2014 say ") + chalk5.cyan('"go ahead"') + chalk5.dim(" or ") + chalk5.cyan('"run analysis"')
10679
+ " " + chalk5.dim("Ready to compute \u2014 press ") + chalk5.cyan("\u23CE") + chalk5.dim(" or say ") + chalk5.cyan('"go ahead"')
10534
10680
  );
10535
10681
  } else if (audit.missing.length > 0) {
10536
10682
  console.log(
10537
- " " + chalk5.dim("Load data (paste a CSV path or say ") + chalk5.cyan("use demo data") + chalk5.dim(")")
10683
+ " " + chalk5.dim("Load data \u2014 paste a CSV path, or press ") + chalk5.cyan("\u23CE") + chalk5.dim(" to ") + chalk5.cyan("use demo data")
10538
10684
  );
10539
10685
  }
10540
10686
  console.log();
@@ -10545,6 +10691,22 @@ var init_gap_card = __esm({
10545
10691
  }
10546
10692
  });
10547
10693
 
10694
+ // src/ui/spinner.ts
10695
+ import ora from "ora";
10696
+ function makeSpinner(text, opts = {}) {
10697
+ return ora({
10698
+ text,
10699
+ color: "cyan",
10700
+ indent: opts.indent ?? 2,
10701
+ discardStdin: false
10702
+ }).start();
10703
+ }
10704
+ var init_spinner = __esm({
10705
+ "src/ui/spinner.ts"() {
10706
+ "use strict";
10707
+ }
10708
+ });
10709
+
10548
10710
  // src/metrics/companion.ts
10549
10711
  import chalk6 from "chalk";
10550
10712
  function getCompanionRecommendation(input) {
@@ -11556,7 +11718,7 @@ Restraint: you diagnose and prescribe the system; you do not build it here. Name
11556
11718
  - THEN THE DRIVERS. Support the headline with 2-3 distinct, non-overlapping drivers, each with its own number. If two points share a cause, merge them.
11557
11719
  - THEN THE SO-WHAT. Close with what it means for the decision at hand: the action, the owner-shaped next step, or the sharpest next cut.
11558
11720
  - THE RECALL TEST. A busy executive should be able to repeat your headline and one number to their CEO an hour later. If they couldn't, tighten it.
11559
- - ALTITUDE CONTROL. Answer at the altitude asked: a high-level question gets the 30,000-ft story (one narrative sentence, three numbers max) with an offer to descend; a "how do we fix it" question gets prescriptive, in-the-weeds steps \u2014 you have built this before, so prescribe the known-good sequence adapted to their numbers, not generic advice.`;
11721
+ - ALTITUDE CONTROL. Answer at the altitude asked: a high-level question gets the 30,000-ft story (one narrative sentence, three numbers max) with an offer to descend; a "how do we fix it" / plan-of-attack question prefers draft_strategy (or, if answering one play inline: one play + mechanism + what to verify \u2014 observe and recommend, never invent a multi-week Phase 1/2/3 program).`;
11560
11722
  FINDINGS_SCHEMA_BLOCK = `[
11561
11723
  {
11562
11724
  "severity": "critical" | "warning" | "info",
@@ -11799,24 +11961,31 @@ function padRight(text, width) {
11799
11961
  const gap = Math.max(0, width - visibleWidth(text));
11800
11962
  return `${text}${" ".repeat(gap)}`;
11801
11963
  }
11964
+ function padLeft(text, width) {
11965
+ const gap = Math.max(0, width - visibleWidth(text));
11966
+ return `${" ".repeat(gap)}${text}`;
11967
+ }
11802
11968
  function truncateVisible(text, maxVisible, ellipsis = "\u2026") {
11803
11969
  if (visibleWidth(text) <= maxVisible) return text;
11804
11970
  if (maxVisible <= ellipsis.length) return stripAnsi2(text).slice(0, maxVisible);
11805
11971
  const target = maxVisible - ellipsis.length;
11806
11972
  let visible = 0;
11807
11973
  let i = 0;
11974
+ let sawAnsi = false;
11808
11975
  while (i < text.length && visible < target) {
11809
11976
  if (text[i] === "\x1B") {
11810
11977
  const match = text.slice(i).match(/^\u001B\[[0-9;]*m/);
11811
11978
  if (match) {
11812
11979
  i += match[0].length;
11980
+ sawAnsi = true;
11813
11981
  continue;
11814
11982
  }
11815
11983
  }
11816
11984
  visible++;
11817
11985
  i++;
11818
11986
  }
11819
- return text.slice(0, i) + ellipsis;
11987
+ const reset = sawAnsi ? "\x1B[0m" : "";
11988
+ return text.slice(0, i) + reset + ellipsis;
11820
11989
  }
11821
11990
  function hr(width, ch = "\u2500") {
11822
11991
  return ch.repeat(Math.max(0, width));
@@ -11847,6 +12016,11 @@ function wrapWords(text, maxW) {
11847
12016
  function termWidth() {
11848
12017
  return process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
11849
12018
  }
12019
+ function resolveCardWidth(opts = {}) {
12020
+ const { min = 60, max = 100, margin = 4 } = opts;
12021
+ const usable = Math.max(20, termWidth() - margin);
12022
+ return Math.max(Math.min(min, usable), Math.min(usable, max));
12023
+ }
11850
12024
  var ANSI_RE;
11851
12025
  var init_layout = __esm({
11852
12026
  "src/ui/layout.ts"() {
@@ -12101,7 +12275,6 @@ var init_llm_attribution = __esm({
12101
12275
 
12102
12276
  // src/output/terminal.ts
12103
12277
  import chalk9 from "chalk";
12104
- import ora from "ora";
12105
12278
  import Table2 from "cli-table3";
12106
12279
  function centerPad(text, width) {
12107
12280
  if (text.length >= width) return text;
@@ -12109,19 +12282,6 @@ function centerPad(text, width) {
12109
12282
  const left = Math.floor(gap / 2);
12110
12283
  return " ".repeat(left) + text + " ".repeat(gap - left);
12111
12284
  }
12112
- function statusColor(status) {
12113
- switch (status) {
12114
- case "green":
12115
- return chalk9.green;
12116
- case "yellow":
12117
- return chalk9.yellow;
12118
- case "red":
12119
- return chalk9.red;
12120
- }
12121
- }
12122
- function statusDot(status) {
12123
- return statusColor(status)("\u25CF");
12124
- }
12125
12285
  function statusBadge(status) {
12126
12286
  switch (status) {
12127
12287
  case "green":
@@ -12136,9 +12296,9 @@ function printHeading(label, detail) {
12136
12296
  console.log(` ${sectionHeading(label)}${detail ? chalk9.dim(` ${detail}`) : ""}`);
12137
12297
  }
12138
12298
  function printResultCard(title, rows) {
12139
- const width = 70;
12299
+ const width = resolveCardWidth({ min: 60, max: 100, margin: 4 });
12140
12300
  const inner = width - 4;
12141
- const border = chalk9.dim;
12301
+ const border = (s) => paint("border", s);
12142
12302
  console.log();
12143
12303
  console.log(` ${border(`\u256D${"\u2500".repeat(width - 2)}\u256E`)}`);
12144
12304
  console.log(` ${border("\u2502 ")}${padRight(sectionHeading(title), inner)}${border(" \u2502")}`);
@@ -12154,20 +12314,33 @@ function printVitalSignRow(vs) {
12154
12314
  const label = VITAL_SIGN_LABELS[vs.vital_sign].padEnd(18);
12155
12315
  const bar = scoreBar(vs.score, vs.status);
12156
12316
  const score = String(Math.round(vs.score)).padStart(4);
12157
- const impact = vs.dollar_value != null && vs.dollar_value > 0 ? `${chalk9.green(formatCurrency(vs.dollar_value))} ${chalk9.dim(vs.dollar_label ?? "")}` : chalk9.dim("\u2014");
12317
+ const impact = vs.dollar_value != null && vs.dollar_value > 0 ? `${paint("success", formatCurrency(vs.dollar_value))} ${chalk9.dim(vs.dollar_label ?? "")}` : chalk9.dim("\u2014");
12158
12318
  console.log(` ${dot} ${label} ${bar} ${chalk9.bold(score)} ${chalk9.dim("\u2502")} ${impact}`);
12159
12319
  }
12160
12320
  function printHealthSummary(result, _pipelineMetrics) {
12161
12321
  const scoreStr = `${chalk9.bold(String(Math.round(result.overall_score)))}${chalk9.dim("/100")}`;
12162
- const impact = result.total_value_at_risk != null && result.total_value_at_risk > 0 ? `${chalk9.green(formatCurrency(result.total_value_at_risk))} ${chalk9.dim("total at risk")}` : chalk9.dim("No dollar-weighted risk detected");
12322
+ const impact = result.total_value_at_risk != null && result.total_value_at_risk > 0 ? `${paint("success", formatCurrency(result.total_value_at_risk))} ${chalk9.dim("total at risk")}` : chalk9.dim("No dollar-weighted risk detected");
12163
12323
  const next = result.overall_status === "red" ? actionHint("Next:", "/playbook", "review recommended plays") : result.overall_status === "yellow" ? actionHint("Next:", "/diagnose --deep", "investigate the weak signal") : actionHint("Next:", "/report", "export the clean snapshot");
12164
12324
  printResultCard("Overall Health", [
12165
- `${chalk9.dim("Score")} ${scoreStr} ${statusBadge(result.overall_status)}`,
12166
- `${chalk9.dim("Gated by")} ${paint("accent", VITAL_SIGN_LABELS[result.gating_vital_sign])}`,
12167
- `${chalk9.dim("Revenue")} ${impact}`,
12325
+ `${chalk9.dim("Score")} ${scoreStr} ${statusBadge(result.overall_status)}`,
12326
+ `${chalk9.dim("Held back by")} ${paint("accent", VITAL_SIGN_LABELS[result.gating_vital_sign])}`,
12327
+ `${chalk9.dim("Revenue")} ${impact}`,
12168
12328
  next
12169
12329
  ]);
12170
12330
  }
12331
+ function printHealthLine(result) {
12332
+ const score = `${chalk9.bold(String(Math.round(result.overall_score)))}${chalk9.dim("/100")}`;
12333
+ const parts = [
12334
+ `${chalk9.dim("Health")} ${score} ${statusBadge(result.overall_status)}`,
12335
+ `${chalk9.dim("held back by")} ${paint("accent", VITAL_SIGN_LABELS[result.gating_vital_sign])}`
12336
+ ];
12337
+ if (result.total_value_at_risk != null && result.total_value_at_risk > 0) {
12338
+ parts.push(`${paint("success", formatCurrency(result.total_value_at_risk))} ${chalk9.dim("total at risk")}`);
12339
+ }
12340
+ console.log();
12341
+ console.log(" " + parts.join(chalk9.dim(" \xB7 ")));
12342
+ console.log();
12343
+ }
12171
12344
  function printVitalSigns(vitals) {
12172
12345
  console.log();
12173
12346
  printHeading("Vital Signs");
@@ -12190,7 +12363,7 @@ function printSegmentSummary(segments) {
12190
12363
  }
12191
12364
  console.log();
12192
12365
  }
12193
- function printTopProblems(segments, limit = 7) {
12366
+ function printTopProblems(segments, limit = 7, opts = {}) {
12194
12367
  if (segments.length === 0) return;
12195
12368
  const problems = [];
12196
12369
  for (const seg of segments) {
@@ -12208,6 +12381,7 @@ function printTopProblems(segments, limit = 7) {
12208
12381
  }
12209
12382
  }
12210
12383
  if (problems.length === 0) {
12384
+ if (opts.compact) return;
12211
12385
  printHeading("Top Problems");
12212
12386
  console.log();
12213
12387
  console.log(" " + chalk9.dim("No dollar-weighted problems found across segments."));
@@ -12220,6 +12394,22 @@ function printTopProblems(segments, limit = 7) {
12220
12394
  const vitalW = Math.max("Vital Sign".length, ...top.map((p) => p.vitalSignLabel.length));
12221
12395
  const dollarStrs = top.map((p) => formatCurrency(p.dollarValue));
12222
12396
  const dollarW = Math.max(...dollarStrs.map((s) => s.length));
12397
+ if (opts.compact) {
12398
+ console.log(` ${sectionHeading("Top Problems")}`);
12399
+ for (let i = 0; i < top.length; i++) {
12400
+ const p = top[i];
12401
+ console.log(
12402
+ ` ${statusDot(p.status)} ${p.segment.padEnd(segW)} ${p.vitalSignLabel.padEnd(vitalW)} ${paint("success", dollarStrs[i].padStart(dollarW))} ${chalk9.dim(p.dollarLabel)}`
12403
+ );
12404
+ }
12405
+ if (problems.length > top.length) {
12406
+ console.log(
12407
+ " " + chalk9.dim(`${problems.length - top.length} more \u2014 `) + paint("accent", "/diagnose") + chalk9.dim(" for the full report")
12408
+ );
12409
+ }
12410
+ console.log();
12411
+ return;
12412
+ }
12223
12413
  const labelW = Math.max("".length, ...top.map((p) => p.dollarLabel.length));
12224
12414
  const impactW = dollarW + 2 + labelW;
12225
12415
  console.log(
@@ -12237,7 +12427,7 @@ function printTopProblems(segments, limit = 7) {
12237
12427
  const dot = statusDot(p.status);
12238
12428
  const seg = p.segment.padEnd(segW);
12239
12429
  const vital = p.vitalSignLabel.padEnd(vitalW);
12240
- const dollar = chalk9.green(dollarStrs[i].padStart(dollarW));
12430
+ const dollar = paint("success", dollarStrs[i].padStart(dollarW));
12241
12431
  const label = chalk9.dim(p.dollarLabel);
12242
12432
  console.log(` ${dot} ${seg} ${vital} ${dollar} ${label}`);
12243
12433
  }
@@ -12247,26 +12437,26 @@ function printTopProblems(segments, limit = 7) {
12247
12437
  }
12248
12438
  console.log();
12249
12439
  }
12440
+ function printFindingCard(finding) {
12441
+ const dot = severityPaint(finding.severity)("\u25CF");
12442
+ const dollarTag = finding.dollar_value != null && finding.dollar_value > 0 ? ` ${chalk9.dim("\xB7")} ${paint("success", formatDollarValue(finding.dollar_value))}` : "";
12443
+ console.log(` ${dot} ${chalk9.bold(finding.segment)}${dollarTag}`);
12444
+ printMarkdown(finding.finding, { indent: 2 });
12445
+ if (finding.recommended_plays && finding.recommended_plays.length > 0) {
12446
+ for (const play of finding.recommended_plays) {
12447
+ console.log(
12448
+ ` ${chalk9.dim("Consider:")} ${paint("accent", play.play_name)} ${chalk9.dim("\u2192")} ${chalk9.dim("/playbook " + play.play_id)}`
12449
+ );
12450
+ }
12451
+ }
12452
+ console.log();
12453
+ }
12250
12454
  function printFindings(findings) {
12251
12455
  if (findings.length === 0) {
12252
12456
  console.log(chalk9.dim(" No findings generated."));
12253
12457
  return;
12254
12458
  }
12255
- for (const finding of findings) {
12256
- const sevColor = finding.severity === "critical" ? chalk9.red : finding.severity === "warning" ? chalk9.yellow : chalk9.blue;
12257
- const dot = sevColor("\u25CF");
12258
- const dollarTag = finding.dollar_value != null && finding.dollar_value > 0 ? ` ${chalk9.dim("\xB7")} ${chalk9.green(formatDollarValue(finding.dollar_value))}` : "";
12259
- console.log(` ${dot} ${chalk9.bold(finding.segment)}${dollarTag}`);
12260
- printMarkdown(finding.finding, { indent: 2 });
12261
- if (finding.recommended_plays && finding.recommended_plays.length > 0) {
12262
- for (const play of finding.recommended_plays) {
12263
- console.log(
12264
- ` ${chalk9.dim("Consider:")} ${paint("accent", play.play_name)} ${chalk9.dim("\u2192")} ${chalk9.dim("/playbook " + play.play_id)}`
12265
- );
12266
- }
12267
- }
12268
- console.log();
12269
- }
12459
+ for (const finding of findings) printFindingCard(finding);
12270
12460
  }
12271
12461
  function printEntityCounts(counts) {
12272
12462
  const table = new Table2({
@@ -12281,23 +12471,23 @@ function printEntityCounts(counts) {
12281
12471
  console.log();
12282
12472
  }
12283
12473
  function printSegmentDetail(seg, aggregate) {
12284
- const color = statusColor(seg.result.overall_status);
12474
+ const color = statusPaint(seg.result.overall_status);
12285
12475
  console.log();
12286
12476
  printHeading(seg.segment.name);
12287
12477
  console.log(
12288
- ` ${statusDot(seg.result.overall_status)} ${color(chalk9.bold(formatScore(seg.result.overall_score)))}${chalk9.dim("/100")} ${chalk9.dim("Gated by:")} ${paint("accent", VITAL_SIGN_LABELS[seg.result.gating_vital_sign])}`
12478
+ ` ${statusDot(seg.result.overall_status)} ${color(chalk9.bold(formatScore(seg.result.overall_score)))}${chalk9.dim("/100")} ${chalk9.dim("Held back by:")} ${paint("accent", VITAL_SIGN_LABELS[seg.result.gating_vital_sign])}`
12289
12479
  );
12290
12480
  console.log();
12291
12481
  for (const vs of seg.result.vital_signs) {
12292
12482
  const aggVs = aggregate.vital_signs.find((a) => a.vital_sign === vs.vital_sign);
12293
12483
  const delta = aggVs ? vs.score - aggVs.score : 0;
12294
- const deltaStr = delta >= 0 ? chalk9.green(`+${Math.round(delta)}`) : chalk9.red(`${Math.round(delta)}`);
12484
+ const deltaStr = delta >= 0 ? paint("success", `+${Math.round(delta)}`) : paint("error", `${Math.round(delta)}`);
12295
12485
  const dot = statusDot(vs.status);
12296
12486
  const label = VITAL_SIGN_LABELS[vs.vital_sign].padEnd(18);
12297
12487
  const bar = scoreBar(vs.score, vs.status);
12298
12488
  const score = String(Math.round(vs.score)).padStart(4);
12299
- const impact = vs.dollar_value != null && vs.dollar_value > 0 ? `${chalk9.green(formatCurrency(vs.dollar_value))} ${chalk9.dim(vs.dollar_label ?? "")}` : chalk9.dim("\u2014");
12300
- console.log(` ${dot} ${label} ${bar} ${chalk9.bold(score)} ${deltaStr.padStart(12)} ${chalk9.dim("\u2502")} ${impact}`);
12489
+ const impact = vs.dollar_value != null && vs.dollar_value > 0 ? `${paint("success", formatCurrency(vs.dollar_value))} ${chalk9.dim(vs.dollar_label ?? "")}` : chalk9.dim("\u2014");
12490
+ console.log(` ${dot} ${label} ${bar} ${chalk9.bold(score)} ${padLeft(deltaStr, 4)} ${chalk9.dim("\u2502")} ${impact}`);
12301
12491
  }
12302
12492
  console.log();
12303
12493
  }
@@ -12305,11 +12495,11 @@ function formatToolName(name) {
12305
12495
  return name.replace(/_/g, " ").replace(/^(get|query)\s+/, "examining ");
12306
12496
  }
12307
12497
  async function renderDiagnoseStream(options) {
12308
- const { diagnoseGenerator, runFindings, storeFindings, deep } = options;
12498
+ const { diagnoseGenerator, runFindings, storeFindings, deep, compact } = options;
12309
12499
  console.log();
12310
12500
  printHeading("Vital Signs");
12311
12501
  console.log();
12312
- const spinner = ora({ text: "Prefetching snapshot\u2026", indent: 2, discardStdin: false }).start();
12502
+ const spinner = makeSpinner("Prefetching snapshot\u2026");
12313
12503
  let fullResult = null;
12314
12504
  try {
12315
12505
  for await (const event of diagnoseGenerator()) {
@@ -12344,25 +12534,44 @@ async function renderDiagnoseStream(options) {
12344
12534
  if (!fullResult) {
12345
12535
  throw new Error("Diagnose stream ended without a complete event");
12346
12536
  }
12347
- console.log();
12348
- printHealthSummary(fullResult.aggregate);
12349
- if (fullResult.segments.length > 0) {
12350
- printTopProblems(fullResult.segments);
12537
+ if (compact) {
12538
+ printHealthLine(fullResult.aggregate);
12539
+ if (fullResult.segments.length > 0) {
12540
+ printTopProblems(fullResult.segments, 3, { compact: true });
12541
+ }
12542
+ } else {
12543
+ console.log();
12544
+ printHealthSummary(fullResult.aggregate);
12545
+ if (fullResult.segments.length > 0) {
12546
+ printTopProblems(fullResult.segments);
12547
+ }
12351
12548
  }
12352
12549
  let collectedFindings = [];
12353
12550
  if (runFindings) {
12354
- const findingsSpinner = ora({
12355
- text: deep ? "Investigating (agentic)\u2026" : "Generating findings\u2026",
12356
- indent: 2,
12357
- discardStdin: false
12358
- }).start();
12551
+ const findingsSpinner = makeSpinner(deep ? "Investigating (agentic)\u2026" : "Generating findings\u2026");
12359
12552
  let toolCalls = 0;
12360
- let iterations = 0;
12553
+ let printed = 0;
12554
+ let headingPrinted = false;
12361
12555
  let modelUsed = "";
12362
12556
  let providerUsed;
12363
12557
  let failover;
12364
12558
  let notices;
12365
12559
  let rawPrompt = "";
12560
+ const printStreamedFinding = (finding, stillStreaming) => {
12561
+ findingsSpinner.stop();
12562
+ if (!headingPrinted) {
12563
+ printHeading(deep ? "Investigation Findings" : "Findings");
12564
+ console.log();
12565
+ headingPrinted = true;
12566
+ }
12567
+ printFindingCard(finding);
12568
+ printed++;
12569
+ if (stillStreaming) {
12570
+ findingsSpinner.start(
12571
+ `Found ${printed} finding${printed === 1 ? "" : "s"} \u2014 still investigating\u2026`
12572
+ );
12573
+ }
12574
+ };
12366
12575
  try {
12367
12576
  for await (const event of runFindings(fullResult)) {
12368
12577
  if (event.type === "tool_call") {
@@ -12370,9 +12579,11 @@ async function renderDiagnoseStream(options) {
12370
12579
  findingsSpinner.text = formatToolName(event.name);
12371
12580
  } else if (event.type === "finding") {
12372
12581
  collectedFindings.push(event.finding);
12373
- iterations++;
12374
- findingsSpinner.text = `Found ${collectedFindings.length} finding${collectedFindings.length === 1 ? "" : "s"}\u2026`;
12582
+ printStreamedFinding(event.finding, true);
12375
12583
  } else if (event.type === "done") {
12584
+ for (const finding of event.findings.slice(printed)) {
12585
+ printStreamedFinding(finding, false);
12586
+ }
12376
12587
  collectedFindings = event.findings;
12377
12588
  modelUsed = event.model_used;
12378
12589
  providerUsed = event.provider_used;
@@ -12382,10 +12593,20 @@ async function renderDiagnoseStream(options) {
12382
12593
  }
12383
12594
  }
12384
12595
  findingsSpinner.stop();
12385
- console.log(
12386
- chalk9.dim(` Investigating... called ${toolCalls} tools across ${iterations} iterations`)
12387
- );
12388
- console.log();
12596
+ if (!headingPrinted) {
12597
+ printHeading(deep ? "Investigation Findings" : "Findings");
12598
+ console.log();
12599
+ }
12600
+ if (collectedFindings.length === 0) {
12601
+ console.log(chalk9.dim(" No findings generated."));
12602
+ console.log();
12603
+ }
12604
+ if (toolCalls > 0) {
12605
+ console.log(
12606
+ chalk9.dim(` Investigated with ${toolCalls} tool call${toolCalls === 1 ? "" : "s"}`)
12607
+ );
12608
+ console.log();
12609
+ }
12389
12610
  if (storeFindings) {
12390
12611
  await storeFindings({
12391
12612
  findings: collectedFindings,
@@ -12395,9 +12616,6 @@ async function renderDiagnoseStream(options) {
12395
12616
  failover
12396
12617
  });
12397
12618
  }
12398
- printHeading(deep ? "Investigation Findings" : "Findings");
12399
- console.log();
12400
- printFindings(collectedFindings);
12401
12619
  printLlmAttribution({
12402
12620
  model_used: modelUsed,
12403
12621
  provider_used: providerUsed,
@@ -12411,18 +12629,6 @@ async function renderDiagnoseStream(options) {
12411
12629
  }
12412
12630
  return { fullResult, findings: collectedFindings };
12413
12631
  }
12414
- function metricStatusDot(status) {
12415
- switch (status) {
12416
- case "green":
12417
- return chalk9.green("\u25CF");
12418
- case "yellow":
12419
- return chalk9.yellow("\u25CF");
12420
- case "red":
12421
- return chalk9.red("\u25CF");
12422
- case "neutral":
12423
- return chalk9.dim("\u25CB");
12424
- }
12425
- }
12426
12632
  function printMetricsTable(metrics, groupOrder) {
12427
12633
  for (const group of groupOrder) {
12428
12634
  const groupMetrics = metrics.filter((m) => m.group === group);
@@ -12430,10 +12636,10 @@ function printMetricsTable(metrics, groupOrder) {
12430
12636
  printHeading(group);
12431
12637
  console.log();
12432
12638
  for (const m of groupMetrics) {
12433
- const dot = m.unavailable_reason ? chalk9.dim("\u25CB") : metricStatusDot(m.status);
12639
+ const dot = m.unavailable_reason ? statusDot("neutral") : statusDot(m.status);
12434
12640
  const label = m.label.padEnd(28);
12435
12641
  const valueStr = m.unavailable_reason ? chalk9.dim("--") : chalk9.bold(m.formatted);
12436
- const confTag = m.confidence != null && m.confidence < 80 && m.confidence_label ? chalk9.yellow(` ${m.confidence_label} (${m.confidence})`) : m.confidence != null && m.confidence_label === "high" ? chalk9.dim(" \u2713") : "";
12642
+ const confTag = m.confidence != null && m.confidence < 80 && m.confidence_label ? paint("warning", ` ${m.confidence_label} (${m.confidence})`) : "";
12437
12643
  const note = m.unavailable_reason ? chalk9.dim(m.unavailable_reason) : m.benchmark_note ? chalk9.dim(m.benchmark_note) : "";
12438
12644
  console.log(` ${dot} ${label} ${valueStr}${confTag}${note ? " " + note : ""}`);
12439
12645
  if (m.reliability_gate?.requirements?.length && (m.confidence ?? 100) < 80) {
@@ -12449,6 +12655,7 @@ function printMetricsTable(metrics, groupOrder) {
12449
12655
  var init_terminal = __esm({
12450
12656
  "src/output/terminal.ts"() {
12451
12657
  "use strict";
12658
+ init_spinner();
12452
12659
  init_formatters();
12453
12660
  init_markdown();
12454
12661
  init_theme();
@@ -12487,7 +12694,7 @@ function renderMetricsReport(ctx, metrics, coverage, sourceType, options = {}) {
12487
12694
  console.log(" " + bold("Pattern checks"));
12488
12695
  console.log();
12489
12696
  for (const insight of deterministic.slice(0, 5)) {
12490
- const dot = insight.severity === "warning" ? chalk10.yellow("\u25CF") : insight.severity === "critical" ? chalk10.red("\u25CF") : chalk10.dim("\u25CB");
12697
+ const dot = insight.severity === "warning" ? statusDot("yellow") : insight.severity === "critical" ? statusDot("red") : statusDot("neutral");
12491
12698
  if (insight.headline) continue;
12492
12699
  console.log(` ${dot} ${chalk10.dim(wrapInsight(insight.message))}`);
12493
12700
  }
@@ -12603,6 +12810,69 @@ var init_divergence = __esm({
12603
12810
  }
12604
12811
  });
12605
12812
 
12813
+ // src/ai/json-stream.ts
12814
+ var StreamingJsonArrayParser;
12815
+ var init_json_stream = __esm({
12816
+ "src/ai/json-stream.ts"() {
12817
+ "use strict";
12818
+ StreamingJsonArrayParser = class {
12819
+ buf = "";
12820
+ pos = 0;
12821
+ inArray = false;
12822
+ arrayClosed = false;
12823
+ inString = false;
12824
+ escaped = false;
12825
+ depth = 0;
12826
+ elementStart = -1;
12827
+ /** Feed a chunk; returns the objects completed by this chunk, in order. */
12828
+ push(chunk) {
12829
+ this.buf += chunk;
12830
+ const out = [];
12831
+ if (this.arrayClosed) return out;
12832
+ while (this.pos < this.buf.length) {
12833
+ const ch = this.buf[this.pos];
12834
+ if (!this.inArray) {
12835
+ if (ch === "[") this.inArray = true;
12836
+ this.pos++;
12837
+ continue;
12838
+ }
12839
+ if (this.inString) {
12840
+ if (this.escaped) this.escaped = false;
12841
+ else if (ch === "\\") this.escaped = true;
12842
+ else if (ch === '"') this.inString = false;
12843
+ this.pos++;
12844
+ continue;
12845
+ }
12846
+ if (ch === '"') {
12847
+ this.inString = true;
12848
+ } else if (ch === "{") {
12849
+ if (this.depth === 0) this.elementStart = this.pos;
12850
+ this.depth++;
12851
+ } else if (ch === "}") {
12852
+ if (this.depth > 0) {
12853
+ this.depth--;
12854
+ if (this.depth === 0 && this.elementStart >= 0) {
12855
+ const raw = this.buf.slice(this.elementStart, this.pos + 1);
12856
+ this.elementStart = -1;
12857
+ try {
12858
+ out.push(JSON.parse(raw));
12859
+ } catch {
12860
+ }
12861
+ }
12862
+ }
12863
+ } else if (ch === "]" && this.depth === 0) {
12864
+ this.arrayClosed = true;
12865
+ this.pos++;
12866
+ break;
12867
+ }
12868
+ this.pos++;
12869
+ }
12870
+ return out;
12871
+ }
12872
+ };
12873
+ }
12874
+ });
12875
+
12606
12876
  // src/ai/findings.ts
12607
12877
  function companyContextSection() {
12608
12878
  const block = buildCompanyProfileBlock();
@@ -12711,6 +12981,8 @@ async function* streamFindings(input, ctx) {
12711
12981
  assertReplAi(ctx);
12712
12982
  const userMessage = buildUserMessage(input);
12713
12983
  let meta = { model_used: "unknown", provider_used: "anthropic", failover: false };
12984
+ const scanner = new StreamingJsonArrayParser();
12985
+ let streamed = 0;
12714
12986
  for await (const event of streamWithFailover(
12715
12987
  {
12716
12988
  surface: "findings",
@@ -12720,12 +12992,18 @@ async function* streamFindings(input, ctx) {
12720
12992
  },
12721
12993
  { ctx }
12722
12994
  )) {
12995
+ if (event.type === "text_delta") {
12996
+ for (const parsed of scanner.push(event.text)) {
12997
+ streamed++;
12998
+ yield { type: "finding", finding: parsed };
12999
+ }
13000
+ }
12723
13001
  if (event.type === "done") {
12724
13002
  meta = event.meta;
12725
13003
  const parsed = parseJsonArrayFromText(event.response.text);
12726
13004
  if (parsed === null) throw new Error("AI response is not valid JSON");
12727
13005
  const findings = parsed;
12728
- for (const finding of findings) yield { type: "finding", finding };
13006
+ for (const finding of findings.slice(streamed)) yield { type: "finding", finding };
12729
13007
  yield {
12730
13008
  type: "done",
12731
13009
  findings,
@@ -12744,6 +13022,7 @@ var init_findings = __esm({
12744
13022
  init_failover();
12745
13023
  init_prompt_parts();
12746
13024
  init_json_response();
13025
+ init_json_stream();
12747
13026
  }
12748
13027
  });
12749
13028
 
@@ -13021,15 +13300,15 @@ __export(diagnose_exports, {
13021
13300
  handler: () => handler
13022
13301
  });
13023
13302
  import chalk11 from "chalk";
13024
- import ora2 from "ora";
13025
13303
  async function handler(args, ctx) {
13026
13304
  await hydrateAnalysisFromPersistedState(ctx);
13027
- const { flags } = parseArgs(args, ["findings", "deep"]);
13305
+ const { flags } = parseArgs(args, ["findings", "deep", "compact"]);
13028
13306
  const options = {
13029
13307
  findings: getBool(flags, "findings") || getBool(flags, "deep"),
13030
13308
  deep: getBool(flags, "deep"),
13031
13309
  segments: !getFalse(flags, "segments"),
13032
- segment: getString(flags, "segment")
13310
+ segment: getString(flags, "segment"),
13311
+ compact: getBool(flags, "compact")
13033
13312
  };
13034
13313
  if (isStructuredOutput(ctx.execution)) {
13035
13314
  try {
@@ -13073,10 +13352,11 @@ async function handler(args, ctx) {
13073
13352
  markLensCompleted(ctx, "gtm_health");
13074
13353
  if (ctx.stage === "new") ctx.stage = "analyzed";
13075
13354
  saveSessionState(ctx);
13076
- if (!ctx.oneShot && !isStructuredOutput(ctx.execution)) {
13355
+ if (!ctx.oneShot && !isStructuredOutput(ctx.execution) && !ctx.suppressCompanionFooter) {
13077
13356
  const companion = await resolveCompanionRecommendation(ctx);
13078
13357
  printCompanionFooter(ctx, companion, { justCompleted: "gtm_health" });
13079
13358
  }
13359
+ ctx.suppressCompanionFooter = false;
13080
13360
  if (!ctx.skipTimeBankDiagnoseCredit) {
13081
13361
  creditDiagnoseComplete(ctx, options.findings);
13082
13362
  }
@@ -13135,7 +13415,8 @@ async function runDiagnose(options, ctx) {
13135
13415
  diagnoseGenerator,
13136
13416
  runFindings,
13137
13417
  storeFindings: storeFindingsFn,
13138
- deep: options.deep
13418
+ deep: options.deep,
13419
+ compact: options.compact
13139
13420
  });
13140
13421
  return buildDiagnoseSummary(fullResult.aggregate, findings);
13141
13422
  } catch (err) {
@@ -13144,7 +13425,7 @@ async function runDiagnose(options, ctx) {
13144
13425
  }
13145
13426
  }
13146
13427
  async function runSegmentDiagnose(options) {
13147
- const spinner = ora2({ text: "Computing vital signs...", discardStdin: false }).start();
13428
+ const spinner = makeSpinner("Computing vital signs\u2026");
13148
13429
  let result;
13149
13430
  try {
13150
13431
  result = await computeFullHealth();
@@ -13203,6 +13484,7 @@ function buildDiagnoseSummary(aggregate, findings) {
13203
13484
  var init_diagnose = __esm({
13204
13485
  "src/commands/diagnose.ts"() {
13205
13486
  "use strict";
13487
+ init_spinner();
13206
13488
  init_schema();
13207
13489
  init_segments();
13208
13490
  init_divergence();
@@ -14261,11 +14543,90 @@ function validateStrategistPlan(raw, opts) {
14261
14543
  };
14262
14544
  return { plan, issues, measurableTargets, totalTargets };
14263
14545
  }
14546
+ function buildGroundedFallbackPlan(input) {
14547
+ const today = parseIsoDate(input.todayIso) ?? /* @__PURE__ */ new Date();
14548
+ const triggered = matchTriggeredPlays(input.vitals, LAYERS);
14549
+ const issues = [
14550
+ "LLM plan JSON invalid \u2014 using grounded fallback from triggered plays and live vitals"
14551
+ ];
14552
+ const sources = triggered.length > 0 ? triggered.slice(0, 3) : input.vitals.slice().sort((a, b) => a.score - b.score).slice(0, 2).map((vital) => {
14553
+ const play = getPlaybook().find((p) => p.trigger_vital_sign === vital.vital_sign) ?? getPlaybook()[0];
14554
+ return { play, vital, layer: 1 };
14555
+ });
14556
+ const workstreams = sources.map(({ play, vital }, index) => {
14557
+ const score = Math.round(vital.score);
14558
+ const dollar = vital.dollar_value != null && vital.dollar_value > 0 ? `$${Math.round(vital.dollar_value).toLocaleString("en-US")}` : null;
14559
+ const baseline = dollar ?? String(score);
14560
+ const targetLow = vital.dollar_value != null && vital.dollar_value > 0 ? `$${Math.round(vital.dollar_value * 0.4).toLocaleString("en-US")}` : String(Math.min(100, score + 20));
14561
+ const targetHigh = vital.dollar_value != null && vital.dollar_value > 0 ? `$${Math.round(vital.dollar_value * 0.6).toLocaleString("en-US")}` : String(Math.min(100, score + 35));
14562
+ const checkDate = toIso(addDays(today, 21 + index * 7));
14563
+ const outcome = {
14564
+ metric: vital.vital_sign,
14565
+ baseline,
14566
+ target_range: `${baseline} -> ${targetLow}-${targetHigh}`,
14567
+ check_date: checkDate,
14568
+ measured_by: `${vital.vital_sign} vital sign`
14569
+ };
14570
+ return {
14571
+ order: index + 1,
14572
+ title: play.name,
14573
+ problem: `${vital.vital_sign} score ${score} (${vital.status})${dollar ? ` \u2014 ${dollar} ${vital.dollar_label ?? ""}`.trimEnd() : ""}`,
14574
+ rationale: index === 0 ? "Layer-order first: clean or unblock the gating vital before downstream work" : "Next in dependency order after the prior workstream",
14575
+ play_ids: [play.id],
14576
+ actions: play.steps.slice(0, 3),
14577
+ effort_hours: 8 + index * 4,
14578
+ milestones: [
14579
+ {
14580
+ label: `Check ${vital.vital_sign} movement`,
14581
+ due: checkDate,
14582
+ verification: `${vital.vital_sign} score moves toward ${targetLow}-${targetHigh} (baseline ${baseline})`
14583
+ }
14584
+ ],
14585
+ deliverables: [
14586
+ {
14587
+ label: `${play.name} triage list`,
14588
+ kind: "artifact",
14589
+ due: toIso(addDays(today, 7 + index * 7))
14590
+ }
14591
+ ],
14592
+ expected_outcome: outcome,
14593
+ leading_indicators: [],
14594
+ contingency: {
14595
+ trigger: `${vital.vital_sign} flat or worse at first check`,
14596
+ trigger_check_date: checkDate,
14597
+ fallback: "Descope to the single highest-dollar entity cohort and re-run /strategy review"
14598
+ }
14599
+ };
14600
+ });
14601
+ const gating = input.gatingVitalSign ?? sources[0]?.vital.vital_sign ?? "freshness";
14602
+ const varLabel = input.totalValueAtRisk != null && input.totalValueAtRisk > 0 ? `$${Math.round(input.totalValueAtRisk).toLocaleString("en-US")} at risk` : "material pipeline dollars at risk";
14603
+ const plan = {
14604
+ title: "Grounded recovery plan",
14605
+ objective: input.objective,
14606
+ summary_30k: `${gating} is the gating pressure (${varLabel}). This fallback sequences ${workstreams.length} playbook workstream(s) in layer order so data trust and handoffs unlock pipeline movement. Treat baselines as live vital readings; refine with /strategy after the first review.`,
14607
+ hypothesis: "If plays execute in layer order against the live vital scores, the objective metrics should move into the stated ranges within one review cycle.",
14608
+ target_segment: "Whole pipeline",
14609
+ priority: "high",
14610
+ review_cadence: "Weekly",
14611
+ confidence: 0.45,
14612
+ constraints: ["Generated without a validated LLM plan JSON \u2014 confirm capacity before staffing"],
14613
+ assumptions: ["Outcome ranges are heuristic halves/increments of live vitals, not model-authored forecasts"],
14614
+ risks: ["Fallback plans lack stress-test revisions \u2014 run /strategy once the engine emits valid JSON"],
14615
+ workstreams
14616
+ };
14617
+ return {
14618
+ plan,
14619
+ issues,
14620
+ measurableTargets: workstreams.length,
14621
+ totalTargets: workstreams.length
14622
+ };
14623
+ }
14264
14624
  var NUMBER_RE, SUFFIX_MULTIPLIER, INSTRUMENT_TOKENS, ISO_DATE_RE;
14265
14625
  var init_strategist_validate = __esm({
14266
14626
  "src/ai/strategist-validate.ts"() {
14267
14627
  "use strict";
14268
14628
  init_playbook();
14629
+ init_health_score();
14269
14630
  init_json_response();
14270
14631
  NUMBER_RE = /\$?\s*(\d[\d,]*\.?\d*)\s*(m|k|b|million|thousand|billion)?\b/gi;
14271
14632
  SUFFIX_MULTIPLIER = {
@@ -14406,14 +14767,14 @@ async function* strategistPlanSession(options) {
14406
14767
  let lastMeta = { provider_used: "anthropic", model_used: "unknown" };
14407
14768
  const loopGuard = new ToolLoopGuard();
14408
14769
  const allowedTools = new Set(tools.map((t) => t.name));
14409
- const callLlm = async (surface, withTools) => {
14770
+ const callLlm = async (surface, withTools, maxTokens = STAGE_MAX_TOKENS) => {
14410
14771
  const attempt = () => completeWithFailover(
14411
14772
  {
14412
14773
  surface,
14413
14774
  messages,
14414
14775
  system: systemPrompt,
14415
14776
  tools: withTools && tools.length > 0 ? tools : void 0,
14416
- max_tokens: STAGE_MAX_TOKENS
14777
+ max_tokens: maxTokens
14417
14778
  },
14418
14779
  { tier: tierForSurface(surface, llmCfg.tier), ctx: options.ctx }
14419
14780
  );
@@ -14428,11 +14789,17 @@ async function* strategistPlanSession(options) {
14428
14789
  lastMeta = result.meta;
14429
14790
  return result.response;
14430
14791
  };
14431
- async function* runStage(surface, maxRounds, budgetNudge) {
14792
+ async function* runStage(surface, maxRounds, budgetNudge, requireJson = false) {
14432
14793
  for (let round = 0; round < maxRounds; round++) {
14433
14794
  const response = await callLlm(surface, true);
14434
14795
  if (response.tool_calls.length === 0) {
14435
14796
  messages.push(response.assistant_message);
14797
+ if (requireJson && !parseJsonObjectFromText(response.text)) {
14798
+ messages.push({ role: "user", content: budgetNudge });
14799
+ const forced = await callLlm(surface, false, PLAN_JSON_MAX_TOKENS);
14800
+ messages.push(forced.assistant_message);
14801
+ return forced.text;
14802
+ }
14436
14803
  return response.text;
14437
14804
  }
14438
14805
  messages.push(response.assistant_message);
@@ -14449,10 +14816,26 @@ async function* strategistPlanSession(options) {
14449
14816
  }
14450
14817
  }
14451
14818
  messages.push({ role: "user", content: budgetNudge });
14452
- const final = await callLlm(surface, false);
14819
+ const final = await callLlm(surface, false, requireJson ? PLAN_JSON_MAX_TOKENS : STAGE_MAX_TOKENS);
14453
14820
  messages.push(final.assistant_message);
14454
14821
  return final.text;
14455
14822
  }
14823
+ const vitalsForFallback = options.computeResult.aggregate.vital_signs.map((v) => ({
14824
+ vital_sign: v.vital_sign,
14825
+ score: v.score,
14826
+ status: v.status,
14827
+ dollar_value: v.dollar_value,
14828
+ dollar_label: v.dollar_label
14829
+ }));
14830
+ function groundedFallback() {
14831
+ return buildGroundedFallbackPlan({
14832
+ objective: options.objective,
14833
+ vitals: vitalsForFallback,
14834
+ todayIso,
14835
+ gatingVitalSign: options.computeResult.aggregate.gating_vital_sign,
14836
+ totalValueAtRisk: options.computeResult.aggregate.total_value_at_risk
14837
+ });
14838
+ }
14456
14839
  yield { type: "stage", stage: "ground", label: STAGE_LABELS.ground };
14457
14840
  const digestText = yield* runStage(
14458
14841
  "strategist",
@@ -14466,34 +14849,78 @@ async function* strategistPlanSession(options) {
14466
14849
  const digestForEvidence = digest ? JSON.stringify(digest) : digestText;
14467
14850
  yield { type: "stage", stage: "backcast", label: STAGE_LABELS.backcast };
14468
14851
  messages.push({ role: "user", content: buildBackcastMessage(options.objective) });
14469
- yield* runStage(
14852
+ const backcastText = yield* runStage(
14470
14853
  "strategist",
14471
14854
  BACKCAST_MAX_ROUNDS,
14472
- "Tool budget reached for planning. Respond with the full plan JSON now \u2014 strict JSON only."
14855
+ "Tool budget reached for planning. Respond with the full plan JSON now \u2014 strict JSON only. No hypotheses, no prose.",
14856
+ true
14473
14857
  );
14858
+ const evidenceTextEarly = `${digestForEvidence}
14859
+ ${healthSnapshot}`;
14860
+ let candidatePlan = validatePlanText(
14861
+ backcastText,
14862
+ evidenceTextEarly,
14863
+ todayIso
14864
+ );
14865
+ if (!candidatePlan) {
14866
+ const why = describePlanValidationFailure(backcastText, evidenceTextEarly, todayIso);
14867
+ messages.push({
14868
+ role: "user",
14869
+ content: `Backcast output did not validate (${why}). Respond with ONLY the full plan JSON object now \u2014 no hypotheses, no tools.
14870
+ Keep \u22643 workstreams. Schema:
14871
+ ${STRATEGIST_PLAN_SCHEMA_BLOCK}`
14872
+ });
14873
+ const forced = await callLlm("strategist", false, PLAN_JSON_MAX_TOKENS);
14874
+ messages.push(forced.assistant_message);
14875
+ candidatePlan = validatePlanText(forced.text, evidenceTextEarly, todayIso);
14876
+ }
14877
+ if (!candidatePlan) {
14878
+ candidatePlan = groundedFallback();
14879
+ yield {
14880
+ type: "notice",
14881
+ text: "Backcast JSON invalid \u2014 armed grounded playbook fallback if stress-test also fails."
14882
+ };
14883
+ } else {
14884
+ yield {
14885
+ type: "notice",
14886
+ text: "Backcast plan validated \u2014 will use it if the stress-test revision fails validation."
14887
+ };
14888
+ }
14474
14889
  yield { type: "stage", stage: "stress", label: STAGE_LABELS.stress };
14475
14890
  messages.push({ role: "user", content: buildStressTestMessage() });
14476
14891
  const finalText = yield* runStage(
14477
14892
  "strategist_stress",
14478
14893
  STRESS_MAX_ROUNDS,
14479
- "Tool budget reached. Respond with the FINAL revised plan JSON now \u2014 strict JSON only."
14894
+ "Tool budget reached. Respond with the FINAL revised plan JSON now \u2014 strict JSON only. No prose. \u22643 workstreams.",
14895
+ true
14480
14896
  );
14481
- const evidenceText = `${digestForEvidence}
14482
- ${healthSnapshot}`;
14897
+ const evidenceText = evidenceTextEarly;
14483
14898
  let validated = validatePlanText(finalText, evidenceText, todayIso);
14484
14899
  if (!validated) {
14900
+ const why = describePlanValidationFailure(finalText, evidenceText, todayIso);
14485
14901
  messages.push({
14486
14902
  role: "user",
14487
- content: "That response did not validate as a usable plan (parseable JSON object with at least one workstream containing a measurable outcome or dated milestone). Respond with ONLY the corrected plan JSON in the required schema."
14903
+ content: `That response did not validate as a usable plan. Reason: ${why}
14904
+ Respond with ONLY the corrected plan JSON object in the required schema (title, objective, summary_30k, workstreams with measurable expected_outcome, dated milestones, contingency). \u22643 workstreams. Copy baselines from the health snapshot numbers exactly.`
14488
14905
  });
14489
- const retry = await callLlm("strategist_stress", false);
14906
+ const retry = await callLlm("strategist_stress", false, PLAN_JSON_MAX_TOKENS);
14490
14907
  messages.push(retry.assistant_message);
14491
14908
  validated = validatePlanText(retry.text, evidenceText, todayIso);
14492
14909
  }
14910
+ if (!validated && candidatePlan) {
14911
+ const fromFallback = candidatePlan.issues.some((i) => i.includes("grounded fallback"));
14912
+ yield {
14913
+ type: "notice",
14914
+ text: fromFallback ? "Stress-test revision invalid \u2014 using grounded playbook fallback plan." : "Stress-test revision invalid \u2014 using backcast plan."
14915
+ };
14916
+ validated = candidatePlan;
14917
+ }
14493
14918
  if (!validated) {
14494
- throw new Error(
14495
- "The strategist could not produce a structurally valid plan. Try again, or narrow the objective (e.g. one vital sign or segment)."
14496
- );
14919
+ validated = groundedFallback();
14920
+ yield {
14921
+ type: "notice",
14922
+ text: "Strategist JSON failed validation \u2014 emitting grounded fallback plan from live vitals."
14923
+ };
14497
14924
  }
14498
14925
  for (const issue of validated.issues) {
14499
14926
  yield { type: "notice", text: issue };
@@ -14519,7 +14946,21 @@ function validatePlanText(text, evidenceText, todayIso) {
14519
14946
  if (!raw) return null;
14520
14947
  return validateStrategistPlan(raw, { evidenceText, todayIso });
14521
14948
  }
14522
- var GROUND_MAX_ROUNDS, BACKCAST_MAX_ROUNDS, STRESS_MAX_ROUNDS, STAGE_MAX_TOKENS, STAGE_LABELS;
14949
+ function describePlanValidationFailure(text, evidenceText, todayIso) {
14950
+ const raw = parseJsonObjectFromText(text);
14951
+ if (!raw) {
14952
+ if (text.includes("{") && !text.trim().endsWith("}")) {
14953
+ return "JSON object appears truncated (increase brevity: \u22643 workstreams) or incomplete";
14954
+ }
14955
+ return "not parseable as a JSON object (prose or truncated output)";
14956
+ }
14957
+ const result = validateStrategistPlan(raw, { evidenceText, todayIso });
14958
+ if (!result) {
14959
+ return "JSON parsed but no usable workstreams remained after measurability checks (need \u22651 workstream with measurable outcome or dated milestone)";
14960
+ }
14961
+ return "unknown validation failure";
14962
+ }
14963
+ var GROUND_MAX_ROUNDS, BACKCAST_MAX_ROUNDS, STRESS_MAX_ROUNDS, STAGE_MAX_TOKENS, PLAN_JSON_MAX_TOKENS, STAGE_LABELS;
14523
14964
  var init_strategist2 = __esm({
14524
14965
  "src/ai/strategist.ts"() {
14525
14966
  "use strict";
@@ -14535,10 +14976,12 @@ var init_strategist2 = __esm({
14535
14976
  init_thread();
14536
14977
  init_strategist_prompt();
14537
14978
  init_strategist_validate();
14979
+ init_strategist_prompt();
14538
14980
  GROUND_MAX_ROUNDS = 6;
14539
14981
  BACKCAST_MAX_ROUNDS = 4;
14540
14982
  STRESS_MAX_ROUNDS = 2;
14541
14983
  STAGE_MAX_TOKENS = 4096;
14984
+ PLAN_JSON_MAX_TOKENS = 8192;
14542
14985
  STAGE_LABELS = {
14543
14986
  ground: "Grounding \u2014 reading health, metrics, segments, history",
14544
14987
  backcast: "Sequencing \u2014 backcasting from objective",
@@ -14657,9 +15100,9 @@ __export(strategist_flow_exports, {
14657
15100
  promptQueuedAiStrategist: () => promptQueuedAiStrategist,
14658
15101
  queueStrategistForAnalysis: () => queueStrategistForAnalysis,
14659
15102
  resumeStrategistAfterCompute: () => resumeStrategistAfterCompute,
15103
+ resumeStrategistAfterConnect: () => resumeStrategistAfterConnect,
14660
15104
  startStrategistFlow: () => startStrategistFlow
14661
15105
  });
14662
- import ora3 from "ora";
14663
15106
  import chalk13 from "chalk";
14664
15107
  function isStrategistIntent(input) {
14665
15108
  const line = input.trim();
@@ -14799,9 +15242,13 @@ async function runStrategistSession(ctx) {
14799
15242
  if (!canUseReplAi(ctx)) {
14800
15243
  await printKeylessSkeletonPlan(ctx, objective);
14801
15244
  if (ctx.scope) ctx.scope.intent_summary = objective;
14802
- ctx.strategistState = void 0;
15245
+ ctx.strategistState = {
15246
+ ...state2,
15247
+ step: "awaiting_connect",
15248
+ objective
15249
+ };
14803
15250
  saveSessionState(ctx);
14804
- return "Skeleton plan (no key)";
15251
+ return "Skeleton plan (awaiting connect)";
14805
15252
  }
14806
15253
  if (ctx.rl && !state2.constraintsNote) {
14807
15254
  const prompts = createPromptSession(ctx.rl, ctx);
@@ -14817,7 +15264,7 @@ async function runStrategistSession(ctx) {
14817
15264
  }
14818
15265
  }
14819
15266
  console.log();
14820
- const spinner = ora3({ text: "Grounding\u2026", color: "cyan", indent: 2, discardStdin: false }).start();
15267
+ const spinner = makeSpinner("Grounding\u2026");
14821
15268
  let plan = null;
14822
15269
  let stats = { measurable_targets: 0, total_targets: 0 };
14823
15270
  let baselineBatchId = null;
@@ -14932,7 +15379,7 @@ async function runStrategistSession(ctx) {
14932
15379
  }
14933
15380
  async function ensureSnapshot(ctx) {
14934
15381
  if (ctx.snapshot.computeResult) return ctx.snapshot.computeResult;
14935
- const spinner = ora3({ text: "Reading latest vitals\u2026", color: "cyan", indent: 2, discardStdin: false }).start();
15382
+ const spinner = makeSpinner("Reading latest vitals\u2026");
14936
15383
  try {
14937
15384
  const snapshot = await computeFullHealth();
14938
15385
  ctx.snapshot.computeResult = snapshot;
@@ -14960,7 +15407,7 @@ function printObjectiveCard(ctx, objective, proposed) {
14960
15407
  );
14961
15408
  console.log();
14962
15409
  console.log(
14963
- " " + chalk13.dim("Confirm? ") + chalk13.cyan("yes") + chalk13.dim(" \xB7 ") + chalk13.cyan("adjust") + chalk13.dim(" \xB7 ") + chalk13.cyan("cancel")
15410
+ " " + chalk13.dim("Confirm? ") + chalk13.cyan("\u23CE yes") + chalk13.dim(" \xB7 ") + chalk13.cyan("adjust") + chalk13.dim(" \xB7 ") + chalk13.cyan("cancel")
14964
15411
  );
14965
15412
  console.log();
14966
15413
  }
@@ -15006,17 +15453,28 @@ async function printKeylessSkeletonPlan(ctx, objective) {
15006
15453
  }
15007
15454
  }
15008
15455
  console.log(
15009
- " " + chalk13.dim("For the full strategist \u2014 milestones, outcome ranges, contingencies \u2014 run ") + paint("accent", "/connect") + chalk13.dim(" and paste any provider's key.")
15456
+ " " + chalk13.dim("For the full strategist \u2014 milestones, outcome ranges, contingencies \u2014 press ") + paint("accent", "\u23CE") + chalk13.dim(" to run ") + paint("accent", "/connect") + chalk13.dim(" and paste any provider's key.")
15010
15457
  );
15011
15458
  console.log(
15012
- " " + chalk13.dim("Objective kept \u2014 once connected, say ") + chalk13.cyan('"how should we fix this?"') + chalk13.dim(" or run ") + paint("accent", "/strategy") + chalk13.dim(" to build the full plan.")
15459
+ " " + chalk13.dim("Objective kept \u2014 after ") + paint("accent", "/connect") + chalk13.dim(" I'll bring back the confirm card so you can run the full plan.")
15013
15460
  );
15014
15461
  console.log();
15015
15462
  }
15463
+ async function resumeStrategistAfterConnect(ctx) {
15464
+ const state2 = ctx.strategistState;
15465
+ if (!state2 || state2.step !== "awaiting_connect" || !state2.objective) return false;
15466
+ state2.step = "objective_confirm";
15467
+ saveSessionState(ctx);
15468
+ console.log();
15469
+ console.log(" " + paint("accent", "Engine connected \u2014 ready to build the full strategy."));
15470
+ printObjectiveCard(ctx, state2.objective, true);
15471
+ return true;
15472
+ }
15016
15473
  var STRATEGIST_INTENT_RE, CANCEL_RE, CONFIRM_RE, ADJUST_RE, QUESTION_RE;
15017
15474
  var init_strategist_flow = __esm({
15018
15475
  "src/conversation/strategist-flow.ts"() {
15019
15476
  "use strict";
15477
+ init_spinner();
15020
15478
  init_context2();
15021
15479
  init_handoff_draft();
15022
15480
  init_repl_api();
@@ -15038,99 +15496,1003 @@ var init_strategist_flow = __esm({
15038
15496
  }
15039
15497
  });
15040
15498
 
15041
- // src/conversation/compute.ts
15042
- var compute_exports2 = {};
15043
- __export(compute_exports2, {
15044
- isComputeIntent: () => isComputeIntent,
15045
- runConversationCompute: () => runConversationCompute
15499
+ // src/conversation/keyless-ask.ts
15500
+ var keyless_ask_exports = {};
15501
+ __export(keyless_ask_exports, {
15502
+ isKeylessVitalsAsk: () => isKeylessVitalsAsk,
15503
+ tryKeylessAskAnswer: () => tryKeylessAskAnswer
15046
15504
  });
15047
- import ora4 from "ora";
15048
15505
  import chalk14 from "chalk";
15049
- async function runConversationCompute(ctx) {
15050
- const lens = ctx.scope?.primary_lens ?? ctx.analysis.primary;
15051
- ctx.computeInProgress = true;
15052
- try {
15053
- if (lens === "revenue_metrics") {
15054
- const { runMetricsAnalysis: runMetricsAnalysis2, extractHeadlineMetrics: extractHeadlineMetrics2 } = await Promise.resolve().then(() => (init_metrics_analysis(), metrics_analysis_exports));
15055
- const { renderMetricsReport: renderMetricsReport2 } = await Promise.resolve().then(() => (init_metrics_report(), metrics_report_exports));
15056
- const spinner = ora4({ text: "Computing SaaS metrics\u2026", indent: 2, discardStdin: false }).start();
15057
- try {
15058
- const result = await runMetricsAnalysis2({
15059
- findings: false,
15060
- sessionAnalysis: ctx.analysis
15061
- });
15062
- spinner.succeed("SaaS metrics computed");
15063
- ctx.analysis.coverage = result.coverage;
15064
- ctx.analysis.data_source_type = result.data_source_type;
15065
- ctx.analysis.recommended = result.companion_recommendation;
15066
- ctx.analysis.headline = extractHeadlineMetrics2(result.metrics.aggregate.metrics);
15067
- markLensCompleted(ctx, "revenue_metrics");
15068
- ctx.stage = "analyzed";
15069
- ctx.snapshot.computeResult = null;
15070
- invalidateGapAudit(ctx);
15071
- saveSessionState(ctx);
15072
- creditGapCompute(ctx);
15073
- creditMetricsComplete(ctx, false);
15074
- renderMetricsReport2(ctx, result.metrics.aggregate.metrics, result.coverage, result.data_source_type, {
15075
- snapshot: result.snapshot,
15076
- findings: result.findings,
15077
- companion: result.companion_recommendation ?? null,
15078
- interactive: true
15079
- });
15080
- await resumeQueuedStrategist(ctx);
15081
- return "SaaS metrics ready";
15082
- } catch (err) {
15083
- spinner.fail("Metrics failed");
15084
- throw err;
15085
- }
15506
+ function isKeylessVitalsAsk(input) {
15507
+ return KEYLESS_ASK_RE.test(input.trim());
15508
+ }
15509
+ function pickMostExpensive(vitals) {
15510
+ let best = null;
15511
+ for (const vs of vitals) {
15512
+ const dollars = vs.dollar_value ?? 0;
15513
+ if (!best) {
15514
+ best = vs;
15515
+ continue;
15086
15516
  }
15087
- const { handler: diagnose } = await Promise.resolve().then(() => (init_diagnose(), diagnose_exports));
15088
- ctx.skipTimeBankDiagnoseCredit = true;
15089
- creditGapCompute(ctx);
15090
- const summary = await diagnose([], ctx);
15091
- markLensCompleted(ctx, "gtm_health");
15092
- ctx.stage = "analyzed";
15093
- ctx.snapshot.computeResult = null;
15094
- invalidateGapAudit(ctx);
15095
- saveSessionState(ctx);
15096
- const companion = await resolveCompanionRecommendation(ctx);
15097
- printCompanionFooter(ctx, companion, { justCompleted: "gtm_health" });
15098
- await resumeQueuedStrategist(ctx);
15099
- return typeof summary === "string" ? summary : "Health analysis ready";
15100
- } catch (err) {
15101
- console.error(" " + chalk14.red(String(err.message ?? err)));
15102
- return;
15103
- } finally {
15104
- ctx.computeInProgress = false;
15105
- ctx.deliverIntent = false;
15517
+ const bestDollars = best.dollar_value ?? 0;
15518
+ if (dollars > bestDollars) best = vs;
15519
+ else if (dollars === bestDollars && vs.score < best.score) best = vs;
15106
15520
  }
15521
+ return best;
15107
15522
  }
15108
- async function resumeQueuedStrategist(ctx) {
15109
- if (ctx.strategistState?.step !== "awaiting_analysis") return;
15110
- ctx.computeInProgress = false;
15111
- const { resumeStrategistAfterCompute: resumeStrategistAfterCompute2 } = await Promise.resolve().then(() => (init_strategist_flow(), strategist_flow_exports));
15112
- await resumeStrategistAfterCompute2(ctx);
15523
+ function formatVitalLine(vs) {
15524
+ const label = VITAL_SIGN_LABELS[vs.vital_sign] ?? vs.vital_sign;
15525
+ const dollars = vs.dollar_value != null && vs.dollar_value > 0 ? `${formatCurrency(vs.dollar_value)} ${vs.dollar_label ?? ""}`.trim() : null;
15526
+ return dollars ? `${label} \u2014 score ${Math.round(vs.score)}, ${dollars}` : `${label} \u2014 score ${Math.round(vs.score)} (${vs.status})`;
15113
15527
  }
15114
- function isComputeIntent(input) {
15115
- return /\b(run analysis|compute|go ahead|analyze|let'?s go|do it)\b/i.test(input.trim());
15528
+ function formatRunnerBit(vs) {
15529
+ const label = VITAL_SIGN_LABELS[vs.vital_sign] ?? vs.vital_sign;
15530
+ const dollars = `${formatCurrency(vs.dollar_value ?? 0)} ${vs.dollar_label ?? ""}`.trim();
15531
+ return `${label} ${dollars}`;
15532
+ }
15533
+ async function tryKeylessAskAnswer(ctx, input, opts = {}) {
15534
+ if (!isKeylessVitalsAsk(input)) return false;
15535
+ let snapshot = ctx.snapshot.computeResult;
15536
+ if (!snapshot) {
15537
+ try {
15538
+ snapshot = await computeFullHealth();
15539
+ ctx.snapshot.computeResult = snapshot;
15540
+ } catch {
15541
+ return false;
15542
+ }
15543
+ }
15544
+ const aggregate = snapshot.aggregate;
15545
+ const gating = aggregate.vital_signs.find((v) => v.vital_sign === aggregate.gating_vital_sign);
15546
+ const expensive = pickMostExpensive(aggregate.vital_signs);
15547
+ const primary = expensive ?? gating;
15548
+ if (!primary) return false;
15549
+ const label = VITAL_SIGN_LABELS[primary.vital_sign] ?? primary.vital_sign;
15550
+ const dollarBit = primary.dollar_value != null && primary.dollar_value > 0 ? `${formatCurrency(primary.dollar_value)} ${primary.dollar_label ?? ""}`.trim() : null;
15551
+ const headline = dollarBit ? `${label} \u2014 ${dollarBit} \u2014 is the most expensive problem to solve right now.` : `${label} (score ${Math.round(primary.score)}, ${primary.status}) is the problem to fix first.`;
15552
+ const runners = [...aggregate.vital_signs].filter((v) => v.vital_sign !== primary.vital_sign && (v.dollar_value ?? 0) > 0).sort((a, b) => (b.dollar_value ?? 0) - (a.dollar_value ?? 0)).slice(0, 2);
15553
+ console.log();
15554
+ console.log(" " + chalk14.bold(headline));
15555
+ if (opts.fromResume) {
15556
+ if (runners.length > 0) {
15557
+ console.log(
15558
+ " " + chalk14.dim("Next after that: ") + chalk14.dim(runners.map(formatRunnerBit).join(" \xB7 "))
15559
+ );
15560
+ }
15561
+ } else {
15562
+ console.log();
15563
+ if (gating && gating.vital_sign !== primary.vital_sign) {
15564
+ console.log(
15565
+ " " + chalk14.dim("Gating vital: ") + paint("accent", VITAL_SIGN_LABELS[gating.vital_sign]) + chalk14.dim(` (score ${Math.round(gating.score)}) \u2014 it bounds what you can trust downstream.`)
15566
+ );
15567
+ }
15568
+ if (runners.length > 0) {
15569
+ console.log(" " + chalk14.dim("Also on the board:"));
15570
+ for (const vs of runners) {
15571
+ console.log(" " + chalk14.dim("\xB7 ") + formatVitalLine(vs));
15572
+ }
15573
+ }
15574
+ if (aggregate.total_value_at_risk != null && aggregate.total_value_at_risk > 0) {
15575
+ console.log(
15576
+ " " + chalk14.dim("Total at risk: ") + chalk14.green(formatCurrency(aggregate.total_value_at_risk))
15577
+ );
15578
+ }
15579
+ }
15580
+ console.log();
15581
+ console.log(
15582
+ " " + chalk14.dim("Press ") + paint("accent", "\u23CE") + chalk14.dim(" to connect a key (") + paint("accent", "/connect") + chalk14.dim(") for the why and the plan \u2014 I'll finish this question when you do.")
15583
+ );
15584
+ console.log();
15585
+ if (!opts.fromResume) {
15586
+ recordMessage(ctx, "user", input);
15587
+ }
15588
+ recordMessage(ctx, "agent", headline);
15589
+ saveSessionState(ctx);
15590
+ return true;
15591
+ }
15592
+ var KEYLESS_ASK_RE;
15593
+ var init_keyless_ask = __esm({
15594
+ "src/conversation/keyless-ask.ts"() {
15595
+ "use strict";
15596
+ init_context2();
15597
+ init_formatters();
15598
+ init_theme();
15599
+ init_health_score();
15600
+ KEYLESS_ASK_RE = /\b(most expensive|biggest risk|expensive problem|what should (i|we) fix|fix first|biggest problem|largest risk|where (are we|do we) (bleed|leak|hurt))\b/i;
15601
+ }
15602
+ });
15603
+
15604
+ // src/conversation/orchestrator.ts
15605
+ import chalk15 from "chalk";
15606
+ import { writeFileSync as writeFileSync13 } from "fs";
15607
+ import { join as join19 } from "path";
15608
+ async function handleExploreWithoutKey(ctx, input) {
15609
+ if (isKeylessVitalsAsk(input)) {
15610
+ queuePendingAsk(ctx, input, "explore");
15611
+ const answered = await tryKeylessAskAnswer(ctx, input);
15612
+ if (answered) {
15613
+ if (ctx.pendingAsk) {
15614
+ ctx.pendingAsk = { ...ctx.pendingAsk, keylessAnswered: true };
15615
+ saveSessionState(ctx);
15616
+ }
15617
+ return;
15618
+ }
15619
+ }
15620
+ const holder = ctx;
15621
+ const hits = (holder[NO_KEY_NUDGES] ?? 0) + 1;
15622
+ holder[NO_KEY_NUDGES] = hits;
15623
+ recordMessage(ctx, "user", input);
15624
+ if (looksLikeQuestion(input)) {
15625
+ queuePendingAsk(ctx, input, "explore");
15626
+ }
15627
+ if (hits === 1) {
15628
+ console.log();
15629
+ console.log(" " + chalk15.red("AI interpretation needs an LLM API key saved in config."));
15630
+ console.log(
15631
+ " " + chalk15.dim("Run ") + paint("accent", "/connect") + chalk15.dim(" and paste any provider's key (Anthropic, OpenAI, Groq, Gemini, ...).")
15632
+ );
15633
+ console.log(" " + chalk15.dim("Number crunching works without a key \u2014 only Q&A in the REPL needs one."));
15634
+ if (ctx.pendingAsk) {
15635
+ console.log(
15636
+ " " + chalk15.dim("Your question is queued \u2014 I'll answer it right after ") + paint("accent", "/connect") + chalk15.dim(".")
15637
+ );
15638
+ }
15639
+ if (ctx.gapAudit) {
15640
+ printGapCard(ctx.gapAudit);
15641
+ }
15642
+ console.log();
15643
+ recordMessage(
15644
+ ctx,
15645
+ "agent",
15646
+ "No LLM engine connected \u2014 Q&A needs a key. Pointed to /connect."
15647
+ );
15648
+ return;
15649
+ }
15650
+ console.log();
15651
+ console.log(" " + chalk15.yellow("Still no engine connected \u2014 Q&A stays offline until you run ") + paint("accent", "/connect") + chalk15.yellow("."));
15652
+ console.log(" " + chalk15.dim("These work without one:"));
15653
+ console.log(" " + paint("accent", "/playbook") + chalk15.dim(" recommended plays from your computed vitals"));
15654
+ console.log(" " + chalk15.cyan('"how should we fix this?"') + chalk15.dim(" deterministic skeleton plan"));
15655
+ console.log(" " + paint("accent", "/handoff") + chalk15.dim(" export this analysis for another tool"));
15656
+ console.log();
15657
+ recordMessage(
15658
+ ctx,
15659
+ "agent",
15660
+ "No LLM engine connected \u2014 offered keyless paths (/playbook, skeleton plan, /handoff)."
15661
+ );
15662
+ }
15663
+ var NO_KEY_NUDGES;
15664
+ var init_orchestrator = __esm({
15665
+ "src/conversation/orchestrator.ts"() {
15666
+ "use strict";
15667
+ init_context2();
15668
+ init_store();
15669
+ init_theme();
15670
+ init_phase();
15671
+ init_scope();
15672
+ init_gap_audit();
15673
+ init_gap_card();
15674
+ init_compute2();
15675
+ init_handoff_draft();
15676
+ init_prompts();
15677
+ init_time_bank();
15678
+ init_pending_ask();
15679
+ init_keyless_ask();
15680
+ NO_KEY_NUDGES = /* @__PURE__ */ Symbol.for("ntrp.noKeyNudges");
15681
+ }
15682
+ });
15683
+
15684
+ // src/repositories/bundle.ts
15685
+ async function buildRepositoryExportPackage(options) {
15686
+ await initSchema();
15687
+ const bundle = await loadSessionAnalysisBundle();
15688
+ const diagnosis = bundle.diagnosis;
15689
+ if (!diagnosis) {
15690
+ if (bundle.metrics) {
15691
+ throw new NtrpError(
15692
+ "diagnosis_required",
15693
+ "Publish packages require a GTM health snapshot. Run /diagnose (companion) or /handoff report for metrics-only export.",
15694
+ 4 /* NoData */
15695
+ );
15696
+ }
15697
+ if (!hasAnyAnalysis(bundle)) {
15698
+ throw new NtrpError("diagnosis_required", "No analysis data found. Run /new, /diagnose, or /metrics first.", 4 /* NoData */);
15699
+ }
15700
+ throw new NtrpError("diagnosis_required", "No diagnosis data found. Run /diagnose first.", 4 /* NoData */);
15701
+ }
15702
+ const strategies = await listStrategies("all");
15703
+ const strategiesWithSources = await Promise.all(
15704
+ strategies.map(async (strategy) => ({
15705
+ strategy,
15706
+ sources: await listStrategySources(strategy.id)
15707
+ }))
15708
+ );
15709
+ const proposals = await listActionProposals(100);
15710
+ const actions = await Promise.all(
15711
+ proposals.map(async (proposal) => ({
15712
+ proposal,
15713
+ executions: await listActionExecutions(proposal.id)
15714
+ }))
15715
+ );
15716
+ const sections = buildSections({
15717
+ diagnosis,
15718
+ strategiesCount: strategiesWithSources.length,
15719
+ actionsCount: actions.length
15720
+ });
15721
+ return {
15722
+ schema_version: "ntrp.repository_export.v1",
15723
+ export_id: uuid(),
15724
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
15725
+ target: options.target,
15726
+ summary: {
15727
+ overall_score: diagnosis.health.overall_score,
15728
+ overall_status: diagnosis.health.overall_status,
15729
+ gating_vital_sign: diagnosis.health.gating_vital_sign,
15730
+ total_value_at_risk: diagnosis.health.total_value_at_risk ?? null,
15731
+ findings_count: diagnosis.findings.length,
15732
+ strategies_count: strategiesWithSources.length,
15733
+ action_proposals_count: actions.length
15734
+ },
15735
+ diagnosis: {
15736
+ health: diagnosis.health,
15737
+ segments: diagnosis.segments.map((segment) => ({
15738
+ segment: segment.segment,
15739
+ result: segment.result
15740
+ })),
15741
+ findings: diagnosis.findings,
15742
+ entity_counts: diagnosis.entityCounts,
15743
+ upload_batch_id: diagnosis.uploadBatchId
15744
+ },
15745
+ strategies: strategiesWithSources,
15746
+ actions,
15747
+ sections,
15748
+ provenance: {
15749
+ command: options.command ?? "publish",
15750
+ model_or_fixture: options.modelOrFixture,
15751
+ source: options.source ?? "local_duckdb",
15752
+ notes: [
15753
+ "Generated from the latest persisted diagnosis.",
15754
+ "Repository writes are approval-gated through local action proposals."
15755
+ ]
15756
+ }
15757
+ };
15758
+ }
15759
+ function buildSections(input) {
15760
+ const { diagnosis } = input;
15761
+ const health = diagnosis.health;
15762
+ return [
15763
+ {
15764
+ id: "cover",
15765
+ title: "Cover Summary",
15766
+ summary: `${health.overall_score}/100 ${health.overall_status}, gated by ${health.gating_vital_sign}`,
15767
+ markdown: [
15768
+ `Overall score: **${health.overall_score}/100** (${health.overall_status})`,
15769
+ `Gating vital sign: **${VITAL_SIGN_LABELS[health.gating_vital_sign]}**`,
15770
+ `Total value at risk: **${health.total_value_at_risk ? formatCurrency(health.total_value_at_risk) : "N/A"}**`,
15771
+ `Findings: **${diagnosis.findings.length}**`,
15772
+ `Strategies: **${input.strategiesCount}**`,
15773
+ `Action proposals: **${input.actionsCount}**`
15774
+ ].join("\n\n")
15775
+ },
15776
+ {
15777
+ id: "vital-signs",
15778
+ title: "Vital Signs",
15779
+ summary: `${health.vital_signs.length} vital signs`,
15780
+ markdown: health.vital_signs.map((vs) => `- **${VITAL_SIGN_LABELS[vs.vital_sign]}:** ${Math.round(vs.score)}/100 (${vs.status}) \u2014 ${formatDollarImpact(vs.dollar_value, vs.dollar_label)}`).join("\n"),
15781
+ children: health.vital_signs.map((vs) => ({
15782
+ id: `vital-${vs.vital_sign}`,
15783
+ title: `${VITAL_SIGN_LABELS[vs.vital_sign]}: ${Math.round(vs.score)}/100`,
15784
+ summary: formatDollarImpact(vs.dollar_value, vs.dollar_label),
15785
+ markdown: [
15786
+ `Status: **${vs.status}**`,
15787
+ `Value: **${formatDollarImpact(vs.dollar_value, vs.dollar_label)}**`,
15788
+ `Flagged entities: **${vs.entity_details.length}**`,
15789
+ "",
15790
+ "Components:",
15791
+ "```json",
15792
+ JSON.stringify(vs.components, null, 2),
15793
+ "```",
15794
+ "",
15795
+ "Top entity details:",
15796
+ "```json",
15797
+ JSON.stringify(vs.entity_details.slice(0, 25), null, 2),
15798
+ "```"
15799
+ ].join("\n"),
15800
+ metadata: { vital_sign: vs.vital_sign }
15801
+ }))
15802
+ },
15803
+ {
15804
+ id: "findings",
15805
+ title: "Findings and Deep Analysis",
15806
+ summary: `${diagnosis.findings.length} findings`,
15807
+ markdown: diagnosis.findings.length > 0 ? diagnosis.findings.map((finding) => `- **${finding.severity.toUpperCase()}** ${finding.segment}: ${finding.finding}`).join("\n") : "_No findings recorded._",
15808
+ children: diagnosis.findings.map((finding, index) => ({
15809
+ id: `finding-${index + 1}`,
15810
+ title: `${finding.severity.toUpperCase()} \u2014 ${finding.segment}`,
15811
+ summary: finding.dollar_value ? formatCurrency(finding.dollar_value) : void 0,
15812
+ markdown: [
15813
+ finding.finding,
15814
+ "",
15815
+ finding.recommended_plays && finding.recommended_plays.length > 0 ? `Recommended plays: ${finding.recommended_plays.map((play) => `**${play.play_name}**`).join(", ")}` : "Recommended plays: _None recorded._",
15816
+ "",
15817
+ "Scores:",
15818
+ "```json",
15819
+ JSON.stringify(finding.vital_signs, null, 2),
15820
+ "```"
15821
+ ].join("\n")
15822
+ }))
15823
+ },
15824
+ {
15825
+ id: "segments",
15826
+ title: "Segments",
15827
+ summary: `${diagnosis.segments.length} segments`,
15828
+ markdown: diagnosis.segments.length > 0 ? diagnosis.segments.map((segment) => `- **${segment.segment.name}:** ${Math.round(segment.result.overall_score)}/100 (${segment.result.overall_status}), gated by ${segment.result.gating_vital_sign}`).join("\n") : "_No segments recorded._"
15829
+ }
15830
+ ];
15831
+ }
15832
+ var init_bundle = __esm({
15833
+ "src/repositories/bundle.ts"() {
15834
+ "use strict";
15835
+ init_queries();
15836
+ init_session_analysis();
15837
+ init_schema();
15838
+ init_formatters();
15839
+ init_errors2();
15840
+ init_types2();
15841
+ }
15842
+ });
15843
+
15844
+ // src/repositories/markdown.ts
15845
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync14 } from "fs";
15846
+ import { basename as basename3, dirname as dirname2, join as join20, resolve as resolve6 } from "path";
15847
+ import { stringify as stringifyYaml2 } from "yaml";
15848
+ function renderMarkdownFiles(pkg) {
15849
+ const bundleJson = JSON.stringify(pkg, null, 2) + "\n";
15850
+ const vitalDetails = renderVitalEvidence(pkg);
15851
+ const findings = renderFindings(pkg);
15852
+ const receipts = JSON.stringify({
15853
+ export_id: pkg.export_id,
15854
+ generated_at: pkg.generated_at,
15855
+ actions: pkg.actions
15856
+ }, null, 2) + "\n";
15857
+ const strategyFiles = pkg.strategies.map((entry) => ({
15858
+ relativePath: `strategies/${safeFilename(entry.strategy.slug || entry.strategy.title)}`,
15859
+ contents: renderStrategy(entry),
15860
+ description: `Strategy: ${entry.strategy.title}`
15861
+ }));
15862
+ return [
15863
+ {
15864
+ relativePath: "index.md",
15865
+ contents: renderIndex(pkg),
15866
+ description: "Repository export index"
15867
+ },
15868
+ {
15869
+ relativePath: "evidence/vital-signs.md",
15870
+ contents: vitalDetails,
15871
+ description: "Detailed vital-sign evidence"
15872
+ },
15873
+ {
15874
+ relativePath: "evidence/findings.md",
15875
+ contents: findings,
15876
+ description: "Findings and recommended plays"
15877
+ },
15878
+ ...strategyFiles,
15879
+ {
15880
+ relativePath: "receipts/actions.json",
15881
+ contents: receipts,
15882
+ description: "Action proposal and execution receipts"
15883
+ },
15884
+ {
15885
+ relativePath: "bundle.json",
15886
+ contents: bundleJson,
15887
+ description: "Canonical repository export package"
15888
+ }
15889
+ ];
15890
+ }
15891
+ function renderIndex(pkg) {
15892
+ const frontmatter = stringifyYaml2({
15893
+ export_id: pkg.export_id,
15894
+ generated_at: pkg.generated_at,
15895
+ target: pkg.target.kind,
15896
+ overall_score: pkg.summary.overall_score,
15897
+ overall_status: pkg.summary.overall_status,
15898
+ gating_vital_sign: pkg.summary.gating_vital_sign,
15899
+ total_value_at_risk: pkg.summary.total_value_at_risk,
15900
+ tags: ["ntrp", "repository-export", pkg.summary.gating_vital_sign.replace(/_/g, "-")]
15901
+ }).trim();
15902
+ return [
15903
+ "---",
15904
+ frontmatter,
15905
+ "---",
15906
+ "",
15907
+ "# NTRP Repository Export",
15908
+ "",
15909
+ `Generated: ${pkg.generated_at}`,
15910
+ "",
15911
+ `Overall score: **${pkg.summary.overall_score}/100** (${pkg.summary.overall_status})`,
15912
+ `Gating vital sign: **${VITAL_SIGN_LABELS[pkg.summary.gating_vital_sign]}**`,
15913
+ `Total value at risk: **${pkg.summary.total_value_at_risk ? formatCurrency(pkg.summary.total_value_at_risk) : "N/A"}**`,
15914
+ "",
15915
+ "## Sections",
15916
+ "",
15917
+ ...pkg.sections.map(renderSection),
15918
+ "## Files",
15919
+ "",
15920
+ "- [[evidence/vital-signs|Vital-sign evidence]]",
15921
+ "- [[evidence/findings|Findings]]",
15922
+ "- `bundle.json`",
15923
+ "- `receipts/actions.json`",
15924
+ ""
15925
+ ].join("\n");
15926
+ }
15927
+ function renderSection(section) {
15928
+ const childMarkdown = section.children && section.children.length > 0 ? ["", ...section.children.map(renderNestedSection)].join("\n") : "";
15929
+ return [
15930
+ "<details>",
15931
+ `<summary>${escapeSummary(section.title)}${section.summary ? ` \u2014 ${escapeSummary(section.summary)}` : ""}</summary>`,
15932
+ "",
15933
+ section.markdown,
15934
+ childMarkdown,
15935
+ "",
15936
+ "</details>",
15937
+ ""
15938
+ ].join("\n");
15939
+ }
15940
+ function renderNestedSection(section) {
15941
+ return [
15942
+ "<details>",
15943
+ `<summary>${escapeSummary(section.title)}${section.summary ? ` \u2014 ${escapeSummary(section.summary)}` : ""}</summary>`,
15944
+ "",
15945
+ section.markdown,
15946
+ "",
15947
+ "</details>"
15948
+ ].join("\n");
15949
+ }
15950
+ function renderVitalEvidence(pkg) {
15951
+ const lines = ["# Vital-Sign Evidence", ""];
15952
+ for (const vs of pkg.diagnosis.health.vital_signs) {
15953
+ lines.push(`## ${VITAL_SIGN_LABELS[vs.vital_sign]}`);
15954
+ lines.push("");
15955
+ lines.push(`Score: **${Math.round(vs.score)}/100** (${vs.status})`);
15956
+ lines.push(`Value: **${formatDollarImpact(vs.dollar_value, vs.dollar_label)}**`);
15957
+ lines.push(`Flagged entities: **${vs.entity_details.length}**`);
15958
+ lines.push("");
15959
+ lines.push("<details>");
15960
+ lines.push("<summary>Components</summary>");
15961
+ lines.push("");
15962
+ lines.push("```json");
15963
+ lines.push(JSON.stringify(vs.components, null, 2));
15964
+ lines.push("```");
15965
+ lines.push("");
15966
+ lines.push("</details>");
15967
+ lines.push("");
15968
+ lines.push("<details>");
15969
+ lines.push("<summary>Entity details</summary>");
15970
+ lines.push("");
15971
+ lines.push("```json");
15972
+ lines.push(JSON.stringify(vs.entity_details, null, 2));
15973
+ lines.push("```");
15974
+ lines.push("");
15975
+ lines.push("</details>");
15976
+ lines.push("");
15977
+ }
15978
+ return lines.join("\n");
15979
+ }
15980
+ function renderFindings(pkg) {
15981
+ if (pkg.diagnosis.findings.length === 0) return "# Findings\n\n_No findings recorded._\n";
15982
+ const lines = ["# Findings", ""];
15983
+ for (const finding of pkg.diagnosis.findings) {
15984
+ lines.push(`## ${finding.severity.toUpperCase()} \u2014 ${finding.segment}`);
15985
+ lines.push("");
15986
+ lines.push(finding.finding);
15987
+ lines.push("");
15988
+ if (finding.dollar_value) lines.push(`Dollar value: **${formatCurrency(finding.dollar_value)}**`);
15989
+ if (finding.recommended_plays && finding.recommended_plays.length > 0) {
15990
+ lines.push(`Recommended plays: ${finding.recommended_plays.map((play) => `**${play.play_name}**`).join(", ")}`);
15991
+ }
15992
+ lines.push("");
15993
+ }
15994
+ return lines.join("\n");
15995
+ }
15996
+ function renderStrategy(entry) {
15997
+ const { strategy, sources } = entry;
15998
+ const frontmatter = stringifyYaml2({
15999
+ id: strategy.id,
16000
+ slug: strategy.slug,
16001
+ status: strategy.status,
16002
+ priority: strategy.priority,
16003
+ linked_play_ids: strategy.linked_play_ids,
16004
+ source_count: sources.length,
16005
+ updated_at: strategy.updated_at
16006
+ }).trim();
16007
+ return [
16008
+ "---",
16009
+ frontmatter,
16010
+ "---",
16011
+ "",
16012
+ `# ${strategy.title}`,
16013
+ "",
16014
+ `Goal: ${strategy.goal}`,
16015
+ "",
16016
+ `Hypothesis: ${strategy.hypothesis}`,
16017
+ "",
16018
+ `Target segment: ${strategy.target_segment}`,
16019
+ "",
16020
+ "## Recommended Actions",
16021
+ "",
16022
+ strategy.recommended_actions.length > 0 ? strategy.recommended_actions.map((action) => `- ${action}`).join("\n") : "_None specified._",
16023
+ "",
16024
+ "## Source Metadata",
16025
+ "",
16026
+ "```json",
16027
+ JSON.stringify(sources, null, 2),
16028
+ "```",
16029
+ ""
16030
+ ].join("\n");
16031
+ }
16032
+ function getRootPath(target) {
16033
+ return resolve6(target.directory ?? `ntrp-repository-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
16034
+ }
16035
+ function safeFilename(value) {
16036
+ return (basename3(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "strategy") + ".md";
16037
+ }
16038
+ function escapeSummary(value) {
16039
+ return value.replace(/[<>]/g, "");
16040
+ }
16041
+ var markdownRepositoryAdapter;
16042
+ var init_markdown2 = __esm({
16043
+ "src/repositories/markdown.ts"() {
16044
+ "use strict";
16045
+ init_formatters();
16046
+ markdownRepositoryAdapter = {
16047
+ kind: "markdown",
16048
+ describeTarget(target) {
16049
+ return target.directory ? `local markdown folder ${resolve6(target.directory)}` : "local markdown folder";
16050
+ },
16051
+ planWrite(pkg) {
16052
+ const files = renderMarkdownFiles(pkg);
16053
+ return {
16054
+ target: pkg.target,
16055
+ root_path: getRootPath(pkg.target),
16056
+ files: files.map((file) => ({
16057
+ path: file.relativePath,
16058
+ bytes: Buffer.byteLength(file.contents, "utf-8"),
16059
+ description: file.description
16060
+ }))
16061
+ };
16062
+ },
16063
+ write(pkg) {
16064
+ const root = getRootPath(pkg.target);
16065
+ const files = renderMarkdownFiles(pkg);
16066
+ mkdirSync8(root, { recursive: true });
16067
+ const written = [];
16068
+ for (const file of files) {
16069
+ const absolutePath = join20(root, file.relativePath);
16070
+ mkdirSync8(dirname2(absolutePath), { recursive: true });
16071
+ writeFileSync14(absolutePath, file.contents, "utf-8");
16072
+ written.push(absolutePath);
16073
+ }
16074
+ return {
16075
+ mode: "repository_export",
16076
+ target: pkg.target,
16077
+ root_path: root,
16078
+ files_written: written,
16079
+ bundle_id: pkg.export_id,
16080
+ message: `Wrote ${written.length} repository export files to ${root}.`,
16081
+ external_side_effects: false
16082
+ };
16083
+ }
16084
+ };
16085
+ }
16086
+ });
16087
+
16088
+ // src/repositories/adapters.ts
16089
+ function getRepositoryAdapter(kind) {
16090
+ switch (kind) {
16091
+ case "markdown":
16092
+ return markdownRepositoryAdapter;
16093
+ case "notion":
16094
+ case "airtable":
16095
+ case "github":
16096
+ throw new NtrpError("repository_target_planned", `${kind} repository exports are planned but not implemented yet. Use --target markdown for now.`, 2 /* Usage */);
16097
+ }
16098
+ }
16099
+ var init_adapters = __esm({
16100
+ "src/repositories/adapters.ts"() {
16101
+ "use strict";
16102
+ init_markdown2();
16103
+ init_errors2();
16104
+ init_types2();
16105
+ }
16106
+ });
16107
+
16108
+ // src/services/publish.ts
16109
+ async function proposeRepositoryExport(options) {
16110
+ await initSchema();
16111
+ const pkg = await buildPackage(options, "publish propose");
16112
+ const adapter = getRepositoryAdapter(pkg.target.kind);
16113
+ const plan = adapter.planWrite(pkg);
16114
+ const id = await insertActionProposal({
16115
+ handle_title: options.source === "smoke_protocol" ? "Monkey Pelican Export" : "NTRP Repository Export",
16116
+ kind: "repository_export",
16117
+ title: options.source === "smoke_protocol" ? "Monkey Pelican Export" : `Publish NTRP evidence bundle to ${pkg.target.kind}`,
16118
+ summary: `Export diagnosis, findings, strategies, evidence, and action receipts to ${adapter.describeTarget(pkg.target)}.`,
16119
+ permission_class: "execute",
16120
+ status: "pending_approval",
16121
+ target: {
16122
+ connector_id: `${pkg.target.kind}-repository`,
16123
+ connector_type: pkg.target.kind,
16124
+ operation: "write_repository_export"
16125
+ },
16126
+ payload: {
16127
+ repository_export: pkg,
16128
+ write_plan: plan
16129
+ },
16130
+ dry_run: {
16131
+ mode: "dry_run",
16132
+ summary: `Would write ${plan.files.length} file${plan.files.length === 1 ? "" : "s"} to ${plan.root_path}.`,
16133
+ would_execute: false,
16134
+ expected_mutations: plan.files.map((file) => `${file.path} (${file.description})`),
16135
+ risk_notes: [
16136
+ "Requires explicit local approval before writing files.",
16137
+ "Markdown target writes only to the local filesystem.",
16138
+ "Notion and Airtable targets are planned adapter mappings only in this slice."
16139
+ ]
16140
+ },
16141
+ source: options.source ?? "publish"
16142
+ });
16143
+ const proposal = await getActionProposal(id);
16144
+ if (!proposal) {
16145
+ throw new NtrpError("publish_proposal_missing", `Publish proposal was not found after insert: ${id}`, 1 /* RuntimeError */);
16146
+ }
16147
+ return { action: "propose", proposal, plan };
16148
+ }
16149
+ async function buildPackage(options, command) {
16150
+ const target = {
16151
+ kind: options.target,
16152
+ directory: options.directory
16153
+ };
16154
+ return buildRepositoryExportPackage({
16155
+ target,
16156
+ command,
16157
+ source: options.source ?? "publish",
16158
+ modelOrFixture: options.modelOrFixture
16159
+ });
16160
+ }
16161
+ var init_publish = __esm({
16162
+ "src/services/publish.ts"() {
16163
+ "use strict";
16164
+ init_queries();
16165
+ init_schema();
16166
+ init_errors2();
16167
+ init_types2();
16168
+ init_bundle();
16169
+ init_adapters();
16170
+ }
16171
+ });
16172
+
16173
+ // src/services/smoke-protocol.ts
16174
+ import { join as join21 } from "path";
16175
+ function isSmokeProtocolTrigger(input) {
16176
+ return normalize(input).includes(SMOKE_TRIGGER_PHRASE);
16177
+ }
16178
+ async function runSmokeProtocol(_input, ctx) {
16179
+ const diagnosis = await runDiagnosis({ findings: false, segments: true });
16180
+ ctx.snapshot.computeResult = {
16181
+ aggregate: diagnosis.health,
16182
+ segments: diagnosis.segments
16183
+ };
16184
+ ctx.snapshot.divergences = diagnosis.divergences;
16185
+ const gatingVital = diagnosis.health.vital_signs.find((vs) => vs.vital_sign === diagnosis.health.gating_vital_sign) ?? diagnosis.health.vital_signs[0];
16186
+ const play = getPlaysForVitalSign(diagnosis.health.gating_vital_sign)[0];
16187
+ const finding = buildSmokeFinding(
16188
+ diagnosis.health.vital_signs,
16189
+ gatingVital,
16190
+ play?.id ?? "review-playbook",
16191
+ play?.name ?? "Review the playbook"
16192
+ );
16193
+ await insertFinding({
16194
+ findings: [finding],
16195
+ model_used: "smoke-protocol-v1",
16196
+ raw_prompt: `Smoke trigger: ${SMOKE_TRIGGER_PHRASE}`
16197
+ });
16198
+ const strategyResult = await addStrategyText(renderSmokeStrategy(finding, play?.name ?? "Review the playbook"), {
16199
+ useAi: false,
16200
+ sourceMetadata: {
16201
+ connector_kind: "smoke_protocol",
16202
+ connector_name: "Monkey Pelican Trigger",
16203
+ sync_mode: "local_fixture",
16204
+ write_back: "approval_required_future",
16205
+ trigger_phrase: SMOKE_TRIGGER_PHRASE
16206
+ }
16207
+ });
16208
+ const proposalResult = await proposeRepositoryExport({
16209
+ target: "markdown",
16210
+ directory: join21(getExportsDir(), "repository-smoke"),
16211
+ source: "smoke_protocol",
16212
+ modelOrFixture: "smoke-protocol-v1"
16213
+ });
16214
+ const answer = renderSmokeAnswer({
16215
+ overallScore: diagnosis.health.overall_score,
16216
+ overallStatus: diagnosis.health.overall_status,
16217
+ gatingVital: diagnosis.health.gating_vital_sign,
16218
+ totalValueAtRisk: diagnosis.health.total_value_at_risk,
16219
+ finding,
16220
+ strategyTitle: strategyResult.strategy.title,
16221
+ strategyPath: strategyResult.library_path
16222
+ });
16223
+ return {
16224
+ answer,
16225
+ finding,
16226
+ strategy: strategyResult.strategy,
16227
+ action_proposal: proposalResult.proposal,
16228
+ health: {
16229
+ overall_score: diagnosis.health.overall_score,
16230
+ overall_status: diagnosis.health.overall_status,
16231
+ gating_vital_sign: diagnosis.health.gating_vital_sign,
16232
+ total_value_at_risk: diagnosis.health.total_value_at_risk ?? null
16233
+ }
16234
+ };
16235
+ }
16236
+ function buildSmokeFinding(vitals, gatingVital, playId, playName) {
16237
+ const vitalSigns = Object.fromEntries(vitals.map((vs) => [vs.vital_sign, Math.round(vs.score)]));
16238
+ const value = gatingVital?.dollar_value ?? null;
16239
+ const valueLabel = gatingVital?.dollar_label ?? "value at risk";
16240
+ const score = Math.round(gatingVital?.score ?? 0);
16241
+ const vital = gatingVital?.vital_sign ?? "freshness";
16242
+ const formattedValue = value === null ? "N/A" : formatCurrency(value);
16243
+ return {
16244
+ severity: gatingVital?.status === "red" ? "critical" : gatingVital?.status === "yellow" ? "warning" : "info",
16245
+ segment: "All Pipeline",
16246
+ finding: `**${formattedValue} ${valueLabel}** is the smoke-test headline. The current gating vital sign is **${vital}** at **${score}/100**, so the placeholder deep analysis would recommend **${playName}** as the next play.`,
16247
+ vital_signs: vitalSigns,
16248
+ entity_count: gatingVital?.entity_details.length ?? 0,
16249
+ recommended_focus: vital,
16250
+ dollar_value: value,
16251
+ recommended_plays: [{
16252
+ play_id: playId,
16253
+ play_name: playName,
16254
+ rationale: "Selected from the current gating vital sign to exercise the diagnosis-to-play smoke workflow."
16255
+ }]
16256
+ };
16257
+ }
16258
+ function renderSmokeStrategy(finding, playName) {
16259
+ return `# Smoke Test: ${playName}
16260
+
16261
+ Goal: Validate the flow from natural-language trigger to diagnosis, deep-analysis-style response, saved strategy, and approval-gated library write-back.
16262
+
16263
+ Target Segment: ${finding.segment}
16264
+
16265
+ Recommended play: ${playName}
16266
+
16267
+ Smoke finding: ${finding.finding.replace(/\*\*/g, "")}
16268
+ `;
16269
+ }
16270
+ function renderSmokeAnswer(input) {
16271
+ const totalValue = input.totalValueAtRisk === null ? "N/A" : formatCurrency(input.totalValueAtRisk);
16272
+ return `### Smoke Protocol Complete
16273
+
16274
+ I treated the monkey/pelican phrase as a local smoke trigger and ran the placeholder protocol without calling an AI model.
16275
+
16276
+ - Diagnosis: overall score **${Math.round(input.overallScore)}/100** (${input.overallStatus}), gated by **${input.gatingVital}**, with **${totalValue}** total value at risk.
16277
+ - Deep analysis fixture: ${input.finding.finding}
16278
+ - Saved play/strategy: **${input.strategyTitle}** at \`${input.strategyPath}\`.
16279
+ - Repository export prepared for an approval-gated Obsidian-compatible markdown bundle.
16280
+
16281
+ ---
16282
+ *Next: run \`/actions continue\` to approve the export, then \`/actions continue\` again to write it to the repository.*`;
16283
+ }
16284
+ function normalize(input) {
16285
+ return input.trim().toLowerCase().replace(/\s+/g, " ");
16286
+ }
16287
+ var SMOKE_TRIGGER_PHRASE;
16288
+ var init_smoke_protocol = __esm({
16289
+ "src/services/smoke-protocol.ts"() {
16290
+ "use strict";
16291
+ init_queries();
16292
+ init_diagnosis();
16293
+ init_strategy();
16294
+ init_publish();
16295
+ init_playbook();
16296
+ init_store();
16297
+ init_formatters();
16298
+ SMOKE_TRIGGER_PHRASE = "the monkey is green and riding a pelican";
16299
+ }
16300
+ });
16301
+
16302
+ // src/cli/nl.ts
16303
+ var nl_exports = {};
16304
+ __export(nl_exports, {
16305
+ runNaturalLanguage: () => runNaturalLanguage
16306
+ });
16307
+ import chalk16 from "chalk";
16308
+ async function runNaturalLanguage(input, ctx) {
16309
+ if (isSmokeProtocolTrigger(input)) {
16310
+ recordMessage(ctx, "user", input);
16311
+ console.log();
16312
+ const spinner2 = makeSpinner("Running smoke protocol\u2026");
16313
+ try {
16314
+ const result = await runSmokeProtocol(input, ctx);
16315
+ spinner2.succeed("Smoke protocol complete");
16316
+ printAnswer(result.answer);
16317
+ recordMessage(ctx, "agent", result.answer);
16318
+ console.log();
16319
+ return extractSummary(result.answer);
16320
+ } catch (err) {
16321
+ spinner2.fail("Smoke protocol failed");
16322
+ console.error(" " + chalk16.red(String(err.message ?? err)));
16323
+ console.log();
16324
+ return;
16325
+ }
16326
+ }
16327
+ const phase = resolveConversationPhase(ctx);
16328
+ if (!canUseReplAi(ctx) && (phase === "explore" || phase === "deliver")) {
16329
+ await handleExploreWithoutKey(ctx, input);
16330
+ return;
16331
+ }
16332
+ if (!canUseReplAi(ctx)) {
16333
+ return;
16334
+ }
16335
+ recordMessage(ctx, "user", input);
16336
+ let snapshot = ctx.snapshot.computeResult;
16337
+ if (!snapshot) {
16338
+ const metricsFirst = prefersMetricsFirstContext(ctx);
16339
+ const spinner2 = makeSpinner(metricsFirst ? "Loading session context\u2026" : "Computing vital signs\u2026");
16340
+ try {
16341
+ snapshot = await computeFullHealth();
16342
+ ctx.snapshot.computeResult = snapshot;
16343
+ const divInput = snapshot.segments.map((s) => ({
16344
+ segmentId: s.segment.id,
16345
+ segmentName: s.segment.name,
16346
+ result: s.result
16347
+ }));
16348
+ ctx.snapshot.divergences = detectDivergences(snapshot.aggregate, divInput).divergences;
16349
+ spinner2.succeed(metricsFirst ? "Session context ready" : "Health snapshot ready");
16350
+ } catch (err) {
16351
+ spinner2.fail("Could not compute health snapshot");
16352
+ console.error(" " + chalk16.red(String(err.message ?? err)));
16353
+ console.log(" " + chalk16.dim("Run ") + paint("accent", "/new") + chalk16.dim(" \u2192 pick Demo to load sample data."));
16354
+ console.log();
16355
+ return;
16356
+ }
16357
+ }
16358
+ console.log();
16359
+ const memoryBlock = await buildMemoryBlock(input).catch(() => "");
16360
+ const spinner = makeSpinner("Thinking\u2026");
16361
+ let lastAnswer = "";
16362
+ let rawHistory = [];
16363
+ const toolsUsed = [];
16364
+ setAgentContext(ctx);
16365
+ try {
16366
+ const analysisBlock = buildAnalysisBlock(ctx);
16367
+ const conversationBlock = getConversationPhaseBlock(ctx);
16368
+ const bundle = await loadSessionAnalysisBundle();
16369
+ const sessionArtifact = hasAnyAnalysis(bundle) ? buildExploreContextBlock(bundle, ctx) : "";
16370
+ const responseMode = resolveExploreResponseMode(input, ctx, ctx.conversation.length);
16371
+ for await (const event of agenticFindings(snapshot, ctx.snapshot.divergences, {
16372
+ mode: "fresh",
16373
+ userQuestion: input,
16374
+ sessionContext: ctx.resumedSessionSummary,
16375
+ includeMetrics: true,
16376
+ analysisBlock,
16377
+ conversationBlock,
16378
+ sessionArtifact,
16379
+ responseMode,
16380
+ priorMessages: ctx.conversation,
16381
+ memoryBlock,
16382
+ ctx
16383
+ })) {
16384
+ switch (event.type) {
16385
+ case "tool_call":
16386
+ toolsUsed.push(event.name);
16387
+ spinner.text = `Querying ${event.name}\u2026`;
16388
+ break;
16389
+ case "thinking":
16390
+ spinner.stop();
16391
+ console.log(" " + chalk16.dim.italic(event.text));
16392
+ spinner.start("Thinking\u2026");
16393
+ break;
16394
+ case "answer":
16395
+ spinner.stop();
16396
+ lastAnswer = event.text;
16397
+ printAnswer(event.text);
16398
+ break;
16399
+ case "finding":
16400
+ spinner.stop();
16401
+ printFindingInline(event.finding);
16402
+ break;
16403
+ case "done":
16404
+ spinner.stop();
16405
+ rawHistory = event.conversation_history;
16406
+ break;
16407
+ }
16408
+ }
16409
+ } catch (err) {
16410
+ spinner.fail("Error while investigating");
16411
+ console.error(" " + chalk16.red(String(err.message ?? err)));
16412
+ console.log();
16413
+ return;
16414
+ } finally {
16415
+ setAgentContext(null);
16416
+ }
16417
+ if (rawHistory.length > 0) {
16418
+ ctx.conversation = distillThread(rawHistory);
16419
+ }
16420
+ if (!lastAnswer) {
16421
+ console.log(" " + chalk16.dim("(no answer returned)"));
16422
+ } else {
16423
+ recordMessage(ctx, "agent", lastAnswer);
16424
+ if (ctx.pendingAsk) {
16425
+ const { clearPendingAsk: clearPendingAsk2 } = await Promise.resolve().then(() => (init_pending_ask(), pending_ask_exports));
16426
+ clearPendingAsk2(ctx);
16427
+ } else {
16428
+ saveSessionState(ctx);
16429
+ }
16430
+ creditNlAnswer(ctx, Math.floor(ctx.messages.length / 2));
16431
+ recordAnalysis({ question: input, answer: lastAnswer, tools: toolsUsed, session_id: ctx.sessionId });
16432
+ ctx.lastExchange = { question: input, answer: lastAnswer };
16433
+ }
16434
+ console.log();
16435
+ const { promptQueuedAiStrategist: promptQueuedAiStrategist2 } = await Promise.resolve().then(() => (init_strategist_flow(), strategist_flow_exports));
16436
+ promptQueuedAiStrategist2(ctx);
16437
+ return lastAnswer ? extractSummary(lastAnswer) : void 0;
16438
+ }
16439
+ function printAnswer(text) {
16440
+ printMarkdown(text, { indent: 2 });
16441
+ }
16442
+ function extractSummary(text) {
16443
+ const plain = text.replace(/#{1,6}\s+/g, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^[-*]\s+/gm, "").trim();
16444
+ const sentenceMatch = plain.match(/^(.+?[.!?])(?:\s|$)/);
16445
+ const sentence = sentenceMatch ? sentenceMatch[1] : plain.split("\n")[0];
16446
+ if (sentence.length <= 60) return sentence;
16447
+ const truncated = sentence.slice(0, 60).replace(/\s+\S*$/, "");
16448
+ return truncated + "\u2026";
16449
+ }
16450
+ function printFindingInline(finding) {
16451
+ const sev = finding.severity;
16452
+ console.log();
16453
+ console.log(" " + severityPaint(sev)(`[${sev}]`) + " " + chalk16.bold(finding.segment));
16454
+ printMarkdown(finding.finding, { indent: 2 });
16455
+ const play = finding.recommended_plays?.[0];
16456
+ if (play) console.log(" " + chalk16.dim("\u2192 " + play.play_name + " \u2014 " + play.rationale));
15116
16457
  }
15117
- var init_compute2 = __esm({
15118
- "src/conversation/compute.ts"() {
16458
+ var init_nl = __esm({
16459
+ "src/cli/nl.ts"() {
15119
16460
  "use strict";
16461
+ init_spinner();
15120
16462
  init_context2();
15121
- init_gap_audit();
15122
- init_companion();
16463
+ init_phase();
16464
+ init_agent_context();
16465
+ init_orchestrator();
16466
+ init_agentic_loop();
16467
+ init_explore_mode();
16468
+ init_thread();
16469
+ init_store2();
16470
+ init_health_score();
16471
+ init_divergence();
16472
+ init_repl_api();
16473
+ init_theme();
16474
+ init_markdown();
16475
+ init_smoke_protocol();
16476
+ init_session_analysis();
15123
16477
  init_time_bank();
15124
16478
  }
15125
16479
  });
15126
16480
 
15127
16481
  // src/config/demo.ts
15128
- import chalk15 from "chalk";
16482
+ var demo_exports = {};
16483
+ __export(demo_exports, {
16484
+ DEMO_DISABLED_MESSAGE: () => DEMO_DISABLED_MESSAGE,
16485
+ guardDemoEnabled: () => guardDemoEnabled,
16486
+ isDemoEnabled: () => isDemoEnabled,
16487
+ printDemoDisabled: () => printDemoDisabled,
16488
+ setDemoEnabled: () => setDemoEnabled
16489
+ });
16490
+ import chalk17 from "chalk";
15129
16491
  function printDemoDisabled() {
15130
16492
  console.log();
15131
- console.log(" " + chalk15.red(DEMO_DISABLED_MESSAGE));
16493
+ console.log(" " + chalk17.red(DEMO_DISABLED_MESSAGE));
15132
16494
  console.log(
15133
- " " + chalk15.dim("Re-enable with ") + paint("accent", "/config set demo-enabled true") + chalk15.dim(".")
16495
+ " " + chalk17.dim("Re-enable with ") + paint("accent", "/config set demo-enabled true") + chalk17.dim(".")
15134
16496
  );
15135
16497
  console.log();
15136
16498
  }
@@ -15145,6 +16507,11 @@ function isDemoEnabled() {
15145
16507
  if (value === false || value === "false") return false;
15146
16508
  return true;
15147
16509
  }
16510
+ function setDemoEnabled(enabled) {
16511
+ const config = loadConfig();
16512
+ config["demo-enabled"] = enabled;
16513
+ saveConfig(config);
16514
+ }
15148
16515
  var DEMO_DISABLED_MESSAGE;
15149
16516
  var init_demo = __esm({
15150
16517
  "src/config/demo.ts"() {
@@ -15155,6 +16522,225 @@ var init_demo = __esm({
15155
16522
  }
15156
16523
  });
15157
16524
 
16525
+ // src/conversation/pending-ask.ts
16526
+ var pending_ask_exports = {};
16527
+ __export(pending_ask_exports, {
16528
+ cancelPendingAskNotice: () => cancelPendingAskNotice,
16529
+ clearPendingAsk: () => clearPendingAsk,
16530
+ isClearAutoConfirmIntent: () => isClearAutoConfirmIntent,
16531
+ looksLikeQuestion: () => looksLikeQuestion,
16532
+ offerDemoToAnswer: () => offerDemoToAnswer,
16533
+ printFocusChip: () => printFocusChip,
16534
+ queuePendingAsk: () => queuePendingAsk,
16535
+ resumePendingAsk: () => resumePendingAsk
16536
+ });
16537
+ import chalk18 from "chalk";
16538
+ function looksLikeQuestion(input) {
16539
+ const text = input.trim();
16540
+ if (!text) return false;
16541
+ if (/\?\s*$/.test(text)) return true;
16542
+ if (/^(what|why|how|which|where|who|is|are|can|should|do|does|did|will|would|could)\b/i.test(text)) {
16543
+ return true;
16544
+ }
16545
+ return /\b(most expensive|biggest risk|what should (i|we)|fix first|plan of attack)\b/i.test(text);
16546
+ }
16547
+ function isClearAutoConfirmIntent(input) {
16548
+ const text = input.trim();
16549
+ return /\b(most expensive|biggest risk|expensive problem|what should (i|we) fix|fix first|plan of attack)\b/i.test(text) || /\b(pipeline health|stuck deals|handoff leak|stale pipeline|vital signs?)\b/i.test(text) || /\b(is (our |my )?retention real|nrr real|board.*(retention|nrr))\b/i.test(text);
16550
+ }
16551
+ function queuePendingAsk(ctx, text, origin) {
16552
+ const trimmed = text.trim();
16553
+ if (!trimmed) return;
16554
+ ctx.pendingAsk = {
16555
+ text: trimmed,
16556
+ queued_at: (/* @__PURE__ */ new Date()).toISOString(),
16557
+ origin,
16558
+ keylessAnswered: ctx.pendingAsk?.keylessAnswered
16559
+ };
16560
+ saveSessionState(ctx);
16561
+ }
16562
+ function clearPendingAsk(ctx) {
16563
+ if (!ctx.pendingAsk) return;
16564
+ ctx.pendingAsk = void 0;
16565
+ saveSessionState(ctx);
16566
+ }
16567
+ function printFocusChip(ctx) {
16568
+ if (!ctx.scope) return;
16569
+ const lens = ctx.scope.primary_lens === "revenue_metrics" ? "SaaS metrics" : "pipeline health";
16570
+ const period = ctx.scope.time_horizon ? ` \xB7 ${ctx.scope.time_horizon}` : "";
16571
+ console.log();
16572
+ console.log(
16573
+ " " + chalk18.dim("Focus: ") + paint("accent", lens) + chalk18.dim(period) + chalk18.dim(" \u2014 type ") + chalk18.cyan("adjust") + chalk18.dim(" to change")
16574
+ );
16575
+ console.log();
16576
+ }
16577
+ async function resumePendingAsk(ctx) {
16578
+ const pending = ctx.pendingAsk;
16579
+ if (!pending?.text) return false;
16580
+ ctx.computeInProgress = false;
16581
+ if (canUseReplAi(ctx)) {
16582
+ console.log();
16583
+ console.log(
16584
+ " " + chalk18.dim(
16585
+ pending.keylessAnswered ? "Picking up your question with the connected engine\u2026" : "Picking up your question\u2026"
16586
+ )
16587
+ );
16588
+ console.log();
16589
+ const { runNaturalLanguage: runNaturalLanguage2 } = await Promise.resolve().then(() => (init_nl(), nl_exports));
16590
+ await runNaturalLanguage2(pending.text, ctx);
16591
+ clearPendingAsk(ctx);
16592
+ return true;
16593
+ }
16594
+ const { tryKeylessAskAnswer: tryKeylessAskAnswer2 } = await Promise.resolve().then(() => (init_keyless_ask(), keyless_ask_exports));
16595
+ const answered = await tryKeylessAskAnswer2(ctx, pending.text, { fromResume: true });
16596
+ if (answered) {
16597
+ ctx.pendingAsk = { ...pending, keylessAnswered: true };
16598
+ saveSessionState(ctx);
16599
+ return true;
16600
+ }
16601
+ return false;
16602
+ }
16603
+ async function offerDemoToAnswer(ctx) {
16604
+ if (!ctx.pendingAsk || !ctx.rl) return false;
16605
+ const { guardDemoEnabled: guardDemoEnabled2, isDemoEnabled: isDemoEnabled2 } = await Promise.resolve().then(() => (init_demo(), demo_exports));
16606
+ if (!isDemoEnabled2()) return false;
16607
+ const { sessionHasData: sessionHasData2 } = await Promise.resolve().then(() => (init_phase(), phase_exports));
16608
+ if (sessionHasData2(ctx)) return false;
16609
+ const { createPromptSession: createPromptSession2 } = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
16610
+ const { loadDemoFromChat: loadDemoFromChat2 } = await Promise.resolve().then(() => (init_ingest_chat(), ingest_chat_exports));
16611
+ const prompts = createPromptSession2(ctx.rl, ctx);
16612
+ try {
16613
+ console.log();
16614
+ const go = await prompts.confirm("Use demo data to answer this?", true);
16615
+ if (!go) {
16616
+ console.log(
16617
+ " " + chalk18.dim("Paste a CSV path when ready, or say ") + chalk18.cyan("use demo data") + chalk18.dim(".")
16618
+ );
16619
+ console.log();
16620
+ return false;
16621
+ }
16622
+ } finally {
16623
+ prompts.close();
16624
+ }
16625
+ if (!guardDemoEnabled2()) return false;
16626
+ await loadDemoFromChat2(ctx, void 0, { autoCompute: true });
16627
+ return true;
16628
+ }
16629
+ function cancelPendingAskNotice(ctx) {
16630
+ if (!ctx.pendingAsk) return;
16631
+ recordMessage(ctx, "agent", "Cleared queued question.");
16632
+ clearPendingAsk(ctx);
16633
+ }
16634
+ var init_pending_ask = __esm({
16635
+ "src/conversation/pending-ask.ts"() {
16636
+ "use strict";
16637
+ init_context2();
16638
+ init_repl_api();
16639
+ init_theme();
16640
+ }
16641
+ });
16642
+
16643
+ // src/conversation/compute.ts
16644
+ var compute_exports2 = {};
16645
+ __export(compute_exports2, {
16646
+ isComputeIntent: () => isComputeIntent,
16647
+ runConversationCompute: () => runConversationCompute
16648
+ });
16649
+ import chalk19 from "chalk";
16650
+ async function runConversationCompute(ctx) {
16651
+ const lens = ctx.scope?.primary_lens ?? ctx.analysis.primary;
16652
+ ctx.computeInProgress = true;
16653
+ const willAnswer = Boolean(ctx.pendingAsk?.text) && !ctx.strategistState;
16654
+ try {
16655
+ if (lens === "revenue_metrics") {
16656
+ const { runMetricsAnalysis: runMetricsAnalysis2, extractHeadlineMetrics: extractHeadlineMetrics2 } = await Promise.resolve().then(() => (init_metrics_analysis(), metrics_analysis_exports));
16657
+ const { renderMetricsReport: renderMetricsReport2 } = await Promise.resolve().then(() => (init_metrics_report(), metrics_report_exports));
16658
+ const spinner = makeSpinner("Computing SaaS metrics\u2026");
16659
+ try {
16660
+ const result = await runMetricsAnalysis2({
16661
+ findings: false,
16662
+ sessionAnalysis: ctx.analysis
16663
+ });
16664
+ spinner.succeed("SaaS metrics computed");
16665
+ ctx.analysis.coverage = result.coverage;
16666
+ ctx.analysis.data_source_type = result.data_source_type;
16667
+ ctx.analysis.recommended = result.companion_recommendation;
16668
+ ctx.analysis.headline = extractHeadlineMetrics2(result.metrics.aggregate.metrics);
16669
+ markLensCompleted(ctx, "revenue_metrics");
16670
+ ctx.stage = "analyzed";
16671
+ ctx.snapshot.computeResult = null;
16672
+ invalidateGapAudit(ctx);
16673
+ saveSessionState(ctx);
16674
+ renderMetricsReport2(ctx, result.metrics.aggregate.metrics, result.coverage, result.data_source_type, {
16675
+ snapshot: result.snapshot,
16676
+ findings: result.findings,
16677
+ companion: result.companion_recommendation ?? null,
16678
+ interactive: !willAnswer
16679
+ });
16680
+ await resumeQueuedStrategist(ctx);
16681
+ await closeComputeTurn(ctx, willAnswer, "revenue_metrics");
16682
+ creditGapCompute(ctx);
16683
+ creditMetricsComplete(ctx, false);
16684
+ return "SaaS metrics ready";
16685
+ } catch (err) {
16686
+ spinner.fail("Metrics failed");
16687
+ throw err;
16688
+ }
16689
+ }
16690
+ const { handler: diagnose } = await Promise.resolve().then(() => (init_diagnose(), diagnose_exports));
16691
+ ctx.skipTimeBankDiagnoseCredit = true;
16692
+ ctx.suppressCompanionFooter = willAnswer;
16693
+ const summary = await diagnose(["--compact"], ctx);
16694
+ markLensCompleted(ctx, "gtm_health");
16695
+ ctx.stage = "analyzed";
16696
+ ctx.snapshot.computeResult = null;
16697
+ invalidateGapAudit(ctx);
16698
+ saveSessionState(ctx);
16699
+ await resumeQueuedStrategist(ctx);
16700
+ await closeComputeTurn(ctx, willAnswer, "gtm_health");
16701
+ creditGapCompute(ctx);
16702
+ return typeof summary === "string" ? summary : "Health analysis ready";
16703
+ } catch (err) {
16704
+ console.error(" " + chalk19.red(String(err.message ?? err)));
16705
+ return;
16706
+ } finally {
16707
+ ctx.computeInProgress = false;
16708
+ ctx.deliverIntent = false;
16709
+ }
16710
+ }
16711
+ async function resumeQueuedStrategist(ctx) {
16712
+ if (ctx.strategistState?.step !== "awaiting_analysis") return;
16713
+ ctx.computeInProgress = false;
16714
+ const { resumeStrategistAfterCompute: resumeStrategistAfterCompute2 } = await Promise.resolve().then(() => (init_strategist_flow(), strategist_flow_exports));
16715
+ await resumeStrategistAfterCompute2(ctx);
16716
+ }
16717
+ async function resumePendingAskAfterCompute(ctx) {
16718
+ if (!ctx.pendingAsk?.text) return false;
16719
+ if (ctx.strategistState) return false;
16720
+ const { resumePendingAsk: resumePendingAsk2 } = await Promise.resolve().then(() => (init_pending_ask(), pending_ask_exports));
16721
+ return resumePendingAsk2(ctx);
16722
+ }
16723
+ async function closeComputeTurn(ctx, suppressedFooter, lens) {
16724
+ const answered = await resumePendingAskAfterCompute(ctx);
16725
+ if (suppressedFooter && !answered) {
16726
+ const companion = await resolveCompanionRecommendation(ctx);
16727
+ printCompanionFooter(ctx, companion, { justCompleted: lens });
16728
+ }
16729
+ }
16730
+ function isComputeIntent(input) {
16731
+ return /\b(run analysis|compute|go ahead|analyze|let'?s go|do it)\b/i.test(input.trim());
16732
+ }
16733
+ var init_compute2 = __esm({
16734
+ "src/conversation/compute.ts"() {
16735
+ "use strict";
16736
+ init_spinner();
16737
+ init_context2();
16738
+ init_gap_audit();
16739
+ init_companion();
16740
+ init_time_bank();
16741
+ }
16742
+ });
16743
+
15158
16744
  // src/pipeline/csv-parse.ts
15159
16745
  var csv_parse_exports = {};
15160
16746
  __export(csv_parse_exports, {
@@ -15707,6 +17293,7 @@ function blendScenarios(_research) {
15707
17293
  label: "Research-Derived Blend",
15708
17294
  description: "Realistic data with mild-to-moderate problems across all vital signs.",
15709
17295
  story: "Generated with default research blend. Problems are seeded across all vital signs at realistic levels.",
17296
+ hook: "Mild-to-moderate problems seeded across all five vitals.",
15710
17297
  // Bump all problems slightly above baseline for discoverability
15711
17298
  staleContactRatio: 0.2,
15712
17299
  pastCloseDateRatio: 0.15,
@@ -15751,6 +17338,7 @@ var init_scenarios = __esm({
15751
17338
  label: "The Hidden Crisis",
15752
17339
  description: "Overall health looks yellow but Enterprise is deep red, masked by strong SMB numbers.",
15753
17340
  story: "Your aggregate numbers look okay \u2014 but when you break it by segment, Enterprise is dying. 60% of enterprise contacts have gone dark, deals are single-threaded, and SMB is carrying the average.",
17341
+ hook: "SMB is carrying the average while Enterprise dies quietly.",
15754
17342
  staleContactRatio: 0.3,
15755
17343
  staleContactRatioEnterprise: 0.6,
15756
17344
  staleContactRatioSmb: 0.1,
@@ -15765,6 +17353,7 @@ var init_scenarios = __esm({
15765
17353
  label: "The Leaky Bucket",
15766
17354
  description: "Marketing generates plenty of leads but 40% vanish at handoff to sales.",
15767
17355
  story: "Marketing is doing its job \u2014 MQLs are flowing. But 40% of qualified leads never show up in sales workflows. They're falling through the cracks at handoff, and nobody's noticing because marketing reports MQL count and sales reports pipeline value.",
17356
+ hook: "MQLs flow in, then 40% vanish at the sales handoff.",
15768
17357
  mqlDropRatio: 0.4,
15769
17358
  qualifiedNoOutreachRatio: 0.35,
15770
17359
  staleContactRatio: 0.2
@@ -15775,6 +17364,7 @@ var init_scenarios = __esm({
15775
17364
  label: "The Stale Pipeline",
15776
17365
  description: "Big pipeline number but half the deals are zombies stuck in late stages.",
15777
17366
  story: "The pipeline report says $5M. But look closer: half those deals have close dates in the past, 40% are stuck in Negotiation for 120+ days, and nobody's touching them. You're forecasting on fiction.",
17367
+ hook: "Half the pipeline is zombies \u2014 you're forecasting on fiction.",
15778
17368
  pastCloseDateRatio: 0.5,
15779
17369
  stuckDealRatio: 0.4,
15780
17370
  stuckInNegotiationDays: 120,
@@ -15787,6 +17377,7 @@ var init_scenarios = __esm({
15787
17377
  label: "The Lone Wolf",
15788
17378
  description: "One rep has great numbers but every single deal is single-threaded.",
15789
17379
  story: "Your top rep is crushing it on paper \u2014 biggest pipeline, highest close rate. But every deal has exactly one contact. One champion goes on vacation, gets promoted, or leaves, and the entire pipeline collapses.",
17380
+ hook: "Top rep, huge pipeline, one contact per deal \u2014 one exit from collapse.",
15790
17381
  loneWolfRepIndex: 0,
15791
17382
  loneWolfSingleThreadRatio: 1,
15792
17383
  singleThreadRatio: 0.15
@@ -15797,6 +17388,7 @@ var init_scenarios = __esm({
15797
17388
  label: "The Busy Bees",
15798
17389
  description: "High activity volume across the team, but most of it hits dead ends.",
15799
17390
  story: "Your team is busy. Activity metrics look great \u2014 calls are up, emails are up, meetings are up. But 60% of that activity is aimed at contacts with no associated pipeline. Reps are spraying, not aiming.",
17391
+ hook: "Reps are spraying, not aiming.",
15800
17392
  activityVolumeMultiplier: 3,
15801
17393
  noiseActivityRatio: 0.6,
15802
17394
  staleContactRatio: 0.2
@@ -17878,12 +19470,12 @@ var init_generator = __esm({
17878
19470
  });
17879
19471
 
17880
19472
  // src/demo/taxonomy-cache.ts
17881
- import { readFileSync as readFileSync17, writeFileSync as writeFileSync13, existsSync as existsSync18, mkdirSync as mkdirSync8, unlinkSync as unlinkSync3 } from "fs";
19473
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync15, existsSync as existsSync18, mkdirSync as mkdirSync9, unlinkSync as unlinkSync3 } from "fs";
17882
19474
  import { homedir as homedir5 } from "os";
17883
- import { join as join19 } from "path";
19475
+ import { join as join22 } from "path";
17884
19476
  function ensureDir5() {
17885
19477
  if (!existsSync18(NTRP_DIR4)) {
17886
- mkdirSync8(NTRP_DIR4, { recursive: true });
19478
+ mkdirSync9(NTRP_DIR4, { recursive: true });
17887
19479
  }
17888
19480
  }
17889
19481
  function loadCachedTaxonomy(profile) {
@@ -17899,14 +19491,14 @@ function loadCachedTaxonomy(profile) {
17899
19491
  }
17900
19492
  function saveCachedTaxonomy(taxonomy) {
17901
19493
  ensureDir5();
17902
- writeFileSync13(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
19494
+ writeFileSync15(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
17903
19495
  }
17904
19496
  var NTRP_DIR4, TAXONOMY_PATH;
17905
19497
  var init_taxonomy_cache = __esm({
17906
19498
  "src/demo/taxonomy-cache.ts"() {
17907
19499
  "use strict";
17908
- NTRP_DIR4 = join19(homedir5(), ".ntrp");
17909
- TAXONOMY_PATH = join19(NTRP_DIR4, "demo-taxonomy.json");
19500
+ NTRP_DIR4 = join22(homedir5(), ".ntrp");
19501
+ TAXONOMY_PATH = join22(NTRP_DIR4, "demo-taxonomy.json");
17910
19502
  }
17911
19503
  });
17912
19504
 
@@ -18142,16 +19734,16 @@ var generate_exports = {};
18142
19734
  __export(generate_exports, {
18143
19735
  handler: () => handler2
18144
19736
  });
18145
- import chalk16 from "chalk";
18146
- import ora5 from "ora";
19737
+ import chalk20 from "chalk";
18147
19738
  async function handler2(args, ctx) {
18148
- const { flags } = parseArgs(args, ["list-scenarios", "regen-taxonomy"]);
19739
+ const { flags } = parseArgs(args, ["list-scenarios", "regen-taxonomy", "brief"]);
18149
19740
  const quiet = ctx.execution.quiet;
19741
+ const brief = getBool(flags, "brief");
18150
19742
  if (getBool(flags, "list-scenarios")) {
18151
- console.log(chalk16.bold("\n Available Scenarios:\n"));
19743
+ console.log(chalk20.bold("\n Available Scenarios:\n"));
18152
19744
  for (const s of SCENARIO_LIST) {
18153
- console.log(` ${chalk16.cyan(s.key.padEnd(20))} ${s.label}`);
18154
- console.log(` ${chalk16.dim(" ".repeat(20))} ${s.description}
19745
+ console.log(` ${chalk20.cyan(s.key.padEnd(20))} ${s.label}`);
19746
+ console.log(` ${chalk20.dim(" ".repeat(20))} ${s.description}
18155
19747
  `);
18156
19748
  }
18157
19749
  return true;
@@ -18161,9 +19753,9 @@ async function handler2(args, ctx) {
18161
19753
  const skipProfile = getFalse(flags, "profile");
18162
19754
  if (!isProfileConfigured(profile) && !skipProfile) {
18163
19755
  console.error();
18164
- console.error(" " + chalk16.red("No company profile found."));
18165
- console.error(" " + chalk16.dim("Run ") + paint("accent", "/onboard") + chalk16.dim(" first for a richer demo,"));
18166
- console.error(" " + chalk16.dim("or pass ") + paint("accent", "--no-profile") + chalk16.dim(" to skip."));
19756
+ console.error(" " + chalk20.red("No company profile found."));
19757
+ console.error(" " + chalk20.dim("Run ") + paint("accent", "/onboard") + chalk20.dim(" first for a richer demo,"));
19758
+ console.error(" " + chalk20.dim("or pass ") + paint("accent", "--no-profile") + chalk20.dim(" to skip."));
18167
19759
  console.error();
18168
19760
  markFailure(ctx);
18169
19761
  return false;
@@ -18171,8 +19763,8 @@ async function handler2(args, ctx) {
18171
19763
  const explicitScenario = getString(flags, "scenario", "s");
18172
19764
  const resolvedScenario = resolveScenarioInput(explicitScenario);
18173
19765
  if (resolvedScenario === null) {
18174
- console.error(chalk16.red(` Unknown scenario: ${explicitScenario}`));
18175
- console.log(chalk16.dim(` Valid: ${SCENARIO_LIST.map((s) => s.key).join(", ")}`));
19766
+ console.error(chalk20.red(` Unknown scenario: ${explicitScenario}`));
19767
+ console.log(chalk20.dim(` Valid: ${SCENARIO_LIST.map((s) => s.key).join(", ")}`));
18176
19768
  markFailure(ctx);
18177
19769
  return false;
18178
19770
  }
@@ -18185,11 +19777,15 @@ async function handler2(args, ctx) {
18185
19777
  if (!explicitScenario && !quiet) {
18186
19778
  const s = getScenario(scenario);
18187
19779
  console.log();
18188
- console.log(" " + paint("accent", "\u2713 Scenario: ") + bold(s.label));
18189
- console.log(" " + chalk16.dim(s.story));
18190
- console.log();
19780
+ if (brief) {
19781
+ console.log(" " + paint("accent", "\u2713 Demo: ") + bold(s.label) + chalk20.dim(" \u2014 " + s.hook));
19782
+ } else {
19783
+ console.log(" " + paint("accent", "\u2713 Scenario: ") + bold(s.label));
19784
+ console.log(" " + chalk20.dim(s.story));
19785
+ console.log();
19786
+ }
18191
19787
  }
18192
- const spinner = quiet ? null : ora5({ text: "Initializing database...", discardStdin: false }).start();
19788
+ const spinner = quiet ? null : makeSpinner("Initializing database\u2026");
18193
19789
  try {
18194
19790
  await initSchema();
18195
19791
  const metricsLens = ctx.analysis.primary === "revenue_metrics";
@@ -18212,17 +19808,21 @@ async function handler2(args, ctx) {
18212
19808
  const result = await generateDemoData(config);
18213
19809
  if (result.mode === "direct") {
18214
19810
  if (spinner) {
18215
- spinner.succeed(`Generated demo data for "${chalk16.cyan(scenario)}" scenario`);
18216
- console.log();
18217
- printEntityCounts(result.counts);
19811
+ if (brief) {
19812
+ spinner.succeed(`Demo loaded \u2014 ${briefCounts(result.counts)}`);
19813
+ } else {
19814
+ spinner.succeed(`Generated demo data for "${chalk20.cyan(scenario)}" scenario`);
19815
+ console.log();
19816
+ printEntityCounts(result.counts);
19817
+ }
18218
19818
  }
18219
- if (!quiet && ctx.analysis.primary !== "revenue_metrics") {
18220
- console.log(chalk16.dim("\n Run /diagnose to compute vital signs.\n"));
19819
+ if (!quiet && !brief && ctx.analysis.primary !== "revenue_metrics") {
19820
+ console.log(chalk20.dim("\n Run /diagnose to compute vital signs.\n"));
18221
19821
  }
18222
19822
  }
18223
19823
  } catch (err) {
18224
19824
  if (spinner) spinner.fail("Generation failed");
18225
- console.error(chalk16.red(String(err)));
19825
+ console.error(chalk20.red(String(err)));
18226
19826
  markFailure(ctx);
18227
19827
  return false;
18228
19828
  }
@@ -18233,13 +19833,26 @@ function markFailure(ctx) {
18233
19833
  process.exitCode = 1;
18234
19834
  }
18235
19835
  }
19836
+ function briefCounts(counts) {
19837
+ const fmt = (n) => n >= 1e4 ? `${(n / 1e3).toFixed(1)}K` : n.toLocaleString("en-US");
19838
+ const parts = [];
19839
+ const take = (key, label) => {
19840
+ const n = counts[key];
19841
+ if (n && n > 0) parts.push(`${fmt(n)} ${label}`);
19842
+ };
19843
+ take("organizations", "accounts");
19844
+ take("people", "contacts");
19845
+ take("opportunities", "deals");
19846
+ take("activities", "activities");
19847
+ return parts.join(" \xB7 ");
19848
+ }
18236
19849
  async function loadOrBuildTaxonomy(profile, forceRegen, ctx) {
18237
19850
  if (!forceRegen) {
18238
19851
  const cached2 = loadCachedTaxonomy(profile);
18239
19852
  if (cached2) return cached2;
18240
19853
  }
18241
19854
  const spinnerText = forceRegen ? "Rebuilding market taxonomy\u2026" : "Researching your market taxonomy\u2026";
18242
- const spinner = ora5({ text: spinnerText, discardStdin: false }).start();
19855
+ const spinner = makeSpinner(spinnerText);
18243
19856
  try {
18244
19857
  const taxonomy = await buildDemoTaxonomy(profile, ctx);
18245
19858
  saveCachedTaxonomy(taxonomy);
@@ -18247,13 +19860,14 @@ async function loadOrBuildTaxonomy(profile, forceRegen, ctx) {
18247
19860
  return taxonomy;
18248
19861
  } catch (err) {
18249
19862
  spinner.fail("Couldn't build market taxonomy \u2014 using generic data pools");
18250
- console.log(" " + chalk16.dim(String(err.message ?? err)));
19863
+ console.log(" " + chalk20.dim(String(err.message ?? err)));
18251
19864
  return void 0;
18252
19865
  }
18253
19866
  }
18254
19867
  var init_generate = __esm({
18255
19868
  "src/commands/generate.ts"() {
18256
19869
  "use strict";
19870
+ init_spinner();
18257
19871
  init_schema();
18258
19872
  init_generator();
18259
19873
  init_scenarios();
@@ -18337,10 +19951,9 @@ var ingest_exports = {};
18337
19951
  __export(ingest_exports, {
18338
19952
  handler: () => handler3
18339
19953
  });
18340
- import chalk17 from "chalk";
18341
- import ora6 from "ora";
19954
+ import chalk21 from "chalk";
18342
19955
  import { readFileSync as readFileSync18, existsSync as existsSync19 } from "fs";
18343
- import { basename as basename3 } from "path";
19956
+ import { basename as basename4 } from "path";
18344
19957
  async function handler3(args, ctx) {
18345
19958
  const { positional, flags } = parseArgs(args, [
18346
19959
  "skip-resolve",
@@ -18359,28 +19972,28 @@ async function handler3(args, ctx) {
18359
19972
  const source = getString(flags, "source", "s") ?? "salesforce";
18360
19973
  const skipResolve = getBool(flags, "skip-resolve");
18361
19974
  if (!file) {
18362
- console.error(chalk17.red(" Usage: /ingest <file> [--source salesforce|hubspot|outreach]"));
18363
- console.error(chalk17.dim(" /ingest --demo [--scenario <name>]"));
19975
+ console.error(chalk21.red(" Usage: /ingest <file> [--source salesforce|hubspot|outreach]"));
19976
+ console.error(chalk21.dim(" /ingest --demo [--scenario <name>]"));
18364
19977
  process.exit(1);
18365
19978
  }
18366
19979
  if (!existsSync19(file)) {
18367
- console.error(chalk17.red(` File not found: ${file}`));
19980
+ console.error(chalk21.red(` File not found: ${file}`));
18368
19981
  process.exit(1);
18369
19982
  }
18370
19983
  const profile = loadProfile();
18371
19984
  const skipProfile = getFalse(flags, "profile");
18372
19985
  if (!profile && !skipProfile) {
18373
19986
  console.error();
18374
- console.error(" " + chalk17.red("No company profile found."));
18375
- console.error(" " + chalk17.dim("Run ") + paint("accent", "/onboard") + chalk17.dim(" first for better column mapping,"));
18376
- console.error(" " + chalk17.dim("or pass ") + paint("accent", "--no-profile") + chalk17.dim(" to skip."));
19987
+ console.error(" " + chalk21.red("No company profile found."));
19988
+ console.error(" " + chalk21.dim("Run ") + paint("accent", "/onboard") + chalk21.dim(" first for better column mapping,"));
19989
+ console.error(" " + chalk21.dim("or pass ") + paint("accent", "--no-profile") + chalk21.dim(" to skip."));
18377
19990
  console.error();
18378
19991
  process.exit(1);
18379
19992
  }
18380
- const spinner = ora6({ text: "Initializing database...", discardStdin: false }).start();
19993
+ const spinner = makeSpinner("Initializing database\u2026");
18381
19994
  try {
18382
19995
  await initSchema();
18383
- spinner.text = "Parsing CSV...";
19996
+ spinner.text = "Parsing CSV\u2026";
18384
19997
  const content = readFileSync18(file, "utf-8");
18385
19998
  const { rows, headers } = parseCSV(content);
18386
19999
  if (rows.length === 0) {
@@ -18389,11 +20002,11 @@ async function handler3(args, ctx) {
18389
20002
  }
18390
20003
  const { detectRevenueLedgerHeaders: detectRevenueLedgerHeaders2 } = await Promise.resolve().then(() => (init_classify_source(), classify_source_exports));
18391
20004
  if (detectRevenueLedgerHeaders2(headers)) {
18392
- spinner.text = "Importing revenue ledger rows...";
20005
+ spinner.text = "Importing revenue ledger rows\u2026";
18393
20006
  const { importRevenueRows: importRevenueRows2 } = await Promise.resolve().then(() => (init_revenue_importer(), revenue_importer_exports));
18394
20007
  const uploadId2 = await insertCSVUpload({
18395
20008
  source_system: source,
18396
- original_filename: basename3(file),
20009
+ original_filename: basename4(file),
18397
20010
  row_count: rows.length,
18398
20011
  column_mappings: { entity_type: "revenue_ledger" },
18399
20012
  status: "processing"
@@ -18405,28 +20018,28 @@ async function handler3(args, ctx) {
18405
20018
  row_count: result2.imported
18406
20019
  });
18407
20020
  spinner.succeed(
18408
- `Imported ${chalk17.bold(result2.imported.toString())} revenue events from ${chalk17.dim(basename3(file))}`
20021
+ `Imported ${chalk21.bold(result2.imported.toString())} revenue events from ${chalk21.dim(basename4(file))}`
18409
20022
  );
18410
20023
  if (result2.errors.length > 0) {
18411
- console.log(chalk17.yellow(` ${result2.errors.length} rows skipped`));
20024
+ console.log(chalk21.yellow(` ${result2.errors.length} rows skipped`));
18412
20025
  }
18413
20026
  if (ctx.analysis) {
18414
20027
  ctx.analysis.data_source_type = "revenue_ledger";
18415
20028
  }
18416
- console.log(chalk17.dim(" Run ") + chalk17.cyan("/metrics") + chalk17.dim(" for SaaS metrics with ledger-backed retention."));
18417
- return `${result2.imported} revenue events from ${basename3(file)}`;
20029
+ console.log(chalk21.dim(" Run ") + chalk21.cyan("/metrics") + chalk21.dim(" for SaaS metrics with ledger-backed retention."));
20030
+ return `${result2.imported} revenue events from ${basename4(file)}`;
18418
20031
  }
18419
- spinner.text = "Detecting entity type...";
20032
+ spinner.text = "Detecting entity type\u2026";
18420
20033
  const detection = detectEntityType(headers, source);
18421
20034
  if (!detection) {
18422
20035
  spinner.fail(`Could not auto-detect entity type for source: ${source}`);
18423
- console.log(chalk17.dim(" Headers found: " + headers.join(", ")));
20036
+ console.log(chalk21.dim(" Headers found: " + headers.join(", ")));
18424
20037
  process.exit(1);
18425
20038
  }
18426
20039
  spinner.text = `Importing ${rows.length} ${detection.entityType} rows...`;
18427
20040
  const uploadId = await insertCSVUpload({
18428
20041
  source_system: source,
18429
- original_filename: basename3(file),
20042
+ original_filename: basename4(file),
18430
20043
  row_count: rows.length,
18431
20044
  column_mappings: detection.mappings,
18432
20045
  status: "processing"
@@ -18443,19 +20056,19 @@ async function handler3(args, ctx) {
18443
20056
  row_count: result.imported
18444
20057
  });
18445
20058
  spinner.succeed(
18446
- `Imported ${chalk17.bold(result.imported.toString())} ${detection.entityType} from ${chalk17.dim(basename3(file))} (${source})`
20059
+ `Imported ${chalk21.bold(result.imported.toString())} ${detection.entityType} from ${chalk21.dim(basename4(file))} (${source})`
18447
20060
  );
18448
20061
  if (result.errors.length > 0) {
18449
- console.log(chalk17.yellow(` ${result.errors.length} rows skipped`));
20062
+ console.log(chalk21.yellow(` ${result.errors.length} rows skipped`));
18450
20063
  for (const err of result.errors.slice(0, 3)) {
18451
- console.log(chalk17.dim(` - ${err}`));
20064
+ console.log(chalk21.dim(` - ${err}`));
18452
20065
  }
18453
20066
  if (result.errors.length > 3) {
18454
- console.log(chalk17.dim(` ... and ${result.errors.length - 3} more`));
20067
+ console.log(chalk21.dim(` ... and ${result.errors.length - 3} more`));
18455
20068
  }
18456
20069
  }
18457
20070
  if (!skipResolve) {
18458
- const resolveSpinner = ora6({ text: "Running identity resolution...", discardStdin: false }).start();
20071
+ const resolveSpinner = makeSpinner("Running identity resolution\u2026");
18459
20072
  const resolved = await resolveIdentities();
18460
20073
  if (resolved.resolved > 0) {
18461
20074
  resolveSpinner.succeed(
@@ -18465,16 +20078,17 @@ async function handler3(args, ctx) {
18465
20078
  resolveSpinner.succeed("No duplicates found");
18466
20079
  }
18467
20080
  }
18468
- return `${result.imported} ${detection.entityType} from ${basename3(file)}`;
20081
+ return `${result.imported} ${detection.entityType} from ${basename4(file)}`;
18469
20082
  } catch (err) {
18470
20083
  spinner.fail("Import failed");
18471
- console.error(chalk17.red(String(err)));
20084
+ console.error(chalk21.red(String(err)));
18472
20085
  process.exit(1);
18473
20086
  }
18474
20087
  }
18475
20088
  var init_ingest = __esm({
18476
20089
  "src/commands/ingest.ts"() {
18477
20090
  "use strict";
20091
+ init_spinner();
18478
20092
  init_schema();
18479
20093
  init_csv_parse();
18480
20094
  init_csv_detect();
@@ -18498,9 +20112,9 @@ __export(ingest_chat_exports, {
18498
20112
  looksLikeFilePath: () => looksLikeFilePath
18499
20113
  });
18500
20114
  import { existsSync as existsSync20 } from "fs";
18501
- import { basename as basename4, resolve as resolve6 } from "path";
20115
+ import { basename as basename5, resolve as resolve7 } from "path";
18502
20116
  import { homedir as homedir6 } from "os";
18503
- import chalk18 from "chalk";
20117
+ import chalk22 from "chalk";
18504
20118
  function extractFilePath(input) {
18505
20119
  const trimmed = input.trim();
18506
20120
  const patterns = [
@@ -18527,23 +20141,23 @@ function extractFilePath(input) {
18527
20141
  return null;
18528
20142
  }
18529
20143
  function expandPath(p) {
18530
- if (p.startsWith("~/")) return resolve6(homedir6(), p.slice(2));
18531
- return resolve6(p);
20144
+ if (p.startsWith("~/")) return resolve7(homedir6(), p.slice(2));
20145
+ return resolve7(p);
18532
20146
  }
18533
20147
  function looksLikeFilePath(input) {
18534
20148
  return extractFilePath(input) !== null;
18535
20149
  }
18536
20150
  async function ingestFromChat(ctx, filePath) {
18537
20151
  if (!ctx.rl) {
18538
- console.log(" " + chalk18.red("Ingest confirm requires interactive mode."));
20152
+ console.log(" " + chalk22.red("Ingest confirm requires interactive mode."));
18539
20153
  return false;
18540
20154
  }
18541
- const name = basename4(filePath);
20155
+ const name = basename5(filePath);
18542
20156
  const prompts = createPromptSession(ctx.rl, ctx);
18543
20157
  try {
18544
20158
  const ok = await prompts.confirm(`Ingest ${name} as CRM export?`, true);
18545
20159
  if (!ok) {
18546
- console.log(" " + chalk18.dim("Ingest cancelled."));
20160
+ console.log(" " + chalk22.dim("Ingest cancelled."));
18547
20161
  return false;
18548
20162
  }
18549
20163
  } finally {
@@ -18571,7 +20185,7 @@ async function ingestFromChat(ctx, filePath) {
18571
20185
  false
18572
20186
  );
18573
20187
  if (useAi) {
18574
- console.log(" " + chalk18.dim("AI column mapping is not wired to ingest yet \u2014 trying standard ingest."));
20188
+ console.log(" " + chalk22.dim("AI column mapping is not wired to ingest yet \u2014 trying standard ingest."));
18575
20189
  }
18576
20190
  } finally {
18577
20191
  prompts2.close();
@@ -18598,12 +20212,18 @@ async function ingestFromChat(ctx, filePath) {
18598
20212
  invalidateGapAudit(ctx);
18599
20213
  saveSessionState(ctx);
18600
20214
  console.log();
18601
- console.log(" " + paint("accent", "\u2713 Data loaded") + chalk18.dim(` \u2014 ${name}`));
20215
+ console.log(" " + paint("accent", "\u2713 Data loaded") + chalk22.dim(` \u2014 ${name}`));
18602
20216
  recordMessage(ctx, "user", `[ingested ${name}]`);
18603
20217
  recordMessage(ctx, "agent", `Loaded ${name}. Checking what we can analyze\u2026`);
18604
20218
  const audit = await refreshGapAudit(ctx);
18605
20219
  printGapCard(audit);
18606
20220
  if (audit.can_compute && ctx.scope?.confirmed_at) {
20221
+ if (ctx.pendingAsk) {
20222
+ console.log();
20223
+ console.log(" " + chalk22.dim("Computing so I can answer\u2026"));
20224
+ await runConversationCompute(ctx);
20225
+ return true;
20226
+ }
18607
20227
  const auto = await maybeAutoCompute(ctx);
18608
20228
  if (auto) return true;
18609
20229
  }
@@ -18626,10 +20246,10 @@ async function maybeAutoCompute(ctx) {
18626
20246
  function isDemoIntent(input) {
18627
20247
  return /\b(use demo|demo data|sample data|try demo|load demo)\b/i.test(input.trim());
18628
20248
  }
18629
- async function loadDemoFromChat(ctx, scenario) {
20249
+ async function loadDemoFromChat(ctx, scenario, opts = {}) {
18630
20250
  if (!guardDemoEnabled()) return false;
18631
20251
  const { handler: demo } = await Promise.resolve().then(() => (init_generate(), generate_exports));
18632
- const args = ["--no-profile"];
20252
+ const args = ["--no-profile", "--brief"];
18633
20253
  if (scenario) args.push("--scenario", scenario);
18634
20254
  const ok = await demo(args, ctx);
18635
20255
  if (!ok) return false;
@@ -18650,15 +20270,23 @@ async function loadDemoFromChat(ctx, scenario) {
18650
20270
  ctx.scope = proposal.scope;
18651
20271
  ctx.scope.confirmed_at = (/* @__PURE__ */ new Date()).toISOString();
18652
20272
  ctx.analysis.primary = ctx.scope.primary_lens;
20273
+ } else if (!ctx.scope.confirmed_at) {
20274
+ ctx.scope = { ...ctx.scope, confirmed_at: (/* @__PURE__ */ new Date()).toISOString() };
20275
+ ctx.analysis.primary = ctx.scope.primary_lens;
18653
20276
  }
18654
20277
  invalidateGapAudit(ctx);
18655
20278
  saveSessionState(ctx);
18656
- console.log();
18657
- console.log(" " + paint("accent", "\u2713 Demo data loaded"));
18658
20279
  recordMessage(ctx, "user", "use demo data");
18659
20280
  recordMessage(ctx, "agent", "Demo dataset ready. Here's what we can work with:");
18660
20281
  const audit = await refreshGapAudit(ctx);
18661
- printGapCard(audit);
20282
+ const shouldAuto = opts.autoCompute || Boolean(ctx.pendingAsk && audit.can_compute && ctx.scope?.confirmed_at);
20283
+ if (shouldAuto && audit.can_compute) {
20284
+ console.log();
20285
+ console.log(" " + chalk22.dim("Computing so I can answer\u2026"));
20286
+ await runConversationCompute(ctx);
20287
+ return true;
20288
+ }
20289
+ printGapCard(audit, { skipSatisfied: true });
18662
20290
  return true;
18663
20291
  }
18664
20292
  var init_ingest_chat = __esm({
@@ -19363,12 +20991,15 @@ Do not run compute until audit_data_gaps reports can_compute. Prefer ingest_file
19363
20991
  - If the question is ambiguous, ask one sharp clarifying question \u2014 still scannable (one line).`;
19364
20992
  const deepJob = `YOUR JOB (DEEP MODE \u2014 user asked for detail or a new data cut):
19365
20993
  - Decide whether you need tools to answer, whether a direct answer is enough, or both. Don't re-call a tool whose result you already have from earlier in the conversation.
19366
- - Match the altitude asked. A high-level question gets the one-sentence story with three numbers max, then an offer to descend. A "how" question gets prescriptive, in-the-weeds steps \u2014 you have built this before: read the play with get_play_detail and prescribe the known-good sequence adapted to their numbers, never generic advice.
20994
+ - Match the altitude asked. A high-level question gets the one-sentence story with three numbers max, then an offer to descend.
20995
+ - Plan-of-attack / "what should we do" questions: prefer draft_strategy with a crisp objective \u2014 do not improvise a multi-week Phase 1/2/3 program inline.
20996
+ - If answering a single-play "how" inline (not a full program): one play + mechanism + what to verify \u2014 read get_play_detail when needed. No phased multi-week roadmap.
20997
+ - After a successful draft_strategy tool result: reply with at most 1\u20132 sentences introducing the handoff. Do not write the plan \u2014 the strategist engine owns grounding, sequencing, and stress-test.
19367
20998
  - Lead with the answer, then structure \u2014 headline, drivers, so-what. Avoid walls of text.
19368
20999
  - When you use numbers, include dollar values where available and lead with financial impact.
19369
21000
  - Reference specific segments, vital signs, or plays by name when it helps the user act.
19370
21001
  - Reference the company by name and use language appropriate to their industry and ICP.
19371
- - PRESCRIPTIVE-STRATEGIC questions \u2014 the user asking what to DO ("what should we do about this?", "how do we turn this around?", "give me a plan of attack") \u2014 call draft_strategy with a crisp objective instead of improvising a multi-step plan inline. The strategist engine grounds, sequences, and stress-tests properly. Descriptive questions (what is happening, why) stay normal Q&A; naming a single playbook play or prescribing its steps is also fine without the strategist.
21002
+ - Descriptive questions (what is happening, why) stay normal Q&A; naming a single playbook play is fine without the strategist.
19372
21003
  - If the question is genuinely ambiguous given everything you already know, ask one sharp clarifying question instead of guessing.`;
19373
21004
  const analystSection = responseMode === "brief" && experiment === "production" ? `ANALYST INSTINCT (apply lightly in brief mode \u2014 one causal link max, no full triage):
19374
21005
  - Lead with the single most expensive or urgent point relevant to this question.
@@ -19553,18 +21184,19 @@ async function* agenticFindings(computeResult, divergences, options) {
19553
21184
  yield doneEvent(findings3, lastMeta, initialContext, messages);
19554
21185
  return;
19555
21186
  }
21187
+ messages.push(response.assistant_message);
19556
21188
  let findings2 = parseFindings(fullText);
19557
- if (findings2.length === 0 && fullText.trim().length > 0) {
19558
- messages.push(response.assistant_message);
19559
- messages.push({
19560
- role: "user",
19561
- content: "Please format your findings as the JSON array specified in your instructions. Respond with ONLY the JSON array, no other text."
19562
- });
19563
- const retry = await callLlm(messages, maxTokens, tools.length > 0);
21189
+ if (findings2.length === 0) {
21190
+ messages.push({ role: "user", content: FINDINGS_JSON_NUDGE });
21191
+ const retry = await callLlm(messages, maxTokens, false);
19564
21192
  findings2 = parseFindings(retry.text);
19565
21193
  messages.push(retry.assistant_message);
19566
- } else {
19567
- messages.push(response.assistant_message);
21194
+ }
21195
+ if (findings2.length === 0) {
21196
+ messages.push({ role: "user", content: FINDINGS_JSON_SCHEMA_NUDGE });
21197
+ const retry2 = await callLlm(messages, maxTokens, false);
21198
+ findings2 = parseFindings(retry2.text);
21199
+ messages.push(retry2.assistant_message);
19568
21200
  }
19569
21201
  for (const finding of findings2) yield { type: "finding", finding };
19570
21202
  yield doneEvent(findings2, lastMeta, initialContext, messages);
@@ -19582,13 +21214,19 @@ async function* agenticFindings(computeResult, divergences, options) {
19582
21214
  });
19583
21215
  messages.push({ role: "tool", tool_call_id: tc.id, content: result });
19584
21216
  }
21217
+ if (mode === "investigation" && iteration + 1 === INVESTIGATION_EVIDENCE_NUDGE_AFTER && iteration + 1 < MAX_ITERATIONS) {
21218
+ messages.push({
21219
+ role: "user",
21220
+ content: "You have enough evidence from the tools above. Next response: emit your findings as ONLY the JSON array (no more tool calls unless a single critical gap remains)."
21221
+ });
21222
+ }
19585
21223
  }
19586
21224
  if (mode === "fresh") {
19587
21225
  messages.push({
19588
21226
  role: "user",
19589
21227
  content: "You've reached the tool budget. Please answer the user's question now with what you've learned."
19590
21228
  });
19591
- const final2 = await callLlm(messages, responseMode === "brief" ? BRIEF_MAX_TOKENS : 2048, tools.length > 0);
21229
+ const final2 = await callLlm(messages, responseMode === "brief" ? BRIEF_MAX_TOKENS : 2048, false);
19592
21230
  if (final2.text.trim()) yield { type: "answer", text: final2.text.trim() };
19593
21231
  messages.push(final2.assistant_message);
19594
21232
  yield doneEvent([], lastMeta, initialContext, messages);
@@ -19598,10 +21236,16 @@ async function* agenticFindings(computeResult, divergences, options) {
19598
21236
  role: "user",
19599
21237
  content: "You've reached the investigation limit. Please provide your findings now as a JSON array."
19600
21238
  });
19601
- const final = await callLlm(messages, 4096, false);
19602
- const findings = parseFindings(final.text);
19603
- for (const finding of findings) yield { type: "finding", finding };
21239
+ let final = await callLlm(messages, 4096, false);
21240
+ let findings = parseFindings(final.text);
19604
21241
  messages.push(final.assistant_message);
21242
+ if (findings.length === 0) {
21243
+ messages.push({ role: "user", content: FINDINGS_JSON_SCHEMA_NUDGE });
21244
+ final = await callLlm(messages, 4096, false);
21245
+ findings = parseFindings(final.text);
21246
+ messages.push(final.assistant_message);
21247
+ }
21248
+ for (const finding of findings) yield { type: "finding", finding };
19605
21249
  yield doneEvent(findings, lastMeta, initialContext, messages);
19606
21250
  }
19607
21251
  function buildInitialContext(computeResult, divergences, userQuestion) {
@@ -19635,7 +21279,7 @@ Here is the current GTM health snapshot as background. Use it plus any tools you
19635
21279
  function parseFindings(text) {
19636
21280
  return parseJsonArrayFromText(text) ?? [];
19637
21281
  }
19638
- var MAX_ITERATIONS, BRIEF_MAX_TOKENS, DEEP_MAX_TOKENS;
21282
+ var MAX_ITERATIONS, INVESTIGATION_EVIDENCE_NUDGE_AFTER, BRIEF_MAX_TOKENS, DEEP_MAX_TOKENS, FINDINGS_JSON_NUDGE, FINDINGS_JSON_SCHEMA_NUDGE;
19639
21283
  var init_agentic_loop = __esm({
19640
21284
  "src/ai/agentic-loop.ts"() {
19641
21285
  "use strict";
@@ -19652,8 +21296,12 @@ var init_agentic_loop = __esm({
19652
21296
  init_thread();
19653
21297
  init_prompt_parts();
19654
21298
  MAX_ITERATIONS = 10;
21299
+ INVESTIGATION_EVIDENCE_NUDGE_AFTER = 5;
19655
21300
  BRIEF_MAX_TOKENS = 768;
19656
21301
  DEEP_MAX_TOKENS = 4096;
21302
+ FINDINGS_JSON_NUDGE = "Please format your findings as the JSON array specified in your instructions. Respond with ONLY the JSON array, no other text.";
21303
+ FINDINGS_JSON_SCHEMA_NUDGE = `Respond with ONLY a JSON array of finding objects \u2014 no prose, no markdown fences. Each object needs: severity, segment, finding, vital_signs, entity_count, recommended_focus, dollar_value, recommended_plays. Shape:
21304
+ ${FINDINGS_SCHEMA_BLOCK}`;
19657
21305
  }
19658
21306
  });
19659
21307