Building a Self-Healing QA Net — From Idea to Execution in One Conversation

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.

QA Validation Petri Net — Fan-Out / Fan-In with Recycle p-cmd queue 8 tokens t-execute checks CMD p-raw results p-cmd done @result when:success @input.data catch-all t-recycle PASS recycle: @input.data preserves original command t-analyze AGNT p-qa report Legend: Place Transition Recycle arc Future (agent)

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.

Dual-Emit Pattern: Results + Done Parking Command Token kind: “command” executor: “bash” id: “qa-chat-build” t-execute checks FOREACH consume Execution Result exitCode: 0 stdout: “Build OK…” success: true Original Command kind: “command” executor: “bash” Preserved for recycle emit 1 emit 2 from: @result when: “success” from: @input.data when: (catch-all)

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.

Six Bugs on the Path to Working Execution 1 Token Format Mismatch Executor received nested {value: “JSON_STRING”} instead of flat command token data Fix: Pattern C unwrapping 2 @input.data Not Resolved Executor had no support for @input.data in emit rules — everything resolved to @result Fix: resolveFromExpression() 3 Catch-All Never Fired Emit rules with no “when” condition were skipped instead of matching every phase Fix: appliesOn() semantics 4 Error Payloads Lost On failure, EmissionService only checked errorPayloads but catch-all was in successPayloads Fix: Merge both maps on failure 5 Stale Executor Process All code fixes compiled but running executor was started before patches — old bytecode Fix: Kill and restart with env 6 Exit Code Ignored BashCommandHandler always returned SUCCESS — exit code was in output JSON but not status Fix: Check output.success field Discovery order during live debugging session Code fixes (Bugs 1-4) Process restart (Bug 5) Final fix (Bug 6) All working

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:

End-to-End Token Flow: Verified p-cmd-queue 8 fire t-execute FOREACH p-raw-results 7 p-cmd-done 8 t-recycle FOREACH queue 8 Verified Results: 7 successful checks (exit 0) build, typecheck, deps, bundle, shebang, structure, cli-dep 1 expected failure (exit 1) splitter: ESM bundle not require()-able (expected behavior) 8 tokens parked in p-cmd-done All original command data preserved (verified via tree API) 8 tokens recycled back to queue Original command JSON intact — ready for next QA run

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.

Leave a Reply

Your email address will not be published. Required fields are marked *