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.
Files changed (29) hide show
  1. package/assets/machine/hooks/lib/pr-template-validate.cjs +2 -0
  2. package/assets/machine/hooks/pyle.cjs +3 -4
  3. package/assets/skills/commit-changes/SKILL.md +3 -2
  4. package/assets/skills/make-pr/SKILL.md +5 -7
  5. package/assets/skills/ui-design/reference/adapt.md +2 -2
  6. package/assets/skills/ui-design/reference/adapt.native.md +1 -1
  7. package/assets/skills/ui-design/reference/critique.md +6 -6
  8. package/assets/skills/ui-design/reference/harden.md +5 -3
  9. package/assets/skills/ui-design/reference/hooks.md +1 -1
  10. package/assets/skills/ui-design/reference/styles/direction.md +5 -4
  11. package/assets/skills/ui-design/reference/typeset.md +1 -0
  12. package/assets/skills/ui-design/scripts/ENGINE-CHECKSUMS +6 -0
  13. package/assets/skills/ui-design/scripts/impeccable +78 -188
  14. package/assets/skills/ui-design/scripts/impeccable.cmd +2 -212
  15. package/assets/skills/ui-design/scripts/impeccable.ps1 +93 -0
  16. package/assets/skills/ui-design/scripts/ui-design.mjs +40 -9
  17. package/assets/skills/ui-motion/SKILL.md +5 -3
  18. package/assets/skills/ui-motion/reference/review.md +9 -7
  19. package/assets/skills/ui-motion/reference/web.md +6 -4
  20. package/assets/techne/scripts/hooks/config-guard.js +15 -11
  21. package/assets/techne/scripts/hooks/stop-checks.js +5 -1
  22. package/assets/techne/skills/go/reference/patterns.md +1 -1
  23. package/assets/techne/skills/quality/reference/verification-loop.md +6 -3
  24. package/native/aarch64-apple-darwin/exonous +0 -0
  25. package/native/aarch64-unknown-linux-gnu/exonous +0 -0
  26. package/native/x86_64-apple-darwin/exonous +0 -0
  27. package/native/x86_64-pc-windows-msvc/exonous.exe +0 -0
  28. package/native/x86_64-unknown-linux-gnu/exonous +0 -0
  29. 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, validatePrCreate, prFields;
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
- ({ validatePrCreate, prFields } = require('./lib/pr-template-validate.cjs'));
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
- const result = validatePrCreate(prFields(parsed), call.cwd);
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> → gh pr create --body-file <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 como comando isolado (opcionalmente após um `cd <dir> &&`):
19
- `gh pr create --base <base> --head <current-branch> --title "<title>" --body-file <draft-path>`
20
- Gate bloqueou: leia a razão, rode `exonous workflow pr`, corrija e tente novamente. Falha do `gh`: reporte; não grave handoff de PR criada.
21
- 7. **Só após sucesso do `gh`**, preserve a URL retornada e grave silenciosamente um JSON temporário privado com:
22
- `{"v":1,"k":"pr","g":[["pr","url","<URL retornada>"],["pr","did","<principal mudança concreta>"],["pr","proof","<comando e resultado realmente observado; ou validação pendente>"],["pr","next","<próxima ação concreta>"]]}`
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`: note next UI edit triggers hook.
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. Minimal briefs still need real photography in key slots.
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, `min-h-[100dvh]`, cleanup on effects, empty/load/error states
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
- if [ -n "${IMPECCABLE_BIN:-}" ] && [ -x "${IMPECCABLE_BIN}" ]; then
42
- exec "${IMPECCABLE_BIN}" "$@"
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
- case "$(uname -s 2>/dev/null || echo unknown)" in
46
- Darwin) os=darwin ;;
47
- Linux) os=linux ;;
48
- MINGW*|MSYS*|CYGWIN*|Windows_NT) os=windows ;;
49
- *) os=unknown ;;
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 2>/dev/null || echo unknown)" in
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
- bin="$dir/bin/$os-$arch/impeccable"
58
- [ "$os" = windows ] && bin="$bin.exe"
59
-
60
- if [ -x "$bin" ]; then
61
- exec "$bin" "$@"
62
- fi
63
- if [ -f "$bin" ]; then
64
- # Lost the executable bit in transit (zip extraction, some copiers).
65
- chmod +x "$bin" 2>/dev/null && exec "$bin" "$@"
66
- fi
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
- # Last resort: fetch this version's binary for the current platform from the
88
- # public release channel into the user cache. Needs network; sandboxes without
89
- # egress preinstall the binary on PATH instead.
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
- return 1
54
+ digest=$(sha256sum "$1") || refuse 'SHA256 failed'
102
55
  fi
56
+ actual=${digest%% *}
57
+ [ "$actual" = "$expected" ] || refuse "checksum mismatch: $1"
103
58
  }
104
- check_download() {
105
- download_file=${1:-$tmp}
106
- if [ ! -f "$download_file" ]; then
107
- rm -f "$tmp.sha256"
108
- 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." >&2
109
- exit 127
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
- if [ -n "$probing" ]; then
118
- # Inside another launcher's probe: no download, fail fast and quiet.
119
- exit 127
120
- fi
121
- if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
122
- base="${IMPECCABLE_DOWNLOAD_BASE:-https://github.com/pbakaus/impeccable/releases/download}"
123
- asset="impeccable-$os-$arch"
124
- [ "$os" = windows ] && asset="$asset.exe"
125
- url="$base/engine-v$version/$asset"
126
- tmp="$cache_root/bin/$version/.impeccable.part.$$"
127
- if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
128
- echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
129
- setup_help
130
- exit 127
131
- fi
132
- # Check the actual staging file, not just directory existence: a cache from
133
- # an earlier run can be readable but no longer writable inside a sandbox.
134
- if ! (umask 077; : > "$tmp") 2>/dev/null; then
135
- echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
136
- setup_help
137
- exit 127
138
- fi
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
- echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
205
- echo "Download impeccable-$os-$arch from https://github.com/pbakaus/impeccable/releases (tag engine-v$version) into $cache_root/bin/$version/impeccable$exe (then chmod +x), or set IMPECCABLE_BIN to a preinstalled engine binary. Docs: https://impeccable.style" >&2
206
- exit 127
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
- rem Impeccable launcher (Windows). Runs bin\windows-<arch>\impeccable.exe next
4
- rem to this file, else a cached or freshly downloaded engine binary.
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
- /** The trusted engine binaries impeccable.cmd would run (no probe needed), so arguments skip cmd.exe entirely. */
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
- const arch = /^arm64$/i.test(env.PROCESSOR_ARCHITECTURE ?? '') ? 'arm64' : 'x64';
52
- const versionFile = path.join(dir, 'VERSION');
53
- const version = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, 'utf8').split(/\r?\n/)[0].trim() : '';
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
- env.IMPECCABLE_BIN,
57
- path.join(dir, 'bin', `windows-${arch}`, 'impeccable.exe'),
58
- version && cacheRoot && path.join(cacheRoot, 'bin', version, 'impeccable.exe'),
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
- return candidates.find((file) => file && fs.existsSync(file)) ?? null;
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
- Animate **transform** and **opacity** (plus sanctioned **clip-path** where recipes allow). Avoid width/height/margin/padding/top/left — they trigger layout every frame.
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
- - Motion library shorthand `x`/`y`/`scale` runs on the main thread; under load use `transform: "translateX(...)"` for GPU paths.
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. **GPU props** — transform/opacity; no layout props; Motion shorthands under load → full transform string.
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 &gt;300ms unjustified; center origin on anchored popover; keyframes on toasts/toggles; layout animation; Motion x/y on busy pages; parent CSS var driving child transform; missing reduced-motion or hover gate; symmetric press/release on hold flows; group entrance without 30–80ms stagger.
26
+ `transition: all`; `scale(0)`; ease-in on UI; animation on keyboard/high-frequency; UI &gt;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
- 1. Delete animation → 2. Reduce → 3. Fix easing → 4. Fix origin/physicality → 5. Interruptible mechanism → 6. GPU path → 7. Asymmetric timing → 8. Polish (stagger, blur, `@starting-style`, spring) → 9. A11y & cohesion.
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, easy GPU fix missed) or **Approve**.
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 &gt; ~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 &gt; ~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; Motion → transform string; CSS/WAAPI for predetermined under load; blur &lt;20px.
81
+ Prefer transform/opacity; no `transition: all`; no parent `--var` driving child transform; blur &lt;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, CSS perf | WAAPI `element.animate()` |
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: full `transform` string, not `x`/`y` shorthands under load.
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
- Transitions (or springs) for rapid fire; symmetric enter/exit paths for dismiss surfaces; slow deliberate press, fast release (hold-to-confirm pattern).
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
- | Motion x/y under load | `transform` string |
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 file = input.file_path || input.path;
37
- if (typeof file !== 'string' || !isGuarded(file)) return null;
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
- const absolute = path.resolve(payload.cwd || process.cwd(), file);
40
- try {
41
- fs.lstatSync(absolute);
42
- } catch (err) {
43
- if (err.code === 'ENOENT') return null;
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 relevant = res.output.split('\n').filter((line) => group.some((f) => line.includes(path.relative(project, f))));
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`; misuse adds complexity.
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 | head -30
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
- npm run lint 2>&1 | head -30
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
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"exonous","version":"2.10.4","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":"2d79f67602b31dd6886a58c78f9c3e223e843ea9","bin":{"exonous":"bin/native.cjs"},"engines":{"node":">=18.0.0"}}
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"}}