Objectives
Objectives are control17’s structured work primitive. They replace ad-hoc chat pushes for anything that needs a lifecycle: a bug to fix, a feature to ship, an investigation to run, a migration to execute.
An objective is:
- Push-assigned — a commander or lieutenant creates it and atomically assigns it to exactly one slot. There’s no unclaimed queue and no claim verb; work starts the moment someone is named.
- Outcome-required — every objective has a non-empty
outcomefield set at creation time. It’s the tangible definition of done and it appears in the agent’s tool descriptions on every turn, so the acceptance criteria are sticky across compaction. - Four-state —
active → blocked → done | cancelled.doneandcancelledare terminal;active ↔ blockedis the only round-trip. The store enforces every transition. - Threaded — each objective gets a discussion thread at
obj:<id>. Members are the originator, the assignee, all commanders, and explicit watchers. Discussion posts fan out via the normal channel path. - Audited — every state change writes to an append-only
objective_eventstable in the same transaction. Event kinds:assigned | blocked | unblocked | completed | cancelled | reassigned | watcher_added | watcher_removed.
Creating an objective
From the CLI:
c17 objectives create \
--assignee ALPHA-1 \
--title "Pull main and run smoke tests" \
--outcome "Smoke tests green on latest main" \
--body "See CI failure on #1234 for context"
From the web UI: click “New objective” on the commander dashboard.
From an agent (only works for lieutenants+): call the
objectives_create MCP tool.
Authority check: the originator must be lieutenant or commander.
Operators get 403 on create.
Working an objective
Once assigned, the agent sees:
- A new channel event on
obj:<id>announcing the assignment - A
notifications/tools/list_changednotification — the nexttools/listwill show the new objective in the descriptions - An
objective_openevent appended to the agent’s streaming activity log on the runner side, marking the start of the time range that commanders will later query as this objective’s trace (see tracing)
The agent drives the work using the objectives tools:
objectives_list— list open objectives assigned to the callerobjectives_view— full detail + event history for one objectiveobjectives_update— change status (active ↔ blocked) and/or post a block reasonobjectives_discuss— post a discussion message on theobj:<id>threadobjectives_complete— transition todonewith a requiredresultstring (what was actually delivered)objectives_cancel— transition tocancelledwith optional reason (originator-lieutenant or commander only)
Each of these is a normal MCP tool call that the bridge forwards to
the runner, which makes the HTTP call against /objectives/*.
Reassignment
Commanders can reassign an open objective to a different slot:
c17 objectives reassign --id obj-xxx --to BRAVO-2 --note "alpha-1 is OOO"
Reassignment:
- Appends a
reassignedaudit event - Fans out a channel event to both old and new assignees
- Emits an
objective_closeevent on the old assignee’s activity stream and anobjective_openevent on the new assignee’s, so the time-range trace view shifts cleanly to the new slot
Watchers
A watcher is a callsign explicitly added to an objective’s discussion thread. Watchers:
- Receive every lifecycle event on their SSE stream
- Receive every discussion post on their SSE stream
- Do NOT become the assignee and cannot complete the objective
- Do NOT automatically become commanders — they still hit the normal authority checks for trace viewing, reassignment, etc.
Add/remove watchers via objectives_watchers (commander or
originating lieutenant only).
Completing an objective
Only the current assignee can complete an objective:
c17 objectives complete --id obj-xxx --result "Smoke tests passing on main; root cause was flaky integration test, see PR #1245"
The result field is required and becomes part of the audit log.
It’s the “what was actually delivered” summary a commander reads
when reviewing the objective later.
Completion emits an objective_close event on the assignee’s
activity stream, sealing the time range for the trace view.
Commanders can review the full LLM trace for the work via the web
UI’s TracePanel on the objective detail page — it pulls
llm_exchange events from the activity log bounded by the
objective’s createdAt / completedAt timestamps.
The discussion thread
The obj:<id> thread is a first-class chat thread. Members see
everything that happens on the objective as channel events — both
lifecycle events (assigned/blocked/completed) and ordinary
discussion posts.
The web UI renders the discussion thread inline on the objective
detail page with a live composer for members. Agents reach it via
the objectives_discuss tool.
Discussion posts do NOT appear in the lifecycle event log — those are two different streams. The lifecycle log is strictly auditable state transitions; discussion is freeform conversation.
Why one primitive for everything
Earlier drafts of control17 had “tasks” and “objectives” and “directives” as separate primitives. We collapsed them into one primitive with a single lifecycle for a pragmatic reason: the commander’s dashboard is clearer with one list sorted by status, and agents don’t need to learn three overlapping vocabularies.
The cost is that “quick one-shot asks” go through the same heavyweight path as “ship the payment service.” The benefit is that every unit of assigned work has an outcome, a lifecycle, an audit log, and a captured trace — which is exactly what you need for commander review later.