Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
4.8 KiB
KeiSeiKit Architectural Decisions
ADR-style log. Each entry: context → decision → consequences. New entries at the top. Cross-link from
_primitives/_rust/<crate>/README.mdwhen a decision is crate-local.
2026-04-28 — Three scheduling abstractions in workspace
Context
After Hermes import (P4.2 kei-cron-scheduler) the KeiSeiKit workspace
contains three scheduler-like primitives. A naive audit reads this as
duplication; in practice each occupies a distinct layer of the stack and
removing any one would break a downstream consumer. This ADR documents the
boundary so a future reader does not consolidate them by mistake.
The three primitives
| Crate | Storage | Concurrency | Owns runner? | Canonical use |
|---|---|---|---|---|
kei-scheduler |
rusqlite (sync, metadata-only) |
sync | no | per-call queryable schedule index |
kei-cron-scheduler |
JSON-on-disk + fcntl advisory lock |
tokio async |
yes | Hermes parity (/schedule parser + cron loop) |
kei-pipe cron triggers |
embedded in pipe TOML | driven by pipe runtime | depends on pipe | pipeline-level cron embedded in a pipe definition |
Decision
Keep all three. Do not consolidate. Each abstraction encodes a different ownership contract and a different blast radius on failure.
Rationale, primitive by primitive
kei-scheduler — synchronous metadata-only store
Synchronous rusqlite schedule store. Stores cron expression, next-run
timestamp, owner, payload pointer. Does not dispatch — the caller asks
"what should I run between t and t+Δ" and the caller is responsible for
execution.
This separation matters because two callers want exactly that contract:
kei-pipequeries the schedule from the pipe-runtime loop (already its own scheduler) — it must not have a competing async runner inside the store.cron-wrapper-agenttest harness wants deterministic, blocking lookups with no background tasks. Atokioruntime would fight the harness.
A SQLite-backed metadata store is the smallest abstraction that satisfies
both callers. Any tokio infrastructure inside this crate would force its
contract on the harness and break determinism.
kei-cron-scheduler — async runner for Hermes parity
Async tokio-based runner. JSON-on-disk persistence (one file per job),
fcntl advisory lock to keep multiple binaries from racing the same job
file, owns its own loop, supports interval + standard 5-field cron. This
is the surface imported from Hermes (HERMES-MIGRATION-PLAN P4.2) — the
contract is "set-and-forget recurring scheduler with the runner inside the
crate."
A SQLite-only store like kei-scheduler cannot satisfy this contract:
- File-per-job is the unit of
fcntllocking; a single SQLite file would serialise all locks through the SQLite write mutex. - The runner is part of the public surface — Hermes callers expect to hand the crate a job and walk away. Splitting the runner into a separate crate would re-litigate the contract on every consumer.
kei-pipe cron triggers — pipeline-level cron embedded in a pipe
Pipes (KeiSei pipeline definitions, TOML) can declare a cron trigger inline. The pipe runtime evaluates the trigger as part of the pipe's own state machine, alongside event triggers, file-watch triggers, and HTTP triggers. The cron trigger is not a separate scheduler — it is a trigger source within the pipe runtime, which is itself the scheduler.
Re-implementing this on top of kei-cron-scheduler would either (a)
duplicate the pipe runtime's lifecycle into the cron crate, or (b) split
a single pipe's triggers across two runtimes, which loses the atomic
"trigger-fired-and-pipe-started" guarantee the pipe runtime provides.
Consequences
- Choosing the right primitive for a new caller. Decision tree:
- Need a recurring background runner with
fcntldurability and minimal blast radius if a single binary crashes? →kei-cron-scheduler. - Need a queryable index of "what should I run", with execution owned
elsewhere? →
kei-scheduler. - Trigger is one of many in a pipe definition, lives next to the data
flow, dies with the pipe? →
kei-pipecron trigger.
- Need a recurring background runner with
- Fail-loud overlap. If you find yourself porting a feature from one
to another (e.g. "let
kei-scheduleralso dispatch"), STOP — that is the No-Patching/No-Overlay smell from the umbrella rules. Add the feature to the right primitive instead, or write a new one. - Audit signal. A future audit may flag "three schedulers" as a code smell. This ADR is the canonical answer; link here from any review comment that surfaces the question again.
References
_primitives/_rust/kei-scheduler/_primitives/_rust/kei-cron-scheduler/_primitives/_rust/kei-pipe/(cron trigger source)HERMES-MIGRATION-PLAN.md§P4.2 — Hermes parity import