minhduydev-pi-learning

内容来源:README.md(说明文档) · 原始地址 · 查看安装指南

原始内容

@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-active states and are labelled auto-safe everywhere 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 through show / 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-core moved to ^0.3.0. See CHANGELOG.md.

What it does (and does not) do

  • Two gates, never conflated. The manual /learn review -> approve -> activate flow produces approved / active. Auto-safe produces auto-approved / auto-active. The replay physically cannot put a system actor into a human-gated state, and the record fields are separate (approval/activation vs autoApproval/autoActivation).
  • Auto-safe is bounded. It only acts on the conservative kinds pattern and discovery, only when every evidence reference meets minEvidenceTrust (verified-command by 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: false context message.
  • Project-local. Learnings live under .pi/artifacts/learning/v1/ in the current project. There is no global/home store.
  • Append-only, recoverable. events.jsonl is the source of truth; index.json is 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 none or read-write, separately from automation, plus an explicit append-only securityQuarantine capability. 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 -> active bypass. Human approval and activation are separate, explicit actions, and only actor: "human" events reach approved / active.
  • The auto-safe gate is a parallel path, not a shortcut through the human one. It requires actor: "system" and mode: "auto-safe" on the event; either alone is rejected as authorized-actor-required.
  • /learn approve <id> on an auto-approved / auto-active record promotes it: the machine gate is cleared, your approval is bound to the exact content digest, and the record drops to approved until you activate it.
  • Retrieval serves both active and auto-active, and every result carries a trust: "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, optional session/task/worktree ids, and a trust classification. 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.tsSafetyScanner 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=value credential 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.

  • observedAt must be a real ISO-8601 timestamp with a timezone.
  • EvidenceInput has no trust field: trust is derived from the declared source and bound into the evidence digest. The v1 command accepts only human evidence; future adapters must authenticate stronger source classes.
  • locator / sessionId / taskId / worktreeId are 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.bytes is the canonical encoding binding projectKey, an optional worktreeKey, a non-empty bounded query, 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.
  • buildReviewPayload does not trust passed EvidenceBuildResult objects: 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

  • parseCandidates treats analyzer (or manual) output as untrusted: unknown fields are rejected, candidate count is bounded, title/guidance/ applicability are 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's payloadDigest must equal the reviewed payload digest.
  • LearningService.createCandidate and revise do not accept an arbitrary RecordContent. They accept only a parsed candidate bound to an exact ReviewPayload digest and construct the RecordContent internally, 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 human actor 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 active records 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 text field plus provenance (id, content digest, score, evidence sources).

identity.ts / command.ts / index.ts — one /learn command

  • identity.ts walks parent directories (read-only node:fs, no spawning) to the nearest .git root and derives an opaque sha256: 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 (.git file + 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 /learn from a subdirectory finds the same state as the project root.
  • command.ts owns 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/ recall are non-creating reads; every mutation (enable/disable/review/ approve/activate/retire/purge) requires hasUI + a trusted project + explicit confirmation. review only creates a candidate (never approves or activates in the same flow); approve previews the exact current contentDigest; activate is a separate confirmation acting only on approved records; retire is final; purge uses high-friction confirmation (type "purge" + confirm).
  • show and recall use 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.bytes verbatim plus the digest, then confirms that digest — a stronger binding than an editor round-trip (no mutated-bytes state to reconcile). Evidence source stays human; the user cannot self-attest a higher trust level.
  • index.ts is a tiny SDK adapter: one registerCommand("learn"), no tools/hooks/model/network/system-prompt/timers/spawn. notify works in noninteractive contexts (read-only subcommands produce output without UI); only confirm/input require hasUI. 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-v2 ledger_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:

  1. .pi/settings.jsonpi-learning (project-owned override)
  2. .pi/artifacts/learning/v1/config.json (written by /learn enable|disable)
  3. 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-agent 0.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.