@sonnechasser/ntrp 1.8.1 → 2.0.1

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/index.js CHANGED
@@ -288,6 +288,56 @@ var init_formatters = __esm({
288
288
  }
289
289
  });
290
290
 
291
+ // src/io/diagnostics.ts
292
+ import { renameSync } from "fs";
293
+ function isDiagnosticsEnabled() {
294
+ const raw = process.env.NTRP_DEBUG;
295
+ if (!raw) return false;
296
+ return raw !== "0" && raw.toLowerCase() !== "false";
297
+ }
298
+ function describeError(err) {
299
+ if (!(err instanceof Error)) return String(err);
300
+ const parts = [err.message || err.name];
301
+ let cause = err.cause;
302
+ const seen = /* @__PURE__ */ new Set([err]);
303
+ while (cause && !seen.has(cause)) {
304
+ seen.add(cause);
305
+ parts.push(cause instanceof Error ? cause.message || cause.name : String(cause));
306
+ cause = cause instanceof Error ? cause.cause : void 0;
307
+ }
308
+ return parts.join(": ");
309
+ }
310
+ function debugError(scope, err, note) {
311
+ if (!isDiagnosticsEnabled()) return;
312
+ const detail = describeError(err);
313
+ console.error(` [ntrp:debug] ${scope}${note ? ` (${note})` : ""}: ${detail}`);
314
+ if (err instanceof Error && err.stack) {
315
+ console.error(err.stack.split("\n").slice(1).join("\n"));
316
+ }
317
+ }
318
+ function warnOnce(key, message) {
319
+ if (warnedKeys.has(key)) return;
320
+ warnedKeys.add(key);
321
+ console.error(` ${message}`);
322
+ }
323
+ function quarantineCorruptFile(path, scope) {
324
+ const target = `${path}.corrupt-${Date.now()}`;
325
+ try {
326
+ renameSync(path, target);
327
+ return target;
328
+ } catch (err) {
329
+ debugError(scope, err, `could not quarantine ${path}`);
330
+ return null;
331
+ }
332
+ }
333
+ var warnedKeys;
334
+ var init_diagnostics = __esm({
335
+ "src/io/diagnostics.ts"() {
336
+ "use strict";
337
+ warnedKeys = /* @__PURE__ */ new Set();
338
+ }
339
+ });
340
+
291
341
  // src/config/store.ts
292
342
  var store_exports = {};
