skip to main content

kb — CLI and TUI — Implementation Plan

See cli-tui-design.md for the full audit and confirmed decisions. This covers the phased build: busy_timeout fix, cmd/kb scaffold, verb groups, the folded-in merge verb (retiring cmd/kbmerge), JSON output, then the TUI.

Work items ordered W1 → W8. Per this workspace’s TDD-first convention, each verb group’s tests are written before its handler, confirmed red, then implemented. No CLI framework, no new dependency until W7 (bubbletea, confirmed in the design).


W1 — busy_timeout fix

File to modify

knowledge.go — add PRAGMA busy_timeout = 5000; to the schema const (alongside the existing PRAGMA foreign_keys = ON/PRAGMA journal_mode = WAL).

Test to add

TestOpen_SetsBusyTimeout — open a db, query PRAGMA busy_timeout;, assert it’s 5000. Simple, no red/green ceremony needed for a one-line PRAGMA addition, but still worth a regression test so a future schema edit can’t silently drop it.

Acceptance criteria


W2 — cmd/kb scaffold: dispatch, global flags, usage

Files to create

File Contents
cmd/kb/main.go main(): parse --db/--json (global, before the verb), resolve dbPath via knowledge.DefaultPath(cwd) or the --db override, open the KB once, dispatch to the matched verb’s handler, print top-level usage on no/unknown verb or -h/--help/help
cmd/kb/dispatch.go verbs map[string]verbFunc table + dispatch(kb *knowledge.KnowledgeBase, jsonOut bool, args []string, out, errOut io.Writer) int (returns the process exit code)
cmd/kb/output.go Shared helpers: printJSON(out io.Writer, v any) error, printError(errOut io.Writer, jsonOut bool, err error) (implements decision 2’s stderr/JSON-envelope/exit-code convention in one place, reused by every verb)

verbFunc signature (each verb group implements one or more of these):

type verbFunc func(kb *knowledge.KnowledgeBase, jsonOut bool, args []string, out io.Writer) error

Handlers take an already-open *knowledge.KnowledgeBase (opened once in main, not per-verb) and the raw remaining args (each handler owns its own flag.FlagSet for verb-specific flags, e.g. --project, --identifier-type).

Tests to add

Acceptance criteria


Files to create

File Verbs Wraps
cmd/kb/project.go project add\|list\|show\|concepts AddProject, Projects, ProjectByName, ProjectConcepts
cmd/kb/observation.go observation add\|list\|show\|sources AddObservation/AddObservationWithSource, Observations, ObservationByID, ObservationSources
cmd/kb/concept.go concept add\|list AddConcept/AddConceptWithIdentifier, Concepts
cmd/kb/link.go link project\|observation LinkProjectConcept, LinkObservationConcept

Per decision 7: show/list print bare rows (the struct as-is), text mode as simple aligned columns (reuse the truncate/pad style already established in harvey/commands_kb.go for familiarity, but this is new, independent code — no shared package with harvey).

Tests to add (one file each, mirroring the verb files above)

For every verb: a success-path test and at least one error-path test (missing required flag, project not found, invalid kind, etc.), following the existing knowledge_test.go pattern of a temp-dir Opened database per test. Text-mode and --json-mode output both asserted for at least one verb per file (doesn’t need to be exhaustive per verb — the shared printJSON/printError helpers from W2 already carry most of that weight).

Acceptance criteria


W4 — Source, search, summary, format verbs

Files to create

File Verbs Wraps
cmd/kb/source.go source add\|list\|show\|remove\|retract\|link\|check-retractions AddSource, ListSources, ShowSource, RemoveSource, RetractSource, LinkObservationSource, CheckRetractions
cmd/kb/search.go search, summary, format Search, Summary, FormatMarkdown

kb format’s output is Markdown text in both modes — --json wraps it as {"markdown": "..."} rather than trying to structure prose, since there’s no natural JSON shape for a formatted document; scripts wanting structured data should call project show/observation list/etc. directly instead.

Tests to add

Same pattern as W3 — one test file per source file, success + error paths per verb.

Acceptance criteria


