# Agent Runtime — Conversations, Skills & Scheduled Jobs

> End-to-end lifecycle of an agent run: persona templating (persona-manager.ts), conversation creation and transcript storage (conversation-store.ts, conversation-runner.ts), skill loading and scoping (skills/loader.ts, skills/scope.ts), heartbeat polling (heartbeat.ts), action parsing and dispatching (action-parser.ts, action-dispatcher.ts), and the cron-based job scheduler (jobs/job-manager.ts, job-normalization.ts). Includes the agent library of 20 pre-built templates.

- Repository: hilash/cabinet
- GitHub: https://github.com/hilash/cabinet
- Human wiki: https://grok-wiki.com/public/wiki/hilash-cabinet-73c70f449a59
- Complete Markdown: https://grok-wiki.com/public/wiki/hilash-cabinet-73c70f449a59/llms-full.txt

## Source Files

- `src/lib/agents/conversation-runner.ts`
- `src/lib/agents/conversation-store.ts`
- `src/lib/agents/persona-manager.ts`
- `src/lib/agents/skills/loader.ts`
- `src/lib/agents/skills/scope.ts`
- `src/lib/agents/action-dispatcher.ts`
- `src/lib/agents/heartbeat.ts`
- `src/lib/jobs/job-manager.ts`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [src/lib/agents/conversation-runner.ts](src/lib/agents/conversation-runner.ts)
- [src/lib/agents/conversation-store.ts](src/lib/agents/conversation-store.ts)
- [src/lib/agents/persona-manager.ts](src/lib/agents/persona-manager.ts)
- [src/lib/agents/persona-templating.ts](src/lib/agents/persona-templating.ts)
- [src/lib/agents/heartbeat.ts](src/lib/agents/heartbeat.ts)
- [src/lib/agents/skills/loader.ts](src/lib/agents/skills/loader.ts)
- [src/lib/agents/skills/scope.ts](src/lib/agents/skills/scope.ts)
- [src/lib/agents/action-parser.ts](src/lib/agents/action-parser.ts)
- [src/lib/agents/action-dispatcher.ts](src/lib/agents/action-dispatcher.ts)
- [src/lib/agents/library-manager.ts](src/lib/agents/library-manager.ts)
- [src/lib/jobs/job-manager.ts](src/lib/jobs/job-manager.ts)
- [src/lib/jobs/job-normalization.ts](src/lib/jobs/job-normalization.ts)
</details>

# Agent Runtime — Conversations, Skills & Scheduled Jobs

Cabinet's agent runtime coordinates every step between a user message (or cron trigger) and a persisted, structured result. It spans persona loading, prompt assembly, skill injection, daemon-session management, heartbeat scheduling, action parsing, and the cron-based job system. Understanding this pipeline is essential for extending agents, debugging failed runs, or reasoning about multi-agent task delegation.

This page follows the lifecycle end-to-end: how a persona is read and rendered into a system prompt, how a conversation is created and stored, how skills are resolved and injected, how the runtime polls for completion, how agents propose follow-on actions, and how those actions become new runs or persistent scheduled jobs.

---

## Overall Architecture

