Become a member!

Delphi AI Skills 0.3.0: the language, and the code audit

🌐
This article is also available in other languages:
🇮🇹 Italiano  •  🇪🇸 Español  •  🇩🇪 Deutsch  •  🇧🇷 Português

delphi-ai-skills 0.3.0 brings the generic Delphi skills promised at launch: ten open source skills that teach Claude Code, Codex, Cursor and Gemini the language and the RTL, and how to audit a unit nobody has looked at in eight years.

Logo of the open source project delphi-ai-skills: skills for AI coding agents dedicated to Delphi and DelphiMVCFramework.

The last paragraph of the first announcement, back in July, said that the seven skills of the time were deliberately vertical on DelphiMVCFramework, that this was the starting point and not the destination, and that the ones coming next would be about Delphi as a platform. 0.3.0 is the first part of that missing piece.

There are ten skills now. Two of the three new ones have nothing to do with DMVCFramework: they apply to any Delphi code at all, a VCL form, a Windows service, a library, a unit nobody has touched since 2004.

In short

  • delphi: the language and the RTL, assuming no framework and no project layout: version gating, memory and lifetime, strings, exceptions, generics, threading.
  • delphi-code-smells: the audit pass over the code you already have. Compiler warnings, memory leaks, access violations, double frees, and for every defect the way to find it.
  • dmvcframework-jsonrpc: JSON-RPC 2.0, from publishing the class to the IMVCJSONRPCExecutor client.
  • Still Apache-2.0, still plain Markdown, still on Claude Code, Codex, Cursor, Gemini CLI and any agent able to read a file.
  • Repository: github.com/danieleteti/delphi-ai-skills

The delphi skill: the one that assumes nothing

An AI agent has read far more C# and TypeScript than Object Pascal. The result is not obviously wrong code: it is code that looks like Delphi. The begin keywords are in the right places, the capitalisation is right, but inside there is an s[0] to read the first character, a try ... except ... finally ... end in a single block, a multiline string that does not exist on Delphi 11, and other noise arriving from other languages.

The delphi skill covers exactly that layer, and the part that pays off first is version gating. The model does not know which version you compile on, and within the same unit it mixes different eras without noticing: a syntax that arrived with Florence sitting next to an idiom people wrote in 2004.

// Delphi 12 Athens and later. On 11 Alexandria it does not compile.
var lSql := '''
  select * from customers
  ''';

// Delphi 13 Florence and later: if-then-else as an expression.
X := if Left < 100 then 22 else 45;

The skill carries the table of CompilerVersion values release by release, and before that the rule that makes it useful: the target version is established, not assumed. The agent asks the compiler, because dcc32.exe --version prints exactly the CompilerVersion it needs; if that does not work, it looks at which Studios are installed; and if it finds more than one, or none, it asks a single question: 11 Alexandria, 12 Athens or 13 Florence? The .dproj is not an acceptable answer, because <ProjectVersion> is the version of the project file format, not of the product, and a project last saved by an old IDE opens unchanged in a new one.

From there on it adapts. On a confirmed 13 Florence, the if expression is simply used, and wrapping it in a {$IF} is noise. On 11 it must not appear at all. And when the answer does not arrive, the skill writes for 11 Alexandria (the minimum it assumes), puts in the explicit guard and tells you it did, instead of letting you discover the choice at compile time.

{$IF CompilerVersion >= 36}    // Delphi 12 Athens and later
  ...
{$ENDIF}

Then there is the catalogue of the mistakes an LLM makes in Delphi with an almost touching regularity, each one with the right shape next to it and the reason why. A few lines, to give the idea:

Wrong Right Why
s[0] for the first character s[1], or s.Chars[0] string is 1-based, but TStringHelper (Chars, IndexOf, Substring) is compiled 0-based. Two indexing bases in the same type.
return X; Result := X;, or Exit(X) Result is an implicit variable, not a statement. Read before being assigned, it hands back garbage.
try ... except ... finally ... end nest them A single block cannot have both. It is a syntax error, not a preference.
with lObj do ... a local variable with masks identifiers: a field added to lObj six months later takes over a name from the enclosing scope, silently, and compiles.
procedure Foo(AText: string) procedure Foo(const AText: string) On a managed type, const avoids a refcount and a copy.
TStringList.Create hoping it will free the Objects[] TStringList.Create(True) The two containers have opposite defaults: TObjectList<T> owns, TStringList does not.

The bulk of the material sits in six reference/ files that the agent opens only when the task calls for it: memory and lifetime, strings and encodings, exceptions, generics and RTTI, concurrency, style. The context window stays free until it is really needed, which is the reason a skill is a file on disk and not a block pasted at the top of the chat.

