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

49 KiB

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:

{
  "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:

{
  "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 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:

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.