@wazir-dev/cli 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +74 -10
  2. package/README.md +15 -15
  3. package/assets/demo.cast +47 -0
  4. package/assets/demo.gif +0 -0
  5. package/docs/anti-patterns/AP-23-skipping-enabled-workflows.md +28 -0
  6. package/docs/anti-patterns/AP-24-clarifier-deciding-scope.md +34 -0
  7. package/docs/concepts/architecture.md +1 -1
  8. package/docs/concepts/roles-and-workflows.md +2 -0
  9. package/docs/concepts/why-wazir.md +59 -0
  10. package/docs/decisions/2026-03-19-deferred-items.md +564 -0
  11. package/docs/decisions/2026-03-19-enhancement-decisions.md +300 -0
  12. package/docs/readmes/INDEX.md +21 -5
  13. package/docs/readmes/features/expertise/README.md +2 -2
  14. package/docs/readmes/features/exports/README.md +2 -2
  15. package/docs/readmes/features/hooks/pre-compact-summary.md +1 -1
  16. package/docs/readmes/features/schemas/README.md +3 -0
  17. package/docs/readmes/features/skills/README.md +17 -0
  18. package/docs/readmes/features/skills/clarifier.md +5 -0
  19. package/docs/readmes/features/skills/claude-cli.md +5 -0
  20. package/docs/readmes/features/skills/codex-cli.md +5 -0
  21. package/docs/readmes/features/skills/dispatching-parallel-agents.md +5 -0
  22. package/docs/readmes/features/skills/executing-plans.md +5 -0
  23. package/docs/readmes/features/skills/executor.md +5 -0
  24. package/docs/readmes/features/skills/finishing-a-development-branch.md +5 -0
  25. package/docs/readmes/features/skills/gemini-cli.md +5 -0
  26. package/docs/readmes/features/skills/humanize.md +5 -0
  27. package/docs/readmes/features/skills/init-pipeline.md +5 -0
  28. package/docs/readmes/features/skills/receiving-code-review.md +5 -0
  29. package/docs/readmes/features/skills/requesting-code-review.md +5 -0
  30. package/docs/readmes/features/skills/reviewer.md +5 -0
  31. package/docs/readmes/features/skills/subagent-driven-development.md +5 -0
  32. package/docs/readmes/features/skills/using-git-worktrees.md +5 -0
  33. package/docs/readmes/features/skills/wazir.md +5 -0
  34. package/docs/readmes/features/skills/writing-skills.md +5 -0
  35. package/docs/readmes/features/workflows/prepare-next.md +1 -1
  36. package/docs/reference/configuration-reference.md +47 -6
  37. package/docs/reference/hooks.md +1 -0
  38. package/docs/reference/launch-checklist.md +4 -4
  39. package/docs/reference/review-loop-pattern.md +119 -9
  40. package/docs/reference/roles-reference.md +1 -0
  41. package/docs/reference/skill-tiers.md +147 -0
  42. package/docs/reference/tooling-cli.md +3 -1
  43. package/docs/truth-claims.yaml +12 -0
  44. package/expertise/antipatterns/process/ai-coding-antipatterns.md +214 -1
  45. package/exports/hosts/claude/.claude/commands/plan-review.md +3 -1
  46. package/exports/hosts/claude/.claude/commands/verify.md +30 -1
  47. package/exports/hosts/claude/.claude/settings.json +9 -0
  48. package/exports/hosts/claude/CLAUDE.md +1 -1
  49. package/exports/hosts/claude/export.manifest.json +6 -4
  50. package/exports/hosts/claude/host-package.json +3 -1
  51. package/exports/hosts/codex/AGENTS.md +1 -1
  52. package/exports/hosts/codex/export.manifest.json +6 -4
  53. package/exports/hosts/codex/host-package.json +3 -1
  54. package/exports/hosts/cursor/.cursor/hooks.json +4 -0
  55. package/exports/hosts/cursor/.cursor/rules/wazir-core.mdc +1 -1
  56. package/exports/hosts/cursor/export.manifest.json +6 -4
  57. package/exports/hosts/cursor/host-package.json +3 -1
  58. package/exports/hosts/gemini/GEMINI.md +1 -1
  59. package/exports/hosts/gemini/export.manifest.json +6 -4
  60. package/exports/hosts/gemini/host-package.json +3 -1
  61. package/hooks/context-mode-router +191 -0
  62. package/hooks/definitions/context_mode_router.yaml +19 -0
  63. package/hooks/hooks.json +31 -6
  64. package/hooks/protected-path-write-guard +8 -0
  65. package/hooks/routing-matrix.json +45 -0
  66. package/hooks/session-start +62 -1
  67. package/llms-full.txt +937 -134
  68. package/package.json +2 -4
  69. package/schemas/hook.schema.json +2 -1
  70. package/schemas/phase-report.schema.json +89 -0
  71. package/schemas/usage.schema.json +25 -1
  72. package/schemas/wazir-manifest.schema.json +19 -0
  73. package/skills/brainstorming/SKILL.md +32 -157
  74. package/skills/clarifier/SKILL.md +289 -111
  75. package/skills/claude-cli/SKILL.md +320 -0
  76. package/skills/codex-cli/SKILL.md +260 -0
  77. package/skills/debugging/SKILL.md +13 -0
  78. package/skills/design/SKILL.md +13 -0
  79. package/skills/dispatching-parallel-agents/SKILL.md +13 -0
  80. package/skills/executing-plans/SKILL.md +13 -0
  81. package/skills/executor/SKILL.md +139 -19
  82. package/skills/finishing-a-development-branch/SKILL.md +13 -0
  83. package/skills/gemini-cli/SKILL.md +260 -0
  84. package/skills/humanize/SKILL.md +13 -0
  85. package/skills/init-pipeline/SKILL.md +72 -164
  86. package/skills/prepare-next/SKILL.md +81 -10
  87. package/skills/receiving-code-review/SKILL.md +13 -0
  88. package/skills/requesting-code-review/SKILL.md +13 -0
  89. package/skills/reviewer/SKILL.md +369 -24
  90. package/skills/run-audit/SKILL.md +13 -0
  91. package/skills/scan-project/SKILL.md +13 -0
  92. package/skills/self-audit/SKILL.md +217 -16
  93. package/skills/skill-research/SKILL.md +188 -0
  94. package/skills/subagent-driven-development/SKILL.md +13 -0
  95. package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +2 -0
  96. package/skills/subagent-driven-development/implementer-prompt.md +8 -0
  97. package/skills/subagent-driven-development/spec-reviewer-prompt.md +7 -0
  98. package/skills/tdd/SKILL.md +13 -0
  99. package/skills/using-git-worktrees/SKILL.md +13 -0
  100. package/skills/using-skills/SKILL.md +13 -0
  101. package/skills/verification/SKILL.md +54 -3
  102. package/skills/wazir/SKILL.md +464 -381
  103. package/skills/writing-plans/SKILL.md +14 -1
  104. package/skills/writing-skills/SKILL.md +13 -0
  105. package/templates/artifacts/implementation-plan.md +3 -0
  106. package/templates/artifacts/tasks-template.md +133 -0
  107. package/templates/examples/phase-report.example.json +48 -0
  108. package/tooling/src/adapters/composition-engine.js +256 -0
  109. package/tooling/src/adapters/model-router.js +84 -0
  110. package/tooling/src/capture/command.js +41 -2
  111. package/tooling/src/capture/run-config.js +3 -1
  112. package/tooling/src/capture/store.js +56 -0
  113. package/tooling/src/capture/usage.js +106 -0
  114. package/tooling/src/capture/user-input.js +66 -0
  115. package/tooling/src/checks/ac-matrix.js +256 -0
  116. package/tooling/src/checks/command-registry.js +12 -0
  117. package/tooling/src/checks/docs-truth.js +1 -1
  118. package/tooling/src/checks/security-sensitivity.js +69 -0
  119. package/tooling/src/checks/skills.js +111 -0
  120. package/tooling/src/cli.js +31 -20
  121. package/tooling/src/commands/stats.js +161 -0
  122. package/tooling/src/commands/validate.js +5 -1
  123. package/tooling/src/export/compiler.js +33 -37
  124. package/tooling/src/gating/agent.js +145 -0
  125. package/tooling/src/guards/phase-prerequisite-guard.js +185 -0
  126. package/tooling/src/hooks/routing-logic.js +69 -0
  127. package/tooling/src/init/auto-detect.js +258 -0
  128. package/tooling/src/init/command.js +38 -170
  129. package/tooling/src/input/scanner.js +46 -0
  130. package/tooling/src/reports/command.js +103 -0
  131. package/tooling/src/reports/phase-report.js +323 -0
  132. package/tooling/src/state/command.js +160 -0
  133. package/tooling/src/state/db.js +287 -0
  134. package/tooling/src/status/command.js +58 -1
  135. package/tooling/src/verify/proof-collector.js +299 -0
  136. package/wazir.manifest.yaml +26 -14
  137. package/workflows/plan-review.md +3 -1
  138. package/workflows/verify.md +30 -1
@@ -0,0 +1,323 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ /**
6
+ * Parse Node.js test runner output to extract pass/fail/skip counts.
7
+ * Handles the built-in `node --test` reporter format.
8
+ * @param {string} output - raw test runner stdout+stderr
9
+ * @returns {{ total: number, passed: number, failed: number, skipped: number }}
10
+ */
11
+ export function parseTestOutput(output) {
12
+ const result = { total: 0, passed: 0, failed: 0, skipped: 0 };
13
+
14
+ if (!output) {
15
+ return result;
16
+ }
17
+
18
+ // Node built-in test runner summary lines:
19
+ // # tests 12
20
+ // # pass 10
21
+ // # fail 1
22
+ // # skipped 1
23
+ const totalMatch = output.match(/^# tests\s+(\d+)/m);
24
+ const passMatch = output.match(/^# pass\s+(\d+)/m);
25
+ const failMatch = output.match(/^# fail\s+(\d+)/m);
26
+ const skipMatch = output.match(/^# skipped\s+(\d+)/m);
27
+
28
+ if (totalMatch) {
29
+ result.total = Number.parseInt(totalMatch[1], 10);
30
+ }
31
+
32
+ if (passMatch) {
33
+ result.passed = Number.parseInt(passMatch[1], 10);
34
+ }
35
+
36
+ if (failMatch) {
37
+ result.failed = Number.parseInt(failMatch[1], 10);
38
+ }
39
+
40
+ if (skipMatch) {
41
+ result.skipped = Number.parseInt(skipMatch[1], 10);
42
+ }
43
+
44
+ // Fallback: if no summary lines found, try TAP-style counting
45
+ if (!totalMatch && !passMatch && !failMatch) {
46
+ const okLines = output.match(/^ok \d+/gm);
47
+ const notOkLines = output.match(/^not ok \d+/gm);
48
+ const skipLines = output.match(/^ok \d+ .+# skip/gim);
49
+
50
+ result.passed = (okLines?.length ?? 0) - (skipLines?.length ?? 0);
51
+ result.failed = notOkLines?.length ?? 0;
52
+ result.skipped = skipLines?.length ?? 0;
53
+ result.total = result.passed + result.failed + result.skipped;
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * Run tests and parse results, or return null on failure.
61
+ * @param {string} projectRoot
62
+ * @returns {{ total: number, passed: number, failed: number, skipped: number } | null}
63
+ */
64
+ function collectTestResults(projectRoot) {
65
+ try {
66
+ const output = execFileSync(
67
+ 'node',
68
+ ['--test', '--experimental-test-snapshots'],
69
+ { cwd: projectRoot, encoding: 'utf8', timeout: 120_000 },
70
+ );
71
+
72
+ return parseTestOutput(output);
73
+ } catch (error) {
74
+ // Test command may exit non-zero if tests fail — still parse output
75
+ if (error.stdout || error.stderr) {
76
+ return parseTestOutput(`${error.stdout ?? ''}${error.stderr ?? ''}`);
77
+ }
78
+
79
+ return null;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Parse `git diff --stat` output for files changed, insertions, deletions.
85
+ * @param {string} statOutput - output of git diff --stat
86
+ * @returns {{ files_changed: number, insertions: number, deletions: number }}
87
+ */
88
+ export function parseDiffStat(statOutput) {
89
+ const result = { files_changed: 0, insertions: 0, deletions: 0 };
90
+
91
+ if (!statOutput) {
92
+ return result;
93
+ }
94
+
95
+ // Summary line: " 5 files changed, 120 insertions(+), 30 deletions(-)"
96
+ const summaryMatch = statOutput.match(
97
+ /(\d+) files? changed(?:,\s*(\d+) insertions?\(\+\))?(?:,\s*(\d+) deletions?\(-\))?/,
98
+ );
99
+
100
+ if (summaryMatch) {
101
+ result.files_changed = Number.parseInt(summaryMatch[1], 10);
102
+ result.insertions = summaryMatch[2] ? Number.parseInt(summaryMatch[2], 10) : 0;
103
+ result.deletions = summaryMatch[3] ? Number.parseInt(summaryMatch[3], 10) : 0;
104
+ }
105
+
106
+ return result;
107
+ }
108
+
109
+ /**
110
+ * Parse `git diff --name-status` output into added/modified/deleted arrays.
111
+ * @param {string} nameStatusOutput
112
+ * @returns {{ added: string[], modified: string[], deleted: string[] }}
113
+ */
114
+ export function parseNameStatus(nameStatusOutput) {
115
+ const result = { added: [], modified: [], deleted: [] };
116
+
117
+ if (!nameStatusOutput) {
118
+ return result;
119
+ }
120
+
121
+ for (const line of nameStatusOutput.split('\n')) {
122
+ const trimmed = line.trim();
123
+
124
+ if (!trimmed) {
125
+ continue;
126
+ }
127
+
128
+ const statusChar = trimmed[0];
129
+ const filePath = trimmed.slice(1).trim();
130
+
131
+ if (!filePath) {
132
+ continue;
133
+ }
134
+
135
+ switch (statusChar) {
136
+ case 'A':
137
+ result.added.push(filePath);
138
+ break;
139
+ case 'M':
140
+ result.modified.push(filePath);
141
+ break;
142
+ case 'D':
143
+ result.deleted.push(filePath);
144
+ break;
145
+ default:
146
+ // R (rename), C (copy), etc. — treat as modified
147
+ result.modified.push(filePath);
148
+ break;
149
+ }
150
+ }
151
+
152
+ return result;
153
+ }
154
+
155
+ /**
156
+ * Collect diff stats from git.
157
+ * @param {string} projectRoot
158
+ * @param {string} baseBranch
159
+ * @returns {{ files_changed: number, insertions: number, deletions: number } | null}
160
+ */
161
+ function collectDiffStats(projectRoot, baseBranch) {
162
+ try {
163
+ const output = execFileSync(
164
+ 'git',
165
+ ['diff', '--stat', `${baseBranch}...HEAD`],
166
+ { cwd: projectRoot, encoding: 'utf8', timeout: 15_000 },
167
+ );
168
+
169
+ return parseDiffStat(output);
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Collect file change summary from git.
177
+ * @param {string} projectRoot
178
+ * @param {string} baseBranch
179
+ * @returns {{ added: string[], modified: string[], deleted: string[] } | null}
180
+ */
181
+ function collectFileChanges(projectRoot, baseBranch) {
182
+ try {
183
+ const output = execFileSync(
184
+ 'git',
185
+ ['diff', '--name-status', `${baseBranch}...HEAD`],
186
+ { cwd: projectRoot, encoding: 'utf8', timeout: 15_000 },
187
+ );
188
+
189
+ return parseNameStatus(output);
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * List artifact files in run directories.
197
+ * @param {string} stateRoot
198
+ * @param {string} runId
199
+ * @returns {string[]}
200
+ */
201
+ function collectArtifacts(stateRoot, runId) {
202
+ const artifacts = [];
203
+ const clarifiedDir = path.join(stateRoot, 'runs', runId, 'clarified');
204
+ const artifactsDir = path.join(stateRoot, 'runs', runId, 'artifacts');
205
+
206
+ for (const dir of [clarifiedDir, artifactsDir]) {
207
+ if (fs.existsSync(dir)) {
208
+ try {
209
+ const entries = fs.readdirSync(dir);
210
+
211
+ for (const entry of entries) {
212
+ artifacts.push(path.join(dir, entry));
213
+ }
214
+ } catch {
215
+ // ignore read errors
216
+ }
217
+ }
218
+ }
219
+
220
+ return artifacts;
221
+ }
222
+
223
+ /**
224
+ * Compute phase duration from events.ndjson.
225
+ * Looks for phase_enter and phase_exit events matching the given phase.
226
+ * @param {string} stateRoot
227
+ * @param {string} runId
228
+ * @param {string} phase
229
+ * @returns {number | null} duration in seconds, or null if unavailable
230
+ */
231
+ function collectDuration(stateRoot, runId, phase) {
232
+ const eventsPath = path.join(stateRoot, 'runs', runId, 'events.ndjson');
233
+
234
+ if (!fs.existsSync(eventsPath)) {
235
+ return null;
236
+ }
237
+
238
+ try {
239
+ const content = fs.readFileSync(eventsPath, 'utf8');
240
+ const lines = content.split('\n').filter(Boolean);
241
+
242
+ let enterTime = null;
243
+ let exitTime = null;
244
+
245
+ for (const line of lines) {
246
+ const event = JSON.parse(line);
247
+
248
+ if (event.phase !== phase) {
249
+ continue;
250
+ }
251
+
252
+ if (event.event === 'phase_enter' && event.created_at) {
253
+ enterTime = new Date(event.created_at).getTime();
254
+ }
255
+
256
+ if (event.event === 'phase_exit' && event.created_at) {
257
+ exitTime = new Date(event.created_at).getTime();
258
+ }
259
+ }
260
+
261
+ if (enterTime !== null && exitTime !== null && exitTime > enterTime) {
262
+ return Math.round((exitTime - enterTime) / 1000);
263
+ }
264
+
265
+ return null;
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Collect metrics for a phase report.
273
+ * @param {object} opts
274
+ * @param {string} opts.projectRoot - project root path
275
+ * @param {string} opts.stateRoot - state root path
276
+ * @param {string} opts.runId - current run ID
277
+ * @param {string} opts.phase - phase name (init|clarifier|executor|final_review)
278
+ * @param {string} [opts.baseBranch] - base branch for diff (default: 'main')
279
+ * @returns {object} structured metrics
280
+ */
281
+ export function collectPhaseMetrics(opts) {
282
+ const { projectRoot, stateRoot, runId, phase } = opts;
283
+ const baseBranch = opts.baseBranch ?? 'main';
284
+
285
+ return {
286
+ run_id: runId,
287
+ phase,
288
+ generated_at: new Date().toISOString(),
289
+ tests: collectTestResults(projectRoot),
290
+ diff: collectDiffStats(projectRoot, baseBranch),
291
+ files: collectFileChanges(projectRoot, baseBranch),
292
+ artifacts: collectArtifacts(stateRoot, runId),
293
+ duration_seconds: collectDuration(stateRoot, runId, phase),
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Build a complete phase report JSON.
299
+ * Deterministic fields from code, qualitative fields left as placeholders for agent.
300
+ * @param {object} metrics - output from collectPhaseMetrics
301
+ * @param {object} [qualitative] - agent-provided qualitative fields
302
+ * @returns {object} complete report
303
+ */
304
+ export function buildPhaseReport(metrics, qualitative = {}) {
305
+ return {
306
+ run_id: metrics.run_id,
307
+ phase: metrics.phase,
308
+ generated_at: metrics.generated_at,
309
+ metrics: {
310
+ tests: metrics.tests ?? { total: 0, passed: 0, failed: 0, skipped: 0 },
311
+ diff: metrics.diff ?? { files_changed: 0, insertions: 0, deletions: 0 },
312
+ files: metrics.files ?? { added: [], modified: [], deleted: [] },
313
+ artifacts: metrics.artifacts ?? [],
314
+ duration_seconds: metrics.duration_seconds ?? null,
315
+ },
316
+ qualitative: {
317
+ summary: qualitative.summary ?? '',
318
+ drift_analysis: qualitative.drift_analysis ?? '',
319
+ decisions: qualitative.decisions ?? [],
320
+ risks: qualitative.risks ?? [],
321
+ },
322
+ };
323
+ }
@@ -0,0 +1,160 @@
1
+ import path from 'node:path';
2
+
3
+ import { parseCommandOptions } from '../command-options.js';
4
+ import { readYamlFile } from '../loaders.js';
5
+ import { findProjectRoot } from '../project-root.js';
6
+ import { resolveStateRoot } from '../state-root.js';
7
+ import {
8
+ closeStateDb,
9
+ getAuditTrend,
10
+ getFindingsByRun,
11
+ getLearningsByScope,
12
+ getStateCounts,
13
+ getUsageSummary,
14
+ openStateDb,
15
+ } from './db.js';
16
+
17
+ function success(payload, options = {}) {
18
+ if (options.json) {
19
+ return {
20
+ exitCode: 0,
21
+ stdout: `${JSON.stringify(payload, null, 2)}\n`,
22
+ };
23
+ }
24
+
25
+ return {
26
+ exitCode: 0,
27
+ stdout: `${options.formatText ? options.formatText(payload) : String(payload)}\n`,
28
+ };
29
+ }
30
+
31
+ function failure(message, exitCode = 1) {
32
+ return {
33
+ exitCode,
34
+ stderr: `${message}\n`,
35
+ };
36
+ }
37
+
38
+ function loadProjectContext(context, stateRootOverride) {
39
+ const projectRoot = findProjectRoot(context.cwd ?? process.cwd());
40
+ const manifest = readYamlFile(path.join(projectRoot, 'wazir.manifest.yaml'));
41
+ const stateRoot = resolveStateRoot(projectRoot, manifest, {
42
+ cwd: context.cwd ?? process.cwd(),
43
+ override: stateRootOverride,
44
+ });
45
+
46
+ return {
47
+ projectRoot,
48
+ manifest,
49
+ stateRoot,
50
+ };
51
+ }
52
+
53
+ export function runStateCommand(parsed, context = {}) {
54
+ try {
55
+ const { positional, options } = parseCommandOptions(parsed.args, {
56
+ boolean: ['json'],
57
+ string: ['state-root', 'limit'],
58
+ });
59
+ const { stateRoot } = loadProjectContext(context, options.stateRoot);
60
+
61
+ switch (parsed.subcommand) {
62
+ case 'stats': {
63
+ const db = openStateDb(stateRoot);
64
+
65
+ try {
66
+ const counts = getStateCounts(db);
67
+ const usage = getUsageSummary(db);
68
+
69
+ return success({ ...counts, usage }, {
70
+ json: options.json,
71
+ formatText: (value) => [
72
+ `Learnings: ${value.learning_count}`,
73
+ `Findings: ${value.finding_count}`,
74
+ `Audits: ${value.audit_count}`,
75
+ `Usage records: ${value.usage_count}`,
76
+ ].join('\n'),
77
+ });
78
+ } finally {
79
+ closeStateDb(db);
80
+ }
81
+ }
82
+
83
+ case 'learnings': {
84
+ const db = openStateDb(stateRoot);
85
+
86
+ try {
87
+ const limit = options.limit ? Number(options.limit) : undefined;
88
+ const learnings = getLearningsByScope(db, { limit });
89
+
90
+ return success(learnings, {
91
+ json: options.json,
92
+ formatText: (rows) => rows.length === 0
93
+ ? 'No learnings recorded.'
94
+ : rows.map((row) => {
95
+ const scope = [row.scope_roles, row.scope_stacks, row.scope_concerns]
96
+ .filter(Boolean)
97
+ .join(', ');
98
+ return `[${row.category}] ${row.content}${scope ? ` (${scope})` : ''} x${row.recurrence_count}`;
99
+ }).join('\n'),
100
+ });
101
+ } finally {
102
+ closeStateDb(db);
103
+ }
104
+ }
105
+
106
+ case 'findings': {
107
+ const runId = positional[0];
108
+
109
+ if (!runId) {
110
+ return failure('Usage: wazir state findings <run-id> [--state-root <path>] [--json]');
111
+ }
112
+
113
+ const db = openStateDb(stateRoot);
114
+
115
+ try {
116
+ const findings = getFindingsByRun(db, runId);
117
+
118
+ return success(findings, {
119
+ json: options.json,
120
+ formatText: (rows) => rows.length === 0
121
+ ? `No findings for run ${runId}.`
122
+ : rows.map((row) => {
123
+ const status = row.resolved ? 'RESOLVED' : 'OPEN';
124
+ return `[${row.severity}] [${status}] ${row.description} (${row.source}, ${row.phase})`;
125
+ }).join('\n'),
126
+ });
127
+ } finally {
128
+ closeStateDb(db);
129
+ }
130
+ }
131
+
132
+ case 'trend': {
133
+ const db = openStateDb(stateRoot);
134
+
135
+ try {
136
+ const limit = options.limit ? Number(options.limit) : 10;
137
+ const trend = getAuditTrend(db, limit);
138
+
139
+ return success(trend, {
140
+ json: options.json,
141
+ formatText: (rows) => rows.length === 0
142
+ ? 'No audit history.'
143
+ : rows.map((row) => {
144
+ const before = row.quality_score_before != null ? row.quality_score_before.toFixed(1) : '?';
145
+ const after = row.quality_score_after != null ? row.quality_score_after.toFixed(1) : '?';
146
+ return `${row.date} ${row.run_id} findings=${row.finding_count} fixed=${row.fix_count} manual=${row.manual_count} quality=${before}->${after}`;
147
+ }).join('\n'),
148
+ });
149
+ } finally {
150
+ closeStateDb(db);
151
+ }
152
+ }
153
+
154
+ default:
155
+ return failure('Usage: wazir state <stats|learnings|findings|trend> [options]');
156
+ }
157
+ } catch (error) {
158
+ return failure(error.message);
159
+ }
160
+ }