@sonnechasser/ntrp 1.4.3 → 1.4.6

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.
@@ -5742,8 +5742,22 @@ var init_pseudonymize = __esm({
5742
5742
  });
5743
5743
 
5744
5744
  // src/config/profile.ts
5745
+ var profile_exports = {};
5746
+ __export(profile_exports, {
5747
+ isProfileConfigured: () => isProfileConfigured,
5748
+ loadProfile: () => loadProfile,
5749
+ profileExists: () => profileExists,
5750
+ profilePath: () => profilePath,
5751
+ saveProfile: () => saveProfile,
5752
+ updateProfile: () => updateProfile
5753
+ });
5745
5754
  import { readFileSync as readFileSync11, writeFileSync as writeFileSync10, existsSync as existsSync12, mkdirSync as mkdirSync9 } from "fs";
5746
5755
  import { join as join12 } from "path";
5756
+ function ensureDir5() {
5757
+ if (!existsSync12(NTRP_DIR3)) {
5758
+ mkdirSync9(NTRP_DIR3, { recursive: true });
5759
+ }
5760
+ }
5747
5761
  function profilePath() {
5748
5762
  return PROFILE_PATH;
5749
5763
  }
@@ -5764,6 +5778,35 @@ function loadProfile() {
5764
5778
  return null;
5765
5779
  }
5766
5780
  }
5781
+ function saveProfile(profile) {
5782
+ ensureDir5();
5783
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
5784
+ const toWrite = {
5785
+ ...profile,
5786
+ schema_version: 1,
5787
+ created_at: profile.created_at || now2,
5788
+ updated_at: now2
5789
+ };
5790
+ writeFileSync10(PROFILE_PATH, JSON.stringify(toWrite, null, 2) + "\n");
5791
+ }
5792
+ function updateProfile(patch) {
5793
+ const existing = loadProfile();
5794
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
5795
+ const merged = {
5796
+ schema_version: 1,
5797
+ company_name: "",
5798
+ industry: "",
5799
+ product_description: "",
5800
+ target_customer: "",
5801
+ sales_motion: "mid_market",
5802
+ created_at: now2,
5803
+ updated_at: now2,
5804
+ ...existing ?? {},
5805
+ ...patch
5806
+ };
5807
+ saveProfile(merged);
5808
+ return merged;
5809
+ }
5767
5810
  var NTRP_DIR3, PROFILE_PATH;
