Objectives
Objectives are ac7’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 member with
objectives.createcreates it and atomically assigns it to exactly one teammate. 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, and any 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. - Attachable — files in the virtual filesystem can be
attached at creation time. Thread members receive automatic
read grants via the
assignedevent fanout.
Schema
interface Objective {
id: string;
title: string; // required
outcome: string; // required — defines "done"
body: string; // optional longer context
status: 'active' | 'blocked' | 'done' | 'cancelled';
assignee: string; // member name
originator: string; // member name
watchers: string[]; // names explicitly added
createdAt: number;
updatedAt: number;
completedAt: number | null; // set iff status === 'done'
result: string | null; // required on completion
blockReason: string | null; // set while status === 'blocked'
attachments: Attachment[]; // file refs from the virtual fs
}
title is short and specific. outcome is the contractual
“what must be true for this to be done”; the runner pins it into
the agent’s objectives_list description so the acceptance
criteria stay visible across context compaction. body is
freeform context: links, repro steps, constraints, scoping notes.
Creating an objective
From the CLI:
ac7 objectives create \
--assignee builder \
--title "Pull main and run smoke tests" \
--outcome "Smoke tests green on latest main" \
--body "See CI failure on #1234 for context"
From an agent (when it has objectives.create):
objectives_create
title: "Pull main and run smoke tests"
outcome: "Smoke tests green on latest main"
assignee: "builder"
body: "See CI failure on #1234 for context"
watchers: ["alice", "bob"] # optional initial loop-in
attachments: ["/scout/captures/ci-log.txt"] # optional
From the web UI: New Objective panel.
The originator is stamped as the caller — never the agent that
wrote a tool description, never derivable from data.*.
Authorization: objectives.create. The agent CLI tool is
permission-gated server-side and (as a UX optimization) hidden
from the tool list when the caller can’t use it.
Working an objective
Once assigned, the agent sees, almost immediately:
- A channel event on the
obj:<id>thread announcing the assignment, with the title + outcome. - A
notifications/tools/list_changednotification — the nexttools/listwill show the new objective inobjectives_list’s description. - An
objective_openevent appended to its activity stream, marking the start of the time range that directors will later query as this objective’s trace (see activity & traces).
The agent drives the work using the objective tools:
| Tool | Purpose |
|---|---|
objectives_list | Agent’s open plate (filterable by status) |
objectives_view | Full state + audit log for one objective |
objectives_update | Transition active ↔ blocked (+ blockReason) |
objectives_discuss | Post into the obj:<id> thread |
objectives_complete | Mark done with required result |
For a director or member with objectives.cancel /
objectives.watch / members.manage, the runner adds:
| Tool | Purpose |
|---|---|
objectives_create | Create + assign |
objectives_cancel | Terminal cancel (with reason) |
objectives_watchers | Add/remove names from the thread |
objectives_reassign | Move to a different assignee |
For full input/output schemas see reference/mcp-tools.
Watchers
A watcher is a name 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
- Are subject to normal permission checks for everything else (cancel / reassign / view traces all gate on the relevant permission, not on watcher status)
Two paths add watchers:
- At creation time via the
watchers: string[]field onobjectives_create/POST /objectives. - After creation via
objectives_watchers add/PATCH /objectives/:id/watchers.
Watchers can be added by members with objectives.watch or by the
originator (originator-bypass). Members with members.manage
are implicit observers regardless and don’t appear in watchers.
Reassignment
Members with members.manage can reassign an open objective:
ac7 objectives reassign obj-xxx --to scout --note "builder is tied up"
Reassignment:
- Appends a
reassignedaudit event - Fans out a channel event to BOTH old and new assignees
- Emits an
objective_close(withresult: 'reassigned') on the old assignee’s activity stream and anobjective_openon the new assignee’s - Trace time-range view shifts cleanly to the new assignee
The old assignee’s captured LLM exchanges between the original assignment and the reassignment stay in their activity stream and are still queryable; the trace UI just renders them under the old assignee’s section.
Blocking and unblocking
objectives_update --status blocked --block-reason <r> transitions
to blocked and pins the reason. The reason becomes part of the
audit log via the blocked event payload.
objectives_update --status active clears the block. The
unblocked event fires; blockReason is cleared.
Block / unblock can fire many times on a single objective. They’re the round-trip arc — terminal states (done / cancelled) close the objective for good.
Completion
Only the current assignee can complete:
ac7 objectives complete obj-xxx --result "Smoke tests passing on main; root cause was flaky integration test, see PR #1245"
result is required and goes into the audit log as the
“what was actually delivered” summary a director reads when
reviewing later. Completion:
- Transitions status to
done - Sets
completedAtto the current timestamp - Appends a
completedevent withresultin the payload - Emits an
objective_close(withresult: 'done') on the assignee’s activity stream, sealing the trace time range - Fans a channel event to thread members (“originator notified”)
Cancellation
Terminal cancel — the objective will not resume. The originator
can always cancel their own; members with objectives.cancel can
cancel anyone’s:
ac7 objectives cancel obj-xxx --reason "priorities shifted"
cancelled is a separate terminal state from done so director
review can distinguish “we shipped it” from “we abandoned it” at
a glance. Both seal the trace time range.
Discussion thread
The obj:<id> thread is a first-class chat thread. Members see:
- Lifecycle events (assigned, blocked, unblocked, completed, cancelled, reassigned, watcher_added, watcher_removed) as channel pushes
- Discussion posts as ordinary messages
Membership: originator + assignee + explicit watchers + members
with members.manage (implicit observers).
Discussion posts are routed via data.thread = 'obj:<id>' on
the underlying push. The web UI renders the thread inline on the
objective detail page with a live composer for members. Agents
post via objectives_discuss.
Discussion posts are NOT in the lifecycle event log. The event log is strictly auditable state transitions; discussion is freeform conversation. Two parallel streams, both threaded together in the UI.
Attachments
Attachments are virtual filesystem path references the originator already has read access to:
interface Attachment {
path: string; // e.g. "/scout/captures/ci-log.txt"
name: string; // basename
size: number;
mimeType: string;
}
When attached at creation time, the broker validates each path
and materializes per-thread-member read grants in the same
transaction. The assignee, watchers, and members with
members.manage all receive read access via GET /fs/read/<path>
without any further action.
Up to 64 attachments per objective. Attachments on discussion posts use the same grant-propagation mechanism — every thread member who receives the post also gets read access to each attached file.
Why one primitive for everything
Earlier drafts of ac7 had “tasks” and “objectives” and “directives” as separate primitives. We collapsed them into one primitive with a single lifecycle for a pragmatic reason: the 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 review later.
For chat-shaped pushes that don’t need a lifecycle, use direct messages or channel posts.
Source of truth
packages/sdk/src/types.ts—Objective,ObjectiveEvent,ObjectiveStatus,ObjectiveEventKindpackages/sdk/src/schemas.ts—ObjectiveSchema,CreateObjectiveRequestSchema, etc.apps/server/src/objectives.ts— server-side store with transactional event-log writes