@elitedcs/ghl-mcp 3.94.0 → 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
  }
@@ -32397,7 +32397,7 @@ async function brainWireGhl(deps, args) {
32397
32397
  }
32398
32398
  const specs = wiringSpecs({ testTag: args.testTag, leadTag });
32399
32399
  const held2 = [];
32400
- 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);
32401
32401
  const survey = async () => {
32402
32402
  const refuse4 = (error) => ({ refusal: { ok: false, wrote: "nothing", error } });
32403
32403
  const meta = deps.meta();
@@ -32406,7 +32406,7 @@ async function brainWireGhl(deps, args) {
32406
32406
  try {
32407
32407
  list4 = await writer.listWorkflows();
32408
32408
  } catch (e) {
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: ${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.`);
32410
32410
  }
32411
32411
  const found = [];
32412
32412
  for (const { id: id2 } of list4) {
@@ -32414,7 +32414,7 @@ async function brainWireGhl(deps, args) {
32414
32414
  try {
32415
32415
  read2 = await writer.getWorkflow(id2);
32416
32416
  } catch (e) {
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: ${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.`);
32418
32418
  }
32419
32419
  const shape = readShape(read2, id2);
32420
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.`);
@@ -32438,7 +32438,7 @@ async function brainWireGhl(deps, args) {
32438
32438
  read2 = await writer.getWorkflow(id2);
32439
32439
  } catch (e) {
32440
32440
  if (isNotFound(e)) continue;
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: ${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.`);
32442
32442
  }
32443
32443
  const shape = readShape(read2, id2);
32444
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.`);
@@ -32576,7 +32576,7 @@ async function brainWireGhl(deps, args) {
32576
32576
  if ((await writer.getWorkflow(id2)).status !== "published") await fenced.publishWorkflow(id2);
32577
32577
  } catch (e) {
32578
32578
  if (e instanceof LeaseLost) throw e;
32579
- error = clean5(e);
32579
+ error = clean6(e);
32580
32580
  }
32581
32581
  if (!id2) {
32582
32582
  results.push({ workflow: spec.workflowName, built: false, error });
@@ -32616,7 +32616,7 @@ async function brainWireGhl(deps, args) {
32616
32616
  });
32617
32617
  } catch (e) {
32618
32618
  if (e instanceof LeaseLost) throw e;
32619
- 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 } : {} });
32620
32620
  }
32621
32621
  }
32622
32622
  if (staleTest) {
@@ -32626,7 +32626,7 @@ async function brainWireGhl(deps, args) {
32626
32626
  results.push({ workflow: "earlier test-tag workflow", id: staleTest, removed: true });
32627
32627
  } catch (e) {
32628
32628
  if (e instanceof LeaseLost) throw e;
32629
- 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." });
32630
32630
  }
32631
32631
  }
32632
32632
  for (const r of results) {
@@ -34598,8 +34598,8 @@ function datePatterns(value) {
34598
34598
  if (!y || !(mo >= 1 && mo <= 12) || !(d >= 1 && d <= 31)) return [];
34599
34599
  const year = `(?:${y}|${y.slice(2)})`;
34600
34600
  const n = (x) => `0?${x}`;
34601
- const sep6 = "[-/.\\s]";
34602
- 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}`];
34603
34603
  const month = `(?:${MONTHS2[mo - 1]}|${MONTHS2[mo - 1].slice(0, 3)})\\.?`;
34604
34604
  const day = `0?${d}(?:st|nd|rd|th)?`;
34605
34605
  out.push(`${month}\\s+${day},?\\s+${y}`, `${day}\\s+(?:of\\s+)?${month},?\\s+${y}`);
@@ -36643,8 +36643,8 @@ function sanitizeEngagements(raw) {
36643
36643
  for (const [id2, rec2] of Object.entries(src).slice(0, MAX_CLIENTS)) {
36644
36644
  const key2 = str4(id2, 64);
36645
36645
  if (!key2 || FORBIDDEN_KEY.has(key2)) continue;
36646
- const clean5 = sanitizeEngagement(rec2);
36647
- if (clean5) clients[key2] = clean5;
36646
+ const clean6 = sanitizeEngagement(rec2);
36647
+ if (clean6) clients[key2] = clean6;
36648
36648
  }
36649
36649
  }
36650
36650
  return { v: 1, clients: { ...clients }, updated_at: str4(r.updated_at, 40) };
@@ -40669,7 +40669,33 @@ function buildRunRecord(input) {
40669
40669
  };
40670
40670
  }
40671
40671
  function appendBuildStart(input, 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 }, 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;
40673
40699
  }
40674
40700
  function appendBuildIntent(input, base) {
40675
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);
@@ -41524,7 +41550,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
41524
41550
  engineAcct = { engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked };
41525
41551
  if (!engineCheck.ok) {
41526
41552
  onEvent({ kind: "error", line: engineCheck.reason });
41527
- 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 });
41528
41554
  return;
41529
41555
  }
41530
41556
  onEvent({ kind: "status", line: `Starting ${spec.name} for ${locationName}.` });
@@ -43009,9 +43035,41 @@ var init_handoff_run = __esm({
43009
43035
  }
43010
43036
  });
43011
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
+
43012
43069
  // src/member-progress.ts
43013
- function memberProgress(lines, running, ok) {
43014
- 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];
43015
43073
  if (lines.some((l) => /writing.*review/i.test(l))) result.push("Command OS is preparing the client review.");
43016
43074
  if (lines.some((l) => /plain-English version/i.test(l))) result.push("Command OS is preparing the result for review.");
43017
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, "."));
@@ -43345,6 +43403,7 @@ var VERIFY_PASSED_COUNTING, readLabels, readLabel, BUILDER_SIGNIN_BODY, BUILDER_
43345
43403
  var init_member_progress = __esm({
43346
43404
  "src/member-progress.ts"() {
43347
43405
  "use strict";
43406
+ init_stage_failure();
43348
43407
  init_workflow_read_connection();
43349
43408
  VERIFY_PASSED_COUNTING = "The check finished. The list of jobs left for you is on the Manual tasks step.";
43350
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" };
@@ -43429,7 +43488,10 @@ function plainFallback(text3, opts = {}) {
43429
43488
  if (!/[.!?]$/.test(s)) s += ".";
43430
43489
  if (opts.nextStep && !/Settings|then |→/.test(s)) {
43431
43490
  const hint = nextStepHint(text3);
43432
- if (hint) s += " " + hint;
43491
+ if (hint && !opts.given?.has(hint)) {
43492
+ s += " " + hint;
43493
+ opts.given?.add(hint);
43494
+ }
43433
43495
  }
43434
43496
  return s.replace(/—/g, ".").replace(/Brand DNA/gi, "your brand").replace(/Agency OS/gi, "Command OS").replace(/\bMCP\b/g, "GHL Command");
43435
43497
  }
@@ -43437,7 +43499,10 @@ function plainFallbackOutcome(raw) {
43437
43499
  if (raw.verification) return renderVerifyReport(raw.verification, plainFallbackOutcome({ summary: raw.summary, issues: raw.issues }));
43438
43500
  return {
43439
43501
  ...raw.summary && { summary: plainFallback(raw.summary) },
43440
- ...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
+ })() }
43441
43506
  };
43442
43507
  }
43443
43508
  function parseOutcomeRewrite(text3, raw) {
@@ -43960,7 +44025,7 @@ function deriveQueue(input) {
43960
44025
  title: title(next, name2),
43961
44026
  needs: "you",
43962
44027
  action: opensStep ? "open" : "mark",
43963
- 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,
43964
44029
  rank: RANK.waitingOnYou
43965
44030
  });
43966
44031
  continue;
@@ -44194,7 +44259,7 @@ function beyondGoHighLevel(live, engagements, roster, beyond = { ownCalendarConn
44194
44259
  }
44195
44260
  clients.sort((a, b) => a.client.localeCompare(b.client));
44196
44261
  const addressesNotRead = [...live?.clientContactsUnread ?? []].sort();
44197
- 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.` : "";
44198
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;
44199
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.";
44200
44265
  const ask = [
@@ -44284,13 +44349,13 @@ function dailyBrief(input) {
44284
44349
  } else if (accountsRead === 0 && (live.loading?.length ?? 0) > 0 && !live.unreachable?.length) {
44285
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.");
44286
44351
  } else if (accountsRead === 0) {
44287
- 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.");
44288
44353
  }
44289
44354
  for (const note of live?.notes ?? []) notChecked.push(note);
44290
44355
  const yourDayNote = input.ownDay?.yourDayNote ?? live?.yourDayNote;
44291
44356
  if (yourDayNote) notChecked.push(yourDayNote);
44292
44357
  for (const u of live?.unreachable ?? []) {
44293
- 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.`);
44294
44359
  }
44295
44360
  const stillLoading = live?.loading ?? [];
