Files
improvise/context/code/kb-tool.md
T
Edward Langley 7fd57c06b3 docs: say what to do; attribute what agents post
Two user rulings.

Documentation should not spend itself on history -- that is bloat. Prose docs
now lead with the rule and keep only the line that stops it looking arbitrary;
the worked examples and the how-we-found-it move into doctrine/2 text, which is
what that predicate is for and where a reader goes deliberately rather than
while looking for a command. Trimmed: both skills, context/code/kb-tool.md, and
the kb paragraph in AGENTS.md.

Anything an agent posts to GitHub goes out under the owner's account, so every
comment, review and issue body now opens with a line naming the model and that
an agent wrote it. A PR body's generated-with footer already did this; comments
carried nothing, and an unattributed one reads as his own words to every reader
-- including a reviewer deciding how much weight to give it. The reply on PR #68
has been amended in place.

improvise-xfsb files the structural fix: a GitHub App or machine user so agents
post under their own identity. An attribution line is a convention an agent can
forget; a separate identity cannot be.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 16:04:33 -07:00

4.8 KiB

Maintaining the knowledge base

prolog-sessions/kb.pl is executable knowledge, checked by CI. This is the argument for how it is written, and why the Prolog MCP stopped being the way.

Why the MCP was retired

loadProgram replaces a whole predicate, so adding one clause means re-sending every existing clause of it through an agent's context window — and a short payload silently drops the rest. Three further blockers: saveSession re-adds the MCP's own host_rpc shims and reserializes the whole file, and assertz raises permission_error(modify, static_procedure) against a consulted predicate, so there is no incremental route through it at all.

What replaced it

scripts/kb, a shell entry point over scripts/kb-merge.pl.

Clauses are read as DATA, never consulted. read_term/3 in a loop yields a list of terms; merging is list manipulation; portray_clause/1 writes them back. Nothing is ever asserted, so the static-procedure wall is not reached and the cost of adding a clause is the clause.

Canonical form is read_term in, portray_clause out. This was not designed, it was measured: all 495 facts in the existing file — including every escaped quote — round-trip byte-identically, and the form is a fixed point in one pass. The only change on first use is that rules move from the MCP's two-line layout to one line, which is why the reformat lands with this tool rather than before it: the MCP would re-expand them on its next save and the two would thrash.

trealla comes from the dev shell. Reaching it through nix shell per invocation cost more than the work: the tool's first real use timed out past two minutes on a write that takes 0.42 seconds with tpl on PATH.

The guards, and why each exists

Every one of these was added because injecting its fault showed the tool accepting something it should refuse. None was found by reading.

The round-trip check. After writing, the file is read back and required to be the clauses intended. A serializer bug is the wipe-the-knowledge-base failure with a new owner, and this is the only check that sees it. Note it compares variant equality via numbervars over a copy — reading a term back yields fresh variables, so == compares variable identity and reports every rule as a difference.

kb_consistent runs on the CANDIDATE. The file is replaced only after the check passes, so a delta that breaks the knowledge base leaves it untouched rather than requiring a revert.

Parse errors are caught explicitly. An uncaught syntax error drops trealla into its REPL, which hangs with a terminal and exits 0 without one — a refusal that reads as acceptance. scripts/kb also closes stdin on every call as a backstop.

Every run reaches a halt/1. tpl exits 0 unless a halt is reached: measured on 3.10.3, -g "fail" and -g "throw(boom)" both exit 0, and so do fail,halt and throw(boom),halt, because a goal that fails or throws never reaches the halt beside it. halt(2) alone exits 2, so halt works — the default is the hazard. The goal used is therefore

(catch(main,E,(write(user_error,E),nl(user_error),halt(2))) -> halt(0) ; halt(1))

which gives 0, 2 and 1 for success, throw and failure. Without it, if ! merge ... reads a crashed merge as a clean one and replaces the knowledge base.

Terms must be callable. 42. parses, is not a clause, and reached the knowledge base as a fact whose head is a number. kb_consistent has no opinion about stray terms, so nothing objected.

A clause can be retired. kb rm takes keys and refuses if any of them is absent, because a typo'd key would otherwise report success and leave the clause in place. Without it no correction could ever drop a fact — the first thing it was used for was retiring kb_is_maintained_through_the_mcp, whose name was the claim that went false.

Duplicate keys are refused by default. A key is the functor plus every argument but the last, so doctrine(Name,_) keys on the name and decision(Id,Key,_) on the pair. replace is the door a correction goes through, and it overwrites in place — a count that grew would be two clauses under one name.

What no test here reaches

The key rule is looser than functional/1 for crate/3, which keys on the first argument alone; kb_consistent is the backstop for that case rather than the merge.

A clause name cited inside another clause's string is unchecked — dropping or renaming a clause silently dangles every clause referring to it (improvise-2f97).

A shebang would let kb-merge.pl run directly and collapse the two files into one. It does not work on trealla 3.10.3: #! is a syntax error that aborts the consult, so main/0 is never defined, and tpl wants -- args after the file, which a shebang cannot inject. Worth re-testing if trealla gains script support.