Plans & tasks
The planner proposes; a person disposes; the ledger says what happened. Every dispatch writes one task — who ordered what, when, under which line, and what came of it.
Threats
GET /api/threats returns the current engagement state: assessed threats, the
active plan, and the candidates awaiting a decision. An operator's judgement enters
through designation:
curl -X POST https://<your-backend-host>/api/threats/designate \
-H "Content-Type: application/json" \
-d '{ "id": "T-00492", "verdict": "hostile", "by": "op-1" }'
verdict is hostile (this IS a threat: onto the board, engage-eligible under the
authority gate) or benign (this is not: resolved off the board, and it sticks,
scoped to this one object).
by is recorded but not checked against the credential — the caller's own identity
is on the session, and by is a label beside it. GET /api/threats/designations
lists the designations in force.
Designation keeps a route of its own rather than becoming an asset command, and deliberately: it writes an assertion about a track, not an order to an asset, and the affiliation ceiling is enforced on that write.
Tasks
Every dispatch writes exactly one. An operator's click, a plan's assignment, a route, an autonomous release: all of them leave a row, and a refusal leaves one too, carrying the refusing rule's own sentence.
curl "https://<your-backend-host>/api/tasks?actor=ast_6f0c1a3e…&status=refused"
curl https://<your-backend-host>/api/tasks/tsk_9b21c40e…
| Filter | Answers |
|---|---|
actor | What has this airframe been told? |
status | What was refused? What is still open? |
origin | operator, plan or autonomy — what did a human order? |
plan | Which tasks did this plan mint? |
object | What is tasked against this track? |
since | Everything from an instant onward. |
limit | At most this many, newest first. Clamped to 500. |
A task's fields:
| Field | Meaning |
|---|---|
id | tsk_… |
actor | The asset being told to do something. |
verb, params | The same pair a command takes. |
object | What it is addressed at — a track, or a place. Derived from params. |
origin | Who ordered it. autonomy carries the line that released it. |
level, gate | The rung and the ladder's answer, stamped at authoring. The dial moves during a run; "why did it ask me?" is answered by what the line said when the task was drawn. |
status | proposed → issued → executing → complete | refused | superseded | reverted |
reason | Why it was refused or reverted, verbatim. |
refused is a status, not a field beside one. The record this replaced left a
refused start reading assigned, which looks like "assigned and fine" to anything
reading status alone.
superseded is how a new order closes the one it replaces: a hold supersedes the
route it aborts, by id, with the replacement named. An effect supersedes another
effect against the same object, so two claims on one camera have a loser that says
why.
The veto window
An act the line releases with a stop window is a task in proposed with a clock on
it. Two routes bound it:
curl -X POST https://<your-backend-host>/api/tasks/tsk_9b21c40e…/seen
curl -X POST https://<your-backend-host>/api/tasks/tsk_9b21c40e…/stop
seen is load-bearing, not telemetry: a window that lapses unwatched does not
fire. The delegation is "you may act, and I can take it back", and nobody who never
saw it could. stop takes it back and settles the record as reverted. Both answer
409 when the window is no longer open — it already fired, or somebody else already
stopped it — which is not a success and is not reported as one.
Candidate plans
A plan is a set of tasks awaiting one approval decision. The decision loop publishes candidates; each waits for a verdict:
curl https://<your-backend-host>/api/plans
curl -X POST https://<your-backend-host>/api/plans/P-3/approve
curl -X POST https://<your-backend-host>/api/plans/P-3/reject
Approving mints one task per assignment, origin: { source: "plan", plan_id },
issued, and the order transport delivers each one to whatever holds the asset,
real or simulated.
What an assignment names
subject.asset_id is the id GET /api/assets returns, the one the registry minted. It
is not the serial an aircraft broadcasts and not its callsign, so a subject looks up in
the asset list directly and a client needs no second index to read a plan.
Resolving a broadcast serial to a registry id happens once, server-side, before a plan is authored. A cooperative track whose serial this workspace does not own is not in the fleet the planner draws from, so no plan can name it. Announcing a callsign is not enough: every cooperative track carries a do-not-shoot flag, an airliner included.
Approving part of one falls out of that. { "only": ["a1", "a2"] } keeps those
legs and supersedes the rest — four tasks issued and one superseded, naming the
approval that dropped it. A dropped leg used to leave no record at all.
| Status | When |
|---|---|
404 | The plan id is not in the current candidate set. Candidates are superseded on every replan, so a stale id is expected client behaviour to handle. |
409 | The line withholds an assignment's effect, or a partial approval named a leg the plan no longer carries. The error carries the reason. |
400 | A partial approval kept nothing. That is a rejection, and /reject is where it belongs. |
Watch the engagement_slice and plan events on
the stream rather than polling GET /api/plans;
candidates change on the planner's clock, not yours. POST /api/plans/run asks the
planner for a fresh pass (optionally { "variation_count": 3 }).
The autonomy line
There is no engagement mode. Four fixed points on a scale that is continuous became
one dial, and it rides GET /api/decision-config and
PUT /api/decision-config/autonomy — see
Configuration. The line is what every gate reads, and a task
records what it answered rather than re-asking it.
Tasking from the map
There is no map-tasking route. Sending the nearest free drone to a point is two calls, and they are the two things it is: a selection and a command.
curl "https://<your-backend-host>/api/assets?kind=vehicle&free=true&nearest=1.3521,103.8198"
curl -X POST https://<your-backend-host>/api/assets/ast_6f0c1a3e…/command \
-H "Content-Type: application/json" \
-d '{ "verb": "surveil",
"params": { "kind": "area", "lat": 1.3521, "lon": 103.8198, "radius_m": 400 } }'
The route this replaced did both in one call and reached the simulator directly, so clicking the map with a real airframe connected did nothing at all.