---
name: LSEL Curator
slug: lsel-curator
category: AI Engineering
description: LSEL Curator drains the .moai/lessons-inbox.jsonl, filters Bash timeout/sandbox noise, clusters remaining failures by event key, and stages high-signal candidates for the Local Self-Evolution Loop. Use it to turn raw tool failure stubs into curated proposal candidates.
github: "https://github.com/modu-ai/moai-adk/tree/main/.claude/skills/hns-lsel-curator"
language: Go
stars: 1176
forks: 220
install: "npx degit https://github.com/modu-ai/moai-adk/tree/main/.claude/skills/hns-lsel-curator ~/.claude/skills/hns-lsel-curator"
installs_to: ~/.claude/skills/hns-lsel-curator
source_path: .claude/skills/hns-lsel-curator/SKILL.md
collection_size: 25
category_size: 2451
collection_url: "https://dirskills.com/collections/modu-ai/moai-adk"
added: 2026-08-20T07:55:47.333Z
last_synced: 2026-08-20T07:55:47.333Z
canonical_url: "https://dirskills.com/skills/lsel-curator"
---

# LSEL Curator

LSEL Curator drains the .moai/lessons-inbox.jsonl, filters Bash timeout/sandbox noise, clusters remaining failures by event key, and stages high-signal candidates for the Local Self-Evolution Loop. Use it to turn raw tool failure stubs into curated proposal candidates.

**Install:**

```bash
npx degit https://github.com/modu-ai/moai-adk/tree/main/.claude/skills/hns-lsel-curator ~/.claude/skills/hns-lsel-curator
```

## README

# hns-lsel-curator — LSEL CLUSTER + drain engine

> **Namespace:** `hns-lsel-*` is user-owned dogfood (CLAUDE.local.md §24). This skill is
> NOT mirrored into `internal/template/templates/` — it lives only in this repo. Graduation
> to `moai-lsel-*` + 16-language distribution is a separate SPEC (out of scope per spec.md §G).
>
> **M1 scope:** drain + cluster + stage candidates. NO APPROVE, NO APPLY (M3).
> **M2 scope:** drain + cluster + **PROPOSE shadow** (no APPROVE, no APPLY). The PROPOSE stage
> emits shadow proposals + self-critiques; APPROVE/APPLY land in M3 via the fresh
> `hns-lsel-applier` path. M2 does NOT write to `memory/` — the first `feedback_*.md` topic
> file is an M3+ deliverable after APPROVE.

## What this skill does

The MoAI-ADK repo accumulates tool-failure stubs in `.moai/lessons-inbox.jsonl` (624 stubs at
M1 start, re-measured — a moving target). The constitution names the orchestrator as the drain
actor, but until this skill there was **zero mechanical drain code** — the drain existed only as
a doctrine paragraph (`moai-constitution.md:147`). This skill closes that gap in user-owned
surfaces, without touching the frozen Go applier (`internal/harness/applier.go:22` —
its write-flag stays `false`; REQ-LSEL-003: bypass, never unfreeze).

The drain is split into a **mechanical core** (`drain.sh`, deterministic, testable) and a
**model-mediated layer** (this SKILL.md + your judgment, invoked for M2+ importance refinement
and proposal drafting).

## The mechanical core — `drain.sh`

`drain.sh` is a portable bash + jq script that lives next to this SKILL.md. It performs the
deterministic half of the drain:

```
drain.sh --inbox <path-to-lessons-inbox.jsonl> --state-dir <path-to-lsel-state>
```

Pipeline (REQ-LSEL-009 + AC-LSEL-009 / AC-LSEL-010):

1. **Companion offset** — read `<state-dir>/drain-offset.json` (seed `{"offset":0}` if absent).
   The inbox is append-only and is NEVER mutated; the offset marks consumed stubs
   (SPEC-HARNESS-RATCHET-REWIRE-001 D3 companion-offset pattern).
