Before substantial work:
- Skill check: run
pnpm dlx @tanstack/intent@latest list, or use skills already listed in context. - Skill guidance: if one local skill clearly matches the task, run
pnpm dlx @tanstack/intent@latest load <package>#<skill>and follow the returnedSKILL.md. - Monorepos: when working across packages, run the skill check from the workspace root and prefer the local skill for the package being changed.
- Multiple matches: prefer the most specific local skill for the package or concern you are changing; load additional skills only when the task spans multiple packages or concerns.
Conventions for agents and contributors working in this repo. Product/architecture overview lives in README.md; this file is the non-obvious operational rules.
- pnpm (exact version pinned in root
package.jsonpackageManager), Node >= 22. Monorepo orchestrated by turbo. - Local turbo runs default to 50% concurrency (
turbo.json) so builds don't saturate the workstation; CI re-uncaps withTURBO_CONCURRENCY=100%per workflow. Agent/background gate runs useTURBO_CONCURRENCY=70%and are not niced; test gates stay serial (TURBO_CONCURRENCY=1 VITEST_MAX_FORKS=1). - Build:
pnpm build. Typecheck:pnpm typecheck. Test:pnpm test. Lint:pnpm lint(oxlint). Format:pnpm format:check/pnpm format(oxfmt). pnpm testbuilds first (turbo run testdependsOnbuild). Don't hand-rebuilddist/; use turbo. So a greentestalready proves the build: don't run a separatebuildgate unless the package has notestscript (@conciv/serve,@conciv/solid-diffs,@conciv/ui-kit-tap, theconciv-e2e-*apps).- Package gates filter bare:
turbo run test --filter=<pkg>. A TRAILING<pkg>...means "and all its DEPENDENCIES" (28 real suites here instead of 1), not "and its dependents";.claude/hooks/turbo-filter-gate.shblocks it for test/typecheck. The dependents selector is the LEADING form--filter=...<pkg>. - Affected-only shortcuts for a branch:
pnpm test:affected/typecheck:affected/build:affected(--filter=...[origin/main]). - Commit hooks:
prek(devDep@j178/prek, config.pre-commit-config.yaml) runs oxfmt + oxlint on staged files.pnpm installauto-activates the hook via thepreparescript; no per-clone step. Whole-project gates (typecheck/build/test) are not in hooks; run them manually. - Dev loop (
pnpm dev): browser packages (ui-kits, solid libs, client/grab/page/storage-history) hot-serve from source in vite hosts: edit and reload, no rebuild. Mechanism: the conciv plugin'sresolveIdmaps a workspace-resolved@conciv/*dist entry to itssrc/sibling (concivSrcEntryin extension-compiler) and solid-compiles the TSX (isConcivSrcTsx); manifests stay plain dist exports, so tsc, node, and non-vite bundlers (Turbopack in the nextjs example) resolve dist.@conciv/extension(shared singleton) and node-side packages (core, harness, tools, plugin) always resolve dist: rebuild embed for widget-shell edits, restart the dev server for server-side edits. NEW UnoCSS utility classes added in ui-kit src need an embed rebuild to appear (css is generated at embed build). - Vite 8 auto-enables
server.forwardConsolewhen it detects an agent env (AI_AGENTchecked first, thenCLAUDECODEetc.); combined with@tanstack/devtools-vite's server-to-browser console pipe, a single pageconsole.errorround-trips forever as growing[Server] ...entries (12+ GB tabs observed).concivSolidConfig(extension-compiler) now setsserver.forwardConsole: falseunconditionally for every host wired through the conciv vite plugin, so scrubbing agent env vars before starting a dev server is no longer load-bearing. - On large commits the prek hook can abort with a
next-index-*.lock.lockerror (file-lock race). Recover by runningpnpm formatmanually, thengit commit --no-verify. - Never kill a dev server with
kill $(lsof -ti tcp:PORT). That also matches the user's connected browser and kills their tab. Uselsof -ti tcp:PORT -sTCP:LISTEN(orpkill -f vite).
- Functions, not classes. (Sole exception:
DelegatingTextAdapterbehindmakeTextAdapterinpackages/harness/src/_shared/text-adapter.ts, which the library's typing forces.) - No IIFEs unless explicitly required.
- Zero code comments in TS/JS (only tool directives like
@ts-/eslint-survive). Theconciv/no-commentslint rule autofix-DELETES anything else, so don't write comments and let lint strip them; write self-explanatory code. - TypeScript is strict (
noUncheckedIndexedAccess,verbatimModuleSyntax, NodeNext). Avoidany/as/@ts-ignore. - oxfmt: no semicolons, single quotes, no bracket spacing, trailing commas, printWidth 120.
- Before any router work:
pnpm dlx @tanstack/intent@latest load @tanstack/router-core#<skill>(router-core/search-paramsfor params,router-core/code-splittingforgetRouteApi). - Files under
src/routes/use route-scoped APIs only:Route.useSearch(),Route.useNavigate(),Route.useParams(),Route.useLoaderData(), orgetRouteApi('/path')in a split component. BareuseSearch/useNavigate/useParams/useLoaderDataimports there are a lint error. - Shared cross-route components read params with
useSearch({strict: false}). Never mineuseRouterState().matchesand never hand-parsewindow.location: ask the router (router.matchRoutes). - Declare a search param on the shallowest route whose layout reads it; inheritance is
downward-only. Children contribute their own defaults through
search.middlewares, and masked defaults are stripped withstripSearchParams. validateSearchmust never throw: every zod field carries.default()or.optional()AND.catch()(zod v4, plain.catch(), nozodValidatoradapter, nofallback()). Search schemas live in files named*-schemas.tswithconst <name>SearchSchema, which is what theconciv/router-idiomslint rule matches on to check fields across files.
- Widget UI is tested in a REAL browser (Playwright/Chromium), never jsdom/happy-dom.
- Widget integration tests load the PREBUILT bundle (
packages/embed/dist/conciv-widget.global.js): rebuild it (pnpm turbo run build --filter=@conciv/embed) before running them, or you test stale code. - In widget ITs use
browser.newPage(), notnewContext()(contexts leak and spike CPU/memory). - Never add tests under
apps/examples/*: example apps are demos; verify behavior via the owning package's tests,@conciv/extension-testkit, or ane2e/consumer app. Sanctioned exception:apps/examples/tanstack-startcarries a handful of demo tests so the in-chat test runner has something to run out of the box; they are demo content, not behavior verification — behavior tests still live in the owning packages. - Every Solid package's
vitest.config.tsmust pintest: {environment: 'node'}.vite-plugin-solidotherwise injects a jsdom environment and the run exits 1 even with all tests passing. - Never wait for Playwright
networkidleon a page with the live widget: its SSE stream keeps the network busy forever; wait fordomcontentloaded(or a UI signal) instead. - zod validates every untrusted input where it enters: RPC procedures declare
oc.input(...)inpackages/contract/src/contract.ts; plain Hono routes zod-parse each piece they read (body, params, query, headers), e.g.NativeFileSchema.safeParse(c.req.param('file'))inpackages/core/src/api/native-page.ts.
- Before finishing a task, run
pnpm exec fallow audit --changed-since main --format jsonand fix anything it flags as INTRODUCED: dead code, unused exports/deps, duplication, complexity, circular deps. Fallow builds the whole module graph, so it catches cross-file dead code and unused deps you can't see from context. CI runs the same audit (.github/workflows/fallow.yml) and blocks on newly-introduced findings. - Before deleting a supposedly-unused export/dep, verify with
pnpm exec fallow dead-code --trace 'file.ts:Symbol'(or--trace-dependency <pkg>). "USED but file unreachable" means a missing entry point, not dead code. - Config is
.fallowrc.json.publicPackageslists our published libraries whose exports are public API and never "unused"; don't delete those. CI builds packages first so@conciv/*imports resolve against their dist-only exports; don't re-add anignoreUnresolvedImports: @conciv/*hack.
- Publishing is CI-only, via OIDC trusted publishing (
.github/workflows/release.ymlrunschangesets/actionwithid-token: write,NPM_TOKENempty). There is NO npm token for humans; runningpnpm releaselocally 404s (E404on the registry PUT). Never publish from a laptop. - The flow, end to end:
- Land a PR that adds a changeset (
pnpm changeset, or hand-write.changeset/<name>.md). Do NOT runrelease:versionorreleaseyourself; those are the CI steps. - On merge to
main,changesets/actionopens achore: version packagesPR that runspnpm release:version(consumes changesets → bumps versions + CHANGELOGs, resyncs the lockfile). - Merging that version PR triggers
pnpm releasein CI:turbo run build publint attw, thenchangeset publishto npm with provenance.
- Land a PR that adds a changeset (
- All
@conciv/*share ONE version:.changeset/config.jsonsetsfixed: [["@conciv/*"]], so a single changeset bumps the whole set in lockstep (currently the 0.0.x patch line). One changeset entry naming any@conciv/*package is enough to release them all. - EVERY PR that changes published packages (including
apps/conciv, which ships inside@conciv/embed) needs a changeset or theno-changesetlabel — CIcheck-changesets --require-coverageblocks otherwise. Verify locally before handoff:pnpm exec conciv-publish check-changesets --require-coverage --base origin/main. - Adding a new PUBLISHED package (
privateunset/false)? Add its name toPUBLIC_PACKAGESinpackages/publish/src/guards.tsorassertPublicSetaborts the release on drift; give ithomepage: https://conciv.dev+ arepositoryblock with itsdirectory(matches every public manifest). - Before opening a release PR:
pnpm typecheck && pnpm build && pnpm test, runpnpm exec fallow audit --changed-since main --format jsonand fix anything INTRODUCED (see the fallow section).pnpm release:check(build + publint + attw) mirrors the CI validate step locally.
- A harness is
chatConfig(deps)returning a published@tanstack/ai-*text adapter (+ optionalmodelOptions/prepareMessages) plus sidecars (models,history,connect,tty,commands). Turns run throughchat()with the conciv sandbox + permission-gate middleware; never spawn or decode a CLI yourself, and never special-case a CLI in core/widget. HarnessAdapteris capability-typed (packages/protocol/src/harness-types.ts):transcriptHistory: true⇒historyrequired;slashCommands≠'none'⇒commandsrequired, enforced at compile time. Add a harness by satisfying the capability contract.- Harness workdirs are sandbox-virtual: the local-process sandbox root IS the cwd and adapters
default to
/workspace. Never pass a host-absolute cwd into an adapter config: it nests a junkUsers/...tree inside the workspace and runs the CLI there. - Test runners follow the same registry/stub pattern.
- zag gates a listbox item's
data-highlighted/focused/focusVisibleon the listbox itself holding DOM focus (@zag-js/listboxlistbox.connect.mjs:47), and markshighlighteddeprecated in its own source. Under virtual focus (the editor keeps focus, listbox content is not a tab stop) they never fire, so highlight styling keys offhighlightedValuethrough our owndata-active(ui-kit-system/src/anchored-listbox.tsx). That is compensation, not duplicated state: don't "consolidate" it ontodata-highlighted.highlightedValueis not focus-gated, which is exactly whyaria-activedescendantworks.
- Whiteboard (TanStack DB over libSQL): never write to the db inside a collection subscription, effect, or render body: it triggers a re-render storm. Writes go in event handlers only.
- The whiteboard integration tests (
packages/extensions/whiteboard/test/*.it.test.tsand the browser project) each boot a real Chromium + server; unit tests there are plain node. Its vitest config self-capsmaxWorkersat 3 off-CI (localForkCap), which makes local runs safe: build first (pnpm turbo run build --filter=@conciv/extension-whiteboard), thenpnpm exec vitest runfrom the package. Never bypass or remove that cap; uncapped runs have saturated a workstation before. - The widget bundle must externalize every
@conciv/extension/*subpath and shared Ark/Solid deps; a second bundled copy splits the Solid/Ark context and extension popovers render at 0,0. Guarded by the mount-externals build test; don't weaken it.
- The core dev server binds
127.0.0.1only. Never commit or log credentials/tokens. - Risky Bash from the agent is gated (
packages/core/src/chat/gate.ts): read-only commands auto-allow, everything else asks. Keep that policy conservative when editing it.
- Pre-release (v0), no external users: reshape internal APIs freely and update all call sites; no back-compat shims.
Before any git commit or git push, run fallow audit --format json --quiet --explain --gate-marker agent. If the verdict is fail, fix the reported findings before retrying. Treat JSON runtime errors like { "error": true, ... } as non-blocking.
Audit defaults to gate=new-only: only findings introduced by the current changeset affect the verdict. Inherited findings on touched files are reported under attribution and annotated with introduced: false, but do not block the commit. Set [audit] gate = "all" in fallow.toml to gate every finding in changed files.
For non-skill agents, treat the task map below as the local onboarding source: run the listed fallow command before destructive edits, before commits, and before pull request handoff.
| When the agent is about to... | Run |
|---|---|
| delete an "unused" export or file | fallow dead-code --trace <file>:<export> |
| delete an "unused" dependency | fallow dead-code --trace-dependency <name> |
| commit or open a PR | fallow audit --base <ref> |
| prioritize refactoring | fallow health --hotspots --targets |
| ask who owns code | fallow health --ownership |
| check untested-but-reachable code | fallow health --coverage-gaps |
| consolidate duplication | fallow dupes --trace dup:<fingerprint> |
| find feature flags | fallow flags |
| surface security candidates | fallow security |
| understand a finding | fallow explain <issue-type> |
| scope a monorepo | --workspace <glob> / --changed-workspaces <ref> (global flags, prefix any command) |