```text
                    ┌─────────────────────────────────────┐
                    │           Persona Manager            │
                    │  persona.md (gray-matter) → in-mem  │
                    │  resolution: cabinet-local > global  │
                    │  > any-cabinet scan                  │
                    └──────────────┬──────────────────────┘
                                   │ AgentPersona
                    ┌──────────────▼──────────────────────┐
                    │         conversation-runner           │
                    │  buildManualConversationPrompt()      │
                    │  buildEditorConversationPrompt()      │
                    │  startJobConversation()               │
                    │  ├── Skills: resolveDesiredSkills()  │
                    │  │          buildSkillIndex()         │
                    │  │          prepareSkillMount()       │
                    │  └── persona-templating.renderBody() │
                    └──────────────┬──────────────────────┘
                                   │ ConversationMeta
                    ┌──────────────▼──────────────────────┐
                    │         conversation-store            │
                    │  createConversation() → dir layout   │
                    │  meta.json / prompt.md / transcript  │
                    │  appendConversationTranscript()       │
                    │  finalizeConversation() → parse      │
                    │  cabinet block → artifact paths       │
                    └──────────────┬──────────────────────┘
                                   │
              ┌────────────────────┼──────────────────────┐
              │                    │                       │
  ┌───────────▼──────────┐  ┌──────▼──────────┐  ┌───────▼────────────┐
  │    heartbeat.ts       │  │  action-parser  │  │    job-manager     │
  │  runHeartbeat()       │  │  parseAgent     │  │  loadAllJobs()     │
  │  processOutput()      │  │  Actions()      │  │  executeJob()      │
  │  memory block parse   │  └──────┬──────────┘  │  saveAgentJob()    │
  │  goal/task/slack      │         │              └────────────────────┘
  └───────────────────────┘  ┌──────▼──────────┐
                             │ action-dispatcher│
                             │ dispatchApproved │
                             │ Actions()        │
                             │ LAUNCH_TASK      │
                             │ SCHEDULE_JOB     │
                             │ SCHEDULE_TASK    │
                             └─────────────────┘
```

---

## Persona Management

### Persona Files and the `AgentPersona` Structure

Every agent is backed by a `persona.md` file parsed with `gray-matter`. The frontmatter carries all runtime configuration; the markdown body is the agent's instruction text.

Key `AgentPersona` fields:

| Field | Type | Purpose |
|---|---|---|
| `slug` | `string` | Unique identifier; also the directory name |
| `name` | `string` | Display name |
| `role` | `string` | Short role description surfaced in the roster |
| `provider` | `string` | LLM provider id (e.g. `claude-code`) |
| `adapterType` | `string?` | Execution adapter override |
| `heartbeat` | `string` | Cron expression (`0 8 * * *` default) |
| `budget` | `number` | Max heartbeats per month (default: 100) |
| `active` / `heartbeatEnabled` | `boolean` | Controls cron registration |
| `workdir` | `string` | Working directory for adapter (`/data` default) |
| `skills` | `string[]?` | Skill keys attached to this persona |
| `canDispatch` | `boolean?` | May propose LAUNCH_TASK/SCHEDULE_JOB actions |
| `type` | `"lead"` \| `"specialist"` | `lead` defaults to `canDispatch: true` |
| `scope` | `"global"` \| `"cabinet"` | Computed from where the file was found |
| `body` | `string` | Raw markdown body (instructions) |

Sources: [src/lib/agents/persona-manager.ts:155-225](src/lib/agents/persona-manager.ts)

### Resolution Order

`readPersona(slug, preferredCabinetPath)` searches in this order:

1. **Cabinet-local**: `data/<cabinet>/.agents/<slug>/persona.md` — per-cabinet override
2. **Global tier**: `data/.global-agents/<slug>/persona.md` — shared across all cabinets; the `editor` agent is bootstrapped here at startup via `ensureGlobalAgents()`
3. **Any cabinet scan**: walks all discovered cabinet paths until a match is found

This means a cabinet-local persona always shadows a global persona of the same slug. The `editor` slug is the canonical generalist and is resolved as a global fallback from `readEditorConversationPrompt` when no cabinet-local editor exists.

Sources: [src/lib/agents/persona-manager.ts:80-106](src/lib/agents/persona-manager.ts)

### Persona Templating

`renderPersonaBody()` in `persona-templating.ts` substitutes `{{ key.path }}` placeholders at prompt-build time so persona files never bake in cabinet-specific names. Supported variables:

| Placeholder | Source |
|---|---|
| `{{cabinet.name}}` | Cabinet manifest name |
| `{{cabinet.slug}}` / `{{cabinet.path}}` | Cabinet identity |
| `{{user.name}}` | User profile display name |
| `{{agent.name}}` / `{{agent.slug}}` | Persona's own name and slug |
| `{{today}}` | ISO date (caller-supplied) |

Unknown placeholders are left intact rather than silently dropped.

Sources: [src/lib/agents/persona-templating.ts:1-57](src/lib/agents/persona-templating.ts)

---

## Conversation Lifecycle

### Prompt Construction

`buildManualConversationPrompt()` assembles the full system prompt by layering:

1. **Cabinet requirement header** — reminds the agent of the required `cabinet` fenced block
2. **Locale instructions** — `buildLocaleInstructions()` injects language and RTL frontmatter instructions (Hebrew, etc.)
3. **Agent context header** — `buildAgentContextHeader()` renders the persona body through the template engine
4. **Skill index** — `buildSkillIndex(bundles)` injects a compact list of attached skills
5. **KB scope instructions** — tells the agent its root data directory and cabinet scope
6. **Epilogue instructions** — the `cabinet` block spec, optional dispatch roster (LAUNCH_TASK / SCHEDULE_JOB), and the list of available teammates
7. **User request** — extracted mention context and attachment paths appended inline

`buildEditorConversationPrompt()` follows the same structure but also prepends the target page path. `startJobConversation()` uses the same layered approach, substituting `{{date}}`, `{{job.name}}`, `{{job.id}}`, and `{{job.workdir}}` into the job's prompt before assembly.

Sources: [src/lib/agents/conversation-runner.ts:278-380](src/lib/agents/conversation-runner.ts)

### Conversation Creation and Storage

`createConversation()` generates a deterministic ID and writes the following directory structure atomically:

```text
{conversations_dir}/{id}/
  meta.json          ← ConversationMeta (status, agentSlug, trigger, paths, tokens...)
  prompt.md          ← Full system prompt (as sent to the adapter)
  transcript.txt     ← Raw adapter output (appended live during streaming)
  mentions.json      ← Referenced KB page paths
  artifacts.json     ← Parsed ARTIFACT: entries from the cabinet block
```

The conversation ID encodes `{ISO-timestamp}-{sha1-cabinet-scope[8]}-{agentSlug}-{trigger}[-{jobName}]`. Cabinet scope is a SHA-1 hash so IDs remain unique across cabinets even when agent slugs collide.

`appendConversationTranscript()` appends raw output to `transcript.txt` and publishes a throttled `task.updated` SSE event (at most once per 500 ms) so the UI can stream partial output live.

Sources: [src/lib/agents/conversation-store.ts:261-322](src/lib/agents/conversation-store.ts), [src/lib/agents/conversation-store.ts:201-213](src/lib/agents/conversation-store.ts)

### Polling and Completion

After spawning the daemon session, `waitForConversationCompletion()` polls `getDaemonSessionOutput()` in two phases:

| Phase | Duration | Interval |
|---|---|---|
| Cold-start (fast poll) | First 5 seconds | 250 ms |
| Steady-state | Until 15-minute deadline | 700 ms |

On every poll, if the transcript grew, a `task.updated` event is published. When the daemon reports a terminal status (`completed` or `failed`), `finalizeConversation()` is called and a completion toast notification is enqueued.

Sources: [src/lib/agents/conversation-runner.ts:572-638](src/lib/agents/conversation-runner.ts)

### The Cabinet Block Protocol

Every agent turn must end with a fenced `cabinet` block:

```
```cabinet
SUMMARY: one short summary line
CONTEXT: optional memory note
ARTIFACT: path/to/file.md
ARTIFACT: path/to/other.md
```
```

