@sonnechasser/ntrp 1.5.3 → 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 +1853 -283
  2. package/dist/mcp/server.js +1565 -70
  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
  "",
@@ -15683,14 +15827,140 @@ var init_registry = __esm({
15683
15827
  }
15684
15828
  });
15685
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
+
15686
15949
  // src/conversation/recommended-action.ts
15687
15950
  function resolveRecommendedAction(ctx) {
15688
15951
  if (!hasValidLicense()) return { submit: "/activate", hint: "/activate" };
15689
15952
  const phase = resolveConversationPhase(ctx);
15690
15953
  switch (phase) {
15691
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
+ }
15692
15962
  if (ctx.stage === "delivered") return { submit: "/end", hint: "home" };
15693
- return canUseReplAi(ctx) ? null : { submit: "/connect", hint: "/connect" };
15963
+ return null;
15694
15964
  case "awaiting_data":
15695
15965
  if (ctx.gapAudit?.can_compute) return { submit: "go ahead", hint: "go ahead" };
15696
15966
  if (!sessionHasData(ctx)) return { submit: "use demo data", hint: "use demo data" };
@@ -15714,6 +15984,7 @@ var init_recommended_action = __esm({
15714
15984
  init_activation();
15715
15985
  init_registry();
15716
15986
  init_phase();
15987
+ init_store3();
15717
15988
  }
15718
15989
  });
15719
15990
 
@@ -16511,17 +16782,17 @@ __export(play_outcomes_exports, {
16511
16782
  listPlayOutcomes: () => listPlayOutcomes,
16512
16783
  recordPlayOutcomes: () => recordPlayOutcomes
16513
16784
  });
16514
- import { existsSync as existsSync23, readFileSync as readFileSync19, appendFileSync as appendFileSync6 } from "fs";
16515
- import { join as join23 } from "path";
16516
- 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";
16517
16788
  function outcomesPath() {
16518
- return join23(getMemoryDir(), OUTCOMES_FILE);
16789
+ return join24(getMemoryDir(), OUTCOMES_FILE);
16519
16790
  }
