novahiz 0.9.2 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/README.md +23 -5
  3. package/adapters/opencode/agent/novahiz.md +2 -2
  4. package/adapters/opencode/commands/novahiz-obsidian-audit.md +28 -0
  5. package/adapters/opencode/commands/novahiz-obsidian.md +27 -0
  6. package/adapters/opencode/commands/novahiz-skills-update.md +24 -7
  7. package/adapters/opencode/instructions.md +2 -2
  8. package/adapters/opencode/novahiz-plugin.ts +164 -4
  9. package/bin/veille-mensuelle.ps1 +26 -0
  10. package/catalog/categories.json +13 -11
  11. package/catalog/providers.json +14 -6
  12. package/catalog/rules.json +35 -0
  13. package/dist/cli.js +8 -3
  14. package/dist/commands/context.js +10 -0
  15. package/dist/commands/doctor.js +11 -3
  16. package/dist/commands/gate.js +10 -4
  17. package/dist/commands/inspect.js +110 -39
  18. package/dist/commands/second-memory.js +6 -1
  19. package/dist/commands/upgrade.js +8 -1
  20. package/dist/complexity.js +18 -0
  21. package/dist/deps.js +127 -0
  22. package/dist/gate-daemon.js +126 -0
  23. package/dist/gate.js +55 -6
  24. package/dist/ledger.js +29 -5
  25. package/dist/providers.js +47 -5
  26. package/dist/spec.js +5 -1
  27. package/dist/wireframe.js +595 -0
  28. package/docs/INSTALL.md +1 -1
  29. package/docs/PROVIDERS.md +26 -7
  30. package/docs/TOKENS.md +1 -1
  31. package/install/install.mjs +32 -8
  32. package/install/install.sh +0 -0
  33. package/install/lib.mjs +3 -1
  34. package/mcp/argus/IMPLEMENTATION-SUMMARY.md +1 -1
  35. package/mcp/novahiz-tools/index.mjs +92 -4
  36. package/mcp/stitch-template/recommandation.md +1 -1
  37. package/package.json +1 -1
  38. package/skills/browser-session/SKILL.md +1 -1
  39. package/skills/novahiz-browser/SKILL.md +1 -1
  40. package/skills/novahiz-prd/SKILL.md +116 -0
  41. package/skills/novahiz-prd/references/prd-template.md +101 -0
  42. package/skills/novahiz-skill-craft/SKILL.md +213 -0
  43. package/skills/novahiz-skill-craft/assets/evals-template.json +39 -0
  44. package/skills/novahiz-skill-craft/assets/frontmatter-checklist.md +75 -0
  45. package/skills/novahiz-skill-craft/assets/skill-template.md +91 -0
  46. package/skills/novahiz-skill-craft/evals.json +58 -0
  47. package/skills/novahiz-skill-craft/references/evals-spec.md +136 -0
  48. package/skills/novahiz-skill-craft/references/frontmatter-spec.md +75 -0
  49. package/skills/novahiz-skill-craft/references/progressive-disclosure.md +81 -0
  50. package/skills/novahiz-skill-craft/references/schemas.md +163 -0
  51. package/skills/novahiz-skill-craft/scripts/check-memory.py +256 -0
  52. package/skills/novahiz-skill-craft/scripts/extract-description.py +243 -0
  53. package/skills/novahiz-skill-craft/scripts/generate-comparison.py +179 -0
  54. package/skills/novahiz-skill-craft/scripts/generate-grading.py +223 -0
  55. package/skills/novahiz-skill-craft/scripts/run-eval.py +265 -0
  56. package/skills/novahiz-skill-craft/scripts/scan-skill.py +213 -0
  57. package/skills/novahiz-skill-craft/scripts/validate-frontmatter.py +176 -0
  58. package/skills/novahiz-wireframe/SKILL.md +278 -0
  59. package/skills/novahiz-wireframe/references/format.md +190 -0
  60. package/skills/novahiz-wireframe/references/shape-handoff.md +92 -0
  61. package/src/cli.ts +8 -5
  62. package/src/commands/context.ts +11 -0
  63. package/src/commands/doctor.ts +16 -3
  64. package/src/commands/gate.ts +10 -4
  65. package/src/commands/inspect.ts +113 -39
  66. package/src/commands/second-memory.ts +6 -1
  67. package/src/commands/upgrade.ts +8 -1
  68. package/src/complexity.ts +19 -0
  69. package/src/deps.ts +149 -1
  70. package/src/gate-daemon.ts +140 -0
  71. package/src/gate.ts +60 -6
  72. package/src/ledger.ts +30 -5
  73. package/src/providers.ts +51 -6
  74. package/src/spec.ts +26 -1
  75. package/src/wireframe.ts +751 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,236 @@
3
3
  All notable changes to Novahiz are documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [Unreleased]
