Agentic Loop Build Matrix#
The agentic loop cycles through a build matrix — a set of build configurations that each define a preset, a sanitizer, a build directory, and an optional test command. This ensures the loop doesn’t only test one build type but regularly exercises ASAN, TSan, profile, and release builds across both Windows (Stevedore) and Linux (Rancher Desktop).
Configuration#
The build matrix is defined in the project’s AgenticLoop.config.json under
the buildMatrix key. Each platform (windows, linux) has an array of
entries:
{
"buildMatrix": {
"windows": [
{
"name": "clangcl-debug",
"sanitizer": "asan",
"buildDir": "build-clangcl-debug",
"buildType": "Debug",
"testCommand": "ctest --test-dir build-clangcl-debug --output-on-failure -C Debug"
},
{
"name": "clangcl-profile",
"sanitizer": "none",
"buildDir": "build-clangcl-profile",
"buildType": "RelWithDebInfo",
"testCommand": "ctest --test-dir build-clangcl-profile --output-on-failure -C RelWithDebInfo"
},
{
"name": "clangcl-release",
"sanitizer": "none",
"buildDir": "build-clangcl-release",
"buildType": "Release",
"testCommand": null
}
],
"linux": [
{
"name": "linux-debug-asan-clang",
"sanitizer": "asan",
"buildDir": "build-asan-clang",
"buildType": "Debug",
"testCommand": "ctest --test-dir build-asan-clang --output-on-failure -C Debug"
},
{
"name": "linux-debug-tsan-clang",
"sanitizer": "tsan",
"buildDir": "build-tsan-clang",
"buildType": "Debug",
"testCommand": "ctest --test-dir build-tsan-clang --output-on-failure -C Debug"
},
{
"name": "linux-profile-clang",
"sanitizer": "none",
"buildDir": "build-profile-clang",
"buildType": "RelWithDebInfo",
"testCommand": "ctest --test-dir build-profile-clang --output-on-failure -C RelWithDebInfo"
},
{
"name": "linux-release-clang",
"sanitizer": "none",
"buildDir": "build-release-clang",
"buildType": "Release",
"testCommand": null
}
]
}
}
Entry Fields#
Field |
Type |
Description |
|---|---|---|
|
|
Handed to the build script: |
|
|
|
|
|
Linux |
|
|
CMake build type ( |
|
|
Shell command for tests. |
Backward Compatibility#
The module also accepts the legacy buildConfigurations format (arrays of
strings). Strings are normalized to entries with sanitizer: "none" and no
per-config test command. This means existing configs keep working without
changes.
Sanitizer-Aware Test Execution#
When a matrix entry has sanitizer: "asan" or sanitizer: "tsan", the
module automatically sets the appropriate environment variables before
running the test command, then restores the original environment:
Sanitizer |
Env Var |
Value |
|---|---|---|
|
|
|
|
|
|
|
— |
No env vars set |
This ensures sanitizer-instrumented tests actually catch memory errors and data races, rather than silently passing because the sanitizer was not configured to halt on error.
The variables are set in the loop’s own process. A testCommand that runs its
tests inside a container sees them only if it forwards them (-e ASAN_OPTIONS).
Windows ASAN Note#
On Windows, the ASAN debug binary needs clang_rt.asan_dynamic-x86_64.dll
next to the executable. Staging it is the consumer’s job, not the loop’s, and
it must be Microsoft’s runtime:
windows-clang-cl-sanitizers.md. The
ASAN_OPTIONS env var must use a relative log_path — an absolute
C:\... path breaks ASAN option parsing at the drive-letter colon. The
module’s default ASAN_OPTIONS does not set log_path, avoiding this
issue.
Full Matrix Sweep#
In addition to cycling one config per build trigger, the loop supports a full matrix sweep — a build that falls in every Nth iteration runs ALL configs in sequence instead of just the next one. This ensures all configs are exercised regularly, not just the one that happens to be next in the cycle. Executor-only mode never sweeps: its iteration counter stays at 0.
Enable it in the config:
{
"intervals": {
"fullMatrixEveryNIterations": 5
}
}
0= disabled (cycle one config per build trigger, the default)5= every 5th iteration, run all configs in sequence
Build Matrix Cycling#
Without a full sweep, the loop cycles through the matrix entries one at a
time. After every buildEveryNTasks completed tasks, the next entry in the
matrix is used:
Build # |
Windows |
Linux |
|---|---|---|
1 |
|
|
2 |
|
|
3 |
|
|
4 |
|
|
5 |
|
|
This ensures that over multiple build triggers, every config is exercised.
Cross-Platform Support#
The same config file works on both Windows and Linux. The PowerShell module
picks the windows or linux matrix from the host it runs on; the Bash loop
takes the platform as run_agentic_loop’s third argument (the template passes
linux). Each build is one call to a script the consumer owns:
Windows:
pwsh -File <build.windowsScript> -Configurations <name> -SkipTests, default scriptscripts/windows/Build-Windows-Container.ps1(a Stevedore container build over the hub’sWindowsContainerBuild.Reuse). What<name>means is that script’s business: aBuild-Windows.config.psd1entry in BeschleunigerBallett, aBuild-Windows.ps1preset alias in OmniAccelerANT.Linux:
bash <build.linuxScript> --preset <name> --build-dir <buildDir>. The PowerShell module defaults the script toscripts/linux/cmake-configure-build.sh; the Bash loop has no default, so set the key. For a CMake project that script can be a thin wrapper over the hub’slinux/scripts/lib/cmake-build.sh, whose--build-diroverrides the preset’sbinaryDirso each config gets its own build directory.
Module API#
PowerShell (WindowsAgenticLoop.Common.psm1)#
The PowerShell functions implementing this behaviour
(Resolve-BuildMatrixEntry, Get-SanitizerEnvVars,
Invoke-SanitizerTestCommand, and the high-level Invoke-AgenticLoop)
are documented in the module API reference,
windows-agentic-loop.md.
Bash (agentic-loop.sh)#
Function |
Purpose |
|---|---|
|
Echo env var assignment string for the sanitizer |
|
Set env vars, run tests, restore env |
|
Parse a matrix entry to globals |
|
Count matrix entries for a platform |
|
Get entry name by index (backward-compatible) |
|
Full loop with build matrix, sanitizer tests, quality gates |
|
Phase 1 of an iteration: skip while actionable tasks pend, starvation guard, refactor-cycle prompt; reports whether it ran |
The two bash files#
agentic-loop.sh is the only file a consumer sources; it sources
agentic-engines.sh from its own directory on load. The split follows the one
seam the file had: which agent to run and how to talk to it versus what to
run it on.
File |
Owns |
|---|---|
|
|
|
|
The dependency points one way only. The engine half calls log and appends to
LOG_FILE, both defined by the loop half before it sources the engines — so
agentic-engines.sh is a half of one library, not a standalone one. It is
therefore skipped by test-lib-modules.sh for the same reason
agentic-loop.sh always was: neither is a lib/ module that must reach
01-core/logging.sh and define info/warn/err. test-lib-smoke.sh still
covers both (parses, sources clean under set -euo pipefail, defines
functions), and test-agentic-loop.sh drives the seam from the loop side: it
sources agentic-loop.sh only, and its config-precedence and invoke_agent
cases land in the engine half through it.
Two engines are supported: opencode invokes
opencode run --agent <role> --model <model> --standalone (plus --auto for
the executor; opencode v2 only, v1 is refused:
opencode v2); claude invokes
claude -p --model <model> with the role system prompt appended from the
configured prompt file.
Role-prompt composition#
resolve_role_prompt_file in lib/agentic-engines.sh is the Bash twin of the
PowerShell module’s New-AgenticComposedPrompt / Write-AgenticOpenCodeAgentFile.
load_engine_config calls it for both roles on every engine, which closes two
gaps that shared one root cause — the composition only ever existed on the
claude command line:
The Bash half read
.engines.claude.<role>PromptFileand nothing else, so a consumer that had migrated to the preferred overlay shape got no role prompt at all on Linux — and got it silently, becauseinvoke_claudeonly warns about a prompt file it was given and could not read.opencodetakes no system-prompt file on its command line; it reads an agent’s role prompt from.opencode/agents/<role>.mdin the repo. Nothing wrote that file, so everyopencodeconsumer hand-maintained its own copy of the shared role prompt, and one of them ended up holding two full copies that had drifted 271 lines apart (seeNew-AgenticComposedPromptinWindowsAgenticLoop.Common.psm1).
Hence three branches, one invariant: .opencode/agents/<role>.md is written on
every one of them, so opencode is never left without a role prompt. The two
engines get the same text whenever a prompt is configured; with none, claude
gets no role prompt and opencode the shared one.
Config shape |
|
|
|---|---|---|
|
shared role prompt + overlay, composed into |
the same composed text |
|
that file |
that file, labelled “migrate it to an overlay” |
neither configured |
nothing |
|
The generated file carries a DO-NOT-EDIT header, is rewritten only when its
content changed, and is written under DRY_RUN too — it is a derived artefact,
and a dry run whose point is checking the prompt wiring has to produce it. If the
consumer’s .gitignore does not exclude .opencode/agents/, the run warns:
a committed copy is how the prompts forked in the first place.
Prompt and overlay paths are stored repo-relative in the config file;
_agentic_repo_path anchors a relative one at <repo_root> before any of the
above runs, and leaves an absolute one alone.
Environment overrides#
All optional; each one beats the value in the config JSON.
Variable |
Effect |
|---|---|
|
|
|
overrides the planner model id |
|
overrides the executor model id |
|
|
|
skip that phase |
|
single-phase mode |
|
overrides |
Typical use from a project’s Run-AgenticLoop.sh (the full copy-and-edit
wrapper is shared/agentic-loop/templates/Run-AgenticLoop.sh):
source "${REPO_ROOT}/third_party/ANTfrastructure/linux/scripts/lib/agentic-loop.sh"
init_agentic_loop "MyProject" "$REPO_ROOT"
run_agentic_loop "$CONFIG_PATH" "$REPO_ROOT" linux