novahiz 0.9.8 → 0.10.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.
- package/CHANGELOG.md +97 -0
- package/README.md +6 -6
- package/adapters/opencode/agent/novahiz.md +1 -0
- package/adapters/opencode/commands/novahiz-upgrade.md +1 -1
- package/adapters/opencode/novahiz-plugin.ts +11 -3
- package/catalog/categories.json +209 -2
- package/catalog/rules.json +2 -35
- package/dist/classify.js +5 -1
- package/dist/commands/doctor.js +3 -1
- package/dist/commands/gate.js +36 -1
- package/dist/commands/inspect.js +43 -2
- package/dist/commands/upgrade.js +47 -3
- package/dist/complexity.js +12 -6
- package/dist/db.js +8 -2
- package/dist/gate.js +16 -18
- package/dist/prompt-rewriter.js +1 -0
- package/dist/readonly.js +119 -0
- package/dist/session.js +63 -0
- package/docs/ARCHITECTURE.md +2 -2
- package/docs/CATALOG.md +1 -1
- package/docs/CLASSIFICATION.md +16 -3
- package/docs/GATE.md +7 -15
- package/docs/ROADMAPS.md +2 -2
- package/docs/RULES.md +5 -5
- package/install/install.mjs +53 -2
- package/install/lib.d.mts +36 -0
- package/install/lib.mjs +106 -2
- package/install/uninstall.mjs +5 -8
- package/package.json +1 -1
- package/skills/novahiz-capture/SKILL.md +123 -0
- package/skills/novahiz-capture/scripts/shot-spec.mjs +113 -0
- package/skills/novahiz-gate/SKILL.md +2 -2
- package/skills/novahiz-planner/SKILL.md +1 -1
- package/src/classify.ts +5 -1
- package/src/commands/doctor.ts +3 -1
- package/src/commands/gate.ts +36 -1
- package/src/commands/inspect.ts +44 -3
- package/src/commands/upgrade.ts +50 -3
- package/src/complexity.ts +12 -6
- package/src/db.ts +8 -2
- package/src/gate.ts +16 -16
- package/src/prompt-rewriter.ts +1 -0
- package/src/readonly.ts +123 -0
- package/src/session.ts +99 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,103 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.10.0] - 2026-10-22
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **The gate and the injected block now agree on the pipeline** - `R6-Novahiz`
|
|
13
|
+
required plan/clarify/analyse/implement/converge on every workflow prompt,
|
|
14
|
+
while the roadmap loop only enforced `kind: "skill"`. A lite or Flutter task
|
|
15
|
+
could therefore announce nothing (or five skills) and be blocked for seven.
|
|
16
|
+
`R6` is removed and every non-optional roadmap step that carries
|
|
17
|
+
`requireSkills` is enforced, whatever its kind (`skill`, `edit`, `verify`,
|
|
18
|
+
`advisory`). `R10-security` is removed too: the audit roadmap already names
|
|
19
|
+
`novahiz-security`. `R11-accessibility` is narrowed to `design-ui`. The
|
|
20
|
+
catalogue now holds 13 rules instead of 15.
|
|
21
|
+
- **A trivial follow-up no longer disarms the gate** - the plugin kept the
|
|
22
|
+
classification in memory only. `continue` / `ok`, or a plugin restart, fell
|
|
23
|
+
back to `general/trivial` and the gate allowed every edit with `missing: []`.
|
|
24
|
+
The last non-trivial classification is now persisted in the `sessions` table
|
|
25
|
+
(schema v3 adds the `prompt` and `tier` columns), `classify --session`
|
|
26
|
+
returns the effective classification, and the gate CLI reads that state from
|
|
27
|
+
the database when categories and prompt are empty.
|
|
28
|
+
- **`doctor` reports a schema mismatch** - the schema check hardcoded
|
|
29
|
+
`ok: true`; it now compares `PRAGMA user_version` with `SCHEMA_VERSION`.
|
|
30
|
+
- **The gate no longer blocks provably read-only shell commands** -
|
|
31
|
+
`git status`, `git log`, `git diff`, `ls`, `Get-Content` and their
|
|
32
|
+
equivalents are exempt (`src/readonly.ts`). The gate used to ask for the
|
|
33
|
+
design skills before it would let an agent read a file.
|
|
34
|
+
- **French UI prompts are no longer fail-open** - `ajoute` / `ajouter` now
|
|
35
|
+
score as lite and `implémente` as full, so a French additive request reaches
|
|
36
|
+
the pipeline instead of falling through to trivial. `design-ui` recognises
|
|
37
|
+
`bouton`, `écran`, `connexion`, `formulaire`, `menu`, `champ`, `icône`,
|
|
38
|
+
`carte`, `modal`, `header`, `sidebar` and their English forms. Numeric
|
|
39
|
+
micro-changes (`ajoute 8px de marge`) stay trivial, and debug words
|
|
40
|
+
(`bug`, `error`, `crash`, ...) subtract from `design-ui` so
|
|
41
|
+
`fix the login bug` stays in `debug` instead of dragging the design pipeline
|
|
42
|
+
into a fix.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- The gate CLI falls back to the persisted session state when the caller
|
|
47
|
+
passes no categories and no prompt, instead of letting the decision rest on
|
|
48
|
+
an empty classification.
|
|
49
|
+
- The plugin asks `classify --session` and, for a trivial continuation,
|
|
50
|
+
inherits the previous task's prompt so the tier does not drop back to
|
|
51
|
+
trivial mid-task.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- **Session state persistence** - `src/session.ts` stores the categories, tier
|
|
56
|
+
and prompt of the last non-trivial task, with a TTL that keeps a stale
|
|
57
|
+
session from being mistaken for a continuation.
|
|
58
|
+
- **A read-only allowlist for the gate** - `src/readonly.ts` lists the shell
|
|
59
|
+
commands the gate can prove do not write.
|
|
60
|
+
- **The `novahiz-capture` skill** - reliable screenshot capture for the
|
|
61
|
+
physical Playwright browser, when a plain capture comes back stale, blank
|
|
62
|
+
or cropped.
|
|
63
|
+
- **Three test files** - `tests/session-state.test.ts`, `tests/readonly.test.ts`
|
|
64
|
+
and `tests/gate-alignment.test.ts` pin the session state, the read-only
|
|
65
|
+
exemption and the alignment between the classifier and the gate.
|
|
66
|
+
|
|
67
|
+
## [0.9.9] - 2026-10-16
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- **`novahiz upgrade --apply` now finishes the job it started** - it stopped at
|
|
72
|
+
`npm install -g novahiz@latest`, which refreshes the global package and
|
|
73
|
+
nothing else: `~/.config/novahiz`, the plugin copy and the MCP entrypoints
|
|
74
|
+
stayed on the previous version. It now resyncs the whole installation from
|
|
75
|
+
the fresh package (`install.mjs --yes --clean`) on both channels, so
|
|
76
|
+
`novahiz update` behaves exactly the same. If the update or the resync
|
|
77
|
+
fails, the command stops and reports the error instead of announcing a
|
|
78
|
+
completed upgrade.
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
|
|
82
|
+
- **A cleanup of the previous installation, opt-in** - an update used to leave
|
|
83
|
+
behind whatever it no longer shipped: a renamed skill stayed loadable, a
|
|
84
|
+
removed plugin stayed registered. `install.mjs --clean` now removes the files
|
|
85
|
+
a previous install wrote whose source has disappeared. The only proof of
|
|
86
|
+
ownership is the install manifest (`.novahiz-install.json`, field `created`):
|
|
87
|
+
a skill or a command placed by a pack is not in it, so it cannot be removed.
|
|
88
|
+
The rule "absent from the source" was the tempting one and it is wrong here —
|
|
89
|
+
it would have deleted `impeccable`, which the pack keeps in
|
|
90
|
+
`~/.agents/skills`. Three guards hold: only files under the four installed
|
|
91
|
+
roots, never after a failed step, never without `--clean`. A `.novahiz-bak`
|
|
92
|
+
goes with the file it backed up and never alone. `novahiz.config.json`,
|
|
93
|
+
`project-memory/` and `novahiz.sqlite` sit outside those roots, untouched by
|
|
94
|
+
construction.
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- **`pruneEmptyDirs` never removed anything** - it called
|
|
99
|
+
`rmSync(dir, { recursive: false })`, which throws `ERR_FS_EISDIR` on a
|
|
100
|
+
directory, and the surrounding `catch` swallowed the error. The function
|
|
101
|
+
looked like it worked and silently left every emptied directory behind, for
|
|
102
|
+
`novahiz uninstall` as well. It now uses `recursive: true`, which is safe
|
|
103
|
+
because the directory is already known to be empty.
|
|
104
|
+
|
|
8
105
|
## [0.9.8] - 2026-10-15
|
|
9
106
|
|
|
10
107
|
### Removed
|
package/README.md
CHANGED
|
@@ -125,12 +125,10 @@ flowchart TD
|
|
|
125
125
|
C --> D[R13 : prompt design-ui ou fichier style ?<br/>exiger humanizer + ui-slop + ui-craft]
|
|
126
126
|
C --> F[R3 : le prompt était Supabase ?<br/>exiger novahiz-supabase + postgres]
|
|
127
127
|
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
128
|
|
|
130
|
-
D --> I{Application de la roadmap}
|
|
129
|
+
D --> I{Application de la roadmap<br/>étapes non optionnelles}
|
|
131
130
|
F --> I
|
|
132
131
|
G --> I
|
|
133
|
-
H --> I
|
|
134
132
|
|
|
135
133
|
I --> J{Détection de placeholder<br/>tokens TODO / FIXME / placeholder}
|
|
136
134
|
|
|
@@ -160,18 +158,20 @@ flowchart TD
|
|
|
160
158
|
|-------|------------------|-------|
|
|
161
159
|
| R3-supabase | Un chemin Supabase ou un prompt Supabase | novahiz-supabase, novahiz-postgres |
|
|
162
160
|
| 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
161
|
| R7-assessment | Un prompt d'assessment | novahiz-assess-intake, -research, -define, -shape, -decide |
|
|
165
162
|
| R8-docs | Une édition sous `novahiz-docs/**/*.md` | novahiz-docs |
|
|
166
163
|
| R9-code-review | Un prompt de review ou un fichier de code en review | novahiz-code-review |
|
|
167
|
-
|
|
|
168
|
-
| R11-accessibility | Un prompt design-ui ou audit | novahiz-wcag-audit |
|
|
164
|
+
| R11-accessibility | Un prompt design-ui | novahiz-wcag-audit |
|
|
169
165
|
| R12-web-extract | Un prompt de recherche | novahiz-web-extract |
|
|
170
166
|
| R13-design-craft | Un prompt design-ui ou un fichier style (css/scss/less/html) | novahiz-humanizer, ui-slop-remover, ui-craft-rules |
|
|
171
167
|
| R14-impeccable | Un prompt design-ui ou un fichier style (css/scss/less/html) | impeccable |
|
|
172
168
|
|
|
173
169
|
`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
170
|
|
|
171
|
+
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.
|
|
172
|
+
|
|
173
|
+
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.
|
|
174
|
+
|
|
175
175
|
### Gate reload
|
|
176
176
|
|
|
177
177
|
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.
|
|
@@ -8,6 +8,6 @@ description: Check for a new Novahiz version and update it via npm. Arguments: $
|
|
|
8
8
|
|
|
9
9
|
3. If an update is available, report the installed -> latest versions, then ask the user for explicit approval. Never run the update without that approval (skip this confirmation only when `$ARGUMENTS` contains `--yes`).
|
|
10
10
|
|
|
11
|
-
4. After approval, run `novahiz upgrade --apply` (same CLI used in step 1).
|
|
11
|
+
4. After approval, run `novahiz upgrade --apply` (same CLI used in step 1). It installs `novahiz@latest` globally for npm installs, or pulls with `--ff-only` for source checkouts, then resyncs the Novahiz home and the opencode config from the fresh package and cleans what the previous version left behind. The cleanup only removes files the previous install manifest proves Novahiz wrote and whose source no longer exists: pack skills, `novahiz.config.json`, `project-memory/` and the vault are never touched. If the update or the resync fails, the command stops before the cleanup. Report the exact result, including any npm/git error verbatim — never claim success you did not observe.
|
|
12
12
|
|
|
13
13
|
5. Finish by telling the user to restart OpenCode completely so the new version loads. Offer `novahiz doctor` as the follow-up check.
|
|
@@ -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
|
-
|
|
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/catalog/categories.json
CHANGED
|
@@ -756,7 +756,206 @@
|
|
|
756
756
|
"accessibility",
|
|
757
757
|
"a11y",
|
|
758
758
|
"dashboard",
|
|
759
|
-
|
|
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",
|
package/catalog/rules.json
CHANGED
|
@@ -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
|
|
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"
|
|
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
|
-
|
|
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/commands/doctor.js
CHANGED
|
@@ -691,17 +691,19 @@ export async function commandDoctor(parsed) {
|
|
|
691
691
|
// matches by the time this reads it. The value is read so a future migration
|
|
692
692
|
// has something to branch on, and so a database from an older build shows up.
|
|
693
693
|
let schemaDetail = "database unreadable";
|
|
694
|
+
let schemaOk = false;
|
|
694
695
|
try {
|
|
695
696
|
const db = openDb(dbFile);
|
|
696
697
|
const row = db.prepare("PRAGMA user_version").get();
|
|
697
698
|
const version = Number(row?.user_version ?? 0);
|
|
698
699
|
db.close();
|
|
699
700
|
schemaDetail = `version ${version} (expected ${SCHEMA_VERSION})`;
|
|
701
|
+
schemaOk = version === SCHEMA_VERSION;
|
|
700
702
|
}
|
|
701
703
|
catch (error) {
|
|
702
704
|
schemaDetail = `unreadable: ${error.message}`;
|
|
703
705
|
}
|
|
704
|
-
checks.push({ id: "schema", label: "Schema version", ok:
|
|
706
|
+
checks.push({ id: "schema", label: "Schema version", ok: schemaOk, detail: schemaDetail, blocking: false });
|
|
705
707
|
const adapterSource = join(root, "adapters", "opencode", "novahiz-plugin.ts");
|
|
706
708
|
const opencodeDir = process.env.OPENCODE_CONFIG_DIR && process.env.OPENCODE_CONFIG_DIR.length > 0
|
|
707
709
|
? process.env.OPENCODE_CONFIG_DIR
|
package/dist/commands/gate.js
CHANGED
|
@@ -10,6 +10,8 @@ import { changeText } from "../content.js";
|
|
|
10
10
|
import { classify } from "../classify.js";
|
|
11
11
|
import { enforceLedgerChecks, evaluateGate } from "../gate.js";
|
|
12
12
|
import { extractTargetPaths } from "../targets.js";
|
|
13
|
+
import { isReadOnlyCommand } from "../readonly.js";
|
|
14
|
+
import { isValidSessionId, readSessionClassification } from "../session.js";
|
|
13
15
|
export function commandGate(parsed) {
|
|
14
16
|
const root = NovahizHome();
|
|
15
17
|
let spec;
|
|
@@ -91,7 +93,7 @@ export function commandGate(parsed) {
|
|
|
91
93
|
const single = asString(parsed.flags.file);
|
|
92
94
|
let content = asString(parsed.flags.content);
|
|
93
95
|
// C1: the prompt that triggered the edit drives the complexity tier.
|
|
94
|
-
|
|
96
|
+
let prompt = asString(parsed.flags.prompt);
|
|
95
97
|
let paths;
|
|
96
98
|
if (single.length > 0) {
|
|
97
99
|
paths = [single];
|
|
@@ -109,6 +111,18 @@ export function commandGate(parsed) {
|
|
|
109
111
|
return;
|
|
110
112
|
}
|
|
111
113
|
}
|
|
114
|
+
// P0: a read-only shell command (status/log/diff/ls/...) is not an edit.
|
|
115
|
+
// It is exempted before any prompt rule, category or ledger check runs —
|
|
116
|
+
// the gate's job is to guard writes, not to make `git status` cost five
|
|
117
|
+
// skill loads. Unrecognised or chained commands fall through unchanged.
|
|
118
|
+
if ((tool === "bash" || tool === "shell") && args !== null && typeof args === "object") {
|
|
119
|
+
const record = args;
|
|
120
|
+
const command = typeof record.command === "string" ? record.command : typeof record.cmd === "string" ? record.cmd : "";
|
|
121
|
+
if (isReadOnlyCommand(command)) {
|
|
122
|
+
print({ allow: true, tool, readonly: true, reason: "read-only shell command" });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
112
126
|
paths = extractTargetPaths(tool, args);
|
|
113
127
|
if (content.length === 0)
|
|
114
128
|
content = changeText(tool, args);
|
|
@@ -118,6 +132,27 @@ export function commandGate(parsed) {
|
|
|
118
132
|
process.exitCode = 1;
|
|
119
133
|
return;
|
|
120
134
|
}
|
|
135
|
+
// P0: after a plugin restart the in-memory categories are gone and the
|
|
136
|
+
// caller sends neither categories nor prompt. Read the last non-trivial
|
|
137
|
+
// classification back from the session instead of classifying an empty seed
|
|
138
|
+
// into general/trivial — which allowed every edit with missing: [].
|
|
139
|
+
if (categories.length === 0 && prompt.length === 0 && session.length > 0 && isValidSessionId(session)) {
|
|
140
|
+
let stateDb = null;
|
|
141
|
+
try {
|
|
142
|
+
stateDb = openDb(dbPathFor(root, spec));
|
|
143
|
+
const state = readSessionClassification(stateDb, session);
|
|
144
|
+
if (state) {
|
|
145
|
+
categories = state.categories;
|
|
146
|
+
prompt = state.prompt;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
process.stderr.write(`novahiz: session state read skipped: ${String(error).slice(0, 200)}\n`);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
stateDb?.close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
121
156
|
// Audit 2026-09-25 (P1-3): empty categories no longer fall through to an
|
|
122
157
|
// unscoped evaluation — the CLI classifies the seed itself (same contract as
|
|
123
158
|
// the MCP gate) and fails closed only when classification itself is broken.
|
package/dist/commands/inspect.js
CHANGED
|
@@ -8,6 +8,7 @@ import { buildMcpEntries, enabledProviders, installCommands, pendingInstallComma
|
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
9
|
import { bootstrapFor, coreBootstrap, dependencyReport, missingPrerequisites, reloadPathFromRegistry, toolchainStatus } from "../deps.js";
|
|
10
10
|
import { classify } from "../classify.js";
|
|
11
|
+
import { isContinuation, isValidSessionId, readSessionClassification, writeSessionClassification } from "../session.js";
|
|
11
12
|
import { runScript } from "../exec.js";
|
|
12
13
|
import { activeTask, buildWorkPackets, getTask } from "../ledger.js";
|
|
13
14
|
export function commandCheck() {
|
|
@@ -60,6 +61,38 @@ export function commandClassify(parsed) {
|
|
|
60
61
|
minScore: numberFlag(parsed, "min-score", { min: 0 }),
|
|
61
62
|
maxCategories: numberFlag(parsed, "max-categories", { min: 1, integer: true })
|
|
62
63
|
});
|
|
64
|
+
const session = asString(parsed.flags.session);
|
|
65
|
+
if (!isValidSessionId(session)) {
|
|
66
|
+
print({ prompt: text, ...result });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const db = openDb(dbPathFor(root, spec));
|
|
70
|
+
try {
|
|
71
|
+
const stored = readSessionClassification(db, session);
|
|
72
|
+
// A trivial follow-up ("continue", "ok") inherits the last non-trivial
|
|
73
|
+
// task of this session. Without this the plugin overwrote its categories
|
|
74
|
+
// with general/trivial and the gate allowed every edit (missing: []).
|
|
75
|
+
if (result.tier === "trivial" && stored && isContinuation(stored)) {
|
|
76
|
+
const continued = classify(spec, stored.prompt, {
|
|
77
|
+
minScore: numberFlag(parsed, "min-score", { min: 0 }),
|
|
78
|
+
maxCategories: numberFlag(parsed, "max-categories", { min: 1, integer: true })
|
|
79
|
+
});
|
|
80
|
+
print({ prompt: text, ...continued, continuedFrom: stored.prompt });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (result.tier !== "trivial") {
|
|
84
|
+
writeSessionClassification(db, {
|
|
85
|
+
session,
|
|
86
|
+
categories: result.categories.map((entry) => entry.id),
|
|
87
|
+
requiredSkills: result.enforcedSkills,
|
|
88
|
+
tier: result.tier,
|
|
89
|
+
prompt: text
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
db.close();
|
|
95
|
+
}
|
|
63
96
|
print({ prompt: text, ...result });
|
|
64
97
|
}
|
|
65
98
|
export function commandSkills(parsed) {
|
|
@@ -142,9 +175,17 @@ export function commandSessionState(parsed) {
|
|
|
142
175
|
}
|
|
143
176
|
const db = openDb(dbPathFor(root, spec));
|
|
144
177
|
const loaded = db.prepare("SELECT skill FROM skill_invocations WHERE session_id = ?").all(session).map((row) => row.skill);
|
|
145
|
-
const state = db.prepare("SELECT categories, required_skills FROM sessions WHERE id = ?").get(session);
|
|
178
|
+
const state = db.prepare("SELECT categories, required_skills, prompt, tier, updated_at FROM sessions WHERE id = ?").get(session);
|
|
146
179
|
db.close();
|
|
147
|
-
print({
|
|
180
|
+
print({
|
|
181
|
+
session,
|
|
182
|
+
loaded,
|
|
183
|
+
categories: state ? safeJsonArray(state.categories) : [],
|
|
184
|
+
requiredSkills: state ? safeJsonArray(state.required_skills) : [],
|
|
185
|
+
prompt: state?.prompt ?? "",
|
|
186
|
+
tier: state?.tier ?? null,
|
|
187
|
+
updatedAt: state?.updated_at ?? null
|
|
188
|
+
});
|
|
148
189
|
}
|
|
149
190
|
export function commandCatalog(parsed) {
|
|
150
191
|
const spec = loadSpec();
|