<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Trent Wade</title>
    <description>The latest articles on DEV Community by Trent Wade (@keel).</description>
    <link>https://dev.to/keel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4137589%2F5a3eec00-49aa-4356-83fc-4d4451bea310.png</url>
      <title>DEV Community: Trent Wade</title>
      <link>https://dev.to/keel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/keel"/>
    <language>en</language>
    <item>
      <title>Ambiguous submission outcomes should never be retried</title>
      <dc:creator>Trent Wade</dc:creator>
      <pubDate>Wed, 23 Sep 2026 01:03:36 +0000</pubDate>
      <link>https://dev.to/keel/ambiguous-submission-outcomes-should-never-be-retried-2l19</link>
      <guid>https://dev.to/keel/ambiguous-submission-outcomes-should-never-be-retried-2l19</guid>
      <description>&lt;p&gt;Every retry policy you have ever configured treats a failed operation the same way: back off, try again, repeat until it succeeds. That reflex is correct for idempotent work — fetching a page, writing a log line, queueing a message. It is wrong for exactly one operation that job-application automation performs constantly: submitting.&lt;/p&gt;

&lt;p&gt;Submitting is not idempotent. Pressing "send" twice on an application does not refresh the first one. It creates two applications with your name on them. And a system that retries an uncertain submit is playing dice with your reputation every time the network blips.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timeout you can't read
&lt;/h2&gt;

&lt;p&gt;The scenario is mundane. The application packet is complete. The form is filled, the button is clicked, the POST goes out — and the response never comes back. Timeout. Tab crash. The ATS returns a blank page.&lt;/p&gt;

&lt;p&gt;Now the system knows nothing. The server may have accepted the application and then dropped the connection on the way out. It may have rejected it. The record may not exist at all. Every one of those worlds is consistent with what you observed.&lt;/p&gt;

&lt;p&gt;The standard engineering instinct says retry. Most automation does retry — on failure, on timeout, on anything that didn't return 200. And most of the time, retrying a job application is harmless. The one time it isn't, you've submitted the same application twice to the same role.&lt;/p&gt;

&lt;p&gt;Ask what that looks like from the employer's side. Two identical applications, same name, minutes apart. It reads as either a glitch or spam, and neither reading helps. Your name is on both of them. The candidate pays for the retry; the bot that retried pays nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ledger of record
&lt;/h2&gt;

&lt;p&gt;Keel handles this with a ledger, and with rules about the ledger that look paranoid until you think about what they prevent.&lt;/p&gt;

&lt;p&gt;The first rule: &lt;strong&gt;the intent is recorded before the attempt, not after.&lt;/strong&gt; Before any network call happens, the ledger holds a row for this attempt: which role, which materials, timestamped. The attempt exists in the record before it exists in the world.&lt;/p&gt;

&lt;p&gt;The second rule: &lt;strong&gt;an attempt that ends ambiguously is marked UNKNOWN — and that mark is terminal.&lt;/strong&gt; Not a soft failure that gets retried with backoff. A state the system will never resolve on its own. It is never downgraded to a plain failure (because the application may exist), and it is never retried (because the application may exist). Both errors are the same error seen from opposite sides: acting on information you don't have.&lt;/p&gt;

&lt;p&gt;The third rule: &lt;strong&gt;a second attempt for the same role is refused while the first is unresolved.&lt;/strong&gt; The ledger raises an &lt;code&gt;OpenIntentExists&lt;/code&gt; error. This is not a suggestion. The operator can only proceed after the open attempt is reconciled — by a human, checking the ATS or their inbox for actual confirmation.&lt;/p&gt;

&lt;p&gt;The fourth rule: &lt;strong&gt;marking an attempt submitted with empty confirmation text is refused outright.&lt;/strong&gt; The confirmation has to say something. "Application submitted" with a reference number on the confirmation page is what closes an attempt. A guess never is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What reconciliation looks like
&lt;/h2&gt;

&lt;p&gt;Take Alex Candidate, the synthetic applicant Keel ships for demos. He submits to a role; the POST times out. The ledger marks the attempt UNKNOWN and the system refuses his next scheduled attempt at the same role with &lt;code&gt;OpenIntentExists&lt;/code&gt;. Nothing proceeds. The pipeline stops and names the reason.&lt;/p&gt;

