@nerviq/cli 1.9.0 → 1.11.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.
@@ -1,123 +1,127 @@
1
- /**
2
- * Aider Freshness Operationalization
3
- *
4
- * Release gates, recurring probes, propagation checklists,
5
- * and staleness blocking for Aider surfaces.
6
- */
7
-
8
- const { version } = require('../../package.json');
9
-
10
- /**
11
- * P0 sources that must be fresh before any Aider release claim.
12
- */
13
- const P0_SOURCES = [
14
- {
15
- key: 'aider-docs',
16
- label: 'Aider Official Docs',
17
- url: 'https://aider.chat',
18
- stalenessThresholdDays: 30,
19
- verifiedAt: null,
20
- },
21
- {
22
- key: 'aider-config-reference',
23
- label: 'Aider Config Reference',
24
- url: 'https://aider.chat/docs/config/aider_conf.html',
25
- stalenessThresholdDays: 30,
26
- verifiedAt: null,
27
- },
28
- {
29
- key: 'aider-github-releases',
30
- label: 'Aider GitHub Releases',
31
- url: 'https://github.com/paul-gauthier/aider/releases',
32
- stalenessThresholdDays: 14,
33
- verifiedAt: null,
34
- },
35
- {
36
- key: 'aider-model-docs',
37
- label: 'Aider Model Documentation',
38
- url: 'https://aider.chat/docs/llms.html',
39
- stalenessThresholdDays: 30,
40
- verifiedAt: null,
41
- },
42
- {
43
- key: 'aider-pypi',
44
- label: 'Aider PyPI Package',
45
- url: 'https://pypi.org/project/aider-chat/',
46
- stalenessThresholdDays: 14,
47
- verifiedAt: null,
48
- },
49
- ];
50
-
51
- /**
52
- * Propagation checklist: when an Aider source changes, these must update.
53
- */
54
- const PROPAGATION_CHECKLIST = [
55
- {
56
- trigger: 'Aider release with new config keys',
57
- targets: [
58
- 'src/aider/techniques.js — update checks for new keys',
59
- 'src/aider/config-parser.js — update if YAML handling changes',
60
- 'src/aider/setup.js — update generated .aider.conf.yml template',
61
- ],
62
- },
63
- {
64
- trigger: 'New Aider model support or role changes',
65
- targets: [
66
- 'src/aider/techniques.js — update model config checks',
67
- 'src/aider/context.js — update modelRoles()',
68
- 'src/aider/governance.js — update policy packs if needed',
69
- ],
70
- },
71
- {
72
- trigger: 'New Aider edit format or architect changes',
73
- targets: [
74
- 'src/aider/techniques.js — update edit format checks',
75
- 'src/aider/setup.js — update template comments',
76
- ],
77
- },
78
- {
79
- trigger: 'Aider CLI flag changes (renamed/removed)',
80
- targets: [
81
- 'src/aider/techniques.js — update flag pattern matching',
82
- 'src/aider/setup.js — update generated config',
83
- 'src/aider/interactive.js — update wizard options',
84
- ],
85
- },
86
- {
87
- trigger: 'Aider domain pack definitions change',
88
- targets: [
89
- 'src/aider/domain-packs.js — update pack registry',
90
- 'src/aider/governance.js — governance export picks up changes',
91
- ],
92
- },
93
- ];
94
-
95
- /**
1
+ /**
2
+ * Aider Freshness Operationalization
3
+ *
4
+ * Release gates, recurring probes, propagation checklists,
5
+ * and staleness blocking for Aider surfaces.
6
+ */
7
+
8
+ const { version } = require('../../package.json');
9
+
10
+ /**
11
+ * P0 sources that must be fresh before any Aider release claim.
12
+ */
13
+ const P0_SOURCES = [
14
+ {
15
+ key: 'aider-docs',
16
+ label: 'Aider Official Docs',
17
+ url: 'https://aider.chat',
18
+ stalenessThresholdDays: 30,
19
+ verifiedAt: '2026-04-08',
20
+ },
21
+ {
22
+ key: 'aider-config-reference',
23
+ label: 'Aider Config Reference',
24
+ url: 'https://aider.chat/docs/config/aider_conf.html',
25
+ stalenessThresholdDays: 30,
26
+ verifiedAt: '2026-04-08',
27
+ },
28
+ {
29
+ key: 'aider-github-releases',
30
+ label: 'Aider GitHub Releases',
31
+ url: 'https://github.com/Aider-AI/aider/releases',
32
+ stalenessThresholdDays: 14,
33
+ verifiedAt: '2026-04-08',
34
+ },
35
+ {
36
+ key: 'aider-model-docs',
37
+ label: 'Aider Model Documentation',
38
+ url: 'https://aider.chat/docs/llms.html',
39
+ stalenessThresholdDays: 30,
40
+ verifiedAt: '2026-04-08',
41
+ },
42
+ {
43
+ key: 'aider-pypi',
44
+ label: 'Aider PyPI Package',
45
+ url: 'https://pypi.org/project/aider-chat/',
46
+ stalenessThresholdDays: 14,
47
+ verifiedAt: '2026-04-08',
48
+ },
49
+ ];
50
+
51
+ /**
52
+ * Propagation checklist: when an Aider source changes, these must update.
53
+ */
54
+ const PROPAGATION_CHECKLIST = [
55
+ {
56
+ trigger: 'Aider release with new config keys',
57
+ targets: [
58
+ 'src/aider/techniques.js — update checks for new keys',
59
+ 'src/aider/config-parser.js — update if YAML handling changes',
60
+ 'src/aider/setup.js — update generated .aider.conf.yml template',
61
+ ],
62
+ },
63
+ {
64
+ trigger: 'New Aider model support or role changes',
65
+ targets: [
66
+ 'src/aider/techniques.js — update model config checks',
67
+ 'src/aider/context.js — update modelRoles()',
68
+ 'src/aider/governance.js — update policy packs if needed',
69
+ ],
70
+ },
71
+ {
72
+ trigger: 'New Aider edit format or architect changes',
73
+ targets: [
74
+ 'src/aider/techniques.js — update edit format checks',
75
+ 'src/aider/setup.js — update template comments',
76
+ ],
77
+ },
78
+ {
79
+ trigger: 'Aider CLI flag changes (renamed/removed)',
80
+ targets: [
81
+ 'src/aider/techniques.js — update flag pattern matching',
82
+ 'src/aider/setup.js — update generated config',
83
+ 'src/aider/interactive.js — update wizard options',
84
+ ],
85
+ },
86
+ {
87
+ trigger: 'Aider domain pack definitions change',
88
+ targets: [
89
+ 'src/aider/domain-packs.js — update pack registry',
90
+ 'src/aider/governance.js — governance export picks up changes',
91
+ ],
92
+ },
93
+ ];
94
+
95
+ /**
96
96
  * Check release gate — are all P0 sources fresh?
97
97
  */
