enigma-cli 1.35.7 → 1.35.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +12 -11
  2. package/assets/registry/primitives/registry.json +46 -0
  3. package/assets/registry/utils/registry.json +69 -0
  4. package/assets/skills/anti-overengineering-policy/skill.json +1 -1
  5. package/assets/skills/anti-overengineering-review/skill.json +1 -1
  6. package/assets/skills/backend-policy/skill.json +1 -1
  7. package/assets/skills/ciphera-style-policy/skill.json +1 -1
  8. package/assets/skills/code-review-policy/skill.json +1 -1
  9. package/assets/skills/core-engineering-policy/skill.json +1 -1
  10. package/assets/skills/database-expert/skill.json +1 -1
  11. package/assets/skills/debugging-policy/skill.json +1 -1
  12. package/assets/skills/dependency-policy/skill.json +1 -1
  13. package/assets/skills/email-policy/skill.json +1 -1
  14. package/assets/skills/frontend-design/skill.json +1 -1
  15. package/assets/skills/frontend-policy/SKILL.md +17 -9
  16. package/assets/skills/frontend-policy/skill.json +5 -5
  17. package/assets/skills/git-policy/skill.json +1 -1
  18. package/assets/skills/logo-sourcing-policy/skill.json +1 -1
  19. package/assets/skills/security-policy/skill.json +1 -1
  20. package/assets/skills/skill-creator/skill.json +1 -1
  21. package/assets/skills/task-completion-policy/skill.json +1 -1
  22. package/assets/skills/technical-writing-policy/skill.json +1 -1
  23. package/assets/skills/testing-policy/skill.json +1 -1
  24. package/assets/skills/validation-policy/SKILL.md +8 -2
  25. package/assets/skills/validation-policy/skill.json +5 -5
  26. package/bin/checksums.json +4 -4
  27. package/bin/enigma.mjs +4 -2
  28. package/bin/statusline.mjs +128 -13
  29. package/dist/guardrails.js +66 -0
  30. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Everything you need to work with a coding agent, in one command. `enigma`
4
4
  installs a shared set of engineering **policy skills** into the agents you
5
- actually use (Claude Code, OpenAI Codex, opencode) and sets up portable **git
5
+ actually use (Claude Code, OpenAI Codex, opencode, Kimi Code) and sets up portable **git
6
6
  security hooks** that block secrets, `.env` files, and dependency dirs from being
7
7
  committed.
8
8
 
