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
link(root, src, *, needs=None, in_=None, to=None, rel='see-also', agents=None, skills=None, trains=None, actor='api')
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
unlink(root, src, *, needs=None, in_=None, to=None, agents=None, skills=None, trains=None, actor='api')
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.
Link
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.