Everything was copied from the RTL/VCL sources installed on disk, or confirmed on the docwiki. The skill also knows it can be incomplete, and when it needs a signature it does not have, it has the order of operations written inside it: first read the source, then the docwiki, and if it is still not sure, it says so. I am not certain that TFoo.Bar exists, check in System.Classes.pas is a useful answer. A confident wrong one is not.

And it reads the right tree: if you compile with 12 Athens, the check has to happen in Studio\23.0\source\rtl, not in the copy of the same file that sits under 37.0. The RTL grows release after release, and a type or an overload that exists in Florence may simply not exist in Athens. The same goes for what the skills bring with them: the DUnitX container you use to make a build fail on a memory leak is called TDUnitXServiceLocator in the version shipped with 13 Florence and TDUnitXIoC in the one shipped with 12 Athens, with the exact same call underneath. A detail like that is not something you remember, it is something you go and read.

A declaration, though, is only half the answer. It tells you how many parameters are needed and of what type, and says nothing about the rest: who frees what, in what order things must be called, what the idiomatic shape is. For that you need a real call site, and the skills look for one in this order: your code first, which also brings along the house conventions to follow, then the sources themselves (the RTL uses its own APIs constantly, so a grep returns working examples instead of documentation prose), then the framework samples or the docwiki.

And if the agent does not know where those sources are, the rule is to ask instead of guessing. The answer is not lost at the end of the session, though: after checking that the path exists, the agent offers to write it into the instruction file it already reads, the project’s CLAUDE.md or the AGENTS.md, in a block of its own:

<!-- delphi-local-sources -->
Delphi RTL/VCL source: C:\Program Files (x86)\Embarcadero\Studio\23.0\source   (12 Athens, CompilerVersion 36.0)
DelphiMVCFramework checkout: C:\DEV\dmvcframework   (sources/ + samples/)
<!-- /delphi-local-sources -->

That block is the first thing the agent looks at when the session starts, and the question gets asked once per project instead of once a day. If a path no longer exists, or you change Delphi version, it tells you and asks again: it would rather admit it does not know than dredge something up from memory.

The delphi-code-smells skill: the machine first, the opinion after

The other half of the job is not writing code, it is looking at the code that is already there. This is where agents behave worst, and not because they get things wrong: because they are polite. Ask for a review and you get a page of observations about variable names, the order of the uses clause and the length of the methods. Zero leaks. A review that returns fifteen style notes and no lifetime problem is not a review, it is an opinion.

delphi-code-smells imposes an order of attack, and the first two points involve no human judgement at all:

  1. Compile with warnings and hints on, and read every line of the output. Free, objective, and with a higher yield than anything else you can do.
  2. Run with ReportMemoryLeaksOnShutdown := True, and if there is a test suite, run that too.
  3. Ownership by hand: every Create in the unit, who frees it, on which paths, including the one that raises.
  4. Exception handling: every except without an on, every empty handler, every try/except that meant to be a try/finally.
  5. Concurrency: everything reachable from a TThread.Execute or from the body of a TTask.Run.
  6. The rest. Naming, with, long methods, magic numbers.

The rule holding the list together is written into the skill in a fairly undiplomatic way: a leak the compiler cannot see beats a naming convention, always. And every defect must be reported with what it costs at runtime, not with how ugly it is to read.

Defect number one in the ranking is this one, and I still see it every week during my consulting work:

// WRONG: the constructor is INSIDE the try
try
  lList := TStringList.Create;
  ...
finally
  lList.Free;   // if Create raises, this calls Destroy on uninitialised memory
end;

// RIGHT
lList := TStringList.Create;
try
  ...
finally
  lList.Free;   // .Free is nil-safe: "if x <> nil then x.Free" is noise
end;

Two lines swapped around. The first version produces an access violation only when the constructor fails, which is to say almost never, which is to say on a Tuesday morning at the customer who has not restarted the server in eight months.

The skill also brings the five-minute pass: a handful of greps ordered by yield, from the constructor inside the try to the exception swallowed by an except end, from the FreeAndNil on a local to FreeOnTerminate. These are the checks a human never feels like doing and a machine does in three seconds.

rg -n -U 'try\b[^;]*?\n\s*\w+\s*:=\s*T\w+\.Create'   # constructor INSIDE the try
rg -n -P '(?s)except\s*(//[^\n]*\n\s*)*end'          # swallowed exception
rg -n 'FreeOnTerminate'                              # thread lifetime

Then there is the part that makes the audit repeatable instead of episodic: how you actually read the memory manager report, when RegisterExpectedMemoryLeak is needed, and how to configure DUnitX so that a leak fails the build. A leak found once is a day of work. A leak that breaks the pipeline from tomorrow on is a class of bug closed.

One last rule, my favourite because it holds for people too: never report a smell you cannot say how to find. If there is no warning code, no tool and no grep to prove it, it is a preference, and nobody asked for it.

And then JSON-RPC