`parseCabinetBlock()` extracts SUMMARY, CONTEXT, and all ARTIFACT paths from the last such block in the output. The parser handles:
- Prompt echo stripping (lines that appear verbatim in the system prompt are ignored)
- ANSI escape code removal
- Multi-file ARTIFACT lines (comma- or semicolon-separated, split and normalized)
- Placeholder detection (e.g. `SUMMARY: one short summary line` is treated as unfilled and dropped)
- Path guards: artifact paths must contain a directory separator or known file extension; prose-like values are rejected

If the run completes but no usable `cabinet` block is found, `isCabinetBlockMissing()` returns `true` and `waitForConversationCompletion()` issues a single automatic follow-up (`continueConversationRun`) asking the agent to emit only the block. A second miss is accepted as-is.

Sources: [src/lib/agents/conversation-store.ts:371-440](src/lib/agents/conversation-store.ts), [src/lib/agents/conversation-store.ts:601-612](src/lib/agents/conversation-store.ts)

### Multi-Turn Continuation

`continueConversationRun()` supports two modes:

- **Resume mode**: the underlying adapter session is still alive; only the epilogue instructions and any newly-mentioned skills are prepended before the follow-up user message, keeping the prompt compact.
- **Replay mode**: the session has expired or the adapter is stateless; the full system prompt (persona, skill index, KB scope, history serialized as `<turn-user>/<turn-assistant>` XML) is rebuilt from scratch.

Session codec blobs (for providers that carry opaque resume state) are persisted to `session.json` adjacent to the conversation directory and read back on the next continuation.

Sources: [src/lib/agents/conversation-runner.ts:700-800](src/lib/agents/conversation-runner.ts)

---

## Skills System

### Origins and Precedence

`listSkills()` / `readSkill()` walk five origins in priority order:

| Priority | Origin key | Physical location |
|---|---|---|
| 1 (highest) | `cabinet-scoped` | `data/<cabinet>/.agents/skills/<key>/` |
| 2 | `cabinet-root` | `<project>/.agents/skills/<key>/` |
| 3 | `linked-repo` | (reserved; not yet wired) |
| 4 | `system` | `~/.claude/skills/`, `~/.agents/skills/`, Claude plugin marketplace layouts |
| 5 (lowest) | `legacy-home` | `~/.cabinet/skills/<key>/` |

When the same key exists at multiple levels, the highest-precedence entry wins. Claude plugin marketplace skills are tagged with `pluginSource` (marketplace + plugin name) to avoid key collisions between plugins.

Sources: [src/lib/agents/skills/loader.ts:230-275](src/lib/agents/skills/loader.ts)

### Skill Bundles and File Inventory

Each skill directory is scanned by `walkBundle()` which classifies every file:

| Kind | Criteria |
|---|---|
| `skill` | Exactly `SKILL.md` |
| `reference` | Under `references/` or `rules/` |
| `asset` | Under `assets/`, or image/data extensions |
| `script` | Under `scripts/`, executable bit set, or script extensions (`.sh`, `.py`, `.ts`, etc.) |
| `markdown` | `.md` files |
| `other` | Everything else |

`deriveTrustLevel()` rolls up the inventory: `markdown_only` if all files are markdown, `assets` if any non-markdown non-script files exist, `scripts_executables` if any scripts are present. The trust level surfaces in the skill picker UI but does not gate runtime loading.

Sources: [src/lib/agents/skills/loader.ts:47-100](src/lib/agents/skills/loader.ts)

### Skill Loading at Run Time

The conversation runner calls `resolveDesiredSkills(mergedKeys, cabinetPath)` to hydrate slug lists into `SkillBundle[]`. The merged key set combines:
- The persona's persisted `skills:` frontmatter list
- Any `@skill-name` mentions from the composer (run-only, not persisted)

`buildSkillIndex(bundles)` formats a compact bullet list injected into the system prompt so the model knows which skills are available. The physical skill directories are separately mounted into a per-session tmpdir via `prepareSkillMount()` and forwarded to the adapter (e.g. via `--add-dir` for Claude Code). The tmpdir is cleaned up when `finalizeConversation()` runs.

