@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.
- package/dist/ai/findings-stream-smoke.js +185 -0
- package/dist/ai/findings-stream-smoke.js.map +1 -0
- package/dist/ai/guardrails-smoke.js +2143 -323
- package/dist/ai/guardrails-smoke.js.map +1 -1
- package/dist/conversation/loop-guard-smoke.js +20395 -9338
- package/dist/conversation/loop-guard-smoke.js.map +1 -1
- package/dist/demo/whimsy-smoke.js +5 -0
- package/dist/demo/whimsy-smoke.js.map +1 -1
- package/dist/index.js +8179 -7211
- package/dist/index.js.map +1 -1
- package/dist/investigation/quality-eval-cli.js +21742 -0
- package/dist/investigation/quality-eval-cli.js.map +1 -0
- package/dist/investigation/verbosity-cli.js +1977 -329
- package/dist/investigation/verbosity-cli.js.map +1 -1
- package/dist/mcp/server.js +6286 -5191
- package/dist/mcp/server.js.map +1 -1
- package/dist/services/transcript-smoke.js +1 -0
- package/dist/services/transcript-smoke.js.map +1 -1
- package/dist/strategist/strategist-smoke.js +233 -10
- package/dist/strategist/strategist-smoke.js.map +1 -1
- package/dist/whimsy/time-bank-smoke.js +4910 -3329
- package/dist/whimsy/time-bank-smoke.js.map +1 -1
- package/package.json +3 -1
|
@@ -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)
|
|
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,
|
|
1347
|
+
function num(value, fallback2) {
|
|
1254
1348
|
const n = typeof value === "number" ? value : Number(value);
|
|
1255
|
-
return Number.isFinite(n) ? n :
|
|
1349
|
+
return Number.isFinite(n) ? n : fallback2;
|
|
1256
1350
|
}
|
|
1257
|
-
function enumValue(value, allowed,
|
|
1258
|
-
return typeof value === "string" && allowed.includes(value) ? value :
|
|
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
|
|
1351
|
-
if (!trigger || !
|
|
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);
|