novahiz 0.3.1 → 0.3.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.
Files changed (46) hide show
  1. package/NOTICE.md +1 -0
  2. package/README.md +1 -1
  3. package/bin/novahiz.mjs +30 -2
  4. package/catalog/providers.json +15 -4
  5. package/dist/autodocs.js +170 -0
  6. package/dist/catalog.js +245 -0
  7. package/dist/classify.js +181 -0
  8. package/dist/cli.js +218 -0
  9. package/dist/commands/autodocs.js +218 -0
  10. package/dist/commands/clean.js +106 -0
  11. package/dist/commands/context.js +167 -0
  12. package/dist/commands/doctor.js +221 -0
  13. package/dist/commands/gate.js +218 -0
  14. package/dist/commands/graft.js +146 -0
  15. package/dist/commands/init.js +325 -0
  16. package/dist/commands/inspect.js +314 -0
  17. package/dist/commands/report.js +87 -0
  18. package/dist/commands/task.js +354 -0
  19. package/dist/commands/tokens.js +69 -0
  20. package/dist/complexity.js +265 -0
  21. package/dist/content.js +99 -0
  22. package/dist/db.js +176 -0
  23. package/dist/deps.js +24 -0
  24. package/dist/exec.js +108 -0
  25. package/dist/gate-repair.js +76 -0
  26. package/dist/gate.js +493 -0
  27. package/dist/graft.js +257 -0
  28. package/dist/ledger.js +431 -0
  29. package/dist/memory.js +441 -0
  30. package/dist/prompt-rewriter.js +481 -0
  31. package/dist/providers.js +47 -0
  32. package/dist/relevance.js +58 -0
  33. package/dist/render.js +90 -0
  34. package/dist/spec.js +208 -0
  35. package/dist/targets.js +375 -0
  36. package/docs/CATALOG.md +1 -1
  37. package/docs/CONFIGURATION.md +2 -2
  38. package/docs/PROVIDERS.md +4 -3
  39. package/install/bootstrap.mjs +25 -6
  40. package/install/install.mjs +36 -48
  41. package/install/lib.mjs +41 -0
  42. package/novahiz.config.example.json +1 -1
  43. package/package.json +5 -3
  44. package/src/exec.ts +6 -1
  45. package/src/spec.ts +5 -2
  46. package/docs/audit-2026-09-25.md +0 -275
