<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[DynoKore]]></title><description><![CDATA[DynoKore]]></description><link>https://dynokore.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 02 Nov 2026 07:47:00 GMT</lastBuildDate><atom:link href="https://dynokore.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Our coding agent stopped three times on one task. Here's every question it asked.]]></title><description><![CDATA[On 22 October 2026 a coding agent picked up a task in DynoInvoice: counterparty master data for the master-data service. Resolve a buyer or seller named on…]]></description><link>https://dynokore.hashnode.dev/our-coding-agent-stopped-three-times-on-one-task-here-s-every-question-it-asked</link><guid isPermaLink="true">https://dynokore.hashnode.dev/our-coding-agent-stopped-three-times-on-one-task-here-s-every-question-it-asked</guid><category><![CDATA[software architecture]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[DynoKore Team]]></dc:creator><pubDate>Thu, 29 Oct 2026 13:32:48 GMT</pubDate><content:encoded><![CDATA[<p>On 22 October 2026 a coding agent picked up a task in DynoInvoice: counterparty master data for the master-data service. Resolve a buyer or seller named on an invoice, record where its identifiers came from, keep its history, and flag whether it belongs to the same GST group or the same customer account as the business filing the invoice.</p>
<p>Overnight, the agent stopped three times. Each time it put design questions on the record instead of guessing, the task waited while other work carried on, and it resumed once the questions were answered. By early afternoon the task was done and merged. Here is every question, and who answered it.</p>
<h2>The requirement behind it</h2>
<p>The requirement says: when a document names a counterparty, flag <code>sameGroup</code> and <code>sameTenant</code>. Whether supplies inside a GST group have to be reported is still waiting on an answer from a tax adviser, so the rule for them is defined as provisional data in a rules pack, which can be changed by publishing data, without changing code.</p>
<p>That requirement has been revised as decisions landed: once when the owner ruled that build waves follow dependencies, once when the provisional part was narrowed to same-group supplies only and a non-blocking warning was added, and twice more in October as the coding task exposed gaps.</p>
<h2>Stop 1: the contract didn't say enough</h2>
<p>The contract had no legal name or address for a counterparty, so those were added, with a list of findings when they're missing. Nothing recorded where an identifier came from, which became an optional inline source object. There was no version history either, so a paged history operation was added.</p>
<p>The fourth question mattered most: where do the two flags come from? The ruling was a read model the service owns, fed by two organisation-structure events. A synchronous call to the tenant service was rejected, because an earlier published rule makes this a leaf service that doesn't make calls.</p>
<h2>Stop 2: the edges of the data</h2>
<p>An event needed to say which entity it was about, so the request got an optional <code>entityRef</code>. A 21-character scheme code didn't fit a 20-character field, and the limit went up to 32. Individuals couldn't be looked up until a <code>partyRef</code> parameter was added. And there was no operation to write ingested counterparties, so one was added on an existing service edge.</p>
<p>These followed a ruling the owner had made the same day: "Who may call whom, and when, must follow the system architecture design, not a mechanical judgement."</p>
<h2>Stop 3: where a refusal comes from</h2>
<p>The contract said callers outside their scope get a 403, but not where that check lives. The ruling was one generic runtime filter driven by the contract. Inferring scope from property names was rejected, because a guess in a security filter either lets requests through or refuses too many.</p>
<p>All nine questions were ruled by the coordinating agent, and the rulings at two of the three stops cite an earlier ruling as precedent. None went to the owner: under the owner's standing rule, questions that don't touch functionality or direction are ruled by the coordinating agent when a published rule or precedent settles them.</p>
<h2>Where a person did step in</h2>
<p>A week earlier, the agents had found that the first-wave contracts lacked identifiers the second wave would need, and the recommendation was a dedicated follow-up task. The owner chose differently: "Contract-first means the contract is settled before it's implemented. It doesn't mean it has to be complete in the first wave. Add to the contract when it's needed." That ruling is why the contract was extended right before this task, and it is on the record with the owner's name on it.</p>
<h2>What review caught, and what we'd flag ourselves</h2>
<p>The day before, a revision of the task was sent back with four findings: its context still cited a deleted criterion; "the set held by the policy port" didn't say which set, so it couldn't be checked; a criterion said no merge request existed when one did; and an inline example used a request body the service now rejects. All four were fixed before it was approved.</p>
<p>One thing we'd point out ourselves. One acceptance criterion says two entities in the same account record a supply and a purchase that pair up. Its test checks the flags. The pairing belongs to another requirement, so the test's name alone doesn't prove pairing works. You can read the <a href="https://github.com/dynokore/DynoInvoice/blob/main/svc-masterdata/masterdata-bootstrap/src/test/kotlin/com/dynokore/gst/masterdata/bootstrap/CounterpartySamenessTest.kt">three tests</a> and the <a href="https://github.com/dynokore/DynoInvoice/commit/1de1ed9f">first commit</a> in the public repository.</p>
<h2>If your agents build from a spec</h2>
<ol>
<li><strong>Let the agent stop, and make stopping cheap.</strong> Only the task that hit the gap should wait. If a stop freezes everything, agents learn to guess.</li>
<li><strong>Ask for options and a recommendation.</strong> "Which of these, and why" gets ruled fast. "What should I do" doesn't.</li>
<li><strong>Write down who decides what, in advance.</strong> Here, anything about what the product does, its direction, or loosening a check goes to a person. Contract shapes and data limits don't.</li>
<li><strong>Answer from precedent.</strong> Keep rulings where the next question can cite them, so the same question isn't argued twice.</li>
<li><strong>Ship undecided business rules as data, marked provisional.</strong> The service code can be finished while the tax question is still open.</li>
</ol>
<h2>How the record holds it together</h2>
<p>In Keel, the requirement, its acceptance criteria, the task and every design question are linked records. A design question holds only the task it is about. On DynoInvoice, a coding agent that hits a gap is told to raise a question with options; only that task waits; the answer is recorded with who decided, and rulings are kept so a later question can be answered from them. As of 26 October 2026, DynoInvoice's coding agents had stopped about 127 times on about 113 tasks this way, raising 281 questions. The task is marked done from its merged change and CI verdict, not from the agent's report.</p>
<p>More examples, each traced from requirement to merged code: <a href="https://dynokore.com/work/dynoinvoice/traces/">How DynoInvoice is built, commit by commit</a>.</p>
<p><em>Written by the DynoKore team with AI assistance. First published on</em> <a href="https://dynokore.com/blog/three-stops-one-task/"><em>dynokore.com</em></a><em>. Events from DynoInvoice's public repository and Keel record, October 2026. Owner quotes are translated from chat. We're opening DynoKore Keel to a small group of design partners:</em> <a href="https://dynokore.ai/?ref=hashnode"><em>dynokore.ai</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[We changed a core rule two weeks in. No service had to be rewritten.]]></title><description><![CDATA[On 12 October 2026, two weeks into building DynoInvoice, the owner sent one line: "The system must access the database in one unified way." At that moment…]]></description><link>https://dynokore.hashnode.dev/we-changed-a-core-rule-two-weeks-in-no-service-had-to-be-rewritten</link><guid isPermaLink="true">https://dynokore.hashnode.dev/we-changed-a-core-rule-two-weeks-in-no-service-had-to-be-rewritten</guid><category><![CDATA[software architecture]]></category><category><![CDATA[database]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[DynoKore Team]]></dc:creator><pubDate>Thu, 29 Oct 2026 13:30:13 GMT</pubDate><content:encoded><![CDATA[<p>On 12 October 2026, two weeks into building DynoInvoice, the owner sent one line: "The system must access the database in one unified way." At that moment, 29 production files in six shared library modules talked to the database through plain JDBC, which the standards had allowed until then. One of those libraries had merged two days earlier.</p>
<p>Within about 46 minutes, 16 reviewed changes were published to the project's record: the architecture decision and the requirement were amended, 7 tasks were created, 15 existing tasks were amended, and later service work was made to depend on the change. No service had to be rewritten, because no service had been built on the old pattern yet.</p>
<h2>What one line in <code>CLAUDE.md</code> wouldn't have done</h2>
<p>If that rule had lived in a <code>CLAUDE.md</code> or <code>AGENTS.md</code> file, the edit would take a minute. Here is what the edit wouldn't do:</p>
<ul>
<li>tell the 15 tasks already written against the old rule that their done-when criteria were now wrong;</li>
<li>stop a task that was ready to start from starting on the old pattern;</li>
<li>convert the library code that had already merged;</li>
<li>stop the next merge request from bringing the old pattern back.</li>
</ul>
<p>An issue tracker helps with the first one, if someone goes through the tickets by hand. The other three need something else.</p>
<h2>What happened instead</h2>
<p><strong>The decision went on the record first.</strong> The coordinating agent had put a design question on the record with five options. The owner chose "jOOQ only, everywhere", then came back half an hour later and asked whether JPA could handle simple reads and writes. The answer set out what that would cost in tenant safety, caching and mixed transactions, and the owner settled on jOOQ only, with a thin convenience layer on top. Both rulings are records, with who decided.</p>
<p><strong>Tasks changed in the same hour.</strong> The architecture decision and the requirement were revised through reviewed changesets, and the affected tasks were amended alongside them. One task moved itself from ready back to blocked the moment its new dependency was published, so no agent could pick it up against the old rule. Minutes later a design question found two code templates the first pass had missed, and they were added.</p>
<p><strong>The rule became a build check.</strong> Six libraries were banned, and dependency, naming and bytecode rules now refuse the old pattern. The reviewer spent five rounds probing ways around those rules: an inherited method, a lambda, a pool subclass, reflective writers, a string bean definition. Each hole was closed or ruled on.</p>
<p><strong>Old code was converted from a list that could only shrink.</strong> The list of modules still to convert went from four to none. A module can't be added back, and an empty entry fails the test until someone removes it. The first build-wide run of the new rule produced 48 findings, each with a reason and an owning task. None was exempted.</p>
<p><strong>The owner narrowed the rule three times.</strong> The first build rules read the ruling too strictly and refused any reference to <code>java.sql</code>. The owner clarified that the point was one way of access and no direct SQL statements, not a ban on the types. Later: tests may use JDBC, production code may not. Each time the rule was loosened on the record, with a reason. Nobody worked around it in code.</p>
<p>As of 26 October 2026, apart from three named exceptions (the tenant kernel's connection core and the wiring that hands connections to jOOQ), DynoInvoice's production code has no JDBC statements.</p>
<h2>Before your next rule change</h2>
<ol>
<li><strong>Put a decision in one place and point at it.</strong> Tasks, standards and templates should cite the decision by ID instead of restating it. When it changes, you search for the ID, not for every way someone paraphrased it.</li>
<li><strong>Turn the rule into a build check as early as you can.</strong> A rule that agents only read gets broken by the next agent that skims it.</li>
<li><strong>Convert old code from a list that can only shrink.</strong> A minimal version in bash:</li>
</ol>
<pre><code class="language-bash"># allowlist.txt: files still allowed to use the old pattern. It may only shrink.
pattern='java\.sql\.(Statement|PreparedStatement)'
found=$(grep -rlE "$pattern" src/main | sort)
new=$(comm -23 &lt;(echo "$found") &lt;(sort allowlist.txt))
gone=$(comm -13 &lt;(echo "$found") &lt;(sort allowlist.txt))
[ -z "$new" ]  || { echo "new use of the old pattern:"; echo "$new"; exit 1; }
[ -z "$gone" ] || { echo "clean now, remove from allowlist.txt:"; echo "$gone"; exit 1; }
</code></pre>
<p>It fails on any new file that uses the pattern, and on any listed file that is already clean, so the list can't quietly keep entries it no longer needs. Plant a new use and check it goes red before you trust it.</p>
<ol start="4">
<li><strong>Expect to narrow the rule.</strong> When the check turns out stricter than the decision meant, change the decision's wording on the record, with a reason. Don't add an exception in code.</li>
</ol>
<h2>The record behind it</h2>
<p>In Keel, requirements, architecture decisions, rules and tasks are small versioned records with typed links, and design questions are answered on the record with who decided. Here, the ruling, the decision and the tasks were revised through changesets that were reviewed before they were published. A task with an unmet dependency is held back as blocked, which is why one task moved itself back to blocked the moment its new dependency was published.</p>
<p>The build check and the shrinking list live in DynoInvoice's own build. Keel holds the decision they enforce, and when a linked record changes, it marks the links below it for review. The full timeline, with commits, is on <a href="https://dynokore.com/work/dynoinvoice/change/">When a decision changes</a>.</p>
<p><em>Written by the DynoKore team with AI assistance. First published on</em> <a href="https://dynokore.com/blog/one-rule-22-tasks/"><em>dynokore.com</em></a><em>. Events from DynoInvoice's public repository and Keel record, October 2026; counts as of 26 October 2026. Owner quotes are translated from chat. We're opening DynoKore Keel to a small group of design partners:</em> <a href="https://dynokore.ai/?ref=hashnode"><em>dynokore.ai</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[We added a gate to catch tests that never run]]></title><description><![CDATA[Your pipeline is green. Did every test actually run? In October 2026, a DynoInvoice merge request passed two green pipelines even though 16 of its 17…]]></description><link>https://dynokore.hashnode.dev/we-added-a-gate-to-catch-tests-that-never-run</link><guid isPermaLink="true">https://dynokore.hashnode.dev/we-added-a-gate-to-catch-tests-that-never-run</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[Testing]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[DynoKore Team]]></dc:creator><pubDate>Thu, 29 Oct 2026 13:28:13 GMT</pubDate><content:encoded><![CDATA[<p>Your pipeline is green. Did every test actually run? In October 2026, a DynoInvoice merge request passed two green pipelines even though 16 of its 17 negative tests had been skipped.</p>
<p>The fix took more than changing the tests. We added a gate to compare the test cases reported by the runner with the test methods in the source, so the same kind of miss is harder to overlook.</p>
<h2>Sixteen tests, one reported case per class</h2>
<p>The merge request was for a database library in DynoInvoice, the e-invoicing system agents are building with Keel. Its task called for negative tests covering stale versions, concurrent updates, another tenant’s rows, missing transactions, and row-level security on and off. The coding agent wrote 17 tests in a shared base class.</p>
<p>The tests were written in Kotlin like this:</p>
<pre><code class="language-kotlin">@Test
fun `rejects a stale version`() = withService {
    assertThrows&lt;StaleVersionException&gt; { store.update(staleRow) }
}
</code></pre>
<p><code>withService</code> returns the value of its lambda. Here, that value is the exception returned by <code>assertThrows</code>, so the test method returns a value instead of Kotlin’s <code>Unit</code>.</p>
<p>JUnit Jupiter only runs test methods that return <code>void</code> (in Kotlin, <code>Unit</code>). It skips the others without failing the build. The pipeline was green, listed six test classes, and each class had run one case.</p>
<p>The fix was to use a block body, so the method returns <code>Unit</code>:</p>
<pre><code class="language-kotlin">@Test
fun `rejects a stale version`() {
    withService {
        assertThrows&lt;StaleVersionException&gt; { store.update(staleRow) }
    }
}
</code></pre>
<h2>The first review missed it</h2>
<p>The independent reviewer found a real gap: a missing batch-audit test. But the review did not catch that most tests were not running. Nor did a check that required each changed test class to have at least one passing case. Each class had exactly one.</p>
<p>A separate agent noticed later while working on something else. It opened the test report, saw one case per class, and compared that with the 17 <code>@Test</code> methods in the shared base class. Re-running one class reported two tests.</p>
<p>An independent agent caught it before merge. The review round and the existing check both missed it.</p>
<h2>Make the count part of the gate</h2>
<p>Fixing the 16 methods addressed this merge request. It didn’t address how the same mistake could slip by again. So we made two changes:</p>
<ul>
<li>Every <code>@Test</code> method must return <code>Unit</code>.</li>
<li>A new gate compares the number of test cases in each class’s report with the number of <code>@Test</code> lines in its file.</li>
</ul>
<p>The gate has tests of its own: an expression-bodied test must make the count come up one short, and a missing class must count as zero. After the fix, every case in the six classes ran, with no failures or skips. The change merged on 18 October 2026, in commit <code>daaaaabd</code> in the <a href="https://github.com/dynokore/DynoInvoice">public repository</a>.</p>
<h2>Checks worth borrowing</h2>
<p><strong>Read the case count, not just the colour.</strong> Green means the checks that ran passed. It doesn’t tell you whether all the expected cases ran.</p>
<p><strong>Compare the report with the source.</strong> For each class, check that the reported case count matches the number of test methods. Adapt the comparison for inherited and parameterised tests, then deliberately add a skipped test and confirm the gate fails.</p>
<p><strong>Prove negative tests can fail.</strong> Break the behaviour a test is supposed to protect and check that it goes red. Otherwise, another layer may be rejecting the input and making the test look effective when it isn’t.</p>
<p>Here is a minimal starting point for Kotlin tests with Gradle’s JUnit XML reports:</p>
<pre><code class="language-bash"># fail if any test class ran fewer cases than it declares
for src in $(grep -rl --include='*.kt' '@Test' src/test); do
  cls=$(basename "$src" .kt)
  declared=$(grep -cE '@Test([^A-Za-z]|$)' "$src") # not @TestInstance
  ran=$(grep -ho "testsuite name=\"[^\"]*\.$cls\" tests=\"[0-9]*\"" build/test-results/test/*.xml \
    | grep -o 'tests="[0-9]*"' | grep -o '[0-9]*' | head -1)
  [ "${ran:-0}" -ge "$declared" ] || {
    echo "$cls: declared $declared, ran ${ran:-0}"
    exit 1
  }
done
</code></pre>
<h2>What Keel did</h2>
<p>Keel’s records tie a task’s tests to its acceptance criteria. The coding agent can’t change those criteria, and the server marks a task done from the merged change and its CI verdict. On DynoInvoice, our rules forbid weakening a check to get green, and an independent reviewer agent checks every merge request, including test changes. Here, that review missed the skipped tests. After the miss surfaced, the team added a case-count gate to the build.</p>
<p>We’re opening Keel to design partners building with coding agents. Design partners get Keel free during the beta, with hands-on setup and onboarding. <strong>Apply at <a href="https://dynokore.ai/?ref=hashnode#access">dynokore.ai</a>.</strong></p>
<p><em>Written by the DynoKore team with AI assistance. First published on <a href="https://dynokore.com/blog/16-of-17-tests-never-ran/">dynokore.com</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[We're building an invoicing system with AI agents, in public]]></title><description><![CDATA[DynoInvoice is an e-invoicing and GST compliance platform for Singapore, and AI coding agents are writing it. Its source is now on GitHub at…]]></description><link>https://dynokore.hashnode.dev/we-re-building-an-invoicing-system-with-ai-agents-in-public</link><guid isPermaLink="true">https://dynokore.hashnode.dev/we-re-building-an-invoicing-system-with-ai-agents-in-public</guid><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[DynoKore Team]]></dc:creator><pubDate>Sat, 24 Oct 2026 04:28:53 GMT</pubDate><content:encoded><![CDATA[<p>DynoInvoice is an e-invoicing and GST compliance platform for Singapore, and AI coding agents are writing it. Its source is now on GitHub at <a href="https://github.com/dynokore/DynoInvoice">github.com/dynokore/DynoInvoice</a>. The mirror syncs every hour, so you can watch the agents' commits land while they work. Development is ongoing, and incomplete features are expected.</p>
<p>The source is published for study only. It's licensed under PolyForm Strict 1.0.0, which allows reading and running it for non-commercial purposes, and nothing else. We don't accept contributions. It isn't open source in the OSI sense, and we don't call it that.</p>
<h2>What DynoInvoice is</h2>
<p>Singapore is moving GST invoicing onto InvoiceNow, the national e-invoicing network built on Peppol. GST-registered businesses will have to send their invoice data to IRAS through it, using an InvoiceNow-Ready solution and an accredited Access Point.</p>
<p>DynoInvoice sits beside the systems a business already runs (ERP, billing, point of sale, expenses) rather than replacing them. It handles what the InvoiceNow requirement asks of a GST-registered business:</p>
<ul>
<li>deciding which transactions are in scope, and mapping internal tax codes to IRAS GST categories;</li>
<li>building and validating the invoice data IRAS requires, and working out when each submission is due;</li>
<li>submitting it to IRAS through an accredited Access Point, and handling resubmissions and credit notes;</li>
<li>keeping the evidence, and reconciling what was submitted against what was filed in the GST return.</li>
</ul>
<p>Accounting firms and tax agents can act for their clients, with each client's consent for each entity. It runs as a multi-tenant service or as a single-tenant private deployment, from the same code. It doesn't run its own Access Point, prepare GST returns, or replace accounts receivable and payable.</p>
<p>The foundations are in place: organisation structure, access control (including acting on a client's behalf) and the shared contracts between services. Document handling, submission and reconciliation are still being built.</p>
<h2>Why we're building it</h2>
<p>The requirement already applies to new voluntary GST registrations, and IRAS has set dates to bring in most other GST-registered businesses in phases through April 2031, so most businesses will need something between their existing systems and the network.</p>
<p>We're building DynoInvoice for two reasons. It's a product we intend to bring to market when it's ready. And it's the hardest test we could find for DynoKore Keel. Invoicing has tenants that must never see each other's data, rules that come from regulation rather than taste, and an audit trail someone will eventually ask for. If agents can build that under governance, the method should hold for less demanding software.</p>
<h2>How it's designed</h2>
<p>DynoInvoice is designed the way a regulated, multi-tenant system has to be. The diagram shows the design; dashed parts aren't built yet.</p>
<img src="https://dynokore.com/blog/launch-dynoinvoice/architecture.png" alt="DynoInvoice architecture. Solid boxes have code on main and are still in progress; dashed boxes are designed but not built yet." style="display:block;margin:0 auto" />

<p><em>Solid boxes have code on main and are still in progress; dashed boxes are designed but not built yet.</em></p>
<ul>
<li><strong>One domain per service, one schema per service.</strong> Each service owns its own database schema and can't read another's tables. Services talk through contracts published from one place and checked automatically: a few synchronous APIs where the caller needs an answer, and events everywhere else, delivered at least once to consumers that must handle repeats.</li>
<li><strong>Tenant isolation in the database.</strong> Every table carries the tenant, and isolation is enforced by the database rather than by application filters. A private deployment runs the same multi-tenant code with a single tenant.</li>
<li><strong>Submissions keep flowing.</strong> Tokens are checked offline at the gateway, and evidence, reconciliation and notifications consume events without holding up the pipeline.</li>
<li><strong>Evidence you can verify.</strong> Receipts, decisions and audit logs go to an append-only store kept apart from operational data, with integrity checks.</li>
<li><strong>Rules as signed data.</strong> Tax rules are versioned data in signed rule packs, so the same date and input give the same result. The control plane that signs them runs only at the operator, never receives tenant data, and isn't needed at run time: a private deployment can run fully offline, without new rule packs until it reconnects or loads one.</li>
</ul>
<h2>How the agents are governed</h2>
<p>The agents work under DynoKore Keel, which we built for ourselves first.</p>
<p><strong>One chain, end to end.</strong> Requirements, acceptance criteria and tasks are versioned records in Keel, and every change traces back to them. Before a coding agent starts, the server checks the task's requirement and design readiness and lists each missing item; a task that fails stays blocked.</p>
<p><strong>The author doesn't grade its own work.</strong> By rule, the agent that drafts a requirement or decision change submits it, and a separate examiner agent approves and publishes it. Every merge request must pass the repository's automated gates and an independent reviewer agent's check against the project's written engineering standards, including any change to tests (documentation-only changes run text checks only).</p>
<p><strong>Done is decided by the server, not the coder.</strong> Tests must prove acceptance criteria held in Keel. The coding agent can't change those criteria, and a rule forbids weakening a check to get green. A coding task is marked done by the server from the merged change and its CI verdict, not by an agent, and a change to a task's acceptance criteria sends it back for review.</p>
<p><strong>People answer the questions that matter.</strong> Questions about functionality, direction, exemptions or relaxing a check are answered by a person, and the answer is recorded in Keel with who decided. Loosening a check is never an agent's call. When an agent reaches one of these questions, that task waits and the agents carry on with other work; once the person answers, it picks up again. Most other approvals are made by agents under a delegation from that person.</p>
<p><strong>Credentials stay out of the agents' reach.</strong> A hook blocks agents from reading credential files; tools read credentials in their own process and never print them. Before every sync, the published history is scanned for credentials and internal identifiers, and any finding stops the sync.</p>
<h2>What went wrong</h2>
<p>Agents fail in ways that look like success. A few things we caught, and what we changed:</p>
<ul>
<li><strong>Green, but not done.</strong> A coding agent reported a task complete with a green pipeline. It had pushed only scripts and fixtures; the main change was never committed. Coding agents are now required to have a clean working tree, with a diff covering every file the task names, before reporting done, and independent review checks the diff against the task.</li>
<li><strong>A looser reading to get through.</strong> An agent decided on its own that part of a requirement didn't apply, which made the task easy to pass. It was sent back. A coding agent that hits a gap is instructed to stop and report it; it may not pick a looser reading of the task.</li>
<li><strong>A test that passed on review day.</strong> A test hard-coded a moment in time while the SQL used the database clock. It was green when reviewed, then turned main red a couple of days later and blocked every merge request. Review now looks for time literals, and time-sensitive tests run with both the application and database clocks shifted.</li>
<li><strong>A scan that saw nothing.</strong> A licence scan with no findings was cited as evidence, but it had scanned the source tree and never saw the built artifacts. Before a green scan counts as evidence, we check that it actually saw what it claims to cover.</li>
<li><strong>A quotation nobody wrote.</strong> An agent drafting a decision included a quotation that didn't exist in its source. The independent examiner caught it before publication. Quotations must now be exact and cite the version they come from.</li>
<li><strong>Agents waiting on each other.</strong> One agent was allowed to dispatch another. When every slot was full, each waited on a slot the other held. Agents no longer dispatch other agents; the coordinator does, and a script caps how many run at once.</li>
</ul>
<h2>What's next</h2>
<p>We're building a public view of the record, where you'll be able to pick a commit and follow it back to its task, requirement and review.</p>
<p>DynoInvoice is still being built. If you want to ask how the agents built something, the repository's <a href="https://github.com/dynokore/DynoInvoice/discussions">Discussions</a> are open.</p>
<p>We're also opening Keel to a small group of design partners: engineering teams already using coding agents who need to answer to an auditor, a client or a regulator. Apply at <a href="https://dynokore.ai">dynokore.ai</a>.</p>
<p>If you find something to criticise in the code, that's the point. Tell us.</p>
<p><em>Originally published at</em> <a href="https://dynokore.com/blog/launch-dynoinvoice/"><em>dynokore.com</em></a><em>. Written by the DynoKore team with AI assistance.</em></p>
]]></content:encoded></item><item><title><![CDATA[From vibe coding to governed development]]></title><description><![CDATA[In 1957, John Backus and his team at IBM presented a paper called The FORTRAN Automatic Coding System. The promise was in the name: you would describe the…]]></description><link>https://dynokore.hashnode.dev/from-vibe-coding-to-governed-development</link><guid isPermaLink="true">https://dynokore.hashnode.dev/from-vibe-coding-to-governed-development</guid><category><![CDATA[AI]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[architecture]]></category><dc:creator><![CDATA[DynoKore Team]]></dc:creator><pubDate>Fri, 23 Oct 2026 20:21:01 GMT</pubDate><content:encoded><![CDATA[<p>In 1957, John Backus and his team at IBM presented a paper called <em>The FORTRAN Automatic Coding System</em>. The promise was in the name: you would describe the computation, and the machine would do the programming. It didn't end programming. It created far more of it — because the people writing FORTRAN no longer had to think in registers.</p>
<p>Every step up the ladder has looked like that. Machine code gave way to assembly, assembly to high-level languages. Each step let people write less about <em>how</em> and more about <em>what</em>. And each step moved the source of truth up a level: once compilers could be trusted, nobody maintained the assembly. They maintained the source.</p>
<p>Coding agents are the next step. What you hand an agent is, at bottom, a requirement: what the system should do. So it's tempting to conclude that programming is becoming natural language, and that the code is turning into an intermediate artefact nobody reads.</p>
<p>We think the direction is right. The conclusion is premature, and the reason why matters.</p>
<h2>An agent is not a compiler</h2>
<p>A compiler translates a formal language deterministically, and it preserves meaning. Compile the same source a hundred times and you get the same behaviour. That guarantee is why we stopped reading assembly.</p>
<p>An agent translates natural language probabilistically, and nothing guarantees it preserves meaning. Natural language is ambiguous to begin with. Give the same request twice and you can get two different programs, both of which pass their own tests.</p>
<p>In the late 1970s, Dijkstra wrote a short note called <em>On the foolishness of "natural language programming"</em>. His point was that the value of a formal notation is its precision. He was right. And that's the problem teams are now living with.</p>
<h2>What's breaking</h2>
<p>We went through about 7,000 Hacker News comments from the past year about working with coding agents, with agents doing the first pass, and kept the 178 in which an engineer described something that actually went wrong for them. The most common complaints:</p>
<ul>
<li><strong>Code that works today and nobody can change tomorrow.</strong> Logic duplicated at the wrong layer, the repo's design ignored, a feature another team had to rewrite before it fit. This was the single most frequent theme.</li>
<li><strong>Review that can't keep up.</strong> Agents produce more than people can read, and review turns into a bottleneck or a rubber stamp.</li>
<li><strong>Someone else's AI output landing on you.</strong> Large generated changes from colleagues, which you're expected to review, fix or own.</li>
<li><strong>Losing your grip on your own codebase.</strong> You merged it, but you no longer hold a mental model of it.</li>
<li><strong>"It passes" isn't "it does what was asked."</strong> The code compiles, the tests are green, and it solves a slightly different problem.</li>
</ul>
<p>None of these is fixed by better prompts. They're what happens when the source of truth moves up a level but nothing guarantees the translation.</p>
<h2>Requirements become the source code</h2>
<p>If requirements are where intent now lives, they need everything source code has needed for fifty years:</p>
<ul>
<li><strong>Versioning.</strong> Every change to a requirement is a recorded revision, not an edit to a chat history.</li>
<li><strong>Review.</strong> A change to what the system should do is checked before anyone builds on it.</li>
<li><strong>Tests.</strong> Each requirement carries acceptance criteria precise enough to check, not adjectives.</li>
<li><strong>Traceability.</strong> Every line of code can answer: which requirement am I for?</li>
</ul>
<p>That isn't "write a long spec, then generate". Heavy specs have their own failure mode. One engineer in our sample described a thirty-minute fix that turned into a 37-page AI planning document and a week of process. Governance is not more paperwork. It's making intent precise, keeping it in one place, and checking every output against it.</p>
<h2>How we do it</h2>
<p>We built DynoKore Keel around three ideas.</p>
<p><strong>One source of truth.</strong> Requirements, acceptance criteria, tasks and decisions are versioned records in Keel, and every change traces back to them. Changes to requirements go through an independent examiner agent before they're published. When a question needs a person — how a feature should behave, whether an exemption is acceptable, whether a check may be relaxed — the answer is recorded, with who decided.</p>
<p><strong>Controlled execution.</strong> Coding agents work under the project's written engineering standards; for example, library modules may only depend downward, enforced by architecture checks in CI. They can't change the acceptance criteria their tests must prove, and a rule forbids weakening a check to get green. A separate reviewer agent checks every merge request, test changes included. The agent that wrote the code never reviews it.</p>
<p><strong>Verifiable outcomes.</strong> A task can't be marked done until its requirement chain exists and passes Keel's readiness check, and no code change merges until the automated checks pass. Routine approvals are delegated to agents; questions that need a person are answered by one, and recorded with who decided.</p>
<p>Notice what isn't in that list: a person reading every line. We couldn't, and neither can you. The work is in making that safe.</p>
<h2>What we got wrong</h2>
<p>We learned the hard way that a sentence in a prompt is not a control. We told our agents never to print credentials. Three times, a credential fragment still ended up in an agent's output. The fix wasn't a better prompt. It was mechanism: tools that need credentials read them in their own process and never print them, and a hook now blocks reading credential files at all.</p>
<p>That lesson generalises. In a governed system, a mistake that happens twice gets a mechanism, not a reminder.</p>
<p>And we should be honest about scope. DynoInvoice, the system we've built this way, is weeks old, not years. We can show you how it's structured and how every change was checked. We can't yet show you what it looks like after five years of maintenance. Nobody can, for agent-built systems. That's exactly why the structure matters now.</p>
<h2>Where this goes</h2>
<p>Will code eventually disappear from view, the way assembly did? Maybe, but only once checking an agent's output becomes as trustworthy as a compiler. Until then, the job isn't to stop engineering. It's to move engineering up a level, to the requirements, and keep the checks underneath honest.</p>
<p>If your team is already shipping agent-written code into a system you'll maintain for years, three things you can start tomorrow:</p>
<ol>
<li>Write down what "done" means before the agent starts, in terms a test can check.</li>
<li>Never let the agent that wrote a change be the one that approves it.</li>
<li>When a mistake happens twice, replace the reminder with a mechanism.</li>
</ol>
<p>Vibe coding is a fine way to build a prototype. A system you'll run for years needs something else: governed development.</p>
<p>Every requirement. Every decision. Every proof.</p>
<p><em>We're opening Keel to a small group of design partners: engineering teams already building with agents who need to answer for what they ship.</em> <a href="https://dynokore.ai"><em>dynokore.ai</em></a></p>
]]></content:encoded></item><item><title><![CDATA[When a colleague's AI-generated pull request lands on you]]></title><description><![CDATA[When a colleague uses an agent to write a large change and asks you to review it, the cost of that change moves to you. Engineers describe huge pull…]]></description><link>https://dynokore.hashnode.dev/when-a-colleague-s-ai-generated-pull-request-lands-on-you</link><guid isPermaLink="true">https://dynokore.hashnode.dev/when-a-colleague-s-ai-generated-pull-request-lands-on-you</guid><category><![CDATA[AI]]></category><category><![CDATA[code review]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[DynoKore Team]]></dc:creator><pubDate>Fri, 23 Oct 2026 20:18:18 GMT</pubDate><content:encoded><![CDATA[<p>When a colleague uses an agent to write a large change and asks you to review it, the cost of that change moves to you. Engineers describe huge pull requests, intent that doesn't match the business, and authors who can't explain their own code. Stricter review alone doesn't fix it; the review needs something fixed to check against, and a limit on what can be submitted.</p>
<h2>What we found</h2>
<p>We went through about 7,000 Hacker News comments from the past year about working with coding agents, and kept 178 in which an engineer described something that actually went wrong for them. In 23 of those, the problem wasn't their own agent. It was AI-generated work from someone else (a teammate, a contractor, a manager or an open-source contributor) that they were expected to review, fix or take over. We rated 6 of those 23 as high severity, more than any other theme in the sample.</p>
<p>The same patterns came up again and again:</p>
<ul>
<li><strong>Size.</strong> Pull requests of thousands of lines, every day. One engineer spent hours on a single 3,800-line shell-script change from a contractor who couldn't answer basic questions about the system. (<a href="https://news.ycombinator.com/item?id=47289922">source</a>)</li>
<li><strong>The wrong problem, solved confidently.</strong> Teammates fed half-finished code to a model and sent back versions that got the business requirements wrong. (<a href="https://news.ycombinator.com/item?id=47391792">source</a>)</li>
<li><strong>Work that doesn't fit.</strong> A feature from another team had to be stripped down and redesigned for more than a week before it fit the main API. The engineer who did the fixing was the one who looked slow. (<a href="https://news.ycombinator.com/item?id=47390383">source</a>)</li>
<li><strong>Proof that isn't proof.</strong> Scripts that print what the author expects to happen, instead of demonstrating that it does. (<a href="https://news.ycombinator.com/item?id=47680414">source</a>)</li>
<li><strong>Debt merged under deadline.</strong> A lead let contractors' fast AI code through to hit a budget, and is still cleaning it up. (<a href="https://news.ycombinator.com/item?id=47392440">source</a>)</li>
</ul>
<p>Open-source maintainers described the same thing at larger scale: a flood of generated pull requests acting like a denial-of-service attack on their attention. (<a href="https://news.ycombinator.com/item?id=49333761">source</a>)</p>
<h2>What people tried</h2>
<p>Most responses fell into three groups:</p>
<ul>
<li><strong>Review harder.</strong> Raise the bar, reject "AI slop", read every line. It works until the volume grows, and it makes the strict reviewer unpopular.</li>
<li><strong>Restrict who can use agents.</strong> New hires limited to planning mode until they understand the codebase. Some engineers stopped using agents altogether.</li>
<li><strong>Remove the source.</strong> One lead let the contractors go.</li>
</ul>
<p>Each of these puts the whole load on the reviewer's judgement, at the end of the process, after the work is done. None of them changes what arrives for review.</p>
<h2>What actually helps</h2>
<p>The common thread in these stories is that the reviewer has nothing fixed to check against. They have a diff and a description, both written by the same person (or the same agent), and they have to work out what the change was supposed to do before they can decide whether it does it. These are the changes that help, whatever tools you use:</p>
<ol>
<li><strong>No pull request without a stated requirement.</strong> Before work starts, write down what the change is for and how you'll know it works, precisely enough that a test can check it. The reviewer then checks the change against that, not against a guess.</li>
<li><strong>The author can't move the target.</strong> If the acceptance criteria live somewhere the author (or their agent) can't edit, nobody can make a change pass by quietly redefining "done".</li>
<li><strong>Proof means running checks.</strong> A test that exercises the behaviour, in CI. Not a script that prints a success message, and not the author's say-so.</li>
<li><strong>Cap the size.</strong> A change too big to review isn't reviewed, whoever or whatever wrote it. Split it, or send it back.</li>
<li><strong>The same gate for everyone.</strong> Human-written, agent-written, contractor or senior engineer: one set of checks. Then the argument stops being about who used AI, and becomes about whether the change does what was asked.</li>
</ol>
<h2>How we handle it</h2>
<p>At DynoKore most of our code is written by agents, so "someone else's AI output" is our normal day. We built DynoKore Keel so that what arrives for review is already anchored. Requirements, acceptance criteria and tasks are versioned records, and every change traces back to them. Tests must prove those acceptance criteria, and the coding agent can't change them. An independent reviewer agent checks every merge request, including any change to tests. A task can't be marked done until its requirement chain exists and passes Keel's readiness check.</p>
<p>We're a small team, and our "colleagues" are mostly agents. We haven't yet seen how this holds up across a large organisation with many human authors. That's what our design partners are for.</p>
<h2>The short version</h2>
<p>If AI-generated work keeps landing on you, the fix isn't reading harder. Agree what "done" means before the work starts, keep that definition out of the author's reach, and send back anything that doesn't prove it.</p>
<p><em>This is part of What Breaks, our series on what actually goes wrong when teams build with coding agents. If this matches what you're seeing, or doesn't, tell us at <a href="mailto:hello@dynokore.com">hello@dynokore.com</a>. We're opening DynoKore Keel to a small group of design partners: <a href="https://dynokore.ai">dynokore.ai</a>.</em></p>
]]></content:encoded></item></channel></rss>