Imported from noctcore/nightcore (
AGENTS.md). Install upstream withnpx skills add noctcore/nightcore. Copyright stays with the author.
Nightcore — Agent Contract
Read this before editing. These are hard guardrails, enforced by bun run lint, bun run typecheck, bun run test:all, and the tools/lint-meta engine. Severity is error or off, never warn — a rule that matters is an error; fix the failure, do not silence it.
Repository shape
- Deployable surfaces live in
apps/*; reusable libraries/capabilities inpackages/*. Every workspace is named@nightcore/<dir>matching its folder, exposes a singlesrc/index.tsbarrel, compiles todist/, and pointsmain/module/types/exportsat the built output. - Allowed dependency direction:
contracts → shared → storage → engine → surfaces. Never import upward or sideways across that order (storage depends on shared, so shared ranks below storage). The built packages are config, contracts, engine, eslint-plugin, session-fold, shared, storage; thelayer-rankrule reserves a co-tierskillsrank next tostorage, but nopackages/skillsexists yet.
Hard import boundaries
- The Claude Agent SDK (
@anthropic-ai/claude-agent-sdk) may ONLY be a dependency of@nightcore/engine. Its runtime API (query()and the session-store functions) is confined topackages/engine/src/session/sdk-adapter.ts— enforced by lint (@typescript-eslint/no-restricted-importswithallowTypeImports). Other engine modules mayimport typeSDK shapes (e.g.policy/permission-layer.ts,policy/question-layer.ts,policy/hook-bus.ts) but never a runtime value. Every surface and capability package reaches the model through the@nightcore/enginefaçade. If a new package needs the model, route it through the engine — do not add an SDK dependency. - Library packages below the engine (
contracts,shared,storage,config,session-fold) MUST NEVER import@nightcore/engine. The engine pulls capabilities in (dependency inversion), never the reverse; any futurepackages/skillscapability tier is bound by the same rule. - Cross-package imports use the package barrel
@nightcore/<pkg>ONLY — never a deep subpath@nightcore/<pkg>/...into internals (enforced bynoctcore-monorepo/no-deep-package-imports). If a deep entry is truly needed, add an explicitexportssubpath to that package. - A package may only import workspace siblings it declares as
workspace:*deps, andtsconfigreferencesmust mirror those edges. Add both in the same change.
Contracts & codegen — regenerate, never hand-edit
@nightcore/contracts(zod) is the single source of truth at the sidecar boundary and the dependency-graph leaf (zod only). Add new wire fields to the zod schema FIRST.- Both contract boundaries are code-generated: zod→Rust via
tools/codegen/gen-rust-contracts.ts(bun run codegen:contracts), and Rust serde→web TS via ts-rs (cargo test). NEVER hand-editapps/web/src/lib/generated/**orapps/desktop/src-tauri/src/contracts/generated.rs. Change the schema/struct and regenerate. - Run every
cargocommand with cwd =apps/desktop/src-tauri, never--manifest-pathfrom the repo root. Cargo reads.cargo/config.tomlby walking up from the WORKING DIRECTORY, and that file sets ts-rs'sTS_RS_EXPORT_DIR(→apps/web/src/lib/generated) +TS_RS_LARGE_INT=number. A root-cwd run dumpsbigintbindings into the gitignoredsrc-tauri/bindings/and the drift guard then passes VACUOUSLY — nothing was written to the directory it diffs (#422).bindings::export's tests fail loudly if that env is missing, andbun run verify:drift-guard(CI,rust-checks) proves the guard still trips by perturbing a#[derive(TS)]type and requiring the diff to fail. - After any contract change,
bun run codegen:checkverifies ALL generated artifacts are in sync in one command — zod→Rust drift, the TS codegen canaries + cross-boundary conformance, and the Rust→web ts-rs bindings + contract parity/round-trip. (These individual gates also run inlint/test:node/check:rust+ CI; this is the local one-shot.) - Persisted/wire structs are serde-additive: every new field is
Option(Rust) / optional (zod) with aNone/absent default in its own additive block, plus a field-absent pinning test. Never add a breaking required field. - Contracts may also host the matching semantics of a wire field when BOTH a surface and the engine must agree on them — today
policy-patterns.ts(the harness policy glob/regex/tool matchers) andpolicy-lint.ts(their authoring diagnostics). The rule: a surface can never import@nightcore/engine(it owns the SDK), so a second implementation inapps/webwould be free to disagree with enforcement. Such a module stays PURE (no zod, no node builtins, no I/O) and the engine consumes it through thin logger-aware wrappers — never the reverse. Enforcement ORDER and verdicts stay in the engine gate.
Naming
- Exported zod schema = PascalCase const suffixed
Schema, paired withexport type Foo = z.infer<typeof FooSchema>— enforced bynoctcore-contracts/zod-schema-naming(erroronpackages/contracts/src/**). Discriminated-union member schemas intentionally use role suffixesEvent/Command/Query, notSchema; the rule carves them out (their naming contract isnoctcore-contracts/wire-message-naming). - Wire field names are camelCase on BOTH sides; Rust structs serialized to the contract carry
#[serde(rename_all = "camelCase")]. - Message schemas:
<Noun><PastVerb>Event/<Verb><Noun>Command/<Verb><Noun>Query; the wiretypediscriminant is the const name minus its role suffix, kebab-cased. - Numeric Nightcore session id is
sessionId(number); the SDK UUID issdkSessionId(string). Never reuse one name for the other.
Testing
- node/TS packages use
bun:test(with/// <reference types="bun" />);apps/webandpackages/eslint-pluginuse Vitest. Never mix runners. - The real gate is
bun run test:all(it includestest:rust); plaintestomits the Rust suite. - EVERY tier carries a coverage floor that FAILS the build below threshold, and each is a ratchet — raise it as coverage grows, never lower it: node =
bun run test:node:coverage(tools/coverage/check-node-coverage.ts), web =bun run test:web:coverage(Vitest istanbul thresholds inapps/web/vitest.config.ts), Rust =bun run test:rust:coverage(tools/coverage/check-rust-coverage.ts,cargo llvm-cov; its ownrust-coverageCI job because instrumented artifacts share nothing withrust-checks'). - The SDK/model boundary MUST be stubbed in engine tests — no live
query()ever runs.
Lint discipline
- Always run
bun run lint(it rebuilds@nightcore/eslint-plugintodist/first) — never a bareeslint .. - Git hooks (Husky): after
bun install,pre-commitrunsbun run lintandbun run build;pre-pushaddsbun run check:rust(fmt, clippy,test:rust, ts-rs drift — therust-checksCI job). Rust integration tests spawn nestedgit worktreecalls and cannot run duringpre-commitwhile Git holds the index lock; push is the right gate for them. On Windows,check:rustruns single-threadedcargo testplus the ts-rs drift diff but skips fmt/clippy (CRLF checkout and cfg-gated imports false-fail vs Linux CI). Skip hooks in an emergency withHUSKY=0 git commit …/HUSKY=0 git push …. - Architectural boundaries are lint rules, not docs. A new legitimate cross-layer need adds a named seam (façade method / bridge command), it does not relax a rule.
.editorconfigis the sole formatting authority for TS/JS (no Prettier/Biome; style beyond indent/EOL/final-newline is intentionally unenforced).- Import ordering is enforced by
simple-import-sort/imports+/exports(error, autofixable): side-effect imports → node/bun builtins → third-party →@nightcore/*+@/→ relative, blank-line separated. Runeslint . --fixrather than hand-sorting. - The Rust core has its own lint gate in the
rust-checksCI job:cargo fmt --check(style pinned byapps/desktop/src-tauri/rustfmt.toml) andcargo clippy --all-targets -- -D warnings. It lives there, NOT in lint-meta — the Bun lint job has no Tauri system deps.
Enforced harness additions (lint-meta + plugin)
These guardrails are mechanical — bun run lint runs the ESLint plugin then tools/lint-meta. Severity is error or off, never warn (enforced by no-warn-severity).
noctcore-contracts/wire-message-naming(ESLint, error onpackages/contracts/src/**): a const endingEvent/Command/Querywhose zod object declares atype: z.literal(...)MUST set that literal tokebab-case(constName minus its role suffix)— e.g.TaskCompletedEvent→'task-completed',RunTaskCommand→'run-task'.package-shape(lint-meta): every workspace is named@nightcore/<dir>; library packages exposesrc/index.tsand pointmain/module/types/exportsat./dist/.layer-rank(lint-meta): the spinecontracts → shared → storage → engine → surfacesis enforced — a module may import only strictly-lower-ranked@nightcorepackages; upward/sideways imports fail CI. (The rule also reserves a co-tierskillsrank next tostorage, currently unused.)workspace-graph-parity(lint-meta): every imported@nightcore/*must be a declaredworkspace:*dep, andtsconfigreferencesmust mirror those deps.no-warn-severity(lint-meta): no ESLint rule may be set to'warn'— error or off only.test-workspace-enrollment(lint-meta): a node package with*.test.tsmust be listed in thetest:nodescript.test-runner-segregation(lint-meta):bun:testfor node packages +apps/sidecar; Vitest forapps/web+packages/eslint-plugin. Never mix runners.decision-register-integrity(lint-meta): everydocs/decisions/INDEX.mdrow must carry a date and cite only paths that resolve on disk; every dated doc underdocs/decisions/must be linked from a row.agents-doc-presence(lint-meta): anAGENTS.mdmust exist at the repo root, in everyapps/*, and in every non-leafpackages/*(leaf packages are an explicit opt-out list in the rule).ui-primitive-shape(lint-meta): acomponents/uiprimitive that graduates to a folder (own dir +index.ts) must ship<Name>.test.tsxand<Name>.stories.tsx; flat single-file primitives stay pure presentational.test-sibling-enforcement(lint-meta): every<base>.utils.tsunderapps/web/srcmust have sibling<base>.utils.test.ts(x).canonical-helpers-single-home(lint-meta): pure helpers must live in one canonical.utils.tshome (flag duplicates when pattern adopted).rust-module-shape(lint-meta): in the desktop Rust crate, everymod.rsis a manifest (onlymod/usedeclarations, docs, and attributes — nofn/impl/struct/enum/trait/constbodies), and no code file exceeds 400 code lines measured EXCLUDING#[cfg(test)]blocks (siblingtests.rsfiles are not counted). ENFORCED: today's god-files + logic-bearingmod.rsare grandfathered by a shrinking ratchet (baselines/rust-module-shape.json) — a NEW over-cap file or amod.rsthat gains logic fails CI; a grandfathered file that GROWS past its frozen count fails. Fix an offender by splitting it, thenbun run lint:meta -- --update-baselineto lower its entry (never raise it). Permanent exemptions (never counted):contracts/generated.rs,store/run_store.rs,sidecar/harness/apply.rs. Pure text, nevercargo.rust-layer-rank(lint-meta): the desktop crate'scrate::Ximports may point only STRICTLY DOWN a 6-tier rank —contracts/infra/sync/engine_api(1) →git(2) →store/worktree/provider(3) →analysis(4) → theorchestration/sidecar/workflowengine tier (5) →commands(6). Crate-root facades (crate::task→store,crate::merge→workflow,crate::platform→infra, …) are resolved first, and#[cfg(test)]blocks are stripped. The engine tier is a genuine SCC, so sideways imports among the three are tolerated — EXCEPTsidecar → orchestration, which must go throughArc<dyn EngineApi>.lib.rs(composition root) andbindings/**(ts-rs aggregator) are exempt. Upward/sideways edges elsewhere fail CI.rust-command-placement(lint-meta): a#[tauri::command]handler is forbidden in the leaf tier (contracts/infra/sync/git/engine_api/store/worktree/provider) — put it incommands/or co-locate it in its feature/engine module. This is a leaf-tier ban, NOT a "commands/-only" rule (feature handlers stay co-located).rust-engine-seam(lint-meta): nothing undersidecar/**may referencecrate::orchestration::— the sidecar reaches the engine ONLY throughArc<dyn EngineApi>(theengine_apiseam the 2026-06-28 decomposition paid for). A direct import re-closes the broken cycle.no-cloned-component-folders(lint-meta): same-named component folders across features are disallowed (clones diverge); today's groups frozen in shrinking allowlist inside the rule (enforcement detail).scan-family-parity(lint-meta): scan-view families (harness/insight/scorecard/issues/prreview) must build on the sharedlib/useScanRun+lib/scan-run(no local reimpls); new families must be enrolled (enforcement detail).agent-contract-parity(lint-meta): every wirednightcore/*ESLint rule must be mentioned in AGENTS.md (self-exempt for lint-meta rules).codegen-drift(lint-meta): zod→Rust generated contracts must match source (runsgen-rust-contracts.ts --check); the reverse direction is covered bycargo test(enforcement detail).
Architectural decisions are recorded in docs/decisions/INDEX.md (status: active/superseded) — update the register in the SAME change that reverses a decision.