2. **Slice** — read stubs from the offset onwards (`tail -n +<offset+1>`).
3. **Drain-side severity filter** (AC-LSEL-010) — discard noise BEFORE clustering:
   - `tool_failure:Bash:UnknownFailure` — the opaque ~65% timeout/sandbox bucket (the dominant
     noise share; report §2).
   - `tool_failure:Bash:SandboxViolation` — environment constraint, not a code defect.
   - any `*:TimeoutError` (Bash + MCP timeouts).
   The filter is drain-side because `internal/hook/failure_observer.go` (the inbox writer) is
   OUTSIDE the six loop-writable surfaces (plan.md §F.1 [DECISION RESOLVED]), so the loop cannot
   edit the writer — it filters on read instead.
4. **Cluster** by `event_key` with frequency count, first/last seen, and up to 3 sample summaries.
5. **Singleton gate** — discard clusters with `frequency < 2` (single-occurrence noise per the
   constitution Lessons Protocol drain paragraph).
6. **Importance** — score each survivor with a Generative-Agents-style 1-10 gate:
   `importance = min(10, frequency)` (frequency as proxy; the model augments this in M2+ with a
   severity hint and retrieval-weighted judgment).
7. **Emit** candidates to `<state-dir>/clusters.json`; advance the companion offset.

### `clusters.json` schema

```json
{
  "drained_at": "2026-08-04T08:41:00Z",
  "offset_before": 0,
  "offset_after": 624,
  "total_read": 624,
  "noise_discarded": 533,
  "singletons_discarded": 4,
  "candidates": [
    {
      "event_key": "tool_failure:Agent:UnknownFailure",
      "frequency": 41,
      "first_seen": "...",
      "last_seen": "...",
      "sample_summaries": ["...", "...", "..."],
      "source": "tool:Agent",
      "importance": 10
    }
  ]
}
```

### Empty-delta no-op

If the inbox has not grown past the offset, `drain.sh` writes an empty-candidate `clusters.json`
and leaves the offset unchanged. Not a failure (acceptance.md §E edge case).

## The model-mediated layer (you, when invoked)

`drain.sh` produces the deterministic candidate set. When this skill is invoked for a real
curation pass (M2+), your job on top of the mechanical output is:

- **Read `clusters.json`** and rank candidates by `importance` then `frequency`.
- **Augment importance** with a severity hint the mechanical core cannot see: a recurring
  `Bash:ExitError` cluster points at a real command-shape defect (high signal); a recurring
  `Agent:ContextCancelled` cluster may be session-teardown noise (lower signal). Record the
  rationale in the candidate's prose when you draft the M2 proposal — do NOT rewrite
  `clusters.json` (it is the mechanical artifact; your augmentation lives in the proposal).
- **Do NOT write to `memory/` in M1.** Candidates stage in `clusters.json` only. The first
  `feedback_*.md` topic file is produced by the M2 PROPOSE stage after retrieval-before-propose
  and self-critique (REQ-LSEL-010).

## What this skill does NOT do (M1 boundaries)

- **No APPROVE / APPLY** — the parallel user-owned applier (`hns-lsel-applier`) is M3.
- **No edits to frozen doctrine** — `.claude/rules/moai/**`, `CLAUDE.md`,
  `internal/template/templates/**`, retained agents, `moai-*` skills, and the frozen Go
  applier / `curator_dispatch.go` are all byte-for-byte untouched (REQ-LSEL-001 / §B.3).
- **No new `.moai/config/sections/` file** — loop state lives under `.moai/state/lsel/`
  (a new section file would be wiped on `moai update`; plan.md §B.4 / AP-LSEL-005).
- **No orchestrator-only synchronous user-question channel** — this is a subagent-owned
  mechanism skill; it never invokes the orchestrator's user gate. On a missing input,
  return a structured blocker report; the orchestrator runs the user gate (CLAUDE.md §8).

## Verification (run before declaring a drain complete)

