diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d358e3..b8536a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ All notable changes to Flynn are documented in this file. ### Added +- **Operator Setup Pack + Strict Doctor Mode** -- `flynn setup` Automation now + supports an Operator Pack that preconfigures backup scheduling/notifications, + heartbeat alerts, daily briefing, and MinIO sync with configurable output + channel/peer routing. `flynn doctor --strict` now treats warnings as failures + for CI and preflight gating workflows. + +- **Heartbeat Notification Cooldown Config** -- `automation.heartbeat.notify_cooldown` + adds throttling for repeated failure/recovery alerts (default `30m`) to reduce + noisy notifications during unstable periods. + - **Native Audio Support** -- Smart routing for voice messages: audio-capable models (Gemini, OpenAI, GitHub) receive raw audio directly via `AudioSource` content parts; non-audio models (Anthropic, Bedrock, Ollama, llama.cpp) get Whisper transcription @@ -130,6 +140,9 @@ All notable changes to Flynn are documented in this file. ### Fixed +- **Heartbeat Cooldown Parse Resilience** -- Invalid `automation.heartbeat.notify_cooldown` + values no longer throw during alerting; monitor falls back to `30m` and logs a warning. + - **Voice Message Failure Handling** -- Telegram voice/audio messages now send user feedback on download failures instead of silently dropping. When audio transcription is not configured for non-audio-capable models, a graceful error message is sent to the user instead of diff --git a/README.md b/README.md index 5edb5ca..71795e7 100644 --- a/README.md +++ b/README.md @@ -771,6 +771,35 @@ Repeated failure/recovery notifications are throttled by `notify_cooldown`. `flynn setup` now includes an Operator Pack option in Automation that preconfigures scheduled backups, heartbeat alerts, a daily briefing, and a default MinIO sync task, with prompts for output channel/peer routing. +Example Operator Pack output routing: + +```yaml +backup: + enabled: true + schedule: "0 2 * * *" + notify: + channel: telegram + peer: "123456789" + +automation: + heartbeat: + enabled: true + notify: + channel: telegram + peer: "123456789" + notify_cooldown: "30m" + daily_briefing: + enabled: true + output: + channel: telegram + peer: "123456789" + minio_sync: + enabled: true + notify: + channel: telegram + peer: "123456789" +``` + ## Gmail Pub/Sub Watcher Monitor a Gmail inbox and forward new messages into the agent pipeline. diff --git a/docs/plans/state.json b/docs/plans/state.json index ed3c561..b9a6d60 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -193,8 +193,9 @@ "status": "completed", "date": "2026-02-16", "updated": "2026-02-16", - "summary": "Implemented operator-focused hardening and onboarding polish: added setup Operator Pack flows in both Automation menu and first-run wizard to preconfigure scheduled backups, heartbeat alerts, daily briefing, and default MinIO sync; added operator-pack prompts for output channel/peer routing; added heartbeat notification throttling via `automation.heartbeat.notify_cooldown` (with invalid-value fallback handling); and added `flynn doctor --strict` to treat warnings as failures. Updated docs/default config examples and onboarding tests accordingly.", + "summary": "Implemented operator-focused hardening and onboarding polish: added setup Operator Pack flows in both Automation menu and first-run wizard to preconfigure scheduled backups, heartbeat alerts, daily briefing, and default MinIO sync; added operator-pack prompts for output channel/peer routing with redundant prompt skip when already configured; added heartbeat notification throttling via `automation.heartbeat.notify_cooldown` (with invalid-value fallback handling); and added `flynn doctor --strict` to treat warnings as failures. Added docs/changelog updates and a consolidated regression run record.", "files_modified": [ + "CHANGELOG.md", "src/cli/setup/config.ts", "src/cli/setup/config.test.ts", "src/cli/setup/automation.test.ts", @@ -203,6 +204,7 @@ "src/cli/setup/integration.test.ts", "src/cli/setup/summary.ts", "src/cli/setup/summary.test.ts", + "src/cli/index.test.ts", "src/automation/heartbeat.ts", "src/automation/heartbeat.test.ts", "src/config/schema.ts", @@ -213,7 +215,7 @@ "README.md", "docs/plans/state.json" ], - "test_status": "pnpm test:run src/cli/setup/summary.test.ts src/cli/setup/integration.test.ts src/cli/setup/automation.test.ts src/cli/setup/config.test.ts src/automation/heartbeat.test.ts src/config/schema.test.ts src/cli/doctor.test.ts + pnpm typecheck passing" + "test_status": "pnpm test:run src/cli/index.test.ts src/cli/setup/automation.test.ts src/cli/setup/integration.test.ts src/cli/setup/summary.test.ts src/automation/heartbeat.test.ts src/config/schema.test.ts src/cli/doctor.test.ts + pnpm typecheck passing" }, "backup-session-summary-audit-trail": { "status": "completed", @@ -3499,7 +3501,7 @@ } }, "overall_progress": { - "total_test_count": 1870, + "total_test_count": 1872, "all_tests_passing": true, "p0_completion": "3/3 (100%)", "p1_completion": "4/4 (100%)", diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index df3cf11..fcb19b9 100644 --- a/src/cli/index.test.ts +++ b/src/cli/index.test.ts @@ -21,6 +21,13 @@ describe('CLI program', () => { expect(commandNames).toContain('zai-auth'); }); + it('registers doctor strict flag on doctor command', () => { + const program = createProgram(); + const doctor = program.commands.find((c) => c.name() === 'doctor'); + const strictOption = doctor?.options.find((o) => o.long === '--strict'); + expect(strictOption).toBeDefined(); + }); + it('has version info', () => { const program = createProgram(); expect(program.version()).toBeDefined(); diff --git a/src/cli/setup/automation.test.ts b/src/cli/setup/automation.test.ts index 86998e9..f1c9519 100644 --- a/src/cli/setup/automation.test.ts +++ b/src/cli/setup/automation.test.ts @@ -127,4 +127,34 @@ describe('setupAutomation', () => { expect(automation?.heartbeat).toBeUndefined(); expect(automation?.minio_sync).toBeUndefined(); }); + + it('skips operator-pack reconfiguration prompts when already configured and not reconfigured', async () => { + const builder = new ConfigBuilder(); + builder.applyOperatorPack({ + outputChannel: 'telegram', + outputPeer: '123', + backupSchedule: '0 2 * * *', + dailyBriefingSchedule: '0 8 * * *', + enableMinioSync: true, + }); + + const p = makePrompter({ + confirms: [ + false, // reconfigure operator pack? + false, // enable cron scheduler + false, // enable webhook receiver + false, // configure google services + ], + }); + + await setupAutomation(p, builder); + + const config = builder.build() as Record; + const backup = config.backup as Record; + const automation = config.automation as Record; + const briefing = automation.daily_briefing as Record; + + expect(backup.schedule).toBe('0 2 * * *'); + expect(briefing.schedule).toBe('0 8 * * *'); + }); }); diff --git a/src/cli/setup/automation.ts b/src/cli/setup/automation.ts index bca3cfc..1f3344e 100644 --- a/src/cli/setup/automation.ts +++ b/src/cli/setup/automation.ts @@ -57,12 +57,26 @@ const GOOGLE_SERVICES: GoogleService[] = [ ]; export async function setupAutomation(p: Prompter, builder: ConfigBuilder): Promise { - const enableOperatorPack = await p.confirm( - 'Enable operator automation pack (scheduled backups + heartbeat alerts + daily briefing + MinIO sync)?', - false, + const config = builder.build(); + const automation = (config.automation ?? {}) as Record; + const backup = (config.backup ?? {}) as Record; + const operatorPackConfigured = Boolean( + backup.enabled === true + && (automation.heartbeat as { enabled?: boolean } | undefined)?.enabled + && (automation.daily_briefing as { enabled?: boolean } | undefined)?.enabled, ); - if (enableOperatorPack) { - const config = builder.build(); + + const configureOperatorPack = operatorPackConfigured + ? await p.confirm( + 'Operator automation pack appears enabled. Reconfigure operator pack settings now?', + false, + ) + : await p.confirm( + 'Enable operator automation pack (scheduled backups + heartbeat alerts + daily briefing + MinIO sync)?', + false, + ); + + if (configureOperatorPack) { const telegramPeer = config.telegram?.allowed_chat_ids?.[0]; const defaultOutputChannel = telegramPeer ? 'telegram' : 'webchat'; const defaultOutputPeer = telegramPeer ? String(telegramPeer) : 'operator';