Sources: [src/lib/agents/skills/loader.ts:305-360](src/lib/agents/skills/loader.ts), [src/lib/agents/conversation-runner.ts:375-420](src/lib/agents/conversation-runner.ts)

### Scope Validation

`resolveSkillsScopeRoot()` in `skills/scope.ts` validates API-provided scope strings:

- `"root"` or `undefined` → `<project>/.agents/skills/`
- `"cabinet:<path>"` → validated through `resolveContentPath()` which enforces the `DATA_DIR` boundary (no `../` traversal out of the data directory)

Skill key validation: `isValidSkillKey()` accepts only `[a-z0-9][a-z0-9-]*`, max 64 characters.

Sources: [src/lib/agents/skills/scope.ts:1-55](src/lib/agents/skills/scope.ts)

---

## Heartbeat System

### Scheduling

`registerHeartbeat(slug, cronExpr)` wraps `node-cron.schedule()`. On app boot, `registerAllHeartbeats()` reads all personas and registers cron jobs for those where `active && heartbeatEnabled && heartbeatsUsed < budget`. Budget is tracked in `memory/stats.json` alongside `lastHeartbeat`. The `nextHeartbeat` field on `AgentPersona` is computed from the cron expression and `lastHeartbeat` via `computeNextCronRun()`.

Sources: [src/lib/agents/persona-manager.ts:390-420](src/lib/agents/persona-manager.ts)

### Context Assembly

`buildHeartbeatContext()` assembles the heartbeat prompt by reading:

| Source | Description |
|---|---|
| `memory/context.md` | Rolling context from previous heartbeats (last 20 entries) |
| `memory/decisions.md` | Append-only log of key decisions |
| `memory/learnings.md` | Long-term insights |
| Inbox | Unread messages from other agents |
| Focus-area `index.md` files | First 500 bytes of each focus path |
| Goal state | Current values from `goal-manager.ts` against targets/floors |
| Task inbox | Pending and in-progress tasks assigned to this agent |

The persona body goes through `renderPersonaBody()` with cabinet name, user name, and today's date substituted in.

Sources: [src/lib/agents/heartbeat.ts:12-100](src/lib/agents/heartbeat.ts)

### The Memory Block Protocol

The agent's heartbeat response is expected to include a `memory` fenced block:

```
```memory
CONTEXT_UPDATE: What happened this heartbeat.
DECISION: A key decision, with reasoning.
LEARNING: A new long-term insight.
GOAL_UPDATE [metric_name]: +N
MESSAGE_TO [agent-slug]: Message text.
SLACK [channel-name]: Message to post.
TASK_CREATE [target-agent] [priority]: title | description
TASK_COMPLETE [task-id]: result summary
```
```

`processHeartbeatOutput()` parses this block and dispatches each directive:

- `CONTEXT_UPDATE` → appends to `context.md`, trims to last 20 entries
- `DECISION` / `LEARNING` → append-only writes to their respective files
- `GOAL_UPDATE` → calls `updateGoal()` to increment metrics
- `MESSAGE_TO` → calls `sendMessage()` to write to another agent's inbox
- `SLACK` → calls `postMessage()` to Agent Slack
- `TASK_CREATE` → creates a structured task handoff with `createTask()`
- `TASK_COMPLETE` → marks a task done with `updateTask()`

Goal floor alerts fire a Slack alert to the `alerts` channel when a metric is below its floor with ≥80% of the period elapsed.

**Auto-pause**: after 3 consecutive heartbeat failures, the persona is written back with `active: false` and a Slack alert is posted. Failure recovery requires manual re-activation.

Sources: [src/lib/agents/heartbeat.ts:130-230](src/lib/agents/heartbeat.ts)

---

## Action Parsing and Dispatching

### Parsing Agent Actions

`parseAgentActions(output, prompt)` extracts structured actions from agent output in three passes, preferring more-explicit forms:

1. **`cabinet-actions` JSON blocks** (highest authority): parsed first; supports multi-line prompts and full JSON field names. An array of action objects or a `{"actions": [...]}` wrapper.
2. **Inline markers inside `cabinet` blocks**: `LAUNCH_TASK: <slug> | <title> | <prompt>`, `SCHEDULE_JOB: <slug> | <name> | <cron> | <prompt>`, `SCHEDULE_TASK: <slug> | <ISO> | <title> | <prompt>`
3. **Stray inline markers**: same syntax, outside any fenced block (fallback for CLIs that strip fencing)

Deduplication: each parsed action is fingerprinted (type + agent + title/name + prompt hash); duplicates and prompt-echo actions (fingerprints that appear verbatim in the system prompt) are silently dropped. A maximum of `MAX_ACTIONS_PER_TURN` actions are kept; additional ones set `truncated: true`.

Runtime hints (`model=`, `effort=`, `provider=`, `adapterType=`) can be appended as `key=value` segments after the pipe-delimited fields.

Sources: [src/lib/agents/action-parser.ts:1-80](src/lib/agents/action-parser.ts)

### Dispatch Types and Runtime Inheritance

`dispatchApprovedActions()` iterates human-approved actions and dispatches each:

```mermaid
stateDiagram-v2
    [*] --> PendingAction : agent proposes (cabinet block)
    PendingAction --> AwaitingApproval : finalizeConversation
    AwaitingApproval --> Dispatched : human approves
    AwaitingApproval --> Rejected : unknown agent / bad params
    Dispatched --> RunningConversation : LAUNCH_TASK
    Dispatched --> ScheduledJob : SCHEDULE_JOB
    Dispatched --> ScheduledTask_OneShot : SCHEDULE_TASK (future)
    Dispatched --> RunningConversation : SCHEDULE_TASK (≤60s)
    RunningConversation --> [*]
    ScheduledJob --> [*]
    ScheduledTask_OneShot --> [*]
```

**Runtime inheritance** for sub-tasks follows this precedence (highest → lowest):

1. Action-authored override (agent explicitly set `model=` / `effort=` on the action)
2. Parent conversation's runtime (provider + model + effort propagate by default)
3. Target persona's defaults

Model strings are only inherited when the resolved provider matches the parent's (cross-provider model IDs are rejected). Effort levels are portable when the target provider declares support for that level.

After dispatching LAUNCH_TASK, `tagLineage()` writes `parentTaskId`, `triggeringAgent`, and `spawnDepth` onto the spawned conversation's metadata.

SCHEDULE_TASK that is more than 60 s in the future is converted to a one-shot cron job (`oneShot: true`, `runAfter: ISO`) rather than a standing recurring schedule.

Sources: [src/lib/agents/action-dispatcher.ts:20-80](src/lib/agents/action-dispatcher.ts), [src/lib/agents/action-dispatcher.ts:103-200](src/lib/agents/action-dispatcher.ts)

---

## Cron-Based Job Scheduler

### Job Storage

Jobs are persisted as YAML files at `{cabinet}/.jobs/{id}.yaml`. Each file contains a normalized `JobConfig`:

| Field | Default | Description |
|---|---|---|
| `id` | kebab-case from `name` | Unique identifier |
| `name` | — | Human-readable label |
| `schedule` | `"0 9 * * *"` | Cron expression |
| `enabled` | `true` | Whether cron fires this job |
| `provider` | `"claude-code"` | LLM provider |
| `adapterType` | derived from `provider` | Execution adapter |
| `timeout` | `600` (seconds) | Run-level timeout |
| `prompt` | — | Template-variable-aware instructions |
| `on_complete` / `on_failure` | `[]` | Post-action hooks (e.g. `git_commit`) |
| `oneShot` | `false` | Disable after first fire |
| `runAfter` | — | ISO datetime guard for one-shot tasks |
| `ownerTaskId` | — | Traceability back to the spawning conversation |
| `cabinetPath` | — | Cabinet scope |

