Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

codedetailsWhen
invalid_slugslug, whyThe slug breaks the shape rule; why says which part.
slug_takenslug, holder, kindAnother 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_mismatchquestion_slug, slugportfolio create --question <question-slug> with an explicit --slug that is not that question’s slug.
slug_given_twicefieldnote 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
}
FieldMeaning
dry_runtrue for a preview.
target, from, new_slugThe note’s path and current slug before the rename, and the slug it takes.
found_by_former_nameThe former name the selector matched, or null.
moves{from, to} pairs; a portfolio’s folder, or a question and its portfolio, move together.
editsOne {path, changes} per rewritten path, in path order; see changes below.
references, notesReferences rewritten (links plus fields) and the notes holding them.
warnings{kind, message, …} entries that do not stop the plan; kinds below.
plan_hashAn opaque digest of the plan; the MCP tool takes it back as expected_plan.
log_lineThe renamed entry without its time stamp, or null for an empty plan.
written, index_staleWhether anything was written; whether the index failed to follow (files are correct, the next open reconciles).
verificationMCP 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:

codedetailsWhen
invalid_slug, slug_takenas aboveThe new slug breaks the rule, or another note holds it.
note_not_renamablepath, reasonreason is type <name>, archived, folder, rename the question, or what stops the alias being recorded.
stem_sharedslug, path, othersAnother note has the same file stem, so links to it cannot be told apart.
stale_planexpected, actualThe vault changed between the preview and the write. Preview again.
reference_driftpath, driftRewriting the references in path would change more than them.
reference_unaddressablepath, keyA reference field names the note in a form the rewrite cannot address.
rename_incompletepaths, sourceA write failed and the rollback could not restore the paths in paths; check them by hand.
behind_upstreambehind, remedyThe 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_progressreasonA 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; outcome is the closure, completed or dropped, and never a parent. This row is the CLI’s own: MCP list_projects with include_closed returns 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]]"
    }
  ]
}
  • project is null when 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.