Imported from maxswjeon/cadence (
AGENTS.md). Install upstream withnpx skills add maxswjeon/cadence. Copyright stays with the author.
AGENTS.md — Cadence contributor/agent guide
This file is for anyone (human or agent) extending Cadence's Milestone 1 foundation spine. Read README.md first for what's built; this file covers the invariants you must uphold, how to extend the code, and what not to touch.
Architecture invariants (non-negotiable)
These are enforced in code and by tests — do not weaken them to make a feature "just work."
-
D1/R2 raw boundary — no verbatim raw content in D1 or R2. D1 (
cadence/stores/d1.py) and R2 (cadence/stores/r2.py) may hold only structured/derived fields, source-event IDs, hashes, confidence values, timestamps, and short non-verbatim summaries. Two layers enforce this (cadence/stores/raw_boundary.py):SchemaBoundary— the real guarantee. Built from the ORM metadata (_schema_boundary_from_modelsincadence/stores/d1.py), it knows every D1 column and the shape of value each may carry (ColumnKind:SCALAR/JSON/ID/HASH/ENUM/LABEL/SUMMARY). A write may reference only known columns, and free-text is accepted only on columns typedLABEL/SUMMARY/ENUM— a rawTextcolumn with no such typing, or an unknown column entirely, is rejected.D1Store.enforce_boundarycallsSchemaBoundary.enforce_rowon every write;CloudflareD1Replica.enqueuecalls it again before queuing.PayloadClassifier— a heuristic backstop for callers with no table context: a field-name denylist (_RAW_FIELD_TOKENSplus substring matches likeemailbody), separator-tolerant + Luhn-checked account/card-number detection, and base64-blob detection. A violation raisesRawBoundaryViolationand fires theraw_to_cloud_violationalarm. Seetests/test_raw_boundary.py,tests/test_data_layer_hardening.py, and the end-to-end check intests/test_e2e.py. If you add a column, it is picked up automatically bySchemaBoundaryfrom the model metadata — pick a name/type that reflects its real shape (don't declare a raw-text column and expect the boundary to catch it after the fact).
-
Local-canonical D1 — no cloud on the hot path. Every read/write in M1 hits the local SQLite file (
Settings.d1_path/D1Store.engine) directly. TheCloudflareD1Replicaincadence/stores/d1.pyis written to asynchronously (replicate/enqueue) and modeled as a durable off-site copy / degraded-mode read source — it is never on the decision path. Do not add code that reads from or blocks on the replica during normal ingestion. -
Provenance on every derived fact. Anything asserted into the fact graph (
cadence/brain/facts.py) must carrysource_event_ids, a provenance pointer (raw_evidence_id/raw_evidence_hashinto NAS), aconfidence_value+confidence_type, and (where applicable)expires_at.FactGraph.assert_factdedupes on a stablededupe_keyand merges provenance into an existing fact rather than duplicating it — preserve that merge behavior if you touch this path. Feedback history lives in the separatefeedbacktable, not inline on the fact. The same discipline extends to typed projected rows (cadence/brain/projection.py): every projector attaches_provenance(event)(confidence, source-event ids, NAS pointer, summary) to the row it emits. -
Credential vault is NAS-only — never cloud.
CredentialVault(cadence/adapters/base.py) implementations must store secrets only under a NAS-local path.FileCredentialVault(cadence/adapters/vault.py) positively validates its base directory resolves (after following symlinks) insidesettings.nas_root— the NAS trust boundary, which defaults to the common ancestor ofdata_dir/nas_dir/r2_dir/d1_path.parent(seeSettings._default_nas_root) — plus a belt-and-suspenders substring blocklist (r2_blobs,d1.sqlite). It also refuses to start whenCADENCE_ENV=prodandvault_master_keyis still the checked-inDEFAULT_VAULT_MASTER_KEY. The vault directory is chmod'd0700and every credential file0600; writes go through_atomic_write(temp file +os.replace) so a crash mid-write can't corrupt a credential or the sidecar index. Every access firescredential_vault_access. Secrets are per-account scoped and individually revocable (revoke()); do not add a bulk-secret-dump path. -
Exactly two raw-content egress channels. Per Decision E, raw content may leave the machine only via
EgressChannel.LLM_TEXT(raw text to the user-configured LLM) orEgressChannel.DAGLO_AUDIO(raw audio to Daglo STT) — both logged incadence/obs/egress.py(RawEgressLog), keyed by content hash, never verbatim payload.cadence/obs/stt.py'sDagloSTTAdapterrecords the would-be egress event but then raisesLiveCallDisabled— M1 makes no live STT call. Do not add a third silent egress path; any new raw-content-leaving-the-machine code must record throughget_egress_log().
The Adapter contract — adding a new source
The framework is in cadence/adapters/base.py; github.py, gcal.py, and email.py are the reference implementations to copy the pattern from. To add a new source:
- Subclass
Adapter, setprovider: str(concrete, not"abstract") andacquisition_tier: AcquisitionTier. - Implement
fetch(self) -> Iterable[RawRecord]— pull raw records. The reference adapters accept either an injectedrecordslist or afixture_pathJSON file (seetests/fixtures/{github,google_calendar,email}/) and raise if neither is given — no live network call is required or made. - Implement
normalize(self, raw) -> Event— convert one raw record into a provenance-taggedEvent. Store the verbatim raw record in NAS yourself (the reference adapters use a shared_store_verbatimhelper that JSON-serializes the record deterministically and callsnas.put) and setraw_evidence_ref/payload_hashon theEventfrom the returnedBlobRef; only structured fields, a non-verbatimsummary, and provenance pointers flow onward. TheEventmodel isextra="forbid"— you cannot smuggle araw_body-style field through it. - If the event kind should produce a first-class typed row (not just a generic
Fact), register a projector incadence/brain/projection.py'sdefault_projection_registry()— seeproject_calendar_event/project_task/project_email_taskfor the pattern (conservative: only project when the mapping is clean, e.g.project_email_taskonly fires whenstructured["actionable"]is true). - Leave
emit()as-is unless your source has different streaming semantics — the defaultfetch → normalize → tag acquisition_tier → dedupe_id → yieldorchestration is what the pipeline expects. - Register the class with
@registry.registersoAdapterRegistry.create(provider, account_ref, vault=...)can build per-account instances. - Resolve credentials via
self.credentials(), which reads from the injected NAS-onlyCredentialVault— never pass secrets around in the clear beyond the adapter instance.
The DeadlineExtractor / RuleDeadlineExtractor
cadence/brain/deadlines.py defines the contract: DeadlineExtractor.extract(event) -> list[DeadlineCandidate], pure functions over an Event (no I/O, no reaching into raw NAS evidence beyond what the Event exposes). DeadlineCandidate.origin is "explicit" or "inferred"; the convention — prefer explicit-source deadlines over inferred, and flag divergence (divergence_flag) rather than silently overriding — is implemented by RuleDeadlineExtractor._reconcile. RuleDeadlineExtractor is wired live in cadence/brain/app.py's create_app lifespan (not NullDeadlineExtractor, which remains available for tests/spine-only use). It reads an explicit due date off Event.structured (_EXPLICIT_STRUCTURED_KEYS) and separately pattern-matches an inferred one out of Event.summary only (never raw NAS evidence). To attach a future LLM-backed inference pass, pass llm_hook: Callable[[Event], list[DeadlineCandidate]] to RuleDeadlineExtractor.__init__ — its candidates flow through the same reconciliation as the rule-based pass. No llm_hook is implemented in M1.
Module map
cadence/
adapters/
base.py Adapter ABC, Event schema, AcquisitionTier, AdapterRegistry, CredentialVault ABC
vault.py FileCredentialVault (NAS-only, nas_root-checked, encrypted-at-rest stub)
github.py GitHub reference adapter (issues/PRs/review-requests → task candidates)
gcal.py Google Calendar reference adapter (events/attendees → calendar_event)
email.py Email reference adapter (messages → task/deadline candidates)
brain/
app.py FastAPI app factory: POST /ingest/event, GET /healthz, GET /metrics,
require_mtls (fails closed by default)
facts.py FactGraph — provenance fact graph write/dedupe path
projection.py ProjectionRegistry — typed calendar_event/task rows alongside the Fact
deadlines.py DeadlineExtractor contract, NullDeadlineExtractor, RuleDeadlineExtractor
ingest/
pipeline.py IngestPipeline, WALBuffer (backpressure), dedupe, projection + deadline routing,
per-event lock for concurrent-request safety
stores/
models.py SQLAlchemy models: calendar_event, task, deadline, person, place,
source_account, fact, nudge, feedback, sync_session; UTCDateTime type decorator
d1.py D1Store (local-canonical) + CloudflareD1Replica (async stub);
builds the SchemaBoundary from ORM metadata
raw_boundary.py SchemaBoundary (structural allowlist) + PayloadClassifier (heuristic backstop)
nas.py NASStore — content-addressed raw-evidence blob dir
r2.py R2Store + TieringRouter — content-addressed derived-blob dir + tier routing
obs/
logging.py Structured JSON logging (never logs verbatim raw content)
alarms.py AlarmSink: raw_to_cloud_violation, credential_vault_access, replication_queue_depth
egress.py RawEgressLog — the two sanctioned raw-content egress channels
stt.py STTProvider ABC + DagloSTTAdapter (interface-only, no live call)
metrics.py Prometheus-text renderer for /metrics
config.py Settings (env prefix CADENCE_) — storage-tier paths, nas_root trust boundary,
env (prod fail-closed gates), require_mtls, replica/vault/Daglo placeholders
alembic/
env.py Calls Settings.ensure_dirs() before resolving the DB URL (clean-checkout safe)
versions/0001_initial_schema.py Frozen, explicit-DDL D1 schema snapshot (not autogenerated)
tests/ One test module per component, plus tests/fixtures/{github,google_calendar,email}/
for the reference adapters; conftest.py provides isolated tmp-path settings +
an in-memory D1 store per test, and resets the alarm/egress singletons between tests
Running tests and lint
pip install -e ".[dev]"
pytest # 132 tests as of this writing; testpaths = ["tests"]
ruff check . # see [tool.ruff] in pyproject.toml (line-length 100, py311 target)
alembic upgrade head # apply the D1 schema to var/d1.sqlite (path from cadence/config.py);
# works on a clean checkout — env.py creates the dirs first
pytest.ini_options in pyproject.toml turns on asyncio_mode = "auto" and errors on any DeprecationWarning raised from cadence.* — don't introduce one and silence it instead of fixing it.
Do NOT build here / gated features
The following are explicitly out of scope for the foundation spine and gated by later milestones (.omc/plans/cadence-consensus-plan.md, Phase 0 / S0.2 / S0.5):
- Office Raspberry Pi capture node (Decision I) — presence-gated design with an unresolved legal risk acceptance (counsel recommended); do not implement any part of its capture pipeline here.
- Any recording/audio capture, VAD, or speaker-ID on live audio — gated behind S0.5 compliance controls.
cadence/obs/stt.pyis interface-only by design; do not makeDagloSTTAdapter.transcribeactually call out. - CODEF financial/government data integration — gated behind S0.5; no credentials, no live calls.
- Live inference / confidence-calibrated priority inference / the Nudge Governor — gated behind S0.2 calibration.
RuleDeadlineExtractoris rule/heuristic only (no LLM call); itsllm_hookseam exists but do not wire a real inference engine into it without going through the S0.2 gate. - Device agents (Android, Windows, watch) — not part of this repo's scope.
- Real mTLS certificate verification —
require_mtlsincadence/brain/app.pyfails closed on missingX-Client-Certbut only checks header presence, not a real certificate; it's the designated choke point for later, not something to half-implement now. - A real Cloudflare-D1 HTTP client —
CloudflareD1Replicais an in-memory, no-HTTP stub; don't add live network calls to it without also handling the failover/lease semantics in Decision C (out of scope for M1). - VM surface (Instagram Stories sweep) and co-presence/speaker-ID sensing — not started.
When in doubt about whether something belongs in this milestone, check .omc/plans/cadence-milestone-1-foundation.md's task list and the "Explicitly EXCLUDED" line at its top before building.