Skip to content

Commit fb53ccf

Browse files
committed
feat(vigil): add vigil persistence and dirge vigil CLI management
First slice of the vigil phase-5 decomposition: config types + SQLite store + CLI CRUD, independent of the runtime/keeper/triggers. - vigil_db: VigilStore (open/upsert/get/remove/set_status, list_non_resting) with Active/Paused/Resting status - config: VigilEntry/VigilTrigger/VigilRite/VigilCommand/SocketMode, gated on the `vigil` feature (opt-in, NOT in default) - cli: `dirge vigil` subcommand with VigilAction + VigilAddTrigger - main: handle_vigil_command + build_vigil_entry dispatch - tests: vigil_db store CRUD/status + build_vigil_entry parsing + config serde shape - docs/config.md: document the `vigils` top-level key
1 parent ebdeb78 commit fb53ccf

9 files changed

Lines changed: 663 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ no-plugin = [
129129
# The release workflow builds the Windows target with this set.
130130
windows-default = ['no-plugin']
131131
loop = []
132+
vigil = []
132133
# Run dirge itself as an MCP server (`dirge mcp`) so another agent (e.g.
133134
# Claude Code) can delegate implementation tasks to dirge and review them.
134135
# Pulls rmcp's server side + stdio transport + the tool macros.

docs/config.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ Accepted top-level keys:
147147
| `mcp_servers` | object | MCP server map when compiled with the `mcp` feature. When omitted, defaults to a single Exa Web Search server; see below. |
148148
| `acp_servers` | object | ACP server config map when compiled with the `acp` feature. See the ACP section below. |
149149
| `editor_open_command` | string | Opt-in editor follow-along: a command template with `{path}` and `{line}` placeholders (e.g. `"zed {path}:{line}"`, `"code --goto {path}:{line}"`). When set, dirge opens files it reads or edits in this external GUI editor, detached and non-blocking — the editor "follows along" like Zed's AI panel. `None` (unset) disables the feature entirely. |
150+
| `vigils` | array | Vigil definitions consulted only when `--vigil` is active (compiled with the `vigil` feature). Each entry: `name`, a `trigger` (`toll` timer with `interval_secs`, `watcher` on a `path`, or `harbinger` TCP socket with `address` and `socket_mode`), an optional `reap_interval_secs` (default 30), an optional `prompt` for the observance turn, and optional `procession` (Janet) and `rite` gate. |
150151

151152
### Desktop Notifications
152153

src/cli.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,48 @@ pub enum Command {
307307
#[arg(long = "sandbox")]
308308
sandbox: Option<String>,
309309
},
310+
/// Manage vigils — list, add, remove, pause, resume, restart.
311+
#[cfg(feature = "vigil")]
312+
Vigil {
313+
#[command(subcommand)]
314+
action: VigilAction,
315+
},
316+
}
317+
318+
/// Vigil management subcommands.
319+
#[cfg(feature = "vigil")]
320+
#[derive(clap::Subcommand, Debug)]
321+
pub enum VigilAction {
322+
/// List all configured vigils and their status.
323+
List,
324+
/// Add a new vigil trigger.
325+
Add {
326+
/// Vigil name.
327+
name: String,
328+
/// Trigger type: toll, watcher, or harbinger.
329+
#[arg(value_enum)]
330+
trigger: VigilAddTrigger,
331+
/// Additional trigger args as key=value pairs.
332+
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
333+
args: Vec<String>,
334+
},
335+
/// Remove a vigil by name.
336+
Remove { name: String },
337+
/// Pause a running vigil.
338+
Pause { name: String },
339+
/// Resume a paused vigil.
340+
Resume { name: String },
341+
/// Restart a vigil (stop and re-create its trigger).
342+
Rest { name: String },
343+
}
344+
345+
/// Trigger type for `dirge vigil add`.
346+
#[cfg(feature = "vigil")]
347+
#[derive(clap::ValueEnum, Debug, Clone)]
348+
pub enum VigilAddTrigger {
349+
Toll,
350+
Watcher,
351+
Harbinger,
310352
}
311353