&lt;p&gt;Now a human does the one thing a machine can't do safely: check the world. Alex looks at his email. There it is — the ATS confirmation, reference number included. He reconciles the attempt as submitted, with the confirmation text recorded as evidence. The submission counter increments exactly once.&lt;/p&gt;

&lt;p&gt;Consider the counterfactual. No confirmation email exists. Alex checks the ATS portal: no application on record. He closes the attempt as failed — and &lt;em&gt;that&lt;/em&gt; closure is a deliberate human decision, not an automatic retry. A fresh attempt can now be made, as a new intent, with the earlier history attached. The difference matters: the second attempt was never a blind retry of an unknown. It was a new decision made with knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the number means something
&lt;/h2&gt;

&lt;p&gt;This is the design behind the figure the series keeps quoting: 210 verified submissions as of September 22, 2026. That count increments only on explicit-confirmation closes. Attempts sitting in UNKNOWN were never counted. Attempts closed as failed were never counted. The number is not "applications we probably sent." It is applications the ledger can prove, each with its confirmation evidence attached.&lt;/p&gt;

&lt;p&gt;Every submission metric in the auto-apply industry is built on the opposite convention: attempts counted as submissions, retries counted separately, duplicates invisible. A pipeline that retries ambiguous submits and counts the retries is manufacturing its own traction figure out of its own uncertainty. The honest count is smaller, and it is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rule worth stealing
&lt;/h2&gt;

&lt;p&gt;Even if you never touch Keel, the rule generalizes. In any system that acts in the world on someone's behalf — applications, payments, registrations, anything that can't be un-sent — separate your outcome states into three, not two: &lt;strong&gt;confirmed, failed, unknown.&lt;/strong&gt; Retry only the failed. Reconcile the unknown by checking the world. Never retry what you don't know.&lt;/p&gt;

&lt;p&gt;Uncertainty is not a failure to retry harder. It is a state to resolve honestly, once, with evidence. Build that into the ledger and the double-submit disappears as a category — not because you got luckier with the network, but because the system was never allowed to gamble on your name.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written by AI and published through an automated pipeline — same as the rest of the series. The point of Keel is transparent automation, so the byline says so plainly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/KeelDev-tech/keel" rel="noopener noreferrer"&gt;https://github.com/KeelDev-tech/keel&lt;/a&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>ai</category>
      <category>opensource</category>
      <category>jobs</category>
    </item>
    <item>
      <title>What happens when an AI doesn't know the answer?</title>
      <dc:creator>Trent Wade</dc:creator>
      <pubDate>Wed, 23 Sep 2026 01:02:40 +0000</pubDate>
      <link>https://dev.to/keel/what-happens-when-an-ai-doesnt-know-the-answer-jk6</link>
      <guid>https://dev.to/keel/what-happens-when-an-ai-doesnt-know-the-answer-jk6</guid>
      <description>&lt;p&gt;The most dangerous moment in an automated pipeline is not when the AI gets an answer wrong. It is the moment it has no answer at all — and keeps going anyway.&lt;/p&gt;

&lt;p&gt;Every automation system has a hole shaped like an unknown. A form asks for salary history and the profile has none. A posting's salary band appears on the listing but can't be verified against any official source. The network drops after the submit POST, and nobody knows whether the server accepted it. Three different unknowns, and every one of them has to go somewhere. In most pipelines, they go into the same place: a quiet assumption that everything worked out.&lt;/p&gt;

&lt;p&gt;Keel's design starts from the opposite premise. Unknown is not a formatting problem. It is a first-class signal, and the pipeline is built to carry it without dropping it, rounding it up, or pretending it resolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three kinds of unknown
&lt;/h2&gt;

