CLI Provider Contract
June 8, 2026 · View on GitHub
Status: v1.0.0 candidate · Last updated: 2026-06-03 · Audience: external CLI provider authors and ADHDev maintainers · See also: marketplace design and the audit of currently shipped providers.
This document is the source of truth for what a CLI provider must export, what it receives as input, what it must return, and how its lifecycle is driven by daemon-core. Everything written here is observable in oss/packages/daemon-core/src/cli-adapters/ — the contract did not exist as a single document before; this is its first complete write-up.
External authors targeting v1.0.0 should rely on this document plus the published @adhdev/provider-types and @adhdev/provider-schemas packages (Apache 2.0). They should not need to read daemon-core source.
Table of contents
- Overview
- Directory layout
provider.json— full reference- Script entry —
scripts/<version>/scripts.js - Handler reference
- Input types
- Output types
- Lifecycle
- Capability handlers (optional)
- Native history
- Resume support
- Settings
- Versioning and compatibility
- Backward-compatibility notes (v1.0.0 ↔ today)
1. Overview
A CLI provider tells ADHDev daemon how to:
- spawn a command-line agent (Claude Code, Codex CLI, Aider, etc.),
- parse its terminal output into structured chat messages,
- detect its status (idle / generating / waiting for approval),
- extract approval modals when they appear,
- optionally read its native session-history files,
- optionally drive capability switches (model picker, slash commands, etc.).
The contract is implemented as a Node.js module loaded at daemon startup. Daemon owns the PTY (via node-pty), reads bytes from the agent, and calls the provider's handlers with structured input — see §6.
A CLI provider is not:
- A long-running process of its own. It's a stateless module (with optional per-session state via
createState). - A network client. The PTY is the only channel between agent and daemon.
- A UI. The dashboard renders what the provider's handlers return.
2. Directory layout
my-cli/ ← provider package root
├── provider.json ← required, top-level manifest
├── scripts/
│ └── 1.0/ ← versioned script directory
│ ├── scripts.js ← required, main entry
│ ├── parse_session.js ← optional, per-handler split
│ ├── parse_approval.js
│ ├── detect_status.js
│ ├── parse_output.js
│ ├── new_session.js ← optional, capability handlers
│ ├── list_models.js
│ ├── set_model.js
│ └── …
├── fixtures/ ← recommended, PTY capture + expected output
│ ├── cold-start.pty
│ ├── cold-start.expected.json
│ └── …
└── README.md ← recommended, brief usage notes
The version directory name (1.0) is the provider's script-set version, matched against compatibility[].scriptDir in the manifest (see §3.6). Multiple versions can coexist to support breaking CLI updates.
fixtures/ is consumed by adhdev provider test (Phase 1 deliverable). They are PTY byte captures plus expected handler outputs. Strongly recommended for any provider that intends to live in the marketplace — they form the regression test suite.
3. provider.json — full reference
A complete reference of every field. Required fields are bold; everything else is optional with sensible defaults documented inline.
{
// ── Identity ─────────────────────────────────────────────────────────
"type": "my-cli", // *** required *** stable identifier, lowercase kebab-case
"name": "My CLI", // *** required *** short canonical name
"displayName": "My CLI Agent", // optional; defaults to `name`
"category": "cli", // *** required *** must be "cli"
"icon": "🤖", // optional; emoji or single-character glyph
"aliases": ["mycli", "my"], // optional; alternative identifiers accepted at launch
// ── Engines + versioning ────────────────────────────────────────────
// Added in v1; range against daemon contract. See §13.
"engines": { "adhdev": "^1.0.0" },
"providerVersion": "1.0.0", // optional; the provider package's own SemVer
"contractVersion": 2, // optional; reserved for legacy compatibility
"status": "Stable", // optional; Stable | Beta | Experimental | Deprecated
"details": "Terminal only", // optional; short blurb shown in marketplace
// ── Process spawn ───────────────────────────────────────────────────
"binary": "my-cli", // *** required *** binary basename. Used for `which`-style detection.
"versionCommand": "my-cli --version", // optional; how the daemon probes the installed version
"spawn": { // *** required ***
"command": "my-cli", // *** required *** the actual program to launch
"args": ["--interactive"], // optional; default []
"shell": false, // optional; true to wrap in shell. Defaults to false.
"env": { "MY_CLI_THEME": "dark" } // optional; merged into spawn env
},
// ── Input/output behavior ───────────────────────────────────────────
"sendDelayMs": 500, // optional; delay between paste of prompt text and Enter
"sendKey": "\r", // optional; default "\r"
"submitStrategy": "wait_for_echo", // optional; "wait_for_echo" | "immediate". Default "immediate".
"requirePromptEchoBeforeSubmit": true, // optional; default false
"allowInputDuringGeneration": false, // optional; default false
"requiresFinalAssistantBeforeIdle": false,
// optional; default false. When true, daemon defers the
// "generating → idle" transition until parseSession includes
// a final standard assistant message. Codex needs this.
// ── Patterns (regex hints for legacy detect path) ───────────────────
// Mostly relevant if your provider relies on the daemon's fallback detector.
// Modern providers express these via primitives in v2 manifests; legacy
// providers may continue to declare patterns here.
"patterns": {
"prompt": [ { "source": "^❯\\s*$", "flags": "m" } ],
"generating": [ { "source": "Working \\([0-9hms\\s]+", "flags": "i" } ],
"approval": [ { "source": "Do you want to proceed", "flags": "i" } ],
"ready": [],
"dialog": []
},
// ── Approval ────────────────────────────────────────────────────────
// The key to send for each button index when resolving a modal.
// "0" → first button label, etc.
"approvalKeys": { "0": "1", "1": "2", "2": "3", "3": "4" },
// Substrings considered "positive" (treated as accept) when auto-approve picks a button.
"approvalPositiveHints": ["yes", "allow once", "approve"],
// ── Timeouts (ms) ───────────────────────────────────────────────────
// All optional. Defaults shown.
"timeouts": {
"ptyFlush": 50, // gap between writes before considering a chunk settled
"dialogAccept": 200, // delay before sending an approval key
"approvalCooldown": 3000, // window during which a freshly resolved modal is suppressed
"generatingIdle": 6000, // fallback "still generating" assumption window
"idleFinish": 5000, // grace before declaring generating→idle final
"idleFinishConfirm": 5000, // second-pass confirmation window
"statusActivityHold": 5000, // PTY-quiet threshold before marking activity ended
"maxResponse": 600000, // hard upper bound on a single generation
"shutdownGrace": 4000, // SIGTERM→SIGKILL window
"outputSettle": 300 // post-write debounce
},
// ── Resume ──────────────────────────────────────────────────────────
// See §11.
"resume": {
"supported": true,
"stopStrategy": "command", // "command" | "ctrl_c" | "signal"
"stopCommand": "exit", // when stopStrategy="command"
"shutdownGraceMs": 4000,
"sessionIdFormat": "uuid", // "uuid" | "free"
"newSessionArgs": ["--session-id", "{{id}}"],
"resumeSessionArgs": ["--resume", "{{id}}"],
"resumeArgs": ["--continue"]
},
// ── Compatibility map ──────────────────────────────────────────────
// Maps installed agent versions to script directories or declarative specs.
"compatibility": [
{ "ideVersion": ">=2.1.0", "spec": "specs/2.1.json" }
],
"defaultScriptDir": "scripts/1.0", // optional; used when no compatibility entry matches
// ── Capabilities (advertised to the dashboard) ─────────────────────
"capabilities": {
"input": {
"multipart": true,
"mediaTypes": ["text", "image"],
"strategies": [
{ "mediaType": "image", "strategies": ["resource_link", "text_fallback"], "native": false, "degradation": ["text_fallback"] }
]
},
"output": { "richContent": false, "mediaTypes": ["text"] },
"controls": { "typedResults": true }
},
// ── Mesh coordinator (optional) ────────────────────────────────────
"meshCoordinator": {
"supported": true,
"mcpConfig": {
"mode": "manual", // "manual" | "auto_import"
"serverName": "adhdev-mesh",
"requiresRestart": true,
"instructions": "...", // user-facing setup string
"template": "my-cli mcp add {{serverName}} -- {{adhdevMcpCommand}} {{adhdevMcpArgs}}"
},
"systemPromptInjection": {
"mode": "config_override",
"flag": "-c",
"template": "developer_instructions={prompt_json}"
},
"delegatedWorkerIsolation": {
"args": [
{
"mode": "config_override",
"flag": "-c",
"key": "mcp_servers.adhdev-mesh.enabled",
"value": "false",
"dedupeKey": "mcp_servers.adhdev-mesh"
}
]
}
},
// ── Native history (optional) ──────────────────────────────────────
// Daemon recognises this and routes read-chat history through the provider's
// readNativeHistory handler (see §10).
"canonicalHistory": {
"format": "my-cli-jsonl",
"watchPath": "~/.my-cli/sessions/**/*.jsonl",
"mode": "native-source",
"contractVersion": "2.0",
"scripts": {
"readSession": "readNativeHistory",
"listSessions": "listNativeHistory"
}
},
// ── Settings (UI toggles, persisted to ~/.adhdev/config.json) ───────
"settings": {
"autoApprove": {
"type": "boolean",
"default": false,
"public": true,
"label": "Auto Approve",
"description": "Automatically approve all tool calls without user confirmation"
}
// see §12 for full setting schema
},
// ── Controls (dashboard chips/buttons that invoke capability scripts) ─
"controls": [
{
"id": "model",
"type": "select",
"label": "Model",
"icon": "🤖",
"placement": "bar",
"order": 10,
"dynamic": true,
"listScript": "listModels",
"setScript": "setModel",
"readFrom": "model"
}
]
}
4. Script entry — scripts/<version>/scripts.js
scripts.js is a CommonJS module. Daemon loads it once per provider per daemon lifetime. The module exports handler functions that the daemon dispatches by name. Handlers are pure where possible; per-session mutable state goes through createState.
4.1 Required exports
Every CLI provider must export the following four handlers. A missing handler is a hard load error.
| Export | Purpose |
|---|---|
parseSession(state, input) | Read PTY buffer → structured session view (messages, modal, status, ...) |
detectStatus(input) | Lightweight status probe used inside hot evaluation loops |
parseApproval(input) | Extract approval modal ({ message, buttons }) or return null |
parseOutput(state, input) | Top-level wrapper, usually calls parseSession, returns full session payload |
4.2 Optional exports
| Export | Purpose | When required |
|---|---|---|
createState() | Factory for per-session mutable state | Recommended when any handler keeps state between calls |
readNativeHistory(input) | Read provider's own on-disk history file | Required when canonicalHistory.mode === "native-source" |
listNativeHistory() | List sessions in the on-disk history root | Required when canonicalHistory.scripts.listSessions is set |
newSession(state, input) | Hook called when starting a fresh session | Optional |
listModels, setModel, listModes, setMode, setEffort, setFast, setCompact, setProvider, setReasoning, setYolo, runSlashCommand, retryLast, rollbackList, undoLast, showProviders, openModelPicker, openReasoningPicker | Capability handlers driven by controls and meshCoordinator configs | Per-provider |
4.3 Idiomatic structure
'use strict';
const path = require('path');
const DIR = __dirname;
function loadModule(name) {
try { return require(path.join(DIR, name)); }
catch { return null; }
}
const detectStatus = loadModule('./detect_status.js');
const parseSession = loadModule('./parse_session.js');
const parseApproval = loadModule('./parse_approval.js');
const parseOutput = loadModule('./parse_output.js');
module.exports.createState = () => ({ /* per-session mutable defaults */ });
module.exports.detectStatus = detectStatus;
module.exports.parseSession = parseSession;
module.exports.parseApproval = parseApproval;
module.exports.parseOutput = parseOutput;
This pattern keeps each handler in its own file (easier to test and override).
5. Handler reference
5.1 parseSession(state, input) → ParsedSession
The most important handler. Daemon calls this every time it wants a structured view of the agent's current session.
Signature
function parseSession(
state: ProviderState | undefined,
input: CliScriptInput
): ParsedSession;
Returns (ParsedSession):
{
status: 'idle' | 'generating' | 'waiting_approval' | 'starting' | 'error';
messages: Array<CliChatMessage>;
modal: { message: string; buttons: string[] } | null;
parsedStatus: string | null; // optional; for debug surfaces
errorMessage?: string;
errorReason?: string;
providerSessionId?: string; // when the provider can extract the agent's own session id
transcriptAuthority?: 'provider' | 'daemon';
coverage?: 'full' | 'tail' | 'current-turn';
}
Rules
messagesis the user-visible chat. Order is oldest-first. Roles:'user' | 'assistant' | 'system' | 'tool'.kinddistinguishes'standard' | 'tool' | 'thinking' | 'system'.modalis null when there's no active approval prompt. When non-null, must include bothmessageand at least one button label.- During
waiting_approval,messagesshould still include the visible transcript leading up to the modal. Wiping all messages was a regression we fixed in 2026-06; do not repeat it. providerSessionIdshould be the agent's own conversation/session identifier, not the daemon's runtime UUID. Used for native history mapping.transcriptAuthority: 'provider'tells the daemon to treat your messages as the canonical chat; PTY-derived fallback parsing is suppressed. Use this when your provider has high-confidence transcript extraction (e.g. via native history files).
5.2 detectStatus(input) → string | null
A lightweight status probe. Called many times during hot paths to drive idle/generating/waiting_approval transitions.
Signature
function detectStatus(input: CliStatusInput): 'idle' | 'generating' | 'waiting_approval' | null;
Returns
'generating'when the agent is actively producing output (spinner visible, tool call in progress, "esc to interrupt" footer, etc.).'waiting_approval'when an approval modal is on screen (paired withparseApprovalreturning non-null).'idle'when the agent is ready for the next prompt.nullwhen uncertain. Daemon treats null as "no change".
Rules
- Should be cheap. No file I/O, no large regex, no per-call state allocation.
- Should not rely on
state(no parameter for it — that's by design). - Generation cues take precedence over idle cues. If both are present in the live window, return
'generating'. The codex-cli "Working visible but flipped to idle" regression in this sprint came from violating this.
5.3 parseApproval(input) → { message, buttons } | null
Extract the approval modal, if any.
Signature
function parseApproval(input: CliApprovalInput): { message: string; buttons: string[] } | null;
Rules
- Return
nullwhen no modal is visible. Returning an empty-button modal is a contract violation and will trigger an auto-approve"1"spam regression — daemon now gates onbuttons.length > 0. buttonsshould be the raw labels as the user sees them. Do not rewrite (e.g. "Yes, and don't ask again for X commands in Y" → "Always allow"); the user wants to see what they're confirming.- Scope your regex search to the live modal frame. Claude TUI redraws the prompt area in pieces; matching on the whole screen surfaces phantom modals from earlier assistant prose. We use a "between-last-two-separators" rule in claude-cli — your provider should do something equivalent.
5.4 parseOutput(state, input) → unknown
The top-level handler. Most providers implement this as a thin wrapper around parseSession plus formatting for the daemon's read_chat envelope:
function parseOutput(state, input) {
const session = parseSession(state, input);
return {
id: 'cli_session',
status: session.status,
title: input.settings?.providerName ?? 'My CLI',
messages: session.messages,
activeModal: session.modal,
providerSessionId: session.providerSessionId,
transcriptAuthority: session.transcriptAuthority,
coverage: session.coverage,
};
}
6. Input types
6.1 CliScriptInput
Provided to parseSession and parseOutput. The richest input shape — contains both raw buffer, rendered buffer, and the terminal screen.
interface CliScriptInput {
buffer: string; // accumulated PTY output, ANSI cleaned
rawBuffer: string; // raw PTY bytes including ANSI sequences
recentBuffer: string; // last ~1000 chars of buffer
screenText: string; // the terminal screen as rendered by ghostty/xterm
workspace?: string; // user's chosen workspace path
workingDir?: string; // same; legacy alias
providerSessionId?: string; // last known provider session id (may be empty)
historySessionId?: string; // alias used for some native-history flows
screen: CliScreenSnapshot;
bufferScreen: CliScreenSnapshot;
recentScreen: CliScreenSnapshot;
messages: CliChatMessage[]; // accumulated from previous parse calls
partialResponse: string; // current in-flight assistant text
isWaitingForResponse?: boolean;
promptText?: string; // the user's prompt for the active turn
settings?: Record<string, any>; // provider settings from ~/.adhdev/config.json
args?: Record<string, any>; // command-specific args (e.g. handler invocation parameters)
spawnAt?: number; // ms timestamp of process spawn; useful for native rollover guards
}
6.2 CliStatusInput
Provided to detectStatus. Trimmed for hot-path performance.
interface CliStatusInput {
tail: string;
screenText?: string;
rawBuffer?: string;
isWaitingForResponse?: boolean;
screen: CliScreenSnapshot;
tailScreen: CliScreenSnapshot;
}
6.3 CliApprovalInput
Provided to parseApproval.
interface CliApprovalInput {
buffer: string;
screenText?: string;
rawBuffer?: string;
tail: string;
screen: CliScreenSnapshot;
bufferScreen: CliScreenSnapshot;
tailScreen: CliScreenSnapshot;
}
6.4 CliScreenSnapshot
The rendered terminal screen, pre-tokenised.
interface CliScreenSnapshot {
text: string;
lineCount: number;
lines: CliScreenLine[];
nonEmptyLines: CliScreenLine[];
firstNonEmptyLineIndex: number;
lastNonEmptyLineIndex: number;
firstNonEmptyLine: CliScreenLine | null;
lastNonEmptyLine: CliScreenLine | null;
promptLineIndex: number;
promptLine: CliScreenLine | null;
linesAbovePrompt: CliScreenLine[];
linesBelowPrompt: CliScreenLine[];
}
Provider helpers can use these without re-parsing the screen.
7. Output types
7.1 CliChatMessage
interface CliChatMessage {
role: 'user' | 'assistant' | 'system' | 'tool';
content: string | unknown; // string is the common case; arrays/objects for rich content
timestamp?: number;
receivedAt?: number;
kind?: 'standard' | 'tool' | 'thinking' | 'system' | string;
id?: string;
index?: number;
providerUnitKey?: string; // stable identity used by ChatSourceMachine
bubbleId?: string;
bubbleState?: string;
senderName?: string;
meta?: Record<string, unknown>;
}
providerUnitKey is the most important field for v1. It lets ChatSourceMachine (introduced 2026-06) compare native vs PTY messages by stable identity instead of array position. Providers with native history should populate this from their on-disk session-id + turn-number.
7.2 ParsedSession
See §5.1.
8. Lifecycle
spawn provider binary
↓
PTY stream emits bytes
↓
daemon accumulates → buffer, rawBuffer, recentBuffer, screen
↓
on every settle (after PTY quiets for `timeouts.outputSettle` ms):
┌───────────────────────────────────────────────────────────────┐
│ detectStatus(input) → 'idle' | 'generating' | 'waiting…' │
│ parseApproval(input) → modal | null │
│ parseSession(state, input) → ParsedSession │
│ parseOutput(state, input) → wrapped session for read_chat │
└───────────────────────────────────────────────────────────────┘
↓
daemon updates its CliStateEngine:
- status transitions
- approval modal lifecycle
- turn boundaries (user prompt → assistant response → idle)
↓
dashboard subscribes to session.chat_tail → receives latest read_chat
The handlers themselves are stateless from the daemon's perspective: every call provides the full current buffer. Per-session continuity is via the state parameter returned by createState().
8.1 State
module.exports.createState = () => ({
lastSeenSessionId: '',
modelChips: [],
// … any mutable per-session info
});
Daemon constructs one state per session (per PTY) and passes it to every handler call for that session. State persists across handler calls but is destroyed when the session ends.
8.2 Status state machine (driven by daemon, not provider)
Provider handlers report what they observe. Daemon owns what the user sees. Specifically:
- A
detectStatus → 'waiting_approval'paired withparseApproval → nulldoes not put the session inwaiting_approval. v1.0 contract: daemon ignores the status until a real modal is captured. This was a regression in 2026-06 (the"1"spam) — gate is now hard. requiresFinalAssistantBeforeIdle: truedefers'generating' → 'idle'untilparseSession.messagesincludes a final standard assistant message.allowInputDuringGeneration: truepermits the user to send a new prompt mid-turn. Default false.
9. Capability handlers (optional)
Beyond the four required handlers, capability handlers power dashboard controls and external orchestration. None are required; declare only what your CLI supports.
| Handler | Triggered by | Input | Output |
|---|---|---|---|
listModels(state, input) | dashboard model picker | { workspace, settings, args } | { models: Array<{ id, label }> } |
setModel(state, input) | dashboard model select | { args: { model: string } } | { writeRaw?: string, sendMessage?: string, ok: boolean } |
listModes(state, input) / setMode(state, input) | dashboard mode picker | ditto | ditto |
setEffort / setFast / setCompact / setProvider / setReasoning / setYolo | dashboard toggle | ditto | ditto |
newSession(state, input) | "new session" UI | {} | { writeRaw?: string } |
runSlashCommand(state, input) | dashboard slash menu | { args: { command: string } } | { writeRaw?: string } |
openModelPicker / openReasoningPicker | dashboard control | {} | { writeRaw?: string } |
retryLast / undoLast / rollbackList / showProviders | dashboard actions | {} | depends |
readNativeHistory(input) | read_chat calls when canonicalHistory.mode === "native-source" | { historySessionId, workspace, spawnAt? } | { messages, source, providerSessionId, sourcePath, sourceMtimeMs, nativeHistoryCoverage } or null |
listNativeHistory() | list_saved_sessions calls | {} | { sessions: Array<…> } |
Capability handlers that drive the agent should return either:
{ writeRaw: string }— daemon writes the string directly to the PTY (typically command + Enter).{ sendMessage: string }— daemon uses the prompt-submission flow (echo wait, etc.).{ ok: true, ... }— pure read; no PTY interaction.
10. Native history
When the agent maintains a session-history file on disk that is more complete or canonical than the daemon's PTY parse, the provider can declare it via canonicalHistory and implement readNativeHistory / listNativeHistory.
"canonicalHistory": {
"format": "my-cli-jsonl",
"watchPath": "~/.my-cli/sessions/**/*.jsonl",
"mode": "native-source",
"scripts": {
"readSession": "readNativeHistory",
"listSessions": "listNativeHistory"
}
}
function readNativeHistory(input: {
historySessionId?: string;
workspace?: string;
spawnAt?: number; // ms timestamp of provider spawn — use to reject pre-spawn rollovers
}): NativeHistoryReadResult | null;
type NativeHistoryReadResult = {
source: 'provider-native';
providerSessionId: string;
messages: Array<{
role: 'user' | 'assistant' | 'system' | 'tool';
content: unknown;
kind?: string;
providerUnitKey?: string;
receivedAt?: number;
workspace?: string;
historySessionId?: string;
}>;
sourcePath: string;
sourceMtimeMs: number;
nativeHistoryCoverage: 'full' | 'tail' | 'current-turn';
workspace?: string;
unavailableReason?: string;
};
ChatSourceMachine consumes messages[].providerUnitKey + array order to compare native vs PTY. If you cannot supply stable per-message unit keys, return nativeHistoryCoverage: 'tail' so the machine treats your messages as a recovery snapshot, not a watermark.
The spawnAt parameter was added in 2026-06 to fix a race where workspace-only resolution picked the previous run's history file. If your provider uses workspace-only fallback, reject results whose sourceMtimeMs is older than spawnAt - 3000.
11. Resume support
Declare resume capability so the daemon can reattach to a previous agent session.
"resume": {
"supported": true,
"stopStrategy": "command", // "command" | "ctrl_c" | "signal"
"stopCommand": "exit", // when stopStrategy="command"
"shutdownGraceMs": 4000,
"sessionIdFormat": "uuid", // "uuid" | "free"
"newSessionArgs": ["--session-id", "{{id}}"],
"resumeSessionArgs": ["--resume", "{{id}}"],
"resumeArgs": ["--continue"]
}
newSessionArgsis appended tospawn.argswhen starting a new session under daemon-issued id.resumeSessionArgsis appended when resuming a specific session id.resumeArgsis appended when "continue most recent" semantics are wanted.
The {{id}} placeholder is replaced at launch time with the actual session id.
12. Settings
Settings are persisted to ~/.adhdev/config.json under providerSettings.<type>. Schema (per setting):
{
"type": "boolean" | "number" | "string" | "select",
"default": <value>,
"public": true, // exposed in dashboard UI
"label": "Auto Approve",
"description": "…",
"options": ["low", "medium", "high"], // when type === "select"
"min": 30, "max": 600 // when type === "number"
}
Common settings (recommended to declare for consistency across providers):
| Key | Type | Purpose |
|---|---|---|
notifications | boolean | Display desktop notifications on state changes |
autoApprove | boolean | Auto-approve modal prompts |
approvalAlert | boolean | Push notification on approval needed |
longGeneratingAlert | boolean | Push notification when generation exceeds threshold |
longGeneratingThresholdSec | number | Threshold for the above |
autoApprove default must be false. Daemon prior to 2026-06 forced default true; that was a bug — see oss/packages/daemon-core/src/providers/provider-loader.ts commit history.
13. Versioning and compatibility
13.1 Two version axes
engines.adhdev— SemVer range against the ADHDev contract version. v1.0.0 of the contract corresponds to this document.- Per-version
compatibility[]— maps installed agent versions to script subdirectories or declarative spec files. Different CLI versions can require different parsing.
13.2 Breaking change protocol
Once a contract version is published, breaking changes only happen on major bumps. The provider-marketplace.md design document specifies the policy (1-year grace period after deprecation, automatic migration tools, daemon ↔ primitive compatibility matrix). v1 of this contract follows that policy.
13.3 Contract version vs marketplace tier
- Contract version is what the daemon understands.
- Marketplace tier (verified / declarative-only / extended) is independent: a v1-contract provider can be any tier.
14. Backward-compatibility notes (v1.0.0 ↔ today)
This document is the first complete contract spec. All 10 currently shipped CLI providers (aider-cli, antigravity-cli, claude-cli, codex-cli, cursor-cli, gemini-cli, github-copilot-cli, goose-cli, hermes-cli, opencode-cli) implement the contract as described above. The audit document audit-cli-v1.md details, per provider, which parts of the current implementation map directly to v1 contract terms and which parts will benefit from the upcoming declarative primitives (Phase 1 of the marketplace plan).
Things that are technically allowed today but discouraged for marketplace v1:
- Direct
_shared/native_history.jsimports. Currently 7 of 10 providers import this 1239-line shared helper. v1 contract treats native history as a per-provider implementation; the shared helper continues to work but will be split into smaller per-format adapters in Phase 1. patterns.*regex declarations. These were used by daemon's fallback detector. Modern providers express the same intent via primitives. We keeppatternsfor legacy reasons in v1.- String-mutation
argsshapes. Capability handlers should consume typedargs(e.g.{ model: string }) rather than freeformargs: any. The audit calls out per-handler offenders.
None of the above blocks v1 launch. They are the prioritised refactor targets for v1.1.
Appendix — Quick reference card
// Minimal CLI provider — copy + adapt.
// provider.json
{
"type": "my-cli",
"name": "My CLI",
"category": "cli",
"engines": { "adhdev": "^1.0.0" },
"binary": "my-cli",
"spawn": { "command": "my-cli", "args": [], "shell": false, "env": {} },
"approvalKeys": { "0": "1", "1": "2" },
"approvalPositiveHints": ["yes", "approve"],
"compatibility": [{ "ideVersion": ">=0.1.0", "scriptDir": "scripts/1.0" }],
"settings": {
"autoApprove": { "type": "boolean", "default": false, "public": true, "label": "Auto Approve" }
}
}
// scripts/1.0/scripts.js
'use strict';
module.exports.createState = () => ({});
module.exports.detectStatus = (input) => /\bWorking\b/.test(input.screenText) ? 'generating' : 'idle';
module.exports.parseApproval = (input) => null; // no approval support
module.exports.parseSession = (state, input) => ({
status: 'idle',
messages: [],
modal: null,
parsedStatus: null,
});
module.exports.parseOutput = (state, input) => ({
id: 'cli_session',
status: 'idle',
title: 'My CLI',
messages: [],
activeModal: null,
});
Real providers add more. See the claude-cli, codex-cli, antigravity-cli, and hermes-cli implementations for production-quality examples.