harness-everything 0.42.2 → 0.43.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/.agents/harness-everything/document-inventory.json +167 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/ci.yml +73 -0
- package/CHANGELOG.md +11 -0
- package/ci/mechanism-2ax-doc-lifecycle.test.js +816 -0
- package/ci/mechanism-325-f1-doc-check.test.js +178 -0
- package/docs/own-repo-governance-e1-plan-2026-10-25-2139.md +253 -0
- package/docs/platform-capabilities.md +8 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-lifecycle.js +296 -3
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +296 -3
- package/sbom.cdx.json +7 -7
- package/scripts/doc-governance-guard.js +335 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const GUARD_PATH = 'scripts/doc-governance-guard.js';
|
|
8
|
+
const TRUST_SURFACE_PATHS = [
|
|
9
|
+
GUARD_PATH,
|
|
10
|
+
'ci/mechanism-2ax-doc-lifecycle.test.js',
|
|
11
|
+
'ci/mechanism-325-f1-doc-check.test.js',
|
|
12
|
+
'harness-everything/scripts/doc-check.js',
|
|
13
|
+
'harness-everything/scripts/doc-lifecycle.js',
|
|
14
|
+
'harness-everything/scripts/project-docs-resolver.js',
|
|
15
|
+
'plugins/harness-everything/skills/harness-everything/scripts/doc-check.js',
|
|
16
|
+
'plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js',
|
|
17
|
+
'plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js',
|
|
18
|
+
'.agents/harness-everything/project-docs.json',
|
|
19
|
+
'.agents/harness-everything/document-inventory.json',
|
|
20
|
+
'.github/workflows/ci.yml',
|
|
21
|
+
'package.json',
|
|
22
|
+
'self-evolve/scripts/self-regression.js',
|
|
23
|
+
'ci/mechanism-test.js',
|
|
24
|
+
'ci/mutation-check.js',
|
|
25
|
+
'ci/lib/isolated-test-process.js',
|
|
26
|
+
'ci/test-helper.js',
|
|
27
|
+
'scripts/verify-all.js'
|
|
28
|
+
];
|
|
29
|
+
const TRUST_SURFACE_PREFIXES = ['ci/fixtures/doc-lifecycle/scenarios/'];
|
|
30
|
+
|
|
31
|
+
function sha256(bytes) {
|
|
32
|
+
return crypto.createHash('sha256').update(bytes).digest('hex');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function git(repo, args) {
|
|
36
|
+
const result = spawnSync('git', ['-C', repo, ...args], {
|
|
37
|
+
encoding: 'buffer',
|
|
38
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
39
|
+
env: { ...process.env, GIT_CONFIG_NOSYSTEM: '1', GIT_NO_REPLACE_OBJECTS: '1' }
|
|
40
|
+
});
|
|
41
|
+
if (result.error) throw new Error(`git ${args[0]} could not run: ${result.error.message}`);
|
|
42
|
+
if (result.status !== 0) {
|
|
43
|
+
const detail = Buffer.from(result.stderr || result.stdout || '').toString('utf8').trim();
|
|
44
|
+
throw new Error(`git ${args[0]} failed${detail ? `: ${detail}` : ''}`);
|
|
45
|
+
}
|
|
46
|
+
return Buffer.from(result.stdout || '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveCommit(repo, input, label) {
|
|
50
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(input || '')) throw new Error(`${label} must be a full Git commit SHA`);
|
|
51
|
+
const output = git(repo, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${input}^{commit}`]).toString('ascii').trim();
|
|
52
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(output) || output.toLowerCase() !== input.toLowerCase()) {
|
|
53
|
+
throw new Error(`${label} did not resolve to the supplied full commit SHA`);
|
|
54
|
+
}
|
|
55
|
+
return output.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function splitNul(bytes, label) {
|
|
59
|
+
if (!bytes.length) return [];
|
|
60
|
+
if (bytes[bytes.length - 1] !== 0) throw new Error(`${label} is not NUL terminated`);
|
|
61
|
+
const fields = [];
|
|
62
|
+
let start = 0;
|
|
63
|
+
for (let index = 0; index < bytes.length; index++) {
|
|
64
|
+
if (bytes[index] === 0) {
|
|
65
|
+
fields.push(bytes.subarray(start, index));
|
|
66
|
+
start = index + 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return fields;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseTree(bytes) {
|
|
73
|
+
const entries = new Map();
|
|
74
|
+
for (const field of splitNul(bytes, 'git ls-tree output')) {
|
|
75
|
+
const tab = field.indexOf(9);
|
|
76
|
+
if (tab < 0) throw new Error('malformed raw Git tree entry');
|
|
77
|
+
const [mode, type, oid] = field.subarray(0, tab).toString('ascii').split(' ');
|
|
78
|
+
const pathBytes = Buffer.from(field.subarray(tab + 1));
|
|
79
|
+
if (!/^[0-7]{6}$/.test(mode || '') || !['blob', 'tree', 'commit'].includes(type) || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(oid || '') || !pathBytes.length) {
|
|
80
|
+
throw new Error('malformed raw Git tree identity');
|
|
81
|
+
}
|
|
82
|
+
const key = pathBytes.toString('base64');
|
|
83
|
+
if (entries.has(key)) throw new Error('duplicate raw Git tree path');
|
|
84
|
+
entries.set(key, { mode, type, oid: oid.toLowerCase(), pathBytes });
|
|
85
|
+
}
|
|
86
|
+
return entries;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function tree(repo, commit) {
|
|
90
|
+
return parseTree(git(repo, ['ls-tree', '-r', '-z', '--full-tree', commit]));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function regularBlob(entry) {
|
|
94
|
+
return Boolean(entry && entry.type === 'blob' && (entry.mode === '100644' || entry.mode === '100755'));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readJson(repo, entries, relativePath, label) {
|
|
98
|
+
const entry = entries.get(Buffer.from(relativePath, 'utf8').toString('base64'));
|
|
99
|
+
if (!regularBlob(entry)) throw new Error(`${label} is missing or is not a regular Git blob: ${relativePath}`);
|
|
100
|
+
const bytes = git(repo, ['cat-file', 'blob', entry.oid]);
|
|
101
|
+
const source = bytes.toString('utf8');
|
|
102
|
+
if (!Buffer.from(source, 'utf8').equals(bytes)) throw new Error(`${label} is not valid UTF-8: ${relativePath}`);
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(source);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw new Error(`${label} is malformed JSON: ${relativePath} (${error.message})`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isRecord(value) {
|
|
111
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function validateProjectDocs(value, label) {
|
|
115
|
+
if (!isRecord(value) || !Array.isArray(value.governed) || !Array.isArray(value.derived)) {
|
|
116
|
+
throw new Error(`${label} must contain governed and derived arrays`);
|
|
117
|
+
}
|
|
118
|
+
const seen = new Set();
|
|
119
|
+
for (const [kind, records, field] of [['governed', value.governed, 'role'], ['derived', value.derived, 'generator']]) {
|
|
120
|
+
for (const record of records) {
|
|
121
|
+
if (!isRecord(record) || typeof record.path !== 'string' || !record.path || typeof record[field] !== 'string' || !record[field]) {
|
|
122
|
+
throw new Error(`${label} has a malformed ${kind} record`);
|
|
123
|
+
}
|
|
124
|
+
if (record.path.startsWith('/') || record.path.split('/').includes('..') || /[*?\[\]]/.test(record.path)) {
|
|
125
|
+
// shortcut: the baseline registry uses exact Git paths; glob support needs a Git-aware matcher before adoption.
|
|
126
|
+
throw new Error(`${label} has a non-exact governed or derived path: ${record.path}`);
|
|
127
|
+
}
|
|
128
|
+
const key = `${kind}:${record.path}`;
|
|
129
|
+
if (seen.has(key)) throw new Error(`${label} has duplicate ${kind} path: ${record.path}`);
|
|
130
|
+
seen.add(key);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function validateInventory(value, label) {
|
|
137
|
+
if (!isRecord(value) || value.schemaVersion !== 1 || !Array.isArray(value.documentGlobs) || !Array.isArray(value.rules)) {
|
|
138
|
+
throw new Error(`${label} must contain schemaVersion 1, documentGlobs, and rules`);
|
|
139
|
+
}
|
|
140
|
+
if (!value.documentGlobs.every(item => typeof item === 'string' && item.length)) throw new Error(`${label} has a malformed documentGlobs entry`);
|
|
141
|
+
for (const rule of value.rules) {
|
|
142
|
+
if (!isRecord(rule) || ['match', 'role', 'lifecycle', 'disposition', 'reason'].some(key => typeof rule[key] !== 'string' || !rule[key])) {
|
|
143
|
+
throw new Error(`${label} has a malformed rule`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function stable(value) {
|
|
150
|
+
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
151
|
+
if (isRecord(value)) return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stable(value[key])}`).join(',')}}`;
|
|
152
|
+
return JSON.stringify(value);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function policyChanges(base, candidate, kind) {
|
|
156
|
+
const byPath = new Map(candidate[kind].map(record => [record.path, record]));
|
|
157
|
+
const basePaths = new Set(base[kind].map(record => record.path));
|
|
158
|
+
const missingOrChanged = base[kind].filter(record => stable(byPath.get(record.path)) !== stable(record)).map(record => ({
|
|
159
|
+
path: record.path,
|
|
160
|
+
base: record,
|
|
161
|
+
candidate: byPath.get(record.path) || null
|
|
162
|
+
}));
|
|
163
|
+
const added = candidate[kind].filter(record => !basePaths.has(record.path));
|
|
164
|
+
return { state: missingOrChanged.length || added.length ? 'review_required' : 'pass', preserved: base[kind].length - missingOrChanged.length, missingOrChanged, added };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function pathKey(relativePath) {
|
|
168
|
+
return Buffer.from(relativePath, 'utf8').toString('base64');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function pathText(pathBytes) {
|
|
172
|
+
const value = pathBytes.toString('utf8');
|
|
173
|
+
return Buffer.from(value, 'utf8').equals(pathBytes) ? value : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function identity(entry) {
|
|
177
|
+
return entry ? { mode: entry.mode, type: entry.type, oid: entry.oid } : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sameIdentity(left, right) {
|
|
181
|
+
return stable(identity(left)) === stable(identity(right));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function trustSurfacePaths(baseTree, candidateTree) {
|
|
185
|
+
const fixed = TRUST_SURFACE_PATHS.map(relativePath => ({ key: pathKey(relativePath), path: relativePath }));
|
|
186
|
+
const fixedKeys = new Set(fixed.map(item => item.key));
|
|
187
|
+
const fixtureEntries = new Map();
|
|
188
|
+
for (const sourceTree of [baseTree, candidateTree]) {
|
|
189
|
+
for (const [key, entry] of sourceTree) {
|
|
190
|
+
if (fixedKeys.has(key) || !TRUST_SURFACE_PREFIXES.some(prefix => entry.pathBytes.subarray(0, Buffer.byteLength(prefix)).equals(Buffer.from(prefix)))) continue;
|
|
191
|
+
fixtureEntries.set(key, pathText(entry.pathBytes));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const fixtures = [...fixtureEntries]
|
|
195
|
+
.sort(([left], [right]) => Buffer.compare(Buffer.from(left, 'base64'), Buffer.from(right, 'base64')))
|
|
196
|
+
.map(([key, path]) => ({ key, path }));
|
|
197
|
+
return [...fixed, ...fixtures];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function documentPath(pathBytes) {
|
|
201
|
+
const lower = pathBytes.toString('latin1').toLowerCase();
|
|
202
|
+
return /\.(?:md|mdx|markdown|rst|adoc|txt)$/.test(lower)
|
|
203
|
+
|| /(?:^|\/)(?:readme|license|notice|contributing|changelog|code_of_conduct|security)$/.test(lower)
|
|
204
|
+
|| /(?:^|\/)(?:readme|changelog)[^/]*$/.test(lower);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function unknownNewDocuments(baseTree, candidateTree) {
|
|
208
|
+
const result = [];
|
|
209
|
+
for (const [key, entry] of candidateTree) {
|
|
210
|
+
if (baseTree.has(key) || !documentPath(entry.pathBytes)) continue;
|
|
211
|
+
result.push({
|
|
212
|
+
path: pathText(entry.pathBytes),
|
|
213
|
+
pathBase64: key,
|
|
214
|
+
...identity(entry),
|
|
215
|
+
role: 'unknown',
|
|
216
|
+
disposition: 'unknown',
|
|
217
|
+
validation: 'unchecked',
|
|
218
|
+
reason: 'New document has no baseline governed/derived record; candidate policy cannot approve it.'
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return result.sort((left, right) => left.pathBase64.localeCompare(right.pathBase64));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function checkConfiguredPaths(baseTree, candidateTree, config) {
|
|
225
|
+
const details = [];
|
|
226
|
+
for (const kind of ['governed', 'derived']) {
|
|
227
|
+
for (const record of config[kind]) {
|
|
228
|
+
const key = pathKey(record.path);
|
|
229
|
+
const before = baseTree.get(key);
|
|
230
|
+
const after = candidateTree.get(key);
|
|
231
|
+
if (!regularBlob(before)) throw new Error(`baseline ${kind} path is missing or not a regular file: ${record.path}`);
|
|
232
|
+
const identityPreserved = regularBlob(after) && before.mode === after.mode && before.oid === after.oid;
|
|
233
|
+
details.push({ kind, path: record.path, state: identityPreserved ? 'pass' : 'review_required', base: identity(before), candidate: identity(after) });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const failures = details.filter(item => item.state !== 'pass');
|
|
237
|
+
return { state: failures.length ? 'review_required' : 'pass', paths: details, missingOrChanged: failures };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function parseArgs(argv) {
|
|
241
|
+
const values = {};
|
|
242
|
+
for (let index = 0; index < argv.length; index++) {
|
|
243
|
+
const name = argv[index];
|
|
244
|
+
if (!['--repo', '--base', '--candidate'].includes(name) || values[name]) throw new Error(`unexpected or repeated argument: ${name}`);
|
|
245
|
+
const value = argv[++index];
|
|
246
|
+
if (!value || value.startsWith('--')) throw new Error(`missing value for ${name}`);
|
|
247
|
+
values[name] = value;
|
|
248
|
+
}
|
|
249
|
+
for (const name of ['--repo', '--base', '--candidate']) if (!values[name]) throw new Error(`required argument missing: ${name}`);
|
|
250
|
+
return { repo: values['--repo'], baseInput: values['--base'], candidateInput: values['--candidate'] };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function run(argv) {
|
|
254
|
+
let verifierBytes = Buffer.alloc(0);
|
|
255
|
+
try { verifierBytes = fs.readFileSync(__filename); } catch (_) {}
|
|
256
|
+
const report = {
|
|
257
|
+
schema: 'harness.doc-governance-guard.v1',
|
|
258
|
+
verdict: 'FAIL',
|
|
259
|
+
inputs: { repo: null, base: { input: null, commit: null }, candidate: { input: null, commit: null } },
|
|
260
|
+
verifier: {
|
|
261
|
+
path: GUARD_PATH,
|
|
262
|
+
sha256: sha256(verifierBytes),
|
|
263
|
+
provenance: 'unverified',
|
|
264
|
+
baseObject: null,
|
|
265
|
+
matchesBaseObject: false
|
|
266
|
+
},
|
|
267
|
+
policyPreservation: { state: 'unchecked', governed: null, derived: null, configuredPaths: null },
|
|
268
|
+
trustSurface: { state: 'unchecked', paths: [], changed: [] },
|
|
269
|
+
newDocuments: [],
|
|
270
|
+
reviewAuthority: 'External reviewer only; candidate-controlled approval files or reports are never read.',
|
|
271
|
+
limits: [
|
|
272
|
+
'Candidate-owned CI is not independent enforcement; bootstrap remains unchecked until an external reviewer binds frozen guard bytes to final exact SHAs.',
|
|
273
|
+
'Document semantics and derived-view parity are not validated; configured documents are required to keep their baseline Git identities.'
|
|
274
|
+
],
|
|
275
|
+
errors: []
|
|
276
|
+
};
|
|
277
|
+
let bootstrapUnchecked = false;
|
|
278
|
+
try {
|
|
279
|
+
const args = parseArgs(argv);
|
|
280
|
+
report.inputs.repo = args.repo;
|
|
281
|
+
report.inputs.base.input = args.baseInput;
|
|
282
|
+
report.inputs.candidate.input = args.candidateInput;
|
|
283
|
+
const repo = args.repo;
|
|
284
|
+
const base = resolveCommit(repo, args.baseInput, '--base');
|
|
285
|
+
const candidate = resolveCommit(repo, args.candidateInput, '--candidate');
|
|
286
|
+
report.inputs.base.commit = base;
|
|
287
|
+
report.inputs.candidate.commit = candidate;
|
|
288
|
+
const baseTree = tree(repo, base);
|
|
289
|
+
const candidateTree = tree(repo, candidate);
|
|
290
|
+
|
|
291
|
+
const baseGuard = baseTree.get(pathKey(GUARD_PATH));
|
|
292
|
+
if (regularBlob(baseGuard)) {
|
|
293
|
+
const bytes = git(repo, ['cat-file', 'blob', baseGuard.oid]);
|
|
294
|
+
report.verifier.baseObject = identity(baseGuard);
|
|
295
|
+
report.verifier.matchesBaseObject = bytes.equals(verifierBytes);
|
|
296
|
+
}
|
|
297
|
+
report.verifier.provenance = report.verifier.matchesBaseObject ? 'exact bytes match baseline Git object' : 'baseline verifier object unavailable or differs';
|
|
298
|
+
if (!report.verifier.matchesBaseObject) bootstrapUnchecked = true;
|
|
299
|
+
|
|
300
|
+
const baseProject = validateProjectDocs(readJson(repo, baseTree, '.agents/harness-everything/project-docs.json', 'baseline project-docs policy'), 'baseline project-docs policy');
|
|
301
|
+
const candidateProject = validateProjectDocs(readJson(repo, candidateTree, '.agents/harness-everything/project-docs.json', 'candidate project-docs policy'), 'candidate project-docs policy');
|
|
302
|
+
validateInventory(readJson(repo, baseTree, '.agents/harness-everything/document-inventory.json', 'baseline document inventory'), 'baseline document inventory');
|
|
303
|
+
validateInventory(readJson(repo, candidateTree, '.agents/harness-everything/document-inventory.json', 'candidate document inventory'), 'candidate document inventory');
|
|
304
|
+
|
|
305
|
+
report.policyPreservation.governed = policyChanges(baseProject, candidateProject, 'governed');
|
|
306
|
+
report.policyPreservation.derived = policyChanges(baseProject, candidateProject, 'derived');
|
|
307
|
+
report.policyPreservation.configuredPaths = checkConfiguredPaths(baseTree, candidateTree, baseProject);
|
|
308
|
+
report.policyPreservation.state = [report.policyPreservation.governed, report.policyPreservation.derived, report.policyPreservation.configuredPaths]
|
|
309
|
+
.some(item => item.state !== 'pass') ? 'review_required' : 'pass';
|
|
310
|
+
|
|
311
|
+
report.trustSurface.paths = trustSurfacePaths(baseTree, candidateTree).map(({ key, path }) => {
|
|
312
|
+
const before = baseTree.get(key);
|
|
313
|
+
const after = candidateTree.get(key);
|
|
314
|
+
return { path, ...(path === null ? { pathBase64: key } : {}), base: identity(before), candidate: identity(after), changed: !sameIdentity(before, after) };
|
|
315
|
+
});
|
|
316
|
+
report.trustSurface.changed = report.trustSurface.paths.filter(item => item.changed);
|
|
317
|
+
report.trustSurface.state = report.trustSurface.changed.length ? 'review_required' : 'pass';
|
|
318
|
+
report.newDocuments = unknownNewDocuments(baseTree, candidateTree);
|
|
319
|
+
|
|
320
|
+
const reviewRequired = report.policyPreservation.state === 'review_required' || report.trustSurface.state === 'review_required';
|
|
321
|
+
report.verdict = bootstrapUnchecked ? 'BOOTSTRAP_UNCHECKED' : reviewRequired ? 'REVIEW_REQUIRED' : 'PASS';
|
|
322
|
+
} catch (error) {
|
|
323
|
+
report.errors.push({ message: error.message || String(error) });
|
|
324
|
+
report.verdict = bootstrapUnchecked ? 'BOOTSTRAP_UNCHECKED' : 'FAIL';
|
|
325
|
+
}
|
|
326
|
+
return report;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function exitCode(verdict) {
|
|
330
|
+
return verdict === 'PASS' ? 0 : verdict === 'REVIEW_REQUIRED' ? 2 : verdict === 'BOOTSTRAP_UNCHECKED' ? 3 : 1;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const report = run(process.argv.slice(2));
|
|
334
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
335
|
+
process.exitCode = exitCode(report.verdict);
|