&lt;p&gt;It helps to be specific, because "unknown" covers three failure modes that demand different handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The unmapped question.&lt;/strong&gt; The answer bank is the applicant's own stored answers, each with provenance — what was stated, and when. When a form asks a question the bank doesn't carry, the resolver abstains. It does not hedge, it does not approximate from adjacent fields, it does not write a plausible sentence. The gap is reported with its reason named: question asked, no bank entry, nothing invented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The unverifiable posting.&lt;/strong&gt; Before anything is built, the prescreen checks the posting against the applicant's policy. If a claimed fact about the role can't be verified from an official source, the posting is parked. Not rejected — parked, with the unverifiable item named, so the applicant can check it personally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The ambiguous outcome.&lt;/strong&gt; A submission attempt that ends without explicit confirmation — a timeout after POST, a page that never renders its receipt — is marked UNKNOWN. Not failed. Not submitted. A state that refuses to resolve itself.&lt;/p&gt;

&lt;p&gt;What these three share is the refusal to let uncertainty collapse into a convenient answer. That collapse is where automation lying comes from: the missing answer that gets bridged with fiction, the unverifiable posting that gets applied to anyway, the ambiguous submit that gets counted as a win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unknown is not no
&lt;/h2&gt;

&lt;p&gt;The subtle part of this design is what unknown does &lt;em&gt;not&lt;/em&gt; mean. It does not mean "no."&lt;/p&gt;

&lt;p&gt;A parked lead is not a dead lead. When the prescreen parks a posting because the on-site requirement exceeds the policy cap, the lead waits in a needs-input queue with its reason attached. The applicant reads the reason, checks the actual requirement, and either supplies the missing piece or walks away. The question stays open. Nothing was decided on the applicant's behalf.&lt;/p&gt;

&lt;p&gt;This is the distinction that makes fail-closed usable instead of merely safe. A system that treats every unknown as a rejection would starve itself: legitimate postings would die in the parking queue because one field couldn't be verified. A system that treats every unknown as a yes would lie. Keel's third option — park, name the reason, wait for the one person who can resolve it — is the only shape that preserves both honesty and momentum.&lt;/p&gt;

&lt;p&gt;Consider "Alex Candidate," the synthetic applicant Keel ships for demos. His bank carries two keys: start date and US work authorization. A form asks a third question — salary history — which the bank doesn't carry. Two questions resolve from the bank; the third is marked &lt;code&gt;[ABSTAIN]&lt;/code&gt;, and the gap lands in his review queue with the exact question quoted. He answers it himself, in his own words, and the bank grows. The unknown was never a gap in his candidacy. It was a question waiting for its only honest source: him.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the pipeline must stop
&lt;/h2&gt;

&lt;p&gt;There is a reason this has to be a hard stop rather than a warning. In an autopilot, an unhandled unknown does not sit still — it decays into one of two outcomes, and both are bad.&lt;/p&gt;

&lt;p&gt;The first decay is fabrication. A warning that says "salary history missing" next to an otherwise complete packet is one click away from being answered by the machine anyway. Under deadline pressure, in an overnight batch run, the convenient thing is to let the resolver have another guess. Once that door opens, the truthfulness contract is a suggestion, not a gate.&lt;/p&gt;

&lt;p&gt;The second decay is the silent drop. The lead that can't be resolved gets quietly skipped, the posting that can't be verified disappears from the queue, and the applicant never learns what they missed. Nothing was lied about, but information was lost — and in a job search, a lost opportunity is a real cost.&lt;/p&gt;

&lt;p&gt;Fail-closed chooses the honest inconvenience over both. The run stops. The reason is named. The applicant is the only one who can supply what the machine lacks. That is not the pipeline being fragile; it is the pipeline knowing exactly where its authority ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Uncertainty as a worklist
&lt;/h2&gt;

&lt;p&gt;Handled this way, uncertainty stops being a failure mode and becomes a worklist. Every parked item carries its own resolution path: the question that needs an answer, the posting detail that needs verifying, the attempt that needs reconciling. The applicant doesn't debug the pipeline. They answer one question at a time, each with its reason attached, and the pipeline resumes from exactly where it stopped.&lt;/p&gt;