44296
44361
  if (stillLoading.length) {
@@ -44355,20 +44420,20 @@ function dailyBrief(input) {
44355
44420
  headline3 = `Nothing is waiting on you. ${neverStarted.length} account${neverStarted.length === 1 ? " has" : "s have"} never been started.`;
44356
44421
  } else if (halfRead.length && !unread.length) {
44357
44422
  const what = halfRead.length === 1 ? `${halfRead[0].client}'s ${halfRead[0].what}` : `${halfRead.length} accounts were only read in part`;
44358
- headline3 = `Nothing needs you in what I could read \u2014 but ${what}.`;
44423
+ headline3 = `Nothing needs you in what I could read. But ${what}.`;
44359
44424
  } else if (unread.length === 1) {
44360
- 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.`;
44361
44426
  } else if (unread.length > 1) {
44362
- 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.`;
44363
44428
  } else if (yourDayNote) {
44364
- 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.";
44365
44430
  } else if (floorUncheckable.length) {
44366
44431
  const c = floorUncheckable[0].client;
44367
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.`;
44368
44433
  } else if (renewalUnknown.length) {
44369
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.`;
44370
44435
  } else {
44371
- headline3 = "Nothing needs you this morning.";
44436
+ headline3 = "Nothing needs you right now.";
44372
44437
  }
44373
44438
  const readFrom = accountsRead > 0 ? { accounts: accountsRead } : void 0;
44374
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) };
@@ -44945,7 +45010,7 @@ async function readYourDay(now, tz, signal, secrets) {
44945
45010
  }
44946
45011
  return { yourDay: ownDayFromCalendar(result) };
44947
45012
  } catch {
44948
- 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." };
44949
45014
  }
44950
45015
  }
