Weekly check
Resolve the brain: use registry.get_active() when running interactively;
scheduled runs receive the brain path from the routine configuration.
Run core.contracts.check_schema(brain_path) first. If not compatible,
relay the action string and stop before any of the steps below.
Read profile. Launch analytics-reporting-chief and serp-ai-monitor agents
(parallel) with the profile path.
Save their reports under runs/YYYYMMDD-weekly/ (01-analytics.md,
02-serp-ai.md, REPORT.md synthesis).
Append the week's headline observations as signals. Include the
week-over-week ai_referrals trend read from the week's daily signal
lines (the AI-surface list is maintained in hoo-daily's SKILL.md -
quote it, never extend it here); days without an ai_referrals line
are stated as gaps, never interpolated. A sustained rise on a single
landing page (3 or more of the week's daily lines) feeds the citation
tracker: name the page in REPORT.md so the next citation run checks
whether a new AI citation explains the traffic.
3.5. Stage classification. Build rows from this run's GSC page/query pull
(one dict per query: clicks, impressions, position, plus query and page
where the pull has them) and call core.stage.classify(rows):
PYTHONPATH="$CLAUDE_PLUGIN_ROOT/lib" python3 -c "..." importing
core.stage. The thresholds live in that module and are never
restated here (see docs/INFORMATION-MAP.md in the repo). Record the
result as ONE structured signal line - the exact token
stage: <early|growing|established> followed by the returned reason
sentence - and repeat it as the first line of REPORT.md. No GSC pull
this run means no stage: line at all, never a guessed one.
GROWING or ESTABLISHED: every section below runs exactly as it runs
today; this step changes nothing for a site that has clicks. EARLY:
run the early-stage report below in place of the three volume-gated
detectors.
Invoke the organic-os:hoo-reflector skill to propose skillbook deltas from
this week's signals + outcomes.
Queue any proposed work items; rebuild queue; notify per approval channel
with one call:
PYTHONPATH="$CLAUDE_PLUGIN_ROOT/lib" python3 -c "..." importing
core.approval and calling notify_pending(<brain>, send), where send
delivers over the configured channel (same channel selection as
skills/onsite-propose step 4). An item is marked notified ONLY after its
send returns without raising, so a failed send is retried on the next run
rather than lost. Do not call is_notified or mark_notified by hand.
Commit + push if git.
Any content brief this run emits for comparison-intent queries (vs,
alternative, best X for) sets brief_type="comparison" on create_item;
absence means explainer (see docs/site-repo-contract.md).
Run cost: wrapper-invoked runs land one row in
~/.config/organic-os/cost-ledger-YYYYMM.tsv (written by the runtime
wrapper, never by this skill); the Monday report is what surfaces it.
Keyword portfolio
Runs when the GSC connector is reachable AND keywords/tracking.yaml has
entries (read it with core.keywords.load_tracked - never parse the YAML
directly). Either one missing: skip and note it as one line in REPORT.md
("keyword portfolio: skipped, no GSC connector" / "keyword portfolio:
skipped, no tracked keywords") instead of guessing.
- For each tracked keyword, pull the last-28-day GSC average position,
clicks, and impressions for queries matching the tracked term.
1.5. True SERP position, when a rank-capable search-data adapter's tools
are present in this session (the search-data slot, ADR-0009 in the
repo; the known adapter is OpenSEO - tool surface in
$CLAUDE_PLUGIN_ROOT/docs/connectors.md, verify against the tools
actually present, never pin): read each tracked keyword's live
position via get_serp_results, or via the user's configured rank
tracker (get_rank_tracker) when one exists for this site. This is a
different measurement from step 1, and the reason it exists: GSC only
sees queries where the site actually surfaced; the adapter sees where
it did not - on an early site that difference is the whole point.
Record it alongside the GSC average, never instead of it, and name
the adapter as the source on every line that carries its data.
Adapter absent: skip this step, leave the rest of the section exactly
as it is, and add one honest line to REPORT.md: "rank read: GSC
average only - no independent rank source in this session."
- Append one history line per keyword to
keywords/history.tsv
(create it with its header row if absent): date, keyword, position,
clicks, impressions, serp_position, serp_source - tab-separated,
append-only, never edited (format canonical in
docs/site-repo-contract.md). The last two columns are the additive
v0.7 extension carrying step 1.5's read: serp_position is the
adapter's position, serp_source names the adapter; both EMPTY when no
adapter read ran. New columns sit at the end, so old five-column
lines still parse, and an existing file's five-column header is never
rewritten (append-only covers the header too) - readers treat missing
trailing fields as empty. A keyword with zero impressions has an
unknown GSC position: record the row with clicks and impressions 0
and the position field EMPTY - absent, never guessed. Same rule for
serp_position when the adapter did not return the keyword.
- Report movement vs the previous recorded week in REPORT.md: for each
keyword with a prior history line, one line - position now, position
then, the delta, and the clicks/impressions direction. When both
weeks carry a serp_position, add its delta to the same line, named as
the adapter's read. First-ever run: state that history starts today;
there is no movement to report.
- The biggest mover (either direction, by absolute position change)
gets one line in the Monday report's What moved section - the Monday
report reads it from the history file and this run's REPORT.md (see
skills/hoo-monday-report).
THE HONESTY RULE, stated here and repeated in every output that quotes
these numbers: the position column is GSC average position for queries
matching the tracked term - real user impressions, not a scraped SERP
snapshot. The serp_position column, when filled, is a licensed SERP read
the user paid their own search-data adapter for, named in serp_source -
licensed data, not scraping, so docs/adr/0006 in the repo is untouched
either way. Positions unknown to a source are recorded as absent, never
guessed, and neither column ever stands in for the other.
Attribution, for every detector below
The attribution rule is canonical in hoo-daily's anomaly section (step
2.7); it binds the three detectors below without restatement. In short: a
cause may not be asserted without naming the comparison that was actually
run, and every causal claim carries the claim, the comparison performed,
and what would falsify it. Where the comparison was not run, the signal
records cause: unknown (no <comparison> run) instead of a likely story.
A named diagnosis that no one checked is worse than an admitted unknown,
because the next run treats it as settled.
Early-stage report
Runs in place of the three detectors below when step 3.5 classified the
site EARLY and this run pulled any impressions. Those detectors all gate
on click or impression volume, which is precisely the signal a new site
does not have yet, so on an EARLY site they return nothing and the week
reads as silence. It is not silence: position is readable from day one.
- Call
core.stage.early_opportunities(rows) on the same rows step 3.5
built. In REPORT.md and as signals, report how many queries the site
is visible for and across how many pages, then one line per returned
opportunity: query | page | position | impressions | band | the lever
from the note. The note names the lever; it never claims a position
change will follow, the same discipline the attribution rule above
enforces on causes.
- State the expectation plainly: zero clicks at these positions is
normal and not a fault. Nothing is being ranked and skipped over;
there is nothing high enough yet to be clicked.
- Say what would change the picture, naming this week's specific pages
and queries rather than generic advice: title and description work on
the
top band, on-page work on page-two, depth or authority on
visible, and time - indexing and position move over weeks.
- Name the dormant detectors and the threshold that activates each, one
summary line in REPORT.md, so the operator knows why three sections
below are empty:
- striking distance: positions 4.0-15.0 with impressions above the
site's median impressions for the period.
- cannibalization: two pages each earning impressions on one query.
- content decay: 50 or more clicks on a page in the older 28-day
window.
- Week-over-week progress is the headline at this stage, so state it:
new queries the site became visible for since last week, and any
opportunity that changed band. First run on an EARLY site: say that
the comparison starts this week rather than implying movement.
Classified EARLY with zero impressions: skip this section and note it in
REPORT.md as one line ("early-stage report: skipped, no impressions
yet"). There are no positions to band.
Striking distance
EARLY site (step 3.5): dormant, see the early-stage report above.
- Pull GSC queries for the last 28 days for the profile's site.
- Filter to positions 4.0-15.0 with impressions above the site's median
impressions for the period.
- Group the filtered queries by landing page.
- For the top 5 opportunities, write one P2 signal per opportunity in
falsifiable form: query | page | position | impressions | leading
indicator to watch. Any reason offered for why a page sits stuck at
that position - thin content, missing internal links, a stronger
competitor - names the comparison that produced it (the pages
actually inspected, the link graph actually read). No comparison, no
reason: write
cause: unknown and let the opportunity stand on the
numbers, which are enough to justify the work.
- Where a single page carries 2+ striking-distance queries,
create_item( kind="onpage-fix", ...) naming the specific on-page focus (the queries
it should consolidate around) - gated through the approval queue like
every other proposal, never applied directly.
No GSC connector: skip this section and note it as one line in REPORT.md
("striking distance: skipped, no GSC connector") instead of guessing.
Cannibalization
EARLY site (step 3.5): dormant, see the early-stage report above.
- From the same 28-day GSC query pull, find queries where two or more
pages each earned impressions and neither holds a stable majority
(guideline: the second page carries 20% or more of the query's
impressions).
- For the top 3 offending queries by total impressions, write one P2
signal each: the query, both pages with their positions, the
impression split between them, the comparison that produced the
diagnosis (the per-page impression split across the same 28-day pull,
named explicitly), and the falsifiability check - "if consolidating
did not lift the primary page's position within 28 days, the
diagnosis was wrong." Cannibalization is a claim about two pages
competing; without the split actually computed for both, it is a
guess and the signal says
cause: unknown instead.
- Where a page on this list also appears in the striking-distance list
above, note the linkage in the signal: cannibalization is often the
blocker behind a stuck striking-distance position, not a content or
authority gap.
- For the single clearest case (largest impression split, most obvious
primary-page pick),
create_item(kind="onpage-fix", ...) naming the
recommended consolidation direction - canonical tag, 301 redirect, or
content merge - as a proposal item, gated through the approval queue
like every other proposal, never applied directly. At most 1 gated
proposal from this section per run.
No GSC connector: skip this section and note it as one line in REPORT.md
("cannibalization: skipped, no GSC connector") instead of guessing.
Content decay
EARLY site (step 3.5): dormant, see the early-stage report above.
- Pull each page's GSC clicks for the last 28 days and for the same
page's 28-day window starting 90 days prior - two date-windowed pulls,
not a single trend line.
- Flag pages with a 30% or greater click decline between the two windows
AND at least 50 clicks in the older window (noise floor - a 5-click
page swinging 30% is not a signal).
- For the top 3 flagged pages by absolute click loss, write one P2
signal each: the page, both window values, the decline percent, and a
cause line naming the comparison behind it. The comparison here is
position-vs-CTR movement across the same two windows - position fell
= ranking problem; position held but CTR fell = SERP feature
intrusion or title/meta staleness. Name which one the data points to
AND state that this is the comparison that produced it. If position
and CTR were not both pulled for both windows, the comparison did not
happen: record
cause: unknown (position-vs-CTR not pulled for both windows) and report the decline on its own.
- For the single clearest case,
create_item(kind="content-brief", ...)
as a refresh brief - target the decayed page, cite the decline and the
likely-cause hypothesis, and let it move through the normal brief
lifecycle (approve -> skills/ce-produce -> skills/onsite-publish). At
most 1 gated proposal from this section per run; never drafted or
applied directly.
No GSC connector: skip this section and note it as one line in REPORT.md
("content decay: skipped, no GSC connector") instead of guessing.
Mention opportunities
Runs after the detectors above, capped at roughly 10 minutes of work per
run - this is a sample, not a census.
- From the profile take the top 3 topics (keywords.targets, file order)
and the top 2 competitors (competitors, file order).
- Via WebSearch, sample where the brand and those competitors are
mentioned across public surfaces for those topics - industry
roundups, comparison posts, community threads. Record per surface:
URL, does the brand appear, does each competitor appear.
WebSearch/WebFetch of public pages within the session is the
sanctioned mechanism (docs/adr/0006 in the repo, same as the
citation tracker); never scrape engines or third-party tools.
- For each surface where a competitor appears and the brand does not,
write one P3 signal in falsifiable form: the surface | why it
matters (one line: what the surface answers and for whom) | the
falsifiability check - "if a mention landed here does not show up in
AI answers or referral traffic within 90 days, this surface mattered
less than it looked."
- At most ONE outreach proposal per run: for the single best-fit gap,
create_item(kind="strategy", ...) naming the target surface, the
angle (why that editor or thread would plausibly include the brand),
and the existing asset to reference (a page, a tool, a data point
already published - never one to be invented). Gated through the
approval queue like every other proposal; a human executes the
outreach. This skill NEVER contacts anyone - no emails, no form
fills, no posts, no DMs.
- Rationale line, carried next to this section's output in REPORT.md:
brand mentions correlate roughly 3x more strongly with AI visibility
than backlinks do, per Ahrefs' 75,000-brand study
(https://ahrefs.com/blog/ai-brand-visibility-correlations/).
- Sampling caveat, same discipline as the AI-visibility baseline:
state which surfaces this session actually reached, and never let
the report imply broader coverage than that.
No web access this session: skip this section and note it as one line
in REPORT.md ("mention opportunities: skipped, no web access") instead
of guessing.
1---2name: hoo-weekly3description: Use for the weekly health check + reflection - "run the weekly", scheduled weekly routine. Runs analytics-reporting-chief and serp-ai-monitor, then hands the week to the reflector.4---56# Weekly check78Resolve the brain: use registry.get_active() when running interactively;9scheduled runs receive the brain path from the routine configuration.10110. Run `core.contracts.check_schema(brain_path)` first. If not compatible,12 relay the action string and stop before any of the steps below.13141. Read profile. Launch analytics-reporting-chief and serp-ai-monitor agents15 (parallel) with the profile path.162. Save their reports under runs/YYYYMMDD-weekly/ (01-analytics.md,17 02-serp-ai.md, REPORT.md synthesis).183. Append the week's headline observations as signals. Include the19 week-over-week `ai_referrals` trend read from the week's daily signal20 lines (the AI-surface list is maintained in hoo-daily's SKILL.md -21 quote it, never extend it here); days without an `ai_referrals` line22 are stated as gaps, never interpolated. A sustained rise on a single23 landing page (3 or more of the week's daily lines) feeds the citation24 tracker: name the page in REPORT.md so the next citation run checks25 whether a new AI citation explains the traffic.263.5. Stage classification. Build rows from this run's GSC page/query pull27 (one dict per query: clicks, impressions, position, plus query and page28 where the pull has them) and call `core.stage.classify(rows)`:29 `PYTHONPATH="$CLAUDE_PLUGIN_ROOT/lib" python3 -c "..."` importing30 `core.stage`. The thresholds live in that module and are never31 restated here (see docs/INFORMATION-MAP.md in the repo). Record the32 result as ONE structured signal line - the exact token33 `stage: <early|growing|established>` followed by the returned reason34 sentence - and repeat it as the first line of REPORT.md. No GSC pull35 this run means no `stage:` line at all, never a guessed one.36 GROWING or ESTABLISHED: every section below runs exactly as it runs37 today; this step changes nothing for a site that has clicks. EARLY:38 run the early-stage report below in place of the three volume-gated39 detectors.404. Invoke the organic-os:hoo-reflector skill to propose skillbook deltas from41 this week's signals + outcomes.425. Queue any proposed work items; rebuild queue; notify per approval channel43 with one call:44 `PYTHONPATH="$CLAUDE_PLUGIN_ROOT/lib" python3 -c "..."` importing45 `core.approval` and calling `notify_pending(<brain>, send)`, where `send`46 delivers over the configured channel (same channel selection as47 skills/onsite-propose step 4). An item is marked notified ONLY after its48 send returns without raising, so a failed send is retried on the next run49 rather than lost. Do not call `is_notified` or `mark_notified` by hand.50 Commit + push if git.5152Any content brief this run emits for comparison-intent queries (vs,53alternative, best X for) sets `brief_type="comparison"` on `create_item`;54absence means explainer (see docs/site-repo-contract.md).5556Run cost: wrapper-invoked runs land one row in57`~/.config/organic-os/cost-ledger-YYYYMM.tsv` (written by the runtime58wrapper, never by this skill); the Monday report is what surfaces it.5960## Keyword portfolio6162Runs when the GSC connector is reachable AND keywords/tracking.yaml has63entries (read it with `core.keywords.load_tracked` - never parse the YAML64directly). Either one missing: skip and note it as one line in REPORT.md65("keyword portfolio: skipped, no GSC connector" / "keyword portfolio:66skipped, no tracked keywords") instead of guessing.67681. For each tracked keyword, pull the last-28-day GSC average position,69 clicks, and impressions for queries matching the tracked term.701.5. True SERP position, when a rank-capable search-data adapter's tools71 are present in this session (the search-data slot, ADR-0009 in the72 repo; the known adapter is OpenSEO - tool surface in73 `$CLAUDE_PLUGIN_ROOT/docs/connectors.md`, verify against the tools74 actually present, never pin): read each tracked keyword's live75 position via `get_serp_results`, or via the user's configured rank76 tracker (`get_rank_tracker`) when one exists for this site. This is a77 different measurement from step 1, and the reason it exists: GSC only78 sees queries where the site actually surfaced; the adapter sees where79 it did not - on an early site that difference is the whole point.80 Record it alongside the GSC average, never instead of it, and name81 the adapter as the source on every line that carries its data.82 Adapter absent: skip this step, leave the rest of the section exactly83 as it is, and add one honest line to REPORT.md: "rank read: GSC84 average only - no independent rank source in this session."852. Append one history line per keyword to `keywords/history.tsv`86 (create it with its header row if absent): date, keyword, position,87 clicks, impressions, serp_position, serp_source - tab-separated,88 append-only, never edited (format canonical in89 docs/site-repo-contract.md). The last two columns are the additive90 v0.7 extension carrying step 1.5's read: serp_position is the91 adapter's position, serp_source names the adapter; both EMPTY when no92 adapter read ran. New columns sit at the end, so old five-column93 lines still parse, and an existing file's five-column header is never94 rewritten (append-only covers the header too) - readers treat missing95 trailing fields as empty. A keyword with zero impressions has an96 unknown GSC position: record the row with clicks and impressions 097 and the position field EMPTY - absent, never guessed. Same rule for98 serp_position when the adapter did not return the keyword.993. Report movement vs the previous recorded week in REPORT.md: for each100 keyword with a prior history line, one line - position now, position101 then, the delta, and the clicks/impressions direction. When both102 weeks carry a serp_position, add its delta to the same line, named as103 the adapter's read. First-ever run: state that history starts today;104 there is no movement to report.1054. The biggest mover (either direction, by absolute position change)106 gets one line in the Monday report's What moved section - the Monday107 report reads it from the history file and this run's REPORT.md (see108 skills/hoo-monday-report).109110THE HONESTY RULE, stated here and repeated in every output that quotes111these numbers: the position column is GSC average position for queries112matching the tracked term - real user impressions, not a scraped SERP113snapshot. The serp_position column, when filled, is a licensed SERP read114the user paid their own search-data adapter for, named in serp_source -115licensed data, not scraping, so docs/adr/0006 in the repo is untouched116either way. Positions unknown to a source are recorded as absent, never117guessed, and neither column ever stands in for the other.118119## Attribution, for every detector below120121The attribution rule is canonical in hoo-daily's anomaly section (step1222.7); it binds the three detectors below without restatement. In short: a123cause may not be asserted without naming the comparison that was actually124run, and every causal claim carries the claim, the comparison performed,125and what would falsify it. Where the comparison was not run, the signal126records `cause: unknown (no <comparison> run)` instead of a likely story.127A named diagnosis that no one checked is worse than an admitted unknown,128because the next run treats it as settled.129130## Early-stage report131132Runs in place of the three detectors below when step 3.5 classified the133site EARLY and this run pulled any impressions. Those detectors all gate134on click or impression volume, which is precisely the signal a new site135does not have yet, so on an EARLY site they return nothing and the week136reads as silence. It is not silence: position is readable from day one.1371381. Call `core.stage.early_opportunities(rows)` on the same rows step 3.5139 built. In REPORT.md and as signals, report how many queries the site140 is visible for and across how many pages, then one line per returned141 opportunity: query | page | position | impressions | band | the lever142 from the note. The note names the lever; it never claims a position143 change will follow, the same discipline the attribution rule above144 enforces on causes.1452. State the expectation plainly: zero clicks at these positions is146 normal and not a fault. Nothing is being ranked and skipped over;147 there is nothing high enough yet to be clicked.1483. Say what would change the picture, naming this week's specific pages149 and queries rather than generic advice: title and description work on150 the `top` band, on-page work on `page-two`, depth or authority on151 `visible`, and time - indexing and position move over weeks.1524. Name the dormant detectors and the threshold that activates each, one153 summary line in REPORT.md, so the operator knows why three sections154 below are empty:155 - striking distance: positions 4.0-15.0 with impressions above the156 site's median impressions for the period.157 - cannibalization: two pages each earning impressions on one query.158 - content decay: 50 or more clicks on a page in the older 28-day159 window.1605. Week-over-week progress is the headline at this stage, so state it:161 new queries the site became visible for since last week, and any162 opportunity that changed band. First run on an EARLY site: say that163 the comparison starts this week rather than implying movement.164165Classified EARLY with zero impressions: skip this section and note it in166REPORT.md as one line ("early-stage report: skipped, no impressions167yet"). There are no positions to band.168169## Striking distance170171EARLY site (step 3.5): dormant, see the early-stage report above.1721731. Pull GSC queries for the last 28 days for the profile's site.1742. Filter to positions 4.0-15.0 with impressions above the site's median175 impressions for the period.1763. Group the filtered queries by landing page.1774. For the top 5 opportunities, write one P2 signal per opportunity in178 falsifiable form: query | page | position | impressions | leading179 indicator to watch. Any reason offered for why a page sits stuck at180 that position - thin content, missing internal links, a stronger181 competitor - names the comparison that produced it (the pages182 actually inspected, the link graph actually read). No comparison, no183 reason: write `cause: unknown` and let the opportunity stand on the184 numbers, which are enough to justify the work.1855. Where a single page carries 2+ striking-distance queries, `create_item(186 kind="onpage-fix", ...)` naming the specific on-page focus (the queries187 it should consolidate around) - gated through the approval queue like188 every other proposal, never applied directly.189190No GSC connector: skip this section and note it as one line in REPORT.md191("striking distance: skipped, no GSC connector") instead of guessing.192193## Cannibalization194195EARLY site (step 3.5): dormant, see the early-stage report above.1961971. From the same 28-day GSC query pull, find queries where two or more198 pages each earned impressions and neither holds a stable majority199 (guideline: the second page carries 20% or more of the query's200 impressions).2012. For the top 3 offending queries by total impressions, write one P2202 signal each: the query, both pages with their positions, the203 impression split between them, the comparison that produced the204 diagnosis (the per-page impression split across the same 28-day pull,205 named explicitly), and the falsifiability check - "if consolidating206 did not lift the primary page's position within 28 days, the207 diagnosis was wrong." Cannibalization is a claim about two pages208 competing; without the split actually computed for both, it is a209 guess and the signal says `cause: unknown` instead.2103. Where a page on this list also appears in the striking-distance list211 above, note the linkage in the signal: cannibalization is often the212 blocker behind a stuck striking-distance position, not a content or213 authority gap.2144. For the single clearest case (largest impression split, most obvious215 primary-page pick), `create_item(kind="onpage-fix", ...)` naming the216 recommended consolidation direction - canonical tag, 301 redirect, or217 content merge - as a proposal item, gated through the approval queue218 like every other proposal, never applied directly. At most 1 gated219 proposal from this section per run.220221No GSC connector: skip this section and note it as one line in REPORT.md222("cannibalization: skipped, no GSC connector") instead of guessing.223224## Content decay225226EARLY site (step 3.5): dormant, see the early-stage report above.2272281. Pull each page's GSC clicks for the last 28 days and for the same229 page's 28-day window starting 90 days prior - two date-windowed pulls,230 not a single trend line.2312. Flag pages with a 30% or greater click decline between the two windows232 AND at least 50 clicks in the older window (noise floor - a 5-click233 page swinging 30% is not a signal).2343. For the top 3 flagged pages by absolute click loss, write one P2235 signal each: the page, both window values, the decline percent, and a236 cause line naming the comparison behind it. The comparison here is237 position-vs-CTR movement across the same two windows - position fell238 = ranking problem; position held but CTR fell = SERP feature239 intrusion or title/meta staleness. Name which one the data points to240 AND state that this is the comparison that produced it. If position241 and CTR were not both pulled for both windows, the comparison did not242 happen: record `cause: unknown (position-vs-CTR not pulled for both243 windows)` and report the decline on its own.2444. For the single clearest case, `create_item(kind="content-brief", ...)`245 as a refresh brief - target the decayed page, cite the decline and the246 likely-cause hypothesis, and let it move through the normal brief247 lifecycle (approve -> skills/ce-produce -> skills/onsite-publish). At248 most 1 gated proposal from this section per run; never drafted or249 applied directly.250251No GSC connector: skip this section and note it as one line in REPORT.md252("content decay: skipped, no GSC connector") instead of guessing.253254## Mention opportunities255256Runs after the detectors above, capped at roughly 10 minutes of work per257run - this is a sample, not a census.2582591. From the profile take the top 3 topics (keywords.targets, file order)260 and the top 2 competitors (competitors, file order).2612. Via WebSearch, sample where the brand and those competitors are262 mentioned across public surfaces for those topics - industry263 roundups, comparison posts, community threads. Record per surface:264 URL, does the brand appear, does each competitor appear.265 WebSearch/WebFetch of public pages within the session is the266 sanctioned mechanism (docs/adr/0006 in the repo, same as the267 citation tracker); never scrape engines or third-party tools.2683. For each surface where a competitor appears and the brand does not,269 write one P3 signal in falsifiable form: the surface | why it270 matters (one line: what the surface answers and for whom) | the271 falsifiability check - "if a mention landed here does not show up in272 AI answers or referral traffic within 90 days, this surface mattered273 less than it looked."2744. At most ONE outreach proposal per run: for the single best-fit gap,275 `create_item(kind="strategy", ...)` naming the target surface, the276 angle (why that editor or thread would plausibly include the brand),277 and the existing asset to reference (a page, a tool, a data point278 already published - never one to be invented). Gated through the279 approval queue like every other proposal; a human executes the280 outreach. This skill NEVER contacts anyone - no emails, no form281 fills, no posts, no DMs.2825. Rationale line, carried next to this section's output in REPORT.md:283 brand mentions correlate roughly 3x more strongly with AI visibility284 than backlinks do, per Ahrefs' 75,000-brand study285 (https://ahrefs.com/blog/ai-brand-visibility-correlations/).2866. Sampling caveat, same discipline as the AI-visibility baseline:287 state which surfaces this session actually reached, and never let288 the report imply broader coverage than that.289290No web access this session: skip this section and note it as one line291in REPORT.md ("mention opportunities: skipped, no web access") instead292of guessing.