98
98
  function checkReleaseGate(overrides = {}) {
99
99
  const now = new Date();
100
100
  const results = P0_SOURCES.map(source => {
101
- const verifiedAt = overrides[source.key] || source.verifiedAt;
102
- if (!verifiedAt) {
103
- return { ...source, status: 'unverified', daysStale: null };
104
- }
105
-
106
- const verifiedDate = new Date(verifiedAt);
107
- const daysSince = Math.floor((now - verifiedDate) / (1000 * 60 * 60 * 24));
108
-
109
- return {
110
- ...source,
111
- verifiedAt,
112
- status: daysSince <= source.stalenessThresholdDays ? 'fresh' : 'stale',
113
- daysStale: daysSince,
114
- };
115
- });
116
-
117
- const allFresh = results.every(r => r.status === 'fresh');
101
+ const verifiedAt = overrides[source.key] || source.verifiedAt;
102
+ if (!verifiedAt) {
103
+ return { ...source, status: 'unverified', daysStale: null };
104
+ }
105
+
106
+ const verifiedDate = new Date(verifiedAt);
107
+ const daysSince = Math.floor((now - verifiedDate) / (1000 * 60 * 60 * 24));
108
+
109
+ return {
110
+ ...source,
111
+ verifiedAt,
112
+ status: daysSince <= source.stalenessThresholdDays ? 'fresh' : 'stale',
113
+ daysStale: daysSince,
114
+ };
115
+ });
116
+
117
+ const stale = results.filter((result) => result.status === 'stale' || result.status === 'unverified');
118
+ const fresh = results.filter((result) => result.status === 'fresh');
119
+ const allFresh = stale.length === 0;
118
120
 
