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.
@@ -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 to avoid broken relative path in installed plugin
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
  };
@@ -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` | Env var to disable the 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 → clean up session state on session.deleted │
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.Novahiz = {
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 → allow with warning (gate unavailable) │
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 allowed (gate unavailable)
149
- - If `gate` exits non-zero non-two → tool call allowed with warning
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`: the variable that disables the gate for one session. Defaults to `novahiz_GATE`; values `off`, `0`, `false`, `no`, `disabled` disable it. Read by the CLI, the hook mode, the MCP gate tool, and the opencode plugin.
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
 
@@ -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
- categories: { type: "array", items: { type: "string" } },
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: args?.categories ? args.categories.map(String) : [],
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
- if (action === "drop") return toolResult(dropTodo(db, String(args?.id ?? ""), args?.reason ? String(args.reason) : ""));
476
- if (action === "reorder") {
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.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": "./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"
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 : `novahiz_GATE=off` (ou `0`, `false`, `no`, `disabled`) dans l'environnement, lu via `gate.envEscape`. 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.
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.