Files
pepa-pi-bot/runtime/knowledge/schema.sql
T
7d96e44804 v0.3.0: Maslow + Awareness — self-learning bot with needs ladder, event-driven reflex, and TimeWeb fast advisor (#27)
* v0.3.0-rc.1: live skill registry + fast advisor scaffold

Roots out the v0.2.x failure mode: Pi-extracted lessons routinely named
hallucinated skill ids (relocate.surface, choose.safe.surface,
survive.shelter, gather.visible_log, …). All 47 Pi-lessons in the live DB
had applied_count=0 because normalisePreferSkill couldn't find them.

Fix:
1. runtime/skill-registry.js — single source of truth derived from
   skills/index.js. Exports listSkillIds, isRegistered, and a
   prompt-ready block (skillRegistryPrompt) grouped by namespace.
2. Pi prompts (coach/postmortem, coach/reflect) embed the live registry
   with a "USE ONLY THESE, never invent" instruction. Lessons are
   filtered at write-time too — anything not in the registry and not a
   known mode name gets dropped.
3. coach/advice.js — normalisePreferSkill now returns null for unknown
   ids, hardening consult() against any hallucinations that slip
   through. Warn-logged for visibility.

Also lays the LLM substrate for the rest of v0.3.0:

- runtime/llm/provider.js — OpenAI-compatible chat client. Configured
  via PEPA_FAST_LLM_{BASE_URL,API_KEY,MODEL,TIMEOUT_MS}. Safe no-op
  unless API_KEY is set. Supports JSON-mode.
- runtime/coach/fast-advisor.js — tactical advisor tier (scaffold).
  Exposes advise() that asks the fast LLM what to do RIGHT NOW when
  the reflex is wedged/stuck. Rejects hallucinated skill ids using the
  registry. Rate-limited 6/h, 30s cooldown. Not auto-triggered yet —
  wired into reflex in rc.3 (awareness layer).

Tests: 279 green (+24 vs rc.3): 5 registry, 9 provider, 10 advisor.

See dev/v0.3.0/PLAN.md for the full iteration design (manifesto needs
ladder, event-driven awareness, skill pre-emption) and STATUS.md for
shipped/pending tracking.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* v0.3.0-rc.2: manifesto / needs ladder L0-L10

Adds an explicit hierarchical needs catalogue that the reflex consults
on every tick. The bot now pursues tangible intermediate goals (food,
wood tools, shelter, stone tools, ...) instead of inheriting whatever
the curriculum thought was "next".

Ladder:
  L0  alive          HP>5, food>0, not in lava, not panic-near hostile
  L1  food           ≥6 food items in inventory (or sated + any food)
  L2  tools_wood     wooden_pickaxe + wooden_axe + wooden_sword
  L3  shelter_basic  bed placed nearby or in inventory
  L4  tools_stone    stone-tier triplet
  L5  armor_basic    any chestplate (pursue=null until craft.leather-*
                     lands; ladder gracefully skips)
  L6  food_security  ≥16 food items
  L7  tools_iron     iron-tier triplet (pursue=gather.stone for now)
  L8  armor_iron     iron chestplate (pursue=null for now)
  L9  village_seed   bed + chest in nearby blocks
  L10 village_full   never detected, falls through to curriculum

Each need has detect(snapshot) → bool and pursue(snapshot) →
{skillId, args} | null. The ladder picks the LOWEST unsatisfied
pursuable need. Needs whose pursue is null get recorded as
blockedNeeds and the walk continues — no stalling on missing skills.

Wired into curriculumReflex: manifesto takes precedence over
curriculum.plan when it has a concrete suggestion. Tests can pass
ctx.disableManifesto=true to exercise the curriculum branch
in isolation (existing reflex tests keep passing this way).

Pi self-reflection prompt now includes
"activeNeed (Maslow ladder L0-L10): L2 tools_wood → gather.logs"
so Pi advises at the right level instead of giving generic guidance.

skillId returned by pursue() is validated against the live registry
(rc.1 plumbing) — manifesto cannot accidentally dispatch a
hallucinated skill name.

Tests: 315 green (was 279 on rc.1, +36 new):
- runtime/manifesto/needs.test.js — 24 tests (per-need detect/pursue,
  helper sums)
- runtime/manifesto/state.test.js — 10 tests (ladder walk, hostile
  takeover at L0, armor skipping, caching)
- runtime/reflex.test.js — 2 integration tests (manifesto overrides
  curriculum plan; well-fed bot pursues tools_stone)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* v0.3.0-rc.3: event-driven awareness + skill pre-emption

Adds a reactive layer on top of the polling reflex. The bot now
notices environmental shocks (forced moves, HP plunges, hostile
spawns) within ~100ms instead of waiting for the next DISPATCH tick,
and the in-flight skill is preempted so the next reflex cycle can
re-plan against the current world state.

This is the rc that wires the "rc.1 plumbing + rc.2 manifesto" into
a feedback loop:
  - awareness fires preempt → dispatch aborts
  - reflex tick re-evaluates → manifesto walks the ladder
  - new dispatch picks the right skill for the new world state

Pieces:

- runtime/awareness/events.js (new) — bot.on listeners:
  - move: single-tick Δposition ≥ 5 blocks → forced_move flag + preempt
  - health: HP drop ≥ 2 → health_plunge flag + preempt
  - entitySpawn: hostile mob within 12 blocks → hostile_added + preempt
  - blockUpdate: nearby block change → env_changed flag (no preempt,
    throttled 800ms; otherwise gather skills would self-preempt
    every dig)

- runtime/skills/index.js — RUNNER_CODES.PREEMPTED + raceWithAbort()
  wraps every execute() against ctx.abortSignal. Existing skills get
  preemption for free; they don't have to check the signal manually.

- runtime/bot.js:
  - dispatchAction creates a fresh AbortController per dispatch and
    stores it on reflexCtx.currentAbort
  - attachAwareness fires controller.abort() when something disrupts
    the active skill; runSkill returns code: "preempted" and the
    reflex moves on
  - reflexCtx.lastPreempt records the most recent shock

Tests: 332 green (was 315 on rc.2, +17 new):
- runtime/awareness/events.test.js — 12 tests (each event type +
  thresholds + throttling + passive-mob filter)
- runtime/skills/contract.test.js — 3 abortSignal tests
  (mid-flight, pre-armed, clean signal)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(.env): add PEPA_FAST_LLM_* placeholders for v0.3.0 fast advisor

Empty values keep the fast-advisor tier disabled (safe no-op). Fill
in BASE_URL + API_KEY + MODEL to enable. TimeWeb-style endpoint
example included.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(v0.3.0): rename fast-LLM env vars to TIMEWEB_* (match other projects)

Aligns with the user's other repos (proso) which use TIMEWEB_API_GROK /
TIMEWEB_URL_GROK. Single naming convention across projects avoids the
'which env var was it for this repo' mental tax.

  PEPA_FAST_LLM_BASE_URL → TIMEWEB_BASE_URL
  PEPA_FAST_LLM_API_KEY  → TIMEWEB_API_KEY
  PEPA_FAST_LLM_MODEL    → TIMEWEB_MODEL
  PEPA_FAST_LLM_TIMEOUT_MS → TIMEWEB_TIMEOUT_MS

Provider still works with any OpenAI-compatible endpoint — TimeWeb is
the default but the variable name doesn't lock us in. Tests + docs +
.env / .env.example updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(scripts): TimeWeb smoke test + bump default LLM timeout to 20s

scripts/check-timeweb.js — three probes: plain text, JSON mode, full
fast-advisor stack (registry injection + skill validation). Loads .env,
prints {ok, latency, reply preview} for each. Doesn't touch bot state.

Bumped DEFAULT_TIMEOUT_MS 8s → 20s in runtime/llm/provider.js. TimeWeb's
hosted agent endpoint takes 5-15s for the fast-advisor prompt
(registry block + snapshot context), so 8s was producing spurious
timeouts. OpenAI direct returns much faster; env var TIMEWEB_TIMEOUT_MS
overrides if needed.

Smoke verified live (PR #27 branch):
  probe 1: 6.3s, plain prompt → "pepa hears you"
  probe 2: 5.4s, JSON mode → {"alive":true,"name":"pepa"}
  probe 3: 14.9s, advise() → action=switch_skill, skill=recovery.tunnel-out
           (correct registered skill, sensible rationale — registry
           injection successfully prevents hallucination)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(v0.3.0): auto-trigger fast-advisor + token usage tracking

Closes the awareness → LLM → action loop that the rc.1/2/3 sequence
left as a followup. When the bot is wedged, looping, or just suffered
a preempt-then-retry, the reflex fires advise() in the background;
when the recommendation lands it overrides the next dispatch.

Async by design: advise() takes 5-15s on TimeWeb's hosted endpoint —
too slow for a synchronous reflex tick. tickAdvisor() is fire-and-
forget, the result lands on ctx.advisorRecommendation, and the *next*
tick reads and consumes it. Recommendations age out after 60s.

Components:

- runtime/coach/advisor-trigger.js — policy + async fire path
  - tickAdvisor(ctx, {plannedSkillId}) checks three triggers:
    1. wedged > 60s (no significant move)
    2. last 4+ dispatches are the same skill AND it's planned again
    3. preempt within last 30s + same skill being retried
  - 90s trigger cooldown, single-in-flight guard
  - consumeFreshRecommendation(ctx) reads/clears the cache
- runtime/reflex.js — curriculumReflex calls tickAdvisor() every tick
  and consumes a fresh recommendation BEFORE dispatching. ctx flag
  disableAdvisor=true for tests.
- runtime/bot.js — dispatchAction maintains a rolling 8-slot
  reflexCtx.recentSkillIds for the loop-detection trigger.

Token usage:

- runtime/llm/provider.js — normaliseUsage() reads OpenAI/TimeWeb-
  style {prompt_tokens, completion_tokens, total_tokens} from the
  response. Returned on every complete() result and logged at info
  level as "in=Nt/out=Mt".
- runtime/coach/fast-advisor.js — getUsageSnapshot() aggregates
  total tokens across all calls in the session.

Measured on live TimeWeb endpoint (gpt-5.4-mini agent):
  per call: ~705 input + 45 output = ~750 tokens
  rate limit: 6 calls/hour
  worst case at full budget: ~108K tokens/day
  estimated cost (OpenAI gpt-5-mini reference price): ~$0.60/month

Well within any reasonable budget — model can run hot 24/7.

Smoke verified: scripts/check-timeweb.js probe 4 produces
  trigger fired: true (wedged_90s)
  recommendation: recovery.tunnel-out
  rationale: "Stuck wedged for 90s; exploration is failing."
  latency: 5302ms

Tests: 345 green (was 332, +13 advisor-trigger).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(v0.3.0): paradigm shift — TimeWeb-only LLM + persistent advisor trail + improvement queue

This is the rc.4 batch the user requested:

  1. Emergency triggers (low HP + close hostile, lava-under-foot)
     bypass the long cooldown so the LLM is consulted BEFORE the bot
     dies, not after.
  2. Active manifesto need is now included in the advisor user prompt
     — the LLM picks suggestions that satisfy the bot's current
     concrete need (L2 tools_wood → "gather logs nearby" not
     "explore further").
  3. Every advisor recommendation is persisted to SQLite
     (advisor_recommendations table) with full token usage. The
     reflex marks 'applied=1' when it dispatches and updates
     outcome_ok/code when the dispatch completes. Ground truth for
     "is the LLM actually helping" lives in the DB, not in logs.
  4. Pi CLI is OUT of every background loop. coach/postmortem and
     coach/reflect now go through the same TimeWeb endpoint
     fast-advisor uses, via the shared coach/llm-call.js helper.
     Pi is reserved for manual operator commands.
  5. The LLM (postmortem, reflect, advisor) can flag "structural
     gaps" — missing skills/features the operator should implement.
     These land in the new improvement_requests table. Dedup by
     title bumps `votes` instead of inserting duplicates so the
     queue doesn't bloat. Operator views via
     `node scripts/list-improvements.js`.
  6. A deterministic trigger-tuner runs hourly: reads 24h of
     recommendation stats, flags triggers whose success rate is
     below 25% (sample ≥ 5) or whose prompts are expensive (>1000
     input tokens) with mediocre payoff. Improvements get
     source="tuner", category="tuning". No LLM call.

New files:
  runtime/coach/llm-call.js        — askAnalytical() helper
  runtime/coach/trigger-tuner.js   — stats → improvements
  runtime/coach/trigger-tuner.test.js
  scripts/list-improvements.js     — operator CLI

Schema additions:
  advisor_recommendations: id, ts, trigger_reason, planned_skill,
    recommended_skill, action, rationale, active_need, tokens_in,
    tokens_out, latency_ms, applied, outcome_ok, outcome_code, outcome_at
  improvement_requests: id, ts, source, category, title, description,
    context, priority, status, duplicate_of, votes, implemented_at, notes

Renamed env-var consumers:
  Pi-coach drainOnce({ askPi })   → drainOnce({ askAnalyticalFn? })
  Pi-reflect runOnce({ askPi })   → runOnce({ askAnalyticalFn? })
  bot.js attachCoach/attachReflect no longer pass askPi
  attachTuner() added to bot.js spawn handler
  lessons.source 'pi-coach'   → 'timeweb-coach'
  lessons.source 'pi-reflect' → 'timeweb-reflect'

Token cost measured live:
  ~705 input + 45 output = ~750 total per advisor call
  worst case @ 6 calls/hour rate cap = ~108K tokens/day
  OpenAI gpt-5-mini reference price: ~$0.60/month

Operator usage:
  node scripts/list-improvements.js                # open queue
  node scripts/list-improvements.js --stats        # advisor performance
  node scripts/list-improvements.js --done 17 "shipped in 0.3.1"
  node scripts/list-improvements.js --reject 18 "duplicate"

Tests: 360 green (was 332, +28 new).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:37:56 +03:00

242 lines
11 KiB
SQL

-- pepa-pi-bot knowledge schema v1
-- Single-process SQLite store at state/<host>/knowledge.db.
-- Idempotent: applied on every boot. Migrations go below the CREATE TABLE
-- block, gated by schema_version.
CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER NOT NULL PRIMARY KEY,
applied_at INTEGER NOT NULL
);
----------------------------------------------------------------------
-- Recipes (seeded from docs/minecraft-recipes.json + augmented by wiki)
----------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS recipes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
shape TEXT NOT NULL, -- JSON array of rows
shapeless INTEGER NOT NULL DEFAULT 0,
yields INTEGER NOT NULL DEFAULT 1,
requires_table INTEGER NOT NULL DEFAULT 1, -- 0=hand,1=table,2=furnace,3=smithing
source TEXT,
source_url TEXT,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_recipes_name ON recipes(name);
----------------------------------------------------------------------
-- Mob intel — what to do when you see a mob
----------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mob_intel (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
hostility TEXT NOT NULL, -- 'hostile' | 'neutral' | 'passive' | 'tamable'
threat_level INTEGER NOT NULL, -- 1..5
approach_range REAL, -- blocks at which it engages
burns_in_sun INTEGER NOT NULL DEFAULT 0,
ranged INTEGER NOT NULL DEFAULT 0,
weakness TEXT,
drops TEXT, -- JSON array of names
verdict_no_weapon TEXT, -- 'flee' | 'shelter' | 'avoid' | 'pillar'
verdict_with_sword TEXT, -- 'kite' | 'attack' | 'avoid'
notes TEXT,
source TEXT,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_mob_name ON mob_intel(name);
----------------------------------------------------------------------
-- Block intel — what tool, what drops, lighting
----------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS block_intel (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
required_tool TEXT, -- 'any'|'wood_pickaxe'|'stone_pickaxe'|'iron_pickaxe'|'shovel'|'axe'
drops TEXT, -- JSON array
light_emit INTEGER DEFAULT 0,
walkable INTEGER DEFAULT 1,
notes TEXT,
source TEXT,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_block_name ON block_intel(name);
----------------------------------------------------------------------
-- Lessons — generalised "what to do / what to avoid" learned over time
----------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS lessons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
text TEXT NOT NULL,
category TEXT NOT NULL, -- 'combat'|'pathing'|'crafting'|'survival'|'social'|'self-improve'
trigger_skill TEXT,
trigger_hostile TEXT,
trigger_situation TEXT, -- coarse hash key from scenario-memory
avoid_skill TEXT,
prefer_skill TEXT,
confidence REAL NOT NULL DEFAULT 0.5,
applied_count INTEGER NOT NULL DEFAULT 0,
succeeded_count INTEGER NOT NULL DEFAULT 0,
source TEXT NOT NULL, -- 'postmortem'|'pi-coach'|'wiki'|'operator'|'rule'
source_ref TEXT
);
CREATE INDEX IF NOT EXISTS idx_lessons_category ON lessons(category);
CREATE INDEX IF NOT EXISTS idx_lessons_skill ON lessons(trigger_skill);
CREATE INDEX IF NOT EXISTS idx_lessons_hostile ON lessons(trigger_hostile);
CREATE INDEX IF NOT EXISTS idx_lessons_ts ON lessons(ts);
----------------------------------------------------------------------
-- Death events — captured by coach/postmortem.js on every death
----------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS deaths (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
x REAL, y REAL, z REAL,
cause TEXT, -- 'hostile'|'fall'|'lava'|'drowning'|'starvation'|'suffocation'|'other'|'unknown'
hostile TEXT,
last_skill TEXT,
last_skill_code TEXT,
hp_at_death REAL,
food_at_death REAL,
inventory_lost TEXT, -- JSON
context_blob TEXT, -- JSON: last 30s of events
analysed INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_deaths_ts ON deaths(ts);
CREATE INDEX IF NOT EXISTS idx_deaths_analysed ON deaths(analysed);
CREATE INDEX IF NOT EXISTS idx_deaths_cause ON deaths(cause);
----------------------------------------------------------------------
-- Pi-extracted post-mortems linking back to deaths
----------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS postmortems (
id INTEGER PRIMARY KEY AUTOINCREMENT,
death_id INTEGER NOT NULL,
ts INTEGER NOT NULL,
cause TEXT,
lesson TEXT,
next_action TEXT,
raw_response TEXT,
source TEXT, -- 'pi' | 'rule'
FOREIGN KEY (death_id) REFERENCES deaths(id)
);
CREATE INDEX IF NOT EXISTS idx_postmortems_death ON postmortems(death_id);
----------------------------------------------------------------------
-- Points of interest — queryable spatial memory
----------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS poi (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL, -- 'tree'|'ore'|'water'|'mob_spawner'|'danger'|'foreign_build'|'base'|'chest'
name TEXT,
x REAL NOT NULL, y REAL NOT NULL, z REAL NOT NULL,
cell_x INTEGER NOT NULL,
cell_z INTEGER NOT NULL,
ts INTEGER NOT NULL,
expires_at INTEGER,
notes TEXT
);
CREATE INDEX IF NOT EXISTS idx_poi_cell ON poi(cell_x, cell_z);
CREATE INDEX IF NOT EXISTS idx_poi_kind ON poi(kind);
----------------------------------------------------------------------
-- Cached wiki pages (rc.2)
----------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS wiki_pages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE,
url TEXT NOT NULL,
body TEXT,
etag TEXT,
fetched_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL
);
----------------------------------------------------------------------
-- Full chat log (durable; per-speaker LRU in memory is unchanged)
----------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS chat_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
direction TEXT NOT NULL, -- 'in' | 'out'
speaker TEXT,
text TEXT NOT NULL,
intent TEXT,
replied_with TEXT
);
CREATE INDEX IF NOT EXISTS idx_chat_speaker ON chat_log(speaker);
CREATE INDEX IF NOT EXISTS idx_chat_ts ON chat_log(ts);
----------------------------------------------------------------------
-- Self-rewrite audit
----------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS code_changes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
proposal_slug TEXT,
files TEXT, -- JSON array
diff_hash TEXT,
outcome TEXT, -- 'applied'|'rolled_back'|'rejected'
notes TEXT
);
----------------------------------------------------------------------
-- Advisor recommendations (v0.3.0+ fast LLM trail)
-- Every time runtime/coach/advisor-trigger.js asks the fast LLM and
-- the answer is cached on ctx, we write a row here. When the reflex
-- consumes the recommendation and dispatches, we attach the dispatch
-- result later via outcome_ok / outcome_code. The history is the
-- ground truth for trigger-tuner.js stats and for the operator's
-- "what is the LLM suggesting and is it actually helping" question.
----------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS advisor_recommendations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
trigger_reason TEXT NOT NULL, -- 'wedged_*', 'repeat_*', 'preempt_retry_*', 'emergency_*'
planned_skill TEXT, -- what manifesto/curriculum was about to dispatch
recommended_skill TEXT, -- what the LLM said to do instead
action TEXT NOT NULL, -- 'switch_skill' | 'continue' | 'wait'
rationale TEXT,
active_need TEXT, -- 'L2 tools_wood' etc.
tokens_in INTEGER,
tokens_out INTEGER,
latency_ms INTEGER,
applied INTEGER NOT NULL DEFAULT 0, -- 1 if reflex actually dispatched recommended_skill
outcome_ok INTEGER, -- NULL until dispatch finishes
outcome_code TEXT,
outcome_at INTEGER
);
CREATE INDEX IF NOT EXISTS idx_advisor_ts ON advisor_recommendations(ts);
CREATE INDEX IF NOT EXISTS idx_advisor_trigger ON advisor_recommendations(trigger_reason);
CREATE INDEX IF NOT EXISTS idx_advisor_outcome ON advisor_recommendations(outcome_ok);
----------------------------------------------------------------------
-- Improvement requests (v0.3.0+)
-- The LLM (postmortem / reflect / advisor) can flag situations where
-- the bot lacked the right skill or feature. Instead of trying to
-- self-patch (which we explicitly disabled), it writes an entry here.
-- The operator reads `scripts/list-improvements.js` and decides what
-- to implement. Implemented entries get marked so the bot stops
-- re-flagging the same gap.
----------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS improvement_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
source TEXT NOT NULL, -- 'postmortem'|'reflect'|'advisor'|'tuner'|'manual'
category TEXT, -- 'skill'|'tuning'|'perception'|'planning'|'social'|'other'
title TEXT NOT NULL,
description TEXT,
context TEXT, -- JSON: position, snapshot tail, related lesson ids
priority INTEGER NOT NULL DEFAULT 3, -- 1..5 (1=urgent, 5=nice-to-have)
status TEXT NOT NULL DEFAULT 'open', -- 'open'|'in_progress'|'implemented'|'rejected'|'duplicate'
duplicate_of INTEGER, -- another row id if dup
votes INTEGER NOT NULL DEFAULT 1, -- bumped each time the bot re-flags same gap
implemented_at INTEGER,
notes TEXT,
FOREIGN KEY (duplicate_of) REFERENCES improvement_requests(id)
);
CREATE INDEX IF NOT EXISTS idx_improvements_status ON improvement_requests(status);
CREATE INDEX IF NOT EXISTS idx_improvements_priority ON improvement_requests(priority);
CREATE INDEX IF NOT EXISTS idx_improvements_source ON improvement_requests(source);
CREATE INDEX IF NOT EXISTS idx_improvements_ts ON improvement_requests(ts);