@@ -0,0 +1,181 @@
1
+ import { providersForCategories } from "./providers.js";
2
+ import { determineTier } from "./complexity.js";
3
+ export function fold(value) {
4
+ return value
5
+ .toLowerCase()
6
+ .normalize("NFD")
7
+ .replace(/[\u0300-\u036f]/g, "");
8
+ }
9
+ function escapeRegex(value) {
10
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11
+ }
12
+ function termMatches(text, keyword) {
13
+ const needle = fold(keyword).trim();
14
+ if (needle.length === 0)
15
+ return false;
16
+ const pattern = new RegExp(`(^|[^\\p{L}\\p{N}])${escapeRegex(needle)}([^\\p{L}\\p{N}]|$)`, "u");
17
+ return pattern.test(text);
18
+ }
19
+ function keywordTerm(keyword) {
20
+ return typeof keyword === "string" ? keyword : keyword.term;
21
+ }
22
+ function keywordWeight(keyword) {
23
+ if (typeof keyword === "string")
24
+ return 1;
25
+ return typeof keyword.weight === "number" ? keyword.weight : 1;
26
+ }
27
+ function matchedAlternative(text, keyword) {
28
+ const raw = keywordTerm(keyword);
29
+ const alternatives = raw.includes("|") ? raw.split("|") : [raw];
30
+ for (const alternative of alternatives) {
31
+ const term = alternative.trim();
32
+ if (term.length > 0 && termMatches(text, term))
33
+ return term;
34
+ }
35
+ return null;
36
+ }
37
+ function byCodeUnit(a, b) {
38
+ return a < b ? -1 : a > b ? 1 : 0;
39
+ }
40
+ // Confidence maps a raw keyword score to [0, 1) using a half-saturation curve.
41
+ // At score=2 the confidence is ~0.5; at score=5 it's ~0.71; at score=10 it's ~0.83.
42
+ // The "+2" constant prevents low-score categories from having disproportionate confidence.
43
+ function confidence(score) {
44
+ if (score <= 0)
45
+ return 0;
46
+ return Math.round((score / (score + 2)) * 100) / 100;
47
+ }
48
+ function skillsOfCategory(category) {
49
+ const skills = [...category.defaultSkills];
50
+ for (const step of category.roadmap?.steps ?? []) {
51
+ for (const skill of step.requireSkills ?? []) {
52
+ if (!skills.includes(skill))
53
+ skills.push(skill);
54
+ }
55
+ }
56
+ return skills;
57
+ }
58
+ function enforcedOfCategory(category) {
59
+ const skills = [];
60
+ for (const step of category.roadmap?.steps ?? []) {
61
+ if (step.kind !== "skill" || step.optional)
62
+ continue;
63
+ for (const skill of step.requireSkills ?? []) {
64
+ if (!skills.includes(skill))
65
+ skills.push(skill);
66
+ }
67
+ }
68
+ return skills;
69
+ }
70
+ export function classify(spec, prompt, options = {}) {
71
+ const minScore = options.minScore ?? spec.config.classify.minScore;
72
+ const maxCategories = options.maxCategories ?? spec.config.classify.maxCategories;
73
+ const fallbackCategory = options.fallbackCategory ?? spec.config.classify.fallbackCategory;
74
+ const text = fold(prompt);
75
+ // Determine complexity tier
76
+ const tier = determineTier(prompt);
77
+ const scored = [];
78
+ for (const category of spec.categories) {
79
+ let score = 0;
80
+ const terms = [];
81
+ const negatives = [];
82
+ for (const keyword of category.keywords) {
83
+ const matched = matchedAlternative(text, keyword);
84
+ if (!matched)
85
+ continue;
86
+ let weight = keywordWeight(keyword);
87
+ if (matched.includes(" "))
88
+ weight += 0.5;
89
+ score += weight;
90
+ terms.push(matched);
91
+ }
92
+ for (const negative of category.negativeKeywords ?? []) {
93
+ const matched = matchedAlternative(text, negative);
94
+ if (!matched)
95
+ continue;
96
+ score -= keywordWeight(negative);
97
+ negatives.push(matched);
98
+ }
99
+ if (score >= minScore) {
100
+ scored.push({ id: category.id, score, confidence: confidence(score), margin: 0, terms, negatives });
101
+ }
102
+ }
103
+ const priorityOf = (id) => spec.categories.find((category) => category.id === id)?.priority ?? 0;
104
+ scored.sort((a, b) => b.score - a.score || priorityOf(b.id) - priorityOf(a.id) || byCodeUnit(a.id, b.id));
105
+ for (let index = 0; index < scored.length; index += 1) {
106
+ const next = scored[index + 1]?.score ?? 0;
107
+ scored[index].margin = Math.round((scored[index].score - next) * 1000) / 1000;
108
+ }
109
+ const selected = scored.slice(0, Math.max(0, maxCategories));
110
+ // H8: when nothing matched and fallback is misconfigured, force "general" so
111
+ // the gate still applies R6 rules instead of silently allowing everything.
112
+ if (selected.length === 0) {
113
+ if (fallbackCategory.length > 0 && spec.categories.some((entry) => entry.id === fallbackCategory)) {
114
+ selected.push({ id: fallbackCategory, score: 0, confidence: 0, margin: 0, terms: [], negatives: [] });
115
+ }
116
+ else if (spec.categories.some((entry) => entry.id === "general")) {
117
+ selected.push({ id: "general", score: 0, confidence: 0, margin: 0, terms: [], negatives: [] });
118
+ }
119
+ }
120
+ const requiredSkills = [];
121
+ const invocations = [];
122
+ const roadmaps = [];
123
+ for (const item of selected) {
124
+ const category = spec.categories.find((entry) => entry.id === item.id);
125
+ if (!category)
126
+ continue;
127
+ // Tier-based skill filtering
128
+ if (tier === "trivial") {
129
+ // Trivial: no skills required from roadmap
130
+ continue;
131
+ }
132
+ if (tier === "lite") {
133
+ // Lite: only implement + converge skills (skip plan, clarify, etc.)
134
+ for (const step of category.roadmap?.steps ?? []) {
135
+ const allowed = (step.requireSkills ?? []).filter(s => s === "novahiz-implement" || s === "novahiz-converge");
136
+ for (const skill of allowed) {
137
+ if (!requiredSkills.includes(skill))
138
+ requiredSkills.push(skill);
139
+ }
140
+ }
141
+ }
142
+ else {
143
+ // Full: all skills
144
+ for (const skill of skillsOfCategory(category)) {
145
+ if (!requiredSkills.includes(skill))
146
+ requiredSkills.push(skill);
147
+ }
148
+ }
149
+ for (const step of category.roadmap?.steps ?? []) {
150
+ if (step.kind !== "skill")
151
+ continue;
152
+ invocations.push({
153
+ category: category.id,
154
+ step: step.id,
155
+ label: step.label,
156
+ kind: step.kind,
157
+ skills: step.requireSkills ?? [],
158
+ optional: step.optional === true
159
+ });
160
+ }
161
+ if (category.roadmap) {
162
+ roadmaps.push({ category: category.id, id: category.roadmap.id, steps: category.roadmap.steps });
163
+ }
164
+ }
165
+ const primary = selected[0]?.id ?? null;
166
+ const primaryCategory = primary ? spec.categories.find((entry) => entry.id === primary) : undefined;
167
+ // C1: enforcedSkills must respect tier gating — trivial prompts should not
168
+ // expose enforced skills, otherwise downstream consumers load skills unnecessarily.
169
+ const enforcedSkills = (tier !== "trivial" && primaryCategory) ? enforcedOfCategory(primaryCategory) : [];
170
+ const providers = providersForCategories(spec, selected.map((entry) => entry.id)).map((provider) => provider.id);
171
+ return {
172
+ categories: selected,
173
+ primary,
174
+ tier,
175
+ requiredSkills,
176
+ enforcedSkills,
177
+ invocations,
178
+ providers,
179
+ roadmaps
180
+ };
181
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,218 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join, dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { spawnSync } from "node:child_process";
5
+ import { parse, print, flagOn } from "./commands/context.js";
6
+ import { expandHome } from "./spec.js";
7
+ import { commandCheck, commandSync, commandClassify, commandSkills, commandCategories, commandRules, commandSessionLoad, commandSessionState, commandCatalog, commandRoadmap, commandStep, commandProviders, commandDeps, commandDispatch } from "./commands/inspect.js";
8
+ import { commandGate } from "./commands/gate.js";
9
+ import { commandReport } from "./commands/report.js";
10
+ import { commandTask } from "./commands/task.js";
11
+ import { commandClean } from "./commands/clean.js";
12
+ import { commandDoctor } from "./commands/doctor.js";
13
+ import { commandTokens } from "./commands/tokens.js";
14
+ import { graftCommand } from "./commands/graft.js";
15
+ import { commandInit } from "./commands/init.js";
16
+ import { commandAutodocs } from "./commands/autodocs.js";
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ // M6: HOME was a module-level const, so `--home` (which sets the env var in
19
+ // main()) was silently ignored by the current execution. Read it lazily.
20
+ function homeDir() {
21
+ return (process.env.NOVAHIZ_HOME ||
22
+ join(process.env.HOME || process.env.USERPROFILE || "", ".config", "novahiz"));
23
+ }
24
+ function runSync() {
25
+ const home = homeDir();
26
+ const cli = join(home, "src", "cli.ts");
27
+ if (existsSync(cli)) {
28
+ spawnSync(process.execPath, [cli, "sync"], {
29
+ encoding: "utf8",
30
+ stdio: "inherit",
31
+ env: { ...process.env, NOVAHIZ_HOME: home }
32
+ });
33
+ }
34
+ }
35
+ function usage() {
36
+ print({
37
+ name: "novahiz",
38
+ commands: [
39
+ "init Initialize Novahiz in the current project",
40
+ "setup Install Novahiz (config, skills, catalog)",
41
+ "autodocs [--flush] Sync docs and memory after major project changes",
42
+ "doctor Check that everything works",
43
+ "status Show current classification and gate state",
44
+ "task new <title> Start a new tracked task",
45
+ "task status Show task progress",
46
+ "task done <id> Mark a todo as complete",
47
+ "report Session report",
48
+ "clean Remove old logs and sessions",
49
+ "graft Version-control the ledger (graft init/log/diff/status/restore)",
50
+ "upgrade Pull latest and rebuild catalog",
51
+ "version Show version",
52
+ "",
53
+ "",
54
+ "Options:",
55
+ " --home <path> Override Novahiz home directory",
56
+ " --version, -v Show version",
57
+ " --help, -h Show this help",
58
+ "",
59
+ "Advanced (for power users and adapters):",
60
+ " classify <text> Classify a prompt",
61
+ " gate --tool <t> --file <f> Check if an edit is allowed",
62
+ " skills [--category c] List loaded skills",
63
+ " catalog <query> Search the skill catalog",
64
+ " roadmap --category c Show execution roadmap",
65
+ " dispatch [--task id] Generate work packets",
66
+ " tokens Token diagnostics",
67
+ ]
68
+ });
69
+ }
70
+ function printVersion() {
71
+ try {
72
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
73
+ process.stdout.write(`Novahiz ${pkg.version}\n`);
74
+ }
75
+ catch {
76
+ process.stdout.write("Novahiz (unknown version)\n");
77
+ }
78
+ }
79
+ function runInit() {
80
+ const home = homeDir();
81
+ const installScript = join(home, "install", "install.mjs");
82
+ if (existsSync(installScript)) {
83
+ process.stdout.write("Installing Novahiz...\n");
84
+ const result = spawnSync(process.execPath, [installScript, "--yes"], {
85
+ encoding: "utf8",
86
+ stdio: "inherit",
87
+ env: { ...process.env, NOVAHIZ_HOME: home }
88
+ });
89
+ if (result.status !== 0) {
90
+ process.stderr.write("Installation failed. Run `novahiz doctor` for details.\n");
91
+ process.exitCode = 1;
92
+ return;
93
+ }
94
+ }
95
+ process.stdout.write("\nBuilding skill catalog...\n");
96
+ runSync();
97
+ process.stdout.write("\nDone! Restart opencode to activate Novahiz.\n");
98
+ }
99
+ function runUpgrade(parsed) {
100
+ const home = homeDir();
101
+ if (!existsSync(join(home, ".git"))) {
102
+ process.stderr.write("Not a git repository. Install from source first.\n");
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+ // P2-C (LOW): `git pull` rewrote the running code (hooks included) with no
107
+ // preview. Default to a dry run; `--apply` is required to actually pull.
108
+ if (!flagOn(parsed, "apply")) {
109
+ const dry = spawnSync("git", ["pull", "--dry-run"], { cwd: home, encoding: "utf8" });
110
+ const preview = `${dry.stdout ?? ""}${dry.stderr ?? ""}`.trim();
111
+ if (preview)
112
+ process.stdout.write(preview + "\n");
113
+ if (dry.status !== 0) {
114
+ process.stderr.write("Dry run failed; fix git first.\n");
115
+ process.exitCode = 1;
116
+ return;
117
+ }
118
+ process.stdout.write("Dry run only. Re-run `novahiz upgrade --apply` to pull and rebuild.\n");
119
+ process.exitCode = 1;
120
+ return;
121
+ }
122
+ process.stdout.write("Pulling latest changes...\n");
123
+ const pull = spawnSync("git", ["pull", "--ff-only"], { cwd: home, stdio: "inherit" });
124
+ if (pull.status !== 0) {
125
+ process.stderr.write("Pull failed; nothing rebuilt.\n");
126
+ process.exitCode = 1;
127
+ return;
128
+ }
129
+ process.stdout.write("\nRebuilding skill catalog...\n");
130
+ runSync();
131
+ process.stdout.write("\nUpgraded! Restart opencode to apply changes.\n");
132
+ }
133
+ function main(argv) {
134
+ const parsed = parse(argv);
135
+ if (typeof parsed.flags.home === "string" && parsed.flags.home.length > 0) {
136
+ process.env.NOVAHIZ_HOME = resolve(expandHome(parsed.flags.home));
137
+ }
138
+ const command = parsed.positionals[0];
139
+ if (command === undefined || command === "help" || command === "--help" || command === "-h") {
140
+ usage();
141
+ return;
142
+ }
143
+ if (parsed.flags.version || parsed.flags.v) {
144
+ printVersion();
145
+ return;
146
+ }
147
+ switch (command) {
148
+ case "init":
149
+ return commandInit(parsed);
150
+ case "setup":
151
+ return runInit();
152
+ case "autodocs":
153
+ return commandAutodocs(parsed);
154
+ case "doctor":
155
+ return commandDoctor(parsed);
156
+ case "status":
157
+ case "check":
158
+ return commandCheck();
159
+ case "report":
160
+ return commandReport(parsed);
161
+ case "clean":
162
+ return commandClean(parsed);
163
+ case "upgrade":
164
+ case "update":
165
+ return runUpgrade(parsed);
166
+ case "version":
167
+ case "-v":
168
+ case "--version":
169
+ return printVersion();
170
+ case "task":
171
+ return commandTask(parsed);
172
+ case "sync":
173
+ return commandSync();
174
+ case "classify":
175
+ return commandClassify(parsed);
176
+ case "gate":
177
+ return commandGate(parsed);
178
+ case "skills":
179
+ return commandSkills(parsed);
180
+ case "categories":
181
+ return commandCategories();
182
+ case "rules":
183
+ return commandRules();
184
+ case "session-load":
185
+ return commandSessionLoad(parsed);
186
+ case "session-state":
187
+ return commandSessionState(parsed);
188
+ case "catalog":
189
+ return commandCatalog(parsed);
190
+ case "roadmap":
191
+ return commandRoadmap(parsed);
192
+ case "step":
193
+ return commandStep(parsed);
194
+ case "providers":
195
+ return commandProviders(parsed);
196
+ case "deps":
197
+ return commandDeps(parsed);
198
+ case "dispatch":
199
+ return commandDispatch(parsed);
200
+ case "tokens":
201
+ return commandTokens(parsed);
202
+ case "graft":
203
+ return graftCommand(parsed.positionals.slice(1));
204
+ default:
205
+ process.stderr.write(`novahiz: unknown command "${command}"\n\n`);
206
+ usage();
207
+ process.exitCode = 1;
208
+ return;
209
+ }
210
+ }
211
+ try {
212
+ main(process.argv.slice(2));
213
+ }
214
+ catch (error) {
215
+ const message = error instanceof Error ? error.message : String(error);
216
+ process.stderr.write(`novahiz: ${message}\n`);
217
+ process.exitCode = 1;
218
+ }
@@ -0,0 +1,218 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { autoDocsEnabled, clearDirty, dateStamp, isMajorPath, markDirty, readProjectConfig, readState, upsertChangesSection, writeProjectConfig } from "../autodocs.js";
4
+ import { emit, flagOn } from "./context.js";
5
+ import { memoryRoot, writeEntry } from "../memory.js";
6
+ import * as ui from "../render.js";
7
+ const MAX_DOC_PATHS = 12;
8
+ const ARCHITECTURE = "novahiz-docs/ARCHITECTURE.md";
9
+ export function ensureProjectAutoDocsConfig(cwd) {
10
+ try {
11
+ const config = readProjectConfig(cwd);
12
+ if (config.autoDocs === true)
13
+ return "existing";
14
+ writeProjectConfig(cwd, { ...config, autoDocs: true });
15
+ return "created";
16
+ }
17
+ catch {
18
+ return "failed";
19
+ }
20
+ }
21
+ function skipResult(reason, paths = []) {
22
+ const state = readState(process.cwd());
23
+ return {
24
+ status: "skipped",
25
+ reason,
26
+ docsPath: null,
27
+ docsUpdated: false,
28
+ memorySlot: null,
29
+ paths,
30
+ pending: state.pending.length,
31
+ lastSync: state.lastSync
32
+ };
33
+ }
34
+ function flushDocs(cwd, paths) {
35
+ const docPath = join(cwd, "novahiz-docs", "ARCHITECTURE.md");
36
+ if (!existsSync(docPath))
37
+ return { path: ARCHITECTURE, updated: false };
38
+ const sample = paths.slice(0, MAX_DOC_PATHS);
39
+ if (sample.length === 0)
40
+ return { path: ARCHITECTURE, updated: false };
41
+ const doc = readFileSync(docPath, "utf8");
42
+ const lines = sample.map((path) => `${path} (${dateStamp()})`);
43
+ const next = upsertChangesSection(doc, lines);
44
+ if (next === doc)
45
+ return { path: ARCHITECTURE, updated: false };
46
+ writeFileSync(docPath, next, "utf8");
47
+ return { path: ARCHITECTURE, updated: true };
48
+ }
49
+ function flushMemory(cwd, paths) {
50
+ const sample = paths.slice(0, MAX_DOC_PATHS);
51
+ if (sample.length === 0)
52
+ return null;
53
+ try {
54
+ const result = writeEntry({
55
+ title: "Auto docs sync",
56
+ description: "Change log written by novahiz autodocs",
57
+ tags: ["autodocs", "sync"],
58
+ root: memoryRoot(cwd),
59
+ content: [`Sync: ${new Date().toISOString()}`, ...sample.map((path) => `- ${path}`)].join("\n")
60
+ });
61
+ return result.slot.id;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ export function flushAutoDocs(cwd) {
68
+ if (!autoDocsEnabled(cwd)) {
69
+ return skipResult("autoDocs disabled (set autoDocs=true in .novahiz/config.json or unset NOVAHIZ_AUTODOCS)");
70
+ }
71
+ const state = readState(cwd);
72
+ if (!state.dirty && state.pending.length === 0) {
73
+ return skipResult("nothing dirty", state.pending);
74
+ }
75
+ try {
76
+ const paths = state.pending;
77
+ const docs = flushDocs(cwd, paths);
78
+ const memorySlot = flushMemory(cwd, paths);
79
+ const next = clearDirty(cwd);
80
+ return {
81
+ status: "flushed",
82
+ reason: `${paths.length} path(s) recorded`,
83
+ docsPath: docs.path,
84
+ docsUpdated: docs.updated,
85
+ memorySlot,
86
+ paths,
87
+ pending: next.pending.length,
88
+ lastSync: next.lastSync
89
+ };
90
+ }
91
+ catch (error) {
92
+ return {
93
+ status: "failed",
94
+ reason: error.message,
95
+ docsPath: null,
96
+ docsUpdated: false,
97
+ memorySlot: null,
98
+ paths: state.pending,
99
+ pending: state.pending.length,
100
+ lastSync: state.lastSync
101
+ };
102
+ }
103
+ }
104
+ function markFromArgs(cwd, mark, positionals) {
105
+ const raw = typeof mark === "string" && mark.length > 0
106
+ ? mark
107
+ : positionals.join(",");
108
+ const list = raw
109
+ .split(",")
110
+ .map((entry) => entry.trim())
111
+ .filter(Boolean);
112
+ if (list.length === 0) {
113
+ return {
114
+ status: "failed",
115
+ reason: "--mark needs at least one path",
116
+ docsPath: null,
117
+ docsUpdated: false,
118
+ memorySlot: null,
119
+ paths: [],
120
+ pending: readState(cwd).pending.length,
121
+ lastSync: readState(cwd).lastSync
122
+ };
123
+ }
124
+ const accepted = [];
125
+ const rejected = [];
126
+ for (const path of list) {
127
+ if (isMajorPath(path)) {
128
+ markDirty(cwd, path);
129
+ accepted.push(path);
130
+ }
131
+ else {
132
+ rejected.push(path);
133
+ }
134
+ }
135
+ const state = readState(cwd);
136
+ if (accepted.length === 0) {
137
+ return {
138
+ status: "skipped",
139
+ reason: `no major path among ${rejected.join(", ")}`,
140
+ docsPath: null,
141
+ docsUpdated: false,
142
+ memorySlot: null,
143
+ paths: rejected,
144
+ pending: state.pending.length,
145
+ lastSync: state.lastSync
146
+ };
147
+ }
148
+ return {
149
+ status: "marked",
150
+ reason: rejected.length > 0
151
+ ? `marked ${accepted.length}, ignored ${rejected.length} non-major`
152
+ : `marked ${accepted.length}`,
153
+ docsPath: null,
154
+ docsUpdated: false,
155
+ memorySlot: null,
156
+ paths: accepted,
157
+ pending: state.pending.length,
158
+ lastSync: state.lastSync
159
+ };
160
+ }
161
+ function statusView(cwd) {
162
+ const config = readProjectConfig(cwd);
163
+ const state = readState(cwd);
164
+ return {
165
+ cwd,
166
+ autoDocs: config.autoDocs === true,
167
+ enabled: autoDocsEnabled(cwd),
168
+ dirty: state.dirty,
169
+ pending: state.pending,
170
+ lastSync: state.lastSync,
171
+ sessions: state.sessions
172
+ };
173
+ }
174
+ export function commandAutodocs(parsed) {
175
+ const cwd = process.cwd();
176
+ const wantFlush = flagOn(parsed, "flush");
177
+ const markFlag = parsed.flags.mark;
178
+ const isMark = markFlag !== undefined && markFlag !== false;
179
+ let outcome = null;
180
+ if (isMark) {
181
+ outcome = markFromArgs(cwd, markFlag, parsed.positionals.slice(1));
182
+ }
183
+ else if (wantFlush) {
184
+ outcome = flushAutoDocs(cwd);
185
+ }
186
+ const value = outcome
187
+ ? { ...statusView(cwd), action: isMark ? "mark" : "flush", ...outcome }
188
+ : { ...statusView(cwd), action: "status" };
189
+ emit(parsed, value, () => {
190
+ const view = statusView(cwd);
191
+ const lines = [
192
+ ui.heading("novahiz autodocs"),
193
+ ui.kv([
194
+ ["Cwd", String(view.cwd)],
195
+ ["autoDocs", String(view.autoDocs)],
196
+ ["Enabled", String(view.enabled)],
197
+ ["Dirty", String(view.dirty)],
198
+ ["Pending", String(view.pending)],
199
+ ["Last sync", String(view.lastSync)]
200
+ ])
201
+ ];
202
+ if (outcome) {
203
+ lines.push("", ui.kv([
204
+ ["Action", outcome.status],
205
+ ["Reason", outcome.reason],
206
+ ["Docs", outcome.docsPath ? `${outcome.docsPath}${outcome.docsUpdated ? " updated" : " unchanged"}` : "n/a"],
207
+ ["Memory", outcome.memorySlot ?? "n/a"]
208
+ ]));
209
+ lines.push("", outcome.status === "failed" ? ui.style("red", outcome.reason) : ui.style("green", outcome.reason));
210
+ }
211
+ else {
212
+ lines.push("", ui.style("dim", "Use --flush to sync, --mark <path> to record a change."));
213
+ }
214
+ return lines.join("\n");
215
+ });
216
+ if (outcome?.status === "failed")
217
+ process.exitCode = 1;
218
+ }