Files
improvise/.claude/skills/kb/SKILL.md
T
Edward Langley 4ca3a47b41 kb: refusal no longer offers committing the loss; skill and kb-tool.md describe the guard (improvise-za97)
The kb skill (both copies) now describes the HEAD comparison, the ledger,
exit code 3 and the recovery. kb-tool.md records the ih3k/yea8 arguments and
corrects the residue paragraph: .gitignore hides the leftovers, and there are
six, not one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:17:17 -07:00

5.5 KiB

name, description
name description
kb Read and write the Prolog knowledge base at prolog-sessions/kb.pl. Use when adding a doctrine, invariant, decision or work_done fact, correcting or retiring one, or querying the formal model. Never edit that file by hand and never write it through the Prolog MCP.

Writing the knowledge base

prolog-sessions/kb.pl is executable knowledge, gated by CI. scripts/kb is the only thing that writes it. tpl is assumed on PATH from the dev shell.

scripts/kb check              # kb_consistent, exactly what CI runs
scripts/kb check --with <d.pl># does the model accept a design you have not built?
scripts/kb count              # clauses per keyed predicate
scripts/kb query '<goal>'     # run a goal; exit 1 if it fails
scripts/kb add <delta.pl>     # add clauses; refuses if a key already exists
scripts/kb replace <delta.pl> # add, overwriting existing keys
scripts/kb rm <keys.pl>       # remove by key; refuses if a key is absent
scripts/kb fmt                # canonical form (idempotent)

A delta is an ordinary Prolog file of clauses. For rm, a key is a clause head with the last argument left as _:

doctrine(a_name_that_went_false,_).

Designing against the model

Before implementing a ticket, state the design as facts and ask whether the model accepts it:

crate(improvise_report,'crates/improvise-report','render a View as a report').
crate_depends_on(improvise_report,improvise_core).
layer(report,improvise_report).

scripts/kb check --with design.pl merges those into a throwaway candidate and runs kb_consistent over the union. Nothing is written either way.

It derives consequences you would not enumerate: one wrong dependency edge returns six layering_inverted violations through the transitive closure. It also catches schema mistakes — layer/2 is (layer, crate), and the wrong order comes back as layer_names_unknown_crate rather than being accepted.

What the model can refuse today: layering and dependency direction (illegal_dependency, layering_inverted, may_use), ViewState membership (portable, snapshot_safe, view_state_admits, field_misplaced_in_view_state), record obligations, and functional-key conflicts. A design touching those areas is worth stating; one that touches none of them will be accepted vacuously, so extend the model rather than trusting the green.

Rules

Write a fact when you learn it. There is no reason to batch.

Verify the claim against the tree before writing it. A clause stating the negation of what the code does is worse than no clause (doctrine(a_stale_gotcha_asserts_the_negation)).

If the NAME went false, retire it — rm the old key, add the new one, and say in the new clause that it was renamed. A reader who trusts a name never reads the body.

Put the argument in the clause. Doctrine and decision text is where worked examples and history belong; prose docs should stay operational.

Traps

  • assertz does not work here. A consulted predicate is static; assertz/retract raise permission_error(modify, static_procedure). This is why the tool reads clauses as data.
  • kb_consistent does not check everything. A clause name cited inside another clause's string is unchecked, so retiring a clause can dangle every clause referring to it (improvise-2f97).
  • Reading through the MCP is fine. loadSession at session start is still how a session gets the model in hand; only writing moved.

Every write checks three things before it replaces the file: the result reads back as intended, it still holds every clause HEAD has, and kb_consistent passes. So a bad delta leaves the file untouched. count before and after is still the cheapest evidence a write did what you meant.

When a write is refused

Exit codes: 0 done, 1 refused, 2 usage, 3 HEAD's kb.pl could not be read, so nothing was compared, 127 no tpl. Only 1 is a verdict.

"clauses HEAD has are gone, with no kb rm recorded" means the working-tree kb.pl is short of the committed one. Your delta did not cause it. The file was already damaged, by a killed write, a bad merge resolution or an edit. The refusal lists the clauses that are missing. The check counts clauses per key, so losing one of two clauses that share a key is caught too.

  • Recover: git show HEAD:prolog-sessions/kb.pl > prolog-sessions/kb.pl, then re-run your add/replace/rm deltas.
  • Do not commit the short file to get past the refusal. HEAD is what the check compares against, so a committed loss becomes the new floor and is never reported again.
  • To remove a clause on purpose, use kb rm. It records the removal in a per-worktree ledger (git rev-parse --git-path kb-removed.pl, never committed), and that record is what lets later writes accept the missing clause. A hand deletion has no record, so it is refused.

The ledger vouches only for the exact kb.pl file the tool last wrote. Any rewrite outside the tool voids it, including a restore, git checkout, cp or an editor. After an rm, a restore therefore also undoes the rm, and you must redo it. That is deliberate. Otherwise the abandoned rm would go on excusing a later, accidental loss of the same clause.

A killed write can leave kb.pl.cand.* files and a kb.pl.lock directory in prolog-sessions/. Git ignores them. The lock makes the next write wait 30 seconds and then fail, so delete it if nothing else is writing.

context/code/kb-tool.md has the argument. scripts/kb-test.sh proves each refusal by injecting the fault it names.