@wrangled/cli 0.3.162 → 0.3.164

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
@@ -7,17 +7,30 @@ import { PULL_STATE_FILE } from "./pull.js";
7
7
  const cli = brand.slug;
8
8
  /** The funnel test the guide shows; the app's parser must accept it (src/lib/cli-consistency.test.ts). */
9
9
  export const funnelTestExample = {
10
- hypothesis: "A video at the top of the sign-up page will get more people to sign up, because buyers on calls say they did not know what the webinar was about.",
10
+ name: "Video at the top of the sign-up page",
11
11
  funnel_key: "webinar",
12
12
  step_key: "signup",
13
- variable: "top of the page: letter to video",
14
- control: { label: "letter at the top", url: "https://example.com/signup" },
15
- variants: [{ label: "video at the top", url: "https://example.com/signup-video" }],
16
- metric: "step_conversion",
17
- expected_change: "sign-ups per visitor from 22% to 26%",
18
- min_people_per_arm: 400,
19
- split_method: "adzara_ads",
13
+ change: "top of the page: letter to video",
14
+ because: "Buyers on sales calls say they did not know what the webinar was about, and sign-ups per visitor fell from 30% to 22% this month.",
15
+ audience: "new visitors from Facebook ads",
16
+ expected_lift: 0.18,
17
+ prediction_confidence: "medium",
18
+ disproved_if: "sign-ups per visitor rise less than 5% after 400 people each side",
20
19
  evidence: [{ kind: "customer_words", ref: "sales call 2026-09-30", note: "I didn't know what the webinar was about." }],
20
+ design_type: "ad_split_duplicated",
21
+ metric: "step_conversion",
22
+ unit: "people",
23
+ guardrails: ["cost per sale"],
24
+ baseline_rate: 0.22,
25
+ baseline_from: "2026-09-01",
26
+ baseline_to: "2026-09-30",
27
+ min_lift: 0.1,
28
+ people_per_arm: 400,
29
+ decision_rule: "Scale the video if sign-ups per visitor rise at least 10% with cost per sale no worse.",
30
+ elements: ["hero"],
31
+ levers: ["clarity"],
32
+ control: { label: "letter at the top", content: "A letter from the founder: why this webinar exists.", url: "https://example.com/signup" },
33
+ variants: [{ label: "video at the top", content: "A 90-second video of the founder saying what the webinar covers.", url: "https://example.com/signup-video" }],
21
34
  };
22
35
  /** The dashboard the guide shows; the app's parser must accept it (src/lib/cli-consistency.test.ts). */
