@wrangled/cli 0.3.163 → 0.3.165

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/guide.js CHANGED
@@ -103,7 +103,7 @@ ${brand.name} writes and sends no emails or texts; the client's own tool (GoHigh
103
103
 
104
104
  ## What winners and flops taught: lessons
105
105
 
106
- Lessons are short plain sentences about why the winners won and the flops flopped, such as "Ads that open with a retiree's own money worry beat ads that open with a statistic: 4 of the top 5 this week, cost per sale $180 vs $410." A finished page or funnel test, something learned about a funnel step, or a follow-up email, text or sequence that helps sales makes a lesson too. Each lists what proves it: ads, pages, funnel steps, tests, emails, texts or sequences. A lesson that holds on an ad and an email is one lesson with both as proof. Read them before writing ads or pages: \`${cli} lessons\` lists the ones that hold, the most often seen first (\`--status retired\` lists the ones no longer true, with why; \`--about ads|pages|funnel|messages\` only the ones with a proof of that kind, \`--about cross_channel\` only the ones proven on two or more of ads, pages and messages; \`--dimension <d>\`, repeatable, only the ones along that dimension), and the brief carries them too. No person approves a lesson.
106
+ Lessons are short plain sentences about why the winners won and the flops flopped, such as "Ads that open with a retiree's own money worry beat ads that open with a statistic: 4 of the top 5 this week, cost per sale $180 vs $410." A finished page or funnel test, something learned about a funnel step, or a follow-up email, text or sequence that helps sales makes a lesson too. Each lists what proves it: ads, pages, funnel steps, tests, emails, texts or sequences. A lesson that holds on an ad and an email is one lesson with both as proof. Read them before writing ads or pages: \`${cli} lessons\` lists the ones that hold, the most often seen first (\`--status retired\` lists the ones no longer true, with why; \`--about ads|pages|funnel|messages\` only the ones with a proof of that kind, \`--about cross_channel\` only the ones proven on two or more of ads, pages and messages; \`--dimension <d>\`, repeatable, only the ones along that dimension), and the brief carries them too. No person approves a lesson. Lessons are kept in the workspace's warehouse table \`lessons\`, so you can join them with the tests and numbers behind them in one query: \`${cli} warehouse query "SELECT text, dimension, status FROM lessons"\`.
107
107
 
108
108
  Write one as JSON, \`{"text": "...", "dimension": "hook", "evidence": [{"ad": "<Meta ad id or SKU>", "numbers": "cost per sale $180 vs $410"}], "week": "2026-09-28"}\`, and run \`${cli} lessons add --file lesson.json\`. Each proof names exactly one of \`ad\`, \`page\` (a page's URL, kept as its page key, or a content item id), \`step\` (\`<funnel_key>.<step_key>\`), \`test\` (a funnel test id or Adzara test_id), \`message\` (an email or text's content item id, or its SKU, kept as its item id; one the content library does not have is refused) or \`sequence\` (\`ghl:<workflow id>\` or \`klaviyo:<flow id>\`), with its \`numbers\`. \`dimension\` (hook, angle, format, offer, persona, page, funnel_step, follow_up or other) and \`week\` (any day of the winners report's week; default the latest complete week) are optional. A lesson with the same words that still holds, or the one named in \`"reinforces": "<id>"\`, is reinforced instead of copied: it counts once more for a week it had not counted, so running the same week again changes no count. \`${cli} lessons retire <id> --note "<why>"\` retires one the numbers no longer back; it is kept with why. The \`lessons\` playbook (\`${cli} skill install lessons\` installs it as the \`${LESSONS_SKILL_NAME}\` skill) writes the week's lessons from the winners report.
109
109
 
@@ -117,7 +117,7 @@ The \`weekly-loop\` playbook (\`${cli} skill install weekly-loop\` installs it a
117
117
 
118
118
  ## Trying one change: tests
119
119
 
120
- A test writes down one change to a page or funnel step, tried against what runs now. Each test is kept in the workspace's warehouse, in \`cro_tests\` (one row per test), \`cro_test_arms\` (its sides) and \`cro_test_events\` (its history), so you can join tests with visits, sign-ups and sales in one query: \`${cli} warehouse query "SELECT test_id, status, elements FROM cro_tests"\`.
120
+ A test writes down one change to a page or funnel step, tried against what runs now. Each test is kept in the workspace's warehouse, in \`cro_tests\` (one row per test), \`cro_test_arms\` (its sides), \`cro_test_events\` (its history), \`cro_test_results_daily\` (each side's numbers per day) and \`cro_test_reported\` (numbers other tools counted), so you can join tests with visits, sign-ups and sales in one query: \`${cli} warehouse query "SELECT test_id, status, elements FROM cro_tests"\`.
121
121
 
122
122
  A test moves through these stages: \`idea\` (only the one change and where are needed), \`planned\` (the full plan is in and the owner said yes), \`running\` (its plan is frozen), then \`decided\`, \`abandoned\` or \`invalid\` (the tracking broke or traffic did not split as planned). Where is a funnel step (\`funnel_key\` and \`step_key\` from the funnels map), a page by its address (\`page_url\`, even one not on the map yet), or both. The full plan is: \`because\` (what led to the idea), \`audience\`, \`expected_lift\` (relative, 0.18 for 18%), \`prediction_confidence\` (low, medium or high), \`disproved_if\`, \`design_type\` (how traffic is split: ad_split_randomized, Meta's own A/B test; ad_split_duplicated, two copies of a campaign, one to each page, which is not a true random split; tool_split, inside a page builder or webinar tool named in a \`design_note\`; link_split; before_after; or adaptive), \`traffic\` (\`{campaigns, optimization_goal, note}\`: which campaigns feed it and what they optimize for, frozen with the plan), \`metric\` (step_conversion, which needs a step, sales_per_visitor, revenue_per_visitor, cost_per_sale or cost_per_lead, spend per sign-up) and \`unit\` (people or sessions), \`guardrails\` (numbers that must not get worse), \`baseline_rate\` with \`baseline_from\` and \`baseline_to\`, \`min_lift\`, \`people_per_arm\`, \`min_days\` and \`max_days\` (14 and 56 unless set), \`decision_rule\`, \`concurrent_changes\`, and the control and 1 to 3 \`variants\`, each with the exact \`content\` it shows and, when a tool runs the split, its \`platform_ref\` (the tool's own id for the side). Tags come from fixed lists: \`elements\` (what changes: headline, subheadline, hero, call_to_action, form, offer, price, guarantee, proof, urgency, page_length, layout, checkout, order_bump, follow_up, tool_or_platform, other) and \`levers\` (why it should work: clarity, relevance, trust, value, urgency, less_effort, motivation, other). \`ice_impact\`, \`ice_confidence\` and \`ice_ease\` (1 to 10) sort the idea list. \`evidence\` quotes the proof, each \`{kind, ref, note}\`, kind one of leak, lesson, customer_words, page, ad or other. A test changes one thing: a \`change\` naming two is refused.
123
123
 
@@ -125,10 +125,12 @@ Write it as JSON, such as \`${JSON.stringify(funnelTestExample)}\`, and run \`${
125
125
 
126
126
  A test that already ran, such as one from before ${brand.name}, goes in with \`logged_after: true\` and its \`started_on\` and \`ended_on\`; it goes straight from idea to decided with \`${cli} tests decide <test_id> --decision <d> --outcome <o>\`.
127
127
 
128
- \`${cli} tests results <test_id>\` reads a started test: each side's people, how many moved on to the next step, how many of those people bought (each once, however many times they bought) and what they paid, spend and the test's metric, over the days since it started (a \`before_after\` test compares the same number of days before it started). People are counted by their first visit: the landing page they arrived on, or the Adzara ad (by \`asset_version\` and the test's \`test_id\`) that brought them; a page no one's first visit was recorded on is counted by its site visitors instead. Then it says plainly whether the test can be called yet: "Not enough people yet: 210 of the 400 each side needs.", "Video at the top is ahead, 36% vs 30%, and that gap is big enough to trust." or "No real difference after 900 people each side". A gap counts when a two-proportion test says it is real at 95%; cost per sale and revenue per visitor are judged on sales per visitor under them, and cost per lead on sign-ups per visitor.
128
+ \`${cli} tests results <test_id>\` reads a started test: each side's people, how many moved on to the next step, how many of those people bought (each once, however many times they bought) and what they paid, spend and the test's metric, over the days since it started (a \`before_after\` test compares the same number of days before it started). People are counted by their first visit: the landing page they arrived on, or the Adzara ad (by \`asset_version\` and the test's \`test_id\`) that brought them; a page no one's first visit was recorded on is counted by its visits instead, and a plan whose \`unit\` is sessions counts its pages by visits (a visit is one visitor on the page on one day). A side counted by people is never called against one counted by visits. Each side's numbers per day are in \`cro_test_results_daily\` (a person counts on their first-visit day; spend on the day it was spent), refreshed after each sync, so you can chart a test with \`${cli} warehouse query\`. Once a test is decided, its results are saved with it and \`${cli} tests results\` shows them as they were at the decision. Then it says plainly whether the test can be called yet: "Not enough people yet: 210 of the 400 each side needs.", "Video at the top is ahead, 36% vs 30%, and that gap is big enough to trust." or "No real difference after 900 people each side". A gap counts when a two-proportion test says it is real at 95%; cost per sale and revenue per visitor are judged on sales per visitor under them, and cost per lead on sign-ups per visitor.
129
129
 
130
130
  \`${cli} tests decide <test_id> --decision <d> --outcome <won|lost|inconclusive|invalid> --note "<why>"\` records the decision on a running test: \`scale\` (the new version won; use it everywhere), \`continue\` (keep running), \`iterate\` (promising; it also writes down a sharper version linked to this one), \`hold\` (pause it; something outside the test got in the way) or \`stop\` (it lost or made no difference). scale, iterate and stop need \`--outcome\`. Each decision writes a lesson with the test, its pages and its step as proof, in one go with the decision. Deciding changes nothing live: switching a page or ads still goes through approval.
131
131
 
132
+ When the split runs where ${brand.name} cannot count it (GoHighLevel's own split-test stats, a Mailchimp subject test, a Meta A/B test), enter each side's numbers as the tool shows them with \`${cli} tests report <test_id> --file counts.json\`, for a running test or one written down after it ran. The file is \`{"source_tool": "GoHighLevel", "unit": "people", "from": "2026-09-01", "to": "2026-09-30", "arms": [{"arm": "control", "people": 400, "conversions": 80}, {"arm": "variant_1", "people": 410, "conversions": 110}]}\`: conversions is what the test is judged on, as the tool counts it (moved on, bought or signed up), and \`revenue\`, \`spend\`, \`currency\`, \`tool_confidence\` (the tool's own confidence, such as Meta's 87% as 0.87) and \`note\` are optional. They are kept in \`cro_test_reported\`, apart from what ${brand.name} counts, and \`${cli} tests results\` calls them on their own with words starting "Counted by <tool>". To correct them, report the same tool, sides and dates again; the latest wins. A tool's own confidence is shown as its figure, never as the chance the variant is better, and Meta's purchase count alone never calls a test judged on sales: compare it with the sales in the client's own records.
133
+
132
134
  The \`cro\` playbook (\`${cli} skill install cro\` installs it as the \`${CRO_SKILL_NAME}\` skill; \`${cli} docs cro\` prints it) runs this once a week: it turns the costliest leaks from \`${cli} funnels leaks\` into at most 3 test ideas, each with one change, its full plan and its proof, for the owner to approve in the agent's own session, and reads the tests already running. It never publishes, edits a live page, or touches ads or messages.
133
135
 
134
136
  ## Content: search before SQL
@@ -589,7 +591,7 @@ ${cli} number-alerts [--since <date>] the key numbers that swung far from norm
589
591
  ${cli} facts propose --file <fact.json> suggest a fact for a person to confirm
590
592
  ${cli} lessons [--status active|retired] [--about ads|pages|funnel|messages|cross_channel] [--dimension <d>] | lessons add --file <lesson.json> | lessons retire <id> --note "<why>" lessons from winners and flops, writing one, retiring one
591
593
  ${cli} tests p1 this brand's P1 test ads by verdict, judged on its own sales against its P1 thresholds, with Meta's count beside
592
- ${cli} tests [--status <status>] [--funnel <key>] [--limit <n>] | tests show <id> | tests draft --file <test.json> | tests start <id> | tests abandon <id> [--note "<why>"] | tests results <id> | tests decide <id> --decision <d> [--note "<why>"] funnel tests, one change each
594
+ ${cli} tests [--status <status>] [--funnel <key>] [--limit <n>] | tests show <id> | tests draft --file <test.json> | tests start <id> | tests abandon <id> [--note "<why>"] | tests results <id> | tests decide <id> --decision <d> [--note "<why>"] | tests report <id> --file <counts.json> funnel tests, one change each
593
595
  ${cli} ads tags [--ad <id>] [--status <status>] [--limit <n>] | ads tag --file <tags.json> the marketing tags of ads not made here, and suggesting them
594
596
  ${cli} ads names [--unparsed] Meta ad names and the tags each gives; --unparsed lists those that fit no pattern
595
597
  ${cli} ads tag-formats tag every untagged ad's format (image, video or carousel) from Meta's records, free; run it before ads autotag
package/dist/main.js CHANGED
@@ -3342,6 +3342,18 @@ const commands = {
3342
3342
  console.log(`Wrote down the next version as ${next.test_id}.`);
3343
3343
  },
3344
3344
  },
3345
+ "tests report": {
3346
+ usage: "tests report <test_id> --file <counts.json> | < counts.json enter each side's numbers as another tool counted them (GoHighLevel, Meta, Mailchimp...), for a running test or one written down after it ran; reporting the same tool, side and dates again corrects them",
3347
+ async run(args, options) {
3348
+ if (args.length !== 1)
3349
+ throw new CliError("Usage: tests report <test_id> --file <counts.json> | < counts.json", exitCodes.usage);
3350
+ const reported = await api(await resolveConnection(options), `${testPath(args[0])}/reported`, await readToolInput(options));
3351
+ if (options.format === "json")
3352
+ return printJson(reported);
3353
+ console.log(`Kept the numbers for test ${reported.test.test_id}.`);
3354
+ printReported(reported.reported);
3355
+ },
3356
+ },
3345
3357
  "lessons retire": {
3346
3358
  usage: 'lessons retire <id> --note "<why it is no longer true>" retire a lesson the numbers no longer back; it is kept with why',
3347
3359
  async run(args, options) {
@@ -4861,6 +4873,8 @@ function eventWords(e) {
4861
4873
  return `note: ${d.note}`;
4862
4874
  case "decided":
4863
4875
  return `decided ${d.decision}${d.outcome ? ` (${d.outcome})` : ""}${d.note ? `: ${d.note}` : ""}`;
4876
+ case "reported":
4877
+ return `${d.source_tool}'s numbers for ${(d.arms ?? []).join(", ")}, ${d.from} to ${d.to}`;
4864
4878
  }
