Skip to main content

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…
FilterAnswers
actorWhat has this airframe been told?
statusWhat was refused? What is still open?
originoperator, plan or autonomy — what did a human order?
planWhich tasks did this plan mint?
objectWhat is tasked against this track?
sinceEverything from an instant onward.
limitAt most this many, newest first. Clamped to 500.

A task's fields:

FieldMeaning
idtsk_…
actorThe asset being told to do something.
verb, paramsThe same pair a command takes.
objectWhat it is addressed at — a track, or a place. Derived from params.
originWho ordered it. autonomy carries the line that released it.
level, gateThe 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.
statusproposed → issued → executing → complete | refused | superseded | reverted
reasonWhy 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.

StatusWhen
404The 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.
409The line withholds an assignment's effect, or a partial approval named a leg the plan no longer carries. The error carries the reason.
400A 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.