@nerviq/cli 1.10.0 → 1.12.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.
- package/README.md +176 -47
- package/bin/cli.js +842 -287
- package/package.json +2 -2
- package/src/activity.js +225 -59
- package/src/adoption-advisor.js +299 -0
- package/src/aider/freshness.js +28 -25
- package/src/aider/techniques.js +16 -11
- package/src/analyze.js +131 -1
- package/src/anti-patterns.js +17 -2
- package/src/audit.js +197 -96
- package/src/behavioral-drift.js +801 -0
- package/src/benchmark.js +15 -10
- package/src/continuous-ops.js +681 -0
- package/src/cost-tracking.js +61 -0
- package/src/cursor/techniques.js +17 -12
- package/src/deep-review.js +83 -0
- package/src/diff-only.js +280 -0
- package/src/doctor.js +118 -55
- package/src/governance.js +72 -50
- package/src/hook-validation.js +342 -0
- package/src/index.js +7 -1
- package/src/integrations.js +144 -60
- package/src/mcp-validation.js +337 -0
- package/src/opencode/techniques.js +12 -7
- package/src/operating-profile.js +574 -0
- package/src/org.js +97 -13
- package/src/permission-rules.js +218 -0
- package/src/plans.js +192 -8
- package/src/platform-change-manifest.js +86 -0
- package/src/policy-layers.js +210 -0
- package/src/profiles.js +4 -1
- package/src/prompt-injection.js +74 -0
- package/src/repo-archetype.js +386 -0
- package/src/secret-patterns.js +9 -0
- package/src/server.js +398 -3
- package/src/setup.js +36 -2
- package/src/source-urls.js +132 -132
- package/src/supplemental-checks.js +13 -12
- package/src/techniques/api.js +407 -0
- package/src/techniques/automation.js +316 -0
- package/src/techniques/compliance.js +257 -0
- package/src/techniques/hygiene.js +294 -0
- package/src/techniques/instructions.js +243 -0
- package/src/techniques/observability.js +226 -0
- package/src/techniques/optimization.js +142 -0
- package/src/techniques/quality.js +317 -0
- package/src/techniques/security.js +237 -0
- package/src/techniques/shared.js +443 -0
- package/src/techniques/stacks.js +2294 -0
- package/src/techniques/tools.js +106 -0
- package/src/techniques/workflow.js +413 -0
- package/src/techniques.js +78 -5611
- package/src/terminology.js +73 -0
- package/src/token-estimate.js +35 -0
- package/src/watch.js +18 -0
- package/src/windsurf/techniques.js +17 -12
- package/src/workspace.js +105 -8
package/src/org.js
CHANGED
|
@@ -1,48 +1,119 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { resolvePolicyLayers, applyPolicyLayersToOptions } = require('./policy-layers');
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
function summarizePolicyCoverage(contract) {
|
|
6
|
+
const validLayers = (contract?.layers || []).filter((layer) => layer.valid);
|
|
7
|
+
return {
|
|
8
|
+
layerCount: validLayers.length,
|
|
9
|
+
layerKeys: validLayers.map((layer) => layer.layer),
|
|
10
|
+
org: validLayers.some((layer) => layer.layer === 'org'),
|
|
11
|
+
team: validLayers.some((layer) => layer.layer === 'team'),
|
|
12
|
+
repo: validLayers.some((layer) => layer.layer === 'repo'),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildScoreBands(repos) {
|
|
17
|
+
const bands = {
|
|
18
|
+
strong: 0,
|
|
19
|
+
developing: 0,
|
|
20
|
+
bootstrap: 0,
|
|
21
|
+
unknown: 0,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
for (const repo of repos) {
|
|
25
|
+
if (typeof repo.score !== 'number') {
|
|
26
|
+
bands.unknown += 1;
|
|
27
|
+
} else if (repo.score >= 70) {
|
|
28
|
+
bands.strong += 1;
|
|
29
|
+
} else if (repo.score >= 40) {
|
|
30
|
+
bands.developing += 1;
|
|
31
|
+
} else {
|
|
32
|
+
bands.bootstrap += 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return bands;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildTopEvidence(repos) {
|
|
40
|
+
const counts = new Map();
|
|
41
|
+
for (const repo of repos) {
|
|
42
|
+
const key = repo.topActionKey;
|
|
43
|
+
if (!key) continue;
|
|
44
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return [...counts.entries()]
|
|
48
|
+
.map(([key, repoCount]) => ({ key, repoCount }))
|
|
49
|
+
.sort((a, b) => b.repoCount - a.repoCount)
|
|
50
|
+
.slice(0, 5);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function scanOrg(dirs, options = {}) {
|
|
5
54
|
const { audit } = require('./audit');
|
|
6
55
|
const targets = Array.isArray(dirs) ? dirs : [];
|
|
7
56
|
const repos = [];
|
|
57
|
+
const fallbackPlatform = options.platform || 'claude';
|
|
8
58
|
|
|
9
59
|
for (const dir of targets) {
|
|
10
|
-
const
|
|
11
|
-
|
|
60
|
+
const resolvedDir = path.resolve(dir);
|
|
61
|
+
const policyContract = resolvePolicyLayers(resolvedDir);
|
|
62
|
+
const policyCoverage = summarizePolicyCoverage(policyContract);
|
|
63
|
+
|
|
64
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
12
65
|
repos.push({
|
|
13
66
|
name: path.basename(dir),
|
|
14
|
-
dir:
|
|
15
|
-
platform,
|
|
67
|
+
dir: resolvedDir,
|
|
68
|
+
platform: fallbackPlatform,
|
|
69
|
+
scoreType: 'live-repo-audit-score',
|
|
16
70
|
score: null,
|
|
17
71
|
passed: 0,
|
|
18
72
|
total: 0,
|
|
19
73
|
topAction: null,
|
|
74
|
+
topActionKey: null,
|
|
75
|
+
policyCoverage,
|
|
76
|
+
policyLayers: policyContract,
|
|
20
77
|
error: 'directory not found',
|
|
21
78
|
});
|
|
22
79
|
continue;
|
|
23
80
|
}
|
|
24
81
|
|
|
82
|
+
const repoOptions = applyPolicyLayersToOptions(policyContract, {
|
|
83
|
+
...options,
|
|
84
|
+
dir: resolvedDir,
|
|
85
|
+
silent: true,
|
|
86
|
+
});
|
|
87
|
+
|
|
25
88
|
try {
|
|
26
|
-
const result = await audit(
|
|
89
|
+
const result = await audit(repoOptions);
|
|
27
90
|
repos.push({
|
|
28
|
-
name: path.basename(
|
|
29
|
-
dir:
|
|
30
|
-
platform,
|
|
91
|
+
name: path.basename(resolvedDir),
|
|
92
|
+
dir: resolvedDir,
|
|
93
|
+
platform: repoOptions.platform || fallbackPlatform,
|
|
94
|
+
scoreType: 'live-repo-audit-score',
|
|
31
95
|
score: result.score,
|
|
32
96
|
passed: result.passed,
|
|
33
97
|
total: result.checkCount,
|
|
34
98
|
topAction: result.topNextActions?.[0]?.name || null,
|
|
99
|
+
topActionKey: result.topNextActions?.[0]?.key || null,
|
|
100
|
+
policyCoverage,
|
|
101
|
+
policyLayers: policyContract,
|
|
35
102
|
result,
|
|
36
103
|
});
|
|
37
104
|
} catch (error) {
|
|
38
105
|
repos.push({
|
|
39
|
-
name: path.basename(
|
|
40
|
-
dir:
|
|
41
|
-
platform,
|
|
106
|
+
name: path.basename(resolvedDir),
|
|
107
|
+
dir: resolvedDir,
|
|
108
|
+
platform: repoOptions.platform || fallbackPlatform,
|
|
109
|
+
scoreType: 'live-repo-audit-score',
|
|
42
110
|
score: null,
|
|
43
111
|
passed: 0,
|
|
44
112
|
total: 0,
|
|
45
113
|
topAction: null,
|
|
114
|
+
topActionKey: null,
|
|
115
|
+
policyCoverage,
|
|
116
|
+
policyLayers: policyContract,
|
|
46
117
|
error: error.message,
|
|
47
118
|
});
|
|
48
119
|
}
|
|
@@ -54,11 +125,24 @@ async function scanOrg(dirs, platform = 'claude') {
|
|
|
54
125
|
: 0;
|
|
55
126
|
|
|
56
127
|
return {
|
|
57
|
-
platform,
|
|
128
|
+
platform: fallbackPlatform,
|
|
58
129
|
repoCount: repos.length,
|
|
59
130
|
averageScore,
|
|
131
|
+
scoreType: 'org-live-average-score',
|
|
132
|
+
scoreSemantics: {
|
|
133
|
+
repoScoreType: 'live-repo-audit-score',
|
|
134
|
+
rollupScoreType: 'org-live-average-score',
|
|
135
|
+
note: 'Repo rows are live per-repo audits. The org average is a rollup across those live repo scores, not a snapshot score or benchmark projection.',
|
|
136
|
+
},
|
|
60
137
|
maxScore: validScores.length > 0 ? Math.max(...validScores) : 0,
|
|
61
138
|
minScore: validScores.length > 0 ? Math.min(...validScores) : 0,
|
|
139
|
+
scoreBands: buildScoreBands(repos),
|
|
140
|
+
policyCoverage: {
|
|
141
|
+
orgPolicyRepos: repos.filter((repo) => repo.policyCoverage.org).length,
|
|
142
|
+
teamPolicyRepos: repos.filter((repo) => repo.policyCoverage.team).length,
|
|
143
|
+
repoPolicyRepos: repos.filter((repo) => repo.policyCoverage.repo).length,
|
|
144
|
+
},
|
|
145
|
+
topEvidence: buildTopEvidence(repos),
|
|
62
146
|
repos,
|
|
63
147
|
};
|
|
64
148
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const PATH_ACTIONS = new Set(['read', 'write', 'edit', 'multiedit']);
|
|
5
|
+
const SECRET_PATH_RE = /(^|\/)(\.env(?:[^/]*)?|secrets?)(\/|$)/i;
|
|
6
|
+
|
|
7
|
+
function normalizeSlash(value) {
|
|
8
|
+
return String(value || '').replace(/\\/g, '/');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function stripWrappingQuotes(value) {
|
|
12
|
+
const trimmed = String(value || '').trim();
|
|
13
|
+
if (!trimmed) return '';
|
|
14
|
+
const first = trimmed[0];
|
|
15
|
+
const last = trimmed[trimmed.length - 1];
|
|
16
|
+
if ((first === '"' || first === "'") && first === last) {
|
|
17
|
+
return trimmed.slice(1, -1);
|
|
18
|
+
}
|
|
19
|
+
return trimmed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getProjectRoot(rootDir) {
|
|
23
|
+
try {
|
|
24
|
+
return fs.realpathSync.native(rootDir);
|
|
25
|
+
} catch {
|
|
26
|
+
return path.resolve(rootDir);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function splitPatternSegments(rawPattern, isAbsolute) {
|
|
31
|
+
const normalized = normalizeSlash(rawPattern);
|
|
32
|
+
|
|
33
|
+
if (/^[A-Za-z]:\//.test(normalized)) {
|
|
34
|
+
return normalized.slice(3).split('/').filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (isAbsolute && normalized.startsWith('/')) {
|
|
38
|
+
return normalized.slice(1).split('/').filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return normalized.split('/').filter((segment) => segment && segment !== '.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasGlob(segment) {
|
|
45
|
+
return /[*?[\]{}]/.test(segment);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildAbsolutePattern(rootDir, rawPattern) {
|
|
49
|
+
const normalized = stripWrappingQuotes(normalizeSlash(rawPattern).replace(/^file:\/\//i, ''));
|
|
50
|
+
if (!normalized) {
|
|
51
|
+
return {
|
|
52
|
+
absolutePattern: null,
|
|
53
|
+
normalizedInput: '',
|
|
54
|
+
isAbsolute: false,
|
|
55
|
+
traversalSegments: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isAbsolute = /^[A-Za-z]:\//.test(normalized) || normalized.startsWith('/');
|
|
60
|
+
const traversalSegments = normalized.split('/').some((segment) => segment === '..');
|
|
61
|
+
const segments = splitPatternSegments(normalized, isAbsolute);
|
|
62
|
+
let current = isAbsolute ? path.parse(path.resolve(normalized)).root : getProjectRoot(rootDir);
|
|
63
|
+
|
|
64
|
+
for (const segment of segments) {
|
|
65
|
+
const candidate = path.join(current, segment);
|
|
66
|
+
if (hasGlob(segment)) {
|
|
67
|
+
current = candidate;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
current = fs.realpathSync.native(candidate);
|
|
73
|
+
} catch {
|
|
74
|
+
current = candidate;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
absolutePattern: current,
|
|
80
|
+
normalizedInput: normalized,
|
|
81
|
+
isAbsolute,
|
|
82
|
+
traversalSegments,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizePathPayload(rawPayload, rootDir) {
|
|
87
|
+
const {
|
|
88
|
+
absolutePattern,
|
|
89
|
+
normalizedInput,
|
|
90
|
+
isAbsolute,
|
|
91
|
+
traversalSegments,
|
|
92
|
+
} = buildAbsolutePattern(rootDir, rawPayload);
|
|
93
|
+
|
|
94
|
+
if (!absolutePattern) {
|
|
95
|
+
return {
|
|
96
|
+
normalizedPath: '',
|
|
97
|
+
repoRelativePath: '',
|
|
98
|
+
outsideRepo: false,
|
|
99
|
+
invalid: true,
|
|
100
|
+
isAbsolute,
|
|
101
|
+
traversalSegments,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const projectRoot = getProjectRoot(rootDir);
|
|
106
|
+
const relativePath = normalizeSlash(path.relative(projectRoot, absolutePattern));
|
|
107
|
+
const outsideRepo = relativePath === '..' || relativePath.startsWith('../') || /^[A-Za-z]:\//.test(relativePath);
|
|
108
|
+
const repoRelativePath = outsideRepo ? null : relativePath || '.';
|
|
109
|
+
const normalizedPath = outsideRepo
|
|
110
|
+
? normalizeSlash(absolutePattern)
|
|
111
|
+
: `./${repoRelativePath}`;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
normalizedPath,
|
|
115
|
+
repoRelativePath,
|
|
116
|
+
outsideRepo,
|
|
117
|
+
invalid: traversalSegments && outsideRepo && !isAbsolute,
|
|
118
|
+
isAbsolute,
|
|
119
|
+
traversalSegments,
|
|
120
|
+
normalizedInput,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeCommandPayload(rawPayload) {
|
|
125
|
+
return stripWrappingQuotes(rawPayload).replace(/\s+/g, ' ').trim();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizePermissionRule(rule, rootDir) {
|
|
129
|
+
if (typeof rule !== 'string' || !rule.trim()) return null;
|
|
130
|
+
const trimmed = rule.trim();
|
|
131
|
+
const match = trimmed.match(/^([A-Za-z]+)\((.*)\)$/);
|
|
132
|
+
if (!match) {
|
|
133
|
+
return {
|
|
134
|
+
raw: trimmed,
|
|
135
|
+
action: null,
|
|
136
|
+
payload: trimmed,
|
|
137
|
+
normalized: trimmed,
|
|
138
|
+
dedupeKey: trimmed.toLowerCase(),
|
|
139
|
+
kind: 'raw',
|
|
140
|
+
invalid: false,
|
|
141
|
+
outsideRepo: false,
|
|
142
|
+
protectsSecrets: false,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const action = match[1];
|
|
147
|
+
const payload = match[2].trim();
|
|
148
|
+
const actionKey = action.toLowerCase();
|
|
149
|
+
|
|
150
|
+
if (PATH_ACTIONS.has(actionKey)) {
|
|
151
|
+
const details = normalizePathPayload(payload, rootDir);
|
|
152
|
+
const dedupeKey = `${actionKey}:${details.normalizedPath.toLowerCase()}`;
|
|
153
|
+
return {
|
|
154
|
+
raw: trimmed,
|
|
155
|
+
action,
|
|
156
|
+
payload,
|
|
157
|
+
normalized: `${action}(${details.normalizedPath})`,
|
|
158
|
+
normalizedPath: details.normalizedPath,
|
|
159
|
+
repoRelativePath: details.repoRelativePath,
|
|
160
|
+
dedupeKey,
|
|
161
|
+
kind: 'path',
|
|
162
|
+
invalid: details.invalid,
|
|
163
|
+
outsideRepo: details.outsideRepo,
|
|
164
|
+
traversalSegments: details.traversalSegments,
|
|
165
|
+
isAbsolute: details.isAbsolute,
|
|
166
|
+
protectsSecrets: !details.outsideRepo && SECRET_PATH_RE.test(details.repoRelativePath || ''),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const normalizedPayload = normalizeCommandPayload(payload);
|
|
171
|
+
return {
|
|
172
|
+
raw: trimmed,
|
|
173
|
+
action,
|
|
174
|
+
payload,
|
|
175
|
+
normalized: `${action}(${normalizedPayload})`,
|
|
176
|
+
dedupeKey: `${actionKey}:${normalizedPayload.toLowerCase()}`,
|
|
177
|
+
kind: 'command',
|
|
178
|
+
invalid: false,
|
|
179
|
+
outsideRepo: false,
|
|
180
|
+
protectsSecrets: false,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizePermissionRules(rules, rootDir) {
|
|
185
|
+
const seen = new Set();
|
|
186
|
+
const normalized = [];
|
|
187
|
+
|
|
188
|
+
for (const rule of Array.isArray(rules) ? rules : []) {
|
|
189
|
+
const entry = normalizePermissionRule(rule, rootDir);
|
|
190
|
+
if (!entry || entry.invalid) continue;
|
|
191
|
+
if (seen.has(entry.dedupeKey)) continue;
|
|
192
|
+
seen.add(entry.dedupeKey);
|
|
193
|
+
normalized.push(entry);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return normalized;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function collectClaudeDenyRules(ctx) {
|
|
200
|
+
const shared = ctx.jsonFile('.claude/settings.json');
|
|
201
|
+
const local = ctx.jsonFile('.claude/settings.local.json');
|
|
202
|
+
const denyRules = []
|
|
203
|
+
.concat(shared?.permissions?.deny || [])
|
|
204
|
+
.concat(local?.permissions?.deny || []);
|
|
205
|
+
|
|
206
|
+
return normalizePermissionRules(denyRules, ctx.dir);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function hasSecretDenyRule(rules) {
|
|
210
|
+
return (Array.isArray(rules) ? rules : []).some((rule) => rule && rule.protectsSecrets);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
collectClaudeDenyRules,
|
|
215
|
+
hasSecretDenyRule,
|
|
216
|
+
normalizePermissionRule,
|
|
217
|
+
normalizePermissionRules,
|
|
218
|
+
};
|
package/src/plans.js
CHANGED
|
@@ -59,6 +59,32 @@ const FALLBACK_TEMPLATE_BY_KEY = {
|
|
|
59
59
|
agentsHaveMaxTurns: 'agents',
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
const VERIFICATION_TRIGGER_KEYS = new Set([
|
|
63
|
+
'verificationLoop',
|
|
64
|
+
'testCommand',
|
|
65
|
+
'lintCommand',
|
|
66
|
+
'buildCommand',
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const GOVERNANCE_TRIGGER_KEYS = new Set([
|
|
70
|
+
'permissionDeny',
|
|
71
|
+
'secretsProtection',
|
|
72
|
+
'preToolUseHook',
|
|
73
|
+
'postToolUseHook',
|
|
74
|
+
'sessionStartHook',
|
|
75
|
+
'hooksInSettings',
|
|
76
|
+
'settingsPermissions',
|
|
77
|
+
'securityReview',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const AUTOMATION_TRIGGER_KEYS = new Set([
|
|
81
|
+
'customCommands',
|
|
82
|
+
'skills',
|
|
83
|
+
'agents',
|
|
84
|
+
'multipleAgents',
|
|
85
|
+
'multipleMcpServers',
|
|
86
|
+
]);
|
|
87
|
+
|
|
62
88
|
function previewContent(content) {
|
|
63
89
|
return content.split('\n').slice(0, 12).join('\n');
|
|
64
90
|
}
|
|
@@ -372,9 +398,140 @@ function toProposal(templateKey, triggers, templateFiles, ctx) {
|
|
|
372
398
|
};
|
|
373
399
|
}
|
|
374
400
|
|
|
401
|
+
function proposalMatchesTriggerKeys(proposal, keySet) {
|
|
402
|
+
return (proposal.triggers || []).some((trigger) => keySet.has(trigger.key));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function collectCampaignProposals(proposals, predicate) {
|
|
406
|
+
return proposals.filter((proposal) => proposal.readyToApply && predicate(proposal));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function buildCampaigns(bundle) {
|
|
410
|
+
const proposals = Array.isArray(bundle?.proposals) ? bundle.proposals : [];
|
|
411
|
+
const campaigns = [];
|
|
412
|
+
const maturity = bundle?.projectSummary?.maturity || 'unknown';
|
|
413
|
+
|
|
414
|
+
const starterBaseline = collectCampaignProposals(
|
|
415
|
+
proposals,
|
|
416
|
+
(proposal) => ['claude-md', 'commands', 'rules', 'hooks', 'agents'].includes(proposal.id),
|
|
417
|
+
);
|
|
418
|
+
if (starterBaseline.length > 0) {
|
|
419
|
+
campaigns.push({
|
|
420
|
+
key: 'starter-baseline',
|
|
421
|
+
label: 'Starter baseline',
|
|
422
|
+
summary: 'Establish the managed baseline surfaces Nerviq expects before deeper upgrades.',
|
|
423
|
+
proposalIds: starterBaseline.map((proposal) => proposal.id),
|
|
424
|
+
milestone: maturity === 'mature' ? 'pre-upgrade' : 'baseline',
|
|
425
|
+
focusAreas: ['config-drift', 'maturity-opportunity'],
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const verificationClosure = collectCampaignProposals(
|
|
430
|
+
proposals,
|
|
431
|
+
(proposal) => proposalMatchesTriggerKeys(proposal, VERIFICATION_TRIGGER_KEYS),
|
|
432
|
+
);
|
|
433
|
+
if (verificationClosure.length > 0) {
|
|
434
|
+
campaigns.push({
|
|
435
|
+
key: 'verification-closure',
|
|
436
|
+
label: 'Verification closure',
|
|
437
|
+
summary: 'Close missing test/lint/build loops so audits can be verified continuously.',
|
|
438
|
+
proposalIds: verificationClosure.map((proposal) => proposal.id),
|
|
439
|
+
milestone: 'post-fix',
|
|
440
|
+
focusAreas: ['config-drift', 'maturity-opportunity'],
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const governanceHardening = collectCampaignProposals(
|
|
445
|
+
proposals,
|
|
446
|
+
(proposal) => proposal.id === 'hooks' || proposalMatchesTriggerKeys(proposal, GOVERNANCE_TRIGGER_KEYS),
|
|
447
|
+
);
|
|
448
|
+
if (governanceHardening.length > 0) {
|
|
449
|
+
campaigns.push({
|
|
450
|
+
key: 'governance-hardening',
|
|
451
|
+
label: 'Governance hardening',
|
|
452
|
+
summary: 'Tighten permissions, hooks, secret protection, and reviewable safety defaults.',
|
|
453
|
+
proposalIds: governanceHardening.map((proposal) => proposal.id),
|
|
454
|
+
milestone: 'pre-upgrade',
|
|
455
|
+
focusAreas: ['policy-drift', 'config-drift'],
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const reviewableAutomation = collectCampaignProposals(
|
|
460
|
+
proposals,
|
|
461
|
+
(proposal) => proposal.id === 'agents' || proposal.id === 'commands' || proposalMatchesTriggerKeys(proposal, AUTOMATION_TRIGGER_KEYS),
|
|
462
|
+
);
|
|
463
|
+
if (reviewableAutomation.length > 0) {
|
|
464
|
+
campaigns.push({
|
|
465
|
+
key: 'reviewable-automation',
|
|
466
|
+
label: 'Reviewable automation',
|
|
467
|
+
summary: 'Add automation surfaces that keep upgrades inspectable instead of ad-hoc.',
|
|
468
|
+
proposalIds: reviewableAutomation.map((proposal) => proposal.id),
|
|
469
|
+
milestone: 'pre-upgrade',
|
|
470
|
+
focusAreas: ['maturity-opportunity', 'config-drift'],
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return campaigns.filter((campaign, index, all) =>
|
|
475
|
+
all.findIndex((item) => item.key === campaign.key) === index,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function filterBundleByCampaigns(bundle, campaignKeys = []) {
|
|
480
|
+
if (!Array.isArray(campaignKeys) || campaignKeys.length === 0) {
|
|
481
|
+
return bundle;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const available = new Map((bundle.campaigns || []).map((campaign) => [campaign.key, campaign]));
|
|
485
|
+
const missing = campaignKeys.filter((key) => !available.has(key));
|
|
486
|
+
if (missing.length > 0) {
|
|
487
|
+
throw new Error(`unknown campaign(s): ${missing.join(', ')}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const selectedIds = new Set();
|
|
491
|
+
for (const key of campaignKeys) {
|
|
492
|
+
for (const proposalId of available.get(key).proposalIds || []) {
|
|
493
|
+
selectedIds.add(proposalId);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
...bundle,
|
|
499
|
+
selectedCampaigns: campaignKeys,
|
|
500
|
+
proposals: bundle.proposals.filter((proposal) => selectedIds.has(proposal.id)),
|
|
501
|
+
campaigns: (bundle.campaigns || []).filter((campaign) => campaignKeys.includes(campaign.key)),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function resolveSelectionSet(bundle, options = {}) {
|
|
506
|
+
const proposalIds = new Set((bundle.proposals || []).map((proposal) => proposal.id));
|
|
507
|
+
const onlySet = options.only && options.only.length > 0 ? new Set(options.only) : null;
|
|
508
|
+
const campaignSet = options.campaigns && options.campaigns.length > 0
|
|
509
|
+
? new Set((bundle.campaigns || []).flatMap((campaign) => {
|
|
510
|
+
if (!options.campaigns.includes(campaign.key)) return [];
|
|
511
|
+
return campaign.proposalIds || [];
|
|
512
|
+
}))
|
|
513
|
+
: null;
|
|
514
|
+
|
|
515
|
+
if (onlySet && campaignSet) {
|
|
516
|
+
return new Set([...onlySet].filter((proposalId) => campaignSet.has(proposalId) && proposalIds.has(proposalId)));
|
|
517
|
+
}
|
|
518
|
+
if (onlySet) {
|
|
519
|
+
return new Set([...onlySet].filter((proposalId) => proposalIds.has(proposalId)));
|
|
520
|
+
}
|
|
521
|
+
if (campaignSet) {
|
|
522
|
+
return new Set([...campaignSet].filter((proposalId) => proposalIds.has(proposalId)));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
|
|
375
528
|
async function buildProposalBundle(options) {
|
|
376
529
|
if (options.platform === 'codex') {
|
|
377
|
-
|
|
530
|
+
const bundle = await buildCodexProposalBundle(options);
|
|
531
|
+
return filterBundleByCampaigns({
|
|
532
|
+
...bundle,
|
|
533
|
+
campaigns: buildCampaigns(bundle),
|
|
534
|
+
}, options.campaigns);
|
|
378
535
|
}
|
|
379
536
|
|
|
380
537
|
const ctx = new ProjectContext(options.dir);
|
|
@@ -405,7 +562,7 @@ async function buildProposalBundle(options) {
|
|
|
405
562
|
return impactB - impactA;
|
|
406
563
|
});
|
|
407
564
|
|
|
408
|
-
return {
|
|
565
|
+
return filterBundleByCampaigns({
|
|
409
566
|
schemaVersion: 1,
|
|
410
567
|
generatedBy: `nerviq@${version}`,
|
|
411
568
|
createdAt: new Date().toISOString(),
|
|
@@ -416,7 +573,8 @@ async function buildProposalBundle(options) {
|
|
|
416
573
|
riskNotes: report.riskNotes,
|
|
417
574
|
mcpPreflightWarnings,
|
|
418
575
|
proposals,
|
|
419
|
-
|
|
576
|
+
campaigns: buildCampaigns({ proposals, projectSummary: report.projectSummary }),
|
|
577
|
+
}, options.campaigns);
|
|
420
578
|
}
|
|
421
579
|
|
|
422
580
|
function printProposalBundle(bundle, options = {}) {
|
|
@@ -429,8 +587,19 @@ function printProposalBundle(bundle, options = {}) {
|
|
|
429
587
|
console.log(' nerviq plan');
|
|
430
588
|
console.log(' ═══════════════════════════════════════');
|
|
431
589
|
console.log(` ${bundle.projectSummary.name} | maturity=${bundle.projectSummary.maturity} | score=${bundle.projectSummary.score}/100`);
|
|
590
|
+
if (bundle.projectSummary.archetype) {
|
|
591
|
+
console.log(` archetype=${bundle.projectSummary.archetype} | workflow=${bundle.projectSummary.workflow || 'unknown'} | risk=${bundle.projectSummary.riskLevel || 'unknown'}`);
|
|
592
|
+
}
|
|
593
|
+
if (bundle.projectSummary.operatingProfile) {
|
|
594
|
+
console.log(` operating-profile=${bundle.projectSummary.operatingProfile}`);
|
|
595
|
+
}
|
|
432
596
|
console.log('');
|
|
433
597
|
|
|
598
|
+
if (bundle.selectedCampaigns && bundle.selectedCampaigns.length > 0) {
|
|
599
|
+
console.log(` Selected campaigns: ${bundle.selectedCampaigns.join(', ')}`);
|
|
600
|
+
console.log('');
|
|
601
|
+
}
|
|
602
|
+
|
|
434
603
|
if (bundle.mcpPreflightWarnings && bundle.mcpPreflightWarnings.length > 0) {
|
|
435
604
|
console.log(' MCP Preflight Warnings');
|
|
436
605
|
for (const warning of bundle.mcpPreflightWarnings) {
|
|
@@ -445,6 +614,16 @@ function printProposalBundle(bundle, options = {}) {
|
|
|
445
614
|
return;
|
|
446
615
|
}
|
|
447
616
|
|
|
617
|
+
if (bundle.campaigns && bundle.campaigns.length > 0) {
|
|
618
|
+
console.log(' Upgrade campaigns');
|
|
619
|
+
for (const campaign of bundle.campaigns) {
|
|
620
|
+
console.log(` - ${campaign.key} (${campaign.milestone})`);
|
|
621
|
+
console.log(` ${campaign.label} — ${campaign.summary}`);
|
|
622
|
+
console.log(` proposals: ${campaign.proposalIds.join(', ')}`);
|
|
623
|
+
}
|
|
624
|
+
console.log('');
|
|
625
|
+
}
|
|
626
|
+
|
|
448
627
|
console.log(' Proposal Bundles');
|
|
449
628
|
for (const proposal of bundle.proposals) {
|
|
450
629
|
const applyState = proposal.readyToApply ? 'ready' : 'manual-review';
|
|
@@ -536,9 +715,12 @@ function applyRuntimeSettingsOverlays(bundle, options) {
|
|
|
536
715
|
|
|
537
716
|
function resolvePlan(bundle, options) {
|
|
538
717
|
if (options.planFile) {
|
|
539
|
-
return
|
|
718
|
+
return filterBundleByCampaigns(
|
|
719
|
+
applyRuntimeSettingsOverlays(JSON.parse(fs.readFileSync(options.planFile, 'utf8')), options),
|
|
720
|
+
options.campaigns,
|
|
721
|
+
);
|
|
540
722
|
}
|
|
541
|
-
return applyRuntimeSettingsOverlays(bundle, options);
|
|
723
|
+
return filterBundleByCampaigns(applyRuntimeSettingsOverlays(bundle, options), options.campaigns);
|
|
542
724
|
}
|
|
543
725
|
|
|
544
726
|
async function applyProposalBundle(options) {
|
|
@@ -546,9 +728,7 @@ async function applyProposalBundle(options) {
|
|
|
546
728
|
const bundle = resolvePlan(liveBundle, options);
|
|
547
729
|
const mcpPreflightWarnings = getMcpPackPreflight(options.mcpPacks || [])
|
|
548
730
|
.filter(item => item.missingEnvVars.length > 0);
|
|
549
|
-
const selectedIds =
|
|
550
|
-
? new Set(options.only)
|
|
551
|
-
: null;
|
|
731
|
+
const selectedIds = resolveSelectionSet(bundle, options);
|
|
552
732
|
const selected = bundle.proposals.filter(proposal => {
|
|
553
733
|
if (selectedIds && !selectedIds.has(proposal.id)) return false;
|
|
554
734
|
return proposal.readyToApply;
|
|
@@ -606,6 +786,7 @@ async function applyProposalBundle(options) {
|
|
|
606
786
|
return {
|
|
607
787
|
proposalCount: bundle.proposals.length,
|
|
608
788
|
appliedProposalIds: selected.map(item => item.id),
|
|
789
|
+
selectedCampaigns: bundle.selectedCampaigns || options.campaigns || [],
|
|
609
790
|
createdFiles,
|
|
610
791
|
patchedFiles: patchedFiles.map(file => file.path),
|
|
611
792
|
skippedFiles,
|
|
@@ -629,6 +810,9 @@ function printApplyResult(result, options = {}) {
|
|
|
629
810
|
console.log(' Dry-run only. No files were written.');
|
|
630
811
|
}
|
|
631
812
|
console.log(` Applied proposal bundles: ${result.appliedProposalIds.join(', ') || 'none'}`);
|
|
813
|
+
if (result.selectedCampaigns && result.selectedCampaigns.length > 0) {
|
|
814
|
+
console.log(` Campaigns: ${result.selectedCampaigns.join(', ')}`);
|
|
815
|
+
}
|
|
632
816
|
console.log(` Created files: ${result.createdFiles.join(', ') || 'none'}`);
|
|
633
817
|
console.log(` Patched files: ${result.patchedFiles.join(', ') || 'none'}`);
|
|
634
818
|
if (result.mcpPreflightWarnings && result.mcpPreflightWarnings.length > 0) {
|