16520
16791
  function listPlayOutcomes() {
16521
16792
  const path = outcomesPath();
16522
- if (!existsSync23(path)) return [];
16793
+ if (!existsSync24(path)) return [];
16523
16794
  const out = [];
16524
- for (const line of readFileSync19(path, "utf-8").split("\n")) {
16795
+ for (const line of readFileSync20(path, "utf-8").split("\n")) {
16525
16796
  const trimmed = line.trim();
16526
16797
  if (!trimmed) continue;
16527
16798
  try {
@@ -16551,7 +16822,7 @@ function recordPlayOutcomes(strategy, outcomes, batchId) {
16551
16822
  if (seen.has(key)) continue;
16552
16823
  seen.add(key);
16553
16824
  const record = {
16554
- id: randomUUID7(),
16825
+ id: randomUUID8(),
16555
16826
  play_id: playId,
16556
16827
  strategy_slug: strategy.slug,
16557
16828
  workstream_order: outcome.workstream_order,
@@ -16614,11 +16885,11 @@ function jsonSafe(value) {
16614
16885
  }
16615
16886
  return value;
16616
16887
  }
16617
- function envelope(command, data, warnings) {
16888
+ function envelope(command, data, warnings, status = "ok") {
16618
16889
  return {
16619
16890
  schema_version: HEADLESS_SCHEMA_VERSION,
16620
16891
  command,
16621
- status: "ok",
16892
+ status,
16622
16893
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
16623
16894
  data,
16624
16895
  ...warnings && warnings.length > 0 ? { warnings } : {}
@@ -16634,8 +16905,8 @@ function errorEnvelope(command, err) {
16634
16905
  error: ntrpError.toHeadlessError()
16635
16906
  };
16636
16907
  }
16637
- function emitResult(command, data, warnings) {
16638
- 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));
16639
16910
  }
16640
16911
  function emitError(command, err) {
16641
16912
  const ntrpError = err instanceof NtrpError ? err : toNtrpError(err);
@@ -19988,18 +20259,18 @@ var init_terminal = __esm({
19988
20259
  });
19989
20260
 
19990
20261
  // src/demo/taxonomy-cache.ts
19991
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync15, existsSync as existsSync24, mkdirSync as mkdirSync13, unlinkSync as unlinkSync5 } from "fs";
20262
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync16, existsSync as existsSync25, mkdirSync as mkdirSync13, unlinkSync as unlinkSync5 } from "fs";
19992
20263
  import { homedir as homedir7 } from "os";
19993
- import { join as join24 } from "path";
20264
+ import { join as join25 } from "path";
19994
20265
  function ensureDir7() {
19995
- if (!existsSync24(NTRP_DIR4)) {
20266
+ if (!existsSync25(NTRP_DIR4)) {
19996
20267
  mkdirSync13(NTRP_DIR4, { recursive: true });
19997
20268
  }
19998
20269
  }
19999
20270
  function loadCachedTaxonomy(profile) {
20000
- if (!existsSync24(TAXONOMY_PATH)) return null;
20271
+ if (!existsSync25(TAXONOMY_PATH)) return null;
20001
20272
  try {
20002
- const parsed = JSON.parse(readFileSync20(TAXONOMY_PATH, "utf-8"));
20273
+ const parsed = JSON.parse(readFileSync21(TAXONOMY_PATH, "utf-8"));
20003
20274
  if (!parsed || typeof parsed !== "object") return null;
20004
20275
  if (parsed.profile_updated_at !== profile.updated_at) return null;
20005
20276
  return parsed;
@@ -20009,10 +20280,10 @@ function loadCachedTaxonomy(profile) {
20009
20280
  }
20010
20281
  function saveCachedTaxonomy(taxonomy) {
20011
20282
  ensureDir7();
20012
- writeFileSync15(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
20283
+ writeFileSync16(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
20013
20284
  }
20014
20285
  function invalidateTaxonomy() {
20015
- if (existsSync24(TAXONOMY_PATH)) {
20286
+ if (existsSync25(TAXONOMY_PATH)) {
20016
20287
  try {
20017
20288
  unlinkSync5(TAXONOMY_PATH);
20018
20289
  } catch {
@@ -20023,8 +20294,8 @@ var NTRP_DIR4, TAXONOMY_PATH;
20023
20294
  var init_taxonomy_cache = __esm({
20024
20295
  "src/demo/taxonomy-cache.ts"() {
20025
20296
  "use strict";
20026
- NTRP_DIR4 = join24(homedir7(), ".ntrp");
20027
- TAXONOMY_PATH = join24(NTRP_DIR4, "demo-taxonomy.json");
20297
+ NTRP_DIR4 = join25(homedir7(), ".ntrp");
20298
+ TAXONOMY_PATH = join25(NTRP_DIR4, "demo-taxonomy.json");
20028
20299
  }
20029
20300
  });
20030
20301
 
@@ -20482,7 +20753,7 @@ __export(inbox_setup_exports, {
20482
20753
  shouldOfferInboxSkillSetup: () => shouldOfferInboxSkillSetup
20483
20754
  });
20484
20755
  import chalk19 from "chalk";
20485
- import { existsSync as existsSync25 } from "fs";
20756
+ import { existsSync as existsSync26 } from "fs";
20486
20757
  function markDemoOffered() {
20487
20758
  setConfigValue("ai-inbox-nudge-seen", "true");
20488
20759
  }
@@ -20514,7 +20785,7 @@ function printSkipHint(beat) {
20514
20785
  async function reuseInboxFolderIfPresent(session, beat, folderPath) {
20515
20786
  if (getAiInboxDir()) return false;
20516
20787
  const candidates = folderPath ? [folderPath] : [.../* @__PURE__ */ new Set([defaultAiInboxDir(), legacyAiInboxDir()])];
20517
- const existing = candidates.find((p) => existsSync25(p));
20788
+ const existing = candidates.find((p) => existsSync26(p));
20518
20789
  if (!existing) return false;
20519
20790
  console.log(" " + chalk19.dim("Pickup folder still on disk: ") + existing);
20520
20791
  const reuse = await session.confirm("Reuse this pickup folder?", true);
@@ -20620,7 +20891,7 @@ __export(ingest_exports, {
20620
20891
  handler: () => handler2
20621
20892
  });
20622
20893
  import chalk20 from "chalk";
20623
- import { readFileSync as readFileSync21, existsSync as existsSync26 } from "fs";
20894
+ import { readFileSync as readFileSync22, existsSync as existsSync27 } from "fs";
20624
20895
  import { basename as basename6 } from "path";
20625
20896
  async function handler2(args, ctx) {
20626
20897
  const { positional, flags } = parseArgs2(args, [
@@ -20644,7 +20915,7 @@ async function handler2(args, ctx) {
20644
20915
  console.error(chalk20.dim(" /ingest --demo [--scenario <name>]"));
20645
20916
  process.exit(1);
20646
20917
  }
20647
- if (!existsSync26(file)) {
20918
+ if (!existsSync27(file)) {
20648
20919
  console.error(chalk20.red(` File not found: ${file}`));
20649
20920
  process.exit(1);
20650
20921
  }
@@ -20662,7 +20933,7 @@ async function handler2(args, ctx) {
20662
20933
  try {
20663
20934
  await initSchema();
20664
20935
  spinner.text = "Parsing CSV\u2026";
20665
- const content = readFileSync21(file, "utf-8");
20936
+ const content = readFileSync22(file, "utf-8");
20666
20937
  const { rows, headers } = parseCSV(content);
20667
20938
  if (rows.length === 0) {
20668
20939
  spinner.fail("CSV is empty");
@@ -23029,9 +23300,9 @@ async function handleGetSessionBrief(input) {
23029
23300
  if (!target) {
23030
23301
  return { error: `No session matching "${raw}".` };
23031
23302
  }
23032
- const { existsSync: existsSync38, readFileSync: readFileSync24 } = await import("fs");
23303
+ const { existsSync: existsSync39, readFileSync: readFileSync25 } = await import("fs");
23033
23304
  const briefPath = contextDocPathForSession2(target.id);
23034
- if (!existsSync38(briefPath)) {
23305
+ if (!existsSync39(briefPath)) {
23035
23306
  return {
23036
23307
  session_id: target.id,
23037
23308
  error: "No context brief on disk for this session (created before brief storage existed).",
@@ -23042,7 +23313,7 @@ async function handleGetSessionBrief(input) {
23042
23313
  return {
23043
23314
  session_id: target.id,
23044
23315
  security_notice: UNTRUSTED_CONTENT_NOTICE,
23045
- brief: wrapUntrustedContent(readFileSync24(briefPath, "utf-8"))
23316
+ brief: wrapUntrustedContent(readFileSync25(briefPath, "utf-8"))
23046
23317
  };
23047
23318
  }
23048
23319
  function auditDenied(name, input, resultJson, start) {
@@ -25348,7 +25619,7 @@ __export(onboard_tiers_exports, {
25348
25619
  resetOnboardTierProgress: () => resetOnboardTierProgress,
25349
25620
  resolveNextOnboardTier: () => resolveNextOnboardTier
25350
25621
  });
25351
- import { existsSync as existsSync27, statSync as statSync4 } from "fs";
25622
+ import { existsSync as existsSync28, statSync as statSync4 } from "fs";
25352
25623
  function flagSet(tier) {
25353
25624
  return Boolean(getConfigValue(TIER_CONFIG_KEYS[tier]));
25354
25625
  }
@@ -25375,7 +25646,7 @@ function hasProductionDataset(ctx) {
25375
25646
  if (source.startsWith("/") || /^[A-Za-z]:[\\/]/.test(source) || source.startsWith("~")) {
25376
25647
  return true;
25377
25648
  }
25378
- if (!source.includes(":") && existsSync27(source)) return true;
25649
+ if (!source.includes(":") && existsSync28(source)) return true;
25379
25650
  if (source.startsWith("csv:") || source.startsWith("file:") || source.startsWith("folder:")) {
25380
25651
  return true;
25381
25652
  }
@@ -25434,7 +25705,7 @@ function markDemoDataSeen() {
25434
25705
  }
25435
25706
  function pathLooksPresent(raw) {
25436
25707
  try {
25437
- return existsSync27(raw) && (statSync4(raw).isFile() || statSync4(raw).isDirectory());
25708
+ return existsSync28(raw) && (statSync4(raw).isFile() || statSync4(raw).isDirectory());
25438
25709
  } catch {
25439
25710
  return false;
25440
25711
  }
@@ -25839,7 +26110,7 @@ __export(onboard_exports, {
25839
26110
  profileExists: () => profileExists
25840
26111
  });
25841
26112
  import chalk25 from "chalk";
25842
- import { existsSync as existsSync28 } from "fs";
26113
+ import { existsSync as existsSync29 } from "fs";
25843
26114
  import { basename as basename7 } from "path";
25844
26115
  async function handler5(args, ctx) {
25845
26116
  const { flags } = parseArgs2(args, ["force", "skip-brand"]);
@@ -26114,7 +26385,7 @@ async function runProductionTier(session, ctx) {
26114
26385
  return "Production data skipped";
26115
26386
  }
26116
26387
  const resolved = resolveUserPath(trimmed);
26117
- if (!existsSync28(resolved)) {
26388
+ if (!existsSync29(resolved)) {
26118
26389
  console.log(" " + chalk25.red(`Path not found: ${resolved}`));
26119
26390
  console.log(" " + chalk25.dim("Try again with /onboard, or drop the path into the REPL."));
26120
26391
  return "Production path not found";
@@ -26521,7 +26792,7 @@ __export(new_exports, {
26521
26792
  handler: () => handler6
26522
26793
  });
26523
26794
  import chalk26 from "chalk";
26524
- import { existsSync as existsSync29 } from "fs";
26795
+ import { existsSync as existsSync30 } from "fs";
26525
26796
  import { basename as basename8 } from "path";
26526
26797
  async function handler6(args, ctx) {
26527
26798
  const { positional, flags } = parseArgs2(args, ["demo", "empty", "list-scenarios", "regen-taxonomy"]);
@@ -26543,7 +26814,7 @@ async function handler6(args, ctx) {
26543
26814
  console.error(chalk26.red(" Usage: /new <file.csv> | --demo [--scenario <name>] | --empty [--lens health|metrics]"));
26544
26815
  return;
26545
26816
  }
26546
- if (source.kind === "file" && !existsSync29(source.path)) {
26817
+ if (source.kind === "file" && !existsSync30(source.path)) {
26547
26818
  console.error(chalk26.red(` File not found: ${source.path}`));
26548
26819
  return;
26549
26820
  }
@@ -26770,7 +27041,7 @@ __export(end_exports, {
26770
27041
  handler: () => handler7
26771
27042
  });
26772
27043
  import chalk27 from "chalk";
26773
- import { existsSync as existsSync30 } from "fs";
27044
+ import { existsSync as existsSync31 } from "fs";
26774
27045
  async function handler7(args, ctx) {
26775
27046
  if (args.length > 0) {
26776
27047
  console.error(chalk27.red(" Usage: /end"));
@@ -26807,10 +27078,10 @@ async function handler7(args, ctx) {
26807
27078
  if (summary) {
26808
27079
  console.log(" " + chalk27.dim(summary));
26809
27080
  }
26810
- if (existsSync30(transcriptPathForSession(endedId))) {
27081
+ if (existsSync31(transcriptPathForSession(endedId))) {
26811
27082
  console.log(" " + chalk27.dim("Transcript: ") + chalk27.dim(transcriptPathForSession(endedId)));
26812
27083
  }
26813
- if (existsSync30(contextDocPathForSession(endedId))) {
27084
+ if (existsSync31(contextDocPathForSession(endedId))) {
26814
27085
  console.log(" " + chalk27.dim("Context brief: ") + chalk27.dim(contextDocPathForSession(endedId)));
26815
27086
  }
26816
27087
  console.log();
@@ -26831,8 +27102,8 @@ __export(session_exports, {
26831
27102
  handler: () => handler8
26832
27103
  });
26833
27104
  import chalk28 from "chalk";
26834
- import { join as join25 } from "path";
26835
- import { existsSync as existsSync31 } from "fs";
27105
+ import { join as join26 } from "path";
27106
+ import { existsSync as existsSync32 } from "fs";
26836
27107
  async function handler8(args, ctx) {
26837
27108
  const sub = args[0];
26838
27109
  if (!sub) return listSessionsView(ctx);
@@ -26935,7 +27206,7 @@ async function pickUp(idArg, ctx) {
26935
27206
  }
26936
27207
  resetContextForSwitch(ctx, {
26937
27208
  sessionId: target.id,
26938
- sessionFile: join25(getSessionsDir(), `${target.id}.json`),
27209
+ sessionFile: join26(getSessionsDir(), `${target.id}.json`),
26939
27210
  sessionName: session.name,
26940
27211
  messages: [...session.messages],
26941
27212
  conversation: session.thread ? [...session.thread] : [],
@@ -26950,7 +27221,8 @@ async function pickUp(idArg, ctx) {
26950
27221
  llm: session.llm ? { ...session.llm } : void 0,
26951
27222
  strategistState: session.strategist,
26952
27223
  thinkState: session.think,
26953
- pendingAsk: session.pending_ask
27224
+ pendingAsk: session.pending_ask,
27225
+ lastCraftJobId: session.last_craft_job_id
26954
27226
  });
26955
27227
  ctx.datasetPath = datasetPathForSession(target.id);
26956
27228
  await setActiveDbPath(ctx.datasetPath);
@@ -26968,7 +27240,11 @@ async function pickUp(idArg, ctx) {
26968
27240
  if (session.strategist && session.strategist.step !== "awaiting_analysis") {
26969
27241
  const objective = session.strategist.objective;
26970
27242
  console.log(
26971
- " " + 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.")
26972
27248
  );
26973
27249
  }
26974
27250
  if (session.think && session.think.step === "active") {
@@ -26978,7 +27254,7 @@ async function pickUp(idArg, ctx) {
26978
27254
  );
26979
27255
  }
26980
27256
  const contextPath = contextDocPathForSession(target.id);
26981
- if (existsSync31(contextPath)) {
27257
+ if (existsSync32(contextPath)) {
26982
27258
  console.log(" " + chalk28.dim("Context brief: ") + chalk28.dim(contextPath));
26983
27259
  }
26984
27260
  console.log();
@@ -26993,6 +27269,7 @@ var init_session = __esm({
26993
27269
  init_connection();
26994
27270
  init_theme();
26995
27271
  init_layout();
27272
+ init_store3();
26996
27273
  GENERIC_DATASET_RE = /^(demo dataset|.*\bdemo)$/i;
26997
27274
  }
26998
27275
  });
@@ -27269,7 +27546,7 @@ __export(report_exports, {
27269
27546
  handler: () => handler9
27270
27547
  });
27271
27548
  import chalk29 from "chalk";
27272
- import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync16 } from "fs";
27549
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync17 } from "fs";
27273
27550
  import { dirname as dirname5 } from "path";
27274
27551
  async function handler9(args, ctx) {
27275
27552
  const { flags } = parseArgs2(args);
@@ -27366,7 +27643,7 @@ async function handler9(args, ctx) {
27366
27643
  console.warn(chalk29.yellow(` Warning: writing report outside NTRP home (${dirname5(resolvedOutput)})`));
27367
27644
  }
27368
27645
  mkdirSync14(dirname5(resolvedOutput), { recursive: true });
27369
- writeFileSync16(resolvedOutput, rendered);
27646
+ writeFileSync17(resolvedOutput, rendered);
27370
27647
  console.log(chalk29.green(` Report written to ${resolvedOutput}`));
27371
27648
  } else if (rendered) {
27372
27649
  console.log(rendered);
@@ -27398,7 +27675,7 @@ var init_report2 = __esm({
27398
27675
 
27399
27676
  // src/output/notes-export.ts
27400
27677
  import { mkdirSync as mkdirSync15 } from "fs";
27401
- import { join as join26 } from "path";
27678
+ import { join as join27 } from "path";
27402
27679
  function exportToNotes(data) {
27403
27680
  const { computeResult, divergences, findings, exchanges } = data;
27404
27681
  const { aggregate, segments } = computeResult;
@@ -27408,7 +27685,7 @@ function exportToNotes(data) {
27408
27685
  const filename = `${dateStr}-${timeStr}-gtm-health.md`;
27409
27686
  const dir = data.dir ?? getArchiveKindDir("notes");
27410
27687
  mkdirSync15(dir, { recursive: true });
27411
- const filepath = join26(dir, filename);
27688
+ const filepath = join27(dir, filename);
27412
27689
  const severityTags = /* @__PURE__ */ new Set();
27413
27690
  for (const f of findings) severityTags.add(f.severity);
27414
27691
  const tags = ["ntrp", "gtm-health", ...severityTags];
@@ -27653,8 +27930,8 @@ __export(backmeup_exports, {
27653
27930
  });
27654
27931
  import chalk31 from "chalk";
27655
27932
  import Papa5 from "papaparse";
27656
- import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync17 } from "fs";
27657
- import { join as join27 } from "path";
27933
+ import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync18 } from "fs";
27934
+ import { join as join28 } from "path";
27658
27935
  function sanitizeCsvValue(value) {
27659
27936
  if (typeof value !== "string") return value;
27660
27937
  return CSV_FORMULA_RE.test(value) ? `'${value}` : value;
@@ -27690,7 +27967,7 @@ async function handler11(args, _ctx) {
27690
27967
  if (!isInsideNtrp(baseDir)) {
27691
27968
  console.warn(chalk31.yellow(` Warning: writing backup outside NTRP home (${baseDir})`));
27692
27969
  }
27693
- const folder = join27(baseDir, folderName);
27970
+ const folder = join28(baseDir, folderName);
27694
27971
  mkdirSync16(folder, { recursive: true });
27695
27972
  const generatedAt = now2.toISOString();
27696
27973
  let fileCount = 0;
@@ -27705,7 +27982,7 @@ async function handler11(args, _ctx) {
27705
27982
  "Total At Risk": health.total_value_at_risk != null ? formatCurrency(health.total_value_at_risk) : "N/A",
27706
27983
  "Generated At": generatedAt
27707
27984
  }));
27708
- writeFileSync17(join27(folder, "cover-sheet.csv"), Papa5.unparse(sanitizeCsvRows(coverRows)), "utf-8");
27985
+ writeFileSync18(join28(folder, "cover-sheet.csv"), Papa5.unparse(sanitizeCsvRows(coverRows)), "utf-8");
27709
27986
  fileCount++;
27710
27987
  if (findings.length > 0) {
27711
27988
  const findingsRows = findings.map((f) => ({
@@ -27715,7 +27992,7 @@ async function handler11(args, _ctx) {
27715
27992
  Finding: f.finding,
27716
27993
  "Recommended Plays": f.recommended_plays ? f.recommended_plays.map((p) => p.play_name).join("; ") : ""
27717
27994
  }));
27718
- writeFileSync17(join27(folder, "findings.csv"), Papa5.unparse(sanitizeCsvRows(findingsRows)), "utf-8");
27995
+ writeFileSync18(join28(folder, "findings.csv"), Papa5.unparse(sanitizeCsvRows(findingsRows)), "utf-8");
27719
27996
  fileCount++;
27720
27997
  }
27721
27998
  for (const vs of health.vital_signs) {
@@ -27725,7 +28002,7 @@ async function handler11(args, _ctx) {
27725
28002
  ...detail
27726
28003
  }));
27727
28004
  const filename = EVIDENCE_FILENAMES[vs.vital_sign] ?? `${vs.vital_sign}.csv`;
27728
- writeFileSync17(join27(folder, filename), Papa5.unparse(sanitizeCsvRows(rows)), "utf-8");
28005
+ writeFileSync18(join28(folder, filename), Papa5.unparse(sanitizeCsvRows(rows)), "utf-8");
27729
28006
  fileCount++;
27730
28007
  }
27731
28008
  const event = recordExportWrite({
@@ -27929,8 +28206,8 @@ var init_bundle = __esm({
27929
28206
  });
27930
28207
 
27931
28208
  // src/repositories/markdown.ts
27932
- import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync18 } from "fs";
27933
- import { basename as basename9, dirname as dirname6, join as join28, 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";
27934
28211
  import { stringify as stringifyYaml2 } from "yaml";
27935
28212
  function renderMarkdownFiles(pkg) {
27936
28213
  const bundleJson = JSON.stringify(pkg, null, 2) + "\n";
@@ -28153,9 +28430,9 @@ var init_markdown3 = __esm({
28153
28430
  mkdirSync17(root, { recursive: true });
28154
28431
  const written = [];
28155
28432
  for (const file of files) {
28156
- const absolutePath = join28(root, file.relativePath);
28433
+ const absolutePath = join29(root, file.relativePath);
28157
28434
  mkdirSync17(dirname6(absolutePath), { recursive: true });
28158
- writeFileSync18(absolutePath, file.contents, "utf-8");
28435
+ writeFileSync19(absolutePath, file.contents, "utf-8");
28159
28436
  written.push(absolutePath);
28160
28437
  }
28161
28438
  return {
@@ -28459,7 +28736,7 @@ __export(handoff_exports, {
28459
28736
  handler: () => handler13
28460
28737
  });
28461
28738
  import chalk33 from "chalk";
28462
- import { join as join29 } from "path";
28739
+ import { join as join30 } from "path";
28463
28740
  async function handler13(args, ctx) {
28464
28741
  const sub = args[0];
28465
28742
  if (!sub) {
@@ -28559,7 +28836,7 @@ async function runPublish2(args, ctx) {
28559
28836
  );
28560
28837
  if (sub === "propose" && !hasDir) {
28561
28838
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
28562
- const dir = join29(getArchiveKindDir("publish"), `ntrp-repository-${stamp}`);
28839
+ const dir = join30(getArchiveKindDir("publish"), `ntrp-repository-${stamp}`);
28563
28840
  publishArgs.push("--dir", dir);
28564
28841
  }
28565
28842
  const result = await publish(publishArgs, ctx);
@@ -29334,150 +29611,6 @@ var init_segment = __esm({
29334
29611
  }
29335
29612
  });
29336
29613
 
29337
- // src/services/strategist.ts
29338
- import { createHash as createHash2 } from "crypto";
29339
- function serializeGapAudit(audit) {
29340
- const lines = [`Can compute: ${audit.can_compute} (lens: ${audit.primary_lens})`];
29341
- for (const item of audit.satisfied) {
29342
- lines.push(`- HAVE ${item.label}: ${item.detail}`);
29343
- }
29344
- for (const item of audit.missing) {
29345
- lines.push(`- MISSING ${item.label}: ${item.why}`);
29346
- }
29347
- for (const item of audit.optional) {
29348
- lines.push(`- LIMITED ${item.label}: ${item.detail}`);
29349
- }
29350
- return lines.join("\n");
29351
- }
29352
- async function prepareStrategistInputs(ctx, objective) {
29353
- let snapshot = ctx.snapshot.computeResult;
29354
- if (!snapshot) {
29355
- snapshot = await computeFullHealth();
29356
- ctx.snapshot.computeResult = snapshot;
29357
- const divInput = snapshot.segments.map((s) => ({
29358
- segmentId: s.segment.id,
29359
- segmentName: s.segment.name,
29360
- result: s.result
29361
- }));
29362
- ctx.snapshot.divergences = detectDivergences(snapshot.aggregate, divInput).divergences;
29363
- }
29364
- const audit = ctx.gapAudit ?? await refreshGapAudit(ctx).catch(() => null);
29365
- const gapAuditBlock = audit ? serializeGapAudit(audit) : "";
29366
- const memoryBlock = await Promise.resolve().then(() => (init_store2(), store_exports2)).then((m) => m.buildMemoryBlock(objective)).catch(() => "");
29367
- let baselineBatchId = null;
29368
- try {
29369
- const reading = await getLatestHealthReading();
29370
- baselineBatchId = reading?.upload_batch_id ?? null;
29371
- } catch {
29372
- }
29373
- return {
29374
- snapshot,
29375
- divergences: ctx.snapshot.divergences,
29376
- gapAuditBlock,
29377
- memoryBlock,
29378
- baselineBatchId,
29379
- includeMetrics: true
29380
- };
29381
- }
29382
- function proposeObjectiveFromSnapshot(snapshot) {
29383
- const { aggregate } = snapshot;
29384
- const gating = aggregate.gating_vital_sign;
29385
- if (!gating) return null;
29386
- const vital = aggregate.vital_signs.find((v) => v.vital_sign === gating);
29387
- if (!vital) return null;
29388
- const label = VITAL_SIGN_LABELS[gating] ?? gating;
29389
- const dollar = vital.dollar_value != null && vital.dollar_value > 0 ? ` and recover the ${formatCurrency(vital.dollar_value)} ${vital.dollar_label ?? "at stake"}` : "";
29390
- return `Move ${label} from ${Math.round(vital.score)} to 60+${dollar} within 60 days`;
29391
- }
29392
- function slugify2(value) {
29393
- const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
29394
- return slug || `strategy-${Date.now()}`;
29395
- }
29396
- function outcomeToMetric(outcome) {
29397
- return {
29398
- name: outcome.metric,
29399
- target: outcome.target_range,
29400
- baseline: outcome.baseline,
29401
- timeframe: `by ${outcome.check_date}`
29402
- };
29403
- }
29404
- async function persistStrategistPlan(plan, opts = {}) {
29405
- await initSchema();
29406
- const slug = slugify2(plan.title);
29407
- const libraryPath = strategyLibraryPath(slug);
29408
- const linkedPlayIds = [...new Set(plan.workstreams.flatMap((ws) => ws.play_ids))];
29409
- const successMetrics = plan.workstreams.map((ws) => outcomeToMetric(ws.expected_outcome));
29410
- const leadingIndicators = plan.workstreams.flatMap((ws) => ws.leading_indicators.map(outcomeToMetric));
29411
- const recommendedActions = plan.workstreams.flatMap((ws) => ws.actions.map((action) => `[WS${ws.order}] ${action}`)).slice(0, 15);
29412
- const reviewProtocol = [
29413
- `Review ${plan.review_cadence.toLowerCase()} with /strategy review ${slug}.`,
29414
- `Check each milestone at its due date against the named verification method.`,
29415
- `At each outcome check date, compare the measured value to its target range against baseline batch ${opts.baselineBatchId ?? "(latest)"}.`,
29416
- `If a contingency trigger fires, activate the pre-agreed fallback.`
29417
- ].join(" ");
29418
- const id = await upsertStrategy({
29419
- slug,
29420
- title: plan.title,
29421
- status: opts.status ?? "active",
29422
- source_type: "agent",
29423
- source_path: null,
29424
- goal: plan.objective,
29425
- hypothesis: plan.hypothesis,
29426
- target_segment: plan.target_segment,
29427
- priority: plan.priority,
29428
- linked_play_ids: linkedPlayIds,
29429
- success_metrics: successMetrics,
29430
- leading_indicators: leadingIndicators,
29431
- risks: plan.risks,
29432
- recommended_actions: recommendedActions,
29433
- experiment_design: reviewProtocol,
29434
- review_cadence: plan.review_cadence,
29435
- confidence: plan.confidence,
29436
- raw_excerpt: plan.summary_30k,
29437
- library_path: libraryPath,
29438
- origin: "strategist",
29439
- objective: plan.objective,
29440
- constraints: plan.constraints,
29441
- workstreams: plan.workstreams,
29442
- assumptions: plan.assumptions,
29443
- baseline_batch_id: opts.baselineBatchId ?? null
29444
- });
29445
- const strategy = await getStrategyBySlugOrId(id);
29446
- if (!strategy) {
29447
- throw new NtrpError("strategy_persist_failed", "Strategy was not found after saving.", 1 /* RuntimeError */);
29448
- }
29449
- const writtenPath = writeStrategyMarkdown(strategy);
29450
- await insertStrategySource({
29451
- strategy_id: strategy.id,
29452
- source_type: "agent",
29453
- source_path: null,
29454
- content_hash: createHash2("sha256").update(JSON.stringify(plan)).digest("hex"),
29455
- extracted_text_excerpt: plan.summary_30k.slice(0, 800),
29456
- metadata: {
29457
- origin: "strategist",
29458
- objective: plan.objective,
29459
- workstream_count: plan.workstreams.length,
29460
- baseline_batch_id: opts.baselineBatchId ?? null
29461
- }
29462
- });
29463
- return { strategy: { ...strategy, library_path: writtenPath }, library_path: writtenPath };
29464
- }
29465
- var init_strategist = __esm({
29466
- "src/services/strategist.ts"() {
29467
- "use strict";
29468
- init_schema();
29469
- init_queries();
29470
- init_health_score();
29471
- init_divergence();
29472
- init_gap_audit();
29473
- init_library();
29474
- init_errors2();
29475
- init_types2();
29476
- init_formatters();
29477
- init_formatters();
29478
- }
29479
- });
29480
-
29481
29614
  // src/ai/strategist-prompt.ts
29482
29615
  function companyContextSection4() {
29483
29616
  const block = buildCompanyProfileBlock();
@@ -29616,6 +29749,52 @@ ${AAR_BLOCK}
29616
29749
 
29617
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.`;
29618
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
+ }
29619
29798
  var STRATEGIST_PLAN_SCHEMA_BLOCK;
29620
29799
  var init_strategist_prompt = __esm({
29621
29800
  "src/ai/strategist-prompt.ts"() {
@@ -30143,6 +30322,297 @@ var init_strategist_validate = __esm({
30143
30322
  }
30144
30323
  });
30145
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
+
30146
30616
  // src/ai/strategist.ts
30147
30617
  function buildHealthSnapshot(computeResult, divergences) {
30148
30618
  const { aggregate, segments } = computeResult;
@@ -30365,6 +30835,15 @@ Respond with ONLY the corrected plan JSON object in the required schema (title,
30365
30835
  for (const issue of validated.issues) {
30366
30836
  yield { type: "notice", text: issue };
30367
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
+ }
30368
30847
  yield {
30369
30848
  type: "plan",
30370
30849
  plan: validated.plan,
@@ -30401,7 +30880,7 @@ function describePlanValidationFailure(text, evidenceText, todayIso) {
30401
30880
  return "unknown validation failure";
30402
30881
  }
30403
30882
  var GROUND_MAX_ROUNDS, BACKCAST_MAX_ROUNDS, STRESS_MAX_ROUNDS, STAGE_MAX_TOKENS, PLAN_JSON_MAX_TOKENS, STAGE_LABELS;
30404
- var init_strategist2 = __esm({
30883
+ var init_strategist = __esm({
30405
30884
  "src/ai/strategist.ts"() {
30406
30885
  "use strict";
30407
30886
  init_types();
@@ -30416,6 +30895,7 @@ var init_strategist2 = __esm({
30416
30895
  init_thread();
30417
30896
  init_strategist_prompt();
30418
30897
  init_strategist_validate();
30898
+ init_strategist_rubric();
30419
30899
  init_strategist_prompt();
30420
30900
  GROUND_MAX_ROUNDS = 6;
30421
30901
  BACKCAST_MAX_ROUNDS = 4;
@@ -30430,6 +30910,803 @@ var init_strategist2 = __esm({
30430
30910
  }
30431
30911
  });
30432
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
+
30433
31710
  // src/output/strategy-brief.ts
30434
31711
  import chalk36 from "chalk";
30435
31712
  function printWrapped(text, width, prefix = INDENT, style) {
@@ -30521,6 +31798,40 @@ function printStrategyBrief(plan, stats) {
30521
31798
  console.log(`${INDENT}${coverageStyled}${chalk36.dim(` \xB7 ~${Math.round(totalHours)} total team hours across ${plan.workstreams.length} workstream${plan.workstreams.length === 1 ? "" : "s"}`)}`);
30522
31799
  console.log();
30523
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
+ }
30524
31835
  var INDENT;
30525
31836
  var init_strategy_brief = __esm({
30526
31837
  "src/output/strategy-brief.ts"() {
@@ -30535,7 +31846,9 @@ var init_strategy_brief = __esm({
30535
31846
  var strategist_flow_exports = {};
30536
31847
  __export(strategist_flow_exports, {
30537
31848
  extractObjectiveSeed: () => extractObjectiveSeed,
31849
+ handleKeepGoingLine: () => handleKeepGoingLine,
30538
31850
  handleStrategizeFlow: () => handleStrategizeFlow,
31851
+ isKeepGoingIntent: () => isKeepGoingIntent,
30539
31852
  isStrategistIntent: () => isStrategistIntent,
30540
31853
  promptQueuedAiStrategist: () => promptQueuedAiStrategist,
30541
31854
  queueStrategistForAnalysis: () => queueStrategistForAnalysis,
@@ -30558,7 +31871,8 @@ function queueStrategistForAnalysis(ctx, opts) {
30558
31871
  ctx.strategistState = {
30559
31872
  step: "awaiting_analysis",
30560
31873
  objective: opts.seed,
30561
- origin: opts.origin
31874
+ origin: opts.origin,
31875
+ mode: opts.mode
30562
31876
  };
30563
31877
  saveSessionState(ctx);
30564
31878
  console.log();
@@ -30583,7 +31897,7 @@ async function startStrategistFlow(ctx, opts) {
30583
31897
  objective = (snapshot ? proposeObjectiveFromSnapshot(snapshot) : null) ?? "";
30584
31898
  }
30585
31899
  if (!objective) {
30586
- ctx.strategistState = { step: "objective_input", origin: opts.origin };
31900
+ ctx.strategistState = { step: "objective_input", origin: opts.origin, mode: opts.mode };
30587
31901
  saveSessionState(ctx);
30588
31902
  console.log();
30589
31903
  console.log(" " + chalk37.dim('What is the objective? State a finish line. Example: "cut stale pipeline in half before Q4".'));
@@ -30591,7 +31905,7 @@ async function startStrategistFlow(ctx, opts) {
30591
31905
  recordMessage(ctx, "agent", "Strategist: asked for objective");
30592
31906
  return "Awaiting objective";
30593
31907
  }
30594
- ctx.strategistState = { step: "objective_confirm", objective, origin: opts.origin };
31908
+ ctx.strategistState = { step: "objective_confirm", objective, origin: opts.origin, mode: opts.mode };
30595
31909
  saveSessionState(ctx);
30596
31910
  printObjectiveCard(ctx, objective, !opts.seed);
30597
31911
  recordMessage(ctx, "agent", `Strategist objective proposed: ${objective}`);
@@ -30604,7 +31918,8 @@ async function resumeStrategistAfterCompute(ctx) {
30604
31918
  console.log(" " + paint("accent", "Analysis is ready. The strategy session continues."));
30605
31919
  await startStrategistFlow(ctx, {
30606
31920
  seed: state2.objective,
30607
- origin: state2.origin ?? "nl"
31921
+ origin: state2.origin ?? "nl",
31922
+ mode: state2.mode
30608
31923
  });
30609
31924
  }
30610
31925
  function promptQueuedAiStrategist(ctx) {
@@ -30614,6 +31929,64 @@ function promptQueuedAiStrategist(ctx) {
30614
31929
  }
30615
31930
  printObjectiveCard(ctx, state2.objective, true);
30616
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
+ }
30617
31990
  async function handleStrategizeFlow(input, ctx) {
30618
31991
  const state2 = ctx.strategistState;
30619
31992
  if (!state2) return;
@@ -30640,6 +32013,11 @@ async function handleStrategizeFlow(input, ctx) {
30640
32013
  printObjectiveCard(ctx, state2.objective, false);
30641
32014
  return "Objective proposed";
30642
32015
  }
32016
+ if (CRAFT_RE.test(line)) {
32017
+ state2.mode = "craft";
32018
+ saveSessionState(ctx);
32019
+ return runStrategistSession(ctx);
32020
+ }
30643
32021
  if (CONFIRM_RE.test(line)) {
30644
32022
  return runStrategistSession(ctx);
30645
32023
  }
@@ -30690,7 +32068,7 @@ async function runStrategistSession(ctx) {
30690
32068
  saveSessionState(ctx);
30691
32069
  return "Skeleton plan (awaiting connect)";
30692
32070
  }
30693
- if (ctx.rl && !state2.constraintsNote) {
32071
+ if (ctx.rl && !state2.constraintsNote && state2.mode !== "craft") {
30694
32072
  const prompts = createPromptSession(ctx.rl, ctx);
30695
32073
  try {
30696
32074
  const note = await prompts.ask(
@@ -30704,7 +32082,39 @@ async function runStrategistSession(ctx) {
30704
32082
  }
30705
32083
  }
30706
32084
  console.log();
30707
- 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
+ }
30708
32118
  let plan = null;
30709
32119
  let stats = { measurable_targets: 0, total_targets: 0 };
30710
32120
  let baselineBatchId = null;
@@ -30788,7 +32198,10 @@ async function runStrategistSession(ctx) {
30788
32198
  }
30789
32199
  if (saved) {
30790
32200
  try {
30791
- const persisted = await persistStrategistPlan(plan, { baselineBatchId });
32201
+ const persisted = await persistStrategistPlan(plan, {
32202
+ baselineBatchId,
32203
+ constraintLine: formatConstraintLine(ctx.snapshot.computeResult?.aggregate)
32204
+ });
30792
32205
  ctx.deliverables.push({
30793
32206
  kind: "strategy",
30794
32207
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -30836,19 +32249,54 @@ async function ensureSnapshot(ctx) {
30836
32249
  return null;
30837
32250
  }
30838
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
+ }
30839
32279
  function printObjectiveCard(ctx, objective, proposed) {
32280
+ const craftMode = ctx.strategistState?.mode === "craft";
30840
32281
  console.log();
30841
32282
  console.log(" " + chalk37.bold("Strategy session"));
30842
32283
  console.log(
30843
32284
  " " + chalk37.dim(proposed ? "Proposed objective: " : "Objective: ") + paint("accent", objective)
30844
32285
  );
30845
32286
  console.log(
30846
- " " + 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
+ )
30847
32290
  );
30848
32291
  console.log();
30849
32292
  console.log(
30850
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")
30851
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
+ }
30852
32300
  console.log();
30853
32301
  }
30854
32302
  async function printKeylessSkeletonPlan(ctx, objective) {
@@ -30910,7 +32358,7 @@ async function resumeStrategistAfterConnect(ctx) {
30910
32358
  printObjectiveCard(ctx, state2.objective, true);
30911
32359
  return true;
30912
32360
  }
30913
- 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;
30914
32362
  var init_strategist_flow = __esm({
30915
32363
  "src/conversation/strategist-flow.ts"() {
30916
32364
  "use strict";
@@ -30922,17 +32370,20 @@ var init_strategist_flow = __esm({
30922
32370
  init_prompts();
30923
32371
  init_health_score();
30924
32372
  init_divergence();
30925
- init_strategist();
30926
32373
  init_strategist2();
32374
+ init_strategist();
32375
+ init_strategist_run();
30927
32376
  init_strategy_brief();
30928
32377
  init_llm_attribution();
30929
32378
  init_time_bank();
30930
32379
  init_formatters();
32380
+ init_store3();
30931
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;
30932
32382
  CANCEL_RE = /^(cancel|stop|quit|abort|never\s?mind|nevermind|forget it)\s*[.!]?\s*$/i;
30933
32383
  CONFIRM_RE = /^(y|yes|yep|yeah|confirm|go|go ahead|do it|proceed|sounds good|looks good|lgtm|ok|okay)\b/i;
30934
32384
  ADJUST_RE = /^(n|no|adjust|change|edit|different|not quite|refine)\b/i;
30935
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;
30936
32387
  }
30937
32388
  });
30938
32389
 
@@ -30945,8 +32396,8 @@ __export(strategy_review_exports, {
30945
32396
  persistStrategyReview: () => persistStrategyReview,
30946
32397
  resolveReviewableStrategy: () => resolveReviewableStrategy
30947
32398
  });
30948
- import { writeFileSync as writeFileSync19 } from "fs";
30949
- import { join as join30 } from "path";
32399
+ import { writeFileSync as writeFileSync20 } from "fs";
32400
+ import { join as join31 } from "path";
30950
32401
  function groupByBatch(vitals, metrics) {
30951
32402
  const map = /* @__PURE__ */ new Map();
30952
32403
  const order = [];
@@ -31236,7 +32687,7 @@ async function persistStrategyReview(report, milestoneVerdicts, notes) {
31236
32687
  }
31237
32688
  function logWin(strategy, hits, milestoneWins) {
31238
32689
  const date = isoToday();
31239
- const path = join30(getWinsDir(), `${strategy.slug}-${date}.md`);
32690
+ const path = join31(getWinsDir(), `${strategy.slug}-${date}.md`);
31240
32691
  const lines = [
31241
32692
  `# Win \u2014 ${strategy.title}`,
31242
32693
  "",
@@ -31253,7 +32704,7 @@ function logWin(strategy, hits, milestoneWins) {
31253
32704
  for (const label of milestoneWins) lines.push(`- ${label}`);
31254
32705
  }
31255
32706
  lines.push("", "_Logged by /strategy review._", "");
31256
- writeFileSync19(path, lines.join("\n"));
32707
+ writeFileSync20(path, lines.join("\n"));
31257
32708
  return path;
31258
32709
  }
31259
32710
  var VITAL_TOKENS, COMPONENT_TOKENS;
@@ -31485,9 +32936,15 @@ __export(strategy_exports2, {
31485
32936
  });
31486
32937
  import chalk39 from "chalk";
31487
32938
  async function handler16(args, ctx) {
31488
- const { positional, flags } = parseArgs2(args, ["no-ai"]);
32939
+ const { positional, flags } = parseArgs2(args, ["no-ai", "save", "handoff"]);
31489
32940
  const first = positional[0];
31490
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
+ }
31491
32948
  if (interactive && (!first || !RESERVED_SUBCOMMANDS.has(first))) {
31492
32949
  const seed = positional.join(" ").trim() || void 0;
31493
32950
  const { startStrategistFlow: startStrategistFlow2 } = await Promise.resolve().then(() => (init_strategist_flow(), strategist_flow_exports));
@@ -31498,14 +32955,18 @@ async function handler16(args, ctx) {
31498
32955
  try {
31499
32956
  const result = await runStrategy(sub, positional, flags, ctx);
31500
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
+ }
31501
32962
  emitResult("strategy", result);
31502
32963
  return;
31503
32964
  }
31504
32965
  renderStrategyResult(result);
31505
32966
  } catch (err) {
31506
- if (isStructuredOutput(ctx.execution)) emitError("strategy", err);
32967
+ if (isStructuredOutput(ctx.execution)) emitError(first === "craft" ? "strategy.craft" : "strategy", err);
31507
32968
  console.error(chalk39.red(` ${err instanceof Error ? err.message : String(err)}`));
31508
- process.exit(1);
32969
+ process.exit(err instanceof NtrpError ? err.exitCode : 1);
31509
32970
  }
31510
32971
  }
31511
32972
  async function runStrategy(sub, positional, flags, ctx) {
@@ -31580,6 +33041,8 @@ async function runStrategy(sub, positional, flags, ctx) {
31580
33041
  const { runStrategyReview: runStrategyReview2 } = await Promise.resolve().then(() => (init_strategy_review_cmd(), strategy_review_cmd_exports));
31581
33042
  return { action: "review", ...await runStrategyReview2(ctx, positional[1]) };
31582
33043
  }
33044
+ case "craft":
33045
+ return runCraft(positional, flags, ctx);
31583
33046
  default:
31584
33047
  throw new NtrpError("unknown_strategy_subcommand", `Unknown strategy subcommand: ${sub}`, 2 /* Usage */);
31585
33048
  }
@@ -31587,8 +33050,79 @@ async function runStrategy(sub, positional, flags, ctx) {
31587
33050
  function shouldSpin(ctx) {
31588
33051
  return !isStructuredOutput(ctx.execution) && ctx.execution.progress;
31589
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
+ }
31590
33121
  function renderStrategyResult(result) {
31591
33122
  switch (result.action) {
33123
+ case "craft":
33124
+ renderCraftResult(result);
33125
+ return;
31592
33126
  case "review":
31593
33127
  return;
31594
33128
  case "ingest":
@@ -31621,6 +33155,31 @@ function renderStrategyResult(result) {
31621
33155
  return;
31622
33156
  }
31623
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
+ }
31624
33183
  function printStrategyList(strategies) {
31625
33184
  console.log(chalk39.bold("\n Strategies\n"));
31626
33185
  if (strategies.length === 0) {
@@ -31731,7 +33290,10 @@ var init_strategy2 = __esm({
31731
33290
  init_types2();
31732
33291
  init_strategy();
31733
33292
  init_repl_api();
31734
- 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"]);
31735
33297
  }
31736
33298
  });
31737
33299
 
@@ -32650,15 +34212,15 @@ var init_checkout = __esm({
32650
34212
  });
32651
34213
 
32652
34214
  // src/services/setup.ts
32653
- import { existsSync as existsSync32, mkdirSync as mkdirSync18, readFileSync as readFileSync22, writeFileSync as writeFileSync20 } from "fs";
32654
- import { join as join31 } 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";
32655
34217
  function setupCheck() {
32656
34218
  const home = ntrpHome();
32657
34219
  let writable = false;
32658
34220
  try {
32659
34221
  mkdirSync18(home, { recursive: true });
32660
- const probe = join31(home, ".write-check");
32661
- writeFileSync20(probe, "ok\n");
34222
+ const probe = join32(home, ".write-check");
34223
+ writeFileSync21(probe, "ok\n");
32662
34224
  writable = true;
32663
34225
  } catch {
32664
34226
  writable = false;
@@ -32697,7 +34259,7 @@ function setupCheck() {
32697
34259
  };
32698
34260
  }
32699
34261
  function readProfileInput(pathOrDash) {
32700
- const raw = pathOrDash === "-" ? readFileSync22(0, "utf-8") : readFileSync22(pathOrDash, "utf-8");
34262
+ const raw = pathOrDash === "-" ? readFileSync23(0, "utf-8") : readFileSync23(pathOrDash, "utf-8");
32701
34263
  return JSON.parse(raw);
32702
34264
  }
32703
34265
  function writeAgentProfile(input) {
@@ -33487,7 +35049,7 @@ var init_orchestrator = __esm({
33487
35049
  });
33488
35050
 
33489
35051
  // src/services/smoke-protocol.ts
33490
- import { join as join32 } from "path";
35052
+ import { join as join33 } from "path";
33491
35053
  function isSmokeProtocolTrigger(input) {
33492
35054
  return normalize2(input).includes(SMOKE_TRIGGER_PHRASE);
33493
35055
  }
@@ -33523,7 +35085,7 @@ async function runSmokeProtocol(_input, ctx) {
33523
35085
  });
33524
35086
  const proposalResult = await proposeRepositoryExport({
33525
35087
  target: "markdown",
33526
- directory: join32(getExportsDir(), "repository-smoke"),
35088
+ directory: join33(getExportsDir(), "repository-smoke"),
33527
35089
  source: "smoke_protocol",
33528
35090
  modelOrFixture: "smoke-protocol-v1"
33529
35091
  });
@@ -34455,8 +36017,8 @@ var init_recall = __esm({
34455
36017
 
34456
36018
  // src/memory/feedback.ts
34457
36019
  import { appendFileSync as appendFileSync7 } from "fs";
34458
- import { join as join33 } from "path";
34459
- import { randomUUID as randomUUID8 } from "crypto";
36020
+ import { join as join34 } from "path";
36021
+ import { randomUUID as randomUUID9 } from "crypto";
34460
36022
  function summarize(text) {
34461
36023
  return text.replace(/[#*`>_]/g, "").replace(/\s+/g, " ").trim().slice(0, 200);
34462
36024
  }
@@ -34478,7 +36040,7 @@ function findCalibrationToSupersede(question, note) {
34478
36040
  }
34479
36041
  function recordFeedback(input) {
34480
36042
  const entry = {
34481
- id: randomUUID8(),
36043
+ id: randomUUID9(),
34482
36044
  rating: input.rating,
34483
36045
  question: scrubText(input.question).slice(0, 300),
34484
36046
  answer_summary: scrubText(summarize(input.answer)),
@@ -34487,7 +36049,7 @@ function recordFeedback(input) {
34487
36049
  created_at: (/* @__PURE__ */ new Date()).toISOString()
34488
36050
  };
34489
36051
  try {
34490
- appendFileSync7(join33(getMemoryDir(), "feedback.jsonl"), JSON.stringify(entry) + "\n");
36052
+ appendFileSync7(join34(getMemoryDir(), "feedback.jsonl"), JSON.stringify(entry) + "\n");
34491
36053
  } catch {
34492
36054
  }
34493
36055
  if (input.rating === "positive") {
@@ -34657,7 +36219,7 @@ __export(sessions_exports, {
34657
36219
  handler: () => handler35
34658
36220
  });
34659
36221
  import chalk62 from "chalk";
34660
- import { existsSync as existsSync33 } from "fs";
36222
+ import { existsSync as existsSync34 } from "fs";
34661
36223
  async function handler35(args, _ctx) {
34662
36224
  const sub = args[0] ?? "list";
34663
36225
  if (sub === "list" || !args[0]) {
@@ -34764,12 +36326,12 @@ function showSession(idArg) {
34764
36326
  console.log();
34765
36327
  const transcriptPath = transcriptPathForSession(session.id);
34766
36328
  const contextPath = contextDocPathForSession(session.id);
34767
- if (existsSync33(transcriptPath) || existsSync33(contextPath)) {
36329
+ if (existsSync34(transcriptPath) || existsSync34(contextPath)) {
34768
36330
  console.log(" " + chalk62.dim("\u2500".repeat(40)));
34769
- if (existsSync33(contextPath)) {
36331
+ if (existsSync34(contextPath)) {
34770
36332
  console.log(" " + chalk62.dim("Context brief: ") + chalk62.dim(contextPath));
34771
36333
  }
34772
- if (existsSync33(transcriptPath)) {
36334
+ if (existsSync34(transcriptPath)) {
34773
36335
  console.log(" " + chalk62.dim("Full transcript: ") + chalk62.dim(transcriptPath));
34774
36336
  }
34775
36337
  console.log();
@@ -34922,7 +36484,7 @@ var switch_exports = {};
34922
36484
  __export(switch_exports, {
34923
36485
  handler: () => handler38
34924
36486
  });
34925
- import { join as join34 } from "path";
36487
+ import { join as join35 } from "path";
34926
36488
  import chalk65 from "chalk";
34927
36489
  async function handler38(args, ctx) {
34928
36490
  if (args.length === 0) {
@@ -34952,7 +36514,7 @@ async function handler38(args, ctx) {
34952
36514
  }
34953
36515
  const context = buildSwitchContext(session);
34954
36516
  const newId = makeSessionId();
34955
- const newFile = join34(getSessionsDir(), `${newId}.json`);
36517
+ const newFile = join35(getSessionsDir(), `${newId}.json`);
34956
36518
  resetContextForSwitch(ctx, {
34957
36519
  sessionId: newId,
34958
36520
  sessionFile: newFile,
@@ -34965,7 +36527,8 @@ async function handler38(args, ctx) {
34965
36527
  stage: session.stage,
34966
36528
  dataset: session.dataset,
34967
36529
  deliverables: session.deliverables,
34968
- llm: session.llm ? { ...session.llm } : void 0
36530
+ llm: session.llm ? { ...session.llm } : void 0,
36531
+ lastCraftJobId: session.last_craft_job_id
34969
36532
  });
34970
36533
  console.log();
34971
36534
  console.log(" " + paint("accent", `Switched to "${targetName}"`));
@@ -34978,7 +36541,7 @@ async function handler38(args, ctx) {
34978
36541
  return `Switched to "${targetName}"`;
34979
36542
  } else {
34980
36543
  const newId = makeSessionId();
34981
- const newFile = join34(getSessionsDir(), `${newId}.json`);
36544
+ const newFile = join35(getSessionsDir(), `${newId}.json`);
34982
36545
  resetContextForSwitch(ctx, {
34983
36546
  sessionId: newId,
34984
36547
  sessionFile: newFile,
@@ -36118,8 +37681,8 @@ __export(relaunch_exports, {
36118
37681
  resolveRelaunchEntry: () => resolveRelaunchEntry,
36119
37682
  updateRestartSummary: () => updateRestartSummary
36120
37683
  });
36121
- import { existsSync as existsSync34 } from "fs";
36122
- import { join as join35 } from "path";
37684
+ import { existsSync as existsSync35 } from "fs";
37685
+ import { join as join36 } from "path";
36123
37686
  import { fileURLToPath as fileURLToPath2 } from "url";
36124
37687
  import { spawnSync } from "child_process";
36125
37688
  function encodeJustUpdated(fromVersion, toVersion) {
@@ -36143,8 +37706,8 @@ function updateRestartSummary(toVersion) {
36143
37706
  function npmGlobalEntry() {
36144
37707
  const listed = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
36145
37708
  if (listed.status !== 0) return null;
36146
- const entry = join35(listed.stdout.trim(), NPM_PACKAGE, "dist/index.js");
36147
- return existsSync34(entry) ? entry : null;
37709
+ const entry = join36(listed.stdout.trim(), NPM_PACKAGE, "dist/index.js");
37710
+ return existsSync35(entry) ? entry : null;
36148
37711
  }
36149
37712
  function thisBundleEntry() {
36150
37713
  return fileURLToPath2(import.meta.url);
@@ -36154,10 +37717,10 @@ function resolveRelaunchEntry(toVersion) {
36154
37717
  (p) => Boolean(p)
36155
37718
  );
36156
37719
  for (const entry of candidates) {
36157
- if (!existsSync34(entry)) continue;
37720
+ if (!existsSync35(entry)) continue;
36158
37721
  if (readVersionNearEntry(entry) === toVersion) return entry;
36159
37722
  }
36160
- return candidates.find((p) => existsSync34(p)) ?? thisBundleEntry();
37723
+ return candidates.find((p) => existsSync35(p)) ?? thisBundleEntry();
36161
37724
  }
36162
37725
  function relaunchArgv(toVersion) {
36163
37726
  return [resolveRelaunchEntry(toVersion)];
@@ -36388,10 +37951,10 @@ function renderProgressReport() {
36388
37951
  `${chalk74.dim("Deliverables")} ${chalk74.bold(String(usage5.deliverables))}`,
36389
37952
  `${chalk74.dim("AI exchanges")} ${chalk74.bold(String(usage5.nl_exchanges))}`
36390
37953
  ]);
36391
- const totalTokens = usage5.input_tokens + usage5.output_tokens;
37954
+ const totalTokens2 = usage5.input_tokens + usage5.output_tokens;
36392
37955
  printCard("AI usage", [
36393
37956
  `${chalk74.dim("LLM calls")} ${chalk74.bold(String(usage5.llm_calls))}`,
36394
- `${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)`
36395
37958
  ]);
36396
37959
  const weeks = [...usage5.weekly].sort((a, b) => a.week.localeCompare(b.week)).slice(-8);
36397
37960
  const weekHours = weeks.map((w) => w.minutes_saved / 60);
@@ -36697,8 +38260,8 @@ __export(exports_exports, {
36697
38260
  handler: () => handler48
36698
38261
  });
36699
38262
  import chalk78 from "chalk";
36700
- import { existsSync as existsSync35 } from "fs";
36701
- import { join as join36 } from "path";
38263
+ import { existsSync as existsSync36 } from "fs";
38264
+ import { join as join37 } from "path";
36702
38265
  function usage4() {
36703
38266
  console.log(chalk78.dim(" Usage:"));
36704
38267
  console.log(chalk78.dim(" /exports list [kind]"));
@@ -36755,7 +38318,7 @@ function printInboxShow() {
36755
38318
  console.log(" " + paint("accent", "AI inbox: ") + inbox);
36756
38319
  const latest = inboxLatestHandoffPath();
36757
38320
  if (latest) console.log(" " + chalk78.dim("Latest handoff: ") + latest);
36758
- console.log(" " + chalk78.dim("Finder skill: ") + join36(inbox, "SKILL.md"));
38321
+ console.log(" " + chalk78.dim("Finder skill: ") + join37(inbox, "SKILL.md"));
36759
38322
  console.log(" " + chalk78.dim("Reprint: ") + paint("accent", "/inbox skill"));
36760
38323
  } else {
36761
38324
  console.log(" " + chalk78.dim("AI inbox: (not set). Type ") + paint("accent", "/inbox set <folder>"));
@@ -36777,8 +38340,8 @@ function printOpen() {
36777
38340
  console.log(" " + chalk78.dim("AI inbox: ") + inbox);
36778
38341
  const latest = inboxLatestHandoffPath();
36779
38342
  if (latest) console.log(" " + chalk78.dim("Inbox latest: ") + latest);
36780
- console.log(" " + chalk78.dim("Pickup skill: ") + join36(inbox, "latest-pickup.md"));
36781
- 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"));
36782
38345
  } else {
36783
38346
  console.log(" " + chalk78.dim("AI inbox is not set. Type ") + paint("accent", "/inbox set <folder>"));
36784
38347
  const loc = handoffLocations();
@@ -36856,7 +38419,7 @@ function runMove(args, ctx) {
36856
38419
  }
36857
38420
  try {
36858
38421
  const destDir = resolveUserPath(dest);
36859
- if (!existsSync35(destDir)) {
38422
+ if (!existsSync36(destDir)) {
36860
38423
  }
36861
38424
  const event = moveExport(idOrName, destDir);
36862
38425
  console.log();
@@ -37502,7 +39065,7 @@ This command is hidden. Type \`/ingest --demo\` instead. That command calls this
37502
39065
  name: strategy
37503
39066
  description: Make a measured strategy from your data
37504
39067
  section: More
37505
- args: [objective] | [list|show|review|ingest|add|sync|sources] [args]
39068
+ args: [objective] | [list|show|review|ingest|add|sync|sources|craft] [args]
37506
39069
  handler: ../commands/strategy.ts
37507
39070
  ---
37508
39071
 
@@ -37511,6 +39074,8 @@ NTRP uses live data. It works back from the objective.
37511
39074
  The result is sequenced workstreams with dated milestones, deliverables, outcome ranges, and a contingency per workstream.
37512
39075
  Saved plans go to the strategy library. Later answers use them.
37513
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>\`.
37514
39079
 
37515
39080
  Library commands: \`/strategy list\`, \`/strategy show <slug>\`.
37516
39081
  Type \`/strategy ingest <file>\` for markdown, YAML, PDF, text, or \`-\` for stdin.
@@ -37892,8 +39457,8 @@ Remaining nuances merge into a custom_context paragraph that flows into all AI s
37892
39457
  });
37893
39458
 
37894
39459
  // src/ai/prompt-parts.ts
37895
- import { existsSync as existsSync36, readFileSync as readFileSync23 } from "fs";
37896
- import { join as join37 } from "path";
39460
+ import { existsSync as existsSync37, readFileSync as readFileSync24 } from "fs";
39461
+ import { join as join38 } from "path";
37897
39462
  function buildCompanyProfileBlock() {
37898
39463
  const p = loadProfile();
37899
39464
  if (!p) return "";
@@ -37911,10 +39476,10 @@ function buildCompanyProfileBlock() {
37911
39476
  return lines.join("\n");
37912
39477
  }
37913
39478
  function loadAnalystFile() {
37914
- const path = join37(ntrpHome(), ANALYST_FILE_NAME);
39479
+ const path = join38(ntrpHome(), ANALYST_FILE_NAME);
37915
39480
  try {
37916
- if (!existsSync36(path)) return null;
37917
- const raw = sanitizeExternalText(readFileSync23(path, "utf-8").trim());
39481
+ if (!existsSync37(path)) return null;
39482
+ const raw = sanitizeExternalText(readFileSync24(path, "utf-8").trim());
37918
39483
  if (!raw) return null;
37919
39484
  if (raw.length <= ANALYST_FILE_MAX_CHARS) return raw;
37920
39485
  const head = raw.slice(0, Math.floor(ANALYST_FILE_MAX_CHARS * 0.75));
@@ -38448,8 +40013,8 @@ __export(ingest_chat_exports, {
38448
40013
  loadDemoFromChat: () => loadDemoFromChat,
38449
40014
  looksLikeFilePath: () => looksLikeFilePath
38450
40015
  });
38451
- import { existsSync as existsSync37, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
38452
- 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";
38453
40018
  import { homedir as homedir8 } from "os";
38454
40019
  import chalk80 from "chalk";
38455
40020
  function extractFilePath(input) {
@@ -38471,7 +40036,7 @@ function extractFilePath(input) {
38471
40036
  if (!candidate) continue;
38472
40037
  if (!looksLikePathToken(candidate)) continue;
38473
40038
  const p = expandPath(candidate);
38474
- if (existsSync37(p)) {
40039
+ if (existsSync38(p)) {
38475
40040
  try {
38476
40041
  const st = statSync5(p);
38477
40042
  if (st.isFile() || st.isDirectory()) return p;
@@ -38500,7 +40065,7 @@ function looksLikeFilePath(input) {
38500
40065
  function listCsvsInFolder(dir) {
38501
40066
  try {
38502
40067
  if (!statSync5(dir).isDirectory()) return [];
38503
- 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();
38504
40069
  } catch {
38505
40070
  return [];
38506
40071
  }
@@ -38582,12 +40147,12 @@ async function ingestFromChat(ctx, filePath) {
38582
40147
  }
38583
40148
  const { handler: ingest } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
38584
40149
  const { detectEntityType: detectEntityType2 } = await Promise.resolve().then(() => (init_csv_detect(), csv_detect_exports));
38585
- const { readFileSync: readFileSync24 } = await import("fs");
40150
+ const { readFileSync: readFileSync25 } = await import("fs");
38586
40151
  const { parseCSV: parseCSV2 } = await Promise.resolve().then(() => (init_csv_parse(), csv_parse_exports));
38587
40152
  const { getStoredApiKey } = await Promise.resolve().then(() => (init_repl_api(), repl_api_exports));
38588
40153
  let headerCheckFailed = false;
38589
40154
  try {
38590
- const raw = readFileSync24(filePath, "utf-8");
40155
+ const raw = readFileSync25(filePath, "utf-8");
38591
40156
  const { headers } = parseCSV2(raw);
38592
40157
  const detected = detectEntityType2(headers, "unknown");
38593
40158
  if (!detected) headerCheckFailed = true;
@@ -39585,6 +41150,10 @@ async function conversationRouter(input, ctx) {
39585
41150
  const summary = await handleStrategizeFlow(line, ctx) ?? void 0;
39586
41151
  return { handled: true, summary };
39587
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
+ }
39588
41157
  if (phase === "think") {
39589
41158
  const summary = await handleThinkFlow(line, ctx) ?? void 0;
39590
41159
  return { handled: true, summary };
@@ -39895,7 +41464,7 @@ var init_loop_guard2 = __esm({
39895
41464
  },
39896
41465
  strategize: {
39897
41466
  mode: "strategy objective confirm",
39898
- 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",
39899
41468
  leave: "cancel drops the strategy session and returns to Q&A"
39900
41469
  }
39901
41470
  };
@@ -40380,7 +41949,8 @@ var init_deepdive_complete = __esm({
40380
41949
  "inbox",
40381
41950
  "claude",
40382
41951
  "skill",
40383
- "strategy"
41952
+ "strategy",
41953
+ "keep-going"
40384
41954
  ];
40385
41955
  }
40386
41956
  });
@@ -40491,7 +42061,7 @@ __export(repl_exports, {
40491
42061
  import { createInterface as createInterface2 } from "readline/promises";
40492
42062
  import { clearLine as clearLine2, cursorTo as cursorTo2 } from "readline";
40493
42063
  import chalk88 from "chalk";
40494
- import { join as join39 } from "path";
42064
+ import { join as join40 } from "path";
40495
42065
  function buildPrompt(ctx) {
40496
42066
  return buildConversationPrompt(ctx);
40497
42067
  }
@@ -40819,14 +42389,14 @@ function printHelp() {
40819
42389
  console.log(" " + chalk88.dim("Type the question. You do not need a slash command."));
40820
42390
  console.log(" " + chalk88.dim("Paste a CSV path or type ") + paint("accent", '"use demo data"') + chalk88.dim(" to load data."));
40821
42391
  console.log(" " + chalk88.dim("After analysis, type questions in English."));
40822
- 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."));
40823
42393
  console.log(" " + chalk88.dim("Type ") + paint("accent", '"ship a board deck"') + chalk88.dim(" to write a handoff."));
40824
42394
  console.log(" " + chalk88.dim("The ") + paint("accent", "\u203A") + chalk88.dim(" prompt shows brief or deep after analysis. Brief is the default."));
40825
42395
  console.log();
40826
42396
  console.log(" " + sectionHeading("Keys"));
40827
42397
  console.log(
40828
42398
  " " + paint("accent", "\u23CE") + chalk88.dim(
40829
- " Accepts the default. At the main prompt it runs the armed action (yes, use demo data, go ahead, /connect, /update)."
42399
+ " Accepts the default. At the main prompt it runs the armed action (yes, use demo data, go ahead, /connect, /update, keep going)."
40830
42400
  )
40831
42401
  );
40832
42402
  console.log(
@@ -40877,7 +42447,7 @@ function printHelp() {
40877
42447
  ["/remember <fact>", "Store a fact, a decision, or a preference"],
40878
42448
  ["/recall [topic]", "Show what NTRP stores about your business"],
40879
42449
  ["/rate good|bad <note>", "Correct the last answer. A bad note becomes a calibration"],
40880
- [`${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)"]
40881
42451
  ];
40882
42452
  const teachMaxW = Math.max(...teach.map(([c]) => c.length)) + 2;
40883
42453
  for (const [cmd, desc] of teach) {