In v0.2.x, 47 of 47 Pi-extracted lessons had applied_count = 0. Why?
Pi (the coach LLM) routinely invented skill ids that don't exist — relocate.surface, choose.safe.surface, survive.shelter, gather.visible_log, tunnel-out (without recovery. prefix). The
dispatcher's normalisePreferSkill() had a fuzzy mapper but no live
registry to validate against, so these dead lessons sat in the DB
producing nothing.
This rc closes that loop at two boundaries:
Write-time: Pi prompts (coach/postmortem.js, coach/reflect.js)
now include the live skill registry as a system-prompt block with a
"USE ONLY THESE, never invent" instruction. On insert, any prefer_skill / avoid_skill that isn't a real id or a known mode
name gets dropped (warn-logged for visibility).
Read-time: normalisePreferSkill() now returns null for
unknown ids. Anything that slipped through write-time validation
still can't dispatch a nonexistent skill.
It also lays the LLM substrate for the rest of v0.3.0
(needs-ladder integration in rc.2, awareness-driven advisor calls in rc.3).
Changes
runtime/skill-registry.js (new) — single source of truth wrapping skills/index.js. Exports listSkillIds, isRegistered, skillRegistryPrompt. No circular import risk.
runtime/llm/provider.js (new) — OpenAI-compatible chat client.
Env-driven: PEPA_FAST_LLM_{BASE_URL,API_KEY,MODEL,TIMEOUT_MS}. Safe
no-op when API_KEY is unset (so this PR is shippable disabled and
flipped on later with one env var). Supports JSON-mode.
runtime/coach/fast-advisor.js (new) — tactical advisor tier. advise() returns {action: 'switch_skill'|'continue'|'wait', skillId?, rationale}. Not auto-triggered in rc.1 — scaffold only,
wired into the reflex in rc.3.
runtime/coach/advice.js — normalisePreferSkill() hardened to
reject anything not in registry/mode-map. Warn line when a
hallucinated id is dropped.
runtime/coach/postmortem.js, runtime/coach/reflect.js — embed skillRegistryPrompt() in their Pi prompts. Filter Pi's response at
write time.
Configuration (when ready to flip on)
# TimeWeb (or any OpenAI-compatible endpoint)exportPEPA_FAST_LLM_BASE_URL="https://<endpoint>/v1"exportPEPA_FAST_LLM_API_KEY="<key>"exportPEPA_FAST_LLM_MODEL="gpt-5-mini"# or whatever
Without the env vars, fast-advisor is a no-op and the bot runs
exactly as before — just with hallucinated lessons properly rejected.
Test plan
npm test — 279 green (was 257 on rc.3, +24 new):
5 tests for skill-registry
9 tests for llm/provider (incl. stubbed fetch + 401 path)
10 tests for fast-advisor (incl. rejection of hallucinated skill)
After merge: deploy and watch SELECT source, COUNT(*), SUM(applied_count > 0) FROM lessons GROUP BY source
— Pi-coach / Pi-reflect applied count should start growing.
rc.3: Event-driven awareness layer + AbortSignal plumbing into
long-running skills, so the bot reacts to forced moves / HP plunges /
hostile spawns within ~100ms instead of waiting for the next dispatch
tick. Awareness layer is what auto-triggers fast-advisor.advise().
## What and why
In v0.2.x, **47 of 47 Pi-extracted lessons** had `applied_count = 0`. Why?
Pi (the coach LLM) routinely invented skill ids that don't exist —
`relocate.surface`, `choose.safe.surface`, `survive.shelter`,
`gather.visible_log`, `tunnel-out` (without `recovery.` prefix). The
dispatcher's `normalisePreferSkill()` had a fuzzy mapper but no live
registry to validate against, so these dead lessons sat in the DB
producing nothing.
This rc closes that loop at two boundaries:
1. **Write-time**: Pi prompts (`coach/postmortem.js`, `coach/reflect.js`)
now include the live skill registry as a system-prompt block with a
"USE ONLY THESE, never invent" instruction. On insert, any
`prefer_skill` / `avoid_skill` that isn't a real id or a known mode
name gets dropped (warn-logged for visibility).
2. **Read-time**: `normalisePreferSkill()` now returns `null` for
unknown ids. Anything that slipped through write-time validation
still can't dispatch a nonexistent skill.
It also lays the **LLM substrate** for the rest of v0.3.0
(needs-ladder integration in rc.2, awareness-driven advisor calls in rc.3).
## Changes
- `runtime/skill-registry.js` (new) — single source of truth wrapping
`skills/index.js`. Exports `listSkillIds`, `isRegistered`,
`skillRegistryPrompt`. No circular import risk.
- `runtime/llm/provider.js` (new) — OpenAI-compatible chat client.
Env-driven: `PEPA_FAST_LLM_{BASE_URL,API_KEY,MODEL,TIMEOUT_MS}`. Safe
no-op when API_KEY is unset (so this PR is shippable disabled and
flipped on later with one env var). Supports JSON-mode.
- `runtime/coach/fast-advisor.js` (new) — tactical advisor tier.
`advise()` returns `{action: 'switch_skill'|'continue'|'wait',
skillId?, rationale}`. **Not auto-triggered in rc.1** — scaffold only,
wired into the reflex in rc.3.
- `runtime/coach/advice.js` — `normalisePreferSkill()` hardened to
reject anything not in registry/mode-map. Warn line when a
hallucinated id is dropped.
- `runtime/coach/postmortem.js`, `runtime/coach/reflect.js` — embed
`skillRegistryPrompt()` in their Pi prompts. Filter Pi's response at
write time.
## Configuration (when ready to flip on)
```bash
# TimeWeb (or any OpenAI-compatible endpoint)
export PEPA_FAST_LLM_BASE_URL="https://<endpoint>/v1"
export PEPA_FAST_LLM_API_KEY="<key>"
export PEPA_FAST_LLM_MODEL="gpt-5-mini" # or whatever
```
Without the env vars, fast-advisor is a no-op and the bot runs
exactly as before — just with hallucinated lessons properly rejected.
## Test plan
- [x] `npm test` — 279 green (was 257 on rc.3, +24 new):
- 5 tests for `skill-registry`
- 9 tests for `llm/provider` (incl. stubbed fetch + 401 path)
- 10 tests for `fast-advisor` (incl. rejection of hallucinated skill)
- `advice.test.js` regression: `normalisePreferSkill("relocate.surface")` → null
- [ ] After merge: deploy and watch
`SELECT source, COUNT(*), SUM(applied_count > 0) FROM lessons GROUP BY source`
— Pi-coach / Pi-reflect `applied` count should start growing.
## Followup (not in this PR)
- rc.2: `runtime/manifesto/needs.js` (L0-L10 Maslow ladder) + curriculum
integration. Bot pursues concrete intermediate goals (tools_wood →
shelter_basic → tools_stone → ...) instead of "explore further".
- rc.3: Event-driven awareness layer + `AbortSignal` plumbing into
long-running skills, so the bot reacts to forced moves / HP plunges /
hostile spawns within ~100ms instead of waiting for the next dispatch
tick. Awareness layer is what auto-triggers `fast-advisor.advise()`.
See [`dev/v0.3.0/PLAN.md`](dev/v0.3.0/PLAN.md) for the full design and
[`dev/v0.3.0/STATUS.md`](dev/v0.3.0/STATUS.md) for shipped/pending.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What and why
In v0.2.x, 47 of 47 Pi-extracted lessons had
applied_count = 0. Why?Pi (the coach LLM) routinely invented skill ids that don't exist —
relocate.surface,choose.safe.surface,survive.shelter,gather.visible_log,tunnel-out(withoutrecovery.prefix). Thedispatcher's
normalisePreferSkill()had a fuzzy mapper but no liveregistry to validate against, so these dead lessons sat in the DB
producing nothing.
This rc closes that loop at two boundaries:
coach/postmortem.js,coach/reflect.js)now include the live skill registry as a system-prompt block with a
"USE ONLY THESE, never invent" instruction. On insert, any
prefer_skill/avoid_skillthat isn't a real id or a known modename gets dropped (warn-logged for visibility).
normalisePreferSkill()now returnsnullforunknown ids. Anything that slipped through write-time validation
still can't dispatch a nonexistent skill.
It also lays the LLM substrate for the rest of v0.3.0
(needs-ladder integration in rc.2, awareness-driven advisor calls in rc.3).
Changes
runtime/skill-registry.js(new) — single source of truth wrappingskills/index.js. ExportslistSkillIds,isRegistered,skillRegistryPrompt. No circular import risk.runtime/llm/provider.js(new) — OpenAI-compatible chat client.Env-driven:
PEPA_FAST_LLM_{BASE_URL,API_KEY,MODEL,TIMEOUT_MS}. Safeno-op when API_KEY is unset (so this PR is shippable disabled and
flipped on later with one env var). Supports JSON-mode.
runtime/coach/fast-advisor.js(new) — tactical advisor tier.advise()returns{action: 'switch_skill'|'continue'|'wait', skillId?, rationale}. Not auto-triggered in rc.1 — scaffold only,wired into the reflex in rc.3.
runtime/coach/advice.js—normalisePreferSkill()hardened toreject anything not in registry/mode-map. Warn line when a
hallucinated id is dropped.
runtime/coach/postmortem.js,runtime/coach/reflect.js— embedskillRegistryPrompt()in their Pi prompts. Filter Pi's response atwrite time.
Configuration (when ready to flip on)
Without the env vars, fast-advisor is a no-op and the bot runs
exactly as before — just with hallucinated lessons properly rejected.
Test plan
npm test— 279 green (was 257 on rc.3, +24 new):skill-registryllm/provider(incl. stubbed fetch + 401 path)fast-advisor(incl. rejection of hallucinated skill)advice.test.jsregression:normalisePreferSkill("relocate.surface")→ nullSELECT source, COUNT(*), SUM(applied_count > 0) FROM lessons GROUP BY source— Pi-coach / Pi-reflect
appliedcount should start growing.Followup (not in this PR)
runtime/manifesto/needs.js(L0-L10 Maslow ladder) + curriculumintegration. Bot pursues concrete intermediate goals (tools_wood →
shelter_basic → tools_stone → ...) instead of "explore further".
AbortSignalplumbing intolong-running skills, so the bot reacts to forced moves / HP plunges /
hostile spawns within ~100ms instead of waiting for the next dispatch
tick. Awareness layer is what auto-triggers
fast-advisor.advise().See
dev/v0.3.0/PLAN.mdfor the full design anddev/v0.3.0/STATUS.mdfor shipped/pending.🤖 Generated with Claude Code
Superseded by single v0.3.0 PR (rolling rc.1+rc.2+rc.3 into one)
Pull request closed