· DYNOKORE TEAM
We changed one rule mid-build. Within the hour, 22 tasks were created or amended.
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.
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.
What one line in CLAUDE.md wouldn't have done
If that rule had lived in a CLAUDE.md or AGENTS.md file, the edit would take a minute. Here is what the edit wouldn't do:
- tell the 15 tasks already written against the old rule that their done-when criteria were now wrong;
- stop a task that was ready to start from starting on the old pattern;
- convert the library code that had already merged;
- stop the next merge request from bringing the old pattern back.
An issue tracker helps with the first one, if someone goes through the tickets by hand. The other three need something else.
What happened instead
The decision went on the record first. 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.
Tasks changed in the same hour. 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.
The rule became a build check. 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.
Old code was converted from a list that could only shrink. 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.
The owner narrowed the rule three times. The first build rules read the ruling too strictly and refused any reference to java.sql. 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.
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.
Before your next rule change
- Put a decision in one place and point at it. 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.
- Turn the rule into a build check as early as you can. A rule that agents only read gets broken by the next agent that skims it.
- Convert old code from a list that can only shrink. A minimal version in 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 <(echo "$found") <(sort allowlist.txt))
gone=$(comm -13 <(echo "$found") <(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; }
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.
- Expect to narrow the rule. 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.
The record behind it
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. In Keel, 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.
The build check and the shrinking list live in DynoInvoice's own build. Keel holds the decision they enforce. The full timeline, with commits, is on When a decision changes.
Written by the DynoKore team with AI assistance. 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: dynokore.ai.