# Hector Search Term Negation

> Use ONLY when the user explicitly asks to add negative search terms based on bleeding data — trigger phrases include "run search term negation for X", "negate bleeding search terms for X", "add negatives for bleeding in X", "execute bleeding negation on X", and close paraphrases where "negation" or "negative" + a named brand co-occur. Identifies bleeding search terms per the standard Hector bleeding rules (SP-only, ≥ floor(1/CR) clicks, 0 orders, exclude branded, exclude ASIN-pattern, ungrouped), then negates each identified SearchTerm as a NEGATIVE EXACT MATCH in the specific ad group where it is bleeding. Does NOT fire on conversational mentions of negation, on single-term negation requests, on audit requests, or on requests to edit this skill. ALWAYS presents a preview and requires explicit CONFIRM before any write. Runs independently of the audit skill — no prior audit required.

- Skill: `hectorai-live/hector-search-term-negation` (Agent Skill)
- Install (CLI): `npx skillmds@latest add hectorai-live/hector-search-term-negation`
- Raw SKILL.md: https://api.skillmd.com/api/skills/hectorai-live/hector-search-term-negation/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: hectorai-live (https://skillmd.com/u/hectorai-live)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/hectorai-live/hector-search-term-negation

---


# Hector Search Term Negation (v1)

> ## ⚠️ CRITICAL OBLIGATIONS — READ BEFORE EVERY USE
>
> 1. **ALWAYS present preview + require explicit CONFIRM before any write.** Present summary + top-10 preview. User must type CONFIRM (case-insensitive) to proceed. Any other input = CANCEL. This is a hard gate — never write without an explicit CONFIRM.
> 2. **NEVER guess the profile_id.** Resolve via `find_profile_by_name`. If a brand was already discussed earlier in this chat, ask the user to confirm rather than asking cold. If multiple profiles match, present them and let user pick. If zero match, stop.
> 3. **Match type is LOCKED at negative exact match.** Never negative phrase, never negative broad. This is non-negotiable.
> 4. **Negations are scoped to the specific ad group where the term is bleeding.** If SearchTerm "soap" bleeds in ad group A AND ad group B, that becomes two separate negation writes — one per ad group. Never negate the same term across all ad groups.
> 5. **Bleeding logic is IDENTICAL to the audit skill.** SP-only, `Clicks > floor(1/CR)-1`, `Orders = 0`, exclude branded (via brand classifier), exclude ASIN-pattern (`^b0[a-z0-9]{8}$`), ungrouped. Same rules, same filter, same click floor computation.
> 6. **Duplicate negations are SKIPPED and LOGGED.** If a SearchTerm is already negated as exact in the target ad group, do not re-add. Log the skip in the change-log CSV with reason "already negated."
> 7. **Every run produces a change-log CSV** at `/mnt/user-data/outputs/negation_<brand>_<timestamp>.csv` with columns: SearchTerm, Campaign, AdGroup, AdGroupID, Status (added / skipped-duplicate / failed), Reason. This is the audit trail for the write operation.
> 8. **Runs independently of the audit skill.** Fresh MCP pull every time. No dependency on prior audit runs, no reading of audit temp files.
> 9. **Run the closing self-check before delivering.**

---

## Why this skill exists

The Hector brand audit surfaces bleeding search terms in Pillar 2 but does not act on them. Historically, an analyst would export the bleeding table and manually add negatives ad group by ad group — slow, error-prone, and inconsistent across analysts. This skill executes the negation cleanly: same logic as the audit, scoped to the correct ad group, exact match only, with a full audit trail.

The skill is designed to run independently. A user can invoke it without ever having run an audit — the bleeding pull is fresh every time.

---

## When this skill activates

**Fires on:**
- "Run search term negation for \<brand\>"
- "Negate bleeding search terms for \<brand\>"
- "Add negatives for bleeding search terms in \<brand\>"
- "Execute bleeding negation on \<brand\>"
- Close paraphrases where "negation" or "negative" + a named brand co-occur

**Does NOT fire on:**
- Conversational mentions of negation ("we ran negation last week")
- Single-term negation requests ("negate 'soap' for X") — those are atomic tool calls, not this skill
- Audit requests — even if the audit surfaces bleeding, running the audit does NOT trigger negation
- Requests to edit, improve, or review this skill (those go to skill-architect)
- Requests to negate branded terms or terms already in exact match — the skill refuses these by design

---

## Blocking precondition

> **You CANNOT proceed to any MCP write call until:**
> 1. All REQUIRED precursors are collected (account+country, date range, campaign scope)
> 2. Bleeding pull is complete and reviewed
> 3. User has typed CONFIRM (case-insensitive) in response to the preview

---

## Step-by-Step Execution

### Step 1 — Present the precursor block

Open with one sentence acknowledging the negation request. Then present:

```
Before I start, I need a few things:

REQUIRED
• Account: brand name AND country (e.g. "<brand name>, India") — I'll ask you to confirm if a brand was already discussed in this chat
• Date range: e.g. "Jan 1 – May 26, 2026". Default if unspecified: last 60 days.
• Scope: negate across ALL campaigns, or a SPECIFIC list of campaigns? (default: ALL)

I'll auto-detect brand tokens from the name you give — spaced, joined, "&"/"and", initialism, and common typo forms — and echo them before running so you can add/remove. Branded search terms are excluded from negation.
```

**Context-aware precursor:** If a brand was clearly referenced earlier in this chat (e.g., an audit was recently run), replace the account prompt with a confirmation:
```
I see we discussed <brand> in this chat. Confirm this is the account for negation, or specify a different brand + country.
```

If the user already supplied all REQUIRED precursors, skip to Step 2.

**Stopping condition:** all REQUIRED precursors collected.

### Step 2 — Resolve the profile

Call `find_profile_by_name` with the confirmed brand. Filter to the user's country.

- One match in country: confirm in one line, proceed
- Multiple in country: list with profile_id + currency, ask user to pick
- Zero in country: surface, ask about a different country, or stop

**Stopping condition:** exactly one profile_id confirmed.

### Step 3 — Echo brand classifier tokens

Auto-detect brand tokens using the same variant-building rules as the audit skill:
the original spaced form, the joined form, the "&" and "and" alternatives,
initialisms where the name has multiple words, and plausible misspellings.

Apply them to whatever brand the user named — the skill is not scoped to any
particular set of accounts.

Echo the generated tokens. Wait one turn for override. Default-accept if no response.

**Stopping condition:** brand tokens locked.

### Step 4 — Compute account CR and bleeding click floor

One call: `analytics_account_health`
- Filters: date range
- Metrics: `["CR_perc"]`

Compute: `click_floor = floor(1 / (CR_perc / 100))`

Store as `click_floor` for the next step.

### Step 5 — Pull bleeding data

One call: `analytics_search_term_list`

`master_search_filters`:
- `{level: "Clicks", type: "greater_than", keywords: click_floor - 1, condition: "and"}`
- `{level: "Orders", type: "equal", keywords: 0, condition: "and"}`
- `{level: "CampaignType", type: "is", keywords: ["sponsored products"], condition: "and"}`
- If scope = specific campaigns: `{level: "CampaignName", type: "is", keywords: [list of campaign names], condition: "and"}`

Metrics: `["Spend","Clicks"]`
`limit=1000`, `sort_column=Spend`, `sort_type=DESC`

Save to `/home/claude/negation_data/bleeders_raw.json`. Python script:

1. Exclude branded SearchTerms via brand classifier regex from Step 3
2. Exclude ASIN-pattern SearchTerms (`^b0[a-z0-9]{8}$`)
3. Keep UNGROUPED — one row per SearchTerm × Campaign × AdGroup
4. Compute per-row CPC = Spend / Clicks
5. Sort by Spend DESC

Store as `bleeders_qualified.json`. Surface only aggregates + top-10 preview in chat.

If zero qualifying rows: stop with message "No bleeding search terms found for this account and window meeting the negation criteria."

If exactly 1000 rows returned: flag truncation — the actual bleed set may be larger. Proceed with what was returned; note truncation in the change-log.

### Step 6 — Resolve ad group IDs

The bleeding pull returns Campaign name + AdGroup name. Negation writes need AdGroup IDs.

Extract unique (Campaign name, AdGroup name) pairs from `bleeders_qualified.json`. For each unique pair, resolve to AdGroup ID. Use whichever tool the Hector MCP exposes for ad-group lookup — likely `analytics_ad_group_list` filtered by campaign name and ad-group name, or `query_ad_group` if it accepts name lookup. If both are unavailable, use `search_records` with the ad-group name and campaign filter as fallback.

Store the resolved mapping in `adgroup_id_map.json`. If any pair fails to resolve, log it with reason "adgroup ID resolution failed" — these rows will be skipped in Step 8.

### Step 7 — Check for existing negatives (dedupe)

For each unique AdGroup ID in the mapping, pull the current set of negative exact keywords via `negative_keyword_tools` (list mode) or equivalent. Store as `existing_negatives.json`.

Cross-reference each bleeding row against the existing negatives:
- If the SearchTerm is already negated as exact in that AdGroup → mark as `skipped-duplicate`
- Otherwise → mark as `to-add`

Store as `negation_plan.json` with the final action per row.

### Step 8 — Present preview and wait for CONFIRM

Present in chat:

```
SEARCH TERM NEGATION — PREVIEW

Account: <brand>, <country>, profile <id>
Date range: <start> to <end>
Scope: <ALL campaigns | X campaigns>

Bleeding analysis complete:
• <total> search terms bleeding across <unique campaign count> campaigns
• <to-add count> will be added as negative exact match
• <skipped-duplicate count> already negated in target ad group — will be skipped
• <resolution-failed count> ad group resolution failed — will be skipped

TOP 10 BY WASTED SPEND (ordered by Spend DESC):
1. "<term>" — <campaign> / <adgroup> — <clicks>c, ₹<spend>
2. ...
...

Reply CONFIRM to add these <to-add count> negatives as EXACT MATCH negatives.
Reply CANCEL to stop.
```

**Wait for user response.** If they type CONFIRM (case-insensitive, exact word) → proceed to Step 9. Anything else = CANCEL, stop, no writes performed. Log the cancellation.

### Step 9 — Execute negations

For each `to-add` row in `negation_plan.json`:

1. Call the negative-keyword add tool (likely `negative_keyword_tools` in add mode) with:
   - AdGroup ID
   - Keyword: SearchTerm
   - Match type: `negativeExact` (or equivalent exact-negative match type per the tool schema)
   - State: enabled
2. Capture the response
3. Update `negation_plan.json` with status: `added` (success) or `failed` (with error message as reason)

Process in batches if the tool supports bulk mode. Otherwise iterate one at a time.

**Stopping condition:** all rows processed (either added, skipped, or failed).

### Step 10 — Write change-log CSV

Write to `/mnt/user-data/outputs/negation_<brand_slug>_<YYYYMMDD_HHMMSS>.csv`:

Columns:
- SearchTerm
- Campaign
- AdGroup
- AdGroupID
- Status (added / skipped-duplicate / adgroup-resolution-failed / write-failed)
- Reason (empty for added; error message for failed; "already negated as exact" for skipped-duplicate; etc.)

Every row from the qualifying bleeders is represented in the CSV, regardless of final status. This is the complete audit trail.

### Step 11 — Present the CSV and recap

Call `present_files` with the CSV path. Post a recap:

```
NEGATION COMPLETE

• <added> negatives added
• <skipped-duplicate> skipped (already negated)
• <resolution-failed + write-failed> failed
• Full log: negation_<brand>_<timestamp>.csv

Any failed rows should be reviewed manually. Duplicate skips are expected on repeat runs.
```

Run the closing self-check.

---

## Closing Self-Check (RUN BEFORE EVERY RESPONSE)

- [ ] Precursor block presented (or skipped only if all REQUIRED supplied in opening message)
- [ ] Account+country resolved via `find_profile_by_name` — never guessed
- [ ] Brand classifier tokens echoed
- [ ] Bleeding click floor = `floor(1 / (CR_perc/100))` — never hardcoded
- [ ] Bleeding filter is SP-only (server-side)
- [ ] Bleeding excludes branded SearchTerms (client-side, via brand classifier)
- [ ] Bleeding excludes ASIN-pattern SearchTerms
- [ ] Bleeding is UNGROUPED — one row per SearchTerm × Campaign × AdGroup
- [ ] AdGroup IDs resolved via lookup — never assumed
- [ ] Existing negatives checked; duplicates flagged as skipped
- [ ] Preview presented with summary + top-10
- [ ] User typed CONFIRM (exact word, case-insensitive) before ANY write happened
- [ ] Match type on every negation is EXACT — never phrase, never broad
- [ ] Each negation scoped to the specific AdGroup where the term is bleeding — never global
- [ ] Change-log CSV written with all rows and final statuses
- [ ] `present_files` called on the CSV
- [ ] Recap posted with counts

If any check fails, fix it before delivering.

---

## What NOT to Do

- **Never write ANY negation without an explicit CONFIRM from the user.** Even if the preview looks correct, wait for CONFIRM. No implicit consent.
- **Never use phrase or broad match negation.** Exact match only.
- **Never negate a term globally or across multiple ad groups from a single decision.** Each negation is scoped to a specific AdGroup.
- **Never negate branded search terms.** The brand classifier exclusion is applied at Step 5.
- **Never negate ASIN-pattern search terms.** Those are competitor product searches — different treatment.
- **Never skip the AdGroup ID resolution step.** Writing to the wrong ID corrupts the account.
- **Never proceed if AdGroup ID resolution fails for a row.** Log it, skip it, surface the count in the recap.
- **Never re-add a negation that already exists in the target ad group.** Skip and log.
- **Never rely on audit temp files or prior conversation state for bleeding data.** Fresh pull every time.
- **Never group the bleeding table.** Ungrouped is mandatory — negation is per ad group.
- **Never omit the change-log CSV.** Even if zero negations were added, the CSV is written (for cancellation logging).
- **Never continue to Step 9 if the user replies anything other than CONFIRM.** "yes", "ok", "sure", "go" = CANCEL. Only the literal word CONFIRM proceeds.