293
343
  __export(store_exports, {
@@ -318,7 +368,8 @@ function ntrpHome() {
318
368
  function chmodQuiet(path, mode) {
319
369
  try {
320
370
  chmodSync(path, mode);
321
- } catch {
371
+ } catch (err) {
372
+ debugError("config.chmod", err, path);
322
373
  }
323
374
  }
324
375
  function ensureDir() {
@@ -345,7 +396,13 @@ function loadConfig() {
345
396
  }
346
397
  try {
347
398
  cachedConfig = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
348
- } catch {
399
+ } catch (err) {
400
+ debugError("config.load", err, CONFIG_PATH);
401
+ const quarantined = quarantineCorruptFile(CONFIG_PATH, "config.load");
402
+ warnOnce(
403
+ "config-unreadable",
404
+ `Could not read ${CONFIG_PATH} (${describeError(err)}).` + (quarantined ? ` Kept a copy at ${quarantined}; starting from an empty config.` : " Starting from an empty config \u2014 the file will be overwritten on the next save.")
405
+ );
349
406
  cachedConfig = {};
350
407
  }
351
408
  return cachedConfig;
@@ -490,6 +547,7 @@ var NTRP_DIR, CONFIG_PATH, cachedConfig;
490
547
  var init_store = __esm({
491
548
  "src/config/store.ts"() {
492
549
  "use strict";
550
+ init_diagnostics();
493
551
  NTRP_DIR = process.env.NTRP_HOME ? resolve(process.env.NTRP_HOME) : join(homedir(), ".ntrp");
494
552
  CONFIG_PATH = join(NTRP_DIR, "config.json");
495
553
  cachedConfig = null;
@@ -500,14 +558,23 @@ var init_store = __esm({
500
558
  function stripAnsi(value) {
501
559
  return value.replace(ANSI_ANY, "").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
502
560
  }
561
+ function mask(value) {
562
+ return `${value.slice(0, 6)}\u2026[redacted]`;
563
+ }
564
+ function looksLikeSecretValue(value) {
565
+ return /\d/.test(value) || value.length >= 20;
566
+ }
503
567
  function redactSecrets(line) {
504
- let out = line;
568
+ let out = line.replace(
569
+ SECRET_ARGUMENT_RE,
570
+ (m, prefix, value) => looksLikeSecretValue(value) ? `${prefix}${mask(value)}` : m
571
+ );
505
572
  for (const pattern of SECRET_PATTERNS) {
506
- out = out.replace(pattern, (m) => `${m.slice(0, 6)}\u2026[redacted]`);
573
+ out = out.replace(pattern, mask);
507
574
  }
508
575
  return out;
509
576
  }
510
- var MAX_LINES_DEFAULT, DROP_CHUNK, ANSI_ANY, SECRET_PATTERNS, SCREEN_CLEAR_MARKER, TerminalCapture;
577
+ var MAX_LINES_DEFAULT, DROP_CHUNK, ANSI_ANY, SECRET_PATTERNS, SECRET_ARGUMENT_RE, SCREEN_CLEAR_MARKER, TerminalCapture;
511
578
  var init_terminal_capture = __esm({
512
579
  "src/services/terminal-capture.ts"() {
513
580
  "use strict";
@@ -532,9 +599,24 @@ var init_terminal_capture = __esm({
532
599
  // Fireworks
533
600
  /\bAIza[A-Za-z0-9_-]{10,}/g,
534
601
  // Google
602
+ /\btvly-[A-Za-z0-9_-]{8,}/g,
603
+ // Tavily web retrieval
604
+ /\bBSA[A-Za-z0-9_-]{20,}/g,
605
+ // Brave Search subscription token
606
+ /\bhf_[A-Za-z0-9]{16,}/g,
607
+ // Hugging Face
608
+ /\bnvapi-[A-Za-z0-9_-]{8,}/g,
609
+ // NVIDIA NIM
610
+ /\bgithub_pat_[A-Za-z0-9_]{20,}/g,
611
+ // GitHub fine-grained token
612
+ /\bgh[pousr]_[A-Za-z0-9]{20,}/g,
613
+ // GitHub classic tokens
614
+ /\bAKIA[0-9A-Z]{16}\b/g,
615
+ // AWS access key id
535
616
  /\bNTRP-[A-Z0-9][A-Z0-9-]{8,}/g
536
- // license keys
617
+ // dev/CI license keys
537
618
  ];
619
+ SECRET_ARGUMENT_RE = /((?:--(?:key|llm-key|license-key|api-key|token)|\bactivate|\bapi[\s_-]?key|\blicense[\s_-]?key|\bBearer|\bNTRP_SIGNING_SECRET)(?:\s*[=:]\s*|\s+))(\S{8,})/gi;
538
620
  SCREEN_CLEAR_MARKER = "\u2500\u2500 screen cleared \u2500\u2500";
539
621
  TerminalCapture = class {
540
622
  constructor(maxLines = MAX_LINES_DEFAULT) {
@@ -1228,7 +1310,7 @@ import {
1228
1310
  mkdirSync as mkdirSync4,
1229
1311
  readFileSync as readFileSync2,
1230
1312
  readdirSync,
1231
- renameSync,
1313
+ renameSync as renameSync2,
1232
1314
  rmSync,
1233
1315
  statSync,
1234
1316
  writeFileSync as writeFileSync3
@@ -1302,7 +1384,8 @@ function readManifestEvents(root = getExportsDir()) {
1302
1384
  if (!trimmed) continue;
1303
1385
  try {
1304
1386
  events.push(JSON.parse(trimmed));
1305
- } catch {
1387
+ } catch (err) {
1388
+ debugError("exports.manifest.read", err, "skipped corrupt line");
1306
1389
  }
1307
1390
  }
1308
1391
  return events;
@@ -1491,7 +1574,8 @@ function pruneInboxArchive(archiveDir, keep = INBOX_ARCHIVE_KEEP) {
1491
1574
  const p = join3(archiveDir, name);
1492
1575
  try {
1493
1576
  return { name, path: p, mtime: statSync(p).mtimeMs };
1494
- } catch {
1577
+ } catch (err) {
1578
+ debugError("exports.pruneArchive.stat", err, p);
1495
1579
  return null;
1496
1580
  }
1497
1581
  }).filter((e) => e != null).sort((a, b) => b.mtime - a.mtime);
@@ -1582,7 +1666,7 @@ function moveExport(idOrPath, destDir) {
1582
1666
  if (existsSync3(destPath)) {
1583
1667
  destPath = join3(destRoot, `${exportStamp()}-${name}`);
1584
1668
  }
1585
- renameSync(item.path, destPath);
1669
+ renameSync2(item.path, destPath);
1586
1670
  const previous = [...item.previous_paths ?? [], item.path];
1587
1671
  updateArchiveLatest(item.kind, destPath, ensureExportsLayout());
1588
1672
  const event = {
@@ -1636,6 +1720,7 @@ var init_exports_registry = __esm({
1636
1720
  init_path_safety();
1637
1721
  init_export_kinds();
1638
1722
  init_handoff_skill();
1723
+ init_diagnostics();
1639
1724
  init_export_kinds();
1640
1725
  KIND_DIRS = ["handoffs", "reports", "notes", "csv", "publish"];
1641
1726
  INBOX_ARCHIVE_KEEP = 20;
@@ -1810,7 +1895,8 @@ function buildSessionContextDoc(file, opts = {}) {
1810
1895
  } else {
1811
1896
  lines.push("- AI inbox: unset \u2014 `/inbox set <folder>` then paste `/inbox skill` into your agent");
1812
1897
  }
1813
- } catch {
1898
+ } catch (err) {
1899
+ debugError("contextDoc.exportCatalog", err);
1814
1900
  lines.push("- Export catalog unavailable.");
1815
1901
  }
1816
1902
  lines.push("");
@@ -1857,13 +1943,15 @@ function writeSessionContextDoc(ctx) {
1857
1943
  const file = buildSessionFileSnapshot(ctx);
1858
1944
  const doc = buildSessionContextDoc(file, { snapshot: ctx.snapshot.computeResult });
1859
1945
  writeFileSync4(contextDocPathForSession(ctx.sessionId), doc);
1860
- } catch {
1946
+ } catch (err) {
1947
+ debugError("contextDoc.write", err, ctx.sessionId);
1861
1948
  }
1862
1949
  }
1863
1950
  function writeContextDocForSessionFile(file, opts = {}) {
1864
1951
  try {
1865
1952
  writeFileSync4(contextDocPathForSession(file.id), buildSessionContextDoc(file, opts));
1866
- } catch {
1953
+ } catch (err) {
1954
+ debugError("contextDoc.writeForFile", err, file.id);
1867
1955
  }
1868
1956
  }
1869
1957
  var AGENT_EXCERPT_CHARS;
@@ -1875,6 +1963,7 @@ var init_context_doc = __esm({
1875
1963
  init_store();
1876
1964
  init_exports_registry();
1877
1965
  init_terminal_capture();
1966
+ init_diagnostics();
1878
1967
  AGENT_EXCERPT_CHARS = 400;
1879
1968
  }
1880
1969
  });
@@ -1923,7 +2012,8 @@ function discardSessionTranscript(sessionId) {
1923
2012
  }
1924
2013
  try {
1925
2014
  rmSync2(transcriptPathForSession(sessionId), { force: true });
1926
- } catch {
2015
+ } catch (err) {
2016
+ debugError("transcript.discard", err, sessionId);
1927
2017
  }
1928
2018
  }
1929
2019
  function pauseTranscriptCapture() {
@@ -1955,7 +2045,8 @@ function installTees() {
1955
2045
  state.capture.feed(text);
1956
2046
  scheduleFlush();
1957
2047
  }
1958
- } catch {
2048
+ } catch (err) {
2049
+ debugError("transcript.capture", err);
1959
2050
  }
1960
2051
  return original(chunk, encoding, callback);
1961
2052
  })
@@ -1979,7 +2070,8 @@ function createState(sessionId) {
1979
2070
  if (existsSync4(filePath)) {
1980
2071
  try {
1981
2072
  base = readFileSync3(filePath, "utf-8").trimEnd() + "\n";
1982
- } catch {
2073
+ } catch (err) {
2074
+ debugError("transcript.readExisting", err, filePath);
1983
2075
  base = "";
1984
2076
  }
1985
2077
  }
@@ -2039,7 +2131,8 @@ function flushNow(closedNote) {
2039
2131
  try {
2040
2132
  getSessionsDir();
2041
2133
  writeFileSync5(s.filePath, render(s, closedNote));
2042
- } catch {
2134
+ } catch (err) {
2135
+ debugError("transcript.flush", err, s.filePath);
2043
2136
  }
2044
2137
  }
2045
2138
  function scheduleFlush() {
@@ -2073,6 +2166,7 @@ var init_transcript = __esm({
2073
2166
  "use strict";
2074
2167
  init_context2();
2075
2168
  init_terminal_capture();
2169
+ init_diagnostics();
2076
2170
  FLUSH_THROTTLE_MS = 250;
2077
2171
  FLUSH_MAX_STALENESS_MS = 900;
2078
2172
  state = null;
@@ -2172,7 +2266,7 @@ async function discardConnection() {
2172
2266
  db = null;
2173
2267
  lastHealthCheckMs = 0;
2174
2268
  if (currentConn) {
2175
- await closeConnection(currentConn).catch(() => void 0);
2269
+ await closeConnection(currentConn).catch((err) => debugError("db.discard.closeConnection", err));
2176
2270
  }
2177
2271
  if (currentDb) {
2178
2272
  await new Promise((resolve10) => {
@@ -2248,6 +2342,7 @@ var init_connection = __esm({
2248
2342
  "src/db/connection.ts"() {
2249
2343
  "use strict";
2250
2344
  init_store();
2345
+ init_diagnostics();
2251
2346
  NTRP_DIR2 = process.env.NTRP_HOME ? resolve4(process.env.NTRP_HOME) : join5(process.env.HOME ?? "", ".ntrp");
2252
2347
  DEFAULT_DB_PATH = process.env.NTRP_DB_PATH ? resolve4(process.env.NTRP_DB_PATH) : join5(NTRP_DIR2, "ntrp.duckdb");
2253
2348
  DB_PATH_PINNED = !!process.env.NTRP_DB_PATH;
@@ -2344,7 +2439,7 @@ async function inTransaction(fn) {
2344
2439
  await run("COMMIT");
2345
2440
  return result;
2346
2441
  } catch (err) {
2347
- await run("ROLLBACK").catch(() => void 0);
2442
+ await run("ROLLBACK").catch((rollbackErr) => debugError("db.rollback", rollbackErr));
2348
2443
  throw err;
2349
2444
  }
2350
2445
  }
@@ -3053,6 +3148,7 @@ var init_queries = __esm({
3053
3148
  "src/db/queries.ts"() {
3054
3149
  "use strict";
3055
3150
  init_connection();
3151
+ init_diagnostics();
3056
3152
  init_formatters();
3057
3153
  init_connection();
3058
3154
  }
@@ -3424,7 +3520,7 @@ var init_install = __esm({
3424
3520
  });
3425
3521
 
3426
3522
  // src/config/progress-migrate.ts
3427
- import { existsSync as existsSync7, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "fs";
3523
+ import { existsSync as existsSync7, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync7 } from "fs";
3428
3524
  import { join as join7 } from "path";
3429
3525
  function legacyStatePath() {
3430
3526
  return join7(ntrpHome(), "state.json");
@@ -3455,11 +3551,13 @@ function migrateLegacyStateIfNeeded(installId) {
3455
3551
  };
3456
3552
  writeFileSync7(progressPath(), JSON.stringify(progress, null, 2) + "\n");
3457
3553
  try {
3458
- renameSync2(legacyPath, legacyStateBackupPath());
3459
- } catch {
3554
+ renameSync3(legacyPath, legacyStateBackupPath());
3555
+ } catch (err) {
3556
+ debugError("progress.migrate.backup", err, legacyPath);
3460
3557
  }
3461
3558
  return progress;
3462
- } catch {
3559
+ } catch (err) {
3560
+ debugError("progress.migrate", err);
3463
3561
  return null;
3464
3562
  }
3465
3563
  }
@@ -3467,6 +3565,7 @@ var init_progress_migrate = __esm({
3467
3565
  "src/config/progress-migrate.ts"() {
3468
3566
  "use strict";
3469
3567
  init_store();
3568
+ init_diagnostics();
3470
3569
  }
3471
3570
  });
3472
3571
 
@@ -4382,7 +4481,13 @@ function loadCustomProviders() {
4382
4481
  try {
4383
4482
  const parsed = JSON.parse(readFileSync7(path, "utf-8"));
4384
4483
  cachedEntries = Array.isArray(parsed.providers) ? parsed.providers : [];
4385
- } catch {
4484
+ } catch (err) {
4485
+ debugError("llm.customProviders.load", err, path);
4486
+ const quarantined = quarantineCorruptFile(path, "llm.customProviders.load");
4487
+ warnOnce(
4488
+ "providers-unreadable",
4489
+ `Could not read ${path} (${describeError(err)}).` + (quarantined ? ` Kept a copy at ${quarantined}; re-add custom endpoints with /connect --base-url.` : " Re-add custom endpoints with /connect --base-url.")
4490
+ );
4386
4491
  cachedEntries = [];
4387
4492
  }
4388
4493
  return cachedEntries;
@@ -4456,6 +4561,7 @@ var init_providers = __esm({
4456
4561
  "src/ai/llm/providers.ts"() {
4457
4562
  "use strict";
4458
4563
  init_store();
4564
+ init_diagnostics();
4459
4565
  BUILTIN_SPECS = [
4460
4566
  {
4461
4567
  id: "anthropic",
@@ -5192,7 +5298,8 @@ function loadFile() {
5192
5298
  try {
5193
5299
  const parsed = JSON.parse(readFileSync8(path, "utf-8"));
5194
5300
  cached = { version: 1, providers: parsed.providers ?? {} };
5195
- } catch {
5301
+ } catch (err) {
5302
+ debugError("llm.modelsCache.load", err, path);
5196
5303
  cached = { version: 1, providers: {} };
5197
5304
  }
5198
5305
  return cached;
@@ -5247,6 +5354,7 @@ var init_models_cache = __esm({
5247
5354
  "src/ai/llm/models-cache.ts"() {
5248
5355
  "use strict";
5249
5356
  init_store();
5357
+ init_diagnostics();
5250
5358
  CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
5251
5359
  cached = null;
5252
5360
  }
@@ -5288,14 +5396,14 @@ var init_catalog = __esm({
5288
5396
  init_models_cache();
5289
5397
  ENTRIES = [
5290
5398
  {
5291
- id: "claude-opus-4-6",
5399
+ id: "claude-fable-5",
5292
5400
  provider: "anthropic",
5293
5401
  tier: "high",
5294
5402
  status: "active",
5295
5403
  successor_id: null,
5296
5404
  supports_tools: true,
5297
- max_context_tokens: 2e5,
5298
- display_name: "Claude Opus 4.6",
5405
+ max_context_tokens: 1e6,
5406
+ display_name: "Claude Fable 5",
5299
5407
  relative_cost: 3
5300
5408
  },
5301
5409
  {
@@ -5321,36 +5429,36 @@ var init_catalog = __esm({
5321
5429
  relative_cost: 1
5322
5430
  },
5323
5431
  {
5324
- id: "gpt-4.1",
5432
+ id: "gpt-5",
5325
5433
  provider: "openai",
5326
5434
  tier: "high",
5327
5435
  status: "active",
5328
5436
  successor_id: null,
5329
5437
  supports_tools: true,
5330
5438
  max_context_tokens: 1047576,
5331
- display_name: "GPT-4.1",
5439
+ display_name: "GPT-5",
5332
5440
  relative_cost: 3
5333
5441
  },
5334
5442
  {
5335
- id: "gpt-4.1-mini",
5443
+ id: "gpt-5-mini",
5336
5444
  provider: "openai",
5337
5445
  tier: "medium",
5338
5446
  status: "active",
5339
5447
  successor_id: null,
5340
5448
  supports_tools: true,
5341
5449
  max_context_tokens: 1047576,
5342
- display_name: "GPT-4.1 Mini",
5450
+ display_name: "GPT-5 Mini",
5343
5451
  relative_cost: 2
5344
5452
  },
5345
5453
  {
5346
- id: "gpt-4.1-nano",
5454
+ id: "gpt-5-nano",
5347
5455
  provider: "openai",
5348
5456
  tier: "low",
5349
5457
  status: "active",
5350
5458
  successor_id: null,
5351
5459
  supports_tools: true,
5352
5460
  max_context_tokens: 1047576,
5353
- display_name: "GPT-4.1 Nano",
5461
+ display_name: "GPT-5 Nano",
5354
5462
  relative_cost: 1
5355
5463
  }
5356
5464
  ];
@@ -5370,9 +5478,11 @@ function fixtureResponse(url, headers) {
5370
5478
  if (entry.auth_includes && !headerValues.includes(entry.auth_includes)) continue;
5371
5479
  return { status: entry.status, ok: entry.status >= 200 && entry.status < 300, body: entry.body };
5372
5480
  }
5373
- } catch {
5481
+ } catch (err) {
5482
+ debugError("llm.http.fixture", err, process.env.NTRP_LLM_HTTP_FIXTURE);
5483
+ return { status: 0, ok: false, body: void 0, error: `fixture unreadable: ${describeError(err)}` };
5374
5484
  }
5375
- return { status: 0, ok: false, body: void 0 };
5485
+ return { status: 0, ok: false, body: void 0, error: `no fixture entry matched ${url}` };
5376
5486
  }
5377
5487
  async function llmHttpGetJson(url, headers, timeoutMs = 6e3) {
5378
5488
  if (process.env.NTRP_LLM_HTTP_FIXTURE) {
@@ -5385,12 +5495,20 @@ async function llmHttpGetJson(url, headers, timeoutMs = 6e3) {
5385
5495
  let body;
5386
5496
  try {
5387
5497
  body = await res.json();
5388
- } catch {
5498
+ } catch (err) {
5499
+ debugError("llm.http.parse", err, url);
5389
5500
  body = void 0;
5390
5501
  }
5391
5502
  return { status: res.status, ok: res.ok, body };
5392
- } catch {
5393
- return { status: 0, ok: false, body: void 0 };
5503
+ } catch (err) {
5504
+ debugError("llm.http.get", err, url);
5505
+ const aborted = err instanceof Error && err.name === "AbortError";
5506
+ return {
5507
+ status: 0,
5508
+ ok: false,
5509
+ body: void 0,
5510
+ error: aborted ? `timed out after ${timeoutMs}ms` : describeError(err)
5511
+ };
5394
5512
  } finally {
5395
5513
  clearTimeout(timer);
5396
5514
  }
@@ -5398,6 +5516,7 @@ async function llmHttpGetJson(url, headers, timeoutMs = 6e3) {
5398
5516
  var init_http = __esm({
5399
5517
  "src/ai/llm/http.ts"() {
5400
5518
  "use strict";
5519
+ init_diagnostics();
5401
5520
  }
5402
5521
  });
5403
5522
 
@@ -5416,86 +5535,157 @@ function extractVersion(id) {
5416
5535
  const match = id.match(/(\d+(?:\.\d+)?)/);
5417
5536
  return match ? Number(match[1]) : 0;
5418
5537
  }
5419
- function pickByPatterns(models, patterns) {
5420
- for (const pattern of patterns) {
5421
- const matches = models.filter((m) => pattern.test(m.id));
5422
- if (matches.length > 0) return [...matches].sort(compareModels)[0];
5423
- }
5424
- return void 0;
5425
- }
5426
- function genericBucket(model) {
5427
- if (GENERIC_HIGH.test(model.id)) return "high";
5428
- if (GENERIC_LOW.test(model.id)) return "low";
5538
+ function matchesAny(id, patterns) {
5539
+ return patterns.some((p) => p.test(id));
5540
+ }
5541
+ function toolsScore(model, noTools) {
5542
+ if (noTools.has(model.id)) return 0;
5543
+ if (model.supports_tools === true) return 2;
5544
+ if (model.supports_tools === false) return 0;
5545
+ return 1;
5546
+ }
5547
+ function compareCompatible(a, b, noTools = /* @__PURE__ */ new Set()) {
5548
+ const scoreA = toolsScore(a, noTools);
5549
+ const scoreB = toolsScore(b, noTools);
5550
+ if (scoreA !== scoreB) return scoreB - scoreA;
5551
+ return compareModels(a, b);
5552
+ }
5553
+ function bestOf(models, noTools) {
5554
+ if (models.length === 0) return void 0;
5555
+ return [...models].sort((a, b) => compareCompatible(a, b, noTools))[0];
5556
+ }
5557
+ function ladderFor(providerId) {
5558
+ return PROVIDER_LADDERS[providerId];
5559
+ }
5560
+ function classifyModel(providerId, model) {
5561
+ const id = model.id;
5562
+ const ladder = ladderFor(providerId);
5563
+ if (ladder) {
5564
+ if (matchesAny(id, ladder.high)) return "high";
5565
+ if (matchesAny(id, ladder.medium)) return "medium";
5566
+ if (matchesAny(id, ladder.low)) return "low";
5567
+ }
5568
+ if (GENERIC_LOW.test(id)) return "low";
5569
+ if (ladder && matchesAny(id, ladder.namespaces)) return "high";
5570
+ if (GENERIC_HIGH.test(id)) return "high";
5429
5571
  return "medium";
5430
5572
  }
5431
- function genericPick(models, tier) {
5432
- const bucket = models.filter((m) => genericBucket(m) === tier);
5433
- if (bucket.length > 0) return [...bucket].sort(compareModels)[0];
5434
- return void 0;
5435
- }
5436
- function rankModels(providerId, models) {
5573
+ function rankModels(providerId, models, opts = {}) {
5437
5574
  if (models.length === 0) return null;
5438
- const preferences = PROVIDER_PREFERENCES[providerId];
5439
- const picks = {};
5440
- for (const tier of ["high", "medium", "low"]) {
5441
- const preferred = preferences ? pickByPatterns(models, preferences[tier]) : void 0;
5442
- const generic = preferred ?? genericPick(models, tier);
5443
- if (generic) picks[tier] = generic.id;
5444
- }
5445
- const anyModel = [...models].sort(compareModels)[0].id;
5446
- const high = picks.high ?? picks.medium ?? picks.low ?? anyModel;
5447
- const medium = picks.medium ?? picks.high ?? picks.low ?? anyModel;
5448
- const low = picks.low ?? picks.medium ?? picks.high ?? anyModel;
5449
- return { high, medium, low };
5450
- }
5451
- var PROVIDER_PREFERENCES, GENERIC_LOW, GENERIC_HIGH;
5575
+ const noTools = new Set(opts.noTools ?? []);
5576
+ const ceiling = [];
5577
+ const mid = [];
5578
+ const floor = [];
5579
+ for (const model of models) {
5580
+ const band = classifyModel(providerId, model);
5581
+ if (band === "high") ceiling.push(model);
5582
+ else if (band === "low") floor.push(model);
5583
+ else mid.push(model);
5584
+ }
5585
+ const highPick = bestOf(ceiling, noTools) ?? bestOf(mid, noTools) ?? bestOf(floor, noTools) ?? bestOf(models, noTools);
5586
+ const used = /* @__PURE__ */ new Set([highPick.id]);
5587
+ const mediumPick = bestOf(
5588
+ mid.filter((m) => !used.has(m.id)),
5589
+ noTools
5590
+ ) ?? bestOf(
5591
+ ceiling.filter((m) => !used.has(m.id)),
5592
+ noTools
5593
+ ) ?? bestOf(
5594
+ floor.filter((m) => !used.has(m.id)),
5595
+ noTools
5596
+ ) ?? highPick;
5597
+ used.add(mediumPick.id);
5598
+ const lowPick = bestOf(
5599
+ floor.filter((m) => !used.has(m.id)),
5600
+ noTools
5601
+ ) ?? bestOf(
5602
+ mid.filter((m) => !used.has(m.id)),
5603
+ noTools
5604
+ ) ?? bestOf(
5605
+ ceiling.filter((m) => !used.has(m.id)),
5606
+ noTools
5607
+ ) ?? mediumPick;
5608
+ return { high: highPick.id, medium: mediumPick.id, low: lowPick.id };
5609
+ }
5610
+ var PROVIDER_LADDERS, GENERIC_LOW, GENERIC_HIGH;
5452
5611
  var init_ranking = __esm({
5453
5612
  "src/ai/llm/ranking.ts"() {
5454
5613
  "use strict";
5455
- PROVIDER_PREFERENCES = {
5614
+ PROVIDER_LADDERS = {
5456
5615
  anthropic: {
5457
- high: [/^claude-opus/i, /^claude-sonnet/i],
5458
- medium: [/^claude-sonnet/i, /^claude-haiku/i],
5459
- low: [/^claude-haiku/i, /^claude-sonnet/i]
5616
+ namespaces: [/^claude-/i],
5617
+ high: [/^claude-mythos/i, /^claude-fable/i, /^claude-opus/i],
5618
+ medium: [/^claude-sonnet/i],
5619
+ low: [/^claude-haiku/i]
5460
5620
  },
5461
5621
  openai: {
5462
- high: [/^gpt-5(?!.*(mini|nano|chat))/i, /^gpt-4\.1(?!.*(mini|nano))/i, /^gpt-4o(?!.*mini)/i, /^o3(?!.*mini)/i],
5463
- medium: [/^gpt-5.*mini/i, /^gpt-4\.1-mini/i, /^gpt-4o-mini/i, /^o4-mini/i],
5622
+ namespaces: [/^gpt-/i, /^o\d/i],
5623
+ high: [
5624
+ /^gpt-5(?!.*(mini|nano|chat))/i,
5625
+ /^gpt-4\.1(?!.*(mini|nano))/i,
5626
+ /^gpt-4o(?!.*mini)/i,
5627
+ /^o\d(?!.*mini)/i
5628
+ ],
5629
+ medium: [/^gpt-5.*mini/i, /^gpt-4\.1-mini/i, /^gpt-4o-mini/i, /^o\d.*mini/i],
5464
5630
  low: [/^gpt-5.*nano/i, /^gpt-4\.1-nano/i, /^gpt-4o-mini/i]
5465
5631
  },
5466
5632
  google: {
5467
- high: [/^gemini-[\d.]+-pro/i, /^gemini-[\d.]+-flash(?!-lite)/i],
5468
- medium: [/^gemini-[\d.]+-flash(?!-lite|-8b)/i, /^gemini-[\d.]+-pro/i],
5469
- low: [/^gemini-[\d.]+-flash-lite/i, /flash-8b/i, /^gemini-[\d.]+-flash(?!-lite)/i]
5633
+ namespaces: [/^gemini-/i],
5634
+ high: [/^gemini-[\d.]+-pro/i, /^gemini-[\d.]+-ultra/i],
5635
+ medium: [/^gemini-[\d.]+-flash(?!-lite|-8b)/i],
5636
+ low: [/^gemini-[\d.]+-flash-lite/i, /flash-8b/i]
5637
+ },
5638
+ xai: {
5639
+ namespaces: [/^grok-/i, /^grok(?!-)/i],
5640
+ high: [/^grok-\d+(?!.*(mini|fast))/i, /^grok(?!.*(mini|fast))/i],
5641
+ medium: [/^grok.*mini(?!.*fast)/i, /^grok.*fast/i],
5642
+ low: [/^grok.*mini.*fast/i, /^grok.*mini/i]
5470
5643
  },
5471
5644
  groq: {
5472
- high: [/llama-3\.3-70b/i, /gpt-oss-120b/i, /70b/i, /deepseek-r1/i],
5645
+ // Any hosted chat id is a peer — new OSS renames should still ceiling.
5646
+ namespaces: [/./],
5647
+ high: [/llama-3\.3-70b/i, /gpt-oss-120b/i, /70b/i, /405b/i, /deepseek-r1/i],
5473
5648
  medium: [/llama-3\.1-8b-instant/i, /gpt-oss-20b/i, /llama.*8b/i],
5474
5649
  low: [/8b-instant/i, /llama.*8b/i, /gemma/i]
5475
5650
  },
5476
5651
  deepseek: {
5477
- high: [/reasoner/i, /chat/i],
5652
+ namespaces: [/^deepseek-/i],
5653
+ high: [/reasoner/i],
5478
5654
  medium: [/chat/i],
5479
5655
  low: [/chat/i]
5480
5656
  },
5481
5657
  mistral: {
5482
- high: [/large/i, /medium/i],
5483
- medium: [/medium/i, /^mistral-small/i],
5658
+ namespaces: [/^mistral-/i, /^ministral-/i, /^codestral-/i, /^pixtral-/i, /^devstral-/i],
5659
+ high: [/large/i],
5660
+ medium: [/medium/i, /^mistral-small/i, /^codestral/i, /^pixtral/i, /^devstral/i],
5484
5661
  low: [/ministral/i, /small/i, /tiny/i]
5485
5662
  },
5486
- xai: {
5487
- high: [/^grok-\d+(?!.*(mini|fast))/i, /^grok(?!.*(mini|fast))/i],
5488
- medium: [/^grok.*mini(?!.*fast)/i, /^grok.*fast/i],
5489
- low: [/^grok.*mini.*fast/i, /^grok.*mini/i]
5490
- },
5491
5663
  openrouter: {
5492
- high: [/^openrouter\/auto$/i, /claude.*opus/i, /^openai\/gpt-5(?!.*(mini|nano))/i, /gemini.*pro/i],
5664
+ namespaces: [
5665
+ /^anthropic\//i,
5666
+ /^openai\//i,
5667
+ /^google\//i,
5668
+ /^x-ai\//i,
5669
+ /^meta-llama\//i,
5670
+ /^mistralai\//i,
5671
+ /^deepseek\//i,
5672
+ /^openrouter\//i,
5673
+ /^[^/]+\/[^/]+/
5674
+ ],
5675
+ high: [
5676
+ /^openrouter\/auto$/i,
5677
+ /claude.*(mythos|fable|opus)/i,
5678
+ /^openai\/gpt-5(?!.*(mini|nano))/i,
5679
+ /^openai\/o\d(?!.*mini)/i,
5680
+ /gemini.*pro/i,
5681
+ /gemini.*ultra/i
5682
+ ],
5493
5683
  medium: [/claude.*sonnet/i, /gpt-5.*mini/i, /gpt-4\.1-mini/i, /gemini.*flash(?!-lite)/i],
5494
5684
  low: [/claude.*haiku/i, /nano/i, /flash-lite/i, /mini/i]
5495
5685
  }
5496
5686
  };
5497
5687
  GENERIC_LOW = /(mini|nano|lite|tiny|micro|small|haiku|instant|flash|turbo|\b0?\.?5b\b|\b[1-8]b\b)/i;
5498
- GENERIC_HIGH = /(opus|ultra|large|max\b|\bpro\b|405b|253b|235b|120b|72b|70b|reason|-r1\b|think|deep)/i;
5688
+ GENERIC_HIGH = /(opus|fable|mythos|ultra|large|max\b|\bpro\b|405b|253b|235b|120b|72b|70b|reasoner|reason|-r1\b|think|deep)/i;
5499
5689
  }
5500
5690
  });
5501
5691
 
@@ -5540,7 +5730,9 @@ async function fetchProviderModels(spec, apiKey, timeoutMs = 6e3) {
5540
5730
  let url = modelsUrl(spec);
5541
5731
  for (let page = 0; page < 5 && url; page++) {
5542
5732
  const res2 = await llmHttpGetJson(url, headers, timeoutMs);
5543
- if (!res2.ok) return models2.length > 0 ? { ok: true, models: models2 } : { ok: false, status: res2.status };
5733
+ if (!res2.ok) {
5734
+ return models2.length > 0 ? { ok: true, models: models2 } : { ok: false, status: res2.status, ...res2.error ? { error: res2.error } : {} };
5735
+ }
5544
5736
  const body2 = res2.body;
5545
5737
  for (const item of body2?.data ?? []) {
5546
5738
  const model = normalizeItem(spec, item);
@@ -5551,7 +5743,7 @@ async function fetchProviderModels(spec, apiKey, timeoutMs = 6e3) {
5551
5743
  return { ok: true, models: models2 };
5552
5744
  }
5553
5745
  const res = await llmHttpGetJson(modelsUrl(spec), headers, timeoutMs);
5554
- if (!res.ok) return { ok: false, status: res.status };
5746
+ if (!res.ok) return { ok: false, status: res.status, ...res.error ? { error: res.error } : {} };
5555
5747
  const body = res.body;
5556
5748
  const list = Array.isArray(body) ? body : body?.data ?? [];
5557
5749
  const models = [];
@@ -5570,9 +5762,9 @@ function storeDiscoveredModels(providerId, rawModels) {
5570
5762
  if (!spec || rawModels.length === 0) return null;
5571
5763
  const chat = filterChatModels(spec, rawModels);
5572
5764
  const usable = chat.length > 0 ? chat : rawModels;
5573
- const stack = rankModels(providerId, usable);
5574
- if (!stack) return null;
5575
5765
  const prior = getProviderModels(providerId);
5766
+ const stack = rankModels(providerId, usable, { noTools: prior?.quirks?.no_tools });
5767
+ if (!stack) return null;
5576
5768
  const entry = {
5577
5769
  fetched_at: (/* @__PURE__ */ new Date()).toISOString(),
5578
5770
  models: usable,
@@ -5598,7 +5790,7 @@ function rerankExcluding(providerId, deadModelId) {
5598
5790
  const prior = getProviderModels(providerId);
5599
5791
  if (!prior) return null;
5600
5792
  const survivors = prior.models.filter((m) => m.id !== deadModelId);
5601
- const stack = rankModels(providerId, survivors);
5793
+ const stack = rankModels(providerId, survivors, { noTools: prior.quirks?.no_tools });
5602
5794
  if (!stack) return null;
5603
5795
  const entry = { ...prior, models: survivors, tier_stack: stack };
5604
5796
  setProviderModels(providerId, entry);
@@ -6397,7 +6589,13 @@ function loadProfile() {
6397
6589
  const parsed = JSON.parse(readFileSync11(PROFILE_PATH, "utf-8"));
6398
6590
  if (!parsed || typeof parsed !== "object") return null;
6399
6591
  return parsed;
6400
- } catch {
6592
+ } catch (err) {
6593
+ debugError("profile.load", err, PROFILE_PATH);
6594
+ const quarantined = quarantineCorruptFile(PROFILE_PATH, "profile.load");
6595
+ warnOnce(
6596
+ "profile-unreadable",
6597
+ `Could not read ${PROFILE_PATH} (${describeError(err)}).` + (quarantined ? ` Kept a copy at ${quarantined}; run /onboard to rebuild it.` : " Run /onboard to rebuild it.")
6598
+ );
6401
6599
  return null;
6402
6600
  }
6403
6601
  }
@@ -6435,6 +6633,7 @@ var init_profile = __esm({
6435
6633
  "src/config/profile.ts"() {
6436
6634
  "use strict";
6437
6635
  init_store();
6636
+ init_diagnostics();
6438
6637
  NTRP_DIR3 = ntrpHome();
6439
6638
  PROFILE_PATH = join12(NTRP_DIR3, "profile.json");
6440
6639
  }
@@ -6640,7 +6839,9 @@ async function resolveModelWithDiscovery(provider, cfg, opts) {
6640
6839
  const override = overrideForProvider(opts.modelOverride, provider, cfg.activeProvider);
6641
6840
  const direct = resolveModelSafe(provider, cfg.tier, override);
6642
6841
  if (direct) return direct;
6643
- await refreshProviderModels(provider, { apiKey: opts.apiKey, force: true }).catch(() => null);
6842
+ await refreshProviderModels(provider, { apiKey: opts.apiKey, force: true }).catch(
6843
+ (err) => debugError("llm.refreshProviderModels", err, provider)
6844
+ );
6644
6845
  return resolveModelSafe(provider, cfg.tier, override);
6645
6846
  }
6646
6847
  function stripTools(req) {
@@ -6908,6 +7109,7 @@ var init_failover = __esm({
6908
7109
  init_resolver();
6909
7110
  init_pseudonymize();
6910
7111
  init_lexicon_seed();
7112
+ init_diagnostics();
6911
7113
  NO_PROVIDER_MESSAGE = "No LLM provider configured. Run /connect and paste any API key (Anthropic, OpenAI, Groq, Gemini, ...).";
6912
7114
  }
6913
7115
  });
@@ -7002,7 +7204,8 @@ async function callProvider(texts) {
7002
7204
  const json = await res.json();
7003
7205
  if (!json.data) return null;
7004
7206
  return json.data.map((d) => d.embedding);
7005
- } catch {
7207
+ } catch (err) {
7208
+ debugError("ai.embeddings.fetch", err);
7006
7209
  return null;
7007
7210
  }
7008
7211
  }
@@ -7054,6 +7257,7 @@ var init_embeddings = __esm({
7054
7257
  init_llm_config();
7055
7258
  init_pseudonymize();
7056
7259
  init_lexicon_seed();
7260
+ init_diagnostics();
7057
7261
  VOYAGE_MODEL = "voyage-3";
7058
7262
  OPENAI_MODEL = "text-embedding-3-small";
7059
7263
  cache = /* @__PURE__ */ new Map();
@@ -7111,7 +7315,8 @@ async function rankByRelevance(query, items, topK) {
7111
7315
  if (scored.length > 0) return scored.slice(0, topK);
7112
7316
  }
7113
7317
  }
7114
- } catch {
7318
+ } catch (err) {
7319
+ debugError("memory.rankByRelevance", err, "fell back to keyword ranking");
7115
7320
  }
7116
7321
  return keywordRank(query, items, topK);
7117
7322
  }
@@ -7119,6 +7324,7 @@ var STOPWORDS;
7119
7324
  var init_retrieval = __esm({
7120
7325
  "src/memory/retrieval.ts"() {
7121
7326
  "use strict";
7327
+ init_diagnostics();
7122
7328
  STOPWORDS = /* @__PURE__ */ new Set([
7123
7329
  "the",
7124
7330
  "and",
@@ -7317,7 +7523,7 @@ async function readPdf(sourcePath, data) {
7317
7523
  2 /* Usage */
7318
7524
  );
7319
7525
  } finally {
7320
- await parser.destroy().catch(() => void 0);
7526
+ await parser.destroy().catch((err) => debugError("strategies.reader.destroy", err));
7321
7527
  }
7322
7528
  }
7323
7529
  function createDocument(sourceType, sourcePath, rawText, structuredHint, metadata = {}) {
@@ -7363,6 +7569,7 @@ var init_readers = __esm({
7363
7569
  "use strict";
7364
7570
  init_errors2();
7365
7571
  init_types2();
7572
+ init_diagnostics();
7366
7573
  MAX_STRATEGY_BYTES = 10 * 1024 * 1024;
7367
7574
  MAX_PDF_PAGES = 50;
7368
7575
  }
@@ -7384,7 +7591,8 @@ function loadKnowledgeChunks() {
7384
7591
  if (!trimmed) continue;
7385
7592
  try {
7386
7593
  out.push(JSON.parse(trimmed));
7387
- } catch {
7594
+ } catch (err) {
7595
+ debugError("knowledge.load", err, "skipped malformed chunk line");
7388
7596
  }
7389
7597
  }
7390
7598
  return out;
@@ -7427,7 +7635,8 @@ async function addKnowledgeFile(pathOrDash, titleOverride) {
7427
7635
  }));
7428
7636
  try {
7429
7637
  appendFileSync2(knowledgePath(), rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""));
7430
- } catch {
7638
+ } catch (err) {
7639
+ debugError("knowledge.append", err, knowledgePath());
7431
7640
  }
7432
7641
  return { doc_id: docId, title, source_path: doc.source_path, chunks: rows.length };
7433
7642
  }
@@ -7453,7 +7662,8 @@ function listKnowledgeDocs() {
7453
7662
  function listKnowledgeDirFiles(dir) {
7454
7663
  try {
7455
7664
  return readdirSync2(dir).filter((n) => !n.toLowerCase().endsWith("readme.md") && /\.(md|markdown|txt|pdf|yaml|yml)$/i.test(n));
7456
- } catch {
7665
+ } catch (err) {
7666
+ debugError("knowledge.listDir", err, dir);
7457
7667
  return [];
7458
7668
  }
7459
7669
  }
@@ -7463,6 +7673,7 @@ var init_knowledge = __esm({
7463
7673
  "use strict";
7464
7674
  init_store();
7465
7675
  init_readers();
7676
+ init_diagnostics();
7466
7677
  KNOWLEDGE_FILE = "knowledge.jsonl";
7467
7678
  MAX_CHUNK_CHARS = 1200;
7468
7679
  }
@@ -7470,7 +7681,6 @@ var init_knowledge = __esm({
7470
7681
 
7471
7682
  // src/ai/privacy.ts
7472
7683
  import { existsSync as existsSync16, mkdirSync as mkdirSync10, appendFileSync as appendFileSync3 } from "fs";
7473
- import { homedir as homedir4 } from "os";
7474
7684
  import { join as join14 } from "path";
7475
7685
  function scrubSensitiveText(text) {
7476
7686
  return redactSecrets(text).replace(EMAIL_RE2, "[email]");
@@ -7489,24 +7699,29 @@ function stripPII(obj) {
7489
7699
  }
7490
7700
  return out;
7491
7701
  }
7492
- function ensureAuditDir() {
7493
- if (!existsSync16(AUDIT_DIR)) {
7494
- mkdirSync10(AUDIT_DIR, { recursive: true });
7702
+ function auditDir() {
7703
+ const dir = join14(secureNtrpHome(), "audit");
7704
+ if (!existsSync16(dir)) {
7705
+ mkdirSync10(dir, { recursive: true, mode: 448 });
7495
7706
  }
7707
+ chmodQuiet(dir, 448);
7708
+ return dir;
7496
7709
  }
7497
7710
  function logToolCall(entry) {
7498
- ensureAuditDir();
7499
7711
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7500
- const path = join14(AUDIT_DIR, `agentic-${date}.jsonl`);
7712
+ const path = join14(auditDir(), `agentic-${date}.jsonl`);
7501
7713
  appendFileSync3(
7502
7714
  path,
7503
- JSON.stringify({ ...entry, result_preview: scrubSensitiveText(entry.result_preview) }) + "\n"
7715
+ JSON.stringify({ ...entry, result_preview: scrubSensitiveText(entry.result_preview) }) + "\n",
7716
+ { mode: 384 }
7504
7717
  );
7718
+ chmodQuiet(path, 384);
7505
7719
  }
7506
- var PII_FIELDS, EMAIL_RE2, AUDIT_DIR;
7720
+ var PII_FIELDS, EMAIL_RE2;
7507
7721
  var init_privacy = __esm({
7508
7722
  "src/ai/privacy.ts"() {
7509
7723
  "use strict";
7724
+ init_store();
7510
7725
  init_terminal_capture();
7511
7726
  PII_FIELDS = /* @__PURE__ */ new Set([
7512
7727
  "name",
@@ -7537,7 +7752,6 @@ var init_privacy = __esm({
7537
7752
  "metadata"
7538
7753
  ]);
7539
7754
  EMAIL_RE2 = /[\w.+-]+@[\w-]+\.[\w.-]+/g;
7540
- AUDIT_DIR = join14(homedir4(), ".ntrp", "audit");
7541
7755
  }
7542
7756
  });
7543
7757
 
@@ -7603,6 +7817,83 @@ var init_untrusted = __esm({
7603
7817
  }
7604
7818
  });
7605
7819
 
7820
+ // src/ai/json-response.ts
7821
+ function stripJsonFences(text) {
7822
+ const trimmed = text.trim();
7823
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
7824
+ if (fenced) return fenced[1].trim();
7825
+ return trimmed.replace(/```(?:json)?\s*/gi, "").replace(/```/g, "").trim();
7826
+ }
7827
+ function parseJsonObjectFromText(text) {
7828
+ const cleaned = stripJsonFences(text);
7829
+ const start = cleaned.indexOf("{");
7830
+ if (start === -1) return null;
7831
+ let depth = 0;
7832
+ let inString = false;
7833
+ let escaped = false;
7834
+ let end = -1;
7835
+ for (let i = start; i < cleaned.length; i++) {
7836
+ const ch = cleaned[i];
7837
+ if (escaped) {
7838
+ escaped = false;
7839
+ continue;
7840
+ }
7841
+ if (ch === "\\") {
7842
+ if (inString) escaped = true;
7843
+ continue;
7844
+ }
7845
+ if (ch === '"') {
7846
+ inString = !inString;
7847
+ continue;
7848
+ }
7849
+ if (inString) continue;
7850
+ if (ch === "{") depth++;
7851
+ else if (ch === "}") {
7852
+ depth--;
7853
+ if (depth === 0) {
7854
+ end = i;
7855
+ break;
7856
+ }
7857
+ }
7858
+ }
7859
+ if (end === -1) return null;
7860
+ try {
7861
+ const parsed = JSON.parse(cleaned.slice(start, end + 1));
7862
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
7863
+ } catch {
7864
+ return null;
7865
+ }
7866
+ }
7867
+ function parseJsonArrayFromText(text) {
7868
+ const cleaned = stripJsonFences(text);
7869
+ const start = cleaned.indexOf("[");
7870
+ if (start === -1) return null;
7871
+ let depth = 0;
7872
+ let end = -1;
7873
+ for (let i = start; i < cleaned.length; i++) {
7874
+ if (cleaned[i] === "[") depth++;
7875
+ else if (cleaned[i] === "]") {
7876
+ depth--;
7877
+ if (depth === 0) {
7878
+ end = i;
7879
+ break;
7880
+ }
7881
+ }
7882
+ }
7883
+ if (end === -1) return null;
7884
+ try {
7885
+ const parsed = JSON.parse(cleaned.slice(start, end + 1));
7886
+ return Array.isArray(parsed) ? parsed : null;
7887
+ } catch {
7888
+ return null;
7889
+ }
7890
+ }
7891
+ var init_json_response = __esm({
7892
+ "src/ai/json-response.ts"() {
7893
+ "use strict";
7894
+ }
7895
+ });
7896
+
7606
7897
  // src/data/gtm-counsel/play-routing.ts
7607
7898
  function getPlayRouting(playId) {
7608
7899
  return PLAY_ROUTING[playId];
@@ -7798,7 +8089,8 @@ function getCustomPlays() {
7798
8089
  try {
7799
8090
  const play = JSON.parse(trimmed);
7800
8091
  out.push({ ...play, source: "learned" });
7801
- } catch {
8092
+ } catch (err) {
8093
+ debugError("playbook.learned.read", err, "skipped malformed line");
7802
8094
  }
7803
8095
  }
7804
8096
  return out;
@@ -7827,7 +8119,8 @@ function addCustomPlay(input) {
7827
8119
  };
7828
8120
  try {
7829
8121
  appendFileSync4(playsPath(), JSON.stringify(play) + "\n");
7830
- } catch {
8122
+ } catch (err) {
8123
+ debugError("playbook.learned.append", err, playsPath());
7831
8124
  }
7832
8125
  return play;
7833
8126
  }
@@ -7910,6 +8203,7 @@ var init_playbook = __esm({
7910
8203
  "use strict";
7911
8204
  init_store();
7912
8205
  init_play_routing();
8206
+ init_diagnostics();
7913
8207
  PLAYBOOK = [
7914
8208
  {
7915
8209
  id: "multi-thread-deals",
@@ -8312,12 +8606,6 @@ var init_playbook = __esm({
8312
8606
  });
8313
8607
 
8314
8608
  // src/ai/strategy-normalize.ts
8315
- function stripFences(text) {
8316
- const trimmed = text.trim();
8317
- const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
8318
- if (fenced) return fenced[1].trim();
8319
- return trimmed;
8320
- }
8321
8609
  async function normalizeStrategy(input, ctx) {
8322
8610
  assertReplAi(ctx);
8323
8611
  const playbookBlock = getPlaybook().map((play) => `- ${play.id}: ${play.name} (${play.trigger_vital_sign}) \u2014 ${play.why}`).join("\n");
@@ -8341,7 +8629,7 @@ Normalize this strategy now as strict JSON.`,
8341
8629
  );
8342
8630
  let parsed;
8343
8631
  try {
8344
- parsed = JSON.parse(stripFences(text));
8632
+ parsed = JSON.parse(stripJsonFences(text));
8345
8633
  } catch {
8346
8634
  throw new Error("AI response is not valid strategy JSON");
8347
8635
  }
@@ -8417,6 +8705,7 @@ var init_strategy_normalize = __esm({
8417
8705
  "use strict";
8418
8706
  init_repl_api();
8419
8707
  init_complete();
8708
+ init_json_response();
8420
8709
  init_playbook();
8421
8710
  SYSTEM_PROMPT = `You are a strategy-ingestion engine for NTRP, a GTM pipeline-health and orchestration tool.
8422
8711
 
@@ -8459,6 +8748,29 @@ JSON SHAPE:
8459
8748
  }
8460
8749
  });
8461
8750
 
8751
+ // src/output/strategy-signal.ts
8752
+ function isProcessMetaLine(text) {
8753
+ return PROCESS_META_RE.test(text.trim());
8754
+ }
8755
+ function filterOperatorLines(lines) {
8756
+ return lines.map((l) => l.trim()).filter((l) => l.length > 0 && !isProcessMetaLine(l));
8757
+ }
8758
+ function isGenericWorkstreamRationale(rationale) {
8759
+ return GENERIC_RATIONALE_RE.test(rationale.trim());
8760
+ }
8761
+ function usedGroundedFallback(notices) {
8762
+ return notices.some((n) => /grounded fallback/i.test(n));
8763
+ }
8764
+ var PROCESS_META_RE, GENERIC_RATIONALE_RE, STRATEGY_FALLBACK_BANNER;
8765
+ var init_strategy_signal = __esm({
8766
+ "src/output/strategy-signal.ts"() {
8767
+ "use strict";
8768
+ PROCESS_META_RE = /LLM\b|plan JSON|JSON invalid|grounded fallback|validated LLM|emitting grounded|Backcast JSON|Stress-test revision|Counsel reflection|Reality digest|Strategist JSON failed|Plan gap\s*\(/i;
8769
+ GENERIC_RATIONALE_RE = /Layer-order first|Next in dependency order after the prior workstream/i;
8770
+ STRATEGY_FALLBACK_BANNER = "Drafted from live vitals \u2014 refine with /strategy after the first review.";
8771
+ }
8772
+ });
8773
+
8462
8774
  // src/strategies/library.ts
8463
8775
  import { writeFileSync as writeFileSync11 } from "fs";
8464
8776
  import { join as join16 } from "path";
@@ -8480,24 +8792,32 @@ function formatEffortSum(workstreams) {
8480
8792
  return `~${Math.round(sum)} team-hours across ${count} workstream${count === 1 ? "" : "s"}`;
8481
8793
  }
8482
8794
  function renderConstraintHeading(constraintLine) {
8795
+ const line = constraintLine?.trim();
8796
+ if (!line) return "";
8483
8797
  return `## Constraint
8484
- ${constraintLine?.trim() || "none stated"}
8798
+ ${line}
8485
8799
  `;
8486
8800
  }
8487
8801
  function renderScopeHeading(constraints, outOfScope) {
8488
- const inLines = constraints.length > 0 ? formatList(constraints) : "- none stated";
8802
+ const inItems = filterOperatorLines(constraints);
8489
8803
  const outItems = (outOfScope ?? []).map((s) => s.trim()).filter(Boolean);
8490
- const outLines = outItems.length > 0 ? formatList(outItems) : "- none stated";
8491
- return `## In scope / out of scope
8492
- In scope:
8493
- ${inLines}
8494
-
8495
- Out of scope:
8496
- ${outLines}
8497
- `;
8804
+ if (inItems.length === 0 && outItems.length === 0) return "";
8805
+ const parts = ["## In scope / out of scope"];
8806
+ if (inItems.length > 0) {
8807
+ parts.push("In scope:");
8808
+ parts.push(formatList(inItems));
8809
+ }
8810
+ if (outItems.length > 0) {
8811
+ if (inItems.length > 0) parts.push("");
8812
+ parts.push("Out of scope:");
8813
+ parts.push(formatList(outItems));
8814
+ }
8815
+ return parts.join("\n") + "\n";
8498
8816
  }
8499
8817
  function renderKilledAlternativeLine(killedAlternative) {
8500
- return `Killed alternative: ${killedAlternative?.trim() || "none stated"}`;
8818
+ const line = killedAlternative?.trim();
8819
+ if (!line) return "";
8820
+ return `Killed alternative: ${line}`;
8501
8821
  }
8502
8822
  function renderEffortHeading(workstreams) {
8503
8823
  return `## Effort
@@ -8536,14 +8856,15 @@ ${strategy.objective}
8536
8856
  ## Workstreams
8537
8857
  ${strategy.workstreams.map(formatWorkstream).join("\n")}
8538
8858
  ` : "";
8539
- const constraintsSection = strategy.constraints.length > 0 ? `
8859
+ const constraintsSection = filterOperatorLines(strategy.constraints).length > 0 ? `
8540
8860
  ## Constraints
8541
- ${formatList(strategy.constraints)}
8861
+ ${formatList(filterOperatorLines(strategy.constraints))}
8542
8862
  ` : "";
8543
- const assumptionsSection = strategy.assumptions.length > 0 ? `
8863
+ const assumptionsSection = filterOperatorLines(strategy.assumptions).length > 0 ? `
8544
8864
  ## Assumptions (unverified)
8545
- ${formatList(strategy.assumptions)}
8865
+ ${formatList(filterOperatorLines(strategy.assumptions))}
8546
8866
  ` : "";
8867
+ const risks = filterOperatorLines(strategy.risks);
8547
8868
  return `---
8548
8869
  ${frontmatter}
8549
8870
  ---
@@ -8569,7 +8890,7 @@ ${formatMetrics(strategy.leading_indicators)}
8569
8890
  ${formatList(strategy.recommended_actions)}
8570
8891
  ${constraintsSection}${assumptionsSection}
8571
8892
  ## Risks
8572
- ${formatList(strategy.risks)}
8893
+ ${formatList(risks)}
8573
8894
 
8574
8895
  ## Experiment Design
8575
8896
  ${strategy.experiment_design}
@@ -8591,76 +8912,75 @@ ${strategy.objective}
8591
8912
  ## Workstreams
8592
8913
  ${strategy.workstreams.map(formatWorkstream).join("\n")}
8593
8914
  ` : "";
8594
- const assumptionsSection = strategy.assumptions.length > 0 ? `
8915
+ const assumptions = filterOperatorLines(strategy.assumptions);
8916
+ const assumptionsSection = assumptions.length > 0 ? `
8595
8917
  ## Assumptions (unverified)
8596
- ${formatList(strategy.assumptions)}
8918
+ ${formatList(assumptions)}
8597
8919
  ` : "";
8598
- const craftLogSection = extras?.craftLogPath ? `
8599
- ## Craft log
8600
- See \`${extras.craftLogPath}\`.
8601
- ` : "";
8602
- return `---
8920
+ const risks = filterOperatorLines(strategy.risks);
8921
+ const constraintBlock = renderConstraintHeading(extras?.constraintLine);
8922
+ const scopeBlock = renderScopeHeading(strategy.constraints, extras?.outOfScope);
8923
+ const killedLine = renderKilledAlternativeLine(extras?.killedAlternative);
8924
+ const parts = [
8925
+ `---
8603
8926
  ${frontmatter}
8604
8927
  ---
8605
-
8606
- # ${strategy.title}
8607
- ${callSection}${objectiveSection}
8608
- ${renderConstraintHeading(extras?.constraintLine)}
8609
- ## Goal
8610
- ${strategy.goal}
8611
-
8612
- ${renderScopeHeading(strategy.constraints, extras?.outOfScope)}
8613
- ## Hypothesis
8614
- ${strategy.hypothesis}
8615
-
8616
- ${renderKilledAlternativeLine(extras?.killedAlternative)}
8617
-
8618
- ## Target Segment
8619
- ${strategy.target_segment}
8620
-
8621
- ${renderEffortHeading(strategy.workstreams)}${workstreamSection}
8622
- ## Success Metrics
8623
- ${formatMetrics(strategy.success_metrics)}
8624
-
8625
- ## Leading Indicators
8626
- ${formatMetrics(strategy.leading_indicators)}
8627
-
8628
- ## Recommended Actions
8629
- ${formatList(strategy.recommended_actions)}
8630
- ${assumptionsSection}
8631
- ## Risks
8632
- ${formatList(strategy.risks)}
8633
-
8634
- ## Experiment Design
8635
- ${strategy.experiment_design}
8636
-
8637
- ${renderReviewHeading({ cadence: strategy.review_cadence, slug: strategy.slug })}${craftLogSection}
8638
- ## Source Excerpt
8639
- ${strategy.raw_excerpt || "_No excerpt captured._"}
8640
- `;
8928
+ `,
8929
+ `# ${strategy.title}`,
8930
+ callSection.trimEnd(),
8931
+ objectiveSection.trimEnd(),
8932
+ constraintBlock.trimEnd(),
8933
+ "## Goal",
8934
+ strategy.goal,
8935
+ scopeBlock.trimEnd(),
8936
+ "## Hypothesis",
8937
+ strategy.hypothesis,
8938
+ killedLine,
8939
+ "## Target Segment",
8940
+ strategy.target_segment,
8941
+ renderEffortHeading(strategy.workstreams).trimEnd(),
8942
+ workstreamSection.trimEnd(),
8943
+ "## Success Metrics",
8944
+ formatMetrics(strategy.success_metrics),
8945
+ "## Leading Indicators",
8946
+ formatMetrics(strategy.leading_indicators),
8947
+ "## Recommended Actions",
8948
+ formatList(strategy.recommended_actions),
8949
+ assumptionsSection.trimEnd(),
8950
+ "## Risks",
8951
+ formatList(risks),
8952
+ "## Experiment Design",
8953
+ strategy.experiment_design,
8954
+ renderReviewHeading({ cadence: strategy.review_cadence, slug: strategy.slug }).trimEnd(),
8955
+ "## Source Excerpt",
8956
+ strategy.raw_excerpt || "_No excerpt captured._"
8957
+ ].filter((p) => p != null && String(p).length > 0);
8958
+ return parts.join("\n\n") + "\n";
8641
8959
  }
8642
8960
  function formatWorkstream(ws) {
8643
8961
  const lines = [];
8644
8962
  lines.push(`### ${ws.order}. ${ws.title}`);
8645
8963
  lines.push(`- Problem: ${ws.problem}`);
8646
- lines.push(`- Why this order: ${ws.rationale}`);
8964
+ if (ws.rationale && !isGenericWorkstreamRationale(ws.rationale)) {
8965
+ lines.push(`- Why this order: ${ws.rationale}`);
8966
+ }
8647
8967
  if (ws.play_ids.length > 0) lines.push(`- Plays: ${ws.play_ids.join(", ")}`);
8648
8968
  lines.push(
8649
- `- Expected outcome: ${ws.expected_outcome.metric} \u2014 ${ws.expected_outcome.baseline} -> ${ws.expected_outcome.target_range} by ${ws.expected_outcome.check_date} (measured by ${ws.expected_outcome.measured_by})`
8969
+ `- Expected outcome: ${ws.expected_outcome.metric} \u2014 ${ws.expected_outcome.baseline} \u2192 ${ws.expected_outcome.target_range} by ${ws.expected_outcome.check_date} (measured by ${ws.expected_outcome.measured_by})`
8650
8970
  );
8651
8971
  for (const li of ws.leading_indicators) {
8652
- lines.push(`- Leading indicator: ${li.metric} \u2014 ${li.baseline} -> ${li.target_range} by ${li.check_date} (measured by ${li.measured_by})`);
8972
+ lines.push(`- Leading indicator: ${li.metric} \u2014 ${li.baseline} \u2192 ${li.target_range} by ${li.check_date} (measured by ${li.measured_by})`);
8653
8973
  }
8654
8974
  if (ws.milestones.length > 0) {
8655
8975
  lines.push(`- Milestones:`);
8656
8976
  for (const m of ws.milestones) {
8657
- lines.push(` - [ ] ${m.due} \u2014 ${m.label} (verify: ${m.verification})`);
8977
+ lines.push(` - [ ] ${m.due} \u2014 ${m.label}`);
8658
8978
  }
8659
8979
  }
8660
8980
  if (ws.deliverables.length > 0) {
8661
8981
  lines.push(`- Deliverables:`);
8662
8982
  for (const d of ws.deliverables) {
8663
- lines.push(` - [ ] ${d.label} (${d.kind.replace("_", " ")}, due ${d.due})`);
8983
+ lines.push(` - [ ] ${d.label} (due ${d.due})`);
8664
8984
  }
8665
8985
  }
8666
8986
  if (ws.actions.length > 0) {
@@ -8669,7 +8989,7 @@ function formatWorkstream(ws) {
8669
8989
  lines.push(` - ${action}`);
8670
8990
  }
8671
8991
  }
8672
- lines.push(`- Contingency: if ${ws.contingency.trigger} (check ${ws.contingency.trigger_check_date}) -> ${ws.contingency.fallback}`);
8992
+ lines.push(`- Contingency: if ${ws.contingency.trigger} (check ${ws.contingency.trigger_check_date}) \u2192 ${ws.contingency.fallback}`);
8673
8993
  lines.push(`- Effort: ~${Math.round(ws.effort_hours)} team-hours`);
8674
8994
  return lines.join("\n") + "\n";
8675
8995
  }
@@ -8691,12 +9011,13 @@ var init_library = __esm({
8691
9011
  "src/strategies/library.ts"() {
8692
9012
  "use strict";
8693
9013
  init_store();
9014
+ init_strategy_signal();
8694
9015
  }
8695
9016
  });
8696
9017
 
8697
9018
  // src/strategies/connectors.ts
8698
9019
  import { readdirSync as readdirSync3, statSync as statSync2 } from "fs";
8699
- import { homedir as homedir5 } from "os";
9020
+ import { homedir as homedir4 } from "os";
8700
9021
  import { basename as basename4, extname as extname2, join as join17, relative, resolve as resolve6, sep as sep3 } from "path";
8701
9022
  function createLocalFolderConnector(options) {
8702
9023
  const rootPath = resolveUserPath2(options.rootPath);
@@ -8743,15 +9064,15 @@ function walkLocalFolder(rootPath, currentPath, refs, opts) {
8743
9064
  const absolutePath = join17(currentPath, entry.name);
8744
9065
  const relativePath = normalizePath(relative(rootPath, absolutePath));
8745
9066
  if (entry.isDirectory()) {
8746
- if (shouldSkipDirectory(entry.name) || matchesAny(relativePath, opts.excludePatterns)) continue;
9067
+ if (shouldSkipDirectory(entry.name) || matchesAny2(relativePath, opts.excludePatterns)) continue;
8747
9068
  walkLocalFolder(rootPath, absolutePath, refs, opts);
8748
9069
  continue;
8749
9070
  }
8750
9071
  if (!entry.isFile()) continue;
8751
9072
  const ext = extname2(entry.name).toLowerCase();
8752
9073
  if (!SUPPORTED_EXTENSIONS.has(ext)) continue;
8753
- if (opts.includePatterns.length > 0 && !matchesAny(relativePath, opts.includePatterns)) continue;
8754
- if (matchesAny(relativePath, opts.excludePatterns)) continue;
9074
+ if (opts.includePatterns.length > 0 && !matchesAny2(relativePath, opts.includePatterns)) continue;
9075
+ if (matchesAny2(relativePath, opts.excludePatterns)) continue;
8755
9076
  const stat = safeStat(absolutePath);
8756
9077
  if (!stat || stat.size > opts.maxBytes) continue;
8757
9078
  refs.push({
@@ -8782,7 +9103,7 @@ function safeStat(path) {
8782
9103
  function normalizePatterns(patterns) {
8783
9104
  return (patterns ?? []).map((pattern) => pattern.trim()).filter(Boolean).map(normalizePath);
8784
9105
  }
8785
- function matchesAny(relativePath, patterns) {
9106
+ function matchesAny2(relativePath, patterns) {
8786
9107
  return patterns.some((pattern) => matchesPattern(relativePath, pattern));
8787
9108
  }
8788
9109
  function matchesPattern(relativePath, pattern) {
@@ -8803,8 +9124,8 @@ function normalizePath(path) {
8803
9124
  return path.split(sep3).join("/");
8804
9125
  }
8805
9126
  function resolveUserPath2(path) {
8806
- if (path === "~") return homedir5();
8807
- if (path.startsWith("~/")) return join17(homedir5(), path.slice(2));
9127
+ if (path === "~") return homedir4();
9128
+ if (path.startsWith("~/")) return join17(homedir4(), path.slice(2));
8808
9129
  return resolve6(path);
8809
9130
  }
8810
9131
  var DEFAULT_MAX_FILES, DEFAULT_MAX_BYTES, SUPPORTED_EXTENSIONS, DEFAULT_EXCLUDED_DIRS;
@@ -9034,7 +9355,8 @@ function readJsonl(file) {
9034
9355
  if (!trimmed) continue;
9035
9356
  try {
9036
9357
  out.push(JSON.parse(trimmed));
9037
- } catch {
9358
+ } catch (err) {
9359
+ debugError("memory.readJsonl", err, `${file}: skipped malformed line`);
9038
9360
  }
9039
9361
  }
9040
9362
  return out;
@@ -9042,13 +9364,15 @@ function readJsonl(file) {
9042
9364
  function appendJsonl(file, obj) {
9043
9365
  try {
9044
9366
  appendFileSync5(memPath(file), JSON.stringify(obj) + "\n");
9045
- } catch {
9367
+ } catch (err) {
9368
+ debugError("memory.appendJsonl", err, file);
9046
9369
  }
9047
9370
  }
9048
9371
  function rewriteJsonl(file, rows) {
9049
9372
  try {
9050
9373
  writeFileSync12(memPath(file), rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""));
9051
- } catch {
9374
+ } catch (err) {
9375
+ debugError("memory.rewriteJsonl", err, file);
9052
9376
  }
9053
9377
  }
9054
9378
  function scrubText(text) {
@@ -9152,7 +9476,8 @@ async function loadStrategySnippets() {
9152
9476
  title: s.title,
9153
9477
  text: `${s.title}. Goal: ${s.goal} Hypothesis: ${s.hypothesis} Target: ${s.target_segment}`
9154
9478
  }));
9155
- } catch {
9479
+ } catch (err) {
9480
+ debugError("memory.loadStrategySnippets", err);
9156
9481
  return [];
9157
9482
  }
9158
9483
  }
@@ -9168,7 +9493,8 @@ function loadWinSnippets() {
9168
9493
  out.push({ id: `win:${name}`, title, text: `${title}. ${body}` });
9169
9494
  }
9170
9495
  return out;
9171
- } catch {
9496
+ } catch (err) {
9497
+ debugError("memory.loadWinSnippets", err);
9172
9498
  return [];
9173
9499
  }
9174
9500
  }
@@ -9188,7 +9514,8 @@ async function buildMemoryBlock(query, opts = {}) {
9188
9514
  let knowledge = [];
9189
9515
  try {
9190
9516
  knowledge = loadKnowledgeChunks();
9191
- } catch {
9517
+ } catch (err) {
9518
+ debugError("memory.loadKnowledgeChunks", err);
9192
9519
  knowledge = [];
9193
9520
  }
9194
9521
  const sections = [];
@@ -9270,6 +9597,7 @@ var init_store2 = __esm({
9270
9597
  init_knowledge();
9271
9598
  init_privacy();
9272
9599
  init_untrusted();
9600
+ init_diagnostics();
9273
9601
  FACTS_FILE = "facts.jsonl";
9274
9602
  LEDGER_FILE = "ledger.jsonl";
9275
9603
  FACTS_JSONL = FACTS_FILE;
@@ -9461,7 +9789,7 @@ __export(context_exports, {
9461
9789
  });
9462
9790
  import { basename as basename5, join as join19, resolve as resolve7, sep as sep4 } from "path";
9463
9791
  import { existsSync as existsSync19, mkdirSync as mkdirSync11, writeFileSync as writeFileSync13, readFileSync as readFileSync16, readdirSync as readdirSync5, statSync as statSync3, rmSync as rmSync5 } from "fs";
9464
- import { homedir as homedir6 } from "os";
9792
+ import { homedir as homedir5 } from "os";
9465
9793
  import { randomUUID as randomUUID6 } from "crypto";
9466
9794
  function isSessionStale(s) {
9467
9795
  return Date.now() - s.mtime > STALE_SESSION_MS;
@@ -9475,7 +9803,7 @@ function isAnalysisReady(ctx) {
9475
9803
  return Object.values(counts).some((n) => n > 0);
9476
9804
  }
9477
9805
  function ntrpHomeDir() {
9478
- return process.env.NTRP_HOME ? resolve7(process.env.NTRP_HOME) : join19(homedir6(), ".ntrp");
9806
+ return process.env.NTRP_HOME ? resolve7(process.env.NTRP_HOME) : join19(homedir5(), ".ntrp");
9479
9807
  }
9480
9808
  function getSessionsDir() {
9481
9809
  const dir = join19(ntrpHomeDir(), "sessions");
@@ -16258,7 +16586,8 @@ function readVersionFromPackageJson(packageJsonPath) {
16258
16586
  try {
16259
16587
  const pkg = JSON.parse(readFileSync18(packageJsonPath, "utf-8"));
16260
16588
  if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
16261
- } catch {
16589
+ } catch (err) {
16590
+ debugError("version.readPackageJson", err, packageJsonPath);
16262
16591
  }
16263
16592
  return null;
16264
16593
  }
@@ -16282,6 +16611,7 @@ var cachedVersion;
16282
16611
  var init_version = __esm({
16283
16612
  "src/version.ts"() {
16284
16613
  "use strict";
16614
+ init_diagnostics();
16285
16615
  }
16286
16616
  });
16287
16617
 
@@ -16338,7 +16668,7 @@ function applyUpdateCheckResult(ctx, result) {
16338
16668
  function startBackgroundUpdateCheck(ctx) {
16339
16669
  const pending = checkForUpdate({ force: true, timeoutMs: 5e3 });
16340
16670
  ctx.pendingUpdateCheck = pending;
16341
- void pending.then((result) => applyUpdateCheckResult(ctx, result)).catch(() => void 0);
16671
+ void pending.then((result) => applyUpdateCheckResult(ctx, result)).catch((err) => debugError("update.backgroundCheck", err));
16342
16672
  }
16343
16673
  async function fetchLatestVersion(timeoutMs = 5e3) {
16344
16674
  try {
@@ -16346,7 +16676,8 @@ async function fetchLatestVersion(timeoutMs = 5e3) {
16346
16676
  if (!res.ok) return null;
16347
16677
  const data = await res.json();
16348
16678
  return typeof data.version === "string" && data.version.length > 0 ? data.version : null;
16349
- } catch {
16679
+ } catch (err) {
16680
+ debugError("update.fetchLatestVersion", err);
16350
16681
  return null;
16351
16682
  }
16352
16683
  }
@@ -16381,6 +16712,7 @@ var init_registry = __esm({
16381
16712
  "use strict";
16382
16713
  init_update_check();
16383
16714
  init_version();
16715
+ init_diagnostics();
16384
16716
  NPM_PACKAGE = "@sonnechasser/ntrp";
16385
16717
  }
16386
16718
  });
@@ -17229,6 +17561,369 @@ var init_gap_card = __esm({
17229
17561
  }
17230
17562
  });
17231
17563
 
17564
+ // src/conversation/onboard-tiers.ts
17565
+ var onboard_tiers_exports = {};
17566
+ __export(onboard_tiers_exports, {
17567
+ ONBOARD_TIER_ORDER: () => ONBOARD_TIER_ORDER,
17568
+ canRunDomainTier: () => canRunDomainTier,
17569
+ clearOnboardTierFlags: () => clearOnboardTierFlags,
17570
+ describeOnboardTier: () => describeOnboardTier,
17571
+ getOnboardTierFlag: () => getOnboardTierFlag,
17572
+ getOnboardTierStatus: () => getOnboardTierStatus,
17573
+ hasProductionDataset: () => hasProductionDataset,
17574
+ listCompletedOnboardTier: () => listCompletedOnboardTier,
17575
+ markDemoDataSeen: () => markDemoDataSeen,
17576
+ markOnboardTierComplete: () => markOnboardTierComplete,
17577
+ markProductionDataSeen: () => markProductionDataSeen,
17578
+ pathLooksPresent: () => pathLooksPresent,
17579
+ resetOnboardTierProgress: () => resetOnboardTierProgress,
17580
+ resolveNextOnboardTier: () => resolveNextOnboardTier
17581
+ });
17582
+ import { existsSync as existsSync24, statSync as statSync4 } from "fs";
17583
+ function flagSet(tier) {
17584
+ return Boolean(getConfigValue(TIER_CONFIG_KEYS[tier]));
17585
+ }
17586
+ function getOnboardTierFlag(tier) {
17587
+ return getConfigValue(TIER_CONFIG_KEYS[tier]);
17588
+ }
17589
+ function markOnboardTierComplete(...tiers) {
17590
+ const at = (/* @__PURE__ */ new Date()).toISOString();
17591
+ for (const tier of tiers) {
17592
+ if (!flagSet(tier)) setConfigValue(TIER_CONFIG_KEYS[tier], at);
17593
+ }
17594
+ }
17595
+ function clearOnboardTierFlags(...tiers) {
17596
+ for (const tier of tiers) {
17597
+ deleteConfigValue(TIER_CONFIG_KEYS[tier]);
17598
+ }
17599
+ }
17600
+ function resetOnboardTierProgress() {
17601
+ clearOnboardTierFlags(...ONBOARD_TIER_ORDER);
17602
+ }
17603
+ function hasProductionDataset(ctx) {
17604
+ const source = ctx?.dataset?.source;
17605
+ if (source && !source.startsWith("demo:")) {
17606
+ if (source.startsWith("/") || /^[A-Za-z]:[\\/]/.test(source) || source.startsWith("~")) {
17607
+ return true;
17608
+ }
17609
+ if (!source.includes(":") && existsSync24(source)) return true;
17610
+ if (source.startsWith("csv:") || source.startsWith("file:") || source.startsWith("folder:")) {
17611
+ return true;
17612
+ }
17613
+ }
17614
+ if (ctx?.attachments && ctx.attachments.length > 0) return true;
17615
+ return flagSet("production");
17616
+ }
17617
+ function hasDemoExperience(ctx) {
17618
+ if (flagSet("demo")) return true;
17619
+ if (getPreferredDemoScenario()) return true;
17620
+ const source = ctx?.dataset?.source;
17621
+ if (source?.startsWith("demo:")) return true;
17622
+ return false;
17623
+ }
17624
+ function listCompletedOnboardTier(ctx) {
17625
+ const done = [];
17626
+ const profileOk = flagSet("profile") || isProfileConfigured(loadProfile());
17627
+ if (profileOk) done.push("profile");
17628
+ else return done;
17629
+ if (flagSet("domain")) done.push("domain");
17630
+ else return done;
17631
+ if (hasDemoExperience(ctx)) done.push("demo");
17632
+ else return done;
17633
+ if (hasProductionDataset(ctx) || flagSet("production")) done.push("production");
17634
+ return done;
17635
+ }
17636
+ function resolveNextOnboardTier(ctx) {
17637
+ const done = new Set(listCompletedOnboardTier(ctx));
17638
+ for (const tier of ONBOARD_TIER_ORDER) {
17639
+ if (!done.has(tier)) return tier;
17640
+ }
17641
+ return null;
17642
+ }
17643
+ function getOnboardTierStatus(ctx) {
17644
+ const completed = listCompletedOnboardTier(ctx);
17645
+ const next = resolveNextOnboardTier(ctx);
17646
+ const meta = next ? TIER_META[next] : null;
17647
+ return {
17648
+ completed,
17649
+ next,
17650
+ nextLabel: meta?.label ?? null,
17651
+ nextHint: meta?.hint ?? null
17652
+ };
17653
+ }
17654
+ function describeOnboardTier(tier) {
17655
+ return TIER_META[tier];
17656
+ }
17657
+ function canRunDomainTier() {
17658
+ return isProfileConfigured(loadProfile()) && hasAnyLlmProvider();
17659
+ }
17660
+ function markProductionDataSeen() {
17661
+ markOnboardTierComplete("production");
17662
+ }
17663
+ function markDemoDataSeen() {
17664
+ markOnboardTierComplete("demo");
17665
+ }
17666
+ function pathLooksPresent(raw) {
17667
+ try {
17668
+ return existsSync24(raw) && (statSync4(raw).isFile() || statSync4(raw).isDirectory());
17669
+ } catch {
17670
+ return false;
17671
+ }
17672
+ }
17673
+ var ONBOARD_TIER_ORDER, TIER_CONFIG_KEYS, TIER_META;
17674
+ var init_onboard_tiers = __esm({
17675
+ "src/conversation/onboard-tiers.ts"() {
17676
+ "use strict";
17677
+ init_store();
17678
+ init_profile();
17679
+ init_repl_api();
17680
+ init_scenario_fit();
17681
+ ONBOARD_TIER_ORDER = [
17682
+ "profile",
17683
+ "domain",
17684
+ "demo",
17685
+ "production"
17686
+ ];
17687
+ TIER_CONFIG_KEYS = {
17688
+ profile: "onboard-tier-profile",
17689
+ domain: "onboard-tier-domain",
17690
+ demo: "onboard-tier-demo",
17691
+ production: "onboard-tier-production"
17692
+ };
17693
+ TIER_META = {
17694
+ profile: {
17695
+ label: "Company profile",
17696
+ hint: "Name, industry, ICP \u2014 works without an API key"
17697
+ },
17698
+ domain: {
17699
+ label: "Domain research",
17700
+ hint: "Connect a key and let NTRP research your company"
17701
+ },
17702
+ demo: {
17703
+ label: "Sample data",
17704
+ hint: "Load a fitted demo book of business"
17705
+ },
17706
+ production: {
17707
+ label: "Your data",
17708
+ hint: "Drag-drop a CSV or folder path into the REPL"
17709
+ }
17710
+ };
17711
+ }
17712
+ });
17713
+
17714
+ // src/conversation/teaching-suggestions.ts
17715
+ function hasPriorExploreExchange() {
17716
+ return (getUsageStats().nl_exchanges ?? 0) > 0;
17717
+ }
17718
+ function shouldShowTeachingSuggestions(ctx) {
17719
+ if (hasPriorExploreExchange()) return false;
17720
+ if (hasProductionDataset(ctx)) return false;
17721
+ return true;
17722
+ }
17723
+ var init_teaching_suggestions = __esm({
17724
+ "src/conversation/teaching-suggestions.ts"() {
17725
+ "use strict";
17726
+ init_usage_stats();
17727
+ init_onboard_tiers();
17728
+ }
17729
+ });
17730
+
17731
+ // src/conversation/suggested-asks.ts
17732
+ function clip(s, maxLen) {
17733
+ const t = s.replace(/\s+/g, " ").trim();
17734
+ if (t.length <= maxLen) return t;
17735
+ return t.slice(0, Math.max(0, maxLen - 1)).trimEnd() + "\u2026";
17736
+ }
17737
+ function money(vs) {
17738
+ if (vs.dollar_value == null || vs.dollar_value <= 0) return null;
17739
+ return formatCurrency(vs.dollar_value);
17740
+ }
17741
+ function askForVital(vs) {
17742
+ const label = VITAL_SIGN_LABELS[vs.vital_sign] ?? vs.vital_sign;
17743
+ const $ = money(vs);
17744
+ switch (vs.vital_sign) {
17745
+ case "freshness":
17746
+ return $ ? `Which accounts hold ${$} in stale pipeline?` : `What's driving Freshness at ${Math.round(vs.score)}?`;
17747
+ case "flow_rate":
17748
+ return $ ? `Which deals make up ${$} stuck in pipeline?` : `Which deals are stuck the longest?`;
17749
+ case "drop_rate":
17750
+ return $ ? `Where do we lose an estimated ${$} at handoff?` : `Where are we losing leads in the handoff?`;
17751
+ case "signal_to_noise":
17752
+ return $ ? `Where is ${$} of misdirected effort going?` : `Where is effort going that isn't tied to pipeline?`;
17753
+ case "thread_depth":
17754
+ return $ ? `Which deals make up ${$} of single-threaded risk?` : `Which large deals are single-threaded?`;
17755
+ default:
17756
+ return `What's behind ${label} at ${Math.round(vs.score)}?`;
17757
+ }
17758
+ }
17759
+ function secondPressure(health, gating) {
17760
+ const ranked = [...health.vital_signs].filter((v) => v.vital_sign !== gating).sort((a, b) => {
17761
+ const aBad = a.status === "red" ? 0 : a.status === "yellow" ? 1 : 2;
17762
+ const bBad = b.status === "red" ? 0 : b.status === "yellow" ? 1 : 2;
17763
+ if (aBad !== bBad) return aBad - bBad;
17764
+ return a.score - b.score;
17765
+ });
17766
+ return ranked[0] ?? null;
17767
+ }
17768
+ function playAsk(gating) {
17769
+ const play = getPlaysForVitalSign(gating)[0];
17770
+ if (!play) return null;
17771
+ const label = VITAL_SIGN_LABELS[gating] ?? gating;
17772
+ return `Would "${play.name}" help with ${label}?`;
17773
+ }
17774
+ function scopeAsk(summary, maxLen) {
17775
+ if (!summary) return null;
17776
+ const cleaned = summary.replace(/\s+/g, " ").trim();
17777
+ if (cleaned.length < 8) return null;
17778
+ if (/\?$/.test(cleaned)) return clip(cleaned, maxLen);
17779
+ return clip(`Given our focus \u2014 ${cleaned} \u2014 what matters most?`, maxLen);
17780
+ }
17781
+ function findingAsk(findings, maxLen) {
17782
+ const f = findings?.find((x) => x.finding?.trim());
17783
+ if (!f?.finding) return null;
17784
+ const head = f.finding.replace(/\s+/g, " ").trim();
17785
+ const short = head.length > 48 ? `${head.slice(0, 45).trimEnd()}\u2026` : head;
17786
+ return clip(`What's the so-what on "${short}"?`, maxLen);
17787
+ }
17788
+ function uniqueAsks(asks, limit, maxLen) {
17789
+ const out = [];
17790
+ const seen = /* @__PURE__ */ new Set();
17791
+ for (const raw of asks) {
17792
+ const q = clip(raw, maxLen);
17793
+ if (!q) continue;
17794
+ const key = q.toLowerCase();
17795
+ if (seen.has(key)) continue;
17796
+ seen.add(key);
17797
+ out.push(q);
17798
+ if (out.length >= limit) break;
17799
+ }
17800
+ return out;
17801
+ }
17802
+ function buildGtmSuggestedAsks(health, opts = {}) {
17803
+ const maxLen = opts.maxLen ?? DEFAULT_MAX;
17804
+ const gating = health.gating_vital_sign;
17805
+ const gatingVs = health.vital_signs.find((v) => v.vital_sign === gating) ?? health.vital_signs[0];
17806
+ const candidates = [];
17807
+ if (gatingVs) candidates.push(askForVital(gatingVs));
17808
+ const second = secondPressure(health, gating);
17809
+ if (second) candidates.push(askForVital(second));
17810
+ const scoped = scopeAsk(opts.scopeSummary, maxLen);
17811
+ if (scoped) candidates.push(scoped);
17812
+ const fromFinding = findingAsk(opts.findings, maxLen);
17813
+ if (fromFinding) candidates.push(fromFinding);
17814
+ const play = playAsk(gating);
17815
+ if (play) candidates.push(play);
17816
+ const gateLabel = VITAL_SIGN_LABELS[gating] ?? gating;
17817
+ const gate$ = gatingVs ? money(gatingVs) : null;
17818
+ candidates.push(
17819
+ gate$ ? `What should I fix first given ${gateLabel} is gating (${gate$})?` : `What should I fix first given ${gateLabel} is gating?`
17820
+ );
17821
+ if (health.total_value_at_risk != null && health.total_value_at_risk > 0) {
17822
+ candidates.push(
17823
+ `Where does the ${formatCurrency(health.total_value_at_risk)} at risk concentrate?`
17824
+ );
17825
+ }
17826
+ return uniqueAsks(candidates, 3, maxLen);
17827
+ }
17828
+ function buildMetricsSuggestedAsks(insightAsks, headline, opts = {}) {
17829
+ const maxLen = opts.maxLen ?? DEFAULT_MAX;
17830
+ const candidates = [...insightAsks.filter(Boolean)];
17831
+ const byKey = (key) => headline?.find((h) => h.metric === key);
17832
+ const nrr = byKey("nrr");
17833
+ const arr = byKey("arr");
17834
+ const coverage = byKey("pipeline_coverage");
17835
+ const win = byKey("win_rate");
17836
+ const grr = byKey("grr");
17837
+ if (nrr?.formatted) candidates.push(`Why is NRR at ${nrr.formatted}?`);
17838
+ if (grr?.formatted && grr.formatted !== nrr?.formatted) {
17839
+ candidates.push(`Is GRR at ${grr.formatted} telling the real retention story?`);
17840
+ }
17841
+ if (arr?.formatted) candidates.push(`Which deals drove ARR to ${arr.formatted}?`);
17842
+ if (coverage?.formatted) {
17843
+ candidates.push(`Is pipeline coverage of ${coverage.formatted} realistic?`);
17844
+ }
17845
+ if (win?.formatted) candidates.push(`How reliable is a ${win.formatted} win rate here?`);
17846
+ const scoped = scopeAsk(opts.scopeSummary, maxLen);
17847
+ if (scoped) candidates.push(scoped);
17848
+ candidates.push("What should I fix first in the revenue picture?");
17849
+ return uniqueAsks(candidates, 3, maxLen);
17850
+ }
17851
+ function resolveSuggestedAsks(ctx, justCompleted, suggestedAsks, health, insightAsks) {
17852
+ if (suggestedAsks && suggestedAsks.length > 0) {
17853
+ return uniqueAsks(suggestedAsks, 3, DEFAULT_MAX);
17854
+ }
17855
+ const scopeSummary = ctx.scope?.intent_summary;
17856
+ if (justCompleted === "revenue_metrics") {
17857
+ return buildMetricsSuggestedAsks(insightAsks ?? [], ctx.analysis.headline, {
17858
+ scopeSummary
17859
+ });
17860
+ }
17861
+ const fromSnapshot = health ?? ctx.snapshot.computeResult?.aggregate ?? null;
17862
+ if (fromSnapshot) {
17863
+ return buildGtmSuggestedAsks(fromSnapshot, { scopeSummary });
17864
+ }
17865
+ return [...FALLBACK_GTM_ASKS];
17866
+ }
17867
+ function asksToGhostHints(asks) {
17868
+ return asks.map((q) => {
17869
+ const bare = q.replace(/^try\s+/i, "").replace(/^"|"$/g, "");
17870
+ return `try "${bare}"`;
17871
+ });
17872
+ }
17873
+ function dedupeGhosts(hints) {
17874
+ const out = [];
17875
+ const seen = /* @__PURE__ */ new Set();
17876
+ for (const h of hints) {
17877
+ const key = h.toLowerCase();
17878
+ if (seen.has(key)) continue;
17879
+ seen.add(key);
17880
+ out.push(h);
17881
+ }
17882
+ return out;
17883
+ }
17884
+ function buildExploreTeachingGhosts(ctx, staticExplore) {
17885
+ const cached2 = ctx.teachingAskHints;
17886
+ if (cached2 && cached2.length > 0) {
17887
+ const ghosts = asksToGhostHints(cached2);
17888
+ const gating = ctx.snapshot.computeResult?.aggregate.gating_vital_sign;
17889
+ if (gating) ghosts.push(`try /deepdive ${gating}`);
17890
+ ghosts.push('try "how should we fix this?"');
17891
+ return dedupeGhosts(ghosts);
17892
+ }
17893
+ const health = ctx.snapshot.computeResult?.aggregate;
17894
+ if (health) {
17895
+ const asks = buildGtmSuggestedAsks(health, {
17896
+ scopeSummary: ctx.scope?.intent_summary
17897
+ });
17898
+ return dedupeGhosts([
17899
+ ...asksToGhostHints(asks),
17900
+ `try /deepdive ${health.gating_vital_sign}`,
17901
+ 'try "how should we fix this?"'
17902
+ ]);
17903
+ }
17904
+ if (ctx.analysis.completed.includes("revenue_metrics") && ctx.analysis.headline?.length) {
17905
+ const asks = buildMetricsSuggestedAsks([], ctx.analysis.headline, {
17906
+ scopeSummary: ctx.scope?.intent_summary
17907
+ });
17908
+ return dedupeGhosts([...asksToGhostHints(asks), 'try "how should we fix this?"']);
17909
+ }
17910
+ return staticExplore;
17911
+ }
17912
+ var DEFAULT_MAX, FALLBACK_GTM_ASKS;
17913
+ var init_suggested_asks = __esm({
17914
+ "src/conversation/suggested-asks.ts"() {
17915
+ "use strict";
17916
+ init_formatters();
17917
+ init_playbook();
17918
+ DEFAULT_MAX = 72;
17919
+ FALLBACK_GTM_ASKS = [
17920
+ "Which deals are stuck the longest?",
17921
+ "Where are we losing leads in the handoff?",
17922
+ "What should I fix first?"
17923
+ ];
17924
+ }
17925
+ });
17926
+
17232
17927
  // src/metrics/companion.ts
17233
17928
  import chalk13 from "chalk";
17234
17929
  function getCompanionRecommendation(input) {
@@ -17271,23 +17966,26 @@ function printCompanionBanner(invoked, primary) {
17271
17966
  }
17272
17967
  }
17273
17968
  function printCompanionFooter(ctx, companion, options = { justCompleted: "gtm_health" }) {
17274
- const { justCompleted, suggestedAsks } = options;
17969
+ const { justCompleted, suggestedAsks, health, insightAsks } = options;
17275
17970
  const completed = new Set(ctx.analysis.completed);
17276
17971
  printAnalysisComplete(justCompleted);
17277
- console.log();
17278
- console.log(" " + chalk13.bold("Try asking"));
17279
- const defaults = justCompleted === "revenue_metrics" ? [
17280
- "Why is NRR showing 100%?",
17281
- "Which deals drove ARR this quarter?",
17282
- "Is our pipeline coverage realistic?"
17283
- ] : [
17284
- "Which deals are stuck the longest?",
17285
- "Where are we losing leads in the handoff?",
17286
- "What should I fix first?"
17287
- ];
17288
- const asks = (suggestedAsks?.length ? suggestedAsks : defaults).slice(0, 3);
17289
- for (const q of asks) {
17290
- console.log(chalk13.dim(` "${q}"`));
17972
+ const showTeaching = shouldShowTeachingSuggestions(ctx);
17973
+ if (showTeaching) {
17974
+ const asks = resolveSuggestedAsks(
17975
+ ctx,
17976
+ justCompleted,
17977
+ suggestedAsks,
17978
+ health,
17979
+ insightAsks
17980
+ );
17981
+ ctx.teachingAskHints = asks;
17982
+ console.log();
17983
+ console.log(" " + chalk13.bold("Try asking"));
17984
+ for (const q of asks) {
17985
+ console.log(chalk13.dim(` "${q}"`));
17986
+ }
17987
+ } else {
17988
+ ctx.teachingAskHints = void 0;
17291
17989
  }
17292
17990
  console.log();
17293
17991
  const extras = [];
@@ -17322,6 +18020,8 @@ var init_companion = __esm({
17322
18020
  "src/metrics/companion.ts"() {
17323
18021
  "use strict";
17324
18022
  init_theme();
18023
+ init_teaching_suggestions();
18024
+ init_suggested_asks();
17325
18025
  }
17326
18026
  });
17327
18027
 
@@ -18583,7 +19283,7 @@ __export(play_outcomes_exports, {
18583
19283
  listPlayOutcomes: () => listPlayOutcomes,
18584
19284
  recordPlayOutcomes: () => recordPlayOutcomes
18585
19285
  });
18586
- import { existsSync as existsSync24, readFileSync as readFileSync20, appendFileSync as appendFileSync6 } from "fs";
19286
+ import { existsSync as existsSync25, readFileSync as readFileSync20, appendFileSync as appendFileSync6 } from "fs";
18587
19287
  import { join as join24 } from "path";
18588
19288
  import { randomUUID as randomUUID8 } from "crypto";
18589
19289
  function outcomesPath() {
@@ -18591,14 +19291,15 @@ function outcomesPath() {
18591
19291
  }
18592
19292
  function listPlayOutcomes() {
18593
19293
  const path = outcomesPath();
18594
- if (!existsSync24(path)) return [];
19294
+ if (!existsSync25(path)) return [];
18595
19295
  const out = [];
18596
19296
  for (const line of readFileSync20(path, "utf-8").split("\n")) {
18597
19297
  const trimmed = line.trim();
18598
19298
  if (!trimmed) continue;
18599
19299
  try {
18600
19300
  out.push(JSON.parse(trimmed));
18601
- } catch {
19301
+ } catch (err) {
19302
+ debugError("memory.playOutcomes.load", err, "skipped malformed line");
18602
19303
  }
18603
19304
  }
18604
19305
  return out;
@@ -18638,7 +19339,8 @@ function recordPlayOutcomes(strategy, outcomes, batchId) {
18638
19339
  try {
18639
19340
  appendFileSync6(outcomesPath(), JSON.stringify(record) + "\n");
18640
19341
  written++;
18641
- } catch {
19342
+ } catch (err) {
19343
+ debugError("memory.playOutcomes.append", err);
18642
19344
  }
18643
19345
  }
18644
19346
  }
@@ -18670,6 +19372,7 @@ var init_play_outcomes = __esm({
18670
19372
  "src/memory/play-outcomes.ts"() {
18671
19373
  "use strict";
18672
19374
  init_store();
19375
+ init_diagnostics();
18673
19376
  OUTCOMES_FILE = "play_outcomes.jsonl";
18674
19377
  }
18675
19378
  });
@@ -21863,7 +22566,9 @@ async function connectCustomEndpoint(opts) {
21863
22566
  const result = await fetchProviderModels(spec, opts.key);
21864
22567
  if (!result.ok) {
21865
22568
  if (result.status === 0) {
21866
- throw new ConnectError(`Couldn't reach ${baseUrl} \u2014 check the URL (expects an OpenAI-compatible /models endpoint).`);
22569
+ throw new ConnectError(
22570
+ `Couldn't reach ${baseUrl}${result.error ? ` (${result.error})` : ""} \u2014 check the URL (expects an OpenAI-compatible /models endpoint).`
22571
+ );
21867
22572
  }
21868
22573
  if (result.status === 401 || result.status === 403) {
21869
22574
  throw new ConnectError(
@@ -22404,6 +23109,33 @@ var init_divergence = __esm({
22404
23109
  }
22405
23110
  });
22406
23111
 
23112
+ // src/vitals/context-snapshot.ts
23113
+ function cacheHealthSnapshot(ctx, snapshot) {
23114
+ ctx.snapshot.computeResult = snapshot;
23115
+ ctx.snapshot.divergences = detectDivergences(
23116
+ snapshot.aggregate,
23117
+ snapshot.segments.map((segment) => ({
23118
+ segmentId: segment.segment.id,
23119
+ segmentName: segment.segment.name,
23120
+ result: segment.result
23121
+ }))
23122
+ ).divergences;
23123
+ return snapshot;
23124
+ }
23125
+ async function computeAndCacheHealthSnapshot(ctx) {
23126
+ return cacheHealthSnapshot(ctx, await computeFullHealth());
23127
+ }
23128
+ async function ensureHealthSnapshot(ctx) {
23129
+ return ctx.snapshot.computeResult ?? computeAndCacheHealthSnapshot(ctx);
23130
+ }
23131
+ var init_context_snapshot = __esm({
23132
+ "src/vitals/context-snapshot.ts"() {
23133
+ "use strict";
23134
+ init_divergence();
23135
+ init_health_score();
23136
+ }
23137
+ });
23138
+
22407
23139
  // src/services/strategist.ts
22408
23140
  import { createHash as createHash2 } from "crypto";
22409
23141
  function serializeGapAudit(audit) {
@@ -22420,20 +23152,16 @@ function serializeGapAudit(audit) {
22420
23152
  return lines.join("\n");
22421
23153
  }
22422
23154
  async function prepareStrategistInputs(ctx, objective) {
22423
- let snapshot = ctx.snapshot.computeResult;
22424
- if (!snapshot) {
22425
- snapshot = await computeFullHealth();
22426
- ctx.snapshot.computeResult = snapshot;
22427
- const divInput = snapshot.segments.map((s) => ({
22428
- segmentId: s.segment.id,
22429
- segmentName: s.segment.name,
22430
- result: s.result
22431
- }));
22432
- ctx.snapshot.divergences = detectDivergences(snapshot.aggregate, divInput).divergences;
22433
- }
22434
- const audit = ctx.gapAudit ?? await refreshGapAudit(ctx).catch(() => null);
23155
+ const snapshot = await ensureHealthSnapshot(ctx);
23156
+ const audit = ctx.gapAudit ?? await refreshGapAudit(ctx).catch((err) => {
23157
+ debugError("strategist.refreshGapAudit", err);
23158
+ return null;
23159
+ });
22435
23160
  const gapAuditBlock = audit ? serializeGapAudit(audit) : "";
22436
- const memoryBlock = await Promise.resolve().then(() => (init_store2(), store_exports2)).then((m) => m.buildMemoryBlock(objective)).catch(() => "");
23161
+ const memoryBlock = await Promise.resolve().then(() => (init_store2(), store_exports2)).then((m) => m.buildMemoryBlock(objective)).catch((err) => {
23162
+ debugError("strategist.buildMemoryBlock", err);
23163
+ return "";
23164
+ });
22437
23165
  let baselineBatchId = null;
22438
23166
  try {
22439
23167
  const reading = await getLatestHealthReading();
@@ -22543,13 +23271,13 @@ var init_strategist = __esm({
22543
23271
  "use strict";
22544
23272
  init_schema();
22545
23273
  init_queries();
22546
- init_health_score();
22547
- init_divergence();
23274
+ init_context_snapshot();
22548
23275
  init_gap_audit();
22549
23276
  init_library();
22550
23277
  init_errors2();
22551
23278
  init_types2();
22552
23279
  init_formatters();
23280
+ init_diagnostics();
22553
23281
  }
22554
23282
  });
22555
23283
 
@@ -24658,84 +25386,7 @@ var init_strategist_prompt = __esm({
24658
25386
  }
24659
25387
  });
24660
25388
 
24661
- // src/ai/json-response.ts
24662
- function stripJsonFences(text) {
24663
- const trimmed = text.trim();
24664
- const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
24665
- if (fenced) return fenced[1].trim();
24666
- return trimmed.replace(/```(?:json)?\s*/gi, "").replace(/```/g, "").trim();
24667
- }
24668
- function parseJsonArrayFromText(text) {
24669
- const cleaned = stripJsonFences(text);
24670
- const start = cleaned.indexOf("[");
24671
- if (start === -1) return null;
24672
- let depth = 0;
24673
- let end = -1;
24674
- for (let i = start; i < cleaned.length; i++) {
24675
- if (cleaned[i] === "[") depth++;
24676
- else if (cleaned[i] === "]") {
24677
- depth--;
24678
- if (depth === 0) {
24679
- end = i;
24680
- break;
24681
- }
24682
- }
24683
- }
24684
- if (end === -1) return null;
24685
- try {
24686
- const parsed = JSON.parse(cleaned.slice(start, end + 1));
24687
- return Array.isArray(parsed) ? parsed : null;
24688
- } catch {
24689
- return null;
24690
- }
24691
- }
24692
- var init_json_response = __esm({
24693
- "src/ai/json-response.ts"() {
24694
- "use strict";
24695
- }
24696
- });
24697
-
24698
25389
  // src/ai/strategist-validate.ts
24699
- function parseJsonObjectFromText(text) {
24700
- const cleaned = stripJsonFences(text);
24701
- const start = cleaned.indexOf("{");
24702
- if (start === -1) return null;
24703
- let depth = 0;
24704
- let inString = false;
24705
- let escaped = false;
24706
- let end = -1;
24707
- for (let i = start; i < cleaned.length; i++) {
24708
- const ch = cleaned[i];
24709
- if (escaped) {
24710
- escaped = false;
24711
- continue;
24712
- }
24713
- if (ch === "\\") {
24714
- if (inString) escaped = true;
24715
- continue;
24716
- }
24717
- if (ch === '"') {
24718
- inString = !inString;
24719
- continue;
24720
- }
24721
- if (inString) continue;
24722
- if (ch === "{") depth++;
24723
- else if (ch === "}") {
24724
- depth--;
24725
- if (depth === 0) {
24726
- end = i;
24727
- break;
24728
- }
24729
- }
24730
- }
24731
- if (end === -1) return null;
24732
- try {
24733
- const parsed = JSON.parse(cleaned.slice(start, end + 1));
24734
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
24735
- } catch {
24736
- return null;
24737
- }
24738
- }
24739
25390
  function extractNumbers(text) {
24740
25391
  const out = [];
24741
25392
  for (const match of text.matchAll(NUMBER_RE)) {
@@ -25019,31 +25670,34 @@ function buildGroundedFallbackPlan(input) {
25019
25670
  });
25020
25671
  const workstreams = sources.map(({ play, vital }, index) => {
25021
25672
  const score = Math.round(vital.score);
25673
+ const label = VITAL_SIGN_LABELS[vital.vital_sign] ?? String(vital.vital_sign);
25022
25674
  const dollar = vital.dollar_value != null && vital.dollar_value > 0 ? `$${Math.round(vital.dollar_value).toLocaleString("en-US")}` : null;
25023
25675
  const baseline = dollar ?? String(score);
25024
25676
  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));
25025
25677
  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));
25678
+ const targetRange = `${targetLow}\u2013${targetHigh}`;
25026
25679
  const checkDate = toIso(addDays(today, 21 + index * 7));
25027
25680
  const outcome = {
25028
25681
  metric: vital.vital_sign,
25029
25682
  baseline,
25030
- target_range: `${baseline} -> ${targetLow}-${targetHigh}`,
25683
+ target_range: targetRange,
25031
25684
  check_date: checkDate,
25032
25685
  measured_by: `${vital.vital_sign} vital sign`
25033
25686
  };
25687
+ const problem = dollar ? `${label} is under pressure \u2014 ${dollar} ${vital.dollar_label ?? ""}`.trimEnd() : `${label} is under pressure (score ${score}, ${vital.status})`;
25034
25688
  return {
25035
25689
  order: index + 1,
25036
25690
  title: play.name,
25037
- problem: `${vital.vital_sign} score ${score} (${vital.status})${dollar ? ` \u2014 ${dollar} ${vital.dollar_label ?? ""}`.trimEnd() : ""}`,
25691
+ problem,
25038
25692
  rationale: index === 0 ? "Layer-order first: clean or unblock the gating vital before downstream work" : "Next in dependency order after the prior workstream",
25039
25693
  play_ids: [play.id],
25040
25694
  actions: play.steps.slice(0, 3),
25041
25695
  effort_hours: 8 + index * 4,
25042
25696
  milestones: [
25043
25697
  {
25044
- label: `Check ${vital.vital_sign} movement`,
25698
+ label: `Check ${label} movement`,
25045
25699
  due: checkDate,
25046
- verification: `${vital.vital_sign} score moves toward ${targetLow}-${targetHigh} (baseline ${baseline})`
25700
+ verification: `${label} moves toward ${targetRange} (baseline ${baseline})`
25047
25701
  }
25048
25702
  ],
25049
25703
  deliverables: [
@@ -25056,26 +25710,29 @@ function buildGroundedFallbackPlan(input) {
25056
25710
  expected_outcome: outcome,
25057
25711
  leading_indicators: [],
25058
25712
  contingency: {
25059
- trigger: `${vital.vital_sign} flat or worse at first check`,
25713
+ trigger: `${label} flat or worse at first check`,
25060
25714
  trigger_check_date: checkDate,
25061
25715
  fallback: "Descope to the single highest-dollar entity cohort and re-run /strategy review"
25062
25716
  }
25063
25717
  };
25064
25718
  });
25065
- const gating = input.gatingVitalSign ?? sources[0]?.vital.vital_sign ?? "freshness";
25719
+ const gatingSign = input.gatingVitalSign ?? sources[0]?.vital.vital_sign ?? "freshness";
25720
+ const gatingLabel = VITAL_SIGN_LABELS[gatingSign] ?? String(gatingSign);
25066
25721
  const varLabel = input.totalValueAtRisk != null && input.totalValueAtRisk > 0 ? `$${Math.round(input.totalValueAtRisk).toLocaleString("en-US")} at risk` : "material pipeline dollars at risk";
25067
25722
  const plan = {
25068
25723
  title: "Grounded recovery plan",
25069
25724
  objective: input.objective,
25070
- 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.`,
25725
+ summary_30k: `${gatingLabel} is the gating pressure (${varLabel}). Sequence ${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.`,
25071
25726
  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.",
25072
25727
  target_segment: "Whole pipeline",
25073
25728
  priority: "high",
25074
25729
  review_cadence: "Weekly",
25075
25730
  confidence: 0.45,
25076
- constraints: ["Generated without a validated LLM plan JSON \u2014 confirm capacity before staffing"],
25077
- assumptions: ["Outcome ranges are heuristic halves/increments of live vitals, not model-authored forecasts"],
25078
- risks: ["Fallback plans lack stress-test revisions \u2014 run /strategy once the engine emits valid JSON"],
25731
+ constraints: ["Confirm team capacity before staffing these workstreams"],
25732
+ assumptions: [
25733
+ "Outcome ranges are heuristic estimates from live vitals, not forecast models"
25734
+ ],
25735
+ risks: ["Ranges may shift after the first review \u2014 refine the plan then"],
25079
25736
  workstreams
25080
25737
  };
25081
25738
  return {
@@ -25090,6 +25747,7 @@ var init_strategist_validate = __esm({
25090
25747
  "src/ai/strategist-validate.ts"() {
25091
25748
  "use strict";
25092
25749
  init_playbook();
25750
+ init_formatters();
25093
25751
  init_health_score();
25094
25752
  init_json_response();
25095
25753
  NUMBER_RE = /\$?\s*(\d[\d,]*\.?\d*)\s*(m|k|b|million|thousand|billion)?\b/gi;
@@ -26766,16 +27424,25 @@ function renderCraftHandoffMarkdown(opts) {
26766
27424
  lines.push("");
26767
27425
  lines.push(plan.objective);
26768
27426
  lines.push("");
26769
- lines.push(renderConstraintHeading(opts.constraintLine).trimEnd());
26770
- lines.push("");
26771
- lines.push(renderScopeHeading(plan.constraints, opts.outOfScope).trimEnd());
26772
- lines.push("");
27427
+ const constraint = renderConstraintHeading(opts.constraintLine).trimEnd();
27428
+ if (constraint) {
27429
+ lines.push(constraint);
27430
+ lines.push("");
27431
+ }
27432
+ const scope = renderScopeHeading(plan.constraints, opts.outOfScope).trimEnd();
27433
+ if (scope) {
27434
+ lines.push(scope);
27435
+ lines.push("");
27436
+ }
26773
27437
  lines.push("## Hypothesis");
26774
27438
  lines.push("");
26775
27439
  lines.push(plan.hypothesis);
26776
27440
  lines.push("");
26777
- lines.push(renderKilledAlternativeLine(opts.killedAlternative));
26778
- lines.push("");
27441
+ const killed = renderKilledAlternativeLine(opts.killedAlternative);
27442
+ if (killed) {
27443
+ lines.push(killed);
27444
+ lines.push("");
27445
+ }
26779
27446
  lines.push(renderEffortHeading(plan.workstreams).trimEnd());
26780
27447
  lines.push("");
26781
27448
  lines.push("## Workstreams");
@@ -26784,14 +27451,16 @@ function renderCraftHandoffMarkdown(opts) {
26784
27451
  lines.push(`### ${ws.order}. ${ws.title}`);
26785
27452
  lines.push("");
26786
27453
  lines.push(`- Problem: ${ws.problem}`);
26787
- lines.push(`- Why this order: ${ws.rationale}`);
27454
+ if (ws.rationale && !isGenericWorkstreamRationale(ws.rationale)) {
27455
+ lines.push(`- Why this order: ${ws.rationale}`);
27456
+ }
26788
27457
  if (ws.actions[0]) lines.push(`- First action (48h): ${ws.actions[0]}`);
26789
27458
  lines.push(`- Effort: ~${Math.round(ws.effort_hours)} team-hours`);
26790
27459
  lines.push(
26791
- `- Exam: ${ws.expected_outcome.metric} ${ws.expected_outcome.baseline} -> ${ws.expected_outcome.target_range} by ${ws.expected_outcome.check_date} (${ws.expected_outcome.measured_by})`
27460
+ `- Exam: ${ws.expected_outcome.metric} ${ws.expected_outcome.baseline} \u2192 ${ws.expected_outcome.target_range} by ${ws.expected_outcome.check_date} (${ws.expected_outcome.measured_by})`
26792
27461
  );
26793
27462
  lines.push(
26794
- `- Contingency: if ${ws.contingency.trigger} (check ${ws.contingency.trigger_check_date}) -> ${ws.contingency.fallback}`
27463
+ `- Contingency: if ${ws.contingency.trigger} (check ${ws.contingency.trigger_check_date}) \u2192 ${ws.contingency.fallback}`
26795
27464
  );
26796
27465
  lines.push("");
26797
27466
  }
@@ -26801,33 +27470,22 @@ function renderCraftHandoffMarkdown(opts) {
26801
27470
  lines.push(opts.roundtableDigest);
26802
27471
  lines.push("");
26803
27472
  }
26804
- if (plan.risks.length > 0) {
27473
+ const risks = filterOperatorLines(plan.risks);
27474
+ if (risks.length > 0) {
26805
27475
  lines.push("## Risks");
26806
27476
  lines.push("");
26807
- for (const r of plan.risks) lines.push(`- ${r}`);
27477
+ for (const r of risks) lines.push(`- ${r}`);
26808
27478
  lines.push("");
26809
27479
  }
26810
- if (plan.assumptions.length > 0) {
27480
+ const assumptions = filterOperatorLines(plan.assumptions);
27481
+ if (assumptions.length > 0) {
26811
27482
  lines.push("## Assumptions");
26812
27483
  lines.push("");
26813
- for (const a of plan.assumptions) lines.push(`- ${a}`);
27484
+ for (const a of assumptions) lines.push(`- ${a}`);
26814
27485
  lines.push("");
26815
27486
  }
26816
27487
  lines.push(renderReviewHeading({ cadence: plan.review_cadence, slug: opts.slug }).trimEnd());
26817
27488
  lines.push("");
26818
- if (opts.journalPath) {
26819
- lines.push("## Craft log");
26820
- lines.push("");
26821
- lines.push(`Status: ${opts.status}`);
26822
- lines.push(`Log: ${opts.journalPath}`);
26823
- if (opts.libraryPath) lines.push(`Strategy library: ${opts.libraryPath}`);
26824
- lines.push("");
26825
- } else if (opts.libraryPath) {
26826
- lines.push("## Strategy library");
26827
- lines.push("");
26828
- lines.push(opts.libraryPath);
26829
- lines.push("");
26830
- }
26831
27489
  return lines.join("\n");
26832
27490
  }
26833
27491
  function writeCraftPlanHandoff(opts) {
@@ -26858,6 +27516,7 @@ var init_handoff = __esm({
26858
27516
  "use strict";
26859
27517
  init_exports_registry();
26860
27518
  init_redact_write();
27519
+ init_strategy_signal();
26861
27520
  init_library();
26862
27521
  init_store3();
26863
27522
  }
@@ -27063,42 +27722,38 @@ function printWrapped(text, width, prefix = INDENT, style) {
27063
27722
  }
27064
27723
  }
27065
27724
  function outcomeLine(outcome) {
27066
- return `${chalk17.bold(outcome.metric)}: ${outcome.baseline} ${chalk17.dim("->")} ${chalk17.bold(outcome.target_range)} ${chalk17.dim(`by ${outcome.check_date} \xB7 ${outcome.measured_by}`)}`;
27725
+ return `${chalk17.bold(outcome.metric)}: ${outcome.baseline} ${chalk17.dim("\u2192")} ${chalk17.bold(outcome.target_range)} ${chalk17.dim(`by ${outcome.check_date}`)}`;
27067
27726
  }
27068
27727
  function printWorkstream(ws, width) {
27069
- const plays = ws.play_ids.length > 0 ? chalk17.dim(` Play: ${ws.play_ids.join(", ")}`) : "";
27070
- console.log(`${INDENT}${paint("accent", `${ws.order}.`)} ${chalk17.bold(ws.title)}${plays}`);
27728
+ console.log(`${INDENT}${paint("accent", `${ws.order}.`)} ${chalk17.bold(ws.title)}`);
27071
27729
  printWrapped(ws.problem, width - 5, INDENT + " ", (s) => chalk17.dim(s));
27072
- if (ws.rationale) {
27073
- printWrapped(`Reason: ${ws.rationale}`, width - 5, INDENT + " ", (s) => chalk17.dim(s));
27074
- }
27075
27730
  console.log(`${INDENT} ${outcomeLine(ws.expected_outcome)}`);
27076
27731
  for (const li of ws.leading_indicators) {
27077
27732
  console.log(`${INDENT} ${chalk17.dim("Lead:")} ${outcomeLine(li)}`);
27078
27733
  }
27734
+ if (ws.actions.length > 0) {
27735
+ console.log(`${INDENT} ${chalk17.dim("First steps")}`);
27736
+ for (const action of ws.actions.slice(0, 3)) {
27737
+ printWrapped(`- ${action}`, width - 7, INDENT + " ", (s) => chalk17.dim(s));
27738
+ }
27739
+ }
27079
27740
  if (ws.milestones.length > 0) {
27080
27741
  console.log(`${INDENT} ${chalk17.dim("Milestones")}`);
27081
27742
  for (const m of ws.milestones) {
27082
- console.log(`${INDENT} ${paint("accent", m.due)} ${m.label} ${chalk17.dim(`(verify: ${m.verification})`)}`);
27743
+ console.log(`${INDENT} ${paint("accent", m.due)} ${m.label}`);
27083
27744
  }
27084
27745
  }
27085
27746
  if (ws.deliverables.length > 0) {
27086
27747
  console.log(`${INDENT} ${chalk17.dim("Deliverables")}`);
27087
27748
  for (const d of ws.deliverables) {
27088
- console.log(`${INDENT} ${chalk17.dim("[ ]")} ${d.label} ${chalk17.dim(`(${d.kind.replace("_", " ")} \xB7 due ${d.due})`)}`);
27089
- }
27090
- }
27091
- if (ws.actions.length > 0) {
27092
- console.log(`${INDENT} ${chalk17.dim("First steps")}`);
27093
- for (const action of ws.actions.slice(0, 4)) {
27094
- printWrapped(`- ${action}`, width - 7, INDENT + " ", (s) => chalk17.dim(s));
27749
+ console.log(`${INDENT} ${chalk17.dim("[ ]")} ${d.label} ${chalk17.dim(`due ${d.due}`)}`);
27095
27750
  }
27096
27751
  }
27097
27752
  printWrapped(
27098
27753
  `If ${ws.contingency.trigger} (check ${ws.contingency.trigger_check_date}), then ${ws.contingency.fallback}`,
27099
27754
  width - 5,
27100
27755
  INDENT + " ",
27101
- (s) => chalk17.hex("#eab308")(s)
27756
+ (s) => chalk17.dim(s)
27102
27757
  );
27103
27758
  console.log(`${INDENT} ${chalk17.dim(`~${Math.round(ws.effort_hours)} team hours`)}`);
27104
27759
  console.log();
@@ -27110,31 +27765,34 @@ function printStrategyBrief(plan, stats) {
27110
27765
  `${INDENT}${chalk17.bold(`Strategy brief \u2014 ${plan.title}`)} ${chalk17.dim(`confidence ${plan.confidence.toFixed(2)} \xB7 ${plan.priority} priority \xB7 review ${plan.review_cadence.toLowerCase()}`)}`
27111
27766
  );
27112
27767
  console.log(INDENT + chalk17.dim(hr(width)));
27113
- printWrapped(`Objective: ${plan.objective}`, width, INDENT, (s) => paint("accent", s));
27114
- console.log();
27115
- console.log(`${INDENT}${chalk17.dim("Summary")}`);
27768
+ console.log(`${INDENT}${chalk17.dim("The Call")}`);
27116
27769
  printWrapped(plan.summary_30k, width);
27117
27770
  console.log();
27771
+ printWrapped(`Objective: ${plan.objective}`, width, INDENT, (s) => paint("accent", s));
27772
+ console.log();
27118
27773
  for (const ws of plan.workstreams) {
27119
27774
  printWorkstream(ws, width);
27120
27775
  }
27121
- if (plan.constraints.length > 0) {
27776
+ const constraints = filterOperatorLines(plan.constraints);
27777
+ if (constraints.length > 0) {
27122
27778
  console.log(`${INDENT}${chalk17.dim("Constraints")}`);
27123
- for (const c of plan.constraints) {
27779
+ for (const c of constraints) {
27124
27780
  printWrapped(`- ${c}`, width - 2, INDENT, (s) => chalk17.dim(s));
27125
27781
  }
27126
27782
  console.log();
27127
27783
  }
27128
- if (plan.assumptions.length > 0) {
27784
+ const assumptions = filterOperatorLines(plan.assumptions);
27785
+ if (assumptions.length > 0) {
27129
27786
  console.log(`${INDENT}${chalk17.dim("Assumptions (not verified, not targets)")}`);
27130
- for (const a of plan.assumptions) {
27787
+ for (const a of assumptions) {
27131
27788
  printWrapped(`- ${a}`, width - 2, INDENT, (s) => chalk17.dim(s));
27132
27789
  }
27133
27790
  console.log();
27134
27791
  }
27135
- if (plan.risks.length > 0) {
27792
+ const risks = filterOperatorLines(plan.risks);
27793
+ if (risks.length > 0) {
27136
27794
  console.log(`${INDENT}${chalk17.dim("Risks")}`);
27137
- for (const r of plan.risks) {
27795
+ for (const r of risks) {
27138
27796
  printWrapped(`- ${r}`, width - 2, INDENT, (s) => chalk17.dim(s));
27139
27797
  }
27140
27798
  console.log();
@@ -27146,6 +27804,12 @@ function printStrategyBrief(plan, stats) {
27146
27804
  console.log(`${INDENT}${coverageStyled}${chalk17.dim(` \xB7 ~${Math.round(totalHours)} total team hours across ${plan.workstreams.length} workstream${plan.workstreams.length === 1 ? "" : "s"}`)}`);
27147
27805
  console.log();
27148
27806
  }
27807
+ function printStrategyBriefFooter(notices, meta) {
27808
+ if (usedGroundedFallback(notices)) {
27809
+ console.log(INDENT + chalk17.dim(STRATEGY_FALLBACK_BANNER));
27810
+ }
27811
+ printLlmAttribution(meta);
27812
+ }
27149
27813
  function printCraftWrapUp(opts) {
27150
27814
  console.log();
27151
27815
  if (opts.status === "ready") {
@@ -27177,15 +27841,14 @@ function printCraftWrapUp(opts) {
27177
27841
  }
27178
27842
  console.log();
27179
27843
  }
27180
- function isCraftRoundNotice(text) {
27181
- return /^craft \d+\/\d+/.test(text);
27182
- }
27183
27844
  var INDENT;
27184
27845
  var init_strategy_brief = __esm({
27185
27846
  "src/output/strategy-brief.ts"() {
27186
27847
  "use strict";
27187
27848
  init_theme();
27188
27849
  init_layout();
27850
+ init_strategy_signal();
27851
+ init_llm_attribution();
27189
27852
  INDENT = " ";
27190
27853
  }
27191
27854
  });
@@ -27548,10 +28211,7 @@ async function runStrategistSession(ctx) {
27548
28211
  return;
27549
28212
  }
27550
28213
  printStrategyBrief(plan, stats);
27551
- for (const notice of notices.slice(0, 6)) {
27552
- console.log(" " + chalk18.dim(notice));
27553
- }
27554
- printLlmAttribution(meta);
28214
+ printStrategyBriefFooter(notices, meta);
27555
28215
  console.log();
27556
28216
  let saved = false;
27557
28217
  if (ctx.rl) {
@@ -27600,14 +28260,7 @@ async function ensureSnapshot(ctx) {
27600
28260
  if (ctx.snapshot.computeResult) return ctx.snapshot.computeResult;
27601
28261
  const spinner = makeSpinner("Reading latest vitals\u2026");
27602
28262
  try {
27603
- const snapshot = await computeFullHealth();
27604
- ctx.snapshot.computeResult = snapshot;
27605
- const divInput = snapshot.segments.map((s) => ({
27606
- segmentId: s.segment.id,
27607
- segmentName: s.segment.name,
27608
- result: s.result
27609
- }));
27610
- ctx.snapshot.divergences = detectDivergences(snapshot.aggregate, divInput).divergences;
28263
+ const snapshot = await computeAndCacheHealthSnapshot(ctx);
27611
28264
  spinner.stop();
27612
28265
  return snapshot;
27613
28266
  } catch {
@@ -27624,10 +28277,7 @@ function printCraftReplResult(ctx, result, objective) {
27624
28277
  } else {
27625
28278
  console.log(" " + chalk18.dim("(No plan produced)"));
27626
28279
  }
27627
- for (const notice of result.notices.slice(0, 8)) {
27628
- if (!isCraftRoundNotice(notice)) console.log(" " + chalk18.dim(notice));
27629
- }
27630
- printLlmAttribution(result.usage);
28280
+ printStrategyBriefFooter(result.notices, result.usage);
27631
28281
  if (result.library_path) creditStrategySession(ctx);
27632
28282
  printCraftWrapUp({
27633
28283
  status: result.status,
@@ -27736,13 +28386,11 @@ var init_strategist_flow = __esm({
27736
28386
  init_repl_api();
27737
28387
  init_theme();
27738
28388
  init_prompts();
27739
- init_health_score();
27740
- init_divergence();
28389
+ init_context_snapshot();
27741
28390
  init_strategist();
27742
28391
  init_strategist2();
27743
28392
  init_strategist_run();
27744
28393
  init_strategy_brief();
27745
- init_llm_attribution();
27746
28394
  init_time_bank();
27747
28395
  init_formatters();
27748
28396
  init_store3();
@@ -27756,14 +28404,24 @@ var init_strategist_flow = __esm({
27756
28404
  });
27757
28405
 
27758
28406
  // src/conversation/ghost-hints.ts
27759
- function ghostExamplesForPhase(phase, limit = 2) {
27760
- const hints = PHASE_GHOST_HINTS[phase] ?? [];
28407
+ function ghostHintsForPhase(phase, ctx) {
28408
+ if (ctx && !shouldShowTeachingSuggestions(ctx)) return [];
28409
+ const staticHints = PHASE_GHOST_HINTS[phase] ?? [];
28410
+ if (phase === "explore" && ctx) {
28411
+ return buildExploreTeachingGhosts(ctx, staticHints);
28412
+ }
28413
+ return staticHints;
28414
+ }
28415
+ function ghostExamplesForPhase(phase, limit = 2, ctx) {
28416
+ const hints = ghostHintsForPhase(phase, ctx);
27761
28417
  return hints.slice(0, limit).map((h) => h.replace(/^try\s+/i, ""));
27762
28418
  }
27763
28419
  var PHASE_GHOST_HINTS;
27764
28420
  var init_ghost_hints = __esm({
27765
28421
  "src/conversation/ghost-hints.ts"() {
27766
28422
  "use strict";
28423
+ init_teaching_suggestions();
28424
+ init_suggested_asks();
27767
28425
  PHASE_GHOST_HINTS = {
27768
28426
  orient: [
27769
28427
  'try "pipeline health"',
@@ -27801,7 +28459,8 @@ function buildSituationalAwarenessBlock(ctx, opts = {}) {
27801
28459
  const action = resolveRecommendedAction(ctx);
27802
28460
  const examples = ghostExamplesForPhase(
27803
28461
  phase === "think" || phase === "strategize" || phase === "deliver" ? "explore" : phase,
27804
- 2
28462
+ 2,
28463
+ ctx
27805
28464
  );
27806
28465
  const lines = [
27807
28466
  "WHERE YOU ARE IN NTRP:",
@@ -28627,14 +29286,7 @@ async function runThinkTurn(input, ctx) {
28627
29286
  const metricsFirst = prefersMetricsFirstContext(ctx);
28628
29287
  const spinner2 = makeSpinner(metricsFirst ? "Loading session context\u2026" : "Computing vital signs\u2026");
28629
29288
  try {
28630
- snapshot = await computeFullHealth();
28631
- ctx.snapshot.computeResult = snapshot;
28632
- const divInput = snapshot.segments.map((s) => ({
28633
- segmentId: s.segment.id,
28634
- segmentName: s.segment.name,
28635
- result: s.result
28636
- }));
28637
- ctx.snapshot.divergences = detectDivergences(snapshot.aggregate, divInput).divergences;
29289
+ snapshot = await computeAndCacheHealthSnapshot(ctx);
28638
29290
  spinner2.succeed(metricsFirst ? "Session context ready" : "Health snapshot ready");
28639
29291
  } catch (err) {
28640
29292
  spinner2.fail("Could not compute health snapshot");
@@ -28645,7 +29297,10 @@ async function runThinkTurn(input, ctx) {
28645
29297
  }
28646
29298
  }
28647
29299
  console.log();
28648
- const memoryBlock = await buildMemoryBlock(input).catch(() => "");
29300
+ const memoryBlock = await buildMemoryBlock(input).catch((err) => {
29301
+ debugError("think.buildMemoryBlock", err);
29302
+ return "";
29303
+ });
28649
29304
  const spinner = makeSpinner("Thinking with you\u2026");
28650
29305
  let lastAnswer = "";
28651
29306
  let rawHistory = [];
@@ -28747,13 +29402,13 @@ var init_think = __esm({
28747
29402
  init_agentic_loop();
28748
29403
  init_thread();
28749
29404
  init_store2();
28750
- init_health_score();
28751
- init_divergence();
29405
+ init_context_snapshot();
28752
29406
  init_repl_api();
28753
29407
  init_theme();
28754
29408
  init_markdown();
28755
29409
  init_session_analysis();
28756
29410
  init_time_bank();
29411
+ init_diagnostics();
28757
29412
  }
28758
29413
  });
28759
29414
 
@@ -30498,14 +31153,7 @@ async function runNaturalLanguage(input, ctx) {
30498
31153
  const metricsFirst = prefersMetricsFirstContext(ctx);
30499
31154
  const spinner2 = makeSpinner(metricsFirst ? "Loading session context\u2026" : "Computing vital signs\u2026");
30500
31155
  try {
30501
- snapshot = await computeFullHealth();
30502
- ctx.snapshot.computeResult = snapshot;
30503
- const divInput = snapshot.segments.map((s) => ({
30504
- segmentId: s.segment.id,
30505
- segmentName: s.segment.name,
30506
- result: s.result
30507
- }));
30508
- ctx.snapshot.divergences = detectDivergences(snapshot.aggregate, divInput).divergences;
31156
+ snapshot = await computeAndCacheHealthSnapshot(ctx);
30509
31157
  spinner2.succeed(metricsFirst ? "Session context ready" : "Health snapshot ready");
30510
31158
  } catch (err) {
30511
31159
  spinner2.fail("Could not compute health snapshot");
@@ -30516,7 +31164,10 @@ async function runNaturalLanguage(input, ctx) {
30516
31164
  }
30517
31165
  }
30518
31166
  console.log();
30519
- const memoryBlock = await buildMemoryBlock(input).catch(() => "");
31167
+ const memoryBlock = await buildMemoryBlock(input).catch((err) => {
31168
+ debugError("nl.buildMemoryBlock", err);
31169
+ return "";
31170
+ });
30520
31171
  const spinner = makeSpinner("Thinking\u2026");
30521
31172
  let lastAnswer = "";
30522
31173
  let rawHistory = [];
@@ -30641,14 +31292,14 @@ var init_nl = __esm({
30641
31292
  init_explore_mode();
30642
31293
  init_thread();
30643
31294
  init_store2();
30644
- init_health_score();
30645
- init_divergence();
31295
+ init_context_snapshot();
30646
31296
  init_repl_api();
30647
31297
  init_theme();
30648
31298
  init_markdown();
30649
31299
  init_smoke_protocol();
30650
31300
  init_session_analysis();
30651
31301
  init_time_bank();
31302
+ init_diagnostics();
30652
31303
  }
30653
31304
  });
30654
31305
 
@@ -31415,22 +32066,23 @@ var init_terminal = __esm({
31415
32066
  });
31416
32067
 
31417
32068
  // src/demo/taxonomy-cache.ts
31418
- import { readFileSync as readFileSync21, writeFileSync as writeFileSync17, existsSync as existsSync25, mkdirSync as mkdirSync14, unlinkSync as unlinkSync5 } from "fs";
31419
- import { homedir as homedir7 } from "os";
32069
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync17, existsSync as existsSync26, mkdirSync as mkdirSync14, unlinkSync as unlinkSync5 } from "fs";
32070
+ import { homedir as homedir6 } from "os";
31420
32071
  import { join as join27 } from "path";
31421
32072
  function ensureDir7() {
31422
- if (!existsSync25(NTRP_DIR4)) {
32073
+ if (!existsSync26(NTRP_DIR4)) {
31423
32074
  mkdirSync14(NTRP_DIR4, { recursive: true });
31424
32075
  }
31425
32076
  }
31426
32077
  function loadCachedTaxonomy(profile) {
31427
- if (!existsSync25(TAXONOMY_PATH)) return null;
32078
+ if (!existsSync26(TAXONOMY_PATH)) return null;
31428
32079
  try {
31429
32080
  const parsed = JSON.parse(readFileSync21(TAXONOMY_PATH, "utf-8"));
31430
32081
  if (!parsed || typeof parsed !== "object") return null;
31431
32082
  if (parsed.profile_updated_at !== profile.updated_at) return null;
31432
32083
  return parsed;
31433
- } catch {
32084
+ } catch (err) {
32085
+ debugError("demo.taxonomyCache.load", err, TAXONOMY_PATH);
31434
32086
  return null;
31435
32087
  }
31436
32088
  }
@@ -31439,10 +32091,11 @@ function saveCachedTaxonomy(taxonomy) {
31439
32091
  writeFileSync17(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
31440
32092
  }
31441
32093
  function invalidateTaxonomy() {
31442
- if (existsSync25(TAXONOMY_PATH)) {
32094
+ if (existsSync26(TAXONOMY_PATH)) {
31443
32095
  try {
31444
32096
  unlinkSync5(TAXONOMY_PATH);
31445
- } catch {
32097
+ } catch (err) {
32098
+ debugError("demo.taxonomyCache.invalidate", err, TAXONOMY_PATH);
31446
32099
  }
31447
32100
  }
31448
32101
  }
@@ -31450,7 +32103,8 @@ var NTRP_DIR4, TAXONOMY_PATH;
31450
32103
  var init_taxonomy_cache = __esm({
31451
32104
  "src/demo/taxonomy-cache.ts"() {
31452
32105
  "use strict";
31453
- NTRP_DIR4 = join27(homedir7(), ".ntrp");
32106
+ init_diagnostics();
32107
+ NTRP_DIR4 = join27(homedir6(), ".ntrp");
31454
32108
  TAXONOMY_PATH = join27(NTRP_DIR4, "demo-taxonomy.json");
31455
32109
  }
31456
32110
  });
@@ -31473,7 +32127,7 @@ async function buildDemoTaxonomy(profile, ctx) {
31473
32127
 
31474
32128
  Produce the demo taxonomy now as STRICT JSON matching the schema in the system prompt.`;
31475
32129
  const { text } = await llmCompleteText("demo_taxonomy", SYSTEM_PROMPT3, userMessage, 4096, ctx);
31476
- const cleaned = stripFences2(text);
32130
+ const cleaned = stripJsonFences(text);
31477
32131
  let parsed;
31478
32132
  try {
31479
32133
  parsed = JSON.parse(cleaned);
@@ -31482,12 +32136,6 @@ Produce the demo taxonomy now as STRICT JSON matching the schema in the system p
31482
32136
  }
31483
32137
  return validateTaxonomy(parsed, profile);
31484
32138
  }
31485
- function stripFences2(text) {
31486
- const trimmed = text.trim();
31487
- const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
31488
- if (fenced) return fenced[1].trim();
31489
- return trimmed;
31490
- }
31491
32139
  function validateTaxonomy(raw, profile) {
31492
32140
  const strArray3 = (key, min = 3) => {
31493
32141
  const v = raw[key];
@@ -31609,6 +32257,7 @@ var init_demo_taxonomy = __esm({
31609
32257
  "use strict";
31610
32258
  init_repl_api();
31611
32259
  init_complete();
32260
+ init_json_response();
31612
32261
  SYSTEM_PROMPT3 = `You are a senior GTM market researcher. Given a company profile, produce a taxonomy of synthetic-but-plausible entities a pipeline-health tool would show this operator in their demo data.
31613
32262
 
31614
32263
  CORE PRINCIPLES:
@@ -31909,7 +32558,7 @@ __export(inbox_setup_exports, {
31909
32558
  shouldOfferInboxSkillSetup: () => shouldOfferInboxSkillSetup
31910
32559
  });
31911
32560
  import chalk30 from "chalk";
31912
- import { existsSync as existsSync26 } from "fs";
32561
+ import { existsSync as existsSync27 } from "fs";
31913
32562
  function markDemoOffered() {
31914
32563
  setConfigValue("ai-inbox-nudge-seen", "true");
31915
32564
  }
@@ -31941,7 +32590,7 @@ function printSkipHint(beat) {
31941
32590
  async function reuseInboxFolderIfPresent(session, beat, folderPath) {
31942
32591
  if (getAiInboxDir()) return false;
31943
32592
  const candidates = folderPath ? [folderPath] : [.../* @__PURE__ */ new Set([defaultAiInboxDir(), legacyAiInboxDir()])];
31944
- const existing = candidates.find((p) => existsSync26(p));
32593
+ const existing = candidates.find((p) => existsSync27(p));
31945
32594
  if (!existing) return false;
31946
32595
  console.log(" " + chalk30.dim("Pickup folder still on disk: ") + existing);
31947
32596
  const reuse = await session.confirm("Reuse this pickup folder?", true);
@@ -32047,7 +32696,7 @@ __export(ingest_exports, {
32047
32696
  handler: () => handler2
32048
32697
  });
32049
32698
  import chalk31 from "chalk";
32050
- import { readFileSync as readFileSync22, existsSync as existsSync27 } from "fs";
32699
+ import { readFileSync as readFileSync22, existsSync as existsSync28 } from "fs";
32051
32700
  import { basename as basename7 } from "path";
32052
32701
  async function handler2(args, ctx) {
32053
32702
  const { positional, flags } = parseArgs2(args, [
@@ -32071,7 +32720,7 @@ async function handler2(args, ctx) {
32071
32720
  console.error(chalk31.dim(" /ingest --demo [--scenario <name>]"));
32072
32721
  process.exit(1);
32073
32722
  }
32074
- if (!existsSync27(file)) {
32723
+ if (!existsSync28(file)) {
32075
32724
  console.error(chalk31.red(` File not found: ${file}`));
32076
32725
  process.exit(1);
32077
32726
  }
@@ -32260,8 +32909,8 @@ function renderMetricsReport(ctx, metrics, coverage, sourceType, options = {}) {
32260
32909
  printFindings(options.findings);
32261
32910
  }
32262
32911
  if (options.interactive !== false) {
32263
- const suggestedAsks = deterministic.map((i) => i.suggested_ask).filter((q) => !!q);
32264
- printMetricsNextSteps(ctx, options.companion ?? null, suggestedAsks);
32912
+ const insightAsks = deterministic.map((i) => i.suggested_ask).filter((q) => !!q);
32913
+ printMetricsNextSteps(ctx, options.companion ?? null, insightAsks);
32265
32914
  }
32266
32915
  }
32267
32916
  function wrapInsight(text) {
@@ -32305,10 +32954,10 @@ function formatShort(n) {
32305
32954
  if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
32306
32955
  return String(Math.round(n));
32307
32956
  }
32308
- function printMetricsNextSteps(ctx, companion, suggestedAsks) {
32957
+ function printMetricsNextSteps(ctx, companion, insightAsks) {
32309
32958
  printCompanionFooter(ctx, companion, {
32310
32959
  justCompleted: "revenue_metrics",
32311
- suggestedAsks
32960
+ insightAsks
32312
32961
  });
32313
32962
  }
32314
32963
  var GROUP_ORDER;
@@ -32435,7 +33084,16 @@ async function handler3(args, ctx) {
32435
33084
  saveSessionState(ctx);
32436
33085
  if (!ctx.oneShot && !isStructuredOutput(ctx.execution) && !ctx.suppressCompanionFooter) {
32437
33086
  const companion = await resolveCompanionRecommendation(ctx);
32438
- printCompanionFooter(ctx, companion, { justCompleted: "gtm_health" });
33087
+ let healthResult = ctx.snapshot.computeResult?.aggregate ?? null;
33088
+ if (!healthResult) {
33089
+ const { loadLatestDiagnosis: loadLatestDiagnosis2 } = await Promise.resolve().then(() => (init_queries(), queries_exports));
33090
+ const latest = await loadLatestDiagnosis2();
33091
+ healthResult = latest?.health ?? null;
33092
+ }
33093
+ printCompanionFooter(ctx, companion, {
33094
+ justCompleted: "gtm_health",
33095
+ health: healthResult
33096
+ });
32439
33097
  }
32440
33098
  ctx.suppressCompanionFooter = false;
32441
33099
  if (!ctx.skipTimeBankDiagnoseCredit) {
@@ -32500,6 +33158,7 @@ async function runDiagnose(options, ctx) {
32500
33158
  compact: options.compact,
32501
33159
  ctx
32502
33160
  });
33161
+ ctx.snapshot.computeResult = fullResult;
32503
33162
  return buildDiagnoseSummary(fullResult.aggregate, findings);
32504
33163
  } catch (err) {
32505
33164
  const { isLlmAuthError: isLlmAuthError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
@@ -32621,7 +33280,7 @@ Research this company deeply using your training knowledge. Identify the canonic
32621
33280
  const { text } = await llmCompleteText("onboard", SYSTEM_PROMPT4, userMessage, 2048, ctx, {
32622
33281
  skipPseudonymize: true
32623
33282
  });
32624
- const cleaned = stripFences3(text);
33283
+ const cleaned = stripJsonFences(text);
32625
33284
  let parsed;
32626
33285
  try {
32627
33286
  parsed = JSON.parse(cleaned);
@@ -32634,12 +33293,6 @@ Research this company deeply using your training knowledge. Identify the canonic
32634
33293
  if (seed.company_url && !draft.company_url) draft.company_url = seed.company_url;
32635
33294
  return draft;
32636
33295
  }
32637
- function stripFences3(text) {
32638
- const trimmed = text.trim();
32639
- const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
32640
- if (fenced) return fenced[1].trim();
32641
- return trimmed;
32642
- }
32643
33296
  function validateDraft(raw) {
32644
33297
  const out = {};
32645
33298
  const str3 = (k) => {
@@ -32677,6 +33330,7 @@ var init_profile_draft = __esm({
32677
33330
  init_repl_api();
32678
33331
  init_complete();
32679
33332
  init_lexicon_seed();
33333
+ init_json_response();
32680
33334
  SYSTEM_PROMPT4 = `You are a senior GTM researcher with deep recall of the B2B / SaaS / services landscape. Given a company website (or, as a fallback, a plain company name), you produce a rich, specific profile of the business so a pipeline-health tool can tailor every answer to their reality.
32681
33335
 
32682
33336
  RESEARCH DEPTH \u2014 this is the most important part:
@@ -32726,46 +33380,6 @@ Any field you are not confident about MUST be omitted entirely (do not include i
32726
33380
  function normLabel2(s) {
32727
33381
  return s.trim().toLowerCase();
32728
33382
  }
32729
- function parseJsonObject(text) {
32730
- const cleaned = stripJsonFences(text);
32731
- const start = cleaned.indexOf("{");
32732
- if (start === -1) return null;
32733
- let depth = 0;
32734
- let inString = false;
32735
- let escaped = false;
32736
- let end = -1;
32737
- for (let i = start; i < cleaned.length; i++) {
32738
- const ch = cleaned[i];
32739
- if (escaped) {
32740
- escaped = false;
32741
- continue;
32742
- }
32743
- if (ch === "\\") {
32744
- if (inString) escaped = true;
32745
- continue;
32746
- }
32747
- if (ch === '"') {
32748
- inString = !inString;
32749
- continue;
32750
- }
32751
- if (inString) continue;
32752
- if (ch === "{") depth++;
32753
- else if (ch === "}") {
32754
- depth--;
32755
- if (depth === 0) {
32756
- end = i;
32757
- break;
32758
- }
32759
- }
32760
- }
32761
- if (end === -1) return null;
32762
- try {
32763
- const parsed = JSON.parse(cleaned.slice(start, end + 1));
32764
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
32765
- } catch {
32766
- return null;
32767
- }
32768
- }
32769
33383
  function sortQuestionRecommendedFirst(q) {
32770
33384
  const flagged = q.options.find((o) => o.recommended);
32771
33385
  const rec = flagged?.label ?? q.options[0]?.label;
@@ -32797,7 +33411,7 @@ function parseVerdict(raw) {
32797
33411
  return null;
32798
33412
  }
32799
33413
  function parseOptionSetEval(text) {
32800
- const rec = parseJsonObject(text);
33414
+ const rec = parseJsonObjectFromText(text);
32801
33415
  if (!rec) return null;
32802
33416
  if (!Array.isArray(rec.questions)) return null;
32803
33417
  const questions = [];
@@ -32960,12 +33574,6 @@ OUTPUT \u2014 STRICT JSON only, no fences, no commentary:
32960
33574
  });
32961
33575
 
32962
33576
  // src/ai/profile-clarify.ts
32963
- function stripFences4(text) {
32964
- const trimmed = text.trim();
32965
- const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
32966
- if (fenced) return fenced[1].trim();
32967
- return trimmed;
32968
- }
32969
33577
  function formatDraft(draft) {
32970
33578
  const parts = [];
32971
33579
  if (draft.industry) parts.push(`Industry: ${draft.industry}`);
@@ -32980,7 +33588,7 @@ function formatDraft(draft) {
32980
33588
  return parts.join("\n");
32981
33589
  }
32982
33590
  function parseClarifyingQuestions(text) {
32983
- const cleaned = stripFences4(text);
33591
+ const cleaned = stripJsonFences(text);
32984
33592
  let parsed;
32985
33593
  try {
32986
33594
  parsed = JSON.parse(cleaned);
@@ -33077,7 +33685,7 @@ ${draftBlock}${userBlock}${answersBlock}
33077
33685
 
33078
33686
  Emit the refined profile now as STRICT JSON.`;
33079
33687
  const { text } = await llmCompleteText("onboard", REFINE_SYSTEM_PROMPT, userMessage, 2048, ctx);
33080
- const cleaned = stripFences4(text);
33688
+ const cleaned = stripJsonFences(text);
33081
33689
  let parsed;
33082
33690
  try {
33083
33691
  parsed = JSON.parse(cleaned);
@@ -33122,6 +33730,7 @@ var init_profile_clarify = __esm({
33122
33730
  "use strict";
33123
33731
  init_repl_api();
33124
33732
  init_complete();
33733
+ init_json_response();
33125
33734
  init_option_set();
33126
33735
  CLARIFY_SYSTEM_PROMPT = `You are a senior GTM researcher helping NTRP \u2014 a pipeline-health "stethoscope" \u2014 tune its understanding of a specific company before it diagnoses their sales funnel.
33127
33736
 
@@ -33442,155 +34051,6 @@ var init_profile2 = __esm({
33442
34051
  }
33443
34052
  });
33444
34053
 
33445
- // src/conversation/onboard-tiers.ts
33446
- var onboard_tiers_exports = {};
33447
- __export(onboard_tiers_exports, {
33448
- ONBOARD_TIER_ORDER: () => ONBOARD_TIER_ORDER,
33449
- canRunDomainTier: () => canRunDomainTier,
33450
- clearOnboardTierFlags: () => clearOnboardTierFlags,
33451
- describeOnboardTier: () => describeOnboardTier,
33452
- getOnboardTierFlag: () => getOnboardTierFlag,
33453
- getOnboardTierStatus: () => getOnboardTierStatus,
33454
- listCompletedOnboardTier: () => listCompletedOnboardTier,
33455
- markDemoDataSeen: () => markDemoDataSeen,
33456
- markOnboardTierComplete: () => markOnboardTierComplete,
33457
- markProductionDataSeen: () => markProductionDataSeen,
33458
- pathLooksPresent: () => pathLooksPresent,
33459
- resetOnboardTierProgress: () => resetOnboardTierProgress,
33460
- resolveNextOnboardTier: () => resolveNextOnboardTier
33461
- });
33462
- import { existsSync as existsSync28, statSync as statSync4 } from "fs";
33463
- function flagSet(tier) {
33464
- return Boolean(getConfigValue(TIER_CONFIG_KEYS[tier]));
33465
- }
33466
- function getOnboardTierFlag(tier) {
33467
- return getConfigValue(TIER_CONFIG_KEYS[tier]);
33468
- }
33469
- function markOnboardTierComplete(...tiers) {
33470
- const at = (/* @__PURE__ */ new Date()).toISOString();
33471
- for (const tier of tiers) {
33472
- if (!flagSet(tier)) setConfigValue(TIER_CONFIG_KEYS[tier], at);
33473
- }
33474
- }
33475
- function clearOnboardTierFlags(...tiers) {
33476
- for (const tier of tiers) {
33477
- deleteConfigValue(TIER_CONFIG_KEYS[tier]);
33478
- }
33479
- }
33480
- function resetOnboardTierProgress() {
33481
- clearOnboardTierFlags(...ONBOARD_TIER_ORDER);
33482
- }
33483
- function hasProductionDataset(ctx) {
33484
- const source = ctx?.dataset?.source;
33485
- if (source && !source.startsWith("demo:")) {
33486
- if (source.startsWith("/") || /^[A-Za-z]:[\\/]/.test(source) || source.startsWith("~")) {
33487
- return true;
33488
- }
33489
- if (!source.includes(":") && existsSync28(source)) return true;
33490
- if (source.startsWith("csv:") || source.startsWith("file:") || source.startsWith("folder:")) {
33491
- return true;
33492
- }
33493
- }
33494
- if (ctx?.attachments && ctx.attachments.length > 0) return true;
33495
- return flagSet("production");
33496
- }
33497
- function hasDemoExperience(ctx) {
33498
- if (flagSet("demo")) return true;
33499
- if (getPreferredDemoScenario()) return true;
33500
- const source = ctx?.dataset?.source;
33501
- if (source?.startsWith("demo:")) return true;
33502
- return false;
33503
- }
33504
- function listCompletedOnboardTier(ctx) {
33505
- const done = [];
33506
- const profileOk = flagSet("profile") || isProfileConfigured(loadProfile());
33507
- if (profileOk) done.push("profile");
33508
- else return done;
33509
- if (flagSet("domain")) done.push("domain");
33510
- else return done;
33511
- if (hasDemoExperience(ctx)) done.push("demo");
33512
- else return done;
33513
- if (hasProductionDataset(ctx) || flagSet("production")) done.push("production");
33514
- return done;
33515
- }
33516
- function resolveNextOnboardTier(ctx) {
33517
- const done = new Set(listCompletedOnboardTier(ctx));
33518
- for (const tier of ONBOARD_TIER_ORDER) {
33519
- if (!done.has(tier)) return tier;
33520
- }
33521
- return null;
33522
- }
33523
- function getOnboardTierStatus(ctx) {
33524
- const completed = listCompletedOnboardTier(ctx);
33525
- const next = resolveNextOnboardTier(ctx);
33526
- const meta = next ? TIER_META[next] : null;
33527
- return {
33528
- completed,
33529
- next,
33530
- nextLabel: meta?.label ?? null,
33531
- nextHint: meta?.hint ?? null
33532
- };
33533
- }
33534
- function describeOnboardTier(tier) {
33535
- return TIER_META[tier];
33536
- }
33537
- function canRunDomainTier() {
33538
- return isProfileConfigured(loadProfile()) && hasAnyLlmProvider();
33539
- }
33540
- function markProductionDataSeen() {
33541
- markOnboardTierComplete("production");
33542
- }
33543
- function markDemoDataSeen() {
33544
- markOnboardTierComplete("demo");
33545
- }
33546
- function pathLooksPresent(raw) {
33547
- try {
33548
- return existsSync28(raw) && (statSync4(raw).isFile() || statSync4(raw).isDirectory());
33549
- } catch {
33550
- return false;
33551
- }
33552
- }
33553
- var ONBOARD_TIER_ORDER, TIER_CONFIG_KEYS, TIER_META;
33554
- var init_onboard_tiers = __esm({
33555
- "src/conversation/onboard-tiers.ts"() {
33556
- "use strict";
33557
- init_store();
33558
- init_profile();
33559
- init_repl_api();
33560
- init_scenario_fit();
33561
- ONBOARD_TIER_ORDER = [
33562
- "profile",
33563
- "domain",
33564
- "demo",
33565
- "production"
33566
- ];
33567
- TIER_CONFIG_KEYS = {
33568
- profile: "onboard-tier-profile",
33569
- domain: "onboard-tier-domain",
33570
- demo: "onboard-tier-demo",
33571
- production: "onboard-tier-production"
33572
- };
33573
- TIER_META = {
33574
- profile: {
33575
- label: "Company profile",
33576
- hint: "Name, industry, ICP \u2014 works without an API key"
33577
- },
33578
- domain: {
33579
- label: "Domain research",
33580
- hint: "Connect a key and let NTRP research your company"
33581
- },
33582
- demo: {
33583
- label: "Sample data",
33584
- hint: "Load a fitted demo book of business"
33585
- },
33586
- production: {
33587
- label: "Your data",
33588
- hint: "Drag-drop a CSV or folder path into the REPL"
33589
- }
33590
- };
33591
- }
33592
- });
33593
-
33594
34054
  // src/conversation/voice-setup.ts
33595
34055
  var voice_setup_exports = {};
33596
34056
  __export(voice_setup_exports, {
@@ -36045,7 +36505,8 @@ async function executeApprovedAction(id) {
36045
36505
  title: typeof proposal.title === "string" ? proposal.title : "Repository export"
36046
36506
  });
36047
36507
  }
36048
- } catch {
36508
+ } catch (err) {
36509
+ debugError("actions.recordExportWrite", err);
36049
36510
  }
36050
36511
  const executionId = await insertActionExecution({
36051
36512
  proposal_id: proposal.id,
@@ -36078,6 +36539,7 @@ var init_actions = __esm({
36078
36539
  init_errors2();
36079
36540
  init_types2();
36080
36541
  init_publish();
36542
+ init_diagnostics();
36081
36543
  }
36082
36544
  });
36083
36545
 
@@ -37410,10 +37872,7 @@ function renderCraftResult(result) {
37410
37872
  console.log();
37411
37873
  console.log(" " + chalk48.dim("(No plan produced)"));
37412
37874
  }
37413
- for (const notice of result.notices.slice(0, 8)) {
37414
- console.log(" " + chalk48.dim(notice));
37415
- }
37416
- printLlmAttribution(result.usage);
37875
+ printStrategyBriefFooter(result.notices, result.usage);
37417
37876
  printCraftWrapUp({
37418
37877
  status: result.status,
37419
37878
  library_path: result.library_path,
@@ -37537,7 +37996,6 @@ var init_strategy2 = __esm({
37537
37996
  init_repl_api();
37538
37997
  init_strategist_run();
37539
37998
  init_strategy_brief();
37540
- init_llm_attribution();
37541
37999
  RESERVED_SUBCOMMANDS = /* @__PURE__ */ new Set(["ingest", "add", "list", "show", "sync", "sources", "review", "craft"]);
37542
38000
  }
37543
38001
  });
@@ -38666,18 +39124,7 @@ var init_setup2 = __esm({
38666
39124
 
38667
39125
  // src/services/ask.ts
38668
39126
  async function ensureAskSnapshot(ctx) {
38669
- let snapshot = ctx.snapshot.computeResult;
38670
- if (!snapshot) {
38671
- snapshot = await computeFullHealth();
38672
- ctx.snapshot.computeResult = snapshot;
38673
- const divInput = snapshot.segments.map((s) => ({
38674
- segmentId: s.segment.id,
38675
- segmentName: s.segment.name,
38676
- result: s.result
38677
- }));
38678
- ctx.snapshot.divergences = detectDivergences(snapshot.aggregate, divInput).divergences;
38679
- }
38680
- return snapshot;
39127
+ return ensureHealthSnapshot(ctx);
38681
39128
  }
38682
39129
  async function* streamAsk(question, ctx) {
38683
39130
  if (isSmokeProtocolTrigger(question)) {
@@ -38701,7 +39148,10 @@ async function* streamAsk(question, ctx) {
38701
39148
  await hydrateAnalysisFromPersistedState(ctx);
38702
39149
  const snapshot = await ensureAskSnapshot(ctx);
38703
39150
  const { buildMemoryBlock: buildMemoryBlock2 } = await Promise.resolve().then(() => (init_store2(), store_exports2));
38704
- const memoryBlock = await buildMemoryBlock2(question).catch(() => "");
39151
+ const memoryBlock = await buildMemoryBlock2(question).catch((err) => {
39152
+ debugError("ask.buildMemoryBlock", err);
39153
+ return "";
39154
+ });
38705
39155
  const bundle = await loadSessionAnalysisBundle();
38706
39156
  const sessionArtifact = hasAnyAnalysis(bundle) ? buildExploreContextBlock(bundle, ctx) : "";
38707
39157
  const responseMode = resolveExploreResponseMode(question, ctx, ctx.conversation.length);
@@ -38752,13 +39202,13 @@ var init_ask = __esm({
38752
39202
  "use strict";
38753
39203
  init_agentic_loop();
38754
39204
  init_explore_mode();
38755
- init_health_score();
38756
- init_divergence();
39205
+ init_context_snapshot();
38757
39206
  init_repl_api();
38758
39207
  init_context2();
38759
39208
  init_situation();
38760
39209
  init_session_analysis();
38761
39210
  init_smoke_protocol();
39211
+ init_diagnostics();
38762
39212
  }
38763
39213
  });
38764
39214
 
@@ -38950,12 +39400,6 @@ var init_metrics = __esm({
38950
39400
  });
38951
39401
 
38952
39402
  // src/ai/feedback-apply.ts
38953
- function stripFences5(text) {
38954
- const trimmed = text.trim();
38955
- const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
38956
- if (fenced) return fenced[1].trim();
38957
- return trimmed;
38958
- }
38959
39403
  function formatProfile(p) {
38960
39404
  const parts = [];
38961
39405
  parts.push(`Industry: ${p.industry}`);
@@ -38981,7 +39425,7 @@ OPERATOR FEEDBACK:
38981
39425
 
38982
39426
  Apply the feedback now as STRICT JSON.`;
38983
39427
  const { text } = await llmCompleteText("feedback", SYSTEM_PROMPT5, userMessage, 1024, ctx);
38984
- const cleaned = stripFences5(text);
39428
+ const cleaned = stripJsonFences(text);
38985
39429
  let parsed;
38986
39430
  try {
38987
39431
  parsed = JSON.parse(cleaned);
@@ -39033,6 +39477,7 @@ var init_feedback_apply = __esm({
39033
39477
  "use strict";
39034
39478
  init_repl_api();
39035
39479
  init_complete();
39480
+ init_json_response();
39036
39481
  ALLOWED_MOTIONS3 = /* @__PURE__ */ new Set(["plg", "smb_velocity", "mid_market", "enterprise"]);
39037
39482
  ALLOWED_CRMS3 = /* @__PURE__ */ new Set(["salesforce", "hubspot", "pipedrive", "other"]);
39038
39483
  ALLOWED_ENGAGEMENT3 = /* @__PURE__ */ new Set(["outreach", "salesloft", "apollo", "none"]);
@@ -39388,7 +39833,8 @@ function recordFeedback(input) {
39388
39833
  };
39389
39834
  try {
39390
39835
  appendFileSync7(join34(getMemoryDir(), "feedback.jsonl"), JSON.stringify(entry) + "\n");
39391
- } catch {
39836
+ } catch (err) {
39837
+ debugError("memory.feedback.append", err);
39392
39838
  }
39393
39839
  if (input.rating === "positive") {
39394
39840
  addFact({
@@ -39421,6 +39867,7 @@ var init_feedback2 = __esm({
39421
39867
  "use strict";
39422
39868
  init_store();
39423
39869
  init_store2();
39870
+ init_diagnostics();
39424
39871
  }
39425
39872
  });
39426
39873
 
@@ -43524,7 +43971,6 @@ async function runConversationCompute(ctx) {
43524
43971
  const summary = await diagnose(["--compact"], ctx);
43525
43972
  markLensCompleted(ctx, "gtm_health");
43526
43973
  ctx.stage = "analyzed";
43527
- ctx.snapshot.computeResult = null;
43528
43974
  invalidateGapAudit(ctx);
43529
43975
  saveSessionState(ctx);
43530
43976
  await resumeQueuedStrategist(ctx);
@@ -43593,7 +44039,7 @@ __export(ingest_chat_exports, {
43593
44039
  });
43594
44040
  import { existsSync as existsSync38, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
43595
44041
  import { basename as basename10, join as join39, resolve as resolve9 } from "path";
43596
- import { homedir as homedir8 } from "os";
44042
+ import { homedir as homedir7 } from "os";
43597
44043
  import chalk85 from "chalk";
43598
44044
  function extractFilePath(input) {
43599
44045
  const trimmed = input.trim().replace(/^["']|["']$/g, "");
@@ -43634,7 +44080,7 @@ function looksLikePathToken(token) {
43634
44080
  return false;
43635
44081
  }
43636
44082
  function expandPath(p) {
43637
- if (p.startsWith("~/")) return resolve9(homedir8(), p.slice(2));
44083
+ if (p.startsWith("~/")) return resolve9(homedir7(), p.slice(2));
43638
44084
  return resolve9(p);
43639
44085
  }
43640
44086
  function looksLikeFilePath(input) {
@@ -44165,7 +44611,8 @@ function shouldOfferFirstRunFork() {
44165
44611
  markFirstRunCompleted();
44166
44612
  return false;
44167
44613
  }
44168
- } catch {
44614
+ } catch (err) {
44615
+ debugError("firstRun.sessionScan", err);
44169
44616
  }
44170
44617
  return true;
44171
44618
  }
@@ -44178,7 +44625,8 @@ async function runFirstRunFork(ctx, options = {}) {
44178
44625
  try {
44179
44626
  const { offerFirstRunTour: offerFirstRunTour2 } = await Promise.resolve().then(() => (init_metric_tour(), metric_tour_exports));
44180
44627
  await offerFirstRunTour2(ctx);
44181
- } catch {
44628
+ } catch (err) {
44629
+ debugError("firstRun.tour", err);
44182
44630
  }
44183
44631
  const session = createPromptSession(ctx.rl, ctx);
44184
44632
  try {
@@ -44306,7 +44754,8 @@ async function loadFirstRunDemo(ctx, scenario) {
44306
44754
  let counts = {};
44307
44755
  try {
44308
44756
  counts = await getEntityCounts2();
44309
- } catch {
44757
+ } catch (err) {
44758
+ debugError("firstRun.entityCounts", err);
44310
44759
  }
44311
44760
  ctx.dataset = {
44312
44761
  label: `${scenario} demo`,
@@ -44334,6 +44783,7 @@ var init_first_run = __esm({
44334
44783
  init_repl_globals();
44335
44784
  init_global_admin();
44336
44785
  init_activation();
44786
+ init_diagnostics();
44337
44787
  FIRST_RUN_KEY = "first-run-completed";
44338
44788
  }
44339
44789
  });
@@ -45619,7 +46069,7 @@ function pickGhostHint(ctx) {
45619
46069
  activeGhostHint = null;
45620
46070
  if (shouldSuppressInlineSuggestion(ctx)) return;
45621
46071
  if (resolveRecommendedAction(ctx)) return;
45622
- const hints = GHOST_HINTS[resolveConversationPhase(ctx)];
46072
+ const hints = ghostHintsForPhase(resolveConversationPhase(ctx), ctx);
45623
46073
  if (!hints || hints.length === 0) return;
45624
46074
  activeGhostHint = hints[ghostHintTurn++ % hints.length] ?? null;
45625
46075
  }
@@ -45726,7 +46176,7 @@ async function runRepl(ctx, version) {
45726
46176
  if (result.latest === ctx.updateHomePaintedLatest) return;
45727
46177
  console.log(` ${formatUpdateNudge(result.current, result.latest)}`);
45728
46178
  console.log();
45729
- });
46179
+ }).catch((err) => debugError("repl.pendingUpdateCheck", err));
45730
46180
  }
45731
46181
  let pendingSuggestionRender = null;
45732
46182
  const cancelPendingSuggestionRender = () => {
@@ -45786,7 +46236,8 @@ async function runRepl(ctx, version) {
45786
46236
  const answer = rl.question(prompt);
45787
46237
  scheduleInlineSuggestionRender();
45788
46238
  rawLine = await answer;
45789
- } catch {
46239
+ } catch (err) {
46240
+ debugError("repl.readLine", err);
45790
46241
  resumeTranscriptCapture();
45791
46242
  break;
45792
46243
  }
@@ -45995,6 +46446,7 @@ var init_repl = __esm({
45995
46446
  init_deepdive_complete();
45996
46447
  init_thinkwithme_complete();
45997
46448
  init_voice_complete();
46449
+ init_diagnostics();
45998
46450
  init_inline_suggestion();
45999
46451
  REPL_BUILTINS = [
46000
46452
  "/help",
@@ -46060,6 +46512,7 @@ init_theme();
46060
46512
  init_layout();
46061
46513
  init_emit();
46062
46514
  init_errors2();
46515
+ init_diagnostics();
46063
46516
  init_types2();
46064
46517
  init_version();
46065
46518
  import chalk94 from "chalk";
@@ -46084,6 +46537,7 @@ async function closeDb() {
46084
46537
  const { close: close2 } = await Promise.resolve().then(() => (init_connection(), connection_exports));
46085
46538
  await close2();
46086
46539
  }
46540
+ var fatalReporting = { command: "ntrp", structured: false };
46087
46541
  async function main() {
46088
46542
  ensureLlmConfigMigrated();
46089
46543
  const args = parseArgs(process.argv);
@@ -46099,6 +46553,10 @@ async function main() {
46099
46553
  if (!ctx.execution.color) {
46100
46554
  chalk94.level = 0;
46101
46555
  }
46556
+ fatalReporting = {
46557
+ command: firstToken(args.input) || "ntrp",
46558
+ structured: isStructuredOutput(ctx.execution)
46559
+ };
46102
46560
  if (args.globals.stdin) {
46103
46561
  args.input = (await readStdin()).trim();
46104
46562
  }
@@ -46168,7 +46626,7 @@ async function main() {
46168
46626
  printTrialNudge2(lic);
46169
46627
  }
46170
46628
  }
46171
- void Promise.resolve().then(() => (init_discovery(), discovery_exports)).then((m) => m.refreshStaleProviderCaches()).catch(() => void 0);
46629
+ void Promise.resolve().then(() => (init_discovery(), discovery_exports)).then((m) => m.refreshStaleProviderCaches()).catch((err) => debugError("llm.refreshStaleProviderCaches", err));
46172
46630
  const { setActiveDbPath: setActiveDbPath2 } = await Promise.resolve().then(() => (init_connection(), connection_exports));
46173
46631
  const { datasetPathForSession: datasetPathForSession2 } = await Promise.resolve().then(() => (init_context2(), context_exports));
46174
46632
  ctx.datasetPath = datasetPathForSession2(ctx.sessionId);
@@ -46197,10 +46655,25 @@ async function main() {
46197
46655
  await closeDb();
46198
46656
  process.exit(0);
46199
46657
  }
46200
- main().then((shouldCloseDb) => shouldCloseDb ? closeDb() : void 0).catch((err) => {
46201
- console.error(err);
46202
- closeDb().finally(() => process.exit(1));
46658
+ async function die(err, scope) {
46659
+ debugError(scope, err);
46660
+ await closeDb().catch((closeErr) => debugError("db.close", closeErr));
46661
+ if (fatalReporting.structured) {
46662
+ emitError(fatalReporting.command, err);
46663
+ }
46664
+ console.error(chalk94.red(` ${describeError(err)}`));
46665
+ if (!(err instanceof NtrpError) && err instanceof Error && err.stack) {
46666
+ console.error(chalk94.dim(err.stack.split("\n").slice(1).join("\n")));
46667
+ }
46668
+ process.exit(err instanceof NtrpError ? err.exitCode : 1 /* RuntimeError */);
46669
+ }
46670
+ process.on("unhandledRejection", (reason) => {
46671
+ void die(reason, "unhandledRejection");
46672
+ });
46673
+ process.on("uncaughtException", (err) => {
46674
+ void die(err, "uncaughtException");
46203
46675
  });
46676
+ main().then((shouldCloseDb) => shouldCloseDb ? closeDb() : void 0).catch((err) => die(err, "main"));
46204
46677
  async function readStdin() {
46205
46678
  const chunks = [];
46206
46679
  for await (const chunk of process.stdin) {