@nerviq/cli 1.26.0 → 1.27.1

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 (59) hide show
  1. package/CHANGELOG.md +1407 -0
  2. package/README.md +4 -4
  3. package/SECURITY.md +82 -0
  4. package/bin/cli.js +13 -1
  5. package/contracts/audit-webhook-event.schema.json +138 -0
  6. package/contracts/pack-contract.schema.json +15 -0
  7. package/contracts/technique-contract.schema.json +18 -0
  8. package/docs/ARCHITECTURE.md +74 -0
  9. package/docs/api-reference.md +356 -0
  10. package/docs/autofix.md +64 -0
  11. package/docs/bitbucket-pipe.yml +57 -0
  12. package/docs/case-studies.md +149 -0
  13. package/docs/category-definition-kit.md +56 -0
  14. package/docs/ci-integration.md +127 -0
  15. package/docs/claude-code-style.md +24 -0
  16. package/docs/claude-maintainer-ops.md +19 -0
  17. package/docs/external-validation.md +78 -0
  18. package/docs/first-tier-integration-gate.md +59 -0
  19. package/docs/getting-started.md +119 -0
  20. package/docs/gitlab-ci-template.yml +54 -0
  21. package/docs/index.html +597 -0
  22. package/docs/integration-contracts.md +287 -0
  23. package/docs/license-faq.md +53 -0
  24. package/docs/maintenance.md +155 -0
  25. package/docs/methodology.md +236 -0
  26. package/docs/new-platform-guide.md +202 -0
  27. package/docs/open-vsx-publishing.md +46 -0
  28. package/docs/platform-change-ingestion.md +46 -0
  29. package/docs/plugins.md +101 -0
  30. package/docs/pre-commit.md +58 -0
  31. package/docs/security-model.md +63 -0
  32. package/docs/shallow-risk.md +246 -0
  33. package/docs/versioning-policy.md +63 -0
  34. package/docs/why-nerviq.md +82 -0
  35. package/package.json +7 -2
  36. package/sdk/README.md +190 -0
  37. package/src/audit/layers.js +180 -179
  38. package/src/audit.js +118 -48
  39. package/src/codex/setup.js +3 -2
  40. package/src/formatters/csv.js +86 -85
  41. package/src/formatters/junit.js +123 -103
  42. package/src/formatters/markdown.js +164 -135
  43. package/src/gemini/setup.js +3 -2
  44. package/src/init.js +4 -3
  45. package/src/opencode/context.js +42 -3
  46. package/src/opencode/techniques.js +198 -142
  47. package/src/output-icons.js +44 -0
  48. package/src/setup/runtime.js +6 -5
  49. package/src/setup.js +4 -3
  50. package/src/shallow-risk/index.js +56 -0
  51. package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +50 -0
  52. package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +46 -0
  53. package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +46 -0
  54. package/src/shallow-risk/patterns/agent-config-missing-file.js +72 -0
  55. package/src/shallow-risk/patterns/agent-config-secret-literal.js +49 -0
  56. package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +34 -0
  57. package/src/shallow-risk/patterns/hook-script-missing.js +70 -0
  58. package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +52 -0
  59. package/src/shallow-risk/shared.js +520 -0
