Skip to content

Python API

The library behind every surface. Import from the top level:

from kumihimo import Plan, braid, export, KumihimoError

plan = Plan.load("myplan/")
findings = plan.check()  # list[Finding], errors first
prompt = plan.braid(strategy="grouped", where={"status": "todo"})
mermaid_source = export.mermaid(plan)

Mutations go through the ops layer — the same functions the CLI, editor, and MCP server call:

from kumihimo.core import ops

ops.add_node(plan.root, "cache", "task", needs=("api-endpoints",))
ops.link(plan.root, "cache", to="redis-outage", rel="threatened-by")
ops.rename_node(plan.root, "cache", "response-cache")

Every op loads fresh from disk, writes atomically, and returns the reloaded result; structural mistakes (dangling targets, cycle-closing edges, id collisions) raise KumihimoError with a printable message.

Plan

A loaded plan: manifest, resolved kinds, node records, findings.

@purpose The one aggregate every client holds; a snapshot of disk that knows how to write its own changes back. @tags plan, aggregate

manifest property

The parsed manifest.

@purpose Compile settings and plan meta for clients and the braid.

nodes property

Nodes by id.

@purpose The read-only view almost every consumer wants.

records property

Node records by id, in sorted-file order.

@purpose The mutable layer ops work on; most readers want .nodes instead.

root property

The plan directory.

@purpose Clients need it for messages and relative paths.

__init__(loaded, kinds, kind_findings)

Wire a loaded plan to its resolved kind system.

@purpose Constructor for Plan.load; direct use is for tests.

braid(**kwargs)

Compile this plan (or a slice) into one prompt; see compile.braid.

@purpose The public sugar over the pipeline — accepts strategy, where, from_, until, in_, diagram, dry; returns the woven text.

check()

Everything wrong or suspicious about the plan, errors first.

@purpose Load findings plus every rule in validate.py, in deterministic order — the one validation answer every surface renders.

load(path) classmethod

Load the plan directory at path.

@purpose The library's front door; content problems become findings on the returned Plan, only "not a plan" raises.

node(node_id)

One node by id, or a clean error naming it.

@purpose KeyError with context, as a KumihimoError clients can print.

save()

Write every dirty record; return the rel paths written.

@purpose Fidelity contract surface: an untouched plan saves to an empty list and zero writes.

The braid

Compile a plan (or a slice of it) into one deterministic prompt.

@purpose The whole point of the tool, as one function: same plan and arguments in, byte-identical text out. for_agent (--for) compiles one agent's work orders: nodes that mention it, the skills those tasks in turn mention, and the agent itself — anything else the agent's own edges point at degrades through the ordinary stub machinery, like any out-of-selection dependency. Opens with Ground with: when the agent carries a retrieval field. @tags braid, pipeline, for-agent

Everything one braid produced.

@purpose Clients that want more than the text (the editor's preview, tests, --dry) get the structure without re-deriving it.

Operations

@file kumihimo/core/ops.py @purpose The one mutation path (invariant 1): add, update, link, unlink, rename, remove, restore — each loads fresh from disk, edits the record's live frontmatter map so comments survive, refuses structural nonsense (dangling or wrong-kind targets, cycles, id collisions) with clean errors, saves atomically, and returns the reloaded result. link/unlink cover needs, in, links, and the three mention keys (agents, skills, trains — PLAN2 §3.2); mentions carry no ordering, so only needs gets the cycle guard. restore_node (K45) is remove_node's real inverse: it writes a prior file's exact bytes back verbatim rather than building frontmatter through the record system, since there is nothing to edit — content already IS the final file. @layer core @tags ops, mutations, invariant-1, referrer-fixup, mentions, restore @related kumihimo/core/store.py (the records and saves; write_node_text is restore_node's one bypass of the record system), kumihimo/core/graph.py (the cycle guard on link), kumihimo/core/plan.py (Plan.load used before and after) @design PLAN.md §7.1 invariant 1, queue item K5; PLAN2.md §3.2; queue item K45

add_node(root, node_id, kind, *, title=None, body='', fields=None, needs=(), in_=(), actor='api')

Create a node file with canonical frontmatter and return it.

@purpose The only way tools bring a node into existence; every edge target must already exist and the id must be free. actor (K31) names who's asking — "cli"/"mcp"/"editor" from the thin clients, else the default "api" for a raw library caller — logged to the advisory events file, never persisted on the node itself. @tags ops, add

Draw one edge from src: a dependency, a membership, an annotation, or a mention (agents=/skills=/trains=).

@purpose Exactly one edge per call; needs-edges are refused (with the path) when they would close a cycle, so no tool can write one. Mentions carry no ordering — no cycle guard applies to them — but are refused when the target is the wrong kind. actor (K31) — see add_node's own note. @tags ops, link, cycle-guard, mentions

