Building a Self-Healing QA Net
What happens when you ask an AI assistant to plan, build, debug, and verify a complete Petri net — without writing a single line of code yourself? You get a reusable QA validation system that checks your codebase, parks completed tokens, recycles them for the next run, and produces structured results an agent can analyze. Here is the full story.
AgenticOS supports command transitions — transitions that execute shell commands via the agentic-net-executor service. Combine that with the FOREACH execution mode and a recycling pattern, and you get something remarkable: a reusable QA validation net that fans out eight independent checks, collects results, and resets itself for the next run. No cron jobs, no CI pipeline YAML, no throwaway scripts. Just places, transitions, tokens, and arcs.
This article walks through the complete journey: from the initial fan-out/fan-in architecture through six bugs discovered during live debugging, to the final working system where every token flows exactly where it should.
The diagram above shows the complete architecture. Three places and two transitions form the core: p-cmd-queue holds the command tokens, t-execute-checks runs each one via bash, results fan into p-raw-results while the original commands park in p-cmd-done, and t-recycle moves them back to the queue for the next run. The dimmed elements (t-analyze, p-qa-report) are a planned agent phase that will produce structured QA reports automatically.
Phase 1: Planning the Architecture
The goal was straightforward: validate agentic-net-chat (the Telegram bot component) across eight independent quality checks — build, typecheck, dependencies, bundle size, shebang validation, source structure, module exports, and cross-package resolution. Each check is a single bash command that either passes (exit code 0) or fails.
The key insight was that these checks are embarrassingly parallel. Each command token is self-contained. No check depends on another. This maps perfectly to AgenticOS’s FOREACH execution mode, where the executor picks up one token at a time and processes it independently.
But there was a second requirement: reusability. Running QA once is useful. Running it again next week without reconfiguring anything is powerful. That meant the command tokens themselves needed to survive execution — not get consumed and lost. Hence the dual-emit and recycle pattern.
The Eight Command Tokens
Each token follows the CommandToken schema that the executor understands:
{
"kind": "command",
"id": "qa-chat-build",
"executor": "bash",
"command": "exec",
"args": {
"command": "cd /path/to/agentic-net-chat && npm run build 2>&1",
"timeoutMs": 30000
},
"expect": "text",
"meta": {"check": "build", "component": "agentic-net-chat"}
}
The eight checks cover the full quality surface:
| Token ID | Check | What It Validates |
|---|---|---|
qa-chat-build |
Build | TypeScript compiles without errors |
qa-chat-typecheck |
Typecheck | Type system consistency |
qa-chat-deps |
Dependencies | No missing or broken packages |
qa-chat-bundle |
Bundle Size | Output size within expectations |
qa-chat-shebang |
Shebang | Executable flag and hashbang line |
qa-chat-structure |
Structure | Required source files exist |
qa-chat-splitter |
Splitter | Message splitting module accessible |
qa-chat-cli-dep |
CLI Deps | Cross-package require resolution |
Phase 2: The Dual-Emit Pattern
A normal command transition consumes its input token and emits the result. But we needed the original command data to survive. The solution: two emit rules on the same transition.
The inscription uses two emit rules with different from expressions:
"emit": [
{"to": "results", "from": "@result", "when": "success"},
{"to": "done", "from": "@input.data"}
]
The first rule emits the execution result (stdout, exit code, timing) to p-raw-results — but only on success. The second rule is a catch-all with no when condition. It always fires, regardless of whether the command succeeded or failed. Crucially, it uses @input.data instead of @result, which means it emits the original command token data — not the execution output.
This gives us exactly what we need:
- Success: Two emissions — result to p-raw-results, original command to p-cmd-done
- Failure: One emission — original command to p-cmd-done (no result stored for failed checks)
- Either way: The original command data is preserved and ready for recycling
Phase 3: The Recycle Pattern
With completed command tokens parked in p-cmd-done, we need a way to move them back. The t-recycle transition is a simple pass-through that reads from p-cmd-done and writes back to p-cmd-queue:
{
"id": "t-recycle",
"kind": "pass",
"presets": {
"input": {
"placeId": "p-cmd-done",
"host": "autonomous-agent@localhost:8080",
"arcql": "FROM $ LIMIT 1",
"take": "FIRST",
"consume": true
}
},
"postsets": {
"output": {
"placeId": "p-cmd-queue",
"host": "autonomous-agent@localhost:8080"
}
},
"action": {"type": "pass"},
"emit": [{"to": "output", "from": "@input.data"}],
"mode": "FOREACH"
}
The @input.data expression in the emit rule is critical. It strips away the metadata envelope and emits only the original command data — the exact JSON that the executor expects. After recycling, the tokens in p-cmd-queue are indistinguishable from the originals. The net is ready for another QA run.
Phase 4: The Debugging Journey
Planning the architecture took an evening. Making it actually work took considerably longer. Six distinct bugs emerged during live testing, each one teaching something about the gap between designing a distributed execution system and running one.
Bug 1: Token Format Mismatch
Tokens stored as leaves in the tree have their data wrapped in {"value": "JSON_STRING"} format — a stringified JSON inside a value property. The executor’s CommandActionExecutor expected flat command token objects. Solution: Pattern C unwrapping in unwrapDoubleNestedData() that detects this format and parses the inner JSON string.
Bug 2: @input.data Not Resolved
The executor’s TransitionActionExecutor had no support for the @input.data expression in emit rules. Every from expression resolved to @result by default. The fix added a resolveFromExpression() method that understands @input.data, @input._meta, and falls back to @result for null or unrecognized expressions.
Bug 3: Catch-All Never Fired
The appliesOn() method in TransitionInscription.Emit was supposed to match emit rules to execution phases (success/error). But rules with no when condition were being skipped instead of matching everything. The fix: treat null or blank when as a catch-all that matches any phase.
Bug 4: Error Payloads Lost on Failure
The EmissionService checked only errorPayloads on the failure path. But catch-all emit rules (which match both phases) placed their data in successPayloads during the success evaluation. When a command failed, the catch-all payload existed in successPayloads but EmissionService never looked there. The fix merges both maps on the failure path.
Bug 5: Stale Executor Process
This was the most frustrating bug — not in code but in process management. All four code fixes were compiled, but the running executor process had been started hours earlier. Java loads classes at startup; recompiling .class files on disk does not affect a running JVM. The executor was happily running the pre-fix bytecode.
Additionally, the executor defaults to modelId=default via the EXECUTOR_MODEL_ID environment variable. The QA net was deployed under autonomous-agent. After restart with the correct model ID, all code fixes took effect.
Bug 6: Exit Code Ignored
The final bug was in BashCommandHandler.execute(). The method always returned CommandResult.success() from the try block, regardless of exit code. Non-zero exit codes were recorded in the output JSON ("success": false) but the CommandResult.Status was always SUCCESS. This meant failed commands were treated as successful by the emission logic. The fix checks the output’s success field and returns FAILED status for non-zero exit codes:
boolean commandSuccess = !output.has("success")
|| output.get("success").asBoolean(true);
if (commandSuccess) {
return CommandResult.success(token.id(), output, durationMs, ...);
} else {
int exitCode = output.get("exitCode").asInt(-1);
return new CommandResult(token.id(), Status.FAILED, output,
"Command exited with code " + exitCode, durationMs, ...);
}
Phase 5: End-to-End Verification
After all six fixes, the complete flow was verified token by token:
Seven of eight checks passed. The one “failure” (qa-chat-splitter) was actually expected — it tries to require() an ESM bundle, which correctly fails. That is a valid QA signal: the bundle is ESM-only by design.
After firing t-recycle, all eight command tokens moved back to p-cmd-queue with their original data intact. The net was ready for another run.
Why This Matters
This QA net demonstrates three architectural principles that make Agentic-Nets uniquely suited for operational automation:
1. Tokens carry meaning, not just data. A command token is not a row in a database table. It is a complete, self-describing unit of work that any executor can pick up and run. Move it to a different place, and it becomes a different thing — a pending check, a completed result, a parked command waiting for its next run.
2. The net remembers. Results accumulate in p-raw-results. Completed commands park in p-cmd-done. The entire state is browsable through the tree API. An AI agent — or a human with a Telegram bot — can query “what QA checks ran last time?” and get structured answers without touching a log file.
3. Recycle beats re-create. Traditional CI systems regenerate their configuration from YAML on every run. The recycle pattern keeps the actual work tokens alive, flowing through the net in a continuous loop. Add a timed transition, and you have a self-running QA system that checks your codebase every hour without any external scheduler.
The dual-emit pattern is the key. By emitting both the execution result and the original input data to separate places, a single transition serves two purposes — recording what happened and preserving what to do next. This is not possible in systems that conflate “consumed” with “gone.”
What Comes Next
The dimmed elements in the architecture diagram — t-analyze and p-qa-report — are the next step. An agent transition will read all accumulated results from p-raw-results, analyze pass/fail patterns, and produce a structured QA report as a token. That report can then flow into a Telegram notification, a dashboard place, or trigger further nets.
The complete QA validation net was planned, built, debugged, and verified in a single conversation with an AI assistant. No files were manually edited. No CI pipelines were configured. The entire system lives as tokens in places, connected by transitions — visible, queryable, and reusable.
That is what Agentic-Nets are for: turning conversations into infrastructure.