Files
tutti/docs/conventions/tutti-cli-contract.md
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

932 lines
49 KiB
Markdown

# Tutti CLI Contract
This document defines the durable contract for Tutti CLI commands exposed
through the local daemon.
The bundled CLI is a thin client. It discovers command capabilities from
`tuttid`, matches command paths, parses flags using daemon-provided
`InputSchema`, invokes the daemon command endpoint, and renders the returned
`CommandOutput`.
## Error Output
`--json` applies to failure output as well as successful command output.
Expected invocation, transport, and domain failures write one JSON object to
stdout and do not fall back to free-form stderr text:
```json
{
"error": {
"reasonCode": "workspace_agent_session_not_found",
"message": "agent session was not found"
}
}
```
The CLI preserves the daemon protocol `reason` as `reasonCode`, falling back to
the daemon error `code` when no narrower reason exists. Optional daemon
`retryable` and `correlationId` fields are preserved. Errors detected before a
daemon invocation use stable CLI-owned reason codes such as `invalid_input`,
`command_not_found`, and `daemon_unavailable`.
Because `retryable` is omitted when false, consumers must not treat a missing
field as permission to retry an arbitrary structured failure. CLI-owned
transient reasons such as `daemon_unavailable` and `daemon_request_failed`
remain retryable by reason; daemon errors are retryable only when the envelope
explicitly says so.
Domain validation keeps its narrower daemon reason when one exists. For
example, `unsupported_permission_mode_id` means a caller supplied a permission
identifier that is not advertised for the selected Agent target. Automation
must refresh Composer Options and pass one of its `modes[].id` values verbatim;
it must not retry the same value or substitute `modes[].semantic`.
An obsolete persisted default is ignored while reading Composer Options so the
caller can recover by selecting a current advertised id. The same obsolete id,
when explicitly supplied in the current invocation, remains invalid input.
Tutti execution commands use stable rejection reasons such as
`wrong_source_session`, `inactive_execution`, `inactive_checkpoint`,
`stale_graph_revision`, `task_not_found`, `task_not_started`,
`task_superseded`, `missing_agent_target`, `dependency_unsatisfied`,
`dispatch_paused`, `budget_unavailable`, `capacity_exhausted`,
`parallelism_rejected`, `duplicate_task`, `invalid_task_graph`, and
`invalid_mutation_operation`. Their message must include one action-specific
recovery hint. A fence rejection directs the caller to the source-scoped
execution snapshot; a task rejection identifies the affected task. Callers
must not infer a new revision, cycle through unrelated commands, or inspect the
backing database.
Exit codes stay intentionally small and stable: `0` means success, `2` means
the command invocation or input was invalid, and `1` means authentication,
transport, domain, or runtime failure. A daemon HTTP 400 response is invalid
input and therefore exits with `2`; other daemon failures exit with `1`.
Without `--json`, ordinary human-facing failures continue to write concise
text to stderr.
## Agent Turn Cancellation
Agent cancellation is Turn-scoped. Use
`agent cancel-turn --session-id <id> --turn-id <id>` to cancel one exact Turn
while preserving the session for later input. The explicit Turn id prevents a
caller from accidentally canceling a newer Turn that became active after it
last inspected the session.
JSON output returns `agentSessionId`, `turnId`, `canceled`, and the idempotent
result `reason` (`turn_canceled`, `already_settled`, or `not_found`). There is
no CLI operation that cancels or terminates an Agent session.
The old `agent cancel --session-id <id>` path remains an integration-only
compatibility alias. It resolves the currently active Turn, emits a
`deprecated_agent_cancel` warning, and must not be used by new integrations.
## Agent Session References
Each item returned by `agent sessions` carries the raw `agentSessionId` plus
`workspaceId` and a canonical `mentionUri`. Singleton `agent get`, `agent
start`, `agent send`, `agent open`, and `agent wait` JSON results expose the
same three fields at the top level so consumers do not need command-specific
nested lookup:
```json
{
"agentSessionId": "session-1",
"workspaceId": "workspace-1",
"mentionUri": "mention://agent-session/session-1?workspaceId=workspace-1"
}
```
Agents should use `mentionUri` as the destination of a descriptive Markdown
link when returning a session reference. Consumers must treat it as internal
data for Tutti mention routing, not as a web URL, filesystem path, or command.
Nested session objects retain the same reference fields for compatibility, and
raw ids remain available for CLI continuation and automation.
## Boundaries
The daemon has two CLI command surfaces:
- builtin commands owned by `services/tuttid/service/cli/providers/*`
- workspace app commands declared by app-owned `tutti.cli.json` manifests
Builtin commands should be implemented through the daemon CLI framework. The
framework is the source of truth for builtin command metadata, input schema,
input binding, validation, workspace resolution, and output formatting.
Workspace app commands use the frozen `tutti.app.cli.v1` manifest contract.
They are not implemented through the builtin framework.
Builtin summary/detail JSON view rules do not apply to workspace app commands.
External app commands follow their own manifest-declared output and response
contract.
Workspace app `appId` and CLI `scope` are separate identifiers. Discovery and
agent app mentions match commands by app id metadata, then invoke the listed
CLI scope; callers must not assume `scope == appId`.
## Frozen App CLI Contract
The app CLI path is a compatibility boundary for workspace apps.
Keep these semantics stable unless the app CLI manifest version changes:
- `tutti.app.json` points to the app-owned CLI manifest path
- `tutti.cli.json` uses `schemaVersion: "tutti.app.cli.v1"`
- `packages/appcli/core` reads and validates the manifest shape; daemon code
adapts that protocol core to Tutti workspace/app runtime state
- `appcli.Registry` normalizes input according to the manifest input schema
- `appcli.Registry` invokes the app handler with the
`tutti.app.cli.invoke.v1` envelope
- app handlers receive HTTP `POST` requests under `/tutti/cli/*`
- app handlers return the existing `CliCommandOutput` shape
- app command output is validated against the manifest-declared output contract
- app commands may declare optional `visibility: "integration"` to stay out of
ordinary user and Agent discovery while remaining available to app-runtime
integrations; omitted visibility is `public`
- app command input schema properties may include `enum` and `default`
annotations when their values match the declared property type; `default` is
metadata for help and discovery, not a host-side input value that is injected
into handler requests
Do not require migration for existing app manifests when changing builtin CLI
implementation internals.
`visibility` is a discovery hint, not an authorization boundary. Use it to keep
integration-only commands out of `tutti --help`, Agent command guides, and
ordinary command matching. Do not use it for secrets, privileged actions, or
operations that must be blocked when a user already knows the command.
App-runtime CLI discovery should request integration-only commands without
skipping provider availability filters. Use the CLI capabilities
`includeIntegration` query for that path. Reserve `includeHidden` for metadata
or debugging paths that intentionally need both provider-filtered and
visibility-filtered capabilities.
## Builtin Command Source Of Truth
Every builtin command should be declared once through a command spec. The spec
drives:
- `Capability.ID`
- `Capability.Path`
- `Capability.Summary`
- `Capability.Description`
- `Capability.InputSchema`
- `Capability.Output`
- input binding and validation
- default output formatting
- command kind compliance
Provider handlers should contain business behavior only. They should not
hand-maintain JSON schema, duplicate flag parsing, duplicate required-field
validation, or duplicate default output branching when the framework can derive
it from the spec.
The framework must return the existing `cliservice.Command` type. The registry,
OpenAPI route shape, and app CLI registry remain separate.
Builtin capabilities may declare `Capability.Visibility` as `integration` when
the command is intended for app-runtime integrations rather than ordinary user
or Agent discovery. Omitted visibility defaults to `public`.
`workspace-apps.app.open` is public so agents can open requested workspace app
windows. Some built-in app ids, such as Agent GUI and issue-manager windows,
map to workbench nodes rather than installed app packages. Agent-facing skills
must still treat app opening as an explicit activation action: use it only when
the user asks to open or show an app window, or confirms that an app window
should be opened. For ordinary app work, agents should prefer the app-specific
CLI capability over opening the app UI.
### Computer command surfaces
The builtin `computer` provider has two intentionally different layers, both
owned by `services/tuttid/service/computer` and exposed through daemon command
specs:
- stable aliases such as `computer screenshot`, `click`, `type`, and `scroll`
provide a small window-oriented compatibility contract and may normalize
legacy tool names;
- `computer tool list|describe|call` discovers and invokes the native
cua-driver MCP catalog without adding a Tutti binding for every driver tool;
this is the complete extensible entry point for authorized native
capabilities that do not have a stable alias.
Stable screenshot-coordinate commands (`screenshot`, `click`, `double-click`,
`right-click`, and `scroll`) expose only the window workflow. They do not expose
a synthetic shared `scope`. `pid` and `window-id` are accepted only as a pair;
when both are omitted, the service may select an eligible visible window.
Coordinates for pointer and scroll commands are local to the selected window
screenshot. Keyboard commands accept the same explicit pid/window pair to
avoid target drift. `move-cursor` is deliberately target- and scope-less: it
accepts agent-cursor screen points, not pixels copied from either screenshot
surface.
Pointer delivery is dispatch-only: pixel clicks post background CGEvents that
cua-driver cannot verify, and Electron-based apps may drop them silently. The
skill contract therefore prefers element-token actions (native `click` with an
`element_token` from screenshot structured content), requires effect
verification through a fresh screenshot rather than the agent-cursor overlay
(a separate visual channel that can render offset after display-configuration
changes), and escalates unresponsive background clicks via the native
`delivery_mode: "foreground"` contract instead of repeating the same pixel
click. See
[Computer Use Troubleshooting](./troubleshooting/computer-use.md#a-computer-click-reports-success-but-the-ui-does-not-change)
for the symptom-driven verification and recovery checklist.
Desktop automation follows each native cua-driver tool's real contract rather
than a Tutti-wide scope model:
- `get_config` reads cua-driver's host-global persisted `capture_scope`;
- `get_desktop_state` captures the desktop only when that persisted setting is
already `desktop`; the call does not choose a capture scope;
- native `click` has its own per-call `scope: "desktop"` contract;
- native `scroll` requires a PID and has no true desktop-coordinate mode, so
Tutti must not advertise desktop scrolling.
Stable commands and native calls must not implement a hidden set/call/restore
sequence around `capture_scope`. The `system.config.write` capability used by
native `set_config` is authorized by Tutti's default computer policy, so an
agent may explicitly set `capture_scope` to `desktop` through `computer tool
call` before invoking `get_desktop_state`. The mutation is host-global and
persisted by cua-driver; the agent must not disguise that fact or automatically
restore the previous value after capture. Stable `computer screenshot` remains
window-only regardless of the persisted scope.
The native catalog is also the policy input. Tutti applies a default-deny
capability allowlist in `service/computer`. `tool list` and `tool describe`
retain the complete live catalog and annotate every tool with the Tutti-owned
`allowed` decision and `denialReason`; `tool call` enforces the same policy.
MCP effect annotations are descriptive hints and never grant authority. Both
the catalog `schema_version` and `capability_version` are breaking-contract
boundaries; missing or unknown versions fail closed before any tool is listed,
described, or invoked. Every advertised capability must be explicitly
recognized and allowed before invocation. Tools with no capability metadata,
any unknown or denied capability, or a name absent from the live catalog remain
unavailable even when another capability on that tool is allowed. Stable
aliases enumerate their supported operations and must not fall through to raw
native invocation.
Stable aliases return compact, explicitly projected JSON. Native `tool call`
is the narrow exception: its JSON mode preserves the complete MCP tool result,
including unknown and future content variants, so extending cua-driver does
not require a Tutti result adapter. Plain mode may still project the collected
text for terminal use. A valid MCP result with `isError: true` is still emitted
unchanged by native JSON mode so callers retain its structured diagnostics;
transport and protocol failures remain CLI errors. Stable aliases continue to
translate `isError` results into their compatibility error behavior. Do not use
the native surface to bypass stable alias validation or the capability policy.
## Command Kinds
Builtin commands must declare one command kind.
| Kind | Purpose | Default output expectation |
| -------- | --------------------------------------- | ----------------------------------------- |
| `list` | Return a collection or page of records | table for terminal output, compact JSON |
| `get` | Return one detailed record | detail JSON |
| `action` | Start, mutate, cancel, open, or trigger | explicit concise result chosen by command |
List commands must use the `summary` JSON view by default. Get commands must
use the `detail` JSON view by default. Action commands must use the `summary`
JSON view by default.
`--json` means machine-readable output. It does not mean every domain field is
returned. The command kind chooses the stable JSON view: list/action commands
return concise summaries, and get commands return detail with nearby context.
Action commands should return the smallest useful confirmation payload. For
agent session actions, this normally means session id, exact `agentTargetId`,
provider runtime metadata, status, and whether a launch/open request was
published. Existing-session consumers must validate `agentTargetId` before
sending or attaching; provider equality is not sufficient because several
Agent Targets may share one provider.
Agent submission actions also return the exact top-level `turnId` established
or targeted by that command. `agent start` returns the initial Turn created for
its prompt, and `agent send` returns the Turn created by a normal send or
targeted by active-turn guidance. Callers must use this exact identity for
`agent turn-resources` and later Turn-scoped operations instead of inferring it
from `session.activeTurnId` or querying the transcript.
Agent session detail JSON uses the same protocol-v2 entities as HTTP/OpenAPI:
`activeTurnId`, `activeTurn`, `latestTurn`, and pending Interaction records.
It must not expose the provider-runtime `turnLifecycle` or
`submitAvailability` mirrors as session-domain fields. CLI status labels are
derived from the typed Turn projection rather than persisted on Session.
`agent send --guidance` sends a one-shot prompt as active-turn guidance for an
existing running session. It requires an active turn, does not attach to stdout
or subscribe to session events, and must fail instead of falling back to a normal
next-turn send when no active turn is present.
Issue-manager breakdown commands should preserve authored task order in the
daemon instead of relying on callers to serialize several single-task creates.
Use `issue task create-batch` for multiple new child tasks. Its `tasks-json`
input is a JSON array of task objects, and the daemon appends tasks in array
order with contiguous issue-local `sortIndex` values.
`agent get --json` progressively discloses agent context. Its default
`conversation` view returns the three most recent Turns newest-first. Messages
inside each Turn remain chronological and include only user/assistant body
content; tool calls and session audits are excluded. Each Turn exposes its
durable final assistant result separately as `finalMessage`, plus
`hasMoreMessages` when the bounded body scan did not cover the complete raw
trace. `--turns <N>` expands the recent window from 1 to 20 Turns, while
`--turn-id <id>` selects one exact Turn. `--before-turn-id <id>` pages the
`conversation` or metadata-only `turns` view towards older Turns; the cursor is
exclusive and callers use the oldest returned Turn id when `hasMoreTurns` is
true. `--view session` returns session metadata without messages. `--view
turns` returns the same newest-first Turn metadata without reading message
records. `--view trace --turn-id <id>` returns one Turn's tool-level message
records and full payloads, bounded by `--messages <1-100>` and pageable
backwards with `--before-version`.
Turns are the outer ordering unit: recent Turns come first, while messages
inside a Turn never reverse. The session record includes exact `agentTargetId`
alongside provider runtime metadata so callers can safely validate
attach/resume identity. When a compact conversation message contains image
prompt content, include an
`images` array with `attachmentId`, `mimeType`, `name`, and a daemon-local
`localPath` when the attachment file is available on disk. Keep `payload`
omitted from the conversation shape; expose full payloads only in the explicit
trace view. The deprecated integration-only `agent session-summary` path
remains temporarily available for compatibility with its existing pagination
flags and exact top-level JSON shape. Deprecation is advertised through command
metadata, not a runtime warning that would wrap or mutate legacy JSON output.
`agent wait --json` is the blocking progress helper for launched or continued
agent sessions. It should wait for the next meaningful stop point such as turn
completion, failure, cancellation, waiting for approval, waiting for user
input, or timeout. Its JSON result stays narrow: compact session status, the
exact top-level `turnId` associated with the stop point when one exists, wait
reason, latest version, effective wait cursor, and timeout flag. A completed or
failed turn additionally returns its last assistant text as `finalMessage`;
that nested record repeats the owning `turnId` for local correlation. An
approval or input stop additionally returns the pending `interactions` with
self-described actions and a JSON input summary of at most 2 KiB. It must not
reuse a historical settled Turn for an idle timeout: timeout carries a
`turnId` only while a Turn is active. It must not return execution-message
pagination or a full transcript; callers that need broader context should
follow with `agent get`, paging older Turns with `--before-turn-id` or selecting
one Turn's trace only when necessary. Keep message window controls out of the
public wait command shape, and keep timeout output free of result or
interaction detail.
The wait implementation skips transcript pagination and performs result
enrichment separately. New settled turns carry a durable final-assistant
resolution marker and, when present at settlement, the exact message anchor.
Anchored reads select that exact message; a resolved marker with no anchor means
the Turn had no final assistant text and must return no `finalMessage`, even if
an assistant message arrives later. Only legacy turns without resolution
metadata fall back to at most three descending message pages. If neither path
finds the message, omit `finalMessage` rather than returning an older assistant
response.
When a caller continues an existing session with `agent send`, the send action
should return a `waitAfterVersion` cursor, and the next wait call should pass
that cursor as `agent wait --after-version <waitAfterVersion> ...` so the wait
blocks for the new stop point instead of immediately replaying the previous
session stop state.
`agent respond --json` answers one pending interaction selected by
`--session-id`, `--turn-id`, and `--request-id`. Provider request ids are
transport-local and may repeat across Turns, so callers must pass the exact
Turn returned by `agent wait`. `--action`, `--option`, and object-valued
`--payload` map directly to the Host interactive input. `--semantic` is a thin
shortcut that resolves exactly one matching `actions[].semantic` from the
stored interaction; the CLI must not keep a provider-to-semantic mapping.
Unknown exact interaction tuples and missing or ambiguous semantics are invalid input errors.
The first responder atomically claims the pending interaction. Later responses
with the same action/option/payload return `answered`; different responses
return `superseded`. The result exposes the request and turn ids plus that Host
disposition, without surfacing raw operation conflict or in-progress errors.
After a successful claim, an authoritative terminal runtime disposition still
wins; for example, a runtime `superseded` result must not be rewritten to
`answered` merely because the durable claim already marked the Interaction.
## Tutti Mode Plan Commands
`tutti plan` is the Agent-callable observation and proposal surface for the
Tutti-owned workspace workflow. Its capabilities are discoverable independently
of the current Tutti Mode activation badge and independently of a provider's
Default or Plan collaboration mode. The badge is host preference state and is
not evidence that a workflow exists. Plan and execution mutations nevertheless
require the caller Session's current user-controlled activation to be active.
Activation itself is not Agent-callable. The builtin registry exposes no
`tutti mode set` command, and the activation service rejects the legacy
`agent_command` source for new writes. When a gated mutation finds Tutti Mode
inactive, it tells the Agent to ask the user to turn it on manually; the Agent
must not attempt to change the toggle through another CLI or tool path. Stored
legacy `agent_command` revisions remain readable for upgrade compatibility.
The flow is single-shot: one `propose` submits the complete plan and opens the
single user review checkpoint. There is no separate configuration phase and no
daemon-derived decomposition turn.
The public command set is deliberately narrow:
- `tutti plan propose --file <absolute-path> --request-id <stable-id>` creates
the initial revision and the single review checkpoint. The document must be
one complete `tutti-mode-plan/v1` Markdown file: the plan narrative in the
body plus the full task graph in the `tasks` frontmatter (at least one task
is required). `phase` may be omitted; it defaults to `task_graph`.
Configuration-only documents are rejected. When the Tutti Host Context is
active, combine its `effect` and `speed` preferences without averaging them:
effect raises the model-capability floor and the required task-verification
breadth; speed selects the fastest model that still clears that floor and
supplies the bounded parallel target `0-24 -> 1`, `25-49 -> 2`,
`50-74 -> 3`, `75-100 -> 4`. Encode the original preferences in the additive
`execution.effect` and `execution.speed` snapshots. The existing v1
`execution.reasoningIntensity` and `execution.orchestrationIntensity` fields
retain their provider-reasoning and Issue
decomposition/dependency/review/retry meanings; speed must not be written
into `orchestrationIntensity`. A legacy v1 document without explicit
preference snapshots maps `orchestrationIntensity` to effect and uses the
balanced speed default. The injected `$tutti-model-allocation` skill owns the
C0-C3 assignment rubric and model-family routing priors: intersect its tier table
with the current target's `composer-options` output, use effect to establish
the capability and verification floor, then let speed rank only the models
that clear that floor and shape real independent workstreams toward its
parallel target. The table never proves model availability. The target is an
upper planning bound, not a promise: task count and actual parallelism still
follow dependency, ownership, safe-isolation, budget, readiness, and
workspace-capacity boundaries;
- `tutti plan revise --workflow-id <id> --file <absolute-path> --request-id
<stable-id>` appends a complete replacement plan document (narrative plus full
task graph) after the user requests changes. When the user rejects the
review, the daemon also proactively starts a feedback turn on the source
session instructing the Agent to revise, so the Agent does not have to poll;
- `tutti plan get --workflow-id <id>` returns the caller-session-scoped
authoritative snapshot;
- `tutti plan issue get --issue-id <id>` returns the source-session-scoped
authoritative execution snapshot: execution status, active checkpoint,
graph revision, visible pending checkpoints, task launch settings,
supersession and dependency facts, per-task readiness blockers, ready task
IDs, `dispatchPaused`, allowed actions, and a checkpoint-specific recovery
hint. It is the only supported refresh operation after a fenced execution
command is rejected;
- `tutti plan issue resume --issue-id <id>` clears a durable dispatch pause
without changing the active checkpoint or graph revision. Caller authority
comes only from the original source Agent Session. A Session whose frozen
runtime snapshot predates this command may use
`tutti issue update --issue-id <id> --dispatch-paused=false`; the Issue
provider recognizes only that exact managed-Issue resume shape and routes it
through the same source-scoped control. Other generic mutations of a
Tutti-owned Issue remain rejected;
- `tutti plan issue schedule --issue-id <id> --checkpoint-id <id>
--expected-graph-revision <revision> --task-ids-json <json-array>
--request-id <stable-id>` resolves the active execution checkpoint by
atomically admitting exactly the requested ready task set. Caller authority
comes only from the daemon-provided Agent session context; there is no caller
session flag;
- `tutti plan issue mutate --issue-id <id> --checkpoint-id <id>
--expected-graph-revision <revision> --operations-json <json-array>
--request-id <stable-id>` atomically applies one or more `add`, `update`,
`rework`, or `supersede` operations to the active Issue graph. The checkpoint
and graph revision fence the entire operation set; success increments the
graph revision and rebinds the checkpoint to that revision. Every operation
object uses the `kind` key (`op` is invalid): `add` carries `task`, `update`
carries `taskId` plus `task`, `rework` carries `taskId` plus its replacement
`task` (`replacement` is invalid), and `supersede` carries `taskId`. A
new task must include `agentTargetId`. `update` and `rework` are
presence-aware: omitted fields are preserved, while explicit empty strings,
empty arrays, `false`, and zero values remain distinguishable and are applied
when the field is clearable. Required task invariants such as a nonempty
title and Agent target still fail closed. A sparse `rework` inherits omitted
launch settings, execution directory, and dependencies from the superseded
task; explicit replacement values override those defaults. A successful
`rework` also redirects every active, not-started task dependency from the
superseded task ID to the replacement task ID in the same transaction.
Rework is rejected if that redirect would require changing a task that has
already started;
- `tutti plan issue acknowledge --issue-id <id> --checkpoint-id <id>
--expected-graph-revision <revision> --request-id <stable-id>` resolves a
reviewed `task_settled`, `task_failed`, or `task_canceled` checkpoint without
scheduling a successor. It is allowed only while another Run is active or a
later checkpoint is queued; `all_tasks_terminal` always requires the
dedicated Goal-review flow and is rejected here;
- `tutti plan issue complete --issue-id <id> --checkpoint-id <id>
--expected-graph-revision <revision> --request-id <stable-id>
--decision goal_satisfied [--disagreement-reason <reason>]` completes the
exact active Goal Review. The source Session comes from trusted invoke
context. A disagreement reason is required when the source Agent overrides a
negative or inconclusive independent verdict;
- `tutti plan issue stop --issue-id <id> --checkpoint-id <id>
--expected-graph-revision <revision> --reason <reason>
--request-id <stable-id>` stops an Issue that was replaced or should no longer
continue. The daemon derives the source Session from trusted invoke context,
fences the stop against the active checkpoint and graph revision, then uses
the recoverable archive path to cancel open Runs and close pending
checkpoints and wakes. The reason is retained as audit evidence.
The active checkpoint defines the legal action set:
| Checkpoint | Legal recovery |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `initial_schedule` | schedule an exact ready set, mutate, or stop |
| `task_settled` | schedule or mutate; acknowledge only when another Run is active or a later checkpoint is pending; or stop |
| `task_failed`, `task_canceled` | rework the terminal task with a new task ID, then schedule the replacement with the mutation result revision; acknowledge only under the same backlog condition; or stop |
| `all_tasks_terminal` | complete Goal Review, or mutate and schedule exact follow-up work; or stop |
When `dispatchPaused=true`, remove `schedule` from the table's current action
set and add `resume`. Mutation, acknowledgement, completion, and terminal stop
retain their normal checkpoint rules. A pause does not mint a new checkpoint
or revision.
The source Agent refreshes `plan issue get` at most once for an
`inactive_checkpoint` or `stale_graph_revision` rejection and retries the
intended command at most once with the returned fence. It reads mutation help
at most once for `invalid_mutation_operation`. Repeating the same rejection
after that bounded recovery is reported verbatim instead of triggering tool
exploration or backing-store access.
Tasks may carry optional `agentTargetId`, `modelPlanId`, `model`,
`permissionModeId`, and `reasoningEffort` assignments. The user can override
these per task in the review panel; overrides are recorded with the accepted
decision and win over the document values at Issue materialization.
There is no Agent CLI plan-approval command. Accept, reject, feedback, and
cancel remain user-owned daemon interactions. After acceptance, execution
commands are source-Agent-owned and checkpoint/revision fenced; a schedule
rejection never admits a subset of the requested tasks.
`propose` and `revise` require caller-generated request IDs because response
loss must not cause an unintentional second mutation. The durable identity is
`(workspace, source session, mutation kind, workflow scope, request ID)`.
Retrying the same key with the same exact Markdown bytes returns the original
workflow, revision, and checkpoint and reports `replayed: true`. Reusing that
key with different bytes is a conflict. A new request ID is an intentional new
mutation even when the bytes and content-addressed file are identical; a
content digest is integrity evidence, not user intent.
Schedule mutations use the corresponding durable identity
`(workspace, source session, schedule, Issue, request ID)`. Replaying the same
identity and payload returns the original execution/checkpoint/revision and Run
IDs. Reusing it with a different checkpoint, revision, or ordered task-ID set
fails closed. External Agent launch occurs only after the admission transaction
commits and is recovered from a durable launch intent using
`issue-run:<runID>` as the deterministic submit identity. The persisted
launch-intent value, not a delivery-adapter reconstruction, is passed unchanged
to Agent Host. The delivery owner renews its durable lease while the external
launch is in flight; normal replay cannot steal it, and startup recovery
requeues only expired ownership before retrying. The periodic Issue Run
reconciliation pass repeats the same expired-owner recovery, so a pre-expiry
restart cannot strand the intent after its final lease later expires.
Graph mutations use
`(workspace, source session, mutate, Issue, request ID)` as their durable
identity. The payload digest binds the exact checkpoint, expected graph
revision, and normalized ordered operation list. Replaying the same identity
and payload returns the original execution, checkpoint, new graph revision,
and affected task IDs with `replayed: true`; changing any bound field or
operation fails closed.
Acknowledge mutations use
`(workspace, source session, acknowledge, Issue, request ID)` and bind the
exact checkpoint plus expected graph revision into the payload digest.
Same-payload replay returns the original result with `replayed: true`;
conflicting reuse fails closed. A successful acknowledge resolves only the
active settlement checkpoint and promotes the oldest queued checkpoint. It
does not infer or dispatch a successor.
Goal Review completion mutations use
`(workspace, source session, complete, Issue, request ID)` and bind the exact
checkpoint, expected graph revision, `goal_satisfied` decision, and optional
disagreement reason into the payload digest. Same-payload replay returns the
original completion result with `replayed: true`; a different actor or payload
under the same durable identity fails closed.
Workflow lookup is isolated to the Agent session supplied by the daemon CLI
runtime context. A workflow created by another source session is reported as
not found. App CLI parent-command identity is not Agent Turn or tool-call
provenance and must never be stored as such. Proposal input files must be
absolute, bounded in size, parsed by the daemon, and retained as immutable
Markdown content; CLI clients must not implement the workflow state machine.
`agent turn-resources --json` is the narrow helper for looking up resources from
one explicit session turn. It requires `--session-id` and `--turn-id`, filters at
the message query layer, and returns resource-bearing user messages with images
grouped under their source message. Do not flatten images across turns in this
command; the calling agent decides which turns to inspect and which returned
`localPath` values to pass to provider launchers as `--image`.
Agent discovery and launch are target-first. `agent list --json` returns every
enabled Agent Target in stable target order, including its exact agent id,
display name, provider metadata, current runtime availability, and an explicit
`defaultAgentTargetId` resolved from the current desktop preference. The
JSON entry may also include `executablePath`, the host-resolved executable for
that exact Agent Target. App-owned runtimes must treat it as opaque launch
metadata instead of reconstructing paths or relying on their inherited `PATH`;
older callers may ignore the optional field. The
preference resolves to the exact built-in target id before considering another
target that shares its provider, so a user-created agent cannot silently replace
the desktop default. Preference-read failures use the built-in default instead
of failing discovery. The default identifies preference, not readiness: it may
be unavailable. An `--agent-id` filter narrows only `agents`; it does not rewrite
the global default, so the default id may be absent from a filtered response.
An unfiltered catalog read refines installed Agent Extensions with their runtime
setup/authentication result so released apps that predate exact-target probing
do not mistake installation readiness for a logged-in runtime. Extension probes
run concurrently, so the broad catalog waits at most for the slowest bounded
probe rather than their sum. Tutti coalesces broad and exact-target probes
across app callers and keeps a short-lived daemon result; `--refresh` bypasses
that result. New callers may still use `agent list --agent-id <agent-id>` when
they only need one target.
The command must not collapse several agents that share one provider or make
callers guess a default from list order. Callers select an exact id and start it with
`agent start --agent-id <agent-id> ...`; provider-specific command
families such as `codex start` and `claude start` are not part of the
agent-facing CLI contract. A disabled Agent Target is absent from `agent list`,
and an explicit `agent start` for its id must fail before session creation; CLI
callers cannot bypass the daemon-owned enablement state.
During the agent-id migration window, old app integrations may continue to
invoke `agent providers`, provider-based composer/skill inputs, and the exact
`codex start` / `claude start` aliases. These are compatibility adapters, not
agent-facing discovery: generated runtime skills and command guides must omit
them. A provider selector may resolve only when exactly one enabled target uses
that provider; zero or multiple matches must fail with recovery guidance to
run `agent list --json`. Target-first composer-options and skill-bundle
responses use schema version 2 and carry `agentTargetId`; compatibility
responses preserve their prior schema version.
Remove this window only after released kit consumers and organization-owned
apps have migrated and old materialized skills have crossed the support
window.
`--model` remains optional on `agent start`. When omitted, tuttid resolves the
model from the selected agent's composer defaults or its runtime provider
default. `agent start` must reject unknown or disabled agent ids and point the
caller back to `agent list`; it must never create a provider-only session.
Provider is retained as derived runtime and diagnostic metadata, not launch
identity.
`--show` on `agent start` requests AgentGUI activation only; it must not
change the created session's visibility. User-started sessions should stay on
the normal visible default; only an explicit `--hidden` launcher input should
create a hidden session.
`agent start --isolation worktree` creates the session in
`<state-dir>/agent/worktrees/<worktree-id>` on branch
`tutti/worktree/<worktree-id>`, based
on the resolved launch cwd's `HEAD`. The resolved cwd follows the normal
explicit-cwd then caller-session-cwd chain. Isolation is fail-closed: a missing
git executable, non-git cwd, nested repository or submodule, or failed
`git worktree add` rejects the launch before a session is created. Source
checkout changes are not copied; a dirty source checkout produces a warning.
The session runtime context persists the independent worktree id, path, branch,
and base commit, and compact session/action JSON exposes those coordinates as
`isolation`. This is a runtime fact, not ownership: Session deletion, restore,
Fork, and purge never retain or release a worktree.
Successful managed worktrees have an explicit lifecycle. Startup recovery and
periodic Host workers never remove them. The Workspace managed-worktree API
lists resources and deletes one only on an explicit request; deletion refuses
a dirty checkout or a branch with commits beyond the recorded base commit.
Failure before the creating Session transaction commits still rolls back the
new worktree as part of that transaction.
## Naming Rules
Command path segments and input names use lowercase kebab-case.
Examples:
- `issue list`
- `agent get`
- `agent respond`
- `topic-id`
- `wait`
- `page-size`
Do not introduce snake_case, camelCase, spaces, or leading dashes in command
path segments or input names.
Command IDs should stay stable. Renaming a builtin command path or deleting a
builtin command is a user-visible compatibility change. It does not affect app
commands unless an app attempts to use a reserved top-level scope.
Reserved app CLI scopes are owned by builtin commands and help/status plumbing.
Workspace apps must not claim these scopes:
- `agent`
- `help`
- `issue`
- `plan`
- `status`
## Input Schema
Builtin input schema is generated from typed Go input structs. The generated
schema should stay within the same small object-only subset used by app CLI
manifests unless the daemon HTTP contract is intentionally extended:
- top-level `type: "object"`
- `properties`
- `required`
- property `type`
- property `description`
Supported property types:
- `string`
- `boolean`
- `integer`
- `array` with string items for repeatable string flags
The framework may support Go `int64` internally, but the capability schema
should still expose it as `integer`.
Repeatable string inputs are represented as `[]string` fields in builtin input
structs and emitted as `type: "array"` with `items.type: "string"`. The bundled
CLI aggregates repeated flags before invoking the daemon, so commands can expose
inputs such as `--image <path>` multiple times without provider-specific parsing
in the terminal client.
Agent launcher and send commands accept image file inputs through the builtin
agent CLI provider. The CLI provider reads supported local image files, encodes
them as image `PromptContentBlock` values, and appends them after the text block
created from `--prompt`. Keep this compatibility conversion in the CLI provider
layer; downstream agent session services should receive structured prompt
content, not raw CLI image flags.
When an agent delegates work through `agent start --agent-id <agent-id>`, local
file references in the handoff prompt should use
`[@filename](/absolute/path)` instead of bare paths. Images have two valid
representations, and the delegating agent should choose one per image: pass
`--image <localPath>` for structured visual input, or use
`[@filename](/absolute/path)` in the prompt when preserving the file reference's
prompt/turn ordering is more important. Do not duplicate the same image through
both representations unless the user explicitly asks.
Input structs should use tags for CLI field names, validation, and recovery
hints. Required inputs must include a recovery hint when a user can reasonably
discover the value with another Tutti command.
Example:
```go
type IssueListInput struct {
TopicID string `cli:"topic-id" validate:"required" hint:"Use issue topic list --json to discover workspace topics."`
Status string `cli:"status"`
Search string `cli:"search"`
PageSize int `cli:"page-size" validate:"min=1,max=100"`
PageToken string `cli:"page-token"`
}
```
## Input Binding And Validation
The framework owns builtin input binding.
Rules:
- ignore unknown inputs for forward compatibility across app and daemon versions
- accept strings from `apps/cli` for string, boolean, and integer fields
- parse boolean strings using ordinary CLI-friendly values such as `true` and
`false`
- parse integer strings as base-10 integers
- reject invalid type conversions
- reject missing required inputs
- enforce declared numeric ranges
- keep business-specific cross-field validation in command code
Error wording should be consistent:
- missing required input: `required input "topic-id" is missing`
- invalid input: `invalid input "page-size"`
All builtin input validation failures must wrap `cliservice.ErrInvalidInput` so
the existing invoke route status-code mapping remains stable.
## Recovery Hints
Recovery hints are diagnostic guidance for humans and agents. They are not a
stable localization contract.
Required inputs should include a hint when there is a known discovery command.
For example, `topic-id` should mention `issue topic list`.
Until the HTTP invoke error response has a structured hint field, framework
input errors may include the hint in the diagnostic message while still
wrapping `cliservice.ErrInvalidInput`.
When a structured invoke error hint is added later, add it as an optional
backward-compatible field and keep existing error classification stable.
## Workspace Resolution
Builtin specs must declare their workspace policy:
- `required`: a workspace is required, and startup workspace resolution may be
used when the request does not provide one
- `startup-default`: use the requested workspace when provided, otherwise use
the startup workspace
- `optional`: the command may run without a workspace
Workspace resolution belongs in `tuttid`, not in `apps/cli`.
Workspace app commands keep using `appcli.Registry` workspace resolution. Do
not route app commands through the builtin framework to reuse workspace logic.
Do not apply builtin summary/detail JSON view projection to app command output.
## Output Contract
Builtin output formatting is generated from the command output spec and the
typed business result.
The framework should support these output shapes:
- table columns plus row projection
- summary JSON projection
- detail JSON projection
- plain text projection
- markdown text projection
Do not use reflection to expose entire business structs as CLI JSON. JSON
output should be an explicit projection so runtime-only fields, private fields,
large payloads, and unstable implementation details do not leak into the CLI
contract.
For list commands:
- terminal default should usually be `table`
- JSON default must be `summary`
- summary JSON should include the records needed for automation plus pagination
metadata when available
- lists are for discovery; use the matching get command for detailed context
For get commands:
- default output should usually be `json`
- JSON default must be `detail`
- detail JSON should include the detailed record and closely related child
records or context
For action commands:
- output should confirm the action result without dumping full state
- table output is acceptable for interactive terminal use
- JSON default must be `summary`
- summary JSON should be compact and script-friendly
Builtin commands should not use a bare JSON formatter. They should declare
`JSONViews` with the default view required by their command kind. Raw JSON is
allowed only for narrow existing payloads with `RawJSON: true` and a reason.
Summary JSON must not include obvious UI or audit fields such as
`creatorAvatarUrl`, `creatorDisplayName`, or `creatorUserId`. It should also
avoid large default fields such as `content`, `runtimeContext`, `contextRefs`,
or unbounded messages. Do not use reflection to expose whole business records.
The existing `--json` CLI behavior remains client-side compatible: the client
requests JSON output mode, and the daemon chooses the stable JSON view declared
by the command spec.
## Client Compatibility
`apps/cli` should remain a thin daemon client.
It may:
- discover capabilities
- match command path segments
- parse flags according to `InputSchema`
- request JSON when `--json` is present
- render `CommandOutput`
It must not:
- duplicate daemon business validation
- resolve workspace business rules
- call desktop, renderer, or agent runtime internals directly
- hardcode builtin command-specific output logic beyond temporary positional
argument conveniences already owned by the CLI
Future CLI enhancements such as non-TTY default JSON, `--format full`, and
structured recovery hint rendering are client features. They should be added
after the daemon framework exists and should not be required for builtin
framework migration.
## Compliance
Framework tests should cover:
- struct tags to input schema generation
- string, boolean, integer, and int64 binding
- required input validation
- numeric min/max validation
- unknown input is ignored
- no-input commands ignore extra input
- table, JSON, plain, and markdown output formatting
- command registration defaults
Builtin command compliance tests should assert:
- each builtin command has a valid spec
- capabilities match the generated spec
- command path segments and input names are kebab-case
- required inputs have hints when discoverable
- list commands declare compact JSON or an explicit opt-out reason
- declared default output mode has a formatter
- app CLI commands are not validated as builtin framework commands
Run focused daemon CLI tests before finishing framework or provider changes.
Broader daemon changes should also follow the normal `services/tuttid`
validation rules.