enigma-cli 1.31.3 → 1.31.5
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.
- package/assets/skills/anti-overengineering-policy/skill.json +1 -1
- package/assets/skills/anti-overengineering-review/skill.json +1 -1
- package/assets/skills/backend-policy/skill.json +1 -1
- package/assets/skills/ciphera-style-policy/skill.json +1 -1
- package/assets/skills/code-review-policy/skill.json +1 -1
- package/assets/skills/core-engineering-policy/skill.json +1 -1
- package/assets/skills/database-expert/skill.json +1 -1
- package/assets/skills/debugging-policy/skill.json +1 -1
- package/assets/skills/dependency-policy/skill.json +1 -1
- package/assets/skills/frontend-design/skill.json +1 -1
- package/assets/skills/frontend-policy/SKILL.md +7 -1
- package/assets/skills/frontend-policy/skill.json +5 -5
- package/assets/skills/git-policy/skill.json +1 -1
- package/assets/skills/logo-sourcing-policy/skill.json +1 -1
- package/assets/skills/security-policy/skill.json +1 -1
- package/assets/skills/skill-creator/skill.json +1 -1
- package/assets/skills/task-completion-policy/skill.json +1 -1
- package/assets/skills/technical-writing-policy/skill.json +1 -1
- package/assets/skills/testing-policy/skill.json +1 -1
- package/assets/skills/validation-policy/skill.json +1 -1
- package/bin/checksums.json +4 -4
- package/bin/enigma.mjs +9 -22
- package/bin/statusline.mjs +323 -0
- package/dist/guardrails.js +11 -0
- package/package.json +1 -1
|
@@ -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.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
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, API and request optimization (batching, avoiding redundant calls, skipping no-op writes), server-side caching (Redis), and Zod boundary validation.",
|
|
6
6
|
"updated": "2026-07-23T04:41:39+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
8
8
|
"sha": "d091e8708888d23ee14cdd6c0e71f386861f2252d95360895a31c73db090a689"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Ciphera code style conventions (formatting, naming, imports, comments, code-level anti-patterns; TypeScript-first, language-agnostic).",
|
|
6
6
|
"updated": "2026-06-26T13:40:52+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
8
8
|
"sha": "dc9ceb784004b05a0117c464e4bed05946835a04acea282586c0b735ee7c2284"
|
|
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-06-01T00:45:28+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
8
8
|
"sha": "3d3bbe0602d5bbb4afe37648fe3c2fa39376b1bcbac5d8c441f01fad1e866ed0"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Core engineering execution policy and harness orchestration (highest-authority rules).",
|
|
6
6
|
"updated": "2026-07-16T22:43:53+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
8
8
|
"sha": "d132f20db08806054d95e58d3d56c2aa7ebc8e6dd24d902b0a0ed9ddfae216c5"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Senior database architecture policy: query optimization, anti-duplication/normalization, scalability, and RGPD/GDPR encryption.",
|
|
6
6
|
"updated": "2026-06-03T14:19:50+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
8
8
|
"sha": "2883bcecb3202683ae6f81b073c3d6a9cec9c55029e011bdd06ba7ac3537297e"
|
|
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-06-01T00:45:28+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
8
8
|
"sha": "14b0064c8b33a0dc85e51464b05005cf5801c756b1101789a6924b9548420f6b"
|
|
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.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
8
8
|
"sha": "6375d835c2aef2c9bd31ce116444dc3d796f510f9970a213aa3ac4696d7e21b9"
|
|
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.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
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), 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 vs pagination, 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), responsive/adaptive layout (fluid units, breakpoints, no overlap or horizontal overflow, viewport meta, touch targets), 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, 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, 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 vs pagination, 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), responsive/adaptive layout (fluid units, breakpoints, no overlap or horizontal overflow, viewport meta, touch targets), 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, 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
|
|
@@ -161,6 +161,12 @@ The general rule: before running any operation, check whether its result would e
|
|
|
161
161
|
|
|
162
162
|
- Track the saved snapshot when the form/settings loads or after a successful save; compute dirtiness by comparing the edited values against it.
|
|
163
163
|
- Example: a value goes x -> z -> back to x before saving. The net change is zero - the form is NOT dirty, and Save must send nothing to the backend.
|
|
164
|
+
- **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.
|
|
165
|
+
- **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.
|
|
166
|
+
- **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.
|
|
167
|
+
- **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.
|
|
168
|
+
- 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.
|
|
169
|
+
- **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.
|
|
164
170
|
- When the form is not dirty, Save must never hit the network. Two acceptable UX options:
|
|
165
171
|
- Preferred: disable / neutralize the Save button while the edited values equal the snapshot, so there is nothing to submit until a real change exists.
|
|
166
172
|
- 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.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frontend-policy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
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), 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/pagination, virtualization, skeletons, progressive loading), 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), variable-length text (min-width:0 in flex/grid, wrap vs truncate, long unbroken strings, worst-case content checks), and AI chat/agent interfaces via Vercel's AI Elements registry instead of hand-rolled message threads.",
|
|
6
|
-
"updated": "2026-07-29T01:
|
|
7
|
-
"cliVersion": "1.31.
|
|
8
|
-
"sha": "
|
|
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/pagination, virtualization, skeletons, progressive loading), 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), variable-length text (min-width:0 in flex/grid, wrap vs truncate, long unbroken strings, worst-case content checks), and AI chat/agent interfaces via Vercel's AI Elements registry instead of hand-rolled message threads.",
|
|
6
|
+
"updated": "2026-07-29T01:26:14+02:00",
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
8
|
+
"sha": "71ebc71510c8aa54d7925445a7dffc55dbce9eba209fb6a63bae05d119217d79"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Git & contribution policy (senior engineering standards).",
|
|
6
6
|
"updated": "2026-07-16T22:44:02+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
8
8
|
"sha": "e6dfbc33884000d9d25841bd9c5a84d6558ffd374882cb7b34451eb2cebc2161"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Application and AI-agent security: secrets, authn/authz (least privilege), OWASP Top 10, transport/crypto baseline, secure logging, and agent/MCP/tool-use safety.",
|
|
6
6
|
"updated": "2026-06-01T00:45:28+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
8
8
|
"sha": "9971e9d9127397d0152e89d24aad3191e2935e55a8483db7fd15f5d4d7a60e7a"
|
|
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-07-29T01:18:36+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
8
8
|
"sha": "13d24c217bdb6fe83fe16835d8f5c3d397a9f3338f16876c61ef96e97f34c90a"
|
|
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-07-27T17:22:17+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
8
8
|
"sha": "b9046c15fd636057a8e42e199d2eeb47297477f38a0a26a07a7b0bf71d61fcb4"
|
|
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.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
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.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
8
8
|
"sha": "3bdf591057b760f674fb2b1425f63acb426cda2c4f042e1a74c5a5d3807df664"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Strict frontend + backend schema validation, schema consistency, and safe client-facing error handling.",
|
|
6
6
|
"updated": "2026-07-22T01:41:06+02:00",
|
|
7
|
-
"cliVersion": "1.31.
|
|
7
|
+
"cliVersion": "1.31.5",
|
|
8
8
|
"sha": "d937df0052d1ec151728a28f6567744d9e7ca0a65d84b4d5c9a697f1e40d704f"
|
|
9
9
|
}
|
package/bin/checksums.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"enigma-darwin-arm64": "
|
|
3
|
-
"enigma-linux-arm64": "
|
|
4
|
-
"enigma-linux-x64": "
|
|
5
|
-
"enigma-win32-x64.exe": "
|
|
2
|
+
"enigma-darwin-arm64": "8706843ef8eb3bb58cabb4ecfefd4b5759fa046fe63d241ba59774dd398a2fd7",
|
|
3
|
+
"enigma-linux-arm64": "d077e465d985310d0d774d7593ce7354c887e30774925883b38b83bf9c274c51",
|
|
4
|
+
"enigma-linux-x64": "e1fd9298a22e93a61ea01e30755f5295c34c005a5a1a6afad3cd887fbf342795",
|
|
5
|
+
"enigma-win32-x64.exe": "30a76b1f92915b34123b5e3fd438d388bf932eb6e25f6d633c53e5458c0a191a"
|
|
6
6
|
}
|
package/bin/enigma.mjs
CHANGED
|
@@ -15,36 +15,23 @@
|
|
|
15
15
|
* here, since its own __dirname lives in Bun's virtual filesystem.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { spawn } from "node:child_process";
|
|
19
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
-
import { join } from "node:path";
|
|
21
18
|
import os from "node:os";
|
|
22
|
-
import {
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { existsSync } from "node:fs";
|
|
21
|
+
import { spawn } from "node:child_process";
|
|
23
22
|
import { downloadBinary, installedBinary } from "./download.mjs";
|
|
23
|
+
import { ARCH, PLATFORM, packageVersion, pkgRoot } from "./platform.mjs";
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Fast path: an agent status bar (e.g. Claude Code's statusLine) calls `enigma
|
|
27
|
-
* statusline` on every refresh. Handle it here in the lightweight Node launcher
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* the level, e.g. `[ENIGMA:FULL]`.
|
|
27
|
+
* statusline` on every refresh. Handle it here in the lightweight Node launcher so
|
|
28
|
+
* it never resolves or spawns the Bun binary. The renderer is imported lazily, so
|
|
29
|
+
* every other command pays nothing for it.
|
|
31
30
|
*/
|
|
32
|
-
function readOutputStyle() {
|
|
33
|
-
let style = "off";
|
|
34
|
-
// Mirror config.ts precedence: global (~/.enigma.json) then local (cwd), nearest wins.
|
|
35
|
-
for (const dir of [os.homedir(), process.cwd()]) {
|
|
36
|
-
try {
|
|
37
|
-
const raw = JSON.parse(readFileSync(join(dir, ".enigma.json"), "utf8"));
|
|
38
|
-
if (raw && typeof raw.outputStyle === "string") style = raw.outputStyle;
|
|
39
|
-
} catch { /* missing/invalid .enigma.json - ignore */ }
|
|
40
|
-
}
|
|
41
|
-
return style;
|
|
42
|
-
};
|
|
43
31
|
if (process.argv[2] === "statusline") {
|
|
44
32
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
process.stdout.write(process.env.NO_COLOR ? `[${label}]` : `\x1b[38;5;172m[${label}]\x1b[0m`);
|
|
33
|
+
const { printStatusline } = await import("./statusline.mjs");
|
|
34
|
+
printStatusline();
|
|
48
35
|
} catch { /* a status bar must never error */ }
|
|
49
36
|
process.exit(0);
|
|
50
37
|
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Status bar renderer for agent status lines (Claude Code's `statusLine`).
|
|
4
|
+
*
|
|
5
|
+
* Lives on the launcher's fast path (bin/enigma.mjs), so it stays dependency-free
|
|
6
|
+
* and never resolves the Bun binary: a status bar re-runs on every refresh, and
|
|
7
|
+
* cold-starting a ~260 MB binary each time is not an option. That constraint is
|
|
8
|
+
* also why gate progress arrives through a small snapshot file the daemon writes
|
|
9
|
+
* instead of the gate's SQLite database, which only the Bun runtime can open.
|
|
10
|
+
*
|
|
11
|
+
* Both inputs are optional: the session JSON the agent pipes on stdin, and the
|
|
12
|
+
* gate snapshot. Output is one line at rest, two while a gate run is active.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here may throw. A status bar that errors displays the error until the
|
|
15
|
+
* next refresh, so every reader is guarded and every field is treated as absent
|
|
16
|
+
* until proven otherwise.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
|
|
23
|
+
/** Snapshot schema this renderer understands; a newer file is ignored, not guessed at. */
|
|
24
|
+
const SNAPSHOT_VERSION = 1;
|
|
25
|
+
|
|
26
|
+
/** Spinner frames, advanced once per second (the status bar's fastest refresh). */
|
|
27
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
28
|
+
|
|
29
|
+
const BAR_FILLED = "▰";
|
|
30
|
+
const BAR_EMPTY = "▱";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 256-color SGR codes. The status/state colors use the low indices so they follow
|
|
34
|
+
* the terminal's own palette, but greys are pinned to the greyscale ramp: index 8
|
|
35
|
+
* ("bright black") is rendered near-black by many terminals, which makes secondary
|
|
36
|
+
* text vanish on a dark background. 245 matches the weight of the harness's own
|
|
37
|
+
* hint text; `track` is a step quieter, since an empty meter cell is a background
|
|
38
|
+
* rail rather than something to read.
|
|
39
|
+
*/
|
|
40
|
+
const COLORS = { red: 1, green: 2, yellow: 3, blue: 4, cyan: 6, dim: 245, track: 240, amber: 172 };
|
|
41
|
+
|
|
42
|
+
/** Built at runtime so no raw control byte ever lives in this source file. */
|
|
43
|
+
const ESC = String.fromCharCode(27);
|
|
44
|
+
|
|
45
|
+
/** Matches any SGR sequence, so styled text can be measured by visible width. */
|
|
46
|
+
const SGR = new RegExp(`${ESC}\\[[0-9;]*m`, "g");
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* How a step status paints its cell in the pipeline bar. `pulse` marks the
|
|
50
|
+
* states that animate, so a parked or working run is visibly alive.
|
|
51
|
+
*/
|
|
52
|
+
const STEP_STYLES = {
|
|
53
|
+
completed: { color: "green", glyph: BAR_FILLED },
|
|
54
|
+
running: { color: "blue", glyph: BAR_FILLED },
|
|
55
|
+
fixing: { color: "cyan", glyph: BAR_FILLED },
|
|
56
|
+
fix_review: { color: "yellow", glyph: BAR_FILLED, pulse: true },
|
|
57
|
+
awaiting_approval: { color: "yellow", glyph: BAR_FILLED, pulse: true },
|
|
58
|
+
failed: { color: "red", glyph: BAR_FILLED },
|
|
59
|
+
skipped: { color: "dim", glyph: BAR_FILLED },
|
|
60
|
+
pending: { color: "track", glyph: BAR_EMPTY }
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Step statuses that mean the pipeline is working or parked rather than settled. */
|
|
64
|
+
const ACTIVE_STEP_STATUSES = new Set(["running", "fixing", "fix_review", "awaiting_approval"]);
|
|
65
|
+
|
|
66
|
+
/** Run statuses worth showing; a settled run leaves the status bar alone. */
|
|
67
|
+
const ACTIVE_RUN_STATUSES = new Set(["pending", "running"]);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parses JSON, tolerating a leading UTF-8 BOM. Windows tooling emits one freely,
|
|
71
|
+
* and every reader here fails silently by design, so a BOM would otherwise show
|
|
72
|
+
* up as an inexplicably blank status bar.
|
|
73
|
+
*/
|
|
74
|
+
function parseJson(text) {
|
|
75
|
+
return JSON.parse(text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Wraps text in a 256-color SGR sequence, or returns it unchanged without color. */
|
|
79
|
+
function paint(text, color, color256) {
|
|
80
|
+
if (!color256) return text;
|
|
81
|
+
return `${ESC}[38;5;${COLORS[color] ?? COLORS.dim}m${text}${ESC}[0m`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Dims a string to the secondary palette entry. */
|
|
85
|
+
function dim(text, color256) {
|
|
86
|
+
return paint(text, "dim", color256);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Visible width of a string, ignoring SGR sequences. */
|
|
90
|
+
function width(text) {
|
|
91
|
+
return text.replace(SGR, "").length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Joins parts with a separator, dropping the lowest-priority parts (those latest
|
|
96
|
+
* in the list with `optional: true`) until the line fits `max` columns. Required
|
|
97
|
+
* parts are never dropped; an over-long line is truncated as a last resort.
|
|
98
|
+
*/
|
|
99
|
+
function fit(parts, sep, max) {
|
|
100
|
+
const kept = parts.filter((p) => p && p.text);
|
|
101
|
+
while (kept.length > 1) {
|
|
102
|
+
const line = kept.map((p) => p.text).join(sep);
|
|
103
|
+
if (width(line) <= max) return line;
|
|
104
|
+
const victim = kept.map((p, i) => (p.optional ? i : -1)).filter((i) => i !== -1).pop();
|
|
105
|
+
if (victim === undefined) break;
|
|
106
|
+
kept.splice(victim, 1);
|
|
107
|
+
}
|
|
108
|
+
const line = kept.map((p) => p.text).join(sep);
|
|
109
|
+
return width(line) <= max ? line : `${line.slice(0, Math.max(0, max - 1))}…`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Renders an n-cell meter, e.g. context window usage. */
|
|
113
|
+
function meter(fraction, cells, color, color256) {
|
|
114
|
+
const filled = Math.max(0, Math.min(cells, Math.round(fraction * cells)));
|
|
115
|
+
return paint(BAR_FILLED.repeat(filled), color, color256)
|
|
116
|
+
+ paint(BAR_EMPTY.repeat(cells - filled), "track", color256);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Colors a context percentage by pressure: green under 70, yellow under 90, red beyond. */
|
|
120
|
+
function pressureColor(pct) {
|
|
121
|
+
if (pct >= 90) return "red";
|
|
122
|
+
if (pct >= 70) return "yellow";
|
|
123
|
+
return "green";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Compact elapsed time: "42s", "7m12s", "2h05m". */
|
|
127
|
+
function formatElapsed(seconds) {
|
|
128
|
+
const s = Math.max(0, Math.floor(seconds));
|
|
129
|
+
if (s < 60) return `${s}s`;
|
|
130
|
+
const m = Math.floor(s / 60);
|
|
131
|
+
if (m < 60) return `${m}m${String(s % 60).padStart(2, "0")}s`;
|
|
132
|
+
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Reads the token-efficient output level from .enigma.json, mirroring config.ts
|
|
137
|
+
* precedence: global (~/.enigma.json) then local (cwd), nearest wins.
|
|
138
|
+
*/
|
|
139
|
+
function readOutputStyle() {
|
|
140
|
+
let style = "off";
|
|
141
|
+
for (const dir of [os.homedir(), process.cwd()]) {
|
|
142
|
+
try {
|
|
143
|
+
const raw = parseJson(readFileSync(join(dir, ".enigma.json"), "utf8"));
|
|
144
|
+
if (raw && typeof raw.outputStyle === "string") style = raw.outputStyle;
|
|
145
|
+
} catch { /* missing/invalid .enigma.json - ignore */ }
|
|
146
|
+
}
|
|
147
|
+
return style;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The [ENIGMA] badge, carrying the active token-efficient level when one is set. */
|
|
151
|
+
function badge(color256) {
|
|
152
|
+
const style = readOutputStyle();
|
|
153
|
+
const label = (!style || style === "off") ? "ENIGMA" : `ENIGMA:${style.toUpperCase()}`;
|
|
154
|
+
return paint(`[${label}]`, "amber", color256);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Path to the snapshot the gate daemon writes, mirroring gate/paths.ts. */
|
|
158
|
+
export function snapshotPath() {
|
|
159
|
+
const root = process.env.ENIGMA_GATE_HOME || join(os.homedir(), ".enigma", "gate");
|
|
160
|
+
return join(root, "statusline.json");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Reports whether a process is alive, used to discard a snapshot from a dead daemon. */
|
|
164
|
+
function pidAlive(pid) {
|
|
165
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
166
|
+
try {
|
|
167
|
+
process.kill(pid, 0);
|
|
168
|
+
return true;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
// EPERM means the process exists but belongs to another user.
|
|
171
|
+
return err && err.code === "EPERM";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Loads the gate snapshot for `cwd`, or null when there is nothing to show.
|
|
177
|
+
* Rejects a snapshot that is for another repository, from an unknown schema
|
|
178
|
+
* version, already settled, or left behind by a daemon that is no longer running.
|
|
179
|
+
*/
|
|
180
|
+
export function readSnapshot(cwd, path = snapshotPath()) {
|
|
181
|
+
let snap;
|
|
182
|
+
try {
|
|
183
|
+
snap = parseJson(readFileSync(path, "utf8"));
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
if (!snap || snap.version !== SNAPSHOT_VERSION) return null;
|
|
188
|
+
if (!ACTIVE_RUN_STATUSES.has(snap.status)) return null;
|
|
189
|
+
if (!Array.isArray(snap.steps) || snap.steps.length === 0) return null;
|
|
190
|
+
if (typeof snap.repoPath !== "string" || !isInside(cwd, snap.repoPath)) return null;
|
|
191
|
+
if (!pidAlive(snap.pid)) return null;
|
|
192
|
+
return snap;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Reports whether `cwd` is the repo root or a directory beneath it. */
|
|
196
|
+
function isInside(cwd, root) {
|
|
197
|
+
const a = String(cwd || "").replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
198
|
+
const b = root.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
199
|
+
return a === b || a.startsWith(`${b}/`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Renders the base line: the enigma badge, model, project, context meter and
|
|
204
|
+
* session cost. Everything after the badge is optional and drops away on a
|
|
205
|
+
* narrow terminal, in reverse order of usefulness.
|
|
206
|
+
*/
|
|
207
|
+
function renderBase(session, columns, color256) {
|
|
208
|
+
const model = session?.model?.display_name;
|
|
209
|
+
const dir = session?.workspace?.current_dir || session?.cwd;
|
|
210
|
+
const project = typeof dir === "string" ? dir.replace(/\\/g, "/").split("/").filter(Boolean).pop() : null;
|
|
211
|
+
const pct = session?.context_window?.used_percentage;
|
|
212
|
+
const cost = session?.cost?.total_cost_usd;
|
|
213
|
+
|
|
214
|
+
const parts = [{ text: badge(color256) }];
|
|
215
|
+
if (model) parts.push({ text: paint(model, "cyan", color256), optional: true });
|
|
216
|
+
if (project) parts.push({ text: dim(project, color256), optional: true });
|
|
217
|
+
if (typeof pct === "number") {
|
|
218
|
+
const rounded = Math.round(pct);
|
|
219
|
+
parts.push({
|
|
220
|
+
text: `${meter(rounded / 100, 8, pressureColor(rounded), color256)} ${dim(`${rounded}%`, color256)}`,
|
|
221
|
+
optional: true
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (typeof cost === "number" && cost > 0) {
|
|
225
|
+
parts.push({ text: dim(`$${cost.toFixed(2)}`, color256), optional: true });
|
|
226
|
+
}
|
|
227
|
+
return fit(parts, " ", columns);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Renders the gate line: a spinner, one bar cell per pipeline step, the step
|
|
232
|
+
* count, the active step with its elapsed time, and the PR once one exists.
|
|
233
|
+
*
|
|
234
|
+
* The bar doubles as the legend - cell N is step N - so the whole pipeline fits
|
|
235
|
+
* one line. The active cell alternates glyph each frame, which is what makes a
|
|
236
|
+
* long step visibly still working rather than hung.
|
|
237
|
+
*/
|
|
238
|
+
function renderGate(snap, columns, frame, nowSec, color256) {
|
|
239
|
+
const steps = snap.steps.filter((s) => s && typeof s.name === "string");
|
|
240
|
+
const done = steps.filter((s) => s.status === "completed" || s.status === "skipped").length;
|
|
241
|
+
const active = steps.find((s) => ACTIVE_STEP_STATUSES.has(s.status));
|
|
242
|
+
const failed = steps.find((s) => s.status === "failed");
|
|
243
|
+
const current = active ?? failed;
|
|
244
|
+
|
|
245
|
+
const cells = steps.map((s) => {
|
|
246
|
+
const style = STEP_STYLES[s.status] ?? STEP_STYLES.pending;
|
|
247
|
+
const glyph = style.pulse && frame % 2 === 1 ? BAR_EMPTY : style.glyph;
|
|
248
|
+
return paint(glyph, style.color, color256);
|
|
249
|
+
}).join("");
|
|
250
|
+
|
|
251
|
+
// The spinner turns only while a step is actually working; a failed run holds
|
|
252
|
+
// a red marker instead, so a stalled pipeline never looks like a busy one.
|
|
253
|
+
const spinner = active
|
|
254
|
+
? paint(SPINNER[frame % SPINNER.length], snap.awaiting ? "yellow" : "blue", color256)
|
|
255
|
+
: paint(BAR_FILLED, failed ? "red" : "dim", color256);
|
|
256
|
+
|
|
257
|
+
let labelColor = "blue";
|
|
258
|
+
if (snap.awaiting) labelColor = "yellow";
|
|
259
|
+
else if (!active && failed) labelColor = "red";
|
|
260
|
+
const label = snap.awaiting ? "needs you" : (current ? current.name : snap.status);
|
|
261
|
+
// A negative age means the snapshot and this process disagree about the clock.
|
|
262
|
+
// Dropping the segment is honest; a confident "0s" is not.
|
|
263
|
+
const started = current?.startedAt ?? snap.startedAt;
|
|
264
|
+
const age = typeof started === "number" ? nowSec - started : -1;
|
|
265
|
+
const elapsed = age >= 0 ? formatElapsed(age) : null;
|
|
266
|
+
|
|
267
|
+
const parts = [
|
|
268
|
+
{ text: `${spinner} ${dim("gate", color256)}` },
|
|
269
|
+
{ text: cells },
|
|
270
|
+
{ text: dim(`${done}/${steps.length}`, color256) },
|
|
271
|
+
{ text: paint(label, labelColor, color256) }
|
|
272
|
+
];
|
|
273
|
+
if (elapsed) parts.push({ text: dim(elapsed, color256), optional: true });
|
|
274
|
+
if (typeof snap.branch === "string" && snap.branch) {
|
|
275
|
+
parts.push({ text: dim(snap.branch, color256), optional: true });
|
|
276
|
+
}
|
|
277
|
+
if (typeof snap.prUrl === "string") {
|
|
278
|
+
const num = snap.prUrl.match(/\/(\d+)(?:\/)?$/);
|
|
279
|
+
if (num) parts.push({ text: dim(`PR #${num[1]}`, color256), optional: true });
|
|
280
|
+
}
|
|
281
|
+
return fit(parts, " ", columns);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Builds the full status bar. `frame` drives the animation and is derived from
|
|
286
|
+
* wall-clock seconds by the caller, so consecutive refreshes advance it without
|
|
287
|
+
* this module holding any state.
|
|
288
|
+
*/
|
|
289
|
+
export function render({ session, snapshot, columns = 80, frame = 0, nowSec = 0, color256 = true }) {
|
|
290
|
+
const lines = [renderBase(session, columns, color256)];
|
|
291
|
+
if (snapshot) lines.push(renderGate(snapshot, columns, frame, nowSec, color256));
|
|
292
|
+
return lines.join("\n");
|
|
293
|
+
}
|
|
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
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Entry point for `enigma statusline`. Prints the bar and never throws. */
|
|
306
|
+
export function printStatusline() {
|
|
307
|
+
try {
|
|
308
|
+
const session = readSession();
|
|
309
|
+
const cwd = session?.workspace?.current_dir || session?.cwd || process.cwd();
|
|
310
|
+
const columns = Number(process.env.COLUMNS) || 80;
|
|
311
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
312
|
+
process.stdout.write(render({
|
|
313
|
+
session,
|
|
314
|
+
snapshot: readSnapshot(cwd),
|
|
315
|
+
columns,
|
|
316
|
+
frame: nowSec,
|
|
317
|
+
nowSec,
|
|
318
|
+
color256: !process.env.NO_COLOR
|
|
319
|
+
}));
|
|
320
|
+
} catch {
|
|
321
|
+
// A status bar must never error or emit noise.
|
|
322
|
+
}
|
|
323
|
+
}
|
package/dist/guardrails.js
CHANGED
|
@@ -336,6 +336,17 @@ var BUILTIN_RULES = [
|
|
|
336
336
|
severity: "block",
|
|
337
337
|
skill: "technical-writing-policy"
|
|
338
338
|
},
|
|
339
|
+
// NOTE: there is deliberately no "Save button with no dirty check" rule, even though
|
|
340
|
+
// no-op detection is one of frontend-policy's headline rules. It has no file-local
|
|
341
|
+
// signature that survives measurement. The dirtiness normally lives in a parent, a store
|
|
342
|
+
// or a form library, so its absence from THIS file proves nothing; the policy explicitly
|
|
343
|
+
// allows an enabled Save that short-circuits on click, so a missing `disabled` is not
|
|
344
|
+
// evidence of the defect either; and the same markup is correct on a create form, which
|
|
345
|
+
// has nothing to compare against. Measured on the corpus: 2 save buttons in 108
|
|
346
|
+
// form-capable files (no signal), and the interaction-based dirty flag that WOULD be a
|
|
347
|
+
// precise signature (a dirty/hasChanges flag assigned a literal true) returned 0 real
|
|
348
|
+
// hits and 2 false ones, a CLI tracking whether it had rewritten a config file. A rule
|
|
349
|
+
// here would fire on correct code, so it stays guidance in frontend-policy.
|
|
339
350
|
// NOTE: there is deliberately no "card inside a card" or "border with no information"
|
|
340
351
|
// rule, even though both are named in frontend-policy. They are RELATIONAL defects: a
|
|
341
352
|
// container is redundant only relative to the ancestor it sits in and the spacing around
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enigma-cli",
|
|
3
|
-
"version": "1.31.
|
|
3
|
+
"version": "1.31.5",
|
|
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": {
|