W5 — merge verb (retires cmd/kbmerge)

File to create

cmd/kb/merge.go — the run(aPath, bPath, outPath string, force bool, out io.Writer) error logic from cmd/kbmerge/main.go becomes the merge verb’s handler almost verbatim (it already only depends on knowledge.CollisionReport/ReconcileCollisions/MergeKnowledgeBases, all in-package now). Flags: -a, -b, -out, -force — unchanged names, so any existing muscle-memory/scripts using kbmerge’s flags map directly onto kb merge’s.

File to delete

cmd/kbmerge/ (the whole directory) — this is the “retire” part of decision 1. Its existing tests, if any get added before this point, move with it; per DECISIONS.md, cmd/kbmerge currently has none.

Tests to add

Port the manual smoke-test pattern already used twice this project (2026-07-27 entries in harvey/DECISIONS.md) into an actual automated test this time: TestMerge_TwoIdenticalCopiesProduceMatchingCounts, TestMerge_CollisionRequiresForce, TestMerge_CollisionReconciledWithForce.

Acceptance criteria


W6 — JSON mode audit pass

Not new functionality — a dedicated review pass across every verb added in W3–W5, since JSON support was supposed to be built in per-verb but is easy to under-test one at a time.

Acceptance criteria


W7 — TUI (bubbletea)

Files to create

File Contents
cmd/kb/tui.go Entry point: runTUI(kb *knowledge.KnowledgeBase) error, called from main() when no verb is given
cmd/kb/tui_model.go The bubbletea.Model — project list (via bubbles/list) as the root view, Enter drills into that project’s observations + concepts (a second bubbles/list), / opens a search prompt (via bubbles/textinput) that calls Search and shows results in a third list, Esc/q navigates back/quits

Read-mostly per decision 4 — no add/edit/link/retract key bindings in this version.

Dependency to add

github.com/charmbracelet/bubbletea + github.com/charmbracelet/bubbles to go.mod — the first use of a charmbracelet package anywhere in this Laboratory (noted in the design doc; not a blocker, just worth the go get/go mod tidy pass being deliberate rather than incidental).

Tests to add

bubbletea models are testable via tea.Model.Update directly without a real terminal — TestTUIModel_EnterDrillsIntoProject, TestTUIModel_SearchFiltersResults, TestTUIModel_EscNavigatesBack, following bubbletea’s own recommended testing pattern (construct the model, send tea.KeyMsgs to Update, assert on the resulting model state) rather than anything terminal-emulation-based.

Acceptance criteria


W8 — Real help text (helptext.go pattern) + docs + final verification

Per design decision 5a: replace W2’s flat usageText constant with the same pattern harvey/helptext.go uses.

Files to create/modify

File Contents
cmd/kb/helptext.go Replaces usage.go. One Pandoc-Markdown man-page constant per topic: HelpText (kb(1), shown by bare kb -h/kb help), ProjectHelpText (kb-project(1)), ObservationHelpText, ConceptHelpText, LinkHelpText, SourceHelpText, SearchHelpText (covers search/summary/format), MergeHelpText. Each follows harvey’s section structure (title block with {app_name}(1) user manual \| version {version} {release_hash}, # NAME/# SYNOPSIS/# DESCRIPTION/# OPTIONS) and is formatted through knowledge.FmtHelp before printing.
cmd/kb/help_dispatch.go printHelp(out io.Writer, topic string) — looks up the right constant (empty topic = HelpText), formats via knowledge.FmtHelp(text, "kb", knowledge.Version, knowledge.ReleaseDate, knowledge.ReleaseHash), writes it. Replaces the current bare printUsage call sites in main.go/dispatch.go.
Makefile (new in this repo — doesn’t exist yet outside what cmt generates) Targets mirroring harvey/Makefile’s pattern: ./bin/kb -help > kb.1.md, one per verb group (./bin/kb project -h > kb-project.1.md, etc.), then pandoc $@.md --from markdown --to man -s > man/man1/$@ for each.

Acceptance criteria

Remaining W8 items (unchanged from the original draft)


Out of scope here