Docs/CLI/HITL delegations (actions)

HITL delegations (solomon actions)

solomon actions is the human-in-the-loop delegation surface behind the Ops Delegation board. An autonomous worker posts an Action, a human resolves it (or the stock action-items widget does, via drag/checkbox), and a blocking await polls until it leaves the pending set. The shared status/answer rules are implemented once, so the CLI and the Ops board agree byte-for-byte.

One Action = one markdown note in Actions/ carrying hitl: v1 frontmatter.

Why actions always prefers a force-mode key

Every other CLI command uses the default token as-is (propose by default). actions is different: it must apply to the live CRDT doc — the Ops Delegation board reads the live doc, and a propose-scoped key would only stage a snapshot the board never sees. So actions looks for a force-mode key in mcp.json's keys[] first, and only falls back to the default (propose) token — with a loud warning that the posted Action will be staged and invisible on the board until approved — when no force key exists.

actions also uniquely opts in to the synced-vault auto-upgrade: when the app is closed and the resolved local target is also a synced vault (has a .solomon/vault.json marker) and a sync credential resolves, the transport auto-upgrades from the raw-fs single-writer core to the Hocuspocus sync peer — so the write actually reaches other devices/the cloud instead of sitting fs-only until the desktop app reseeds it. If a sync credential does not resolve, the CLI still writes fs-only but prints a loud warning ("other devices will NOT see this Action"). Every other CLI command keeps today's fs-only behavior when the app is closed.

Subcommands

solomon actions post

Creates an Action note under Actions/; prints {id, rel_path, transport} as JSON (transport is one of "loopback", "local-fs", "sync-peer" — which transport actually served the create, per the transport-truth-pass contract).

Flags:

  • --kind <question|approval|task> — required.
  • --title <text> — required, non-empty; mirrors the note's H1 and (sanitized) filename.
  • --detail <text> — optional one-line summary.
  • --option <text> (repeatable) — an option for a question; mirrored into - ( ) Option body lines. Only valid with --kind question.
  • --body-file <path> — file whose contents become the note's body prose.
  • --from <agent> — requesting agent/stream. Defaults to $SOLOMON_AGENT, else cli.
solomon actions post --kind question --title "Pick storage" \
  --option R2 --option Railway --detail "Need a call before Friday" --from research-agent

The CLI stamps a ULID id itself (so the app-closed create path — which writes content verbatim and never stamps — still yields a stable handle) and dedupes colliding titles: Actions/<name>.md, then Actions/<name> 2.md, Actions/<name> 3.md, … up to 999 tries. Only a genuine "already exists" collision advances the suffix.

solomon actions list

Lists protocol Actions (hitl: v1 only) as a JSON array, newest first.

  • --status <open|resolved|all> — default open.
solomon actions list --status open

solomon actions await <target>

Blocks until an Action leaves the pending set, then prints the outcome JSON. Re-runs the whole transport decision on every poll — it holds no lock across the wait, so a concurrent resolve in another shell can still land, and it works with the desktop app closed.

  • target — an Action id or an Actions/…md path.
  • --timeout <secs> — give up after this many seconds (default: wait forever).
  • --interval <ms> — poll interval, default 1500.

Exit codes: 0 answered/done · 2 timeout (still open) · 3 rejected · 1 error.

solomon actions await 01J... --timeout 3600

solomon actions resolve <target>

Records a human decision on an Action. First resolution wins: the CLI re-reads the note immediately before writing and refuses (no write) if the status already left the pending set.

  • target — an Action id or an Actions/…md path.
  • Exactly one verb is required:
    • --answer <text> [--free] — answer a question. Must match one of the note's options unless --free is passed. (--answer is only valid on a question.)
    • --approve — approve an approval → done. (Only valid on an approval.)
    • --reject [--reason <text>] — reject → rejected (valid on approval/task/question); --reason is stored as the answer.
    • --done [--note <text>] — complete a task → done. (Only valid on a task.) --note is stored as the answer.
solomon actions resolve 01J... --answer Railway
solomon actions resolve "Actions/Ship it.md" --approve
solomon actions resolve 01J... --reject --reason "too risky"

Status vocabulary and the widget-compatibility mapping

An Action stays blocking (await still waits) while its raw status is one of the pending set (case-insensitive): open, to do, in progress, waiting. open is the protocol's own initial status; to do/in progress/waiting are what the stock action-items widget's status select writes when a human drags a card without going through resolve.

A raw status maps to the protocol's canonical vocabulary (open | answered | done | rejected):

  • anything in the pending set → open;
  • the widget's terminal column, complete, → answered when an answer extracts (frontmatter answer, a body **Answer:** line, or a checked - (x) option), else done;
  • answered / done / rejected pass through (normalized to lowercase);
  • anything else non-pending is reported verbatim.

Answer extraction order: frontmatter answer, then the body's **Answer:** line, then the text of a checked - (x) option, else None. Empty strings never count as an answer. This lets a human resolve entirely through the widget (check an option, drag to Complete) and still have await/list report the correct canonical status and answer — the CLI and widget share the exact option/**Answer:** line grammar.

Body layout

A posted Action's body is: an # <Title> H1, optional prose (from --detail/--body-file), then — for a question — an unchecked - ( ) <Option> line per --option. On resolve, the matching option is checked (- (x)), every other option is left unchecked, any prior **Answer:** line is dropped, and a single **Answer:** <answer> line is appended at the end — so re-resolving never stacks duplicate Answer lines.

Result object shape (list/await/resolve output)

{
  "id": "01J...",
  "rel_path": "Actions/Pick storage.md",
  "kind": "question",
  "status": "answered",
  "answer": "Railway",
  "requested_by": "research-agent",
  "created": "2026-07-11T18:09:12Z",
  "resolved": "2026-07-12T09:00:00Z"
}

Related

  • [[solomon CLI reference]] — global flags, the three-transport model, and the other 17 subcommands actions sits alongside.
  • [[MCP tool surface]] — the propose-vs-force key model actions also reads out of mcp.json.
  • [[Bases and widgets]] — the stock action-items widget whose status/answer conventions this protocol shares byte-for-byte.