@sonnechasser/ntrp 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +1986 -1052
  2. package/dist/mcp/server.js +389 -59
  3. package/package.json +2 -1
@@ -8131,7 +8131,7 @@ function buildSessionFileSnapshot(ctx) {
8131
8131
  if (ctx.dataset) file.dataset = ctx.dataset;
8132
8132
  if (ctx.deliverables.length > 0) file.deliverables = ctx.deliverables;
8133
8133
  if (ctx.conversation.length > 0) file.thread = ctx.conversation;
8134
- if (ctx.thinkConversation.length > 0) file.think_thread = ctx.thinkConversation;
8134
+ if ((ctx.thinkConversation?.length ?? 0) > 0) file.think_thread = ctx.thinkConversation;
8135
8135
  if (ctx.resumedFromId) file.resumed_from = ctx.resumedFromId;
8136
8136
  if (ctx.analysis) file.analysis = ctx.analysis;
8137
8137
  if (ctx.scope) file.scope = ctx.scope;
@@ -8470,7 +8470,7 @@ async function finalizeSession(ctx, stage) {
8470
8470
  if (ctx.conversation.length > 0) {
8471
8471
  file.thread = ctx.conversation;
8472
8472
  }
8473
- if (ctx.thinkConversation.length > 0) {
8473
+ if ((ctx.thinkConversation?.length ?? 0) > 0) {
8474
8474
  file.think_thread = ctx.thinkConversation;
8475
8475
  }
8476
8476
  if (ctx.analysis) {
@@ -10449,6 +10449,7 @@ handler: ../commands/config.ts
10449
10449
  Manage CLI configuration stored at \`~/.ntrp/config.json\`. Useful keys:
10450
10450
  \`api-key\` (Anthropic), \`openai-api-key\` (and \`groq-api-key\`, \`google-api-key\`, ...),
10451
10451
  \`llm-primary\` (default provider), \`llm-tier\`, \`llm-auto-failover\`,
10452
+ \`voice-personality\`, \`voice-roast\`,
10452
10453
  \`default-format\`, \`export-dir\`, \`ai-inbox-dir\` (or type \`/inbox set\`).
10453
10454
 
10454
10455
  Setting a provider key opens a hidden prompt and auto-discovers that provider's models.
@@ -10481,6 +10482,21 @@ handler: ../commands/tier.ts
10481
10482
  Set the quality and cost tier for this session. Agentic surfaces respect the tier.
10482
10483
  Some single-shot surfaces keep fixed defaults. Type \`/tier list\` to highlight the active stack.
10483
10484
  Add \`--default\` to persist to config.`
10485
+ },
10486
+ {
10487
+ name: "voice",
10488
+ raw: `---
10489
+ name: voice
10490
+ description: Set personality and roast (how NTRP talks)
10491
+ section: Settings
10492
+ args: [list|personality <level>|roast <level>|<personality> <roast>]
10493
+ handler: ../commands/voice.ts
10494
+ ---
10495
+
10496
+ Set how NTRP talks. Personality is robotic, composed, casual, or loose.
10497
+ Roast is light, medium, dark, or heavy. Communication only \u2014 scores and dollars do not change.
10498
+ Type \`/voice\` to pick from a numbered list (Enter keeps the current setting). Type \`/voice list\` to print the catalog. Type \`/voice casual heavy\` to set both.
10499
+ This writes to config immediately.`
10484
10500
  },
10485
10501
  {
10486
10502
  name: "model",
@@ -10648,7 +10664,7 @@ function formatCatalogLine(meta) {
10648
10664
  const note = DESTRUCTIVE_COMMAND_NOTES[meta.name] ? ` (${DESTRUCTIVE_COMMAND_NOTES[meta.name]})` : "";
10649
10665
  return `- /${meta.name}${args} \u2014 ${meta.description}${note}`;
10650
10666
  }
10651
- var ANALYST_FILE_NAME, ANALYST_FILE_MAX_CHARS, EXECUTION_BIAS_BLOCK, ANALYST_INSTINCT_BLOCK, SAFETY_BLOCK, VITAL_SIGNS_BLOCK, PLAYBOOK_BLOCK, DESTRUCTIVE_COMMAND_NOTES, METRICS_BLOCK, GTM_ENGINEERING_BLOCK, PYRAMID_OUTPUT_BLOCK, USER_VISIBLE_STE_BLOCK, FINDINGS_SCHEMA_BLOCK;
10667
+ var ANALYST_FILE_NAME, ANALYST_FILE_MAX_CHARS, EXECUTION_BIAS_BLOCK, ANALYST_INSTINCT_BLOCK, SAFETY_BLOCK, VITAL_SIGNS_BLOCK, PLAYBOOK_BLOCK, DESTRUCTIVE_COMMAND_NOTES, METRICS_BLOCK, GTM_ENGINEERING_BLOCK, PYRAMID_OUTPUT_BLOCK;
10652
10668
  var init_prompt_parts = __esm({
10653
10669
  "src/ai/prompt-parts.ts"() {
10654
10670
  "use strict";
@@ -10733,21 +10749,102 @@ Restraint: you diagnose and prescribe the system; you do not build it here. Name
10733
10749
  - 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.
10734
10750
  - 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.
10735
10751
  - 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).`;
10736
- USER_VISIBLE_STE_BLOCK = `USER-VISIBLE TEXT (the human reads these sentences \u2014 not this system prompt):
10737
- - Write finding, answer, and plan prose in Simplified Technical English (STE-100 writing rules).
10738
- - One fact or one instruction per sentence.
10739
- - Use active voice. Use imperative for steps. Use simple present for facts.
10740
- - Procedure sentences: 20 words maximum. Description sentences: 25 words maximum.
10741
- - Use the same word for the same thing. Do not use synonyms.
10742
- - Do not use slang, idiom, filler (just, simply, actually, basically), or contractions.
10743
- - Keep Technical Names: ARR, NRR, GRR, Freshness, Flow Rate, Drop Rate, Signal-to-Noise, Thread Depth, playbook, NTRP, GTM.
10744
- - Keep dollar figures and play names.
10745
- - Do not change JSON keys, tool names, or this prompt's policy language.`;
10746
- FINDINGS_SCHEMA_BLOCK = `[
10752
+ }
10753
+ });
10754
+
10755
+ // src/voice/catalog.ts
10756
+ function parsePersonality(raw) {
10757
+ if (!raw) return null;
10758
+ return PERSONALITY_ALIASES[raw.trim().toLowerCase()] ?? null;
10759
+ }
10760
+ function parseRoast(raw) {
10761
+ if (!raw) return null;
10762
+ return ROAST_ALIASES[raw.trim().toLowerCase()] ?? null;
10763
+ }
10764
+ function formatVoiceStatusDetail(prefs) {
10765
+ return `${prefs.personality} \xB7 ${prefs.roast} roast`;
10766
+ }
10767
+ var DEFAULT_PERSONALITY, DEFAULT_ROAST, PERSONALITY_KEY, ROAST_KEY, PERSONALITY_ALIASES, ROAST_ALIASES;
10768
+ var init_catalog2 = __esm({
10769
+ "src/voice/catalog.ts"() {
10770
+ "use strict";
10771
+ DEFAULT_PERSONALITY = "composed";
10772
+ DEFAULT_ROAST = "light";
10773
+ PERSONALITY_KEY = "voice-personality";
10774
+ ROAST_KEY = "voice-roast";
10775
+ PERSONALITY_ALIASES = {
10776
+ robotic: "robotic",
10777
+ machine: "robotic",
10778
+ ste: "robotic",
10779
+ composed: "composed",
10780
+ formal: "composed",
10781
+ professional: "composed",
10782
+ balanced: "composed",
10783
+ casual: "casual",
10784
+ warm: "casual",
10785
+ loose: "loose",
10786
+ vibe: "loose",
10787
+ relaxed: "loose"
10788
+ };
10789
+ ROAST_ALIASES = {
10790
+ light: "light",
10791
+ gentle: "light",
10792
+ soft: "light",
10793
+ medium: "medium",
10794
+ mid: "medium",
10795
+ dark: "dark",
10796
+ blunt: "dark",
10797
+ surgical: "dark",
10798
+ heavy: "heavy",
10799
+ burnt: "heavy",
10800
+ extra: "heavy",
10801
+ max: "heavy"
10802
+ };
10803
+ }
10804
+ });
10805
+
10806
+ // src/voice/prefs.ts
10807
+ function loadVoicePrefs() {
10808
+ return {
10809
+ personality: parsePersonality(getConfigValue(PERSONALITY_KEY)) ?? DEFAULT_PERSONALITY,
10810
+ roast: parseRoast(getConfigValue(ROAST_KEY)) ?? DEFAULT_ROAST
10811
+ };
10812
+ }
10813
+ var init_prefs = __esm({
10814
+ "src/voice/prefs.ts"() {
10815
+ "use strict";
10816
+ init_store();
10817
+ init_catalog2();
10818
+ }
10819
+ });
10820
+
10821
+ // src/voice/prompt.ts
10822
+ function buildUserVisibleProseBlock(prefs) {
10823
+ const voice2 = prefs ?? loadVoicePrefs();
10824
+ if (voice2.personality === "robotic") {
10825
+ return `${ROBOTIC_STE_BLOCK}
10826
+ VOICE (communication only \u2014 never change the numbers):
10827
+ - Personality: robotic.
10828
+ ${ROAST_RULES[voice2.roast]}`;
10829
+ }
10830
+ return `USER-VISIBLE TEXT (the human reads these sentences \u2014 not this system prompt):
10831
+ ${PERSONALITY_RULES[voice2.personality]}
10832
+ ${ROAST_RULES[voice2.roast]}
10833
+ ${SHARED_INVARIANTS}`;
10834
+ }
10835
+ function buildFindingsFindingHint(prefs) {
10836
+ const voice2 = prefs ?? loadVoicePrefs();
10837
+ const register = voice2.personality === "robotic" ? "STE-100 sentences. No contractions, slang, or filler." : voice2.personality === "composed" ? "Composed consultant sentences. Light contractions OK." : voice2.personality === "casual" ? "Casual consultant sentences. Warm, still short." : "Loose consultant sentences. Personality in the wrap, not the math.";
10838
+ const roast = voice2.roast === "light" ? "Light roast: what is working, then what to improve." : voice2.roast === "medium" ? "Medium roast: name the miss. No padding." : voice2.roast === "dark" ? "Dark roast: surgical. Verdict + dollar in one breath." : "Heavy roast: maximum roast after the number, never instead of it.";
10839
+ return `Pyramid-shaped, 2-3 sentences: (1) HEADLINE \u2014 verdict + dollar figure in one short sentence (\u226420 words); (2) EVIDENCE \u2014 the one or two numbers that prove it; (3) SO-WHAT \u2014 the consequence or the action. ${register} ${roast} Keep dollar figures and play names. Do not invent numbers. An executive should be able to repeat sentence 1 from memory.`;
10840
+ }
10841
+ function buildFindingsSchemaBlock(prefs) {
10842
+ const finding = buildFindingsFindingHint(prefs);
10843
+ return `[
10747
10844
  {
10748
10845
  "severity": "critical" | "warning" | "info",
10749
10846
  "segment": "segment name or 'Overall'",
10750
- "finding": "Pyramid-shaped, 2-3 STE-100 sentences: (1) HEADLINE \u2014 verdict + dollar figure in one short sentence (\u226420 words); (2) EVIDENCE \u2014 the one or two numbers that prove it; (3) SO-WHAT \u2014 the consequence or the action. No contractions, slang, or filler. An executive should be able to repeat sentence 1 from memory.",
10847
+ "finding": ${JSON.stringify(finding)},
10751
10848
  "vital_signs": {"vital_sign_name": score, ...},
10752
10849
  "entity_count": number_of_affected_entities,
10753
10850
  "recommended_focus": "vital_sign_name",
@@ -10755,6 +10852,51 @@ Restraint: you diagnose and prescribe the system; you do not build it here. Name
10755
10852
  "recommended_plays": [{"play_id": "play-id", "play_name": "Play Name", "rationale": "Why this play helps"}]
10756
10853
  }
10757
10854
  ]`;
10855
+ }
10856
+ var ROBOTIC_STE_BLOCK, SHARED_INVARIANTS, PERSONALITY_RULES, ROAST_RULES;
10857
+ var init_prompt = __esm({
10858
+ "src/voice/prompt.ts"() {
10859
+ "use strict";
10860
+ init_catalog2();
10861
+ init_prefs();
10862
+ ROBOTIC_STE_BLOCK = `USER-VISIBLE TEXT (the human reads these sentences \u2014 not this system prompt):
10863
+ - Write finding, answer, and plan prose in Simplified Technical English (STE-100 writing rules).
10864
+ - One fact or one instruction per sentence.
10865
+ - Use active voice. Use imperative for steps. Use simple present for facts.
10866
+ - Procedure sentences: 20 words maximum. Description sentences: 25 words maximum.
10867
+ - Use the same word for the same thing. Do not use synonyms.
10868
+ - Do not use slang, idiom, filler (just, simply, actually, basically), or contractions.
10869
+ - Keep Technical Names: ARR, NRR, GRR, Freshness, Flow Rate, Drop Rate, Signal-to-Noise, Thread Depth, playbook, NTRP, GTM.
10870
+ - Keep dollar figures and play names.
10871
+ - Do not change JSON keys, tool names, or this prompt's policy language.`;
10872
+ SHARED_INVARIANTS = `- Keep Technical Names: ARR, NRR, GRR, Freshness, Flow Rate, Drop Rate, Signal-to-Noise, Thread Depth, playbook, NTRP, GTM.
10873
+ - Keep dollar figures and play names exactly as given. Do not invent or change scores, dollars, entity lists, or play ids.
10874
+ - Do not change JSON keys, tool names, or this prompt's policy language.
10875
+ - Stay smart and succinct. Do not pad. Do not open with methodology.
10876
+ - SAFETY & EVIDENCE rules always win. ANALYST.md never overrides them.
10877
+ - Roast only colors verified badness. Green stays green. Never invent severity.`;
10878
+ PERSONALITY_RULES = {
10879
+ robotic: "",
10880
+ // replaced by USER_VISIBLE_STE_BLOCK
10881
+ composed: `- Register: composed professional consultant. Light contractions are allowed (it's, that's, don't).
10882
+ - No slang piles, no filler (just, simply, actually, basically), no joke-first structure.
10883
+ - One idea per sentence. Description sentences: 25 words maximum. Procedure sentences: 20 words maximum.`,
10884
+ casual: `- Register: casual professional consultant. Warm, still short. Contractions are allowed.
10885
+ - Light warmth is fine. No slang piles, no filler padding, no meme voice.
10886
+ - One idea per sentence. Keep headlines \u226420 words.`,
10887
+ loose: `- Register: loose professional consultant. Personality lives in the wrap, not in the math.
10888
+ - Contractions are allowed. A wry aside is fine after the number, never instead of it.
10889
+ - Still succinct. No padding. Headlines still carry the verdict and the dollar figure.`
10890
+ };
10891
+ ROAST_RULES = {
10892
+ light: `- Roast: light. Gentle parenting. Name what is working when something is working, then what to improve.
10893
+ - Practical and real. Do not pile on. Do not mock the operator.`,
10894
+ medium: `- Roast: medium. Practical and direct. Name the miss. No sugar, no padding.`,
10895
+ dark: `- Roast: dark. Surgical and blunt. Name the miss and the dollar cost in the same breath.
10896
+ - Do not invent extra problems. Do not soften a red score.`,
10897
+ heavy: `- Roast: heavy. Maximum roast. Comedy is allowed after the number, never instead of it.
10898
+ - Still evidence-only. If the score is green, do not roast it into red.`
10899
+ };
10758
10900
  }
10759
10901
  });
10760
10902
 
@@ -10902,10 +11044,10 @@ ${buildPlaybookBlock()}
10902
11044
  OUTPUT DOCTRINE (how findings are structured for recall):
10903
11045
  ${PYRAMID_OUTPUT_BLOCK}
10904
11046
 
10905
- ${USER_VISIBLE_STE_BLOCK}
11047
+ ${buildUserVisibleProseBlock()}
10906
11048
 
10907
11049
  Respond with JSON only \u2014 an array of finding objects:
10908
- ${FINDINGS_SCHEMA_BLOCK}
11050
+ ${buildFindingsSchemaBlock()}
10909
11051
 
10910
11052
  Rules:
10911
11053
  - Maximum 8 findings
@@ -11009,6 +11151,7 @@ var init_findings = __esm({
11009
11151
  init_repl_api();
11010
11152
  init_failover();
11011
11153
  init_prompt_parts();
11154
+ init_prompt();
11012
11155
  init_json_response();
11013
11156
  init_json_stream();
11014
11157
  init_playbook();
@@ -13007,6 +13150,15 @@ var init_layout = __esm({
13007
13150
  }
13008
13151
  });
13009
13152
 
13153
+ // src/whimsy/home-taglines.ts
13154
+ var HOME_TAGLINE_WHIMSY_CHANCE;
13155
+ var init_home_taglines = __esm({
13156
+ "src/whimsy/home-taglines.ts"() {
13157
+ "use strict";
13158
+ HOME_TAGLINE_WHIMSY_CHANCE = 1 / 8;
13159
+ }
13160
+ });
13161
+
13010
13162
  // src/ui/banner.ts
13011
13163
  import chalk4 from "chalk";
13012
13164
  var init_banner = __esm({
@@ -13014,6 +13166,8 @@ var init_banner = __esm({
13014
13166
  "use strict";
13015
13167
  init_theme();
13016
13168
  init_layout();
13169
+ init_home_taglines();
13170
+ init_home_taglines();
13017
13171
  }
13018
13172
  });
13019
13173
 
@@ -13356,8 +13510,8 @@ function resolveAskMultiInput(raw, options) {
13356
13510
  function resolveChooseInput(raw, choices, defaultValue) {
13357
13511
  if (choices.length === 0) return null;
13358
13512
  const defaultIdx = resolveChooseDefaultIndex(choices, defaultValue);
13359
- const pick2 = raw.trim() || String(defaultIdx + 1);
13360
- const n = Number(pick2);
13513
+ const pick3 = raw.trim() || String(defaultIdx + 1);
13514
+ const n = Number(pick3);
13361
13515
  if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
13362
13516
  return choices[n - 1].value;
13363
13517
  }
@@ -14114,14 +14268,14 @@ ${ANALYST_INSTINCT_BLOCK}
14114
14268
  OUTPUT DOCTRINE (how findings are structured for recall):
14115
14269
  ${PYRAMID_OUTPUT_BLOCK}
14116
14270
 
14117
- ${USER_VISIBLE_STE_BLOCK}
14271
+ ${buildUserVisibleProseBlock()}
14118
14272
 
14119
14273
  Respond with JSON only \u2014 an array of finding objects:
14120
14274
  [
14121
14275
  {
14122
14276
  "severity": "critical" | "warning" | "info",
14123
14277
  "segment": "Overall",
14124
- "finding": "Pyramid-shaped, 2-3 STE-100 sentences: (1) HEADLINE \u2014 verdict + number (\u226420 words); (2) EVIDENCE \u2014 the figures that prove it, citing confidence when below 80; (3) SO-WHAT \u2014 the consequence or action. No contractions, slang, or filler.",
14278
+ "finding": ${JSON.stringify(buildFindingsFindingHint())},
14125
14279
  "vital_signs": {},
14126
14280
  "entity_count": 0,
14127
14281
  "recommended_focus": "nrr",
@@ -14199,6 +14353,7 @@ var init_metrics_findings = __esm({
14199
14353
  init_repl_api();
14200
14354
  init_failover();
14201
14355
  init_prompt_parts();
14356
+ init_prompt();
14202
14357
  init_playbook();
14203
14358
  init_metrics_benchmarks();
14204
14359
  init_profile();
@@ -14566,6 +14721,158 @@ var init_slides = __esm({
14566
14721
  }
14567
14722
  });
14568
14723
 
14724
+ // src/voice/copy.ts
14725
+ function voice(prefs) {
14726
+ return prefs ?? loadVoicePrefs();
14727
+ }
14728
+ function voiceKeylessHeadline(input, prefs) {
14729
+ const v = voice(prefs);
14730
+ const { label, dollarBit, score, status } = input;
14731
+ if (dollarBit) {
14732
+ return pick2(v, {
14733
+ robotic: `${label} \u2014 ${dollarBit} \u2014 is the most expensive problem to solve right now.`,
14734
+ composed: `${label} is the most expensive problem right now: ${dollarBit}.`,
14735
+ casual: `${label} is the expensive one \u2014 ${dollarBit}.`,
14736
+ loose: {
14737
+ light: `${label} is the expensive problem: ${dollarBit}.`,
14738
+ medium: `${label} is eating ${dollarBit}. Fix that first.`,
14739
+ dark: `${label} is eating ${dollarBit}. That is the miss.`,
14740
+ heavy: `${label} is eating ${dollarBit}. That is the fire.`
14741
+ }
14742
+ });
14743
+ }
14744
+ return pick2(v, {
14745
+ robotic: `${label} (score ${Math.round(score)}, ${status}) is the problem to fix first.`,
14746
+ composed: `${label} (score ${Math.round(score)}) is the problem to fix first.`,
14747
+ casual: `Start with ${label} \u2014 score ${Math.round(score)}, ${status}.`,
14748
+ loose: {
14749
+ light: `Start with ${label} (score ${Math.round(score)}).`,
14750
+ medium: `${label} at ${Math.round(score)} is the one to fix first.`,
14751
+ dark: `${label} at ${Math.round(score)} is the miss.`,
14752
+ heavy: `${label} at ${Math.round(score)} is the joke, and not the funny kind.`
14753
+ }
14754
+ });
14755
+ }
14756
+ function voiceKeylessGating(_label, _score, prefs) {
14757
+ const v = voice(prefs);
14758
+ return pick2(v, {
14759
+ robotic: `it bounds what you can trust downstream.`,
14760
+ composed: `it bounds what you can trust downstream.`,
14761
+ casual: `nothing downstream is trustworthy until this moves.`,
14762
+ loose: {
14763
+ light: `it caps what you can trust downstream.`,
14764
+ medium: `fix this before you trust anything downstream.`,
14765
+ dark: `everything downstream is a story until this moves.`,
14766
+ heavy: `everything downstream is fan fiction until this moves.`
14767
+ }
14768
+ });
14769
+ }
14770
+ function voiceKeylessGatingWrap(label, score, prefs) {
14771
+ const clause = voiceKeylessGating(label, score, prefs);
14772
+ return `(score ${Math.round(score)}) \u2014 ${clause}`;
14773
+ }
14774
+ function voiceKeylessAlsoLabel(prefs) {
14775
+ const v = voice(prefs);
14776
+ return pick2(v, {
14777
+ robotic: "Also on the board:",
14778
+ composed: "Also on the board:",
14779
+ casual: "Also in play:",
14780
+ loose: {
14781
+ light: "Also in play:",
14782
+ medium: "Also costing you:",
14783
+ dark: "Also on fire:",
14784
+ heavy: "Also embarrassing:"
14785
+ }
14786
+ });
14787
+ }
14788
+ function voiceKeylessNextLabel(prefs) {
14789
+ const v = voice(prefs);
14790
+ return pick2(v, {
14791
+ robotic: "Next after that: ",
14792
+ composed: "Next after that: ",
14793
+ casual: "Then: ",
14794
+ loose: "Then: "
14795
+ });
14796
+ }
14797
+ function voiceKeylessConnectCta(prefs) {
14798
+ const v = voice(prefs);
14799
+ return pick2(v, {
14800
+ robotic: `Press \u23CE to connect a key (/connect) for the why and the plan. NTRP will finish this question after you connect.`,
14801
+ composed: `Press \u23CE to connect a key (/connect) for the why and the plan. NTRP will finish this question after you connect.`,
14802
+ casual: `Press \u23CE or type /connect for the why and the plan. NTRP will finish this question after you connect.`,
14803
+ loose: `Press \u23CE or type /connect when you want the why. NTRP will finish this question after you connect.`
14804
+ });
14805
+ }
14806
+ function voiceGapReadyCta(prefs) {
14807
+ const v = voice(prefs);
14808
+ return pick2(v, {
14809
+ robotic: `Ready to compute. Press \u23CE or type "go ahead"`,
14810
+ composed: `Ready to compute. Press \u23CE or type "go ahead"`,
14811
+ casual: `Ready. Press \u23CE or type "go ahead"`,
14812
+ loose: `Ready. Press \u23CE or type "go ahead"`
14813
+ });
14814
+ }
14815
+ function voiceGapLoadCta(prefs) {
14816
+ const v = voice(prefs);
14817
+ return pick2(v, {
14818
+ robotic: `Load data. Paste a CSV path, or press \u23CE to use demo data`,
14819
+ composed: `Load data. Paste a CSV path, or press \u23CE to use demo data`,
14820
+ casual: `Load data \u2014 paste a CSV, or press \u23CE to use demo data`,
14821
+ loose: `No data yet. Paste a CSV, or press \u23CE to use demo data`
14822
+ });
14823
+ }
14824
+ function voiceHealthGloss(status, prefs) {
14825
+ const v = voice(prefs);
14826
+ if (status === "green") {
14827
+ return v.personality === "robotic" ? null : "holding";
14828
+ }
14829
+ if (status === "yellow") {
14830
+ return pick2(v, {
14831
+ robotic: "watch this",
14832
+ composed: "watch this",
14833
+ casual: "soft",
14834
+ loose: {
14835
+ light: "watch this",
14836
+ medium: "soft",
14837
+ dark: "thin ice",
14838
+ heavy: "limping"
14839
+ }
14840
+ });
14841
+ }
14842
+ return pick2(v, {
14843
+ robotic: "needs attention",
14844
+ composed: "needs attention",
14845
+ casual: "the leak",
14846
+ loose: {
14847
+ light: "needs attention",
14848
+ medium: "the leak",
14849
+ dark: "the miss",
14850
+ heavy: "this is the fire"
14851
+ }
14852
+ });
14853
+ }
14854
+ function voiceHandoffAddendum(prefs) {
14855
+ const v = voice(prefs);
14856
+ return [
14857
+ `Write in this voice: personality=${v.personality}, roast=${v.roast} (${formatVoiceStatusDetail(v)}).`,
14858
+ "Ground every recommendation in the specific numbers provided. Do not invent data.",
14859
+ v.personality === "robotic" ? "STE-100 for the prose you produce. No contractions." : "Stay smart and succinct. Personality in the wrap, not in the math.",
14860
+ v.roast === "light" ? "Light roast: name what is working, then what to improve." : v.roast === "medium" ? "Medium roast: name the miss. No padding." : v.roast === "dark" ? "Dark roast: surgical. Verdict and dollar in the same breath." : "Heavy roast: maximum roast after the number, never instead of it. Green stays green."
14861
+ ].join(" ");
14862
+ }
14863
+ function pick2(prefs, map) {
14864
+ const entry = map[prefs.personality];
14865
+ if (typeof entry === "string") return entry;
14866
+ return entry[prefs.roast];
14867
+ }
14868
+ var init_copy = __esm({
14869
+ "src/voice/copy.ts"() {
14870
+ "use strict";
14871
+ init_catalog2();
14872
+ init_prefs();
14873
+ }
14874
+ });
14875
+
14569
14876
  // src/output/terminal.ts
14570
14877
  import chalk13 from "chalk";
14571
14878
  import Table2 from "cli-table3";
@@ -14613,9 +14920,10 @@ function printVitalSignRow(vs) {
14613
14920
  function printHealthSummary(result, _pipelineMetrics) {
14614
14921
  const scoreStr = `${chalk13.bold(String(Math.round(result.overall_score)))}${chalk13.dim("/100")}`;
14615
14922
  const impact = result.total_value_at_risk != null && result.total_value_at_risk > 0 ? `${paint("success", formatCurrency(result.total_value_at_risk))} ${chalk13.dim("total at risk")}` : chalk13.dim("No dollar-weighted risk detected");
14923
+ const gloss = voiceHealthGloss(result.overall_status);
14616
14924
  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");
14617
14925
  printResultCard("Overall Health", [
14618
- `${chalk13.dim("Score")} ${scoreStr} ${statusBadge(result.overall_status)}`,
14926
+ `${chalk13.dim("Score")} ${scoreStr} ${statusBadge(result.overall_status)}${gloss ? chalk13.dim(` ${gloss}`) : ""}`,
14619
14927
  `${chalk13.dim("Held back by")} ${paint("accent", VITAL_SIGN_LABELS[result.gating_vital_sign])}`,
14620
14928
  `${chalk13.dim("Revenue")} ${impact}`,
14621
14929
  next
@@ -14627,8 +14935,9 @@ function printHealthSummary(result, _pipelineMetrics) {
14627
14935
  }
14628
14936
  function printHealthLine(result) {
14629
14937
  const score = `${chalk13.bold(String(Math.round(result.overall_score)))}${chalk13.dim("/100")}`;
14938
+ const gloss = voiceHealthGloss(result.overall_status);
14630
14939
  const parts = [
14631
- `${chalk13.dim("Health")} ${score} ${statusBadge(result.overall_status)}`,
14940
+ `${chalk13.dim("Health")} ${score} ${statusBadge(result.overall_status)}${gloss ? chalk13.dim(` ${gloss}`) : ""}`,
14632
14941
  `${chalk13.dim("held back by")} ${paint("accent", VITAL_SIGN_LABELS[result.gating_vital_sign])}`
14633
14942
  ];
14634
14943
  if (result.total_value_at_risk != null && result.total_value_at_risk > 0) {
@@ -14969,6 +15278,7 @@ var init_terminal = __esm({
14969
15278
  init_layout();
14970
15279
  init_llm_attribution();
14971
15280
  init_slides();
15281
+ init_copy();
14972
15282
  }
14973
15283
  });
14974
15284
 
@@ -16643,6 +16953,8 @@ function wrapForTarget(target, analysisBlock, conversationBlock, openQuestions,
16643
16953
  "",
16644
16954
  "Ground every recommendation in the specific numbers provided. Do not invent data.",
16645
16955
  "",
16956
+ voiceHandoffAddendum(),
16957
+ "",
16646
16958
  "---",
16647
16959
  "",
16648
16960
  analysisBlock,
@@ -16701,6 +17013,7 @@ var init_handoff_draft = __esm({
16701
17013
  init_profile();
16702
17014
  init_session_analysis();
16703
17015
  init_metric_explainers();
17016
+ init_copy();
16704
17017
  QUESTION_LEAD_RE = /^\s*(what|why|how|when|where|who|which|is|are|was|were|do|does|did|explain|tell me|help me understand)\b/i;
16705
17018
  SHIP_INTENT_RE = /\b(ship|export|deliver|write[- ]?up|board memo|action plan|turn (this|it|that) into|(draft|create|make|build|prepare|generate|send)\s+(me\s+)?(a\s+|the\s+)?hand[- ]?off|hand[- ]?off\s+(prompt|doc|document|plan))\b/i;
16706
17019
  }
@@ -17030,7 +17343,7 @@ ALTITUDE CONTRACT (the plan must work at every altitude a client reads it at):
17030
17343
  - 10,000 FT: each workstream's title + problem line is a delegation unit \u2014 a one-liner an owner could receive in Slack and know what they own, why it's theirs, and what number they move.
17031
17344
  - GROUND LEVEL: actions are Monday-morning prescriptive. You have built this before \u2014 read the linked play's full detail with get_play_detail and prescribe its known-good sequence adapted to THIS company's numbers and constraints. The first action of every workstream must be startable within 48 hours with no new tooling.
17032
17345
 
17033
- ${USER_VISIBLE_STE_BLOCK}
17346
+ ${buildUserVisibleProseBlock()}
17034
17347
 
17035
17348
  GTM ENGINEERING (plans install systems, not heroics):
17036
17349
  ${GTM_ENGINEERING_BLOCK}
@@ -17120,6 +17433,7 @@ var init_strategist_prompt = __esm({
17120
17433
  "src/ai/strategist-prompt.ts"() {
17121
17434
  "use strict";
17122
17435
  init_prompt_parts();
17436
+ init_prompt();
17123
17437
  STRATEGIST_PLAN_SCHEMA_BLOCK = `{
17124
17438
  "title": "Short plan name, e.g. 'Q4 Pipeline Recovery'",
17125
17439
  "objective": "The measurable destination, restated precisely",
@@ -18794,11 +19108,11 @@ function printGapCard(audit, opts = {}) {
18794
19108
  console.log();
18795
19109
  if (audit.can_compute) {
18796
19110
  console.log(
18797
- " " + chalk20.dim("Ready to compute. Press ") + chalk20.cyan("\u23CE") + chalk20.dim(" or type ") + chalk20.cyan('"go ahead"')
19111
+ " " + chalk20.dim(voiceGapReadyCta())
18798
19112
  );
18799
19113
  } else if (audit.missing.length > 0) {
18800
19114
  console.log(
18801
- " " + chalk20.dim("Load data. Paste a CSV path, or press ") + chalk20.cyan("\u23CE") + chalk20.dim(" to ") + chalk20.cyan("use demo data")
19115
+ " " + chalk20.dim(voiceGapLoadCta())
18802
19116
  );
18803
19117
  }
18804
19118
  console.log();
@@ -18806,6 +19120,7 @@ function printGapCard(audit, opts = {}) {
18806
19120
  var init_gap_card = __esm({
18807
19121
  "src/conversation/gap-card.ts"() {
18808
19122
  "use strict";
19123
+ init_copy();
18809
19124
  }
18810
19125
  });
18811
19126
 
@@ -18861,25 +19176,30 @@ async function tryKeylessAskAnswer(ctx, input, opts = {}) {
18861
19176
  if (!primary) return false;
18862
19177
  const label = VITAL_SIGN_LABELS[primary.vital_sign] ?? primary.vital_sign;
18863
19178
  const dollarBit = primary.dollar_value != null && primary.dollar_value > 0 ? `${formatCurrency(primary.dollar_value)} ${primary.dollar_label ?? ""}`.trim() : null;
18864
- 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.`;
19179
+ const headline = voiceKeylessHeadline({
19180
+ label,
19181
+ dollarBit,
19182
+ score: primary.score,
19183
+ status: primary.status
19184
+ });
18865
19185
  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);
18866
19186
  console.log();
18867
19187
  console.log(" " + chalk21.bold(headline));
18868
19188
  if (opts.fromResume) {
18869
19189
  if (runners.length > 0) {
18870
19190
  console.log(
18871
- " " + chalk21.dim("Next after that: ") + chalk21.dim(runners.map(formatRunnerBit).join(" \xB7 "))
19191
+ " " + chalk21.dim(voiceKeylessNextLabel()) + chalk21.dim(runners.map(formatRunnerBit).join(" \xB7 "))
18872
19192
  );
18873
19193
  }
18874
19194
  } else {
18875
19195
  console.log();
18876
19196
  if (gating && gating.vital_sign !== primary.vital_sign) {
18877
19197
  console.log(
18878
- " " + chalk21.dim("Gating vital: ") + paint("accent", VITAL_SIGN_LABELS[gating.vital_sign]) + chalk21.dim(` (score ${Math.round(gating.score)}) \u2014 it bounds what you can trust downstream.`)
19198
+ " " + chalk21.dim("Gating vital: ") + paint("accent", VITAL_SIGN_LABELS[gating.vital_sign]) + chalk21.dim(voiceKeylessGatingWrap(VITAL_SIGN_LABELS[gating.vital_sign], gating.score))
18879
19199
  );
18880
19200
  }
18881
19201
  if (runners.length > 0) {
18882
- console.log(" " + chalk21.dim("Also on the board:"));
19202
+ console.log(" " + chalk21.dim(voiceKeylessAlsoLabel()));
18883
19203
  for (const vs of runners) {
18884
19204
  console.log(" " + chalk21.dim("\xB7 ") + formatVitalLine(vs));
18885
19205
  }
@@ -18891,9 +19211,7 @@ async function tryKeylessAskAnswer(ctx, input, opts = {}) {
18891
19211
  }
18892
19212
  }
18893
19213
  console.log();
18894
- console.log(
18895
- " " + chalk21.dim("Press ") + paint("accent", "\u23CE") + chalk21.dim(" to connect a key (") + paint("accent", "/connect") + chalk21.dim(") for the why and the plan. NTRP will finish this question after you connect.")
18896
- );
19214
+ console.log(" " + chalk21.dim(voiceKeylessConnectCta()));
18897
19215
  console.log();
18898
19216
  if (!opts.fromResume) {
18899
19217
  recordMessage(ctx, "user", input);
@@ -18910,6 +19228,7 @@ var init_keyless_ask = __esm({
18910
19228
  init_formatters();
18911
19229
  init_theme();
18912
19230
  init_health_score();
19231
+ init_copy();
18913
19232
  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;
18914
19233
  }
18915
19234
  });
@@ -25138,7 +25457,7 @@ FORMATTING (your answer is rendered in a terminal via a small markdown renderer)
25138
25457
  - Prefer short tables (\u22643 columns, \u22645 rows, cells \u226430 chars).
25139
25458
  - End with a dim horizontal rule (---) followed by one italicized follow-up question or fork.
25140
25459
 
25141
- ${USER_VISIBLE_STE_BLOCK}
25460
+ ${buildUserVisibleProseBlock()}
25142
25461
  `;
25143
25462
  const commandSection = `PRESET COMMANDS (slash commands the user can type \u2014 you may SUGGEST these; you cannot run them):
25144
25463
  ${buildCommandCatalogBlock()}
@@ -25165,7 +25484,7 @@ ${GTM_ENGINEERING_BLOCK}
25165
25484
  OUTPUT DOCTRINE (how answers are structured for recall):
25166
25485
  ${PYRAMID_OUTPUT_BLOCK}
25167
25486
 
25168
- ${USER_VISIBLE_STE_BLOCK}
25487
+ ${buildUserVisibleProseBlock()}
25169
25488
 
25170
25489
  CONTEXT:
25171
25490
  The user's health scores and top divergences were included as JSON at the start of this conversation. Use them, the think-channel history, and SESSION STATE as background and as hints for which tools to call.
@@ -25204,10 +25523,15 @@ var init_think_prompt = __esm({
25204
25523
  "src/ai/think-prompt.ts"() {
25205
25524
  "use strict";
25206
25525
  init_prompt_parts();
25526
+ init_prompt();
25207
25527
  }
25208
25528
  });
25209
25529
 
25210
25530
  // src/ai/agentic-loop.ts
25531
+ function findingsJsonSchemaNudge() {
25532
+ return `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:
25533
+ ${buildFindingsSchemaBlock()}`;
25534
+ }
25211
25535
  function companyContextSection4() {
25212
25536
  const block = buildCompanyProfileBlock();
25213
25537
  if (!block) return "";
@@ -25252,7 +25576,7 @@ ${buildPlaybookBlock()}
25252
25576
  OUTPUT DOCTRINE (how findings are structured for recall):
25253
25577
  ${PYRAMID_OUTPUT_BLOCK}
25254
25578
 
25255
- ${USER_VISIBLE_STE_BLOCK}
25579
+ ${buildUserVisibleProseBlock()}
25256
25580
 
25257
25581
  RULES:
25258
25582
  - Build evidence BEFORE claiming a finding. Call at least 2 tools before emitting a finding.
@@ -25274,7 +25598,7 @@ FORMATTING (the \`finding\` field on each finding is rendered in a terminal via
25274
25598
  - Never place emoji inside a table cell or heading \u2014 they break column alignment.
25275
25599
 
25276
25600
  OUTPUT FORMAT \u2014 when you're ready to present findings, respond with ONLY a JSON array:
25277
- ${FINDINGS_SCHEMA_BLOCK}
25601
+ ${buildFindingsSchemaBlock()}
25278
25602
 
25279
25603
  Keep investigating until you have a clear picture. Don't guess \u2014 use the tools.`;
25280
25604
  return { stable, dynamic: buildRuntimeBlock() };
@@ -25357,7 +25681,7 @@ FORMATTING (your answer is rendered in a terminal via a small markdown renderer)
25357
25681
  - Use \`inline code\` for CRM field names, SQL snippets, or literal values.
25358
25682
  - End with a dim horizontal rule (---) followed by one italicized follow-up question.
25359
25683
 
25360
- ${USER_VISIBLE_STE_BLOCK}
25684
+ ${buildUserVisibleProseBlock()}
25361
25685
  ` : `
25362
25686
  FORMATTING (brief mode \u2014 scannable, not chunky):
25363
25687
  - Open with ONE direct sentence that answers the question. Then a blank line before any list.
@@ -25372,7 +25696,7 @@ FORMATTING (brief mode \u2014 scannable, not chunky):
25372
25696
  - No ### headings, no tables, no --- footer, no closing "want me to dig deeper?" question.
25373
25697
  - Single-point answers can stay one sentence \u2014 don't force bullets when one line is enough.
25374
25698
 
25375
- ${USER_VISIBLE_STE_BLOCK}
25699
+ ${buildUserVisibleProseBlock()}
25376
25700
  `;
25377
25701
  const commandStyleRule = responseMode === "brief" ? '- Style in brief mode: a suggestion is at most ONE short trailing line after your answer (e.g. "`/segment compare enterprise smb` gives the full side-by-side.") \u2014 no heading, no footer.' : "- Style: weave the suggestion naturally into your close \u2014 one sentence with the exact invocation in `inline code`.";
25378
25702
  const commandSection = `PRESET COMMANDS (slash commands the user can type at the prompt \u2014 you may SUGGEST these; you have no ability to run them):
@@ -25400,7 +25724,7 @@ ${GTM_ENGINEERING_BLOCK}
25400
25724
  OUTPUT DOCTRINE (how answers are structured for recall):
25401
25725
  ${PYRAMID_OUTPUT_BLOCK}
25402
25726
 
25403
- ${USER_VISIBLE_STE_BLOCK}
25727
+ ${buildUserVisibleProseBlock()}
25404
25728
 
25405
25729
  `;
25406
25730
  const referenceSection = responseMode === "brief" ? "" : `CONTEXT:
@@ -25544,7 +25868,7 @@ async function* agenticFindings(computeResult, divergences, options) {
25544
25868
  messages.push(retry.assistant_message);
25545
25869
  }
25546
25870
  if (findings2.length === 0) {
25547
- messages.push({ role: "user", content: FINDINGS_JSON_SCHEMA_NUDGE });
25871
+ messages.push({ role: "user", content: findingsJsonSchemaNudge() });
25548
25872
  const retry2 = await callLlm(messages, maxTokens, false);
25549
25873
  findings2 = parseFindings(retry2.text);
25550
25874
  messages.push(retry2.assistant_message);
@@ -25591,7 +25915,7 @@ async function* agenticFindings(computeResult, divergences, options) {
25591
25915
  let findings = parseFindings(final.text);
25592
25916
  messages.push(final.assistant_message);
25593
25917
  if (findings.length === 0) {
25594
- messages.push({ role: "user", content: FINDINGS_JSON_SCHEMA_NUDGE });
25918
+ messages.push({ role: "user", content: findingsJsonSchemaNudge() });
25595
25919
  final = await callLlm(messages, 4096, false);
25596
25920
  findings = parseFindings(final.text);
25597
25921
  messages.push(final.assistant_message);
@@ -25630,7 +25954,7 @@ Here is the current GTM health snapshot as background. Use it plus any tools you
25630
25954
  function parseFindings(text) {
25631
25955
  return (parseJsonArrayFromText(text) ?? []).map(withKnownRecommendedPlays);
25632
25956
  }
25633
- var MAX_ITERATIONS, INVESTIGATION_EVIDENCE_NUDGE_AFTER, BRIEF_MAX_TOKENS, DEEP_MAX_TOKENS, FINDINGS_JSON_NUDGE, FINDINGS_JSON_SCHEMA_NUDGE;
25957
+ var MAX_ITERATIONS, INVESTIGATION_EVIDENCE_NUDGE_AFTER, BRIEF_MAX_TOKENS, DEEP_MAX_TOKENS, FINDINGS_JSON_NUDGE;
25634
25958
  var init_agentic_loop = __esm({
25635
25959
  "src/ai/agentic-loop.ts"() {
25636
25960
  "use strict";
@@ -25649,13 +25973,12 @@ var init_agentic_loop = __esm({
25649
25973
  init_untrusted();
25650
25974
  init_prompt_parts();
25651
25975
  init_think_prompt();
25976
+ init_prompt();
25652
25977
  MAX_ITERATIONS = 10;
25653
25978
  INVESTIGATION_EVIDENCE_NUDGE_AFTER = 5;
25654
25979
  BRIEF_MAX_TOKENS = 768;
25655
25980
  DEEP_MAX_TOKENS = 4096;
25656
25981
  FINDINGS_JSON_NUDGE = "Please format your findings as the JSON array specified in your instructions. Respond with ONLY the JSON array, no other text.";
25657
- 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:
25658
- ${FINDINGS_SCHEMA_BLOCK}`;
25659
25982
  }
25660
25983
  });
25661
25984
 
@@ -25959,22 +26282,29 @@ var init_setup = __esm({
25959
26282
  import { existsSync as existsSync26, readFileSync as readFileSync22 } from "fs";
25960
26283
  import { dirname as dirname5, join as join26 } from "path";
25961
26284
  import { fileURLToPath } from "url";
26285
+ function readVersionFromPackageJson(packageJsonPath) {
26286
+ if (!existsSync26(packageJsonPath)) return null;
26287
+ try {
26288
+ const pkg = JSON.parse(readFileSync22(packageJsonPath, "utf-8"));
26289
+ if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
26290
+ } catch {
26291
+ }
26292
+ return null;
26293
+ }
26294
+ function readVersionNearEntry(entryPath) {
26295
+ const start = dirname5(entryPath);
26296
+ for (const rel of [join26(start, "..", "package.json"), join26(start, "../..", "package.json")]) {
26297
+ const version = readVersionFromPackageJson(rel);
26298
+ if (version) return version;
26299
+ }
26300
+ return null;
26301
+ }
26302
+ function readInstalledVersionFromDisk() {
26303
+ return readVersionNearEntry(fileURLToPath(import.meta.url));
26304
+ }
25962
26305
  function getInstalledVersion() {
25963
26306
  if (cachedVersion) return cachedVersion;
25964
- const start = dirname5(fileURLToPath(import.meta.url));
25965
- for (const rel of ["../package.json", "../../package.json"]) {
25966
- const path = join26(start, rel);
25967
- if (!existsSync26(path)) continue;
25968
- try {
25969
- const pkg = JSON.parse(readFileSync22(path, "utf-8"));
25970
- if (typeof pkg.version === "string" && pkg.version.length > 0) {
25971
- cachedVersion = pkg.version;
25972
- return cachedVersion;
25973
- }
25974
- } catch {
25975
- }
25976
- }
25977
- cachedVersion = "0.0.0";
26307
+ cachedVersion = readInstalledVersionFromDisk() ?? "0.0.0";
25978
26308
  return cachedVersion;
25979
26309
  }
25980
26310
  var cachedVersion;