| name | lisa-persist-learning |
| description | a candidate learning (from a… |
Persist Learning
Route ONE candidate learning through the judgment gate and act on the verdict. Candidate: $ARGUMENTS
Most candidates are dropped — that is the gate working, not a failure. Nothing is ever silent (every drop leaves a visible note), and no learning content ever reaches the learnings surface outside a pull request.
Candidate Input
Accept the candidate as JSON or key=value fields:
rule — the proposed learning (must fit the executable contract: ≤240 chars, ≤2 lines)
why — the causal claim
provenance — stable refs (issues/PRs/commits/comments), ≤20
evidence_links — concrete evidence refs available for citation
scope_hint — project | upstream (a hint; the judge decides)
triggering_issue — the issue/work item whose failure produced this candidate (required)
fingerprint — optional; computed below when absent
Phase 0 — Fingerprint (stable dedupe key)
Every marker and branch below keys off one deterministic fingerprint:
fingerprint = "sll4-" + first 12 hex chars of sha1(normalized_rule + "\n" + triggering_issue)
normalized_rule = rule lowercased, all whitespace runs collapsed to single spaces, trimmed
NORM=$(printf '%s' "$RULE" | tr '[:upper:]' '[:lower:]' | tr -s '[:space:]' ' ' | sed 's/^ *//; s/ *$//')
FP="sll4-$(printf '%s\n%s' "$NORM" "$TRIGGERING_ISSUE" | shasum -a 1 | cut -c1-12)"
(Use sha1sum where shasum is unavailable.) The same rule for the same triggering issue always produces the same fingerprint, so re-runs dedupe instead of duplicating comments, PRs, or branches.
Phase 1 — Judge (mandatory, never skipped)
Invoke the learning-judge agent (via the Agent/Task tool with subagent_type: "learning-judge" — the same invoke pattern the gardener uses for the skill-evaluator ladder router) with the full candidate including the fingerprint. It returns a verdict: classification, cited_evidence[], rationale, confidence (durable only), disposition.
Respect the verdict — do not override it. Never re-run the judge hoping for a different answer, and never persist anything the judge did not classify durable-learning.
Phase 2 — Route by disposition
All issue/PR comments below follow the marker-dedupe discipline from lisa-github-write-prd Phase 2, each with its own producer tag: match on the marker, never the title or text; exactly one marker per body; never write a markerless body (it breaks all future dedupe); include the eventual-consistency guard — when the gh search index is stale, also enumerate the bodies directly (gh issue view <n> --json comments --jq '.comments[].body' or gh pr list --json number,body) and grep for the marker before deciding to create.
drop (classification one-off or misunderstanding/spec-gap)
Post one comment on the triggering issue and write nothing — zero bytes — to the learnings surface (not a stub, not a placeholder):
<!-- [lisa-learning-drop] key=<fingerprint> -->
Dropped (<classification> — <plain-language gloss>): <reason>.
The note is one line naming the classification (with its fixed plain-language gloss) and the reason, readable by a non-technical operator. Use exactly these glosses per class:
| Classification | Gloss |
|---|
one-off | a one-time fluke, not a recurring pattern |
misunderstanding/spec-gap | traced to an unclear requirement, not a durable lesson |
lisa-upstream | root cause suspected in Lisa; routed for upstream attribution |
(A lisa-upstream classification never produces a drop note — it routes through the handoff-upstream flow below, whose step-1 note uses this pre-attribution wording because filing only happens after attribution confirms the Lisa surface.) Dedupe before posting: if any comment on the triggering issue already carries [lisa-learning-drop] key=<fingerprint>, do not post again — report the existing note.
handoff-upstream (classification lisa-upstream)
This disposition completes the SLL-5 loop (#1583): on a Lisa-attributed failure the upstream Lisa ticket is filed automatically. Filing lives here — not in lisa-attribute-failure — because that skill is deliberately read-only (doctor delegates to it inside its own read-only contract), while this skill already owns exactly the verdict's side effects and the marker-dedupe discipline. Never persist a local rule for a Lisa-attributed failure; the host project's only durable trace is the brief linking note in step 6.
-
Post the handoff marker on the triggering issue (same one-comment marker dedupe; the marker key is unchanged). The visible line must not claim a filing that has not happened yet — attribution and filing come after this step:
<!-- [lisa-learning-upstream-handoff] key=<fingerprint> -->
Candidate routed for upstream attribution (root cause suspected in Lisa): <reason>.
-
Require a confirmed lisa verdict from lisa-attribute-failure — always. Run the lisa-attribute-failure skill on the failure event before any filing. The judge's cited_evidence seeds the event (implicated files, surface in play, failure class) but never substitutes for the verdict — a path or commit reference alone is not attribution. File only when the skill returns a conclusive lisa verdict that names the Lisa surface with cited evidence. Any other outcome — ambiguous, project, or a verdict that cannot name a concrete Lisa surface — files NOTHING upstream: the candidate stays local and low-confidence, the run summary says attribution was inconclusive, and the step-1 note is resolved with one corrective follow-up comment on the triggering issue (marker-deduped so re-runs never repeat it; the suffix is distinct from the filing-failure marker in step 8 so one outcome never suppresses the other):
<!-- [lisa-learning-upstream-handoff] key=<fingerprint>-inconclusive -->
Attribution was inconclusive — nothing was filed upstream and nothing was persisted locally.
-
Derive the root-cause key from the LISA SURFACE, never the host project or the local issue. Two projects hitting the same Lisa bug MUST collide on the same key — that collision is the design (update, not duplicate):
root-cause-key = <lisa-surface>#<failure-class>
<lisa-surface> — the exact public Lisa-relative path of the surface at fault (e.g. plugins/src/base/skills/lisa-doctor/SKILL.md, typescript/copy-overwrite/.github/workflows/quality.yml). Canonical names, aliases, host paths, and unmanifested paths are prohibited.
persist (classification durable-learning)
Continue to Phase 3.
Phase 3 — Persist via PR (durable-learning only)
No learning content is ever committed without a PR — there is no other write path, and the PR must touch only the learnings surface (any other changed file is a bug).
-
PR dedupe. Search all PRs for the marker [lisa-learning-pr] key=<fingerprint> in the body (gh pr list --state all --search '"<marker>" in:body' --json number,url), with the stale-index guard above. If one exists, reference it and stop — never open a duplicate.
-
Resolve the learnings surface path — never hardcode it. The canonical path is resolveProjectLearningsFile from @codyswann/lisa/learnings: the machine-managed ledger resolved from .lisa.config.json (the learnings.file override, else the default .lisa/PROJECT_LEARNINGS.md — a cold path, never an auto-loaded rules tree):
LEARNINGS_FILE=$(node -e 'import("@codyswann/lisa/learnings").then(async m => { const c = await m.readProjectConfig(process.cwd()); console.log(m.resolveProjectLearningsFile(c)); })')
-
Consolidation check (mandatory before writing). Parse the existing entries (parseLearningsFile from @codyswann/lisa/learnings) and look for entries related to the new rule (same failure class, overlapping topic, or near-duplicate wording). Then write through the executable contract — never hand-edit the markdown:
-
Related entry found → consolidate with the exact versions returned by the parser: persistConsolidatedLearning(projectRoot, entry, { supersede: [{ id: <related id>, fingerprint: <related fingerprint> }], onStaleSupersede: targets => report(targets) }). The writer checks every stamp together inside the lock. If any target is absent or its fingerprint changed, the whole supersede is stale: it removes nothing, safely appends the new fingerprint, and reports the mismatch. Never append a near-duplicate sibling by choice — a sibling is a bug that fails review; the writer's stale append is the deliberate race-safe preservation path.
-
No related entry → append via persistLearningEntry(projectRoot, entry) and state in the PR body why appending was correct.
-
Entry mapping (eight fields): fingerprint = the deterministic Phase 0 fingerprint and initial id = fingerprint; rule/why/provenance from the candidate; first_learned = last_confirmed = today (ISO date; on consolidation keep the superseded entry's earliest ); = the judge's /. On an exact stamped consolidation the writer carries forward the deterministic primary target id while persisting the new fingerprint as the version token. A duplicate fingerprint fails before mutation. The writer re-asserts the entry and token budgets — an over-budget failure means consolidate harder or drop, never truncate by hand.
Rules
- Headless-safe: no interactive prompts; must run identically under an intake cron.
- Never block the build: if judging or persistence fails, report the failure and let the primary flow continue — shipping the triggering issue always outranks recording a learning about it.
- No learning loops about learning: never feed this skill a candidate whose triggering artifact is itself learning machinery (a
[lisa-learning-*]-marked comment, learning PR, or handoff); the judge's Step 0 guard backstops this.
- Idempotent: re-running with the same candidate posts no duplicate comment, opens no duplicate PR, and writes no duplicate entry — the fingerprint and markers guarantee it.
- One write path: the learnings surface changes only through
persistLearningEntry / persistConsolidatedLearning inside a PR. Never hand-edit the file, never commit it to the default branch directly.
- Never resolve a ledger conflict by taking one side. Concurrent passes each work on their own
learning/<fingerprint> branch, so two PRs routinely touch the same JSONL block. The lisa-learnings union merge driver reconciles them by entry id, but when it cannot it exits non-zero without writing conflict markers — leaving a clean-looking file that holds OUR SIDE ONLY. git add-ing it, or running git checkout --ours, silently deletes the other branch's learnings. Re-union both sides instead: keep every distinct entry id, honour supersessions from either side, then re-run lisa check-learnings-budget. Literal conflict markers in the ledger mean the driver was not registered — run lisa install-merge-driver . and redo the merge.