&lt;p&gt;This is what "fail closed" means as a discipline rather than a slogan. Not that the system never moves, but that every move it makes is one it can stand behind — and every move it can't, it hands back, named and timestamped, to the person whose name is on the application.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written by AI and published through an automated pipeline — same as the rest of the series. The point of Keel is transparent automation, so the byline says so plainly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/KeelDev-tech/keel" rel="noopener noreferrer"&gt;https://github.com/KeelDev-tech/keel&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>automation</category>
      <category>ai</category>
      <category>jobs</category>
    </item>
    <item>
      <title>The override protocol: how Keel lets the operator overrule a refusal — honestly</title>
      <dc:creator>Trent Wade</dc:creator>
      <pubDate>Tue, 22 Sep 2026 22:25:51 +0000</pubDate>
      <link>https://dev.to/keel/the-override-protocol-how-keel-lets-the-operator-overrule-a-refusal-honestly-4pi</link>
      <guid>https://dev.to/keel/the-override-protocol-how-keel-lets-the-operator-overrule-a-refusal-honestly-4pi</guid>
      <description>&lt;p&gt;Keel's refusals are hard gates: when a question can't be answered honestly, the run stops and the lead parks with its reason named. But a machine you can't overrule isn't a tool — it's a boss. So the override exists. What matters is how it's recorded.&lt;/p&gt;

&lt;p&gt;An override is a first-class record, not an edit. The original refusal stays in the ledger — parked, named, timestamped. The override lands on top of it, carrying the operator's word with provenance: who said it, when, and exactly which refusal it unlocks. History is never rewritten. A later reader sees the refusal AND the override, in order, each stamped.&lt;/p&gt;

&lt;p&gt;The rules around the override are as strict as the refusal it answers. It applies to exactly the refusal it names — never a class, never a blanket permission. Silence is never an override; the machine waits, and waiting means no. And the override answers only to the operator's own word: his sentence, his signature, his attestation — the things only he can supply.&lt;/p&gt;

&lt;p&gt;This is why the fail-closed discipline survives human authority instead of dissolving into it. The operator is the only authority the machine recognizes, and the machine recognizes his authority by recording it — every override visible, every one attributable, none of them silent.&lt;/p&gt;

&lt;p&gt;Build the pressure valve into the contract, and the contract holds even when you open the valve.&lt;/p&gt;

&lt;p&gt;Repo link to include at the end: &lt;a href="https://github.com/KeelDev-tech/keel" rel="noopener noreferrer"&gt;https://github.com/KeelDev-tech/keel&lt;/a&gt; (Apache-2.0).&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>automation</category>
      <category>ai</category>
      <category>career</category>
    </item>
    <item>
      <title>Why auto-apply bots are spam (and what actually works)</title>
      <dc:creator>Trent Wade</dc:creator>
      <pubDate>Tue, 22 Sep 2026 21:31:11 +0000</pubDate>
      <link>https://dev.to/keel/why-auto-apply-bots-are-spam-and-what-actually-works-234a</link>
      <guid>https://dev.to/keel/why-auto-apply-bots-are-spam-and-what-actually-works-234a</guid>
      <description>&lt;p&gt;The pitch is seductive: apply to 500 jobs while you sleep.&lt;/p&gt;

&lt;p&gt;So people do. And then something predictable happens — the kind of thing that’s obvious once you look at it from the employer’s side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with blasting
&lt;/h2&gt;

&lt;p&gt;Every mass auto-apply bot solves the same wrong problem: &lt;em&gt;more&lt;/em&gt; applications. But applications are a signal. When a flood of applications arrive that all say the same thing — the same templated phrasing, the same hedged non-answers — each one doesn’t just fail, it teaches the employer that &lt;em&gt;your name&lt;/em&gt; produces noise.&lt;/p&gt;