7
+
8
+ ## [0.9.5] - 2026-10-15
9
+
10
+ > 0.9.4 was prepared on 2026-10-14 but never tagged nor published to npm
11
+ > (the registry went straight from 0.9.3 to 0.9.5). Its changes are included
12
+ > here and remain described under their own heading below.
13
+
14
+ ### Added
15
+
16
+ - **Wireframe-first design pipeline** - new `novahiz_wireframe` MCP tool and
17
+ `src/wireframe.ts` module
18
+ (`init/screen/section/sections/plan/review/index/attach/mark/status`).
19
+ Each screen gets a subfolder of ordered, annotated sections plus a
20
+ regenerated `INDEX.md`; `attach` returns the whole wireframe as the
21
+ attachment block fed to `impeccable shape`. `sections` creates a whole
22
+ batch in one call (one `INDEX` rebuild instead of one per section, existing
23
+ names skipped); `plan` classifies the screen and says whether it needs
24
+ several sections and why; `review` reports the sections that were
25
+ forgotten and gates approval (`mark` refuses while `verdict != pass`).
26
+ Sections are renumbered from the highest existing index instead of
27
+ `length + 1`. Gate rule `R17-wireframe` and the `wireframe` roadmap step
28
+ (before `impeccable-shape`) make the sequence mandatory on `design-ui`
29
+ work and on screen/page/component paths. New skill `novahiz-wireframe`,
30
+ tested by `tests/wireframe.test.ts`.
31
+ - **`novahiz deps` reports real dependency health** - the single provider
32
+ check is replaced by a four-part `dependencyReport`: toolchain probing
33
+ (node, npm, npx, uv, python), local MCP entrypoint `existsSync`, npm home
34
+ health (declared deps resolved + lockfile present), and provider
35
+ prerequisites. `novahiz_deps` returns the same shape.
36
+ - **Core toolchain bootstrap** - `deps --install` can now install `uv` and
37
+ Python on win32 / darwin / linux. The argv lives in `src/deps.ts`
38
+ (`coreBootstrap`), not in the catalog, and runs through a plain spawn:
39
+ `runScript`'s allowlist deliberately refuses shells and package managers,
40
+ so catalog data can never widen it. `--core-only` installs only toolchain
41
+ and npm health; the installer uses it when providers are declined.
42
+ - **Idempotent provider installs** - a new `installedWhen` field on a
43
+ provider lists skill ids whose combined presence marks the pack already
44
+ installed; when all of them are on disk the install command is skipped, so
45
+ a second `deps --install --yes` runs nothing. Providers without the field
46
+ keep the historical always-run behaviour.
47
+ - **Gate rule `R18-skill-craft`** - `novahiz-skill-craft` is now actually
48
+ required on any `SKILL.md` edit, matching what its own description
49
+ already promised.
50
+ - **Skills `novahiz-prd` and `novahiz-skill-craft`** ship in the package.
51
+ - **Commands `novahiz-obsidian` and `novahiz-obsidian-audit`**, plus
52
+ `bin/veille-mensuelle.ps1`.
53
+ - **A real refresh path for the provider skill packs** - `deps --install
54
+ --update` and a per-provider `update` command in `catalog/providers.json`.
55
+ `deps --install` drops every pack whose `installedWhen` markers are all on
56
+ disk, so it could only fill gaps: `/novahiz-skills-update` called it and
57
+ therefore refreshed nothing, which is why flutter/dart/expo were never
58
+ actually updated. `updateCommands()` reads the provider's `update` command
59
+ instead and applies no `installedWhen` gate. For the skills CLI that command
60
+ is the upstream `skills update -g -y`, already present in the pinned
61
+ `skills@1.7.0`. An identical argv shared by several providers is deduplicated
62
+ and records the packs it covers under `covers`, in the dry-run plan and in
63
+ the result, so a skipped pack can no longer be mistaken for an updated one.
64
+ `/novahiz-skills-update` gained an explicit step 4 running it.
65
+ - **The Flutter skill pack is installed from its official repository** -
66
+ `flutter/agent-plugins` is now recorded as `flutter/skills`, the name used by
67
+ the Flutter blog post *Introducing Skills for Dart and Flutter*. The two
68
+ resolve to the same repository (identical `main` HEAD `0ef3972` and branch
69
+ list), so this is a provenance correction, not a source change.
70
+
71
+ ### Fixed
72
+
73
+ - **`tests/plugin-warmstart.test.ts` failed `tsc --noEmit`** with three real
74
+ type errors: the `editor` parameter shadowed the `editor` binding, making
75
+ `typeof editor` self-referential (TS2502), and `await cleanup()` was called
76
+ on the `void | Cleanup` union without a guard (TS2349, twice). The previous
77
+ session reported this file as type-clean; that report was wrong.
78
+ - **`Agent harness copy outdated`** — the last non-blocking `doctor` failure —
79
+ is repaired by `node install/install.mjs --yes` alone. Only the rewrite of
80
+ `novahiz.config.json` is gated on `--force`; the plugin, agent and command
81
+ copies are not, so a plain run refreshes them while the config reports
82
+ `Existing config preserved`.
83
+
84
+ ### Changed
85
+
86
+ - **`novahiz sync` is now listed in `--help`** - the command existed
87
+ (`case "sync"` → `commandSync()`), and both `gate.ts:498` and
88
+ `gate-repair.ts:87` pointed users at it, but it was missing from the help
89
+ output.
90
+ - **The installer no longer backs up the published package inside its own
91
+ home** - the home is a copy of what npm already holds, so preserving its
92
+ previous version only accumulated `*.novahiz-bak` (235 of the 320 found by
93
+ the audit). Backups are kept only where they carry something that exists
94
+ nowhere else: `catalog/` (user-edited configuration) and the files written
95
+ into the opencode config dir, which are exactly what `novahiz uninstall`
96
+ restores (`uninstall.mjs:53`).
97
+ - **`scripts` is part of `CORE_ITEMS`** - `package.json` copied into the home
98
+ declares `ci:local = node scripts/ci-local.mjs`, but the directory was
99
+ never copied, so `npm run ci:local` failed there.
100
+ - **Windows PATH staleness is no longer reported as a missing dependency** -
101
+ the parent process may predate the install (opencode started before uv or
102
+ Python landed), so its inherited PATH hides binaries that ARE on disk.
103
+ `toolchainStatus` reloads PATH from the registry (Machine + User) once and
104
+ re-probes. `python` is probed with `--version` rather than `where python`,
105
+ because the `WindowsApps` execution alias answers on machines without any
106
+ Python.
107
+ - **The installer asks about providers by default** (`true` instead of
108
+ `false`) and adds a `--no-deps-install` flag. Automatic dependency
109
+ installation now defaults to on (`autoInstall !== false`); when providers
110
+ are declined the toolchain and npm health still install, being system
111
+ dependencies rather than optional providers.
112
+ - **`npm install` runs with `--ignore-scripts`** and `npm_config_global=false`,
113
+ scoped to the Novahiz home, so nothing in the home re-enters the
114
+ installer.
115
+
116
+ ### Notes
117
+
118
+ - `novahiz deps` and the `novahiz_deps` tool change their JSON contract:
119
+ `{node, platform, dependencies}` becomes
120
+ `{node, platform, toolchain, dependencies, entrypoints, npm}`, and the
121
+ dry-run `plan` becomes `{core, npm, bootstrap, install}`. Anything parsing
122
+ the old shape must be updated.
123
+
124
+ ## [0.9.4] - 2026-10-14
125
+
126
+ ### Fixed
127
+
128
+ - **No absolute machine path is shipped any more** - the tree handed to every
129
+ member of the community carried the drive letters, the folders and the
130
+ account name of the machine Novahiz was developed on, so the system was
131
+ broken on arrival for anyone else.
132
+ - `adapters/opencode/instructions.md` (the system prompt injected into every
133
+ session), `skills/novahiz-browser/SKILL.md` and
134
+ `skills/browser-session/SKILL.md` pointed the Playwright profile at a
135
+ hardcoded `C:\Users\<account>\.opencode\playwright-profile`. The profile
136
+ rule was unusable and the browser profile wrong for every other user. All
137
+ three now use `~/.opencode/playwright-profile`.
138
+ - `docs/TOKENS.md`, `docs/audit-2026-09-25.md`,
139
+ `mcp/argus/IMPLEMENTATION-SUMMARY.md` and
140
+ `mcp/stitch-template/recommandation.md` presented absolute paths as
141
+ instructions to run. They now use `~` or `<NOVAHIZ_HOME>`.
142
+ - The placeholders shipped to the agent were corrupted:
143
+ `node NNovahiz-home>/src/cli.ts classify "Nrequest>"` and
144
+ `skill({ name: "Nskill>" })` in `adapters/opencode/agent/novahiz.md`. They
145
+ are restored to `<Novahiz-home>`, `<request>` and `<skill>`, so the first
146
+ step of the pipeline is executable as written.
147
+
148
+ - **Home resolution no longer yields a ghost home** - `src/cli.ts`,
149
+ `src/commands/upgrade.ts` and `src/commands/second-memory.ts` built the home
150
+ from `process.env.HOME || process.env.USERPROFILE || ""`. When both are
151
+ absent - a Windows service, the gate daemon child, a container - the joined
152
+ result was a path relative to the cwd, so `detectTarget`, `runSync` and the
153
+ Stitch recommendation all operated silently on a directory that does not
154
+ exist. All three now resolve through `NovahizHome()` or `homedir()`.
155
+
156
+ - **The C3 index guard fires on Windows** - `src/gate.ts` compared the
157
+ protected `build/installed-skills.json` and `build/catalog.json` suffixes
158
+ spelled with `/` against a path carrying the platform separator, so the guard
159
+ never triggered on Windows. Those two files define what the gate requires and
160
+ were editable like ordinary build output on the platform most of the
161
+ community installs on. The path is now normalised before comparison.
162
+
163
+ - **The gate daemon stops flashing a console window on Windows** -
164
+ `adapters/opencode/novahiz-plugin.ts` spawned it without `windowsHide: true`,
165
+ the only spawn site in the file missing it.
166
+
167
+ - **Duplicated `NOVAHIZ_HOME` reads** in `src/spec.ts` and `install/lib.mjs`
168
+ collapse into one canonical read. A user who exported the older variable name
169
+ silently kept the default home while believing they had redirected it.
170
+
171
+ - **`novahiz doctor` stops failing at home root on project context** -
172
+ `src/commands/doctor.ts` resolved `PRODUCT.md` and `DESIGN.md` against the
173
+ novahiz home, producing two permanent rows no command could close. The rows
174
+ are scoped to a project directory and remain unchanged inside one.
175
+
176
+ - **A plan that declares its dependencies by label no longer wedges** -
177
+ `src/ledger.ts` resolved each dependency by todo id only, while
178
+ `novahiz task plan` accepts labels ("T2", "P5"). Every label-declared
179
+ dependency read as unfinished and blocked its dependent step for the whole
180
+ life of the task. Resolution now accepts both, and fails closed on an
181
+ unknown or ambiguous reference.
182
+
183
+ ### Added
184
+
185
+ - **A persistent gate daemon** (`src/gate-daemon.ts`, wired in
186
+ `adapters/opencode/novahiz-plugin.ts`) replaces one Node process spawn per
187
+ gate call. Every request re-executes `commandGate`, which re-reads
188
+ `rules.json`, `categories.json` and the skill index from disk, so no verdict
189
+ can go stale; any daemon failure returns as `spawnError` and the existing
190
+ deny path applies unchanged. Measured on this machine: one-shot median 331 ms
191
+ against 8.1 ms warm.
192
+
193
+ ### Changed
194
+
195
+ - **Portability is enforced by a test** - `tests/portability-paths.test.ts`
196
+ fails the build on an absolute machine path, a machine account name, an MCP
197
+ command the catalog cannot resolve to an absolute path, or a regression of
198
+ the C3 guard. It ships a negative proof: an injected path makes it fail by
199
+ file and line.
200
+
201
+ - **`.novahiz-install.json` and `novahiz.config.json` are no longer tracked** -
202
+ the install manifest embedded the absolute paths and the account name of the
203
+ machine that ran the installer, and was published with them. Both are
204
+ machine-local state regenerated on demand, and are now ignored.
205
+
206
+ ## [0.9.3] - 2026-10-11
207
+
208
+ ### Changed
209
+
210
+ - **CI verifies the suite and the installer dry-run on Windows and macOS**
211
+ — the matrix keeps the Ubuntu node grid (22 / 22.18.0 / 24) and adds one
212
+ node-24 job each on `windows-latest` and `macos-latest` (5 jobs).
213
+ Previously only Ubuntu ran in CI; Windows rested on local runs and macOS
214
+ on nothing at all.
215
+
216
+ ### Fixed
217
+
218
+ - **`install.sh` is executable in the package** — mode moved from 100644
219
+ to 100755, so `./install/install.sh` no longer fails with permission
220
+ denied on Linux/macOS.
221
+ - **macOS memory CLI test compares canonical paths** — `$TMPDIR` on macOS
222
+ is a symlink (`/var` → `/private/var`); the first-ever macOS CI run
223
+ caught `tests/memory-cli.test.ts` asserting the CLI's resolved root
224
+ against a raw string. Both sides now go through `realpathSync`.
225
+
226
+ ### Notes
227
+
228
+ - Cross-platform claims re-checked against primary sources: GitHub docs
229
+ (matrix `include` adds non-mergeable combinations → 5 jobs, confirmed by
230
+ the runs), npm docs (`--allow-scripts=novahiz` is the documented way to
231
+ approve a global install; a package cannot self-authorize), Playwright
232
+ docs (WebKit runs on all three OSes; Linux needs its system libraries).
233
+ The README now carries the Linux remedy: `npx playwright install-deps
234
+ webkit`.
235
+
6
236
  ## [0.9.2] - 2026-10-11
7
237
 
8
238
  ### Fixed
package/README.md CHANGED
@@ -63,6 +63,8 @@ npm antérieur à 11.16 exécute les scripts par défaut : `npm install -g novah
63
63
 
64
64
  > Piège : `npm install -g --allow-scripts=novahiz` **sans** le nom du paquet installe le dossier courant et échoue avec `ENOENT: package.json`. Le nom du paquet est obligatoire : `npm install -g --allow-scripts=novahiz novahiz`.
65
65
 
66
+ > Linux : le serveur Playwright MCP s'exécute sous WebKit (règle maison : Chromium interdit). WebKit exige ses dépendances système : si le navigateur ne démarre pas, `npx playwright install-deps webkit` (sudo requis sur Debian/Ubuntu).
67
+
66
68
  Puis vérifiez :
67
69
 
68
70
  ```bash
@@ -312,7 +314,7 @@ Deux pièces complémentaires : la session consomme moins sans perdre en puissan
312
314
 
313
315
  ## Skills installées
314
316
 
315
- Novahiz livre 94 skills couvrant toutes les catégories — 46 dans le cœur du dépôt, les autres dans les packs officiels installés par défaut au setup :
317
+ Novahiz livre 95 skills au total — le cœur du dépôt, plus les packs officiels installés par défaut au setup :
316
318
 
317
319
  | Catégorie | Skills | Objectif |
318
320
  |-----------|--------|----------|
@@ -325,9 +327,11 @@ Novahiz livre 94 skills couvrant toutes les catégories — 46 dans le cœur du
325
327
  | `browser` | novahiz-browser, browser-session, novahiz-web-extract, ... | Automatisation web, captures, extraction |
326
328
  | `audit` | novahiz-security, package-risk-audit, llm-threat-review, ... | Sécurité, conformité, vulnérabilités |
327
329
  | `expo` | expo-overview, expo-router, expo-module, expo-dev-client, ... | Expo / React Native : routes, modules natifs, builds |
328
- | `devops` | eas-workflows, eas-app-stores, novahiz-release, ... | CI/CD, déploiements, releases versionnées |
330
+ | `devops` | novahiz-release, novahiz-init, ... | Releases versionnées, onboarding, installation |
331
+
332
+ Le décompte exact se relit à tout moment : `novahiz sync` (index) et `novahiz doctor` (ligne `Referenced skills`). `npx novahiz skills --all` donne la liste complète.
329
333
 
330
- Lancer `npx novahiz skills --all` pour voir la liste complète.
334
+ > Les skills `eas-*` (workflows, stores, hosting, observe, update) sont des packs **payants** upstream : elles ne sont pas installées par défaut et `novahiz doctor` les exclut de `Referenced skills` par conception.
331
335
 
332
336
  ---
333
337
 
@@ -346,9 +350,22 @@ Novahiz auto-enregistre les serveurs MCP externes selon la catégorie du prompt
346
350
  | novahiz-scheduler | `mcp/clepsydre/index.mjs` (local, zéro dépendance) | Apache-2.0 | devops |
347
351
  | dart | `dart mcp-server` (Dart SDK) | BSD-3-Clause | code, debug, design-ui, flutter |
348
352
 
349
- Packs de skills (installés depuis les dépôts officiels, jamais vendorés, et livrés **par défaut** au setup via `providers.autoInstall`) : `flutter/agent-plugins` (10 skills), `dart-lang/skills` (15 skills), `expo/skills` (19 skills, le groupe `expo-*` seulement ; les services payants `eas-*` exclus), `pbakaus/impeccable` (1 skill, la skill design upstream `impeccable`).
353
+ Packs de skills (installés depuis les dépôts officiels, jamais vendorés, et livrés **par défaut** au setup via `providers.autoInstall`) : [`flutter/skills`](https://github.com/flutter/skills) (10 skills), `dart-lang/skills` (15 skills), `expo/skills` (19 skills, le groupe `expo-*` seulement ; les services payants `eas-*` exclus), `pbakaus/impeccable` (1 skill, la skill design upstream `impeccable`).
354
+
355
+ **`git` est requis** par le CLI officiel des skills : sans lui, l'installation des packs échoue silencieusement et les skills manquent au premier lancement. `novahiz doctor` le nomme (ligne `git (skill packs)`).
356
+
357
+ ### Installer, puis mettre à jour : deux étapes distinctes
358
+
359
+ `deps --install` ne **remplit que les trous**. Dès qu'un pack est présent, sa commande d'installation est écartée (marqueur `installedWhen` satisfait) — sinon une seconde passe réinstallerait inutilement 45 skills. Conséquence directe : **cette même commande ne peut jamais rafraîchir un pack déjà installé.**
360
+
361
+ ```
362
+ novahiz deps --install # installe ce qui manque (packs absents)
363
+ novahiz deps --install --update # rafraîchit tout depuis upstream
364
+ ```
365
+
366
+ `--update` exécute la commande `update` déclarée par chaque provider — pour le CLI des skills, la sous-commande officielle `skills update -g -y`, qui parcourt les quatre sources (dart, flutter, expo, impeccable) en une passe. Contrairement à l'installation, **elle n'est jamais écartée** : rafraîchir un pack à jour est exactement le travail demandé.
350
367
 
351
- **`git` est requis** par le CLI officiel des skills : sans lui, l'installation des packs échoue silencieusement et les skills manquent au premier lancement. `novahiz doctor` le nomme (ligne `git (skill packs)`), et **`/novahiz-skills-update`** — ou `novahiz deps --install` — réinstalle proprement tous les packs depuis leurs providers officiels. Voir [docs/PROVIDERS.md](docs/PROVIDERS.md).
368
+ `/novahiz-skills-update` enchaîne les deux. Chaque plan porte `step: "update"` et, quand une même commande couvre plusieurs packs, la liste `covers` — on ne peut donc pas confondre un pack écarté et un pack mis à jour. Voir [docs/PROVIDERS.md](docs/PROVIDERS.md).
352
369
 
353
370
  Dépôts upstream et provenance complète des providers MCP et plugins opencode : [docs/PROVIDERS.md](docs/PROVIDERS.md), [docs/HARNESSES.md](docs/HARNESSES.md), [NOTICE.md](NOTICE.md).
354
371
 
@@ -394,6 +411,7 @@ Voir [docs/CONFIGURATION.md](docs/CONFIGURATION.md) pour toutes les options.
394
411
  | `novahiz roadmap` | Afficher la roadmap d'exécution |
395
412
  | `novahiz dispatch` | Générer des work packets |
396
413
  | `novahiz sync` | Reconstruire l'index des skills installées |
414
+ | `novahiz deps --install [--update]` | Installer les packs de skills manquants, ou les rafraîchir tous avec `--update` (voir [Providers](#providers)) |
397
415
  | `novahiz clean` | Supprimer les vieux logs |
398
416
  | `novahiz upgrade` | Pull du dernier + rebuild |
399
417
  | `novahiz version` | Afficher la version |
@@ -16,8 +16,8 @@ Clarification questions must be asked in the same language as the user's input p
16
16
 
17
17
  Work in this order for every request:
18
18
 
19
- 1. Classify. Call the `novahiz_classify` MCP tool with the user request, or run `node NNovahiz-home>/src/cli.ts classify "Nrequest>"`. Read the returned categories and required skills.
20
- 2. Load skills. Load every required skill with `skill({ name: "Nskill>" })` before touching any file. The gate enforces this: `edit`, `write`, `patch`, `apply_patch`, and shell writes are blocked until the required skills are loaded.
19
+ 1. Classify. Call the `novahiz_classify` MCP tool with the user request, or run `node <Novahiz-home>/src/cli.ts classify "<request>"`. Read the returned categories and required skills.
20
+ 2. Load skills. Load every required skill with `skill({ name: "<skill>" })` before touching any file. The gate enforces this: `edit`, `write`, `patch`, `apply_patch`, and shell writes are blocked until the required skills are loaded.
21
21
  3. Plan. For anything beyond a trivial change, write a short todo list with acceptance criteria.
22
22
  4. Execute. Make the change. Prefer small, reversible edits. Keep architecture modular, scalable, and maintainable.
23
23
  5. Verify. Run the relevant tests or commands. Report what you ran and what it returned. Never claim a result you did not observe.
@@ -0,0 +1,28 @@
1
+ ---
2
+ description: Audit AND auto-fix the whole Obsidian data arborescence: structure, links, coherence. Runs doctor --apply (category by category), then sync --apply, then lint. Arguments: $ARGUMENTS
3
+ ---
4
+
5
+ 1. Locate the Novahiz home: `NOVAHIZ_HOME` if set, otherwise `~/.config/novahiz`. The CLI is `<home>/src/cli.ts`.
6
+
7
+ 2. Run `node src/cli.ts second-memory doctor --apply $ARGUMENTS` from the Novahiz home. This **auto-fixes** everything category by category: missing folders created, MOCs created, links repaired, root notes relocated, naming fixed. Every moved file is backed up first. Pass `--no-plugins` to skip the mandatory plugin check, `--json` for structured output.
8
+
9
+ 3. The command exits 1 while issues remain and 0 when the vault is clean. For each `[fail]` check:
10
+ - `skeleton` — the vault is missing `INDEX.md`, `STRUCTURE.md` or a system folder: auto-recreated by `doctor --apply`.
11
+ - `arborescence` — a note sits at the vault root, a folder has no `_MOC.md`, a child MOC is not linked from its parent, a domain is only partially materialised, or `INDEX.md → ## Categories` does not list exactly the active domains. Auto-fixed by `doctor --apply`.
12
+ - `links` — broken wikilinks or orphans: auto-fixed by `doctor --apply` (runs the `fix` repairs).
13
+ - `plugins` — a mandatory community plugin is missing or disabled: auto-fixed by `doctor --apply`.
14
+ - `outside-tree folders` (warn) — a top-level folder is not in `catalog/vault-structure.json`. Doctor does not move folders: report them and migrate their notes by hand.
15
+ - `memory` (warn) — project memory has pending changes: auto-fixed by `sync --apply` below.
16
+
17
+ 4. Then the **global coherence pass** — from the project directory, beyond what doctor checks structurally:
18
+ - `node src/cli.ts second-memory sync --apply`: pending slot pulls/pushes AND `novahiz-docs/` mirror drift are auto-applied. Mirror drift means the repo docs moved ahead of the vault.
19
+ - Verify the active project's binding exists (`vault.json` at the memory root `sync` uses —
20
+ `project-memory/vault.json` or `~/.config/project-memory/vault.json`) and its chain is
21
+ materialised (`Code/<branch>/<project>/{docs,journal,decisions}/`). A memory root without a binding
22
+ means `project-init` never ran: notes then fall back to content-keyword routing and may
23
+ land in `Inbox` or a wrong branch. Report every note found outside the bound chain, but
24
+ NEVER move a note automatically — propose the move and wait for consent.
25
+ - Consistency across the three layers (repo `novahiz-docs/` ↔ vault `docs/` chain ↔ vault `memory/` chain): same project folder spelling everywhere, no duplicate project folders (a leftover `general/` or old-chain folder next to the real one), no empty bound folder left after a rebind.
26
+ - When doctor passes but something still looks off, run the strict pass: `node src/cli.ts second-memory lint`.
27
+
28
+ 5. Report what the audit found, what was actually repaired (with the backup location), what remains — including coherence suggestions you did not apply. Never claim a repair you did not run.
@@ -0,0 +1,27 @@
1
+ ---
2
+ description: Initialize Obsidian vault structure for the active project. Arguments: $ARGUMENTS
3
+ ---
4
+
5
+ 1. Locate the Novahiz home: `NOVAHIZ_HOME` if set, otherwise `~/.config/novahiz`. The CLI is `<home>/src/cli.ts`.
6
+
7
+ 2. If `$ARGUMENTS` is `init` (or empty), first run the preview from the project directory (the active project — its cwd gives the name, its dependencies the routing):
8
+
9
+ `node src/cli.ts second-memory project-init --dry-run --json`
10
+
11
+ Show the routed chain and the mirrored files. Unless the user only wanted the preview, run it for real:
12
+
13
+ `node src/cli.ts second-memory project-init --json`
14
+
15
+ Extra flags when the routing must be forced: `--project <name>`, `--domain <Domain> --branch <Branch>` (both together).
16
+
17
+ 3. What it does — additive and idempotent, nothing existing is overwritten:
18
+ - creates the project chain `Code/<branch>/{memory,docs}/<project>/` (folders, `_MOC.md` links at every level, `INDEX.md` entry, domain activated whole);
19
+ - writes the routing binding in a `vault.json` sidecar at the memory root `sync` uses
20
+ (`project-memory/vault.json` in the project, or `~/.config/project-memory/` when that
21
+ global root exists — future `second-memory sync` runs send this project's slots and doc
22
+ mirror to the bound chain instead of guessing by content keywords);
23
+ - mirrors `novahiz-docs/*.md` into the docs chain (source of truth stays the repo).
24
+
25
+ 4. Exit codes: 0 created or nothing to do; 1 when the vault is absent (run `node src/cli.ts second-memory init` first), when routing is impossible from the project signal (rerun with `--domain`/`--branch`), or on an unknown flag.
26
+
27
+ 5. Report the bound chain (`Code/.../<project>/`), the binding line, and every file created or mirrored. If the branch picked by the keywords looks wrong, rerun with `--domain <D> --branch <B>` before any note exists. Never claim a folder you did not create.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Reinstall every optional skill pack (dart, flutter, expo, impeccable) cleanly from their official providers. Arguments: $ARGUMENTS
2
+ description: Install any missing skill pack and refresh every installed pack (dart, flutter, expo, impeccable) from their official providers. Arguments: $ARGUMENTS
3
3
  ---
4
4
 
5
5
  1. Locate the Novahiz home: `NOVAHIZ_HOME` if set, otherwise `~/.config/novahiz`.
@@ -9,21 +9,38 @@ description: Reinstall every optional skill pack (dart, flutter, expo, impeccabl
9
9
  silently. If git is missing, tell the user to install it first
10
10
  (Windows: `winget install Git.Git`) and stop.
11
11
 
12
- 3. Reinstall/update every provider from the official registry
13
- (`catalog/providers.json` - dart-lang/skills, flutter/agent-plugins,
12
+ 3. Install whatever pack is missing from the official registry
13
+ (`catalog/providers.json` - dart-lang/skills, flutter/skills,
14
14
  expo/skills, pbakaus/impeccable, plus the provider MCP servers):
15
15
 
16
16
  `node <home>/src/cli.ts deps --install --yes`
17
17
 
18
- 4. Refresh the Impeccable CLI and engine from npm (official package):
18
+ This step only *fills gaps*. Every pack already on disk is skipped by
19
+ design (`installedWhen` satisfied), so it can never bring an installed
20
+ pack up to date — that is what step 4 is for.
21
+
22
+ 4. Refresh the installed packs from upstream. This is the real update: it
23
+ runs each provider's `update` command, which for the skills CLI is the
24
+ upstream `skills update -g -y` subcommand and covers dart, flutter, expo
25
+ and impeccable in one pass:
26
+
27
+ `node <home>/src/cli.ts deps --install --update --yes`
28
+
29
+ Confirm the JSON reports `"step": "update"` with `"ok": true` (a shared
30
+ command lists every provider under `covers`). If it reports
31
+ `"step": "install"` instead, the `--update` flag did not reach the CLI —
32
+ stop and say so rather than reporting an update that never ran.
33
+
34
+ 5. Refresh the Impeccable CLI and engine from npm (official package):
19
35
 
20
36
  `npm install -g impeccable`
21
37
  `impeccable install --global --yes --force`
22
38
 
23
- 5. Realign the skill index:
39
+ 6. Realign the skill index:
24
40
 
25
41
  `node <home>/src/cli.ts sync`
26
42
 
27
- 6. Verify with `node <home>/src/cli.ts doctor`: the `Referenced skills` row
43
+ 7. Verify with `node <home>/src/cli.ts doctor`: the `Referenced skills` row
28
44
  must show every pack present (the 7 `eas-*` paid-service skills stay
29
- excluded by design). Report what was reinstalled.
45
+ excluded by design). Report what was installed and what was updated,
46
+ separately — never call a skipped pack an update.
@@ -30,11 +30,11 @@ Do **not** consult it for trivial edits, unrelated questions, or when the prompt
30
30
 
31
31
  Playwright uses a **persistent profile** that preserves data across sessions (cookies, localStorage, sessionStorage, history, logged-in sessions).
32
32
 
33
- **Profile folder:** `C:\Users\hiz\.opencode\playwright-profile`
33
+ **Profile folder:** `~/.opencode/playwright-profile`
34
34
 
35
35
  ### Rules
36
36
  1. **NEVER use Chrome or Chromium** — Use only Microsoft Edge via the Playwright MCP server (`--browser=msedge`). Never launch `chrome.exe`, `chromium`, or any Chromium-based process manually or programmatically. All browser automation goes through the MCP tools (`playwright_browser_*`). If Edge is not installed, fall back to another non-Chromium browser (Firefox, WebKit) via the Playwright MCP server.
37
- 2. Never disable `--user-data-dir` in the Playwright MCP config. The profile must always point to `C:/Users/hiz/.opencode/playwright-profile`.
37
+ 2. Never disable `--user-data-dir` in the Playwright MCP config. The profile must always point to `~/.opencode/playwright-profile`.
38
38
  3. Never purge this folder without explicit user consent.
39
39
  4. Never launch Playwright with an ephemeral context (without user-data-dir) for tasks requiring persistence.
40
40
  5. If the profile is corrupted or causes issues, inform the user and propose a backup before any reset.
@@ -615,6 +615,123 @@ async function run(args: string[], input?: string): Promise<RunResult> {
615
615
  return spawnNode([CLI, ...args], input);
616
616
  }
617
617
 
618
+ // GATE-DAEMON client -----------------------------------------------------------------
619
+ // Measured 2026-10-14: one `run(["gate", ...])` cost 299.1 ms, of which only
620
+ // 0.74 ms is gate computation (loadSpec + evaluateGate) — ~99.6% was process
621
+ // startup, paid on every gated tool call. The daemon keeps one warm process and
622
+ // answers over newline-delimited JSON, cutting that to a pipe write.
623
+ //
624
+ // Fail-closed contract: every failure below is returned as `spawnError`, the
625
+ // exact shape spawnNode uses, so the caller's existing handling (deny with
626
+ // "gate unavailable") applies untouched. There is no path here that returns
627
+ // allow, and the daemon itself caches nothing — it re-runs commandGate, which
628
+ // re-reads rules.json/categories.json/the skill index from disk on each request.
629
+ const DAEMON = join(HOME, "src", "gate-daemon.ts");
630
+ const GATE_TIMEOUT_MS = 20_000;
631
+
632
+ type DaemonChild = ReturnType<typeof spawn>;
633
+ let gateChild: DaemonChild | null = null;
634
+ let gateBuffer = "";
635
+ let gateNextId = 1;
636
+ const gatePending = new Map<number, { resolve: (result: RunResult) => void; timer: ReturnType<typeof setTimeout> }>();
637
+
638
+ function gateFail(reason: string): RunResult {
639
+ return { status: 1, stdout: "", stderr: reason, spawnError: reason };
640
+ }
641
+
642
+ function releaseGatePending(reason: string): void {
643
+ for (const pending of gatePending.values()) {
644
+ clearTimeout(pending.timer);
645
+ pending.resolve(gateFail(reason));
646
+ }
647
+ gatePending.clear();
648
+ }
649
+
650
+ function ensureGateChild(): DaemonChild | null {
651
+ if (gateChild && gateChild.exitCode === null && gateChild.stdin) return gateChild;
652
+ // A dead child must not leave requests hanging: fail them, then respawn.
653
+ releaseGatePending("gate daemon exited before answering");
654
+ gateBuffer = "";
655
+ let child: DaemonChild;
656
+ try {
657
+ child = spawn(NODE, ["--experimental-strip-types", DAEMON], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
658
+ } catch (error) {
659
+ gateChild = null;
660
+ return null;
661
+ }
662
+ child.on("error", (error) => {
663
+ releaseGatePending(`gate daemon error: ${String(error).slice(0, 200)}`);
664
+ });
665
+ child.on("exit", () => {
666
+ if (gateChild === child) gateChild = null;
667
+ releaseGatePending("gate daemon exited before answering");
668
+ });
669
+ if (child.stdin) {
670
+ // A broken pipe is surfaced through the pending request, not as a crash.
671
+ child.stdin.on("error", () => {});
672
+ }
673
+ if (child.stdout) {
674
+ child.stdout.setEncoding("utf8");
675
+ child.stdout.on("data", (chunk: string) => {
676
+ gateBuffer += chunk;
677
+ let newline = gateBuffer.indexOf("\n");
678
+ while (newline >= 0) {
679
+ const line = gateBuffer.slice(0, newline);
680
+ gateBuffer = gateBuffer.slice(newline + 1);
681
+ if (line.trim().length > 0) {
682
+ try {
683
+ const message = JSON.parse(line) as { id?: unknown; status?: unknown; stdout?: unknown; stderr?: unknown };
684
+ const id = typeof message.id === "number" ? message.id : -1;
685
+ const pending = gatePending.get(id);
686
+ if (pending) {
687
+ gatePending.delete(id);
688
+ clearTimeout(pending.timer);
689
+ pending.resolve({
690
+ status: typeof message.status === "number" ? message.status : 1,
691
+ stdout: typeof message.stdout === "string" ? message.stdout : "",
692
+ stderr: typeof message.stderr === "string" ? message.stderr : ""
693
+ });
694
+ }
695
+ } catch {
696
+ // Non-protocol line: ignore. A request that never gets a valid
697
+ // answer times out below and is denied.
698
+ }
699
+ }
700
+ newline = gateBuffer.indexOf("\n");
701
+ }
702
+ });
703
+ }
704
+ gateChild = child;
705
+ return child;
706
+ }
707
+
708
+ async function runGate(args: string[], input?: string): Promise<RunResult> {
709
+ const child = ensureGateChild();
710
+ if (!child || !child.stdin) return gateFail("gate daemon could not start");
711
+ const id = gateNextId++;
712
+ return new Promise<RunResult>((resolve) => {
713
+ const timer = setTimeout(() => {
714
+ gatePending.delete(id);
715
+ // A wedged daemon must not answer a later request out of order.
716
+ try {
717
+ child.kill();
718
+ } catch {
719
+ /* already gone */
720
+ }
721
+ if (gateChild === child) gateChild = null;
722
+ resolve(gateFail("gate daemon timed out"));
723
+ }, GATE_TIMEOUT_MS);
724
+ gatePending.set(id, { resolve, timer });
725
+ try {
726
+ child.stdin?.write(`${JSON.stringify({ id, argv: args, input: input ?? "" })}\n`);
727
+ } catch (error) {
728
+ gatePending.delete(id);
729
+ clearTimeout(timer);
730
+ resolve(gateFail(`gate daemon write failed: ${String(error).slice(0, 200)}`));
731
+ }
732
+ });
733
+ }
734
+
618
735
  type McpCall = { ok: boolean; data: Record<string, unknown> | null; error: string | null };
619
736
 
620
737
  // S-AUTO: tour unique vers le serveur MCP (mode --call). Le plugin installe
@@ -1167,6 +1284,14 @@ async function setup(ctx: Context): Promise<Cleanup> {
1167
1284
 
1168
1285
  ensureProjectMemory(process.cwd());
1169
1286
 
1287
+ // Warm-start the gate daemon so the first gated tool call doesn't pay the
1288
+ // ~300 ms process startup cost. The daemon is fail-closed: if it fails to
1289
+ // start, ensureGateChild returns null and the first gate call will retry
1290
+ // (or deny via the existing fail-closed path). Skipped when the gate is
1291
+ // escaped (NOVAHIZ_GATE=off): runGate is never reached then, so spawning
1292
+ // would only leave an orphan process.
1293
+ if (!DISABLED) ensureGateChild();
1294
+
1170
1295
  // S-AUTO: aides d'auto-capture/auto-lecture — fermetures de session (etat,
1171
1296
  // logs et enforcement restent dans le meme scope que les hooks).
1172
1297
  //
@@ -1705,9 +1830,20 @@ async function setup(ctx: Context): Promise<Cleanup> {
1705
1830
  // the installed index — an unverified name never counts as loaded.
1706
1831
  // H3 stays: failures are surfaced in the log instead of vanishing,
1707
1832
  // with the exit code — a bare empty reason hid real failures.
1708
- const loadResult = await run(["session-load", "--session", event.sessionID, "--skill", name]);
1833
+ // A-RETRY (2026-09-13): a transient failure (observed SQLite
1834
+ // contention) once left a genuinely loaded skill unrecorded, so the
1835
+ // very next identical edit hit the same block again: retry once
1836
+ // before declaring it "not recorded". Recorded state only records
1837
+ // what the agent already holds in memory, and the write is
1838
+ // idempotent (INSERT ... ON CONFLICT DO NOTHING), so a second
1839
+ // attempt cannot loosen anything; when both attempts fail, behaviour
1840
+ // is unchanged - the warning is logged and `loaded` is left alone.
1841
+ let loadResult = await run(["session-load", "--session", event.sessionID, "--skill", name]);
1842
+ if (loadResult.status !== 0) {
1843
+ loadResult = await run(["session-load", "--session", event.sessionID, "--skill", name]);
1844
+ }
1709
1845
  if (loadResult.status !== 0) {
1710
- await log("warn", `session-load failed for skill ${name} (exit ${loadResult.status}), not recorded: ${(loadResult.stderr || loadResult.stdout || "").trim().slice(0, 200)}`);
1846
+ await log("warn", `session-load failed for skill ${name} (exit ${loadResult.status}) after retry, not recorded: ${(loadResult.stderr || loadResult.stdout || "").trim().slice(0, 200)}`);
1711
1847
  } else {
1712
1848
  loaded.add(name);
1713
1849
  }
@@ -1726,7 +1862,9 @@ async function setup(ctx: Context): Promise<Cleanup> {
1726
1862
  }
1727
1863
 
1728
1864
  const categories = categoriesBySession.get(event.sessionID) ?? [];
1729
- const result = await run(
1865
+ // Served by the warm gate daemon; the result shape and every rule below
1866
+ // are unchanged, so a daemon failure still lands on the fail-closed path.
1867
+ const result = await runGate(
1730
1868
  [
1731
1869
  "gate",
1732
1870
  "--tool",
@@ -1851,7 +1989,29 @@ async function setup(ctx: Context): Promise<Cleanup> {
1851
1989
  }
1852
1990
  });
1853
1991
 
1854
- return () => events.abort();
1992
+ return () => {
1993
+ events.abort();
1994
+ // The daemon only exits on stdin end and holds this process's event loop,
1995
+ // so a plugin reload without this teardown would leak one process per
1996
+ // load (observed 2026-10-15: 9 orphans under opencode-cli.exe). Fail-closed
1997
+ // is untouched: if a later session still needs the gate, ensureGateChild
1998
+ // respawns it and a spawn failure still denies.
1999
+ const child = gateChild;
2000
+ gateChild = null;
2001
+ releaseGatePending("plugin unloaded");
2002
+ if (child) {
2003
+ try {
2004
+ child.stdin?.end();
2005
+ } catch {
2006
+ /* pipe already closed */
2007
+ }
2008
+ try {
2009
+ child.kill();
2010
+ } catch {
2011
+ /* already gone */
2012
+ }
2013
+ }
2014
+ };
1855
2015
  }
1856
2016
 
1857
2017
  const novahizPlugin: Plugin = { id: "novahiz-workflow", setup };