`normalizeJobConfig()` validates and applies all defaults; `normalizeJobId()` produces a kebab-case ID from the `name` when `id` is absent. On every `loadNormalizedJobFile()` call the YAML is re-written if normalization changed anything, keeping files on disk always canonical.

Sources: [src/lib/jobs/job-normalization.ts:1-105](src/lib/jobs/job-normalization.ts), [src/lib/jobs/job-manager.ts:55-105](src/lib/jobs/job-manager.ts)

### Execution

`executeJob(job)` → `startJobConversation(job)` → `startConversationRun(...)`. The job prompt has `{{date}}`, `{{datetime}}`, `{{job.name}}`, `{{job.id}}`, and `{{job.workdir}}` substituted at fire time. Post-actions (currently only `git_commit`) run on completion or failure via `processPostActions()`.

`initScheduler()` and `scheduleJob()` both delegate to `reloadDaemonSchedules()` — the in-process cron map is managed by the Cabinet daemon process (which also owns the `node-cron` jobs for heartbeats), so schedule changes are applied by sending a reload signal rather than directly mutating the map from Next.js.

Sources: [src/lib/jobs/job-manager.ts:130-175](src/lib/jobs/job-manager.ts), [src/lib/agents/conversation-runner.ts:840-900](src/lib/agents/conversation-runner.ts)

---

## Agent Library Templates

Cabinet ships **42 pre-built persona templates** under `src/lib/agents/library/` (seeded at runtime into `data/.agents/.library/`). Each template is a `persona.md` with frontmatter and a rich instruction body. They span professional, personal, and creative domains:

**Executive / Business:** `ceo`, `coo`, `cto`, `cfo`, `product-manager`, `legal`, `people-ops`, `sales`, `customer-success`

**Content / Writing:** `editor` (global default generalist), `copywriter`, `writing-coach`, `content-marketer`, `script-writer`, `post-optimizer`, `social-media`, `seo`

**Research / Analysis:** `researcher`, `research-lead`, `data-analyst`, `lit-reviewer`, `trend-scout`, `citation-keeper`, `note-synthesizer`

**Engineering / Design:** `devops`, `qa`, `tinkerer`, `ux-designer`, `image-creator`

**Personal / Life:** `home-manager`, `meal-planner`, `grocery-buyer`, `budget-keeper`, `calendar-keeper`, `habit-tracker`, `planner`, `kid-coordinator`, `inbox-triage`

**Education / Knowledge:** `teaching-assistant`, `librarian`, `assistant`, `growth-marketer`

`resolveAgentLibraryDir()` checks the seeded data path first, falling back to the source tree so templates are always available in development. `ensureGlobalAgents()` bootstraps the `editor` as a shared global agent on first boot, since it serves as the fallback generalist across all cabinets.

Sources: [src/lib/agents/library-manager.ts:14-50](src/lib/agents/library-manager.ts), [src/lib/agents/library-manager.ts:120-160](src/lib/agents/library-manager.ts)

---

## Summary

The agent runtime is a layered pipeline where each module has a single, well-bounded responsibility. `persona-manager.ts` owns file-backed identity and cron registration; `conversation-runner.ts` assembles prompts and drives the daemon lifecycle; `conversation-store.ts` owns all durable conversation state and the `cabinet` block parsing protocol; the skills subsystem resolves a precedenced multi-origin catalog and injects it as both a prompt index and a mounted directory; `heartbeat.ts` drives proactive agent behavior and the structured `memory` block protocol; `action-parser.ts` + `action-dispatcher.ts` implement the human-in-the-loop proposal→approval→spawn flow for multi-agent delegation; and `job-manager.ts` + `job-normalization.ts` provide the cron-backed scheduled task layer. The library of 42 templates makes this infrastructure immediately usable for a wide range of roles.