The third new skill, dmvcframework-jsonrpc, comes back inside the framework’s perimeter: a JSON-RPC 2.0 endpoint in DelphiMVCFramework is an ordinary Delphi class published on a URL segment, with no per-method routing attributes. The skill covers what is callable and what is not, the distinction between function (request) and procedure (notification), named and positional parameters, the three ownership rules about who frees what, the error codes, the hooks, and the IMVCJSONRPCExecutor client for calling that endpoint from Delphi.

The ten skills, in one table

Skill What it covers
delphi The language and the RTL: version gating, inline var, memory and lifetime, strings and encodings, exceptions, System.Generics.Collections, RTTI, threading, conventions.
delphi-code-smells The review: warnings and hints that indicate a real bug, warnings-as-errors, leak hunting, how to make a build fail on a leak, static analysis, a catalogue of smells with the way to find them.
dmvcframework The core of the framework: bootstrap and engine, controllers and functional actions, routing, IMVCResponse, ownership, the ActiveRecord ORM, Repository, the DI container, validation, middleware, JWT, SSE, dotEnv.
dmvcframework-minimal-api Routes as anonymous methods: route groups (Prefix, MapGet, MapPost), type-driven binding, uploads, endpoint filters and HTTP filters, .AsWeb.
dmvcframework-webapp Server-side web apps: TemplatePro, template inheritance, fragments, ViewData, cookie/JWT login, static files, the HTMX helpers on the Delphi side.
dmvcframework-ui The presentation layer the wizard generates: Bootstrap 5.3, baselayout.html, the tokens in style.css, dark mode, toasts.
dmvcframework-security Server-side secure coding: access control and IDOR, mass assignment, SQL injection, XSS in TemplatePro, CSRF, path traversal, uploads, SSRF, headers, JWT hardening, secrets.
dmvcframework-jsonrpc JSON-RPC 2.0: publishing, requests and notifications, parameters, ownership, errors, hooks, client.
dmvcframework-testing DUnitX, in-process IMVCServer, IMVCRESTClient, CRUD, authentication and authorization tests, database fixtures.
htmx-skill The index of every page of the official htmx.org documentation, so the agent reads the right page instead of remembering htmx its own way.

The two Delphi skills have no requirements: no project layout, no framework. The seven DMVCFramework ones start from a project created with the IDE wizard, and dmvcframework-security is pulled in automatically by any endpoint that receives input from a client.

How to install them and how to use them

git clone https://github.com/danieleteti/delphi-ai-skills.git
cd delphi-ai-skills
install_in_claude.bat