@@ -1,135 +1,164 @@
1
- /**
2
- * Markdown Formatter
3
- *
4
- * Converts a nerviq audit result into GitHub-flavoured markdown suitable
5
- * for posting as a PR comment. Structure:
6
- *
7
- * - Header with score badge and pass/fail/skip counts
8
- * - Top 5 topNextActions as a GitHub task-list checklist
9
- * - Collapsible <details> block with the full failed-checks table
10
- * - Footer with Nerviq link, version, timestamp
11
- *
12
- * Output is plain GitHub-flavoured markdown. The only HTML used is
13
- * <details>/<summary>, which GitHub renders natively.
14
- */
15
-
16
- 'use strict';
17
-
18
- const { version: nerviqVersion } = require('../../package.json');
19
-
20
- function escapeCell(value) {
21
- if (value === null || value === undefined) return '';
22
- return String(value)
23
- .replace(/\r?\n/g, ' ')
24
- .replace(/\|/g, '\\|');
25
- }
26
-
27
- function escapeInline(value) {
28
- if (value === null || value === undefined) return '';
29
- return String(value).replace(/\r?\n/g, ' ');
30
- }
31
-
32
- function scoreBadge(score) {
33
- const s = Number.isFinite(score) ? Math.round(score) : 0;
34
- let color;
35
- if (s >= 80) color = 'brightgreen';
36
- else if (s >= 60) color = 'yellow';
37
- else color = 'red';
38
- return `![score](https://img.shields.io/badge/nerviq_score-${s}%2F100-${color})`;
39
- }
40
-
41
- function severityFor(item) {
42
- return item.severity || item.impact || 'medium';
43
- }
44
-
45
- function formatMarkdown(auditResult, options = {}) {
46
- const platformLabel = auditResult.platformLabel || auditResult.platform || 'claude';
47
- const score = auditResult.score ?? 0;
48
- const passed = auditResult.passed ?? 0;
49
- const failed = auditResult.failed ?? 0;
50
- const skipped = auditResult.skipped ?? 0;
51
- const version = auditResult.version || nerviqVersion;
52
- const timestamp = auditResult.timestamp || new Date().toISOString();
53
-
54
- const lines = [];
55
-
56
- lines.push(`## Score: ${score}/100 ${scoreBadge(score)}`);
57
- lines.push('');
58
- lines.push(`**Platform:** ${platformLabel} `);
59
- lines.push(`**Checks:** ${passed} passed, ${failed} failed, ${skipped} skipped`);
60
- lines.push('');
61
-
62
- const top = Array.isArray(auditResult.topNextActions)
63
- ? auditResult.topNextActions.slice(0, 5)
64
- : [];
65
-
66
- if (top.length > 0) {
67
- lines.push('### Top next actions');
68
- lines.push('');
69
- for (const item of top) {
70
- const sev = severityFor(item).toString().toUpperCase();
71
- const title = escapeInline(item.name || item.title || item.key);
72
- const key = escapeInline(item.key || item.id || '');
73
- let loc = '';
74
- if (item.file) {
75
- loc = ` — \`${escapeInline(item.file)}${item.line ? ':' + item.line : ''}\``;
76
- }
77
- let delta = '';
78
- if (Number.isFinite(item.projectedScoreDelta) && item.projectedScoreDelta > 0) {
79
- delta = ` (+${item.projectedScoreDelta} pts ${item.projectedScoreAfter}/100)`;
80
- }
81
- // CTO-08: include scope layer as a small suffix after the key so
82
- // evaluators see which layer each next-action belongs to.
83
- const layerSuffix = item.layer ? ` · _layer: ${escapeInline(item.layer)}_` : '';
84
- lines.push(`- [ ] **[${sev}] ${title}** (\`${key}\`)${loc}${delta}${layerSuffix}`);
85
- const hint = item.fix || item.hint || '';
86
- if (hint) {
87
- lines.push(` - ${escapeInline(hint)}`);
88
- }
89
- if (item.snippet) {
90
- lines.push('');
91
- lines.push(' ```');
92
- for (const snipLine of String(item.snippet).split(/\r?\n/)) {
93
- lines.push(` ${snipLine}`);
94
- }
95
- lines.push(' ```');
96
- }
97
- }
98
- lines.push('');
99
- }
100
-
101
- const failedResults = Array.isArray(auditResult.results)
102
- ? auditResult.results.filter((r) => r.passed === false)
103
- : [];
104
-
105
- if (failedResults.length > 0) {
106
- lines.push('<details>');
107
- lines.push(`<summary>All failed checks (${failedResults.length})</summary>`);
108
- lines.push('');
109
- // CTO-08: add layer column between category and rating.
110
- lines.push('| key | name | category | layer | rating | file | line |');
111
- lines.push('| --- | --- | --- | --- | --- | --- | --- |');
112
- for (const r of failedResults) {
113
- const row = [
114
- escapeCell(r.key),
115
- escapeCell(r.name),
116
- escapeCell(r.category),
117
- escapeCell(r.layer || ''),
118
- escapeCell(r.rating ?? ''),
119
- escapeCell(r.file || ''),
120
- escapeCell(r.line || ''),
121
- ];
122
- lines.push(`| ${row.join(' | ')} |`);
123
- }
124
- lines.push('');
125
- lines.push('</details>');
126
- lines.push('');
127
- }
128
-
129
- lines.push(`---`);
130
- lines.push(`Generated by [Nerviq](https://nerviq.net) v${version} · ${timestamp}`);
131
-
132
- return lines.join('\n');
133
- }
134
-
135
- module.exports = { formatMarkdown };
1
+ /**
2
+ * Markdown Formatter
3
+ *
4
+ * Converts a nerviq audit result into GitHub-flavoured markdown suitable
5
+ * for posting as a PR comment. Structure:
6
+ *
7
+ * - Header with score badge and pass/fail/skip counts
8
+ * - Top 5 topNextActions as a GitHub task-list checklist
9
+ * - Collapsible <details> block with the full failed-checks table
10
+ * - Footer with Nerviq link, version, timestamp
11
+ *
12
+ * Output is plain GitHub-flavoured markdown. The only HTML used is
13
+ * <details>/<summary>, which GitHub renders natively.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const { version: nerviqVersion } = require('../../package.json');
19
+ const { SHALLOW_RISK_BANNER_LINES } = require('../shallow-risk');
20
+
21
+ function escapeCell(value) {
22
+ if (value === null || value === undefined) return '';
23
+ return String(value)
24
+ .replace(/\r?\n/g, ' ')
25
+ .replace(/\|/g, '\\|');
26
+ }
27
+
28
+ function escapeInline(value) {
29
+ if (value === null || value === undefined) return '';
30
+ return String(value).replace(/\r?\n/g, ' ');
31
+ }
32
+
33
+ function scoreBadge(score) {
34
+ const s = Number.isFinite(score) ? Math.round(score) : 0;
35
+ let color;
36
+ if (s >= 80) color = 'brightgreen';
37
+ else if (s >= 60) color = 'yellow';
38
+ else color = 'red';
39
+ return `![score](https://img.shields.io/badge/nerviq_score-${s}%2F100-${color})`;
40
+ }
41
+
42
+ function severityFor(item) {
43
+ return item.severity || item.impact || 'medium';
44
+ }
45
+
46
+ function formatMarkdown(auditResult, options = {}) {
47
+ const platformLabel = auditResult.platformLabel || auditResult.platform || 'claude';
48
+ const score = auditResult.score ?? 0;
49
+ const passed = auditResult.passed ?? 0;
50
+ const failed = auditResult.failed ?? 0;
51
+ const skipped = auditResult.skipped ?? 0;
52
+ const version = auditResult.version || nerviqVersion;
53
+ const timestamp = auditResult.timestamp || new Date().toISOString();
54
+
55
+ const lines = [];
56
+
57
+ lines.push(`## Score: ${score}/100 ${scoreBadge(score)}`);
58
+ lines.push('');
59
+ lines.push(`**Platform:** ${platformLabel} `);
60
+ lines.push(`**Checks:** ${passed} passed, ${failed} failed, ${skipped} skipped`);
61
+ lines.push('');
62
+
63
+ const top = Array.isArray(auditResult.topNextActions)
64
+ ? auditResult.topNextActions.slice(0, 5)
65
+ : [];
66
+
67
+ if (top.length > 0) {
68
+ lines.push('### Top next actions');
69
+ lines.push('');
70
+ for (const item of top) {
71
+ const sev = severityFor(item).toString().toUpperCase();
72
+ const title = escapeInline(item.name || item.title || item.key);
73
+ const key = escapeInline(item.key || item.id || '');
74
+ let loc = '';
75
+ if (item.file) {
76
+ loc = ` — \`${escapeInline(item.file)}${item.line ? ':' + item.line : ''}\``;
77
+ }
78
+ let delta = '';
79
+ if (Number.isFinite(item.projectedScoreDelta) && item.projectedScoreDelta > 0) {
80
+ delta = ` (+${item.projectedScoreDelta} pts → ${item.projectedScoreAfter}/100)`;
81
+ }
82
+ // CTO-08: include scope layer as a small suffix after the key so
83
+ // evaluators see which layer each next-action belongs to.
84
+ const layerSuffix = item.layer ? ` · _layer: ${escapeInline(item.layer)}_` : '';
85
+ lines.push(`- [ ] **[${sev}] ${title}** (\`${key}\`)${loc}${delta}${layerSuffix}`);
86
+ const hint = item.fix || item.hint || '';
87
+ if (hint) {
88
+ lines.push(` - ${escapeInline(hint)}`);
89
+ }
90
+ if (item.snippet) {
91
+ lines.push('');
92
+ lines.push(' ```');
93
+ for (const snipLine of String(item.snippet).split(/\r?\n/)) {
94
+ lines.push(` ${snipLine}`);
95
+ }
96
+ lines.push(' ```');
97
+ }
98
+ }
99
+ lines.push('');
100
+ }
101
+
102
+ if (Array.isArray(auditResult.shallowRiskHints)) {
103
+ lines.push('### Shallow Risk (experimental, opt-in)');
104
+ lines.push('');
105
+ for (const line of SHALLOW_RISK_BANNER_LINES) {
106
+ lines.push(`> ${line}`);
107
+ }
108
+ lines.push('');
109
+
110
+ if (auditResult.shallowRiskHints.length === 0) {
111
+ lines.push('_No shallow-risk hints found._');
112
+ lines.push('');
113
+ } else {
114
+ for (const item of auditResult.shallowRiskHints) {
115
+ const sev = severityFor(item).toString().toUpperCase();
116
+ const title = escapeInline(item.name || item.title || item.key);
117
+ let loc = '';
118
+ if (item.file) {
119
+ loc = ` — \`${escapeInline(item.file)}${item.line ? ':' + item.line : ''}\``;
120
+ }
121
+ lines.push(`- **[${sev}] ${title}** (\`${escapeInline(item.key || '')}\`)${loc}`);
122
+ if (item.fix) {
123
+ lines.push(` - ${escapeInline(item.fix)}`);
124
+ }
125
+ }
126
+ lines.push('');
127
+ }
128
+ }
129
+
130
+ const failedResults = Array.isArray(auditResult.results)
131
+ ? auditResult.results.filter((r) => r.passed === false)
132
+ : [];
133
+
134
+ if (failedResults.length > 0) {
135
+ lines.push('<details>');
136
+ lines.push(`<summary>All failed checks (${failedResults.length})</summary>`);
137
+ lines.push('');
138
+ // CTO-08: add layer column between category and rating.
139
+ lines.push('| key | name | category | layer | rating | file | line |');
140
+ lines.push('| --- | --- | --- | --- | --- | --- | --- |');
141
+ for (const r of failedResults) {
142
+ const row = [
143
+ escapeCell(r.key),
144
+ escapeCell(r.name),
145
+ escapeCell(r.category),
146
+ escapeCell(r.layer || ''),
147
+ escapeCell(r.rating ?? ''),
148
+ escapeCell(r.file || ''),
149
+ escapeCell(r.line || ''),
150
+ ];
151
+ lines.push(`| ${row.join(' | ')} |`);
152
+ }
153
+ lines.push('');
154
+ lines.push('</details>');
155
+ lines.push('');
156
+ }
157
+
158
+ lines.push(`---`);
159
+ lines.push(`Generated by [Nerviq](https://nerviq.net) v${version} · ${timestamp}`);
160
+
161
+ return lines.join('\n');
162
+ }
163
+
164
+ module.exports = { formatMarkdown };
@@ -6,6 +6,7 @@ const { STACKS } = require('../techniques');
6
6
  const { writeActivityArtifact, writeRollbackArtifact } = require('../activity');
7
7
  const { GeminiProjectContext } = require('./context');
8
8
  const { recommendGeminiMcpPacks, packToJson } = require('./mcp-packs');
9
+ const { icon } = require('../output-icons');
9
10
 
10
11
  function detectScripts(ctx) {
11
12
  const pkg = ctx.jsonFile('package.json');
@@ -622,14 +623,14 @@ async function setupGemini(options) {
622
623
  const fullPath = path.join(options.dir, file.path);
623
624
  if (fs.existsSync(fullPath)) {
624
625
  preservedFiles.push(file.path);
625
- log(` \x1b[2m⏭️ Skipped ${file.path} (already exists your version is kept)\x1b[0m`);
626
+ log(` \x1b[2m${icon('skip')} Skipped ${file.path} (already exists - your version is kept)\x1b[0m`);
626
627
  continue;
627
628
  }
628
629
 
629
630
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
630
631
  fs.writeFileSync(fullPath, file.content, 'utf8');
631
632
  writtenFiles.push(file.path);
632
- log(` \x1b[32m✅\x1b[0m Created ${file.path}`);
633
+ log(` \x1b[32m${icon('ok')}\x1b[0m Created ${file.path}`);
633
634
  }
634
635
 
635
636
  const skippedSet = new Set(preservedFiles);
package/src/init.js CHANGED
@@ -6,6 +6,7 @@ const { audit } = require('./audit');
6
6
  const { setup } = require('./setup');
7
7
  const { ProjectContext } = require('./context');
8
8
  const { STACKS } = require('./techniques');
9
+ const { icon } = require('./output-icons');
9
10
 
10
11
  const PLATFORM_LABELS = {
11
12
  claude: 'Claude Code',
@@ -136,10 +137,10 @@ async function runInit(dir, flags) {
136
137
  });
137
138
 
138
139
  for (const f of setupResult.writtenFiles) {
139
- console.log(` ${green}✅${reset} Created ${f}`);
140
+ console.log(` ${green}${icon('ok')}${reset} Created ${f}`);
140
141
  }
141
142
  for (const f of setupResult.preservedFiles) {
142
- console.log(` ${dim}⏭️ Kept ${f} (already exists)${reset}`);
143
+ console.log(` ${dim}${icon('skip')} Kept ${f} (already exists)${reset}`);
143
144
  }
144
145
 
145
146
  // --- Run additional platform setups ---
@@ -153,7 +154,7 @@ async function runInit(dir, flags) {
153
154
  mcpPacks: [],
154
155
  });
155
156
  for (const f of extraResult.writtenFiles) {
156
- console.log(` ${green}✅${reset} Created ${f}`);
157
+ console.log(` ${green}${icon('ok')}${reset} Created ${f}`);
157
158
  }
158
159
  } catch {
159
160
  // Platform setup not available, skip
@@ -203,12 +203,51 @@ class OpenCodeProjectContext extends ProjectContext {
203
203
  }
204
204
 
205
205
  skillDirs() {
206
- const skillsDir = path.join(this.dir, '.opencode', 'commands');
207
- return listDirs(skillsDir).map(entry => entry.name);
206
+ const names = new Set();
207
+ const roots = [
208
+ path.join('.opencode', 'skills'),
209
+ path.join('.opencode', 'skill'),
210
+ path.join('.claude', 'skills'),
211
+ path.join('.agents', 'skills'),
212
+ ];
213
+
214
+ for (const root of roots) {
215
+ const fullRoot = path.join(this.dir, root);
216
+ for (const entry of listDirs(fullRoot)) {
217
+ if (this.fileContent(path.join(root, entry.name, 'SKILL.md'))) {
218
+ names.add(entry.name);
219
+ }
220
+ }
221
+ }
222
+
223
+ // Legacy NERVIQ compatibility: older generated fixtures placed skills
224
+ // under .opencode/commands/<name>/SKILL.md before OpenCode's native
225
+ // .opencode/skills/ path was verified.
226
+ const legacyCommandsRoot = path.join(this.dir, '.opencode', 'commands');
227
+ for (const entry of listDirs(legacyCommandsRoot)) {
228
+ if (this.fileContent(path.join('.opencode', 'commands', entry.name, 'SKILL.md'))) {
229
+ names.add(entry.name);
230
+ }
231
+ }
232
+
233
+ return [...names];
208
234
  }
209
235
 
210
236
  skillMetadata(name) {
211
- return this.fileContent(path.join('.opencode', 'commands', name, 'SKILL.md'));
237
+ const candidates = [
238
+ path.join('.opencode', 'skills', name, 'SKILL.md'),
239
+ path.join('.opencode', 'skill', name, 'SKILL.md'),
240
+ path.join('.claude', 'skills', name, 'SKILL.md'),
241
+ path.join('.agents', 'skills', name, 'SKILL.md'),
242
+ path.join('.opencode', 'commands', name, 'SKILL.md'),
243
+ ];
244
+
245
+ for (const candidate of candidates) {
246
+ const content = this.fileContent(candidate);
247
+ if (content) return content;
248
+ }
249
+
250
+ return null;
212
251
  }
213
252
 
214
253
  themeFiles() {