CLI reference
The binary is kf. Every command except kf init looks for .works/ from the current directory upwards to the project root, so you can run it from the root or from any subdirectory.
Every lifecycle command also has a canonical git-flow-style spelling: kf feature <verb> or kf bug <verb> with verbs start | list | show | move | approve | ship | finish | drop (ship is the deprecated alias of kf ship) — dispatching to the same handlers (feature move is kf stage, feature finish is kf archive, feature drop is kf cancel, bug start implies --type bug, and so on). Using the wrong noun on an existing item is refused; the flat spellings in this reference stay permanent aliases. kf help <noun> <verb> resolves nested help. An unknown verb exits 1 with unknown verb: <token> and that noun’s verb list; kf feature --help prints the noun help, and kf feature move --help prints Usage: kf feature move.
kf init is the exception: it resolves its path argument against the current directory and does not search upwards. Running it inside a subdirectory of a project that is already initialised creates a second .works/, .kf/ and skills set there rather than finding the existing one.
Setting up and creating work
| Command | What it does |
|---|---|
kf init [path] |
Onboarding. On a TTY it offers Quick setup (the detected defaults) or Customize — which asks for the contexts (comma separated, first is the default, empty stays unrestricted), the stacks, the reviewer, the GitHub repository (detected from origin, none leaves the project unlinked), the agents, whether to install the GitHub sync hook pack (only when a repository is linked), the .gitignore entry and whether to seed a demo feature; without a TTY it takes the defaults. Creates .works/, docs/specs/ (with a README.md when missing) and .kf, installs the project-scope skills, and seeds AGENTS.md when neither AGENTS.md nor CLAUDE.md exists. See agent onboarding for the human walkthrough. |
kf init --defaults |
Non-interactive onboarding on default values, for agents and scripts. |
kf init --minimal |
Skips the questions, not the scaffolding. Creates .works/, docs/specs/ (with a README.md when missing), the skills, .kf/config.json with the schema and the harness.verify lists, the empty .kf/{templates,hooks,review/rules} directories, and AGENTS.md when neither AGENTS.md nor CLAUDE.md exists. What it does not do is copy template, hook or review-rule files into those directories — the full path does that. With --context on a genuinely new project it declares that context, exactly as the full path does. |
kf new <feature> [--context <ctx>] [--goal <text>] [--type feature\|bug] |
Creates a feature or bug in brainstorm; a bug routes through /kf:bug. Feature and context names must match [a-z0-9][a-z0-9_-]* case-insensitively, so LoginFlow is accepted. Context names are then compared case-insensitively against the declared list, which refuses Auth beside auth. When the project declares contexts, an undeclared one is refused with the nearest declared name. Feature specs must declare ## Affected surfaces — the user-facing surfaces the change touches, or N/A — before leaving brainstorm. |
kf list [--json] |
Lists every work item with its kind and state. |
kf doctor [--json] |
Diagnoses the project rather than a work item: stage directories, a config that parses, work item metadata that can be read, the kf plugin and the kf-<name> copies still installed, config fields that no longer mean what they say, unknown_config_key (a top-level .kf/config.json key kf does not read, so it is silently ignored; the action names the intended key when adding or dropping a trailing s gives one, e.g. worktrees → worktree; a warning --fix leaves alone), a repository link missing while the origin remote is a GitHub repo, and the leftovers of the removed shipping stage: statusmap_legacy (project.statusMap still keyed dones or shipping), a leftover ship block in .kf/config.json (ignored — kf ship is deprecated), an orphaned .kf/hooks/shipping.sh that no stage runs any more, a .kf/hooks/dones.sh (it still runs for done, with this warning, only while done.sh is missing; kf doctor --fix renames it to done.sh, keeping its content), a Legacy docs not consolidated: <context> <N>, … WARNING while legacy feature snapshots remain under docs/requirement, docs/use-cases or docs/testplan (action: run kf:docs to consolidate them into living docs under docs/specs/<context>/; --fix never touches them; --json lists the feature names per context under legacyDocs), and docs_branch_unpushed (canonical docs an earlier release’s close committed to a docs/<context>-<feature> branch that is not on origin and has not landed — its tip is not in the default branch and no merged GitHub PR has that branch at that tip as its head; once the local branch is gone, the docs commit recorded at close (meta.docsCommit) stands in for the tip, and a gone branch whose canonical docs are byte-identical on origin/<default> counts as landed; an item closed before docsCommit existed is never looked up on GitHub — only that content comparison can prove it; the warning names why the landing could not be proven; when the branch is gone here and nothing proves it landed, the action is the same decision kf status --all shows (canonical docs for <f> never landed: re-land them on a fresh docs branch for a PR with kf archive <f>, or leave them); when the branch is gone here and origin/HEAD is not set, so nothing can be proven or re-landed, the warning says the docs are not proven landed (never that they never landed) and the action is git remote set-head origin -a, then re-run kf doctor whether or not a docs commit was recorded, and GitHub is not asked; with no origin remote at all it is git remote add origin <url>, then re-run kf doctor; when the merged-PR lookup could not run, the action is the same next step kf status --all gives: nothing to push; re-run kf doctor once the merged-PR lookup can run, or re-land them on a fresh docs branch with kf archive <f>; doctor never writes the record), leftover .works/dones/ or .works/shipping/ folders (kf status migrates them), a .gitignore without .kf/travel/ (--fix adds it), a lifecycle log (<item>/lifecycle.jsonl) that cannot be read or is behind its .kfw.json (a WARNING naming the unreadable line or the fields that differ; an item with no log is not reported; see artifacts), harness_legacy_key (one warning per removed harness key: main, roles, stages, runners, parallel, retries, maxLoops, budget) and meta_legacy_run_state (an item whose .kfw.json still holds drive, runs, sessions or tasks left by the removed kf drive; a pid it names is reported only while that process still looks like a kf drive or worker — its command line names kanban-flow, or a kf or dist/index.js word followed by drive or run (read with ps, 1 s timeout) — so you can stop it yourself; when kf cannot check (no ps, or ps fails or times out) it reports the pid and says it could not confirm; kf never kills it). When worktree.enabled it also probes the domain infra: *.<domainZone> resolving to the proxy listen address, the proxy listening, and the routes file being readable — all WARNING-level, with sudo kf worktree setup as the fix. Read-only, keeps going after the first problem, and exits 1 when anything is at ERROR. Work items that are not valid are counted but never change the verdict — that is the normal state of a pipeline in motion. |
kf doctor --fix |
Applies the repairs that need no human decision, then reports the after-state: recreates missing .works/ stage dirs, writes config defaults when the file is absent, removes legacy fields that are already ignored (stack beside stacks, defaultContext beside contexts), rewrites a legacy project.statusMap (dones → done, shipping dropped), renames .kf/hooks/dones.sh to done.sh when done.sh is missing (content kept), removes the legacy harness keys (keeping harness.verify) and the leftover drive, runs, sessions and tasks item metadata under the item lock (a failed rewrite is an ERROR and exits 1), adds a missing .kf/travel/ entry to .gitignore, opens a lifecycle log (lifecycle.jsonl) for every work item that has none and reconciles one that is behind its .kfw.json (stamping a missing item id and keeping the file’s indentation; a log that cannot be read is left as it is, and so is an item that still holds leftover drive run state), restores missing hook-pack files in a project that uses the pack (lib-github.sh present or sourced by a stage hook; a hook the project edited is never overwritten), fills repository from the GitHub origin remote, leaves an item whose recorded pid is still a running kf untouched until that process stops, reinstalls missing skills, and reinstalls the managed agents (code-reviewer, scout, spec-reviewer, developer, test-writer, automation-writer, researcher) when one is absent or stale (a file of yours under one of those names is left alone); run from a linked git worktree of the package, the global-scope skills/agents repair is refused (an ERROR finding, exit 1, “run it from the primary checkout”) while the other repairs still apply. Broken JSON and unreadable .kfw.json stay broken — fixing them is a judgment call. --json adds a fixed list to the report. |
kf config show [--json] [--global\|--workspace] |
Prints the effective config, each leaf with the layer it came from (global, workspace, project), and the paths of the layer files found. --global / --workspace print only that file, marking each key that does not apply there as not applied. See config layers. |
kf config get <key> [--global\|--workspace] |
Prints one value by dotted path (skills.scope): scalars raw, objects and arrays as JSON. An unset key exits 1 with not set. |
kf config set <key> <value> [--global\|--workspace] [--workspace-dir <path>] |
Writes one value by dotted path to the project file, or to the global or workspace file. The value is parsed as JSON when it parses, else taken as a string. A key kf does not know is refused (is not a kf config key), a key that does not apply at the layer is refused (applies at the project layer only), and so is a result that fails validation; the file is then left unchanged. The write is atomic, and every other top-level key keeps its source text and order. --global creates ~/.kf/config.json when missing. --workspace writes the nearest workspace file, or creates .kf/workspace.json in --workspace-dir or else in the project’s parent. It refuses the home directory, a directory that is not an ancestor of the project, and a directory a nearer workspace file would shadow. |
kf contexts [--json] |
Lists the declared contexts with a work-item count each, marks any context in use but not declared, and prints a survey brief when none are declared. Read-only. See contexts. |
kf show <feature> [--json] |
Shows a work item’s requirement or bug report. |
kf view [--json] |
Workflow statistics in the terminal; the JSON carries metrics, charts and the per-stage detail. |
kf dashboard [--port <1-65535>] |
KPI and chart dashboard with context and kind filters, on port 8787 by default. See how each number is computed. |
Driving the workflow
| Command | What it does |
|---|---|
kf status --change <feature> [--json] |
One work item’s state, including how many gate or hook bypasses have been recorded, and any blocking findings the validator raises. Findings that never block are listed in a Notices block (notices in --json; blockers is unchanged): for an item in planning, estimate_missing and estimate_should_split, and estimate_history when the estimation history cannot be read and the estimate is sized with factor 1. The artifact checklist only says which files exist and are filled; the blocking list is what kf approve or kf stage would refuse on, so the two cannot disagree. The human approval gate is reported by the Approval: field rather than counted as a blocker. When the item has a worktree, its path, branch, domain and port are shown as a Worktree:/Domain: block and in JSON under worktree. kf status (either form) also closes landed items: a review PASS item whose PR is merged — or that has landing: none and nothing ahead of origin/<default> — is closed into done through the same transaction as kf archive. It prints ✓ done <feature> (PR merged: <url>) for each one it closed. An item still waiting prints Awaiting merge: <url> and Next: merge <url> on GitHub, then kf status --change <feature>; a PR closed without merging prints PR closed without merging: <url> and a Next: asking for a human decision (reopen and merge it, open a new PR and record it with kf issues link <feature> <url> --pr --replace, move the item back, or kf cancel); a close that was refused prints Not closed: <reason>. An item with docsBranch set shows Docs branch: <branch>. The first kf status or kf sync in a project that still has .works/dones/ or .works/shipping/ moves those items once (dones → done, shipping → review; a merged PR is closed at once). |
kf status --all [--json] |
Every work item, backlog and done included. The text output ends with two buckets: Needs you (items waiting on a human decision — confirm a spec, approve a plan or --backlog, a BLOCKED testing result, a review FAIL/REQUIREMENT_BUG, a PR closed without merging, review PASS but not closed: <refusal heading> <failed check codes> when the automatic close was refused), Waiting to merge (a review PASS with an unmerged PR: its URL and gh pr merge <n>; and a canonical-docs branch: <f> — <branch> awaiting push: git push -u origin <branch> when it was never pushed, <f> — <branch> not proven merged (<reason>): git push -u origin <branch> when it was pushed but its merge cannot be proven; when the local branch is gone and there is no remote-tracking ref for it, a lookup that could not run shows as <f> — <branch> not proven merged (<reason>): re-run kf status --all once the merged-PR lookup can run, or re-land them on a fresh docs branch with kf archive <f> — the same next step kf doctor gives — and its JSON entry has command kf status --all, kind lookup-rerun and that whole next step as next, which the text line prints. A docs branch proven landed is recorded in the item’s metadata as docsLanded (by: ancestor, merged-pr or content) in place of docsBranch, so later runs do not ask GitHub about it again; the item block of the same run no longer lists the docs branch. When another kf command holds the item’s lock, that record waits for a later run and the run prints one ⚠ <f>: docs branch <branch> landed; … line instead of failing. After the first gh timeout in a run, the remaining merged-PR lookups are skipped: a branch gone locally then shows as not proven merged with the skip as its reason, while a local branch with no trace of a push still shows as awaiting push. A hung gh therefore costs one 15 s timeout per kf status --all run, every run until GitHub answers again). A docs branch gone locally with no remote-tracking ref is landed when its canonical docs are byte-identical on origin/<default>; otherwise, when the merged-PR lookup ran and found no PR carrying the docs commit recorded at close, it is a Needs you decision instead: canonical docs for <f> never landed (<reason>): re-land them on a fresh docs branch for a PR with kf archive <f>, or leave them; when origin/HEAD is not set, nothing was looked up, so the decision does not say never landed: it is run git remote set-head origin -a first, then re-run kf status --all: canonical docs for <f> are not proven landed (<reason>); if they still are not, re-land them on a fresh docs branch for a PR with kf archive <f>, or leave them whether or not a docs commit was recorded, and GitHub is not asked; with no origin remote it names git remote add origin <url> instead. Items closed before docsCommit was recorded have nothing to tie a merged PR to, so GitHub is not asked about them: the content comparison alone decides, and without it the reason is … no docs commit was recorded and origin/<default> does not hold these docs. With --json the buckets are needsYou[] (feature, decision) and waitingToMerge[] (feature, command, and pr for a PR or docsBranch plus an optional reason for a docs branch, and kind + next for one whose lookup must be re-run); a docs branch that never landed is a needsYou[] entry with no command. |
kf instruct <artifact\|use-case\|living-doc\|living-doc-index> [--change <feature>] [--id UC-###] [--json] |
Renders an artifact template; use-case produces the instruction for exactly one file under use-cases/, named after the id — --id UC-001-create-task writes use-cases/UC-001-create-task.md. living-doc and living-doc-index print the living-doc templates with their path, docs/specs/<context>/<capability>.md or docs/specs/<context>/README.md; they need no --change, which only fills the context. |
kf templates [--json] |
Lists the templates currently resolved and where each came from. |
kf validate --change <feature> [--strict] [--json] |
Checks artifacts, placeholders, secret-like content, traceability, the plan’s task graph (task_duplicate, task_dep_missing, task_dep_cycle) and the gates. From planning it also warns (affected_surface_uncovered) when a surface declared in the spec’s ## Affected surfaces never appears in the implementation plan — informative, not blocking. For an item in planning it warns estimate_missing when the contract has no usable ## Estimate (- Agent minutes: <n>) and estimate_should_split when the estimate sizes XL; both are WARNINGs, raised in planning only. It also checks the plan’s ## Documentation Impact for every feature in planning, and in later stages only for an item approved with living docs, which also has the review report’s docs: / docs_reason checked (the codes are listed under gates). When worktree.enabled, worktree.domains is not false and the domain infra is not set up it appends a worktree_infra_missing WARNING (only a blocker under --strict). Exits 1 on failure. |
kf validate --all [--base <ref>] [--strict] [--json] |
Validates every work item, then checks the living docs project-wide at the git top-level: every capability under docs/specs/<context>/ carries the five living-doc headings and is linked from the context’s README.md, which links no missing file; and every legacy feature removed under docs/requirement, docs/use-cases or docs/testplan since the merge-base with --base (default origin/HEAD) is removed whole and named in a living doc’s ## History or an index’s ## Retired. With no .works/ (a CI checkout) only this docs check runs. A merge-base that cannot be resolved skips the legacy-removal check with a docs_base_unresolved WARNING, which --strict turns into a failure; a failed git lookup is a docs_git_failed ERROR. The text output prints this check as one (project) block (✓ (project) — valid, or ✗ (project) — invalid|warnings with one [SEVERITY] <file>: <message> (<code>) line per finding). With --json the array gains a last entry {"feature": "(project)", "stage": "-", "valid": <bool>, "issues": [...]} after the work items (the only entry when there is no .works/); its issues carry severity, code, file and message only — no feature or stage field, unlike the work items’ issues. CI runs kf validate --all --strict --base origin/<base_ref> on pull requests, on a full-history checkout. |
kf approve <feature> [--backlog] [--by <name>] |
The human gate on the execution contract in planning; records the approver, the moment and the contract hash — and starts the item: it moves to implementation (provisioning the worktree when worktree.enabled) and prints the /kf:implement skill as the next step. With --backlog it records the same approval and moves the item to backlog instead (Next: kf stage <feature> implementation). If the move is refused or its hook fails, the approval is rolled back, the item stays in planning and the command exits 1 with the reason. The approval also records the item’s estimate (meta.estimate) from the contract’s ## Estimate section — - Agent minutes: <n>, the distinct Task Breakdown Files entries and the size they give with the learned factor — or none and why; kf estimate show <feature> prints it. |
kf stage <feature> <next-stage> [--force] [--skip-hooks] |
Performs a legal transition and runs the destination state’s hook. Before a move into planning or implementation it also syncs the base checkout — fetch + fast-forward to upstream when clean, a ⚠ warning when the tree is dirty or diverged — so the contract survey and the <prefix>/<feature> branch never start from stale code. Planning may go to backlog or to implementation (normally via kf approve); done is reached through the close (kf status, kf sync or kf archive). There is no shipping stage: kf stage <feature> shipping exits 1 with shipping is gone: open the PR at review, merge on GitHub, kf status --change <feature> closes the item, and kf stage <feature> dones exits 1 with the stage is now done: a review PASS item closes on merge via kf status --change <feature>, kf sync or kf archive. A missing destination stage directory is created before the move. A path that exists and is not a directory still fails, and the item stays where it is. With no arguments the usage chain is brainstorm → planning → backlog → implementation → testing → review → done; cancelled stays off that arrow because kf cancel is the way into it. Entering implementation with worktree.enabled (opt-in, off by default) provisions the item’s git worktree and route first — the transition fails closed when that cannot happen, so an opted-in item never implements on the main checkout by accident. Entering review records the number of files the item branch changed against the origin default branch (meta.filesChanged), or null with the reason; the count never refuses the move. |
kf cancel <feature> --reason "<why>" [--by <name>] [--force] [--skip-hooks] |
Stops a work item for good and moves it to .works/cancelled/, recording cancellation { at, by, reason, fromStage }. It never touches docs/; --purge-docs was removed and is refused with exit 1 (--purge-docs was removed: living docs are edited at review (docs/specs/); see kf instruct living-doc). When the item has a worktree it is torn down first — the <prefix>/<feature> branch is kept, and a dirty worktree refuses the cancel. Reopen with kf stage <feature> <fromStage>. |
kf ship <feature> |
Deprecated; changes nothing and exits 1 with kf ship is deprecated: review opens the PR; merge on GitHub; kf status --change <feature> closes the item. The review stage opens the PR once the reviewer and CI pass, you merge it on GitHub, and the next kf status --change <feature> closes the item. A leftover ship block in .kf/config.json is ignored (kf doctor warns). Releases are manual: gh release create v<version> triggers release.yml. |
kf archive <feature> [--force] [--skip-hooks] |
The close. Moves a review item into done once the change has landed: a recorded pr must be verifiably MERGED (OPEN, CLOSED or unverifiable refuses with zero side effects and no --force escape); with no pr, landing: none and no commits ahead of origin/<default> is the proof, and an item whose branch still has commits and no merged PR is refused. kf status and kf sync run the same transaction on their own, so kf archive is the manual trigger. In order: close-in-progress refusal → cancelled refusal → merge gate (and, from review, the merged PR’s head must be the item branch, the local item branch tip must not be past the merged head commit — repair commits pushed after the merge were not delivered, so the close refuses naming both shas — and landing: none must have no commits) → AC gate → review-PASS direction gate → dirty-worktree refusal → done hook (board move to statusMap.done, issue close) → worktree teardown → branch delete (local only when its tip equals the merged PR head, then origin with a lease; a branch that moved on origin since is kept and reported) → move → delivered/deliveredAt stamped last. The close writes nothing under docs/: the living docs under docs/specs/ were edited at review. An item approved before living docs whose PASS report records no docs: prints ⚠ <feature>: no living doc recorded (approved before living docs). --skip-specs was removed and is refused with exit 1 (--skip-specs was removed: living docs are edited at review (docs/specs/); see kf instruct living-doc). Re-run on a done item it reconciles idempotently: hook re-run, flag written, no docs rewritten. Only an item an earlier release closed with a docsBranch (canonical docs committed to an unpushed docs/<context>-<feature> branch, its commit in meta.docsCommit, which kf status --all lists under Waiting to merge and kf doctor flags as docs_branch_unpushed until pushed) still has its docs handled: a branch still local or on origin, or one whose landing cannot be proven yet, keeps its record and nothing is written (one line says the docs wait on it, with git push -u origin <branch> when it was never pushed); one proven landed is recorded as docsLanded; one that is gone here (no local branch, no remote-tracking ref) and not proven landed — a merged-PR lookup that could not run included — is always re-landed on a fresh docs/<context>-<feature> branch cut from origin/<default>, committed, not pushed, and recorded again as docsBranch + docsCommit (awaiting push); it is never written into the working tree. With no origin (the refusal, kf status --all and kf doctor then name git remote add origin <url>), or origin/HEAD not set (the refusal then names git remote set-head origin -a), the re-land refuses: there is no default branch to cut it from. Any overwrite of docs edited on origin since shows in the new branch’s PR diff. A re-land after a merged-PR lookup that could not run prints Canonical docs not proven landed (<reason>) in place of never landed, plus a ⚠ If the earlier PR did merge, this PR reverts later edits to these docs: do not merge it. warning; this path never refuses with changed docs and never needs --force. Once the item is in done, the close measures it into <item>/metrics.json and writes its .works/loop-metrics.md row when the ledger allows; a measurement that cannot be written only warns and never undoes the close (kf metrics <feature>). |
kf sync |
Close sweep. Needs repository (“owner/name”) in .kf/config.json for PR-linked items: without it, items with no recorded pr (the landing: none closes, which never ask GitHub) are still swept, then it exits 1 when any PR-linked item remains (or nothing was swept). With it, it checks merge state (gh pr view) for every review item that has a recorded pr or a PASS report, and every done item not yet delivered, and runs the close for each merged one (the same transaction as kf archive). An unmerged PR is skipped (PR open), a PR closed without merging is reported with a warning. A delivered done item is left alone. Runnable by hand or from cron/launchd; first migrates any legacy .works/dones/ and .works/shipping/ items. |
kf rules [--stack <id> ...] [--list] [--force] |
Copies the stack best-practice review rules into .kf/review/rules/. Detects the stacks automatically, installing several packs in a monorepo; --list shows the packs and --force overwrites files you have edited. |
kf autoconfig |
Prints a briefing for an agent on stdout: the project context, a config checklist of what is done and what is missing with the command to fix it, the effective review rules and the workflow guide, so the agent can configure the project itself. See agent onboarding. |
kf tasks <feature> [--json] [--graph] |
The plan’s task graph: wave, state, dependencies and files of every task (read-only). A task is done when its tasks.md checkbox is ticked, ready when every dependency is done, and pending otherwise. --json adds developers: {max, source} (an invalid harness.developers exits 1; any other invalid config key keeps the task list and reports {max: 1, source: "invalid-config", error}) (the live developer cap’s ceiling harness.developers.max, integer 1..16, default 6; source is config, default or invalid-config (with an error field)), per task refs (the plan row’s “FR / UC references” cell) and expectedOutput (its “Expected output” cell) when the row fills them, and overlaps: per task, one { task, entries } element per other task whose Files overlap; entries is the first overlapping [mine, theirs] pair, or null when either side has no Files (treated as an overlap); a task that overlaps nothing has []. The pipelined loop uses it to decide which ready tasks may run beside each other: a task waits while its Files overlap an open task (a test-writer dispatched or developer dispatched line and no complete line); overlapping tasks open in wave order (a lower wave, then plan order). --graph prints only the graph, instead of the table: critical path: … (length N) (the longest dependency chain), max width: N (…) (the largest set of tasks with no dependency path between any two and pairwise disjoint Files, over all tasks whatever their state; a task without Files overlaps everything; exact up to 30 tasks, above that a greedy lower bound printed max width: ≥N (…)) and earliest start: …; with --json it prints {feature, criticalPath, length, maxWidth, maxWidthExact, widthSet, earliestStart}, maxWidthExact being false for a lower bound. --json without --graph is unchanged. |
kf verify <feature> --task TASK-### --base <sha> |
Checks the commits <sha>..HEAD of one planned task against its plan Files allowlist and runs harness.verify.task, then harness.verify.related with the task’s changed files that still exist appended (each command printed as Ran: <argv>), in the item worktree (else the project root). Each run appends its output and exit code to <item>/loop/task-NNN-verify.log. Exit 0 pass, 1 a verify command failed, 2 a file outside the task’s Files, 3 bad input or kf verify could not run: … (an unexpected error while preparing, stack on stderr; runs nothing). For a bug, TASK-### is a checkbox line of its tasks.md; a bug has no Files, so no allowlist check runs. See harness. |
kf verify <feature> --task TASK-### --commits <sha>[,<sha>…] [--at-head] |
Like --task … --base, for exactly the listed commits: they are checked against the Files allowlist and harness.verify.task runs in a temporary detached worktree at the newest of them, so a later task’s uncommitted work cannot affect the result. --at-head keeps that allowlist check and runs the commands at HEAD instead, for a commit that cannot build without a parallel task’s commit. --base stays the single-task form. Same exit codes. |
kf next <feature> [--resume] [--json] |
Prints the task loop’s actions due now, read from the ledger <item>/loop/progress.md, the plan (a bug’s tasks.md), the task reports, the verify logs and the item branch’s git log: dispatch <seat> TASK-N[ round=R][ cascade=sha] <new|resume|fresh>[ inline], verify TASK-N: <command>, write: <line> with the kf record call that writes it, complete TASK-N, wait TASK-N: <reason>, decide[ TASK-N] (<rule>): <reason> — closes with <line>, stop[ TASK-N]: <reason>, verify wave <w>: kf verify <f> --wave <w> (with harness.verify.stage pinned: the lowest complete wave not yet verified, none while a wave verify runs, and under --resume the running one again; later-wave developers wait wave <w> verify, a failed run is a Wave verify decide, an exit 3 a Wave verify exit 3 decide closed by a re-run or Note wave <w>: decided — <what>) or finished, then the seats, verifies and running wave verifies (wave <w> verify) In flight:. The actions come from one state machine per task folded from the ledger; its liveness guard prints stop TASK-N: kf next has no rule for TASK-N in <phase> / … — report this with the ledger for a task left with no action, In-flight entry or wait, never silence. --resume lists those open seats and verifies for a re-run instead, and gives a Drift decide when a report already names a commit dated after an open developer dispatch. --json prints { feature, n, inFlight, actions } plus waveRuns (the waves whose verify is running) when there are any; a wave action is { kind: "verify-wave", wave, command }. Read-only: changes no file and leaves no process running. Exit 0 actions printed, 1 refused (a ledger line to repair, listed as line <n>: <text> — <reason>, or a plan kf tasks refuses), 3 bad input. |
kf record <feature> <event> [args…] [--model <m>] [--round <R>] [--cascade <sha>] [--status <S>] [--exit <n>] [-- <text>] |
Appends one stamped line ` @ |
kf loop-stats <feature> [--json] [--record] |
Measures a task loop from its ledger <item>/loop/progress.md: waits, rounds and where the time went, plus developers peak (most developer intervals open at once), and developer overlap (minutes with two or more open), both counting finished developer seats (dispatched…done intervals) and never one still in flight, plus dispatch lag (minutes from the gate line that made a task’s first plain dispatch possible to that dispatch: for the developer the latest of the task’s test-writer done and each dependency’s developer done, and after a developer waits line also the overlapping task’s complete (a Files-overlap wait), that seat’s next done of the named task (a (TASK-MMM <seat> in flight) wait) or the first developer done after the wait (a <N> developers in flight wait); for the code-reviewer the task’s developer done or automation-writer done). Lines that do not parse or carry no ` @ |
kf loop-stats --all [--json] |
Summarises the recorded history in .works/loop-metrics.md, ending with a largest: line. Same exit codes. |
kf metrics <feature> [--json] |
Prints the measurement the close wrote to the item’s <item>/metrics.json: the estimate recorded at approval, the actual size and the gap, the time in each stage, the path, the requirement, plan and merge gate spans, fix rounds, seat time and models from the ledger, the files changed (counted on entering review), the review tier and whether the .works/loop-metrics.md row was written. A figure that could not be taken shows missing: <reason>, never 0. A measurement that cannot be written never refuses or delays the close: the close warns. Exit 0 printed, 1 no measurement yet, no .works found, or a metrics.json that is not a valid JSON object, 3 bad input. |
kf ledger status [<feature>] [--json] |
Compares each work item’s append-only streams (lifecycle.jsonl, loop/progress.md from its second line, loop/stage-verify.jsonl, loop/task-*-verify.log, loop/wave-*-verify.log) and its artifacts (phase-*.md, tasks.md, metrics.json, use-cases/*.md, loop/*.md except progress.md, reviews/) with the ledger remote set by ledger.remote: "dir" and ledger.path. One row per stream: in sync, ahead N, behind N, diverged at line L, edited at line L (a line already synced was changed or removed) or off (a verify log with ledger.verifyLogs: false), plus a blobs row for the artifacts (diverged only when the same artifact changed on both sides, behind N when the copies changed different ones); refusals (a stream that is not valid UTF-8, or a stream or artifact this copy cannot read, <path> cannot be read here (<code>), is refused by name and the other items still run), then notices (an item with no id, which kf doctor --fix assigns; a remote item with no folder here, not here (#186)). Writes no local file and no cursor; reading a directory remote heals an interrupted append there (it cuts a partial last line and gives unreceipted lines a fresh receipt), so the remote’s streams/ and locks/ must be writable. --json prints {rows, refusals, notices, secrets?, disabled?, exit}. Exit 0 when nothing is diverged, edited or refused, 1 otherwise, 3 bad input (an unknown subcommand, flag or extra argument, with the usage). |
kf ledger push [<feature>] [--json] |
Sends the new lines of each stream and the changed artifacts to the remote. Before any remote call it scans every line it would send for secret-like content: stream lines, artifact bodies, and the index lines that carry item folders, artifact paths and stream names; a hit sends nothing and prints <file>:<line> <masked hit> (exit 1), where <file> is <id>/blobs or <id>/streams for a hit in an artifact path or a stream name. Every secret-like value in a hit’s file and text, and in the refusals and notices push prints, is masked. There is no flag or config key that skips the scan: --no-scan, --force and any other flag exit 3. A stream whose synced lines were edited or whose file was deleted (edited at line L, its first stream line), a stream that is not valid UTF-8, and a local stream name outside the shippable set (a control character in it) are refused naming the item and the stream, and nothing of them is sent; the other streams still push. When another copy appended first, local lines that are a prefix of the remote’s are adopted and the row reads behind N (pull first); otherwise the stream is refused remote holds N lines, M synced here: line L not sent, pull first. A first push from a copy with no cursor adopts what the remote already holds when the local lines are a prefix of it, and is refused diverged at line L otherwise. For artifacts, a copy with no cursor adopts the remote manifest only when it holds every artifact the manifest names; otherwise it saves no artifact cursor, still sends the artifacts the remote lacks (blobs pushed N), and reads blobs behind N with exit 0 when nothing is left to send, again on every push until a pull, which reads the whole manifest. Run pull before editing artifacts after such a push: until that pull an artifact edited here to content the remote never named is refused on push and on pull; an unedited artifact holding an earlier version the manifest named is its base (pull fast-forwards it, push reads behind). A stream or artifact this copy cannot read (<path> cannot be read here (<code>)) is refused by name. Cursors live in .works/.ledger/, written only after the remote’s receipt. Rows pushed N, nothing to send or behind N. Same exit codes. |
kf ledger pull [<feature>] [--json] |
Reads the remote first, checks every line against its receipt chain, then appends the new lines and writes the changed artifacts under the item lock. Local lines past the last sync that are a prefix of the remote’s are adopted and the rest appended. It refuses, naming the stream and line or the path, a stream whose local lines past the last sync differ from the remote’s or that has an edited line, a local stream that is not valid UTF-8, a local stream that cannot be read or appended to (could not be appended to here (<code>); not pulled), remote lines that do not match their receipts, an artifact changed here since the last sync (on a copy with no artifact cursor, one holding a version the manifest never named; an earlier named version is fast-forwarded), an artifact path that is not a regular file here (a folder, a symlink) or that cannot be read or written, and any path the remote names outside the shippable set (../, an absolute path, a control character, .kfw.json); the refused file is left byte for byte as it was, and the cursor still records every stream that was appended. A missing loop/progress.md is written with this copy’s record header first. Rows pulled N, in sync or ahead N. Same exit codes. Switches, each true when unset: ledger.enabled (false: every subcommand exits 1 with ledger disabled (ledger.enabled: false)), ledger.push, ledger.pull (false refuses that subcommand), ledger.blobs (false: no artifact is sent or written), ledger.verifyLogs (false: verify logs are neither sent nor pulled; a pulled verify done line then has no local log, so kf loop-stats reports it unlogged). No ledger block, a missing ledger.remote or ledger.path, or an invalid value exits 1 naming the key; a remote directory that is missing, not a directory, not accessible or not writable exits 1 naming the path, as does a <feature> that matches no item. With --json each of these refusals, raised before any remote call, prints one object on stdout, {rows: [], refusals: [], notices: [], error, exit: 1} (error naming the key, the feature or the path), or {rows: [], refusals: [], notices: [], disabled: "ledger disabled (ledger.enabled: false)", exit: 1} for ledger.enabled: false; bad input (exit 3) prints the reason and the usage as plain text with or without --json. No other command calls the remote or writes .works/.ledger/. Run the same kf version on every machine that shares a remote. |
kf estimate size <minutes> <files\|unknown> [--kind feature\|bug\|pr] |
The harness estimation model. Prints the T-shirt size (S under 5 minutes and up to 3 files, M up to 20 minutes and 10 files, L up to 60 minutes and 30 files, else XL; the higher of the minutes size and the files size wins), its points (S 1, M 2, L 4, XL 8) and the defaults line; with --kind, the minutes are first multiplied by the factor learned for that kind: the median of work ÷ estmin over its last 30 measured rows, used from 20 rows on and clamped to 0.5–3. The history is every done item’s metrics.json plus the legacy .kf/travel/estimates.md, which kf reads and never writes. Exit 0 printed, 1 any error. |
kf estimate stats |
Prints the budget (the median of daily actual points once 3 days are measured, else 10 tasks), hit rate, bias, throughput, calibration, the per-kind lines with their factor, and a warning when this week’s hit rate fell below last week’s. Same exit codes. |
kf estimate overlap <filesA> <filesB> |
Prints sequential when two plan Files cells can touch the same path, else parallel. Same exit codes. |
kf estimate show <feature> [--json] |
Prints the estimate kf approve recorded from the contract’s ## Estimate section (- Agent minutes: <n>): minutes, files, size and factor; while the item is in brainstorm or planning, the contract’s estimate marked not yet approved; otherwise no estimate: and why. --json prints the same figures. Same exit codes. |
kf verify <feature> --stage [--waive "<reason>"] |
Runs harness.verify.stage in the item worktree (else the project root) and appends the run to <item>/loop/stage-verify.jsonl (HEAD sha, clean tree, each command’s outcome, output sha256, exit); kf stage <feature> review reads that record. Same exit codes; nothing is recorded when nothing ran. --waive records the human’s reason to enter review in a project with no harness.verify.stage (exit 3 for a blank reason, without --stage, or while stage commands are pinned). kf drive, kf run, kf runs and kf harness were removed; the session runs the loop from kf next and calls kf verify. |
kf verify <feature> --wave <w> |
Runs harness.verify.stage once a wave’s tasks are all complete, in a temporary detached worktree at HEAD, and appends a stage record with wave: <w> to <item>/loop/stage-verify.jsonl; its log is <item>/loop/wave-<w>-verify.log. kf next prints it for the lowest complete wave not yet verified, none while a wave verify runs (--resume re-issues a running one), and the next wave’s developers wait until a passing record taken on a clean tree with the pinned stage commands contains every wave commit — a wave verify’s record or a plain --stage record alike, except that a wave with no matched commit is released only by its own wave run. The review gate reuses the last wave’s record when it passed at HEAD on a clean tree, so --stage runs only when kf status does not show PASS at HEAD. --wave stands alone: exit 3 with any of --task, --stage, --base, --commits, --at-head or --waive, for a value that is not a positive integer, or for a wave the plan does not have; exit 3 also when the run passed but its record could not be written, since kf next reads only the record (a failed run keeps exit 1 with the warning). With no harness.verify.stage it runs nothing and records nothing. Same exit codes. |
--force deliberately skips a gate. --skip-hooks skips the destination phase’s hook. --skip-specs (archive) and --purge-docs (cancel) were removed and are refused with exit 1. Whenever --force or --skip-hooks actually skips something, the CLI writes a record into .kfw.json under bypasses[] and says so in its output. See gates.
Worktrees and local domains
Each work item in implementation gets its own git worktree on branch <prefix>/<feature> and a domain <feature>.<baseDomain> (default <feature>.<repo-slug>.test) routed to a per-item port (the domain is printed and probed only when worktree.domains is on). The prefix resolves in order: worktree.branchPrefix in .kf/config.json → worktree.gitconfigName slug → git config user.name slug → kf/. kf works from inside a worktree — .works/ is gitignored, so the project root is resolved through git rev-parse --git-common-dir. Routing is machine-global: one kf proxy serve process reads a shared routes file and forwards Host to 127.0.0.1:<port>, tunnelling websocket upgrades for dev-server HMR. Hosts with no route go to fallbackUpstream when configured, else a 502 listing known routes. Setup is a one-time, sudo-required onboarding.
| Command | What it does |
|---|---|
kf worktree create <feature> |
Creates <baseDir>/<feature> (default <repo>/.worktrees/<feature>) on branch <prefix>/<feature> — after fast-forwarding the base checkout to upstream — allocates the first free port from portBase (registry ∪ routes file) and registers the route + .kfw.json entry. Idempotent — re-running returns the existing registration; a deleted worktree is rebuilt from its branch; a branch that exists but is not managed by the item refuses to be adopted silently. |
kf worktree remove <feature> [--force] |
Removes the worktree and its route; the branch is kept. Refuses while dirty — uncommitted or untracked content — unless --force deliberately discards it. Warns when the branch carries commits not merged into HEAD. |
kf worktree list [--json] |
Every registered worktree (item, stage, path, dirty state, domain — direct: in its place when worktree.domains is false — and port) plus orphans: registry entries whose worktree is missing, git worktrees on a managed prefix (<prefix>/* or legacy kf/*) with no .works item, and stale routes. |
kf worktree setup [--print] |
One-time machine onboarding: a dnsmasq address=/.<zone>/<listen> rule, /etc/resolver/<zone> when absent, and a LaunchDaemon running kf proxy serve. --print shows every file and command without touching the machine; a foreign rule routing the same zone elsewhere (e.g. a leftover Valet one) is a hard conflict — it refuses rather than overwriting. |
kf proxy serve [--listen <h:p>] [--routes <file>] [--fallback <h:p>\|off] |
Runs the proxy in the foreground — launchd’s KeepAlive is the supervisor. Flags override ~/.config/kanban-flow/config.json (proxyListen, domainZone, fallbackUpstream, routesFile). |
Project-side defaults live in .kf/config.json under worktree — enabled (opt-in, default false; set true to get per-item worktrees and .test domains on stage → implementation), baseDir (sibling <repo>-worktrees/), branchPrefix, gitconfigName, baseDomain, portBase, routesFile, domains (boolean, default true; with false there are no .test DNS or proxy probes, kf status prints only http://127.0.0.1:<port>, hooks get no KFW_WORKTREE_DOMAIN, ports and routes are still allocated and JSON outputs still carry domain). Everything still works without the domain infra: the direct http://127.0.0.1:<port> URL is printed wherever the domain is, and validate/doctor point at kf worktree setup until it is done.
Two project hooks finish the job the domain starts: .kf/hooks/worktree-create.sh runs inside a freshly created worktree (install dependencies, cp .env.example .env, boot a dev server on $KFW_WORKTREE_PORT) and a failure rolls the worktree back and fails the transition; .kf/hooks/worktree-remove.sh runs inside the worktree right before teardown (stop dev servers, compose down) and only warns on failure. Both receive KFW_WORKTREE_{PATH,PORT,DOMAIN,BRANCH} on top of the standard hook environment (KFW_WORKTREE_DOMAIN is set only when worktree.domains is on) — see phase hooks.
Skill installation
The 11 skills and 7 agents install at global scope by default, or at project scope when skills.scope: "project" is declared or --project is passed. For Claude Code that means the kf plugin (claude plugin install kf@kanban-flow --scope user for global, --scope project for project; /kf:<name> commands and kf:<agent> agents). For Antigravity (--agent agy), kf installs an Antigravity plugin bundle (plugin.json, skills/*, AGY-formatted agents/*.md, shared/) at ~/.gemini/config/plugins/kf (global), {root}/.agents/plugins/kf (project), or <workspace>/.agents/plugins/kf (workspace). For every other runtime the skills are kf-<name> copies in its skills dir (~/.agents/skills or {root}/.agents/skills for codex, gemini, cursor, opencode and devin; .kiro/skills for kiro), with the agent definitions copied or linked into its agents dir when it has one. kf init installs them during setup. At project scope kf install and kf uninstall work on the project root resolved from the nearest .works/ above the working directory; global scope needs no project, and --scope workspace works on the workspace folder for both Claude Code (claude) and Antigravity (agy). A file of your own that sits at a managed agent name (code-reviewer.md, scout.md, spec-reviewer.md, developer.md, test-writer.md, automation-writer.md, researcher.md) is never replaced or removed; kf doctor points it out.
| Command | What it does |
|---|---|
kf install [--agent <id> ...] [--scope global\|project\|workspace] |
Installs the 11 skills and the 7 agents (code-reviewer, scout, spec-reviewer, developer, test-writer, automation-writer, researcher, whose bodies live in the package’s assets/agents/). For claude it adds the kf plugin through claude plugin marketplace add <package root> and claude plugin install kf@kanban-flow --scope user\|project (kf global is the plugin’s user scope), and the commands are /kf:<name>. A plugin whose version differs from this package is updated with claude plugin marketplace update and claude plugin update; if the claude CLI is missing or fails, the output prints the two /plugin marketplace add and /plugin install steps to run by hand. For agy (--agent agy, Antigravity), it writes the Antigravity plugin bundle (~/.gemini/config/plugins/kf at global scope, {root}/.agents/plugins/kf at project scope, <workspace>/.agents/plugins/kf at workspace scope). Every other runtime (--agent defaults to claude) gets kf-<name> skill copies in its own skills directory, and the agent definitions in its agents dir when it has one (linked at global scope, copied at project scope, or copied at both when the package runs from the npx cache or a symlink is denied). Earlier releases’ kanban-prefixed entries that kf owns are removed on install; a file of yours is left alone. Run from a linked git worktree of the package, a global-scope or workspace-scope install (also kf init with global skills scope and the skills/agents repair of kf doctor --fix) is refused: kf install and kf init exit 1 with the reason “run it from the primary checkout” on stderr and change nothing, and kf doctor --fix reports the refused skills/agents repair as an ERROR finding, exits 1 and still applies its other repairs; project-scope copies are unaffected. KF_ALLOW_WORKTREE_LINK=1 overrides the refusal for development only: the global or workspace links/bundle then point into the worktree and dangle once it is removed. A file of yours under one of those names is left untouched and reported. --scope project must run inside a kanban project (a .works/ exists); without one it exits 1 with No kanban project found at <dir> (missing .works/) — run: kf init. The copies go to one shared .agents/skills dir for codex, gemini, cursor, opencode and devin (the open standard), and to .kiro/skills for kiro. An unknown agent also falls back to .agents/skills. --scope workspace (Claude Code and Antigravity: claude and agy, alone or together; any other agent is refused with exit 1 before any change) installs in the workspace folder: the nearest folder holding .kf/workspace.json, searching the working directory and then its parents (the home directory is skipped); outside a project the working directory itself, where .kf/workspace.json ({}) is written only after all workspace installs succeed; inside a project with no .kf/workspace.json above it the install is refused (exit 1, “no .kf/workspace.json above this project — run kf install from the workspace folder”). In that workspace folder, claude runs claude plugin install kf@kanban-flow --scope local and agy installs the Antigravity bundle to <workspace>/.agents/plugins/kf without invoking claude. The linked-worktree refusal applies as for global. When kf is also installed at user scope (claude user scope or ~/.gemini/config/plugins/kf for agy) and the onboarding did not ask about it (no terminal, or every choice given), the output names kf uninstall --global (for claude) and/or kf uninstall --agent agy --global (for agy). When the onboarding asked, Yes removes the user-scope install(s) only after the workspace install succeeded (checking agy directly without calling claude), and No keeps them without the note. When claude cannot be asked during the onboarding, the removal question for claude is skipped, and the install output reports the failed check (⚠ Could not check for a user-scope kf install — …) or names kf uninstall --global. On a terminal, the choices not given are asked: with no option a Setup mode step (Quick — exactly today’s no-option install; Customize — agents, scope, confirm), otherwise only the missing steps and a confirm, each labelled Step n/N; Cancel prints Nothing changed. (exit 0), Ctrl+C or end of input Aborted — nothing changed. (exit 1). Without a terminal, or with every choice given, nothing is asked and the command runs as before. |
kf uninstall [--agent <id> ...] [--scope global\|project\|workspace] [--purge] [--force] |
Removes exactly the 11 skills and the 7 agents that kanban-flow manages (the kf plugin for Claude Code, the Antigravity bundle entries for agy, the kf-<name> copies elsewhere), leaving every other skill and agent alone (e.g. kf uninstall --agent agy --global removes ~/.gemini/config/plugins/kf). --purge also deletes .works/ and .kf/ (never anything under docs/), asking first on a TTY and requiring --force without one. --scope workspace (Claude Code and Antigravity: claude and agy, alone or together; any other agent is refused with exit 1 before any change) runs claude plugin uninstall kf --scope local for claude and removes managed agy entries from <workspace>/.agents/plugins/kf (keeping custom user files) in the workspace folder, never creates .kf/workspace.json, and keeps existing .kf/workspace.json even under --purge. Removing the Claude plugin at one scope keeps the kanban-flow marketplace while a kf install remains at any scope, a workspace local install included; the marketplace goes only with the last install. On a terminal, the agents, scope and purge choices not given are asked (the purge step only when a .works/ is found, so a workspace folder’s .kf/ is never offered for deletion), then a confirm step that lists the directories a purge deletes and replaces the separate purge prompt. Cancel and Ctrl+C change nothing. |
Uninstall removes the managed skills and agents kanban-flow put there, nothing else. To take the CLI off PATH: npm rm -g @phuthuycoding/kanban-flow.
Error conventions
- Success exits
0. Bad input, a failed gate, a failed hook, a missing work item or an exception exits1. The readable report goes to stdout; stderr carries only a short reason line. A CI step that keeps stderr and discards stdout captures the reason but none of the detail. - The CLI never runs a migration, updates a database, deploys or publishes on its own.
- Hooks resolve in order: the project
.kf/hooks, then the user’s~/.kf/hooks, then the package hooks. The environment handed to a hook carriesKFW_FEATURE,KFW_CONTEXT,KFW_FROM_STAGE,KFW_TO_STAGE,KFW_FEATURE_DIR,KFW_WORK_ROOT,KFW_APPROVALandKFW_REPOSITORY. See phase hooks.
Config layers
kf reads up to three config files and merges them, nearest first:
| Layer | File | Committed |
|---|---|---|
| project | <project>/.kf/config.json |
yes — the team’s config |
| workspace | the nearest .kf/workspace.json in a parent directory of the project (the walk starts at the project’s parent and skips your home directory) |
no — personal |
| global | ~/.kf/config.json |
no — personal |
- Precedence: project > workspace > global. Objects merge key by key; an array or a scalar comes whole from the nearest layer that sets it.
- Keys allowed in the global and workspace layers: only
reviewer,agents,skillsandledger. Aledgerblock merges field by field across the layers; each layer file is checked for field types and unknown fields only, andledger.remoteandledger.pathare required on the merged block when akf ledgercommand runs. Every other key is project-only:worktree,repository,project,contexts,defaultContext,stacks,harness,schemaandcreated. A project-only key found in a global or workspace file is not applied, and kf prints one warning naming the file and the keys.kf doctorreports it as a WARNING;--fixnever edits the global or workspace file. - Worktrees:
worktreefollows the repository. The machine’s proxy and DNS settings stay in~/.config/kanban-flow/config.json. - Writes:
kf init,kf init --minimal, onboarding andkf doctor --fixdecide what to write from the project file alone. Areviewer,agentsorskillsvalue a global or workspace layer already supplies is left out of.kf/config.jsonunless someone answers a different value, so a personal value never lands in the committed file. The onboarding prompts suggest those inherited values, so pressing Enter keeps the project inheriting them.kf doctorlists the layer files it read and reports an invalid one as an ERROR naming it; an invalid layer file fails any other command with an error naming it.
kf config set --global reviewer Alice # every project on this machine
kf config set --workspace agents '["claude"]' # every repository under the parent folder
kf config show # effective config, each value with its layer
kf config get skills.scope # one value, or "not set" with exit 1
Contexts
A context groups work items and their living docs by business domain: docs/specs/{context}/. The legacy snapshots of earlier releases sit under docs/requirement/{context}/, and the same under use-cases and testplan.
A project may declare which contexts exist, in .kf/config.json:
"contexts": ["auth", "billing", "catalog"]
Two things follow from that list, and nothing follows without it:
- The first entry is the default context for
kf newwithout--context. There is no separate default field to keep in step with the list.defaultContextis still read for projects created beforecontextsexisted, and is ignored once a list is declared. kf newrefuses a context that is not on the list, naming the nearest declared spelling. A name differing only in case is refused too, because acceptingAuthbesideauthis how a second docs tree appears on a case-sensitive filesystem.
A project with no contexts key is unrestricted. Re-running kf init there does not add the key, and neither does a project that has work items but no config file yet: a project counts as existing if it has either. kf init writes the list only for a genuinely new project, or when you answer its question on a terminal. The one behaviour that did change for an unrestricted project: kf new now reads the config on every path, so a malformed config fails loudly instead of only when --context was omitted.
kf contexts reports each in-use context with the spelling found on disk, not a lowercased one. Where an undeclared spelling differs from a declared one only by case, it says so instead of telling you to add it, because the config reader refuses that repeat: rename the work items, or change the declared entry. It never writes anything. When no list is declared it prints a brief for an agent to survey the repo and propose one, which a human then confirms and writes.
GitHub issues
A project may link itself to a GitHub repository in .kf/config.json:
"repository": "owner/name"
Any repo reference normalizes to owner/name — https://github.com/owner/repo, git@github.com:owner/repo.git and the short form all write the same value. kf init detects it from the origin remote and asks once on a TTY. The value is handed to every stage hook as KFW_REPOSITORY, so a hook that syncs work items to GitHub never hardcodes the repo.
| Command | What it does |
|---|---|
kf issues [--state open\|closed\|all] [--limit <n>] [--json] |
Lists issues on the configured repository, via gh. |
kf issues view <n> (or kf issues <n>) |
Shows one issue. |
kf issues create <feature> [--title <t>] [--label <l> ...] |
Creates an issue titled by --title, else the work item’s --goal, else the slug — the filled requirement file becomes the body, a stub otherwise; bug items get the bug label, everything else enhancement. The issue URL is recorded as issue in the item’s .kfw.json, so kf status shows it and a hook can read it. Refuses when the item already links to an issue. |
kf issues link <feature> <n\|url> [--pr [--replace]] |
Records an existing issue on the work item — or the pull request delivering it: a …/pull/<n> URL or --pr writes pr in .kfw.json. Refuses to overwrite either link. --replace (PR links only) swaps the recorded pr for a new PR that is open on this repository and, when known, on the item’s branch, and only when gh pr view reports the recorded PR CLOSED without merging — the state in which every close refuses with pr closed; a merged, open or unverifiable recorded PR refuses with the reason and keeps its link. |
kf issues sync <feature> |
Replaces the linked issue’s body with the item’s filled requirement (frontmatter stripped, kf footer appended). Refuses when there is no link or no filled spec — this is the primitive the planning sync hook calls. |
kf issues done <feature> |
Deprecated alias — forwards to kf archive with a warning. Since 0.9.0 archive itself is the delivery gate: merge evidence when pr is recorded, else the human’s run. Tip: Closes #<issue> in the PR body lets GitHub close the issue natively on merge — kf archive still records delivery and syncs the board. |
gh must be installed and authenticated for all of these. Nothing talks to GitHub without the field: a project with no repository keeps its issues local, and hooks read an empty KFW_REPOSITORY.
The hook pack and the project board
kf init offers to install a generic sync pack into .kf/hooks/ when a repository is linked — the same flow monitoring hand-built: an issue on kf new, the requirement synced at planning, assignment and reopen at implementation, close-not-planned at cancel, and at the close the AC gate, the board move to statusMap.done and the issue close. The close is the delivery step: the done hook’s close and board move are correct because the close only runs after the change landed — a recorded pr must be merged, or there is nothing to land. The pack no-ops without KFW_REPOSITORY, fails open on GitHub outages, and fails closed on unreadable .kfw.json.
A project block in .kf/config.json additionally mirrors stage moves to a GitHub Projects board:
"project": {
"owner": "phuthuycoding",
"number": 3,
"statusMap": { "brainstorm": "Plan/Brainstorming", "done": "Done" },
"acGate": true
}
statusMap names the board’s Status options per kf stage — names, never IDs; the pack resolves them per run so a recreated board keeps working. Map done to your closing status (usually “Done”) — done means delivered: the close is the post-delivery step, so the board should read Done exactly when the issue closes. On boards with GitHub Projects’ Auto-close issue workflow enabled, a card moved to the closing status closes the issue natively and the pack’s own close becomes a harmless no-op. A legacy dones key is still read as done and shipping is dropped (kf doctor flags statusmap_legacy; --fix rewrites it). A leftover delivered key from ≤0.8.0 stays parseable but is inert — kf doctor flags it as dead config. acGate: false skips the archive AC gate. Hooks see these as KFW_PROJECT_OWNER, KFW_PROJECT_NUMBER, KFW_PROJECT_AC_GATE and KFW_PROJECT_STATUS_<STAGE>. Board sync needs gh with the project scope (gh auth refresh -s project); kf doctor warns when it is missing, and kf doctor --fix restores deleted pack files without ever overwriting a hook the project edited.