44951
45016
  function operatorTimezone(profileTz) {
@@ -45824,7 +45889,7 @@ var require_package = __commonJS({
45824
45889
  "package.json"(exports2, module2) {
45825
45890
  module2.exports = {
45826
45891
  name: "@elitedcs/ghl-mcp",
45827
- version: "3.94.0",
45892
+ version: "3.94.1",
45828
45893
  mcpName: "io.github.drjerryrelth/ghl-command",
45829
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.",
45830
45895
  main: "dist/index.js",
@@ -47287,7 +47352,7 @@ var init_cockpit_cache = __esm({
47287
47352
  };
47288
47353
  AGE_LINE_JS = `
47289
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"}
47290
- 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"}
47291
47356
  const t=Date.parse(v.loadedAt);const base="Loaded "+ageWords((now==null?Date.now():now)-t);
47292
47357
  if(v.refreshing)return base+" \xB7 refreshing\u2026";
47293
47358
  if(v.lastRefresh&&v.lastRefresh.text&&Date.parse(v.lastRefresh.at)>=t)return base+" \xB7 Refresh failed: "+v.lastRefresh.text;
@@ -47505,8 +47570,8 @@ var init_analyze = __esm({
47505
47570
  function extractUrls(text3, html2) {
47506
47571
  const out = /* @__PURE__ */ new Set();
47507
47572
  const add2 = (u) => {
47508
- const clean5 = u.replace(/&amp;/g, "&").replace(/[.,;:!?]+$/, "");
47509
- 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);
47510
47575
  };
47511
47576
  if (html2) for (const m of html2.matchAll(HREF_RE)) add2(m[1]);
47512
47577
  for (const m of text3.matchAll(URL_RE)) add2(m[0]);
@@ -47702,7 +47767,8 @@ function judgeSteps(rawRows, contactId, workflowNames, lead) {
47702
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.`;
47703
47768
  else if (outcome === "ran") detail = type === "wait_finished" ? "The wait ended." : reason3 && !/^\{/.test(reason3) ? reason3 : "Done.";
47704
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;
47705
- 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 } : {} });
47706
47772
  }
47707
47773
  const sameStep = (a, b) => a.workflowId === b.workflowId && a.pass === b.pass && (a.stepId ?? a.stepName) === (b.stepId ?? b.stepName);
47708
47774
  const problems = [];
@@ -48212,6 +48278,59 @@ async function postJson(url, body) {
48212
48278
  const r = await guardedPost(url, JSON.stringify(body), { "Content-Type": "application/json", Accept: "application/json" });
48213
48279
  return r.status;
48214
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
+ }
48215
48334
  async function listLeadCarriers(client, run) {
48216
48335
  const carriers = /* @__PURE__ */ new Map();
48217
48336
  let partial = false;
@@ -48349,6 +48468,9 @@ async function finishShopper(locationId2, runId, deps) {
48349
48468
  run.status = "done";
48350
48469
  run.finishedAt = t;
48351
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() };
48352
48474
  if (run.fastForward && deps.ffAdmin) {
48353
48475
  const { removeCopies: removeCopies2 } = await Promise.resolve().then(() => (init_fastforward(), fastforward_exports));
48354
48476
  const problems = await removeCopies2(deps.ffAdmin, run.locationId, run.fastForward.copies.map((c) => ({ copyId: c.copyId, name: c.copyName })), deps.base, now);
@@ -48831,7 +48953,7 @@ function ringHtml(score2, caption, complete = true) {
48831
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>`;
48832
48954
  }
48833
48955
  function stepRow(r) {
48834
- 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] ?? ["", ""];
48835
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>`;
48836
48958
  }
48837
48959
  function stepsHtml(a) {
@@ -49072,7 +49194,11 @@ ${COCKPIT_TYPE_JS}
49072
49194
  function showStartPanel(ids){
49073
49195
  var names=ids.map(function(id){var w=(shownWorkflows||[]).filter(function(x){return x.id===id})[0];return w?w.name:id});
49074
49196
  var ff=document.getElementById("ff").checked, email=document.getElementById("owner-email").value, phone=document.getElementById("test-phone").value;
49075
- 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.');
49076
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.');
49077
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.');
49078
49204
  // Walk W38: in the copies, staff alerts, tasks and assignments go to one person, marked TEST; say who, by name.
@@ -49555,7 +49681,7 @@ function deductions(items, capped2) {
49555
49681
  }
49556
49682
  function workflowFlow(steps) {
49557
49683
  return `<div class="cds-flow">${steps.map((r) => {
49558
- 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"];
49559
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>`;
49560
49686
  }).join("")}</div>`;
49561
49687
  }
@@ -51191,7 +51317,8 @@ function draw(){
51191
51317
  const sum=money?.summary;
51192
51318
  if(sum){tile(cashAll(sum.mrr),"monthly recurring revenue");tile(cashAll(sum.received30.totals),"received, 30 days")}
51193
51319
  else if(money){tile(amount(money.owed),"owed on invoices");tile(money.renewals.count,"renewals, 30 days")}
51194
- 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")}
51195
51322
  tiles.appendChild(el("p",readLine(),"cds-src stats-meta"));
51196
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)}
51197
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)}
@@ -54290,7 +54417,7 @@ function parsePricePoints(text3) {
54290
54417
  const out = [];
54291
54418
  const segments2 = text3.split(/\r?\n|;|\.\s+/);
54292
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;
54293
- const clean5 = (s) => s.replace(/[\s.,:;|–—-]+$/, "").trim();
54420
+ const clean6 = (s) => s.replace(/[\s.,:;|–—-]+$/, "").trim();
54294
54421
  const num4 = (s) => s.replace(/,/g, "");
54295
54422
  const recur = (s) => s ? "/" + s.replace(/^\/|^(?:per|an?|each)\b/i, "").trim().toLowerCase() : "";
54296
54423
  for (const segment of segments2) {
@@ -54299,11 +54426,11 @@ function parsePricePoints(text3) {
54299
54426
  const m = t.match(ENTRY);
54300
54427
  const withTerms = m ? null : t.match(/^(.*?)\s*[-–—:|]?\s*\$\s*([\d,]+(?:\.\d+)?)\s*(.+)$/);
54301
54428
  if (m && m[2]) {
54302
- const name2 = clean5(m[1]);
54429
+ const name2 = clean6(m[1]);
54303
54430
  const price = (m[3] ? `${num4(m[2])}-${num4(m[3])}` : num4(m[2])) + recur(m[4]);
54304
54431
  out.push({ name: name2 || t, price });
54305
- } else if (withTerms && clean5(withTerms[1])) {
54306
- 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() });
54307
54434
  } else {
54308
54435
  out.push({ name: t, price: "" });
54309
54436
  }
@@ -55228,17 +55355,17 @@ function checkStaff(listed, users, opts = { canAddForYou: false }) {
55228
55355
  function staffGateReason(check, stageName) {
55229
55356
  if (check.status === "ready") return null;
55230
55357
  if (check.status === "unreadable") {
55231
- 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.`;
55232
55359
  }
55233
55360
  if (check.status === "needs_list") {
55234
55361
  return `${stageName} is waiting: nobody is on the team yet. Add them under "Your team" in step 5, Intake.`;
55235
55362
  }
55236
- 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.`;
55237
55364
  }
55238
55365
  function staffPreflightLine(check) {
55239
- 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." };
55240
55367
  if (check.status === "ready") return { key: "staff", ok: true, text: `Staff in this account: ${listSentence(check.matched.map((m) => m.name))}.` };
55241
- 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.` };
55242
55369
  }
55243
55370
  async function readStaffCheck(deps, listed, now) {
55244
55371
  let users;
@@ -55386,8 +55513,8 @@ var init_staff_first = __esm({
55386
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.";
55387
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.";
55388
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.`;
55389
- 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.";
55390
- 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.";
55391
55518
  STAFF_GATED_STAGES = /* @__PURE__ */ new Set([3]);
55392
55519
  fullName = (c) => [c.firstName, c.lastName === "-" ? "" : c.lastName].filter(Boolean).join(" ");
55393
55520
  STAFF_ADMIN_MARK = "Admin login";
@@ -55500,7 +55627,7 @@ function intakeAnswer2(intake, key2) {
55500
55627
  const label2 = questionByKey(key2)?.label;
55501
55628
  const direct = intake[key2] ?? (label2 ? intake[label2] : void 0);
55502
55629
  const hit = direct ?? Object.entries(intake).find(([k]) => label2 && k.trim().toLowerCase() === label2.trim().toLowerCase())?.[1];
55503
- return clean3(Array.isArray(hit) ? hit.join(", ") : hit);
55630
+ return clean4(Array.isArray(hit) ? hit.join(", ") : hit);
55504
55631
  }
55505
55632
  function readBackBroken(line4) {
55506
55633
  if (line4.state === "CHECKED") return false;
@@ -55511,7 +55638,7 @@ function readBackBroken(line4) {
55511
55638
  function withoutBrokenWorkflows(plan, lines) {
55512
55639
  if (!plan) return null;
55513
55640
  if (!lines?.length || !plan.workflows?.length) return plan;
55514
- 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))) };
55515
55642
  }
55516
55643
  function builtFeatures(plan) {
55517
55644
  const seen = /* @__PURE__ */ new Set();
@@ -55528,9 +55655,9 @@ function hasWorkflow(plan, ref) {
55528
55655
  return (plan?.workflows ?? []).some((w) => w.ref === ref);
55529
55656
  }
55530
55657
  function workflowsConfirmed(plan, lines) {
55531
- const names = (plan?.workflows ?? []).map((w) => clean3(w.name)).filter(Boolean);
55658
+ const names = (plan?.workflows ?? []).map((w) => clean4(w.name)).filter(Boolean);
55532
55659
  if (!names.length || !lines?.length) return false;
55533
- 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"));
55534
55661
  }
55535
55662
  function durationWords(c) {
55536
55663
  if (!c.slotDuration) return "";
@@ -55554,8 +55681,8 @@ function isProgramFollowUp(plan) {
55554
55681
  function context(src) {
55555
55682
  const plan = src.plan ?? null;
55556
55683
  const features = builtFeatures(plan);
55557
- const calendars = (plan?.calendars ?? []).filter((c) => clean3(c.name)).map((c) => ({
55558
- name: calendarLabel(clean3(c.name)),
55684
+ const calendars = (plan?.calendars ?? []).filter((c) => clean4(c.name)).map((c) => ({
55685
+ name: calendarLabel(clean4(c.name)),
55559
55686
  duration: durationWords(c),
55560
55687
  team: (c.teamMemberRefs ?? []).map(String)
55561
55688
  }));
@@ -55564,8 +55691,8 @@ function context(src) {
55564
55691
  const items = boughtItemsFor(offered.length ? offered : features);
55565
55692
  const { ticks, source } = src.boughtState && src.recordKey ? ticksFor(src.boughtState, src.recordKey) : { ticks: [], source: "all" };
55566
55693
  const confirmedTicks = source !== "all";
55567
- const notify2 = clean3(src.roster?.notifyName) || intakeAnswer2(src.intake, "notify_name") || void 0;
55568
- 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;
55569
55696
  const notifyShown = notify2 ? teamNames(src).find((n) => sameName(n, notify2)) : void 0;
55570
55697
  const callAnswering = src.recordKey && src.boughtState?.asks?.[src.recordKey]?.callAnswering || [];
55571
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) };
@@ -55578,12 +55705,12 @@ function calendarPhrase(ctx) {
55578
55705
  return names.length ? `the ${listWords(names, "or")} calendar` : "your booking calendar";
55579
55706
  }
55580
55707
  function teamNames(src) {
55581
- 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);
55582
55709
  if (fromPlan.length) return fromPlan;
55583
- 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);
55584
55711
  }
55585
55712
  function calendarLabel(name2) {
55586
- const n = clean3(name2);
55713
+ const n = clean4(name2);
55587
55714
  const prose = !n || n.length > 40 || n.split(" ").length > 6 || /[@$%;:()\[\],]|\d{3}[-. )]*\d{3,}|https?:|www\./i.test(n);
55588
55715
  return prose ? "booking calendar" : n;
55589
55716
  }
@@ -55707,10 +55834,10 @@ function stepsOf(ctx, plan) {
55707
55834
  }
55708
55835
  function teamOf(ctx, src) {
55709
55836
  const plan = src.plan;
55710
- 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);
55711
55838
  if (!people.length) {
55712
55839
  const listed = parseStaffText(src.roster?.lines || intakeAnswer2(src.intake, "staff_members")).people;
55713
- for (const p of listed) people.push({ name: clean3(p.name) });
55840
+ for (const p of listed) people.push({ name: clean4(p.name) });
55714
55841
  }
55715
55842
  const rows2 = [];
55716
55843
  for (const p of people) {
@@ -55737,7 +55864,7 @@ function teamOf(ctx, src) {
55737
55864
  }
55738
55865
  function stagesOf2(plan) {
55739
55866
  const first2 = (plan?.pipelines ?? [])[0];
55740
- 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);
55741
55868
  return all.length > MAX_STAGE_CHIPS ? [...all.slice(0, MAX_STAGE_CHIPS), `+${all.length - MAX_STAGE_CHIPS} more`] : all;
55742
55869
  }
55743
55870
  function boughtOf(ctx) {
@@ -55790,12 +55917,12 @@ function buildOwnerHandoff(input) {
55790
55917
  const ctx = context(src);
55791
55918
  const { rows: rows2, source } = problemsOf(ctx, src);
55792
55919
  const { steps, after } = stepsOf(ctx, src.plan);
55793
- const agencyName = clean3(src.agency?.name) || void 0;
55920
+ const agencyName = clean4(src.agency?.name) || void 0;
55794
55921
  const timeZone = clientTimeZone(src);
55795
55922
  return {
55796
55923
  // The business's own name from the intake, else the GoHighLevel account's (audit 2026-10-28 #25: an account named
55797
55924
  // for the agency's test titled the client's page).
55798
- accountName: intakeAnswer2(src.intake, "business_name") || clean3(src.accountName),
55925
+ accountName: intakeAnswer2(src.intake, "business_name") || clean4(src.accountName),
55799
55926
  checkedAt: src.checkedAt,
55800
55927
  ...timeZone && { timeZone },
55801
55928
  ...src.revision !== void 0 && { revision: src.revision },
@@ -55804,7 +55931,7 @@ function buildOwnerHandoff(input) {
55804
55931
  ...agencyName && { name: agencyName },
55805
55932
  accent: isHex(src.agency?.accent) ? src.agency.accent : DEFAULT_ACCENT,
55806
55933
  ...isRasterDataUri(src.agency?.logoDataUri) && { logoDataUri: src.agency.logoDataUri },
55807
- ...clean3(src.agency?.contact) && { contact: clean3(src.agency?.contact) }
55934
+ ...clean4(src.agency?.contact) && { contact: clean4(src.agency?.contact) }
55808
55935
  },
55809
55936
  summary: summaryOf2(ctx, src.plan),
55810
55937
  problems: rows2,
@@ -55952,7 +56079,7 @@ ${sections.join("\n")}
55952
56079
  </script>
55953
56080
  </body></html>`;
55954
56081
  }
55955
- 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;
55956
56083
  var init_owner_handoff = __esm({
55957
56084
  "src/command-os/owner-handoff.ts"() {
55958
56085
  "use strict";
@@ -56029,9 +56156,9 @@ var init_owner_handoff = __esm({
56029
56156
  UNIT_MINUTES = { minute: 1, minutes: 1, hour: 60, hours: 60, day: 1440, days: 1440, week: 10080, weeks: 10080 };
56030
56157
  NUMBER_WORDS = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"];
56031
56158
  countWords = (n, unit) => `${NUMBER_WORDS[n] ?? String(n)} ${unit}${n === 1 ? "" : "s"}`;
56032
- clean3 = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
56159
+ clean4 = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
56033
56160
  cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
56034
- 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());
56035
56162
  FINDING_TO_BOUGHT = Object.fromEntries(
56036
56163
  ASSESSMENT_FEATURES.filter((f) => "finding" in f).map((f) => [f.finding, f.id])
56037
56164
  );
@@ -56895,6 +57022,8 @@ var init_cockpit_routes = __esm({
56895
57022
  get("/api/snapshots", "agency", BOTH),
56896
57023
  get("/api/connections", "agency", BOTH),
56897
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),
56898
57027
  get("/api/shopper/report-seat", "agency", BOTH),
56899
57028
  // A report's branding is kept per client (report-brand-choices.json[client]): a one-client row, the client in the body.
56900
57029
  via(post("/api/shopper/report-brand", "client", BOTH), { body: "client" }),
@@ -61333,19 +61462,36 @@ function startDesignRun(request2, opts = {}) {
61333
61462
  });
61334
61463
  return job;
61335
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
+ }
61336
61468
  function designRunStatus(id2) {
61337
61469
  const job = jobs.get(id2);
61338
61470
  if (!job) return null;
61471
+ const shown = { ...job, tail: memberRunLines(job.tail) };
61339
61472
  const runJson = (0, import_node_path13.join)(job.outDir, "run.json");
61340
- if (!(0, import_node_fs15.existsSync)(runJson)) return { ...job };
61473
+ if (!(0, import_node_fs15.existsSync)(runJson)) return shown;
61341
61474
  try {
61342
- return { ...job, run: JSON.parse((0, import_node_fs15.readFileSync)(runJson, "utf8")) };
61475
+ return { ...shown, run: JSON.parse((0, import_node_fs15.readFileSync)(runJson, "utf8")) };
61343
61476
  } catch {
61344
- return { ...job };
61477
+ return shown;
61478
+ }
61479
+ }
61480
+ function designResultFile(id2) {
61481
+ const job = jobs.get(id2);
61482
+ if (!job || job.status !== "done") return null;
61483
+ try {
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;
61489
+ } catch {
61490
+ return null;
61345
61491
  }
61346
61492
  }
61347
61493
  function listDesignRuns() {
61348
- 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));
61349
61495
  }
61350
61496
  function cancelDesignRun(id2) {
61351
61497
  const child = children.get(id2);
@@ -61359,7 +61505,7 @@ function cancelDesignRun(id2) {
61359
61505
  }
61360
61506
  return true;
61361
61507
  }
61362
- 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;
61363
61509
  var init_run_jobs = __esm({
61364
61510
  "src/command-os/design/run-jobs.ts"() {
61365
61511
  "use strict";
@@ -61375,6 +61521,9 @@ var init_run_jobs = __esm({
61375
61521
  JOINED_FLAG = /^--[a-z][a-z-]*=/;
61376
61522
  jobs = /* @__PURE__ */ new Map();
61377
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;
61378
61527
  }
61379
61528
  });
61380
61529
 
@@ -61384,9 +61533,9 @@ var init_run_expectations = __esm({
61384
61533
  "src/command-os/design/run-expectations.ts"() {
61385
61534
  "use strict";
61386
61535
  RUN_MAX_PASSES = 3;
61387
- RUN_PASS_MINUTES = "9 to 13 minutes";
61388
- RUN_PASS_COST = "$1.25 to $1.75";
61389
- 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";
61390
61539
  RUN_TOTAL_COST = "about $5";
61391
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}.`;
61392
61541
  }
@@ -63297,20 +63446,20 @@ function presetOwnWords(s) {
63297
63446
  return s.replace(INLINE_TOKEN_RE, (_whole, _path, def) => def !== void 0 ? ` ${def.trim()} ` : " ").replace(/\|\|/g, " ");
63298
63447
  }
63299
63448
  function claimSafeBrief(brief, blocked) {
63300
- const clean5 = (v) => {
63449
+ const clean6 = (v) => {
63301
63450
  if (typeof v === "string") return blocked(v).length > 0 ? void 0 : v;
63302
- 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);
63303
63452
  if (v && typeof v === "object") {
63304
63453
  const out = {};
63305
63454
  for (const [k, x] of Object.entries(v)) {
63306
- const c = clean5(x);
63455
+ const c = clean6(x);
63307
63456
  if (c !== void 0) out[k] = c;
63308
63457
  }
63309
63458
  return out;
63310
63459
  }
63311
63460
  return v;
63312
63461
  };
63313
- return clean5(brief);
63462
+ return clean6(brief);
63314
63463
  }
63315
63464
  function isHollowMessage(s) {
63316
63465
  return visibleText2(stripOptOutLines(s)).replace(/[\s.,!?-]/g, "") === "";
@@ -65278,6 +65427,8 @@ function stepProgress(i) {
65278
65427
  i.assessment === "attached" || i.assessment === "without" || i.assessment === "" && [2, 3, 4, 5, 6].some((n) => i.stages[n] === "done"),
65279
65428
  // Old clients' stage 0 was a form install; it counts only once answers are really in (review of 64da59d).
65280
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,
65281
65432
  stage(1),
65282
65433
  i.staffReady || stage(3),
65283
65434
  stage(2) && i.approved,
@@ -65305,6 +65456,7 @@ var init_build_steps = __esm({
65305
65456
  "Industry",
65306
65457
  "Assessment",
65307
65458
  "Intake",
65459
+ "What they bought",
65308
65460
  "Brief",
65309
65461
  "Staff",
65310
65462
  "Approve",
@@ -65319,9 +65471,10 @@ var init_build_steps = __esm({
65319
65471
  "What kind of business is this? It picks the intake questions and the plan.",
65320
65472
  "Talk with the owner and attach the Assessment, or build without one.",
65321
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.",
65322
65475
  "Claude reads the intake and writes the plan for this client.",
65323
65476
  "Your team from step 5 must be in the client's GoHighLevel account before anything is built for them.",
65324
- "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.",
65325
65478
  "We build the plan in GoHighLevel. You can watch it happen.",
65326
65479
  "We check that every part works.",
65327
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.",
@@ -65363,7 +65516,7 @@ function bsInputs(c,state,run){const id=c.locationId;const st=(((state.clients||
65363
65516
  const sc=(state.staffChecks||{})[id];const ii=((state.intakeInputs||{})[id])||{};
65364
65517
  const intakeIn=Boolean((state.intakeCompleted||{})[id])||ii.status==="received";
65365
65518
  return {intakeIn:intakeIn,typeConfirmed:Boolean(acc.typeConfirmed),parked:["template","hidden"].includes(typeOf(state,id)),industry:acc.industry||"",assessment:assess,stages:st,stale:stale,
65366
- 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)}}
65367
65520
  /* Where a client stands, for the My clients list: "Waiting on step 5 of 12: Intake". */
65368
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"};
65369
65522
  const p=stepProgress(bsInputs(c,state,run));const n=BS_NAMES.length;
@@ -65440,14 +65593,15 @@ function bsSummary(n,c,state){const id=c.locationId;const acc=((state.accounts||
65440
65593
  if(n===2){const f=INDUSTRIES2.find(x=>x.slug===acc.industry);return f?f.label:(acc.industry||"")}
65441
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."}
65442
65595
  if(n===4)return "Intake finished.";
65443
- if(n===5)return "Brief written. The Blueprint Review is ready for step 8.";
65444
- 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."}
65445
- 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."}
65446
65600
  /* Run 9: the folded step said only "Built.". It says what was made, from the build's own record. */
65447
- 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.";
65448
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"};
65449
65603
  return "Built in GoHighLevel: "+Object.entries(by).map(([k,v])=>v+" "+plural(v,KIND[k]||k)).join(", ")+". Nothing is switched on."}
65450
- 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;
65451
65605
  /* Never "clean" while a job that blocks go-live is open (blind run 2: "clean" beside "no texts can be sent"). */
65452
65606
  /* Command-center 2026-10-03: never "clean" on ticks alone. */
65453
65607
  const tk=(((state.jobCounts||{})[id])||{}).ticked||0;
@@ -65456,14 +65610,14 @@ function bsSummary(n,c,state){const id=c.locationId;const acc=((state.accounts||
65456
65610
  const conf=((oc&&oc.verification&&oc.verification.memberJobs)||{}).confirmed;
65457
65611
  const lead=typeof conf==="number"?"GoHighLevel confirms "+conf+" "+plural(conf,"item")+".":"The check finished.";
65458
65612
  const mine=(tk||typeof conf==="number")?" You confirmed "+tk+" yourself.":"";
65459
- 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.";
65460
65614
  return oc?"Checked. Some parts need another look: see the notes.":"Checked."}
65461
- if(n===10)return "Hand jobs confirmed.";
65615
+ if(n===11)return "Hand jobs confirmed.";
65462
65616
  return "Signed off. Your next jobs in GoHighLevel are listed on this step."}
65463
65617
  /* Steps 2, 3, 4, 5 and 7 can be changed later; the server marks what comes after them out of date. */
65464
- const BS_CHANGEABLE=[1,2,3,4];
65618
+ const BS_CHANGEABLE=[1,2,3,4,5];
65465
65619
  /* The saved stage each step runs or marks (null: a step the cockpit keeps itself). */
65466
- 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];
65467
65621
  function bsBody(n,c,state,run,box){const id=c.locationId;const key="s"+n+":"+id;
65468
65622
  /* An older build recorded in the account is named, never applied (run 7: a fresh start showed old steps as done). */
65469
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."));
@@ -65495,20 +65649,23 @@ function bsBody(n,c,state,run,box){const id=c.locationId;const key="s"+n+":"+id;
65495
65649
  const j=await bsPost("/api/intake-complete",body);bsBusy[key]=false;
65496
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)}));
65497
65651
  bsErrLine(key,box);return}
65498
- 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}
65499
- if(n===6){bsStaffStep(c,state,box);return}
65500
- 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;
65501
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))}
65502
65659
  if(!rData[id])rLoad(id);bsHideAssess=true;try{box.appendChild(unnest(reviewPanel(id,c.name,state,run)))}finally{bsHideAssess=false}return}
65503
- 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;
65504
65661
  if(bp&&!bp.confirmed&&!live){bsPause(c,state,bp.preview,box);return}
65505
65662
  bsOutcome(c,state,3,box);
65506
65663
  /* Run 8 finding 4: it ended on just "Built." The finish says what was made, counted from the build's own record. */
65507
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");
65508
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"};
65509
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}
65510
- 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}
65511
- 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);
65512
65669
  /* The handoff panel is here as well as in step 12: with a chosen home, its publish job blocks step 11 and only
65513
65670
  this panel clears it (round-3 review). With no home chosen it is a later job (option A, 2026-10-02). */
65514
65671
  const view=((state.buildViews||{})[id])||{};if(view.handoff)box.appendChild(unnest(handoffPanel(c,view)));
@@ -65517,7 +65674,7 @@ function bsBody(n,c,state,run,box){const id=c.locationId;const key="s"+n+":"+id;
65517
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."))}
65518
65675
  else bsGate(c,state,5,"Confirm these tasks are finished",box);
65519
65676
  bsRecheck(c,state,run,box);return}
65520
- if(n===11){const view=((state.buildViews||{})[id])||{};
65677
+ if(n===12){const view=((state.buildViews||{})[id])||{};
65521
65678
  /* Jerry 2026-10-03: Sign-Off, not Go-Live. Say what Command OS did, then what the person does by hand. */
65522
65679
  box.appendChild(bsSignOffList(c,state));
65523
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}}
@@ -65532,7 +65689,18 @@ function bsPause(c,state,pv,box){const id=c.locationId;const key="confirm:"+id;
65532
65689
  /* Ticks belong to one list: a new list starts unticked, and only keys on the list shown are sent (review of a38ebe1). */
65533
65690
  if(!bsAllow[id]||bsAllow[id].fp!==pv.fingerprint)bsAllow[id]={fp:pv.fingerprint,keys:{}};const allow=bsAllow[id].keys;
65534
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}
65535
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."));
65536
65704
  /* Walk 2026-10-28 W13/W8: "17 emails, 8 email templates" said nothing about the difference, and the list named nothing.
65537
65705
  Each kind says what it is, and opens to the name of every object of that kind. */
65538
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")};
@@ -65747,7 +65915,7 @@ function bsJobs(c,state){const mt=((state.manualTasks||{})[c.locationId])||[];co
65747
65915
  else if(readJob&&g.timing!=="at-go-live"){
65748
65916
  /* Walk W21: what the check last found, and what to do about it. */
65749
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.";
65750
- 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.")}
65751
65919
  else if(cb.checked&&g.items.some(t=>t.blocks))note("Ticked. The check confirms it next time it runs.");return row};
65752
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");
65753
65921
  const left=must.filter(g=>g.items.some(t=>t.blocks!==undefined?t.blocks:!t.claimed)).length;
@@ -65805,8 +65973,8 @@ function stepsPage(c,state,run){const id=c.locationId;const inp=bsInputs(c,state
65805
65973
  /* A Change closes itself once that step is done with a different answer (review of 64da59d, LOW 15). */
65806
65974
  if(bsChange[ck]&&pr.done[i]&&typeof bsChange[ck]==="string"&&bsSummary(i,c,state)!==bsChange[ck])delete bsChange[ck];
65807
65975
  /* Step 7 stays open while what the last Add them for me said, or its refusal, is there to read (3.90.1). */
65808
- if(i===6)bsRevealPasswords(id,(state.staffChecks||{})[id]||bsStaff[id]);
65809
- 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]));
65810
65978
  const li=mk("li","bs-step");li.id="step-"+(i+1);const open=(i===pr.current)||Boolean(bsChange[ck])||staffNews;
65811
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);
65812
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]));
@@ -65825,8 +65993,8 @@ function stepsPage(c,state,run){const id=c.locationId;const inp=bsInputs(c,state
65825
65993
  ch.onclick=()=>{bsChange[ck]=bsSummary(i,c,state);render(lastState,lastRun)};main.appendChild(ch);
65826
65994
  if(i>=2)main.appendChild(mk("p","muted bs-warn","Saving a change here means the steps after it run again."))}
65827
65995
  /* Step 8 can be taken back until Build runs (review of 64da59d: a team change after Approve had no way back). */
65828
- if(i===7&&idle&&!pr.done[8]){const un=mk("button","cds-link","Undo the approval");un.type="button";const uk="unapprove:"+id;
65829
- 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…",
65830
65998
  run:async()=>{const j=await bsPost("/api/build-approval",{locationId:id,revoke:true});return j.ok?"":j.error},after:async()=>{load(true)}});
65831
65999
  main.appendChild(icAfter(un,uk))}}
65832
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]))}
@@ -65839,7 +66007,7 @@ function stepsPage(c,state,run){const id=c.locationId;const inp=bsInputs(c,state
65839
66007
  if(view.approvedAt)d.appendChild(mk("p","","Building approved on "+niceDate(view.approvedAt,true)+"."));page.appendChild(d)}}
65840
66008
  /* The step that just opened comes into view, once per change (and on first open), after the redraw keeps its scroll. */
65841
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)}
65842
- 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."));
65843
66011
  /* Walk W26 (2026-10-29): after sign-off nothing could start the build again, and the page still said Built after its
65844
66012
  objects were deleted in GoHighLevel. The check can always run again; when it finds built objects gone, it says how
65845
66013
  many and Build again is offered beside it. */
@@ -65968,7 +66136,7 @@ ${cockpitHeaderHtml("agency")}
65968
66136
  </div>
65969
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>
65970
66138
  </div>
65971
- ${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>
65972
66140
  `}
65973
66141
  <form id="f">
65974
66142
  <div class="cds-grid two">
@@ -66274,7 +66442,7 @@ document.getElementById("f").addEventListener("submit",async e=>{
66274
66442
  if(!r.ok||(j&&j.ok===false)){m.textContent=(j&&j.error)||"That did not save, and nothing was changed.";return}
66275
66443
  const pkgNote=settlePackages(j&&j.profile,pkgRead);
66276
66444
  const chatOk=await saveChatCreation();
66277
- 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.");
66278
66446
  });
66279
66447
 
66280
66448
  /* Each client saves on its own. One record at a time is the whole safety story
@@ -68836,7 +69004,7 @@ ${yours}
68836
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>
68837
69005
  <form id="extract" class="cds-stack bp-form">
68838
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>
68839
- <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>
68840
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>
68841
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>
68842
69010
  <div class="cds-row"><button type="submit" class="cds-btn ghost" id="extract-btn">Pull the brand</button></div>
@@ -68868,7 +69036,7 @@ fromHash();
68868
69036
  const rf=document.getElementById("ref-filter");
68869
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"):""});
68870
69038
  })();
68871
- 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}});
68872
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}})}
68873
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}})}
68874
69042
  `)}`;
@@ -69007,7 +69175,10 @@ document.getElementById("make").addEventListener("submit",async function(e){
69007
69175
  var run=s.run||{};
69008
69176
  var design=run.ok?"design PASS on pass "+run.greenAt:"design NOT GREEN";
69009
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+")":"")):"";
69010
- 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)}
69011
69182
  }
69012
69183
  },4000);
69013
69184
  }catch(err){stopPolling();started=0;say("mk-status",err.message,"err");go.disabled=false}
@@ -70390,11 +70561,11 @@ __export(client_calls_exports, {
70390
70561
  function gateAnalysis(transcript, analysis) {
70391
70562
  const rejected = [];
70392
70563
  const kept = {
70393
- saidTheyWant: (analysis.saidTheyWant ?? []).map(clean4).filter(Boolean),
70564
+ saidTheyWant: (analysis.saidTheyWant ?? []).map(clean5).filter(Boolean),
70394
70565
  actuallyNeed: [],
70395
70566
  unnamedProblems: [],
70396
70567
  recommendedSolutions: [],
70397
- nextCallQuestions: (analysis.nextCallQuestions ?? []).map(clean4).filter(Boolean)
70568
+ nextCallQuestions: (analysis.nextCallQuestions ?? []).map(clean5).filter(Boolean)
70398
70569
  };
70399
70570
  const check = (section2, item, quote2) => {
70400
70571
  if (!item) {
@@ -70410,17 +70581,17 @@ function gateAnalysis(transcript, analysis) {
70410
70581
  };
70411
70582
  for (const section2 of ["actuallyNeed", "unnamedProblems"]) {
70412
70583
  for (const p of analysis[section2] ?? []) {
70413
- const point = clean4(p?.point), quote2 = String(p?.quote ?? "").trim();
70584
+ const point = clean5(p?.point), quote2 = String(p?.quote ?? "").trim();
70414
70585
  if (check(section2, point, quote2)) kept[section2].push({ point, quote: quote2 });
70415
70586
  }
70416
70587
  }
70417
70588
  for (const s of analysis.recommendedSolutions ?? []) {
70418
- const solution = clean4(s?.solution), quote2 = String(s?.quote ?? "").trim();
70589
+ const solution = clean5(s?.solution), quote2 = String(s?.quote ?? "").trim();
70419
70590
  if (!SOLUTION_KINDS.includes(s?.kind)) {
70420
70591
  rejected.push({ section: "recommendedSolutions", item: solution, quote: quote2, why: `"${String(s?.kind)}" is not a kind; use ${SOLUTION_KINDS.join(", ")}.` });
70421
70592
  continue;
70422
70593
  }
70423
- 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 });
70424
70595
  }
70425
70596
  return { kept, rejected };
70426
70597
  }
@@ -70739,7 +70910,7 @@ async function saveToContact(input, deps, gated, source) {
70739
70910
  if (accepted.length === 0) {
70740
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 });
70741
70912
  }
70742
- 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;
70743
70914
  if (!biz) return fail("neither the contact nor a saved assessment has a business name. Give businessName and try again.");
70744
70915
  out.businessName = out.businessName ?? biz;
70745
70916
  const skip = { ...record?.state.skip ?? {} };
@@ -70836,7 +71007,7 @@ async function processClientCall(input, deps) {
70836
71007
  To confirm:
70837
71008
  ${confirmList(gated.held).map((l) => `- ${l}`).join("\n")}
70838
71009
  ${CONFIRM_INSTRUCTION}`;
70839
- const clientName = save?.businessName ?? save?.contactName ?? clean4(input.businessName);
71010
+ const clientName = save?.businessName ?? save?.contactName ?? clean5(input.businessName);
70840
71011
  const confirmPass = (input.confirmed?.length ?? 0) > 0;
70841
71012
  const clientFile = clientName ? buildClientFile({ clientName, date, source: input.source, gated, analysis, transcript: input.transcript, saveLine, confirmedByOwner: save?.confirmedByOwner }) : null;
70842
71013
  if (clientFile && confirmPass) {
@@ -70878,7 +71049,7 @@ ${body}
70878
71049
  localCopy
70879
71050
  };
70880
71051
  }
70881
- 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;
70882
71053
  var init_client_calls = __esm({
70883
71054
  "src/command-os/client-calls.ts"() {
70884
71055
  "use strict";
@@ -70890,7 +71061,7 @@ var init_client_calls = __esm({
70890
71061
  init_assessment();
70891
71062
  RECORDERS = ["Plaud", "Zoom", "Other"];
70892
71063
  SOLUTION_KINDS = ["GoHighLevel build", "AI tool", "custom build", "process"];
70893
- clean4 = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
71064
+ clean5 = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
70894
71065
  ANALYSIS_MIN_WORDS = 3;
70895
71066
  CLIENT_FILES_ROOT = ["Command OS", "Clients"];
70896
71067
  WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
@@ -71585,8 +71756,29 @@ function composePreview(input) {
71585
71756
  else reuse.push({ key: key2, type: item.type, name: item.name, id: item.existingId });
71586
71757
  }
71587
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);
71588
71769
  const core = { locationId: input.locationId, planHash: input.planHash, reuse, change, logins: [...input.logins ?? []] };
71589
- 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
+ };
71590
71782
  }
71591
71783
  function allowedWorkflowIds(allow) {
71592
71784
  return new Set(allow.filter((k) => k.startsWith("workflows:")).map((k) => k.slice("workflows:".length)));
@@ -71606,6 +71798,7 @@ function boardAllowed(preview, confirm2) {
71606
71798
  }
71607
71799
  function confirmMatches(confirm2, preview) {
71608
71800
  if (!previewNamesLogins(preview)) return false;
71801
+ if (preview.earlierBuild?.length) return false;
71609
71802
  if (!confirm2 || confirm2.fingerprint !== preview.fingerprint || !Array.isArray(confirm2.allow)) return false;
71610
71803
  const changeKeys = new Set(preview.change.map((e) => e.key));
71611
71804
  return confirm2.allow.every((k) => changeKeys.has(k));
@@ -71680,6 +71873,14 @@ function listedValue(raw, id2) {
71680
71873
  const value = hit && typeof hit === "object" ? Reflect.get(hit, "value") : void 0;
71681
71874
  return typeof value === "string" ? value : void 0;
71682
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
+ }
71683
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;
71684
71885
  var init_build_confirm = __esm({
71685
71886
  "src/build-confirm.ts"() {
@@ -72192,6 +72393,7 @@ __export(dashboard_exports, {
72192
72393
  forgetVerifiedHandoff: () => forgetVerifiedHandoff,
72193
72394
  guarded: () => guarded,
72194
72395
  hasChosenScope: () => hasChosenScope,
72396
+ industryChoices: () => industryChoices,
72195
72397
  insideDir: () => insideDir,
72196
72398
  intakeAskFacts: () => intakeAskFacts,
72197
72399
  intakeErrorDetail: () => intakeErrorDetail,
@@ -72217,6 +72419,7 @@ __export(dashboard_exports, {
72217
72419
  memberFacingError: () => memberFacingError,
72218
72420
  mergeQueueIntakes: () => mergeQueueIntakes,
72219
72421
  newClientFailure: () => newClientFailure,
72422
+ noClientPageHtml: () => noClientPageHtml,
72220
72423
  normalizedIntake: () => normalizedIntake,
72221
72424
  notPreparedHtml: () => notPreparedHtml,
72222
72425
  notSignedInMsg: () => notSignedInMsg,
@@ -72904,7 +73107,7 @@ function startRun(locationId2, stage, onDone, meta = {}) {
72904
73107
  const at = (/* @__PURE__ */ new Date()).toISOString();
72905
73108
  const shared = { at, locationId: locationId2, stage, engineVersion: outcome.engineVersion, engineModel: outcome.engineModel };
72906
73109
  if (outcome.engineCheckRan || outcome.engineCheckCostUsd !== void 0) {
72907
- recordRunCost({ ...shared, ok: true, kind: "engine-check", costUsd: outcome.engineCheckCostUsd });
73110
+ recordRunCost({ ...shared, ok: outcome.engineCheckOk !== false, kind: "engine-check", costUsd: outcome.engineCheckCostUsd });
72908
73111
  }
72909
73112
  recordRunCost({
72910
73113
  ...shared,
@@ -72930,6 +73133,9 @@ function startRun(locationId2, stage, onDone, meta = {}) {
72930
73133
  outcome.issues = [];
72931
73134
  outcome.plain = { summary: BRIEF_NEEDS_INTAKE2, issues: [] };
72932
73135
  }
73136
+ const noted = failureNoteFor(stage, outcome, run.lines, scopeRefusal || briefNeedsIntake);
73137
+ const failNote = noted !== null;
73138
+ if (noted) Object.assign(outcome, noted);
72933
73139
  if (stage === 3 && !outcome.summary && !outcome.issues?.length) {
72934
73140
  try {
72935
73141
  const st = readCockpitState();
@@ -72945,8 +73151,8 @@ function startRun(locationId2, stage, onDone, meta = {}) {
72945
73151
  const st = readCockpitState();
72946
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 }) } } });
72947
73153
  };
72948
- save(scopeRefusal || briefNeedsIntake ? outcome.plain : void 0);
72949
- 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) {
72950
73156
  run.lines.push("Writing the plain-English version of what came back\u2026");
72951
73157
  const rewritten = await rewriteOutcomePlain(STAGES2[stage], verifyRaw ?? outcome, { onEvent: (l) => run.lines.push(l), onCost: (costUsd) => {
72952
73158
  recordRunCost({ at, locationId: locationId2, stage, kind: "plain rewrite", ok: true, costUsd });
@@ -73100,6 +73306,10 @@ async function cockpitAlreadyRunning(port) {
73100
73306
  return false;
73101
73307
  }
73102
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
+ }
73103
73313
  function renderCockpitHtml(roster, seatToken = "", screen = "board", cap2) {
73104
73314
  return html(roster, seatToken, screen, cap2);
73105
73315
  }
@@ -73461,8 +73671,8 @@ function intakeRequestError(e) {
73461
73671
  function intakeErrorDetail(e) {
73462
73672
  const raw = e instanceof Error ? e.message : typeof e === "string" ? e : "";
73463
73673
  if (!raw || INTAKE_OWN_MESSAGES.includes(raw)) return void 0;
73464
- 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();
73465
- 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;
73466
73676
  }
73467
73677
  function intakeRefusal(e) {
73468
73678
  const detail = intakeErrorDetail(e);
@@ -73673,6 +73883,16 @@ ${cockpitHeaderHtml("for", { trail: ["For this client", what] })}
73673
73883
  ${cockpitShellClose()}
73674
73884
  <script>${COCKPIT_TYPE_JS}</script></body></html>`;
73675
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
+ }
73676
73896
  function unknownCapabilityHtml() {
73677
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">
73678
73898
  ${cockpitHeaderHtml("for", { trail: ["For this client", "Page not found"] })}
@@ -73898,7 +74118,7 @@ function followRefresh(id,n){if(n>20)return;const tok=clientToken();setTimeout(a
73898
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;
73899
74119
  if(!stillFor(tok))return;
73900
74120
  if(JSON.stringify(sr.snapshots)!==JSON.stringify(snapshots)){snapshots=sr.snapshots;if(!userIsBusy()&&!unsavedTyping()&&lastState)renderFor(tok);else redrawPending=true}else paintAges()}
73901
- const INDUSTRIES=${scriptJson([...INDUSTRY_PACKS.map((p) => ({ slug: p.slug, label: p.label })), { slug: "other", label: "Other" }])};
74121
+ const INDUSTRIES=${scriptJson(industryChoices())};
73902
74122
  const INDUSTRIES2=${scriptJson(INDUSTRY_PACKS.map((p) => ({ slug: p.slug, label: p.label })))};
73903
74123
  /* One account at a time (Jerry, 2026-08-26: "way too busy\u2026 tempting to work on multiple
73904
74124
  accounts at once"). One card is ever expanded; every other account is a one-line row. */
@@ -74680,7 +74900,9 @@ function scopeBlock(id,state){
74680
74900
  /* Run 8 finding 9: about 5 seconds with nothing showing it was working. */
74681
74901
  sv.disabled=true;const was=sv.textContent;sv.textContent="Saving\u2026";status.textContent="Saving what they bought\u2026";
74682
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);
74683
- 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.";load()}
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()}
74684
74906
  if(sv.isConnected){sv.disabled=false;sv.textContent=was}};
74685
74907
  /* Audit 2026-10-28 #9: the always-built pipeline counts as built ("2 bought, 1 built" while it is always built). */
74686
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. */
@@ -74717,15 +74939,19 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
74717
74939
  if(busy){const p=document.createElement("p");p.className="muted";
74718
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)}
74719
74941
  /* rc.3 c6 hole 1: a review written before the answers changed may promise what the build will not make. */
74720
- if(r&&r.ok&&r.stale&&!busy){const w=mk("p","cds-callout warn","This review was written before your client's answers or what they bought changed, so it may promise what this build will not make. Press Regenerate before you share it; the client document waits for the new one.");w.setAttribute("role","alert");d.appendChild(w)}
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)}
74721
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)}
74722
- if(r&&r.ok&&Array.isArray(r.trimmed)&&r.trimmed.length&&!busy)d.appendChild(mk("p","muted","Taken out because they promised more than this build makes: "+r.trimmed.join("; ")+"."));
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)}
74723
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";
74724
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.";
74725
74950
  d.appendChild(p)}
74726
74951
  d.appendChild(gen);
74727
- /* What they bought decides the build, so it is here even before the review is written (2026-09-29). */
74728
- 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));
74729
74955
  /* On the steps page Approve never waits for the review: the server does not need it (round-4 review). */
74730
74956
  if(bsHideAssess&&scopeChosen(state,id)&&!(state.buildApproved||{})[id]&&!PROTECTED[id])d.appendChild(approveButton(id,name,state,"review","cds-btn"));
74731
74957
  return d}
@@ -74758,7 +74984,7 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
74758
74984
  s4.appendChild(bl);d.appendChild(s4)}}
74759
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>";
74760
74986
  const p=document.createElement("p");p.textContent=r.unknowns.join(" \xB7 ");s5.appendChild(p);d.appendChild(s5)}
74761
- d.appendChild(scopeBlock(id,state));
74987
+ if(!bsHideAssess)d.appendChild(scopeBlock(id,state));
74762
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";
74763
74989
  const an=document.createElement("input");an.placeholder="Your agency name (on the client doc)";an.value=(state.agencyName||"");
74764
74990
  an.className="cds-input";an.setAttribute("aria-label","Your agency name on the client document");
@@ -74780,6 +75006,7 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
74780
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)}
74781
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)}
74782
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"}
74783
75010
  row.appendChild(gen);d.appendChild(row);
74784
75011
  return d}
74785
75012
  const NC_AGENCY=${scriptJson(readAgencyProfile().agencyName?.trim() || "in GoHighLevel")};
@@ -75563,7 +75790,7 @@ function autoBuildClaim(id,b){if(!autoBuild[id])return;queueMicrotask(()=>{const
75563
75790
  function approveSpec(id,name,state){const sandbox=typeOf(state,id)==="sandbox";
75564
75791
  const view=(state.buildViews||{})[id];const repairs=(view&&view.planHash&&view.repairs)||[];
75565
75792
  return {title:"Approve this plan for "+name,
75566
- 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."],
75567
75794
  after:async()=>{load()},
75568
75795
  fields:[NAME_FIELD()].concat(sandbox?[]:[{id:"client",label:"Type the client name to confirm: "+name,value:""}]),
75569
75796
  ok:"Approve",busyLabel:"Approving\u2026",
@@ -75867,7 +76094,8 @@ function renderCapability(m,state,run){
75867
76094
  if(CAP.key==="checkup"){const v=capCheckupPanel(last,bb);capReadsInto(v,c,hasBuild,true);m.appendChild(v)}
75868
76095
  else{
75869
76096
  /* In this account now, read live from GoHighLevel. */
75870
- 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");
75871
76099
  capReadsInto(card,c,hasBuild,false);m.appendChild(card)}
75872
76100
  m.appendChild(capDoPanel(c,state,run,words,hasBuild,busy,last));
75873
76101
  /* From the last build: only this page's own items. */
@@ -76003,7 +76231,7 @@ function capOwedPanel(owed,counts,hl){if(!counts){counts={before:0,atGoLive:0,la
76003
76231
  const jobs=owed.filter(x=>x.job),lines=hl||[];
76004
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}
76005
76233
  const c=counts||{before:0,atGoLive:0,later:0};
76006
- 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."));
76007
76235
  if(lines.length)p.appendChild(mk("p","muted",handoffCountLine(lines,c)));
76008
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}
76009
76237
  /* Intake and documents: what is answered, what is still open, the key facts, and the files. */
@@ -77549,7 +77777,7 @@ async function runDashboard(argv) {
77549
77777
  if (other) cockpitCache.invalidate(account2.locationId, ["brief-account"]);
77550
77778
  const v = other ? { cold: true, refreshing: v0.refreshing, stale: true } : v0;
77551
77779
  seen.push({ account: account2, v });
77552
- 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);
77553
77781
  return v.data.read;
77554
77782
  },
77555
77783
  ages: () => {
@@ -78337,7 +78565,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78337
78565
  res.writeHead(200, { "Content-Type": "application/json" });
78338
78566
  const proClient = gate3.plan === "pro" ? new URL(req.url || "/", "http://x").searchParams.get("locationId") ?? "" : void 0;
78339
78567
  const mine = activeRun2 && (proClient === void 0 || activeRun2.locationId === proClient);
78340
- 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: [] }));
78341
78569
  return;
78342
78570
  }
78343
78571
  if (req.method === "GET" && url === "/api/run/secrets") {
@@ -78855,6 +79083,17 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78855
79083
  });
78856
79084
  return;
78857
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
+ }
78858
79097
  if (req.method === "GET" && url === "/api/design/run") {
78859
79098
  const id2 = new URL(req.url || "/", "http://x").searchParams.get("id");
78860
79099
  const payload = id2 ? designRunStatus(id2) : { runs: listDesignRuns() };
@@ -78978,8 +79217,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78978
79217
  if (req.method === "GET" && url === "/handoff") {
78979
79218
  const locationId2 = new URL(req.url || "/", "http://x").searchParams.get("locationId") || "";
78980
79219
  if (!readableAccount(locationId2)) {
78981
- res.writeHead(400);
78982
- 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"));
78983
79222
  return;
78984
79223
  }
78985
79224
  try {
@@ -78993,8 +79232,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78993
79232
  if (req.method === "GET" && url === "/handoff/detail") {
78994
79233
  const locationId2 = new URL(req.url || "/", "http://x").searchParams.get("locationId") || "";
78995
79234
  if (!readableAccount(locationId2)) {
78996
- res.writeHead(400);
78997
- 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"));
78998
79237
  return;
78999
79238
  }
79000
79239
  try {
@@ -79104,8 +79343,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
79104
79343
  const st = readCockpitState();
79105
79344
  const review = st.reviews?.[locationId2];
79106
79345
  if (!review?.ok) {
79107
- res.writeHead(404, { "Content-Type": "text/plain" });
79108
- 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"));
79109
79348
  return;
79110
79349
  }
79111
79350
  if (reviewIsStale(st, locationId2, review)) {
@@ -79180,9 +79419,10 @@ ${opportunityNoteFailure?.detail ?? ""}`);
79180
79419
  const rk = who2.recordKey;
79181
79420
  if (PROTECTED_LOCATIONS[who2.locationId]) throw new Error(PROTECTED_LOCATIONS[who2.locationId] + " can never be built.");
79182
79421
  const current = readCockpitState();
79183
- 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.");
79184
79423
  const preview = readBuildPreview(rk);
79185
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);
79186
79426
  const changeKeys = new Set(preview.change.map((e) => e.key));
79187
79427
  const allowed = Array.isArray(allow) ? [...new Set(allow.filter((k) => typeof k === "string"))] : [];
79188
79428
  if (allowed.some((k) => !changeKeys.has(k))) throw new Error("Only items on the list can be changed.");
@@ -79250,6 +79490,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
79250
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 };
79251
79491
  approvals[rk] = approval;
79252
79492
  }
79493
+ if (!revoke && approvals[rk] && reviewIsStale(current, rk, current.reviews?.[rk])) approvals[rk] = { ...approvals[rk], approvedOverStaleReview: true };
79253
79494
  const latest = readCockpitState();
79254
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 });
79255
79496
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -79958,6 +80199,7 @@ var init_dashboard = __esm({
79958
80199
  init_client_status_publish();
79959
80200
  init_handoff_snapshot();
79960
80201
  init_member_progress();
80202
+ init_stage_failure();
79961
80203
  init_build_obligations();
79962
80204
  fs53 = __toESM(require("fs"));
79963
80205
  path53 = __toESM(require("path"));
@@ -80079,15 +80321,15 @@ var init_dashboard = __esm({
80079
80321
  CHOOSE_BEFORE_BUILD = "Choose what the client bought before building: Blueprint Review, then What they bought, then Save.";
80080
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.";
80081
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;
80082
- APPROVAL_WITHDRAWN_FOR_STAFF = "Your team changed after you approved, so the approval was taken back. Approve again in step 8.";
80083
- 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.";
80084
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;
80085
80327
  NOTHING_BOUGHT = "Tick at least one item they bought, then save.";
80086
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.";
80087
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.";
80088
80330
  APPROVAL_WITHDRAWN_FOR_ASSESSMENT = "The Assessment answer changed, so the build approval was withdrawn. Approve this plan again before building.";
80089
80331
  APPROVE_NEEDS_INTAKE = "Approve opens after step 5, Intake, is finished.";
80090
- APPROVE_NEEDS_BRIEF = "Approve opens after step 6, Brief, has run.";
80332
+ APPROVE_NEEDS_BRIEF = "Approve opens after step 7, Brief, has run.";
80091
80333
  INDUSTRY_FIRST = "Choose what kind of business this is in step 3 first.";
80092
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.";
80093
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.";
@@ -86968,14 +87210,14 @@ function registerDesignStudioTools(server2) {
86968
87210
  d.provenance.evidence["shape.radius"] = `set by ${stamp}`;
86969
87211
  }
86970
87212
  if (a.logoSvg) {
86971
- let clean5;
87213
+ let clean6;
86972
87214
  try {
86973
- clean5 = sanitizeSvg(a.logoSvg);
87215
+ clean6 = sanitizeSvg(a.logoSvg);
86974
87216
  } catch (e) {
86975
87217
  throw new Error(`${e.message} Nothing was changed.`);
86976
87218
  }
86977
- a.logoSvg = clean5;
86978
- 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" };
86979
87221
  d.provenance.evidence["logo"] = `set by ${stamp}`;
86980
87222
  }
86981
87223
  if (a.tone) d.voice.tone = a.tone;
@@ -90652,8 +90894,8 @@ async function listProcs(platform2 = process.platform, exec = execText, timeoutM
90652
90894
  return listingOf(platform2, await bounded(exec(file3, args, timeoutMs), timeoutMs));
90653
90895
  }
90654
90896
  function jobMarker(jobDir, platform2 = process.platform) {
90655
- const sep6 = platform2 === "win32" ? "\\" : "/";
90656
- return `--user-data-dir=${jobDir.replace(/[\\/]+$/, "")}${sep6}`;
90897
+ const sep7 = platform2 === "win32" ? "\\" : "/";
90898
+ return `--user-data-dir=${jobDir.replace(/[\\/]+$/, "")}${sep7}`;
90657
90899
  }
90658
90900
  function holdsMarker(command, marker, platform2 = process.platform) {
90659
90901
  return namesJob(command, markerDir(marker), platform2);
@@ -95406,7 +95648,16 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
95406
95648
  }
95407
95649
  const { changes: workflowChanges, states: workflowStates } = await previewWorkflowChanges(deps, result, ours, saved?.calendarPending);
95408
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 }));
95409
- 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) } });
95410
95661
  try {
95411
95662
  saveBuildPreview(preview);
95412
95663
  } catch (e) {
@@ -95417,7 +95668,8 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
95417
95668
  return jsonResponse({
95418
95669
  ok: false,
95419
95670
  phase: "confirm",
95420
- 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 } : {},
95421
95673
  willCreate: preview.createCount,
95422
95674
  willUse: preview.reuse.map((e) => `${PREVIEW_KIND[e.type] ?? e.type} "${e.name}"`),
95423
95675
  wouldChange: preview.change.map((e) => ({ what: `${PREVIEW_KIND[e.type] ?? e.type} "${e.name}"`, changes: e.changes ?? [] }))
@@ -95434,7 +95686,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
95434
95686
  const runId = newRunId();
95435
95687
  const journalParents = parentRefIndex(typedPlan);
95436
95688
  try {
95437
- appendBuildStart({ runId, locationId: activeLocation, locationName, planId: typedPlan.planId });
95689
+ appendBuildStart({ runId, locationId: activeLocation, locationName, planId: typedPlan.planId, planHash: contentHash2(typedPlan) });
95438
95690
  if (notOurs.length) appendBuildNotOurs({ runId, locationId: activeLocation, objects: notOurs.map((e) => ({ type: e.type, name: e.name, ghlId: e.id })) });
95439
95691
  if (allowedChanges.length) appendBuildChanges({ runId, locationId: activeLocation, changes: allowedChanges });
95440
95692
  } catch (e) {
@@ -96821,6 +97073,21 @@ var os11 = __toESM(require("node:os"));
96821
97073
  function sha2563(buf) {
96822
97074
  return (0, import_node_crypto34.createHash)("sha256").update(buf).digest("hex");
96823
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
+ }
96824
97091
  function isOlderRelease(candidate, installed) {
96825
97092
  const parts = (v) => v.trim().split(".").map((n) => Number.parseInt(n, 10));
96826
97093
  const a = parts(candidate), b = parts(installed);
@@ -96868,6 +97135,7 @@ function installBundledSkills(opts) {
96868
97135
  updated: [],
96869
97136
  skippedUserModified: [],
96870
97137
  skippedOlderPackage: [],
97138
+ skippedSymlink: [],
96871
97139
  unchanged: [],
96872
97140
  errors: [],
96873
97141
  targetDir: opts.targetDir ?? defaultSkillsDir(),
@@ -96882,6 +97150,11 @@ function installBundledSkills(opts) {
96882
97150
  result.errors.push(`cannot create ${result.targetDir}: ${String(e)}`);
96883
97151
  return result;
96884
97152
  }
97153
+ if (isLink(result.targetDir)) {
97154
+ result.targetIsLink = true;
97155
+ result.skippedSymlink.push(...listFiles(bundled));
97156
+ return result;
97157
+ }
96885
97158
  const markerPath = path65.join(result.targetDir, ".ghl-command-skills.json");
96886
97159
  const marker = readMarker(markerPath) ?? { packageVersion: "", files: {} };
96887
97160
  const newMarker = { packageVersion: opts.packageVersion, files: { ...marker.files } };
@@ -96889,6 +97162,10 @@ function installBundledSkills(opts) {
96889
97162
  try {
96890
97163
  const src = fs65.readFileSync(path65.join(bundled, rel));
96891
97164
  const dest = path65.join(result.targetDir, rel);
97165
+ if (linkOnTheWay(result.targetDir, rel)) {
97166
+ result.skippedSymlink.push(rel);
97167
+ continue;
97168
+ }
96892
97169
  const srcHash = sha2563(src);
96893
97170
  let destBuf = null;
96894
97171
  try {
@@ -96926,6 +97203,7 @@ function installBundledSkills(opts) {
96926
97203
  }
96927
97204
  }
96928
97205
  try {
97206
+ if (isLink(markerPath)) throw new Error("the marker file is a symbolic link; not written");
96929
97207
  fs65.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
96930
97208
  } catch (e) {
96931
97209
  result.errors.push(`marker write failed: ${String(e)}`);
@@ -96946,6 +97224,7 @@ function summarizeInstall(r) {
96946
97224
  bucket(r.updated, "updated");
96947
97225
  bucket(r.skippedUserModified, "kept with your edits");
96948
97226
  bucket(r.skippedOlderPackage, "kept, newer than this version");
97227
+ bucket(r.skippedSymlink ?? [], "not touched, linked elsewhere");
96949
97228
  if (r.errors.length) bits.push(`${r.errors.length} file error${r.errors.length === 1 ? "" : "s"}`);
96950
97229
  if (bits.length === 0) return "";
96951
97230
  return `Skills: ${bits.join(", ")} \u2192 ${r.targetDir}`;
@@ -96973,7 +97252,7 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
96973
97252
  ok: result.errors.length === 0,
96974
97253
  targetDir: result.targetDir,
96975
97254
  // Skills, by name, before the per-file detail (BUG-2026-09-14-03).
96976
- 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) },
96977
97256
  installed: result.installed,
96978
97257
  updated: result.updated,
96979
97258
  keptYourEdits: result.skippedUserModified,
@@ -96982,7 +97261,14 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
96982
97261
  keptNewerThanThisVersion: result.skippedOlderPackage,
96983
97262
  unchanged: result.unchanged.length,
96984
97263
  errors: result.errors,
96985
- 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(" ")
96986
97272
  });
96987
97273
  } catch (error) {
96988
97274
  return errorResponse(error);
@@ -96990,6 +97276,10 @@ function registerSkillsTool(server2, packageVersion, baseDir) {
96990
97276
  }
96991
97277
  );
96992
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
+ }
96993
97283
 
96994
97284
  // src/index.ts
96995
97285
  init_brain();