119
121
  return {
120
122
  ready: allFresh,
123
+ stale,
124
+ fresh,
121
125
  results,
122
126
  nerviqVersion: version,
123
127
  checkedAt: now.toISOString(),
@@ -127,42 +131,41 @@ function checkReleaseGate(overrides = {}) {
127
131
  /**
128
132
  * Format release gate for display.
129
133
  */
130
- function formatReleaseGate(overrides = {}) {
131
- const gateResult = checkReleaseGate(overrides);
134
+ function formatReleaseGate(gateResult) {
132
135
  const lines = [
133
136
  `Aider Release Freshness Gate (nerviq v${version})`,
134
137
  `Status: ${gateResult.ready ? 'READY' : 'NOT READY'}`,
135
138
  '',
136
139
  ];
137
-
138
- for (const result of gateResult.results) {
139
- const icon = result.status === 'fresh' ? '✓' : result.status === 'stale' ? '✗' : '?';
140
- const age = result.daysStale !== null ? ` (${result.daysStale}d ago)` : ' (unverified)';
141
- lines.push(` ${icon} ${result.label}${age} — threshold: ${result.stalenessThresholdDays}d`);
142
- }
143
-
144
- if (!gateResult.ready) {
145
- lines.push('');
146
- lines.push('Action required: verify stale/unverified sources before claiming release freshness.');
147
- }
148
-
149
- return lines.join('\n');
150
- }
151
-
152
- /**
153
- * Get propagation targets for a given trigger.
154
- */
155
- function getPropagationTargets(triggerKeyword) {
156
- const keyword = triggerKeyword.toLowerCase();
157
- return PROPAGATION_CHECKLIST.filter(item =>
158
- item.trigger.toLowerCase().includes(keyword)
159
- );
160
- }
161
-
162
- module.exports = {
163
- P0_SOURCES,
164
- PROPAGATION_CHECKLIST,
165
- checkReleaseGate,
166
- formatReleaseGate,
167
- getPropagationTargets,
168
- };
140
+
141
+ for (const result of gateResult.results) {
142
+ const icon = result.status === 'fresh' ? '✓' : result.status === 'stale' ? '✗' : '?';
143
+ const age = result.daysStale !== null ? ` (${result.daysStale}d ago)` : ' (unverified)';
144
+ lines.push(` ${icon} ${result.label}${age} — threshold: ${result.stalenessThresholdDays}d`);
145
+ }
146
+
147
+ if (!gateResult.ready) {
148
+ lines.push('');
149
+ lines.push('Action required: verify stale/unverified sources before claiming release freshness.');
150
+ }
151
+
152
+ return lines.join('\n');
153
+ }
154
+
155
+ /**
156
+ * Get propagation targets for a given trigger.
157
+ */
158
+ function getPropagationTargets(triggerKeyword) {
159
+ const keyword = triggerKeyword.toLowerCase();
160
+ return PROPAGATION_CHECKLIST.filter(item =>
161
+ item.trigger.toLowerCase().includes(keyword)
162
+ );
163
+ }
164
+
165
+ module.exports = {
166
+ P0_SOURCES,
167
+ PROPAGATION_CHECKLIST,
168
+ checkReleaseGate,
169
+ formatReleaseGate,
170
+ getPropagationTargets,
171
+ };
package/src/analyze.js CHANGED
@@ -11,6 +11,7 @@ const { STACKS } = require('./techniques');
11
11
  const { detectDomainPacks } = require('./domain-packs');
12
12
  const { detectCodexDomainPacks } = require('./codex/domain-packs');
13
13
  const { recommendMcpPacks } = require('./mcp-packs');
14
+ const { collectClaudeDenyRules } = require('./permission-rules');
14
15
 
15
16
  const COLORS = {
16
17
  reset: '\x1b[0m',
@@ -101,6 +102,7 @@ function collectClaudeAssets(ctx) {
101
102
  const sharedSettings = ctx.jsonFile('.claude/settings.json');
102
103
  const localSettings = ctx.jsonFile('.claude/settings.local.json');
103
104
  const settings = sharedSettings || localSettings || null;
105
+ const denyRules = collectClaudeDenyRules(ctx);
104
106
 
105
107
  const assetFiles = {
106
108
  claudeMd: ctx.fileContent('CLAUDE.md') ? 'CLAUDE.md' : (ctx.fileContent('.claude/CLAUDE.md') ? '.claude/CLAUDE.md' : null),
@@ -129,7 +131,7 @@ function collectClaudeAssets(ctx) {
129
131
  },
130
132
  permissions: settings && settings.permissions ? {
131
133
  defaultMode: settings.permissions.defaultMode || null,
132
- hasDenyRules: Array.isArray(settings.permissions.deny) && settings.permissions.deny.length > 0,
134
+ hasDenyRules: denyRules.length > 0,
133
135
  } : null,
134
136
  settingsSource: assetFiles.settings,
135
137
  summaryLine: `Commands: ${assetFiles.commands.length} | Rules: ${assetFiles.rules.length} | Hooks: ${assetFiles.hooks.length} | Agents: ${assetFiles.agents.length} | Skills: ${assetFiles.skills.length} | MCP servers: ${settings && settings.mcpServers ? Object.keys(settings.mcpServers).length : 0}`,
@@ -3,7 +3,13 @@
3
3
  * Provides a static catalog and a runtime detector that checks a project context.
4
4
  */
5
5
 
6
- const path = require('path');
6
+ const {
7
+ getRepoInstructionBundle,
8
+ hasDocumentedVerificationGuidance,
9
+ hasDocumentedTestCommand,
10
+ } = require('./instruction-surfaces');
11
+ const { collectClaudeDenyRules } = require('./permission-rules');
12
+ const { containsEmbeddedSecret } = require('./secret-patterns');
7
13
 
8
14
  const ANTI_PATTERNS = [
9
15
  {
@@ -28,7 +34,7 @@ const ANTI_PATTERNS = [
28
34
  detect: (ctx) => {
29
35
  const settings = ctx.jsonFile('.claude/settings.json') || ctx.jsonFile('.claude/settings.local.json');
30
36
  if (!settings || !settings.permissions) return true;
31
- return !Array.isArray(settings.permissions.deny) || settings.permissions.deny.length === 0;
37
+ return collectClaudeDenyRules(ctx).length === 0;
32
38
  },
33
39
  },
34
40
  {
@@ -93,15 +99,13 @@ const ANTI_PATTERNS = [
93
99
  id: 'AP007',
94
100
  name: 'No verification commands',
95
101
  severity: 'medium',
96
- description: 'Without test, lint, or build commands in CLAUDE.md, the agent cannot self-verify its changes.',
102
+ description: 'Without test, lint, or build commands across the repo instruction surfaces, agents cannot self-verify changes consistently.',
97
103
  platforms: ['claude', 'codex', 'cursor', 'windsurf', 'copilot', 'gemini', 'aider', 'opencode'],
98
- fix: 'Add ## Verification Commands section with test, lint, and build commands to your instruction file.',
104
+ fix: 'Add a canonical verification section or command doc in your repo instruction surfaces (for example CLAUDE.md, AGENTS.md, README, or platform rules).',
99
105
  detect: (ctx) => {
100
- const content = ctx.fileContent('CLAUDE.md') || ctx.fileContent('.claude/CLAUDE.md') || '';
106
+ const content = getRepoInstructionBundle(ctx);
101
107
  if (!content) return false;
102
- const hasVerification = /\b(test|lint|build|check|verify)\b/i.test(content) &&
103
- /\b(npm |yarn |pnpm |pytest|cargo |go |make )/i.test(content);
104
- return !hasVerification;
108
+ return !hasDocumentedVerificationGuidance(content);
105
109
  },
106
110
  },
107
111
  {
@@ -203,13 +207,13 @@ const ANTI_PATTERNS = [
203
207
  id: 'AP014',
204
208
  name: 'No test command defined',
205
209
  severity: 'medium',
206
- description: 'Without a test command, the agent cannot verify its changes work before presenting them for review.',
210
+ description: 'Without a canonical test command in repo instructions or scripts, agents cannot verify changes reliably before handoff.',
207
211
  platforms: ['claude', 'codex', 'cursor', 'windsurf', 'copilot', 'gemini', 'aider', 'opencode'],
208
- fix: 'Add a test command in your instruction file, e.g., "Test: npm test" or "Test: pytest".',
212
+ fix: 'Add a canonical test command in repo instructions or package scripts, e.g. "Test: npm test" or "Test: pytest".',
209
213
  detect: (ctx) => {
210
- const content = ctx.fileContent('CLAUDE.md') || ctx.fileContent('.claude/CLAUDE.md') || '';
214
+ const content = getRepoInstructionBundle(ctx);
211
215
  const pkg = ctx.jsonFile('package.json');
212
- const hasTestInMd = /(?:test|testing)\s*(?:command)?[:\s]+[`"']*(?:npm|yarn|pnpm|pytest|cargo|go|make)\s/i.test(content);
216
+ const hasTestInMd = hasDocumentedTestCommand(content);
213
217
  const hasTestScript = pkg && pkg.scripts && pkg.scripts.test;
214
218
  return !hasTestInMd && !hasTestScript;
215
219
  },
@@ -320,7 +324,7 @@ const ANTI_PATTERNS = [
320
324
  ];
321
325
  for (const file of hookFiles) {
322
326
  const content = ctx.fileContent(`.claude/hooks/${file}`) || '';
323
- if (secretPatterns.some(p => p.test(content))) {
327
+ if (secretPatterns.some(p => p.test(content)) || containsEmbeddedSecret(content)) {
324
328
  return true;
325
329
  }
326
330
  }