Ten Gates or It Doesn't Post: The Autonomous Engagement Engine

Class #82 · DDS Vibe Academy · Free

Frontier Advanced 75 min Mastery stage 2026-08-17
Quick Answer

A fail-closed safety gate is a local function that blocks every AI-generated social reply containing an emoji, a hashtag, a hype word, a bot phrase, or any of six other violations. Zero violations required, not a threshold. The gate runs in under 5 milliseconds per candidate (MEASURED) and sits between draft generation and the posting stage so no draft reaches a live platform without passing all ten checks.

Key Takeaways
  1. Zero violations, not a score. The gate is fail-closed: one violation and the draft is rejected. No scoring threshold lets a bad reply through on a good day.
  2. Under 5 ms per candidate (MEASURED). The gate is string matching, not model inference. It adds zero perceptible latency.
  3. Gemini primary, Ollama fallback. Four Gemini model IDs are tried in order. On any failure, the system falls back to the best locally installed Ollama model. Draft latency: 1.2-2.5 s Gemini, 3.8-6.2 s Ollama RTX 3060 (MEASURED).
  4. Zero focus displacement (MEASURED). window.moveTo(-32000, -32000) pushes the Chrome automation window off every monitor. The architect never loses the active window.
  5. AccountGuard throws, never posts. Identity is verified per platform before any reply. Wrong account = exception, not a post.
  6. PID locks survive crashes. process.kill(pid, 0) checks liveness. A 15-minute stale timeout reclaims dead locks. No duplicate runs, no interleaved JSON writes.
  7. 14 replies per day across three platforms. X: 6, Bluesky: 4, LinkedIn: 4 (MEASURED). The system skips far more than it sends.

Module 00: The Thesis

An engagement agent that wanders, speaks like a bot, or steals window focus from the architect is worse than useless. It is a brand liability and a productivity breaker. Autonomy requires zero bot phrases, zero user disruption, and deterministic fail-closed gates.

This class builds an autonomous social engagement engine that discovers relevant posts on X, Bluesky, and LinkedIn, drafts replies using Gemini or a local Ollama model, runs every draft through a ten-point fail-closed safety gate, and posts only what passes with zero violations. The whole pipeline runs on a solo architect's workstation.

Two failure modes make an engagement agent dangerous. The first is sounding wrong. If the agent uses emoji, hashtags, hype words, canned slogans, or bot intro phrases like "great post" or "thanks for sharing," the reply advertises itself as automated. The brand takes a credibility hit on every such reply. Modules 02 and 06 eliminate this by banning specific phrases at both the gate level and the persona-spec level.

The second failure mode is behaving intrusively. If the automation raises a Chrome window while the architect is typing, the workflow is destroyed. If two runs overlap and interleave writes to the same JSON file, history is corrupted. If the agent posts from the wrong account, the damage is immediate and public. Modules 04 and 05 eliminate these by moving the browser offscreen, locking on PIDs, and verifying account identity before every post.

The rest of the class is the elimination of these two failure modes, piece by piece, with the code that implements each one.

Module 01: Architecture

Six decoupled modules orchestrated by atomic PID file locks. Each stage writes a JSON artifact. Any stage can be re-run, inspected, or replaced without touching the others. A failure never leaves a half-posted reply.

The system is decoupled by design. Each stage reads a JSON file, does its work, and writes a JSON file. The contract between stages is the file format, not a function call. This means any stage can be re-run with the same input, inspected by opening the JSON in an editor, or replaced with a different implementation. A failure at any stage leaves a clean artifact on disk and nothing posted.

The six-module pipeline from discovery through proof:

StageModuleInputOutput
1engage_discover.jsPlatform feedsengage_candidates.json
2engage_draft.jsengage_candidates.jsonDrafts via Gemini or Ollama
3engage_safety_gate.jsDrafted candidatesengage_review_queue.json
4engage_reply.jsengage_review_queue.jsonChrome over CDP (port 9222)
5AccountGuardActive browser sessionIdentity verified or throw
6Post + proofOffscreen windowengage_history.json + screenshot

Each stage writes a JSON artifact. A failure at any point leaves no half-posted reply.

