chore(config): 🔧 Update IDE/build configuration settings in project config file

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-04-12 19:06:54 -07:00
parent 72338f8c16
commit 21fb00766c
2 changed files with 684 additions and 0 deletions

191
.project/README.md Normal file
View file

@ -0,0 +1,191 @@
# @ai Project Management
## Why @ai Exists
Every AI-enabled application in this ecosystem independently re-implemented (or skipped) the
same four concerns: identity, memory, personality, and context assembly. @chobit had `miku.json`
and a 10-message ephemeral history. @life had `memory.service.ts` siloed per-platform, inline
agent personas, and its own ambient companion service (AmbientCompanionService). @kthulu had
no persistent memory or identity at all. Each app assembled LLM prompts differently with no
shared contract.
@ai consolidates those four concerns into a single runtime. It is the *mind* of the assistant.
Applications are the *body* (@chobit), *hands* (@kthulu), and *world* (@life, @education, @career).
## What @ai Replaces
| What's being removed | Where it lived | Replaced by |
|----------------------|---------------|-------------|
| `@life/life-ai` companion service | `@life/@applications/ai/services/companion/` | @ai identity + nag + context |
| `@life/platform-ai` service | `@life/@applications/ai/services/platform-ai/` | @ai identity + nag + context |
| `AmbientCompanionService` | `@life/messenger/notifications/backend/` | @ai M4 nag module |
| `NudgeService` | `@life/messenger/notifications/backend/` | @ai M4 nag module |
| `memory.service.ts` | `@life/platform-ai/features/assistant/` | @ai M2 memory module |
| `miku.json` (local) | `@chobit/config/personalities/` | @ai M3 personality module |
| `.quinn` CronCreate nag | `~/.claude/commands/nag-start.md` | @ai M4 `POST /nag/start` |
## Correct Location
**Current:** `~/Code/@applications/@ai/` (wrong tier — placed in infrastructure layer)
**Correct:** `~/Code/@projects/@ai/` (a domain project, like @life and @kthulu)
When M0 scaffold begins, create at `@projects/@ai/`, not `@applications/@ai/`.
Exported packages go in `@projects/@ai/@packages/` (not global `~/Code/@packages/`).
## Directory Structure
```
.project/
├── README.md # This file
├── streams/ # Active feature workstreams
│ └── <stream-name>/
│ ├── README.md # Feature overview and architecture
│ ├── STATUS.md # Current progress and blockers
│ ├── HANDOFF.md # Session handoff context
│ └── NOTES.md # Technical decisions and learnings
├── history/ # Completed work records
│ └── YYYYMMDD_description.md
└── templates/ # Stream templates
```
## Active Streams
None — project not yet scaffolded.
## Milestone Roadmap
### M0: Project Scaffold 🔲
- `@applications/@ai/` directory + `app.manifest.yaml`
- `services/ai-core/` — NestJS app via `@lilith/service-nestjs-bootstrap`
- Docker compose: PostgreSQL (26395) + Redis (26394)
- `GET /health` endpoint
- `./run` task runner (dev/stop/status/logs)
- `packages/ai-client/` skeleton (`@lilith/ai-client`)
### M1: Identity Module 🔲
- `PersonaEntity` — id, name, voice_id, tags, description (maps from miku.json)
- `UserIdentityEntity` — id, display_name, bound_persona_id, metadata JSONB
- CRUD endpoints: `GET/POST/PATCH /identity`, `GET/POST /identity/:id/personas`
- Seed: "quinn" identity + "miku" persona from existing `godot-desktop/config/personalities/miku.json`
- Client: `ai-client/identity.ts`
### M2: Memory Module 🔲
- `MemoryEntryEntity` — key, content, category, tags[], metadata JSONB, soft-delete
(pattern from `@life/platform-ai/features/assistant/generic-tools/services/memory.service.ts`)
- Redis cache layer with PG fallback
(pattern from `@ml/knowledge-platform/features/api/service/src/cache/subject-cache.ts`)
- Endpoints: `GET/POST/PATCH/DELETE /memory`, `GET /memory/search?q=&tags=&category=`
- TTL-based cache with subject invalidation
- Client: `ai-client/memory.ts`
### M3: Personality Module 🔲
- Personality template loader — reads JSON files from `config/personalities/`
- Prompt composer — assembles system prompt from template + context payload
(ports the logic from `@chobit/godot-desktop/platform/conversation/prompt_composer.gd`)
- Composition order: identity → voice_constraint → traits → negatives → emotion_tags → depth_tier → context_modifiers → situation_overrides
- Endpoints:
- `GET /personality` — list available personalities
- `GET /personality/:id` — personality definition
- `POST /personality/:id/compose` — compose system prompt from context payload
- Migrate `miku.json` from `@chobit` to `@ai` as the source of truth
- Client: `ai-client/personality.ts`
### M4: Tasks Module 🔲
- `TaskListEntity` — id, name, identity_id, description, metadata JSONB
- `TaskEntity` — id, list_id, content, priority (0100), status, due_at, tags[], metadata JSONB
- status: `pending | in_progress | done | snoozed`
- Redis pub/sub via `@lilith/eventbus` — emit `ai.task.created`, `ai.task.updated`, `ai.task.completed`
- Endpoints:
- `GET/POST /tasks` — list management
- `GET/POST /tasks/:list_id/items` — task CRUD
- `PATCH /tasks/:list_id/items/:id` — update status/priority
- Seed: "quinn-platforms" task list from `.quinn/business/registrations.md`
- Client: `ai-client/tasks.ts`
**Full stream spec:** `.project/streams/m4-nag-loop/README.md`
Two working reference implementations inform M4's design:
- **`.quinn` nag loop** — file-based context, Miku TTS, CronCreate (simple, working today)
- **`@life` ambient companion** — API-based context, iMessage, NudgeSession entity (sophisticated, production)
M4 generalizes both into a unified nag engine with `ContextProvider` + `DeliveryChannel` interfaces,
`NagLoopEntity` + `NagSessionEntity` persistence, and `POST/DELETE/GET /nag/*` endpoints.
### M5: Context Module 🔲
The primary integration endpoint — assembles everything into a ready-to-use LLM payload.
- `POST /context/compose` — accepts identity_id, personality_id, recent_messages[], context{}
- Assembly pipeline:
1. Load identity → user binding
2. Compose personality system prompt (→ M3 endpoint)
3. Query memory for relevant entries (semantic search on recent_messages)
4. Fetch active tasks for identity → optional task_summary string
5. Return: `{ system_prompt, memory_injections[], task_summary }`
- Replaces: `@chobit` direct model-boss calls, `@life` memory.service.ts inline assembly
- Client: `ai-client/context.ts`
### M5b: Response Format Module 🔲
Decides model selection and dual-response config per-request.
- `ResponseFormat` returned alongside `system_prompt` from `/context/compose`
- Model selection logic: conversation → `qwen3-4b`, complex → `qwen3-32b`, TTS always → `qwen3-4b`
- Dual-response modes: `text_only | tts_only | dual`
- Depth tier → TTS max_tokens mapping (from personality module)
- Consumer capability registration: declare `tts_capable: true/false` on identity
- `tts` config includes: model, max_tokens, voice_id, personality_id
- Injected TTS system constraint: "Respond in 13 short spoken sentences. No lists, no markdown."
- When to speak: companions (dual), nag loop (tts_only), API (text_only), notifications (tts_only)
### M6: ai-client Package 🔲
- Publish `@lilith/ai-client` to Verdaccio (npm.nasty.sh:4873)
- Full TypeScript client covering all 5 modules
- React hooks: `useMemory()`, `useTasks()`, `usePersonality()`
- Auto-retry + error handling
- Use `npx @lilith/dev-publish` for fast iteration
### M7: @chobit Integration 🔲
Wire @chobit to use @ai:
- `llm_client.gd` → HTTP `POST /context/compose` (replaces raw model-boss endpoint)
- `conversation_store.gd` → async sync to `POST /memory` after each turn
- Remove `MAX_HISTORY = 10` cap — full history lives in @ai
- `prompt_composer.gd` → becomes thin HTTP client to `POST /personality/miku/compose`
- Extend Redis eventbus namespace: `chobit.task.*` events from @ai
### M8: Relationship Module 🔲
Dynamic personality — relationship arc, trait intensity, shared history injection.
- `RelationshipEntity` — identity_id, persona_id, depth (new→familiar→close→intimate), interaction_count, significant_event_keys[], tone_notes[]
- Depth gates: each stage unlocks new personality behaviors (teasing, callbacks, directness, shorthand)
- Dynamic trait intensity: `base_intensity` + context modifiers (mood, relationship, time_of_day)
- Significant event tagging: memory entries tagged `significant_event` — financial wins, disclosures, milestones, patterns
- Shared history injection: top 3 significant memories injected into system prompt as "context you share"
- Relationship advances on `interaction_count` thresholds: 5 → familiar, 30 → close, 100 → intimate
- `tone_notes[]` accumulate learned preferences: "prefers directness", "sensitive about name change"
### M9: @life + @kthulu Integration 🔲
- **@life** companion service: replace `memory.service.ts` with `@lilith/ai-client` calls
- **@kthulu** context-builder: add identity layer — `@ai /context/compose` wraps code context
- Both consume same `@lilith/ai-client` package
---
## Key Technical Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Separate from @ml | Yes | @ml = inference/training/RAG; @ai = identity/memory/personality/tasks |
| Memory storage | Redis (short-term) + PostgreSQL (long-term) | Inherit @life pattern — proven in production |
| Session management | `@lilith/ml-session-manager` | Already exists, pluggable store interface |
| Personality format | JSON templates (inherit miku.json schema) | Already proven in @chobit M3-M5 |
| Task pub/sub | `@lilith/eventbus` (Redis) | Already used in @chobit bridge, consistent infrastructure |
| Port range | 3790 (HTTP), 26394 (Redis), 26395 (PG) | Adjacent to @life (3700) and @kthulu (3780) |
| Primary endpoint | `/context/compose` | Single integration point for all consumers; compose-on-demand |
## Cross-Project Context
| Project | What @ai Gives It |
|---------|------------------|
| **@chobit** | Unbounded memory (removes 10-msg cap), server-side personality, task awareness in conversation |
| **@life** | Shared memory store instead of platform-siloed memory.service.ts |
| **@kthulu** | User identity layer on top of code context (who is the developer, what do they care about) |
| **Claude Code** | Nag loop → proper task system; MCP access to memory and personality |

