enigma-cli 1.35.7 → 1.35.8

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.
@@ -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.8",
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.8",
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.8",
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.8",
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.8",
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.8",
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.8",
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.8",
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.8",
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.8",
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.8",
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
@@ -234,13 +234,13 @@ The general rule: before running any operation, check whether its result would e
234
234
  - **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
235
  - **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
236
  - **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.
237
+ - **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.
238
+ - 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
239
  - **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
240
  - 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.
241
+ - 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
242
  - 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.
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 blocked, or the click just confirms success without a request.
244
244
 
245
245
  ### Enforce it server-side too
246
246
 
@@ -287,6 +287,7 @@ Where this lives and when it runs:
287
287
  - 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
288
  - Validate the normalized value, so a trailing space the user has not finished typing never renders as an error.
289
289
  - 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.
290
+ - 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
291
 
291
292
  ---
292
293
 
@@ -309,20 +310,20 @@ Auth is the first screen a user meets and the one most often shipped half-built.
309
310
 
310
311
  - 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
312
  - 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.
313
+ - 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
314
  - 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
315
  - 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
316
  - 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
317
  - 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
318
  - 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".
319
+ - 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
320
  - 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
321
  - **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
322
  - Auto-submit needs three guards, and without them it burns the user's attempt budget:
322
323
  - **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
324
  - **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
325
  - **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.
326
+ - 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
327
  - 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
328
  - 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
329
 
@@ -487,7 +488,7 @@ Guard every destructive action behind friction proportional to how bad an accide
487
488
 
488
489
  - 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
490
  - 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.
491
+ - 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
492
 
492
493
  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
494
 
@@ -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-11T17:10:56+02:00",
7
+ "cliVersion": "1.35.8",
8
+ "sha": "54d8dcec18cf14a69617bbb919e61e329f72eaf0205f2034faef4ea31e9c7544"
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.8",
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.8",
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.8",
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.8",
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.8",
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.8",
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.8",
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.8",
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": "f80c9e61502413425c345c9c47dba54d6e4f3643b29dd5adab2df4faa1862080",
3
+ "enigma-linux-arm64": "942ffce99f67dbc4c0891d8bcd11cc3d30651b377ed8a45091f2dcc3ee2e9376",
4
+ "enigma-linux-x64": "27826793783f56800fb48b01a81c1685e56c3f7762ee549ab140ac6a46dae08d",
5
+ "enigma-win32-x64.exe": "2e028371022a814c2812965b55f90e0165a4afa1ba9afa1f2e09d42ac06a26f2"
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigma-cli",
3
- "version": "1.35.7",
3
+ "version": "1.35.8",
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": {