Skip to main content

Autonomy + self-learning

Crewship has three orthogonal knobs that shape how often an operator gets involved in an agent’s decisions:
  1. crews.autonomy_levelper-crew trust dial: strict | guided | trusted | full
  2. crews.behavior_modeper-crew response posture for behavior-monitor anti-pattern hits: warn | block
  3. agents.self_learning_enabledper-agent boolean: whether keeper-evaluator ALLOW decisions auto-apply or queue for operator approval
This page explains what each knob actually changes downstream, how they compose, and the design choice behind keeping them orthogonal instead of bundling them into a single “trust level” enum.

Why three knobs, not one

A naive design would expose a single per-crew dial — crew.trust = low / med / high — and have everything else derive from it. Crewship rejects that because operators in practice want:
  • A crew where every action needs approval BUT a specific senior agent in that crew can evolve on its own (operator-trusts-bot-A-not-bot-B)
  • A crew that auto-executes most things BUT blocks any anti-pattern hard (relaxed autonomy + strict behavior posture)
  • A crew that warns on anti-patterns but does NOT auto-promote lessons (governance-strict per-agent learning, relaxed at the crew level)
A single dial can’t express these. The three knobs decompose the trust decision into the dimensions it really has:
  • What can the agent do without asking?autonomy_level (crew-wide)
  • How does the system react when the agent does something the behavior monitor flags?behavior_mode (crew-wide; orthogonal to autonomy because warning vs blocking is a response policy not a permission policy)
  • Can the agent update its own long-term posture without approval?self_learning_enabled (per-agent because long-term posture is per-agent)

autonomy_level — what an agent can do without asking

Per-crew, four-state enum stored as crews.autonomy_level (migration v101). Default for new crews is guided. The matrix is the contract — extending it requires updating internal/policy/types.go::DecideAction AND the test matrix in types_test.go in lockstep, so silent drift becomes a test failure rather than a behavior change nobody notices. Every row is also monotonic: raising a crew’s autonomy level never makes an action harder. TestPolicy_DecideAction_MonotonicInAutonomy reads the actions out of the source and checks the whole matrix, so a tuning pass that relaxes guided while leaving trusted blocking — a dial where turning trust up turns capability down — fails rather than shipping.

What an agent may create on its own

