Date: 2026-04-09 Type: phase-update


What I was trying to achieve: closing the fourth plugin loop

Four plugin seams, four frameworks. Strategy got Drools forward-chaining rules. Economics got Quarkus Flow. Tactics got Drools plus a hand-rolled GOAP planner. Scouting was the last one, and the plan was Drools Fusion CEP — temporal event accumulation for build-order detection. Can we tell a Zerg roach rush from a Terran 3-rax by watching unit sightings accumulate over time?

BasicScoutingTask was already working: passive intel (army size, nearest threat) and an active probe scout. The new layer was the intelligence.

What we believed going in: Drools Fusion would handle the time windows

The natural path is window:time(3m) in the DRL, STREAM event processing mode, a KieSession with a PseudoClock. That’s the documented CEP approach.

I brought Claude in to work through the implementation. We hit the wall quickly: the drools-quarkus extension builds around the rule unit model — RuleUnitInstance, DataStore<T> — not traditional KieSession. STREAM mode needs kmodule.xml configuration that conflicts with how the extension compiles its KieBase. They don’t coexist cleanly.

Drools as accumulator, Java as timekeeper

So we pivoted. Instead of Drools managing the time windows, Java does. ScoutingSessionManager maintains three Deque<Event> buffers — unit first-seen events (3-minute window), army-near-base events (10-second window), expansion sightings (permanent). Each tick: evict expired events, build a fresh ScoutingRuleUnit from the current buffers, fire a RuleUnitInstance. The rules themselves are simple accumulators:

rule "Zerg Roach Rush"
when
    accumulate(
        /unitEvents[ this.type() == UnitType.ROACH ];
        $count : count();
        $count >= 6
    )
then
    detectedBuilds.add("ZERG_ROACH_RUSH");
end

The temporal logic stays in Java — easy to test, easy to reason about. The rules stay stateless. We lost the elegance of window:time() but gained a pattern that actually compiles.

One thing the code review caught: the first implementation only wrote ENEMY_BUILD_ORDER to the CaseFile when a build was detected — absent otherwise. That’s inconsistent with ENEMY_POSTURE (always written, with “UNKNOWN” as fallback) and with what producedKeys() advertised. Claude flagged it as the only Important issue. Fixed to always write with “UNKNOWN” as the fallback.

QuarkusMind

Four plugins done — and the project still had a placeholder name. “starcraft” was the repo folder, the Maven artifact, the package root. Fine for a testbed; wrong for something that’s becoming a platform.

I wanted a name that worked across races (we started Protoss, but that won’t stay true), signalled the tech stack, and wasn’t purely a StarCraft reference. After some back-and-forth: QuarkusMind. Quarkus in full — no abbreviation. Mind for the intelligence layer, the CaseHub blackboard, and a quiet nod to the Zerg OverMind that the multi-agent architecture has always been channelling.

The rename was a full refactor: org.acme.starcraftio.quarkmind, StarCraftCaseFileQuarkMindCaseFile, the GitHub repo, the Maven coordinates. SC2-specific code kept its SC2 references — the sc2 sub-package, @CaseType("starcraft-game"), replay paths. Those things are factually about StarCraft 2. Renaming them would just be wrong.

One gotcha from the rename: mv /starcraft /quarkmind exits 0 and the shell’s cwd becomes invalid — every subsequent bash call fails with “Working directory no longer exists.” Do the folder rename last.


<
Previous Post
Permuplate — Rename Redirect for Every Element Type
>
Next Post
What I was trying to achieve: a UI that looked like something I’d actually want to use