312354
#[derive(clap::Subcommand, Debug)]

src/config/mod.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::collections::HashMap;
22
use std::path::{Path, PathBuf};
33

44
use serde::Deserialize;
5+
#[cfg(feature = "vigil")]
6+
use serde::Serialize;
57

68
use crate::session::storage;
79

@@ -1373,6 +1375,92 @@ pub struct Config {
13731375
/// future expansion but are not honored today.
13741376
#[cfg(feature = "acp")]
13751377
pub acp_servers: Option<HashMap<String, AcpServerConfig>>,
1378+
1379+
/// Vigil definitions loaded from `config.toml` under `[vigils.<name>]`.
1380+
/// Only consulted when `--vigil` is active.
1381+
#[cfg(feature = "vigil")]
1382+
#[serde(default)]
1383+
pub vigils: Option<Vec<VigilEntry>>,
1384+
}
1385+
1386+
/// A single vigil definition from config.
1387+
#[cfg(feature = "vigil")]
1388+
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1389+
#[serde(default)]
1390+
pub struct VigilEntry {
1391+
pub name: String,
1392+
pub trigger: VigilTrigger,
1393+
#[serde(default = "default_reap_interval")]
1394+
pub reap_interval_secs: u64,
1395+
#[serde(default)]
1396+
pub prompt: String,
1397+
/// Optional Janet script for per-observance procession.
1398+
#[serde(default)]
1399+
pub procession: Option<String>,
1400+
#[serde(default)]
1401+
pub rite: Option<VigilRite>,
1402+
}
1403+
1404+
#[cfg(feature = "vigil")]
1405+
fn default_reap_interval() -> u64 {
1406+
30
1407+
}
1408+
1409+
/// What triggers a vigil to fire.
1410+
#[cfg(feature = "vigil")]
1411+
#[derive(Debug, Clone, Deserialize, Serialize)]
1412+
#[serde(tag = "type", rename_all = "kebab-case")]
1413+
pub enum VigilTrigger {
1414+
/// Timer-based: fires every N seconds.
1415+
Toll { interval_secs: u64 },
1416+
/// Filesystem watcher: fires on changes under `path`.
1417+
Watcher { path: String },
1418+
/// Network socket: external process sends events to a TCP port.
1419+
Harbinger {
1420+
address: String,
1421+
#[serde(default)]
1422+
protocol: String,
1423+
/// `template` or `commands` — see `SocketMode`.
1424+
#[serde(default)]
1425+
socket_mode: SocketMode,
1426+
#[serde(default)]
1427+
commands: HashMap<String, VigilCommand>,
1428+
},
1429+
}
1430+
1431+
#[cfg(feature = "vigil")]
1432+
impl Default for VigilTrigger {
1433+
fn default() -> Self {
1434+
VigilTrigger::Toll { interval_secs: 30 }
1435+
}
1436+
}
1437+
1438+
/// Harbinger socket mode.
1439+
#[cfg(feature = "vigil")]
1440+
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)]
1441+
#[serde(rename_all = "kebab-case")]
1442+
pub enum SocketMode {
1443+
#[default]
1444+
Template,
1445+
Commands,
1446+
}
1447+
1448+
/// A pre-registered command for `commands` socket mode.
1449+
#[cfg(feature = "vigil")]
1450+
#[derive(Debug, Clone, Deserialize, Serialize)]
1451+
pub struct VigilCommand {
1452+
pub tool: String,
1453+
#[serde(default)]
1454+
pub args: serde_json::Map<String, serde_json::Value>,
1455+
}
1456+
1457+
/// Optional gate condition checked before an observance runs.
1458+
#[cfg(feature = "vigil")]
1459+
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1460+
pub struct VigilRite {
1461+
pub cmd: Option<String>,
1462+
#[serde(default)]
1463+
pub git_dirty: bool,
13761464
}
13771465

13781466
impl Config {

src/extras/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@ pub mod session_search;
4242
pub mod skill_db;
4343
pub mod skills;
4444
pub mod spec_db;
45+
pub mod vigil_db;

0 commit comments

Comments
 (0)