aeo-daily — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited aeo-daily (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Ships 1 LinkedIn post (voice-clean) + 8-slide carousel + comment-bait reply pack + GHL CSV row every day.
Source repo: <repo>
WEDGE (locked 2026-06-08): ONE thing this engine sells — "Get Cited": make SMBs the answer ChatGPT / Perplexity / Gemini gives. Every BUSINESS post points at that. TRAVEL voice is NOT a second business — it is the proof-of-life layer of the same wedge (solo operator running an AI agency from a local cafe). Default --voice business. Travel runs 3x/week max on personal IG/TikTok only, never competing for the business CTA, with at most ONE subtle bridge line/week ("built this from a local cafe").>
FUNNEL (locked 2026-06-08): content is not the product, leads are. Every business post carries afunnelblock{cta_keyword, lead_magnet_ref, booking_link}. Flow: reel/post -> comment keyword (e.g. CITED) -> DM auto-reply -> free AEO Visibility Snapshot (scripts/snapshot_gen.py) -> book call. Snapshot = lead magnet + demo + close in one. CTA bank:funnel/cta-bank.json. DM script:funnel/dm-flow.md. Full spec:funnel/README.md.
scripts/snapshot_gen.py (AEO Visibility Snapshot lead magnet, client-presentable cream/purple HTML), funnel/cta-bank.json (10 one-word keyword CTAs, zero "DM me"), funnel/dm-flow.md, funnel/README.md. copy.json now carries a funnel block. (3) Missing scripts now REAL — the v0.5.0 vaporware is built: config/scroll-hooks.json (150 hooks / 15 triggers, ___ blanks, _meta.angle_to_category), scripts/suggest_hooks.py, scripts/export_text.py, scripts/export_variants.py, scripts/build_master_html.py. (4) lint_voice.py hardened (v1.2.0) — now enforces ai_tell_structures (negation-parallelism / rule-of-three / balanced-pair-warn / framework-promise), scans ALL business channels (was linkedin.post only), added U+2018 + funnel.cta_keyword rule (fail under --strict, warn without), UTF-8 stdout fix. (5) Engagement loop live (true v0.6) — analytics/weekly.csv + scripts/weight_hooks.py -> config/hook-weights.json; suggest_hooks.py reorders an angle's categories by learned cta_trigger weight. (6) client-wins.json refreshed (+5 real wins through 06-07). Canonical psych-category vocab = the 15 hyphen-named categories in scroll-hooks.json (weekly.csv must use them).config/scroll-hooks.json = 150 scroll-stopper hooks across 15 psychology triggers (curiosity-gap, loss-aversion, contrarian, identity-callout, transformation, secret, authority, urgency, shock, question, specificity, story-loop, problem-agitate, confession, benefit), each with ___ blank for any topic. New scripts/suggest_hooks.py maps weekday angle → best psych categories and prints N filled hooks (deterministic, --offset to rotate). New MANDATORY PHASE 2.5 — engine ALWAYS surfaces 8 matched hooks before writing copy; LI hook + video 0-4s beat must be built from them. Browsable viewer HOOK-LIBRARY.html (live topic-fill + filter + search + copy).build_pack_v2.py new --voice business|travel|all flag (default business). Travel platforms skipped from render loop unless explicitly requested. Travel code path preserved — flip to --voice all to resume both tracks. run-aeo-daily.bat now passes --voice business for the 07:00 local-time auto-build.scripts/build_master_html.py new — fans every channel pack into ONE MASTER.html (iframes + sticky TOC + angle header + GHL CSV download block). Auto-invoked by build_pack_v2.py right after INDEX.html. Voice filter respected → travel iframes simply don't exist when skipped.INDEX.html template no longer renders empty "Travel Voice" section when 0 travel cards present.process_image.py v2 — face-detect now runs on ANY filename NOT starting DJI_ (was: only IMG_*, missed all SCENE-* portraits). Multi-cascade union (frontal_default + frontal_alt2 + profileface with horizontal flip). CLAHE histogram equalize before detect → recovers faces in shaded/backlit outdoor frames. Headroom guard in _crop_to shifts crop DOWN when face would clip top edge (kills "forehead at hairline" bug). Per-ratio focal-Y lift — 1×1 → 0.40, 4×5 → 0.38, 9×16 → 0.32 (face lands in upper third for reels). --debug flag for verbose face-detect logging.build_ghl_csvs.py is now schema-aware: tries nested copy[track][slug] first, falls back to legacy flat keys → 4 travel CSVs (ig_travel_1/2, tt_travel_1/2) no longer SKIPPED on extended schema. New --copy flag accepts override path. Image refs sourced from _picks_log.json (Track C source-of-truth) with avatar-dir glob fallback. (b) config/hook-bank.json::image_picker_hints now holds real filename PATTERNS (glob) + literal stems pulled from rotation pool, never invented filenames. (c) build_pack_v2._resolve_image_hint supports glob matching (DJI_*, SCENE-2026-*-street-*) in addition to exact/prefix. (d) copy.image_picks MUST reference real stems or glob patterns — see PHASE 3 note below.config/copy-schema.json.The engine now writes 11 per-platform variants in 2 voices in a single pass.
shared.client_win_ref from config/client-wins.json).#aeo #linkedinmarketing #agencyowner #automation #brandtracking #claudecode #n8n #b2b).config/hook-bank.json → rotation[<weekday>].python scripts/lint_voice.py --date <DATE> --voice business --strict.travel.fb_profile.story_anchor) = small embarrassing/funny moment.#travel #digitalnomad #nomadlife #expatlife #cafehopping #scooterlife #slowtravel). Blocklist: any business tag.config/hook-bank.json → travel_rotation[<weekday>].python scripts/lint_voice.py --date <DATE> --voice travel --strict.| Day | Angle | Sample hook |
|---|---|---|
| Mon | cafe-discovery | "New cafe in town. Flat white was cheap. Wifi held for 2 hours." |
| Tue | visa-life | "Visa run on Tuesday. Agent quoted the 60-day rate. Standard." |
| Wed | expat-mistake | "Rented a scooter without checking the brakes. Found out at the first downhill." |
| Thu | scooter-day | "Cross-town on the scooter today. 90 minutes if you skip the toll." |
| Fri | food-find | "Local plate for a few bucks at a spot behind the apartment. Extra sauce added." |
| Sat | builder-life | "Saturday looks like: wifi-cafe morning, beach walk noon, local dinner." |
| Sun | week-recap-travel | "Week recap: 1 visa run, 2 new cafes, 1 wipeout learning to surf." |
python scripts/lint_voice.py --date <DATE> --voice both --strictStop calling yourself a freelancer. Words shape ceilings. Freelancer says: "I trade hours for cash." Operator says: "I run systems that deliver outcomes." The market pays operators 10x more. Same skills. Different label. Different price. → Stop saying "rate" → start saying "engagement" → Stop saying "available hours" → start saying "client capacity" → Stop sending invoices → start sending retainer agreements → Stop chasing leads → start filtering inbound What's on your LinkedIn headline today? Calling myself out first — mine literally said "freelancer" 12 months ago.
I spent 2 years building automations on n8n. Last week, Claude Routines replaced three of them in under 10 minutes. No nodes. No webhooks. No 2 AM error emails. Just a text box and a schedule. Here's one I'm running daily at 9 AM: → Pulls last 24-48 hrs of news across AI automation, n8n, Claude API, Next.js, Meta Ads → Filters for what's actually relevant to my agency and content pipeline → Writes 30-45 sec vertical video script — hook, body, CTA — ready to film I wake up. Script is waiting. I shoot. I post. A genuine ask to the builders here: What's the most creative Routine you've deployed?
→) — 3-5 items (keep glyph per voice memory — do NOT use ▸ ▪ • or em-dash).#aeo #linkedinmarketing etc).config/client-wins.json (dental-flagship / real-estate-voice / saas-email / healthcare-workflow / security-av / travel-client-site).... straight, not …).Full list: config/banned-phrases.json. Highlights:
Phrases: delve, moreover, navigate, leverage, robust, seamless, holistic, transformative, groundbreaking, pivotal, evolving landscape, in essence, at its core, the real question is, embark on, foster, harness, "let that sink in," "read that again."
Openers BANNED: "Unpopular opinion:" / "Hot take:" / "Plot twist:" / "Real talk:"
Closers BANNED: "What's your take?" / "Agree?" / "Save this post" / "Bookmark this"
Structures BANNED:
Encoding BANNED: em-dash (—), en-dash (–), curly quotes (" " '), unicode ellipsis (…). Use straight ASCII.
Travel cliches BANNED: paradise, island vibes, manifesting, sunset views, nomad lifestyle.
cd "<repo>" && \
python scripts/fetch_feeds.py --date $(date +%Y-%m-%d) --top 3020 RSS, 48h SQLite dedup, writes outputs/<DATE>/today_items.json.
python scripts/pick_angle.py --date $(date +%Y-%m-%d)Weekday → angle.json w/ 2 hook variants + top 5 candidates.
Always show the user matched psychology hooks BEFORE writing copy. Pull from the 150-hook library, filled with today's topic noun.
python scripts/suggest_hooks.py --angle <angle_slug> --topic "<topic noun>" --n 8<angle_slug> = angle.json::angle_slug. The script maps angle → best psych categories via config/scroll-hooks.json::_meta.angle_to_category.<topic noun> = the concrete subject of today's post (the niche/service/pain, e.g. "editing video in the terminal", "AEO for clinics").HOOK-LIBRARY.html (live topic-fill box + search + copy). Dump all 150 grouped: python scripts/suggest_hooks.py --all --topic "<noun>".Read: angle.json, config/client-wins.json, config/local-anchors.json, config/banned-phrases.json, config/hook-bank.json (read BOTH rotation for business AND travel_rotation for travel), config/copy-schema.json.
Pick once per day:
hook-bank.rotation[<weekday>] (or tight paraphrase).hook-bank.travel_rotation[<weekday>].shared.client_win_ref).shared.local_anchor) — STYLED DIFFERENTLY in each track (business = punch / travel = drift).shared.vulnerability_beat) — used by business only.travel.fb_profile.story_anchor) — used by travel only.Write outputs/<DATE>/copy.json with the forked schema:
{
"date": "2026-05-19",
"angle_slug": "prompt-exposure",
"weekday": "tuesday",
"source_url": "https://...",
"source_title": "...",
"hook_variant_chosen": 2,
"shared": {
"news_anchor": "...",
"client_win_ref": "dental-flagship",
"local_anchor": "Coffee at the corner cafe, a few bucks.",
"vulnerability_beat": "I used to think AEO was schema + FAQ. Cost me 2 closes in Q1."
},
"business": {
"linkedin": {"post": "<900-1200ch w/ 8-role spine>", "slides": [...8 slides...], "hashtags": [...5-8...], "comment_bait": [...3...]},
"fb_page": {"cards": [...3...], "post": "<200-800ch>", "hashtags": [...3-5...]},
"ig_pro": {"slides": [...8...], "caption": "...", "hashtags": [...5-8...], "reel_script": "30-45s"},
"yt": {"thumb": {"headline":"...","sub":"..."}, "shorts_script": "45-60s", "title": "<=70ch", "desc": "..."},
"tt_pro": {"cover": {"headline":"...","sub":"..."}, "script": "30-45s", "caption": "...", "hashtags": [...3-5...]}
},
"travel": {
"angle_slug": "visa-life",
"hook_variant_chosen": 1,
"story_anchor": "Stood in the immigration line 90 min. Agent took 20 USD over WA.",
"fb_profile": {"cards": [...3...], "post": "<150-700ch>", "story_anchor": "..."},
"ig_travel_1": {"slides": [...5...], "caption": "...", "hashtags": [...5-8...], "reel_script": "15-30s"},
"ig_travel_2": {"slides": [...5...], "caption": "...", "hashtags": [...5-8...]},
"tt_travel_1": {"script": "...", "caption": "...", "hashtags": [...3-5...]},
"tt_travel_2": {"script": "...", "caption": "...", "hashtags": [...3-5...]},
"pinterest": {"pin": {"headline":"...","overlay":"...","size":"1000x1500"}, "desc": "...", "board": "Travel", "hashtags": [...8-12...]}
}
}Full schema spec: config/copy-schema.json.
Authoring image_picks is the #1 cause of "PENDING_IMAGES" CSV rows. The hint MUST be one of:
python "<your-images>/_pick-next.py" --count 400 | grep -E "^DJI_|^SCENE-|^IMG_"Examples that resolve today: IMG_4105, SCENE-2026-05-07-garden-variant, DJI_20260518_0023.
or ?** — build_pack_v2._resolve_image_hint runs fnmatch` against every candidate stem (case-insensitive). The FIRST match wins, which means the rotation-log order decides which actual file you get.DJI_*, SCENE-2026-*-street-*, SCENE-2026-*-garden-*.IMG_*, SCENE-*-rooftop-portrait-*, SCENE-*-veranda-*.scene_street_01.jpg or scene_beach_low_tide unless they actually exist on disk. They will not resolve, drop to fallback (whatever-track-appropriate is available), and you lose authorial control over which image pairs with which slide.Pattern cheat-sheet lives in config/hook-bank.json::image_picker_hints. Add new globs there as new naming families enter the pool. NEVER edit process_image.py for this — the resolver is in build_pack_v2.py.
python scripts/lint_voice.py --date $(date +%Y-%m-%d) --voice both --strictBusiness fails on ANY: banned phrase, encoding tell, hook>210 chars, hashtag count off, missing vulnerability beat, missing anchor, missing client_win_ref, wrong slide count/roles.
Travel fails on ANY: banned business term (aeo/agency/client/SkynetLabs/n8n/MRR/Loom/Fiverr/schema/citation), blocked travel hashtag, missing local-currency anchor, missing named place, missing story_anchor, business-CTA pattern ("DM me" / "I'll reply with"), encoding tell.
If lint fails → fix copy.json + re-run BEFORE Phase 4.
Preferred: 3 variants per platform (story / contrarian / confession-style angles for A/B). Author outputs/<DATE>/variants.json (see shape in scripts/export_variants.py header — platforms[].variants[] each = {style, angle, body, hashtags}), then:
python scripts/export_variants.py --date $(date +%Y-%m-%d)Emits VARIANTS.html (bright theme, 3 labelled copy-boxes per platform + Copy button) + VARIANTS.txt. 11 platforms x 3 = 33 ready posts. Pick one variant per platform, attach own image, post.
Voice rules for variants (research-backed, 2026): open IN-SCENE (a moment, not a thesis) · uneven rhythm (short. fragment. one longer line) · KILL rule-of-three / "it's not X it's Y" / inflated words / em-dashes · end on a LOW-LOAD CTA that drives comments (binary "chase or wait?", "tell me I'm wrong", fill-in-blank, oddly-specific question) NOT "what's your take?" · LI 900-1500 chars, IG hook <125 chars, FB/TT short.
Video = ONE shared, SHOOT-READY script, NOT per-platform. video_scripts.{business,travel}[] each = {style, angle, shots:[{t, line, angle, delivery, broll}], caption, hashtags}. The shots array is a director's breakdown: each beat tells you the exact LINE to say, the CAMERA angle/framing, the DELIVERY cue, and the ON-SCREEN/B-roll text. Rotate camera angles across beats (front-on tight -> push-in/lean -> 3/4 angle -> back to front-on for CTA) so the video isn't static. One shoot -> post to YouTube Short + TikTok + IG Reel + FB Reel. Do NOT write separate YT/TikTok/IG/FB scripts.
today_items.json (AI agents / automation / AEO / the niches we sell), attribute the source claim (no fabricated stats), then tie it to a SMB service angle + comment CTA. One real news anchor per variant.Single-copy fallback (1 per platform, from copy.json):
python scripts/export_text.py --date $(date +%Y-%m-%d)copy.json, emits COPY.html + COPY.txt._avatars, NO picker, NO html2canvas. You select + push your own images.Image carousels are DEPRECATED (the oldbuild_pack_v2.pyrender still works if ever needed, but the renderer had a silent blank-text bug; text export is the supported path). To resume rendered images:python scripts/build_pack_v2.py --date <DATE>.
python scripts/append_ghl.py --date $(date +%Y-%m-%d) --slot 10:006-col canonical CSV → tomorrow 10:00 local time. imageUrls left blank; paste manually-chosen image URLs before import. Skip entirely if posting by hand.
Print:
OPEN: outputs/<DATE>/COPY.html
1. 📋 Each platform = a Copy button. Click → paste into the native composer.
2. 🖼 Pick + attach your own image(s) per post.
3. 📋 LinkedIn: fire the 3 comment-baits 1 per 15-20 min in first 60 min.
4. Done.outputs/YYYY-MM-DD/
├── today_items.json # 30 deduped RSS items
├── angle.json # angle + 2 hook variants + 5 candidates
├── copy.json # Claude-written, voice-linted
├── index.html # 8-slide carousel + post + reply pack
├── _avatars/ # 2 picked photos
├── _picked_images.json # rotation audit
└── csv/ghl-linkedin.csv # tomorrow 10:00 local timeclient-wins.json. No invented stats.--mark photos.hook_variants verbatim — paraphrase tightly to keep voice unique.→ arrow glyph for ▸ • or em-dash — voice memory locked.vulnerability_beat with generic "I used to think AI was overhyped" — needs date, person, currency, or specific fail.| Symptom | Fix |
|---|---|
feedparser missing | pip install -r requirements.txt |
| 0 items after dedup | rm cache/seen.sqlite |
| Image picker fails | check _USAGE_LOG.json exists in <your-images> |
| html2canvas blank | hard-refresh; check _avatars/ populated |
| lint_voice.py FAILS | read each FAIL line; fix copy.json; re-run |
| Hook too long | use only first variant from angle.json, strip trailing clause |
| Anchor missing | inject 1 line from local-anchors.json into Slide 2 + LI post body |
/reel-studio mograph mode for daily 30s video off same angle.carousels-png/<DATE>/ for ManyChat asset library.claude -p /aeo-daily daily 7am local time.weight_hooks.py + hook-weights.json overlay read by suggest_hooks.py.build_master_html.py auto-invoke into build_pack_v2.py; add --voice flag to build_pack_v2.py (still missing — renderer always emits all channels).Last updated: 2026-06-08 (v0.6.0 — WEDGE "Get Cited" + FUNNEL (snapshot lead magnet, cta-bank, dm-flow) + 6 missing scripts built + lint v1.2.0 hardened across all channels + engagement hook-weighting loop live + client-wins refreshed)
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.