JSON output
Adding --json to a supported verb swaps the formatted table for machine-readable JSON. These shapes
match the MCP server DTOs, with the exceptions noted below, so you get the same structures from the CLI and from an
AI client. (Which verbs support --json is covered in the CLI overview.)
Write verbs → a result object
Every write verb emits the same small object and runs non-interactively (project complete and
project drop add to it; see below):
{
"path": "projects/surrogate-model.md",
"message": "Created projects/surrogate-model.md"
}
path is the vault-relative file written or updated; message is the human-readable line. For the
two-file cases (e.g. action add --note), path is the file the verb considers primary.
project complete / drop --json → a result or a refusal
A close that goes ahead emits the result object with three more arrays: what the drop let go, and the linked standalone commitments, which no close ever touches.
{
"path": "projects/_done/2026/alpha-study.md",
"message": "Dropped alpha-study. 0 of 5 slots in use. Let go: 1 action, 1 milestone.",
"dropped_actions": ["Define first concrete step (light)"],
"dropped_milestones": ["Submit"],
"untouched_commitments": []
}
A close refused because actions or milestones are open prints the rejection object the MCP server
returns, on stdout, and exits 1:
{
"code": "project_has_open_items",
"message": "project 'alpha-study' has 2 open item(s) — tick the ones that are done, or let them go explicitly",
"details": {
"slug": "alpha-study",
"actions": [{ "text": "Define first concrete step (light)", "note": null, "note_status": null }],
"milestones": [{ "title": "Submit", "date": "2026-12-01", "hard": false }],
"untouched_commitments": [],
"open_items_hash": "76d0efb83a540bf9"
}
}
Slug refusals
An explicit --slug that is refused prints the rejection object on stdout and exits 1, on any of the
eight create verbs (the slug rule). The shape is the one the
MCP server returns, and details depends on the code:
code | details | When |
|---|---|---|
invalid_slug | slug, why | The slug breaks the shape rule; why says which part. |
slug_taken | slug, holder, kind | Another note already holds the name. holder is that note’s path; kind is stem (its filename), folder (the folder of a folder note) or alias (a former name another note still lists). |
portfolio_slug_mismatch | question_slug, slug | portfolio create --question <question-slug> with an explicit --slug that is not that question’s slug. |
slug_given_twice | field | note create given --slug beside --field slug=…; field is slug. |
cdno portfolio create --question "Sparse vs dense attention OOD" --slug surrogate-model --json
{
"code": "slug_taken",
"details": {
"holder": "projects/surrogate-model.md",
"kind": "stem",
"slug": "surrogate-model"
},
"message": "slug 'surrogate-model' is taken by 'projects/surrogate-model.md' (as its stem)"
}
Nothing is written on a refusal. Do not retry with a -2 of your own; ask for another name.
Rename refusals
cdno note rename --json prints the plan on success (see note rename):
{
"dry_run": false,
"target": "projects/old.md",
"from": "old",
"new_slug": "surrogate-model",
"found_by_former_name": null,
"moves": [{ "from": "projects/old.md", "to": "projects/surrogate-model.md" }],
"edits": [
{ "path": "journal/2026/daily/2026-11-06.md", "changes": [{ "kind": "links", "count": 2 }, { "kind": "log_line" }] },
{ "path": "projects/old.md", "changes": [{ "kind": "alias" }] }
],
"references": 2,
"notes": 1,
"log_line": "renamed [[surrogate-model]] — Old\n was: old",
"warnings": [],
"plan_hash": "5c380833154ebb71",
"written": true,
"index_stale": false
}
| Field | Meaning |
|---|---|
dry_run | true for a preview. |
target, from, new_slug | The note’s path and current slug before the rename, and the slug it takes. |
found_by_former_name | The former name the selector matched, or null. |
moves | {from, to} pairs; a portfolio’s folder, or a question and its portfolio, move together. |
edits | One {path, changes} per rewritten path, in path order; see changes below. |
references, notes | References rewritten (links plus fields) and the notes holding them. |
warnings | {kind, message, …} entries that do not stop the plan; kinds below. |
plan_hash | An opaque digest of the plan; the MCP tool takes it back as expected_plan. |
log_line | The renamed entry without its time stamp, or null for an empty plan. |
written, index_stale | Whether anything was written; whether the index failed to follow (files are correct, the next open reconciles). |
verification | MCP only: the read-back proof, null unless a write landed. |
written is whether anything was written (false on --dry-run and for a rename to the current slug); log_line is the unstamped renamed entry appended to today’s daily note. Under --relink the shape is the same, with moves empty, new_slug equal to from, written false when there was no straggler, and log_line the relinked entry. The MCP rename_note tool returns the same object plus verification. A warnings entry is {kind, message, …}: frozen_prefix_already_diverged adds path (an archived referrer that already failed its archival check, rewritten anyway), old_slug_already_an_alias adds holder (the old slug is already another note’s alias, so it will resolve to nothing once shared), and sync_check is a warning of the sync check (a failed fetch, no upstream, a state git could not report, an override that let a behind vault through), on a dry run and a write alike. A changes entry is links (with count), field (with key), alias or log_line. A refusal
prints the usual {code, message, details} object and exits 1, writing nothing:
code | details | When |
|---|---|---|
invalid_slug, slug_taken | as above | The new slug breaks the rule, or another note holds it. |
note_not_renamable | path, reason | reason is type <name>, archived, folder, rename the question, or what stops the alias being recorded. |
stem_shared | slug, path, others | Another note has the same file stem, so links to it cannot be told apart. |
stale_plan | expected, actual | The vault changed between the preview and the write. Preview again. |
reference_drift | path, drift | Rewriting the references in path would change more than them. |
reference_unaddressable | path, key | A reference field names the note in a form the rewrite cannot address. |
rename_incomplete | paths, source | A write failed and the rollback could not restore the paths in paths; check them by hand. |
behind_upstream | behind, remedy | The vault is behind commits behind its upstream (after a fetch; for a dry run, as last fetched). remedy says what to do: sync first, or pass --skip-sync-check. |
git_operation_in_progress | reason | A git operation is paused mid-way (reason names it, e.g. .git/MERGE_HEAD present). Not overridable: finish or abort it first. |
search --json → an array of hits
Ranked best-first; each hit:
[
{
"path": "portfolios/sparse-vs-dense-attention-ood/2026-03-15-chen-2025.md",
"note_type": "evidence",
"title": "Chen et al. 2025",
"snippet": "...4x speedup at 95% accuracy on the OOD split...",
"score": 1.7
}
]
A lower score ranks earlier (best match first). No matches → [].
open --json → the resolved path
cdno open today --json
{ "path": "/home/you/vault/journal/2026/daily/2026-08-21.md" }
Absolute, so the value composes from any directory. A reference that resolves to nothing — or to more than one note — is an error, not an empty result.
open --list --json → an array of candidates
Every note in the vault, most-recently-modified first. The plain-text form of the same listing is
tab-separated for piping into a fuzzy finder; --json is for programmatic consumers.
[
{
"path": "projects/surrogate-model.md",
"title": "Surrogate model",
"type": "project"
}
]
title is the note’s body H1, and is null for a note that has none.
list verbs → arrays of summaries
cdno project list --json
[
{
"slug": "surrogate-model",
"status": "active",
"state_snippet": "Mesh scaling works; assembly is the bottleneck",
"top_action": { "text": "Profile the assembly step", "energy": "medium" }
}
]
project list --closed→[{ "slug", "title", "context", "outcome", "closed_on" }], most recently closed first;outcomeis the closure,completedordropped, and never a parent. This row is the CLI’s own: MCPlist_projectswithinclude_closedreturns closed maps as{ "slug", "frontmatter" }.portfolio list→[{ "slug", "question", "evidence_count", "last_updated", "staleness_days" }]stewardship list→[{ "slug", "name", "context", "variant", "tracking_count" }]action list→[{ "text", "energy", "attached": { "slug", "status" } | null }]
show verbs → a detail object
cdno project show surrogate-model --json # same shape as a project-list element
cdno portfolio show --portfolio sparse-vs-dense-attention-ood --json
{
"slug": "sparse-vs-dense-attention-ood",
"question": "Sparse vs dense attention OOD",
"created": "2026-03-01",
"project": "[[projects/surrogate-model]]",
"evidence": [
{
"path": "portfolios/sparse-vs-dense-attention-ood/2026-03-15-chen-2025.md",
"created": "2026-03-15",
"source": "Chen et al. 2025",
"origin": "[[projects/surrogate-model]]"
}
]
}
projectisnullwhen the portfolio isn’t linked to one.- An evidence entry gains a
"kind"field (e.g."pdf") only for attachment stubs; it’s omitted for plain prose notes. stewardship show→{ "slug", "name", "context", "variant", "body_markdown" }.
orient --json → the orientation object
cdno orient --json
{
"commitments": [],
"projects": [
{
"slug": "surrogate-model",
"status": "active",
"context": "work",
"core_question": "[[questions/research/surrogate-generalises]]",
"state_snippet": "Mesh scaling works; assembly is the bottleneck",
"top_action": { "text": "Profile the assembly step", "energy": "medium" },
"held_by": null,
"counts": true,
"waiting_on_empty": true
},
{
"slug": "mesh-study",
"status": "active",
"context": "work",
"core_question": "[[surrogate-generalises]]",
"state_snippet": "Coarse-mesh run validated end to end",
"top_action": { "text": "Write up the convergence plot", "energy": "light" },
"held_by": "the modelling group",
"counts": false,
"waiting_on_empty": false
}
],
"groups": [
{
"parent": { "kind": "question", "path": "questions/research/surrogate-generalises.md" },
"projects": ["surrogate-model", "mesh-study"]
}
],
"slots": { "active": 2, "counted": 1, "cap": 5, "counted_slugs": ["surrogate-model"] },
"lapsed_habits": []
}
projects holds one project-list element per active project; each carries core_question, the
map’s frontmatter value exactly as written, or null when it sets none (project list and
project show elements carry it too). Each also carries three keys about the cap (project list
and project show elements too): held_by is the name of whoever else is moving the map, trimmed,
only when the map’s held_by: names someone else, and null when the map is yours (no held_by:,
or me, tbd or another entry of holder_sentinels) or the value is
unreadable (not a plain string); counts is true when the map holds one of the
max_active_projects slots, that is, it is active and does not name someone else as its holder
(branch on counts, never on the name); and waiting_on_empty is true when the map’s ## Waiting On is missing, empty, or only
(nothing yet). MCP get_orientation projects, get_weekly_context.projects and
get_monthly_context’s parked and stuck entries carry the same three keys; the weekly ones add
changed_this_week and focus_days_while_held (see the
review fields). groups arranges the same projects, by slug, under the
core question they share: a short and a full link to one question group together, since the key is
the question’s vault path (or the link’s raw target when it resolves to no note), and only the
field counts, never a passing mention of the question in a map’s body. Every project falls in
exactly one group, groups follow the order of their first member, and a project with no question
is a group of its own with "parent": null. MCP get_orientation carries groups in the same
shape. slots is the active projects
against the cap: active counts the maps with status: active, counted those holding a slot
(the active maps whose counts is true), which is the number the cap is checked against, cap is the configured max_active_projects, and
counted_slugs names the counted maps. MCP get_orientation carries slots as
{ "active", "cap" }, the object list_projects returns.
Casing
Enumerations serialise in their canonical lowercase/kebab form, matching the MCP DTOs:
status → active/parked/completed/dropped, energy → deep/medium/light, stewardship variant →
flat/expanded, context → work/side-project/household/… So a value is identical whether you
read it from cdno --json or over MCP.