remove_node(root, node_id, *, force=False, actor='api')

Delete a node; with force, strip every reference to it first.

@purpose A referenced node refuses to die quietly — the error names the referrers, and force removes the edges in the same operation so the plan is never left dangling. actor (K31) — see add_node's own note; the logged targets are the removed id plus every referrer stripped (empty unless force found any). @tags ops, remove, mentions

rename_node(root, old, new, *, actor='api')

Move a node to a new id, fixing every referrer and the view layout.

@purpose Renames are safe or they don't happen: the renamed file's bytes never change (the id is the filename), and no reference is left pointing at the old name. actor (K31) — see add_node's own note; the logged targets are the old id, the new id, and every referrer whose file got rewritten — everything a payload digest diff will see change. @tags ops, rename, referrer-fixup, mentions

restore_node(root, node_id, content, *, position=None, actor='api')

Bring a removed node's file back, byte-for-byte.

@purpose remove_node's real inverse (K45): writes content back as the node's exact prior file bytes — no frontmatter parse, no re-render, since content already IS the final text, newline style and BOM baked in exactly the shape store.py's own load captured them in (see store.write_node_text). Refused, cleanly, when the id already exists: silently overwriting a file that has since been recreated (a fresh add_node, or a second restore racing the first) would be exactly the byte-fidelity violation invariant 7 exists to prevent — and it is, honestly, this op's own precondition failing in words: a restore trail entry means "this id is absent," and this is that check. position, when given, restores the node's prior view.yaml layout entry; omitted, the node simply has none yet, same as anything freshly added. Referrers a force-remove stripped are never resurrected — this returns the node file only, nothing else on disk is touched, so those edges stay stripped until undone from their own trail entries (see ops_api's restore inverse and docs/howto/editor.md's undo section). actor (K31) — see add_node's own note. @tags ops, restore, remove-inverse, undo

Remove one edge from src.

@purpose The inverse of link; removing an absent edge is an error, not a shrug, so tools notice their own stale state. actor (K31) — see add_node's own note. @tags ops, unlink, mentions

update_node(root, node_id, *, kind=None, title=None, body=None, priority=None, set_fields=None, unset_fields=(), actor='api')

Change a node's kind, title, body, priority, or kind-defined fields.

@purpose Field values stay permissive (check reports schema breaches); structure stays strict (reserved keys are not fields, kinds must exist). actor (K31) — see add_node's own note. @tags ops, update

Model

@file kumihimo/core/model.py @purpose The pure data model: nodes with their two semantic edge kinds, annotation links, three mention edges (agents, skills, trains — PLAN2 §3.2), findings, field specs, kind definitions, and the manifest. No IO, no behaviour beyond validation and defaults. @layer core @tags model, node, edges, kinds, manifest, findings, mentions @related kumihimo/core/store.py (reads/writes these from disk), kumihimo/core/kinds.py (resolves and validates kind fields), kumihimo/core/validate.py (checks mention targets and kinds) @design PLAN.md §3.1-3.2, PLAN2.md §3.1-3.2, §3.6

CompileSettings

Bases: BaseModel

Plan-level braid defaults from the manifest.

@purpose The user's standing answers to "how should this compile" so the CLI flags are overrides, not requirements.

FieldSpec

Bases: BaseModel

Schema for one kind-defined field.

@purpose Small enough to author by hand in YAML, rich enough to drive validation now and editor forms/JSON Schema later.

Finding

Bases: BaseModel

One validation result, error or warning, tied to where it was found.

@purpose The unit check returns everywhere — CLI table, editor panel, MCP — so every surface reports identically.

render()

One-line human form.

@purpose Shared formatting so CLI and logs agree.

KindDef

Bases: BaseModel

A node kind: its field schemas and (from M2) its render template.

@purpose Where node meaning lives, per the generic/opinionated line — the compiler never reads these fields directly, templates do.

Bases: BaseModel

An annotation edge: free-form relation, zero compiler semantics.

@purpose The pressure valve of the model — any relationship users invent fits here without core changes (PLAN.md §3.1).

Manifest

Bases: BaseModel

Parsed kumihimo.yaml: plan meta, kind pack + overrides, compile defaults.

@purpose Everything plan-wide in one validated object; raw kind overrides stay unparsed here and resolve in kinds.resolve_kinds.

Node

Bases: BaseModel

One thread of the braid: identity, prose, order, membership, annotation, and mention (who's assigned, what skill, who trains it).

@purpose The six things core understands about a node; everything else lives in the kind-validated fields bag. @tags node, needs, membership, mentions

default_title(node_id)

Humanize an id into a display title: last segment, dashes to spaces.

@purpose Titles are optional in frontmatter; every node still renders with one.