novahiz 0.2.2 → 0.2.3
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/README.md +355 -355
- package/adapters/opencode/commands/novahiz-init.md +10 -0
- package/adapters/opencode/novahiz.ts +181 -3
- package/docs/CONFIGURATION.md +2 -2
- package/docs/PLUGIN.md +24 -6
- package/docs/RULES.md +1 -1
- package/install/install.mjs +2 -0
- package/mcp/novahiz-tools/index.mjs +35 -6
- package/package.json +6 -6
- package/skills/novahiz-gate/SKILL.md +1 -1
- package/skills/novahiz-init/SKILL.md +112 -0
- package/src/autodocs.ts +192 -0
- package/src/cli.ts +8 -1
- package/src/commands/autodocs.ts +250 -0
- package/src/commands/doctor.ts +226 -217
- package/src/commands/init.ts +356 -0
- package/src/commands/task.ts +20 -7
- package/src/ledger.ts +23 -0
- package/src/targets.ts +43 -7
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Initialize Novahiz in this project (memory, docs, analysis, reviewed cleanup)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Load the `novahiz-init` skill and follow its pipeline from the current working directory.
|
|
6
|
+
|
|
7
|
+
1. Run `novahiz init --dry-run --json` and show the plan.
|
|
8
|
+
2. Unless the user only wanted a preview, run `novahiz init --json`.
|
|
9
|
+
3. Deep-read the project (`novahiz-analyse`), fill `novahiz-docs/`, seed memory.
|
|
10
|
+
4. Offer cleanup only after listing candidates; apply solely on explicit consent.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
5
|
+
import { join, relative, resolve } from "node:path";
|
|
6
6
|
|
|
7
|
-
// Inlined from src/prompt-rewriter.ts
|
|
7
|
+
// Inlined from src/prompt-rewriter.ts — the installed plugin lives in
|
|
8
|
+
// ~/.config/opencode/plugins/ and cannot resolve ../../src/*.
|
|
8
9
|
type RewriteResult = { original: string; rewritten: string; sourceLanguage: string; wasRewritten: boolean };
|
|
9
10
|
|
|
10
11
|
function detectLanguage(prompt: string): string {
|
|
@@ -50,6 +51,138 @@ function rewritePrompt(prompt: string): RewriteResult {
|
|
|
50
51
|
return { original: prompt, rewritten: r, sourceLanguage, wasRewritten: true };
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// Inlined from src/autodocs.ts — same reason as prompt-rewriter: relative
|
|
55
|
+
// imports to ../../src break once the plugin is copied into opencode's plugins/.
|
|
56
|
+
const NOVAHIZ_DIR = ".novahiz";
|
|
57
|
+
const STATE_NAME = "state.json";
|
|
58
|
+
const CONFIG_NAME = "config.json";
|
|
59
|
+
|
|
60
|
+
type AutoDocsState = {
|
|
61
|
+
dirty: boolean;
|
|
62
|
+
pending: string[];
|
|
63
|
+
lastSync: string | null;
|
|
64
|
+
sessions: number;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const EMPTY_STATE: AutoDocsState = { dirty: false, pending: [], lastSync: null, sessions: 0 };
|
|
68
|
+
const MAJOR_DIRS = /^(src|lib|app|routes|pages|api|server|internal|pkg|cmd)\//;
|
|
69
|
+
const MAJOR_FILES = new Set([
|
|
70
|
+
"package.json",
|
|
71
|
+
"pyproject.toml",
|
|
72
|
+
"Cargo.toml",
|
|
73
|
+
"go.mod",
|
|
74
|
+
"composer.json",
|
|
75
|
+
"Gemfile"
|
|
76
|
+
]);
|
|
77
|
+
const MAJOR_EXT = new Set([
|
|
78
|
+
".ts",
|
|
79
|
+
".tsx",
|
|
80
|
+
".js",
|
|
81
|
+
".jsx",
|
|
82
|
+
".mjs",
|
|
83
|
+
".cjs",
|
|
84
|
+
".py",
|
|
85
|
+
".rs",
|
|
86
|
+
".go",
|
|
87
|
+
".php",
|
|
88
|
+
".rb",
|
|
89
|
+
".java",
|
|
90
|
+
".kt",
|
|
91
|
+
".swift",
|
|
92
|
+
".dart",
|
|
93
|
+
".sql"
|
|
94
|
+
]);
|
|
95
|
+
const SKIP_DIRS = new Set([
|
|
96
|
+
"node_modules",
|
|
97
|
+
".git",
|
|
98
|
+
"project-memory",
|
|
99
|
+
"novahiz-docs",
|
|
100
|
+
".novahiz",
|
|
101
|
+
"dist",
|
|
102
|
+
"build",
|
|
103
|
+
"coverage",
|
|
104
|
+
".next"
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
function projectDir(cwd: string): string {
|
|
108
|
+
return join(cwd, NOVAHIZ_DIR);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function configPath(cwd: string): string {
|
|
112
|
+
return join(projectDir(cwd), CONFIG_NAME);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function statePath(cwd: string): string {
|
|
116
|
+
return join(projectDir(cwd), STATE_NAME);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ensureNovahizDir(path: string): void {
|
|
120
|
+
if (!existsSync(path)) mkdirSync(path, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function autoDocsEnabled(cwd: string): boolean {
|
|
124
|
+
const escape = (process.env.NOVAHIZ_AUTODOCS ?? "").toLowerCase();
|
|
125
|
+
if (["off", "0", "false", "no", "disabled"].includes(escape)) return false;
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(readFileSync(configPath(cwd), "utf8")) as { autoDocs?: unknown };
|
|
128
|
+
return parsed !== null && typeof parsed === "object" && parsed.autoDocs === true;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function readState(cwd: string): AutoDocsState {
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(readFileSync(statePath(cwd), "utf8")) as Partial<AutoDocsState>;
|
|
137
|
+
if (!parsed || typeof parsed !== "object") return { ...EMPTY_STATE };
|
|
138
|
+
return {
|
|
139
|
+
dirty: parsed.dirty === true,
|
|
140
|
+
pending: Array.isArray(parsed.pending)
|
|
141
|
+
? parsed.pending.filter((entry): entry is string => typeof entry === "string").slice(0, 64)
|
|
142
|
+
: [],
|
|
143
|
+
lastSync: typeof parsed.lastSync === "string" ? parsed.lastSync : null,
|
|
144
|
+
sessions: typeof parsed.sessions === "number" && Number.isFinite(parsed.sessions) ? parsed.sessions : 0
|
|
145
|
+
};
|
|
146
|
+
} catch {
|
|
147
|
+
return { ...EMPTY_STATE };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function writeState(cwd: string, state: AutoDocsState): void {
|
|
152
|
+
ensureNovahizDir(projectDir(cwd));
|
|
153
|
+
writeFileSync(statePath(cwd), `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function normalizePath(filePath: string): string {
|
|
157
|
+
return filePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isMajorPath(filePath: string): boolean {
|
|
161
|
+
const path = normalizePath(filePath);
|
|
162
|
+
const parts = path.split("/");
|
|
163
|
+
if (parts.some((part) => SKIP_DIRS.has(part))) return false;
|
|
164
|
+
const base = parts[parts.length - 1] ?? "";
|
|
165
|
+
if (MAJOR_FILES.has(base)) return true;
|
|
166
|
+
const dot = base.lastIndexOf(".");
|
|
167
|
+
const ext = dot >= 0 ? base.slice(dot).toLowerCase() : "";
|
|
168
|
+
if (MAJOR_EXT.has(ext)) return true;
|
|
169
|
+
return MAJOR_DIRS.test(path);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function markDirty(cwd: string, filePath: string): AutoDocsState {
|
|
173
|
+
const path = normalizePath(filePath);
|
|
174
|
+
const state = readState(cwd);
|
|
175
|
+
const pending = state.pending.includes(path) ? state.pending : [...state.pending, path].slice(-64);
|
|
176
|
+
const next: AutoDocsState = {
|
|
177
|
+
...state,
|
|
178
|
+
dirty: true,
|
|
179
|
+
pending,
|
|
180
|
+
sessions: state.sessions + 1
|
|
181
|
+
};
|
|
182
|
+
writeState(cwd, next);
|
|
183
|
+
return next;
|
|
184
|
+
}
|
|
185
|
+
|
|
53
186
|
function ensureProjectMemory(cwd: string): boolean {
|
|
54
187
|
try {
|
|
55
188
|
const root = join(cwd, "project-memory");
|
|
@@ -219,6 +352,28 @@ export const NovahizPlugin: Plugin = async ({ client }) => {
|
|
|
219
352
|
|
|
220
353
|
event: async ({ event }) => {
|
|
221
354
|
const type = (event as { type?: string }).type ?? "";
|
|
355
|
+
if (type === "session.idle") {
|
|
356
|
+
// Fail-open: never block idle; skip when disabled or nothing pending.
|
|
357
|
+
try {
|
|
358
|
+
const cwd = process.cwd();
|
|
359
|
+
if (autoDocsEnabled(cwd)) {
|
|
360
|
+
const state = readState(cwd);
|
|
361
|
+
if (state.dirty || state.pending.length > 0) {
|
|
362
|
+
const child = spawn(NODE, [CLI, "autodocs", "--flush"], {
|
|
363
|
+
cwd,
|
|
364
|
+
stdio: "ignore",
|
|
365
|
+
timeout: RUN_TIMEOUT_MS,
|
|
366
|
+
windowsHide: true
|
|
367
|
+
});
|
|
368
|
+
child.on("error", () => undefined);
|
|
369
|
+
child.unref();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
// fail-open
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
222
377
|
if (type !== "session.deleted") return;
|
|
223
378
|
const properties = (event as { properties?: { info?: { id?: string }; sessionID?: string } }).properties ?? {};
|
|
224
379
|
const sessionID = properties.info?.id ?? properties.sessionID;
|
|
@@ -395,6 +550,29 @@ export const NovahizPlugin: Plugin = async ({ client }) => {
|
|
|
395
550
|
await log("warn", `Gate error, denying the tool call as precaution: ${String(error)}`);
|
|
396
551
|
throw new Error(`Novahiz gate blocked ${input.tool}: internal gate error. Set NOVAHIZ_GATE=off to disable.`);
|
|
397
552
|
}
|
|
553
|
+
},
|
|
554
|
+
|
|
555
|
+
"tool.execute.after": async (input) => {
|
|
556
|
+
// Fail-open: mark major paths only; never break the tool result.
|
|
557
|
+
try {
|
|
558
|
+
const tool = input.tool.toLowerCase();
|
|
559
|
+
if (!["edit", "write", "patch", "apply_patch"].includes(tool)) return;
|
|
560
|
+
const args = (input.args ?? {}) as Record<string, unknown>;
|
|
561
|
+
const raw =
|
|
562
|
+
(typeof args.filePath === "string" && args.filePath) ||
|
|
563
|
+
(typeof args.file_path === "string" && args.file_path) ||
|
|
564
|
+
(typeof args.path === "string" && args.path) ||
|
|
565
|
+
"";
|
|
566
|
+
if (!raw) return;
|
|
567
|
+
const cwd = process.cwd();
|
|
568
|
+
const abs = resolve(cwd, raw);
|
|
569
|
+
const rel = relative(cwd, abs).replace(/\\/g, "/");
|
|
570
|
+
if (!rel || rel.startsWith("..")) return;
|
|
571
|
+
if (!isMajorPath(rel)) return;
|
|
572
|
+
markDirty(cwd, rel);
|
|
573
|
+
} catch {
|
|
574
|
+
// fail-open
|
|
575
|
+
}
|
|
398
576
|
}
|
|
399
577
|
};
|
|
400
578
|
};
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -61,7 +61,7 @@ These files live in `catalog/` and are part of the git repository:
|
|
|
61
61
|
| `skillRoots` | `["./skills", "./bundled-skills"]` | Directories to scan for skills |
|
|
62
62
|
| `gate.enabled` | `true` | Enable/disable the gate |
|
|
63
63
|
| `gate.mode` | `block` | `block`, `warn`, or `audit` |
|
|
64
|
-
| `gate.envEscape` | `NOVAHIZ_GATE` |
|
|
64
|
+
| `gate.envEscape` | `NOVAHIZ_GATE` | Schema field only. The kill-switch name is hardcoded to `NOVAHIZ_GATE` in the CLI, MCP gate, and plugin; a config value cannot redirect it. |
|
|
65
65
|
| `gate.tools` | `[edit, write, patch, ...]` | Tools to intercept |
|
|
66
66
|
| `gate.placeholders` | `true` | Block edits with placeholder markers |
|
|
67
67
|
| `classify.minScore` | `1` | Minimum score to match a category |
|
|
@@ -79,7 +79,7 @@ These files live in `catalog/` and are part of the git repository:
|
|
|
79
79
|
| Variable | Purpose |
|
|
80
80
|
|----------|---------|
|
|
81
81
|
| `NOVAHIZ_HOME` | Override Novahiz home directory |
|
|
82
|
-
| `NOVAHIZ_GATE` | Set to `off` to disable the gate |
|
|
82
|
+
| `NOVAHIZ_GATE` | Set to `off` to disable the gate (hardcoded name; not configurable) |
|
|
83
83
|
| `NOVAHIZ_NODE` | Override node executable path |
|
|
84
84
|
| `NOVAHIZ_DB` | Override database path |
|
|
85
85
|
| `OPENCODE_CONFIG_DIR` | Override opencode config directory |
|
package/docs/PLUGIN.md
CHANGED
|
@@ -13,13 +13,17 @@ The opencode adapter is a thin plugin that bridges the Novahiz core with the ope
|
|
|
13
13
|
│ 3. Check DISABLED flag (env escape or config) │
|
|
14
14
|
│ 4. Register hooks: │
|
|
15
15
|
│ • config → inject MCP server + providers │
|
|
16
|
-
│ • event →
|
|
16
|
+
│ • event → session.deleted cleanup; │
|
|
17
|
+
│ session.idle → flush autodocs (fail-open) │
|
|
17
18
|
│ • chat.message → classify prompt, build enforcement │
|
|
18
19
|
│ • experimental.chat.system.transform → inject into prompt│
|
|
19
20
|
│ • tool.execute.before → gate check on edits │
|
|
21
|
+
│ • tool.execute.after → mark major path dirty (autodocs) │
|
|
20
22
|
└─────────────────────────────────────────────────────────────┘
|
|
21
23
|
```
|
|
22
24
|
|
|
25
|
+
The plugin is self-contained: prompt rewriting and autodocs helpers are inlined so the installed copy under `~/.config/opencode/plugins/` does not depend on `../../src/*` (that path does not resolve after install).
|
|
26
|
+
|
|
23
27
|
## Hooks
|
|
24
28
|
|
|
25
29
|
### `config`
|
|
@@ -28,8 +32,8 @@ Registers the Novahiz MCP server and any additional providers from `catalog/prov
|
|
|
28
32
|
|
|
29
33
|
```typescript
|
|
30
34
|
config: async (input) => {
|
|
31
|
-
// Register Novahiz MCP server
|
|
32
|
-
config.mcp.
|
|
35
|
+
// Register Novahiz MCP server (key is lowercase: config.mcp.novahiz)
|
|
36
|
+
config.mcp.novahiz = {
|
|
33
37
|
type: "local",
|
|
34
38
|
command: [NODE, join(HOME, "mcp", "novahiz-tools", "index.mjs")],
|
|
35
39
|
enabled: true
|
|
@@ -108,10 +112,23 @@ The core enforcement hook. Intercepts tool calls and runs the gate:
|
|
|
108
112
|
│ Gate output: │
|
|
109
113
|
│ • exit 0 → allow │
|
|
110
114
|
│ • exit 2 → BLOCK (throw error with missing skills list) │
|
|
111
|
-
│ • exit != 0 →
|
|
115
|
+
│ • exit != 0 → BLOCK (fail-closed, same as spawn error) │
|
|
112
116
|
└─────────────────────────────────────────────────────────────┘
|
|
113
117
|
```
|
|
114
118
|
|
|
119
|
+
### `event`
|
|
120
|
+
|
|
121
|
+
Two event types:
|
|
122
|
+
|
|
123
|
+
| Event | Behavior |
|
|
124
|
+
|-------|----------|
|
|
125
|
+
| `session.deleted` | Forget in-memory session state |
|
|
126
|
+
| `session.idle` | If autodocs is enabled and state is dirty, spawn `novahiz autodocs --flush` (fail-open, unref'd child) |
|
|
127
|
+
|
|
128
|
+
### `tool.execute.after`
|
|
129
|
+
|
|
130
|
+
After an edit-like tool succeeds, if the path is a major source file (`src/…`, `package.json`, `.ts`, …), call the inlined `markDirty` so the next `session.idle` can flush docs. Non-edit tools and non-major paths are ignored. Never throws.
|
|
131
|
+
|
|
115
132
|
## Session state
|
|
116
133
|
|
|
117
134
|
The plugin maintains per-session state in memory:
|
|
@@ -145,6 +162,7 @@ When disabled, all hooks return early without doing anything.
|
|
|
145
162
|
## Error handling
|
|
146
163
|
|
|
147
164
|
- If `classify` fails → warning logged, no enforcement injected
|
|
148
|
-
- If `gate` fails with spawn error → tool call
|
|
149
|
-
- If `gate` exits non-zero non-two → tool call
|
|
165
|
+
- If `gate` fails with spawn error → tool call **blocked** (fail-closed)
|
|
166
|
+
- If `gate` exits non-zero non-two → tool call **blocked** (fail-closed)
|
|
150
167
|
- If `gate` exits 2 → error thrown, tool call blocked
|
|
168
|
+
- Autodocs flush on `session.idle` and `tool.execute.after` are fail-open
|
package/docs/RULES.md
CHANGED
|
@@ -75,7 +75,7 @@ The `gate` block in `novahiz.config.json` controls behavior:
|
|
|
75
75
|
|
|
76
76
|
- `enabled`: disable the whole gate.
|
|
77
77
|
- `mode`: `block`, `warn`, or `audit`.
|
|
78
|
-
- `envEscape`:
|
|
78
|
+
- `envEscape`: schema field only. The kill-switch variable is hardcoded to `NOVAHIZ_GATE` (not configurable); values `off`, `0`, `false`, `no`, `disabled` disable the gate. Read by the CLI, the hook mode, the MCP gate tool, and the opencode plugin.
|
|
79
79
|
- `tools`: the tool names the gate intercepts.
|
|
80
80
|
- `ignoreFiles`: globs skipped by the gate.
|
|
81
81
|
|
package/install/install.mjs
CHANGED
|
@@ -46,6 +46,8 @@ function defaultConfig(skillsDir) {
|
|
|
46
46
|
gate: {
|
|
47
47
|
enabled: true,
|
|
48
48
|
mode: "block",
|
|
49
|
+
// Kept for schema compatibility only — the kill-switch name is hardcoded
|
|
50
|
+
// to NOVAHIZ_GATE in the CLI, MCP gate, and plugin (see src/spec.ts).
|
|
49
51
|
envEscape: "NOVAHIZ_GATE",
|
|
50
52
|
tools: ["edit", "write", "patch", "apply_patch", "bash", "shell"]
|
|
51
53
|
},
|
|
@@ -11,7 +11,7 @@ import { rankSkills } from "../../src/relevance.ts";
|
|
|
11
11
|
import { openDb } from "../../src/db.ts";
|
|
12
12
|
import { enabledProviders } from "../../src/providers.ts";
|
|
13
13
|
import { checkDependencies } from "../../src/deps.ts";
|
|
14
|
-
import { activeTask, addTodos, amendTodo, blockTodo, buildWorkPackets, completeTodo, createTask, dropTodo, getTask, getTodo, insertTodo, ledgerSummary, listTodos, recordTodoDone, reorderTodos, resume, reviewDue, reviewTask, revisionSignals, startTodo } from "../../src/ledger.ts";
|
|
14
|
+
import { activeTask, addTodos, amendTodo, blockTodo, buildWorkPackets, completeTodo, createTask, dropTask, dropTodo, getTask, getTodo, insertTodo, ledgerSummary, listTodos, recordTodoDone, reorderTodos, resume, reviewDue, reviewTask, revisionSignals, startTodo } from "../../src/ledger.ts";
|
|
15
15
|
import { DEFAULT_LIMIT_CHARS, DEFAULT_LIMIT_LINES, ensureMemoryRoot, getSlot, listSlots, memoryRoot, parseSlotInput, rebuildIndex, writeEntry } from "../../src/memory.ts";
|
|
16
16
|
|
|
17
17
|
const SUPPORTED_PROTOCOLS = ["2024-11-05", "2025-06-18"];
|
|
@@ -63,7 +63,8 @@ const TOOLS = [
|
|
|
63
63
|
filePath: { type: "string", description: "Alias of file. Some harnesses rename the parameter when they surface the tool." },
|
|
64
64
|
tool: { type: "string", description: "edit, write or patch." },
|
|
65
65
|
content: { type: "string", description: "The edited content, used for content-aware rules." },
|
|
66
|
-
|
|
66
|
+
prompt: { type: "string", description: "Optional prompt used to auto-classify when categories is omitted or empty." },
|
|
67
|
+
categories: { type: "array", items: { type: "string" }, description: "Category ids. When omitted or empty, inferred from prompt, content, or file path." },
|
|
67
68
|
loaded: { type: "array", items: { type: "string" } }
|
|
68
69
|
}
|
|
69
70
|
}
|
|
@@ -118,7 +119,7 @@ const TOOLS = [
|
|
|
118
119
|
description: "What to do with the ledger."
|
|
119
120
|
},
|
|
120
121
|
title: { type: "string", description: "Task title for action new." },
|
|
121
|
-
id: { type: "string", description: "Task id (new) or todo id (start, done, block)." },
|
|
122
|
+
id: { type: "string", description: "Task id (new) or todo/task id (start, done, block, drop)." },
|
|
122
123
|
task: { type: "string", description: "Task id. Defaults to the active task." },
|
|
123
124
|
session: { type: "string", description: "Session id used to scope the active task." },
|
|
124
125
|
label: { type: "string", description: "Todo label for action todo." },
|
|
@@ -329,12 +330,34 @@ function callTool(name, args) {
|
|
|
329
330
|
if (args?.tool !== undefined && typeof args.tool !== "string") {
|
|
330
331
|
throw new Error("Invalid params: tool must be a string");
|
|
331
332
|
}
|
|
333
|
+
if (args?.prompt !== undefined && typeof args.prompt !== "string") {
|
|
334
|
+
throw new Error("Invalid params: prompt must be a string");
|
|
335
|
+
}
|
|
336
|
+
if (typeof args?.prompt === "string" && args.prompt.length > MAX_PROMPT_LEN) {
|
|
337
|
+
throw new Error(`Invalid params: prompt exceeds ${MAX_PROMPT_LEN} characters`);
|
|
338
|
+
}
|
|
339
|
+
// Auto-classify when categories is omitted or empty: seed from prompt,
|
|
340
|
+
// fall back to content, then file path. Explicit categories always win.
|
|
341
|
+
let categories = args?.categories ? args.categories.map(String) : [];
|
|
342
|
+
if (categories.length === 0) {
|
|
343
|
+
const seed =
|
|
344
|
+
(typeof args?.prompt === "string" && args.prompt.length > 0 ? args.prompt : "") ||
|
|
345
|
+
(typeof args?.content === "string" && args.content.length > 0 ? args.content : "") ||
|
|
346
|
+
file;
|
|
347
|
+
try {
|
|
348
|
+
categories = classify(spec, seed).categories.map((entry) => entry.id);
|
|
349
|
+
} catch {
|
|
350
|
+
// Fail closed on classify errors: keep empty categories so
|
|
351
|
+
// evaluateGate still runs path/content rules instead of crashing.
|
|
352
|
+
categories = [];
|
|
353
|
+
}
|
|
354
|
+
}
|
|
332
355
|
const index = loadInstalledSkills(spec);
|
|
333
356
|
const result = evaluateGate({
|
|
334
357
|
tool: String(args?.tool ?? "edit"),
|
|
335
358
|
filePath: file,
|
|
336
359
|
content: typeof args?.content === "string" ? args.content : "",
|
|
337
|
-
categories
|
|
360
|
+
categories,
|
|
338
361
|
loadedSkills: args?.loaded ? args.loaded.map(String) : [],
|
|
339
362
|
installedSkills: index.skills,
|
|
340
363
|
installedIndexAvailable: index.available,
|
|
@@ -472,8 +495,14 @@ function callTool(name, args) {
|
|
|
472
495
|
const position = raw === undefined || raw === "" ? "end" : /^\d+$/.test(String(raw)) ? Number(raw) : String(raw);
|
|
473
496
|
return toolResult(insertTodo(db, taskId, normalizeTodo(args), position));
|
|
474
497
|
}
|
|
475
|
-
|
|
476
|
-
|
|
498
|
+
if (action === "drop") {
|
|
499
|
+
const dropId = String(args?.id ?? "");
|
|
500
|
+
const reason = args?.reason ? String(args.reason) : "";
|
|
501
|
+
if (getTask(db, dropId)) return toolResult({ task: dropTask(db, dropId, reason) });
|
|
502
|
+
if (getTodo(db, dropId)) return toolResult({ todo: dropTodo(db, dropId, reason) });
|
|
503
|
+
throw new Error(`unknown id: ${dropId} (expected a task id or todo id)`);
|
|
504
|
+
}
|
|
505
|
+
if (action === "reorder") {
|
|
477
506
|
const taskId = args?.task ? String(args.task) : activeTask(db, session)?.id;
|
|
478
507
|
if (!taskId) return toolResult("no active task", true);
|
|
479
508
|
const order = Array.isArray(args?.order) ? args.order.map(String) : [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "novahiz",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Enforce AI agent skills before every file edit: prompt classifier, skill catalog, and pre-edit gate.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"bin": {
|
|
30
|
-
"novahiz": "
|
|
31
|
-
"novahiz-bootstrap": "
|
|
32
|
-
"novahiz-install": "
|
|
33
|
-
"novahiz-uninstall": "
|
|
34
|
-
"novahiz-mcp": "
|
|
30
|
+
"novahiz": "bin/novahiz.mjs",
|
|
31
|
+
"novahiz-bootstrap": "install/bootstrap.mjs",
|
|
32
|
+
"novahiz-install": "install/install.mjs",
|
|
33
|
+
"novahiz-uninstall": "install/uninstall.mjs",
|
|
34
|
+
"novahiz-mcp": "mcp/novahiz-tools/index.mjs"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=22.18.0"
|
|
@@ -67,7 +67,7 @@ Charger chaque skill manquante avec `skill({name})`. Le chargement est enregistr
|
|
|
67
67
|
|
|
68
68
|
## Contournement
|
|
69
69
|
|
|
70
|
-
Le seul prévu par le code : `
|
|
70
|
+
Le seul prévu par le code : `NOVAHIZ_GATE=off` (ou `0`, `false`, `no`, `disabled`) dans l'environnement. Le nom de la variable est codé en dur (le champ `gate.envEscape` de la config ne le redirige pas). L'utilisateur peut aussi demander explicitement de passer outre. Dans les deux cas, la dérogation se dit à voix haute et se rattrape après coup. Le jugement de l'agent n'est pas un contournement valide.
|
|
71
71
|
|
|
72
72
|
## Anti-patterns
|
|
73
73
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: novahiz-init
|
|
3
|
+
description: |
|
|
4
|
+
novahiz-init bootstraps Novahiz inside a project: memory skeleton, novahiz-docs/,
|
|
5
|
+
deep read of the codebase, documentation fill-in, and a reviewed cleanup list.
|
|
6
|
+
Use when the user says "/novahiz init", "init novahiz", "initialize novahiz in this project",
|
|
7
|
+
"set up project docs and memory", or wants a one-shot project onboarding.
|
|
8
|
+
license: Apache-2.0
|
|
9
|
+
compatibility: opencode
|
|
10
|
+
metadata:
|
|
11
|
+
author: Novahiz
|
|
12
|
+
organization: Novahiz
|
|
13
|
+
version: "1.0.0"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# novahiz-init
|
|
17
|
+
|
|
18
|
+
One entry point to bring Novahiz into the current project: scaffold first, then deepen with an agent read.
|
|
19
|
+
|
|
20
|
+
## When to run
|
|
21
|
+
|
|
22
|
+
- `/novahiz init` or `novahiz init` in the project root
|
|
23
|
+
- A new repo needs docs + `project-memory/`
|
|
24
|
+
- Docs or memory are missing after a clone
|
|
25
|
+
|
|
26
|
+
Do **not** use this to install Novahiz on the machine. That stays `novahiz setup` / `install.mjs`.
|
|
27
|
+
|
|
28
|
+
## Pipeline
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
1. CLI scaffold novahiz init [--dry-run] [--docs-only|--memory-only]
|
|
32
|
+
2. Deep read novahiz-analyse on the real code
|
|
33
|
+
3. Fill docs novahiz-docs templates already created by CLI
|
|
34
|
+
4. Seed memory memory_write a baseline slot from findings
|
|
35
|
+
5. Cleanup review confirm list, then novahiz init --apply --json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 1. Scaffold (CLI, deterministic)
|
|
39
|
+
|
|
40
|
+
From the project root:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
novahiz init --dry-run --json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Review the plan, then run without `--dry-run` unless the user only wanted a preview.
|
|
47
|
+
|
|
48
|
+
Flags:
|
|
49
|
+
|
|
50
|
+
| Flag | Effect |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `--dry-run` | Print steps and cleanup candidates, write nothing |
|
|
53
|
+
| `--docs-only` | Only `novahiz-docs/` |
|
|
54
|
+
| `--memory-only` | Only `project-memory/` |
|
|
55
|
+
| `--no-seed` | Create memory root without the baseline slot |
|
|
56
|
+
| `--apply` | Delete the listed cleanup candidates (never without review) |
|
|
57
|
+
| `--json` | Machine-readable result |
|
|
58
|
+
|
|
59
|
+
The CLI creates:
|
|
60
|
+
|
|
61
|
+
- `project-memory/index.json` + `slots/` (plus one baseline slot unless `--no-seed`)
|
|
62
|
+
- `novahiz-docs/{ARCHITECTURE,CONVENTIONS,DECISIONS,STANDARDS}.md` from the `novahiz-docs` templates when the folder is absent
|
|
63
|
+
- A cleanup candidate list (logs, `*.orig`, `*.novahiz-bak`, …). Nothing is deleted without `--apply`.
|
|
64
|
+
|
|
65
|
+
### 2. Deep read
|
|
66
|
+
|
|
67
|
+
Load `novahiz-analyse` and walk manifests, entry points, and data flow. Every claim cites a path. This step is agent work; the CLI does not invent architecture.
|
|
68
|
+
|
|
69
|
+
### 3. Fill documentation
|
|
70
|
+
|
|
71
|
+
Edit only the four files under `novahiz-docs/`:
|
|
72
|
+
|
|
73
|
+
- Replace HTML comment placeholders with observed facts
|
|
74
|
+
- Keep DECISIONS.md empty until a real decision exists
|
|
75
|
+
- If older docs live in `README.md` or `docs/`, migrate unique facts into the right file, then leave a pointer in the README. Do not delete the README.
|
|
76
|
+
|
|
77
|
+
Load `novahiz-humanizer` before writing prose.
|
|
78
|
+
|
|
79
|
+
### 4. Seed memory
|
|
80
|
+
|
|
81
|
+
After docs are filled, append one slot via MCP `memory_write` or `novahiz init` baseline (already written by CLI):
|
|
82
|
+
|
|
83
|
+
- stack and markers
|
|
84
|
+
- entry points
|
|
85
|
+
- where docs live
|
|
86
|
+
- open unknowns
|
|
87
|
+
|
|
88
|
+
### 5. Cleanup (optional, reviewed)
|
|
89
|
+
|
|
90
|
+
1. Show the CLI cleanup list to the user.
|
|
91
|
+
2. Use the `question` tool: apply all, apply none, or hand-pick.
|
|
92
|
+
3. Only after an explicit choice:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
novahiz init --apply --json
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`--yes` alone is never enough to delete files the user has not seen.
|
|
99
|
+
|
|
100
|
+
## Exit criteria
|
|
101
|
+
|
|
102
|
+
- `novahiz init --json` has `failed: []`
|
|
103
|
+
- `novahiz-docs/` has four files with no remaining fill-in comments on architecture/conventions/standards
|
|
104
|
+
- `project-memory/` has at least one slot
|
|
105
|
+
- Cleanup either applied with consent or left unapplied on purpose
|
|
106
|
+
|
|
107
|
+
## Pitfalls
|
|
108
|
+
|
|
109
|
+
- Running `novahiz init` thinking it installs the global tool (`setup` does that).
|
|
110
|
+
- Filling templates with guessed architecture instead of a real read.
|
|
111
|
+
- Deleting cleanup candidates without showing the list.
|
|
112
|
+
- Duplicating content that already belongs in `novahiz-docs` update / converge.
|