Phronesis — Changelog
Changelog
All notable changes to this project are documented here. The format
follows Keep a
Changelog. This project is pre-1.0: while 0.x, MINOR
versions may carry breaking changes.
[0.26.0] - 2026-08-09
Added
- Rule-to-code staleness bindings. Conservative,
unqualified call-shaped literals in rules bind to locally defined
functions in the structural graph. When every established referent
disappears, a formerly blocking rule warns instead of claiming authority
from stale evidence. Method calls, namespace calls, attributes, prose,
foreign symbols, and rules with
binds: falsedo not bind. - Code drift in the consolidated drift surface.
phr-mcp drift --source codeandget_drift(source="code")report stale rule bindings alongside the existing prose, memory, and decision corpora. - MCP graph recovery.
get_code_graph_statusreports missing, fresh, stale, or outdated graph state with generation, edge, file, and binding counts.rebuild_code_graphperforms a server-rooted full rebuild, reconciles bindings, and records the generation transition in the action log. - GitHub release binaries. Release automation builds
and attaches
phr-mcparchives for Linux x86-64, macOS x86-64, macOS Apple Silicon, and Windows x86-64 after release-plz creates the matching package release.
Changed
- The complete language-agnostic platform is now the
default.
initand every non-nonepack selection includellm,confidence,journey,structural, andcontext; language packs remain additive.noneis an explicit, mutually exclusive escape hatch. - Smaller durable context is the default. Fresh projects receive the bounded kernel/context scaffolding and graph state without a separate pack opt-in.
- MCP collection results use stable object envelopes.
list_rules,list_facts,get_agenda,get_consequences, andget_action_lognow return named JSON objects through bothstructuredContentand compatibility text, avoiding SDK failures on top-level arrays. - Documentation is web-native and current. The GitHub
Pages navigation no longer sends readers into Markdown sources; the loop
guide and changelog now have dedicated HTML pages. The README,
explainer, and catalogue use a new mineral teal/indigo visual system
with progressive SVG diagrams for the governance boundary, default
subsystems, structural code graph, RETE internals, and concrete rule
behavior. The docs describe the unified
drift,stats, default-platform, graph lifecycle, and MCP envelope surfaces.
Safety
- Missing, malformed, stale-graph, or generation-mismatched binding evidence never demotes a block. Direct rule-file edits and MCP rule mutations reconcile bindings without advancing the graph generation.
[0.25.0] - 2026-08-01
Changed
init::Packis#[non_exhaustive]. Adding a pack is this crate’s most routine extension point — four landed in recent releases (Confidence,Journey,Structural,Context) and more languages are expected — but each one was a semver break twice over: a variant added to an exhaustive enum, plus a discriminant shift for every variant after the insertion point.cargo-semver-checksflagged both on theContextaddition. Marking the enum non-exhaustive makes future packs a non-event.Migration: only affects code outside this workspace that matches on
Pack. Add a_ => ...arm. Nothing inside the workspace changes, since#[non_exhaustive]does not constrain the defining crate.
Added
- Token-aware durable context (opt-in). A project
that creates
.phronesis/context.jsongets deterministic, budgeted context packing instead of unconditional full-file reinjection. Every renderable unit is an indivisible item measured with its headings and separators, admitted only if it fits its kind ceiling, the shared byte capacity, and — when configured — a soft estimated-token budget (ceil(bytes / 3)). Current enforcement activity gets first claim on the payload, then the kernel, then situational nudges, then activity that overflowed its reserve. .phronesis/kernel.md, the always-on core. Written byinit --packs context.durable.mdkeeps its meaning as the session-level project document and is never rewritten, repurposed, or shrunk.- Situational nudge capsules.
.phronesis/nudges/*.mdcarry strict JSON frontmatter and a static Markdown body, selected by positive facts through the ordinary RETE engine. Bodies never interpolate fact arguments, so a filename or tool payload cannot become a second-order prompt-injection channel. Only four allowlisted predicates may trigger a capsule; adding one is a reviewed code change, not configuration. phr-mcp context inspect | predicates | stats.inspectis a true dry run: it writes no observation, so reading the diagnostic cannot contaminate the data it reports. It lists candidates, costs, ceilings, per-item omission reasons (kind_ceiling,byte_capacity,token_capacity,displaced_by_nudge), capsule load failures, and fact-hydration failures. Human and--jsonoutput are projections of one value.--packs base. Expands to every language-agnostic pack —llm,confidence,journey,structural,context— so the usual shape isbase,<your language>. Language packs are deliberately excluded: several match raw substrings gated only by path, so composing them produces cross-language false positives (the TypeScript: anyrule fires on Rust’s: anyhow::Error).- Context observations. Bounded
kind: "context"records in the existing rotated log carry bytes, estimated tokens, per-kind omission counts, capsule ids, latency, and a raw-truncation flag — no bodies, fact arguments, or user content, and no claim about whether the model read or followed anything.
Changed
- Context packing splits Markdown on
##sections, not blank lines. The section — heading, lead-in, and the list under it — is the indivisible unit. Blank-line paragraphs let an over-budget list be dropped while its lead-in survived, producing text that promised content it did not deliver (“Three heuristic tools …:” followed by nothing). A section is now delivered whole or not at all. - Context source files are capped at 64 KiB. These
are read on every hook invocation, so an accidentally huge file is
ignored with a diagnostic rather than read and discarded each turn. This
is the one place opt-out behavior differs from before: a
durable.mdover 64 KiB now yields no context where it previously yielded a 4 KiB truncation.
Fixed
- cargo-nextest runs grounded no test signal. The
cargo toolchain def accepted
cargo nextest, but its onlytest_summarypattern was libtest’stest result:line, which nextest never emits. The def claimed the command, parsed nothing, and recorded no tests signal — so a project gated oncargo nextest runstayed in the low confidence band and had every commit blocked regardless of how green the suite was.test_summarynow accepts one pattern or several (bare string or array, so existingtoolchains.jsondefs are unaffected); the first pattern that matches wins and all of its matches are summed. .phronesis/nudges/README.mdwas parsed as a capsule. The fileinititself writes produced a load diagnostic on every hook invocation.- A limited
read_recentparsed the entire action log. Every hook asks for a handful of recent entries, and the read parsed every line in the log plus its rotated predecessor to return the last few — so the cost grew without bound as a project accumulated history. A 3.8 MB log cost 16 ms per hook, and a real project measured 30 ms. The read now scans backward from the newest record and stops once it has enough matching entries (counting matches, not lines, so a filter excluding the newest records still reaches back far enough), and only consults the rotated file when the current one cannot satisfy the limit. This affects every caller, including the legacy context path,stats, andtrend. - Activity bullets rendered a dangling
inwith no path. Rules that fire on a shell command rather than a file edit log an emptyfile, which formatted as- WARNED 36m ago: some-rule in— malformed text in the prompt the model reads. The location clause is now omitted when there is no path. This changes the legacy renderer’s output for command-rule entries.
Compatibility
- Without
.phronesis/context.jsonthe session and interaction payloads are byte-identical to previous behavior, capsules are not scanned, and no context observations are written. Pinned by test. Two deliberate exceptions, both listed above: adurable.mdover 64 KiB is now ignored rather than truncated, and command-rule activity bullets no longer carry a danglinginclause. - Re-running
initnever overwrites an existingcontext.json,kernel.md,durable.md, or nudgesREADME.md. session.charter_max_bytesis defaulted, so configuration written before the charter existed keeps loading.
Measurements
Measured on a base,rust fixture carrying this
repository’s own 3,292-byte durable file and five blocked edits,
comparing legacy against opted-in:
- interaction payload 3,602 → 680 bytes (81.1% reduction), 1,201 → 227 estimated tokens;
- session charter 4,062 bytes and truncated mid-rule-list → 2,664 bytes and intact;
- all five current blocking items retained in both;
- zero raw truncations across 22 payloads;
- p95 context construction 3.0 ms with no journey/outcomes hydration, on a fixture whose action log held a handful of records.
That latency figure describes a fresh project and is not
representative on its own. Context construction reads recent hook
decisions, and before the read_recent fix below that read
parsed the entire log: a real project with a 3.8 MB log measured 16 ms,
and one in the field measured 30 ms. Both are over the specification’s 5
ms target. The fix removes the dependence on log size; the figures above
should be read together with it.
Limitations
- The specification’s measurement gate also asks for a second external
corpus and a false-relevance review of capsule matches. The corpus
measurement has not been run; the capsule review is vacuous because no
capsules ship by default.
contextis therefore opt-in via--packs, and is not yet part of the default pack set. - Graph hydration is excluded from capsule selection. The session charter reports graph freshness as a state line only.
per_testextraction remains libtest-only, so the known-bug registry does not see cargo-nextest per-test results.
[0.24.0] - 2026-07-31
Added
- TypeScript structural code graphs. The graph now
discovers npm package units and extracts
.ts,.tsx,.mts, and.ctsmodules, functions, imports, direct test-call coverage, and non-null assertions. Resolution supports relative specifiers,indexmodules,tsconfig.jsonbaseUrl, andpathsaliases while excludingnode_modulesunconditionally. - TypeScript structural warnings.
warn-import-cycleapplies to resolved TypeScript module cycles, andwarn-untested-risky-calluses the narrow!watchlist to report untested unchecked type assumptions. Both remain advisory.
Limitations
- TypeScript project references, JavaScript extraction, and monorepo cross-unit resolution are not included. Unresolved relative and cross-unit imports are counted as skipped evidence rather than silently treated as a clean graph.
- Real-corpus validation against tough-cookie measured 1,221 base edges, 54 derived edges, 105 resolved imports, and zero skipped items across 47 files.
[0.23.1] - 2026-07-30
Fixed
query_code_graphadvertised “Rust only” and a stale identity form. The MCP tool description is what a model reads to decide whether a tool applies, so both claims changed behavior rather than merely being out of date. Python graphs build and query correctly —init --packs structuralin a Python project produces a graph andgraph query defines_fnreturnspython:<dist>::<pkg>::<mod>::<fn>— but the description reported the language unsupported. Its worked example also still used the pre-0.23crate::wmeidentity form, so a caller following it queried a key nothing holds, received zero results, and could reasonably read that as an empty graph rather than a malformed query. The description now names both languages, gives the current identity form with an example per language, and markscalls_apias the Rust-only relation it is.
[0.23.0] - 2026-07-30
Added
- Structural code-graph facts
(
structuralpack, aliasgraph). A durable, gitignored graph of architectural relations at.phronesis/graph.jsonl, extracted by thePostToolUsesensor and hydrated into the RETE network atPreToolUse. Ships twowarnrules:warn-untested-risky-call(a production function calling a panicking API with no direct test) andwarn-import-cycle(a module in an import cycle). Both joinedited_file, so they report the file in front of you rather than the whole repository on every edit. Seedocs/specs/SPEC-triple-store-rete.md. - Rust and Python extractors. Entity identity is
<lang>:<package>[#<target>]::<module path>. Rust resolves Cargo packages, compilation targets, and dependency aliases including[workspace.dependencies]inheritance; Python resolves distributions frompyproject.toml(PEP 621 and Poetry), bothsrc/and flat layouts, and imports across sibling distributions in one repository. - Graph CLI and MCP surface.
phr-mcp graph rebuild,graph status, andgraph query, plus thequery_code_graphMCP tool. event.file_relfor Rhai predicate providers — the edited path in the repo-relative form the graph keys files by, so provider-emitted facts can join graph facts on a path.event.file_pathremains the host’s absolute path.
Changed
- Graph identity carries an explicit format stamp.
.phronesis/graph.indexrecords the identity scheme it was built under. A graph built by an older version is reported as outdated rather than fresh, and the next save rebuilds it — content hashes cannot detect an identity change, because the files themselves do not change.
Fixed
- The
PostToolUsegraph sensor never ran through a real hook. A traversal guard rejected absolute paths, which is the only form hosts send; the sensor was additionally gated behind post-phase rules, which a pre-phase-only pack never has. Both are fixed, andrepo_relativenow resolves symlinked project roots (/varvs/private/varon macOS). - A parse failure destroyed the file’s graph evidence and reported success. An empty extraction was indistinguishable from “this file defines nothing”, so a malformed mid-edit save erased every function, call, and import the file had and recorded its hash as successfully indexed. Parse failure now preserves prior evidence and leaves the file reported stale.
#[cfg(not(test))]and#[cfg(feature = "test-utils")]were classified as test attributes, dropping production functions fromdefines_fnand turning their calls into coverage edges.cfgpredicates are now parsed rather than token-scanned.- A deleted file kept its edges and its index entry, leaving the graph permanently reporting drift.
- The Codex adapter was not wired for the code graph — no sensor, no hydration, and no agenda update before firing, so every purely RETE-derived verdict was computed and discarded.
- The MCP
audit_codebasetool omitted the graph merge the CLI performs, reporting zero structural debt regardless of the graph’s contents and writing that zero into the debt trend. - The sensor built a graph in every project, opted in or
not. Moving it ahead of rule loading (necessary, since the
structural pack ships
phase: "pre"rules exclusively) removed an accidental gate without adding a deliberate one, so a project on--packs llmgained an unasked-for.phronesis/graph.jsonland a per-save extraction pass. The graph’s own presence is now the opt-in signal.
Known limits
calls_apiis deliberately empty for Python: there is no defensible equivalent of Rust’s closed panic watchlist. Python projects therefore firewarn-import-cycleonly.tested_bymatches by bare short name and over-approximates coverage, sountestedunder-approximates. That direction is chosen — a missed warning is recoverable, a false “untested” verdict is not.- Both rules
warn. Promotion toblockawaits a second measured corpus.
Migration
- The graph and its index are derived, gitignored state; nothing needs
to be committed or hand-edited. An existing graph is detected as
outdated and rebuilt automatically on the next save, or on demand with
phr-mcp graph rebuild.
[0.22.0] - 2026-07-25
Added
- Codex lifecycle integration.
phr-mcp codex-hooknow implements the currentPreToolUse,PostToolUse, session, prompt, compaction, and subagent contracts for Bash andapply_patch;phr-mcp initsafely merges project hooks and project-scoped stdio MCP registration without bypassing Codex’s/hookstrust review. - Extensible predicates. Project-owned Rhai providers
under
.phronesis/predicates/*.rhaican derive new RETE facts from normalized hook events. MCP tools add, inspect, test, list, and remove providers so agents can evolve the rule vocabulary alongside rules. Multi-file operations expose a once-per-operationevent.filesbatch context before per-file evaluation; the repository includes a dogfoodchange_set.rhaiclassifier.
Changed
- Interaction context terminology. The per-prompt
context command is now
interaction-context;turn-contextremains a compatible CLI alias and the old Rust helpers remain deprecated wrappers. The unrelated markdownset_section_contextMCP workflow is unchanged.
Fixed
- Contract-grounded hook behavior. Current snake-case payload fields and PascalCase event names are decoded, pre-action violations return a real deny decision, post-action feedback remains advisory, patch paths are validated, and executed calls retain action-log, journey, and grounded outcome data.
- Honest Codex fixtures. Schema-authored fixtures are
labeled
authoredand use the payload-corpus envelope instead of claiming unverified runtime capture provenance.
[0.20.0] - 2026-07-13
Changed
- Context-struct API migration. Four call surfaces
now group related arguments into explicit input types:
Consequence::from_rule_firingtakesRuleFiringContext;journey::derive::assert_factstakesDeriveInput;outcomes::extracttakesExtractInput; andoutcomes::adapter::extract_fromtakesExtractFromInput. These are breaking Rust API changes; construct the corresponding context/input struct and pass it in place of the former positional arguments. - Audit precision and maintainability. Rust parameter-count auditing now evaluates each function independently instead of combining same-named methods, and high-complexity paths across the engine, hook, journey, and outcome layers use focused helpers and input types.
Fixed
- Honest payload scrubbing. Scrubber construction rejects empty, relative, and filesystem-root scrub roots. Scrubbing recognizes colon-delimited bearer credentials and credential URLs with an empty username, detects residual sensitive material, and aborts before backup or overwrite when safety cannot be established.
- Evidence integrity. Residual-risk failures are surfaced rather than silently treated as successful anonymization, scrub integration tests are top-level tests that are actually discovered, and the checked-in toolchain definitions are regression-tested against the generated scaffold.
Migration
let consequence = Consequence::from_rule_firing(
RuleFiringContext {
rule_id,
predicate,
bound_facts,
kind,
},
&payload,
)?;
journey::derive::assert_facts(&mut network, DeriveInput {
project_root,
rules: &rules,
config: &config,
scope: WindowScope {
current_sid: session_id,
now_ts: now,
},
}).await?;
let facts = outcomes::extract(outcomes::adapter::ExtractInput {
root,
subject,
command,
output,
command_exit,
});
let (tags, subject) = outcomes::adapter::extract_from(ExtractFromInput {
project_root,
tool_name,
command,
output,
command_exit,
});[0.19.0] - 2026-07-12
Added
PHRONESIS_CAPTURE_DIR— when set, pre-check/post-check tee the raw stdin payload to<dir>/payloads.jsonlbefore parsing (flock’d for concurrent-hook safety, best-effort and off by default). This is how the payload-contract corpus below gets refreshed against a real CLI’s current payload shape.payload_scrubmodule +phr-mcp scrub-payload <path> [--write] [--project-root DIR]— anonymizes captured payloads for committing as fixtures. Operates on JSONL in/out;--writebacks the original up to<path>.bakbefore overwriting;--project-rootdefaults to the current working directory so in-project paths survive scrubbing while$HOME, username,session_id,transcript_path, and other out-of-project paths are rewritten to deterministic, indexed placeholders. A residual leak or unrecognized shape aborts the run before anything is written.- Payload-contract corpus. Fixture payloads for
Claude Code and Gemini CLI hook events under
crates/phronesis-mcp/tests/fixtures/payloads/, each taggedprovenance: "authored"— hand-written approximations of the real envelopes pending supersession by live captures via the tee above. A contract runner replays every fixture through the real binary and asserts rule liveness (a hook or tagger that silently no-ops now fails CI) and journey-journal outcome tags. A companion hook-event registry test suite pinsinit’s hook wiring to event names that actually exist on each host CLI, including a regression pin on the 0.17.1BeforeModelRequestincident.
[0.18.0] - 2026-07-11
Added
- Neutral toolchain outcomes. Build/test grading is
no longer cargo-specific: declarative
ToolchainDefs (built-ins ∪ project.phronesis/toolchains.json, project ids overriding built-ins) drive outcome detection via named-capture regexes, with the command exit code as the authoritative build signal and regex refinement layered on top.phr-mcp initscaffolds example pytest/tsc defs;phr-mcp toolchains [--json]lists the effective registry. command_exitcapture. PostToolUse payloads from shell tools (Bash,run_shell_command) are probed for a numeric exit code (exit_code/exitCode/returncode/code/status, then a trailingexit code: Ntext fallback), journaled ascommand_exit, and used to grade outcomes — a non-zero exit with a test summary grades build-pass/test-fail (the pytest exit-1 case).- Journal compaction.
.phronesis/journey/events.jsonlis bounded (16 MiB default,PHRONESIS_MAX_JOURNAL_BYTESoverride, 1 GiB ceiling): compaction retains the 10k-record tail plus the latestoutcome:*record per subject, atomically via temp+rename with fd/inode revalidation so concurrent appends never land in a stale file.
Removed
CargoAdapter— cargo grading now flows through the same toolchain-def registry as every other toolchain.
[0.17.1] - 2026-07-06
Fixed
- Gemini CLI turn-context hook never fired.
initwired the turn-context hook into.gemini/settings.jsonunderBeforeModelRequest, which is not a Gemini CLI hook event — Gemini silently ignored it, so per-turn context injection (recent hook decisions + durable directives) never ran in Gemini sessions. Now wired underBeforeAgent, the per-prompt analogue of Claude Code’sUserPromptSubmit. Re-runninginit(orinit --hooks-only) also removes the dead legacyBeforeModelRequestkey from existing settings. The emittedhookEventNamestays"UserPromptSubmit"for both CLIs: Claude Code validates the field, Gemini reads onlyadditionalContextand ignores the echo.
0.17.0 - 2026-07-04
phr-mcp, phr, and phronesis-rhai all release as
0.17.0 — the workspace adopts lockstep versioning
([workspace.package] version); from this release one number
covers all three crates. (Previous: phr-mcp 0.16.2, phr 0.14.0,
phronesis-rhai 0.1.0; the jumps are version-line unification, not
breaking changes.)
Changed
- MCP-crate decomposition.
hook.rs(1764 LOC) andsyntax/rust.rs(1622 LOC) split into focused submodules;main,audit::run/run_profiled(deduped via a shared core), and ~30 further functions decomposed below the let-count audit thresholds. Audit debt drops 59 → 8 hits; the remaining 8 are core-engine functions deferred to the embedded-consumer-gated engine spec. Behavior-preserving; no public API changes. Implementsdocs/superpowers/specs/2026-06-28-mcp-crate-decomposition-design.md.
0.16.2 - 2026-07-03
Added
phr-mcp migrate-extracted-rules <path> [--dry-run]— the salvage command deferred from 0.14.0. Rewrites pre-0.14.0extract_rulesoutput in place (with a.bakbackup): strips the bracketed extraction-time prefixes ([pattern],[anti_pattern],[context],[problem],[directive]) from messages, demotesblockactions towarn, and demotes tologany extracted rule duplicating a structural Rust-pack rule (the SPEC’s static keyword table: unwrap, clone, Deref, &String, &Vec, thiserror). Extracted rules are detected by theirmarkdown_rulecondition, so hand-written rules are never touched. Idempotent. Implements the salvage path indocs/specs/SPEC-extract-rules-defaults.md.
[0.16.1] - 2026-07-03
Added
- Named function detail on AST-predicate audit hits.
The audit table/JSON previously rendered whole-function hits
(let-binding counts, etc.) as
lines: 1, 1, 1— the placeholder line number.FileAuditgains adetailsfield parallel tolines, rendered asaudit.rs — run (26 let bindings), run_profiled (32 let bindings)in both output formats.
Changed
- Rust pack: audit let-rules scope to
src/.audit-rust-let-binding-count-high/-let-mut-count-highgain afile_path_matches: "src"gate so examples, benches, and tests are no longer flagged for let-count debt. - Rust pack:
audit-newtype-id-stringhonors doc exceptions. The rule gainsdoc_excepted: true, so a///field doc marks an intentional string ID as an accepted exception.
Security
- Migrate
serde_yml→serde_norway.serde_yml 0.0.12and itslibymlbackend are archived and flagged unsound (RUSTSEC-2025-0068 / RUSTSEC-2025-0067) with no fix coming.serde_norwayis the RustSec-recommended maintainedserde_yamlfork with the same API; the only call site (wiki frontmatter parsing) changes crate path only. Removeslibymlfrom the dependency tree entirely.
Fixed
- Wiki frontmatter closing fence must be exactly
---on its own line. The parser previously accepted any line beginning with three dashes (----,--- see appendix) as the closing fence, silently truncating the YAML and leaking the line’s tail into the body. The fence search now skips lookalikes; a page with no true fence reports “missing closing---fence” instead of parsing corrupted content. Pinned by five new parser tests (lookalike lines, fence at EOF, CRLF endings).
[0.16.0] - 2026-07-03
phr-mcp 0.16.0; phr library bumps to 0.14.0 (engine changes this
round — new scripting trait, a removed method, and a new feature gate);
new phronesis-rhai 0.1.0.
Three changes that tighten the engine/embedding-host boundary ahead of a 1.0 line: an expressive scripting layer, removal of the last consumer-specific engine API, and a feature gate that makes the default public surface equal what the bundled MCP consumes.
Added
phronesis-rhaicrate +ScriptEvaltrait. The core__script__evaluator now lives behind aScriptEvaltrait (ReteNetwork::with_script_evaluator). The newphronesis-rhaicrate providesRhaiScriptEvaluator, a sandboxed Rhai implementation (Engine::new_raw+ StandardPackage, operation/call-depth/string/array/map caps,sync) supporting numeric comparisons and boolean combinators over fact arguments — the guard expressions the builtin two-primitive DSL can’t express. Scripts seefacts(array of#{predicate, args}) andbindings(map) and must returnbool; errors/non-bool are treated as a blocked guard.CompositeScriptEvaluatorroutes builtin-DSL forms (facts_contain/facts_count) to the builtin evaluator and everything else to Rhai, so bundled packs and Rhai guards coexist in one rules.json. Wired intophronesis-mcpbehind an off-by-defaultrhaifeature (server- pre/post hooks via a
net::build_networkseam). Implementsdocs/superpowers/specs/2026-06-01-rhai-script-evaluator-design.md.
- pre/post hooks via a
embedding-hostcargo feature onphronesis(off by default). Gates the ~10 publicReteNetworkmethods only an external embedding host needs (restore_persistent_facts*,execute_next_agenda_item,fact_ids_matching,fact_count,facts_matching_predicate,get_rules_count,get_wmes_by_condition, and the instrumentation getters). The default surface equals what the bundled MCP consumes, so the compiler enforces the symmetry. CI exercises the feature config. Implementsdocs/superpowers/specs/2026-06-13-embedding-host-feature-gate-design.md.
Changed
ScriptEvaluatorrenamed toBuiltinScriptEvaluator(implementsScriptEval).ScriptEvaluatorremains as a backwards-compatible alias and the inherentevaluatestill returnsReteError, so existing callers are unaffected. The misleading “Rhai” docstrings in core (the builtin is a hand-rolled DSL, not Rhai) are corrected.
Removed
ReteNetwork::get_persistent_factsand its hardcodedPERSISTENT_PREDICATES— a downstream consumer’s game-state vocabulary baked into a “domain-neutral” engine, deprecated since 0.11 and now that the consumer has migrated ontofacts_matching_predicates, deleted. The remaining consumer-flavored doc/example vocabulary in the engine and MCP fixtures is neutralized.restore_persistent_facts*stay (generic bulk-assert; now behindembedding-host). Implementsdocs/superpowers/specs/2026-06-13-domain-neutral-persistent-facts-design.md.
[0.15.0] - 2026-06-24
Added
- Loop-based agent programming guide
(
docs/loop-programming-guide.md) — writing recurring /loop-driven agent workflows against phronesis, with captures from live sessions in this repo. journey_derivescaling bench plus an ADR recording the scaling behavior of journey fact derivation.
Fixed
- Journey rules with undefined selectors fail closed.
A rule referencing a tag absent from
.phronesis/journey.jsonwas fail-open: a stderr warning, then the rule loaded anyway — and for absence-style rules (== 0) the missing tagger looked like zero occurrences, so the rule fired on every call. Configuration errors (BadWindow,UndefinedSelector) now propagate — the hook exits 2 (pre-check) / 1 (post-check) naming the offending rule id and missing selector — while transient journal I/O errors stay fail-open. See the decision page2026-06-23-undefined-selector-rejection.md.
0.14.0 - 2026-06-21
Dogfooding-driven polish. The 0.13.x patch line was driven by
playtest bugs visible only after install; 0.14.0 closes the four
next-deepest friction points the same playtests surfaced. Compiled under
docs/specs/SPEC-0.14.0-dogfooding-polish.md.
Added
journey_filtered_since_ge(target, counted, k)aggregator — the existingjourney_since_gecounts distance over every record; a long Bash session could trip “8+ tool calls since build” with no writes. The new aggregator emits a k-ladder up to the count ofcountedrecords appearing after the most recenttargetrecord. Rules can now express “8 writes since last build” directly:facts_count('journey_filtered_since_ge', ['build','write','8']) >= 1with awritetagger keying onchange_type=edit|write|multiedit|replace|write_file. The existing five aggregators are unchanged. Seedocs/specs/SPEC-journey-filtered-since.md.confidence_enabledzero-arg marker fact — asserted at every hook fire when.phronesis/confidence.jsonexists, mirroring theclock_facts.rs::business_hours_localpattern. Lets rules condition on opt-in state via the existingfacts_count('confidence_enabled', []) == 0absence form. Generalizable: future packs can shipjourney_enabled,wiki_present, etc. using the same shape. Seedocs/specs/SPEC-pack-opt-in-facts.md.
Changed
- Confidence gate broadens to all commit-producing porcelain
commands.
confidence-low-blocks-commitandconfidence-medium-warns-commitnow matchbash_command_matches: "git (commit|merge|rebase|cherry-pick|revert|pull)"instead of the literal"git commit". Closes the gate-bypass-by-merge hole surfaced during the journey-facts merge night (5 of 6 commit-producing commands silently bypassed the gate). Seedocs/specs/SPEC-gate-merge-commits.md. nudge-verify-before-commitself-deactivates when confidence is on. The rule gained a secondwhenclause:{ "__script__": "facts_count('confidence_enabled', []) == 0" }. The confidence gate enforces the same call-chain-tracing discipline by countingsignal_passfacts; the nudge was redundant in that mode and was double-warning on everygit commit. Projects without confidence are unaffected.extract_rulesdefaults actionwarn, notblock. A live invocation added 27 block-action rules to a project rules.json overnight; with any section context set, every pre-check fired 6 simultaneousconstraint_violations and exited 2 on every tool call. Block is reserved for known-bad code shapes; pattern reminders are advisory. Seedocs/specs/SPEC-extract-rules-defaults.md.extract_rulesstrips the bracketed metadata prefix ([pattern],[anti_pattern],[context],[problem]) from the user-facing message. Those were extraction-time discriminators leaking into prose.
Migration
- Projects that ran
phr-mcp init --packs confidencebefore 0.14.0 carry the narrow gate pattern in.phronesis/rules.json. Either re-runphr-mcp init --rules-only --force --packs confidence(rewrites the rule pack with the broadened pattern, backs up to.bak) or hand-edit the twobash_command_matchesclauses. - Projects with the old
nudge-verify-before-commitrule should add the secondwhenclause to opt into the supersession. Same--rules-only --forceflow works. - Projects that already invoked
extract_rulesand want to salvage their extracted rules can apply the in-tree recipe indocs/specs/SPEC-extract-rules-defaults.md§“Salvage path.” Aphr-mcp migrate-extracted-rulescommand is deferred to a follow-up PATCH.
Deferred (intentional, with specs on disk for future work)
extract_rules: per-pattern marker conditions (Problem 3b), structural-rule skip-list (Problem 4a), and themigrate-extracted-rulescommand. The umbrella spec scopes 0.14.0 to the action/prefix defaults; the rest rides a follow-up PATCH.- Subject inheritance across merge commits. Real
design surface; the
SPEC-gate-merge-commitsopen question flags it. - Repo-lifetime journey windows (
r) — still phase 2 ofSPEC-journey-facts.
Notes
- Coverage. Workspace lines at 86.20%+ across the four implementations, up from the post-0.13.x baseline of 85.94%.
phrlibrary version unchanged at 0.13.3. The engine wasn’t touched in 0.14.0; onlyphr-mcpbumps.phr-mcp’sphrdep stays pinned at0.13.3.
0.13.2 - 2026-06-20
Fixed
bash_command_matchestaggers actually fire.journey::tagger::tagger_factsbuilt only file/content facts and relied on a “tagger regex pass” implied by a misleading comment but never implemented. The defaultbuildtagger ({ "bash_command_matches": "cargo (build|check|test)" }) silently no-fired on everycargoinvocation.tagger_factsnow walkstaggers[*].when[*](including nestedorclauses) collectingbash_command_matchespatterns, regex-matches each against the bash command, and asserts one syntheticbash_command_matches:<pattern>Fact per match — the same patterncheck_bash_command_patternsuses for top-level rules (hook_facts.rs:316). Surfaced in a live playtest, not in unit tests.HookPayload.tool_outputacceptstool_responseas a serde alias. Claude Code’s PostToolUse hook delivers Bash output undertool_response, nottool_output. Without the alias, the field wasNone/ empty string, socompiled("")returned true (no error patterns match → spuriousoutcome:compile_ok) andTEST_RESULT.captures_iter("")returned nothing (outcome:test_passnever fired). Net effect: confidence-scoring was wedged at “low / compile” for every realcargorun, even when tests were green — the whole gate-by-band feature was non-functional in production. Tests and fixtures all passed because they synthesized payloads undertool_output; only a live hook payload surfaced it. Backward compatible with Gemini and existing fixtures.
0.13.1 - 2026-06-20
Fixed
- Same-day sid fallback collision. When
.phronesis/journey/sessionwas missing, the journey fallback was the literal placeholders-YYYY-MM-DD-fallback, collapsing distinct sessions to the same id. Nowjourney::current_sidreads-or-creates atomically in thecontext::ensure_session_idformat (s-YYYY-MM-DD-<6 hex>); the placeholder is gone. - Triple-duplicated
current_sidconsolidated. Three independent implementations (inhook,main, andserver::get_journey) coalesced into a singlejourney::current_sid(project_root)helper. Same semantics, one source of truth.
Changed
- CLAUDE.md packs list now includes
confidencealongsidejourney. The scaffolded CLAUDE.md previously enumeratedjourneyonly. phr-mcp journeynudges on empty config. When.phronesis/journey.jsonis missing or empty, the CLI emits a stderr suggestion (“runphr-mcp init --packs journeyto scaffold one”) before falling back to an empty config. The hook stays silent — fail-open is advisory there, not user-facing.
Fixed (engine)
- Pure-script rules now fire. Rules whose
whenwas entirely__script__clauses had no alpha state, no terminal id, no p-state — they never reached the agenda, because__script__clauses are post-filters on activations and with no other clause there were no activations to filter.update_agendanow branches onreal_condition_count == 0(count of non-__script__conditions per loaded rule) and, for pure-script rules, evaluates the script clauses against the current fact base with empty bindings, emitting an activation when every clause passes. Dedupe key is<rule_id>— fire-once-ever, the right semantics for threshold rules. Alpha/beta network and the production network shape are unmodified; mixed-script behaviour is unchanged. Surfaced by the journey-facts SPEC’s headlineauth-churn-without-testsrule, which is naturally two__script__clauses (facts_count(...) >= 5ANDfacts_count(...) == 0). Thejourney_seenanchor leaf added as a workaround is no longer required.
0.13.0 - 2026-06-20
Added
- Journey facts (new fact family + new hook stage) —
call-window and session-scale predicates that summarise
trajectory, not the current diff. Five aggregators over
project-defined tags (
journey_occurrence,journey_count,journey_seen,journey_since_ge,journey_distinct) with windowed selectors (5cfor last 5 calls,30m/2h/7dwall-clock,sfor session; repo-lifetimeris phase 2). Rule-driven derivation; the journal is the substrate, the predicates are recomputed each cycle. Seedocs/specs/SPEC-journey-facts.md.- Append-only journal
(
.phronesis/journey/events.jsonl) writes a record per post-check with subject + tags + monotonic seq. Tail-read for hot queries (SUFFIX_HARD_CAP = 10_000lines) and per-subject read for outcomes folding. - Taggers reuse the predicate engine —
taggers[*].whenclauses are the same DSL as rule conditions.bash_command_matches,new_content_contains,file_path_matchesall available. Project-defined via.phronesis/journey.json. - Derivation pass runs at every pre-check and
post-check via
journey::derive::assert_facts; selector validation rejects malformed journey config without exit-2. - Outcomes ledger folded into the journey journal
(the notable storage change of 0.13.0).
outcomes/ledger.rsis gone;outcomes/cargo.rsnow returns(tags, subject)and the hook stamps them on a single journal record.outcomes/derive::signalsreads viajourney::journal::read_recent_subject. Confidence-scoring behaviour is byte-identical; the storage is unified. SessionStartstamps.phronesis/journey/session; pre/post-check read it.PHRONESIS_NO_JOURNEY=1disables both paths. Fail-open throughout — corruptjourney.jsonor missing journal degrades to “no journey facts,” never exit 2.phr-mcp journey [--json] [--explain <rule-id>]renders thejourney_*facts a derivation pass would assert against the current journal, with--explainfiltering to a single rule’s dependencies.- MCP tool
get_journeymirrors the same table/JSON view so the agent can ask “what does my trajectory look like” mid-conversation. phr-mcp init --packs journeywrites a starterjourney.jsonand ensures it is tracked.
- Append-only journal
(
Changed
- Workspace bumps to 0.13.0.
phrandphr-mcpmove together;phr-mcp’sphrdep bumps to match.
Notes
- Coverage discipline. The workspace stayed at or above the pre-feature baseline of 85.4% lines across the journey-facts merges; the journal and tagger modules sit near ~90%.
0.12.0 - 2026-06-19
Added
- Confidence scoring (first milestone) — gate LLM
output on three grounded outcomes before a
git commit: does it compile, do the tests pass, does it catch a known bug (a TDD test red on the buggy baseline that goes green). Seedocs/specs/SPEC-confidence-scoring.md.- Domain-neutral outcome facts (
build_outcome,test_outcome,bug_check_outcome) behind a per-toolchain adapter layer (cargofirst; pytest/tsc/go later emit the same neutral facts). - A per-subject ledger
(
.phronesis/outcomes/<subject>.jsonl) bridges the stateless hook invocations; the pre-check re-derivessignal_passfacts and gate rules count them with the existingfacts_count(...)DSL (<=1blocks,==2warns, 3 passes clean). - Post-check parses a build/test command’s captured output into the
ledger; a
git commitsettles the open work unit. - Known-bug registry in
.phronesis/bugs.json. phr-mcp confidence [--subject <id>] [--json]— read-only band/signals report for the open work unit.phr-mcp init --packs confidence— writes the commit-gate rules plus the.phronesis/confidence.jsonopt-in marker and.phronesis/bugs.jsonregistry, and carves both back into.gitignoreas tracked config.- MCP tools
get_confidence(band/signals report) andsubmit_suggestion(declare an explicit work unit, e.g. a translation, and accrue signals to it). - Opt-in per project via
.phronesis/confidence.json; fail-open throughout, so projects that haven’t enabled it are unaffected.
- Domain-neutral outcome facts (
0.11.0 - 2026-06-13
Added
- Public fact-query API on
ReteNetwork—facts_snapshot,facts_matching_predicate,facts_matching_predicates(predicate-set membership),facts_matching(positional-arg filters),fact_ids_matching,get_fact_by_id,fact_count. Sync, owned results sorted by fact id, so embedding hosts need not reach intowme_manager. - Richer
list_factsMCP tool — the existingpredicatefilter plus newpredicates(set membership) andarg_filters(positionalarg = value) params, backed by the fact-query API. Lets coding agents query working memory by predicate set or argument, not just list-all. bash_command_matchespredicate — regex rules over Bash/command-tool text, gated to command tools (file content quoting the same text never fires). Ships two LLM-pack guard rules (stage-explicitly, don’t-kill-build).- Tree-sitter AST predicates for Python and
TypeScript — Python:
python_bare_except,python_mutable_default_arg,python_function_param_count_high,python_function_missing_docstring; TypeScript (TSX grammar included):ts_explicit_any,ts_non_null_assertion,ts_suppression_comment,ts_function_param_count_high. - Silent zero-result audit diagnostics —
phr-mcp auditand theaudit_codebasetool now explain a no-hits result when the cause is recoverable (no rules carryaudit: true, or the walker scanned 0 files) instead of returning an empty shape indistinguishable from a failure. - CI — GitHub Actions workflow (fmt + clippy
-D warnings+ tests, on MSRV 1.90 and stable). - Typed-error, retraction-semantics, and salience-order test suites.
Changed (breaking)
Result<_, ReteError>replacesResult<_, String>across the engine crate.ReteErroris a matchable enum (FactNotFound,LockPoisoned,DuplicateFactId,BindingConflict, …) implementingstd::error::Error;From<ReteError> for Stringeases migration for string-carrying hosts.- Duplicate fact ids are rejected. Asserting an id
already present with different content errors
(
DuplicateFactId); an identical re-assert is an idempotent no-op. Previously a duplicate silently corrupted the predicate index (the same fact was returned twice fromget_by_predicate). - Same-salience agenda items fire in FIFO (insertion)
order. Previously tie order was
BinaryHeap-arbitrary; firing order is now deterministic.
Deprecated
ReteNetwork::get_persistent_facts— it hardcodes consumer-specific predicates, which don’t belong in a domain-neutral engine. Define your own predicate set and callfacts_matching_predicates(&YOUR_SET). Slated for removal in 0.12.
Fixed
- Retraction purges stale agenda items referencing the retracted fact, so a pending rule can no longer fire against a fact that is no longer true.
- Refraction keys compare exact WME ids — retracting
f1no longer clobbers the refraction state off10(was a substring match). get_memory_driftmarks guidance actionable only when it maps to an expressible predicate (named command, file/path/code shape, or function shape); operational prose is bucketed ambient. Actionable entries now also register coverage fromdurable.md, so the drift list converges.
Known follow-ups (specs landed, implementation deferred)
SPEC-gate-merge-commits.md— broaden the confidence gate’sbash_command_matchespattern from"git commit"to"git (commit|merge|rebase|cherry-pick|revert|pull)". Five of six commit-producing porcelain commands currently bypass the gate. Live-tested during the journey-facts merge night. PATCH-shaped change for 0.13.x.SPEC-pack-opt-in-facts.md— pack-level supersession via zero-arg marker facts. Whenconfidenceis opted in, assertconfidence_enabledat hook fire (mirroringclock_facts) and conditionnudge-verify-before-commiton its absence via the existingfacts_count(...) == 0form. Removes the double-warn on everygit commitfor projects running bothllmandconfidencepacks. PATCH for 0.13.x.
Earlier releases
Pre-0.11 history (0.10.0 and earlier) is recorded in the git log and
docs/specs/. Notably, 0.10.0 added wiki-drift, the
block-pattern rules, and the v2 rule schema.