4865
4879
  }
4866
4880
  const RESULT_COLUMNS = ["side", "label", "counted by", "people", "moved on", "sign-ups", "buyers", "revenue", "spend", "metric", "dates"];
@@ -4879,10 +4893,33 @@ function printResults(r) {
4879
4893
  metric: a.metric_value === null ? "" : r.test.metric === "cost_per_sale" || r.test.metric === "cost_per_lead" || r.test.metric === "revenue_per_visitor" ? money(a.metric_value, a.currency) : `${(a.metric_value * 100).toFixed(1)}%`,
4880
4894
  dates: `${a.window.from} to ${a.window.to}`,
4881
4895
  }));
4882
- const lines = [`${r.test.test_id} (${r.test.status}) judged by ${r.test.metric ?? "no main number"}, ${r.test.people_per_arm ?? "no count of"} people each side`, renderTable(RESULT_COLUMNS, rows), "", ...r.calls.map((c) => c.words)];
4896
+ const saved = r.saved_at === null ? "" : ` numbers as saved at the decision, ${r.saved_at.slice(0, 10)}`;
4897
+ const lines = [`${r.test.test_id} (${r.test.status}) judged by ${r.test.metric ?? "no main number"}, ${r.test.people_per_arm ?? "no count of"} people each side${saved}`];
4898
+ if (r.arms.length > 0)
4899
+ lines.push(renderTable(RESULT_COLUMNS, rows), "", ...r.calls.map((c) => c.words));
4900
+ lines.push(...(r.words && r.arms.every((a) => a.people === 0) ? ["", r.words] : []));
4883
4901
  if (r.caveats.length > 0)