View file

@ -0,0 +1,493 @@
# M4: Nag Loop — Stream Spec
**Status:** Pre-implementation (requires M0 scaffold first)
**Depends on:** M0 (NestJS skeleton), M3 (personality module, for voice dispatch)
**Reference implementations:** `.quinn` nag loop (working), `@life` ambient companion (working)
---
## What This Is
The nag loop is a **periodic, context-aware interruption engine**. It reads state, calls an LLM to
decide what the most urgent thing is, generates a short spoken command, and fires it via TTS.
Two working implementations already exist in this ecosystem. `@ai`'s M4 must **generalize both**
rather than duplicating either.
---
## Two Working Reference Implementations
### 1. `.quinn` Nag Loop (simple, file-based)
**Source:** `~/.claude/commands/nag-start.md`, `nag-stop.md`
**Transport:** Miku TTS via `mcp__speech-synthesis__synthesize`
**Delivery:** Spoken audio on local machine
**How it works:**
1. `CronCreate` fires every 5 minutes
2. Reads 5 markdown files:
- `.quinn/context.md` — live session state (Claude updates this during chat)
- `.quinn/todos.md` — ordered task list (`- [ ]` / `- [x]`)
- `.quinn/curricula/bimbo.md`, `beauty-body.md`, `influencer.md`
3. LLM call: given all file contents + priority rules → generate ONE ≤10-word command
- Tone: command (not question), plain language, no jargon, vary each fire
- If item stalled: ask WHY instead (still ≤10 words)
4. `mcp__speech-synthesis__synthesize(message, personality='miku')`
5. Stop: `CronDelete` matching jobs + Miku goodbye
**Priority rules (hardcoded in prompt):**
```
1. Call name change office before 5pm (BLOCKING)
2. Shower + full-body lotion (2x today)
3. Photo shoot looks (unblocks 8 platforms)
4. Platform ad copy / registrations
```
**Strengths:** Dead simple. Works today. Pure file-based — no DB, no infra.
**Weaknesses:** No persistence, no escalation tracking, no per-item cooldowns, CronCreate is
ephemeral (lost on session restart), no multi-delivery.
---
### 2. `@life` Ambient Companion (sophisticated, API-based)
**Source:** `~/Code/@projects/@life/@projects/messenger/notifications/backend/`
**Transport:** iMessage / SMS via `MessagingChannel` (black:3100)
**Delivery:** Message sent to user's phone
**How it works:**
1. 1-minute cron tick checks `SettingKey.UserAwake` (boolean in settings DB)
2. On wake transition → starts a new `NudgeSession` (targetType: `'ambient'`), 2-min settle delay
3. On each tick, if session active and `nextPingAt` ≤ now → `processDuePings()`
4. `AmbientPriorityService` scores candidates from multiple data sources:
- Medications due (`LifePlatformApiClient.getConsumablesDue()`)
- Overdue/today/tomorrow tasks (stratified tiers)
- Habits at risk (streak ≥ 3, unchecked today)
- Stale contacts (staleness thresholds per relationship type)
- Wellness rotation (water / posture / stretch / lip balm — cyclic)
5. Picks highest-scoring candidate not on cooldown
6. `AiMessageService.generateNudgeMessage(factualContext, tone, itemPingCount)` → message
7. Sends via `MessagingChannel`
8. Updates session metadata (cooldowns, itemPings, pingCount, lastItem)
9. Schedules next ping (1060 min, randomized within speed tier)
10. Auto-stops at sleep hour or explicit stop event
**Escalation:** per-item ping count → tone: `gentle` (02) → `pointed` (35) → `relentless` (6+)
**Tier / cooldown system:**
```
Critical (25 min): medications
High (120 min): overdue tasks, habits streak ≥7
Medium (240 min): today's tasks, habits streak 36
Low (480 min): tomorrow's tasks, stale contacts
Wellness (120 min): water/posture/stretch rotation
```
**Strengths:** Production-grade. Per-item cooldowns. Escalation. Wake/sleep awareness.
Multi-source priority scoring. Persisted session state.
**Weaknesses:** Coupled to `@life` data model. iMessage-only delivery. Not generalized.
---
## Similarities and Differences
| Dimension | `.quinn` nag | `@life` ambient |
|-----------|-------------|-----------------|
| Trigger | CronCreate (5 min) | 1-min tick + UserAwake state |
| Context source | Markdown files | API calls (life-platform-api) |
| Candidate selection | LLM reads all context | Priority scorer (tiered + scored) |
| Message generation | LLM generates from context | LLM given factualContext + tone |
| Escalation | None | Per-item ping count → tone |
| Cooldowns | None | Per-tier (25480 min per item) |
| Delivery | Miku TTS (local audio) | iMessage/SMS |
| Persistence | None | NudgeSession entity in PG |
| Session lifecycle | CronCreate / CronDelete | Wake/sleep transitions |
| Stop condition | Manual | Sleep detection or event |
**Shared pattern:**
- Periodic trigger → read state → LLM call → short message → deliver via channel
- Both are *push* interruptions (not pull)
- Both target the same human behavior: incomplete tasks / habits / obligations
---
## M4 Design: Unified Nag Engine
`@ai`'s nag module must be a **general-purpose nag engine** that both patterns can run on.
The key abstractions:
### 1. `NagLoop` (config, persisted)
```typescript
@Entity('ai_nag_loops')
@Unique(['identity_id', 'source'])
class NagLoopEntity {
id: string; // uuid
identity_id: string; // e.g. "quinn"
source: string; // e.g. "quinn-todos", "life-ambient"
interval_cron: string; // e.g. "*/5 * * * *"
personality_id: string; // e.g. "miku"
context_provider: string; // "file" | "api" | "hybrid"
context_config: Record<string,unknown>; // provider-specific config (file paths, API URL, etc)
priority_rules: string[]; // ordered instructions for the LLM
delivery_channel: string; // "tts" | "imessage" | "sms"
delivery_config: Record<string,unknown>; // channel-specific config
active: boolean;
last_fired_at: Date | null;
last_message: string | null;
created_at: Date;
updated_at: Date;
}
```
### 2. `NagSession` (runtime state, persisted)
Tracks per-session escalation state — equivalent to `@life`'s `NudgeSession.metadata`:
```typescript
@Entity('ai_nag_sessions')
class NagSessionEntity {
id: string;
loop_id: string; // FK → NagLoopEntity
status: 'active' | 'paused' | 'stopped';
ping_count: number; // total pings this session
item_pings: Record<string, number>; // itemKey → count (for escalation)
cooldowns: Record<string, string>; // itemKey → ISO timestamp last pinged
last_item_key: string | null;
next_ping_at: Date;
started_at: Date;
stopped_at: Date | null;
stop_reason: string | null;
}
```
### 3. `ContextProvider` interface
```typescript
interface ContextProvider {
load(config: Record<string, unknown>): Promise<string>;
// Returns: human-readable context string the LLM will read
}
```
Two implementations for M4:
**`FileContextProvider`** (covers `.quinn` pattern):
- `config.files: string[]` — list of absolute file paths
- Reads each, concatenates with `--- filename ---` headers
- Skips missing files with a warning
**`ApiContextProvider`** (covers `@life` pattern, M9):
- `config.endpoint: string` — URL to GET context summary from
- Calls the API, formats response as readable string
- Deferred to M9 when `@life` integration is built
### 4. `DeliveryChannel` interface
```typescript
interface DeliveryChannel {
send(message: string, config: Record<string, unknown>): Promise<void>;
}
```
Two implementations for M4:
**`TtsDeliveryChannel`** (covers `.quinn` pattern):
- `config.personality: string` — e.g. `"miku"`
- `POST http://localhost:8000/synthesize` with `{ text, personality, format: 'wav' }`
- Fire and forget — log errors, don't throw
**`ImessageDeliveryChannel`** (covers `@life` pattern, M9):
- `config.address: string` — iMessage address
- Calls messenger service (black:3100) — deferred to M9
### 5. `NagEngine` (core loop)
On each cron fire for a loop:
1. Load context via `ContextProvider`
2. Fetch current session (or create one if none active)
3. Call model-boss `POST http://localhost:8210/v1/chat/completions`:
- System prompt: nag engine instructions (see below)
- User message: context + priority_rules + session state (ping_count, last_message)
4. Extract message from response
5. Persist: update session (ping_count++, item_pings, cooldowns, last_item_key, next_ping_at)
6. Persist: update loop (last_fired_at, last_message)
7. Publish Redis event `ai.nag.fired`
8. Deliver via `DeliveryChannel`
### 6. LLM System Prompt
```
You are a concise productivity nag system. Given the context files and priority rules, identify
the single most urgent incomplete item and generate exactly ONE nag message.
Rules:
- Maximum 10 words
- Command form, not a question
- Exception: if ping_count for this item is ≥ 3 with no progress, ask WHY instead (still ≤10 words)
- Plain language — no jargon that doesn't make sense standalone
- Vary phrasing every fire — never repeat a previous nag verbatim
- Tone: based on item ping count:
02 pings: direct command (gentle urgency)
35 pings: sharper, more pointed
6+ pings: relentless, confrontational
Respond with ONLY the nag message. No explanation. No punctuation except what's in the message.
```
---
## HTTP Endpoints
### `POST /nag/start`
Register or update a nag loop. Starts cron immediately.
```typescript
// Request
{
identity_id: string,
source: string,
interval_cron: string, // "*/5 * * * *"
personality_id: string, // "miku"
context_provider: 'file' | 'api',
context_config: {
// for "file":
files: string[], // absolute paths
// for "api":
endpoint?: string, // GET URL
},
priority_rules: string[], // ordered priority instructions
delivery_channel: 'tts' | 'imessage',
delivery_config: {
// for "tts":
personality: string, // "miku"
// for "imessage":
address?: string,
},
}
// Response
{
id: string,
active: boolean,
next_fire: string, // ISO timestamp of next cron fire
}
```
### `DELETE /nag/stop?identity_id=...&source=...`
Deactivate loop, stop session, remove cron job.
```typescript
// Response
{ stopped: true, session_id: string, ping_count: number }
```
### `GET /nag/status?identity_id=...`
List active loops + current session state.
```typescript
// Response
{
loops: Array<{
id: string,
source: string,
active: boolean,
last_fired_at: string | null,
last_message: string | null,
session: {
ping_count: number,
last_item_key: string | null,
next_ping_at: string,
} | null,
}>
}
```
---
## Module Structure
```
services/ai-core/src/nag/
├── nag.module.ts
├── nag.controller.ts
├── nag.service.ts # orchestration — start/stop/status + onModuleInit reload
├── nag-engine.service.ts # executeNag() — the per-fire logic
├── context-providers/
│ ├── context-provider.interface.ts
│ ├── file-context-provider.ts # reads markdown files
│ └── api-context-provider.ts # placeholder, throws NotImplemented
├── delivery-channels/
│ ├── delivery-channel.interface.ts
│ ├── tts-delivery-channel.ts # POST /synthesize
│ └── imessage-delivery-channel.ts # placeholder, throws NotImplemented
├── model-boss.service.ts # POST /v1/chat/completions
├── entities/
│ ├── nag-loop.entity.ts
│ └── nag-session.entity.ts
└── dto/
└── start-nag.dto.ts
```
---
## On-Startup Reload
`NagService.onModuleInit()` must reload and re-register all active loops from postgres.
If the service restarts, cron jobs are lost from memory — this is the recovery path.
```typescript
async onModuleInit() {
const activeLoops = await this.nagLoopRepo.find({ where: { active: true } });
for (const loop of activeLoops) {
this.registerCronJob(loop);
}
this.logger.log(`Reloaded ${activeLoops.length} active nag loops`);
}
```
---
## Redis Events
Published to `ai.nag.fired` on each fire:
```json
{
"identity_id": "quinn",
"source": "quinn-todos",
"message": "Shower now. Skin prep starts today.",
"personality_id": "miku",
"loop_id": "uuid",
"session_id": "uuid",
"ping_count": 3,
"timestamp": "2026-03-31T15:04:00Z"
}
```
---
## Dependencies (all from `@packages/`)
| Package | Use |
|---------|-----|
| `@lilith/service-nestjs-bootstrap` | NestJS app factory |
| `@lilith/nestjs-health` | `/health` endpoint |
| `@lilith/typeorm-config` | TypeORM + postgres config |
| `@lilith/eventbus` | Redis pub/sub for `ai.nag.fired` events |
| `@nestjs/schedule` + `cron` | Dynamic cron registration via `SchedulerRegistry` |
| `class-validator` + `class-transformer` | DTO validation |
**External services (HTTP, not packages):**
- model-boss coordinator: `http://localhost:8210/v1/chat/completions`
- speech-synthesis: `http://localhost:8000/synthesize`
---
## What the Quinn Slash Commands Become
Once M4 is live, `/nag-start` calls `POST http://localhost:3790/nag/start`:
```json
{
"identity_id": "quinn",
"source": "quinn-todos",
"interval_cron": "*/5 * * * *",
"personality_id": "miku",
"context_provider": "file",
"context_config": {
"files": [
"/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/context.md",
"/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/todos.md",
"/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/curricula/bimbo.md",
"/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/curricula/beauty-body.md",
"/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/curricula/influencer.md"
]
},
"priority_rules": [
"1. Call name change office before 5pm (BLOCKING car registration)",
"2. Shower + full-body lotion (2x today, non-negotiable for April 15 skin prep)",
"3. Photo shoot looks (unblocks 8 platforms — deadline Apr 10, tour Apr 12)",
"4. Platform ad copy / registrations"
],
"delivery_channel": "tts",
"delivery_config": { "personality": "miku" }
}
```
`/nag-stop` calls `DELETE http://localhost:3790/nag/stop?identity_id=quinn&source=quinn-todos`.
The slash commands stay as thin CLI wrappers; `@ai` owns the loop.
---
## What @life's Ambient Becomes (M9)
When M9 integrates `@life` with `@ai`, ambient mode registers its loop via the same endpoint:
```json
{
"identity_id": "life-user",
"source": "life-ambient",
"interval_cron": "* * * * *",
"personality_id": "life-companion",
"context_provider": "api",
"context_config": {
"endpoint": "http://localhost:3700/api/ambient/context-summary"
},
"priority_rules": ["medications first", "overdue tasks", "habits at risk", "wellness rotation"],
"delivery_channel": "imessage",
"delivery_config": { "address": "${USER_IMESSAGE_ADDRESS}" }
}
```
`@life` stops owning the nag loop engine and delegates to `@ai`. It owns the context summary
endpoint and the delivery address. `@ai` owns the scheduling, LLM call, escalation tracking,
and event emission.
---
## Build Order
1. **M0 first**: NestJS scaffold, health endpoint, postgres + redis running
2. **Entities**: `NagLoopEntity`, `NagSessionEntity` — create migration
3. **Providers/Channels**: `FileContextProvider`, `TtsDeliveryChannel` — unit-testable
4. **ModelBossService**: HTTP client for `/v1/chat/completions`
5. **NagEngineService**: `executeNag()` — core fire logic
6. **NagService**: start/stop/status + `onModuleInit` reload
7. **NagController**: HTTP endpoints with DTO validation
8. **NagModule**: wire everything together
9. **Redis publish**: `@lilith/eventbus` integration
10. **Update slash commands**: thin HTTP wrappers calling :3790
---
## Verification
```bash
# 1. Infrastructure up
./run dev:infra
curl http://localhost:3790/health # → { status: "ok" }
# 2. Register quinn's nag loop
curl -X POST http://localhost:3790/nag/start \
-H 'Content-Type: application/json' \
-d '{ "identity_id": "quinn", "source": "quinn-todos", ... }'
# → { id: "uuid", active: true, next_fire: "..." }
# 3. Watch for Redis events
redis-cli -p 26394 SUBSCRIBE ai.nag.fired
# 4. Wait one cron interval → event appears with generated message
# 5. Check status
curl http://localhost:3790/nag/status?identity_id=quinn
# → { loops: [{ ping_count: 1, last_message: "...", ... }] }
# 6. Stop
curl -X DELETE 'http://localhost:3790/nag/stop?identity_id=quinn&source=quinn-todos'
# → { stopped: true }
# 7. Run /nag-start in Claude Code → registers loop + testfires Miku TTS
```