docs: add Phase 5a design and implementation plans

This commit is contained in:
William Valentin
2026-02-05 22:10:41 -08:00
parent 7c41ffad71
commit 224c023028
2 changed files with 2339 additions and 0 deletions
@@ -0,0 +1,230 @@
# Phase 5a Design: CLI Surface, Cron/Scheduling, Doctor Diagnostics
## Overview
Three independent Phase 5 features that make Flynn usable as a proper CLI tool and proactive agent. CLI provides the command interface, cron makes Flynn schedule-driven, doctor validates setup.
**Dependencies**: Phases 1-4 complete. 298 tests passing. Typecheck clean.
---
## 1. CLI Surface
### Architecture
Single entry point at `src/cli/index.ts` replaces both `src/index.ts` and `src/tui.ts`. Uses **commander** for subcommand routing. `package.json` gets a `bin` field pointing to `dist/cli/index.js`.
### Commands
| Command | Description | Notes |
|---------|-------------|-------|
| `flynn start` | Start daemon (foreground) | Delegates to `startDaemon()` |
| `flynn tui` | Launch TUI | Moves existing `src/tui.ts` logic |
| `flynn send <message>` | One-shot message, print response, exit | Standalone (no daemon needed) |
| `flynn sessions` | List active sessions | Reads from SessionStore directly |
| `flynn doctor` | Validate config + health | See Section 3 |
| `flynn config` | Show resolved config (redacted secrets) | Loads and displays |
| `flynn version` | Print version | From package.json |
### File Structure
```
src/cli/
├── index.ts # Commander setup, bin entry point
├── start.ts # 'flynn start' -- daemon startup
├── tui.ts # 'flynn tui' -- moves existing TUI logic
├── send.ts # 'flynn send' -- one-shot agent invocation
├── sessions.ts # 'flynn sessions' -- list/inspect sessions
├── doctor.ts # 'flynn doctor' -- diagnostics
├── config-cmd.ts # 'flynn config' -- show resolved config
└── shared.ts # Config loading, output formatting, error handling
```
### Key Decisions
- **`flynn send` works without a daemon**: Creates a lightweight agent inline (like current TUI does), processes the message, prints the response, exits. No session persistence.
- **`flynn start` replaces `src/index.ts`**: The old entry point becomes the `start` subcommand.
- **`flynn tui` replaces `src/tui.ts`**: The old TUI entry point becomes the `tui` subcommand with `--fullscreen` flag preserved.
- **`package.json` `bin` field**: `"bin": { "flynn": "dist/cli/index.js" }` enables `npx flynn` and global install.
- **Shebang**: `#!/usr/bin/env node` at top of `src/cli/index.ts`.
### Config Loading in CLI
`shared.ts` exports `loadConfigSafe()` which:
1. Checks `FLYNN_CONFIG` env var or `~/.config/flynn/config.yaml`
2. Returns `{ config, error }` instead of throwing
3. Used by commands that need config (start, send, tui, doctor)
4. Commands that don't need config (version) skip it
---
## 2. Cron/Scheduling
### Architecture
`CronScheduler` implements `ChannelAdapter` from `src/channels/types.ts`. It registers with the `ChannelRegistry` like any other adapter. Each cron job gets a session ID of `cron:<job-name>`.
When a job fires:
1. Emits `InboundMessage` through the channel adapter's `onMessage` handler
2. Channel registry routes to unified message handler -> `NativeAgent`
3. Agent processes and responds
4. `CronScheduler.send()` receives the response
5. Looks up the configured output channel from the channel registry
6. Forwards the response to the output channel+peer
### Config Schema
```typescript
const cronJobSchema = z.object({
name: z.string().min(1),
schedule: z.string().min(1), // Cron expression: "0 9 * * *"
message: z.string().min(1), // Message to send to agent
output: z.object({
channel: z.string().min(1), // e.g. "telegram"
peer: z.string().min(1), // e.g. "12345" (chat ID)
}),
enabled: z.boolean().default(true),
timezone: z.string().optional(), // default UTC
});
// Top-level config addition:
automation: z.object({
cron: z.array(cronJobSchema).default([]),
}).default({})
```
### Example Config
```yaml
automation:
cron:
- name: morning-briefing
schedule: "0 9 * * *"
message: "Give me a morning briefing: weather, calendar, news"
output:
channel: telegram
peer: "12345"
- name: daily-backup-check
schedule: "0 22 * * *"
message: "Check if today's backups completed successfully"
output:
channel: telegram
peer: "12345"
enabled: true
timezone: "America/New_York"
```
### Library
**croner** -- lightweight, zero-dep cron scheduler for Node.js. ESM-native, timezone support, cron expression validation.
### File Structure
```
src/automation/
├── cron.ts # CronScheduler (ChannelAdapter) + CronJob management
```
Types are inline (no separate types.ts needed for this scope).
### Lifecycle
- `connect()`: Creates `Cron` instances for each enabled job
- `disconnect()`: Stops all cron instances
- Registers shutdown handler via daemon lifecycle
- Channel registry manages start/stop
### Output Forwarding
`CronScheduler` holds a reference to `ChannelRegistry`. When `send(peerId, message)` is called with the agent's response:
- `peerId` is the cron job name
- Looks up the job's output config
- Gets the output channel adapter from registry
- Calls `outputAdapter.send(job.output.peer, message)`
- If output channel is unavailable, logs warning (don't crash)
---
## 3. Doctor Diagnostics
### Architecture
`flynn doctor` validates configuration and checks system health. Runs standalone (no daemon needed). Each check is independent and produces a status line.
### Checks
| # | Check | Pass criteria | Depends on |
|---|-------|--------------|------------|
| 1 | Config file exists | File present at expected path | Nothing |
| 2 | Config parses | Valid YAML syntax | #1 |
| 3 | Config validates | Zod schema passes | #2 |
| 4 | Env vars resolved | No unresolved `${VAR}` refs | #2 |
| 5 | Data directory writable | `~/.local/share/flynn` write test | Nothing |
| 6 | Session DB accessible | SQLite open + query | #5 |
| 7 | Model connectivity | Default model responds | #3 |
| 8 | Telegram bot valid | `getMe` API call succeeds | #3 |
| 9 | MCP servers available | Configured servers can start | #3 |
| 10 | Skills loaded | All skill dirs load without error | #3 |
### Output Format
```
Flynn Doctor
============
[PASS] Config file exists (~/.config/flynn/config.yaml)
[PASS] Config parses (valid YAML)
[FAIL] Config validates: telegram.bot_token is required
[SKIP] Env vars resolved (config invalid)
[PASS] Data directory writable (~/.local/share/flynn)
[PASS] Session DB accessible (sessions.db)
[SKIP] Model connectivity (config invalid)
[SKIP] Telegram bot valid (config invalid)
[SKIP] MCP servers available (config invalid)
[SKIP] Skills loaded (config invalid)
Results: 3 passed, 1 failed, 6 skipped
```
### Implementation
```typescript
interface CheckResult {
status: 'pass' | 'fail' | 'warn' | 'skip';
label: string;
detail?: string;
}
type Check = (ctx: DoctorContext) => Promise<CheckResult>;
interface DoctorContext {
configPath: string;
config?: Config; // Set after config checks pass
dataDir: string;
}
```
Each check is a function. Checks that depend on config are skipped if config loading failed. Exit code 0 if all pass/warn/skip, 1 if any fail.
### Model Connectivity Check
Sends a minimal `chat()` request: `{ messages: [{ role: 'user', content: 'ping' }], max_tokens: 1 }`. Success = any response without error. Timeout: 10 seconds.
---
## Implementation Order
1. **CLI surface first** -- foundation for the other two
2. **Doctor diagnostics** -- uses CLI, no daemon dependency
3. **Cron/scheduling** -- needs daemon running, builds on channel system
## New Dependencies
- `commander` -- CLI framework (~50KB)
- `croner` -- Cron scheduler (~15KB, zero deps)
---
*Design Version: 1.0*
*Created: 2026-02-05*
*Features: CLI surface, cron/scheduling, doctor diagnostics*
File diff suppressed because it is too large Load Diff