Add damem notes and skills
Check / check (pull_request) Successful in 37s

This commit is contained in:
Danny Kim
2026-10-16 08:28:47 +09:00
parent 8f23da2b42
commit c02ab9b156
32 changed files with 1241 additions and 4 deletions
+39
View File
@@ -0,0 +1,39 @@
---
description: Every repo has AGENTS.md starting with the damem recall header; durable context lives in .agents/memory (flat, one fact per file) and .agents/skills
---
Standing convention (2026-10-12): every repo has an `AGENTS.md` whose first
lines are exactly the damem header (cunarist/damem):
```markdown
At the start of every new session, run `damem recall` in the terminal and treat
its output as the context for this repository. If the `damem` command is not
found, read [INSTALL.md](https://github.com/cunarist/damem/blob/main/INSTALL.md)
and follow it before reasoning or responding.
```
Below it: `# AGENTS.md`, then `## Project` (what the repo is, runtime,
toolchain, where it came from) and `## Rules` (short bullets, each pointing at
a memory where the reason lives). No `CLAUDE.md`, `GEMINI.md`, or other
per-agent files; `AGENTS.md` is the single entry point.
## `.agents/` layout
- `memory/*.md`: one fact per file, flat (subdirectories are not listed),
frontmatter with a one-line `description` that states the fact. Link
related notes with double-bracket wiki links using the file name without
`.md`; a link to a missing note fails `damem doctor`.
- `skills/<name>/SKILL.md`: frontmatter `name` and `description` (when to use
it), body says how. Helper scripts live next to it and are Python 3.12+.
- `scratch/`: holds a `.gitignore` containing `*`.
- Every memory and `SKILL.md` stays under 8 KB.
## Keeping it right
- Update an existing note instead of adding a near-duplicate; delete notes
that turn out wrong.
- Write a note when the user corrects something, states a preference, or
when understanding something took reading many files.
- Run `damem doctor` before finishing and fix what it reports.
- Changing anything outside `.agents/` (source, config, `AGENTS.md`) follows
the normal branch and PR flow ([[git-pr-workflow]]).
+36
View File
@@ -0,0 +1,36 @@
---
description: Containerfiles build the frontend in a node:24-alpine stage with npm ci and vp build; CI uses setup-vp and runs vp check, vp build, vp test (plus lit-check)
---
## Containerfiles
- The frontend build stage is `docker.io/library/node:24-alpine`, replacing
`denoland/deno:alpine`. Vite+ publishes `linux-x64-musl` binaries, so Alpine
works. (`ghcr.io/voidzero-dev/vite-plus` is a glibc alternative that would
need a pinned digest; not used.)
- The stage copies `package.json` and `package-lock.json` first, runs
`npm ci`, then copies the sources and configs and runs `npx vp build`.
- The runtime stage copies the built `js/` and `css/` directories (not single
files) from the node stage.
- No Deno binary, `deno install`, or `deno task` remains in any stage.
```dockerfile
FROM docker.io/library/node:24-alpine AS frontend
WORKDIR /src/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ ./
RUN npx vp build
```
## CI
- GitHub repos (temeddix): `.github/workflows`. Capsulizers repos (Gitea):
`.gitea/workflows`. Don't move a repo's CI between hosts.
- Install with `voidzero-dev/setup-vp`, then run `vp check`, `vp build`, and
`vp test`; Lit repos also run `npm run lit-check`
([[lit-analyzer-ts6]]).
- Repos that ran CI on a self-hosted runner keep it.
- Until `@cunarist/eslint-plugin-*` are on npm, `npm ci` fails in CI. That is
expected and stated in the PR; for local validation install them from local
clones or tarballs without committing that.
+20
View File
@@ -0,0 +1,20 @@
---
description: Code that must ship as standalone JS (memona framed_find.js, brain stream-recovery.js) is written as .ts, compiled at build time, and its output gitignored
---
Some apps ship plain JavaScript that runs outside the main bundle: memona's
`framed_find.js` and brain's `stream-recovery.js`. Under the TS-only rule
([[ts-only-files-and-imports]]) they are handled like this (2026-10-16):
- The source is a `.ts` file, linted and type-checked like everything else.
- The `.js` is produced at build time by the existing build (a Vite input or
a build step in `vite.config.ts` or `scripts/`), never committed.
- The output path is gitignored and, if it lands in a linted folder, listed
in `lint.ignorePatterns`; never in `overrides`, never with disable comments.
- Whatever loads the file (Rust `include_str!`, a container `COPY`, an HTML
`<script>`) keeps the same path and name as before, so behavior is
unchanged. Builds that need the file must run the compile first; check
that the Containerfile and CI order the steps that way.
Don't commit a hand-maintained `.js` copy "for convenience", and don't turn
it into `.mjs` or `.cjs`.
@@ -0,0 +1,41 @@
---
description: Apps use the npm lint plugins' configs, port their old deno-lint-plugin.ts rules into scripts/project-lint.ts, never override source files, and refactor instead of raising limits
---
## Plugins
Apps consume the published plugins as npm devDependencies, through `jsPlugins`
and the configs equivalent to what their Deno setup enabled:
- `@cunarist/eslint-plugin-explicit-types`: `concrete`, `naming`, `absence`
- `@cunarist/eslint-plugin-import-check`: `import-check`
- `@cunarist/eslint-plugin-lit`: `core`, `strict`, `dom-ref`,
`reactive-controller`, `naming`
## Repo-local rules
The local `scripts/deno-lint-plugin.ts` that home, eximium, stockvibe, brain,
memona, and memona-home carried is ported into `scripts/project-lint.ts`
([[project-lint-plugin]]) under the `project` namespace, same rule names and
behavior. Depending on the repo: `no-self-sync`, `no-sync-in-render`,
`app-module-conventions`, `always-wildcard-export`, `no-class-extends`,
`no-lit-nothing`, `no-title-attribute`, `prefer-wa-components`,
`no-satisfies`, `no-unknown`, `restricted-types`, `no-line-lint-ignore`,
`no-app-disposables-in-component`, `no-crepe-react-ui`. `@hugoalh` rules map to
Oxlint built-ins in `configs.project`.
memona's `deno-lint-plugin.ts` and its `coding-standards` memory are the house
style the explicit-types rules came from; they win any conflict.
## No overrides, refactor instead
Apps use **no `overrides` for source files at all** (the rule-registry
exception exists only in plugin repos). Oxlint's default limits stay, and
violations are refactored without behavior change in the same PR: barrels for
`max-dependencies`, splits for `max-lines`/`max-lines-per-function`/
`max-classes-per-file`, an options object for `max-params`. Long Lit
`render()` templates are split into smaller template functions or
components. See [[lint-overrides-policy]].
If a hand-written exception still looks truly necessary, ask the user first;
when they're away, leave it failing or refactor, and log the question.
@@ -0,0 +1,29 @@
---
description: bot-agents migration decisions: live @main action for six repos, JSON profiles, Node 24.2+, Deno kept only for agents, no @cunarist plugins needed
---
Decisions for `commons/bot-agents` (Capsulizers Gitea), from the Deno to
Node + Vite+ migration (PR #20, `node-migration`, 2026-10-15/16):
- **Live action.** Six consumer repos use `commons/bot-agents@main`, so a merge
switches all of them at once. It changes only through a PR and a careful
review. Suggested before merging a risky change: tag the current `main`
(e.g. `deno-final`) as a rollback target and canary the branch from one
consumer (`uses: .../commons/bot-agents@<branch>`) on a throwaway PR.
- **Profiles are JSON.** `profiles.yml` became `profiles.json` with the same
content (encrypted `v1:` values unchanged), read with `JSON.parse`. That
removed the only runtime dependency (`@std/yaml`); keep it dependency-free.
- **Node 24.2+**, not 22: `src/post.ts` uses `import.meta.main`. `engines.node`
is `>=24.2`, which the user accepted for this repo
([[bot-agents-runtime-no-build]]).
- **Deno stays in the action's global npm install** (`action.yml`), only
because the agents still check Deno-based consumer repos (memona,
memona-home). Remove it once those repos have moved; it is not a
dependency of this repo's TypeScript.
- **No `@cunarist/eslint-plugin-*`.** The Deno setup enabled no plugin, so
this repo depends on none and CI passes without them being published.
- **Stricter input checks were accepted** in the migration: Gitea `/user` and
comment responses, the runtime manifest, and the outcome's profile type are
validated; some type errors became `TypeError` with the same messages.
- Merging is the user's call; the `bot` account's approval is required first
([[git-pr-workflow]]).
+43
View File
@@ -0,0 +1,43 @@
---
description: Frontends build with vp build in Vite app mode, keeping the exact output paths the Rust backend and HTML expect; emptyOutDir false when outDir holds committed files
---
`deno bundle` was replaced by `vp build` in **Vite app mode** (not
`build.lib`). Behavior must not change, so the output keeps the file names and
locations the backend serves and the HTML references.
Typical shape for the Rust-backed frontends (home, eximium, stockvibe,
memona-home), where `frontend/` builds into `backend/static`:
```ts
build: {
outDir: "../backend/static",
emptyOutDir: false, // the folder holds committed files (index.html, ...)
rollupOptions: {
input: { bundle: "src/index.ts" },
output: { entryFileNames: "js/[name].js" }, // -> js/bundle.js
},
},
```
- **Fixed names, no hashes:** set `entryFileNames` (and `assetFileNames`
where assets are emitted) explicitly. Vite's hashed defaults would 404,
since Rust and the templates request `js/bundle.js` and `css/theme.css`.
- **`emptyOutDir: false` whenever `outDir` is outside the Vite root or holds
committed files.** Never let it default to true there: it would wipe them.
- Output stays gitignored, so `.gitignore`, `.containerignore`, and the Rust
side need no edits; home's startup check for `js/bundle.js` and
`css/theme.css` keeps passing.
- **CSS is never imported from TS** (no `import "./theme.css"`); the HTML
links the stylesheet. How the CSS file is produced is per repo; read the
repo's `vite.config.ts` before changing it.
- Copy whole `js/` and `css/` directories in Containerfiles, not single files,
so code-split chunks ship too ([[app-containers-and-ci]]).
- Where a repo serves an `index.html` entry (brain's dashboard), use plain app
mode with `index.html` as input, `base` set to the mount path, and `outDir:
"dist"` with the default `emptyOutDir`.
- `vp dev` must work too, and the decorators plugin must cover build, dev, and
test ([[decorators-standard-accessor]]).
Check the repo's own migration PR for its final values before editing; these
are the conventions it followed, not a config to paste.
@@ -0,0 +1,20 @@
---
description: bot-agents lint has prefer-readonly-parameter-types off (Node/web types can't satisfy it) and one scripts/** override for no-unsafe-enum-comparison; nothing else
---
The repo follows [[lint-config-shape]] with two recorded exceptions, both
present when the migration PR was approved:
- `typescript/prefer-readonly-parameter-types` is `"off"` in
`scripts/project-lint.ts`, as in the explicit-types template, because
Node/web types (`Response`, `Stats`, `RequestInit`) can never satisfy it.
- `vite.config.ts` has one `overrides` entry, for `scripts/**` only:
`typescript/no-unsafe-enum-comparison` off, because the lint plugin compares
AST node `type`s, which the AST types declare as an enum. This is the
tooling exception [[lint-overrides-policy]] allows.
Oxlint's default size limits apply to `src/`; the migration split `run.ts`
into `run-context.ts`, `agents.ts`, and `agent-{claude,codex,opencode}.ts`,
and `post.ts` into `post.ts` + `post-artifacts.ts`, rather than raising them.
Sequential awaits in loops became recursion or a `reduce` chain to stay
sequential. Add no other opt-out without the user.
@@ -0,0 +1,25 @@
---
description: src/command.ts reproduces Deno.Command on child_process.spawn (stdio defaults, clearEnv, uid/gid, 128+signo); src/errors.ts maps NotFound to ENOENT/ESRCH
---
The Deno code was ported one for one; two small modules keep the old shapes
so callers barely changed:
- **`src/command.ts`** wraps `child_process.spawn` with `Deno.Command`'s API:
`output()` and `spawn()`, `clearEnv`, `uid`/`gid` (the agent isolation runs
each profile as its own UID, 20000+), and Deno's stdio defaults: stdin
inherited; stdout/stderr piped for `output()`, inherited for `spawn()`. A
signal exit maps to status `128 + signo`. Status settles on process exit,
not on stream close. Use it instead of calling `node:child_process`
directly so isolation and status handling stay uniform.
- **`src/errors.ts`** replaces `Deno.errors.NotFound` checks with an
`ENOENT`/`ESRCH` code check.
- `makeTempFile` became an exclusive `wx` create with mode `0600`.
- `src/json.ts` holds type guards used instead of `as` casts on parsed JSON.
- `process` and `Buffer` must come from `node:` imports:
`no-restricted-globals` in `scripts/project-lint.ts` maps Deno's
`no-window`/`no-process-global`/`no-node-globals` there
([[lint-parity-never-shrinks]]).
Related: how each agent CLI reports bad credentials is in
[[agent-cli-auth-failures]].
@@ -0,0 +1,31 @@
---
description: The action runs src/<phase>.ts directly with node on node:24-bookworm (no build); protect-source.sh copies only src/*.ts, package.json, profiles.json, prompt.md
---
`action.yml` is a composite action running three phases as
`node "<source>/src/{prepare,run,post}.ts"`. Node's built-in type stripping
runs the TypeScript; there is **no build step, no bundle, and no runtime
dependency**. Hence `tsconfig.json` has `erasableSyntaxOnly` and
`allowImportingTsExtensions`: only erasable syntax (no `enum`, no parameter
properties, no decorators) and relative `.ts` imports.
- **Image:** the steps assume the `node:24-bookworm` job container. Node must
be 24.2+ for `import.meta.main` in `post.ts`; `check.yml` prints
`node --version` to confirm the cached image's minor.
- **Root-owned source copy:** `scripts/protect-source.sh` copies the action
into `$RUNNER_TEMP` so an agent editing the checkout can't swap a later
phase. It copies exactly `src/*.ts`, `package.json` (`"type": "module"`
makes Node load the sources as ES modules), `profiles.json`, and
`prompt.md`. **A new runtime file or subdirectory must be added there**, or
it is missing at run time. `src/` is flat; a subfolder would not be copied.
- `tests/entrypoints.test.ts` runs each phase under real Node with a cleared
environment and expects its first "X is not set" error, which proves the
whole import graph loads without a build. Keep it in sync with the phases.
- **None of the app build conventions apply here:** no `vp build`, no
frontend or Rust/Tauri output paths, no Containerfile, no decorators
plugin, no lit-check, no generated JS ([[app-vite-build-output]],
[[app-containers-and-ci]], [[app-generated-js-as-ts]] describe the other
apps). Don't add any of them.
- **CI** (`.gitea/workflows/check.yml`) runs on the self-hosted runner's
default image as root: `npm ci`, `npx vp check`, `npx vp test`. It does not
use `setup-vp`. YAML stays declarative ([[yaml-stays-declarative]]).
@@ -0,0 +1,22 @@
---
description: Isolation and credential tests need root and use test.skipIf(!IS_ROOT); locally 5 skip, CI runs as root so all 61 run; verify isolation changes in node:24-bookworm as root
---
Some tests exercise real UID isolation, process cleanup, and `prepare.ts`
credential cleanup. They need root and are written as
`test.skipIf(!IS_ROOT)(...)` (`IS_ROOT` from `src/testing.ts`), so they show
as skipped instead of silently passing.
- As a normal user `vp test` passes 56 and skips 5. In CI (self-hosted
runner, root) all 61 run.
- When changing `isolation.ts`, `runtime.ts`, `credentials.ts`, `prepare.ts`,
or `command.ts`, run the suite as root in `node:24-bookworm` (e.g. a
container with the repo mounted) before pushing; a green non-root run
proves nothing about those paths.
- The migration also ran an end-to-end smoke as root with fake
`codex`/`claude`/`npx` binaries and a fake Gitea through
`protect-source.sh` -> `prepare.ts` -> `run.ts` -> `post.ts`; repeat that
for changes to the phase flow ([[bot-agents-runtime-no-build]]).
- `src/testing.ts` holds the shared test helpers; tests follow
[[tests-layout-vitest]] (unit tests in `src/*.test.ts`, repo-level ones in
`tests/`).
@@ -0,0 +1,44 @@
---
description: Keep TC39 standard decorators with accessor; Oxc can't lower them, so scripts/decorators.ts (esbuild Vite plugin) does; never legacy decorators
---
Applies to every repo that uses decorators (the Lit frontends).
**Decision (2026-10-16):** keep standard (TC39) decorators with `accessor`,
exactly as the Deno code had them. No legacy decorators.
```ts
@customElement("app-card")
export class AppCard extends LitElement {
@property() accessor name = ""; // keep this form
}
```
**Gotcha, verified 2026-10-16:** Vite 8's Oxc transformer parses standard
decorators but can **not** lower them, so `vp build`, `vp dev`, and `vp test`
emit decorator syntax browsers and Node don't run. Check again on each Vite+
upgrade; when Oxc can lower them, delete the plugin and the `esbuild`
devDependency.
**Fix in use:** `scripts/decorators.ts`, a small Vite plugin backed by
`esbuild` (devDependency), registered in `vite.config.ts` `plugins` so it
covers build, dev server, and Vitest alike:
- `enforce: "pre"`, so it runs before Oxc;
- transforms only `.ts` modules whose source contains `@`;
- `esbuild.transform(code, { loader: "ts", target: "es2022", sourcemap,
sourcefile: id })`, returning `{ code, map }`.
It is tooling under `scripts/`: no tests, and it must pass every lint rule.
**The options as presented to the user:**
| Option | What | Why not |
| ------ | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| A | Legacy decorators: two tsconfig flags (`experimentalDecorators`, ...) and a codemod removing `accessor` | the user ruled out legacy decorators |
| B | Wait for Oxc to lower `accessor` decorators | not available yet; blocks the migration |
| C | No decorators: `static properties = {...}` + `declare` fields | rewrites every component |
| D | A transform step in the Vite build | chosen, with esbuild as the transformer (above) |
Don't add `experimentalDecorators` to `tsconfig.json`, and don't strip
`accessor` from fields.
+45
View File
@@ -0,0 +1,45 @@
---
description: Every repo moved from Deno to Node + Vite+ in Oct 2026 because Cloudflare bought Deno; nothing goes back to Deno
---
On 2026-10-12 the user moved every project from Deno to Node.js + Vite+
(`vp`). Cloudflare bought Deno on 2026-10-09; the runtime gets roughly 12
months of maintenance and Deno Deploy shuts down around April 2027. None of the
repos used Deploy or KV, so the move is about the toolchain, not hosting.
Do not reintroduce `deno.json`, `deno.lock`, `Deno.*` APIs, `jsr:` or `npm:`
specifiers, `mod.ts` entries, or `deno task` anywhere, docs and scripts
included.
## Order the migration followed
1. The custom Deno lint plugins were ported to npm first, as
`@cunarist/eslint-plugin-explicit-types`, `-import-check`, and `-lit`.
`cunarist/eslint-plugin-explicit-types` is the template every other repo
copies (config shape, `scripts/project-lint.ts`, AGENTS.md, notes).
2. Then `commons/bot-agents` (six repos use it as an action), then the apps:
home, eximium, stockvibe, brain, memona, memona-home.
3. deno-practice, interview-practice, web-landing, and vemium were left out
(archivable). Old `cunarist/deno-*` repos are archived one at a time, and
only after their `eslint-plugin-*` replacement is published to npm.
## What "migrated" means
- Same behavior. A migration PR changes tooling, not features.
- Lint coverage is at least what Deno enforced: [[lint-parity-never-shrinks]].
- Only three config files and only `.ts`: [[toolchain-node-vite-plus]],
[[ts-only-files-and-imports]].
- Deno.test suites became Vitest: [[tests-layout-vitest]].
The custom plugins' Deno names map like this:
| Deno | npm |
| ----------------------------------------------- | ---------------------------------------- |
| `jsr:@cunarist/deno-lint-plugin-explicit-types` | `@cunarist/eslint-plugin-explicit-types` |
| `jsr:@cunarist/deno-import-check` (`./lint`) | `@cunarist/eslint-plugin-import-check` |
| `jsr:@cunarist/deno-lint-plugin-lit` | `@cunarist/eslint-plugin-lit` |
| `scripts/deno-lint-plugin.ts` (per app) | `scripts/project-lint.ts` (per app) |
Until those packages are on npm, a repo that depends on them fails `npm ci` in
CI; that is expected and stated in the PR, never worked around by committing
tarballs or `file:` dependencies.
+52
View File
@@ -0,0 +1,52 @@
---
description: Work on a branch, plain imperative commits without AI trailers, one PR with the Claude Code footer, review until approved, never merge yourself; squash only
---
## Branches and commits
- Never push to `main`. Every change goes through a branch and a PR, even a
one-line fix.
- Commit messages are plain imperative sentences ("Port the import-check
rules to an ESLint plugin"). No conventional-commit prefixes, no AI
trailers (`Co-Authored-By: Claude`, "Generated with ..."), no emoji.
- A version bump is its own commit titled `Version X.Y.Z` ([[publishing-ci-only]]).
- When several agents share a branch, each works in its own `git worktree`,
runs `git pull --rebase` before pushing, and retries a rejected push after
rebasing. Remove the worktree and temp branch afterwards.
- `vp check`, `vp test`, and the build pass locally before every push.
## Pull requests
- One PR per repo for a migration, everything included (size-rule refactors
too).
- The body covers: summary, what changed, deviations and risks, verification
done, and open questions. It ends with exactly this line:
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
```
- Then run the review cycle (skill `review-cycle`): on Capsulizers Gitea repos
that have `.gitea/workflows/agents.yml`, comment `@bot` and address the `bot`
account's review; elsewhere, a subagent reviews the diff and posts the
review as a PR comment. Repeat until fully approved, then ask the user to
review.
## Merging
- **Never merge on your own.** The user merges, unless they explicitly allow
it for one specific PR.
- GitHub repos are squash-merge only, so a PR lands as one commit titled after
the PR.
- Also never, without the user: publish, archive a repo, or delete a branch
the user may still use.
## Hosts
- **cunarist** and **temeddix**: GitHub only, CI in `.github/workflows`, use
the `gh` CLI.
- **Capsulizers**: Gitea at git.capsulizers.com (Gitea 1.26), CI in
`.gitea/workflows`, PRs through the Gitea API. Push and open PRs as the
user's own account (credentials in the user's environment; source them in a
subshell and never print them); `bot` is a separate review account. The
org's codified rules live in the `commons/code-rules` README.
+43
View File
@@ -0,0 +1,43 @@
---
description: Icons are .ts modules exporting a typed Lit svg template (memona's style), one per file behind an index.ts; never .svg imports as text
---
memona's way of declaring icons is the standard (2026-10-14). An icon is a
`.ts` module that exports a Lit `TemplateResult` holding the inline SVG:
```ts
// src/icons/lock.ts
import { html, type TemplateResult } from "lit";
/** Font Awesome Free's solid `lock`, as its SVG source. */
export const lockIcon: TemplateResult = html`<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512"
>
<path fill="currentColor" d="M128 96l0 64 ..." />
</svg>`;
```
```ts
// usage
import { lockIcon } from "#icons";
render() { return html`<span class="icon">${lockIcon}</span>`; }
```
- One icon per file, camelCase name with an `Icon` suffix, a doc comment
saying what it depicts, exported through `src/icons/index.ts`.
- Use `fill="currentColor"` so CSS controls the colour.
- Keep a third-party licence comment that the SVG source carries (Font
Awesome Free's `<!--! ... -->` notice) inside the template.
**Replaced pattern:** eximium, stockvibe, and memona-home imported Font
Awesome `.svg` files as text in `icons.ts`:
```ts
import lock from "@fortawesome/fontawesome-free/svgs/solid/lock.svg" with { type: "text" }; // no
```
That breaks [[ts-only-files-and-imports]] (non-`.ts` import plus import
attributes) and was converted to memona's style during the frontend
migration. Before writing new icon code in any repo, read how that repo (or
memona) already declares icons and match it.
+48
View File
@@ -0,0 +1,48 @@
---
description: Shared code comes through # aliases in package.json imports, each directory's entry is index.ts, barrels keep imports few; never ../ out of a directory
---
## `#` aliases
Shared code is imported through Node subpath imports declared in
`package.json` `imports`, never by a relative path that climbs out of a
directory:
```jsonc
"imports": {
"#helpers": "./src/helpers/index.ts",
"#components/*": "./src/components/*"
}
```
```ts
import { typeNameTail } from "#helpers"; // yes
import { typeNameTail } from "../helpers/ast.ts"; // no: import/no-relative-parent-imports
```
Node, TypeScript, Vite, Vite+'s config bundler, and Oxlint's plugin loader
all resolve them natively, so `.ts` source runs unbuilt. `vp pack` inlines them,
so they never reach package consumers.
- Deno's trailing-slash mappings (`"#components/"`) don't exist in Node; they
become patterns, `"#components/*": "./src/components/*"`.
- Aliases point at `.ts` files only. There is no `#package` alias
([[ts-only-files-and-imports]]).
- In repos linted by import-check, the key order of `imports` is the layer
order `enforce-layer-order` checks; add a new alias at the right layer.
- Keep test-only helpers in their own alias (e.g. `#testing`) so they never
land in a bundle.
## `index.ts` entries and barrels
- Every directory's entry file is `index.ts`, never `mod.ts` (decided
2026-10-12; import-check's `enforce-mod-file` now requires `index.ts`).
- A directory used from outside exposes its API through its `index.ts`
barrel, and callers import the barrel (`#ui`), not files inside it
(`no-barrel-bypass`).
- Barrels are also the standard fix for `import/max-dependencies`: one barrel
import replaces many direct ones ([[lint-overrides-policy]]).
- `import/no-cycle` is on; a barrel must not create a cycle.
Relative imports inside one directory (`./sibling.ts`) are fine, with the
`.ts` extension written out.
+47
View File
@@ -0,0 +1,47 @@
---
description: vite.config.ts lint uses Oxlint categories at error with default limits, names no rule itself, and spreads configs from the plugins and scripts/project-lint.ts
---
The lint config is short and category-driven (user decisions 2026-10-14/15):
turn on whole categories, then list only what parity or a real need adds.
```ts
lint: {
plugins: ["typescript", "unicorn", "oxc", "import"],
jsPlugins: ["@cunarist/eslint-plugin-explicit-types", "./scripts/project-lint.ts"],
categories: { correctness: "error", suspicious: "error", pedantic: "error", perf: "error" },
rules: { ...explicitTypes.configs.concrete.rules, ...projectLint.configs.project.rules },
ignorePatterns: ["src/generated/**"], // generated files only, see below
options: { typeAware: true, typeCheck: true, maxWarnings: 0, reportUnusedDisableDirectives: "error" },
},
```
- **Categories:** `correctness`, `suspicious`, `pedantic`, `perf` at `error`.
`style` and `restriction` are never turned on wholesale; pick single rules
from them in `configs.project`. Extra rules that categories bring are fine.
- **Default limits stay** (2026-10-16): pedantic's `max-lines` 300,
`max-lines-per-function` 50, `max-classes-per-file` 1,
`max-dependencies` 10, `max-params` 3, `complexity` 20. Don't raise them to
make code pass; refactor (see [[lint-overrides-policy]]). Apps follow this
strictly. The plugin template already turns off `max-lines`,
`max-lines-per-function`, `typescript/prefer-readonly-parameter-types`, and
`typescript/no-unsafe-enum-comparison` because AST visitors fight them; do
not add new opt-outs anywhere without asking the user.
- **`vite.config.ts` names no rule.** Every rule id, built-in ones included,
lives in `configs.project` of `scripts/project-lint.ts`
([[project-lint-plugin]]). `vite.config.ts` only spreads configs.
- **Published plugins are consumed through their configs**: ESLint flat config
uses `plugin.configs.<name>`; Oxlint lists the package in `jsPlugins` and
spreads `plugin.configs.<name>.rules`, since Oxlint plugins carry no configs.
Use the configs equivalent to what the repo enabled under Deno.
- **`maxWarnings: 0`** keeps Deno's "every diagnostic fails" behaviour.
- **`ignorePatterns`** is only for generated files (compiled output, codegen).
Hand-written code is never ignored.
- **No lint disable comments**: [[lint-no-disable-comments]].
- **Parity:** [[lint-parity-never-shrinks]]. **Naming:** [[naming-id-match]].
- Don't add a second import sorter; import-check's `enforce-import-order` is
the only one, and Oxfmt's `sortImports` stays off.
`perf` came last: in the template it flagged nine `.includes` on constant
arrays, fixed as `ReadonlySet` + `.has()` rather than by turning a rule off.
That is the expected response to any new rule: change the code.
@@ -0,0 +1,30 @@
---
description: Every eslint-/oxlint-disable or -enable comment is banned in every form; three checks enforce it together, keep all three
---
Banned (2026-10-16): `eslint-disable`, `eslint-disable-line`,
`eslint-disable-next-line`, `eslint-enable`, and the same with `oxlint-`, as
line or block comments, anywhere in the repo. Fix the code; if the code truly
can't be fixed, ask the user, don't silence the rule. Old
`deno-lint-ignore` lines are removed during the migration and their cause
fixed.
Oxlint (1.87) has no `noInlineConfig` and no `eslint-comments` rules, and
`respectEslintDisableDirectives: false` only ignores `eslint-*` directives. So
three checks enforce the ban together, and none is redundant:
1. `project/no-disable-comments` (custom, in `scripts/project-lint.ts`) fails
any directive.
2. `unicorn/no-abusive-eslint-disable` fails a directive that names no rule,
which would otherwise silence the custom rule on its own line.
3. `options.reportUnusedDisableDirectives: "error"` fails a directive naming
`project/no-disable-comments` (the custom rule skips that one, so the
directive silences nothing and is reported as unused).
Every form was probed and fails `vp check`.
A rule that fires on legitimate code is a rule problem: change the rule or its
options in `configs.project` with the user's agreement, not the call site.
Known example: rule records are `Map`s keyed by id rather than object
literals with kebab-case keys, which keeps naming rules happy without any
directive.
+45
View File
@@ -0,0 +1,45 @@
---
description: No lint overrides for source files; narrow ones only for scripts/**; generated files go in ignorePatterns; refactor (barrels, splits) instead of raising limits
---
Final policy (2026-10-16). The user considers per-file `overrides` bad
practice.
- **Source files: never.** No `overrides` entry may target `src/**` or any
other hand-written production or test code.
- **The one exception** is in lint-plugin repos: `import/max-dependencies`
off for `src/rules/index.ts`, the rule registry, which must import every
rule. It is not a precedent for apps.
- **`scripts/**`:** a narrow override is allowed when a tooling script truly
needs it, e.g. `no-console` in a CLI script.
- **Generated files:** list them in `ignorePatterns`, never `overrides`.
- **Anything else needs the user's approval first.** When the user is away,
don't add it: refactor, or leave the violation failing and log the question.
## Fix the code instead
Oxlint's defaults stay; violations are refactored without behavior change,
covered by the tests, inside the same migration PR.
| Rule | Usual fix |
| ------------------------- | ------------------------------------------------------- |
| `import/max-dependencies` | import a barrel (`#ui`) instead of many direct modules |
| `max-lines` | split the file by responsibility |
| `max-lines-per-function` | extract helpers; split a long Lit template into parts |
| `max-classes-per-file` | one class per file |
| `max-params` | pass one options object |
| `prefer-set-has` | `const K: ReadonlySet<string> = new Set([...])`, `.has` |
```ts
// before: 14 imports, over the limit of 10
import { a } from "#ui/button";
import { b } from "#ui/dialog";
// after: one barrel
import { a, b } from "#ui";
```
Measured on memona before the move (Oxlint 1.87, default limits): 921
pedantic hits in 274 files, of which `max-lines-per-function` 206,
`max-lines` 145, `max-classes-per-file` 59, `max-dependencies` 45 (median 14
imports, worst 29). The user chose refactoring over raising limits or
matching memona's old hugoalh limits (2048 lines per file, no function limit).
@@ -0,0 +1,46 @@
---
description: Lint coverage never drops below what Deno enforced; every Deno rule maps to an Oxlint rule, a plugin rule, the compiler, or our own rule
---
Hard rule (2026-10-12): after the move, a repo has at least the lint coverage
it had under Deno: built-in rules (the `recommended` and `jsr` tags plus
opt-ins like `camelcase`, `eqeqeq`, `guard-for-in`, `no-console`,
`no-eval`, `no-throw-literal`, `no-non-null-assertion`,
`verbatim-module-syntax`), `@hugoalh` rules, and the custom plugins. Rules may
be added, never lost. Formatting may change. "Gap" is not an acceptable end
state: a Deno rule Oxlint lacks is replaced by a plugin rule or our own rule in
[[project-lint-plugin]].
Before removing any line from `configs.project`, find what else covers it.
## How the template mapped Deno's 97 rules
- **Same rule** in `configs.project` at `error`.
- **Renamed equivalent:** `no-new-symbol` → `no-new-native-nonconstructor`;
`no-window` → `no-restricted-globals: ["error", "window"]`;
`ban-untagged-ignore` → `unicorn/no-abusive-eslint-disable`;
`ban-unused-ignore`, `ban-unknown-rule-code` →
`reportUnusedDisableDirectives`; `camelcase` → built-in `id-match`
([[naming-id-match]]).
- **Caught by the parser or `typeCheck`:** `no-dupe-args`, `no-octal`,
`no-await-in-sync-fn`, `no-import-assertions` (TS2880), `no-sloppy-imports`
(TS2835), `verbatim-module-syntax` (TS1484).
- **`no-slow-types`:** `isolatedDeclarations`, enforced by `vp pack` (TS9010)
in packages.
- **Vacuous:** JSX rules (no `.tsx` exists) and Deno-only rules.
- **`@hugoalh` rules** map to Oxlint built-ins in `configs.project`.
- **import-check's CLI cycle check** → `import/no-cycle`.
The template's list in `configs.project` is the baseline to copy: `curly`,
`guard-for-in`, `no-console`, `no-empty`, `no-regex-spaces`,
`no-restricted-globals`, `no-var`, `prefer-const`, `import/no-cycle`,
`import/no-relative-parent-imports`, `typescript/adjacent-overload-signatures`,
`typescript/consistent-type-imports`, `typescript/no-empty-interface`,
`typescript/no-explicit-any`, `typescript/no-namespace`,
`typescript/no-non-null-assertion`, the two `project/*` rules,
`unicorn/no-abusive-eslint-disable`, `id-match`, and
`typescript/switch-exhaustiveness-check` with
`considerDefaultExhaustiveForUnions`.
Each migration PR states its parity mapping (or links the template's table in
cunarist/eslint-plugin-explicit-types#1) so the user can check it.
+40
View File
@@ -0,0 +1,40 @@
---
description: lit-analyzer runs from scripts/lit-check.ts, which maps "typescript" to the aliased typescript-6 devDependency; npm run lit-check in CI
---
Applies to Lit repos that ran `cunarist/deno-lit-analyzer` under Deno.
lit-analyzer is built on TypeScript's JavaScript compiler API and is run
against TypeScript 6 (the open question was whether it works with TS 7 /
tsgo). The repo installs TS 6 under another name and redirects lit-analyzer
to it, so the rest of the toolchain is unaffected (decided 2026-10-16):
```jsonc
// package.json
"scripts": { "lit-check": "node scripts/lit-check.ts" },
"devDependencies": { "typescript-6": "npm:typescript@^6", "lit-analyzer": "..." }
```
```ts
// scripts/lit-check.ts (shape)
import { registerHooks } from "node:module";
registerHooks({
resolve(specifier, context, next) {
// when lit-analyzer asks for "typescript", hand it TS 6
return next(specifier === "typescript" ? "typescript-6" : specifier, context);
},
});
// then run lit-analyzer
```
- Node runs the `.ts` script directly (type stripping), so no build step.
- **Gotcha:** the obvious `await import("lit-analyzer/cli.js")` fails
`project/no-non-ts-imports` (a `.js` subpath). Import the package entry, or
spawn the package's bin with `node:child_process`, instead.
- CI runs `npm run lit-check` as its own step.
- It is tooling under `scripts/`: no tests; a narrow `overrides` entry for it
(e.g. `no-console`) is allowed if truly needed.
Don't replace this with a global TypeScript downgrade, a second
`tsconfig`, or a `.js` wrapper script.
+48
View File
@@ -0,0 +1,48 @@
---
description: Deno's camelcase is Oxlint id-match ^_*[^_]*_*$|^[^a-z]*$; leading _ for unused vars; rename snake_case imports; PascalCase types; single-letter type params
---
Oxlint has no `camelcase`. Its built-in `id-match` replaces it, set in
`configs.project` (user decision 2026-10-16, replacing a ~400-line port):
```ts
"id-match": ["error", "^_*[^_]*_*$|^[^a-z]*$", {
properties: false, classFields: false, onlyDeclarations: false,
ignoreDestructuring: true,
}],
```
**Meaning** (Deno's `is_underscored`): after trimming leading and trailing
underscores, the name has no `_`, **or** the name has no lowercase ASCII
letter. `id-match` can't tell what kind of name it sees, so one regex accepts
every valid style; other rules decide which style goes where.
```ts
const userName = 1; // ok
class UserStore {} // ok here; pascal-case-types enforces PascalCase
const MAX_SIZE = 10; // ok: no lowercase, so UPPER_SNAKE passes
const _unused = 1; // ok: leading _ marks an unused binding
const user_name = 1; // error
```
- **Leading underscores stay allowed** for unused parameters and variables
(`items.map((_item, index) => index)`). The user considered banning them and
kept them for exactly this.
- **Rename snake_case imports at the import** (user wants this flagged):
`import { parse_args as parseArgs } from "some-lib";`
- `declare` lines and snake_case globals are rare and accepted as they are.
- **Types:** classes, interfaces, type aliases, enums are PascalCase
(`pascal-case-types` from explicit-types' `naming` config); string-union
members are UPPER_SNAKE_CASE; no `enum`.
- **Type parameters:** the user wants single capital letters (`T`, `K`), not
`Item`; explicit-types PR 2 adds `single-capital-type-params` to `naming`.
Write new code that way already.
- No autofix exists for `id-match`; rename by hand.
**Known differences from Deno's `camelcase`** (against its 130 test cases):
it misses 9 (unquoted object keys like `{ bar_baz: 1 }` and `{ bar_baz }`,
shorthand destructuring in parameters, `{ a_b: a_b }`,
`{ foo: bar_baz = 1 }`) and over-reports 17 (non-renamed imports, assignments
to unresolved globals, 11 ambient `declare` cases). Beyond Deno it also checks
type parameters, interface and type-literal members, class methods, and
labels. The user accepted these differences.
+51
View File
@@ -0,0 +1,51 @@
---
description: package.json key order is metadata, scripts, exports, imports, devDependencies, dependencies; standard fields only; no peerDependencies or packageManager
---
## Key order (2026-10-14)
Metadata first, then `scripts`, `exports`, `imports`, `devDependencies`,
`dependencies`. Oxfmt would re-sort the file, so `vite.config.ts` sets
`fmt: { sortPackageJson: false }`; keep it, and keep the order by hand.
```jsonc
{
"name": "@cunarist/eslint-plugin-x",
"version": "0.1.0",
"description": "...",
"keywords": ["eslint", "eslint-plugin", "eslintplugin", "oxlint"],
"license": "MIT",
"repository": { "type": "git", "url": "git+https://github.com/..." },
"type": "module",
"files": ["dist"],
"engines": { "node": ">=22" },
"eslint-doc-generator": {/* tool options that can only live here */},
"scripts": { "docs": "eslint-doc-generator" },
"exports": { ".": "./dist/index.js" },
"imports": { "#helpers": "./src/helpers/index.ts" },
"devDependencies": { "vite-plus": "^1.1.0" },
"dependencies": {},
}
```
The `imports` key order can carry meaning: import-check's
`enforce-layer-order` reads it as the layer order. Never reorder it casually.
## Fields
- **Standard fields only.** `engines` has only `node` (never `engines.bun`).
A tool-owned key (`eslint-doc-generator`) is allowed only because the tool
reads nothing else and no other config file is allowed.
- **No `packageManager`.** npm is detected from `package-lock.json`.
- **No `peerDependencies`, ever** (user preference, 2026-10-14). If something
is needed at runtime, it goes in `dependencies`.
- **`dependencies`** = what runs or is type-referenced at runtime by consumers
(the lit plugin ships `parse5`, `oxc-parser`, `oxc-resolver`, and
`@eslint/core` for its public types; they stay external, never bundled).
- **`devDependencies`** = everything else: `vite-plus`, `eslint`,
`@typescript-eslint/parser`, `eslint-doc-generator`, `esbuild`,
`typescript-6`, `@types/node`. ESLint is a devDependency only; plugins need
nothing from it at runtime.
Refresh the lockfile with `npm install` (or `npm install
--package-lock-only` for a version bump) and commit it with the change.
+55
View File
@@ -0,0 +1,55 @@
---
description: scripts/project-lint.ts is the repo-local plugin "project" holding the repo's rule list (typed DummyRuleMap) and repo-only rules; never published, no tests
---
Every repo has `scripts/project-lint.ts`, copied from
`cunarist/eslint-plugin-explicit-types` and extended. It is a repo-local
Oxlint plugin with `meta.name` `"project"`, loaded through `jsPlugins` in
`vite.config.ts`. It holds:
- **`configs.project.rules`**: the repo's whole rule list beyond the published
plugin configs: Deno parity rules, extras, per-rule options, and opt-outs,
built-in Oxlint ids included (`curly`, `id-match`, `import/no-cycle`, ...).
- **Custom rules**, enabled as `project/<rule>`. Every repo has
`no-disable-comments` and `no-non-ts-imports`. Apps also port the rules of
their old `scripts/deno-lint-plugin.ts` here under the same names and
behavior (`no-self-sync`, `app-module-conventions`, `restricted-types`, ...).
Rules and plugin stay in this one file; don't add `scripts/rules/`.
```ts
import type { DummyRuleMap } from "vite-plus/lint";
const rules: DummyRuleMap = {
"id-match": ["error", "^_*[^_]*_*$|^[^a-z]*$", {/* ... */}],
curly: "error",
"project/no-disable-comments": "error",
};
const plugin = {
meta: { name: "project" },
rules: { "no-disable-comments": noDisableCommentsRule /* ... */ },
configs: { project: { rules } },
};
export default plugin;
```
## Why `DummyRuleMap` and an unannotated plugin (decided 2026-10-15)
- `DummyRuleMap` (re-exported by `vite-plus/lint`) is Oxlint's type for
`OxlintConfig.rules`. Despite the name it lists every built-in rule with its
options, so a typo in a rule id or option is a type error, not just a CI
failure. Without it, TypeScript widens `"error"` to `string`; `as const`
doesn't help because Oxlint wants mutable tuples.
- `@eslint/core`'s `Plugin` is too loose to read back: `configs` is optional,
any key may be missing, and a config may be an array, so
`projectLint.configs.project.rules` fails in `vite.config.ts` (and `!` is
banned). Optional chaining still leaves the array case.
- Oxlint's own `Plugin` type (`vite-plus/lint/plugins`) has no `configs`.
- Two hand-written interfaces worked but were dropped as noise; a plain
config object was rejected because custom rules need the plugin form anyway
and every repo should look the same.
`scripts/` is tooling, not production code: never packed or published, and
no tests. Narrow `overrides` for `scripts/**` are allowed when a script truly
needs one (e.g. `no-console`); see [[lint-overrides-policy]].
+37
View File
@@ -0,0 +1,37 @@
---
description: Packages publish only from CI when a GitHub release vX.Y.Z is published, via npm trusted publishing; never npm publish by hand; notes live only on the release page
---
Standing convention (2026-10-12), applied to every package:
- **Only CI publishes.** Publishing a GitHub release `vX.Y.Z` triggers
`.github/workflows/publish.yml`. Never run `npm publish` locally and never
push a bare `v*` tag. Use the `release` skill, and only when the user asked
for a release.
- **The workflow** checks that the tag equals `v` + `package.json` `version`,
runs the full CI as a reusable workflow (every Node version in the matrix),
then `vp pack` and `npm publish --access public --provenance`. A release
marked pre-release publishes under the `next` dist-tag, others `latest`.
It runs on Node 24 (npm 11, which trusted publishing needs) with no cache.
- **Auth is npm trusted publishing** (OIDC, `id-token: write`), which also
attaches provenance. npm only allows a trusted publisher on a package that
already exists, so the very first release bootstraps with a short-lived
token in the `NPM_TOKEN` repo secret (the workflow uses it only when set).
Afterwards the user configures the trusted publisher for `publish.yml`,
deletes the secret, and sets the package to disallow tokens.
- **Release notes live only on the GitHub release page**
(`--generate-notes`, edited there). There is no `CHANGELOG.md`; never add
one.
- **The version lives in two places in plugin packages:** `package.json` and
`meta.version` in `src/index.ts`; `tests/package-meta.test.ts` fails if they
differ.
- **Before 1.0**, a change that breaks users (renamed rule id, a rule that
fires on more code, removed behavior) is a minor bump.
- **Before a release**, pack the package and lint a known-bad and a known-good
file from separate consumer projects, once through `vp lint` with
`jsPlugins` and once through ESLint 9 with the configs. On the lit plugin
this caught two bugs that 313 unit tests missed.
The cunarist lint plugins (explicit-types, import-check, lit) are published
together, with a token the user provides; the old `deno-*` repos are archived
only after that, one at a time.
+36
View File
@@ -0,0 +1,36 @@
---
description: Vitest via vp test; unit tests sit next to code as *.test.ts, regression/integration tests in tests/ via the public entry; scripts/ has no tests
---
Decided 2026-10-14 for all repos.
- **Runner:** Vitest through `vp test`. Import test APIs from
`vite-plus/test` (`describe`, `expect`, `it`).
- **Unit tests** sit next to the code: `src/foo/bar.ts` is tested in
`src/foo/bar.test.ts`. Vitest's default include finds them; no `test`
config needed. Packages publish only `dist/`, so colocated tests cost nothing
in the tarball.
- **Regression and integration tests** live in the top-level `tests/` folder
and use only the public entry point (an alias like `#plugin`), never
internals. `vp test tests` runs just these.
- **No in-source tests** (`import.meta.vitest`).
- **`scripts/` gets no tests** (2026-10-15): it is repo tooling, not
production code (`project-lint.ts`, `decorators.ts`, `lit-check.ts`,
release helpers).
- Test files follow every lint rule; no relaxed overrides for tests.
## Migrating Deno tests
`Deno.test` suites were converted by a codemod
(`~/.local/share/deno-migration/src/convert.ts` on the user's machine), then
its TODOs finished by hand. Watch for:
- `@std/assert` → `expect` (`assertEquals` → `toStrictEqual` or `toBe`,
`assertThrows` → `toThrow`, `assertRejects` → `rejects.toThrow`);
- `Deno.*` file, env, and process APIs → `node:fs`, `process.env`,
`node:child_process`;
- `t.step` → nested `describe`/`it`;
- sanitizer options, which Vitest doesn't have.
A migration PR must keep every test case; a dropped case is lost coverage.
Tests must pass locally before any push.
@@ -0,0 +1,53 @@
---
description: Node.js 22+ with Vite+ (vp) and npm; only package.json, tsconfig.json, vite.config.ts plus a committed package-lock.json
---
## The stack
- **Runtime:** Node.js 22 or newer. `engines` is `{ "node": ">=22" }` and
nothing else. Containers and the publish job use Node 24 (LTS); a repo may
need more where a feature demands it (bot-agents uses Node 24.2+).
- **Bun is not supported** (dropped 2026-10-15). No Bun CI legs, docs, or
runtime branches. Oxlint's `RuleTester` refuses to run under Bun anyway, and
`vp` brings its own Node, the way uv brings its own Python.
- **Toolchain:** Vite+ (`vite-plus` devDependency, command `vp`): Vite,
Vitest, Oxlint, Oxfmt, type checking, tsdown.
- **Package manager:** npm everywhere. `package-lock.json` is committed in
apps _and_ libraries (user agreed 2026-10-14: `npm ci` needs it). There is no
`packageManager` field; Vite+ detects npm from the lockfile.
## Config files
Exactly three, plus the lockfile: `package.json`, `tsconfig.json`,
`vite.config.ts`. No `.oxlintrc.json`, `.prettierrc`, `eslint.config.*`,
`vitest.config.*`, `.npmrc`, or `tsdown.config.*`. Tool options go into
`vite.config.ts` keys (`lint`, `fmt`, `test`, `pack`) or, when a tool only
reads `package.json` (eslint-doc-generator), into a `package.json` key.
The target is a short `vite.config.ts`, around 30 lines (not strict).
## Commands
| Command | Does |
| ------------------------ | ---------------------------------------------------------- |
| `vp check` | format check, Oxlint (type-aware), and type check together |
| `vp check --fix` | the same, applying formatting and safe fixes |
| `vp test` | Vitest; `vp test tests` runs only the top-level `tests/` |
| `vp build` | Vite production build (apps) |
| `vp dev` | Vite dev server (apps) |
| `vp pack` | tsdown library build into `dist/` (packages) |
| `npm ci` / `npm install` | install; never another package manager |
`vp check`, `vp test`, and the build (`vp build` or `vp pack`) must all pass
before any push. CI installs the toolchain with `voidzero-dev/setup-vp`.
## Gotchas found during the migration
- `vp check` does **not** report `isolatedDeclarations` errors (TS9010); only
`vp pack` does, so packages run `vp pack` in CI on every PR.
- Oxfmt sorts `package.json` keys by default, which destroys the required key
order. Set `fmt: { sortPackageJson: false }` ([[package-json-shape]]).
- Oxlint and Oxfmt skip what `.gitignore` lists; that is the only reason
`dist/` and `node_modules/` are not linted. Keep build output gitignored.
- Vite+ bundles `vite.config.ts` itself and resolves `#` imports in it.
- Oxlint loads a `.ts` plugin file directly on Node 24 (`jsPlugins`), so
repo-local lint plugins need no build step.
@@ -0,0 +1,48 @@
---
description: Every file is .ts (no js/mjs/cjs/jsx/tsx, configs included) and source imports only .ts; CSS comes from HTML, JSON/SVG become .ts modules
---
The user's rule, from the Deno days and restated for Node (2026-10-12): "No
JS, not even JSX/TSX, only TS." Source, scripts, tests, and configs are all
plain `.ts`. Build output in a gitignored folder is the only JS.
`tsconfig.json`: no `allowJs`, no `jsx`, and the template's strict flags
(`verbatimModuleSyntax`, `erasableSyntaxOnly`, `allowImportingTsExtensions`,
`isolatedDeclarations` in packages, `exactOptionalPropertyTypes`,
`noUncheckedIndexedAccess`). If a tool or framework seems to need a JS or TSX
file, stop and flag it instead of adding one.
## Only `.ts` imports (2026-10-14)
`project/no-non-ts-imports` (in `scripts/project-lint.ts`) fails:
- a relative, absolute, `#`, or package-subpath specifier whose last segment
has a non-`.ts` extension: `.css`, `.json`, `.svg`, `.js`, `.mjs`, `.tsx`;
- any `?query` suffix (`?raw`, `?url`, `?inline`);
- any import attributes (`with { type: "json" }`, `with { type: "text" }`).
Bare packages (`lit`, `@types/json-schema`, `lodash.debounce`), `#` aliases,
and `node:` builtins pass. `@fortawesome/x/svgs/a.svg` fails.
```ts
import "./theme.css"; // no
import data from "./scenes.json" with { type: "json" }; // no
import icon from "./lock.svg?raw"; // no
import { scenes } from "#data/scenes"; // yes: scenes.ts
```
## What to do instead
- **CSS:** load it from HTML with `<link rel="stylesheet" href="...">`. Never
import CSS from TS, not even for Vite. (This also removes most of the
`import/no-unassigned-import` hits the `suspicious` category reports.)
- **JSON data:** turn it into a `.ts` module exporting a typed constant.
- **SVG:** a `.ts` module exporting a Lit template; see [[icons-memona-style]].
- **`package.json` fields at runtime:** never `import pkg from
"./package.json"` and there is no `#package` alias. Write the value out in
`.ts` and add a test in `tests/` that reads `package.json` with `node:fs`
and fails on drift (the plugin repos do this for `meta.name`/`meta.version`).
- **Reading a file is not importing it.** `readFileSync` + `JSON.parse` is
fine in tooling and tests.
- **A tool's `.js` entry:** don't `import("pkg/cli.js")`; use the package
entry or spawn the package's bin ([[lit-analyzer-ts6]]).
+53
View File
@@ -0,0 +1,53 @@
---
description: Ask one decision at a time with a code example, follow ESLint/ecosystem conventions and ask before deviating, keep config minimal, and never block when the user is away
---
## Asking
- **One question at a time.** Each decision question stands alone, with the
options and a recommendation.
- **Show code.** The user is new to the ESLint ecosystem and asked for
example code blocks whenever possible (2026-10-14): before/after snippets
or config examples, not prose alone.
```ts
// A. per-file override // B. refactor with a barrel
overrides: [{ files: [...] }] import { a, b } from "#ui";
```
- **Keep answers short.** State the decision and what changed; detail belongs
in `.agents/memory/`, not chat.
- The user often probes with "why?" and "is this the simplest form?". Have
the reason ready, and prefer the option with fewer lines, fewer files, and
fewer custom types when the result is equivalent.
## Conventions over invention
- Follow ESLint's and the wider ecosystem's conventions (typescript-eslint,
unicorn as references): `docs/rules/<rule>.md`, one plugin per package,
flat configs at `plugin.configs`, `meta.docs.url`, `index.ts` entries.
- **Any deviation is a question, not a default**, even one carried over from
the Deno version. The user was unhappy when rule docs were quietly placed in
`src/` ("You should have asked me about this though. We should respect
ESLint's way").
- Before finishing structural work, compare against well-known projects and
list every difference for the user.
## When the user is away
- **Never block on the user.** No approval requests, no waiting. Pick the
conservative option consistent with these notes, write the choice and its
reason in the PR or report, and continue. (An idle approval request once
cost hours; the user was emphatic.)
- Log open questions where the user will look: the PR body, and the shared
questions file the coordinator names (during the migration,
`~/.local/share/deno-migration/morning-questions.md`).
- Hard limits still hold without asking: no merging, no publishing, no
archiving, no pushing to `main`, no external state changes.
## Working style the user expects from agents
- Keep the main conversation free: repo edits, commits, and long checks run
in background subagents.
- Fan independent work out to many parallel subagents (more than five when it
splits), each in its own worktree when they share a branch.
+1
View File
@@ -0,0 +1 @@
*
+69
View File
@@ -0,0 +1,69 @@
---
name: migrate-deno-repo
description: Checklist for moving a Deno repo (or a leftover Deno part of one) to Node 22+ + Vite+ with npm, same behavior, full lint parity, one PR
---
Template: `cunarist/eslint-plugin-explicit-types` on `main`. Read its
`AGENTS.md`, `vite.config.ts`, `scripts/project-lint.ts`, `package.json`, and
CI before starting, and copy their shape. Never ask the user mid-way when
they're away: decide by the memories, record the choice in the PR.
Branch `node-migration` (apps) or `initial-port` (new plugin repos); one PR
with everything.
**Inventory first**
- [ ] List every `deno.json` task, import-map entry, lint rule (tags,
includes, `@hugoalh`, custom plugins), test, `Deno.*` call, JSON/CSS/SVG
import, and every place the output is consumed (Rust, HTML,
Containerfile, CI).
**Toolchain**
- [ ] `package.json` in the required key order, `engines.node ">=22"`, no
`packageManager`, no `peerDependencies`; `npm install`; commit
`package-lock.json`.
- [ ] `tsconfig.json` from the template; `vite.config.ts` with `fmt:
{ sortPackageJson: false }` and the category-based `lint` block.
- [ ] Delete `deno.json`, `deno.lock`, and Deno-only files. `mod.ts` →
`index.ts`; `deno.json` imports → `package.json` `imports` (`#x/*`
patterns for trailing-slash mappings); `jsr:`/`npm:` → npm packages.
**Code**
- [ ] Only `.ts` files; no non-`.ts` imports: CSS via `<link>`, JSON and SVG
as `.ts` modules, icons in memona's style.
- [ ] `Deno.*` → `node:*` / `process`. Generated JS written as `.ts`, built,
gitignored.
- [ ] Decorators stay standard with `accessor`; add `scripts/decorators.ts`
if the repo has any. Lit repos: `scripts/lit-check.ts` + `npm run
lit-check`.
**Lint**
- [ ] `scripts/project-lint.ts` copied; old local Deno lint rules ported into
it; parity mapping for every former rule; `id-match`; no disable
comments; no source `overrides`; refactor size-rule hits.
**Tests**
- [ ] `Deno.test` → Vitest (codemod, then finish TODOs); unit tests next to
code, integration in `tests/`; no case dropped.
**Build, ship, CI**
- [ ] `vp build` in app mode with the same output paths, or `vp pack` for
packages. Containerfile node stage on `node:24-alpine`. CI with
`setup-vp`: `vp check`, build, `vp test` (+ lit-check). Packages:
release-triggered `publish.yml`.
- [ ] `AGENTS.md` with the damem header, `.agents/` notes from the damem kit,
README updated (Node.js 22+, no Deno instructions). `damem doctor`
clean.
**Finish**
- [ ] `vp check`, `vp test`, build all pass locally; the app runs.
- [ ] Commit in plain imperative sentences; open the PR (summary, changes,
parity mapping, deviations/risks, "CI fails until @cunarist/eslint-plugin-*
are published" if relevant, verification, footer line).
- [ ] Run `review-cycle`. Never merge, publish, or archive.
+40
View File
@@ -0,0 +1,40 @@
---
name: review-cycle
description: After opening a PR, run review rounds until fully approved; @bot on Capsulizers Gitea repos, an adversarial subagent review posted as a PR comment elsewhere; then hand to the user, never merge
---
Every PR gets reviewed before the user sees it. Repeat rounds until the
review is a full approval (cap: 6 rounds; if still not approved, stop and
report what remains). Never approve to finish early, and never merge.
## Capsulizers repos (git.capsulizers.com) with `.gitea/workflows/agents.yml`
1. Comment `@bot` on the PR through the Gitea API, as the user's own account
(credentials from the user's environment, sourced in a subshell, never
printed).
2. Wait for the review from the `bot` account (poll about every 2 minutes,
up to about 30 minutes per round).
3. For each point: fix valid ones with commits on the PR branch; reply
briefly with reasons to ones that are wrong.
4. Re-run `vp check`, the build, and `vp test`, push, and comment `@bot`
again. Done when the bot approves.
## Other repos (GitHub, or Gitea without the bot)
1. Have a separate subagent review the **full PR diff** adversarially:
correctness, behavior changes, missed Deno APIs, broken build paths, lint
parity, and violations of the repo's `.agents/memory` rules.
2. Post the review as a PR comment headed `Automated review (round N)`,
ending with exactly one verdict line: `VERDICT: APPROVED` or
`VERDICT: CHANGES REQUESTED`.
3. Address every point with commits, re-run checks, push, and reply to the
review comment summarizing the fixes.
4. Next round reviews from scratch, not just the fixes. Done when a round's
verdict is `APPROVED`.
## After approval
- Ask the user to review (or, when the user is away, list the PR and any open
questions in the report and the questions file). Don't wait idle.
- The user merges (squash). Don't merge even after approval unless the user
explicitly allowed it for this PR.
+4 -4
View File
@@ -1,7 +1,7 @@
At the start of every new session, run `damem recall` and treat its output as
the repository context. If `damem` is unavailable, read and follow
[its installation instructions](https://github.com/cunarist/damem/blob/main/INSTALL.md)
before reasoning or responding.
At the start of every new session, run `damem recall` in the terminal and treat
its output as the context for this repository. If the `damem` command is not
found, read [INSTALL.md](https://github.com/cunarist/damem/blob/main/INSTALL.md)
and follow it before reasoning or responding.
# Pull requests