Imported from kzndotsh/watchdog (
packages/core/AGENTS.md). Install upstream withnpx skills add kzndotsh/watchdog --skill core. Copyright stays with the author.
Core package (@watchdog/core)
Scope:
packages/core(inherits root AGENTS.md unless noted)
Domain services for Case Graph, Jobs, evidence, Tasks (case work items — not Graph writes), export, and vault. Drizzle-free — talks to Postgres only via @watchdog/db repos.
Commands
| Task | Command |
|---|---|
| Typecheck | pnpm --filter @watchdog/core typecheck |
| Unit tests | pnpm test:unit |
| Integration tests | pnpm test:integration |
Boundaries
| Do | Don’t |
|---|---|
| Own transactions, SSE notify Effects, domain errors | Import Drizzle / write SQL in core |
Normalize display fields (trim, slugify, InvalidError) before repo writes |
Rely on repos to reject blank name/text |
Trim actorLabel via actorLabelForPersist (trimmedOrNull) before jobs / graph_writes / evidence insert |
Expect repo-level actorLabel trim |
Caps via catalog + interpret → Proposal |
Let Caps or Jobs write Graph directly |
Enqueue only via enqueueCapJobEffect / boss helpers |
Open ad-hoc pg-boss clients per call site |
Tests: Generic padded-UUID trim and requireActorIdEffect / optionalActorId belong in unit owners (graph/patch/__tests__/guards.test.ts, actors/__tests__/require-actor-id.test.ts, @watchdog/schemas primitives-trim.test.ts; repo scoped IDs in packages/db scoped-ids.test.ts). Core *.int.test.ts should assert domain contracts only (slug/dueDate display, wire-record normalize, workflow actor gates), not re-prove padded canonical UUIDs on every Effect.
Gotchas
- Services call repos with
exec: DbExecfirst; never open TX inside repos. - Core tests must not import
drizzle-orm(assert via repos / returned records). - Job pipeline:
executeJobEffect/executeJobOnMap(jobId)requireJobFibersinR.JobFibersis a scopedContext.Service(JobFibers.layer) owning the FiberMap and abort-reason map; workerrunMainprovides it once. Cancel poll yieldsJobFibersandyield*fibers.abortwhich sets"timeout"|"cancel"thenFiber.interrupt. Capctx.signalis the fiberEffect.abortSignal(one timeout sleeper inrunReadyJobEffect). Collect splits reclaim / cache-hit /cap.runconstructors. Stages keepDomainTag/ToolsTaginE. Effect 4 sticky interrupt:catchCausedoes not turn interrupt into Success — persist fail viaonExitIf(interrupt-only) and mapFiber.awaitinterrupt Exit inexecuteJobOnMap(clear abort reason there). PublicexecuteJobEffectstaysE = never. After collect, land-evidence / interpret / suppress / propose / finish / cache / playbook success-fail tails are Effect stages (interpretErrorstays data). Cap stages live underjobs/stages/. CaptimeoutMsdrives expire / graceful stop / stale reclaim.collectEffectyieldscap.run(ctx)(norunCap). - Playbooks:
advancePlaybookRunEffectinjobs/stages/chain.tsis the only chain (success, failure, stale fail, stuck reclaim). KeepsDomainTaginE; tests bridge withrunDomain. FiberMap-owned enqueue tails mayorDie(enqueueCapJobEffectinsideenqueueReleasedEffect). Do not restorereleasePlaybookDependents/abandonPlaybookOnFailure.runPlaybookinserts the run + step-0queuedJob;enqueueStepJobslazy-creates later steps (playbookFanIndex). Historicalblockedrows may flip toqueuedin the same TX — do not pre-insert the rest of the recipe.handoffis computed ininterpret.ts(including cache hits) and persisted on the success write infinish.ts. Advance defects/errors on the success path are logged viaEffect.catchCause;reconcileStuckPlaybookRunsEffectre-advancesrunningruns whose member Jobs are all terminal. - Stale reclaim:
reconcileStaleJobsEffect/reconcileStuckPlaybookRunsEffectkeepDomainTaginEand run at worker boot insidestartWorkerResourcesEffect(worker edgecatchCause+ log). Cancel poll usesfindCancelledJobIdsEffect. - Tasks (
src/tasks/): case-scoped CRUD +reorderTasks(positionwithin a status column; board order is notcreatedAt-only) +notifyTaskChangedEffectafter commit; not a Graph write / patch path. Effect exports:createTaskEffect/updateTaskEffect/deleteTaskEffect/listTasksForCaseEffect/getTaskInCaseEffect/reorderTasksEffect. applyPatchdispatches tograph/patch/apply-*-op.tsper resource; shared helpers ingraph/patch/apply-patch-helpers.ts.- Layout:
graph/holds per-resource graph-node services;graph/patch/is the patch apply pipeline;cases/andproposals/mirror other layers (Case CRUD, Inbox accept/reject). Worker code imports@watchdog/core/workerinstead of the full barrel. - Edge updates:
edge-update.ts—validateEdgeUpdate/buildEdgePatch/applyValidatedEdgeUpdate(used fromedges.ts). - Identifier Accept:
apply-identifier-op.tsusesvalidateIdentifierWrite(value + handle→platform; same as DossiercreateIdentifier/updateIdentifier). Type-only updates re-validate under the new type. - Inbox list annotates
identifierCollisionsvialoadIdentifierCollisionsEffect+identifiersRepo.listForCase(index by type+value). Warn, don’t block Accept. Invalid Identifier values block Accept (schemaslistInvalidIdentifierOpspreflight + core TX). - Entity create:
seedDefaultQuestions/seedDefaultQuestionsEffectinquestions.ts(Partial<Record<EntityKind, …>>— person seeds today). Do not inline kindifs increateEntity. - Agent ingress: propose by default;
graph write+userOverride→ Graph @unverified+graph_writesaudit.parseAgentPatchEffectis the shape gate (norunSync). - Errors: Case CRUD, Tasks, Entities, Claims, Identifiers, Edges, Events, Questions, Evidence, Proposals, Jobs, Search, and Activity are Effect programs (
*Effect). Tests bridge withrunDomain(no production Promise service edges). CapContext vault/blob methods are Effects (ToolsTag). Graph patch apply isapplyPatchEffect. Vault slots are Effect-first (getCredentialEffect/putCredentialEffect/ …). Service TX bodies usetransact(yield tagged errors). Infra Layer in use:JobFibers(worker). Vault/blob/Postgres/JobQueue/repo identity Layers were removed — calltryDb/ module Effects directly. - Vault slots:
listCredentialSlots/putCredentialSlot(Settings +/credentialsAPI); never return plaintext. - Inbox Accept/Reject is one TX — attestation + patch + status (and reject fingerprints) in a single
transact;proposalsRepo.lockInCase(SELECT … FOR UPDATE) then re-checkstatus = 'pending'before apply/accept. - Org-scoped case children — API/actor Effects take
organizationIdand gate withassertCaseInOrgEffect(missing / foreign-org Case →not_found). Worker/export paths that already trust a Case id from a Job or child row useassertCaseExistsUncheckedEffect/casesRepo.getByIdUnchecked— do not widen that to HTTP handlers. - TX asserts use
tx— passtxintoassertEntityInCase/assertEvidenceInCase/assertCaseInOrgEffect(or unchecked inside a trusted TX); never assert on the global pool while writing ontx. - Collect scratch FS maps to
ScratchIOError(then stage handling); export writers map domain failures toExportIOErrorthenlogSwallowed— keepDomainTagon render/reconcile/playbook-advance edges forrunApp/runDomain. - Dossier create+link / replace Evidence: one
transact; helpers inevidence-links.ts. - Jobs list:
listJobsForCase→JobListRecord(input + artifact output; nologs). Detail viagetJobForCase. - Client-safe display:
@watchdog/core/job-displayexportscapabilityIdLabel,playbookIdLabel,summarizeJobInput,jobActivityLabelwithout pulling blob/db/env. Web client code must not import the@watchdog/corebarrel for these helpers. - One boss per process: web/API
ensureBossProducerEffect/enqueueCapJobEffect(supervise: false); workerensureBossWorkerEffect(supervise: true); playbook chain reuses the live worker boss — never a second pool from Vite. Enqueue and start map pg-boss failures toInvalidError(including a second role in the same process). - Dual SoT cancel: product
jobs.statusis authoritative; workercancelPollLoopEffect(Schedule.spaced("2 seconds"), first tick immediate) +JobFibers.abortinterrupts the Job fiber (FiberMap). Do not bridge pg-bossjob.signal."timeout"vs"cancel"is stored before interrupt (fibers.setReason/fibers.abort), not recovered fromCause. - Export sync:
scheduleCaseExportEffectcoalesces viaSynchronizedRef(dirty set + in-flight fiber). Calling it marks dirty synchronously (runSyncclaim) so fire-and-forgetvoid Effect.runPromise(...)still coalesces. The loop rechecks dirty before dropping the fiber so a concurrent mark is not dropped.renderEntityMarkdownEffect/renderCaseExportEffectkeepDomainTaginE(APIrunApp); writerswriteCaseExportEffect/writeEntityExportEffectmap that toExportIOErrorthenlogSwallowed. Case delete/rename usesremoveCaseExportDirEffect/renameCaseExportDirEffect. Do not start parallel case writes from the worker. Case name rename regenerates slug (slugForCaseName); conflict if taken; best-effort rename thenscheduleCaseExportEffect. Empty slugify →invalid. - Process logs:
@watchdog/log(logSwallowed/logProcess—logSwalloweduseslog.error);executeJobEffect/executeJobOnMapreturnJobRunOutcome(outcome/stopReason/abortReason). evlog ≠Job.logs/graph_writescustody. Job stages carryEffect.withSpan(cap.execute,cap.preflight,cap.collect,cap.interpret,cap.finish). WorkerrunMainruns jobs withexecuteJobOnMap(jobId)under scopedJobFibers. Export LISTEN is dblistenForEventsStream(Stream.runForEach); web SSE stays on callbacklistenForEvents. After-commit SSE fan-out isnotifyEntityChangedEffect/notifyEvidenceChangedEffect/notifyTaskChangedEffect/notifyJobUpdateEffect/notifyProposalCreatedEffect/notifyProposalQueueChangedEffect(notifyEvent+ ignore +forkDetach). Inbox accept/reject and attestation helpers only notify when not inside a parenttransact(input.tx === undefinedoncreateAttestationEffect; accept/reject notify aftertransactcommits). Domain TX usestransact(one nestedrunPromisefor Drizzle). - Activity (
src/activity/): org-scoped recent activity merge (listRecentActivity) for Dashboard Activity — read-only; no notify / live channel. - Case search (
src/search/):searchCase— Active Caseilikeacross entities / identifiers / evidence / tasks / jobs / pending proposals, plus Cases-by-name for switch. Repos own the SQL (containsPattern); core does not concatenate%wildcards.
See also / External References
| Need | File |
|---|---|
| Repo contract | packages/db/AGENTS.md |
| Cap authoring | packages/caps/AGENTS.md |
| Patch custody | packages/policy/AGENTS.md |
| Process logging | packages/log/AGENTS.md |
| Platform architecture | docs/reference/platform/README.md |