For Claude Code that is the end of it: the skills are discovered on their own. For the other agents there are install_in_codex.bat, install_in_cursor.bat and install_in_gemini.bat, which copy the skills and write the pointer into the right instruction file (AGENTS.md, .cursor/rules/*.mdc, GEMINI.md), because those agents do not discover them by themselves. Each script accepts a project path, if you prefer to version the skills in the repository and give them to the whole team:

install_in_claude.bat C:\DEV\my-project

You do not “call” a skill: you describe the task and the agent picks. The strongest trigger is naming the technology. “Find the leak” is ambiguous, “find the leak in this Delphi unit” is not.

Does this unit compile on Delphi 11, or am I using syntax that only exists from Athens on? This service leaks memory over a few days: find the spot. Review this unit and tell me what is actually wrong with it, not the style. Which compiler warnings am I ignoring that hide a real bug? Make the build fail when there is a memory leak. This code runs in a thread and touches a VCL label: what is wrong with it? TObjectList<T> with OwnsObjects, or a plain TList<T>?

When you want the guarantee, you name the skill and that is it: on Claude Code with /delphi or /delphi-code-smells, on the others by citing the path (“read skills/delphi-code-smells/SKILL.md, then review this unit”). It works everywhere, because it is a text file, not a function.

One piece of advice worth the ten seconds it costs: if you have the DelphiMVCFramework sources on disk, tell the agent at the start of the session (“the DMVCFramework sources are in C:\DEV\dmvcframework”). The skills are instructed to verify instead of guessing, and with the sources within reading distance they verify much better.

Why the skills cannot contain an invented name

There is a basic problem in a project whose only purpose is to stop an agent from inventing API names: if the one inventing a name is the skill, the damage is worse than before, because now the mistake has the authoritative air of documentation.

That is why the skills do not ship unless they get through a pipeline that compares them with the real code: the DelphiMVCFramework sources and the RTL of every supported Delphi version. Every name mentioned must be a name that exists in there. If even one is missing, the release stops, and it is not an opinion you can argue about in review: either the name is in the sources or it is not in the skills.

That is the check that surfaced the DUnitX defect on 12 Athens described above, and it surfaced it here instead of at your place, which is exactly the point.

What a check like that cannot tell you is whether the API is used well: for that you need a compiler, and for an ownership mistake you need to run the program. It is the first of the three levels the skills are checked with, and the one that costs so little it can stay on all the time.

Material and videos on Patreon

The skills tell the agent what is true. What stays outside is the how: what a real working session looks like, where it pays to stop, what to ask and in what order, when the agent is heading down a road that will cost you two hours.

Over the coming weeks I will publish a fair amount of material, written and on video, on the DelphiMVCFramework Patreon page: whole sessions on Delphi and on DMVCFramework with the skills at work, with the points where something goes wrong left in, because those are the part you learn from.

The repository stays Apache-2.0 and complete: nothing you need in order to use the skills is behind a subscription. Patreon is where the explanatory material lives, and it is also the way anyone who wants to can support the work on DMVCFramework and on everything around it. If you find it useful, that is the channel. If you would rather take the skills and go your own way, that is perfectly fine too: it is exactly why they are on GitHub.

Frequently asked questions about delphi-ai-skills 0.3.0

What is new in delphi-ai-skills 0.3.0? Three more skills than the first release, ten in total: delphi (the language and the RTL, with no framework), delphi-code-smells (the review of existing code) and dmvcframework-jsonrpc (JSON-RPC 2.0). 0.3.0 also corrects two wrong statements in the memory reference of the delphi skill.

Do I have to use DelphiMVCFramework to use these skills? No, and that was the plan from the first announcement. delphi and delphi-code-smells assume no framework and no project layout: they apply to a VCL form, a Windows service, a library, a legacy unit. The other seven remain specific to DelphiMVCFramework.

What does the Delphi code audit skill do? delphi-code-smells puts the review in the right order: the compiler first, with warnings and hints on, then a run with ReportMemoryLeaksOnShutdown := True, then ownership by hand, exceptions, concurrency and only at the end style. It covers the warnings that indicate a real bug (with the code, for example W1035), $WARN and warnings-as-errors, how to read a leak report, how to make a build fail on a leak with DUnitX, third-party static analysers, and a catalogue of defects where every entry says what it costs at runtime and how to find it.

Do the skills handle security? On the server side yes, and it is dmvcframework-security: a mandatory dependency of all the DMVCFramework skills, applied to any endpoint that receives input from a client. It covers access control and IDOR, mass assignment, SQL injection, XSS in TemplatePro, CSRF, path traversal, uploads, SSRF and open redirect, security headers, JWT hardening and secret management. delphi-code-smells, on the other hand, is a defect audit, not an OWASP audit: it deals with leaks, access violations, double frees and silently wrong results.

Which Delphi version do they work on? On 11 Alexandria, 12 Athens and 13 Florence. The skill establishes which one you have: it asks the compiler with dcc32.exe --version, failing that it looks at which Studios are installed, and if it is still ambiguous it asks you which one you use, instead of guessing. With the version known, it adapts. When the answer does not arrive, the target is Delphi 11 Alexandria, with an {$IF CompilerVersion >= ...} guard around everything that depends on the version. The content of the skill is verified against the RTL/VCL sources of Delphi 13 Florence. The DMVCFramework skills target 3.5.0 (silicon).

Which AI agents do they work with? Claude Code, Codex, Cursor, Gemini CLI, Windsurf, Continue and any agent able to read instructions in Markdown. The repository includes the installation script for the first four. On agents other than Claude Code, loading depends on how much that agent respects its own instruction file, so it pays to name the technology in the prompt, or the skill itself.

How do I know the skills do not contain invented API names? Because they cannot. Before every release, a pipeline compares every name mentioned in the skills with the real code: the DelphiMVCFramework sources and the RTL of the supported Delphi versions. If a single identifier does not exist in there, that release does not ship. It is not a careful re-read by someone competent, it is a gate: the same discipline the skills impose on the agent, applied to the skills. In practice, when a skill states something about an API, that statement has already been checked against the code instead of against somebody’s memory. What the check does not prove is that the API is used well: for that you need the compiler and a run, which are the two levels above.

The road ahead

We are still in 0.x, and the shape of the set is to be considered unstable: skills may be split, merged, renamed or removed as real usage shows what is actually needed. 1.0.0 will arrive once the set has proven itself on enough real projects. In the meantime, reports are worth more than any roadmap: if a skill made you write wrong code, that is the defect I want to see first.

There is still only one criterion for contributing: every statement must be verifiable against the sources, citing the file. No API name written from memory. It is the discipline we demand from the agents, and it would be odd not to apply it to ourselves.

The project is here: github.com/danieleteti/delphi-ai-skills. Issues and pull requests are welcome.


An agent that writes Delphi without knowing Delphi produces code that compiles badly and ages worse. An instructed agent produces code you can read two years from now without wondering who wrote it. The difference, for now, is ten Markdown files.

Comments

comments powered by Disqus