@sonnechasser/ntrp 1.5.2 → 1.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +2077 -479
  2. package/dist/mcp/server.js +1678 -141
  3. package/package.json +2 -1
@@ -74,6 +74,17 @@ function formatCurrency(value) {
74
74
  if (value >= 1e3) return `$${(value / 1e3).toFixed(0)}K`;
75
75
  return `$${value.toFixed(0)}`;
76
76
  }
77
+ function formatConstraintLine(aggregate) {
78
+ if (!aggregate) return void 0;
79
+ const gating = aggregate.gating_vital_sign;
80
+ const label = VITAL_SIGN_LABELS[gating] ?? gating;
81
+ const vital = aggregate.vital_signs.find((v) => v.vital_sign === gating);
82
+ if (vital?.dollar_value != null && vital.dollar_value > 0) {
83
+ const dollars = `${formatCurrency(vital.dollar_value)} ${vital.dollar_label ?? ""}`.trim();
84
+ return `${label} (${dollars})`;
85
+ }
86
+ return label;
87
+ }
77
88
  function extractPipelineMetrics(vitals) {
78
89
  const flowRate = vitals.find((v) => v.vital_sign === "flow_rate");
79
90
  if (!flowRate) return null;
@@ -122,6 +133,7 @@ __export(store_exports, {
122
133
  getExportsDir: () => getExportsDir,
123
134
  getKnowledgeDir: () => getKnowledgeDir,
124
135
  getMemoryDir: () => getMemoryDir,
136
+ getRuminationsDir: () => getRuminationsDir,
125
137
  getStrategiesDir: () => getStrategiesDir,
126
138
  getWinsDir: () => getWinsDir,
127
139
  loadConfig: () => loadConfig,
@@ -265,6 +277,22 @@ from work done outside this platform.
265
277
  }
266
278
  return dir;
267
279
  }
280
+ function getRuminationsDir() {
281
+ const dir = join(NTRP_DIR, "ruminations");
282
+ if (!existsSync(dir)) {
283
+ mkdirSync(dir, { recursive: true });
284
+ writeFileSync(
285
+ join(dir, "README.md"),
286
+ `# Craft logs
287
+
288
+ This directory stores overnight strategy craft jobs. Each job has a machine file (\`<id>.json\`) and a short log (\`<id>.md\`).
289
+
290
+ Resume a job with \`ntrp strategy craft --resume <id>\`.
291
+ `
292
+ );
293
+ }
294
+ return dir;
295
+ }
268
296
  function getWinsDir() {
269
297
  const dir = join(NTRP_DIR, "wins");
270
298
  if (!existsSync(dir)) {
@@ -568,6 +596,14 @@ var init_path_safety = __esm({
568
596
  });
569
597
 
570
598
  // src/services/export-kinds.ts
599
+ function archiveSubdirForKind(kind) {
600
+ if (kind.startsWith("prompt:")) return "handoffs";
601
+ if (kind === "report") return "reports";
602
+ if (kind === "notes") return "notes";
603
+ if (kind === "csv") return "csv";
604
+ if (kind === "publish") return "publish";
605
+ return "handoffs";
606
+ }
571
607
  function latestBasenameForKind(kind) {
572
608
  if (kind.startsWith("prompt:")) {
573
609
  const target = kind.slice("prompt:".length);
@@ -967,6 +1003,9 @@ import {
967
1003
  } from "fs";
968
1004
  import { basename as basename2, dirname as dirname2, join as join3, resolve as resolve3, sep as sep2 } from "path";
969
1005
  import { randomUUID } from "crypto";
1006
+ function exportStamp(d = /* @__PURE__ */ new Date()) {
1007
+ return d.toISOString().replace(/T/, "-").replace(/:/g, "").slice(0, 15);
1008
+ }
970
1009
  function ensureExportsLayout(root = getExportsDir()) {
971
1010
  mkdirSync4(root, { recursive: true });
972
1011
  mkdirSync4(join3(root, "latest"), { recursive: true });
@@ -985,6 +1024,15 @@ function ensureExportsLayout(root = getExportsDir()) {
985
1024
  }
986
1025
  return root;
987
1026
  }
1027
+ function getArchiveKindDir(kind) {
1028
+ const root = ensureExportsLayout();
1029
+ const dir = join3(root, archiveSubdirForKind(kind));
1030
+ mkdirSync4(dir, { recursive: true });
1031
+ return dir;
1032
+ }
1033
+ function resolveArchivePath(kind, filename) {
1034
+ return join3(getArchiveKindDir(kind), filename);
1035
+ }
988
1036
  function getAiInboxDir() {
989
1037
  return getConfiguredAiInboxDir();
990
1038
  }
@@ -1043,6 +1091,16 @@ function listExports(opts = {}) {
1043
1091
  }
1044
1092
  return items.slice(0, limit);
1045
1093
  }
1094
+ function updateArchiveLatest(kind, sourcePath, root) {
1095
+ const latestDir = join3(root, "latest");
1096
+ mkdirSync4(latestDir, { recursive: true });
1097
+ const name = latestBasenameForKind(kind);
1098
+ const dest = join3(latestDir, name);
1099
+ copyPath(sourcePath, dest);
1100
+ if (kind.startsWith("prompt:")) {
1101
+ copyPath(sourcePath, join3(latestDir, "handoff.md"));
1102
+ }
1103
+ }
1046
1104
  function copyPath(src, dest) {
1047
1105
  mkdirSync4(dirname2(dest), { recursive: true });
1048
1106
  if (existsSync3(dest)) {
@@ -1240,6 +1298,29 @@ function syncRecentToInbox(limit = 10) {
1240
1298
  if (newest) persistHandoffSkillFiles(newest);
1241
1299
  return n;
1242
1300
  }
1301
+ function recordExportWrite(opts) {
1302
+ const root = ensureExportsLayout();
1303
+ const path = resolve3(opts.path);
1304
+ if (!existsSync3(path)) {
1305
+ throw new Error(`Export path does not exist: ${path}`);
1306
+ }
1307
+ updateArchiveLatest(opts.kind, path, root);
1308
+ const event = {
1309
+ id: randomUUID().slice(0, 8),
1310
+ op: "write",
1311
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1312
+ kind: opts.kind,
1313
+ path,
1314
+ session_id: opts.sessionId,
1315
+ title: opts.title
1316
+ };
1317
+ persistHandoffSkillFiles(event);
1318
+ const inboxPath = syncAiInbox({ kind: opts.kind, path });
1319
+ if (inboxPath) event.inbox_path = inboxPath;
1320
+ appendManifestEvent(event, root);
1321
+ regenerateIndex(root);
1322
+ return event;
1323
+ }
1243
1324
  function archiveIndexPath() {
1244
1325
  return join3(ensureExportsLayout(), "INDEX.md");
1245
1326
  }
@@ -1380,6 +1461,8 @@ function buildSessionContextDoc(file, opts = {}) {
1380
1461
  lines.push(`- Constraints: ${file.strategist.constraintsNote}`);
1381
1462
  }
1382
1463
  if (file.strategist.origin) lines.push(`- Origin: ${file.strategist.origin}`);
1464
+ if (file.strategist.mode) lines.push(`- Mode: ${file.strategist.mode}`);
1465
+ if (file.strategist.ruminationId) lines.push(`- Craft job: ${file.strategist.ruminationId}`);
1383
1466
  lines.push("");
1384
1467
  }
1385
1468
  if (file.think) {
@@ -5106,6 +5189,7 @@ var init_surfaces = __esm({
5106
5189
  strategy: { defaultTier: "medium", allowUserTier: false },
5107
5190
  strategist: { defaultTier: "high", allowUserTier: true },
5108
5191
  strategist_stress: { defaultTier: "high", allowUserTier: false },
5192
+ strategist_critic: { defaultTier: "high", allowUserTier: false },
5109
5193
  option_eval: { defaultTier: "low", allowUserTier: false }
5110
5194
  };
5111
5195
  }
@@ -7267,12 +7351,51 @@ import { stringify as stringifyYaml } from "yaml";
7267
7351
  function strategyLibraryPath(slug) {
7268
7352
  return join16(getStrategiesDir(), `${slug}.md`);
7269
7353
  }
7270
- function writeStrategyMarkdown(strategy) {
7354
+ function writeStrategyMarkdown(strategy, extras) {
7271
7355
  const path = strategyLibraryPath(strategy.slug);
7272
- writeFileSync11(path, renderStrategyMarkdown(strategy), "utf-8");
7356
+ writeFileSync11(path, renderStrategyMarkdown(strategy, extras), "utf-8");
7273
7357
  return path;
7274
7358
  }
7275
- function renderStrategyMarkdown(strategy) {
7359
+ function formatEffortSum(workstreams) {
7360
+ const sum = workstreams.reduce(
7361
+ (acc, ws) => acc + (Number.isFinite(ws.effort_hours) ? ws.effort_hours : 0),
7362
+ 0
7363
+ );
7364
+ const count = workstreams.length;
7365
+ return `~${Math.round(sum)} team-hours across ${count} workstream${count === 1 ? "" : "s"}`;
7366
+ }
7367
+ function renderConstraintHeading(constraintLine) {
7368
+ return `## Constraint
7369
+ ${constraintLine?.trim() || "none stated"}
7370
+ `;
7371
+ }
7372
+ function renderScopeHeading(constraints, outOfScope) {
7373
+ const inLines = constraints.length > 0 ? formatList(constraints) : "- none stated";
7374
+ const outItems = (outOfScope ?? []).map((s) => s.trim()).filter(Boolean);
7375
+ const outLines = outItems.length > 0 ? formatList(outItems) : "- none stated";
7376
+ return `## In scope / out of scope
7377
+ In scope:
7378
+ ${inLines}
7379
+
7380
+ Out of scope:
7381
+ ${outLines}
7382
+ `;
7383
+ }
7384
+ function renderKilledAlternativeLine(killedAlternative) {
7385
+ return `Killed alternative: ${killedAlternative?.trim() || "none stated"}`;
7386
+ }
7387
+ function renderEffortHeading(workstreams) {
7388
+ return `## Effort
7389
+ ${formatEffortSum(workstreams)}
7390
+ `;
7391
+ }
7392
+ function renderReviewHeading(opts) {
7393
+ const cmd = opts.slug?.trim() ? `/strategy review ${opts.slug.trim()}` : "/strategy review";
7394
+ return `## Review
7395
+ Cadence: ${opts.cadence}. Check progress with ${cmd}.
7396
+ `;
7397
+ }
7398
+ function renderStrategyMarkdown(strategy, extras) {
7276
7399
  const frontmatter = stringifyYaml({
7277
7400
  id: strategy.id,
7278
7401
  slug: strategy.slug,
@@ -7287,6 +7410,9 @@ function renderStrategyMarkdown(strategy) {
7287
7410
  ...strategy.baseline_batch_id ? { baseline_batch_id: strategy.baseline_batch_id } : {},
7288
7411
  updated_at: strategy.updated_at
7289
7412
  }).trim();
7413
+ if (strategy.origin === "strategist") {
7414
+ return renderStrategistLibraryMarkdown(strategy, extras, frontmatter);
7415
+ }
7290
7416
  const objectiveSection = strategy.objective ? `
7291
7417
  ## Objective
7292
7418
  ${strategy.objective}
@@ -7333,6 +7459,67 @@ ${formatList(strategy.risks)}
7333
7459
  ## Experiment Design
7334
7460
  ${strategy.experiment_design}
7335
7461
 
7462
+ ## Source Excerpt
7463
+ ${strategy.raw_excerpt || "_No excerpt captured._"}
7464
+ `;
7465
+ }
7466
+ function renderStrategistLibraryMarkdown(strategy, extras, frontmatter) {
7467
+ const callSection = strategy.raw_excerpt ? `
7468
+ ## The Call
7469
+ ${strategy.raw_excerpt}
7470
+ ` : "";
7471
+ const objectiveSection = strategy.objective ? `
7472
+ ## Locked objective
7473
+ ${strategy.objective}
7474
+ ` : "";
7475
+ const workstreamSection = strategy.workstreams.length > 0 ? `
7476
+ ## Workstreams
7477
+ ${strategy.workstreams.map(formatWorkstream).join("\n")}
7478
+ ` : "";
7479
+ const assumptionsSection = strategy.assumptions.length > 0 ? `
7480
+ ## Assumptions (unverified)
7481
+ ${formatList(strategy.assumptions)}
7482
+ ` : "";
7483
+ const craftLogSection = extras?.craftLogPath ? `
7484
+ ## Craft log
7485
+ See \`${extras.craftLogPath}\`.
7486
+ ` : "";
7487
+ return `---
7488
+ ${frontmatter}
7489
+ ---
7490
+
7491
+ # ${strategy.title}
7492
+ ${callSection}${objectiveSection}
7493
+ ${renderConstraintHeading(extras?.constraintLine)}
7494
+ ## Goal
7495
+ ${strategy.goal}
7496
+
7497
+ ${renderScopeHeading(strategy.constraints, extras?.outOfScope)}
7498
+ ## Hypothesis
7499
+ ${strategy.hypothesis}
7500
+
7501
+ ${renderKilledAlternativeLine(extras?.killedAlternative)}
7502
+
7503
+ ## Target Segment
7504
+ ${strategy.target_segment}
7505
+
7506
+ ${renderEffortHeading(strategy.workstreams)}${workstreamSection}
7507
+ ## Success Metrics
7508
+ ${formatMetrics(strategy.success_metrics)}
7509
+
7510
+ ## Leading Indicators
7511
+ ${formatMetrics(strategy.leading_indicators)}
7512
+
7513
+ ## Recommended Actions
7514
+ ${formatList(strategy.recommended_actions)}
7515
+ ${assumptionsSection}
7516
+ ## Risks
7517
+ ${formatList(strategy.risks)}
7518
+
7519
+ ## Experiment Design
7520
+ ${strategy.experiment_design}
7521
+
7522
+ ${renderReviewHeading({ cadence: strategy.review_cadence, slug: strategy.slug })}${craftLogSection}
7336
7523
  ## Source Excerpt
7337
7524
  ${strategy.raw_excerpt || "_No excerpt captured._"}
7338
7525
  `;
@@ -8203,6 +8390,7 @@ function buildSessionFileSnapshot(ctx) {
8203
8390
  if (ctx.strategistState) file.strategist = ctx.strategistState;
8204
8391
  if (ctx.thinkState) file.think = ctx.thinkState;
8205
8392
  if (ctx.pendingAsk) file.pending_ask = ctx.pendingAsk;
8393
+ if (ctx.lastCraftJobId) file.last_craft_job_id = ctx.lastCraftJobId;
8206
8394
  return file;
8207
8395
  }
8208
8396
  function defaultSessionAnalysis(primary = "gtm_health") {
@@ -8557,6 +8745,9 @@ async function finalizeSession(ctx, stage) {
8557
8745
  if (ctx.pendingAsk) {
8558
8746
  file.pending_ask = ctx.pendingAsk;
8559
8747
  }
8748
+ if (ctx.lastCraftJobId) {
8749
+ file.last_craft_job_id = ctx.lastCraftJobId;
8750
+ }
8560
8751
  try {
8561
8752
  writeFileSync13(ctx.sessionFile, JSON.stringify(file, null, 2) + "\n");
8562
8753
  } catch {
@@ -8628,6 +8819,7 @@ function resetContextForSwitch(ctx, opts) {
8628
8819
  ctx.strategistState = opts.strategistState;
8629
8820
  ctx.thinkState = opts.thinkState;
8630
8821
  ctx.pendingAsk = opts.pendingAsk;
8822
+ ctx.lastCraftJobId = opts.lastCraftJobId;
8631
8823
  ctx.gapAudit = void 0;
8632
8824
  ctx.deliverIntent = false;
8633
8825
  ctx.computeInProgress = false;
@@ -10025,7 +10217,9 @@ section: Settings
10025
10217
  handler: ../commands/update.ts
10026
10218
  ---
10027
10219
 
10028
- Install the latest global NTRP package via npm.`
10220
+ Install the latest global NTRP package via npm.
10221
+ Interactive confirms with \u23CE yes, then re-execs onto home.
10222
+ One-shot installs immediately and prints a restart hint.`
10029
10223
  },
10030
10224
  {
10031
10225
  name: "resume",
@@ -10252,7 +10446,7 @@ This command is hidden. Type \`/ingest --demo\` instead. That command calls this
10252
10446
  name: strategy
10253
10447
  description: Make a measured strategy from your data
10254
10448
  section: More
10255
- args: [objective] | [list|show|review|ingest|add|sync|sources] [args]
10449
+ args: [objective] | [list|show|review|ingest|add|sync|sources|craft] [args]
10256
10450
  handler: ../commands/strategy.ts
10257
10451
  ---
10258
10452
 
@@ -10261,6 +10455,8 @@ NTRP uses live data. It works back from the objective.
10261
10455
  The result is sequenced workstreams with dated milestones, deliverables, outcome ranges, and a contingency per workstream.
10262
10456
  Saved plans go to the strategy library. Later answers use them.
10263
10457
  Type \`/strategy review [slug]\` to check the plan against live data when new batches arrive.
10458
+ Type \`keep going\` at the confirm card to keep working until the plan is ready.
10459
+ One-shot: \`ntrp strategy craft <objective>\`. Resume with \`ntrp strategy craft --resume <id>\`.
10264
10460
 
10265
10461
  Library commands: \`/strategy list\`, \`/strategy show <slug>\`.
10266
10462
  Type \`/strategy ingest <file>\` for markdown, YAML, PDF, text, or \`-\` for stdin.
@@ -14156,14 +14352,211 @@ var init_activation = __esm({
14156
14352
  }
14157
14353
  });
14158
14354
 
14355
+ // src/config/update-check.ts
14356
+ import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync19, unlinkSync as unlinkSync3, writeFileSync as writeFileSync14 } from "fs";
14357
+ import { join as join22 } from "path";
14358
+ var init_update_check = __esm({
14359
+ "src/config/update-check.ts"() {
14360
+ "use strict";
14361
+ init_store();
14362
+ }
14363
+ });
14364
+
14365
+ // src/version.ts
14366
+ import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
14367
+ import { dirname as dirname4, join as join23 } from "path";
14368
+ import { fileURLToPath } from "url";
14369
+ function readVersionFromPackageJson(packageJsonPath) {
14370
+ if (!existsSync23(packageJsonPath)) return null;
14371
+ try {
14372
+ const pkg = JSON.parse(readFileSync20(packageJsonPath, "utf-8"));
14373
+ if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
14374
+ } catch {
14375
+ }
14376
+ return null;
14377
+ }
14378
+ function readVersionNearEntry(entryPath) {
14379
+ const start = dirname4(entryPath);
14380
+ for (const rel of [join23(start, "..", "package.json"), join23(start, "../..", "package.json")]) {
14381
+ const version = readVersionFromPackageJson(rel);
14382
+ if (version) return version;
14383
+ }
14384
+ return null;
14385
+ }
14386
+ function readInstalledVersionFromDisk() {
14387
+ return readVersionNearEntry(fileURLToPath(import.meta.url));
14388
+ }
14389
+ function getInstalledVersion() {
14390
+ if (cachedVersion) return cachedVersion;
14391
+ cachedVersion = readInstalledVersionFromDisk() ?? "0.0.0";
14392
+ return cachedVersion;
14393
+ }
14394
+ var cachedVersion;
14395
+ var init_version = __esm({
14396
+ "src/version.ts"() {
14397
+ "use strict";
14398
+ }
14399
+ });
14400
+
14401
+ // src/update/registry.ts
14402
+ function parseVersionParts(version) {
14403
+ const cleaned = version.trim().replace(/^v/i, "");
14404
+ const core = cleaned.split("-")[0] ?? cleaned;
14405
+ const parts = core.split(".").map((p) => parseInt(p, 10));
14406
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
14407
+ }
14408
+ function isNewerVersion(latest, current) {
14409
+ const [lMaj, lMin, lPatch] = parseVersionParts(latest);
14410
+ const [cMaj, cMin, cPatch] = parseVersionParts(current);
14411
+ if (lMaj !== cMaj) return lMaj > cMaj;
14412
+ if (lMin !== cMin) return lMin > cMin;
14413
+ return lPatch > cPatch;
14414
+ }
14415
+ function hasAvailableUpdate(update) {
14416
+ return Boolean(update && isNewerVersion(update.latest, update.current));
14417
+ }
14418
+ var init_registry2 = __esm({
14419
+ "src/update/registry.ts"() {
14420
+ "use strict";
14421
+ init_update_check();
14422
+ init_version();
14423
+ }
14424
+ });
14425
+
14426
+ // src/ruminations/store.ts
14427
+ import { existsSync as existsSync24, readFileSync as readFileSync21, writeFileSync as writeFileSync15 } from "fs";
14428
+ import { join as join24 } from "path";
14429
+ import { randomUUID as randomUUID8 } from "crypto";
14430
+ function makeRuminationId(now2 = /* @__PURE__ */ new Date()) {
14431
+ const day = now2.toISOString().slice(0, 10).replace(/-/g, "");
14432
+ return `${day}-${randomUUID8().slice(0, 4)}`;
14433
+ }
14434
+ function ruminationJsonPath(id) {
14435
+ return join24(getRuminationsDir(), `${id}.json`);
14436
+ }
14437
+ function ruminationLogPath(id) {
14438
+ return join24(getRuminationsDir(), `${id}.md`);
14439
+ }
14440
+ function unfinishedCraftJob(id) {
14441
+ if (!id?.trim()) return null;
14442
+ const job = loadRuminationJob(id);
14443
+ if (!job || job.status === "ready") return null;
14444
+ return job;
14445
+ }
14446
+ function loadRuminationJob(id) {
14447
+ const path = ruminationJsonPath(id);
14448
+ if (!existsSync24(path)) return null;
14449
+ try {
14450
+ const parsed = JSON.parse(readFileSync21(path, "utf-8"));
14451
+ if (!parsed || typeof parsed !== "object" || parsed.id !== id) return null;
14452
+ if (parsed.best_plan === void 0) parsed.best_plan = parsed.plan ?? null;
14453
+ if (parsed.best_score === void 0) parsed.best_score = parsed.last_critic?.score ?? null;
14454
+ return parsed;
14455
+ } catch {
14456
+ return null;
14457
+ }
14458
+ }
14459
+ function createRuminationJob(opts) {
14460
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
14461
+ return {
14462
+ id: opts.id ?? makeRuminationId(),
14463
+ objective: opts.objective,
14464
+ constraints_note: opts.constraintsNote,
14465
+ baseline_batch_id: opts.baselineBatchId ?? null,
14466
+ snapshot_hash: opts.snapshotHash,
14467
+ plan: null,
14468
+ best_plan: null,
14469
+ best_score: null,
14470
+ last_critic: null,
14471
+ rubric_gaps: [],
14472
+ iterations: [],
14473
+ cumulative_input_tokens: 0,
14474
+ cumulative_output_tokens: 0,
14475
+ status: "running",
14476
+ from_fallback: false,
14477
+ revised_once: false,
14478
+ extra_ground_used: false,
14479
+ min_score: opts.minScore,
14480
+ max_rounds: opts.maxRounds,
14481
+ max_tokens: opts.maxTokens,
14482
+ created_at: now2,
14483
+ updated_at: now2,
14484
+ no_improve_streak: 0,
14485
+ last_critic_score: null
14486
+ };
14487
+ }
14488
+ function renderRuminationLog(job) {
14489
+ const lines = [];
14490
+ lines.push(`# Craft log \u2014 ${job.id}`);
14491
+ lines.push("");
14492
+ lines.push(`Objective: ${job.objective}`);
14493
+ lines.push(`Status: ${job.status}${job.stop_reason ? ` (${job.stop_reason})` : ""}`);
14494
+ lines.push(
14495
+ `Tokens: ${job.cumulative_input_tokens + job.cumulative_output_tokens} total (${job.cumulative_input_tokens} in / ${job.cumulative_output_tokens} out)`
14496
+ );
14497
+ if (job.library_path) lines.push(`Strategy: ${job.library_path}`);
14498
+ if (job.handoff_path) lines.push(`Handoff: ${job.handoff_path}`);
14499
+ lines.push("");
14500
+ lines.push("## Rounds");
14501
+ lines.push("");
14502
+ if (job.iterations.length === 0) {
14503
+ lines.push("_No rounds yet._");
14504
+ } else {
14505
+ for (const it of job.iterations) {
14506
+ const codes = it.blocking_codes.length > 0 ? it.blocking_codes.join(", ") : "none";
14507
+ lines.push(
14508
+ `- Round ${it.round}: score ${it.critic_score ?? "\u2014"} \xB7 ${it.decision} \xB7 blocking ${codes} \xB7 ${it.tokens_in + it.tokens_out} tok`
14509
+ );
14510
+ }
14511
+ }
14512
+ if (job.last_critic?.partner_pushback) {
14513
+ lines.push("");
14514
+ lines.push("## Partner pushback");
14515
+ lines.push("");
14516
+ lines.push(job.last_critic.partner_pushback);
14517
+ }
14518
+ if (job.last_critic?.killed_alternative) {
14519
+ lines.push("");
14520
+ lines.push("## Killed alternative");
14521
+ lines.push("");
14522
+ lines.push(job.last_critic.killed_alternative);
14523
+ }
14524
+ lines.push("");
14525
+ return lines.join("\n");
14526
+ }
14527
+ function saveRuminationJob(job) {
14528
+ job.updated_at = (/* @__PURE__ */ new Date()).toISOString();
14529
+ getRuminationsDir();
14530
+ writeFileSync15(ruminationJsonPath(job.id), JSON.stringify(job, null, 2) + "\n", "utf-8");
14531
+ writeFileSync15(ruminationLogPath(job.id), renderRuminationLog(job), "utf-8");
14532
+ }
14533
+ function addRuminationIteration(job, iteration) {
14534
+ job.iterations.push({ ...iteration, at: (/* @__PURE__ */ new Date()).toISOString() });
14535
+ job.cumulative_input_tokens += iteration.tokens_in;
14536
+ job.cumulative_output_tokens += iteration.tokens_out;
14537
+ }
14538
+ var init_store3 = __esm({
14539
+ "src/ruminations/store.ts"() {
14540
+ "use strict";
14541
+ init_store();
14542
+ }
14543
+ });
14544
+
14159
14545
  // src/conversation/recommended-action.ts
14160
14546
  function resolveRecommendedAction(ctx) {
14161
14547
  if (!hasValidLicense()) return { submit: "/activate", hint: "/activate" };
14162
14548
  const phase = resolveConversationPhase(ctx);
14163
14549
  switch (phase) {
14164
14550
  case "explore":
14551
+ if (!canUseReplAi(ctx)) {
14552
+ if (ctx.stage === "delivered") return { submit: "/end", hint: "home" };
14553
+ return { submit: "/connect", hint: "/connect" };
14554
+ }
14555
+ if (unfinishedCraftJob(ctx.lastCraftJobId)) {
14556
+ return { submit: "keep going", hint: "keep going" };
14557
+ }
14165
14558
  if (ctx.stage === "delivered") return { submit: "/end", hint: "home" };
14166
- return canUseReplAi(ctx) ? null : { submit: "/connect", hint: "/connect" };
14559
+ return null;
14167
14560
  case "awaiting_data":
14168
14561
  if (ctx.gapAudit?.can_compute) return { submit: "go ahead", hint: "go ahead" };
14169
14562
  if (!sessionHasData(ctx)) return { submit: "use demo data", hint: "use demo data" };
@@ -14174,6 +14567,8 @@ function resolveRecommendedAction(ctx) {
14174
14567
  return ctx.strategistState?.step === "objective_confirm" ? { submit: "yes", hint: "yes" } : null;
14175
14568
  case "think":
14176
14569
  return null;
14570
+ case "orient":
14571
+ return hasAvailableUpdate(ctx.updateAvailable) ? { submit: "/update", hint: "/update" } : null;
14177
14572
  default:
14178
14573
  return null;
14179
14574
  }
@@ -14183,7 +14578,9 @@ var init_recommended_action = __esm({
14183
14578
  "use strict";
14184
14579
  init_repl_api();
14185
14580
  init_activation();
14581
+ init_registry2();
14186
14582
  init_phase();
14583
+ init_store3();
14187
14584
  }
14188
14585
  });
14189
14586
 
@@ -15890,11 +16287,11 @@ function jsonSafe(value) {
15890
16287
  }
15891
16288
  return value;
15892
16289
  }
15893
- function envelope(command, data, warnings) {
16290
+ function envelope(command, data, warnings, status = "ok") {
15894
16291
  return {
15895
16292
  schema_version: HEADLESS_SCHEMA_VERSION,
15896
16293
  command,
15897
- status: "ok",
16294
+ status,
15898
16295
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
15899
16296
  data,
15900
16297
  ...warnings && warnings.length > 0 ? { warnings } : {}
@@ -15910,8 +16307,8 @@ function errorEnvelope(command, err) {
15910
16307
  error: ntrpError.toHeadlessError()
15911
16308
  };
15912
16309
  }
15913
- function emitResult(command, data, warnings) {
15914
- console.log(JSON.stringify(jsonSafe(envelope(command, data, warnings)), null, 2));
16310
+ function emitResult(command, data, warnings, status = "ok") {
16311
+ console.log(JSON.stringify(jsonSafe(envelope(command, data, warnings, status)), null, 2));
15915
16312
  }
15916
16313
  function emitError(command, err) {
15917
16314
  const ntrpError = err instanceof NtrpError ? err : toNtrpError(err);
@@ -17511,7 +17908,13 @@ async function persistStrategistPlan(plan, opts = {}) {
17511
17908
  if (!strategy) {
17512
17909
  throw new NtrpError("strategy_persist_failed", "Strategy was not found after saving.", 1 /* RuntimeError */);
17513
17910
  }
17514
- const writtenPath = writeStrategyMarkdown(strategy);
17911
+ const extras = {
17912
+ craftLogPath: opts.craftLogPath,
17913
+ constraintLine: opts.constraintLine,
17914
+ killedAlternative: opts.killedAlternative,
17915
+ outOfScope: opts.outOfScope
17916
+ };
17917
+ const writtenPath = writeStrategyMarkdown(strategy, extras);
17515
17918
  await insertStrategySource({
17516
17919
  strategy_id: strategy.id,
17517
17920
  source_type: "agent",
@@ -17539,7 +17942,6 @@ var init_strategist = __esm({
17539
17942
  init_errors2();
17540
17943
  init_types2();
17541
17944
  init_formatters();
17542
- init_formatters();
17543
17945
  }
17544
17946
  });
17545
17947
 
@@ -17825,6 +18227,52 @@ ${AAR_BLOCK}
17825
18227
 
17826
18228
  Then respond with the FINAL revised plan as strict JSON in the same schema \u2014 no markdown fences, no prose. Fold what you learned into constraints, assumptions, risks, and confidence. This version is the one that gets saved and reviewed against, so make every number one you are willing to be checked on.`;
17827
18229
  }
18230
+ function buildCriticMessage(input) {
18231
+ return `CRAFT CRITIC. You are a skeptical partner reviewing a GTM plan before it ships to a client. The objective is locked and must be echoed verbatim.
18232
+
18233
+ ARMED OBJECTIVE (byte-identical; do not rewrite):
18234
+ "${input.objective}"
18235
+
18236
+ CODE RUBRIC GAPS (already blocking \u2014 you cannot ship until these are gone):
18237
+ ${input.rubricGaps || "(none)"}
18238
+
18239
+ LIVE SNAPSHOT (cite these numbers only):
18240
+ ${input.healthSnapshot}
18241
+
18242
+ CURRENT PLAN JSON:
18243
+ ${input.planJson}
18244
+
18245
+ ${HEILMEIER_GATE}
18246
+
18247
+ ${AAR_BLOCK}
18248
+
18249
+ Decide whether a partner would ship this. Respond with STRICT JSON only:
18250
+ {
18251
+ "ship": false,
18252
+ "score": 0,
18253
+ "objective_echo": ${JSON.stringify(input.objective)},
18254
+ "gaps": [{ "code": "bottleneck", "severity": "blocking", "note": "...", "fix": "..." }],
18255
+ "killed_alternative": "one approach that failed newness/stake/exams",
18256
+ "partner_pushback": "one paragraph a partner would actually say"
18257
+ }
18258
+
18259
+ ship may be true only when objective_echo matches, blocking gaps are empty, and you would defend this plan in the room. Advisory gaps do not block. Score 0-100.`;
18260
+ }
18261
+ function buildReviseMessage(input) {
18262
+ return `CRAFT REVISE. Keep the objective verbatim. Patch only the listed gap codes. Copy baselines from the current plan / snapshot exactly \u2014 do not invent numbers.
18263
+
18264
+ ARMED OBJECTIVE (must appear unchanged as "objective"):
18265
+ "${input.objective}"
18266
+
18267
+ GAPS TO CLOSE:
18268
+ ${input.gapsJson}
18269
+
18270
+ CURRENT PLAN:
18271
+ ${input.planJson}
18272
+
18273
+ Respond with ONLY the full plan JSON object in this schema \u2014 no prose, no fences:
18274
+ ${STRATEGIST_PLAN_SCHEMA_BLOCK}`;
18275
+ }
17828
18276
  var STRATEGIST_PLAN_SCHEMA_BLOCK;
17829
18277
  var init_strategist_prompt = __esm({
17830
18278
  "src/ai/strategist-prompt.ts"() {
@@ -18352,53 +18800,344 @@ var init_strategist_validate = __esm({
18352
18800
  }
18353
18801
  });
18354
18802
 
18355
- // src/ai/strategist.ts
18356
- function buildHealthSnapshot(computeResult, divergences) {
18357
- const { aggregate, segments } = computeResult;
18358
- return JSON.stringify(
18359
- {
18360
- aggregate: {
18361
- overall_score: aggregate.overall_score,
18362
- overall_status: aggregate.overall_status,
18363
- gating_vital_sign: aggregate.gating_vital_sign,
18364
- total_value_at_risk: aggregate.total_value_at_risk,
18365
- vital_signs: Object.fromEntries(
18366
- aggregate.vital_signs.map((v) => [
18367
- v.vital_sign,
18368
- { score: v.score, status: v.status, dollar_value: v.dollar_value, dollar_label: v.dollar_label }
18369
- ])
18370
- )
18371
- },
18372
- segment_names: segments.map((s) => s.segment.name),
18373
- top_divergences: divergences.slice(0, 5).map((d) => ({
18374
- segment: d.segmentName,
18375
- vital_sign: d.vitalSign,
18376
- segment_score: d.segmentScore,
18377
- aggregate_score: d.aggregateScore,
18378
- delta: d.delta
18379
- }))
18380
- },
18381
- null,
18382
- 2
18383
- );
18803
+ // src/ai/strategist-rubric.ts
18804
+ function numbersMatch2(a, b) {
18805
+ if (a === b) return true;
18806
+ if (a === 0 || b === 0) return Math.abs(a - b) < 0.5;
18807
+ return Math.abs(a - b) / Math.max(Math.abs(a), Math.abs(b)) <= 0.02;
18384
18808
  }
18385
- async function* strategistPlanSession(options) {
18386
- assertReplAi(options.ctx);
18387
- const llmCfg = loadLlmConfig();
18388
- const todayIso = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
18389
- const systemPrompt = buildStrategistSystemPrompt(todayIso);
18390
- const tools2 = [...AGENTIC_TOOLS];
18391
- if (isWebRetrievalEnabled()) tools2.push(WEB_SEARCH_TOOL);
18392
- const toolCtx = {
18393
- computeResult: options.computeResult,
18394
- divergences: options.divergences
18395
- };
18396
- if (options.includeMetrics) {
18397
- try {
18398
- const { computeFullMetrics: computeFullMetrics2 } = await Promise.resolve().then(() => (init_compute(), compute_exports));
18399
- toolCtx.metrics = (await computeFullMetrics2()).aggregate.metrics;
18400
- } catch {
18401
- }
18809
+ function parseIsoDate2(value) {
18810
+ const match = value.trim().match(/^(\d{4})-(\d{2})-(\d{2})/);
18811
+ if (!match) return null;
18812
+ const date = /* @__PURE__ */ new Date(`${match[1]}-${match[2]}-${match[3]}T00:00:00Z`);
18813
+ return Number.isNaN(date.getTime()) ? null : date;
18814
+ }
18815
+ function addDays2(date, days) {
18816
+ const out = new Date(date);
18817
+ out.setUTCDate(out.getUTCDate() + days);
18818
+ return out;
18819
+ }
18820
+ function workstreamText(ws) {
18821
+ return [ws.title, ws.problem, ws.rationale, ws.actions.join(" "), ws.play_ids.join(" ")].join(" ");
18822
+ }
18823
+ function hasOwnerShape(ws) {
18824
+ const blob = workstreamText(ws).toLowerCase();
18825
+ return OWNER_TOKENS.some((token) => blob.includes(token));
18826
+ }
18827
+ function firstWorkstreamMovesConstraint(ws, gating) {
18828
+ const plays = getPlaybook();
18829
+ if (ws.play_ids.some((id) => plays.find((p) => p.id === id)?.trigger_vital_sign === gating)) {
18830
+ return true;
18831
+ }
18832
+ const blob = workstreamText(ws).toLowerCase();
18833
+ return VITAL_ALIASES[gating].some((alias) => blob.includes(alias));
18834
+ }
18835
+ function citesStake(plan, snapshot) {
18836
+ const dollars = [];
18837
+ if (snapshot.aggregate.total_value_at_risk != null && snapshot.aggregate.total_value_at_risk > 0) {
18838
+ dollars.push(snapshot.aggregate.total_value_at_risk);
18839
+ }
18840
+ for (const vs of snapshot.aggregate.vital_signs) {
18841
+ if (vs.dollar_value != null && vs.dollar_value > 0) dollars.push(vs.dollar_value);
18842
+ }
18843
+ if (dollars.length === 0) return true;
18844
+ const blob = `${plan.summary_30k} ${plan.hypothesis} ${plan.workstreams[0]?.problem ?? ""}`;
18845
+ const found = extractNumbers(blob);
18846
+ if (found.some((n) => dollars.some((d) => numbersMatch2(n, d)))) return true;
18847
+ const formatted = dollars.map((d) => formatCurrency(d).toLowerCase());
18848
+ const lower = blob.toLowerCase();
18849
+ return formatted.some((f) => lower.includes(f.toLowerCase()));
18850
+ }
18851
+ function hasStartWithin48h(plan, todayIso) {
18852
+ const today = parseIsoDate2(todayIso) ?? /* @__PURE__ */ new Date(`${todayIso}T00:00:00Z`);
18853
+ const limit = addDays2(today, 2);
18854
+ const first = plan.workstreams[0];
18855
+ if (!first) return false;
18856
+ if (first.actions.some((action) => START_SOON_RE.test(action))) return true;
18857
+ const dated = [
18858
+ ...first.milestones.map((m) => m.due),
18859
+ ...first.deliverables.map((d) => d.due)
18860
+ ];
18861
+ for (const iso of dated) {
18862
+ const parsed = parseIsoDate2(iso);
18863
+ if (parsed && parsed.getTime() <= limit.getTime()) return true;
18864
+ }
18865
+ return plan.workstreams.some(
18866
+ (ws) => ws.actions.some((action) => START_SOON_RE.test(action))
18867
+ );
18868
+ }
18869
+ function examsHold(ws) {
18870
+ const o = ws.expected_outcome;
18871
+ return extractNumbers(o.baseline).length > 0 && extractNumbers(o.target_range).length > 0 && isKnownInstrument(o.measured_by);
18872
+ }
18873
+ function inventedNumbers(plan, evidenceText) {
18874
+ const evidenceNums = extractNumbers(evidenceText);
18875
+ if (evidenceNums.length === 0) return false;
18876
+ for (const ws of plan.workstreams) {
18877
+ const baselines = extractNumbers(ws.expected_outcome.baseline);
18878
+ if (baselines.length === 0) continue;
18879
+ if (!baselines.some((b) => evidenceNums.some((e) => numbersMatch2(b, e)))) return true;
18880
+ }
18881
+ return false;
18882
+ }
18883
+ function scoreConsultantPlan(plan, opts) {
18884
+ const gaps = [];
18885
+ const gating = opts.snapshot.aggregate.gating_vital_sign;
18886
+ const first = plan.workstreams[0];
18887
+ const evidence = opts.evidenceText ?? JSON.stringify({
18888
+ aggregate: opts.snapshot.aggregate
18889
+ });
18890
+ if (opts.armedObjective && plan.objective !== opts.armedObjective) {
18891
+ gaps.push({
18892
+ code: "objective_drift",
18893
+ severity: "blocking",
18894
+ note: "Plan objective does not match the armed objective.",
18895
+ fix: "Copy the armed objective into plan.objective with no edits."
18896
+ });
18897
+ }
18898
+ if (!first || !firstWorkstreamMovesConstraint(first, gating)) {
18899
+ const label = VITAL_SIGN_LABELS[gating] ?? gating;
18900
+ gaps.push({
18901
+ code: "bottleneck",
18902
+ severity: "blocking",
18903
+ note: `First workstream does not move the gating vital (${label}).`,
18904
+ fix: `Lead with a play or problem that moves ${gating}. Non-constraint work waits.`
18905
+ });
18906
+ }
18907
+ if (!citesStake(plan, opts.snapshot)) {
18908
+ gaps.push({
18909
+ code: "stake",
18910
+ severity: "blocking",
18911
+ note: "30k or hypothesis does not cite snapshot dollars / cost of inaction.",
18912
+ fix: "Open with the gating dollar figure from the live snapshot."
18913
+ });
18914
+ }
18915
+ for (const ws of plan.workstreams) {
18916
+ if (!hasOwnerShape(ws)) {
18917
+ gaps.push({
18918
+ code: "owner",
18919
+ severity: "blocking",
18920
+ note: `Workstream "${ws.title}" has no function-shaped owner.`,
18921
+ fix: "Name RevOps, sales manager, CS lead, marketing ops, or AE lead in the actions."
18922
+ });
18923
+ break;
18924
+ }
18925
+ }
18926
+ if (!hasStartWithin48h(plan, opts.todayIso)) {
18927
+ gaps.push({
18928
+ code: "start_48h",
18929
+ severity: "blocking",
18930
+ note: "No action or milestone is startable within 48 hours.",
18931
+ fix: "Put a dated first action or milestone within two days of today."
18932
+ });
18933
+ }
18934
+ const missingEffort = plan.workstreams.filter(
18935
+ (ws) => !Number.isFinite(ws.effort_hours) || ws.effort_hours <= 0
18936
+ );
18937
+ if (missingEffort.length > 0 || plan.workstreams.length === 0) {
18938
+ gaps.push({
18939
+ code: "effort",
18940
+ severity: "blocking",
18941
+ note: "Each workstream needs a finite effort_hours greater than zero.",
18942
+ fix: "Set effort_hours to a real team-hour estimate per workstream."
18943
+ });
18944
+ }
18945
+ if (opts.constraintsNote?.trim() && plan.constraints.length === 0) {
18946
+ gaps.push({
18947
+ code: "scope",
18948
+ severity: "blocking",
18949
+ note: "Operator stated constraints, but the plan constraints list is empty.",
18950
+ fix: "Copy operator constraints into plan.constraints. Name what is out of scope."
18951
+ });
18952
+ }
18953
+ if (plan.workstreams.some((ws) => !examsHold(ws))) {
18954
+ gaps.push({
18955
+ code: "exams",
18956
+ severity: "blocking",
18957
+ note: "A workstream expected outcome is missing a numeric baseline, range, or known instrument.",
18958
+ fix: "Give every expected_outcome a baseline, target range, and measured_by instrument from live data."
18959
+ });
18960
+ }
18961
+ if (inventedNumbers(plan, evidence)) {
18962
+ gaps.push({
18963
+ code: "invented_numbers",
18964
+ severity: "blocking",
18965
+ note: "An outcome baseline is not in the grounded snapshot.",
18966
+ fix: "Copy baselines from the health snapshot exactly."
18967
+ });
18968
+ }
18969
+ const blocking = gaps.filter((g) => g.severity === "blocking");
18970
+ return { pass: blocking.length === 0, gaps, blocking };
18971
+ }
18972
+ function asGapCode(value) {
18973
+ return typeof value === "string" && GAP_CODES.includes(value) ? value : null;
18974
+ }
18975
+ function parseCriticResponse(raw, opts) {
18976
+ if (!raw) {
18977
+ return {
18978
+ ship: false,
18979
+ score: 0,
18980
+ objective_echo: "",
18981
+ gaps: [
18982
+ {
18983
+ code: "thin_evidence",
18984
+ severity: "blocking",
18985
+ note: "Critic reply was not valid JSON.",
18986
+ fix: "Respond with the critic JSON object only."
18987
+ }
18988
+ ],
18989
+ killed_alternative: "",
18990
+ partner_pushback: ""
18991
+ };
18992
+ }
18993
+ const objectiveEcho = typeof raw.objective_echo === "string" ? raw.objective_echo : "";
18994
+ const killed = typeof raw.killed_alternative === "string" ? raw.killed_alternative.trim() : "";
18995
+ const pushback = typeof raw.partner_pushback === "string" ? raw.partner_pushback.trim() : "";
18996
+ const scoreRaw = typeof raw.score === "number" ? raw.score : Number(raw.score);
18997
+ const score = Number.isFinite(scoreRaw) ? Math.max(0, Math.min(100, scoreRaw)) : 0;
18998
+ const gaps = [];
18999
+ if (Array.isArray(raw.gaps)) {
19000
+ for (const item of raw.gaps) {
19001
+ if (!item || typeof item !== "object") continue;
19002
+ const rec = item;
19003
+ const code = asGapCode(rec.code);
19004
+ if (!code) continue;
19005
+ gaps.push({
19006
+ code,
19007
+ severity: rec.severity === "advisory" ? "advisory" : "blocking",
19008
+ note: typeof rec.note === "string" ? rec.note : "",
19009
+ fix: typeof rec.fix === "string" ? rec.fix : ""
19010
+ });
19011
+ }
19012
+ }
19013
+ if (objectiveEcho !== opts.armedObjective) {
19014
+ gaps.push({
19015
+ code: "objective_drift",
19016
+ severity: "blocking",
19017
+ note: "Critic objective_echo does not match the armed objective.",
19018
+ fix: "Echo the armed objective exactly."
19019
+ });
19020
+ }
19021
+ if (!killed) {
19022
+ gaps.push({
19023
+ code: "alternatives_killed",
19024
+ severity: "blocking",
19025
+ note: "Critic did not name an approach that failed newness, stake, or exams.",
19026
+ fix: "Name one killed alternative so the plan is not a restatement of the status quo."
19027
+ });
19028
+ }
19029
+ for (const gap of opts.rubricBlocking) {
19030
+ if (!gaps.some((g) => g.code === gap.code)) gaps.push(gap);
19031
+ }
19032
+ const blocking = gaps.filter((g) => g.severity === "blocking");
19033
+ const modelShip = raw.ship === true;
19034
+ const ship = modelShip && blocking.length === 0 && score >= opts.minScore && objectiveEcho === opts.armedObjective && killed.length > 0;
19035
+ return {
19036
+ ship,
19037
+ score,
19038
+ objective_echo: objectiveEcho,
19039
+ gaps,
19040
+ killed_alternative: killed,
19041
+ partner_pushback: pushback
19042
+ };
19043
+ }
19044
+ function lockPlanObjective(plan, armedObjective) {
19045
+ return { ...plan, objective: armedObjective };
19046
+ }
19047
+ var OWNER_TOKENS, VITAL_ALIASES, START_SOON_RE, GAP_CODES;
19048
+ var init_strategist_rubric = __esm({
19049
+ "src/ai/strategist-rubric.ts"() {
19050
+ "use strict";
19051
+ init_playbook();
19052
+ init_strategist_validate();
19053
+ init_formatters();
19054
+ OWNER_TOKENS = [
19055
+ "revops",
19056
+ "rev ops",
19057
+ "revenue ops",
19058
+ "sales manager",
19059
+ "sales ops",
19060
+ "cs lead",
19061
+ "customer success",
19062
+ "marketing ops",
19063
+ "ae lead",
19064
+ "account executive lead",
19065
+ "sdr lead",
19066
+ "bdr lead",
19067
+ "demand gen",
19068
+ "enablement"
19069
+ ];
19070
+ VITAL_ALIASES = {
19071
+ freshness: ["freshness", "stale", "zombie", "dead pipeline"],
19072
+ flow_rate: ["flow_rate", "flow rate", "stuck", "velocity", "stage"],
19073
+ drop_rate: ["drop_rate", "drop rate", "handoff", "leak"],
19074
+ signal_to_noise: ["signal_to_noise", "signal-to-noise", "signal to noise", "noise", "misdirected"],
19075
+ thread_depth: ["thread_depth", "thread depth", "single-thread", "single threaded", "multi-thread"]
19076
+ };
19077
+ START_SOON_RE = /\b(48\s*-?hours?|48h|tomorrow|today|monday|this week|within two days|within 2 days)\b/i;
19078
+ GAP_CODES = [
19079
+ "bottleneck",
19080
+ "stake",
19081
+ "owner",
19082
+ "start_48h",
19083
+ "effort",
19084
+ "scope",
19085
+ "exams",
19086
+ "alternatives_killed",
19087
+ "thin_evidence",
19088
+ "invented_numbers",
19089
+ "objective_drift"
19090
+ ];
19091
+ }
19092
+ });
19093
+
19094
+ // src/ai/strategist.ts
19095
+ function buildHealthSnapshot(computeResult, divergences) {
19096
+ const { aggregate, segments } = computeResult;
19097
+ return JSON.stringify(
19098
+ {
19099
+ aggregate: {
19100
+ overall_score: aggregate.overall_score,
19101
+ overall_status: aggregate.overall_status,
19102
+ gating_vital_sign: aggregate.gating_vital_sign,
19103
+ total_value_at_risk: aggregate.total_value_at_risk,
19104
+ vital_signs: Object.fromEntries(
19105
+ aggregate.vital_signs.map((v) => [
19106
+ v.vital_sign,
19107
+ { score: v.score, status: v.status, dollar_value: v.dollar_value, dollar_label: v.dollar_label }
19108
+ ])
19109
+ )
19110
+ },
19111
+ segment_names: segments.map((s) => s.segment.name),
19112
+ top_divergences: divergences.slice(0, 5).map((d) => ({
19113
+ segment: d.segmentName,
19114
+ vital_sign: d.vitalSign,
19115
+ segment_score: d.segmentScore,
19116
+ aggregate_score: d.aggregateScore,
19117
+ delta: d.delta
19118
+ }))
19119
+ },
19120
+ null,
19121
+ 2
19122
+ );
19123
+ }
19124
+ async function* strategistPlanSession(options) {
19125
+ assertReplAi(options.ctx);
19126
+ const llmCfg = loadLlmConfig();
19127
+ const todayIso = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
19128
+ const systemPrompt = buildStrategistSystemPrompt(todayIso);
19129
+ const tools2 = [...AGENTIC_TOOLS];
19130
+ if (isWebRetrievalEnabled()) tools2.push(WEB_SEARCH_TOOL);
19131
+ const toolCtx = {
19132
+ computeResult: options.computeResult,
19133
+ divergences: options.divergences
19134
+ };
19135
+ if (options.includeMetrics) {
19136
+ try {
19137
+ const { computeFullMetrics: computeFullMetrics2 } = await Promise.resolve().then(() => (init_compute(), compute_exports));
19138
+ toolCtx.metrics = (await computeFullMetrics2()).aggregate.metrics;
19139
+ } catch {
19140
+ }
18402
19141
  }
18403
19142
  const healthSnapshot = buildHealthSnapshot(options.computeResult, options.divergences);
18404
19143
  const messages = [
@@ -18574,6 +19313,15 @@ Respond with ONLY the corrected plan JSON object in the required schema (title,
18574
19313
  for (const issue of validated.issues) {
18575
19314
  yield { type: "notice", text: issue };
18576
19315
  }
19316
+ const rubric = scoreConsultantPlan(validated.plan, {
19317
+ snapshot: options.computeResult,
19318
+ constraintsNote: options.constraintsNote,
19319
+ todayIso,
19320
+ evidenceText
19321
+ });
19322
+ for (const gap of rubric.blocking) {
19323
+ yield { type: "notice", text: `Plan gap (${gap.code}): ${gap.note}` };
19324
+ }
18577
19325
  yield {
18578
19326
  type: "plan",
18579
19327
  plan: validated.plan,
@@ -18625,6 +19373,7 @@ var init_strategist2 = __esm({
18625
19373
  init_thread();
18626
19374
  init_strategist_prompt();
18627
19375
  init_strategist_validate();
19376
+ init_strategist_rubric();
18628
19377
  init_strategist_prompt();
18629
19378
  GROUND_MAX_ROUNDS = 6;
18630
19379
  BACKCAST_MAX_ROUNDS = 4;
@@ -18639,6 +19388,654 @@ var init_strategist2 = __esm({
18639
19388
  }
18640
19389
  });
18641
19390
 
19391
+ // src/ai/strategist-craft.ts
19392
+ import { createHash as createHash3 } from "crypto";
19393
+ function snapshotHash(snapshotJson) {
19394
+ return createHash3("sha256").update(snapshotJson).digest("hex").slice(0, 16);
19395
+ }
19396
+ function totalTokens(job) {
19397
+ return job.cumulative_input_tokens + job.cumulative_output_tokens;
19398
+ }
19399
+ function fallbackNotice(text) {
19400
+ return /grounded (playbook )?fallback/i.test(text);
19401
+ }
19402
+ function canShip(job, critic, rubricBlocking) {
19403
+ if (!critic.ship) return false;
19404
+ if (rubricBlocking.length > 0) return false;
19405
+ if (critic.score < job.min_score) return false;
19406
+ if (job.from_fallback && !job.revised_once) return false;
19407
+ return true;
19408
+ }
19409
+ function recordScoreStreak(job, score) {
19410
+ if (job.last_critic_score != null && score <= job.last_critic_score) {
19411
+ job.no_improve_streak += 1;
19412
+ } else {
19413
+ job.no_improve_streak = 0;
19414
+ }
19415
+ job.last_critic_score = score;
19416
+ }
19417
+ async function* strategistCraftSession(options) {
19418
+ assertReplAi(options.ctx);
19419
+ const todayIso = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
19420
+ const healthSnapshot = buildHealthSnapshot(options.computeResult, options.divergences);
19421
+ const maxRounds = options.maxRounds ?? DEFAULT_CRAFT_MAX_ROUNDS;
19422
+ const minScore = options.minScore ?? DEFAULT_CRAFT_MIN_SCORE;
19423
+ const complete = options.complete ?? completeWithFailover;
19424
+ const runIteration0 = options.runIteration0 ?? strategistPlanSession;
19425
+ const job = options.job ?? createRuminationJob({
19426
+ objective: options.objective,
19427
+ constraintsNote: options.constraintsNote,
19428
+ baselineBatchId: options.baselineBatchId,
19429
+ snapshotHash: snapshotHash(healthSnapshot),
19430
+ minScore,
19431
+ maxRounds,
19432
+ maxTokens: options.maxTokens
19433
+ });
19434
+ if (!options.job) job.objective = options.objective;
19435
+ job.min_score = minScore;
19436
+ job.max_rounds = maxRounds;
19437
+ if (options.maxTokens != null) job.max_tokens = options.maxTokens;
19438
+ saveRuminationJob(job);
19439
+ options.ctx.lastCraftJobId = job.status === "ready" ? void 0 : job.id;
19440
+ saveSessionState(options.ctx);
19441
+ const evidenceText = healthSnapshot;
19442
+ const armedObjective = job.objective;
19443
+ let lastMeta = { provider_used: "unknown", model_used: "unknown" };
19444
+ let measurable = { measurable_targets: 0, total_targets: 0 };
19445
+ const scorePlan = (plan) => scoreConsultantPlan(plan, {
19446
+ snapshot: options.computeResult,
19447
+ constraintsNote: options.constraintsNote ?? job.constraints_note,
19448
+ armedObjective,
19449
+ todayIso,
19450
+ evidenceText
19451
+ });
19452
+ const rememberBest = (plan, score) => {
19453
+ if (score == null) {
19454
+ if (!job.best_plan) job.best_plan = plan;
19455
+ return;
19456
+ }
19457
+ if (job.best_score == null || score > job.best_score) {
19458
+ job.best_score = score;
19459
+ job.best_plan = plan;
19460
+ }
19461
+ };
19462
+ try {
19463
+ if (job.status === "ready" && job.plan) {
19464
+ const locked = lockPlanObjective(job.plan, armedObjective);
19465
+ job.plan = locked;
19466
+ saveRuminationJob(job);
19467
+ yield {
19468
+ type: "plan",
19469
+ plan: locked,
19470
+ issues: [],
19471
+ measurable_targets: measurable.measurable_targets,
19472
+ total_targets: measurable.total_targets,
19473
+ baseline_batch_id: options.baselineBatchId ?? null
19474
+ };
19475
+ yield {
19476
+ type: "done",
19477
+ model_used: lastMeta.model_used,
19478
+ provider_used: lastMeta.provider_used,
19479
+ usage: lastMeta
19480
+ };
19481
+ return { job, plan: locked, status: job.status, stop_reason: job.stop_reason };
19482
+ }
19483
+ if (!job.plan) {
19484
+ yield { type: "stage", stage: "ground", label: "Craft \u2014 grounding, sequencing, stress-testing" };
19485
+ for await (const event of runIteration0({ ...options, objective: armedObjective })) {
19486
+ if (options.interrupted?.()) {
19487
+ job.status = "unfinished";
19488
+ job.stop_reason = "interrupt";
19489
+ saveRuminationJob(job);
19490
+ break;
19491
+ }
19492
+ if (event.type === "notice" && fallbackNotice(event.text)) {
19493
+ job.from_fallback = true;
19494
+ }
19495
+ if (event.type === "plan") {
19496
+ const locked = lockPlanObjective(event.plan, armedObjective);
19497
+ job.plan = locked;
19498
+ rememberBest(locked, null);
19499
+ measurable = {
19500
+ measurable_targets: event.measurable_targets,
19501
+ total_targets: event.total_targets
19502
+ };
19503
+ const rubric = scorePlan(locked);
19504
+ job.rubric_gaps = rubric.gaps;
19505
+ yield { ...event, plan: locked, issues: [...event.issues, ...rubric.blocking.map((g) => `Plan gap (${g.code}): ${g.note}`)] };
19506
+ } else if (event.type === "done") {
19507
+ lastMeta = event.usage ?? lastMeta;
19508
+ job.cumulative_input_tokens += event.usage?.input_tokens ?? 0;
19509
+ job.cumulative_output_tokens += event.usage?.output_tokens ?? 0;
19510
+ yield event;
19511
+ } else {
19512
+ yield event;
19513
+ }
19514
+ }
19515
+ saveRuminationJob(job);
19516
+ }
19517
+ if (!job.plan) {
19518
+ job.status = "unfinished";
19519
+ job.stop_reason = job.stop_reason ?? "error";
19520
+ saveRuminationJob(job);
19521
+ yield {
19522
+ type: "done",
19523
+ model_used: lastMeta.model_used,
19524
+ provider_used: lastMeta.provider_used,
19525
+ usage: lastMeta
19526
+ };
19527
+ return { job, plan: null, status: job.status, stop_reason: job.stop_reason };
19528
+ }
19529
+ rememberBest(job.plan, job.best_score);
19530
+ const systemPrompt = buildStrategistSystemPrompt(todayIso);
19531
+ const callText = async (surface, userContent, maxTokens) => {
19532
+ const result = await complete(
19533
+ {
19534
+ surface,
19535
+ messages: [{ role: "user", content: userContent }],
19536
+ system: systemPrompt,
19537
+ max_tokens: maxTokens
19538
+ },
19539
+ { ctx: options.ctx }
19540
+ );
19541
+ lastMeta = result.meta;
19542
+ return {
19543
+ text: result.response.text,
19544
+ input: result.response.token_usage?.input_tokens ?? result.meta.input_tokens ?? 0,
19545
+ output: result.response.token_usage?.output_tokens ?? result.meta.output_tokens ?? 0
19546
+ };
19547
+ };
19548
+ let stop;
19549
+ const startRound = (job.iterations.at(-1)?.round ?? 0) + 1;
19550
+ for (let round = startRound; round <= maxRounds; round++) {
19551
+ if (options.interrupted?.()) {
19552
+ stop = "interrupt";
19553
+ break;
19554
+ }
19555
+ if (job.max_tokens != null && totalTokens(job) >= job.max_tokens) {
19556
+ stop = "budget_tokens";
19557
+ break;
19558
+ }
19559
+ const plan = job.plan;
19560
+ if (!plan) {
19561
+ stop = "error";
19562
+ break;
19563
+ }
19564
+ const rubric = scorePlan(plan);
19565
+ job.rubric_gaps = rubric.gaps;
19566
+ const criticRaw = await callText(
19567
+ "strategist_critic",
19568
+ buildCriticMessage({
19569
+ objective: armedObjective,
19570
+ planJson: JSON.stringify(plan),
19571
+ rubricGaps: rubric.blocking.map((g) => `${g.code}: ${g.note}`).join("\n") || "(none)",
19572
+ healthSnapshot
19573
+ }),
19574
+ CRITIC_MAX_TOKENS
19575
+ );
19576
+ const critic = parseCriticResponse(parseJsonObjectFromText(criticRaw.text), {
19577
+ armedObjective,
19578
+ minScore,
19579
+ rubricBlocking: rubric.blocking
19580
+ });
19581
+ job.last_critic = critic;
19582
+ recordScoreStreak(job, critic.score);
19583
+ rememberBest(plan, critic.score);
19584
+ const tokensThisRound = criticRaw.input + criticRaw.output;
19585
+ let decision = "revise";
19586
+ if (canShip(job, critic, rubric.blocking)) {
19587
+ decision = "ship";
19588
+ } else if (job.no_improve_streak >= 2) {
19589
+ decision = "no-delta";
19590
+ }
19591
+ addRuminationIteration(job, {
19592
+ round,
19593
+ critic_score: critic.score,
19594
+ ship: critic.ship,
19595
+ blocking_codes: critic.gaps.filter((g) => g.severity === "blocking").map((g) => g.code),
19596
+ tokens_in: criticRaw.input,
19597
+ tokens_out: criticRaw.output,
19598
+ decision
19599
+ });
19600
+ yield {
19601
+ type: "craft_round",
19602
+ round,
19603
+ max: maxRounds,
19604
+ score: critic.score,
19605
+ tokensThisRound,
19606
+ tokensTotal: totalTokens(job),
19607
+ decision
19608
+ };
19609
+ yield {
19610
+ type: "notice",
19611
+ text: `craft ${round}/${maxRounds} \xB7 score ${critic.score} \xB7 ${tokensThisRound} tok this round \xB7 ${totalTokens(job)} total`
19612
+ };
19613
+ saveRuminationJob(job);
19614
+ if (decision === "ship") {
19615
+ stop = "ship";
19616
+ break;
19617
+ }
19618
+ if (decision === "no-delta") {
19619
+ stop = "no_delta";
19620
+ break;
19621
+ }
19622
+ const needsGround = critic.gaps.some((g) => g.code === "thin_evidence") && !job.extra_ground_used;
19623
+ if (needsGround) {
19624
+ job.extra_ground_used = true;
19625
+ yield { type: "notice", text: "Critic asked for thinner evidence \u2014 one extra ground pass." };
19626
+ for await (const event of runIteration0({ ...options, objective: armedObjective })) {
19627
+ if (event.type === "notice" && fallbackNotice(event.text)) {
19628
+ job.from_fallback = true;
19629
+ } else if (event.type === "plan") {
19630
+ job.plan = lockPlanObjective(event.plan, armedObjective);
19631
+ rememberBest(job.plan, null);
19632
+ measurable = {
19633
+ measurable_targets: event.measurable_targets,
19634
+ total_targets: event.total_targets
19635
+ };
19636
+ } else if (event.type !== "done") {
19637
+ yield event;
19638
+ }
19639
+ }
19640
+ saveRuminationJob(job);
19641
+ continue;
19642
+ }
19643
+ if (options.interrupted?.()) {
19644
+ stop = "interrupt";
19645
+ break;
19646
+ }
19647
+ if (job.max_tokens != null && totalTokens(job) >= job.max_tokens) {
19648
+ stop = "budget_tokens";
19649
+ break;
19650
+ }
19651
+ const reviseRaw = await callText(
19652
+ "strategist",
19653
+ buildReviseMessage({
19654
+ objective: armedObjective,
19655
+ planJson: JSON.stringify(plan),
19656
+ gapsJson: JSON.stringify(critic.gaps)
19657
+ }),
19658
+ REVISE_MAX_TOKENS
19659
+ );
19660
+ job.cumulative_input_tokens += reviseRaw.input;
19661
+ job.cumulative_output_tokens += reviseRaw.output;
19662
+ const parsed = parseJsonObjectFromText(reviseRaw.text);
19663
+ const validated = parsed ? validateStrategistPlan(parsed, { evidenceText, todayIso }) : null;
19664
+ if (validated) {
19665
+ job.plan = lockPlanObjective(validated.plan, armedObjective);
19666
+ job.revised_once = true;
19667
+ job.from_fallback = false;
19668
+ measurable = {
19669
+ measurable_targets: validated.measurableTargets,
19670
+ total_targets: validated.totalTargets
19671
+ };
19672
+ const nextRubric = scorePlan(job.plan);
19673
+ job.rubric_gaps = nextRubric.gaps;
19674
+ } else {
19675
+ yield { type: "notice", text: "Revise JSON invalid \u2014 keeping prior plan." };
19676
+ }
19677
+ saveRuminationJob(job);
19678
+ }
19679
+ if (!stop) stop = "budget_rounds";
19680
+ if (stop !== "ship" && job.best_plan) {
19681
+ job.plan = lockPlanObjective(job.best_plan, armedObjective);
19682
+ }
19683
+ job.stop_reason = stop;
19684
+ job.status = stop === "ship" ? "ready" : "unfinished";
19685
+ saveRuminationJob(job);
19686
+ if (job.plan) {
19687
+ const rubric = scorePlan(job.plan);
19688
+ yield {
19689
+ type: "plan",
19690
+ plan: job.plan,
19691
+ issues: rubric.blocking.map((g) => `Plan gap (${g.code}): ${g.note}`),
19692
+ measurable_targets: measurable.measurable_targets,
19693
+ total_targets: measurable.total_targets,
19694
+ baseline_batch_id: options.baselineBatchId ?? null
19695
+ };
19696
+ }
19697
+ yield {
19698
+ type: "done",
19699
+ model_used: lastMeta.model_used,
19700
+ provider_used: lastMeta.provider_used,
19701
+ failover: lastMeta.failover,
19702
+ usage: {
19703
+ ...lastMeta,
19704
+ input_tokens: job.cumulative_input_tokens,
19705
+ output_tokens: job.cumulative_output_tokens
19706
+ }
19707
+ };
19708
+ return { job, plan: job.plan, status: job.status, stop_reason: job.stop_reason };
19709
+ } catch (err) {
19710
+ job.status = "unfinished";
19711
+ job.stop_reason = "error";
19712
+ if (job.best_plan) job.plan = lockPlanObjective(job.best_plan, armedObjective);
19713
+ saveRuminationJob(job);
19714
+ throw err;
19715
+ }
19716
+ }
19717
+ function formatCraftRoundLine(event) {
19718
+ const score = event.score == null ? "\u2014" : String(event.score);
19719
+ const tok = event.tokensThisRound >= 1e3 ? `${Math.round(event.tokensThisRound / 1e3)}k` : String(event.tokensThisRound);
19720
+ const total = event.tokensTotal >= 1e3 ? `${Math.round(event.tokensTotal / 1e3)}k` : String(event.tokensTotal);
19721
+ return `craft ${event.round}/${event.max} \xB7 score ${score} \xB7 ${tok} tok this round \xB7 ${total} total`;
19722
+ }
19723
+ var CRITIC_MAX_TOKENS, REVISE_MAX_TOKENS, DEFAULT_CRAFT_MAX_ROUNDS, DEFAULT_CRAFT_MIN_SCORE;
19724
+ var init_strategist_craft = __esm({
19725
+ "src/ai/strategist-craft.ts"() {
19726
+ "use strict";
19727
+ init_failover();
19728
+ init_repl_api();
19729
+ init_context2();
19730
+ init_strategist2();
19731
+ init_strategist_validate();
19732
+ init_strategist_rubric();
19733
+ init_strategist_prompt();
19734
+ init_store3();
19735
+ CRITIC_MAX_TOKENS = 2048;
19736
+ REVISE_MAX_TOKENS = 8192;
19737
+ DEFAULT_CRAFT_MAX_ROUNDS = 5;
19738
+ DEFAULT_CRAFT_MIN_SCORE = 85;
19739
+ }
19740
+ });
19741
+
19742
+ // src/ruminations/handoff.ts
19743
+ function renderCraftHandoffMarkdown(opts) {
19744
+ const { plan } = opts;
19745
+ const lines = [];
19746
+ lines.push("# Action plan \u2014 NTRP craft");
19747
+ lines.push("");
19748
+ lines.push("Use this file as a prompt for another agent or as a morning brief.");
19749
+ lines.push("");
19750
+ lines.push("## The Call");
19751
+ lines.push("");
19752
+ lines.push(plan.summary_30k);
19753
+ lines.push("");
19754
+ lines.push("## Locked objective");
19755
+ lines.push("");
19756
+ lines.push(plan.objective);
19757
+ lines.push("");
19758
+ lines.push(renderConstraintHeading(opts.constraintLine).trimEnd());
19759
+ lines.push("");
19760
+ lines.push(renderScopeHeading(plan.constraints, opts.outOfScope).trimEnd());
19761
+ lines.push("");
19762
+ lines.push("## Hypothesis");
19763
+ lines.push("");
19764
+ lines.push(plan.hypothesis);
19765
+ lines.push("");
19766
+ lines.push(renderKilledAlternativeLine(opts.killedAlternative));
19767
+ lines.push("");
19768
+ lines.push(renderEffortHeading(plan.workstreams).trimEnd());
19769
+ lines.push("");
19770
+ lines.push("## Workstreams");
19771
+ lines.push("");
19772
+ for (const ws of plan.workstreams) {
19773
+ lines.push(`### ${ws.order}. ${ws.title}`);
19774
+ lines.push("");
19775
+ lines.push(`- Problem: ${ws.problem}`);
19776
+ lines.push(`- Why this order: ${ws.rationale}`);
19777
+ if (ws.actions[0]) lines.push(`- First action (48h): ${ws.actions[0]}`);
19778
+ lines.push(`- Effort: ~${Math.round(ws.effort_hours)} team-hours`);
19779
+ lines.push(
19780
+ `- 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})`
19781
+ );
19782
+ lines.push(
19783
+ `- Contingency: if ${ws.contingency.trigger} (check ${ws.contingency.trigger_check_date}) -> ${ws.contingency.fallback}`
19784
+ );
19785
+ lines.push("");
19786
+ }
19787
+ if (plan.risks.length > 0) {
19788
+ lines.push("## Risks");
19789
+ lines.push("");
19790
+ for (const r of plan.risks) lines.push(`- ${r}`);
19791
+ lines.push("");
19792
+ }
19793
+ if (plan.assumptions.length > 0) {
19794
+ lines.push("## Assumptions");
19795
+ lines.push("");
19796
+ for (const a of plan.assumptions) lines.push(`- ${a}`);
19797
+ lines.push("");
19798
+ }
19799
+ lines.push(renderReviewHeading({ cadence: plan.review_cadence, slug: opts.slug }).trimEnd());
19800
+ lines.push("");
19801
+ if (opts.journalPath) {
19802
+ lines.push("## Craft log");
19803
+ lines.push("");
19804
+ lines.push(`Status: ${opts.status}`);
19805
+ lines.push(`Log: ${opts.journalPath}`);
19806
+ if (opts.libraryPath) lines.push(`Strategy library: ${opts.libraryPath}`);
19807
+ lines.push("");
19808
+ } else if (opts.libraryPath) {
19809
+ lines.push("## Strategy library");
19810
+ lines.push("");
19811
+ lines.push(opts.libraryPath);
19812
+ lines.push("");
19813
+ }
19814
+ return lines.join("\n");
19815
+ }
19816
+ function writeCraftPlanHandoff(opts) {
19817
+ const journalPath = opts.jobId ? ruminationLogPath(opts.jobId) : void 0;
19818
+ const markdown = renderCraftHandoffMarkdown({
19819
+ plan: opts.plan,
19820
+ journalPath,
19821
+ libraryPath: opts.libraryPath,
19822
+ status: opts.status,
19823
+ constraintLine: opts.constraintLine,
19824
+ killedAlternative: opts.killedAlternative,
19825
+ outOfScope: opts.outOfScope,
19826
+ slug: opts.slug
19827
+ });
19828
+ const path = resolveArchivePath("prompt:plan", `handoff-plan-${exportStamp()}.md`);
19829
+ writeRedactedText(path, markdown);
19830
+ const event = recordExportWrite({
19831
+ kind: "prompt:plan",
19832
+ path,
19833
+ sessionId: opts.sessionId,
19834
+ title: `${opts.plan.title} craft plan`
19835
+ });
19836
+ return { path, inboxPath: event.inbox_path };
19837
+ }
19838
+ var init_handoff = __esm({
19839
+ "src/ruminations/handoff.ts"() {
19840
+ "use strict";
19841
+ init_exports_registry();
19842
+ init_redact_write();
19843
+ init_library();
19844
+ init_store3();
19845
+ }
19846
+ });
19847
+
19848
+ // src/services/strategist-run.ts
19849
+ async function assertComputableData() {
19850
+ await initSchema();
19851
+ let counts = {};
19852
+ try {
19853
+ counts = await getEntityCounts();
19854
+ } catch {
19855
+ counts = {};
19856
+ }
19857
+ if (!Object.values(counts).some((n) => (n ?? 0) > 0)) {
19858
+ throw new NtrpError(
19859
+ "strategy_no_data",
19860
+ "No pipeline data loaded. Ingest a CSV, run demo data, then craft a plan.",
19861
+ 4 /* NoData */
19862
+ );
19863
+ }
19864
+ }
19865
+ function rememberCraftJob(ctx, job) {
19866
+ if (!job) return;
19867
+ ctx.lastCraftJobId = job.status === "ready" ? void 0 : job.id;
19868
+ saveSessionState(ctx);
19869
+ }
19870
+ async function executeStrategistJob(req) {
19871
+ if (!canUseReplAi(req.ctx)) {
19872
+ throw new NtrpError(
19873
+ "strategy_no_key",
19874
+ "No LLM API key configured. Run /connect and paste a key, then retry craft.",
19875
+ 3 /* Auth */
19876
+ );
19877
+ }
19878
+ const resumeJob = req.resumeId ? loadRuminationJob(req.resumeId) : null;
19879
+ if (req.resumeId && !resumeJob) {
19880
+ throw new NtrpError(
19881
+ "strategy_resume_missing",
19882
+ `No craft job "${req.resumeId}". Check ~/.ntrp/ruminations/.`,
19883
+ 2 /* Usage */
19884
+ );
19885
+ }
19886
+ if (resumeJob) rememberCraftJob(req.ctx, resumeJob);
19887
+ const objective = resumeJob?.objective || req.objective;
19888
+ if (!objective.trim()) {
19889
+ throw new NtrpError(
19890
+ "strategy_objective_required",
19891
+ 'strategy craft requires an objective. Example: ntrp strategy craft "cut stale pipeline before Q4"',
19892
+ 2 /* Usage */
19893
+ );
19894
+ }
19895
+ await assertComputableData();
19896
+ const inputs = await prepareStrategistInputs(req.ctx, objective);
19897
+ const notices = [];
19898
+ let plan = resumeJob?.plan ?? null;
19899
+ let stats = { measurable_targets: 0, total_targets: 0 };
19900
+ let meta = {};
19901
+ let job = resumeJob ?? void 0;
19902
+ const sessionOpts = {
19903
+ objective,
19904
+ computeResult: inputs.snapshot,
19905
+ divergences: inputs.divergences,
19906
+ includeMetrics: inputs.includeMetrics,
19907
+ memoryBlock: inputs.memoryBlock,
19908
+ gapAuditBlock: inputs.gapAuditBlock,
19909
+ constraintsNote: req.constraintsNote ?? resumeJob?.constraints_note,
19910
+ baselineBatchId: inputs.baselineBatchId,
19911
+ ctx: req.ctx
19912
+ };
19913
+ const useCraft = req.mode === "craft" && req.untilReady !== false;
19914
+ const handleEvent = (event) => {
19915
+ req.onEvent?.(event);
19916
+ if (event.type === "notice") notices.push(event.text);
19917
+ if (event.type === "plan") {
19918
+ plan = event.plan;
19919
+ stats = {
19920
+ measurable_targets: event.measurable_targets,
19921
+ total_targets: event.total_targets
19922
+ };
19923
+ }
19924
+ if (event.type === "done") {
19925
+ meta = event.usage ?? { model_used: event.model_used, provider_used: event.provider_used };
19926
+ }
19927
+ if (event.type === "craft_round") {
19928
+ notices.push(formatCraftRoundLine(event));
19929
+ }
19930
+ };
19931
+ if (useCraft) {
19932
+ const gen = strategistCraftSession({
19933
+ ...sessionOpts,
19934
+ maxRounds: req.maxRounds ?? resumeJob?.max_rounds ?? DEFAULT_CRAFT_MAX_ROUNDS,
19935
+ maxTokens: req.maxTokens ?? resumeJob?.max_tokens,
19936
+ minScore: req.minScore ?? resumeJob?.min_score ?? DEFAULT_CRAFT_MIN_SCORE,
19937
+ job: resumeJob ?? void 0,
19938
+ interrupted: req.interrupted
19939
+ });
19940
+ let next = await gen.next();
19941
+ while (!next.done) {
19942
+ handleEvent(next.value);
19943
+ next = await gen.next();
19944
+ }
19945
+ job = next.value.job;
19946
+ plan = next.value.plan;
19947
+ } else {
19948
+ for await (const event of strategistPlanSession(sessionOpts)) {
19949
+ handleEvent(event);
19950
+ }
19951
+ }
19952
+ const blocking = job?.rubric_gaps.filter((g) => g.severity === "blocking").map((g) => g.code) ?? [];
19953
+ const status = job?.status === "unfinished" || !plan ? "unfinished" : "ready";
19954
+ const result = {
19955
+ plan,
19956
+ status,
19957
+ stop_reason: job?.stop_reason,
19958
+ objective,
19959
+ critic_score: job?.last_critic?.score ?? null,
19960
+ rounds: job?.iterations.length ?? 0,
19961
+ usage: meta,
19962
+ journal_path: job ? ruminationLogPath(job.id) : void 0,
19963
+ blocking_gaps: blocking,
19964
+ measurable_targets: stats.measurable_targets,
19965
+ total_targets: stats.total_targets,
19966
+ notices,
19967
+ job
19968
+ };
19969
+ const constraintLine = formatConstraintLine(inputs.snapshot.aggregate);
19970
+ const killedAlternative = job?.last_critic?.killed_alternative;
19971
+ if (plan && req.save) {
19972
+ const persisted = await persistStrategistPlan(plan, {
19973
+ baselineBatchId: inputs.baselineBatchId,
19974
+ craftLogPath: job ? ruminationLogPath(job.id) : void 0,
19975
+ constraintLine,
19976
+ killedAlternative
19977
+ });
19978
+ result.slug = persisted.strategy.slug;
19979
+ result.library_path = persisted.library_path;
19980
+ req.ctx.deliverables.push({
19981
+ kind: "strategy",
19982
+ at: (/* @__PURE__ */ new Date()).toISOString(),
19983
+ path: persisted.library_path,
19984
+ note: plan.title
19985
+ });
19986
+ if (job) {
19987
+ job.library_path = persisted.library_path;
19988
+ }
19989
+ }
19990
+ if (plan && req.handoff) {
19991
+ const written = writeCraftPlanHandoff({
19992
+ plan,
19993
+ jobId: job?.id,
19994
+ status: result.status,
19995
+ libraryPath: result.library_path,
19996
+ sessionId: req.ctx.sessionId,
19997
+ constraintLine,
19998
+ killedAlternative,
19999
+ slug: result.slug
20000
+ });
20001
+ result.handoff_path = written.path;
20002
+ result.inbox_path = written.inboxPath;
20003
+ if (job) {
20004
+ job.handoff_path = written.path;
20005
+ job.inbox_path = written.inboxPath;
20006
+ }
20007
+ req.ctx.deliverables.push({
20008
+ kind: "prompt:plan",
20009
+ at: (/* @__PURE__ */ new Date()).toISOString(),
20010
+ path: written.path,
20011
+ note: plan.title
20012
+ });
20013
+ }
20014
+ if (job) {
20015
+ if (req.ctx.strategistState) req.ctx.strategistState.ruminationId = job.id;
20016
+ saveRuminationJob(job);
20017
+ rememberCraftJob(req.ctx, job);
20018
+ }
20019
+ return result;
20020
+ }
20021
+ var init_strategist_run = __esm({
20022
+ "src/services/strategist-run.ts"() {
20023
+ "use strict";
20024
+ init_context2();
20025
+ init_errors2();
20026
+ init_types2();
20027
+ init_repl_api();
20028
+ init_strategist2();
20029
+ init_strategist_craft();
20030
+ init_formatters();
20031
+ init_strategist();
20032
+ init_queries();
20033
+ init_schema();
20034
+ init_store3();
20035
+ init_handoff();
20036
+ }
20037
+ });
20038
+
18642
20039
  // src/output/strategy-brief.ts
18643
20040
  import chalk16 from "chalk";
18644
20041
  function printWrapped(text, width, prefix = INDENT, style) {
@@ -18730,6 +20127,40 @@ function printStrategyBrief(plan, stats) {
18730
20127
  console.log(`${INDENT}${coverageStyled}${chalk16.dim(` \xB7 ~${Math.round(totalHours)} total team hours across ${plan.workstreams.length} workstream${plan.workstreams.length === 1 ? "" : "s"}`)}`);
18731
20128
  console.log();
18732
20129
  }
20130
+ function printCraftWrapUp(opts) {
20131
+ console.log();
20132
+ if (opts.status === "ready") {
20133
+ console.log(INDENT + paint("accent", "Plan ready"));
20134
+ if (opts.library_path) console.log(INDENT + chalk16.dim(opts.library_path));
20135
+ if (opts.slug) {
20136
+ console.log(
20137
+ INDENT + chalk16.dim("Type ") + paint("accent", `/strategy review ${opts.slug}`) + chalk16.dim(" to check progress.")
20138
+ );
20139
+ }
20140
+ const handoff = opts.inbox_path ?? opts.handoff_path;
20141
+ if (handoff) console.log(INDENT + chalk16.dim(handoff));
20142
+ } else {
20143
+ console.log(INDENT + paint("accent", "Best so far."));
20144
+ const path = opts.library_path ?? opts.handoff_path ?? opts.inbox_path;
20145
+ if (path) console.log(INDENT + chalk16.dim(path));
20146
+ if (opts.oneShot && opts.jobId) {
20147
+ console.log(
20148
+ INDENT + chalk16.dim("Resume: ") + paint("accent", `ntrp strategy craft --resume ${opts.jobId}`)
20149
+ );
20150
+ } else {
20151
+ console.log(
20152
+ INDENT + chalk16.dim("Type ") + chalk16.cyan("keep going") + chalk16.dim(" to continue.")
20153
+ );
20154
+ }
20155
+ }
20156
+ if (opts.oneShot && opts.journal_path) {
20157
+ console.log(INDENT + chalk16.dim(`Craft log: ${opts.journal_path}`));
20158
+ }
20159
+ console.log();
20160
+ }
20161
+ function isCraftRoundNotice(text) {
20162
+ return /^craft \d+\/\d+/.test(text);
20163
+ }
18733
20164
  var INDENT;
18734
20165
  var init_strategy_brief = __esm({
18735
20166
  "src/output/strategy-brief.ts"() {
@@ -18744,7 +20175,9 @@ var init_strategy_brief = __esm({
18744
20175
  var strategist_flow_exports = {};
18745
20176
  __export(strategist_flow_exports, {
18746
20177
  extractObjectiveSeed: () => extractObjectiveSeed,
20178
+ handleKeepGoingLine: () => handleKeepGoingLine,
18747
20179
  handleStrategizeFlow: () => handleStrategizeFlow,
20180
+ isKeepGoingIntent: () => isKeepGoingIntent,
18748
20181
  isStrategistIntent: () => isStrategistIntent,
18749
20182
  promptQueuedAiStrategist: () => promptQueuedAiStrategist,
18750
20183
  queueStrategistForAnalysis: () => queueStrategistForAnalysis,
@@ -18767,7 +20200,8 @@ function queueStrategistForAnalysis(ctx, opts) {
18767
20200
  ctx.strategistState = {
18768
20201
  step: "awaiting_analysis",
18769
20202
  objective: opts.seed,
18770
- origin: opts.origin
20203
+ origin: opts.origin,
20204
+ mode: opts.mode
18771
20205
  };
18772
20206
  saveSessionState(ctx);
18773
20207
  console.log();
@@ -18792,7 +20226,7 @@ async function startStrategistFlow(ctx, opts) {
18792
20226
  objective = (snapshot ? proposeObjectiveFromSnapshot(snapshot) : null) ?? "";
18793
20227
  }
18794
20228
  if (!objective) {
18795
- ctx.strategistState = { step: "objective_input", origin: opts.origin };
20229
+ ctx.strategistState = { step: "objective_input", origin: opts.origin, mode: opts.mode };
18796
20230
  saveSessionState(ctx);
18797
20231
  console.log();
18798
20232
  console.log(" " + chalk17.dim('What is the objective? State a finish line. Example: "cut stale pipeline in half before Q4".'));
@@ -18800,7 +20234,7 @@ async function startStrategistFlow(ctx, opts) {
18800
20234
  recordMessage(ctx, "agent", "Strategist: asked for objective");
18801
20235
  return "Awaiting objective";
18802
20236
  }
18803
- ctx.strategistState = { step: "objective_confirm", objective, origin: opts.origin };
20237
+ ctx.strategistState = { step: "objective_confirm", objective, origin: opts.origin, mode: opts.mode };
18804
20238
  saveSessionState(ctx);
18805
20239
  printObjectiveCard(ctx, objective, !opts.seed);
18806
20240
  recordMessage(ctx, "agent", `Strategist objective proposed: ${objective}`);
@@ -18813,7 +20247,8 @@ async function resumeStrategistAfterCompute(ctx) {
18813
20247
  console.log(" " + paint("accent", "Analysis is ready. The strategy session continues."));
18814
20248
  await startStrategistFlow(ctx, {
18815
20249
  seed: state2.objective,
18816
- origin: state2.origin ?? "nl"
20250
+ origin: state2.origin ?? "nl",
20251
+ mode: state2.mode
18817
20252
  });
18818
20253
  }
18819
20254
  function promptQueuedAiStrategist(ctx) {
@@ -18823,6 +20258,64 @@ function promptQueuedAiStrategist(ctx) {
18823
20258
  }
18824
20259
  printObjectiveCard(ctx, state2.objective, true);
18825
20260
  }
20261
+ function isKeepGoingIntent(input) {
20262
+ return CRAFT_RE.test(input.trim());
20263
+ }
20264
+ async function handleKeepGoingLine(ctx, input) {
20265
+ const line = input.trim();
20266
+ recordMessage(ctx, "user", line);
20267
+ const job = unfinishedCraftJob(ctx.lastCraftJobId);
20268
+ if (!job) {
20269
+ console.log();
20270
+ console.log(
20271
+ " " + chalk17.dim("No plan in progress. Type ") + chalk17.cyan("how should we fix this?") + chalk17.dim(" to start one.")
20272
+ );
20273
+ console.log();
20274
+ recordMessage(ctx, "agent", "No plan in progress");
20275
+ return "No plan in progress";
20276
+ }
20277
+ if (!canUseReplAi(ctx)) {
20278
+ console.log();
20279
+ console.log(
20280
+ " " + chalk17.dim("Type ") + chalk17.cyan("/connect") + chalk17.dim(" to connect a key. Then type keep going.")
20281
+ );
20282
+ console.log();
20283
+ recordMessage(ctx, "agent", "Keep going needs a key");
20284
+ return "Keep going needs a key";
20285
+ }
20286
+ const spinner = makeSpinner("Working the plan\u2026");
20287
+ try {
20288
+ const result = await executeStrategistJob({
20289
+ ctx,
20290
+ objective: job.objective,
20291
+ constraintsNote: job.constraints_note,
20292
+ mode: "craft",
20293
+ untilReady: true,
20294
+ save: true,
20295
+ handoff: true,
20296
+ resumeId: job.id,
20297
+ onEvent: (event) => {
20298
+ if (event.type === "thinking") {
20299
+ spinner.stop();
20300
+ console.log(" " + chalk17.dim.italic(event.text));
20301
+ spinner.start();
20302
+ spinner.text = "Working the plan\u2026";
20303
+ }
20304
+ }
20305
+ });
20306
+ spinner.stop();
20307
+ printCraftReplResult(ctx, result, job.objective);
20308
+ ctx.strategistState = void 0;
20309
+ saveSessionState(ctx);
20310
+ return result.status === "ready" ? `Strategy crafted: ${result.plan?.title}` : "Strategy craft unfinished";
20311
+ } catch (err) {
20312
+ spinner.fail("Strategy craft failed");
20313
+ console.error(" " + chalk17.red(String(err.message ?? err)));
20314
+ ctx.strategistState = void 0;
20315
+ saveSessionState(ctx);
20316
+ return;
20317
+ }
20318
+ }
18826
20319
  async function handleStrategizeFlow(input, ctx) {
18827
20320
  const state2 = ctx.strategistState;
18828
20321
  if (!state2) return;
@@ -18849,6 +20342,11 @@ async function handleStrategizeFlow(input, ctx) {
18849
20342
  printObjectiveCard(ctx, state2.objective, false);
18850
20343
  return "Objective proposed";
18851
20344
  }
20345
+ if (CRAFT_RE.test(line)) {
20346
+ state2.mode = "craft";
20347
+ saveSessionState(ctx);
20348
+ return runStrategistSession(ctx);
20349
+ }
18852
20350
  if (CONFIRM_RE.test(line)) {
18853
20351
  return runStrategistSession(ctx);
18854
20352
  }
@@ -18899,7 +20397,7 @@ async function runStrategistSession(ctx) {
18899
20397
  saveSessionState(ctx);
18900
20398
  return "Skeleton plan (awaiting connect)";
18901
20399
  }
18902
- if (ctx.rl && !state2.constraintsNote) {
20400
+ if (ctx.rl && !state2.constraintsNote && state2.mode !== "craft") {
18903
20401
  const prompts = createPromptSession(ctx.rl, ctx);
18904
20402
  try {
18905
20403
  const note = await prompts.ask(
@@ -18913,7 +20411,39 @@ async function runStrategistSession(ctx) {
18913
20411
  }
18914
20412
  }
18915
20413
  console.log();
18916
- const spinner = makeSpinner("Reading live data\u2026");
20414
+ const spinner = makeSpinner(state2.mode === "craft" ? "Working the plan\u2026" : "Reading live data\u2026");
20415
+ if (state2.mode === "craft") {
20416
+ try {
20417
+ const result = await executeStrategistJob({
20418
+ ctx,
20419
+ objective,
20420
+ constraintsNote: state2.constraintsNote,
20421
+ mode: "craft",
20422
+ untilReady: true,
20423
+ save: true,
20424
+ handoff: true,
20425
+ onEvent: (event) => {
20426
+ if (event.type === "thinking") {
20427
+ spinner.stop();
20428
+ console.log(" " + chalk17.dim.italic(event.text));
20429
+ spinner.start();
20430
+ spinner.text = "Working the plan\u2026";
20431
+ }
20432
+ }
20433
+ });
20434
+ spinner.stop();
20435
+ printCraftReplResult(ctx, result, objective);
20436
+ ctx.strategistState = void 0;
20437
+ saveSessionState(ctx);
20438
+ return result.status === "ready" ? `Strategy crafted: ${result.plan?.title}` : "Strategy craft unfinished";
20439
+ } catch (err) {
20440
+ spinner.fail("Strategy craft failed");
20441
+ console.error(" " + chalk17.red(String(err.message ?? err)));
20442
+ ctx.strategistState = void 0;
20443
+ saveSessionState(ctx);
20444
+ return;
20445
+ }
20446
+ }
18917
20447
  let plan = null;
18918
20448
  let stats = { measurable_targets: 0, total_targets: 0 };
18919
20449
  let baselineBatchId = null;
@@ -18997,7 +20527,10 @@ async function runStrategistSession(ctx) {
18997
20527
  }
18998
20528
  if (saved) {
18999
20529
  try {
19000
- const persisted = await persistStrategistPlan(plan, { baselineBatchId });
20530
+ const persisted = await persistStrategistPlan(plan, {
20531
+ baselineBatchId,
20532
+ constraintLine: formatConstraintLine(ctx.snapshot.computeResult?.aggregate)
20533
+ });
19001
20534
  ctx.deliverables.push({
19002
20535
  kind: "strategy",
19003
20536
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -19045,19 +20578,54 @@ async function ensureSnapshot(ctx) {
19045
20578
  return null;
19046
20579
  }
19047
20580
  }
20581
+ function printCraftReplResult(ctx, result, objective) {
20582
+ if (result.plan) {
20583
+ printStrategyBrief(result.plan, {
20584
+ measurable_targets: result.measurable_targets,
20585
+ total_targets: result.total_targets
20586
+ });
20587
+ } else {
20588
+ console.log(" " + chalk17.dim("(No plan produced)"));
20589
+ }
20590
+ for (const notice of result.notices.slice(0, 8)) {
20591
+ if (!isCraftRoundNotice(notice)) console.log(" " + chalk17.dim(notice));
20592
+ }
20593
+ printLlmAttribution(result.usage);
20594
+ if (result.library_path) creditStrategySession(ctx);
20595
+ printCraftWrapUp({
20596
+ status: result.status,
20597
+ library_path: result.library_path,
20598
+ inbox_path: result.inbox_path,
20599
+ handoff_path: result.handoff_path,
20600
+ slug: result.slug,
20601
+ jobId: result.job?.id,
20602
+ oneShot: false
20603
+ });
20604
+ if (result.library_path || result.handoff_path) {
20605
+ recordMessage(ctx, "agent", `Strategy crafted: ${result.plan?.title ?? objective} (${result.status})`);
20606
+ }
20607
+ }
19048
20608
  function printObjectiveCard(ctx, objective, proposed) {
20609
+ const craftMode = ctx.strategistState?.mode === "craft";
19049
20610
  console.log();
19050
20611
  console.log(" " + chalk17.bold("Strategy session"));
19051
20612
  console.log(
19052
20613
  " " + chalk17.dim(proposed ? "Proposed objective: " : "Objective: ") + paint("accent", objective)
19053
20614
  );
19054
20615
  console.log(
19055
- " " + chalk17.dim("NTRP uses your live data. It sequences the work and sets measured milestones. It then stress-tests the plan.")
20616
+ " " + chalk17.dim(
20617
+ craftMode ? "NTRP uses your live data. It keeps working until the plan is ready, then saves it and writes a handoff." : "NTRP uses your live data. It sequences the work and sets measured milestones. It then stress-tests the plan."
20618
+ )
19056
20619
  );
19057
20620
  console.log();
19058
20621
  console.log(
19059
20622
  " " + chalk17.dim("Confirm? ") + chalk17.cyan("\u23CE yes") + chalk17.dim(" \xB7 ") + chalk17.cyan("b back") + chalk17.dim(" \xB7 ") + chalk17.cyan("adjust") + chalk17.dim(" \xB7 ") + chalk17.cyan("cancel")
19060
20623
  );
20624
+ if (!craftMode) {
20625
+ console.log(
20626
+ " " + chalk17.dim("or type ") + chalk17.cyan("keep going") + chalk17.dim(" to keep working until the plan is ready")
20627
+ );
20628
+ }
19061
20629
  console.log();
19062
20630
  }
19063
20631
  async function printKeylessSkeletonPlan(ctx, objective) {
@@ -19119,7 +20687,7 @@ async function resumeStrategistAfterConnect(ctx) {
19119
20687
  printObjectiveCard(ctx, state2.objective, true);
19120
20688
  return true;
19121
20689
  }
19122
- var STRATEGIST_INTENT_RE, CANCEL_RE, CONFIRM_RE, ADJUST_RE, QUESTION_RE;
20690
+ var STRATEGIST_INTENT_RE, CANCEL_RE, CONFIRM_RE, ADJUST_RE, QUESTION_RE, CRAFT_RE;
19123
20691
  var init_strategist_flow = __esm({
19124
20692
  "src/conversation/strategist-flow.ts"() {
19125
20693
  "use strict";
@@ -19133,15 +20701,18 @@ var init_strategist_flow = __esm({
19133
20701
  init_divergence();
19134
20702
  init_strategist();
19135
20703
  init_strategist2();
20704
+ init_strategist_run();
19136
20705
  init_strategy_brief();
19137
20706
  init_llm_attribution();
19138
20707
  init_time_bank();
19139
20708
  init_formatters();
20709
+ init_store3();
19140
20710
  STRATEGIST_INTENT_RE = /\b(strateg(y|ize|ic)|game\s?plan|battle\s?plan|roadmap|(build|draft|make|create|put together)\s+(me\s+)?(a\s+|the\s+)?plan\b|plan\s+(to|for)\s+(fix|improv|reduc|recover|hit|reach|get|grow|turn)|how\s+(should|do|can)\s+we\s+(fix|approach|tackle|attack|prioritize|sequence|turn\s+(this|it)\s+around)|what\s+should\s+we\s+(do|fix|tackle|prioritize|focus\s+on)\s+(first|next)|what\s+order\s+should|where\s+(do|should)\s+we\s+start)\b/i;
19141
20711
  CANCEL_RE = /^(cancel|stop|quit|abort|never\s?mind|nevermind|forget it)\s*[.!]?\s*$/i;
19142
20712
  CONFIRM_RE = /^(y|yes|yep|yeah|confirm|go|go ahead|do it|proceed|sounds good|looks good|lgtm|ok|okay)\b/i;
19143
20713
  ADJUST_RE = /^(n|no|adjust|change|edit|different|not quite|refine)\b/i;
19144
20714
  QUESTION_RE = /(\?\s*$)|^(what|why|how|when|which|where|who)\b/i;
20715
+ CRAFT_RE = /^(craft|keep going|until tight|keep working)\b/i;
19145
20716
  }
19146
20717
  });
19147
20718
 
@@ -19252,7 +20823,9 @@ function buildSituationalAwarenessBlock(ctx, opts = {}) {
19252
20823
  );
19253
20824
  } else if (phase === "orient") {
19254
20825
  lines.push("Already done: nothing locked yet");
19255
- lines.push("Available now: help the user name a focus; CLI will propose scope from their words");
20826
+ lines.push(
20827
+ "Available now: help the user name a focus; CLI will propose scope from their words. If \u23CE /update is armed, the CLI owns the install confirm \u2014 do not re-ask about updating"
20828
+ );
19256
20829
  } else if (phase === "think") {
19257
20830
  lines.push("Already done: analysis complete; think channel open");
19258
20831
  lines.push("Available now: socratic exploration; draft_strategy / draft_handoff when ready to graduate");
@@ -20169,8 +21742,8 @@ var init_bundle = __esm({
20169
21742
  });
20170
21743
 
20171
21744
  // src/repositories/markdown.ts
20172
- import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync14 } from "fs";
20173
- import { basename as basename5, dirname as dirname4, join as join22, resolve as resolve8 } from "path";
21745
+ import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync16 } from "fs";
21746
+ import { basename as basename5, dirname as dirname5, join as join25, resolve as resolve8 } from "path";
20174
21747
  import { stringify as stringifyYaml2 } from "yaml";
20175
21748
  function renderMarkdownFiles(pkg) {
20176
21749
  const bundleJson = JSON.stringify(pkg, null, 2) + "\n";
@@ -20390,12 +21963,12 @@ var init_markdown2 = __esm({
20390
21963
  write(pkg) {
20391
21964
  const root = getRootPath(pkg.target);
20392
21965
  const files = renderMarkdownFiles(pkg);
20393
- mkdirSync12(root, { recursive: true });
21966
+ mkdirSync13(root, { recursive: true });
20394
21967
  const written = [];
20395
21968
  for (const file of files) {
20396
- const absolutePath = join22(root, file.relativePath);
20397
- mkdirSync12(dirname4(absolutePath), { recursive: true });
20398
- writeFileSync14(absolutePath, file.contents, "utf-8");
21969
+ const absolutePath = join25(root, file.relativePath);
21970
+ mkdirSync13(dirname5(absolutePath), { recursive: true });
21971
+ writeFileSync16(absolutePath, file.contents, "utf-8");
20399
21972
  written.push(absolutePath);
20400
21973
  }
20401
21974
  return {
@@ -20498,7 +22071,7 @@ var init_publish = __esm({
20498
22071
  });
20499
22072
 
20500
22073
  // src/services/smoke-protocol.ts
20501
- import { join as join23 } from "path";
22074
+ import { join as join26 } from "path";
20502
22075
  function isSmokeProtocolTrigger(input) {
20503
22076
  return normalize(input).includes(SMOKE_TRIGGER_PHRASE);
20504
22077
  }
@@ -20534,7 +22107,7 @@ async function runSmokeProtocol(_input, ctx) {
20534
22107
  });
20535
22108
  const proposalResult = await proposeRepositoryExport({
20536
22109
  target: "markdown",
20537
- directory: join23(getExportsDir(), "repository-smoke"),
22110
+ directory: join26(getExportsDir(), "repository-smoke"),
20538
22111
  source: "smoke_protocol",
20539
22112
  modelOrFixture: "smoke-protocol-v1"
20540
22113
  });
@@ -21274,7 +22847,7 @@ var init_scenario_fit = __esm({
21274
22847
  });
21275
22848
 
21276
22849
  // src/conversation/onboard-tiers.ts
21277
- import { existsSync as existsSync22, statSync as statSync4 } from "fs";
22850
+ import { existsSync as existsSync25, statSync as statSync4 } from "fs";
21278
22851
  function flagSet(tier) {
21279
22852
  return Boolean(getConfigValue(TIER_CONFIG_KEYS[tier]));
21280
22853
  }
@@ -23897,18 +25470,18 @@ var init_generator = __esm({
23897
25470
  });
23898
25471
 
23899
25472
  // src/demo/taxonomy-cache.ts
23900
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync15, existsSync as existsSync23, mkdirSync as mkdirSync13, unlinkSync as unlinkSync3 } from "fs";
25473
+ import { readFileSync as readFileSync22, writeFileSync as writeFileSync17, existsSync as existsSync26, mkdirSync as mkdirSync14, unlinkSync as unlinkSync4 } from "fs";
23901
25474
  import { homedir as homedir7 } from "os";
23902
- import { join as join24 } from "path";
25475
+ import { join as join27 } from "path";
23903
25476
  function ensureDir6() {
23904
- if (!existsSync23(NTRP_DIR4)) {
23905
- mkdirSync13(NTRP_DIR4, { recursive: true });
25477
+ if (!existsSync26(NTRP_DIR4)) {
25478
+ mkdirSync14(NTRP_DIR4, { recursive: true });
23906
25479
  }
23907
25480
  }
23908
25481
  function loadCachedTaxonomy(profile) {
23909
- if (!existsSync23(TAXONOMY_PATH)) return null;
25482
+ if (!existsSync26(TAXONOMY_PATH)) return null;
23910
25483
  try {
23911
- const parsed = JSON.parse(readFileSync19(TAXONOMY_PATH, "utf-8"));
25484
+ const parsed = JSON.parse(readFileSync22(TAXONOMY_PATH, "utf-8"));
23912
25485
  if (!parsed || typeof parsed !== "object") return null;
23913
25486
  if (parsed.profile_updated_at !== profile.updated_at) return null;
23914
25487
  return parsed;
@@ -23918,14 +25491,14 @@ function loadCachedTaxonomy(profile) {
23918
25491
  }
23919
25492
  function saveCachedTaxonomy(taxonomy) {
23920
25493
  ensureDir6();
23921
- writeFileSync15(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
25494
+ writeFileSync17(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
23922
25495
  }
23923
25496
  var NTRP_DIR4, TAXONOMY_PATH;
23924
25497
  var init_taxonomy_cache = __esm({
23925
25498
  "src/demo/taxonomy-cache.ts"() {
23926
25499
  "use strict";
23927
- NTRP_DIR4 = join24(homedir7(), ".ntrp");
23928
- TAXONOMY_PATH = join24(NTRP_DIR4, "demo-taxonomy.json");
25500
+ NTRP_DIR4 = join27(homedir7(), ".ntrp");
25501
+ TAXONOMY_PATH = join27(NTRP_DIR4, "demo-taxonomy.json");
23929
25502
  }
23930
25503
  });
23931
25504
 
@@ -24383,7 +25956,7 @@ __export(inbox_setup_exports, {
24383
25956
  shouldOfferInboxSkillSetup: () => shouldOfferInboxSkillSetup
24384
25957
  });
24385
25958
  import chalk27 from "chalk";
24386
- import { existsSync as existsSync24 } from "fs";
25959
+ import { existsSync as existsSync27 } from "fs";
24387
25960
  function markDemoOffered() {
24388
25961
  setConfigValue("ai-inbox-nudge-seen", "true");
24389
25962
  }
@@ -24415,7 +25988,7 @@ function printSkipHint(beat) {
24415
25988
  async function reuseInboxFolderIfPresent(session, beat, folderPath) {
24416
25989
  if (getAiInboxDir()) return false;
24417
25990
  const candidates = folderPath ? [folderPath] : [.../* @__PURE__ */ new Set([defaultAiInboxDir(), legacyAiInboxDir()])];
24418
- const existing = candidates.find((p) => existsSync24(p));
25991
+ const existing = candidates.find((p) => existsSync27(p));
24419
25992
  if (!existing) return false;
24420
25993
  console.log(" " + chalk27.dim("Pickup folder still on disk: ") + existing);
24421
25994
  const reuse = await session.confirm("Reuse this pickup folder?", true);
@@ -24521,7 +26094,7 @@ __export(ingest_exports, {
24521
26094
  handler: () => handler3
24522
26095
  });
24523
26096
  import chalk28 from "chalk";
24524
- import { readFileSync as readFileSync20, existsSync as existsSync25 } from "fs";
26097
+ import { readFileSync as readFileSync23, existsSync as existsSync28 } from "fs";
24525
26098
  import { basename as basename6 } from "path";
24526
26099
  async function handler3(args, ctx) {
24527
26100
  const { positional, flags } = parseArgs(args, [
@@ -24545,7 +26118,7 @@ async function handler3(args, ctx) {
24545
26118
  console.error(chalk28.dim(" /ingest --demo [--scenario <name>]"));
24546
26119
  process.exit(1);
24547
26120
  }
24548
- if (!existsSync25(file)) {
26121
+ if (!existsSync28(file)) {
24549
26122
  console.error(chalk28.red(` File not found: ${file}`));
24550
26123
  process.exit(1);
24551
26124
  }
@@ -24563,7 +26136,7 @@ async function handler3(args, ctx) {
24563
26136
  try {
24564
26137
  await initSchema();
24565
26138
  spinner.text = "Parsing CSV\u2026";
24566
- const content = readFileSync20(file, "utf-8");
26139
+ const content = readFileSync23(file, "utf-8");
24567
26140
  const { rows, headers } = parseCSV(content);
24568
26141
  if (rows.length === 0) {
24569
26142
  spinner.fail("CSV is empty");
@@ -24933,8 +26506,8 @@ __export(ingest_chat_exports, {
24933
26506
  loadDemoFromChat: () => loadDemoFromChat,
24934
26507
  looksLikeFilePath: () => looksLikeFilePath
24935
26508
  });
24936
- import { existsSync as existsSync26, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
24937
- import { basename as basename7, join as join25, resolve as resolve9 } from "path";
26509
+ import { existsSync as existsSync29, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
26510
+ import { basename as basename7, join as join28, resolve as resolve9 } from "path";
24938
26511
  import { homedir as homedir8 } from "os";
24939
26512
  import chalk30 from "chalk";
24940
26513
  function extractFilePath(input) {
@@ -24956,7 +26529,7 @@ function extractFilePath(input) {
24956
26529
  if (!candidate) continue;
24957
26530
  if (!looksLikePathToken(candidate)) continue;
24958
26531
  const p = expandPath(candidate);
24959
- if (existsSync26(p)) {
26532
+ if (existsSync29(p)) {
24960
26533
  try {
24961
26534
  const st = statSync5(p);
24962
26535
  if (st.isFile() || st.isDirectory()) return p;
@@ -24985,7 +26558,7 @@ function looksLikeFilePath(input) {
24985
26558
  function listCsvsInFolder(dir) {
24986
26559
  try {
24987
26560
  if (!statSync5(dir).isDirectory()) return [];
24988
- return readdirSync6(dir).filter((name) => name.toLowerCase().endsWith(".csv")).map((name) => join25(dir, name)).sort();
26561
+ return readdirSync6(dir).filter((name) => name.toLowerCase().endsWith(".csv")).map((name) => join28(dir, name)).sort();
24989
26562
  } catch {
24990
26563
  return [];
24991
26564
  }
@@ -25067,12 +26640,12 @@ async function ingestFromChat(ctx, filePath) {
25067
26640
  }
25068
26641
  const { handler: ingest } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
25069
26642
  const { detectEntityType: detectEntityType2 } = await Promise.resolve().then(() => (init_csv_detect(), csv_detect_exports));
25070
- const { readFileSync: readFileSync23 } = await import("fs");
26643
+ const { readFileSync: readFileSync25 } = await import("fs");
25071
26644
  const { parseCSV: parseCSV2 } = await Promise.resolve().then(() => (init_csv_parse(), csv_parse_exports));
25072
26645
  const { getStoredApiKey } = await Promise.resolve().then(() => (init_repl_api(), repl_api_exports));
25073
26646
  let headerCheckFailed = false;
25074
26647
  try {
25075
- const raw = readFileSync23(filePath, "utf-8");
26648
+ const raw = readFileSync25(filePath, "utf-8");
25076
26649
  const { headers } = parseCSV2(raw);
25077
26650
  const detected = detectEntityType2(headers, "unknown");
25078
26651
  if (!detected) headerCheckFailed = true;
@@ -25985,9 +27558,9 @@ async function handleGetSessionBrief(input) {
25985
27558
  if (!target) {
25986
27559
  return { error: `No session matching "${raw}".` };
25987
27560
  }
25988
- const { existsSync: existsSync29, readFileSync: readFileSync23 } = await import("fs");
27561
+ const { existsSync: existsSync31, readFileSync: readFileSync25 } = await import("fs");
25989
27562
  const briefPath = contextDocPathForSession2(target.id);
25990
- if (!existsSync29(briefPath)) {
27563
+ if (!existsSync31(briefPath)) {
25991
27564
  return {
25992
27565
  session_id: target.id,
25993
27566
  error: "No context brief on disk for this session (created before brief storage existed).",
@@ -25998,7 +27571,7 @@ async function handleGetSessionBrief(input) {
25998
27571
  return {
25999
27572
  session_id: target.id,
26000
27573
  security_notice: UNTRUSTED_CONTENT_NOTICE,
26001
- brief: wrapUntrustedContent(readFileSync23(briefPath, "utf-8"))
27574
+ brief: wrapUntrustedContent(readFileSync25(briefPath, "utf-8"))
26002
27575
  };
26003
27576
  }
26004
27577
  function auditDenied(name, input, resultJson, start) {
@@ -27002,15 +28575,15 @@ var init_ask = __esm({
27002
28575
  });
27003
28576
 
27004
28577
  // src/services/setup.ts
27005
- import { existsSync as existsSync27, mkdirSync as mkdirSync14, readFileSync as readFileSync21, writeFileSync as writeFileSync16 } from "fs";
27006
- import { join as join26 } from "path";
28578
+ import { existsSync as existsSync30, mkdirSync as mkdirSync15, readFileSync as readFileSync24, writeFileSync as writeFileSync18 } from "fs";
28579
+ import { join as join29 } from "path";
27007
28580
  function setupCheck() {
27008
28581
  const home = ntrpHome();
27009
28582
  let writable = false;
27010
28583
  try {
27011
- mkdirSync14(home, { recursive: true });
27012
- const probe = join26(home, ".write-check");
27013
- writeFileSync16(probe, "ok\n");
28584
+ mkdirSync15(home, { recursive: true });
28585
+ const probe = join29(home, ".write-check");
28586
+ writeFileSync18(probe, "ok\n");
27014
28587
  writable = true;
27015
28588
  } catch {
27016
28589
  writable = false;
@@ -27060,42 +28633,6 @@ var init_setup = __esm({
27060
28633
  }
27061
28634
  });
27062
28635
 
27063
- // src/version.ts
27064
- import { existsSync as existsSync28, readFileSync as readFileSync22 } from "fs";
27065
- import { dirname as dirname5, join as join27 } from "path";
27066
- import { fileURLToPath } from "url";
27067
- function readVersionFromPackageJson(packageJsonPath) {
27068
- if (!existsSync28(packageJsonPath)) return null;
27069
- try {
27070
- const pkg = JSON.parse(readFileSync22(packageJsonPath, "utf-8"));
27071
- if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
27072
- } catch {
27073
- }
27074
- return null;
27075
- }
27076
- function readVersionNearEntry(entryPath) {
27077
- const start = dirname5(entryPath);
27078
- for (const rel of [join27(start, "..", "package.json"), join27(start, "../..", "package.json")]) {
27079
- const version = readVersionFromPackageJson(rel);
27080
- if (version) return version;
27081
- }
27082
- return null;
27083
- }
27084
- function readInstalledVersionFromDisk() {
27085
- return readVersionNearEntry(fileURLToPath(import.meta.url));
27086
- }
27087
- function getInstalledVersion() {
27088
- if (cachedVersion) return cachedVersion;
27089
- cachedVersion = readInstalledVersionFromDisk() ?? "0.0.0";
27090
- return cachedVersion;
27091
- }
27092
- var cachedVersion;
27093
- var init_version = __esm({
27094
- "src/version.ts"() {
27095
- "use strict";
27096
- }
27097
- });
27098
-
27099
28636
  // src/mcp/server.ts
27100
28637
  init_context2();
27101
28638
  init_diagnosis();