4884
4902
  lines.push("", ...r.caveats.map((c) => `Note: ${c}`));
4885
4903
  console.log(lines.join("\n"));
4904
+ printReported(r.reported);
4905
+ }
4906
+ const REPORTED_COLUMNS = ["side", "label", "counted", "conversions", "revenue", "spend", "tool's confidence", "dates", "entered"];
4907
+ function printReported(reported) {
4908
+ const money = (n, currency) => (n === null ? "" : `${n.toFixed(2)}${currency ? ` ${currency}` : ""}`);
4909
+ for (const r of reported) {
4910
+ const rows = r.arms.map((a) => ({
4911
+ side: a.arm,
4912
+ label: excerpt(a.label, 30),
4913
+ counted: `${a.people} ${r.unit}`,
4914
+ conversions: a.conversions ?? "",
4915
+ revenue: money(a.revenue, a.currency),
4916
+ spend: money(a.spend, a.currency),
4917
+ "tool's confidence": a.tool_confidence === null ? "" : `${(a.tool_confidence * 100).toFixed(0)}%`,
4918
+ dates: `${a.from} to ${a.to}`,
4919
+ entered: a.entered_at.slice(0, 10),
4920
+ }));
4921
+ console.log(["", `Reported by ${r.source_tool}`, renderTable(REPORTED_COLUMNS, rows), "", r.words, ...r.caveats.map((c) => `Note: ${c}`)].join("\n"));
4922
+ }
4886
4923
  }
4887
4924
  const LESSON_COLUMNS = ["id", "seen", "last week", "dimension", "lesson"];
4888
4925
  function lessonRow(l) {
package/dist/playbooks.js CHANGED
@@ -2137,6 +2137,7 @@ Without a shell: MCP \`get_funnel_test_results\` with \`{"id": "<test_id>"}\`.
2137
2137
  \`calls\` has one call for each changed version against the control: \`not_enough_yet\` (with the \`people\` each side has and the \`need\`), \`variant_ahead\`, \`control_ahead\` or \`no_real_difference\`, and \`words\` says it plainly. \`caveats\` names what weakens the call.
2138
2138
 
2139
2139
  - **Every call is \`not_enough_yet\`:** leave the test running; that needs no command. Only when something outside the test got in the way (the page broke, a price changed, the ads stopped), suggest \`hold\` to the owner.
2140
+ - **The split runs in another tool** (a \`tool_split\` in GoHighLevel, a Meta A/B test): read each side's numbers in that tool and enter them with \`${cli} tests report <test_id> --file counts.json\` (MCP \`report_test_numbers\`), then read the results again; their words start "Counted by <tool>". Never decide a test judged on sales on Meta's purchase count alone.
2140
2141
  - **A call is ready:** suggest \`scale\` (the new version won), \`stop\` (it lost or made no difference) or \`iterate\` (promising; it writes down a sharper version). \`iterate\` writes a new test, so suggest it only while the week is under its 3; otherwise suggest \`stop\` and draft the sharper version next week.
2141
2142
 
2142
2143
  Put each suggestion to the owner with the ideas from step 4, then record the owner's decision:
@@ -2168,6 +2169,7 @@ ${cli} tests plan <test_id> --approved-by "<owner>" record the owner's ye
2168
2169
  ${cli} tests start <test_id> record that a planned, live test is running
2169
2170
  ${cli} tests abandon <test_id> --note "<why>" drop a test the owner rejected
2170
2171
  ${cli} tests results <test_id> --format json a running test's numbers per side and whether it can be called
2172
+ ${cli} tests report <test_id> --file <counts.json> enter each side's numbers from the tool that runs the split
2171
2173
  ${cli} tests decide <test_id> --decision <d> --outcome <o> --note "<why>" record the owner's decision; it writes the lesson
2172
2174
  \`\`\`
2173
2175
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.163",
3
+ "version": "0.3.165",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {