@elitedcs/ghl-mcp 3.89.0-beta.5 → 3.89.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -27616,6 +27616,23 @@ function calendarSlotMismatch(planCal, existing) {
27616
27616
  if (want === void 0 || have === void 0 || want === have) return void 0;
27617
27617
  return `Calendar "${planCal.name}" is ${have} min in the account, the plan says ${want} \u2014 change it in Calendars \u2192 Settings (slot duration) or tell Blueprint to update it (update_calendar ${existing.id}). Blueprint never modifies an existing calendar on its own.`;
27618
27618
  }
27619
+ function openHoursKey(blocks) {
27620
+ if (!Array.isArray(blocks) || blocks.length === 0) return void 0;
27621
+ const byDay = /* @__PURE__ */ new Map();
27622
+ const t = (h, m) => `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
27623
+ for (const b of blocks) for (const d of b.daysOfTheWeek ?? []) {
27624
+ const list4 = byDay.get(d) ?? [];
27625
+ for (const w of b.hours ?? []) list4.push(`${t(w.openHour, w.openMinute)}-${t(w.closeHour, w.closeMinute)}`);
27626
+ byDay.set(d, list4);
27627
+ }
27628
+ return [...byDay.entries()].sort((a, b) => a[0] - b[0]).map(([d, ws]) => `${d}:${ws.sort().join(",")}`).join(" ");
27629
+ }
27630
+ function calendarHoursMismatch(planCal, existing) {
27631
+ const want = openHoursKey(planCal.openHours);
27632
+ const have = openHoursKey(existing.openHours);
27633
+ if (want === void 0 || have === void 0 || want === have) return void 0;
27634
+ return `Calendar "${planCal.name}" was already in the account, so its open hours were left as they are, and they differ from the hours in the plan (the intake's business hours). Set its hours by hand if the client's hours are the right ones. Build never changes a calendar that is already there.`;
27635
+ }
27619
27636
  function calendarNeedsStaff(cal) {
27620
27637
  if (!calendarHoldsTeam(cal)) return false;
27621
27638
  return cal.requiresStaff === true || STAFF_CALENDAR_TYPES.has(cal.calendarType);
@@ -28611,7 +28628,7 @@ function resolvePlan(plan, existing, opts = {}) {
28611
28628
  for (const cal of plan.calendars ?? []) {
28612
28629
  const existingCal = (existing.calendars ?? []).find((c) => norm2(c.name) === norm2(cal.name));
28613
28630
  if (existingCal) {
28614
- const reason3 = calendarSlotMismatch(cal, existingCal);
28631
+ const reason3 = [calendarSlotMismatch(cal, existingCal), calendarHoursMismatch(cal, existingCal)].filter(Boolean).join(" ") || void 0;
28615
28632
  if (reason3) calendarMismatch.push({ ref: cal.ref, name: cal.name, existingId: existingCal.id, reason: reason3 });
28616
28633
  const named = calendarTeamMemberRefs(cal).map((r) => ({ id: idMap.get(r) ?? "", name: idx.userName.get(r) ?? r }));
28617
28634
  const claimable = named.filter((m) => m.id && m.id !== PENDING_USER_REF);
@@ -29839,6 +29856,26 @@ var init_claude_cli = __esm({
29839
29856
  }
29840
29857
  });
29841
29858
 
29859
+ // src/brief-issues.ts
29860
+ function isCleanCheckReport(text2) {
29861
+ const t = text2.toLowerCase().replace(/[`*_]/g, " ").replace(/\s+/g, " ").trim().replace(/^(?:warning|warn|note|info)\s*:\s*/, "");
29862
+ if (!t || /^(?:none|no issues|no gaps|no warnings|nothing to report|n\/a)\.?$/.test(t)) return true;
29863
+ if (/^(?:the )?(?:brief|validate brief|validation|check)(?: check)? (?:is valid|was valid|passed|passes|found (?:no|nothing)(?: gaps| issues| problems)?)(?: validation)?\.?$/.test(t)) return true;
29864
+ if (/\b(?:but|however|except|although|missing|lacks?|needs?|should|invalid|failed|empty|blank|unknown|not (?:set|given|provided))\b|valid\s*[:=]?\s*false/.test(t)) return false;
29865
+ const counts = [...t.matchAll(/\b(\d+|no|zero)\s+(?:errors?|warnings?|gaps?|issues?|problems?)\b/g)];
29866
+ const saysClean = /\bvalid\s*[:=]?\s*true\b|\bis valid\b|\bpassed\b|\bclean\b/.test(t) || counts.length > 0;
29867
+ return saysClean && counts.length > 0 && counts.every((c) => c[1] === "0" || c[1] === "no" || c[1] === "zero");
29868
+ }
29869
+ function briefIssuesShown(issues) {
29870
+ if (!issues) return issues;
29871
+ return issues.map((x) => String(x).replace(/^\s*(?:warning|warn)\s*:\s*/i, "").trim()).filter((x) => x && !isCleanCheckReport(x)).map((x) => x.charAt(0).toUpperCase() + x.slice(1));
29872
+ }
29873
+ var init_brief_issues = __esm({
29874
+ "src/brief-issues.ts"() {
29875
+ "use strict";
29876
+ }
29877
+ });
29878
+
29842
29879
  // src/client-record.ts
29843
29880
  function clientRecordKey(locationId2, clientKey) {
29844
29881
  return clientKey ? `${locationId2}${CLIENT_RECORD_SEPARATOR}${clientKey}` : locationId2;
@@ -29900,7 +29937,11 @@ function buildPermission(locationId2, opts = {}) {
29900
29937
  reason: `${opts.name || locationId2} is not approved for building yet. Approve it on the card first \u2014 that is the deliberate step that lets automation write into a real client account.`
29901
29938
  };
29902
29939
  }
29903
- function buildStageReusePrompt(locationId2, locationName, saved) {
29940
+ function buildFactsRule(ctx) {
29941
+ const staff = (ctx?.verifiedStaff ?? []).filter((n) => n.trim());
29942
+ return `Say who a person is only from get_users or the build reply's staff list, and never judge anyone by their name: a name such as "Test Provider" or "Front Desk" is a real person when the account lists them. Never call a staff member a placeholder, a test user or not a real person. ` + (staff.length ? `The cockpit's staff check matched these people in THIS account by email before the build: ${staff.join(", ")}. They are real staff members. ` : "") + `Say a custom value is empty only when the build reply's customValues.empty lists it, and name exactly those; a hand-off's instruction lists every value it covers, filled or not, so it is never evidence that one is empty. Say a calendar keeps template hours only when the build reply's composed warnings say so.`;
29943
+ }
29944
+ function buildStageReusePrompt(locationId2, locationName, saved, ctx) {
29904
29945
  return `You are running ONE unattended step of a GHL Command Blueprint build. Use ONLY the ghl MCP tools.
29905
29946
  AN APPROVED BUILD PLAN IS ALREADY ON FILE for this account (plan ${saved.planId ?? "(no id)"}, saved ${saved.savedAt}). This run REUSES it. Do NOT write, rewrite, improve or re-derive a plan, and do NOT rebuild the Brief.
29906
29947
  What the saved plan says should exist:
@@ -29911,6 +29952,7 @@ ${saved.summary}
29911
29952
  4. Read back what landed (get_pipelines, list_workflows_full, get_calendars) and compare it, name for name, with the saved plan above. ${BUILD_REPLY_RULE}
29912
29953
  5. ${STAFF_NOTIFICATION_RULE} A notification step whose recipient is empty is listed in issues as waiting for a staff member, never left unmentioned.
29913
29954
  6. ${MISSING_STEP_RULE} ${STAFF_PAUSE_RULE}
29955
+ 7. ${buildFactsRule(ctx)}
29914
29956
  If the tool reports that no saved plan exists, STOP and finish with RESULT: {"ok":false,"error":"no saved plan on this machine \u2014 run the build stage fresh"}.
29915
29957
  If apply_build_plan answers phase:"scope" (nothing is selected for this client, or the selection changed after the plan was saved), STOP: build nothing else and finish with RESULT: {"ok":false,"error":"<its error, word for word>"}.
29916
29958
  ${NO_INVENTED_CAUSE_RULE}
@@ -30103,6 +30145,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
30103
30145
  } catch {
30104
30146
  }
30105
30147
  }
30148
+ if (stage === 1 && outcome.issues) outcome.issues = briefIssuesShown(outcome.issues);
30106
30149
  const detail = outcome.error || (outcome.issues?.length ? outcome.issues.join(" \xB7 ") : "") || outcome.summary || "no detail reported";
30107
30150
  const success = outcome.formId ? `Done: form "${outcome.formName}" (${outcome.formId})` : `Done: ${outcome.summary ?? "stage complete"}`;
30108
30151
  onEvent({ kind: "result", line: outcome.ok ? success : `Not passed: ${detail}` });
@@ -30122,7 +30165,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
30122
30165
  });
30123
30166
  });
30124
30167
  }
30125
- var import_child_process3, fs21, STAFF_NOTIFICATION_RULE, STAFF_PAUSE_RULE, BUILD_REPLY_RULE, MISSING_STEP_RULE, NO_INVENTED_CAUSE_RULE, STAGE_SPECS, HUMAN_GATE_STAGES, RESULT_LAST_RULE;
30168
+ var import_child_process3, fs21, BRIEF_NEEDS_INTAKE, STAFF_NOTIFICATION_RULE, STAFF_PAUSE_RULE, BUILD_REPLY_RULE, MISSING_STEP_RULE, NO_INVENTED_CAUSE_RULE, STAGE_SPECS, HUMAN_GATE_STAGES, RESULT_LAST_RULE;
30126
30169
  var init_stage_runner = __esm({
30127
30170
  "src/stage-runner.ts"() {
30128
30171
  "use strict";
@@ -30138,13 +30181,15 @@ var init_stage_runner = __esm({
30138
30181
  init_claude_cli();
30139
30182
  init_claude_locate();
30140
30183
  init_result_text();
30184
+ init_brief_issues();
30141
30185
  init_account_rules();
30142
30186
  init_account_rules();
30143
30187
  init_client_record();
30144
30188
  init_workflow_builder_client();
30189
+ BRIEF_NEEDS_INTAKE = "First complete the intake. It fills most answers from a call or your notes.";
30145
30190
  STAFF_NOTIFICATION_RULE = `Staff notifications ALWAYS stay in the workflow. Call get_users on THIS account and use only a user id it returns as the recipient. If get_users returns no users, leave the recipient EMPTY ("") \u2014 never use a user id from another account, from the agency, from an earlier conversation or from memory (a foreign id looks like it worked and notifies nobody). An empty recipient can make publish fail: then keep the workflow as a draft with the notification step still in it, and list that step in the RESULT issues as "waiting for a staff member: add one in ${GHL_ADD_STAFF_CLICK_PATH}, then pick them on the notification step and publish".`;
30146
30191
  STAFF_PAUSE_RULE = `If apply_build_plan answers with staffPause, copy its reason into issues word for word and do not call those calendars built.`;
30147
- BUILD_REPLY_RULE = `A long apply_build_plan reply comes back shortened, with replyTrimmed on it; it still holds everything your report needs (summary, missingSteps, workflowsNotBuilt, staffPause, calendars, countsByType, notClean, templates, nextManualSteps and, on a dry run, composed.copySlots), so report from it. If you need a section it lists under sectionsNotShownInFull, read it with apply_build_plan detail:{of:"dry_run" or "execute",section:"<name>"}; that call builds nothing.`;
30192
+ BUILD_REPLY_RULE = `A long apply_build_plan reply comes back shortened, with replyTrimmed on it; it still holds everything your report needs (summary, missingSteps, workflowsNotBuilt, staffPause, calendars, countsByType, notClean, templates, nextManualSteps, customValues and, on a dry run, composed.copySlots), so report from it. If you need a section it lists under sectionsNotShownInFull, read it with apply_build_plan detail:{of:"dry_run" or "execute",section:"<name>"}; that call builds nothing.`;
30148
30193
  MISSING_STEP_RULE = `A step the plan asks for that is not in the account is NEVER dropped from your report. If apply_build_plan answers with a non-empty missingSteps, put EVERY one of those steps in issues, naming the workflow and the step, and say the build is not finished. A later run reporting "refreshed in place" does not close a gap an earlier run reported \u2014 only the step actually being in the workflow does. Never call a build complete while missingSteps has anything in it. If apply_build_plan answers with a non-empty workflowsNotBuilt, put every one of those workflows in issues with its reason, and never call the build complete while workflowsNotBuilt has anything in it.`;
30149
30194
  NO_INVENTED_CAUSE_RULE = `NEVER invent a reason a tool failed, and never state an HTTP status code a tool did not actually return. When a tool answers with an explanation of its own \u2014 an "errorKind", a "next", or a plain-English reason \u2014 quote THAT, verbatim, as the cause, and stop there. Do not add your own diagnosis on top of it, and do not guess at credentials, scopes or permissions: those guesses read as fact to the person acting on them and send them days in the wrong direction. If a tool failed and told you nothing useful, say exactly that \u2014 "it failed and did not say why" \u2014 rather than filling the gap.`;
30150
30195
  STAGE_SPECS = {
@@ -30219,10 +30264,10 @@ If anything fails, finish with: RESULT: {"ok":false,"error":"<short reason>"}`
30219
30264
  2. Find the answers. They arrive one of TWO ways and you must check BOTH:
30220
30265
  (a) a real form submission \u2014 get_forms then get_form_submissions_full; or
30221
30266
  (b) an intake completed in the cockpit \u2014 search_contacts for the tag "blueprint-intake-received" and read that contact's custom fields (get_contact), which hold the answers under "Intake: <question>" fields.
30222
- 3. Turn whichever you found into a Brief with normalize_submission_to_brief (for a form submission), then check it with validate_brief. If the answers came from the tagged contact, build the Brief from those field values directly and call validate_brief on it too. Booking calendars go at the TOP level of the Brief as calendars:[{"name":"...","type":"one_on_one"|"round_robin"|"class","staffNames":["..."],"durationMinutes":30}], one entry per calendar, never inside goal. Report validate_brief gaps in issues as warnings and finish ok:true.
30267
+ 3. Turn whichever you found into a Brief with normalize_submission_to_brief (for a form submission), then check it with validate_brief. If the answers came from the tagged contact, build the Brief from those field values directly and call validate_brief on it too. Booking calendars go at the TOP level of the Brief as calendars:[{"name":"...","type":"one_on_one"|"round_robin"|"class","staffNames":["..."],"durationMinutes":30}], one entry per calendar, never inside goal. Put each real gap validate_brief reports in issues, in plain words, and finish ok:true. When validate_brief finds nothing, issues is [] \u2014 never report the clean result itself as an issue.
30223
30268
  Use these authoritative normalized intake answers when provided: ${JSON.stringify(ctx?.normalizedAnswers ?? {})}.
30224
30269
  Do NOT create, update or delete anything \u2014 this step only reads and normalizes.
30225
- Only if BOTH paths come up empty, finish with RESULT: {"ok":false,"error":"Open the real form, submit it, then refresh."}.
30270
+ Only if BOTH paths come up empty, finish with RESULT: {"ok":false,"error":${JSON.stringify(BRIEF_NEEDS_INTAKE)}}.
30226
30271
  ${NO_INVENTED_CAUSE_RULE}
30227
30272
  Finish with EXACTLY one final line:
30228
30273
  RESULT: {"ok":true,"summary":"<one sentence: who the client is and what they want built>","issues":["<validate_brief gaps, if any>"]}`
@@ -30266,13 +30311,14 @@ RESULT: {"ok":true,"summary":"<one sentence: who the client is and what they wan
30266
30311
  "mcp__ghl__get_contact",
30267
30312
  "mcp__ghl__get_custom_fields"
30268
30313
  ],
30269
- prompt: (locationId2, locationName, ctx) => ctx?.savedPlan ? buildStageReusePrompt(locationId2, locationName, ctx.savedPlan) : `You are running ONE unattended step of a GHL Command Blueprint build. Use ONLY the ghl MCP tools.
30314
+ prompt: (locationId2, locationName, ctx) => ctx?.savedPlan ? buildStageReusePrompt(locationId2, locationName, ctx.savedPlan, ctx) : `You are running ONE unattended step of a GHL Command Blueprint build. Use ONLY the ghl MCP tools.
30270
30315
  THE PLAN IS COMPOSED IN CODE, NOT BY YOU (plan and build were split 2026-08-26 after a run spent 18 of its 25 minutes writing 108 workflow actions by hand). You never author workflows, actions, emails or texts unless step 6's fallback fires.
30271
30316
  1. switch_location to ${locationId2} (${locationName}); verify with get_current_location and stop if it is not that location.
30272
30317
  2. Rebuild the Brief. The answers arrive one of TWO ways and you must check BOTH:
30273
30318
  (a) a real form submission \u2014 get_forms then get_form_submissions_full, then normalize_submission_to_brief; or
30274
30319
  (b) an intake completed in the cockpit \u2014 search_contacts for the tag "blueprint-intake-received", read that contact with get_contact, and assemble the Brief directly from its "Intake: <question>" custom fields (get_custom_fields maps ids to names). apply_build_plan accepts a Brief assembled this way.
30275
30320
  Only if BOTH come up empty, stop and report that there are no intake answers yet \u2014 never tell the operator to fill in the form when path (b) was never checked.
30321
+ Carry the "Business Hours" answer onto the Brief as business.hours, word for word: the build sets each calendar's open hours from it.
30276
30322
  Use these authoritative normalized intake answers when provided: ${JSON.stringify(ctx?.normalizedAnswers ?? {})}.
30277
30323
  3. Call apply_build_plan with fromPreset:true, preset:"${presetForIndustry(ctx?.industry)}", brief:<the Brief from step 2>, mode:"dry_run". The server expands the industry preset with the client's staff, calendars (appointment lengths included), pipeline stages, offer and notification rules in under a second, validates it, and reports what would be built.
30278
30324
  ${BUILD_REPLY_RULE}
@@ -30283,6 +30329,7 @@ Use these authoritative normalized intake answers when provided: ${JSON.stringif
30283
30329
  8. ${STAFF_NOTIFICATION_RULE} In a hand-authored fallback plan, a notification's "to" is that user id, or "" when the account has no users.
30284
30330
  9. ${MISSING_STEP_RULE} ${STAFF_PAUSE_RULE}
30285
30331
  10. ${NO_INVENTED_CAUSE_RULE}
30332
+ 11. ${buildFactsRule(ctx)}
30286
30333
  If apply_build_plan answers phase:"scope" (nothing is confirmed or selected for this client, or the selection changed), STOP: build nothing else and finish with RESULT: {"ok":false,"error":"<its error, word for word>"}.
30287
30334
  Finish with EXACTLY one final line:
30288
30335
  RESULT: {"ok":true,"summary":"<what was created: counts of pipelines/fields/tags/workflows, and whether the plan came from the preset or the fallback>","issues":["<anything skipped or needing a human, including every notification step waiting for a staff member>"]}` + loadPlanGuide(ctx?.industry)
@@ -31589,7 +31636,7 @@ function uniqueVerifyJobs(jobs2, notificationCount, owned = 0) {
31589
31636
  function renderVerifyReport(records, notes) {
31590
31637
  const { rows: rows2, allowPublishing } = verifyReadback(records);
31591
31638
  const normal2 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
31592
- let removed = false;
31639
+ const removedClaims = /* @__PURE__ */ new Set();
31593
31640
  const aliases2 = (r) => [
31594
31641
  r.name,
31595
31642
  r.ref,
@@ -31601,12 +31648,12 @@ function renderVerifyReport(records, notes) {
31601
31648
  ...r.type === "workflow" ? ["all workflows"] : []
31602
31649
  ].map(normal2).filter(Boolean);
31603
31650
  const names = [...new Set(rows2.flatMap(aliases2))].sort((a, b) => b.length - a.length);
31604
- const mentionPattern = names.length ? new RegExp(`\\b(?:${names.join("|")})\\b`, "g") : void 0;
31651
+ const mentionPattern2 = names.length ? new RegExp(`\\b(?:${names.join("|")})\\b`, "g") : void 0;
31605
31652
  const action = /^(?:please )?(?:assign|add|open|connect|register|confirm|review|check|select|choose|set|finish|complete|build|create|publish|get|enter|update|remove|fix|configure|test|retry|run|use|see|inspect|verify|invite|schedule|book|enable|disable|save|send|provide|upload|download|link|map|edit|change|replace|resolve|contact|ask|install|purchase|obtain|reconnect|authorize|grant|authenticate|sign)\b/i;
31606
31653
  const judge = (sentence) => {
31607
31654
  const mentions = [];
31608
31655
  const text2 = normal2(sentence.replace(/\b(?:isn't|wasn't|aren't|weren't)\b/gi, "is not").replace(/\b(?:doesn't|don't)\b/gi, "does not").replace(/[;,]/g, " CLAUSEBREAK "));
31609
- const tokenized = mentionPattern ? text2.replace(mentionPattern, (name) => {
31656
+ const tokenized = mentionPattern2 ? text2.replace(mentionPattern2, (name) => {
31610
31657
  mentions.push(rows2.filter((r) => aliases2(r).includes(name)));
31611
31658
  return `OBJECT${mentions.length - 1}`;
31612
31659
  }) : text2;
@@ -31653,7 +31700,7 @@ function renderVerifyReport(records, notes) {
31653
31700
  const filter = (text2) => sentences(text2).filter((sentence) => {
31654
31701
  if (/could not read:|unchecked:/i.test(sentence)) return false;
31655
31702
  if (judge(sentence).contradicted || !allowPublishing && /\b(everything|all (?:planned )?objects|the account)\b.*\b(ready|complete|working|built)\b/i.test(sentence)) {
31656
- removed = true;
31703
+ removedClaims.add(normal2(sentence));
31657
31704
  return false;
31658
31705
  }
31659
31706
  return allowPublishing || !publishing.test(sentence);
@@ -31715,14 +31762,54 @@ function renderVerifyReport(records, notes) {
31715
31762
  const summary = [
31716
31763
  `${records.buildOk === true ? verifyPassedLine(records.memberJobs ?? rows2.filter((r) => r.status === "pending").length + issues.length) : `${present} of ${rows2.length} planned items are in the account; ${attention} need attention.`}${drafts ? `
31717
31764
  ${drafts}` : ""}${liveLine ? `
31718
- ${liveLine}` : ""}${removed ? "\nRemoved a claim the read-back contradicts." : ""}`,
31765
+ ${liveLine}` : ""}`,
31719
31766
  ["What is not right", ...diagnosis.length ? diagnosis : ["No planned items need attention."]].join("\n"),
31720
31767
  ["Every planned item", ...[...grouped].flatMap(([type, items]) => [typeLabels[type], ...items.map((r) => ` ${r.line}`)])].join("\n"),
31721
- ...summaryNotes.length ? [["Notes from the check", ...summaryNotes].join("\n")] : []
31768
+ // Test 01 (BUG-2026-09-30-36): the left-out note is a quiet line in the notes, after the lists, not in the headline.
31769
+ ...summaryNotes.length || removedClaims.size ? [["Notes from the check", ...summaryNotes, ...removedClaims.size ? [removedClaimNote(removedClaims.size)] : []].join("\n")] : []
31722
31770
  ].join("\n\n");
31723
- return { summary, issues };
31771
+ const cvNames = customValueDisplayNames(rows2, records.deps);
31772
+ return { summary: plainCustomValueNames(summary, cvNames), issues: issues.map((i) => plainCustomValueNames(i, cvNames)) };
31773
+ }
31774
+ function removedClaimNote(count2) {
31775
+ return `${count2 === 1 ? "One check note did not match the account and is not shown" : `${count2} check notes did not match the account and are not shown`}. The lists above are what the account holds.`;
31776
+ }
31777
+ function quietRemovedClaimLine(summary) {
31778
+ if (!summary) return summary;
31779
+ const blocks = summary.split("\n\n");
31780
+ const head4 = blocks[0].split("\n");
31781
+ const old = /^(One thing the check reported was|(\d+) things the check reported were) left out\b/;
31782
+ const at = head4.findIndex((l) => old.test(l.trim()));
31783
+ if (at < 0) return summary;
31784
+ const m = old.exec(head4[at].trim());
31785
+ const note = removedClaimNote(m[2] ? Number(m[2]) : 1);
31786
+ head4.splice(at, 1);
31787
+ blocks[0] = head4.join("\n");
31788
+ const notesAt = blocks.findIndex((b) => b.startsWith("Notes from the check"));
31789
+ if (notesAt >= 0) blocks[notesAt] = `${blocks[notesAt]}
31790
+ ${note}`;
31791
+ else blocks.push(`Notes from the check
31792
+ ${note}`);
31793
+ return blocks.join("\n\n");
31794
+ }
31795
+ function customValueDisplayName(key) {
31796
+ const words2 = key.trim().replace(/^cv\./, "").replace(/[_\s]+/g, " ").trim();
31797
+ return words2 ? words2.charAt(0).toUpperCase() + words2.slice(1) : key;
31798
+ }
31799
+ function customValueDisplayNames(rows2, deps = []) {
31800
+ const keys = /* @__PURE__ */ new Set();
31801
+ for (const r of [...rows2, ...deps]) {
31802
+ if (typeof r.ref === "string" && r.ref.startsWith("cv.")) keys.add(r.ref.slice(3));
31803
+ if (r.type === "custom_value" && typeof r.name === "string") keys.add(r.name.trim());
31804
+ }
31805
+ return new Map([...keys].filter((k) => /^[a-z0-9]+(?:_[a-z0-9]+)+$/.test(k)).map((k) => [k, customValueDisplayName(k)]));
31806
+ }
31807
+ function plainCustomValueNames(text2, names) {
31808
+ if (!names.size) return text2;
31809
+ const keys = [...names.keys()].sort((a, b) => b.length - a.length);
31810
+ return text2.replace(new RegExp(`(?<![\\w.{(])(?:${keys.join("|")})(?![\\w})])`, "g"), (k) => names.has(k) ? `${names.get(k)} (${k})` : k);
31724
31811
  }
31725
- var readLabels, readLabel, BUILDER_SIGNIN_BODY, BUILDER_SIGNIN_SPANS, BUILDER_SIGNIN_TOKENS, BUSY_SIGNIN_LINE, BUSY_MESSAGE, cleanVerifyName, cleanVerifyText, sentences, publishing, detailClauses, oneSentence, oneSentenceOf, expectedDraft, typeLabels, firstFive, verifyTypeOrder, verifyGroup, jobText, countWord, normalJob;
31812
+ var readLabels, readLabel, BUILDER_SIGNIN_BODY, BUILDER_SIGNIN_SPANS, BUILDER_SIGNIN_TOKENS, BUSY_SIGNIN_LINE, BUSY_MESSAGE, cleanVerifyName, cleanVerifyText, sentences, publishing, detailClauses, oneSentence, oneSentenceOf, expectedDraft, typeLabels, firstFive, verifyTypeOrder, verifyGroup, jobText, countWord, normalJob, REMOVED_CLAIM_LINE;
31726
31813
  var init_member_progress = __esm({
31727
31814
  "src/member-progress.ts"() {
31728
31815
  "use strict";
@@ -31765,6 +31852,7 @@ var init_member_progress = __esm({
31765
31852
  jobText = (text2) => text2.trim().replace(/\s+/g, " ").replace(/([^.!?])$/, "$1.");
31766
31853
  countWord = (text2) => /^\d+$/.test(text2) ? Number(text2) : ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"].indexOf(text2.toLowerCase());
31767
31854
  normalJob = (text2) => text2.toLowerCase().replace(/^manual action:\s*/, "").replace(/[^a-z0-9]+/g, " ").trim();
31855
+ REMOVED_CLAIM_LINE = /^(?:One check note did not match|\d+ check notes did not match) the account\b|^(?:One thing the check reported was|\d+ things the check reported were) left out\b|^Removed a claim\b/i;
31768
31856
  }
31769
31857
  });
31770
31858
 
@@ -34015,7 +34103,7 @@ var require_package = __commonJS({
34015
34103
  "package.json"(exports2, module2) {
34016
34104
  module2.exports = {
34017
34105
  name: "@elitedcs/ghl-mcp",
34018
- version: "3.89.0-beta.5",
34106
+ version: "3.89.0-beta.7",
34019
34107
  mcpName: "io.github.drjerryrelth/ghl-command",
34020
34108
  description: "GoHighLevel MCP Server for Claude. 250 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
34021
34109
  main: "dist/index.js",
@@ -34248,7 +34336,7 @@ ${cockpitSidebarHtml(active, opts.capability)}
34248
34336
  <a class="cds-menu-item" role="menuitem" href="/connections">Connections</a>
34249
34337
  <a class="cds-menu-item" role="menuitem" href="${HUB_URL}#do">Members Hub</a>
34250
34338
  <div class="cds-menu-sep" role="separator"></div>
34251
- <div class="cds-menu-row"><span>Appearance</span><div class="cds-seg" role="group" aria-label="Colour theme"><button type="button" data-theme-choice="auto" aria-pressed="true">Auto</button><button type="button" data-theme-choice="light" aria-pressed="false">Light</button><button type="button" data-theme-choice="dark" aria-pressed="false">Dark</button></div></div>
34339
+ <div class="cds-menu-row"><span>Appearance</span><div class="cds-seg" role="group" aria-label="Color theme"><button type="button" data-theme-choice="auto" aria-pressed="true">Auto</button><button type="button" data-theme-choice="light" aria-pressed="false">Light</button><button type="button" data-theme-choice="dark" aria-pressed="false">Dark</button></div></div>
34252
34340
  </div>
34253
34341
  </details>
34254
34342
  </div>
@@ -36440,7 +36528,7 @@ function reportDocumentHtml(run, card, opts) {
36440
36528
  const failed = stepProblems.filter((p) => p.kind === "failed_holding" || p.kind === "failed_gave_up").length;
36441
36529
  const stepsKnown = !!st?.read && allSteps.length > 0;
36442
36530
  const period = `${localTime2(run.startedAt)}${run.finishedAt ? ` to ${localTime2(run.finishedAt)}` : ""}`;
36443
- const clock2 = (ms) => new Date(ms).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
36531
+ const clock3 = (ms) => new Date(ms).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
36444
36532
  const tiles = [
36445
36533
  stepsKnown ? statTile2(judged, "Steps checked") : statTile2("\u2013", "Steps checked", "warn"),
36446
36534
  statTile2(f.sent, "Messages sent"),
@@ -36452,7 +36540,7 @@ function reportDocumentHtml(run, card, opts) {
36452
36540
  ].join("");
36453
36541
  const worst = f.problems[0];
36454
36542
  const sentAt = worst ? f.timeline.find((t) => t.messageId === worst.messageId) : void 0;
36455
- const quote = worst ? callout2(`A new lead got "${marked2(worst.quote, "")}"`, { quote: true, tone: "bad", cite: `${worst.sentBy ?? worst.title}${sentAt ? `, sent ${clock2(run.startedAt + sentAt.minute * 6e4)}` : ""}${worst.kind === "blank_field" && !run.leadHasFirstName ? " to a lead with no first name on file" : ""}` }) : stepProblems[0] ? callout2(`"${esc4(stepProblems[0].stepName)}" ${stepProblems[0].kind === "did_nothing" ? "did nothing for the test lead, and the workflow carried on as if it had worked" : stepProblems[0].kind.startsWith("failed") ? "failed" : "was skipped"}.`, { quote: true, tone: "bad", cite: stepProblems[0].workflowName }) : run.draftWorkflows?.length ? callout2(`A chosen workflow is still a draft, so GoHighLevel took the test lead and ran none of its steps.`, { quote: true, tone: "bad", cite: run.draftWorkflows.join(", ") }) : callout2(f.sent ? "Nothing a customer would notice: every message that arrived read correctly." : "No message arrived during the check.", { quote: true, tone: f.sent ? "info" : "warn" });
36543
+ const quote = worst ? callout2(`A new lead got "${marked2(worst.quote, "")}"`, { quote: true, tone: "bad", cite: `${worst.sentBy ?? worst.title}${sentAt ? `, sent ${clock3(run.startedAt + sentAt.minute * 6e4)}` : ""}${worst.kind === "blank_field" && !run.leadHasFirstName ? " to a lead with no first name on file" : ""}` }) : stepProblems[0] ? callout2(`"${esc4(stepProblems[0].stepName)}" ${stepProblems[0].kind === "did_nothing" ? "did nothing for the test lead, and the workflow carried on as if it had worked" : stepProblems[0].kind.startsWith("failed") ? "failed" : "was skipped"}.`, { quote: true, tone: "bad", cite: stepProblems[0].workflowName }) : run.draftWorkflows?.length ? callout2(`A chosen workflow is still a draft, so GoHighLevel took the test lead and ran none of its steps.`, { quote: true, tone: "bad", cite: run.draftWorkflows.join(", ") }) : callout2(f.sent ? "Nothing a customer would notice: every message that arrived read correctly." : "No message arrived during the check.", { quote: true, tone: f.sent ? "info" : "warn" });
36456
36544
  const stepTitle = (p) => POINTS[p.kind].what;
36457
36545
  const stepCard = (p, controls) => fixCard(stepTitle(p), POINTS[p.kind].points, p.detail, esc4(`"${p.stepName}" in ${p.workflowName}`), p.fix, controls);
36458
36546
  const DRAFT_WHY = "Publishing starts sending to every real lead, so publish it yourself in GoHighLevel when it is ready.";
@@ -37425,9 +37513,9 @@ async function previewUndo(deps, run, key) {
37425
37513
  return view(p);
37426
37514
  }
37427
37515
  async function approveFix(deps, run, hash, confirmedWording) {
37428
- const clock2 = deps.now ?? Date.now;
37516
+ const clock3 = deps.now ?? Date.now;
37429
37517
  const p = previews.get(hash);
37430
- if (!p || p.expires < clock2()) {
37518
+ if (!p || p.expires < clock3()) {
37431
37519
  previews.delete(hash);
37432
37520
  return { ok: false, written: false, error: "This preview has expired or was already used. Nothing was changed. Show the change again." };
37433
37521
  }
@@ -37459,7 +37547,7 @@ async function approveFix(deps, run, hash, confirmedWording) {
37459
37547
  workflowName: p.workflowName,
37460
37548
  stepName: p.stepName,
37461
37549
  previewHash: p.hash,
37462
- at: clock2(),
37550
+ at: clock3(),
37463
37551
  state: "writing",
37464
37552
  prior: current,
37465
37553
  written: p.after,
@@ -37515,7 +37603,7 @@ async function approveFix(deps, run, hash, confirmedWording) {
37515
37603
  const verdict = verifyReadBack(p.after, back, p.added, p.checks);
37516
37604
  const detail = verdict.held ? p.undo ? "Read back from GoHighLevel: the step is back the way it was." : "Read back from GoHighLevel: the change held." : `${p.undo ? "The undo was written" : "Written"}, but reading it back did not match: ${verdict.problems.join("; ")}. ${NO_AUTO_ROLLBACK}`;
37517
37605
  putRecord(run.locationId, run.runId, { ...rec2, state: "applied", held: verdict.held, detail, afterHash: templatesHash(back) }, deps.base);
37518
- if (undone && verdict.held) putRecord(run.locationId, run.runId, { ...undone, undoneAt: clock2(), undoneBy: rec2.id }, deps.base);
37606
+ if (undone && verdict.held) putRecord(run.locationId, run.runId, { ...undone, undoneAt: clock3(), undoneBy: rec2.id }, deps.base);
37519
37607
  return { ok: true, held: verdict.held, detail, undo: p.undo };
37520
37608
  } finally {
37521
37609
  writing.delete(p.workflowId);
@@ -38072,7 +38160,7 @@ function createBriefRedactor(secrets) {
38072
38160
  const matcher = new RegExp(`(?=(${pattern}))`, "g");
38073
38161
  return (text2) => {
38074
38162
  matcher.lastIndex = 0;
38075
- const pieces = [];
38163
+ const pieces2 = [];
38076
38164
  let copiedUntil = 0;
38077
38165
  let start = -1;
38078
38166
  let end = 0;
@@ -38086,15 +38174,15 @@ function createBriefRedactor(secrets) {
38086
38174
  end = nextEnd;
38087
38175
  } else if (nextStart <= end) end = Math.max(end, nextEnd);
38088
38176
  else {
38089
- pieces.push(text2.slice(copiedUntil, start), marker);
38177
+ pieces2.push(text2.slice(copiedUntil, start), marker);
38090
38178
  copiedUntil = end;
38091
38179
  start = nextStart;
38092
38180
  end = nextEnd;
38093
38181
  }
38094
38182
  }
38095
38183
  if (start < 0) return text2;
38096
- pieces.push(text2.slice(copiedUntil, start), marker, text2.slice(end));
38097
- return pieces.join("");
38184
+ pieces2.push(text2.slice(copiedUntil, start), marker, text2.slice(end));
38185
+ return pieces2.join("");
38098
38186
  };
38099
38187
  }
38100
38188
  var init_brief_redaction = __esm({
@@ -38847,7 +38935,7 @@ function clientTitle(name) {
38847
38935
  return s.charAt(0).toUpperCase() + s.slice(1);
38848
38936
  }
38849
38937
  function clientCheckHeadline(summary) {
38850
- return summary.split(/\n\s*\n/)[0].split("\n").filter((line3) => line3.trim() && !/^Removed a claim/i.test(line3.trim())).join(" ").trim();
38938
+ return summary.split(/\n\s*\n/)[0].split("\n").filter((line3) => line3.trim() && !REMOVED_CLAIM_LINE.test(line3.trim())).join(" ").trim();
38851
38939
  }
38852
38940
  function scrubLatestCheck(raw) {
38853
38941
  if (!raw) return void 0;
@@ -39176,6 +39264,7 @@ var init_client_status = __esm({
39176
39264
  init_handoff_pack();
39177
39265
  init_branding();
39178
39266
  init_svg_sanitize();
39267
+ init_member_progress();
39179
39268
  CLIENT_STATE_LABEL = {
39180
39269
  "in-place": "Live in your account",
39181
39270
  "not-yet": "Not confirmed yet",
@@ -40967,12 +41056,12 @@ function splitList(text2, opts = {}) {
40967
41056
  }
40968
41057
  function parsePricePoints(text2) {
40969
41058
  const out = [];
40970
- const segments = text2.split(/\r?\n|;|\.\s+/);
41059
+ const segments2 = text2.split(/\r?\n|;|\.\s+/);
40971
41060
  const ENTRY = /^(.*?)\s*[-–—:|]?\s*\$?\s*([\d,]+(?:\.\d+)?)\s*(?:[-–—]\s*\$?\s*([\d,]+(?:\.\d+)?))?\s*(\/\s*[a-z]+|(?:per|an?|each)\s+[a-z]+)?\s*$/i;
40972
41061
  const clean4 = (s) => s.replace(/[\s.,:;|–—-]+$/, "").trim();
40973
41062
  const num3 = (s) => s.replace(/,/g, "");
40974
41063
  const recur = (s) => s ? "/" + s.replace(/^\/|^(?:per|an?|each)\b/i, "").trim().toLowerCase() : "";
40975
- for (const segment of segments) {
41064
+ for (const segment of segments2) {
40976
41065
  const t = segment.trim().replace(/\.$/, "");
40977
41066
  if (!t) continue;
40978
41067
  const m = t.match(ENTRY);
@@ -41006,8 +41095,8 @@ function staffFromLooseLine(line3) {
41006
41095
  const after = line3.slice(m.index + m[0].length).replace(/^\s*[)>\]]+/, "").replace(LIGHT_EDGE, "");
41007
41096
  let mobile;
41008
41097
  const roleParts = [];
41009
- const pieces = after.split(/\s+[-–—|]\s+|[,;|]/).map((x) => x.replace(LIGHT_EDGE, "").replace(/^[(\[<](.*)[)\]>]$/, "$1").trim()).filter(Boolean);
41010
- for (const piece of pieces) {
41098
+ const pieces2 = after.split(/\s+[-–—|]\s+|[,;|]/).map((x) => x.replace(LIGHT_EDGE, "").replace(/^[(\[<](.*)[)\]>]$/, "$1").trim()).filter(Boolean);
41099
+ for (const piece of pieces2) {
41011
41100
  const bare = piece.replace(/^(mobile|phone|cell|role)\s*[:=]\s*/i, "");
41012
41101
  if (!mobile && PHONE_RE.test(bare) && /\d{7,}/.test(bare.replace(/\D/g, ""))) mobile = bare;
41013
41102
  else if (!new RegExp(EMAIL_ANYWHERE.source).test(bare)) roleParts.push(bare);
@@ -41021,8 +41110,8 @@ function parseStaffLines(text2) {
41021
41110
  const emailsIn = (t) => (t.match(EMAIL_ANYWHERE) ?? []).length;
41022
41111
  const lines = cleanLines(text2).flatMap((line3) => {
41023
41112
  if (!line3.includes(";") || emailsIn(line3) < 2) return [line3];
41024
- const pieces = line3.split(";").map((p) => p.trim()).filter(Boolean);
41025
- return pieces.every((p) => emailsIn(p) === 1) ? pieces : [line3];
41113
+ const pieces2 = line3.split(";").map((p) => p.trim()).filter(Boolean);
41114
+ return pieces2.every((p) => emailsIn(p) === 1) ? pieces2 : [line3];
41026
41115
  });
41027
41116
  lines.forEach((line3, i) => {
41028
41117
  if (emailsIn(line3) > 1) {
@@ -43816,7 +43905,7 @@ async function clientPreflight(registry2, locationId2, deps = {}) {
43816
43905
  }
43817
43906
  lines.push({
43818
43907
  key: "authentication",
43819
- ok: authenticated,
43908
+ ok: authenticated || !key,
43820
43909
  text: authenticated ? "Your Anthropic key authenticates, so powered runs will bill it." : key ? "The saved Anthropic key did not authenticate, so powered runs will fall back to your Claude sign-in on this computer. Replace it in Your agency, or remove it." : "No Anthropic key saved. Powered runs use your Claude sign-in on this computer and cost you nothing extra."
43821
43910
  });
43822
43911
  const blocking = lines.filter((l) => l.key !== "authentication" && !l.advisory);
@@ -44321,7 +44410,7 @@ var init_customization = __esm({
44321
44410
  q({ key: "capacity_limited", label: "Do trial spots really run out most weeks?", section: "offer", sectionLabel: "Offer and pricing", dataType: "SINGLE_OPTIONS", options: ["Yes", "No"], briefPath: "offer.capacityLimited", briefKind: "boolean" }),
44322
44411
  q({ key: "programs", label: "Programs you sell after the trial, with prices", section: "offer", sectionLabel: "Offer and pricing", dataType: "LARGE_TEXT", placeholder: "One per line, e.g. Signature 12-session program $2,997", briefPath: "offer.programs", briefKind: "pricePoints" }),
44323
44412
  q({ key: "contraindications", label: "Who should not book (your screening list)", section: "offer", sectionLabel: "Offer and pricing", dataType: "LARGE_TEXT", placeholder: "e.g. pregnant or breastfeeding, cancer in the last 12 months, pacemaker", briefPath: "offer.contraindications" }),
44324
- q({ key: "prep_instructions", label: "How should clients prepare for the visit?", section: "offer", sectionLabel: "Offer and pricing", dataType: "LARGE_TEXT", placeholder: "e.g. Please arrive 10 minutes early and wear loose clothing. One or two sentences. This replaces the day-before reminder's usual 'come 10 minutes early' line, so include arriving early if you want it said.", briefPath: "offer.prepInstructions" }),
44413
+ q({ key: "prep_instructions", label: "How should clients prepare for the visit?", section: "offer", sectionLabel: "Offer and pricing", dataType: "LARGE_TEXT", placeholder: "e.g. Please arrive 10 minutes early and wear loose clothing. One or two sentences. They go in the day-before reminder in place of its usual line, 'Please come about 10 minutes early', so include arriving early if you want the reminder to say it. The booking confirmation already asks them to come 10 minutes early.", briefPath: "offer.prepInstructions" }),
44325
44414
  q({ key: "anchor_price", label: "What a comparable treatment usually costs elsewhere (optional)", section: "offer", sectionLabel: "Offer and pricing", dataType: "TEXT", placeholder: "e.g. $300 to $500. Leave blank if you are not sure.", briefPath: "offer.anchorPrice" }),
44326
44415
  q({ key: "visit_only_price", label: "Do you offer a program price that is only good for 72 hours after the trial visit?", section: "offer", sectionLabel: "Offer and pricing", dataType: "SINGLE_OPTIONS", options: ["Yes", "No"], briefPath: "offer.visitOnlyPrice", briefKind: "boolean" }),
44327
44416
  q({ key: "testimonial_quote", label: "One short client quote you may use in texts (optional, no name, no health or weight claims)", section: "assets", sectionLabel: "Assets on hand", dataType: "LARGE_TEXT", placeholder: "Leave blank if you have none", briefPath: "proof.testimonialQuote" }),
@@ -44701,6 +44790,159 @@ var init_testimonial_guard = __esm({
44701
44790
  }
44702
44791
  });
44703
44792
 
44793
+ // src/intake-to-build/business-hours.ts
44794
+ function dayIndex(word) {
44795
+ const w = word.toLowerCase().replace(/\.$/, "");
44796
+ for (const [re, n] of DAY_WORDS) if (re.test(w)) return n;
44797
+ return null;
44798
+ }
44799
+ function parseDays(spec) {
44800
+ const s = spec.toLowerCase().trim();
44801
+ if (GROUP_WORDS[s]) return [...GROUP_WORDS[s]];
44802
+ const days = [];
44803
+ for (const part of s.split(/\s*(?:,|&|\/|\band\b)\s*/).filter(Boolean)) {
44804
+ const range = part.split(/\s*(?:-|–|—|\bto\b|\bthrough\b|\bthru\b)\s*/);
44805
+ if (range.length === 1) {
44806
+ const d = dayIndex(range[0]);
44807
+ if (d === null) return null;
44808
+ days.push(d);
44809
+ } else if (range.length === 2) {
44810
+ const a = dayIndex(range[0]), b = dayIndex(range[1]);
44811
+ if (a === null || b === null) return null;
44812
+ for (let d = a; ; d = (d + 1) % 7) {
44813
+ days.push(d);
44814
+ if (d === b) break;
44815
+ }
44816
+ } else return null;
44817
+ }
44818
+ return days.length ? days : null;
44819
+ }
44820
+ function parseClock(text2) {
44821
+ const t = text2.toLowerCase().replace(/\s+/g, "").replace(/\./g, "");
44822
+ if (t === "noon") return { h: 12, m: 0, meridiem: "pm" };
44823
+ const m = /^(\d{1,2})(?::(\d{2}))?(am|pm|a|p)?$/.exec(t);
44824
+ if (!m) return null;
44825
+ const h = Number(m[1]), min = m[2] ? Number(m[2]) : 0;
44826
+ if (h > 23 || min > 59) return null;
44827
+ const mer = m[3] ? m[3].startsWith("a") ? "am" : "pm" : void 0;
44828
+ if (mer && (h < 1 || h > 12)) return null;
44829
+ return { h, m: min, meridiem: mer };
44830
+ }
44831
+ function to24(c) {
44832
+ if (c.meridiem === "am") return c.h === 12 ? 0 : c.h;
44833
+ if (c.meridiem === "pm") return c.h === 12 ? 12 : c.h + 12;
44834
+ return c.h;
44835
+ }
44836
+ function parseWindow(text2) {
44837
+ const parts = text2.split(/\s*(?:-|–|—|\bto\b|\buntil\b|\btill\b)\s*/i).filter((p) => p.trim());
44838
+ if (parts.length !== 2) return null;
44839
+ const open2 = parseClock(parts[0]), close = parseClock(parts[1]);
44840
+ if (!open2 || !close) return null;
44841
+ let openH;
44842
+ if (open2.meridiem) openH = to24(open2);
44843
+ else if (open2.h >= 13) openH = open2.h;
44844
+ else if (!close.meridiem && close.h >= 13) openH = open2.h;
44845
+ else if (!close.meridiem && (open2.h < 7 || open2.h > 12)) return null;
44846
+ else if (open2.h >= 5 && open2.h <= 12) openH = open2.h;
44847
+ else return null;
44848
+ let closeH;
44849
+ if (close.meridiem) closeH = to24(close);
44850
+ else if (close.h >= 13 || close.h === 0) closeH = close.h;
44851
+ else closeH = close.h > openH || close.h === openH && close.m > open2.m ? close.h : close.h + 12;
44852
+ if (!open2.meridiem && open2.h <= 12 && close.meridiem === "pm") {
44853
+ const openPm = open2.h === 12 ? 12 : open2.h + 12;
44854
+ if (openPm !== openH && openPm * 60 + open2.m < closeH * 60 + close.m) return null;
44855
+ }
44856
+ if (!open2.meridiem && !close.meridiem && open2.h <= 12 && closeH * 60 + close.m < 12 * 60) return null;
44857
+ const openMin = openH * 60 + open2.m, closeMin = closeH * 60 + close.m;
44858
+ if (closeH > 23 || closeMin <= openMin) return null;
44859
+ return { openHour: openH, openMinute: open2.m, closeHour: closeH, closeMinute: close.m };
44860
+ }
44861
+ function segments(text2) {
44862
+ const out = [];
44863
+ for (const piece of text2.split(/\s*(?:;|\n|\|)\s*/)) {
44864
+ let current = "";
44865
+ for (const part of piece.split(/\s*,\s*/)) {
44866
+ if (current && /\d|\b(?:closed|off|shut|noon)\b/i.test(current) && /^(?:sun|mon|tue|wed|thu|fri|sat|weekdays|weekends?|daily)/i.test(part)) {
44867
+ out.push(current);
44868
+ current = part;
44869
+ } else current = current ? `${current}, ${part}` : part;
44870
+ }
44871
+ if (current.trim()) out.push(current);
44872
+ }
44873
+ return out.filter((s) => s.trim());
44874
+ }
44875
+ function parseBusinessHours(answer2) {
44876
+ const text2 = String(answer2 ?? "").trim();
44877
+ if (!text2) return null;
44878
+ const byDay = /* @__PURE__ */ new Map();
44879
+ for (const raw of segments(text2)) {
44880
+ const seg = raw.trim().replace(/[.]$/, "");
44881
+ const m = DAYS_SPEC.exec(seg);
44882
+ if (!m) return null;
44883
+ const days = parseDays(m[1]);
44884
+ if (!days) return null;
44885
+ const rest = m[2].trim();
44886
+ const value = /^(?:closed|off|shut)$/i.test(rest) ? "closed" : parseWindow(rest);
44887
+ if (!value) return null;
44888
+ for (const d of days) {
44889
+ if (byDay.has(d)) return null;
44890
+ byDay.set(d, value);
44891
+ }
44892
+ }
44893
+ const open2 = [...byDay.entries()].filter((e) => e[1] !== "closed");
44894
+ if (!open2.length) return null;
44895
+ const order = [1, 2, 3, 4, 5, 6, 0];
44896
+ const blocks = [];
44897
+ for (const d of order) {
44898
+ const w = byDay.get(d);
44899
+ if (!w || w === "closed") continue;
44900
+ const same2 = blocks.find((b) => JSON.stringify(b.hours[0]) === JSON.stringify(w));
44901
+ if (same2) same2.daysOfTheWeek.push(d);
44902
+ else blocks.push({ daysOfTheWeek: [d], hours: [{ ...w }] });
44903
+ }
44904
+ return blocks;
44905
+ }
44906
+ function clock(h, m) {
44907
+ const suffix = h < 12 ? "am" : "pm";
44908
+ const h12 = h % 12 === 0 ? 12 : h % 12;
44909
+ return `${h12}${m ? `:${String(m).padStart(2, "0")}` : ""}${suffix}`;
44910
+ }
44911
+ function describeOpenHours(blocks) {
44912
+ return blocks.map((b) => {
44913
+ const days = [...b.daysOfTheWeek];
44914
+ const consecutive = days.every((d, i) => i === 0 || d === (days[i - 1] + 1) % 7);
44915
+ const dayText = days.length > 2 && consecutive ? `${DAY_NAMES2[days[0]]} to ${DAY_NAMES2[days[days.length - 1]]}` : days.map((d) => DAY_NAMES2[d]).join(days.length === 2 ? " and " : ", ");
44916
+ return `${dayText} ${b.hours.map((w) => `${clock(w.openHour, w.openMinute)} to ${clock(w.closeHour, w.closeMinute)}`).join(" and ")}`;
44917
+ }).join("; ");
44918
+ }
44919
+ var DAY_WORDS, GROUP_WORDS, DAY_TOKEN, DAYS_SPEC, DAY_NAMES2;
44920
+ var init_business_hours = __esm({
44921
+ "src/intake-to-build/business-hours.ts"() {
44922
+ "use strict";
44923
+ DAY_WORDS = [
44924
+ [/^sun(?:day)?s?$/, 0],
44925
+ [/^mon(?:day)?s?$/, 1],
44926
+ [/^tue(?:s(?:day)?)?s?$/, 2],
44927
+ [/^wed(?:s|nesday)?s?$/, 3],
44928
+ [/^thu(?:r(?:s(?:day)?)?)?s?$/, 4],
44929
+ [/^fri(?:day)?s?$/, 5],
44930
+ [/^sat(?:urday)?s?$/, 6]
44931
+ ];
44932
+ GROUP_WORDS = {
44933
+ weekdays: [1, 2, 3, 4, 5],
44934
+ weekends: [0, 6],
44935
+ weekend: [0, 6],
44936
+ daily: [0, 1, 2, 3, 4, 5, 6],
44937
+ "every day": [0, 1, 2, 3, 4, 5, 6],
44938
+ "7 days": [0, 1, 2, 3, 4, 5, 6]
44939
+ };
44940
+ DAY_TOKEN = String.raw`(?:sun|mon|tue|wed|thu|fri|sat)[a-z.]*`;
44941
+ DAYS_SPEC = new RegExp(String.raw`^((?:weekdays|weekends?|daily|every day|7 days)|${DAY_TOKEN}(?:\s*(?:-|–|—|,|&|/|\band\b|\bto\b|\bthrough\b|\bthru\b)\s*${DAY_TOKEN})*)\s*:?\s*(.*)$`, "i");
44942
+ DAY_NAMES2 = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
44943
+ }
44944
+ });
44945
+
44704
44946
  // src/intake-to-build/plan-from-preset.ts
44705
44947
  function toGsm7(s) {
44706
44948
  return GSM7_REPLACEMENTS.reduce((out, [re, to]) => out.replace(re, to), s);
@@ -45212,7 +45454,7 @@ function composeCalendars(skeletonCalendars, ctx) {
45212
45454
  if (kept.length > 0 && ctx.bookingWanted) {
45213
45455
  ctx.warnings.push(`The intake did not describe a booking calendar, so the preset's "${String(kept[0].name)}" calendar is built as-is \u2014 say how many calendars and who is on each to change it.`);
45214
45456
  }
45215
- return kept;
45457
+ return applyIntakeHours(kept, ctx);
45216
45458
  }
45217
45459
  const out = [];
45218
45460
  const usedRefs = /* @__PURE__ */ new Set();
@@ -45253,7 +45495,26 @@ function composeCalendars(skeletonCalendars, ctx) {
45253
45495
  resolveOwnerTeam(kept[i]);
45254
45496
  out.push(kept[i]);
45255
45497
  }
45256
- return out;
45498
+ return applyIntakeHours(out, ctx);
45499
+ }
45500
+ function applyIntakeHours(cals, ctx) {
45501
+ const answer2 = (ctx.brief.business?.hours ?? "").trim();
45502
+ if (!answer2 || cals.length === 0) return cals;
45503
+ const hours = parseBusinessHours(answer2);
45504
+ if (!hours) {
45505
+ const template = cals.find((c) => Array.isArray(c.openHours) && c.openHours.length)?.openHours;
45506
+ ctx.warnings.push(`The intake's business hours ("${answer2}") could not be read one way, so the calendars keep the template hours${template ? ` (${describeOpenHours(template)})` : ""}. Set each calendar's hours in GoHighLevel to match the client's.`);
45507
+ return cals;
45508
+ }
45509
+ for (const cal of cals) {
45510
+ cal.openHours = hours.map((b) => ({ daysOfTheWeek: [...b.daysOfTheWeek], hours: b.hours.map((w) => ({ ...w })) }));
45511
+ if (cal.availabilityType === void 0) cal.availabilityType = 0;
45512
+ }
45513
+ const shared = cals.filter((c) => c.calendarType === "round_robin" || c.calendarType === "collective").map((c) => `"${String(c.name)}"`);
45514
+ if (shared.length) {
45515
+ ctx.warnings.push(`Calendar hours were set from the intake (${describeOpenHours(hours)}). On ${shared.join(" and ")}, GoHighLevel offers a time only when the person assigned also has that time in their own availability, so check each person's availability covers these hours.`);
45516
+ }
45517
+ return cals;
45257
45518
  }
45258
45519
  function resolveActionUsers(action, ctx) {
45259
45520
  if (action.userRef === "user.owner") {
@@ -46043,6 +46304,7 @@ var init_plan_from_preset = __esm({
46043
46304
  init_offer_normalize();
46044
46305
  init_testimonial_guard();
46045
46306
  init_sms_rules();
46307
+ init_business_hours();
46046
46308
  init_plan_guide();
46047
46309
  init_claims_guard();
46048
46310
  PRESET_TOKEN_ROOTS = /* @__PURE__ */ new Set(["business", "offer", "audience", "voice", "goal", "proof"]);
@@ -46758,6 +47020,10 @@ function prefillPrompt(labels, transcript) {
46758
47020
  "Answer ONLY from the material provided. Never invent a fact, a number, or a name.",
46759
47021
  "Every answer must carry a QUOTE: the exact words from the material it comes from, copied verbatim, under 300 characters. An answer with no quote will be thrown away, and a quote that is not in the material will be thrown away.",
46760
47022
  'If the material does not clearly answer a question, leave it out and list it under "unknown".',
47023
+ "When the material STATES a fact, answer it. Do not leave a stated fact out.",
47024
+ 'Quote ONLY the words that give that one answer, not the whole line or paragraph around them. For a number, quote just the words with that one number, for example "$79 total" or "No more than 6 trials a day". For the staff and team size, quote the lines that name the people.',
47025
+ "Copy the material's own words into the value wherever the question allows it, rather than rewording them.",
47026
+ 'Give every answer a "basis": "stated" when the material says it, or "inferred" when the material does not say it and you are working it out from other facts.',
46761
47027
  "Keep answers short and concrete \u2014 this feeds an automated build, not a report.",
46762
47028
  "",
46763
47029
  "QUESTIONS (use these labels verbatim as keys):",
@@ -46767,7 +47033,7 @@ function prefillPrompt(labels, transcript) {
46767
47033
  transcript.slice(0, 6e4),
46768
47034
  "",
46769
47035
  "Reply with ONE line of JSON and nothing else:",
46770
- 'RESULT: {"answers":{"<label>":{"value":"<answer>","quote":"<their exact words>"}},"unknown":["<label>"]}'
47036
+ 'RESULT: {"answers":{"<label>":{"value":"<answer>","quote":"<their exact words>","basis":"stated or inferred"}},"unknown":["<label>"]}'
46771
47037
  ].join("\n");
46772
47038
  }
46773
47039
  function prefillPromptForSource(labels, source) {
@@ -46789,7 +47055,8 @@ async function prefillFromSource(spawnClaude, labels, source, toolContext = { se
46789
47055
  if (empty) return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: "nothing to read" };
46790
47056
  if (source.kind === "url") return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: LINK_READING_OFF_MESSAGE };
46791
47057
  if (source.kind === "text" && source.text.length > 6e4) return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: "The limit is 60,000 characters. Paste an excerpt or shorten this text file." };
46792
- const result = await runPrefill(() => spawnClaude(prefillPromptForSource(labels, source) + "\nQuestion schema (use only these choices; multiple choices use an array in the cited answer object\u2019s value):\n" + JSON.stringify(questions), prefillTools(source, toolContext)), labels, source.kind === "text" ? source.text : void 0);
47058
+ const byLabel = new Map(questions.map((q3) => [q3.label, q3]));
47059
+ const result = await runPrefill(() => spawnClaude(prefillPromptForSource(labels, source) + "\nQuestion schema (use only these choices; multiple choices use an array in the cited answer object\u2019s value):\n" + JSON.stringify(questions), prefillTools(source, toolContext)), labels, source.kind === "text" ? source.text : void 0, byLabel);
46793
47060
  result.answers = validatePrefillAnswers(questions, result.answers);
46794
47061
  for (const [label2, held] of Object.entries(result.held)) {
46795
47062
  const checked = validatePrefillAnswers(questions, { [label2]: held.value });
@@ -46800,7 +47067,87 @@ async function prefillFromSource(spawnClaude, labels, source, toolContext = { se
46800
47067
  result.unknown = labels.filter((label2) => !(label2 in result.answers) && !(label2 in result.held));
46801
47068
  return result;
46802
47069
  }
46803
- async function runPrefill(call2, labels, material) {
47070
+ function numbersIn(text2) {
47071
+ return [...String(text2 ?? "").matchAll(/\d+(?:,\d{3})*(?:\.\d+)?/g)].map((m) => Number(m[0].replace(/,/g, ""))).filter(Number.isFinite);
47072
+ }
47073
+ function isYesNo(options) {
47074
+ return Boolean(options?.includes("Yes") && options?.includes("No"));
47075
+ }
47076
+ function wordsInQuote(value, quote) {
47077
+ const q3 = ` ${tokens(quote).join(" ")} `;
47078
+ const lines = value.split(/\n+/).map((l) => tokens(l).join(" ")).filter(Boolean);
47079
+ return lines.length > 0 && lines.every((l) => q3.includes(` ${l} `));
47080
+ }
47081
+ function onlyNumber(quote) {
47082
+ const nums = [...new Set(numbersIn(quote))];
47083
+ return nums.length === 1 ? nums[0] : null;
47084
+ }
47085
+ function amountBefore(quote, word) {
47086
+ const m = new RegExp(`${AMOUNT}\\s*${word}\\b`, "i").exec(quote) ?? new RegExp(`\\b${word}\\b[^0-9$]{0,12}${AMOUNT}`, "i").exec(quote);
47087
+ return m ? Number(m[1].replace(/,/g, "")) : null;
47088
+ }
47089
+ function depositIn(quote) {
47090
+ return amountBefore(quote, "deposit");
47091
+ }
47092
+ function totalIn(quote) {
47093
+ return amountBefore(quote, "total");
47094
+ }
47095
+ function staffNamedIn(quote) {
47096
+ return new Set((String(quote).match(/[^\s@()<>,;]+@[^\s@()<>,;]+\.[a-z]{2,}/gi) ?? []).map((e) => e.toLowerCase())).size;
47097
+ }
47098
+ function teamRange(n) {
47099
+ return n <= 1 ? "Just me" : n <= 5 ? "2-5" : "6+";
47100
+ }
47101
+ function leadRange(n) {
47102
+ return n < 100 ? "Under 100" : n < 500 ? "100-500" : n < 1e3 ? "500-1000" : "1000+";
47103
+ }
47104
+ function quoteGives(value, quote, label2 = "", options) {
47105
+ const n = numericValue(value);
47106
+ if (/^Balance due/i.test(label2)) {
47107
+ const t = totalIn(quote), d = depositIn(quote);
47108
+ return Boolean(n && !n.percent && t !== null && d !== null && t > d && t - d === n.n);
47109
+ }
47110
+ if (/^Deposit/i.test(label2)) return Boolean(n && !n.percent && depositIn(quote) === n.n);
47111
+ if (options?.length) {
47112
+ if (!options.includes(value)) return false;
47113
+ if (/[a-z]/i.test(value) && (/\d/.test(value) || tokens(value).length >= 3) && wordsInQuote(value, quote)) return true;
47114
+ if (/^Team Size$/i.test(label2)) {
47115
+ const named = staffNamedIn(quote);
47116
+ if (named) return named >= 2 && teamRange(named) === value;
47117
+ const one = onlyNumber(quote);
47118
+ return one !== null && /\b(?:staff|team|people|employees?|providers?)\b/i.test(quote) && teamRange(one) === value;
47119
+ }
47120
+ if (/Lead Volume/i.test(label2)) {
47121
+ const one = onlyNumber(quote);
47122
+ return one !== null && /\bleads?\b/i.test(quote) && leadRange(one) === value;
47123
+ }
47124
+ return false;
47125
+ }
47126
+ if (n) {
47127
+ if (n.percent || onlyNumber(quote) !== n.n) return false;
47128
+ if (/price|cost|value|\$/i.test(label2) && !/\$/.test(quote)) return false;
47129
+ if (/^Trial price/i.test(label2) && /deposit/i.test(quote)) return false;
47130
+ if (/How long is the trial/i.test(label2) && (!/\btrials?\b/i.test(quote) || !/minute/i.test(quote) || /\$/.test(quote))) return false;
47131
+ if (/in one day|per day/i.test(label2) && !/\b(?:trials?|visits?|clients?)\s+(?:a|per)\s+day\b/i.test(quote)) return false;
47132
+ if (/^Average program value/i.test(label2) && !/\baverage\b|\bmost popular\b|\bprogram\b/i.test(quote)) return false;
47133
+ return true;
47134
+ }
47135
+ return wordsInQuote(value, quote);
47136
+ }
47137
+ function intakeCitation(material, value, quote, basis, question) {
47138
+ const v = String(value ?? "").trim();
47139
+ if (v === "") return { verdict: "refused", because: "no value was given." };
47140
+ const said = quoteCheck(material, quote);
47141
+ if (said.verdict === "refused") return said;
47142
+ const shown = quote.length > 140 ? quote.slice(0, 137).trimEnd() + "\u2026" : quote;
47143
+ const based = `Based on \u201C${shown}\u201D.`;
47144
+ if (question && CLAIMS_GUARD_QUESTION.test(question.label)) return { verdict: "held", because: `This answer decides what the texts may promise, so it always waits for your click. ${based}`, claims: true };
47145
+ if (question && isYesNo(question.options)) return { verdict: "held", because: `Yes or no answers always wait for your click. ${based}` };
47146
+ if (!quoteGives(v, quote, question?.label, question?.options)) return { verdict: "held", because: based };
47147
+ if (basis === "inferred") return { verdict: "held", because: `Your Claude marked this as worked out. ${based}` };
47148
+ return { verdict: "ok" };
47149
+ }
47150
+ async function runPrefill(call2, labels, material, questions = /* @__PURE__ */ new Map()) {
46804
47151
  const fail = (error) => ({ ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error });
46805
47152
  try {
46806
47153
  const out = await call2();
@@ -46815,16 +47162,18 @@ async function runPrefill(call2, labels, material) {
46815
47162
  const candidate = raw && typeof raw === "object" && !Array.isArray(raw) ? raw.value : raw;
46816
47163
  const value = typeof candidate === "string" ? candidate.trim() : Array.isArray(candidate) && candidate.every((v) => typeof v === "string") ? candidate.map((v) => v.trim()).filter(Boolean) : "";
46817
47164
  const quote = typeof raw?.quote === "string" ? String(raw.quote).trim() : "";
47165
+ const basis = raw?.basis === "inferred" ? "inferred" : void 0;
46818
47166
  if (!value.length) continue;
46819
47167
  if (material === void 0) {
46820
47168
  answers[k] = value;
46821
47169
  unverified.push(k);
46822
47170
  continue;
46823
47171
  }
46824
- const checks = (Array.isArray(value) ? value : [value]).map((v) => citationCheck(material, v, quote));
47172
+ const q3 = questions.get(k);
47173
+ const checks = (Array.isArray(value) ? value : [value]).map((v) => intakeCitation(material, v, quote, basis, q3 ? { label: k, options: q3.options } : void 0));
46825
47174
  const cite = checks.find((c) => c.verdict === "refused") ?? checks.find((c) => c.verdict === "held") ?? checks[0];
46826
47175
  if (cite.verdict === "ok") answers[k] = value;
46827
- else if (cite.verdict === "held") held[k] = { value, because: cite.because };
47176
+ else if (cite.verdict === "held") held[k] = { value, because: cite.because, ...cite.claims ? { claims: true } : {} };
46828
47177
  }
46829
47178
  const unknown = labels.filter((l) => !(l in answers) && !(l in held));
46830
47179
  return { ok: true, answers, unknown, held, unverified };
@@ -46846,7 +47195,7 @@ function validatePrefillAnswers(questions, proposed) {
46846
47195
  }
46847
47196
  return result;
46848
47197
  }
46849
- var INTAKE_FORM_NAME2, INTAKE_CONTACT_TAG, FICTIONAL_PHONE_REASON, RECORDER_HINTS, RECORDER_READ_VERBS, RECORDER_WRITE_VERBS, PREFILL_BUILTIN_DENY, LINK_READING_OFF_MESSAGE, WORD_PDF_MESSAGE, READABLE_DOCUMENT;
47198
+ var INTAKE_FORM_NAME2, INTAKE_CONTACT_TAG, FICTIONAL_PHONE_REASON, RECORDER_HINTS, RECORDER_READ_VERBS, RECORDER_WRITE_VERBS, PREFILL_BUILTIN_DENY, LINK_READING_OFF_MESSAGE, WORD_PDF_MESSAGE, READABLE_DOCUMENT, CLAIMS_GUARD_QUESTION, AMOUNT;
46850
47199
  var init_client_intake = __esm({
46851
47200
  "src/client-intake.ts"() {
46852
47201
  "use strict";
@@ -46857,6 +47206,7 @@ var init_client_intake = __esm({
46857
47206
  init_intake_overlay();
46858
47207
  init_form_template();
46859
47208
  init_transcript();
47209
+ init_words();
46860
47210
  init_result_text();
46861
47211
  init_links();
46862
47212
  INTAKE_FORM_NAME2 = "GHL Command Blueprint \u2014 Client Intake";
@@ -46913,6 +47263,201 @@ var init_client_intake = __esm({
46913
47263
  LINK_READING_OFF_MESSAGE = "Reading a link isn't available in this beta. Open the page, copy its text, and paste it into Paste a transcript.";
46914
47264
  WORD_PDF_MESSAGE = "Word and PDF files can't be read yet. Open the file, copy the text, and paste it into Paste a transcript.";
46915
47265
  READABLE_DOCUMENT = /[.](txt|md|csv|vtt|srt)$/i;
47266
+ CLAIMS_GUARD_QUESTION = /really run out|only good for 72 hours/i;
47267
+ AMOUNT = String.raw`\$?\s*(\d[\d,]*(?:\.\d+)?)`;
47268
+ }
47269
+ });
47270
+
47271
+ // src/build-facts.ts
47272
+ function customValueKeyOf(name) {
47273
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
47274
+ }
47275
+ function customValueStatusOf(planValues, accountValues, now = () => /* @__PURE__ */ new Date()) {
47276
+ const readAt = now().toISOString();
47277
+ if (!accountValues || planValues.length > 0 && accountValues.length === 0) return { filled: [], empty: [], unreadable: true, readAt };
47278
+ const byKey = new Map(accountValues.map((v) => [customValueKeyOf(v.name), v]));
47279
+ const filled = [];
47280
+ const empty = [];
47281
+ const unknown = [];
47282
+ for (const cv of planValues) {
47283
+ const live = byKey.get(customValueKeyOf(cv.name));
47284
+ if (!live) empty.push(cv.name);
47285
+ else if (typeof live.value !== "string") unknown.push(cv.name);
47286
+ else if (live.value.trim()) filled.push(cv.name);
47287
+ else empty.push(cv.name);
47288
+ }
47289
+ return { filled, empty, ...unknown.length ? { unknown } : {}, readAt };
47290
+ }
47291
+ function handoffCustomValueNote(handoffRef, planValues, status) {
47292
+ if (!status || status.unreadable) return "";
47293
+ const covered = planValues.filter((cv) => cv.filledBy === handoffRef).map((cv) => cv.name);
47294
+ if (!covered.length) return "";
47295
+ const filled = covered.filter((n) => status.filled.includes(n));
47296
+ const empty = covered.filter((n) => status.empty.includes(n));
47297
+ const list4 = (xs) => xs.length <= 1 ? xs.join("") : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
47298
+ const parts = [
47299
+ filled.length ? `${list4(filled)} ${filled.length === 1 ? "is" : "are"} filled` : "",
47300
+ empty.length ? `${list4(empty)} ${empty.length === 1 ? "is" : "are"} empty` : ""
47301
+ ].filter(Boolean);
47302
+ return parts.length ? ` Read back from the account after this build: ${parts.join("; ")}.` : "";
47303
+ }
47304
+ function pieces(text2) {
47305
+ const guarded2 = text2.replace(/\bDr\./g, "Dr\0");
47306
+ const out = [];
47307
+ const re = /([\s\S]*?(?:[.!?](?=\s|$)|(?=\n)|$))(\s*)/g;
47308
+ let m;
47309
+ while ((m = re.exec(guarded2)) && m[0].length) out.push({ sentence: m[1].replace(/Dr\u0000/g, "Dr."), sep: m[2] });
47310
+ return out;
47311
+ }
47312
+ function rejoin(parts) {
47313
+ const kept = parts.filter((p) => p !== null && p.sentence.trim() !== "");
47314
+ return kept.map((p, i) => p.sentence + (i < kept.length - 1 ? p.sep : "")).join("");
47315
+ }
47316
+ function claimsVerifiedPersonNotReal(sentence, verifiedStaff) {
47317
+ for (const raw of verifiedStaff) {
47318
+ const name = raw.trim();
47319
+ if (!name) continue;
47320
+ const n = esc11(name);
47321
+ const after = new RegExp(String.raw`\b${n}\b(?:,\s*|\s+(?:is|was|looks like|appears to be|seems to be|may be|might be)\s+|\s+(?:isn'?t|wasn'?t)\s+)(?:only\s+|just\s+|still\s+|probably\s+)?(?:(?:a|an|the)\s+)?${NOT_REAL_PHRASE}`, "i");
47322
+ const before = new RegExp(String.raw`\b(?:placeholder|fake|dummy|test|sample|demo)(?: (?:user|staff member|person|provider|name))?,?\s+(?:named\s+|called\s+)?${n}\b`, "i");
47323
+ const isnt = new RegExp(String.raw`\b${n}\b\s+(?:isn'?t|is not|wasn'?t|was not)\s+(?:(?:a|an)\s+)?(?:real|actual|genuine)\b`, "i");
47324
+ if (after.test(sentence) || isnt.test(sentence) || before.test(sentence)) return true;
47325
+ }
47326
+ return false;
47327
+ }
47328
+ function mentionPattern(key) {
47329
+ const words2 = customValueKeyOf(key).split("_").map(esc11);
47330
+ return new RegExp(`\\b${words2.join("[\\s_]+")}\\b`, "gi");
47331
+ }
47332
+ function correctClause(clause, status) {
47333
+ const pred = new RegExp(EMPTY_PRED, "i").exec(clause);
47334
+ if (!pred) return { text: clause, changed: false };
47335
+ const head4 = clause.slice(0, pred.index);
47336
+ const keys = [...status.filled, ...status.empty, ...status.unknown ?? []];
47337
+ const found = [];
47338
+ for (const key of keys) for (const m of head4.matchAll(mentionPattern(key))) found.push({ key, start: m.index, end: m.index + m[0].length });
47339
+ if (!found.length) return { text: clause, changed: false };
47340
+ found.sort((a, b) => a.start - b.start);
47341
+ const last = found[found.length - 1];
47342
+ const tail = head4.slice(last.end);
47343
+ if (!/^\s*(?:custom values?\s*)?(?:\([^)]*\)\s*)?$/i.test(tail)) return { text: clause, changed: false };
47344
+ for (let i = 1; i < found.length; i++) if (!/^\s*(?:,|and|,\s*and|&)\s*$/i.test(head4.slice(found[i - 1].end, found[i].start))) return { text: clause, changed: false };
47345
+ const filledNamed = found.filter((f) => status.filled.includes(f.key));
47346
+ if (!filledNamed.length) return { text: clause, changed: false };
47347
+ const stillEmpty = found.filter((f) => status.empty.includes(f.key));
47348
+ if (!stillEmpty.length) return { text: "", changed: true };
47349
+ const listed = listOf(stillEmpty.map((f) => head4.slice(f.start, f.end)));
47350
+ let out = clause.slice(0, found[0].start) + listed + clause.slice(last.end);
47351
+ if (stillEmpty.length === 1 && found.length > 1) {
47352
+ return { text: out.replace(/\bcustom values\b/i, "custom value").replace(/\b(are|were)(?=\s+(?:still\s+)?(?:left\s+)?(?:empty|blank|unfilled|not|missing)\b)/i, (v) => v.toLowerCase() === "are" ? "is" : "was"), changed: true };
47353
+ }
47354
+ return { text: out, changed: true };
47355
+ }
47356
+ function maskAndNames(clause, keys) {
47357
+ const masked = [];
47358
+ let text2 = clause;
47359
+ for (const key of keys.filter((k) => /(?:^|_)and(?:_|$)/.test(customValueKeyOf(k)))) {
47360
+ text2 = text2.replace(mentionPattern(key), (m) => {
47361
+ masked.push(m);
47362
+ return `${masked.length - 1}`;
47363
+ });
47364
+ }
47365
+ return { text: text2, unmask: (t) => t.replace(/\u0001(\d+)\u0001/g, (_m, i) => masked[Number(i)]) };
47366
+ }
47367
+ function splitAndClaims(clause, keys = []) {
47368
+ const { text: text2, unmask } = maskAndNames(clause, keys);
47369
+ return splitMasked(text2).map(unmask);
47370
+ }
47371
+ function claimsWithin(clause, keys) {
47372
+ const { text: text2, unmask } = maskAndNames(clause, keys);
47373
+ const claims = (t) => new RegExp(EMPTY_PRED, "i").test(t);
47374
+ const bits = text2.split(/(,?\s+and\s+)/i);
47375
+ let from = claims(bits[0]) ? 0 : -1;
47376
+ for (let i = 2; from < 0 && i < bits.length; i += 2) if (claims(bits.slice(i).join(""))) from = i;
47377
+ if (from < 0) return [];
47378
+ return splitMasked(bits.slice(from).join("")).map(unmask).filter((_, i) => i % 2 === 0).filter(claims);
47379
+ }
47380
+ function splitMasked(clause) {
47381
+ const bits = clause.split(/(,?\s+and\s+)/i);
47382
+ const out = [bits[0]];
47383
+ const claims = (t) => new RegExp(EMPTY_PRED, "i").test(t);
47384
+ for (let i = 1; i < bits.length; i += 2) {
47385
+ const joiner = bits[i], next = bits[i + 1] ?? "";
47386
+ if (claims(out[out.length - 1]) && claims(next)) out.push(joiner, next);
47387
+ else out[out.length - 1] += joiner + next;
47388
+ }
47389
+ return out;
47390
+ }
47391
+ function correctEmptinessClaim(sentence, status) {
47392
+ if (!status || status.unreadable) return { text: sentence, changed: false };
47393
+ const keys = [...status.filled, ...status.empty, ...status.unknown ?? []];
47394
+ const parts = [];
47395
+ const outer = sentence.split(CLAUSE_SPLIT);
47396
+ for (let i = 0; i < outer.length; i += 2) {
47397
+ parts.push(...splitAndClaims(outer[i], keys));
47398
+ if (i + 1 < outer.length) parts.push(outer[i + 1]);
47399
+ }
47400
+ let changed = false;
47401
+ const kept = [];
47402
+ let dropNextConsequence = false;
47403
+ for (let i = 0; i < parts.length; i += 2) {
47404
+ const joiner = parts[i - 1] ?? "";
47405
+ if (dropNextConsequence && /^\s*,?\s*(?:and so|so)\s+$/i.test(joiner)) {
47406
+ changed = true;
47407
+ for (const piece of claimsWithin(parts[i], keys)) {
47408
+ const c2 = correctClause(piece, status);
47409
+ if (c2.text.trim()) kept.push({ text: c2.text, joiner: ", and " });
47410
+ }
47411
+ continue;
47412
+ }
47413
+ dropNextConsequence = false;
47414
+ const c = correctClause(parts[i], status);
47415
+ if (c.changed) changed = true;
47416
+ if (c.text.trim()) kept.push({ text: c.text, joiner });
47417
+ else dropNextConsequence = true;
47418
+ }
47419
+ if (!changed) return { text: sentence, changed: false };
47420
+ let text2 = kept.map((k, i) => (i ? k.joiner : "") + k.text).join("").trim();
47421
+ if (text2 && !/[.!?]$/.test(text2)) text2 += ".";
47422
+ text2 = text2.charAt(0).toUpperCase() + text2.slice(1);
47423
+ return { text: /^[.!?]?$/.test(text2) ? "" : text2, changed: true };
47424
+ }
47425
+ function guardBuildReport(report3, facts) {
47426
+ const staff = (facts.verifiedStaff ?? []).filter((n) => n.trim());
47427
+ const others = (facts.unverifiedStaff ?? []).filter((n) => n.trim());
47428
+ const fix = (text2) => {
47429
+ let staffClaim = false, changed = false;
47430
+ const parts = pieces(text2).map((p) => {
47431
+ if (staff.length && claimsVerifiedPersonNotReal(p.sentence, staff)) {
47432
+ staffClaim = true;
47433
+ changed = true;
47434
+ return null;
47435
+ }
47436
+ const bullet3 = /^(\s*(?:[-*•]|\d+[.)])\s+)/.exec(p.sentence)?.[1] ?? "";
47437
+ const c = correctEmptinessClaim(p.sentence.slice(bullet3.length), facts.customValues);
47438
+ if (!c.changed) return p;
47439
+ changed = true;
47440
+ return c.text ? { sentence: bullet3 + c.text, sep: p.sep } : null;
47441
+ });
47442
+ return { text: changed ? rejoin(parts) : text2, staffClaim };
47443
+ };
47444
+ const namesOther = (text2) => others.some((n) => new RegExp(`\\b${esc11(n)}\\b`, "i").test(text2) || new RegExp(`\\b${esc11(n.split(/\s+/)[0])}\\b`).test(text2));
47445
+ const out = { ...report3 };
47446
+ if (typeof report3.summary === "string") out.summary = fix(report3.summary).text;
47447
+ if (Array.isArray(report3.issues)) {
47448
+ out.issues = report3.issues.map((x) => ({ original: x, ...fix(x) })).filter((r) => r.text.trim() && !(r.staffClaim && !namesOther(r.original))).map((r) => r.text);
47449
+ }
47450
+ return out;
47451
+ }
47452
+ var esc11, listOf, NOT_REAL_PHRASE, EMPTY_PRED, CLAUSE_SPLIT;
47453
+ var init_build_facts = __esm({
47454
+ "src/build-facts.ts"() {
47455
+ "use strict";
47456
+ esc11 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47457
+ listOf = (xs) => xs.length <= 1 ? xs.join("") : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
47458
+ NOT_REAL_PHRASE = String.raw`(?:placeholders?|not (?:a |an )?(?:real|actual|genuine)\b|fake|dummy|made[- ]up|stand-?ins?|(?:test|sample|example|demo) (?:users?|accounts?|staff(?: members?)?|persons?|people|providers?|members?|names?|entr(?:y|ies)))`;
47459
+ EMPTY_PRED = String.raw`\b(?:is|are|was|were|remains?|stays?)\s+(?:still\s+)?(?:left\s+)?(?:empty|blank|unfilled|not (?:yet )?(?:been )?(?:filled(?: in)?|set|entered)|missing)\b(?!\s+(?:its|their|the|a|an|from|in the)\b)`;
47460
+ CLAUSE_SPLIT = /(;\s*|,\s*(?:but|while|although|though|so|and so|because|whereas)\s+|\s+but\s+)/i;
46916
47461
  }
46917
47462
  });
46918
47463
 
@@ -46927,8 +47472,8 @@ function agencyProfilePageHtml(p, clients = [], seatToken = "") {
46927
47472
  const staff = (p.staff ?? []).map((m) => [m.name, m.role, m.email].filter(Boolean).join(" \xB7 ")).join("\n");
46928
47473
  const hintOf2 = (id, hint) => hint ? `<div class="hint" id="${id}-hint">${hint}</div>` : "";
46929
47474
  const described = (id, hint) => hint ? ` aria-describedby="${id}-hint"` : "";
46930
- const input = (id, label2, value, o = {}) => `<div class="cds-field"><label for="${id}">${label2}</label>${hintOf2(id, o.hint)}<input class="cds-input" id="${id}" name="${id}"${o.numeric ? ' inputmode="numeric"' : ""} value="${esc11(value)}"${o.placeholder ? ` placeholder="${esc11(o.placeholder)}"` : ""}${described(id, o.hint)}></div>`;
46931
- const area = (id, label2, value, o = {}) => `<div class="cds-field"><label for="${id}">${label2}</label>${hintOf2(id, o.hint)}<textarea class="cds-textarea" id="${id}" name="${id}"${o.rows ? ` rows="${o.rows}"` : ""}${o.placeholder ? ` placeholder="${o.placeholder}"` : ""}${described(id, o.hint)}>${esc11(value)}</textarea></div>`;
47475
+ const input = (id, label2, value, o = {}) => `<div class="cds-field"><label for="${id}">${label2}</label>${hintOf2(id, o.hint)}<input class="cds-input" id="${id}" name="${id}"${o.numeric ? ' inputmode="numeric"' : ""} value="${esc12(value)}"${o.placeholder ? ` placeholder="${esc12(o.placeholder)}"` : ""}${described(id, o.hint)}></div>`;
47476
+ const area = (id, label2, value, o = {}) => `<div class="cds-field"><label for="${id}">${label2}</label>${hintOf2(id, o.hint)}<textarea class="cds-textarea" id="${id}" name="${id}"${o.rows ? ` rows="${o.rows}"` : ""}${o.placeholder ? ` placeholder="${o.placeholder}"` : ""}${described(id, o.hint)}>${esc12(value)}</textarea></div>`;
46932
47477
  const filled = (xs) => xs.filter((x) => Array.isArray(x) ? x.length > 0 : x !== void 0 && x !== null && String(x).trim() !== "").length;
46933
47478
  const progress = (xs) => {
46934
47479
  const n = filled(xs);
@@ -46946,20 +47491,20 @@ function agencyProfilePageHtml(p, clients = [], seatToken = "") {
46946
47491
  const cur = e.currency || p.pricing?.currency || "";
46947
47492
  const recorded = Object.keys(e).filter((k) => k !== "updated_at").length > 0;
46948
47493
  const sel = (k, opts, current) => `<select class="cds-select f" data-k="${k}">${opts.map(([val, text2]) => `<option value="${val}"${(current ?? "") === val ? " selected" : ""}>${text2}</option>`).join("")}</select>`;
46949
- return ` <li><details class="cl cds-details" data-id="${esc11(c.locationId)}"${recorded ? "" : ' data-empty="1"'}>
46950
- <summary class="ap-sum"><span class="ttl cn">${esc11(c.name)}<span class="sub cs">${recorded ? esc11([e.offer, e.price !== void 0 ? `${fmt(e.price, cur)}${e.cadence === "monthly" ? "/mo" : ""}` : "", e.status && e.status !== "active" ? e.status.charAt(0).toUpperCase() + e.status.slice(1) : ""].filter(Boolean).join(" \xB7 ") || "Recorded") : "Nothing recorded yet"}</span></span><span class="ap-end">${under && floor !== void 0 ? `<span class="cds-chip warn">Under your floor of ${esc11(fmt(floor, cur))}</span>` : ""}<span class="cds-link open">Open</span></span></summary>
47494
+ return ` <li><details class="cl cds-details" data-id="${esc12(c.locationId)}"${recorded ? "" : ' data-empty="1"'}>
47495
+ <summary class="ap-sum"><span class="ttl cn">${esc12(c.name)}<span class="sub cs">${recorded ? esc12([e.offer, e.price !== void 0 ? `${fmt(e.price, cur)}${e.cadence === "monthly" ? "/mo" : ""}` : "", e.status && e.status !== "active" ? e.status.charAt(0).toUpperCase() + e.status.slice(1) : ""].filter(Boolean).join(" \xB7 ") || "Recorded") : "Nothing recorded yet"}</span></span><span class="ap-end">${under && floor !== void 0 ? `<span class="cds-chip warn">Under your floor of ${esc12(fmt(floor, cur))}</span>` : ""}<span class="cds-link open">Open</span></span></summary>
46951
47496
  <div class="ap-body">
46952
47497
  <div class="cds-grid">
46953
47498
  <div class="cds-field"><label>What this is</label>${sel("kind", [["client", "A client whose GoHighLevel account you run"], ["software", "Software they bought from you"], ["other", "Other income: event fee, revenue share, training"]], e.kind ?? "client")}</div>
46954
- <div class="cds-field"><label>Offer</label><input class="cds-input f" data-k="offer" value="${esc11(e.offer)}" placeholder="${esc11(p.offers?.[0]?.name || "Speed to Lead")}"></div>
46955
- <div class="cds-field"><label>They pay</label><input class="cds-input f" data-k="price" inputmode="numeric" value="${esc11(e.price)}"></div>
47499
+ <div class="cds-field"><label>Offer</label><input class="cds-input f" data-k="offer" value="${esc12(e.offer)}" placeholder="${esc12(p.offers?.[0]?.name || "Speed to Lead")}"></div>
47500
+ <div class="cds-field"><label>They pay</label><input class="cds-input f" data-k="price" inputmode="numeric" value="${esc12(e.price)}"></div>
46956
47501
  <div class="cds-field"><label>How often</label>${sel("cadence", [["", "Not set"], ["monthly", "Monthly"], ["one-time", "One-time"]], e.cadence)}</div>
46957
- <div class="cds-field"><label>Started</label><input class="cds-input f" data-k="startedOn" placeholder="2026-01-15" value="${esc11(e.startedOn)}"></div>
46958
- <div class="cds-field"><label>Renews</label><input class="cds-input f" data-k="renewsOn" placeholder="2027-01-15" value="${esc11(e.renewsOn)}"></div>
47502
+ <div class="cds-field"><label>Started</label><input class="cds-input f" data-k="startedOn" placeholder="2026-01-15" value="${esc12(e.startedOn)}"></div>
47503
+ <div class="cds-field"><label>Renews</label><input class="cds-input f" data-k="renewsOn" placeholder="2027-01-15" value="${esc12(e.renewsOn)}"></div>
46959
47504
  <div class="cds-field"><label>Status</label>${sel("status", [["", "Not set"], ["active", "Active"], ["paused", "Paused"], ["ended", "Ended"]], e.status)}</div>
46960
- <div class="cds-field"><label>Who runs it</label><input class="cds-input f" data-k="owner" value="${esc11(e.owner)}" placeholder="${esc11(p.staff?.[0]?.name || "")}"></div>
47505
+ <div class="cds-field"><label>Who runs it</label><input class="cds-input f" data-k="owner" value="${esc12(e.owner)}" placeholder="${esc12(p.staff?.[0]?.name || "")}"></div>
46961
47506
  </div>
46962
- <div class="cds-field"><label>Anything specific to this client: one per line</label><textarea class="cds-textarea f" data-k="sops" placeholder="Call the owner before any send">${esc11((e.sops ?? []).join("\n"))}</textarea></div>
47507
+ <div class="cds-field"><label>Anything specific to this client: one per line</label><textarea class="cds-textarea f" data-k="sops" placeholder="Call the owner before any send">${esc12((e.sops ?? []).join("\n"))}</textarea></div>
46963
47508
  <div class="cds-row"><button type="button" class="cds-btn ghost save-cl">Save this client</button><span class="cmsg" role="status"></span></div>
46964
47509
  </div>
46965
47510
  </details></li>`;
@@ -47015,7 +47560,7 @@ ${p.agencyName ? "" : ` <div class="cds-callout warn"><span class="ttl">Add an a
47015
47560
  ${input("legalName", "Registered entity, if different", p.legalName, { placeholder: "Your Agency LLC, used on contracts and A2P" })}
47016
47561
  <div class="cds-grid">
47017
47562
  ${input("website", "Website", p.website, { placeholder: "https://youragency.com" })}
47018
- <div class="cds-field"><label for="accent">Brand colour</label><input type="color" class="cds-input" id="accent" name="accent" value="${esc11(p.accent || "#b8934a")}"></div>
47563
+ <div class="cds-field"><label for="accent">Brand colour</label><input type="color" class="cds-input" id="accent" name="accent" value="${esc12(p.accent || "#b8934a")}"></div>
47019
47564
  </div>
47020
47565
  ${input("logoUrl", "Logo URL", p.logoUrl, { placeholder: "https://youragency.com/logo.svg", hint: "Must start with https. A logo link that isn't https is refused, because this one goes on pages your prospects open." })}
47021
47566
  ${input("logoFile", "Logo file on this computer (for client reports)", p.logoFile, { placeholder: "/Users/you/Documents/logo.png", hint: "Optional. The full path to a PNG, JPEG, WebP, GIF or SVG under 256 KB. Reports you hand clients carry it inside the page, so it prints and travels with a saved copy." })}
@@ -47060,7 +47605,7 @@ ${p.agencyName ? "" : ` <div class="cds-callout warn"><span class="ttl">Add an a
47060
47605
  <div class="cds-panel-head"><div class="txt"><div class="cds-panel-label">What you deliver</div><h2 class="cds-panel-title">Services you sell</h2></div></div>
47061
47606
  <p>Tick what you actually deliver. A report you run on a prospect's website offers a fix only under a service ticked here; anything else shows as a plain finding, so a prospect is never offered something you do not do.</p>
47062
47607
  <div class="cds-grid two">
47063
- ${SERVICE_IDS.map((id) => ` <label class="cds-check"><input type="checkbox" name="svc" value="${id}"${sold.offered.includes(id) ? " checked" : ""}><span>${esc11(SERVICES[id].label)}</span></label>`).join("\n")}
47608
+ ${SERVICE_IDS.map((id) => ` <label class="cds-check"><input type="checkbox" name="svc" value="${id}"${sold.offered.includes(id) ? " checked" : ""}><span>${esc12(SERVICES[id].label)}</span></label>`).join("\n")}
47064
47609
  </div>
47065
47610
  ${area("svcCustom", "Your own services: one per line", sold.custom.map((c) => c.name).join("\n"), { rows: Math.max(3, sold.custom.length + 1), placeholder: "Local Lift package", hint: "Saved as yours. To have one pitched on a report, tell Claude which service above it covers, for example: my Local Lift package covers local search." })}
47066
47611
  </section>
@@ -47143,8 +47688,8 @@ ${SERVICE_IDS.map((id) => ` <label class="cds-check"><input type="checkbox" n
47143
47688
  <details class="cds-details"><summary>Details</summary>
47144
47689
  <div class="cds-stack">
47145
47690
  <p>Install or update Claude Code on this computer with the Claude Code setup instructions. It needs version 2.1.0 or newer.</p>
47146
- <p>${esc11(claudeCodeAddHelp())}</p>
47147
- <p>On GoHighLevel's Agency Pro plan, connect your agency Private Integration key in GHL Command so it can create sub-accounts and staff users. Create that key in GoHighLevel's agency view (not inside a sub-account): ${esc11(AGENCY_PIT_CLICK_PATH)}. GoHighLevel allows both only on Agency Pro; on other plans, create the sub-account and add its staff in GoHighLevel yourself.</p>
47691
+ <p>${esc12(claudeCodeAddHelp())}</p>
47692
+ <p>On GoHighLevel's Agency Pro plan, connect your agency Private Integration key in GHL Command so it can create sub-accounts and staff users. Create that key in GoHighLevel's agency view (not inside a sub-account): ${esc12(AGENCY_PIT_CLICK_PATH)}. GoHighLevel allows both only on Agency Pro; on other plans, create the sub-account and add its staff in GoHighLevel yourself.</p>
47148
47693
  <p>If a client\u2019s workflow builder connection fails, say to Claude: \u201CUnlock the Workflow Builder.\u201D Log into GoHighLevel in the Chrome window that opens, then quit Claude fully and open it again.</p>
47149
47694
  <p>Enable the intake and workflow tools in your GHL Command settings.</p>
47150
47695
  <p>Use Check and save this key, in the Claude key panel, to verify Claude authentication.</p>
@@ -47285,7 +47830,7 @@ document.querySelectorAll(".save-cl").forEach(btn=>btn.addEventListener("click",
47285
47830
  </script>
47286
47831
  <script>${COCKPIT_TYPE_JS}</script></body></html>`;
47287
47832
  }
47288
- var READ_NUMBER_JS, fmt, esc11;
47833
+ var READ_NUMBER_JS, fmt, esc12;
47289
47834
  var init_agency_profile_page = __esm({
47290
47835
  "src/agency-profile-page.ts"() {
47291
47836
  "use strict";
@@ -47305,7 +47850,7 @@ var init_agency_profile_page = __esm({
47305
47850
  return Number.isFinite(n)?{value:n}:{bad:true};
47306
47851
  }`;
47307
47852
  fmt = (n, currency) => `${currency ? currency + " " : ""}${n.toLocaleString("en-US")}`;
47308
- esc11 = (x) => String(x ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
47853
+ esc12 = (x) => String(x ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
47309
47854
  }
47310
47855
  });
47311
47856
 
@@ -49671,24 +50216,24 @@ function storedDay(iso2, now = /* @__PURE__ */ new Date()) {
49671
50216
  function storedCard(r) {
49672
50217
  const st = brandState(r);
49673
50218
  if (r.problem) {
49674
- return `<article class="cds-card bp-card"><div class="cds-row"><h3 class="cds-panel-title">${esc12(r.slug)}</h3><span class="cds-chip ${st.tone}">${st.word}</span></div>
50219
+ return `<article class="cds-card bp-card"><div class="cds-row"><h3 class="cds-panel-title">${esc13(r.slug)}</h3><span class="cds-chip ${st.tone}">${st.word}</span></div>
49675
50220
  <p>This brand's file could not be opened. Pull it again from the website, or ask for help with the detail below.</p>
49676
- <details class="cds-details"><summary>Details</summary><p>${esc12(r.problem)}</p></details></article>`;
50221
+ <details class="cds-details"><summary>Details</summary><p>${esc13(r.problem)}</p></details></article>`;
49677
50222
  }
49678
50223
  const href = `/brand/${encodeURIComponent(r.slug)}`;
49679
50224
  const f = fontWords(r.display, r.body);
49680
50225
  return `<article class="cds-card bp-card">
49681
- <div class="cds-row"><h3 class="cds-panel-title">${esc12(r.name)}</h3><span class="cds-chip ${st.tone}">${st.word}</span>${f.broken ? `<span class="cds-chip warn">Check the fonts</span>` : ""}</div>
50226
+ <div class="cds-row"><h3 class="cds-panel-title">${esc13(r.name)}</h3><span class="cds-chip ${st.tone}">${st.word}</span>${f.broken ? `<span class="cds-chip warn">Check the fonts</span>` : ""}</div>
49682
50227
  ${r.accent ? `<div class="bp-sw">${swatch(r.accent, "Accent")}</div>` : ""}
49683
- <p>${esc12(f.text)}</p>
49684
- <div class="cds-src">${esc12(brandSource(r.extractedVia))}</div>
50228
+ <p>${esc13(f.text)}</p>
50229
+ <div class="cds-src">${esc13(brandSource(r.extractedVia))}</div>
49685
50230
  <div class="cds-row"><a class="cds-btn ghost" href="${href}">Open the brand</a></div>
49686
- <details class="cds-details"><summary>Details</summary><p>Confidence ${Math.round(r.confidence * 100)}%, read by ${esc12(r.extractedVia)}, ${esc12(r.scheme)} background${r.extractedAt ? `, stored ${esc12(r.extractedAt.slice(0, 10))}` : ""}.</p>${f.broken ? brokenFontsNote(r.display, r.body) : ""}</details>
50231
+ <details class="cds-details"><summary>Details</summary><p>Confidence ${Math.round(r.confidence * 100)}%, read by ${esc13(r.extractedVia)}, ${esc13(r.scheme)} background${r.extractedAt ? `, stored ${esc13(r.extractedAt.slice(0, 10))}` : ""}.</p>${f.broken ? brokenFontsNote(r.display, r.body) : ""}</details>
49687
50232
  </article>`;
49688
50233
  }
49689
50234
  function houseRow(d) {
49690
50235
  const sub = [d.voice.tone, fontWords(d.typography.display.family, d.typography.body.family).text].filter(Boolean).join(". ");
49691
- return `<li><span class="ttl">${esc12(d.name)}<span class="sub">${esc12(sub)}</span></span><span class="end"><span class="bp-sw">${swatch(d.colors.bg.hex, "Background")}${swatch(d.colors.fg.hex, "Text")}${swatch(d.colors.accent.hex, "Accent")}${swatch(d.colors.surface.hex, "Surface")}</span><button type="button" class="cds-btn ghost" data-house="${esc12(d.slug)}">Use this look</button></span></li>`;
50236
+ return `<li><span class="ttl">${esc13(d.name)}<span class="sub">${esc13(sub)}</span></span><span class="end"><span class="bp-sw">${swatch(d.colors.bg.hex, "Background")}${swatch(d.colors.fg.hex, "Text")}${swatch(d.colors.accent.hex, "Accent")}${swatch(d.colors.surface.hex, "Surface")}</span><button type="button" class="cds-btn ghost" data-house="${esc13(d.slug)}">Use this look</button></span></li>`;
49692
50237
  }
49693
50238
  function brandsPageHtml(rows2, house, refs2, seatToken = "") {
49694
50239
  const first2 = rows2.slice(0, SHOW_FIRST), rest = rows2.slice(SHOW_FIRST);
@@ -49723,7 +50268,7 @@ ${refs2.length ? `<details id="refs-more" class="cds-more"><summary>More looks f
49723
50268
  <div class="cds-stack">
49724
50269
  <p>Colours, type and shape only, fetched when you pick one. No logo comes with it, and the pages you make never name the brand it came from.</p>
49725
50270
  <div class="cds-field bp-form"><label for="ref-filter">Find a look</label><input class="cds-input" id="ref-filter" type="search" autocomplete="off" placeholder="Type a name"><div class="hint" id="ref-count" aria-live="polite"></div></div>
49726
- <div class="bp-refs" id="refs">${refs2.map((r) => `<button type="button" class="cds-chip" data-look="${esc12(r.slug)}" title="${esc12(r.website)}">${esc12(r.name)}</button>`).join("")}</div>
50271
+ <div class="bp-refs" id="refs">${refs2.map((r) => `<button type="button" class="cds-chip" data-look="${esc13(r.slug)}" title="${esc13(r.website)}">${esc13(r.name)}</button>`).join("")}</div>
49727
50272
  </div>
49728
50273
  </details>` : ""}
49729
50274
  <div class="cds-field"><p class="hint" id="look-status" role="status" aria-live="polite"></p></div>
@@ -49762,20 +50307,20 @@ function brandCardHtml(d, opts = {}) {
49762
50307
  };
49763
50308
  const fw = composeFonts(faceWords(disp), faceWords(bod), !disp.family?.trim() && !bod.family?.trim());
49764
50309
  const v = brandVerdict(brandState({ confidence: d.provenance.confidence }), fw.broken);
49765
- const named = (hex, label2) => `<div class="bp-swatch"><span data-sw="${esc12(hex)}" role="img" aria-label="${esc12(label2)} colour"></span><span class="cds-src">${esc12(label2)}</span></div>`;
49766
- const sw = (label2, t) => `<tr><td><span class="bp-sw">${swatch(t.hex, label2)}</span></td><th scope="row">${esc12(label2)}</th><td class="cds-num">${esc12(t.hex)}</td><td><small>${esc12(t.source)}</small></td></tr>`;
49767
- const face = (label2, f) => `<tr><th scope="row">${esc12(label2)}</th><td><b>${esc12(f.family)}</b></td><td>${f.renderIntent === "google" ? `<span class="cds-chip ok">Google Fonts</span>` : `<span class="cds-chip warn">${f.renderIntent === "licensed-webfont" ? "licensed" : "system"}</span> renders as <b>${esc12(renderedFamily(f))}</b>`}</td><td><small>${esc12(f.source)}</small></td></tr>`;
49768
- const pairs = d.contrast.pairs.map((p) => `<tr><td class="cds-num">${esc12(p.fg)} on ${esc12(p.bg)}</td><td class="cds-num">${p.ratio}:1 ${p.ratio >= 4.5 ? `<span class="cds-chip ok">AA</span>` : `<span class="cds-chip warn">under 4.5</span>`}</td><td>${p.correctedFg ? `<small>text shade corrected to ${esc12(p.correctedFg)} (${p.correctedRatio}:1)</small>` : ""}</td></tr>`).join("");
49769
- const samples = d.voice.samples.map((s) => `<li><span class="ttl">${esc12(s)}</span></li>`);
50310
+ const named = (hex, label2) => `<div class="bp-swatch"><span data-sw="${esc13(hex)}" role="img" aria-label="${esc13(label2)} colour"></span><span class="cds-src">${esc13(label2)}</span></div>`;
50311
+ const sw = (label2, t) => `<tr><td><span class="bp-sw">${swatch(t.hex, label2)}</span></td><th scope="row">${esc13(label2)}</th><td class="cds-num">${esc13(t.hex)}</td><td><small>${esc13(t.source)}</small></td></tr>`;
50312
+ const face = (label2, f) => `<tr><th scope="row">${esc13(label2)}</th><td><b>${esc13(f.family)}</b></td><td>${f.renderIntent === "google" ? `<span class="cds-chip ok">Google Fonts</span>` : `<span class="cds-chip warn">${f.renderIntent === "licensed-webfont" ? "licensed" : "system"}</span> renders as <b>${esc13(renderedFamily(f))}</b>`}</td><td><small>${esc13(f.source)}</small></td></tr>`;
50313
+ const pairs = d.contrast.pairs.map((p) => `<tr><td class="cds-num">${esc13(p.fg)} on ${esc13(p.bg)}</td><td class="cds-num">${p.ratio}:1 ${p.ratio >= 4.5 ? `<span class="cds-chip ok">AA</span>` : `<span class="cds-chip warn">under 4.5</span>`}</td><td>${p.correctedFg ? `<small>text shade corrected to ${esc13(p.correctedFg)} (${p.correctedRatio}:1)</small>` : ""}</td></tr>`).join("");
50314
+ const samples = d.voice.samples.map((s) => `<li><span class="ttl">${esc13(s)}</span></li>`);
49770
50315
  const headlines = samples.length ? `<ul class="cds-list">${samples.slice(0, SHOW_FIRST).join("")}</ul>${samples.length > SHOW_FIRST ? `<details class="cds-more"><summary>Show all ${samples.length} headlines</summary><ul class="cds-list">${samples.slice(SHOW_FIRST).join("")}</ul></details>` : ""}` : "";
49771
50316
  const cleanSvg = d.logo?.inlineSvg ? sanitizeSvgOrDrop(d.logo.inlineSvg) : void 0;
49772
- const logo = cleanSvg ? `<img src="data:image/svg+xml;base64,${Buffer.from(cleanSvg.replace(/currentColor/g, c.fg.hex)).toString("base64")}" alt="${esc12(d.name)} logo">` : d.logo?.inlineSvg ? `<span>The stored logo is not a plain drawing, so it is not shown</span>` : isRasterDataUri(d.logo?.dataUri) ? `<img src="${esc12(d.logo.dataUri)}" alt="${esc12(d.name)} logo">` : d.logo?.dataUri ? `<span>The stored logo image is not a plain picture, so it is not shown</span>` : `<span>No logo yet. Add one under Change something.</span>`;
50317
+ const logo = cleanSvg ? `<img src="data:image/svg+xml;base64,${Buffer.from(cleanSvg.replace(/currentColor/g, c.fg.hex)).toString("base64")}" alt="${esc13(d.name)} logo">` : d.logo?.inlineSvg ? `<span>The stored logo is not a plain drawing, so it is not shown</span>` : isRasterDataUri(d.logo?.dataUri) ? `<img src="${esc13(d.logo.dataUri)}" alt="${esc13(d.name)} logo">` : d.logo?.dataUri ? `<span>The stored logo image is not a plain picture, so it is not shown</span>` : `<span>No logo yet. Add one under Change something.</span>`;
49773
50318
  const day = storedDay(d.provenance.extractedAt);
49774
- const sub = [d.website ? esc12(d.website) : "", esc12(brandSource(d.provenance.extractedVia)), day ? `stored ${esc12(day)}` : ""].filter(Boolean).join(" \xB7 ");
50319
+ const sub = [d.website ? esc13(d.website) : "", esc13(brandSource(d.provenance.extractedVia)), day ? `stored ${esc13(day)}` : ""].filter(Boolean).join(" \xB7 ");
49775
50320
  const table = (cols, body) => `<div class="cds-table-wrap"><table class="cds-table"><thead><tr>${cols.map((h) => `<th scope="col">${h}</th>`).join("")}</tr></thead><tbody>${body}</tbody></table></div>`;
49776
50321
  const details = `<details class="cds-details" id="brand-details"><summary>Details</summary><div class="cds-stack">
49777
- <p>Confidence ${Math.round(d.provenance.confidence * 100)}%, read by ${esc12(d.provenance.extractedVia)}, ${esc12(d.scheme)} scheme, ${esc12(d.provenance.extractedAt.slice(0, 10))}.</p>
49778
- ${d.pageStudioTheme.warnings.length ? `<h3>Before this reaches a client</h3><ul>${d.pageStudioTheme.warnings.map((w) => `<li>${esc12(w)}</li>`).join("")}</ul>` : ""}
50322
+ <p>Confidence ${Math.round(d.provenance.confidence * 100)}%, read by ${esc13(d.provenance.extractedVia)}, ${esc13(d.scheme)} scheme, ${esc13(d.provenance.extractedAt.slice(0, 10))}.</p>
50323
+ ${d.pageStudioTheme.warnings.length ? `<h3>Before this reaches a client</h3><ul>${d.pageStudioTheme.warnings.map((w) => `<li>${esc13(w)}</li>`).join("")}</ul>` : ""}
49779
50324
  ${fw.broken ? brokenFontsNote(disp.family, bod.family) : ""}
49780
50325
  <h3>Colours</h3>
49781
50326
  ${table(['<span class="cds-sr">Swatch</span>', "Role", "Hex", "Where it came from"], `${sw("Background", c.bg)}${sw("Text", c.fg)}${sw("Accent", c.accent)}${sw("Muted text", c.muted)}${sw("Border", c.border)}${sw("Surface", c.surface)}${sw("Accent, soft", c.primarySoft)}`)}
@@ -49784,34 +50329,34 @@ ${table(["Pair", "Ratio", '<span class="cds-sr">Correction</span>'], pairs)}
49784
50329
  <h3>Fonts</h3>
49785
50330
  ${table(["Use", "Face", "Renders as", "Source"], `${face("Display", disp)}${face("Body", bod)}`)}
49786
50331
  <h3>Logo</h3>
49787
- <p>${esc12(d.provenance.evidence["logo"] ?? "No note on where the logo came from.")}${d.logo?.whiteFillFixed ? " \xB7 white fills replaced with currentColor" : ""}</p>
50332
+ <p>${esc13(d.provenance.evidence["logo"] ?? "No note on where the logo came from.")}${d.logo?.whiteFillFixed ? " \xB7 white fills replaced with currentColor" : ""}</p>
49788
50333
  <h3>Shape and voice</h3>
49789
- <p>Radius ${d.shape.radius.sm} / ${d.shape.radius.md} / ${d.shape.radius.lg} px \xB7 shadows ${esc12(d.shape.shadows)} \xB7 ${d.shape.gridBase}-point grid</p>
49790
- ${d.voice.tone ? `<p>Tone: ${esc12(d.voice.tone)}${d.voice.audience ? ` \xB7 Audience: ${esc12(d.voice.audience)}` : ""}</p>` : ""}
50334
+ <p>Radius ${d.shape.radius.sm} / ${d.shape.radius.md} / ${d.shape.radius.lg} px \xB7 shadows ${esc13(d.shape.shadows)} \xB7 ${d.shape.gridBase}-point grid</p>
50335
+ ${d.voice.tone ? `<p>Tone: ${esc13(d.voice.tone)}${d.voice.audience ? ` \xB7 Audience: ${esc13(d.voice.audience)}` : ""}</p>` : ""}
49791
50336
  ${headlines}
49792
50337
  <h3>For Page Studio</h3>
49793
50338
  <p>The six-slot theme compose_page and compose_website accept. A re-skin of the donor template, not a port of the design system.</p>
49794
- <pre class="bp-pre">${esc12(JSON.stringify({ primary: d.pageStudioTheme.primary, primarySoft: d.pageStudioTheme.primarySoft, surface: d.pageStudioTheme.surface, textDark: d.pageStudioTheme.textDark, headingFont: d.pageStudioTheme.headingFont, bodyFont: d.pageStudioTheme.bodyFont }, null, 2))}</pre>
50339
+ <pre class="bp-pre">${esc13(JSON.stringify({ primary: d.pageStudioTheme.primary, primarySoft: d.pageStudioTheme.primarySoft, surface: d.pageStudioTheme.surface, textDark: d.pageStudioTheme.textDark, headingFont: d.pageStudioTheme.headingFont, bodyFont: d.pageStudioTheme.bodyFont }, null, 2))}</pre>
49795
50340
  <h3>Where every value came from</h3>
49796
- ${table(["Value", "Source"], Object.entries(d.provenance.evidence).map(([k, val]) => `<tr><th scope="row">${esc12(k)}</th><td><small>${esc12(val)}</small></td></tr>`).join(""))}
49797
- ${d.provenance.source ? `<p>Source: ${esc12(d.provenance.source.url)}${d.provenance.source.commit ? ` @ ${esc12(d.provenance.source.commit.slice(0, 7))}` : ""}${d.provenance.source.license ? ` \xB7 ${esc12(d.provenance.source.license)}` : ""}</p>` : ""}
49798
- ${opts.dir ? `<p>Stored at ${esc12(opts.dir)}</p>` : ""}
50341
+ ${table(["Value", "Source"], Object.entries(d.provenance.evidence).map(([k, val]) => `<tr><th scope="row">${esc13(k)}</th><td><small>${esc13(val)}</small></td></tr>`).join(""))}
50342
+ ${d.provenance.source ? `<p>Source: ${esc13(d.provenance.source.url)}${d.provenance.source.commit ? ` @ ${esc13(d.provenance.source.commit.slice(0, 7))}` : ""}${d.provenance.source.license ? ` \xB7 ${esc13(d.provenance.source.license)}` : ""}</p>` : ""}
50343
+ ${opts.dir ? `<p>Stored at ${esc13(opts.dir)}</p>` : ""}
49799
50344
  </div></details>`;
49800
50345
  return `${head2(`${d.name} \xB7 brand`)}
49801
- <div class="cds-page-head"><div class="txt"><div class="cds-kicker">Brand</div><h1 class="cds-page-title">Check the ${esc12(d.name)} brand, then make something in it</h1><p class="cds-page-sub">${sub}</p></div><a class="cds-btn ghost" href="/brands">All brands</a></div>
49802
- <section class="cds-panel bp-glance" id="glance" aria-labelledby="glance-title"><div class="cds-panel-head"><div class="txt"><div class="cds-panel-label">At a glance</div><h2 class="cds-panel-title" id="glance-title">How ${esc12(d.name)} looks</h2></div></div>
49803
- <div class="cds-row" id="verdict"><span class="cds-chip ${v.tone}">${v.word}</span><p>${esc12(v.say)}</p></div>
50346
+ <div class="cds-page-head"><div class="txt"><div class="cds-kicker">Brand</div><h1 class="cds-page-title">Check the ${esc13(d.name)} brand, then make something in it</h1><p class="cds-page-sub">${sub}</p></div><a class="cds-btn ghost" href="/brands">All brands</a></div>
50347
+ <section class="cds-panel bp-glance" id="glance" aria-labelledby="glance-title"><div class="cds-panel-head"><div class="txt"><div class="cds-panel-label">At a glance</div><h2 class="cds-panel-title" id="glance-title">How ${esc13(d.name)} looks</h2></div></div>
50348
+ <div class="cds-row" id="verdict"><span class="cds-chip ${v.tone}">${v.word}</span><p>${esc13(v.say)}</p></div>
49804
50349
  <dl class="cds-kv">
49805
50350
  <dt>Colours</dt><dd><div class="bp-swatches">${named(c.accent.hex, "Accent")}${named(c.fg.hex, "Text")}${named(c.bg.hex, "Background")}${named(c.surface.hex, "Cards")}</div></dd>
49806
- <dt>Fonts</dt><dd>${esc12(fw.text)}</dd>
49807
- <dt>Logo</dt><dd><div class="bp-logo" data-ink="${esc12(c.fg.hex)}" data-sw="${esc12(c.bg.hex)}">${logo}</div></dd>
50351
+ <dt>Fonts</dt><dd>${esc13(fw.text)}</dd>
50352
+ <dt>Logo</dt><dd><div class="bp-logo" data-ink="${esc13(c.fg.hex)}" data-sw="${esc13(c.bg.hex)}">${logo}</div></dd>
49808
50353
  </dl>
49809
- ${opts.screenshot ? `<details class="cds-more" id="their-site"><summary>Show their website as we saw it</summary><img class="bp-shot" src="/brand/${encodeURIComponent(d.slug)}/screenshot.png" alt="Screenshot of ${esc12(d.name)}'s website"></details>` : ""}
50354
+ ${opts.screenshot ? `<details class="cds-more" id="their-site"><summary>Show their website as we saw it</summary><img class="bp-shot" src="/brand/${encodeURIComponent(d.slug)}/screenshot.png" alt="Screenshot of ${esc13(d.name)}'s website"></details>` : ""}
49810
50355
  ${details}
49811
50356
  </section>
49812
50357
  <div class="cds-grid two">
49813
50358
  ${panel2("Make", "Make something in this brand", `<p>Runs on <b>your own Claude</b>, on this computer, never our key.</p>
49814
- <details class="cds-details" id="make-details"><summary>Details</summary><p>${esc12(RUN_EXPECTATION)} The run keeps going if you close this tab. Write the headline and the points you want made, not instructions.</p></details>
50359
+ <details class="cds-details" id="make-details"><summary>Details</summary><p>${esc13(RUN_EXPECTATION)} The run keeps going if you close this tab. Write the headline and the points you want made, not instructions.</p></details>
49815
50360
  <form id="make" class="cds-stack" novalidate><div class="cds-grid">
49816
50361
  <div class="cds-field"><label for="mk-medium">What</label><select class="cds-select" id="mk-medium"><option value="deck">A deck</option><option value="site">A landing page</option><option value="report">A report</option></select></div>
49817
50362
  <div class="cds-field"><label for="mk-slides">Slides (a deck only)</label><input class="cds-input" id="mk-slides" type="number" min="${DESIGN_FORM_LIMITS.slidesMin}" max="${DESIGN_FORM_LIMITS.slidesMax}" step="1" value="6"></div>
@@ -49827,10 +50372,10 @@ ${panel2("Make", "Make something in this brand", `<p>Runs on <b>your own Claude<
49827
50372
  <div class="cds-field"><p class="hint" id="mk-status" role="status" aria-live="polite"></p></div><pre id="mk-log" class="bp-pre" hidden></pre></form>`, "make-panel")}
49828
50373
  ${panel2("Change", "Change something", `<p>Fill in only what you want to change. An empty field keeps what is there.</p>
49829
50374
  <form id="override" class="cds-stack">
49830
- <div class="cds-field"><label for="accent">Accent colour</label><div class="cds-row bp-accent"><input class="cds-input" id="accent" name="accent" type="text" placeholder="${esc12(c.accent.hex)}" autocomplete="off"><input class="cds-input bp-pick" id="accent-pick" type="color" value="${esc12(pickerHex(c.accent.hex))}" aria-label="Pick the accent colour"></div></div>
49831
- <div class="cds-field"><label for="displayFont">Heading font</label><input class="cds-input" id="displayFont" type="text" placeholder="${esc12(plausibleFontName(disp.family) ? disp.family : "Type the font name")}" autocomplete="off"></div>
49832
- <div class="cds-field"><label for="bodyFont">Text font</label><input class="cds-input" id="bodyFont" type="text" placeholder="${esc12(plausibleFontName(bod.family) ? bod.family : "Type the font name")}" autocomplete="off"></div>
49833
- <div class="cds-field"><label for="brandName">Brand name</label><input class="cds-input" id="brandName" type="text" placeholder="${esc12(d.name)}" autocomplete="off"></div>
50375
+ <div class="cds-field"><label for="accent">Accent colour</label><div class="cds-row bp-accent"><input class="cds-input" id="accent" name="accent" type="text" placeholder="${esc13(c.accent.hex)}" autocomplete="off"><input class="cds-input bp-pick" id="accent-pick" type="color" value="${esc13(pickerHex(c.accent.hex))}" aria-label="Pick the accent colour"></div></div>
50376
+ <div class="cds-field"><label for="displayFont">Heading font</label><input class="cds-input" id="displayFont" type="text" placeholder="${esc13(plausibleFontName(disp.family) ? disp.family : "Type the font name")}" autocomplete="off"></div>
50377
+ <div class="cds-field"><label for="bodyFont">Text font</label><input class="cds-input" id="bodyFont" type="text" placeholder="${esc13(plausibleFontName(bod.family) ? bod.family : "Type the font name")}" autocomplete="off"></div>
50378
+ <div class="cds-field"><label for="brandName">Brand name</label><input class="cds-input" id="brandName" type="text" placeholder="${esc13(d.name)}" autocomplete="off"></div>
49834
50379
  <div class="cds-field"><label for="logoSvg">Logo</label><div class="hint" id="logoSvg-hint">Paste the client's logo as SVG code. Their designer can send it.</div><input class="cds-input" id="logoSvg" type="text" placeholder="&lt;svg \u2026&gt;\u2026&lt;/svg&gt;" autocomplete="off" aria-describedby="logoSvg-hint"></div>
49835
50380
  <div class="cds-row"><button type="submit" class="cds-btn ghost" id="save">Save changes</button></div><div class="cds-field"><p class="hint" id="save-status" role="status" aria-live="polite"></p></div></form>`, "change-panel")}
49836
50381
  </div>
@@ -49897,7 +50442,7 @@ t.addEventListener("input",()=>{const v=t.value.trim();if(/^#[0-9a-f]{6}$/i.test
49897
50442
  document.getElementById("override").addEventListener("submit",async(e)=>{e.preventDefault();const g=(id)=>document.getElementById(id).value.trim();const body={slug:${scriptJson(d.slug)}};if(g("accent"))body.accent=g("accent");if(g("displayFont"))body.displayFont=g("displayFont");if(g("bodyFont"))body.bodyFont=g("bodyFont");if(g("brandName"))body.name=g("brandName");if(g("logoSvg"))body.logoSvg=g("logoSvg");if(Object.keys(body).length===1){say("save-status","Nothing to change.");return}const b=document.getElementById("save");b.disabled=true;try{await post("/api/brand/set",body);say("save-status","Saved.","ok");location.reload()}catch(err){say("save-status",err.message,"err");b.disabled=false}});
49898
50443
  `)}`;
49899
50444
  }
49900
- var esc12, STYLE, SHELL_JS, foot, SENTENCE_WORDS, pickerHex, swatch, brokenFontsNote, SHOW_FIRST, panel2;
50445
+ var esc13, STYLE, SHELL_JS, foot, SENTENCE_WORDS, pickerHex, swatch, brokenFontsNote, SHOW_FIRST, panel2;
49901
50446
  var init_brand_page = __esm({
49902
50447
  "src/command-os/design/brand-page.ts"() {
49903
50448
  "use strict";
@@ -49907,7 +50452,7 @@ var init_brand_page = __esm({
49907
50452
  init_cockpit_shell();
49908
50453
  init_script_json();
49909
50454
  init_svg_sanitize();
49910
- esc12 = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
50455
+ esc13 = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
49911
50456
  STYLE = `
49912
50457
  [hidden]{display:none!important}
49913
50458
  .bp-cards{display:grid;gap:var(--space-4);grid-template-columns:repeat(auto-fill,minmax(240px,1fr))}
@@ -49949,8 +50494,8 @@ for(const s of document.querySelectorAll("[data-ink]")){const v=s.dataset.ink;if
49949
50494
  if (/^#[0-9a-f]{8}$/i.test(h)) return h.slice(0, 7).toLowerCase();
49950
50495
  return "#000000";
49951
50496
  };
49952
- swatch = (hex, label2) => `<span data-sw="${esc12(hex)}" title="${esc12(label2)} ${esc12(hex)}" role="img" aria-label="${esc12(label2)} ${esc12(hex)}"></span>`;
49953
- brokenFontsNote = (display, body) => `<p>Fonts as read from the site: headings "${esc12(display)}", text "${esc12(body)}".</p>`;
50497
+ swatch = (hex, label2) => `<span data-sw="${esc13(hex)}" title="${esc13(label2)} ${esc13(hex)}" role="img" aria-label="${esc13(label2)} ${esc13(hex)}"></span>`;
50498
+ brokenFontsNote = (display, body) => `<p>Fonts as read from the site: headings "${esc13(display)}", text "${esc13(body)}".</p>`;
49954
50499
  SHOW_FIRST = 8;
49955
50500
  panel2 = (label2, title2, body, id = "") => `<section class="cds-panel"${id ? ` id="${id}" aria-labelledby="${id}-title"` : ""}><div class="cds-panel-head"><div class="txt"><div class="cds-panel-label">${label2}</div><h2 class="cds-panel-title"${id ? ` id="${id}-title"` : ""}>${title2}</h2></div></div>${body}</section>`;
49956
50501
  }
@@ -50160,9 +50705,9 @@ async function generateReview(spawnClaude, input) {
50160
50705
  function reviewDocumentHtml(review, opts) {
50161
50706
  const accent = opts.accent && /^#[0-9a-fA-F]{6}$/.test(opts.accent) ? opts.accent : "#B8934A";
50162
50707
  const agency = opts.agencyName?.trim() || "";
50163
- const esc16 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
50708
+ const esc17 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
50164
50709
  const date = (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, { year: "numeric", month: "long", day: "numeric" });
50165
- const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc16(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc16(g.cost)}</div>` : ""}${g.fix ? `<div class="gf">What we do: ${esc16(g.fix)}</div>` : ""}</div>`;
50710
+ const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc17(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc17(g.cost)}</div>` : ""}${g.fix ? `<div class="gf">What we do: ${esc17(g.fix)}</div>` : ""}</div>`;
50166
50711
  const o = review.offer;
50167
50712
  const confirmed = opts.scope && !opts.scope.wholePreset && !opts.scope.awaitingConfirmation ? opts.scope : void 0;
50168
50713
  const purchased = confirmed || opts.purchasedFeatureIds ? [.../* @__PURE__ */ new Set([
@@ -50172,8 +50717,8 @@ function reviewDocumentHtml(review, opts) {
50172
50717
  const unbuilt = opts.scope?.notBuilt ?? [];
50173
50718
  const represented = [...opts.scope?.built ?? [], ...unbuilt].map((f) => BUILD_ASSESSMENT_FEATURE[f.id]).concat(opts.scope && !opts.scope.awaitingConfirmation ? ["pipeline"] : []);
50174
50719
  const assessmentNext = opts.assessment && purchased ? assessmentFeatureLines(opts.assessment, [...purchased, ...represented]) : [];
50175
- const available = unbuilt.map((f) => `<div class="build"><b>${esc16(f.name)}</b><span>${esc16(f.what)}${f.figure ? ` <b>${esc16(f.figure)}</b>` : ""}</span></div>`).join("") + assessmentNext.map((line3) => `<p>${esc16(line3)}</p>`).join("");
50176
- return `<!doctype html><html><head><meta charset="utf-8"><title>${esc16(opts.clientName)} \u2014 Growth Blueprint</title><style>
50720
+ const available = unbuilt.map((f) => `<div class="build"><b>${esc17(f.name)}</b><span>${esc17(f.what)}${f.figure ? ` <b>${esc17(f.figure)}</b>` : ""}</span></div>`).join("") + assessmentNext.map((line3) => `<p>${esc17(line3)}</p>`).join("");
50721
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${esc17(opts.clientName)} \u2014 Growth Blueprint</title><style>
50177
50722
  @page{margin:0.6in}
50178
50723
  *{box-sizing:border-box}
50179
50724
  body{font-family:-apple-system,Segoe UI,Helvetica,sans-serif;color:#1f2430;margin:0;line-height:1.55}
@@ -50208,40 +50753,40 @@ function reviewDocumentHtml(review, opts) {
50208
50753
  <div class="wrap">
50209
50754
  <div class="cover">
50210
50755
  <p class="eyebrow">Growth Blueprint</p>
50211
- <h1>${esc16(opts.clientName)}</h1>
50212
- <p class="sub">${agency ? `Prepared by ${esc16(agency)} \xB7 ` : ""}${date}</p>
50756
+ <h1>${esc17(opts.clientName)}</h1>
50757
+ <p class="sub">${agency ? `Prepared by ${esc17(agency)} \xB7 ` : ""}${date}</p>
50213
50758
  </div>
50214
- ${opts.planLines?.length ? `<div class="head">Executable plan for approval</div>${opts.planLines.map((line3) => `<p style="white-space:pre-wrap">${esc16(line3)}</p>`).join("")}` : ""}
50215
- ${review.headline ? `<div class="lead">${esc16(review.headline)}</div>` : ""}
50759
+ ${opts.planLines?.length ? `<div class="head">Executable plan for approval</div>${opts.planLines.map((line3) => `<p style="white-space:pre-wrap">${esc17(line3)}</p>`).join("")}` : ""}
50760
+ ${review.headline ? `<div class="lead">${esc17(review.headline)}</div>` : ""}
50216
50761
  ${review.ica ? `<div class="head">Who you're really selling to</div>
50217
- <p>${esc16(review.ica.who)}</p>
50218
- ${review.ica.painPoints?.length ? `<p class="kv"><b>What keeps them up:</b> ${esc16(review.ica.painPoints.join(" \xB7 "))}</p>` : ""}
50219
- ${review.ica.objections?.length ? `<p class="kv"><b>Why they hesitate:</b> ${esc16(review.ica.objections.join(" \xB7 "))}</p>` : ""}
50220
- ${review.ica.buyingTrigger ? `<p class="kv"><b>What makes them finally buy:</b> ${esc16(review.ica.buyingTrigger)}</p>` : ""}
50221
- ${review.ica.whereTheyAre ? `<p class="kv"><b>Where they already are:</b> ${esc16(review.ica.whereTheyAre)}</p>` : ""}` : ""}
50762
+ <p>${esc17(review.ica.who)}</p>
50763
+ ${review.ica.painPoints?.length ? `<p class="kv"><b>What keeps them up:</b> ${esc17(review.ica.painPoints.join(" \xB7 "))}</p>` : ""}
50764
+ ${review.ica.objections?.length ? `<p class="kv"><b>Why they hesitate:</b> ${esc17(review.ica.objections.join(" \xB7 "))}</p>` : ""}
50765
+ ${review.ica.buyingTrigger ? `<p class="kv"><b>What makes them finally buy:</b> ${esc17(review.ica.buyingTrigger)}</p>` : ""}
50766
+ ${review.ica.whereTheyAre ? `<p class="kv"><b>Where they already are:</b> ${esc17(review.ica.whereTheyAre)}</p>` : ""}` : ""}
50222
50767
  ${o ? `<div class="head">Your offer, graded</div>
50223
50768
  <div class="scorebar"><span class="scorenum">${o.score}/10</span><span class="track"><span class="fill" style="width:${Math.max(0, Math.min(100, o.score * 10))}%"></span></span></div>
50224
- ${o.dreamOutcome ? `<p class="kv"><b>Worth wanting:</b> ${esc16(o.dreamOutcome)}</p>` : ""}
50225
- ${o.believability ? `<p class="kv"><b>Believable:</b> ${esc16(o.believability)}</p>` : ""}
50226
- ${o.speed ? `<p class="kv"><b>Speed to result:</b> ${esc16(o.speed)}</p>` : ""}
50227
- ${o.effort ? `<p class="kv"><b>Effort asked of them:</b> ${esc16(o.effort)}</p>` : ""}
50228
- ${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${esc16(o.biggestLever)}</p>` : ""}
50229
- ${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc16(o.recommendedOffer)}</div>` : ""}` : ""}
50769
+ ${o.dreamOutcome ? `<p class="kv"><b>Worth wanting:</b> ${esc17(o.dreamOutcome)}</p>` : ""}
50770
+ ${o.believability ? `<p class="kv"><b>Believable:</b> ${esc17(o.believability)}</p>` : ""}
50771
+ ${o.speed ? `<p class="kv"><b>Speed to result:</b> ${esc17(o.speed)}</p>` : ""}
50772
+ ${o.effort ? `<p class="kv"><b>Effort asked of them:</b> ${esc17(o.effort)}</p>` : ""}
50773
+ ${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${esc17(o.biggestLever)}</p>` : ""}
50774
+ ${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc17(o.recommendedOffer)}</div>` : ""}` : ""}
50230
50775
  ${review.gaps?.length ? `<div class="head">What's leaking today (${review.gaps.length})</div>${review.gaps.map(gap).join("")}` : ""}
50231
- ${review.build?.length ? `<div class="head">What we're building</div>${review.build.map((b) => `<div class="build"><b>${esc16(b.item)}</b><span>${esc16(b.because)}</span></div>`).join("")}` : ""}
50776
+ ${review.build?.length ? `<div class="head">What we're building</div>${review.build.map((b) => `<div class="build"><b>${esc17(b.item)}</b><span>${esc17(b.because)}</span></div>`).join("")}` : ""}
50232
50777
  ${opts.scope?.awaitingConfirmation ? `<div class="head">What this build includes</div>
50233
50778
  <p>Not confirmed yet. The build waits until the features to build are confirmed.</p>
50234
50779
  ` : opts.scope ? `<div class="head">What this build includes</div>
50235
- <p>${opts.scope.package ? `Built to your <b>${esc16(opts.scope.package)}</b> package: ` : ""}${opts.scope.built.length ? esc16(opts.scope.built.map((b) => b.name).join(", ")) : "the foundation only"}. Always included: the pipeline and its stages, the fields, tags and custom values, the intake form, the calendar, and the reply routing.</p>
50780
+ <p>${opts.scope.package ? `Built to your <b>${esc17(opts.scope.package)}</b> package: ` : ""}${opts.scope.built.length ? esc17(opts.scope.built.map((b) => b.name).join(", ")) : "the foundation only"}. Always included: the pipeline and its stages, the fields, tags and custom values, the intake form, the calendar, and the reply routing.</p>
50236
50781
  ` : ""}
50237
50782
  ${opts.bought ? `<div class="head">What you bought</div>
50238
- <p>${esc16(opts.bought.assessment)}</p>
50239
- ${opts.bought.confirmed ? opts.bought.bought.map((b) => `<div class="build"><b>${esc16(b.name)}</b><span>${esc16(b.means)}. ${esc16(b.delivery)}.</span></div>`).join("") : "<p>Not confirmed yet. The build waits until what you bought is saved.</p>"}
50240
- ${opts.bought.confirmed && opts.bought.stillToDo.length ? `<div class="head">Still to do</div><div class="ask"><ul>${opts.bought.stillToDo.map((line3) => `<li>${esc16(line3)}</li>`).join("")}</ul></div>` : ""}` : ""}
50241
- ${review.unknowns?.length ? `<div class="head">What we still need from you</div><div class="ask"><ul>${review.unknowns.map((u) => `<li>${esc16(u)}</li>`).join("")}</ul></div>` : ""}
50242
- ${opts.assessment ? `<div class="head">Assessment</div><p>Snapshot attached on ${esc16(opts.assessment.attachedAt.slice(0, 10))}.</p>${assessmentFeatureLines(opts.assessment, [], false).filter((line3) => line3.includes("by your own numbers")).map((line3) => `<p>${esc16(line3)}</p>`).join("")}` : ""}
50783
+ <p>${esc17(opts.bought.assessment)}</p>
50784
+ ${opts.bought.confirmed ? opts.bought.bought.map((b) => `<div class="build"><b>${esc17(b.name)}</b><span>${esc17(b.means)}. ${esc17(b.delivery)}.</span></div>`).join("") : "<p>Not confirmed yet. The build waits until what you bought is saved.</p>"}
50785
+ ${opts.bought.confirmed && opts.bought.stillToDo.length ? `<div class="head">Still to do</div><div class="ask"><ul>${opts.bought.stillToDo.map((line3) => `<li>${esc17(line3)}</li>`).join("")}</ul></div>` : ""}` : ""}
50786
+ ${review.unknowns?.length ? `<div class="head">What we still need from you</div><div class="ask"><ul>${review.unknowns.map((u) => `<li>${esc17(u)}</li>`).join("")}</ul></div>` : ""}
50787
+ ${opts.assessment ? `<div class="head">Assessment</div><p>Snapshot attached on ${esc17(opts.assessment.attachedAt.slice(0, 10))}.</p>${assessmentFeatureLines(opts.assessment, [], false).filter((line3) => line3.includes("by your own numbers")).map((line3) => `<p>${esc17(line3)}</p>`).join("")}` : ""}
50243
50788
  ${available ? `<div class="head">Available next</div>${available}` : opts.assessment ? `<div class="head">Available next</div><p>${purchased ? "Every listed feature is included in what you bought." : "What you bought has not been recorded yet."}</p>` : ""}
50244
- <footer><span>${agency ? esc16(agency) : "Growth Blueprint"}</span><span>${esc16(opts.clientName)} \xB7 ${date}</span></footer>
50789
+ <footer><span>${agency ? esc17(agency) : "Growth Blueprint"}</span><span>${esc17(opts.clientName)} \xB7 ${date}</span></footer>
50245
50790
  </div></body></html>`;
50246
50791
  }
50247
50792
  var GAP_CHECKLIST;
@@ -50361,7 +50906,7 @@ var init_client_fields = __esm({
50361
50906
  });
50362
50907
 
50363
50908
  // src/command-os/assessment/report.js
50364
- function esc13(s) {
50909
+ function esc14(s) {
50365
50910
  return String(s === void 0 || s === null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
50366
50911
  }
50367
50912
  function isFigure(v) {
@@ -50381,7 +50926,7 @@ function renderReport2(opts) {
50381
50926
  var assumptionLabels = opts.assumptionLabels || {};
50382
50927
  var date = opts.date || "";
50383
50928
  var accent = safeColour(agency.accent);
50384
- var biz = esc13(state.biz || "your business");
50929
+ var biz = esc14(state.biz || "your business");
50385
50930
  var F = Array.isArray(r.F) ? r.F : [];
50386
50931
  var O = Array.isArray(r.O) ? r.O : [];
50387
50932
  var hrs = Array.isArray(r.hrs) ? r.hrs : [];
@@ -50392,23 +50937,23 @@ function renderReport2(opts) {
50392
50937
  out.push("<title>Where " + biz + " is leaking</title>");
50393
50938
  out.push("<style>" + css(accent) + "</style></head><body><div class=wrap>");
50394
50939
  out.push("<header>");
50395
- if (agency.name) out.push('<p class="by">Prepared by ' + esc13(agency.name) + "</p>");
50940
+ if (agency.name) out.push('<p class="by">Prepared by ' + esc14(agency.name) + "</p>");
50396
50941
  out.push("<h1>Where " + biz + " is leaking</h1>");
50397
- var who = state.client && state.client.name ? esc13(state.client.name) + (state.client.role ? ", " + esc13(state.client.role) : "") : "";
50942
+ var who = state.client && state.client.name ? esc14(state.client.name) + (state.client.role ? ", " + esc14(state.client.role) : "") : "";
50398
50943
  if (who) out.push('<p class="for">Prepared for ' + who + "</p>");
50399
- if (date) out.push('<p class="when">' + esc13(date) + "</p>");
50944
+ if (date) out.push('<p class="when">' + esc14(date) + "</p>");
50400
50945
  if (agency.method || agency.promise) {
50401
- out.push('<p class="about">' + [agency.method, agency.promise].filter(Boolean).map(esc13).join(" ") + "</p>");
50946
+ out.push('<p class="about">' + [agency.method, agency.promise].filter(Boolean).map(esc14).join(" ") + "</p>");
50402
50947
  }
50403
50948
  out.push("</header>");
50404
50949
  if (r.verdict === "insufficient") {
50405
- out.push('<div class="verdict stop"><b>We could not put a number on this yet</b>' + esc13(r.line || "") + "</div>");
50950
+ out.push('<div class="verdict stop"><b>We could not put a number on this yet</b>' + esc14(r.line || "") + "</div>");
50406
50951
  out.push(measureFirst(O));
50407
50952
  out.push(footer(agency));
50408
50953
  return out.join("\n") + "</div></body></html>";
50409
50954
  }
50410
50955
  if (r.verdict === "thin") {
50411
- out.push('<div class="verdict warn"><b>Honestly, there is not much here</b>' + esc13(r.line || "") + "</div>");
50956
+ out.push('<div class="verdict warn"><b>Honestly, there is not much here</b>' + esc14(r.line || "") + "</div>");
50412
50957
  }
50413
50958
  out.push('<div class="hgrid">');
50414
50959
  if (r.hrsWk > 0) out.push(head3(r.hrsWk.toFixed(1), "hours a week going into work a system should do"));
@@ -50420,7 +50965,7 @@ function renderReport2(opts) {
50420
50965
  for (var a = 0; a < opts.answered.length; a++) {
50421
50966
  var q3 = opts.answered[a];
50422
50967
  var numeric = q3.unit !== void 0 ? q3.unit !== "text" : isFigure(q3.value);
50423
- out.push("<tr><td>" + esc13(q3.ask) + '</td><td class="' + (numeric ? "num" : "said") + '">' + (q3.unknown ? '<span class="unk">you did not know</span>' : esc13(q3.value)) + "</td></tr>");
50968
+ out.push("<tr><td>" + esc14(q3.ask) + '</td><td class="' + (numeric ? "num" : "said") + '">' + (q3.unknown ? '<span class="unk">you did not know</span>' : esc14(q3.value)) + "</td></tr>");
50424
50969
  }
50425
50970
  out.push("</table>");
50426
50971
  }
@@ -50429,15 +50974,15 @@ function renderReport2(opts) {
50429
50974
  out.push('<tr><th>What is happening</th><th class="num">A month</th></tr>');
50430
50975
  for (var i = 0; i < F.length; i++) {
50431
50976
  var f = F[i];
50432
- var note = f.assumption ? '<span class="assume">' + esc13(assumptionLabels[f.assumption] || "This is your assumption, not a measurement.") + "</span>" : "";
50433
- out.push("<tr><td><strong>" + esc13(f.title) + "</strong>" + note + '</td><td class="num">' + money6(f.monthly) + "</td></tr>");
50977
+ var note = f.assumption ? '<span class="assume">' + esc14(assumptionLabels[f.assumption] || "This is your assumption, not a measurement.") + "</span>" : "";
50978
+ out.push("<tr><td><strong>" + esc14(f.title) + "</strong>" + note + '</td><td class="num">' + money6(f.monthly) + "</td></tr>");
50434
50979
  }
50435
50980
  out.push('<tr class="tot"><td><strong>Leaking, every month</strong></td><td class="num"><strong>' + money6(r.monthly) + "</strong></td></tr>");
50436
50981
  if (r.stop > 0) {
50437
- out.push('<tr><td><strong>What we expect to actually stop</strong><span class="assume">' + esc13(assumptionLabels.recovery || "No system catches everything. This is your figure.") + '</span></td><td class="num"><strong>' + money6(r.stop) + "</strong></td></tr>");
50982
+ out.push('<tr><td><strong>What we expect to actually stop</strong><span class="assume">' + esc14(assumptionLabels.recovery || "No system catches everything. This is your figure.") + '</span></td><td class="num"><strong>' + money6(r.stop) + "</strong></td></tr>");
50438
50983
  }
50439
50984
  if (r.oneoff !== null && r.oneoff !== void 0 && r.oneoff > 0) {
50440
- out.push('<tr><td>Your dormant list, worked once<span class="assume">' + esc13(assumptionLabels.reactivation || "Your assumption, not a measurement.") + ' This is a one-off and is left out of every annual figure above.</span></td><td class="num">' + money6(r.oneoff) + "</td></tr>");
50985
+ out.push('<tr><td>Your dormant list, worked once<span class="assume">' + esc14(assumptionLabels.reactivation || "Your assumption, not a measurement.") + ' This is a one-off and is left out of every annual figure above.</span></td><td class="num">' + money6(r.oneoff) + "</td></tr>");
50441
50986
  }
50442
50987
  out.push("</table>");
50443
50988
  }
@@ -50448,14 +50993,14 @@ function renderReport2(opts) {
50448
50993
  out.push("<h2>Named, and deliberately not priced</h2><ul>");
50449
50994
  for (var j = 0; j < named.length; j++) {
50450
50995
  var o = named[j];
50451
- out.push("<li><strong>" + esc13(o.title) + "</strong>: " + esc13(o.declined ? "priced only on an assumption you chose not to make." : o.reason) + "</li>");
50996
+ out.push("<li><strong>" + esc14(o.title) + "</strong>: " + esc14(o.declined ? "priced only on an assumption you chose not to make." : o.reason) + "</li>");
50452
50997
  }
50453
50998
  out.push("</ul><p class=small>We can see these matter and we are not going to invent a number for them.</p>");
50454
50999
  }
50455
51000
  if (hrs.length) {
50456
51001
  out.push("<h2>Where the time goes</h2><table>");
50457
51002
  for (var h = 0; h < hrs.length; h++) {
50458
- out.push("<tr><td>" + esc13(hrs[h].t) + '</td><td class="num">' + Number(hrs[h].h).toFixed(1) + " hrs</td></tr>");
51003
+ out.push("<tr><td>" + esc14(hrs[h].t) + '</td><td class="num">' + Number(hrs[h].h).toFixed(1) + " hrs</td></tr>");
50459
51004
  }
50460
51005
  out.push('<tr class="tot"><td><strong>A week</strong></td><td class="num"><strong>' + Number(r.hrsWk).toFixed(1) + "</strong></td></tr></table>");
50461
51006
  }
@@ -50467,7 +51012,7 @@ function renderReport2(opts) {
50467
51012
  var svc = services.find(function(s) {
50468
51013
  return Array.isArray(s.covers) && s.covers.indexOf(fx.id) !== -1;
50469
51014
  });
50470
- out.push("<tr><td>" + esc13(fx.title) + "</td><td>" + esc13(svc ? svc.name : fx.fix || "") + "</td></tr>");
51015
+ out.push("<tr><td>" + esc14(fx.title) + "</td><td>" + esc14(svc ? svc.name : fx.fix || "") + "</td></tr>");
50471
51016
  }
50472
51017
  out.push("</table>");
50473
51018
  if (!services.length) {
@@ -50483,7 +51028,7 @@ function renderReport2(opts) {
50483
51028
  }, 0);
50484
51029
  out.push("<h2>The investment</h2><table>");
50485
51030
  for (var p = 0; p < priced.length; p++) {
50486
- out.push("<tr><td>" + esc13(priced[p].name) + '</td><td class="num">' + money6(priced[p].price) + "</td></tr>");
51031
+ out.push("<tr><td>" + esc14(priced[p].name) + '</td><td class="num">' + money6(priced[p].price) + "</td></tr>");
50487
51032
  }
50488
51033
  out.push('<tr class="tot"><td><strong>Total</strong></td><td class="num"><strong>' + money6(total) + "</strong></td></tr>");
50489
51034
  out.push('<tr><td>Against what we expect to stop, first year</td><td class="num">' + money6(r.annualStop) + "</td></tr>");
@@ -50498,7 +51043,7 @@ function renderReport2(opts) {
50498
51043
  return out.join("\n") + "</div></body></html>";
50499
51044
  }
50500
51045
  function head3(n, label2) {
50501
- return '<div><span class="hn">' + esc13(n) + '</span><span class="hl">' + esc13(label2) + "</span></div>";
51046
+ return '<div><span class="hn">' + esc14(n) + '</span><span class="hl">' + esc14(label2) + "</span></div>";
50502
51047
  }
50503
51048
  function measureFirst(O) {
50504
51049
  var seen = [];
@@ -50507,18 +51052,18 @@ function measureFirst(O) {
50507
51052
  for (var j = 0; j < m.length; j++) if (seen.indexOf(m[j]) === -1) seen.push(m[j]);
50508
51053
  }
50509
51054
  if (!seen.length) return "";
50510
- return "<h2>What to measure first</h2><p>These are the numbers that would let us price the rest. None of them need a system. A week of writing them down is enough.</p><ul><li>" + seen.map(esc13).join("</li><li>") + "</li></ul>";
51055
+ return "<h2>What to measure first</h2><p>These are the numbers that would let us price the rest. None of them need a system. A week of writing them down is enough.</p><ul><li>" + seen.map(esc14).join("</li><li>") + "</li></ul>";
50511
51056
  }
50512
51057
  function sourceLine(state, r, labels) {
50513
51058
  var used = [];
50514
51059
  var F = r.F || [];
50515
51060
  for (var i = 0; i < F.length; i++) if (F[i].assumption && used.indexOf(F[i].assumption) === -1) used.push(F[i].assumption);
50516
51061
  var n = used.length + (r.stop > 0 ? 1 : 0);
50517
- return '<p class="src">Every figure here came from what you told us' + (state.trade ? ", or from your own " + esc13(state.trade) + " business" : "") + ". " + (n > 0 ? n + " of them " + (n === 1 ? "is an estimate" : "are estimates") + " we chose together, each marked where it is used. " : "") + "Nothing here is an industry average applied to your business.</p>";
51062
+ return '<p class="src">Every figure here came from what you told us' + (state.trade ? ", or from your own " + esc14(state.trade) + " business" : "") + ". " + (n > 0 ? n + " of them " + (n === 1 ? "is an estimate" : "are estimates") + " we chose together, each marked where it is used. " : "") + "Nothing here is an industry average applied to your business.</p>";
50518
51063
  }
50519
51064
  function footer(agency) {
50520
51065
  if (!agency.name) return "";
50521
- return "<footer>" + esc13(agency.name) + (agency.contact ? " &middot; " + esc13(agency.contact) : "") + (agency.guarantee ? '<br><span class="guarantee">' + esc13(agency.guarantee) + "</span>" : "") + "</footer>";
51066
+ return "<footer>" + esc14(agency.name) + (agency.contact ? " &middot; " + esc14(agency.contact) : "") + (agency.guarantee ? '<br><span class="guarantee">' + esc14(agency.guarantee) + "</span>" : "") + "</footer>";
50522
51067
  }
50523
51068
  function css(accent) {
50524
51069
  return [
@@ -51788,9 +52333,11 @@ __export(dashboard_exports, {
51788
52333
  APPROVAL_WITHDRAWN_FOR_ASSESSMENT: () => APPROVAL_WITHDRAWN_FOR_ASSESSMENT,
51789
52334
  APPROVAL_WITHDRAWN_FOR_SCOPE: () => APPROVAL_WITHDRAWN_FOR_SCOPE,
51790
52335
  ASSESSMENT_CHANGED_SINCE_APPROVAL: () => ASSESSMENT_CHANGED_SINCE_APPROVAL,
52336
+ BRIEF_NEEDS_INTAKE: () => BRIEF_NEEDS_INTAKE2,
51791
52337
  CHOOSE_BEFORE_BUILD: () => CHOOSE_BEFORE_BUILD,
51792
52338
  CHOOSE_WHAT_THEY_BOUGHT: () => CHOOSE_WHAT_THEY_BOUGHT,
51793
52339
  CLAUDE_REPAIR_MESSAGES: () => CLAUDE_REPAIR_MESSAGES,
52340
+ DOC_NOT_SAVED: () => DOC_NOT_SAVED,
51794
52341
  INTAKE_ANSWERS_MISSING: () => INTAKE_ANSWERS_MISSING,
51795
52342
  INTAKE_CONNECT_REQUIRED: () => INTAKE_CONNECT_REQUIRED,
51796
52343
  INTAKE_FORM_REQUIRED: () => INTAKE_FORM_REQUIRED,
@@ -51841,10 +52388,13 @@ __export(dashboard_exports, {
51841
52388
  guarded: () => guarded,
51842
52389
  hasChosenScope: () => hasChosenScope,
51843
52390
  insideDir: () => insideDir,
52391
+ intakeErrorDetail: () => intakeErrorDetail,
51844
52392
  intakeInput: () => intakeInput,
52393
+ intakeRefusal: () => intakeRefusal,
51845
52394
  intakeRequestError: () => intakeRequestError,
51846
52395
  intakeUnavailableError: () => intakeUnavailableError,
51847
52396
  isAllowedCockpitHost: () => isAllowedCockpitHost,
52397
+ isBriefNeedsIntake: () => isBriefNeedsIntake,
51848
52398
  isLoopback: () => isLoopback,
51849
52399
  isUpstreamFailure: () => isUpstreamFailure,
51850
52400
  launcherAlreadyOffered: () => launcherAlreadyOffered,
@@ -51866,6 +52416,7 @@ __export(dashboard_exports, {
51866
52416
  readFailureText: () => readFailureText,
51867
52417
  readOverlay: () => readOverlay,
51868
52418
  readRunCosts: () => readRunCosts,
52419
+ readSavedClientDoc: () => readSavedClientDoc,
51869
52420
  readableAccount: () => readableAccount,
51870
52421
  recordLauncherOffered: () => recordLauncherOffered,
51871
52422
  recoverStuckStages: () => recoverStuckStages,
@@ -51887,8 +52438,11 @@ __export(dashboard_exports, {
51887
52438
  setClientScope: () => setClientScope,
51888
52439
  setPending: () => setPending,
51889
52440
  setStage: () => setStage,
52441
+ statusFromThisRun: () => statusFromThisRun,
51890
52442
  uniqueDocName: () => uniqueDocName,
51891
52443
  unknownCapabilityHtml: () => unknownCapabilityHtml,
52444
+ unverifiedStaffNames: () => unverifiedStaffNames,
52445
+ verifiedStaffNames: () => verifiedStaffNames,
51892
52446
  verifyPanelOutcome: () => verifyPanelOutcome,
51893
52447
  withBoughtJobs: () => withBoughtJobs,
51894
52448
  withCockpitShell: () => withCockpitShell,
@@ -52110,8 +52664,19 @@ async function resolvePlan2(creds) {
52110
52664
  }
52111
52665
  return { error: "Could not verify the license (offline and no valid cached attestation)." };
52112
52666
  }
52667
+ function unverifiedStaffNames(check) {
52668
+ return (check?.missing ?? []).map((m) => String(m.name ?? "").trim()).filter(Boolean);
52669
+ }
52670
+ function statusFromThisRun(status, runStartedAt) {
52671
+ if (!status?.readAt || !runStartedAt) return void 0;
52672
+ return Date.parse(status.readAt) >= Date.parse(runStartedAt) ? status : void 0;
52673
+ }
52674
+ function verifiedStaffNames(check) {
52675
+ return (check?.matched ?? []).map((m) => String(m.name ?? "").trim()).filter(Boolean);
52676
+ }
52113
52677
  function verifyPanelOutcome(value) {
52114
- return value.plain?.summary?.trim() ? value.plain : { summary: value.summary, issues: value.issues };
52678
+ const shown = value.plain?.summary?.trim() ? value.plain : { summary: value.summary, issues: value.issues };
52679
+ return { ...shown, summary: quietRemovedClaimLine(shown.summary) };
52115
52680
  }
52116
52681
  function startRun(locationId2, stage, onDone, meta2 = {}) {
52117
52682
  const rk = clientRecordKey(locationId2, meta2.clientKey);
@@ -52145,6 +52710,8 @@ function startRun(locationId2, stage, onDone, meta2 = {}) {
52145
52710
  // A build re-run reuses the approved plan already saved for this account (finding 10).
52146
52711
  // ...and only while it still builds the scope the member has confirmed now: a
52147
52712
  // changed selection composes the plan fresh from the preset (delta round 3, T-M4).
52713
+ // Rehearsal 5 (BUG-2026-09-30-10): Build is told who the staff check matched in this account.
52714
+ ...stage === 3 ? { verifiedStaff: verifiedStaffNames(state.staffChecks?.[locationId2]) } : {},
52148
52715
  savedPlan: (() => {
52149
52716
  const sp = stage === 3 ? readSavedPlan(rk) : null;
52150
52717
  return sp && sp.inputHash === meta2.inputHash && savedPlanMatchesScope(sp, state.scope?.[rk]) ? { planId: sp.planId, savedAt: sp.savedAt, summary: planSummary(sp) } : void 0;
@@ -52220,6 +52787,12 @@ function startRun(locationId2, stage, onDone, meta2 = {}) {
52220
52787
  outcome.issues = [];
52221
52788
  outcome.plain = { summary: outcome.error, issues: [] };
52222
52789
  }
52790
+ const briefNeedsIntake = stage === 1 && !outcome.ok && isBriefNeedsIntake(outcome.error);
52791
+ if (briefNeedsIntake) {
52792
+ outcome.summary = BRIEF_NEEDS_INTAKE2;
52793
+ outcome.issues = [];
52794
+ outcome.plain = { summary: BRIEF_NEEDS_INTAKE2, issues: [] };
52795
+ }
52223
52796
  if (stage === 3 && !outcome.summary && !outcome.issues?.length) {
52224
52797
  try {
52225
52798
  const st = readCockpitState();
@@ -52227,19 +52800,22 @@ function startRun(locationId2, stage, onDone, meta2 = {}) {
52227
52800
  } catch {
52228
52801
  }
52229
52802
  }
52803
+ const buildFacts = stage === 3 ? { verifiedStaff: context2.verifiedStaff ?? [], unverifiedStaff: unverifiedStaffNames(state.staffChecks?.[locationId2]), customValues: statusFromThisRun(readSavedPlan(rk)?.customValueStatus, run.startedAt) } : void 0;
52804
+ if (buildFacts) Object.assign(outcome, guardBuildReport({ summary: outcome.summary, issues: outcome.issues }, buildFacts));
52230
52805
  if (stage === 4 || outcome.summary || outcome.issues?.length) {
52231
52806
  const at = (/* @__PURE__ */ new Date()).toISOString();
52232
52807
  const save = (plain) => {
52233
52808
  const st = readCockpitState();
52234
- writeCockpitState({ ...st, outcomes: { ...st.outcomes ?? {}, [`${rk}:${stage}`]: { ok: outcome.ok, error: outcome.error, summary: outcome.summary, issues: outcome.issues, ...plain && { plain }, at, inputHash: runInputHash, ...stage === 4 ? { buildHash: runBuildHash, verification: verifyRaw?.verification } : {}, hash: contentHash({ inputHash: runInputHash, at, ok: outcome.ok, summary: outcome.summary, issues: outcome.issues }) } } });
52809
+ 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: contentHash({ inputHash: runInputHash, at, ok: outcome.ok, summary: outcome.summary, issues: outcome.issues }) } } });
52235
52810
  };
52236
- save(scopeRefusal ? outcome.plain : void 0);
52237
- if (PLAIN_REPORT_STAGES.has(stage) && !scopeRefusal) {
52811
+ save(scopeRefusal || briefNeedsIntake ? outcome.plain : void 0);
52812
+ if (PLAIN_REPORT_STAGES.has(stage) && !scopeRefusal && !briefNeedsIntake) {
52238
52813
  run.lines.push("Writing the plain-English version of what came back\u2026");
52239
- const plain = await rewriteOutcomePlain(STAGES2[stage], verifyRaw ?? outcome, { onEvent: (l) => run.lines.push(l), onCost: (costUsd) => {
52814
+ const rewritten = await rewriteOutcomePlain(STAGES2[stage], verifyRaw ?? outcome, { onEvent: (l) => run.lines.push(l), onCost: (costUsd) => {
52240
52815
  recordRunCost({ at, locationId: locationId2, stage, kind: "plain rewrite", ok: true, costUsd });
52241
52816
  run.lines.push(`Plain rewrite: ${costUsd === void 0 ? "cost not reported" : formatUsd(costUsd)}.`);
52242
52817
  } });
52818
+ const plain = buildFacts ? guardBuildReport(rewritten, buildFacts) : rewritten;
52243
52819
  save(plain);
52244
52820
  }
52245
52821
  }
@@ -52408,7 +52984,7 @@ function clientDocsDir(locationId2, base = appDataDir()) {
52408
52984
  return path40.join(base, "clients", safeLoc, "documents");
52409
52985
  }
52410
52986
  function safeDocName(name) {
52411
- const cleaned = path40.basename(String(name ?? "")).replace(/[^A-Za-z0-9._ -]/g, "_").replace(/\s+/g, " ").trim();
52987
+ const cleaned = path40.basename(String(name ?? "")).replace(/[^A-Za-z0-9._ ()-]/g, "_").replace(/\s+/g, " ").trim();
52412
52988
  const ext = path40.extname(cleaned).toLowerCase();
52413
52989
  if (!cleaned || cleaned.startsWith(".") || !DOC_EXTS.has(ext)) throw new Error("Only plain text (.txt, .md, .csv), .vtt and .srt files can be saved here.");
52414
52990
  const stem = cleaned.slice(0, cleaned.length - ext.length).slice(0, 120 - ext.length).trim();
@@ -52569,6 +53145,16 @@ function insideDir(dir, p) {
52569
53145
  return false;
52570
53146
  }
52571
53147
  }
53148
+ function readSavedClientDoc(locationId2, file2, base = appDataDir()) {
53149
+ const name = safeDocName(file2);
53150
+ const dir = clientDocsDir(locationId2, base);
53151
+ const p = path40.join(dir, name);
53152
+ if (!fs42.existsSync(p)) throw new Error(DOC_NOT_SAVED);
53153
+ if (fs42.lstatSync(p).isSymbolicLink() || !insideDir(dir, p)) throw new Error(DOC_NOT_SAVED);
53154
+ const text2 = fs42.readFileSync(p, "utf8");
53155
+ if (text2.length > 6e4) throw new Error("The limit is 60,000 characters. Paste an excerpt or shorten this text file.");
53156
+ return text2;
53157
+ }
52572
53158
  function uniqueDocName(dir, name) {
52573
53159
  const ext = path40.extname(name), stem = name.slice(0, name.length - ext.length);
52574
53160
  let candidate = name;
@@ -52607,9 +53193,12 @@ function intakeUnavailableError(snap) {
52607
53193
  const reason3 = snap.unavailableReason;
52608
53194
  return new Error(reason3 === "auth" ? INTAKE_UNREADABLE : reason3 === "missing" ? INTAKE_ANSWERS_MISSING : reason3 === "error" ? INTAKE_READ_FAILED : INTAKE_READ_TRANSIENT);
52609
53195
  }
53196
+ function isBriefNeedsIntake(error) {
53197
+ return typeof error === "string" && /^\s*First complete the intake\b/i.test(error);
53198
+ }
52610
53199
  function runRefusal(e) {
52611
53200
  const error = e && typeof e.message === "string" ? e.message : "bad request";
52612
- return error === INTAKE_ANSWERS_MISSING ? { ok: false, error, reason: "intake-missing" } : error === RUN_IN_PROGRESS ? { ok: false, error, reason: "busy" } : { ok: false, error };
53201
+ return error === INTAKE_ANSWERS_MISSING ? { ok: false, error, reason: "intake-missing" } : error === RUN_IN_PROGRESS ? { ok: false, error, reason: "busy" } : error === BRIEF_NEEDS_INTAKE2 ? { ok: false, error, reason: "intake-needed" } : { ok: false, error };
52613
53202
  }
52614
53203
  function planClaimChecks(saved) {
52615
53204
  const plan = saved?.plan && typeof saved.plan === "object" ? saved.plan : void 0;
@@ -52642,6 +53231,16 @@ function intakeRequestError(e) {
52642
53231
  const text2 = e instanceof Error ? e.message : "";
52643
53232
  return INTAKE_OWN_MESSAGES.includes(text2) ? text2 : "The intake request did not finish. Retry this step.";
52644
53233
  }
53234
+ function intakeErrorDetail(e) {
53235
+ const raw = e instanceof Error ? e.message : typeof e === "string" ? e : "";
53236
+ if (!raw || INTAKE_OWN_MESSAGES.includes(raw)) return void 0;
53237
+ const clean4 = 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();
53238
+ return clean4 ? clean4.length > 240 ? clean4.slice(0, 237) + "\u2026" : clean4 : void 0;
53239
+ }
53240
+ function intakeRefusal(e) {
53241
+ const detail = intakeErrorDetail(e);
53242
+ return { ok: false, error: intakeRequestError(e), ...detail ? { detail } : {} };
53243
+ }
52645
53244
  function escHtml2(s) {
52646
53245
  return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
52647
53246
  }
@@ -52794,6 +53393,10 @@ function initials(name){const p=String(name||"").trim().split(/\\s+/);return ((p
52794
53393
  /* Command Design System pieces, built as text (never markup from data). */
52795
53394
  function mk(tag,cls,text){const e=document.createElement(tag);if(cls)e.className=cls;if(text!=null)e.textContent=text;return e}
52796
53395
  function chip(tone,text){return mk("span","cds-chip"+(tone?" "+tone:""),text)}
53396
+ /* Sentences joined for display (rehearsal 5, BUG-2026-09-30-17): a part that already ends a sentence
53397
+ gets no second full stop, so "cannot send." + "Buy a local..." never reads "cannot send.. Buy". */
53398
+ function endSentence(s){const t=String(s==null?"":s).trim();return !t||/[.!?]["')\\]]?$/.test(t)?t:t+"."}
53399
+ function joinSentences(...parts){const xs=parts.map(p=>String(p==null?"":p).trim()).filter(Boolean);return xs.map((p,i)=>i<xs.length-1?endSentence(p):p).join(" ")}
52797
53400
  function panel(label,title,meta){const s=mk("section","cds-panel");const h=mk("div","cds-panel-head");const t=mk("div","txt");
52798
53401
  if(label)t.appendChild(mk("div","cds-panel-label",label));if(title)t.appendChild(mk("h2","cds-panel-title",title));h.appendChild(t);
52799
53402
  if(meta){const m=mk("div","cds-panel-meta");m.appendChild(meta);h.appendChild(m)}s.appendChild(h);return s}
@@ -52966,7 +53569,7 @@ function preparedFormBlock(box,id){const p=iPrepared[id];
52966
53569
  "Left blank because GoHighLevel's form will not accept a 555-01xx test number. Type a real number there, or leave it empty":"left blank, because GoHighLevel's form will not take a 555-01xx test number. Type a real number there, or leave it empty"};
52967
53570
  for(const l of left){const li=document.createElement("li");li.textContent=l.label+": ";
52968
53571
  l.values.forEach((v,i)=>{if(i)li.appendChild(document.createTextNode(", "));const val=mk("span","copy-val",String(v));li.appendChild(val);li.appendChild(document.createTextNode(" "));li.appendChild(copyButton(String(v),val))});
52969
- li.appendChild(document.createTextNode(". "+(WHY[l.reason]||l.reason)+"."));ul.appendChild(li)}
53572
+ li.appendChild(document.createTextNode(". "+endSentence(WHY[l.reason]||l.reason)));ul.appendChild(li)}
52970
53573
  box.appendChild(ul)}
52971
53574
  const rf=document.createElement("button");rf.className="cds-btn ghost";rf.textContent="Refresh status";
52972
53575
  rf.onclick=()=>iLoad(id);box.appendChild(rf)}
@@ -52994,7 +53597,8 @@ function outcomePanel(id,i,state){const oc=(state.outcomes||{})[id+":"+i];if(!oc
52994
53597
  t.textContent="Every planned item: "+part.split(NL).filter(l=>l.startsWith(" ")&&l.trim().length>0).length+" read back from the account";box.appendChild(t);
52995
53598
  const body=document.createElement("p");body.className="runlog";body.textContent=part.split(NL).slice(1).join(NL);
52996
53599
  box.appendChild(body);d.appendChild(box);continue}
52997
- const p=document.createElement("p");if(i===4)p.className="runlog";p.textContent=part;d.appendChild(p)}}
53600
+ /* Notes from the check are background, drawn quietly (test 01, BUG-2026-09-30-36). */
53601
+ const p=document.createElement("p");if(i===4)p.className=/^Notes from the check/.test(part)?"runlog muted small vnotes":"runlog";p.textContent=part;d.appendChild(p)}}
52998
53602
  for(const x of iss){const g=document.createElement("div");g.className="cds-callout warn pendpanel";
52999
53603
  g.textContent=x;d.appendChild(g)}
53000
53604
  const when=document.createElement("p");when.className="cds-src";
@@ -53058,7 +53662,7 @@ let iFieldN=0;
53058
53662
  function iField(id,q,val,prefilled){const w=document.createElement("div");w.className="cds-field ifield"+(prefilled?" prefilled":"");
53059
53663
  const fid="ifield-"+(++iFieldN);
53060
53664
  const l=document.createElement("label");l.htmlFor=fid;l.textContent=q.label+(q.required?" *":"");if(prefilled)l.setAttribute("data-src",iPreSrc[id]||"your call");w.appendChild(l);
53061
- if(prefilled)w.appendChild(mk("div","hint","Filled in from "+(iPreSrc[id]||"your call")+". Check it."));
53665
+ if(prefilled)w.appendChild(mk("div","hint","Filled in from "+iSrcWords(id)+". Check it."));
53062
53666
  let inp;
53063
53667
  if(q.multi&&q.options&&q.options.length){
53064
53668
  // Several answers allowed (the GHL field is multi-choice) . checkboxes, stored as a list. (2026-08-25, finding 8)
@@ -53152,17 +53756,13 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
53152
53756
  :"Your own Claude asks the recorder you connected (Plaud, Otter, Fireflies\u2026) for that call; the transcript then goes to your Claude account and nowhere else.";
53153
53757
  const r=await iWork(id,async()=>(await clientPageRequest("/api/intake-prefill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)})).json());
53154
53758
  b.disabled=false;b.textContent="Fill the intake from this";
53155
- if(!r.ok){st.textContent=clientPageError(r.error,"Check the source and try reading it again.");return}
53156
- if(r.costs)iData[id].costs=r.costs;iPre[id]=iApplyProposals(id,r.answers);iPreSrc[id]=sub==="url"?"the link":"your call";for(const n of ["transcript","url","recorder"])delete iDraft[id+":"+n];
53759
+ if(!r.ok){iFailNote(st,r);return}
53760
+ for(const n of ["transcript","url","recorder"])delete iDraft[id+":"+n];
53157
53761
  /* Codex F2 (2026-09-06): a HELD answer (the words were said, the value was worked out) is a
53158
53762
  suggestion beside the field, never an answer. It reaches iAnswers only when the operator
53159
- clicks "Use it". Unverified answers (read by their Claude, not pasted here) are answers,
53160
- but the banner names them. */
53161
-
53162
- iHeld[id]=r.held||{};iUnv[id]=r.unverified||[];
53163
- const heldKeys=Object.keys(iHeld[id]),unv=iUnv[id];
53164
- iNote[id]=(heldKeys.length?heldKeys.length+" answer"+(heldKeys.length===1?" was":"s were")+" worked out rather than heard and NOT filled in: "+heldKeys.join(", ")+". Each is shown beside its field with a Use it button. ":"")+(unv.length?unv.length+" could not be checked against the material because your Claude read it rather than you pasting it here: "+unv.join(", ")+". Check them.":"");
53165
- iMode[id]="fill";render(lastState,lastRun)};
53763
+ clicks "Use it" or "Use all suggestions". Unverified answers (read by their Claude, not
53764
+ pasted here) are answers, but the banner names them. */
53765
+ iAfterFill(id,r,sub==="url"?"the link":"your call")};
53166
53766
  b.onclick=()=>readIt(false);icAfter(b,limitKey);
53167
53767
  const urlIn=document.createElement("input");urlIn.placeholder="https://\u2026 a shared transcript or recording page";
53168
53768
  urlIn.className="cds-input";urlIn.setAttribute("aria-label","Link to the transcript or recording");keepDraft(urlIn,id,"url");
@@ -53198,8 +53798,8 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
53198
53798
  rb.onclick=async()=>{rb.disabled=true;rb.textContent="Reading it\u2026";st.textContent="The text of "+f.name+" goes to your own Claude account to be read, and nowhere else.";
53199
53799
  const r=await iWork(id,async()=>(await clientPageRequest("/api/intake-prefill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:id,labels:d.questions.map(q=>q.label),file:f.name})})).json());
53200
53800
  rb.disabled=false;rb.textContent="Fill the intake from this";
53201
- if(!r.ok){st.textContent=clientPageError(r.error,"Check the source and try reading it again.");return}
53202
- if(r.costs)iData[id].costs=r.costs;iPre[id]=iApplyProposals(id,r.answers);iPreSrc[id]="the document";iHeld[id]=r.held||{};iUnv[id]=r.unverified||[];const heldKeys=Object.keys(iHeld[id]),unv=iUnv[id];iNote[id]=(heldKeys.length?heldKeys.length+" answer"+(heldKeys.length===1?" was":"s were")+" worked out rather than heard and NOT filled in: "+heldKeys.join(", ")+". Each is shown beside its field with a Use it button. ":"")+(unv.length?unv.length+" could not be checked against the material because your Claude read it rather than you pasting it here: "+unv.join(", ")+". Check them.":"");iMode[id]="fill";render(lastState,lastRun)};
53801
+ if(!r.ok){iFailNote(st,r);return}
53802
+ iAfterFill(id,r,"the document")};
53203
53803
  const end=mk("span","end");end.appendChild(rb);row.appendChild(end);list.appendChild(row)}
53204
53804
 
53205
53805
  w.appendChild(list);
@@ -53209,9 +53809,7 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
53209
53809
  if(iSub[id]!=="doc"){w.appendChild(b);w.appendChild(st)}box.appendChild(w)}
53210
53810
  if(mode==="fill"||mode==="interview"){
53211
53811
  const qs=d.questions;const ans=iAnswers[id]||{};const pre=iPre[id]||[];
53212
- if(pre.length&&mode==="fill"){const banner=document.createElement("div");banner.className="cds-callout info pendpanel";
53213
- banner.textContent=pre.length+" answers pre-filled from "+(iPreSrc[id]||"your call")+". Correct anything wrong; "+(qs.length-pre.length)+" still need you.";box.appendChild(banner)}
53214
- if(mode==="fill"&&iNote[id]){const nb=document.createElement("div");nb.className="cds-callout warn pendpanel";nb.textContent=iNote[id];box.appendChild(nb)}
53812
+ if(mode==="fill")iFillBanners(id,qs,box);
53215
53813
  if(mode==="interview"){const i=Math.min(iIdx[id]||0,qs.length-1);const q=qs[i];
53216
53814
  const prog=document.createElement("p");prog.className="cds-panel-label";prog.textContent="Question "+(i+1)+" of "+qs.length+" \xB7 "+q.sectionLabel;
53217
53815
  const qq=document.createElement("div");qq.className="cds-panel-title iq";qq.textContent=q.label;
@@ -53231,13 +53829,13 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
53231
53829
  {const rp=iReplacePanel(id,q.label);if(rp)box.appendChild(rp)}
53232
53830
  const held=(iHeld[id]||{})[q.label];
53233
53831
  if(held){const hr=document.createElement("div");hr.className="cds-callout warn cds-row";
53234
- const ht=document.createElement("span");ht.textContent="Worked out from your call, not heard: \u201C"+held.value+"\u201D. "+(held.because||"");hr.appendChild(ht);
53832
+ const ht=document.createElement("span");ht.textContent="Suggested from "+iSrcWords(id)+": \u201C"+iShowVal(held.value)+"\u201D. "+(held.because||"");hr.appendChild(ht);
53235
53833
  const use=document.createElement("button");use.className="cds-btn ghost";use.textContent="Use it";
53236
53834
  const heldKey="replaceheld:"+id+":"+q.label;
53237
53835
  const dropHeld=()=>{const h=Object.assign({},iHeld[id]);delete h[q.label];iHeld[id]=h};
53238
53836
  use.onclick=()=>{const cur=(iAnswers[id]||{})[q.label];
53239
53837
  if(iConflicts(iAnswers[id]||{},q.label,held.value))return icShow(hr,heldKey,iReplaceSpec(id,q.label,cur,held.value,dropHeld));
53240
- iApplyProposals(id,{[q.label]:held.value});dropHeld();render(lastState,lastRun)};
53838
+ iUseHeld(id,{[q.label]:held.value});dropHeld();render(lastState,lastRun)};
53241
53839
  hr.appendChild(use);box.appendChild(icAfter(hr,heldKey))}}
53242
53840
  if(hasForm){const save=document.createElement("button");save.className=d.connectionPending?"cds-btn ghost":"cds-btn";
53243
53841
  save.textContent="Review and submit the real form";save.disabled=Boolean(d.connectionPending);
@@ -53264,6 +53862,45 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
53264
53862
  if(hasForm&&iPrepared[id])preparedFormBlock(box,id)}}
53265
53863
  return box}
53266
53864
  let iNote={},iHeld={},iUnv={};
53865
+ /* A failed read says the plain sentence, and the real reason under Details (BUG-2026-09-30-38). */
53866
+ function iFailNote(st,r){st.textContent=clientPageError(r&&r.error,"Check the source and try reading it again.");
53867
+ if(r&&r.detail){const d=mk("details","cds-details ifaildetail");d.appendChild(mk("summary","","Details"));d.appendChild(mk("p","small",r.detail));st.appendChild(document.createTextNode(" "));st.appendChild(d)}}
53868
+ /* What the intake was filled from, in the banner's words. */
53869
+ /* One word for the source in every banner line (review nit): your notes for a document, the transcript otherwise. */
53870
+ function iSrcWords(id){return (iPreSrc[id]||"your call")==="the document"?"your notes":"the transcript"}
53871
+ /* After Fill the intake from this (test 01 walkthrough, BUG-2026-09-30-35): the stated answers land, the
53872
+ suggestions wait beside their fields, and the banners are drawn from what is on screen at each redraw. */
53873
+ function iAfterFill(id,r,src){if(r.costs)iData[id].costs=r.costs;iPre[id]=iApplyProposals(id,r.answers);iPreSrc[id]=src;
53874
+ iHeld[id]=r.held||{};iHeldLeft[id]=0;iUnv[id]=r.unverified||[];const unv=iUnv[id];
53875
+ iNote[id]=unv.length?unv.length+" could not be checked against the material because your Claude read it rather than you pasting it here: "+unv.join(", ")+". Check them.":"";
53876
+ iMode[id]="fill";render(lastState,lastRun)}
53877
+ /* A suggestion the operator took counts as filled from the source, and is marked to check like the rest. */
53878
+ function iUseHeld(id,proposals){const applied=iApplyProposals(id,proposals);const pre=iPre[id]||(iPre[id]=[]);for(const l of applied)if(!pre.includes(l))pre.push(l);return applied}
53879
+ function iHasAnswer(v){return Array.isArray(v)?v.length>0:String(v==null?"":v).trim()!==""}
53880
+ /* Use all suggestions (BUG-2026-09-30-34): every suggestion that would not replace an answer already there.
53881
+ The ones that would replace one stay beside their fields for the person to decide. Returns how many stayed. */
53882
+ function iUseAllHeld(id){const held=iHeld[id]||{};const answers=iAnswers[id]||{};const keep={};const take={};
53883
+ /* The claims guard's questions (spots running out, the 72-hour price) are never taken in bulk (review): each waits for its own Use it. */
53884
+ for(const [label,h] of Object.entries(held)){if(h.claims||iConflicts(answers,label,h.value))keep[label]=h;else take[label]=h.value}
53885
+ iUseHeld(id,take);iHeld[id]=keep;return Object.keys(keep).length}
53886
+ /* The fill banners, counted from what is on screen: filled, suggestions waiting for a click, and what the
53887
+ source did not answer at all, by name. */
53888
+ function iFillBanners(id,qs,box){const pre=iPre[id]||[];const held=iHeld[id]||{};const ans=iAnswers[id]||{};const heldN=Object.keys(held).length;
53889
+ if(!pre.length&&!heldN){if(iNote[id]){const nb=mk("div","cds-callout warn pendpanel",iNote[id]);box.appendChild(nb)}return}
53890
+ const missing=qs.filter(q=>!iHasAnswer(ans[q.label])&&!held[q.label]).map(q=>q.label);
53891
+ const src=iSrcWords(id);
53892
+ const banner=mk("div","cds-callout info pendpanel ifillbanner");
53893
+ banner.appendChild(mk("p","",[pre.length+" filled from "+src+". Correct anything wrong.",heldN?heldN+" suggested, waiting for your click.":"",missing.length?missing.length+" not in "+src+": listed below.":"Nothing else is missing."].filter(Boolean).join(" ")));
53894
+ if(missing.length){const ul=mk("ul","cds-list imissing");for(const l of missing)ul.appendChild(mk("li","",l));banner.appendChild(ul)}
53895
+ box.appendChild(banner);
53896
+ if(heldN){const nb=mk("div","cds-callout warn pendpanel cds-stack iheldbanner");
53897
+ nb.appendChild(mk("p","",heldN+" more "+(heldN===1?"answer was":"answers were")+" suggested from "+src+". Check "+(heldN===1?"it":"them")+" and click Use all suggestions, or Use it on each."));
53898
+ const all=mk("button","cds-btn","Use all suggestions");all.type="button";
53899
+ all.onclick=()=>{const left=iUseAllHeld(id);iHeldLeft[id]=left;render(lastState,lastRun)};nb.appendChild(all);
53900
+ if(iHeldLeft[id])nb.appendChild(mk("p","muted",iHeldLeft[id]+" "+(iHeldLeft[id]===1?"suggestion was":"suggestions were")+" not used because "+(iHeldLeft[id]===1?"it":"they")+" would replace an answer already there, or decide"+(iHeldLeft[id]===1?"s":"")+" what the texts may promise. Use it on each to choose."));
53901
+ box.appendChild(nb)}
53902
+ if(iNote[id])box.appendChild(mk("div","cds-callout warn pendpanel",iNote[id]))}
53903
+ let iHeldLeft={};
53267
53904
  ${ASSESSMENT_PANEL_JS}
53268
53905
  let rOpen={},rData={},rBusy={},rSeen={};
53269
53906
  let profCache=null,profLoading=false;
@@ -53379,7 +54016,7 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
53379
54016
  if((r.gaps||[]).length){const s3=document.createElement("div");s3.className="cds-stack";const gh=document.createElement("h4");gh.className="cds-panel-label";gh.textContent="What's missing ("+r.gaps.length+")";s3.appendChild(gh);
53380
54017
  const gl=mk("ul","cds-list gaps");
53381
54018
  for(const g of r.gaps){const gg=mk("li","gap "+(g.priority||"soon"));
53382
- const pri=g.priority||"soon";const gt=mk("span","ttl",String(g.title||""));gt.appendChild(mk("span","sub",moneyText((g.cost?g.cost+". ":"")+(g.fix||""))));gg.appendChild(gt);
54019
+ const pri=g.priority||"soon";const gt=mk("span","ttl",String(g.title||""));gt.appendChild(mk("span","sub",moneyText(joinSentences(g.cost,g.fix))));gg.appendChild(gt);
53383
54020
  const ge=mk("span","end");ge.appendChild(chip(pri==="now"?"warn":"",pri==="now"?"Fix now":pri==="later"?"Later":"Soon"));gg.appendChild(ge);gl.appendChild(gg)}
53384
54021
  s3.appendChild(gl);
53385
54022
  d.appendChild(s3)}
@@ -53451,6 +54088,13 @@ b.textContent=answer?"Yes":"No";
53451
54088
  let agencyReady=false;
53452
54089
  function ncField(label,control,hint,full){const w=mk("div","cds-field"+(full?" ncfull":""));const l=mk("label","",label);l.htmlFor=control.id;w.appendChild(l);
53453
54090
  if(hint){const h=mk("div","hint",hint);h.id=control.id+"-hint";control.setAttribute("aria-describedby",h.id);w.appendChild(h)}w.appendChild(control);return w}
54091
+ /* What was typed on Build a client account, by field id (test 01 walkthrough, BUG-2026-09-30-32). The screen
54092
+ draws again when late data arrives (the snapshot list, the agency check), and a sub-account id typed in the
54093
+ first seconds was wiped. Each field takes its typed value back when drawn; a successful create or register
54094
+ clears it. */
54095
+ let ncDraft={};
54096
+ function ncKeep(el){const k=el.id;if(ncDraft[k]!=null)el.value=ncDraft[k];const save=()=>{ncDraft[k]=el.value};el.addEventListener("input",save);el.addEventListener("change",save);return el}
54097
+ function ncForget(ids){for(const k of ids)delete ncDraft[k]}
53454
54098
  function newClientForm(){const f=document.createElement("div");f.className="cds-panel ncform";
53455
54099
  f.innerHTML="<h2 class='cds-panel-title'>Create the sub-account</h2><p>GHL Command creates sub-accounts on GoHighLevel's Agency Pro plan; on other plans you create it in GoHighLevel and paste its key into Command OS.</p>";
53456
54100
  const g=document.createElement("div");g.className="ncgrid";
@@ -53460,6 +54104,7 @@ function newClientForm(){const f=document.createElement("div");f.className="cds-
53460
54104
  const snap=document.createElement("select");snap.id="nc-snapshot";snap.innerHTML='<option value="">No snapshot (blank account)</option>';snap.className="cds-select nccontrol";
53461
54105
  if(snapshots)for(const sn of snapshots){const e=document.createElement("option");e.value=sn.id;e.textContent=(sn.type==="own"?"Yours: ":"")+sn.name;snap.appendChild(e)}
53462
54106
  const fn=mkIn("nc-first"),ln=mkIn("nc-last"),em=mkIn("nc-email"),city=mkIn("nc-city"),st=mkIn("nc-state"),tz=mkIn("nc-timezone");em.type="email";tz.value=${scriptJson(readAgencyProfile().defaults?.timezone || "")};
54107
+ for(const el of [name,ind,snap,fn,ln,em,city,st,tz])ncKeep(el);
53463
54108
  for(const [label,el,hint,full] of [["Business name *",name,"",true],["Industry *",ind,"Tailors this client's intake."],["Snapshot",snap,"",false],["Owner first name",fn],["Owner last name",ln],["Owner email",em,"",true],["City",city],["State",st],["Timezone",tz,"Confirm this client\u2019s timezone for appointment times. For example America/Phoenix.",true]])g.appendChild(ncField(label,el,hint,full));
53464
54109
  f.appendChild(g);
53465
54110
  if(snapshotError)showClientError(snapshotError,f);
@@ -53467,7 +54112,7 @@ function newClientForm(){const f=document.createElement("div");f.className="cds-
53467
54112
  b.onclick=async()=>{if(!ind.value){showClientError("Choose the client industry.",f);return}if(!name.value.trim()){showClientError("Enter the business name, then create the sub-account.",f);return}
53468
54113
  b.disabled=true;b.textContent="Creating\u2026";
53469
54114
  const r=await(await clientPageRequest("/api/new-client",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:name.value.trim(),industry:ind.value,snapshot_id:snap.value||undefined,first_name:fn.value.trim()||undefined,last_name:ln.value.trim()||undefined,email:em.value.trim()||undefined,city:city.value.trim()||undefined,state:st.value.trim()||undefined,timezone:tz.value.trim()||undefined})})).json();
53470
- if(r.ok){canCreateSubAccounts=true;planNotice="";registerNotice="";chooseIntakeClient({locationId:r.locationId,name:r.name,pending:!r.registered})}
54115
+ if(r.ok){ncForget(["nc-name","nc-industry","nc-snapshot","nc-first","nc-last","nc-email","nc-city","nc-state","nc-timezone"]);canCreateSubAccounts=true;planNotice="";registerNotice="";chooseIntakeClient({locationId:r.locationId,name:r.name,pending:!r.registered})}
53471
54116
  else{if(r.canCreateSubAccounts===false){canCreateSubAccounts=false;planNotice=r.error;render(lastState,lastRun)}
53472
54117
  else showClientError(r.error,f,"GHL Command creates sub-accounts on GoHighLevel's Agency Pro plan; on other plans you create it in GoHighLevel and paste its key into Command OS.");b.disabled=false;b.textContent="Create sub-account"}};
53473
54118
  b.disabled=!agencyReady;
@@ -53478,7 +54123,8 @@ function pasteKeyCard(quiet){const card=document.createElement("section");card.c
53478
54123
  const help=document.createElement("p");help.textContent="Create the sub-account in GoHighLevel, then paste its Private Integration key here.";card.appendChild(help);
53479
54124
  const grid=document.createElement("div");grid.className="ncgrid";card.appendChild(grid);
53480
54125
  const field=(text,id)=>{const input=document.createElement("input");input.id=id;input.className="cds-input nccontrol";input.setAttribute("aria-label",text);grid.appendChild(ncField(text,input));return input};
53481
- const id=field("Sub-account id","register-client-id"),key=field("Private Integration key","register-client-key");key.type="password";key.autocomplete="off";key.setAttribute("spellcheck","false");key.setAttribute("data-lpignore","true");key.disabled=registerBusy;id.disabled=registerBusy;
54126
+ /* The id is kept across a redraw; the key never is, in memory or anywhere else (review LOW 9). */
54127
+ const id=ncKeep(field("Sub-account id","register-client-id")),key=field("Private Integration key","register-client-key");key.type="password";key.autocomplete="off";key.setAttribute("spellcheck","false");key.setAttribute("data-lpignore","true");key.disabled=registerBusy;id.disabled=registerBusy;
53482
54128
  // Paths live in ghl-click-paths.ts (key path checked 2026-09-06 through Basic info, and 2026-09-29 with the
53483
54129
  // Location ID in Business Profile). Scopes step exact labels: [to confirm live]; do not name them here.
53484
54130
  const helpDetails=document.createElement("details");helpDetails.className="cds-details";
@@ -53493,8 +54139,11 @@ function pasteKeyCard(quiet){const card=document.createElement("section");card.c
53493
54139
  const r=await(await clientPageRequest("/api/register-client",{method:"POST",headers:{"Content-Type":"application/json"},body:body})).json();
53494
54140
  key.value="";key.disabled=false;id.disabled=false;registerBusy=false;registerFailed=!r.ok;
53495
54141
  registerNotice=r.ok?r.message:(r.error||"The sub-account could not be registered; check the connection and try again.");
53496
- if(r.ok){planNotice="";registerNextId=String(r.locationId||"");chooseIntakeClient({locationId:r.locationId,name:r.name})}
54142
+ if(r.ok){ncForget(["register-client-id"]);planNotice="";registerNextId=String(r.locationId||"");chooseIntakeClient({locationId:r.locationId,name:r.name})}
53497
54143
  else{const shown=document.getElementById("register-client-status")||status,action=document.getElementById("register-client-button")||button;shown.setAttribute("role","alert");shown.textContent=registerNotice;const shownKey=document.getElementById("register-client-key"),shownId=document.getElementById("register-client-id");if(shownKey){shownKey.value="";shownKey.disabled=false}if(shownId)shownId.disabled=false;action.disabled=false;action.textContent="Register"}};
54144
+ /* Enter in either field submits, as a form would (rehearsal 5, BUG-2026-09-30-18): the key was
54145
+ pasted, Register never clicked, and nothing said so. The button's own handler does the work. */
54146
+ for(const input of [id,key])input.addEventListener("keydown",e=>{if(e.key!=="Enter"||e.isComposing)return;e.preventDefault();if(!button.disabled&&!registerBusy)button.click()});
53498
54147
  return card}
53499
54148
  /* The live build panel. Renders run.progress when the cockpit reports it (grouped counts, the
53500
54149
  line being written right now); with no progress object it falls back to today's plain log. */
@@ -53572,6 +54221,32 @@ function staffCard(c,state){const box=document.createElement("section");box.clas
53572
54221
  for(const line of staffSaid[c.locationId])said.appendChild(mk("p","",line));
53573
54222
  const done=mk("button","cds-btn ghost","Done");done.type="button";done.onclick=()=>{delete staffSaid[c.locationId];said.remove()};said.appendChild(done);box.appendChild(said)}
53574
54223
  return box}
54224
+ /* Is a submitted intake in for this client? From the intake as last read here, else as the server last read it. */
54225
+ function intakeIsIn(id,state){const d=iData[id];if(d&&d.ok)return d.status==="received";return (((state||{}).intakeInputs||{})[id]||{}).status==="received"}
54226
+ /* The Complete the intake button, its status chip and, when open, the intake itself (one copy, drawn where the card puts it). */
54227
+ let iUnder={};
54228
+ function intakeEntry(c,div,under){const ir=document.createElement("div");ir.className="cds-row";ir.id="intake-"+c.locationId;
54229
+ const ib2=document.createElement("button");ib2.className="cds-btn ghost";ib2.textContent=iOpen[c.locationId]?"Close the intake":"Complete the intake";ib2.setAttribute("aria-expanded",String(Boolean(iOpen[c.locationId])));
54230
+ ib2.onclick=async()=>{iOpen[c.locationId]=!iOpen[c.locationId];if(iOpen[c.locationId]&&under)iUnder[c.locationId]=true;if(!iOpen[c.locationId]){iRetryStop(c.locationId);delete iUnder[c.locationId]}if(iOpen[c.locationId]&&!iData[c.locationId])await iLoad(c.locationId);else render(lastState,lastRun)};
54231
+ ir.appendChild(ib2);
54232
+ const dd=iData[c.locationId];
54233
+ if(dd&&dd.ok){const ic=document.createElement("span");ic.className="cds-chip istatus "+(dd.status==="received"?"ok":dd.status==="sent"?"warn":"");
54234
+ ic.textContent=dd.status==="received"?("ANSWERS IN"+(dd.channel==="form"?" \xB7 FORM":dd.channel==="cockpit"?" \xB7 SAVED HERE":"")):dd.status==="sent"?"SENT":"NOT STARTED";
54235
+ ic.title=dd.channel==="form"?"A real form submission, visible in GHL's Submissions and re-readable later.":dd.channel==="cockpit"?"Saved from Command OS onto a tagged contact (no form submission).":"";ir.appendChild(ic)}
54236
+ div.appendChild(ir);
54237
+ if(iOpen[c.locationId])div.appendChild(unnest(intakeWorkspace(c.locationId,c.name)))}
54238
+ /* Opens the intake where the card draws it and scrolls to it (test 01, BUG-2026-09-30-30/-31). A first opening
54239
+ goes straight to From a call or a document, the fastest way to fill it. */
54240
+ /* The scroll waits for the redraw (BUG-2026-09-30-39): a smooth scroll started before the intake had drawn,
54241
+ and the redraw that followed (its document list arriving) put the page back where it was. For a moment
54242
+ after the click, every redraw ends by bringing the intake into view, instantly. */
54243
+ let iScrollTo=null;const I_SCROLL_MS=2500;
54244
+ function iScrollAfterRender(){const p=iScrollTo;if(!p)return;if(Date.now()>p.until){iScrollTo=null;return}
54245
+ const t=document.getElementById("intake-"+p.id);if(!t)return;try{t.scrollIntoView({block:"start"})}catch(e){}
54246
+ if(!p.focused){p.focused=true;const f=t.querySelector("button");if(f)try{f.focus({preventScroll:true})}catch(e){}}}
54247
+ function openIntake(id){iOpen[id]=true;iUnder[id]=true;if(!iMode[id])iMode[id]="transcript";iScrollTo={id:id,until:Date.now()+I_SCROLL_MS};
54248
+ const go=()=>{render(lastState,lastRun);requestAnimationFrame(iScrollAfterRender)};
54249
+ if(!iData[id])iLoad(id).then(go,go);else go()}
53575
54250
  function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES.map(()=>"pending")}).stages;
53576
54251
  const sandbox=typeOf(state,c.locationId)==="sandbox";
53577
54252
  // A template or hidden account opens only so its owner can lock or unlock it and change its
@@ -53603,8 +54278,18 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
53603
54278
  // and the handler it had in the seven-column layout; the next-step panel draws the SAME
53604
54279
  // buttons again for the current step, so nothing appears there that its row would not offer.
53605
54280
  const staleOf=i=>state.buildViews?.[c.locationId]?.stale?.find(x=>x.stage===i);
53606
- const isDone=i=>(st[i]||"pending")==="done"&&!staleOf(i);
54281
+ /* A step that is running right now is never counted done (rehearsal 5, BUG-2026-09-30-15): Brief is
54282
+ marked done and then writes the Blueprint Review while still running, and the header read
54283
+ "Step 3 of 7: Approve \xB7 2 done" beside a Brief row that said Running. */
54284
+ const liveI=run&&run.running&&run.locationId===c.locationId&&typeof run.stage==="number"?run.stage:-1;
54285
+ const isDone=i=>(st[i]||"pending")==="done"&&!staleOf(i)&&i!==liveI;
53607
54286
  let curI=-1;for(let i=0;i<STAGES.length;i++){if(!isDone(i)){curI=i;break}}
54287
+ /* Test 01 walkthrough (BUG-2026-09-30-31): while Brief is next and no intake is in, the intake is the
54288
+ next job, so its entry point sits right under the steps instead of four screens down. */
54289
+ const intakeNext=!parkedType&&curI===1&&st[0]==="done"&&!intakeIsIn(c.locationId,state);
54290
+ /* Opened under the steps, it stays there while it is open, after a submit too (review LOW 7). */
54291
+ const intakeUp=!parkedType&&st[0]==="done"&&(intakeNext||Boolean(iUnder[c.locationId]&&iOpen[c.locationId]));
54292
+ const intakeFirst=intakeNext;
53608
54293
  /* A step still waiting on the owner's approval is not counted as done, so the count matches its "Your call" chip. */
53609
54294
  const nDone=STAGES.filter((_,i)=>isDone(i)&&!(i===2&&!(state.buildApproved||{})[c.locationId])).length;
53610
54295
  // Where it stands, in words; each row's pin repeats its state and its chip names it.
@@ -53672,6 +54357,10 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
53672
54357
  /* What the last run found, in owner words: a warn chip counting the issues with its See details toggle;
53673
54358
  Verify's own verdict; the plain summary; a raw system note only under Details. */
53674
54359
  const mkNote=()=>{let note=null;
54360
+ /* Brief's own run found no intake (review LOW 8): the gate's sentence, and its button unless the next step already shows one. */
54361
+ if(!live&&oc&&i===1&&oc.reason==="intake-needed"){note=mk("span","snote sintakenote",${scriptJson(BRIEF_NEEDS_INTAKE2)});
54362
+ if(!intakeFirst){const go=mk("button","cds-btn ghost srunintake","Complete the intake");go.type="button";go.onclick=e=>{e.stopPropagation();openIntake(c.locationId)};note.appendChild(document.createTextNode(" "));note.appendChild(go)}
54363
+ return note}
53675
54364
  if(!live&&oc&&(oc.summary||(oc.issues||[]).length)){
53676
54365
  const passed=i===4&&oc.ok===true,plainSaid=Boolean(oc.plain&&oc.plain.summary);
53677
54366
  if(!passed&&nIss){note=mk("span","snote");if(!doneWithIssues&&!staleIssues)note.appendChild(chip("warn",nIss+" "+plural(nIss,"issue")));const see=mk("button","cds-link snote-see","See details");see.type="button";see.onclick=toggleIssues;note.appendChild(see);return note}
@@ -53717,6 +54406,8 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
53717
54406
  rnShow(btn,runKeyOf,null);const j=await postRun();
53718
54407
  if(j&&j.ok){load();return}
53719
54408
  if(i===1&&j&&j.reason==="intake-missing"){rnShow(btn,runKeyOf,{wait:true,waitId:c.locationId,text:BRIEF_WAIT_TEXT});await briefWait(c.locationId,runKeyOf,postRun,j.error);return}
54409
+ /* One Complete the intake button (review nit): the line carries it only when the next step does not. */
54410
+ if(i===1&&j&&j.reason==="intake-needed"){rnShow(btn,runKeyOf,intakeFirst?{text:runFailText(j)}:{text:runFailText(j),openIntake:c.locationId});load();return}
53720
54411
  rnShow(btn,runKeyOf,{text:runFailText(j),wasDone:wasDone});load()};
53721
54412
  const mkRun=(cls,label)=>{const b=document.createElement("button");b.className=cls;
53722
54413
  const busy=run&&run.running;b.textContent=busy&&run.locationId===c.locationId&&run.stage===i?"Running\u2026":label;
@@ -53732,11 +54423,13 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
53732
54423
  const h3=document.createElement("h3");h3.className="cds-panel-title";h3.textContent=parkedType?"Nothing is built into this account":(i+1)+". "+LABELS[i];main.appendChild(h3);
53733
54424
  const pa=document.createElement("p");pa.textContent=parkedType?"It opens here so you can lock or unlock it, or change its type to Client to build it.":stepAbout(i,c,true);main.appendChild(pa);
53734
54425
  if(mustChoose&&!parkedType){const pc=document.createElement("p");pc.className="swarn";pc.textContent="First choose what the client bought. Only those parts get built.";main.appendChild(pc)}
54426
+ if(i===1&&intakeFirst&&!live&&!(oc&&oc.reason==="intake-needed")){main.appendChild(mk("p","swarn sintake",${scriptJson(BRIEF_NEEDS_INTAKE2)}))}
53735
54427
  if(live)main.appendChild(mkLive());
53736
54428
  if(stale&&!live){const w=document.createElement("div");w.className="cds-row";w.appendChild(chip(chipOf[1],chipOf[0]));w.appendChild(mk("span","swarn",stale.reason));main.appendChild(w)}
53737
54429
  const pn=mkNote();if(pn){const w=document.createElement("div");w.className="snoteline";w.appendChild(pn);main.appendChild(w)}
53738
54430
  const btns=document.createElement("div");btns.className="sbtns";
53739
54431
  if(gateOpen&&!anyBusy&&!parkedType)btns.appendChild(mkGate("cds-btn gatebtn sbig"));
54432
+ if(i===1&&intakeFirst&&!live&&!iOpen[c.locationId]){const ib=mk("button","cds-btn sbig sintakebtn","Complete the intake");ib.type="button";ib.onclick=ev=>{ev.stopPropagation();openIntake(c.locationId)};btns.appendChild(ib)}
53740
54433
  if(canBuild&&POWERED.includes(i))btns.appendChild(mkRun("cds-btn runbtn sbig","Run "+LABELS[i]));
53741
54434
  if(!parkedType&&canBuild&&POWERED.includes(i)&&run&&run.running&&run.locationId!==c.locationId){const why=document.createElement("p");why.className="muted swhy";why.textContent="A step is running for another client. This one can start when it finishes.";btns.appendChild(why)}
53742
54435
  if(!btns.children.length&&!live&&!parkedType){const why=document.createElement("p");why.className="muted swhy";
@@ -53754,6 +54447,8 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
53754
54447
  const needsN=(curI<0||curLive?0:1)+approvalWaits;const afterN=Math.max(0,after-approvalWaits);
53755
54448
  s.textContent=nDone+" done"+(curI<0&&!approvalWaits?"":(curLive?" \xB7 1 running now":"")+(needsN?" \xB7 "+needsN+" needs you":"")+(curI<0?"":" \xB7 "+afterN+" after that"));plabel.appendChild(s)}
53756
54449
  div.appendChild(row);
54450
+ /* Closed, the next step's own Complete the intake button is the one way in (review nit: one button); open, it sits here. */
54451
+ if(intakeUp&&iOpen[c.locationId])intakeEntry(c,div,true);
53757
54452
  for(let i=0;i<STAGES.length;i++){if(oOpen[c.locationId+":"+i]){const op=outcomePanel(c.locationId,i,state);if(op)div.appendChild(unnest(op))}}
53758
54453
  const view=(state.buildViews||{})[c.locationId];
53759
54454
  // The plan is reference material, not the page. Printed open it was 33,759 px of
@@ -53859,16 +54554,7 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
53859
54554
  rb.onclick=async()=>{rOpen[c.locationId]=!rOpen[c.locationId];rSeen[c.locationId]=true;if(rOpen[c.locationId]&&!rData[c.locationId])await rLoad(c.locationId);else render(lastState,lastRun)};
53860
54555
  rr.appendChild(rb);div.appendChild(rr);
53861
54556
  if(rOpen[c.locationId])div.appendChild(unnest(reviewPanel(c.locationId,c.name,state,run)))}
53862
- if(st[0]==="done"){const ir=document.createElement("div");ir.className="cds-row";
53863
- const ib2=document.createElement("button");ib2.className="cds-btn ghost";ib2.textContent=iOpen[c.locationId]?"Close the intake":"Complete the intake";ib2.setAttribute("aria-expanded",String(Boolean(iOpen[c.locationId])));
53864
- ib2.onclick=async()=>{iOpen[c.locationId]=!iOpen[c.locationId];if(!iOpen[c.locationId])iRetryStop(c.locationId);if(iOpen[c.locationId]&&!iData[c.locationId])await iLoad(c.locationId);else render(lastState,lastRun)};
53865
- ir.appendChild(ib2);
53866
- const dd=iData[c.locationId];
53867
- if(dd&&dd.ok){const ic=document.createElement("span");ic.className="cds-chip istatus "+(dd.status==="received"?"ok":dd.status==="sent"?"warn":"");
53868
- ic.textContent=dd.status==="received"?("ANSWERS IN"+(dd.channel==="form"?" \xB7 FORM":dd.channel==="cockpit"?" \xB7 SAVED HERE":"")):dd.status==="sent"?"SENT":"NOT STARTED";
53869
- ic.title=dd.channel==="form"?"A real form submission, visible in GHL's Submissions and re-readable later.":dd.channel==="cockpit"?"Saved from Command OS onto a tagged contact (no form submission).":"";ir.appendChild(ic)}
53870
- div.appendChild(ir);
53871
- if(iOpen[c.locationId])div.appendChild(unnest(intakeWorkspace(c.locationId,c.name)))}
54557
+ if(st[0]==="done"&&!intakeUp)intakeEntry(c,div,false);
53872
54558
  if(run&&run.locationId===c.locationId&&(run.running||run.lines&&run.lines.length)){
53873
54559
  const bp=buildPanel(run);
53874
54560
  if(bp){div.appendChild(unnest(bp));
@@ -53966,6 +54652,8 @@ function icLive(key){return [...document.querySelectorAll(".ic-panel")].filter(x
53966
54652
  let runNote={},rnQueue=[];
53967
54653
  function runFailText(j){return (j&&j.error)||"Command OS could not confirm the result. Refresh this client to read its saved state before retrying."}
53968
54654
  function rnEl(key){const n=runNote[key];const p=mk("p",n.wait?"muted srunnote":"cds-callout warn srunnote",n.text);p.dataset.rn=key;p.setAttribute("role",n.wait?"status":"alert");
54655
+ /* Brief refused for want of an intake (test 01, BUG-2026-09-30-30): the line carries the way to the intake panel. */
54656
+ if(n.openIntake){const go=mk("button","cds-btn ghost srunintake","Complete the intake");go.type="button";go.onclick=e=>{e.stopPropagation();openIntake(n.openIntake)};p.appendChild(document.createTextNode(" "));p.appendChild(go)}
53969
54657
  if(n.wait&&n.waitId){const stop=mk("button","cds-link srunstop","Stop waiting");stop.type="button";stop.onclick=e=>{e.stopPropagation();briefStop(n.waitId,key)};p.appendChild(document.createTextNode(" "));p.appendChild(stop)}
53970
54658
  return p}
53971
54659
  function rnLive(key){return [...document.querySelectorAll(".srunnote")].filter(x=>x.dataset.rn===key)}
@@ -54482,7 +55170,7 @@ function restoreView(v){const m=document.getElementById("main");if(!v||!m)return
54482
55170
  if(v.focus){const el=[...m.querySelectorAll(v.focus.tag)].filter(x=>fieldKey(x,m)===v.focus.key)[v.focus.n];
54483
55171
  if(el&&document.activeElement!==el){try{el.focus({preventScroll:true})}catch(e){}try{if(v.focus.st!=null)el.setSelectionRange(v.focus.st,v.focus.en)}catch(e){}}}
54484
55172
  if(window.scrollX!==v.x||window.scrollY!==v.y){try{window.scrollTo(v.x,v.y)}catch(e){}}}
54485
- function render(state,run){const v=captureView();try{if(state)icPrune(state);rnPrune(state,run);renderPage(state,run);icFlush();rnFlush()}finally{lastDrawnSig=dataSig(state,run);restoreView(v)}}
55173
+ function render(state,run){const v=captureView();try{if(state)icPrune(state);rnPrune(state,run);renderPage(state,run);icFlush();rnFlush()}finally{lastDrawnSig=dataSig(state,run);restoreView(v);iScrollAfterRender()}}
54486
55174
  function renderPage(state,run){if(state.roster){ROSTER.splice(0,ROSTER.length,...state.roster)}syncReviews(state);lastState=state;lastRun=run;
54487
55175
  if(state&&state.protectedAccounts)PROTECTED=state.protectedAccounts;const m=document.getElementById("main");m.innerHTML="";
54488
55176
  /* Four screens share this page's data and differ only in what they show. */
@@ -55781,9 +56469,10 @@ ${opportunityNoteFailure?.detail ?? ""}`);
55781
56469
  return titleCache.get(rk);
55782
56470
  };
55783
56471
  const named = (o, rk) => ({ ...o, ...o.summary !== void 0 && { summary: nameHandoffRefs(o.summary, titlesFor(rk)) }, ...o.issues && { issues: o.issues.map((x) => nameHandoffRefs(x, titlesFor(rk))) } });
56472
+ const briefShown = (key, o) => key.endsWith(":1") ? { ...o, ...o.issues && { issues: briefIssuesShown(o.issues) }, ...o.plain?.issues && { plain: { ...o.plain, issues: briefIssuesShown(o.plain.issues) } } } : o;
55784
56473
  const outcomes = Object.fromEntries(Object.entries(st.outcomes ?? {}).map(([key, raw]) => {
55785
56474
  const rk = key.slice(0, key.lastIndexOf(":"));
55786
- const value = { ...named(raw, rk), ...raw.plain && { plain: named(raw.plain, rk) } };
56475
+ const value = briefShown(key, { ...named(raw, rk), ...raw.plain && { plain: named(raw.plain, rk) } });
55787
56476
  return [key, { ...value, ...key.endsWith(":4") && value.verification ? { plain: verifyPanelOutcome(value) } : { ...plainFallbackOutcome(value), plain: plainFallbackOutcome(value.plain ?? value) }, error: value.error ? "The step did not finish. Check the account before retrying." : void 0 }];
55788
56477
  }));
55789
56478
  res.end(JSON.stringify({ ...st, assessmentFigures, boughtViews, outcomes, manualTasks: Object.fromEntries(Object.entries(st.manualTasks ?? {}).map(([id, tasks2]) => {
@@ -55896,7 +56585,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
55896
56585
  if (!c) throw new Error(INTAKE_UNREADABLE);
55897
56586
  const snap = await readIntakeSnapshot(c, locationId2);
55898
56587
  if (snap.status === "unavailable") throw intakeUnavailableError(snap);
55899
- if (snap.status !== "received") throw new Error("Open the real form, submit it, then refresh.");
56588
+ if (snap.status !== "received") throw new Error(BRIEF_NEEDS_INTAKE2);
55900
56589
  const required = composeQuestionSet({ industry: st0.accounts?.[locationId2]?.industry, agency: readOverlay() }).filter((q3) => q3.required && !String(normalizedAnswers?.[q3.label] ?? normalizedAnswers?.[q3.key] ?? "").trim());
55901
56590
  if (required.length) throw new Error("Complete these required intake answers: " + required.map((q3) => q3.label).join(", ") + ".");
55902
56591
  }
@@ -55995,7 +56684,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
55995
56684
  res.writeHead(400, { "Content-Type": "application/json" });
55996
56685
  logQuietly(`client-intake ${new URL(req.url || "/", "http://x").searchParams.get("locationId") || ""}: ${e instanceof Error ? e.message : String(e)}`);
55997
56686
  if (e instanceof Error && e.message === INTAKE_READ_TRANSIENT) res.end(JSON.stringify({ ok: false, transient: true, error: INTAKE_REFRESH_RETRYING }));
55998
- else res.end(JSON.stringify({ ok: false, error: intakeRequestError(e) }));
56687
+ else res.end(JSON.stringify(intakeRefusal(e)));
55999
56688
  }
56000
56689
  })();
56001
56690
  return;
@@ -56016,7 +56705,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
56016
56705
  res.end(JSON.stringify({ ok: true, url: pf.url, leftovers: pf.leftovers }));
56017
56706
  } catch (e) {
56018
56707
  res.writeHead(400, { "Content-Type": "application/json" });
56019
- res.end(JSON.stringify({ ok: false, error: intakeRequestError(e) }));
56708
+ logQuietly(`${url}: ${e instanceof Error ? e.message : String(e)}`);
56709
+ res.end(JSON.stringify(intakeRefusal(e)));
56020
56710
  }
56021
56711
  });
56022
56712
  return;
@@ -56053,17 +56743,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
56053
56743
  if (ready.smoked) recordRunCost({ at: (/* @__PURE__ */ new Date()).toISOString(), locationId: args.locationId, stage: 0, kind: "engine-check", ok: ready.ok, costUsd: ready.smokeCostUsd });
56054
56744
  if (!ready.ok) throw new Error(engineRepair(ready.version));
56055
56745
  if (typeof args.transcript === "string" && args.transcript.length > 6e4) throw new Error("The limit is 60,000 characters. Paste an excerpt or shorten this text file.");
56056
- let fileSource = null;
56057
- if (args.file) {
56058
- const name = safeDocName(args.file);
56059
- const dir = clientDocsDir(args.locationId);
56060
- const p = path40.join(dir, name);
56061
- if (!fs42.existsSync(p)) throw new Error("that document is not saved for this client");
56062
- if (fs42.lstatSync(p).isSymbolicLink() || !insideDir(dir, p)) throw new Error("that document is not saved for this client");
56063
- const text2 = fs42.readFileSync(p, "utf8");
56064
- if (text2.length > 6e4) throw new Error("The limit is 60,000 characters. Paste an excerpt or shorten this text file.");
56065
- fileSource = { kind: "text", text: text2 };
56066
- }
56746
+ const fileSource = args.file ? { kind: "text", text: readSavedClientDoc(args.locationId, args.file) } : null;
56067
56747
  const source = fileSource ? fileSource : args.url ? { kind: "url", url: String(args.url) } : args.instruction ? { kind: "recorder", instruction: String(args.instruction) } : { kind: "text", text: String(args.transcript ?? "") };
56068
56748
  const toolContext = { servers: [] };
56069
56749
  if (source.kind === "recorder") {
@@ -56147,7 +56827,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
56147
56827
  return;
56148
56828
  } catch (e) {
56149
56829
  res.writeHead(400, { "Content-Type": "application/json" });
56150
- res.end(JSON.stringify({ ok: false, error: intakeRequestError(e) }));
56830
+ logQuietly(`${url}: ${e instanceof Error ? e.message : String(e)}`);
56831
+ res.end(JSON.stringify(intakeRefusal(e)));
56151
56832
  }
56152
56833
  });
56153
56834
  return;
@@ -57060,7 +57741,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
57060
57741
  process.on("SIGTERM", stop2);
57061
57742
  });
57062
57743
  }
57063
- var import_zod67, import_node_util5, fs42, path40, http, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, SCOPE_REFUSAL, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanOnce, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, INTAKE_ANSWERS_MISSING, INTAKE_READ_FAILED, RUN_IN_PROGRESS, BUSINESS_TYPE_FOR_INDUSTRY, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
57744
+ var import_zod67, import_node_util5, fs42, path40, http, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, SCOPE_REFUSAL, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanOnce, DOC_NOT_SAVED, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, INTAKE_ANSWERS_MISSING, INTAKE_READ_FAILED, RUN_IN_PROGRESS, BRIEF_NEEDS_INTAKE2, BUSINESS_TYPE_FOR_INDUSTRY, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
57064
57745
  var init_dashboard = __esm({
57065
57746
  "src/dashboard.ts"() {
57066
57747
  "use strict";
@@ -57133,6 +57814,8 @@ var init_dashboard = __esm({
57133
57814
  init_client_intake();
57134
57815
  init_client_intake();
57135
57816
  init_plan_store();
57817
+ init_brief_issues();
57818
+ init_build_facts();
57136
57819
  init_plain_outcome();
57137
57820
  init_staff_first();
57138
57821
  init_user_provisioning();
@@ -57200,6 +57883,7 @@ var init_dashboard = __esm({
57200
57883
  DOC_EXTS = /* @__PURE__ */ new Set([".md", ".txt", ".vtt", ".srt", ".csv"]);
57201
57884
  DOC_MAX_BYTES = 20 * 1024 * 1024;
57202
57885
  queueScanOnce = scanGate();
57886
+ DOC_NOT_SAVED = "That document is not saved for this client any more. Choose it again from the list, or add it again.";
57203
57887
  PARKED_NOT_BUILT = "Template and hidden accounts are not built from Command OS. Change the account's type to Client first.";
57204
57888
  NEEDS_AGENCY_KEY = "No agency key on file. Run register_agency_key in GHL Command first.";
57205
57889
  INTAKE_UNREADABLE = "Command OS could not read this client's account. Paste its key again in Build a client account.";
@@ -57208,6 +57892,7 @@ var init_dashboard = __esm({
57208
57892
  INTAKE_ANSWERS_MISSING = "The intake form shows a submission, but no contact in this client's account has the answers yet. If the client has not submitted the form, send it again. If they have, press Refresh status in a minute.";
57209
57893
  INTAKE_READ_FAILED = "The intake could not be read. Press Refresh status to try again.";
57210
57894
  RUN_IN_PROGRESS = "a run is already in progress";
57895
+ BRIEF_NEEDS_INTAKE2 = BRIEF_NEEDS_INTAKE;
57211
57896
  BUSINESS_TYPE_FOR_INDUSTRY = { "med-spa": "Med spa", "wellness-clinic": "Wellness clinic", clinic: "Clinic / practice", coach: "Coach / consultant", "local-service": "Local service", ecommerce: "Ecommerce", agency: "Agency" };
57212
57897
  INTAKE_FORM_REQUIRED = "Run the intake step in Client builds to create the form, then come back and submit.";
57213
57898
  INTAKE_CONNECT_REQUIRED = "Connect this sub-account by pasting its Private Integration key in Command OS.";
@@ -57223,7 +57908,9 @@ var init_dashboard = __esm({
57223
57908
  INTAKE_FORM_REQUIRED,
57224
57909
  "Confirm the contact creation and email before sending the questionnaire.",
57225
57910
  "Choose a registered client before saving the intake.",
57226
- "Enter intake answers as text or a list of choices."
57911
+ "Enter intake answers as text or a list of choices.",
57912
+ DOC_NOT_SAVED,
57913
+ "Only plain text (.txt, .md, .csv), .vtt and .srt files can be saved here."
57227
57914
  ];
57228
57915
  currentRosterRef = () => [];
57229
57916
  staffGateRef = async () => null;
@@ -62946,10 +63633,10 @@ function renderVerificationHtml(v, branding = {}, base) {
62946
63633
  ];
62947
63634
  const s = v.summary;
62948
63635
  const verdictClass = v.verdict === "PASS" ? "ok" : v.verdict === "FAIL" ? "fail" : "warn";
62949
- const row = (r) => `<tr class="${STATE_CLASS[r.state]}"><td class="id">${esc14(r.ruleId)}</td><td><b>${esc14(r.name)}</b><div class="thr">${esc14(r.threshold)}</div></td><td><span class="pill ${STATE_CLASS[r.state]}">${STATE_LABEL[r.state]}</span></td><td>${esc14(r.measured ?? "")}${r.detail ? `<div class="det">${esc14(r.detail)}</div>` : ""}</td><td class="ev">${esc14(r.evidence ?? "")}</td></tr>`;
63636
+ const row = (r) => `<tr class="${STATE_CLASS[r.state]}"><td class="id">${esc15(r.ruleId)}</td><td><b>${esc15(r.name)}</b><div class="thr">${esc15(r.threshold)}</div></td><td><span class="pill ${STATE_CLASS[r.state]}">${STATE_LABEL[r.state]}</span></td><td>${esc15(r.measured ?? "")}${r.detail ? `<div class="det">${esc15(r.detail)}</div>` : ""}</td><td class="ev">${esc15(r.evidence ?? "")}</td></tr>`;
62950
63637
  return `<!doctype html>
62951
63638
  <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
62952
- <title>Design check \u2014 ${esc14(v.target.split("/").pop() ?? v.target)}</title>
63639
+ <title>Design check \u2014 ${esc15(v.target.split("/").pop() ?? v.target)}</title>
62953
63640
  <style>
62954
63641
  :root{--accent:${b.accent};--ink:#1F2428;--muted:#5C6570;--line:#E3E6E9;--bg:#FFFFFF;--ok:#2F7A5A;--fail:#A63D3D;--warn:#B9621C;--na:#6E7681}
62955
63642
  body{margin:0;background:var(--bg);color:var(--ink);font:18px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
@@ -62980,23 +63667,23 @@ td.ev{font-family:ui-monospace,Menlo,monospace;font-size:12.5px;color:var(--mute
62980
63667
  .shots a{font-size:13px;color:var(--accent)}
62981
63668
  footer{margin-top:40px;color:var(--muted);font-size:13px;border-top:1px solid var(--line);padding-top:12px}
62982
63669
  </style></head><body><div class="wrap">
62983
- <header><div>${isRasterDataUri(b.logoDataUri) ? `<img src="${esc14(b.logoDataUri)}" alt="" style="height:28px;margin-bottom:8px">` : ""}<h1>Design check</h1><div class="agency">${b.agencyName ? esc14(b.agencyName) + " \xB7 " : ""}${esc14(v.medium)} \xB7 ${esc14(v.target)} \xB7 ${esc14(v.at.slice(0, 16).replace("T", " "))} \xB7 ${v.renderedWith === "browser" ? "rendered in a browser" : "static checks only"}</div></div><div class="verdict ${verdictClass}">${v.verdict === "PASS" ? "Passes every measured rule" : v.verdict === "FAIL" ? "Fails a measured rule" : "Not graded"}</div></header>
63670
+ <header><div>${isRasterDataUri(b.logoDataUri) ? `<img src="${esc15(b.logoDataUri)}" alt="" style="height:28px;margin-bottom:8px">` : ""}<h1>Design check</h1><div class="agency">${b.agencyName ? esc15(b.agencyName) + " \xB7 " : ""}${esc15(v.medium)} \xB7 ${esc15(v.target)} \xB7 ${esc15(v.at.slice(0, 16).replace("T", " "))} \xB7 ${v.renderedWith === "browser" ? "rendered in a browser" : "static checks only"}</div></div><div class="verdict ${verdictClass}">${v.verdict === "PASS" ? "Passes every measured rule" : v.verdict === "FAIL" ? "Fails a measured rule" : "Not graded"}</div></header>
62984
63671
  <p class="scope"><strong>What this report is not.</strong> Every rule here measures craft \u2014 type, spacing, colour, contrast, structure, performance, and the tells that make a page look machine-made. <strong>None of them measures whether the page persuades anyone.</strong> A page can pass every rule and still be boring, unconvincing and unsellable: no hook, no proof, no reason to care. Judge the writing separately, and never read a pass here as "this is a good page".</p>
62985
- <div class="conf ${v.conformance.ok ? "ok" : ""}"><b>${v.conformance.ok ? "The file honours the output contract." : "The file breaks the output contract, so the design rules were not graded."}</b>${v.conformance.problems.length ? `<ul>${v.conformance.problems.map((p) => `<li>${esc14(p)}</li>`).join("")}</ul>` : ""}${v.conformance.warnings.length ? `<div class="det">Warnings: ${v.conformance.warnings.map(esc14).join(" \xB7 ")}</div>` : ""}</div>
63672
+ <div class="conf ${v.conformance.ok ? "ok" : ""}"><b>${v.conformance.ok ? "The file honours the output contract." : "The file breaks the output contract, so the design rules were not graded."}</b>${v.conformance.problems.length ? `<ul>${v.conformance.problems.map((p) => `<li>${esc15(p)}</li>`).join("")}</ul>` : ""}${v.conformance.warnings.length ? `<div class="det">Warnings: ${v.conformance.warnings.map(esc15).join(" \xB7 ")}</div>` : ""}</div>
62986
63673
  <div class="sum"><div><b>${s.measuredPass}</b><span>measured, pass</span></div><div><b>${s.measuredFail}</b><span>measured, fail</span></div><div><b>${s.proxyPass}</b><span>proxy, pass</span></div><div><b>${s.proxyFail}</b><span>proxy, fail</span></div><div><b>${s.asserted}</b><span>judgment calls</span></div><div><b>${s.na + s.unchecked}</b><span>not applicable / not checked</span></div></div>
62987
- ${v.brandMatch ? `<p class="note"><b>Brand:</b> ${esc14(v.brandMatch.detail)}</p>` : ""}
63674
+ ${v.brandMatch ? `<p class="note"><b>Brand:</b> ${esc15(v.brandMatch.detail)}</p>` : ""}
62988
63675
  ${groups.filter((g) => g.rows.length).map((g) => `<h2>${g.title}</h2><p class="note">${g.note}</p><table><thead><tr><th>Rule</th><th>What it asks</th><th>State</th><th>Measured</th><th>Evidence</th></tr></thead><tbody>${g.rows.map(row).join("")}</tbody></table>`).join("")}
62989
- ${v.screenshots.length ? `<h2>Screenshots</h2><div class="shots">${v.screenshots.map((f) => `<a href="${esc14(f)}">${esc14(f)}</a>`).join("")}</div>` : ""}
62990
- <footer>Ruleset ${esc14(v.ruleset)} \xB7 viewports ${esc14(v.viewports.join(", ") || "\u2014")} \xB7 verification@${v.schemaVersion}. Measured lines are computed by software; proxy lines approximate a judgment; judgment calls are listed so they are never forgotten. Nothing here is hidden.</footer>
63676
+ ${v.screenshots.length ? `<h2>Screenshots</h2><div class="shots">${v.screenshots.map((f) => `<a href="${esc15(f)}">${esc15(f)}</a>`).join("")}</div>` : ""}
63677
+ <footer>Ruleset ${esc15(v.ruleset)} \xB7 viewports ${esc15(v.viewports.join(", ") || "\u2014")} \xB7 verification@${v.schemaVersion}. Measured lines are computed by software; proxy lines approximate a judgment; judgment calls are listed so they are never forgotten. Nothing here is hidden.</footer>
62991
63678
  </div></body></html>`;
62992
63679
  }
62993
- var esc14, STATE_LABEL, STATE_CLASS;
63680
+ var esc15, STATE_LABEL, STATE_CLASS;
62994
63681
  var init_report2 = __esm({
62995
63682
  "src/command-os/design/report.ts"() {
62996
63683
  "use strict";
62997
63684
  init_branding();
62998
63685
  init_svg_sanitize();
62999
- esc14 = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
63686
+ esc15 = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
63000
63687
  STATE_LABEL = {
63001
63688
  "MEASURED-PASS": "Pass",
63002
63689
  "MEASURED-FAIL": "Fail",
@@ -67923,10 +68610,10 @@ var init_run3 = __esm({
67923
68610
 
67924
68611
  // src/command-os/prospect/report-page.ts
67925
68612
  function gradeRing2(score2, caption, size2 = 176) {
67926
- if (score2 === void 0) return `<div class="cds-ring" role="img" aria-label="Not graded. ${esc15(caption)}">${chip4("warn", "Not graded")}<div class="cap">${esc15(caption)}</div></div>`;
68613
+ if (score2 === void 0) return `<div class="cds-ring" role="img" aria-label="Not graded. ${esc16(caption)}">${chip4("warn", "Not graded")}<div class="cap">${esc16(caption)}</div></div>`;
67927
68614
  const s = Math.max(0, Math.min(100, Math.round(score2)));
67928
68615
  const r = 52, c = 2 * Math.PI * r, g = gradeOf(s), k = gcls2(s);
67929
- return `<div class="cds-ring" role="img" aria-label="Grade ${g}, ${s} out of 100. ${esc15(caption)}"><svg width="${size2}" height="${size2}" viewBox="0 0 120 120"><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>${caption ? `<div class="cap">${esc15(caption)}</div>` : ""}</div>`;
68616
+ return `<div class="cds-ring" role="img" aria-label="Grade ${g}, ${s} out of 100. ${esc16(caption)}"><svg width="${size2}" height="${size2}" viewBox="0 0 120 120"><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>${caption ? `<div class="cap">${esc16(caption)}</div>` : ""}</div>`;
67930
68617
  }
67931
68618
  function withoutPrice(name) {
67932
68619
  return name.replace(/[$\u00a3\u20ac]\s?\d[\d,.]*(\s?[kK])?(\s?(\/|per)\s?(mo|month|yr|year|week|wk))?/g, "").replace(/\b\d[\d,.]*\s?(\/|per)\s?(mo|month|yr|year)\b/gi, "").replace(/\s*[-|:,(]\s*\)?\s*$/, "").replace(/\(\s*\)/g, "").replace(/\s{2,}/g, " ").trim();
@@ -68020,11 +68707,11 @@ function bookButton(b, prefill) {
68020
68707
  const t = walkthroughTarget(b, prefill);
68021
68708
  if (!t) return "";
68022
68709
  const printed = t.via === "booking" ? shortUrl(b.bookingUrl.trim()) : t.via === "phone" ? `Call ${b.phone}` : `Email ${b.email}`;
68023
- return `${link2(t.href, WALKTHROUGH.button, "cds-btn rp-book")}<span class="rp-print-only">${esc15(printed)}</span>`;
68710
+ return `${link2(t.href, WALKTHROUGH.button, "cds-btn rp-book")}<span class="rp-print-only">${esc16(printed)}</span>`;
68024
68711
  }
68025
68712
  function walkthroughBlock(id, b, workflowAudit, prefill) {
68026
68713
  const reach = reachLinks(b);
68027
- return `<section data-section="${id}" id="${id}" class="rp-cta"><h2>${esc15(WALKTHROUGH.h2)}</h2>${workflowAudit ? `<p>${esc15(WALKTHROUGH.line)}</p>` : ""}${bookButton(b, prefill)}${reach.length ? `<p class="rp-reach">${esc15(b.name)} \xB7 ${reach.join(" \xB7 ")}</p>` : ""}</section>`;
68714
+ return `<section data-section="${id}" id="${id}" class="rp-cta"><h2>${esc16(WALKTHROUGH.h2)}</h2>${workflowAudit ? `<p>${esc16(WALKTHROUGH.line)}</p>` : ""}${bookButton(b, prefill)}${reach.length ? `<p class="rp-reach">${esc16(b.name)} \xB7 ${reach.join(" \xB7 ")}</p>` : ""}</section>`;
68028
68715
  }
68029
68716
  function competitorsAhead(r) {
68030
68717
  const mine = r.business.score.score;
@@ -68053,16 +68740,16 @@ function patientHeadline(gaps, a) {
68053
68740
  function checksCard(area, checks) {
68054
68741
  const cs = checks.filter((c) => c.area === area);
68055
68742
  const known = cs.filter((c) => c.verdict !== "unknown").length;
68056
- return `<div class="rp-card"><div class="rp-card-head"><h3>${esc15(AREAS3[area].label)}</h3><span class="rp-tag">${known} of ${cs.length} checked</span></div><ul class="rp-checks">${cs.map((c) => {
68743
+ return `<div class="rp-card"><div class="rp-card-head"><h3>${esc16(AREAS3[area].label)}</h3><span class="rp-tag">${known} of ${cs.length} checked</span></div><ul class="rp-checks">${cs.map((c) => {
68057
68744
  const [tone, word] = VERDICT[c.verdict];
68058
- return `<li><span class="q">${esc15(c.label)}</span>${chip4(tone, word)}<span class="ev">${esc15(c.evidence)}</span></li>`;
68745
+ return `<li><span class="q">${esc16(c.label)}</span>${chip4(tone, word)}<span class="ev">${esc16(c.evidence)}</span></li>`;
68059
68746
  }).join("")}</ul></div>`;
68060
68747
  }
68061
68748
  function brandCard(r) {
68062
68749
  const b = r.site.brand;
68063
68750
  const look = r.site.phone?.ok ? r.site.phone.look : void 0;
68064
68751
  if (!b) return `<div class="rp-card"><p class="rp-meta">The brand was not read.</p></div>`;
68065
- const sw = (label2, c) => c && HEX2.test(c.hex) ? `<span class="rp-swatch"><i style="background:${c.hex.toLowerCase()}"></i>${esc15(label2)} ${esc15(c.hex.toLowerCase())}</span>` : "";
68752
+ const sw = (label2, c) => c && HEX2.test(c.hex) ? `<span class="rp-swatch"><i style="background:${c.hex.toLowerCase()}"></i>${esc16(label2)} ${esc16(c.hex.toLowerCase())}</span>` : "";
68066
68753
  const norm6 = (x) => x.toLowerCase().replace(/[^a-z0-9]/g, "");
68067
68754
  const core = b.names.filter((n) => n.where !== "page title").map((n) => n.name);
68068
68755
  const base = core[0] ?? "";
@@ -68073,12 +68760,12 @@ function brandCard(r) {
68073
68760
  if (!b.shareImage) notes.push("No share image: a link to the site posted on Facebook or in a text shows no picture.");
68074
68761
  if (clash) notes.push("The site does not call itself by one name everywhere (listed on the left).");
68075
68762
  return `<div class="rp-two"><div class="rp-card"><h3>What a visitor sees</h3><dl class="rp-kv">
68076
- ${b.names.map((n) => `<dt>${esc15(n.where.charAt(0).toUpperCase() + n.where.slice(1))}</dt><dd>${esc15(n.name)}</dd>`).join("")}
68077
- <dt>Logo</dt><dd>${b.logo ? esc15(b.logo.url ? `${b.logo.where}: ${b.logo.url}` : b.logo.where) : "Not found"}</dd>
68078
- <dt>Browser-tab icon</dt><dd>${b.favicon ? esc15(b.favicon) : "Not found"}</dd>
68079
- <dt>Share image</dt><dd>${b.shareImage ? esc15(b.shareImage) : "Not found"}</dd>
68080
- ${b.headlines.length ? `<dt>Headlines</dt><dd>${b.headlines.map((h) => `"${esc15(h)}"`).join("<br>")}</dd>` : ""}
68081
- </dl></div><div class="rp-card"><h3>Colours and type</h3>${look && (look.background || look.button) ? `<div class="rp-swatches">${sw("Buttons", look.button ? { hex: look.button, source: "" } : void 0)}${sw("Background", look.background ? { hex: look.background, source: "" } : void 0)}${sw("Text", look.text ? { hex: look.text, source: "" } : void 0)}</div>${look.headingFont || look.bodyFont ? `<dl class="rp-kv"><dt>Headings</dt><dd>${esc15(look.headingFont ?? "Not read")}</dd><dt>Body text</dt><dd>${esc15(look.bodyFont ?? "Not read")}</dd></dl>` : ""}<p class="rp-meta">As the phone view drew the home page.</p>` : b.colors ? `<div class="rp-swatches">${sw("Accent", b.colors.accent)}${sw("Background", b.colors.background)}${sw("Text", b.colors.text)}</div>${b.fonts ? `<dl class="rp-kv"><dt>Headings</dt><dd>${esc15(b.fonts.display)}</dd><dt>Body text</dt><dd>${esc15(b.fonts.body)}</dd></dl>` : ""}<p class="rp-meta">Read from the site's stylesheets only (the phone view did not run), so these are a best reading${b.confidence !== void 0 ? `: ${Math.round(b.confidence * 100)}% were declared by the site, the rest are the most used values` : ""}.</p>` : `<p class="rp-meta">${esc15(b.unread ?? "Colours were not read.")}</p>`}${notes.length ? `<ul class="rp-list">${notes.map((n) => `<li>${esc15(n)}</li>`).join("")}</ul>` : `<p class="rp-meta">Logo, tab icon, share image and name are all in place.</p>`}</div></div>`;
68763
+ ${b.names.map((n) => `<dt>${esc16(n.where.charAt(0).toUpperCase() + n.where.slice(1))}</dt><dd>${esc16(n.name)}</dd>`).join("")}
68764
+ <dt>Logo</dt><dd>${b.logo ? esc16(b.logo.url ? `${b.logo.where}: ${b.logo.url}` : b.logo.where) : "Not found"}</dd>
68765
+ <dt>Browser-tab icon</dt><dd>${b.favicon ? esc16(b.favicon) : "Not found"}</dd>
68766
+ <dt>Share image</dt><dd>${b.shareImage ? esc16(b.shareImage) : "Not found"}</dd>
68767
+ ${b.headlines.length ? `<dt>Headlines</dt><dd>${b.headlines.map((h) => `"${esc16(h)}"`).join("<br>")}</dd>` : ""}
68768
+ </dl></div><div class="rp-card"><h3>Colours and type</h3>${look && (look.background || look.button) ? `<div class="rp-swatches">${sw("Buttons", look.button ? { hex: look.button, source: "" } : void 0)}${sw("Background", look.background ? { hex: look.background, source: "" } : void 0)}${sw("Text", look.text ? { hex: look.text, source: "" } : void 0)}</div>${look.headingFont || look.bodyFont ? `<dl class="rp-kv"><dt>Headings</dt><dd>${esc16(look.headingFont ?? "Not read")}</dd><dt>Body text</dt><dd>${esc16(look.bodyFont ?? "Not read")}</dd></dl>` : ""}<p class="rp-meta">As the phone view drew the home page.</p>` : b.colors ? `<div class="rp-swatches">${sw("Accent", b.colors.accent)}${sw("Background", b.colors.background)}${sw("Text", b.colors.text)}</div>${b.fonts ? `<dl class="rp-kv"><dt>Headings</dt><dd>${esc16(b.fonts.display)}</dd><dt>Body text</dt><dd>${esc16(b.fonts.body)}</dd></dl>` : ""}<p class="rp-meta">Read from the site's stylesheets only (the phone view did not run), so these are a best reading${b.confidence !== void 0 ? `: ${Math.round(b.confidence * 100)}% were declared by the site, the rest are the most used values` : ""}.</p>` : `<p class="rp-meta">${esc16(b.unread ?? "Colours were not read.")}</p>`}${notes.length ? `<ul class="rp-list">${notes.map((n) => `<li>${esc16(n)}</li>`).join("")}</ul>` : `<p class="rp-meta">Logo, tab icon, share image and name are all in place.</p>`}</div></div>`;
68082
68769
  }
68083
68770
  function memberColumn(ids, packages, sold) {
68084
68771
  if (!sold && !packages.length) return void 0;
@@ -68115,17 +68802,17 @@ function compareTable(r, member) {
68115
68802
  const shown = COMPARE.filter(([id]) => sites.some((s) => s.checks.some((c) => c.id === id && c.verdict !== "unknown")));
68116
68803
  const col = member ? memberColumn(shown.map(([id]) => id), member.packages, member.sold) : void 0;
68117
68804
  const covered = col ? [...col.values()].filter(Boolean).length : 0;
68118
- const ours = col && member ? `<th scope="col">${esc15(`With ${member.name}`)}<br><small>${esc15(`${covered} of ${shown.length} included`)}</small></th>` : "";
68805
+ const ours = col && member ? `<th scope="col">${esc16(`With ${member.name}`)}<br><small>${esc16(`${covered} of ${shown.length} included`)}</small></th>` : "";
68119
68806
  const oursCell = (id) => {
68120
68807
  if (!col) return "";
68121
68808
  const n = col.get(id);
68122
- return n ? `<td class="cds-yes">Yes<br><small>${esc15(n)}</small></td>` : `<td class="cds-unk">Not included</td>`;
68809
+ return n ? `<td class="cds-yes">Yes<br><small>${esc16(n)}</small></td>` : `<td class="cds-unk">Not included</td>`;
68123
68810
  };
68124
- const head4 = sites.map((s, i) => `<th scope="col"${hl(i)}>${esc15(s.name)}<br><small>${esc15(new URL(s.site.origin).hostname.replace(/^www\./, ""))}</small></th>`).join("") + ours;
68811
+ const head4 = sites.map((s, i) => `<th scope="col"${hl(i)}>${esc16(s.name)}<br><small>${esc16(new URL(s.site.origin).hostname.replace(/^www\./, ""))}</small></th>`).join("") + ours;
68125
68812
  const gradeRow = `<tr><th scope="row">Grade<small>Only the areas checked for each</small></th>${sites.map((s, i) => td(i, s.score.score !== void 0 ? `<td class="cds-num rp-grade"><span class="${gcls2(s.score.score)}">${gradeOf(s.score.score)}</span> ${s.score.score}</td>` : `<td class="cds-unk">Could not open</td>`)).join("")}${col ? `<td></td>` : ""}</tr>`;
68126
- const rows2 = shown.map(([id, label2]) => `<tr><th scope="row">${esc15(label2)}</th>${sites.map((s, i) => td(i, cell2(s, id))).join("")}${oursCell(id)}</tr>`).join("");
68813
+ const rows2 = shown.map(([id, label2]) => `<tr><th scope="row">${esc16(label2)}</th>${sites.map((s, i) => td(i, cell2(s, id))).join("")}${oursCell(id)}</tr>`).join("");
68127
68814
  const colNote = col && member ? ` The last column is what ${member.name} sets up, named as ${member.name} sells it.` : "";
68128
- return `<div class="rp-card"><div class="cds-matrix-wrap rp-scroll"><table class="cds-matrix"><thead><tr><th scope="col"></th>${head4}</tr></thead><tbody>${gradeRow}${rows2}</tbody></table></div><p class="rp-meta">Every site was read the same way within minutes of each other: the home page and up to ${r.competitors.length ? "two" : "three"} more pages its menu links to, robots.txt, the sitemap and one phone-size view. ${r.competitors.length ? "Competitors are the sites the agency named; none were searched for or added." : ""}${esc15(colNote)}</p>${sites.filter((s) => !s.site.reachable).map((s) => callout3(esc15(`${s.name}: ${s.site.unreachableReason ?? "could not be opened"}. Its column says "Not checked", never a fail.`), { tone: "warn" })).join("")}</div>`;
68815
+ return `<div class="rp-card"><div class="cds-matrix-wrap rp-scroll"><table class="cds-matrix"><thead><tr><th scope="col"></th>${head4}</tr></thead><tbody>${gradeRow}${rows2}</tbody></table></div><p class="rp-meta">Every site was read the same way within minutes of each other: the home page and up to ${r.competitors.length ? "two" : "three"} more pages its menu links to, robots.txt, the sitemap and one phone-size view. ${r.competitors.length ? "Competitors are the sites the agency named; none were searched for or added." : ""}${esc16(colNote)}</p>${sites.filter((s) => !s.site.reachable).map((s) => callout3(esc16(`${s.name}: ${s.site.unreachableReason ?? "could not be opened"}. Its column says "Not checked", never a fail.`), { tone: "warn" })).join("")}</div>`;
68129
68816
  }
68130
68817
  function offerParts(r, packages, sold, audience = "client") {
68131
68818
  const gaps = gapsOf(r.business.checks);
@@ -68151,10 +68838,10 @@ function offerParts(r, packages, sold, audience = "client") {
68151
68838
  const pk = allHave ? packages.filter((p) => fs52.every((f) => p.features.includes(f))).map((p) => withoutPrice(p.name)).filter(Boolean) : [];
68152
68839
  const inPk = pk.length ? `Included in ${list3(pk)}.` : allHave && fs52.length === 1 ? FEATURES[fs52[0]].what : "";
68153
68840
  const more = e.gaps.length > 2 ? ` And ${e.gaps.length - 2} more like ${e.gaps.length - 2 === 1 ? "it" : "them"}.` : "";
68154
- return `<li><span class="svc">${esc15(svc)}</span><span class="why">${esc15(e.gaps.slice(0, 2).join(" ") + more)}</span>${inPk ? `<span class="why">${esc15(inPk)}</span>` : ""}</li>`;
68841
+ return `<li><span class="svc">${esc16(svc)}</span><span class="why">${esc16(e.gaps.slice(0, 2).join(" ") + more)}</span>${inPk ? `<span class="why">${esc16(inPk)}</span>` : ""}</li>`;
68155
68842
  }).join("");
68156
68843
  const pitchCard = items ? `<div class="rp-card"><ul class="rp-offer">${items}</ul></div>` : "";
68157
- const plainCard = plain.length ? `<div class="rp-card"><h3>${items ? "Also found" : "What was found"}</h3><ul class="rp-list">${plain.map((g) => `<li>${esc15(g)}</li>`).join("")}</ul></div>` : "";
68844
+ const plainCard = plain.length ? `<div class="rp-card"><h3>${items ? "Also found" : "What was found"}</h3><ul class="rp-list">${plain.map((g) => `<li>${esc16(g)}</li>`).join("")}</ul></div>` : "";
68158
68845
  return {
68159
68846
  label: items ? "What closes the gaps" : "The gaps",
68160
68847
  // The one gap count (gapCount), the same number as the hero and the tile.
@@ -68170,7 +68857,7 @@ function speedNote(r) {
68170
68857
  const ph = r.business.site.phone;
68171
68858
  const fallback = ph?.ok && ph.lcpMs !== void 0 ? "The speed shown was measured by a phone-size Chrome on a fast office connection, which flatters slow sites." : "No other speed measurement was made, so speed is not checked in this report.";
68172
68859
  const why = sc.google === "no_key" ? "Google PageSpeed did not run for this report: no PageSpeed key is set up." : `Google PageSpeed was asked but gave no score: ${sc.reason.replace(/\.$/, "")}.`;
68173
- return callout3(esc15(`${why} ${fallback}`), { tone: "warn", title: "Speed check" });
68860
+ return callout3(esc16(`${why} ${fallback}`), { tone: "warn", title: "Speed check" });
68174
68861
  }
68175
68862
  function scoringTable() {
68176
68863
  const total = AREA_IDS.reduce((a, id) => a + SCORING.areas[id].weight, 0);
@@ -68178,19 +68865,19 @@ function scoringTable() {
68178
68865
  const rows2 = AREA_IDS.map((a) => {
68179
68866
  const cs = CHECK_IDS.filter((id) => SCORING.checks[id].area === a);
68180
68867
  const w = SCORING.areas[a].weight;
68181
- return `<tr><th scope="row">${esc15(SCORING.areas[a].label)}</th><td class="rp-num">${w} (${Math.round(100 * w / total)}%)</td><td><ul class="rp-list">${cs.map((id) => `<li>${esc15(SCORING.checks[id].label)}: ${pts(SCORING.checks[id].points)}</li>`).join("")}</ul></td></tr>`;
68868
+ return `<tr><th scope="row">${esc16(SCORING.areas[a].label)}</th><td class="rp-num">${w} (${Math.round(100 * w / total)}%)</td><td><ul class="rp-list">${cs.map((id) => `<li>${esc16(SCORING.checks[id].label)}: ${pts(SCORING.checks[id].points)}</li>`).join("")}</ul></td></tr>`;
68182
68869
  }).join("");
68183
- return `<div class="rp-card"><div class="rp-scroll"><table class="rp-score"><thead><tr><th scope="col">Area</th><th scope="col">Weight (share of the grade)</th><th scope="col">What is checked, and its points</th></tr></thead><tbody>${rows2}</tbody></table></div><p class="rp-meta">${esc15(`Each area scores 0 to 100 from its checks: a check earns all its points on a yes, half on a partly (or its measured share, such as a Google score of 62 out of 100), and none on a no. The grade is the average of the areas that were checked, each counted by its weight; an area that was not checked is left out and the rest count for more. With no working way to contact the business at all, the grade is capped at ${SCORING.capNoContact}.`)}</p></div>`;
68870
+ return `<div class="rp-card"><div class="rp-scroll"><table class="rp-score"><thead><tr><th scope="col">Area</th><th scope="col">Weight (share of the grade)</th><th scope="col">What is checked, and its points</th></tr></thead><tbody>${rows2}</tbody></table></div><p class="rp-meta">${esc16(`Each area scores 0 to 100 from its checks: a check earns all its points on a yes, half on a partly (or its measured share, such as a Google score of 62 out of 100), and none on a no. The grade is the average of the areas that were checked, each counted by its weight; an area that was not checked is left out and the rest count for more. With no working way to contact the business at all, the grade is capped at ${SCORING.capNoContact}.`)}</p></div>`;
68184
68871
  }
68185
68872
  function evidenceCard(r) {
68186
68873
  const sites = [r.business, ...r.competitors];
68187
68874
  return sites.map((s) => {
68188
- const rows2 = s.site.log.map((l) => `<tr><td>${esc15(clock(l.at))}</td><td>${esc15(l.what)}</td><td>${esc15(l.url)}${l.finalUrl ? `<br>moved to ${esc15(l.finalUrl)}` : ""}</td><td>${l.status || "none"}</td><td>${l.ms} ms</td><td>${esc15(l.note ?? "")}</td></tr>`).join("");
68875
+ const rows2 = s.site.log.map((l) => `<tr><td>${esc16(clock2(l.at))}</td><td>${esc16(l.what)}</td><td>${esc16(l.url)}${l.finalUrl ? `<br>moved to ${esc16(l.finalUrl)}` : ""}</td><td>${l.status || "none"}</td><td>${l.ms} ms</td><td>${esc16(l.note ?? "")}</td></tr>`).join("");
68189
68876
  const ph = s.site.phone;
68190
- const phoneRow = ph ? `<p class="rp-meta">${ph.ok ? esc15(`Phone view (Chrome at 390 by 844 pixels, a fresh browser with no cookies), ${clock(ph.at)}: ${ph.requests} files loaded from ${ph.hosts.length} addresses; main content ${ph.lcpMs !== void 0 ? `after ${(ph.lcpMs / 1e3).toFixed(1)} seconds` : "time not reported"}; fully loaded ${ph.loadMs !== void 0 ? `after ${(ph.loadMs / 1e3).toFixed(1)} seconds` : "time not reported"}. Measured on an office connection, not a phone network.`) : esc15(`Phone view could not run: ${ph.reason}.`)}</p>` : "";
68877
+ const phoneRow = ph ? `<p class="rp-meta">${ph.ok ? esc16(`Phone view (Chrome at 390 by 844 pixels, a fresh browser with no cookies), ${clock2(ph.at)}: ${ph.requests} files loaded from ${ph.hosts.length} addresses; main content ${ph.lcpMs !== void 0 ? `after ${(ph.lcpMs / 1e3).toFixed(1)} seconds` : "time not reported"}; fully loaded ${ph.loadMs !== void 0 ? `after ${(ph.loadMs / 1e3).toFixed(1)} seconds` : "time not reported"}. Measured on an office connection, not a phone network.`) : esc16(`Phone view could not run: ${ph.reason}.`)}</p>` : "";
68191
68878
  const ps = s.site.pagespeed;
68192
- const psRow = ps ? `<p class="rp-meta">${esc15(ps.ok ? `Google PageSpeed (phone), ${clock(ps.at)}: speed ${ps.performance}/100${ps.seo !== void 0 ? `, search ${ps.seo}/100` : ""}.` : `Google PageSpeed not available: ${ps.reason}.`)}</p>` : "";
68193
- return `<details class="rp-card"><summary>${esc15(s.name)}: ${s.site.log.length} requests</summary><p class="rp-meta">${esc15(s.site.robots.unreadable ? `robots.txt could not be read (${s.site.robots.unreadable}), so the site was treated as closed to visitors.` : `robots.txt ${s.site.robots.found ? "was read and followed" : "was not there (the address answered not found), so nothing is off limits"}${s.site.finalOrigin && s.site.finalOrigin !== s.site.origin ? `, for ${s.site.origin.replace(/^https?:\/\//, "")} and again for ${s.site.finalOrigin.replace(/^https?:\/\//, "")} where it moved` : ""}.${s.site.robots.homeAllowed ? "" : " It asks visitors not to open the home page, so nothing was read."}`)}</p>${phoneRow}${psRow}<div class="rp-scroll"><table class="rp-log"><thead><tr><th>Time</th><th>What</th><th>Address</th><th>Answer</th><th>Took</th><th>Note</th></tr></thead><tbody>${rows2}</tbody></table></div></details>`;
68879
+ const psRow = ps ? `<p class="rp-meta">${esc16(ps.ok ? `Google PageSpeed (phone), ${clock2(ps.at)}: speed ${ps.performance}/100${ps.seo !== void 0 ? `, search ${ps.seo}/100` : ""}.` : `Google PageSpeed not available: ${ps.reason}.`)}</p>` : "";
68880
+ return `<details class="rp-card"><summary>${esc16(s.name)}: ${s.site.log.length} requests</summary><p class="rp-meta">${esc16(s.site.robots.unreadable ? `robots.txt could not be read (${s.site.robots.unreadable}), so the site was treated as closed to visitors.` : `robots.txt ${s.site.robots.found ? "was read and followed" : "was not there (the address answered not found), so nothing is off limits"}${s.site.finalOrigin && s.site.finalOrigin !== s.site.origin ? `, for ${s.site.origin.replace(/^https?:\/\//, "")} and again for ${s.site.finalOrigin.replace(/^https?:\/\//, "")} where it moved` : ""}.${s.site.robots.homeAllowed ? "" : " It asks visitors not to open the home page, so nothing was read."}`)}</p>${phoneRow}${psRow}<div class="rp-scroll"><table class="rp-log"><thead><tr><th>Time</th><th>What</th><th>Address</th><th>Answer</th><th>Took</th><th>Note</th></tr></thead><tbody>${rows2}</tbody></table></div></details>`;
68194
68881
  }).join("");
68195
68882
  }
68196
68883
  function prospectReportHtml(r, opts = {}) {
@@ -68227,21 +68914,21 @@ function prospectReportHtml(r, opts = {}) {
68227
68914
  })()
68228
68915
  ].join("");
68229
68916
  const worst = gaps[0];
68230
- const quote = worst ? callout3(esc15(inAudience(worst.gap, aud)), { quote: true, tone: "bad", cite: worst.evidence }) : callout3(site.reachable ? "Everything that was checked is in place." : esc15(site.unreachableReason ?? "The site could not be opened."), { quote: true, tone: site.reachable ? "info" : "warn" });
68231
- const bars = sc.areas.map((a) => a.score !== void 0 ? `<div class="cds-bar-row"><span class="name">${esc15(a.label)}</span><span class="trk"><i class="fill ${gcls2(a.score)}" style="width:${a.score}%"></i></span><span class="val"><span class="${gcls2(a.score)}">${gradeOf(a.score)}</span> ${a.score}</span></div>` : `<div class="cds-bar-row"><span class="name">${esc15(a.label)}</span><span class="trk"></span><span class="val cds-unk">Not checked</span></div>`).join("");
68917
+ const quote = worst ? callout3(esc16(inAudience(worst.gap, aud)), { quote: true, tone: "bad", cite: worst.evidence }) : callout3(site.reachable ? "Everything that was checked is in place." : esc16(site.unreachableReason ?? "The site could not be opened."), { quote: true, tone: site.reachable ? "info" : "warn" });
68918
+ const bars = sc.areas.map((a) => a.score !== void 0 ? `<div class="cds-bar-row"><span class="name">${esc16(a.label)}</span><span class="trk"><i class="fill ${gcls2(a.score)}" style="width:${a.score}%"></i></span><span class="val"><span class="${gcls2(a.score)}">${gradeOf(a.score)}</span> ${a.score}</span></div>` : `<div class="cds-bar-row"><span class="name">${esc16(a.label)}</span><span class="trk"></span><span class="val cds-unk">Not checked</span></div>`).join("");
68232
68919
  const areasOrder = ["website", "search", "ai", "google", "contact"];
68233
68920
  const cards = areasOrder.map((a) => checksCard(a, b.checks));
68234
68921
  const gbpLinks = site.home ? [...new Set([site.home, ...site.pages].flatMap((p) => p.googleProfileLinks))] : [];
68235
68922
  const mapsEmbed = site.home ? [site.home, ...site.pages].some((p) => p.mapsEmbed) : false;
68236
- const gbpNote = callout3(esc15(`What the website shows about the profile: ${gbpLinks.length ? `${gbpLinks.length === 1 ? "a link" : `${gbpLinks.length} links`} to Google (${list3([...new Set(gbpLinks.map(googleLinkKind))])})` : "no link to a Google profile"}${mapsEmbed ? " and an embedded Google map" : ", no embedded Google map"}. Whether the profile exists, its rating and review count, and whether its hours and phone match the website need a Google Maps key, which is not set up yet, so they are not checked and not scored.`), { tone: "info", title: "Google Business Profile" });
68237
- const aiList = r.aiAnswers.length ? `<div class="rp-card"><h3>What AI said when asked</h3><ul class="rp-list">${r.aiAnswers.map((a) => `<li>${esc15(`"${a.question}" (${a.engine}, ${when2(a.askedAt)}): ${a.namesBusiness ? `named ${name}` : `did not name ${name}`}${a.othersNamed.length ? `; named ${list3([...a.othersNamed.slice(0, 5), ...a.othersNamed.length > 5 ? [`${a.othersNamed.length - 5} more`] : []])}` : ""}.`)}</li>`).join("")}</ul></div>` : "";
68923
+ const gbpNote = callout3(esc16(`What the website shows about the profile: ${gbpLinks.length ? `${gbpLinks.length === 1 ? "a link" : `${gbpLinks.length} links`} to Google (${list3([...new Set(gbpLinks.map(googleLinkKind))])})` : "no link to a Google profile"}${mapsEmbed ? " and an embedded Google map" : ", no embedded Google map"}. Whether the profile exists, its rating and review count, and whether its hours and phone match the website need a Google Maps key, which is not set up yet, so they are not checked and not scored.`), { tone: "info", title: "Google Business Profile" });
68924
+ const aiList = r.aiAnswers.length ? `<div class="rp-card"><h3>What AI said when asked</h3><ul class="rp-list">${r.aiAnswers.map((a) => `<li>${esc16(`"${a.question}" (${a.engine}, ${when2(a.askedAt)}): ${a.namesBusiness ? `named ${name}` : `did not name ${name}`}${a.othersNamed.length ? `; named ${list3([...a.othersNamed.slice(0, 5), ...a.othersNamed.length > 5 ? [`${a.othersNamed.length - 5} more`] : []])}` : ""}.`)}</li>`).join("")}</ul></div>` : "";
68238
68925
  const offer = offerParts(r, opts.packages ?? [], opts.services, aud);
68239
68926
  const fu = r.followUp;
68240
68927
  const sellsFollowUp = !!serviceFor("followup", offeredNames(opts.services));
68241
68928
  const offerLine = fu.form === "none" ? "We'll go through it with you on the walkthrough." : "We'll run it with you on the walkthrough.";
68242
- const followHtml = callout3(`<p class="rp-meta">${esc15(sellsFollowUp ? fu.note.replace(/\s*Not run for this report\.$/, "") : fu.note)}</p>${sellsFollowUp ? `<p><strong>${esc15(offerLine)}</strong></p>${bookButton(brand, opts.prefill)}` : ""}`, { tone: fu.form === "found" ? "info" : "warn", title: "Follow-up test" });
68929
+ const followHtml = callout3(`<p class="rp-meta">${esc16(sellsFollowUp ? fu.note.replace(/\s*Not run for this report\.$/, "") : fu.note)}</p>${sellsFollowUp ? `<p><strong>${esc16(offerLine)}</strong></p>${bookButton(brand, opts.prefill)}` : ""}`, { tone: fu.form === "found" ? "info" : "warn", title: "Follow-up test" });
68243
68930
  const reach = reachLinks(brand);
68244
- const footRight = [reach.length ? `${esc15(brand.name)} \xB7 ${reach.join(" \xB7 ")}` : esc15(brand.name), ...opts.credit ? [esc15(PRODUCT_CREDIT)] : []].join("<br>");
68931
+ const footRight = [reach.length ? `${esc16(brand.name)} \xB7 ${reach.join(" \xB7 ")}` : esc16(brand.name), ...opts.credit ? [esc16(PRODUCT_CREDIT)] : []].join("<br>");
68245
68932
  const notChecked = [
68246
68933
  "the Google Business Profile itself (needs a Google Maps key)",
68247
68934
  "keyword rankings and backlinks (need paid data)",
@@ -68250,33 +68937,33 @@ function prospectReportHtml(r, opts = {}) {
68250
68937
  "how fast they answer a new enquiry (the follow-up add-on)",
68251
68938
  "phone calls, including after hours"
68252
68939
  ];
68253
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc15(`Secret Shopper \xB7 ${name}`)}</title>
68940
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc16(`Secret Shopper \xB7 ${name}`)}</title>
68254
68941
  <style>${DS_TOKENS_CSS}</style>
68255
68942
  <style>${DS_BUNDLE_CSS}</style>
68256
68943
  <style>${LAYOUT_CSS2}</style></head><body>
68257
68944
  <div class="rp">
68258
68945
  <div class="rp-tools cds-no-print"><button class="cds-btn" type="button" id="save-pdf">Save as PDF</button></div>
68259
- <header data-section="bar" class="rp-bar"><div class="rp-brand">${esc15(brand.name)} <span>Secret Shopper</span></div>${reach.length ? `<div class="rp-contact">${reach.join(" \xB7 ")}</div>` : ""}</header>
68260
- <section data-section="hero" class="rp-hero"><div class="rp-hero-text"><div class="rp-kicker">${esc15(`We looked at ${name} the way a new ${aud} does`)}</div><h1>${esc15(h1)}</h1><p class="rp-sub">${esc15(sub)}</p><p class="rp-meta">${esc15(meta2)}</p></div><div class="rp-ringbox">${gradeRing2(sc.score, caption)}</div></section>
68946
+ <header data-section="bar" class="rp-bar"><div class="rp-brand">${esc16(brand.name)} <span>Secret Shopper</span></div>${reach.length ? `<div class="rp-contact">${reach.join(" \xB7 ")}</div>` : ""}</header>
68947
+ <section data-section="hero" class="rp-hero"><div class="rp-hero-text"><div class="rp-kicker">${esc16(`We looked at ${name} the way a new ${aud} does`)}</div><h1>${esc16(h1)}</h1><p class="rp-sub">${esc16(sub)}</p><p class="rp-meta">${esc16(meta2)}</p></div><div class="rp-ringbox">${gradeRing2(sc.score, caption)}</div></section>
68261
68948
  <section data-section="tiles" class="rp-band"><div class="rp-tiles">${tiles}</div></section>
68262
68949
  <section data-section="quote" class="rp-band">${quote}</section>
68263
68950
  ${section("brand", "1 \xB7 Branding", "How the brand comes across", "Read from the home page and its stylesheets", brandCard(b))}
68264
- ${section("areas", "2 \xB7 Website check", "Score by area", "Only areas that could be checked are graded", `<div class="rp-card"><div class="cds-bars">${bars}</div><div class="cds-src">${esc15(`From ${r.requests} requests to the public websites and one phone-size view each, ${when2(r.generatedAt)}. Weighted average of the areas checked; "Not checked" is never counted as a fail.`)}</div></div>`)}
68951
+ ${section("areas", "2 \xB7 Website check", "Score by area", "Only areas that could be checked are graded", `<div class="rp-card"><div class="cds-bars">${bars}</div><div class="cds-src">${esc16(`From ${r.requests} requests to the public websites and one phone-size view each, ${when2(r.generatedAt)}. Weighted average of the areas checked; "Not checked" is never counted as a fail.`)}</div></div>`)}
68265
68952
  <section data-section="checks" class="rp-band"><div class="rp-two">${cards.join("")}</div><div class="rp-gap"></div>${speedNote(r) ? `${speedNote(r)}<div class="rp-gap"></div>` : ""}${gbpNote}${aiList ? `<div class="rp-gap"></div>${aiList}` : ""}</section>
68266
68953
  ${section("compare", "3 \xB7 Competitors", r.competitors.length ? `How ${name} compares` : "No competitors were named", r.competitors.length ? `${r.competitors.length} nearby ${r.competitors.length === 1 ? "business" : "businesses"} named by the agency` : "Name up to 3 competitor websites to add this comparison", r.competitors.length ? compareTable(r, { name: brand.name, packages: opts.packages ?? [], sold: opts.services }) : `<div class="rp-card"><p class="rp-meta">This report compares only with sites the agency names. None were named, so no comparison is shown and none is invented.</p></div>`, true)}
68267
68954
  ${section("offer", offer.label, offer.h2, "", offer.inner)}
68268
68955
  ${walkthroughBlock("walkthrough", brand, opts.workflowAudit === true, opts.prefill)}
68269
68956
  ${section("followup", "4 \xB7 Follow-up test", "How fast they answer a new enquiry", "Optional add-on, 1 to 2 days", followHtml)}
68270
68957
  ${section("evidence", "Evidence", "Every request this report made", "Public pages only; nothing was logged into or submitted", evidenceCard(r), true)}
68271
- <section data-section="coverage" class="rp-band">${callout3(esc15(`Not checked in this report: ${list3(notChecked)}.`), { tone: "info", title: "Not checked" })}</section>
68958
+ <section data-section="coverage" class="rp-band">${callout3(esc16(`Not checked in this report: ${list3(notChecked)}.`), { tone: "info", title: "Not checked" })}</section>
68272
68959
  ${section("scoring", "How the score is built", "Every area and check, with its weight", "The same for every business in this report", scoringTable(), true)}
68273
68960
  ${walkthroughBlock("book", brand, opts.workflowAudit === true, opts.prefill)}
68274
- <footer data-section="footer" class="rp-foot"><span>${esc15(`Read from ${name}'s public website${r.competitors.length ? " and the competitors named" : ""} on ${when2(r.generatedAt)}. Nothing was changed, submitted or logged into.`)}</span><span class="rp-who">${footRight}</span></footer>
68961
+ <footer data-section="footer" class="rp-foot"><span>${esc16(`Read from ${name}'s public website${r.competitors.length ? " and the competitors named" : ""} on ${when2(r.generatedAt)}. Nothing was changed, submitted or logged into.`)}</span><span class="rp-who">${footRight}</span></footer>
68275
68962
  </div>
68276
68963
  <script>document.getElementById("save-pdf").addEventListener("click",function(){window.print()});window.addEventListener("beforeprint",function(){document.querySelectorAll("details").forEach(function(d){d.open=true})});</script>
68277
68964
  </body></html>`;
68278
68965
  }
68279
- var esc15, when2, clock, HEX2, gcls2, chip4, VERDICT, statTile3, callout3, section, link2, shortUrl, BOOKING_UTM, isGhlBookingWidget, LAYOUT_CSS2, DEFAULT_PROSPECT_BRANDING, PRODUCT_CREDIT, WALKTHROUGH, COMPARE, featureOf, gapCount, gapWords, AUDIENCES, PATIENT_WORDS, CLIENT_WORDS, inAudience, PATIENT_LINE, gapsOf;
68966
+ var esc16, when2, clock2, HEX2, gcls2, chip4, VERDICT, statTile3, callout3, section, link2, shortUrl, BOOKING_UTM, isGhlBookingWidget, LAYOUT_CSS2, DEFAULT_PROSPECT_BRANDING, PRODUCT_CREDIT, WALKTHROUGH, COMPARE, featureOf, gapCount, gapWords, AUDIENCES, PATIENT_WORDS, CLIENT_WORDS, inAudience, PATIENT_LINE, gapsOf;
68280
68967
  var init_report_page2 = __esm({
68281
68968
  "src/command-os/prospect/report-page.ts"() {
68282
68969
  "use strict";
@@ -68286,19 +68973,19 @@ var init_report_page2 = __esm({
68286
68973
  init_features();
68287
68974
  init_services();
68288
68975
  init_weights();
68289
- esc15 = (s) => s.replace(new RegExp("\\p{Extended_Pictographic}\\uFE0F?", "gu"), "").replace(/\s*\u2014\s*/g, " - ").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
68976
+ esc16 = (s) => s.replace(new RegExp("\\p{Extended_Pictographic}\\uFE0F?", "gu"), "").replace(/\s*\u2014\s*/g, " - ").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
68290
68977
  when2 = (iso2) => new Date(iso2).toLocaleString("en-US", { weekday: "short", month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short" });
68291
- clock = (iso2) => new Date(iso2).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit" });
68978
+ clock2 = (iso2) => new Date(iso2).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit" });
68292
68979
  HEX2 = /^#[0-9a-f]{6}$/i;
68293
68980
  gcls2 = (score2) => `cds-g-${gradeOf(score2).toLowerCase()}`;
68294
- chip4 = (tone, text2) => `<span class="cds-chip ${tone}">${esc15(text2)}</span>`;
68981
+ chip4 = (tone, text2) => `<span class="cds-chip ${tone}">${esc16(text2)}</span>`;
68295
68982
  VERDICT = { pass: ["ok", "Yes"], partial: ["warn", "Partly"], fail: ["bad", "No"], unknown: ["", "Not checked"] };
68296
- statTile3 = (value, label2, tone = "") => `<div class="cds-stat"><div class="big ${tone}">${esc15(String(value))}</div><div class="lbl">${esc15(label2)}</div></div>`;
68297
- callout3 = (inner, o) => `<div class="cds-callout ${o.quote ? "quote" : ""} ${o.tone ?? ""}">${o.title ? `<span class="ttl">${esc15(o.title)}</span>` : ""}${inner}${o.cite ? `<cite>${esc15(o.cite)}</cite>` : ""}</div>`;
68298
- section = (id, label2, h2, right, inner, divider = false) => `<section data-section="${id}" id="${id}" class="rp-section"><div class="rp-head${divider ? " rp-divider" : ""}"><div><div class="rp-label">${esc15(label2)}</div><h2>${esc15(h2)}</h2></div>${right ? `<div class="rp-right">${esc15(right)}</div>` : ""}</div>${inner}</section>`;
68983
+ statTile3 = (value, label2, tone = "") => `<div class="cds-stat"><div class="big ${tone}">${esc16(String(value))}</div><div class="lbl">${esc16(label2)}</div></div>`;
68984
+ callout3 = (inner, o) => `<div class="cds-callout ${o.quote ? "quote" : ""} ${o.tone ?? ""}">${o.title ? `<span class="ttl">${esc16(o.title)}</span>` : ""}${inner}${o.cite ? `<cite>${esc16(o.cite)}</cite>` : ""}</div>`;
68985
+ section = (id, label2, h2, right, inner, divider = false) => `<section data-section="${id}" id="${id}" class="rp-section"><div class="rp-head${divider ? " rp-divider" : ""}"><div><div class="rp-label">${esc16(label2)}</div><h2>${esc16(h2)}</h2></div>${right ? `<div class="rp-right">${esc16(right)}</div>` : ""}</div>${inner}</section>`;
68299
68986
  link2 = (href, text2, cls = "") => {
68300
68987
  const h = safeHref(href);
68301
- return h ? `<a${cls ? ` class="${cls}"` : ""} href="${esc15(h)}">${esc15(text2)}</a>` : esc15(text2);
68988
+ return h ? `<a${cls ? ` class="${cls}"` : ""} href="${esc16(h)}">${esc16(text2)}</a>` : esc16(text2);
68302
68989
  };
68303
68990
  shortUrl = (u) => u.replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
68304
68991
  BOOKING_UTM = "utm_source=secret-shopper";
@@ -69065,6 +69752,8 @@ function compactExecute(full) {
69065
69752
  published: full.published,
69066
69753
  publishFailed: full.publishFailed,
69067
69754
  nextManualSteps: full.nextManualSteps,
69755
+ // The account's custom values after the build (BUG-2026-09-30-11): the only source for "empty".
69756
+ ...full.customValues ? { customValues: full.customValues, customValuesNote: full.customValuesNote } : {},
69068
69757
  ...full.journalWarning ? { journalWarning: full.journalWarning } : {},
69069
69758
  ...composed ? { composed: { presetId: composed.presetId, presetVersion: composed.presetVersion, note: composed.note, warnings: composed.warnings, scope: composed.scope, scopeSource: composed.scopeSource } } : {},
69070
69759
  // A second client in a shared sub-account: what was scoped and who else lives here.
@@ -69284,7 +69973,7 @@ var init_build_reply = __esm({
69284
69973
  { name: "composed.warnings", apply: (r) => shrinkWarnings(r) }
69285
69974
  ];
69286
69975
  CARRIES = {
69287
- execute: "summary, countsByType, notClean, calendars, missingSteps, workflowsNotBuilt, staffPause, recipients, templates, publishFailed and nextManualSteps",
69976
+ execute: "summary, countsByType, notClean, calendars, missingSteps, workflowsNotBuilt, staffPause, recipients, templates, publishFailed, nextManualSteps and customValues",
69288
69977
  dry_run: "summary, validation, countsByType, calendars, workflows, handoffs and composed.copySlots"
69289
69978
  };
69290
69979
  }
@@ -70199,7 +70888,7 @@ async function executeBackbone(plan, deps, opts = {}) {
70199
70888
  }
70200
70889
  if (matches.length === 1) {
70201
70890
  idMap[cal.ref] = matches[0].id;
70202
- const mismatch = calendarSlotMismatch(cal, matches[0]);
70891
+ const mismatch = [calendarSlotMismatch(cal, matches[0]), calendarHoursMismatch(cal, matches[0])].filter(Boolean).join(" ") || void 0;
70203
70892
  const boundExpected = resolveExpectedTeam(cal);
70204
70893
  let boundSeen = matches[0].teamMemberIds;
70205
70894
  let teamReadError2;
@@ -71289,6 +71978,7 @@ function pickCalendars(raw) {
71289
71978
  if (typeof o.slotDuration === "number") rec2.slotDuration = o.slotDuration;
71290
71979
  else if (typeof o.slotDuration === "string" && /^\d+$/.test(o.slotDuration)) rec2.slotDuration = Number(o.slotDuration);
71291
71980
  if (typeof o.slotDurationUnit === "string") rec2.slotDurationUnit = o.slotDurationUnit;
71981
+ if (Array.isArray(o.openHours) && o.openHours.length) rec2.openHours = o.openHours;
71292
71982
  if (Array.isArray(o.teamMembers)) {
71293
71983
  rec2.teamMemberIds = o.teamMembers.map((tm) => tm && typeof tm === "object" ? tm.userId : void 0).filter((v) => typeof v === "string" && v.length > 0);
71294
71984
  }
@@ -72396,10 +73086,19 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
72396
73086
  throw raw;
72397
73087
  }
72398
73088
  progressSink?.finish(exec);
73089
+ let accountCustomValues;
73090
+ try {
73091
+ const raw = await client.get(`/locations/${activeLocation}/customValues`);
73092
+ const hasList = Array.isArray(raw) || !!raw && typeof raw === "object" && Array.isArray(raw.customValues);
73093
+ accountCustomValues = hasList ? pickCustomValues(raw) : void 0;
73094
+ } catch {
73095
+ accountCustomValues = void 0;
73096
+ }
73097
+ const customValueStatus = customValueStatusOf(typedPlan.customValues ?? [], accountCustomValues);
72399
73098
  let savedPlanPath;
72400
73099
  const publishRecord = savedPublishRecord(saved, exec, typedPlan, publishWorkflows ?? false, gatedWorkflowRefs);
72401
73100
  try {
72402
- savedPlanPath = savePlanRecord({ ...saved, locationId: activeLocation, ...clientFields, planId: typedPlan.planId, savedAt: (/* @__PURE__ */ new Date()).toISOString(), plan: typedPlan, inputHash: process.env.GHL_COMMAND_INTAKE_HASH || saved?.inputHash, contentHash: contentHash(typedPlan), dependencies: planDependencies(typedPlan), preview: result, idMap: exec.idMap, built: exec.built, manual: exec.manual, calendarPending: exec.calendarPending, scope: scopeForValidation, heldWorkflows, ...publishRecord });
73101
+ savedPlanPath = savePlanRecord({ ...saved, locationId: activeLocation, ...clientFields, planId: typedPlan.planId, savedAt: (/* @__PURE__ */ new Date()).toISOString(), plan: typedPlan, inputHash: process.env.GHL_COMMAND_INTAKE_HASH || saved?.inputHash, contentHash: contentHash(typedPlan), dependencies: planDependencies(typedPlan), preview: result, idMap: exec.idMap, built: exec.built, manual: exec.manual, calendarPending: exec.calendarPending, scope: scopeForValidation, heldWorkflows, customValueStatus, ...publishRecord });
72403
73102
  } catch {
72404
73103
  }
72405
73104
  let journalPathOut;
@@ -72430,7 +73129,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
72430
73129
  ...w.manual.map((m) => `[${w.name}] ${m.reason}`),
72431
73130
  ...w.needsContent.map((c) => `[${w.name}] ${c.reason}`)
72432
73131
  ]);
72433
- const handoffLines = result.handoffs.filter((h) => !h.met).map((h) => `[${h.owner}] ${h.title}: ${h.instruction}`);
73132
+ const handoffLines = result.handoffs.filter((h) => !h.met).map((h) => `[${h.owner}] ${h.title}: ${h.instruction}${handoffCustomValueNote(h.ref, typedPlan.customValues ?? [], customValueStatus)}`);
72434
73133
  return buildReplyResponse({
72435
73134
  ok: exec.ok,
72436
73135
  mode: "execute",
@@ -72498,6 +73197,8 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
72498
73197
  deferred: exec.deferred,
72499
73198
  deferredNote: "GHL Command builds staff users, CRM objects, calendars, forms, message templates, and draft workflows. Pending staff and calendar setup appear as manual tasks. Review each workflow in GoHighLevel under Automation > Workflows before switching it on.",
72500
73199
  nextManualSteps: [...execManualLines, ...manualLines, ...handoffLines],
73200
+ customValues: customValueStatus,
73201
+ customValuesNote: customValueStatus.unreadable ? "The account's custom values could not be read after the build, so whether any is empty is unknown: say that, and name none as empty." : "Read back from the account after this build. A custom value is empty only when it is listed under empty here; never take it from a hand-off's instruction, which lists every value the hand-off covers.",
72501
73202
  summary: exec.ok ? `Built ${exec.built.filter((b) => b.status === "created").length} new object(s), bound ${exec.built.filter((b) => b.status === "existing" || b.status === "refreshed").length} existing${exec.refreshed.length ? ` (${exec.refreshed.length} workflow(s) refreshed in place \u2014 see refreshed[])` : ""}${exec.built.some((b) => b.status === "uncertain") ? `, ${exec.built.filter((b) => b.status === "uncertain").length} to check (found after a failed create)` : ""}.${exec.staff.length ? ` Staff: ${exec.staff.filter((s) => s.status === "created").length} created, ${exec.staff.filter((s) => s.status === "existing").length} already in the account, ${exec.staff.filter((s) => s.status === "pending").length} pending.${exec.staff.some((s) => s.status === "uncertain") ? ` ${exec.staff.filter((s) => s.status === "uncertain").length} created or found after a failed create; left in place; check it by hand.` : ""}` : ""}${exec.templates.emails.length + exec.templates.sms.length ? ` Templates: ${exec.templates.emails.length} email (${exec.templates.emails.filter((t) => t.status === "created" && !t.warning).length} created with the body verified in the preview${exec.templatesUnverified ? `, ${exec.templatesUnverified} created but NOT verified \u2014 see templates.emails[].warning` : ""}${exec.templates.emails.filter((t) => t.status === "existing").length ? `, ${exec.templates.emails.filter((t) => t.status === "existing").length} already existed` : ""}${exec.templates.emails.filter((t) => t.status === "failed").length ? `, ${exec.templates.emails.filter((t) => t.status === "failed").length} failed` : ""}), ${exec.templates.sms.length} SMS.` : ""}${publishSummaryClause({ published: exec.published, publishFailed: exec.publishFailed, anyWorkflowCreated: exec.built.some((b) => b.type === "workflow" && b.status === "created") })}${exec.manual.some((m) => m.type === "calendar") || exec.calendarPending.length ? ` Calendars: ${exec.manual.filter((m) => m.type === "calendar").length} deferred to manual; ${new Set(exec.calendarPending.filter((c) => c.status === "pending").map((c) => c.workflowRef)).size} workflow(s) built with the calendar unpinned${exec.calendarPending.some((c) => c.status === "calendar_ready") ? `, ${new Set(exec.calendarPending.filter((c) => c.status === "calendar_ready").map((c) => c.workflowRef)).size} ready to pin` : ""}.` : ""}${exec.staffPause ? ` STAFF PAUSE: ${exec.staffPause.reason}` : ""}${exec.manual.some((m) => m.type === "calendar_team") ? ` ${exec.manual.filter((m) => m.type === "calendar_team").length} calendar(s) were built WITHOUT the people the plan named \u2014 see nextManualSteps; do not report those calendars as staffed.` : ""}${exec.missingSteps.length ? ` NOT COMPLETE: ${exec.missingSteps.reduce((n, w) => n + w.steps.length, 0)} step(s) the plan asks for are still missing from ${exec.missingSteps.length} workflow(s) that already existed \u2014 add them by hand (see missingSteps).` : ""}${exec.workflowsNotBuilt.length ? ` NOT COMPLETE: ${exec.workflowsNotBuilt.length} workflow(s) were not finished this run because of a passing GoHighLevel error (${exec.workflowsNotBuilt.map((w) => w.name).join(", ")}); everything else was built. Run Build again to finish them (see workflowsNotBuilt).` : ""}${exec.manual.length ? ` ${exec.manual.length} item(s) need a manual step (see nextManualSteps).` : ""}${exec.deferred.length ? " Deferred: " + exec.deferred.map((d) => `${d.count} ${d.section}`).join(", ") + " (manual)." : ""}` : `HALTED at ${exec.halted?.atRef} (${exec.halted?.reason}). ${exec.built.length} object(s) were created before the halt \u2014 see idMap to resume or clean up. NEVER-CLOBBER means a re-run will bind those, not duplicate them.`
72502
73203
  }, "execute", activeLocation);
72503
73204
  }
@@ -73029,6 +73730,7 @@ var init_intake_to_build = __esm({
73029
73730
  init_features();
73030
73731
  init_executor();
73031
73732
  init_execute();
73733
+ init_build_facts();
73032
73734
  customFieldItemSchema = import_zod93.z.object({
73033
73735
  id: import_zod93.z.string(),
73034
73736
  name: import_zod93.z.string(),