@elitedcs/ghl-mcp 3.93.0 → 3.94.0

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
@@ -156,7 +156,7 @@ var init_account_read_retry = __esm({
156
156
  inBuildRequestScope = () => buildScope.getStore() !== void 0;
157
157
  inAccountReadAttempt = () => readScope.getStore() === true;
158
158
  withBuildRequestScope = (run, wait = sleep) => buildScope.run({ wait }, run);
159
- sleep = (ms) => new Promise((resolve14) => setTimeout(resolve14, ms));
159
+ sleep = (ms) => new Promise((resolve15) => setTimeout(resolve15, ms));
160
160
  buildRequestWait = (ms) => (buildScope.getStore()?.wait ?? sleep)(ms);
161
161
  }
162
162
  });
@@ -250,9 +250,9 @@ function planFromPayload(payload) {
250
250
  }
251
251
  function legacyDeviceFingerprint() {
252
252
  const os12 = require("node:os");
253
- const crypto14 = require("node:crypto");
253
+ const crypto16 = require("node:crypto");
254
254
  const raw = `${os12.hostname()}:${os12.userInfo().username}:${os12.platform()}:${os12.arch()}`;
255
- return crypto14.createHash("sha256").update(raw).digest("hex").slice(0, 16);
255
+ return crypto16.createHash("sha256").update(raw).digest("hex").slice(0, 16);
256
256
  }
257
257
  function registerDeviceIdResolver(fn) {
258
258
  deviceIdResolver = fn;
@@ -263,7 +263,7 @@ function deviceFingerprint(opts) {
263
263
  opts.persist?.(opts.cached);
264
264
  return opts.cached;
265
265
  }
266
- const crypto14 = require("node:crypto");
266
+ const crypto16 = require("node:crypto");
267
267
  if (!opts && deviceIdResolver) {
268
268
  try {
269
269
  return deviceIdResolver();
@@ -277,7 +277,7 @@ function deviceFingerprint(opts) {
277
277
  const creds = store2.readCredentials();
278
278
  if (creds?.device_id) return creds.device_id;
279
279
  const adopted = creds?.signed_attestation ? parseAttestationPayload(creds.signed_attestation)?.device_fingerprint : void 0;
280
- const id2 = adopted || crypto14.randomBytes(8).toString("hex");
280
+ const id2 = adopted || crypto16.randomBytes(8).toString("hex");
281
281
  if (creds) {
282
282
  try {
283
283
  store2.updateCredentials(creds, { device_id: id2 });
@@ -289,7 +289,7 @@ function deviceFingerprint(opts) {
289
289
  return legacyDeviceFingerprint();
290
290
  }
291
291
  }
292
- const fresh = crypto14.randomBytes(8).toString("hex");
292
+ const fresh = crypto16.randomBytes(8).toString("hex");
293
293
  opts.persist?.(fresh);
294
294
  return fresh;
295
295
  }
@@ -811,13 +811,13 @@ function setProtectedAccount(locationId2, name2, file3 = accountRulesPath()) {
811
811
  function isWriteMethod(method) {
812
812
  return WRITE_METHODS.has(method.toUpperCase());
813
813
  }
814
- function locationIdsInRequest(path66, opts = {}) {
814
+ function locationIdsInRequest(path67, opts = {}) {
815
815
  const found = /* @__PURE__ */ new Set();
816
816
  const add2 = (v) => {
817
817
  if (typeof v === "string" && v.trim()) found.add(v.trim());
818
818
  else if (Array.isArray(v)) v.forEach(add2);
819
819
  };
820
- const seg = /\/locations\/([^/?#]+)/i.exec(path66);
820
+ const seg = /\/locations\/([^/?#]+)/i.exec(path67);
821
821
  if (seg) add2(decodeURIComponent(seg[1]));
822
822
  for (const src of [opts.params, opts.body]) {
823
823
  if (!src || typeof src !== "object") continue;
@@ -844,7 +844,7 @@ function protectedRefusalFor(locationId2, rulesFile) {
844
844
  const name2 = rules.protected[id2];
845
845
  return name2 ? protectedRefusalMessage(name2, false) : null;
846
846
  }
847
- function assertWriteAllowed(method, path66, opts = {}) {
847
+ function assertWriteAllowed(method, path67, opts = {}) {
848
848
  if (!isWriteMethod(method)) return;
849
849
  let rules;
850
850
  try {
@@ -853,7 +853,7 @@ function assertWriteAllowed(method, path66, opts = {}) {
853
853
  throw new Error(UNREADABLE_RULES_MSG);
854
854
  }
855
855
  if (Object.keys(rules.protected).length === 0) return;
856
- for (const id2 of locationIdsInRequest(path66, opts)) {
856
+ for (const id2 of locationIdsInRequest(path67, opts)) {
857
857
  const name2 = rules.protected[id2];
858
858
  if (name2) throw new Error(protectedRefusalMessage(name2, false));
859
859
  }
@@ -1013,8 +1013,8 @@ var init_ghl_client = __esm({
1013
1013
  Version: version || GHL_API_VERSION
1014
1014
  };
1015
1015
  }
1016
- buildUrl(path66, params) {
1017
- const url = new URL(path66, GHL_BASE_URL);
1016
+ buildUrl(path67, params) {
1017
+ const url = new URL(path67, GHL_BASE_URL);
1018
1018
  if (params) {
1019
1019
  for (const [key2, value] of Object.entries(params)) {
1020
1020
  if (value !== void 0 && value !== null) {
@@ -1024,19 +1024,19 @@ var init_ghl_client = __esm({
1024
1024
  }
1025
1025
  return url.toString();
1026
1026
  }
1027
- async request(method, path66, options = {}, attempt2 = 0) {
1027
+ async request(method, path67, options = {}, attempt2 = 0) {
1028
1028
  this.signal?.throwIfAborted();
1029
1029
  if (inBuildRequestScope() && method === "GET" && !inAccountReadAttempt()) {
1030
- return retryAccountRead(() => this.request(method, path66, options, attempt2), `${method} ${path66}`);
1030
+ return retryAccountRead(() => this.request(method, path67, options, attempt2), `${method} ${path67}`);
1031
1031
  }
1032
1032
  if (attempt2 === 0) {
1033
- assertWriteAllowed(method, path66, {
1033
+ assertWriteAllowed(method, path67, {
1034
1034
  params: options.params,
1035
1035
  body: options.body,
1036
1036
  defaultLocationId: this.defaultLocationId
1037
1037
  });
1038
1038
  }
1039
- const url = this.buildUrl(path66, options.params);
1039
+ const url = this.buildUrl(path67, options.params);
1040
1040
  const headers = this.buildHeaders(options.version);
1041
1041
  if (options.form) delete headers["Content-Type"];
1042
1042
  if (this.beforeAttempt) {
@@ -1064,14 +1064,14 @@ var init_ghl_client = __esm({
1064
1064
  clearTimeout(timeout);
1065
1065
  this.signal?.throwIfAborted();
1066
1066
  if (error instanceof Error && error.name === "AbortError") {
1067
- throw requestError(new Error(`Request timeout (30s): ${method} ${path66}`));
1067
+ throw requestError(new Error(`Request timeout (30s): ${method} ${path67}`));
1068
1068
  }
1069
1069
  if (!inBuildRequestScope() && !options.noRetry && !options.form && attempt2 < MAX_RETRIES) {
1070
1070
  const delay5 = computeRetryDelay(null, attempt2, BASE_DELAY_MS);
1071
- process.stderr.write(`[ghl-mcp] Network error on ${method} ${path66}, retry ${attempt2 + 1}/${MAX_RETRIES} in ${delay5}ms
1071
+ process.stderr.write(`[ghl-mcp] Network error on ${method} ${path67}, retry ${attempt2 + 1}/${MAX_RETRIES} in ${delay5}ms
1072
1072
  `);
1073
1073
  await new Promise((r) => setTimeout(r, delay5));
1074
- return this.request(method, path66, options, attempt2 + 1);
1074
+ return this.request(method, path67, options, attempt2 + 1);
1075
1075
  }
1076
1076
  throw requestError(error);
1077
1077
  } finally {
@@ -1079,10 +1079,10 @@ var init_ghl_client = __esm({
1079
1079
  }
1080
1080
  if (!inBuildRequestScope() && !options.noRetry && !options.form && (response.status === 429 || response.status >= 500) && attempt2 < MAX_RETRIES) {
1081
1081
  const delay5 = computeRetryDelay(response.headers.get("Retry-After"), attempt2, BASE_DELAY_MS);
1082
- process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path66}, retry ${attempt2 + 1}/${MAX_RETRIES} in ${delay5}ms
1082
+ process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path67}, retry ${attempt2 + 1}/${MAX_RETRIES} in ${delay5}ms
1083
1083
  `);
1084
1084
  await new Promise((r) => setTimeout(r, delay5));
1085
- return this.request(method, path66, options, attempt2 + 1);
1085
+ return this.request(method, path67, options, attempt2 + 1);
1086
1086
  }
1087
1087
  if (!response.ok) {
1088
1088
  let errorBody = "";
@@ -1091,7 +1091,7 @@ var init_ghl_client = __esm({
1091
1091
  } catch {
1092
1092
  }
1093
1093
  throw requestError(new Error(
1094
- `GHL API Error ${response.status} ${response.statusText}: ${method} ${path66}
1094
+ `GHL API Error ${response.status} ${response.statusText}: ${method} ${path67}
1095
1095
  ${errorBody}`
1096
1096
  ));
1097
1097
  }
@@ -1113,20 +1113,20 @@ ${errorBody}`
1113
1113
  combined?.dispose();
1114
1114
  }
1115
1115
  }
1116
- async get(path66, options) {
1117
- return this.request("GET", path66, options);
1116
+ async get(path67, options) {
1117
+ return this.request("GET", path67, options);
1118
1118
  }
1119
- async post(path66, options) {
1120
- return this.request("POST", path66, options);
1119
+ async post(path67, options) {
1120
+ return this.request("POST", path67, options);
1121
1121
  }
1122
- async put(path66, options) {
1123
- return this.request("PUT", path66, options);
1122
+ async put(path67, options) {
1123
+ return this.request("PUT", path67, options);
1124
1124
  }
1125
- async patch(path66, options) {
1126
- return this.request("PATCH", path66, options);
1125
+ async patch(path67, options) {
1126
+ return this.request("PATCH", path67, options);
1127
1127
  }
1128
- async delete(path66, options) {
1129
- return this.request("DELETE", path66, options);
1128
+ async delete(path67, options) {
1129
+ return this.request("DELETE", path67, options);
1130
1130
  }
1131
1131
  /**
1132
1132
  * Helper: resolves locationId from args or falls back to default
@@ -1703,26 +1703,26 @@ function decodeName(name2) {
1703
1703
  return name2;
1704
1704
  }
1705
1705
  }
1706
- function maskCredential(value, rec2, path66, component = "=") {
1706
+ function maskCredential(value, rec2, path67, component = "=") {
1707
1707
  if (!value.trim() || MASK_PRESENT_RE.test(value) || value.includes("[MASKED") || isMergeOnly(value)) return value;
1708
1708
  const m = CREDENTIAL_SCHEME_RE.exec(value);
1709
1709
  const real = m ? m[2] : value;
1710
1710
  const token2 = maskToken(real);
1711
- rec2.pairs.push({ path: pathKey(path66), group: groupKey(path66), component, token: token2, real });
1711
+ rec2.pairs.push({ path: pathKey(path67), group: groupKey(path67), component, token: token2, real });
1712
1712
  return m ? `${m[1]}${token2}` : token2;
1713
1713
  }
1714
1714
  function looksLikeCredential(value) {
1715
1715
  return CREDENTIAL_SCHEME_RE.test(value) && !isMergeOnly(value);
1716
1716
  }
1717
- function maskUrl(url, rec2 = newRecorder(), path66 = []) {
1718
- let out = url.replace(USERINFO_RE, (_m, pre, pass, at) => `${pre}${maskCredential(pass, rec2, path66, "@")}${at}`);
1717
+ function maskUrl(url, rec2 = newRecorder(), path67 = []) {
1718
+ let out = url.replace(USERINFO_RE, (_m, pre, pass, at) => `${pre}${maskCredential(pass, rec2, path67, "@")}${at}`);
1719
1719
  const seen = /* @__PURE__ */ new Map();
1720
1720
  out = out.replace(QUERY_RE, (m, sep6, name2, val) => {
1721
1721
  const decoded2 = decodeName(name2);
1722
1722
  const n = seen.get(decoded2) ?? 0;
1723
1723
  seen.set(decoded2, n + 1);
1724
1724
  if (!val || !isSensitiveName(decoded2)) return m;
1725
- return `${sep6}${name2}=${maskCredential(val, rec2, path66, `?${decoded2}#${n}`)}`;
1725
+ return `${sep6}${name2}=${maskCredential(val, rec2, path67, `?${decoded2}#${n}`)}`;
1726
1726
  });
1727
1727
  return out;
1728
1728
  }
@@ -1746,11 +1746,11 @@ function segmentsFor(items) {
1746
1746
  return `[${index}]`;
1747
1747
  });
1748
1748
  }
1749
- function maskWebhookBag(node, rec2, path66) {
1749
+ function maskWebhookBag(node, rec2, path67) {
1750
1750
  if (Array.isArray(node)) {
1751
1751
  const segs = segmentsFor(node);
1752
1752
  node.forEach((item, i) => {
1753
- const at = [...path66, segs[i]];
1753
+ const at = [...path67, segs[i]];
1754
1754
  if (typeof item === "string") {
1755
1755
  if (looksLikeCredential(item)) node[i] = maskCredential(item, rec2, at);
1756
1756
  } else maskWebhookBag(item, rec2, at);
@@ -1762,7 +1762,7 @@ function maskWebhookBag(node, rec2, path66) {
1762
1762
  const label2 = labelProp ? node[labelProp] : void 0;
1763
1763
  for (const [prop, val] of Object.entries(node)) {
1764
1764
  if (prop === labelProp) continue;
1765
- const at = [...path66, prop];
1765
+ const at = [...path67, prop];
1766
1766
  if (typeof val === "string") {
1767
1767
  if (prop === "value" && typeof label2 === "string") {
1768
1768
  if (isSensitiveName(label2) || looksLikeCredential(val)) node[prop] = maskCredential(val, rec2, at);
@@ -1782,21 +1782,21 @@ function isWebhookNode(o) {
1782
1782
  function isWebhookSummary(o) {
1783
1783
  return (Array.isArray(o.headers) || isRecord2(o.headers)) && (typeof o.url === "string" || typeof o.method === "string");
1784
1784
  }
1785
- function walk(node, rec2, path66) {
1785
+ function walk(node, rec2, path67) {
1786
1786
  if (Array.isArray(node)) {
1787
- node.forEach((item, i) => walk(item, rec2, [...path66, `[${i}]`]));
1787
+ node.forEach((item, i) => walk(item, rec2, [...path67, `[${i}]`]));
1788
1788
  return;
1789
1789
  }
1790
1790
  if (!isRecord2(node)) return;
1791
1791
  if (isWebhookNode(node)) {
1792
- maskWebhookBag(node.attributes, rec2, [...path66, "attributes"]);
1792
+ maskWebhookBag(node.attributes, rec2, [...path67, "attributes"]);
1793
1793
  return;
1794
1794
  }
1795
1795
  if (isWebhookSummary(node)) {
1796
- maskWebhookBag(node, rec2, path66);
1796
+ maskWebhookBag(node, rec2, path67);
1797
1797
  return;
1798
1798
  }
1799
- for (const [k, val] of Object.entries(node)) walk(val, rec2, [...path66, k]);
1799
+ for (const [k, val] of Object.entries(node)) walk(val, rec2, [...path67, k]);
1800
1800
  }
1801
1801
  function maskWorkflowSecrets(data) {
1802
1802
  const copy = structuredClone(data);
@@ -1954,11 +1954,11 @@ function containsMask(value) {
1954
1954
  function maskedWriteRefusal(where, token2) {
1955
1955
  return `Refused: ${where} still carries a masked secret (${token2}). GHL Command hides webhook keys when it shows a workflow, and saving the placeholder would replace the real key and break that webhook. A mask is kept only in the same field of the same step it was read from (the real key is then kept for you); anywhere else, type the real value. Nothing was saved.`;
1956
1956
  }
1957
- function mapStrings(node, path66, fn) {
1957
+ function mapStrings(node, path67, fn) {
1958
1958
  if (Array.isArray(node)) {
1959
1959
  const segs = segmentsFor(node);
1960
1960
  node.forEach((item, i) => {
1961
- const at = [...path66, segs[i]];
1961
+ const at = [...path67, segs[i]];
1962
1962
  if (typeof item === "string") node[i] = fn(at, item);
1963
1963
  else mapStrings(item, at, fn);
1964
1964
  });
@@ -1966,15 +1966,15 @@ function mapStrings(node, path66, fn) {
1966
1966
  }
1967
1967
  if (!isRecord2(node)) return;
1968
1968
  for (const [k, v] of Object.entries(node)) {
1969
- if (typeof v === "string") node[k] = fn([...path66, k], v);
1970
- else if (v !== null && typeof v === "object") mapStrings(v, [...path66, k], fn);
1969
+ if (typeof v === "string") node[k] = fn([...path67, k], v);
1970
+ else if (v !== null && typeof v === "object") mapStrings(v, [...path67, k], fn);
1971
1971
  }
1972
1972
  }
1973
1973
  function stringsByGroup(node) {
1974
1974
  const out = { byPath: /* @__PURE__ */ new Map(), byGroup: /* @__PURE__ */ new Map() };
1975
- mapStrings(structuredClone(node), [], (path66, str16) => {
1976
- out.byPath.set(pathKey(path66), str16);
1977
- const g = groupKey(path66);
1975
+ mapStrings(structuredClone(node), [], (path67, str16) => {
1976
+ out.byPath.set(pathKey(path67), str16);
1977
+ const g = groupKey(path67);
1978
1978
  out.byGroup.set(g, [...out.byGroup.get(g) ?? [], str16]);
1979
1979
  return str16;
1980
1980
  });
@@ -2000,10 +2000,10 @@ function restoreMaskedSecrets(incoming, current) {
2000
2000
  const shown = typeof action.id === "string" ? shownById.get(action.id) : void 0;
2001
2001
  const sent = stringsByGroup(action);
2002
2002
  const copy = structuredClone(action);
2003
- mapStrings(copy, [], (path66, str16) => {
2003
+ mapStrings(copy, [], (path67, str16) => {
2004
2004
  if (!MASK_PRESENT_RE.test(str16)) return str16;
2005
- const pk = pathKey(path66);
2006
- const gk = groupKey(path66);
2005
+ const pk = pathKey(path67);
2006
+ const gk = groupKey(path67);
2007
2007
  const pick3 = (component, token2) => {
2008
2008
  const cg = componentGroup(component);
2009
2009
  const reals = new Set(pairs.filter((p) => p.group === gk && componentGroup(p.component) === cg && p.token === token2).map((p) => p.real));
@@ -2056,8 +2056,8 @@ var init_secret_mask = __esm({
2056
2056
  SENSITIVE_NAME_RE = /auth|key|token|secret|signature|signing|passw|pwd|credential|cookie|session|hmac|private|^sig$/i;
2057
2057
  CREDENTIAL_SCHEME_RE = /^(\s*(?:bearer|basic|token)\s+)(\S[\s\S]*)$/i;
2058
2058
  MERGE_ONLY_RE = /^\s*(?:\{\{[^{}]+\}\}\s*)+$/;
2059
- pathKey = (path66) => JSON.stringify(path66);
2060
- groupKey = (path66) => JSON.stringify(path66.map((seg) => seg.replace(/#\d+$/, "")));
2059
+ pathKey = (path67) => JSON.stringify(path67);
2060
+ groupKey = (path67) => JSON.stringify(path67.map((seg) => seg.replace(/#\d+$/, "")));
2061
2061
  componentGroup = (c) => c.replace(/#\d+$/, "");
2062
2062
  TOKEN_SRC = String.raw`\[MASKED(?: …[^\]\s]{1,4})?\]`;
2063
2063
  USERINFO_RE = new RegExp(String.raw`^(\s*[a-z][a-z0-9+.-]*:\/\/[^/@:?#\s]+:)((?:${TOKEN_SRC}|[^/@?#\s])+)(@)`, "i");
@@ -4954,8 +4954,8 @@ function buildEvent(event, opts) {
4954
4954
  }
4955
4955
  function ephemeralId() {
4956
4956
  if (!processEphemeralId) {
4957
- const crypto14 = require("node:crypto");
4958
- processEphemeralId = crypto14.randomBytes(16).toString("hex");
4957
+ const crypto16 = require("node:crypto");
4958
+ processEphemeralId = crypto16.randomBytes(16).toString("hex");
4959
4959
  }
4960
4960
  return processEphemeralId;
4961
4961
  }
@@ -8760,7 +8760,7 @@ function pinnedLookup(address, family) {
8760
8760
  };
8761
8761
  }
8762
8762
  function realRequest(url, address, family, signal) {
8763
- return new Promise((resolve14, reject) => {
8763
+ return new Promise((resolve15, reject) => {
8764
8764
  const req = https.request(url, {
8765
8765
  method: "GET",
8766
8766
  signal,
@@ -8772,7 +8772,7 @@ function realRequest(url, address, family, signal) {
8772
8772
  lookup: pinnedLookup(address, family)
8773
8773
  }, (res) => {
8774
8774
  const len = Number(res.headers["content-length"]);
8775
- resolve14({
8775
+ resolve15({
8776
8776
  status: res.statusCode ?? 0,
8777
8777
  location: typeof res.headers.location === "string" ? res.headers.location : void 0,
8778
8778
  contentType: typeof res.headers["content-type"] === "string" ? res.headers["content-type"] : void 0,
@@ -9773,11 +9773,11 @@ function registerEmailTools(server2, client) {
9773
9773
  function registerEmailBuilderInternalTools(server2, builderClient, publicClient) {
9774
9774
  if (!builderClient) return;
9775
9775
  const client = builderClient;
9776
- async function builderRequest2(method, path66, body) {
9777
- const response = await builderRequest(client, method, `${EMAIL_BUILDER_BASE}${path66}`, body ? { body } : {});
9776
+ async function builderRequest2(method, path67, body) {
9777
+ const response = await builderRequest(client, method, `${EMAIL_BUILDER_BASE}${path67}`, body ? { body } : {});
9778
9778
  if (!response.ok) {
9779
9779
  const text4 = await response.text();
9780
- throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path66}
9780
+ throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path67}
9781
9781
  ${text4}`);
9782
9782
  }
9783
9783
  const text3 = await response.text();
@@ -12075,9 +12075,9 @@ function bindingClash(number, agents, exceptAgentId, poolObjects = []) {
12075
12075
  const pool = poolBinding(a);
12076
12076
  if (pool) {
12077
12077
  const obj2 = poolObjects.find((p) => poolId(p) === pool);
12078
- const held = obj2 ? poolNumbers(obj2) : [];
12079
- if (held.includes(number)) return `${number} is in number pool ${pool}, which agent "${String(a.agentName)}" (${String(a.id)}) answers through. Binding it here would take those calls away. Unbind the pool there first, or choose another number.`;
12080
- if (held.length === 0) return `Agent "${String(a.agentName)}" (${String(a.id)}) answers through number pool ${pool}, and this API does not say which numbers that pool holds, so ${number} cannot be called free. Check the pool in GoHighLevel (Settings \u2192 Phone Numbers \u2192 Number Pools); if ${number} is not in it, unbind nothing and bind by hand there, or move that agent off the pool first.`;
12078
+ const held2 = obj2 ? poolNumbers(obj2) : [];
12079
+ if (held2.includes(number)) return `${number} is in number pool ${pool}, which agent "${String(a.agentName)}" (${String(a.id)}) answers through. Binding it here would take those calls away. Unbind the pool there first, or choose another number.`;
12080
+ if (held2.length === 0) return `Agent "${String(a.agentName)}" (${String(a.id)}) answers through number pool ${pool}, and this API does not say which numbers that pool holds, so ${number} cannot be called free. Check the pool in GoHighLevel (Settings \u2192 Phone Numbers \u2192 Number Pools); if ${number} is not in it, unbind nothing and bind by hand there, or move that agent off the pool first.`;
12081
12081
  }
12082
12082
  }
12083
12083
  return null;
@@ -12128,9 +12128,9 @@ function registerVoiceAiTools(server2, shared) {
12128
12128
  const poolBound = agents.filter((a) => poolBinding(a) !== null).map((a) => {
12129
12129
  const pool = poolBinding(a);
12130
12130
  const obj2 = poolObjects.find((p) => poolId(p) === pool);
12131
- const held = obj2 ? poolNumbers(obj2) : [];
12132
- for (const n of held) if (!holders.has(n)) holders.set(n, { agentId: String(a.id), agentName: String(a.agentName), via: `pool ${pool}` });
12133
- return { agentId: String(a.id), agentName: String(a.agentName), poolId: pool, poolNumbers: held.length ? held : "unknown \u2014 this API does not list the pool's numbers" };
12131
+ const held2 = obj2 ? poolNumbers(obj2) : [];
12132
+ for (const n of held2) if (!holders.has(n)) holders.set(n, { agentId: String(a.id), agentName: String(a.agentName), via: `pool ${pool}` });
12133
+ return { agentId: String(a.id), agentName: String(a.agentName), poolId: pool, poolNumbers: held2.length ? held2 : "unknown \u2014 this API does not list the pool's numbers" };
12134
12134
  });
12135
12135
  const poolRouting = poolBound.filter((p) => Array.isArray(p.poolNumbers) && p.poolNumbers.length > 0);
12136
12136
  const poolUnproven = poolBound.filter((p) => typeof p.poolNumbers === "string");
@@ -12325,11 +12325,11 @@ function registerVoiceAiTools(server2, shared) {
12325
12325
  const holder = others.find((a) => poolBinding(a) === numberPoolId);
12326
12326
  if (holder) throw new Error(`Number pool ${numberPoolId} already answers for agent "${String(holder.agentName)}" (${String(holder.id)}). Binding it here would take those calls away. Unbind it there first (update_voice_agent with numberPoolId: null), or choose another pool.`);
12327
12327
  const target = poolObjects.find((p) => poolId(p) === numberPoolId);
12328
- const held = target ? poolNumbers(target) : [];
12328
+ const held2 = target ? poolNumbers(target) : [];
12329
12329
  const othersBound = others.filter((a) => boundNumbers(a).length > 0);
12330
- if (held.length) {
12330
+ if (held2.length) {
12331
12331
  for (const a of othersBound) {
12332
- const overlap = boundNumbers(a).filter((n) => held.includes(n));
12332
+ const overlap = boundNumbers(a).filter((n) => held2.includes(n));
12333
12333
  if (overlap.length) throw new Error(`Number pool ${numberPoolId} holds ${overlap.join(", ")}, which already answers for agent "${String(a.agentName)}" (${String(a.id)}) by inboundNumber. Binding the pool here would take those calls away. Unbind there first, or choose another pool.`);
12334
12334
  }
12335
12335
  } else if (othersBound.length) {
@@ -15472,7 +15472,7 @@ function isPrivateAddress(ip) {
15472
15472
  return false;
15473
15473
  }
15474
15474
  function withDeadline(p, ms, why, signal) {
15475
- return new Promise((resolve14, reject) => {
15475
+ return new Promise((resolve15, reject) => {
15476
15476
  if (signal?.aborted) {
15477
15477
  p.catch(() => void 0);
15478
15478
  reject(new Error("aborted"));
@@ -15487,7 +15487,7 @@ function withDeadline(p, ms, why, signal) {
15487
15487
  p.then((v) => {
15488
15488
  clearTimeout(t);
15489
15489
  signal?.removeEventListener("abort", onAbort);
15490
- resolve14(v);
15490
+ resolve15(v);
15491
15491
  }, (e) => {
15492
15492
  clearTimeout(t);
15493
15493
  signal?.removeEventListener("abort", onAbort);
@@ -15539,7 +15539,7 @@ function closeTransferBudget(b) {
15539
15539
  b.agents.https.destroy();
15540
15540
  }
15541
15541
  function guardedTransfer(url, init) {
15542
- return new Promise((resolve14, reject) => {
15542
+ return new Promise((resolve15, reject) => {
15543
15543
  let u;
15544
15544
  try {
15545
15545
  u = new URL(url);
@@ -15594,7 +15594,7 @@ function guardedTransfer(url, init) {
15594
15594
  const kept = unzip ? out.filter(([k]) => !/^(?:content-encoding|content-length)$/i.test(k)) : out;
15595
15595
  const chunks = [];
15596
15596
  let size2 = 0;
15597
- const finish3 = (incomplete) => settle2(() => resolve14({ status: res.statusCode ?? 0, headers: kept, body: Buffer.concat(chunks), incomplete }));
15597
+ const finish3 = (incomplete) => settle2(() => resolve15({ status: res.statusCode ?? 0, headers: kept, body: Buffer.concat(chunks), incomplete }));
15598
15598
  stream.on("data", (c) => {
15599
15599
  size2 += c.length;
15600
15600
  init.budget.bytesLeft -= c.length;
@@ -15650,7 +15650,7 @@ async function guardedPost(url, body, headers, timeoutMs = 2e4) {
15650
15650
  const left = Math.max(1, timeoutMs - (Date.now() - started));
15651
15651
  const u = new URL(url);
15652
15652
  const mod = u.protocol === "https:" ? import_node_https.default : import_node_http.default;
15653
- return new Promise((resolve14, reject) => {
15653
+ return new Promise((resolve15, reject) => {
15654
15654
  let settled = false;
15655
15655
  const chunks = [];
15656
15656
  let size2 = 0;
@@ -15661,7 +15661,7 @@ async function guardedPost(url, body, headers, timeoutMs = 2e4) {
15661
15661
  f();
15662
15662
  };
15663
15663
  const req = mod.request(u, { method: "POST", headers: { ...headers, "content-length": String(Buffer.byteLength(body)) }, lookup: publicOnlyLookup }, (res) => {
15664
- const done = () => settle2(() => resolve14({ status: res.statusCode ?? 0, text: Buffer.concat(chunks).toString("utf8") }));
15664
+ const done = () => settle2(() => resolve15({ status: res.statusCode ?? 0, text: Buffer.concat(chunks).toString("utf8") }));
15665
15665
  res.on("data", (c) => {
15666
15666
  size2 += c.length;
15667
15667
  if (size2 <= 1e6) chunks.push(c);
@@ -15797,14 +15797,14 @@ async function startGuardProxy(opts = {}) {
15797
15797
  client.on("close", () => up.destroy());
15798
15798
  });
15799
15799
  server2.on("clientError", (_e, s) => s.destroy());
15800
- await new Promise((resolve14, reject) => {
15800
+ await new Promise((resolve15, reject) => {
15801
15801
  const fail = (e) => {
15802
15802
  server2.removeListener("listening", ok);
15803
15803
  reject(e);
15804
15804
  };
15805
15805
  const ok = () => {
15806
15806
  server2.removeListener("error", fail);
15807
- resolve14();
15807
+ resolve15();
15808
15808
  };
15809
15809
  server2.once("error", fail);
15810
15810
  server2.once("listening", ok);
@@ -15862,12 +15862,12 @@ async function readPageFull(client, pageId, fetcher = fetch) {
15862
15862
  const usedIdToken = headers["token-id"];
15863
15863
  headers.Origin = "https://app.gohighlevel.com";
15864
15864
  headers.Referer = "https://app.gohighlevel.com/";
15865
- const path66 = `/page/${pageId}?locationId=${client.locationId}`;
15866
- const response = await fetcher(`https://backend.leadconnectorhq.com/funnels${path66}`, { method: "GET", headers });
15865
+ const path67 = `/page/${pageId}?locationId=${client.locationId}`;
15866
+ const response = await fetcher(`https://backend.leadconnectorhq.com/funnels${path67}`, { method: "GET", headers });
15867
15867
  if (!response.ok) {
15868
15868
  if (response.status === 401) client.invalidateIdToken?.(usedIdToken);
15869
15869
  const text4 = await response.text();
15870
- throw new Error(`Funnel API Error ${response.status}: GET ${path66}
15870
+ throw new Error(`Funnel API Error ${response.status}: GET ${path67}
15871
15871
  ${text4}`);
15872
15872
  }
15873
15873
  const text3 = await response.text();
@@ -15930,20 +15930,20 @@ function registerFunnelBuilderTools(server2, builderClient) {
15930
15930
  return;
15931
15931
  }
15932
15932
  const client = builderClient;
15933
- async function internalGet(path66) {
15934
- return client.request("GET", path66);
15933
+ async function internalGet(path67) {
15934
+ return client.request("GET", path67);
15935
15935
  }
15936
- async function internalPost(path66, body) {
15937
- return client.request("POST", path66, body);
15936
+ async function internalPost(path67, body) {
15937
+ return client.request("POST", path67, body);
15938
15938
  }
15939
- async function internalPut(path66, body) {
15940
- return client.request("PUT", path66, body);
15939
+ async function internalPut(path67, body) {
15940
+ return client.request("PUT", path67, body);
15941
15941
  }
15942
- async function internalDelete(path66) {
15943
- return client.request("DELETE", path66);
15942
+ async function internalDelete(path67) {
15943
+ return client.request("DELETE", path67);
15944
15944
  }
15945
- async function funnelRequest(method, path66, body) {
15946
- const url = `https://backend.leadconnectorhq.com/funnels${path66}`;
15945
+ async function funnelRequest(method, path67, body) {
15946
+ const url = `https://backend.leadconnectorhq.com/funnels${path67}`;
15947
15947
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
15948
15948
  const response = await builderRequest(client, method, url, {
15949
15949
  ...sendsBody ? { body } : {},
@@ -15956,7 +15956,7 @@ function registerFunnelBuilderTools(server2, builderClient) {
15956
15956
  });
15957
15957
  if (!response.ok) {
15958
15958
  const text4 = await response.text();
15959
- throw new Error(`Funnel API Error ${response.status}: ${method} ${path66}
15959
+ throw new Error(`Funnel API Error ${response.status}: ${method} ${path67}
15960
15960
  ${text4}`);
15961
15961
  }
15962
15962
  const text3 = await response.text();
@@ -16547,17 +16547,17 @@ function requireCompany(api) {
16547
16547
  }
16548
16548
  return company;
16549
16549
  }
16550
- async function call(api, path66, init) {
16550
+ async function call(api, path67, init) {
16551
16551
  const headers = { ...await api.headers(), Origin: "https://app.gohighlevel.com", Referer: "https://app.gohighlevel.com/" };
16552
- if (MUTATING_PATHS.some((mutating) => path66.startsWith(mutating))) {
16553
- assertWriteAllowed("POST", SVC + path66, { defaultLocationId: api.locationId() });
16552
+ if (MUTATING_PATHS.some((mutating) => path67.startsWith(mutating))) {
16553
+ assertWriteAllowed("POST", SVC + path67, { defaultLocationId: api.locationId() });
16554
16554
  }
16555
16555
  const doFetch = api.fetch ?? fetch;
16556
- const response = await doFetch(SVC + path66, { ...init, headers });
16556
+ const response = await doFetch(SVC + path67, { ...init, headers });
16557
16557
  const text3 = await response.text();
16558
16558
  if (!response.ok) {
16559
16559
  const hint = response.status === 401 && /not authorized to access this company/i.test(text3) ? " \u2014 the Firebase session is authenticated as a DIFFERENT company than the one that owns this sub-account. Run switch_location for this sub-account and try again." : response.status === 401 ? " \u2014 the template library needs the workflow-builder Firebase session, not the API key alone. Run enable_workflow_builder for this sub-account." : "";
16560
- throw new Error(`GoHighLevel template library ${response.status} on ${path66}${hint}
16560
+ throw new Error(`GoHighLevel template library ${response.status} on ${path67}${hint}
16561
16561
  ${text3.slice(0, 300)}`);
16562
16562
  }
16563
16563
  return text3 ? JSON.parse(text3) : {};
@@ -16915,12 +16915,12 @@ function normaliseColour(value) {
16915
16915
  function renderedPalette(envelope) {
16916
16916
  const defs = pageVariables(envelope);
16917
16917
  const paints = [];
16918
- const walk2 = (node, path66, prop) => {
16919
- if (Array.isArray(node)) return void node.forEach((x, i) => walk2(x, `${path66}[${i}]`, prop));
16918
+ const walk2 = (node, path67, prop) => {
16919
+ if (Array.isArray(node)) return void node.forEach((x, i) => walk2(x, `${path67}[${i}]`, prop));
16920
16920
  if (isRecord11(node)) {
16921
16921
  for (const [k, x] of Object.entries(node)) {
16922
16922
  if (DEFINITION_KEYS.has(k) || k.startsWith("--")) continue;
16923
- walk2(x, `${path66}.${k}`, WRAPPER_KEYS.has(k) ? prop : k);
16923
+ walk2(x, `${path67}.${k}`, WRAPPER_KEYS.has(k) ? prop : k);
16924
16924
  }
16925
16925
  return;
16926
16926
  }
@@ -16930,7 +16930,7 @@ function renderedPalette(envelope) {
16930
16930
  const resolved2 = resolveValue(raw, defs);
16931
16931
  const colour = normaliseColour(resolved2);
16932
16932
  if (colour === null && resolved2 !== null) return;
16933
- paints.push({ path: path66, prop, kind: kindOf(prop), colour });
16933
+ paints.push({ path: path67, prop, kind: kindOf(prop), colour });
16934
16934
  };
16935
16935
  walk2(envelope, "", "");
16936
16936
  const face = (name2, settingsKey) => {
@@ -17025,7 +17025,7 @@ var init_rendered_palette = __esm({
17025
17025
  VISIBLE_CHANGE = 0.05;
17026
17026
  BARELY_SHARE = 0.5;
17027
17027
  CARRIES_COLOUR = 0.02;
17028
- paintGroup = (path66) => /^\.(sections|popups)\[\d+\]/.exec(path66)?.[0]?.slice(1) ?? "page";
17028
+ paintGroup = (path67) => /^\.(sections|popups)\[\d+\]/.exec(path67)?.[0]?.slice(1) ?? "page";
17029
17029
  looksDifferent = (a, b) => {
17030
17030
  if (a === b) return false;
17031
17031
  if (a === "transparent" || b === "transparent" || a.startsWith("rgba") || b.startsWith("rgba")) return a !== b;
@@ -22272,9 +22272,9 @@ function removalHoldBack(opts) {
22272
22272
  if (opts.multiPageStepNames.length > 0) return `${opts.multiPageStepNames.length} step(s) carry more than one page (${opts.multiPageStepNames.join(", ")}) and only the first of each was rebuilt`;
22273
22273
  return null;
22274
22274
  }
22275
- function brandedPath(path66, name2, taken) {
22275
+ function brandedPath(path67, name2, taken) {
22276
22276
  const normalize2 = (s) => s.replace(/^\/+|\/+$/g, "").toLowerCase();
22277
- const base = normalize2(path66 ?? "") || name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "page";
22277
+ const base = normalize2(path67 ?? "") || name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "page";
22278
22278
  const used = new Set([...taken].map(normalize2));
22279
22279
  let candidate = `${base}-branded`;
22280
22280
  for (let n = 2; used.has(candidate); n++) candidate = `${base}-branded-${n}`;
@@ -22302,9 +22302,9 @@ function rewriteTemplateLinks(envelope, destinations) {
22302
22302
  representation = "page";
22303
22303
  matches = destinations.filter((d2) => d2.pageId === preview[2]);
22304
22304
  } else if (!/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(from) && !from.startsWith("#")) {
22305
- const [path66] = from.split(/[?#]/);
22306
- suffix = from.slice(path66.length);
22307
- matches = destinations.filter((d2) => d2.path && normalize2(d2.path) === normalize2(path66));
22305
+ const [path67] = from.split(/[?#]/);
22306
+ suffix = from.slice(path67.length);
22307
+ matches = destinations.filter((d2) => d2.path && normalize2(d2.path) === normalize2(path67));
22308
22308
  }
22309
22309
  }
22310
22310
  if (matches.length !== 1) {
@@ -22387,8 +22387,8 @@ function registerPageStudioTools(server2, builderClient) {
22387
22387
  }
22388
22388
  return { map: out, truncated };
22389
22389
  }
22390
- async function funnelRequest(method, path66, body) {
22391
- const response = await builderRequest(client, method, `https://backend.leadconnectorhq.com/funnels${path66}`, {
22390
+ async function funnelRequest(method, path67, body) {
22391
+ const response = await builderRequest(client, method, `https://backend.leadconnectorhq.com/funnels${path67}`, {
22392
22392
  ...body ? { body } : {},
22393
22393
  headers: {
22394
22394
  // Internal-API calls require the app origin (autosave needs the
@@ -22397,7 +22397,7 @@ function registerPageStudioTools(server2, builderClient) {
22397
22397
  Referer: "https://app.gohighlevel.com/"
22398
22398
  }
22399
22399
  });
22400
- if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path66}
22400
+ if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path67}
22401
22401
  ${await response.text()}`);
22402
22402
  const text3 = await response.text();
22403
22403
  return text3 ? JSON.parse(text3) : {};
@@ -22905,7 +22905,7 @@ ${await response.text()}`);
22905
22905
  }
22906
22906
  const confirmed = /* @__PURE__ */ new Set();
22907
22907
  for (let attempt2 = 0; accepted.length && attempt2 < 5; attempt2++) {
22908
- if (attempt2) await new Promise((resolve14) => setTimeout(resolve14, attempt2 * 1e3));
22908
+ if (attempt2) await new Promise((resolve15) => setTimeout(resolve15, attempt2 * 1e3));
22909
22909
  try {
22910
22910
  const listed = await funnelRequest("GET", `/funnel/list?locationId=${client.locationId}&limit=${FUNNEL_PAGE}`);
22911
22911
  const current = listed.funnels?.find((item) => item._id === funnel._id);
@@ -23269,13 +23269,13 @@ function cleanFormDataForSave(formData) {
23269
23269
  const cleaned = dropRedundantCaseCollidingKeys(formData);
23270
23270
  return isJsonObject(cleaned) ? cleaned : formData;
23271
23271
  }
23272
- async function formApiRequest(client, method, path66, body) {
23273
- const url = `https://backend.leadconnectorhq.com/forms${path66}`;
23272
+ async function formApiRequest(client, method, path67, body) {
23273
+ const url = `https://backend.leadconnectorhq.com/forms${path67}`;
23274
23274
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
23275
23275
  const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
23276
23276
  if (!response.ok) {
23277
23277
  const text4 = await response.text();
23278
- throw new Error(`Form API Error ${response.status}: ${method} ${path66}
23278
+ throw new Error(`Form API Error ${response.status}: ${method} ${path67}
23279
23279
  ${text4}`);
23280
23280
  }
23281
23281
  const text3 = await response.text();
@@ -23317,7 +23317,7 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
23317
23317
  );
23318
23318
  return;
23319
23319
  }
23320
- const formRequest = (method, path66, body) => formApiRequest(client, method, path66, body);
23320
+ const formRequest = (method, path67, body) => formApiRequest(client, method, path67, body);
23321
23321
  server2.tool(
23322
23322
  "get_form_full",
23323
23323
  "Get a form with full builder data: all fields (labels, types, IDs, validation), conditional logic, auto-responder config, email notification settings, styling, and version history. This is the internal API \u2014 it returns what the form builder UI shows, with one exception: where GoHighLevel stores the same value under two spellings of one key that differ only in case (it writes custom fields with both `Id` and `id`), only the lowercase one is returned. No value is lost, and the result can be edited and passed straight back to update_form.",
@@ -23451,10 +23451,10 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
23451
23451
  },
23452
23452
  async ({ formId, limit, skip }) => {
23453
23453
  try {
23454
- let path66 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
23455
- if (formId) path66 += `&formId=${formId}`;
23456
- if (skip) path66 += `&skip=${skip}`;
23457
- const result = await formRequest("GET", path66);
23454
+ let path67 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
23455
+ if (formId) path67 += `&formId=${formId}`;
23456
+ if (skip) path67 += `&skip=${skip}`;
23457
+ const result = await formRequest("GET", path67);
23458
23458
  return {
23459
23459
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
23460
23460
  };
@@ -23559,7 +23559,7 @@ function parseContact(raw) {
23559
23559
  };
23560
23560
  }
23561
23561
  async function searchByEmail(client, locationId2, email) {
23562
- const paged = await readContactPages((path66, o) => client.get(path66, o), { locationId: locationId2, query: email, limit: 20 });
23562
+ const paged = await readContactPages((path67, o) => client.get(path67, o), { locationId: locationId2, query: email, limit: 20 });
23563
23563
  if (!paged) throw new SearchUnreadable("the contact search answered in a shape that could not be read");
23564
23564
  const want = email.toLowerCase();
23565
23565
  const ids = [];
@@ -24044,13 +24044,13 @@ var init_funnel_qa = __esm({
24044
24044
  function registerPipelineBuilderTools(server2, builderClient) {
24045
24045
  if (!builderClient) return;
24046
24046
  const client = builderClient;
24047
- async function pipelineRequest(method, path66, body) {
24048
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path66}`;
24047
+ async function pipelineRequest(method, path67, body) {
24048
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path67}`;
24049
24049
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT" || method === "PATCH");
24050
24050
  const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
24051
24051
  if (!response.ok) {
24052
24052
  const text4 = await response.text();
24053
- throw new Error(`Pipeline API Error ${response.status}: ${method} ${path66}
24053
+ throw new Error(`Pipeline API Error ${response.status}: ${method} ${path67}
24054
24054
  ${text4}`);
24055
24055
  }
24056
24056
  const text3 = await response.text();
@@ -24340,6 +24340,8 @@ var init_features = __esm({
24340
24340
  BY_WORKFLOW_REF = {
24341
24341
  "workflow.speed_to_lead": "speed_to_lead",
24342
24342
  "workflow.missed_call": "missed_call",
24343
+ // F6 (2026-10-29): a missed caller with no card gets one. Goes with the text-back it hangs off.
24344
+ "workflow.missed_call_card": "missed_call",
24343
24345
  "workflow.lead_nurture": "nurture",
24344
24346
  "workflow.win_back": "winback",
24345
24347
  "workflow.appointment_booked": "booking",
@@ -26084,10 +26086,14 @@ var init_registry = __esm({
26084
26086
  whatItIs: "Google sign-in gives this machine permission to read your calendar without pasting a password here.",
26085
26087
  checkedOn: null,
26086
26088
  gives: "Adds your own day beside your clients' appointments as a separate part of the brief.",
26087
- needs: ["A Google account with a calendar you can read.", "Google sign-in configured for this installation; Connect will tell you if it is unavailable."],
26088
- before: "Once Google sign-in is set up on this computer:",
26089
+ // ba's Hub audit (2026-10-29): the guide read as if this connects today, while CHANGELOG 3.92.2 and the card say
26090
+ // CALENDAR_SETUP_MESSAGE. It does not connect until GHL Command ships its own Google sign-in, so the guide says so
26091
+ // first and the steps below are what it will be.
26092
+ needs: ["A Google account with a calendar you can read.", "Google sign-in for GHL Command, which is not available yet."],
26093
+ before: `${CALENDAR_SETUP_MESSAGE} Choosing Connect says so and saves nothing. When Google sign-in arrives in an update, these are the steps:`,
26089
26094
  steps: ["On the Google Calendar card on the Connections screen, choose Connect to open Google sign-in.", "Select your Google account and allow read-only calendar access.", "Return to the Google Calendar card and choose Finish. Your sign-in is saved only after your calendars can be read."],
26090
- say: ["Connect my own Google Calendar.", "Finish connecting my Google Calendar.", "Read my calendar for today."],
26095
+ say: [],
26096
+ // none until Google sign-in ships: each one would fail (ba audit 2026-10-29)
26091
26097
  revoke: "Choose Disconnect on this system's Connections card to remove the saved sign-in and request revocation with Google. You can also remove the app in your Google Account's third-party connections at myaccount.google.com/connections.",
26092
26098
  tools: { connect: "connect_google_calendar", disconnect: "connect_google_calendar", read: "read_my_calendar" }
26093
26099
  },
@@ -26476,7 +26482,7 @@ function calendarSetupDetail() {
26476
26482
  }
26477
26483
  function openBrowser(url) {
26478
26484
  const [file3, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32.exe", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
26479
- return new Promise((resolve14) => (0, import_node_child_process.execFile)(file3, args, { timeout: 5e3 }, (error) => resolve14(!error)));
26485
+ return new Promise((resolve15) => (0, import_node_child_process.execFile)(file3, args, { timeout: 5e3 }, (error) => resolve15(!error)));
26480
26486
  }
26481
26487
  function parsed(schema, value) {
26482
26488
  const result = schema.safeParse(value);
@@ -26573,16 +26579,16 @@ function createGoogleCalendar(deps = {}) {
26573
26579
  requireOnlyCalendar(result.scope);
26574
26580
  return result.access_token;
26575
26581
  }
26576
- async function pages(path66, access, item, query = {}) {
26582
+ async function pages(path67, access, item, query = {}) {
26577
26583
  const result = [];
26578
26584
  const seen = /* @__PURE__ */ new Set();
26579
26585
  let pageToken;
26580
26586
  for (let page2 = 0; page2 < 100; page2++) {
26581
- const url = new URL(`${API}${path66}`);
26587
+ const url = new URL(`${API}${path67}`);
26582
26588
  for (const [k, v] of Object.entries(query)) url.searchParams.set(k, v);
26583
26589
  if (pageToken) url.searchParams.set("pageToken", pageToken);
26584
26590
  const data = parsed(
26585
- import_zod53.z.object({ items: import_zod53.z.array(item).optional(), nextPageToken: import_zod53.z.string().optional(), kind: import_zod53.z.string().optional() }).refine((p) => p.items !== void 0 || p.kind === (path66.endsWith("/events") ? "calendar#events" : "calendar#calendarList")),
26591
+ import_zod53.z.object({ items: import_zod53.z.array(item).optional(), nextPageToken: import_zod53.z.string().optional(), kind: import_zod53.z.string().optional() }).refine((p) => p.items !== void 0 || p.kind === (path67.endsWith("/events") ? "calendar#events" : "calendar#calendarList")),
26586
26592
  await request2(url.href, { headers: { Authorization: `Bearer ${access}` } })
26587
26593
  );
26588
26594
  result.push(...data.items ?? []);
@@ -26634,9 +26640,9 @@ function createGoogleCalendar(deps = {}) {
26634
26640
  server2.requestTimeout = 5e3;
26635
26641
  server2.headersTimeout = 5e3;
26636
26642
  try {
26637
- await new Promise((resolve14, reject) => {
26643
+ await new Promise((resolve15, reject) => {
26638
26644
  server2.once("error", reject);
26639
- server2.listen(0, "127.0.0.1", resolve14);
26645
+ server2.listen(0, "127.0.0.1", resolve15);
26640
26646
  });
26641
26647
  } catch {
26642
26648
  server2.close();
@@ -27561,7 +27567,7 @@ var init_supabase = __esm({
27561
27567
  }
27562
27568
  token;
27563
27569
  fetchFn;
27564
- async call(what, path66, init = {}) {
27570
+ async call(what, path67, init = {}) {
27565
27571
  const headers = { Authorization: `Bearer ${this.token}`, Accept: "application/json" };
27566
27572
  let body;
27567
27573
  if (init.form) body = init.form;
@@ -27571,7 +27577,7 @@ var init_supabase = __esm({
27571
27577
  }
27572
27578
  let res;
27573
27579
  try {
27574
- res = await this.fetchFn(`${MANAGEMENT_API}${path66}`, { method: init.method ?? "GET", headers, body, redirect: "error" });
27580
+ res = await this.fetchFn(`${MANAGEMENT_API}${path67}`, { method: init.method ?? "GET", headers, body, redirect: "error" });
27575
27581
  } catch {
27576
27582
  throw new BrainApiError(`${what}: Supabase could not be reached. Nothing on your side changed.`, null);
27577
27583
  }
@@ -28298,8 +28304,8 @@ function redactConnectionKey(value, key2) {
28298
28304
  return value;
28299
28305
  }
28300
28306
  function stripeFetcher(key2, signal) {
28301
- return async (path66, query) => {
28302
- const url = new URL(`https://api.stripe.com${path66}`);
28307
+ return async (path67, query) => {
28308
+ const url = new URL(`https://api.stripe.com${path67}`);
28303
28309
  for (const [k, v] of Object.entries(query ?? {})) url.searchParams.set(k, v);
28304
28310
  const res = await fetch(url, { headers: { Authorization: `Bearer ${key2}` }, signal });
28305
28311
  if (!res.ok) {
@@ -28448,11 +28454,11 @@ var init_handlers = __esm({
28448
28454
  });
28449
28455
 
28450
28456
  // src/command-os/connectors/stripe-read.ts
28451
- async function listAll(f, path66, schema, extra = {}) {
28457
+ async function listAll(f, path67, schema, extra = {}) {
28452
28458
  const out = [];
28453
28459
  let after;
28454
28460
  for (let page2 = 0; page2 < 20; page2++) {
28455
- const result = import_zod58.z.object({ data: import_zod58.z.array(schema), has_more: import_zod58.z.boolean().optional() }).parse(await f(path66, { limit: "100", ...extra, ...after ? { starting_after: after } : {} }));
28461
+ const result = import_zod58.z.object({ data: import_zod58.z.array(schema), has_more: import_zod58.z.boolean().optional() }).parse(await f(path67, { limit: "100", ...extra, ...after ? { starting_after: after } : {} }));
28456
28462
  out.push(...result.data);
28457
28463
  if (!result.has_more) return out;
28458
28464
  const last = import_zod58.z.object({ id: import_zod58.z.string().min(1) }).parse(result.data.at(-1));
@@ -32290,8 +32296,8 @@ function classifyBrainWorkflow(read2, functionUrl) {
32290
32296
  return null;
32291
32297
  }
32292
32298
  if (u.origin !== base.origin || u.search !== "" || u.hash !== "") return null;
32293
- const path66 = u.pathname.replace(/\/+$/, "");
32294
- return path66 === `${base.pathname}/event` ? "/event" : path66 === `${base.pathname}/reply` ? "/reply" : null;
32299
+ const path67 = u.pathname.replace(/\/+$/, "");
32300
+ return path67 === `${base.pathname}/event` ? "/event" : path67 === `${base.pathname}/reply` ? "/reply" : null;
32295
32301
  };
32296
32302
  const urlOf = (a) => a?.attributes?.url;
32297
32303
  const hits = steps.filter((s) => exact(urlOf(s)) !== null);
@@ -32390,8 +32396,8 @@ async function brainWireGhl(deps, args) {
32390
32396
  return { ok: false, wrote: "nothing", error: e instanceof Error ? e.message : String(e) };
32391
32397
  }
32392
32398
  const specs = wiringSpecs({ testTag: args.testTag, leadTag });
32393
- const held = [];
32394
- const clean5 = (e) => scrub(e instanceof Error ? e.message : String(e), held);
32399
+ const held2 = [];
32400
+ const clean5 = (e) => scrub(e instanceof Error ? e.message : String(e), held2);
32395
32401
  const survey = async () => {
32396
32402
  const refuse4 = (error) => ({ refusal: { ok: false, wrote: "nothing", error } });
32397
32403
  const meta = deps.meta();
@@ -32417,7 +32423,7 @@ async function brainWireGhl(deps, args) {
32417
32423
  if (c.kind === "uncertain") return refuse4(`Workflow ${id2} reaches this brain, but ${c.why}, so the brain cannot tell what it is. Nothing was changed. ${RESTART}`);
32418
32424
  if (c.kind === "brain") {
32419
32425
  if (known && known !== c.signal) return refuse4(`Workflow ${id2} is saved on this computer as the brain's ${known} workflow but now reports ${c.signal}, so the brain cannot tell what it is. Nothing was changed. ${RESTART}`);
32420
- held.push(c.key);
32426
+ held2.push(c.key);
32421
32427
  found.push({ id: id2, signal: c.signal, stepId: c.stepId, key: c.key, keyHash: await hashKey2(c.key) });
32422
32428
  } else if (known && shape.kind === "empty") {
32423
32429
  found.push({ id: id2, signal: known, stepId: null, key: null, keyHash: null });
@@ -32440,7 +32446,7 @@ async function brainWireGhl(deps, args) {
32440
32446
  if (c.kind === "uncertain") return refuse4(`Workflow ${id2} reaches this brain, but ${c.why}, so the brain cannot tell what it is. Nothing was changed. ${RESTART}`);
32441
32447
  if (c.kind === "brain") {
32442
32448
  if (known !== c.signal) return refuse4(`Workflow ${id2} is saved on this computer as the brain's ${known} workflow but now reports ${c.signal}, so the brain cannot tell what it is. Nothing was changed. ${RESTART}`);
32443
- held.push(c.key);
32449
+ held2.push(c.key);
32444
32450
  found.push({ id: id2, signal: c.signal, stepId: c.stepId, key: c.key, keyHash: await hashKey2(c.key) });
32445
32451
  } else if (shape.kind === "empty") found.push({ id: id2, signal: known, stepId: null, key: null, keyHash: null });
32446
32452
  else return refuse4(`Workflow ${id2} (${workflowToFind(known, id2)}) no longer holds the brain's one webhook step. Nothing was changed. ${RESTART}`);
@@ -32537,13 +32543,13 @@ async function brainWireGhl(deps, args) {
32537
32543
  else if (now.keyState.wiredAt !== null) {
32538
32544
  if (!onlyKnownKeyless([...bySignal.values()], now.keyState.hash)) return { ok: false, wrote: "nothing", error: NO_CURRENT_KEY };
32539
32545
  writeKey = newKey("write");
32540
- held.push(writeKey);
32546
+ held2.push(writeKey);
32541
32547
  if (!await store2.replaceFirstWriteKey(client.id, now.keyState.hash, await hashKey2(writeKey), lease)) {
32542
32548
  return { ok: false, wrote: "nothing", error: "The brain's key changed while this ran, or another operation took over, so nothing was changed. Run brain_wire_ghl again." };
32543
32549
  }
32544
32550
  } else {
32545
32551
  writeKey = newKey("write");
32546
- held.push(writeKey);
32552
+ held2.push(writeKey);
32547
32553
  const leftovers = [...bySignal.values()].filter((w) => w.keyHash !== null).map((w) => [w.id, w.keyHash]);
32548
32554
  if (leftovers.length) deps.saveMeta({ [leftoversKey]: JSON.stringify(leftovers) });
32549
32555
  if (!await store2.setFirstWriteKey(client.id, now.keyState.hash, await hashKey2(writeKey), lease)) {
@@ -32679,12 +32685,12 @@ function keyOf(a) {
32679
32685
  async function clearSetupLock(deps, api, ref, args) {
32680
32686
  const table = await api.query(ref, "select to_regclass('brain.setup_lease') is not null as present;");
32681
32687
  if (table[0]?.present !== true) return { ok: true, wrote: "nothing", note: "No setup lock is held on this project." };
32682
- const held = import_zod63.z.array(import_zod63.z.object({ holder: import_zod63.z.string(), taken_at: import_zod63.z.string() })).parse(await api.query(ref, "select holder, taken_at::text as taken_at from brain.setup_lease;"));
32683
- if (!held.length) return { ok: true, wrote: "nothing", note: "No setup lock is held on this project." };
32684
- const plan = { mode: "clear_setup_lock", project: ref, lock: { takenAt: held[0].taken_at }, writes: "Deletes the setup lock row only. Nothing else in the project changes. Only do this if no brain_setup is running for this project right now." };
32685
- const g = gate2(plan, args, deps, { operation: "brain_retire:clear_setup_lock", projectRef: ref, clientId: "setup-lock", locationId: held[0].holder });
32688
+ const held2 = import_zod63.z.array(import_zod63.z.object({ holder: import_zod63.z.string(), taken_at: import_zod63.z.string() })).parse(await api.query(ref, "select holder, taken_at::text as taken_at from brain.setup_lease;"));
32689
+ if (!held2.length) return { ok: true, wrote: "nothing", note: "No setup lock is held on this project." };
32690
+ const plan = { mode: "clear_setup_lock", project: ref, lock: { takenAt: held2[0].taken_at }, writes: "Deletes the setup lock row only. Nothing else in the project changes. Only do this if no brain_setup is running for this project right now." };
32691
+ const g = gate2(plan, args, deps, { operation: "brain_retire:clear_setup_lock", projectRef: ref, clientId: "setup-lock", locationId: held2[0].holder });
32686
32692
  if (!g.go) return g.response;
32687
- await api.query(ref, `delete from brain.setup_lease where holder = '${held[0].holder.replace(/'/g, "''")}';`);
32693
+ await api.query(ref, `delete from brain.setup_lease where holder = '${held2[0].holder.replace(/'/g, "''")}';`);
32688
32694
  const left = await api.query(ref, "select 1 from brain.setup_lease;");
32689
32695
  return { ok: left.length === 0, cleared: left.length === 0, next: "Run brain_setup again." };
32690
32696
  }
@@ -33147,8 +33153,8 @@ function normUrl(u) {
33147
33153
  try {
33148
33154
  const url = new URL(scheme + t);
33149
33155
  const host = url.hostname.replace(/\.$/, "").replace(/^www\./, "");
33150
- const path66 = url.pathname.replace(/\/+$/, "");
33151
- return `${host}${url.port ? `:${url.port}` : ""}${path66}`.toLowerCase();
33156
+ const path67 = url.pathname.replace(/\/+$/, "");
33157
+ return `${host}${url.port ? `:${url.port}` : ""}${path67}`.toLowerCase();
33152
33158
  } catch {
33153
33159
  return t.replace(/^https?:\/\//, "").replace(/^[^/@]*@/, "").replace(/^www\./, "").replace(/[?#].*$/, "").replace(/\/+$/, "");
33154
33160
  }
@@ -33565,8 +33571,8 @@ async function fingerprint(body, firstName) {
33565
33571
  let at = 0;
33566
33572
  for (const l of linksIn(t)) {
33567
33573
  out += t.slice(at, l.index);
33568
- const path66 = l.canon.split("/").filter((seg) => seg && !/^(?=.*\d)[\p{L}\p{N}_-]{12,}$/u.test(seg) && !/^[0-9a-f-]{16,}$/i.test(seg));
33569
- out += ` link ${path66.join(" ").replace(/[^\p{L}\p{N}]+/gu, " ")} `;
33574
+ const path67 = l.canon.split("/").filter((seg) => seg && !/^(?=.*\d)[\p{L}\p{N}_-]{12,}$/u.test(seg) && !/^[0-9a-f-]{16,}$/i.test(seg));
33575
+ out += ` link ${path67.join(" ").replace(/[^\p{L}\p{N}]+/gu, " ")} `;
33570
33576
  at = l.index + l.length;
33571
33577
  }
33572
33578
  t = (out + t.slice(at)).replace(/[\p{L}\p{N}._%+-]+@[\p{L}\p{N}.-]+\.[\p{L}]{2,}/gu, " ").replace(/\b\d{4}-\d{1,2}-\d{1,2}\b|\b\d{1,2}[/.-]\d{1,2}(?:[/.-]\d{2,4})?\b/g, " ").replace(/\b\d{1,2}(?::\d{2})?\s*(?:[ap]\.?\s?m\b\.?)|\b\d{1,2}:\d{2}\b/g, " ").replace(/(?:[$€£¥₹]|\b(?:usd|eur|gbp|aed|sar|cad|aud)\b)\s?\d[\d,]*(?:\.\d+)?|\b\d[\d,]*(?:\.\d+)?\s?(?:%|percent\b|dollars?\b|(?:usd|eur|gbp|aed|sar|cad|aud)\b)/g, " ").replace(/\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\.?\s+\d{1,2}(?:st|nd|rd|th)?\b(?:,?\s+\d{4}\b)?/g, " ").replace(/\b\d{1,2}(?:st|nd|rd|th)?\s+(?:of\s+)?(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\.?(?:,?\s+\d{4}\b)?/g, " ").replace(/\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\.?,?\s+\d{4}\b/g, " ").replace(/\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\.?\b/g, " ").replace(/\b(?:mon|tue|tues|wed|thu|thur|thurs|fri|sat|sun)(?:day|sday|nesday|rsday|urday)?\b/g, " ").replace(/[^\p{L}\p{M}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
@@ -33864,15 +33870,15 @@ var init_cockpit_budget = __esm({
33864
33870
 
33865
33871
  // src/command-os/brain/account-proposals.ts
33866
33872
  function clientSettingsGet(client) {
33867
- return (path66, params, opts) => client.get(path66, opts?.onText ? { params, onText: opts.onText } : { params });
33873
+ return (path67, params, opts) => client.get(path67, opts?.onText ? { params, onText: opts.onText } : { params });
33868
33874
  }
33869
- function settingsPathAllowed(path66, locationId2) {
33870
- return path66 === "/calendars/" || path66 === "/products/" || path66 === "/users/" || /^\/products\/[A-Za-z0-9_-]{1,64}\/price$/.test(path66) || path66 === `/locations/${encodeURIComponent(locationId2)}`;
33875
+ function settingsPathAllowed(path67, locationId2) {
33876
+ return path67 === "/calendars/" || path67 === "/products/" || path67 === "/users/" || /^\/products\/[A-Za-z0-9_-]{1,64}\/price$/.test(path67) || path67 === `/locations/${encodeURIComponent(locationId2)}`;
33871
33877
  }
33872
33878
  function settingsOnly(locationId2, get2) {
33873
- return async (path66, params, opts) => {
33874
- if (!settingsPathAllowed(path66, locationId2)) throw new NotASettingError(`${path66} is not an account setting, so it was not read`);
33875
- return opts ? get2(path66, params, opts) : get2(path66, params);
33879
+ return async (path67, params, opts) => {
33880
+ if (!settingsPathAllowed(path67, locationId2)) throw new NotASettingError(`${path67} is not an account setting, so it was not read`);
33881
+ return opts ? get2(path67, params, opts) : get2(path67, params);
33876
33882
  };
33877
33883
  }
33878
33884
  function openHoursOf(raw) {
@@ -34076,10 +34082,10 @@ function continuationOf(raw) {
34076
34082
  async function readAccountSettings(get2, locationId2) {
34077
34083
  const out = { candidates: [], notRead: [], empty: [], held: [] };
34078
34084
  const more = /* @__PURE__ */ new Map();
34079
- const read2 = async (what, schema, path66, params, onText) => {
34085
+ const read2 = async (what, schema, path67, params, onText) => {
34080
34086
  let raw;
34081
34087
  try {
34082
- raw = await (onText ? get2(path66, params, { onText }) : get2(path66, params));
34088
+ raw = await (onText ? get2(path67, params, { onText }) : get2(path67, params));
34083
34089
  } catch (e) {
34084
34090
  const budget = budgetFailureText(e);
34085
34091
  out.notRead.push(budget !== null ? `Not read, ${what}: the brain could not pay for the request, so nothing was sent to GoHighLevel.` : e instanceof Error && e.name === "NotASettingError" ? `Not read, ${what}: it is not an account setting, so the brain did not ask for it.` : `Could not read ${what}: GoHighLevel did not answer.`);
@@ -35791,20 +35797,20 @@ async function brainAudit(deps, args) {
35791
35797
  const started = Date.now();
35792
35798
  let calls = 0;
35793
35799
  const notChecked = [];
35794
- const read2 = async (what, path66, params) => {
35800
+ const read2 = async (what, path67, params) => {
35795
35801
  if (calls >= MAX_GHL_CALLS || Date.now() - started > TIME_BUDGET_MS) {
35796
35802
  notChecked.push(what);
35797
35803
  return { got: "skipped" };
35798
35804
  }
35799
35805
  calls += 1;
35800
35806
  try {
35801
- return { got: "data", data: await deps.ghlGet(path66, params) };
35807
+ return { got: "data", data: await deps.ghlGet(path67, params) };
35802
35808
  } catch {
35803
35809
  return { got: "error" };
35804
35810
  }
35805
35811
  };
35806
- const ghl = async (what, path66, params) => {
35807
- const r = await read2(what, path66, params);
35812
+ const ghl = async (what, path67, params) => {
35813
+ const r = await read2(what, path67, params);
35808
35814
  if (r.got === "error") notChecked.push(`${what} (GoHighLevel could not be read)`);
35809
35815
  return r.got === "data" ? r.data : void 0;
35810
35816
  };
@@ -40647,6 +40653,7 @@ function journalObjectFor(b, parents) {
40647
40653
  };
40648
40654
  const parent = parents.get(b.ref);
40649
40655
  if (parent) o.parentRef = parent;
40656
+ if (b.type === "calendar" && b.teamIds?.length) o.teamIds = [...b.teamIds];
40650
40657
  return o;
40651
40658
  }
40652
40659
  function buildRunRecord(input) {
@@ -40823,6 +40830,32 @@ function describeRuns(locationId2, base) {
40823
40830
  reverted: reverted.has(r.runId)
40824
40831
  })).reverse();
40825
40832
  }
40833
+ function calendarTeamsFromJournal(locationId2, base) {
40834
+ const out = {};
40835
+ for (const r of readJournal(locationId2, base)) {
40836
+ if (r.kind !== "build-object") continue;
40837
+ const o = r.object;
40838
+ if (o?.type === "calendar" && o.action === "created" && o.teamIds?.length) out[o.name.trim().toLowerCase()] = [...o.teamIds];
40839
+ }
40840
+ return out;
40841
+ }
40842
+ function madeByBuild(locationId2, planId, base) {
40843
+ const records = readJournal(locationId2, base);
40844
+ const planOf = /* @__PURE__ */ new Map();
40845
+ let since = "";
40846
+ for (const r of records) {
40847
+ if (r.kind === "build-start") planOf.set(r.runId, r.planId);
40848
+ if (r.kind === "revert" && (!planId || planOf.get(r.runId) === planId)) since = r.at > since ? r.at : since;
40849
+ }
40850
+ const out = /* @__PURE__ */ new Map();
40851
+ for (const r of records) {
40852
+ if (r.kind !== "build-object" || r.at <= since) continue;
40853
+ if (planId && planOf.get(r.runId) !== planId) continue;
40854
+ const o = r.object;
40855
+ if (o?.action === "created" && !out.has(o.ref)) out.set(o.ref, { ref: o.ref, type: o.type, name: o.name });
40856
+ }
40857
+ return [...out.values()];
40858
+ }
40826
40859
  var fs24, path24, DIRECTORY_FLUSH_UNSUPPORTED;
40827
40860
  var init_build_journal = __esm({
40828
40861
  "src/build-journal.ts"() {
@@ -40863,15 +40896,15 @@ function shimTarget(shim) {
40863
40896
  return null;
40864
40897
  }
40865
40898
  const refs3 = [...text3.matchAll(/"%~?dp0%?\\([^"\r\n]+)"/g)].map((m) => m[1]);
40866
- const resolve14 = (rel) => path25.join(path25.dirname(shim), ...rel.split(/[\\/]/));
40899
+ const resolve15 = (rel) => path25.join(path25.dirname(shim), ...rel.split(/[\\/]/));
40867
40900
  const script = refs3.find((r) => /\.(?:js|cjs|mjs)$/i.test(r));
40868
40901
  if (script) {
40869
- const p = resolve14(script);
40902
+ const p = resolve15(script);
40870
40903
  return fs25.existsSync(p) ? { kind: "script", path: p } : null;
40871
40904
  }
40872
40905
  const program = refs3.find((r) => /\.exe$/i.test(r) && !/(^|[\\/])node\.exe$/i.test(r));
40873
40906
  if (program) {
40874
- const p = resolve14(program);
40907
+ const p = resolve15(program);
40875
40908
  return fs25.existsSync(p) ? { kind: "program", path: p } : null;
40876
40909
  }
40877
40910
  return null;
@@ -41110,12 +41143,12 @@ function noteObservedEngine(observed, file3 = enginePath()) {
41110
41143
  return true;
41111
41144
  }
41112
41145
  function runSmoke(bin, timeoutMs = 9e4) {
41113
- return new Promise((resolve14) => {
41146
+ return new Promise((resolve15) => {
41114
41147
  let settled = false;
41115
41148
  const done = (r) => {
41116
41149
  if (!settled) {
41117
41150
  settled = true;
41118
- resolve14(r);
41151
+ resolve15(r);
41119
41152
  }
41120
41153
  };
41121
41154
  const cmd = claudeCommand(bin, [
@@ -41331,7 +41364,7 @@ What the saved plan says should exist:
41331
41364
  ${saved.summary}
41332
41365
  1. switch_location to ${locationId2} (${locationName}); verify with get_current_location and stop if it is not that location.
41333
41366
  2. Call apply_build_plan with useSavedPlan:true and mode:"dry_run" (no plan argument \u2014 the tool loads the saved plan itself). Read the report: same-named objects that already exist are bound, never duplicated.
41334
- 3. Call apply_build_plan with useSavedPlan:true and mode:"execute". Never delete or recreate anything that already exists.
41367
+ 3. Call apply_build_plan with useSavedPlan:true and mode:"execute". Never delete or recreate anything that already exists. ${ONE_EXECUTE_RULE}
41335
41368
  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}
41336
41369
  5. ${STAFF_NOTIFICATION_RULE} A notification step whose recipient is empty is listed in issues as waiting for a staff member, never left unmentioned.
41337
41370
  6. ${MISSING_STEP_RULE} ${STAFF_PAUSE_RULE}
@@ -41471,19 +41504,19 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
41471
41504
  const spec = opts.spec ?? STAGE_SPECS[stage];
41472
41505
  if (!spec) return Promise.reject(new Error(`Stage ${stage} is not powered yet.`));
41473
41506
  let engineAcct = {};
41474
- return new Promise((resolve14) => {
41507
+ return new Promise((resolve15) => {
41475
41508
  void (async () => {
41476
41509
  const bin = claudeBin();
41477
41510
  if (!opts.preflightServer && !stageServerPinned()) {
41478
41511
  const reason3 = "Command OS could not make a fixed copy of its own server beside it (the install folder may be read-only), so the build step was not started. Reinstall GHL Command in a folder you can write to, or ask for help.";
41479
41512
  onEvent({ kind: "error", line: reason3 });
41480
- resolve14({ ok: false, error: reason3 });
41513
+ resolve15({ ok: false, error: reason3 });
41481
41514
  return;
41482
41515
  }
41483
41516
  const preflight = await (opts.preflightServer ?? preflightPinnedServer)();
41484
41517
  if (!preflight.ok) {
41485
41518
  onEvent({ kind: "error", line: preflight.reason });
41486
- resolve14({ ok: false, error: preflight.reason });
41519
+ resolve15({ ok: false, error: preflight.reason });
41487
41520
  return;
41488
41521
  }
41489
41522
  onEvent({ kind: "status", line: `Writing through GHL Command build ${ownBuildId()} from ${pinnedMcpConfig().mcpServers.ghl.args[0]}.` });
@@ -41491,7 +41524,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
41491
41524
  engineAcct = { engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked };
41492
41525
  if (!engineCheck.ok) {
41493
41526
  onEvent({ kind: "error", line: engineCheck.reason });
41494
- resolve14({ ok: false, error: engineCheck.reason, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked, engineVersion: engineCheck.version, engineModel: engineCheck.smokeModel });
41527
+ resolve15({ ok: false, error: engineCheck.reason, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked, engineVersion: engineCheck.version, engineModel: engineCheck.smokeModel });
41495
41528
  return;
41496
41529
  }
41497
41530
  onEvent({ kind: "status", line: `Starting ${spec.name} for ${locationName}.` });
@@ -41507,7 +41540,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
41507
41540
  if (!opts.preflightServer && buildIdOf(pinned2) !== ownBuildId()) {
41508
41541
  const reason3 = `The build step's GHL Command server file changed after it was checked (it is now build ${buildIdOf(pinned2)}, this cockpit is build ${ownBuildId()}), so nothing was started. Restart the cockpit, then try again.`;
41509
41542
  onEvent({ kind: "error", line: reason3 });
41510
- resolve14({ ok: false, error: reason3, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked });
41543
+ resolve15({ ok: false, error: reason3, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked });
41511
41544
  return;
41512
41545
  }
41513
41546
  const cmd = claudeCommand(bin, buildClaudeArgs(spec, locationId2, locationName, opts.context));
@@ -41604,7 +41637,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
41604
41637
  if (inventoryError) {
41605
41638
  if (progressPoll) clearInterval(progressPoll);
41606
41639
  onEvent({ kind: "error", line: inventoryError });
41607
- resolve14({ ok: false, error: inventoryError, engineVersion, engineModel, costUsd, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked });
41640
+ resolve15({ ok: false, error: inventoryError, engineVersion, engineModel, costUsd, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked });
41608
41641
  return;
41609
41642
  }
41610
41643
  if (progressPoll) clearInterval(progressPoll);
@@ -41625,7 +41658,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
41625
41658
  if (!timedOut && code !== 0 && texts.empty) {
41626
41659
  const outcome2 = { ok: false, error: `claude exited ${code}: ${stderrTail.trim().slice(-200) || "no output"}`, ...engine };
41627
41660
  onEvent({ kind: "error", line: outcome2.error });
41628
- resolve14(outcome2);
41661
+ resolve15(outcome2);
41629
41662
  return;
41630
41663
  }
41631
41664
  const outcome = timedOut ? { ok: false, error: "the run timed out and was stopped before it finished", ...engine } : texts.failure ? { ok: false, error: `the run did not finish (${texts.failure})`, raw: texts.pick().slice(-400), ...engine } : { ...parseResultLine(texts.pick()), ...engine };
@@ -41639,7 +41672,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
41639
41672
  const detail = outcome.error || (outcome.issues?.length ? outcome.issues.join(" \xB7 ") : "") || outcome.summary || "no detail reported";
41640
41673
  const success = outcome.formId ? `Done: form "${outcome.formName}" (${outcome.formId})` : `Done: ${outcome.summary ?? "stage complete"}`;
41641
41674
  onEvent({ kind: "result", line: outcome.ok ? success : `Not passed: ${detail}` });
41642
- resolve14(outcome);
41675
+ resolve15(outcome);
41643
41676
  });
41644
41677
  child.on("error", (err) => {
41645
41678
  spawnFailed = true;
@@ -41647,16 +41680,16 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
41647
41680
  if (progressPoll) clearInterval(progressPoll);
41648
41681
  const outcome = { ok: false, error: `could not start claude: ${err.message}`, engineCheckCostUsd: engineCheck.smokeCostUsd, engineCheckRan: engineCheck.smoked };
41649
41682
  onEvent({ kind: "error", line: outcome.error });
41650
- resolve14(outcome);
41683
+ resolve15(outcome);
41651
41684
  });
41652
41685
  })().catch((err) => {
41653
41686
  const line4 = err instanceof Error ? err.message : String(err);
41654
41687
  onEvent({ kind: "error", line: line4 });
41655
- resolve14({ ok: false, error: line4, ...engineAcct });
41688
+ resolve15({ ok: false, error: line4, ...engineAcct });
41656
41689
  });
41657
41690
  });
41658
41691
  }
41659
- var import_child_process3, fs27, BRIEF_NEEDS_INTAKE, STAFF_NOTIFICATION_RULE, STAFF_PAUSE_RULE, BUILD_REPLY_RULE, MISSING_STEP_RULE, CONFIRM_PAUSE_RULE, NO_INVENTED_CAUSE_RULE, STAGE_SPECS, HUMAN_GATE_STAGES, RESULT_LAST_RULE;
41692
+ var import_child_process3, fs27, BRIEF_NEEDS_INTAKE, ONE_EXECUTE_RULE, STAFF_NOTIFICATION_RULE, STAFF_PAUSE_RULE, BUILD_REPLY_RULE, MISSING_STEP_RULE, CONFIRM_PAUSE_RULE, NO_INVENTED_CAUSE_RULE, STAGE_SPECS, HUMAN_GATE_STAGES, RESULT_LAST_RULE;
41660
41693
  var init_stage_runner = __esm({
41661
41694
  "src/stage-runner.ts"() {
41662
41695
  "use strict";
@@ -41679,6 +41712,7 @@ var init_stage_runner = __esm({
41679
41712
  init_client_record();
41680
41713
  init_workflow_builder_client();
41681
41714
  BRIEF_NEEDS_INTAKE = "First complete the intake. It fills most answers from a call or your notes.";
41715
+ ONE_EXECUTE_RULE = "Make ONE execute call and wait for its answer, however long it takes; never call apply_build_plan again while an earlier call has not answered. If it answers buildRunning, another build is writing to this account: do not call it again; finish with RESULT ok:false naming that.";
41682
41716
  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".`;
41683
41717
  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.`;
41684
41718
  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.`;
@@ -41818,7 +41852,7 @@ Use these authoritative normalized intake answers when provided: ${JSON.stringif
41818
41852
  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.
41819
41853
  ${BUILD_REPLY_RULE}
41820
41854
  4. Read the dry-run report. Its composed.copySlots list the message templates; their copy is already filled from the brief and send-ready. ONLY where the brief gives client-specific material the preset copy misses (the client's signature line, a named offer or price), pass copy:[{ref:"email_template.<slug>", subject, html}] / [{ref:"sms_template.<slug>", body}] for JUST those templates on the next call. Never restructure; never rewrite every message.
41821
- 5. Call apply_build_plan again with the SAME fromPreset:true, preset and brief (plus your copy overrides, if any) and mode:"execute". Never delete or recreate anything that already exists.
41855
+ 5. Call apply_build_plan again with the SAME fromPreset:true, preset and brief (plus your copy overrides, if any) and mode:"execute". Never delete or recreate anything that already exists. ${ONE_EXECUTE_RULE}
41822
41856
  6. FALLBACK \u2014 only if apply_build_plan answered with phase:"compose" and errors that point at a genuine preset gap (not a fixable brief problem): author the plan yourself per the PLAN GUIDE below, check it with validate_build_plan, STOP if it does not validate, then apply_build_plan dry_run \u2192 execute with that plan.
41823
41857
  7. Read back what landed (get_pipelines, list_workflows_full, get_calendars).
41824
41858
  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.
@@ -42911,13 +42945,13 @@ var init_handoff_pack = __esm({
42911
42945
 
42912
42946
  // src/command-os/handoff-run.ts
42913
42947
  function askNoTools(prompt, opts = {}) {
42914
- return new Promise((resolve14) => {
42948
+ return new Promise((resolve15) => {
42915
42949
  let bin;
42916
42950
  try {
42917
42951
  bin = claudeBin();
42918
42952
  } catch {
42919
42953
  opts.onCost?.();
42920
- resolve14("");
42954
+ resolve15("");
42921
42955
  return;
42922
42956
  }
42923
42957
  const cmd = claudeCommand(bin, ["-p", prompt, "--output-format", "stream-json", "--verbose", "--max-turns", String(opts.maxTurns ?? 3), "--allowedTools", "", "--disallowedTools", `${NO_TOOLS_DENY},${MCP_DENY}`, ...NO_MEMBER_HOOKS, ...NO_MCP_ARGS]);
@@ -42951,12 +42985,12 @@ function askNoTools(prompt, opts = {}) {
42951
42985
  child.on("error", () => {
42952
42986
  clearTimeout(timeout);
42953
42987
  report3();
42954
- resolve14(texts.pick());
42988
+ resolve15(texts.pick());
42955
42989
  });
42956
42990
  child.on("close", () => {
42957
42991
  clearTimeout(timeout);
42958
42992
  report3();
42959
- resolve14(texts.pick());
42993
+ resolve15(texts.pick());
42960
42994
  });
42961
42995
  });
42962
42996
  }
@@ -43505,6 +43539,7 @@ function goLiveTopic(text3) {
43505
43539
  function jobTiming(text3) {
43506
43540
  const t = String(text3).trim();
43507
43541
  if (t === HANDOFF_NO_HOME_JOB) return "later";
43542
+ if (HELD_JOB.test(t)) return "later";
43508
43543
  if (LEAD_MAGNET_PAGES_JOB.test(t)) return "later";
43509
43544
  if (SWITCH_ON_JOB.test(t) || STILL_OFF_NOTE.test(t)) return "at-go-live";
43510
43545
  if (LATER_JOB.test(t)) return "later";
@@ -43621,11 +43656,12 @@ function manualSignOffRefusal(tasks2, verifyHasRun) {
43621
43656
  }
43622
43657
  return "Everything is ticked, but the check has not confirmed it yet. Run the check again and this will clear on its own.";
43623
43658
  }
43624
- var SELF_REPORTED, SWITCH_ON_JOB, LATER_JOB, LEAD_MAGNET_PAGES_JOB, HANDOFF_NO_HOME_JOB, GO_LIVE_TOPICS, NAMED_LATER, STILL_OFF_NOTE, keepsTick;
43659
+ var SELF_REPORTED, HELD_JOB, SWITCH_ON_JOB, LATER_JOB, LEAD_MAGNET_PAGES_JOB, HANDOFF_NO_HOME_JOB, GO_LIVE_TOPICS, NAMED_LATER, STILL_OFF_NOTE, keepsTick;
43625
43660
  var init_manual_tasks = __esm({
43626
43661
  "src/manual-tasks.ts"() {
43627
43662
  "use strict";
43628
43663
  SELF_REPORTED = /^[^:]+: Bought, but not built automatically yet\./;
43664
+ HELD_JOB = /^(?:Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
43629
43665
  SWITCH_ON_JOB = /^[^\n]+: This workflow is built and waiting for you to switch it on in GoHighLevel under Automation > Workflows\.$/;
43630
43666
  LATER_JOB = /^Manual action: (?:[^:\n]*\b(?:after (?:a|the|each) (?:trial )?visit|before any bulk)\b[^:\n]*|How [^:\n]{1,80} starts): /;
43631
43667
  LEAD_MAGNET_PAGES_JOB = /^[^:\n]+: Bought, but not built automatically yet\. The form is built\. The pages are separate work\.$/;
@@ -43640,6 +43676,7 @@ var init_manual_tasks = __esm({
43640
43676
  { key: "price", title: "Check the trial price, deposit and balance", re: /trial price|deposit amount|balance amount|trial_price/i }
43641
43677
  ];
43642
43678
  NAMED_LATER = [
43679
+ HELD_JOB,
43643
43680
  /^[^:\n]+: Bought, but not built automatically yet\. (?:Set up rebooking and recall|Payment setup is done|Set up referral requests|The form is built|It is not in the plan)/,
43644
43681
  /^Manual action: Set up the trial balance link\b/,
43645
43682
  /^Manual action: Limit the Trial Balance Paid workflow\b/,
@@ -44650,7 +44687,7 @@ function stopReasonSentence(reason3, fetched, cap2) {
44650
44687
  }
44651
44688
  }
44652
44689
  function delay(ms) {
44653
- return new Promise((resolve14) => setTimeout(resolve14, ms));
44690
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
44654
44691
  }
44655
44692
  async function fetchAllContacts(client, locationId2, options = {}) {
44656
44693
  const cap2 = options.maxContacts ?? DEFAULT_MAX_CONTACTS;
@@ -45488,7 +45525,7 @@ var init_client_capabilities = __esm({
45488
45525
  { key: "reputation", label: "Reputation and content", blurb: "Reviews, social posts and the blog." },
45489
45526
  { key: "health", label: "Health and handoff", blurb: "Proving it works, keeping it clean, and handing it over." }
45490
45527
  ];
45491
- N = (label2, path66, params, key2, nameField, version) => ({ label: label2, path: path66, params, key: key2, nameField, version });
45528
+ N = (label2, path67, params, key2, nameField, version) => ({ label: label2, path: path67, params, key: key2, nameField, version });
45492
45529
  W = (read2, extra) => ({ ...read2, ...extra });
45493
45530
  AI2 = "2021-04-15";
45494
45531
  CAPABILITIES = [
@@ -45616,7 +45653,10 @@ var init_client_capabilities = __esm({
45616
45653
  hub: "command-os/ai-employee.html",
45617
45654
  reads: [
45618
45655
  W(N("knowledge bases", "/knowledge-bases/", { locationId: "{loc}", limit: "50" }, "knowledgeBases", "name", AI2), { one: "knowledge base" }),
45619
- N("bots", "/conversation-ai/agents/search", { limit: "50" }, "agents", "name", AI2)
45656
+ N("bots", "/conversation-ai/agents/search", { limit: "50" }, "agents", "name", AI2),
45657
+ // Walk W42: Ready to answer reads the account live, the way Verify does.
45658
+ { ...N("booking calendars", "/calendars/", { locationId: "{loc}" }, "calendars", "name"), activeOnly: true },
45659
+ N("phone numbers", "/phone-system/numbers/", { locationId: "{loc}" }, "phoneNumbers", "title")
45620
45660
  ]
45621
45661
  },
45622
45662
  {
@@ -45626,7 +45666,7 @@ var init_client_capabilities = __esm({
45626
45666
  what: "GoHighLevel's name for the agent that answers the phone. Built without a number, given one on purpose, proven by the transcript.",
45627
45667
  says: "Check whether a call can land on {client}'s account, then build a Voice AI agent that answers, books and takes a message. Leave it not yet connected to a phone number or calendar until I approve those connections. List a test conversation as still needed.",
45628
45668
  hub: "modules/11-your-ai-employee/lesson-05-the-phone-agent.html",
45629
- reads: [N("phone agents", "/voice-ai/agents", { locationId: "{loc}", page: "1", pageSize: "50" }, "agents", "agentName", AI2), N("phone numbers", "/phone-system/numbers/", { locationId: "{loc}" }, "phoneNumbers", "title")]
45669
+ reads: [N("phone agents", "/voice-ai/agents", { locationId: "{loc}", page: "1", pageSize: "50" }, "agents", "agentName", AI2), N("phone numbers", "/phone-system/numbers/", { locationId: "{loc}" }, "phoneNumbers", "title"), { ...N("booking calendars", "/calendars/", { locationId: "{loc}" }, "calendars", "name"), activeOnly: true }]
45630
45670
  },
45631
45671
  {
45632
45672
  key: "demo",
@@ -45635,7 +45675,13 @@ var init_client_capabilities = __esm({
45635
45675
  what: "One sentence about a prospect's business becomes a bot they can text, on your own demo account, and one command puts it back. It messages a real prospect, so it runs from your own Claude with you watching, never from a button.",
45636
45676
  says: "Prepare an AI employee demo for {client} from their website and arm it so they can text it.",
45637
45677
  hub: "modules/11-your-ai-employee/lesson-04-the-five-minute-demo.html",
45638
- reads: []
45678
+ // command-center 2026-10-29: read live, the same two lists the demo tools use (the demo bot, and one knowledge
45679
+ // base per prepared prospect, named "Demo: ..."). Whether a demo is armed is the bot's mode, which only
45680
+ // show_ai_employee_demo reads; this page does not claim it.
45681
+ reads: [
45682
+ N("bots", "/conversation-ai/agents/search", { limit: "50" }, "agents", "name", AI2),
45683
+ W(N("knowledge bases", "/knowledge-bases/", { locationId: "{loc}", limit: "50" }, "knowledgeBases", "name", AI2), { one: "knowledge base" })
45684
+ ]
45639
45685
  },
45640
45686
  // ── Money and paperwork ─────────────────────────────────────────────────
45641
45687
  {
@@ -45778,7 +45824,7 @@ var require_package = __commonJS({
45778
45824
  "package.json"(exports2, module2) {
45779
45825
  module2.exports = {
45780
45826
  name: "@elitedcs/ghl-mcp",
45781
- version: "3.93.0",
45827
+ version: "3.94.0",
45782
45828
  mcpName: "io.github.drjerryrelth/ghl-command",
45783
45829
  description: "GoHighLevel MCP Server for Claude. 250 tools for CRM, automation and marketing, an account-wide workflow audit, live funnel-capture verification and a workflow builder, across client accounts.",
45784
45830
  main: "dist/index.js",
@@ -46135,13 +46181,9 @@ function cockpitMoney(amount,currency){
46135
46181
  var SAY=" (opens in a new tab)";
46136
46182
  function mark(a){if(a.getAttribute("data-nt")||a.hidden)return;a.setAttribute("data-nt","1");if((a.textContent||"").indexOf("opens in a new tab")<0){var s=document.createElement("span");s.className="nt";s.textContent=SAY;a.appendChild(s)}}
46137
46183
  function markAll(root){var list=(root||document).querySelectorAll?((root||document).querySelectorAll('a[target="_blank"]')):[];for(var i=0;i<list.length;i++)mark(list[i])}
46138
- document.addEventListener("click",function(e){if(e.defaultPrevented||e.button!==0||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey)return;
46139
- var a=e.target&&e.target.closest?e.target.closest('a[target="_blank"]'):null;if(!a||!a.href)return;
46140
- /* 8a LOW-5: only web addresses are opened by hand; anything else (javascript:, data:) is left to the browser. */
46141
- if(a.protocol!=="http:"&&a.protocol!=="https:")return;
46142
- /* "noopener" in the features makes window.open answer null even when it worked, so the opener is cut by hand. */
46143
- e.preventDefault();var w=null;try{w=window.open(a.href,"_blank")}catch(x){w=null}
46144
- if(w){try{w.opener=null;w.focus()}catch(x){}}else{var same=document.createElement("a");same.href=a.href;same.click()}},false);
46184
+ /* Walk W34 (2026-10-29, Safari): opening the page by hand inside the click fell back to this tab when Safari refused,
46185
+ and the cockpit was lost. The link is left to the browser: a real click on a target=_blank link opens a new tab in
46186
+ every browser. The redraw hold (clickInFlight) keeps the link under the pointer from being swapped mid-click. */
46145
46187
  markAll(document);
46146
46188
  try{new MutationObserver(function(ms){for(var i=0;i<ms.length;i++){var n=ms[i].addedNodes;for(var j=0;j<n.length;j++){var x=n[j];if(x.nodeType!==1)continue;if(x.matches&&x.matches('a[target="_blank"]'))mark(x);markAll(x)}}}).observe(document.documentElement,{childList:true,subtree:true})}catch(e){}
46147
46189
  })();`;
@@ -47161,9 +47203,9 @@ var init_cockpit_cache = __esm({
47161
47203
  */
47162
47204
  readNow(locationId2, page2, opts, priority = 1) {
47163
47205
  const key2 = this.cache.key(locationId2, page2);
47164
- const p = new Promise((resolve14) => {
47206
+ const p = new Promise((resolve15) => {
47165
47207
  const w = this.waiters.get(key2) ?? [];
47166
- w.push(resolve14);
47208
+ w.push(resolve15);
47167
47209
  this.waiters.set(key2, w);
47168
47210
  });
47169
47211
  this.queue(locationId2, page2, priority, opts);
@@ -47187,7 +47229,7 @@ var init_cockpit_cache = __esm({
47187
47229
  const key2 = this.cache.key(locationId2, page2);
47188
47230
  const ticket = given ?? this.cache.ticket(locationId2, page2);
47189
47231
  return new Promise((done) => {
47190
- const resolve14 = (v) => {
47232
+ const resolve15 = (v) => {
47191
47233
  setTimeout(() => done(v.ok && !this.cache.stillValid(ticket) ? { ok: false, error: new StaleReadError() } : v.ok ? { ...v, ticket } : v), 0);
47192
47234
  };
47193
47235
  this.pacer.enqueue(`${key2}#live${++this.liveSeq}`, locationId2 === "agency" ? "agency" : locationId2, priority, 0, (gate3) => this.cache.inRegion(ticket, async () => {
@@ -47198,17 +47240,17 @@ var init_cockpit_cache = __esm({
47198
47240
  const data = await Promise.race([opts.read((client) => pacedClient(client, gate3, count4), (fn) => !late && !gate3.expired?.() && this.cache.commitIfCurrent(ticket, fn)), gate3.deadline ?? NEVER]);
47199
47241
  const kept = this.cache.putFor(ticket, data, { reach: opts.reach, calls: count4.calls, ghlMs: this.now() - started, holdSave: opts.holdSave });
47200
47242
  if (kept === null) {
47201
- resolve14({ ok: false, error: new StaleReadError() });
47243
+ resolve15({ ok: false, error: new StaleReadError() });
47202
47244
  return;
47203
47245
  }
47204
47246
  if (kept.changed) opts.onChange?.(data);
47205
- resolve14({ ok: true, data });
47247
+ resolve15({ ok: true, data });
47206
47248
  } catch (e) {
47207
47249
  if (e instanceof PacerDeadlineError) late = true;
47208
47250
  this.cache.failFor(ticket, e instanceof PacerCeilingError ? DAILY_CEILING_TEXT : e instanceof PacerDeadlineError ? DEADLINE_TEXT : isSlowDown(e) ? SLOW_DOWN_TEXT : opts.failureText?.(e) ?? "could not read: try Refresh");
47209
- resolve14({ ok: false, error: e });
47251
+ resolve15({ ok: false, error: e });
47210
47252
  }
47211
- }), () => resolve14({ ok: false, error: new StaleReadError() }));
47253
+ }), () => resolve15({ ok: false, error: new StaleReadError() }));
47212
47254
  });
47213
47255
  }
47214
47256
  queue(locationId2, page2, priority, opts) {
@@ -47807,8 +47849,8 @@ function scoreRun(run) {
47807
47849
  const drafts = run.draftWorkflows ?? [];
47808
47850
  const stepProblems = st?.read ? (st.problems ?? []).length : 0;
47809
47851
  if (f.sent === 0 && judgedSteps === 0 && drafts.length === 0 && stepProblems === 0) return { graded: false, reason: "Nothing the test lead received and no workflow step could be judged, so there is nothing to grade. Check that the workflows are published, then run it again." };
47810
- const held = (f.notYetDue ?? []).filter((m) => m.heldByWait).length;
47811
- if (held) return { graded: false, reason: `GoHighLevel was still holding the test lead, so ${held} planned message${held === 1 ? " was" : "s were"} not judged. A grade now would look better than the truth. Run the check again.` };
47852
+ const held2 = (f.notYetDue ?? []).filter((m) => m.heldByWait).length;
47853
+ if (held2) return { graded: false, reason: `GoHighLevel was still holding the test lead, so ${held2} planned message${held2 === 1 ? " was" : "s were"} not judged. A grade now would look better than the truth. Run the check again.` };
47812
47854
  const deductions2 = [];
47813
47855
  const add2 = (kind, area, text3) => {
47814
47856
  const p = POINTS[kind];
@@ -48174,7 +48216,7 @@ async function listLeadCarriers(client, run) {
48174
48216
  const carriers = /* @__PURE__ */ new Map();
48175
48217
  let partial = false;
48176
48218
  try {
48177
- const paged = await readContactPages((path66, o) => client.get(path66, o), { locationId: run.locationId, query: run.leadEmail, limit: 20 });
48219
+ const paged = await readContactPages((path67, o) => client.get(path67, o), { locationId: run.locationId, query: run.leadEmail, limit: 20 });
48178
48220
  if (!paged || !paged.complete) partial = true;
48179
48221
  for (const c of paged?.rows ?? []) {
48180
48222
  const k = carrierOf(c);
@@ -48231,23 +48273,23 @@ function claimRun(locationId2, runId, base) {
48231
48273
  };
48232
48274
  } catch (e) {
48233
48275
  if (e.code !== "EEXIST") throw e;
48234
- let held = "";
48276
+ let held2 = "";
48235
48277
  let age = 0;
48236
48278
  try {
48237
- held = import_node_fs7.default.readFileSync(lock, "utf8");
48279
+ held2 = import_node_fs7.default.readFileSync(lock, "utf8");
48238
48280
  age = Date.now() - import_node_fs7.default.statSync(lock).mtimeMs;
48239
48281
  } catch {
48240
48282
  continue;
48241
48283
  }
48242
48284
  let pid = 0;
48243
48285
  try {
48244
- pid = Number(JSON.parse(held).pid);
48286
+ pid = Number(JSON.parse(held2).pid);
48245
48287
  } catch {
48246
48288
  }
48247
48289
  const stale = age > 10 * 6e4 || pid > 0 && !pidAlive(pid);
48248
48290
  if (!stale) return null;
48249
48291
  try {
48250
- if (import_node_fs7.default.readFileSync(lock, "utf8") === held) import_node_fs7.default.unlinkSync(lock);
48292
+ if (import_node_fs7.default.readFileSync(lock, "utf8") === held2) import_node_fs7.default.unlinkSync(lock);
48251
48293
  } catch {
48252
48294
  }
48253
48295
  }
@@ -48284,10 +48326,10 @@ async function finishShopper(locationId2, runId, deps) {
48284
48326
  if (deps.stepLog && run.entry.kind === "workflows") {
48285
48327
  run.steps = await auditSteps(deps.stepLog, run.entry.workflowIds, run.workflowNames ?? {}, run.contactId, run.startedAt, t, { hasPhone: Boolean(run.testPhone), hasEmail: Boolean(run.leadEmail), ...run.staffGaps?.length ? { staffGaps: run.staffGaps } : {} });
48286
48328
  }
48287
- const held = closed && run.steps?.read ? heldWaits(run.steps.rows) : [];
48329
+ const held2 = closed && run.steps?.read ? heldWaits(run.steps.rows) : [];
48288
48330
  const base = run.originalClosesAt ?? run.closesAt;
48289
48331
  const cap2 = base + MAX_EXTEND_MINUTES * 6e4;
48290
- const soon = held.filter((h) => h.waitUntil !== void 0 && h.waitUntil <= t + 5 * 6e4);
48332
+ const soon = held2.filter((h) => h.waitUntil !== void 0 && h.waitUntil <= t + 5 * 6e4);
48291
48333
  if (soon.length && t < cap2) {
48292
48334
  run.originalClosesAt = base;
48293
48335
  run.closesAt = Math.min(cap2, Math.max(t, ...soon.map((h) => h.waitUntil)) + 3 * 6e4);
@@ -48295,7 +48337,7 @@ async function finishShopper(locationId2, runId, deps) {
48295
48337
  saveRun(run, deps.base);
48296
48338
  return run;
48297
48339
  }
48298
- const late = held.filter((h) => h.waitUntil !== void 0 && h.waitUntil <= t);
48340
+ const late = held2.filter((h) => h.waitUntil !== void 0 && h.waitUntil <= t);
48299
48341
  if (closed && late.length) {
48300
48342
  const rows2 = run.steps.rows;
48301
48343
  const stillHeld = run.findings.missing.filter((m) => m.workflowId && m.stepId && late.some((h) => h.workflowId === m.workflowId && h.stepId !== void 0 && (m.afterWaits ?? []).includes(h.stepId)) && !rows2.some((r) => r.workflowId === m.workflowId && r.stepId === m.stepId));
@@ -48462,7 +48504,6 @@ function planCopy(src, opts) {
48462
48504
  const nodes = src.templates.map(asObj9);
48463
48505
  if (nodes.length === 0) return { ok: false, refusal: `"${src.name}" has no steps to test.` };
48464
48506
  if (nodes.length > 40) return { ok: false, refusal: `"${src.name}" has ${nodes.length} steps; fast-forward copies at most 40.` };
48465
- if (str11(src.status)?.toLowerCase() === "draft") return { ok: false, refusal: `"${src.name}" is still a draft, so real leads never go through it. Publish it first, or test it as it is.` };
48466
48507
  const idMap = /* @__PURE__ */ new Map();
48467
48508
  for (const n of nodes) {
48468
48509
  const id2 = str11(n.id);
@@ -48861,17 +48902,17 @@ ${ringHtml(card.score, card.coverageLabel ?? card.coverage, true)}</div>`;
48861
48902
  }
48862
48903
  const minutesLeft = Math.max(0, Math.ceil((run.closesAt - now) / 6e4));
48863
48904
  const planned = run.planNote.source !== "none" && run.expect.length > 0;
48864
- const held = f?.notYetDue.filter((m) => m.heldByWait) ?? [];
48865
- const title2 = !f ? "Watching what the test lead receives" : f.sent === 0 ? !watching && nothingArrivedHeading(run.steps) || `Nothing arrived in ${watching ? "the time so far" : `${run.watchMinutes} minutes`}` : f.problems.length || f.missing.length ? "What a new lead actually received" : held.length ? "Not finished: GoHighLevel was still holding the test lead" : planned ? "A new lead got exactly what you planned" : "No problems in what the lead received";
48905
+ const held2 = f?.notYetDue.filter((m) => m.heldByWait) ?? [];
48906
+ const title2 = !f ? "Watching what the test lead receives" : f.sent === 0 ? !watching && nothingArrivedHeading(run.steps) || `Nothing arrived in ${watching ? "the time so far" : `${run.watchMinutes} minutes`}` : f.problems.length || f.missing.length ? "What a new lead actually received" : held2.length ? "Not finished: GoHighLevel was still holding the test lead" : planned ? "A new lead got exactly what you planned" : "No problems in what the lead received";
48866
48907
  const head4 = pageHead(title2, `${productName()} made a test lead, then read every text and email this account sent it, word for word. ${esc2(run.entryNote)}`, action);
48867
48908
  const worst = f?.problems[0];
48868
48909
  const quote2 = (tone, text3, cite) => `<div class="cds-callout quote${tone ? ` ${tone}` : ""}">${text3}<cite>${cite}</cite></div>`;
48869
- const quoteHtml = f && f.sent === 0 ? quote2("warn", "The test lead received no text and no email.", esc2(nothingArrivedWhy(run.steps, run.draftWorkflows))) : worst ? quote2("bad", `${worst.kind === "blank_field" ? "A new lead with no first name on file got" : "A new lead got"} "${marked(worst.quote, "")}"`, `${esc2(worst.title)}${f && f.problems.length > 1 ? `. The first of ${f.problems.length}.` : "."}`) : f && held.length && !f.missing.length ? quote2("warn", `${held.length} planned message${held.length === 1 ? " was" : "s were"} still waiting behind a wait GoHighLevel had not let go of.`, "Not judged. Run the check again.") : f && f.missing.length ? quote2("warn", `${f.missing.length} planned message${f.missing.length === 1 ? " never" : "s never"} arrived.`, "Everything that did arrive read correctly.") : f ? quote2("", `Every message read correctly${planned ? " and arrived when planned" : ""}.`, `Checked for the first ${run.watchMinutes} minutes. Later messages are listed below as not yet checked.`) : "";
48910
+ const quoteHtml = f && f.sent === 0 ? quote2("warn", "The test lead received no text and no email.", esc2(nothingArrivedWhy(run.steps, run.draftWorkflows))) : worst ? quote2("bad", `${worst.kind === "blank_field" ? "A new lead with no first name on file got" : "A new lead got"} "${marked(worst.quote, "")}"`, `${esc2(worst.title)}${f && f.problems.length > 1 ? `. The first of ${f.problems.length}.` : "."}`) : f && held2.length && !f.missing.length ? quote2("warn", `${held2.length} planned message${held2.length === 1 ? " was" : "s were"} still waiting behind a wait GoHighLevel had not let go of.`, "Not judged. Run the check again.") : f && f.missing.length ? quote2("warn", `${f.missing.length} planned message${f.missing.length === 1 ? " never" : "s never"} arrived.`, "Everything that did arrive read correctly.") : f ? quote2("", `Every message read correctly${planned ? " and arrived when planned" : ""}.`, `Checked for the first ${run.watchMinutes} minutes. Later messages are listed below as not yet checked.`) : "";
48870
48911
  const stats = f ? `<div class="stats">
48871
48912
  ${statTile(f.sent, "Messages sent")}
48872
48913
  ${statTile(f.problems.length, "A customer would notice", f.problems.length ? "bad" : "")}
48873
48914
  ${run.steps?.read ? statTile(run.steps.rows.length ? run.steps.problems.length : "\u2013", "Steps failed, skipped or did nothing", run.steps.problems.length ? "bad" : run.steps.rows.length && !run.steps.unreadWorkflows?.length && !run.steps.partialWorkflows?.length && !run.steps.counts.unclear ? "" : "warn") : ""}
48874
- ${statTile(run.planNote.source === "none" ? "\u2013" : f.missing.length, "Planned, never sent", f.missing.length || held.length ? "warn" : "")}
48915
+ ${statTile(run.planNote.source === "none" ? "\u2013" : f.missing.length, "Planned, never sent", f.missing.length || held2.length ? "warn" : "")}
48875
48916
  ${(() => {
48876
48917
  const opened = f.links.total - f.links.skipped;
48877
48918
  return opened ? statTile(`${f.links.ok} of ${opened}`, `Links open${f.links.skipped ? ` \xB7 ${f.links.skipped} not opened` : ""}`, f.links.broken ? "bad" : "") : statTile(0, `Links opened${f.links.skipped ? ` \xB7 ${f.links.skipped} not` : ""}`);
@@ -48949,7 +48990,7 @@ function startForm(d, inlineSubmit) {
48949
48990
  <div class="cds-field" id="alert-to-wrap" hidden><label for="alert-to">Staff alerts, tasks and assignments go to</label><div class="hint" id="alert-to-hint">Instead of your staff, marked TEST.</div><select class="cds-select" id="alert-to" name="alertUserId" aria-describedby="alert-to-hint"></select></div>
48950
48991
  <fieldset class="cds-field" id="hooks" hidden><legend class="lbl">Outside services these workflows send the lead to</legend><div class="wf" id="hooks-list"></div></fieldset>
48951
48992
  ${callout("warn", "", "The test lead has no first name, on purpose. The workflows you tick will really send to your address and may alert your staff once.")}
48952
- ${inlineSubmit ? `<div class="cds-row"><button class="cds-btn ghost" type="submit" id="start-btn" disabled>Start the check</button></div>` : ""}
48993
+ <div class="cds-row"><button class="cds-btn${inlineSubmit ? " ghost" : ""}" type="submit" id="start-btn" data-start="" disabled>Start the check</button></div>
48953
48994
  <div id="start-ask"></div>
48954
48995
  <p class="cds-callout result" id="start-result" role="status" aria-live="polite"></p>
48955
48996
  </form>`;
@@ -48960,7 +49001,7 @@ function shopperPageHtml(d, seatToken, seatHeader) {
48960
49001
  const run = d.run;
48961
49002
  const graded2 = !!(run && run.status === "done" && run.findings && scoreRun(run).graded);
48962
49003
  const canStart = !!d.client && run?.status !== "watching";
48963
- const action = graded2 && run ? `<a class="cds-btn" href="${reportHref(run)}" target="_blank" rel="noopener">Open the report</a>` : canStart ? `<button class="cds-btn" type="submit" form="start-form" id="start-btn" disabled>Start the check</button>` : "";
49004
+ const action = graded2 && run ? `<a class="cds-btn" href="${reportHref(run)}" target="_blank" rel="noopener">Open the report</a>` : canStart ? `<button class="cds-btn" type="submit" form="start-form" id="start-btn-top" data-start="" disabled>Start the check</button>` : "";
48964
49005
  let body;
48965
49006
  if (!d.client) {
48966
49007
  body = pageHead("See what a new lead actually receives", `Pick a client account. ${productName()} makes a test lead with your own email address, puts it through the workflows you choose, then reads every text and email the account sent it, word for word, and tells you what a real customer would have seen wrong.`) + picker;
@@ -48986,7 +49027,7 @@ ${COCKPIT_TYPE_JS}
48986
49027
  function drawWorkflows(ws,keep){shownWorkflows=ws.map(function(w){return {id:w.id,name:w.name,status:w.status}});
48987
49028
  if(!ws.length){list.innerHTML='<p class="err">This account has no workflows to test.</p>';return}
48988
49029
  list.innerHTML=ws.map(function(w){return '<label class="cds-check"><input type="checkbox" name="wf" value="'+esc(w.id)+'"'+(keep&&keep[w.id]?' checked':'')+'><span>'+esc(w.name)+(w.status&&w.status!=="published"?' <span class="cds-chip warn">'+esc(w.status)+'</span>':'')+'</span></label>'}).join("");
48989
- var b=document.getElementById("start-btn");if(b)b.disabled=false}
49030
+ [].forEach.call(document.querySelectorAll("[data-start]"),function(b){b.disabled=false})}
48990
49031
  // The cockpit keeps what it has loaded (cockpit-cache.ts): the list says how old it is, with one Refresh; a stale
48991
49032
  // list is re-read behind the page, and a look that found nothing new never redraws it.
48992
49033
  ${AGE_LINE_JS}
@@ -49015,12 +49056,30 @@ ${COCKPIT_TYPE_JS}
49015
49056
  if(run&&run.status==="watching"&&run.due){fetch("/api/shopper/finish",{method:"POST",headers:hdrs({"Content-Type":"application/json"}),body:JSON.stringify({client:client,runId:run.runId})}).then(function(r){return r.json()}).then(function(r){if(r.ok)location.reload()})}
49016
49057
  function post(path, body){return fetch(path,{method:"POST",headers:hdrs({"Content-Type":"application/json"}),body:JSON.stringify(body)}).then(function(r){return r.json()})}
49017
49058
  var form=document.getElementById("start-form"), startConfirmed=false;
49059
+ function startBody(ids,hosts){return {client:client,workflowIds:ids,ownerEmail:document.getElementById("owner-email").value,testPhone:document.getElementById("test-phone").value||undefined,watchMinutes:(document.getElementById("watch").value==="auto"?undefined:Number(document.getElementById("watch").value)),fastForward:document.getElementById("ff").checked,allowedHosts:hosts,alertUserId:(document.getElementById("alert-to").value||undefined),workflowsShown:shownWorkflows}}
49060
+ /* Walk W39: the panel asks the start's own checks first (preview, nothing written) and never promises a start the
49061
+ server would refuse: a refusal is shown with no confirm button. */
49018
49062
  function showStartAsk(ids){
49063
+ var ask=document.getElementById("start-ask");
49064
+ var hosts=[].slice.call(form.querySelectorAll('input[name="hook"]:checked')).map(function(i){return i.value});
49065
+ ask.innerHTML='<div class="cds-callout"><span class="ttl">Checking the workflows\u2026</span>Nothing is written yet.</div>';
49066
+ var body=startBody(ids,hosts);body.preview=true;
49067
+ post("/api/shopper/start",body).then(function(r){
49068
+ if(!r||!r.ok){ask.innerHTML='<div class="cds-callout warn"><span class="ttl">This check cannot start</span>'+esc((r&&r.error)||"The workflows could not be checked.")+'</div>';if(r&&r.stale&&r.workflows){var keep={};ids.forEach(function(id){keep[id]=true});drawWorkflows(r.workflows,keep)}return}
49069
+ if(r.refusals&&r.refusals.length){ask.innerHTML='<div class="cds-callout warn"><span class="ttl">This check cannot start as it is</span><ul>'+r.refusals.map(function(x){return '<li>'+esc(x)+'</li>'}).join("")+'</ul>Nothing was written.</div>';return}
49070
+ showStartPanel(ids)})
49071
+ .catch(function(){ask.innerHTML='<div class="cds-callout warn"><span class="ttl">This check cannot start</span>${productName()} is not answering on this computer.</div>'})}
49072
+ function showStartPanel(ids){
49019
49073
  var names=ids.map(function(id){var w=(shownWorkflows||[]).filter(function(x){return x.id===id})[0];return w?w.name:id});
49020
49074
  var ff=document.getElementById("ff").checked, email=document.getElementById("owner-email").value, phone=document.getElementById("test-phone").value;
49021
49075
  var li=['A test contact named "Secret Shopper" with your email ('+esc(email)+')'+(phone?' and the phone '+esc(phone):'')+', and the tag command-os-secret-shopper. It stays in the account afterwards; delete it when you no longer need it.'];
49022
49076
  if(ff)li.push('Temporary copies of '+names.length+' workflow'+(names.length===1?'':'s')+', switched on while the check runs: '+names.map(function(n){return esc("${FF_PREFIX} "+n)}).join(", ")+' (each name also carries a short code). The test lead goes into the copies, not the originals, and the copies are deleted when the check ends.');
49023
49077
  else li.push('The test lead goes into '+names.map(esc).join(", ")+'. Those workflows really text and email you, and may alert your staff once.');
49078
+ // Walk W38: in the copies, staff alerts, tasks and assignments go to one person, marked TEST; say who, by name.
49079
+ if(ff){var at=document.getElementById("alert-to"),opt=at&&at.value?at.options[at.selectedIndex]:null,who=opt?String(opt.textContent||"").replace(/\\s*\\([^()]*@[^()]*\\)\\s*$/,""):"";
49080
+ li.push(who?'Staff alerts, tasks and assignments in the copies go to '+esc(who)+', marked TEST.':'Staff alerts, tasks and assignments in the copies go to your own login in this account, if your email matches one, marked TEST.');
49081
+ // Walk W40: the copy's safeguards, said before the click.
49082
+ li.push('The copies have no triggers, so only the test lead goes in and no real lead can enter them. The workflows themselves are not changed, and a draft stays a draft.')}
49024
49083
  var ask=document.getElementById("start-ask");
49025
49084
  ask.innerHTML='<div class="cds-callout"><span class="ttl">Before the check starts: this writes to '+esc(clientName)+'</span><ul>'+li.map(function(x){return '<li>'+x+'</li>'}).join("")+'</ul><div class="cds-actions"><button class="cds-btn" type="button" id="start-yes">Start the check in '+esc(clientName)+'</button><button class="cds-btn ghost" type="button" id="start-no">Cancel</button></div></div>';
49026
49085
  document.getElementById("start-yes").onclick=function(){startConfirmed=true;form.dispatchEvent(new Event("submit",{cancelable:true}))};
@@ -49039,15 +49098,15 @@ ${COCKPIT_TYPE_JS}
49039
49098
  // Walk 2026-10-28 W8: before anything is written, say what the check writes, in names, in this account.
49040
49099
  if(!startConfirmed){showStartAsk(ids);return}
49041
49100
  startConfirmed=false;document.getElementById("start-ask").innerHTML="";
49042
- var btn=document.getElementById("start-btn");btn.disabled=true;say("Starting\u2026");
49101
+ var btns=[].slice.call(document.querySelectorAll("[data-start]"));btns.forEach(function(b){b.disabled=true});say("Starting\u2026");
49043
49102
  var hosts=[].slice.call(form.querySelectorAll('input[name="hook"]:checked')).map(function(i){return i.value});
49044
- post("/api/shopper/start",{client:client,workflowIds:ids,ownerEmail:document.getElementById("owner-email").value,testPhone:document.getElementById("test-phone").value||undefined,watchMinutes:(document.getElementById("watch").value==="auto"?undefined:Number(document.getElementById("watch").value)),fastForward:document.getElementById("ff").checked,allowedHosts:hosts,alertUserId:(document.getElementById("alert-to").value||undefined),workflowsShown:shownWorkflows})
49103
+ post("/api/shopper/start",startBody(ids,hosts))
49045
49104
  .then(function(r){if(r.ok){location.reload();return}
49046
49105
  // Out of date: the list is drawn again as the account has it now, the ticks still there are kept, and the
49047
49106
  // refusal says what changed. Nothing started; pressing Start again sends the list now shown.
49048
49107
  if(r.stale&&r.workflows){var keep={};ids.forEach(function(id){keep[id]=true});drawWorkflows(r.workflows,keep)}
49049
- btn.disabled=false;say(r.error||"That did not start.")})
49050
- .catch(function(){btn.disabled=false;say("That did not start. Check that ${productName()} is still running.")});
49108
+ btns.forEach(function(b){b.disabled=false});say(r.error||"That did not start.")})
49109
+ .catch(function(){btns.forEach(function(b){b.disabled=false});say("That did not start. Check that ${productName()} is still running.")});
49051
49110
  })}
49052
49111
  // Fast-forward sends the test lead to the workflows' outside services for real:
49053
49112
  // read them from the chosen workflows and let the owner tick each one.
@@ -49205,6 +49264,68 @@ function websiteHost(url) {
49205
49264
  return void 0;
49206
49265
  }
49207
49266
  }
49267
+ function smallCopyPaths(file3, st) {
49268
+ let dir;
49269
+ try {
49270
+ dir = path32.join(appDataDir(), "logo-copies");
49271
+ } catch {
49272
+ return void 0;
49273
+ }
49274
+ const key2 = crypto11.createHash("sha256").update(`${path32.resolve(file3)}|${st.size}|${st.mtimeMs}`).digest("hex").slice(0, 24);
49275
+ return [{ px: 600, format: "png", mime: "image/png" }, { px: 400, format: "png", mime: "image/png" }, { px: 600, format: "jpeg", mime: "image/jpeg" }].map((t) => ({ ...t, out: path32.join(dir, `${key2}-${t.px}.${t.format === "png" ? "png" : "jpg"}`) }));
49276
+ }
49277
+ function smallLogoCopy(file3, st, maxBytes) {
49278
+ for (const t of smallCopyPaths(file3, st) ?? []) {
49279
+ try {
49280
+ const buf = fs33.readFileSync(t.out);
49281
+ if (buf.length <= maxBytes && RASTER_TYPES.some((r) => r.mime === t.mime && r.test(buf))) return { mime: t.mime, buf };
49282
+ } catch {
49283
+ }
49284
+ }
49285
+ return void 0;
49286
+ }
49287
+ function prepareSmallLogo(file3, maxBytes = LOGO_MAX_BYTES) {
49288
+ if (!file3 || process.platform !== "darwin") return Promise.resolve();
49289
+ const abs = path32.resolve(file3);
49290
+ const running = preparing.get(abs);
49291
+ if (running) return running;
49292
+ const job = (async () => {
49293
+ let st;
49294
+ try {
49295
+ st = fs33.statSync(abs);
49296
+ } catch {
49297
+ return;
49298
+ }
49299
+ if (!st.isFile() || st.size <= maxBytes || st.size > LOGO_SOURCE_MAX_BYTES) return;
49300
+ if (smallLogoCopy(abs, st, maxBytes)) return;
49301
+ const tries = smallCopyPaths(abs, st);
49302
+ if (!tries) return;
49303
+ try {
49304
+ ensureAppDataDir();
49305
+ fs33.mkdirSync(path32.dirname(tries[0].out), { recursive: true, mode: 448 });
49306
+ } catch {
49307
+ return;
49308
+ }
49309
+ for (const t of tries) {
49310
+ const tmp = `${t.out}.tmp-${crypto11.randomUUID()}.${t.format === "png" ? "png" : "jpg"}`;
49311
+ const ok = await new Promise((done) => (0, import_node_child_process3.execFile)("/usr/bin/sips", ["-Z", String(t.px), "-s", "format", t.format, ...t.format === "jpeg" ? ["-s", "formatOptions", "80"] : [], abs, "--out", tmp], { timeout: 2e4 }, (e) => done(!e)));
49312
+ try {
49313
+ if (ok && fs33.statSync(tmp).size <= maxBytes) {
49314
+ fs33.renameSync(tmp, t.out);
49315
+ return;
49316
+ }
49317
+ } catch {
49318
+ }
49319
+ try {
49320
+ fs33.rmSync(tmp, { force: true });
49321
+ } catch {
49322
+ }
49323
+ }
49324
+ notSmallEnough.add(fileKey(abs, st));
49325
+ })().finally(() => preparing.delete(abs));
49326
+ preparing.set(abs, job);
49327
+ return job;
49328
+ }
49208
49329
  function logoFileDataUri(file3, maxBytes = LOGO_MAX_BYTES) {
49209
49330
  const name2 = path32.basename(file3);
49210
49331
  let st;
@@ -49214,7 +49335,7 @@ function logoFileDataUri(file3, maxBytes = LOGO_MAX_BYTES) {
49214
49335
  return { problem: `The logo file ${name2} was not found on this computer.` };
49215
49336
  }
49216
49337
  if (!st.isFile()) return { problem: `The logo path ${name2} is not a file.` };
49217
- if (st.size > maxBytes) return { problem: `The logo file ${name2} is ${Math.ceil(st.size / 1024)} KB, over the ${Math.round(maxBytes / 1024)} KB limit for a report. A smaller export of it would work.` };
49338
+ if (st.size > LOGO_SOURCE_MAX_BYTES) return { problem: `The logo file ${name2} is ${Math.round(st.size / 1024 / 1024)} MB, more than the ${LOGO_SOURCE_MAX_BYTES / 1024 / 1024} MB a logo file can be.` };
49218
49339
  let buf;
49219
49340
  try {
49220
49341
  buf = fs33.readFileSync(file3);
@@ -49222,7 +49343,16 @@ function logoFileDataUri(file3, maxBytes = LOGO_MAX_BYTES) {
49222
49343
  return { problem: `The logo file ${name2} could not be read.` };
49223
49344
  }
49224
49345
  const raster = RASTER_TYPES.find((t) => t.test(buf));
49225
- if (raster) return { src: `data:${raster.mime};base64,${buf.toString("base64")}` };
49346
+ if (raster && buf.length <= maxBytes) return { src: `data:${raster.mime};base64,${buf.toString("base64")}` };
49347
+ if (raster) {
49348
+ const small = smallLogoCopy(file3, st, maxBytes);
49349
+ if (small) return { src: `data:${small.mime};base64,${small.buf.toString("base64")}` };
49350
+ if (process.platform !== "darwin") return { problem: `The logo file ${name2} is large, and making a small copy of it needs a Mac. The logo link from Your agency works instead.` };
49351
+ if (notSmallEnough.has(fileKey(path32.resolve(file3), st))) return { problem: `The logo file ${name2} could not be made small enough for a report. The logo link from Your agency works instead.` };
49352
+ if (!preparing.has(path32.resolve(file3))) void prepareSmallLogo(file3, maxBytes);
49353
+ return { problem: `The small copy of the logo file ${name2} for reports is being made. It shows from the next report; the logo link from Your agency works meanwhile.` };
49354
+ }
49355
+ if (st.size > maxBytes) return { problem: `The logo file ${name2} is not a PNG, JPEG, WebP or GIF image, and is too large to embed as an SVG drawing.` };
49226
49356
  const text3 = buf.toString("utf8").replace(/^\ufeff/, "");
49227
49357
  if (/<svg[\s>]/i.test(text3)) {
49228
49358
  let svg;
@@ -49359,10 +49489,12 @@ function brandChooserHtml(b, form) {
49359
49489
  const save = form.save ? `<p class="rp-soon" id="rp-choose-status" role="status" hidden></p><button type="button" class="cds-btn ghost" id="rp-choose-retry" hidden>Try again</button><script>${brandChooserSaveJs(form.save)}</script>` : "";
49360
49490
  return `<form class="rp-choose" method="get" action="${esc3(form.action)}"><span>Branding on this report</span>${hidden2}<span class="cds-seg">${opt("agency", !b.agencyAvailable)}${opt("neutral")}${opt("standard")}</span></form>${save}${notes}`;
49361
49491
  }
49362
- var fs33, path32, REPORT_BRAND_MODES, REPORT_BRAND_LABELS, LEGACY_MODES, MIN_ACCENT_CONTRAST, TEXT_BACKGROUNDS, GHL_FOOTER, LOGO_MAX_BYTES, RASTER_TYPES, LOGO_DATA_URI, REPORT_BRAND_CHOICES_ENV, LOCATION_ID, esc3, BRAND_LAYOUT_CSS, BRAND_LOGO_JS, BRAND_SAVE_UNREACHABLE;
49492
+ var crypto11, import_node_child_process3, fs33, path32, REPORT_BRAND_MODES, REPORT_BRAND_LABELS, LEGACY_MODES, MIN_ACCENT_CONTRAST, TEXT_BACKGROUNDS, GHL_FOOTER, LOGO_MAX_BYTES, LOGO_SOURCE_MAX_BYTES, preparing, notSmallEnough, fileKey, RASTER_TYPES, LOGO_DATA_URI, REPORT_BRAND_CHOICES_ENV, LOCATION_ID, esc3, BRAND_LAYOUT_CSS, BRAND_LOGO_JS, BRAND_SAVE_UNREACHABLE;
49363
49493
  var init_report_brand = __esm({
49364
49494
  "src/command-os/report-brand.ts"() {
49365
49495
  "use strict";
49496
+ crypto11 = __toESM(require("node:crypto"));
49497
+ import_node_child_process3 = require("node:child_process");
49366
49498
  fs33 = __toESM(require("node:fs"));
49367
49499
  path32 = __toESM(require("node:path"));
49368
49500
  init_agency_profile();
@@ -49379,6 +49511,10 @@ var init_report_brand = __esm({
49379
49511
  TEXT_BACKGROUNDS = ["surface", "bg", "surface-sunk"];
49380
49512
  GHL_FOOTER = "Made with GHL Command \xB7 ghlcommand.com";
49381
49513
  LOGO_MAX_BYTES = 256 * 1024;
49514
+ LOGO_SOURCE_MAX_BYTES = 20 * 1024 * 1024;
49515
+ preparing = /* @__PURE__ */ new Map();
49516
+ notSmallEnough = /* @__PURE__ */ new Set();
49517
+ fileKey = (abs, st) => `${abs}|${st.size}|${st.mtimeMs}`;
49382
49518
  RASTER_TYPES = [
49383
49519
  { mime: "image/png", test: (b) => b.length > 8 && b.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) },
49384
49520
  { mime: "image/jpeg", test: (b) => b.length > 3 && b[0] === 255 && b[1] === 216 && b[2] === 255 },
@@ -50107,7 +50243,7 @@ function planFindCard(nodes, offer, ctx) {
50107
50243
  if (tailIds.has(str12(n.id) ?? "")) continue;
50108
50244
  after.push(n);
50109
50245
  }
50110
- const at = (id2, path66, want, what, mode) => ({ nodeId: id2, path: path66, want, what, ...mode ? { mode } : {} });
50246
+ const at = (id2, path67, want, what, mode) => ({ nodeId: id2, path: path67, want, what, ...mode ? { mode } : {} });
50111
50247
  const checks = [
50112
50248
  at(findId, ["type"], "find_opportunity", "the new Find opportunity step"),
50113
50249
  at(findId, ["attributes", "__customInputFields__", "[filterField=pipeline_id]", "secondValue"], pipelineId, "the pipeline the Find opportunity step looks in"),
@@ -50206,9 +50342,9 @@ function stored(node) {
50206
50342
  a = normalizeInternalCreateOpportunityAction(a);
50207
50343
  return asObj10(a);
50208
50344
  }
50209
- function pick2(node, path66) {
50345
+ function pick2(node, path67) {
50210
50346
  let cur = node;
50211
- for (const seg of path66) {
50347
+ for (const seg of path67) {
50212
50348
  const sel = /^\[(\w+)=(.*)\]$/.exec(seg);
50213
50349
  if (sel) {
50214
50350
  cur = asArr6(cur).map(asObj10).find((x) => String(x[sel[1]]) === sel[2]);
@@ -50751,6 +50887,20 @@ async function accountUsers(client, locationId2) {
50751
50887
  email: str15(u.email)
50752
50888
  }));
50753
50889
  }
50890
+ async function fastForwardRefusals(s) {
50891
+ if (s.workflowIds.length > 5) return ["A fast-forward check copies at most 5 workflows."];
50892
+ let owner;
50893
+ if (s.alertUserId) {
50894
+ if (!(await accountUsers(s.client, s.locationId)).some((u) => u.id === s.alertUserId)) return ["The person chosen to receive staff alerts is not a user of this account any more. Pick someone from the list again."];
50895
+ owner = s.alertUserId;
50896
+ } else owner = await ownerUserId(s.client, s.locationId, s.ownerEmail);
50897
+ const out = [];
50898
+ for (const id2 of s.workflowIds) {
50899
+ const r = planCopy(toSource(id2, await s.admin.get(id2)), { ownerUserId: owner, allowedHosts: s.allowedHosts });
50900
+ if (!r.ok) out.push(r.refusal);
50901
+ }
50902
+ return out;
50903
+ }
50754
50904
  async function sweepLeftovers(admin, locationId2, base, now) {
50755
50905
  const keep = listRuns(locationId2, base).filter((r) => r.status === "watching").flatMap((r) => r.fastForward?.copies.map((c) => c.copyId) ?? []);
50756
50906
  return sweep(admin, locationId2, base, now, keep);
@@ -51804,7 +51954,7 @@ async function readVerifyAccount(saved, client, builderSource = () => workflowRe
51804
51954
  } };
51805
51955
  const plannedTags = new Set(planned.filter((p) => p.type === "tag").map((p) => p.name.trim().toLowerCase()));
51806
51956
  const meantLive = meantLiveRefs(saved);
51807
- const held = heldDraftGates(saved);
51957
+ const held2 = heldDraftGates(saved);
51808
51958
  await Promise.allSettled(records.filter((r) => r.type === "workflow" && r.id).map(async (record) => {
51809
51959
  try {
51810
51960
  const full = await builder.getWorkflow(record.id);
@@ -51839,8 +51989,8 @@ async function readVerifyAccount(saved, client, builderSource = () => workflowRe
51839
51989
  const draft = full.status !== "published";
51840
51990
  if (errors.length || goneTags.length || draft && full.status !== "draft") record.status = "invalid";
51841
51991
  else if (unreadable.length) record.status = "unreadable";
51842
- else if (full.status === "draft") record.status = held.has(record.ref) || meantLive.has(record.ref) ? "invalid" : "pending";
51843
- const heldGates = full.status === "draft" && !errors.length && !goneTags.length && !unreadable.length ? held.get(record.ref) : void 0;
51992
+ else if (full.status === "draft") record.status = held2.has(record.ref) || meantLive.has(record.ref) ? "invalid" : "pending";
51993
+ const heldGates = full.status === "draft" && !errors.length && !goneTags.length && !unreadable.length ? held2.get(record.ref) : void 0;
51844
51994
  if (heldGates) record.held = true;
51845
51995
  record.detail = heldGates ? heldDraftDetail(heldGates) : [record.status === "pending" ? "This workflow is built and waiting for you to switch it on in GoHighLevel under Automation > Workflows." : draft && full.status === "draft" && meantLive.has(record.ref) && !errors.length ? SHOULD_BE_LIVE : draft ? `Workflow status is ${full.status}.` : "", ...errors.map((f) => f.message), ...goneTags.map((t) => `It uses the tag "${t}", which is no longer in the account. Run Build again to put it back.`), ...unreadable.map((f) => `${f.message} ${lists.get(f.category)?.error || ""}`)].filter(Boolean).join(" ");
51846
51996
  const triggers = Array.isArray(full.triggers) ? full.triggers : [];
@@ -52589,8 +52739,8 @@ function executablePlanLines(saved) {
52589
52739
  const refs3 = [...names.keys()].sort((a, b) => b.length - a.length);
52590
52740
  const refPattern = refs3.length ? new RegExp(`(?<![\\w./:@])(?:${refs3.map(escapeRE).join("|")})(?![\\w]|\\.[\\w])`, "g") : void 0;
52591
52741
  const missingRefPattern = /(?<![\w./:@])(user|pipeline|stage|field|tag|workflow|form|calendar|email_template|sms_template|email|sms|cv|handoff|funnel)\.[a-z0-9_]+(?![\w]|\.[\w])/g;
52592
- const resolve14 = (s) => s.replace(missingRefPattern, (ref) => names.get(ref) || `Unnamed ${kindOf2(ref.split(".")[0])}`);
52593
- const renderRefs = (s) => resolve14(refPattern ? s.replace(refPattern, (ref) => names.get(ref)) : s);
52742
+ const resolve15 = (s) => s.replace(missingRefPattern, (ref) => names.get(ref) || `Unnamed ${kindOf2(ref.split(".")[0])}`);
52743
+ const renderRefs = (s) => resolve15(refPattern ? s.replace(refPattern, (ref) => names.get(ref)) : s);
52594
52744
  const mergeLabel = (field2) => {
52595
52745
  const [group, ...parts] = field2.trim().split(".");
52596
52746
  const key2 = parts.join(".");
@@ -52903,7 +53053,7 @@ function compute(ans, skip, asm) {
52903
53053
  function() {
52904
53054
  return raw("calls_missed") * share("missed_never_return") * c * v;
52905
53055
  },
52906
- "Call answering, day and night"
53056
+ "Call answering"
52907
53057
  );
52908
53058
  add2(
52909
53059
  "waited",
@@ -53586,7 +53736,12 @@ function programBuiltLabel(name2) {
53586
53736
  }
53587
53737
  return `Built: Program follow-up${win ? ` (texts over the ${win} after the visit)` : ""}`;
53588
53738
  }
53589
- function boughtItemsFor(offered, wording = {}) {
53739
+ function statusLabel(label2, feature, status) {
53740
+ const what = label2.replace(/^Built: /, "");
53741
+ if (!status?.made) return `Will build: ${what}`;
53742
+ return status.made.has(feature) ? `Built: ${what}` : `Not built yet: ${what}`;
53743
+ }
53744
+ function boughtItemsFor(offered, wording = {}, status) {
53590
53745
  return ASSESSMENT_FEATURES.map((f) => {
53591
53746
  const id2 = f.id;
53592
53747
  const base = { id: id2, name: f.name, means: BOUGHT_MEANS[id2] };
@@ -53596,11 +53751,12 @@ function boughtItemsFor(offered, wording = {}) {
53596
53751
  if (!offered.includes(feature)) {
53597
53752
  return { ...base, kind: "not-in-plan", feature, ...next, delivery: `Not built for this client: ${FEATURES[feature].name.toLowerCase()} is not in the plan for this industry.` };
53598
53753
  }
53599
- const delivery = id2 === "quotes" && wording.programFollowUp ? programBuiltLabel(wording.programFollowUp) : BUILT_LABEL[id2];
53754
+ const label2 = id2 === "quotes" && wording.programFollowUp ? programBuiltLabel(wording.programFollowUp) : BUILT_LABEL[id2];
53755
+ const delivery = status?.held?.[id2] ?? statusLabel(label2, feature, status);
53600
53756
  return { ...base, kind: "built", feature, ...next, delivery };
53601
53757
  }
53602
53758
  const other = OTHER_DELIVERY[id2];
53603
- return { ...base, kind: other.kind, delivery: other.text };
53759
+ return { ...base, kind: other.kind, delivery: status?.held?.[id2] ?? other.text };
53604
53760
  });
53605
53761
  }
53606
53762
  function featuresFromBought(ticks, offered) {
@@ -53646,13 +53802,19 @@ function assessmentGateRefusal(state2, rk, clientKey, name2) {
53646
53802
  function approvalBindingHash(scopeHash2, state2, rk) {
53647
53803
  const saved = state2.bought?.[rk];
53648
53804
  const answer2 = assessmentAnswerOf(state2, rk);
53649
- if (!Array.isArray(saved) && !answer2) return scopeHash2;
53805
+ const asks = asksBound(state2, rk);
53806
+ if (!Array.isArray(saved) && !answer2 && !asks) return scopeHash2;
53650
53807
  const snapshot = state2.assessmentAttachments?.[rk];
53651
- return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, assessment: answer2, attached: snapshot ? contentHash2(snapshot) : null });
53808
+ return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, assessment: answer2, attached: snapshot ? contentHash2(snapshot) : null, ...asks ? { asks } : {} });
53652
53809
  }
53653
53810
  function boughtBindingHash(scopeHash2, state2, rk) {
53654
53811
  const saved = state2.bought?.[rk];
53655
- return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null });
53812
+ const asks = asksBound(state2, rk);
53813
+ return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, ...asks ? { asks } : {} });
53814
+ }
53815
+ function asksBound(state2, rk) {
53816
+ const a = state2.asks?.[rk];
53817
+ return a && Object.keys(a).length ? a : void 0;
53656
53818
  }
53657
53819
  function intakeChannelAnswers(answers) {
53658
53820
  const get2 = (key2, label2) => {
@@ -53672,6 +53834,12 @@ function boughtJobs(ticks, offered, channels = {}) {
53672
53834
  reason: item.kind === "not-in-plan" ? "Bought, but not built automatically yet. It is not in the plan for this client's industry." : `Bought, but not built automatically yet. ${OTHER_DELIVERY[item.id].job}`
53673
53835
  }));
53674
53836
  }
53837
+ function heldJobs(held2) {
53838
+ return held2.map((h) => {
53839
+ const at = h.line.indexOf(": ");
53840
+ return { ref: `bought:held-${h.id}`, type: BOUGHT_JOB_TYPE, name: h.line.slice(0, at), reason: h.line.slice(at + 2) };
53841
+ });
53842
+ }
53675
53843
  function boughtButExcludedJobs(ticks, offered, scopeFeatures) {
53676
53844
  if (scopeFeatures === "all") return [];
53677
53845
  return boughtItemsFor(offered).filter((item) => ticks.includes(item.id) && item.kind === "built" && item.feature && !scopeFeatures.includes(item.feature)).map((item) => ({
@@ -53694,10 +53862,10 @@ function assessmentChoiceLine(answer2, attached = answer2 === "attached") {
53694
53862
  if (answer2 === "run-first") return "The Assessment will be run first.";
53695
53863
  return "Not answered yet: has the Assessment been run for this client?";
53696
53864
  }
53697
- function boughtViewFor(state2, rk, offered, wording = {}) {
53865
+ function boughtViewFor(state2, rk, offered, wording = {}, status) {
53698
53866
  const { ticks, source } = ticksFor(state2, rk);
53699
53867
  return {
53700
- items: boughtItemsFor(offered, wording),
53868
+ items: boughtItemsFor(offered, wording, status),
53701
53869
  ticks,
53702
53870
  source,
53703
53871
  answer: assessmentAnswerOf(state2, rk),
@@ -53706,17 +53874,20 @@ function boughtViewFor(state2, rk, offered, wording = {}) {
53706
53874
  suggestions: source === "saved" ? claudeSuggestions(ticks) : []
53707
53875
  };
53708
53876
  }
53709
- function boughtDocumentFor(state2, rk, offered, wording = {}) {
53877
+ function boughtDocumentFor(state2, rk, offered, wording = {}, status) {
53710
53878
  const { ticks, source } = ticksFor(state2, rk);
53711
- const items = boughtItemsFor(offered, wording);
53879
+ const items = boughtItemsFor(offered, wording, status);
53712
53880
  const confirmed = source === "saved" || source === "scope";
53713
53881
  const ticked = items.filter((i) => ticks.includes(i.id));
53714
53882
  return {
53715
53883
  confirmed,
53716
53884
  assessment: assessmentChoiceLine(assessmentAnswerOf(state2, rk), Boolean(state2.assessmentAttachments?.[rk])),
53717
- bought: ticked.map((i) => ({ name: i.name, means: i.means, delivery: clientDelivery(i) })),
53885
+ // "No call goes unanswered" is a promise only an AI answering live keeps, and this version builds none: the client
53886
+ // reads what the build did for calls (promise-vs-build F4 wording, 2026-10-29).
53887
+ bought: ticked.map((i) => ({ name: i.name, means: i.id === "missed" ? status?.made?.has("missed_call") ? "Missed calls get a text back" : "How calls are answered" : i.means, delivery: clientDelivery(i) })),
53718
53888
  // The client reads this: never the agency's job text, and never "with Claude" (review of ea3526f).
53719
53889
  stillToDo: ticked.flatMap((i) => {
53890
+ if (HELD_LINE.test(i.delivery)) return [`${i.name}: ${clientDelivery(i).toLowerCase()}.`];
53720
53891
  if (i.kind === "go-live") return [`${i.name}: being set up in your account.`];
53721
53892
  if (i.kind === "separate") return [`${i.name}: the form is ready. The pages are a separate piece of work.`];
53722
53893
  if (i.kind === "not-in-plan") return [`${i.name}: being set up separately from this build.`];
@@ -53726,13 +53897,15 @@ function boughtDocumentFor(state2, rk, offered, wording = {}) {
53726
53897
  };
53727
53898
  }
53728
53899
  function clientDelivery(i) {
53900
+ const held2 = HELD_LINE.exec(i.delivery);
53901
+ if (held2) return held2[1] === "Waiting for your client's answer" ? "Waiting on your answer" : held2[1] === "Not built, your client said not yet" ? "Not set up, as you said not yet" : "Chosen, and coming in a later update";
53729
53902
  if (i.kind === "go-live") return "Being set up in your account";
53730
53903
  if (i.kind === "separate") return "The form is built; the pages are separate work";
53731
53904
  if (i.kind === "not-in-plan") return "Not part of this build";
53732
53905
  if (i.kind === "claude") return "A weekly report on what is working";
53733
53906
  return i.delivery;
53734
53907
  }
53735
- var BOUGHT_IDS, BOUGHT_MEANS, FEATURE_FOR_BOUGHT, BUILT_LABEL, OTHER_DELIVERY, CALL_ANSWERING_NEXT, ASSESSMENT_CHOICES, ANSWER_THE_ASSESSMENT_QUESTION, ATTACH_THE_ASSESSMENT, RUN_THE_ASSESSMENT_FIRST, BOUGHT_JOB_TYPE;
53908
+ var BOUGHT_IDS, BOUGHT_MEANS, FEATURE_FOR_BOUGHT, BUILT_LABEL, OTHER_DELIVERY, CALL_ANSWERING_NEXT, ASSESSMENT_CHOICES, ANSWER_THE_ASSESSMENT_QUESTION, ATTACH_THE_ASSESSMENT, RUN_THE_ASSESSMENT_FIRST, BOUGHT_JOB_TYPE, HELD_LINE;
53736
53909
  var init_bought_list = __esm({
53737
53910
  "src/bought-list.ts"() {
53738
53911
  "use strict";
@@ -53741,11 +53914,13 @@ var init_bought_list = __esm({
53741
53914
  init_build_contract();
53742
53915
  BOUGHT_IDS = ASSESSMENT_FEATURES.map((f) => f.id);
53743
53916
  BOUGHT_MEANS = {
53744
- missed: "No call goes unanswered",
53917
+ // rc.1 MED-1: true whatever the client chooses; nothing answers live until a voice agent is built.
53918
+ missed: "Callers get the answer your client chooses",
53745
53919
  waited: "Every new lead hears back in seconds",
53746
53920
  quotes: "No quote goes cold",
53747
53921
  noshow: "Bookings confirmed, reminded and rescued",
53748
- reactivation: "Old leads and past clients brought back",
53922
+ // rc.1 MED-2: what Win-back reaches (leads who go cold from now on); an old list only once a campaign for it is built.
53923
+ reactivation: "Leads who go quiet are brought back",
53749
53924
  nurture: "Leads kept warm until they are ready",
53750
53925
  recall: "Clients come back on schedule",
53751
53926
  deposits: "Fewer no-shows, cash up front",
@@ -53783,6 +53958,7 @@ var init_bought_list = __esm({
53783
53958
  ATTACH_THE_ASSESSMENT = "Attach the Assessment in step 4, or choose Build without an Assessment there.";
53784
53959
  RUN_THE_ASSESSMENT_FIRST = "You chose to run the Assessment first. Attach it in step 4 when it is done, or choose Build without an Assessment there.";
53785
53960
  BOUGHT_JOB_TYPE = "bought-not-built";
53961
+ HELD_LINE = /^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
53786
53962
  }
53787
53963
  });
53788
53964
 
@@ -53840,6 +54016,23 @@ var init_offer_normalize = __esm({
53840
54016
  }
53841
54017
  });
53842
54018
 
54019
+ // src/intake-to-build/texting.ts
54020
+ function textingAnswer(brief) {
54021
+ const channels = brief && typeof brief === "object" ? brief.channels : void 0;
54022
+ const sms = channels && typeof channels === "object" ? channels.sms : void 0;
54023
+ if (sms === true) return "yes";
54024
+ if (sms === false) return "not_yet";
54025
+ return "unanswered";
54026
+ }
54027
+ function smsOn(brief) {
54028
+ return textingAnswer(brief) === "yes";
54029
+ }
54030
+ var init_texting = __esm({
54031
+ "src/intake-to-build/texting.ts"() {
54032
+ "use strict";
54033
+ }
54034
+ });
54035
+
53843
54036
  // src/intake-to-build/brief.ts
53844
54037
  function norm4(s) {
53845
54038
  return s.trim().toLowerCase();
@@ -54598,9 +54791,9 @@ function distillProof(brief) {
54598
54791
  if (quote2) proof.testimonials = [{ text: quote2, consent }];
54599
54792
  if (Object.keys(proof).length === 0) delete brief.proof;
54600
54793
  }
54601
- function setPath(target, path66, value) {
54794
+ function setPath(target, path67, value) {
54602
54795
  if (value === void 0) return;
54603
- const parts = path66.split(".");
54796
+ const parts = path67.split(".");
54604
54797
  let node = target;
54605
54798
  for (let i = 0; i < parts.length - 1; i++) {
54606
54799
  const k = parts[i];
@@ -54614,7 +54807,7 @@ function deriveFlags(brief) {
54614
54807
  const channels = brief.channels ?? {};
54615
54808
  const goal = brief.goal ?? {};
54616
54809
  const a2p = typeof channels.a2pStatus === "string" ? channels.a2pStatus.toLowerCase() : "";
54617
- if (channels.sms !== false && a2p !== "approved" && a2p !== "not needed") {
54810
+ if (smsOn(brief) && a2p !== "approved" && a2p !== "not needed") {
54618
54811
  flags.push("needs_a2p");
54619
54812
  }
54620
54813
  if (channels.payment === "Stripe not connected" || channels.payment === void 0) flags.push("stripe_not_connected");
@@ -54622,7 +54815,7 @@ function deriveFlags(brief) {
54622
54815
  flags.push("calendar_oauth_needed");
54623
54816
  }
54624
54817
  if (channels.email === false) flags.push("email_not_ready");
54625
- if (channels.sms !== false && channels.hasPhoneNumber !== "yes") {
54818
+ if (smsOn(brief) && channels.hasPhoneNumber !== "yes") {
54626
54819
  flags.push("phone_number_needed");
54627
54820
  }
54628
54821
  return flags;
@@ -54793,6 +54986,7 @@ var init_normalizer = __esm({
54793
54986
  "src/intake-to-build/normalizer.ts"() {
54794
54987
  "use strict";
54795
54988
  init_offer_normalize();
54989
+ init_texting();
54796
54990
  init_question_set();
54797
54991
  init_brief2();
54798
54992
  EMAIL_RE3 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -54893,10 +55087,10 @@ function livePending(pending, nowMs) {
54893
55087
  return out;
54894
55088
  }
54895
55089
  function applyStaffPending(check, pending, nowMs) {
54896
- const held = livePending(pending, nowMs);
54897
- const waiting = check.missing.filter((m) => held[normEmail(m.email)]);
55090
+ const held2 = livePending(pending, nowMs);
55091
+ const waiting = check.missing.filter((m) => held2[normEmail(m.email)]);
54898
55092
  if (!waiting.length) return check;
54899
- const isHeld = (email) => Boolean(held[normEmail(email)]);
55093
+ const isHeld = (email) => Boolean(held2[normEmail(email)]);
54900
55094
  const lines = new Map(waiting.map((m) => [normEmail(m.email), staffWaitingLine(m.name)]));
54901
55095
  return {
54902
55096
  ...check,
@@ -55373,7 +55567,8 @@ function context(src) {
55373
55567
  const notify2 = clean3(src.roster?.notifyName) || intakeAnswer2(src.intake, "notify_name") || void 0;
55374
55568
  const calls = clean3(src.roster?.callsName) || intakeAnswer2(src.intake, "calls_name") || void 0;
55375
55569
  const notifyShown = notify2 ? teamNames(src).find((n) => sameName(n, notify2)) : void 0;
55376
- return { features, live: workflowsConfirmed(plan, src.lines), calendars, notify: notify2, ...notifyShown && { notifyShown }, calls, items, ticked: confirmedTicks ? ticks : [], confirmedTicks, programFollowUp: isProgramFollowUp(plan), review: reviewTimingOf(plan), noShowRescue: hasWorkflow(plan, "workflow.no_show"), reply: replyChannels(plan) };
55570
+ const callAnswering = src.recordKey && src.boughtState?.asks?.[src.recordKey]?.callAnswering || [];
55571
+ return { features, live: workflowsConfirmed(plan, src.lines), calendars, notify: notify2, ...notifyShown && { notifyShown }, calls, items, ticked: confirmedTicks ? ticks : [], callAnswering, confirmedTicks, programFollowUp: isProgramFollowUp(plan), review: reviewTimingOf(plan), noShowRescue: hasWorkflow(plan, "workflow.no_show"), reply: replyChannels(plan) };
55377
55572
  }
55378
55573
  function printableCalendars(ctx) {
55379
55574
  return [...new Set(ctx.calendars.map((c) => c.name))].filter((n) => n !== "booking calendar");
@@ -55435,7 +55630,9 @@ function summaryOf2(ctx, plan) {
55435
55630
  }
55436
55631
  const extras = [
55437
55632
  f("missed_call") ? "missed calls get a text back" : "",
55438
- f("winback") ? "old leads get a win-back message" : "",
55633
+ // Win-back starts from a tag Lead Nurture or No-Show Rescue adds, so it reaches leads who go cold from now on,
55634
+ // not an old list (promise-vs-build F5, 2026-10-29).
55635
+ f("winback") ? "leads who go cold get a win-back message" : "",
55439
55636
  f("quote_followup") ? ctx.programFollowUp ? PROGRAM_WORDS.quotes.summary : "every quote is followed up" : "",
55440
55637
  f("review_requests") ? "happy clients are asked for a review" : ""
55441
55638
  ].filter(Boolean);
@@ -55552,7 +55749,9 @@ function boughtOf(ctx) {
55552
55749
  if (built) working.push(boughtLabel(item, true, ctx));
55553
55750
  else if (item.kind !== "built") next.push(boughtLabel(item, false, ctx));
55554
55751
  else next.push(item.name);
55555
- if (item.id === "missed" && built && ctx.ticked.includes("missed")) next.push("Live call answering");
55752
+ if (item.id === "missed" && ctx.ticked.includes("missed")) {
55753
+ for (const c of ctx.callAnswering) if (c !== "missed_text") next.push(c === "live_voice" ? "An AI that answers the phone live (chosen, coming in a later update)" : "An AI that replies by text or chat and books (chosen, coming in a later update)");
55754
+ }
55556
55755
  }
55557
55756
  return { workingLabel: ctx.live ? "Working now" : "Built, ready to switch on", working, next };
55558
55757
  }
@@ -56554,10 +56753,10 @@ var init_build_obligations = __esm({
56554
56753
  });
56555
56754
 
56556
56755
  // src/command-os/cockpit-routes.ts
56557
- function routeOf(method, path66) {
56756
+ function routeOf(method, path67) {
56558
56757
  const m = method === "HEAD" ? "GET" : method;
56559
56758
  const fits = (r) => r.method === "ANY" || r.method === m;
56560
- return COCKPIT_ROUTES.find((r) => r.match === "exact" && r.path === path66 && fits(r)) ?? COCKPIT_ROUTES.find((r) => r.match === "pattern" && r.pattern.test(path66) && fits(r)) ?? COCKPIT_ROUTES.find((r) => r.match === "prefix" && path66.startsWith(r.path) && path66.length > r.path.length && fits(r));
56759
+ return COCKPIT_ROUTES.find((r) => r.match === "exact" && r.path === path67 && fits(r)) ?? COCKPIT_ROUTES.find((r) => r.match === "pattern" && r.pattern.test(path67) && fits(r)) ?? COCKPIT_ROUTES.find((r) => r.match === "prefix" && path67.startsWith(r.path) && path67.length > r.path.length && fits(r));
56561
56760
  }
56562
56761
  function routeAllows(route, plan) {
56563
56762
  return route.plans.includes(plan ?? "");
@@ -56574,10 +56773,10 @@ var init_cockpit_routes = __esm({
56574
56773
  OS = ["command-os"];
56575
56774
  BOTH = ["pro", "command-os"];
56576
56775
  PRO_ONLY = ["pro"];
56577
- page = (path66, scope, plans, match = "exact", pattern) => ({ method: "GET", path: path66, match, pattern, scope, plans, write: false, kind: "page" });
56578
- get = (path66, scope, plans) => ({ method: "GET", path: path66, match: "exact", scope, plans, write: false, kind: "api" });
56579
- post = (path66, scope, plans) => ({ method: "POST", path: path66, match: "exact", scope, plans, write: true, kind: "api" });
56580
- any = (path66, scope, plans, write, match = "exact", kind = "api") => ({ method: "ANY", path: path66, match, scope, plans, write, kind });
56776
+ page = (path67, scope, plans, match = "exact", pattern) => ({ method: "GET", path: path67, match, pattern, scope, plans, write: false, kind: "page" });
56777
+ get = (path67, scope, plans) => ({ method: "GET", path: path67, match: "exact", scope, plans, write: false, kind: "api" });
56778
+ post = (path67, scope, plans) => ({ method: "POST", path: path67, match: "exact", scope, plans, write: true, kind: "api" });
56779
+ any = (path67, scope, plans, write, match = "exact", kind = "api") => ({ method: "ANY", path: path67, match, scope, plans, write, kind });
56581
56780
  none = (r) => ({ ...r, client: { none: true } });
56582
56781
  via = (r, client) => ({ ...r, client });
56583
56782
  COCKPIT_ROUTES = [
@@ -56872,22 +57071,22 @@ function helperPresence(opts) {
56872
57071
  }
56873
57072
  };
56874
57073
  }
56875
- var import_node_child_process3, import_node_crypto25, fs39, path38, import_zod79, execHelper, HelperOut, PinSchema, SIGN_TIMEOUT_MS, READ_TIMEOUT_MS, DIGEST_HEX;
57074
+ var import_node_child_process4, import_node_crypto25, fs39, path38, import_zod79, execHelper, HelperOut, PinSchema, SIGN_TIMEOUT_MS, READ_TIMEOUT_MS, DIGEST_HEX;
56876
57075
  var init_presence = __esm({
56877
57076
  "src/command-os/brain/presence.ts"() {
56878
57077
  "use strict";
56879
- import_node_child_process3 = require("node:child_process");
57078
+ import_node_child_process4 = require("node:child_process");
56880
57079
  import_node_crypto25 = require("node:crypto");
56881
57080
  fs39 = __toESM(require("node:fs"));
56882
57081
  path38 = __toESM(require("node:path"));
56883
57082
  import_zod79 = require("zod");
56884
57083
  init_shapes2();
56885
- execHelper = (file3, args, timeoutMs) => new Promise((resolve14) => {
56886
- (0, import_node_child_process3.execFile)(file3, args, { timeout: timeoutMs, maxBuffer: 64 * 1024, shell: false, windowsHide: true, encoding: "utf8" }, (err, stdout) => {
57084
+ execHelper = (file3, args, timeoutMs) => new Promise((resolve15) => {
57085
+ (0, import_node_child_process4.execFile)(file3, args, { timeout: timeoutMs, maxBuffer: 64 * 1024, shell: false, windowsHide: true, encoding: "utf8" }, (err, stdout) => {
56887
57086
  const code = err && "code" in err ? err.code : null;
56888
- if (code === "ENOENT" || code === "EACCES") return resolve14({ code: null, stdout: "", missing: true });
56889
- if (err && "killed" in err && err.killed) return resolve14({ code: null, stdout: "", timedOut: true });
56890
- resolve14({ code: err ? typeof code === "number" ? code : null : 0, stdout: typeof stdout === "string" ? stdout : "" });
57087
+ if (code === "ENOENT" || code === "EACCES") return resolve15({ code: null, stdout: "", missing: true });
57088
+ if (err && "killed" in err && err.killed) return resolve15({ code: null, stdout: "", timedOut: true });
57089
+ resolve15({ code: err ? typeof code === "number" ? code : null : 0, stdout: typeof stdout === "string" ? stdout : "" });
56891
57090
  });
56892
57091
  });
56893
57092
  HelperOut = import_zod79.z.object({
@@ -57387,10 +57586,10 @@ async function leadLabels(deps, locationId2, contactIds) {
57387
57586
  await Promise.all(unique.map(async (id2) => {
57388
57587
  const stop3 = new AbortController();
57389
57588
  let timer;
57390
- const late = new Promise((resolve14) => {
57589
+ const late = new Promise((resolve15) => {
57391
57590
  timer = setTimeout(() => {
57392
57591
  stop3.abort(new Error("lead lookup timed out"));
57393
- resolve14(null);
57592
+ resolve15(null);
57394
57593
  }, LEAD_LOOKUP_MS);
57395
57594
  });
57396
57595
  try {
@@ -58819,8 +59018,8 @@ async function checkLead(deps, ghl, store2, args) {
58819
59018
  const check = runPrecheck(args.text, precheckContext(briefNow, client, glossary, rules, optOutWhy({ brainOptedOut: b.brief.optedOut, contact, messages }), now));
58820
59019
  const failed = check.checks.filter((c) => !c.pass);
58821
59020
  if (failed.length) return stop("checks", `A check fails now: ${failed.map((c) => `${c.check} (${c.why ?? ""})`).join("; ")}`);
58822
- const held = await gateAgain(ghl, store2, { client, d, text: args.text, contactId, now, brief: briefNow, glossary, firstName: contact.firstName ?? null, ...live ? { live } : {} });
58823
- if (held) return held;
59021
+ const held2 = await gateAgain(ghl, store2, { client, d, text: args.text, contactId, now, brief: briefNow, glossary, firstName: contact.firstName ?? null, ...live ? { live } : {} });
59022
+ if (held2) return held2;
58824
59023
  return { channel, day: today2.date, messages, sentToday: sentToday2, contact };
58825
59024
  }
58826
59025
  async function gateAgain(ghl, store2, x) {
@@ -58895,15 +59094,15 @@ async function checkTextNow(deps, store2, x) {
58895
59094
  const check = runPrecheck(x.text, precheckContext(b.brief, x.client, glossary, rules, optOutWhy({ brainOptedOut: b.brief.optedOut, contact }), x.now));
58896
59095
  const found = findingsOf(check.checks.filter((c) => !c.pass)).map((f) => `${checkLabel(f.check)}: ${f.found}.`);
58897
59096
  if (found.length) return { ok: true, found };
58898
- const held = await gateAgain(ghl, store2, { client: x.client, d: x.d, text: x.text, contactId, now: x.now, brief: b.brief, glossary, firstName: contact.firstName ?? null });
58899
- if (held && !held.ok && held.sent === false) return { ok: true, found: [held.why.replace(/^Not sent: /, "")] };
59097
+ const held2 = await gateAgain(ghl, store2, { client: x.client, d: x.d, text: x.text, contactId, now: x.now, brief: b.brief, glossary, firstName: contact.firstName ?? null });
59098
+ if (held2 && !held2.ok && held2.sent === false) return { ok: true, found: [held2.why.replace(/^Not sent: /, "")] };
58900
59099
  return { ok: true, found: [] };
58901
59100
  }
58902
59101
  function composeBody(channel, contactId, text3, businessName, emailTo) {
58903
59102
  return channel === "SMS" ? { type: "SMS", contactId, message: text3 } : { type: "Email", contactId, subject: `Re: your message to ${businessName}`, html: `<p style="margin:0px;">${escHtml2(text3)}</p>`, ...emailTo ? { emailTo } : {} };
58904
59103
  }
58905
- function productionPath(deps, path66) {
58906
- return path66 === "cockpit" && deps.liveSendEnabled?.() === true && deps.outOfBandReady?.() === true;
59104
+ function productionPath(deps, path67) {
59105
+ return path67 === "cockpit" && deps.liveSendEnabled?.() === true && deps.outOfBandReady?.() === true;
58907
59106
  }
58908
59107
  function sentNote(x) {
58909
59108
  const who2 = x.path === "sandbox" ? "from the sandbox send command" : `approved by ${x.approvedBy ?? "the owner"} in the Command OS cockpit`;
@@ -59932,7 +60131,7 @@ function json2(res, status, body) {
59932
60131
  res.end(JSON.stringify(body));
59933
60132
  }
59934
60133
  function readJson(req, limit) {
59935
- return new Promise((resolve14, reject) => {
60134
+ return new Promise((resolve15, reject) => {
59936
60135
  let size2 = 0;
59937
60136
  const chunks = [];
59938
60137
  req.on("data", (c) => {
@@ -59944,7 +60143,7 @@ function readJson(req, limit) {
59944
60143
  });
59945
60144
  req.on("end", () => {
59946
60145
  try {
59947
- resolve14(JSON.parse(Buffer.concat(chunks).toString("utf8")));
60146
+ resolve15(JSON.parse(Buffer.concat(chunks).toString("utf8")));
59948
60147
  } catch {
59949
60148
  reject(new Error("not json"));
59950
60149
  }
@@ -59955,24 +60154,24 @@ function readJson(req, limit) {
59955
60154
  function createBrainRoutes(opts) {
59956
60155
  return (req, res) => {
59957
60156
  const u = new URL(req.url ?? "/", "http://127.0.0.1");
59958
- const path66 = u.pathname;
59959
- if (path66 !== "/brain" && path66 !== "/brain/handoff" && path66 !== MASTER_PAGE_PATH && !path66.startsWith("/api/brain/")) return false;
60157
+ const path67 = u.pathname;
60158
+ if (path67 !== "/brain" && path67 !== "/brain/handoff" && path67 !== MASTER_PAGE_PATH && !path67.startsWith("/api/brain/")) return false;
59960
60159
  if (opts.plan !== "command-os") {
59961
60160
  res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" });
59962
60161
  res.end(opts.upgradeMessage.trim());
59963
60162
  return true;
59964
60163
  }
59965
60164
  const onBoard = (id2) => opts.accounts().find((a) => a.locationId === id2);
59966
- if (req.method === "GET" && path66 === "/brain/handoff") {
60165
+ if (req.method === "GET" && path67 === "/brain/handoff") {
59967
60166
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
59968
60167
  res.end(brainHandoffPageHtml(opts.seatFor(req), opts.seatHeader));
59969
60168
  return true;
59970
60169
  }
59971
- if (req.method === "GET" && path66 === "/api/brain/handoff") {
60170
+ if (req.method === "GET" && path67 === "/api/brain/handoff") {
59972
60171
  void handoffView(opts.handoffDeps()).then((v) => json2(res, 200, v)).catch(() => json2(res, 200, { ok: false, error: "Your brain could not be read. Check your internet connection and the Brain card in Connections, then Refresh." }));
59973
60172
  return true;
59974
60173
  }
59975
- if (req.method === "POST" && path66 === "/api/brain/export") {
60174
+ if (req.method === "POST" && path67 === "/api/brain/export") {
59976
60175
  void exportBundle(opts.handoffDeps()).then((r) => {
59977
60176
  if (!r.ok) {
59978
60177
  json2(res, 200, r);
@@ -59985,7 +60184,7 @@ function createBrainRoutes(opts) {
59985
60184
  });
59986
60185
  return true;
59987
60186
  }
59988
- if (req.method === "POST" && path66 === "/api/brain/pause") {
60187
+ if (req.method === "POST" && path67 === "/api/brain/pause") {
59989
60188
  void readJson(req, 2e3).then(async (body) => {
59990
60189
  const parsed2 = PauseRequest.safeParse(body);
59991
60190
  if (!parsed2.success) {
@@ -59998,13 +60197,13 @@ function createBrainRoutes(opts) {
59998
60197
  });
59999
60198
  return true;
60000
60199
  }
60001
- if (path66 === MASTER_PAGE_PATH || path66 === MASTER_API_PATH) {
60200
+ if (path67 === MASTER_PAGE_PATH || path67 === MASTER_API_PATH) {
60002
60201
  if (req.method !== "GET") {
60003
60202
  res.writeHead(405, { "Content-Type": "application/json", "Cache-Control": "no-store", Allow: "GET" });
60004
60203
  res.end(JSON.stringify({ ok: false, error: "This page is read only." }));
60005
60204
  return true;
60006
60205
  }
60007
- if (path66 === MASTER_PAGE_PATH) {
60206
+ if (path67 === MASTER_PAGE_PATH) {
60008
60207
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
60009
60208
  res.end(brainMasterPageHtml(opts.seatFor(req), opts.seatHeader));
60010
60209
  return true;
@@ -60017,14 +60216,14 @@ function createBrainRoutes(opts) {
60017
60216
  void Promise.resolve().then(() => masterView(md())).then((v) => json2(res, 200, v)).catch(() => json2(res, 200, { ok: false, error: "The brain could not be read. Check the Brain card in Connections, then Refresh." }));
60018
60217
  return true;
60019
60218
  }
60020
- if (req.method === "GET" && path66 === "/brain") {
60219
+ if (req.method === "GET" && path67 === "/brain") {
60021
60220
  const accounts = [...opts.accounts()].sort((a, b) => a.name.localeCompare(b.name));
60022
- const client = onBoard(u.searchParams.get("client") ?? "");
60221
+ const client = onBoard(u.searchParams.get("client") ?? "") ?? (accounts.length === 1 && !u.searchParams.get("client") ? onBoard(accounts[0].locationId) : void 0);
60023
60222
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
60024
60223
  res.end(brainApprovePageHtml({ accounts, client }, opts.seatFor(req), opts.seatHeader));
60025
60224
  return true;
60026
60225
  }
60027
- if (req.method === "GET" && path66 === BRAIN_PENDING_PATH) {
60226
+ if (req.method === "GET" && path67 === BRAIN_PENDING_PATH) {
60028
60227
  const client = onBoard(u.searchParams.get("client") ?? "");
60029
60228
  if (!client) {
60030
60229
  json2(res, 200, { ok: false, error: "That account is not on the board." });
@@ -60033,7 +60232,7 @@ function createBrainRoutes(opts) {
60033
60232
  void listPending(opts.deps(), client.locationId).then((view2) => json2(res, 200, view2)).catch(() => json2(res, 200, { ok: false, error: "The brain could not be read. Check the Brain card in Connections, then Refresh." }));
60034
60233
  return true;
60035
60234
  }
60036
- if (req.method === "GET" && path66 === BRAIN_SEND_LOG_PATH) {
60235
+ if (req.method === "GET" && path67 === BRAIN_SEND_LOG_PATH) {
60037
60236
  const client = onBoard(u.searchParams.get("client") ?? "");
60038
60237
  if (!client) {
60039
60238
  json2(res, 200, { ok: false, error: "That account is not on the board." });
@@ -60043,7 +60242,7 @@ function createBrainRoutes(opts) {
60043
60242
  json2(res, 200, rows2 === "unconfirmed" ? { ok: false, error: "The send log on this computer (brain-send-log.jsonl in the Command OS app-data folder) could not be read. Nothing can be sent until it is repaired or moved." } : { ok: true, client: client.name, rows: rows2 });
60044
60243
  return true;
60045
60244
  }
60046
- if (req.method === "POST" && (path66 === "/api/brain/decide" || path66 === "/api/brain/send" || path66 === "/api/brain/send-mode" || path66 === "/api/brain/confirm-mappings")) {
60245
+ if (req.method === "POST" && (path67 === "/api/brain/decide" || path67 === "/api/brain/send" || path67 === "/api/brain/send-mode" || path67 === "/api/brain/confirm-mappings")) {
60047
60246
  void readJson(req, 2e4).then(async (body) => {
60048
60247
  try {
60049
60248
  await answer2(body);
@@ -60054,7 +60253,7 @@ function createBrainRoutes(opts) {
60054
60253
  if (!res.headersSent) json2(res, 400, { ok: false, wrote: "nothing", error: "Send a valid request." });
60055
60254
  });
60056
60255
  const answer2 = async (body) => {
60057
- if (path66 === "/api/brain/decide") {
60256
+ if (path67 === "/api/brain/decide") {
60058
60257
  const parsed3 = DecideRequest.safeParse(body);
60059
60258
  if (!parsed3.success) {
60060
60259
  json2(res, 400, { ok: false, wrote: "nothing", error: "That request is not a decision this screen makes." });
@@ -60067,7 +60266,7 @@ function createBrainRoutes(opts) {
60067
60266
  json2(res, 200, parsed3.data.preview ? await previewDecision(opts.deps(), parsed3.data) : await decide(opts.deps(), parsed3.data));
60068
60267
  return;
60069
60268
  }
60070
- if (path66 === "/api/brain/confirm-mappings") {
60269
+ if (path67 === "/api/brain/confirm-mappings") {
60071
60270
  const parsed3 = ConfirmMappingsRequest.safeParse(body);
60072
60271
  if (!parsed3.success) {
60073
60272
  json2(res, 400, { ok: false, wrote: "nothing", error: "That request is not a confirmation this screen makes." });
@@ -60080,7 +60279,7 @@ function createBrainRoutes(opts) {
60080
60279
  json2(res, 200, await confirmMappings(opts.deps(), parsed3.data));
60081
60280
  return;
60082
60281
  }
60083
- if (path66 === "/api/brain/send-mode") {
60282
+ if (path67 === "/api/brain/send-mode") {
60084
60283
  const parsed3 = SendModeRequest.safeParse(body);
60085
60284
  if (!parsed3.success) {
60086
60285
  json2(res, 400, { ok: false, wrote: "nothing", error: "That request is not a send-mode change this screen makes." });
@@ -60821,11 +61020,11 @@ function defaultHasSavedSetup() {
60821
61020
  return false;
60822
61021
  }
60823
61022
  }
60824
- var import_node_child_process4, import_node_fs14, GHL_SERVER_NAME, CLAUDE_CODE_ADD_ARGS, CLAUDE_CODE_INSTALL_LINE, CLAUDE_CODE_ADD_HELP, OURS, defaultRunner;
61023
+ var import_node_child_process5, import_node_fs14, GHL_SERVER_NAME, CLAUDE_CODE_ADD_ARGS, CLAUDE_CODE_INSTALL_LINE, CLAUDE_CODE_ADD_HELP, OURS, defaultRunner;
60825
61024
  var init_claude_code_registration = __esm({
60826
61025
  "src/claude-code-registration.ts"() {
60827
61026
  "use strict";
60828
- import_node_child_process4 = require("node:child_process");
61027
+ import_node_child_process5 = require("node:child_process");
60829
61028
  init_claude_locate();
60830
61029
  init_running_channel();
60831
61030
  import_node_fs14 = require("node:fs");
@@ -60836,7 +61035,7 @@ var init_claude_code_registration = __esm({
60836
61035
  CLAUDE_CODE_INSTALL_LINE = "npx -y @elitedcs/ghl-mcp@latest cli install --claude-code";
60837
61036
  CLAUDE_CODE_ADD_HELP = claudeCodeAddHelp("latest");
60838
61037
  OURS = /ghl-mcp|start-mcp\.sh|dist[\\/]index\.js/i;
60839
- defaultRunner = (command, args, env2, timeoutMs) => new Promise((resolve14) => {
61038
+ defaultRunner = (command, args, env2, timeoutMs) => new Promise((resolve15) => {
60840
61039
  let stdout = "";
60841
61040
  let stderr = "";
60842
61041
  let settled = false;
@@ -60845,12 +61044,12 @@ var init_claude_code_registration = __esm({
60845
61044
  if (!settled) {
60846
61045
  settled = true;
60847
61046
  if (timer) clearTimeout(timer);
60848
- resolve14(r);
61047
+ resolve15(r);
60849
61048
  }
60850
61049
  };
60851
61050
  let child;
60852
61051
  try {
60853
- child = (0, import_node_child_process4.spawn)(command, args, { stdio: ["ignore", "pipe", "pipe"], env: env2 });
61052
+ child = (0, import_node_child_process5.spawn)(command, args, { stdio: ["ignore", "pipe", "pipe"], env: env2 });
60854
61053
  } catch {
60855
61054
  done({ code: null, stdout, stderr, failed: true });
60856
61055
  return;
@@ -61099,7 +61298,7 @@ function startDesignRun(request2, opts = {}) {
61099
61298
  const job = { id: id2, status: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString(), outDir, request: request2, tail: [] };
61100
61299
  jobs.set(id2, job);
61101
61300
  const entry = opts.entry ?? process.argv[1];
61102
- const spawner = opts.spawnFn ?? import_node_child_process5.spawn;
61301
+ const spawner = opts.spawnFn ?? import_node_child_process6.spawn;
61103
61302
  const child = spawner(process.execPath, [entry, ...argvFor(request2, outDir)], {
61104
61303
  cwd: outDir,
61105
61304
  stdio: ["ignore", "pipe", "pipe"],
@@ -61160,11 +61359,11 @@ function cancelDesignRun(id2) {
61160
61359
  }
61161
61360
  return true;
61162
61361
  }
61163
- var import_node_child_process5, import_node_fs15, import_node_path13, import_node_crypto29, SLUG2, MODEL, JOINED_FLAG, jobs, children;
61362
+ var import_node_child_process6, import_node_fs15, import_node_path13, import_node_crypto29, SLUG2, MODEL, JOINED_FLAG, jobs, children;
61164
61363
  var init_run_jobs = __esm({
61165
61364
  "src/command-os/design/run-jobs.ts"() {
61166
61365
  "use strict";
61167
- import_node_child_process5 = require("node:child_process");
61366
+ import_node_child_process6 = require("node:child_process");
61168
61367
  import_node_fs15 = require("node:fs");
61169
61368
  import_node_path13 = require("node:path");
61170
61369
  import_node_crypto29 = require("node:crypto");
@@ -61252,14 +61451,14 @@ var init_run_cost = __esm({
61252
61451
  // src/claude-call.ts
61253
61452
  function callClaudeRecorded(bin, argv, context2, onCost = () => {
61254
61453
  }) {
61255
- return new Promise((resolve14, reject) => {
61454
+ return new Promise((resolve15, reject) => {
61256
61455
  const run = new RunTexts();
61257
61456
  let buffer = "";
61258
61457
  let stderr = "";
61259
61458
  let facts = {};
61260
61459
  let settled = false;
61261
61460
  const cmd = claudeCommand(bin, [...argv, "--output-format", "stream-json", "--verbose", ...NO_MEMBER_HOOKS]);
61262
- const child = (0, import_node_child_process6.spawn)(cmd.command, cmd.args, { stdio: ["ignore", "pipe", "pipe"] });
61461
+ const child = (0, import_node_child_process7.spawn)(cmd.command, cmd.args, { stdio: ["ignore", "pipe", "pipe"] });
61263
61462
  const timer = setTimeout(() => child.kill("SIGKILL"), 6 * 60 * 1e3);
61264
61463
  const finish3 = (ok) => {
61265
61464
  if (settled) return;
@@ -61271,7 +61470,7 @@ function callClaudeRecorded(bin, argv, context2, onCost = () => {
61271
61470
  onCost("The cost record could not be saved.");
61272
61471
  }
61273
61472
  onCost(`${context2.kind}: ${facts.costUsd === void 0 ? "cost not reported" : formatUsd(facts.costUsd)}.`);
61274
- if (ok) resolve14(run.pick());
61473
+ if (ok) resolve15(run.pick());
61275
61474
  else reject(new Error("Check your Claude authentication in Your agency before reading this source."));
61276
61475
  };
61277
61476
  const consume = (line4) => {
@@ -61300,11 +61499,11 @@ function callClaudeRecorded(bin, argv, context2, onCost = () => {
61300
61499
  });
61301
61500
  });
61302
61501
  }
61303
- var import_node_child_process6;
61502
+ var import_node_child_process7;
61304
61503
  var init_claude_call = __esm({
61305
61504
  "src/claude-call.ts"() {
61306
61505
  "use strict";
61307
- import_node_child_process6 = require("node:child_process");
61506
+ import_node_child_process7 = require("node:child_process");
61308
61507
  init_run_cost();
61309
61508
  init_claude_cli();
61310
61509
  init_claude_locate();
@@ -61434,9 +61633,10 @@ function calendarStaff(h, facts, planned) {
61434
61633
  }
61435
61634
  return { done: true, teams };
61436
61635
  }
61437
- function withBuiltCalendarTeams(facts, saved) {
61438
- const built = saved?.calendarTeamsBuilt;
61439
- return built && Object.keys(built).length ? { ...facts, calendarTeamsBuilt: built } : facts;
61636
+ function withBuiltCalendarTeams(facts, saved, journal2 = {}) {
61637
+ const built = { ...journal2 };
61638
+ for (const [name2, ids] of Object.entries(saved?.calendarTeamsBuilt ?? {})) built[name2] = [.../* @__PURE__ */ new Set([...built[name2] ?? [], ...ids])];
61639
+ return Object.keys(built).length ? { ...facts, calendarTeamsBuilt: built } : facts;
61440
61640
  }
61441
61641
  function mergeCalendarTeamsBuilt(kept, built) {
61442
61642
  return { ...kept ?? {}, ...Object.fromEntries(built.filter((b) => b.type === "calendar" && b.teamIds?.length).map((b) => [b.name.trim().toLowerCase(), [...b.teamIds]])) };
@@ -61571,6 +61771,22 @@ function phoneInfo(facts) {
61571
61771
  if (phones.some((p) => digits(p) === digits(biz))) return void 0;
61572
61772
  return `Your texts come from ${phones[0]}; your business line is ${biz}. That is fine if the business line is the clinic's own number.`;
61573
61773
  }
61774
+ function fitHandoff(h, all, facts) {
61775
+ let out = h;
61776
+ const booking = facts?.customValues?.find((v) => v.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_") === "booking_link")?.value;
61777
+ if (typeof booking === "string" && booking.trim() && out.instruction && /booking_link custom value/i.test(out.instruction)) {
61778
+ out = { ...out, instruction: out.instruction.replace(/\s*[^.]*\bbooking_link custom value\.?/i, "").trim() };
61779
+ }
61780
+ if (out.ref === "handoff.business_details" && all.some((x) => x?.ref === "handoff.phone_number") && /business_phone/.test(out.instruction ?? "")) {
61781
+ out = {
61782
+ ...out,
61783
+ title: "Confirm the name messages are signed with",
61784
+ instruction: "Settings > Custom Values: set owner_first_name to the first name every email is signed with. The build pre-fills it from the intake when it can. The business phone is the phone number job above.",
61785
+ successCheck: "owner_first_name shows a real value, not a blank"
61786
+ };
61787
+ }
61788
+ return out;
61789
+ }
61574
61790
  function catalogJobs(input) {
61575
61791
  const out = [];
61576
61792
  const seen = /* @__PURE__ */ new Set();
@@ -61583,11 +61799,12 @@ function catalogJobs(input) {
61583
61799
  for (const h of input.handoffs) {
61584
61800
  if (!h || typeof h.ref !== "string") continue;
61585
61801
  const plannedCalendars = input.plannedCalendars ?? /* @__PURE__ */ new Map();
61586
- const state2 = handoffInAccount(h, input.facts ?? {}, input.readback ?? [], input.plannedUsers ?? [], openRecipientJobs, plannedCalendars);
61802
+ const fit = fitHandoff(h, input.handoffs, input.facts);
61803
+ const state2 = handoffInAccount(fit, input.facts ?? {}, input.readback ?? [], input.plannedUsers ?? [], openRecipientJobs, plannedCalendars);
61587
61804
  const text3 = handoffJobText(h);
61588
61805
  if (!(state2.readable && state2.done)) {
61589
- const why = (state2.readable || h.ref === ASSIGN_CALENDAR_STAFF) && input.facts ? handoffReadReason(h, input.facts, plannedCalendars) : void 0;
61590
- push({ id: h.ref, text: handoffShownText(h, input.facts), timing: jobTiming(text3), selfReport: !state2.readable, key: requirementsKey([text3]), ...why ? { why } : {} });
61806
+ const why = (state2.readable || h.ref === ASSIGN_CALENDAR_STAFF) && input.facts ? handoffReadReason(fit, input.facts, plannedCalendars) : void 0;
61807
+ push({ id: h.ref, text: handoffShownText(fit, input.facts), timing: jobTiming(text3), selfReport: !state2.readable, key: requirementsKey([text3]), ...why ? { why } : {} });
61591
61808
  }
61592
61809
  const part = HANDOFF_PARTS[h.ref];
61593
61810
  if (part) {
@@ -62609,6 +62826,7 @@ function evalCondition(cond, ctx) {
62609
62826
  const [, pathExpr, op, rawRhs] = cmp;
62610
62827
  let lhs = getPath(ctx.brief, pathExpr);
62611
62828
  if (pathExpr === "goal.bookingNeeded") lhs = ctx.bookingWanted;
62829
+ if (pathExpr === "channels.sms") lhs = smsOn(ctx.brief);
62612
62830
  const rhs = rawRhs.trim().replace(/^'(.*)'$/, "$1").replace(/^"(.*)"$/, "$1");
62613
62831
  let equal;
62614
62832
  if (rhs === "true" || rhs === "false") equal = lhs === (rhs === "true");
@@ -63478,7 +63696,7 @@ function planFromPreset(input) {
63478
63696
  const skTemplates = sk.templates ?? {};
63479
63697
  const emailTemplates = keptByCondition(skTemplates.emails, ctx).map((t) => fillNode(t, ctx));
63480
63698
  const gsm7 = typeof p.conventions?.gsm7 === "string";
63481
- const smsTemplates = keptByCondition(skTemplates.sms, ctx).map((t) => {
63699
+ const smsTemplates = (smsOn(input.brief) ? keptByCondition(skTemplates.sms, ctx) : []).map((t) => {
63482
63700
  const filled2 = fillNode(t, ctx);
63483
63701
  if (gsm7 && typeof filled2.body === "string") filled2.body = toGsm7(filled2.body);
63484
63702
  return filled2;
@@ -63890,6 +64108,7 @@ var init_plan_from_preset = __esm({
63890
64108
  init_sms_rules();
63891
64109
  init_business_hours();
63892
64110
  init_plan_guide();
64111
+ init_texting();
63893
64112
  init_claims_guard();
63894
64113
  PRESET_TOKEN_ROOTS = /* @__PURE__ */ new Set(["business", "offer", "audience", "voice", "goal", "proof"]);
63895
64114
  GSM7_REPLACEMENTS = [
@@ -64272,7 +64491,7 @@ function unitLooksLike(unit) {
64272
64491
  function gateExtraction(transcript, proposed, questions) {
64273
64492
  const knownIds = questions;
64274
64493
  const accepted = [];
64275
- const held = [];
64494
+ const held2 = [];
64276
64495
  const rejected = [];
64277
64496
  const patch = {};
64278
64497
  const seen = /* @__PURE__ */ new Set();
@@ -64309,10 +64528,10 @@ function gateExtraction(transcript, proposed, questions) {
64309
64528
  accepted.push(item);
64310
64529
  patch[item.questionId] = item.value;
64311
64530
  } else {
64312
- held.push({ ...item, because: cite.because });
64531
+ held2.push({ ...item, because: cite.because });
64313
64532
  }
64314
64533
  }
64315
- return { accepted, held, rejected, patch };
64534
+ return { accepted, held: held2, rejected, patch };
64316
64535
  }
64317
64536
  function mergeAccepted(ans, result) {
64318
64537
  return { ...ans, ...result.patch };
@@ -64641,10 +64860,10 @@ async function prefillFromSource(spawnClaude, labels, source, toolContext = { se
64641
64860
  const byLabel = new Map(questions.map((q3) => [q3.label, q3]));
64642
64861
  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);
64643
64862
  result.answers = validatePrefillAnswers(questions, result.answers);
64644
- for (const [label2, held] of Object.entries(result.held)) {
64645
- const checked = validatePrefillAnswers(questions, { [label2]: held.value });
64863
+ for (const [label2, held2] of Object.entries(result.held)) {
64864
+ const checked = validatePrefillAnswers(questions, { [label2]: held2.value });
64646
64865
  if (!(label2 in checked)) delete result.held[label2];
64647
- else held.value = checked[label2];
64866
+ else held2.value = checked[label2];
64648
64867
  }
64649
64868
  result.unverified = result.unverified.filter((label2) => label2 in result.answers);
64650
64869
  result.unknown = labels.filter((label2) => !(label2 in result.answers) && !(label2 in result.held));
@@ -64738,7 +64957,7 @@ async function runPrefill(call2, labels, material, questions = /* @__PURE__ */ n
64738
64957
  if (json3 === void 0) return fail("the model did not return a result line");
64739
64958
  const parsed2 = JSON.parse(json3);
64740
64959
  const answers = {};
64741
- const held = {};
64960
+ const held2 = {};
64742
64961
  const unverified = [];
64743
64962
  for (const [k, raw] of Object.entries(parsed2.answers ?? {})) {
64744
64963
  if (!labels.includes(k)) continue;
@@ -64756,10 +64975,10 @@ async function runPrefill(call2, labels, material, questions = /* @__PURE__ */ n
64756
64975
  const checks = (Array.isArray(value) ? value : [value]).map((v) => intakeCitation(material, v, quote2, basis, q3 ? { label: k, options: q3.options } : void 0));
64757
64976
  const cite = checks.find((c) => c.verdict === "refused") ?? checks.find((c) => c.verdict === "held") ?? checks[0];
64758
64977
  if (cite.verdict === "ok") answers[k] = value;
64759
- else if (cite.verdict === "held") held[k] = { value, because: cite.because, ...cite.claims ? { claims: true } : {} };
64978
+ else if (cite.verdict === "held") held2[k] = { value, because: cite.because, ...cite.claims ? { claims: true } : {} };
64760
64979
  }
64761
- const unknown = labels.filter((l) => !(l in answers) && !(l in held));
64762
- return { ok: true, answers, unknown, held, unverified };
64980
+ const unknown = labels.filter((l) => !(l in answers) && !(l in held2));
64981
+ return { ok: true, answers, unknown, held: held2, unverified };
64763
64982
  } catch (e) {
64764
64983
  return fail(e instanceof Error ? e.message : String(e));
64765
64984
  }
@@ -65225,7 +65444,7 @@ function bsSummary(n,c,state){const id=c.locationId;const acc=((state.accounts||
65225
65444
  if(n===6){const sc=(state.staffChecks||{})[id];return sc&&sc.matched&&sc.matched.length?"In the account: "+sc.matched.map(m=>m.name).join(", ")+".":"Staff confirmed."}
65226
65445
  if(n===7){const a=(state.buildApproved||{})[id];return a?"Approved "+niceDate(a,true)+".":"Approved."}
65227
65446
  /* Run 9: the folded step said only "Built.". It says what was made, from the build's own record. */
65228
- if(n===8){const made=((((state.buildViews||{})[id])||{}).built||[]).filter(x=>x.status==="created");if(!made.length)return "Built.";
65447
+ if(n===8){const bv0=(((state.buildViews||{})[id])||{});const made=(bv0.made&&bv0.made.length)?bv0.made:(bv0.built||[]).filter(x=>x.status==="created");if(!made.length)return "Built.";
65229
65448
  const by={};for(const x of made)by[x.type]=(by[x.type]||0)+1;const KIND={pipeline:"pipeline",stage:"pipeline stage",field:"custom field",tag:"tag",cv:"custom value",calendar:"calendar",form:"form",email_template:"email template",sms_template:"text template",workflow:"draft automation",user:"staff login"};
65230
65449
  return "Built in GoHighLevel: "+Object.entries(by).map(([k,v])=>v+" "+plural(v,KIND[k]||k)).join(", ")+". Nothing is switched on."}
65231
65450
  if(n===9){const oc=(state.outcomes||{})[id+":4"];const must=bsMustDo(c,state).length;
@@ -65285,7 +65504,7 @@ function bsBody(n,c,state,run,box){const id=c.locationId;const key="s"+n+":"+id;
65285
65504
  if(bp&&!bp.confirmed&&!live){bsPause(c,state,bp.preview,box);return}
65286
65505
  bsOutcome(c,state,3,box);
65287
65506
  /* Run 8 finding 4: it ended on just "Built." The finish says what was made, counted from the build's own record. */
65288
- if(!live&&(((state.clients||{})[id]||{}).stages||[])[3]==="done"){const made=((((state.buildViews||{})[id])||{}).built||[]).filter(x=>x.status==="created");
65507
+ if(!live&&(((state.clients||{})[id]||{}).stages||[])[3]==="done"){const bv0=(((state.buildViews||{})[id])||{});const made=(bv0.made&&bv0.made.length)?bv0.made:(bv0.built||[]).filter(x=>x.status==="created");
65289
65508
  if(made.length){const by={};for(const x of made)by[x.type]=(by[x.type]||0)+1;const KIND={pipeline:"pipeline",stage:"pipeline stage",field:"custom field",tag:"tag",cv:"custom value",calendar:"calendar",form:"form",email_template:"email template",sms_template:"text template",workflow:"draft automation",user:"staff login"};
65290
65509
  box.appendChild(mk("p","","Made in GoHighLevel this build: "+Object.entries(by).map(([k,n])=>n+" "+plural(n,KIND[k]||k)).join(", ")+". Every automation is a draft until you switch it on."))}}if(!live)bsSnippets(c,state,box);bsRun(c,state,run,3,(((state.clients[id]||{}).stages||[])[3]==="done")?"Build again":"Start the build",box);return}
65291
65510
  if(n===9){bsOutcome(c,state,4,box);bsRun(c,state,run,4,(state.outcomes||{})[id+":4"]?"Run the check again":"Run the check",box);if((state.outcomes||{})[id+":4"])bsRebuild(c,state,run,box);return}
@@ -65620,7 +65839,14 @@ function stepsPage(c,state,run){const id=c.locationId;const inp=bsInputs(c,state
65620
65839
  if(view.approvedAt)d.appendChild(mk("p","","Building approved on "+niceDate(view.approvedAt,true)+"."));page.appendChild(d)}}
65621
65840
  /* The step that just opened comes into view, once per change (and on first open), after the redraw keeps its scroll. */
65622
65841
  if(bsSeen[id]!==pr.current){bsSeen[id]=pr.current;if(pr.current>0)setTimeout(()=>{const el=document.getElementById("step-"+(pr.current+1));if(el)try{el.scrollIntoView({block:"start",behavior:"smooth"})}catch(e){el.scrollIntoView()}},0)}
65623
- if(pr.current<0)page.appendChild(mk("p","cds-callout info","All "+n+" steps are done. The build is signed off. Switching the client on is yours to do in GoHighLevel: see step 12's list."));
65842
+ if(pr.current<0){page.appendChild(mk("p","cds-callout info","All "+n+" steps are done. The build is signed off. Switching the client on is yours to do in GoHighLevel: see step 12's list."));
65843
+ /* Walk W26 (2026-10-29): after sign-off nothing could start the build again, and the page still said Built after its
65844
+ objects were deleted in GoHighLevel. The check can always run again; when it finds built objects gone, it says how
65845
+ many and Build again is offered beside it. */
65846
+ const after=mk("div","bs-after-signoff");const gone=((state.builtMissing||{})[id])||0;
65847
+ if(gone)after.appendChild(mk("p","cds-callout warn bs-gone","The last check found "+gone+" "+plural(gone,"thing")+" this build made missing from "+c.name+" in GoHighLevel. Build again to put "+(gone===1?"it":"them")+" back, then run the check."));
65848
+ else after.appendChild(mk("p","muted","Changed something in GoHighLevel since? Check the account again to see it as it is now."));
65849
+ bsRun(c,state,run,4,"Check the account again",after,!gone);if(gone)bsRebuild(c,state,run,after);page.appendChild(after)}
65624
65850
  return page}
65625
65851
  `;
65626
65852
  BS_STAFF_WHY = "Everyone on your team needs their own login in this client's GoHighLevel account. The build only gives appointments, alerts and tasks to people who have one, so each person below must be in the account before the build runs.";
@@ -65740,7 +65966,7 @@ ${cockpitHeaderHtml("agency")}
65740
65966
  <h1 class="cds-page-title">Your agency's own details, set once and used on every client</h1>
65741
65967
  <p class="cds-page-sub">Filled in once. Reused on every client document, every proposal, and every demo page, so nothing has to ask you again.</p>
65742
65968
  </div>
65743
- <div class="ap-save"><span id="msgTop" role="status"></span><button type="submit" form="f" class="cds-btn">Save your agency details</button></div>
65969
+ <div class="ap-save"><span id="msgTop" role="status"></span><button type="submit" form="f" class="cds-btn" data-msg="msgTop">Save your agency details</button></div>
65744
65970
  </div>
65745
65971
  ${p.agencyName ? "" : ` <div class="cds-callout warn"><span class="ttl">Add an agency name to use this on client documents.</span>Everything else can wait. The name is what every report and proposal carries.</div>
65746
65972
  `}
@@ -65755,7 +65981,7 @@ ${p.agencyName ? "" : ` <div class="cds-callout warn"><span class="ttl">Add an a
65755
65981
  <div class="cds-field"><label for="accent">Brand color</label><input type="color" class="cds-input" id="accent" name="accent" value="${esc14(p.accent || "#b8934a")}"></div>
65756
65982
  </div>
65757
65983
  ${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." })}
65758
- ${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." })}
65984
+ ${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. Any size: we make a small copy for reports, and your file is not changed. Reports you hand clients carry it inside the page, so it prints and travels with a saved copy." })}
65759
65985
  ${input("bookingUrl", "Booking link", p.bookingUrl, { placeholder: "https://your-calendar-link", hint: "Your own calendar link, usually a GoHighLevel booking page. The Book the walkthrough button on a report you send a prospect opens it. Must start with https." })}
65760
65986
  </section>
65761
65987
  <section class="cds-panel">
@@ -65855,6 +66081,9 @@ ${SERVICE_IDS.map((id2) => ` <label class="cds-check"><input type="checkbox"
65855
66081
  </ul>
65856
66082
  </section>
65857
66083
 
66084
+ <!-- Jerry's rule (walk 2026-10-29): a long form's action sits at the bottom as well as the top; one submit, one result
66085
+ line, beside the Save that was pressed. -->
66086
+ <div class="ap-save ap-save-bottom"><button type="submit" class="cds-btn" data-msg="msgBottom">Save your agency details</button><span id="msgBottom" role="status"></span></div>
65858
66087
  </form>
65859
66088
 
65860
66089
  <div class="cds-grid two ap-start">
@@ -66003,6 +66232,9 @@ function settlePackages(profile,read){
66003
66232
  const v=id=>document.getElementById(id).value.trim();
66004
66233
  const lines=id=>v(id).split("\\n").map(s=>s.trim()).filter(Boolean);
66005
66234
  document.getElementById("f").addEventListener("submit",async e=>{
66235
+ /* The one result line is beside the Save that was pressed; the other is cleared. */
66236
+ const msgId=(e.submitter&&e.submitter.getAttribute("data-msg"))||"msgTop";
66237
+ for(const id of ["msgTop","msgBottom"])if(id!==msgId){const x=document.getElementById(id);if(x)x.textContent=""}
66006
66238
  e.preventDefault();
66007
66239
  const offers=lines("offers").map(l=>{
66008
66240
  const m=l.match(/^(.*?)\\s*[.-]\\s*\\$?([\\d,.]+)\\s*(\\/mo)?$/);
@@ -66010,7 +66242,7 @@ document.getElementById("f").addEventListener("submit",async e=>{
66010
66242
  return{name:m[1].trim(),priceFloor:Number(m[2].replace(/,/g,""))||undefined,cadence:m[3]?"monthly":"one-time"};
66011
66243
  });
66012
66244
  const pkgRead=readPackages();
66013
- if(pkgRead.clash){document.getElementById("msgTop").textContent="Two packages, "+pkgRead.clash[0]+" and "+pkgRead.clash[1]+", would be saved as one. Give one of them a different name. Nothing was saved.";return}
66245
+ if(pkgRead.clash){document.getElementById(msgId).textContent="Two packages, "+pkgRead.clash[0]+" and "+pkgRead.clash[1]+", would be saved as one. Give one of them a different name. Nothing was saved.";return}
66014
66246
  const staff=lines("staff").map(l=>{const [name,role,email]=l.split("\xB7").map(s=>s.trim());return{name,role:role||undefined,email:email||undefined}});
66015
66247
  /* EMPTY clears; UNREADABLE stops the save. undefined is the "stop" signal here. */
66016
66248
  const num=id=>{const r=readNumber(v(id));return r.clear?null:(r.bad?undefined:r.value)};
@@ -66030,8 +66262,8 @@ document.getElementById("f").addEventListener("submit",async e=>{
66030
66262
  vocabulary:lines("icaVocabulary").length?lines("icaVocabulary"):null,hangouts:lines("icaHangouts").length?lines("icaHangouts"):null,villains:lines("icaVillains").length?lines("icaVillains"):null},
66031
66263
  offer:{name:v("offerName")||null,promise:v("offerPromise")||null,price:v("offerPrice")||null,guarantee:v("offerGuarantee")||null,
66032
66264
  valueStack:lines("offerStack").length?lines("offerStack"):null,accelerators:lines("offerAccelerators").length?lines("offerAccelerators"):null}}};
66033
- /* Said beside the one Save, in the page header. */
66034
- const m={set textContent(t){document.getElementById("msgTop").textContent=t}};
66265
+ /* Said beside the Save that was pressed. */
66266
+ const m={set textContent(t){document.getElementById(msgId).textContent=t}};
66035
66267
  const badNum=["minMonthly","minSetup"].find(id=>body.pricing[id]===undefined);
66036
66268
  if(badNum){m.textContent="That is not a number I can read, so nothing was saved. Check the "+(badNum==="minMonthly"?"minimum monthly":"minimum setup fee")+".";return}
66037
66269
  m.textContent="Saving\u2026";
@@ -66703,7 +66935,7 @@ function compute(ans, skip, asm) {
66703
66935
  add("missed", "Calls you never get back to",
66704
66936
  [{ id: "calls_missed" }, { id: "missed_never_return", share: true }], null,
66705
66937
  function () { return raw("calls_missed") * share("missed_never_return") * c * v; },
66706
- "Call answering, day and night");
66938
+ "Call answering");
66707
66939
 
66708
66940
  add("waited", "Enquiries that waited",
66709
66941
  [{ id: "waited_month" }], "response_penalty",
@@ -69043,11 +69275,11 @@ var init_closed_roots = __esm({
69043
69275
  READ_RESULT_MAX_CHARS = 16e6;
69044
69276
  OVER_BUDGET_KEY = "__secretShopperOverBudget";
69045
69277
  overBudget = (v) => !!v && typeof v === "object" && OVER_BUDGET_KEY in v;
69046
- deadline = (p, ms, what) => new Promise((resolve14, reject) => {
69278
+ deadline = (p, ms, what) => new Promise((resolve15, reject) => {
69047
69279
  const t = setTimeout(() => reject(new Error(`${what} took longer than ${ms} ms`)), ms);
69048
69280
  p.then((v) => {
69049
69281
  clearTimeout(t);
69050
- resolve14(v);
69282
+ resolve15(v);
69051
69283
  }, (e) => {
69052
69284
  clearTimeout(t);
69053
69285
  reject(e instanceof Error ? e : new Error(String(e)));
@@ -69364,10 +69596,10 @@ async function browserPass(url, opts = {}) {
69364
69596
  let timer;
69365
69597
  let expiredAt = false;
69366
69598
  let closeOnce;
69367
- const expired2 = new Promise((resolve14) => {
69599
+ const expired2 = new Promise((resolve15) => {
69368
69600
  timer = setTimeout(() => {
69369
69601
  expiredAt = true;
69370
- resolve14({ ok: false, reason: `The rendered pass took longer than ${Math.round(deadlineMs / 1e3)} seconds and was stopped.` });
69602
+ resolve15({ ok: false, reason: `The rendered pass took longer than ${Math.round(deadlineMs / 1e3)} seconds and was stopped.` });
69371
69603
  }, deadlineMs);
69372
69604
  });
69373
69605
  const work = (async () => {
@@ -69486,6 +69718,14 @@ function reviewPrompt(input) {
69486
69718
  "For each gap, state the cost using their own numbers when they gave any, and what we build to close it:",
69487
69719
  ...GAP_CHECKLIST.map((g) => ` \u2022 ${g}`),
69488
69720
  "",
69721
+ ...input.promises ? [
69722
+ "WHAT THIS BUILD MAKES. You do not list what we build or how each gap is fixed: the document takes that from this list itself. You write the diagnosis only. Your recommended offer and biggest lever may name a system or automation ONLY if it is on this list:",
69723
+ ...input.promises.willBuild.length ? input.promises.willBuild.map((w) => ` \u2022 ${w}`) : [" \u2022 (nothing yet)"],
69724
+ ...input.promises.held.length ? ["WAITING ON THE CLIENT'S ANSWER (never say we build these):", ...input.promises.held.map((h) => ` \u2022 ${h}`)] : [],
69725
+ 'Never promise that calls are answered live, "day and night", "24/7" or "every call": nothing in this build answers the phone.',
69726
+ ...input.promises.textingOn ? [] : ["This client is not texting: never promise a text, an SMS or a text-back anywhere."],
69727
+ ""
69728
+ ] : [],
69489
69729
  "THE CLIENT'S INTAKE ANSWERS:",
69490
69730
  answers || "(no answers provided)",
69491
69731
  "",
@@ -69497,7 +69737,7 @@ function reviewPrompt(input) {
69497
69737
  ] : [],
69498
69738
  'A gap about deposits or prepayment is never priority "now": deposits are set up later and never hold up sign-off.',
69499
69739
  "Reply with ONE line of JSON and nothing else:",
69500
- 'RESULT: {"headline":"<one sentence a busy owner would nod at>","ica":{"who":"","painPoints":[""],"objections":[""],"buyingTrigger":"","whereTheyAre":""},"offer":{"score":0,"dreamOutcome":"","believability":"","speed":"","effort":"","biggestLever":"","recommendedOffer":""},"gaps":[{"title":"","cost":"","fix":"","priority":"now"}],"build":[{"item":"","because":""}],"unknowns":[""]}'
69740
+ 'RESULT: {"headline":"<one sentence a busy owner would nod at>","ica":{"who":"","painPoints":[""],"objections":[""],"buyingTrigger":"","whereTheyAre":""},"offer":{"score":0,"dreamOutcome":"","believability":"","speed":"","effort":"","biggestLever":"","recommendedOffer":""},"gaps":[{"title":"","cost":"","priority":"now"}],"unknowns":[""]}'
69501
69741
  ].join("\n");
69502
69742
  }
69503
69743
  function accountPrefixes(clientName) {
@@ -69553,6 +69793,97 @@ function strictRetryPrompt(input) {
69553
69793
  "No preamble, no explanation, no markdown fences, no closing remarks."
69554
69794
  ].join("\n");
69555
69795
  }
69796
+ function renderedLines(r) {
69797
+ const out = [];
69798
+ const add2 = (where, text3) => {
69799
+ if (typeof text3 === "string" && text3.trim()) out.push({ where, text: text3 });
69800
+ };
69801
+ add2("headline", r.headline);
69802
+ if (r.ica) {
69803
+ add2("ica.who", r.ica.who);
69804
+ add2("ica.buyingTrigger", r.ica.buyingTrigger);
69805
+ add2("ica.whereTheyAre", r.ica.whereTheyAre);
69806
+ (r.ica.painPoints ?? []).forEach((t, i) => add2(`ica.painPoints.${i}`, t));
69807
+ (r.ica.objections ?? []).forEach((t, i) => add2(`ica.objections.${i}`, t));
69808
+ }
69809
+ if (r.offer) for (const k of ["dreamOutcome", "believability", "speed", "effort", "biggestLever", "recommendedOffer"]) add2(`offer.${k}`, r.offer[k]);
69810
+ (r.gaps ?? []).forEach((g, i) => {
69811
+ add2(`gaps.${i}.title`, g.title);
69812
+ add2(`gaps.${i}.cost`, g.cost);
69813
+ });
69814
+ (r.unknowns ?? []).forEach((t, i) => add2(`unknowns.${i}`, t));
69815
+ return out;
69816
+ }
69817
+ function overPromises(r, p) {
69818
+ if (!p || !r.ok) return [];
69819
+ const made = p.willBuild.join(" | ");
69820
+ return renderedLines(r).filter((l) => LIVE_CALLS.test(l.text) || (!p.textingOn || p.held.some((h) => /texting/i.test(h))) && TEXTS.test(l.text) || TOPICS.some((t) => t.re.test(l.text) && !t.built.test(made))).map((l) => l.where);
69821
+ }
69822
+ function graderPrompt(r, p) {
69823
+ return [
69824
+ "You check a client document for promises. A line promises something if it says or implies that we will build, set up, run or deliver it for the client.",
69825
+ "WHAT THIS BUILD MAKES (the only things a line may promise):",
69826
+ ...p.willBuild.length ? p.willBuild.map((w) => `- ${w}`) : ["- (nothing)"],
69827
+ ...p.held.length ? ["NOT BUILT, WAITING ON THE CLIENT'S ANSWER (a line promising these is a violation):", ...p.held.map((h) => `- ${h}`)] : [],
69828
+ `Texting is ${p.textingOn ? "on" : "OFF: any promise of a text, SMS or text-back is a violation"}. Nothing in this build answers the phone live: any promise of answered calls is a violation.`,
69829
+ "Diagnosis, observations about the client and questions are not promises. Advice clearly marked as for later is not a promise.",
69830
+ "THE LINES (id: text):",
69831
+ ...renderedLines(r).map((l) => `${l.where}: ${l.text}`),
69832
+ 'Reply with ONE line of JSON and nothing else: RESULT: {"violations":["<id>", ...]}'
69833
+ ].join("\n");
69834
+ }
69835
+ function parseGrader(text3, r) {
69836
+ const json3 = extractResultJson(text3);
69837
+ if (json3 === void 0) return null;
69838
+ try {
69839
+ const v = JSON.parse(json3).violations;
69840
+ if (!Array.isArray(v)) return null;
69841
+ const known = new Set(renderedLines(r).map((l) => l.where));
69842
+ return v.map(String).filter((x) => known.has(x));
69843
+ } catch {
69844
+ return null;
69845
+ }
69846
+ }
69847
+ function trimLines(r, bad) {
69848
+ if (!bad.length) return r;
69849
+ const set = new Set(bad);
69850
+ const trimmed = renderedLines(r).filter((l) => set.has(l.where)).map((l) => l.text);
69851
+ const keep = (where) => !set.has(where);
69852
+ const ica = r.ica && {
69853
+ ...r.ica,
69854
+ who: keep("ica.who") ? r.ica.who : "",
69855
+ buyingTrigger: keep("ica.buyingTrigger") ? r.ica.buyingTrigger : "",
69856
+ whereTheyAre: keep("ica.whereTheyAre") ? r.ica.whereTheyAre : "",
69857
+ painPoints: (r.ica.painPoints ?? []).filter((_, i) => keep(`ica.painPoints.${i}`)),
69858
+ objections: (r.ica.objections ?? []).filter((_, i) => keep(`ica.objections.${i}`))
69859
+ };
69860
+ const offer = r.offer && Object.fromEntries(Object.entries(r.offer).map(([k, v]) => [k, typeof v === "string" && !keep(`offer.${k}`) ? "" : v]));
69861
+ return {
69862
+ ...r,
69863
+ headline: keep("headline") ? r.headline : void 0,
69864
+ ...ica ? { ica } : {},
69865
+ ...offer ? { offer } : {},
69866
+ gaps: (r.gaps ?? []).filter((_, i) => keep(`gaps.${i}.title`) && keep(`gaps.${i}.cost`)),
69867
+ unknowns: (r.unknowns ?? []).filter((_, i) => keep(`unknowns.${i}`)),
69868
+ trimmed: [...r.trimmed ?? [], ...trimmed]
69869
+ };
69870
+ }
69871
+ async function violations(spawnClaude, r, p) {
69872
+ if (!p || !r.ok) return { ids: [], graded: true };
69873
+ const fast = overPromises(r, p);
69874
+ let found = null;
69875
+ for (let attempt2 = 0; attempt2 < 2 && found === null; attempt2++) {
69876
+ try {
69877
+ found = parseGrader(await spawnClaude(graderPrompt(r, p)), r);
69878
+ } catch {
69879
+ found = null;
69880
+ }
69881
+ }
69882
+ return { ids: [.../* @__PURE__ */ new Set([...fast, ...found ?? []])], graded: found !== null };
69883
+ }
69884
+ function structural(r, p) {
69885
+ return { ...r, build: [], gaps: (r.gaps ?? []).map((g) => ({ ...g, fix: "" })), ...p ? { promises: p } : {} };
69886
+ }
69556
69887
  async function generateReview(spawnClaude, input) {
69557
69888
  const answered = Object.values(input.answers).filter((v) => v && String(v).trim()).length;
69558
69889
  if (answered < 5) {
@@ -69565,7 +69896,21 @@ async function generateReview(spawnClaude, input) {
69565
69896
  lastRaw = raw;
69566
69897
  const names = [input.clientName, ...Object.entries(input.answers).filter(([k]) => /business[ _]?name/i.test(k)).map(([, v]) => v)].join(" ");
69567
69898
  const parsed2 = parseReview(raw, names);
69568
- if (parsed2.ok) return parsed2;
69899
+ if (parsed2.ok) {
69900
+ const first2 = structural(parsed2, input.promises);
69901
+ const over2 = await violations(spawnClaude, first2, input.promises);
69902
+ if (!over2.ids.length) return { ...first2, graded: over2.graded };
69903
+ try {
69904
+ const again = parseReview(await spawnClaude([reviewPrompt(input), "", `IMPORTANT: your previous answer promised what this build does not make (${over2.ids.join(", ")}). Promise only what is listed under WHAT THIS BUILD MAKES.`].join("\n")), names);
69905
+ if (again.ok) {
69906
+ const second = structural(again, input.promises);
69907
+ const v = await violations(spawnClaude, second, input.promises);
69908
+ return { ...trimLines(second, v.ids), graded: v.graded };
69909
+ }
69910
+ } catch {
69911
+ }
69912
+ return { ...trimLines(first2, over2.ids), graded: over2.graded };
69913
+ }
69569
69914
  } catch (e) {
69570
69915
  lastRaw = e instanceof Error ? e.message : String(e);
69571
69916
  }
@@ -69576,12 +69921,15 @@ async function generateReview(spawnClaude, input) {
69576
69921
  raw: lastRaw.slice(-600)
69577
69922
  };
69578
69923
  }
69924
+ function clientHeldLine(line4) {
69925
+ return line4.replace(/^Waiting for your client's answer: /, "Waiting on your answer: ").replace(/^Not built, your client said not yet: /, "Not set up, as you said not yet: ").replace(/^Not built yet, chosen and coming in a later update: /, "Chosen, and coming in a later update: ");
69926
+ }
69579
69927
  function reviewDocumentHtml(review, opts) {
69580
69928
  const accent = opts.accent && /^#[0-9a-fA-F]{6}$/.test(opts.accent) ? opts.accent : "#B8934A";
69581
69929
  const agency = opts.agencyName?.trim() || "";
69582
69930
  const esc18 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
69583
69931
  const date = (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, { year: "numeric", month: "long", day: "numeric" });
69584
- const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc18(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc18(g.cost)}</div>` : ""}${g.fix ? `<div class="gf">What we do: ${esc18(g.fix)}</div>` : ""}</div>`;
69932
+ const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc18(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc18(g.cost)}</div>` : ""}</div>`;
69585
69933
  const o = review.offer;
69586
69934
  const confirmed = opts.scope && !opts.scope.wholePreset && !opts.scope.awaitingConfirmation ? opts.scope : void 0;
69587
69935
  const purchased = confirmed || opts.purchasedFeatureIds ? [.../* @__PURE__ */ new Set([
@@ -69592,7 +69940,7 @@ function reviewDocumentHtml(review, opts) {
69592
69940
  const represented = [...opts.scope?.built ?? [], ...unbuilt].map((f) => BUILD_ASSESSMENT_FEATURE[f.id]).concat(opts.scope && !opts.scope.awaitingConfirmation ? ["pipeline"] : []);
69593
69941
  const assessmentNext = opts.assessment && purchased ? assessmentFeatureLines(opts.assessment, [...purchased, ...represented]) : [];
69594
69942
  const available = unbuilt.map((f) => `<div class="build"><b>${esc18(f.name)}</b><span>${esc18(f.what)}${f.figure ? ` <b>${esc18(f.figure)}</b>` : ""}</span></div>`).join("") + assessmentNext.map((line4) => `<p>${esc18(line4)}</p>`).join("");
69595
- return `<!doctype html><html><head><meta charset="utf-8"><title>${esc18(opts.clientName)} \u2014 Growth Blueprint</title><style>
69943
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${esc18(opts.clientName)}: Growth Blueprint</title><style>
69596
69944
  @page{margin:0.6in}
69597
69945
  *{box-sizing:border-box}
69598
69946
  body{font-family:-apple-system,Segoe UI,Helvetica,sans-serif;color:#1f2430;margin:0;line-height:1.55}
@@ -69647,7 +69995,7 @@ function reviewDocumentHtml(review, opts) {
69647
69995
  ${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${esc18(o.biggestLever)}</p>` : ""}
69648
69996
  ${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc18(o.recommendedOffer)}</div>` : ""}` : ""}
69649
69997
  ${review.gaps?.length ? `<div class="head">What's leaking today (${review.gaps.length})</div>${review.gaps.map(gap).join("")}` : ""}
69650
- ${review.build?.length ? `<div class="head">What we're building</div>${review.build.map((b) => `<div class="build"><b>${esc18(b.item)}</b><span>${esc18(b.because)}</span></div>`).join("")}` : ""}
69998
+ ${review.promises && (review.promises.willBuild.length || review.promises.held.length) ? `<div class="head">What we're building</div>${review.promises.willBuild.map((w) => `<div class="build"><b>${esc18(w)}</b></div>`).join("")}${review.promises.held.map((h) => `<div class="build"><b>${esc18(clientHeldLine(h))}</b></div>`).join("")}` : ""}
69651
69999
  ${opts.scope?.awaitingConfirmation ? `<div class="head">What this build includes</div>
69652
70000
  <p>Not confirmed yet. The build waits until the features to build are confirmed.</p>
69653
70001
  ` : opts.scope ? `<div class="head">What this build includes</div>
@@ -69663,7 +70011,7 @@ function reviewDocumentHtml(review, opts) {
69663
70011
  <footer><span>${agency ? esc18(agency) : "Growth Blueprint"}</span><span>${esc18(opts.clientName)} \xB7 ${date}</span></footer>
69664
70012
  </div></body></html>`;
69665
70013
  }
69666
- var GAP_CHECKLIST;
70014
+ var GAP_CHECKLIST, LIVE_CALLS, TEXTS, TOPICS;
69667
70015
  var init_blueprint_review = __esm({
69668
70016
  "src/blueprint-review.ts"() {
69669
70017
  "use strict";
@@ -69683,6 +70031,12 @@ var init_blueprint_review = __esm({
69683
70031
  "SMS capability (A2P registered) if they want to text",
69684
70032
  "Reporting they will actually look at"
69685
70033
  ];
70034
+ LIVE_CALLS = /(?:\bday\s*(?:and|&)\s*night\b|\b24\s*[\/\-\u2013]\s*7\b|\b24 hours\b|\b(?:a)?round[- ]the[- ]clock\b|\bafter[- ]hours?\b|\bevery (?:single )?(?:call|caller|enquiry|inquiry|ring)s?\b|\banswer(?:s|ed|ing)?\b[^.]{0,40}\b(?:phones?|calls?|rings?)\b|\b(?:phones?|calls?)\b[^.]{0,20}\banswer(?:s|ed|ing)?\b|\bcalls? (?:are |get )?answered\b|\bpick(?:s|ing)? up\b|\bsomeone rings\b|\bphones? (?:are |is )?covered\b|\b(?:call|phone) coverage\b|\bcoverage on (?:your |the )?phones?\b|\bno missed calls\b|\bnever miss(?:es)?\b[^.]{0,40}\b(?:calls?|leads?|phone)\b|\bno (?:call|caller) (?:goes|is) (?:unanswered|missed)\b|\blive (?:call |phone )?answering\b|\banswering 24\b|\b(?:ai|virtual) (?:receptionist|voice|phone)\b|\breceptionist\b)/i;
70035
+ TEXTS = /\b(?:texts?|texted|texting|sms|text-?backs?|whatsapp|by message on (?:their|your) (?:mobile|phone)|on their mobile|mobile messages?)\b/i;
70036
+ TOPICS = [
70037
+ { re: /\b(?:missed[- ]calls?|text[- ]?backs?|callers?)\b/i, built: /missed-call text-back/i },
70038
+ { re: /\b(?:reactivat\w*|win[- ]?backs?|past (?:clients|customers|patients)|old (?:leads|lists?|clients|customers|patients)|dormant|lapsed|bring (?:them|back)\b|brought back)\b/i, built: /win-back/i }
70039
+ ];
69686
70040
  }
69687
70041
  });
69688
70042
 
@@ -70085,8 +70439,8 @@ function callDate(source, now) {
70085
70439
  }
70086
70440
  return { date: now.toISOString().slice(0, 10), fromRecording: false };
70087
70441
  }
70088
- function confirmList(held) {
70089
- return held.map((h) => {
70442
+ function confirmList(held2) {
70443
+ return held2.map((h) => {
70090
70444
  const saved = /already has "([^"]*)" saved/.exec(h.because)?.[1];
70091
70445
  return `${ASK.get(h.questionId) ?? h.questionId} \u2192 ${h.value} (their words: ${q2(h.quote)})${saved !== void 0 ? ` [replaces the saved "${saved}"]` : ""}`;
70092
70446
  });
@@ -70367,8 +70721,8 @@ async function saveToContact(input, deps, gated, source) {
70367
70721
  for (const c of input.confirmed ?? []) {
70368
70722
  const id2 = String(c?.questionId), value = String(c?.value ?? "");
70369
70723
  if (confirmedIds.has(id2)) continue;
70370
- const held = gated.held.find((h) => h.questionId === id2);
70371
- out.confirmationsNotUsed.push({ questionId: id2, value, why: held ? `the held answer is "${held.value}", not "${value}". Propose "${value}" with its quote, then confirm it.` : gated.rejected.some((r) => r.questionId === id2) ? "that answer was thrown out: its quote is not in the transcript." : "no answer to this question is waiting for confirmation." });
70724
+ const held2 = gated.held.find((h) => h.questionId === id2);
70725
+ out.confirmationsNotUsed.push({ questionId: id2, value, why: held2 ? `the held answer is "${held2.value}", not "${value}". Propose "${value}" with its quote, then confirm it.` : gated.rejected.some((r) => r.questionId === id2) ? "that answer was thrown out: its quote is not in the transcript." : "no answer to this question is waiting for confirmation." });
70372
70726
  }
70373
70727
  const candidates = [...gated.accepted, ...gated.held.filter((h) => confirmedIds.has(h.questionId)).map(({ because: _b, ...a }) => a)];
70374
70728
  const prior = record?.state.ans ?? {};
@@ -71412,13 +71766,13 @@ function briefFromAnswers(answers, questions, industry, briefId) {
71412
71766
  if (cleared.length) brief.__cleared = cleared;
71413
71767
  return brief;
71414
71768
  }
71415
- function getPath2(o, path66) {
71769
+ function getPath2(o, path67) {
71416
71770
  let cur = o;
71417
- for (const k of path66.split(".")) cur = isRec2(cur) ? cur[k] : void 0;
71771
+ for (const k of path67.split(".")) cur = isRec2(cur) ? cur[k] : void 0;
71418
71772
  return cur;
71419
71773
  }
71420
- function clearPath(o, path66) {
71421
- const keys = path66.split(".");
71774
+ function clearPath(o, path67) {
71775
+ const keys = path67.split(".");
71422
71776
  let cur = o;
71423
71777
  for (const k of keys.slice(0, -1)) cur = isRec2(cur) ? cur[k] : void 0;
71424
71778
  if (isRec2(cur)) delete cur[keys[keys.length - 1]];
@@ -71551,12 +71905,12 @@ function withLocationLock(dir, locationId2, fn, opts = {}) {
71551
71905
  return fn();
71552
71906
  } finally {
71553
71907
  if (fd !== void 0) fs52.closeSync(fd);
71554
- let held = "";
71908
+ let held2 = "";
71555
71909
  try {
71556
- held = fs52.readFileSync(lock, "utf8");
71910
+ held2 = fs52.readFileSync(lock, "utf8");
71557
71911
  } catch {
71558
71912
  }
71559
- if (held === token2) fs52.rmSync(lock, { force: true });
71913
+ if (held2 === token2) fs52.rmSync(lock, { force: true });
71560
71914
  }
71561
71915
  }
71562
71916
  function claimClinicPrefix(locationId2, prefix, clientKey, opts) {
@@ -71616,6 +71970,116 @@ var init_clinic_prefix_registry = __esm({
71616
71970
  }
71617
71971
  });
71618
71972
 
71973
+ // src/client-asks.ts
71974
+ function parseAsks(raw) {
71975
+ if (raw === void 0 || raw === null) return { ok: true, asks: {} };
71976
+ if (typeof raw !== "object" || Array.isArray(raw)) return { ok: false, error: "The answers could not be read. Choose them again, then save." };
71977
+ const r = raw;
71978
+ const out = {};
71979
+ if (r.texting !== void 0) {
71980
+ if (r.texting !== "yes" && r.texting !== "not_yet") return { ok: false, error: "Texting is Yes or Not yet." };
71981
+ out.texting = r.texting;
71982
+ }
71983
+ if (r.callAnswering !== void 0) {
71984
+ if (!Array.isArray(r.callAnswering) || !r.callAnswering.every((c) => CALL_ANSWERING_CHOICES.includes(c))) {
71985
+ return { ok: false, error: "Call answering choices could not be read. Choose them again, then save." };
71986
+ }
71987
+ const chosen = [...new Set(r.callAnswering)];
71988
+ if (chosen.length) out.callAnswering = CALL_ANSWERING_CHOICES.filter((c) => chosen.includes(c));
71989
+ }
71990
+ if (r.reactivation !== void 0) {
71991
+ const x = r.reactivation;
71992
+ if (!x || typeof x !== "object") return { ok: false, error: "Reactivation answers could not be read. Choose them again, then save." };
71993
+ if (!REACTIVATION_LISTS.includes(x.list)) return { ok: false, error: "Choose who reactivation should reach." };
71994
+ const offer = x.offer;
71995
+ const text3 = typeof offer?.text === "string" ? offer.text.trim() : "";
71996
+ if (!offer || offer.from !== "brief" && offer.from !== "typed" || !text3) return { ok: false, error: "Choose the offer that brings them back, or type one." };
71997
+ if (text3.length > MAX_OFFER) return { ok: false, error: `Keep the offer under ${MAX_OFFER} characters.` };
71998
+ if (x.channel !== "email" && x.channel !== "email_and_text") return { ok: false, error: "Choose Email, or Email and text." };
71999
+ const a = { list: x.list, offer: { from: offer.from, text: text3 }, channel: x.channel };
72000
+ if (a.list === "in_account_not_booked") {
72001
+ const d = Number(x.notBookedDays);
72002
+ if (!Number.isInteger(d) || d < 1 || d > 3650) return { ok: false, error: "Not booked in how many days? Use a whole number from 1 to 3650." };
72003
+ a.notBookedDays = d;
72004
+ }
72005
+ if (x.textConsentConfirmed === true) a.textConsentConfirmed = true;
72006
+ out.reactivation = a;
72007
+ }
72008
+ return { ok: true, asks: out };
72009
+ }
72010
+ function effectiveTexting(asks, intakeSms) {
72011
+ if (asks?.texting === "yes") return "yes";
72012
+ if (asks?.texting === "not_yet") return "not_yet";
72013
+ if (intakeSms === true) return "yes";
72014
+ if (intakeSms === false) return "not_yet";
72015
+ return "unanswered";
72016
+ }
72017
+ function asksContradiction(asks, texting) {
72018
+ if (texting !== "yes" && asks.callAnswering?.includes("missed_text")) return "A text back after a missed call needs Texting: Yes.";
72019
+ const re = asks.reactivation;
72020
+ if (re?.channel === "email_and_text") {
72021
+ if (texting !== "yes") return "Email and text needs Texting: Yes.";
72022
+ if (!re.textConsentConfirmed) return "Email and text needs your client to confirm these people agreed to get texts.";
72023
+ }
72024
+ return null;
72025
+ }
72026
+ function heldItems(ticks, asks, texting) {
72027
+ const out = [];
72028
+ const wait = (id2, name2) => ({ id: id2, name: name2, reason: "unanswered", line: `Waiting for your client's answer: ${name2}` });
72029
+ const textsMatter = ticks.includes("sms") || ticks.includes("missed") || ticks.includes("reactivation");
72030
+ if (textsMatter && texting === "unanswered") out.push(wait("texting", "Texting"));
72031
+ if (ticks.includes("sms") && texting === "not_yet") out.push({ id: "texting", name: "Texting", reason: "not_yet", line: "Not built, your client said not yet: Texting" });
72032
+ if (ticks.includes("missed")) {
72033
+ const c = asks?.callAnswering ?? [];
72034
+ if (!c.length) out.push(wait("call_answering", "Call answering"));
72035
+ else {
72036
+ const later = c.filter((x) => x === "live_voice" || x === "chat_bot");
72037
+ if (later.length) out.push({ id: "call_answering", name: "Call answering", reason: "later_update", line: `Not built yet, chosen and coming in a later update: ${later.map((x) => x === "live_voice" ? "an AI that answers the phone live" : "an AI that replies by text or chat and books").join(" and ")}` });
72038
+ if (c.includes("missed_text") && texting !== "yes") out.push(wait("texting", "Texting (the text back after a missed call needs it)"));
72039
+ }
72040
+ }
72041
+ if (ticks.includes("reactivation")) {
72042
+ const re = asks?.reactivation;
72043
+ if (!re) out.push(wait("reactivation", "Reactivation (who it reaches, which offer)"));
72044
+ else if (re.list !== "cold_from_now") out.push({ id: "reactivation", name: "Reactivation", reason: "later_update", line: `Not built yet, chosen and coming in a later update: reactivation for ${re.list === "import" ? "a list your client will give you" : `contacts here with no booking in ${re.notBookedDays} days`}` });
72045
+ }
72046
+ return out.filter((h, i) => out.findIndex((o) => o.id === h.id && o.reason === h.reason) === i);
72047
+ }
72048
+ function featuresAllowedByAsks(features, ticks, asks) {
72049
+ return features.filter((f) => {
72050
+ if (f === "missed_call" && ticks.includes("missed")) return asks?.callAnswering?.includes("missed_text") === true;
72051
+ if (f === "winback" && ticks.includes("reactivation")) return asks?.reactivation?.list === "cold_from_now";
72052
+ return true;
72053
+ });
72054
+ }
72055
+ function briefWithAsks(brief, asks, ticks, presetWorkflows) {
72056
+ const ch = brief.channels && typeof brief.channels === "object" ? brief.channels : {};
72057
+ const texting = effectiveTexting(asks, typeof ch.sms === "boolean" ? ch.sms : void 0);
72058
+ const { sms: _was, ...rest } = ch;
72059
+ let out = { ...brief, channels: { ...rest, ...texting === "unanswered" ? {} : { sms: texting === "yes" } } };
72060
+ if (!ticks) return out;
72061
+ const scope = out.scope;
72062
+ const offered = [...new Set(presetWorkflows.map((w) => featureOfWorkflow(String(w.ref ?? ""), w.feature)).filter((f) => f !== null))];
72063
+ const current = scope ? Array.isArray(scope.features) ? scope.features.map(String) : [] : offered;
72064
+ const allowed = featuresAllowedByAsks(current, ticks, asks);
72065
+ if (allowed.length !== current.length) out = { ...out, scope: { ...scope?.package ? { package: scope.package } : {}, features: allowed } };
72066
+ return out;
72067
+ }
72068
+ function asksBindingHash(asks, ticks) {
72069
+ return contentHash2({ asks: asks && Object.keys(asks).length ? asks : null, ticks: ticks ? [...new Set(ticks)].sort() : null });
72070
+ }
72071
+ var CALL_ANSWERING_CHOICES, REACTIVATION_LISTS, MAX_OFFER;
72072
+ var init_client_asks = __esm({
72073
+ "src/client-asks.ts"() {
72074
+ "use strict";
72075
+ init_features();
72076
+ init_build_contract();
72077
+ CALL_ANSWERING_CHOICES = ["live_voice", "chat_bot", "missed_text"];
72078
+ REACTIVATION_LISTS = ["import", "in_account_not_booked", "cold_from_now"];
72079
+ MAX_OFFER = 300;
72080
+ }
72081
+ });
72082
+
71619
72083
  // src/dashboard.ts
71620
72084
  var dashboard_exports = {};
71621
72085
  __export(dashboard_exports, {
@@ -71630,6 +72094,7 @@ __export(dashboard_exports, {
71630
72094
  ASSESSMENT_CHANGED_SINCE_APPROVAL: () => ASSESSMENT_CHANGED_SINCE_APPROVAL,
71631
72095
  ASSESSMENT_PIPELINE_MISSING: () => ASSESSMENT_PIPELINE_MISSING,
71632
72096
  BRIEF_NEEDS_INTAKE: () => BRIEF_NEEDS_INTAKE2,
72097
+ BUILD_WHAT_WAS_WAITING: () => BUILD_WHAT_WAS_WAITING,
71633
72098
  CHOOSE_BEFORE_BUILD: () => CHOOSE_BEFORE_BUILD,
71634
72099
  CHOOSE_CLIENT: () => CHOOSE_CLIENT,
71635
72100
  CHOOSE_WHAT_THEY_BOUGHT: () => CHOOSE_WHAT_THEY_BOUGHT,
@@ -71686,6 +72151,7 @@ __export(dashboard_exports, {
71686
72151
  brandPagesForPro: () => brandPagesForPro,
71687
72152
  briefReach: () => briefReach,
71688
72153
  buildPreviewsFor: () => buildPreviewsFor,
72154
+ buildStatusFor: () => buildStatusFor,
71689
72155
  callClaudeRecorded: () => callClaudeRecorded,
71690
72156
  canonicalIndustry: () => canonicalIndustry,
71691
72157
  capabilityJobs: () => capabilityJobs,
@@ -71727,6 +72193,7 @@ __export(dashboard_exports, {
71727
72193
  guarded: () => guarded,
71728
72194
  hasChosenScope: () => hasChosenScope,
71729
72195
  insideDir: () => insideDir,
72196
+ intakeAskFacts: () => intakeAskFacts,
71730
72197
  intakeErrorDetail: () => intakeErrorDetail,
71731
72198
  intakeInput: () => intakeInput,
71732
72199
  intakeMissingMessage: () => intakeMissingMessage,
@@ -71782,15 +72249,20 @@ __export(dashboard_exports, {
71782
72249
  rereadQueueIntakes: () => rereadQueueIntakes,
71783
72250
  resetCockpitStateRecovery: () => resetCockpitStateRecovery,
71784
72251
  resolvePlan: () => resolvePlan2,
72252
+ reviewIsStale: () => reviewIsStale,
72253
+ reviewPromisesFor: () => reviewPromisesFor,
72254
+ reviewPromisesHash: () => reviewPromisesHash,
71785
72255
  routeGate: () => routeGate,
71786
72256
  runDashboard: () => runDashboard,
71787
72257
  runForTests: () => runForTests,
71788
72258
  runRefusal: () => runRefusal,
71789
72259
  runStopReason: () => runStopReason,
71790
72260
  safeDocName: () => safeDocName,
72261
+ savedPlanFitsAnswers: () => savedPlanFitsAnswers,
71791
72262
  scanGate: () => scanGate,
71792
72263
  scopeHashOf: () => scopeHashOf,
71793
72264
  scopePrefillFor: () => scopePrefillFor,
72265
+ scopeWithAsks: () => scopeWithAsks,
71794
72266
  secondClientInputHash: () => secondClientInputHash,
71795
72267
  setAccountIndustry: () => setAccountIndustry,
71796
72268
  setAccountType: () => setAccountType,
@@ -71981,7 +72453,7 @@ function withdrawIfStaffChanged(state2, locationId2) {
71981
72453
  return { state: { ...setBuildApproval(state2, locationId2, null), planApprovals: approvals }, withdrawn: true, message: ap.staffHash === void 0 ? APPROVAL_WITHDRAWN_NO_TEAM : APPROVAL_WITHDRAWN_FOR_STAFF };
71982
72454
  }
71983
72455
  function withBoughtJobs(tasks2, lines) {
71984
- const kept = tasks2.filter((t) => !SELF_REPORTED.test(t.text));
72456
+ const kept = tasks2.filter((t) => !SELF_REPORTED.test(t.text) && !HELD_JOB.test(t.text));
71985
72457
  const prior = new Map(tasks2.map((t) => [t.id, t]));
71986
72458
  return [...kept, ...lines.map((line4) => {
71987
72459
  if (typeof line4 === "string") return prior.get(taskId(line4)) ?? { id: taskId(line4), text: line4 };
@@ -72081,6 +72553,71 @@ function presetWorkflowsFor(state2, recordKey) {
72081
72553
  }
72082
72554
  return null;
72083
72555
  }
72556
+ function scopeWithAsks(state2, rk) {
72557
+ const current = state2.scope?.[rk];
72558
+ const ticks = state2.bought?.[rk];
72559
+ if (!current || !Array.isArray(ticks)) return current;
72560
+ const base = current.wholePreset ? offeredFeaturesFor(state2, rk) : current.features;
72561
+ const allowed = featuresAllowedByAsks(base, ticks, state2.asks?.[rk]);
72562
+ return allowed.length === base.length ? current : { features: allowed, setAt: current.setAt };
72563
+ }
72564
+ function intakeAskFacts(answers) {
72565
+ const get2 = (key2, label2) => {
72566
+ const v = answers?.[label2] ?? answers?.[key2];
72567
+ return typeof v === "string" && v.trim() ? v.trim() : void 0;
72568
+ };
72569
+ const { sms } = intakeChannelAnswers(answers);
72570
+ const phone = get2("has_phone_number", "Do you already have a phone number in GoHighLevel?")?.toLowerCase();
72571
+ const offer = get2("core_offer", "Core Offer");
72572
+ return { ...sms !== void 0 ? { sms } : {}, ...phone === "yes" || phone === "no" || phone === "unsure" ? { phone } : {}, ...offer ? { offer: offer.slice(0, 300) } : {} };
72573
+ }
72574
+ function buildStatusFor(st, rk, locationId2) {
72575
+ const ticks = st.bought?.[rk] ?? [];
72576
+ const intake = intakeChannelAnswers(st.intakeCompleted?.[locationId2]?.answers ?? st.intakeInputs?.[locationId2]?.answers);
72577
+ const held2 = {};
72578
+ const item = { texting: "sms", call_answering: "missed", reactivation: "reactivation" };
72579
+ for (const h of heldItems(ticks, st.asks?.[rk], effectiveTexting(st.asks?.[rk], intake.sms))) held2[item[h.id]] ??= h.line;
72580
+ const sp = readSavedPlan(rk);
72581
+ const ran = st.outcomes?.[`${rk}:3`]?.ok === true || (sp?.built?.length ?? 0) > 0;
72582
+ if (!ran) return { held: held2 };
72583
+ const featureOf = (ref) => {
72584
+ try {
72585
+ return featureOfWorkflow(ref);
72586
+ } catch {
72587
+ return null;
72588
+ }
72589
+ };
72590
+ const made = /* @__PURE__ */ new Set();
72591
+ for (const o of sp?.planId ? madeByBuild(locationId2, sp.planId) : []) if (o.type === "workflow") {
72592
+ const f = featureOf(o.ref);
72593
+ if (f) made.add(f);
72594
+ }
72595
+ for (const b of sp?.built ?? []) if (b.type === "workflow" && b.realId) {
72596
+ const f = featureOf(String(b.ref));
72597
+ if (f) made.add(f);
72598
+ }
72599
+ return { made, held: held2 };
72600
+ }
72601
+ function reviewPromisesFor(st, rk, locationId2) {
72602
+ const ticks = new Set(st.bought?.[rk] ?? []);
72603
+ const items = boughtItemsFor(offeredFeaturesFor(st, rk), boughtWordingFor(st, rk), buildStatusFor(st, rk, locationId2)).filter((i) => ticks.has(i.id));
72604
+ const willBuild = items.map((i) => /^(?:Will build|Built|Always built): (.+)$/.exec(i.delivery)?.[1]).filter((x) => Boolean(x));
72605
+ const held2 = items.map((i) => i.delivery).filter((d) => HELD_JOB.test(d));
72606
+ const intake = intakeChannelAnswers(st.intakeCompleted?.[locationId2]?.answers ?? st.intakeInputs?.[locationId2]?.answers);
72607
+ return { willBuild, held: held2, textingOn: effectiveTexting(st.asks?.[rk], intake.sms) === "yes" };
72608
+ }
72609
+ function reviewPromisesHash(st, rk, locationId2 = rk) {
72610
+ return contentHash2(reviewPromisesFor(st, rk, locationId2));
72611
+ }
72612
+ function reviewIsStale(st, rk, review) {
72613
+ return Boolean(review?.ok) && review.promisesHash !== reviewPromisesHash(st, rk);
72614
+ }
72615
+ function savedPlanFitsAnswers(sp, state2, rk) {
72616
+ if (!sp || !savedPlanMatchesScope(sp, scopeWithAsks(state2, rk))) return false;
72617
+ const asks = state2.asks?.[rk];
72618
+ if (!sp.asksHash) return !(asks && Object.keys(asks).length);
72619
+ return sp.asksHash === asksBindingHash(asks, state2.bought?.[rk]);
72620
+ }
72084
72621
  function offeredFeaturesFor(state2, recordKey) {
72085
72622
  const workflows = presetWorkflowsFor(state2, recordKey);
72086
72623
  return workflows ? featuresInPreset(workflows) : FEATURE_IDS;
@@ -72201,12 +72738,12 @@ async function resolvePlan2(creds, opts = {}) {
72201
72738
  }
72202
72739
  return { error: "Could not verify the license (offline and no valid cached attestation)." };
72203
72740
  }
72204
- async function recheckCockpitLicense(resolve14 = resolvePlan2, opts = {}) {
72741
+ async function recheckCockpitLicense(resolve15 = resolvePlan2, opts = {}) {
72205
72742
  const id2 = pinnedLicenseIdentity();
72206
72743
  if (!id2) return;
72207
72744
  const seq = beginLicenseCheck(id2);
72208
72745
  const startedWith = readCredentials();
72209
- const r = await resolve14({ ...id2 }, { persist: false }).catch(() => ({ error: "unreachable" }));
72746
+ const r = await resolve15({ ...id2 }, { persist: false }).catch(() => ({ error: "unreachable" }));
72210
72747
  if ("error" in r) {
72211
72748
  if (r.reason) applyLicenseVerdict(seq, id2, r.reason === "team_limit" ? { ok: false, reason: "team_limit", detail: r.error, serverTime: r.serverTime } : { ok: false, reason: r.reason, serverTime: r.serverTime }, opts);
72212
72749
  if (licenseSessionRefusal(Date.now(), "cockpit")) cockpitLicenceRefused("licence re-check refused");
@@ -72219,6 +72756,17 @@ function assertCockpitLicenseHolds(now = Date.now()) {
72219
72756
  const refusal2 = licenseSessionRefusal(now, "cockpit");
72220
72757
  if (refusal2) throw new Error(refusal2);
72221
72758
  }
72759
+ function claimRunStart() {
72760
+ if (activeRun2?.running || runStarting) throw new Error(RUN_IN_PROGRESS);
72761
+ runStarting = true;
72762
+ let done = false;
72763
+ return () => {
72764
+ if (!done) {
72765
+ done = true;
72766
+ runStarting = false;
72767
+ }
72768
+ };
72769
+ }
72222
72770
  function takeOneTimeLine(entry, runLocationId, l) {
72223
72771
  if (l.locationId !== runLocationId) return false;
72224
72772
  const k = l.email.trim().toLowerCase();
@@ -72279,7 +72827,7 @@ function startRun(locationId2, stage, onDone, meta = {}) {
72279
72827
  // or before Your team decided) is made again, never built with someone the member was not shown.
72280
72828
  savedPlan: (() => {
72281
72829
  const sp = stage === 3 ? readSavedPlan(rk) : null;
72282
- return sp && sp.inputHash === meta.inputHash && savedPlanMatchesScope(sp, state2.scope?.[rk]) && savedPlanTeamIsCurrent(sp, meta.normalizedAnswers, composed) ? { planId: sp.planId, savedAt: sp.savedAt, summary: planSummary(sp) } : void 0;
72830
+ return sp && sp.inputHash === meta.inputHash && savedPlanFitsAnswers(sp, state2, rk) && savedPlanTeamIsCurrent(sp, meta.normalizedAnswers, composed) ? { planId: sp.planId, savedAt: sp.savedAt, summary: planSummary(sp) } : void 0;
72283
72831
  })(),
72284
72832
  textSnippets: Boolean(state2.textSnippets?.[rk])
72285
72833
  };
@@ -72295,7 +72843,7 @@ function startRun(locationId2, stage, onDone, meta = {}) {
72295
72843
  } catch {
72296
72844
  runFacts = {};
72297
72845
  }
72298
- runFacts = withBuiltCalendarTeams(runFacts, readSavedPlan(rk));
72846
+ runFacts = withBuiltCalendarTeams(runFacts, readSavedPlan(rk), calendarTeamsFromJournal(locationId2));
72299
72847
  } catch (e) {
72300
72848
  context2.readback = plannedReadbacks(verificationPlan).map(({ ref, type, name: name2 }) => ({ ref, type, name: name2, status: "unreadable", detail: e instanceof Error ? e.message : String(e) }));
72301
72849
  }
@@ -72330,7 +72878,7 @@ function startRun(locationId2, stage, onDone, meta = {}) {
72330
72878
  const ticked = standingTicks(tasksNow, currentKeys);
72331
72879
  const openRecipients = readBuildObligations(rk).filter((o) => o.type === "staff_pending" && o.ref.startsWith("workflow.") && !ticked.has(`${o.type}:${o.ref}`)).length;
72332
72880
  const openHandoffs = planHandoffs.filter((h) => {
72333
- const st = handoffInAccount(h, runFacts, readback, plannedUsers, openRecipients, plannedCalendars);
72881
+ const st = handoffInAccount(fitHandoff(h, planHandoffs, runFacts), runFacts, readback, plannedUsers, openRecipients, plannedCalendars);
72334
72882
  const mainOpen = !(st.readable && st.done) && !ticked.has(h.ref);
72335
72883
  const part = handoffPartId(h.ref);
72336
72884
  return mainOpen || part !== void 0 && !ticked.has(part);
@@ -72713,7 +73261,7 @@ function mergeQueueIntakes(latest, reread, before) {
72713
73261
  return { ...latest, intakeInputs: { ...latest.intakeInputs ?? {}, ...fresh } };
72714
73262
  }
72715
73263
  function gateReadBody(req) {
72716
- return new Promise((resolve14) => {
73264
+ return new Promise((resolve15) => {
72717
73265
  let body = "", over2 = false;
72718
73266
  req.on("data", (c) => {
72719
73267
  if (over2) return;
@@ -72724,8 +73272,8 @@ function gateReadBody(req) {
72724
73272
  req.resume?.();
72725
73273
  }
72726
73274
  });
72727
- req.on("end", () => resolve14(over2 ? null : body));
72728
- req.on("error", () => resolve14(null));
73275
+ req.on("end", () => resolve15(over2 ? null : body));
73276
+ req.on("error", () => resolve15(null));
72729
73277
  });
72730
73278
  }
72731
73279
  function readBody(req, res, max, onDone, tooLarge = "That request is too large.", sensitive = false) {
@@ -72739,10 +73287,10 @@ function readBody(req, res, max, onDone, tooLarge = "That request is too large."
72739
73287
  } catch {
72740
73288
  }
72741
73289
  };
72742
- const held = req[GATE_BODY];
72743
- if (typeof held === "string") {
73290
+ const held2 = req[GATE_BODY];
73291
+ if (typeof held2 === "string") {
72744
73292
  setImmediate(() => {
72745
- if (held.length > max) {
73293
+ if (held2.length > max) {
72746
73294
  try {
72747
73295
  res.writeHead(413, { "Content-Type": "application/json" });
72748
73296
  res.end(JSON.stringify({ ok: false, error: tooLarge }));
@@ -72751,7 +73299,7 @@ function readBody(req, res, max, onDone, tooLarge = "That request is too large."
72751
73299
  return;
72752
73300
  }
72753
73301
  try {
72754
- const r = onDone(held);
73302
+ const r = onDone(held2);
72755
73303
  if (r && typeof r.catch === "function") r.catch(fail);
72756
73304
  } catch (e) {
72757
73305
  fail(e);
@@ -74026,6 +74574,60 @@ async function profLoad(){if(profLoading)return;profLoading=true;try{profCache=(
74026
74574
  waits for the Assessment question above the list. */
74027
74575
  /* The ticks and package chosen but not yet saved, kept outside the block so a redraw keeps them (review of c09e946). */
74028
74576
  let scopeDraft={},boughtNotice={};
74577
+ /* Ask first (Jerry 2026-10-29): texting, call answering and reactivation are asked of the client before Build. The
74578
+ answers save with "Save what they bought" (server: parseAsks, asksContradiction). Nothing is pre-selected: the
74579
+ client's own form answer shows with a Change link; a phone number is only a hint. Blank = held, never built. */
74580
+ let asksDraft={};
74581
+ function asksClean(a){const o={};if(a.texting)o.texting=a.texting;if(a.callAnswering&&a.callAnswering.length)o.callAnswering=[...a.callAnswering];
74582
+ /* W44: typed text counts whenever no other offer was chosen, whatever was clicked first. */
74583
+ const r=a.reactivation;if(r&&r.list){const off=r.offer&&r.offer.text?r.offer:(r._typed?{from:"typed",text:r._typed}:{});const x={list:r.list,offer:off,channel:r.channel};if(r.list==="in_account_not_booked")x.notBookedDays=Number(r.notBookedDays);if(r.textConsentConfirmed)x.textConsentConfirmed=true;o.reactivation=x}return o}
74584
+ function asksBox(id,v,getTicked,keep){
74585
+ const box=mk("div","cds-stack ask-first");box.id="asks-"+id;const ia=v.intakeAsks||{};const savedJson=JSON.stringify(v.asks||{});
74586
+ if(!asksDraft[id]||asksDraft[id].base!==savedJson)asksDraft[id]={base:savedJson,a:JSON.parse(savedJson),change:false};
74587
+ const d=asksDraft[id],a=d.a;
74588
+ const radio=(name,val,lab,on,dis,why,act)=>{const l=mk("label","cds-check");const r=document.createElement("input");r.type="radio";r.name=name;r.checked=on;r.disabled=!!dis;r.onchange=act;l.appendChild(r);const s=mk("span","");s.appendChild(mk("b","",lab));if(why)s.appendChild(mk("small","muted",why));l.appendChild(s);return l};
74589
+ function draw(){box.innerHTML="";const t=getTicked();
74590
+ if(!(t.has("sms")||t.has("missed")||t.has("reactivation"))){box.hidden=true;return}box.hidden=false;
74591
+ box.appendChild(mk("h4","cds-panel-label","Confirm with your client before Build"));
74592
+ box.appendChild(mk("p","","Ask your client these, then choose. Nothing is built for a question left blank; it waits for your client's answer."));
74593
+ box.appendChild(mk("p","muted","The answers save with Save what they bought. Changing one after the plan is approved means approving it again."));
74594
+ const texting=a.texting||(ia.sms===true?"yes":ia.sms===false?"not_yet":"");
74595
+ const tx=mk("div","cds-stack");tx.appendChild(mk("b","","Texting"));tx.appendChild(mk("p","","Can this business text leads?"));
74596
+ if(!a.texting&&ia.sms!==undefined&&!d.change){const p=mk("p","","Your client's form said: "+(ia.sms?"Yes":"Not yet")+" ");const ch=mk("button","cds-link","Change");ch.type="button";ch.onclick=()=>{d.change=true;draw()};p.appendChild(ch);tx.appendChild(p)}
74597
+ else{const row=mk("div","cds-row");row.appendChild(radio("tx-"+id,"yes","Yes",a.texting==="yes",false,"",()=>{a.texting="yes";keep();draw()}));row.appendChild(radio("tx-"+id,"not_yet","Not yet",a.texting==="not_yet",false,"",()=>{a.texting="not_yet";keep();draw()}));tx.appendChild(row)}
74598
+ if(ia.phone==="yes")tx.appendChild(mk("small","muted","Hint: your client's form said they have a phone number in GoHighLevel."));
74599
+ box.appendChild(tx);
74600
+ if(t.has("missed")){const ca=mk("div","cds-stack");ca.appendChild(mk("b","","Call answering"));ca.appendChild(mk("p","","What did your client pay for? Choose all that apply."));
74601
+ const cur=new Set(a.callAnswering||[]);
74602
+ const opt=(val,lab,sub,why,greyed)=>{const l=mk("label","cds-check");const c=document.createElement("input");c.type="checkbox";c.checked=cur.has(val);c.disabled=greyed&&!cur.has(val);c.onchange=()=>{if(c.checked)cur.add(val);else cur.delete(val);a.callAnswering=[...cur];keep();draw()};l.appendChild(c);const s=mk("span","");s.appendChild(mk("b","",lab));if(sub)s.appendChild(mk("small","",sub));if(why)s.appendChild(mk("small","muted",why));l.appendChild(s);ca.appendChild(l)};
74603
+ opt("live_voice","An AI answers the phone live","Your client connects their number to it afterwards. Once it is connected, GoHighLevel charges for every minute it talks, usually from your agency's wallet.","",false);
74604
+ opt("chat_bot","An AI replies by text or chat and books the visit","",texting==="yes"?"":"Texting replies need Texting: Yes.",false);
74605
+ opt("missed_text","A text back after a missed call","",texting==="yes"?"":"Needs texting: Yes",texting!=="yes");
74606
+ box.appendChild(ca)}
74607
+ if(t.has("reactivation")){const re=a.reactivation||(a.reactivation={});const rv=mk("div","cds-stack");rv.appendChild(mk("b","","Reactivation"));
74608
+ rv.appendChild(mk("p","","Who should it reach?"));
74609
+ rv.appendChild(radio("rl-"+id,"import","A list your client will give you to import",re.list==="import",false,"",()=>{re.list="import";keep();draw()}));
74610
+ /* rc.3 c6 hole 4: the number is in the sentence as text, and its box sits on its own labelled line. */
74611
+ const nDays=re.notBookedDays||90;
74612
+ rv.appendChild(radio("rl-"+id,"in_account_not_booked","Contacts already here who have not booked in "+nDays+" days",re.list==="in_account_not_booked",false,"",()=>{re.list="in_account_not_booked";if(!re.notBookedDays)re.notBookedDays=90;keep();draw()}));
74613
+ if(re.list==="in_account_not_booked"){const row=mk("label","cds-row");row.appendChild(mk("span","","Days with no booking: "));const days=document.createElement("input");days.type="number";days.min="1";days.max="3650";days.className="cds-input";days.style.width="6em";days.value=String(nDays);days.onchange=()=>{const n=Math.round(Number(days.value));if(n>=1&&n<=3650){re.notBookedDays=n;keep();draw()}else days.value=String(nDays)};row.appendChild(days);rv.appendChild(row);
74614
+ rv.appendChild(mk("small","muted","You will see how many people this reaches and approve it before anyone gets a message."))}
74615
+ rv.appendChild(radio("rl-"+id,"cold_from_now","Only leads who go cold from now on",re.list==="cold_from_now",false,"",()=>{re.list="cold_from_now";keep();draw()}));
74616
+ rv.appendChild(mk("p","","What offer brings them back?"));
74617
+ /* W44: with no offer in the brief, typing one is the only choice, so it starts chosen. */
74618
+ if(!ia.offer&&!re.offer)re.offer={from:"typed",text:""};
74619
+ if(ia.offer)rv.appendChild(radio("ro-"+id,"brief","From the brief: \\u201c"+ia.offer+"\\u201d",re.offer&&re.offer.from==="brief",false,"",()=>{re.offer={from:"brief",text:ia.offer};keep();draw()}));
74620
+ const typed=radio("ro-"+id,"typed","Type one:",re.offer&&re.offer.from==="typed",false,"",()=>{re.offer={from:"typed",text:ot.value.trim()};keep();draw()});
74621
+ /* W45 (Jerry, Safari): inside the label the box drew as a tiny oval. It is its own full-width field under its choice. */
74622
+ const ot=document.createElement("textarea");ot.rows=2;ot.maxLength=300;ot.className="cds-input ask-offer";ot.style.width="100%";ot.style.boxSizing="border-box";ot.placeholder="e.g. a free skin check";ot.value=re.offer&&re.offer.from==="typed"?re.offer.text:"";ot.setAttribute("aria-label","The offer, in your client's words");ot.oninput=()=>{re._typed=ot.value.trim();re.offer={from:"typed",text:re._typed};keep();typed.querySelector("input").checked=true};
74623
+ rv.appendChild(typed);rv.appendChild(ot);
74624
+ rv.appendChild(mk("p","","How?"));
74625
+ const canText=texting==="yes"&&re.textConsentConfirmed===true;
74626
+ rv.appendChild(radio("rc-"+id,"email","Email",re.channel==="email",false,"",()=>{re.channel="email";keep();draw()}));
74627
+ rv.appendChild(radio("rc-"+id,"email_and_text","Email and text",re.channel==="email_and_text",!canText&&re.channel!=="email_and_text",texting!=="yes"?"Needs texting: Yes":re.textConsentConfirmed?"":"Needs your client to confirm these people agreed to get texts",()=>{re.channel="email_and_text";keep();draw()}));
74628
+ const cl=mk("label","cds-check");const cc=document.createElement("input");cc.type="checkbox";cc.checked=re.textConsentConfirmed===true;cc.onchange=()=>{if(cc.checked)re.textConsentConfirmed=true;else{delete re.textConsentConfirmed;if(re.channel==="email_and_text")re.channel="email"}keep();draw()};cl.appendChild(cc);cl.appendChild(mk("span","","My client confirmed these people agreed to get texts"));rv.appendChild(cl);
74629
+ box.appendChild(rv)}}
74630
+ draw();box.redraw=draw;return box}
74029
74631
  const BOUGHT_ITEMS=${scriptJson(boughtItemsFor(FEATURE_IDS))};
74030
74632
  function scopeChosen(state,id){const s=(state.scope||{})[id];const v=(state.boughtViews||{})[id];return Boolean(s&&(s.wholePreset||(Array.isArray(s.features)&&s.features.length)))&&!(v&&v.ready===false)}
74031
74633
  function openWhatTheyBought(id){rOpen[id]=true;rSeen[id]=true;const go=()=>{render(lastState,lastRun);requestAnimationFrame(()=>{const t=document.getElementById("wtb-"+id);if(t){t.scrollIntoView({behavior:"smooth",block:"start"});const f=t.querySelector("button,input,select");if(f)f.focus({preventScroll:true})}})};if(!rData[id])rLoad(id).then(go,go);else go()}
@@ -74048,7 +74650,7 @@ function scopeBlock(id,state){
74048
74650
  const v=boughtView(state,id);
74049
74651
  if(!bsHideAssess)sc.appendChild(assessmentQuestion(id,v));
74050
74652
  sc.appendChild(mk("h4","cds-panel-label","What they bought"));
74051
- sc.appendChild(mk("p","",v.source==="all"?"Everything starts ticked. Untick what this client did not buy.":v.source==="assessment"?"Ticked from what the Assessment recorded as bought. Untick anything they did not buy.":"Tick what the client bought. Only the items marked Built get built."));
74653
+ sc.appendChild(mk("p","",v.source==="all"?"Everything starts ticked. Untick what this client did not buy.":v.source==="assessment"?"Ticked from what the Assessment recorded as bought. Untick anything they did not buy.":"Tick what the client bought. Only the items marked Will build get built."));
74052
74654
  if(!profCache){profLoad();const p=document.createElement("p");p.className="muted";p.textContent="Loading your packages\u2026";sc.appendChild(p);return sc}
74053
74655
  const saved=(state.scope||{})[id]||null;const pk=(profCache.packages||[]);
74054
74656
  const sel=document.createElement("select");sel.className="cds-select";sel.setAttribute("aria-label","Package they bought");
@@ -74059,17 +74661,17 @@ function scopeBlock(id,state){
74059
74661
  const keepScope=()=>{scopeDraft[id]={ticked:[...ticked],pkg:sel.value,base:base};unsaved.hidden=false};
74060
74662
  const everything=document.createElement("button");everything.className="cds-btn ghost";everything.textContent="Tick everything";
74061
74663
  everything.title="Ticks all fourteen. Nothing is saved until you press Save what they bought.";
74062
- everything.onclick=()=>{ticked=new Set(v.items.map(i=>i.id));keepScope();drawBoxes();drawFigs()};
74664
+ everything.onclick=()=>{ticked=new Set(v.items.map(i=>i.id));keepScope();drawBoxes();drawFigs();if(askEl)askEl.redraw()};
74063
74665
  sc.appendChild(everything);
74064
74666
  const boxes=document.createElement("div");boxes.className="cds-stack";
74065
74667
  function drawBoxes(){boxes.innerHTML="";for(const it of v.items){const l=document.createElement("label");l.className="cds-check";
74066
- l.dataset.k=it.id;const c=document.createElement("input");c.type="checkbox";c.checked=ticked.has(it.id);c.onchange=()=>{if(c.checked)ticked.add(it.id);else ticked.delete(it.id);keepScope();drawFigs()};l.appendChild(c);
74668
+ l.dataset.k=it.id;const c=document.createElement("input");c.type="checkbox";c.checked=ticked.has(it.id);c.onchange=()=>{if(c.checked)ticked.add(it.id);else ticked.delete(it.id);keepScope();drawFigs();if(askEl)askEl.redraw()};l.appendChild(c);
74067
74669
  const t=document.createElement("span");const tb=document.createElement("b");tb.textContent=it.name;t.appendChild(tb);t.appendChild(mk("small","",it.means));
74068
- t.appendChild(mk("small","bought-delivery",it.delivery));if(it.next)t.appendChild(mk("small","bought-next",it.next));l.appendChild(t);boxes.appendChild(l)}}
74670
+ t.appendChild(mk("small","bought-delivery",it.delivery));if(it.next&&!(((asksDraft[id]&&asksDraft[id].a.callAnswering)||(v.asks||{}).callAnswering||[]).includes("live_voice")))t.appendChild(mk("small","bought-next",it.next));l.appendChild(t);boxes.appendChild(l)}}
74069
74671
  const figs=((state.assessmentFigures||{})[id])||{};
74070
74672
  function drawFigs(){for(const l of boxes.querySelectorAll("label")){const it=v.items.find(i=>i.id===l.dataset.k);const old=l.querySelector("em");if(old)old.remove();if(it&&it.feature&&figs[it.feature]){const e=document.createElement("em");e.className="cds-chip"+(ticked.has(it.id)?"":" warn");e.textContent="Assessment: "+figs[it.feature]+(ticked.has(it.id)?".":". Not ticked.");l.querySelector("span").appendChild(e)}}}
74071
74673
  /* A package names build features: it ticks their items and unticks the other built ones; the rest stay as they are. */
74072
- sel.onchange=()=>{const p=pk.find(x=>x.id===sel.value);if(p)for(const it of v.items){if(!it.feature)continue;if((p.features||[]).includes(it.feature))ticked.add(it.id);else ticked.delete(it.id)}keepScope();drawBoxes();drawFigs()};drawBoxes();drawFigs();
74674
+ sel.onchange=()=>{const p=pk.find(x=>x.id===sel.value);if(p)for(const it of v.items){if(!it.feature)continue;if((p.features||[]).includes(it.feature))ticked.add(it.id);else ticked.delete(it.id)}keepScope();drawBoxes();drawFigs();if(askEl)askEl.redraw()};let askEl=null;drawBoxes();drawFigs();askEl=asksBox(id,v,()=>ticked,keepScope);
74073
74675
  const note=document.createElement("p");note.className="muted";note.textContent="Always built, whatever is ticked: the pipeline and its stages, the fields, tags and custom values, the intake form, the calendar, and the reply routing. A ticked item that is not built automatically becomes one of your jobs in the account.";
74074
74676
  const sv=document.createElement("button");sv.className="cds-btn ghost";sv.textContent="Save what they bought";
74075
74677
  const status=document.createElement("p");status.className="muted";status.setAttribute("role","status");
@@ -74077,14 +74679,15 @@ function scopeBlock(id,state){
74077
74679
  if(!v.items.some(it=>it.kind==="built"&&ticked.has(it.id))){status.textContent="None of the ticked items is built for this client's industry. Tick at least one item marked Built.";return}
74078
74680
  /* Run 8 finding 9: about 5 seconds with nothing showing it was working. */
74079
74681
  sv.disabled=true;const was=sv.textContent;sv.textContent="Saving\u2026";status.textContent="Saving what they bought\u2026";
74080
- const body={locationId:id,bought:[...ticked]};if(sel.value)body.package=sel.value;const r=await postJSON("/api/scope",body);
74081
- if(r){unsaved.hidden=true;if(r.approvalWithdrawn)boughtNotice[id]=r.message;else delete boughtNotice[id];delete scopeDraft[id];delete assessmentCards[id];status.textContent="Saved.";load()}
74682
+ const body={locationId:id,bought:[...ticked]};if(sel.value)body.package=sel.value;if(asksDraft[id])body.asks=asksClean(asksDraft[id].a);const r=await postJSON("/api/scope",body);
74683
+ if(r){unsaved.hidden=true;delete asksDraft[id];if(r.approvalWithdrawn)boughtNotice[id]=r.message;else delete boughtNotice[id];delete scopeDraft[id];delete assessmentCards[id];status.textContent="Saved.";load()}
74082
74684
  if(sv.isConnected){sv.disabled=false;sv.textContent=was}};
74083
74685
  /* Audit 2026-10-28 #9: the always-built pipeline counts as built ("2 bought, 1 built" while it is always built). */
74084
- const nBought=v.ticks.length,nBuilt=v.items.filter(it=>(it.kind==="built"||it.kind==="always")&&v.ticks.includes(it.id)).length;
74085
- status.textContent=saved&&v.source==="saved"?"Saved: "+nBought+" of "+v.items.length+" bought, "+nBuilt+" of them built automatically"+(saved.package?(pk.find(x=>x.id===saved.package)?", from the "+pk.find(x=>x.id===saved.package).name+" package":", from a package no longer on Your agency"):"")+". The build follows this.":saved?(saved.wholePreset?"Saved: everything in the plan. The build follows this.":"Saved before this list: "+saved.features.length+" features. Save again to record what they bought."):"Not saved yet. Save what they bought before approving.";
74686
+ /* rc.3 c6 hole 2: the count follows the labels: what will be built (or was) and what waits on your client's answer. */
74687
+ const tickedItems=v.items.filter(it=>v.ticks.includes(it.id));const nBought=v.ticks.length,nBuilt=tickedItems.filter(it=>/^(Will build|Built|Always built): /.test(it.delivery)).length,nHeld=tickedItems.filter(it=>/^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /.test(it.delivery)).length;
74688
+ status.textContent=saved&&v.source==="saved"?"Saved: "+nBought+" of "+v.items.length+" bought. "+nBuilt+(nBuilt===1?" is":" are")+" built automatically"+(nHeld?", "+nHeld+(nHeld===1?" waits":" wait")+" for your client's answer":"")+(saved.package?(pk.find(x=>x.id===saved.package)?", from the "+pk.find(x=>x.id===saved.package).name+" package":", from a package no longer on Your agency"):"")+". The build follows this.":saved?(saved.wholePreset?"Saved: everything in the plan. The build follows this.":"Saved before this list: "+saved.features.length+" features. Save again to record what they bought."):"Not saved yet. Save what they bought before approving.";
74086
74689
  const unsaved=mk("span","cds-chip warn","Not saved yet");unsaved.hidden=!scopeDraft[id];
74087
- sc.appendChild(sel);sc.appendChild(boxes);sc.appendChild(note);sc.appendChild(sv);sc.appendChild(unsaved);sc.appendChild(status);
74690
+ sc.appendChild(sel);sc.appendChild(boxes);sc.appendChild(askEl);sc.appendChild(note);sc.appendChild(sv);sc.appendChild(unsaved);sc.appendChild(status);
74088
74691
  if(boughtNotice[id]){const n=mk("p","cds-callout warn",boughtNotice[id]);n.setAttribute("role","alert");sc.appendChild(n)}
74089
74692
  return sc}
74090
74693
  async function rLoad(id){const tok=clientToken();const r=await(await fetch("/api/review?locationId="+id)).json();rData[id]=r;renderFor(tok)}
@@ -74113,6 +74716,10 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
74113
74716
  rBusy[id]=false;load()};
74114
74717
  if(busy){const p=document.createElement("p");p.className="muted";
74115
74718
  p.textContent="Reading their answers and writing the review, usually 1 to 2 minutes. This panel updates itself when it is ready.";d.appendChild(p)}
74719
+ /* rc.3 c6 hole 1: a review written before the answers changed may promise what the build will not make. */
74720
+ if(r&&r.ok&&r.stale&&!busy){const w=mk("p","cds-callout warn","This review was written before your client's answers or what they bought changed, so it may promise what this build will not make. Press Regenerate before you share it; the client document waits for the new one.");w.setAttribute("role","alert");d.appendChild(w)}
74721
+ if(r&&r.ok&&!r.stale&&r.graded!==true&&!busy){const w=mk("p","cds-callout warn","Not checked for over-promises. Regenerate.");w.setAttribute("role","alert");d.appendChild(w)}
74722
+ if(r&&r.ok&&Array.isArray(r.trimmed)&&r.trimmed.length&&!busy)d.appendChild(mk("p","muted","Taken out because they promised more than this build makes: "+r.trimmed.join("; ")+"."));
74116
74723
  if(!r||!r.ok){if(!busy){const p=document.createElement("p");if(r&&r.error&&r.error!=="not generated yet"){p.className="cds-callout warn";p.setAttribute("role","alert")}else p.className="muted";
74117
74724
  p.textContent=r&&r.error?r.error:"Turn their intake answers into strategy: who they really sell to, whether the offer holds up, what the account is missing, and what to build first.";
74118
74725
  d.appendChild(p)}
@@ -74139,15 +74746,16 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
74139
74746
  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);
74140
74747
  const gl=mk("ul","cds-list gaps");
74141
74748
  for(const g of r.gaps){const gg=mk("li","gap "+(g.priority||"soon"));
74142
- 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);
74749
+ const pri=g.priority||"soon";const gt=mk("span","ttl",String(g.title||""));gt.appendChild(mk("span","sub",moneyText(String(g.cost||""))));/* rc.5: the fix is never the model's words; what we build is listed below, from the build */gg.appendChild(gt);
74143
74750
  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)}
74144
74751
  s3.appendChild(gl);
74145
74752
  d.appendChild(s3)}
74146
- if((r.build||[]).length){const s4=document.createElement("div");s4.className="cds-stack";s4.innerHTML="<h4 class='cds-panel-label'>What we build</h4>";
74753
+ /* rc.5 (c6 ruling): What we build is the build's own will-build list and held lines, never the model's prose. */
74754
+ {const pr=r.promises;if(pr&&((pr.willBuild||[]).length||(pr.held||[]).length)){const s4=document.createElement("div");s4.className="cds-stack";s4.innerHTML="<h4 class='cds-panel-label'>What we build</h4>";
74147
74755
  const bl=document.createElement("ul");bl.className="cds-list";
74148
- for(const b of r.build){const bb=document.createElement("li");bb.className="buildline";
74149
- const bt2=mk("span","ttl",String(b.item||""));bt2.appendChild(mk("span","sub",moneyText(b.because||"")));bb.appendChild(bt2);bl.appendChild(bb)}
74150
- s4.appendChild(bl);d.appendChild(s4)}
74756
+ for(const w of (pr.willBuild||[])){const bb=document.createElement("li");bb.className="buildline";bb.appendChild(mk("span","ttl",String(w)));bl.appendChild(bb)}
74757
+ for(const h of (pr.held||[])){const bb=document.createElement("li");bb.className="buildline";bb.appendChild(mk("span","ttl muted",String(h)));bl.appendChild(bb)}
74758
+ s4.appendChild(bl);d.appendChild(s4)}}
74151
74759
  if((r.unknowns||[]).length){const s5=document.createElement("div");s5.className="cds-stack";s5.innerHTML="<h4 class='cds-panel-label'>Ask them next</h4>";
74152
74760
  const p=document.createElement("p");p.textContent=r.unknowns.join(" \xB7 ");s5.appendChild(p);d.appendChild(s5)}
74153
74761
  d.appendChild(scopeBlock(id,state));
@@ -74256,6 +74864,8 @@ function unsavedTyping(){try{
74256
74864
  return [...m.querySelectorAll("input,textarea")].some(el=>textField(el)&&!el.disabled&&typedKeys.has(fieldKey(el,m))&&String(el.value||"").trim())}catch(e){return false}}
74257
74865
  /* A write in flight arms it too: a link that leaves the page would cut it off. */
74258
74866
  window.addEventListener("beforeunload",e=>{if(liveWrites>0||unsavedTyping()){e.preventDefault();e.returnValue=""}});
74867
+ /* Walk W27: the folded Start a new client stays open across redraws once the member opened it. */
74868
+ let ncFoldOpen=false;
74259
74869
  function newClientForm(){const f=document.createElement("div");f.className="cds-panel ncform";
74260
74870
  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>";
74261
74871
  const g=document.createElement("div");g.className="ncgrid";
@@ -75178,7 +75788,26 @@ function capEmpty(title,text){const e=mk("div","cds-empty");e.appendChild(mk("h3
75178
75788
  /* Capability jobs that read and change nothing (audit 2026-10-28 #22). */
75179
75789
  /* Payments makes catalog products from the approved plan's package names, and nothing without them (audit #18). */
75180
75790
  function capBuildNote(){return CAP.key==="payments"?"Uses the package names in the approved plan. With none approved, it creates nothing and says so.":""}
75181
- function capBuildLabel(c){return CAP.key==="checkup"?"Run the checkup":CAP.key==="payments"?"Prepare the three packages in the catalog":CAP.key==="contacts"?"Find and review everyone who has not booked in 90 days":"Build this for "+c.name}
75791
+ /* Walk W41: what the main button does, in plain words ("Build this for <client>" alone said nothing). */
75792
+ const CAP_ACTION={pipelines:"build the pipelines and stages",calendars:"build the booking calendars",forms:"build the forms",pages:"build the pages and funnels",fields:"add the fields, values and tags",staff:"check the staff and users",phone:"check the phone numbers and A2P",workflows:"build the workflows as drafts",reactivation:"build the reactivation campaign as drafts",templates:"write the email and SMS templates",broadcasts:"draft the broadcast emails","conversation-ai":"build the Conversation AI bot","voice-ai":"build the Voice AI agent",payments:"prepare the three packages in the catalog",memberships:"build the course and membership",reviews:"build the review requests as drafts",blogs:"draft the blog post",checkup:"run the checkup",contacts:"find everyone who has not booked in 90 days",integrations:"check the integrations and webhooks"};
75793
+ function capBuildLabel(c){return "Have Claude "+(CAP_ACTION[CAP.key]||"do this for "+c.name)}
75794
+ /* The main button (top and bottom alike): opens the one before-panel beside itself. */
75795
+ function capMainButton(c,words,busy){const b=mk("button","cds-btn cap-build",capBuildLabel(c));b.type="button";if(busy)b.disabled=true;b.onclick=()=>capAsk(c,words,b);return b}
75796
+ /* The W8 before-panel, one at a time, beside the button pressed; nothing runs until Yes. */
75797
+ function capAsk(c,words,btn){for(const x of document.querySelectorAll(".cap-ask"))x.remove();
75798
+ const ask=mk("div","cds-callout cap-ask");ask.setAttribute("role","group");ask.appendChild(mk("span","ttl","Before Claude starts: this works in "+c.name));
75799
+ ask.appendChild(mk("p","",words));const before=String(CAP.before||"").split("{client}").join(c.name);if(before)ask.appendChild(mk("p","",before));
75800
+ const row=mk("div","cds-actions");const yes=mk("button","cds-btn","Yes, "+capBuildLabel(c).charAt(0).toLowerCase()+capBuildLabel(c).slice(1));yes.type="button";const no=mk("button","cds-btn ghost","Cancel");no.type="button";
75801
+ no.onclick=()=>ask.remove();yes.onclick=async()=>{yes.disabled=no.disabled=true;yes.textContent="Starting\u2026";await capStart(c,ask)};row.appendChild(yes);row.appendChild(no);ask.appendChild(row);
75802
+ const host=btn.parentNode;host.insertBefore(ask,btn.nextSibling);if(ask.scrollIntoView)try{ask.scrollIntoView({block:"nearest"})}catch(e){}}
75803
+ async function capStart(c,ask){const tok=clientToken();
75804
+ const shownReads=capReadsFor===c.locationId&&capReads&&capReads.ok&&Array.isArray(capReads.fingerprints)?capReads.fingerprints:null;
75805
+ const r=await(await fetch("/api/run-capability",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:c.locationId,key:CAP.key,confirmed:true,readsShown:shownReads})})).json();
75806
+ ask.remove();
75807
+ /* Out of date (safety release): the panel shows what the account has now, and the refusal says what changed. */
75808
+ if(r.stale&&Array.isArray(r.reads)){if(!stillFor(tok)){alertFor(c.locationId,r.error);return}capReadsFor=c.locationId;capReads={ok:true,key:CAP.key,locationId:c.locationId,reads:r.reads,fingerprints:r.fingerprints};render(lastState,lastRun);alertFor(c.locationId,r.error);return}
75809
+ if(!r.ok){alertFor(c.locationId,r.error||"Could not start");return}
75810
+ if(!stillFor(tok))return;capRun={locationId:c.locationId};pollCap()}
75182
75811
  /* What one read counts, in the owner's words and the right number: "1 bot", "2 bots". */
75183
75812
  function readLabel(label,n){if(Number(n)!==1)return label;const r=((CAP&&CAP.reads)||[]).find(x=>x.label===label);return (r&&r.one)||String(label).replace(/s$/,"")}
75184
75813
  /* One row per thing the read found: its name, and the sub line the read gives (amount, status and date; N stages). */
@@ -75201,22 +75830,17 @@ function renderCapability(m,state,run){
75201
75830
  sel.setAttribute("data-switch","");sel.onchange=()=>goClient(sel.value);pick.appendChild(sel);end.appendChild(pick);
75202
75831
  const hasBuild=CAP.key!=="intake"&&CAP.key!=="handoff"&&!CAP.noBuild;
75203
75832
  let bb=null;
75204
- if(hasBuild){bb=mk("button","cds-btn cap-build",capBuildLabel(c));if(busy)bb.disabled=true;
75205
- /* Audit 2026-10-28 #22: a read-only job never claims to create drafts. */
75206
- bb.onclick=async()=>{if(!confirm("For "+c.name+": "+words+String.fromCharCode(10)+String(CAP.before||"").split("{client}").join(c.name)+String.fromCharCode(10)+"Go ahead?"))return;bb.disabled=true;bb.textContent="Starting\u2026";
75207
- const tok=clientToken();
75208
- const shownReads=capReadsFor===c.locationId&&capReads&&capReads.ok&&Array.isArray(capReads.fingerprints)?capReads.fingerprints:null;
75209
- const r=await(await fetch("/api/run-capability",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:c.locationId,key:CAP.key,confirmed:true,readsShown:shownReads})})).json();
75210
- /* Out of date (safety release): the panel shows what the account has now, and the refusal says what changed. */
75211
- if(r.stale&&Array.isArray(r.reads)){if(!stillFor(tok)){alertFor(c.locationId,r.error);return}capReadsFor=c.locationId;capReads={ok:true,key:CAP.key,locationId:c.locationId,reads:r.reads,fingerprints:r.fingerprints};render(lastState,lastRun);alertFor(c.locationId,r.error);return}
75212
- if(!r.ok){alertFor(c.locationId,r.error||"Could not start");bb.disabled=false;bb.textContent=capBuildLabel(c);return}
75213
- if(!stillFor(tok))return;capRun={locationId:c.locationId};pollCap()};
75214
- end.appendChild(bb);if(capBuildNote())end.appendChild(mk("p","muted cap-note",capBuildNote()))}
75833
+ /* Walk W41 (2026-10-29, Jerry: "it is not clear at all"): the page's main action is the plain-worded build through
75834
+ the member's own Claude, at the top here and again at the bottom after Ready to answer; both open the one W8
75835
+ before-panel beside the button pressed (never a browser dialog). */
75836
+ if(hasBuild){bb=capMainButton(c,words,busy);end.appendChild(bb);if(capBuildNote())end.appendChild(mk("p","muted cap-note",capBuildNote()))}
75215
75837
  /* Walk 2026-10-28 W9: the client's first page (where Your clients leads) has its build as a button. */
75216
75838
  if(CAP.key==="intake"){const go=mk("a","cds-btn cap-build-account","Build this client's account");end.appendChild(asClientLink(go,"/new",c.locationId))}
75217
75839
  m.appendChild(screenHead(CAP.label,CAP.what,CAP.groupLabel,end));
75218
75840
  const sc=document.getElementById("side-client");if(sc)sc.textContent=c.name;
75219
- if(CAP.key==="intake"){renderIntakeCapability(m,c);return}
75841
+ /* W43 (Jerry's watched walk, 2026-10-29): he saw no Build button on the page Your clients leads to. The header carries
75842
+ one; this puts the next step at the top of the page itself, where nobody can miss it. */
75843
+ if(CAP.key==="intake"){const nx=mk("div","cds-callout cap-next-build");nx.appendChild(mk("span","ttl","Next step: build this client's account"));nx.appendChild(mk("span","","Answers, approval and the build itself are on the build page."));const go=mk("a","cds-btn cap-build-account-main","Build this client's account");nx.appendChild(asClientLink(go,"/new",c.locationId));m.appendChild(nx);renderIntakeCapability(m,c);return}
75220
75844
  const built=capBuiltItems(view,CAP.key);
75221
75845
  const toCheck=built.filter(x=>x.status==="uncertain");
75222
75846
  const health=HEALTH_PAGES.includes(CAP.key);
@@ -75257,7 +75881,25 @@ function renderCapability(m,state,run){
75257
75881
  if(health)m.appendChild(capOwedPanel(owed,(state.jobCounts||{})[c.locationId],((view||{}).handoff||{}).lines))}
75258
75882
  /* What this page reads live: one list for one read. With several reads, a read of 3 or fewer shows its rows under its own label,
75259
75883
  and a longer one folds under a closed "See the N \u2026". A count read is one row: the number. */
75884
+ /* Sign-off and handoff (command-center 2026-10-29): read from this client's own build record, the same one the build
75885
+ steps show (F2): what is Built, what waits on the client (the held lines), and the jobs still open. Never GoHighLevel's
75886
+ words for it, and never a claim the record does not hold. */
75887
+ const HELD_RE=/^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
75888
+ function capHandoffRecord(card,c){
75889
+ const v=((lastState&&lastState.boughtViews)||{})[c.locationId];
75890
+ if(!v){card.appendChild(mk("p","muted","Nothing is on record for this client yet."));return}
75891
+ const ticked=new Set(v.ticks||[]);const items=(v.items||[]).filter(i=>ticked.has(i.id));
75892
+ const built=items.filter(i=>/^Built: /.test(i.delivery)).map(i=>i.delivery.replace(/^Built: /,""));
75893
+ const waiting=items.filter(i=>HELD_RE.test(i.delivery)).map(i=>i.delivery);
75894
+ const open=(((lastState&&lastState.manualTasks)||{})[c.locationId]||[]).filter(t=>!t.claimed&&!HELD_RE.test(String(t.text))).map(t=>String(t.text));
75895
+ const sec=(title,list,none)=>{const b=mk("div","cap-read");b.appendChild(mk("b","",title));if(!list.length)b.appendChild(mk("p","muted",none));else{const u=mk("ul","");for(const x of list.slice(0,8))u.appendChild(mk("li","",x));if(list.length>8)u.appendChild(mk("li","muted","and "+(list.length-8)+" more"));b.appendChild(u)}card.appendChild(b)};
75896
+ card.appendChild(mk("p","muted","From this client's build record on this computer, the same record the build steps show."));
75897
+ sec("Built",built,"Nothing is recorded as built yet.");
75898
+ sec("Waiting on your client",waiting,"Nothing is waiting on your client.");
75899
+ sec("Still open",open,"No open jobs.");
75900
+ }
75260
75901
  function capReadsInto(card,c,hasBuild,foldAll){
75902
+ if(CAP.key==="handoff"){capHandoffRecord(card,c);return}
75261
75903
  if(!CAP.reads.length){card.appendChild(capEmpty("This page does not read the account yet","Say the words below to Claude, and it reads what this account has for this job, live."));return}
75262
75904
  if(capReadsFor!==c.locationId){card.appendChild(mk("p","muted","Reading this account\u2026"));loadCapReads(c.locationId);return}
75263
75905
  if(!capReads){card.appendChild(mk("p","muted","Reading this account\u2026"));return}
@@ -75267,7 +75909,7 @@ function capReadsInto(card,c,hasBuild,foldAll){
75267
75909
  const stack=mk("div","cds-stack");const several=foldAll||capReads.reads.length>1;
75268
75910
  for(const r of capReads.reads){const block=mk("div","cap-read");
75269
75911
  if(r.status!=="ok"){const w=mk("div","cds-callout warn");w.appendChild(mk("span","ttl",r.countOnly?(capFirst(readLabel(r.label,1))+" count not read yet"):"Could not read the "+r.label));w.appendChild(mk("span","",String(r.reason||"GoHighLevel did not answer.").replace(/^could not read: /,"")));block.appendChild(w)}
75270
- else if(!r.count)block.appendChild(capEmpty("No "+r.label+" yet",hasBuild?"Use Build this above, or say the words below to Claude.":"Say the words below to Claude."));
75912
+ else if(!r.count)block.appendChild(capEmpty("No "+r.label+" yet",hasBuild?"Use "+capBuildLabel(c)+" above, or ask Claude yourself with the words below.":"Ask Claude yourself with the words below."));
75271
75913
  else if(r.countOnly){const ul=mk("ul","cds-list");ul.appendChild(capRow(Number(r.count).toLocaleString("en-US")+" "+readLabel(r.label,r.count),"Counted by GoHighLevel."));block.appendChild(ul)}
75272
75914
  else if(!(r.names||[]).length&&!(r.items||[]).length)block.appendChild(mk("p","muted",r.count+" "+readLabel(r.label,r.count)+" found; GoHighLevel returned no names for them."));
75273
75915
  else if(several&&!foldAll&&r.count<=3){block.appendChild(mk("div","cds-panel-label",r.count+" "+readLabel(r.label,r.count)));const ul=mk("ul","cds-list");readRows(r).forEach(x=>ul.appendChild(x));block.appendChild(ul)}
@@ -75293,21 +75935,44 @@ function capCheckupPanel(last,bb){const p=panel("Last checkup",last?"What the la
75293
75935
  row.appendChild(mk("span","",last.summary||(last.ok?"The checkup finished.":(last.error||"The run did not finish."))));p.appendChild(row);
75294
75936
  if(iss.length)p.appendChild(mk("p","muted","Each one is listed under Still owed, below."));
75295
75937
  return p}
75938
+ /* Walk W42: Ready to answer, from the account as read now (the page's live reads) and the intake. */
75939
+ function capReadiness(c,state,last){const reads=capReadsFor===c.locationId&&capReads&&capReads.ok?capReads.reads:null;
75940
+ const fromRead=(label,none)=>{if(!reads)return {wait:true,text:"Reading the account\u2026"};const r=reads.find(x=>x.label===label);if(!r)return {wait:true,text:"Reading the account\u2026"};
75941
+ if(r.status!=="ok")return {text:"Could not read: "+(r.reason||"GoHighLevel did not answer")};
75942
+ const names=(r.names||[]).filter(Boolean);if(!r.count)return {text:none};
75943
+ return {ok:true,text:names.length?names.slice(0,2).join(", ")+(r.count>2?", and "+(r.count-2)+" more":""):r.count+" in this account"}};
75944
+ const ans=Object.assign({},((state.intakeDrafts||{})[c.locationId]||{}).answers||{},((state.intakeInputs||{})[c.locationId]||{}).answers||{},((state.intakeCompleted||{})[c.locationId]||{}).answers||{});
75945
+ const raw=ans.website??ans.Website;const site=String(raw&&typeof raw==="object"?(raw.value??""):(raw??"")).trim();
75946
+ const said=last&&last.readiness&&last.readiness["Test conversation"];
75947
+ return {Website:site?{ok:true,text:site}:{text:"No website in the intake yet. Add it on step 5, Intake."},
75948
+ "Booking calendar":fromRead("booking calendars","No booking calendar in this account yet."),
75949
+ "Phone number":fromRead("phone numbers","No phone number in this account yet."),
75950
+ "Test conversation":said?{ok:true,text:said}:{text:"Yours to do: have one test conversation with it."}}}
75296
75951
  /* Do it now: the words, the ways to run them, and what the last run left. */
75297
75952
  function capDoPanel(c,state,run,words,hasBuild,busy,last){
75298
- const doc=panel("Do it now","What to say to Claude");doc.classList.add("cap-do");
75953
+ const doc=panel("Do it now",hasBuild?capBuildLabel(c):"Optional help");doc.classList.add("cap-do");
75954
+ /* Walk W41: asking Claude yourself is the optional path, folded to one line; on a page with no build it is the only
75955
+ action, open, and still labelled as optional help. */
75956
+ const fold=mk("details","cds-details cap-words-fold");if(!hasBuild)fold.open=true;
75957
+ fold.appendChild(mk("summary","",hasBuild?"Prefer to ask Claude yourself? Copy the words":"Optional help: ask Claude yourself. Copy the words"));
75299
75958
  const q=mk("div","cds-callout say cap-words");q.appendChild(mk("span","",words));
75300
75959
  const acts=mk("div","cds-actions");
75301
75960
  const cp=mk("button","cds-btn ghost","Copy the words");cp.onclick=async()=>{try{await navigator.clipboard.writeText(words);cp.textContent="Copied"}catch(e){cp.textContent="Select and copy the text above"}setTimeout(()=>{cp.textContent="Copy the words"},2000)};acts.appendChild(cp);
75302
75961
  if(CAP.hub){const h=mk("a","cds-btn ghost","Step by step in the hub");h.href=HUB+CAP.hub;h.target="_blank";h.rel="noopener";acts.appendChild(h)}
75303
- q.appendChild(acts);doc.appendChild(q);
75962
+ q.appendChild(acts);fold.appendChild(q);
75304
75963
  const drafts=DRAFT_PAGES.includes(CAP.key);
75305
75964
  doc.appendChild(mk("p","muted cap-note",CAP.noBuild?"This one messages a real person, so it has no Build button: paste the words into your own Claude and review the complete draft before approving any send or publication.":!hasBuild?"Paste the words into your own Claude and watch it work.":busy?"Another run is in progress, and Build this wakes up when it finishes.":drafts?"This runs through your own Claude account, leaves what it makes as drafts, and sends, posts or deletes nothing.":"This runs through your own Claude account and sends, posts or deletes nothing."));
75306
75965
  if(hasBuild&&!busy){const d=mk("details","cds-details");d.appendChild(mk("summary","","Details"));d.appendChild(mk("p","","This runs through your own Claude account, with only the tools this job needs: the words above and what it reads from this account go to Claude, and nowhere else. "+(drafts?"Workflows and messages are left as drafts for your review; nothing sends until you turn it on, and activation is approved separately. ":"")+"Nothing is sent, posted or deleted; every id is read from this account first. Or paste the words into your own Claude and watch it work."));doc.appendChild(d)}
75307
75966
  if(["conversation-ai","voice-ai"].includes(CAP.key)){const block=mk("div","cap-sub");block.appendChild(mk("div","cds-panel-label","Ready to answer"));
75308
- const checks=["Website","Booking calendar","Phone number","Test conversation"];const said=l=>last&&last.readiness&&last.readiness[l];const nOk=checks.filter(said).length;
75309
- block.appendChild(mk("p","cap-ready-sum",nOk===0?"None of the "+checks.length+" checks is verified yet.":nOk===checks.length?"All "+checks.length+" checks are verified.":nOk+" of the "+checks.length+" checks are verified."));
75310
- const dl=mk("dl","cds-kv cap-ready");for(const l of checks){dl.appendChild(mk("dt","",l));const dd=mk("dd");if(said(l))dd.textContent=said(l);else dd.appendChild(chip("warn","Not verified"));dl.appendChild(dd)}block.appendChild(dl);doc.appendChild(block)}
75967
+ /* Walk W42 (2026-10-29): the checks said "Not verified" for a calendar and number Demo-on-Demand has. Each now says
75968
+ what the account holds, read live like Verify (the page's own reads), or exactly what is missing. The test
75969
+ conversation stays the member's to do. */
75970
+ const checks=["Website","Booking calendar","Phone number","Test conversation"];const found=capReadiness(c,state,last);const nOk=checks.filter(l=>found[l]&&found[l].ok).length;
75971
+ block.appendChild(mk("p","cap-ready-sum",nOk===checks.length?"All "+checks.length+" are in place.":nOk+" of the "+checks.length+" are in place."));
75972
+ const dl=mk("dl","cds-kv cap-ready");for(const l of checks){dl.appendChild(mk("dt","",l));const dd=mk("dd");const f=found[l];if(f.ok)dd.textContent=f.text;else{dd.appendChild(chip(f.wait?"":"warn",f.wait?"Reading":"Missing"));dd.appendChild(document.createTextNode(" "+f.text))}dl.appendChild(dd)}block.appendChild(dl);doc.appendChild(block)}
75973
+ /* Walk W41: the main action again at the bottom, after what it checks; then the optional copy-the-words fold. */
75974
+ if(hasBuild&&CAP.key!=="checkup"){const row=mk("div","cds-actions cap-main-bottom");row.appendChild(capMainButton(c,words,busy));doc.appendChild(row)}
75975
+ doc.appendChild(fold);
75311
75976
  /* The run, live */
75312
75977
  if(run&&run.capability===CAP.key&&run.locationId===c.locationId){const d=mk("details","cds-details cap-run");d.open=Boolean(run.running);d.appendChild(mk("summary","","What Claude is doing"));
75313
75978
  const pre=mk("pre","cds-callout cap-log",(run.lines||[]).slice(-40).join(String.fromCharCode(10))||"Starting\u2026");d.appendChild(pre);doc.appendChild(d);
@@ -75325,8 +75990,11 @@ function capDoPanel(c,state,run,words,hasBuild,busy,last){
75325
75990
  checked" while step 10 said "GoHighLevel confirms 81 items. You confirmed 6 yourself."). One sentence, every surface. */
75326
75991
  /* Walk 2026-10-28 W18: "confirms 66 \u2026 covers 75" left nine items unexplained. The rest are named by state. */
75327
75992
  function handoffCountLine(lines,counts){const done=lines.filter(l=>l.state==="CHECKED").length,mine=(counts&&counts.ticked)||0;
75328
- const waiting=lines.filter(l=>l.state==="WAITING").length,unread=lines.length-done-waiting;
75329
- const rest=[unread?unread+" "+(unread===1?"was":"were")+" built but GoHighLevel could not confirm "+(unread===1?"it":"them")+" yet":"",waiting?waiting+" "+(waiting===1?"waits":"wait")+" on a step outside the account, such as an approval":""].filter(Boolean);
75993
+ const waiting=lines.filter(l=>l.state==="WAITING").length;
75994
+ /* Walk W33: the draft automations are built; they are off until the member switches them on, not unconfirmed. */
75995
+ const drafts=lines.filter(l=>l.state!=="CHECKED"&&l.state!=="WAITING"&&/draft/i.test(String(l.reason||""))).length;
75996
+ const unread=lines.length-done-waiting-drafts;
75997
+ const rest=[drafts?"the "+drafts+" "+plural(drafts,"automation")+" "+(drafts===1?"is":"are")+" built and switched off until you turn "+(drafts===1?"it":"them")+" on":"",unread?unread+" "+(unread===1?"was":"were")+" built but GoHighLevel could not confirm "+(unread===1?"it":"them")+" yet":"",waiting?waiting+" "+(waiting===1?"waits":"wait")+" on a step outside the account, such as an approval":""].filter(Boolean);
75330
75998
  return "GoHighLevel confirms "+done+" "+plural(done,"item")+". You confirmed "+mine+" yourself. The handoff document covers "+lines.length+" "+plural(lines.length,"item")+"."+(rest.length?" Of the other "+(lines.length-done)+", "+rest.join(", and ")+".":"")}
75331
75999
  /* The Health pages' own list: what is verified and what is still owed. */
75332
76000
  /* Counts the same jobs, the same way, as step 11 (/api/state jobCounts), and the handoff document separately in
@@ -75499,12 +76167,15 @@ function renderPage(state,run){if(state.roster)mergeRoster(state.roster);syncRev
75499
76167
  if(SCREEN==="new"){m.classList.add("ncflow");const head=screenHead(HEADS.new.title,HEADS.new.sub,HEADS.new.kicker);head.querySelector(".cds-page-sub").classList.add("nc-lead");m.appendChild(head);
75500
76168
  const choices=ROSTER.filter(c=>["client","sandbox"].includes(typeOf(state,c.locationId)));
75501
76169
  const c0=ROSTER.find(c=>c.locationId===newClientId);
75502
- {const wc=bsWaitingCard(state,run,c0?c0.locationId:"");if(wc&&(!c0||bsWhere(c0,state,run).step<3))m.appendChild(wc)}
76170
+ let waiting=null;{const wc=bsWaitingCard(state,run,c0?c0.locationId:"");if(wc&&(!c0||bsWhere(c0,state,run).step<3)){m.appendChild(wc);waiting=wc}}
75503
76171
  if(!c0){
75504
- if(canCreateSubAccounts===null)m.appendChild(agencyPlanQuestion());
75505
- else if(canCreateSubAccounts){m.appendChild(newClientForm());const fold=document.createElement("details");fold.className="cds-more";const title=document.createElement("summary");title.textContent="Register a sub-account you created in GoHighLevel";fold.appendChild(title);fold.appendChild(pasteKeyCard(true));m.appendChild(fold)}
75506
- else{m.appendChild(pasteKeyCard());const quiet=document.createElement("p");quiet.className="muted";
75507
- quiet.innerHTML="Your agency is on GoHighLevel's Agency Pro plan? <a class='cds-link' href='/agency#canCreateSubAccounts'>Turn on creating sub-accounts here</a>.";m.appendChild(quiet)}}
76172
+ /* Walk W27 (2026-10-29): with a build in progress the page led with an open Create form, and Jerry nearly filled it
76173
+ in. The builds in progress come first; starting a new client is folded under them. */
76174
+ let host=m;if(waiting){const more=document.createElement("details");more.className="cds-more nc-new-fold";more.open=ncCreating||ncFoldOpen;more.addEventListener("toggle",()=>{ncFoldOpen=more.open});const s=document.createElement("summary");s.textContent="Start a new client account instead";more.appendChild(s);m.appendChild(more);host=more}
76175
+ if(canCreateSubAccounts===null)host.appendChild(agencyPlanQuestion());
76176
+ else if(canCreateSubAccounts){host.appendChild(newClientForm());const fold=document.createElement("details");fold.className="cds-more";const title=document.createElement("summary");title.textContent="Register a sub-account you created in GoHighLevel";fold.appendChild(title);fold.appendChild(pasteKeyCard(true));host.appendChild(fold)}
76177
+ else{host.appendChild(pasteKeyCard());const quiet=document.createElement("p");quiet.className="muted";
76178
+ quiet.innerHTML="Your agency is on GoHighLevel's Agency Pro plan? <a class='cds-link' href='/agency#canCreateSubAccounts'>Turn on creating sub-accounts here</a>.";host.appendChild(quiet)}}
75508
76179
  if(planNotice){const note=document.createElement("p");note.className="cds-callout warn";note.setAttribute("role","alert");note.textContent=planNotice;m.appendChild(note)}
75509
76180
  if(c0&&registerNotice){const note=document.createElement("p");note.className="cds-callout info";note.setAttribute("role","status");note.textContent=registerNotice;m.appendChild(note)}
75510
76181
  const existing=document.createElement("a");existing.href="#existing-clients";existing.className="cds-link";existing.textContent=c0?"Work on a different client":"Already registered? Pick the client";
@@ -75914,6 +76585,14 @@ function createBriefRoutes(seatToken, port, plan, registry2, fromCache, viewer =
75914
76585
  return true;
75915
76586
  }
75916
76587
  if (url === "/brief") {
76588
+ if (plan === "pro" && !new URL(req.url || "/", "http://x").searchParams.get("client")) {
76589
+ const only = registry2?.listLocations() ?? [];
76590
+ if (only.length === 1) {
76591
+ res.writeHead(302, { Location: `/brief?client=${encodeURIComponent(only[0].locationId)}` });
76592
+ res.end();
76593
+ return true;
76594
+ }
76595
+ }
75917
76596
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
75918
76597
  const picked = plan === "pro" ? new URL(req.url || "/", "http://x").searchParams.get("client") ?? "" : "";
75919
76598
  const clientName = picked ? registry2?.listLocations().find((l) => l.locationId === picked)?.name : void 0;
@@ -76290,7 +76969,11 @@ async function runDashboard(argv) {
76290
76969
  }
76291
76970
  return { label: r.label, status: "ok", count: total2, names: [], items: [], countOnly: true };
76292
76971
  }
76293
- const items = findArray(raw, r.key);
76972
+ const all = findArray(raw, r.key);
76973
+ const items = all && r.activeOnly ? all.filter((i) => {
76974
+ const o = Object(i);
76975
+ return o.isActive !== false && o.deleted !== true;
76976
+ }) : all;
76294
76977
  if (!items) {
76295
76978
  logQuietly(`capability read ${cap2.key}/${r.label}: unrecognised shape from ${p}`);
76296
76979
  return { label: r.label, status: "unavailable", count: 0, names: [], reason: "could not read: the answer had a shape this page does not know" };
@@ -76413,7 +77096,7 @@ async function runDashboard(argv) {
76413
77096
  }
76414
77097
  if (req.method === "GET" && url === "/shopper") {
76415
77098
  const accounts = currentRoster().filter((r) => readableAccount(r.locationId)).map((r) => ({ locationId: r.locationId, name: r.name })).sort((a, b) => a.name.localeCompare(b.name));
76416
- const client = accounts.find((a) => a.locationId === (u.searchParams.get("client") ?? ""));
77099
+ const client = accounts.find((a) => a.locationId === (u.searchParams.get("client") ?? "")) ?? (accounts.length === 1 && !u.searchParams.get("client") ? accounts[0] : void 0);
76417
77100
  const run = client ? latestRun(client.locationId) : void 0;
76418
77101
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
76419
77102
  res.end(shopperPageHtml({ accounts, client, run, ownerEmail: readAgencyProfile().sender?.replyTo, now: Date.now() }, mayServeSeat(req, port) ? seatToken : "", SEAT_TOKEN_HEADER));
@@ -76609,6 +77292,26 @@ async function runDashboard(argv) {
76609
77292
  }
76610
77293
  const ownerEmail = typeof input.ownerEmail === "string" ? input.ownerEmail : "";
76611
77294
  const testPhone = typeof input.testPhone === "string" && input.testPhone.trim() ? input.testPhone.trim() : void 0;
77295
+ const draftsTicked = liveWorkflows.filter((w) => workflowIds.includes(w.id) && (w.status ?? "").toLowerCase() === "draft");
77296
+ const draftRefusals = input.fastForward === true ? [] : draftsTicked.map((w) => `"${w.name}" is a draft, and a draft never runs: tick Fast-forward to test it.`);
77297
+ if (input.preview === true) {
77298
+ let refusals = draftRefusals;
77299
+ if (input.fastForward === true) {
77300
+ const rawHosts = Array.isArray(input.allowedHosts) ? input.allowedHosts.filter((h) => typeof h === "string") : [];
77301
+ try {
77302
+ const b = workflowReadConnection(registry2, c);
77303
+ refusals = await fastForwardRefusals({ client: c, admin: builderAdmin(b, publishCheckFor(c, b, { locationId: locationId2 })), locationId: locationId2, ownerEmail, alertUserId: typeof input.alertUserId === "string" && input.alertUserId ? input.alertUserId : void 0, workflowIds, allowedHosts: rawHosts.map(normalizeHost).filter(Boolean) });
77304
+ } catch (e) {
77305
+ refusals = [e instanceof Error ? e.message.split("\n")[0] : "The workflows could not be checked for fast-forward."];
77306
+ }
77307
+ }
77308
+ shopperJson(res, 200, { ok: true, preview: true, written: false, refusals });
77309
+ return;
77310
+ }
77311
+ if (draftRefusals.length) {
77312
+ shopperJson(res, 200, { ok: false, written: false, error: draftRefusals.join(" ") });
77313
+ return;
77314
+ }
76612
77315
  if (input.fastForward === true) {
76613
77316
  const rawHosts = Array.isArray(input.allowedHosts) ? input.allowedHosts.filter((h) => typeof h === "string") : typeof input.allowedHosts === "string" ? input.allowedHosts.split(/[\s,]+/) : [];
76614
77317
  const allowedHosts = rawHosts.map(normalizeHost).filter(Boolean);
@@ -76743,10 +77446,11 @@ async function runDashboard(argv) {
76743
77446
  const hit = await pickIntakeContact(c, locationId2, byId2, snap.contactId, INTAKE_FORM_NAME2);
76744
77447
  const completed = st0.intakeCompleted?.[locationId2]?.answers;
76745
77448
  const answers = Object.fromEntries(Object.entries(completed ?? normalizedIntake({ answers: hit?.answers ?? {}, answeredAt: snap.answeredAt }, st0.intakeDrafts?.[locationId2])).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : v]));
76746
- return generateReview(
77449
+ const review = await generateReview(
76747
77450
  (prompt) => callClaudeRecorded(claudeBin(), ["-p", prompt, "--allowedTools", "", "--disallowedTools", `Bash,Write,Edit,WebFetch,WebSearch,Task,Read,Glob,Grep,${MCP_DENY}`, "--max-turns", "6", ...NO_MCP_ARGS], { locationId: locationId2, stage: 1, kind: "review" }),
76748
- { clientName, industry: st0.accounts?.[locationId2]?.industry, answers, ...st0.assessmentAttachments?.[locationId2] ? { assessmentLines: findingLines(st0.assessmentAttachments[locationId2]) } : {} }
77451
+ { clientName, industry: st0.accounts?.[locationId2]?.industry, answers, ...st0.assessmentAttachments?.[locationId2] ? { assessmentLines: findingLines(st0.assessmentAttachments[locationId2]) } : {}, promises: reviewPromisesFor(st0, locationId2, locationId2) }
76749
77452
  );
77453
+ return review.ok ? { ...review, promisesHash: contentHash2(review.promises ?? reviewPromisesFor(st0, locationId2, locationId2)) } : review;
76750
77454
  };
76751
77455
  const currentRoster = () => {
76752
77456
  const registered = registry2.listLocations();
@@ -76989,12 +77693,13 @@ async function runDashboard(argv) {
76989
77693
  }
76990
77694
  if (req.method === "POST" && url === "/api/run-capability") {
76991
77695
  readBody(req, res, 1e4, async (body) => {
77696
+ let releaseStart;
76992
77697
  try {
76993
77698
  const { locationId: locationId2, key: key2, confirmed, readsShown } = JSON.parse(body || "{}");
76994
77699
  if (confirmed !== true) throw new Error("Review this action and approve it on the client page first.");
76995
77700
  const cap2 = capabilityByKey(String(key2 ?? ""));
76996
77701
  if (!cap2) throw new Error("no such capability");
76997
- if (activeRun2?.running) throw new Error("a run is already in progress");
77702
+ releaseStart = claimRunStart();
76998
77703
  const client = currentRoster().find((r) => r.locationId === locationId2);
76999
77704
  if (!client) throw new Error("that account is not on the board");
77000
77705
  if (cap2.reads.length) {
@@ -77047,6 +77752,8 @@ async function runDashboard(argv) {
77047
77752
  } catch (e) {
77048
77753
  res.writeHead(200, { "Content-Type": "application/json" });
77049
77754
  res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : String(e) }));
77755
+ } finally {
77756
+ releaseStart?.();
77050
77757
  }
77051
77758
  });
77052
77759
  return;
@@ -77397,7 +78104,12 @@ ${opportunityNoteFailure?.detail ?? ""}`);
77397
78104
  const assessmentFigures = {};
77398
78105
  for (const loc of Object.keys(st.assessmentAttachments ?? {})) assessmentFigures[loc] = assessmentFiguresFor(st, loc);
77399
78106
  const scopeOffered = Object.fromEntries(rosterNow.map((c) => [c.locationId, offeredFeaturesFor(st, c.locationId)]));
77400
- const boughtViews = Object.fromEntries(rosterNow.map((c) => [c.locationId, boughtViewFor(st, c.locationId, scopeOffered[c.locationId], boughtWordingFor(st, c.locationId))]));
78107
+ const boughtViews = Object.fromEntries(rosterNow.map((c) => [c.locationId, {
78108
+ ...boughtViewFor(st, c.locationId, scopeOffered[c.locationId], boughtWordingFor(st, c.locationId), buildStatusFor(st, c.locationId, c.locationId)),
78109
+ // Ask first (2026-10-29): the saved answers and what the client's own form said, for the questions box.
78110
+ asks: st.asks?.[c.locationId] ?? {},
78111
+ intakeAsks: intakeAskFacts(st.intakeCompleted?.[c.locationId]?.answers ?? st.intakeInputs?.[c.locationId]?.answers)
78112
+ }]));
77401
78113
  const buildViews = Object.fromEntries(rosterNow.map((c) => {
77402
78114
  const saved = readSavedPlan(c.locationId);
77403
78115
  const approval = st.planApprovals?.[c.locationId];
@@ -77412,6 +78124,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
77412
78124
  handoff: readHandoffSnapshot(c.locationId),
77413
78125
  publication: readStatusRecord(c.locationId),
77414
78126
  delivery: st.handoffDeliveries?.[c.locationId],
78127
+ // Walk W29: everything this build made across its runs (the journal), not only the last run's share.
78128
+ made: saved?.planId ? madeByBuild(c.locationId, saved.planId) : [],
77415
78129
  // The two pages exist only after Verify (cockpit audit 2026-10-28 #36), and go with an undo (#43).
77416
78130
  prepared: fs53.existsSync(localCopyPath(c.locationId)),
77417
78131
  detailPrepared: fs53.existsSync(detailCopyPath(c.locationId)),
@@ -77428,6 +78142,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
77428
78142
  approvedAt: approval?.at,
77429
78143
  stale: stageFreshness(saved?.inputHash, saved, approval, st.outcomes?.[`${rk}:3`], st.outcomes?.[`${rk}:4`], st.outcomes?.[`${rk}:1`]),
77430
78144
  built: saved?.built ?? [],
78145
+ made: saved?.planId ? madeByBuild(profile.locationId, saved.planId) : [],
77431
78146
  repairs: [],
77432
78147
  handoff: null,
77433
78148
  publication: readStatusRecord(profile.locationId),
@@ -77471,6 +78186,9 @@ ${opportunityNoteFailure?.detail ?? ""}`);
77471
78186
  }).filter((t) => t.text)];
77472
78187
  })),
77473
78188
  // Walk W15: a job the check read as done leaves the list; step 11 says what it confirmed, by name.
78189
+ // Walk W26 (2026-10-29): built objects the latest check found gone from the account, per client, so a signed-off
78190
+ // build can say so and offer Build again.
78191
+ builtMissing: Object.fromEntries(Object.entries(st.jobEvidence ?? {}).map(([id2, ev]) => [id2, (ev?.readback ?? []).filter((r) => r.status === "missing").length]).filter(([, n]) => n > 0)),
77474
78192
  confirmedJobs: Object.fromEntries(Object.keys(st.manualTasks ?? {}).map((id2) => {
77475
78193
  const shape = planShape(readSavedPlan(id2)?.plan);
77476
78194
  return [id2, confirmedHandoffLines(mergeHandoffDefs(st.jobHandoffs?.[id2] ?? [], shape.handoffs), st.jobEvidence?.[id2]?.facts, shape.calendars)];
@@ -77632,9 +78350,10 @@ ${opportunityNoteFailure?.detail ?? ""}`);
77632
78350
  }
77633
78351
  if (req.method === "POST" && url === "/api/run") {
77634
78352
  readBody(req, res, 1e4, async (body) => {
78353
+ let releaseStart;
77635
78354
  try {
77636
78355
  const { locationId: locationId2, stage, intakeConfirmed, clientKey, intakeShown, inputHashShown, planHashShown } = JSON.parse(body);
77637
- if (activeRun2?.running) throw new Error(RUN_IN_PROGRESS);
78356
+ releaseStart = claimRunStart();
77638
78357
  const who2 = cockpitClient(readCockpitState(), currentRoster(), locationId2, clientKey);
77639
78358
  const rk = who2.recordKey;
77640
78359
  if (who2.clientKey && (stage === 0 || stage === 1)) throw new Error(SECOND_CLIENT_NO_INTAKE);
@@ -77664,7 +78383,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
77664
78383
  if (stage === 3) {
77665
78384
  assertStaffUnchanged(st0.planApprovals?.[rk], st0.staffRoster?.[locationId2]);
77666
78385
  const sp = readSavedPlan(rk);
77667
- if (!sp || !savedPlanMatchesScope(sp, st0.scope?.[rk])) throw new Error("The features selected for this client changed after its plan was prepared. Prepare the plan again, approve it, then build.");
78386
+ if (!sp || !savedPlanFitsAnswers(sp, st0, rk)) throw new Error("The features selected for this client, or the answers about them, changed after its plan was prepared. Prepare the plan again, approve it, then build.");
77668
78387
  }
77669
78388
  if (stage === 4) {
77670
78389
  const build = st0.outcomes?.[`${rk}:3`];
@@ -77771,6 +78490,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
77771
78490
  } catch (e) {
77772
78491
  res.writeHead(400, { "Content-Type": "application/json" });
77773
78492
  res.end(JSON.stringify(runRefusal(e)));
78493
+ } finally {
78494
+ releaseStart?.();
77774
78495
  }
77775
78496
  });
77776
78497
  return;
@@ -77985,7 +78706,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
77985
78706
  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 ?? "") };
77986
78707
  const toolContext = { servers: [] };
77987
78708
  if (source.kind === "recorder") {
77988
- const collect = (cmdArgs, done) => new Promise((resolve14) => {
78709
+ const collect = (cmdArgs, done) => new Promise((resolve15) => {
77989
78710
  const cmd = claudeCommand(claudeBin(), cmdArgs);
77990
78711
  const child = (0, import_child_process5.spawn)(cmd.command, cmd.args, { stdio: ["ignore", "pipe", "ignore"] });
77991
78712
  let buf = "";
@@ -77994,7 +78715,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
77994
78715
  if (settled) return;
77995
78716
  settled = true;
77996
78717
  child.kill("SIGKILL");
77997
- resolve14({ out: buf, ok });
78718
+ resolve15({ out: buf, ok });
77998
78719
  };
77999
78720
  child.stdout.on("data", (d) => {
78000
78721
  buf += d.toString();
@@ -78387,9 +79108,19 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78387
79108
  res.end("No review generated for this client yet.");
78388
79109
  return;
78389
79110
  }
79111
+ if (reviewIsStale(st, locationId2, review)) {
79112
+ res.writeHead(409, { "Content-Type": "text/plain; charset=utf-8" });
79113
+ res.end("This client's Blueprint Review was written before their answers or what they bought changed. Press Regenerate on the review, then open the client document again.");
79114
+ return;
79115
+ }
79116
+ if (review.graded !== true) {
79117
+ res.writeHead(409, { "Content-Type": "text/plain; charset=utf-8" });
79118
+ res.end("This client's Blueprint Review was not checked for over-promises. Press Regenerate on the review, then open the client document again.");
79119
+ return;
79120
+ }
78390
79121
  const name2 = clientDisplayName(st, locationId2, currentRoster().find((r) => r.locationId === locationId2)?.name || locationId2);
78391
79122
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
78392
- res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2, readSavedPlan(locationId2)), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), bought: boughtDocumentFor(st, locationId2, offeredFeaturesFor(st, locationId2), boughtWordingFor(st, locationId2)), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name2, agencyName: q3.get("agency") || st.agencyName, accent: q3.get("accent") || st.agencyAccent }));
79123
+ res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2, readSavedPlan(locationId2)), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), bought: boughtDocumentFor(st, locationId2, offeredFeaturesFor(st, locationId2), boughtWordingFor(st, locationId2), buildStatusFor(st, locationId2, locationId2)), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name2, agencyName: q3.get("agency") || st.agencyName, accent: q3.get("accent") || st.agencyAccent }));
78393
79124
  return;
78394
79125
  }
78395
79126
  if (req.method === "GET" && url === "/api/review") {
@@ -78397,7 +79128,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78397
79128
  res.writeHead(200, { "Content-Type": "application/json" });
78398
79129
  const stR = readCockpitState();
78399
79130
  const businessName = clientDisplayName(stR, locationId2, currentRoster().find((r) => r.locationId === locationId2)?.name ?? "");
78400
- res.end(JSON.stringify({ ...stR.reviews?.[locationId2] ?? { ok: false, error: "not generated yet" }, ...businessName ? { businessName } : {} }));
79131
+ const savedReview = stR.reviews?.[locationId2];
79132
+ res.end(JSON.stringify({ ...savedReview ?? { ok: false, error: "not generated yet" }, ...businessName ? { businessName } : {}, ...reviewIsStale(stR, locationId2, savedReview) ? { stale: true } : {} }));
78401
79133
  return;
78402
79134
  }
78403
79135
  if (req.method === "POST" && url === "/api/shared-adopt") {
@@ -78727,6 +79459,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78727
79459
  if (!companyId || !apiKey2) throw new Error("Agency profile unavailable");
78728
79460
  return new GHLClient({ apiKey: apiKey2 }).get(`/companies/${encodeURIComponent(companyId)}`, { noRetry: true });
78729
79461
  });
79462
+ void prepareSmallLogo(saved.logoFile);
78730
79463
  const st = readCockpitState();
78731
79464
  writeCockpitState({ ...st, agencyName: saved.agencyName ?? st.agencyName, agencyAccent: saved.accent ?? st.agencyAccent });
78732
79465
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -78940,7 +79673,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78940
79673
  if (req.method === "POST" && url === "/api/scope") {
78941
79674
  readBody(req, res, 1e4, (body) => {
78942
79675
  try {
78943
- const { locationId: locationId2, clientKey, package: pkg2, features, bought } = JSON.parse(body);
79676
+ const { locationId: locationId2, clientKey, package: pkg2, features, bought, asks: asksIn } = JSON.parse(body);
78944
79677
  const who2 = cockpitClient(readCockpitState(), currentRoster(), locationId2, clientKey);
78945
79678
  let ticks;
78946
79679
  let derived;
@@ -78964,21 +79697,36 @@ ${opportunityNoteFailure?.detail ?? ""}`);
78964
79697
  else ticks = savedTicks[rk];
78965
79698
  nextState = { ...nextState, bought: savedTicks };
78966
79699
  if (!Object.keys(savedTicks).length) delete nextState.bought;
79700
+ const intakeSms = intakeChannelAnswers(nextState.intakeCompleted?.[who2.locationId]?.answers ?? nextState.intakeInputs?.[who2.locationId]?.answers).sms;
79701
+ if (asksIn !== void 0) {
79702
+ const parsed2 = parseAsks(asksIn);
79703
+ if (!parsed2.ok) throw new Error(parsed2.error);
79704
+ const clash = asksContradiction(parsed2.asks, effectiveTexting(parsed2.asks, intakeSms));
79705
+ if (clash) throw new Error(clash);
79706
+ const all = { ...nextState.asks ?? {} };
79707
+ if (Object.keys(parsed2.asks).length) all[rk] = parsed2.asks;
79708
+ else delete all[rk];
79709
+ nextState = { ...nextState, asks: all };
79710
+ if (!Object.keys(all).length) delete nextState.asks;
79711
+ }
78967
79712
  const { state: bound, withdrawn } = withdrawIfBindingChanged(nextState, rk);
78968
79713
  nextState = bound;
78969
79714
  {
78970
79715
  const offered = offeredFeaturesFor(nextState, rk);
78971
79716
  const channels = intakeChannelAnswers(nextState.intakeCompleted?.[who2.locationId]?.answers ?? nextState.intakeInputs?.[who2.locationId]?.answers);
79717
+ const texting = effectiveTexting(nextState.asks?.[rk], channels.sms);
78972
79718
  const scopeNow = nextState.scope?.[rk];
78973
79719
  const excluded = ticks ? boughtButExcludedJobs(ticks, offered, !scopeNow || scopeNow.wholePreset ? "all" : scopeNow.features ?? []) : [];
78974
- const boughtList = ticks ? [...boughtJobs(ticks, offered, channels), ...excluded] : [];
79720
+ const held2 = ticks ? heldJobs(heldItems(ticks, nextState.asks?.[rk], texting)) : [];
79721
+ const boughtList = ticks ? [...boughtJobs(ticks, offered, { ...channels, sms: texting === "yes" }), ...excluded, ...held2] : [];
78975
79722
  replaceBuildObligationsOfType(rk, BOUGHT_JOB_TYPE, boughtList);
78976
79723
  const tasks2 = withBoughtJobs(nextState.manualTasks?.[rk] ?? [], ticks ? catalogJobs({ handoffs: [], obligations: boughtList }) : []);
78977
79724
  if (tasks2.length || nextState.manualTasks?.[rk]) nextState = { ...nextState, manualTasks: { ...nextState.manualTasks, [rk]: tasks2 } };
78978
79725
  }
78979
79726
  writeCockpitState(nextState);
78980
79727
  res.writeHead(200, { "Content-Type": "application/json" });
78981
- res.end(JSON.stringify({ ok: true, scope: readCockpitState().scope?.[rk] ?? null, ...withdrawn ? { approvalWithdrawn: true, message: APPROVAL_WITHDRAWN_FOR_SCOPE } : {} }));
79728
+ const builtBefore = nextState.outcomes?.[`${rk}:3`]?.ok === true;
79729
+ res.end(JSON.stringify({ ok: true, scope: readCockpitState().scope?.[rk] ?? null, ...withdrawn ? { approvalWithdrawn: true, message: builtBefore && asksIn !== void 0 ? BUILD_WHAT_WAS_WAITING : APPROVAL_WITHDRAWN_FOR_SCOPE } : {} }));
78982
79730
  } catch (e) {
78983
79731
  res.writeHead(400, { "Content-Type": "application/json" });
78984
79732
  res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "bad request" }));
@@ -79111,11 +79859,11 @@ ${opportunityNoteFailure?.detail ?? ""}`);
79111
79859
  const run = () => {
79112
79860
  const row = routeOf(req.method, url);
79113
79861
  if (row?.write && req.method !== "GET" && req.method !== "HEAD") {
79114
- const held = req[GATE_BODY];
79862
+ const held2 = req[GATE_BODY];
79115
79863
  let client = new URL(req.url || "/", "http://x").searchParams.get("locationId") ?? void 0;
79116
- if (!client && held) {
79864
+ if (!client && held2) {
79117
79865
  try {
79118
- const b = JSON.parse(held);
79866
+ const b = JSON.parse(held2);
79119
79867
  const c = b.locationId ?? b.client ?? b.clientLocationId;
79120
79868
  if (typeof c === "string") client = c;
79121
79869
  } catch {
@@ -79136,9 +79884,9 @@ ${opportunityNoteFailure?.detail ?? ""}`);
79136
79884
  });
79137
79885
  }, { brainHandoff: () => isConnected("brain") }));
79138
79886
  try {
79139
- await new Promise((resolve14, reject) => {
79887
+ await new Promise((resolve15, reject) => {
79140
79888
  server2.once("error", reject);
79141
- server2.listen(port, "127.0.0.1", resolve14);
79889
+ server2.listen(port, "127.0.0.1", resolve15);
79142
79890
  });
79143
79891
  } catch (e) {
79144
79892
  const msg3 = e?.code === "EADDRINUSE" ? `Port ${port} is busy with another program. Close it, or start Command OS on a different port.` : `Command OS could not start: ${e instanceof Error ? e.message : String(e)}`;
@@ -79160,16 +79908,16 @@ ${opportunityNoteFailure?.detail ?? ""}`);
79160
79908
  if (adopted) process.stderr.write(` Synced ${adopted} client${adopted === 1 ? "" : "s"} from your GHL (teammate updates).
79161
79909
  `);
79162
79910
  });
79163
- return await new Promise((resolve14) => {
79911
+ return await new Promise((resolve15) => {
79164
79912
  const stop3 = () => {
79165
79913
  closeAllSecretChannels();
79166
- server2.close(() => resolve14(0));
79914
+ server2.close(() => resolve15(0));
79167
79915
  };
79168
79916
  process.on("SIGINT", stop3);
79169
79917
  process.on("SIGTERM", stop3);
79170
79918
  });
79171
79919
  }
79172
- var import_zod94, import_node_util5, fs53, path53, http2, import_child_process4, import_child_process5, osmod, VIEWER_COCKPIT_MSG, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, stateRecovery, stateSaveFs, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, TEXT_SNIPPETS_LOCKED, SCOPE_REFUSAL, APPROVAL_WITHDRAWN_FOR_STAFF, APPROVAL_WITHDRAWN_NO_TEAM, HANDOFF_WORKFLOW, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, APPROVE_NEEDS_INTAKE, APPROVE_NEEDS_BRIEF, INDUSTRY_FIRST, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, INACTIVE, OFF_LICENSE, LICENSE_RECHECK_MS, activeRun2, buildOneTime, oneTimeView, runForTests, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanWatch, queueScanOnce, ASSESSMENT_PIPELINE_MISSING, GATE_BODY, GATE_BODY_MAX, DOC_NOT_SAVED, NO_KEY_ON_FILE, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, ACCOUNT_CHANGED_DURING_READ, commitWithoutRead, SNAPSHOTS_CHANGED_DURING_READ, IntakeColdError, INTAKE_ANSWERS_MISSING, INTAKE_READ_FAILED, RUN_IN_PROGRESS, BRIEF_NEEDS_INTAKE2, BUSINESS_TYPE_FOR_INDUSTRY, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, NO_SUCH_PAGE, CHOOSE_CLIENT, ONE_CLIENT_ONLY, anthropicKeyWrites, SEAT_TOKEN_HEADER, notSignedInMsg, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, COCKPIT_AFTER_CANCEL_PATHS, licenceRefusedHooks, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
79920
+ var import_zod94, import_node_util5, fs53, path53, http2, import_child_process4, import_child_process5, osmod, VIEWER_COCKPIT_MSG, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, stateRecovery, stateSaveFs, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, TEXT_SNIPPETS_LOCKED, SCOPE_REFUSAL, APPROVAL_WITHDRAWN_FOR_STAFF, APPROVAL_WITHDRAWN_NO_TEAM, HANDOFF_WORKFLOW, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, APPROVE_NEEDS_INTAKE, APPROVE_NEEDS_BRIEF, INDUSTRY_FIRST, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, BUILD_WHAT_WAS_WAITING, INACTIVE, OFF_LICENSE, LICENSE_RECHECK_MS, activeRun2, runStarting, buildOneTime, oneTimeView, runForTests, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanWatch, queueScanOnce, ASSESSMENT_PIPELINE_MISSING, GATE_BODY, GATE_BODY_MAX, DOC_NOT_SAVED, NO_KEY_ON_FILE, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, ACCOUNT_CHANGED_DURING_READ, commitWithoutRead, SNAPSHOTS_CHANGED_DURING_READ, IntakeColdError, INTAKE_ANSWERS_MISSING, INTAKE_READ_FAILED, RUN_IN_PROGRESS, BRIEF_NEEDS_INTAKE2, BUSINESS_TYPE_FOR_INDUSTRY, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, NO_SUCH_PAGE, CHOOSE_CLIENT, ONE_CLIENT_ONLY, anthropicKeyWrites, SEAT_TOKEN_HEADER, notSignedInMsg, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, COCKPIT_AFTER_CANCEL_PATHS, licenceRefusedHooks, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
79173
79921
  var init_dashboard = __esm({
79174
79922
  "src/dashboard.ts"() {
79175
79923
  "use strict";
@@ -79309,6 +80057,7 @@ var init_dashboard = __esm({
79309
80057
  init_daily_brief2();
79310
80058
  init_daily_brief_collect();
79311
80059
  init_brief_deadline();
80060
+ init_client_asks();
79312
80061
  VIEWER_COCKPIT_MSG = "Your team login on this license is view only, and Command OS runs work that changes accounts, so it did not open. Ask the license owner to make you a team member, then open Command OS again.";
79313
80062
  STAGES2 = [
79314
80063
  "Intake",
@@ -79343,10 +80092,12 @@ var init_dashboard = __esm({
79343
80092
  NOTHING_TICKED_IN_PLAN = "None of the ticked features is in the plan for this client's industry, so nothing was saved. Tick features from the list, or choose Everything in the plan.";
79344
80093
  SCOPE_CHANGED_SINCE_APPROVAL = "What they bought changed after the build was approved, so Build did not start. Approve this plan again, then build.";
79345
80094
  APPROVAL_WITHDRAWN_FOR_SCOPE = "What they bought changed, so the build approval was withdrawn. Approve this plan again before building.";
80095
+ BUILD_WHAT_WAS_WAITING = "Your client's answers changed what gets built, so the build approval was withdrawn. Approve the updated plan, then run Build: it adds only what was waiting and makes nothing twice.";
79346
80096
  INACTIVE = "License is not active. Contact support@ghlcommand.com.";
79347
80097
  OFF_LICENSE = "This computer is no longer on your license. Free a device on your license, then restart Command OS.";
79348
80098
  LICENSE_RECHECK_MS = 4 * 60 * 60 * 1e3;
79349
80099
  activeRun2 = null;
80100
+ runStarting = false;
79350
80101
  buildOneTime = /* @__PURE__ */ new Map();
79351
80102
  oneTimeView = (e) => ({ locationId: e.locationId, ...e.clientKey ? { clientKey: e.clientKey } : {}, ...e.clientName ? { clientName: e.clientName } : {}, lines: Object.values(e.byEmail) });
79352
80103
  runForTests = {
@@ -79374,7 +80125,7 @@ var init_dashboard = __esm({
79374
80125
  DOC_NOT_SAVED = "That document is not saved for this client any more. Choose it again from the list, or add it again.";
79375
80126
  NO_KEY_ON_FILE = "No key on file for this account. Register it in GHL Command first.";
79376
80127
  PARKED_NOT_BUILT = "Template and hidden accounts are not built from Command OS. Change the account's type to Client first.";
79377
- NEEDS_AGENCY_KEY = "No agency key on file. Run register_agency_key in GHL Command first.";
80128
+ NEEDS_AGENCY_KEY = "Your agency is not connected yet. Open Your agency, then Connections, and add your agency's key there.";
79378
80129
  INTAKE_UNREADABLE = "Command OS could not read this client's account. Paste its key again under Reconnect this client, at the top of its steps on Build a client account.";
79379
80130
  INTAKE_READ_TRANSIENT = "We could not reach this client's account just now. Try again in a moment.";
79380
80131
  INTAKE_REFRESH_RETRYING = "Could not refresh just now; retrying.";
@@ -80082,7 +80833,7 @@ var init_location_switcher = __esm({
80082
80833
 
80083
80834
  // src/tools/bulk-operations.ts
80084
80835
  function delay2(ms) {
80085
- return new Promise((resolve14) => setTimeout(resolve14, ms));
80836
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
80086
80837
  }
80087
80838
  function formatResults(op, results, total2) {
80088
80839
  return `${op}: ${results.success} success, ${results.failed} failed out of ${total2}.${results.errors.length ? "\nErrors:\n" + results.errors.join("\n") : ""}`;
@@ -80487,7 +81238,7 @@ var init_handoff_doc = __esm({
80487
81238
 
80488
81239
  // src/tools/account-export.ts
80489
81240
  function delay3(ms) {
80490
- return new Promise((resolve14) => setTimeout(resolve14, ms));
81241
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
80491
81242
  }
80492
81243
  function builderSectionAvailability(builderLocationId, requestedLocationId) {
80493
81244
  if (!builderLocationId) {
@@ -81037,12 +81788,12 @@ function registerWorkflowClonerTools(server2, builderClient) {
81037
81788
  const triggers = source.triggers || [];
81038
81789
  for (const action of actions) {
81039
81790
  if (action.id) {
81040
- idMap.set(action.id, crypto13.randomUUID());
81791
+ idMap.set(action.id, crypto14.randomUUID());
81041
81792
  }
81042
81793
  }
81043
81794
  for (const trigger of triggers) {
81044
81795
  if (trigger.id) {
81045
- idMap.set(trigger.id, crypto13.randomUUID());
81796
+ idMap.set(trigger.id, crypto14.randomUUID());
81046
81797
  }
81047
81798
  }
81048
81799
  const remap = (id2) => {
@@ -81155,12 +81906,12 @@ function registerWorkflowClonerTools(server2, builderClient) {
81155
81906
  }
81156
81907
  );
81157
81908
  }
81158
- var import_zod98, crypto13;
81909
+ var import_zod98, crypto14;
81159
81910
  var init_workflow_cloner = __esm({
81160
81911
  "src/tools/workflow-cloner.ts"() {
81161
81912
  "use strict";
81162
81913
  import_zod98 = require("zod");
81163
- crypto13 = __toESM(require("crypto"));
81914
+ crypto14 = __toESM(require("crypto"));
81164
81915
  }
81165
81916
  });
81166
81917
 
@@ -81206,13 +81957,13 @@ function contactServiceNote(payload) {
81206
81957
  function registerSmartListTools(server2, builderClient) {
81207
81958
  if (!builderClient) return;
81208
81959
  const client = builderClient;
81209
- async function smartListRequest(method, path66, body) {
81210
- const url = `${SMARTLIST_BASE}${path66}`;
81960
+ async function smartListRequest(method, path67, body) {
81961
+ const url = `${SMARTLIST_BASE}${path67}`;
81211
81962
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
81212
81963
  const response = await builderRequest(client, method, url, sendsBody ? { body } : {});
81213
81964
  if (!response.ok) {
81214
81965
  const text4 = await response.text();
81215
- throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path66}
81966
+ throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path67}
81216
81967
  ${text4}`);
81217
81968
  }
81218
81969
  const text3 = await response.text();
@@ -81457,11 +82208,11 @@ var init_smart_lists = __esm({
81457
82208
  function registerReputationTools(server2, builderClient) {
81458
82209
  if (!builderClient) return;
81459
82210
  const client = builderClient;
81460
- async function reputationRequest(method, path66) {
81461
- const response = await builderRequest(client, method, `${REPUTATION_BASE}${path66}`);
82211
+ async function reputationRequest(method, path67) {
82212
+ const response = await builderRequest(client, method, `${REPUTATION_BASE}${path67}`);
81462
82213
  if (!response.ok) {
81463
82214
  const text4 = await response.text();
81464
- throw new Error(`Reputation API Error ${response.status}: ${method} ${path66}
82215
+ throw new Error(`Reputation API Error ${response.status}: ${method} ${path67}
81465
82216
  ${text4}`);
81466
82217
  }
81467
82218
  const text3 = await response.text();
@@ -81599,11 +82350,11 @@ var init_email_campaigns = __esm({
81599
82350
  function registerMembershipTools(server2, builderClient) {
81600
82351
  if (!builderClient) return;
81601
82352
  const client = builderClient;
81602
- async function membershipRequest(path66, method = "GET", body) {
81603
- const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${path66}`, body ? { body } : {});
82353
+ async function membershipRequest(path67, method = "GET", body) {
82354
+ const response = await builderRequest(client, method, `${MEMBERSHIP_BASE2}${path67}`, body ? { body } : {});
81604
82355
  if (!response.ok) {
81605
82356
  const text4 = await response.text();
81606
- throw new Error(`Membership API Error ${response.status}: ${method} ${path66}
82357
+ throw new Error(`Membership API Error ${response.status}: ${method} ${path67}
81607
82358
  ${text4}`);
81608
82359
  }
81609
82360
  const text3 = await response.text();
@@ -81789,7 +82540,7 @@ var init_memberships = __esm({
81789
82540
 
81790
82541
  // src/tools/template-deployer.ts
81791
82542
  function delay4(ms) {
81792
- return new Promise((resolve14) => setTimeout(resolve14, ms));
82543
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
81793
82544
  }
81794
82545
  function isAlreadyExistsError(message) {
81795
82546
  return /already\s+exists?\b/i.test(message) || /duplicate/i.test(message);
@@ -81922,7 +82673,7 @@ function registerTemplateDeployerTools(server2, client) {
81922
82673
  const locId = client.resolveLocationId(locationId2);
81923
82674
  const safePath = validateTemplatePath(templateFile);
81924
82675
  const template = TemplateSchema.parse(JSON.parse(fs54.readFileSync(safePath, "utf-8")));
81925
- const resolve14 = (text3) => {
82676
+ const resolve15 = (text3) => {
81926
82677
  if (typeof text3 !== "string") return text3;
81927
82678
  let result = text3;
81928
82679
  for (const [key2, value] of Object.entries(answers)) {
@@ -81935,7 +82686,7 @@ function registerTemplateDeployerTools(server2, client) {
81935
82686
  return result;
81936
82687
  };
81937
82688
  const resolveObj = (obj2) => {
81938
- if (typeof obj2 === "string") return resolve14(obj2);
82689
+ if (typeof obj2 === "string") return resolve15(obj2);
81939
82690
  if (Array.isArray(obj2)) return obj2.map(resolveObj);
81940
82691
  if (obj2 && typeof obj2 === "object") {
81941
82692
  const result = {};
@@ -82644,10 +83395,10 @@ async function confirm(run) {
82644
83395
  async function runProbes(client, loc) {
82645
83396
  return Promise.all(
82646
83397
  PROBES.map((probe) => confirm(async () => {
82647
- const path66 = probe.path.replace("{loc}", loc);
83398
+ const path67 = probe.path.replace("{loc}", loc);
82648
83399
  const params = probe.params ? Object.fromEntries(Object.entries(probe.params).map(([k, v]) => [k, substitute(v, loc)])) : void 0;
82649
83400
  try {
82650
- const raw = await client.get(path66, { params, noRetry: true });
83401
+ const raw = await client.get(path67, { params, noRetry: true });
82651
83402
  const count4 = countRows(raw, probe.listKeys);
82652
83403
  return {
82653
83404
  key: probe.key,
@@ -85894,7 +86645,7 @@ function claudeGenerate(model = DEFAULT_MODEL, bin = "claude") {
85894
86645
  return (instruction, { cwd }) => new Promise((resolveP) => {
85895
86646
  const t0 = Date.now();
85896
86647
  const cmd = claudeCommand(bin, ["-p", "--output-format", "json", "--max-turns", "1", "--tools", "", "--strict-mcp-config", "--model", model, ...NO_MEMBER_HOOKS]);
85897
- const child = (0, import_node_child_process7.spawn)(cmd.command, cmd.args, {
86648
+ const child = (0, import_node_child_process8.spawn)(cmd.command, cmd.args, {
85898
86649
  cwd,
85899
86650
  env: { ...process.env, CLAUDECODE: "", CLAUDE_CODE_ENTRYPOINT: "" },
85900
86651
  stdio: ["pipe", "pipe", "pipe"]
@@ -86046,11 +86797,11 @@ function summariseRun(r) {
86046
86797
  const copy = !r.copy ? "" : "error" in r.copy ? ` \xB7 copy NOT GRADED (${r.copy.error})` : ` \xB7 copy ${r.copy.verdict}${r.copy.firstBreakdown ? ` (first breakdown: ${r.copy.firstBreakdown})` : ""}`;
86047
86798
  return `${r.medium} for ${r.brandSlug}: design ${r.ok ? `PASS on pass ${r.greenAt}` : "NOT GREEN"}${copy} \xB7 ${passes} \xB7 $${r.costUsd.toFixed(3)} \xB7 ${Math.round(r.ms / 1e3)}s \xB7 ${r.model ?? "model unrecorded"}${r.file ? ` \xB7 ${r.file}` : ""}${r.report ? ` \xB7 ${r.report}` : ""}`;
86048
86799
  }
86049
- var import_node_child_process7, import_node_fs18, import_node_path16, DEFAULT_MODEL, FIRST2;
86800
+ var import_node_child_process8, import_node_fs18, import_node_path16, DEFAULT_MODEL, FIRST2;
86050
86801
  var init_run2 = __esm({
86051
86802
  "src/command-os/design/run.ts"() {
86052
86803
  "use strict";
86053
- import_node_child_process7 = require("node:child_process");
86804
+ import_node_child_process8 = require("node:child_process");
86054
86805
  init_claude_locate();
86055
86806
  init_result_text();
86056
86807
  import_node_fs18 = require("node:fs");
@@ -89837,23 +90588,23 @@ function ownsForSignal(p, jobDir, installs, platform2 = process.platform, stat)
89837
90588
  if (!installs.some((dir) => exeInside(p, dir, platform2, stat))) return false;
89838
90589
  return profileInside(argvOf(p, platform2), jobDir, platform2);
89839
90590
  }
89840
- var import_node_fs19, import_node_path17, import_node_child_process8, LIST_TIMEOUT_MS, execText, execTextSync, WIN_FIELDS, LISTING, listingCommand, oneCommand, exeCommand, listingOf, bounded, argvOf, sepOf, foldText, CODE_SIGN_CLONE;
90591
+ var import_node_fs19, import_node_path17, import_node_child_process9, LIST_TIMEOUT_MS, execText, execTextSync, WIN_FIELDS, LISTING, listingCommand, oneCommand, exeCommand, listingOf, bounded, argvOf, sepOf, foldText, CODE_SIGN_CLONE;
89841
90592
  var init_ownership = __esm({
89842
90593
  "src/command-os/prospect/ownership.ts"() {
89843
90594
  "use strict";
89844
90595
  import_node_fs19 = __toESM(require("node:fs"));
89845
90596
  import_node_path17 = __toESM(require("node:path"));
89846
- import_node_child_process8 = require("node:child_process");
90597
+ import_node_child_process9 = require("node:child_process");
89847
90598
  LIST_TIMEOUT_MS = 1e4;
89848
- execText = (file3, args, timeoutMs) => new Promise((resolve14) => {
89849
- (0, import_node_child_process8.execFile)(file3, args, { maxBuffer: 64 << 20, windowsHide: true, timeout: Math.max(1, Math.floor(timeoutMs)), killSignal: "SIGKILL" }, (e, stdout) => {
90599
+ execText = (file3, args, timeoutMs) => new Promise((resolve15) => {
90600
+ (0, import_node_child_process9.execFile)(file3, args, { maxBuffer: 64 << 20, windowsHide: true, timeout: Math.max(1, Math.floor(timeoutMs)), killSignal: "SIGKILL" }, (e, stdout) => {
89850
90601
  const code = e ? typeof e.code === "number" ? e.code : null : 0;
89851
- resolve14({ code, stdout: String(stdout ?? "") });
90602
+ resolve15({ code, stdout: String(stdout ?? "") });
89852
90603
  });
89853
90604
  });
89854
90605
  execTextSync = (file3, args, timeoutMs) => {
89855
90606
  try {
89856
- return { code: 0, stdout: String((0, import_node_child_process8.execFileSync)(file3, args, { encoding: "utf8", timeout: Math.max(1, Math.floor(timeoutMs)), killSignal: "SIGKILL", maxBuffer: 64 << 20, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] })) };
90607
+ return { code: 0, stdout: String((0, import_node_child_process9.execFileSync)(file3, args, { encoding: "utf8", timeout: Math.max(1, Math.floor(timeoutMs)), killSignal: "SIGKILL", maxBuffer: 64 << 20, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] })) };
89857
90608
  } catch {
89858
90609
  return { code: null, stdout: "" };
89859
90610
  }
@@ -89988,12 +90739,12 @@ async function killJob(child, marker, o = {}) {
89988
90739
  return { killed: [...killed], remaining: remaining.map((p) => p.pid), failures, confirmed: clear && rootGone };
89989
90740
  }
89990
90741
  function shopperSlot() {
89991
- let held = false;
90742
+ let held2 = false;
89992
90743
  for (const j of liveJobs) {
89993
90744
  if (j.stuck) return "stuck";
89994
- if (j.held) held = true;
90745
+ if (j.held) held2 = true;
89995
90746
  }
89996
- return held ? "held" : "free";
90747
+ return held2 ? "held" : "free";
89997
90748
  }
89998
90749
  function killJobsNow(platform2 = process.platform) {
89999
90750
  if (!liveJobs.size) return [];
@@ -90061,7 +90812,7 @@ async function runIsolated(input, o = {}) {
90061
90812
  const jobDir = import_node_fs20.default.mkdtempSync(import_node_path18.default.join(import_node_fs20.default.realpathSync(import_node_os2.default.tmpdir()), "ghl-shopper-job-"));
90062
90813
  const marker = jobMarker(jobDir, platform2);
90063
90814
  o.onJob?.({ dir: jobDir, marker });
90064
- const child = (0, import_node_child_process9.spawn)(process.execPath, [`--max-old-space-size=${o.maxOldSpaceMb ?? CHILD_MAX_OLD_SPACE_MB}`, o.entry ?? defaultEntry(), ...o.childArg ? [o.childArg] : []], {
90815
+ const child = (0, import_node_child_process10.spawn)(process.execPath, [`--max-old-space-size=${o.maxOldSpaceMb ?? CHILD_MAX_OLD_SPACE_MB}`, o.entry ?? defaultEntry(), ...o.childArg ? [o.childArg] : []], {
90065
90816
  stdio: ["pipe", "ignore", "pipe", "pipe", "pipe", "pipe"],
90066
90817
  // The job's folder is its temp folder, on every platform (Windows reads TEMP/TMP), so Chrome's profile is in it.
90067
90818
  env: { ...o.env ?? childEnv(), TMPDIR: jobDir, TMP: jobDir, TEMP: jobDir },
@@ -90290,7 +91041,7 @@ async function sweepStaleJobFolders(o = {}) {
90290
91041
  }
90291
91042
  return removed;
90292
91043
  }
90293
- var import_node_fs20, import_node_os2, import_node_path18, import_node_stream, import_node_child_process9, JOB_DEADLINE_MS, CHILD_MAX_OLD_SPACE_MB, TREE_MEMORY_MAX_BYTES, RESULT_MAX_BYTES, STDERR_PASS_BYTES, STDERR_TAIL_BYTES, STDERR_SCAN_BYTES, OUT_OF_MEMORY, NC_TIME_LIMIT, NC_MEMORY_LIMIT, BLIND_LOG, NC_ENDED, CHILD_ENV_KEYS, markerDir, CHANNEL_EXE, isMain, sleep5, ROOT_WAIT_MS, liveJobs, CLEANUP_RETRY_MS, CLEANUP_HOLD_MS, HELD_LOG, STUCK_LOG, RELEASED_LOG, exitHookSet, FOLDER_WAIT_MS, FOLDER_LEFT_LOG, STALE_AFTER_MS;
91044
+ var import_node_fs20, import_node_os2, import_node_path18, import_node_stream, import_node_child_process10, JOB_DEADLINE_MS, CHILD_MAX_OLD_SPACE_MB, TREE_MEMORY_MAX_BYTES, RESULT_MAX_BYTES, STDERR_PASS_BYTES, STDERR_TAIL_BYTES, STDERR_SCAN_BYTES, OUT_OF_MEMORY, NC_TIME_LIMIT, NC_MEMORY_LIMIT, BLIND_LOG, NC_ENDED, CHILD_ENV_KEYS, markerDir, CHANNEL_EXE, isMain, sleep5, ROOT_WAIT_MS, liveJobs, CLEANUP_RETRY_MS, CLEANUP_HOLD_MS, HELD_LOG, STUCK_LOG, RELEASED_LOG, exitHookSet, FOLDER_WAIT_MS, FOLDER_LEFT_LOG, STALE_AFTER_MS;
90294
91045
  var init_isolate = __esm({
90295
91046
  "src/command-os/prospect/isolate.ts"() {
90296
91047
  "use strict";
@@ -90298,7 +91049,7 @@ var init_isolate = __esm({
90298
91049
  import_node_os2 = __toESM(require("node:os"));
90299
91050
  import_node_path18 = __toESM(require("node:path"));
90300
91051
  import_node_stream = require("node:stream");
90301
- import_node_child_process9 = require("node:child_process");
91052
+ import_node_child_process10 = require("node:child_process");
90302
91053
  init_run3();
90303
91054
  init_ownership();
90304
91055
  init_ownership();
@@ -90950,14 +91701,14 @@ function buildReplyResponse(full, mode, locationId2) {
90950
91701
  function readBuildDetail(locationId2, detail, budget = BUILD_REPLY_BUDGET) {
90951
91702
  let full = lastFull.get(keyOf5(locationId2, detail.of, detail.clientKey));
90952
91703
  if (!full && !detail.clientKey) {
90953
- const held = [...lastFull.keys()].filter((k) => k.startsWith(`${locationId2}|`) && k.endsWith(`|${detail.of}`));
90954
- if (held.length > 1) return { ok: false, error: `Replies for more than one client in this account are held. Pass detail.clientKey (the clientKey on the reply you are reading).` };
90955
- if (held.length === 1) full = lastFull.get(held[0]);
91704
+ const held2 = [...lastFull.keys()].filter((k) => k.startsWith(`${locationId2}|`) && k.endsWith(`|${detail.of}`));
91705
+ if (held2.length > 1) return { ok: false, error: `Replies for more than one client in this account are held. Pass detail.clientKey (the clientKey on the reply you are reading).` };
91706
+ if (held2.length === 1) full = lastFull.get(held2[0]);
90956
91707
  }
90957
91708
  if (!full) return { ok: false, error: `No ${detail.of} reply from apply_build_plan is held for this ${detail.clientKey ? "client" : "account"} in this session. Run apply_build_plan with mode:"${detail.of}" first.` };
90958
- const path66 = detail.section.split(".");
91709
+ const path67 = detail.section.split(".");
90959
91710
  let value = full;
90960
- for (const p of path66) value = obj(value)[p];
91711
+ for (const p of path67) value = obj(value)[p];
90961
91712
  if (value === void 0) return { ok: false, error: `The ${detail.of} reply has no section "${detail.section}".`, sections: Object.keys(full) };
90962
91713
  const room = budget - 600;
90963
91714
  let pages;
@@ -91193,6 +91944,123 @@ var init_publish_report = __esm({
91193
91944
  }
91194
91945
  });
91195
91946
 
91947
+ // src/build-lock.ts
91948
+ function pidAlive2(pid) {
91949
+ if (!Number.isInteger(pid) || pid <= 0) return false;
91950
+ try {
91951
+ process.kill(pid, 0);
91952
+ return true;
91953
+ } catch (e) {
91954
+ return e.code === "EPERM";
91955
+ }
91956
+ }
91957
+ function buildLockPath(locationId2, base = appDataDir()) {
91958
+ return path63.join(base, "build-locks", `${locationId2.replace(/[^A-Za-z0-9_-]/g, "_")}.lock`);
91959
+ }
91960
+ function claimBuildLock(locationId2, base) {
91961
+ const lock = buildLockPath(locationId2, base);
91962
+ fs63.mkdirSync(path63.dirname(lock), { recursive: true, mode: 448 });
91963
+ const mine = JSON.stringify({ pid: process.pid, token: crypto15.randomUUID(), at: (/* @__PURE__ */ new Date()).toISOString() });
91964
+ for (let attempt2 = 0; attempt2 < 3; attempt2++) {
91965
+ try {
91966
+ const fd = fs63.openSync(lock, "wx", 384);
91967
+ try {
91968
+ fs63.writeSync(fd, mine);
91969
+ } finally {
91970
+ fs63.closeSync(fd);
91971
+ }
91972
+ held.set(lock, mine);
91973
+ return () => {
91974
+ if (held.get(lock) === mine) held.delete(lock);
91975
+ try {
91976
+ if (fs63.readFileSync(lock, "utf8") === mine) fs63.unlinkSync(lock);
91977
+ } catch {
91978
+ }
91979
+ };
91980
+ } catch (e) {
91981
+ if (e.code !== "EEXIST") throw e;
91982
+ let seen = "";
91983
+ let age = 0;
91984
+ try {
91985
+ seen = fs63.readFileSync(lock, "utf8");
91986
+ age = Date.now() - fs63.statSync(lock).mtimeMs;
91987
+ } catch {
91988
+ continue;
91989
+ }
91990
+ let pid = 0;
91991
+ try {
91992
+ pid = Number(JSON.parse(seen).pid);
91993
+ } catch {
91994
+ }
91995
+ const stale = pid > 0 && !pidAlive2(pid) || age > BUILD_LOCK_STALE_MS;
91996
+ if (!stale) return null;
91997
+ if (!takeOver(lock, seen)) return null;
91998
+ }
91999
+ }
92000
+ return null;
92001
+ }
92002
+ function takeOver(lock, seen, retried = false) {
92003
+ const mutex = `${lock}.takeover`;
92004
+ let fd;
92005
+ try {
92006
+ fd = fs63.openSync(mutex, "wx", 384);
92007
+ } catch (e) {
92008
+ if (e.code !== "EEXIST") throw e;
92009
+ let aged = false;
92010
+ try {
92011
+ if (Date.now() - fs63.statSync(mutex).mtimeMs > TAKEOVER_STALE_MS) {
92012
+ fs63.unlinkSync(mutex);
92013
+ aged = true;
92014
+ }
92015
+ } catch {
92016
+ }
92017
+ return aged && !retried ? takeOver(lock, seen, true) : false;
92018
+ }
92019
+ try {
92020
+ try {
92021
+ if (fs63.readFileSync(lock, "utf8") === seen) fs63.unlinkSync(lock);
92022
+ } catch {
92023
+ }
92024
+ } finally {
92025
+ fs63.closeSync(fd);
92026
+ try {
92027
+ fs63.unlinkSync(mutex);
92028
+ } catch {
92029
+ }
92030
+ }
92031
+ return true;
92032
+ }
92033
+ function touchBuildLock(locationId2, base) {
92034
+ const lock = buildLockPath(locationId2, base);
92035
+ const mine = held.get(lock);
92036
+ if (!mine) return;
92037
+ try {
92038
+ if (fs63.readFileSync(lock, "utf8") !== mine) {
92039
+ held.delete(lock);
92040
+ return;
92041
+ }
92042
+ const now = /* @__PURE__ */ new Date();
92043
+ fs63.utimesSync(lock, now, now);
92044
+ } catch {
92045
+ }
92046
+ }
92047
+ function buildRunningRefusal(accountName2) {
92048
+ return `A build is already running for ${accountName2}. Nothing was written by this call. Wait for that build's answer before running Build again; calling it again while it runs would create everything twice.`;
92049
+ }
92050
+ var crypto15, fs63, path63, BUILD_LOCK_STALE_MS, held, TAKEOVER_STALE_MS;
92051
+ var init_build_lock = __esm({
92052
+ "src/build-lock.ts"() {
92053
+ "use strict";
92054
+ crypto15 = __toESM(require("node:crypto"));
92055
+ fs63 = __toESM(require("node:fs"));
92056
+ path63 = __toESM(require("node:path"));
92057
+ init_credentials_store();
92058
+ BUILD_LOCK_STALE_MS = 20 * 6e4;
92059
+ held = /* @__PURE__ */ new Map();
92060
+ TAKEOVER_STALE_MS = 1e4;
92061
+ }
92062
+ });
92063
+
91196
92064
  // src/intake-to-build/revert.ts
91197
92065
  function plainDate(iso3, timeZone) {
91198
92066
  const d = new Date(iso3);
@@ -91297,7 +92165,7 @@ function planRevert(record, opts) {
91297
92165
  function errText(e) {
91298
92166
  return e instanceof Error ? e.message : String(e);
91299
92167
  }
91300
- async function executeRevert(plan, deps, wait = (ms) => new Promise((resolve14) => setTimeout(resolve14, ms))) {
92168
+ async function executeRevert(plan, deps, wait = (ms) => new Promise((resolve15) => setTimeout(resolve15, ms))) {
91301
92169
  const removed = [];
91302
92170
  const refused = [];
91303
92171
  const attempted = [];
@@ -92971,8 +93839,8 @@ async function executeBackbone(plan, rawDeps, opts = {}) {
92971
93839
  calendarPending.push({ workflowRef: wf.ref, workflowName: wf.name, calendarRef: g.calendarRef, calendarName: g.calendarName, steps: g.steps, status: "pending" });
92972
93840
  }
92973
93841
  const waitsOnCalendar = expansion.calendarPending.length > 0;
92974
- const held = opts.heldWorkflowGates?.get(wf.ref);
92975
- if (held?.length) manual.push({ ref: wf.ref, type: "workflow-publish", name: wf.name, reason: heldWorkflowReason(wf.name, held) });
93842
+ const held2 = opts.heldWorkflowGates?.get(wf.ref);
93843
+ if (held2?.length) manual.push({ ref: wf.ref, type: "workflow-publish", name: wf.name, reason: heldWorkflowReason(wf.name, held2) });
92976
93844
  if (opts.publishWorkflows && !opts.gatedWorkflowRefs?.has(wf.ref) && waitsOnCalendar) {
92977
93845
  manual.push({ ref: wf.ref, type: "workflow-publish", name: wf.name, reason: `[${wf.name}] left as a draft and NOT switched on: it waits on a calendar that does not exist yet. Switch it on only after the calendar is picked on its trigger.` });
92978
93846
  }
@@ -93543,13 +94411,13 @@ ${text4}`);
93543
94411
  };
93544
94412
  }
93545
94413
  function makePipelineApi(builderClient) {
93546
- return async (method, path66, body) => {
93547
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path66}`;
94414
+ return async (method, path67, body) => {
94415
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path67}`;
93548
94416
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
93549
94417
  const response = await builderRequest(builderClient, method, url, sendsBody ? { body } : {});
93550
94418
  if (!response.ok) {
93551
94419
  const text4 = await response.text();
93552
- throw new Error(`Pipeline API ${response.status}: ${method} ${path66}
94420
+ throw new Error(`Pipeline API ${response.status}: ${method} ${path67}
93553
94421
  ${text4}`);
93554
94422
  }
93555
94423
  const text3 = await response.text();
@@ -93562,8 +94430,8 @@ ${text4}`);
93562
94430
  };
93563
94431
  }
93564
94432
  function makeFunnelApi(builderClient) {
93565
- return async (method, path66, body) => {
93566
- const url = `https://backend.leadconnectorhq.com/funnels${path66}`;
94433
+ return async (method, path67, body) => {
94434
+ const url = `https://backend.leadconnectorhq.com/funnels${path67}`;
93567
94435
  const sendsBody = body !== void 0 && (method === "POST" || method === "PUT");
93568
94436
  const response = await builderRequest(builderClient, method, url, {
93569
94437
  ...sendsBody ? { body } : {},
@@ -93576,7 +94444,7 @@ function makeFunnelApi(builderClient) {
93576
94444
  });
93577
94445
  if (!response.ok) {
93578
94446
  const text4 = await response.text();
93579
- throw new Error(`Funnel API ${response.status}: ${method} ${path66}
94447
+ throw new Error(`Funnel API ${response.status}: ${method} ${path67}
93580
94448
  ${text4.slice(0, 300)}`);
93581
94449
  }
93582
94450
  const text3 = await response.text();
@@ -94081,7 +94949,7 @@ function createdId(response, key2) {
94081
94949
  }
94082
94950
  function runningStageFor(locationId2) {
94083
94951
  try {
94084
- const st = JSON.parse(fs63.readFileSync(path63.join(appDataDir(), "cockpit-state.json"), "utf8"));
94952
+ const st = JSON.parse(fs64.readFileSync(path64.join(appDataDir(), "cockpit-state.json"), "utf8"));
94085
94953
  const clients = st && typeof st === "object" ? Reflect.get(st, "clients") : void 0;
94086
94954
  const entry = clients && typeof clients === "object" ? Reflect.get(clients, locationId2) : void 0;
94087
94955
  const stages = entry && typeof entry === "object" ? Reflect.get(entry, "stages") : void 0;
@@ -94158,6 +95026,27 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
94158
95026
  clientKey: import_zod120.z.string().optional().describe(`Which client in this sub-account the build belongs to, when the sub-account holds more than one (lowercase letters, digits, hyphens, e.g. "qpt"). The saved plan, review scope, approval, obligations and progress for this client are kept apart from the sub-account's default client, so a second clinic's build never overwrites the first one's records. Omit for the sub-account's default client (every build before this existed). Defaults to the clinicPrefix in lower case when a prefix is given.`)
94159
95027
  };
94160
95028
  const buildPlanArgs = import_zod120.z.object(buildPlanSchema2);
95029
+ const runLockedBuildPlan = async (args) => {
95030
+ if ((args.mode ?? "dry_run") !== "execute") return runBuildPlan(args);
95031
+ const locationId2 = client.resolveLocationId();
95032
+ const release = claimBuildLock(locationId2);
95033
+ if (!release) {
95034
+ let name2 = locationId2;
95035
+ try {
95036
+ name2 = (await readLocationBasics(client, locationId2)).name || locationId2;
95037
+ } catch {
95038
+ }
95039
+ return jsonResponse({ ok: false, phase: "build", buildRunning: true, error: buildRunningRefusal(name2) });
95040
+ }
95041
+ const beat = setInterval(() => touchBuildLock(locationId2), 6e4);
95042
+ beat.unref?.();
95043
+ try {
95044
+ return await runBuildPlan(args);
95045
+ } finally {
95046
+ clearInterval(beat);
95047
+ release();
95048
+ }
95049
+ };
94161
95050
  const runBuildPlan = async ({ plan: planArgIn, fromPreset, brief: briefArg, preset: presetArg, copy: copyArg, useSavedPlan, replaceSavedPlan, mode, locationId: locationId2, metHandoffs, publishWorkflows, textTemplates, onConflict, clinicPrefix: clinicPrefixArg, clientKey: clientKeyArg, acknowledgeSharedWorkflows }, persistPreview = false) => {
94162
95051
  try {
94163
95052
  const resolvedMode = mode ?? "dry_run";
@@ -94198,6 +95087,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
94198
95087
  const recordKey = clientRecordKey(activeLocation, clientKey);
94199
95088
  let planArg = planArgIn;
94200
95089
  let composed;
95090
+ let asksHash;
94201
95091
  if (fromPreset) {
94202
95092
  if (planArg) {
94203
95093
  return jsonResponse({ ok: false, phase: "compose", error: "fromPreset composes the plan itself \u2014 pass brief (and optionally preset / copy), not a plan. To run a hand-authored plan, drop fromPreset." });
@@ -94255,6 +95145,12 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
94255
95145
  if (!chosen.ok) {
94256
95146
  return jsonResponse({ ok: false, phase: "compose", error: chosen.error });
94257
95147
  }
95148
+ {
95149
+ const board = readBoardState();
95150
+ const ticks = board.bought?.[recordKey];
95151
+ briefForCompose = briefWithAsks(briefForCompose, board.asks?.[recordKey], Array.isArray(ticks) ? ticks : void 0, chosen.preset.skeleton?.workflows ?? []);
95152
+ asksHash = asksBindingHash(board.asks?.[recordKey], Array.isArray(ticks) ? ticks : void 0);
95153
+ }
94258
95154
  const composition = planFromPreset({ preset: chosen.preset, brief: briefForCompose, locationId: activeLocation, copy: copyArg });
94259
95155
  if (!composition.ok) {
94260
95156
  return jsonResponse({
@@ -94385,7 +95281,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
94385
95281
  const { name: locationName, timeZone: locationTimeZone } = await readLocationBasics(client, activeLocation);
94386
95282
  const { assets, warnings: scanWarnings } = await scanExistingAssets(client, activeLocation, registry2);
94387
95283
  const result = resolvePlan(typedPlan, assets, { metHandoffs });
94388
- const clientFields = { ...clientKey ? { clientKey } : {}, ...clinicPrefix ? { clinicPrefix } : {}, ...locationTimeZone ? { locationTimeZone } : {} };
95284
+ const clientFields = { ...clientKey ? { clientKey } : {}, ...asksHash ? { asksHash } : {}, ...clinicPrefix ? { clinicPrefix } : {}, ...locationTimeZone ? { locationTimeZone } : {} };
94389
95285
  if (clinicPrefix && clinicScoping) {
94390
95286
  let checked = false;
94391
95287
  let findings = [];
@@ -94555,6 +95451,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
94555
95451
  const record = (item) => {
94556
95452
  try {
94557
95453
  appendBuildObject({ runId, locationId: activeLocation, item, parents: journalParents });
95454
+ touchBuildLock(activeLocation);
94558
95455
  } catch (e) {
94559
95456
  throw new JournalWriteError(e instanceof Error ? e.message : String(e), { ref: item.ref, type: item.type, realId: item.realId });
94560
95457
  }
@@ -94820,7 +95717,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
94820
95717
  clientKey: import_zod120.z.string().optional().describe("The clientKey on the reply being read, when it carries one (two clients in one sub-account keep separate replies).")
94821
95718
  }).optional().describe("READ ONLY, builds nothing: when a reply was shortened to fit (it carries replyTrimmed), read one left-out section of that full reply, a page at a time. Pass detail alone, with no other argument.")
94822
95719
  },
94823
- ({ detail, ...args }) => detail ? Promise.resolve({ content: [{ type: "text", text: JSON.stringify(readBuildDetail(client.resolveLocationId(), detail)) }] }) : runBuildPlan(args)
95720
+ ({ detail, ...args }) => detail ? Promise.resolve({ content: [{ type: "text", text: JSON.stringify(readBuildDetail(client.resolveLocationId(), detail)) }] }) : runLockedBuildPlan(args)
94824
95721
  );
94825
95722
  const withClaimWarnings = (r) => r.plan ? { ...r, warnings: [...r.warnings, ...claimWarningsForPlan(r.plan)] } : r;
94826
95723
  server2.tool(
@@ -95300,7 +96197,7 @@ function intakeFormMatches(expected, actual) {
95300
96197
  const keys = ["id", "tag", "type", "hiddenFieldQueryKey", "required", "label", "options", "picklistOptions"];
95301
96198
  return expected.every((field2, i) => keys.every((key2) => field2[key2] === void 0 || JSON.stringify(field2[key2]) === JSON.stringify(actual[i]?.[key2])));
95302
96199
  }
95303
- var fs63, path63, import_zod120, buildNet, customFieldItemSchema, sleep7, NO_SELECTION_SCOPE, NOT_CONFIRMED_SCOPE, WHOLE_PRESET_NOT_KEPT, workflowInventorySchema, notConfirmedAsk, STAGE_LABELS;
96200
+ var fs64, path64, import_zod120, buildNet, customFieldItemSchema, sleep7, NO_SELECTION_SCOPE, NOT_CONFIRMED_SCOPE, WHOLE_PRESET_NOT_KEPT, workflowInventorySchema, notConfirmedAsk, STAGE_LABELS;
95304
96201
  var init_intake_to_build = __esm({
95305
96202
  "src/tools/intake-to-build.ts"() {
95306
96203
  "use strict";
@@ -95309,8 +96206,8 @@ var init_intake_to_build = __esm({
95309
96206
  init_build_contract();
95310
96207
  init_build_obligations();
95311
96208
  init_allowed_writes();
95312
- fs63 = __toESM(require("node:fs"));
95313
- path63 = __toESM(require("node:path"));
96209
+ fs64 = __toESM(require("node:fs"));
96210
+ path64 = __toESM(require("node:path"));
95314
96211
  init_credentials_store();
95315
96212
  import_zod120 = require("zod");
95316
96213
  init_workflow_builder_client();
@@ -95334,6 +96231,7 @@ var init_intake_to_build = __esm({
95334
96231
  init_brief2();
95335
96232
  init_plan_store();
95336
96233
  init_jobs();
96234
+ init_build_lock();
95337
96235
  init_client_record();
95338
96236
  init_clinic_prefix();
95339
96237
  init_clinic_prefix_registry();
@@ -95347,6 +96245,7 @@ var init_intake_to_build = __esm({
95347
96245
  init_claims_guard();
95348
96246
  init_plan_from_preset();
95349
96247
  init_board_roster();
96248
+ init_client_asks();
95350
96249
  init_features();
95351
96250
  init_executor();
95352
96251
  init_build_confirm();
@@ -95427,7 +96326,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env2 = process
95427
96326
  return findings.map((finding) => ({ finding, because: `"${finding.id}" cannot be checked: no saved key for sub-account ${loc}. Register it (register_location) or point the server at it first.` }));
95428
96327
  }
95429
96328
  const reader = token2 ? new GHLClient({ apiKey: token2.apiKey, locationId: loc }) : client;
95430
- return refuseUnseenFindings2((path66, params) => reader.get(path66, params ? { params } : void 0), loc, findings);
96329
+ return refuseUnseenFindings2((path67, params) => reader.get(path67, params ? { params } : void 0), loc, findings);
95431
96330
  }, () => client.defaultLocationId, clientScope);
95432
96331
  registerClientEngagementTools(wrap(CLIENT_ENGAGEMENTS_MODULE), registry2, clientScope);
95433
96332
  registerAuditReportTools(wrap(AUDIT_REPORT_MODULE));
@@ -95746,7 +96645,7 @@ function parseArgs3(argv) {
95746
96645
  function openInBrowser(file3) {
95747
96646
  const cmd = process.platform === "darwin" ? ["open", file3] : process.platform === "win32" ? ["cmd", "/c", "start", "", file3] : ["xdg-open", file3];
95748
96647
  try {
95749
- (0, import_node_child_process10.spawn)(cmd[0], cmd.slice(1), { stdio: "ignore", detached: true }).unref();
96648
+ (0, import_node_child_process11.spawn)(cmd[0], cmd.slice(1), { stdio: "ignore", detached: true }).unref();
95750
96649
  } catch {
95751
96650
  }
95752
96651
  }
@@ -95834,13 +96733,13 @@ async function runDesignCli(argv, io = { out: (s) => process.stdout.write(s + "\
95834
96733
  }
95835
96734
  return r.ok ? 0 : 1;
95836
96735
  }
95837
- var import_zod122, import_node_child_process10, import_node_path19, import_node_os3, USAGE2;
96736
+ var import_zod122, import_node_child_process11, import_node_path19, import_node_os3, USAGE2;
95838
96737
  var init_cli2 = __esm({
95839
96738
  "src/command-os/design/cli.ts"() {
95840
96739
  "use strict";
95841
96740
  import_zod122 = require("zod");
95842
96741
  init_brief_validation();
95843
- import_node_child_process10 = require("node:child_process");
96742
+ import_node_child_process11 = require("node:child_process");
95844
96743
  import_node_path19 = require("node:path");
95845
96744
  import_node_os3 = require("node:os");
95846
96745
  init_run2();
@@ -95868,8 +96767,8 @@ __export(index_exports, {
95868
96767
  });
95869
96768
  module.exports = __toCommonJS(index_exports);
95870
96769
  var dotenv2 = __toESM(require("dotenv"));
95871
- var path65 = __toESM(require("path"));
95872
- var fs65 = __toESM(require("fs"));
96770
+ var path66 = __toESM(require("path"));
96771
+ var fs66 = __toESM(require("fs"));
95873
96772
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
95874
96773
  init_version_compare();
95875
96774
  init_ghl_client();
@@ -95916,8 +96815,8 @@ var import_zod121 = require("zod");
95916
96815
 
95917
96816
  // src/skill-installer.ts
95918
96817
  var import_node_crypto34 = require("node:crypto");
95919
- var fs64 = __toESM(require("node:fs"));
95920
- var path64 = __toESM(require("node:path"));
96818
+ var fs65 = __toESM(require("node:fs"));
96819
+ var path65 = __toESM(require("node:path"));
95921
96820
  var os11 = __toESM(require("node:os"));
95922
96821
  function sha2563(buf) {
95923
96822
  return (0, import_node_crypto34.createHash)("sha256").update(buf).digest("hex");
@@ -95932,9 +96831,9 @@ function isOlderRelease(candidate, installed) {
95932
96831
  return false;
95933
96832
  }
95934
96833
  function bundledSkillsDir(baseDir) {
95935
- const candidate = path64.resolve(baseDir, "..", "skills");
96834
+ const candidate = path65.resolve(baseDir, "..", "skills");
95936
96835
  try {
95937
- return fs64.statSync(candidate).isDirectory() ? candidate : null;
96836
+ return fs65.statSync(candidate).isDirectory() ? candidate : null;
95938
96837
  } catch {
95939
96838
  return null;
95940
96839
  }
@@ -95942,10 +96841,10 @@ function bundledSkillsDir(baseDir) {
95942
96841
  function listFiles(root) {
95943
96842
  const out = [];
95944
96843
  const walk2 = (dir) => {
95945
- for (const entry of fs64.readdirSync(dir, { withFileTypes: true })) {
95946
- const full = path64.join(dir, entry.name);
96844
+ for (const entry of fs65.readdirSync(dir, { withFileTypes: true })) {
96845
+ const full = path65.join(dir, entry.name);
95947
96846
  if (entry.isDirectory()) walk2(full);
95948
- else if (entry.isFile()) out.push(path64.relative(root, full));
96847
+ else if (entry.isFile()) out.push(path65.relative(root, full));
95949
96848
  }
95950
96849
  };
95951
96850
  walk2(root);
@@ -95953,7 +96852,7 @@ function listFiles(root) {
95953
96852
  }
95954
96853
  function readMarker(markerPath) {
95955
96854
  try {
95956
- const parsed2 = JSON.parse(fs64.readFileSync(markerPath, "utf8"));
96855
+ const parsed2 = JSON.parse(fs65.readFileSync(markerPath, "utf8"));
95957
96856
  return parsed2 && typeof parsed2 === "object" && parsed2.files ? parsed2 : null;
95958
96857
  } catch {
95959
96858
  return null;
@@ -95961,7 +96860,7 @@ function readMarker(markerPath) {
95961
96860
  }
95962
96861
  function defaultSkillsDir(env2 = process.env) {
95963
96862
  const dir = env2.CLAUDE_CONFIG_DIR?.trim();
95964
- return dir ? path64.join(dir, "skills") : path64.join(os11.homedir(), ".claude", "skills");
96863
+ return dir ? path65.join(dir, "skills") : path65.join(os11.homedir(), ".claude", "skills");
95965
96864
  }
95966
96865
  function installBundledSkills(opts) {
95967
96866
  const result = {
@@ -95978,28 +96877,28 @@ function installBundledSkills(opts) {
95978
96877
  result.bundledDir = bundled;
95979
96878
  if (!bundled) return result;
95980
96879
  try {
95981
- fs64.mkdirSync(result.targetDir, { recursive: true });
96880
+ fs65.mkdirSync(result.targetDir, { recursive: true });
95982
96881
  } catch (e) {
95983
96882
  result.errors.push(`cannot create ${result.targetDir}: ${String(e)}`);
95984
96883
  return result;
95985
96884
  }
95986
- const markerPath = path64.join(result.targetDir, ".ghl-command-skills.json");
96885
+ const markerPath = path65.join(result.targetDir, ".ghl-command-skills.json");
95987
96886
  const marker = readMarker(markerPath) ?? { packageVersion: "", files: {} };
95988
96887
  const newMarker = { packageVersion: opts.packageVersion, files: { ...marker.files } };
95989
96888
  for (const rel of listFiles(bundled)) {
95990
96889
  try {
95991
- const src = fs64.readFileSync(path64.join(bundled, rel));
95992
- const dest = path64.join(result.targetDir, rel);
96890
+ const src = fs65.readFileSync(path65.join(bundled, rel));
96891
+ const dest = path65.join(result.targetDir, rel);
95993
96892
  const srcHash = sha2563(src);
95994
96893
  let destBuf = null;
95995
96894
  try {
95996
- destBuf = fs64.readFileSync(dest);
96895
+ destBuf = fs65.readFileSync(dest);
95997
96896
  } catch {
95998
96897
  destBuf = null;
95999
96898
  }
96000
96899
  if (destBuf === null) {
96001
- fs64.mkdirSync(path64.dirname(dest), { recursive: true });
96002
- fs64.writeFileSync(dest, src);
96900
+ fs65.mkdirSync(path65.dirname(dest), { recursive: true });
96901
+ fs65.writeFileSync(dest, src);
96003
96902
  newMarker.files[rel] = srcHash;
96004
96903
  result.installed.push(rel);
96005
96904
  continue;
@@ -96016,7 +96915,7 @@ function installBundledSkills(opts) {
96016
96915
  newMarker.packageVersion = marker.packageVersion;
96017
96916
  result.skippedOlderPackage.push(rel);
96018
96917
  } else if (lastWritten && destHash === lastWritten) {
96019
- fs64.writeFileSync(dest, src);
96918
+ fs65.writeFileSync(dest, src);
96020
96919
  newMarker.files[rel] = srcHash;
96021
96920
  result.updated.push(rel);
96022
96921
  } else {
@@ -96027,7 +96926,7 @@ function installBundledSkills(opts) {
96027
96926
  }
96028
96927
  }
96029
96928
  try {
96030
- fs64.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
96929
+ fs65.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
96031
96930
  } catch (e) {
96032
96931
  result.errors.push(`marker write failed: ${String(e)}`);
96033
96932
  }
@@ -96193,7 +97092,7 @@ dotenv2.config();
96193
97092
  setPkgVersion(pkg.version);
96194
97093
  {
96195
97094
  const configDirOverride = process.env.GHL_MCP_CONFIG_DIR?.trim();
96196
- if (configDirOverride && !path65.isAbsolute(configDirOverride)) {
97095
+ if (configDirOverride && !path66.isAbsolute(configDirOverride)) {
96197
97096
  process.stderr.write(
96198
97097
  `[ghl-mcp] GHL_MCP_CONFIG_DIR must be an absolute path (got "${configDirOverride}"). Use e.g. /data/ghl-mcp in a container, with a volume mounted at /data.
96199
97098
  `
@@ -96206,20 +97105,20 @@ process.on("unhandledRejection", (reason3) => {
96206
97105
  `);
96207
97106
  });
96208
97107
  function hardenSecretFilePerms() {
96209
- const repoDir = path65.resolve(__dirname, "..");
97108
+ const repoDir = path66.resolve(__dirname, "..");
96210
97109
  const candidates = [
96211
- { file: path65.join(repoDir, "start-mcp.sh"), mode: 448 },
97110
+ { file: path66.join(repoDir, "start-mcp.sh"), mode: 448 },
96212
97111
  // Legacy registry location (pre-migration); new location lives in app-data.
96213
- { file: path65.join(repoDir, ".ghl-tokens.json"), mode: 384 },
97112
+ { file: path66.join(repoDir, ".ghl-tokens.json"), mode: 384 },
96214
97113
  { file: tokenRegistryPath(), mode: 384 }
96215
97114
  ];
96216
97115
  for (const { file: file3, mode } of candidates) {
96217
97116
  let current;
96218
97117
  try {
96219
- if (!fs65.existsSync(file3)) continue;
96220
- current = fs65.statSync(file3).mode & 511;
97118
+ if (!fs66.existsSync(file3)) continue;
97119
+ current = fs66.statSync(file3).mode & 511;
96221
97120
  if (current !== mode) {
96222
- fs65.chmodSync(file3, mode);
97121
+ fs66.chmodSync(file3, mode);
96223
97122
  }
96224
97123
  } catch (error) {
96225
97124
  const message = error instanceof Error ? error.message : String(error);