A prototype command console for manned-unmanned teaming (MUM-T), where one human stays in charge of several uncrewed aircraft. The scenarios are air defence of sovereign airspace over Berlin. The machines can never act on their own, a person approves or refuses every action that matters, and the console writes each decision to an AI decision log that a reviewer can read afterwards. When a wingman loses contact its weapon goes safe, and one action stands the whole element down.
Prototype · 4 scenarios · 5-ship element · 306 tests
At a glance
What
A command console that keeps one human in charge of several uncrewed aircraft. The operator assigns tasks to an element of Collaborative Combat Aircraft (CCA, uncrewed wingmen) screening a crewed fighter over Berlin, approves or refuses each request with a key press, and every decision lands in an AI decision log on disk. A wingman that loses its link goes weapons safe and flies itself home, and one action stands the whole element down.
Why it matters
European air forces are ordering uncrewed aircraft faster than anyone has designed the seat that controls them. The open question is whether a person can stay genuinely accountable for several machines under time pressure. INTENT is one answer: safe by default, a bounded approval window, and a replayable record that shows who decided what.
How it was built
A Python simulator with toy flight physics at 60 Hz, a Berlin map baked from Overture Maps, and a PettingZoo environment for multi-agent reinforcement learning (MARL, several agents learning together). The console, safety gate and decision log are my own code, implemented with Claude Code agents from my spec. A trained policy was set aside because a scripted pilot flew better.
The sixty second escort_transit sortie, recorded headless by the shipping app
(python main.py demo escort_transit --autopilot --record --headless).
Five uncrewed CCAs screen the crewed lead across Berlin against four waves of incoming fighters. The fourth wave appears inside minimum standoff range and has to be met close in. Every element on screen (map, aircraft, mission goals, engagement authorization, AI decision log) is drawn by the same code that renders the interactive window. The capture ends on the MISSION ACCOMPLISHED card, which appears only when every declared goal is met, and then on the after-action review with its decision timeline. Debrief: nothing ever fired at the human pilot, five of five uncrewed aircraft came home, all four waves were turned back.
Problem
One human, several uncrewed wingmen, time pressure.
As MUM-T concepts move from slideware into procurement, the flight profile is the easier half of the problem. The hard half is one operator tasking several aircraft at once, authorizing a weapon release within seconds, and leaving a message trail that survives an after-action review.
Existing MARL testbeds exist to train agents. The operator experience is an afterthought where it exists at all. A product team building the operator side needs the console prototyped on the same simulator that would train the autonomy, so one scenario replay exercises both. That is what INTENT is.
What it does
Four tasks, three postures, one human approval per action.
All four scenarios are defensive. The element escorts a transit and answers intruders that come to it, and nothing is engaged without a human approval inside the authorization window. INTENT turns operator intent into aircraft behaviour and keeps a record of every step. The operator selects a wingman or the whole element, assigns one of four tasks, sets the rules of engagement (ROE, the posture that says when a weapon may be used) and answers engagement requests with a key press. Scripted autonomy flies the aircraft, a deterministic safety gate bounds its actions, and mission goals declared in the scenario file are scored every frame and summarised in the after-action review.
Weapons are safe by default. A release happens only in the AUTHORIZED state of the positive human control (PHC) queue. Under WEAPONS TIGHT each request opens a 5 second operator window, under WEAPONS FREE the operator has pre-delegated release inside the safety gate, and every release is followed by a 2 second cooldown. multi_shield_europe/authorization.py
Four task primitives, ESCORT, SCREEN, INTERCEPT and RTB, plus an element-wide ROE posture HOLD, TIGHT or FREE, applied to one wingman or the whole element. multi_shield_europe/tasking.py
Every tasking, request, grant, denial, release, state change and safety intervention is one JSON line with step, class, source, destination and payload in logs/session_<ts>.jsonl. multi_shield_europe/messages.py
A deterministic safety gate clips actions before physics: a 100 px no-fly radius around the lead, a 1250 px leash, and a minimum firing distance near friendlies. multi_shield_europe/safety_gate.py
Every request explains itself. Under each pending request the console shows the range, the closure rate, the seconds until the intruder could detect the requesting wingman, and how many wingmen hold the track. The map draws the request as a line from the wingman to its target with the seconds left to decide, and the same reasons go into the decision log. multi_shield_europe/sensing.py, console/panels.py
Element abort. X pressed twice, or two clicks on the ABORT control, stands the whole element down in one action: every pending request is cancelled and every unused clearance revoked, the ROE goes straight to WEAPONS HOLD, and all five wingmen return to the human pilot. The shipped run measures every weapon safe in the same frame. main.py, multi_shield_europe/authorization.py
Lost-link procedure. A wingman that loses its link to the console goes weapons safe at once and raises no request while out of contact. It holds its last task for ten seconds, then flies home on its own authority, and the log records that as its decision, not the operator’s. When the link returns it waits for a fresh order before it counts as under control again. Orders sent while it is out of contact are logged as not delivered, and tracks only it can see are shown as stale. multi_shield_europe/datalink.py
Mission goals are declarative data in the scenario pack, evaluated every frame, shown live in the MISSION panel and replayed as a checklist with JSON export in the after-action review. multi_shield_europe/mission.py, console/aar.py
Solution
Console first, testbed underneath.
INTENT is an operator console (tasking, PHC engagement authorization, ROE cycling, AI decision log) on top of a PettingZoo ParallelEnv, the standard multi-agent simulation interface. The console attaches only in main.py. The training scripts import no console code, which I keep as a reviewed convention rather than a tested boundary.
The autonomy that flies the demo is scripted. A route pilot follows the scenario waypoints, a heuristic pilot changes behaviour with task and ROE, and the safety gate clips every action before physics. A 5M step PPO policy exists in the repo, regressed below the scripted pilot in training, and is never loaded by the demo. Four scenarios ship in the box, each with a ghost operator timeline that drives the console for replay. The --record flag streams frames straight into an H.264 encode, or an animated GIF where ffmpeg is absent, so the video above is produced by the app itself.
Scenario
escort_transit
West to east Berlin transit, four waves. Three are broken beyond the intruders’ own detection range. The fourth is a terrain-masked pop-up met at close range, with no standoff option left. 26 timeline entries.
Scenario
pincer
Two-axis pressure. Wingmen have to be re-tasked mid-mission and ROE cycling matters. 16 timeline entries.
Scenario
saturation
Enough tracks to exceed a single operator’s span of control. Stress test for the console. 19 timeline entries.
Scenario
contested_link
A wingman loses its link with a request pending and flies the lost-link procedure home. A late raid is turned back; the second raider runs for the border and the operator aborts rather than pursue. 18 timeline entries.
Result
The protected aircraft was never fired on, and a test asserts it.
In the shipped escort_transit run, five wingmen turn back four waves of incoming aircraft and the crewed lead is never fired on. tests/test_superiority_outcome.py runs the same headless demo path and asserts that outcome. A determinism test takes a fingerprint of 500 steps of trajectory with sha256, a standard way to tell whether two files are identical, and asserts the two runs match. The AI decision log of the shipped run holds 79 lines: 14 taskings and ROE changes, 15 engagement requests, 19 authorizations, 16 weapon releases and 15 state transitions. All five wingmen appear as the source of a request, an authorization or a release, because the whole element is under the console.
Human approved
16 / 16
Wingmen home
5 / 5
Shots at the protected lead
0
Decision log lines
79
Why a wingman cannot fire on its own. A weapon leaves the rail only from the AUTHORIZED state. Under WEAPONS TIGHT the operator has five seconds to answer each request, and silence returns the platform to SAFE.Why the outcome is emergent rather than scripted. The wingman sees the intruder at three times the range the intruder sees the wingman, so it can act first from standoff distance. The sensor model in sensing.py uses the radar range equation, so three of four waves break before they detect anything.
Contested link
Out of contact means weapons safe, and control comes back through a person.
In the contested_link run, CCA-2 loses its link to the console while its engagement request is waiting for an answer. The request is cancelled in the same step and its weapon stays safe for the whole outage. CCA-3 is tasked onto the track. After ten seconds out of contact CCA-2 turns for home on its own, and the log names CCA-2, not the operator, as the source of that decision. The link returns after 25 seconds. CCA-2 keeps flying home with its weapon safe until the operator gives it a new order. Near the end a two-ship raid comes in. CCA-3 asks to engage the lead raider, the operator clears it and the raider is turned back. The second raider breaks off and runs for the border while a wingman is still asking to engage it. Chasing it would take the element out of the corridor, so the operator aborts: the open request is cancelled in the same frame, every weapon goes safe, and a banner on the map says what happened and why. tests/test_contested_link_outcome.py runs the shipped headless loop and checks each of these claims against the decision log itself, and a second run must produce an identical log.
Releases out of contact
0
Time out of contact
25 s
Abort to all weapons safe
same frame
Human approved
10 / 10
The contested_link sortie, recorded the same way as the video above. Watch the CCA-2 card: LINK LOST with the seconds since contact, then the lost-link procedure, then LINK BACK with a prompt to re-task. Around 40 seconds the element fires on the lead raider of the late raid. At 45 seconds the abort is deliberate: the remaining raider is leaving, and following it out of the corridor is not the mission. Only the link to the console is modelled. The aircraft still share their sensor picture with each other.
The same console in its dark theme (--dark), six seconds in. Three wingmen ask to engage the same track. Each request is a line on the map with the seconds left to decide, and under the queue the console says why: 10.2 km, closing at 839 m/s, the intruder could see the wingman in 5 seconds, four wingmen hold the track.
Solution architecture
What it stands on, and what is new.
Inputs and datamap and scenarios
Overture Maps Berlinabout 119,000 building footprints baked into a 4096 px tactical map, fetched with DuckDB
scenario packsroutes, raid waves, link outages, ghost operator timeline and mission goals for four sorties, JSON
baked map and one scenario per run
Models and algorithmswhat flies and what sees
flight physics 60 Hztoy fixed-wing kinematics with turn-rate and bank limits
RCS sensor modelradar range equation on RCS, the radar cross-section that says how big a target looks to radar. Detection range scales with the fourth root of it
scripted pilottask-modulated heuristic pilot, waypoint pilot and ghost operator
SB3 PPO, shelvedStable-Baselines3, supersuit, PyTorch. PPO, a standard reinforcement learning method, trained for 5M steps, scored below the scripted pilot, never loaded by the demo
proposed actions per wingman per step
Engineown code, Python
PettingZoo envMissileDefenseEnv on the ParallelEnv API, the multi-agent simulation loop
safety gatedeterministic clip of every action before physics, logs interventions
PHC auth queueSAFE, ARMED, REQUESTED, AUTHORIZED, COOLDOWN per platform, with a forced-safe path for abort and lost link
C2 link modelscripted outages, the lost-link procedure and link status messages
C2 message busten command and control message classes modelled on STANAG-4586, a NATO messaging standard, ring buffer and JSONL sink
AI decision logone JSON line per event with step, class, source, destination, payload
after-action reviewdecision timeline from the log, goal checklist, protection, survival, interventions, JSON export
pygame-ce, ffmpegLGPL window and drawing, H.264 recorder with GIF fallback, headless SDL driver
Built in this projectThird party
Sources: Overture Maps data is CDLA-Permissive 2.0 with the buildings theme partly ODbL via OpenStreetMap, fetched with DuckDB (MIT). PettingZoo, Gymnasium, Stable-Baselines3 and supersuit are MIT, PyTorch is BSD, pygame-ce is LGPL, Pillow is HPND, ffmpeg is an optional LGPL or GPL binary. The C2 (command and control) message classes are modelled on the NATO STANAG-4586 groups and are not compliant with the standard. Everything else is this project.
Why it matters
The human stays accountable, and the record proves it.
Uncrewed aircraft are arriving in European air defence whether or not the control seat is ready, and the question that decides whether they are acceptable is who remains answerable for what they do. INTENT puts that question in code rather than in slides. The machine holds no authority of its own, the path to any consequential action runs through a person, and the log lets a reviewer reconstruct each decision and name who made it. It also covers the moment contact is lost: the weapon goes safe, and control comes back only through a human order. The saturation scenario also shows where one operator runs out of attention. That limit matters for product design as much as for engineering.
In five years
Consoles of this kind will sit between crewed aircraft and their uncrewed escorts in European air forces, and the message model will have to speak real command and control standards. Meaningful human control is already moving from principle into regulation, and the decision log is what turns the principle into something an auditor can check. INTENT is an early, toy scale version of that seat, with the human approval loop already in the right place.
How it was built
A sandbox, a failed policy, then the console.
Python
20,958lines
Tests
306
Commits
60
Build span
241days
Active days
13
Runtime deps
9+3 unlisted
Spec, PRD, product doctrine, thresholds and review by hand. Implementation with Claude Code agents. The console work is squashed into one commit. Measured from the repository.
The sandbox came first. Physics, the baked Berlin map and the PettingZoo environment landed in the first two days. Reinforcement learning followed with the RLlib wiring, SB3 PPO training and the safety gate around it. The trained policy regressed below the scripted pilot, so I shelved it and the project stood still for six months. When work resumed, it pivoted to the operator console, three scripted demo scenarios and the sensor model that makes the outcome emergent. A later pass put the whole five-ship element under the console and added the abort, the lost-link procedure and a fourth scenario to exercise them. A design pass after that made every request explain itself, reserved red for consequences, added a dark theme and a decision timeline, and fixed an abort that looked like a missed engagement on video because nothing on screen said why the wingmen held fire. The hardest part was making the demo falsifiable, so the superiority test drives the shipped demo path. Thrown away: the centralized critic claim and manual pilot mode as the main entry point.
Algorithms and mathFixed-wing kinematics with bank and turn-rate limits at 60 Hz, a radar range equation sensor model and a PHC state machine. No learning that worked.
Systems integrationOverture Maps through DuckDB, GeoJSON baked to a 4096 px PNG, PettingZoo and Gymnasium, SB3 and an RLlib wrapper, pygame-ce, ffmpeg piping and headless SDL in one loop.
Data and domainMUM-T doctrine, ROE postures, STANAG-4586 message groups and the radar equation drive the design of the PRD, the message bus and the sensor model.
Evaluation rigor306 tests, a sha256 determinism test, a superiority test on the shipped demo path and a contested-link test that checks its claims against the decision log, against a physics scale mismatch of 4 versus 5.8 m per px and an RL result evaluated only as regressed.
Interface and productA full operator console with tasking, an authorization queue that explains each request, element abort, link status, light and dark themes, event strip, mission panel, after-action review with JSON export, auto-director camera and mp4 recorder.
Difficulty 19 of 25 · AdvancedRelative to one competent engineer building it alone
Success criteria
When this counts as working.
Every authorization action is written to the AI decision log on disk with a step, ROE state, track ID and outcome. A reviewer can reconstruct the decision without asking the operator.
The ghost operator completes the escort, pincer, saturation and contested_link scenarios end to end without human intervention.
A wingman out of contact never releases a weapon, and the decision log shows it: zero releases between LINK LOST and LINK RESTORED.
One operator action makes every weapon on the element safe in the frame it is given.
The console attaches only in main.py and the training scripts import no console code. This is a reviewed convention with no test behind it yet.
The same code path renders the pygame window interactively and dumps frames headless under SDL_VIDEODRIVER=dummy.
Scenario replays are deterministic. Fixed seed, deterministic environment and a deterministic operator timeline give the same trajectory hash.
Metrics, KPIs, evals
Numbers from the shipped runs.
KPI
Value
What it means
Time to authorize
5 s window
From request to Y or N key press. Single key press authorization is a policy choice. It is fast and it makes the decision log entry unambiguous. An unanswered request expires to SAFE.
C2 message classes
10
Platform status, task assign and accept, engagement request, authorization and denial, weapon release, state transition, safety intervention and link status. Each one writes a JSON line to the AI decision log.
Scenario reproducibility
sha256 over 500 steps
Same seed, same scenario, same operator timeline gives the same trajectory hash. The decision log is identical except for the wall-clock timestamp field on each line, and the contested_link test asserts exactly that across two runs.
Headless recall
SDL dummy
Same code path renders the live window and the recording. The video above is one such capture, unedited.
Span of control
5 of 5
The console tasks and authorizes all five wingmen. Until this pass it reached only three, and orders to the other two were dropped without a trace. The saturation scenario stress-tests the limit.
Releases out of contact
0
Counted from the decision log in the contested_link run, not asserted. Two independent interlocks: the authorization queue holds a link-lost wingman safe, and the fire command checks link state again.
Abort to all weapons safe
same frame
Weapons first, then WEAPONS HOLD, then everyone home, so nothing can be released while the abort is being applied. A single X press only arms it: quit sits one key away.
Training decoupling
convention
The console lives in main.py and the training scripts import no console code. Reviewed on every change, with no test asserting the boundary.
What this does not do yet.
Fly a learned policy. A 5M step PPO run regressed below the scripted and random baselines and converged to leash boundary behaviour that the safety gate contained. The demo never loads it, and a fresh clone runs the heuristic pilot.
Networked C2 transport. The decision log is written locally, and a real deployment needs a wire protocol and a synced clock.
Radio and jamming model. Link loss is scripted per scenario: the outage, its timing and its length are data, with no radio propagation, latency or jamming behind them.
Operator ergonomics testing. The KPIs above are engineering KPIs and no user has sat in the seat, so there are no workload (NASA-TLX) or trust calibration trials yet.
Standard symbology. Map chips carry NATO APP-6 style affiliation frames, but the full symbol set is not implemented, and the track table still ranks by range rather than by explained threat.
Candidates for the next epic are theater expansion, RL rehabilitation and an intelligence layer. None is started.