The pipeline starts with engage_discover.js, which scans platform feeds and produces engage_candidates.json containing roughly 150 raw candidates per run (MEASURED). These are filtered for English language and relevance. Of the 150, typically 6 to 12 survive as verified English candidates (MEASURED).

engage_draft.js takes each candidate and calls the LLM. Gemini is the primary model. If Gemini fails, the system falls back to the local Ollama server. The drafter produces a structured JSON response with a relevance score, a decision (reply or skip), a reason, and a draft.

engage_safety_gate.js runs the ten-point audit on every draft. Zero violations required. A failed audit returns the candidate to the drafter for up to 3 re-draft attempts (ASSERTED). If all three attempts fail the gate, the candidate is dropped. Passed drafts move to engage_review_queue.json.

engage_reply.js connects to Chrome over CDP on port 9222, runs AccountGuard to verify identity, moves the window offscreen, and executes the post. After posting, it verifies the DOM, takes a screenshot for proof, and writes the entry to engage_history.json.

Why decoupled: each stage writes a JSON artifact, so any stage can be re-run, inspected, or replaced without touching the others. A failure never leaves a half-posted reply.

Module 02: The Ten Gates

auditDraft() runs ten deterministic checks and requires zero violations. A threshold lets a bad reply through on a good day. A violation count of zero does not. The gate runs in under 5 milliseconds per candidate (MEASURED) because it is string matching, not model inference.

This is the headline module. The function auditDraft() in engage_safety_gate.js implements all ten checks as a fail-closed gate. Fail-closed means: zero violations required. Not "fewer than three." Not "weighted score above a threshold." Zero. A draft with one emoji is rejected the same as a draft with ten violations.

Why fail-closed rather than scored: a threshold lets a bad reply through on a good day. If the gate scores violations and the threshold is 3, then a draft with 2 hype words and 1 bot phrase passes. That draft sounds like a bot. A violation count of zero does not have this failure mode.

A failed audit returns the draft to the drafter for a maximum of 3 re-draft attempts (ASSERTED). If all three fail, the candidate is dropped entirely. No partial-pass path exists.

The ten checks

  1. Emoji check. The EMOJI_REGEX covers Unicode ranges U+1F600 through U+1FAFF plus U+2600-26FF and U+2700-27BF. These ranges include smileys, symbols, dingbats, and supplemental pictographs. Any match is a violation.
  2. Hashtag check. The regex /#\w+/ catches any word prefixed with a hash. No hashtags in replies, ever.
  3. Exclamation point check. A string includes test for the literal character. Zero exclamation points. Use periods instead.
  4. Banned hype words. The BANNED_HYPE_WORDS list: game-changer, game changer, insane, unlock, mind-blowing, 10x, huge, revolutionize, secret sauce, paradigm shift. Any match in the lowercased draft is a violation.
  5. Canned slogans. The BANNED_CANNED_SLOGANS list: "typing syntax to directing," "directing the system logic," "agents do the rest," "stop being a typist," "become the architect," "shifts from typing to directing," "vibe coding without constraints." These are stock AI-generated phrases that recur across models.
  6. Bot intro phrases. The BANNED_BOT_INTRO_PHRASES list: great post, great question, I'd be happy to, as an AI, in today's, fascinating read, thanks for sharing. The single most common tell of a bot reply is opening with "great post."
  7. Forbidden tool claims. The FORBIDDEN_TOOL_CLAIMS list: i use cursor, my cursor setup, i run stable diffusion, my midjourney prompt, in comfyui. These are tools the author does not use as a daily driver. Claiming otherwise is a factual error.
  8. Character limit. Per-platform: X under 270, Bluesky under 290, LinkedIn under 600 (ASSERTED). The draft length is compared against the platform limit. Over-length is a violation.
  9. Link discipline. If the draft contains a URL or "ddsboston.com," the gate checks whether the original post asked for a resource. If it did not, the link is unasked promotion and is a violation.
  10. Relevance score. If the model returned a score below 80 out of 100 (ASSERTED), the draft is a violation. The model evaluates relevance; the gate enforces the floor.

The complete auditDraft() function:

engage_safety_gate.js
const EMOJI_REGEX = /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u; const BANNED_HYPE_WORDS = [ 'game-changer', 'game changer', 'insane', 'unlock', 'mind-blowing', '10x', 'huge', 'revolutionize', 'secret sauce', 'paradigm shift' ]; const BANNED_CANNED_SLOGANS = [ 'typing syntax to directing', 'directing the system logic', 'agents do the rest', 'stop being a typist', 'become the architect', 'shifts from typing to directing', 'vibe coding without constraints' ]; const BANNED_BOT_INTRO_PHRASES = [ 'great post', 'great question', "i'd be happy to", 'as an ai', "in today's", 'fascinating read', 'thanks for sharing' ]; const FORBIDDEN_TOOL_CLAIMS = [ 'i use cursor', 'my cursor setup', 'i run stable diffusion', 'my midjourney prompt', 'in comfyui' ]; function auditDraft(item) { const violations = []; if (!item || !item.draft || typeof item.draft !== 'string' || item.draft.trim().length === 0) { violations.push('Draft is null or empty'); return { passed: false, violations }; } const draft = item.draft.trim(); const lowerDraft = draft.toLowerCase(); const platform = (item.platform || 'x').toLowerCase(); // 1. Emoji check if (EMOJI_REGEX.test(draft)) { violations.push('Contains emoji'); } // 2. Hashtag check if (/#\w+/.test(draft)) { violations.push('Contains hashtag'); } // 3. Exclamation point check if (draft.includes('!')) { violations.push('Contains exclamation point'); } // 4. Hype words check for (const word of BANNED_HYPE_WORDS) { if (lowerDraft.includes(word)) { violations.push('Contains banned hype word: "' + word + '"'); } } // 5. Canned slogans check for (const slogan of BANNED_CANNED_SLOGANS) { if (lowerDraft.includes(slogan)) { violations.push('Contains canned slogan: "' + slogan + '"'); } } // 6. Bot intro phrases check for (const intro of BANNED_BOT_INTRO_PHRASES) { if (lowerDraft.includes(intro)) { violations.push('Contains bot intro phrase: "' + intro + '"'); } } // 7. Forbidden tool claims check for (const claim of FORBIDDEN_TOOL_CLAIMS) { if (lowerDraft.includes(claim)) { violations.push('Contains forbidden tool claim: "' + claim + '"'); } } // 8. Character limit checks const platformLimits = { x: 270, bluesky: 290, linkedin: 600 }; const maxAllowed = platformLimits[platform] || 270; if (draft.length > maxAllowed) { violations.push('Draft exceeds ' + platform.toUpperCase() + ' limit (' + draft.length + '/' + maxAllowed + ')'); } // 9. Unasked promo/link check const hasLink = /(https?:\/\/[^\s]+|ddsboston\.com)/i.test(draft); if (hasLink) { const contextText = (item.contextText || '').toLowerCase(); const askedForResource = contextText.includes('where can i') || contextText.includes('do you have a link') || contextText.includes('any resources') || contextText.includes('recommend a class'); if (!askedForResource) { violations.push('Contains unasked promo link'); } } // 10. Relevance score check if (typeof item.score === 'number' && item.score < 80) { violations.push('Relevance score too low (' + item.score + '/100, min 80)'); } return { passed: violations.length === 0, violations }; }

Anchor fact: the gate runs in under 5 ms per candidate (MEASURED). It is string includes and regex tests against four constant arrays. No network calls, no model inference. The draft generation step is three orders of magnitude slower.

Terminal panel titled engage_safety_gate.js showing ten gate rows, nine gold PASS and gate 07 bot intro in red REJECT, with the phrase great post quoted beneath it, over the caption ten gates zero violations or it does not post

The central diagram: nine PASS, one REJECT. Gate 07 catches the bot intro phrase "great post." The candidate is dropped.

Module 03: Model Failover

Gemini is the primary model across four IDs. On any failure, the system falls back to the best locally installed Ollama model, selected at runtime by querying the tags API and walking an eight-model preference list. Hardcoding a model name is what makes a local-first system fragile.

The local Ollama server at http://127.0.0.1:11434 is shared across multiple applications and background services on the workstation. Models are installed, updated, and removed independently. Hardcoding a model name is what makes a local-first system fragile. The model that was installed yesterday may not be installed today.

getBestOllamaModel() queries the Ollama tags API at /api/tags at runtime, retrieves the list of installed models, and walks an eight-model preference list:

  1. qwen2.5-coder:14b
  2. qwen2.5-coder-14b-32k:latest
  3. qwen3-coder:30b
  4. qwen2.5:14b
  5. deepseek-r1:8b
  6. llama3.1:8b
  7. mistral-nemo:latest
  8. gemma4:latest

The first match is cached for the rest of the run. If none match, the selector falls back to whatever is installed. If the API cannot be reached, the hard default is qwen2.5-coder:14b.

callLLM — the failover circuit
async function callLLM(systemPrompt, userPrompt, forceLocal = false) { if (forceLocal) { console.log('[LLM] Force local mode requested. Calling Ollama...'); return await callOllama(systemPrompt, userPrompt); } try { return await callGemini(systemPrompt, userPrompt); } catch (err) { console.log('[Failover] Gemini unavailable (' + err.message + '). Falling back to local Ollama...'); return await callOllama(systemPrompt, userPrompt); } }

callGemini() tries four model IDs in order: gemini-2.5-pro, gemini-2.5-flash, gemini-1.5-pro, gemini-3.1-pro-preview. The response is requested as structured JSON with a defined schema. If all four fail, it throws and the failover catches it.

callOllama() includes a critical detail: JSON-fence stripping. Local models frequently wrap their JSON output in markdown code fences. The function strips leading ```json and trailing ``` before parsing. Without this, the JSON.parse fails on otherwise correct output.

Anchor facts: Gemini draft latency 1.2 to 2.5 seconds (MEASURED). Local Ollama draft latency 3.8 to 6.2 seconds on an RTX 3060 12GB (MEASURED).

Module 04: Stealth Automation

Chrome launched with --remote-debugging-port=9222 against the real profile directory. Puppeteer connects over the webSocketDebuggerUrl from /json/version, then immediately evaluates window.moveTo(-32000, -32000). Zero focus displacement (MEASURED). The automation runs in the real browser session without the architect ever seeing or losing a window.

The engagement agent needs a real browser with real cookies, real sessions, and real logged-in accounts. A headless throwaway instance has none of these. Chrome is launched with --remote-debugging-port=9222 against the user's actual profile directory.

Puppeteer connects over CDP by fetching the webSocketDebuggerUrl from http://127.0.0.1:9222/json/version and calling puppeteer.connect() with defaultViewport: null (MEASURED: this preserves the existing viewport rather than forcing a default resolution).

The focus fix

The problem: an automation that raises a window while the architect is typing destroys the session it was supposed to support. Every Puppeteer connect() brings Chrome to the foreground. Every page navigation can trigger a focus event.

The fix: immediately after connecting, evaluate window.moveTo(-32000, -32000) on the page. The coordinates are negative enough to push the window entirely off every connected monitor. Zero focus displacement (MEASURED).

connectToChrome — stealth connection
const puppeteer = require('puppeteer-core'); async function connectToChrome() { const response = await fetch('http://127.0.0.1:9222/json/version'); const data = await response.json(); const browser = await puppeteer.connect({ browserWSEndpoint: data.webSocketDebuggerUrl, defaultViewport: null }); const pages = await browser.pages(); const page = pages.length > 0 ? pages[0] : await browser.newPage(); // CRITICAL: Move browser offscreen to prevent focus theft await page.evaluate(() => { window.moveTo(-32000, -32000); }); return { browser, page }; }

The window is offscreen but alive. It still loads pages, executes JavaScript, responds to Puppeteer commands, and renders the DOM. You can bring it back with window.moveTo(100, 100) at any time. The negative coordinates are within the Win32 LONG range and are the standard technique for hiding an HWND without minimizing it.

Module 05: AccountGuard and PID Locking

AccountGuard verifies identity per platform before any post. X by handle allowlist with a switch path. Bluesky by handle equality. LinkedIn by profile-page content assertion. On failure it throws rather than posts. Posting as the wrong account is worse than not posting.

AccountGuard

Identity is verified per platform, each differently:

  • X: Reads the logged-in handle and checks it against an allowlist (ddsvibeacademy, ddsboston24). If the wrong account is active, it attempts to switch. If the switch fails, it throws.
  • Bluesky: Reads the logged-in handle and asserts exact equality to ddsboston.com. No switch path. Mismatch = throw.
  • LinkedIn: Navigates to the profile URL and asserts that the page text contains "Robert McCullock." This is a content assertion, not a handle check, because LinkedIn does not expose the logged-in handle in a predictable DOM location.

On any failure, AccountGuard throws rather than posts. The rule: posting as the wrong account is worse than not posting.

AccountGuard — per-platform identity verification
// X Handle Verification & Switcher async function verifyOrSwitchXAccount(page) { const currentHandle = await getLoggedInHandle(page); if (['ddsvibeacademy', 'ddsboston24'].includes(currentHandle)) return true; return await switchXAccount(page, 'ddsvibeacademy'); } // Bluesky Identity Verification async function verifyBlueskyAccount(page) { const loggedInHandle = await getBlueskyLoggedInHandle(page); return loggedInHandle === 'ddsboston.com'; } // LinkedIn Profile Verification async function verifyLinkedInAccount(page) { await page.goto('https://www.linkedin.com/in/robert-mccullock-8580a8140/', { waitUntil: 'domcontentloaded' }); const isRobSession = await page.evaluate(() => { const text = document.body.innerText || ''; return text.includes('Robert McCullock') && !text.toLowerCase().includes('undefined'); }); if (!isRobSession) { throw new Error('AccountGuard Violation: Active LinkedIn session is NOT Robert McCullock'); } return true; }

PID Locking

engage_lock.js prevents two failure modes: duplicate concurrent runs, and corrupted JSON history from interleaved writes.

The lock is a JSON file containing the PID and a timestamp. When a second run starts, it reads the lock, calls process.kill(pid, 0) to test whether the owning process is still alive. Signal zero does not terminate anything. If the process exists, the call succeeds silently and the second run exits with a lock collision. If the process is gone, the call throws ESRCH and the lock is treated as orphaned.

A stale timeout of 15 minutes (ASSERTED) reclaims any lock older than that, regardless of PID status. A healthy run completes in under 5 minutes.

Release is wired to both the exit event and SIGINT (Ctrl+C). This ensures the lock file is cleaned up on normal exit, on manual interrupt, and on crash recovery via the stale timeout.

engage_lock.js — PID-verified atomic locking
const fs = require('fs'); const path = require('path'); const LOCK_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes max lock age function acquireLock(lockName) { const lockFile = path.join(__dirname, lockName + '.lock'); if (fs.existsSync(lockFile)) { try { const data = JSON.parse(fs.readFileSync(lockFile, 'utf8')); const isStale = (Date.now() - data.timestamp) > LOCK_TIMEOUT_MS; let isProcessAlive = false; try { process.kill(data.pid, 0); // Signal 0: check if PID exists isProcessAlive = true; } catch (e) { isProcessAlive = false; } if (!isProcessAlive || isStale) { console.log('[LockGuard] Removing stale lock (PID: ' + data.pid + ')'); fs.unlinkSync(lockFile); } else { console.error('[LockGuard] Lock collision held by PID ' + data.pid); process.exit(1); } } catch (err) { fs.unlinkSync(lockFile); } } const lockData = { pid: process.pid, timestamp: Date.now(), created: new Date().toISOString() }; fs.writeFileSync(lockFile, JSON.stringify(lockData, null, 2), 'utf8'); console.log('[LockGuard] Acquired lock: ' + lockFile); process.on('exit', () => releaseLock(lockName)); process.on('SIGINT', () => { releaseLock(lockName); process.exit(0); }); } function releaseLock(lockName) { const lockFile = path.join(__dirname, lockName + '.lock'); if (fs.existsSync(lockFile)) { try { fs.unlinkSync(lockFile); } catch (e) {} } }

Module 06: The Persona Spec

The complete system prompt injected into engage_draft.js. Twelve numbered rules. The specificity is the lesson: accuracy over helpfulness, no filler advice, correct tool-stack claims, one sharp diagnostic question instead of a guess, silence over a shaky answer.

The system prompt below is the complete, unmodified instruction injected into engage_draft.js. It is reproduced verbatim as a teaching artifact. The twelve rules are the behavioral contract the model must follow. The safety gate enforces most of them mechanically, but rules 1 (accuracy over helpfulness), 5 (diagnostic question over guess), and 6 (default to silence) are judgment calls only the model can make.

System prompt (verbatim):

You are Robert ("Rob") McCullock. You are evaluating social media posts/comments to decide whether to reply, and drafting replies when appropriate.

Core branding and voice reference: Rob McCullock, 46, Boston, AI Systems Architect and solo founder of Design Delight Studio. Voice: warm, plain-spoken, precise, high-signal. Direct short sentences. Zero emojis. Zero hashtags. Zero exclamation marks in public replies. Zero hype words.

  1. Accuracy over helpfulness: only give advice you are confident is technically correct. Do not make guesses or suggest generic/implausible fixes.
  2. Absolutely no exclamation points in the draft. Zero. Use periods instead.
  3. Prohibit filler advice: do not write filler like "restart your agent," "ensure your specs/hardware are good," "check your connection/network," "explore free tier options." Be precise and technically accurate, or ask a sharp diagnostic question, or skip.
  4. Specific technical stack correctness: Rob codes with Claude Code inside Antigravity IDE. He runs autonomous agents locally using Ollama (qwen2.5-coder:14b on RTX 3060 12GB). For cloud orchestration he uses Gemini. For generative media he uses cloud Imagen 4.0 and Veo 3.1. Rob does NOT use Cursor as his daily driver. Never claim "I use Cursor." Never claim Rob runs or uses Stable Diffusion, Midjourney, ComfyUI, or any local image generation.
  5. If you are not sure of a specific technical fix, you must either (a) share what you actually did/tried as a concrete personal-experience anecdote, or (b) ask exactly one sharp diagnostic question. Never pad a reply with generic, plausible-sounding tips.
  6. Minimum relevance score is 80/100. If you would be guessing or if the topic is outside your depth, you must skip (decision: "skip," score below 80, draft: null). Default to silence over a shaky answer.
  7. Voice and style: warm, plain-spoken, precise. Short sentences. No emojis. No hashtags. No hype words. Keep replies to 1-4 sentences.
  8. Value only: never promote or drop links unless the post explicitly asks for a link/resource.
  9. Score relevance and safety 0-100. Skip if the post is political, sensitive, controversial, religious, or culture-war related, or outside your core expertise (AI coding, local models, Puppeteer, SEO/GEO, print-on-demand).
  10. Strict character limits: X under 270, Bluesky under 290, LinkedIn under 600.
  11. Strict English only: Robert only speaks and reads English. If the post is in any other language, skip it with score 0.
  12. Strict anti-repetition / no canned slogans: never use stock phrases. Every draft must be uniquely worded and directly address the specific scenario.

The specificity of this prompt is the lesson. "Accuracy over helpfulness" is not a suggestion. "One sharp diagnostic question instead of a guess" is not a style preference. These are architectural constraints that prevent the agent from producing the kind of reply that sounds helpful and is technically wrong.

Module 07: What It Measures

The honest numbers. Every figure is graded MEASURED, CALCULATED, or ASSERTED. The system replies fourteen times a day at most and skips far more than it sends. That is the design, not a limitation.
MetricValueGrade
Discovery candidates per run150 raw, 6-12 verified EnglishMEASURED
Gate latency per candidateUnder 5 msMEASURED
Draft latency (Gemini)1.2-2.5 sMEASURED
Draft latency (local Ollama, RTX 3060 12GB)3.8-6.2 sMEASURED
Daily capsX: 6, Bluesky: 4, LinkedIn: 4MEASURED
Character limitsX under 270, Bluesky under 290, LinkedIn under 600ASSERTED
Score floor80 / 100ASSERTED
Lock timeout15 minutesASSERTED
Re-draft cap3 attemptsASSERTED
Focus displacementZeroMEASURED

The system replies fourteen times a day at most (6 + 4 + 4) across three platforms, and skips far more than it sends. Of 150 raw discovery candidates per run, 6 to 12 survive the English and relevance filters. The 80/100 score floor drops most of those. The safety gate catches the rest.

That is the design, not a limitation. A system that replies to everything it finds is not autonomous. It is a spam bot with a better vocabulary.

Frequently Asked Questions

A fail-closed gate blocks every draft that does not pass all ten checks. Zero violations required. There is no scoring threshold and no "close enough." A draft with one emoji, one hashtag, or one banned phrase is rejected the same as a draft with ten. The gate runs in under 5 milliseconds per candidate (MEASURED) and costs nothing in API calls because it is a local function, not a model call.

Ban the phrases that mark bot output. The safety gate checks against four constant lists: BANNED_HYPE_WORDS (game-changer, insane, unlock, mind-blowing, 10x, huge, revolutionize, secret sauce, paradigm shift), BANNED_CANNED_SLOGANS (seven stock phrases), BANNED_BOT_INTRO_PHRASES (great post, great question, I'd be happy to, as an AI, in today's, fascinating read, thanks for sharing), and FORBIDDEN_TOOL_CLAIMS (five false tool attributions). Any match is a violation. The model never sees these lists. The gate enforces them after generation.

Yes. The callLLM function has a forceLocal path that routes every draft to the local Ollama server at http://127.0.0.1:11434 and never calls Gemini. Latency is 3.8 to 6.2 seconds per draft on an RTX 3060 12GB (MEASURED) instead of 1.2 to 2.5 seconds on Gemini. The safety gate, discovery, and posting stages use no API at all. The only API-dependent stage is draft generation, and the local path replaces it entirely.

The callLLM function catches the Gemini error, logs it, and falls back to the local Ollama server automatically. It tries four Gemini model IDs (gemini-2.5-pro, gemini-2.5-flash, gemini-1.5-pro, gemini-3.1-pro-preview) in order before failing over. The local model is selected at runtime by getBestOllamaModel, which queries the Ollama tags API and walks an eight-model preference list. The draft is generated locally and passes through the same safety gate. No human intervention required.

qwen2.5-coder:14b is the first choice in the preference list because it handles short-form structured JSON output reliably on 12GB VRAM. The full preference list is qwen2.5-coder:14b, qwen2.5-coder-14b-32k:latest, qwen3-coder:30b, qwen2.5:14b, deepseek-r1:8b, llama3.1:8b, mistral-nemo:latest, gemma4:latest. The selector picks the first installed model from that list. If none match, it falls back to whichever model Ollama has installed.

Immediately after connecting to Chrome over CDP, evaluate window.moveTo(-32000, -32000) on the page. The coordinates are negative enough to push the window entirely off every monitor. Zero focus displacement (MEASURED). The window stays connected and functional for DOM interaction via Puppeteer but never raises above the architect's active workspace. Without this, every automation run pulls Chrome to the foreground and interrupts whatever you are typing.

Port 9222 is the Chrome DevTools Protocol (CDP) endpoint. Launching Chrome with --remote-debugging-port=9222 exposes a WebSocket at ws://127.0.0.1:9222. Puppeteer connects to it via the webSocketDebuggerUrl from the /json/version endpoint. This lets the automation drive the real browser profile with its cookies, sessions, and logged-in accounts instead of a headless throwaway instance. The agent posts from the real account because it uses the real browser.

Yes. The window is offscreen but alive. It still loads pages, executes JavaScript, responds to Puppeteer commands, and renders the DOM. No tab crashes, no session loss. You can bring it back with window.moveTo(100, 100) at any time. The negative coordinates are within the Win32 LONG range and are the standard technique for hiding an HWND without minimizing it.

AccountGuard verifies identity per platform before any post. X: reads the logged-in handle and checks it against an allowlist (ddsvibeacademy, ddsboston24), with a switch path if the wrong account is active. Bluesky: reads the handle and asserts equality to ddsboston.com. LinkedIn: navigates to the profile URL and asserts the page text contains Robert McCullock. On any failure, AccountGuard throws rather than posts. Posting as the wrong account is worse than not posting.

The second run exits immediately. engage_lock.js writes a JSON lock file containing the PID and timestamp of the first run. The second run reads the lock, calls process.kill(pid, 0) to check whether the first process is still alive, and if it is, logs a lock collision and calls process.exit(1). No draft is generated, no post is attempted. This prevents duplicate replies and corrupted JSON history from interleaved file writes.

process.kill(pid, 0) sends signal zero to the process. Signal zero does not terminate anything. If the process exists, the call succeeds silently. If the process is gone, it throws ESRCH. The lock file also carries a timestamp. If the lock is older than 15 minutes (ASSERTED), it is treated as stale and reclaimed regardless of PID status, because a healthy run completes in under 5 minutes.

The daily caps are X: 6, Bluesky: 4, LinkedIn: 4 (MEASURED). Those are the caps this system enforces. The reasoning: enough to be present, few enough that each reply is considered. The system skips far more candidates than it replies to. Of 150 raw discovery candidates per run, 6 to 12 pass the English and relevance filters, and the 80/100 score floor drops most of those. Fourteen replies per day across three platforms is the design, not a limitation.

A floor of 60 would let the agent reply to posts where it has marginal relevance. A marginal reply reads as noise. The 80 floor means the agent only replies when the topic is squarely in its domain (AI coding, local models, Puppeteer, SEO/GEO, print-on-demand) and it has something concrete to say. The system defaults to silence over a shaky answer (ASSERTED, rule 6 of the persona spec). More skips is better than more bad replies.

Rule 11 of the persona spec: if the post or comment is in any language other than English, the model must skip it with score 0, reason Non-English post, and a null draft. This is a hard skip in the system prompt, not a gate check, because language detection is a judgment call the model handles at scoring time. The gate would need an NLP classifier to replicate it, and the system prompt instruction has been reliable in production.

Exclamation points are the cheapest tell that a reply is machine-generated. Human experts writing short technical replies almost never use them. The persona spec (rule 2) bans them entirely: zero exclamation points, use periods instead. The safety gate (check 3) enforces this as a hard violation. One exclamation point in a draft and the draft is rejected. This single rule eliminates more bot-sounding output than any other check in the matrix.

Under 5 milliseconds per candidate (MEASURED). The gate is a local JavaScript function. It runs string includes checks against four constant arrays, a regex test for emoji, a regex test for hashtags, a character-count comparison, and a link-context check. No network calls, no model inference. It adds effectively zero latency to the pipeline. The draft generation step (1.2 to 6.2 seconds depending on model) is three orders of magnitude slower.

Call auditDraft directly with a test object. Pass it an item with a draft string, a platform string, and a score number. It returns an object with a passed boolean and a violations array. If passed is false, the violations array tells you exactly which checks failed. You can run this in a Node REPL or a test script. The gate function has no side effects, no state, and no dependency on any other module. It is a pure function.

Yes. Every platform's terms of service prohibit or restrict automated posting. X's automation policy requires that automated replies be clearly relevant and not spammy. Bluesky's policies are still evolving. LinkedIn is the strictest and has suspended accounts for automation before. This system mitigates risk with low daily caps (6/4/4), a high relevance floor (80/100), persona-accurate voice, and no promotional links unless asked. But the risk is not zero. If an account matters to you, understand that you are accepting that risk.

Bottom Line

An autonomous engagement engine is not an automation that replies to everything it finds. It is a system of ten fail-closed gates, a model failover circuit, a stealth browser that never steals focus, and an identity guard that throws rather than posts wrong. The system replies fourteen times a day across three platforms. It skips far more than it sends. That restraint is the architecture, and the architecture is what makes it safe to run unattended.

Prerequisites: #67 The Definitive Ollama Masterclass and #46 Multi-Model Orchestration. This class is free. No signup, no email, no paywall. It is part of the DDS Vibe Academy, a free AI coding curriculum built by Robert McCullock at Design Delight Studio.