@wrangled/cli 0.3.31 → 0.3.33

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
@@ -3,6 +3,20 @@ import { brand, envPrefix } from "./brand.js";
3
3
  import { BACK_TAG_SKILL_NAME, GRAMMAR_SKILL_NAME, LEARN_NAMES_SKILL_NAME, LESSONS_SKILL_NAME, MAP_OFFERS_SKILL_NAME, PLAYBOOK_CHOICES, STORY_SKILL_NAME } from "./playbooks.js";
4
4
  import { PULL_STATE_FILE } from "./pull.js";
5
5
  const cli = brand.slug;
6
+ /** The funnel test the guide shows; the app's parser must accept it (src/lib/cli-consistency.test.ts). */
7
+ export const funnelTestExample = {
8
+ 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.",
9
+ funnel_key: "webinar",
10
+ step_key: "signup",
11
+ variable: "top of the page: letter to video",
12
+ control: { label: "letter at the top", url: "https://example.com/signup" },
13
+ variants: [{ label: "video at the top", url: "https://example.com/signup-video" }],
14
+ metric: "step_conversion",
15
+ expected_change: "sign-ups per visitor from 22% to 26%",
16
+ min_people_per_arm: 400,
17
+ split_method: "adzara_ads",
18
+ evidence: [{ kind: "customer_words", ref: "sales call 2026-09-30", note: "I didn't know what the webinar was about." }],
19
+ };
6
20
  export const guide = `# ${brand.name} CLI
7
21
 
8
22
  \`${cli}\` gives read-only SQL access to one client's marketing data warehouse (ad spend, orders, and other synced sources), runs paid generation and research tools for that client, and keeps the creative ledger. Each API key belongs to exactly one workspace, and every command works only in that workspace.
@@ -38,6 +52,12 @@ Write one as JSON, \`{"text": "...", "dimension": "hook", "evidence": [{"ad": "<
38
52
 
39
53
  \`${cli} winners send-to-exodus\` sends the week's winners to Exodus so it makes new ads from them: one package per Facebook page, each winner with its reason, its numbers, and a note of the lessons it proves, persona lessons and the week's freshest customer words. Winners whose page, words or format are unknown are left out and listed with why. \`--week <YYYY-MM-DD>\` picks the week (default: the latest complete week). \`--dry-run\` asks Exodus what it would create or update and writes nothing. Sending the same week again is safe: an unchanged package is not sent twice, and Exodus updates each ad in place. A real send needs the workspace's Exodus brand and a Scrape Creators key on the Exodus account.
40
54
 
55
+ ## Trying one change: funnel tests
56
+
57
+ 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.
58
+
59
+ 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 publishes 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 Adzara. \`${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.
60
+
41
61
  ## Content: search before SQL
42
62
 
43
63
  To find content, such as "the 10 emails with the best click rate", "our 5 best videos by return on ad spend" or "every landing page that mentions the guarantee", use \`${cli} content search\` before writing SQL on the \`content_\` tables.
@@ -193,11 +213,11 @@ The confirmed definition with key \`ad_name_key\` is the client's name key: the
193
213
 
194
214
  ## Ad tags
195
215
 
196
- Ads not made here (built by hand or by an agency, names in no pattern) get the marketing grammar's tags (docs/marketing-grammar.md) by Meta ad id. \`${cli} ads tags\` lists them (\`--ad <id>\`, \`--status proposed|confirmed|rejected|replaced\`, \`--limit <n>\`, \`--json\`); by default the live ones, proposed and confirmed. A proposed tag is usable at once, but not yet confirmed; only an Owner or Admin confirms, rejects or changes tags, on the app's Ad tags page.
216
+ Ads not made here (built by hand or by an agency, names in no pattern) get the marketing grammar's tags (docs/marketing-grammar.md) by Meta ad id. \`${cli} ads tags\` lists them (\`--ad <id>\`, \`--status proposed|confirmed|rejected|replaced\`, \`--limit <n>\`, \`--json\`); by default the live ones, proposed and confirmed. A proposed tag is usable at once, but not yet confirmed; only an Owner or Admin confirms, rejects or changes tags, on the app's Tag check page.
197
217
 
198
218
  To suggest tags, write them as JSON and run \`${cli} ads tag --file tags.json\` (or pipe it on stdin), up to 500 ads at a time: \`{"ads": [{"ad_id": "120214567890123", "tags": {"angle": "discovery", "desire": "money", "hook_spot": "spoken", "hook_text": "$29,000 is the smallest lead", "hook_kind": "big_win", "format": "video", "style": "presenter", "tag_reasons": {"worry": "none"}}, "confidence": "medium", "evidence": {"ad_name": "County lead v2", "transcript_start": "$29,000 is the smallest lead"}}]}\`. The tag fields are \`marketing\`, \`offer_key\`, \`landing_funnel_key\`, \`landing_step_key\`, \`persona\`, \`awareness\`, \`concept\`, \`angle\`, \`desire\`, \`hook_spot\`, \`hook_text\`, \`hook_kind\`, \`hook2_spot\`, \`hook2_kind\`, \`format\`, \`style\`, \`speaker\`, \`cta\`, \`proof\` and \`worry\`; leave out what you did not judge. A part with no value goes in \`tag_reasons\` as \`none\` (nothing on its list applies) or \`unclear\` (too little to judge, never \`high\` confidence). Confidence is \`high\`, \`medium\` or \`low\`. A value off its list is refused with the allowed values. New tags replace an ad's proposed ones, which stay as \`replaced\`; an ad whose tags a person confirmed is kept as it is.
199
219
 
200
- To have the server tag ads for you, run \`${cli} ads autotag\` (\`--limit <n>\`, 1 to 100, default 25; \`--ad <id>\` for one ad; \`--json\`). It takes the biggest spenders with no tags yet, so each run moves on to the next ones (with \`--ad\`, that ad, its proposed tags asked about again and confirmed ones kept), reads each one's copy, video transcript and picture description, and asks TypeSafe's Jev the grammar's questions through the tool gateway, so a run's tool cap applies. Ads with the same copy, headlines, button, link and media are asked about once and share the tags; asking again about an unchanged ad costs nothing. It costs a tiny fraction of a cent per ad. The tags are proposed, with each part's confidence in the evidence; read them with \`${cli} ads tags\`. The \`back-tag\` playbook (\`${cli} skill install back-tag\` installs it as the \`${BACK_TAG_SKILL_NAME}\` skill; \`${cli} docs back-tag\` prints it) runs it in batches over a client's old ads and reports how much of the spend is tagged; a person then checks the ads carrying the top 20% of spend on the app's Ad tags page.
220
+ To have the server tag ads for you, run \`${cli} ads autotag\` (\`--limit <n>\`, 1 to 100, default 25; \`--ad <id>\` for one ad; \`--json\`). It takes the biggest spenders with no tags yet, so each run moves on to the next ones (with \`--ad\`, that ad, its proposed tags asked about again and confirmed ones kept), reads each one's copy, video transcript and picture description, and asks TypeSafe's Jev the grammar's questions through the tool gateway, so a run's tool cap applies. Ads with the same copy, headlines, button, link and media are asked about once and share the tags; asking again about an unchanged ad costs nothing. It costs a tiny fraction of a cent per ad. The tags are proposed, with each part's confidence in the evidence; read them with \`${cli} ads tags\`. The \`back-tag\` playbook (\`${cli} skill install back-tag\` installs it as the \`${BACK_TAG_SKILL_NAME}\` skill; \`${cli} docs back-tag\` prints it) runs it in batches over a client's old ads and reports how much of the spend is tagged; a person then checks the ads carrying the top 20% of spend on the app's Tag check page.
201
221
 
202
222
  For what worked, query the \`ad_performance_tagged\` view: every Meta ad's daily results with its live tags next to them, \`tags_source\` (\`ad_tags\`, or null when untagged) and \`tags_status\`.
203
223
 
@@ -267,6 +287,7 @@ ${cli} knowledge tag <entry-id> --hook-kind <v> --confidence <c> tag a library
267
287
  ${cli} guide what this workspace's data holds, how fresh it is, and what is missing: read it first
268
288
  ${cli} brief [--from <date>] [--to <date>] [--channel <channel>] [--campaign <id>] [--top <n>] the offer, voice, best and worst ads, pages, results and audience: start here before writing ads
269
289
  ${cli} pages brief --step <key> [--funnel <key>] | --page <ref> the offer, step, current page, best and worst pages of its kind, its ads and how to hand a page back: start here before writing a landing page
290
+ ${cli} pages results [--from <date>] [--to <date>] [--page <ref>] each landing page's, and each ad and page pair's, real first-touch sales, cost per sale and sales per page view
270
291
  ${cli} winners [--week <date>] [--top <n>] which Facebook ads won and flopped this week and over 30 days, judged on first-touch sales
271
292
  ${cli} winners send-to-exodus [--week <date>] [--dry-run] send the week's winners, with their reasons, lessons and customer words, to Exodus; safe to send again
272
293
  ${cli} warehouse schema [--summary] [--table <name> ...] [--search <words>] list tables and columns; --summary for one row per table
@@ -324,6 +345,7 @@ ${cli} funnels draft --file <map.json> [--note <why>] [--evidence '<json>' | @fi
324
345
  ${cli} facts [--kind <kind>] [--subject <type>[:<ref>]] [--status <status>] [--json] | facts show <id> settled facts about the data, to treat as true
325
346
  ${cli} facts propose --file <fact.json> suggest a fact for a person to confirm
326
347
  ${cli} lessons [--status active|retired] | lessons add --file <lesson.json> | lessons retire <id> --note "<why>" lessons from winners and flops, writing one, retiring one
348
+ ${cli} tests [--status <status>] [--funnel <key>] [--limit <n>] | tests show <id> | tests draft --file <test.json> | tests start <id> | tests abandon <id> [--note "<why>"] funnel tests, one change each
327
349
  ${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
328
350
  ${cli} ads autotag [--limit <n>] [--ad <id>] have the server propose tags for the biggest spenders not yet tagged
329
351
  ${cli} content tags [<item-id>] [--kind email|text|page] | content to-check the marketing tags of emails, texts and landing pages, and those waiting for a person
package/dist/main.js CHANGED
@@ -658,6 +658,17 @@ const commands = {
658
658
  process.stdout.write(brief.text);
659
659
  },
660
660
  },
661
+ "pages results": {
662
+ usage: "pages results [--from <YYYY-MM-DD>] [--to <YYYY-MM-DD>] [--page <address | page key | item id>] (which landing pages, and which ad and page pairs, bring real first-touch sales)",
663
+ async run(args, options) {
664
+ if (args.length > 0)
665
+ throw new CliError("Usage: pages results [--from <date>] [--to <date>] [--page <address | page key | item id>]", exitCodes.usage);
666
+ const results = await api(await resolveConnection(options.apiUrl), `/api/v1/pages/results${reportQuery(options, { page: options.page })}`);
667
+ if (options.format === "json")
668
+ return printJson(results);
669
+ printPageResults(results);
670
+ },
671
+ },
661
672
  winners: {
662
673
  usage: "winners [--week <YYYY-MM-DD>] [--top <n>] (which Facebook ads won and flopped, judged on real first-touch sales)",
663
674
  async run(args, options) {
@@ -1420,6 +1431,70 @@ const commands = {
1420
1431
  : `Lesson ${lesson.id} already counted this week (${weeks}); its proof was updated.`);
1421
1432
  },
1422
1433
  },
1434
+ tests: {
1435
+ usage: "tests [list] [--status <status>] [--funnel <funnel_key>] [--limit <n>] the page and funnel tests written down, newest first (50 unless --limit, at most 200)",
1436
+ async run(args, options) {
1437
+ if (!(args.length === 0 || (args.length === 1 && args[0] === "list")))
1438
+ throw new CliError("Usage: tests [list] [--status <status>] [--funnel <funnel_key>] [--limit <n>]", exitCodes.usage);
1439
+ const query = new URLSearchParams();
1440
+ if (options.status)
1441
+ query.set("status", options.status);
1442
+ if (options.funnel)
1443
+ query.set("funnel", options.funnel);
1444
+ if (options.limit)
1445
+ query.set("limit", options.limit);
1446
+ const { tests } = await api(await resolveConnection(options.apiUrl), `${TESTS_PATH}${query.size ? `?${query}` : ""}`);
1447
+ if (options.format === "json")
1448
+ return printJson({ tests });
1449
+ if (tests.length === 0)
1450
+ return console.log("No funnel tests.");
1451
+ console.log(renderTable(TEST_COLUMNS, tests.map(testRow)));
1452
+ },
1453
+ },
1454
+ "tests show": {
1455
+ usage: "tests show <id|test_id> one funnel test in full",
1456
+ async run(args, options) {
1457
+ if (args.length !== 1)
1458
+ throw new CliError("Usage: tests show <id|test_id>", exitCodes.usage);
1459
+ const { test } = await api(await resolveConnection(options.apiUrl), testPath(args[0]));
1460
+ if (options.format === "json")
1461
+ return printJson({ test });
1462
+ printTest(test);
1463
+ },
1464
+ },
1465
+ "tests draft": {
1466
+ usage: "tests draft --file <test.json> | < test.json write down a funnel test: one change against a control, a metric and the evidence",
1467
+ async run(args, options) {
1468
+ if (args.length > 0)
1469
+ throw new CliError("Usage: tests draft --file <test.json> | < test.json", exitCodes.usage);
1470
+ const { test } = await api(await resolveConnection(options.apiUrl), TESTS_PATH, await readToolInput(options));
1471
+ if (options.format === "json")
1472
+ return printJson({ test });
1473
+ console.log(`Drafted test ${test.test_id} (${test.id}).`);
1474
+ },
1475
+ },
1476
+ "tests start": {
1477
+ usage: "tests start <id|test_id> mark a draft test as running from today; nothing goes live",
1478
+ async run(args, options) {
1479
+ if (args.length !== 1)
1480
+ throw new CliError("Usage: tests start <id|test_id>", exitCodes.usage);
1481
+ const { test } = await api(await resolveConnection(options.apiUrl), `${testPath(args[0])}/start`, {});
1482
+ if (options.format === "json")
1483
+ return printJson({ test });
1484
+ console.log(`Test ${test.test_id} is running from ${test.started_on}.`);
1485
+ },
1486
+ },
1487
+ "tests abandon": {
1488
+ usage: 'tests abandon <id|test_id> [--note "<why>"] drop a test that has not ended',
1489
+ async run(args, options) {
1490
+ if (args.length !== 1)
1491
+ throw new CliError('Usage: tests abandon <id|test_id> [--note "<why>"]', exitCodes.usage);
1492
+ const { test } = await api(await resolveConnection(options.apiUrl), `${testPath(args[0])}/abandon`, options.note ? { note: options.note } : {});
1493
+ if (options.format === "json")
1494
+ return printJson({ test });
1495
+ console.log(`Abandoned test ${test.test_id}.`);
1496
+ },
1497
+ },
1423
1498
  "lessons retire": {
1424
1499
  usage: 'lessons retire <id> --note "<why it is no longer true>" retire a lesson the numbers no longer back; it is kept with why',
1425
1500
  async run(args, options) {
@@ -1999,6 +2074,30 @@ function reportQuery(options, extra) {
1999
2074
  query.set(name, value);
2000
2075
  return query.size ? `?${query}` : "";
2001
2076
  }
2077
+ const PAGE_NUMBERS = ["spend", "page views", "leads", "sales", "by ad link", "revenue", "cost/sale", "sales/view"];
2078
+ function pageNumbers(r) {
2079
+ return {
2080
+ page: r.pageKey === null ? "(no page named)" : r.pageKey.replace(/^web:page:/, ""),
2081
+ currency: r.currency,
2082
+ spend: r.spend,
2083
+ "page views": r.landingPageViews,
2084
+ leads: r.leads,
2085
+ sales: r.sales,
2086
+ "by ad link": r.salesByAdLink,
2087
+ revenue: r.revenue,
2088
+ "cost/sale": r.costPerSale,
2089
+ "sales/view": r.salesPerLandingPageView,
2090
+ };
2091
+ }
2092
+ function printPageResults(results) {
2093
+ if (results.pages.length === 0)
2094
+ return console.log(`No page results from ${results.from} to ${results.to}.`);
2095
+ console.log(`Landing pages, most first-touch sales first, ${results.from} to ${results.to}:`);
2096
+ console.log(renderTable(["page", "headline", "currency", ...PAGE_NUMBERS], results.pages.map((r) => ({ ...pageNumbers(r), headline: r.headline === null ? "" : excerpt(r.headline, 40) }))));
2097
+ console.log("\nAd and page pairs:");
2098
+ console.log(renderTable(["page", "ad", "ad name", "currency", ...PAGE_NUMBERS], results.adPages.map((r) => ({ ...pageNumbers(r), ad: r.adId ?? "(no Meta ad)", "ad name": r.adName === null ? "" : excerpt(r.adName, 30) }))));
2099
+ console.log('\n"by ad link" counts sales whose buyer\'s landing page was not recorded, credited to the page the ad links to now.');
2100
+ }
2002
2101
  /** The rows, then the totals, which are one row per currency. */
2003
2102
  /** What confirmed leave-out rules took out of `figures`, one line per rule and currency. */
2004
2103
  function printLeftOut(leftOut, figures) {
@@ -2030,6 +2129,32 @@ const GRAMMAR_PATH = "/api/v1/grammar";
2030
2129
  const VOICE_LISTS_PATH = "/api/v1/voice-lists";
2031
2130
  const VOICE_SUGGESTIONS_PATH = "/api/v1/voice-lists/suggestions";
2032
2131
  const LESSONS_PATH = "/api/v1/lessons";
2132
+ const TESTS_PATH = "/api/v1/funnel-tests";
2133
+ const TEST_COLUMNS = ["test_id", "status", "funnel", "step", "change", "metric"];
2134
+ const testPath = (ref) => `${TESTS_PATH}/${encodeURIComponent(ref)}`;
2135
+ function testRow(t) {
2136
+ 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 };
2137
+ }
2138
+ function printTest(t) {
2139
+ 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(" · ");
2140
+ const lines = [
2141
+ `${t.test_id} (${t.status}) idea ${t.hypothesis_id} id ${t.id}`,
2142
+ `Idea: ${t.hypothesis}`,
2143
+ `Where: ${t.funnel_key} / ${t.step_key}`,
2144
+ `The one change: ${t.variable}`,
2145
+ `Stays the same: ${arm(t.control)}`,
2146
+ ...t.variants.map((v, i) => `${t.variants.length === 1 ? "Changed" : `Changed ${i + 1}`}: ${arm(v)}`),
2147
+ `Judged by: ${t.metric}, ${t.expected_change}, ${t.min_people_per_arm} people each side`,
2148
+ `Split: ${t.split_method}${t.split_note ? `, ${t.split_note}` : ""}`,
2149
+ "Evidence:",
2150
+ ...t.evidence.map((e) => ` ${e.kind}: ${e.ref}${e.note ? ` (${e.note})` : ""}`),
2151
+ ];
2152
+ if (t.started_on)
2153
+ lines.push(`Started: ${t.started_on}${t.ended_on ? `, ended ${t.ended_on}` : ""}`);
2154
+ if (t.decision_note)
2155
+ lines.push(`Note: ${t.decision_note}`);
2156
+ console.log(lines.join("\n"));
2157
+ }
2033
2158
  const LESSON_COLUMNS = ["id", "seen", "last week", "dimension", "lesson"];
2034
2159
  function lessonRow(l) {
2035
2160
  const row = { id: l.id, seen: l.timesSeen, "last week": l.lastSeenWeek, dimension: l.dimension ?? "", lesson: excerpt(l.text, 100) };
@@ -2168,8 +2293,8 @@ Options:
2168
2293
  --force content unpublish: take the page down even while delivering Meta ads link to it
2169
2294
  --plan <json> meta adset publish: the ad set to make, {"kind": "copy", ...} or {"kind": "create", ...}
2170
2295
  --yes confirm intake delete or import remove, which delete saved data for good
2171
- --from <YYYY-MM-DD> first day funnel, campaigns, money and brief cover (default: 29 days before --to)
2172
- --to <YYYY-MM-DD> last day funnel, campaigns, money and brief cover (default: today in the workspace reporting time zone)
2296
+ --from <YYYY-MM-DD> first day funnel, campaigns, money, brief and pages results cover (default: 29 days before --to)
2297
+ --to <YYYY-MM-DD> last day funnel, campaigns, money, brief and pages results cover (default: today in the workspace reporting time zone)
2173
2298
  --grain <grain> how funnel and money group days: day (default), week (starting Monday) or month
2174
2299
  --channel <channel> only this channel's campaigns (and, with brief, its ads): meta, google, affiliate, a utm_source, unknown or recorded_in_crm;
2175
2300
  for content track-link, where the link goes: facebook, email, sms or web (default)
@@ -2188,7 +2313,8 @@ Options:
2188
2313
  --object-type <type> what that platform's sync calls it (campaign_message or flow_message on klaviyo); needed where
2189
2314
  the sync matches several kinds, so its next read joins the item
2190
2315
  --notes <text> what changed in the version skill publish adds
2191
- --note <why> why: what content create and edit changed, or why lessons retire says a lesson is no longer true
2316
+ --note <why> why: what content create and edit changed, why lessons retire says a lesson is no longer true,
2317
+ or why tests abandon drops one
2192
2318
  --version <n> the map version funnels show prints (default: the confirmed one)
2193
2319
  --based-on <n> the map version funnels draft was written from; refused when someone saved since
2194
2320
  (leave it out only for the first map, when there is neither a map nor a draft)
@@ -2201,7 +2327,8 @@ Options:
2201
2327
  --step is in, when two funnels have a step with that key
2202
2328
  --step <key> with plans create, the step of --funnel this push itself is, as on the confirmed map;
2203
2329
  with pages brief, the step whose page the brief is for
2204
- --page <ref> with pages brief, the page to make a new version of: its address, page key, item id or SKU
2330
+ --page <ref> with pages brief, the page to make a new version of: its address, page key, item id or SKU;
2331
+ with pages results, the one page to show: its address, page key or item id
2205
2332
  --harness <list> which agents skill publish marks the skill for: claude, codex, or claude,codex (default)
2206
2333
  --text <words> words content search finds in a subject, headline, body or video transcript
2207
2334
  --mode word|meaning how content search matches --text: word (default, the words themselves) or meaning (close in meaning, nearest first; no --sort)
package/dist/playbooks.js CHANGED
@@ -830,7 +830,7 @@ You tag one client workspace's old Facebook ads in ${brand.name} with the market
830
830
 
831
831
  You do not work out the tags yourself. The server's tagger does: \`${cli} ads autotag\` reads each ad's copy, headlines, video transcript and picture description and answers the grammar's questions, following the hard cases in docs/marketing-grammar.md (Persona is who, Angle is the way in, a hook judged at its spot, a video that says one thing while the text says another). Your job is to run it in batches, stop at the right time, and report how much of the spend is now tagged.
832
832
 
833
- Every tag the tagger saves is proposed: usable at once, marked not yet confirmed. You never confirm, fix or reject tags; an API key cannot. A person checks the ads that carry the top 20% of spend on the Ad tags page in the ${brand.name} web app.
833
+ Every tag the tagger saves is proposed: usable at once, marked not yet confirmed. You never confirm, fix or reject tags; an API key cannot. A person checks the ads that carry the top 20% of spend on the Tag check page in the ${brand.name} web app.
834
834
 
835
835
  You work only through the \`${cli}\` CLI. Every command acts on the one workspace the saved API key belongs to.
836
836
 
@@ -877,7 +877,7 @@ Run the coverage query from step 1 again, then finish with a short report in pla
877
877
  - How many ads you tagged this run and what it cost.
878
878
  - How many ads that spent anything now have tags, and what share of the spend that is (\`pct_spend_tagged\`); what share of spend a person has confirmed (\`pct_spend_confirmed\`). Give each currency's shares on their own; never add them together.
879
879
  - How many ads are still untagged and why, if the batches said: \`nothingToRead\` ads have nothing to read (no copy, headline, transcript or picture description), plus any failures.
880
- - What to do next: open the Ad tags page in the ${brand.name} web app, press Make the list (or Make a new list) under Check these first, and confirm, fix or skip the ads that carry the top 20% of spend. Everything else stays as the tagger's best guess, marked not yet confirmed.
880
+ - What to do next: open the Tag check page in the ${brand.name} web app, press Make the list (or Make a new list) under Check these first, and confirm, fix or skip the ads that carry the top 20% of spend. Everything else stays as the tagger's best guess, marked not yet confirmed.
881
881
 
882
882
  ## Commands
883
883
 
@@ -889,7 +889,7 @@ ${cli} warehouse query --file coverage.sql how much of the spend is tagg
889
889
  `;
890
890
  export const backTagSkill = `---
891
891
  name: ${BACK_TAG_SKILL_NAME}
892
- description: Tag a ${brand.name} client workspace's old Facebook ads with the marketing grammar, biggest spenders first. Runs the server's tagger (\`${cli} ads autotag\`) in batches until no untagged ads with spend are left or the budget is reached, never confirms (a person checks the top 20% of spend on the Ad tags page), and reports how much of the spend is tagged and confirmed. Use when asked to back-tag a client's ads, tag old ads, or run the ${BACK_TAG_SKILL_NAME} playbook.
892
+ description: Tag a ${brand.name} client workspace's old Facebook ads with the marketing grammar, biggest spenders first. Runs the server's tagger (\`${cli} ads autotag\`) in batches until no untagged ads with spend are left or the budget is reached, never confirms (a person checks the top 20% of spend on the Tag check page), and reports how much of the spend is tagged and confirmed. Use when asked to back-tag a client's ads, tag old ads, or run the ${BACK_TAG_SKILL_NAME} playbook.
893
893
  ---
894
894
 
895
895
  ${backTagPlaybook}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.31",
3
+ "version": "0.3.33",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {