原始内容
@minhduydev/pi-learning
Append-only, project-local learning package for Pi: a trust ledger that records observations and promotes them through an explicit state machine with two separate gates — one you drive by hand, and one auto-safe mode drives for you.
Read this first. In a Pi-trusted project with no configuration of its own, auto-safe is on by default in Pi-trusted projects: low-risk observations are captured, approved, and activated without a human step, and activated learnings are injected into agent turns. Those records land in the separate
auto-approved/auto-activestates and are labelledauto-safeeverywhere they surface, so they are never presented as something you reviewed. In an untrusted project the runtime is fully inert: no capture, mutation, activation, or injection runs. Learning records are not readable throughshow/recall, even if an operator-enabled ledger already exists. The sole write exception is the separate phase5 security-quarantine inbox: rejected external signals are durably recorded there, never applied to the learning ledger. To turn automation off, see Turning it off.
This is a selective fork of Matt Devy's pi-continuous-learning (MIT). See
UPSTREAM.md for provenance, kept concepts, intentional
divergence, and sync policy. The upstream runtime was rewritten — this is not a
compatibility fork and ships no compatibility shims.
What's new in 0.5.0
- V2 context requests retrieve context for evidence-free launch intents while reserving proof binding for completion.
- Privacy-safe telemetry reports bounded timing, counts, result codes, and tagged digests without prompt text, learning content, paths, or raw errors.
- Correlated delivery and usage receipts preserve durable learning identity across restart, injection, usage, and task outcome.
- Peer
@minhduydev/pi-coremoved to^0.3.0. SeeCHANGELOG.md.
What it does (and does not) do
- Two gates, never conflated. The manual
/learn review -> approve -> activateflow producesapproved/active. Auto-safe producesauto-approved/auto-active. The replay physically cannot put a system actor into a human-gated state, and the record fields are separate (approval/activationvsautoApproval/autoActivation). - Auto-safe is bounded. It only acts on the conservative kinds
patternanddiscovery, only when every evidence reference meetsminEvidenceTrust(verified-commandby default), never on content matching the high-risk pattern (credentials, permissions, destructive operations), and never on a record that conflicts with an existing one. - No model, no network, no system prompt. It does not call a model or provider,
mutate the system prompt, start timers, spawn processes, or register tools.
Injection into a turn is an additive,
display: falsecontext message. - Project-local. Learnings live under
.pi/artifacts/learning/v1/in the current project. There is no global/home store. - Append-only, recoverable.
events.jsonlis the source of truth;index.jsonis a rebuildable projection. Writes are idempotent, locked across processes,fsync'd, and validated before they are persisted. - Fail-closed configuration. Every configuration read that fails — missing, unreadable, malformed, unknown field, unsafe value — disables automation. A typo while trying to switch it off can never switch it on.
- Explicit access capability. Effective config exposes learning-record access
as
noneorread-write, separately from automation, plus an explicit append-onlysecurityQuarantinecapability. Untrusted projects can never acquire learning-record access or automation; only rejected external signals may enter the quarantine inbox.
Turning it off
Either of these, in order of precedence:
// .pi/settings.json — project-owned, wins over everything else
{ "pi-learning": { "enabled": false, "mode": "manual" } }
/learn disable # writes .pi/artifacts/learning/v1/config.json
/learn status always prints the effective configuration and where it came
from, so the UI and the runtime cannot disagree.
Trust model (v1)
human gate
observation -> candidate -> approved -> active ----> retired
| ^
| auto-safe gate |
+-> auto-approved -> auto-active -+
| |
+------------+--> /learn approve promotes
into the human gate
- No
candidate -> activebypass. Human approval and activation are separate, explicit actions, and onlyactor: "human"events reachapproved/active. - The auto-safe gate is a parallel path, not a shortcut through the human one. It
requires
actor: "system"andmode: "auto-safe"on the event; either alone is rejected asauthorized-actor-required. /learn approve <id>on anauto-approved/auto-activerecord promotes it: the machine gate is cleared, your approval is bound to the exact content digest, and the record drops toapproveduntil you activate it.- Retrieval serves both
activeandauto-active, and every result carries atrust: "human" | "auto-safe"label. - Approval binds an exact SHA-256 content digest. Editing a record creates a new revision with a new digest and invalidates any prior approval.
- Approved/active records are never auto-mutated, decayed, or deleted.
- Retiring records a reason and an optional supersession link.
- Evidence references carry
source,locator,digest,observedAt, optionalsession/task/worktreeids, and atrustclassification. No float confidence is used as a trust gate.
Safe analysis pipeline (library, no model calls)
Four focused, dependency-light modules provide the evidence -> review -> retrieval
path used by the manual /learn workflow. Nothing here calls a model, provider,
network, tool, mutates the system prompt, or runs a background scheduler (static
and runtime asserted by tests, including src/index.ts).
safety.ts — SafetyScanner port + conservative default
A self-contained, dependency-free scanner. The package cannot import private
pi-harness safety internals, so it exposes a SafetyScanner port with a
ConservativeSafetyScanner default a consumer may override.
- Quarantines (blocking): hidden / bidi / zero-width / control Unicode and prompt-injection override patterns. Content with a blocking finding is never forwarded to a payload.
- Redacts (defense-in-depth): AWS access keys, GitHub tokens, Slack tokens,
PEM private-key blocks, bearer tokens,
key=valuecredential assignments, and absolute POSIX/Windows home paths (/Users/...,/home/...,C:\Users\...). Safe project-relative paths are preserved. Redaction reports carry only counts and kinds — never raw secret material. - Source allowlisting remains the primary control; redaction is secondary.
evidence.ts — manual evidence -> validated, frozen payload
Turns allowlisted manual EvidenceInput into validated EvidenceRef s
and a deterministic, frozen ReviewPayload.
observedAtmust be a real ISO-8601 timestamp with a timezone.EvidenceInputhas notrustfield: trust is derived from the declaredsourceand bound into the evidence digest. The v1 command accepts only human evidence; future adapters must authenticate stronger source classes.locator/sessionId/taskId/worktreeIdare bounded and normalized: safe opaque ids are kept; path-like, secret-bearing, or otherwise unsafe identifiers are sha256-hashed. Hidden Unicode or prompt injection in any field is quarantined (rejected), never hashed-through.- Excerpts are bounded (
maxExcerptChars), scanned (injection/hidden-Unicode rejected), redacted (secrets + home paths), and keep safe project-relative paths. ReviewPayload.bytesis the canonical encoding bindingprojectKey, an optionalworktreeKey, a non-empty boundedquery, and the evidence (at least one ref).digest = sha256(bytes);estimatedTokens = ceil(bytes/4); plus an aggregated redaction report. The preview bytes are exactly the analyzed bytes.buildReviewPayloaddoes not trust passedEvidenceBuildResultobjects: it re-derives trust from source, re-normalizes ids, re-scans excerpts, recomputes each ref digest and rejects a mismatch (forgery/mutation), and freezes a fresh canonical snapshot.- Hard bounds enforced:
maxEvidenceRefs,maxExcerptChars,maxPayloadChars,maxInputTokens,MAX_QUERY_CHARS. No session mirror and no automatic capture.
review.ts — strict candidate parser + human-gated LearningService
parseCandidatestreats analyzer (or manual) output as untrusted: unknown fields are rejected, candidate count is bounded,title/guidance/applicabilityare length-bounded, every text field is scanned (injection/ hidden-Unicode rejected, secrets redacted), and each evidence reference must bind to the reviewed payload by canonical equality (which also locks its digest and derived trust). The candidate'spayloadDigestmust equal the reviewed payload digest.LearningService.createCandidateandrevisedo not accept an arbitraryRecordContent. They accept only a parsed candidate bound to an exactReviewPayloaddigest and construct theRecordContentinternally, taking project/worktree scope and evidence from the payload. Safety and evidence binding are revalidated at persistence time, so a tampered or unbound candidate is rejected.- Lifecycle: candidate -> human exact-current-digest approval -> separate
human activation -> human revision (invalidates approval) / retirement.
There is no candidate -> active path: the domain builders hardcode the
humanactor for every transition after proposal. Runtime human presence is enforced at the trusted interactive command boundary; lower-level service APIs remain internal package implementation. No model or network calls.
retrieval.ts — explicit, bounded, project-scoped retrieval
- Query-driven only: an explicit non-empty query is required; there is no automatic retrieval or injection.
- Considers only
activerecords whose activation enabled retrieval, within a strict project/worktree scope (project-wide records match any worktree; worktree-scoped records match only their worktree). - Unicode-aware lexical relevance (no embeddings): query terms are segmented
with
Intl.Segmenter(word granularity, with a property-escape fallback) and scored against title / applicability / guidance / evidence excerpts with fixed weights. Vietnamese, CJK, and Korean matching work. Zero-score records are excluded. - Active content — including evidence excerpts — is scanned before output and unsafe records are dropped.
- Absolute hard ceilings that config cannot exceed: 3 items, 400 serialized
chars/item, 1200 serialized chars aggregate, counted on the actual canonical
serialization of each item and the items array. Each item returns one bounded
textfield plus provenance (id, content digest, score, evidence sources).
identity.ts / command.ts / index.ts — one /learn command
identity.tswalks parent directories (read-onlynode:fs, no spawning) to the nearest.gitroot and derives an opaquesha256:project key from the sanitized remote identity when available (credentials stripped, scheme/port dropped), else the hashed canonical root. A separate opaque worktree key is derived inside a linked git worktree (.gitfile +commondir). Raw paths and credential-bearing remote URLs never leave the module. The store, evidence payloads, and retrieval all share this one identity, so running/learnfrom a subdirectory finds the same state as the project root.command.tsowns the full command surface —help,status,enable,disable,show,recall,review,approve,activate,retire,purge— over a structural context (no SDK import).status/help/show/recallare non-creating reads; every mutation (enable/disable/review/approve/activate/retire/purge) requireshasUI+ a trusted project + explicit confirmation.reviewonly creates a candidate (never approves or activates in the same flow);approvepreviews the exact current contentDigest;activateis a separate confirmation acting only on approved records;retireis final;purgeuses high-friction confirmation (type "purge" + confirm).showandrecalluse a typed learning-record capability and are refused in an untrusted checkout. The phase5 signal inbox is a separate append-only security quarantine: it may persist a rejected request for audit, but never mutates, activates, or injects a learning record.- Pi 0.81.1 has no editor API, so the review flow notifies the exact
payload.bytesverbatim plus the digest, then confirms that digest — a stronger binding than an editor round-trip (no mutated-bytes state to reconcile). Evidence source stayshuman; the user cannot self-attest a higher trust level. index.tsis a tiny SDK adapter: oneregisterCommand("learn"), no tools/hooks/model/network/system-prompt/timers/spawn.notifyworks in noninteractive contexts (read-only subcommands produce output without UI); onlyconfirm/inputrequirehasUI. A top-level error boundary turns invalid/quarantined input or a corrupt store into a concise error notification rather than an uncaught command crash.
What this slice does not do
- No model, provider, network, tool, system-prompt mutation, or
background-scheduler calls in any of these modules or
index.ts(static + runtime asserted by tests). - No embeddings / semantic search; retrieval is lexical only.
- No automatic session or transcript capture; evidence is manual and allowlisted.
- No age-based retention schedule. Local records remain until explicit retirement
or confirmed
/learn purge. The store may compact bounded event history into a schema-v2ledger_checkpoint; compaction preserves the replay projection and exact historical event-id membership, and never silently discards a record. Unsupported retention config is rejected.
Ledger integrity and compaction
Ordinary lifecycle events retain the on-disk schema v1. A compacted generation
uses an explicitly versioned schema v2 checkpoint, binds to the prior chain
tail, and carries the state snapshot plus the event ids already committed. The
store verifies both the event log and its sidecar chain before compaction. A
durable intent file makes interrupted compaction and purge recover forward under
the append lock; lock-free snapshots fail closed while files are between
generations. Event lines, records, chain entries, and recovery markers have hard
byte/count limits. Threshold-triggered compaction is best-effort maintenance:
once an event append is fsynced, a checkpoint-size or maintenance failure cannot
retroactively report that append as rejected. LearningStore.compact() remains
the explicit API when a caller needs the compaction result or error.
Config defaults
Defaults below apply to a Pi-trusted project with no configuration of its own.
An untrusted project is forced to enabled: false, mode: "manual" with every
automation switch off and learning-record access none, regardless of what its
.pi/settings.json or persisted config says. The separate phase5
securityQuarantine capability remains append-only so rejected external input
can be retained as audit evidence; it is not persisted learning state.
| Field | Default | Note |
|---|---|---|
enabled |
true |
automation is on for trusted projects |
mode |
auto-safe |
manual and assist are also accepted; automatic is rejected |
scope |
project |
global is rejected |
capture |
manual |
automatic is rejected. Auto-safe capture runs through mode, not this field |
autoActivate / autoInject |
true |
only legal while mode is auto-safe (autoInject also in assist); forced false otherwise |
autoActivation.allowedKinds |
["pattern", "discovery"] |
the only kinds auto-safe may act on |
autoActivation.minEvidenceTrust |
verified-command |
every evidence ref must meet this |
retrieval |
explicit, 3 / 400 / 1200 |
maxItems / maxItemChars / maxTotalChars |
allowGlobalScope / autonomousEdits |
false |
setting either to true fails closed |
approval.requireHuman / approval.separateActivation |
true |
invariant of the approved/active states; false fails closed |
Config is parsed strictly and fails closed on unknown schema versions, unknown fields, or unsupported unsafe values — and a failed parse disables automation rather than falling back to these defaults.
Where configuration comes from
Highest precedence first:
.pi/settings.json→pi-learning(project-owned override).pi/artifacts/learning/v1/config.json(written by/learn enable|disable)- The package defaults above — trusted projects only
One resolver (resolveLearningConfig) serves both the runtime and the /learn
command, and /learn status names the source it used.
Requirements
- Pi
@earendil-works/pi-coding-agent0.81.x (optional peer; tested against 0.81.1 and provided by Pi at load time). The package imports only types from it. - Node >= 22.19.0.
Development
npm install
npm run typecheck # tsc --noEmit
npm test # vitest run
npm run build # tsc -p tsconfig.build.json -> dist/
npm pack --dry-run # inspect the published file list
License
MIT. The upstream MIT license (Copyright (c) 2026 Matt Devy) is preserved verbatim in
LICENSE.