Skip to content

Latest commit

 

History

History
226 lines (190 loc) · 15.9 KB

File metadata and controls

226 lines (190 loc) · 15.9 KB

Skill Loading

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 returned SKILL.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.

AGENTS.md

Conventions for agents and contributors working in this repo. Product/architecture overview lives in README.md; this file is the non-obvious operational rules.

Toolchain

  • pnpm (exact version pinned in root package.json packageManager), 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 with TURBO_CONCURRENCY=100% per workflow. Agent/background gate runs use TURBO_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 test builds first (turbo run test dependsOn build). Don't hand-rebuild dist/; use turbo. So a green test already proves the build: don't run a separate build gate unless the package has no test script (@conciv/serve, @conciv/solid-diffs, @conciv/ui-kit-tap, the conciv-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.sh blocks 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 install auto-activates the hook via the prepare script; 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's resolveId maps a workspace-resolved @conciv/* dist entry to its src/ sibling (concivSrcEntry in 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.forwardConsole when it detects an agent env (AI_AGENT checked first, then CLAUDECODE etc.); combined with @tanstack/devtools-vite's server-to-browser console pipe, a single page console.error round-trips forever as growing [Server] ... entries (12+ GB tabs observed). concivSolidConfig (extension-compiler) now sets server.forwardConsole: false unconditionally 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.lock error (file-lock race). Recover by running pnpm format manually, then git 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. Use lsof -ti tcp:PORT -sTCP:LISTEN (or pkill -f vite).

Code style

  • Functions, not classes. (Sole exception: DelegatingTextAdapter behind makeTextAdapter in packages/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). The conciv/no-comments lint 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). Avoid any/as/@ts-ignore.
  • oxfmt: no semicolons, single quotes, no bracket spacing, trailing commas, printWidth 120.

TanStack Router

  • Before any router work: pnpm dlx @tanstack/intent@latest load @tanstack/router-core#<skill> (router-core/search-params for params, router-core/code-splitting for getRouteApi).
  • Files under src/routes/ use route-scoped APIs only: Route.useSearch(), Route.useNavigate(), Route.useParams(), Route.useLoaderData(), or getRouteApi('/path') in a split component. Bare useSearch/useNavigate/useParams/useLoaderData imports there are a lint error.
  • Shared cross-route components read params with useSearch({strict: false}). Never mine useRouterState().matches and never hand-parse window.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 with stripSearchParams.
  • validateSearch must never throw: every zod field carries .default() or .optional() AND .catch() (zod v4, plain .catch(), no zodValidator adapter, no fallback()). Search schemas live in files named *-schemas.ts with const <name>SearchSchema, which is what the conciv/router-idioms lint rule matches on to check fields across files.

Testing

  • 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(), not newContext() (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 an e2e/ consumer app. Sanctioned exception: apps/examples/tanstack-start carries 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.ts must pin test: {environment: 'node'}. vite-plugin-solid otherwise injects a jsdom environment and the run exits 1 even with all tests passing.
  • Never wait for Playwright networkidle on a page with the live widget: its SSE stream keeps the network busy forever; wait for domcontentloaded (or a UI signal) instead.
  • zod validates every untrusted input where it enters: RPC procedures declare oc.input(...) in packages/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')) in packages/core/src/api/native-page.ts.

Codebase analysis (fallow)

  • Before finishing a task, run pnpm exec fallow audit --changed-since main --format json and 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. publicPackages lists 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 an ignoreUnresolvedImports: @conciv/* hack.

Releasing (npm publish)

  • Publishing is CI-only, via OIDC trusted publishing (.github/workflows/release.yml runs changesets/action with id-token: write, NPM_TOKEN empty). There is NO npm token for humans; running pnpm release locally 404s (E404 on the registry PUT). Never publish from a laptop.
  • The flow, end to end:
    1. Land a PR that adds a changeset (pnpm changeset, or hand-write .changeset/<name>.md). Do NOT run release:version or release yourself; those are the CI steps.
    2. On merge to main, changesets/action opens a chore: version packages PR that runs pnpm release:version (consumes changesets → bumps versions + CHANGELOGs, resyncs the lockfile).
    3. Merging that version PR triggers pnpm release in CI: turbo run build publint attw, then changeset publish to npm with provenance.
  • All @conciv/* share ONE version: .changeset/config.json sets fixed: [["@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 the no-changeset label — CI check-changesets --require-coverage blocks otherwise. Verify locally before handoff: pnpm exec conciv-publish check-changesets --require-coverage --base origin/main.
  • Adding a new PUBLISHED package (private unset/false)? Add its name to PUBLIC_PACKAGES in packages/publish/src/guards.ts or assertPublicSet aborts the release on drift; give it homepage: https://conciv.dev + a repository block with its directory (matches every public manifest).
  • Before opening a release PR: pnpm typecheck && pnpm build && pnpm test, run pnpm exec fallow audit --changed-since main --format json and fix anything INTRODUCED (see the fallow section). pnpm release:check (build + publint + attw) mirrors the CI validate step locally.

Harness & runner adapters

  • A harness is chatConfig(deps) returning a published @tanstack/ai-* text adapter (+ optional modelOptions/prepareMessages) plus sidecars (models, history, connect, tty, commands). Turns run through chat() with the conciv sandbox + permission-gate middleware; never spawn or decode a CLI yourself, and never special-case a CLI in core/widget.
  • HarnessAdapter is capability-typed (packages/protocol/src/harness-types.ts): transcriptHistory: true ⇒ history required; slashCommands ≠ 'none' ⇒ commands required, 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 junk Users/... tree inside the workspace and runs the CLI there.
  • Test runners follow the same registry/stub pattern.

Solid & Ark landmines

  • zag gates a listbox item's data-highlighted/focused/focusVisible on the listbox itself holding DOM focus (@zag-js/listbox listbox.connect.mjs:47), and marks highlighted deprecated 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 off highlightedValue through our own data-active (ui-kit-system/src/anchored-listbox.tsx). That is compensation, not duplicated state: don't "consolidate" it onto data-highlighted. highlightedValue is not focus-gated, which is exactly why aria-activedescendant works.

Extension landmines

  • 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.ts and the browser project) each boot a real Chromium + server; unit tests there are plain node. Its vitest config self-caps maxWorkers at 3 off-CI (localForkCap), which makes local runs safe: build first (pnpm turbo run build --filter=@conciv/extension-whiteboard), then pnpm exec vitest run from 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.

Security & safety

  • The core dev server binds 127.0.0.1 only. 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.

Project status

  • Pre-release (v0), no external users: reshape internal APIs freely and update all call sites; no back-compat shims.

Fallow local gate

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.

Fallow task map

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)