---

## Edge Cases

| Situation | Behaviour |
|---|---|
| User says "run negation for X" with no other info | Run full precursor block |
| Brand was discussed earlier in chat | Ask user to confirm, don't re-elicit cold |
| Multiple profiles match brand+country | List them, ask user to pick |
| Zero bleeding rows qualify | Stop after Step 5. Message: "No bleeding search terms found meeting the negation criteria." No CSV written. |
| Exactly 1000 rows returned (truncation) | Flag in preview. Continue with what was returned. Note in recap. |
| User replies "yes" instead of "CONFIRM" | Treat as CANCEL. Message: "Cancelled — must reply CONFIRM exactly. Re-run the skill to try again." |
| User cancels | Log the cancellation in a CSV with "cancelled" status on all rows. No writes performed. |
| AdGroup ID resolution fails for some rows | Log those as `adgroup-resolution-failed`. Continue with the rest. Report count in recap. |
| Negative-keyword-add tool returns error on a row | Log as `write-failed` with error message. Continue with remaining rows. |
| Skill invoked twice in one chat for different brands | Re-run full precursor block. Reset all temp files under `/home/claude/negation_data/`. |
| User specifies scope as "specific campaigns" | Elicit the campaign list. Pass to the bleeding pull as a CampaignName filter. |
| Focus-ASIN request | Politely decline — explain that negation lives at ad-group level, not ASIN level. |
| Audit skill was run earlier in same chat | Ignore its temp files. Fresh pull as normal. |
| Account has zero spend in window | Stop with message: "Account had no ad spend in the specified window — no bleeding to negate." |
| User asks the skill to also add positive keywords (harvesting) | Decline — that's the Harvesting skill (separate). |
| CR_perc from account_health returns 0 or null | Stop with error: "Cannot compute click floor — account CR is zero for this window. Choose a different date range." |

---

## Worked Examples

### Example 1 — Happy path

**User:** *"Run search term negation for <brand> India, last 30 days"*

1. Acknowledge. All REQUIRED except scope supplied — ask for scope only.
2. User: "All campaigns."
3. Resolve the profile for <brand> IN → confirm.
4. Echo the generated brand tokens. Accept.
5. Pull CR → compute click floor (e.g., 7).
6. Bleeding pull: 87 rows returned.
7. Filter branded + ASIN → 74 qualifying rows.
8. Resolve AdGroup IDs → 71 resolve, 3 fail.
9. Dedupe against existing negatives → 68 to-add, 3 skipped-duplicate.
10. Present preview: "74 bleeding terms across 12 campaigns. 68 will be added, 3 already negated, 3 resolution failed."
11. User: "CONFIRM"
12. Execute 68 writes. 67 succeed, 1 fails with API error.
13. Write CSV, present, recap.
14. Self-check ✓.

### Example 2 — Context-aware account resolution

**User (earlier in chat):** *"Run the audit for <brand> India, Jan to May"* (audit runs)

**User (later):** *"Now run negation for the same account, same window"*

1. Recognize context. Confirm: "Negation for <brand> India, Jan 1 to May 26, all campaigns — CONFIRM this scope."
2. User confirms.
3. Proceed with bleeding pull (fresh — do not read audit temp files).
4. Rest of flow normal.

### Example 3 — Cancellation

**User:** *"Run negation for <brand> India, last 60 days, all campaigns"*

1. Resolve the profile for <brand> IN.
2. Bleeding pull: 145 rows → 92 qualifying after brand/ASIN exclusion.
3. Preview shown.
4. User: "Actually not now, cancel."
5. Skill treats as CANCEL. Writes cancellation-only CSV (all rows marked "cancelled by user"). No MCP writes. Recap: "0 negations added. Cancelled."

### Example 4 — Zero qualifying rows

**User:** *"Run negation for <brand> India, last 7 days"*

1. Resolve profile.
2. Bleeding pull: 0 rows.
3. Stop. Message: "No bleeding search terms found for <brand> in the last 7 days meeting the negation criteria (Sponsored Products, ≥ N clicks, 0 orders, excluding branded and ASIN terms)."
4. No CSV written. No writes attempted.

---

## Versioning

**v1** — May 27 2026. Initial release. Executes search-term negation based on Hector bleeding pillar logic. Ungrouped, SP-only, negative exact match, scoped to the specific ad group per row. Excludes branded and ASIN-pattern terms. Preview + CONFIRM gate. Change-log CSV every run. Runs independently of the audit skill.

**Planned v1.1+:**
- Add "undo" companion skill that reads a change-log CSV and reverses the negations
- Add optional dry-run mode that produces the CSV without executing writes
- Integrate with future `audit_get_bleeders` composite endpoint when tech team ships it
- Support batching by tool if `negative_keyword_tools` exposes a bulk-add mode

