@elitedcs/ghl-mcp 3.93.1 → 3.94.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1717,12 +1717,12 @@ function looksLikeCredential(value) {
1717
1717
  function maskUrl(url, rec2 = newRecorder(), path67 = []) {
1718
1718
  let out = url.replace(USERINFO_RE, (_m, pre, pass, at) => `${pre}${maskCredential(pass, rec2, path67, "@")}${at}`);
1719
1719
  const seen = /* @__PURE__ */ new Map();
1720
- out = out.replace(QUERY_RE, (m, sep6, name2, val) => {
1720
+ out = out.replace(QUERY_RE, (m, sep7, name2, val) => {
1721
1721
  const decoded2 = decodeName(name2);
1722
1722
  const n = seen.get(decoded2) ?? 0;
1723
1723
  seen.set(decoded2, n + 1);
1724
1724
  if (!val || !isSensitiveName(decoded2)) return m;
1725
- return `${sep6}${name2}=${maskCredential(val, rec2, path67, `?${decoded2}#${n}`)}`;
1725
+ return `${sep7}${name2}=${maskCredential(val, rec2, path67, `?${decoded2}#${n}`)}`;
1726
1726
  });
1727
1727
  return out;
1728
1728
  }
@@ -1911,7 +1911,7 @@ function redactText(text3) {
1911
1911
  out = out.replace(/\b(Bearer|Basic)(\s+)([A-Za-z0-9._~+/=-]{8,})/g, (_m, s, sp, v) => `${s}${sp}${maskToken(v)}`);
1912
1912
  out = out.replace(
1913
1913
  /\b([A-Za-z0-9_-]*(?:key|token|secret|auth|signature|passw)[A-Za-z0-9_-]*)(\s*[:=]\s*)([^\s,;"'&<>]{8,})/gi,
1914
- (m, name2, sep6, val) => MASK_PRESENT_RE.test(val) || /^(bearer|basic)$/i.test(val) || !isSensitiveName(name2) ? m : `${name2}${sep6}${maskToken(val)}`
1914
+ (m, name2, sep7, val) => MASK_PRESENT_RE.test(val) || /^(bearer|basic)$/i.test(val) || !isSensitiveName(name2) ? m : `${name2}${sep7}${maskToken(val)}`
1915
1915
  );
1916
1916
  return out;
1917
1917
  }
@@ -2020,11 +2020,11 @@ function restoreMaskedSecrets(incoming, current) {
2020
2020
  };
2021
2021
  let out = str16.replace(USERINFO_RE, (m, pre, pass, at) => MASK_PRESENT_RE.test(pass) ? `${pre}${whole(pass, "@")}${at}` : m);
2022
2022
  const seen = /* @__PURE__ */ new Map();
2023
- out = out.replace(QUERY_RE, (m, sep6, name2, val) => {
2023
+ out = out.replace(QUERY_RE, (m, sep7, name2, val) => {
2024
2024
  const decoded2 = decodeName(name2);
2025
2025
  const n = seen.get(decoded2) ?? 0;
2026
2026
  seen.set(decoded2, n + 1);
2027
- return MASK_PRESENT_RE.test(val) ? `${sep6}${name2}=${whole(val, `?${decoded2}#${n}`)}` : m;
2027
+ return MASK_PRESENT_RE.test(val) ? `${sep7}${name2}=${whole(val, `?${decoded2}#${n}`)}` : m;
2028
2028
  });
2029
2029
  if (MASK_PRESENT_RE.test(out)) out = whole(out, "=");
2030
2030
  return out;
@@ -10998,18 +10998,18 @@ async function readAgent(client, agentId) {
10998
10998
  return a;
10999
10999
  }
11000
11000
  async function updateAgentFields(client, agentId, change) {
11001
- const clean5 = {};
11002
- for (const [k, val] of Object.entries(change)) if (val !== void 0) clean5[k] = val;
11003
- if (Object.keys(clean5).length === 0) throw new Error("Nothing to update \u2014 pass at least one field to change.");
11001
+ const clean6 = {};
11002
+ for (const [k, val] of Object.entries(change)) if (val !== void 0) clean6[k] = val;
11003
+ if (Object.keys(clean6).length === 0) throw new Error("Nothing to update \u2014 pass at least one field to change.");
11004
11004
  const before = await readAgent(client, agentId);
11005
- const body = { ...promptTrio(before), ...clean5 };
11005
+ const body = { ...promptTrio(before), ...clean6 };
11006
11006
  const merged = promptTrio(body);
11007
11007
  if (!merged.personality || !merged.goal || !merged.instructions) {
11008
11008
  throw new Error(`Agent ${agentId} read back without its personality/goal/instructions, and GoHighLevel refuses an update that does not carry all three. Pass all three explicitly (get_conversation_agent shows what is stored).`);
11009
11009
  }
11010
11010
  await client.put(`/conversation-ai/agents/${agentId}`, { version: AI_API_VERSION, body });
11011
11011
  const after = await readAgent(client, agentId);
11012
- return { before, changed: Object.keys(clean5), after };
11012
+ return { before, changed: Object.keys(clean6), after };
11013
11013
  }
11014
11014
  function registerConversationAiTools(server2, client) {
11015
11015
  const v = { version: AI_API_VERSION };
@@ -21145,12 +21145,12 @@ function deriveBrandDna(raw, opts = {}) {
21145
21145
  fixed = true;
21146
21146
  }
21147
21147
  const vb = svg.match(/viewBox=["']\s*[\d.-]+\s+[\d.-]+\s+([\d.]+)\s+([\d.]+)/i);
21148
- const clean5 = sanitizeSvgOrDrop(svg);
21149
- if (clean5) {
21150
- logoSvg = clean5;
21151
- logo = { inlineSvg: clean5, whiteFillFixed: fixed, source: bl.where, rights: opts.rights ? "declared" : "inherits", ...vb && { width: parseFloat(vb[1]), height: parseFloat(vb[2]) } };
21148
+ const clean6 = sanitizeSvgOrDrop(svg);
21149
+ if (clean6) {
21150
+ logoSvg = clean6;
21151
+ logo = { inlineSvg: clean6, whiteFillFixed: fixed, source: bl.where, rights: opts.rights ? "declared" : "inherits", ...vb && { width: parseFloat(vb[1]), height: parseFloat(vb[2]) } };
21152
21152
  declared++;
21153
- evidence["logo"] = `${bl.where}${fixed ? "; white fills replaced with currentColor" : ""}${clean5 !== svg ? "; scripts, handlers and outside references stripped" : ""}`;
21153
+ evidence["logo"] = `${bl.where}${fixed ? "; white fills replaced with currentColor" : ""}${clean6 !== svg ? "; scripts, handlers and outside references stripped" : ""}`;
21154
21154
  } else {
21155
21155
  evidence["logo"] = `${bl.where}: not a plain drawing (or over 60 KB), so it was not kept`;
21156
21156
  }
@@ -24340,6 +24340,8 @@ var init_features = __esm({
24340
24340
  BY_WORKFLOW_REF = {
24341
24341
  "workflow.speed_to_lead": "speed_to_lead",
24342
24342
  "workflow.missed_call": "missed_call",
24343
+ // F6 (2026-10-29): a missed caller with no card gets one. Goes with the text-back it hangs off.
24344
+ "workflow.missed_call_card": "missed_call",
24343
24345
  "workflow.lead_nurture": "nurture",
24344
24346
  "workflow.win_back": "winback",
24345
24347
  "workflow.appointment_booked": "booking",
@@ -26084,10 +26086,14 @@ var init_registry = __esm({
26084
26086
  whatItIs: "Google sign-in gives this machine permission to read your calendar without pasting a password here.",
26085
26087
  checkedOn: null,
26086
26088
  gives: "Adds your own day beside your clients' appointments as a separate part of the brief.",
26087
- needs: ["A Google account with a calendar you can read.", "Google sign-in configured for this installation; Connect will tell you if it is unavailable."],
26088
- before: "Once Google sign-in is set up on this computer:",
26089
+ // ba's Hub audit (2026-10-29): the guide read as if this connects today, while CHANGELOG 3.92.2 and the card say
26090
+ // CALENDAR_SETUP_MESSAGE. It does not connect until GHL Command ships its own Google sign-in, so the guide says so
26091
+ // first and the steps below are what it will be.
26092
+ needs: ["A Google account with a calendar you can read.", "Google sign-in for GHL Command, which is not available yet."],
26093
+ before: `${CALENDAR_SETUP_MESSAGE} Choosing Connect says so and saves nothing. When Google sign-in arrives in an update, these are the steps:`,
26089
26094
  steps: ["On the Google Calendar card on the Connections screen, choose Connect to open Google sign-in.", "Select your Google account and allow read-only calendar access.", "Return to the Google Calendar card and choose Finish. Your sign-in is saved only after your calendars can be read."],
26090
- say: ["Connect my own Google Calendar.", "Finish connecting my Google Calendar.", "Read my calendar for today."],
26095
+ say: [],
26096
+ // none until Google sign-in ships: each one would fail (ba audit 2026-10-29)
26091
26097
  revoke: "Choose Disconnect on this system's Connections card to remove the saved sign-in and request revocation with Google. You can also remove the app in your Google Account's third-party connections at myaccount.google.com/connections.",
26092
26098
  tools: { connect: "connect_google_calendar", disconnect: "connect_google_calendar", read: "read_my_calendar" }
26093
26099
  },
@@ -32391,7 +32397,7 @@ async function brainWireGhl(deps, args) {
32391
32397
  }
32392
32398
  const specs = wiringSpecs({ testTag: args.testTag, leadTag });
32393
32399
  const held2 = [];
32394
- const clean5 = (e) => scrub(e instanceof Error ? e.message : String(e), held2);
32400
+ const clean6 = (e) => scrub(e instanceof Error ? e.message : String(e), held2);
32395
32401
  const survey = async () => {
32396
32402
  const refuse4 = (error) => ({ refusal: { ok: false, wrote: "nothing", error } });
32397
32403
  const meta = deps.meta();
@@ -32400,7 +32406,7 @@ async function brainWireGhl(deps, args) {
32400
32406
  try {
32401
32407
  list4 = await writer.listWorkflows();
32402
32408
  } catch (e) {
32403
- return refuse4(`The full list of this account's workflows could not be read from GoHighLevel, so the brain cannot show it has every one of its workflows in view. Nothing was changed: ${clean5(e)} Run brain_wire_ghl again.`);
32409
+ return refuse4(`The full list of this account's workflows could not be read from GoHighLevel, so the brain cannot show it has every one of its workflows in view. Nothing was changed: ${clean6(e)} Run brain_wire_ghl again.`);
32404
32410
  }
32405
32411
  const found = [];
32406
32412
  for (const { id: id2 } of list4) {
@@ -32408,7 +32414,7 @@ async function brainWireGhl(deps, args) {
32408
32414
  try {
32409
32415
  read2 = await writer.getWorkflow(id2);
32410
32416
  } catch (e) {
32411
- return refuse4(`Workflow ${id2} could not be read from GoHighLevel, so the brain cannot show it has every one of its workflows in view. Nothing was changed: ${clean5(e)} Run brain_wire_ghl again.`);
32417
+ return refuse4(`Workflow ${id2} could not be read from GoHighLevel, so the brain cannot show it has every one of its workflows in view. Nothing was changed: ${clean6(e)} Run brain_wire_ghl again.`);
32412
32418
  }
32413
32419
  const shape = readShape(read2, id2);
32414
32420
  if (shape.kind === "uncertain") return refuse4(`Workflow ${id2} could not be judged: ${shape.why}, so the brain cannot show it has every one of its workflows in view. Nothing was changed. Run brain_wire_ghl again.`);
@@ -32432,7 +32438,7 @@ async function brainWireGhl(deps, args) {
32432
32438
  read2 = await writer.getWorkflow(id2);
32433
32439
  } catch (e) {
32434
32440
  if (isNotFound(e)) continue;
32435
- return refuse4(`Workflow ${id2} is saved on this computer but GoHighLevel's list did not show it, and it could not be read, so the brain cannot show it has every one of its workflows in view. Nothing was changed: ${clean5(e)} Run brain_wire_ghl again.`);
32441
+ return refuse4(`Workflow ${id2} is saved on this computer but GoHighLevel's list did not show it, and it could not be read, so the brain cannot show it has every one of its workflows in view. Nothing was changed: ${clean6(e)} Run brain_wire_ghl again.`);
32436
32442
  }
32437
32443
  const shape = readShape(read2, id2);
32438
32444
  if (shape.kind === "uncertain") return refuse4(`Workflow ${id2} is saved on this computer but could not be judged: ${shape.why}. Nothing was changed. Run brain_wire_ghl again.`);
@@ -32570,7 +32576,7 @@ async function brainWireGhl(deps, args) {
32570
32576
  if ((await writer.getWorkflow(id2)).status !== "published") await fenced.publishWorkflow(id2);
32571
32577
  } catch (e) {
32572
32578
  if (e instanceof LeaseLost) throw e;
32573
- error = clean5(e);
32579
+ error = clean6(e);
32574
32580
  }
32575
32581
  if (!id2) {
32576
32582
  results.push({ workflow: spec.workflowName, built: false, error });
@@ -32610,7 +32616,7 @@ async function brainWireGhl(deps, args) {
32610
32616
  });
32611
32617
  } catch (e) {
32612
32618
  if (e instanceof LeaseLost) throw e;
32613
- results.push({ workflow: spec.workflowName, id: id2, refreshed: false, readBack: `could not be read back: ${clean5(e)}. Run brain_wire_ghl with rewire: true again to finish`, ...error ? { error } : {} });
32619
+ results.push({ workflow: spec.workflowName, id: id2, refreshed: false, readBack: `could not be read back: ${clean6(e)}. Run brain_wire_ghl with rewire: true again to finish`, ...error ? { error } : {} });
32614
32620
  }
32615
32621
  }
32616
32622
  if (staleTest) {
@@ -32620,7 +32626,7 @@ async function brainWireGhl(deps, args) {
32620
32626
  results.push({ workflow: "earlier test-tag workflow", id: staleTest, removed: true });
32621
32627
  } catch (e) {
32622
32628
  if (e instanceof LeaseLost) throw e;
32623
- results.push({ workflow: "earlier test-tag workflow", id: staleTest, removed: false, error: clean5(e), note: "It carries the brain's current key, so it keeps working. Run brain_wire_ghl with rewire: true again to remove it." });
32629
+ results.push({ workflow: "earlier test-tag workflow", id: staleTest, removed: false, error: clean6(e), note: "It carries the brain's current key, so it keeps working. Run brain_wire_ghl with rewire: true again to remove it." });
32624
32630
  }
32625
32631
  }
32626
32632
  for (const r of results) {
@@ -34592,8 +34598,8 @@ function datePatterns(value) {
34592
34598
  if (!y || !(mo >= 1 && mo <= 12) || !(d >= 1 && d <= 31)) return [];
34593
34599
  const year = `(?:${y}|${y.slice(2)})`;
34594
34600
  const n = (x) => `0?${x}`;
34595
- const sep6 = "[-/.\\s]";
34596
- const out = [`${y}${sep6}${n(mo)}${sep6}${n(d)}`, `${n(mo)}${sep6}${n(d)}${sep6}${year}`, `${n(d)}${sep6}${n(mo)}${sep6}${year}`];
34601
+ const sep7 = "[-/.\\s]";
34602
+ const out = [`${y}${sep7}${n(mo)}${sep7}${n(d)}`, `${n(mo)}${sep7}${n(d)}${sep7}${year}`, `${n(d)}${sep7}${n(mo)}${sep7}${year}`];
34597
34603
  const month = `(?:${MONTHS2[mo - 1]}|${MONTHS2[mo - 1].slice(0, 3)})\\.?`;
34598
34604
  const day = `0?${d}(?:st|nd|rd|th)?`;
34599
34605
  out.push(`${month}\\s+${day},?\\s+${y}`, `${day}\\s+(?:of\\s+)?${month},?\\s+${y}`);
@@ -36637,8 +36643,8 @@ function sanitizeEngagements(raw) {
36637
36643
  for (const [id2, rec2] of Object.entries(src).slice(0, MAX_CLIENTS)) {
36638
36644
  const key2 = str4(id2, 64);
36639
36645
  if (!key2 || FORBIDDEN_KEY.has(key2)) continue;
36640
- const clean5 = sanitizeEngagement(rec2);
36641
- if (clean5) clients[key2] = clean5;
36646
+ const clean6 = sanitizeEngagement(rec2);
36647
+ if (clean6) clients[key2] = clean6;
36642
36648
  }
36643
36649
  }
36644
36650
  return { v: 1, clients: { ...clients }, updated_at: str4(r.updated_at, 40) };
@@ -40663,7 +40669,33 @@ function buildRunRecord(input) {
40663
40669
  };
40664
40670
  }
40665
40671
  function appendBuildStart(input, base) {
40666
- return appendJournal({ kind: "build-start", runId: input.runId, locationId: input.locationId, locationName: input.locationName, at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(), planId: input.planId }, base);
40672
+ return appendJournal({ kind: "build-start", runId: input.runId, locationId: input.locationId, locationName: input.locationName, at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(), planId: input.planId, ...input.planHash ? { planHash: input.planHash } : {} }, base);
40673
+ }
40674
+ function createdByOtherPlans(locationId2, plan, base) {
40675
+ const records = readJournal(locationId2, base);
40676
+ const starts = /* @__PURE__ */ new Map();
40677
+ for (const r of records) if (r.kind === "build-start") starts.set(r.runId, r);
40678
+ const removedBy = /* @__PURE__ */ new Map();
40679
+ const removed = (runId) => {
40680
+ let m = removedBy.get(runId);
40681
+ if (!m) {
40682
+ m = alreadyRemovedIds(locationId2, runId, base);
40683
+ removedBy.set(runId, m);
40684
+ }
40685
+ return m;
40686
+ };
40687
+ const other = (runId) => {
40688
+ const st = starts.get(runId);
40689
+ if (!st) return false;
40690
+ return st.planHash ? st.planHash !== plan.planHash : st.planId !== plan.planId;
40691
+ };
40692
+ const out = /* @__PURE__ */ new Map();
40693
+ for (const r of records) {
40694
+ if (r.kind !== "build-object" || !other(r.runId)) continue;
40695
+ const o = r.object;
40696
+ if (o?.action === "created" && o.ghlId && !out.has(o.ghlId) && !removed(r.runId).has(o.ghlId)) out.set(o.ghlId, { type: o.type, name: o.name, runId: r.runId });
40697
+ }
40698
+ return out;
40667
40699
  }
40668
40700
  function appendBuildIntent(input, base) {
40669
40701
  return appendJournal({ kind: "build-intent", runId: input.runId, locationId: input.locationId, at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(), intent: { type: input.type, name: input.name } }, base);
@@ -41518,7 +41550,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
41518
41550
  engineAcct = { engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked };
41519
41551
  if (!engineCheck.ok) {
41520
41552
  onEvent({ kind: "error", line: engineCheck.reason });
41521
- resolve15({ ok: false, error: engineCheck.reason, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked, engineVersion: engineCheck.version, engineModel: engineCheck.smokeModel });
41553
+ resolve15({ ok: false, error: engineCheck.reason, engineCheckOk: false, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked, engineVersion: engineCheck.version, engineModel: engineCheck.smokeModel });
41522
41554
  return;
41523
41555
  }
41524
41556
  onEvent({ kind: "status", line: `Starting ${spec.name} for ${locationName}.` });
@@ -43003,9 +43035,41 @@ var init_handoff_run = __esm({
43003
43035
  }
43004
43036
  });
43005
43037
 
43038
+ // src/stage-failure.ts
43039
+ function claudeSignedOut(error, lines = []) {
43040
+ return [typeof error === "string" ? error : "", ...lines].some((l) => CLAUDE_SIGNED_OUT.test(String(l)));
43041
+ }
43042
+ function failureNoteFor(stage, outcome, lines, already) {
43043
+ if (stage === 4 || outcome.ok || already || outcome.summary || outcome.issues?.length) return null;
43044
+ const s = stageFailureSummary(stage, outcome.error, lines);
43045
+ return { ...outcome, summary: s, issues: [], plain: { summary: s, issues: [] } };
43046
+ }
43047
+ function stageFailureSummary(stage, error, lines = []) {
43048
+ const step = STEP_NAME[stage] ?? "This step";
43049
+ const again = AGAIN[stage];
43050
+ const safe = READ_ONLY_STAGES.has(stage) ? " Nothing in the account was changed." : "";
43051
+ if (claudeSignedOut(error, lines)) {
43052
+ return `${step} did not run: Claude Code is signed out on this computer. Sign in to Claude Code again (type /login in Claude Code), then click ${again ? `"${again}"` : "the button"} again.${safe}`;
43053
+ }
43054
+ const why = typeof error === "string" && error.trim() ? ` ${clean3(error).replace(/[.]?$/, ".")}` : "";
43055
+ return `${step} did not finish.${why}${safe}${READ_ONLY_STAGES.has(stage) && again ? ` You can click "${again}" again.` : ""}`;
43056
+ }
43057
+ var CLAUDE_SIGNED_OUT, READ_ONLY_STAGES, STEP_NAME, AGAIN, clean3;
43058
+ var init_stage_failure = __esm({
43059
+ "src/stage-failure.ts"() {
43060
+ "use strict";
43061
+ CLAUDE_SIGNED_OUT = /\bnot logged in\b|please run \/login/i;
43062
+ READ_ONLY_STAGES = /* @__PURE__ */ new Set([1, 4]);
43063
+ STEP_NAME = { 1: "The Brief", 3: "Build", 4: "Verify" };
43064
+ AGAIN = { 1: "Write the Brief", 3: "Build", 4: "Verify" };
43065
+ clean3 = (s) => s.replace(/\s+/g, " ").replace(/\s*—\s*/g, ". ").trim().slice(0, 300);
43066
+ }
43067
+ });
43068
+
43006
43069
  // src/member-progress.ts
43007
- function memberProgress(lines, running, ok) {
43008
- const result = [running ? "Command OS is working on this step." : ok === true ? "This step finished. Review the result below." : "This step did not finish. Review the account before retrying."];
43070
+ function memberProgress(lines, running, ok, stage) {
43071
+ const failed = stage !== void 0 && READ_ONLY_STAGES.has(stage) ? "This step did not finish. It changes nothing in the account, so it is safe to run again." : "This step did not finish. Review the account before retrying.";
43072
+ const result = [running ? "Command OS is working on this step." : ok === true ? "This step finished. Review the result below." : failed];
43009
43073
  if (lines.some((l) => /writing.*review/i.test(l))) result.push("Command OS is preparing the client review.");
43010
43074
  if (lines.some((l) => /plain-English version/i.test(l))) result.push("Command OS is preparing the result for review.");
43011
43075
  for (const line4 of lines) if (/^(This step used |This step: cost not reported[.]|Checking Claude Code still works with Command OS used |Plain rewrite: (cost not reported|\$|under ))/.test(line4)) result.push(line4.replace(/—/g, "."));
@@ -43339,6 +43403,7 @@ var VERIFY_PASSED_COUNTING, readLabels, readLabel, BUILDER_SIGNIN_BODY, BUILDER_
43339
43403
  var init_member_progress = __esm({
43340
43404
  "src/member-progress.ts"() {
43341
43405
  "use strict";
43406
+ init_stage_failure();
43342
43407
  init_workflow_read_connection();
43343
43408
  VERIFY_PASSED_COUNTING = "The check finished. The list of jobs left for you is on the Manual tasks step.";
43344
43409
  readLabels = { user: "staff list", staff: "staff list", pipeline: "pipeline list", stage: "pipeline stage list", custom_field: "custom field list", tag: "tag list", custom_value: "custom value list", calendar: "calendar list", form: "form list", funnel: "funnel list", page: "page content", workflow: "workflow list", survey: "survey list", email_template: "email template list", sms_template: "SMS template list" };
@@ -43423,7 +43488,10 @@ function plainFallback(text3, opts = {}) {
43423
43488
  if (!/[.!?]$/.test(s)) s += ".";
43424
43489
  if (opts.nextStep && !/Settings|then |→/.test(s)) {
43425
43490
  const hint = nextStepHint(text3);
43426
- if (hint) s += " " + hint;
43491
+ if (hint && !opts.given?.has(hint)) {
43492
+ s += " " + hint;
43493
+ opts.given?.add(hint);
43494
+ }
43427
43495
  }
43428
43496
  return s.replace(/—/g, ".").replace(/Brand DNA/gi, "your brand").replace(/Agency OS/gi, "Command OS").replace(/\bMCP\b/g, "GHL Command");
43429
43497
  }
@@ -43431,7 +43499,10 @@ function plainFallbackOutcome(raw) {
43431
43499
  if (raw.verification) return renderVerifyReport(raw.verification, plainFallbackOutcome({ summary: raw.summary, issues: raw.issues }));
43432
43500
  return {
43433
43501
  ...raw.summary && { summary: plainFallback(raw.summary) },
43434
- ...raw.issues?.length && { issues: raw.issues.map((x) => plainFallback(x, { nextStep: true })) }
43502
+ ...raw.issues?.length && { issues: (() => {
43503
+ const given = /* @__PURE__ */ new Set();
43504
+ return raw.issues.map((x) => plainFallback(x, { nextStep: true, given }));
43505
+ })() }
43435
43506
  };
43436
43507
  }
43437
43508
  function parseOutcomeRewrite(text3, raw) {
@@ -43533,6 +43604,7 @@ function goLiveTopic(text3) {
43533
43604
  function jobTiming(text3) {
43534
43605
  const t = String(text3).trim();
43535
43606
  if (t === HANDOFF_NO_HOME_JOB) return "later";
43607
+ if (HELD_JOB.test(t)) return "later";
43536
43608
  if (LEAD_MAGNET_PAGES_JOB.test(t)) return "later";
43537
43609
  if (SWITCH_ON_JOB.test(t) || STILL_OFF_NOTE.test(t)) return "at-go-live";
43538
43610
  if (LATER_JOB.test(t)) return "later";
@@ -43649,11 +43721,12 @@ function manualSignOffRefusal(tasks2, verifyHasRun) {
43649
43721
  }
43650
43722
  return "Everything is ticked, but the check has not confirmed it yet. Run the check again and this will clear on its own.";
43651
43723
  }
43652
- var SELF_REPORTED, SWITCH_ON_JOB, LATER_JOB, LEAD_MAGNET_PAGES_JOB, HANDOFF_NO_HOME_JOB, GO_LIVE_TOPICS, NAMED_LATER, STILL_OFF_NOTE, keepsTick;
43724
+ var SELF_REPORTED, HELD_JOB, SWITCH_ON_JOB, LATER_JOB, LEAD_MAGNET_PAGES_JOB, HANDOFF_NO_HOME_JOB, GO_LIVE_TOPICS, NAMED_LATER, STILL_OFF_NOTE, keepsTick;
43653
43725
  var init_manual_tasks = __esm({
43654
43726
  "src/manual-tasks.ts"() {
43655
43727
  "use strict";
43656
43728
  SELF_REPORTED = /^[^:]+: Bought, but not built automatically yet\./;
43729
+ HELD_JOB = /^(?:Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
43657
43730
  SWITCH_ON_JOB = /^[^\n]+: This workflow is built and waiting for you to switch it on in GoHighLevel under Automation > Workflows\.$/;
43658
43731
  LATER_JOB = /^Manual action: (?:[^:\n]*\b(?:after (?:a|the|each) (?:trial )?visit|before any bulk)\b[^:\n]*|How [^:\n]{1,80} starts): /;
43659
43732
  LEAD_MAGNET_PAGES_JOB = /^[^:\n]+: Bought, but not built automatically yet\. The form is built\. The pages are separate work\.$/;
@@ -43668,6 +43741,7 @@ var init_manual_tasks = __esm({
43668
43741
  { key: "price", title: "Check the trial price, deposit and balance", re: /trial price|deposit amount|balance amount|trial_price/i }
43669
43742
  ];
43670
43743
  NAMED_LATER = [
43744
+ HELD_JOB,
43671
43745
  /^[^:\n]+: Bought, but not built automatically yet\. (?:Set up rebooking and recall|Payment setup is done|Set up referral requests|The form is built|It is not in the plan)/,
43672
43746
  /^Manual action: Set up the trial balance link\b/,
43673
43747
  /^Manual action: Limit the Trial Balance Paid workflow\b/,
@@ -43951,7 +44025,7 @@ function deriveQueue(input) {
43951
44025
  title: title(next, name2),
43952
44026
  needs: "you",
43953
44027
  action: opensStep ? "open" : "mark",
43954
- why: handRefusal ? `${handRefusal} Open step 11 to see them.` : next === 6 ? `${step.why} Open step 12 to read the list and sign off.` : step.why,
44028
+ why: handRefusal ? `${handRefusal} Open step 12 to see them.` : next === 6 ? `${step.why} Open step 13 to read the list and sign off.` : step.why,
43955
44029
  rank: RANK.waitingOnYou
43956
44030
  });
43957
44031
  continue;
@@ -44185,7 +44259,7 @@ function beyondGoHighLevel(live, engagements, roster, beyond = { ownCalendarConn
44185
44259
  }
44186
44260
  clients.sort((a, b) => a.client.localeCompare(b.client));
44187
44261
  const addressesNotRead = [...live?.clientContactsUnread ?? []].sort();
44188
- const notReadClause = addressesNotRead.length ? ` Then say "Not checked for mail: ${addressesNotRead.join(", ")}" because those clients' addresses could not be read this morning.` : "";
44262
+ const notReadClause = addressesNotRead.length ? ` Then say "Not checked for mail: ${addressesNotRead.join(", ")}" because those clients' addresses could not be read for this brief.` : "";
44189
44263
  const gmailDo = (clients.length ? 'If you have a Gmail connector: search the last 7 days for mail from the client addresses and domains listed under `clients`, and ONLY those. List every thread whose LAST message is not from the operator under "Client email waiting on you", newest first, one line each: client, subject, how long it has waited. If none: say "No client email is waiting on you."' : 'No client has an address on file, so there is nothing to search for. Say "Client email: no client addresses on file yet" and do not search by name. A client gets addresses once it has a client record (set_income_source), from its sub-account business record and the record\'s own `emails`.') + notReadClause;
44190
44264
  const neverMerge = " Never count these as client appointments and never merge the two lists: a GoHighLevel booking synced to Google is the same meeting seen twice.";
44191
44265
  const ask = [
@@ -44275,13 +44349,13 @@ function dailyBrief(input) {
44275
44349
  } else if (accountsRead === 0 && (live.loading?.length ?? 0) > 0 && !live.unreachable?.length) {
44276
44350
  notChecked.push("Every account is still loading from GoHighLevel, so every section below is your own records and the board only for now. Nothing failed; the brief fills in as each account is read.");
44277
44351
  } else if (accountsRead === 0) {
44278
- notChecked.push("Not one account could be read from GoHighLevel this morning, so every section below is your own records and the board only. That is a connection problem to fix, not a quiet day.");
44352
+ notChecked.push("Not one account could be read from GoHighLevel for this brief, so every section below is your own records and the board only. That is a connection problem to fix, not a quiet day.");
44279
44353
  }
44280
44354
  for (const note of live?.notes ?? []) notChecked.push(note);
44281
44355
  const yourDayNote = input.ownDay?.yourDayNote ?? live?.yourDayNote;
44282
44356
  if (yourDayNote) notChecked.push(yourDayNote);
44283
44357
  for (const u of live?.unreachable ?? []) {
44284
- notChecked.push(`${u.client} could not be read this morning (${u.why}), so nothing from that account is in this brief.`);
44358
+ notChecked.push(`${u.client} could not be read for this brief (${u.why}), so nothing from that account is in this brief.`);
44285
44359
  }
44286
44360
  const stillLoading = live?.loading ?? [];
44287
44361
  if (stillLoading.length) {
@@ -44346,20 +44420,20 @@ function dailyBrief(input) {
44346
44420
  headline3 = `Nothing is waiting on you. ${neverStarted.length} account${neverStarted.length === 1 ? " has" : "s have"} never been started.`;
44347
44421
  } else if (halfRead.length && !unread.length) {
44348
44422
  const what = halfRead.length === 1 ? `${halfRead[0].client}'s ${halfRead[0].what}` : `${halfRead.length} accounts were only read in part`;
44349
- headline3 = `Nothing needs you in what I could read \u2014 but ${what}.`;
44423
+ headline3 = `Nothing needs you in what I could read. But ${what}.`;
44350
44424
  } else if (unread.length === 1) {
44351
- headline3 = `Nothing needs you in what I could read \u2014 but ${unread[0].client} could not be read this morning.`;
44425
+ headline3 = `Nothing needs you in what I could read. But ${unread[0].client} could not be read for this brief.`;
44352
44426
  } else if (unread.length > 1) {
44353
- headline3 = `Nothing needs you in what I could read \u2014 but ${unread.length} accounts could not be read this morning.`;
44427
+ headline3 = `Nothing needs you in what I could read. But ${unread.length} accounts could not be read for this brief.`;
44354
44428
  } else if (yourDayNote) {
44355
- headline3 = "Nothing needs you in what I could read \u2014 but your own calendar could not be read this morning.";
44429
+ headline3 = "Nothing needs you in what I could read. But your own calendar could not be read for this brief.";
44356
44430
  } else if (floorUncheckable.length) {
44357
44431
  const c = floorUncheckable[0].client;
44358
44432
  headline3 = floorUncheckable.length === 1 ? `Nothing needs you \u2014 but ${c}'s price cannot be checked against any floor, so nothing here can tell you whether they are under it.` : `Nothing needs you \u2014 but ${floorUncheckable.length} clients' prices cannot be checked against any floor, ${c} among them.`;
44359
44433
  } else if (renewalUnknown.length) {
44360
44434
  headline3 = `Nothing needs you \u2014 but ${renewalUnknown.length} active client${renewalUnknown.length === 1 ? " has" : "s have"} no renewal date on file, so a quiet renewals list does not mean a quiet month.`;
44361
44435
  } else {
44362
- headline3 = "Nothing needs you this morning.";
44436
+ headline3 = "Nothing needs you right now.";
44363
44437
  }
44364
44438
  const readFrom = accountsRead > 0 ? { accounts: accountsRead } : void 0;
44365
44439
  return { date: localDate3(now), headline: headline3, today: today2, yourDay, waitingOnYou, runningNow, neverStarted, renewals, underFloor, payingButQuiet, newLeads, ...live?.leadsBySource?.length ? { leadsBySource: live.leadsBySource } : {}, notChecked, quiet, ...readFrom ? { readFrom } : {}, beyondGoHighLevel: beyondGoHighLevel(live, engagements, roster, input.beyond) };
@@ -44936,7 +45010,7 @@ async function readYourDay(now, tz, signal, secrets) {
44936
45010
  }
44937
45011
  return { yourDay: ownDayFromCalendar(result) };
44938
45012
  } catch {
44939
- return { yourDayNote: "Your Google Calendar is connected but could not be read this morning, so your own day is missing from this brief." };
45013
+ return { yourDayNote: "Your Google Calendar is connected but could not be read for this brief, so your own day is missing from this brief." };
44940
45014
  }
44941
45015
  }
44942
45016
  function operatorTimezone(profileTz) {
@@ -45666,7 +45740,13 @@ var init_client_capabilities = __esm({
45666
45740
  what: "One sentence about a prospect's business becomes a bot they can text, on your own demo account, and one command puts it back. It messages a real prospect, so it runs from your own Claude with you watching, never from a button.",
45667
45741
  says: "Prepare an AI employee demo for {client} from their website and arm it so they can text it.",
45668
45742
  hub: "modules/11-your-ai-employee/lesson-04-the-five-minute-demo.html",
45669
- reads: []
45743
+ // command-center 2026-10-29: read live, the same two lists the demo tools use (the demo bot, and one knowledge
45744
+ // base per prepared prospect, named "Demo: ..."). Whether a demo is armed is the bot's mode, which only
45745
+ // show_ai_employee_demo reads; this page does not claim it.
45746
+ reads: [
45747
+ N("bots", "/conversation-ai/agents/search", { limit: "50" }, "agents", "name", AI2),
45748
+ W(N("knowledge bases", "/knowledge-bases/", { locationId: "{loc}", limit: "50" }, "knowledgeBases", "name", AI2), { one: "knowledge base" })
45749
+ ]
45670
45750
  },
45671
45751
  // ── Money and paperwork ─────────────────────────────────────────────────
45672
45752
  {
@@ -45809,7 +45889,7 @@ var require_package = __commonJS({
45809
45889
  "package.json"(exports2, module2) {
45810
45890
  module2.exports = {
45811
45891
  name: "@elitedcs/ghl-mcp",
45812
- version: "3.93.1",
45892
+ version: "3.94.1",
45813
45893
  mcpName: "io.github.drjerryrelth/ghl-command",
45814
45894
  description: "GoHighLevel MCP Server for Claude. 250 tools for CRM, automation and marketing, an account-wide workflow audit, live funnel-capture verification and a workflow builder, across client accounts.",
45815
45895
  main: "dist/index.js",
@@ -47272,7 +47352,7 @@ var init_cockpit_cache = __esm({
47272
47352
  };
47273
47353
  AGE_LINE_JS = `
47274
47354
  function ageWords(ms){ms=Math.max(0,Number(ms)||0);const m=Math.floor(ms/60000);if(m<1)return "just now";if(m<60)return m+(m===1?" minute":" minutes")+" ago";const h=Math.floor(m/60);if(h<48)return h+(h===1?" hour":" hours")+" ago";const d=Math.floor(h/24);return d+" days ago"}
47275
- function ageLine(v,now){if(!v)return "";if(v.cold||!v.loadedAt){const f=v.lastRefresh&&v.lastRefresh.text?" \xB7 Refresh failed: "+v.lastRefresh.text:"";return "${COLD_TEXT}"+(v.refreshing||!f?"\u2026":"")+f}
47355
+ function ageLine(v,now){if(!v)return "";if(v.cold||!v.loadedAt){if(!v.refreshing&&v.lastRefresh&&v.lastRefresh.text)return "Could not load from GoHighLevel: "+v.lastRefresh.text;return "${COLD_TEXT}\u2026"}
47276
47356
  const t=Date.parse(v.loadedAt);const base="Loaded "+ageWords((now==null?Date.now():now)-t);
47277
47357
  if(v.refreshing)return base+" \xB7 refreshing\u2026";
47278
47358
  if(v.lastRefresh&&v.lastRefresh.text&&Date.parse(v.lastRefresh.at)>=t)return base+" \xB7 Refresh failed: "+v.lastRefresh.text;
@@ -47490,8 +47570,8 @@ var init_analyze = __esm({
47490
47570
  function extractUrls(text3, html2) {
47491
47571
  const out = /* @__PURE__ */ new Set();
47492
47572
  const add2 = (u) => {
47493
- const clean5 = u.replace(/&amp;/g, "&").replace(/[.,;:!?]+$/, "");
47494
- if (/^https?:\/\//i.test(clean5)) out.add(clean5);
47573
+ const clean6 = u.replace(/&amp;/g, "&").replace(/[.,;:!?]+$/, "");
47574
+ if (/^https?:\/\//i.test(clean6)) out.add(clean6);
47495
47575
  };
47496
47576
  if (html2) for (const m of html2.matchAll(HREF_RE)) add2(m[1]);
47497
47577
  for (const m of text3.matchAll(URL_RE)) add2(m[0]);
@@ -47687,7 +47767,8 @@ function judgeSteps(rawRows, contactId, workflowNames, lead) {
47687
47767
  else if (outcome === "unclear") detail = `GoHighLevel logged "${status || "no status"}" for this step, which this version does not know how to read. Check it in GoHighLevel's Execution logs.`;
47688
47768
  else if (outcome === "ran") detail = type === "wait_finished" ? "The wait ended." : reason3 && !/^\{/.test(reason3) ? reason3 : "Done.";
47689
47769
  else detail = contactDataSkip(type, outcome, reason3, lead) ?? staffGapSkip({ workflowId: wfId, stepId: str9(r.stepId), outcome, reason: reason3 }, lead)?.detail ?? translate(type, outcome, reason3, retryAt).detail;
47690
- rows2.push({ ...base, detail, retryAt: outcome === "failed" && !gaveUp ? retryAt : void 0, ...outcome === "waiting" && retryAt ? { waitUntil: retryAt } : {}, reason: reason3 || void 0, ...gaveUp ? { gaveUp: true } : {} });
47770
+ const notTested = outcome === "did_nothing" && contactDataSkip(type, outcome, reason3, lead) !== null;
47771
+ rows2.push({ ...base, detail, ...notTested ? { notTested: true } : {}, retryAt: outcome === "failed" && !gaveUp ? retryAt : void 0, ...outcome === "waiting" && retryAt ? { waitUntil: retryAt } : {}, reason: reason3 || void 0, ...gaveUp ? { gaveUp: true } : {} });
47691
47772
  }
47692
47773
  const sameStep = (a, b) => a.workflowId === b.workflowId && a.pass === b.pass && (a.stepId ?? a.stepName) === (b.stepId ?? b.stepName);
47693
47774
  const problems = [];
@@ -48197,6 +48278,59 @@ async function postJson(url, body) {
48197
48278
  const r = await guardedPost(url, JSON.stringify(body), { "Content-Type": "application/json", Accept: "application/json" });
48198
48279
  return r.status;
48199
48280
  }
48281
+ async function removeLeadCards(client, run, save) {
48282
+ if (!run.contactId) return;
48283
+ let rows2;
48284
+ try {
48285
+ rows2 = asArr4(asObj8(await client.get("/opportunities/search", { params: { location_id: run.locationId, contact_id: run.contactId, limit: 100 } })).opportunities).map(asObj8);
48286
+ } catch (e) {
48287
+ run.leadCards = [{ id: "", name: "", removed: false, error: `The test contact's pipeline cards could not be read (${e instanceof Error ? e.message : String(e)}), so any card a workflow made for it is still there.` }];
48288
+ return;
48289
+ }
48290
+ let bornHere = false, contactUnread = false;
48291
+ try {
48292
+ const c = asObj8(asObj8(await client.get(`/contacts/${run.contactId}`)).contact);
48293
+ const added = Date.parse(str10(c.dateAdded) ?? str10(c.createdAt) ?? "");
48294
+ bornHere = (str10(c.email) ?? "").trim().toLowerCase() === run.leadEmail.trim().toLowerCase() && Number.isFinite(added) && added >= run.startedAt - 6e4;
48295
+ } catch {
48296
+ bornHere = false;
48297
+ contactUnread = true;
48298
+ }
48299
+ const ours = rows2.filter((o) => {
48300
+ const contact = str10(o.contactId) ?? str10(asObj8(o.contact).id);
48301
+ const made = Date.parse(str10(o.createdAt) ?? str10(o.dateAdded) ?? "");
48302
+ return contact === run.contactId && Number.isFinite(made) && made >= run.startedAt - 6e4 && Boolean(str10(o.id));
48303
+ });
48304
+ if (!ours.length) return;
48305
+ run.leadCards = ours.map((o) => ({ id: str10(o.id), name: str10(o.name) ?? "Test card", ...str10(o.pipelineId) ? { pipelineId: str10(o.pipelineId) } : {}, removed: false, ...bornHere ? {} : { keptBecauseExisting: true, ...contactUnread ? { contactUnread: true } : {} } }));
48306
+ save(run);
48307
+ if (!bornHere) return;
48308
+ for (const c of run.leadCards) {
48309
+ try {
48310
+ await client.delete(`/opportunities/${c.id}`);
48311
+ c.removed = true;
48312
+ } catch (e) {
48313
+ if (notFound(e)) {
48314
+ c.removed = true;
48315
+ continue;
48316
+ }
48317
+ c.error = e instanceof Error ? e.message : String(e);
48318
+ }
48319
+ }
48320
+ }
48321
+ function leadCardsText(cards) {
48322
+ if (!cards?.length) return "";
48323
+ if (cards.length === 1 && !cards[0].id) return cards[0].error ?? "";
48324
+ const kept = cards.filter((c) => c.keptBecauseExisting && !c.contactUnread), unconfirmed = cards.filter((c) => c.keptBecauseExisting && c.contactUnread);
48325
+ const gone = cards.filter((c) => c.removed), left = cards.filter((c) => !c.removed && !c.keptBecauseExisting);
48326
+ const names = (xs) => xs.map((c) => `"${c.name}"`).join(", ");
48327
+ return [
48328
+ gone.length ? `A workflow made ${gone.length === 1 ? "a pipeline card" : `${gone.length} pipeline cards`} for the test contact (${names(gone)}); the check took ${gone.length === 1 ? "it" : "them"} out of the pipeline again.` : "",
48329
+ kept.length ? `This test used an existing contact, so the check left ${kept.length === 1 ? "this pipeline card" : "these pipeline cards"} for you to check: ${names(kept)}.` : "",
48330
+ unconfirmed.length ? `The check could not confirm this test contact was its own, so it left ${unconfirmed.length === 1 ? "this pipeline card" : "these pipeline cards"} for you to check: ${names(unconfirmed)}.` : "",
48331
+ left.length ? `The check could not take ${left.length === 1 ? "this pipeline card" : "these pipeline cards"} out of the pipeline: ${names(left)}. Clear ${left.length === 1 ? "it" : "them"} by hand in GoHighLevel's pipeline.` : ""
48332
+ ].filter(Boolean).join(" ");
48333
+ }
48200
48334
  async function listLeadCarriers(client, run) {
48201
48335
  const carriers = /* @__PURE__ */ new Map();
48202
48336
  let partial = false;
@@ -48334,6 +48468,9 @@ async function finishShopper(locationId2, runId, deps) {
48334
48468
  run.status = "done";
48335
48469
  run.finishedAt = t;
48336
48470
  run.cleanup = await listLeadCarriers(deps.client, run);
48471
+ await removeLeadCards(deps.client, run, (r) => saveRun(r, deps.base));
48472
+ const cardLine = leadCardsText(run.leadCards);
48473
+ if (cardLine) run.cleanup = { ...run.cleanup, detail: `${run.cleanup.detail} ${cardLine}`.trim() };
48337
48474
  if (run.fastForward && deps.ffAdmin) {
48338
48475
  const { removeCopies: removeCopies2 } = await Promise.resolve().then(() => (init_fastforward(), fastforward_exports));
48339
48476
  const problems = await removeCopies2(deps.ffAdmin, run.locationId, run.fastForward.copies.map((c) => ({ copyId: c.copyId, name: c.copyName })), deps.base, now);
@@ -48816,7 +48953,7 @@ function ringHtml(score2, caption, complete = true) {
48816
48953
  return `<div class="cds-ring" role="img" aria-label="Grade ${g}, ${s} out of 100${complete ? "" : ""}. ${esc2(caption)}"><svg width="160" height="160" viewBox="0 0 120 120" aria-hidden="true"><circle class="trk" cx="60" cy="60" r="${r}" stroke-width="10"></circle><circle class="arc ${k}" cx="60" cy="60" r="${r}" stroke-width="10" stroke-dasharray="${c.toFixed(2)}" stroke-dashoffset="${(c * (1 - s / 100)).toFixed(2)}" transform="rotate(-90 60 60)"></circle><text class="letter ${k}" x="60" y="66" text-anchor="middle" font-size="44">${g}</text><text class="out" x="60" y="86" text-anchor="middle">${s}/100</text></svg><div class="cap">${esc2(caption)}</div></div>`;
48817
48954
  }
48818
48955
  function stepRow(r) {
48819
- const [tone, word] = r.gaveUp ? ["bad", "Failed, gave up"] : OUTCOME[r.outcome] ?? ["", ""];
48956
+ const [tone, word] = r.gaveUp ? ["bad", "Failed, gave up"] : r.notTested ? ["", "Not tested"] : OUTCOME[r.outcome] ?? ["", ""];
48820
48957
  return `<li><span class="t"><time data-ms="${r.at}">${esc2(new Date(r.at).toISOString().slice(11, 16))}</time></span>${dot(tone)}<span class="what">${esc2(r.stepName)}<small>${esc2(r.detail)}</small></span>${word ? chip(tone, word) : ""}</li>`;
48821
48958
  }
48822
48959
  function stepsHtml(a) {
@@ -49057,7 +49194,11 @@ ${COCKPIT_TYPE_JS}
49057
49194
  function showStartPanel(ids){
49058
49195
  var names=ids.map(function(id){var w=(shownWorkflows||[]).filter(function(x){return x.id===id})[0];return w?w.name:id});
49059
49196
  var ff=document.getElementById("ff").checked, email=document.getElementById("owner-email").value, phone=document.getElementById("test-phone").value;
49060
- var li=['A test contact named "Secret Shopper" with your email ('+esc(email)+')'+(phone?' and the phone '+esc(phone):'')+', and the tag command-os-secret-shopper. It stays in the account afterwards; delete it when you no longer need it.'];
49197
+ /* OS audit F18: the run writes a new address made from this one (plusAddress in run.ts), never the typed one; say that. */
49198
+ var em=String(email||"").trim().toLowerCase(),at2=em.lastIndexOf("@"),runEmail=at2>0?em.slice(0,at2).split("+")[0]+"+shopper-(a new code)"+em.slice(at2):em;
49199
+ var li=['A test contact named "Secret Shopper" with an address made from your email, '+esc(runEmail)+' (a new code for each check, still your inbox)'+(phone?' and the phone '+esc(phone):'')+', and the tag command-os-secret-shopper. It stays in the account afterwards; delete it when you no longer need it.'];
49200
+ /* OS audit F19: a copy's "Create opportunity" step makes a real card in the account's real pipeline, and it stays. */
49201
+ li.push('If a workflow makes a pipeline card for the test contact, that card is real and sits in your pipeline while the check runs; the check takes it out again when it ends, and says so if it cannot.');
49061
49202
  if(ff)li.push('Temporary copies of '+names.length+' workflow'+(names.length===1?'':'s')+', switched on while the check runs: '+names.map(function(n){return esc("${FF_PREFIX} "+n)}).join(", ")+' (each name also carries a short code). The test lead goes into the copies, not the originals, and the copies are deleted when the check ends.');
49062
49203
  else li.push('The test lead goes into '+names.map(esc).join(", ")+'. Those workflows really text and email you, and may alert your staff once.');
49063
49204
  // Walk W38: in the copies, staff alerts, tasks and assignments go to one person, marked TEST; say who, by name.
@@ -49540,7 +49681,7 @@ function deductions(items, capped2) {
49540
49681
  }
49541
49682
  function workflowFlow(steps) {
49542
49683
  return `<div class="cds-flow">${steps.map((r) => {
49543
- const [tone, word] = r.gaveUp ? ["bad", "Failed, gave up"] : OUTCOME2[r.outcome] ?? ["", "Unknown"];
49684
+ const [tone, word] = r.gaveUp ? ["bad", "Failed, gave up"] : r.notTested ? ["", "Not tested"] : OUTCOME2[r.outcome] ?? ["", "Unknown"];
49544
49685
  return `<div class="cds-node ${tone === "bad" || tone === "warn" ? "hit" : ""}"><span class="pin ${tone}"></span><span class="nm">${esc4(r.stepName)}${r.detail ? `<small>${esc4(r.detail)}</small>` : ""}</span>${chip2(tone === "wait" ? "" : tone, word)}</div>`;
49545
49686
  }).join("")}</div>`;
49546
49687
  }
@@ -51176,7 +51317,8 @@ function draw(){
51176
51317
  const sum=money?.summary;
51177
51318
  if(sum){tile(cashAll(sum.mrr),"monthly recurring revenue");tile(cashAll(sum.received30.totals),"received, 30 days")}
51178
51319
  else if(money){tile(amount(money.owed),"owed on invoices");tile(money.renewals.count,"renewals, 30 days")}
51179
- else{tile("Not read","monthly recurring revenue");tile("Not read","received, 30 days")}
51320
+ /* OS audit F3: "Not read" was said when Stripe was simply not connected or held no record. Say which. */
51321
+ else{const w=stripe?panelWord(stripe):"Not connected";tile(w,"monthly recurring revenue");tile(w,"received, 30 days")}
51180
51322
  tiles.appendChild(el("p",readLine(),"cds-src stats-meta"));
51181
51323
  if(data.accounts.loading>0){const nte=el("p","","cds-callout info notice");nte.appendChild(el("b",plural(data.accounts.loading,"account is","accounts are")+" still loading from GoHighLevel."));nte.appendChild(document.createTextNode(" Nothing failed; the brief fills in as each one is read."));tiles.appendChild(nte)}
51182
51324
  if(data.accounts.unread>0){const nte=el("p","","cds-callout warn notice");nte.appendChild(el("b",plural(data.accounts.unread,"account","accounts")+" could not be read."));nte.appendChild(document.createTextNode(" This brief is not the full picture; the unread accounts are named in the notes below."));tiles.appendChild(nte)}
@@ -53038,7 +53180,7 @@ function compute(ans, skip, asm) {
53038
53180
  function() {
53039
53181
  return raw("calls_missed") * share("missed_never_return") * c * v;
53040
53182
  },
53041
- "Call answering, day and night"
53183
+ "Call answering"
53042
53184
  );
53043
53185
  add2(
53044
53186
  "waited",
@@ -53721,7 +53863,12 @@ function programBuiltLabel(name2) {
53721
53863
  }
53722
53864
  return `Built: Program follow-up${win ? ` (texts over the ${win} after the visit)` : ""}`;
53723
53865
  }
53724
- function boughtItemsFor(offered, wording = {}) {
53866
+ function statusLabel(label2, feature, status) {
53867
+ const what = label2.replace(/^Built: /, "");
53868
+ if (!status?.made) return `Will build: ${what}`;
53869
+ return status.made.has(feature) ? `Built: ${what}` : `Not built yet: ${what}`;
53870
+ }
53871
+ function boughtItemsFor(offered, wording = {}, status) {
53725
53872
  return ASSESSMENT_FEATURES.map((f) => {
53726
53873
  const id2 = f.id;
53727
53874
  const base = { id: id2, name: f.name, means: BOUGHT_MEANS[id2] };
@@ -53731,11 +53878,12 @@ function boughtItemsFor(offered, wording = {}) {
53731
53878
  if (!offered.includes(feature)) {
53732
53879
  return { ...base, kind: "not-in-plan", feature, ...next, delivery: `Not built for this client: ${FEATURES[feature].name.toLowerCase()} is not in the plan for this industry.` };
53733
53880
  }
53734
- const delivery = id2 === "quotes" && wording.programFollowUp ? programBuiltLabel(wording.programFollowUp) : BUILT_LABEL[id2];
53881
+ const label2 = id2 === "quotes" && wording.programFollowUp ? programBuiltLabel(wording.programFollowUp) : BUILT_LABEL[id2];
53882
+ const delivery = status?.held?.[id2] ?? statusLabel(label2, feature, status);
53735
53883
  return { ...base, kind: "built", feature, ...next, delivery };
53736
53884
  }
53737
53885
  const other = OTHER_DELIVERY[id2];
53738
- return { ...base, kind: other.kind, delivery: other.text };
53886
+ return { ...base, kind: other.kind, delivery: status?.held?.[id2] ?? other.text };
53739
53887
  });
53740
53888
  }
53741
53889
  function featuresFromBought(ticks, offered) {
@@ -53781,13 +53929,19 @@ function assessmentGateRefusal(state2, rk, clientKey, name2) {
53781
53929
  function approvalBindingHash(scopeHash2, state2, rk) {
53782
53930
  const saved = state2.bought?.[rk];
53783
53931
  const answer2 = assessmentAnswerOf(state2, rk);
53784
- if (!Array.isArray(saved) && !answer2) return scopeHash2;
53932
+ const asks = asksBound(state2, rk);
53933
+ if (!Array.isArray(saved) && !answer2 && !asks) return scopeHash2;
53785
53934
  const snapshot = state2.assessmentAttachments?.[rk];
53786
- return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, assessment: answer2, attached: snapshot ? contentHash2(snapshot) : null });
53935
+ return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, assessment: answer2, attached: snapshot ? contentHash2(snapshot) : null, ...asks ? { asks } : {} });
53787
53936
  }
53788
53937
  function boughtBindingHash(scopeHash2, state2, rk) {
53789
53938
  const saved = state2.bought?.[rk];
53790
- return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null });
53939
+ const asks = asksBound(state2, rk);
53940
+ return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, ...asks ? { asks } : {} });
53941
+ }
53942
+ function asksBound(state2, rk) {
53943
+ const a = state2.asks?.[rk];
53944
+ return a && Object.keys(a).length ? a : void 0;
53791
53945
  }
53792
53946
  function intakeChannelAnswers(answers) {
53793
53947
  const get2 = (key2, label2) => {
@@ -53807,6 +53961,12 @@ function boughtJobs(ticks, offered, channels = {}) {
53807
53961
  reason: item.kind === "not-in-plan" ? "Bought, but not built automatically yet. It is not in the plan for this client's industry." : `Bought, but not built automatically yet. ${OTHER_DELIVERY[item.id].job}`
53808
53962
  }));
53809
53963
  }
53964
+ function heldJobs(held2) {
53965
+ return held2.map((h) => {
53966
+ const at = h.line.indexOf(": ");
53967
+ return { ref: `bought:held-${h.id}`, type: BOUGHT_JOB_TYPE, name: h.line.slice(0, at), reason: h.line.slice(at + 2) };
53968
+ });
53969
+ }
53810
53970
  function boughtButExcludedJobs(ticks, offered, scopeFeatures) {
53811
53971
  if (scopeFeatures === "all") return [];
53812
53972
  return boughtItemsFor(offered).filter((item) => ticks.includes(item.id) && item.kind === "built" && item.feature && !scopeFeatures.includes(item.feature)).map((item) => ({
@@ -53829,10 +53989,10 @@ function assessmentChoiceLine(answer2, attached = answer2 === "attached") {
53829
53989
  if (answer2 === "run-first") return "The Assessment will be run first.";
53830
53990
  return "Not answered yet: has the Assessment been run for this client?";
53831
53991
  }
53832
- function boughtViewFor(state2, rk, offered, wording = {}) {
53992
+ function boughtViewFor(state2, rk, offered, wording = {}, status) {
53833
53993
  const { ticks, source } = ticksFor(state2, rk);
53834
53994
  return {
53835
- items: boughtItemsFor(offered, wording),
53995
+ items: boughtItemsFor(offered, wording, status),
53836
53996
  ticks,
53837
53997
  source,
53838
53998
  answer: assessmentAnswerOf(state2, rk),
@@ -53841,17 +54001,20 @@ function boughtViewFor(state2, rk, offered, wording = {}) {
53841
54001
  suggestions: source === "saved" ? claudeSuggestions(ticks) : []
53842
54002
  };
53843
54003
  }
53844
- function boughtDocumentFor(state2, rk, offered, wording = {}) {
54004
+ function boughtDocumentFor(state2, rk, offered, wording = {}, status) {
53845
54005
  const { ticks, source } = ticksFor(state2, rk);
53846
- const items = boughtItemsFor(offered, wording);
54006
+ const items = boughtItemsFor(offered, wording, status);
53847
54007
  const confirmed = source === "saved" || source === "scope";
53848
54008
  const ticked = items.filter((i) => ticks.includes(i.id));
53849
54009
  return {
53850
54010
  confirmed,
53851
54011
  assessment: assessmentChoiceLine(assessmentAnswerOf(state2, rk), Boolean(state2.assessmentAttachments?.[rk])),
53852
- bought: ticked.map((i) => ({ name: i.name, means: i.means, delivery: clientDelivery(i) })),
54012
+ // "No call goes unanswered" is a promise only an AI answering live keeps, and this version builds none: the client
54013
+ // reads what the build did for calls (promise-vs-build F4 wording, 2026-10-29).
54014
+ bought: ticked.map((i) => ({ name: i.name, means: i.id === "missed" ? status?.made?.has("missed_call") ? "Missed calls get a text back" : "How calls are answered" : i.means, delivery: clientDelivery(i) })),
53853
54015
  // The client reads this: never the agency's job text, and never "with Claude" (review of ea3526f).
53854
54016
  stillToDo: ticked.flatMap((i) => {
54017
+ if (HELD_LINE.test(i.delivery)) return [`${i.name}: ${clientDelivery(i).toLowerCase()}.`];
53855
54018
  if (i.kind === "go-live") return [`${i.name}: being set up in your account.`];
53856
54019
  if (i.kind === "separate") return [`${i.name}: the form is ready. The pages are a separate piece of work.`];
53857
54020
  if (i.kind === "not-in-plan") return [`${i.name}: being set up separately from this build.`];
@@ -53861,13 +54024,15 @@ function boughtDocumentFor(state2, rk, offered, wording = {}) {
53861
54024
  };
53862
54025
  }
53863
54026
  function clientDelivery(i) {
54027
+ const held2 = HELD_LINE.exec(i.delivery);
54028
+ if (held2) return held2[1] === "Waiting for your client's answer" ? "Waiting on your answer" : held2[1] === "Not built, your client said not yet" ? "Not set up, as you said not yet" : "Chosen, and coming in a later update";
53864
54029
  if (i.kind === "go-live") return "Being set up in your account";
53865
54030
  if (i.kind === "separate") return "The form is built; the pages are separate work";
53866
54031
  if (i.kind === "not-in-plan") return "Not part of this build";
53867
54032
  if (i.kind === "claude") return "A weekly report on what is working";
53868
54033
  return i.delivery;
53869
54034
  }
53870
- var BOUGHT_IDS, BOUGHT_MEANS, FEATURE_FOR_BOUGHT, BUILT_LABEL, OTHER_DELIVERY, CALL_ANSWERING_NEXT, ASSESSMENT_CHOICES, ANSWER_THE_ASSESSMENT_QUESTION, ATTACH_THE_ASSESSMENT, RUN_THE_ASSESSMENT_FIRST, BOUGHT_JOB_TYPE;
54035
+ var BOUGHT_IDS, BOUGHT_MEANS, FEATURE_FOR_BOUGHT, BUILT_LABEL, OTHER_DELIVERY, CALL_ANSWERING_NEXT, ASSESSMENT_CHOICES, ANSWER_THE_ASSESSMENT_QUESTION, ATTACH_THE_ASSESSMENT, RUN_THE_ASSESSMENT_FIRST, BOUGHT_JOB_TYPE, HELD_LINE;
53871
54036
  var init_bought_list = __esm({
53872
54037
  "src/bought-list.ts"() {
53873
54038
  "use strict";
@@ -53876,11 +54041,13 @@ var init_bought_list = __esm({
53876
54041
  init_build_contract();
53877
54042
  BOUGHT_IDS = ASSESSMENT_FEATURES.map((f) => f.id);
53878
54043
  BOUGHT_MEANS = {
53879
- missed: "No call goes unanswered",
54044
+ // rc.1 MED-1: true whatever the client chooses; nothing answers live until a voice agent is built.
54045
+ missed: "Callers get the answer your client chooses",
53880
54046
  waited: "Every new lead hears back in seconds",
53881
54047
  quotes: "No quote goes cold",
53882
54048
  noshow: "Bookings confirmed, reminded and rescued",
53883
- reactivation: "Old leads and past clients brought back",
54049
+ // rc.1 MED-2: what Win-back reaches (leads who go cold from now on); an old list only once a campaign for it is built.
54050
+ reactivation: "Leads who go quiet are brought back",
53884
54051
  nurture: "Leads kept warm until they are ready",
53885
54052
  recall: "Clients come back on schedule",
53886
54053
  deposits: "Fewer no-shows, cash up front",
@@ -53918,6 +54085,7 @@ var init_bought_list = __esm({
53918
54085
  ATTACH_THE_ASSESSMENT = "Attach the Assessment in step 4, or choose Build without an Assessment there.";
53919
54086
  RUN_THE_ASSESSMENT_FIRST = "You chose to run the Assessment first. Attach it in step 4 when it is done, or choose Build without an Assessment there.";
53920
54087
  BOUGHT_JOB_TYPE = "bought-not-built";
54088
+ HELD_LINE = /^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
53921
54089
  }
53922
54090
  });
53923
54091
 
@@ -53975,6 +54143,23 @@ var init_offer_normalize = __esm({
53975
54143
  }
53976
54144
  });
53977
54145
 
54146
+ // src/intake-to-build/texting.ts
54147
+ function textingAnswer(brief) {
54148
+ const channels = brief && typeof brief === "object" ? brief.channels : void 0;
54149
+ const sms = channels && typeof channels === "object" ? channels.sms : void 0;
54150
+ if (sms === true) return "yes";
54151
+ if (sms === false) return "not_yet";
54152
+ return "unanswered";
54153
+ }
54154
+ function smsOn(brief) {
54155
+ return textingAnswer(brief) === "yes";
54156
+ }
54157
+ var init_texting = __esm({
54158
+ "src/intake-to-build/texting.ts"() {
54159
+ "use strict";
54160
+ }
54161
+ });
54162
+
53978
54163
  // src/intake-to-build/brief.ts
53979
54164
  function norm4(s) {
53980
54165
  return s.trim().toLowerCase();
@@ -54232,7 +54417,7 @@ function parsePricePoints(text3) {
54232
54417
  const out = [];
54233
54418
  const segments2 = text3.split(/\r?\n|;|\.\s+/);
54234
54419
  const ENTRY = /^(.*?)\s*[-–—:|]?\s*\$?\s*([\d,]+(?:\.\d+)?)\s*(?:[-–—]\s*\$?\s*([\d,]+(?:\.\d+)?))?\s*(\/\s*[a-z]+|(?:per|an?|each)\s+[a-z]+)?\s*$/i;
54235
- const clean5 = (s) => s.replace(/[\s.,:;|–—-]+$/, "").trim();
54420
+ const clean6 = (s) => s.replace(/[\s.,:;|–—-]+$/, "").trim();
54236
54421
  const num4 = (s) => s.replace(/,/g, "");
54237
54422
  const recur = (s) => s ? "/" + s.replace(/^\/|^(?:per|an?|each)\b/i, "").trim().toLowerCase() : "";
54238
54423
  for (const segment of segments2) {
@@ -54241,11 +54426,11 @@ function parsePricePoints(text3) {
54241
54426
  const m = t.match(ENTRY);
54242
54427
  const withTerms = m ? null : t.match(/^(.*?)\s*[-–—:|]?\s*\$\s*([\d,]+(?:\.\d+)?)\s*(.+)$/);
54243
54428
  if (m && m[2]) {
54244
- const name2 = clean5(m[1]);
54429
+ const name2 = clean6(m[1]);
54245
54430
  const price = (m[3] ? `${num4(m[2])}-${num4(m[3])}` : num4(m[2])) + recur(m[4]);
54246
54431
  out.push({ name: name2 || t, price });
54247
- } else if (withTerms && clean5(withTerms[1])) {
54248
- out.push({ name: clean5(withTerms[1]), price: num4(withTerms[2]) + (withTerms[3].trim().startsWith("/") ? "" : " ") + withTerms[3].trim() });
54432
+ } else if (withTerms && clean6(withTerms[1])) {
54433
+ out.push({ name: clean6(withTerms[1]), price: num4(withTerms[2]) + (withTerms[3].trim().startsWith("/") ? "" : " ") + withTerms[3].trim() });
54249
54434
  } else {
54250
54435
  out.push({ name: t, price: "" });
54251
54436
  }
@@ -54749,7 +54934,7 @@ function deriveFlags(brief) {
54749
54934
  const channels = brief.channels ?? {};
54750
54935
  const goal = brief.goal ?? {};
54751
54936
  const a2p = typeof channels.a2pStatus === "string" ? channels.a2pStatus.toLowerCase() : "";
54752
- if (channels.sms !== false && a2p !== "approved" && a2p !== "not needed") {
54937
+ if (smsOn(brief) && a2p !== "approved" && a2p !== "not needed") {
54753
54938
  flags.push("needs_a2p");
54754
54939
  }
54755
54940
  if (channels.payment === "Stripe not connected" || channels.payment === void 0) flags.push("stripe_not_connected");
@@ -54757,7 +54942,7 @@ function deriveFlags(brief) {
54757
54942
  flags.push("calendar_oauth_needed");
54758
54943
  }
54759
54944
  if (channels.email === false) flags.push("email_not_ready");
54760
- if (channels.sms !== false && channels.hasPhoneNumber !== "yes") {
54945
+ if (smsOn(brief) && channels.hasPhoneNumber !== "yes") {
54761
54946
  flags.push("phone_number_needed");
54762
54947
  }
54763
54948
  return flags;
@@ -54928,6 +55113,7 @@ var init_normalizer = __esm({
54928
55113
  "src/intake-to-build/normalizer.ts"() {
54929
55114
  "use strict";
54930
55115
  init_offer_normalize();
55116
+ init_texting();
54931
55117
  init_question_set();
54932
55118
  init_brief2();
54933
55119
  EMAIL_RE3 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -55169,17 +55355,17 @@ function checkStaff(listed, users, opts = { canAddForYou: false }) {
55169
55355
  function staffGateReason(check, stageName) {
55170
55356
  if (check.status === "ready") return null;
55171
55357
  if (check.status === "unreadable") {
55172
- return `${stageName} is waiting: we could not read this client's staff in GoHighLevel. Click Check again in step 7, Staff.`;
55358
+ return `${stageName} is waiting: we could not read this client's staff in GoHighLevel. Click Check again in step 8, Staff.`;
55173
55359
  }
55174
55360
  if (check.status === "needs_list") {
55175
55361
  return `${stageName} is waiting: nobody is on the team yet. Add them under "Your team" in step 5, Intake.`;
55176
55362
  }
55177
- return `${stageName} is waiting: ${listSentence(check.missing.map(personLabel))} ${check.missing.length === 1 ? "is" : "are"} not in this client's GoHighLevel account yet. Add ${check.missing.length === 1 ? "them" : "everyone"} in step 7, Staff. Nothing is built with a blank name.`;
55363
+ return `${stageName} is waiting: ${listSentence(check.missing.map(personLabel))} ${check.missing.length === 1 ? "is" : "are"} not in this client's GoHighLevel account yet. Add ${check.missing.length === 1 ? "them" : "everyone"} in step 8, Staff. Nothing is built with a blank name.`;
55178
55364
  }
55179
55365
  function staffPreflightLine(check) {
55180
- if (!check) return { key: "staff", ok: false, text: "We could not check this client's staff. Use Check again in step 7, Staff." };
55366
+ if (!check) return { key: "staff", ok: false, text: "We could not check this client's staff. Use Check again in step 8, Staff." };
55181
55367
  if (check.status === "ready") return { key: "staff", ok: true, text: `Staff in this account: ${listSentence(check.matched.map((m) => m.name))}.` };
55182
- return { key: "staff", ok: false, text: `${check.headline}. Finish step 7, Staff, before Build.` };
55368
+ return { key: "staff", ok: false, text: `${check.headline}. Finish step 8, Staff, before Build.` };
55183
55369
  }
55184
55370
  async function readStaffCheck(deps, listed, now) {
55185
55371
  let users;
@@ -55327,8 +55513,8 @@ var init_staff_first = __esm({
55327
55513
  STAFF_STEP_ABOUT = "Calendars and lead alerts need real people. List everyone who takes appointments and everyone who should hear about new leads. We check this client's account for each email address before anything is built, so no calendar or alert is built with a blank name to fix later.";
55328
55514
  STAFF_LIST_HELP = "One person per line: name, email, role. For example: Jane Smith, jane@yourclinic.com, Front desk. If it is just you, add yourself.";
55329
55515
  STAFF_HOW_TO_ADD = `In this client's GoHighLevel account, go to ${GHL_ADD_STAFF_CLICK_PATH}. For each person above, ${GHL_ADD_STAFF_FORM_NOTE} shown here. When you are done, click Check again and we will read the account and confirm each person.`;
55330
- STAFF_REVIEW_WARNING = "Your staff are not all in this client's account yet. Build will not start until they are: without them the calendars cannot be built, and the workflows that start from a booking would be left as drafts with no calendar chosen. Finish step 7, Staff, first.";
55331
- STAFF_REVIEW_UNCHECKED = "We have not checked this client's staff yet. Build will not start until everyone who takes appointments is in the account. Step 7, Staff, checks them.";
55516
+ STAFF_REVIEW_WARNING = "Your staff are not all in this client's account yet. Build will not start until they are: without them the calendars cannot be built, and the workflows that start from a booking would be left as drafts with no calendar chosen. Finish step 8, Staff, first.";
55517
+ STAFF_REVIEW_UNCHECKED = "We have not checked this client's staff yet. Build will not start until everyone who takes appointments is in the account. Step 8, Staff, checks them.";
55332
55518
  STAFF_GATED_STAGES = /* @__PURE__ */ new Set([3]);
55333
55519
  fullName = (c) => [c.firstName, c.lastName === "-" ? "" : c.lastName].filter(Boolean).join(" ");
55334
55520
  STAFF_ADMIN_MARK = "Admin login";
@@ -55441,7 +55627,7 @@ function intakeAnswer2(intake, key2) {
55441
55627
  const label2 = questionByKey(key2)?.label;
55442
55628
  const direct = intake[key2] ?? (label2 ? intake[label2] : void 0);
55443
55629
  const hit = direct ?? Object.entries(intake).find(([k]) => label2 && k.trim().toLowerCase() === label2.trim().toLowerCase())?.[1];
55444
- return clean3(Array.isArray(hit) ? hit.join(", ") : hit);
55630
+ return clean4(Array.isArray(hit) ? hit.join(", ") : hit);
55445
55631
  }
55446
55632
  function readBackBroken(line4) {
55447
55633
  if (line4.state === "CHECKED") return false;
@@ -55452,7 +55638,7 @@ function readBackBroken(line4) {
55452
55638
  function withoutBrokenWorkflows(plan, lines) {
55453
55639
  if (!plan) return null;
55454
55640
  if (!lines?.length || !plan.workflows?.length) return plan;
55455
- return { ...plan, workflows: plan.workflows.filter((w) => !lines.some((l) => clean3(l.name) === clean3(w.name) && readBackBroken(l))) };
55641
+ return { ...plan, workflows: plan.workflows.filter((w) => !lines.some((l) => clean4(l.name) === clean4(w.name) && readBackBroken(l))) };
55456
55642
  }
55457
55643
  function builtFeatures(plan) {
55458
55644
  const seen = /* @__PURE__ */ new Set();
@@ -55469,9 +55655,9 @@ function hasWorkflow(plan, ref) {
55469
55655
  return (plan?.workflows ?? []).some((w) => w.ref === ref);
55470
55656
  }
55471
55657
  function workflowsConfirmed(plan, lines) {
55472
- const names = (plan?.workflows ?? []).map((w) => clean3(w.name)).filter(Boolean);
55658
+ const names = (plan?.workflows ?? []).map((w) => clean4(w.name)).filter(Boolean);
55473
55659
  if (!names.length || !lines?.length) return false;
55474
- return names.every((n) => lines.some((l) => clean3(l.name) === n && l.state === "CHECKED"));
55660
+ return names.every((n) => lines.some((l) => clean4(l.name) === n && l.state === "CHECKED"));
55475
55661
  }
55476
55662
  function durationWords(c) {
55477
55663
  if (!c.slotDuration) return "";
@@ -55495,8 +55681,8 @@ function isProgramFollowUp(plan) {
55495
55681
  function context(src) {
55496
55682
  const plan = src.plan ?? null;
55497
55683
  const features = builtFeatures(plan);
55498
- const calendars = (plan?.calendars ?? []).filter((c) => clean3(c.name)).map((c) => ({
55499
- name: calendarLabel(clean3(c.name)),
55684
+ const calendars = (plan?.calendars ?? []).filter((c) => clean4(c.name)).map((c) => ({
55685
+ name: calendarLabel(clean4(c.name)),
55500
55686
  duration: durationWords(c),
55501
55687
  team: (c.teamMemberRefs ?? []).map(String)
55502
55688
  }));
@@ -55505,10 +55691,11 @@ function context(src) {
55505
55691
  const items = boughtItemsFor(offered.length ? offered : features);
55506
55692
  const { ticks, source } = src.boughtState && src.recordKey ? ticksFor(src.boughtState, src.recordKey) : { ticks: [], source: "all" };
55507
55693
  const confirmedTicks = source !== "all";
55508
- const notify2 = clean3(src.roster?.notifyName) || intakeAnswer2(src.intake, "notify_name") || void 0;
55509
- const calls = clean3(src.roster?.callsName) || intakeAnswer2(src.intake, "calls_name") || void 0;
55694
+ const notify2 = clean4(src.roster?.notifyName) || intakeAnswer2(src.intake, "notify_name") || void 0;
55695
+ const calls = clean4(src.roster?.callsName) || intakeAnswer2(src.intake, "calls_name") || void 0;
55510
55696
  const notifyShown = notify2 ? teamNames(src).find((n) => sameName(n, notify2)) : void 0;
55511
- return { features, live: workflowsConfirmed(plan, src.lines), calendars, notify: notify2, ...notifyShown && { notifyShown }, calls, items, ticked: confirmedTicks ? ticks : [], confirmedTicks, programFollowUp: isProgramFollowUp(plan), review: reviewTimingOf(plan), noShowRescue: hasWorkflow(plan, "workflow.no_show"), reply: replyChannels(plan) };
55697
+ const callAnswering = src.recordKey && src.boughtState?.asks?.[src.recordKey]?.callAnswering || [];
55698
+ return { features, live: workflowsConfirmed(plan, src.lines), calendars, notify: notify2, ...notifyShown && { notifyShown }, calls, items, ticked: confirmedTicks ? ticks : [], callAnswering, confirmedTicks, programFollowUp: isProgramFollowUp(plan), review: reviewTimingOf(plan), noShowRescue: hasWorkflow(plan, "workflow.no_show"), reply: replyChannels(plan) };
55512
55699
  }
55513
55700
  function printableCalendars(ctx) {
55514
55701
  return [...new Set(ctx.calendars.map((c) => c.name))].filter((n) => n !== "booking calendar");
@@ -55518,12 +55705,12 @@ function calendarPhrase(ctx) {
55518
55705
  return names.length ? `the ${listWords(names, "or")} calendar` : "your booking calendar";
55519
55706
  }
55520
55707
  function teamNames(src) {
55521
- const fromPlan = (src.plan?.users ?? []).map((u) => clean3(`${u.firstName ?? ""} ${u.lastName ?? ""}`)).filter(Boolean);
55708
+ const fromPlan = (src.plan?.users ?? []).map((u) => clean4(`${u.firstName ?? ""} ${u.lastName ?? ""}`)).filter(Boolean);
55522
55709
  if (fromPlan.length) return fromPlan;
55523
- return parseStaffText(src.roster?.lines || intakeAnswer2(src.intake, "staff_members")).people.map((p) => clean3(p.name)).filter(Boolean);
55710
+ return parseStaffText(src.roster?.lines || intakeAnswer2(src.intake, "staff_members")).people.map((p) => clean4(p.name)).filter(Boolean);
55524
55711
  }
55525
55712
  function calendarLabel(name2) {
55526
- const n = clean3(name2);
55713
+ const n = clean4(name2);
55527
55714
  const prose = !n || n.length > 40 || n.split(" ").length > 6 || /[@$%;:()\[\],]|\d{3}[-. )]*\d{3,}|https?:|www\./i.test(n);
55528
55715
  return prose ? "booking calendar" : n;
55529
55716
  }
@@ -55570,7 +55757,9 @@ function summaryOf2(ctx, plan) {
55570
55757
  }
55571
55758
  const extras = [
55572
55759
  f("missed_call") ? "missed calls get a text back" : "",
55573
- f("winback") ? "old leads get a win-back message" : "",
55760
+ // Win-back starts from a tag Lead Nurture or No-Show Rescue adds, so it reaches leads who go cold from now on,
55761
+ // not an old list (promise-vs-build F5, 2026-10-29).
55762
+ f("winback") ? "leads who go cold get a win-back message" : "",
55574
55763
  f("quote_followup") ? ctx.programFollowUp ? PROGRAM_WORDS.quotes.summary : "every quote is followed up" : "",
55575
55764
  f("review_requests") ? "happy clients are asked for a review" : ""
55576
55765
  ].filter(Boolean);
@@ -55645,10 +55834,10 @@ function stepsOf(ctx, plan) {
55645
55834
  }
55646
55835
  function teamOf(ctx, src) {
55647
55836
  const plan = src.plan;
55648
- const people = (plan?.users ?? []).map((u) => ({ name: clean3(`${u.firstName ?? ""} ${u.lastName ?? ""}`), ref: u.ref })).filter((p) => p.name);
55837
+ const people = (plan?.users ?? []).map((u) => ({ name: clean4(`${u.firstName ?? ""} ${u.lastName ?? ""}`), ref: u.ref })).filter((p) => p.name);
55649
55838
  if (!people.length) {
55650
55839
  const listed = parseStaffText(src.roster?.lines || intakeAnswer2(src.intake, "staff_members")).people;
55651
- for (const p of listed) people.push({ name: clean3(p.name) });
55840
+ for (const p of listed) people.push({ name: clean4(p.name) });
55652
55841
  }
55653
55842
  const rows2 = [];
55654
55843
  for (const p of people) {
@@ -55675,7 +55864,7 @@ function teamOf(ctx, src) {
55675
55864
  }
55676
55865
  function stagesOf2(plan) {
55677
55866
  const first2 = (plan?.pipelines ?? [])[0];
55678
- const all = [...first2?.stages ?? []].sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((s) => clean3(s.name)).filter(Boolean);
55867
+ const all = [...first2?.stages ?? []].sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((s) => clean4(s.name)).filter(Boolean);
55679
55868
  return all.length > MAX_STAGE_CHIPS ? [...all.slice(0, MAX_STAGE_CHIPS), `+${all.length - MAX_STAGE_CHIPS} more`] : all;
55680
55869
  }
55681
55870
  function boughtOf(ctx) {
@@ -55687,7 +55876,9 @@ function boughtOf(ctx) {
55687
55876
  if (built) working.push(boughtLabel(item, true, ctx));
55688
55877
  else if (item.kind !== "built") next.push(boughtLabel(item, false, ctx));
55689
55878
  else next.push(item.name);
55690
- if (item.id === "missed" && built && ctx.ticked.includes("missed")) next.push("Live call answering");
55879
+ if (item.id === "missed" && ctx.ticked.includes("missed")) {
55880
+ for (const c of ctx.callAnswering) if (c !== "missed_text") next.push(c === "live_voice" ? "An AI that answers the phone live (chosen, coming in a later update)" : "An AI that replies by text or chat and books (chosen, coming in a later update)");
55881
+ }
55691
55882
  }
55692
55883
  return { workingLabel: ctx.live ? "Working now" : "Built, ready to switch on", working, next };
55693
55884
  }
@@ -55726,12 +55917,12 @@ function buildOwnerHandoff(input) {
55726
55917
  const ctx = context(src);
55727
55918
  const { rows: rows2, source } = problemsOf(ctx, src);
55728
55919
  const { steps, after } = stepsOf(ctx, src.plan);
55729
- const agencyName = clean3(src.agency?.name) || void 0;
55920
+ const agencyName = clean4(src.agency?.name) || void 0;
55730
55921
  const timeZone = clientTimeZone(src);
55731
55922
  return {
55732
55923
  // The business's own name from the intake, else the GoHighLevel account's (audit 2026-10-28 #25: an account named
55733
55924
  // for the agency's test titled the client's page).
55734
- accountName: intakeAnswer2(src.intake, "business_name") || clean3(src.accountName),
55925
+ accountName: intakeAnswer2(src.intake, "business_name") || clean4(src.accountName),
55735
55926
  checkedAt: src.checkedAt,
55736
55927
  ...timeZone && { timeZone },
55737
55928
  ...src.revision !== void 0 && { revision: src.revision },
@@ -55740,7 +55931,7 @@ function buildOwnerHandoff(input) {
55740
55931
  ...agencyName && { name: agencyName },
55741
55932
  accent: isHex(src.agency?.accent) ? src.agency.accent : DEFAULT_ACCENT,
55742
55933
  ...isRasterDataUri(src.agency?.logoDataUri) && { logoDataUri: src.agency.logoDataUri },
55743
- ...clean3(src.agency?.contact) && { contact: clean3(src.agency?.contact) }
55934
+ ...clean4(src.agency?.contact) && { contact: clean4(src.agency?.contact) }
55744
55935
  },
55745
55936
  summary: summaryOf2(ctx, src.plan),
55746
55937
  problems: rows2,
@@ -55888,7 +56079,7 @@ ${sections.join("\n")}
55888
56079
  </script>
55889
56080
  </body></html>`;
55890
56081
  }
55891
- var OWNER_UPDATES_LINE, MAX_OWNER_REQUESTS, MAX_PROBLEM_ROWS, MAX_TEAM_ROWS, MAX_STAGE_CHIPS, MAX_CALENDARS_NAMED, NOTHING_NEEDED, PROBLEM, BUILT_FOR, SEE, PROGRAM_WORDS, programWords, UNIT_MINUTES, NUMBER_WORDS, countWords, clean3, cap, sameName, FINDING_TO_BOUGHT, GAP_RULES, gapId, OWNER_REQUESTS, NON_US_ZONE_WORDS, US_PLACE_WORDS, US_TIME_ZONE_WORDS, esc10;
56082
+ var OWNER_UPDATES_LINE, MAX_OWNER_REQUESTS, MAX_PROBLEM_ROWS, MAX_TEAM_ROWS, MAX_STAGE_CHIPS, MAX_CALENDARS_NAMED, NOTHING_NEEDED, PROBLEM, BUILT_FOR, SEE, PROGRAM_WORDS, programWords, UNIT_MINUTES, NUMBER_WORDS, countWords, clean4, cap, sameName, FINDING_TO_BOUGHT, GAP_RULES, gapId, OWNER_REQUESTS, NON_US_ZONE_WORDS, US_PLACE_WORDS, US_TIME_ZONE_WORDS, esc10;
55892
56083
  var init_owner_handoff = __esm({
55893
56084
  "src/command-os/owner-handoff.ts"() {
55894
56085
  "use strict";
@@ -55965,9 +56156,9 @@ var init_owner_handoff = __esm({
55965
56156
  UNIT_MINUTES = { minute: 1, minutes: 1, hour: 60, hours: 60, day: 1440, days: 1440, week: 10080, weeks: 10080 };
55966
56157
  NUMBER_WORDS = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"];
55967
56158
  countWords = (n, unit) => `${NUMBER_WORDS[n] ?? String(n)} ${unit}${n === 1 ? "" : "s"}`;
55968
- clean3 = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
56159
+ clean4 = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
55969
56160
  cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
55970
- sameName = (a, b) => Boolean(a && b && clean3(a).toLowerCase() === clean3(b).toLowerCase());
56161
+ sameName = (a, b) => Boolean(a && b && clean4(a).toLowerCase() === clean4(b).toLowerCase());
55971
56162
  FINDING_TO_BOUGHT = Object.fromEntries(
55972
56163
  ASSESSMENT_FEATURES.filter((f) => "finding" in f).map((f) => [f.finding, f.id])
55973
56164
  );
@@ -56831,6 +57022,8 @@ var init_cockpit_routes = __esm({
56831
57022
  get("/api/snapshots", "agency", BOTH),
56832
57023
  get("/api/connections", "agency", BOTH),
56833
57024
  get("/api/design/run", "agency", BOTH),
57025
+ // OS audit F10: the finished design opens from the brand page (served sandboxed: it is the model's page).
57026
+ page("/design/result", "agency", BOTH),
56834
57027
  get("/api/shopper/report-seat", "agency", BOTH),
56835
57028
  // A report's branding is kept per client (report-brand-choices.json[client]): a one-client row, the client in the body.
56836
57029
  via(post("/api/shopper/report-brand", "client", BOTH), { body: "client" }),
@@ -61269,19 +61462,36 @@ function startDesignRun(request2, opts = {}) {
61269
61462
  });
61270
61463
  return job;
61271
61464
  }
61465
+ function memberRunLines(lines) {
61466
+ return lines.filter((l) => PROBLEM2.test(l) || !CHATTER.some((re) => re.test(l))).map((l) => l.replace(ABS_PATH, (p) => p.split(/[\\/]/).filter(Boolean).pop() ?? p));
61467
+ }
61272
61468
  function designRunStatus(id2) {
61273
61469
  const job = jobs.get(id2);
61274
61470
  if (!job) return null;
61471
+ const shown = { ...job, tail: memberRunLines(job.tail) };
61275
61472
  const runJson = (0, import_node_path13.join)(job.outDir, "run.json");
61276
- if (!(0, import_node_fs15.existsSync)(runJson)) return { ...job };
61473
+ if (!(0, import_node_fs15.existsSync)(runJson)) return shown;
61474
+ try {
61475
+ return { ...shown, run: JSON.parse((0, import_node_fs15.readFileSync)(runJson, "utf8")) };
61476
+ } catch {
61477
+ return shown;
61478
+ }
61479
+ }
61480
+ function designResultFile(id2) {
61481
+ const job = jobs.get(id2);
61482
+ if (!job || job.status !== "done") return null;
61277
61483
  try {
61278
- return { ...job, run: JSON.parse((0, import_node_fs15.readFileSync)(runJson, "utf8")) };
61484
+ const run = JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path13.join)(job.outDir, "run.json"), "utf8"));
61485
+ if (typeof run.file !== "string" || !/\.html?$/i.test(run.file)) return null;
61486
+ const root = (0, import_node_fs15.realpathSync)(job.outDir);
61487
+ const file3 = (0, import_node_fs15.realpathSync)((0, import_node_path13.isAbsolute)(run.file) ? run.file : (0, import_node_path13.join)(job.outDir, run.file));
61488
+ return file3.startsWith(root + import_node_path13.sep) ? file3 : null;
61279
61489
  } catch {
61280
- return { ...job };
61490
+ return null;
61281
61491
  }
61282
61492
  }
61283
61493
  function listDesignRuns() {
61284
- return [...jobs.values()].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
61494
+ return [...jobs.values()].map((j) => ({ ...j, tail: memberRunLines(j.tail) })).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
61285
61495
  }
61286
61496
  function cancelDesignRun(id2) {
61287
61497
  const child = children.get(id2);
@@ -61295,7 +61505,7 @@ function cancelDesignRun(id2) {
61295
61505
  }
61296
61506
  return true;
61297
61507
  }
61298
- var import_node_child_process6, import_node_fs15, import_node_path13, import_node_crypto29, SLUG2, MODEL, JOINED_FLAG, jobs, children;
61508
+ var import_node_child_process6, import_node_fs15, import_node_path13, import_node_crypto29, SLUG2, MODEL, JOINED_FLAG, jobs, children, CHATTER, PROBLEM2, ABS_PATH;
61299
61509
  var init_run_jobs = __esm({
61300
61510
  "src/command-os/design/run-jobs.ts"() {
61301
61511
  "use strict";
@@ -61311,6 +61521,9 @@ var init_run_jobs = __esm({
61311
61521
  JOINED_FLAG = /^--[a-z][a-z-]*=/;
61312
61522
  jobs = /* @__PURE__ */ new Map();
61313
61523
  children = /* @__PURE__ */ new Map();
61524
+ CHATTER = [/^\s*\[ghl-mcp\] Token registry: loaded key/i, /injected env \(\d+\) from .*\btip:/i, /^\s*[\u25c7\u25c8]\s.*\btip:/i];
61525
+ PROBLEM2 = /\b(warning|failed|fail|could not|cannot|invalid|error|bootstrap|denied|refused|unauthori[sz]ed)\b/i;
61526
+ ABS_PATH = /(?:\/(?:private|Users|var|tmp|home|opt)\/[^\s"')]+|[A-Za-z]:\\[^\s"')]+)/g;
61314
61527
  }
61315
61528
  });
61316
61529
 
@@ -61320,9 +61533,9 @@ var init_run_expectations = __esm({
61320
61533
  "src/command-os/design/run-expectations.ts"() {
61321
61534
  "use strict";
61322
61535
  RUN_MAX_PASSES = 3;
61323
- RUN_PASS_MINUTES = "9 to 13 minutes";
61324
- RUN_PASS_COST = "$1.25 to $1.75";
61325
- RUN_TOTAL_MINUTES = "25 to 40 minutes";
61536
+ RUN_PASS_MINUTES = "5 to 13 minutes";
61537
+ RUN_PASS_COST = "$0.85 to $1.75";
61538
+ RUN_TOTAL_MINUTES = "15 to 40 minutes";
61326
61539
  RUN_TOTAL_COST = "about $5";
61327
61540
  RUN_EXPECTATION = `Each pass takes ${RUN_PASS_MINUTES} and costs ${RUN_PASS_COST} of your own Claude usage, and a run may take up to ${RUN_MAX_PASSES} passes. Allow ${RUN_TOTAL_MINUTES} and up to ${RUN_TOTAL_COST}.`;
61328
61541
  }
@@ -62762,6 +62975,7 @@ function evalCondition(cond, ctx) {
62762
62975
  const [, pathExpr, op, rawRhs] = cmp;
62763
62976
  let lhs = getPath(ctx.brief, pathExpr);
62764
62977
  if (pathExpr === "goal.bookingNeeded") lhs = ctx.bookingWanted;
62978
+ if (pathExpr === "channels.sms") lhs = smsOn(ctx.brief);
62765
62979
  const rhs = rawRhs.trim().replace(/^'(.*)'$/, "$1").replace(/^"(.*)"$/, "$1");
62766
62980
  let equal;
62767
62981
  if (rhs === "true" || rhs === "false") equal = lhs === (rhs === "true");
@@ -63232,20 +63446,20 @@ function presetOwnWords(s) {
63232
63446
  return s.replace(INLINE_TOKEN_RE, (_whole, _path, def) => def !== void 0 ? ` ${def.trim()} ` : " ").replace(/\|\|/g, " ");
63233
63447
  }
63234
63448
  function claimSafeBrief(brief, blocked) {
63235
- const clean5 = (v) => {
63449
+ const clean6 = (v) => {
63236
63450
  if (typeof v === "string") return blocked(v).length > 0 ? void 0 : v;
63237
- if (Array.isArray(v)) return v.map(clean5).filter((x) => x !== void 0);
63451
+ if (Array.isArray(v)) return v.map(clean6).filter((x) => x !== void 0);
63238
63452
  if (v && typeof v === "object") {
63239
63453
  const out = {};
63240
63454
  for (const [k, x] of Object.entries(v)) {
63241
- const c = clean5(x);
63455
+ const c = clean6(x);
63242
63456
  if (c !== void 0) out[k] = c;
63243
63457
  }
63244
63458
  return out;
63245
63459
  }
63246
63460
  return v;
63247
63461
  };
63248
- return clean5(brief);
63462
+ return clean6(brief);
63249
63463
  }
63250
63464
  function isHollowMessage(s) {
63251
63465
  return visibleText2(stripOptOutLines(s)).replace(/[\s.,!?-]/g, "") === "";
@@ -63631,7 +63845,7 @@ function planFromPreset(input) {
63631
63845
  const skTemplates = sk.templates ?? {};
63632
63846
  const emailTemplates = keptByCondition(skTemplates.emails, ctx).map((t) => fillNode(t, ctx));
63633
63847
  const gsm7 = typeof p.conventions?.gsm7 === "string";
63634
- const smsTemplates = keptByCondition(skTemplates.sms, ctx).map((t) => {
63848
+ const smsTemplates = (smsOn(input.brief) ? keptByCondition(skTemplates.sms, ctx) : []).map((t) => {
63635
63849
  const filled2 = fillNode(t, ctx);
63636
63850
  if (gsm7 && typeof filled2.body === "string") filled2.body = toGsm7(filled2.body);
63637
63851
  return filled2;
@@ -64043,6 +64257,7 @@ var init_plan_from_preset = __esm({
64043
64257
  init_sms_rules();
64044
64258
  init_business_hours();
64045
64259
  init_plan_guide();
64260
+ init_texting();
64046
64261
  init_claims_guard();
64047
64262
  PRESET_TOKEN_ROOTS = /* @__PURE__ */ new Set(["business", "offer", "audience", "voice", "goal", "proof"]);
64048
64263
  GSM7_REPLACEMENTS = [
@@ -65212,6 +65427,8 @@ function stepProgress(i) {
65212
65427
  i.assessment === "attached" || i.assessment === "without" || i.assessment === "" && [2, 3, 4, 5, 6].some((n) => i.stages[n] === "done"),
65213
65428
  // Old clients' stage 0 was a form install; it counts only once answers are really in (review of 64da59d).
65214
65429
  stage(0) && i.intakeIn !== false,
65430
+ // Clients that approved under the old order (bought inside Approve) count as answered.
65431
+ Boolean(i.scopeSaved) || stage(2) && i.approved,
65215
65432
  stage(1),
65216
65433
  i.staffReady || stage(3),
65217
65434
  stage(2) && i.approved,
@@ -65239,6 +65456,7 @@ var init_build_steps = __esm({
65239
65456
  "Industry",
65240
65457
  "Assessment",
65241
65458
  "Intake",
65459
+ "What they bought",
65242
65460
  "Brief",
65243
65461
  "Staff",
65244
65462
  "Approve",
@@ -65253,9 +65471,10 @@ var init_build_steps = __esm({
65253
65471
  "What kind of business is this? It picks the intake questions and the plan.",
65254
65472
  "Talk with the owner and attach the Assessment, or build without one.",
65255
65473
  "The business details and your team. We fill in what your notes and the call already say.",
65474
+ "Tick what your client paid for, and confirm the questions it asks with them. The plan and the review are written for exactly this.",
65256
65475
  "Claude reads the intake and writes the plan for this client.",
65257
65476
  "Your team from step 5 must be in the client's GoHighLevel account before anything is built for them.",
65258
- "Read the plan, tick what the client bought, and say yes. Nothing is built until you do.",
65477
+ "Read the plan and say yes. Nothing is built until you do.",
65259
65478
  "We build the plan in GoHighLevel. You can watch it happen.",
65260
65479
  "We check that every part works.",
65261
65480
  "The jobs you do by hand in GoHighLevel. Each says how, and gives you a question to ask Claude for step by step help.",
@@ -65297,7 +65516,7 @@ function bsInputs(c,state,run){const id=c.locationId;const st=(((state.clients||
65297
65516
  const sc=(state.staffChecks||{})[id];const ii=((state.intakeInputs||{})[id])||{};
65298
65517
  const intakeIn=Boolean((state.intakeCompleted||{})[id])||ii.status==="received";
65299
65518
  return {intakeIn:intakeIn,typeConfirmed:Boolean(acc.typeConfirmed),parked:["template","hidden"].includes(typeOf(state,id)),industry:acc.industry||"",assessment:assess,stages:st,stale:stale,
65300
- running:(run&&run.running&&run.locationId===id&&typeof run.stage==="number")?run.stage:-1,staffReady:Boolean(sc&&sc.status==="ready"),approved:Boolean((state.buildApproved||{})[id])}}
65519
+ running:(run&&run.running&&run.locationId===id&&typeof run.stage==="number")?run.stage:-1,staffReady:Boolean(sc&&sc.status==="ready"),approved:Boolean((state.buildApproved||{})[id]),scopeSaved:scopeChosen(state,id)}}
65301
65520
  /* Where a client stands, for the My clients list: "Waiting on step 5 of 12: Intake". */
65302
65521
  function bsWhere(c,state,run){const t=typeOf(state,c.locationId);if(t==="template"||t==="hidden")return {step:-1,text:"Not built: a "+TYPE_LABELS[t].toLowerCase()+" account.",cta:"Open"};
65303
65522
  const p=stepProgress(bsInputs(c,state,run));const n=BS_NAMES.length;
@@ -65374,14 +65593,15 @@ function bsSummary(n,c,state){const id=c.locationId;const acc=((state.accounts||
65374
65593
  if(n===2){const f=INDUSTRIES2.find(x=>x.slug===acc.industry);return f?f.label:(acc.industry||"")}
65375
65594
  if(n===3){const a=bsInputs(c,state,null).assessment;return a==="attached"?"Assessment attached.":a==="without"?"Building without an Assessment.":"Answered before this page existed."}
65376
65595
  if(n===4)return "Intake finished.";
65377
- if(n===5)return "Brief written. The Blueprint Review is ready for step 8.";
65378
- if(n===6){const sc=(state.staffChecks||{})[id];return sc&&sc.matched&&sc.matched.length?"In the account: "+sc.matched.map(m=>m.name).join(", ")+".":"Staff confirmed."}
65379
- if(n===7){const a=(state.buildApproved||{})[id];return a?"Approved "+niceDate(a,true)+".":"Approved."}
65596
+ if(n===5){const sp=(state.scope||{})[id]||{};const k=(sp.features||[]).length;return sp.wholePreset?"Everything in the plan.":"Saved: "+k+" "+(k===1?"feature":"features")+" bought."}
65597
+ if(n===6)return "Brief written. The Blueprint Review is ready for step 9.";
65598
+ if(n===7){const sc=(state.staffChecks||{})[id];return sc&&sc.matched&&sc.matched.length?"In the account: "+sc.matched.map(m=>m.name).join(", ")+".":"Staff confirmed."}
65599
+ if(n===8){const a=(state.buildApproved||{})[id];return a?"Approved "+niceDate(a,true)+".":"Approved."}
65380
65600
  /* Run 9: the folded step said only "Built.". It says what was made, from the build's own record. */
65381
- if(n===8){const bv0=(((state.buildViews||{})[id])||{});const made=(bv0.made&&bv0.made.length)?bv0.made:(bv0.built||[]).filter(x=>x.status==="created");if(!made.length)return "Built.";
65601
+ if(n===9){const bv0=(((state.buildViews||{})[id])||{});const made=(bv0.made&&bv0.made.length)?bv0.made:(bv0.built||[]).filter(x=>x.status==="created");if(!made.length)return "Built.";
65382
65602
  const by={};for(const x of made)by[x.type]=(by[x.type]||0)+1;const KIND={pipeline:"pipeline",stage:"pipeline stage",field:"custom field",tag:"tag",cv:"custom value",calendar:"calendar",form:"form",email_template:"email template",sms_template:"text template",workflow:"draft automation",user:"staff login"};
65383
65603
  return "Built in GoHighLevel: "+Object.entries(by).map(([k,v])=>v+" "+plural(v,KIND[k]||k)).join(", ")+". Nothing is switched on."}
65384
- if(n===9){const oc=(state.outcomes||{})[id+":4"];const must=bsMustDo(c,state).length;
65604
+ if(n===10){const oc=(state.outcomes||{})[id+":4"];const must=bsMustDo(c,state).length;
65385
65605
  /* Never "clean" while a job that blocks go-live is open (blind run 2: "clean" beside "no texts can be sent"). */
65386
65606
  /* Command-center 2026-10-03: never "clean" on ticks alone. */
65387
65607
  const tk=(((state.jobCounts||{})[id])||{}).ticked||0;
@@ -65390,14 +65610,14 @@ function bsSummary(n,c,state){const id=c.locationId;const acc=((state.accounts||
65390
65610
  const conf=((oc&&oc.verification&&oc.verification.memberJobs)||{}).confirmed;
65391
65611
  const lead=typeof conf==="number"?"GoHighLevel confirms "+conf+" "+plural(conf,"item")+".":"The check finished.";
65392
65612
  const mine=(tk||typeof conf==="number")?" You confirmed "+tk+" yourself.":"";
65393
- if(oc&&oc.ok===true)return must?lead+mine+" "+must+" "+(must===1?"job is":"jobs are")+" required before sign-off: see step 11.":lead+mine+" Nothing else is required before sign-off.";
65613
+ if(oc&&oc.ok===true)return must?lead+mine+" "+must+" "+(must===1?"job is":"jobs are")+" required before sign-off: see step 12.":lead+mine+" Nothing else is required before sign-off.";
65394
65614
  return oc?"Checked. Some parts need another look: see the notes.":"Checked."}
65395
- if(n===10)return "Hand jobs confirmed.";
65615
+ if(n===11)return "Hand jobs confirmed.";
65396
65616
  return "Signed off. Your next jobs in GoHighLevel are listed on this step."}
65397
65617
  /* Steps 2, 3, 4, 5 and 7 can be changed later; the server marks what comes after them out of date. */
65398
- const BS_CHANGEABLE=[1,2,3,4];
65618
+ const BS_CHANGEABLE=[1,2,3,4,5];
65399
65619
  /* The saved stage each step runs or marks (null: a step the cockpit keeps itself). */
65400
- const BS_STAGE_OF=[null,null,null,null,0,1,null,2,3,4,5,6];
65620
+ const BS_STAGE_OF=[null,null,null,null,0,null,1,null,2,3,4,5,6];
65401
65621
  function bsBody(n,c,state,run,box){const id=c.locationId;const key="s"+n+":"+id;
65402
65622
  /* An older build recorded in the account is named, never applied (run 7: a fresh start showed old steps as done). */
65403
65623
  if(n===0){const old=(state.olderShared||{})[id];if(old){const done=(old.stages||[]).filter(s=>s==="done").length;box.appendChild(mk("p","cds-callout","An older build of this client is recorded in its GoHighLevel account ("+done+" "+plural(done,"step")+" done, last updated "+(niceDate(old.updated_at,true)||old.updated_at)+"). It does not match this client's intake here, so none of it is shown as done. These steps start fresh. If a teammate is building this same client, you can use their progress instead."));
@@ -65429,20 +65649,23 @@ function bsBody(n,c,state,run,box){const id=c.locationId;const key="s"+n+":"+id;
65429
65649
  const j=await bsPost("/api/intake-complete",body);bsBusy[key]=false;
65430
65650
  if(!j.ok)bsErr[key]=j.error;else{delete bsChange[id+":4"];delete bsStaff[id];delete bsDraft[id+":team"];if(j.approvalWithdrawn)bsNote[id]=j.message;iMode[id]=null;if(iDirty[id])iDirty[id].clear();delete iData[id]}load(true)}));
65431
65651
  bsErrLine(key,box);return}
65432
- if(n===5){bsOutcome(c,state,1,box);bsRun(c,state,run,1,(((state.clients[id]||{}).stages||[])[1]==="done")?"Write the Brief again":"Write the Brief",box);return}
65433
- if(n===6){bsStaffStep(c,state,box);return}
65434
- if(n===7){const a=bsInputs(c,state,null).assessment;
65652
+ /* OS audit F11 (command-center ruling (c), 2026-10-30): what the client bought is asked before the Brief, so the
65653
+ Blueprint Review is written for exactly what was sold and the normal order never leaves it out of date. */
65654
+ if(n===5){bsHideAssess=true;try{box.appendChild(scopeBlock(id,state))}finally{bsHideAssess=false}return}
65655
+ if(n===6){bsOutcome(c,state,1,box);bsRun(c,state,run,1,(((state.clients[id]||{}).stages||[])[1]==="done")?"Write the Brief again":"Write the Brief",box);return}
65656
+ if(n===7){bsStaffStep(c,state,box);return}
65657
+ if(n===8){const a=bsInputs(c,state,null).assessment;
65435
65658
  if(a!=="attached"&&a!=="without"){box.appendChild(mk("p","cds-callout warn","Approve waits for step 4: attach the Assessment there, or choose Build without an Assessment."));box.appendChild(bsBtn("Open step 4",()=>{bsChange[id+":3"]=bsSummary(3,c,state);render(lastState,lastRun)},true))}
65436
65659
  if(!rData[id])rLoad(id);bsHideAssess=true;try{box.appendChild(unnest(reviewPanel(id,c.name,state,run)))}finally{bsHideAssess=false}return}
65437
- if(n===8){const bp=((state.buildPreviews||{})[id]);const live=run&&run.running&&run.locationId===id&&run.stage===3;
65660
+ if(n===9){const bp=((state.buildPreviews||{})[id]);const live=run&&run.running&&run.locationId===id&&run.stage===3;
65438
65661
  if(bp&&!bp.confirmed&&!live){bsPause(c,state,bp.preview,box);return}
65439
65662
  bsOutcome(c,state,3,box);
65440
65663
  /* Run 8 finding 4: it ended on just "Built." The finish says what was made, counted from the build's own record. */
65441
65664
  if(!live&&(((state.clients||{})[id]||{}).stages||[])[3]==="done"){const bv0=(((state.buildViews||{})[id])||{});const made=(bv0.made&&bv0.made.length)?bv0.made:(bv0.built||[]).filter(x=>x.status==="created");
65442
65665
  if(made.length){const by={};for(const x of made)by[x.type]=(by[x.type]||0)+1;const KIND={pipeline:"pipeline",stage:"pipeline stage",field:"custom field",tag:"tag",cv:"custom value",calendar:"calendar",form:"form",email_template:"email template",sms_template:"text template",workflow:"draft automation",user:"staff login"};
65443
65666
  box.appendChild(mk("p","","Made in GoHighLevel this build: "+Object.entries(by).map(([k,n])=>n+" "+plural(n,KIND[k]||k)).join(", ")+". Every automation is a draft until you switch it on."))}}if(!live)bsSnippets(c,state,box);bsRun(c,state,run,3,(((state.clients[id]||{}).stages||[])[3]==="done")?"Build again":"Start the build",box);return}
65444
- if(n===9){bsOutcome(c,state,4,box);bsRun(c,state,run,4,(state.outcomes||{})[id+":4"]?"Run the check again":"Run the check",box);if((state.outcomes||{})[id+":4"])bsRebuild(c,state,run,box);return}
65445
- if(n===10){const jobs=bsJobs(c,state);if(jobs)box.appendChild(jobs);
65667
+ if(n===10){bsOutcome(c,state,4,box);bsRun(c,state,run,4,(state.outcomes||{})[id+":4"]?"Run the check again":"Run the check",box);if((state.outcomes||{})[id+":4"])bsRebuild(c,state,run,box);return}
65668
+ if(n===11){const jobs=bsJobs(c,state);if(jobs)box.appendChild(jobs);
65446
65669
  /* The handoff panel is here as well as in step 12: with a chosen home, its publish job blocks step 11 and only
65447
65670
  this panel clears it (round-3 review). With no home chosen it is a later job (option A, 2026-10-02). */
65448
65671
  const view=((state.buildViews||{})[id])||{};if(view.handoff)box.appendChild(unnest(handoffPanel(c,view)));
@@ -65451,7 +65674,7 @@ function bsBody(n,c,state,run,box){const id=c.locationId;const key="s"+n+":"+id;
65451
65674
  if(left5){const off=mk("button","cds-btn sbig","Confirm these tasks are finished");off.type="button";off.disabled=true;box.appendChild(off);box.appendChild(mk("p","muted",left5+" "+plural(left5,"job")+" before sign-off still open. The button turns on when none are left."))}
65452
65675
  else bsGate(c,state,5,"Confirm these tasks are finished",box);
65453
65676
  bsRecheck(c,state,run,box);return}
65454
- if(n===11){const view=((state.buildViews||{})[id])||{};
65677
+ if(n===12){const view=((state.buildViews||{})[id])||{};
65455
65678
  /* Jerry 2026-10-03: Sign-Off, not Go-Live. Say what Command OS did, then what the person does by hand. */
65456
65679
  box.appendChild(bsSignOffList(c,state));
65457
65680
  if(view.handoff)box.appendChild(unnest(handoffPanel(c,view)));bsGate(c,state,6,"Sign off this build",box);bsRecheck(c,state,run,box);return}}
@@ -65466,7 +65689,18 @@ function bsPause(c,state,pv,box){const id=c.locationId;const key="confirm:"+id;
65466
65689
  /* Ticks belong to one list: a new list starts unticked, and only keys on the list shown are sent (review of a38ebe1). */
65467
65690
  if(!bsAllow[id]||bsAllow[id].fp!==pv.fingerprint)bsAllow[id]={fp:pv.fingerprint,keys:{}};const allow=bsAllow[id].keys;
65468
65691
  box.appendChild(mk("h3","","Before Build writes anything"));
65692
+ /* OS audit F9 (command-center ruling, 2026-10-30): a build of a different plan left items this build would build on
65693
+ top of. No building on top: undo that build first (Claude lists what it removes and waits for DELETE), or stop. */
65694
+ if((pv.earlierBuild||[]).length){const eb=pv.earlierBuild,cn=c.name||"This account",runs=[...new Set(eb.map(e=>e.runId))];
65695
+ const w=mk("div","cds-callout warn bs-earlier");w.setAttribute("role","alert");
65696
+ w.appendChild(mk("p","",cn+" already has "+(eb.length===1?"an item":eb.length+" items")+" an earlier build of a different plan made: "+eb.map(e=>"\u201c"+e.name+"\u201d").join(", ")+". Building now would build on top of "+(eb.length===1?"it":"them")+", which is how duplicates happen, so Build will not start. Undo the earlier build first, or stop here."));
65697
+ w.appendChild(mk("b","","Undo the earlier build: ask Claude"));
65698
+ for(const r of runs){const q=mk("p","say","Undo build run "+r+" for "+cn);w.appendChild(q)}
65699
+ w.appendChild(mk("p","muted","Claude lists everything it would remove first, and removes nothing until you type DELETE. Then come back and Start the build."));
65700
+ w.appendChild(mk("b","","Or stop here"));w.appendChild(mk("p","muted","This build has not written anything to "+cn+". Leave this page; nothing changes."));
65701
+ box.appendChild(w);return}
65469
65702
  box.appendChild(mk("p","","Nothing has been written to "+(c.name||"this account")+" yet. Here is what Build will do. Check it, then confirm."));
65703
+ if((pv.unrecordedDrafts||[]).length)box.appendChild(mk("p","cds-callout info","Already in the account as drafts, made by no build on record: "+pv.unrecordedDrafts.map(n=>"\u201c"+n+"\u201d").join(", ")+". Build uses them as they are; check they are the ones you mean."));
65470
65704
  /* Walk 2026-10-28 W13/W8: "17 emails, 8 email templates" said nothing about the difference, and the list named nothing.
65471
65705
  Each kind says what it is, and opens to the name of every object of that kind. */
65472
65706
  const kindWord=(k,n)=>{const w=BS_KIND[k]||k;const sp=w.indexOf(" inside ");return n===1?w:(sp>0?w.slice(0,sp)+"s"+w.slice(sp):w+"s")};
@@ -65681,7 +65915,7 @@ function bsJobs(c,state){const mt=((state.manualTasks||{})[c.locationId])||[];co
65681
65915
  else if(readJob&&g.timing!=="at-go-live"){
65682
65916
  /* Walk W21: what the check last found, and what to do about it. */
65683
65917
  const found=why?" "+String(why).replace(/^GoHighLevel shows /,"GoHighLevel still shows "):" The check reads this in GoHighLevel and has not seen it done yet.";
65684
- note("Waiting for the check:"+found+(/ empty\.$/.test(String(why||""))?" Set it":" Do it in GoHighLevel")+", then press Run the check again in step 10, Verify.")}
65918
+ note("Waiting for the check:"+found+(/ empty\.$/.test(String(why||""))?" Set it":" Do it in GoHighLevel")+", then press Run the check again in step 11, Verify.")}
65685
65919
  else if(cb.checked&&g.items.some(t=>t.blocks))note("Ticked. The check confirms it next time it runs.");return row};
65686
65920
  const must=groups.filter(g=>g.timing==="before"),on=groups.filter(g=>g.timing==="at-go-live"),later=groups.filter(g=>g.timing==="later");
65687
65921
  const left=must.filter(g=>g.items.some(t=>t.blocks!==undefined?t.blocks:!t.claimed)).length;
@@ -65739,8 +65973,8 @@ function stepsPage(c,state,run){const id=c.locationId;const inp=bsInputs(c,state
65739
65973
  /* A Change closes itself once that step is done with a different answer (review of 64da59d, LOW 15). */
65740
65974
  if(bsChange[ck]&&pr.done[i]&&typeof bsChange[ck]==="string"&&bsSummary(i,c,state)!==bsChange[ck])delete bsChange[ck];
65741
65975
  /* Step 7 stays open while what the last Add them for me said, or its refusal, is there to read (3.90.1). */
65742
- if(i===6)bsRevealPasswords(id,(state.staffChecks||{})[id]||bsStaff[id]);
65743
- const staffNews=i===6&&(((bsStaffSaid[id]||[]).length>0)||Boolean(bsErr["staff:"+id]));
65976
+ if(i===7)bsRevealPasswords(id,(state.staffChecks||{})[id]||bsStaff[id]);
65977
+ const staffNews=i===7&&(((bsStaffSaid[id]||[]).length>0)||Boolean(bsErr["staff:"+id]));
65744
65978
  const li=mk("li","bs-step");li.id="step-"+(i+1);const open=(i===pr.current)||Boolean(bsChange[ck])||staffNews;
65745
65979
  const num=mk("span","bs-num",String(i+1));num.setAttribute("aria-hidden","true");li.appendChild(num);const main=mk("div","bs-main");li.appendChild(main);
65746
65980
  if(open){li.className+=" bs-now";main.appendChild(mk("div","bs-kicker","Step "+(i+1)+" of "+n+(i===pr.current?" · do this now":" · changing")));main.appendChild(mk("h3","bs-title",BS_NAMES[i]));main.appendChild(mk("p","muted",BS_ABOUT[i]));
@@ -65759,8 +65993,8 @@ function stepsPage(c,state,run){const id=c.locationId;const inp=bsInputs(c,state
65759
65993
  ch.onclick=()=>{bsChange[ck]=bsSummary(i,c,state);render(lastState,lastRun)};main.appendChild(ch);
65760
65994
  if(i>=2)main.appendChild(mk("p","muted bs-warn","Saving a change here means the steps after it run again."))}
65761
65995
  /* Step 8 can be taken back until Build runs (review of 64da59d: a team change after Approve had no way back). */
65762
- if(i===7&&idle&&!pr.done[8]){const un=mk("button","cds-link","Undo the approval");un.type="button";const uk="unapprove:"+id;
65763
- un.onclick=()=>icShow(un,uk,{title:"Undo the approval for "+c.name,text:["Step 8 opens again. Nothing is built until you approve again."],ok:"Undo the approval",busyLabel:"Undoing…",
65996
+ if(i===8&&idle&&!pr.done[9]){const un=mk("button","cds-link","Undo the approval");un.type="button";const uk="unapprove:"+id;
65997
+ un.onclick=()=>icShow(un,uk,{title:"Undo the approval for "+c.name,text:["Step 9 opens again. Nothing is built until you approve again."],ok:"Undo the approval",busyLabel:"Undoing…",
65764
65998
  run:async()=>{const j=await bsPost("/api/build-approval",{locationId:id,revoke:true});return j.ok?"":j.error},after:async()=>{load(true)}});
65765
65999
  main.appendChild(icAfter(un,uk))}}
65766
66000
  else{li.className+=" bs-locked";const t=mk("div","bs-row");t.appendChild(mk("b","bs-title",BS_NAMES[i]));t.appendChild(chip("","After step "+i));main.appendChild(t);main.appendChild(mk("p","muted",BS_ABOUT[i]))}
@@ -65773,7 +66007,7 @@ function stepsPage(c,state,run){const id=c.locationId;const inp=bsInputs(c,state
65773
66007
  if(view.approvedAt)d.appendChild(mk("p","","Building approved on "+niceDate(view.approvedAt,true)+"."));page.appendChild(d)}}
65774
66008
  /* The step that just opened comes into view, once per change (and on first open), after the redraw keeps its scroll. */
65775
66009
  if(bsSeen[id]!==pr.current){bsSeen[id]=pr.current;if(pr.current>0)setTimeout(()=>{const el=document.getElementById("step-"+(pr.current+1));if(el)try{el.scrollIntoView({block:"start",behavior:"smooth"})}catch(e){el.scrollIntoView()}},0)}
65776
- if(pr.current<0){page.appendChild(mk("p","cds-callout info","All "+n+" steps are done. The build is signed off. Switching the client on is yours to do in GoHighLevel: see step 12's list."));
66010
+ if(pr.current<0){page.appendChild(mk("p","cds-callout info","All "+n+" steps are done. The build is signed off. Switching the client on is yours to do in GoHighLevel: see step 13's list."));
65777
66011
  /* Walk W26 (2026-10-29): after sign-off nothing could start the build again, and the page still said Built after its
65778
66012
  objects were deleted in GoHighLevel. The check can always run again; when it finds built objects gone, it says how
65779
66013
  many and Build again is offered beside it. */
@@ -65902,7 +66136,7 @@ ${cockpitHeaderHtml("agency")}
65902
66136
  </div>
65903
66137
  <div class="ap-save"><span id="msgTop" role="status"></span><button type="submit" form="f" class="cds-btn" data-msg="msgTop">Save your agency details</button></div>
65904
66138
  </div>
65905
- ${p.agencyName ? "" : ` <div class="cds-callout warn"><span class="ttl">Add an agency name to use this on client documents.</span>Everything else can wait. The name is what every report and proposal carries.</div>
66139
+ ${p.agencyName ? "" : ` <div class="cds-callout warn agency-name-nudge"><span class="ttl">Add an agency name to use this on client documents.</span>Everything else can wait. The name is what every report and proposal carries.</div>
65906
66140
  `}
65907
66141
  <form id="f">
65908
66142
  <div class="cds-grid two">
@@ -66208,7 +66442,7 @@ document.getElementById("f").addEventListener("submit",async e=>{
66208
66442
  if(!r.ok||(j&&j.ok===false)){m.textContent=(j&&j.error)||"That did not save, and nothing was changed.";return}
66209
66443
  const pkgNote=settlePackages(j&&j.profile,pkgRead);
66210
66444
  const chatOk=await saveChatCreation();
66211
- m.textContent=(j&&j.usable?"Saved.":"Saved. Add an agency name to use this on client documents.")+pkgNote+(chatOk?"":" The chat creation setting did not save. Try again.");
66445
+ /* OS audit F15: the nudge stayed beside "Saved." until a reload; it goes once the name is saved. */if(j&&j.usable)document.querySelectorAll(".agency-name-nudge").forEach(function(el){el.remove()});m.textContent=(j&&j.usable?"Saved.":"Saved. Add an agency name to use this on client documents.")+pkgNote+(chatOk?"":" The chat creation setting did not save. Try again.");
66212
66446
  });
66213
66447
 
66214
66448
  /* Each client saves on its own. One record at a time is the whole safety story
@@ -66869,7 +67103,7 @@ function compute(ans, skip, asm) {
66869
67103
  add("missed", "Calls you never get back to",
66870
67104
  [{ id: "calls_missed" }, { id: "missed_never_return", share: true }], null,
66871
67105
  function () { return raw("calls_missed") * share("missed_never_return") * c * v; },
66872
- "Call answering, day and night");
67106
+ "Call answering");
66873
67107
 
66874
67108
  add("waited", "Enquiries that waited",
66875
67109
  [{ id: "waited_month" }], "response_penalty",
@@ -68770,7 +69004,7 @@ ${yours}
68770
69004
  <p>Give us the client's website and we read its colors, fonts and logo into a brand you can check and change.</p>
68771
69005
  <form id="extract" class="cds-stack bp-form">
68772
69006
  <div class="cds-field"><label for="url">Website</label><input class="cds-input" id="url" name="url" type="url" placeholder="https://clinic.example" required autocomplete="url"></div>
68773
- <div class="cds-field"><label for="rights">Whose site is it?</label><select class="cds-select" id="rights" name="rights"><option value="client-authorized">My client's, and they asked for this</option><option value="own">Mine</option><option value="written-permission">I have written permission</option><option value="none">None of these</option></select></div>
69007
+ <div class="cds-field"><label for="rights">Whose site is it?</label><select class="cds-select" id="rights" name="rights" required><option value="" selected disabled>Choose one</option><option value="client-authorized">My client's, and they asked for this</option><option value="own">Mine</option><option value="written-permission">I have written permission</option><option value="none">None of these</option></select></div>
68774
69008
  <div class="cds-field"><label for="name">Name (optional)</label><div class="hint" id="name-hint">What to call this brand here. Left empty, we take it from the site's title.</div><input class="cds-input" id="name" name="name" type="text" autocomplete="off" aria-describedby="name-hint"></div>
68775
69009
  <label class="cds-check"><input type="checkbox" id="useBrowser"><span>Also open the site in a browser on this computer<small>Tick this when the first try finds almost nothing. Some sites only draw themselves in a browser. It takes longer.</small></span></label>
68776
69010
  <div class="cds-row"><button type="submit" class="cds-btn ghost" id="extract-btn">Pull the brand</button></div>
@@ -68802,7 +69036,7 @@ fromHash();
68802
69036
  const rf=document.getElementById("ref-filter");
68803
69037
  if(rf)rf.addEventListener("input",()=>{const q=rf.value.trim().toLowerCase();let n=0;for(const b of document.querySelectorAll("[data-look]")){const hit=!q||b.textContent.toLowerCase().includes(q);b.hidden=!hit;if(hit)n++}const c=document.getElementById("ref-count");if(c)c.textContent=q?(n?n+" found":"No look by that name"):""});
68804
69038
  })();
68805
- document.getElementById("extract").addEventListener("submit",async(e)=>{e.preventDefault();const b=document.getElementById("extract-btn");b.disabled=true;say("extract-status","Reading the site\u2026 this takes a few seconds, longer with the browser pass.");try{const j=await post("/api/brand/extract",{url:document.getElementById("url").value,rights:document.getElementById("rights").value,name:document.getElementById("name").value||undefined,useBrowser:document.getElementById("useBrowser").checked});say("extract-status","Stored as "+j.brand.slug+". Opening the card\u2026","ok");location.href="/brand/"+encodeURIComponent(j.brand.slug)}catch(err){say("extract-status",err.message,"err")}finally{b.disabled=false}});
69039
+ document.getElementById("extract").addEventListener("submit",async(e)=>{e.preventDefault();/* OS audit F7: a permission is never answered for the member. */if(!document.getElementById("rights").value){say("extract-status","Choose whose site it is first.","err");return}const b=document.getElementById("extract-btn");b.disabled=true;say("extract-status","Reading the site\u2026 this takes a few seconds, longer with the browser pass.");try{const j=await post("/api/brand/extract",{url:document.getElementById("url").value,rights:document.getElementById("rights").value,name:document.getElementById("name").value||undefined,useBrowser:document.getElementById("useBrowser").checked});say("extract-status","Stored as "+j.brand.slug+". Opening the card\u2026","ok");location.href="/brand/"+encodeURIComponent(j.brand.slug)}catch(err){say("extract-status",err.message,"err")}finally{b.disabled=false}});
68806
69040
  for(const btn of document.querySelectorAll("[data-look]")){btn.addEventListener("click",async()=>{btn.disabled=true;say("look-status","Fetching "+btn.textContent+"\u2026");try{const j=await post("/api/brand/import-look",{look:btn.dataset.look});say("look-status","Imported as "+j.brand.slug+". Opening the card\u2026","ok");location.href="/brand/"+encodeURIComponent(j.brand.slug)}catch(err){say("look-status",err.message,"err");btn.disabled=false}})}
68807
69041
  for(const btn of document.querySelectorAll("[data-house]")){btn.addEventListener("click",async()=>{const as=prompt("Store this look under which name? (letters, digits, dashes)",btn.dataset.house+"-copy");if(!as)return;btn.disabled=true;try{const j=await post("/api/brand/set",{slug:btn.dataset.house,as});location.href="/brand/"+encodeURIComponent(j.brand.slug)}catch(err){alert(err.message);btn.disabled=false}})}
68808
69042
  `)}`;
@@ -68941,7 +69175,10 @@ document.getElementById("make").addEventListener("submit",async function(e){
68941
69175
  var run=s.run||{};
68942
69176
  var design=run.ok?"design PASS on pass "+run.greenAt:"design NOT GREEN";
68943
69177
  var copy=run.copy?(run.copy.error?" \xB7 copy not graded":" \xB7 copy "+run.copy.verdict+(run.copy.firstBreakdown?" (first breakdown: "+run.copy.firstBreakdown+")":"")):"";
68944
- say("mk-status",s.status==="done"?design+copy+" \xB7 "+(run.file||s.outDir):"The run stopped: "+design+copy,s.status==="done"&&run.ok?"ok":"err");
69178
+ var saved=String(run.file||s.outDir||"").split(/[\\\\/]/).filter(Boolean).pop()||"";
69179
+ say("mk-status",s.status==="done"?design+copy+(saved?" \xB7 saved as "+saved:""):"The run stopped: "+design+copy,s.status==="done"&&run.ok?"ok":"err");
69180
+ /* OS audit F10: a way to open what was made. */
69181
+ if(s.status==="done"&&saved&&/.html?$/i.test(saved)){var st=document.getElementById("mk-status");var a=document.createElement("a");a.className="cds-btn";a.href="/design/result?id="+encodeURIComponent(runId);a.target="_blank";a.rel="noopener";a.textContent="Open the result";st.appendChild(document.createTextNode(" "));st.appendChild(a)}
68945
69182
  }
68946
69183
  },4000);
68947
69184
  }catch(err){stopPolling();started=0;say("mk-status",err.message,"err");go.disabled=false}
@@ -69652,6 +69889,14 @@ function reviewPrompt(input) {
69652
69889
  "For each gap, state the cost using their own numbers when they gave any, and what we build to close it:",
69653
69890
  ...GAP_CHECKLIST.map((g) => ` \u2022 ${g}`),
69654
69891
  "",
69892
+ ...input.promises ? [
69893
+ "WHAT THIS BUILD MAKES. You do not list what we build or how each gap is fixed: the document takes that from this list itself. You write the diagnosis only. Your recommended offer and biggest lever may name a system or automation ONLY if it is on this list:",
69894
+ ...input.promises.willBuild.length ? input.promises.willBuild.map((w) => ` \u2022 ${w}`) : [" \u2022 (nothing yet)"],
69895
+ ...input.promises.held.length ? ["WAITING ON THE CLIENT'S ANSWER (never say we build these):", ...input.promises.held.map((h) => ` \u2022 ${h}`)] : [],
69896
+ 'Never promise that calls are answered live, "day and night", "24/7" or "every call": nothing in this build answers the phone.',
69897
+ ...input.promises.textingOn ? [] : ["This client is not texting: never promise a text, an SMS or a text-back anywhere."],
69898
+ ""
69899
+ ] : [],
69655
69900
  "THE CLIENT'S INTAKE ANSWERS:",
69656
69901
  answers || "(no answers provided)",
69657
69902
  "",
@@ -69663,7 +69908,7 @@ function reviewPrompt(input) {
69663
69908
  ] : [],
69664
69909
  'A gap about deposits or prepayment is never priority "now": deposits are set up later and never hold up sign-off.',
69665
69910
  "Reply with ONE line of JSON and nothing else:",
69666
- 'RESULT: {"headline":"<one sentence a busy owner would nod at>","ica":{"who":"","painPoints":[""],"objections":[""],"buyingTrigger":"","whereTheyAre":""},"offer":{"score":0,"dreamOutcome":"","believability":"","speed":"","effort":"","biggestLever":"","recommendedOffer":""},"gaps":[{"title":"","cost":"","fix":"","priority":"now"}],"build":[{"item":"","because":""}],"unknowns":[""]}'
69911
+ 'RESULT: {"headline":"<one sentence a busy owner would nod at>","ica":{"who":"","painPoints":[""],"objections":[""],"buyingTrigger":"","whereTheyAre":""},"offer":{"score":0,"dreamOutcome":"","believability":"","speed":"","effort":"","biggestLever":"","recommendedOffer":""},"gaps":[{"title":"","cost":"","priority":"now"}],"unknowns":[""]}'
69667
69912
  ].join("\n");
69668
69913
  }
69669
69914
  function accountPrefixes(clientName) {
@@ -69719,6 +69964,97 @@ function strictRetryPrompt(input) {
69719
69964
  "No preamble, no explanation, no markdown fences, no closing remarks."
69720
69965
  ].join("\n");
69721
69966
  }
69967
+ function renderedLines(r) {
69968
+ const out = [];
69969
+ const add2 = (where, text3) => {
69970
+ if (typeof text3 === "string" && text3.trim()) out.push({ where, text: text3 });
69971
+ };
69972
+ add2("headline", r.headline);
69973
+ if (r.ica) {
69974
+ add2("ica.who", r.ica.who);
69975
+ add2("ica.buyingTrigger", r.ica.buyingTrigger);
69976
+ add2("ica.whereTheyAre", r.ica.whereTheyAre);
69977
+ (r.ica.painPoints ?? []).forEach((t, i) => add2(`ica.painPoints.${i}`, t));
69978
+ (r.ica.objections ?? []).forEach((t, i) => add2(`ica.objections.${i}`, t));
69979
+ }
69980
+ if (r.offer) for (const k of ["dreamOutcome", "believability", "speed", "effort", "biggestLever", "recommendedOffer"]) add2(`offer.${k}`, r.offer[k]);
69981
+ (r.gaps ?? []).forEach((g, i) => {
69982
+ add2(`gaps.${i}.title`, g.title);
69983
+ add2(`gaps.${i}.cost`, g.cost);
69984
+ });
69985
+ (r.unknowns ?? []).forEach((t, i) => add2(`unknowns.${i}`, t));
69986
+ return out;
69987
+ }
69988
+ function overPromises(r, p) {
69989
+ if (!p || !r.ok) return [];
69990
+ const made = p.willBuild.join(" | ");
69991
+ return renderedLines(r).filter((l) => LIVE_CALLS.test(l.text) || (!p.textingOn || p.held.some((h) => /texting/i.test(h))) && TEXTS.test(l.text) || TOPICS.some((t) => t.re.test(l.text) && !t.built.test(made))).map((l) => l.where);
69992
+ }
69993
+ function graderPrompt(r, p) {
69994
+ return [
69995
+ "You check a client document for promises. A line promises something if it says or implies that we will build, set up, run or deliver it for the client.",
69996
+ "WHAT THIS BUILD MAKES (the only things a line may promise):",
69997
+ ...p.willBuild.length ? p.willBuild.map((w) => `- ${w}`) : ["- (nothing)"],
69998
+ ...p.held.length ? ["NOT BUILT, WAITING ON THE CLIENT'S ANSWER (a line promising these is a violation):", ...p.held.map((h) => `- ${h}`)] : [],
69999
+ `Texting is ${p.textingOn ? "on" : "OFF: any promise of a text, SMS or text-back is a violation"}. Nothing in this build answers the phone live: any promise of answered calls is a violation.`,
70000
+ "Diagnosis, observations about the client and questions are not promises. Advice clearly marked as for later is not a promise.",
70001
+ "THE LINES (id: text):",
70002
+ ...renderedLines(r).map((l) => `${l.where}: ${l.text}`),
70003
+ 'Reply with ONE line of JSON and nothing else: RESULT: {"violations":["<id>", ...]}'
70004
+ ].join("\n");
70005
+ }
70006
+ function parseGrader(text3, r) {
70007
+ const json3 = extractResultJson(text3);
70008
+ if (json3 === void 0) return null;
70009
+ try {
70010
+ const v = JSON.parse(json3).violations;
70011
+ if (!Array.isArray(v)) return null;
70012
+ const known = new Set(renderedLines(r).map((l) => l.where));
70013
+ return v.map(String).filter((x) => known.has(x));
70014
+ } catch {
70015
+ return null;
70016
+ }
70017
+ }
70018
+ function trimLines(r, bad) {
70019
+ if (!bad.length) return r;
70020
+ const set = new Set(bad);
70021
+ const trimmed = renderedLines(r).filter((l) => set.has(l.where)).map((l) => l.text);
70022
+ const keep = (where) => !set.has(where);
70023
+ const ica = r.ica && {
70024
+ ...r.ica,
70025
+ who: keep("ica.who") ? r.ica.who : "",
70026
+ buyingTrigger: keep("ica.buyingTrigger") ? r.ica.buyingTrigger : "",
70027
+ whereTheyAre: keep("ica.whereTheyAre") ? r.ica.whereTheyAre : "",
70028
+ painPoints: (r.ica.painPoints ?? []).filter((_, i) => keep(`ica.painPoints.${i}`)),
70029
+ objections: (r.ica.objections ?? []).filter((_, i) => keep(`ica.objections.${i}`))
70030
+ };
70031
+ const offer = r.offer && Object.fromEntries(Object.entries(r.offer).map(([k, v]) => [k, typeof v === "string" && !keep(`offer.${k}`) ? "" : v]));
70032
+ return {
70033
+ ...r,
70034
+ headline: keep("headline") ? r.headline : void 0,
70035
+ ...ica ? { ica } : {},
70036
+ ...offer ? { offer } : {},
70037
+ gaps: (r.gaps ?? []).filter((_, i) => keep(`gaps.${i}.title`) && keep(`gaps.${i}.cost`)),
70038
+ unknowns: (r.unknowns ?? []).filter((_, i) => keep(`unknowns.${i}`)),
70039
+ trimmed: [...r.trimmed ?? [], ...trimmed]
70040
+ };
70041
+ }
70042
+ async function violations(spawnClaude, r, p) {
70043
+ if (!p || !r.ok) return { ids: [], graded: true };
70044
+ const fast = overPromises(r, p);
70045
+ let found = null;
70046
+ for (let attempt2 = 0; attempt2 < 2 && found === null; attempt2++) {
70047
+ try {
70048
+ found = parseGrader(await spawnClaude(graderPrompt(r, p)), r);
70049
+ } catch {
70050
+ found = null;
70051
+ }
70052
+ }
70053
+ return { ids: [.../* @__PURE__ */ new Set([...fast, ...found ?? []])], graded: found !== null };
70054
+ }
70055
+ function structural(r, p) {
70056
+ return { ...r, build: [], gaps: (r.gaps ?? []).map((g) => ({ ...g, fix: "" })), ...p ? { promises: p } : {} };
70057
+ }
69722
70058
  async function generateReview(spawnClaude, input) {
69723
70059
  const answered = Object.values(input.answers).filter((v) => v && String(v).trim()).length;
69724
70060
  if (answered < 5) {
@@ -69731,7 +70067,21 @@ async function generateReview(spawnClaude, input) {
69731
70067
  lastRaw = raw;
69732
70068
  const names = [input.clientName, ...Object.entries(input.answers).filter(([k]) => /business[ _]?name/i.test(k)).map(([, v]) => v)].join(" ");
69733
70069
  const parsed2 = parseReview(raw, names);
69734
- if (parsed2.ok) return parsed2;
70070
+ if (parsed2.ok) {
70071
+ const first2 = structural(parsed2, input.promises);
70072
+ const over2 = await violations(spawnClaude, first2, input.promises);
70073
+ if (!over2.ids.length) return { ...first2, graded: over2.graded };
70074
+ try {
70075
+ const again = parseReview(await spawnClaude([reviewPrompt(input), "", `IMPORTANT: your previous answer promised what this build does not make (${over2.ids.join(", ")}). Promise only what is listed under WHAT THIS BUILD MAKES.`].join("\n")), names);
70076
+ if (again.ok) {
70077
+ const second = structural(again, input.promises);
70078
+ const v = await violations(spawnClaude, second, input.promises);
70079
+ return { ...trimLines(second, v.ids), graded: v.graded };
70080
+ }
70081
+ } catch {
70082
+ }
70083
+ return { ...trimLines(first2, over2.ids), graded: over2.graded };
70084
+ }
69735
70085
  } catch (e) {
69736
70086
  lastRaw = e instanceof Error ? e.message : String(e);
69737
70087
  }
@@ -69742,12 +70092,15 @@ async function generateReview(spawnClaude, input) {
69742
70092
  raw: lastRaw.slice(-600)
69743
70093
  };
69744
70094
  }
70095
+ function clientHeldLine(line4) {
70096
+ return line4.replace(/^Waiting for your client's answer: /, "Waiting on your answer: ").replace(/^Not built, your client said not yet: /, "Not set up, as you said not yet: ").replace(/^Not built yet, chosen and coming in a later update: /, "Chosen, and coming in a later update: ");
70097
+ }
69745
70098
  function reviewDocumentHtml(review, opts) {
69746
70099
  const accent = opts.accent && /^#[0-9a-fA-F]{6}$/.test(opts.accent) ? opts.accent : "#B8934A";
69747
70100
  const agency = opts.agencyName?.trim() || "";
69748
70101
  const esc18 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
69749
70102
  const date = (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, { year: "numeric", month: "long", day: "numeric" });
69750
- const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc18(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc18(g.cost)}</div>` : ""}${g.fix ? `<div class="gf">What we do: ${esc18(g.fix)}</div>` : ""}</div>`;
70103
+ const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc18(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc18(g.cost)}</div>` : ""}</div>`;
69751
70104
  const o = review.offer;
69752
70105
  const confirmed = opts.scope && !opts.scope.wholePreset && !opts.scope.awaitingConfirmation ? opts.scope : void 0;
69753
70106
  const purchased = confirmed || opts.purchasedFeatureIds ? [.../* @__PURE__ */ new Set([
@@ -69758,7 +70111,7 @@ function reviewDocumentHtml(review, opts) {
69758
70111
  const represented = [...opts.scope?.built ?? [], ...unbuilt].map((f) => BUILD_ASSESSMENT_FEATURE[f.id]).concat(opts.scope && !opts.scope.awaitingConfirmation ? ["pipeline"] : []);
69759
70112
  const assessmentNext = opts.assessment && purchased ? assessmentFeatureLines(opts.assessment, [...purchased, ...represented]) : [];
69760
70113
  const available = unbuilt.map((f) => `<div class="build"><b>${esc18(f.name)}</b><span>${esc18(f.what)}${f.figure ? ` <b>${esc18(f.figure)}</b>` : ""}</span></div>`).join("") + assessmentNext.map((line4) => `<p>${esc18(line4)}</p>`).join("");
69761
- return `<!doctype html><html><head><meta charset="utf-8"><title>${esc18(opts.clientName)} \u2014 Growth Blueprint</title><style>
70114
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${esc18(opts.clientName)}: Growth Blueprint</title><style>
69762
70115
  @page{margin:0.6in}
69763
70116
  *{box-sizing:border-box}
69764
70117
  body{font-family:-apple-system,Segoe UI,Helvetica,sans-serif;color:#1f2430;margin:0;line-height:1.55}
@@ -69813,7 +70166,7 @@ function reviewDocumentHtml(review, opts) {
69813
70166
  ${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${esc18(o.biggestLever)}</p>` : ""}
69814
70167
  ${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc18(o.recommendedOffer)}</div>` : ""}` : ""}
69815
70168
  ${review.gaps?.length ? `<div class="head">What's leaking today (${review.gaps.length})</div>${review.gaps.map(gap).join("")}` : ""}
69816
- ${review.build?.length ? `<div class="head">What we're building</div>${review.build.map((b) => `<div class="build"><b>${esc18(b.item)}</b><span>${esc18(b.because)}</span></div>`).join("")}` : ""}
70169
+ ${review.promises && (review.promises.willBuild.length || review.promises.held.length) ? `<div class="head">What we're building</div>${review.promises.willBuild.map((w) => `<div class="build"><b>${esc18(w)}</b></div>`).join("")}${review.promises.held.map((h) => `<div class="build"><b>${esc18(clientHeldLine(h))}</b></div>`).join("")}` : ""}
69817
70170
  ${opts.scope?.awaitingConfirmation ? `<div class="head">What this build includes</div>
69818
70171
  <p>Not confirmed yet. The build waits until the features to build are confirmed.</p>
69819
70172
  ` : opts.scope ? `<div class="head">What this build includes</div>
@@ -69829,7 +70182,7 @@ function reviewDocumentHtml(review, opts) {
69829
70182
  <footer><span>${agency ? esc18(agency) : "Growth Blueprint"}</span><span>${esc18(opts.clientName)} \xB7 ${date}</span></footer>
69830
70183
  </div></body></html>`;
69831
70184
  }
69832
- var GAP_CHECKLIST;
70185
+ var GAP_CHECKLIST, LIVE_CALLS, TEXTS, TOPICS;
69833
70186
  var init_blueprint_review = __esm({
69834
70187
  "src/blueprint-review.ts"() {
69835
70188
  "use strict";
@@ -69849,6 +70202,12 @@ var init_blueprint_review = __esm({
69849
70202
  "SMS capability (A2P registered) if they want to text",
69850
70203
  "Reporting they will actually look at"
69851
70204
  ];
70205
+ LIVE_CALLS = /(?:\bday\s*(?:and|&)\s*night\b|\b24\s*[\/\-\u2013]\s*7\b|\b24 hours\b|\b(?:a)?round[- ]the[- ]clock\b|\bafter[- ]hours?\b|\bevery (?:single )?(?:call|caller|enquiry|inquiry|ring)s?\b|\banswer(?:s|ed|ing)?\b[^.]{0,40}\b(?:phones?|calls?|rings?)\b|\b(?:phones?|calls?)\b[^.]{0,20}\banswer(?:s|ed|ing)?\b|\bcalls? (?:are |get )?answered\b|\bpick(?:s|ing)? up\b|\bsomeone rings\b|\bphones? (?:are |is )?covered\b|\b(?:call|phone) coverage\b|\bcoverage on (?:your |the )?phones?\b|\bno missed calls\b|\bnever miss(?:es)?\b[^.]{0,40}\b(?:calls?|leads?|phone)\b|\bno (?:call|caller) (?:goes|is) (?:unanswered|missed)\b|\blive (?:call |phone )?answering\b|\banswering 24\b|\b(?:ai|virtual) (?:receptionist|voice|phone)\b|\breceptionist\b)/i;
70206
+ TEXTS = /\b(?:texts?|texted|texting|sms|text-?backs?|whatsapp|by message on (?:their|your) (?:mobile|phone)|on their mobile|mobile messages?)\b/i;
70207
+ TOPICS = [
70208
+ { re: /\b(?:missed[- ]calls?|text[- ]?backs?|callers?)\b/i, built: /missed-call text-back/i },
70209
+ { re: /\b(?:reactivat\w*|win[- ]?backs?|past (?:clients|customers|patients)|old (?:leads|lists?|clients|customers|patients)|dormant|lapsed|bring (?:them|back)\b|brought back)\b/i, built: /win-back/i }
70210
+ ];
69852
70211
  }
69853
70212
  });
69854
70213
 
@@ -70202,11 +70561,11 @@ __export(client_calls_exports, {
70202
70561
  function gateAnalysis(transcript, analysis) {
70203
70562
  const rejected = [];
70204
70563
  const kept = {
70205
- saidTheyWant: (analysis.saidTheyWant ?? []).map(clean4).filter(Boolean),
70564
+ saidTheyWant: (analysis.saidTheyWant ?? []).map(clean5).filter(Boolean),
70206
70565
  actuallyNeed: [],
70207
70566
  unnamedProblems: [],
70208
70567
  recommendedSolutions: [],
70209
- nextCallQuestions: (analysis.nextCallQuestions ?? []).map(clean4).filter(Boolean)
70568
+ nextCallQuestions: (analysis.nextCallQuestions ?? []).map(clean5).filter(Boolean)
70210
70569
  };
70211
70570
  const check = (section2, item, quote2) => {
70212
70571
  if (!item) {
@@ -70222,17 +70581,17 @@ function gateAnalysis(transcript, analysis) {
70222
70581
  };
70223
70582
  for (const section2 of ["actuallyNeed", "unnamedProblems"]) {
70224
70583
  for (const p of analysis[section2] ?? []) {
70225
- const point = clean4(p?.point), quote2 = String(p?.quote ?? "").trim();
70584
+ const point = clean5(p?.point), quote2 = String(p?.quote ?? "").trim();
70226
70585
  if (check(section2, point, quote2)) kept[section2].push({ point, quote: quote2 });
70227
70586
  }
70228
70587
  }
70229
70588
  for (const s of analysis.recommendedSolutions ?? []) {
70230
- const solution = clean4(s?.solution), quote2 = String(s?.quote ?? "").trim();
70589
+ const solution = clean5(s?.solution), quote2 = String(s?.quote ?? "").trim();
70231
70590
  if (!SOLUTION_KINDS.includes(s?.kind)) {
70232
70591
  rejected.push({ section: "recommendedSolutions", item: solution, quote: quote2, why: `"${String(s?.kind)}" is not a kind; use ${SOLUTION_KINDS.join(", ")}.` });
70233
70592
  continue;
70234
70593
  }
70235
- if (check("recommendedSolutions", solution, quote2)) kept.recommendedSolutions.push({ solution, kind: s.kind, why: clean4(s?.why), quote: quote2 });
70594
+ if (check("recommendedSolutions", solution, quote2)) kept.recommendedSolutions.push({ solution, kind: s.kind, why: clean5(s?.why), quote: quote2 });
70236
70595
  }
70237
70596
  return { kept, rejected };
70238
70597
  }
@@ -70551,7 +70910,7 @@ async function saveToContact(input, deps, gated, source) {
70551
70910
  if (accepted.length === 0) {
70552
70911
  return done({ status: "NOTHING TO SAVE", reason: effective.held.length ? "every answer is waiting for your OK (numbers always do), so nothing new was written yet." : "no new answer passed the quote check, so nothing new was written.", answersOnContact: record?.state.ans ?? null });
70553
70912
  }
70554
- const biz = record?.state.biz || clean4(input.businessName) || out.businessName || out.contactName;
70913
+ const biz = record?.state.biz || clean5(input.businessName) || out.businessName || out.contactName;
70555
70914
  if (!biz) return fail("neither the contact nor a saved assessment has a business name. Give businessName and try again.");
70556
70915
  out.businessName = out.businessName ?? biz;
70557
70916
  const skip = { ...record?.state.skip ?? {} };
@@ -70648,7 +71007,7 @@ async function processClientCall(input, deps) {
70648
71007
  To confirm:
70649
71008
  ${confirmList(gated.held).map((l) => `- ${l}`).join("\n")}
70650
71009
  ${CONFIRM_INSTRUCTION}`;
70651
- const clientName = save?.businessName ?? save?.contactName ?? clean4(input.businessName);
71010
+ const clientName = save?.businessName ?? save?.contactName ?? clean5(input.businessName);
70652
71011
  const confirmPass = (input.confirmed?.length ?? 0) > 0;
70653
71012
  const clientFile = clientName ? buildClientFile({ clientName, date, source: input.source, gated, analysis, transcript: input.transcript, saveLine, confirmedByOwner: save?.confirmedByOwner }) : null;
70654
71013
  if (clientFile && confirmPass) {
@@ -70690,7 +71049,7 @@ ${body}
70690
71049
  localCopy
70691
71050
  };
70692
71051
  }
70693
- var fs50, path50, RECORDERS, SOLUTION_KINDS, clean4, ANALYSIS_MIN_WORDS, CLIENT_FILES_ROOT, WINDOWS_RESERVED, bullet, q2, CONFIRM_INSTRUCTION, stop2, EXISTING_WITH_CONTACT, UNCLEAN_RECORD, contactLocks, same, plural2;
71052
+ var fs50, path50, RECORDERS, SOLUTION_KINDS, clean5, ANALYSIS_MIN_WORDS, CLIENT_FILES_ROOT, WINDOWS_RESERVED, bullet, q2, CONFIRM_INSTRUCTION, stop2, EXISTING_WITH_CONTACT, UNCLEAN_RECORD, contactLocks, same, plural2;
70694
71053
  var init_client_calls = __esm({
70695
71054
  "src/command-os/client-calls.ts"() {
70696
71055
  "use strict";
@@ -70702,7 +71061,7 @@ var init_client_calls = __esm({
70702
71061
  init_assessment();
70703
71062
  RECORDERS = ["Plaud", "Zoom", "Other"];
70704
71063
  SOLUTION_KINDS = ["GoHighLevel build", "AI tool", "custom build", "process"];
70705
- clean4 = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
71064
+ clean5 = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
70706
71065
  ANALYSIS_MIN_WORDS = 3;
70707
71066
  CLIENT_FILES_ROOT = ["Command OS", "Clients"];
70708
71067
  WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
@@ -71397,8 +71756,29 @@ function composePreview(input) {
71397
71756
  else reuse.push({ key: key2, type: item.type, name: item.name, id: item.existingId });
71398
71757
  }
71399
71758
  if (input.board) change.push({ key: `${BOARD_KEY_PREFIX}${input.board.id}`, type: "customValues", name: input.board.name, id: input.board.id, changes: [BOARD_CHANGE] });
71759
+ const earlierBuild = [];
71760
+ const earlierSeen = /* @__PURE__ */ new Set();
71761
+ for (const item of input.items) {
71762
+ const hit = item.status === "existing" && item.existingId ? input.otherPlans?.get(item.existingId) : void 0;
71763
+ if (hit && !earlierSeen.has(item.existingId)) {
71764
+ earlierSeen.add(item.existingId);
71765
+ earlierBuild.push({ type: item.type, name: item.name || hit.name, runId: hit.runId });
71766
+ }
71767
+ }
71768
+ const unrecordedDrafts = [...reuse, ...change].filter((e) => e.type === "workflows" && input.workflowStatus?.get(e.id) === "draft").map((e) => e.name);
71400
71769
  const core = { locationId: input.locationId, planHash: input.planHash, reuse, change, logins: [...input.logins ?? []] };
71401
- return { v: 1, recordKey: input.recordKey, at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(), createCount, createByType, createNames, ...core, fingerprint: previewFingerprint(core) };
71770
+ return {
71771
+ v: 1,
71772
+ recordKey: input.recordKey,
71773
+ at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(),
71774
+ createCount,
71775
+ createByType,
71776
+ createNames,
71777
+ ...core,
71778
+ ...earlierBuild.length ? { earlierBuild } : {},
71779
+ ...unrecordedDrafts.length ? { unrecordedDrafts } : {},
71780
+ fingerprint: previewFingerprint(core)
71781
+ };
71402
71782
  }
71403
71783
  function allowedWorkflowIds(allow) {
71404
71784
  return new Set(allow.filter((k) => k.startsWith("workflows:")).map((k) => k.slice("workflows:".length)));
@@ -71418,6 +71798,7 @@ function boardAllowed(preview, confirm2) {
71418
71798
  }
71419
71799
  function confirmMatches(confirm2, preview) {
71420
71800
  if (!previewNamesLogins(preview)) return false;
71801
+ if (preview.earlierBuild?.length) return false;
71421
71802
  if (!confirm2 || confirm2.fingerprint !== preview.fingerprint || !Array.isArray(confirm2.allow)) return false;
71422
71803
  const changeKeys = new Set(preview.change.map((e) => e.key));
71423
71804
  return confirm2.allow.every((k) => changeKeys.has(k));
@@ -71492,6 +71873,14 @@ function listedValue(raw, id2) {
71492
71873
  const value = hit && typeof hit === "object" ? Reflect.get(hit, "value") : void 0;
71493
71874
  return typeof value === "string" ? value : void 0;
71494
71875
  }
71876
+ function earlierBuildText(earlier, clientName) {
71877
+ const runs = [...new Set(earlier.map((e) => e.runId))];
71878
+ const names = earlier.map((e) => `"${e.name}"`).join(", ");
71879
+ return {
71880
+ why: `${clientName} already has ${earlier.length === 1 ? "an item" : `${earlier.length} items`} an earlier build of a different plan made: ${names}. Building now would build on top of ${earlier.length === 1 ? "it" : "them"}, which is how duplicates happen, so Build will not start. Undo the earlier build first, or stop here.`,
71881
+ undo: runs.map((r) => `Undo build run ${r} for ${clientName}`)
71882
+ };
71883
+ }
71495
71884
  var import_node_fs17, import_node_path15, BUILD_PAUSED, BUILD_PREVIEW_CHANGED, keyOf4, stableChange, BOARD_VALUE_NAME, BOARD_KEY_PREFIX, BOARD_CHANGE, PREVIEW_KIND;
71496
71885
  var init_build_confirm = __esm({
71497
71886
  "src/build-confirm.ts"() {
@@ -71782,6 +72171,116 @@ var init_clinic_prefix_registry = __esm({
71782
72171
  }
71783
72172
  });
71784
72173
 
72174
+ // src/client-asks.ts
72175
+ function parseAsks(raw) {
72176
+ if (raw === void 0 || raw === null) return { ok: true, asks: {} };
72177
+ if (typeof raw !== "object" || Array.isArray(raw)) return { ok: false, error: "The answers could not be read. Choose them again, then save." };
72178
+ const r = raw;
72179
+ const out = {};
72180
+ if (r.texting !== void 0) {
72181
+ if (r.texting !== "yes" && r.texting !== "not_yet") return { ok: false, error: "Texting is Yes or Not yet." };
72182
+ out.texting = r.texting;
72183
+ }
72184
+ if (r.callAnswering !== void 0) {
72185
+ if (!Array.isArray(r.callAnswering) || !r.callAnswering.every((c) => CALL_ANSWERING_CHOICES.includes(c))) {
72186
+ return { ok: false, error: "Call answering choices could not be read. Choose them again, then save." };
72187
+ }
72188
+ const chosen = [...new Set(r.callAnswering)];
72189
+ if (chosen.length) out.callAnswering = CALL_ANSWERING_CHOICES.filter((c) => chosen.includes(c));
72190
+ }
72191
+ if (r.reactivation !== void 0) {
72192
+ const x = r.reactivation;
72193
+ if (!x || typeof x !== "object") return { ok: false, error: "Reactivation answers could not be read. Choose them again, then save." };
72194
+ if (!REACTIVATION_LISTS.includes(x.list)) return { ok: false, error: "Choose who reactivation should reach." };
72195
+ const offer = x.offer;
72196
+ const text3 = typeof offer?.text === "string" ? offer.text.trim() : "";
72197
+ if (!offer || offer.from !== "brief" && offer.from !== "typed" || !text3) return { ok: false, error: "Choose the offer that brings them back, or type one." };
72198
+ if (text3.length > MAX_OFFER) return { ok: false, error: `Keep the offer under ${MAX_OFFER} characters.` };
72199
+ if (x.channel !== "email" && x.channel !== "email_and_text") return { ok: false, error: "Choose Email, or Email and text." };
72200
+ const a = { list: x.list, offer: { from: offer.from, text: text3 }, channel: x.channel };
72201
+ if (a.list === "in_account_not_booked") {
72202
+ const d = Number(x.notBookedDays);
72203
+ if (!Number.isInteger(d) || d < 1 || d > 3650) return { ok: false, error: "Not booked in how many days? Use a whole number from 1 to 3650." };
72204
+ a.notBookedDays = d;
72205
+ }
72206
+ if (x.textConsentConfirmed === true) a.textConsentConfirmed = true;
72207
+ out.reactivation = a;
72208
+ }
72209
+ return { ok: true, asks: out };
72210
+ }
72211
+ function effectiveTexting(asks, intakeSms) {
72212
+ if (asks?.texting === "yes") return "yes";
72213
+ if (asks?.texting === "not_yet") return "not_yet";
72214
+ if (intakeSms === true) return "yes";
72215
+ if (intakeSms === false) return "not_yet";
72216
+ return "unanswered";
72217
+ }
72218
+ function asksContradiction(asks, texting) {
72219
+ if (texting !== "yes" && asks.callAnswering?.includes("missed_text")) return "A text back after a missed call needs Texting: Yes.";
72220
+ const re = asks.reactivation;
72221
+ if (re?.channel === "email_and_text") {
72222
+ if (texting !== "yes") return "Email and text needs Texting: Yes.";
72223
+ if (!re.textConsentConfirmed) return "Email and text needs your client to confirm these people agreed to get texts.";
72224
+ }
72225
+ return null;
72226
+ }
72227
+ function heldItems(ticks, asks, texting) {
72228
+ const out = [];
72229
+ const wait = (id2, name2) => ({ id: id2, name: name2, reason: "unanswered", line: `Waiting for your client's answer: ${name2}` });
72230
+ const textsMatter = ticks.includes("sms") || ticks.includes("missed") || ticks.includes("reactivation");
72231
+ if (textsMatter && texting === "unanswered") out.push(wait("texting", "Texting"));
72232
+ if (ticks.includes("sms") && texting === "not_yet") out.push({ id: "texting", name: "Texting", reason: "not_yet", line: "Not built, your client said not yet: Texting" });
72233
+ if (ticks.includes("missed")) {
72234
+ const c = asks?.callAnswering ?? [];
72235
+ if (!c.length) out.push(wait("call_answering", "Call answering"));
72236
+ else {
72237
+ const later = c.filter((x) => x === "live_voice" || x === "chat_bot");
72238
+ if (later.length) out.push({ id: "call_answering", name: "Call answering", reason: "later_update", line: `Not built yet, chosen and coming in a later update: ${later.map((x) => x === "live_voice" ? "an AI that answers the phone live" : "an AI that replies by text or chat and books").join(" and ")}` });
72239
+ if (c.includes("missed_text") && texting !== "yes") out.push(wait("texting", "Texting (the text back after a missed call needs it)"));
72240
+ }
72241
+ }
72242
+ if (ticks.includes("reactivation")) {
72243
+ const re = asks?.reactivation;
72244
+ if (!re) out.push(wait("reactivation", "Reactivation (who it reaches, which offer)"));
72245
+ else if (re.list !== "cold_from_now") out.push({ id: "reactivation", name: "Reactivation", reason: "later_update", line: `Not built yet, chosen and coming in a later update: reactivation for ${re.list === "import" ? "a list your client will give you" : `contacts here with no booking in ${re.notBookedDays} days`}` });
72246
+ }
72247
+ return out.filter((h, i) => out.findIndex((o) => o.id === h.id && o.reason === h.reason) === i);
72248
+ }
72249
+ function featuresAllowedByAsks(features, ticks, asks) {
72250
+ return features.filter((f) => {
72251
+ if (f === "missed_call" && ticks.includes("missed")) return asks?.callAnswering?.includes("missed_text") === true;
72252
+ if (f === "winback" && ticks.includes("reactivation")) return asks?.reactivation?.list === "cold_from_now";
72253
+ return true;
72254
+ });
72255
+ }
72256
+ function briefWithAsks(brief, asks, ticks, presetWorkflows) {
72257
+ const ch = brief.channels && typeof brief.channels === "object" ? brief.channels : {};
72258
+ const texting = effectiveTexting(asks, typeof ch.sms === "boolean" ? ch.sms : void 0);
72259
+ const { sms: _was, ...rest } = ch;
72260
+ let out = { ...brief, channels: { ...rest, ...texting === "unanswered" ? {} : { sms: texting === "yes" } } };
72261
+ if (!ticks) return out;
72262
+ const scope = out.scope;
72263
+ const offered = [...new Set(presetWorkflows.map((w) => featureOfWorkflow(String(w.ref ?? ""), w.feature)).filter((f) => f !== null))];
72264
+ const current = scope ? Array.isArray(scope.features) ? scope.features.map(String) : [] : offered;
72265
+ const allowed = featuresAllowedByAsks(current, ticks, asks);
72266
+ if (allowed.length !== current.length) out = { ...out, scope: { ...scope?.package ? { package: scope.package } : {}, features: allowed } };
72267
+ return out;
72268
+ }
72269
+ function asksBindingHash(asks, ticks) {
72270
+ return contentHash2({ asks: asks && Object.keys(asks).length ? asks : null, ticks: ticks ? [...new Set(ticks)].sort() : null });
72271
+ }
72272
+ var CALL_ANSWERING_CHOICES, REACTIVATION_LISTS, MAX_OFFER;
72273
+ var init_client_asks = __esm({
72274
+ "src/client-asks.ts"() {
72275
+ "use strict";
72276
+ init_features();
72277
+ init_build_contract();
72278
+ CALL_ANSWERING_CHOICES = ["live_voice", "chat_bot", "missed_text"];
72279
+ REACTIVATION_LISTS = ["import", "in_account_not_booked", "cold_from_now"];
72280
+ MAX_OFFER = 300;
72281
+ }
72282
+ });
72283
+
71785
72284
  // src/dashboard.ts
71786
72285
  var dashboard_exports = {};
71787
72286
  __export(dashboard_exports, {
@@ -71796,6 +72295,7 @@ __export(dashboard_exports, {
71796
72295
  ASSESSMENT_CHANGED_SINCE_APPROVAL: () => ASSESSMENT_CHANGED_SINCE_APPROVAL,
71797
72296
  ASSESSMENT_PIPELINE_MISSING: () => ASSESSMENT_PIPELINE_MISSING,
71798
72297
  BRIEF_NEEDS_INTAKE: () => BRIEF_NEEDS_INTAKE2,
72298
+ BUILD_WHAT_WAS_WAITING: () => BUILD_WHAT_WAS_WAITING,
71799
72299
  CHOOSE_BEFORE_BUILD: () => CHOOSE_BEFORE_BUILD,
71800
72300
  CHOOSE_CLIENT: () => CHOOSE_CLIENT,
71801
72301
  CHOOSE_WHAT_THEY_BOUGHT: () => CHOOSE_WHAT_THEY_BOUGHT,
@@ -71852,6 +72352,7 @@ __export(dashboard_exports, {
71852
72352
  brandPagesForPro: () => brandPagesForPro,
71853
72353
  briefReach: () => briefReach,
71854
72354
  buildPreviewsFor: () => buildPreviewsFor,
72355
+ buildStatusFor: () => buildStatusFor,
71855
72356
  callClaudeRecorded: () => callClaudeRecorded,
71856
72357
  canonicalIndustry: () => canonicalIndustry,
71857
72358
  capabilityJobs: () => capabilityJobs,
@@ -71892,7 +72393,9 @@ __export(dashboard_exports, {
71892
72393
  forgetVerifiedHandoff: () => forgetVerifiedHandoff,
71893
72394
  guarded: () => guarded,
71894
72395
  hasChosenScope: () => hasChosenScope,
72396
+ industryChoices: () => industryChoices,
71895
72397
  insideDir: () => insideDir,
72398
+ intakeAskFacts: () => intakeAskFacts,
71896
72399
  intakeErrorDetail: () => intakeErrorDetail,
71897
72400
  intakeInput: () => intakeInput,
71898
72401
  intakeMissingMessage: () => intakeMissingMessage,
@@ -71916,6 +72419,7 @@ __export(dashboard_exports, {
71916
72419
  memberFacingError: () => memberFacingError,
71917
72420
  mergeQueueIntakes: () => mergeQueueIntakes,
71918
72421
  newClientFailure: () => newClientFailure,
72422
+ noClientPageHtml: () => noClientPageHtml,
71919
72423
  normalizedIntake: () => normalizedIntake,
71920
72424
  notPreparedHtml: () => notPreparedHtml,
71921
72425
  notSignedInMsg: () => notSignedInMsg,
@@ -71948,15 +72452,20 @@ __export(dashboard_exports, {
71948
72452
  rereadQueueIntakes: () => rereadQueueIntakes,
71949
72453
  resetCockpitStateRecovery: () => resetCockpitStateRecovery,
71950
72454
  resolvePlan: () => resolvePlan2,
72455
+ reviewIsStale: () => reviewIsStale,
72456
+ reviewPromisesFor: () => reviewPromisesFor,
72457
+ reviewPromisesHash: () => reviewPromisesHash,
71951
72458
  routeGate: () => routeGate,
71952
72459
  runDashboard: () => runDashboard,
71953
72460
  runForTests: () => runForTests,
71954
72461
  runRefusal: () => runRefusal,
71955
72462
  runStopReason: () => runStopReason,
71956
72463
  safeDocName: () => safeDocName,
72464
+ savedPlanFitsAnswers: () => savedPlanFitsAnswers,
71957
72465
  scanGate: () => scanGate,
71958
72466
  scopeHashOf: () => scopeHashOf,
71959
72467
  scopePrefillFor: () => scopePrefillFor,
72468
+ scopeWithAsks: () => scopeWithAsks,
71960
72469
  secondClientInputHash: () => secondClientInputHash,
71961
72470
  setAccountIndustry: () => setAccountIndustry,
71962
72471
  setAccountType: () => setAccountType,
@@ -72147,7 +72656,7 @@ function withdrawIfStaffChanged(state2, locationId2) {
72147
72656
  return { state: { ...setBuildApproval(state2, locationId2, null), planApprovals: approvals }, withdrawn: true, message: ap.staffHash === void 0 ? APPROVAL_WITHDRAWN_NO_TEAM : APPROVAL_WITHDRAWN_FOR_STAFF };
72148
72657
  }
72149
72658
  function withBoughtJobs(tasks2, lines) {
72150
- const kept = tasks2.filter((t) => !SELF_REPORTED.test(t.text));
72659
+ const kept = tasks2.filter((t) => !SELF_REPORTED.test(t.text) && !HELD_JOB.test(t.text));
72151
72660
  const prior = new Map(tasks2.map((t) => [t.id, t]));
72152
72661
  return [...kept, ...lines.map((line4) => {
72153
72662
  if (typeof line4 === "string") return prior.get(taskId(line4)) ?? { id: taskId(line4), text: line4 };
@@ -72247,6 +72756,71 @@ function presetWorkflowsFor(state2, recordKey) {
72247
72756
  }
72248
72757
  return null;
72249
72758
  }
72759
+ function scopeWithAsks(state2, rk) {
72760
+ const current = state2.scope?.[rk];
72761
+ const ticks = state2.bought?.[rk];
72762
+ if (!current || !Array.isArray(ticks)) return current;
72763
+ const base = current.wholePreset ? offeredFeaturesFor(state2, rk) : current.features;
72764
+ const allowed = featuresAllowedByAsks(base, ticks, state2.asks?.[rk]);
72765
+ return allowed.length === base.length ? current : { features: allowed, setAt: current.setAt };
72766
+ }
72767
+ function intakeAskFacts(answers) {
72768
+ const get2 = (key2, label2) => {
72769
+ const v = answers?.[label2] ?? answers?.[key2];
72770
+ return typeof v === "string" && v.trim() ? v.trim() : void 0;
72771
+ };
72772
+ const { sms } = intakeChannelAnswers(answers);
72773
+ const phone = get2("has_phone_number", "Do you already have a phone number in GoHighLevel?")?.toLowerCase();
72774
+ const offer = get2("core_offer", "Core Offer");
72775
+ return { ...sms !== void 0 ? { sms } : {}, ...phone === "yes" || phone === "no" || phone === "unsure" ? { phone } : {}, ...offer ? { offer: offer.slice(0, 300) } : {} };
72776
+ }
72777
+ function buildStatusFor(st, rk, locationId2) {
72778
+ const ticks = st.bought?.[rk] ?? [];
72779
+ const intake = intakeChannelAnswers(st.intakeCompleted?.[locationId2]?.answers ?? st.intakeInputs?.[locationId2]?.answers);
72780
+ const held2 = {};
72781
+ const item = { texting: "sms", call_answering: "missed", reactivation: "reactivation" };
72782
+ for (const h of heldItems(ticks, st.asks?.[rk], effectiveTexting(st.asks?.[rk], intake.sms))) held2[item[h.id]] ??= h.line;
72783
+ const sp = readSavedPlan(rk);
72784
+ const ran = st.outcomes?.[`${rk}:3`]?.ok === true || (sp?.built?.length ?? 0) > 0;
72785
+ if (!ran) return { held: held2 };
72786
+ const featureOf = (ref) => {
72787
+ try {
72788
+ return featureOfWorkflow(ref);
72789
+ } catch {
72790
+ return null;
72791
+ }
72792
+ };
72793
+ const made = /* @__PURE__ */ new Set();
72794
+ for (const o of sp?.planId ? madeByBuild(locationId2, sp.planId) : []) if (o.type === "workflow") {
72795
+ const f = featureOf(o.ref);
72796
+ if (f) made.add(f);
72797
+ }
72798
+ for (const b of sp?.built ?? []) if (b.type === "workflow" && b.realId) {
72799
+ const f = featureOf(String(b.ref));
72800
+ if (f) made.add(f);
72801
+ }
72802
+ return { made, held: held2 };
72803
+ }
72804
+ function reviewPromisesFor(st, rk, locationId2) {
72805
+ const ticks = new Set(st.bought?.[rk] ?? []);
72806
+ const items = boughtItemsFor(offeredFeaturesFor(st, rk), boughtWordingFor(st, rk), buildStatusFor(st, rk, locationId2)).filter((i) => ticks.has(i.id));
72807
+ const willBuild = items.map((i) => /^(?:Will build|Built|Always built): (.+)$/.exec(i.delivery)?.[1]).filter((x) => Boolean(x));
72808
+ const held2 = items.map((i) => i.delivery).filter((d) => HELD_JOB.test(d));
72809
+ const intake = intakeChannelAnswers(st.intakeCompleted?.[locationId2]?.answers ?? st.intakeInputs?.[locationId2]?.answers);
72810
+ return { willBuild, held: held2, textingOn: effectiveTexting(st.asks?.[rk], intake.sms) === "yes" };
72811
+ }
72812
+ function reviewPromisesHash(st, rk, locationId2 = rk) {
72813
+ return contentHash2(reviewPromisesFor(st, rk, locationId2));
72814
+ }
72815
+ function reviewIsStale(st, rk, review) {
72816
+ return Boolean(review?.ok) && review.promisesHash !== reviewPromisesHash(st, rk);
72817
+ }
72818
+ function savedPlanFitsAnswers(sp, state2, rk) {
72819
+ if (!sp || !savedPlanMatchesScope(sp, scopeWithAsks(state2, rk))) return false;
72820
+ const asks = state2.asks?.[rk];
72821
+ if (!sp.asksHash) return !(asks && Object.keys(asks).length);
72822
+ return sp.asksHash === asksBindingHash(asks, state2.bought?.[rk]);
72823
+ }
72250
72824
  function offeredFeaturesFor(state2, recordKey) {
72251
72825
  const workflows = presetWorkflowsFor(state2, recordKey);
72252
72826
  return workflows ? featuresInPreset(workflows) : FEATURE_IDS;
@@ -72456,7 +73030,7 @@ function startRun(locationId2, stage, onDone, meta = {}) {
72456
73030
  // or before Your team decided) is made again, never built with someone the member was not shown.
72457
73031
  savedPlan: (() => {
72458
73032
  const sp = stage === 3 ? readSavedPlan(rk) : null;
72459
- return sp && sp.inputHash === meta.inputHash && savedPlanMatchesScope(sp, state2.scope?.[rk]) && savedPlanTeamIsCurrent(sp, meta.normalizedAnswers, composed) ? { planId: sp.planId, savedAt: sp.savedAt, summary: planSummary(sp) } : void 0;
73033
+ return sp && sp.inputHash === meta.inputHash && savedPlanFitsAnswers(sp, state2, rk) && savedPlanTeamIsCurrent(sp, meta.normalizedAnswers, composed) ? { planId: sp.planId, savedAt: sp.savedAt, summary: planSummary(sp) } : void 0;
72460
73034
  })(),
72461
73035
  textSnippets: Boolean(state2.textSnippets?.[rk])
72462
73036
  };
@@ -72533,7 +73107,7 @@ function startRun(locationId2, stage, onDone, meta = {}) {
72533
73107
  const at = (/* @__PURE__ */ new Date()).toISOString();
72534
73108
  const shared = { at, locationId: locationId2, stage, engineVersion: outcome.engineVersion, engineModel: outcome.engineModel };
72535
73109
  if (outcome.engineCheckRan || outcome.engineCheckCostUsd !== void 0) {
72536
- recordRunCost({ ...shared, ok: true, kind: "engine-check", costUsd: outcome.engineCheckCostUsd });
73110
+ recordRunCost({ ...shared, ok: outcome.engineCheckOk !== false, kind: "engine-check", costUsd: outcome.engineCheckCostUsd });
72537
73111
  }
72538
73112
  recordRunCost({
72539
73113
  ...shared,
@@ -72559,6 +73133,9 @@ function startRun(locationId2, stage, onDone, meta = {}) {
72559
73133
  outcome.issues = [];
72560
73134
  outcome.plain = { summary: BRIEF_NEEDS_INTAKE2, issues: [] };
72561
73135
  }
73136
+ const noted = failureNoteFor(stage, outcome, run.lines, scopeRefusal || briefNeedsIntake);
73137
+ const failNote = noted !== null;
73138
+ if (noted) Object.assign(outcome, noted);
72562
73139
  if (stage === 3 && !outcome.summary && !outcome.issues?.length) {
72563
73140
  try {
72564
73141
  const st = readCockpitState();
@@ -72574,8 +73151,8 @@ function startRun(locationId2, stage, onDone, meta = {}) {
72574
73151
  const st = readCockpitState();
72575
73152
  writeCockpitState({ ...st, outcomes: { ...st.outcomes ?? {}, [`${rk}:${stage}`]: { ok: outcome.ok, error: outcome.error, summary: outcome.summary, issues: outcome.issues, ...plain && { plain }, ...briefNeedsIntake && { reason: "intake-needed" }, at, inputHash: runInputHash, ...stage === 4 ? { buildHash: runBuildHash, verification: verifyRaw?.verification } : {}, hash: contentHash2({ inputHash: runInputHash, at, ok: outcome.ok, summary: outcome.summary, issues: outcome.issues }) } } });
72576
73153
  };
72577
- save(scopeRefusal || briefNeedsIntake ? outcome.plain : void 0);
72578
- if (PLAIN_REPORT_STAGES.has(stage) && !scopeRefusal && !briefNeedsIntake) {
73154
+ save(scopeRefusal || briefNeedsIntake || failNote ? outcome.plain : void 0);
73155
+ if (PLAIN_REPORT_STAGES.has(stage) && !scopeRefusal && !briefNeedsIntake && !failNote) {
72579
73156
  run.lines.push("Writing the plain-English version of what came back\u2026");
72580
73157
  const rewritten = await rewriteOutcomePlain(STAGES2[stage], verifyRaw ?? outcome, { onEvent: (l) => run.lines.push(l), onCost: (costUsd) => {
72581
73158
  recordRunCost({ at, locationId: locationId2, stage, kind: "plain rewrite", ok: true, costUsd });
@@ -72729,6 +73306,10 @@ async function cockpitAlreadyRunning(port) {
72729
73306
  return false;
72730
73307
  }
72731
73308
  }
73309
+ function industryChoices() {
73310
+ const packs = INDUSTRY_PACKS.map((p) => ({ slug: p.slug, label: p.label }));
73311
+ return packs.some((p) => p.slug === "other") ? packs : [...packs, { slug: "other", label: "Other" }];
73312
+ }
72732
73313
  function renderCockpitHtml(roster, seatToken = "", screen = "board", cap2) {
72733
73314
  return html(roster, seatToken, screen, cap2);
72734
73315
  }
@@ -73090,8 +73671,8 @@ function intakeRequestError(e) {
73090
73671
  function intakeErrorDetail(e) {
73091
73672
  const raw = e instanceof Error ? e.message : typeof e === "string" ? e : "";
73092
73673
  if (!raw || INTAKE_OWN_MESSAGES.includes(raw)) return void 0;
73093
- const clean5 = raw.replace(/(?:[A-Za-z]:)?(?:[\\/][^\s"'\\/]+){2,}[\\/]?/g, "[path]").replace(/\b(?:pit|sk|key|token|bearer)[-_ ][A-Za-z0-9._-]{6,}/gi, "[hidden]").replace(/\b[A-Za-z0-9_-]{24,}\b/g, "[hidden]").replace(/\s+/g, " ").trim();
73094
- return clean5 ? clean5.length > 240 ? clean5.slice(0, 237) + "\u2026" : clean5 : void 0;
73674
+ const clean6 = raw.replace(/(?:[A-Za-z]:)?(?:[\\/][^\s"'\\/]+){2,}[\\/]?/g, "[path]").replace(/\b(?:pit|sk|key|token|bearer)[-_ ][A-Za-z0-9._-]{6,}/gi, "[hidden]").replace(/\b[A-Za-z0-9_-]{24,}\b/g, "[hidden]").replace(/\s+/g, " ").trim();
73675
+ return clean6 ? clean6.length > 240 ? clean6.slice(0, 237) + "\u2026" : clean6 : void 0;
73095
73676
  }
73096
73677
  function intakeRefusal(e) {
73097
73678
  const detail = intakeErrorDetail(e);
@@ -73302,6 +73883,16 @@ ${cockpitHeaderHtml("for", { trail: ["For this client", what] })}
73302
73883
  ${cockpitShellClose()}
73303
73884
  <script>${COCKPIT_TYPE_JS}</script></body></html>`;
73304
73885
  }
73886
+ function noClientPageHtml(what, why) {
73887
+ const title2 = why === "no-client" ? `Choose a client to see ${what.toLowerCase().replace(/^the /, "their ")}` : "No Blueprint Review yet";
73888
+ const sub = why === "no-client" ? `${what} belongs to one client. Open it from that client's page.` : "Write the Brief for this client first; the Blueprint Review is written with it.";
73889
+ return `${cockpitHeadHtml(`${title2} \xB7 ${productName()}`, { extraCss: "#main{display:grid;gap:var(--space-6);align-content:start;min-width:0}" })}<body data-screen="for">
73890
+ ${cockpitHeaderHtml("for", { trail: ["For this client", what] })}
73891
+ <main id="main" class="no-client"><div class="cds-page-head"><div class="txt"><div class="cds-kicker">For this client</div><h1 class="cds-page-title">${title2}</h1><p class="cds-page-sub">${sub}</p></div></div>
73892
+ <div class="cds-empty"><p><a class="cds-btn" href="/">Back to the start</a></p></div></main>
73893
+ ${cockpitShellClose()}
73894
+ <script>${COCKPIT_TYPE_JS}</script></body></html>`;
73895
+ }
73305
73896
  function unknownCapabilityHtml() {
73306
73897
  return `${cockpitHeadHtml("No such page \xB7 Command OS", { extraCss: "#main{display:grid;gap:var(--space-6);align-content:start;min-width:0}" })}<body data-screen="for">
73307
73898
  ${cockpitHeaderHtml("for", { trail: ["For this client", "Page not found"] })}
@@ -73527,7 +74118,7 @@ function followRefresh(id,n){if(n>20)return;const tok=clientToken();setTimeout(a
73527
74118
  async function refreshSnapshots(){const tok=clientToken();const sr=await(await fetch("/api/snapshots")).json();if(!sr||!sr.ok)return;if(sr.cache)ageViews.snapshots=sr.cache;
73528
74119
  if(!stillFor(tok))return;
73529
74120
  if(JSON.stringify(sr.snapshots)!==JSON.stringify(snapshots)){snapshots=sr.snapshots;if(!userIsBusy()&&!unsavedTyping()&&lastState)renderFor(tok);else redrawPending=true}else paintAges()}
73530
- const INDUSTRIES=${scriptJson([...INDUSTRY_PACKS.map((p) => ({ slug: p.slug, label: p.label })), { slug: "other", label: "Other" }])};
74121
+ const INDUSTRIES=${scriptJson(industryChoices())};
73531
74122
  const INDUSTRIES2=${scriptJson(INDUSTRY_PACKS.map((p) => ({ slug: p.slug, label: p.label })))};
73532
74123
  /* One account at a time (Jerry, 2026-08-26: "way too busy\u2026 tempting to work on multiple
73533
74124
  accounts at once"). One card is ever expanded; every other account is a one-line row. */
@@ -74203,6 +74794,60 @@ async function profLoad(){if(profLoading)return;profLoading=true;try{profCache=(
74203
74794
  waits for the Assessment question above the list. */
74204
74795
  /* The ticks and package chosen but not yet saved, kept outside the block so a redraw keeps them (review of c09e946). */
74205
74796
  let scopeDraft={},boughtNotice={};
74797
+ /* Ask first (Jerry 2026-10-29): texting, call answering and reactivation are asked of the client before Build. The
74798
+ answers save with "Save what they bought" (server: parseAsks, asksContradiction). Nothing is pre-selected: the
74799
+ client's own form answer shows with a Change link; a phone number is only a hint. Blank = held, never built. */
74800
+ let asksDraft={};
74801
+ function asksClean(a){const o={};if(a.texting)o.texting=a.texting;if(a.callAnswering&&a.callAnswering.length)o.callAnswering=[...a.callAnswering];
74802
+ /* W44: typed text counts whenever no other offer was chosen, whatever was clicked first. */
74803
+ const r=a.reactivation;if(r&&r.list){const off=r.offer&&r.offer.text?r.offer:(r._typed?{from:"typed",text:r._typed}:{});const x={list:r.list,offer:off,channel:r.channel};if(r.list==="in_account_not_booked")x.notBookedDays=Number(r.notBookedDays);if(r.textConsentConfirmed)x.textConsentConfirmed=true;o.reactivation=x}return o}
74804
+ function asksBox(id,v,getTicked,keep){
74805
+ const box=mk("div","cds-stack ask-first");box.id="asks-"+id;const ia=v.intakeAsks||{};const savedJson=JSON.stringify(v.asks||{});
74806
+ if(!asksDraft[id]||asksDraft[id].base!==savedJson)asksDraft[id]={base:savedJson,a:JSON.parse(savedJson),change:false};
74807
+ const d=asksDraft[id],a=d.a;
74808
+ const radio=(name,val,lab,on,dis,why,act)=>{const l=mk("label","cds-check");const r=document.createElement("input");r.type="radio";r.name=name;r.checked=on;r.disabled=!!dis;r.onchange=act;l.appendChild(r);const s=mk("span","");s.appendChild(mk("b","",lab));if(why)s.appendChild(mk("small","muted",why));l.appendChild(s);return l};
74809
+ function draw(){box.innerHTML="";const t=getTicked();
74810
+ if(!(t.has("sms")||t.has("missed")||t.has("reactivation"))){box.hidden=true;return}box.hidden=false;
74811
+ box.appendChild(mk("h4","cds-panel-label","Confirm with your client before Build"));
74812
+ box.appendChild(mk("p","","Ask your client these, then choose. Nothing is built for a question left blank; it waits for your client's answer."));
74813
+ box.appendChild(mk("p","muted","The answers save with Save what they bought. Changing one after the plan is approved means approving it again."));
74814
+ const texting=a.texting||(ia.sms===true?"yes":ia.sms===false?"not_yet":"");
74815
+ const tx=mk("div","cds-stack");tx.appendChild(mk("b","","Texting"));tx.appendChild(mk("p","","Can this business text leads?"));
74816
+ if(!a.texting&&ia.sms!==undefined&&!d.change){const p=mk("p","","Your client's form said: "+(ia.sms?"Yes":"Not yet")+" ");const ch=mk("button","cds-link","Change");ch.type="button";ch.onclick=()=>{d.change=true;draw()};p.appendChild(ch);tx.appendChild(p)}
74817
+ else{const row=mk("div","cds-row");row.appendChild(radio("tx-"+id,"yes","Yes",a.texting==="yes",false,"",()=>{a.texting="yes";keep();draw()}));row.appendChild(radio("tx-"+id,"not_yet","Not yet",a.texting==="not_yet",false,"",()=>{a.texting="not_yet";keep();draw()}));tx.appendChild(row)}
74818
+ if(ia.phone==="yes")tx.appendChild(mk("small","muted","Hint: your client's form said they have a phone number in GoHighLevel."));
74819
+ box.appendChild(tx);
74820
+ if(t.has("missed")){const ca=mk("div","cds-stack");ca.appendChild(mk("b","","Call answering"));ca.appendChild(mk("p","","What did your client pay for? Choose all that apply."));
74821
+ const cur=new Set(a.callAnswering||[]);
74822
+ const opt=(val,lab,sub,why,greyed)=>{const l=mk("label","cds-check");const c=document.createElement("input");c.type="checkbox";c.checked=cur.has(val);c.disabled=greyed&&!cur.has(val);c.onchange=()=>{if(c.checked)cur.add(val);else cur.delete(val);a.callAnswering=[...cur];keep();draw()};l.appendChild(c);const s=mk("span","");s.appendChild(mk("b","",lab));if(sub)s.appendChild(mk("small","",sub));if(why)s.appendChild(mk("small","muted",why));l.appendChild(s);ca.appendChild(l)};
74823
+ opt("live_voice","An AI answers the phone live","Your client connects their number to it afterwards. Once it is connected, GoHighLevel charges for every minute it talks, usually from your agency's wallet.","",false);
74824
+ opt("chat_bot","An AI replies by text or chat and books the visit","",texting==="yes"?"":"Texting replies need Texting: Yes.",false);
74825
+ opt("missed_text","A text back after a missed call","",texting==="yes"?"":"Needs texting: Yes",texting!=="yes");
74826
+ box.appendChild(ca)}
74827
+ if(t.has("reactivation")){const re=a.reactivation||(a.reactivation={});const rv=mk("div","cds-stack");rv.appendChild(mk("b","","Reactivation"));
74828
+ rv.appendChild(mk("p","","Who should it reach?"));
74829
+ rv.appendChild(radio("rl-"+id,"import","A list your client will give you to import",re.list==="import",false,"",()=>{re.list="import";keep();draw()}));
74830
+ /* rc.3 c6 hole 4: the number is in the sentence as text, and its box sits on its own labelled line. */
74831
+ const nDays=re.notBookedDays||90;
74832
+ rv.appendChild(radio("rl-"+id,"in_account_not_booked","Contacts already here who have not booked in "+nDays+" days",re.list==="in_account_not_booked",false,"",()=>{re.list="in_account_not_booked";if(!re.notBookedDays)re.notBookedDays=90;keep();draw()}));
74833
+ if(re.list==="in_account_not_booked"){const row=mk("label","cds-row");row.appendChild(mk("span","","Days with no booking: "));const days=document.createElement("input");days.type="number";days.min="1";days.max="3650";days.className="cds-input";days.style.width="6em";days.value=String(nDays);days.onchange=()=>{const n=Math.round(Number(days.value));if(n>=1&&n<=3650){re.notBookedDays=n;keep();draw()}else days.value=String(nDays)};row.appendChild(days);rv.appendChild(row);
74834
+ rv.appendChild(mk("small","muted","You will see how many people this reaches and approve it before anyone gets a message."))}
74835
+ rv.appendChild(radio("rl-"+id,"cold_from_now","Only leads who go cold from now on",re.list==="cold_from_now",false,"",()=>{re.list="cold_from_now";keep();draw()}));
74836
+ rv.appendChild(mk("p","","What offer brings them back?"));
74837
+ /* W44: with no offer in the brief, typing one is the only choice, so it starts chosen. */
74838
+ if(!ia.offer&&!re.offer)re.offer={from:"typed",text:""};
74839
+ if(ia.offer)rv.appendChild(radio("ro-"+id,"brief","From the brief: \\u201c"+ia.offer+"\\u201d",re.offer&&re.offer.from==="brief",false,"",()=>{re.offer={from:"brief",text:ia.offer};keep();draw()}));
74840
+ const typed=radio("ro-"+id,"typed","Type one:",re.offer&&re.offer.from==="typed",false,"",()=>{re.offer={from:"typed",text:ot.value.trim()};keep();draw()});
74841
+ /* W45 (Jerry, Safari): inside the label the box drew as a tiny oval. It is its own full-width field under its choice. */
74842
+ const ot=document.createElement("textarea");ot.rows=2;ot.maxLength=300;ot.className="cds-input ask-offer";ot.style.width="100%";ot.style.boxSizing="border-box";ot.placeholder="e.g. a free skin check";ot.value=re.offer&&re.offer.from==="typed"?re.offer.text:"";ot.setAttribute("aria-label","The offer, in your client's words");ot.oninput=()=>{re._typed=ot.value.trim();re.offer={from:"typed",text:re._typed};keep();typed.querySelector("input").checked=true};
74843
+ rv.appendChild(typed);rv.appendChild(ot);
74844
+ rv.appendChild(mk("p","","How?"));
74845
+ const canText=texting==="yes"&&re.textConsentConfirmed===true;
74846
+ rv.appendChild(radio("rc-"+id,"email","Email",re.channel==="email",false,"",()=>{re.channel="email";keep();draw()}));
74847
+ rv.appendChild(radio("rc-"+id,"email_and_text","Email and text",re.channel==="email_and_text",!canText&&re.channel!=="email_and_text",texting!=="yes"?"Needs texting: Yes":re.textConsentConfirmed?"":"Needs your client to confirm these people agreed to get texts",()=>{re.channel="email_and_text";keep();draw()}));
74848
+ const cl=mk("label","cds-check");const cc=document.createElement("input");cc.type="checkbox";cc.checked=re.textConsentConfirmed===true;cc.onchange=()=>{if(cc.checked)re.textConsentConfirmed=true;else{delete re.textConsentConfirmed;if(re.channel==="email_and_text")re.channel="email"}keep();draw()};cl.appendChild(cc);cl.appendChild(mk("span","","My client confirmed these people agreed to get texts"));rv.appendChild(cl);
74849
+ box.appendChild(rv)}}
74850
+ draw();box.redraw=draw;return box}
74206
74851
  const BOUGHT_ITEMS=${scriptJson(boughtItemsFor(FEATURE_IDS))};
74207
74852
  function scopeChosen(state,id){const s=(state.scope||{})[id];const v=(state.boughtViews||{})[id];return Boolean(s&&(s.wholePreset||(Array.isArray(s.features)&&s.features.length)))&&!(v&&v.ready===false)}
74208
74853
  function openWhatTheyBought(id){rOpen[id]=true;rSeen[id]=true;const go=()=>{render(lastState,lastRun);requestAnimationFrame(()=>{const t=document.getElementById("wtb-"+id);if(t){t.scrollIntoView({behavior:"smooth",block:"start"});const f=t.querySelector("button,input,select");if(f)f.focus({preventScroll:true})}})};if(!rData[id])rLoad(id).then(go,go);else go()}
@@ -74225,7 +74870,7 @@ function scopeBlock(id,state){
74225
74870
  const v=boughtView(state,id);
74226
74871
  if(!bsHideAssess)sc.appendChild(assessmentQuestion(id,v));
74227
74872
  sc.appendChild(mk("h4","cds-panel-label","What they bought"));
74228
- sc.appendChild(mk("p","",v.source==="all"?"Everything starts ticked. Untick what this client did not buy.":v.source==="assessment"?"Ticked from what the Assessment recorded as bought. Untick anything they did not buy.":"Tick what the client bought. Only the items marked Built get built."));
74873
+ sc.appendChild(mk("p","",v.source==="all"?"Everything starts ticked. Untick what this client did not buy.":v.source==="assessment"?"Ticked from what the Assessment recorded as bought. Untick anything they did not buy.":"Tick what the client bought. Only the items marked Will build get built."));
74229
74874
  if(!profCache){profLoad();const p=document.createElement("p");p.className="muted";p.textContent="Loading your packages\u2026";sc.appendChild(p);return sc}
74230
74875
  const saved=(state.scope||{})[id]||null;const pk=(profCache.packages||[]);
74231
74876
  const sel=document.createElement("select");sel.className="cds-select";sel.setAttribute("aria-label","Package they bought");
@@ -74236,17 +74881,17 @@ function scopeBlock(id,state){
74236
74881
  const keepScope=()=>{scopeDraft[id]={ticked:[...ticked],pkg:sel.value,base:base};unsaved.hidden=false};
74237
74882
  const everything=document.createElement("button");everything.className="cds-btn ghost";everything.textContent="Tick everything";
74238
74883
  everything.title="Ticks all fourteen. Nothing is saved until you press Save what they bought.";
74239
- everything.onclick=()=>{ticked=new Set(v.items.map(i=>i.id));keepScope();drawBoxes();drawFigs()};
74884
+ everything.onclick=()=>{ticked=new Set(v.items.map(i=>i.id));keepScope();drawBoxes();drawFigs();if(askEl)askEl.redraw()};
74240
74885
  sc.appendChild(everything);
74241
74886
  const boxes=document.createElement("div");boxes.className="cds-stack";
74242
74887
  function drawBoxes(){boxes.innerHTML="";for(const it of v.items){const l=document.createElement("label");l.className="cds-check";
74243
- l.dataset.k=it.id;const c=document.createElement("input");c.type="checkbox";c.checked=ticked.has(it.id);c.onchange=()=>{if(c.checked)ticked.add(it.id);else ticked.delete(it.id);keepScope();drawFigs()};l.appendChild(c);
74888
+ l.dataset.k=it.id;const c=document.createElement("input");c.type="checkbox";c.checked=ticked.has(it.id);c.onchange=()=>{if(c.checked)ticked.add(it.id);else ticked.delete(it.id);keepScope();drawFigs();if(askEl)askEl.redraw()};l.appendChild(c);
74244
74889
  const t=document.createElement("span");const tb=document.createElement("b");tb.textContent=it.name;t.appendChild(tb);t.appendChild(mk("small","",it.means));
74245
- t.appendChild(mk("small","bought-delivery",it.delivery));if(it.next)t.appendChild(mk("small","bought-next",it.next));l.appendChild(t);boxes.appendChild(l)}}
74890
+ t.appendChild(mk("small","bought-delivery",it.delivery));if(it.next&&!(((asksDraft[id]&&asksDraft[id].a.callAnswering)||(v.asks||{}).callAnswering||[]).includes("live_voice")))t.appendChild(mk("small","bought-next",it.next));l.appendChild(t);boxes.appendChild(l)}}
74246
74891
  const figs=((state.assessmentFigures||{})[id])||{};
74247
74892
  function drawFigs(){for(const l of boxes.querySelectorAll("label")){const it=v.items.find(i=>i.id===l.dataset.k);const old=l.querySelector("em");if(old)old.remove();if(it&&it.feature&&figs[it.feature]){const e=document.createElement("em");e.className="cds-chip"+(ticked.has(it.id)?"":" warn");e.textContent="Assessment: "+figs[it.feature]+(ticked.has(it.id)?".":". Not ticked.");l.querySelector("span").appendChild(e)}}}
74248
74893
  /* A package names build features: it ticks their items and unticks the other built ones; the rest stay as they are. */
74249
- sel.onchange=()=>{const p=pk.find(x=>x.id===sel.value);if(p)for(const it of v.items){if(!it.feature)continue;if((p.features||[]).includes(it.feature))ticked.add(it.id);else ticked.delete(it.id)}keepScope();drawBoxes();drawFigs()};drawBoxes();drawFigs();
74894
+ sel.onchange=()=>{const p=pk.find(x=>x.id===sel.value);if(p)for(const it of v.items){if(!it.feature)continue;if((p.features||[]).includes(it.feature))ticked.add(it.id);else ticked.delete(it.id)}keepScope();drawBoxes();drawFigs();if(askEl)askEl.redraw()};let askEl=null;drawBoxes();drawFigs();askEl=asksBox(id,v,()=>ticked,keepScope);
74250
74895
  const note=document.createElement("p");note.className="muted";note.textContent="Always built, whatever is ticked: the pipeline and its stages, the fields, tags and custom values, the intake form, the calendar, and the reply routing. A ticked item that is not built automatically becomes one of your jobs in the account.";
74251
74896
  const sv=document.createElement("button");sv.className="cds-btn ghost";sv.textContent="Save what they bought";
74252
74897
  const status=document.createElement("p");status.className="muted";status.setAttribute("role","status");
@@ -74254,14 +74899,17 @@ function scopeBlock(id,state){
74254
74899
  if(!v.items.some(it=>it.kind==="built"&&ticked.has(it.id))){status.textContent="None of the ticked items is built for this client's industry. Tick at least one item marked Built.";return}
74255
74900
  /* Run 8 finding 9: about 5 seconds with nothing showing it was working. */
74256
74901
  sv.disabled=true;const was=sv.textContent;sv.textContent="Saving\u2026";status.textContent="Saving what they bought\u2026";
74257
- const body={locationId:id,bought:[...ticked]};if(sel.value)body.package=sel.value;const r=await postJSON("/api/scope",body);
74258
- if(r){unsaved.hidden=true;if(r.approvalWithdrawn)boughtNotice[id]=r.message;else delete boughtNotice[id];delete scopeDraft[id];delete assessmentCards[id];status.textContent="Saved.";load()}
74902
+ const body={locationId:id,bought:[...ticked]};if(sel.value)body.package=sel.value;if(asksDraft[id])body.asks=asksClean(asksDraft[id].a);const r=await postJSON("/api/scope",body);
74903
+ /* OS audit F11: the Review panel kept the copy it read before this save, so it never said the Review was now out of
74904
+ date while the client document refused it. Read it again after every save. */
74905
+ if(r){unsaved.hidden=true;delete asksDraft[id];if(r.approvalWithdrawn)boughtNotice[id]=r.message;else delete boughtNotice[id];delete scopeDraft[id];delete assessmentCards[id];status.textContent="Saved.";if(rData[id])rLoad(id);load()}
74259
74906
  if(sv.isConnected){sv.disabled=false;sv.textContent=was}};
74260
74907
  /* Audit 2026-10-28 #9: the always-built pipeline counts as built ("2 bought, 1 built" while it is always built). */
74261
- const nBought=v.ticks.length,nBuilt=v.items.filter(it=>(it.kind==="built"||it.kind==="always")&&v.ticks.includes(it.id)).length;
74262
- status.textContent=saved&&v.source==="saved"?"Saved: "+nBought+" of "+v.items.length+" bought, "+nBuilt+" of them built automatically"+(saved.package?(pk.find(x=>x.id===saved.package)?", from the "+pk.find(x=>x.id===saved.package).name+" package":", from a package no longer on Your agency"):"")+". The build follows this.":saved?(saved.wholePreset?"Saved: everything in the plan. The build follows this.":"Saved before this list: "+saved.features.length+" features. Save again to record what they bought."):"Not saved yet. Save what they bought before approving.";
74908
+ /* rc.3 c6 hole 2: the count follows the labels: what will be built (or was) and what waits on your client's answer. */
74909
+ const tickedItems=v.items.filter(it=>v.ticks.includes(it.id));const nBought=v.ticks.length,nBuilt=tickedItems.filter(it=>/^(Will build|Built|Always built): /.test(it.delivery)).length,nHeld=tickedItems.filter(it=>/^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /.test(it.delivery)).length;
74910
+ status.textContent=saved&&v.source==="saved"?"Saved: "+nBought+" of "+v.items.length+" bought. "+nBuilt+(nBuilt===1?" is":" are")+" built automatically"+(nHeld?", "+nHeld+(nHeld===1?" waits":" wait")+" for your client's answer":"")+(saved.package?(pk.find(x=>x.id===saved.package)?", from the "+pk.find(x=>x.id===saved.package).name+" package":", from a package no longer on Your agency"):"")+". The build follows this.":saved?(saved.wholePreset?"Saved: everything in the plan. The build follows this.":"Saved before this list: "+saved.features.length+" features. Save again to record what they bought."):"Not saved yet. Save what they bought before approving.";
74263
74911
  const unsaved=mk("span","cds-chip warn","Not saved yet");unsaved.hidden=!scopeDraft[id];
74264
- sc.appendChild(sel);sc.appendChild(boxes);sc.appendChild(note);sc.appendChild(sv);sc.appendChild(unsaved);sc.appendChild(status);
74912
+ sc.appendChild(sel);sc.appendChild(boxes);sc.appendChild(askEl);sc.appendChild(note);sc.appendChild(sv);sc.appendChild(unsaved);sc.appendChild(status);
74265
74913
  if(boughtNotice[id]){const n=mk("p","cds-callout warn",boughtNotice[id]);n.setAttribute("role","alert");sc.appendChild(n)}
74266
74914
  return sc}
74267
74915
  async function rLoad(id){const tok=clientToken();const r=await(await fetch("/api/review?locationId="+id)).json();rData[id]=r;renderFor(tok)}
@@ -74290,12 +74938,20 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
74290
74938
  rBusy[id]=false;load()};
74291
74939
  if(busy){const p=document.createElement("p");p.className="muted";
74292
74940
  p.textContent="Reading their answers and writing the review, usually 1 to 2 minutes. This panel updates itself when it is ready.";d.appendChild(p)}
74941
+ /* rc.3 c6 hole 1: a review written before the answers changed may promise what the build will not make. */
74942
+ /* OS audit F11 (ruling (c)): one plain line, and the two choices on the row below: Approve anyway, or Update the review. */
74943
+ if(r&&r.ok&&r.stale&&!busy){const w=mk("p","cds-callout warn","This review was written before your last change. Approve anyway, or update the review first; the client document waits for an updated one.");w.setAttribute("role","alert");d.appendChild(w)}
74944
+ if(r&&r.ok&&!r.stale&&r.graded!==true&&!busy){const w=mk("p","cds-callout warn","Not checked for over-promises. Regenerate.");w.setAttribute("role","alert");d.appendChild(w)}
74945
+ /* OS audit F12: the trimmed lines were one run-on sentence ("...regular schedule today?.") read first. Now a folded
74946
+ note: how many lines, why, and each line as it was, with nothing added to its punctuation. */
74947
+ if(r&&r.ok&&Array.isArray(r.trimmed)&&r.trimmed.length&&!busy){const n=r.trimmed.length;const det=mk("details","cds-details review-trimmed");det.appendChild(mk("summary","",n+(n===1?" line was":" lines were")+" taken out of this review because "+(n===1?"it":"they")+" promised more than this build makes"));const ul=mk("ul","cds-list");for(const t of r.trimmed){ul.appendChild(mk("li","",String(t)))}det.appendChild(ul);d.appendChild(det)}
74293
74948
  if(!r||!r.ok){if(!busy){const p=document.createElement("p");if(r&&r.error&&r.error!=="not generated yet"){p.className="cds-callout warn";p.setAttribute("role","alert")}else p.className="muted";
74294
74949
  p.textContent=r&&r.error?r.error:"Turn their intake answers into strategy: who they really sell to, whether the offer holds up, what the account is missing, and what to build first.";
74295
74950
  d.appendChild(p)}
74296
74951
  d.appendChild(gen);
74297
- /* What they bought decides the build, so it is here even before the review is written (2026-09-29). */
74298
- d.appendChild(scopeBlock(id,state));
74952
+ /* What they bought decides the build, so it is here even before the review is written (2026-09-29). On the steps
74953
+ page it is its own step 6, before the Brief (OS audit F11, ruling (c)), so it is not shown twice. */
74954
+ if(!bsHideAssess)d.appendChild(scopeBlock(id,state));
74299
74955
  /* On the steps page Approve never waits for the review: the server does not need it (round-4 review). */
74300
74956
  if(bsHideAssess&&scopeChosen(state,id)&&!(state.buildApproved||{})[id]&&!PROTECTED[id])d.appendChild(approveButton(id,name,state,"review","cds-btn"));
74301
74957
  return d}
@@ -74316,18 +74972,19 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
74316
74972
  if((r.gaps||[]).length){const s3=document.createElement("div");s3.className="cds-stack";const gh=document.createElement("h4");gh.className="cds-panel-label";gh.textContent="What's missing ("+r.gaps.length+")";s3.appendChild(gh);
74317
74973
  const gl=mk("ul","cds-list gaps");
74318
74974
  for(const g of r.gaps){const gg=mk("li","gap "+(g.priority||"soon"));
74319
- const pri=g.priority||"soon";const gt=mk("span","ttl",String(g.title||""));gt.appendChild(mk("span","sub",moneyText(joinSentences(g.cost,g.fix))));gg.appendChild(gt);
74975
+ const pri=g.priority||"soon";const gt=mk("span","ttl",String(g.title||""));gt.appendChild(mk("span","sub",moneyText(String(g.cost||""))));/* rc.5: the fix is never the model's words; what we build is listed below, from the build */gg.appendChild(gt);
74320
74976
  const ge=mk("span","end");ge.appendChild(chip(pri==="now"?"warn":"",pri==="now"?"Fix now":pri==="later"?"Later":"Soon"));gg.appendChild(ge);gl.appendChild(gg)}
74321
74977
  s3.appendChild(gl);
74322
74978
  d.appendChild(s3)}
74323
- if((r.build||[]).length){const s4=document.createElement("div");s4.className="cds-stack";s4.innerHTML="<h4 class='cds-panel-label'>What we build</h4>";
74979
+ /* rc.5 (c6 ruling): What we build is the build's own will-build list and held lines, never the model's prose. */
74980
+ {const pr=r.promises;if(pr&&((pr.willBuild||[]).length||(pr.held||[]).length)){const s4=document.createElement("div");s4.className="cds-stack";s4.innerHTML="<h4 class='cds-panel-label'>What we build</h4>";
74324
74981
  const bl=document.createElement("ul");bl.className="cds-list";
74325
- for(const b of r.build){const bb=document.createElement("li");bb.className="buildline";
74326
- const bt2=mk("span","ttl",String(b.item||""));bt2.appendChild(mk("span","sub",moneyText(b.because||"")));bb.appendChild(bt2);bl.appendChild(bb)}
74327
- s4.appendChild(bl);d.appendChild(s4)}
74982
+ for(const w of (pr.willBuild||[])){const bb=document.createElement("li");bb.className="buildline";bb.appendChild(mk("span","ttl",String(w)));bl.appendChild(bb)}
74983
+ for(const h of (pr.held||[])){const bb=document.createElement("li");bb.className="buildline";bb.appendChild(mk("span","ttl muted",String(h)));bl.appendChild(bb)}
74984
+ s4.appendChild(bl);d.appendChild(s4)}}
74328
74985
  if((r.unknowns||[]).length){const s5=document.createElement("div");s5.className="cds-stack";s5.innerHTML="<h4 class='cds-panel-label'>Ask them next</h4>";
74329
74986
  const p=document.createElement("p");p.textContent=r.unknowns.join(" \xB7 ");s5.appendChild(p);d.appendChild(s5)}
74330
- d.appendChild(scopeBlock(id,state));
74987
+ if(!bsHideAssess)d.appendChild(scopeBlock(id,state));
74331
74988
  d.appendChild(mk("h4","cds-panel-label","The client document: your agency name and color on it"));const brand=document.createElement("div");brand.className="cds-row brand-row";
74332
74989
  const an=document.createElement("input");an.placeholder="Your agency name (on the client doc)";an.value=(state.agencyName||"");
74333
74990
  an.className="cds-input";an.setAttribute("aria-label","Your agency name on the client document");
@@ -74349,6 +75006,7 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
74349
75006
  if((state.buildApproved||{})[id]&&intakeStale){const ap=document.createElement("button");ap.className="cds-btn ghost";ap.textContent="Approve again once the intake is reviewed";ap.disabled=true;row.appendChild(ap)}
74350
75007
  else if((state.buildApproved||{})[id]&&!approvalStale){const ap=document.createElement("button");ap.className="cds-btn ghost";ap.textContent="Approved";ap.disabled=true;row.appendChild(ap)}
74351
75008
  else if(scopeChosen(state,id)&&!["template","hidden"].includes(typeOf(state,id))&&!PROTECTED[id])row.appendChild(approveButton(id,name,state,"review","cds-btn"));
75009
+ if(r.stale&&!busy){gen.textContent="Update the review (uses Claude)";const ab=row.querySelector("button.cds-btn:not(.ghost)");if(ab&&/^Approve this plan$/.test(ab.textContent))ab.textContent="Approve anyway"}
74352
75010
  row.appendChild(gen);d.appendChild(row);
74353
75011
  return d}
74354
75012
  const NC_AGENCY=${scriptJson(readAgencyProfile().agencyName?.trim() || "in GoHighLevel")};
@@ -75132,7 +75790,7 @@ function autoBuildClaim(id,b){if(!autoBuild[id])return;queueMicrotask(()=>{const
75132
75790
  function approveSpec(id,name,state){const sandbox=typeOf(state,id)==="sandbox";
75133
75791
  const view=(state.buildViews||{})[id];const repairs=(view&&view.planHash&&view.repairs)||[];
75134
75792
  return {title:"Approve this plan for "+name,
75135
- text:["Command OS then builds in this account what the Blueprint Review lists"+(repairs.length?", and makes the "+repairs.length+" live workflow "+plural(repairs.length,"repair")+" listed with the plan":"")+".","When you approve, step 9 opens. Click Start the build there."],
75793
+ text:["Command OS then builds in this account what the Blueprint Review lists"+(repairs.length?", and makes the "+repairs.length+" live workflow "+plural(repairs.length,"repair")+" listed with the plan":"")+".","When you approve, step 10 opens. Click Start the build there."],
75136
75794
  after:async()=>{load()},
75137
75795
  fields:[NAME_FIELD()].concat(sandbox?[]:[{id:"client",label:"Type the client name to confirm: "+name,value:""}]),
75138
75796
  ok:"Approve",busyLabel:"Approving\u2026",
@@ -75407,7 +76065,9 @@ function renderCapability(m,state,run){
75407
76065
  if(CAP.key==="intake"){const go=mk("a","cds-btn cap-build-account","Build this client's account");end.appendChild(asClientLink(go,"/new",c.locationId))}
75408
76066
  m.appendChild(screenHead(CAP.label,CAP.what,CAP.groupLabel,end));
75409
76067
  const sc=document.getElementById("side-client");if(sc)sc.textContent=c.name;
75410
- if(CAP.key==="intake"){renderIntakeCapability(m,c);return}
76068
+ /* W43 (Jerry's watched walk, 2026-10-29): he saw no Build button on the page Your clients leads to. The header carries
76069
+ one; this puts the next step at the top of the page itself, where nobody can miss it. */
76070
+ if(CAP.key==="intake"){const nx=mk("div","cds-callout cap-next-build");nx.appendChild(mk("span","ttl","Next step: build this client's account"));nx.appendChild(mk("span","","Answers, approval and the build itself are on the build page."));const go=mk("a","cds-btn cap-build-account-main","Build this client's account");nx.appendChild(asClientLink(go,"/new",c.locationId));m.appendChild(nx);renderIntakeCapability(m,c);return}
75411
76071
  const built=capBuiltItems(view,CAP.key);
75412
76072
  const toCheck=built.filter(x=>x.status==="uncertain");
75413
76073
  const health=HEALTH_PAGES.includes(CAP.key);
@@ -75434,7 +76094,8 @@ function renderCapability(m,state,run){
75434
76094
  if(CAP.key==="checkup"){const v=capCheckupPanel(last,bb);capReadsInto(v,c,hasBuild,true);m.appendChild(v)}
75435
76095
  else{
75436
76096
  /* In this account now, read live from GoHighLevel. */
75437
- const card=panel("Read live from GoHighLevel","In this account now");card.classList.add("cap-reads");
76097
+ /* OS audit F16: a page that reads nothing said "Read live from GoHighLevel" above "This page does not read the account yet". */
76098
+ const card=CAP.reads.length||CAP.key==="handoff"?panel("Read live from GoHighLevel","In this account now"):panel("Not read on this page","Ask Claude below");card.classList.add("cap-reads");
75438
76099
  capReadsInto(card,c,hasBuild,false);m.appendChild(card)}
75439
76100
  m.appendChild(capDoPanel(c,state,run,words,hasBuild,busy,last));
75440
76101
  /* From the last build: only this page's own items. */
@@ -75448,7 +76109,25 @@ function renderCapability(m,state,run){
75448
76109
  if(health)m.appendChild(capOwedPanel(owed,(state.jobCounts||{})[c.locationId],((view||{}).handoff||{}).lines))}
75449
76110
  /* What this page reads live: one list for one read. With several reads, a read of 3 or fewer shows its rows under its own label,
75450
76111
  and a longer one folds under a closed "See the N \u2026". A count read is one row: the number. */
76112
+ /* Sign-off and handoff (command-center 2026-10-29): read from this client's own build record, the same one the build
76113
+ steps show (F2): what is Built, what waits on the client (the held lines), and the jobs still open. Never GoHighLevel's
76114
+ words for it, and never a claim the record does not hold. */
76115
+ const HELD_RE=/^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
76116
+ function capHandoffRecord(card,c){
76117
+ const v=((lastState&&lastState.boughtViews)||{})[c.locationId];
76118
+ if(!v){card.appendChild(mk("p","muted","Nothing is on record for this client yet."));return}
76119
+ const ticked=new Set(v.ticks||[]);const items=(v.items||[]).filter(i=>ticked.has(i.id));
76120
+ const built=items.filter(i=>/^Built: /.test(i.delivery)).map(i=>i.delivery.replace(/^Built: /,""));
76121
+ const waiting=items.filter(i=>HELD_RE.test(i.delivery)).map(i=>i.delivery);
76122
+ const open=(((lastState&&lastState.manualTasks)||{})[c.locationId]||[]).filter(t=>!t.claimed&&!HELD_RE.test(String(t.text))).map(t=>String(t.text));
76123
+ const sec=(title,list,none)=>{const b=mk("div","cap-read");b.appendChild(mk("b","",title));if(!list.length)b.appendChild(mk("p","muted",none));else{const u=mk("ul","");for(const x of list.slice(0,8))u.appendChild(mk("li","",x));if(list.length>8)u.appendChild(mk("li","muted","and "+(list.length-8)+" more"));b.appendChild(u)}card.appendChild(b)};
76124
+ card.appendChild(mk("p","muted","From this client's build record on this computer, the same record the build steps show."));
76125
+ sec("Built",built,"Nothing is recorded as built yet.");
76126
+ sec("Waiting on your client",waiting,"Nothing is waiting on your client.");
76127
+ sec("Still open",open,"No open jobs.");
76128
+ }
75451
76129
  function capReadsInto(card,c,hasBuild,foldAll){
76130
+ if(CAP.key==="handoff"){capHandoffRecord(card,c);return}
75452
76131
  if(!CAP.reads.length){card.appendChild(capEmpty("This page does not read the account yet","Say the words below to Claude, and it reads what this account has for this job, live."));return}
75453
76132
  if(capReadsFor!==c.locationId){card.appendChild(mk("p","muted","Reading this account\u2026"));loadCapReads(c.locationId);return}
75454
76133
  if(!capReads){card.appendChild(mk("p","muted","Reading this account\u2026"));return}
@@ -75552,7 +76231,7 @@ function capOwedPanel(owed,counts,hl){if(!counts){counts={before:0,atGoLive:0,la
75552
76231
  const jobs=owed.filter(x=>x.job),lines=hl||[];
75553
76232
  if(!jobs.length&&!lines.length){p.appendChild(capEmpty("Nothing is recorded as owed yet","The handoff checks and the manual tasks for this client show here once the build has been verified."));return p}
75554
76233
  const c=counts||{before:0,atGoLive:0,later:0};
75555
- p.appendChild(mk("p","",c.before+" "+plural(c.before,"job")+" before sign-off, "+c.atGoLive+" "+plural(c.atGoLive,"automation")+" to switch on, "+c.later+" for later: the same jobs as step 11."));
76234
+ p.appendChild(mk("p","",c.before+" "+plural(c.before,"job")+" before sign-off, "+c.atGoLive+" "+plural(c.atGoLive,"automation")+" to switch on, "+c.later+" for later: the same jobs as step 12."));
75556
76235
  if(lines.length)p.appendChild(mk("p","muted",handoffCountLine(lines,c)));
75557
76236
  p.appendChild(capList(jobs.map(x=>capRow(x.text,"",x.done?chip("ok","Done"):chip(x.timing==="before"?"warn":"",x.timing==="before"?"Before sign-off":x.timing==="at-go-live"?"After sign-off":"For later"))),"jobs"));return p}
75558
76237
  /* Intake and documents: what is answered, what is still open, the key facts, and the files. */
@@ -76995,10 +77674,11 @@ async function runDashboard(argv) {
76995
77674
  const hit = await pickIntakeContact(c, locationId2, byId2, snap.contactId, INTAKE_FORM_NAME2);
76996
77675
  const completed = st0.intakeCompleted?.[locationId2]?.answers;
76997
77676
  const answers = Object.fromEntries(Object.entries(completed ?? normalizedIntake({ answers: hit?.answers ?? {}, answeredAt: snap.answeredAt }, st0.intakeDrafts?.[locationId2])).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : v]));
76998
- return generateReview(
77677
+ const review = await generateReview(
76999
77678
  (prompt) => callClaudeRecorded(claudeBin(), ["-p", prompt, "--allowedTools", "", "--disallowedTools", `Bash,Write,Edit,WebFetch,WebSearch,Task,Read,Glob,Grep,${MCP_DENY}`, "--max-turns", "6", ...NO_MCP_ARGS], { locationId: locationId2, stage: 1, kind: "review" }),
77000
- { clientName, industry: st0.accounts?.[locationId2]?.industry, answers, ...st0.assessmentAttachments?.[locationId2] ? { assessmentLines: findingLines(st0.assessmentAttachments[locationId2]) } : {} }
77679
+ { clientName, industry: st0.accounts?.[locationId2]?.industry, answers, ...st0.assessmentAttachments?.[locationId2] ? { assessmentLines: findingLines(st0.assessmentAttachments[locationId2]) } : {}, promises: reviewPromisesFor(st0, locationId2, locationId2) }
77001
77680
  );
77681
+ return review.ok ? { ...review, promisesHash: contentHash2(review.promises ?? reviewPromisesFor(st0, locationId2, locationId2)) } : review;
77002
77682
  };
77003
77683
  const currentRoster = () => {
77004
77684
  const registered = registry2.listLocations();
@@ -77097,7 +77777,7 @@ async function runDashboard(argv) {
77097
77777
  if (other) cockpitCache.invalidate(account2.locationId, ["brief-account"]);
77098
77778
  const v = other ? { cold: true, refreshing: v0.refreshing, stale: true } : v0;
77099
77779
  seen.push({ account: account2, v });
77100
- if (v.cold) throw !other && v.lastRefresh?.text ? new Error(v.lastRefresh.text) : new AccountLoading(COLD_TEXT);
77780
+ if (v.cold) throw !other && !v.refreshing && v.lastRefresh?.text ? new Error(v.lastRefresh.text) : new AccountLoading(COLD_TEXT);
77101
77781
  return v.data.read;
77102
77782
  },
77103
77783
  ages: () => {
@@ -77652,7 +78332,12 @@ ${opportunityNoteFailure?.detail ?? ""}`);
77652
78332
  const assessmentFigures = {};
77653
78333
  for (const loc of Object.keys(st.assessmentAttachments ?? {})) assessmentFigures[loc] = assessmentFiguresFor(st, loc);
77654
78334
  const scopeOffered = Object.fromEntries(rosterNow.map((c) => [c.locationId, offeredFeaturesFor(st, c.locationId)]));
77655
- const boughtViews = Object.fromEntries(rosterNow.map((c) => [c.locationId, boughtViewFor(st, c.locationId, scopeOffered[c.locationId], boughtWordingFor(st, c.locationId))]));
78335
+ const boughtViews = Object.fromEntries(rosterNow.map((c) => [c.locationId, {
78336
+ ...boughtViewFor(st, c.locationId, scopeOffered[c.locationId], boughtWordingFor(st, c.locationId), buildStatusFor(st, c.locationId, c.locationId)),
78337
+ // Ask first (2026-10-29): the saved answers and what the client's own form said, for the questions box.
78338
+ asks: st.asks?.[c.locationId] ?? {},
78339
+ intakeAsks: intakeAskFacts(st.intakeCompleted?.[c.locationId]?.answers ?? st.intakeInputs?.[c.locationId]?.answers)
78340
+ }]));
77656
78341
  const buildViews = Object.fromEntries(rosterNow.map((c) => {
77657
78342
  const saved = readSavedPlan(c.locationId);
77658
78343
  const approval = st.planApprovals?.[c.locationId];
@@ -77880,7 +78565,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
77880
78565
  res.writeHead(200, { "Content-Type": "application/json" });
77881
78566
  const proClient = gate3.plan === "pro" ? new URL(req.url || "/", "http://x").searchParams.get("locationId") ?? "" : void 0;
77882
78567
  const mine = activeRun2 && (proClient === void 0 || activeRun2.locationId === proClient);
77883
- res.end(JSON.stringify(mine && activeRun2 ? { ...activeRun2, lines: memberProgress(activeRun2.lines, activeRun2.running, activeRun2.ok) } : activeRun2?.running ? { running: true, elsewhere: true, lines: [] } : { running: false, lines: [] }));
78568
+ res.end(JSON.stringify(mine && activeRun2 ? { ...activeRun2, lines: memberProgress(activeRun2.lines, activeRun2.running, activeRun2.ok, activeRun2.capability ? void 0 : activeRun2.stage) } : activeRun2?.running ? { running: true, elsewhere: true, lines: [] } : { running: false, lines: [] }));
77884
78569
  return;
77885
78570
  }
77886
78571
  if (req.method === "GET" && url === "/api/run/secrets") {
@@ -77926,7 +78611,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
77926
78611
  if (stage === 3) {
77927
78612
  assertStaffUnchanged(st0.planApprovals?.[rk], st0.staffRoster?.[locationId2]);
77928
78613
  const sp = readSavedPlan(rk);
77929
- if (!sp || !savedPlanMatchesScope(sp, st0.scope?.[rk])) throw new Error("The features selected for this client changed after its plan was prepared. Prepare the plan again, approve it, then build.");
78614
+ if (!sp || !savedPlanFitsAnswers(sp, st0, rk)) throw new Error("The features selected for this client, or the answers about them, changed after its plan was prepared. Prepare the plan again, approve it, then build.");
77930
78615
  }
77931
78616
  if (stage === 4) {
77932
78617
  const build = st0.outcomes?.[`${rk}:3`];
@@ -78398,6 +79083,17 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78398
79083
  });
78399
79084
  return;
78400
79085
  }
79086
+ if (req.method === "GET" && url === "/design/result") {
79087
+ const file3 = designResultFile(new URL(req.url || "/", "http://x").searchParams.get("id") || "");
79088
+ if (!file3) {
79089
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
79090
+ res.end("That design is not ready, or it was not made on this computer.");
79091
+ return;
79092
+ }
79093
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Content-Security-Policy": "sandbox allow-scripts", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
79094
+ res.end(fs53.readFileSync(file3, "utf8"));
79095
+ return;
79096
+ }
78401
79097
  if (req.method === "GET" && url === "/api/design/run") {
78402
79098
  const id2 = new URL(req.url || "/", "http://x").searchParams.get("id");
78403
79099
  const payload = id2 ? designRunStatus(id2) : { runs: listDesignRuns() };
@@ -78521,8 +79217,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78521
79217
  if (req.method === "GET" && url === "/handoff") {
78522
79218
  const locationId2 = new URL(req.url || "/", "http://x").searchParams.get("locationId") || "";
78523
79219
  if (!readableAccount(locationId2)) {
78524
- res.writeHead(400);
78525
- res.end("Choose a registered client.");
79220
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
79221
+ res.end(noClientPageHtml("The handoff", "no-client"));
78526
79222
  return;
78527
79223
  }
78528
79224
  try {
@@ -78536,8 +79232,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78536
79232
  if (req.method === "GET" && url === "/handoff/detail") {
78537
79233
  const locationId2 = new URL(req.url || "/", "http://x").searchParams.get("locationId") || "";
78538
79234
  if (!readableAccount(locationId2)) {
78539
- res.writeHead(400);
78540
- res.end("Choose a registered client.");
79235
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
79236
+ res.end(noClientPageHtml("The account detail", "no-client"));
78541
79237
  return;
78542
79238
  }
78543
79239
  try {
@@ -78647,13 +79343,23 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78647
79343
  const st = readCockpitState();
78648
79344
  const review = st.reviews?.[locationId2];
78649
79345
  if (!review?.ok) {
78650
- res.writeHead(404, { "Content-Type": "text/plain" });
78651
- res.end("No review generated for this client yet.");
79346
+ res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
79347
+ res.end(noClientPageHtml("The Blueprint Review", readableAccount(locationId2) ? "no-review" : "no-client"));
79348
+ return;
79349
+ }
79350
+ if (reviewIsStale(st, locationId2, review)) {
79351
+ res.writeHead(409, { "Content-Type": "text/plain; charset=utf-8" });
79352
+ res.end("This client's Blueprint Review was written before their answers or what they bought changed. Press Regenerate on the review, then open the client document again.");
79353
+ return;
79354
+ }
79355
+ if (review.graded !== true) {
79356
+ res.writeHead(409, { "Content-Type": "text/plain; charset=utf-8" });
79357
+ res.end("This client's Blueprint Review was not checked for over-promises. Press Regenerate on the review, then open the client document again.");
78652
79358
  return;
78653
79359
  }
78654
79360
  const name2 = clientDisplayName(st, locationId2, currentRoster().find((r) => r.locationId === locationId2)?.name || locationId2);
78655
79361
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
78656
- res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2, readSavedPlan(locationId2)), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), bought: boughtDocumentFor(st, locationId2, offeredFeaturesFor(st, locationId2), boughtWordingFor(st, locationId2)), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name2, agencyName: q3.get("agency") || st.agencyName, accent: q3.get("accent") || st.agencyAccent }));
79362
+ res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2, readSavedPlan(locationId2)), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), bought: boughtDocumentFor(st, locationId2, offeredFeaturesFor(st, locationId2), boughtWordingFor(st, locationId2), buildStatusFor(st, locationId2, locationId2)), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name2, agencyName: q3.get("agency") || st.agencyName, accent: q3.get("accent") || st.agencyAccent }));
78657
79363
  return;
78658
79364
  }
78659
79365
  if (req.method === "GET" && url === "/api/review") {
@@ -78661,7 +79367,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78661
79367
  res.writeHead(200, { "Content-Type": "application/json" });
78662
79368
  const stR = readCockpitState();
78663
79369
  const businessName = clientDisplayName(stR, locationId2, currentRoster().find((r) => r.locationId === locationId2)?.name ?? "");
78664
- res.end(JSON.stringify({ ...stR.reviews?.[locationId2] ?? { ok: false, error: "not generated yet" }, ...businessName ? { businessName } : {} }));
79370
+ const savedReview = stR.reviews?.[locationId2];
79371
+ res.end(JSON.stringify({ ...savedReview ?? { ok: false, error: "not generated yet" }, ...businessName ? { businessName } : {}, ...reviewIsStale(stR, locationId2, savedReview) ? { stale: true } : {} }));
78665
79372
  return;
78666
79373
  }
78667
79374
  if (req.method === "POST" && url === "/api/shared-adopt") {
@@ -78712,9 +79419,10 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78712
79419
  const rk = who2.recordKey;
78713
79420
  if (PROTECTED_LOCATIONS[who2.locationId]) throw new Error(PROTECTED_LOCATIONS[who2.locationId] + " can never be built.");
78714
79421
  const current = readCockpitState();
78715
- if (!current.buildApproved?.[rk]) throw new Error("Approve building (step 8) before confirming the build.");
79422
+ if (!current.buildApproved?.[rk]) throw new Error("Approve building (step 9) before confirming the build.");
78716
79423
  const preview = readBuildPreview(rk);
78717
79424
  if (!preview || !previewNamesLogins(preview) || typeof fingerprint2 !== "string" || preview.fingerprint !== fingerprint2) throw new Error("This list is out of date. Start the build again to see the current one.");
79425
+ if (preview.earlierBuild?.length) throw new Error(earlierBuildText(preview.earlierBuild, who2.name).why);
78718
79426
  const changeKeys = new Set(preview.change.map((e) => e.key));
78719
79427
  const allowed = Array.isArray(allow) ? [...new Set(allow.filter((k) => typeof k === "string"))] : [];
78720
79428
  if (allowed.some((k) => !changeKeys.has(k))) throw new Error("Only items on the list can be changed.");
@@ -78782,6 +79490,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78782
79490
  const approval = { inputHash: hash, at, repairHashes: readWorkflowRepairs(who2.locationId).filter((r) => Array.isArray(repairHashes) && repairHashes.includes(r.hash)).map((r) => r.hash), staffHash: staffListHash(current.staffRoster?.[who2.locationId]), scopeHash: approvalScopeHash(current, rk), boughtHash: approvalBoughtHash(current, rk), ...approvedBy };
78783
79491
  approvals[rk] = approval;
78784
79492
  }
79493
+ if (!revoke && approvals[rk] && reviewIsStale(current, rk, current.reviews?.[rk])) approvals[rk] = { ...approvals[rk], approvedOverStaleReview: true };
78785
79494
  const latest = readCockpitState();
78786
79495
  writeCockpitState({ ...setBuildApproval(latest, rk, revoke ? null : at), planApprovals: { ...latest.planApprovals, [rk]: approvals[rk] }, clients: revoke ? latest.clients : setStage(latest, who2.locationId, 2, "done", who2.clientKey).clients });
78787
79496
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -79205,7 +79914,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
79205
79914
  if (req.method === "POST" && url === "/api/scope") {
79206
79915
  readBody(req, res, 1e4, (body) => {
79207
79916
  try {
79208
- const { locationId: locationId2, clientKey, package: pkg2, features, bought } = JSON.parse(body);
79917
+ const { locationId: locationId2, clientKey, package: pkg2, features, bought, asks: asksIn } = JSON.parse(body);
79209
79918
  const who2 = cockpitClient(readCockpitState(), currentRoster(), locationId2, clientKey);
79210
79919
  let ticks;
79211
79920
  let derived;
@@ -79229,21 +79938,36 @@ ${opportunityNoteFailure?.detail ?? ""}`);
79229
79938
  else ticks = savedTicks[rk];
79230
79939
  nextState = { ...nextState, bought: savedTicks };
79231
79940
  if (!Object.keys(savedTicks).length) delete nextState.bought;
79941
+ const intakeSms = intakeChannelAnswers(nextState.intakeCompleted?.[who2.locationId]?.answers ?? nextState.intakeInputs?.[who2.locationId]?.answers).sms;
79942
+ if (asksIn !== void 0) {
79943
+ const parsed2 = parseAsks(asksIn);
79944
+ if (!parsed2.ok) throw new Error(parsed2.error);
79945
+ const clash = asksContradiction(parsed2.asks, effectiveTexting(parsed2.asks, intakeSms));
79946
+ if (clash) throw new Error(clash);
79947
+ const all = { ...nextState.asks ?? {} };
79948
+ if (Object.keys(parsed2.asks).length) all[rk] = parsed2.asks;
79949
+ else delete all[rk];
79950
+ nextState = { ...nextState, asks: all };
79951
+ if (!Object.keys(all).length) delete nextState.asks;
79952
+ }
79232
79953
  const { state: bound, withdrawn } = withdrawIfBindingChanged(nextState, rk);
79233
79954
  nextState = bound;
79234
79955
  {
79235
79956
  const offered = offeredFeaturesFor(nextState, rk);
79236
79957
  const channels = intakeChannelAnswers(nextState.intakeCompleted?.[who2.locationId]?.answers ?? nextState.intakeInputs?.[who2.locationId]?.answers);
79958
+ const texting = effectiveTexting(nextState.asks?.[rk], channels.sms);
79237
79959
  const scopeNow = nextState.scope?.[rk];
79238
79960
  const excluded = ticks ? boughtButExcludedJobs(ticks, offered, !scopeNow || scopeNow.wholePreset ? "all" : scopeNow.features ?? []) : [];
79239
- const boughtList = ticks ? [...boughtJobs(ticks, offered, channels), ...excluded] : [];
79961
+ const held2 = ticks ? heldJobs(heldItems(ticks, nextState.asks?.[rk], texting)) : [];
79962
+ const boughtList = ticks ? [...boughtJobs(ticks, offered, { ...channels, sms: texting === "yes" }), ...excluded, ...held2] : [];
79240
79963
  replaceBuildObligationsOfType(rk, BOUGHT_JOB_TYPE, boughtList);
79241
79964
  const tasks2 = withBoughtJobs(nextState.manualTasks?.[rk] ?? [], ticks ? catalogJobs({ handoffs: [], obligations: boughtList }) : []);
79242
79965
  if (tasks2.length || nextState.manualTasks?.[rk]) nextState = { ...nextState, manualTasks: { ...nextState.manualTasks, [rk]: tasks2 } };
79243
79966
  }
79244
79967
  writeCockpitState(nextState);
79245
79968
  res.writeHead(200, { "Content-Type": "application/json" });
79246
- res.end(JSON.stringify({ ok: true, scope: readCockpitState().scope?.[rk] ?? null, ...withdrawn ? { approvalWithdrawn: true, message: APPROVAL_WITHDRAWN_FOR_SCOPE } : {} }));
79969
+ const builtBefore = nextState.outcomes?.[`${rk}:3`]?.ok === true;
79970
+ res.end(JSON.stringify({ ok: true, scope: readCockpitState().scope?.[rk] ?? null, ...withdrawn ? { approvalWithdrawn: true, message: builtBefore && asksIn !== void 0 ? BUILD_WHAT_WAS_WAITING : APPROVAL_WITHDRAWN_FOR_SCOPE } : {} }));
79247
79971
  } catch (e) {
79248
79972
  res.writeHead(400, { "Content-Type": "application/json" });
79249
79973
  res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "bad request" }));
@@ -79434,7 +80158,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
79434
80158
  process.on("SIGTERM", stop3);
79435
80159
  });
79436
80160
  }
79437
- var import_zod94, import_node_util5, fs53, path53, http2, import_child_process4, import_child_process5, osmod, VIEWER_COCKPIT_MSG, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, stateRecovery, stateSaveFs, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, TEXT_SNIPPETS_LOCKED, SCOPE_REFUSAL, APPROVAL_WITHDRAWN_FOR_STAFF, APPROVAL_WITHDRAWN_NO_TEAM, HANDOFF_WORKFLOW, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, APPROVE_NEEDS_INTAKE, APPROVE_NEEDS_BRIEF, INDUSTRY_FIRST, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, INACTIVE, OFF_LICENSE, LICENSE_RECHECK_MS, activeRun2, runStarting, buildOneTime, oneTimeView, runForTests, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanWatch, queueScanOnce, ASSESSMENT_PIPELINE_MISSING, GATE_BODY, GATE_BODY_MAX, DOC_NOT_SAVED, NO_KEY_ON_FILE, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, ACCOUNT_CHANGED_DURING_READ, commitWithoutRead, SNAPSHOTS_CHANGED_DURING_READ, IntakeColdError, INTAKE_ANSWERS_MISSING, INTAKE_READ_FAILED, RUN_IN_PROGRESS, BRIEF_NEEDS_INTAKE2, BUSINESS_TYPE_FOR_INDUSTRY, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, NO_SUCH_PAGE, CHOOSE_CLIENT, ONE_CLIENT_ONLY, anthropicKeyWrites, SEAT_TOKEN_HEADER, notSignedInMsg, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, COCKPIT_AFTER_CANCEL_PATHS, licenceRefusedHooks, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
80161
+ var import_zod94, import_node_util5, fs53, path53, http2, import_child_process4, import_child_process5, osmod, VIEWER_COCKPIT_MSG, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, stateRecovery, stateSaveFs, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, TEXT_SNIPPETS_LOCKED, SCOPE_REFUSAL, APPROVAL_WITHDRAWN_FOR_STAFF, APPROVAL_WITHDRAWN_NO_TEAM, HANDOFF_WORKFLOW, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, APPROVE_NEEDS_INTAKE, APPROVE_NEEDS_BRIEF, INDUSTRY_FIRST, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, BUILD_WHAT_WAS_WAITING, INACTIVE, OFF_LICENSE, LICENSE_RECHECK_MS, activeRun2, runStarting, buildOneTime, oneTimeView, runForTests, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanWatch, queueScanOnce, ASSESSMENT_PIPELINE_MISSING, GATE_BODY, GATE_BODY_MAX, DOC_NOT_SAVED, NO_KEY_ON_FILE, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, ACCOUNT_CHANGED_DURING_READ, commitWithoutRead, SNAPSHOTS_CHANGED_DURING_READ, IntakeColdError, INTAKE_ANSWERS_MISSING, INTAKE_READ_FAILED, RUN_IN_PROGRESS, BRIEF_NEEDS_INTAKE2, BUSINESS_TYPE_FOR_INDUSTRY, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, NO_SUCH_PAGE, CHOOSE_CLIENT, ONE_CLIENT_ONLY, anthropicKeyWrites, SEAT_TOKEN_HEADER, notSignedInMsg, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, COCKPIT_AFTER_CANCEL_PATHS, licenceRefusedHooks, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
79438
80162
  var init_dashboard = __esm({
79439
80163
  "src/dashboard.ts"() {
79440
80164
  "use strict";
@@ -79475,6 +80199,7 @@ var init_dashboard = __esm({
79475
80199
  init_client_status_publish();
79476
80200
  init_handoff_snapshot();
79477
80201
  init_member_progress();
80202
+ init_stage_failure();
79478
80203
  init_build_obligations();
79479
80204
  fs53 = __toESM(require("fs"));
79480
80205
  path53 = __toESM(require("path"));
@@ -79574,6 +80299,7 @@ var init_dashboard = __esm({
79574
80299
  init_daily_brief2();
79575
80300
  init_daily_brief_collect();
79576
80301
  init_brief_deadline();
80302
+ init_client_asks();
79577
80303
  VIEWER_COCKPIT_MSG = "Your team login on this license is view only, and Command OS runs work that changes accounts, so it did not open. Ask the license owner to make you a team member, then open Command OS again.";
79578
80304
  STAGES2 = [
79579
80305
  "Intake",
@@ -79595,19 +80321,20 @@ var init_dashboard = __esm({
79595
80321
  CHOOSE_BEFORE_BUILD = "Choose what the client bought before building: Blueprint Review, then What they bought, then Save.";
79596
80322
  TEXT_SNIPPETS_LOCKED = "Text snippets are chosen before the first build. This client's build plan is already saved, so the choice stays as it was.";
79597
80323
  SCOPE_REFUSAL = /Nobody has chosen what|what this client bought|features (are )?selected for this client|No features are selected|features nobody ticked/i;
79598
- APPROVAL_WITHDRAWN_FOR_STAFF = "Your team changed after you approved, so the approval was taken back. Approve again in step 8.";
79599
- APPROVAL_WITHDRAWN_NO_TEAM = "This approval was given before the team was saved with it, so it was taken back. Approve again in step 8.";
80324
+ APPROVAL_WITHDRAWN_FOR_STAFF = "Your team changed after you approved, so the approval was taken back. Approve again in step 9.";
80325
+ APPROVAL_WITHDRAWN_NO_TEAM = "This approval was given before the team was saved with it, so it was taken back. Approve again in step 9.";
79600
80326
  HANDOFF_WORKFLOW = (ref) => ref === "handoff.quote_tag" ? "workflow.quote_follow_up" : ref === "handoff.review_link" || ref.startsWith("handoff.review_tag") ? "workflow.review_request" : void 0;
79601
80327
  NOTHING_BOUGHT = "Tick at least one item they bought, then save.";
79602
80328
  NOTHING_BUILT_FROM_TICKS = "None of the ticked items is built for this client's industry, so nothing was saved. Tick at least one item marked Built.";
79603
80329
  ASSESSMENT_CHANGED_SINCE_APPROVAL = "The Assessment answer or the attached Assessment changed after the build was approved, so Build did not start. Approve this plan again, then build.";
79604
80330
  APPROVAL_WITHDRAWN_FOR_ASSESSMENT = "The Assessment answer changed, so the build approval was withdrawn. Approve this plan again before building.";
79605
80331
  APPROVE_NEEDS_INTAKE = "Approve opens after step 5, Intake, is finished.";
79606
- APPROVE_NEEDS_BRIEF = "Approve opens after step 6, Brief, has run.";
80332
+ APPROVE_NEEDS_BRIEF = "Approve opens after step 7, Brief, has run.";
79607
80333
  INDUSTRY_FIRST = "Choose what kind of business this is in step 3 first.";
79608
80334
  NOTHING_TICKED_IN_PLAN = "None of the ticked features is in the plan for this client's industry, so nothing was saved. Tick features from the list, or choose Everything in the plan.";
79609
80335
  SCOPE_CHANGED_SINCE_APPROVAL = "What they bought changed after the build was approved, so Build did not start. Approve this plan again, then build.";
79610
80336
  APPROVAL_WITHDRAWN_FOR_SCOPE = "What they bought changed, so the build approval was withdrawn. Approve this plan again before building.";
80337
+ BUILD_WHAT_WAS_WAITING = "Your client's answers changed what gets built, so the build approval was withdrawn. Approve the updated plan, then run Build: it adds only what was waiting and makes nothing twice.";
79611
80338
  INACTIVE = "License is not active. Contact support@ghlcommand.com.";
79612
80339
  OFF_LICENSE = "This computer is no longer on your license. Free a device on your license, then restart Command OS.";
79613
80340
  LICENSE_RECHECK_MS = 4 * 60 * 60 * 1e3;
@@ -86483,14 +87210,14 @@ function registerDesignStudioTools(server2) {
86483
87210
  d.provenance.evidence["shape.radius"] = `set by ${stamp}`;
86484
87211
  }
86485
87212
  if (a.logoSvg) {
86486
- let clean5;
87213
+ let clean6;
86487
87214
  try {
86488
- clean5 = sanitizeSvg(a.logoSvg);
87215
+ clean6 = sanitizeSvg(a.logoSvg);
86489
87216
  } catch (e) {
86490
87217
  throw new Error(`${e.message} Nothing was changed.`);
86491
87218
  }
86492
- a.logoSvg = clean5;
86493
- d.logo = { inlineSvg: clean5, whiteFillFixed: false, source: `set by ${stamp}`, rights: "declared" };
87219
+ a.logoSvg = clean6;
87220
+ d.logo = { inlineSvg: clean6, whiteFillFixed: false, source: `set by ${stamp}`, rights: "declared" };
86494
87221
  d.provenance.evidence["logo"] = `set by ${stamp}`;
86495
87222
  }
86496
87223
  if (a.tone) d.voice.tone = a.tone;
@@ -90167,8 +90894,8 @@ async function listProcs(platform2 = process.platform, exec = execText, timeoutM
90167
90894
  return listingOf(platform2, await bounded(exec(file3, args, timeoutMs), timeoutMs));
90168
90895
  }
90169
90896
  function jobMarker(jobDir, platform2 = process.platform) {
90170
- const sep6 = platform2 === "win32" ? "\\" : "/";
90171
- return `--user-data-dir=${jobDir.replace(/[\\/]+$/, "")}${sep6}`;
90897
+ const sep7 = platform2 === "win32" ? "\\" : "/";
90898
+ return `--user-data-dir=${jobDir.replace(/[\\/]+$/, "")}${sep7}`;
90172
90899
  }
90173
90900
  function holdsMarker(command, marker, platform2 = process.platform) {
90174
90901
  return namesJob(command, markerDir(marker), platform2);
@@ -91514,18 +92241,22 @@ function claimBuildLock(locationId2, base) {
91514
92241
  }
91515
92242
  return null;
91516
92243
  }
91517
- function takeOver(lock, seen) {
92244
+ function takeOver(lock, seen, retried = false) {
91518
92245
  const mutex = `${lock}.takeover`;
91519
92246
  let fd;
91520
92247
  try {
91521
92248
  fd = fs63.openSync(mutex, "wx", 384);
91522
92249
  } catch (e) {
91523
92250
  if (e.code !== "EEXIST") throw e;
92251
+ let aged = false;
91524
92252
  try {
91525
- if (Date.now() - fs63.statSync(mutex).mtimeMs > TAKEOVER_STALE_MS) fs63.unlinkSync(mutex);
92253
+ if (Date.now() - fs63.statSync(mutex).mtimeMs > TAKEOVER_STALE_MS) {
92254
+ fs63.unlinkSync(mutex);
92255
+ aged = true;
92256
+ }
91526
92257
  } catch {
91527
92258
  }
91528
- return false;
92259
+ return aged && !retried ? takeOver(lock, seen, true) : false;
91529
92260
  }
91530
92261
  try {
91531
92262
  try {
@@ -94598,6 +95329,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
94598
95329
  const recordKey = clientRecordKey(activeLocation, clientKey);
94599
95330
  let planArg = planArgIn;
94600
95331
  let composed;
95332
+ let asksHash;
94601
95333
  if (fromPreset) {
94602
95334
  if (planArg) {
94603
95335
  return jsonResponse({ ok: false, phase: "compose", error: "fromPreset composes the plan itself \u2014 pass brief (and optionally preset / copy), not a plan. To run a hand-authored plan, drop fromPreset." });
@@ -94655,6 +95387,12 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
94655
95387
  if (!chosen.ok) {
94656
95388
  return jsonResponse({ ok: false, phase: "compose", error: chosen.error });
94657
95389
  }
95390
+ {
95391
+ const board = readBoardState();
95392
+ const ticks = board.bought?.[recordKey];
95393
+ briefForCompose = briefWithAsks(briefForCompose, board.asks?.[recordKey], Array.isArray(ticks) ? ticks : void 0, chosen.preset.skeleton?.workflows ?? []);
95394
+ asksHash = asksBindingHash(board.asks?.[recordKey], Array.isArray(ticks) ? ticks : void 0);
95395
+ }
94658
95396
  const composition = planFromPreset({ preset: chosen.preset, brief: briefForCompose, locationId: activeLocation, copy: copyArg });
94659
95397
  if (!composition.ok) {
94660
95398
  return jsonResponse({
@@ -94785,7 +95523,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
94785
95523
  const { name: locationName, timeZone: locationTimeZone } = await readLocationBasics(client, activeLocation);
94786
95524
  const { assets, warnings: scanWarnings } = await scanExistingAssets(client, activeLocation, registry2);
94787
95525
  const result = resolvePlan(typedPlan, assets, { metHandoffs });
94788
- const clientFields = { ...clientKey ? { clientKey } : {}, ...clinicPrefix ? { clinicPrefix } : {}, ...locationTimeZone ? { locationTimeZone } : {} };
95526
+ const clientFields = { ...clientKey ? { clientKey } : {}, ...asksHash ? { asksHash } : {}, ...clinicPrefix ? { clinicPrefix } : {}, ...locationTimeZone ? { locationTimeZone } : {} };
94789
95527
  if (clinicPrefix && clinicScoping) {
94790
95528
  let checked = false;
94791
95529
  let findings = [];
@@ -94910,7 +95648,16 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
94910
95648
  }
94911
95649
  const { changes: workflowChanges, states: workflowStates } = await previewWorkflowChanges(deps, result, ours, saved?.calendarPending);
94912
95650
  const logins = (typedPlan.users ?? []).filter((u) => result.items.some((i) => i.ref === u.ref && i.type === "users" && i.status === "would_create")).map((u) => ({ name: [u.firstName, u.lastName === "-" ? "" : u.lastName].filter(Boolean).join(" "), email: u.email, role: u.role }));
94913
- const preview = composePreview({ locationId: activeLocation, recordKey, planHash: contentHash2(typedPlan), items: result.items, ours, workflowChanges, workflowStates, board, logins, ...bookingCalendarName(typedPlan, result) && { bookingCalendar: bookingCalendarName(typedPlan, result) } });
95651
+ const otherPlans = createdByOtherPlans(activeLocation, { planHash: contentHash2(typedPlan), planId: typedPlan.planId });
95652
+ const workflowStatus = /* @__PURE__ */ new Map();
95653
+ try {
95654
+ const listed = workflowInventorySchema.safeParse(await client.get("/workflows/", { params: { locationId: activeLocation } }));
95655
+ if (listed.success) {
95656
+ for (const w of listed.data.workflows) if (typeof w.status === "string") workflowStatus.set(w.id, String(w.status));
95657
+ }
95658
+ } catch {
95659
+ }
95660
+ const preview = composePreview({ locationId: activeLocation, recordKey, planHash: contentHash2(typedPlan), items: result.items, ours, workflowChanges, workflowStates, board, logins, otherPlans, workflowStatus, ...bookingCalendarName(typedPlan, result) && { bookingCalendar: bookingCalendarName(typedPlan, result) } });
94914
95661
  try {
94915
95662
  saveBuildPreview(preview);
94916
95663
  } catch (e) {
@@ -94921,7 +95668,8 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
94921
95668
  return jsonResponse({
94922
95669
  ok: false,
94923
95670
  phase: "confirm",
94924
- error: confirm2 ? BUILD_PREVIEW_CHANGED : BUILD_PAUSED,
95671
+ error: preview.earlierBuild?.length ? earlierBuildText(preview.earlierBuild, locationName ?? "This account").why : confirm2 ? BUILD_PREVIEW_CHANGED : BUILD_PAUSED,
95672
+ ...preview.earlierBuild?.length ? { earlierBuild: preview.earlierBuild, undoWithClaude: earlierBuildText(preview.earlierBuild, locationName ?? "This account").undo } : {},
94925
95673
  willCreate: preview.createCount,
94926
95674
  willUse: preview.reuse.map((e) => `${PREVIEW_KIND[e.type] ?? e.type} "${e.name}"`),
94927
95675
  wouldChange: preview.change.map((e) => ({ what: `${PREVIEW_KIND[e.type] ?? e.type} "${e.name}"`, changes: e.changes ?? [] }))
@@ -94938,7 +95686,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
94938
95686
  const runId = newRunId();
94939
95687
  const journalParents = parentRefIndex(typedPlan);
94940
95688
  try {
94941
- appendBuildStart({ runId, locationId: activeLocation, locationName, planId: typedPlan.planId });
95689
+ appendBuildStart({ runId, locationId: activeLocation, locationName, planId: typedPlan.planId, planHash: contentHash2(typedPlan) });
94942
95690
  if (notOurs.length) appendBuildNotOurs({ runId, locationId: activeLocation, objects: notOurs.map((e) => ({ type: e.type, name: e.name, ghlId: e.id })) });
94943
95691
  if (allowedChanges.length) appendBuildChanges({ runId, locationId: activeLocation, changes: allowedChanges });
94944
95692
  } catch (e) {
@@ -95749,6 +96497,7 @@ var init_intake_to_build = __esm({
95749
96497
  init_claims_guard();
95750
96498
  init_plan_from_preset();
95751
96499
  init_board_roster();
96500
+ init_client_asks();
95752
96501
  init_features();
95753
96502
  init_executor();
95754
96503
  init_build_confirm();
@@ -96324,6 +97073,21 @@ var os11 = __toESM(require("node:os"));
96324
97073
  function sha2563(buf) {
96325
97074
  return (0, import_node_crypto34.createHash)("sha256").update(buf).digest("hex");
96326
97075
  }
97076
+ function isLink(p) {
97077
+ try {
97078
+ return fs65.lstatSync(p).isSymbolicLink();
97079
+ } catch {
97080
+ return false;
97081
+ }
97082
+ }
97083
+ function linkOnTheWay(root, rel) {
97084
+ let at = root;
97085
+ for (const part of rel.split(/[\\/]/).filter(Boolean)) {
97086
+ at = path65.join(at, part);
97087
+ if (isLink(at)) return true;
97088
+ }
97089
+ return false;
97090
+ }
96327
97091
  function isOlderRelease(candidate, installed) {
96328
97092
  const parts = (v) => v.trim().split(".").map((n) => Number.parseInt(n, 10));
96329
97093
  const a = parts(candidate), b = parts(installed);
@@ -96371,6 +97135,7 @@ function installBundledSkills(opts) {
96371
97135
  updated: [],
96372
97136
  skippedUserModified: [],
96373
97137
  skippedOlderPackage: [],
97138
+ skippedSymlink: [],
96374
97139
  unchanged: [],
96375
97140
  errors: [],
96376
97141
  targetDir: opts.targetDir ?? defaultSkillsDir(),
@@ -96385,6 +97150,11 @@ function installBundledSkills(opts) {
96385
97150
  result.errors.push(`cannot create ${result.targetDir}: ${String(e)}`);
96386
97151
  return result;
96387
97152
  }
97153
+ if (isLink(result.targetDir)) {
97154
+ result.targetIsLink = true;
97155
+ result.skippedSymlink.push(...listFiles(bundled));
97156
+ return result;
97157
+ }
96388
97158
  const markerPath = path65.join(result.targetDir, ".ghl-command-skills.json");
96389
97159
  const marker = readMarker(markerPath) ?? { packageVersion: "", files: {} };
96390
97160
  const newMarker = { packageVersion: opts.packageVersion, files: { ...marker.files } };
@@ -96392,6 +97162,10 @@ function installBundledSkills(opts) {
96392
97162
  try {
96393
97163
  const src = fs65.readFileSync(path65.join(bundled, rel));
96394
97164
  const dest = path65.join(result.targetDir, rel);
97165
+ if (linkOnTheWay(result.targetDir, rel)) {
97166
+ result.skippedSymlink.push(rel);
97167
+ continue;
97168
+ }
96395
97169
  const srcHash = sha2563(src);
96396
97170
  let destBuf = null;
96397
97171
  try {
@@ -96429,6 +97203,7 @@ function installBundledSkills(opts) {
96429
97203
  }
96430
97204
  }
96431
97205
  try {
97206
+ if (isLink(markerPath)) throw new Error("the marker file is a symbolic link; not written");
96432
97207
  fs65.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
96433
97208
  } catch (e) {
96434
97209
  result.errors.push(`marker write failed: ${String(e)}`);
@@ -96449,6 +97224,7 @@ function summarizeInstall(r) {
96449
97224
  bucket(r.updated, "updated");
96450
97225
  bucket(r.skippedUserModified, "kept with your edits");
96451
97226
  bucket(r.skippedOlderPackage, "kept, newer than this version");
97227
+ bucket(r.skippedSymlink ?? [], "not touched, linked elsewhere");
96452
97228
  if (r.errors.length) bits.push(`${r.errors.length} file error${r.errors.length === 1 ? "" : "s"}`);
96453
97229
  if (bits.length === 0) return "";
96454
97230
  return `Skills: ${bits.join(", ")} \u2192 ${r.targetDir}`;
@@ -96476,7 +97252,7 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
96476
97252
  ok: result.errors.length === 0,
96477
97253
  targetDir: result.targetDir,
96478
97254
  // Skills, by name, before the per-file detail (BUG-2026-09-14-03).
96479
- skills: { installed: skillNames(result.installed), updated: skillNames(result.updated), keptYourEdits: skillNames(result.skippedUserModified) },
97255
+ skills: { installed: skillNames(result.installed), updated: skillNames(result.updated), keptYourEdits: skillNames(result.skippedUserModified), linkedElsewhere: skillNames(result.skippedSymlink) },
96480
97256
  installed: result.installed,
96481
97257
  updated: result.updated,
96482
97258
  keptYourEdits: result.skippedUserModified,
@@ -96485,7 +97261,14 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
96485
97261
  keptNewerThanThisVersion: result.skippedOlderPackage,
96486
97262
  unchanged: result.unchanged.length,
96487
97263
  errors: result.errors,
96488
- next: result.installed.length > 0 || result.updated.length > 0 ? "Fully restart Claude (quit and reopen) so the new/updated skills load." : "Everything already current."
97264
+ // Reviewer MED-2 on rc.1: a skills folder (or skill folder) that is a link elsewhere is never written, and the
97265
+ // reply says so instead of "Everything already current."
97266
+ linkedElsewhere: result.skippedSymlink,
97267
+ next: [
97268
+ result.installed.length > 0 || result.updated.length > 0 ? "Fully restart Claude (quit and reopen) so the new/updated skills load." : "",
97269
+ result.skippedSymlink.length ? `Not installed: ${skillNames(result.skippedSymlink).join(", ")}, because ${linkedWhere(result)} is a link to somewhere else, and GHL Command never writes through a link. Replace the link with a real folder to get these skills.` : "",
97270
+ result.installed.length === 0 && result.updated.length === 0 && result.skippedSymlink.length === 0 ? "Everything already current." : ""
97271
+ ].filter(Boolean).join(" ")
96489
97272
  });
96490
97273
  } catch (error) {
96491
97274
  return errorResponse(error);
@@ -96493,6 +97276,10 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
96493
97276
  }
96494
97277
  );
96495
97278
  }
97279
+ function linkedWhere(r) {
97280
+ if (r.targetIsLink) return `the skills folder (${r.targetDir})`;
97281
+ return `the folder for ${skillNames(r.skippedSymlink).length === 1 ? "that skill" : "each of those skills"}`;
97282
+ }
96496
97283
 
96497
97284
  // src/index.ts
96498
97285
  init_brain();