exonous 2.10.4 → 2.10.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.
- package/assets/machine/hooks/lib/pr-template-validate.cjs +2 -0
- package/assets/machine/hooks/pyle.cjs +3 -4
- package/assets/skills/commit-changes/SKILL.md +3 -2
- package/assets/skills/make-pr/SKILL.md +5 -7
- package/assets/skills/ui-design/reference/adapt.md +2 -2
- package/assets/skills/ui-design/reference/adapt.native.md +1 -1
- package/assets/skills/ui-design/reference/critique.md +6 -6
- package/assets/skills/ui-design/reference/harden.md +5 -3
- package/assets/skills/ui-design/reference/hooks.md +1 -1
- package/assets/skills/ui-design/reference/styles/direction.md +5 -4
- package/assets/skills/ui-design/reference/typeset.md +1 -0
- package/assets/skills/ui-design/scripts/ENGINE-CHECKSUMS +6 -0
- package/assets/skills/ui-design/scripts/impeccable +78 -188
- package/assets/skills/ui-design/scripts/impeccable.cmd +2 -212
- package/assets/skills/ui-design/scripts/impeccable.ps1 +93 -0
- package/assets/skills/ui-design/scripts/ui-design.mjs +40 -9
- package/assets/skills/ui-motion/SKILL.md +5 -3
- package/assets/skills/ui-motion/reference/review.md +9 -7
- package/assets/skills/ui-motion/reference/web.md +6 -4
- package/assets/techne/scripts/hooks/config-guard.js +15 -11
- package/assets/techne/scripts/hooks/stop-checks.js +5 -1
- package/assets/techne/skills/go/reference/patterns.md +1 -1
- package/assets/techne/skills/quality/reference/verification-loop.md +6 -3
- package/native/aarch64-apple-darwin/exonous +0 -0
- package/native/aarch64-unknown-linux-gnu/exonous +0 -0
- package/native/x86_64-apple-darwin/exonous +0 -0
- package/native/x86_64-pc-windows-msvc/exonous.exe +0 -0
- package/native/x86_64-unknown-linux-gnu/exonous +0 -0
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ const { resolveTemplate } = require('./template.cjs');
|
|
|
9
9
|
const { gitEnv } = require('./paths.cjs');
|
|
10
10
|
|
|
11
11
|
const MAX_CHANGED_FILES = 30;
|
|
12
|
+
const PR_CREATE_GUIDANCE = 'Use `exonous pr create --base <base> --head <branch> --title <title> --body-file <file>` so the validated PR and its handoff are created together';
|
|
12
13
|
|
|
13
14
|
function normalize(text) {
|
|
14
15
|
return text.normalize('NFC').toLowerCase();
|
|
@@ -95,6 +96,7 @@ function validateGhPrCreateCommand(command, cwd = process.cwd()) {
|
|
|
95
96
|
|
|
96
97
|
module.exports = {
|
|
97
98
|
MAX_CHANGED_FILES,
|
|
99
|
+
PR_CREATE_GUIDANCE,
|
|
98
100
|
validateBodyContent,
|
|
99
101
|
validateBodyFile,
|
|
100
102
|
validateGhPrCreateCommand,
|
|
@@ -6,7 +6,7 @@ const { spawnSync } = require('child_process');
|
|
|
6
6
|
|
|
7
7
|
// A half-updated or broken runtime must still block git/gh: Windsurf, for one, only blocks on exit 2.
|
|
8
8
|
let payload, token, repoRoot, gitEnv, gitOut, analyze, isolationProblem, redirectionProblem;
|
|
9
|
-
let parseOptions, has, COMMIT, PUSH, GH_PR_CREATE,
|
|
9
|
+
let parseOptions, has, COMMIT, PUSH, GH_PR_CREATE, PR_CREATE_GUIDANCE;
|
|
10
10
|
let loadError = null;
|
|
11
11
|
try {
|
|
12
12
|
payload = require('./lib/payload.cjs');
|
|
@@ -14,7 +14,7 @@ try {
|
|
|
14
14
|
({ repoRoot, gitEnv, gitOut } = require('./lib/paths.cjs'));
|
|
15
15
|
({ analyze, isolationProblem, redirectionProblem } = require('./lib/shell.cjs'));
|
|
16
16
|
({ parseOptions, has, COMMIT, PUSH, GH_PR_CREATE } = require('./lib/cli-options.cjs'));
|
|
17
|
-
({
|
|
17
|
+
({ PR_CREATE_GUIDANCE } = require('./lib/pr-template-validate.cjs'));
|
|
18
18
|
} catch (err) {
|
|
19
19
|
loadError = err;
|
|
20
20
|
}
|
|
@@ -185,8 +185,7 @@ function checkPr(call) {
|
|
|
185
185
|
if (parsed.error) return `cannot verify its options: ${parsed.error}`;
|
|
186
186
|
if (has(parsed, 'help')) return null;
|
|
187
187
|
if (parsed.positional.length) return `unexpected argument "${parsed.positional[0]}"`;
|
|
188
|
-
|
|
189
|
-
return result.ok ? null : result.reason.replace(/\.$/, '');
|
|
188
|
+
return PR_CREATE_GUIDANCE;
|
|
190
189
|
}
|
|
191
190
|
|
|
192
191
|
// ── Gate ─────────────────────────────────────────────────────────────────────
|
|
@@ -12,11 +12,12 @@ exonous gates `git commit`, `git push` and `gh pr create` in every gated AI tool
|
|
|
12
12
|
```
|
|
13
13
|
/commit-changes → /security-review → /code-review → exonous token commit → git commit (token: one commit of the staged tree, 15 min)
|
|
14
14
|
force-push or push to main/master → only on the user's explicit request → exonous token push → git push
|
|
15
|
-
/make-pr → ≤ 30 changed files → body at `exonous draft-path` → exonous validate-pr <file> →
|
|
15
|
+
/make-pr → ≤ 30 changed files → body at `exonous draft-path` → exonous validate-pr <file> → exonous pr create --body-file <file>
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
- Run each gated command **alone** (optionally after one `cd <dir> &&`): no `&&` chains, pipes, `;`, wrappers, `sh -c`, `-a`/`-i`/`-o`/`-p` or pathspecs on `git commit`.
|
|
19
|
-
- If the gate blocks you, read its reason, run `exonous workflow commit` (or `pr`), fix what it names, then retry. Never work around it.
|
|
19
|
+
- If the gate blocks you before execution, read its reason, run `exonous workflow commit` (or `pr`), fix what it names, then retry the managed command. Never work around it.
|
|
20
|
+
- After remote PR success, memory failure or `no-vault` never justifies repeating creation. For ambiguous output, inspect the remote PR state before another attempt; use `/make-pr`'s explicit handoff fallback for a confirmed existing PR.
|
|
20
21
|
|
|
21
22
|
Steps:
|
|
22
23
|
1. Run `git status --short` and `git branch --show-current`. If there is nothing to commit and nothing unpushed, say so and stop.
|
|
@@ -15,10 +15,8 @@ model: sonnet
|
|
|
15
15
|
3. Leia `git log origin/<base>..HEAD --oneline`, `git diff --stat origin/<base>...HEAD` e os hunks relevantes. Conte `git diff --name-only origin/<base>...HEAD`: máximo 30 arquivos. Acima disso, proponha divisão coesa antes de abrir PRs menores/empilhadas.
|
|
16
16
|
4. Execute `exonous template`. Escreva um corpo humano, conciso e em PT-BR, com todos os títulos na ordem do template. No padrão: `## Resumo`, `## Principais mudanças`, `## Arquitetura & implementação`, `## Antes → Agora`, `## Roteiro de teste`. Seção inaplicável mantém título e `_N/A — não aplicável a esta mudança_`. Relate apenas validação observada; sem menção a agentes/IA, créditos ou coautoria. Título convencional em inglês (`type: short description`); `$ARGUMENTS` pode orientar título/contexto.
|
|
17
17
|
5. Execute `exonous draft-path`, grave o corpo nesse caminho literal e execute `exonous validate-pr <draft-path>`. Corrija falhas antes de continuar.
|
|
18
|
-
6. Execute
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Resuma o que foi feito em uma tripla `did` curta e concreta. Use valores únicos e referências curtas, sem nós redundantes, narrativa ou segredos. Execute `exonous handoff save --body-file <temporário>`. O helper acrescenta instante/repo/branch/HEAD e salva no único vault central existente. Capture o resultado sem mostrá-lo; `no-vault`, helper ausente ou erro de gravação não transforma a PR criada em falha nem pede/cria vault.
|
|
24
|
-
8. Remova apenas os temporários desta execução (corpo e handoff). Retorne a URL da PR, sem anúncio adicional sobre o handoff.
|
|
18
|
+
6. Execute `exonous pr create --base <base> --head <current-branch> --title "<title>" --body-file <draft-path>`; acrescente `--draft` quando solicitado. O helper nativo valida uma cópia privada do corpo e envia esses mesmos bytes ao `gh`, sem shell. Criação direta por `gh pr create` é bloqueada. CLI legado: atualize para o nativo; não contorne o gate.
|
|
19
|
+
7. Sucesso com uma única URL canônica `https://github.com/<owner>/<repo>/pull/<número>` grava automaticamente um handoff mínimo no vault central existente. Falha, sinal ou saída ambígua: relate o resultado e verifique o estado remoto antes de qualquer nova tentativa. Erro de memória ou `no-vault` não repete a criação, não pede/cria vault e preserva o rascunho.
|
|
20
|
+
8. O helper remove sua cópia privada e, após salvar, somente o rascunho esperado deste checkout quando ele continua intacto. Preserve corpos arbitrários, alterados ou de outro checkout. Retorne a URL da PR, sem anúncio adicional sobre o handoff.
|
|
21
|
+
|
|
22
|
+
Fallback explícito para uma PR já criada e confirmada: grave um JSON temporário privado `{"v":1,"k":"pr","g":[["pr","url","<URL canônica confirmada>"],["pr","proof","PR criada"],["pr","next","revisar PR"]]}` e execute `exonous handoff save --body-file <temporário>`. A mesma URL retorna `already-saved` sem substituir o primeiro registro. Remova apenas esse temporário; não recrie a PR nem registre título, corpo ou saída do `gh` na memória.
|
|
@@ -52,7 +52,7 @@ Breakpoints (typical): mobile 320–767; tablet 768–1023; desktop 1024+—or c
|
|
|
52
52
|
|
|
53
53
|
Techniques: Grid/Flex reflow; container queries; `clamp()`; media queries; show/hide per context.
|
|
54
54
|
|
|
55
|
-
Touch: larger targets; spacing; replace hover-only paths; touch feedback; thumb zones.
|
|
55
|
+
Touch: larger targets; spacing; replace hover-only paths; touch feedback; thumb zones. Handle `pointercancel`, lost capture, blur, and extra pointers; arbitrate dragging against scrolling and restore a usable state after interruption.
|
|
56
56
|
|
|
57
57
|
Content: avoid heavy `display:none` downloads; progressive enhancement; lazy load; responsive images (`srcset`, `picture`).
|
|
58
58
|
|
|
@@ -64,7 +64,7 @@ Test real devices—DevTools emulation is incomplete.
|
|
|
64
64
|
|
|
65
65
|
## Verify
|
|
66
66
|
|
|
67
|
-
Real devices; orientations; major browsers; OS variants; touch/mouse/keyboard; 320px and 4K edges; throttled network.
|
|
67
|
+
Real devices; orientations; major browsers; OS variants; touch/mouse/keyboard; 320px and 4K edges; throttled network. Test interrupted drags and scroll recovery. Screenshots establish layout, not gesture behavior; record the actual device, OS, and browser engine for gesture tests.
|
|
68
68
|
|
|
69
69
|
Then `/ui-design polish`.
|
|
70
70
|
|
|
@@ -28,7 +28,7 @@ Reconform: platform nav, platform controls, touch-first, Dynamic Type/sp. Full p
|
|
|
28
28
|
|
|
29
29
|
## Verify
|
|
30
30
|
|
|
31
|
-
Drive structure from size classes—never device-model checks. Safe areas/insets in every configuration. Simulators for breadth; hardware for posture, gestures, performance.
|
|
31
|
+
Drive structure from size classes—never device-model checks. Safe areas/insets in every configuration. Simulators for breadth; hardware for posture, gestures, performance. Exercise gesture cancellation, focus loss, extra touches, scroll competition, and recovery. Screenshots prove layout only; name the actual device, OS, and native runtime tested (plus browser engine for embedded web gestures).
|
|
32
32
|
|
|
33
33
|
Then `/ui-design polish`.
|
|
34
34
|
|
|
@@ -7,8 +7,8 @@ Lock a single review target, execute **Track A** (judgment) and **Track B** (ins
|
|
|
7
7
|
- Track A and Track B are both required every run.
|
|
8
8
|
- Exposed Task/subagent tooling → launch A and B as **parallel isolated workers** (see exo-designer briefs below). Parent-thread execution is **degraded** unless no subagent tool exists (or the user declined where the harness asks)—banner: `⚠️ DEGRADED: single-context (<reason>)`.
|
|
9
9
|
- **Track A must finish before Track B influences synthesis.** Instrumentation output still anchors the merge even though it is deterministic.
|
|
10
|
-
- Omitting B fails the command unless the detect CLI is absent or crashed after a genuine attempt.
|
|
11
|
-
- Viewable targets require browser inspection when automation exists.
|
|
10
|
+
- Omitting B fails the command unless the detect CLI is absent or crashed after a genuine attempt. For native-only targets, B inspects platform tooling and runtime behavior; mark the web detector and DOM overlay inapplicable without attempting them. Both tracks and the complete UX judgment remain required; web targets retain every detector/browser requirement.
|
|
11
|
+
- Viewable web targets require browser inspection when automation exists; native-only targets use available platform inspection.
|
|
12
12
|
- Critique-only servers: `--background`, recorded stop method, stop before final report unless the user keeps them.
|
|
13
13
|
- Claim in-page overlays only after successful script injection **and** an in-tab detector pass.
|
|
14
14
|
- **Questions are last** in the message—full report first; nothing after questions.
|
|
@@ -56,7 +56,7 @@ Spawn **exo-designer** with an isolated brief equivalent to:
|
|
|
56
56
|
|
|
57
57
|
Spawn **exo-designer** with an isolated brief equivalent to:
|
|
58
58
|
|
|
59
|
-
**CLI scan:**
|
|
59
|
+
**CLI scan (web targets; native-only targets use applicable platform/runtime evidence):**
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
62
|
<skill-dir>/scripts/ui-design detect --json [markup-targets...]
|
|
@@ -68,7 +68,7 @@ Spawn **exo-designer** with an isolated brief equivalent to:
|
|
|
68
68
|
- Process exit code 0 = clean; 2 = findings present.
|
|
69
69
|
- Missing detector → report unavailable; continue manual/browser review.
|
|
70
70
|
|
|
71
|
-
**Browser overlay (required for viewable targets when automation exists):**
|
|
71
|
+
**Browser overlay (required for viewable web targets when automation exists; native-only targets use platform inspection):**
|
|
72
72
|
|
|
73
73
|
1. Fresh tab; prefer harness browser tools over custom Playwright.
|
|
74
74
|
2. Preflight **mutable** injection (`document.title`, append `<script>`). Read-only evaluate does not count.
|
|
@@ -337,8 +337,8 @@ Renormalize totals when 7 and/or 10 are `n/a`; never display `/40` on a partial
|
|
|
337
337
|
|
|
338
338
|
B must return structured bundles the parent can merge without rerun:
|
|
339
339
|
|
|
340
|
-
1. CLI: exit code, finding count, top rules with paths (or “skipped: URL-only”)
|
|
341
|
-
2. Browser: injection yes/no, port used, console excerpt or “none
|
|
340
|
+
1. CLI: exit code, finding count, top rules with paths (or “skipped: URL-only” / “inapplicable: native-only” with platform evidence)
|
|
341
|
+
2. Browser: injection yes/no, port used, console excerpt or “none”; native-only targets report platform inspection and mark the DOM overlay inapplicable
|
|
342
342
|
3. False positives: list with one-line rebuttal each
|
|
343
343
|
4. Skips: step name + reason (mutation blocked, tool missing, user URL only)
|
|
344
344
|
|
|
@@ -61,14 +61,14 @@ Responsive type: `clamp()`; 16px body floor on mobile (14px only for truly secon
|
|
|
61
61
|
|
|
62
62
|
RTL: logical properties (`margin-inline-start`, `padding-inline`, `border-inline-end`) or `[dir="rtl"]` transforms.
|
|
63
63
|
|
|
64
|
-
UTF-8 everywhere; test CJK and emoji.
|
|
64
|
+
UTF-8 everywhere; test CJK and emoji. Preserve canonical stored text; derive search-normalized values separately. Do not strip meaningful diacritics, joiners, or other script characters to simplify matching.
|
|
65
65
|
|
|
66
66
|
```javascript
|
|
67
67
|
new Intl.DateTimeFormat('de-DE').format(date);
|
|
68
68
|
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n);
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Pluralization via i18n library, not English-only string hacks.
|
|
71
|
+
Use the active locale for dates, numbers, currency, and pluralization; the examples above illustrate APIs, not fixed locales for every user. Pluralization via i18n library, not English-only string hacks.
|
|
72
72
|
|
|
73
73
|
### Errors
|
|
74
74
|
|
|
@@ -82,6 +82,8 @@ Graceful degradation: core path without JS where possible; alt text; progressive
|
|
|
82
82
|
|
|
83
83
|
### Edge cases
|
|
84
84
|
|
|
85
|
+
Gestures: handle `pointercancel`, lost pointer capture, and window blur; define extra-pointer behavior. Resolve scroll conflicts without trapping the page; clear drag state and recover to a usable position after interruption.
|
|
86
|
+
|
|
85
87
|
Empty, loading (named operation, estimates), large data (pagination/virtual scroll), double-submit guards, race handling, optimistic rollback, permission states with explanation, browser feature detection + polyfills.
|
|
86
88
|
|
|
87
89
|
### Validation
|
|
@@ -109,7 +111,7 @@ const throttledScroll = throttle(handleScroll, 100);
|
|
|
109
111
|
|
|
110
112
|
## Testing
|
|
111
113
|
|
|
112
|
-
Manual: extremes, locales, offline, 3G throttle, screen reader, keyboard-only, old browsers.
|
|
114
|
+
Manual: extremes, locales, offline, 3G throttle, screen reader, keyboard-only, old browsers. Screenshots prove layout only; gesture evidence names the actual device, OS, and browser engine tested, including interruption and recovery.
|
|
113
115
|
|
|
114
116
|
Automated: unit edge cases, integration errors, E2E critical paths, visual regression, axe/WAVE.
|
|
115
117
|
|
|
@@ -61,7 +61,7 @@ Manual `<skill-dir>/scripts/ui-design detect` honors project ignores unless `--n
|
|
|
61
61
|
<skill-dir>/scripts/ui-design hooks <action> [args...]
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Relay stdout verbatim. After `off`: note hook stays off until `on`. After `on`:
|
|
64
|
+
Relay stdout verbatim. After `off`: note hook stays off until `on`. After `on`: report configuration enabled; verify host loading and any required approval before claiming the next edit will run the hook. An enabled setting, manifest, or CLI consent record alone does not prove the host loaded or approved it; report unavailable host evidence explicitly. Keep the existing event timing.
|
|
65
65
|
|
|
66
66
|
## Triage
|
|
67
67
|
|
|
@@ -58,7 +58,7 @@ Before implementation, commit to a deterministic layout pick (seed from brief le
|
|
|
58
58
|
|
|
59
59
|
## Motion rules
|
|
60
60
|
|
|
61
|
-
If `MOTION_INTENSITY > 4`, ship visible motion (hero enter, section reveals, CTA hover)—or lower the dial and stay static. Every animation needs a one-sentence purpose (hierarchy, story, feedback, state). Max one horizontal marquee per page. GSAP pin/scrub: `start: "top top"`, cleanup in `useEffect`. Honor `prefers-reduced-motion` for intensity >3. No mixing GSAP/Three with Motion in one component tree. Spring default when using Motion: `stiffness: 100`, `damping: 20`. Grain/noise only on fixed `pointer-events-none` overlays.
|
|
61
|
+
If `MOTION_INTENSITY > 4`, ship visible motion (hero enter, section reveals, CTA hover)—or lower the dial and stay static. Every animation needs a one-sentence purpose (hierarchy, story, feedback, state). Max one horizontal marquee per page. GSAP pin/scrub: `start: "top top"`, cleanup in `useEffect`. Honor `prefers-reduced-motion` for intensity >3. Keep a complete static fallback when motion is reduced, unsupported, or interrupted; content and controls must remain available. No mixing GSAP/Three with Motion in one component tree. Spring default when using Motion: `stiffness: 100`, `damping: 20`. Grain/noise only on fixed `pointer-events-none` overlays.
|
|
62
62
|
|
|
63
63
|
## States & forms
|
|
64
64
|
|
|
@@ -66,7 +66,7 @@ Ship loading (layout-matched skeletons), empty, error, hover, active (`scale-[0.
|
|
|
66
66
|
|
|
67
67
|
## Assets
|
|
68
68
|
|
|
69
|
-
Landings are visual: prefer project image tools, then seeded placeholder photography (`picsum.photos/seed/{topic}/{w}/{h}`), never div-built fake dashboards. Logo walls use real SVG marks (e.g. simple-icons CDN) or invented monogram SVGs—not plain text faux logos; no category subtitles under logos.
|
|
69
|
+
Landings are visual: prefer project image tools, then seeded placeholder photography (`picsum.photos/seed/{topic}/{w}/{h}`), never div-built fake dashboards. Logo walls use real SVG marks (e.g. simple-icons CDN) or invented monogram SVGs—not plain text faux logos; no category subtitles under logos. Choose assets that serve the brief; static or type-led work does not require an asset kit. Verify real assets render at every delivered viewport width. Reserve image dimensions or aspect ratios, use responsive sources, and set loading priority for actual visibility rather than lazy-loading the first-view focal image.
|
|
70
70
|
|
|
71
71
|
## Content bans
|
|
72
72
|
|
|
@@ -78,7 +78,7 @@ Neon outer glows, gradient-filled giant headlines, custom cursors, three equal f
|
|
|
78
78
|
|
|
79
79
|
## Performance & a11y
|
|
80
80
|
|
|
81
|
-
Animate transform/opacity only. Dark mode designed up front with paired tokens. Target LCP <2.5s, INP <200ms, CLS <0.1. Document z-index layers; do not spam arbitrary high z-index. Cards only when elevation means hierarchy; tint shadows to background hue; one radius system per page.
|
|
81
|
+
Animate transform/opacity only. Dark mode designed up front with paired tokens. Target LCP <2.5s, INP <200ms, CLS <0.1. Label lab measurements with their device/network conditions; use available field data for real-user conclusions and disclose when it is absent. Document z-index layers; do not spam arbitrary high z-index. Cards only when elevation means hierarchy; tint shadows to background hue; one radius system per page.
|
|
82
82
|
|
|
83
83
|
## Internal plan (before UI code)
|
|
84
84
|
|
|
@@ -90,6 +90,7 @@ Document briefly: chosen dials and why; AIDA sections present; hero width/line m
|
|
|
90
90
|
- Zero em-dashes; single page theme; locked accent and radius system
|
|
91
91
|
- Hero viewport, nav, eyebrow, zigzag, CTA, and form contrast rules pass
|
|
92
92
|
- Bento/images/logos/marquee/motion/GSAP rules pass; no scroll listeners in state
|
|
93
|
-
- Mobile collapse,
|
|
93
|
+
- Mobile collapse, content-sized sections, cleanup on effects, empty/load/error states; use viewport minimum height only where the composition needs it
|
|
94
|
+
- Assets visible at delivered widths; reserved image dimensions and loading priorities checked; static fallback usable; lab and field evidence identified
|
|
94
95
|
- Content and Section 9-style tells absent; dependencies verified; one icon family
|
|
95
96
|
- Dashboards/tools: stop—use Operate mode and official dense UI systems instead
|
|
@@ -40,6 +40,7 @@ Fewest roles/families for unmistakable hierarchy. Combine size, weight, space, t
|
|
|
40
40
|
|
|
41
41
|
- Body readable and zoomable; 1rem/16px web floor unless dense role, platform norm, or user setting says otherwise.
|
|
42
42
|
- Prose 45–75ch; wider lines → more leading.
|
|
43
|
+
- Progressively enhance web headings with `text-wrap: balance` and body copy with `text-wrap: pretty` where supported; keep readable fallback wrapping and preserve intentional line breaks. Verify at delivered widths and languages.
|
|
43
44
|
- Light-on-dark: slightly more leading, tracking, and weight when the face needs it.
|
|
44
45
|
- Tune leading to face, width, language, contrast—not one global ratio.
|
|
45
46
|
- Repeat roles identically across states.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
version 0.1.5
|
|
2
|
+
0d48b6e16aa97664fdbe607d5da9ae320e389a1843e0ff1328f8c2530530320d impeccable-darwin-arm64
|
|
3
|
+
153971b0c00719dff9ebac13370cb7dbbbe12838b696fbaf3f7ab0b1b3726b52 impeccable-darwin-x64
|
|
4
|
+
bc4fa28bc8ccbb018795a99a4ece95ada898a6f1885f14281389d0e6ef2a2f98 impeccable-linux-arm64
|
|
5
|
+
cf5231a4b1ae66996c85b033800b1dad0797e590eae2f21ef2579430af187f19 impeccable-linux-x64
|
|
6
|
+
477e544fc8880a5e82e427490cb9a5f5adab480c0e02966d33fd50772b531c71 impeccable-windows-x64.exe
|
|
@@ -1,206 +1,96 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# Impeccable launcher. Runs the platform binary shipped next to this script:
|
|
3
|
-
# <this dir>/bin/<os>-<arch>/impeccable
|
|
4
|
-
# Order: $IMPECCABLE_BIN, the sibling binary, ~/.impeccable/bin/impeccable,
|
|
5
|
-
# the version-pinned cache, then `impeccable` on PATH. Never needs Node.
|
|
6
|
-
# The unversioned home binary and the PATH candidate are validated with the
|
|
7
|
-
# engine-probe handshake first: the retired 3.x npm CLI also installed a bin
|
|
8
|
-
# named `impeccable`, and exec'ing it would fail every verb with
|
|
9
|
-
# "Unknown command". Trusted candidates (IMPECCABLE_BIN, the sibling binary,
|
|
10
|
-
# the version-pinned cache) are exec'd without a probe: hooks run them on
|
|
11
|
-
# every edit and must stay fast.
|
|
12
2
|
set -eu
|
|
13
3
|
|
|
14
|
-
# True when the candidate answers the engine handshake (prints
|
|
15
|
-
# "impeccable-engine <version>", exit 0). Quiet and fast (<100ms).
|
|
16
|
-
# IMPECCABLE_LAUNCHER_PROBE marks the child as a probe: a copy of this
|
|
17
|
-
# launcher reached recursively (e.g. symlinked onto PATH as `impeccable`)
|
|
18
|
-
# then skips its own probes and refuses to download, so probing stays cheap
|
|
19
|
-
# and can never loop.
|
|
20
|
-
probe_ok() {
|
|
21
|
-
case "$(IMPECCABLE_LAUNCHER_PROBE=1 "$1" engine-probe 2>/dev/null || true)" in
|
|
22
|
-
impeccable-engine*) return 0 ;;
|
|
23
|
-
esac
|
|
24
|
-
return 1
|
|
25
|
-
}
|
|
26
|
-
probing=${IMPECCABLE_LAUNCHER_PROBE:-}
|
|
27
|
-
|
|
28
4
|
dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
29
|
-
|
|
30
|
-
# What the binary needs to know about its home: the skill directory (for
|
|
31
|
-
# reference/*.md and command-metadata.json) and how to name itself in the
|
|
32
|
-
# commands it prints. Exported BEFORE any exec below, including the
|
|
33
|
-
# IMPECCABLE_BIN override: an engine binary reached with no IMPECCABLE_SKILL_DIR
|
|
34
|
-
# cannot find reference/*.md (so native platform refs never inline) or read its
|
|
35
|
-
# own version (so UPDATE_AVAILABLE never fires). Setting it here covers every
|
|
36
|
-
# candidate the launcher can exec.
|
|
37
5
|
: "${IMPECCABLE_SKILL_DIR:=$(CDPATH= cd -- "$dir/.." && pwd)}"
|
|
38
6
|
: "${IMPECCABLE_SELF:=$0}"
|
|
39
7
|
export IMPECCABLE_SKILL_DIR IMPECCABLE_SELF
|
|
40
8
|
|
|
41
|
-
|
|
42
|
-
|
|
9
|
+
refuse() {
|
|
10
|
+
echo "impeccable: engine integrity: $*; reinstall exonous or set IMPECCABLE_BIN to an explicitly trusted engine." >&2
|
|
11
|
+
exit 127
|
|
12
|
+
}
|
|
13
|
+
if [ "${IMPECCABLE_BIN+x}" = x ]; then
|
|
14
|
+
[ -f "$IMPECCABLE_BIN" ] && [ -x "$IMPECCABLE_BIN" ] || refuse 'invalid IMPECCABLE_BIN override'
|
|
15
|
+
exec "$IMPECCABLE_BIN" "$@"
|
|
43
16
|
fi
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
17
|
+
version=$(awk '
|
|
18
|
+
{ sub(/\r$/, ""); if (NR != 1 || $0 != "0.1.5") bad = 1 }
|
|
19
|
+
END { if (NR != 1 || bad) exit 1; print "0.1.5" }
|
|
20
|
+
' "$dir/VERSION" 2>/dev/null) || refuse 'VERSION missing or invalid'
|
|
21
|
+
[ "$version" = 0.1.5 ] || refuse 'VERSION mismatch'
|
|
22
|
+
manifest="$dir/ENGINE-CHECKSUMS"
|
|
23
|
+
[ -f "$manifest" ] || refuse 'manifest missing'
|
|
24
|
+
awk '
|
|
25
|
+
{ sub(/\r$/, "") }
|
|
26
|
+
NR == 1 { if ($0 != "version 0.1.5") exit 1; next }
|
|
27
|
+
{ if (NF != 2 || length($1) != 64 || $1 ~ /[^a-f0-9]/ || $0 != $1 " " $2 || seen[$2]++) exit 1
|
|
28
|
+
if ($2 !~ /^impeccable-(darwin|linux)-(arm64|x64)$/ && $2 != "impeccable-windows-x64.exe") exit 1 }
|
|
29
|
+
END { if (NR < 2) exit 1 }
|
|
30
|
+
' "$manifest" || refuse 'invalid manifest'
|
|
31
|
+
case "$(uname -s)" in
|
|
32
|
+
Darwin) os=darwin;; Linux) os=linux;; MINGW*|MSYS*|CYGWIN*|Windows_NT) os=windows;; *) refuse 'unsupported platform';;
|
|
50
33
|
esac
|
|
51
|
-
case "$(uname -m
|
|
52
|
-
arm64|aarch64) arch=arm64 ;;
|
|
53
|
-
x86_64|amd64) arch=x64 ;;
|
|
54
|
-
*) arch=unknown ;;
|
|
34
|
+
case "$(uname -m)" in
|
|
35
|
+
arm64|aarch64) arch=arm64;; x86_64|amd64) arch=x64;; *) refuse 'unsupported architecture';;
|
|
55
36
|
esac
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# On Windows (an MSYS/Git Bash shell) the cached names carry .exe so this
|
|
68
|
-
# launcher and impeccable.cmd share one cache.
|
|
69
|
-
exe=""
|
|
70
|
-
[ "$os" = windows ] && exe=".exe"
|
|
71
|
-
home_bin="${HOME:-/nonexistent}/.impeccable/bin/impeccable$exe"
|
|
72
|
-
if [ -z "$probing" ] && [ -x "$home_bin" ] && probe_ok "$home_bin"; then
|
|
73
|
-
exec "$home_bin" "$@"
|
|
74
|
-
fi
|
|
75
|
-
# Version-pinned user cache, filled by the download below or by `impeccable update`.
|
|
76
|
-
version=""
|
|
77
|
-
[ -f "$dir/VERSION" ] && version=$(tr -d '[:space:]' < "$dir/VERSION")
|
|
78
|
-
cache_root="${IMPECCABLE_HOME:-${HOME:-/nonexistent}/.impeccable}"
|
|
79
|
-
cached="$cache_root/bin/$version/impeccable$exe"
|
|
80
|
-
if [ -n "$version" ] && [ -x "$cached" ]; then
|
|
81
|
-
exec "$cached" "$@"
|
|
82
|
-
fi
|
|
83
|
-
if [ -z "$probing" ] && command -v impeccable >/dev/null 2>&1 && probe_ok impeccable; then
|
|
84
|
-
exec impeccable "$@"
|
|
37
|
+
exe=
|
|
38
|
+
if [ "$os" = windows ]; then arch=x64; exe=.exe; fi
|
|
39
|
+
asset="impeccable-$os-$arch$exe"
|
|
40
|
+
expected=$(awk -v asset="$asset" '{sub(/\r$/, ""); if ($2 == asset) print $1}' "$manifest")
|
|
41
|
+
[ -n "$expected" ] || refuse "manifest missing hash for $asset"
|
|
42
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
43
|
+
hash_tool=sha256sum
|
|
44
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
45
|
+
hash_tool=shasum
|
|
46
|
+
else
|
|
47
|
+
refuse 'SHA256 tool unavailable'
|
|
85
48
|
fi
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
setup_help() {
|
|
91
|
-
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
|
|
92
|
-
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
|
|
93
|
-
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
|
|
94
|
-
}
|
|
95
|
-
fetch_url() {
|
|
96
|
-
if command -v curl >/dev/null 2>&1; then
|
|
97
|
-
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
|
|
98
|
-
elif command -v wget >/dev/null 2>&1; then
|
|
99
|
-
wget -q -O "$tmp" "$1" 2>/dev/null
|
|
49
|
+
verify() {
|
|
50
|
+
[ -f "$1" ] || refuse "not a regular file: $1"
|
|
51
|
+
if [ "$hash_tool" = shasum ]; then
|
|
52
|
+
digest=$(shasum -a 256 "$1") || refuse 'SHA256 failed'
|
|
100
53
|
else
|
|
101
|
-
|
|
54
|
+
digest=$(sha256sum "$1") || refuse 'SHA256 failed'
|
|
102
55
|
fi
|
|
56
|
+
actual=${digest%% *}
|
|
57
|
+
[ "$actual" = "$expected" ] || refuse "checksum mismatch: $1"
|
|
103
58
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
fi
|
|
111
|
-
if [ ! -s "$download_file" ]; then
|
|
112
|
-
rm -f "$download_file" "$tmp.sha256"
|
|
113
|
-
echo "impeccable: downloaded file is empty: $url; refusing the unverified download" >&2
|
|
114
|
-
exit 127
|
|
59
|
+
run_verified() {
|
|
60
|
+
verify "$1"
|
|
61
|
+
candidate=$1
|
|
62
|
+
shift
|
|
63
|
+
if [ ! -x "$candidate" ]; then
|
|
64
|
+
chmod +x "$candidate" 2>/dev/null || refuse "cannot make engine executable: $candidate"
|
|
115
65
|
fi
|
|
66
|
+
exec "$candidate" "$@"
|
|
116
67
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
fetched=0
|
|
140
|
-
if fetch_url "$url"; then
|
|
141
|
-
fetched=1
|
|
142
|
-
elif [ "$os" = windows ] && [ "$arch" = arm64 ]; then
|
|
143
|
-
# Windows on ARM runs x64 binaries; fall back when no arm64 asset exists.
|
|
144
|
-
url="$base/engine-v$version/impeccable-windows-x64.exe"
|
|
145
|
-
fetch_url "$url" && fetched=1
|
|
146
|
-
fi
|
|
147
|
-
if [ "$fetched" = 1 ]; then
|
|
148
|
-
check_download
|
|
149
|
-
# Fail closed: a freshly downloaded binary runs only after verifying
|
|
150
|
-
# against its .sha256 sidecar. A sidecar that cannot be fetched, or a
|
|
151
|
-
# machine with no sha256 tool, refuses the download instead of exec'ing
|
|
152
|
-
# an unverified binary. (A binary already on PATH or in the cache that
|
|
153
|
-
# passes engine-probe is unaffected.)
|
|
154
|
-
sidecar_ok=0
|
|
155
|
-
if command -v curl >/dev/null 2>&1; then
|
|
156
|
-
curl -fsSL --retry 2 -o "$tmp.sha256" "$url.sha256" 2>/dev/null && sidecar_ok=1
|
|
157
|
-
elif command -v wget >/dev/null 2>&1; then
|
|
158
|
-
wget -q -O "$tmp.sha256" "$url.sha256" 2>/dev/null && sidecar_ok=1
|
|
159
|
-
fi
|
|
160
|
-
check_download
|
|
161
|
-
expected=""
|
|
162
|
-
[ "$sidecar_ok" = 1 ] && expected=$(cut -d' ' -f1 < "$tmp.sha256")
|
|
163
|
-
actual=""
|
|
164
|
-
if command -v shasum >/dev/null 2>&1; then
|
|
165
|
-
if digest=$(shasum -a 256 "$tmp" 2>/dev/null); then actual=${digest%% *}; fi
|
|
166
|
-
elif command -v sha256sum >/dev/null 2>&1; then
|
|
167
|
-
if digest=$(sha256sum "$tmp" 2>/dev/null); then actual=${digest%% *}; fi
|
|
168
|
-
fi
|
|
169
|
-
check_download
|
|
170
|
-
rm -f "$tmp.sha256"
|
|
171
|
-
if [ -z "$expected" ] || [ -z "$actual" ]; then
|
|
172
|
-
rm -f "$tmp"
|
|
173
|
-
echo "impeccable: cannot verify $url against $url.sha256 (sidecar unavailable or hashing failed); refusing the unverified download" >&2
|
|
174
|
-
exit 127
|
|
175
|
-
fi
|
|
176
|
-
if [ "$actual" != "$expected" ]; then
|
|
177
|
-
rm -f "$tmp"
|
|
178
|
-
echo "impeccable: checksum mismatch downloading $url" >&2
|
|
179
|
-
exit 127
|
|
180
|
-
fi
|
|
181
|
-
check_download
|
|
182
|
-
if ! chmod +x "$tmp" 2>/dev/null; then
|
|
183
|
-
check_download
|
|
184
|
-
rm -f "$tmp"
|
|
185
|
-
echo "impeccable: could not make the verified download executable: $url" >&2
|
|
186
|
-
exit 127
|
|
187
|
-
fi
|
|
188
|
-
check_download
|
|
189
|
-
if ! mv -f "$tmp" "$cached" 2>/dev/null; then
|
|
190
|
-
check_download
|
|
191
|
-
rm -f "$tmp"
|
|
192
|
-
echo "impeccable: could not cache the verified download: $url" >&2
|
|
193
|
-
exit 127
|
|
194
|
-
fi
|
|
195
|
-
check_download "$cached"
|
|
196
|
-
exec "$cached" "$@"
|
|
197
|
-
fi
|
|
198
|
-
rm -f "$tmp" 2>/dev/null
|
|
199
|
-
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
|
|
200
|
-
setup_help
|
|
201
|
-
exit 127
|
|
68
|
+
cache_root="${IMPECCABLE_HOME:-${HOME:-/nonexistent}/.impeccable}"
|
|
69
|
+
cached="$cache_root/bin/$version/impeccable$exe"
|
|
70
|
+
sibling="$dir/bin/$os-$arch/impeccable$exe"
|
|
71
|
+
home_bin="${HOME:-/nonexistent}/.impeccable/bin/impeccable$exe"
|
|
72
|
+
for candidate in "$sibling" "$home_bin" "$cached"; do
|
|
73
|
+
if [ -e "$candidate" ]; then run_verified "$candidate" "$@"; fi
|
|
74
|
+
done
|
|
75
|
+
candidate=$(command -v "impeccable$exe" || true)
|
|
76
|
+
if [ -n "$candidate" ]; then run_verified "$candidate" "$@"; fi
|
|
77
|
+
[ -z "${IMPECCABLE_LAUNCHER_PROBE:-}" ] || refuse 'no authenticated engine installed'
|
|
78
|
+
mkdir -p "$cache_root/bin/$version" 2>/dev/null || refuse "cannot create cache: $cache_root"
|
|
79
|
+
staging=$(umask 077; mktemp -d "$cache_root/bin/$version/.impeccable.XXXXXXXX") || refuse 'cannot create private staging directory'
|
|
80
|
+
trap 'rm -f "$staging/engine"; rmdir "$staging" 2>/dev/null || true' EXIT
|
|
81
|
+
trap 'exit 127' HUP INT TERM
|
|
82
|
+
tmp="$staging/engine"
|
|
83
|
+
url="${IMPECCABLE_DOWNLOAD_BASE:-https://github.com/pbakaus/impeccable/releases/download}/engine-v$version/$asset"
|
|
84
|
+
if command -v curl >/dev/null 2>&1; then
|
|
85
|
+
curl -fsSL --retry 2 -o "$tmp" "$url" || refuse "could not download $url; check network access and cache permissions"
|
|
86
|
+
elif command -v wget >/dev/null 2>&1; then
|
|
87
|
+
wget -q -O "$tmp" "$url" || refuse "could not download $url; check network access and cache permissions"
|
|
88
|
+
else
|
|
89
|
+
refuse 'curl or wget unavailable'
|
|
202
90
|
fi
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
91
|
+
verify "$tmp"
|
|
92
|
+
chmod +x "$tmp" || refuse 'cannot make verified download executable'
|
|
93
|
+
mv -f "$tmp" "$cached" || refuse 'cannot publish verified download'
|
|
94
|
+
rmdir "$staging" || refuse 'cannot remove staging directory'
|
|
95
|
+
trap - EXIT HUP INT TERM
|
|
96
|
+
run_verified "$cached" "$@"
|
|
@@ -1,214 +1,4 @@
|
|
|
1
1
|
@echo off
|
|
2
2
|
setlocal
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
rem
|
|
6
|
-
rem Structure notes (this file is exercised by dry parsing and string-level
|
|
7
|
-
rem tests, not yet on a real Windows machine):
|
|
8
|
-
rem - No multi-line parenthesized blocks: cmd expands %var% at block parse
|
|
9
|
-
rem time, which made the old download path read back empty %url%/%cached%.
|
|
10
|
-
rem Linear goto flow keeps every expansion on its own line, and avoids
|
|
11
|
-
rem delayed expansion eating ! characters in user arguments.
|
|
12
|
-
rem - The unversioned user binary and the PATH candidate are validated with
|
|
13
|
-
rem the engine-probe handshake (see :probe) so the retired 3.x npm CLI,
|
|
14
|
-
rem whose bin is also named impeccable, is never exec'd. IMPECCABLE_BIN,
|
|
15
|
-
rem the sibling binary, and the version-pinned cache stay trusted.
|
|
16
|
-
rem - Downloads are verified against the .sha256 sidecar via certutil and
|
|
17
|
-
rem fail closed: a missing sidecar or hash tool refuses the download. On
|
|
18
|
-
rem ARM64 the arm64 asset is tried first and the x64 asset is the
|
|
19
|
-
rem fallback (Windows on ARM runs x64 binaries).
|
|
20
|
-
if not defined IMPECCABLE_SKILL_DIR set "IMPECCABLE_SKILL_DIR=%~dp0.."
|
|
21
|
-
if not defined IMPECCABLE_SELF set "IMPECCABLE_SELF=%~f0"
|
|
22
|
-
set "arch=x64"
|
|
23
|
-
if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" set "arch=arm64"
|
|
24
|
-
|
|
25
|
-
if not defined IMPECCABLE_BIN goto no_env_bin
|
|
26
|
-
if not exist "%IMPECCABLE_BIN%" goto no_env_bin
|
|
27
|
-
set "run=%IMPECCABLE_BIN%"
|
|
28
|
-
goto run
|
|
29
|
-
:no_env_bin
|
|
30
|
-
|
|
31
|
-
set "bin=%~dp0bin\windows-%arch%\impeccable.exe"
|
|
32
|
-
if not exist "%bin%" goto no_sibling
|
|
33
|
-
set "run=%bin%"
|
|
34
|
-
goto run
|
|
35
|
-
:no_sibling
|
|
36
|
-
|
|
37
|
-
set "home_bin=%USERPROFILE%\.impeccable\bin\impeccable.exe"
|
|
38
|
-
if not exist "%home_bin%" goto no_home_bin
|
|
39
|
-
if defined IMPECCABLE_LAUNCHER_PROBE goto no_home_bin
|
|
40
|
-
call :probe "%home_bin%"
|
|
41
|
-
if not "%probe_ok%"=="1" goto no_home_bin
|
|
42
|
-
set "run=%home_bin%"
|
|
43
|
-
goto run
|
|
44
|
-
:no_home_bin
|
|
45
|
-
|
|
46
|
-
set "version="
|
|
47
|
-
if exist "%~dp0VERSION" set /p version=<"%~dp0VERSION"
|
|
48
|
-
if not defined IMPECCABLE_HOME set "IMPECCABLE_HOME=%USERPROFILE%\.impeccable"
|
|
49
|
-
set "cached=%IMPECCABLE_HOME%\bin\%version%\impeccable.exe"
|
|
50
|
-
if not defined version goto no_cache
|
|
51
|
-
if not exist "%cached%" goto no_cache
|
|
52
|
-
set "run=%cached%"
|
|
53
|
-
goto run
|
|
54
|
-
:no_cache
|
|
55
|
-
|
|
56
|
-
if defined IMPECCABLE_LAUNCHER_PROBE goto download
|
|
57
|
-
where impeccable >nul 2>nul
|
|
58
|
-
if errorlevel 1 goto download
|
|
59
|
-
call :probe impeccable
|
|
60
|
-
if not "%probe_ok%"=="1" goto download
|
|
61
|
-
impeccable %*
|
|
62
|
-
exit /b
|
|
63
|
-
|
|
64
|
-
:download
|
|
65
|
-
rem Last resort: fetch this version's binary from the release channel into
|
|
66
|
-
rem the version-pinned user cache, verify it, then run it. Never inside
|
|
67
|
-
rem another launcher's probe: fail fast and quiet instead.
|
|
68
|
-
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
|
|
69
|
-
if not defined version goto fail
|
|
70
|
-
where curl.exe >nul 2>nul
|
|
71
|
-
if errorlevel 1 goto curl_missing
|
|
72
|
-
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
|
|
73
|
-
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
|
|
74
|
-
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
|
|
75
|
-
if errorlevel 1 goto cache_directory_failed
|
|
76
|
-
:cache_ready
|
|
77
|
-
rem Check the staging file too: an existing directory may be read-only.
|
|
78
|
-
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
|
|
79
|
-
rem branch on the command's failure directly. Never treat a directory as a
|
|
80
|
-
rem staging file (later del cleanup would prompt to delete its contents).
|
|
81
|
-
if exist "%cached%.part\" goto cache_write_failed
|
|
82
|
-
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
|
|
83
|
-
set "asset=impeccable-windows-%arch%.exe"
|
|
84
|
-
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
|
85
|
-
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
|
86
|
-
if not errorlevel 1 goto verify
|
|
87
|
-
if not "%arch%"=="arm64" goto download_failed
|
|
88
|
-
set "asset=impeccable-windows-x64.exe"
|
|
89
|
-
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
|
|
90
|
-
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
|
|
91
|
-
if errorlevel 1 goto download_failed
|
|
92
|
-
|
|
93
|
-
:verify
|
|
94
|
-
call :check_download
|
|
95
|
-
if errorlevel 1 exit /b 127
|
|
96
|
-
rem Mirrors the sh launcher and fails closed: a freshly downloaded binary
|
|
97
|
-
rem runs only after verifying against its .sha256 sidecar. A sidecar that
|
|
98
|
-
rem cannot be fetched, or an empty certutil result, refuses the download
|
|
99
|
-
rem instead of running an unverified binary.
|
|
100
|
-
curl.exe -fsSL -o "%cached%.sha256" "%url%.sha256" >nul 2>nul
|
|
101
|
-
if errorlevel 1 goto verify_refuse
|
|
102
|
-
set "expected="
|
|
103
|
-
set /p expected=<"%cached%.sha256"
|
|
104
|
-
for /f "tokens=1" %%h in ("%expected%") do set "expected=%%h"
|
|
105
|
-
call :check_download
|
|
106
|
-
if errorlevel 1 exit /b 127
|
|
107
|
-
set "actual="
|
|
108
|
-
rem Reuse the sidecar staging file after reading expected. Check certutil's
|
|
109
|
-
rem status before parsing: its error text on stdout is not a digest.
|
|
110
|
-
certutil -hashfile "%cached%.part" SHA256 >"%cached%.sha256" 2>nul
|
|
111
|
-
if errorlevel 1 goto verify_refuse
|
|
112
|
-
call :check_download
|
|
113
|
-
if errorlevel 1 exit /b 127
|
|
114
|
-
for /f "usebackq skip=1 delims=" %%h in ("%cached%.sha256") do if not defined actual set "actual=%%h"
|
|
115
|
-
del "%cached%.sha256" >nul 2>nul
|
|
116
|
-
if not defined expected goto verify_refuse
|
|
117
|
-
if not defined actual goto verify_refuse
|
|
118
|
-
set "actual=%actual: =%"
|
|
119
|
-
if /I "%actual%"=="%expected%" goto place
|
|
120
|
-
del "%cached%.part" >nul 2>nul
|
|
121
|
-
echo impeccable: checksum mismatch downloading %url% 1>&2
|
|
122
|
-
exit /b 127
|
|
123
|
-
|
|
124
|
-
:verify_refuse
|
|
125
|
-
call :check_download
|
|
126
|
-
if errorlevel 1 exit /b 127
|
|
127
|
-
del "%cached%.part" >nul 2>nul
|
|
128
|
-
del "%cached%.sha256" >nul 2>nul
|
|
129
|
-
echo impeccable: cannot verify %url% against %url%.sha256; refusing the unverified download 1>&2
|
|
130
|
-
exit /b 127
|
|
131
|
-
|
|
132
|
-
:check_download
|
|
133
|
-
set "download_file=%~1"
|
|
134
|
-
if not defined download_file set "download_file=%cached%.part"
|
|
135
|
-
if not exist "%download_file%" goto download_missing
|
|
136
|
-
for %%f in ("%download_file%") do if %%~zf==0 goto download_empty
|
|
137
|
-
exit /b 0
|
|
138
|
-
|
|
139
|
-
:download_missing
|
|
140
|
-
del "%cached%.sha256" >nul 2>nul
|
|
141
|
-
echo impeccable: download completed but the file was removed before execution: %url%; check your antivirus quarantine or logs. Refusing to continue; do not disable protection. 1>&2
|
|
142
|
-
exit /b 127
|
|
143
|
-
|
|
144
|
-
:download_empty
|
|
145
|
-
del "%download_file%" >nul 2>nul
|
|
146
|
-
del "%cached%.sha256" >nul 2>nul
|
|
147
|
-
echo impeccable: downloaded file is empty: %url%; refusing the unverified download 1>&2
|
|
148
|
-
exit /b 127
|
|
149
|
-
|
|
150
|
-
:place
|
|
151
|
-
call :check_download
|
|
152
|
-
if errorlevel 1 exit /b 127
|
|
153
|
-
move /y "%cached%.part" "%cached%" >nul 2>nul
|
|
154
|
-
if errorlevel 1 goto place_failed
|
|
155
|
-
call :check_download "%cached%"
|
|
156
|
-
if errorlevel 1 exit /b 127
|
|
157
|
-
set "run=%cached%"
|
|
158
|
-
goto run
|
|
159
|
-
|
|
160
|
-
:place_failed
|
|
161
|
-
call :check_download
|
|
162
|
-
if errorlevel 1 exit /b 127
|
|
163
|
-
del "%cached%.part" >nul 2>nul
|
|
164
|
-
echo impeccable: could not cache the verified download: %url% 1>&2
|
|
165
|
-
exit /b 127
|
|
166
|
-
|
|
167
|
-
:run
|
|
168
|
-
"%run%" %*
|
|
169
|
-
exit /b
|
|
170
|
-
|
|
171
|
-
:probe
|
|
172
|
-
rem Sets probe_ok=1 when %1 answers the engine handshake: prints
|
|
173
|
-
rem "impeccable-engine <version>" and exits 0. The 3.x npm CLI answers any
|
|
174
|
-
rem unknown verb with "Unknown command", exit 1, so it never passes.
|
|
175
|
-
set "probe_ok="
|
|
176
|
-
set "probe_tmp=%TEMP%\impeccable-probe-%RANDOM%%RANDOM%.txt"
|
|
177
|
-
set "IMPECCABLE_LAUNCHER_PROBE=1"
|
|
178
|
-
"%~1" engine-probe >"%probe_tmp%" 2>nul
|
|
179
|
-
set "probe_err=%ERRORLEVEL%"
|
|
180
|
-
set "IMPECCABLE_LAUNCHER_PROBE="
|
|
181
|
-
if not "%probe_err%"=="0" goto probe_done
|
|
182
|
-
findstr /b /c:"impeccable-engine" "%probe_tmp%" >nul 2>nul
|
|
183
|
-
if not errorlevel 1 set "probe_ok=1"
|
|
184
|
-
:probe_done
|
|
185
|
-
del "%probe_tmp%" >nul 2>nul
|
|
186
|
-
exit /b 0
|
|
187
|
-
|
|
188
|
-
:cache_directory_failed
|
|
189
|
-
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
|
190
|
-
goto setup_failed
|
|
191
|
-
|
|
192
|
-
:cache_write_failed
|
|
193
|
-
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
|
|
194
|
-
goto setup_failed
|
|
195
|
-
|
|
196
|
-
:curl_missing
|
|
197
|
-
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
|
|
198
|
-
goto setup_failed
|
|
199
|
-
|
|
200
|
-
:download_failed
|
|
201
|
-
del "%cached%.part" >nul 2>nul
|
|
202
|
-
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
|
|
203
|
-
|
|
204
|
-
:setup_failed
|
|
205
|
-
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
|
|
206
|
-
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
|
|
207
|
-
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
|
|
208
|
-
exit /b 127
|
|
209
|
-
|
|
210
|
-
:fail
|
|
211
|
-
del "%cached%.part" >nul 2>nul
|
|
212
|
-
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
|
|
213
|
-
echo Download impeccable-windows-%arch%.exe from https://github.com/pbakaus/impeccable/releases (tag engine-v%version%) and save it as %cached%, or set IMPECCABLE_BIN to a preinstalled engine binary. Docs: https://impeccable.style 1>&2
|
|
214
|
-
exit /b 127
|
|
3
|
+
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0impeccable.ps1" %*
|
|
4
|
+
exit /b %errorlevel%
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
$engineArgs = $args
|
|
3
|
+
$staging = $null
|
|
4
|
+
|
|
5
|
+
function Refuse([string] $message) {
|
|
6
|
+
throw "engine integrity: $message; reinstall exonous or set IMPECCABLE_BIN to an explicitly trusted engine."
|
|
7
|
+
}
|
|
8
|
+
function Verify([string] $file, [string] $expected) {
|
|
9
|
+
if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { Refuse "not a regular file: $file" }
|
|
10
|
+
$actual = (Get-FileHash -LiteralPath $file -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
11
|
+
if ($actual -cne $expected) { Refuse "checksum mismatch: $file" }
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
if (-not $env:IMPECCABLE_SKILL_DIR) { $env:IMPECCABLE_SKILL_DIR = Split-Path $PSScriptRoot -Parent }
|
|
15
|
+
if (-not $env:IMPECCABLE_SELF) { $env:IMPECCABLE_SELF = Join-Path $PSScriptRoot 'impeccable.cmd' }
|
|
16
|
+
if (Test-Path Env:IMPECCABLE_BIN) {
|
|
17
|
+
if (-not $env:IMPECCABLE_BIN -or -not (Test-Path -LiteralPath $env:IMPECCABLE_BIN -PathType Leaf)) { Refuse 'invalid IMPECCABLE_BIN override' }
|
|
18
|
+
& $env:IMPECCABLE_BIN @engineArgs
|
|
19
|
+
exit $LASTEXITCODE
|
|
20
|
+
}
|
|
21
|
+
$version = (Get-Content -LiteralPath (Join-Path $PSScriptRoot 'VERSION') -Raw).Replace("`r`n", "`n")
|
|
22
|
+
if ($version.EndsWith("`n")) { $version = $version.Substring(0, $version.Length - 1) }
|
|
23
|
+
if ($version -cne '0.1.5') { Refuse 'VERSION mismatch' }
|
|
24
|
+
$lines = @(Get-Content -LiteralPath (Join-Path $PSScriptRoot 'ENGINE-CHECKSUMS'))
|
|
25
|
+
if ($lines.Count -lt 2 -or $lines[0] -cne 'version 0.1.5') { Refuse 'manifest version missing or mismatched' }
|
|
26
|
+
$hashes = @{}
|
|
27
|
+
foreach ($line in $lines[1..($lines.Count - 1)]) {
|
|
28
|
+
if ($line -cnotmatch '^([a-f0-9]{64}) (impeccable-(?:darwin|linux)-(?:arm64|x64)|impeccable-windows-x64\.exe)$') { Refuse 'invalid manifest entry' }
|
|
29
|
+
$assetName = $Matches[2]
|
|
30
|
+
if ($hashes.ContainsKey($assetName)) { Refuse 'duplicate manifest asset' }
|
|
31
|
+
$hashes[$assetName] = $Matches[1]
|
|
32
|
+
}
|
|
33
|
+
$asset = 'impeccable-windows-x64.exe'
|
|
34
|
+
$expected = $hashes[$asset]
|
|
35
|
+
if (-not $expected) { Refuse 'manifest missing Windows x64 hash' }
|
|
36
|
+
$cacheRoot = $env:IMPECCABLE_HOME
|
|
37
|
+
if (-not $cacheRoot) { $cacheRoot = Join-Path $env:USERPROFILE '.impeccable' }
|
|
38
|
+
$cached = Join-Path $cacheRoot 'bin\0.1.5\impeccable.exe'
|
|
39
|
+
$candidates = @(
|
|
40
|
+
(Join-Path $PSScriptRoot 'bin\windows-x64\impeccable.exe'),
|
|
41
|
+
(Join-Path $env:USERPROFILE '.impeccable\bin\impeccable.exe'),
|
|
42
|
+
$cached
|
|
43
|
+
)
|
|
44
|
+
foreach ($entry in ($env:PATH -split ';')) {
|
|
45
|
+
if ($entry) { $candidates += Join-Path $entry 'impeccable.exe' }
|
|
46
|
+
}
|
|
47
|
+
foreach ($candidate in $candidates) {
|
|
48
|
+
if (-not (Test-Path -LiteralPath $candidate)) { continue }
|
|
49
|
+
Verify $candidate $expected
|
|
50
|
+
& $candidate @engineArgs
|
|
51
|
+
exit $LASTEXITCODE
|
|
52
|
+
}
|
|
53
|
+
if ($env:IMPECCABLE_LAUNCHER_PROBE) { Refuse 'no authenticated engine installed' }
|
|
54
|
+
$cacheDir = Split-Path $cached -Parent
|
|
55
|
+
[IO.Directory]::CreateDirectory($cacheDir) | Out-Null
|
|
56
|
+
$staging = Join-Path $cacheDir ('.impeccable.' + [Guid]::NewGuid().ToString('N'))
|
|
57
|
+
$acl = New-Object System.Security.AccessControl.DirectorySecurity
|
|
58
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
59
|
+
$owner = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
60
|
+
$acl.SetOwner($owner)
|
|
61
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new($owner, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
|
|
62
|
+
$acl.AddAccessRule($rule)
|
|
63
|
+
[IO.Directory]::CreateDirectory($staging, $acl) | Out-Null
|
|
64
|
+
$tmp = Join-Path $staging 'engine'
|
|
65
|
+
$base = $env:IMPECCABLE_DOWNLOAD_BASE
|
|
66
|
+
if (-not $base) { $base = 'https://github.com/pbakaus/impeccable/releases/download' }
|
|
67
|
+
$url = "$base/engine-v$version/$asset"
|
|
68
|
+
& curl.exe -fsSL --retry 2 -o $tmp $url
|
|
69
|
+
if ($LASTEXITCODE -ne 0) { Refuse "could not download $url; check network access and cache permissions" }
|
|
70
|
+
Verify $tmp $expected
|
|
71
|
+
if ([IO.File]::Exists($cached)) {
|
|
72
|
+
[IO.File]::Replace($tmp, $cached, $null)
|
|
73
|
+
} else {
|
|
74
|
+
try { [IO.File]::Move($tmp, $cached) }
|
|
75
|
+
catch {
|
|
76
|
+
if (-not [IO.File]::Exists($cached)) { throw }
|
|
77
|
+
[IO.File]::Replace($tmp, $cached, $null)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
Verify $cached $expected
|
|
81
|
+
Remove-Item -LiteralPath $staging
|
|
82
|
+
$staging = $null
|
|
83
|
+
& $cached @engineArgs
|
|
84
|
+
exit $LASTEXITCODE
|
|
85
|
+
} catch {
|
|
86
|
+
[Console]::Error.WriteLine("impeccable: $($_.Exception.Message); reinstall exonous or set IMPECCABLE_BIN explicitly.")
|
|
87
|
+
exit 127
|
|
88
|
+
} finally {
|
|
89
|
+
if ($staging -and (Test-Path -LiteralPath $staging)) {
|
|
90
|
+
Remove-Item -LiteralPath (Join-Path $staging 'engine') -Force -ErrorAction SilentlyContinue
|
|
91
|
+
Remove-Item -LiteralPath $staging -Force -ErrorAction SilentlyContinue
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// ui-design — runs the bundled design engine with exonous defaults: no telemetry, no self-update, rebranded agent-facing output.
|
|
3
3
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
5
6
|
import os from 'node:os';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
@@ -46,18 +47,48 @@ export function cmdQuote(value) {
|
|
|
46
47
|
return `"${text.replace(/(\\+)$/, '$1$1')}"`;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
function integrityError(message) {
|
|
51
|
+
return Object.assign(new Error(`engine integrity: ${message}; reinstall exonous or set IMPECCABLE_BIN to an explicitly trusted engine`), { code: 127 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function engineManifest(dir) {
|
|
55
|
+
try {
|
|
56
|
+
const version = fs.readFileSync(path.join(dir, 'VERSION'), 'utf8').replace(/\r\n/g, '\n').replace(/\n$/, '');
|
|
57
|
+
const lines = fs.readFileSync(path.join(dir, 'ENGINE-CHECKSUMS'), 'utf8').replace(/\r\n/g, '\n').replace(/\n$/, '').split('\n');
|
|
58
|
+
if (version !== '0.1.5' || lines.shift() !== `version ${version}`) throw new Error('version mismatch');
|
|
59
|
+
const hashes = new Map();
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
const match = line.match(/^([a-f0-9]{64}) (impeccable-(?:darwin|linux)-(?:arm64|x64)|impeccable-windows-x64\.exe)$/);
|
|
62
|
+
if (!match || hashes.has(match[2])) throw new Error('invalid or duplicate asset');
|
|
63
|
+
hashes.set(match[2], match[1]);
|
|
64
|
+
}
|
|
65
|
+
if (!hashes.size) throw new Error('missing assets');
|
|
66
|
+
return hashes;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw integrityError(`invalid manifest: ${error.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
export function windowsEngine(dir, env = process.env) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
if (Object.hasOwn(env, 'IMPECCABLE_BIN')) {
|
|
74
|
+
if (env.IMPECCABLE_BIN && fs.statSync(env.IMPECCABLE_BIN, { throwIfNoEntry: false })?.isFile()) return env.IMPECCABLE_BIN;
|
|
75
|
+
throw integrityError('invalid IMPECCABLE_BIN override');
|
|
76
|
+
}
|
|
77
|
+
const expected = engineManifest(dir).get('impeccable-windows-x64.exe');
|
|
78
|
+
if (!expected) throw integrityError('manifest missing Windows x64 hash');
|
|
54
79
|
const cacheRoot = env.IMPECCABLE_HOME || (env.USERPROFILE && path.join(env.USERPROFILE, '.impeccable'));
|
|
55
80
|
const candidates = [
|
|
56
|
-
|
|
57
|
-
path.join(
|
|
58
|
-
|
|
81
|
+
path.join(dir, 'bin', 'windows-x64', 'impeccable.exe'),
|
|
82
|
+
env.USERPROFILE && path.join(env.USERPROFILE, '.impeccable', 'bin', 'impeccable.exe'),
|
|
83
|
+
cacheRoot && path.join(cacheRoot, 'bin', '0.1.5', 'impeccable.exe'),
|
|
84
|
+
...(env.PATH || '').split(path.delimiter).filter(Boolean).map((entry) => path.join(entry, 'impeccable.exe')),
|
|
59
85
|
];
|
|
60
|
-
|
|
86
|
+
for (const file of candidates.filter(Boolean)) {
|
|
87
|
+
if (!fs.existsSync(file)) continue;
|
|
88
|
+
if (!fs.statSync(file).isFile() || createHash('sha256').update(fs.readFileSync(file)).digest('hex') !== expected) throw integrityError(`checksum mismatch: ${file}`);
|
|
89
|
+
return file;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
61
92
|
}
|
|
62
93
|
|
|
63
94
|
export function engineCommand(dir, args, platform = process.platform, env = process.env) {
|
|
@@ -101,7 +132,7 @@ export function main(args = process.argv.slice(2), platform = process.platform,
|
|
|
101
132
|
try {
|
|
102
133
|
run = engineCommand(dir, args, platform);
|
|
103
134
|
} catch (error) {
|
|
104
|
-
fail(error.message, 2);
|
|
135
|
+
fail(error.message, error.code === 127 ? 127 : 2);
|
|
105
136
|
}
|
|
106
137
|
|
|
107
138
|
if (BUFFERED.has(verb)) {
|
|
@@ -79,13 +79,15 @@ High-frequency paths, keyboard actions, and data users must read should stay sti
|
|
|
79
79
|
|
|
80
80
|
### Performance
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
Prefer **transform** and **opacity** (plus sanctioned **clip-path** where recipes allow). Per-frame changes to width/height/margin/padding/top/left can trigger layout; distinguish those updates from library-managed layout transitions.
|
|
83
83
|
|
|
84
84
|
- CSS transitions retarget mid-flight; keyframes restart — use transitions for rapid toggles (toasts, stacks).
|
|
85
|
-
- Springs carry velocity through interruption — good for drags.
|
|
86
|
-
-
|
|
85
|
+
- Springs carry velocity through interruption — good for drags. Cancellation or removal must leave intended content visible, focus valid, and state settled without waiting for callbacks from animations that no longer run.
|
|
86
|
+
- Diagnose before optimizing: identify the installed library version, feature used, and target browser. Profile the interaction under representative load before replacing `x`/`y`/`scale` with a full `transform` string; syntax alone proves neither execution thread nor GPU acceleration.
|
|
87
87
|
- Do not drive child transforms via a CSS variable on a parent — set `transform` on the element being moved.
|
|
88
88
|
|
|
89
|
+
Avoid redundant entrances on both a container and its children. Sequencing must keep controls usable while motion runs.
|
|
90
|
+
|
|
89
91
|
### Physicality shortcuts
|
|
90
92
|
|
|
91
93
|
- Press: `scale(0.97)` on `:active`, ~160ms ease-out.
|
|
@@ -16,18 +16,20 @@ Specialized motion review only. Default **flag**; approval is earned.
|
|
|
16
16
|
4. **Sub-300ms UI** — unless justified (drawer/marketing tiers).
|
|
17
17
|
5. **Origin & physicality** — trigger-anchored origin; no `scale(0)`; modals centered OK.
|
|
18
18
|
6. **Interruptibility** — transitions/springs for rapid/gesture UI; not keyframes that restart.
|
|
19
|
-
7. **
|
|
19
|
+
7. **Measured performance** — prefer transform/opacity; distinguish per-frame layout updates from library-managed layout transitions. Record installed library version, feature, and browser; profile under load before recommending full `transform` over `x`/`y`/`scale`. Neither syntax guarantees GPU acceleration.
|
|
20
20
|
8. **Accessibility** — reduced motion (gentler, not zero); hover behind `(hover: hover) and (pointer: fine)`.
|
|
21
21
|
9. **Asymmetric timing** — slow deliberate user phase, fast system response.
|
|
22
22
|
10. **Cohesion** — motion matches product personality; blur(2px) seam optional on bad crossfades.
|
|
23
23
|
|
|
24
24
|
### Escalate immediately
|
|
25
25
|
|
|
26
|
-
`transition: all`; `scale(0)`; ease-in on UI; animation on keyboard/high-frequency; UI >300ms unjustified; center origin on anchored popover; keyframes on toasts/toggles;
|
|
26
|
+
`transition: all`; `scale(0)`; ease-in on UI; animation on keyboard/high-frequency; UI >300ms unjustified; center origin on anchored popover; keyframes on toasts/toggles; unmeasured per-frame layout updates; parent CSS var driving child transform; missing reduced-motion or hover gate; symmetric press/release on hold flows; redundant container/child entrances; sequencing that blocks input; cancellation that strands content, focus, or final state.
|
|
27
27
|
|
|
28
28
|
### Fix preference order
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Diagnose the cause before optimizing; validate performance findings under representative load.
|
|
31
|
+
|
|
32
|
+
1. Delete animation → 2. Reduce → 3. Fix easing → 4. Fix origin/physicality → 5. Interruptible mechanism → 6. Measured rendering improvement → 7. Asymmetric timing → 8. Polish (stagger, blur, `@starting-style`, spring) → 9. A11y & cohesion.
|
|
31
33
|
|
|
32
34
|
### Required output
|
|
33
35
|
|
|
@@ -38,7 +40,7 @@ Specialized motion review only. Default **flag**; approval is earned.
|
|
|
38
40
|
|
|
39
41
|
**Verdict tiers:** feel-breaking → simplifications → performance → interruptibility → origin/cohesion → a11y.
|
|
40
42
|
|
|
41
|
-
Close with **Block** (any feel-breaking, keyboard motion, scale(0), ease-in,
|
|
43
|
+
Close with **Block** (any feel-breaking, keyboard motion, scale(0), ease-in, confirmed performance defect left unresolved) or **Approve**.
|
|
42
44
|
|
|
43
45
|
Cite `file:line`. Values from reference tables below — never approximate.
|
|
44
46
|
|
|
@@ -72,11 +74,11 @@ No `scale(0)` — use 0.9–0.97 + opacity. Popovers: `transform-origin: var(--t
|
|
|
72
74
|
|
|
73
75
|
### 4. Interruptibility
|
|
74
76
|
|
|
75
|
-
Transitions vs keyframes; `@starting-style` or mount flag; springs for gestures `{ type: "spring", duration: 0.5, bounce: 0.2 }` bounce 0.1–0.3; velocity dismiss > ~0.11; rubber-band not wall; asymmetric hold/release.
|
|
77
|
+
Transitions vs keyframes; `@starting-style` or mount flag; springs for gestures `{ type: "spring", duration: 0.5, bounce: 0.2 }` bounce 0.1–0.3; velocity dismiss > ~0.11; rubber-band not wall; asymmetric hold/release. Cancel or remove motion without losing intended visible content, valid focus, or final state; no waiting for callbacks that removal eliminates.
|
|
76
78
|
|
|
77
79
|
### 5. Performance
|
|
78
80
|
|
|
79
|
-
transform/opacity; no `transition: all`; no parent `--var` driving child transform;
|
|
81
|
+
Prefer transform/opacity; no `transition: all`; no parent `--var` driving child transform; blur <20px. Check installed library version, feature, and browser; separate layout transitions from per-frame updates and profile under representative load before changing `x`/`y`/`scale` to full `transform`. Record evidence rather than assuming thread or GPU behavior.
|
|
80
82
|
|
|
81
83
|
### 6. Accessibility
|
|
82
84
|
|
|
@@ -84,7 +86,7 @@ Reduced motion + hover gating (snippets in `SKILL.md`).
|
|
|
84
86
|
|
|
85
87
|
### 7. Cohesion & tokens
|
|
86
88
|
|
|
87
|
-
Personality match; consolidate duplicate beziers; stagger 30–80ms; crossfade blur mask.
|
|
89
|
+
Personality match; consolidate duplicate beziers; decorative stagger 30–80ms without blocking interaction; crossfade blur mask. Avoid simultaneous redundant entrances on containers and their children.
|
|
88
90
|
|
|
89
91
|
### 8. Missed opportunities
|
|
90
92
|
|
|
@@ -27,7 +27,7 @@ Same frequency and purpose tables as `SKILL.md`. Keyboard-initiated → stop wit
|
|
|
27
27
|
| Class/attribute toggles | CSS transition |
|
|
28
28
|
| Mount entry, no JS | `@starting-style` |
|
|
29
29
|
| Predetermined motion under load | CSS `@keyframes` / animation |
|
|
30
|
-
| JS control
|
|
30
|
+
| JS control over animation timing | WAAPI `element.animate()` |
|
|
31
31
|
| Springs, layout, exit, gestures | Motion (`motion.dev`) |
|
|
32
32
|
|
|
33
33
|
### 4. Properties
|
|
@@ -36,7 +36,7 @@ Same frequency and purpose tables as `SKILL.md`. Keyboard-initiated → stop wit
|
|
|
36
36
|
- No `scale(0)` entrances.
|
|
37
37
|
- Trigger-anchored surfaces: `transform-origin: var(--transform-origin)`.
|
|
38
38
|
- `translateY(100%)` = one element height — prefer over px.
|
|
39
|
-
- Motion
|
|
39
|
+
- Before changing Motion `x`/`y`/`scale` to a full `transform` string, identify the installed version, feature, and browser; profile under representative load. Separate library-managed layout transitions from per-frame layout updates. Neither notation guarantees a compositor or GPU path.
|
|
40
40
|
|
|
41
41
|
### 5. Easing, duration, springs
|
|
42
42
|
|
|
@@ -53,7 +53,9 @@ Bounce 0.1–0.3; reserve visible bounce for dismiss/playful cases.
|
|
|
53
53
|
|
|
54
54
|
### 6. Interruption & exit
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
Diagnose the failing interaction before tuning timing or changing tools. Use transitions (or springs) for rapid fire; symmetric enter/exit paths for dismiss surfaces; slow deliberate press, fast release (hold-to-confirm pattern).
|
|
57
|
+
|
|
58
|
+
When cancelling or removing motion, preserve intended visible content, focus, and final application state. Complete state changes directly; do not await an eliminated animation callback. Avoid redundant container/child entrances and keep controls interactive throughout sequencing.
|
|
57
59
|
|
|
58
60
|
### 7. A11y
|
|
59
61
|
|
|
@@ -67,7 +69,7 @@ Transitions (or springs) for rapid fire; symmetric enter/exit paths for dismiss
|
|
|
67
69
|
| `scale(0)` enter | `scale(0.95)` + opacity |
|
|
68
70
|
| `ease-in` on UI | ease-out + strong bezier |
|
|
69
71
|
| Keyframes on toasts/toggles | Transitions |
|
|
70
|
-
|
|
|
72
|
+
| Unmeasured shorthand replacement | Profile the installed version, feature, and browser under load |
|
|
71
73
|
| Ungated hover | fine-pointer media query |
|
|
72
74
|
|
|
73
75
|
## Recipes
|
|
@@ -21,7 +21,7 @@ const PATTERNS = [
|
|
|
21
21
|
// eslint.config.mjs, prettier.config.base.cjs, stylelint.config.shared.ts, …
|
|
22
22
|
/^(eslint|prettier|stylelint|commitlint|oxlint)\.config(\.[\w-]+)*\.(js|mjs|cjs|ts|mts|cts)$/,
|
|
23
23
|
// .eslintrc.json, .prettierrc.shared.yml, .stylelintrc.base.cjs, .markdownlint.jsonc, …
|
|
24
|
-
/^\.(eslintrc|prettierrc|stylelintrc|markdownlintrc|markdownlint|markdownlint-cli2)(\.[\w-]+)*\.(js|mjs|cjs|ts|json|jsonc|yml|yaml|toml)$/,
|
|
24
|
+
/^\.(eslintrc|prettierrc|stylelintrc|markdownlintrc|markdownlint|markdownlint-cli2)(\.[\w-]+)*\.(js|mjs|cjs|ts|mts|cts|json|jsonc|json5|yml|yaml|toml)$/,
|
|
25
25
|
];
|
|
26
26
|
|
|
27
27
|
function isGuarded(file) {
|
|
@@ -33,18 +33,22 @@ module.exports = function configGuard(payload, { truncated }) {
|
|
|
33
33
|
if (process.env.EXONOUS_ALLOW_CONFIG_EDITS === '1') return null;
|
|
34
34
|
if (truncated) return { block: 'Hook payload too large to inspect; refusing the edit. Make a smaller edit.' };
|
|
35
35
|
const input = payload?.tool_input || {};
|
|
36
|
-
const
|
|
37
|
-
|
|
36
|
+
const destinations = [input, ...(Array.isArray(input.edits) ? input.edits : [])];
|
|
37
|
+
for (const edit of destinations) {
|
|
38
|
+
const file = edit?.file_path || edit?.path || input.file_path || input.path;
|
|
39
|
+
if (typeof file !== 'string' || !isGuarded(file)) continue;
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
const absolute = path.resolve(payload.cwd || process.cwd(), file);
|
|
42
|
+
try {
|
|
43
|
+
fs.lstatSync(absolute);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (err.code === 'ENOENT') continue;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
block: `${path.basename(file)} is a lint/format config. Fix the code so it satisfies the rules instead of weakening them. If the user asked for this config change, they can allow it with EXONOUS_ALLOW_CONFIG_EDITS=1.`,
|
|
49
|
+
};
|
|
44
50
|
}
|
|
45
|
-
return
|
|
46
|
-
block: `${path.basename(file)} is a lint/format config. Fix the code so it satisfies the rules instead of weakening them. If the user asked for this config change, they can allow it with EXONOUS_ALLOW_CONFIG_EDITS=1.`,
|
|
47
|
-
};
|
|
51
|
+
return null;
|
|
48
52
|
};
|
|
49
53
|
|
|
50
54
|
module.exports.isGuarded = isGuarded;
|
|
@@ -103,7 +103,11 @@ function typecheck(files, cwd, run, report) {
|
|
|
103
103
|
if (!tsc) continue;
|
|
104
104
|
const res = run(tsc, ['--noEmit', '--pretty', 'false', '-p', project], project);
|
|
105
105
|
if (res.skipped || res.status === 0) continue;
|
|
106
|
-
const
|
|
106
|
+
const edited = new Set(group.map((file) => path.resolve(file)));
|
|
107
|
+
const relevant = res.output.split('\n').filter((line) => {
|
|
108
|
+
const diagnostic = line.match(/^(.+)\(\d+,\d+\):/);
|
|
109
|
+
return diagnostic && edited.has(path.resolve(project, diagnostic[1].replace(/[\\/]/g, path.sep)));
|
|
110
|
+
});
|
|
107
111
|
if (relevant.length) report(`type errors in edited files:\n${relevant.slice(0, 30).join('\n')}`);
|
|
108
112
|
}
|
|
109
113
|
}
|
|
@@ -57,7 +57,7 @@ Avoid sending on unbuffered channels without a receiver; prefer buffer size 1 pl
|
|
|
57
57
|
| String assembly in loop | `strings.Builder` |
|
|
58
58
|
| Map/set membership hot path | `map[T]struct{}` |
|
|
59
59
|
|
|
60
|
-
Profile before `sync.Pool
|
|
60
|
+
Profile before `sync.Pool`. Copy returned bytes or strings into owned memory before returning the buffer to the pool.
|
|
61
61
|
|
|
62
62
|
## Tooling
|
|
63
63
|
|
|
@@ -7,6 +7,7 @@ Run after a feature chunk, refactor, or before opening a PR. Complements editor
|
|
|
7
7
|
### 1 — Build
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
+
set -o pipefail
|
|
10
11
|
npm run build 2>&1 | tail -20
|
|
11
12
|
# or pnpm build / bun run build — match the repo
|
|
12
13
|
```
|
|
@@ -17,22 +18,24 @@ npm run build 2>&1 | tail -20
|
|
|
17
18
|
|
|
18
19
|
```bash
|
|
19
20
|
set -o pipefail
|
|
20
|
-
npx --no-install tsc --noEmit 2>&1 |
|
|
21
|
+
npx --no-install tsc --noEmit 2>&1 | sed -n '1,30p'
|
|
21
22
|
# Python: pyright . or mypy per project
|
|
22
23
|
```
|
|
23
24
|
|
|
24
|
-
Report error count; fix blocking type errors on touched files at minimum.
|
|
25
|
+
The filter consumes all output; `pipefail` preserves a failing tool status. Report error count; fix blocking type errors on touched files at minimum.
|
|
25
26
|
|
|
26
27
|
### 3 — Lint
|
|
27
28
|
|
|
28
29
|
```bash
|
|
29
|
-
|
|
30
|
+
set -o pipefail
|
|
31
|
+
npm run lint 2>&1 | sed -n '1,30p'
|
|
30
32
|
# Python: ruff check .
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
### 4 — Tests + coverage
|
|
34
36
|
|
|
35
37
|
```bash
|
|
38
|
+
set -o pipefail
|
|
36
39
|
npm run test -- --coverage 2>&1 | tail -50
|
|
37
40
|
```
|
|
38
41
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"exonous","version":"2.10.
|
|
1
|
+
{"name":"exonous","version":"2.10.8","description":"The mind outside your head for coding agents: enforced workflow, anchored memory and a disciplined agent roster for Claude Code and Cursor.","repository":"github:zaqueu-1/exonous","gitHead":"fb3470dcdb28ff9f513895a498d1a7ca7705a37f","bin":{"exonous":"bin/native.cjs"},"engines":{"node":">=18.0.0"}}
|