@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
package/dist/index.js CHANGED
@@ -194,6 +194,17 @@ function formatCurrency(value) {
194
194
  if (value >= 1e3) return `$${(value / 1e3).toFixed(0)}K`;
195
195
  return `$${value.toFixed(0)}`;
196
196
  }
197
+ function formatConstraintLine(aggregate) {
198
+ if (!aggregate) return void 0;
199
+ const gating = aggregate.gating_vital_sign;
200
+ const label = VITAL_SIGN_LABELS[gating] ?? gating;
201
+ const vital = aggregate.vital_signs.find((v) => v.vital_sign === gating);
202
+ if (vital?.dollar_value != null && vital.dollar_value > 0) {
203
+ const dollars = `${formatCurrency(vital.dollar_value)} ${vital.dollar_label ?? ""}`.trim();
204
+ return `${label} (${dollars})`;
205
+ }
206
+ return label;
207
+ }
197
208
  function extractPipelineMetrics(vitals) {
198
209
  const flowRate = vitals.find((v) => v.vital_sign === "flow_rate");
199
210
  if (!flowRate) return null;
@@ -286,6 +297,7 @@ __export(store_exports, {
286
297
  getExportsDir: () => getExportsDir,
287
298
  getKnowledgeDir: () => getKnowledgeDir,
288
299
  getMemoryDir: () => getMemoryDir,
300
+ getRuminationsDir: () => getRuminationsDir,
289
301
  getStrategiesDir: () => getStrategiesDir,
290
302
  getWinsDir: () => getWinsDir,
291
303
  loadConfig: () => loadConfig,
@@ -429,6 +441,22 @@ from work done outside this platform.
429
441
  }
430
442
  return dir;
431
443
  }
444
+ function getRuminationsDir() {
445
+ const dir = join(NTRP_DIR, "ruminations");
446
+ if (!existsSync(dir)) {
447
+ mkdirSync(dir, { recursive: true });
448
+ writeFileSync(
449
+ join(dir, "README.md"),
450
+ `# Craft logs
451
+
452
+ This directory stores overnight strategy craft jobs. Each job has a machine file (\`<id>.json\`) and a short log (\`<id>.md\`).
453
+
454
+ Resume a job with \`ntrp strategy craft --resume <id>\`.
455
+ `
456
+ );
457
+ }
458
+ return dir;
459
+ }
432
460
  function getWinsDir() {
433
461
  const dir = join(NTRP_DIR, "wins");
434
462
  if (!existsSync(dir)) {
@@ -1720,6 +1748,8 @@ function buildSessionContextDoc(file, opts = {}) {
1720
1748
  lines.push(`- Constraints: ${file.strategist.constraintsNote}`);
1721
1749
  }
1722
1750
  if (file.strategist.origin) lines.push(`- Origin: ${file.strategist.origin}`);
1751
+ if (file.strategist.mode) lines.push(`- Mode: ${file.strategist.mode}`);
1752
+ if (file.strategist.ruminationId) lines.push(`- Craft job: ${file.strategist.ruminationId}`);
1723
1753
  lines.push("");
1724
1754
  }
1725
1755
  if (file.think) {
@@ -5622,6 +5652,7 @@ var init_surfaces = __esm({
5622
5652
  strategy: { defaultTier: "medium", allowUserTier: false },
5623
5653
  strategist: { defaultTier: "high", allowUserTier: true },
5624
5654
  strategist_stress: { defaultTier: "high", allowUserTier: false },
5655
+ strategist_critic: { defaultTier: "high", allowUserTier: false },
5625
5656
  option_eval: { defaultTier: "low", allowUserTier: false }
5626
5657
  };
5627
5658
  }
@@ -7934,12 +7965,51 @@ import { stringify as stringifyYaml } from "yaml";
7934
7965
  function strategyLibraryPath(slug) {
7935
7966
  return join16(getStrategiesDir(), `${slug}.md`);
7936
7967
  }
7937
- function writeStrategyMarkdown(strategy) {
7968
+ function writeStrategyMarkdown(strategy, extras) {
7938
7969
  const path = strategyLibraryPath(strategy.slug);
7939
- writeFileSync11(path, renderStrategyMarkdown(strategy), "utf-8");
7970
+ writeFileSync11(path, renderStrategyMarkdown(strategy, extras), "utf-8");
7940
7971
  return path;
7941
7972
  }
7942
- function renderStrategyMarkdown(strategy) {
7973
+ function formatEffortSum(workstreams) {
7974
+ const sum = workstreams.reduce(
7975
+ (acc, ws) => acc + (Number.isFinite(ws.effort_hours) ? ws.effort_hours : 0),
7976
+ 0
7977
+ );
7978
+ const count = workstreams.length;
7979
+ return `~${Math.round(sum)} team-hours across ${count} workstream${count === 1 ? "" : "s"}`;
7980
+ }
7981
+ function renderConstraintHeading(constraintLine) {
7982
+ return `## Constraint
7983
+ ${constraintLine?.trim() || "none stated"}
7984
+ `;
7985
+ }
7986
+ function renderScopeHeading(constraints, outOfScope) {
7987
+ const inLines = constraints.length > 0 ? formatList(constraints) : "- none stated";
7988
+ const outItems = (outOfScope ?? []).map((s) => s.trim()).filter(Boolean);
7989
+ const outLines = outItems.length > 0 ? formatList(outItems) : "- none stated";
7990
+ return `## In scope / out of scope
7991
+ In scope:
7992
+ ${inLines}
7993
+
7994
+ Out of scope:
7995
+ ${outLines}
7996
+ `;
7997
+ }
7998
+ function renderKilledAlternativeLine(killedAlternative) {
7999
+ return `Killed alternative: ${killedAlternative?.trim() || "none stated"}`;
8000
+ }
8001
+ function renderEffortHeading(workstreams) {
8002
+ return `## Effort
8003
+ ${formatEffortSum(workstreams)}
8004
+ `;
8005
+ }
8006
+ function renderReviewHeading(opts) {
8007
+ const cmd = opts.slug?.trim() ? `/strategy review ${opts.slug.trim()}` : "/strategy review";
8008
+ return `## Review
8009
+ Cadence: ${opts.cadence}. Check progress with ${cmd}.
8010
+ `;
8011
+ }
8012
+ function renderStrategyMarkdown(strategy, extras) {
7943
8013
  const frontmatter = stringifyYaml({
7944
8014
  id: strategy.id,
7945
8015
  slug: strategy.slug,
@@ -7954,6 +8024,9 @@ function renderStrategyMarkdown(strategy) {
7954
8024
  ...strategy.baseline_batch_id ? { baseline_batch_id: strategy.baseline_batch_id } : {},
7955
8025
  updated_at: strategy.updated_at
7956
8026
  }).trim();
8027
+ if (strategy.origin === "strategist") {
8028
+ return renderStrategistLibraryMarkdown(strategy, extras, frontmatter);
8029
+ }
7957
8030
  const objectiveSection = strategy.objective ? `
7958
8031
  ## Objective
7959
8032
  ${strategy.objective}
@@ -8000,6 +8073,67 @@ ${formatList(strategy.risks)}
8000
8073
  ## Experiment Design
8001
8074
  ${strategy.experiment_design}
8002
8075
 
8076
+ ## Source Excerpt
8077
+ ${strategy.raw_excerpt || "_No excerpt captured._"}
8078
+ `;
8079
+ }
8080
+ function renderStrategistLibraryMarkdown(strategy, extras, frontmatter) {
8081
+ const callSection = strategy.raw_excerpt ? `
8082
+ ## The Call
8083
+ ${strategy.raw_excerpt}
8084
+ ` : "";
8085
+ const objectiveSection = strategy.objective ? `
8086
+ ## Locked objective
8087
+ ${strategy.objective}
8088
+ ` : "";
8089
+ const workstreamSection = strategy.workstreams.length > 0 ? `
8090
+ ## Workstreams
8091
+ ${strategy.workstreams.map(formatWorkstream).join("\n")}
8092
+ ` : "";
8093
+ const assumptionsSection = strategy.assumptions.length > 0 ? `
8094
+ ## Assumptions (unverified)
8095
+ ${formatList(strategy.assumptions)}
8096
+ ` : "";
8097
+ const craftLogSection = extras?.craftLogPath ? `
8098
+ ## Craft log
8099
+ See \`${extras.craftLogPath}\`.
8100
+ ` : "";
8101
+ return `---
8102
+ ${frontmatter}
8103
+ ---
8104
+
8105
+ # ${strategy.title}
8106
+ ${callSection}${objectiveSection}
8107
+ ${renderConstraintHeading(extras?.constraintLine)}
8108
+ ## Goal
8109
+ ${strategy.goal}
8110
+
8111
+ ${renderScopeHeading(strategy.constraints, extras?.outOfScope)}
8112
+ ## Hypothesis
8113
+ ${strategy.hypothesis}
8114
+
8115
+ ${renderKilledAlternativeLine(extras?.killedAlternative)}
8116
+
8117
+ ## Target Segment
8118
+ ${strategy.target_segment}
8119
+
8120
+ ${renderEffortHeading(strategy.workstreams)}${workstreamSection}
8121
+ ## Success Metrics
8122
+ ${formatMetrics(strategy.success_metrics)}
8123
+
8124
+ ## Leading Indicators
8125
+ ${formatMetrics(strategy.leading_indicators)}
8126
+
8127
+ ## Recommended Actions
8128
+ ${formatList(strategy.recommended_actions)}
8129
+ ${assumptionsSection}
8130
+ ## Risks
8131
+ ${formatList(strategy.risks)}
8132
+
8133
+ ## Experiment Design
8134
+ ${strategy.experiment_design}
8135
+
8136
+ ${renderReviewHeading({ cadence: strategy.review_cadence, slug: strategy.slug })}${craftLogSection}
8003
8137
  ## Source Excerpt
8004
8138
  ${strategy.raw_excerpt || "_No excerpt captured._"}
8005
8139
  `;
@@ -8870,6 +9004,7 @@ function buildSessionFileSnapshot(ctx) {
8870
9004
  if (ctx.strategistState) file.strategist = ctx.strategistState;
8871
9005
  if (ctx.thinkState) file.think = ctx.thinkState;
8872
9006
  if (ctx.pendingAsk) file.pending_ask = ctx.pendingAsk;
9007
+ if (ctx.lastCraftJobId) file.last_craft_job_id = ctx.lastCraftJobId;
8873
9008
  return file;
8874
9009
  }
8875
9010
  function defaultSessionAnalysis(primary = "gtm_health") {
@@ -9224,6 +9359,9 @@ async function finalizeSession(ctx, stage) {
9224
9359
  if (ctx.pendingAsk) {
9225
9360
  file.pending_ask = ctx.pendingAsk;
9226
9361
  }
9362
+ if (ctx.lastCraftJobId) {
9363
+ file.last_craft_job_id = ctx.lastCraftJobId;
9364
+ }
9227
9365
  try {
9228
9366
  writeFileSync13(ctx.sessionFile, JSON.stringify(file, null, 2) + "\n");
9229
9367
  } catch {
@@ -9295,6 +9433,7 @@ function resetContextForSwitch(ctx, opts) {
9295
9433
  ctx.strategistState = opts.strategistState;
9296
9434
  ctx.thinkState = opts.thinkState;
9297
9435
  ctx.pendingAsk = opts.pendingAsk;
9436
+ ctx.lastCraftJobId = opts.lastCraftJobId;
9298
9437
  ctx.gapAudit = void 0;
9299
9438
  ctx.deliverIntent = false;
9300
9439
  ctx.computeInProgress = false;
@@ -12340,7 +12479,9 @@ var init_guide_slides = __esm({
12340
12479
  playbook: "loop",
12341
12480
  remember: "loop",
12342
12481
  sessions: "loop",
12343
- progress: "loop"
12482
+ progress: "loop",
12483
+ "keep-going": "loop",
12484
+ keepgoing: "loop"
12344
12485
  };
12345
12486
  TALK = {
12346
12487
  id: "talk",
@@ -12391,7 +12532,7 @@ var init_guide_slides = __esm({
12391
12532
  "",
12392
12533
  '"what is our ARR?" or "why is this red?" need a loaded dataset. Narrative findings and /ask need a stored key \u2014 /connect pastes any provider key and we detect it.',
12393
12534
  "",
12394
- "Replay this tour with /deepdive, jump to one slide with /deepdive freshness (or nrr, arr, \u2026), or /deepdive guide for this how-to section. When the prompt shows a dim \u23CE hint (yes, use demo data, go ahead, /connect), bare Enter submits it \u2014 and b / back steps back one confirm gate (or the previous tour slide while you're in /deepdive)."
12535
+ "Replay this tour with /deepdive, jump to one slide with /deepdive freshness (or nrr, arr, \u2026), or /deepdive guide for this how-to section. When the prompt shows a dim \u23CE hint (yes, use demo data, go ahead, /connect, keep going), bare Enter submits it \u2014 and b / back steps back one confirm gate (or the previous tour slide while you're in /deepdive)."
12395
12536
  ],
12396
12537
  deepdive: [
12397
12538
  "/connect ollama for a keyless local model. /connect --base-url <url> --id <name> for any OpenAI-compatible endpoint.",
@@ -12437,13 +12578,15 @@ var init_guide_slides = __esm({
12437
12578
  caption: "Stethoscope, not hospital \u2014 we observe and recommend; you decide the surgery.",
12438
12579
  layers: [
12439
12580
  { label: "Listen (/diagnose, /metrics)" },
12440
- { label: 'Plan ("how should we fix this?" / /strategy)', highlight: true },
12581
+ { label: 'Plan ("how should we fix this?" \xB7 keep going)', highlight: true },
12441
12582
  { label: "Review (/strategy review, /playbook)" },
12442
12583
  { label: "Remember (/remember, /rate, ANALYST.md)" }
12443
12584
  ]
12444
12585
  },
12445
12586
  lines: [
12446
- 'The learning loop is how we stay useful after the first diagnose. "how should we fix this?" or /strategy builds a measurable plan with milestones and dollar-anchored ranges; /strategy review checks those against later vital signs.',
12587
+ `After a diagnose we don't stop at the score. "how should we fix this?" or /strategy opens a strategy session: we work back from an objective into sequenced workstreams with dated milestones and dollar-anchored ranges.`,
12588
+ "",
12589
+ "At the confirm card, \u23CE yes builds that plan once. Type keep going to keep working until the plan is ready \u2014 the same armed-Enter pattern as use demo data and go ahead. If you see Best so far, the prompt returns to \u203A and \u23CE keep going continues. Plan ready? Type /strategy review later to check milestones against new vitals.",
12447
12590
  "",
12448
12591
  "When a vital is red, /playbook names a matching play \u2014 and outcomes from reviews annotate the catalog with what actually hit here. /remember stores a durable fact; /rate bad <reason> writes a calibration. Optional ~/.ntrp/ANALYST.md is standing voice and priorities (it never overrides safety rules).",
12449
12592
  "",
@@ -12451,6 +12594,7 @@ var init_guide_slides = __esm({
12451
12594
  ],
12452
12595
  deepdive: [
12453
12596
  '"build me a game plan" before analysis queues the strategist and resumes after compute.',
12597
+ "An unfinished plan arms \u23CE keep going on \u203A. No plan in progress? Type how should we fix this? to start one.",
12454
12598
  "Interactive sessions distill 5 or fewer durable facts on close when an LLM key is stored. One-shot commands do not bank hours or distill.",
12455
12599
  "/scratch factory-resets data and config. progress.json and install.json survive unless you pass --include-progress.",
12456
12600
  "Credits in /progress accrue in interactive ntrp only."
@@ -12880,7 +13024,7 @@ var init_metric_tour = __esm({
12880
13024
  "/metrics \u2014 SaaS scorecard (ARR, NRR, coverage\u2026)",
12881
13025
  "/deepdive \u2014 replay this tour \xB7 /deepdive guide how-to only",
12882
13026
  "/handoff \u2014 ship a file; teach the inbox once (/inbox skill)",
12883
- "/strategy \u2014 measurable plan \xB7 /help shortcuts",
13027
+ "/strategy \u2014 measurable plan \xB7 keep going until it's ready \xB7 /help shortcuts",
12884
13028
  "",
12885
13029
  "Fourteen more SaaS metrics sit behind /deepdive list \u2014 including unit economics that unlock when spend data lands.",
12886
13030
  "",
@@ -15499,14 +15643,324 @@ var init_explore_mode = __esm({
15499
15643
  }
15500
15644
  });
15501
15645
 
15646
+ // src/config/update-check.ts
15647
+ import { existsSync as existsSync21, mkdirSync as mkdirSync12, readFileSync as readFileSync17, unlinkSync as unlinkSync4, writeFileSync as writeFileSync14 } from "fs";
15648
+ import { join as join21 } from "path";
15649
+ function cachePath2() {
15650
+ return join21(ntrpHome(), "update-check.json");
15651
+ }
15652
+ function ensureDir6() {
15653
+ const dir = ntrpHome();
15654
+ if (!existsSync21(dir)) {
15655
+ mkdirSync12(dir, { recursive: true });
15656
+ }
15657
+ }
15658
+ function loadUpdateCheckCache() {
15659
+ const path = cachePath2();
15660
+ if (!existsSync21(path)) return null;
15661
+ try {
15662
+ const parsed = JSON.parse(readFileSync17(path, "utf-8"));
15663
+ if (!parsed || typeof parsed !== "object" || typeof parsed.lastCheck !== "number" || typeof parsed.latestVersion !== "string") {
15664
+ return null;
15665
+ }
15666
+ return parsed;
15667
+ } catch {
15668
+ return null;
15669
+ }
15670
+ }
15671
+ function saveUpdateCheckCache(cache2) {
15672
+ ensureDir6();
15673
+ writeFileSync14(cachePath2(), JSON.stringify(cache2, null, 2) + "\n");
15674
+ }
15675
+ function isCacheFresh(cache2, ttlMs = CACHE_TTL_MS2) {
15676
+ if (!cache2) return false;
15677
+ return Date.now() - cache2.lastCheck < ttlMs;
15678
+ }
15679
+ function invalidateUpdateCheckCache() {
15680
+ const path = cachePath2();
15681
+ if (existsSync21(path)) {
15682
+ unlinkSync4(path);
15683
+ }
15684
+ }
15685
+ var CACHE_TTL_MS2;
15686
+ var init_update_check = __esm({
15687
+ "src/config/update-check.ts"() {
15688
+ "use strict";
15689
+ init_store();
15690
+ CACHE_TTL_MS2 = 864e5;
15691
+ }
15692
+ });
15693
+
15694
+ // src/version.ts
15695
+ import { existsSync as existsSync22, readFileSync as readFileSync18 } from "fs";
15696
+ import { dirname as dirname4, join as join22 } from "path";
15697
+ import { fileURLToPath } from "url";
15698
+ function readVersionFromPackageJson(packageJsonPath) {
15699
+ if (!existsSync22(packageJsonPath)) return null;
15700
+ try {
15701
+ const pkg = JSON.parse(readFileSync18(packageJsonPath, "utf-8"));
15702
+ if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
15703
+ } catch {
15704
+ }
15705
+ return null;
15706
+ }
15707
+ function readVersionNearEntry(entryPath) {
15708
+ const start = dirname4(entryPath);
15709
+ for (const rel of [join22(start, "..", "package.json"), join22(start, "../..", "package.json")]) {
15710
+ const version = readVersionFromPackageJson(rel);
15711
+ if (version) return version;
15712
+ }
15713
+ return null;
15714
+ }
15715
+ function readInstalledVersionFromDisk() {
15716
+ return readVersionNearEntry(fileURLToPath(import.meta.url));
15717
+ }
15718
+ function getInstalledVersion() {
15719
+ if (cachedVersion) return cachedVersion;
15720
+ cachedVersion = readInstalledVersionFromDisk() ?? "0.0.0";
15721
+ return cachedVersion;
15722
+ }
15723
+ var cachedVersion;
15724
+ var init_version = __esm({
15725
+ "src/version.ts"() {
15726
+ "use strict";
15727
+ }
15728
+ });
15729
+
15730
+ // src/update/registry.ts
15731
+ var registry_exports = {};
15732
+ __export(registry_exports, {
15733
+ NPM_PACKAGE: () => NPM_PACKAGE,
15734
+ applyUpdateCheckResult: () => applyUpdateCheckResult,
15735
+ checkForUpdate: () => checkForUpdate,
15736
+ fetchLatestVersion: () => fetchLatestVersion,
15737
+ formatUpdateNudge: () => formatUpdateNudge,
15738
+ hasAvailableUpdate: () => hasAvailableUpdate,
15739
+ hydrateUpdateAvailableFromCache: () => hydrateUpdateAvailableFromCache,
15740
+ isNewerVersion: () => isNewerVersion,
15741
+ startBackgroundUpdateCheck: () => startBackgroundUpdateCheck
15742
+ });
15743
+ function registryUrl() {
15744
+ return process.env.NTRP_REGISTRY_URL ?? "https://registry.npmjs.org/@sonnechasser/ntrp/latest";
15745
+ }
15746
+ function parseVersionParts(version) {
15747
+ const cleaned = version.trim().replace(/^v/i, "");
15748
+ const core = cleaned.split("-")[0] ?? cleaned;
15749
+ const parts = core.split(".").map((p) => parseInt(p, 10));
15750
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
15751
+ }
15752
+ function isNewerVersion(latest, current) {
15753
+ const [lMaj, lMin, lPatch] = parseVersionParts(latest);
15754
+ const [cMaj, cMin, cPatch] = parseVersionParts(current);
15755
+ if (lMaj !== cMaj) return lMaj > cMaj;
15756
+ if (lMin !== cMin) return lMin > cMin;
15757
+ return lPatch > cPatch;
15758
+ }
15759
+ function formatUpdateNudge(current, latest) {
15760
+ return `\u26A1 NTRP v${latest} available (running v${current}) \u2014 \u23CE /update`;
15761
+ }
15762
+ function hasAvailableUpdate(update) {
15763
+ return Boolean(update && isNewerVersion(update.latest, update.current));
15764
+ }
15765
+ function hydrateUpdateAvailableFromCache(current) {
15766
+ const cached2 = loadUpdateCheckCache();
15767
+ if (!cached2?.latestVersion) return void 0;
15768
+ if (!isNewerVersion(cached2.latestVersion, current)) return void 0;
15769
+ return { current, latest: cached2.latestVersion };
15770
+ }
15771
+ function applyUpdateCheckResult(ctx, result) {
15772
+ if (result?.updateAvailable) {
15773
+ ctx.updateAvailable = { current: result.current, latest: result.latest };
15774
+ return;
15775
+ }
15776
+ if (result && !result.updateAvailable) {
15777
+ ctx.updateAvailable = void 0;
15778
+ }
15779
+ }
15780
+ function startBackgroundUpdateCheck(ctx) {
15781
+ const pending = checkForUpdate({ force: true, timeoutMs: 5e3 });
15782
+ ctx.pendingUpdateCheck = pending;
15783
+ void pending.then((result) => applyUpdateCheckResult(ctx, result)).catch(() => void 0);
15784
+ }
15785
+ async function fetchLatestVersion(timeoutMs = 5e3) {
15786
+ try {
15787
+ const res = await fetch(registryUrl(), { signal: AbortSignal.timeout(timeoutMs) });
15788
+ if (!res.ok) return null;
15789
+ const data = await res.json();
15790
+ return typeof data.version === "string" && data.version.length > 0 ? data.version : null;
15791
+ } catch {
15792
+ return null;
15793
+ }
15794
+ }
15795
+ function buildResult(current, latest) {
15796
+ return {
15797
+ current,
15798
+ latest,
15799
+ updateAvailable: isNewerVersion(latest, current)
15800
+ };
15801
+ }
15802
+ async function checkForUpdate(options) {
15803
+ const current = getInstalledVersion();
15804
+ const timeoutMs = options?.timeoutMs ?? 5e3;
15805
+ const cached2 = loadUpdateCheckCache();
15806
+ if (!options?.force && isCacheFresh(cached2)) {
15807
+ return buildResult(current, cached2.latestVersion);
15808
+ }
15809
+ const latest = await fetchLatestVersion(timeoutMs);
15810
+ if (!latest) {
15811
+ if (cached2?.latestVersion) {
15812
+ return buildResult(current, cached2.latestVersion);
15813
+ }
15814
+ return null;
15815
+ }
15816
+ const nextCache = { lastCheck: Date.now(), latestVersion: latest };
15817
+ saveUpdateCheckCache(nextCache);
15818
+ return buildResult(current, latest);
15819
+ }
15820
+ var NPM_PACKAGE;
15821
+ var init_registry = __esm({
15822
+ "src/update/registry.ts"() {
15823
+ "use strict";
15824
+ init_update_check();
15825
+ init_version();
15826
+ NPM_PACKAGE = "@sonnechasser/ntrp";
15827
+ }
15828
+ });
15829
+
15830
+ // src/ruminations/store.ts
15831
+ import { existsSync as existsSync23, readFileSync as readFileSync19, writeFileSync as writeFileSync15 } from "fs";
15832
+ import { join as join23 } from "path";
15833
+ import { randomUUID as randomUUID7 } from "crypto";
15834
+ function makeRuminationId(now2 = /* @__PURE__ */ new Date()) {
15835
+ const day = now2.toISOString().slice(0, 10).replace(/-/g, "");
15836
+ return `${day}-${randomUUID7().slice(0, 4)}`;
15837
+ }
15838
+ function ruminationJsonPath(id) {
15839
+ return join23(getRuminationsDir(), `${id}.json`);
15840
+ }
15841
+ function ruminationLogPath(id) {
15842
+ return join23(getRuminationsDir(), `${id}.md`);
15843
+ }
15844
+ function unfinishedCraftJob(id) {
15845
+ if (!id?.trim()) return null;
15846
+ const job = loadRuminationJob(id);
15847
+ if (!job || job.status === "ready") return null;
15848
+ return job;
15849
+ }
15850
+ function loadRuminationJob(id) {
15851
+ const path = ruminationJsonPath(id);
15852
+ if (!existsSync23(path)) return null;
15853
+ try {
15854
+ const parsed = JSON.parse(readFileSync19(path, "utf-8"));
15855
+ if (!parsed || typeof parsed !== "object" || parsed.id !== id) return null;
15856
+ if (parsed.best_plan === void 0) parsed.best_plan = parsed.plan ?? null;
15857
+ if (parsed.best_score === void 0) parsed.best_score = parsed.last_critic?.score ?? null;
15858
+ return parsed;
15859
+ } catch {
15860
+ return null;
15861
+ }
15862
+ }
15863
+ function createRuminationJob(opts) {
15864
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
15865
+ return {
15866
+ id: opts.id ?? makeRuminationId(),
15867
+ objective: opts.objective,
15868
+ constraints_note: opts.constraintsNote,
15869
+ baseline_batch_id: opts.baselineBatchId ?? null,
15870
+ snapshot_hash: opts.snapshotHash,
15871
+ plan: null,
15872
+ best_plan: null,
15873
+ best_score: null,
15874
+ last_critic: null,
15875
+ rubric_gaps: [],
15876
+ iterations: [],
15877
+ cumulative_input_tokens: 0,
15878
+ cumulative_output_tokens: 0,
15879
+ status: "running",
15880
+ from_fallback: false,
15881
+ revised_once: false,
15882
+ extra_ground_used: false,
15883
+ min_score: opts.minScore,
15884
+ max_rounds: opts.maxRounds,
15885
+ max_tokens: opts.maxTokens,
15886
+ created_at: now2,
15887
+ updated_at: now2,
15888
+ no_improve_streak: 0,
15889
+ last_critic_score: null
15890
+ };
15891
+ }
15892
+ function renderRuminationLog(job) {
15893
+ const lines = [];
15894
+ lines.push(`# Craft log \u2014 ${job.id}`);
15895
+ lines.push("");
15896
+ lines.push(`Objective: ${job.objective}`);
15897
+ lines.push(`Status: ${job.status}${job.stop_reason ? ` (${job.stop_reason})` : ""}`);
15898
+ lines.push(
15899
+ `Tokens: ${job.cumulative_input_tokens + job.cumulative_output_tokens} total (${job.cumulative_input_tokens} in / ${job.cumulative_output_tokens} out)`
15900
+ );
15901
+ if (job.library_path) lines.push(`Strategy: ${job.library_path}`);
15902
+ if (job.handoff_path) lines.push(`Handoff: ${job.handoff_path}`);
15903
+ lines.push("");
15904
+ lines.push("## Rounds");
15905
+ lines.push("");
15906
+ if (job.iterations.length === 0) {
15907
+ lines.push("_No rounds yet._");
15908
+ } else {
15909
+ for (const it of job.iterations) {
15910
+ const codes = it.blocking_codes.length > 0 ? it.blocking_codes.join(", ") : "none";
15911
+ lines.push(
15912
+ `- Round ${it.round}: score ${it.critic_score ?? "\u2014"} \xB7 ${it.decision} \xB7 blocking ${codes} \xB7 ${it.tokens_in + it.tokens_out} tok`
15913
+ );
15914
+ }
15915
+ }
15916
+ if (job.last_critic?.partner_pushback) {
15917
+ lines.push("");
15918
+ lines.push("## Partner pushback");
15919
+ lines.push("");
15920
+ lines.push(job.last_critic.partner_pushback);
15921
+ }
15922
+ if (job.last_critic?.killed_alternative) {
15923
+ lines.push("");
15924
+ lines.push("## Killed alternative");
15925
+ lines.push("");
15926
+ lines.push(job.last_critic.killed_alternative);
15927
+ }
15928
+ lines.push("");
15929
+ return lines.join("\n");
15930
+ }
15931
+ function saveRuminationJob(job) {
15932
+ job.updated_at = (/* @__PURE__ */ new Date()).toISOString();
15933
+ getRuminationsDir();
15934
+ writeFileSync15(ruminationJsonPath(job.id), JSON.stringify(job, null, 2) + "\n", "utf-8");
15935
+ writeFileSync15(ruminationLogPath(job.id), renderRuminationLog(job), "utf-8");
15936
+ }
15937
+ function addRuminationIteration(job, iteration) {
15938
+ job.iterations.push({ ...iteration, at: (/* @__PURE__ */ new Date()).toISOString() });
15939
+ job.cumulative_input_tokens += iteration.tokens_in;
15940
+ job.cumulative_output_tokens += iteration.tokens_out;
15941
+ }
15942
+ var init_store3 = __esm({
15943
+ "src/ruminations/store.ts"() {
15944
+ "use strict";
15945
+ init_store();
15946
+ }
15947
+ });
15948
+
15502
15949
  // src/conversation/recommended-action.ts
15503
15950
  function resolveRecommendedAction(ctx) {
15504
15951
  if (!hasValidLicense()) return { submit: "/activate", hint: "/activate" };
15505
15952
  const phase = resolveConversationPhase(ctx);
15506
15953
  switch (phase) {
15507
15954
  case "explore":
15955
+ if (!canUseReplAi(ctx)) {
15956
+ if (ctx.stage === "delivered") return { submit: "/end", hint: "home" };
15957
+ return { submit: "/connect", hint: "/connect" };
15958
+ }
15959
+ if (unfinishedCraftJob(ctx.lastCraftJobId)) {
15960
+ return { submit: "keep going", hint: "keep going" };
15961
+ }
15508
15962
  if (ctx.stage === "delivered") return { submit: "/end", hint: "home" };
15509
- return canUseReplAi(ctx) ? null : { submit: "/connect", hint: "/connect" };
15963
+ return null;
15510
15964
  case "awaiting_data":
15511
15965
  if (ctx.gapAudit?.can_compute) return { submit: "go ahead", hint: "go ahead" };
15512
15966
  if (!sessionHasData(ctx)) return { submit: "use demo data", hint: "use demo data" };
@@ -15517,6 +15971,8 @@ function resolveRecommendedAction(ctx) {
15517
15971
  return ctx.strategistState?.step === "objective_confirm" ? { submit: "yes", hint: "yes" } : null;
15518
15972
  case "think":
15519
15973
  return null;
15974
+ case "orient":
15975
+ return hasAvailableUpdate(ctx.updateAvailable) ? { submit: "/update", hint: "/update" } : null;
15520
15976
  default:
15521
15977
  return null;
15522
15978
  }
@@ -15526,7 +15982,9 @@ var init_recommended_action = __esm({
15526
15982
  "use strict";
15527
15983
  init_repl_api();
15528
15984
  init_activation();
15985
+ init_registry();
15529
15986
  init_phase();
15987
+ init_store3();
15530
15988
  }
15531
15989
  });
15532
15990
 
@@ -16324,17 +16782,17 @@ __export(play_outcomes_exports, {
16324
16782
  listPlayOutcomes: () => listPlayOutcomes,
16325
16783
  recordPlayOutcomes: () => recordPlayOutcomes
16326
16784
  });
16327
- import { existsSync as existsSync21, readFileSync as readFileSync17, appendFileSync as appendFileSync6 } from "fs";
16328
- import { join as join21 } from "path";
16329
- import { randomUUID as randomUUID7 } from "crypto";
16785
+ import { existsSync as existsSync24, readFileSync as readFileSync20, appendFileSync as appendFileSync6 } from "fs";
16786
+ import { join as join24 } from "path";
16787
+ import { randomUUID as randomUUID8 } from "crypto";
16330
16788
  function outcomesPath() {
16331
- return join21(getMemoryDir(), OUTCOMES_FILE);
16789
+ return join24(getMemoryDir(), OUTCOMES_FILE);
16332
16790
  }
16333
16791
  function listPlayOutcomes() {
16334
16792
  const path = outcomesPath();
16335
- if (!existsSync21(path)) return [];
16793
+ if (!existsSync24(path)) return [];
16336
16794
  const out = [];
16337
- for (const line of readFileSync17(path, "utf-8").split("\n")) {
16795
+ for (const line of readFileSync20(path, "utf-8").split("\n")) {
16338
16796
  const trimmed = line.trim();
16339
16797
  if (!trimmed) continue;
16340
16798
  try {
@@ -16364,7 +16822,7 @@ function recordPlayOutcomes(strategy, outcomes, batchId) {
16364
16822
  if (seen.has(key)) continue;
16365
16823
  seen.add(key);
16366
16824
  const record = {
16367
- id: randomUUID7(),
16825
+ id: randomUUID8(),
16368
16826
  play_id: playId,
16369
16827
  strategy_slug: strategy.slug,
16370
16828
  workstream_order: outcome.workstream_order,
@@ -16427,11 +16885,11 @@ function jsonSafe(value) {
16427
16885
  }
16428
16886
  return value;
16429
16887
  }
16430
- function envelope(command, data, warnings) {
16888
+ function envelope(command, data, warnings, status = "ok") {
16431
16889
  return {
16432
16890
  schema_version: HEADLESS_SCHEMA_VERSION,
16433
16891
  command,
16434
- status: "ok",
16892
+ status,
16435
16893
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
16436
16894
  data,
16437
16895
  ...warnings && warnings.length > 0 ? { warnings } : {}
@@ -16447,8 +16905,8 @@ function errorEnvelope(command, err) {
16447
16905
  error: ntrpError.toHeadlessError()
16448
16906
  };
16449
16907
  }
16450
- function emitResult(command, data, warnings) {
16451
- console.log(JSON.stringify(jsonSafe(envelope(command, data, warnings)), null, 2));
16908
+ function emitResult(command, data, warnings, status = "ok") {
16909
+ console.log(JSON.stringify(jsonSafe(envelope(command, data, warnings, status)), null, 2));
16452
16910
  }
16453
16911
  function emitError(command, err) {
16454
16912
  const ntrpError = err instanceof NtrpError ? err : toNtrpError(err);
@@ -19801,18 +20259,18 @@ var init_terminal = __esm({
19801
20259
  });
19802
20260
 
19803
20261
  // src/demo/taxonomy-cache.ts
19804
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync14, existsSync as existsSync22, mkdirSync as mkdirSync12, unlinkSync as unlinkSync4 } from "fs";
20262
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync16, existsSync as existsSync25, mkdirSync as mkdirSync13, unlinkSync as unlinkSync5 } from "fs";
19805
20263
  import { homedir as homedir7 } from "os";
19806
- import { join as join22 } from "path";
19807
- function ensureDir6() {
19808
- if (!existsSync22(NTRP_DIR4)) {
19809
- mkdirSync12(NTRP_DIR4, { recursive: true });
20264
+ import { join as join25 } from "path";
20265
+ function ensureDir7() {
20266
+ if (!existsSync25(NTRP_DIR4)) {
20267
+ mkdirSync13(NTRP_DIR4, { recursive: true });
19810
20268
  }
19811
20269
  }
19812
20270
  function loadCachedTaxonomy(profile) {
19813
- if (!existsSync22(TAXONOMY_PATH)) return null;
20271
+ if (!existsSync25(TAXONOMY_PATH)) return null;
19814
20272
  try {
19815
- const parsed = JSON.parse(readFileSync18(TAXONOMY_PATH, "utf-8"));
20273
+ const parsed = JSON.parse(readFileSync21(TAXONOMY_PATH, "utf-8"));
19816
20274
  if (!parsed || typeof parsed !== "object") return null;
19817
20275
  if (parsed.profile_updated_at !== profile.updated_at) return null;
19818
20276
  return parsed;
@@ -19821,13 +20279,13 @@ function loadCachedTaxonomy(profile) {
19821
20279
  }
19822
20280
  }
19823
20281
  function saveCachedTaxonomy(taxonomy) {
19824
- ensureDir6();
19825
- writeFileSync14(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
20282
+ ensureDir7();
20283
+ writeFileSync16(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
19826
20284
  }
19827
20285
  function invalidateTaxonomy() {
19828
- if (existsSync22(TAXONOMY_PATH)) {
20286
+ if (existsSync25(TAXONOMY_PATH)) {
19829
20287
  try {
19830
- unlinkSync4(TAXONOMY_PATH);
20288
+ unlinkSync5(TAXONOMY_PATH);
19831
20289
  } catch {
19832
20290
  }
19833
20291
  }
@@ -19836,8 +20294,8 @@ var NTRP_DIR4, TAXONOMY_PATH;
19836
20294
  var init_taxonomy_cache = __esm({
19837
20295
  "src/demo/taxonomy-cache.ts"() {
19838
20296
  "use strict";
19839
- NTRP_DIR4 = join22(homedir7(), ".ntrp");
19840
- TAXONOMY_PATH = join22(NTRP_DIR4, "demo-taxonomy.json");
20297
+ NTRP_DIR4 = join25(homedir7(), ".ntrp");
20298
+ TAXONOMY_PATH = join25(NTRP_DIR4, "demo-taxonomy.json");
19841
20299
  }
19842
20300
  });
19843
20301
 
@@ -20295,7 +20753,7 @@ __export(inbox_setup_exports, {
20295
20753
  shouldOfferInboxSkillSetup: () => shouldOfferInboxSkillSetup
20296
20754
  });
20297
20755
  import chalk19 from "chalk";
20298
- import { existsSync as existsSync23 } from "fs";
20756
+ import { existsSync as existsSync26 } from "fs";
20299
20757
  function markDemoOffered() {
20300
20758
  setConfigValue("ai-inbox-nudge-seen", "true");
20301
20759
  }
@@ -20327,7 +20785,7 @@ function printSkipHint(beat) {
20327
20785
  async function reuseInboxFolderIfPresent(session, beat, folderPath) {
20328
20786
  if (getAiInboxDir()) return false;
20329
20787
  const candidates = folderPath ? [folderPath] : [.../* @__PURE__ */ new Set([defaultAiInboxDir(), legacyAiInboxDir()])];
20330
- const existing = candidates.find((p) => existsSync23(p));
20788
+ const existing = candidates.find((p) => existsSync26(p));
20331
20789
  if (!existing) return false;
20332
20790
  console.log(" " + chalk19.dim("Pickup folder still on disk: ") + existing);
20333
20791
  const reuse = await session.confirm("Reuse this pickup folder?", true);
@@ -20433,7 +20891,7 @@ __export(ingest_exports, {
20433
20891
  handler: () => handler2
20434
20892
  });
20435
20893
  import chalk20 from "chalk";
20436
- import { readFileSync as readFileSync19, existsSync as existsSync24 } from "fs";
20894
+ import { readFileSync as readFileSync22, existsSync as existsSync27 } from "fs";
20437
20895
  import { basename as basename6 } from "path";
20438
20896
  async function handler2(args, ctx) {
20439
20897
  const { positional, flags } = parseArgs2(args, [
@@ -20457,7 +20915,7 @@ async function handler2(args, ctx) {
20457
20915
  console.error(chalk20.dim(" /ingest --demo [--scenario <name>]"));
20458
20916
  process.exit(1);
20459
20917
  }
20460
- if (!existsSync24(file)) {
20918
+ if (!existsSync27(file)) {
20461
20919
  console.error(chalk20.red(` File not found: ${file}`));
20462
20920
  process.exit(1);
20463
20921
  }
@@ -20475,7 +20933,7 @@ async function handler2(args, ctx) {
20475
20933
  try {
20476
20934
  await initSchema();
20477
20935
  spinner.text = "Parsing CSV\u2026";
20478
- const content = readFileSync19(file, "utf-8");
20936
+ const content = readFileSync22(file, "utf-8");
20479
20937
  const { rows, headers } = parseCSV(content);
20480
20938
  if (rows.length === 0) {
20481
20939
  spinner.fail("CSV is empty");
@@ -22842,9 +23300,9 @@ async function handleGetSessionBrief(input) {
22842
23300
  if (!target) {
22843
23301
  return { error: `No session matching "${raw}".` };
22844
23302
  }
22845
- const { existsSync: existsSync38, readFileSync: readFileSync24 } = await import("fs");
23303
+ const { existsSync: existsSync39, readFileSync: readFileSync25 } = await import("fs");
22846
23304
  const briefPath = contextDocPathForSession2(target.id);
22847
- if (!existsSync38(briefPath)) {
23305
+ if (!existsSync39(briefPath)) {
22848
23306
  return {
22849
23307
  session_id: target.id,
22850
23308
  error: "No context brief on disk for this session (created before brief storage existed).",
@@ -22855,7 +23313,7 @@ async function handleGetSessionBrief(input) {
22855
23313
  return {
22856
23314
  session_id: target.id,
22857
23315
  security_notice: UNTRUSTED_CONTENT_NOTICE,
22858
- brief: wrapUntrustedContent(readFileSync24(briefPath, "utf-8"))
23316
+ brief: wrapUntrustedContent(readFileSync25(briefPath, "utf-8"))
22859
23317
  };
22860
23318
  }
22861
23319
  function auditDenied(name, input, resultJson, start) {
@@ -23388,7 +23846,9 @@ function buildSituationalAwarenessBlock(ctx, opts = {}) {
23388
23846
  );
23389
23847
  } else if (phase === "orient") {
23390
23848
  lines.push("Already done: nothing locked yet");
23391
- lines.push("Available now: help the user name a focus; CLI will propose scope from their words");
23849
+ lines.push(
23850
+ "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"
23851
+ );
23392
23852
  } else if (phase === "think") {
23393
23853
  lines.push("Already done: analysis complete; think channel open");
23394
23854
  lines.push("Available now: socratic exploration; draft_strategy / draft_handoff when ready to graduate");
@@ -25159,7 +25619,7 @@ __export(onboard_tiers_exports, {
25159
25619
  resetOnboardTierProgress: () => resetOnboardTierProgress,
25160
25620
  resolveNextOnboardTier: () => resolveNextOnboardTier
25161
25621
  });
25162
- import { existsSync as existsSync25, statSync as statSync4 } from "fs";
25622
+ import { existsSync as existsSync28, statSync as statSync4 } from "fs";
25163
25623
  function flagSet(tier) {
25164
25624
  return Boolean(getConfigValue(TIER_CONFIG_KEYS[tier]));
25165
25625
  }
@@ -25186,7 +25646,7 @@ function hasProductionDataset(ctx) {
25186
25646
  if (source.startsWith("/") || /^[A-Za-z]:[\\/]/.test(source) || source.startsWith("~")) {
25187
25647
  return true;
25188
25648
  }
25189
- if (!source.includes(":") && existsSync25(source)) return true;
25649
+ if (!source.includes(":") && existsSync28(source)) return true;
25190
25650
  if (source.startsWith("csv:") || source.startsWith("file:") || source.startsWith("folder:")) {
25191
25651
  return true;
25192
25652
  }
@@ -25245,7 +25705,7 @@ function markDemoDataSeen() {
25245
25705
  }
25246
25706
  function pathLooksPresent(raw) {
25247
25707
  try {
25248
- return existsSync25(raw) && (statSync4(raw).isFile() || statSync4(raw).isDirectory());
25708
+ return existsSync28(raw) && (statSync4(raw).isFile() || statSync4(raw).isDirectory());
25249
25709
  } catch {
25250
25710
  return false;
25251
25711
  }
@@ -25650,7 +26110,7 @@ __export(onboard_exports, {
25650
26110
  profileExists: () => profileExists
25651
26111
  });
25652
26112
  import chalk25 from "chalk";
25653
- import { existsSync as existsSync26 } from "fs";
26113
+ import { existsSync as existsSync29 } from "fs";
25654
26114
  import { basename as basename7 } from "path";
25655
26115
  async function handler5(args, ctx) {
25656
26116
  const { flags } = parseArgs2(args, ["force", "skip-brand"]);
@@ -25925,7 +26385,7 @@ async function runProductionTier(session, ctx) {
25925
26385
  return "Production data skipped";
25926
26386
  }
25927
26387
  const resolved = resolveUserPath(trimmed);
25928
- if (!existsSync26(resolved)) {
26388
+ if (!existsSync29(resolved)) {
25929
26389
  console.log(" " + chalk25.red(`Path not found: ${resolved}`));
25930
26390
  console.log(" " + chalk25.dim("Try again with /onboard, or drop the path into the REPL."));
25931
26391
  return "Production path not found";
@@ -26332,7 +26792,7 @@ __export(new_exports, {
26332
26792
  handler: () => handler6
26333
26793
  });
26334
26794
  import chalk26 from "chalk";
26335
- import { existsSync as existsSync27 } from "fs";
26795
+ import { existsSync as existsSync30 } from "fs";
26336
26796
  import { basename as basename8 } from "path";
26337
26797
  async function handler6(args, ctx) {
26338
26798
  const { positional, flags } = parseArgs2(args, ["demo", "empty", "list-scenarios", "regen-taxonomy"]);
@@ -26354,7 +26814,7 @@ async function handler6(args, ctx) {
26354
26814
  console.error(chalk26.red(" Usage: /new <file.csv> | --demo [--scenario <name>] | --empty [--lens health|metrics]"));
26355
26815
  return;
26356
26816
  }
26357
- if (source.kind === "file" && !existsSync27(source.path)) {
26817
+ if (source.kind === "file" && !existsSync30(source.path)) {
26358
26818
  console.error(chalk26.red(` File not found: ${source.path}`));
26359
26819
  return;
26360
26820
  }
@@ -26581,7 +27041,7 @@ __export(end_exports, {
26581
27041
  handler: () => handler7
26582
27042
  });
26583
27043
  import chalk27 from "chalk";
26584
- import { existsSync as existsSync28 } from "fs";
27044
+ import { existsSync as existsSync31 } from "fs";
26585
27045
  async function handler7(args, ctx) {
26586
27046
  if (args.length > 0) {
26587
27047
  console.error(chalk27.red(" Usage: /end"));
@@ -26618,10 +27078,10 @@ async function handler7(args, ctx) {
26618
27078
  if (summary) {
26619
27079
  console.log(" " + chalk27.dim(summary));
26620
27080
  }
26621
- if (existsSync28(transcriptPathForSession(endedId))) {
27081
+ if (existsSync31(transcriptPathForSession(endedId))) {
26622
27082
  console.log(" " + chalk27.dim("Transcript: ") + chalk27.dim(transcriptPathForSession(endedId)));
26623
27083
  }
26624
- if (existsSync28(contextDocPathForSession(endedId))) {
27084
+ if (existsSync31(contextDocPathForSession(endedId))) {
26625
27085
  console.log(" " + chalk27.dim("Context brief: ") + chalk27.dim(contextDocPathForSession(endedId)));
26626
27086
  }
26627
27087
  console.log();
@@ -26642,8 +27102,8 @@ __export(session_exports, {
26642
27102
  handler: () => handler8
26643
27103
  });
26644
27104
  import chalk28 from "chalk";
26645
- import { join as join23 } from "path";
26646
- import { existsSync as existsSync29 } from "fs";
27105
+ import { join as join26 } from "path";
27106
+ import { existsSync as existsSync32 } from "fs";
26647
27107
  async function handler8(args, ctx) {
26648
27108
  const sub = args[0];
26649
27109
  if (!sub) return listSessionsView(ctx);
@@ -26746,7 +27206,7 @@ async function pickUp(idArg, ctx) {
26746
27206
  }
26747
27207
  resetContextForSwitch(ctx, {
26748
27208
  sessionId: target.id,
26749
- sessionFile: join23(getSessionsDir(), `${target.id}.json`),
27209
+ sessionFile: join26(getSessionsDir(), `${target.id}.json`),
26750
27210
  sessionName: session.name,
26751
27211
  messages: [...session.messages],
26752
27212
  conversation: session.thread ? [...session.thread] : [],
@@ -26761,7 +27221,8 @@ async function pickUp(idArg, ctx) {
26761
27221
  llm: session.llm ? { ...session.llm } : void 0,
26762
27222
  strategistState: session.strategist,
26763
27223
  thinkState: session.think,
26764
- pendingAsk: session.pending_ask
27224
+ pendingAsk: session.pending_ask,
27225
+ lastCraftJobId: session.last_craft_job_id
26765
27226
  });
26766
27227
  ctx.datasetPath = datasetPathForSession(target.id);
26767
27228
  await setActiveDbPath(ctx.datasetPath);
@@ -26779,7 +27240,11 @@ async function pickUp(idArg, ctx) {
26779
27240
  if (session.strategist && session.strategist.step !== "awaiting_analysis") {
26780
27241
  const objective = session.strategist.objective;
26781
27242
  console.log(
26782
- " " + chalk28.yellow("Resuming mid-strategy") + (objective ? chalk28.dim(`: "${objective}"`) : "") + chalk28.dim(" \u2014 Confirm? ") + chalk28.cyan("\u23CE yes") + chalk28.dim(" \xB7 ") + chalk28.cyan("cancel") + chalk28.dim(" to drop it.")
27243
+ " " + chalk28.yellow("Resuming mid-strategy") + (objective ? chalk28.dim(`: "${objective}"`) : "") + chalk28.dim(" \u2014 Confirm? ") + chalk28.cyan("\u23CE yes") + (session.strategist.mode === "craft" ? chalk28.dim(" \xB7 keep going") : "") + chalk28.dim(" \xB7 ") + chalk28.cyan("cancel") + chalk28.dim(" to drop it.")
27244
+ );
27245
+ } else if (unfinishedCraftJob(session.last_craft_job_id)) {
27246
+ console.log(
27247
+ " " + chalk28.dim("Plan in progress \u2014 type ") + chalk28.cyan("keep going") + chalk28.dim(" to continue.")
26783
27248
  );
26784
27249
  }
26785
27250
  if (session.think && session.think.step === "active") {
@@ -26789,7 +27254,7 @@ async function pickUp(idArg, ctx) {
26789
27254
  );
26790
27255
  }
26791
27256
  const contextPath = contextDocPathForSession(target.id);
26792
- if (existsSync29(contextPath)) {
27257
+ if (existsSync32(contextPath)) {
26793
27258
  console.log(" " + chalk28.dim("Context brief: ") + chalk28.dim(contextPath));
26794
27259
  }
26795
27260
  console.log();
@@ -26804,6 +27269,7 @@ var init_session = __esm({
26804
27269
  init_connection();
26805
27270
  init_theme();
26806
27271
  init_layout();
27272
+ init_store3();
26807
27273
  GENERIC_DATASET_RE = /^(demo dataset|.*\bdemo)$/i;
26808
27274
  }
26809
27275
  });
@@ -27080,8 +27546,8 @@ __export(report_exports, {
27080
27546
  handler: () => handler9
27081
27547
  });
27082
27548
  import chalk29 from "chalk";
27083
- import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync15 } from "fs";
27084
- import { dirname as dirname4 } from "path";
27549
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync17 } from "fs";
27550
+ import { dirname as dirname5 } from "path";
27085
27551
  async function handler9(args, ctx) {
27086
27552
  const { flags } = parseArgs2(args);
27087
27553
  const format = getString(flags, "format", "f") ?? getConfigValue("default-format") ?? "terminal";
@@ -27174,10 +27640,10 @@ async function handler9(args, ctx) {
27174
27640
  if (output) {
27175
27641
  const resolvedOutput = resolveUserPath(output);
27176
27642
  if (!isInsideNtrp(resolvedOutput)) {
27177
- console.warn(chalk29.yellow(` Warning: writing report outside NTRP home (${dirname4(resolvedOutput)})`));
27643
+ console.warn(chalk29.yellow(` Warning: writing report outside NTRP home (${dirname5(resolvedOutput)})`));
27178
27644
  }
27179
- mkdirSync13(dirname4(resolvedOutput), { recursive: true });
27180
- writeFileSync15(resolvedOutput, rendered);
27645
+ mkdirSync14(dirname5(resolvedOutput), { recursive: true });
27646
+ writeFileSync17(resolvedOutput, rendered);
27181
27647
  console.log(chalk29.green(` Report written to ${resolvedOutput}`));
27182
27648
  } else if (rendered) {
27183
27649
  console.log(rendered);
@@ -27208,8 +27674,8 @@ var init_report2 = __esm({
27208
27674
  });
27209
27675
 
27210
27676
  // src/output/notes-export.ts
27211
- import { mkdirSync as mkdirSync14 } from "fs";
27212
- import { join as join24 } from "path";
27677
+ import { mkdirSync as mkdirSync15 } from "fs";
27678
+ import { join as join27 } from "path";
27213
27679
  function exportToNotes(data) {
27214
27680
  const { computeResult, divergences, findings, exchanges } = data;
27215
27681
  const { aggregate, segments } = computeResult;
@@ -27218,8 +27684,8 @@ function exportToNotes(data) {
27218
27684
  const timeStr = formatTime(now2);
27219
27685
  const filename = `${dateStr}-${timeStr}-gtm-health.md`;
27220
27686
  const dir = data.dir ?? getArchiveKindDir("notes");
27221
- mkdirSync14(dir, { recursive: true });
27222
- const filepath = join24(dir, filename);
27687
+ mkdirSync15(dir, { recursive: true });
27688
+ const filepath = join27(dir, filename);
27223
27689
  const severityTags = /* @__PURE__ */ new Set();
27224
27690
  for (const f of findings) severityTags.add(f.severity);
27225
27691
  const tags = ["ntrp", "gtm-health", ...severityTags];
@@ -27464,8 +27930,8 @@ __export(backmeup_exports, {
27464
27930
  });
27465
27931
  import chalk31 from "chalk";
27466
27932
  import Papa5 from "papaparse";
27467
- import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync16 } from "fs";
27468
- import { join as join25 } from "path";
27933
+ import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync18 } from "fs";
27934
+ import { join as join28 } from "path";
27469
27935
  function sanitizeCsvValue(value) {
27470
27936
  if (typeof value !== "string") return value;
27471
27937
  return CSV_FORMULA_RE.test(value) ? `'${value}` : value;
@@ -27501,8 +27967,8 @@ async function handler11(args, _ctx) {
27501
27967
  if (!isInsideNtrp(baseDir)) {
27502
27968
  console.warn(chalk31.yellow(` Warning: writing backup outside NTRP home (${baseDir})`));
27503
27969
  }
27504
- const folder = join25(baseDir, folderName);
27505
- mkdirSync15(folder, { recursive: true });
27970
+ const folder = join28(baseDir, folderName);
27971
+ mkdirSync16(folder, { recursive: true });
27506
27972
  const generatedAt = now2.toISOString();
27507
27973
  let fileCount = 0;
27508
27974
  const coverRows = health.vital_signs.map((vs) => ({
@@ -27516,7 +27982,7 @@ async function handler11(args, _ctx) {
27516
27982
  "Total At Risk": health.total_value_at_risk != null ? formatCurrency(health.total_value_at_risk) : "N/A",
27517
27983
  "Generated At": generatedAt
27518
27984
  }));
27519
- writeFileSync16(join25(folder, "cover-sheet.csv"), Papa5.unparse(sanitizeCsvRows(coverRows)), "utf-8");
27985
+ writeFileSync18(join28(folder, "cover-sheet.csv"), Papa5.unparse(sanitizeCsvRows(coverRows)), "utf-8");
27520
27986
  fileCount++;
27521
27987
  if (findings.length > 0) {
27522
27988
  const findingsRows = findings.map((f) => ({
@@ -27526,7 +27992,7 @@ async function handler11(args, _ctx) {
27526
27992
  Finding: f.finding,
27527
27993
  "Recommended Plays": f.recommended_plays ? f.recommended_plays.map((p) => p.play_name).join("; ") : ""
27528
27994
  }));
27529
- writeFileSync16(join25(folder, "findings.csv"), Papa5.unparse(sanitizeCsvRows(findingsRows)), "utf-8");
27995
+ writeFileSync18(join28(folder, "findings.csv"), Papa5.unparse(sanitizeCsvRows(findingsRows)), "utf-8");
27530
27996
  fileCount++;
27531
27997
  }
27532
27998
  for (const vs of health.vital_signs) {
@@ -27536,7 +28002,7 @@ async function handler11(args, _ctx) {
27536
28002
  ...detail
27537
28003
  }));
27538
28004
  const filename = EVIDENCE_FILENAMES[vs.vital_sign] ?? `${vs.vital_sign}.csv`;
27539
- writeFileSync16(join25(folder, filename), Papa5.unparse(sanitizeCsvRows(rows)), "utf-8");
28005
+ writeFileSync18(join28(folder, filename), Papa5.unparse(sanitizeCsvRows(rows)), "utf-8");
27540
28006
  fileCount++;
27541
28007
  }
27542
28008
  const event = recordExportWrite({
@@ -27740,8 +28206,8 @@ var init_bundle = __esm({
27740
28206
  });
27741
28207
 
27742
28208
  // src/repositories/markdown.ts
27743
- import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync17 } from "fs";
27744
- import { basename as basename9, dirname as dirname5, join as join26, resolve as resolve8 } from "path";
28209
+ import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync19 } from "fs";
28210
+ import { basename as basename9, dirname as dirname6, join as join29, resolve as resolve8 } from "path";
27745
28211
  import { stringify as stringifyYaml2 } from "yaml";
27746
28212
  function renderMarkdownFiles(pkg) {
27747
28213
  const bundleJson = JSON.stringify(pkg, null, 2) + "\n";
@@ -27961,12 +28427,12 @@ var init_markdown3 = __esm({
27961
28427
  write(pkg) {
27962
28428
  const root = getRootPath(pkg.target);
27963
28429
  const files = renderMarkdownFiles(pkg);
27964
- mkdirSync16(root, { recursive: true });
28430
+ mkdirSync17(root, { recursive: true });
27965
28431
  const written = [];
27966
28432
  for (const file of files) {
27967
- const absolutePath = join26(root, file.relativePath);
27968
- mkdirSync16(dirname5(absolutePath), { recursive: true });
27969
- writeFileSync17(absolutePath, file.contents, "utf-8");
28433
+ const absolutePath = join29(root, file.relativePath);
28434
+ mkdirSync17(dirname6(absolutePath), { recursive: true });
28435
+ writeFileSync19(absolutePath, file.contents, "utf-8");
27970
28436
  written.push(absolutePath);
27971
28437
  }
27972
28438
  return {
@@ -28270,7 +28736,7 @@ __export(handoff_exports, {
28270
28736
  handler: () => handler13
28271
28737
  });
28272
28738
  import chalk33 from "chalk";
28273
- import { join as join27 } from "path";
28739
+ import { join as join30 } from "path";
28274
28740
  async function handler13(args, ctx) {
28275
28741
  const sub = args[0];
28276
28742
  if (!sub) {
@@ -28370,7 +28836,7 @@ async function runPublish2(args, ctx) {
28370
28836
  );
28371
28837
  if (sub === "propose" && !hasDir) {
28372
28838
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
28373
- const dir = join27(getArchiveKindDir("publish"), `ntrp-repository-${stamp}`);
28839
+ const dir = join30(getArchiveKindDir("publish"), `ntrp-repository-${stamp}`);
28374
28840
  publishArgs.push("--dir", dir);
28375
28841
  }
28376
28842
  const result = await publish(publishArgs, ctx);
@@ -29145,150 +29611,6 @@ var init_segment = __esm({
29145
29611
  }
29146
29612
  });
29147
29613
 
29148
- // src/services/strategist.ts
29149
- import { createHash as createHash2 } from "crypto";
29150
- function serializeGapAudit(audit) {
29151
- const lines = [`Can compute: ${audit.can_compute} (lens: ${audit.primary_lens})`];
29152
- for (const item of audit.satisfied) {
29153
- lines.push(`- HAVE ${item.label}: ${item.detail}`);
29154
- }
29155
- for (const item of audit.missing) {
29156
- lines.push(`- MISSING ${item.label}: ${item.why}`);
29157
- }
29158
- for (const item of audit.optional) {
29159
- lines.push(`- LIMITED ${item.label}: ${item.detail}`);
29160
- }
29161
- return lines.join("\n");
29162
- }
29163
- async function prepareStrategistInputs(ctx, objective) {
29164
- let snapshot = ctx.snapshot.computeResult;
29165
- if (!snapshot) {
29166
- snapshot = await computeFullHealth();
29167
- ctx.snapshot.computeResult = snapshot;
29168
- const divInput = snapshot.segments.map((s) => ({
29169
- segmentId: s.segment.id,
29170
- segmentName: s.segment.name,
29171
- result: s.result
29172
- }));
29173
- ctx.snapshot.divergences = detectDivergences(snapshot.aggregate, divInput).divergences;
29174
- }
29175
- const audit = ctx.gapAudit ?? await refreshGapAudit(ctx).catch(() => null);
29176
- const gapAuditBlock = audit ? serializeGapAudit(audit) : "";
29177
- const memoryBlock = await Promise.resolve().then(() => (init_store2(), store_exports2)).then((m) => m.buildMemoryBlock(objective)).catch(() => "");
29178
- let baselineBatchId = null;
29179
- try {
29180
- const reading = await getLatestHealthReading();
29181
- baselineBatchId = reading?.upload_batch_id ?? null;
29182
- } catch {
29183
- }
29184
- return {
29185
- snapshot,
29186
- divergences: ctx.snapshot.divergences,
29187
- gapAuditBlock,
29188
- memoryBlock,
29189
- baselineBatchId,
29190
- includeMetrics: true
29191
- };
29192
- }
29193
- function proposeObjectiveFromSnapshot(snapshot) {
29194
- const { aggregate } = snapshot;
29195
- const gating = aggregate.gating_vital_sign;
29196
- if (!gating) return null;
29197
- const vital = aggregate.vital_signs.find((v) => v.vital_sign === gating);
29198
- if (!vital) return null;
29199
- const label = VITAL_SIGN_LABELS[gating] ?? gating;
29200
- const dollar = vital.dollar_value != null && vital.dollar_value > 0 ? ` and recover the ${formatCurrency(vital.dollar_value)} ${vital.dollar_label ?? "at stake"}` : "";
29201
- return `Move ${label} from ${Math.round(vital.score)} to 60+${dollar} within 60 days`;
29202
- }
29203
- function slugify2(value) {
29204
- const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
29205
- return slug || `strategy-${Date.now()}`;
29206
- }
29207
- function outcomeToMetric(outcome) {
29208
- return {
29209
- name: outcome.metric,
29210
- target: outcome.target_range,
29211
- baseline: outcome.baseline,
29212
- timeframe: `by ${outcome.check_date}`
29213
- };
29214
- }
29215
- async function persistStrategistPlan(plan, opts = {}) {
29216
- await initSchema();
29217
- const slug = slugify2(plan.title);
29218
- const libraryPath = strategyLibraryPath(slug);
29219
- const linkedPlayIds = [...new Set(plan.workstreams.flatMap((ws) => ws.play_ids))];
29220
- const successMetrics = plan.workstreams.map((ws) => outcomeToMetric(ws.expected_outcome));
29221
- const leadingIndicators = plan.workstreams.flatMap((ws) => ws.leading_indicators.map(outcomeToMetric));
29222
- const recommendedActions = plan.workstreams.flatMap((ws) => ws.actions.map((action) => `[WS${ws.order}] ${action}`)).slice(0, 15);
29223
- const reviewProtocol = [
29224
- `Review ${plan.review_cadence.toLowerCase()} with /strategy review ${slug}.`,
29225
- `Check each milestone at its due date against the named verification method.`,
29226
- `At each outcome check date, compare the measured value to its target range against baseline batch ${opts.baselineBatchId ?? "(latest)"}.`,
29227
- `If a contingency trigger fires, activate the pre-agreed fallback.`
29228
- ].join(" ");
29229
- const id = await upsertStrategy({
29230
- slug,
29231
- title: plan.title,
29232
- status: opts.status ?? "active",
29233
- source_type: "agent",
29234
- source_path: null,
29235
- goal: plan.objective,
29236
- hypothesis: plan.hypothesis,
29237
- target_segment: plan.target_segment,
29238
- priority: plan.priority,
29239
- linked_play_ids: linkedPlayIds,
29240
- success_metrics: successMetrics,
29241
- leading_indicators: leadingIndicators,
29242
- risks: plan.risks,
29243
- recommended_actions: recommendedActions,
29244
- experiment_design: reviewProtocol,
29245
- review_cadence: plan.review_cadence,
29246
- confidence: plan.confidence,
29247
- raw_excerpt: plan.summary_30k,
29248
- library_path: libraryPath,
29249
- origin: "strategist",
29250
- objective: plan.objective,
29251
- constraints: plan.constraints,
29252
- workstreams: plan.workstreams,
29253
- assumptions: plan.assumptions,
29254
- baseline_batch_id: opts.baselineBatchId ?? null
29255
- });
29256
- const strategy = await getStrategyBySlugOrId(id);
29257
- if (!strategy) {
29258
- throw new NtrpError("strategy_persist_failed", "Strategy was not found after saving.", 1 /* RuntimeError */);
29259
- }
29260
- const writtenPath = writeStrategyMarkdown(strategy);
29261
- await insertStrategySource({
29262
- strategy_id: strategy.id,
29263
- source_type: "agent",
29264
- source_path: null,
29265
- content_hash: createHash2("sha256").update(JSON.stringify(plan)).digest("hex"),
29266
- extracted_text_excerpt: plan.summary_30k.slice(0, 800),
29267
- metadata: {
29268
- origin: "strategist",
29269
- objective: plan.objective,
29270
- workstream_count: plan.workstreams.length,
29271
- baseline_batch_id: opts.baselineBatchId ?? null
29272
- }
29273
- });
29274
- return { strategy: { ...strategy, library_path: writtenPath }, library_path: writtenPath };
29275
- }
29276
- var init_strategist = __esm({
29277
- "src/services/strategist.ts"() {
29278
- "use strict";
29279
- init_schema();
29280
- init_queries();
29281
- init_health_score();
29282
- init_divergence();
29283
- init_gap_audit();
29284
- init_library();
29285
- init_errors2();
29286
- init_types2();
29287
- init_formatters();
29288
- init_formatters();
29289
- }
29290
- });
29291
-
29292
29614
  // src/ai/strategist-prompt.ts
29293
29615
  function companyContextSection4() {
29294
29616
  const block = buildCompanyProfileBlock();
@@ -29427,6 +29749,52 @@ ${AAR_BLOCK}
29427
29749
 
29428
29750
  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.`;
29429
29751
  }
29752
+ function buildCriticMessage(input) {
29753
+ 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.
29754
+
29755
+ ARMED OBJECTIVE (byte-identical; do not rewrite):
29756
+ "${input.objective}"
29757
+
29758
+ CODE RUBRIC GAPS (already blocking \u2014 you cannot ship until these are gone):
29759
+ ${input.rubricGaps || "(none)"}
29760
+
29761
+ LIVE SNAPSHOT (cite these numbers only):
29762
+ ${input.healthSnapshot}
29763
+
29764
+ CURRENT PLAN JSON:
29765
+ ${input.planJson}
29766
+
29767
+ ${HEILMEIER_GATE}
29768
+
29769
+ ${AAR_BLOCK}
29770
+
29771
+ Decide whether a partner would ship this. Respond with STRICT JSON only:
29772
+ {
29773
+ "ship": false,
29774
+ "score": 0,
29775
+ "objective_echo": ${JSON.stringify(input.objective)},
29776
+ "gaps": [{ "code": "bottleneck", "severity": "blocking", "note": "...", "fix": "..." }],
29777
+ "killed_alternative": "one approach that failed newness/stake/exams",
29778
+ "partner_pushback": "one paragraph a partner would actually say"
29779
+ }
29780
+
29781
+ 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.`;
29782
+ }
29783
+ function buildReviseMessage(input) {
29784
+ 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.
29785
+
29786
+ ARMED OBJECTIVE (must appear unchanged as "objective"):
29787
+ "${input.objective}"
29788
+
29789
+ GAPS TO CLOSE:
29790
+ ${input.gapsJson}
29791
+
29792
+ CURRENT PLAN:
29793
+ ${input.planJson}
29794
+
29795
+ Respond with ONLY the full plan JSON object in this schema \u2014 no prose, no fences:
29796
+ ${STRATEGIST_PLAN_SCHEMA_BLOCK}`;
29797
+ }
29430
29798
  var STRATEGIST_PLAN_SCHEMA_BLOCK;
29431
29799
  var init_strategist_prompt = __esm({
29432
29800
  "src/ai/strategist-prompt.ts"() {
@@ -29954,6 +30322,297 @@ var init_strategist_validate = __esm({
29954
30322
  }
29955
30323
  });
29956
30324
 
30325
+ // src/ai/strategist-rubric.ts
30326
+ function numbersMatch2(a, b) {
30327
+ if (a === b) return true;
30328
+ if (a === 0 || b === 0) return Math.abs(a - b) < 0.5;
30329
+ return Math.abs(a - b) / Math.max(Math.abs(a), Math.abs(b)) <= 0.02;
30330
+ }
30331
+ function parseIsoDate2(value) {
30332
+ const match = value.trim().match(/^(\d{4})-(\d{2})-(\d{2})/);
30333
+ if (!match) return null;
30334
+ const date = /* @__PURE__ */ new Date(`${match[1]}-${match[2]}-${match[3]}T00:00:00Z`);
30335
+ return Number.isNaN(date.getTime()) ? null : date;
30336
+ }
30337
+ function addDays2(date, days) {
30338
+ const out = new Date(date);
30339
+ out.setUTCDate(out.getUTCDate() + days);
30340
+ return out;
30341
+ }
30342
+ function workstreamText(ws) {
30343
+ return [ws.title, ws.problem, ws.rationale, ws.actions.join(" "), ws.play_ids.join(" ")].join(" ");
30344
+ }
30345
+ function hasOwnerShape(ws) {
30346
+ const blob = workstreamText(ws).toLowerCase();
30347
+ return OWNER_TOKENS.some((token) => blob.includes(token));
30348
+ }
30349
+ function firstWorkstreamMovesConstraint(ws, gating) {
30350
+ const plays = getPlaybook();
30351
+ if (ws.play_ids.some((id) => plays.find((p) => p.id === id)?.trigger_vital_sign === gating)) {
30352
+ return true;
30353
+ }
30354
+ const blob = workstreamText(ws).toLowerCase();
30355
+ return VITAL_ALIASES[gating].some((alias) => blob.includes(alias));
30356
+ }
30357
+ function citesStake(plan, snapshot) {
30358
+ const dollars = [];
30359
+ if (snapshot.aggregate.total_value_at_risk != null && snapshot.aggregate.total_value_at_risk > 0) {
30360
+ dollars.push(snapshot.aggregate.total_value_at_risk);
30361
+ }
30362
+ for (const vs of snapshot.aggregate.vital_signs) {
30363
+ if (vs.dollar_value != null && vs.dollar_value > 0) dollars.push(vs.dollar_value);
30364
+ }
30365
+ if (dollars.length === 0) return true;
30366
+ const blob = `${plan.summary_30k} ${plan.hypothesis} ${plan.workstreams[0]?.problem ?? ""}`;
30367
+ const found = extractNumbers(blob);
30368
+ if (found.some((n) => dollars.some((d) => numbersMatch2(n, d)))) return true;
30369
+ const formatted = dollars.map((d) => formatCurrency(d).toLowerCase());
30370
+ const lower = blob.toLowerCase();
30371
+ return formatted.some((f) => lower.includes(f.toLowerCase()));
30372
+ }
30373
+ function hasStartWithin48h(plan, todayIso) {
30374
+ const today = parseIsoDate2(todayIso) ?? /* @__PURE__ */ new Date(`${todayIso}T00:00:00Z`);
30375
+ const limit = addDays2(today, 2);
30376
+ const first = plan.workstreams[0];
30377
+ if (!first) return false;
30378
+ if (first.actions.some((action) => START_SOON_RE.test(action))) return true;
30379
+ const dated = [
30380
+ ...first.milestones.map((m) => m.due),
30381
+ ...first.deliverables.map((d) => d.due)
30382
+ ];
30383
+ for (const iso of dated) {
30384
+ const parsed = parseIsoDate2(iso);
30385
+ if (parsed && parsed.getTime() <= limit.getTime()) return true;
30386
+ }
30387
+ return plan.workstreams.some(
30388
+ (ws) => ws.actions.some((action) => START_SOON_RE.test(action))
30389
+ );
30390
+ }
30391
+ function examsHold(ws) {
30392
+ const o = ws.expected_outcome;
30393
+ return extractNumbers(o.baseline).length > 0 && extractNumbers(o.target_range).length > 0 && isKnownInstrument(o.measured_by);
30394
+ }
30395
+ function inventedNumbers(plan, evidenceText) {
30396
+ const evidenceNums = extractNumbers(evidenceText);
30397
+ if (evidenceNums.length === 0) return false;
30398
+ for (const ws of plan.workstreams) {
30399
+ const baselines = extractNumbers(ws.expected_outcome.baseline);
30400
+ if (baselines.length === 0) continue;
30401
+ if (!baselines.some((b) => evidenceNums.some((e) => numbersMatch2(b, e)))) return true;
30402
+ }
30403
+ return false;
30404
+ }
30405
+ function scoreConsultantPlan(plan, opts) {
30406
+ const gaps = [];
30407
+ const gating = opts.snapshot.aggregate.gating_vital_sign;
30408
+ const first = plan.workstreams[0];
30409
+ const evidence = opts.evidenceText ?? JSON.stringify({
30410
+ aggregate: opts.snapshot.aggregate
30411
+ });
30412
+ if (opts.armedObjective && plan.objective !== opts.armedObjective) {
30413
+ gaps.push({
30414
+ code: "objective_drift",
30415
+ severity: "blocking",
30416
+ note: "Plan objective does not match the armed objective.",
30417
+ fix: "Copy the armed objective into plan.objective with no edits."
30418
+ });
30419
+ }
30420
+ if (!first || !firstWorkstreamMovesConstraint(first, gating)) {
30421
+ const label = VITAL_SIGN_LABELS[gating] ?? gating;
30422
+ gaps.push({
30423
+ code: "bottleneck",
30424
+ severity: "blocking",
30425
+ note: `First workstream does not move the gating vital (${label}).`,
30426
+ fix: `Lead with a play or problem that moves ${gating}. Non-constraint work waits.`
30427
+ });
30428
+ }
30429
+ if (!citesStake(plan, opts.snapshot)) {
30430
+ gaps.push({
30431
+ code: "stake",
30432
+ severity: "blocking",
30433
+ note: "30k or hypothesis does not cite snapshot dollars / cost of inaction.",
30434
+ fix: "Open with the gating dollar figure from the live snapshot."
30435
+ });
30436
+ }
30437
+ for (const ws of plan.workstreams) {
30438
+ if (!hasOwnerShape(ws)) {
30439
+ gaps.push({
30440
+ code: "owner",
30441
+ severity: "blocking",
30442
+ note: `Workstream "${ws.title}" has no function-shaped owner.`,
30443
+ fix: "Name RevOps, sales manager, CS lead, marketing ops, or AE lead in the actions."
30444
+ });
30445
+ break;
30446
+ }
30447
+ }
30448
+ if (!hasStartWithin48h(plan, opts.todayIso)) {
30449
+ gaps.push({
30450
+ code: "start_48h",
30451
+ severity: "blocking",
30452
+ note: "No action or milestone is startable within 48 hours.",
30453
+ fix: "Put a dated first action or milestone within two days of today."
30454
+ });
30455
+ }
30456
+ const missingEffort = plan.workstreams.filter(
30457
+ (ws) => !Number.isFinite(ws.effort_hours) || ws.effort_hours <= 0
30458
+ );
30459
+ if (missingEffort.length > 0 || plan.workstreams.length === 0) {
30460
+ gaps.push({
30461
+ code: "effort",
30462
+ severity: "blocking",
30463
+ note: "Each workstream needs a finite effort_hours greater than zero.",
30464
+ fix: "Set effort_hours to a real team-hour estimate per workstream."
30465
+ });
30466
+ }
30467
+ if (opts.constraintsNote?.trim() && plan.constraints.length === 0) {
30468
+ gaps.push({
30469
+ code: "scope",
30470
+ severity: "blocking",
30471
+ note: "Operator stated constraints, but the plan constraints list is empty.",
30472
+ fix: "Copy operator constraints into plan.constraints. Name what is out of scope."
30473
+ });
30474
+ }
30475
+ if (plan.workstreams.some((ws) => !examsHold(ws))) {
30476
+ gaps.push({
30477
+ code: "exams",
30478
+ severity: "blocking",
30479
+ note: "A workstream expected outcome is missing a numeric baseline, range, or known instrument.",
30480
+ fix: "Give every expected_outcome a baseline, target range, and measured_by instrument from live data."
30481
+ });
30482
+ }
30483
+ if (inventedNumbers(plan, evidence)) {
30484
+ gaps.push({
30485
+ code: "invented_numbers",
30486
+ severity: "blocking",
30487
+ note: "An outcome baseline is not in the grounded snapshot.",
30488
+ fix: "Copy baselines from the health snapshot exactly."
30489
+ });
30490
+ }
30491
+ const blocking = gaps.filter((g) => g.severity === "blocking");
30492
+ return { pass: blocking.length === 0, gaps, blocking };
30493
+ }
30494
+ function asGapCode(value) {
30495
+ return typeof value === "string" && GAP_CODES.includes(value) ? value : null;
30496
+ }
30497
+ function parseCriticResponse(raw, opts) {
30498
+ if (!raw) {
30499
+ return {
30500
+ ship: false,
30501
+ score: 0,
30502
+ objective_echo: "",
30503
+ gaps: [
30504
+ {
30505
+ code: "thin_evidence",
30506
+ severity: "blocking",
30507
+ note: "Critic reply was not valid JSON.",
30508
+ fix: "Respond with the critic JSON object only."
30509
+ }
30510
+ ],
30511
+ killed_alternative: "",
30512
+ partner_pushback: ""
30513
+ };
30514
+ }
30515
+ const objectiveEcho = typeof raw.objective_echo === "string" ? raw.objective_echo : "";
30516
+ const killed = typeof raw.killed_alternative === "string" ? raw.killed_alternative.trim() : "";
30517
+ const pushback = typeof raw.partner_pushback === "string" ? raw.partner_pushback.trim() : "";
30518
+ const scoreRaw = typeof raw.score === "number" ? raw.score : Number(raw.score);
30519
+ const score = Number.isFinite(scoreRaw) ? Math.max(0, Math.min(100, scoreRaw)) : 0;
30520
+ const gaps = [];
30521
+ if (Array.isArray(raw.gaps)) {
30522
+ for (const item of raw.gaps) {
30523
+ if (!item || typeof item !== "object") continue;
30524
+ const rec = item;
30525
+ const code = asGapCode(rec.code);
30526
+ if (!code) continue;
30527
+ gaps.push({
30528
+ code,
30529
+ severity: rec.severity === "advisory" ? "advisory" : "blocking",
30530
+ note: typeof rec.note === "string" ? rec.note : "",
30531
+ fix: typeof rec.fix === "string" ? rec.fix : ""
30532
+ });
30533
+ }
30534
+ }
30535
+ if (objectiveEcho !== opts.armedObjective) {
30536
+ gaps.push({
30537
+ code: "objective_drift",
30538
+ severity: "blocking",
30539
+ note: "Critic objective_echo does not match the armed objective.",
30540
+ fix: "Echo the armed objective exactly."
30541
+ });
30542
+ }
30543
+ if (!killed) {
30544
+ gaps.push({
30545
+ code: "alternatives_killed",
30546
+ severity: "blocking",
30547
+ note: "Critic did not name an approach that failed newness, stake, or exams.",
30548
+ fix: "Name one killed alternative so the plan is not a restatement of the status quo."
30549
+ });
30550
+ }
30551
+ for (const gap of opts.rubricBlocking) {
30552
+ if (!gaps.some((g) => g.code === gap.code)) gaps.push(gap);
30553
+ }
30554
+ const blocking = gaps.filter((g) => g.severity === "blocking");
30555
+ const modelShip = raw.ship === true;
30556
+ const ship = modelShip && blocking.length === 0 && score >= opts.minScore && objectiveEcho === opts.armedObjective && killed.length > 0;
30557
+ return {
30558
+ ship,
30559
+ score,
30560
+ objective_echo: objectiveEcho,
30561
+ gaps,
30562
+ killed_alternative: killed,
30563
+ partner_pushback: pushback
30564
+ };
30565
+ }
30566
+ function lockPlanObjective(plan, armedObjective) {
30567
+ return { ...plan, objective: armedObjective };
30568
+ }
30569
+ var OWNER_TOKENS, VITAL_ALIASES, START_SOON_RE, GAP_CODES;
30570
+ var init_strategist_rubric = __esm({
30571
+ "src/ai/strategist-rubric.ts"() {
30572
+ "use strict";
30573
+ init_playbook();
30574
+ init_strategist_validate();
30575
+ init_formatters();
30576
+ OWNER_TOKENS = [
30577
+ "revops",
30578
+ "rev ops",
30579
+ "revenue ops",
30580
+ "sales manager",
30581
+ "sales ops",
30582
+ "cs lead",
30583
+ "customer success",
30584
+ "marketing ops",
30585
+ "ae lead",
30586
+ "account executive lead",
30587
+ "sdr lead",
30588
+ "bdr lead",
30589
+ "demand gen",
30590
+ "enablement"
30591
+ ];
30592
+ VITAL_ALIASES = {
30593
+ freshness: ["freshness", "stale", "zombie", "dead pipeline"],
30594
+ flow_rate: ["flow_rate", "flow rate", "stuck", "velocity", "stage"],
30595
+ drop_rate: ["drop_rate", "drop rate", "handoff", "leak"],
30596
+ signal_to_noise: ["signal_to_noise", "signal-to-noise", "signal to noise", "noise", "misdirected"],
30597
+ thread_depth: ["thread_depth", "thread depth", "single-thread", "single threaded", "multi-thread"]
30598
+ };
30599
+ START_SOON_RE = /\b(48\s*-?hours?|48h|tomorrow|today|monday|this week|within two days|within 2 days)\b/i;
30600
+ GAP_CODES = [
30601
+ "bottleneck",
30602
+ "stake",
30603
+ "owner",
30604
+ "start_48h",
30605
+ "effort",
30606
+ "scope",
30607
+ "exams",
30608
+ "alternatives_killed",
30609
+ "thin_evidence",
30610
+ "invented_numbers",
30611
+ "objective_drift"
30612
+ ];
30613
+ }
30614
+ });
30615
+
29957
30616
  // src/ai/strategist.ts
29958
30617
  function buildHealthSnapshot(computeResult, divergences) {
29959
30618
  const { aggregate, segments } = computeResult;
@@ -30176,6 +30835,15 @@ Respond with ONLY the corrected plan JSON object in the required schema (title,
30176
30835
  for (const issue of validated.issues) {
30177
30836
  yield { type: "notice", text: issue };
30178
30837
  }
30838
+ const rubric = scoreConsultantPlan(validated.plan, {
30839
+ snapshot: options.computeResult,
30840
+ constraintsNote: options.constraintsNote,
30841
+ todayIso,
30842
+ evidenceText
30843
+ });
30844
+ for (const gap of rubric.blocking) {
30845
+ yield { type: "notice", text: `Plan gap (${gap.code}): ${gap.note}` };
30846
+ }
30179
30847
  yield {
30180
30848
  type: "plan",
30181
30849
  plan: validated.plan,
@@ -30212,7 +30880,7 @@ function describePlanValidationFailure(text, evidenceText, todayIso) {
30212
30880
  return "unknown validation failure";
30213
30881
  }
30214
30882
  var GROUND_MAX_ROUNDS, BACKCAST_MAX_ROUNDS, STRESS_MAX_ROUNDS, STAGE_MAX_TOKENS, PLAN_JSON_MAX_TOKENS, STAGE_LABELS;
30215
- var init_strategist2 = __esm({
30883
+ var init_strategist = __esm({
30216
30884
  "src/ai/strategist.ts"() {
30217
30885
  "use strict";
30218
30886
  init_types();
@@ -30227,6 +30895,7 @@ var init_strategist2 = __esm({
30227
30895
  init_thread();
30228
30896
  init_strategist_prompt();
30229
30897
  init_strategist_validate();
30898
+ init_strategist_rubric();
30230
30899
  init_strategist_prompt();
30231
30900
  GROUND_MAX_ROUNDS = 6;
30232
30901
  BACKCAST_MAX_ROUNDS = 4;
@@ -30241,6 +30910,803 @@ var init_strategist2 = __esm({
30241
30910
  }
30242
30911
  });
30243
30912
 
30913
+ // src/ai/strategist-craft.ts
30914
+ import { createHash as createHash2 } from "crypto";
30915
+ function snapshotHash(snapshotJson) {
30916
+ return createHash2("sha256").update(snapshotJson).digest("hex").slice(0, 16);
30917
+ }
30918
+ function totalTokens(job) {
30919
+ return job.cumulative_input_tokens + job.cumulative_output_tokens;
30920
+ }
30921
+ function fallbackNotice(text) {
30922
+ return /grounded (playbook )?fallback/i.test(text);
30923
+ }
30924
+ function canShip(job, critic, rubricBlocking) {
30925
+ if (!critic.ship) return false;
30926
+ if (rubricBlocking.length > 0) return false;
30927
+ if (critic.score < job.min_score) return false;
30928
+ if (job.from_fallback && !job.revised_once) return false;
30929
+ return true;
30930
+ }
30931
+ function recordScoreStreak(job, score) {
30932
+ if (job.last_critic_score != null && score <= job.last_critic_score) {
30933
+ job.no_improve_streak += 1;
30934
+ } else {
30935
+ job.no_improve_streak = 0;
30936
+ }
30937
+ job.last_critic_score = score;
30938
+ }
30939
+ async function* strategistCraftSession(options) {
30940
+ assertReplAi(options.ctx);
30941
+ const todayIso = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
30942
+ const healthSnapshot = buildHealthSnapshot(options.computeResult, options.divergences);
30943
+ const maxRounds = options.maxRounds ?? DEFAULT_CRAFT_MAX_ROUNDS;
30944
+ const minScore = options.minScore ?? DEFAULT_CRAFT_MIN_SCORE;
30945
+ const complete = options.complete ?? completeWithFailover;
30946
+ const runIteration0 = options.runIteration0 ?? strategistPlanSession;
30947
+ const job = options.job ?? createRuminationJob({
30948
+ objective: options.objective,
30949
+ constraintsNote: options.constraintsNote,
30950
+ baselineBatchId: options.baselineBatchId,
30951
+ snapshotHash: snapshotHash(healthSnapshot),
30952
+ minScore,
30953
+ maxRounds,
30954
+ maxTokens: options.maxTokens
30955
+ });
30956
+ if (!options.job) job.objective = options.objective;
30957
+ job.min_score = minScore;
30958
+ job.max_rounds = maxRounds;
30959
+ if (options.maxTokens != null) job.max_tokens = options.maxTokens;
30960
+ saveRuminationJob(job);
30961
+ options.ctx.lastCraftJobId = job.status === "ready" ? void 0 : job.id;
30962
+ saveSessionState(options.ctx);
30963
+ const evidenceText = healthSnapshot;
30964
+ const armedObjective = job.objective;
30965
+ let lastMeta = { provider_used: "unknown", model_used: "unknown" };
30966
+ let measurable = { measurable_targets: 0, total_targets: 0 };
30967
+ const scorePlan = (plan) => scoreConsultantPlan(plan, {
30968
+ snapshot: options.computeResult,
30969
+ constraintsNote: options.constraintsNote ?? job.constraints_note,
30970
+ armedObjective,
30971
+ todayIso,
30972
+ evidenceText
30973
+ });
30974
+ const rememberBest = (plan, score) => {
30975
+ if (score == null) {
30976
+ if (!job.best_plan) job.best_plan = plan;
30977
+ return;
30978
+ }
30979
+ if (job.best_score == null || score > job.best_score) {
30980
+ job.best_score = score;
30981
+ job.best_plan = plan;
30982
+ }
30983
+ };
30984
+ try {
30985
+ if (job.status === "ready" && job.plan) {
30986
+ const locked = lockPlanObjective(job.plan, armedObjective);
30987
+ job.plan = locked;
30988
+ saveRuminationJob(job);
30989
+ yield {
30990
+ type: "plan",
30991
+ plan: locked,
30992
+ issues: [],
30993
+ measurable_targets: measurable.measurable_targets,
30994
+ total_targets: measurable.total_targets,
30995
+ baseline_batch_id: options.baselineBatchId ?? null
30996
+ };
30997
+ yield {
30998
+ type: "done",
30999
+ model_used: lastMeta.model_used,
31000
+ provider_used: lastMeta.provider_used,
31001
+ usage: lastMeta
31002
+ };
31003
+ return { job, plan: locked, status: job.status, stop_reason: job.stop_reason };
31004
+ }
31005
+ if (!job.plan) {
31006
+ yield { type: "stage", stage: "ground", label: "Craft \u2014 grounding, sequencing, stress-testing" };
31007
+ for await (const event of runIteration0({ ...options, objective: armedObjective })) {
31008
+ if (options.interrupted?.()) {
31009
+ job.status = "unfinished";
31010
+ job.stop_reason = "interrupt";
31011
+ saveRuminationJob(job);
31012
+ break;
31013
+ }
31014
+ if (event.type === "notice" && fallbackNotice(event.text)) {
31015
+ job.from_fallback = true;
31016
+ }
31017
+ if (event.type === "plan") {
31018
+ const locked = lockPlanObjective(event.plan, armedObjective);
31019
+ job.plan = locked;
31020
+ rememberBest(locked, null);
31021
+ measurable = {
31022
+ measurable_targets: event.measurable_targets,
31023
+ total_targets: event.total_targets
31024
+ };
31025
+ const rubric = scorePlan(locked);
31026
+ job.rubric_gaps = rubric.gaps;
31027
+ yield { ...event, plan: locked, issues: [...event.issues, ...rubric.blocking.map((g) => `Plan gap (${g.code}): ${g.note}`)] };
31028
+ } else if (event.type === "done") {
31029
+ lastMeta = event.usage ?? lastMeta;
31030
+ job.cumulative_input_tokens += event.usage?.input_tokens ?? 0;
31031
+ job.cumulative_output_tokens += event.usage?.output_tokens ?? 0;
31032
+ yield event;
31033
+ } else {
31034
+ yield event;
31035
+ }
31036
+ }
31037
+ saveRuminationJob(job);
31038
+ }
31039
+ if (!job.plan) {
31040
+ job.status = "unfinished";
31041
+ job.stop_reason = job.stop_reason ?? "error";
31042
+ saveRuminationJob(job);
31043
+ yield {
31044
+ type: "done",
31045
+ model_used: lastMeta.model_used,
31046
+ provider_used: lastMeta.provider_used,
31047
+ usage: lastMeta
31048
+ };
31049
+ return { job, plan: null, status: job.status, stop_reason: job.stop_reason };
31050
+ }
31051
+ rememberBest(job.plan, job.best_score);
31052
+ const systemPrompt = buildStrategistSystemPrompt(todayIso);
31053
+ const callText = async (surface, userContent, maxTokens) => {
31054
+ const result = await complete(
31055
+ {
31056
+ surface,
31057
+ messages: [{ role: "user", content: userContent }],
31058
+ system: systemPrompt,
31059
+ max_tokens: maxTokens
31060
+ },
31061
+ { ctx: options.ctx }
31062
+ );
31063
+ lastMeta = result.meta;
31064
+ return {
31065
+ text: result.response.text,
31066
+ input: result.response.token_usage?.input_tokens ?? result.meta.input_tokens ?? 0,
31067
+ output: result.response.token_usage?.output_tokens ?? result.meta.output_tokens ?? 0
31068
+ };
31069
+ };
31070
+ let stop;
31071
+ const startRound = (job.iterations.at(-1)?.round ?? 0) + 1;
31072
+ for (let round = startRound; round <= maxRounds; round++) {
31073
+ if (options.interrupted?.()) {
31074
+ stop = "interrupt";
31075
+ break;
31076
+ }
31077
+ if (job.max_tokens != null && totalTokens(job) >= job.max_tokens) {
31078
+ stop = "budget_tokens";
31079
+ break;
31080
+ }
31081
+ const plan = job.plan;
31082
+ if (!plan) {
31083
+ stop = "error";
31084
+ break;
31085
+ }
31086
+ const rubric = scorePlan(plan);
31087
+ job.rubric_gaps = rubric.gaps;
31088
+ const criticRaw = await callText(
31089
+ "strategist_critic",
31090
+ buildCriticMessage({
31091
+ objective: armedObjective,
31092
+ planJson: JSON.stringify(plan),
31093
+ rubricGaps: rubric.blocking.map((g) => `${g.code}: ${g.note}`).join("\n") || "(none)",
31094
+ healthSnapshot
31095
+ }),
31096
+ CRITIC_MAX_TOKENS
31097
+ );
31098
+ const critic = parseCriticResponse(parseJsonObjectFromText(criticRaw.text), {
31099
+ armedObjective,
31100
+ minScore,
31101
+ rubricBlocking: rubric.blocking
31102
+ });
31103
+ job.last_critic = critic;
31104
+ recordScoreStreak(job, critic.score);
31105
+ rememberBest(plan, critic.score);
31106
+ const tokensThisRound = criticRaw.input + criticRaw.output;
31107
+ let decision = "revise";
31108
+ if (canShip(job, critic, rubric.blocking)) {
31109
+ decision = "ship";
31110
+ } else if (job.no_improve_streak >= 2) {
31111
+ decision = "no-delta";
31112
+ }
31113
+ addRuminationIteration(job, {
31114
+ round,
31115
+ critic_score: critic.score,
31116
+ ship: critic.ship,
31117
+ blocking_codes: critic.gaps.filter((g) => g.severity === "blocking").map((g) => g.code),
31118
+ tokens_in: criticRaw.input,
31119
+ tokens_out: criticRaw.output,
31120
+ decision
31121
+ });
31122
+ yield {
31123
+ type: "craft_round",
31124
+ round,
31125
+ max: maxRounds,
31126
+ score: critic.score,
31127
+ tokensThisRound,
31128
+ tokensTotal: totalTokens(job),
31129
+ decision
31130
+ };
31131
+ yield {
31132
+ type: "notice",
31133
+ text: `craft ${round}/${maxRounds} \xB7 score ${critic.score} \xB7 ${tokensThisRound} tok this round \xB7 ${totalTokens(job)} total`
31134
+ };
31135
+ saveRuminationJob(job);
31136
+ if (decision === "ship") {
31137
+ stop = "ship";
31138
+ break;
31139
+ }
31140
+ if (decision === "no-delta") {
31141
+ stop = "no_delta";
31142
+ break;
31143
+ }
31144
+ const needsGround = critic.gaps.some((g) => g.code === "thin_evidence") && !job.extra_ground_used;
31145
+ if (needsGround) {
31146
+ job.extra_ground_used = true;
31147
+ yield { type: "notice", text: "Critic asked for thinner evidence \u2014 one extra ground pass." };
31148
+ for await (const event of runIteration0({ ...options, objective: armedObjective })) {
31149
+ if (event.type === "notice" && fallbackNotice(event.text)) {
31150
+ job.from_fallback = true;
31151
+ } else if (event.type === "plan") {
31152
+ job.plan = lockPlanObjective(event.plan, armedObjective);
31153
+ rememberBest(job.plan, null);
31154
+ measurable = {
31155
+ measurable_targets: event.measurable_targets,
31156
+ total_targets: event.total_targets
31157
+ };
31158
+ } else if (event.type !== "done") {
31159
+ yield event;
31160
+ }
31161
+ }
31162
+ saveRuminationJob(job);
31163
+ continue;
31164
+ }
31165
+ if (options.interrupted?.()) {
31166
+ stop = "interrupt";
31167
+ break;
31168
+ }
31169
+ if (job.max_tokens != null && totalTokens(job) >= job.max_tokens) {
31170
+ stop = "budget_tokens";
31171
+ break;
31172
+ }
31173
+ const reviseRaw = await callText(
31174
+ "strategist",
31175
+ buildReviseMessage({
31176
+ objective: armedObjective,
31177
+ planJson: JSON.stringify(plan),
31178
+ gapsJson: JSON.stringify(critic.gaps)
31179
+ }),
31180
+ REVISE_MAX_TOKENS
31181
+ );
31182
+ job.cumulative_input_tokens += reviseRaw.input;
31183
+ job.cumulative_output_tokens += reviseRaw.output;
31184
+ const parsed = parseJsonObjectFromText(reviseRaw.text);
31185
+ const validated = parsed ? validateStrategistPlan(parsed, { evidenceText, todayIso }) : null;
31186
+ if (validated) {
31187
+ job.plan = lockPlanObjective(validated.plan, armedObjective);
31188
+ job.revised_once = true;
31189
+ job.from_fallback = false;
31190
+ measurable = {
31191
+ measurable_targets: validated.measurableTargets,
31192
+ total_targets: validated.totalTargets
31193
+ };
31194
+ const nextRubric = scorePlan(job.plan);
31195
+ job.rubric_gaps = nextRubric.gaps;
31196
+ } else {
31197
+ yield { type: "notice", text: "Revise JSON invalid \u2014 keeping prior plan." };
31198
+ }
31199
+ saveRuminationJob(job);
31200
+ }
31201
+ if (!stop) stop = "budget_rounds";
31202
+ if (stop !== "ship" && job.best_plan) {
31203
+ job.plan = lockPlanObjective(job.best_plan, armedObjective);
31204
+ }
31205
+ job.stop_reason = stop;
31206
+ job.status = stop === "ship" ? "ready" : "unfinished";
31207
+ saveRuminationJob(job);
31208
+ if (job.plan) {
31209
+ const rubric = scorePlan(job.plan);
31210
+ yield {
31211
+ type: "plan",
31212
+ plan: job.plan,
31213
+ issues: rubric.blocking.map((g) => `Plan gap (${g.code}): ${g.note}`),
31214
+ measurable_targets: measurable.measurable_targets,
31215
+ total_targets: measurable.total_targets,
31216
+ baseline_batch_id: options.baselineBatchId ?? null
31217
+ };
31218
+ }
31219
+ yield {
31220
+ type: "done",
31221
+ model_used: lastMeta.model_used,
31222
+ provider_used: lastMeta.provider_used,
31223
+ failover: lastMeta.failover,
31224
+ usage: {
31225
+ ...lastMeta,
31226
+ input_tokens: job.cumulative_input_tokens,
31227
+ output_tokens: job.cumulative_output_tokens
31228
+ }
31229
+ };
31230
+ return { job, plan: job.plan, status: job.status, stop_reason: job.stop_reason };
31231
+ } catch (err) {
31232
+ job.status = "unfinished";
31233
+ job.stop_reason = "error";
31234
+ if (job.best_plan) job.plan = lockPlanObjective(job.best_plan, armedObjective);
31235
+ saveRuminationJob(job);
31236
+ throw err;
31237
+ }
31238
+ }
31239
+ function formatCraftRoundLine(event) {
31240
+ const score = event.score == null ? "\u2014" : String(event.score);
31241
+ const tok = event.tokensThisRound >= 1e3 ? `${Math.round(event.tokensThisRound / 1e3)}k` : String(event.tokensThisRound);
31242
+ const total = event.tokensTotal >= 1e3 ? `${Math.round(event.tokensTotal / 1e3)}k` : String(event.tokensTotal);
31243
+ return `craft ${event.round}/${event.max} \xB7 score ${score} \xB7 ${tok} tok this round \xB7 ${total} total`;
31244
+ }
31245
+ var CRITIC_MAX_TOKENS, REVISE_MAX_TOKENS, DEFAULT_CRAFT_MAX_ROUNDS, DEFAULT_CRAFT_MIN_SCORE;
31246
+ var init_strategist_craft = __esm({
31247
+ "src/ai/strategist-craft.ts"() {
31248
+ "use strict";
31249
+ init_failover();
31250
+ init_repl_api();
31251
+ init_context2();
31252
+ init_strategist();
31253
+ init_strategist_validate();
31254
+ init_strategist_rubric();
31255
+ init_strategist_prompt();
31256
+ init_store3();
31257
+ CRITIC_MAX_TOKENS = 2048;
31258
+ REVISE_MAX_TOKENS = 8192;
31259
+ DEFAULT_CRAFT_MAX_ROUNDS = 5;
31260
+ DEFAULT_CRAFT_MIN_SCORE = 85;
31261
+ }
31262
+ });
31263
+
31264
+ // src/services/strategist.ts
31265
+ import { createHash as createHash3 } from "crypto";
31266
+ function serializeGapAudit(audit) {
31267
+ const lines = [`Can compute: ${audit.can_compute} (lens: ${audit.primary_lens})`];
31268
+ for (const item of audit.satisfied) {
31269
+ lines.push(`- HAVE ${item.label}: ${item.detail}`);
31270
+ }
31271
+ for (const item of audit.missing) {
31272
+ lines.push(`- MISSING ${item.label}: ${item.why}`);
31273
+ }
31274
+ for (const item of audit.optional) {
31275
+ lines.push(`- LIMITED ${item.label}: ${item.detail}`);
31276
+ }
31277
+ return lines.join("\n");
31278
+ }
31279
+ async function prepareStrategistInputs(ctx, objective) {
31280
+ let snapshot = ctx.snapshot.computeResult;
31281
+ if (!snapshot) {
31282
+ snapshot = await computeFullHealth();
31283
+ ctx.snapshot.computeResult = snapshot;
31284
+ const divInput = snapshot.segments.map((s) => ({
31285
+ segmentId: s.segment.id,
31286
+ segmentName: s.segment.name,
31287
+ result: s.result
31288
+ }));
31289
+ ctx.snapshot.divergences = detectDivergences(snapshot.aggregate, divInput).divergences;
31290
+ }
31291
+ const audit = ctx.gapAudit ?? await refreshGapAudit(ctx).catch(() => null);
31292
+ const gapAuditBlock = audit ? serializeGapAudit(audit) : "";
31293
+ const memoryBlock = await Promise.resolve().then(() => (init_store2(), store_exports2)).then((m) => m.buildMemoryBlock(objective)).catch(() => "");
31294
+ let baselineBatchId = null;
31295
+ try {
31296
+ const reading = await getLatestHealthReading();
31297
+ baselineBatchId = reading?.upload_batch_id ?? null;
31298
+ } catch {
31299
+ }
31300
+ return {
31301
+ snapshot,
31302
+ divergences: ctx.snapshot.divergences,
31303
+ gapAuditBlock,
31304
+ memoryBlock,
31305
+ baselineBatchId,
31306
+ includeMetrics: true
31307
+ };
31308
+ }
31309
+ function proposeObjectiveFromSnapshot(snapshot) {
31310
+ const { aggregate } = snapshot;
31311
+ const gating = aggregate.gating_vital_sign;
31312
+ if (!gating) return null;
31313
+ const vital = aggregate.vital_signs.find((v) => v.vital_sign === gating);
31314
+ if (!vital) return null;
31315
+ const label = VITAL_SIGN_LABELS[gating] ?? gating;
31316
+ const dollar = vital.dollar_value != null && vital.dollar_value > 0 ? ` and recover the ${formatCurrency(vital.dollar_value)} ${vital.dollar_label ?? "at stake"}` : "";
31317
+ return `Move ${label} from ${Math.round(vital.score)} to 60+${dollar} within 60 days`;
31318
+ }
31319
+ function slugify2(value) {
31320
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
31321
+ return slug || `strategy-${Date.now()}`;
31322
+ }
31323
+ function outcomeToMetric(outcome) {
31324
+ return {
31325
+ name: outcome.metric,
31326
+ target: outcome.target_range,
31327
+ baseline: outcome.baseline,
31328
+ timeframe: `by ${outcome.check_date}`
31329
+ };
31330
+ }
31331
+ async function persistStrategistPlan(plan, opts = {}) {
31332
+ await initSchema();
31333
+ const slug = slugify2(plan.title);
31334
+ const libraryPath = strategyLibraryPath(slug);
31335
+ const linkedPlayIds = [...new Set(plan.workstreams.flatMap((ws) => ws.play_ids))];
31336
+ const successMetrics = plan.workstreams.map((ws) => outcomeToMetric(ws.expected_outcome));
31337
+ const leadingIndicators = plan.workstreams.flatMap((ws) => ws.leading_indicators.map(outcomeToMetric));
31338
+ const recommendedActions = plan.workstreams.flatMap((ws) => ws.actions.map((action) => `[WS${ws.order}] ${action}`)).slice(0, 15);
31339
+ const reviewProtocol = [
31340
+ `Review ${plan.review_cadence.toLowerCase()} with /strategy review ${slug}.`,
31341
+ `Check each milestone at its due date against the named verification method.`,
31342
+ `At each outcome check date, compare the measured value to its target range against baseline batch ${opts.baselineBatchId ?? "(latest)"}.`,
31343
+ `If a contingency trigger fires, activate the pre-agreed fallback.`
31344
+ ].join(" ");
31345
+ const id = await upsertStrategy({
31346
+ slug,
31347
+ title: plan.title,
31348
+ status: opts.status ?? "active",
31349
+ source_type: "agent",
31350
+ source_path: null,
31351
+ goal: plan.objective,
31352
+ hypothesis: plan.hypothesis,
31353
+ target_segment: plan.target_segment,
31354
+ priority: plan.priority,
31355
+ linked_play_ids: linkedPlayIds,
31356
+ success_metrics: successMetrics,
31357
+ leading_indicators: leadingIndicators,
31358
+ risks: plan.risks,
31359
+ recommended_actions: recommendedActions,
31360
+ experiment_design: reviewProtocol,
31361
+ review_cadence: plan.review_cadence,
31362
+ confidence: plan.confidence,
31363
+ raw_excerpt: plan.summary_30k,
31364
+ library_path: libraryPath,
31365
+ origin: "strategist",
31366
+ objective: plan.objective,
31367
+ constraints: plan.constraints,
31368
+ workstreams: plan.workstreams,
31369
+ assumptions: plan.assumptions,
31370
+ baseline_batch_id: opts.baselineBatchId ?? null
31371
+ });
31372
+ const strategy = await getStrategyBySlugOrId(id);
31373
+ if (!strategy) {
31374
+ throw new NtrpError("strategy_persist_failed", "Strategy was not found after saving.", 1 /* RuntimeError */);
31375
+ }
31376
+ const extras = {
31377
+ craftLogPath: opts.craftLogPath,
31378
+ constraintLine: opts.constraintLine,
31379
+ killedAlternative: opts.killedAlternative,
31380
+ outOfScope: opts.outOfScope
31381
+ };
31382
+ const writtenPath = writeStrategyMarkdown(strategy, extras);
31383
+ await insertStrategySource({
31384
+ strategy_id: strategy.id,
31385
+ source_type: "agent",
31386
+ source_path: null,
31387
+ content_hash: createHash3("sha256").update(JSON.stringify(plan)).digest("hex"),
31388
+ extracted_text_excerpt: plan.summary_30k.slice(0, 800),
31389
+ metadata: {
31390
+ origin: "strategist",
31391
+ objective: plan.objective,
31392
+ workstream_count: plan.workstreams.length,
31393
+ baseline_batch_id: opts.baselineBatchId ?? null
31394
+ }
31395
+ });
31396
+ return { strategy: { ...strategy, library_path: writtenPath }, library_path: writtenPath };
31397
+ }
31398
+ var init_strategist2 = __esm({
31399
+ "src/services/strategist.ts"() {
31400
+ "use strict";
31401
+ init_schema();
31402
+ init_queries();
31403
+ init_health_score();
31404
+ init_divergence();
31405
+ init_gap_audit();
31406
+ init_library();
31407
+ init_errors2();
31408
+ init_types2();
31409
+ init_formatters();
31410
+ }
31411
+ });
31412
+
31413
+ // src/ruminations/handoff.ts
31414
+ function renderCraftHandoffMarkdown(opts) {
31415
+ const { plan } = opts;
31416
+ const lines = [];
31417
+ lines.push("# Action plan \u2014 NTRP craft");
31418
+ lines.push("");
31419
+ lines.push("Use this file as a prompt for another agent or as a morning brief.");
31420
+ lines.push("");
31421
+ lines.push("## The Call");
31422
+ lines.push("");
31423
+ lines.push(plan.summary_30k);
31424
+ lines.push("");
31425
+ lines.push("## Locked objective");
31426
+ lines.push("");
31427
+ lines.push(plan.objective);
31428
+ lines.push("");
31429
+ lines.push(renderConstraintHeading(opts.constraintLine).trimEnd());
31430
+ lines.push("");
31431
+ lines.push(renderScopeHeading(plan.constraints, opts.outOfScope).trimEnd());
31432
+ lines.push("");
31433
+ lines.push("## Hypothesis");
31434
+ lines.push("");
31435
+ lines.push(plan.hypothesis);
31436
+ lines.push("");
31437
+ lines.push(renderKilledAlternativeLine(opts.killedAlternative));
31438
+ lines.push("");
31439
+ lines.push(renderEffortHeading(plan.workstreams).trimEnd());
31440
+ lines.push("");
31441
+ lines.push("## Workstreams");
31442
+ lines.push("");
31443
+ for (const ws of plan.workstreams) {
31444
+ lines.push(`### ${ws.order}. ${ws.title}`);
31445
+ lines.push("");
31446
+ lines.push(`- Problem: ${ws.problem}`);
31447
+ lines.push(`- Why this order: ${ws.rationale}`);
31448
+ if (ws.actions[0]) lines.push(`- First action (48h): ${ws.actions[0]}`);
31449
+ lines.push(`- Effort: ~${Math.round(ws.effort_hours)} team-hours`);
31450
+ lines.push(
31451
+ `- 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})`
31452
+ );
31453
+ lines.push(
31454
+ `- Contingency: if ${ws.contingency.trigger} (check ${ws.contingency.trigger_check_date}) -> ${ws.contingency.fallback}`
31455
+ );
31456
+ lines.push("");
31457
+ }
31458
+ if (plan.risks.length > 0) {
31459
+ lines.push("## Risks");
31460
+ lines.push("");
31461
+ for (const r of plan.risks) lines.push(`- ${r}`);
31462
+ lines.push("");
31463
+ }
31464
+ if (plan.assumptions.length > 0) {
31465
+ lines.push("## Assumptions");
31466
+ lines.push("");
31467
+ for (const a of plan.assumptions) lines.push(`- ${a}`);
31468
+ lines.push("");
31469
+ }
31470
+ lines.push(renderReviewHeading({ cadence: plan.review_cadence, slug: opts.slug }).trimEnd());
31471
+ lines.push("");
31472
+ if (opts.journalPath) {
31473
+ lines.push("## Craft log");
31474
+ lines.push("");
31475
+ lines.push(`Status: ${opts.status}`);
31476
+ lines.push(`Log: ${opts.journalPath}`);
31477
+ if (opts.libraryPath) lines.push(`Strategy library: ${opts.libraryPath}`);
31478
+ lines.push("");
31479
+ } else if (opts.libraryPath) {
31480
+ lines.push("## Strategy library");
31481
+ lines.push("");
31482
+ lines.push(opts.libraryPath);
31483
+ lines.push("");
31484
+ }
31485
+ return lines.join("\n");
31486
+ }
31487
+ function writeCraftPlanHandoff(opts) {
31488
+ const journalPath = opts.jobId ? ruminationLogPath(opts.jobId) : void 0;
31489
+ const markdown = renderCraftHandoffMarkdown({
31490
+ plan: opts.plan,
31491
+ journalPath,
31492
+ libraryPath: opts.libraryPath,
31493
+ status: opts.status,
31494
+ constraintLine: opts.constraintLine,
31495
+ killedAlternative: opts.killedAlternative,
31496
+ outOfScope: opts.outOfScope,
31497
+ slug: opts.slug
31498
+ });
31499
+ const path = resolveArchivePath("prompt:plan", `handoff-plan-${exportStamp()}.md`);
31500
+ writeRedactedText(path, markdown);
31501
+ const event = recordExportWrite({
31502
+ kind: "prompt:plan",
31503
+ path,
31504
+ sessionId: opts.sessionId,
31505
+ title: `${opts.plan.title} craft plan`
31506
+ });
31507
+ return { path, inboxPath: event.inbox_path };
31508
+ }
31509
+ var init_handoff2 = __esm({
31510
+ "src/ruminations/handoff.ts"() {
31511
+ "use strict";
31512
+ init_exports_registry();
31513
+ init_redact_write();
31514
+ init_library();
31515
+ init_store3();
31516
+ }
31517
+ });
31518
+
31519
+ // src/services/strategist-run.ts
31520
+ async function assertComputableData() {
31521
+ await initSchema();
31522
+ let counts = {};
31523
+ try {
31524
+ counts = await getEntityCounts();
31525
+ } catch {
31526
+ counts = {};
31527
+ }
31528
+ if (!Object.values(counts).some((n) => (n ?? 0) > 0)) {
31529
+ throw new NtrpError(
31530
+ "strategy_no_data",
31531
+ "No pipeline data loaded. Ingest a CSV, run demo data, then craft a plan.",
31532
+ 4 /* NoData */
31533
+ );
31534
+ }
31535
+ }
31536
+ function rememberCraftJob(ctx, job) {
31537
+ if (!job) return;
31538
+ ctx.lastCraftJobId = job.status === "ready" ? void 0 : job.id;
31539
+ saveSessionState(ctx);
31540
+ }
31541
+ async function executeStrategistJob(req) {
31542
+ if (!canUseReplAi(req.ctx)) {
31543
+ throw new NtrpError(
31544
+ "strategy_no_key",
31545
+ "No LLM API key configured. Run /connect and paste a key, then retry craft.",
31546
+ 3 /* Auth */
31547
+ );
31548
+ }
31549
+ const resumeJob = req.resumeId ? loadRuminationJob(req.resumeId) : null;
31550
+ if (req.resumeId && !resumeJob) {
31551
+ throw new NtrpError(
31552
+ "strategy_resume_missing",
31553
+ `No craft job "${req.resumeId}". Check ~/.ntrp/ruminations/.`,
31554
+ 2 /* Usage */
31555
+ );
31556
+ }
31557
+ if (resumeJob) rememberCraftJob(req.ctx, resumeJob);
31558
+ const objective = resumeJob?.objective || req.objective;
31559
+ if (!objective.trim()) {
31560
+ throw new NtrpError(
31561
+ "strategy_objective_required",
31562
+ 'strategy craft requires an objective. Example: ntrp strategy craft "cut stale pipeline before Q4"',
31563
+ 2 /* Usage */
31564
+ );
31565
+ }
31566
+ await assertComputableData();
31567
+ const inputs = await prepareStrategistInputs(req.ctx, objective);
31568
+ const notices = [];
31569
+ let plan = resumeJob?.plan ?? null;
31570
+ let stats = { measurable_targets: 0, total_targets: 0 };
31571
+ let meta = {};
31572
+ let job = resumeJob ?? void 0;
31573
+ const sessionOpts = {
31574
+ objective,
31575
+ computeResult: inputs.snapshot,
31576
+ divergences: inputs.divergences,
31577
+ includeMetrics: inputs.includeMetrics,
31578
+ memoryBlock: inputs.memoryBlock,
31579
+ gapAuditBlock: inputs.gapAuditBlock,
31580
+ constraintsNote: req.constraintsNote ?? resumeJob?.constraints_note,
31581
+ baselineBatchId: inputs.baselineBatchId,
31582
+ ctx: req.ctx
31583
+ };
31584
+ const useCraft = req.mode === "craft" && req.untilReady !== false;
31585
+ const handleEvent = (event) => {
31586
+ req.onEvent?.(event);
31587
+ if (event.type === "notice") notices.push(event.text);
31588
+ if (event.type === "plan") {
31589
+ plan = event.plan;
31590
+ stats = {
31591
+ measurable_targets: event.measurable_targets,
31592
+ total_targets: event.total_targets
31593
+ };
31594
+ }
31595
+ if (event.type === "done") {
31596
+ meta = event.usage ?? { model_used: event.model_used, provider_used: event.provider_used };
31597
+ }
31598
+ if (event.type === "craft_round") {
31599
+ notices.push(formatCraftRoundLine(event));
31600
+ }
31601
+ };
31602
+ if (useCraft) {
31603
+ const gen = strategistCraftSession({
31604
+ ...sessionOpts,
31605
+ maxRounds: req.maxRounds ?? resumeJob?.max_rounds ?? DEFAULT_CRAFT_MAX_ROUNDS,
31606
+ maxTokens: req.maxTokens ?? resumeJob?.max_tokens,
31607
+ minScore: req.minScore ?? resumeJob?.min_score ?? DEFAULT_CRAFT_MIN_SCORE,
31608
+ job: resumeJob ?? void 0,
31609
+ interrupted: req.interrupted
31610
+ });
31611
+ let next = await gen.next();
31612
+ while (!next.done) {
31613
+ handleEvent(next.value);
31614
+ next = await gen.next();
31615
+ }
31616
+ job = next.value.job;
31617
+ plan = next.value.plan;
31618
+ } else {
31619
+ for await (const event of strategistPlanSession(sessionOpts)) {
31620
+ handleEvent(event);
31621
+ }
31622
+ }
31623
+ const blocking = job?.rubric_gaps.filter((g) => g.severity === "blocking").map((g) => g.code) ?? [];
31624
+ const status = job?.status === "unfinished" || !plan ? "unfinished" : "ready";
31625
+ const result = {
31626
+ plan,
31627
+ status,
31628
+ stop_reason: job?.stop_reason,
31629
+ objective,
31630
+ critic_score: job?.last_critic?.score ?? null,
31631
+ rounds: job?.iterations.length ?? 0,
31632
+ usage: meta,
31633
+ journal_path: job ? ruminationLogPath(job.id) : void 0,
31634
+ blocking_gaps: blocking,
31635
+ measurable_targets: stats.measurable_targets,
31636
+ total_targets: stats.total_targets,
31637
+ notices,
31638
+ job
31639
+ };
31640
+ const constraintLine = formatConstraintLine(inputs.snapshot.aggregate);
31641
+ const killedAlternative = job?.last_critic?.killed_alternative;
31642
+ if (plan && req.save) {
31643
+ const persisted = await persistStrategistPlan(plan, {
31644
+ baselineBatchId: inputs.baselineBatchId,
31645
+ craftLogPath: job ? ruminationLogPath(job.id) : void 0,
31646
+ constraintLine,
31647
+ killedAlternative
31648
+ });
31649
+ result.slug = persisted.strategy.slug;
31650
+ result.library_path = persisted.library_path;
31651
+ req.ctx.deliverables.push({
31652
+ kind: "strategy",
31653
+ at: (/* @__PURE__ */ new Date()).toISOString(),
31654
+ path: persisted.library_path,
31655
+ note: plan.title
31656
+ });
31657
+ if (job) {
31658
+ job.library_path = persisted.library_path;
31659
+ }
31660
+ }
31661
+ if (plan && req.handoff) {
31662
+ const written = writeCraftPlanHandoff({
31663
+ plan,
31664
+ jobId: job?.id,
31665
+ status: result.status,
31666
+ libraryPath: result.library_path,
31667
+ sessionId: req.ctx.sessionId,
31668
+ constraintLine,
31669
+ killedAlternative,
31670
+ slug: result.slug
31671
+ });
31672
+ result.handoff_path = written.path;
31673
+ result.inbox_path = written.inboxPath;
31674
+ if (job) {
31675
+ job.handoff_path = written.path;
31676
+ job.inbox_path = written.inboxPath;
31677
+ }
31678
+ req.ctx.deliverables.push({
31679
+ kind: "prompt:plan",
31680
+ at: (/* @__PURE__ */ new Date()).toISOString(),
31681
+ path: written.path,
31682
+ note: plan.title
31683
+ });
31684
+ }
31685
+ if (job) {
31686
+ if (req.ctx.strategistState) req.ctx.strategistState.ruminationId = job.id;
31687
+ saveRuminationJob(job);
31688
+ rememberCraftJob(req.ctx, job);
31689
+ }
31690
+ return result;
31691
+ }
31692
+ var init_strategist_run = __esm({
31693
+ "src/services/strategist-run.ts"() {
31694
+ "use strict";
31695
+ init_context2();
31696
+ init_errors2();
31697
+ init_types2();
31698
+ init_repl_api();
31699
+ init_strategist();
31700
+ init_strategist_craft();
31701
+ init_formatters();
31702
+ init_strategist2();
31703
+ init_queries();
31704
+ init_schema();
31705
+ init_store3();
31706
+ init_handoff2();
31707
+ }
31708
+ });
31709
+
30244
31710
  // src/output/strategy-brief.ts
30245
31711
  import chalk36 from "chalk";
30246
31712
  function printWrapped(text, width, prefix = INDENT, style) {
@@ -30332,6 +31798,40 @@ function printStrategyBrief(plan, stats) {
30332
31798
  console.log(`${INDENT}${coverageStyled}${chalk36.dim(` \xB7 ~${Math.round(totalHours)} total team hours across ${plan.workstreams.length} workstream${plan.workstreams.length === 1 ? "" : "s"}`)}`);
30333
31799
  console.log();
30334
31800
  }
31801
+ function printCraftWrapUp(opts) {
31802
+ console.log();
31803
+ if (opts.status === "ready") {
31804
+ console.log(INDENT + paint("accent", "Plan ready"));
31805
+ if (opts.library_path) console.log(INDENT + chalk36.dim(opts.library_path));
31806
+ if (opts.slug) {
31807
+ console.log(
31808
+ INDENT + chalk36.dim("Type ") + paint("accent", `/strategy review ${opts.slug}`) + chalk36.dim(" to check progress.")
31809
+ );
31810
+ }
31811
+ const handoff = opts.inbox_path ?? opts.handoff_path;
31812
+ if (handoff) console.log(INDENT + chalk36.dim(handoff));
31813
+ } else {
31814
+ console.log(INDENT + paint("accent", "Best so far."));
31815
+ const path = opts.library_path ?? opts.handoff_path ?? opts.inbox_path;
31816
+ if (path) console.log(INDENT + chalk36.dim(path));
31817
+ if (opts.oneShot && opts.jobId) {
31818
+ console.log(
31819
+ INDENT + chalk36.dim("Resume: ") + paint("accent", `ntrp strategy craft --resume ${opts.jobId}`)
31820
+ );
31821
+ } else {
31822
+ console.log(
31823
+ INDENT + chalk36.dim("Type ") + chalk36.cyan("keep going") + chalk36.dim(" to continue.")
31824
+ );
31825
+ }
31826
+ }
31827
+ if (opts.oneShot && opts.journal_path) {
31828
+ console.log(INDENT + chalk36.dim(`Craft log: ${opts.journal_path}`));
31829
+ }
31830
+ console.log();
31831
+ }
31832
+ function isCraftRoundNotice(text) {
31833
+ return /^craft \d+\/\d+/.test(text);
31834
+ }
30335
31835
  var INDENT;
30336
31836
  var init_strategy_brief = __esm({
30337
31837
  "src/output/strategy-brief.ts"() {
@@ -30346,7 +31846,9 @@ var init_strategy_brief = __esm({
30346
31846
  var strategist_flow_exports = {};
30347
31847
  __export(strategist_flow_exports, {
30348
31848
  extractObjectiveSeed: () => extractObjectiveSeed,
31849
+ handleKeepGoingLine: () => handleKeepGoingLine,
30349
31850
  handleStrategizeFlow: () => handleStrategizeFlow,
31851
+ isKeepGoingIntent: () => isKeepGoingIntent,
30350
31852
  isStrategistIntent: () => isStrategistIntent,
30351
31853
  promptQueuedAiStrategist: () => promptQueuedAiStrategist,
30352
31854
  queueStrategistForAnalysis: () => queueStrategistForAnalysis,
@@ -30369,7 +31871,8 @@ function queueStrategistForAnalysis(ctx, opts) {
30369
31871
  ctx.strategistState = {
30370
31872
  step: "awaiting_analysis",
30371
31873
  objective: opts.seed,
30372
- origin: opts.origin
31874
+ origin: opts.origin,
31875
+ mode: opts.mode
30373
31876
  };
30374
31877
  saveSessionState(ctx);
30375
31878
  console.log();
@@ -30394,7 +31897,7 @@ async function startStrategistFlow(ctx, opts) {
30394
31897
  objective = (snapshot ? proposeObjectiveFromSnapshot(snapshot) : null) ?? "";
30395
31898
  }
30396
31899
  if (!objective) {
30397
- ctx.strategistState = { step: "objective_input", origin: opts.origin };
31900
+ ctx.strategistState = { step: "objective_input", origin: opts.origin, mode: opts.mode };
30398
31901
  saveSessionState(ctx);
30399
31902
  console.log();
30400
31903
  console.log(" " + chalk37.dim('What is the objective? State a finish line. Example: "cut stale pipeline in half before Q4".'));
@@ -30402,7 +31905,7 @@ async function startStrategistFlow(ctx, opts) {
30402
31905
  recordMessage(ctx, "agent", "Strategist: asked for objective");
30403
31906
  return "Awaiting objective";
30404
31907
  }
30405
- ctx.strategistState = { step: "objective_confirm", objective, origin: opts.origin };
31908
+ ctx.strategistState = { step: "objective_confirm", objective, origin: opts.origin, mode: opts.mode };
30406
31909
  saveSessionState(ctx);
30407
31910
  printObjectiveCard(ctx, objective, !opts.seed);
30408
31911
  recordMessage(ctx, "agent", `Strategist objective proposed: ${objective}`);
@@ -30415,7 +31918,8 @@ async function resumeStrategistAfterCompute(ctx) {
30415
31918
  console.log(" " + paint("accent", "Analysis is ready. The strategy session continues."));
30416
31919
  await startStrategistFlow(ctx, {
30417
31920
  seed: state2.objective,
30418
- origin: state2.origin ?? "nl"
31921
+ origin: state2.origin ?? "nl",
31922
+ mode: state2.mode
30419
31923
  });
30420
31924
  }
30421
31925
  function promptQueuedAiStrategist(ctx) {
@@ -30425,6 +31929,64 @@ function promptQueuedAiStrategist(ctx) {
30425
31929
  }
30426
31930
  printObjectiveCard(ctx, state2.objective, true);
30427
31931
  }
31932
+ function isKeepGoingIntent(input) {
31933
+ return CRAFT_RE.test(input.trim());
31934
+ }
31935
+ async function handleKeepGoingLine(ctx, input) {
31936
+ const line = input.trim();
31937
+ recordMessage(ctx, "user", line);
31938
+ const job = unfinishedCraftJob(ctx.lastCraftJobId);
31939
+ if (!job) {
31940
+ console.log();
31941
+ console.log(
31942
+ " " + chalk37.dim("No plan in progress. Type ") + chalk37.cyan("how should we fix this?") + chalk37.dim(" to start one.")
31943
+ );
31944
+ console.log();
31945
+ recordMessage(ctx, "agent", "No plan in progress");
31946
+ return "No plan in progress";
31947
+ }
31948
+ if (!canUseReplAi(ctx)) {
31949
+ console.log();
31950
+ console.log(
31951
+ " " + chalk37.dim("Type ") + chalk37.cyan("/connect") + chalk37.dim(" to connect a key. Then type keep going.")
31952
+ );
31953
+ console.log();
31954
+ recordMessage(ctx, "agent", "Keep going needs a key");
31955
+ return "Keep going needs a key";
31956
+ }
31957
+ const spinner = makeSpinner("Working the plan\u2026");
31958
+ try {
31959
+ const result = await executeStrategistJob({
31960
+ ctx,
31961
+ objective: job.objective,
31962
+ constraintsNote: job.constraints_note,
31963
+ mode: "craft",
31964
+ untilReady: true,
31965
+ save: true,
31966
+ handoff: true,
31967
+ resumeId: job.id,
31968
+ onEvent: (event) => {
31969
+ if (event.type === "thinking") {
31970
+ spinner.stop();
31971
+ console.log(" " + chalk37.dim.italic(event.text));
31972
+ spinner.start();
31973
+ spinner.text = "Working the plan\u2026";
31974
+ }
31975
+ }
31976
+ });
31977
+ spinner.stop();
31978
+ printCraftReplResult(ctx, result, job.objective);
31979
+ ctx.strategistState = void 0;
31980
+ saveSessionState(ctx);
31981
+ return result.status === "ready" ? `Strategy crafted: ${result.plan?.title}` : "Strategy craft unfinished";
31982
+ } catch (err) {
31983
+ spinner.fail("Strategy craft failed");
31984
+ console.error(" " + chalk37.red(String(err.message ?? err)));
31985
+ ctx.strategistState = void 0;
31986
+ saveSessionState(ctx);
31987
+ return;
31988
+ }
31989
+ }
30428
31990
  async function handleStrategizeFlow(input, ctx) {
30429
31991
  const state2 = ctx.strategistState;
30430
31992
  if (!state2) return;
@@ -30451,6 +32013,11 @@ async function handleStrategizeFlow(input, ctx) {
30451
32013
  printObjectiveCard(ctx, state2.objective, false);
30452
32014
  return "Objective proposed";
30453
32015
  }
32016
+ if (CRAFT_RE.test(line)) {
32017
+ state2.mode = "craft";
32018
+ saveSessionState(ctx);
32019
+ return runStrategistSession(ctx);
32020
+ }
30454
32021
  if (CONFIRM_RE.test(line)) {
30455
32022
  return runStrategistSession(ctx);
30456
32023
  }
@@ -30501,7 +32068,7 @@ async function runStrategistSession(ctx) {
30501
32068
  saveSessionState(ctx);
30502
32069
  return "Skeleton plan (awaiting connect)";
30503
32070
  }
30504
- if (ctx.rl && !state2.constraintsNote) {
32071
+ if (ctx.rl && !state2.constraintsNote && state2.mode !== "craft") {
30505
32072
  const prompts = createPromptSession(ctx.rl, ctx);
30506
32073
  try {
30507
32074
  const note = await prompts.ask(
@@ -30515,7 +32082,39 @@ async function runStrategistSession(ctx) {
30515
32082
  }
30516
32083
  }
30517
32084
  console.log();
30518
- const spinner = makeSpinner("Reading live data\u2026");
32085
+ const spinner = makeSpinner(state2.mode === "craft" ? "Working the plan\u2026" : "Reading live data\u2026");
32086
+ if (state2.mode === "craft") {
32087
+ try {
32088
+ const result = await executeStrategistJob({
32089
+ ctx,
32090
+ objective,
32091
+ constraintsNote: state2.constraintsNote,
32092
+ mode: "craft",
32093
+ untilReady: true,
32094
+ save: true,
32095
+ handoff: true,
32096
+ onEvent: (event) => {
32097
+ if (event.type === "thinking") {
32098
+ spinner.stop();
32099
+ console.log(" " + chalk37.dim.italic(event.text));
32100
+ spinner.start();
32101
+ spinner.text = "Working the plan\u2026";
32102
+ }
32103
+ }
32104
+ });
32105
+ spinner.stop();
32106
+ printCraftReplResult(ctx, result, objective);
32107
+ ctx.strategistState = void 0;
32108
+ saveSessionState(ctx);
32109
+ return result.status === "ready" ? `Strategy crafted: ${result.plan?.title}` : "Strategy craft unfinished";
32110
+ } catch (err) {
32111
+ spinner.fail("Strategy craft failed");
32112
+ console.error(" " + chalk37.red(String(err.message ?? err)));
32113
+ ctx.strategistState = void 0;
32114
+ saveSessionState(ctx);
32115
+ return;
32116
+ }
32117
+ }
30519
32118
  let plan = null;
30520
32119
  let stats = { measurable_targets: 0, total_targets: 0 };
30521
32120
  let baselineBatchId = null;
@@ -30599,7 +32198,10 @@ async function runStrategistSession(ctx) {
30599
32198
  }
30600
32199
  if (saved) {
30601
32200
  try {
30602
- const persisted = await persistStrategistPlan(plan, { baselineBatchId });
32201
+ const persisted = await persistStrategistPlan(plan, {
32202
+ baselineBatchId,
32203
+ constraintLine: formatConstraintLine(ctx.snapshot.computeResult?.aggregate)
32204
+ });
30603
32205
  ctx.deliverables.push({
30604
32206
  kind: "strategy",
30605
32207
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -30647,19 +32249,54 @@ async function ensureSnapshot(ctx) {
30647
32249
  return null;
30648
32250
  }
30649
32251
  }
32252
+ function printCraftReplResult(ctx, result, objective) {
32253
+ if (result.plan) {
32254
+ printStrategyBrief(result.plan, {
32255
+ measurable_targets: result.measurable_targets,
32256
+ total_targets: result.total_targets
32257
+ });
32258
+ } else {
32259
+ console.log(" " + chalk37.dim("(No plan produced)"));
32260
+ }
32261
+ for (const notice of result.notices.slice(0, 8)) {
32262
+ if (!isCraftRoundNotice(notice)) console.log(" " + chalk37.dim(notice));
32263
+ }
32264
+ printLlmAttribution(result.usage);
32265
+ if (result.library_path) creditStrategySession(ctx);
32266
+ printCraftWrapUp({
32267
+ status: result.status,
32268
+ library_path: result.library_path,
32269
+ inbox_path: result.inbox_path,
32270
+ handoff_path: result.handoff_path,
32271
+ slug: result.slug,
32272
+ jobId: result.job?.id,
32273
+ oneShot: false
32274
+ });
32275
+ if (result.library_path || result.handoff_path) {
32276
+ recordMessage(ctx, "agent", `Strategy crafted: ${result.plan?.title ?? objective} (${result.status})`);
32277
+ }
32278
+ }
30650
32279
  function printObjectiveCard(ctx, objective, proposed) {
32280
+ const craftMode = ctx.strategistState?.mode === "craft";
30651
32281
  console.log();
30652
32282
  console.log(" " + chalk37.bold("Strategy session"));
30653
32283
  console.log(
30654
32284
  " " + chalk37.dim(proposed ? "Proposed objective: " : "Objective: ") + paint("accent", objective)
30655
32285
  );
30656
32286
  console.log(
30657
- " " + chalk37.dim("NTRP uses your live data. It sequences the work and sets measured milestones. It then stress-tests the plan.")
32287
+ " " + chalk37.dim(
32288
+ 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."
32289
+ )
30658
32290
  );
30659
32291
  console.log();
30660
32292
  console.log(
30661
32293
  " " + chalk37.dim("Confirm? ") + chalk37.cyan("\u23CE yes") + chalk37.dim(" \xB7 ") + chalk37.cyan("b back") + chalk37.dim(" \xB7 ") + chalk37.cyan("adjust") + chalk37.dim(" \xB7 ") + chalk37.cyan("cancel")
30662
32294
  );
32295
+ if (!craftMode) {
32296
+ console.log(
32297
+ " " + chalk37.dim("or type ") + chalk37.cyan("keep going") + chalk37.dim(" to keep working until the plan is ready")
32298
+ );
32299
+ }
30663
32300
  console.log();
30664
32301
  }
30665
32302
  async function printKeylessSkeletonPlan(ctx, objective) {
@@ -30721,7 +32358,7 @@ async function resumeStrategistAfterConnect(ctx) {
30721
32358
  printObjectiveCard(ctx, state2.objective, true);
30722
32359
  return true;
30723
32360
  }
30724
- var STRATEGIST_INTENT_RE, CANCEL_RE, CONFIRM_RE, ADJUST_RE, QUESTION_RE;
32361
+ var STRATEGIST_INTENT_RE, CANCEL_RE, CONFIRM_RE, ADJUST_RE, QUESTION_RE, CRAFT_RE;
30725
32362
  var init_strategist_flow = __esm({
30726
32363
  "src/conversation/strategist-flow.ts"() {
30727
32364
  "use strict";
@@ -30733,17 +32370,20 @@ var init_strategist_flow = __esm({
30733
32370
  init_prompts();
30734
32371
  init_health_score();
30735
32372
  init_divergence();
30736
- init_strategist();
30737
32373
  init_strategist2();
32374
+ init_strategist();
32375
+ init_strategist_run();
30738
32376
  init_strategy_brief();
30739
32377
  init_llm_attribution();
30740
32378
  init_time_bank();
30741
32379
  init_formatters();
32380
+ init_store3();
30742
32381
  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;
30743
32382
  CANCEL_RE = /^(cancel|stop|quit|abort|never\s?mind|nevermind|forget it)\s*[.!]?\s*$/i;
30744
32383
  CONFIRM_RE = /^(y|yes|yep|yeah|confirm|go|go ahead|do it|proceed|sounds good|looks good|lgtm|ok|okay)\b/i;
30745
32384
  ADJUST_RE = /^(n|no|adjust|change|edit|different|not quite|refine)\b/i;
30746
32385
  QUESTION_RE = /(\?\s*$)|^(what|why|how|when|which|where|who)\b/i;
32386
+ CRAFT_RE = /^(craft|keep going|until tight|keep working)\b/i;
30747
32387
  }
30748
32388
  });
30749
32389
 
@@ -30756,8 +32396,8 @@ __export(strategy_review_exports, {
30756
32396
  persistStrategyReview: () => persistStrategyReview,
30757
32397
  resolveReviewableStrategy: () => resolveReviewableStrategy
30758
32398
  });
30759
- import { writeFileSync as writeFileSync18 } from "fs";
30760
- import { join as join28 } from "path";
32399
+ import { writeFileSync as writeFileSync20 } from "fs";
32400
+ import { join as join31 } from "path";
30761
32401
  function groupByBatch(vitals, metrics) {
30762
32402
  const map = /* @__PURE__ */ new Map();
30763
32403
  const order = [];
@@ -31047,7 +32687,7 @@ async function persistStrategyReview(report, milestoneVerdicts, notes) {
31047
32687
  }
31048
32688
  function logWin(strategy, hits, milestoneWins) {
31049
32689
  const date = isoToday();
31050
- const path = join28(getWinsDir(), `${strategy.slug}-${date}.md`);
32690
+ const path = join31(getWinsDir(), `${strategy.slug}-${date}.md`);
31051
32691
  const lines = [
31052
32692
  `# Win \u2014 ${strategy.title}`,
31053
32693
  "",
@@ -31064,7 +32704,7 @@ function logWin(strategy, hits, milestoneWins) {
31064
32704
  for (const label of milestoneWins) lines.push(`- ${label}`);
31065
32705
  }
31066
32706
  lines.push("", "_Logged by /strategy review._", "");
31067
- writeFileSync18(path, lines.join("\n"));
32707
+ writeFileSync20(path, lines.join("\n"));
31068
32708
  return path;
31069
32709
  }
31070
32710
  var VITAL_TOKENS, COMPONENT_TOKENS;
@@ -31296,9 +32936,15 @@ __export(strategy_exports2, {
31296
32936
  });
31297
32937
  import chalk39 from "chalk";
31298
32938
  async function handler16(args, ctx) {
31299
- const { positional, flags } = parseArgs2(args, ["no-ai"]);
32939
+ const { positional, flags } = parseArgs2(args, ["no-ai", "save", "handoff"]);
31300
32940
  const first = positional[0];
31301
32941
  const interactive = !ctx.oneShot && !isStructuredOutput(ctx.execution);
32942
+ if (interactive && first === "craft") {
32943
+ const seed = positional.slice(1).join(" ").trim() || void 0;
32944
+ const { startStrategistFlow: startStrategistFlow2 } = await Promise.resolve().then(() => (init_strategist_flow(), strategist_flow_exports));
32945
+ await startStrategistFlow2(ctx, { seed, origin: "command", mode: "craft" });
32946
+ return;
32947
+ }
31302
32948
  if (interactive && (!first || !RESERVED_SUBCOMMANDS.has(first))) {
31303
32949
  const seed = positional.join(" ").trim() || void 0;
31304
32950
  const { startStrategistFlow: startStrategistFlow2 } = await Promise.resolve().then(() => (init_strategist_flow(), strategist_flow_exports));
@@ -31309,14 +32955,18 @@ async function handler16(args, ctx) {
31309
32955
  try {
31310
32956
  const result = await runStrategy(sub, positional, flags, ctx);
31311
32957
  if (isStructuredOutput(ctx.execution)) {
32958
+ if (result.action === "craft") {
32959
+ emitResult("strategy.craft", craftJsonData(result), void 0, result.status === "unfinished" ? "unfinished" : "ok");
32960
+ return;
32961
+ }
31312
32962
  emitResult("strategy", result);
31313
32963
  return;
31314
32964
  }
31315
32965
  renderStrategyResult(result);
31316
32966
  } catch (err) {
31317
- if (isStructuredOutput(ctx.execution)) emitError("strategy", err);
32967
+ if (isStructuredOutput(ctx.execution)) emitError(first === "craft" ? "strategy.craft" : "strategy", err);
31318
32968
  console.error(chalk39.red(` ${err instanceof Error ? err.message : String(err)}`));
31319
- process.exit(1);
32969
+ process.exit(err instanceof NtrpError ? err.exitCode : 1);
31320
32970
  }
31321
32971
  }
31322
32972
  async function runStrategy(sub, positional, flags, ctx) {
@@ -31391,6 +33041,8 @@ async function runStrategy(sub, positional, flags, ctx) {
31391
33041
  const { runStrategyReview: runStrategyReview2 } = await Promise.resolve().then(() => (init_strategy_review_cmd(), strategy_review_cmd_exports));
31392
33042
  return { action: "review", ...await runStrategyReview2(ctx, positional[1]) };
31393
33043
  }
33044
+ case "craft":
33045
+ return runCraft(positional, flags, ctx);
31394
33046
  default:
31395
33047
  throw new NtrpError("unknown_strategy_subcommand", `Unknown strategy subcommand: ${sub}`, 2 /* Usage */);
31396
33048
  }
@@ -31398,8 +33050,79 @@ async function runStrategy(sub, positional, flags, ctx) {
31398
33050
  function shouldSpin(ctx) {
31399
33051
  return !isStructuredOutput(ctx.execution) && ctx.execution.progress;
31400
33052
  }
33053
+ async function runCraft(positional, flags, ctx) {
33054
+ if (getFalse(flags, "ai")) {
33055
+ throw new NtrpError(
33056
+ "strategy_craft_requires_ai",
33057
+ "strategy craft needs an LLM key. Drop --no-ai, run /connect, then retry.",
33058
+ 3 /* Auth */
33059
+ );
33060
+ }
33061
+ const resumeId = getString(flags, "resume");
33062
+ const objective = positional.slice(1).join(" ").trim();
33063
+ const untilReady = !getFalse(flags, "until");
33064
+ const save = !getFalse(flags, "save");
33065
+ const handoff = !getFalse(flags, "handoff");
33066
+ let interrupted = false;
33067
+ const onInt = () => {
33068
+ interrupted = true;
33069
+ };
33070
+ process.on("SIGINT", onInt);
33071
+ const spinner = shouldSpin(ctx) ? makeSpinner("Working the plan\u2026") : null;
33072
+ try {
33073
+ const job = await executeStrategistJob({
33074
+ ctx,
33075
+ objective,
33076
+ mode: "craft",
33077
+ untilReady,
33078
+ save,
33079
+ handoff,
33080
+ maxRounds: getNumber(flags, "max-rounds"),
33081
+ maxTokens: getNumber(flags, "max-tokens"),
33082
+ minScore: getNumber(flags, "min-score"),
33083
+ resumeId,
33084
+ interrupted: () => interrupted,
33085
+ onEvent: (event) => {
33086
+ if (!spinner) return;
33087
+ if (event.type === "thinking") {
33088
+ spinner.stop();
33089
+ console.log(" " + chalk39.dim.italic(event.text));
33090
+ spinner.start();
33091
+ spinner.text = "Working the plan\u2026";
33092
+ }
33093
+ }
33094
+ });
33095
+ spinner?.stop();
33096
+ return { action: "craft", ...job };
33097
+ } catch (err) {
33098
+ spinner?.fail("Strategy craft failed");
33099
+ throw err;
33100
+ } finally {
33101
+ process.off("SIGINT", onInt);
33102
+ }
33103
+ }
33104
+ function craftJsonData(result) {
33105
+ return {
33106
+ action: "craft",
33107
+ status: result.status,
33108
+ slug: result.slug ?? null,
33109
+ objective: result.objective,
33110
+ critic_score: result.critic_score,
33111
+ rounds: result.rounds,
33112
+ usage: result.usage,
33113
+ library_path: result.library_path ?? null,
33114
+ journal_path: result.journal_path ?? null,
33115
+ inbox_path: result.inbox_path ?? null,
33116
+ handoff_path: result.handoff_path ?? null,
33117
+ blocking_gaps: result.blocking_gaps,
33118
+ stop_reason: result.stop_reason ?? null
33119
+ };
33120
+ }
31401
33121
  function renderStrategyResult(result) {
31402
33122
  switch (result.action) {
33123
+ case "craft":
33124
+ renderCraftResult(result);
33125
+ return;
31403
33126
  case "review":
31404
33127
  return;
31405
33128
  case "ingest":
@@ -31432,6 +33155,31 @@ function renderStrategyResult(result) {
31432
33155
  return;
31433
33156
  }
31434
33157
  }
33158
+ function renderCraftResult(result) {
33159
+ if (result.plan) {
33160
+ printStrategyBrief(result.plan, {
33161
+ measurable_targets: result.measurable_targets,
33162
+ total_targets: result.total_targets
33163
+ });
33164
+ } else {
33165
+ console.log();
33166
+ console.log(" " + chalk39.dim("(No plan produced)"));
33167
+ }
33168
+ for (const notice of result.notices.slice(0, 8)) {
33169
+ console.log(" " + chalk39.dim(notice));
33170
+ }
33171
+ printLlmAttribution(result.usage);
33172
+ printCraftWrapUp({
33173
+ status: result.status,
33174
+ library_path: result.library_path,
33175
+ inbox_path: result.inbox_path,
33176
+ handoff_path: result.handoff_path,
33177
+ journal_path: result.journal_path,
33178
+ slug: result.slug,
33179
+ jobId: result.job?.id,
33180
+ oneShot: true
33181
+ });
33182
+ }
31435
33183
  function printStrategyList(strategies) {
31436
33184
  console.log(chalk39.bold("\n Strategies\n"));
31437
33185
  if (strategies.length === 0) {
@@ -31542,7 +33290,10 @@ var init_strategy2 = __esm({
31542
33290
  init_types2();
31543
33291
  init_strategy();
31544
33292
  init_repl_api();
31545
- RESERVED_SUBCOMMANDS = /* @__PURE__ */ new Set(["ingest", "add", "list", "show", "sync", "sources", "review"]);
33293
+ init_strategist_run();
33294
+ init_strategy_brief();
33295
+ init_llm_attribution();
33296
+ RESERVED_SUBCOMMANDS = /* @__PURE__ */ new Set(["ingest", "add", "list", "show", "sync", "sources", "review", "craft"]);
31546
33297
  }
31547
33298
  });
31548
33299
 
@@ -32461,15 +34212,15 @@ var init_checkout = __esm({
32461
34212
  });
32462
34213
 
32463
34214
  // src/services/setup.ts
32464
- import { existsSync as existsSync30, mkdirSync as mkdirSync17, readFileSync as readFileSync20, writeFileSync as writeFileSync19 } from "fs";
32465
- import { join as join29 } from "path";
34215
+ import { existsSync as existsSync33, mkdirSync as mkdirSync18, readFileSync as readFileSync23, writeFileSync as writeFileSync21 } from "fs";
34216
+ import { join as join32 } from "path";
32466
34217
  function setupCheck() {
32467
34218
  const home = ntrpHome();
32468
34219
  let writable = false;
32469
34220
  try {
32470
- mkdirSync17(home, { recursive: true });
32471
- const probe = join29(home, ".write-check");
32472
- writeFileSync19(probe, "ok\n");
34221
+ mkdirSync18(home, { recursive: true });
34222
+ const probe = join32(home, ".write-check");
34223
+ writeFileSync21(probe, "ok\n");
32473
34224
  writable = true;
32474
34225
  } catch {
32475
34226
  writable = false;
@@ -32508,7 +34259,7 @@ function setupCheck() {
32508
34259
  };
32509
34260
  }
32510
34261
  function readProfileInput(pathOrDash) {
32511
- const raw = pathOrDash === "-" ? readFileSync20(0, "utf-8") : readFileSync20(pathOrDash, "utf-8");
34262
+ const raw = pathOrDash === "-" ? readFileSync23(0, "utf-8") : readFileSync23(pathOrDash, "utf-8");
32512
34263
  return JSON.parse(raw);
32513
34264
  }
32514
34265
  function writeAgentProfile(input) {
@@ -33298,7 +35049,7 @@ var init_orchestrator = __esm({
33298
35049
  });
33299
35050
 
33300
35051
  // src/services/smoke-protocol.ts
33301
- import { join as join30 } from "path";
35052
+ import { join as join33 } from "path";
33302
35053
  function isSmokeProtocolTrigger(input) {
33303
35054
  return normalize2(input).includes(SMOKE_TRIGGER_PHRASE);
33304
35055
  }
@@ -33334,7 +35085,7 @@ async function runSmokeProtocol(_input, ctx) {
33334
35085
  });
33335
35086
  const proposalResult = await proposeRepositoryExport({
33336
35087
  target: "markdown",
33337
- directory: join30(getExportsDir(), "repository-smoke"),
35088
+ directory: join33(getExportsDir(), "repository-smoke"),
33338
35089
  source: "smoke_protocol",
33339
35090
  modelOrFixture: "smoke-protocol-v1"
33340
35091
  });
@@ -34266,8 +36017,8 @@ var init_recall = __esm({
34266
36017
 
34267
36018
  // src/memory/feedback.ts
34268
36019
  import { appendFileSync as appendFileSync7 } from "fs";
34269
- import { join as join31 } from "path";
34270
- import { randomUUID as randomUUID8 } from "crypto";
36020
+ import { join as join34 } from "path";
36021
+ import { randomUUID as randomUUID9 } from "crypto";
34271
36022
  function summarize(text) {
34272
36023
  return text.replace(/[#*`>_]/g, "").replace(/\s+/g, " ").trim().slice(0, 200);
34273
36024
  }
@@ -34289,7 +36040,7 @@ function findCalibrationToSupersede(question, note) {
34289
36040
  }
34290
36041
  function recordFeedback(input) {
34291
36042
  const entry = {
34292
- id: randomUUID8(),
36043
+ id: randomUUID9(),
34293
36044
  rating: input.rating,
34294
36045
  question: scrubText(input.question).slice(0, 300),
34295
36046
  answer_summary: scrubText(summarize(input.answer)),
@@ -34298,7 +36049,7 @@ function recordFeedback(input) {
34298
36049
  created_at: (/* @__PURE__ */ new Date()).toISOString()
34299
36050
  };
34300
36051
  try {
34301
- appendFileSync7(join31(getMemoryDir(), "feedback.jsonl"), JSON.stringify(entry) + "\n");
36052
+ appendFileSync7(join34(getMemoryDir(), "feedback.jsonl"), JSON.stringify(entry) + "\n");
34302
36053
  } catch {
34303
36054
  }
34304
36055
  if (input.rating === "positive") {
@@ -34468,7 +36219,7 @@ __export(sessions_exports, {
34468
36219
  handler: () => handler35
34469
36220
  });
34470
36221
  import chalk62 from "chalk";
34471
- import { existsSync as existsSync31 } from "fs";
36222
+ import { existsSync as existsSync34 } from "fs";
34472
36223
  async function handler35(args, _ctx) {
34473
36224
  const sub = args[0] ?? "list";
34474
36225
  if (sub === "list" || !args[0]) {
@@ -34575,12 +36326,12 @@ function showSession(idArg) {
34575
36326
  console.log();
34576
36327
  const transcriptPath = transcriptPathForSession(session.id);
34577
36328
  const contextPath = contextDocPathForSession(session.id);
34578
- if (existsSync31(transcriptPath) || existsSync31(contextPath)) {
36329
+ if (existsSync34(transcriptPath) || existsSync34(contextPath)) {
34579
36330
  console.log(" " + chalk62.dim("\u2500".repeat(40)));
34580
- if (existsSync31(contextPath)) {
36331
+ if (existsSync34(contextPath)) {
34581
36332
  console.log(" " + chalk62.dim("Context brief: ") + chalk62.dim(contextPath));
34582
36333
  }
34583
- if (existsSync31(transcriptPath)) {
36334
+ if (existsSync34(transcriptPath)) {
34584
36335
  console.log(" " + chalk62.dim("Full transcript: ") + chalk62.dim(transcriptPath));
34585
36336
  }
34586
36337
  console.log();
@@ -34733,7 +36484,7 @@ var switch_exports = {};
34733
36484
  __export(switch_exports, {
34734
36485
  handler: () => handler38
34735
36486
  });
34736
- import { join as join32 } from "path";
36487
+ import { join as join35 } from "path";
34737
36488
  import chalk65 from "chalk";
34738
36489
  async function handler38(args, ctx) {
34739
36490
  if (args.length === 0) {
@@ -34763,7 +36514,7 @@ async function handler38(args, ctx) {
34763
36514
  }
34764
36515
  const context = buildSwitchContext(session);
34765
36516
  const newId = makeSessionId();
34766
- const newFile = join32(getSessionsDir(), `${newId}.json`);
36517
+ const newFile = join35(getSessionsDir(), `${newId}.json`);
34767
36518
  resetContextForSwitch(ctx, {
34768
36519
  sessionId: newId,
34769
36520
  sessionFile: newFile,
@@ -34776,7 +36527,8 @@ async function handler38(args, ctx) {
34776
36527
  stage: session.stage,
34777
36528
  dataset: session.dataset,
34778
36529
  deliverables: session.deliverables,
34779
- llm: session.llm ? { ...session.llm } : void 0
36530
+ llm: session.llm ? { ...session.llm } : void 0,
36531
+ lastCraftJobId: session.last_craft_job_id
34780
36532
  });
34781
36533
  console.log();
34782
36534
  console.log(" " + paint("accent", `Switched to "${targetName}"`));
@@ -34789,7 +36541,7 @@ async function handler38(args, ctx) {
34789
36541
  return `Switched to "${targetName}"`;
34790
36542
  } else {
34791
36543
  const newId = makeSessionId();
34792
- const newFile = join32(getSessionsDir(), `${newId}.json`);
36544
+ const newFile = join35(getSessionsDir(), `${newId}.json`);
34793
36545
  resetContextForSwitch(ctx, {
34794
36546
  sessionId: newId,
34795
36547
  sessionFile: newFile,
@@ -35916,186 +37668,6 @@ var init_model = __esm({
35916
37668
  }
35917
37669
  });
35918
37670
 
35919
- // src/config/update-check.ts
35920
- import { existsSync as existsSync32, mkdirSync as mkdirSync18, readFileSync as readFileSync21, unlinkSync as unlinkSync5, writeFileSync as writeFileSync20 } from "fs";
35921
- import { join as join33 } from "path";
35922
- function cachePath2() {
35923
- return join33(ntrpHome(), "update-check.json");
35924
- }
35925
- function ensureDir7() {
35926
- const dir = ntrpHome();
35927
- if (!existsSync32(dir)) {
35928
- mkdirSync18(dir, { recursive: true });
35929
- }
35930
- }
35931
- function loadUpdateCheckCache() {
35932
- const path = cachePath2();
35933
- if (!existsSync32(path)) return null;
35934
- try {
35935
- const parsed = JSON.parse(readFileSync21(path, "utf-8"));
35936
- if (!parsed || typeof parsed !== "object" || typeof parsed.lastCheck !== "number" || typeof parsed.latestVersion !== "string") {
35937
- return null;
35938
- }
35939
- return parsed;
35940
- } catch {
35941
- return null;
35942
- }
35943
- }
35944
- function saveUpdateCheckCache(cache2) {
35945
- ensureDir7();
35946
- writeFileSync20(cachePath2(), JSON.stringify(cache2, null, 2) + "\n");
35947
- }
35948
- function isCacheFresh(cache2, ttlMs = CACHE_TTL_MS2) {
35949
- if (!cache2) return false;
35950
- return Date.now() - cache2.lastCheck < ttlMs;
35951
- }
35952
- function invalidateUpdateCheckCache() {
35953
- const path = cachePath2();
35954
- if (existsSync32(path)) {
35955
- unlinkSync5(path);
35956
- }
35957
- }
35958
- var CACHE_TTL_MS2;
35959
- var init_update_check = __esm({
35960
- "src/config/update-check.ts"() {
35961
- "use strict";
35962
- init_store();
35963
- CACHE_TTL_MS2 = 864e5;
35964
- }
35965
- });
35966
-
35967
- // src/version.ts
35968
- import { existsSync as existsSync33, readFileSync as readFileSync22 } from "fs";
35969
- import { dirname as dirname6, join as join34 } from "path";
35970
- import { fileURLToPath } from "url";
35971
- function readVersionFromPackageJson(packageJsonPath) {
35972
- if (!existsSync33(packageJsonPath)) return null;
35973
- try {
35974
- const pkg = JSON.parse(readFileSync22(packageJsonPath, "utf-8"));
35975
- if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
35976
- } catch {
35977
- }
35978
- return null;
35979
- }
35980
- function readVersionNearEntry(entryPath) {
35981
- const start = dirname6(entryPath);
35982
- for (const rel of [join34(start, "..", "package.json"), join34(start, "../..", "package.json")]) {
35983
- const version = readVersionFromPackageJson(rel);
35984
- if (version) return version;
35985
- }
35986
- return null;
35987
- }
35988
- function readInstalledVersionFromDisk() {
35989
- return readVersionNearEntry(fileURLToPath(import.meta.url));
35990
- }
35991
- function getInstalledVersion() {
35992
- if (cachedVersion) return cachedVersion;
35993
- cachedVersion = readInstalledVersionFromDisk() ?? "0.0.0";
35994
- return cachedVersion;
35995
- }
35996
- var cachedVersion;
35997
- var init_version = __esm({
35998
- "src/version.ts"() {
35999
- "use strict";
36000
- }
36001
- });
36002
-
36003
- // src/update/registry.ts
36004
- var registry_exports = {};
36005
- __export(registry_exports, {
36006
- NPM_PACKAGE: () => NPM_PACKAGE,
36007
- applyUpdateCheckResult: () => applyUpdateCheckResult,
36008
- checkForUpdate: () => checkForUpdate,
36009
- fetchLatestVersion: () => fetchLatestVersion,
36010
- formatUpdateNudge: () => formatUpdateNudge,
36011
- hydrateUpdateAvailableFromCache: () => hydrateUpdateAvailableFromCache,
36012
- isNewerVersion: () => isNewerVersion,
36013
- startBackgroundUpdateCheck: () => startBackgroundUpdateCheck
36014
- });
36015
- function registryUrl() {
36016
- return process.env.NTRP_REGISTRY_URL ?? "https://registry.npmjs.org/@sonnechasser/ntrp/latest";
36017
- }
36018
- function parseVersionParts(version) {
36019
- const cleaned = version.trim().replace(/^v/i, "");
36020
- const core = cleaned.split("-")[0] ?? cleaned;
36021
- const parts = core.split(".").map((p) => parseInt(p, 10));
36022
- return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
36023
- }
36024
- function isNewerVersion(latest, current) {
36025
- const [lMaj, lMin, lPatch] = parseVersionParts(latest);
36026
- const [cMaj, cMin, cPatch] = parseVersionParts(current);
36027
- if (lMaj !== cMaj) return lMaj > cMaj;
36028
- if (lMin !== cMin) return lMin > cMin;
36029
- return lPatch > cPatch;
36030
- }
36031
- function formatUpdateNudge(current, latest) {
36032
- return `\u26A1 NTRP v${latest} available (running v${current}) \u2014 type /update`;
36033
- }
36034
- function hydrateUpdateAvailableFromCache(current) {
36035
- const cached2 = loadUpdateCheckCache();
36036
- if (!cached2?.latestVersion) return void 0;
36037
- if (!isNewerVersion(cached2.latestVersion, current)) return void 0;
36038
- return { current, latest: cached2.latestVersion };
36039
- }
36040
- function applyUpdateCheckResult(ctx, result) {
36041
- if (result?.updateAvailable) {
36042
- ctx.updateAvailable = { current: result.current, latest: result.latest };
36043
- return;
36044
- }
36045
- if (result && !result.updateAvailable) {
36046
- ctx.updateAvailable = void 0;
36047
- }
36048
- }
36049
- function startBackgroundUpdateCheck(ctx) {
36050
- const pending = checkForUpdate({ force: true, timeoutMs: 5e3 });
36051
- ctx.pendingUpdateCheck = pending;
36052
- void pending.then((result) => applyUpdateCheckResult(ctx, result)).catch(() => void 0);
36053
- }
36054
- async function fetchLatestVersion(timeoutMs = 5e3) {
36055
- try {
36056
- const res = await fetch(registryUrl(), { signal: AbortSignal.timeout(timeoutMs) });
36057
- if (!res.ok) return null;
36058
- const data = await res.json();
36059
- return typeof data.version === "string" && data.version.length > 0 ? data.version : null;
36060
- } catch {
36061
- return null;
36062
- }
36063
- }
36064
- function buildResult(current, latest) {
36065
- return {
36066
- current,
36067
- latest,
36068
- updateAvailable: isNewerVersion(latest, current)
36069
- };
36070
- }
36071
- async function checkForUpdate(options) {
36072
- const current = getInstalledVersion();
36073
- const timeoutMs = options?.timeoutMs ?? 5e3;
36074
- const cached2 = loadUpdateCheckCache();
36075
- if (!options?.force && isCacheFresh(cached2)) {
36076
- return buildResult(current, cached2.latestVersion);
36077
- }
36078
- const latest = await fetchLatestVersion(timeoutMs);
36079
- if (!latest) {
36080
- if (cached2?.latestVersion) {
36081
- return buildResult(current, cached2.latestVersion);
36082
- }
36083
- return null;
36084
- }
36085
- const nextCache = { lastCheck: Date.now(), latestVersion: latest };
36086
- saveUpdateCheckCache(nextCache);
36087
- return buildResult(current, latest);
36088
- }
36089
- var NPM_PACKAGE;
36090
- var init_registry = __esm({
36091
- "src/update/registry.ts"() {
36092
- "use strict";
36093
- init_update_check();
36094
- init_version();
36095
- NPM_PACKAGE = "@sonnechasser/ntrp";
36096
- }
36097
- });
36098
-
36099
37671
  // src/update/relaunch.ts
36100
37672
  var relaunch_exports = {};
36101
37673
  __export(relaunch_exports, {
@@ -36109,8 +37681,8 @@ __export(relaunch_exports, {
36109
37681
  resolveRelaunchEntry: () => resolveRelaunchEntry,
36110
37682
  updateRestartSummary: () => updateRestartSummary
36111
37683
  });
36112
- import { existsSync as existsSync34 } from "fs";
36113
- import { join as join35 } from "path";
37684
+ import { existsSync as existsSync35 } from "fs";
37685
+ import { join as join36 } from "path";
36114
37686
  import { fileURLToPath as fileURLToPath2 } from "url";
36115
37687
  import { spawnSync } from "child_process";
36116
37688
  function encodeJustUpdated(fromVersion, toVersion) {
@@ -36134,8 +37706,8 @@ function updateRestartSummary(toVersion) {
36134
37706
  function npmGlobalEntry() {
36135
37707
  const listed = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
36136
37708
  if (listed.status !== 0) return null;
36137
- const entry = join35(listed.stdout.trim(), NPM_PACKAGE, "dist/index.js");
36138
- return existsSync34(entry) ? entry : null;
37709
+ const entry = join36(listed.stdout.trim(), NPM_PACKAGE, "dist/index.js");
37710
+ return existsSync35(entry) ? entry : null;
36139
37711
  }
36140
37712
  function thisBundleEntry() {
36141
37713
  return fileURLToPath2(import.meta.url);
@@ -36145,10 +37717,10 @@ function resolveRelaunchEntry(toVersion) {
36145
37717
  (p) => Boolean(p)
36146
37718
  );
36147
37719
  for (const entry of candidates) {
36148
- if (!existsSync34(entry)) continue;
37720
+ if (!existsSync35(entry)) continue;
36149
37721
  if (readVersionNearEntry(entry) === toVersion) return entry;
36150
37722
  }
36151
- return candidates.find((p) => existsSync34(p)) ?? thisBundleEntry();
37723
+ return candidates.find((p) => existsSync35(p)) ?? thisBundleEntry();
36152
37724
  }
36153
37725
  function relaunchArgv(toVersion) {
36154
37726
  return [resolveRelaunchEntry(toVersion)];
@@ -36242,6 +37814,22 @@ async function handler44(_args, ctx) {
36242
37814
  console.log();
36243
37815
  return;
36244
37816
  }
37817
+ if (!ctx.oneShot) {
37818
+ const prompts = createPromptSession(ctx.rl, ctx);
37819
+ let ok2 = false;
37820
+ try {
37821
+ console.log();
37822
+ ok2 = await prompts.confirm(`Install NTRP v${current} \u2192 v${latest}?`, true);
37823
+ } finally {
37824
+ prompts.close();
37825
+ }
37826
+ if (!ok2) {
37827
+ console.log();
37828
+ console.log(chalk73.dim(" Update cancelled."));
37829
+ console.log();
37830
+ return "Update cancelled";
37831
+ }
37832
+ }
36245
37833
  console.log();
36246
37834
  console.log(` Updating NTRP v${current} \u2192 v${latest}...`);
36247
37835
  const { ok, output } = runGlobalInstall();
@@ -36288,6 +37876,7 @@ var PERMISSIONS_URL;
36288
37876
  var init_update = __esm({
36289
37877
  "src/commands/update.ts"() {
36290
37878
  "use strict";
37879
+ init_prompts();
36291
37880
  init_update_check();
36292
37881
  init_registry();
36293
37882
  init_relaunch();
@@ -36362,10 +37951,10 @@ function renderProgressReport() {
36362
37951
  `${chalk74.dim("Deliverables")} ${chalk74.bold(String(usage5.deliverables))}`,
36363
37952
  `${chalk74.dim("AI exchanges")} ${chalk74.bold(String(usage5.nl_exchanges))}`
36364
37953
  ]);
36365
- const totalTokens = usage5.input_tokens + usage5.output_tokens;
37954
+ const totalTokens2 = usage5.input_tokens + usage5.output_tokens;
36366
37955
  printCard("AI usage", [
36367
37956
  `${chalk74.dim("LLM calls")} ${chalk74.bold(String(usage5.llm_calls))}`,
36368
- `${chalk74.dim("Tokens")} ${chalk74.bold(formatTokens(totalTokens))} in+out (${formatTokens(usage5.input_tokens)} in \xB7 ${formatTokens(usage5.output_tokens)} out)`
37957
+ `${chalk74.dim("Tokens")} ${chalk74.bold(formatTokens(totalTokens2))} in+out (${formatTokens(usage5.input_tokens)} in \xB7 ${formatTokens(usage5.output_tokens)} out)`
36369
37958
  ]);
36370
37959
  const weeks = [...usage5.weekly].sort((a, b) => a.week.localeCompare(b.week)).slice(-8);
36371
37960
  const weekHours = weeks.map((w) => w.minutes_saved / 60);
@@ -36671,8 +38260,8 @@ __export(exports_exports, {
36671
38260
  handler: () => handler48
36672
38261
  });
36673
38262
  import chalk78 from "chalk";
36674
- import { existsSync as existsSync35 } from "fs";
36675
- import { join as join36 } from "path";
38263
+ import { existsSync as existsSync36 } from "fs";
38264
+ import { join as join37 } from "path";
36676
38265
  function usage4() {
36677
38266
  console.log(chalk78.dim(" Usage:"));
36678
38267
  console.log(chalk78.dim(" /exports list [kind]"));
@@ -36729,7 +38318,7 @@ function printInboxShow() {
36729
38318
  console.log(" " + paint("accent", "AI inbox: ") + inbox);
36730
38319
  const latest = inboxLatestHandoffPath();
36731
38320
  if (latest) console.log(" " + chalk78.dim("Latest handoff: ") + latest);
36732
- console.log(" " + chalk78.dim("Finder skill: ") + join36(inbox, "SKILL.md"));
38321
+ console.log(" " + chalk78.dim("Finder skill: ") + join37(inbox, "SKILL.md"));
36733
38322
  console.log(" " + chalk78.dim("Reprint: ") + paint("accent", "/inbox skill"));
36734
38323
  } else {
36735
38324
  console.log(" " + chalk78.dim("AI inbox: (not set). Type ") + paint("accent", "/inbox set <folder>"));
@@ -36751,8 +38340,8 @@ function printOpen() {
36751
38340
  console.log(" " + chalk78.dim("AI inbox: ") + inbox);
36752
38341
  const latest = inboxLatestHandoffPath();
36753
38342
  if (latest) console.log(" " + chalk78.dim("Inbox latest: ") + latest);
36754
- console.log(" " + chalk78.dim("Pickup skill: ") + join36(inbox, "latest-pickup.md"));
36755
- console.log(" " + chalk78.dim("Finder skill: ") + join36(inbox, "SKILL.md"));
38343
+ console.log(" " + chalk78.dim("Pickup skill: ") + join37(inbox, "latest-pickup.md"));
38344
+ console.log(" " + chalk78.dim("Finder skill: ") + join37(inbox, "SKILL.md"));
36756
38345
  } else {
36757
38346
  console.log(" " + chalk78.dim("AI inbox is not set. Type ") + paint("accent", "/inbox set <folder>"));
36758
38347
  const loc = handoffLocations();
@@ -36830,7 +38419,7 @@ function runMove(args, ctx) {
36830
38419
  }
36831
38420
  try {
36832
38421
  const destDir = resolveUserPath(dest);
36833
- if (!existsSync35(destDir)) {
38422
+ if (!existsSync36(destDir)) {
36834
38423
  }
36835
38424
  const event = moveExport(idOrName, destDir);
36836
38425
  console.log();
@@ -37247,7 +38836,9 @@ section: Settings
37247
38836
  handler: ../commands/update.ts
37248
38837
  ---
37249
38838
 
37250
- Install the latest global NTRP package via npm.`
38839
+ Install the latest global NTRP package via npm.
38840
+ Interactive confirms with \u23CE yes, then re-execs onto home.
38841
+ One-shot installs immediately and prints a restart hint.`
37251
38842
  },
37252
38843
  {
37253
38844
  name: "resume",
@@ -37474,7 +39065,7 @@ This command is hidden. Type \`/ingest --demo\` instead. That command calls this
37474
39065
  name: strategy
37475
39066
  description: Make a measured strategy from your data
37476
39067
  section: More
37477
- args: [objective] | [list|show|review|ingest|add|sync|sources] [args]
39068
+ args: [objective] | [list|show|review|ingest|add|sync|sources|craft] [args]
37478
39069
  handler: ../commands/strategy.ts
37479
39070
  ---
37480
39071
 
@@ -37483,6 +39074,8 @@ NTRP uses live data. It works back from the objective.
37483
39074
  The result is sequenced workstreams with dated milestones, deliverables, outcome ranges, and a contingency per workstream.
37484
39075
  Saved plans go to the strategy library. Later answers use them.
37485
39076
  Type \`/strategy review [slug]\` to check the plan against live data when new batches arrive.
39077
+ Type \`keep going\` at the confirm card to keep working until the plan is ready.
39078
+ One-shot: \`ntrp strategy craft <objective>\`. Resume with \`ntrp strategy craft --resume <id>\`.
37486
39079
 
37487
39080
  Library commands: \`/strategy list\`, \`/strategy show <slug>\`.
37488
39081
  Type \`/strategy ingest <file>\` for markdown, YAML, PDF, text, or \`-\` for stdin.
@@ -37864,8 +39457,8 @@ Remaining nuances merge into a custom_context paragraph that flows into all AI s
37864
39457
  });
37865
39458
 
37866
39459
  // src/ai/prompt-parts.ts
37867
- import { existsSync as existsSync36, readFileSync as readFileSync23 } from "fs";
37868
- import { join as join37 } from "path";
39460
+ import { existsSync as existsSync37, readFileSync as readFileSync24 } from "fs";
39461
+ import { join as join38 } from "path";
37869
39462
  function buildCompanyProfileBlock() {
37870
39463
  const p = loadProfile();
37871
39464
  if (!p) return "";
@@ -37883,10 +39476,10 @@ function buildCompanyProfileBlock() {
37883
39476
  return lines.join("\n");
37884
39477
  }
37885
39478
  function loadAnalystFile() {
37886
- const path = join37(ntrpHome(), ANALYST_FILE_NAME);
39479
+ const path = join38(ntrpHome(), ANALYST_FILE_NAME);
37887
39480
  try {
37888
- if (!existsSync36(path)) return null;
37889
- const raw = sanitizeExternalText(readFileSync23(path, "utf-8").trim());
39481
+ if (!existsSync37(path)) return null;
39482
+ const raw = sanitizeExternalText(readFileSync24(path, "utf-8").trim());
37890
39483
  if (!raw) return null;
37891
39484
  if (raw.length <= ANALYST_FILE_MAX_CHARS) return raw;
37892
39485
  const head = raw.slice(0, Math.floor(ANALYST_FILE_MAX_CHARS * 0.75));
@@ -38420,8 +40013,8 @@ __export(ingest_chat_exports, {
38420
40013
  loadDemoFromChat: () => loadDemoFromChat,
38421
40014
  looksLikeFilePath: () => looksLikeFilePath
38422
40015
  });
38423
- import { existsSync as existsSync37, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
38424
- import { basename as basename10, join as join38, resolve as resolve9 } from "path";
40016
+ import { existsSync as existsSync38, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
40017
+ import { basename as basename10, join as join39, resolve as resolve9 } from "path";
38425
40018
  import { homedir as homedir8 } from "os";
38426
40019
  import chalk80 from "chalk";
38427
40020
  function extractFilePath(input) {
@@ -38443,7 +40036,7 @@ function extractFilePath(input) {
38443
40036
  if (!candidate) continue;
38444
40037
  if (!looksLikePathToken(candidate)) continue;
38445
40038
  const p = expandPath(candidate);
38446
- if (existsSync37(p)) {
40039
+ if (existsSync38(p)) {
38447
40040
  try {
38448
40041
  const st = statSync5(p);
38449
40042
  if (st.isFile() || st.isDirectory()) return p;
@@ -38472,7 +40065,7 @@ function looksLikeFilePath(input) {
38472
40065
  function listCsvsInFolder(dir) {
38473
40066
  try {
38474
40067
  if (!statSync5(dir).isDirectory()) return [];
38475
- return readdirSync6(dir).filter((name) => name.toLowerCase().endsWith(".csv")).map((name) => join38(dir, name)).sort();
40068
+ return readdirSync6(dir).filter((name) => name.toLowerCase().endsWith(".csv")).map((name) => join39(dir, name)).sort();
38476
40069
  } catch {
38477
40070
  return [];
38478
40071
  }
@@ -38554,12 +40147,12 @@ async function ingestFromChat(ctx, filePath) {
38554
40147
  }
38555
40148
  const { handler: ingest } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
38556
40149
  const { detectEntityType: detectEntityType2 } = await Promise.resolve().then(() => (init_csv_detect(), csv_detect_exports));
38557
- const { readFileSync: readFileSync24 } = await import("fs");
40150
+ const { readFileSync: readFileSync25 } = await import("fs");
38558
40151
  const { parseCSV: parseCSV2 } = await Promise.resolve().then(() => (init_csv_parse(), csv_parse_exports));
38559
40152
  const { getStoredApiKey } = await Promise.resolve().then(() => (init_repl_api(), repl_api_exports));
38560
40153
  let headerCheckFailed = false;
38561
40154
  try {
38562
- const raw = readFileSync24(filePath, "utf-8");
40155
+ const raw = readFileSync25(filePath, "utf-8");
38563
40156
  const { headers } = parseCSV2(raw);
38564
40157
  const detected = detectEntityType2(headers, "unknown");
38565
40158
  if (!detected) headerCheckFailed = true;
@@ -39557,6 +41150,10 @@ async function conversationRouter(input, ctx) {
39557
41150
  const summary = await handleStrategizeFlow(line, ctx) ?? void 0;
39558
41151
  return { handled: true, summary };
39559
41152
  }
41153
+ if (isKeepGoingIntent(line) && (phase === "explore" || phase === "orient" && isAnalysisReady(ctx))) {
41154
+ const summary = await handleKeepGoingLine(ctx, line) ?? void 0;
41155
+ return { handled: true, summary };
41156
+ }
39560
41157
  if (phase === "think") {
39561
41158
  const summary = await handleThinkFlow(line, ctx) ?? void 0;
39562
41159
  return { handled: true, summary };
@@ -39867,7 +41464,7 @@ var init_loop_guard2 = __esm({
39867
41464
  },
39868
41465
  strategize: {
39869
41466
  mode: "strategy objective confirm",
39870
- accepts: "\u23CE or yes (build the plan) \xB7 b / back or adjust (restate the objective) \xB7 cancel",
41467
+ accepts: "\u23CE or yes (build the plan) \xB7 keep going (keep working until the plan is ready) \xB7 b / back or adjust (restate the objective) \xB7 cancel",
39871
41468
  leave: "cancel drops the strategy session and returns to Q&A"
39872
41469
  }
39873
41470
  };
@@ -39917,7 +41514,7 @@ function ntrpStatusRow(version, update) {
39917
41514
  return {
39918
41515
  label: "ntrp",
39919
41516
  state: badge("UPDATE", "warning"),
39920
- detail: `v${update.latest} \xB7 type /update`
41517
+ detail: `v${update.latest} \xB7 \u23CE /update`
39921
41518
  };
39922
41519
  }
39923
41520
  if (disk && isNewerVersion(disk, getInstalledVersion())) {
@@ -40352,7 +41949,8 @@ var init_deepdive_complete = __esm({
40352
41949
  "inbox",
40353
41950
  "claude",
40354
41951
  "skill",
40355
- "strategy"
41952
+ "strategy",
41953
+ "keep-going"
40356
41954
  ];
40357
41955
  }
40358
41956
  });
@@ -40463,7 +42061,7 @@ __export(repl_exports, {
40463
42061
  import { createInterface as createInterface2 } from "readline/promises";
40464
42062
  import { clearLine as clearLine2, cursorTo as cursorTo2 } from "readline";
40465
42063
  import chalk88 from "chalk";
40466
- import { join as join39 } from "path";
42064
+ import { join as join40 } from "path";
40467
42065
  function buildPrompt(ctx) {
40468
42066
  return buildConversationPrompt(ctx);
40469
42067
  }
@@ -40791,14 +42389,14 @@ function printHelp() {
40791
42389
  console.log(" " + chalk88.dim("Type the question. You do not need a slash command."));
40792
42390
  console.log(" " + chalk88.dim("Paste a CSV path or type ") + paint("accent", '"use demo data"') + chalk88.dim(" to load data."));
40793
42391
  console.log(" " + chalk88.dim("After analysis, type questions in English."));
40794
- console.log(" " + chalk88.dim("Type ") + paint("accent", '"how should we fix this?"') + chalk88.dim(" to make a strategy."));
42392
+ console.log(" " + chalk88.dim("Type ") + paint("accent", '"how should we fix this?"') + chalk88.dim(" to make a strategy. Type ") + paint("accent", "keep going") + chalk88.dim(" at confirm to keep working until the plan is ready."));
40795
42393
  console.log(" " + chalk88.dim("Type ") + paint("accent", '"ship a board deck"') + chalk88.dim(" to write a handoff."));
40796
42394
  console.log(" " + chalk88.dim("The ") + paint("accent", "\u203A") + chalk88.dim(" prompt shows brief or deep after analysis. Brief is the default."));
40797
42395
  console.log();
40798
42396
  console.log(" " + sectionHeading("Keys"));
40799
42397
  console.log(
40800
42398
  " " + paint("accent", "\u23CE") + chalk88.dim(
40801
- " Accepts the default. At the main prompt it runs the armed action (yes, use demo data, go ahead, /connect)."
42399
+ " Accepts the default. At the main prompt it runs the armed action (yes, use demo data, go ahead, /connect, /update, keep going)."
40802
42400
  )
40803
42401
  );
40804
42402
  console.log(
@@ -40849,7 +42447,7 @@ function printHelp() {
40849
42447
  ["/remember <fact>", "Store a fact, a decision, or a preference"],
40850
42448
  ["/recall [topic]", "Show what NTRP stores about your business"],
40851
42449
  ["/rate good|bad <note>", "Correct the last answer. A bad note becomes a calibration"],
40852
- [`${join39(ntrpHome(), ANALYST_FILE_NAME)}`, "Standing operator instructions (tone, priorities, house rules)"]
42450
+ [`${join40(ntrpHome(), ANALYST_FILE_NAME)}`, "Standing operator instructions (tone, priorities, house rules)"]
40853
42451
  ];
40854
42452
  const teachMaxW = Math.max(...teach.map(([c]) => c.length)) + 2;
40855
42453
  for (const [cmd, desc] of teach) {