Skip to content

Commit ab4f350

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 33c95c5 commit ab4f350

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

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

13721460
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)