The same dial governs the /api/v1/internal/* routes an agent reaches through its sidecar. This is a behaviour change on the default level: on guided, a crew that has never had its policy touched will now see agents stopped where they previously were not. Why crews and agents block where missions and schedules do not. A crew and an agent are durable principals — each one will act again tomorrow, on its own. Creating an agent additionally writes a caller-supplied system_prompt, which is persona authorship reached through an INSERT; direct persona writes are refused at every level, so an agent quietly acquiring a permanent colleague whose prompt it wrote itself is exactly what an operator wants to see before it happens. A mission creates no principal and is pinned to the acting agent’s own crew, and a schedule stays an operator-editable row — for both, a blocking hold bought the same visibility a notice does while stopping ordinary work. Note that this is an oversight decision, not a security one. The autonomy escape it was introduced alongside — an agent in a strict crew creating a guided one and acting there — is closed by two other things: strict refuses crew creation outright, and a created crew inherits its creator’s autonomy level rather than the column default, so no child crew is ever more permissive than its parent. Loosening the guided cells would cost visibility, not containment.

What a routine may do to the board

The same dial governs the crewship step — a routine writing back into Crewship’s own nouns. Three more actions:
On the routine path, “Inbox approve” means refused. A routine firing at 03:00 has nobody attached to approve anything, so a held decision is reported as a refusal naming the level and the crewship policy set that would lift it. A strict crew’s routines therefore do not write to issues and do not dispatch work.
Why issue writes are journal-only from guided up. Every other row here governs a rare, durable event — a crew, an agent, a cron entry, a plan. A triage routine writes to thirty issues in one run. One inbox item per write is how an inbox stops being read, and it would be a duplicate: every issue write already lands in that issue’s own activity trail, keyed to the issue, which is where an operator looks. What guided gives up is a blocking hold that bought neither containment (the crew binding does that — a routine can only touch its author crew’s issues) nor visibility. Why assignments get a notice where issue writes get silence. An issue write costs a row; an assignment costs a container start, a system prompt and a model turn, and it is the moment a routine stops editing records and starts making the crew work. It does not block at guided for the same reason mission_create does not: the fan-out is bounded on the dispatch door itself (delegation.max_depth + the fan-out cap), and a hold here would push the author to an agent_run step whose agent calls /assign with no policy row at all. Why escalation is flat across all four levels. Every other action bounds what the crew may do to the system, and trust is the right axis for that. An escalation’s cost is a human’s attention, and both inbox-shaped decisions are self-defeating: an approval to authorise an interrupt is the interrupt, and a notice about an escalation duplicates the escalation. Refusing it at strict was considered and rejected — a notify step already reaches the same human with no policy row at all, so refusing the structured, resolvable, audited path would degrade the record without removing the interrupt, and “ask before you act” cannot coherently forbid asking. The volume bound escalation needs instead. The dial decides per call and cannot express a rate, so the number lives on the door: escalation.max_pending_per_crew (default 10, 0 switches routine-raised escalations off) bounds how many unresolved escalations a crew may have before a routine may add another. It counts a backlog rather than a rate, so resolving the queue gives the budget straight back — no window, no timer. It bounds the routine door only; an agent escalating is a live session reporting it is stuck, and refusing that would strand the work with nobody told.
These three actions are not new restrictions on agents. All of them are already reachable, ungated, by an agent inside a crew container — the sidecar’s issue verbs, /assign, and the escalation route consult no autonomy level. Declaring an action does not close that; it makes it closeable. Today the enforcement hangs off the routine door, which means at strict a routine is refused what an agent in the same crew may still do. That asymmetry is the argument for gating the sidecar adapters on these same actions next — it is stated here rather than implied, because a control believed to cover more than it does is worse than a missing one.
Held (202 Accepted) means the row is written but inert until an operator decides:
  • a held crew is pinned to autonomy_level=strict, so nothing can be created inside it; approving restores the creating crew’s level, never higher
  • a held agent is status=PENDING_REVIEW; nothing starts it — not a chat message, not /assign, not an @mention on an issue, not a mission task A mission task assigned to a held agent waits; it is not failed. The mission engine checks before it writes anything, so a hold that stands for an hour costs no rows and no error log, and the tick after an operator approves picks the task up and runs it. That matters because a hold is a wait for a human, and the alternative — reporting the task as failed — would make approving it useless: nothing retries a terminally failed task. The same applies to a held lead: the mission simply does not plan until it is released.
  • a held mission stays PLANNING and cannot be started
Holds land on the approvals queue and are mirrored as a blocking, ADMIN-addressed inbox waitpoint:
The approvals queue is the only door, and crewship approvals approve requires OWNER or ADMIN — which is why the waitpoint is addressed to ADMIN rather than to MANAGER. In particular, crewship hire approve does not release a held agent: that command decides ephemeral hires an operator asked for with crewship hire, a MANAGER-tier action. An agent-created agent arrives with a system prompt another agent wrote, so releasing it is an OWNER/ADMIN decision. Pointing hire approve at one answers 409 and prints the crewship approvals approve <id> that does work. Denying does not delete anything, and neither does a lapsed hold. The seven-day timeout leaves the artefact inert rather than turning into a green light — the gate fails closed. So does an unwired policy resolver: if the gate is not wired, everything holds. Every arm — refused, held or allowed — writes an audit row carrying decision, autonomy_level and policy_action. There is no silent allow.

Strict-mode use cases

  • Compliance-bound workspaces (financial services, healthcare, regulated build pipelines) where every agent action has to be operator-traceable to an explicit approval
  • New crews during onboarding where operators are still building the inbox approval reflex
  • Crews that act on production infrastructure — the cost of one bad call is high enough that the operator prefers the friction

Full-mode use cases

  • Power-team developer workflows where the operator pre-approves the crew’s mandate and wants minimal friction
  • Background routine crews (nightly health checks, scheduled reports) where every action would otherwise generate noise the operator never reads
  • Crews with a narrow, well-understood mandate (e.g. “rotate API keys quarterly”) where the autonomy_level matches the scope

behavior_mode — what happens when the monitor fires

The F4.2 behavior monitor (Keeper Phase 2) samples tool calls per crew config and runs them through a behavior_evaluator. The evaluator returns ALLOW | DENY | ESCALATE. behavior_mode controls what DENY means:
  • warn (default) — DENY lands as a non-blocking inbox notification; the agent’s tool call proceeds. The operator sees the warning and decides whether to tighten autonomy or update the agent’s persona. Recommended for new crews — false positives don’t break workflows.
  • blockDENY interrupts the agent’s next tool call AND opens a blocking inbox approval. The operator has to confirm before the agent can proceed. Recommended for crews with established behavior-monitor confidence (the operator has seen the warn-mode signal pattern for weeks and is ready to trust DENY as a real signal).

Forbidden combination

autonomy=full + behavior_mode=block is rejected at both the API layer and the database CHECK constraint.
The combination is contradictory — full means “trust the agent to act on its own”, block means “let the behavior monitor interrupt the agent” — and the UI CrewPolicyControls panel greys the block option with a tooltip when autonomy=full is selected so the operator can’t even submit it. See internal/policy/types.go::Validate + TestPolicy_DecideBehaviorDeny_FullBlock_FailsClosed for the contract test.

self_learning_enabled — per-agent override on auto-apply

Per-agent, boolean stored as agents.self_learning_enabled (migration v106) with the standard audit triple (set_by_user_id / set_at / reason). Default for new agents is 0 (OFF — governance-first). The flag gates the ALLOW path of two keeper evaluators:
  • F4.4 negative-learning — when the evaluator decides the agent should learn from a failure (writes a kind=negative row to lessons.md)
  • F6 persona-suggest — when the agent proposes an update to its own PERSONA.md
For both:
  • self_learning_enabled = 1 → ALLOW auto-applies (lesson lands, persona updates)
  • self_learning_enabled = 0 → ALLOW queues a blocking inbox row with the full proposal payload; operator approves before the change lands
The flag does NOT override the crew’s autonomy_level. A strict crew + self_learning=1 agent still routes proposals through inbox — autonomy=strict says “every action via inbox” regardless of which agent. The self_learning flag only relaxes the path when policy ALREADY returns an auto-apply decision (which only happens at trusted or full).

When to flip self_learning ON

  • A long-running maintainer agent in a trusted crew that you’ve watched for weeks and trust to refine its own persona
  • An agent whose persona drift has historically been benign (the operator has been approving every persona-suggest proposal anyway — the inbox approval is just noise)
  • An agent whose mandate is well-defined enough that even a wrong lesson auto-applied is recoverable without operator cost

When to keep self_learning OFF

  • New agents during onboarding (default — operators see what the agent proposes before it lands)
  • Agents that act on production data where a wrong lesson could compound across many future interactions
  • Compliance crews where every learning signal needs an operator’s name attached

Setting the flag

Flip the flag through the dedicated audited endpoint (a reason is mandatory — it lands in the audit triple):
Requires ADMIN+. The CLI exposes the same control on agent update:
--learning-reason is required whenever --self-learning is set; omitting it errors before any request is sent.

Composition — worked examples

Crew: autonomy=trusted, behavior_mode=warn Agent in crew: self_learning_enabled=0A tool call fires that the F4.2 behavior monitor flags as DENY. Because behavior_mode=warn, the DENY → non-blocking inbox + agent proceeds. Operator sees the warn later.Same agent later fails a task. The F4.4 negative-learning evaluator decides the failure is worth a kind=negative lesson. Crew autonomy is trusted → policy returns auto_log_journal (i.e. would auto-apply). BUT self_learning_enabled=0 → demoted to blocking inbox. Operator reviews the proposed lesson, edits or approves.Result: agent acts freely day-to-day (autonomy=trusted), the operator sees anti-pattern signals as warnings (behavior=warn), and the operator gates lasting changes to the agent’s posture (self_learning=0).
Crew: autonomy=full, behavior_mode=warn Agent in crew: self_learning_enabled=1Everything auto-applies — tool calls, lessons, persona suggestions. The behavior monitor still warns on anti-patterns (operator sees them in the inbox) but nothing blocks. Use for power-team workflows where the operator has explicitly pre-approved the agent’s mandate.
Crew: autonomy=strict, behavior_mode=block Agent in crew: self_learning_enabled=1self_learning=1 is a no-op here. autonomy=strict → every policy decision is inbox_approve regardless of evaluator decision. The agent can flip the self_learning flag on but it won’t bypass the crew’s strict posture. Useful for documenting intent (the operator believes the agent CAN be trusted to evolve, but the crew’s compliance posture requires explicit approval anyway) without changing behavior.

Where to set them

autonomy_level + behavior_mode — per-crew settings

UI: Crew Canvas → Settings → Autonomy & behavior panel. The panel:
  • Renders the four autonomy options as a 2×2 grid with descriptions
  • Greys out the forbidden combination (block option when autonomy=full is selected)
  • Requires a reason field whenever the policy values are dirty (audit trail)
  • Shows the last change timestamp + reason at the bottom
CLI: crewship policy get/set/list
API: GET/PUT /api/v1/crews/{crewId}/policy (ADMIN+ for PUT)

self_learning_enabled — per-agent setting

UI: Agent Canvas → Settings → Learning posture panel. A single switch with a one-paragraph explainer of what flipping it ON actually changes (so operators don’t flip without understanding the consequence). Requires a reason field when dirty. CLI: crewship agent update <id> --self-learning[=false] --learning-reason "…" (must be run on its own — it can’t be combined with other field updates in the same command). API: GET/PATCH /api/v1/agents/{agentId}/learning (ADMIN+ for PATCH)

Audit history

All three knobs land in audit tables:
  • crews.autonomy_set_by_user_id / autonomy_set_at / autonomy_reason — last policy change (single row, last-write-wins)
  • agents.self_learning_set_by_user_id / self_learning_set_at / self_learning_reason — last flag change
  • keeper_requests — every decision the evaluators make (request_type, decision, reason, prompt, raw LLM response)
Query the keeper requests for a specific agent and date range:

Cross-references