From 2a35c0f26784d8149c9aeb43710b20ba47f983ef Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 18 Aug 2026 11:20:11 +0300 Subject: [PATCH] Release Apple Ads plugin 0.2.1 --- .claude-plugin/plugin.json | 2 +- .github/workflows/lint.yml | 2 + CONTRIBUTING.md | 9 +- commands/asa-budget.md | 9 -- commands/asa-launch.md | 5 +- commands/asa-review.md | 18 ++- docs/IMPROVEMENT-IDEAS.md | 74 --------- examples/prompts.md | 7 +- scripts/lint-playbooks.mjs | 11 +- scripts/lint-workflows.mjs | 42 ++++++ scripts/sync-agent-docs.mjs | 39 +++-- scripts/test-cowork-preflight.mjs | 140 ++++++++++++++++++ skills/adapty-cli-setup/SKILL.md | 30 +++- skills/apple-ads-audit/SKILL.md | 5 +- .../references/playbooks/account-health.md | 8 +- skills/apple-ads-strategy/references/INDEX.md | 3 - .../references/verticals/_TEMPLATE.md | 16 +- .../references/verticals/utility-cleaner.md | 26 ---- .../references/verticals/utility-scanner.md | 26 ---- .../references/verticals/utility-tv-remote.md | 28 ++-- .../references/verticals/utility-vpn.md | 26 ---- skills/apple-ads/SKILL.md | 103 +++++++++++-- skills/apple-ads/references/INDEX.md | 5 +- skills/apple-ads/references/asa-management.md | 22 +-- skills/apple-ads/references/asa-metrics.md | 24 +-- .../references/playbooks/automation-rules.md | 32 ---- .../references/playbooks/bid-optimization.md | 29 ++-- .../playbooks/budget-reallocation.md | 32 ---- .../references/playbooks/campaign-launch.md | 32 ---- .../references/playbooks/cohort-roas.md | 19 ++- .../references/playbooks/keyword-load.md | 32 ---- .../playbooks/keyword-opportunity.md | 3 +- .../references/playbooks/weekly-review.md | 25 +++- 33 files changed, 463 insertions(+), 421 deletions(-) delete mode 100644 commands/asa-budget.md create mode 100644 scripts/test-cowork-preflight.mjs delete mode 100644 skills/apple-ads-strategy/references/verticals/utility-cleaner.md delete mode 100644 skills/apple-ads-strategy/references/verticals/utility-scanner.md delete mode 100644 skills/apple-ads-strategy/references/verticals/utility-vpn.md delete mode 100644 skills/apple-ads/references/playbooks/automation-rules.md delete mode 100644 skills/apple-ads/references/playbooks/budget-reallocation.md delete mode 100644 skills/apple-ads/references/playbooks/campaign-launch.md delete mode 100644 skills/apple-ads/references/playbooks/keyword-load.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 860887e..517b78a 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "apple-ads", - "version": "0.2.0", + "version": "0.2.1", "description": "Run and audit Apple Ads from your agent — cohort bid reviews, Market Intelligence keyword opportunities, search-term harvesting, negatives, CPP routing, and read-only account diagnostics through the Adapty CLI.", "author": { "name": "Adapty", "email": "support@adapty.io" }, "homepage": "https://adapty.io/docs/developer-cli-ads-manager-skill", diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d4d029c..1cb6e29 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,6 +11,8 @@ jobs: run: node scripts/lint-playbooks.mjs - name: Flagship workflow behavior contracts run: node scripts/lint-workflows.mjs + - name: Quiet Cowork preflight fixtures + run: node scripts/test-cowork-preflight.mjs - name: Translations are not stale run: node scripts/lint-i18n.mjs - name: Plugin manifests are valid JSON diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c7f7ead..ba059cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,10 +5,11 @@ The most useful contribution is a **vertical guide** for a category that has non ## Adding a vertical guide 1. Copy `skills/apple-ads-strategy/references/verticals/_TEMPLATE.md`. -2. Fill **every** section. A section you cannot fill gets `> TODO(owner): ...`, never a deletion — - missing sections are what make a guide library feel unreliable. -3. Every number is sourced, given as a range to calibrate, or marked TODO. A fabricated CPI - benchmark is worse than none, because the agent will act on it. +2. Fill **every** section. Do not add the guide until no template slots, checklists, stubs, or + `TODO(owner)` markers remain. +3. Every number is sourced or given as a range to calibrate. When evidence is unavailable, state + which first-party input must be measured and keep its value explicitly unknown. A fabricated + CPI benchmark is worse than none, because the agent will act on it. 4. Add a row to `skills/apple-ads-strategy/references/INDEX.md`. A guide with no INDEX row is never opened. 5. `node scripts/lint-playbooks.mjs` and `node scripts/lint-workflows.mjs` must pass. diff --git a/commands/asa-budget.md b/commands/asa-budget.md deleted file mode 100644 index 8090960..0000000 --- a/commands/asa-budget.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -description: Reallocate budget across campaigns ---- - -Help me reallocate my Apple Search Ads budget. - -Use the `apple-ads` skill, follow `references/playbooks/budget-reallocation.md`. Base the -decision on cohort ROAS at the window that matches my subscription period, not on spend or installs. -Confirm the budget change separately from any bid change — they are separate decisions. diff --git a/commands/asa-launch.md b/commands/asa-launch.md index 6c8d231..f8399b6 100644 --- a/commands/asa-launch.md +++ b/commands/asa-launch.md @@ -6,5 +6,6 @@ Help me launch a new Apple Search Ads campaign. If I have not connected an account yet, or I am still deciding the structure, use the `apple-ads-strategy` skill and give me a plan first — it needs no account. Once the plan is agreed, -switch to `apple-ads` and follow `references/playbooks/campaign-launch.md`. Create everything -`PAUSED`, show me the structure, and enable only after I confirm. +switch to `apple-ads`, use workflow 3 in `SKILL.md`, and read `references/asa-management.md` for +the current command syntax. Create everything `PAUSED`, show me the structure, and enable only +after I confirm. diff --git a/commands/asa-review.md b/commands/asa-review.md index 385780a..cad8cf0 100644 --- a/commands/asa-review.md +++ b/commands/asa-review.md @@ -6,7 +6,17 @@ Run the weekly check-in for my Apple Ads account. Use the `apple-ads` skill. Start with `references/playbooks/preflight.md`, then follow `references/playbooks/weekly-review.md` exactly — including its call budget. Ask me for the date -range if I did not give one, whether I want a comparison, and the metric and cohort window before -ranking anything. Include the same-window Apple total installs versus Adapty installs comparison, -up to two positive changes, up to two concerns, and one primary action. Do not investigate -attribution and do not write. +range if I did not give one and whether I want a comparison. If I did not name a success metric, +offer these choices in this exact order: + +1. Cost per paid +2. Cost per trial +3. Net ROAS at day X + +Ask for my numeric target or guardrail after the metric is selected. Always use net values for +revenue-family metrics; never offer gross or proceeds. Ask for a cohort day only when the selected +metric is `revenue`, `roas`, `arpu`, `arppu`, `arpas`, or `roi`. Never ask for a cohort window, pass +`--by-days` / `--order-by-day`, or label a result `day-X` for `cost_per_paid`, `cost_per_trial`, or +any other non-cohort metric. Include the same-window Apple total installs versus Adapty installs +comparison, up to two positive changes, up to two concerns, and one primary action. Do not +investigate attribution and do not write. diff --git a/docs/IMPROVEMENT-IDEAS.md b/docs/IMPROVEMENT-IDEAS.md index d284dc2..ccc5277 100644 --- a/docs/IMPROVEMENT-IDEAS.md +++ b/docs/IMPROVEMENT-IDEAS.md @@ -60,69 +60,6 @@ Acceptance criteria: - Dependent writes stop when a prerequisite fails. - Tests fail when a safety boundary is removed from a workflow. -## P0 — complete the existing operator chain - -Four current operator playbooks remain stubs. Complete them in this order. - -### 1. Keyword load - -This closes the path from Market Intelligence or strategy output to a verified live keyword list: - -```text -keyword opportunities -→ approved list -→ safe keyword load -→ read-back verification -→ separate bid review -``` - -Required behavior: - -- resolve destination ownership before loading; -- compare active, paused, and negative inventory; -- require explicit match type and bid; -- keep additions at 15 keywords or fewer per call; -- use a fresh idempotency key per batch; -- report applied, skipped, duplicate, rejected, and unverifiable rows; -- never combine loading and later bid optimization under one confirmation. - -### 2. Budget reallocation - -This closes the main execution path from a weekly check-in. - -Required behavior: - -- use a user-approved value metric and cohort window; -- protect campaign minimums and account budget totals; -- separate budget and bid decisions; -- show before and after budgets for every campaign; -- require confirmation for the complete allocation plan; -- read every changed campaign back. - -### 3. Campaign launch - -Required behavior: - -- consume a completed strategy plan; -- resolve organization and app ids from reads; -- create the full hierarchy in dependency order; -- create campaigns paused; -- use pinned, distinct idempotency keys for every write; -- verify each parent before creating a child; -- stop the chain after an ambiguous or failed prerequisite; -- enable only through a second explicit confirmation. - -### 4. Automation rules - -Required behavior: - -- validate the rule schema before creation; -- dry-run every rule that can change bids or budgets; -- show affected entities and proposed changes; -- start a rule only after the user reviews the dry-run; -- preserve run history and explain partial failures; -- never use an automation to bypass workflow confirmation rules. - ## P1 — standardize mutation plans ### Introduce one Action Plan contract @@ -355,17 +292,6 @@ Remove it only in a breaking release after: - documentation and examples no longer link to the old path; - routing evaluations pass without the redirect. -## P3 — expand strategy coverage - -After the operator chain is complete, finish the existing strategy vertical stubs: - -- utility cleaner; -- utility scanner; -- VPN. - -Execution playbooks come first. A detailed strategy is less useful when campaign launch and keyword -load remain incomplete. - ## Deliberate non-goals Do not add these until a concrete product need changes the decision: diff --git a/examples/prompts.md b/examples/prompts.md index be791f8..f69fa43 100644 --- a/examples/prompts.md +++ b/examples/prompts.md @@ -29,10 +29,10 @@ Which Discovery ad groups are missing cross-negatives for verified Exact owners? ``` Which of my ad groups lost money last month at day-30 cohort ROAS? How did last week compare to the week before? -Show me the 20 worst keywords by day-30 gross ROAS. +Show me the 20 worst keywords by day-30 net ROAS. Which US terms appear for these three competitor App Store ids but not in my active keywords? Which competitor terms are blocked by my current negatives? -Which keyword bids should increase under my day-30 proceeds ROAS target? +Which keyword bids should increase under my day-30 net ROAS target? How many active campaigns do I have? Why isn't this ad running? ``` @@ -53,7 +53,8 @@ Set up a rule that pauses any keyword spending over $50 with no conversion, and For "which keywords lost money last month", a correct answer: 1. runs preflight, -2. asks which cohort window matches the subscription period before ranking anything, +2. offers cost per paid, cost per trial, then net ROAS in that order, and asks for a cohort window + only if the selected metric is cohort-aware, 3. answers in **one** metrics call, sorted server-side, 4. splits the result into pays-back / does-not / not-enough-data, 5. recommends an action and its playbook — and does not execute it. diff --git a/scripts/lint-playbooks.mjs b/scripts/lint-playbooks.mjs index 65337ee..b15649d 100644 --- a/scripts/lint-playbooks.mjs +++ b/scripts/lint-playbooks.mjs @@ -16,9 +16,7 @@ import { join, relative, basename, extname } from 'node:path' const ROOT = new URL('..', import.meta.url).pathname const errors = [] -const warnings = [] const fail = (file, msg) => errors.push(`${relative(ROOT, file)}: ${msg}`) -const warn = (file, msg) => warnings.push(`${relative(ROOT, file)}: ${msg}`) // Fields the Agent Skills spec accepts outside Claude Code. Anything else fails validation // when a skill is packaged for claude.ai or the Skills API. @@ -114,8 +112,10 @@ for (const { dir, kind: expectedKind, index } of targets) { fail(path, `not listed in ${relative(ROOT, index)} — an unreachable playbook is a dead playbook`) } - if (/^>\s*\*\*Stub\.\*\*/m.test(text)) warn(path, 'stub — contract only, body not written') - if (text.includes('TODO(owner)')) warn(path, 'has unfilled TODO(owner) slots') + if (/^>\s*\*\*Stub\.\*\*/m.test(text)) fail(path, 'stub content is not shippable; remove the route or finish the file') + if (f !== '_TEMPLATE.md' && text.includes('TODO(owner)')) { + fail(path, 'has unfilled TODO(owner) slots') + } } // Every INDEX row must resolve to a file that exists. @@ -175,7 +175,6 @@ for (const file of publicTextFiles) { } // ---- report --------------------------------------------------------------------------- -for (const w of warnings) console.log(`warn ${w}`) for (const e of errors) console.error(`ERROR ${e}`) -console.log(`\n${errors.length} error(s), ${warnings.length} warning(s)`) +console.log(`\n${errors.length} error(s)`) process.exit(errors.length ? 1 : 0) diff --git a/scripts/lint-workflows.mjs b/scripts/lint-workflows.mjs index e935899..addeb59 100644 --- a/scripts/lint-workflows.mjs +++ b/scripts/lint-workflows.mjs @@ -21,6 +21,9 @@ const read = (path) => { const requireText = (path, text, label = text) => { if (!read(path).includes(text)) fail(path, `missing workflow contract: ${label}`) } +const forbidText = (path, text, label = text) => { + if (read(path).includes(text)) fail(path, `forbidden workflow behavior: ${label}`) +} const requireOrder = (path, first, second) => { const text = read(path) const firstAt = text.indexOf(first) @@ -39,15 +42,34 @@ const auditSkill = join(auditDir, 'SKILL.md') const accountHealth = join(auditDir, 'references/playbooks/account-health.md') const structureAudit = join(auditDir, 'references/playbooks/structure-audit.md') const operatorPlaybooks = join(ROOT, 'skills/apple-ads/references/playbooks') +const operatorSkill = join(ROOT, 'skills/apple-ads/SKILL.md') +const setupSkill = join(ROOT, 'skills/adapty-cli-setup/SKILL.md') const weekly = join(operatorPlaybooks, 'weekly-review.md') const opportunities = join(operatorPlaybooks, 'keyword-opportunity.md') const legacyCompetitors = join(operatorPlaybooks, 'competitor-check.md') const bids = join(operatorPlaybooks, 'bid-optimization.md') +const cohortRoas = join(operatorPlaybooks, 'cohort-roas.md') +const reviewCommand = join(ROOT, 'commands/asa-review.md') +const metricsReference = join(ROOT, 'skills/apple-ads/references/asa-metrics.md') const harvest = join(operatorPlaybooks, 'search-term-harvesting.md') const negatives = join(operatorPlaybooks, 'negative-keyword-hygiene.md') const cpp = join(operatorPlaybooks, 'creative-setup.md') const operatorIndex = join(ROOT, 'skills/apple-ads/references/INDEX.md') +for (const path of [operatorSkill, setupSkill]) { + requireText(path, 'NetworkError', 'NetworkError is distinct from authentication') + requireText(path, 'failed to copy trust settings of system certificate-25291', 'Cowork certificate-noise signature') + requireText(path, 'NODE_USE_SYSTEM_CA=0', 'system CA fallback') + requireText(path, 'Do not install or log in', 'network failures never start setup') +} +requireText(operatorSkill, 'RETRY_OUTSIDE_SANDBOX', 'single host retry handoff') +requireText(operatorSkill, 'NETWORK_BLOCKED', 'concise terminal network state') +requireOrder(operatorSkill, '# agent-preflight:start', '| What it does | What that means |') +const setupDescription = frontmatterValue(read(setupSkill), 'description') +if (setupDescription.includes('402') || setupDescription.includes('ads_manager_subscription_required')) { + fail(setupSkill, '402 must not trigger the setup skill') +} + requireText(auditSkill, 'This skill has no write', 'explicit read-only audit boundary') requireText(auditSkill, 'relative_gap = absolute_gap / apple_installs', 'install-gap formula') requireText(auditSkill, 'only when `apple_installs > 0`', 'zero-denominator guard') @@ -85,6 +107,26 @@ for (const category of ['increase', 'keep', 'decrease', 'pause_candidate', 'insu requireText(bids, 'Do not invent a target', 'no universal bid threshold') requireText(bids, 'Read the scoped keywords back', 'bid read-back verification') +for (const path of [weekly, bids]) { + requireOrder(path, 'cost_per_paid', 'cost_per_trial') + requireOrder(path, 'cost_per_trial', 'net `roas` at day X') + requireText(path, 'only', 'cohort-window applicability is conditional') +} +requireOrder(reviewCommand, '1. Cost per paid', '2. Cost per trial') +requireOrder(reviewCommand, '2. Cost per trial', '3. Net ROAS at day X') +requireText(reviewCommand, 'Never ask for a cohort window', 'non-cohort metrics skip the cohort question') +requireText(reviewCommand, 'never offer gross or proceeds', 'review offers net only') +forbidText(reviewCommand, 'Proceeds ROAS', 'proceeds ROAS choice') +forbidText(reviewCommand, 'Gross ROAS', 'gross ROAS choice') +requireText(weekly, 'every other non-cohort metric', 'non-cohort date-window semantics') +requireText(cohortRoas, 'agent workflows always use and offer `net_`', 'net cohort workflow variant') +for (const path of [weekly, bids, cohortRoas]) { + forbidText(path, 'day-30 proceeds ROAS', 'proceeds ROAS example') + forbidText(path, 'day-30 gross ROAS', 'gross ROAS example') +} +requireText(metricsReference, '`cost_per_paid` and `cost_per_trial` are values for the requested date window', 'cost metric date-window semantics') +requireText(metricsReference, '`--by-days` does not turn either into a day-X metric', 'non-cohort day-X prohibition') + for (const category of ['promote', 'block', 'keep_observing', 'already_owned', 'ownership_conflict', 'insufficient_evidence']) { requireText(harvest, `\`${category}\``, `harvest category ${category}`) } diff --git a/scripts/sync-agent-docs.mjs b/scripts/sync-agent-docs.mjs index b39d923..9c48fe6 100644 --- a/scripts/sync-agent-docs.mjs +++ b/scripts/sync-agent-docs.mjs @@ -5,12 +5,16 @@ // generated upstream, next to the commands, where CI can check them against the real // oclif manifest. This script copies the result here and stamps the version it came from. // -// Usage: node scripts/sync-agent-docs.mjs [--ref v0.4.0] -import { writeFileSync, mkdirSync } from 'node:fs' -import { dirname } from 'node:path' +// Usage: node scripts/sync-agent-docs.mjs [--ref=v0.4.0] +// node scripts/sync-agent-docs.mjs --source-dir=../adapty-cli [--only=] +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' const ROOT = new URL('..', import.meta.url).pathname const ref = (process.argv.find((a) => a.startsWith('--ref='))?.split('=')[1]) ?? 'main' +const sourceDirArg = process.argv.find((a) => a.startsWith('--source-dir=')) +const sourceDir = sourceDirArg ? resolve(sourceDirArg.slice('--source-dir='.length)) : null +const only = process.argv.find((a) => a.startsWith('--only='))?.slice('--only='.length) ?? null const REPO = 'adaptyteam/adapty-cli' // upstream path -> path in this repo @@ -26,15 +30,28 @@ const header = (src) => let failed = 0 for (const [src, dest] of Object.entries(FILES)) { - const url = `https://raw.githubusercontent.com/${REPO}/${ref}/${src}` - const res = await fetch(url) - if (!res.ok) { - console.error(`ERROR ${src}: ${res.status} ${res.statusText}`) - console.error(` upstream has not published this file yet — leaving the local copy alone.`) - failed++ - continue + if (only && src !== only) continue + + let body + if (sourceDir) { + try { + body = readFileSync(join(sourceDir, src), 'utf8') + } catch (error) { + console.error(`ERROR ${src}: ${error instanceof Error ? error.message : 'local read failed'}`) + failed++ + continue + } + } else { + const url = `https://raw.githubusercontent.com/${REPO}/${ref}/${src}` + const res = await fetch(url) + if (!res.ok) { + console.error(`ERROR ${src}: ${res.status} ${res.statusText}`) + console.error(` upstream has not published this file yet — leaving the local copy alone.`) + failed++ + continue + } + body = await res.text() } - let body = await res.text() // a synced SKILL.md keeps its frontmatter first; the header goes after it if (body.startsWith('---\n')) { const end = body.indexOf('\n---', 3) + 4 diff --git a/scripts/test-cowork-preflight.mjs b/scripts/test-cowork-preflight.mjs new file mode 100644 index 0000000..d45e1ef --- /dev/null +++ b/scripts/test-cowork-preflight.mjs @@ -0,0 +1,140 @@ +#!/usr/bin/env node + +import {mkdtempSync, readFileSync, rmSync, writeFileSync} from 'node:fs' +import {tmpdir} from 'node:os' +import {join} from 'node:path' +import {spawnSync} from 'node:child_process' + +const ROOT = new URL('..', import.meta.url).pathname +const skill = readFileSync(join(ROOT, 'skills/apple-ads/SKILL.md'), 'utf8') +const block = skill.match(/# agent-preflight:start\n([\s\S]*?)# agent-preflight:end/)?.[1] + +if (!block) { + console.error('ERROR quiet preflight block is missing from skills/apple-ads/SKILL.md') + process.exit(1) +} + +const networkMessage = + 'Adapty API is unreachable from this sandbox. Allow network access for `adapty.io` and `*.adapty.io`, then start a new task.' +const occurrences = skill.split(networkMessage).length - 1 +if (occurrences !== 1) { + console.error(`ERROR concise network message must appear exactly once, found ${occurrences}`) + process.exit(1) +} + +const temp = mkdtempSync(join(tmpdir(), 'adapty-preflight-')) +const fakeAdapty = join(temp, 'adapty') +const callLog = join(temp, 'calls.log') +const success = [ + 'Company: Example', + 'Access Source: payg', + 'Apple Credentials Status: active', +].join('\n') + +writeFileSync( + fakeAdapty, + `#!/bin/sh +printf '%s\\n' "$*" >> "$PREFLIGHT_CALL_LOG" +if [ "$*" != "asa whoami" ]; then + echo "unexpected adapty invocation: $*" + exit 99 +fi + +noise() { + i=0 + while [ "$i" -lt 60 ]; do + echo 'ERROR: failed to copy trust settings of system certificate-25291' + i=$((i + 1)) + done +} + +case "$PREFLIGHT_FIXTURE" in + noise_then_success) + if [ "\${NODE_USE_SYSTEM_CA:-}" = "0" ]; then + printf '%s\\n' '${success}' + exit 0 + fi + noise + echo 'NetworkError: fetch failed' + exit 1 + ;; + noise_then_fail) + if [ "\${NODE_USE_SYSTEM_CA:-}" != "0" ]; then noise; fi + echo 'NetworkError: fetch failed' + exit 1 + ;; + direct_success) + printf '%s\\n' '${success}' + exit 0 + ;; + auth_required) + echo 'AuthRequiredError: Not authenticated. Run \`adapty auth login\`.' + exit 1 + ;; + no_subscription) + echo 'Error: 402 ads_manager_subscription_required' + exit 1 + ;; + network_blocked) + echo 'NetworkError: fetch failed' + exit 1 + ;; + unexpected) + echo 'Error: unexpected failure' + exit 1 + ;; +esac +`, + {mode: 0o755}, +) + +const failures = [] +const check = (condition, message) => { + if (!condition) failures.push(message) +} +const expectedSuccess = `${success}\nREADY_AUTHED` + +function run(name, expected, expectedStatus = 0, withCli = true) { + writeFileSync(callLog, '') + const path = withCli ? `${temp}:/usr/bin:/bin` : '/usr/bin:/bin' + const result = spawnSync('/bin/sh', ['-c', block], { + encoding: 'utf8', + env: { + ...process.env, + PATH: path, + PREFLIGHT_CALL_LOG: callLog, + PREFLIGHT_FIXTURE: name, + }, + }) + const stdout = result.stdout.trim() + const stderr = result.stderr.trim() + const calls = readFileSync(callLog, 'utf8') + + check(result.status === expectedStatus, `${name}: exit ${result.status}, expected ${expectedStatus}`) + check(stdout === expected, `${name}: output ${JSON.stringify(stdout)}, expected ${JSON.stringify(expected)}`) + check(stderr === '', `${name}: unexpected stderr ${JSON.stringify(stderr)}`) + check(!stdout.includes('failed to copy trust settings'), `${name}: leaked certificate noise`) + check(!calls.includes('auth login'), `${name}: started authentication`) + check(!calls.includes('npm'), `${name}: started installation`) +} + +try { + run('noise_then_success', `${success}\nREADY_AUTHED_SYSTEM_CA_OFF`) + run('noise_then_fail', 'RETRY_OUTSIDE_SANDBOX', 75) + run('direct_success', expectedSuccess) + run('auth_required', 'NEED_SETUP') + run('no_subscription', 'Error: 402 ads_manager_subscription_required\nREADY_AUTHED_NO_SUB') + run('network_blocked', 'NETWORK_BLOCKED') + run('unexpected', 'Error: unexpected failure\nPREFLIGHT_ERROR') + run('missing_cli', 'NEED_SETUP', 0, false) +} finally { + rmSync(temp, {force: true, recursive: true}) +} + +if (failures.length > 0) { + for (const failure of failures) console.error(`ERROR ${failure}`) + console.error(`\n${failures.length} quiet preflight regression(s)`) + process.exit(1) +} + +console.log('Quiet Cowork preflight fixtures passed.') diff --git a/skills/adapty-cli-setup/SKILL.md b/skills/adapty-cli-setup/SKILL.md index d2d0792..3c83486 100644 --- a/skills/adapty-cli-setup/SKILL.md +++ b/skills/adapty-cli-setup/SKILL.md @@ -1,13 +1,11 @@ --- name: adapty-cli-setup license: MIT -description: Use when the Adapty CLI needs installing or authenticating before any Apple Search Ads work — a fresh Cowork or cloud session, "adapty: command not found", AuthRequiredError, an expired auth code, connecting an Apple Search Ads account, or a 402 ads_manager_subscription_required. Triggers on "install the Adapty CLI", "set up adapty", "adapty auth login", "connect Apple Search Ads", "not authenticated", or any Apple Ads request made in a session where the CLI is not yet installed. +description: Use when the Adapty CLI needs installing or authenticating before any Apple Search Ads work — a fresh Cowork or cloud session, "adapty: command not found", AuthRequiredError, an expired auth code, or connecting an Apple Search Ads account. Triggers on "install the Adapty CLI", "set up adapty", "adapty auth login", "connect Apple Search Ads", "not authenticated", or any Apple Ads request made in a session where the CLI is not yet installed. --- - + # Adapty CLI setup @@ -18,6 +16,24 @@ off to `apple-ads`. Nothing here spends money. from a previous session, and nothing in this one carries into the next. Never skip the run because setup "was already done". +## Entry boundary + +Open this skill only when `adapty` is missing, `adapty asa whoami` returns +`AuthRequiredError`, or Apple credentials need connecting. A failed preflight is not automatically +an authentication failure: + +- `402 ads_manager_subscription_required` means authentication succeeded. Do not install or log in. +- `NetworkError` means the CLI could not reach the API. Do not install or log in. +- Repeated `failed to copy trust settings of system certificate-25291` lines mean a macOS sandbox + cannot read the system Keychain. The caller must suppress those lines, retry the same read once + with `NODE_USE_SYSTEM_CA=0`, then retry that read once outside the sandbox when the platform + explicitly supports it. Never route either failure into this setup flow. + +If both network retries fail, stop with exactly this user-facing message and no certificate dump: + +> Adapty API is unreachable from this sandbox. Allow network access for `adapty.io` and +> `*.adapty.io`, then start a new task. + ## Run this Two scripts do the whole job. Run step 1, surface what it prints, run step 2. Do not improvise @@ -117,6 +133,7 @@ the dashboard usually means the token is scoped elsewhere. | `User code not found or expired` in the browser | The waiter died, or the TTL ran out | Step 1 again, and surface the new code faster | | `AuthRequiredError` after `AUTHED` | Almost always a stale `ADAPTY_TOKEN` | `unset ADAPTY_TOKEN`, re-check | | `402 ads_manager_subscription_required` | Authenticated fine; the company has no Ads Manager subscription | **Not a setup bug.** Say so plainly and stop. No flag works around it | +| `NetworkError`, including certificate `-25291` noise | Network or sandbox trust-store access failed before authentication could be checked | Return to the caller's quiet preflight retry. Never install or log in | ## ADAPTY_TOKEN @@ -150,6 +167,9 @@ printing the link. - **Never treat `adapty auth status` as proof of a working token.** It never touches the network; `adapty asa whoami` is the first call that asks the server. - **Never present a `402` as a broken install**, and never hunt for a flag that bypasses it. +- **Never present a `NetworkError` as an authentication or install failure.** In particular, do not + reset Keychain, reinstall Xcode, run `sudo`, reinstall the CLI, or start `auth login` for + certificate `-25291` noise. - **Never echo a full token into chat**, and never write one into a file that could be committed. - **Never run a write command from this skill.** Setup is reads plus `asa connect`. A campaign, bid, budget or keyword means `apple-ads` owns the next step. diff --git a/skills/apple-ads-audit/SKILL.md b/skills/apple-ads-audit/SKILL.md index 3f7579c..769f1fe 100644 --- a/skills/apple-ads-audit/SKILL.md +++ b/skills/apple-ads-audit/SKILL.md @@ -66,7 +66,8 @@ new task. The operator must re-resolve ids and obtain its own confirmation. 2. **Plan the reads.** Use list counts for entity counts and server aggregation for metrics. Keep the standard audit inside the analytics budget; never loop per entity or per day. 3. **Normalize evidence.** Give each returned row or metadata fact an evidence id. Keep the source, - window, entity id, metric, and cohort window attached to it. + window, entity id, and metric attached to it; include a cohort window only for `revenue`, `roas`, + `arpu`, `arppu`, `arpas`, or `roi`. 4. **Separate layers.** Write the observation first, then the explanation, then the recommendation. Never present an explanation as observed fact. 5. **Apply applicability.** Mark controls that cannot be evaluated as `unknown` or @@ -79,7 +80,7 @@ Every audit response contains: 1. `summary` — a short plain-language answer. 2. `scope` — company, app, entity scope, and identifiers used. -3. `date_window` — exact dates, grouping, and cohort window when relevant. +3. `date_window` — exact dates and grouping, plus a cohort window only for a cohort root. 4. `findings` — ordered by severity. 5. `evidence` — ids that map every claim to returned data. 6. `confidence` — overall confidence and the limiting missing data. diff --git a/skills/apple-ads-audit/references/playbooks/account-health.md b/skills/apple-ads-audit/references/playbooks/account-health.md index e2b780c..d2dd5a9 100644 --- a/skills/apple-ads-audit/references/playbooks/account-health.md +++ b/skills/apple-ads-audit/references/playbooks/account-health.md @@ -33,7 +33,8 @@ Resolve or ask for: - company and app; - exact date window; -- the business success metric and cohort window, if the user wants value judgments; +- the business success metric, and a cohort window only when that metric is `revenue`, `roas`, + `arpu`, `arppu`, `arpas`, or `roi`, if the user wants value judgments; - any user-defined spend, CPA, or ROAS limits; - optional campaign scope. @@ -49,7 +50,7 @@ bad, profitable, or unprofitable. 4. Read one account overview for spend, Apple installs, Adapty installs, the requested cost metric, and the requested cohort root. The time series supplies the trend; do not call once per period. 5. Read one ranked campaign result only when the user asks which campaigns need attention. Use the - user's metric, direction, and cohort window. + user's metric and direction, adding a cohort window only for a cohort root. 6. Drill into one suspicious level only when the broad evidence cannot answer the question. 7. Read `ads get` only for an ad whose serving state requires an explanation. 8. Read keyword, negative, product-page, or creative inventory only when the corresponding control @@ -75,7 +76,8 @@ drill-down question. Metadata reads do not justify unscoped lists. ### Performance -- State the metric, date window, cohort window, and revenue variant before interpreting it. +- State the metric and date window before interpreting it. State a cohort window only for a cohort + root, and always use net rather than asking the user to choose a revenue variant. - Keep immature cohorts in `unknown` or `insufficient evidence`. - Compare against the user's target or the account's own requested historical period. Do not invent a universal threshold. diff --git a/skills/apple-ads-strategy/references/INDEX.md b/skills/apple-ads-strategy/references/INDEX.md index 02c84da..c860abe 100644 --- a/skills/apple-ads-strategy/references/INDEX.md +++ b/skills/apple-ads-strategy/references/INDEX.md @@ -5,9 +5,6 @@ One row, one file. Read only the file the row names. | The user is asking | Open | |---|---| | How to launch ASA for a universal TV remote / device-control app | `verticals/utility-tv-remote.md` | -| How to launch ASA for a VPN app | `verticals/utility-vpn.md` | -| How to launch ASA for a scanner / document app | `verticals/utility-scanner.md` | -| How to launch ASA for a cleaner / storage app | `verticals/utility-cleaner.md` | | Which campaigns and ad groups to create, in any category | `account-structure.md` | | Which keywords to target, how to group them, what to negate | `keyword-taxonomy.md` | | A category with no guide here yet | `account-structure.md` + `keyword-taxonomy.md`, then offer to draft one from `verticals/_TEMPLATE.md` | diff --git a/skills/apple-ads-strategy/references/verticals/_TEMPLATE.md b/skills/apple-ads-strategy/references/verticals/_TEMPLATE.md index 13858f6..bd4ca06 100644 --- a/skills/apple-ads-strategy/references/verticals/_TEMPLATE.md +++ b/skills/apple-ads-strategy/references/verticals/_TEMPLATE.md @@ -14,12 +14,12 @@ updated: YYYY-MM-DD + # Apple Search Ads — Managing Campaigns @@ -19,12 +18,12 @@ Every `list` and `get` command in this file returns metadata only, no metrics. E ## Account and discovery -| Command | Notes | -|---|---| -| `asa whoami` | Company, how access was granted, Apple connection state. Run this first. No connected Apple Ads account or no active Ads Manager subscription answers `402 ads_manager_subscription_required` on every other `asa` command. | -| `asa connect [--no-wait]` | Prints the Apple authorization link and waits for the link to be completed; `--no-wait` returns immediately instead of waiting. | -| `asa apps list` | Apps promoted in Apple Search Ads; pagination only. Its rows supply `--adam-id` for `campaigns create`. | -| `asa orgs list` | Apple Search Ads organizations; pagination only. Each row carries two identifiers, not interchangeable: `internal_id` (a UUID) and `org_id` (Apple's numeric id). `--org` on `campaigns create` takes `internal_id` — passing the numeric `org_id` fails with "Invalid org ID format." | +| Command | Flags | Notes | +|---|---|---| +| `asa whoami` | none | Company, how access was granted, Apple connection state. Run this first. No connected Apple Ads account or no active Ads Manager subscription answers `402 ads_manager_subscription_required` on every other `asa` command. | +| `asa connect` | `--no-wait` optional | Prints the Apple authorization link and waits for the link to be completed; `--no-wait` returns immediately instead of waiting. | +| `asa apps list` | pagination only | Apps promoted in Apple Search Ads. Its rows supply `--adam-id` for `campaigns create`. | +| `asa orgs list` | pagination only | Apple Search Ads organizations. Each row carries two identifiers, not interchangeable: `internal_id` (a UUID) and `org_id` (Apple's numeric id). `--org` on `campaigns create` takes `internal_id` — passing the numeric `org_id` fails with "Invalid org ID format." | ## Campaigns @@ -34,6 +33,8 @@ Every `list` and `get` command in this file returns metadata only, no metrics. E | `asa campaigns get ` | positional UUID | Metadata only. | | `asa campaigns create` | `--org`, `--name`, `--adam-id`, `--country` (repeatable), `--daily-budget`; optional `--status` (`ENABLED`/`PAUSED`, no default), `--budget` (lifetime), `--target-cpa`, `--bidding-strategy`, `--supply-source` (repeatable, default `APPSTORE_SEARCH_RESULTS`), `--billing-event` (`IMPRESSIONS`/`TAPS`, default `TAPS`), `--ad-channel-type` (`DISPLAY`/`SEARCH`, default `SEARCH`) | `--org` takes the UUID (`internal_id`) from `asa orgs list`, not that row's numeric `org_id`; `--adam-id` comes from `asa apps list`. `--status` has no default — pass `--status PAUSED` to launch without spending until you enable it. | | `asa campaigns update ` | at least one of `--name`, `--status`, `--country`, `--daily-budget`, `--budget`, `--target-cpa`, `--bidding-strategy` | | +| `asa campaigns bulk-create` | exactly one of `--file` (JSON structure, `-` for stdin) / `--from-file` (Apple Ads template, `.xlsx` or keywords `.csv`); `--org-id` required with `--from-file`; optional `--preview`, `--no-wait`, `--poll-interval` (default `5`), `--timeout` (default `900`) | Creates a whole structure — campaigns → ad groups → keywords/negative keywords/ads — as one queued operation. `--org-id` is the exception to this file's UUID rule: it takes the **numeric** `org_id` from `asa orgs list` (Apple's `campaign_group_id`), not the `internal_id` UUID that `campaigns create --org` takes. `--from-file` converts the template server-side first (its own budget — see [Request budgets](#request-budgets)); with `--preview` the command prints the converted request and creates nothing. By default it polls until the operation finishes (`success`/`partial`/`failed`, per-object failures listed); `--no-wait` prints the `operation_id` and returns — follow up with `bulk-status`. | +| `asa campaigns bulk-status ` | positional operation id, printed by `bulk-create` | Progress of one bulk operation: status, applied/failed counts, and the per-object log with each failure's reason. | ## Ad groups @@ -48,7 +49,7 @@ Every `list` and `get` command in this file returns metadata only, no metrics. E | Command | Flags | Notes | |---|---|---| -| `asa ads list` | scope filters only, **no `--app`** | Ads hang off ad groups, not apps directly — filter by `--ad-group` or `--campaign` instead. Metadata only. | +| `asa ads list` | scope filters only | Ads hang off ad groups, not apps directly — there is no `--app`; filter by `--ad-group` or `--campaign` instead. Metadata only. | | `asa ads get ` | positional UUID | `serving_state_reasons` in the response explains a non-running ad. | | `asa ads create` | `--ad-group`, `--creative-id`, `--name` | The creative id comes from `asa creatives list`. | | `asa ads update ` | `--name` and/or `--status` | The creative and the parent ad group are fixed at creation and cannot be changed. | @@ -139,6 +140,7 @@ Every `asa` command is rate limited per company, not per token: | catalog lists and gets, automation reads | 120/min | | `keywords list` | 30/min, burst 5 per 10s, its own 2-concurrent pool, 60s server timeout | | all writes | 20/min | +| template conversion (`bulk-create --from-file`) | 10/min, one conversion at a time | | `whoami` | 60/min | `keywords list` is the heaviest metadata read in the surface — its own budget is on top of diff --git a/skills/apple-ads/references/asa-metrics.md b/skills/apple-ads/references/asa-metrics.md index 0b368ba..2f47fc5 100644 --- a/skills/apple-ads/references/asa-metrics.md +++ b/skills/apple-ads/references/asa-metrics.md @@ -1,6 +1,5 @@ - + # Apple Search Ads — Metrics and Analytics @@ -52,7 +51,8 @@ names costs at most one call and should never be done on purpose. Cohort roots — `revenue`, `arpu`, `arppu`, `arpas` (alias `cohort_arpas`), `roas`, `roi` — expand to `gross_`, `proceeds_`, and `net_` variants (`gross_roas`, `proceeds_revenue`, `net_arpu`, and so on). To rank by a cohort metric with `--order-by`, use the expanded name -— `--order-by gross_roas`, not `--order-by roas`. There is no `ltv` metric; see [Cohort +— `--order-by net_roas`, not `--order-by roas`. Agent workflows use the `net_` variant and do +not ask users to choose gross or proceeds. There is no `ltv` metric; see [Cohort windows](#cohort-windows). `metrics overview` accepts the root names only (`revenue`, `roas`, `arpu`, …) — no @@ -83,17 +83,21 @@ The single most misunderstood part of the surface: there is no `ltv` metric. Lif is a cohort metric read at a renewal window, so `--by-days` is how you ask for a day-7 or day-90 value. -- `--by-days` is repeatable, at most 16 windows per call, on both `metrics` and `metrics - overview`. Omit it entirely to get the dashboard's default figures instead of cohort - values. +- `--by-days` applies only to the cohort roots `revenue`, `roas`, `arpu`, `arppu`, `arpas`, and + `roi`. It is repeatable, at most 16 windows per call, on both `metrics` and `metrics overview`. + Omit it entirely to get the dashboard's default figures instead of cohort values. - `--order-by-day` ranks rows by one of those windows — the way to get top campaigns by day-90 ROAS in a single call. Its value must be one of the windows passed to `--by-days`. -- Ranking by a cohort metric takes the expanded name — `--order-by gross_roas`, not - `--order-by roas` — see [Metric vocabulary](#metric-vocabulary). +- Ranking by a cohort metric takes the expanded name. Agent workflows use `--order-by net_roas`, + not `--order-by roas` — see [Metric vocabulary](#metric-vocabulary). +- Apple spend, Adapty attribution, count, conversion, and cost metrics are not cohort-windowed. + In particular, `cost_per_paid` and `cost_per_trial` are values for the requested date window; + `--by-days` does not turn either into a day-X metric. Never ask for a cohort day, pass + `--by-days` / `--order-by-day`, or attach a `day-X` label for a non-cohort metric. ```sh adapty asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 \ - --metric roas --by-days 7 --by-days 90 --order-by gross_roas --order-by-day 90 + --metric roas --by-days 7 --by-days 90 --order-by net_roas --order-by-day 90 ``` ## The analytics pool diff --git a/skills/apple-ads/references/playbooks/automation-rules.md b/skills/apple-ads/references/playbooks/automation-rules.md deleted file mode 100644 index 354da48..0000000 --- a/skills/apple-ads/references/playbooks/automation-rules.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Rule automations -intent: user wants a rule to act without them -kind: procedural -risk: writes-money -requires: { cli: ">=0.4.0", subscription: true } -uses: [asa automations create, asa automations run, asa automations runs, asa automations update] -time: TODO ---- - -# Rule automations - -> **Stub.** The contract above is real and the linter checks it; the body is not written yet. -> Until it is, follow `SKILL.md` — its numbered workflows cover the command shapes — and treat -> the checklist below as the outline to fill in. - -## To write - -- [ ] When this playbook applies, and when it does not -- [ ] The minimum number of reads that answer the question -- [ ] How the decision is made, and which parts are the user's call rather than yours -- [ ] What to show before writing, and what an explicit confirmation looks like here -- [ ] The write commands, batching and `--idempotency-key` discipline -- [ ] How to verify it landed -- [ ] The failure modes specific to this job - -## Rules that already apply - -- Every write is previewed and confirmed. `--yes` only on a command you run yourself. -- 15 keywords per call; a fresh idempotency key per call. -- Metrics: 5 calls/min, at most 2 per 10s. Plan the whole answer inside that. -- No delete exists. `--status PAUSED` is the only stop. diff --git a/skills/apple-ads/references/playbooks/bid-optimization.md b/skills/apple-ads/references/playbooks/bid-optimization.md index 4ccde7b..05ddba2 100644 --- a/skills/apple-ads/references/playbooks/bid-optimization.md +++ b/skills/apple-ads/references/playbooks/bid-optimization.md @@ -23,18 +23,25 @@ no changes; forcing a thin row into a decision is a failure. - The user has not selected a performance goal and only wants a report → return outliers without a mutation proposal. -- The question is campaign budget allocation → `budget-reallocation.md`. +- The question is campaign budget allocation → keep it as a separate decision; this playbook does + not implement budget changes. - The cohort is too young for the selected subscription model → `cohort-roas.md`, then wait. ## Required inputs - app and ad-group or campaign scope; - exact date window; -- subscription period and selected cohort window; -- revenue variant: gross, proceeds, or net; -- target ROAS, target CPA, allowable CPI, or another explicit decision rule; +- success metric and target; when offering metrics, use this exact priority: `cost_per_paid`, + `cost_per_trial`, then net `roas` at day X; +- subscription period and selected cohort window only for `revenue`, `roas`, `arpu`, `arppu`, + `arpas`, or `roi`; - optional bid floor, ceiling, and aggressiveness policy. +Always use the `net_` variant for revenue-family metrics and do not ask the user to choose a +variant. Never ask for a cohort window or use `--by-days` / `--order-by-day` for `cost_per_paid`, +`cost_per_trial`, or any other non-cohort metric; those values apply directly to the report's date +window. + Do not invent a target, cutoff, bid, or fixed percentage adjustment. If the user has no bid-change policy, identify the bucket first and ask for the exact proposed amount before writing. @@ -43,8 +50,8 @@ policy, identify the bucket first and ask for the exact proposed amount before w 1. Read scoped active and paused keyword metadata to obtain ids, current bids, status, text, and match type. 2. Make one keyword metrics request for spend, Apple and Adapty installs, the selected cost/value - metrics, rank, search popularity, and impression midpoint. Include the approved `--by-days` - window and rank by the expanded cohort metric when applicable. + metrics, rank, search popularity, and impression midpoint. Only for a cohort root, include the + approved `--by-days` window and rank by the expanded `net_` metric. 3. Match metrics rows to the scoped ids. `metrics` has no scope filters; an account-wide row is not permission to act outside the requested scope. 4. Record coverage. If the full scoped set does not fit in the returned page, do not claim a complete @@ -54,7 +61,7 @@ policy, identify the bucket first and ask for the exact proposed amount before w Evaluate maturity before performance: -- The selected cohort window must fit the subscription renewal cycle. +- For a cohort root, the selected cohort window must fit the subscription renewal cycle. - A row with too little sample to support the user's rule is `insufficient_data`. - A zero or missing value after an immature window is not a pause signal. - State why the sample is sufficient or insufficient; do not invent a universal install count. @@ -78,7 +85,7 @@ Show one row per keyword: - keyword text and id; - current bid and status; -- selected metrics and cohort window; +- selected metrics and cohort window when applicable; - category; - proposed bid or status, when supplied by the approved policy; - evidence and confidence; @@ -101,12 +108,13 @@ Do not combine a budget change with this confirmation. ## Output Return the five categories separately, then the mutation proposal, unknowns, and verification -result. State the target, variant, cohort window, and sample limitation above the table. +result. State the target and sample limitation above the table. For a cohort root, also state the +cohort window and net variant; do not invent either field for a non-cohort metric. ## Example ```text -increase — KW-31, "scanner app": day-30 proceeds ROAS 1.34 against the user's 1.20 target; rank and +increase — KW-31, "scanner app": day-30 net ROAS 1.34 against the user's 1.20 target; rank and impression midpoint indicate room for reach. Current bid: $1.10. Proposed bid: unknown until the user supplies an amount or aggressiveness policy. Confidence: medium; economics are mature, future volume is not guaranteed. @@ -124,5 +132,4 @@ volume is not guaranteed. ## Related playbooks - Choose the cohort window → `cohort-roas.md`. -- Reallocate campaign budgets → `budget-reallocation.md`. - Add newly harvested keywords → `search-term-harvesting.md`. diff --git a/skills/apple-ads/references/playbooks/budget-reallocation.md b/skills/apple-ads/references/playbooks/budget-reallocation.md deleted file mode 100644 index ee414bd..0000000 --- a/skills/apple-ads/references/playbooks/budget-reallocation.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Budget reallocation -intent: user wants to move money between campaigns -kind: procedural -risk: writes-money -requires: { cli: ">=0.4.0", subscription: true } -uses: [asa metrics, asa campaigns list, asa campaigns update] -time: TODO ---- - -# Budget reallocation - -> **Stub.** The contract above is real and the linter checks it; the body is not written yet. -> Until it is, follow `SKILL.md` — its numbered workflows cover the command shapes — and treat -> the checklist below as the outline to fill in. - -## To write - -- [ ] When this playbook applies, and when it does not -- [ ] The minimum number of reads that answer the question -- [ ] How the decision is made, and which parts are the user's call rather than yours -- [ ] What to show before writing, and what an explicit confirmation looks like here -- [ ] The write commands, batching and `--idempotency-key` discipline -- [ ] How to verify it landed -- [ ] The failure modes specific to this job - -## Rules that already apply - -- Every write is previewed and confirmed. `--yes` only on a command you run yourself. -- 15 keywords per call; a fresh idempotency key per call. -- Metrics: 5 calls/min, at most 2 per 10s. Plan the whole answer inside that. -- No delete exists. `--status PAUSED` is the only stop. diff --git a/skills/apple-ads/references/playbooks/campaign-launch.md b/skills/apple-ads/references/playbooks/campaign-launch.md deleted file mode 100644 index 0a8c325..0000000 --- a/skills/apple-ads/references/playbooks/campaign-launch.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Launch a campaign -intent: user wants a new campaign live -kind: procedural -risk: writes-money -requires: { cli: ">=0.4.0", subscription: true } -uses: [asa orgs list, asa apps list, asa campaigns create, asa ad-groups create, asa creatives list, asa ads create] -time: TODO ---- - -# Launch a campaign - -> **Stub.** The contract above is real and the linter checks it; the body is not written yet. -> Until it is, follow `SKILL.md` — its numbered workflows cover the command shapes — and treat -> the checklist below as the outline to fill in. - -## To write - -- [ ] When this playbook applies, and when it does not -- [ ] The minimum number of reads that answer the question -- [ ] How the decision is made, and which parts are the user's call rather than yours -- [ ] What to show before writing, and what an explicit confirmation looks like here -- [ ] The write commands, batching and `--idempotency-key` discipline -- [ ] How to verify it landed -- [ ] The failure modes specific to this job - -## Rules that already apply - -- Every write is previewed and confirmed. `--yes` only on a command you run yourself. -- 15 keywords per call; a fresh idempotency key per call. -- Metrics: 5 calls/min, at most 2 per 10s. Plan the whole answer inside that. -- No delete exists. `--status PAUSED` is the only stop. diff --git a/skills/apple-ads/references/playbooks/cohort-roas.md b/skills/apple-ads/references/playbooks/cohort-roas.md index df2bc94..63edd22 100644 --- a/skills/apple-ads/references/playbooks/cohort-roas.md +++ b/skills/apple-ads/references/playbooks/cohort-roas.md @@ -19,15 +19,18 @@ so it is worth doing precisely. There is **no `ltv` metric.** Lifetime value is a cohort metric read at a renewal window, and `--by-days` is how you ask for it. Cohort roots — `revenue`, `arpu`, `arppu`, `arpas`, `roas`, `roi` — expand into `gross_`, `proceeds_` and `net_` variants. Ranking takes the **expanded** name: -`--order-by gross_roas`, never `--order-by roas`. +`--order-by net_roas`, never `--order-by roas`. -Pick the variant deliberately and say which one you used: +The CLI exposes all three variants, but agent workflows always use and offer `net_`: | Variant | What it is | Use when | |---|---|---| -| `gross_` | before Apple's cut | comparing against Apple-side spend metrics | -| `proceeds_` | after Apple's commission | judging real margin | -| `net_` | after refunds and adjustments | closing the books | +| `gross_` | before Apple's cut | CLI-only alternative; do not offer in an agent workflow | +| `proceeds_` | after Apple's commission | CLI-only alternative; do not offer in an agent workflow | +| `net_` | after refunds and adjustments | required agent-workflow variant | + +Do not ask the user to choose a revenue variant. Gross and proceeds remain documented only so the +agent can understand raw CLI output without mistaking those fields for the workflow metric. ## The one call @@ -35,7 +38,7 @@ Pick the variant deliberately and say which one you used: adapty asa metrics --entity keyword \ --date-from --date-to \ --metric roas --by-days 7 --by-days 30 --by-days 90 \ - --order-by gross_roas --order-by-day 30 \ + --order-by net_roas --order-by-day 30 \ --page-size 100 ``` @@ -61,7 +64,7 @@ this playbook. Every cohort looks bad before its first renewal. ## Turning it into a decision -1. State the window and the variant you used, before the numbers. +1. State the window and that net values are used, before the numbers. 2. Compare against the **allowed** CPI derived from the app's own economics, not against a feeling. 3. Split into three buckets: pays back, does not pay back, not enough data. The third is a real bucket — do not force rows into the first two. @@ -73,4 +76,4 @@ this playbook. Every cohort looks bad before its first renewal. - Never compare cohort windows of different lengths against each other. - Never rank by a root name — the call succeeds and ranks by something you did not mean. - Never call a keyword unprofitable on a sample too small to carry the claim. Say the sample is thin. -- Never mix `gross_` in one sentence and `proceeds_` in the next. +- Never offer or select `gross_` or `proceeds_`; agent decisions use `net_` consistently. diff --git a/skills/apple-ads/references/playbooks/keyword-load.md b/skills/apple-ads/references/playbooks/keyword-load.md deleted file mode 100644 index 5347263..0000000 --- a/skills/apple-ads/references/playbooks/keyword-load.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Load a keyword list into an ad group -intent: user has a keyword list and wants it in the account -kind: procedural -risk: writes-money -requires: { cli: ">=0.4.0", subscription: true } -uses: [asa keywords add, asa negative-keywords add] -time: TODO ---- - -# Load a keyword list into an ad group - -> **Stub.** The contract above is real and the linter checks it; the body is not written yet. -> Until it is, follow `SKILL.md` — its numbered workflows cover the command shapes — and treat -> the checklist below as the outline to fill in. - -## To write - -- [ ] When this playbook applies, and when it does not -- [ ] The minimum number of reads that answer the question -- [ ] How the decision is made, and which parts are the user's call rather than yours -- [ ] What to show before writing, and what an explicit confirmation looks like here -- [ ] The write commands, batching and `--idempotency-key` discipline -- [ ] How to verify it landed -- [ ] The failure modes specific to this job - -## Rules that already apply - -- Every write is previewed and confirmed. `--yes` only on a command you run yourself. -- 15 keywords per call; a fresh idempotency key per call. -- Metrics: 5 calls/min, at most 2 per 10s. Plan the whole answer inside that. -- No delete exists. `--status PAUSED` is the only stop. diff --git a/skills/apple-ads/references/playbooks/keyword-opportunity.md b/skills/apple-ads/references/playbooks/keyword-opportunity.md index bfa95d0..d3184df 100644 --- a/skills/apple-ads/references/playbooks/keyword-opportunity.md +++ b/skills/apple-ads/references/playbooks/keyword-opportunity.md @@ -150,7 +150,8 @@ the app's stated cleanup intent. This does not reveal either competitor's bid or ## Related playbooks -- Add a chosen list directly → `keyword-load.md`. +- Add a chosen list directly → use workflow 3 in `SKILL.md` and the keyword-write contract in + `../asa-management.md`. - Choose or change its bid → `bid-optimization.md`. - Route a theme to a page → `creative-setup.md`. - Design initial taxonomy without competitor ids → `../../../apple-ads-strategy/references/keyword-taxonomy.md`. diff --git a/skills/apple-ads/references/playbooks/weekly-review.md b/skills/apple-ads/references/playbooks/weekly-review.md index 37b9168..78847ad 100644 --- a/skills/apple-ads/references/playbooks/weekly-review.md +++ b/skills/apple-ads/references/playbooks/weekly-review.md @@ -31,11 +31,19 @@ Ask for any missing decision-changing input: - exact report window; - whether to compare it with another period; -- the success metric and revenue variant; -- the cohort window that matches the app's subscription model; +- the success metric; when offering choices, use this exact priority: `cost_per_paid`, + `cost_per_trial`, then net `roas` at day X; - any user-defined target or guardrail; +- the cohort window that matches the app's subscription model, only when the selected metric is a + cohort root: `revenue`, `roas`, `arpu`, `arppu`, `arpas`, or `roi`; - optional app or campaign focus. +Revenue-family analysis always uses the `net_` variant. Do not ask the user to choose gross, +proceeds, or net. `--by-days` and `--order-by-day` apply only to the six cohort roots above. For +`cost_per_paid`, `cost_per_trial`, and every other non-cohort metric, use the value aggregated over +the requested date window as returned: never ask for a cohort window, pass a day flag, or attach a +`day-X` label. + If the user does not supply a business target, report facts and outliers without calling them good, bad, profitable, or unprofitable. Do not add an unrequested period comparison. @@ -44,10 +52,12 @@ bad, profitable, or unprofitable. Do not add an unrequested period comparison. Use at most four analytics-family calls: 1. **Overview and trend.** One `metrics overview` call across the requested window. Request spend, - `total_installs`, `adapty_installs`, the selected cost metric, and the selected cohort root. A - per-period series already contains the comparison; never call once per period. + `total_installs`, `adapty_installs`, and the selected success metric. For a cohort root, request + its root and the approved `--by-days` window, then read the `net_` value. A per-period series + already contains the comparison; never call once per period. 2. **Campaign outliers.** One server-sorted campaign call using the user-approved metric, direction, - and cohort window. + and, only for a cohort root, cohort window. Rank revenue-family metrics by their expanded `net_` + name. 3. **Keyword outliers.** One server-sorted keyword call only when the campaign result warrants that level or the user requested it. 4. **Search terms.** One scoped call only when the report is expected to end in growth or waste @@ -80,7 +90,8 @@ relative_gap = absolute_gap / apple_installs # only when apple_installs > 0 ## Decision method -1. State the metric, date window, cohort window, and revenue variant before the result. +1. State the metric and date window before the result. State the cohort window only for a cohort + root; name net as the revenue variant without asking the user to choose it. 2. Separate direct observations from explanations. 3. Choose up to two positive changes and two concerns supported by the requested metric. 4. Mark thin or immature cohorts `insufficient_data`. @@ -108,7 +119,7 @@ Every finding names evidence ids, entities, metrics, windows, confidence, and li ## Example ```text -Direction: spend rose 8% in the requested comparison while day-30 proceeds ROAS was flat. +Direction: spend rose 8% in the requested comparison while day-30 net ROAS was flat. Apple vs Adapty installs: 540 vs 497, a gap of 43 (8.0% of Apple's count). The systems use different attribution and event definitions, so this is a comparison signal, not proof of an error. Primary action: review bids for the three mature keywords below the user's ROAS target.