23
36
  export const dashboardExample = {
@@ -90,7 +103,7 @@ ${brand.name} writes and sends no emails or texts; the client's own tool (GoHigh
90
103
 
91
104
  ## What winners and flops taught: lessons
92
105
 
93
- 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"\`.
94
107
 
95
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.
96
109
 
@@ -102,17 +115,21 @@ Write one as JSON, \`{"text": "...", "dimension": "hook", "evidence": [{"ad": "<
102
115
 
103
116
  The \`weekly-loop\` playbook (\`${cli} skill install weekly-loop\` installs it as the \`${WEEKLY_LOOP_SKILL_NAME}\` skill; \`${cli} docs weekly-loop\` prints it) is a sample to borrow from, not a schedule, for a local agent that runs a client's Facebook ads with its owner: its steps can run daily, several times a week or weekly, and the winners report covers whatever dates are asked for. It names the command and the MCP tool for each step: the winners and flops report, lessons, fresh customer words, the brief, the offers and funnels map, making ads (Exodus, paid tools or the agent's own skills), publishing approved ads, pauses and budget changes with ${brand.name}'s Meta commands, and the results of what was published (Adzara's labels for ads it published before the switch). Judge winners by ${brand.name}'s Winners and flops report, which judges each client on its goal (\`${cli} winners\`); Meta's own results are Meta's count. Approvals and decisions happen in the agent's own session with the owner; ${brand.name} records none. Ads are published and changed through ${brand.name}'s Meta commands, only after the owner approves and only inside the client's control list.
104
117
 
105
- ## Trying one change: funnel tests
118
+ ## Trying one change: tests
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"\`.
121
+
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.
106
123
 
107
- A funnel test writes down one change to a page or funnel step, tried against what runs now: the idea, the funnel and step (\`funnel_key\` and \`step_key\` from the funnels map), the one thing that changes, the control and 1 to 3 changed versions, the number that judges it (\`metric\`: step_conversion, sales_per_visitor, revenue_per_visitor or cost_per_sale) with the change expected and the people each side needs, how traffic is split (\`split_method\`: adzara_ads, before_after, or other with a \`split_note\`), and the evidence it should work (each \`{kind, ref, note}\`, kind one of leak, lesson, customer_words, page, ad or other). A test changes one thing: a \`variable\` naming two is refused, and so is a test with no metric.
124
+ Write it as JSON, such as \`${JSON.stringify(funnelTestExample)}\`, and run \`${cli} tests draft --file test.json\`. It gets Adzara-style labels from the change and this month (\`hyp-<slug>\` and \`test-<yyyy-mm>-<slug>\`); give \`hypothesis_id\` and \`test_id\` to reuse Adzara's own, so the results of the ads Adzara published for the test (\`ad_test_results_daily\`, \`ad_hypothesis_results_daily\`) join on the same label. \`${cli} tests edit <test_id> --file changes.json\` changes some fields (null clears one). ${brand.name} shows no approval screen: ask the owner in your own session, then record the yes with \`${cli} tests plan <test_id> --approved-by "<owner>"\`, which refuses a plan with a gap and names every missing field. \`${cli} tests start <test_id>\` marks a planned test as running from today and freezes its plan; an edit after that is still saved, and its history and results say so. Starting changes nothing live: the page still goes live through its own approval, and the ads through ${brand.name}'s Meta commands. Until #2202, an ad split test can be measured only on ads Adzara made before the switch, because its sides are counted from Adzara's labels; a \`before_after\` test still works. \`${cli} tests abandon <test_id> --note "<why>"\` drops one that has not ended, \`${cli} tests invalidate <test_id> --note "<what went wrong>"\` ends a running one whose numbers cannot be trusted, and \`${cli} tests note <test_id> --note "<note>"\` adds to its history. \`${cli} tests [--status <status>] [--funnel <funnel_key>] [--limit <n>]\` lists them, newest first (50 unless \`--limit\`, at most 200), and \`${cli} tests show <test_id>\` shows one with its history.
108
125
 
109
- Write it as JSON, such as \`${JSON.stringify(funnelTestExample)}\`, and run \`${cli} tests draft --file test.json\`. It gets Adzara-style labels from the change and this month (\`hyp-<slug>\` and \`test-<yyyy-mm>-<slug>\`); give \`hypothesis_id\` and \`test_id\` to reuse Adzara's own, so the results of the ads Adzara published for the test (\`ad_test_results_daily\`, \`ad_hypothesis_results_daily\`) join on the same label. Approving a test happens outside ${brand.name} for now. \`${cli} tests start <test_id>\` marks a draft as running from today, and changes nothing live: the page still goes live through its own approval, and the ads through ${brand.name}'s Meta commands. Until #2202, an \`adzara_ads\` test can be measured only on ads Adzara made before the switch, because its sides are counted from Adzara's labels; a \`before_after\` test still works. \`${cli} tests abandon <test_id> --note "<why>"\` drops a draft or running test. \`${cli} tests [--status <status>] [--funnel <funnel_key>] [--limit <n>]\` lists them, newest first (50 unless \`--limit\`, at most 200), and \`${cli} tests show <test_id>\` shows one.
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>\`.
110
127
 
111
- \`${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.
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.
112
129
 
113
- \`${cli} tests decide <test_id> --decision <d> --note "<why>"\` records the decision on a running test: \`scale\` (the new version won; use it everywhere), \`continue\` (keep running), \`iterate\` (promising; it also drafts 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). Each decision writes a lesson with the test, its pages and its step as proof. Deciding changes nothing live: switching a page or ads still goes through approval.
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.
114
131
 
115
- 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 drafts, each with one change 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.
132
+ 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.
116
133
 
117
134
  ## Content: search before SQL
118
135
 
package/dist/main.js CHANGED
@@ -3220,32 +3220,55 @@ const commands = {
3220
3220
  },
3221
3221
  },
3222
3222
  "tests show": {
3223
- usage: "tests show <id|test_id> one funnel test in full",
3223
+ usage: "tests show <test_id> one funnel test in full, with its history",
3224
3224
  async run(args, options) {
3225
3225
  if (args.length !== 1)
3226
- throw new CliError("Usage: tests show <id|test_id>", exitCodes.usage);
3227
- const { test } = await api(await resolveConnection(options), testPath(args[0]));
3226
+ throw new CliError("Usage: tests show <test_id>", exitCodes.usage);
3227
+ const shown = await api(await resolveConnection(options), testPath(args[0]));
3228
3228
  if (options.format === "json")
3229
- return printJson({ test });
3230
- printTest(test);
3229
+ return printJson(shown);
3230
+ printTest(shown.test, shown.events);
3231
3231
  },
3232
3232
  },
3233
3233
  "tests draft": {
3234
- usage: "tests draft --file <test.json> | < test.json write down a funnel test: one change against a control, a metric and the evidence",
3234
+ usage: "tests draft --file <test.json> | < test.json write down a test idea: the one change and where (a funnel step or a page); the rest of the plan may come later with tests edit",
3235
3235
  async run(args, options) {
3236
3236
  if (args.length > 0)
3237
3237
  throw new CliError("Usage: tests draft --file <test.json> | < test.json", exitCodes.usage);
3238
3238
  const { test } = await api(await resolveConnection(options), TESTS_PATH, await readToolInput(options));
3239
3239
  if (options.format === "json")
3240
3240
  return printJson({ test });
3241
- console.log(`Drafted test ${test.test_id} (${test.id}).`);
3241
+ console.log(`Wrote down test ${test.test_id} as an idea.`);
3242
+ },
3243
+ },
3244
+ "tests edit": {
3245
+ usage: "tests edit <test_id> --file <changes.json> | < changes.json change some of a test's fields (null clears one); a change to the plan after the start is kept and marked in its history",
3246
+ async run(args, options) {
3247
+ if (args.length !== 1)
3248
+ throw new CliError("Usage: tests edit <test_id> --file <changes.json> | < changes.json", exitCodes.usage);
3249
+ const { test } = await api(await resolveConnection(options), testPath(args[0]), await readToolInput(options), "PATCH");
3250
+ if (options.format === "json")
3251
+ return printJson({ test });
3252
+ console.log(`Changed test ${test.test_id} (${test.status}).`);
3253
+ },
3254
+ },
3255
+ "tests plan": {
3256
+ usage: 'tests plan <test_id> --approved-by "<owner>" [--note "<what they said>"] record the owner\'s yes to a test\'s full plan, after asking them in your session',
3257
+ async run(args, options) {
3258
+ if (args.length !== 1 || !options.approvedBy)
3259
+ throw new CliError('Usage: tests plan <test_id> --approved-by "<owner>" [--note "<what they said>"]', exitCodes.usage);
3260
+ const body = { approved_by: options.approvedBy, ...(options.note ? { note: options.note } : {}) };
3261
+ const { test } = await api(await resolveConnection(options), `${testPath(args[0])}/plan`, body);
3262
+ if (options.format === "json")
3263
+ return printJson({ test });
3264
+ console.log(`Test ${test.test_id} is planned; ${options.approvedBy} said yes.`);
3242
3265
  },
3243
3266
  },
3244
3267
  "tests start": {
3245
- usage: "tests start <id|test_id> mark a draft test as running from today; nothing goes live",
3268
+ usage: "tests start <test_id> mark a planned test as running from today and freeze its plan; nothing goes live",
3246
3269
  async run(args, options) {
3247
3270
  if (args.length !== 1)
3248
- throw new CliError("Usage: tests start <id|test_id>", exitCodes.usage);
3271
+ throw new CliError("Usage: tests start <test_id>", exitCodes.usage);
3249
3272
  const { test } = await api(await resolveConnection(options), `${testPath(args[0])}/start`, {});
3250
3273
  if (options.format === "json")
3251
3274
  return printJson({ test });
@@ -3253,21 +3276,43 @@ const commands = {
3253
3276
  },
3254
3277
  },
3255
3278
  "tests abandon": {
3256
- usage: 'tests abandon <id|test_id> [--note "<why>"] drop a test that has not ended',
3279
+ usage: 'tests abandon <test_id> [--note "<why>"] drop a test that has not ended',
3257
3280
  async run(args, options) {
3258
3281
  if (args.length !== 1)
3259
- throw new CliError('Usage: tests abandon <id|test_id> [--note "<why>"]', exitCodes.usage);
3282
+ throw new CliError('Usage: tests abandon <test_id> [--note "<why>"]', exitCodes.usage);
3260
3283
  const { test } = await api(await resolveConnection(options), `${testPath(args[0])}/abandon`, options.note ? { note: options.note } : {});
3261
3284
  if (options.format === "json")
3262
3285
  return printJson({ test });
3263
3286
  console.log(`Abandoned test ${test.test_id}.`);
3264
3287
  },
3265
3288
  },
3289
+ "tests invalidate": {
3290
+ usage: 'tests invalidate <test_id> --note "<what went wrong>" end a running test whose tracking broke or whose traffic did not split as planned',
3291
+ async run(args, options) {
3292
+ if (args.length !== 1 || !options.note)
3293
+ throw new CliError('Usage: tests invalidate <test_id> --note "<what went wrong>"', exitCodes.usage);
3294
+ const { test } = await api(await resolveConnection(options), `${testPath(args[0])}/invalidate`, { note: options.note });
3295
+ if (options.format === "json")
3296
+ return printJson({ test });
3297
+ console.log(`Test ${test.test_id} is invalid from ${test.ended_on}.`);
3298
+ },
3299
+ },
3300
+ "tests note": {
3301
+ usage: 'tests note <test_id> --note "<note>" add a note to a test\'s history',
3302
+ async run(args, options) {
3303
+ if (args.length !== 1 || !options.note)
3304
+ throw new CliError('Usage: tests note <test_id> --note "<note>"', exitCodes.usage);
3305
+ const { events } = await api(await resolveConnection(options), `${testPath(args[0])}/notes`, { note: options.note });
3306
+ if (options.format === "json")
3307
+ return printJson({ events });
3308
+ console.log(`Noted on test ${args[0]}.`);
3309
+ },
3310
+ },
3266
3311
  "tests results": {
3267
- usage: "tests results <id|test_id> a started test's numbers per side and whether it can be called yet",
3312
+ usage: "tests results <test_id> a started test's numbers per side and whether it can be called yet",
3268
3313
  async run(args, options) {
3269
3314
  if (args.length !== 1)
3270
- throw new CliError("Usage: tests results <id|test_id>", exitCodes.usage);
3315
+ throw new CliError("Usage: tests results <test_id>", exitCodes.usage);
3271
3316
  const { results } = await api(await resolveConnection(options), `${testPath(args[0])}/results`);
3272
3317
  if (options.format === "json")
3273
3318
  return printJson({ results });
@@ -3275,20 +3320,26 @@ const commands = {
3275
3320
  },
3276
3321
  },
3277
3322
  "tests decide": {
3278
- usage: 'tests decide <id|test_id> --decision <scale|continue|iterate|hold|stop> [--note "<why>"] record the decision on a running test, with its lesson; nothing goes live',
3323
+ usage: 'tests decide <test_id> --decision <scale|continue|iterate|hold|stop> [--outcome <won|lost|inconclusive|invalid>] [--note "<why>"] [--learning "<what it taught>"] record the decision, with its lesson; scale, iterate and stop need --outcome; nothing goes live',
3279
3324
  async run(args, options) {
3280
- if (args.length !== 1 || !options.decision) {
3281
- throw new CliError('Usage: tests decide <id|test_id> --decision <scale|continue|iterate|hold|stop> [--note "<why>"]', exitCodes.usage);
3282
- }
3283
- const decided = await api(await resolveConnection(options), `${testPath(args[0])}/decide`, options.note ? { decision: options.decision, note: options.note } : { decision: options.decision });
3325
+ const usage = 'Usage: tests decide <test_id> --decision <scale|continue|iterate|hold|stop> [--outcome <won|lost|inconclusive|invalid>] [--note "<why>"] [--learning "<what it taught>"]';
3326
+ if (args.length !== 1 || !options.decision)
3327
+ throw new CliError(usage, exitCodes.usage);
3328
+ const body = {
3329
+ decision: options.decision,
3330
+ ...(options.outcome ? { outcome: options.outcome } : {}),
3331
+ ...(options.note ? { note: options.note } : {}),
3332
+ ...(options.learning ? { learning: options.learning } : {}),
3333
+ };
3334
+ const decided = await api(await resolveConnection(options), `${testPath(args[0])}/decide`, body);
3284
3335
  if (options.format === "json")
3285
3336
  return printJson(decided);
3286
3337
  const { test, lesson, next_test: next } = decided;
3287
3338
  console.log(test.decision === options.decision
3288
- ? `Test ${test.test_id}: ${test.decision}, now ${test.status}. Lesson ${lesson.id}.`
3339
+ ? `Test ${test.test_id}: ${test.decision}${test.outcome ? ` (${test.outcome})` : ""}, now ${test.status}. Lesson ${lesson.id}.`
3289
3340
  : `Test ${test.test_id}: ${options.decision} recorded; it stays ${test.status} (${test.decision}). Lesson ${lesson.id}.`);
3290
3341
  if (next)
3291
- console.log(`Drafted the next version as ${next.test_id} (${next.id}).`);
3342
+ console.log(`Wrote down the next version as ${next.test_id}.`);
3292
3343
  },
3293
3344
  },
3294
3345
  "lessons retire": {
@@ -4746,32 +4797,73 @@ const RULE_LIST_IDS = ["banned_claims", "words_to_avoid", "required_disclaimers"
4746
4797
  const VOICE_SUGGESTIONS_PATH = "/api/v1/voice-lists/suggestions";
4747
4798
  const LESSONS_PATH = "/api/v1/lessons";
4748
4799
  const TESTS_PATH = "/api/v1/funnel-tests";
4749
- const TEST_COLUMNS = ["test_id", "status", "funnel", "step", "change", "metric"];
4800
+ const TEST_COLUMNS = ["test_id", "status", "where", "change", "metric"];
4750
4801
  const testPath = (ref) => `${TESTS_PATH}/${encodeURIComponent(ref)}`;
4751
4802
  function testRow(t) {
4752
- return { test_id: t.test_id, status: t.status, funnel: t.funnel_key, step: t.step_key, change: excerpt(t.variable, 50), metric: t.metric };
4753
- }
4754
- function printTest(t) {
4755
- const arm = (a) => [a.label, a.page && `page ${a.page}`, a.url, a.item_id && `item ${a.item_id}`, a.asset_version && `version ${a.asset_version}`].filter(Boolean).join(" · ");
4803
+ return { test_id: t.test_id, status: t.status, where: testWhere(t), change: excerpt(t.change, 50), metric: t.metric ?? "" };
4804
+ }
4805
+ const testWhere = (t) => [t.step_key && `${t.funnel_key} / ${t.step_key}`, t.page_key].filter(Boolean).join(", ");
4806
+ function printTest(t, events) {
4807
+ const arm = (a) => [
4808
+ a.label,
4809
+ a.page_key ?? a.url,
4810
+ a.item_id && `item ${a.item_id}`,
4811
+ a.asset_version && `version ${a.asset_version}`,
4812
+ a.platform_ref && `tool id ${a.platform_ref}`,
4813
+ a.weight !== null && `${Math.round(a.weight * 100)}% of traffic`,
4814
+ ]
4815
+ .filter(Boolean)
4816
+ .join(" · ");
4817
+ const pct = (n) => (n === null ? null : `${Math.round(n * 1000) / 10}%`);
4818
+ const line = (label, value) => (value === null || value === undefined || value === "" ? [] : [`${label}: ${value}`]);
4819
+ const variants = t.arms.filter((a) => a.arm !== "control");
4756
4820
  const lines = [
4757
- `${t.test_id} (${t.status}) idea ${t.hypothesis_id} id ${t.id}`,
4758
- `Idea: ${t.hypothesis}`,
4759
- `Where: ${t.funnel_key} / ${t.step_key}`,
4760
- `The one change: ${t.variable}`,
4761
- `Stays the same: ${arm(t.control)}`,
4762
- ...t.variants.map((v, i) => `${t.variants.length === 1 ? "Changed" : `Changed ${i + 1}`}: ${arm(v)}`),
4763
- `Judged by: ${t.metric}, ${t.expected_change}, ${t.min_people_per_arm} people each side`,
4764
- `Split: ${t.split_method}${t.split_note ? `, ${t.split_note}` : ""}`,
4765
- "Evidence:",
4766
- ...t.evidence.map((e) => ` ${e.kind}: ${e.ref}${e.note ? ` (${e.note})` : ""}`),
4821
+ `${t.test_id} (${t.status}) idea ${t.hypothesis_id}${t.logged_after ? " written down after it ran" : ""}`,
4822
+ ...line("Name", t.name === t.change ? null : t.name),
4823
+ `Where: ${testWhere(t)}`,
4824
+ `The one change: ${t.change}`,
4825
+ ...line("Because", t.because),
4826
+ ...line("Who sees it", t.audience),
4827
+ ...line("Expected lift", pct(t.expected_lift) && `${pct(t.expected_lift)}${t.prediction_confidence ? `, ${t.prediction_confidence} confidence` : ""}`),
4828
+ ...line("Wrong if", t.disproved_if),
4829
+ ...t.arms.filter((a) => a.arm === "control").map((a) => `Stays the same: ${arm(a)}`),
4830
+ ...variants.map((v, i) => `${variants.length === 1 ? "Changed" : `Changed ${i + 1}`}: ${arm(v)}`),
4831
+ ...t.arms.filter((a) => a.content !== null).map((a) => ` ${a.arm} shows: ${excerpt(a.content, 100)}`),
4832
+ ...line("Split", t.design_type && `${t.design_type}${t.design_note ? `, ${t.design_note}` : ""}`),
4833
+ ...line("Judged by", t.metric && `${t.metric} by ${t.unit ?? "people"}${t.people_per_arm ? `, ${t.people_per_arm} each side` : ""}, ${t.min_days} to ${t.max_days} days`),
4834
+ ...line("Today", pct(t.baseline_rate) && `${pct(t.baseline_rate)} from ${t.baseline_from} to ${t.baseline_to}; smallest lift worth finding ${pct(t.min_lift)}`),
4835
+ ...line("Must not get worse", t.guardrails.join(", ")),
4836
+ ...line("Decision rule", t.decision_rule),
4837
+ ...line("Also changing", t.concurrent_changes),
4838
+ ...line("Traffic", t.traffic && [t.traffic.campaigns?.length && `campaigns ${t.traffic.campaigns.join(", ")}`, t.traffic.optimization_goal && `optimized for ${t.traffic.optimization_goal}`, t.traffic.note].filter(Boolean).join("; ")),
4839
+ ...line("Tags", [...t.elements, ...t.levers].join(", ")),
4840
+ ...line("ICE", t.ice_impact !== null || t.ice_confidence !== null || t.ice_ease !== null ? `${t.ice_impact ?? "-"} / ${t.ice_confidence ?? "-"} / ${t.ice_ease ?? "-"}` : null),
4841
+ ...(t.evidence.length > 0 ? ["Evidence:", ...t.evidence.map((e) => ` ${e.kind}: ${e.ref}${e.note ? ` (${e.note})` : ""}`)] : []),
4842
+ ...line("Started", t.started_on && `${t.started_on}${t.ended_on ? `, ended ${t.ended_on}` : ""}`),
4843
+ ...line("Decided", t.decision && `${t.decision}${t.outcome ? `, ${t.outcome}` : ""}`),
4844
+ ...line("Note", t.decision_note),
4845
+ ...line("Learned", t.learning),
4767
4846
  ];
4768
- if (t.started_on)
4769
- lines.push(`Started: ${t.started_on}${t.ended_on ? `, ended ${t.ended_on}` : ""}`);
4770
- if (t.decision_note)
4771
- lines.push(`Note: ${t.decision_note}`);
4847
+ if (events.length > 0)
4848
+ lines.push("History:", ...events.map((e) => ` ${e.at.slice(0, 16).replace("T", " ")} ${eventWords(e)} (${e.author})`));
4772
4849
  console.log(lines.join("\n"));
4773
4850
  }
4774
- const RESULT_COLUMNS = ["side", "label", "counted by", "people", "moved on", "buyers", "revenue", "spend", "metric", "dates"];
4851
+ function eventWords(e) {
4852
+ const d = e.detail;
4853
+ switch (e.kind) {
4854
+ case "created":
4855
+ return "written down";
4856
+ case "edited":
4857
+ return `changed ${Object.keys(d.fields ?? {}).join(", ")}${d.after_start ? " after it started" : ""}`;
4858
+ case "status":
4859
+ return `${d.from} to ${d.to}${d.approved_by ? `, ${d.approved_by} said yes` : ""}${d.note ? `: ${d.note}` : ""}`;
4860
+ case "note":
4861
+ return `note: ${d.note}`;
4862
+ case "decided":
4863
+ return `decided ${d.decision}${d.outcome ? ` (${d.outcome})` : ""}${d.note ? `: ${d.note}` : ""}`;
4864
+ }
4865
+ }
4866
+ const RESULT_COLUMNS = ["side", "label", "counted by", "people", "moved on", "sign-ups", "buyers", "revenue", "spend", "metric", "dates"];
4775
4867
  function printResults(r) {
4776
4868
  const money = (n, currency) => (n === null ? "" : `${n.toFixed(2)}${currency ? ` ${currency}` : ""}`);
4777
4869
  const rows = r.arms.map((a) => ({
@@ -4780,13 +4872,14 @@ function printResults(r) {
4780
4872
  "counted by": a.counted_by,
4781
4873
  people: a.people,
4782
4874
  "moved on": a.moved_on ?? "",
4875
+ "sign-ups": a.leads ?? "",
4783
4876
  buyers: a.buyers ?? "",
4784
4877
  revenue: money(a.revenue, a.currency),
4785
4878
  spend: money(a.spend, a.currency),
4786
- metric: a.metric_value === null ? "" : r.test.metric === "cost_per_sale" || r.test.metric === "revenue_per_visitor" ? money(a.metric_value, a.currency) : `${(a.metric_value * 100).toFixed(1)}%`,
4879
+ 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)}%`,
4787
4880
  dates: `${a.window.from} to ${a.window.to}`,
4788
4881
  }));
4789
- const lines = [`${r.test.test_id} (${r.test.status}) judged by ${r.test.metric}, ${r.test.min_people_per_arm} people each side`, renderTable(RESULT_COLUMNS, rows), "", ...r.calls.map((c) => c.words)];
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)];
4790
4883
  if (r.caveats.length > 0)
4791
4884
  lines.push("", ...r.caveats.map((c) => `Note: ${c}`));
4792
4885
  console.log(lines.join("\n"));
@@ -5196,9 +5289,13 @@ Options:
5196
5289
  the sync matches several kinds, so its next read joins the item
5197
5290
  --notes <text> what changed in the version skill publish adds
5198
5291
  --note <why> why: what content create and edit changed, why lessons retire says a lesson is no longer true,
5199
- or why tests abandon drops one; for profile upload, what the files are, in the client's words;
5292
+ why tests abandon drops one, what tests invalidate says went wrong, the note tests note adds, or what the
5293
+ owner said with tests plan; for profile upload, what the files are, in the client's words;
5200
5294
  for meta ad promote into a campaign, a few words on the new ad set's audience;
5201
5295
  for scorecard record, what was checked and what was found; for repairs open and update, a progress line
5296
+ --approved-by <name> tests plan: who said yes to the test's plan, as the owner gave their name in your session
5297
+ --outcome <o> tests decide: what the test found, won, lost, inconclusive or invalid; needed with scale, iterate and stop
5298
+ --learning <text> tests decide: what the test taught, in a sentence, kept on the test
5202
5299
  --about <a> with lessons, only the lessons with a proof about ads, pages, funnel (steps and tests) or messages
5203
5300
  (emails, texts and sequences), or cross_channel: proven on two or more of ads, pages and messages
5204
5301
  --dimension <d> with lessons, only the lessons along that dimension (hook, angle, format, offer, persona, page, funnel_step, follow_up, other); repeat for several
@@ -5436,6 +5533,9 @@ function parseOptions(argv) {
5436
5533
  "merged-at": { type: "string" },
5437
5534
  probes: { type: "string" },
5438
5535
  decision: { type: "string" },
5536
+ outcome: { type: "string" },
5537
+ learning: { type: "string" },
5538
+ "approved-by": { type: "string" },
5439
5539
  harness: { type: "string" },
5440
5540
  platform: { type: "string" },
5441
5541
  library: { type: "string" },
@@ -5664,6 +5764,9 @@ function parseOptions(argv) {
5664
5764
  mergedAt: values["merged-at"],
5665
5765
  probes: values.probes,
5666
5766
  decision: values.decision,
5767
+ outcome: values.outcome,
5768
+ learning: values.learning,
5769
+ approvedBy: values["approved-by"],
5667
5770
  notes: values.notes,
5668
5771
  harness: values.harness,
5669
5772
  platform: values.platform,
package/dist/playbooks.js CHANGED
@@ -1994,21 +1994,33 @@ ${pagesPlaybook}`;
1994
1994
  export const CRO_SKILL_NAME = `${cli}-cro`;
1995
1995
  /** The test the cro playbook shows, exported so a test checks the server accepts it. */
1996
1996
  export const croTestExample = {
1997
- hypothesis: "A text 15 minutes before the webinar will get more sign-ups to show up, because people on sales calls say they did not know it was live.",
1998
1997
  funnel_key: "webinar_to_course",
1999
1998
  step_key: "attend",
2000
- variable: "a reminder text 15 minutes before the start",
2001
- control: { label: "today's reminders: an email the day before and an hour before" },
2002
- variants: [{ label: "the same reminders plus a text 15 minutes before" }],
2003
- metric: "step_conversion",
2004
- expected_change: "sign-ups who attend from 30% to 35%",
2005
- min_people_per_arm: 300,
2006
- split_method: "other",
2007
- split_note: "The reminder workflow sends the text to every other new sign-up.",
1999
+ change: "a reminder text 15 minutes before the start",
2000
+ because: "Attendance fell from 38% to 30% of sign-ups, and people on sales calls say they did not know the webinar was live.",
2001
+ audience: "everyone who signs up for the webinar",
2002
+ expected_lift: 0.15,
2003
+ prediction_confidence: "medium",
2004
+ disproved_if: "attendance rises less than 5% after 300 sign-ups each side",
2008
2005
  evidence: [
2009
2006
  { kind: "leak", ref: "webinar_to_course.attend", note: "Attendance fell from 38% to 30% of sign-ups, about $2,300 a month." },
2010
2007
  { kind: "customer_words", ref: "sales call 2026-10-02", note: "I didn't know the webinar was live." },
2011
2008
  ],
2009
+ design_type: "link_split",
2010
+ design_note: "The reminder workflow sends the text to every other new sign-up.",
2011
+ metric: "step_conversion",
2012
+ unit: "people",
2013
+ guardrails: ["unsubscribes"],
2014
+ baseline_rate: 0.3,
2015
+ baseline_from: "2026-09-01",
2016
+ baseline_to: "2026-09-30",
2017
+ min_lift: 0.1,
2018
+ people_per_arm: 300,
2019
+ decision_rule: "Keep the text if attendance rises at least 10% with unsubscribes no higher.",
2020
+ elements: ["follow_up"],
2021
+ levers: ["urgency"],
2022
+ control: { label: "today's reminders: an email the day before and an hour before", content: "Email the day before and an hour before; no text." },
2023
+ variants: [{ label: "the same reminders plus a text 15 minutes before", content: "We're live in 15 minutes: {link}" }],
2012
2024
  };
2013
2025
  export const croPlaybook = `# Funnel test playbook
2014
2026
 
@@ -2039,7 +2051,7 @@ Without a shell: MCP \`get_funnel_leaks\`, then MCP \`list_funnel_tests\` with \
2039
2051
 
2040
2052
  In \`leaks.json\`, \`leaks.leaks\` lists every step doing worse than its normal, costliest first. Each has its \`funnel_key\` and \`step_key\`, its \`recent\` and \`baseline\` rates, \`people_lost_vs_baseline\`, \`money_at_stake\` per currency (an estimate; never add currencies together) and, for some steps, \`splits\` by channel, campaign and ad. An empty list means no step is leaking: write no ideas and go to step 5.
2041
2053
 
2042
- \`tests.json\` holds the tests already written, newest first, each with its \`status\`, \`funnel_key\`, \`step_key\` and \`created_at\`. Pick the 1 to 3 costliest leaks you can act on whose step has no \`draft\` or \`running\` test. Count every test created since Monday toward the 3, whatever its status now, abandoned ones included: with 3 already, write no new ideas this week.
2054
+ \`tests.json\` holds the tests already written, newest first, each with its \`status\`, \`funnel_key\`, \`step_key\` and \`created_at\`. Pick the 1 to 3 costliest leaks you can act on whose step has no \`idea\`, \`planned\` or \`running\` test. Count every test created since Monday toward the 3, whatever its status now, abandoned ones included: with 3 already, write no new ideas this week.
2043
2055
 
2044
2056
  ## 2. Gather proof for each leak
2045
2057
 
@@ -2066,9 +2078,11 @@ Write each test to its own file with your file tool, such as \`test-1.json\`:
2066
2078
  ${JSON.stringify(croTestExample)}
2067
2079
  \`\`\`
2068
2080
 
2069
- - \`variable\` is the one change. \`control\` is what runs now; \`variants\` is the changed version, each \`{label, page?, url?, item_id?, asset_version?}\`.
2070
- - \`metric\` is the number that should move: \`step_conversion\`, \`sales_per_visitor\`, \`revenue_per_visitor\` or \`cost_per_sale\`. \`expected_change\` says how much, and \`min_people_per_arm\` how many people each side needs before deciding.
2071
- - \`split_method\` is \`adzara_ads\` (two ads, one to each page), \`before_after\`, or \`other\` with a \`split_note\`. Until #2202, an \`adzara_ads\` test is counted only from Adzara's labels, so it can be measured only on ads Adzara made before the switch; for a new test, use \`before_after\`.
2081
+ - \`change\` is the one change. \`because\` is what led to it, \`audience\` who sees it, \`expected_lift\` how much it should move (0.15 for 15%) and \`prediction_confidence\` how sure you are; \`disproved_if\` says what result would prove it wrong.
2082
+ - \`control\` is what runs now; \`variants\` is the changed version, each \`{label, content, page?, url?, item_id?, asset_version?}\`. \`content\` is the exact words or change each side shows.
2083
+ - \`metric\` is the number that should move: \`step_conversion\` (only with a \`step_key\`), \`sales_per_visitor\`, \`revenue_per_visitor\`, \`cost_per_sale\` or \`cost_per_lead\` (spend per sign-up), counted by \`unit\` (people or sessions). \`baseline_rate\` is today's rate over \`baseline_from\` to \`baseline_to\`, \`min_lift\` the smallest lift worth finding, \`people_per_arm\` how many each side needs, \`guardrails\` the numbers that must not get worse, and \`decision_rule\` how you will call it. \`min_days\` and \`max_days\` are 14 and 56 unless set.
2084
+ - \`design_type\` is 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; not a true random split), \`tool_split\` (inside a page builder or webinar tool, named in \`design_note\`), \`link_split\`, \`before_after\` or \`adaptive\`. \`traffic\` says which campaigns feed it and what they optimize for. Until #2202, an ad split test is counted only from Adzara's labels, so it can be measured only on ads Adzara made before the switch; for a new test, use \`before_after\`.
2085
+ - \`elements\` and \`levers\` tag what changes and why it should work, from \`${cli} guide\`'s fixed lists.
2072
2086
  - \`evidence\` quotes the proof, each \`{kind, ref, note}\` with kind \`leak\`, \`lesson\`, \`customer_words\`, \`page\`, \`ad\` or \`other\`. Start with the leak itself.
2073
2087
  - Give \`hypothesis_id\` and \`test_id\` only to reuse Adzara's own labels; left out, they are made for you.
2074
2088
 
@@ -2080,13 +2094,21 @@ ${cli} tests draft --file test-1.json --format json > drafted-1.json
2080
2094
 
2081
2095
  Without a shell: MCP \`draft_funnel_test\` with the same JSON.
2082
2096
 
2083
- A test that changes two things is refused: split it into two tests and keep the stronger one.
2097
+ It is written down as an \`idea\`. A test that changes two things is refused: split it into two tests and keep the stronger one.
2084
2098
 
2085
2099
  ## 4. Put the ideas to the owner
2086
2100
 
2087
- Approvals happen in your own session with the owner. ${brand.name} has no approval step and records none. For each draft, give the owner the leak and what it costs, the one change, the number that should move, and the proof in a line. The owner says yes or no.
2101
+ Approvals happen in your own session with the owner. ${brand.name} shows no approval screen. For each idea, give the owner the leak and what it costs, the one change, the number that should move, and the proof in a line. The owner says yes or no.
2102
+
2103
+ When the owner says yes, record who said it. It refuses a plan with a gap and names each missing field; fill them in with \`${cli} tests edit <test_id> --file changes.json\` and try again.
2104
+
2105
+ \`\`\`sh
2106
+ ${cli} tests plan <test_id> --approved-by "<the owner's name>"
2107
+ \`\`\`
2108
+
2109
+ Without a shell: MCP \`plan_funnel_test\` with \`{"id": "<test_id>", "approved_by": "<the owner's name>"}\`.
2088
2110
 
2089
- When the owner says yes, the change goes live through its own path: a page through its own approval (the pages playbook), ads through ${brand.name}'s Meta commands (an \`adzara_ads\` split cannot be measured on those new ads until #2202). Once it is actually live, and no other test runs on the same step, mark it running. This changes nothing live; it only records the start day.
2111
+ Then the change goes live through its own path: a page through its own approval (the pages playbook), ads through ${brand.name}'s Meta commands (an \`adzara_ads\` split cannot be measured on those new ads until #2202). Once it is actually live, and no other test runs on the same step, mark it running. This changes nothing live; it records the start day and freezes the plan.
2090
2112
 
2091
2113
  \`\`\`sh
2092
2114
  ${cli} tests start <test_id>
@@ -2115,15 +2137,15 @@ Without a shell: MCP \`get_funnel_test_results\` with \`{"id": "<test_id>"}\`.
2115
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.
2116
2138
 
2117
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.
2118
- - **A call is ready:** suggest \`scale\` (the new version won), \`stop\` (it lost or made no difference) or \`iterate\` (promising; it drafts a sharper version). \`iterate\` drafts a new test, so suggest it only while the week is under its 3; otherwise suggest \`stop\` and draft the sharper version next week.
2140
+ - **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.
2119
2141
 
2120
2142
  Put each suggestion to the owner with the ideas from step 4, then record the owner's decision:
2121
2143
 
2122
2144
  \`\`\`sh
2123
- ${cli} tests decide <test_id> --decision <scale|iterate|stop|hold> --note "<why, in a sentence>"
2145
+ ${cli} tests decide <test_id> --decision <scale|iterate|stop|hold> --outcome <won|lost|inconclusive> --note "<why, in a sentence>"
2124
2146
  \`\`\`
2125
2147
 
2126
- Without a shell: MCP \`decide_funnel_test\` with \`{"id": "<test_id>", "decision": "<scale|iterate|stop|hold>", "note": "<why, in a sentence>"}\`.
2148
+ Without a shell: MCP \`decide_funnel_test\` with \`{"id": "<test_id>", "decision": "<scale|iterate|stop|hold>", "outcome": "<won|lost|inconclusive>", "note": "<why, in a sentence>"}\`. scale, iterate and stop need the outcome; hold does not.
2127
2149
 
2128
2150
  Deciding changes nothing live. It writes the lesson itself, with the test, its pages and its step as proof, so write no lesson of your own for it.
2129
2151
 
@@ -2140,11 +2162,13 @@ ${cli} lessons [--about pages|funnel] --format json what earlier tests ta
2140
2162
  ${cli} voice --since <YYYY-MM-DD> --sort useful --json fresh customer words
2141
2163
  ${cli} pages results --format json which landing pages bring real sales
2142
2164
  ${cli} pages brief --step <key> [--funnel <key>] --format json the step's current page and the ads that send people there
2143
- ${cli} tests draft --file <test.json> write down a test
2144
- ${cli} tests start <test_id> record that an approved, live test is running
2165
+ ${cli} tests draft --file <test.json> write down a test idea
2166
+ ${cli} tests edit <test_id> --file <changes.json> fill in or change its plan
2167
+ ${cli} tests plan <test_id> --approved-by "<owner>" record the owner's yes to the full plan
2168
+ ${cli} tests start <test_id> record that a planned, live test is running
2145
2169
  ${cli} tests abandon <test_id> --note "<why>" drop a test the owner rejected
2146
2170
  ${cli} tests results <test_id> --format json a running test's numbers per side and whether it can be called
2147
- ${cli} tests decide <test_id> --decision <d> --note "<why>" record the owner's decision; it writes the lesson
2171
+ ${cli} tests decide <test_id> --decision <d> --outcome <o> --note "<why>" record the owner's decision; it writes the lesson
2148
2172
  \`\`\`
2149
2173
  `;
2150
2174
  export const croSkill = `---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.162",
3
+ "version": "0.3.164",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {