novahiz 0.9.9 → 0.11.0

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 (53) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +22 -17
  3. package/adapters/opencode/agent/novahiz.md +1 -0
  4. package/adapters/opencode/novahiz-plugin.ts +11 -3
  5. package/bin/novahiz.mjs +41 -15
  6. package/catalog/categories.json +209 -2
  7. package/catalog/rules.json +2 -35
  8. package/dist/classify.js +5 -1
  9. package/dist/cli.js +6 -4
  10. package/dist/commands/doctor.js +3 -1
  11. package/dist/commands/gate.js +36 -1
  12. package/dist/commands/inspect.js +73 -6
  13. package/dist/complexity.js +12 -6
  14. package/dist/db.js +8 -2
  15. package/dist/deps.js +47 -3
  16. package/dist/exec.js +1 -1
  17. package/dist/gate.js +16 -18
  18. package/dist/prompt-rewriter.js +1 -0
  19. package/dist/readonly.js +119 -0
  20. package/dist/session.js +63 -0
  21. package/docs/ARCHITECTURE.md +2 -2
  22. package/docs/CATALOG.md +1 -1
  23. package/docs/CLASSIFICATION.md +16 -3
  24. package/docs/GATE.md +7 -15
  25. package/docs/INSTALL.md +28 -3
  26. package/docs/PROVIDERS.md +3 -3
  27. package/docs/ROADMAPS.md +2 -2
  28. package/docs/RULES.md +5 -5
  29. package/install/install.mjs +103 -29
  30. package/install/lib.d.mts +29 -1
  31. package/install/lib.mjs +67 -11
  32. package/package.json +4 -5
  33. package/skills/novahiz-capture/SKILL.md +123 -0
  34. package/skills/novahiz-capture/scripts/shot-spec.mjs +113 -0
  35. package/skills/novahiz-gate/SKILL.md +2 -2
  36. package/skills/novahiz-planner/SKILL.md +1 -1
  37. package/src/classify.ts +5 -1
  38. package/src/cli.ts +6 -4
  39. package/src/commands/doctor.ts +3 -1
  40. package/src/commands/gate.ts +36 -1
  41. package/src/commands/inspect.ts +74 -9
  42. package/src/complexity.ts +12 -6
  43. package/src/db.ts +8 -2
  44. package/src/deps.ts +47 -3
  45. package/src/exec.ts +1 -1
  46. package/src/gate.ts +16 -16
  47. package/src/prompt-rewriter.ts +1 -0
  48. package/src/readonly.ts +123 -0
  49. package/src/session.ts +99 -0
  50. package/bin/veille-mensuelle.ps1 +0 -26
  51. package/install/bootstrap.mjs +0 -324
  52. package/install/install.ps1 +0 -3
  53. package/install/install.sh +0 -4
package/CHANGELOG.md CHANGED
@@ -5,6 +5,109 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.11.0] - 2026-10-22
9
+
10
+ ### Changed
11
+
12
+ - **`npm install -g novahiz` is the whole install instruction** - the installer
13
+ now has two modes and two moments. `--core` is the local half: home, skills,
14
+ plugin, agent, commands, config, catalog. No network, no `sudo`, so it is safe
15
+ from a postinstall, a CI job or an agent with no terminal. `--extras` is the
16
+ network/system half (providers and toolchain, global MCP servers, docs index
17
+ seed, Obsidian vault, npm refresh) and is replayed at the first real `novahiz`
18
+ call. Without a flag the installer still does everything in one pass, so
19
+ `novahiz-install` keeps its historical behaviour.
20
+ - **The first `novahiz` run finishes the setup** - npm 11.16+ blocks install
21
+ scripts unless `allowScripts` covers the package, and a global install has no
22
+ `package.json` to record that approval in. `bin/novahiz.mjs` therefore runs
23
+ `--core` when no manifest exists, then `--extras` in the same invocation, and
24
+ clears `extrasPending`. `NOVAHIZ_NO_EXTRAS=1` and `NOVAHIZ_AUTOINSTALL=0`
25
+ remain the opt-outs.
26
+ - **Linux and macOS compatibility** - `which()` and `commandExists()` scan
27
+ `PATH` (plus `PATHEXT` on Windows) instead of spawning `which`/`where`, which
28
+ Debian minimal and Alpine do not ship. The opencode `shell` setting follows
29
+ `$SHELL` on macOS/Linux (zsh by default on macOS) instead of hardcoding
30
+ `bash`, and keeps the `pwsh`/`powershell` probe on Windows. CI gains an
31
+ `install` job that runs the core installer and the resulting CLI on ubuntu,
32
+ macos and windows.
33
+ - **Nothing privileged is executed any more** - the core toolchain bootstrap no
34
+ longer spawns `sudo apt-get install` or `sh -c "curl … | sh"`. Both are
35
+ printed as the exact manual command (`manual core python - run: …`), so a CI
36
+ job or an agent can no longer hang on a password prompt. `brew` and `winget`
37
+ (structured argv, no privilege escalation) stay executable.
38
+
39
+ ### Removed
40
+
41
+ - **Four redundant entry points** - `install/bootstrap.mjs`,
42
+ `install/install.sh`, `install/install.ps1` and `bin/veille-mensuelle.ps1`,
43
+ together with the `novahiz-bootstrap` bin and the `bootstrap` / `bootstrap:dry`
44
+ scripts. `novahiz-install` is the single installer; `npm run setup:core` and
45
+ `npm run setup:extras` cover the two modes.
46
+
47
+ ### Fixed
48
+
49
+ - **`novahiz --version` printed the usage** - the `command === undefined` branch
50
+ ran before the `--version` flag check.
51
+
52
+ ## [0.10.0] - 2026-10-22
53
+
54
+ ### Fixed
55
+
56
+ - **The gate and the injected block now agree on the pipeline** - `R6-Novahiz`
57
+ required plan/clarify/analyse/implement/converge on every workflow prompt,
58
+ while the roadmap loop only enforced `kind: "skill"`. A lite or Flutter task
59
+ could therefore announce nothing (or five skills) and be blocked for seven.
60
+ `R6` is removed and every non-optional roadmap step that carries
61
+ `requireSkills` is enforced, whatever its kind (`skill`, `edit`, `verify`,
62
+ `advisory`). `R10-security` is removed too: the audit roadmap already names
63
+ `novahiz-security`. `R11-accessibility` is narrowed to `design-ui`. The
64
+ catalogue now holds 13 rules instead of 15.
65
+ - **A trivial follow-up no longer disarms the gate** - the plugin kept the
66
+ classification in memory only. `continue` / `ok`, or a plugin restart, fell
67
+ back to `general/trivial` and the gate allowed every edit with `missing: []`.
68
+ The last non-trivial classification is now persisted in the `sessions` table
69
+ (schema v3 adds the `prompt` and `tier` columns), `classify --session`
70
+ returns the effective classification, and the gate CLI reads that state from
71
+ the database when categories and prompt are empty.
72
+ - **`doctor` reports a schema mismatch** - the schema check hardcoded
73
+ `ok: true`; it now compares `PRAGMA user_version` with `SCHEMA_VERSION`.
74
+ - **The gate no longer blocks provably read-only shell commands** -
75
+ `git status`, `git log`, `git diff`, `ls`, `Get-Content` and their
76
+ equivalents are exempt (`src/readonly.ts`). The gate used to ask for the
77
+ design skills before it would let an agent read a file.
78
+ - **French UI prompts are no longer fail-open** - `ajoute` / `ajouter` now
79
+ score as lite and `implémente` as full, so a French additive request reaches
80
+ the pipeline instead of falling through to trivial. `design-ui` recognises
81
+ `bouton`, `écran`, `connexion`, `formulaire`, `menu`, `champ`, `icône`,
82
+ `carte`, `modal`, `header`, `sidebar` and their English forms. Numeric
83
+ micro-changes (`ajoute 8px de marge`) stay trivial, and debug words
84
+ (`bug`, `error`, `crash`, ...) subtract from `design-ui` so
85
+ `fix the login bug` stays in `debug` instead of dragging the design pipeline
86
+ into a fix.
87
+
88
+ ### Changed
89
+
90
+ - The gate CLI falls back to the persisted session state when the caller
91
+ passes no categories and no prompt, instead of letting the decision rest on
92
+ an empty classification.
93
+ - The plugin asks `classify --session` and, for a trivial continuation,
94
+ inherits the previous task's prompt so the tier does not drop back to
95
+ trivial mid-task.
96
+
97
+ ### Added
98
+
99
+ - **Session state persistence** - `src/session.ts` stores the categories, tier
100
+ and prompt of the last non-trivial task, with a TTL that keeps a stale
101
+ session from being mistaken for a continuation.
102
+ - **A read-only allowlist for the gate** - `src/readonly.ts` lists the shell
103
+ commands the gate can prove do not write.
104
+ - **The `novahiz-capture` skill** - reliable screenshot capture for the
105
+ physical Playwright browser, when a plain capture comes back stale, blank
106
+ or cropped.
107
+ - **Three test files** - `tests/session-state.test.ts`, `tests/readonly.test.ts`
108
+ and `tests/gate-alignment.test.ts` pin the session state, the read-only
109
+ exemption and the alignment between the classifier and the gate.
110
+
8
111
  ## [0.9.9] - 2026-10-16
9
112
 
10
113
  ### Changed
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  17 catégories, 94 skills, 13 gate rules, 8 MCP providers — tout déterministe, tout local, tout JSON.
6
6
 
7
- > **Version 0.9.0** — publiée sur npm (`npm install -g --allow-scripts=novahiz novahiz` installe et configure tout : core, plugin, agent, skills packs et Playwright MCP). `CHANGELOG.md` et `scripts/` sont dans le tarball.
7
+ > **Version 0.11.0** — `npm install -g novahiz` pose le CLI, et le premier `novahiz` lancé termine l'installation (core puis extras). `CHANGELOG.md` et `scripts/` sont dans le tarball.
8
8
 
9
9
  ```
10
10
  ┌──────────────────────────────────────────────────────────────────────────┐
@@ -41,28 +41,33 @@
41
41
 
42
42
  ## Démarrage rapide
43
43
 
44
- ### Option 1 — En une ligne (recommandé)
44
+ ### Option 1 — npm (recommandé)
45
45
 
46
46
  ```bash
47
- npm install -g --allow-scripts=novahiz novahiz
47
+ npm install -g novahiz
48
+ novahiz doctor
48
49
  ```
49
50
 
50
- Une commande, tout est en place : CLI, core (skills, plugin, agent, commands), skills packs et Playwright MCP.
51
+ `npm install -g` pose le CLI. Le premier `novahiz` lancé termine le reste, en deux passes : le **core** (skills, plugin, agent, commands, config, catalogue — local, sans réseau ni sudo), puis les **extras** (providers et toolchain, MCP globaux dont Playwright, vault Obsidian, rafraîchissement npm). Une seule fois.
51
52
 
52
- **Pourquoi ce drapeau ?** npm 11.16+ ne considère plus les scripts de lifecycle (`postinstall`) comme autorisés tant qu'ils ne figurent pas dans `allowScripts`, et une installation globale n'a pas de `package.json` où l'inscrire : npm affiche alors `npm warn allow-scripts novahiz (...)`, et selon la version il saute le script — auquel cas `novahiz-install` n'a jamais tourné et rien n'est configuré. Le drapeau autorise explicitement ce paquet. Replis :
53
+ **Pourquoi au premier lancement et pas dans le postinstall ?** npm 11.16+ ne lance un script de lifecycle que si le paquet y est explicitement autorisé, et une installation globale n'a pas de `package.json` où l'inscrire : npm avertit (`npm warn allow-scripts novahiz`) et saute le script. Le CLI fait donc le même travail à son premier appel, ce qui fonctionne quelle que soit la version de npm. Pour que tout se passe pendant le `npm install` :
53
54
 
54
55
  ```bash
55
- # valeur permanente : `npm install -g novahiz` marche ensuite sans drapeau
56
- npm config set allow-scripts=novahiz --location=user
57
-
58
- # paquet déjà installé sans configuration : une commande suffit
59
- novahiz-install --yes
56
+ npm install -g --allow-scripts=novahiz novahiz
60
57
  ```
61
58
 
62
- npm antérieur à 11.16 exécute les scripts par défaut : `npm install -g novahiz` suffit. Et si le blocage a eu lieu quand même, le premier `novahiz` lancé termine l'installation lui-même (désactivable avec `NOVAHIZ_AUTOINSTALL=0`).
59
+ Variantes :
60
+
61
+ ```bash
62
+ NOVAHIZ_NO_EXTRAS=1 novahiz doctor # core seulement, aucun accès réseau
63
+ NOVAHIZ_AUTOINSTALL=0 novahiz doctor # ni core ni extras
64
+ novahiz-install --extras # rejouer les extras plus tard
65
+ ```
63
66
 
64
67
  > 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
68
 
69
+ > Linux et macOS : les commandes système qui demandent `sudo` ou qui exécutent un script téléchargé ne sont jamais lancées. L'installeur affiche la ligne exacte (`sudo apt-get install -y python3`, `brew install uv`, …) et vous décidez. C'est ce qui rend l'installation utilisable sans terminal interactif.
70
+
66
71
  > 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
72
 
68
73
  Puis vérifiez :
@@ -125,12 +130,10 @@ flowchart TD
125
130
  C --> D[R13 : prompt design-ui ou fichier style ?<br/>exiger humanizer + ui-slop + ui-craft]
126
131
  C --> F[R3 : le prompt était Supabase ?<br/>exiger novahiz-supabase + postgres]
127
132
  C --> G[R4 : le prompt était navigateur ?<br/>exiger novahiz-browser]
128
- C --> H[R6 : prompt de workflow ?<br/>exiger plan/clarify/analyse/implement/converge]
129
133
 
130
- D --> I{Application de la roadmap}
134
+ D --> I{Application de la roadmap<br/>étapes non optionnelles}
131
135
  F --> I
132
136
  G --> I
133
- H --> I
134
137
 
135
138
  I --> J{Détection de placeholder<br/>tokens TODO / FIXME / placeholder}
136
139
 
@@ -160,18 +163,20 @@ flowchart TD
160
163
  |-------|------------------|-------|
161
164
  | R3-supabase | Un chemin Supabase ou un prompt Supabase | novahiz-supabase, novahiz-postgres |
162
165
  | R4-playwright | Une catégorie prompt navigateur, ou un chemin de test navigateur (`**/*.spec.ts`, `**/e2e/**`, `**/playwright/**`, …) | novahiz-browser |
163
- | R6-Novahiz | Un prompt dans une catégorie de workflow | novahiz-plan, -clarify, -analyse, -implement, -converge |
164
166
  | R7-assessment | Un prompt d'assessment | novahiz-assess-intake, -research, -define, -shape, -decide |
165
167
  | R8-docs | Une édition sous `novahiz-docs/**/*.md` | novahiz-docs |
166
168
  | R9-code-review | Un prompt de review ou un fichier de code en review | novahiz-code-review |
167
- | R10-security | Un prompt d'audit ou de sécurité | novahiz-security |
168
- | R11-accessibility | Un prompt design-ui ou audit | novahiz-wcag-audit |
169
+ | R11-accessibility | Un prompt design-ui | novahiz-wcag-audit |
169
170
  | R12-web-extract | Un prompt de recherche | novahiz-web-extract |
170
171
  | R13-design-craft | Un prompt design-ui ou un fichier style (css/scss/less/html) | novahiz-humanizer, ui-slop-remover, ui-craft-rules |
171
172
  | R14-impeccable | Un prompt design-ui ou un fichier style (css/scss/less/html) | impeccable |
172
173
 
173
174
  `novahiz-humanizer` et `ui-slop-remover` ne sont exigées que sur les tâches de design frontend (R13) ; `impeccable` se charge de la même façon (R14) pour garder les playbooks shape, critique, audit, harden et polish accessibles, et la roadmap design-ui porte une étape de vérification `impeccable detect` déterministe avant ship. `novahiz init` et `novahiz doctor` affichent les fichiers de contexte `PRODUCT.md` / `DESIGN.md` d'impeccable en lignes advisory.
174
175
 
176
+ Le pipeline (plan, clarify, analyse, implement, converge) n'est plus une règle : il vient des étapes non optionnelles de la roadmap de la catégorie principale, quel que soit leur `kind` (`skill`, `edit`, `verify`, `advisory`). Une seule source de vérité, donc le bloc affiché et le gate ne peuvent plus diverger sur ce point.
177
+
178
+ Les commandes shell prouvées en lecture seule (`git status`, `git log`, `git diff`, `ls`, `Get-ChildItem`, `Get-Content`, `rg`, `Select-String`, …) sont exemptées avant toute règle ou roadmap. Dès qu'une commande chaîne, redirige, substitue ou porte un flag d'écriture, l'exemption tombe et le gate s'applique normalement.
179
+
175
180
  ### Gate reload
176
181
 
177
182
  Un blocage n'est jamais une impasse. Le refus embarque une recette GATE RELOAD : charger chaque skill nommée via le loader, puis rejouer l'appel exact une fois — aucun outil alternatif, aucune écriture shell, aucun contournement d'édition. Si les mêmes skills sont de nouveau signalées manquantes, le chargement n'a pas été enregistré : exécuter `novahiz doctor`, signaler honnêtement, et s'arrêter. La seule dérogation sanctionnée est `NOVAHIZ_GATE=off` (voir Configuration), déclarée à voix haute.
@@ -2,6 +2,7 @@
2
2
  description: Novahiz primary agent. Classifies the request, loads the required skills, then works under the Novahiz gate.
3
3
  mode: primary
4
4
  temperature: 0.2
5
+ color: "#22c55e"
5
6
  # opencode denies `question` by default. The built-in build and plan agents
6
7
  # re-allow it; a custom agent inherits the denial unless it asks. Without this
7
8
  # block the pipeline cannot ask a clarifying question or validate a plan.
@@ -1633,7 +1633,7 @@ async function setup(ctx: Context): Promise<Cleanup> {
1633
1633
  // P0-B: the prompt travels on stdin. On argv it allowed option
1634
1634
  // injection (--home), broke past the Windows 32k limit, and was
1635
1635
  // readable in the process list.
1636
- const result = await run(["classify", "--stdin"], classifyText);
1636
+ const result = await run(["classify", "--stdin", "--session", sessionID], classifyText);
1637
1637
  if (result.status !== 0) {
1638
1638
  classifyFailedBySession.set(sessionID, `classify exit ${result.status}`);
1639
1639
  await log("warn", `Classify failed (exit ${result.status}), gate tool calls refused for this session: ${result.stderr.trim().slice(0, 200)}`);
@@ -1645,6 +1645,7 @@ async function setup(ctx: Context): Promise<Cleanup> {
1645
1645
  requiredSkills?: string[];
1646
1646
  enforcedSkills?: string[];
1647
1647
  providers?: string[];
1648
+ continuedFrom?: string;
1648
1649
  roadmaps?: { id: string; steps: { label: string; kind: string; requireSkills?: string[] }[] }[];
1649
1650
  };
1650
1651
  try {
@@ -1666,7 +1667,11 @@ async function setup(ctx: Context): Promise<Cleanup> {
1666
1667
  categoriesBySession.set(sessionID, categories);
1667
1668
  // P1-4: the gate takes the last prompt as its tier seed via --prompt even
1668
1669
  // when memory auto is off — set unconditionally, not inside MEMORY_AUTO.
1669
- lastPromptBySession.set(sessionID, text.slice(0, 300));
1670
+ // A trivial follow-up ("continue") inherits the previous task's prompt:
1671
+ // using the literal "continue" here would retier the session as trivial
1672
+ // and skip the roadmap the classify call just restored.
1673
+ const effectivePrompt = parsed.continuedFrom ?? text;
1674
+ lastPromptBySession.set(sessionID, effectivePrompt.slice(0, 300));
1670
1675
  const primary = parsed.primary ?? categories[0] ?? null;
1671
1676
  const enforced = parsed.enforcedSkills ?? [];
1672
1677
  const required = parsed.requiredSkills ?? [];
@@ -1677,6 +1682,9 @@ async function setup(ctx: Context): Promise<Cleanup> {
1677
1682
  "[Novahiz enforcement]",
1678
1683
  `Categories detected: ${categories.join(", ") || "none"}${primary ? ` (primary: ${primary})` : ""}`
1679
1684
  ];
1685
+ if (parsed.continuedFrom) {
1686
+ lines.push(`Continuing the previous task: ${parsed.continuedFrom.slice(0, 160)}`);
1687
+ }
1680
1688
  if (rewrite.sourceLanguage !== "en") {
1681
1689
  lines.push(`User language: ${rewrite.sourceLanguage} — respond in this language, not English.`);
1682
1690
  }
@@ -1699,7 +1707,7 @@ async function setup(ctx: Context): Promise<Cleanup> {
1699
1707
  await log("warn", "Ledger state is invalid JSON, enforcement injected without the task summary");
1700
1708
  }
1701
1709
  }
1702
- lines.push("The gate blocks edit/write/patch/apply_patch/bash/shell until the required skills are loaded via skill({id:\"...\"}).");
1710
+ lines.push("The gate blocks edit/write/patch/apply_patch/bash/shell until the required skills are loaded via skill({id:\"...\"}). Read-only shell commands (status/log/diff/ls/Get-Content/...) are exempt.");
1703
1711
  lines.push("The gate is content-aware: novahiz-humanizer, ui-slop-remover and ui-craft-rules are required only on frontend design tasks (R13), and impeccable on the same design selectors (R14).");
1704
1712
  lines.push("Config edited = opencode restart required (config read at import).");
1705
1713
  lines.push("Memory lives in project-memory/ under the project root (cwd): index.json + fixed-size slots (8000 chars / 200 lines) with compact → archive → new-slot rotation. Use the MCP memory_* tools: memory_search to find relevant slots first, then memory_get to read, memory_write to append, memory_update to correct, memory_archive to retire.");
package/bin/novahiz.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { spawnSync } from "node:child_process";
@@ -21,40 +21,66 @@ const here = dirname(fileURLToPath(import.meta.url));
21
21
  const root = join(here, "..");
22
22
  const underNodeModules = root.replace(/\\/g, "/").toLowerCase().includes("/node_modules/");
23
23
  const dist = join(root, "dist", "cli.js");
24
- const src = join(root, "src", "cli.ts");
24
+ const src = join(root, "src", "cli.ts"); console.error("DEBUG", JSON.stringify({underNodeModules, root, auto: process.env.NOVAHIZ_AUTOINSTALL, noextras: process.env.NOVAHIZ_NO_EXTRAS}));
25
25
 
26
26
  // npm 11.16+ blocks install scripts unless `allowScripts` covers them, and a
27
27
  // global install (`npm install -g`) has no package.json to record that
28
28
  // approval in — so the postinstall can be skipped silently and the machine is
29
- // left half configured. Finishing the job here means the package works
30
- // whether or not npm let the lifecycle script through: the first real `novahiz`
31
- // run installs the core (skills, plugin, agent, MCP), then behaves normally.
32
- // Opt out with NOVAHIZ_AUTOINSTALL=0. Only a package installed from npm is
33
- // touched; a repo checkout keeps its explicit `novahiz-install`.
29
+ // left half configured. The first real `novahiz` run therefore finishes the
30
+ // job, in two passes:
31
+ // 1. no install manifest -> install the core (skills, plugin, agent, config);
32
+ // 2. manifest says extrasPending -> run the network/system extras once
33
+ // (providers, global MCP servers, Obsidian vault, npm refresh).
34
+ // Opt out with NOVAHIZ_AUTOINSTALL=0 (both passes) or NOVAHIZ_NO_EXTRAS=1
35
+ // (extras only). A repo checkout keeps its explicit `novahiz-install`.
36
+ function readManifest(path) {
37
+ try {
38
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
39
+ return parsed && typeof parsed === "object" ? parsed : null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
34
45
  if (underNodeModules && process.env.NOVAHIZ_AUTOINSTALL !== "0") {
35
46
  const args = process.argv.slice(2);
36
47
  const infoOnly = args.length > 0 && args.every((arg) => ["-v", "-V", "--version", "--help", "-h"].includes(arg));
37
48
  const home = process.env.NOVAHIZ_HOME ? resolve(process.env.NOVAHIZ_HOME) : join(homedir(), ".config", "novahiz");
38
- if (!infoOnly && !existsSync(join(home, ".novahiz-install.json"))) {
39
- process.stderr.write(
40
- "novahiz: npm skipped the postinstall script (allow-scripts), so the setup has not run yet.\n" +
41
- "novahiz: installing now — re-run it any time with `novahiz-install`.\n"
42
- );
43
- const setup = spawnSync(process.execPath, [join(root, "install", "install.mjs"), "--yes"], {
49
+ const manifestPath = join(home, ".novahiz-install.json");
50
+ const manifest = readManifest(manifestPath);
51
+ const installer = join(root, "install", "install.mjs");
52
+ const runInstaller = (mode, reason) => {
53
+ process.stderr.write(`novahiz: ${reason}\n`);
54
+ const setup = spawnSync(process.execPath, [installer, "--yes", mode], {
44
55
  stdio: "inherit",
45
56
  cwd: root,
46
57
  env: process.env
47
58
  });
48
59
  if (setup.status !== 0) {
49
- process.stderr.write("novahiz: automatic setup did not complete; run `novahiz-install` to see why.\n");
60
+ process.stderr.write(`novahiz: setup (${mode}) did not complete; re-run it with \`novahiz-install ${mode}\`.\n`);
61
+ }
62
+ return setup.status === 0;
63
+ };
64
+ if (!infoOnly && manifest === null) {
65
+ const coreOk = runInstaller("--core", "first run - installing the core (the npm postinstall was skipped or did not finish).");
66
+ // Meme lancement, deux passes : le premier appel doit tout laisser pret, pas
67
+ // seulement le core. Les extras restent optionnels via NOVAHIZ_NO_EXTRAS=1.
68
+ if (coreOk && process.env.NOVAHIZ_NO_EXTRAS !== "1") {
69
+ runInstaller("--extras", "installing the extras (providers, global MCP servers, Obsidian vault). One time only; skip with NOVAHIZ_NO_EXTRAS=1.");
50
70
  }
71
+ } else if (!infoOnly && manifest?.extrasPending === true && process.env.NOVAHIZ_NO_EXTRAS !== "1") {
72
+ runInstaller("--extras", "finishing the setup installed earlier (providers, global MCP servers, Obsidian vault). One time only; skip with NOVAHIZ_NO_EXTRAS=1.");
51
73
  }
52
74
  }
53
75
 
54
76
  let target;
55
77
  if (underNodeModules) {
56
78
  if (!existsSync(dist)) {
57
- process.stderr.write("novahiz: dist/cli.js is missing from the installed package. Reinstall: npm install novahiz\n");
79
+ process.stderr.write(
80
+ "novahiz: dist/cli.js is missing from this package copy.\n" +
81
+ "novahiz: npm blocked the `prepare` script (allow-scripts). Reinstall with:\n" +
82
+ "novahiz: npm install -g --allow-scripts=novahiz novahiz\n"
83
+ );
58
84
  process.exit(1);
59
85
  }
60
86
  target = dist;
@@ -756,7 +756,206 @@
756
756
  "accessibility",
757
757
  "a11y",
758
758
  "dashboard",
759
- "form",
759
+ {
760
+ "term": "form",
761
+ "weight": 2
762
+ },
763
+ {
764
+ "term": "forms",
765
+ "weight": 2
766
+ },
767
+ {
768
+ "term": "formulaire",
769
+ "weight": 2
770
+ },
771
+ {
772
+ "term": "formulaires",
773
+ "weight": 2
774
+ },
775
+ {
776
+ "term": "bouton",
777
+ "weight": 2
778
+ },
779
+ {
780
+ "term": "boutons",
781
+ "weight": 2
782
+ },
783
+ {
784
+ "term": "button",
785
+ "weight": 2
786
+ },
787
+ {
788
+ "term": "buttons",
789
+ "weight": 2
790
+ },
791
+ {
792
+ "term": "écran",
793
+ "weight": 2
794
+ },
795
+ {
796
+ "term": "screen",
797
+ "weight": 2
798
+ },
799
+ {
800
+ "term": "screens",
801
+ "weight": 2
802
+ },
803
+ {
804
+ "term": "connexion",
805
+ "weight": 1
806
+ },
807
+ {
808
+ "term": "login",
809
+ "weight": 1
810
+ },
811
+ {
812
+ "term": "sign in",
813
+ "weight": 1
814
+ },
815
+ {
816
+ "term": "signin",
817
+ "weight": 1
818
+ },
819
+ {
820
+ "term": "sign up",
821
+ "weight": 1
822
+ },
823
+ {
824
+ "term": "signup",
825
+ "weight": 1
826
+ },
827
+ {
828
+ "term": "inscription",
829
+ "weight": 1
830
+ },
831
+ {
832
+ "term": "mot de passe",
833
+ "weight": 1
834
+ },
835
+ {
836
+ "term": "password",
837
+ "weight": 1
838
+ },
839
+ {
840
+ "term": "champ",
841
+ "weight": 1
842
+ },
843
+ {
844
+ "term": "field",
845
+ "weight": 1
846
+ },
847
+ {
848
+ "term": "input",
849
+ "weight": 1
850
+ },
851
+ {
852
+ "term": "menu",
853
+ "weight": 1
854
+ },
855
+ {
856
+ "term": "icône",
857
+ "weight": 1
858
+ },
859
+ {
860
+ "term": "icon",
861
+ "weight": 1
862
+ },
863
+ {
864
+ "term": "carte",
865
+ "weight": 1
866
+ },
867
+ {
868
+ "term": "card",
869
+ "weight": 1
870
+ },
871
+ {
872
+ "term": "modal",
873
+ "weight": 1
874
+ },
875
+ {
876
+ "term": "dialog",
877
+ "weight": 1
878
+ },
879
+ {
880
+ "term": "dialogue",
881
+ "weight": 1
882
+ },
883
+ {
884
+ "term": "header",
885
+ "weight": 1
886
+ },
887
+ {
888
+ "term": "footer",
889
+ "weight": 1
890
+ },
891
+ {
892
+ "term": "sidebar",
893
+ "weight": 1
894
+ },
895
+ {
896
+ "term": "composant",
897
+ "weight": 1
898
+ },
899
+ {
900
+ "term": "component",
901
+ "weight": 1
902
+ },
903
+ {
904
+ "term": "onglet",
905
+ "weight": 1
906
+ },
907
+ {
908
+ "term": "tab",
909
+ "weight": 1
910
+ },
911
+ {
912
+ "term": "avatar",
913
+ "weight": 1
914
+ },
915
+ {
916
+ "term": "badge",
917
+ "weight": 1
918
+ },
919
+ {
920
+ "term": "tooltip",
921
+ "weight": 1
922
+ },
923
+ {
924
+ "term": "toast",
925
+ "weight": 1
926
+ },
927
+ {
928
+ "term": "snackbar",
929
+ "weight": 1
930
+ },
931
+ {
932
+ "term": "drawer",
933
+ "weight": 1
934
+ },
935
+ {
936
+ "term": "panneau",
937
+ "weight": 1
938
+ },
939
+ {
940
+ "term": "panel",
941
+ "weight": 1
942
+ },
943
+ {
944
+ "term": "accueil",
945
+ "weight": 1
946
+ },
947
+ {
948
+ "term": "welcome",
949
+ "weight": 1
950
+ },
951
+ {
952
+ "term": "home",
953
+ "weight": 1
954
+ },
955
+ {
956
+ "term": "homepage",
957
+ "weight": 1
958
+ },
760
959
  "onboarding",
761
960
  "frontend design",
762
961
  "ui component",
@@ -773,7 +972,15 @@
773
972
  "negativeKeywords": [
774
973
  "migration",
775
974
  "rls",
776
- "edge function"
975
+ "edge function",
976
+ "bug",
977
+ "error",
978
+ "crash",
979
+ "exception",
980
+ "regression",
981
+ "stacktrace",
982
+ "panic",
983
+ "broken"
777
984
  ],
778
985
  "defaultSkills": [
779
986
  "novahiz-humanizer",
@@ -26,30 +26,6 @@
26
26
  },
27
27
  "require": ["novahiz-browser"]
28
28
  },
29
- {
30
- "id": "R6-Novahiz",
31
- "description": "Load Novahiz pipeline skills for any task triggering the workflow.",
32
- "when": {
33
- "promptCategories": [
34
- "code",
35
- "debug",
36
- "review",
37
- "audit",
38
- "browser",
39
- "design-ui",
40
- "database-supabase",
41
- "docs-writing",
42
- "planning",
43
- "devops",
44
- "data",
45
- "general",
46
- "flutter",
47
- "expo",
48
- "test"
49
- ]
50
- },
51
- "require": ["novahiz-plan", "novahiz-clarify", "novahiz-analyse", "novahiz-implement", "novahiz-converge"]
52
- },
53
29
  {
54
30
  "id": "R7-assessment",
55
31
  "description": "Load Idea Assessment pipeline skills.",
@@ -80,21 +56,12 @@
80
56
  },
81
57
  "require": ["novahiz-code-review"]
82
58
  },
83
- {
84
- "id": "R10-security",
85
- "description": "Load novahiz-security on any security audit or hardening task.",
86
- "when": {
87
- "match": "any",
88
- "promptCategories": ["audit"]
89
- },
90
- "require": ["novahiz-security"]
91
- },
92
59
  {
93
60
  "id": "R11-accessibility",
94
- "description": "Load novahiz-wcag-audit on design-ui work and on audit prompts, where accessibility is in scope.",
61
+ "description": "Load novahiz-wcag-audit on design-ui work, where accessibility is in scope.",
95
62
  "when": {
96
63
  "match": "any",
97
- "promptCategories": ["design-ui", "audit"],
64
+ "promptCategories": ["design-ui"],
98
65
  "pathExcludes": ["**/tests/**", "**/__tests__/**", "**/*.test.*", "**/*.spec.*"]
99
66
  },
100
67
  "require": ["novahiz-wcag-audit"]
package/dist/classify.js CHANGED
@@ -58,7 +58,11 @@ function skillsOfCategory(category) {
58
58
  function enforcedOfCategory(category) {
59
59
  const skills = [];
60
60
  for (const step of category.roadmap?.steps ?? []) {
61
- if (step.kind !== "skill" || step.optional)
61
+ // Every non-optional step counts, whatever its kind: `edit:implement` and
62
+ // `verify:converge` carry skills too. Restricting to kind "skill" made
63
+ // lite/debug roadmaps advertise nothing while the gate demanded R6's
64
+ // implement/converge — the block and the gate disagreed.
65
+ if (step.optional)
62
66
  continue;
63
67
  for (const skill of step.requireSkills ?? []) {
64
68
  if (!skills.includes(skill))
package/dist/cli.js CHANGED
@@ -121,14 +121,16 @@ async function main(argv) {
121
121
  process.env.NOVAHIZ_HOME = resolve(expandHome(parsed.flags.home));
122
122
  }
123
123
  const command = parsed.positionals[0];
124
- if (command === undefined || command === "help" || command === "--help" || command === "-h") {
125
- usage();
126
- return;
127
- }
124
+ // `--version` sans commande tombe sur `command === undefined` et affichait
125
+ // l'aide : le test du drapeau doit passer avant celui de la commande.
128
126
  if (parsed.flags.version || parsed.flags.v) {
129
127
  printVersion();
130
128
  return;
131
129
  }
130
+ if (command === undefined || command === "help" || command === "--help" || command === "-h") {
131
+ usage();
132
+ return;
133
+ }
132
134
  switch (command) {
133
135
  case "init":
134
136
  return commandInit(parsed);