```bash
# 1. The drain mechanics (fixture-based characterization test — AC-LSEL-009/010):
.claude/skills/hns-lsel-curator/drain_test.sh

# 2. A real drain of the live backlog (re-measure the count first — it is a moving target):
LIVE_COUNT=$(wc -l < .moai/lessons-inbox.jsonl | tr -d ' ')
.claude/skills/hns-lsel-curator/drain.sh --inbox .moai/lessons-inbox.jsonl --state-dir .moai/state/lsel
jq '.offset_after == ($LIVE_COUNT|tonumber) and (.candidates | length) >= 1' .moai/state/lsel/clusters.json

# 3. M1 invariant — zero memory/ writes from the drain:
find memory -newer <drain-start-timestamp> -name 'feedback_*' 2>/dev/null | wc -l   # must be 0
```

## Characterization test

`drain_test.sh` (next to this SKILL.md) is the TDD RED→GREEN harness. It builds a synthetic
inbox with known noise + signal stubs, runs `drain.sh`, and asserts the drain semantics:
noise excluded pre-cluster, signal clustered with correct frequencies, singletons discarded,
offset advanced, candidates emitted, zero `memory/` writes, idempotent re-drain. Run it after
any edit to `drain.sh`.

## Cross-references

- **SPEC:** `.moai/specs/SPEC-LSEL-LOCAL-EVOLUTION-001/{spec,plan,acceptance,progress}.md`
- **Design report (SSOT):** `.moai/reports/moai-local-self-evolution-design-20260804.html`
  §6 stage 2 (CLUSTER), §10 P1, §11 mustFix B#1/B#3.
- **Frozen applier (reference only):** `internal/harness/applier.go:22`
  (the write-flag, kept `false`), `internal/harness/curator_dispatch.go`.
- **Constitution drain paragraph (the "0 Go code" stub this skill replaces):**
  `.claude/rules/moai/core/moai-constitution.md:147`.
- **Namespace guard:** `internal/template/split_namespace_test.go`,
  `internal/template/internal_content_leak_test.go` (extended in M2 — AC-LSEL-006).

---

## PROPOSE stage (M2 — shadow proposals)

The PROPOSE stage consumes the M1 candidate clusters in `.moai/state/lsel/clusters.json` and
emits **shadow proposals** — one per candidate worth acting on — at
`.moai/state/lsel/proposals/<proposal-id>/`. M2 proposals are SHADOW only: no APPROVE, no APPLY.
APPROVE/APPLY land in M3 via the fresh `hns-lsel-applier` path (NOT via the dead
`moai-harness-learner` Tier-4 flow — see "Tier-4 finding" below).

### Retrieval-before-propose (Reflexion)

BEFORE drafting a proposal, retrieve relevant `feedback_*.md` topic files from
`~/.claude/projects/<hash>/memory/`. The retrieval grounds the proposal in prior
lessons (Reflexion-style) and is evidenced in the proposal's `retrieval_evidence`
block. A proposal without retrieval evidence is malformed and MUST NOT be emitted.

### Proposal payload schema (AC-LSEL-011)

Each proposal lives at `.moai/state/lsel/proposals/<id>/` and contains exactly:

| File | Purpose |
|------|---------|
| `proposal.md` | YAML-frontmatter payload + prose body |
| `diff.patch` | The proposed edit (unified diff; NOT applied in M2) |
| `self-critique.md` | Model-performed critique against frozen doctrine |

`proposal.md` YAML frontmatter carries the full schema (8 required keys):

```yaml
---
proposal_id: lsel-001
target_surface: <one of the 6 evolvable surfaces, spec.md §B.3>
rationale: |
  <what + why>
WHY-not-just-WHAT: |
  <the reasoning, not just the change — catches "what" proposals that skip the "why">
prediction: <a FALSIFIABLE expected effect — the verify_command must be able to falsify it>
verify_command: <a runnable command that, if green, confirms the prediction>
blast_radius: <which surfaces the diff touches; used by the CSA forced-gate match>
memory_type: semantic|procedural|episodic   # CoALA taxonomy
retrieval_evidence:
  - <path to a feedback_*.md retrieved before drafting>
status: blocked   # blocked | ready — blocked if self-critique has an UNRESOLVED objection
---
```

### Self-critique gate

`self-critique.md` is model-performed (NOT a mechanical doctrine checker — report §13 caveat 3:
the model can rationalize; the frozen allowlist + `/moai gate` are the real safety floor). It
lists objections against frozen doctrine; each objection is marked RESOLVED or UNRESOLVED. A
proposal with ANY UNRESOLVED objection is `status: blocked` and MUST NOT proceed to APPROVE.
A proposal that never converges stays blocked; the curator returns a blocker report and the
orchestrator surfaces it (acceptance.md §E edge case — not a ship-blocker for M2; it proves
the gate fires).

### Tier-4 finding (AC-LSEL-012 — do NOT wire the dead flow)

**Finding (verified 2026-08-04 via `tier4_firing_test.sh`): the `moai-harness-learner` Tier-4
synchronous-user-question flow is DEAD at the production invocation layer.** The CLI (`moai harness apply`)
prints a stub string and never invokes the learner skill; `CuratorDispatch` has 0 production
callers (the audit's cautionary precedent); the frozen applier's write-flag (`false`
at `internal/harness/applier.go:22`) is the apply dead-switch; and NO mechanical trigger causes the orchestrator to surface a Tier-4
proposal (the audit's exact failure mode, report §11 mustFix B#1).

Per acceptance.md §E edge case, M2 does NOT wire the PROPOSE→APPROVE handoff to depend on the
Tier-4 flow. APPROVE routes via the M3 fresh path (`hns-lsel-applier` + `decision.json` with a
synchronous-approval marker). M2 emits shadow proposals only. This finding is recorded in
`tier4_firing_test.sh` and cited in the M2 wiring commit.

## CSA forced-gate categories (AC-LSEL-005 / REQ-LSEL-005)

The APPROVE stage (M3, `hns-lsel-applier`) forces a synchronous user-question gate
(orchestrator-run) — regardless of proposer confidence — for any proposal whose blast
radius touches one of the SIX CSA
forced-gate categories:

1. **INVARANTS kernel** — the read-only goal kernel block at the top of `CLAUDE.local.md`.
2. **security/validation exception** bands — input-validation carve-outs, error-handling that
   prevents data loss, OWASP measures.
3. **HIGH-fan-in references** — `@MX:ANCHOR` functions with fan_in ≥ 3 callers.
4. **Bash risk path** — the destructive-primitive set + `BASH_SUBCOMMAND_SOFT_CAP` compound
   commands (coding-standards.md § Bash Risk-Amplifier Doctrine).
5. **`permissions.allow`** additions — explicit security-exception band; per-line synchronous
   approval (every added allow entry is its own forced gate).
6. **execution-meta files** — the four execution-meta categories named in REQ-LSEL-002/005:
   (i) the frozen allowlist meta file at `.claude/lsel/frozen-allowlist.json`, (ii) an applier
   or curator skill body (`hns-lsel-applier/`, `hns-lsel-curator/`), (iii) the apply hook
   script (`lsel-apply.sh` and wrappers), (iv) the `settings.local.json` hook-registration
   subblock.

**Bother-cost-exemption:** forced gates are **bother-cost-exempt** — the bother-cost gating
rule applies ONLY to routine-tier proposals. A forced-gate proposal always triggers a
synchronous user-question gate (orchestrator-run) regardless of bother-cost state.

**Mechanical enforcement (D3):** the applier (`hns-lsel-applier` driving `lsel-apply.sh`,
M3) intercepts every proposal matching the four execution-meta categories and REFUSES to
write unless the proposal's `decision.json` carries an explicit synchronous-approval marker
(an approval artifact produced by the orchestrator's synchronous user-question gate).
A match with no marker aborts the apply,
appends a rejection row to `.moai/logs/lsel-reject.log` naming the matched category, and
writes nothing. Proposals matching none of the four categories proceed through the routine
bother-cost path. This mechanical interception is what makes the self-amending-handcuffs
defense defensible without resting on the regex paradox alone.

`csa_refusal_test.sh` (next to this SKILL.md) is the fixture test for the refusal rule.

---

## REFLECTION stage (M4 — REQ-LSEL-014 / AC-LSEL-016)

The periodic consolidation pass that prevents un-refined accumulation — the
dominant failure mode the design report §10 P4 names: "no consolidation / decay /
pruning → wrong-lesson retrieval". Without REFLECTION, concrete topic files pile
up and retrieval surfaces stale concrete incidents instead of the principle they
collectively support.

### Threshold-fired, not wall-clock-fired

REFLECTION fires when the **accumulated importance** of concrete `feedback_*.md`
topic files clears the threshold (default ~150), NOT on a monthly cron. This is
the Vectorize 4-lever model (importance-gate / merge / decay / evict) the design
report §10 P4 cites: importance is assigned write-time, and the reflection
threshold is an accumulation signal, not a calendar one. A single-topic cohort
below the threshold is a clean no-op (acceptance.md §E edge case).

### The mechanical core — `reflect.sh`

`reflect.sh --memory-dir <m> [--threshold 150] [--min-topics 3]`:

1. Reads the active `feedback_*.md` topic files (maxdepth 1 — never the
   `_archive/` cold tier).
2. Sums their frontmatter `importance`. If `count < min-topics` OR `sum <
   threshold` → clean no-op (exit 0).
3. Synthesizes ONE `feedback_*_principle_*.md` carrying:
   - a `memory_type` label (CoALA taxonomy — `semantic` for a feedback principle;
     `procedural` would route to a `hns-*` skill body instead).
   - the shared theme drawn from the source descriptions (the retrieval cue).
   - `source_count` + `synthesized_at` for the audit trail.
4. **Moves** the originals to `memory/_archive/` (cold tier) — **NEVER deleted**
   (report §10 P4: "축출 ≠ 보관 — 보관은 성능용, 하드 삭제는 규정 준수용; MoAI의
   '삭제 말고 보관' 규칙이 옳음이 입증된다" — archive preserves the audit trail).

### Decay-weighted retrieval

The originals relocate to `_archive/`, so the active recall set (the
`memory/` directory the recall layer scans first) holds the synthesized
principle, NOT the stale concrete originals. A retrieval probe for a related cue
returns the principle ranked ABOVE the archived originals — this is the
decay-weighted retrieval AC-LSEL-016 clause requires. The principle's
`description` is crafted to match the shared cue; the archived originals stay
discoverable (cold tier) but no longer dominate the top of the recall set.

### Model-mediated layer (you, when invoked)

`reflect.sh` performs the mechanical synthesis (deterministic). When this skill
runs a real reflection pass, your job on top is:
- **Read the synthesized principle** and refine its prose into a genuine
  abstract statement (the mechanical core aggregates descriptions; you write the
  actual principle).
- **Confirm the memory_type** — if the consolidated knowledge is procedural (a
  how-to that belongs in a `hns-*` skill body), stamp `memory_type: procedural`
  and route the synthesis to the skill rather than leaving it as a `feedback_*`.
- **Do NOT delete the archive** — originals in `_archive/` are the audit trail.
  If the hot tier (active `feedback_*.md`) approaches the 50-file cap, prefer
  archiving more concrete topics over deleting them (moai-memory.md § Memory
  Hygiene).

### Verification (run before declaring a reflection pass complete)

```bash
# M4 REFLECTION characterization test (AC-LSEL-016) — hermetic temp memory dir:
.claude/skills/hns-lsel-curator/reflect_test.sh

# cold-tier growth vs hot-tier (post-M4 audit, acceptance.md §H):
ls memory/_archive/ | wc -l   # archived originals
ls memory/feedback_*.md | wc -l   # active hot tier
```
