| name | hns-lsel-curator |
| description | Local Self-Evolution Loop (LSEL) curator — the CLUSTER + drain engine for the GOOS-local PROPOSE→APPLY seam closure (SPEC-LSEL-LOCAL-EVOLUTION-001). Companion-offset drain of .moai/lessons-inbox.jsonl with a drain-side severity filter that drops the ~65% Bash-timeout/sandbox noise, event_key clustering with a frequency gate, and a Generative-Agents-style 1-10 importance score. Candidates stage at .moai/state/lsel/clusters.json. M1 = drain only (NO PROPOSE, NO APPLY, NO memory/ writes).
|
| allowed-tools | Read, Grep, Glob, Bash |
| user-invocable | false |
| metadata | {"version":"0.1.0","category":"harness","status":"active","updated":"2026-08-04","tags":"lsel,self-evolution,drain,cluster,harness,dogfood"} |
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):
- 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).
- Slice — read stubs from the offset onwards (
tail -n +<offset+1>).
- 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.
- Cluster by
event_key with frequency count, first/last seen, and up to 3 sample summaries.
- Singleton gate — discard clusters with
frequency < 2 (single-occurrence noise per the
constitution Lessons Protocol drain paragraph).
- 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).
- Emit candidates to
<state-dir>/clusters.json; advance the companion offset.
clusters.json schema
{
"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"
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)
.claude/skills/hns-lsel-curator/drain_test.sh
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
find memory -newer <drain-start-timestamp> -name 'feedback_*' 2>/dev/null | wc -l
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):
---
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
retrieval_evidence:
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:
- INVARANTS kernel — the read-only goal kernel block at the top of
CLAUDE.local.md.
- security/validation exception bands — input-validation carve-outs, error-handling that
prevents data loss, OWASP measures.
- HIGH-fan-in references —
@MX:ANCHOR functions with fan_in ≥ 3 callers.
- Bash risk path — the destructive-primitive set +
BASH_SUBCOMMAND_SOFT_CAP compound
commands (coding-standards.md § Bash Risk-Amplifier Doctrine).
permissions.allow additions — explicit security-exception band; per-line synchronous
approval (every added allow entry is its own forced gate).
- 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]:
- Reads the active
feedback_*.md topic files (maxdepth 1 — never the
_archive/ cold tier).
- Sums their frontmatter
importance. If count < min-topics OR sum < threshold → clean no-op (exit 0).
- 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.
- 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)
.claude/skills/hns-lsel-curator/reflect_test.sh
ls memory/_archive/ | wc -l
ls memory/feedback_*.md | wc -l