@sonnechasser/ntrp 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,14 @@ var __esm = (fn, res) => function __init() {
5
5
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
6
  };
7
7
 
8
+ // src/ui/spinner.ts
9
+ import ora from "ora";
10
+ var init_spinner = __esm({
11
+ "src/ui/spinner.ts"() {
12
+ "use strict";
13
+ }
14
+ });
15
+
8
16
  // src/ai/llm/thread-compat.ts
9
17
  var init_thread_compat = __esm({
10
18
  "src/ai/llm/thread-compat.ts"() {
@@ -61,7 +69,9 @@ import { existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync3,
61
69
  import { homedir } from "os";
62
70
  import { randomUUID } from "crypto";
63
71
  function isAnalysisReady(ctx) {
64
- if (ctx.stage !== "analyzed" || ctx.analysis.completed.length === 0) return false;
72
+ if (ctx.stage !== "analyzed" && ctx.stage !== "delivered" || ctx.analysis.completed.length === 0) {
73
+ return false;
74
+ }
65
75
  if (!ctx.dataset) return false;
66
76
  const counts = ctx.dataset.counts ?? {};
67
77
  return Object.values(counts).some((n) => n > 0);
@@ -141,10 +151,37 @@ var init_queries = __esm({
141
151
 
142
152
  // src/ui/theme.ts
143
153
  import chalk from "chalk";
154
+ var STATUS, TOKENS, BADGE_TONE_COLORS;
144
155
  var init_theme = __esm({
145
156
  "src/ui/theme.ts"() {
146
157
  "use strict";
147
158
  init_formatters();
159
+ STATUS = {
160
+ green: "#22c55e",
161
+ yellow: "#eab308",
162
+ red: "#ef4444",
163
+ neutral: "#64748b"
164
+ };
165
+ TOKENS = {
166
+ accent: "#14b8a6",
167
+ accentBright: "#2dd4bf",
168
+ border: "#334155",
169
+ borderMuted: "#1e293b",
170
+ dim: "#64748b",
171
+ text: "#e2e8f0",
172
+ info: "#3b82f6",
173
+ ...STATUS,
174
+ success: STATUS.green,
175
+ warning: STATUS.yellow,
176
+ error: STATUS.red
177
+ };
178
+ BADGE_TONE_COLORS = {
179
+ success: TOKENS.success,
180
+ warning: TOKENS.warning,
181
+ error: TOKENS.error,
182
+ info: TOKENS.info,
183
+ accent: TOKENS.accent
184
+ };
148
185
  }
149
186
  });
150
187
 
@@ -219,6 +256,7 @@ var init_repl_globals = __esm({
219
256
  // src/cli/prompts.ts
220
257
  import { createInterface } from "readline/promises";
221
258
  import { clearLine, cursorTo } from "readline";
259
+ import { StringDecoder } from "string_decoder";
222
260
  import chalk2 from "chalk";
223
261
  var init_prompts = __esm({
224
262
  "src/cli/prompts.ts"() {
@@ -596,6 +634,15 @@ var init_session_state = __esm({
596
634
  }
597
635
  });
598
636
 
637
+ // src/conversation/recommended-action.ts
638
+ var init_recommended_action = __esm({
639
+ "src/conversation/recommended-action.ts"() {
640
+ "use strict";
641
+ init_repl_api();
642
+ init_phase();
643
+ }
644
+ });
645
+
599
646
  // src/conversation/phase.ts
600
647
  import chalk3 from "chalk";
601
648
  function sessionHasData(ctx) {
@@ -605,7 +652,7 @@ function sessionHasData(ctx) {
605
652
  function resolveConversationPhase(ctx) {
606
653
  if (ctx.deliverIntent) return "deliver";
607
654
  if (ctx.computeInProgress) return "compute";
608
- if (ctx.strategistState && ctx.strategistState.step !== "awaiting_analysis") {
655
+ if (ctx.strategistState && ctx.strategistState.step !== "awaiting_analysis" && ctx.strategistState.step !== "awaiting_connect") {
609
656
  return "strategize";
610
657
  }
611
658
  if (isAnalysisReady(ctx)) return "explore";
@@ -625,6 +672,7 @@ var init_phase = __esm({
625
672
  init_explore_mode();
626
673
  init_session_state();
627
674
  init_theme();
675
+ init_recommended_action();
628
676
  }
629
677
  });
630
678
 
@@ -1190,6 +1238,12 @@ var init_strategist_prompt = __esm({
1190
1238
  });
1191
1239
 
1192
1240
  // src/ai/json-response.ts
1241
+ function stripJsonFences(text) {
1242
+ const trimmed = text.trim();
1243
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
1244
+ if (fenced) return fenced[1].trim();
1245
+ return trimmed.replace(/```(?:json)?\s*/gi, "").replace(/```/g, "").trim();
1246
+ }
1193
1247
  var init_json_response = __esm({
1194
1248
  "src/ai/json-response.ts"() {
1195
1249
  "use strict";
@@ -1197,6 +1251,46 @@ var init_json_response = __esm({
1197
1251
  });
1198
1252
 
1199
1253
  // src/ai/strategist-validate.ts
1254
+ function parseJsonObjectFromText(text) {
1255
+ const cleaned = stripJsonFences(text);
1256
+ const start = cleaned.indexOf("{");
1257
+ if (start === -1) return null;
1258
+ let depth = 0;
1259
+ let inString = false;
1260
+ let escaped = false;
1261
+ let end = -1;
1262
+ for (let i = start; i < cleaned.length; i++) {
1263
+ const ch = cleaned[i];
1264
+ if (escaped) {
1265
+ escaped = false;
1266
+ continue;
1267
+ }
1268
+ if (ch === "\\") {
1269
+ if (inString) escaped = true;
1270
+ continue;
1271
+ }
1272
+ if (ch === '"') {
1273
+ inString = !inString;
1274
+ continue;
1275
+ }
1276
+ if (inString) continue;
1277
+ if (ch === "{") depth++;
1278
+ else if (ch === "}") {
1279
+ depth--;
1280
+ if (depth === 0) {
1281
+ end = i;
1282
+ break;
1283
+ }
1284
+ }
1285
+ }
1286
+ if (end === -1) return null;
1287
+ try {
1288
+ const parsed = JSON.parse(cleaned.slice(start, end + 1));
1289
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
1290
+ } catch {
1291
+ return null;
1292
+ }
1293
+ }
1200
1294
  function extractNumbers(text) {
1201
1295
  const out = [];
1202
1296
  for (const match of text.matchAll(NUMBER_RE)) {
@@ -1250,12 +1344,12 @@ function strArray(value) {
1250
1344
  if (!Array.isArray(value)) return [];
1251
1345
  return value.map(str).filter(Boolean);
1252
1346
  }
1253
- function num(value, fallback) {
1347
+ function num(value, fallback2) {
1254
1348
  const n = typeof value === "number" ? value : Number(value);
1255
- return Number.isFinite(n) ? n : fallback;
1349
+ return Number.isFinite(n) ? n : fallback2;
1256
1350
  }
1257
- function enumValue(value, allowed, fallback) {
1258
- return typeof value === "string" && allowed.includes(value) ? value : fallback;
1351
+ function enumValue(value, allowed, fallback2) {
1352
+ return typeof value === "string" && allowed.includes(value) ? value : fallback2;
1259
1353
  }
1260
1354
  function hasNumber(text) {
1261
1355
  return extractNumbers(text).length > 0;
@@ -1347,12 +1441,12 @@ function validateContingency(raw, today2) {
1347
1441
  if (!raw || typeof raw !== "object") return null;
1348
1442
  const record = raw;
1349
1443
  const trigger = str(record.trigger);
1350
- const fallback = str(record.fallback);
1351
- if (!trigger || !fallback) return null;
1444
+ const fallback2 = str(record.fallback);
1445
+ if (!trigger || !fallback2) return null;
1352
1446
  return {
1353
1447
  trigger,
1354
1448
  trigger_check_date: normalizeDate(record.trigger_check_date, today2, 21).iso,
1355
- fallback
1449
+ fallback: fallback2
1356
1450
  };
1357
1451
  }
1358
1452
  function validPlayIds(value) {
@@ -1468,11 +1562,90 @@ function validateStrategistPlan(raw, opts) {
1468
1562
  };
1469
1563
  return { plan, issues, measurableTargets, totalTargets };
1470
1564
  }
1565
+ function buildGroundedFallbackPlan(input) {
1566
+ const today2 = parseIsoDate(input.todayIso) ?? /* @__PURE__ */ new Date();
1567
+ const triggered2 = matchTriggeredPlays(input.vitals, LAYERS);
1568
+ const issues = [
1569
+ "LLM plan JSON invalid \u2014 using grounded fallback from triggered plays and live vitals"
1570
+ ];
1571
+ const sources = triggered2.length > 0 ? triggered2.slice(0, 3) : input.vitals.slice().sort((a, b) => a.score - b.score).slice(0, 2).map((vital) => {
1572
+ const play = getPlaybook().find((p) => p.trigger_vital_sign === vital.vital_sign) ?? getPlaybook()[0];
1573
+ return { play, vital, layer: 1 };
1574
+ });
1575
+ const workstreams = sources.map(({ play, vital }, index) => {
1576
+ const score = Math.round(vital.score);
1577
+ const dollar = vital.dollar_value != null && vital.dollar_value > 0 ? `$${Math.round(vital.dollar_value).toLocaleString("en-US")}` : null;
1578
+ const baseline = dollar ?? String(score);
1579
+ const targetLow = vital.dollar_value != null && vital.dollar_value > 0 ? `$${Math.round(vital.dollar_value * 0.4).toLocaleString("en-US")}` : String(Math.min(100, score + 20));
1580
+ const targetHigh = vital.dollar_value != null && vital.dollar_value > 0 ? `$${Math.round(vital.dollar_value * 0.6).toLocaleString("en-US")}` : String(Math.min(100, score + 35));
1581
+ const checkDate = toIso(addDays(today2, 21 + index * 7));
1582
+ const outcome = {
1583
+ metric: vital.vital_sign,
1584
+ baseline,
1585
+ target_range: `${baseline} -> ${targetLow}-${targetHigh}`,
1586
+ check_date: checkDate,
1587
+ measured_by: `${vital.vital_sign} vital sign`
1588
+ };
1589
+ return {
1590
+ order: index + 1,
1591
+ title: play.name,
1592
+ problem: `${vital.vital_sign} score ${score} (${vital.status})${dollar ? ` \u2014 ${dollar} ${vital.dollar_label ?? ""}`.trimEnd() : ""}`,
1593
+ rationale: index === 0 ? "Layer-order first: clean or unblock the gating vital before downstream work" : "Next in dependency order after the prior workstream",
1594
+ play_ids: [play.id],
1595
+ actions: play.steps.slice(0, 3),
1596
+ effort_hours: 8 + index * 4,
1597
+ milestones: [
1598
+ {
1599
+ label: `Check ${vital.vital_sign} movement`,
1600
+ due: checkDate,
1601
+ verification: `${vital.vital_sign} score moves toward ${targetLow}-${targetHigh} (baseline ${baseline})`
1602
+ }
1603
+ ],
1604
+ deliverables: [
1605
+ {
1606
+ label: `${play.name} triage list`,
1607
+ kind: "artifact",
1608
+ due: toIso(addDays(today2, 7 + index * 7))
1609
+ }
1610
+ ],
1611
+ expected_outcome: outcome,
1612
+ leading_indicators: [],
1613
+ contingency: {
1614
+ trigger: `${vital.vital_sign} flat or worse at first check`,
1615
+ trigger_check_date: checkDate,
1616
+ fallback: "Descope to the single highest-dollar entity cohort and re-run /strategy review"
1617
+ }
1618
+ };
1619
+ });
1620
+ const gating = input.gatingVitalSign ?? sources[0]?.vital.vital_sign ?? "freshness";
1621
+ const varLabel = input.totalValueAtRisk != null && input.totalValueAtRisk > 0 ? `$${Math.round(input.totalValueAtRisk).toLocaleString("en-US")} at risk` : "material pipeline dollars at risk";
1622
+ const plan = {
1623
+ title: "Grounded recovery plan",
1624
+ objective: input.objective,
1625
+ summary_30k: `${gating} is the gating pressure (${varLabel}). This fallback sequences ${workstreams.length} playbook workstream(s) in layer order so data trust and handoffs unlock pipeline movement. Treat baselines as live vital readings; refine with /strategy after the first review.`,
1626
+ hypothesis: "If plays execute in layer order against the live vital scores, the objective metrics should move into the stated ranges within one review cycle.",
1627
+ target_segment: "Whole pipeline",
1628
+ priority: "high",
1629
+ review_cadence: "Weekly",
1630
+ confidence: 0.45,
1631
+ constraints: ["Generated without a validated LLM plan JSON \u2014 confirm capacity before staffing"],
1632
+ assumptions: ["Outcome ranges are heuristic halves/increments of live vitals, not model-authored forecasts"],
1633
+ risks: ["Fallback plans lack stress-test revisions \u2014 run /strategy once the engine emits valid JSON"],
1634
+ workstreams
1635
+ };
1636
+ return {
1637
+ plan,
1638
+ issues,
1639
+ measurableTargets: workstreams.length,
1640
+ totalTargets: workstreams.length
1641
+ };
1642
+ }
1471
1643
  var NUMBER_RE, SUFFIX_MULTIPLIER, INSTRUMENT_TOKENS, ISO_DATE_RE;
1472
1644
  var init_strategist_validate = __esm({
1473
1645
  "src/ai/strategist-validate.ts"() {
1474
1646
  "use strict";
1475
1647
  init_playbook();
1648
+ init_health_score();
1476
1649
  init_json_response();
1477
1650
  NUMBER_RE = /\$?\s*(\d[\d,]*\.?\d*)\s*(m|k|b|million|thousand|billion)?\b/gi;
1478
1651
  SUFFIX_MULTIPLIER = {
@@ -1550,6 +1723,20 @@ var init_strategist_validate = __esm({
1550
1723
  });
1551
1724
 
1552
1725
  // src/ai/strategist.ts
1726
+ function describePlanValidationFailure(text, evidenceText, todayIso) {
1727
+ const raw = parseJsonObjectFromText(text);
1728
+ if (!raw) {
1729
+ if (text.includes("{") && !text.trim().endsWith("}")) {
1730
+ return "JSON object appears truncated (increase brevity: \u22643 workstreams) or incomplete";
1731
+ }
1732
+ return "not parseable as a JSON object (prose or truncated output)";
1733
+ }
1734
+ const result = validateStrategistPlan(raw, { evidenceText, todayIso });
1735
+ if (!result) {
1736
+ return "JSON parsed but no usable workstreams remained after measurability checks (need \u22651 workstream with measurable outcome or dated milestone)";
1737
+ }
1738
+ return "unknown validation failure";
1739
+ }
1553
1740
  var init_strategist2 = __esm({
1554
1741
  "src/ai/strategist.ts"() {
1555
1742
  "use strict";
@@ -1565,6 +1752,7 @@ var init_strategist2 = __esm({
1565
1752
  init_thread();
1566
1753
  init_strategist_prompt();
1567
1754
  init_strategist_validate();
1755
+ init_strategist_prompt();
1568
1756
  }
1569
1757
  });
1570
1758
 
@@ -1693,7 +1881,6 @@ var init_time_bank = __esm({
1693
1881
  });
1694
1882
 
1695
1883
  // src/conversation/strategist-flow.ts
1696
- import ora from "ora";
1697
1884
  import chalk7 from "chalk";
1698
1885
  function isStrategistIntent(input) {
1699
1886
  const line = input.trim();
@@ -1709,6 +1896,7 @@ var STRATEGIST_INTENT_RE;
1709
1896
  var init_strategist_flow = __esm({
1710
1897
  "src/conversation/strategist-flow.ts"() {
1711
1898
  "use strict";
1899
+ init_spinner();
1712
1900
  init_context2();
1713
1901
  init_handoff_draft();
1714
1902
  init_repl_api();
@@ -1731,6 +1919,7 @@ init_strategist_flow();
1731
1919
  init_phase();
1732
1920
  init_context2();
1733
1921
  init_strategist_validate();
1922
+ init_strategist2();
1734
1923
  init_playbook();
1735
1924
  init_health_score();
1736
1925
  var failures = [];
@@ -1864,6 +2053,40 @@ assert(triggered.length >= 1, "keyless skeleton triggers at least one play");
1864
2053
  assert(triggered[0].layer === 1, "first triggered play respects LAYERS order (freshness first)");
1865
2054
  var nums = extractNumbers("$3.1M stale pipeline, 120 deals");
1866
2055
  assert(nums.some((n) => n >= 3e6), "extractNumbers parses $3.1M");
2056
+ assert(
2057
+ describePlanValidationFailure("not json at all", evidence, today).includes("not parseable"),
2058
+ "describePlanValidationFailure: prose \u2192 not parseable"
2059
+ );
2060
+ assert(
2061
+ describePlanValidationFailure('{"title":"x","workstreams":[]}', evidence, today).includes("no usable workstreams"),
2062
+ "describePlanValidationFailure: empty workstreams \u2192 demoted message"
2063
+ );
2064
+ assert(
2065
+ describePlanValidationFailure('{"title":"cut off", "workstreams": [{"title":', evidence, today).includes("truncated"),
2066
+ "describePlanValidationFailure: truncated JSON \u2192 truncated message"
2067
+ );
2068
+ var fallback = buildGroundedFallbackPlan({
2069
+ objective: "Cut stale pipeline in half before Q4",
2070
+ vitals: ["freshness", "flow_rate", "drop_rate", "signal_to_noise", "thread_depth"].map(
2071
+ (sign) => ({
2072
+ vital_sign: sign,
2073
+ score: sign === "freshness" ? 29 : sign === "flow_rate" ? 40 : 70,
2074
+ status: sign === "freshness" ? "red" : sign === "flow_rate" ? "red" : "yellow",
2075
+ dollar_value: sign === "freshness" ? 31e5 : sign === "flow_rate" ? 12e5 : null,
2076
+ dollar_label: sign === "freshness" ? "pipeline at risk" : sign === "flow_rate" ? "stuck in pipeline" : null
2077
+ })
2078
+ ),
2079
+ todayIso: today,
2080
+ gatingVitalSign: "freshness",
2081
+ totalValueAtRisk: 43e5
2082
+ });
2083
+ assert(fallback.plan.workstreams.length >= 1, "grounded fallback has workstreams");
2084
+ assert(fallback.plan.objective.includes("stale pipeline"), "grounded fallback keeps objective");
2085
+ assert(
2086
+ fallback.plan.workstreams.some((ws) => ws.play_ids.includes("clean-dead-pipeline")),
2087
+ "grounded fallback links freshness play"
2088
+ );
2089
+ assert(fallback.issues.some((i) => i.includes("grounded fallback")), "grounded fallback records issue");
1867
2090
  if (failures.length > 0) {
1868
2091
  console.error("FAIL strategist-smoke:");
1869
2092
  for (const f of failures) console.error(" -", f);