This commit is contained in:
@@ -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]]).
|
||||||
@@ -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.
|
||||||
@@ -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]]).
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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]].
|
||||||
@@ -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.
|
||||||
@@ -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]]).
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
*
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
At the start of every new session, run `damem recall` and treat its output as
|
At the start of every new session, run `damem recall` in the terminal and treat
|
||||||
the repository context. If `damem` is unavailable, read and follow
|
its output as the context for this repository. If the `damem` command is not
|
||||||
[its installation instructions](https://github.com/cunarist/damem/blob/main/INSTALL.md)
|
found, read [INSTALL.md](https://github.com/cunarist/damem/blob/main/INSTALL.md)
|
||||||
before reasoning or responding.
|
and follow it before reasoning or responding.
|
||||||
|
|
||||||
# Pull requests
|
# Pull requests
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user