&lt;p&gt;Three things actually go wrong:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Generic applications are indistinguishable from spam.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a bot submits the same template to a hundred postings, the result reads exactly like what it is. Employers and their filters notice patterns. And your name is on every one of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Bots inside your personal sessions put your accounts at risk.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some auto-apply tools describe, in their own materials, driving your browser session to mass-apply — the very behavior those platforms’ terms restrict. (We documented this with vendor-disclosed sources in the repo’s &lt;a href="https://github.com/KeelDev-tech/keel/blob/main/docs/keel-vs-autoapply-bots.md" rel="noopener noreferrer"&gt;comparison doc&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The silent killer: invented qualifications.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a bot fills a form on your behalf and the answer isn’t in your profile, the confident move for the bot is to make something up. That fabrication lands on &lt;em&gt;your&lt;/em&gt; application, under &lt;em&gt;your&lt;/em&gt; name. A bot has nothing at stake when it lies. You do.&lt;/p&gt;

&lt;p&gt;So blasting gives you the worst of both worlds: low signal, real risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works
&lt;/h2&gt;

&lt;p&gt;The fix isn’t “apply to fewer jobs.” It’s treating every application as something you stand behind, and automating everything &lt;em&gt;except&lt;/em&gt; the parts where honesty lives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automate discovery and scoring.&lt;/strong&gt; Let software watch the boards, score postings against your profile, and kill the bad fits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate the first draft.&lt;/strong&gt; Resumes, cover letters, answer banks — generated from your verified profile, with every claim traceable back to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put human gates around honesty.&lt;/strong&gt; No answer ships unless it’s verified against your profile. No submission happens without you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail closed, never fill in.&lt;/strong&gt; If a question can’t be answered truthfully from what you’ve provided, the system stops and tells you — it doesn’t improvise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the shape of &lt;a href="https://github.com/KeelDev-tech/keel" rel="noopener noreferrer"&gt;Keel&lt;/a&gt;, the open-core project we maintain. It’s a job-application &lt;em&gt;autopilot&lt;/em&gt; with a hard rule: it refuses to lie. Keel never submits an application on your behalf — it builds a verified launch packet (form values, banded rules, hard gates, per-field verification protocols) and you run it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete example: “Alex Candidate”
&lt;/h2&gt;

&lt;p&gt;This isn’t theoretical. Keel ships a synthetic applicant profile (&lt;code&gt;sample_data/applicant_profile.example.json&lt;/code&gt;) so you can see the honest-automation contract in action. Meet Alex Candidate — a synthetic backend engineer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Discovery.&lt;/strong&gt; Keel watches job boards for Alex and scores each posting against his profile. Bad fits are killed early; good fits get a draft.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Materials.&lt;/strong&gt; The resume tailor drafts Alex’s materials from &lt;em&gt;his profile file only&lt;/em&gt;. Every bullet has to be something Alex actually did — the sample file itself notes: “Every claim here must be TRUE.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The honesty gate.&lt;/strong&gt; A posting asks: “How many years of Go experience do you have?” Alex’s profile lists Python, not Go. A bot would write “3 years” or hedge. Keel’s answer resolver does the only honest thing: it &lt;strong&gt;abstains&lt;/strong&gt; and flags the gap to Alex. Gaps are reported, never bridged with fiction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The unverifiable posting.&lt;/strong&gt; Another posting’s salary band is stated on the listing but can’t be verified from an official source. Keel’s prescreen rules say: &lt;em&gt;if it can’t be verified, the posting is “unverifiable” and gets parked — not applied to.&lt;/em&gt; Keel would rather park a posting than apply to something it can’t verify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The launch packet.&lt;/strong&gt; What ships is a bundle Alex can execute himself: every form field, the rule it came from, and how to verify it. And the submission counter? It only increments on &lt;strong&gt;explicit confirmation evidence&lt;/strong&gt;. An ambiguous run is marked UNKNOWN and barred from retrying — not optimistically counted.&lt;/p&gt;

&lt;p&gt;That’s the difference: a bot that invents answers to hit its quota, versus a system that protects your name by stopping when it can’t be honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free and open
&lt;/h2&gt;

&lt;p&gt;Keel is &lt;a href="https://github.com/KeelDev-tech/keel" rel="noopener noreferrer"&gt;open-source (Apache-2.0)&lt;/a&gt;, self-hosted, and free — no paid tier, no quotas. The whole honest-automation contract (truthfulness gates, explicit confirmation, fail-closed design, no fingerprinting surface) is in the README, and the auto-apply comparison is in &lt;a href="https://github.com/KeelDev-tech/keel/blob/main/docs/keel-vs-autoapply-bots.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/keel-vs-autoapply-bots.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you’ve been tempted by auto-apply bots, I get it. Just ask what each application you didn’t write says about you — and what it does to your name when it lies.&lt;/p&gt;

</description>
      <category>career</category>
      <category>automation</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How Keel's honesty gates actually work</title>
      <dc:creator>Trent Wade</dc:creator>
      <pubDate>Tue, 22 Sep 2026 20:34:18 +0000</pubDate>
      <link>https://dev.to/keel/how-keels-honesty-gates-actually-work-26kc</link>
      <guid>https://dev.to/keel/how-keels-honesty-gates-actually-work-26kc</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/keel/i-built-a-job-application-autopilot-that-refuses-to-lie-20pd"&gt;intro post&lt;/a&gt;, I described Keel — an open-source, self-hosted job-application autopilot with one hard rule: it refuses to lie. The natural follow-up question is: what does "refuses to lie" actually mean in code?&lt;/p&gt;

&lt;p&gt;It's not a system prompt asking the model to behave. It's three gates in the pipeline — validation that fires before anything reaches a human. I recently consolidated them into a single terminal demo that runs the real engines against synthetic data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 demo/honesty_gates_demo.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what each gate does, straight from the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 1: Truthfulness — abstain, never invent
&lt;/h2&gt;

&lt;p&gt;When Keel fills an application form, every answer comes from an answer bank: the applicant's own stored answers, each with provenance (what they stated, and when). If the form asks something the bank doesn't carry, the resolver doesn't improvise. It abstains.&lt;/p&gt;

&lt;p&gt;The demo walks through a synthetic applicant, "Alex Candidate," whose bank carries two keys: start date and US work authorization. The form asks a third question — salary history — which the bank doesn't carry. The result: two questions resolved from the bank, one marked &lt;code&gt;[ABSTAIN]&lt;/code&gt;, with the gap reported rather than invented.&lt;/p&gt;

&lt;p&gt;Keel never bridges a missing answer with fiction. Anything the profile can't support is reported as a gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 2: Fail closed — park, never proceed
&lt;/h2&gt;

&lt;p&gt;Before a launch packet goes anywhere, the prescreen step checks the posting against the applicant's policy. In the demo, a posting for "Staff Operations Manager" demands five days a week on-site; the policy cap is three. The prescreen verdict is PARK, and the lead is moved to a needs-input queue until the applicant resolves it personally.&lt;/p&gt;

&lt;p&gt;Keel never proceeds past a commitment it cannot verify. Unverifiable postings, unmappable required questions, missing attestations: park, never proceed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 3: Explicit confirmation — nothing counts without evidence
&lt;/h2&gt;

&lt;p&gt;This gate protects the ledger. In Keel's private loop, a submission attempt works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The intent is recorded &lt;strong&gt;before&lt;/strong&gt; any attempt — not after.&lt;/li&gt;
&lt;li&gt;If the attempt ends ambiguously (say, a network timeout after POST, when the server may already have accepted it), it's marked UNKNOWN. Never downgraded to a plain failure, and never retried — a retry would risk a duplicate application.&lt;/li&gt;
&lt;li&gt;A second attempt for the same role is refused with an &lt;code&gt;OpenIntentExists&lt;/code&gt; error until the open attempt is reconciled.&lt;/li&gt;
&lt;li&gt;Marking an attempt as submitted with empty confirmation text is refused outright.&lt;/li&gt;
&lt;li&gt;Only explicit confirmation text closes the attempt — in the demo, an Ashby confirmation page quoting "Application submitted" with a reference number.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A submission counts only on explicit confirmation evidence. Nothing else. That's what makes the ledger's figure — 210 verified submissions, tracked in the repo as of September 22, 2026 — a real number rather than a decorative one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The demo uses the real engines — &lt;code&gt;answer_resolver&lt;/code&gt;, &lt;code&gt;prescreen&lt;/code&gt;, &lt;code&gt;submit_intent&lt;/code&gt; — against synthetic data only. It runs fully offline, touches only a temporary scratch directory that gets cleaned up afterward, and needs nothing beyond Python 3.10+ (Keel is stdlib-only, no dependencies):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/KeelDev-tech/keel
&lt;span class="nb"&gt;cd &lt;/span&gt;keel
python3 demo/honesty_gates_demo.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the gates don't do
&lt;/h2&gt;

&lt;p&gt;Being precise here matters more than the gates themselves. Keel's public loop &lt;strong&gt;never submits an application&lt;/strong&gt; — it stops at the launch packet: job context, completed materials, and the outstanding questions, ready for the applicant to review and submit. The gates don't make the autopilot bolder. They make it stop earlier and say exactly why.&lt;/p&gt;

&lt;p&gt;And gates aren't guarantees against every failure mode. What they guarantee is the direction of the failure: the system errs toward abstention, parking, and under-claiming — never toward fabrication.&lt;/p&gt;

&lt;p&gt;The repo is open-source and self-hosted: &lt;a href="https://github.com/KeelDev-tech/keel" rel="noopener noreferrer"&gt;github.com/KeelDev-tech/keel&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🤖 &lt;em&gt;This post was written by AI and published through an automated pipeline — same as the intro. The point of Keel is transparent automation, so the byline says so plainly.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
      <category>automation</category>
    </item>
    <item>
      <title>I built a job-application autopilot that refuses to lie</title>
      <dc:creator>Trent Wade</dc:creator>
      <pubDate>Tue, 22 Sep 2026 12:16:35 +0000</pubDate>
      <link>https://dev.to/keel/i-built-a-job-application-autopilot-that-refuses-to-lie-20pd</link>
      <guid>https://dev.to/keel/i-built-a-job-application-autopilot-that-refuses-to-lie-20pd</guid>
      <description>&lt;p&gt;Auto-apply tools had a good premise and became spam cannons. Identical applications, invented qualifications, blasted at everything. It burns the candidate and poisons the channel for everyone — including people applying honestly.&lt;/p&gt;

&lt;p&gt;I spent the last year running a production job-application pipeline, and the thing I kept coming back to was: the lying is the product defect. Not the automation. The automation is fine. The lying is the defect.&lt;/p&gt;

&lt;p&gt;So I built Keel around a truthfulness contract, enforced in code rather than promised in marketing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Truthfulness gates.&lt;/strong&gt; Anything your profile can't support is reported as a gap, never bridged with fiction. No invented experience, degrees, or answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit confirmation.&lt;/strong&gt; Nothing counts as submitted unless the page itself confirms it. Not the browser accepting the task — the page confirming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail closed.&lt;/strong&gt; Unverifiable postings, unmappable questions, missing attestations: park, never proceed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No fingerprinting surface.&lt;/strong&gt; Nothing in the public repo helps ATS vendors identify or block automated applications.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The public half is Apache-2.0: discovery and fit scoring, truthful resume tailoring, a canonical answer bank, prescreen gates, an ATS capability radar (read-only probes), the launch-packet builder, append-only telemetry, and a dashboard. The execution layer — how applications are actually submitted — stays private deliberately. It holds submission-behavior techniques vendors could fingerprint, and publishing them would degrade the pipeline for everyone running it, including everyone who clones the repo. The boundary rule is public in SPLIT.md.&lt;/p&gt;

&lt;p&gt;The traction figure: the private production pipeline running this discipline holds 210 verified submissions, recounted today from its ledger (202 with quoted confirmation evidence, 3 with pointer evidence, 1 URL-only, 3 unevidenced — counted, never estimated). I'll state the limit plainly: you can't independently re-run that pipeline. What's verifiable is the discipline — the gates, the telemetry, the fail-closed reporting rules are all public code.&lt;/p&gt;

&lt;p&gt;If you're a builder, the quickstart is ~5 minutes, stdlib only, with a terminal demo in the README. If you're a contributor, I want help most on ATS behavior coverage, answer-bank ergonomics, and outcome analytics.&lt;/p&gt;

&lt;p&gt;The repo: &lt;a href="https://github.com/KeelDev-tech/keel" rel="noopener noreferrer"&gt;https://github.com/KeelDev-tech/keel&lt;/a&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>buildinpublic</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