5768
5811
  var init_profile = __esm({
5769
5812
  "src/config/profile.ts"() {
@@ -8598,6 +8641,15 @@ var init_context2 = __esm({
8598
8641
  });
8599
8642
 
8600
8643
  // src/pipeline/segments.ts
8644
+ function resolveSegmentByName(query, segments) {
8645
+ const lower = query.toLowerCase();
8646
+ const exact = segments.find((s) => s.segment.name.toLowerCase() === lower);
8647
+ if (exact) return { type: "exact", segment: exact };
8648
+ const matches = segments.filter((s) => s.segment.name.toLowerCase().includes(lower));
8649
+ if (matches.length === 1) return { type: "exact", segment: matches[0] };
8650
+ if (matches.length > 1) return { type: "ambiguous", matches };
8651
+ return { type: "none" };
8652
+ }
8601
8653
  function resolveSegmentScopeFromSnapshot(segment, snapshot) {
8602
8654
  const orgIds = [];
8603
8655
  const peopleIds = [];
@@ -8945,8 +8997,9 @@ function resolveThresholds(salesMotion, computedBaselines) {
8945
8997
  return resolved;
8946
8998
  }
8947
8999
  async function getResolvedThresholds() {
9000
+ const { loadProfile: loadProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
8948
9001
  const { getConfigValue: getConfigValue2 } = await Promise.resolve().then(() => (init_store(), store_exports));
8949
- const motion = getConfigValue2("sales-motion");
9002
+ const motion = loadProfile2()?.sales_motion ?? getConfigValue2("sales-motion");
8950
9003
  return resolveThresholds(motion ?? null, {});
8951
9004
  }
8952
9005
  var init_resolve = __esm({
@@ -10274,7 +10327,7 @@ handler: ../commands/thinkwithme.ts
10274
10327
 
10275
10328
  Open a dedicated \`think \u203A\` channel to explore ideas, challenge assumptions, and pull evidence from your data.
10276
10329
  Bare \`/thinkwithme\` enters the channel. Type \`/thinkwithme <topic>\` to seed the first turn.
10277
- Requires an analysis and an AI key (\`/connect\`). Type \`done\` or \`cancel\` to return to ask \u203A.
10330
+ Requires an analysis and an AI key (\`/connect\`). Type \`done\` or \`cancel\` to return to \u203A.
10278
10331
  When you are ready to commit to a plan, say so or the partner can hand off to \`/strategy\`.`
10279
10332
  },
10280
10333
  {
@@ -10462,6 +10515,7 @@ Manage CLI configuration stored at \`~/.ntrp/config.json\`. Useful keys:
10462
10515
  \`api-key\` (Anthropic), \`openai-api-key\` (and \`groq-api-key\`, \`google-api-key\`, ...),
10463
10516
  \`llm-primary\` (default provider), \`llm-tier\`, \`llm-auto-failover\`,
10464
10517
  \`voice-personality\`, \`voice-roast\`,
10518
+ \`sales-motion\`, \`rep_hourly_cost\`, \`hours_per_activity\`,
10465
10519
  \`default-format\`, \`export-dir\`, \`ai-inbox-dir\` (or type \`/inbox set\`).
10466
10520
 
10467
10521
  Setting a provider key opens a hidden prompt and auto-discovers that provider's models.
@@ -11240,8 +11294,11 @@ function buildInvestigationTools() {
11240
11294
  if (isWebRetrievalEnabled()) tools2.push(WEB_SEARCH_TOOL);
11241
11295
  return tools2;
11242
11296
  }
11243
- function buildFreshNlTools() {
11244
- const tools2 = [...AGENTIC_TOOLS, ...CONVERSATION_TOOLS];
11297
+ function buildFreshNlTools(opts = {}) {
11298
+ const conversationTools = opts.analysisReady ? CONVERSATION_TOOLS.filter(
11299
+ (t) => t.name !== "propose_scope" && t.name !== "confirm_scope" && t.name !== "run_compute"
11300
+ ) : CONVERSATION_TOOLS;
11301
+ const tools2 = [...AGENTIC_TOOLS, ...conversationTools];
11245
11302
  if (isWebRetrievalEnabled()) tools2.push(WEB_SEARCH_TOOL);
11246
11303
  return tools2;
11247
11304
  }
@@ -13817,7 +13874,6 @@ __export(phase_exports, {
13817
13874
  buildConversationPrompt: () => buildConversationPrompt,
13818
13875
  consumeOrientEmptyEnterCoach: () => consumeOrientEmptyEnterCoach,
13819
13876
  formatPhaseLabel: () => formatPhaseLabel,
13820
- getConversationPhaseBlock: () => getConversationPhaseBlock,
13821
13877
  resolveConversationPhase: () => resolveConversationPhase,
13822
13878
  sessionHasData: () => sessionHasData
13823
13879
  });
@@ -13857,6 +13913,8 @@ function formatPhaseLabel(phase) {
13857
13913
  switch (phase) {
13858
13914
  case "orient":
13859
13915
  return "setup";
13916
+ case "awaiting_data":
13917
+ return "data loading";
13860
13918
  case "explore":
13861
13919
  return "ready to ask";
13862
13920
  case "think":
@@ -13865,15 +13923,24 @@ function formatPhaseLabel(phase) {
13865
13923
  return phase.replace(/_/g, " ");
13866
13924
  }
13867
13925
  }
13926
+ function accentPromptHead(phase, sessionName) {
13927
+ const scope = sessionName ? ` ${sessionName}` : "";
13928
+ if (phase === "orient" || phase === "awaiting_data" || phase === "explore") {
13929
+ if (phase === "explore" && sessionName) {
13930
+ return paint("accent", `\u203A`) + chalk8.dim(`${scope} `);
13931
+ }
13932
+ return paint("accent", `\u203A `);
13933
+ }
13934
+ if (phase === "compute") {
13935
+ return paint("accent", `\u2026 `);
13936
+ }
13937
+ const word = PROMPT_LABELS[phase].replace(" \u203A", "");
13938
+ return paint("accent", `${word}${scope} \u203A `);
13939
+ }
13868
13940
  function buildConversationPrompt(ctx) {
13869
13941
  const phase = resolveConversationPhase(ctx);
13870
- const label = PROMPT_LABELS[phase];
13871
- const scope = ctx.sessionName ? ` ${ctx.sessionName}` : "";
13942
+ const sessionName = ctx.sessionName ?? "";
13872
13943
  const action = resolveRecommendedAction(ctx);
13873
- if (phase === "orient") {
13874
- const enterHint2 = action ? chalk8.dim(`\u23CE ${action.hint} `) : "";
13875
- return paint("accent", `${label} `) + enterHint2;
13876
- }
13877
13944
  if (phase === "explore") {
13878
13945
  const mode = defaultExploreResponseMode(ctx, Math.floor(ctx.messages.length / 2));
13879
13946
  const modeTag = mode === "brief" ? "brief" : "deep";
@@ -13881,34 +13948,15 @@ function buildConversationPrompt(ctx) {
13881
13948
  const strategyWait = ctx.strategistState?.step === "awaiting_connect" ? chalk8.dim(" \xB7 strategy after /connect") : "";
13882
13949
  const thinkWait = ctx.thinkState?.step === "awaiting_connect" ? chalk8.dim(" \xB7 think after /connect") : "";
13883
13950
  const enterHint2 = action ? chalk8.dim(` \xB7 \u23CE ${action.hint}`) : "";
13884
- return paint("accent", `ask${scope} \u203A `) + chalk8.dim(`${modeTag} \xB7 ${stack}`) + strategyWait + thinkWait + enterHint2 + " ";
13951
+ return accentPromptHead(phase, sessionName) + chalk8.dim(`${modeTag} \xB7 ${stack}`) + strategyWait + thinkWait + enterHint2 + " ";
13885
13952
  }
13886
13953
  if (phase === "think") {
13887
13954
  const stack = canUseReplAi(ctx) ? formatActiveStackShort(ctx) : "no engine";
13888
13955
  const enterHint2 = action ? chalk8.dim(` \xB7 \u23CE ${action.hint}`) : "";
13889
- return paint("accent", `think${scope} \u203A `) + chalk8.dim(stack) + enterHint2 + " ";
13956
+ return accentPromptHead(phase, sessionName) + chalk8.dim(stack) + enterHint2 + " ";
13890
13957
  }
13891
13958
  const enterHint = action ? chalk8.dim(`\u23CE ${action.hint} `) : "";
13892
- return paint("accent", `${label.replace(" \u203A", "")}${scope} \u203A `) + enterHint;
13893
- }
13894
- function getConversationPhaseBlock(ctx) {
13895
- const phase = resolveConversationPhase(ctx);
13896
- const lines = [`Conversation phase: ${formatPhaseLabel(phase)}`];
13897
- if (ctx.scope) {
13898
- lines.push(`Intent: ${ctx.scope.intent_summary}`);
13899
- lines.push(`Primary lens: ${ctx.scope.primary_lens}`);
13900
- if (ctx.scope.audience) lines.push(`Audience: ${ctx.scope.audience}`);
13901
- if (ctx.scope.time_horizon) lines.push(`Time horizon: ${ctx.scope.time_horizon}`);
13902
- if (ctx.scope.confirmed_at) lines.push(`Scope confirmed: ${ctx.scope.confirmed_at}`);
13903
- }
13904
- if (ctx.dataset?.label) lines.push(`Dataset: ${ctx.dataset.label}`);
13905
- if (ctx.gapAudit) {
13906
- lines.push(`Can compute: ${ctx.gapAudit.can_compute}`);
13907
- if (ctx.gapAudit.missing.length > 0) {
13908
- lines.push(`Data gaps: ${ctx.gapAudit.missing.map((m) => m.label).join(", ")}`);
13909
- }
13910
- }
13911
- return lines.join("\n");
13959
+ return accentPromptHead(phase, sessionName) + enterHint;
13912
13960
  }
13913
13961
  var PROMPT_LABELS;
13914
13962
  var init_phase = __esm({
@@ -13924,9 +13972,9 @@ var init_phase = __esm({
13924
13972
  PROMPT_LABELS = {
13925
13973
  orient: "\u203A",
13926
13974
  scope: "scope \u203A",
13927
- awaiting_data: "data \u203A",
13975
+ awaiting_data: "\u203A",
13928
13976
  compute: "\u2026",
13929
- explore: "ask \u203A",
13977
+ explore: "\u203A",
13930
13978
  think: "think \u203A",
13931
13979
  strategize: "strategy \u203A",
13932
13980
  deliver: "ship \u203A"
@@ -15999,15 +16047,15 @@ var init_metric_definitions = __esm({
15999
16047
  kind: "vital",
16000
16048
  label: "Freshness",
16001
16049
  group: "Vital Signs",
16002
- tagline: "Does the CRM report which records are still active?",
16050
+ tagline: "Is the CRM still talking about live work \u2014 or a museum of old deals?",
16003
16051
  how_computed: "NTRP computes a weighted average of people, organizations, and opportunities with recent activity. Open opportunities must also not be past-due. Default windows: people and organizations 90 days, opportunities 30 days. Weights are 35 / 30 / 35.",
16004
16052
  formula_lines: [
16005
16053
  "freshness = people%\xD70.35 + orgs%\xD70.30 + opps%\xD70.35",
16006
16054
  "people/orgs fresh if activity within 90d",
16007
16055
  "opps fresh if activity within 30d AND not past-due"
16008
16056
  ],
16009
- meaning: "Board question: how much of this pipeline is real versus fiction? Dollar value equals the sum of amount on stale opportunities: pipeline at risk.",
16010
- expert_read: "Cut by owner and by stage first. Freshness reds concentrate on people or process. They rarely spread evenly. In a long-cycle enterprise motion, 30 quiet days can be normal cadence. In a velocity motion, 30 quiet days is a dead deal. A sudden cliff usually means a broken integration or a departed rep. It is not gradual decay. Check this false positive: bulk-imported records that nobody has touched yet.",
16057
+ meaning: 'We treat Freshness as a truth check on the pipeline number. Quiet days mean different things by motion: in enterprise, 30 quiet days can be normal cadence; in a velocity book, that same silence often means the deal is already dead. Stale open amount is why we show "pipeline at risk."',
16058
+ expert_read: `A useful first cut is by owner and by stage \u2014 reds tend to clump on people or process, not sprinkle evenly. A sudden cliff is usually a broken integration or a departed rep, not gradual decay. Watch for the false positive too: bulk-imported records nobody has touched yet can look "stale" when they're just new.`,
16011
16059
  deepdive: [
16012
16060
  "Status bands: green 80 or more, yellow 60 or more, red below 60. Motion presets can change the windows.",
16013
16061
  'Dollar translation: sum of amount on stale open opportunities \u2192 "pipeline at risk".',
@@ -16037,15 +16085,15 @@ var init_metric_definitions = __esm({
16037
16085
  kind: "vital",
16038
16086
  label: "Flow Rate",
16039
16087
  group: "Vital Signs",
16040
- tagline: "How fast do deals move, and where do they stop?",
16088
+ tagline: "Where does deal motion slow \u2014 and what's stuck behind that bottleneck?",
16041
16089
  how_computed: "NTRP sets a base score from average open-deal age versus max_days. It then applies a penalty of up to 20 for the share of stuck deals. A deal is stuck when it has no update beyond stuck_days, or a past-due close. Status uses average open age, not the score alone.",
16042
16090
  formula_lines: [
16043
16091
  "base = 100 \xD7 (1 \u2212 avgOpenAge / max_days)",
16044
16092
  "score = base \u2212 stuckSharePenalty (\u226420)",
16045
16093
  "stuck = no update > stuck_days OR past-due close"
16046
16094
  ],
16047
- meaning: "Board question: is next quarter slipping because deals are stuck? Dollar value equals the amount stuck in pipeline.",
16048
- expert_read: "Cut by stage age, not only deal age. Find the stage where deals stop. That is usually one stage. Compare stuck-deal age to this company's own median cycle. Do not use a generic norm. Stuck deals plus past-due close dates signal optimistic forecasting. That is a credibility problem before it is a revenue problem.",
16095
+ meaning: 'We think of Flow Rate as velocity risk with a dollar tag. When deals stop advancing \u2014 and especially when close dates slip past due \u2014 next quarter starts to feel optimistic on paper. The dollar line is the amount still "stuck in pipeline."',
16096
+ expert_read: "We prefer stage age over deal age alone: find the stage where advancement collapses \u2014 it's usually one stage, not everywhere. Compare stuck age to this company's own median cycle rather than a generic norm. Stuck deals plus past-due closes are often a forecast-credibility problem before they're a revenue miss.",
16049
16097
  deepdive: [
16050
16098
  "Status uses average open age. Green is 45 days or less. Yellow is 90 days or less. Else red. Defaults: max_days 120, stuck_days 60.",
16051
16099
  'Dollar translation: sum of amount on stuck deals \u2192 "stuck in pipeline".',
@@ -16077,15 +16125,15 @@ var init_metric_definitions = __esm({
16077
16125
  kind: "vital",
16078
16126
  label: "Drop Rate",
16079
16127
  group: "Vital Signs",
16080
- tagline: "Where do leads drop between systems?",
16128
+ tagline: "Where does paid demand fall between systems before anyone works it?",
16081
16129
  how_computed: "NTRP blends cross-system retention and opportunity retention. Cross-system retention is marketing people also present in sales. Opportunity retention is open opportunities that are not abandoned. Default weights: cross-system 60 percent, opportunity retention 40 percent. Abandoned means open opportunities with no activity in 30 days.",
16082
16130
  formula_lines: [
16083
16131
  "score = crossSystemRetention\xD70.6 + oppRetention\xD70.4",
16084
16132
  "cross-system = marketing people also in sales CRM",
16085
16133
  "abandoned = open opps with no activity in 30d"
16086
16134
  ],
16087
- meaning: "Board question: how much pipeline did we pay for and never work? Dollar value = droppedCount \xD7 conversionRate \xD7 avgDealSize \u2014 est. lost at handoff.",
16088
- expert_read: "This is almost always a systems failure. Causes include routing rules, unassigned territories, dead rep queues, or a sync gap between marketing and CRM. It is not lazy reps. First cut by lead source. The leak usually concentrates in one or two sources. The cheapest pipeline this business can buy is the leads it already paid for.",
16135
+ meaning: 'We read Drop Rate as the price of the handoff leak \u2014 budget already spent on leads that never reach a working rep. The dollar estimate is droppedCount \xD7 conversion \xD7 avg deal size: "est. lost at handoff."',
16136
+ expert_read: 'In practice this usually looks like a systems story: routing rules, unassigned territories, dead rep queues, or a sync gap between marketing and CRM \u2014 more often than "lazy reps." A useful first cut is by lead source; the leak tends to concentrate in one or two sources. The cheapest pipeline a business can buy is often the leads it already paid for.',
16089
16137
  deepdive: [
16090
16138
  "Status bands: green 80 or more, yellow 60 or more, red below 60.",
16091
16139
  'Dollar translation: dropped \xD7 conversion \xD7 avg deal (fallback: drop% \xD7 open pipeline) \u2192 "est. lost at handoff".',
@@ -16116,15 +16164,15 @@ var init_metric_definitions = __esm({
16116
16164
  kind: "vital",
16117
16165
  label: "Signal:Noise",
16118
16166
  group: "Vital Signs",
16119
- tagline: "How much activity is aimed at deals that can still close?",
16167
+ tagline: "How much of the team's effort still aims at deals that can close?",
16120
16168
  how_computed: "Over a 90-day lookback, NTRP computes (signal activities / all activities) \xD7 100. Signal is activity linked to an open opportunity, a pipeline person, or a pipeline organization.",
16121
16169
  formula_lines: [
16122
16170
  "score = (signalCount / activityCount) \xD7 100",
16123
16171
  "signal = linked to open opp / pipeline person / pipeline org",
16124
16172
  "lookback = trailing 90 days"
16125
16173
  ],
16126
- meaning: "Board question: are we burning capacity on dead accounts? Dollar value = noiseCount \xD7 hours_per_activity \xD7 rep_hourly_cost \u2014 misdirected effort.",
16127
- expert_read: "Cut by rep and by account status. Noise usually means reps work dead accounts they already know. Targeting and account lists are stale. Persistent noise is a coverage-model problem, not a coaching problem. Check whether activity is logged against closed or unlinked records. That is often a hygiene artifact.",
16174
+ meaning: `We treat Signal:Noise as activity efficiency with a capacity price. Noise hours \xD7 hours_per_activity \xD7 rep_hourly_cost becomes "misdirected effort" \u2014 energy aimed at accounts that can't still close.`,
16175
+ expert_read: "A useful cut is by rep and by account status. Persistent noise often means reps keep working dead accounts they already know \u2014 stale targeting or coverage models more than a coaching gap. Also check whether activity is logged against closed or unlinked records; that can be a hygiene artifact pretending to be effort.",
16128
16176
  deepdive: [
16129
16177
  "Status bands: green 65 or more, yellow 40 or more, red below 40.",
16130
16178
  "Dollar defaults: 0.25 hours/activity \xD7 $75/hr (config: hours_per_activity, rep_hourly_cost).",
@@ -16153,15 +16201,15 @@ var init_metric_definitions = __esm({
16153
16201
  kind: "vital",
16154
16202
  label: "Thread Depth",
16155
16203
  group: "Vital Signs",
16156
- tagline: "How fragile is the pipeline if one champion stops?",
16204
+ tagline: "How fragile is revenue if one champion changes jobs?",
16157
16205
  how_computed: "Percent of open deals with at least multi_thread_threshold distinct people active in the last 90 days. The default threshold is 2. People include opportunity-direct contacts and same-organization activity.",
16158
16206
  formula_lines: [
16159
16207
  "score = % open deals with \u22652 active people (90d)",
16160
16208
  "people counted via opp contacts + same-org activity",
16161
16209
  "threshold configurable (default 2)"
16162
16210
  ],
16163
- meaning: "Board question: how much revenue is lost if one contact changes jobs? Dollar value equals the sum of amount on single-threaded deals.",
16164
- expert_read: "Weight by deal size. One single-threaded mega-deal outweighs ten small ones. Single-threading late in the cycle is far more dangerous than early. In enterprise motions, Thread Depth is a leading indicator of slipped quarters. Champions change jobs. There is no second contact.",
16211
+ meaning: "We read Thread Depth as resilience risk. One single-threaded mega-deal can outweigh ten small ones \u2014 and late-cycle single-threading is especially scary because champions change jobs. The dollar line sums amount on single-threaded deals.",
16212
+ expert_read: "Weight by deal size when you triage. Single-threading late in the cycle tends to matter more than early. In enterprise motions, thin threads are often a leading indicator of slipped quarters: the champion leaves and there's no second contact warm enough to carry the deal.",
16165
16213
  deepdive: [
16166
16214
  "Status bands: green 65 or more, yellow 40 or more, red below 40.",
16167
16215
  'Dollar translation: sum of amount on single-threaded deals \u2192 "single-threaded".',
@@ -16193,14 +16241,14 @@ var init_metric_definitions = __esm({
16193
16241
  kind: "saas",
16194
16242
  label: "ARR",
16195
16243
  group: "Revenue",
16196
- tagline: "How big is the revenue engine, and from where?",
16244
+ tagline: "How big is the engine \u2014 and what's the mix that got it there?",
16197
16245
  how_computed: "Sum of amount on closed-won opportunities in the dataset. This is pipeline-inferred ARR when a pure subscription ledger is not available.",
16198
16246
  formula_lines: [
16199
16247
  "ARR \u2248 \u03A3 amount on closed-won opportunities",
16200
16248
  "New + Expansion = growth \xB7 Churned + Contraction = leakage"
16201
16249
  ],
16202
- meaning: "Board question: how fast are we growing, and from where? Always decompose growth into new versus expansion. The mix is the story.",
16203
- expert_read: "Always decompose growth into new versus expansion. The mix is the story. Prefer this company's own trailing history over any external prior. A number below its reliability_gate is a hypothesis, not a fact.",
16250
+ meaning: "We treat ARR as the size of the revenue engine \u2014 but the interesting story is the mix. New versus expansion growth, and how much leakage (churn + contraction) ate into it. From CRM exports we often infer this from closed-won amounts when a pure subscription ledger isn't available.",
16251
+ expert_read: "Always decompose growth into new versus expansion before you brief anyone \u2014 the mix is the story. Prefer this company's own trailing history over an external prior. When a number sits below its reliability_gate, we treat it as a hypothesis, not a fact.",
16204
16252
  deepdive: [
16205
16253
  "Companion metrics: New ARR, Expansion ARR, Churned ARR, Contraction ARR.",
16206
16254
  "Estimation method may be ledger, pipeline_inferred, or snapshot. Read confidence and reliability_gate.",
@@ -16353,15 +16401,15 @@ var init_metric_definitions = __esm({
16353
16401
  kind: "saas",
16354
16402
  label: "Net Revenue Retention",
16355
16403
  group: "Retention",
16356
- tagline: "Would this business grow if sales stopped selling?",
16404
+ tagline: "Would the base still grow if new logo sales paused?",
16357
16405
  how_computed: "startingArr = ARR + Churned + Contraction \u2212 Expansion. NRR = ((starting \u2212 Churned \u2212 Contraction + Expansion) / starting) \xD7 100.",
16358
16406
  formula_lines: [
16359
16407
  "starting = ARR + churned + contraction \u2212 expansion",
16360
16408
  "NRR = (starting \u2212 churned \u2212 contraction + expansion) / starting \xD7 100",
16361
16409
  "NRR = 100% + expansion% \u2212 contraction% \u2212 churn%"
16362
16410
  ],
16363
- meaning: "Board question: would this business grow if sales stopped selling? A value above 100% means growth from existing customers.",
16364
- expert_read: "Decompose before you judge. The same 95% can be a churn problem (product/PMF) or a no-expansion problem (packaging/motion). Those have different owners. Priors by segment: about 97% SMB, about 108% mid-market, about 118% enterprise medians. 110% or more is a strong signal at any stage.",
16411
+ meaning: "We love NRR because it answers a sharp question: would this business still grow if sales stopped selling new logos? Above 100% means the installed base compounds. From CRM we reconstruct the walk (+expansion \u2212contraction \u2212churn) and stay humble when the data is pipeline-inferred.",
16412
+ expert_read: "Decompose before you judge \u2014 the same 95% can be a churn/PMF problem or a no-expansion/packaging problem, and those have different owners. Rough priors by segment sit around 97% SMB, 108% mid-market, 118% enterprise medians; 110%+ is a strong signal at any stage.",
16365
16413
  deepdive: [
16366
16414
  "Always show the waterfall: +expansion \u2212contraction \u2212churn.",
16367
16415
  "GRR is the floor. NRR adds expansion on top.",
@@ -16389,14 +16437,14 @@ var init_metric_definitions = __esm({
16389
16437
  kind: "saas",
16390
16438
  label: "Gross Revenue Retention",
16391
16439
  group: "Retention",
16392
- tagline: "How leaky is the bucket before expansion hides it?",
16440
+ tagline: "How leaky is the bucket before expansion can hide it?",
16393
16441
  how_computed: "GRR = ((startingArr \u2212 Churned \u2212 Contraction) / startingArr) \xD7 100. Expansion is excluded on purpose.",
16394
16442
  formula_lines: [
16395
16443
  "starting = ARR + churned + contraction \u2212 expansion",
16396
16444
  "GRR = (starting \u2212 churned \u2212 contraction) / starting \xD7 100"
16397
16445
  ],
16398
- meaning: "Board question: how leaky is the bucket before expansion hides it? Prior: above 90% is healthy. Above 95% is strong for enterprise.",
16399
- expert_read: "GRR is the honesty metric. Expansion can make NRR look fine while GRR erodes. Always read both.",
16446
+ meaning: "We treat GRR as the honesty metric \u2014 churn plus contraction only, with expansion deliberately left out. That way a shiny NRR can't paper over a leaky bucket. Rough priors: above 90% feels healthy; above 95% is strong for enterprise.",
16447
+ expert_read: "Always read GRR next to NRR. Expansion can make net retention look fine while the floor erodes. Owners tend to split: product/CS for logo churn, packaging for contraction.",
16400
16448
  deepdive: [
16401
16449
  "GRR never includes Expansion. That is the point.",
16402
16450
  "Owners: product and CS for churn. Packaging for contraction."
@@ -16419,14 +16467,14 @@ var init_metric_definitions = __esm({
16419
16467
  kind: "saas",
16420
16468
  label: "Pipeline Coverage",
16421
16469
  group: "Pipeline",
16422
- tagline: "Is next quarter already at risk?",
16470
+ tagline: "Does next quarter already have enough real pipeline?",
16423
16471
  how_computed: "Open pipeline amount divided by trailing-90-day closed-won amount.",
16424
16472
  formula_lines: [
16425
16473
  "Coverage = openPipeline / trailing_90d_won",
16426
16474
  "required \u2248 1 / win_rate (discount for time left)"
16427
16475
  ],
16428
- meaning: "Board question: is next quarter already at risk? Priors scale with cycle length: about 3x for velocity/SMB, 4 to 5x for enterprise.",
16429
- expert_read: "Coverage has no meaning without win rate. Required coverage is about 1 / win rate, discounted for time left in the period. Inflated stages and zombie deals fake coverage. Cross-check with Freshness before you trust it.",
16476
+ meaning: `We read coverage as an early "is next quarter already thin?" check. Priors scale with cycle length \u2014 roughly 3x for velocity/SMB, 4\u20135x for enterprise \u2014 but coverage without win rate is half a story. We also cross-check Freshness so zombies don't fake a healthy multiple.`,
16477
+ expert_read: "Required coverage is roughly 1 / win rate, discounted for time left in the period. Inflated stages and stale deals can manufacture coverage that isn't there. Pair with Win Rate and Freshness before you trust the brief.",
16430
16478
  deepdive: [
16431
16479
  "Always pair with Win Rate and Freshness.",
16432
16480
  "Weighted Pipeline is the credibility-adjusted companion."
@@ -16511,14 +16559,14 @@ var init_metric_definitions = __esm({
16511
16559
  kind: "saas",
16512
16560
  label: "Pipeline Velocity",
16513
16561
  group: "Pipeline",
16514
- tagline: "Revenue throughput per day. Four levers, one number.",
16562
+ tagline: "Four levers, one throughput number \u2014 which lever moved?",
16515
16563
  how_computed: "(openOpps \xD7 avgDeal \xD7 winRate) / avgCycleDays. Requires 3 or more dated closed-won deals. Unit: $/day.",
16516
16564
  formula_lines: [
16517
16565
  "Velocity = (openOpps \xD7 avgDeal \xD7 winRate) / avgCycleDays",
16518
16566
  "four levers: #opps \xB7 deal size \xB7 win rate \xB7 cycle days"
16519
16567
  ],
16520
- meaning: "Board question: which lever moved when throughput changed? This is the most decision-ready pipeline metric.",
16521
- expert_read: "When velocity changes, name which lever moved. A win-rate rise on falling opportunity volume is qualification tightening, not improvement.",
16568
+ meaning: "We like velocity because it's decision-ready: revenue throughput per day with four named levers (volume, size, win rate, cycle). When throughput changes, the interesting question is which lever moved \u2014 not just whether the number went up.",
16569
+ expert_read: 'Name the lever. A win-rate rise on falling opportunity volume often means qualification tightened, not that selling got "better." Needs a handful of dated wins; otherwise we leave it unavailable rather than invent a story.',
16522
16570
  deepdive: [
16523
16571
  "Needs 3 or more dated wins. Otherwise this metric is unavailable.",
16524
16572
  "Pairs with Flow Rate (cycle) and Win Rate (conversion)."
@@ -16540,11 +16588,11 @@ var init_metric_definitions = __esm({
16540
16588
  kind: "saas",
16541
16589
  label: "Win Rate",
16542
16590
  group: "Sales Efficiency",
16543
- tagline: "Of decided deals, how often do we win?",
16591
+ tagline: "Of the deals we decide, how often do we win?",
16544
16592
  how_computed: "closed-won / (won + lost) \xD7 100.",
16545
16593
  formula_lines: ["Win Rate = won / (won + lost) \xD7 100"],
16546
- meaning: "Board question: are we converting the pipeline we create? Priors: 25\u201335% SMB, 18\u201325% mid-market, 12\u201318% enterprise on qualified opportunities.",
16547
- expert_read: "A rising win rate on falling opportunity volume is qualification tightening, not improvement. Check the denominator.",
16594
+ meaning: "We read win rate as conversion of decided deals \u2014 are we winning the pipeline we create? Rough priors: ~25\u201335% SMB, ~18\u201325% mid-market, ~12\u201318% enterprise on qualified opportunities. It also sets how much coverage you need (~1 / win rate).",
16595
+ expert_read: "Watch the denominator. A rising win rate on falling opportunity volume often means qualification tightened, not that the team suddenly sells better. Cut by segment or source before company-wide coaching.",
16548
16596
  deepdive: [
16549
16597
  "Required coverage is about 1 / win rate.",
16550
16598
  "Cut by segment or source before company-wide coaching."
@@ -18759,6 +18807,170 @@ var init_strategist_flow = __esm({
18759
18807
  }
18760
18808
  });
18761
18809
 
18810
+ // src/conversation/ghost-hints.ts
18811
+ function ghostExamplesForPhase(phase, limit = 2) {
18812
+ const hints = PHASE_GHOST_HINTS[phase] ?? [];
18813
+ return hints.slice(0, limit).map((h) => h.replace(/^try\s+/i, ""));
18814
+ }
18815
+ var PHASE_GHOST_HINTS;
18816
+ var init_ghost_hints = __esm({
18817
+ "src/conversation/ghost-hints.ts"() {
18818
+ "use strict";
18819
+ PHASE_GHOST_HINTS = {
18820
+ orient: [
18821
+ 'try "pipeline health"',
18822
+ "try /deepdive",
18823
+ "try /deepdive guide",
18824
+ 'try "is our retention real for the board?"',
18825
+ 'try "what is the most expensive problem to solve?"',
18826
+ 'try "board deck on Q3"'
18827
+ ],
18828
+ awaiting_data: [
18829
+ 'try "use demo data"',
18830
+ "try a CSV path",
18831
+ "try /ingest",
18832
+ 'try "go ahead"'
18833
+ ],
18834
+ explore: [
18835
+ 'try "what is the most expensive problem?"',
18836
+ 'try "how should we fix this?"',
18837
+ 'try "which segment is weakest?"',
18838
+ 'try "what is ARR?"',
18839
+ "try /deepdive freshness",
18840
+ 'try "ship a board deck"'
18841
+ ]
18842
+ };
18843
+ }
18844
+ });
18845
+
18846
+ // src/conversation/situation.ts
18847
+ function buildSituationalAwarenessBlock(ctx, opts = {}) {
18848
+ const phase = resolveConversationPhase(ctx);
18849
+ const channel = opts.channel ?? (phase === "think" ? "think" : "explore");
18850
+ const responseMode = opts.responseMode ?? (channel === "think" ? "deep" : defaultExploreResponseMode(ctx, Math.floor(ctx.messages.length / 2)));
18851
+ const analysisReady = isAnalysisReady(ctx);
18852
+ const action = resolveRecommendedAction(ctx);
18853
+ const examples = ghostExamplesForPhase(
18854
+ phase === "think" || phase === "strategize" || phase === "deliver" ? "explore" : phase,
18855
+ 2
18856
+ );
18857
+ const lines = [
18858
+ "WHERE YOU ARE IN NTRP:",
18859
+ FUNNEL_SPINE,
18860
+ `Phase: ${phase} (${formatPhaseLabel(phase)}) \u2014 ${PHASE_MEANING[phase]}`
18861
+ ];
18862
+ if (channel === "think") {
18863
+ lines.push("Channel: think \u2014 socratic partner; type done/cancel returns to \u203A");
18864
+ lines.push("Response mode: deep (tools on)");
18865
+ } else if (channel === "strategist") {
18866
+ lines.push("Channel: strategist \u2014 measurable plan engine owns sequencing");
18867
+ } else {
18868
+ lines.push(
18869
+ responseMode === "brief" ? "Response mode: brief (tools off; cite COMPLETED SESSION ANALYSIS; ~40\u201390 words)" : "Response mode: deep (tools on; new cuts and drill-downs)"
18870
+ );
18871
+ }
18872
+ if (ctx.scope) {
18873
+ const bits = [`Scope: ${ctx.scope.intent_summary}`, `lens=${ctx.scope.primary_lens}`];
18874
+ if (ctx.scope.audience) bits.push(`audience=${ctx.scope.audience}`);
18875
+ if (ctx.scope.time_horizon) bits.push(`horizon=${ctx.scope.time_horizon}`);
18876
+ lines.push(bits.join(" \xB7 "));
18877
+ if (ctx.scope.confirmed_at) lines.push(`Scope confirmed: ${ctx.scope.confirmed_at}`);
18878
+ }
18879
+ if (ctx.dataset?.label) {
18880
+ lines.push(`Dataset: ${ctx.dataset.label} \xB7 stage=${ctx.stage}`);
18881
+ } else if (sessionHasData(ctx)) {
18882
+ lines.push(`Dataset: loaded \xB7 stage=${ctx.stage}`);
18883
+ } else {
18884
+ lines.push(`Dataset: empty \xB7 stage=${ctx.stage}`);
18885
+ }
18886
+ if (ctx.gapAudit) {
18887
+ lines.push(`Can compute: ${ctx.gapAudit.can_compute}`);
18888
+ if (ctx.gapAudit.missing.length > 0) {
18889
+ lines.push(`Data gaps: ${ctx.gapAudit.missing.map((m) => m.label).join(", ")}`);
18890
+ }
18891
+ }
18892
+ if (analysisReady) {
18893
+ lines.push(
18894
+ "Already done: scope confirmed; compute ran; do not re-propose scope or re-run compute unless the user asks to restart"
18895
+ );
18896
+ lines.push(
18897
+ "Available now: answer from the artifact; suggest at most one of /strategy, /handoff, /deepdive <metric>, /thinkwithme, /playbook"
18898
+ );
18899
+ if (channel === "explore" && responseMode === "deep") {
18900
+ lines.push(
18901
+ "Tools: diagnostics + draft_strategy + draft_handoff (+ audit_data_gaps for re-check). Scope/compute orchestration is off."
18902
+ );
18903
+ }
18904
+ } else if (phase === "awaiting_data") {
18905
+ lines.push("Already done: scope confirmed; analysis not ready");
18906
+ lines.push(
18907
+ 'Available now: tell the user to paste a CSV path, type "use demo data", or \u23CE go ahead when can_compute \u2014 you cannot open files'
18908
+ );
18909
+ } else if (phase === "scope") {
18910
+ lines.push("Already done: intent proposed; waiting on confirm");
18911
+ lines.push("Available now: wait for CLI confirm (\u23CE yes) \u2014 do not invent a different question");
18912
+ } else if (phase === "orient") {
18913
+ lines.push("Already done: nothing locked yet");
18914
+ lines.push("Available now: help the user name a focus; CLI will propose scope from their words");
18915
+ } else if (phase === "think") {
18916
+ lines.push("Already done: analysis complete; think channel open");
18917
+ lines.push("Available now: socratic exploration; draft_strategy / draft_handoff when ready to graduate");
18918
+ }
18919
+ lines.push(
18920
+ `Armed user next step: ${action ? `${action.hint} (\u23CE submits "${action.submit}")` : "none"}`
18921
+ );
18922
+ lines.push(`Engine connected: ${canUseReplAi(ctx) ? "yes" : "no"}`);
18923
+ if (examples.length > 0) {
18924
+ lines.push(`User may try: ${examples.join(" \xB7 ")}`);
18925
+ }
18926
+ lines.push(
18927
+ "Your job in this harness: fill the judgment gap after formula compute \u2014 connect dollars, name the expensive problem, do not restate scorecards the user already saw"
18928
+ );
18929
+ lines.push(
18930
+ "Pre-explore funnel gates are handled by the CLI; you speak in explore/think/strategist unless tools explicitly allow a restart"
18931
+ );
18932
+ return lines.join("\n");
18933
+ }
18934
+ function buildSituationalRuntimeFacts(ctx, opts = {}) {
18935
+ const phase = resolveConversationPhase(ctx);
18936
+ const channel = opts.channel ?? (phase === "think" ? "think" : "explore");
18937
+ const responseMode = opts.responseMode ?? (channel === "think" ? "deep" : defaultExploreResponseMode(ctx, Math.floor(ctx.messages.length / 2)));
18938
+ return {
18939
+ phase,
18940
+ mode: responseMode,
18941
+ lens: ctx.scope?.primary_lens,
18942
+ stage: ctx.stage,
18943
+ can_compute: ctx.gapAudit?.can_compute === void 0 ? void 0 : String(ctx.gapAudit.can_compute),
18944
+ engine: canUseReplAi(ctx) ? "connected" : "none"
18945
+ };
18946
+ }
18947
+ function getConversationPhaseBlock(ctx, opts = {}) {
18948
+ return buildSituationalAwarenessBlock(ctx, opts);
18949
+ }
18950
+ var FUNNEL_SPINE, PHASE_MEANING;
18951
+ var init_situation = __esm({
18952
+ "src/conversation/situation.ts"() {
18953
+ "use strict";
18954
+ init_context2();
18955
+ init_explore_mode();
18956
+ init_repl_api();
18957
+ init_ghost_hints();
18958
+ init_recommended_action();
18959
+ init_phase();
18960
+ FUNNEL_SPINE = "Funnel: orient \u2192 scope \u2192 data \u2192 compute \u2192 explore | side doors: think, strategy, ship";
18961
+ PHASE_MEANING = {
18962
+ orient: "setup \u2014 open chat; user has not locked a scope yet",
18963
+ scope: "scope confirm \u2014 CLI owns yes/adjust; do not re-propose unless asked",
18964
+ awaiting_data: "data gate \u2014 load CSV/demo or compute; CLI owns the gate",
18965
+ compute: "compute in progress \u2014 wait",
18966
+ explore: "analysis complete \u2014 free-form Q&A on this session's artifact",
18967
+ think: "socratic think channel \u2014 return to \u203A via done/cancel",
18968
+ strategize: "strategist flow \u2014 objective confirm or plan drafting",
18969
+ deliver: "ship/handoff wizard \u2014 CLI owns write confirms"
18970
+ };
18971
+ }
18972
+ });
18973
+
18762
18974
  // src/services/think.ts
18763
18975
  var think_exports = {};
18764
18976
  __export(think_exports, {
@@ -18798,7 +19010,7 @@ async function runThinkTurn(input, ctx) {
18798
19010
  setAgentContext(ctx);
18799
19011
  try {
18800
19012
  const analysisBlock = buildAnalysisBlock(ctx);
18801
- const conversationBlock = getConversationPhaseBlock(ctx);
19013
+ const conversationBlock = getConversationPhaseBlock(ctx, { channel: "think", responseMode: "deep" });
18802
19014
  const bundle = await loadSessionAnalysisBundle();
18803
19015
  const sessionArtifact = hasAnyAnalysis(bundle) ? buildExploreContextBlock(bundle, ctx) : "";
18804
19016
  for await (const event of agenticFindings(snapshot, ctx.snapshot.divergences, {
@@ -18885,7 +19097,7 @@ var init_think = __esm({
18885
19097
  "use strict";
18886
19098
  init_spinner();
18887
19099
  init_context2();
18888
- init_phase();
19100
+ init_situation();
18889
19101
  init_agent_context();
18890
19102
  init_agentic_loop();
18891
19103
  init_thread();
@@ -18949,7 +19161,7 @@ function printChannelIntro(seed) {
18949
19161
  console.log(
18950
19162
  " " + chalk19.dim(
18951
19163
  "Socratic channel \u2014 explore, challenge assumptions, pull evidence. Type "
18952
- ) + chalk19.cyan("done") + chalk19.dim(" or ") + chalk19.cyan("cancel") + chalk19.dim(" to return to ask \u203A.")
19164
+ ) + chalk19.cyan("done") + chalk19.dim(" or ") + chalk19.cyan("cancel") + chalk19.dim(" to return to \u203A.")
18953
19165
  );
18954
19166
  if (seed) {
18955
19167
  console.log(" " + chalk19.dim("Seed: ") + seed);
@@ -19035,7 +19247,7 @@ function clearThinkFlow(ctx, reason) {
19035
19247
  console.log();
19036
19248
  console.log(
19037
19249
  " " + chalk19.dim(
19038
- reason === "done" ? "Think channel closed. Back to ask \u203A." : "Think session cancelled. Continue exploration."
19250
+ reason === "done" ? "Think channel closed. Back to \u203A." : "Think session cancelled. Continue exploration."
19039
19251
  )
19040
19252
  );
19041
19253
  console.log();
@@ -20117,10 +20329,10 @@ async function runNaturalLanguage(input, ctx) {
20117
20329
  setAgentContext(ctx);
20118
20330
  try {
20119
20331
  const analysisBlock = buildAnalysisBlock(ctx);
20120
- const conversationBlock = getConversationPhaseBlock(ctx);
20332
+ const responseMode = resolveExploreResponseMode(input, ctx, ctx.conversation.length);
20333
+ const conversationBlock = getConversationPhaseBlock(ctx, { responseMode });
20121
20334
  const bundle = await loadSessionAnalysisBundle();
20122
20335
  const sessionArtifact = hasAnyAnalysis(bundle) ? buildExploreContextBlock(bundle, ctx) : "";
20123
- const responseMode = resolveExploreResponseMode(input, ctx, ctx.conversation.length);
20124
20336
  for await (const event of agenticFindings(snapshot, ctx.snapshot.divergences, {
20125
20337
  mode: "fresh",
20126
20338
  userQuestion: input,
@@ -20218,6 +20430,7 @@ var init_nl = __esm({
20218
20430
  "use strict";
20219
20431
  init_spinner();
20220
20432
  init_context2();
20433
+ init_situation();
20221
20434
  init_phase();
20222
20435
  init_agent_context();
20223
20436
  init_orchestrator();
@@ -23291,7 +23504,7 @@ var init_scenario_fit = __esm({
23291
23504
  import { readFileSync as readFileSync19, writeFileSync as writeFileSync15, existsSync as existsSync22, mkdirSync as mkdirSync13, unlinkSync as unlinkSync3 } from "fs";
23292
23505
  import { homedir as homedir7 } from "os";
23293
23506
  import { join as join24 } from "path";
23294
- function ensureDir5() {
23507
+ function ensureDir6() {
23295
23508
  if (!existsSync22(NTRP_DIR4)) {
23296
23509
  mkdirSync13(NTRP_DIR4, { recursive: true });
23297
23510
  }
@@ -23308,7 +23521,7 @@ function loadCachedTaxonomy(profile) {
23308
23521
  }
23309
23522
  }
23310
23523
  function saveCachedTaxonomy(taxonomy) {
23311
- ensureDir5();
23524
+ ensureDir6();
23312
23525
  writeFileSync15(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
23313
23526
  }
23314
23527
  var NTRP_DIR4, TAXONOMY_PATH;
@@ -25445,12 +25658,12 @@ Cite numbers from here; call tools when you need a new cut or to pressure-test a
25445
25658
 
25446
25659
  ` : "";
25447
25660
  const conversationSection = opts.conversationBlock ? `
25448
- CONVERSATION STATE:
25449
25661
  ${opts.conversationBlock}
25450
25662
 
25451
25663
  ` : "";
25452
25664
  const scratchSection = buildScratchBlock(opts.thinkState);
25453
25665
  const socraticCraft = `HOW YOU THINK WITH THE USER (socratic partner \u2014 not a search box, not a strategist):
25666
+ - Pre-explore funnel gates are handled by the CLI; you speak in the think channel after analysis.
25454
25667
  - You are a thinking partner. Prefer sharp questions that unlock the user's idea over dumping answers.
25455
25668
  - Protect imagination: give "what if" space before the smell-test \u2014 do not kill creativity in the first sentence.
25456
25669
  - Steelman the user's position before you attack it; then deliver one hard pushback grounded in evidence when possible.
@@ -25484,7 +25697,7 @@ ${buildCommandCatalogBlock()}
25484
25697
  COMMAND SUGGESTION RULES:
25485
25698
  - Suggest at most ONE command when it adds capability beyond your answer (e.g. \`/strategy\`, \`/handoff\`).
25486
25699
  - Never claim a command was run. Never invent flags.
25487
- - Type \`done\` or \`cancel\` leaves the think channel back to ask \u203A.`;
25700
+ - Type \`done\` or \`cancel\` leaves the think channel back to \u203A.`;
25488
25701
  const stable = `You are a world-class GTM operating partner in a dedicated THINK WITH ME channel. The user wants collaborative exploration \u2014 imagination, pressure-testing, and evidence \u2014 not a slide deck and not a finished strategy plan.
25489
25702
 
25490
25703
  ${companyContextSection3()}${operatorSection3()}${socraticCraft}
@@ -25534,7 +25747,7 @@ ${SAFETY_BLOCK}`;
25534
25747
  const dynamic = [
25535
25748
  "SESSION STATE (current \u2014 changes as the session progresses):",
25536
25749
  ...dynamicSections,
25537
- buildRuntimeBlock()
25750
+ buildRuntimeBlock(opts.runtimeFacts ?? {})
25538
25751
  ].join("\n\n");
25539
25752
  return { stable, dynamic };
25540
25753
  }
@@ -25651,12 +25864,11 @@ The user already received the full report in the terminal. Answer follow-ups by
25651
25864
 
25652
25865
  ` : "";
25653
25866
  const conversationSection = conversationBlock ? `
25654
- CONVERSATION STATE:
25655
25867
  ${conversationBlock}
25656
- Do not run compute until audit_data_gaps reports can_compute. If the user needs to load data, tell them to paste a CSV path or type "use demo data". You cannot open files.
25657
25868
 
25658
25869
  ` : "";
25659
25870
  const conversationRules = `HOW A GREAT ANALYST CARRIES A CONVERSATION (read carefully \u2014 this is what separates you from a search box):
25871
+ - Pre-explore funnel gates (orient \u2192 scope \u2192 data \u2192 compute) are handled by the CLI. You speak after compute unless tools explicitly allow a restart.
25660
25872
  - You have continuity. The conversation so far is provided to you as prior messages. Read it before answering. You remember every angle you have already explored, every number you have already surfaced, and every recommendation you have already made.
25661
25873
  - Never repeat or re-recommend an analysis you have already presented in this conversation unless the user explicitly asks you to revisit, recall, or connect it. Repetition reads as forgetfulness and destroys trust.
25662
25874
  - Each answer should ADVANCE the discussion: go deeper, introduce a genuinely new angle, challenge an assumption, or synthesize across what you have already found. Move the thinking forward.
@@ -25665,6 +25877,9 @@ Do not run compute until audit_data_gaps reports can_compute. If the user needs
25665
25877
  - If you genuinely have nothing new to add on a topic, say so briefly and offer a different, higher-value direction \u2014 never pad with a recycled recommendation.`;
25666
25878
  const briefJob = `YOUR JOB (BRIEF MODE \u2014 default after analysis is complete):
25667
25879
  - Stay concise \u2014 roughly 40\u201390 words total, not a mini-report. Verdict first: your opening sentence is the headline the user should be able to repeat an hour later.
25880
+ - Lead with the single most expensive or most actionable implication from the completed session analysis \u2014 not a scorecard restatement.
25881
+ - Do not re-list all five vital signs unless the user asked for that inventory.
25882
+ - Prefer naming one playbook play or one next cut over a generic "want me to dig deeper?" close.
25668
25883
  - You are at 30,000 ft by default. If they ask for detail or a new data cut, that routes to deep mode automatically \u2014 don't cram the weeds in here.
25669
25884
  - Do NOT call tools unless the user explicitly asks for a new data cut (by rep, stage, list of deals, etc.). The completed analysis above is your source of truth.
25670
25885
  - Name a playbook play only when the user asks what to do or which action to take.
@@ -25784,7 +25999,7 @@ ${briefCommandSection}` : "";
25784
25999
  const dynamic = [
25785
26000
  "SESSION STATE (current \u2014 changes as the session progresses):",
25786
26001
  ...dynamicSections,
25787
- buildRuntimeBlock()
26002
+ buildRuntimeBlock(promptOptions.runtimeFacts ?? {})
25788
26003
  ].join("\n\n");
25789
26004
  return { stable, dynamic };
25790
26005
  }
@@ -25816,15 +26031,27 @@ async function* agenticFindings(computeResult, divergences, options) {
25816
26031
  analysisBlock: options.analysisBlock,
25817
26032
  conversationBlock: options.conversationBlock,
25818
26033
  sessionArtifact: options.sessionArtifact,
25819
- thinkState: options.ctx.thinkState
26034
+ thinkState: options.ctx.thinkState,
26035
+ runtimeFacts: buildSituationalRuntimeFacts(options.ctx, {
26036
+ channel: "think",
26037
+ responseMode: "deep"
26038
+ })
25820
26039
  }) : mode === "fresh" ? buildFreshNlSystemPrompt(
25821
26040
  options.sessionContext,
25822
26041
  options.memoryBlock,
25823
26042
  options.analysisBlock,
25824
26043
  options.conversationBlock,
25825
- { responseMode, sessionArtifact: options.sessionArtifact, experiment }
26044
+ {
26045
+ responseMode,
26046
+ sessionArtifact: options.sessionArtifact,
26047
+ experiment,
26048
+ runtimeFacts: buildSituationalRuntimeFacts(options.ctx, {
26049
+ responseMode
26050
+ })
26051
+ }
25826
26052
  ) : buildSystemPrompt2();
25827
- const tools2 = useTools ? mode === "think" ? buildThinkTools() : mode === "fresh" ? buildFreshNlTools() : buildInvestigationTools() : [];
26053
+ const analysisReady = isAnalysisReady(options.ctx);
26054
+ const tools2 = useTools ? mode === "think" ? buildThinkTools() : mode === "fresh" ? buildFreshNlTools({ analysisReady }) : buildInvestigationTools() : [];
25828
26055
  const toolCtx = { computeResult, divergences };
25829
26056
  if (options.includeMetrics) {
25830
26057
  try {
@@ -25993,6 +26220,8 @@ var init_agentic_loop = __esm({
25993
26220
  init_prompt_parts();
25994
26221
  init_think_prompt();
25995
26222
  init_prompt();
26223
+ init_context2();
26224
+ init_situation();
25996
26225
  MAX_ITERATIONS = 10;
25997
26226
  INVESTIGATION_EVIDENCE_NUDGE_AFTER = 5;
25998
26227
  BRIEF_MAX_TOKENS = 768;
@@ -26096,22 +26325,19 @@ async function loadReportData(segmentName) {
26096
26325
  let { health, segments, findings, entityCounts } = diagnosis;
26097
26326
  let scopedSegment = null;
26098
26327
  if (segmentName) {
26099
- const lower = segmentName.toLowerCase();
26100
- let match = segments.find((s) => s.segment.name.toLowerCase() === lower);
26101
- if (!match) {
26102
- const subs = segments.filter((s) => s.segment.name.toLowerCase().includes(lower));
26103
- if (subs.length === 1) match = subs[0];
26104
- else if (subs.length > 1) {
26105
- throw new NtrpError("ambiguous_segment", `"${segmentName}" matches multiple segments.`, 2 /* Usage */, {
26106
- matches: subs.map((s) => s.segment.name)
26107
- });
26108
- }
26328
+ const resolved = resolveSegmentByName(segmentName, segments);
26329
+ if (resolved.type === "ambiguous") {
26330
+ throw new NtrpError("ambiguous_segment", `"${segmentName}" matches multiple segments.`, 2 /* Usage */, {
26331
+ matches: resolved.matches.map((s) => s.segment.name)
26332
+ });
26109
26333
  }
26110
- if (!match) {
26334
+ if (resolved.type === "none") {
26111
26335
  throw new NtrpError("segment_not_found", `No segment matching "${segmentName}".`, 2 /* Usage */, {
26112
26336
  available_segments: segments.map((s) => s.segment.name)
26113
26337
  });
26114
26338
  }
26339
+ const match = resolved.segment;
26340
+ const lower = segmentName.toLowerCase();
26115
26341
  scopedSegment = match;
26116
26342
  health = match.result;
26117
26343
  segments = [match];
@@ -26137,6 +26363,7 @@ var init_report = __esm({
26137
26363
  init_errors2();
26138
26364
  init_types2();
26139
26365
  init_session_analysis();
26366
+ init_segments();
26140
26367
  }
26141
26368
  });
26142
26369
 
@@ -26187,7 +26414,7 @@ async function* streamAsk(question, ctx) {
26187
26414
  sessionContext: ctx.resumedSessionSummary,
26188
26415
  includeMetrics: true,
26189
26416
  analysisBlock: buildAnalysisBlock(ctx),
26190
- conversationBlock: getConversationPhaseBlock(ctx),
26417
+ conversationBlock: getConversationPhaseBlock(ctx, { responseMode }),
26191
26418
  sessionArtifact,
26192
26419
  responseMode,
26193
26420
  priorMessages: ctx.conversation,
@@ -26232,7 +26459,7 @@ var init_ask = __esm({
26232
26459
  init_divergence();
26233
26460
  init_repl_api();
26234
26461
  init_context2();
26235
- init_phase();
26462
+ init_situation();
26236
26463
  init_session_analysis();
26237
26464
  init_smoke_protocol();
26238
26465
  }