@@ -47,7 +47,7 @@ the [GitHub CLI](https://cli.github.com), [Bun](https://bun.sh) and [Warp](https
47
47
  |--|--|
48
48
  | <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white" alt="Node.js"/></a> | `>= 18`, ships with `npm` - installs and runs the `enigma` CLI |
49
49
  | <a href="https://git-scm.com"><img src="https://img.shields.io/badge/Git-F05032?style=for-the-badge&logo=git&logoColor=white" alt="Git"/></a> | powers the security hooks and the commit guard |
50
- | <img src="https://img.shields.io/badge/Coding%20agent-555555?style=for-the-badge&logo=claude&logoColor=white" alt="Coding agent"/> | at least one of [Claude Code](https://claude.com/claude-code), [OpenAI Codex](https://github.com/openai/codex) or [opencode](https://opencode.ai) - the skills need a home |
50
+ | <img src="https://img.shields.io/badge/Coding%20agent-555555?style=for-the-badge&logo=claude&logoColor=white" alt="Coding agent"/> | at least one of [Claude Code](https://claude.com/claude-code), [OpenAI Codex](https://github.com/openai/codex), [opencode](https://opencode.ai) or [Kimi Code](https://www.kimi.com/code) - the skills need a home |
51
51
 
52
52
  ### Recommended
53
53
 
@@ -73,7 +73,7 @@ enigma guard [--all] Run the commit guard: staged files, --all for all tracked
73
73
  CI). --json emits one document; exits 1 on findings, 2 if it
74
74
  could not run at all
75
75
  enigma config [k v] Show or set runtime toggles (e.g. config commit-emoji off)
76
- enigma <tool> [acct] Launch claude | codex | opencode with an account
76
+ enigma <tool> [acct] Launch claude | codex | opencode | kimi with an account
77
77
  (resolution: explicit > active profile > tool active);
78
78
  auto-syncs deployed skills first
79
79
  enigma account ... Manage per-tool accounts (list/add/use/login/remove)
@@ -128,7 +128,7 @@ push,pr,ci` and `enigma gate init` work in an image without it.
128
128
 
129
129
  Skills are authored once and deployed to every selected agent (no per-agent
130
130
  duplication). `enigma install` auto-detects which agents are installed (CLI on
131
- `PATH` or a config dir like `~/.claude`, `~/.codex`, `~/.config/opencode`) and
131
+ `PATH` or a config dir like `~/.claude`, `~/.codex`, `~/.config/opencode`, `~/.kimi-code`) and
132
132
  preselects them; `--all` targets every supported agent.
133
133
 
134
134
  | Agent | Scope | Skills | Memory file |
@@ -309,9 +309,10 @@ your **company** Claude Code account and your **personal** one - keeping each
309
309
  fully isolated and switching between them without ever logging out. Each account
310
310
  has its own credentials, session, and history, so client work never mixes with
311
311
  personal projects. Supported tools: **Claude Code** (`CLAUDE_CONFIG_DIR`),
312
- **OpenAI Codex** (`CODEX_HOME`) and **OpenCode** (a private `XDG_DATA_HOME` /
312
+ **OpenAI Codex** (`CODEX_HOME`), **OpenCode** (a private `XDG_DATA_HOME` /
313
313
  `XDG_CONFIG_HOME` pair per managed account; its default account keeps your real
314
- environment untouched).
314
+ environment untouched) and **Kimi Code** (`KIMI_CODE_HOME`, which relocates its
315
+ whole data root - credentials, sessions, skills and `AGENTS.md`).
315
316
 
316
317
  > This is for legitimate, professional account separation (one account per
317
318
  > employer/context, as many organizations require). It is **not** a way to evade
@@ -338,9 +339,9 @@ enigma account rename work corp # rename an account (its config dir moves)
338
339
  enigma account remove work # delete an account and its config dir
339
340
  ```
340
341
 
341
- Your existing `~/.claude` / `~/.codex` / opencode setup is always available as
342
+ Your existing `~/.claude` / `~/.codex` / `~/.kimi-code` / opencode setup is always available as
342
343
  each tool's built-in `default` account (never deleted). New accounts live under
343
- `~/.enigma/<tool>/<name>/`. Bare `claude` / `codex` / `opencode` commands keep
344
+ `~/.enigma/<tool>/<name>/`. Bare `claude` / `codex` / `opencode` / `kimi` commands keep
344
345
  using your real environment as before.
345
346
 
346
347
  Managed accounts inherit your enigma setup automatically: because the tool reads
@@ -372,7 +373,7 @@ enigma profile use none # deactivate
372
373
 
373
374
  From the hub TUI (`enigma`), the **Accounts** panel lists every tool's accounts -
374
375
  with the signed-in identity (email for Claude/Codex, connected providers for
375
- OpenCode) - and lets you **add** (`a`), set active (`enter`), **connect**/log in
376
+ OpenCode and Kimi Code) - and lets you **add** (`a`), set active (`enter`), **connect**/log in
376
377
  (`c`), rename (`r`), or remove (`d`). Adding first asks **which tool** with a
377
378
  searchable selector (type to filter, like opencode's model picker), then the
378
379
  account name, then offers to connect right away. The **Profiles** panel manages
@@ -647,7 +648,7 @@ session transcripts (`~/.claude/projects/.../*.jsonl`) and reports:
647
648
  - **Breakdowns** by model, by project, **by account** (every Claude login - default and
648
649
  managed accounts, not just `~/.claude`), and a **recent sessions** table.
649
650
  - A **provider coverage** line: only Claude Code keeps a readable local usage store, so
650
- Codex and OpenCode are shown as unavailable (no local token store) rather than faked. The
651
+ Codex, OpenCode and Kimi Code are shown as unavailable (no local token store) rather than faked. The
651
652
  session/weekly windows are Claude-specific (they come from Anthropic's rate-limit headers).
652
653
  - A **current 5-hour block** computed locally from transcript timestamps: tokens + cost
653
654
  used in the open window, the **burn rate** (tokens/min) and a projected end-of-window
@@ -672,7 +673,7 @@ Honesty note: cost is an **estimate** (Anthropic does not record per-message cos
672
673
  transcript; real spend is billed by Anthropic). Token counts and prompt-cache reads are
673
674
  measured facts. enigma deliberately does **not** attribute savings to skills or to
674
675
  token-efficient output - a transcript has no counterfactual baseline, so any such figure
675
- would be invented. Only Claude Code is read today; Codex/OpenCode use absent or
676
+ would be invented. Only Claude Code is read today; Codex/OpenCode/Kimi Code use absent or
676
677
  undocumented local session stores and are not guessed.
677
678
 
678
679
  #### Manage accounts & profiles from the dashboard
@@ -0,0 +1,46 @@
1
+ {
2
+ "registry": "@enigmax/primitives",
3
+ "version": 1,
4
+ "kind": "primitive",
5
+ "items": [
6
+ {
7
+ "name": "marquee",
8
+ "title": "Draggable infinite marquee",
9
+ "description": "Looping row that takes a speed in px/s, measures its own lap, and can be grabbed and thrown without swallowing the clicks on its items.",
10
+ "targets": ["vanilla", "astro", "react"],
11
+ "entry": {
12
+ "vanilla": "@enigmax/primitives",
13
+ "astro": "@enigmax/primitives",
14
+ "react": "@enigmax/primitives/react"
15
+ },
16
+ "exports": {
17
+ "vanilla": ["createMarquee"],
18
+ "astro": ["createMarquee"],
19
+ "react": ["useMarquee"]
20
+ },
21
+ "files": [
22
+ {
23
+ "path": "src/core/marquee.ts",
24
+ "dest": "marquee.ts",
25
+ "targets": ["vanilla", "astro", "react"]
26
+ },
27
+ {
28
+ "path": "src/react/use-marquee.ts",
29
+ "dest": "use-marquee.ts",
30
+ "targets": ["react"],
31
+ "rewrite": { "@/core/marquee": "./marquee" }
32
+ }
33
+ ],
34
+ "styles": false,
35
+ "themeHooks": [
36
+ "[data-enigma-marquee]",
37
+ "[data-enigma-marquee][data-dragging]",
38
+ "[data-enigma-marquee][data-hovering]",
39
+ "[data-enigma-marquee][data-reduced-motion=true]",
40
+ "[data-enigma-marquee-track]",
41
+ "[data-enigma-marquee-copy]"
42
+ ],
43
+ "docs": "docs/notes/primitives.md#marquee"
44
+ }
45
+ ]
46
+ }
@@ -0,0 +1,69 @@
1
+ {
2
+ "registry": "@enigmax/utils",
3
+ "version": 1,
4
+ "kind": "utility",
5
+ "items": [
6
+ {
7
+ "name": "cache",
8
+ "title": "Short-TTL read cache",
9
+ "description": "Read-through cache with in-flight deduplication, prefix invalidation and optional local/session persistence.",
10
+ "targets": ["vanilla", "astro", "react"],
11
+ "entry": {
12
+ "vanilla": "@enigmax/utils",
13
+ "astro": "@enigmax/utils",
14
+ "react": "@enigmax/utils/react"
15
+ },
16
+ "exports": {
17
+ "vanilla": ["createCache"],
18
+ "astro": ["createCache"],
19
+ "react": ["useCached"]
20
+ },
21
+ "files": [
22
+ {
23
+ "path": "src/core/cache.ts",
24
+ "dest": "cache.ts",
25
+ "targets": ["vanilla", "astro", "react"]
26
+ },
27
+ {
28
+ "path": "src/react/use-cached.ts",
29
+ "dest": "use-cached.ts",
30
+ "targets": ["react"],
31
+ "rewrite": { "@/core/cache": "./cache" }
32
+ }
33
+ ],
34
+ "styles": false,
35
+ "docs": "docs/notes/primitives.md#cache"
36
+ },
37
+ {
38
+ "name": "notifications",
39
+ "title": "Notification queue",
40
+ "description": "Ordering, dedupe by key, sticky errors and dismiss timers that hold instead of running while the tab is hidden. No rendering.",
41
+ "targets": ["vanilla", "astro", "react"],
42
+ "entry": {
43
+ "vanilla": "@enigmax/utils",
44
+ "astro": "@enigmax/utils",
45
+ "react": "@enigmax/utils/react"
46
+ },
47
+ "exports": {
48
+ "vanilla": ["createNotifications"],
49
+ "astro": ["createNotifications"],
50
+ "react": ["useNotifications"]
51
+ },
52
+ "files": [
53
+ {
54
+ "path": "src/core/notifications.ts",
55
+ "dest": "notifications.ts",
56
+ "targets": ["vanilla", "astro", "react"]
57
+ },
58
+ {
59
+ "path": "src/react/use-notifications.ts",
60
+ "dest": "use-notifications.ts",
61
+ "targets": ["react"],
62
+ "rewrite": { "@/core/notifications": "./notifications" }
63
+ }
64
+ ],
65
+ "styles": false,
66
+ "docs": "docs/notes/primitives.md#notifications"
67
+ }
68
+ ]
69
+ }
@@ -7,6 +7,6 @@
7
7
  "minimalCode"
8
8
  ],
9
9
  "updated": "2026-07-17T00:17:02+02:00",
10
- "cliVersion": "1.35.7",
10
+ "cliVersion": "1.35.10",
11
11
  "sha": "3f0dcc28341bb0407860534f7ce9314cfc91b5e673f8f3d13b89d61851ed75f6"
12
12
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "On-demand over-engineering review - diff review, whole-repo audit, and enigma: debt-marker ledger (tags delete/stdlib/native/yagni/shrink, line/dep scoring); lists cuts, applies nothing.",
6
6
  "updated": "2026-06-16T11:24:30+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "f742a2be3f328b9ea1ff9a35a449177c2cbec35ad16e46f7054b7a873a2ab017"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Backend/API architecture: controller-service-repository layering, modern TypeScript project configuration (module resolution, strict flags, @/* path alias), API and request optimization (batching, avoiding redundant calls, skipping no-op writes), server-side caching (Redis), and Zod boundary validation.",
6
6
  "updated": "2026-08-02T19:59:01+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "652637f818b4ce4f4d5d83fa68fa57fe7842e32fb179523958c5496af54bb938"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Ciphera code style conventions (formatting, naming, imports incl. namespace imports for wide module surfaces, path-alias specifiers and no file extensions, comments, code-level anti-patterns; TypeScript-first, language-agnostic).",
6
6
  "updated": "2026-08-02T19:59:01+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "3beb0383a9cb0331ef8a579308a2517d24d57a8ba33751ad17d16d0a4f66e344"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Pre-delivery self-review gate, prioritized review dimensions, and change-quality criteria.",
6
6
  "updated": "2026-08-02T04:15:47+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "b35f0bb4a29f2346d9a1b00a4bfee9557a9b6a44116d9e6e59abcd945466dbcb"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Core engineering execution policy and harness orchestration (highest-authority rules), including the generalization rule that treats a named example as a class to sweep and fix.",
6
6
  "updated": "2026-08-02T04:28:51+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "95091678d4e72e3a9b1ef3ad0514e503a2a631f0d134bf772b4a1b3c66881995"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Senior database architecture policy: engine selection (PostgreSQL by default, SQLite only for local-first/embedded stores), ORM selection (Prisma in TypeScript/JavaScript), query optimization, anti-duplication/normalization, scalability, and RGPD/GDPR encryption.",
6
6
  "updated": "2026-08-04T18:33:27+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "26fddc14caf697e830448a2cc453ddc470fe83d098d50bd67b278effba63274e"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Reproduce-isolate-fix debugging methodology with root-cause discipline and regression verification.",
6
6
  "updated": "2026-08-02T04:15:47+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "968876bcac9ce6e5a20be05c89ac97191b112c9e08679a8dc506d150c375fec4"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Dependency and supply-chain security: lockfiles and reproducible installs, version pinning, vulnerability auditing, vetting/minimizing packages, vendoring, and SBOM/provenance.",
6
6
  "updated": "2026-06-01T00:45:28+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "6375d835c2aef2c9bd31ce116444dc3d796f510f9970a213aa3ac4696d7e21b9"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Transactional email: React Email templates instead of hand-written HTML tables, server-side rendering, one send module behind the provider SDK, plain-text alternatives, idempotent background sending, link safety, and deliverability (SPF/DKIM/DMARC, bounce suppression, unsubscribe).",
6
6
  "updated": "2026-07-30T19:29:19+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "c9724fdbcdbeab99573be3fd44d4cdd97c2a394d99f3c4395f118f17356b00ed"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one.",
6
6
  "updated": "2026-07-29T01:18:36+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "9e30ee7d8a1a1e8c6e7f4e043857cd01841c68a427752e45bc0cad9ec5cfa279"
9
9
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: frontend-policy
3
- description: Frontend architecture - reusable components, abstraction thresholds, state management, no-op detection (skip any operation whose result equals the current state - form saves, toggles, filters, reorders - not just saves; dirty means the values DIFFER from the loaded snapshot, not that the user touched the field, so a value edited and put back leaves Save disabled), client-side caching (localStorage/sessionStorage to avoid redundant server calls and survive rate limits), instant first paint (render the shell immediately, load data async via the API, show skeletons - never block render on data), perceived performance and responsiveness (instant interaction feedback, prefetch on intent, debounce/throttle, cancel stale requests, avoid request waterfalls, lazy-load heavy widgets), large-list rendering (virtualized infinite scroll as the preferred default with pagination as the deliberate exception when the design or the user calls for it, skeletons, progressive/parallel loading, short-TTL caching), optimistic UI with rollback, visual restraint (never a card inside a card, borders only where they carry information, spacing and background tone before chrome), icon actions (repeated row/card actions like copy, edit, rename, remove, download, refresh are icon-only buttons carrying aria-label plus title, never a text label), navigation that is iconified and grouped into labelled sections once it outgrows a flat list, a Cmd/Ctrl+K command palette with fuse.js fuzzy search over the loaded data once the app has enough destinations and records to hunt through, data views that ship their own affordances by default (a log, expense, transaction or history table is not done when the rows render - it needs the search, the filters its column kinds imply, a date range, sort, filter state kept in the URL, and an export of the filtered set), every reference to an entity being a way into it (a name, id, project or path in a row links to that record, reveals it in a hover card, or at minimum copies and filters by it - never inert text, with machine codes given human labels and raw payloads never dumped into a cell), responsive/adaptive layout (fluid units, breakpoints, no overlap or horizontal overflow, viewport meta, touch targets, and items with a fixed intrinsic size - icons, avatars, badges - pinned with flex-shrink so long text squashes the text and never the glyph), form fields that declare their keyboard and casing (autocapitalize/autocomplete/inputmode/spellcheck per field kind, set once in the shared Input, normalized on blur rather than on every keystroke, with an inline error on every field that has a rule), auth screens (breached-password feedback, strength meter, cookie consent answered before login/register, and a fixed-length 2FA or emailed code that verifies itself when the last digit lands, once per distinct value and never re-firing into the attempt cap), AI chat/assistant/agent interfaces (use Vercel's AI Elements registry for message threads, streaming, reasoning and tool-call panels, prompt inputs - never hand-roll chat UI in React), and periodic React code-health audits (react-doctor). Use when building or changing UI components, client state, forms/save flows, data fetching/caching, lists that show lots of data, a log/activity/expenses/transactions/history table, loading states, dashboards/panels, layout/responsiveness, making the UI feel fast, building a chat/AI/agent/LLM interface, or any frontend structure.
3
+ description: Frontend architecture - reusable components, abstraction thresholds, state management, real-time form validation where an emptied or not-yet-filled field is incomplete rather than invalid (a `*` on the label and an aria-disabled submit, never "email is not valid" over an empty input), no-op detection (skip any operation whose result equals the current state - form saves, toggles, filters, reorders - not just saves; dirty means the values DIFFER from the loaded snapshot, not that the user touched the field, so a value edited and put back leaves Save disabled), client-side caching (localStorage/sessionStorage to avoid redundant server calls and survive rate limits), instant first paint (render the shell immediately, load data async via the API, show skeletons - never block render on data), perceived performance and responsiveness (instant interaction feedback, prefetch on intent, debounce/throttle, cancel stale requests, avoid request waterfalls, lazy-load heavy widgets), large-list rendering (virtualized infinite scroll as the preferred default with pagination as the deliberate exception when the design or the user calls for it, skeletons, progressive/parallel loading, short-TTL caching), optimistic UI with rollback, visual restraint (never a card inside a card, borders only where they carry information, spacing and background tone before chrome), icon actions (repeated row/card actions like copy, edit, rename, remove, download, refresh are icon-only buttons carrying aria-label plus title, never a text label), navigation that is iconified and grouped into labelled sections once it outgrows a flat list, a Cmd/Ctrl+K command palette with fuse.js fuzzy search over the loaded data once the app has enough destinations and records to hunt through, data views that ship their own affordances by default (a log, expense, transaction or history table is not done when the rows render - it needs the search, the filters its column kinds imply, a date range, sort, filter state kept in the URL, and an export of the filtered set), every reference to an entity being a way into it (a name, id, project or path in a row links to that record, reveals it in a hover card, or at minimum copies and filters by it - never inert text, with machine codes given human labels and raw payloads never dumped into a cell), responsive/adaptive layout (fluid units, breakpoints, no overlap or horizontal overflow, viewport meta, touch targets, and items with a fixed intrinsic size - icons, avatars, badges - pinned with flex-shrink so long text squashes the text and never the glyph), form fields that declare their keyboard and casing (autocapitalize/autocomplete/inputmode/spellcheck per field kind, set once in the shared Input, normalized on blur rather than on every keystroke, with an inline error on every field that has a rule), auth screens (breached-password feedback, strength meter, cookie consent answered before login/register, and a fixed-length 2FA or emailed code that verifies itself when the last digit lands, once per distinct value and never re-firing into the attempt cap), AI chat/assistant/agent interfaces (use Vercel's AI Elements registry for message threads, streaming, reasoning and tool-call panels, prompt inputs - never hand-roll chat UI in React), and periodic React code-health audits (react-doctor). Use when building or changing UI components, client state, forms/save flows, data fetching/caching, lists that show lots of data, a log/activity/expenses/transactions/history table, loading states, dashboards/panels, layout/responsiveness, making the UI feel fast, building a chat/AI/agent/LLM interface, or any frontend structure.
4
4
  ---
5
5
 
6
6
  # Frontend Architecture Policy
@@ -21,6 +21,13 @@ description: Frontend architecture - reusable components, abstraction thresholds
21
21
 
22
22
  ---
23
23
 
24
+ ## Headless Primitives Before Hand-Rolled Interaction
25
+
26
+ - Interaction logic - looping/draggable rows, momentum, focus traps, virtualized lists, dismiss timers, read caches - is BEHAVIOR, and behavior that has already been measured and tested is a dependency, not a snippet to rewrite. Check the catalogue before writing it: `enigma add` lists it, `enigma add <name>` adds it as a dependency, `enigma add <name> --copy` vendors the source in when it must be edited.
27
+ - These primitives ship NO visual styles. They apply only what the behavior requires (overflow, touch-action, user-select, will-change, transform) and publish their state as `data-*` attributes, so the look stays entirely yours.
28
+ - Do not hand-roll one of them "just for this page". The hand-rolled version is where the measured bugs come back: a duration-driven marquee that accelerates as items are added, a `setPointerCapture` drag that silently stops every link in the row from opening, a hover slowdown that sticks forever after the first tap because touch never fires `pointerleave`.
29
+ - If the catalogue has no primitive for what you need, write it as one (core + adapter + registry entry + test) rather than inline in a page, so the next screen reuses it instead of rewriting it.
30
+
24
31
  ## Component Reuse (Mandatory)
25
32
 
26
33
  - ALWAYS reuse a single base component and drive its behavior with props; NEVER create separate components for variants of the same element.
@@ -234,13 +241,13 @@ The general rule: before running any operation, check whether its result would e
234
241
  - **Dirty means different, not touched.** Recompute the comparison on every change. Never raise a `dirty` flag from an onChange/onInput handler: a flag set by interaction stays raised after the user types a character and deletes it again, which is the exact case this rule exists for.
235
242
  - **Valid is not dirty.** A Save gated only on validity ("the name is not empty", "there are no errors") still fires a no-op on a form nobody edited. Both conditions have to hold: the values differ AND they are valid.
236
243
  - **Normalize before comparing**, using the same rules the field itself applies: trim surrounding whitespace, and case-fold only where the value is case-insensitive (a username usually is, a display name is not). Leading whitespace is not an edit; a different letter case may be.
237
- - **A disabled Save must say why.** Keep it visible and disabled rather than hidden, and put the reason within reach (a `title` or one line of helper text, "No changes to save"), so the form does not read as broken. Never leave it looking enabled while it silently does nothing.
238
- - On load the form is not dirty, so Save starts disabled; after a successful save the snapshot becomes the saved values and Save returns to disabled.
244
+ - **A blocked Save must say why.** Keep it visible and blocked rather than hidden, and put the reason within reach (a `title` or one line of helper text, "No changes to save"), so the form does not read as broken. Block it with `aria-disabled="true"` rather than the `disabled` attribute - a `disabled` button is out of the tab order, so the very `title` carrying the reason is unreachable by keyboard - and short-circuit in the handler. Never leave it looking enabled while it silently does nothing.
245
+ - On load the form is not dirty, so Save starts blocked; after a successful save the snapshot becomes the saved values and Save returns to blocked.
239
246
  - **Autosave, blur-save and inline edits obey the same check.** On blur or after the debounce, compare against the snapshot and send nothing when the value came back to where it started.
240
247
  - When the form is not dirty, Save must never hit the network. Two acceptable UX options:
241
- - Preferred: disable / neutralize the Save button while the edited values equal the snapshot, so there is nothing to submit until a real change exists.
248
+ - Preferred: neutralize the Save button (`aria-disabled`, per the rule above) while the edited values equal the snapshot, so there is nothing to submit until a real change exists.
242
249
  - Or keep Save enabled but short-circuit on click: show the normal "saved" confirmation instantly and send NO request. Never open a spinner or fire a call for a no-op.
243
- - Example: the user opens their account settings and presses Save without changing the name. The name still equals the loaded value, so the form is not dirty - the button is disabled, or the click just confirms success without a request.
250
+ - Example: the user opens their account settings and presses Save without changing the name. The name still equals the loaded value, so the form is not dirty - the button is blocked, or the click just confirms success without a request.
244
251
 
245
252
  ### Enforce it server-side too
246
253
 
@@ -287,6 +294,7 @@ Where this lives and when it runs:
287
294
  - Normalize on BLUR and before save, never on every keystroke: rewriting the value under the caret moves the cursor and breaks IME composition mid-word. While the field has focus, show what the user typed; on blur, show what will be stored.
288
295
  - Validate the normalized value, so a trailing space the user has not finished typing never renders as an error.
289
296
  - Show the error inline as the user types, from the first blur onward, on EVERY field that has a rule, and keep Save blocked while any field is invalid. Validating the email and leaving the link, the phone and the handle next to it silent is the usual half-built form.
297
+ - An EMPTY field is incomplete, not invalid: clearing an input drops its error instead of turning it into "Email is not valid". Requiredness is shown structurally - a `*` on the label, a legend for the marker, and a submit that refuses to send until the required fields hold a value (`aria-disabled`, not the `disabled` attribute, so it keeps its place in the tab order and can announce what is missing; the type-to-confirm step of an irreversible destroy is the one exception and stays on native `disabled`, per the destructive-action tiers below) - and the format error appears only once there is a value to be wrong. The states, the transitions and the schema shape behind them are validation-policy's "Empty is not invalid".
290
298
 
291
299
  ---
292
300
 
@@ -309,20 +317,20 @@ Auth is the first screen a user meets and the one most often shipped half-built.
309
317
 
310
318
  - Every sign-in form with a password field carries a visible "Forgot your password?" link next to that field, leading to a real reset flow. Building the login screen without it is shipping a dead end.
311
319
  - The reset request screen confirms in the same words whether or not the address is registered ("If that address has an account, we have sent a link"). Never render "no account with that email" - the screen would be an account-existence oracle.
312
- - The new-password screen validates in real time against the same schema the server uses, uses the shared Input (which brings the show/hide toggle), and compares the confirmation field as the user types. Keep Submit disabled until both are valid, with the reason visible.
320
+ - The new-password screen validates in real time against the same schema the server uses, uses the shared Input (which brings the show/hide toggle), and compares the confirmation field as the user types. Keep Submit blocked with `aria-disabled` until both are valid, with the reason visible.
313
321
  - Wherever a password is created (sign-up, reset, change), check it against Have I Been Pwned's Pwned Passwords range API as the user types - debounced, request aborted when the value changes - and refuse a breached one with "This password appeared in a data breach. Choose a different one." Free, no key, and the password never leaves the browser: only the first 5 characters of its SHA-1 do (mechanics and the server-side half in security-policy).
314
322
  - The same screens refuse a password built out of the identity it protects: the email, its local part, the username, the display name, the site name. Compare normalized values (lowercased, accent-stripped, punctuation dropped) so `F.J.R.G_2007` and `fjrg2007` are one string, and check containment and near-matches, not just equality (the rule and the server half are security-policy's). The form already holds the email and the name the user just typed, so run it on every keystroke and name the reason inline: "Your password cannot contain your email address or username."
315
323
  - Next to it, a strength meter: a row of segments that fills and shifts colour (red, amber, green) with a one-line verdict. Drive it from an entropy estimator (`@zxcvbn-ts/core`), fed the user's own email and name as context, not from a regex counting character classes - "Passw0rd!" satisfies every class rule and is guessed instantly. The meter is advisory; the length floor and the breach check are the gate.
316
324
  - The consent banner is answered before either form submits (security-policy owns the cookie rules). Blocking the account on a "reject" is not the ask: record the choice, then continue.
317
325
  - After sign-up the user lands inside the app, already signed in. If the account still needs email verification, say so in the app with a way to resend, and block only the actions that need it.
318
- - Surface throttling honestly. On a `429`, show how long the wait is (from `Retry-After`), keep the button disabled with a countdown, and never swallow the response into a generic "something went wrong".
326
+ - Surface throttling honestly. On a `429`, show how long the wait is (from `Retry-After`), keep the button `aria-disabled` with a countdown - not the `disabled` attribute, which drops the button out of the tab order and takes the countdown next to it out of reach of the keyboard and screen-reader user who has to wait, and who then needs that same control to retry - and let the handler refuse the call until the wait elapses. Never swallow the response into a generic "something went wrong".
319
327
  - A one-time-code field is one input with `autocomplete="one-time-code"`, `inputmode="numeric"`, paste of the whole code, and no clearing of what the user typed on a wrong attempt. Say how many attempts are left only if the server chose to reveal it.
320
328
  - **When the code has a known fixed length, the form submits itself the moment the last character lands.** A 6-digit 2FA or emailed code is complete the instant the sixth digit arrives - typed, pasted, or filled by the OS from an SMS or the authenticator - and asking for a click after that is a step the UI can take on the user's behalf. Take the length from the ONE constant the generator uses, not from a `6` hardcoded in the component, so a change to 8 does not silently break the trigger.
321
329
  - Auto-submit needs three guards, and without them it burns the user's attempt budget:
322
330
  - **Once per distinct complete value.** Remember the value already sent and submit only when the current one is complete AND different. A re-render, a blur, a paste that lands as two events, or an autofill that rewrites the field must not each fire a request.
323
331
  - **Do not re-fire after a failure until the user edits the code.** A wrong code that resubmits on every keystroke can exhaust a five-attempt cap before the user finishes correcting it. Mark the field invalid, keep what they typed, and wait for a change.
324
332
  - **Never auto-retry a `429` or a network error.** Show the wait from `Retry-After` with a countdown, and let the user trigger the next attempt (server-side limits are security-policy's).
325
- - Keep the submit button, disabled while the code is incomplete. It is the affordance for anyone who does not see the field complete itself, the retry control after a failure, and the fallback when autofill misbehaves. Announce the transition in an `aria-live="polite"` region ("Verifying code...", then the result), because a form that submits with no click gives a screen-reader user nothing to go on.
333
+ - Keep the submit button, `aria-disabled` while the code is incomplete - not the `disabled` attribute, which would drop the one control that can say what is missing out of the tab order. It is the affordance for anyone who does not see the field complete itself, the retry control after a failure, and the fallback when autofill misbehaves. Announce the transition in an `aria-live="polite"` region ("Verifying code...", then the result), because a form that submits with no click gives a screen-reader user nothing to go on.
326
334
  - Do not auto-submit when the length is not fixed: a backup or recovery code of variable length, or one the user may paste with separators, has no reliable "complete" moment - normalize the value (strip spaces and dashes) and let the button be the trigger.
327
335
  - Never keep a password, token, or code in `localStorage`, a query string, or an analytics payload. A reset token in the URL stays out of logs and out of any third-party script on the page.
328
336
 
@@ -487,7 +495,7 @@ Guard every destructive action behind friction proportional to how bad an accide
487
495
 
488
496
  - Reversible / low-stakes destroy (remove a row, delete a draft, clear a field): prefer acting immediately with an Undo affordance over a confirmation prompt - soft-delete and show a "Deleted - Undo" toast (optimistic, with rollback per Optimistic UI). The common case gets zero friction and a mistake is one click to recover. Reach for undo before a confirm whenever the delete can actually be reversed.
489
497
  - Standard destructive action (delete an item, remove a member, revoke a key): show a confirmation dialog that NAMES the exact thing being deleted ("Delete project 'Acme'?"), states the consequence, and uses a destructive-styled confirm button. It must be a real dialog/modal component, never the native `confirm()` (see the No-Op / native-dialog rules and the `fe-no-native-dialog` guardrail); default focus to Cancel and let Escape dismiss.
490
- - Critical / irreversible action (delete a repository, organization, account, or workspace; drop a database; transfer ownership; wipe production data): require a type-to-confirm step. The user must type the exact resource identifier (the repo/org name, or an explicit phrase like "delete my account") into an input, and the confirm button stays disabled until the typed value matches EXACTLY (trimmed, case-sensitive comparison against the real name). This is the GitHub / Vercel / Stripe "danger zone" pattern: it forces the user to read what they are about to destroy and makes an accidental click impossible.
498
+ - Critical / irreversible action (delete a repository, organization, account, or workspace; drop a database; transfer ownership; wipe production data): require a type-to-confirm step. The user must type the exact resource identifier (the repo/org name, or an explicit phrase like "delete my account") into an input, and the confirm button stays blocked until the typed value matches EXACTLY (trimmed, case-sensitive comparison against the real name). This tier is the one place in this policy that blocks with the native `disabled` attribute instead of `aria-disabled`, and it keeps the handler guard as well - both, not either. The a11y cost that motivates `aria-disabled` elsewhere is not paid here: the reason the button is blocked is already carried by the confirm input's own label and helper text ("Type `acme-prod` to confirm"), not by a `title` on the button, so nothing becomes unreachable by taking it out of the tab order. Against an unrecoverable destroy, a browser-level block that holds whatever the JS state is worth more than a handler guard that a race, a re-render, or a mis-wired condition can defeat. This is the GitHub / Vercel / Stripe "danger zone" pattern: it forces the user to read what they are about to destroy and makes an accidental click impossible.
491
499
 
492
500
  Scale the friction to the blast radius: do NOT make a trivial single-item delete demand typing a name (that is needless friction, anti-overengineering-policy), and NEVER leave an account-, repo-, or data-destroying action behind a single unconfirmed click. A named confirmation dialog is the floor; type-to-confirm is the ceiling for the truly unrecoverable. When in doubt about which tier applies, ask how recoverable the action is: recoverable -> undo, destructive-but-scoped -> confirm dialog, irreversible/high-blast-radius -> type-to-confirm.
493
501
 
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "frontend-policy",
3
- "version": "1.29.0",
3
+ "version": "1.30.1",
4
4
  "provider": "FJRG2007/enigma",
5
- "description": "Frontend architecture: reusable components, abstraction thresholds, state management, no-op detection (skip any operation whose result equals current state, not just form saves; dirty means different from the loaded snapshot, not touched), instant first paint (render the shell, load data async, skeletons), perceived performance (prefetch on intent, debounce/throttle, cancel stale requests, avoid waterfalls, lazy widgets), large-list rendering (infinite scroll is the preferred default, pagination the deliberate exception when the design or the user calls for it, virtualization, skeletons, progressive loading), data views that ship their own affordances by default (a log, expense, transaction or history table is not done when the rows render: fuse.js search, the filters its column kinds imply, a date range, sort, filter state in the URL, and an export of the filtered set), every reference to an entity being a way into it (a name, id, project or path in a row links to that record, reveals it in a hover card, or at minimum copies and filters by it, never inert text; machine codes get human labels and raw payloads are never dumped into a cell), optimistic UI with rollback, visual restraint (one card level, spacing before borders, one elevation scale), icon actions (repeated row/card actions are icon-only buttons with aria-label plus title, not text labels), responsive/adaptive layout (fluid units, breakpoints, no overlap/overflow, viewport meta, touch targets), form fields that declare their keyboard and casing (autocapitalize/autocomplete/inputmode per field kind, set once in the shared Input, normalized on blur), variable-length text (min-width:0 in flex/grid, wrap vs truncate, long unbroken strings, worst-case content checks), auth screens (breached-password feedback, strength meter, cookie consent before login/register), and AI chat/agent interfaces via Vercel's AI Elements registry instead of hand-rolled message threads.",
6
- "updated": "2026-08-07T17:32:14+02:00",
7
- "cliVersion": "1.35.7",
8
- "sha": "a83b0abf51b618d00d3bd4b66d245585fe27b01e36633533db825923ef9655b7"
5
+ "description": "Frontend architecture: reusable components, abstraction thresholds, state management, real-time form validation where an emptied or not-yet-filled field is incomplete rather than invalid (a `*` on the label and an aria-disabled submit, never \"email is not valid\" over an empty input), no-op detection (skip any operation whose result equals current state, not just form saves; dirty means different from the loaded snapshot, not touched), instant first paint (render the shell, load data async, skeletons), perceived performance (prefetch on intent, debounce/throttle, cancel stale requests, avoid waterfalls, lazy widgets), large-list rendering (infinite scroll is the preferred default, pagination the deliberate exception when the design or the user calls for it, virtualization, skeletons, progressive loading), data views that ship their own affordances by default (a log, expense, transaction or history table is not done when the rows render: fuse.js search, the filters its column kinds imply, a date range, sort, filter state in the URL, and an export of the filtered set), every reference to an entity being a way into it (a name, id, project or path in a row links to that record, reveals it in a hover card, or at minimum copies and filters by it, never inert text; machine codes get human labels and raw payloads are never dumped into a cell), optimistic UI with rollback, visual restraint (one card level, spacing before borders, one elevation scale), icon actions (repeated row/card actions are icon-only buttons with aria-label plus title, not text labels), responsive/adaptive layout (fluid units, breakpoints, no overlap/overflow, viewport meta, touch targets), form fields that declare their keyboard and casing (autocapitalize/autocomplete/inputmode per field kind, set once in the shared Input, normalized on blur), variable-length text (min-width:0 in flex/grid, wrap vs truncate, long unbroken strings, worst-case content checks), auth screens (breached-password feedback, strength meter, cookie consent before login/register), and AI chat/agent interfaces via Vercel's AI Elements registry instead of hand-rolled message threads.",
6
+ "updated": "2026-08-13T18:04:26+02:00",
7
+ "cliVersion": "1.35.10",
8
+ "sha": "837f62ef43fa4ab46e7e74bb88dc6cb83e877f039565a80696e05345e28eb20d"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Git & contribution policy (senior engineering standards).",
6
6
  "updated": "2026-08-04T20:04:42+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "f55f4ed5474d5c6e7fd0804d88ac38fb3c9c3cd863519645d1415c02f4416db7"
9
9
  }
@@ -7,6 +7,6 @@
7
7
  "logoColorPolicy"
8
8
  ],
9
9
  "updated": "2026-07-17T00:17:02+02:00",
10
- "cliVersion": "1.35.7",
10
+ "cliVersion": "1.35.10",
11
11
  "sha": "09cdbefd98625b02a7d03685e5deed128238ff8454a83fe22279610fe3ef8ddf"
12
12
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Application and AI-agent security: secrets, authn/authz (least privilege), credential flows with breach-checked passwords (Have I Been Pwned), OWASP Top 10, transport/crypto baseline, cookies and consent, secure logging, and agent/MCP/tool-use safety.",
6
6
  "updated": "2026-08-02T19:59:01+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "3201b6d41437626eb7bf45648c0d7f8c419dc4de87fbd9d4ca83c5ae3b6b6edf"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Create new skills, modify and improve existing skills, and measure skill performance with evals and benchmarks.",
6
6
  "updated": "2026-08-03T00:16:54+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "a49f6166f57d734e0f13487332da37903e925f32257447856af0fbd6a5f1dc84"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Exhaustive completion discipline for long/multi-item tasks - inventory, coverage ledger, verified done.",
6
6
  "updated": "2026-08-02T04:15:47+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "4c2197954135dd3375f32d839238b82a95c0e93e976ddf5a9cbc4a0cf0146e97"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Concise, realistic technical copy - UI microcopy, descriptions, hints, empty/error states, and README/doc prose that informs without over-explaining or restating the obvious, and never uses a typographic dash.",
6
6
  "updated": "2026-07-28T20:30:23+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "a4b792103eb1f9dad93b9d70ea79dc18fe9cbbc318facf5adb47ae5907d842f9"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Test strategy, coverage gates, deterministic tests, mocking discipline, regression-first bug fixing, and test-suite organization (layout by type/domain, mirrored paths, file naming, fixture/helper placement).",
6
6
  "updated": "2026-06-16T17:11:49+02:00",
7
- "cliVersion": "1.35.7",
7
+ "cliVersion": "1.35.10",
8
8
  "sha": "3bdf591057b760f674fb2b1425f63acb426cda2c4f042e1a74c5a5d3807df664"
9
9
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: validation-policy
3
- description: Strict frontend + backend schema validation (Zod or equivalent), normalization before validation (one shared normalizer on both sides - trim, lowercase the email, capitalize each word of a name, canonicalize a profile link or handle) and the rule that a check which cannot fail is not validation, cross-field rules declared on the object schema rather than the field (password against the email or username, confirmation fields, date ranges), schema consistency between client and server, and safe client-facing error handling. Use when handling any external input - forms, API request bodies, query params, CLI args, file parsing, or third-party payloads.
3
+ description: Strict frontend + backend schema validation (Zod or equivalent), normalization before validation (one shared normalizer on both sides - trim, lowercase the email, capitalize each word of a name, canonicalize a profile link or handle) and the rule that a check which cannot fail is not validation, real-time error timing (an emptied or not-yet-filled field is incomplete, not invalid - mark it required with a `*` and an aria-disabled submit instead of showing "not valid" over an empty input), cross-field rules declared on the object schema rather than the field (password against the email or username, confirmation fields, date ranges), schema consistency between client and server, and safe client-facing error handling. Use when handling any external input - forms, API request bodies, query params, CLI args, file parsing, or third-party payloads.
4
4
  ---
5
5
 
6
6
  # Validation & Error Handling Policy
@@ -33,7 +33,13 @@ description: Strict frontend + backend schema validation (Zod or equivalent), no
33
33
  - Use schema-driven validation (e.g. Zod or equivalent).
34
34
  - Validation must prevent invalid state before submission.
35
35
  - UI must reflect validation state immediately and clearly.
36
- - **Every field, not only the famous formats.** Email and password get validated because their rules are obvious; the fields next to them (a profile link, a phone, a job title, a website) are the ones shipped open. Give each field a rule and show its error inline: a format check where there is a format, a max length everywhere, and an "empty after trimming" check where the value is required. A field whose only rule is `z.string()` is an unvalidated field.
36
+ - **Every field, not only the famous formats.** Email and password get validated because their rules are obvious; the fields next to them (a profile link, a phone, a job title, a website) are the ones shipped open. Give each field a rule in the schema: a format check where there is a format, a max length everywhere, and an "empty after trimming" check where the value is required. Show a failing rule inline in that field's error slot; an empty required field is covered by the "Empty is not invalid" rule below, which forbids an error there. A field whose only rule is `z.string()` is an unvalidated field.
37
+ - **Empty is not invalid.** A field the user has just cleared, or has not filled in yet, is INCOMPLETE - it is not wrong. Rendering "Email is not valid" the moment the last character is deleted scolds the user for a state they are deliberately passing through, and it is the most common way a real-time form turns hostile. Two states, two treatments:
38
+ - **Empty (after trimming) and required** -> no error text at all. Say it structurally instead: a `*` on the label with `aria-required="true"`, one legend near the form explaining the marker ("* Required"), and a submit that refuses to send while any required field is empty. Mark the field `aria-required`, not the native `required` attribute - `required` hands the browser its own constraint UI, which fires "Please fill out this field" over the empty input on submit and preempts your message; keep it only alongside `novalidate` on the form. Block the submit with `aria-disabled="true"` rather than the `disabled` attribute: `disabled` takes the button out of the tab order and silences it, so a keyboard or screen-reader user gets the `*` and no way to ask what is missing. Keep it focusable and activatable, style it as unavailable, and make the handler the actual guard - `aria-disabled` is presentational, so a `type="submit"` still posts on click and on Enter unless the submit handler returns early and calls `preventDefault`. Send nothing, name the missing fields in an `aria-live="polite"` region, and move focus to the first one. The reason stays within reach, per frontend-policy's rule that a blocked action must say why. The one exception is the type-to-confirm step guarding an irreversible destroy (delete an account or a repository, drop a database): there the reason already sits in the confirm input's own label, and the native `disabled` is kept as a fail-safe that no handler race can defeat - frontend-policy's destructive-action tiers own that carve-out.
39
+ - **Non-empty and failing its rule** -> the inline error, in the field's error slot, naming what is wrong ("Enter a valid email address") with `aria-invalid="true"`.
40
+ - **Empty and optional** -> valid, silent, and submittable. `.optional()` alone does not buy this: `z.email().optional()` accepts `undefined` and rejects `""` with a format error, which is the exact defect this rule forbids, because a text input hands you `""` and never `undefined`. Normalize `""` to `undefined` before parsing (`z.preprocess(v => (v === "" ? undefined : v), z.email().optional())`), which keeps the format message the state above requires. A union that admits the empty string (`z.union([z.literal(""), z.email()])`) also accepts it, but a failing union reports one top-level `invalid_union` whose message is the generic "Invalid input" - the "Enter a valid email address" is buried in its sub-issues, and the generic one is what a resolver surfaces - so take that route only with an explicit message on the union.
41
+ - The transition matters as much as the states: when a field that was showing a format error is cleared, that error must DISAPPEAR, not be replaced by "Required". The user knows the field is empty - they emptied it. Errors that only make sense after an attempt to submit may name the empty required fields then, and focus the first one - that attempt is the blocked submit above, which is exactly why it stays activatable instead of going inert.
42
+ - This is a schema-shape decision, not a rendering hack: keep the required check and the format check separate (`z.string().trim().min(1, "Required").pipe(z.email())`, not one regex that fails on `""`), with the trim ahead of both so a field holding only spaces reads as empty instead of as a format error. Then the UI can tell "nothing here yet" apart from "this value is wrong" instead of inferring it from the message text.
37
43
  - Validate cross-record constraints (uniqueness, availability, "already in use") in real time too, not just per-field type/format. When the client already holds the relevant set (the list of accounts, profiles, names, slugs it just rendered), check the input against that loaded data on every change and block submission on a conflict - do not defer the duplicate check to the server round-trip (this gives instant feedback and spares a redundant request and its DB query). The server still re-validates as the authority (client checks can be stale), but the user must see the conflict as they type. Mirror the server's exact rule (same pattern, case-folding, reserved values, and scope - e.g. unique per parent vs. globally) so the two never disagree; exclude the record's own current value when editing so renaming to the same name is not flagged.
38
44
 
39
45
  ### Backend / API Validation (Mandatory)
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "validation-policy",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "provider": "FJRG2007/enigma",
5
- "description": "Strict frontend + backend schema validation, normalization before validation (shared normalizers: trim, lowercase email, capitalize names, canonicalize links and handles), schema consistency, and safe client-facing error handling.",
6
- "updated": "2026-08-01T17:44:16+02:00",
7
- "cliVersion": "1.35.7",
8
- "sha": "225e1e26f4a49fac70714b1bebc104ea42143203cc8fcc9e3330b29f05b6e025"
5
+ "description": "Strict frontend + backend schema validation, normalization before validation (shared normalizers: trim, lowercase email, capitalize names, canonicalize links and handles), real-time error timing (an emptied or not-yet-filled field is incomplete, not invalid: a `*` and an aria-disabled submit instead of \"not valid\" over an empty input), schema consistency, and safe client-facing error handling.",
6
+ "updated": "2026-08-11T17:10:56+02:00",
7
+ "cliVersion": "1.35.10",
8
+ "sha": "24928a4c9eed28ebede9f23f746b09a9d3d8176f8fcca61a7780a853fa5bed4e"
9
9
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "enigma-darwin-arm64": "b74a256429fc887c07e3b3faa40fe6055ea0530e1d04d75f93054fe507adb950",
3
- "enigma-linux-arm64": "295993f2b72b55d30566ec2827e6de28ad59b954fd2178695b5dc38aae7b1046",
4
- "enigma-linux-x64": "c8af432bf2e5caf7a6bed8c6bb8dd02f6eb5e6c2f74222c0fb334a1828e916c0",
5
- "enigma-win32-x64.exe": "b1d25eabd061876871a4e7643fe78478e6179d11893e0fb4262353047dc2a086"
2
+ "enigma-darwin-arm64": "728e35d28b2e8726b1db2994b17157c39c5caa370dd83c2848f58ca3c4a6d99c",
3
+ "enigma-linux-arm64": "494ceda44c8cb913384dba6c38738aeba43662ce4a1a7128925539f26af81690",
4
+ "enigma-linux-x64": "40bfdc690fd443d48d1657799b67e17ec5e704159cd48a19f3a0aa9767d06091",
5
+ "enigma-win32-x64.exe": "e9538f2f23c8e777cc2d55e472c99a6c5f9a173028ebe2341e89ede68ff78803"
6
6
  }
package/bin/enigma.mjs CHANGED
@@ -31,7 +31,9 @@ import { ARCH, PLATFORM, packageVersion, pkgRoot } from "./platform.mjs";
31
31
  if (process.argv[2] === "statusline") {
32
32
  try {
33
33
  const { printStatusline } = await import("./statusline.mjs");
34
- printStatusline();
34
+ // Awaited: the renderer reads the piped session asynchronously and resolves
35
+ // once its own write has drained. Exiting before that truncates the bar.
36
+ await printStatusline();
35
37
  } catch { /* a status bar must never error */ }
36
38
  process.exit(0);
37
39
  }
@@ -85,4 +87,4 @@ child.on("exit", (code) => {
85
87
  // can hang up (close) the controlling terminal. A signal exit from the interactive
86
88
  // binary - e.g. quitting the TUI - is treated as a clean exit.
87
89
  process.exit(code ?? 0);
88
- });
90
+ });
@@ -292,32 +292,147 @@ export function render({ session, snapshot, columns = 80, frame = 0, nowSec = 0,
292
292
  return lines.join("\n");
293
293
  }
294
294
 
295
- /** Reads the piped session JSON, or null when nothing is piped in. */
296
- function readSession() {
297
- if (process.stdin.isTTY) return null;
298
- try {
299
- return parseJson(readFileSync(0, "utf8"));
300
- } catch {
301
- return null;
302
- }
295
+ /**
296
+ * How long to wait for the harness to pipe in the session JSON before rendering
297
+ * without it. Only reached when nothing parseable ever arrives; a complete write
298
+ * resolves the read immediately, whether or not the pipe is then closed.
299
+ */
300
+ const STDIN_TIMEOUT_MS = 2000;
301
+
302
+ /** Hard bound on the drain that follows the write, so the caller always gets to exit. */
303
+ const DRAIN_TIMEOUT_MS = 1000;
304
+
305
+ /** Ceiling on the piped session, past which the stream is garbage rather than a slow writer. */
306
+ const MAX_SESSION_BYTES = 1_000_000;
307
+
308
+ /**
309
+ * Reads the piped session JSON, or null when nothing usable is piped in.
310
+ *
311
+ * Deliberately a streaming read rather than `readFileSync(0)`: that call blocks
312
+ * until EOF, so a harness that writes the session and leaves the pipe open leaves
313
+ * this process hung forever. It was observed leaking one orphaned renderer per
314
+ * session on Windows - each holding ~38 MB and a pipe handle until reboot - which
315
+ * is exactly the machine-wide pressure a status bar must never create.
316
+ *
317
+ * Ways out, in the order they fire in practice: the buffer parses as JSON (the
318
+ * harness has written everything, close or no close), the pipe reaches EOF, or the
319
+ * bar renders in its degraded form because the timeout expired or the writer ran
320
+ * past the point where it is still plausibly a session.
321
+ */
322
+ export function readSession(stream = process.stdin, timeoutMs = STDIN_TIMEOUT_MS) {
323
+ return new Promise((resolve) => {
324
+ if (stream.isTTY) return resolve(null);
325
+ let buffer = "";
326
+ let settled = false;
327
+ const timer = setTimeout(() => finish(null), timeoutMs);
328
+ const parsed = () => {
329
+ try {
330
+ return parseJson(buffer);
331
+ } catch {
332
+ // A partial write is not valid JSON, so this only settles once the
333
+ // harness has finished writing the object.
334
+ return undefined;
335
+ }
336
+ };
337
+ const onData = (chunk) => {
338
+ buffer += chunk;
339
+ if (buffer.length > MAX_SESSION_BYTES) return finish(null);
340
+ // Only a complete object can parse, so the scan is worth one closing
341
+ // brace to skip re-parsing a buffer that is still mid-write.
342
+ if (!buffer.trimEnd().endsWith("}")) return;
343
+ const value = parsed();
344
+ if (value !== undefined) finish(value);
345
+ };
346
+ const onEnd = () => finish(parsed() ?? null);
347
+ const onError = () => finish(null);
348
+ const finish = (value) => {
349
+ if (settled) return;
350
+ settled = true;
351
+ clearTimeout(timer);
352
+ buffer = "";
353
+ // Every exit has to stop the flow as well as answer the caller: a stream
354
+ // left attached goes on appending to a buffer nobody will read, which is
355
+ // the cap failing to cap, and on a stream this process does not own the
356
+ // listeners would outlive the read entirely.
357
+ try {
358
+ stream.removeListener("data", onData);
359
+ stream.removeListener("end", onEnd);
360
+ stream.removeListener("error", onError);
361
+ stream.pause();
362
+ } catch { /* a stream that cannot be detached is one already gone */ }
363
+ resolve(value);
364
+ };
365
+ try {
366
+ stream.setEncoding("utf8");
367
+ stream.on("data", onData);
368
+ stream.on("end", onEnd);
369
+ stream.on("error", onError);
370
+ } catch {
371
+ finish(null);
372
+ }
373
+ });
303
374
  }
304
375
 
305
- /** Entry point for `enigma statusline`. Prints the bar and never throws. */
306
- export function printStatusline() {
376
+ /** Streams whose asynchronous write failures are already swallowed. */
377
+ const silenced = new WeakSet();
378
+
379
+ /**
380
+ * Swallows a stream's asynchronous write failures, once per stream.
381
+ *
382
+ * A reader that is already gone answers the write with EPIPE an event-loop turn
383
+ * later, and Node escalates an unhandled `error` event to an uncaught exception:
384
+ * a stack trace on stderr and a nonzero exit, from a bar whose whole contract is
385
+ * to stay quiet. The listener has to outlive the drain it protects, because the
386
+ * event arrives after the failed write has already reported back.
387
+ */
388
+ function silenceWriteErrors(stream) {
389
+ if (silenced.has(stream)) return;
390
+ silenced.add(stream);
391
+ stream.on("error", () => {});
392
+ }
393
+
394
+ /**
395
+ * Writes the bar and resolves once it has drained, so the caller can exit.
396
+ *
397
+ * Destroying stdin is what releases the process: the harness may leave its end of
398
+ * the pipe open, and an open stdin keeps the event loop - and this process - alive
399
+ * for as long as it lasts. The timer covers the other half of the same problem, a
400
+ * stdout whose reader is already gone and whose drain callback never fires.
401
+ */
402
+ function drain(text) {
403
+ return new Promise((resolve) => {
404
+ try {
405
+ process.stdin.destroy();
406
+ } catch { /* nothing was piped in */ }
407
+ const timer = setTimeout(resolve, DRAIN_TIMEOUT_MS);
408
+ const done = () => { clearTimeout(timer); resolve(); };
409
+ try {
410
+ silenceWriteErrors(process.stdout);
411
+ process.stdout.write(text, done);
412
+ } catch {
413
+ done();
414
+ }
415
+ });
416
+ }
417
+
418
+ /** Entry point for `enigma statusline`. Prints the bar and never throws. Await it: it resolves when the write has drained and the process is free to exit. */
419
+ export async function printStatusline() {
420
+ let text = "";
307
421
  try {
308
- const session = readSession();
422
+ const session = await readSession();
309
423
  const cwd = session?.workspace?.current_dir || session?.cwd || process.cwd();
310
424
  const columns = Number(process.env.COLUMNS) || 80;
311
425
  const nowSec = Math.floor(Date.now() / 1000);
312
- process.stdout.write(render({
426
+ text = render({
313
427
  session,
314
428
  snapshot: readSnapshot(cwd),
315
429
  columns,
316
430
  frame: nowSec,
317
431
  nowSec,
318
432
  color256: !process.env.NO_COLOR
319
- }));
433
+ });
320
434
  } catch {
321
435
  // A status bar must never error or emit noise.
322
436
  }
437
+ await drain(text);
323
438
  }
@@ -141,6 +141,72 @@ var BUILTIN_RULES = [
141
141
  // idiomatic and has no detectable signature, and Rust's serde typed deserialization already
142
142
  // enforces shape - a rule for either would false-positive. The generic "validate every input"
143
143
  // principle for those languages lives in the always-on memory kernel instead.
144
+ {
145
+ id: "fe-marquee-duration",
146
+ label: "Looping row driven by a speed, not a duration",
147
+ files: ["*.ts", "*.tsx", "*.js", "*.jsx", "*.css", "*.scss", "*.astro", "*.vue", "*.svelte"],
148
+ excludeFiles: [
149
+ "*.test.*",
150
+ "*.spec.*",
151
+ "*.stories.*",
152
+ "*.min.js",
153
+ "*.min.css",
154
+ "**/tests/**",
155
+ "tests/**",
156
+ "**/__tests__/**",
157
+ "__tests__/**",
158
+ "**/dist/**",
159
+ "dist/**",
160
+ "**/build/**",
161
+ "build/**",
162
+ "**/node_modules/**",
163
+ "node_modules/**",
164
+ "**/vendor/**",
165
+ "vendor/**"
166
+ ],
167
+ ignoreFileCase: true,
168
+ scope: "file",
169
+ // Only newly written code: a duration-driven marquee is common in existing
170
+ // pages, and the rule has to name the one being added, not the backlog.
171
+ stage: "diff",
172
+ // A duration named on the same line as the looping row it drives. Requiring
173
+ // both words keeps every unrelated animation-duration out of the match.
174
+ pattern: "(marquee|ticker|logo-?wall|infinite-?scroll)[^\\n]*(animation-duration|animationDuration|--duration)|(animation-duration|animationDuration|--duration)[^\\n]*(marquee|ticker|logo-?wall|infinite-?scroll)",
175
+ absent: "px/s|pixels per second|pixelsPerSecond|\\bspeed\\b|@enigmax/primitives|useMarquee|createMarquee|enigma:allow-marquee-duration",
176
+ message: "Looping row driven by a duration. A lap is as long as its content, so the speed becomes content/duration and the row runs faster every time an item is added - measured at 45, 67 and 87 px/s for one rail at 10, 15 and 20 items. Take a speed in px/s and derive the duration, or use the primitive that already does: `enigma add marquee` (@enigmax/primitives), which also measures the lap from the DOM instead of computing it. Mark the line `enigma:allow-marquee-duration` when the duration is genuinely the contract.",
177
+ severity: "warn",
178
+ skill: "frontend-policy"
179
+ },
180
+ {
181
+ id: "fe-pointer-capture-drag",
182
+ label: "Drag bound to window, not setPointerCapture",
183
+ files: ["*.ts", "*.tsx", "*.js", "*.jsx", "*.astro", "*.vue", "*.svelte"],
184
+ excludeFiles: [
185
+ "*.test.*",
186
+ "*.spec.*",
187
+ "*.stories.*",
188
+ "*.min.js",
189
+ "**/tests/**",
190
+ "tests/**",
191
+ "**/__tests__/**",
192
+ "__tests__/**",
193
+ "**/dist/**",
194
+ "dist/**",
195
+ "**/build/**",
196
+ "build/**",
197
+ "**/node_modules/**",
198
+ "node_modules/**",
199
+ "**/vendor/**",
200
+ "vendor/**"
201
+ ],
202
+ scope: "file",
203
+ stage: "diff",
204
+ pattern: "\\.setPointerCapture\\s*\\(",
205
+ absent: "enigma:allow-pointer-capture",
206
+ message: "setPointerCapture retargets the compatibility mouse events too, so a plain click on a descendant arrives on the capturing element and the descendant's link never opens - a row of nine logo links quietly stops being nine links and nothing in the source looks wrong. Bind pointermove/pointerup/pointercancel to `window` instead, which is all the capture was for: a mousedown already captures the mouse at the OS level, so a release outside the window still arrives. Fine when the element has no interactive descendants (a slider thumb, a resize handle) - mark it `enigma:allow-pointer-capture` there (frontend-policy).",
207
+ severity: "warn",
208
+ skill: "frontend-policy"
209
+ },
144
210
  {
145
211
  id: "fe-password-input",
146
212
  label: "Reusable password input (show/hide)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigma-cli",
3
- "version": "1.35.7",
3
+ "version": "1.35.10",
4
4
  "description": "Everything you need to work with a coding agent: install shared policy skills for Claude Code, OpenAI Codex and opencode, and set up portable git security hooks.",
5
5
  "type": "module",
6
6
  "bin": {