@mmerterden/multi-agent-pipeline 20.8.0 → 20.8.2
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/CHANGELOG.md +48 -0
- package/docs/facts.json +3 -3
- package/index.js +1 -0
- package/install/_codex-agents.mjs +1 -1
- package/install/_common.mjs +486 -53
- package/install/_mcp-register.mjs +173 -117
- package/install/claude.mjs +281 -220
- package/install/codex.mjs +7 -7
- package/install/copilot.mjs +13 -11
- package/install/index.mjs +92 -27
- package/install/templates/claude-hooks.json +9 -9
- package/manifest.json +112 -114
- package/package.json +5 -2
- package/pipeline/commands/multi-agent/update/SKILL.md +28 -17
- package/pipeline/lib/confusables.json +79 -33
- package/pipeline/lib/extract-conventions.sh +3 -3
- package/pipeline/lib/json-file-lock.mjs +27 -7
- package/pipeline/lib/normalize-text.mjs +86 -17
- package/pipeline/lib/outbound-gate.mjs +13 -4
- package/pipeline/lib/redact.mjs +87 -13
- package/pipeline/multi-agent-refs/analysis/evidence.md +1 -1
- package/pipeline/multi-agent-refs/analysis/synthesis.md +1 -1
- package/pipeline/multi-agent-refs/component-dispatch.md +1 -1
- package/pipeline/multi-agent-refs/conventions-defaults.md +1 -1
- package/pipeline/multi-agent-refs/features/unattended-security.md +2 -2
- package/pipeline/scripts/agent-guard.py +150 -25
- package/pipeline/scripts/audit-log.sh +3 -4
- package/pipeline/scripts/autopilot-runner.mjs +14 -5
- package/pipeline/scripts/doctor.mjs +8 -2
- package/pipeline/scripts/gen-skills-index.mjs +13 -1
- package/pipeline/scripts/log-metric.sh +9 -3
- package/pipeline/scripts/migrate-prefs.mjs +18 -4
- package/pipeline/scripts/pre-commit-check.sh +119 -27
- package/pipeline/scripts/scan-agent-config.sh +9 -9
- package/pipeline/scripts/unattended_policy.py +12 -3
- package/pipeline/scripts/uninstall.mjs +88 -1
- package/pipeline/scripts/usage-identity.mjs +1 -1
- package/pipeline/scripts/usage-register.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +30 -30
- package/pipeline/skills/shared/README.md +64 -64
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +2 -2
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +2 -2
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +6 -0
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +3 -0
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +3 -0
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +3 -4
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +5 -3
- package/pipeline/skills/shared/external/authentication/SKILL.md +29 -17
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +3 -1
- package/pipeline/skills/shared/external/background-processing/SKILL.md +10 -8
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +7 -7
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +6 -3
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +43 -0
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +4 -2
- package/pipeline/skills/shared/external/core-data/SKILL.md +12 -2
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +31 -0
- package/pipeline/skills/shared/external/coreml/SKILL.md +1 -1
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +1 -1
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +11 -5
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
- package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +6 -6
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +3 -2
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +20 -18
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +4 -4
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +12 -9
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +201 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +64 -30
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +20 -12
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +7 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +26 -12
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +4 -3
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +51 -24
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +19 -9
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +26 -19
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +39 -46
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +37 -63
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +4 -2
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +5 -4
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +3 -2
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +2 -2
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +4 -4
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +15 -6
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +2 -1
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -4
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +1 -1
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +25 -6
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +6 -1
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +3 -2
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +1 -1
- package/pipeline/skills/shared/external/swift-security/SKILL.md +10 -8
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +8 -5
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +13 -9
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +7 -6
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +8 -5
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +3 -2
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +5 -3
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +17 -9
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +4 -2
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +13 -6
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +3 -1
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +8 -5
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +15 -7
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +8 -6
- package/pipeline/scripts/gen-ref-toc.mjs +0 -279
- package/pipeline/scripts/make-manifest.mjs +0 -199
- package/pipeline/scripts/scorecard-snapshot.mjs +0 -178
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// gen-ref-toc.mjs - a table of contents on every long reference file.
|
|
3
|
-
//
|
|
4
|
-
// Anthropic's published Agent Skills guidance: "For reference files longer than
|
|
5
|
-
// 100 lines, include a table of contents". The reason is the same one behind
|
|
6
|
-
// the one-level-deep rule - an agent that reads a long file partially needs to
|
|
7
|
-
// know from the top what is further down, and without that it answers from the
|
|
8
|
-
// first screen and stops.
|
|
9
|
-
//
|
|
10
|
-
// Generated, never hand-written. Fifty-six files' worth of hand-maintained
|
|
11
|
-
// contents lists would be wrong by the second edit, and a wrong ToC is worse
|
|
12
|
-
// than none: it tells a reader a section exists where it does not.
|
|
13
|
-
//
|
|
14
|
-
// Section level is DERIVED, not assumed. These files are not consistent: some
|
|
15
|
-
// open at `##` and section at `###` (keychain.md), others open at `###` and
|
|
16
|
-
// section at `##` (phases/phase-2-dev.md). The rule is the shallowest heading
|
|
17
|
-
// depth that occurs more than once - a depth used exactly once is a title, not
|
|
18
|
-
// a section level.
|
|
19
|
-
//
|
|
20
|
-
// Not every long file is eligible, and the exclusion is measured rather than
|
|
21
|
-
// preferred. A contents list helps a file that is OPENED AND SKIMMED. It is
|
|
22
|
-
// pure cost on a file that is loaded whole by contract, because there is no
|
|
23
|
-
// partial read for it to rescue - and those files are already at their limit:
|
|
24
|
-
//
|
|
25
|
-
// the 8 phase docs a ToC each puts 5 of 8 over their per-phase max and the
|
|
26
|
-
// aggregate 1,533 tokens over 62,700
|
|
27
|
-
// rules.md always loaded; 59,908 of a 60,000 byte ceiling, and a
|
|
28
|
-
// ToC takes it to 60,997
|
|
29
|
-
// the analysis refs mounted as one set per analysis run; 158,500 byte
|
|
30
|
-
// ceiling, and the ToCs put them 2,298 over
|
|
31
|
-
//
|
|
32
|
-
// So the two published rules genuinely conflict there, and the ToC is the one
|
|
33
|
-
// that loses: raising either ceiling to buy navigation nobody navigates is the
|
|
34
|
-
// move this repo spent a release removing.
|
|
35
|
-
//
|
|
36
|
-
// The exclusion is DERIVED from those two budgets, never hand-listed. A
|
|
37
|
-
// document entering or leaving a budget changes its ToC status by itself, which
|
|
38
|
-
// is the difference between a rule and a list somebody has to remember.
|
|
39
|
-
//
|
|
40
|
-
// Usage:
|
|
41
|
-
// node gen-ref-toc.mjs write/refresh every eligible file
|
|
42
|
-
// node gen-ref-toc.mjs --check report drift, write nothing, exit 1 if any
|
|
43
|
-
// node gen-ref-toc.mjs --stats byte cost, write nothing
|
|
44
|
-
// node gen-ref-toc.mjs --verify every generated anchor resolves to a heading
|
|
45
|
-
|
|
46
|
-
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
47
|
-
import { dirname, join, relative } from "node:path";
|
|
48
|
-
import { fileURLToPath } from "node:url";
|
|
49
|
-
|
|
50
|
-
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
51
|
-
const REFS = join(ROOT, "pipeline", "multi-agent-refs");
|
|
52
|
-
const MIN_LINES = 100;
|
|
53
|
-
const MARK_OPEN = "<!-- toc -->";
|
|
54
|
-
const MARK_CLOSE = "<!-- /toc -->";
|
|
55
|
-
|
|
56
|
-
// Every file that some other gate already holds to a byte or token ceiling.
|
|
57
|
-
function budgeted() {
|
|
58
|
-
const out = new Set();
|
|
59
|
-
const budget = JSON.parse(readFileSync(join(ROOT, "pipeline/schemas/token-budget.json"), "utf8"));
|
|
60
|
-
for (const phase of Object.keys(budget.phases)) {
|
|
61
|
-
out.add(join(REFS, "phases", `${phase}.md`));
|
|
62
|
-
}
|
|
63
|
-
// The always-loaded set, read out of the gate that owns it rather than copied.
|
|
64
|
-
const ctx = readFileSync(join(ROOT, "pipeline/scripts/smoke-context-budget.sh"), "utf8");
|
|
65
|
-
const block = ctx.match(/TRACKED="\n([\s\S]*?)"/);
|
|
66
|
-
if (block) {
|
|
67
|
-
for (const line of block[1].split("\n")) {
|
|
68
|
-
const t = line.trim();
|
|
69
|
-
if (t) out.add(join(ROOT, t));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// The analysis set, mounted whole per analysis run under its own ceiling. Same
|
|
74
|
-
// two globs that gate uses - `analysis/<name>.md` named by the analysis
|
|
75
|
-
// command, plus every analysis-template*.md.
|
|
76
|
-
const cmd = readFileSync(join(ROOT, "pipeline/commands/multi-agent/analysis/SKILL.md"), "utf8");
|
|
77
|
-
for (const m of cmd.matchAll(/analysis\/[a-z-]+\.md/g)) out.add(join(REFS, m[0]));
|
|
78
|
-
for (const e of readdirSync(REFS, { withFileTypes: true })) {
|
|
79
|
-
if (e.isFile() && /^analysis-template.*\.md$/.test(e.name)) out.add(join(REFS, e.name));
|
|
80
|
-
}
|
|
81
|
-
return out;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function walk(dir, out = []) {
|
|
85
|
-
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
86
|
-
const p = join(dir, e.name);
|
|
87
|
-
if (e.isDirectory()) walk(p, out);
|
|
88
|
-
else if (e.name.endsWith(".md")) out.push(p);
|
|
89
|
-
}
|
|
90
|
-
return out;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// GitHub's anchor rule, which is what a `](#...)` link resolves against.
|
|
94
|
-
function slug(text) {
|
|
95
|
-
return text
|
|
96
|
-
.toLowerCase()
|
|
97
|
-
.replace(/`/g, "")
|
|
98
|
-
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
99
|
-
.replace(/[^\w\s-]/g, "")
|
|
100
|
-
.trim()
|
|
101
|
-
.replace(/\s+/g, "-");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function sections(lines) {
|
|
105
|
-
const heads = [];
|
|
106
|
-
let fenced = false;
|
|
107
|
-
lines.forEach((l, i) => {
|
|
108
|
-
if (/^\s*```/.test(l)) fenced = !fenced;
|
|
109
|
-
if (fenced) return;
|
|
110
|
-
const m = l.match(/^(#{1,6})\s+(.*\S)\s*$/);
|
|
111
|
-
if (m) heads.push({ depth: m[1].length, text: m[2], line: i });
|
|
112
|
-
});
|
|
113
|
-
if (!heads.length) return { level: 0, heads: [] };
|
|
114
|
-
const counts = {};
|
|
115
|
-
for (const h of heads) counts[h.depth] = (counts[h.depth] || 0) + 1;
|
|
116
|
-
const depths = Object.keys(counts)
|
|
117
|
-
.map(Number)
|
|
118
|
-
.sort((a, b) => a - b);
|
|
119
|
-
// The shallowest depth used more than once. A depth used exactly once is the
|
|
120
|
-
// document's title and listing it as the only entry is a ToC of one.
|
|
121
|
-
const level = depths.find((d) => counts[d] > 1) ?? depths[depths.length - 1];
|
|
122
|
-
return { level, heads: heads.filter((h) => h.depth === level) };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function render(heads) {
|
|
126
|
-
const seen = new Map();
|
|
127
|
-
const rows = heads.map((h) => {
|
|
128
|
-
let a = slug(h.text);
|
|
129
|
-
const n = (seen.get(a) || 0) + 1;
|
|
130
|
-
seen.set(a, n);
|
|
131
|
-
if (n > 1) a = `${a}-${n - 1}`;
|
|
132
|
-
return `- [${h.text.replace(/\|/g, "\\|")}](#${a})`;
|
|
133
|
-
});
|
|
134
|
-
return [MARK_OPEN, ...rows, MARK_CLOSE].join("\n");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// After the frontmatter and after the first heading, so the reader sees what the
|
|
138
|
-
// file IS before a list of what is in it.
|
|
139
|
-
function insertionPoint(lines) {
|
|
140
|
-
let i = 0;
|
|
141
|
-
if (lines[0] === "---") {
|
|
142
|
-
i = 1;
|
|
143
|
-
while (i < lines.length && lines[i] !== "---") i += 1;
|
|
144
|
-
i += 1;
|
|
145
|
-
}
|
|
146
|
-
for (let j = i; j < lines.length; j += 1) {
|
|
147
|
-
if (/^#{1,6}\s/.test(lines[j])) return j + 1;
|
|
148
|
-
}
|
|
149
|
-
return i;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const BUDGETED = budgeted();
|
|
153
|
-
const excluded = [];
|
|
154
|
-
const files = walk(REFS)
|
|
155
|
-
.filter((f) => readFileSync(f, "utf8").split("\n").length - 1 > MIN_LINES)
|
|
156
|
-
.filter((f) => {
|
|
157
|
-
if (!BUDGETED.has(f)) return true;
|
|
158
|
-
excluded.push(relative(ROOT, f));
|
|
159
|
-
return false;
|
|
160
|
-
})
|
|
161
|
-
.sort();
|
|
162
|
-
|
|
163
|
-
// Does every `](#anchor)` in a generated block name a heading that exists in the
|
|
164
|
-
// same file? --check cannot answer that: it compares the file against THIS
|
|
165
|
-
// script's output, so a wrong slug rule agrees with itself. This validates the
|
|
166
|
-
// rule against the real headings instead, and it lives here so there is one
|
|
167
|
-
// definition of `slug`, not a second copy inside a gate.
|
|
168
|
-
if (process.argv.includes("--verify")) {
|
|
169
|
-
let total = 0;
|
|
170
|
-
const bad = [];
|
|
171
|
-
for (const f of walk(REFS)) {
|
|
172
|
-
const raw = readFileSync(f, "utf8");
|
|
173
|
-
if (!raw.includes(MARK_OPEN)) continue;
|
|
174
|
-
const lines = raw.split("\n");
|
|
175
|
-
const seen = new Map();
|
|
176
|
-
const anchors = new Set();
|
|
177
|
-
let fenced = false;
|
|
178
|
-
for (const l of lines) {
|
|
179
|
-
if (/^\s*```/.test(l)) fenced = !fenced;
|
|
180
|
-
if (fenced) continue;
|
|
181
|
-
const m = l.match(/^#{1,6}\s+(.*\S)\s*$/);
|
|
182
|
-
if (!m) continue;
|
|
183
|
-
let a = slug(m[1]);
|
|
184
|
-
const n = (seen.get(a) || 0) + 1;
|
|
185
|
-
seen.set(a, n);
|
|
186
|
-
if (n > 1) a = `${a}-${n - 1}`;
|
|
187
|
-
anchors.add(a);
|
|
188
|
-
}
|
|
189
|
-
const s0 = lines.indexOf(MARK_OPEN);
|
|
190
|
-
const e0 = lines.indexOf(MARK_CLOSE, s0);
|
|
191
|
-
for (const l of lines.slice(s0 + 1, e0)) {
|
|
192
|
-
const m = l.match(/\]\(#(.+)\)\s*$/);
|
|
193
|
-
if (!m) continue;
|
|
194
|
-
total += 1;
|
|
195
|
-
if (!anchors.has(m[1])) bad.push(`${relative(ROOT, f)} -> #${m[1]}`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
if (total === 0) {
|
|
199
|
-
console.log("gen-ref-toc: no anchors found at all - the generator produced nothing");
|
|
200
|
-
process.exit(1);
|
|
201
|
-
}
|
|
202
|
-
if (bad.length) {
|
|
203
|
-
console.log(`gen-ref-toc: ${bad.length} of ${total} anchor(s) point at no heading:`);
|
|
204
|
-
for (const b of bad.slice(0, 10)) console.log(` ${b}`);
|
|
205
|
-
process.exit(1);
|
|
206
|
-
}
|
|
207
|
-
console.log(`gen-ref-toc: ${total} anchors, all resolve to a heading in their own file`);
|
|
208
|
-
process.exit(0);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const mode = process.argv.includes("--check")
|
|
212
|
-
? "check"
|
|
213
|
-
: process.argv.includes("--stats")
|
|
214
|
-
? "stats"
|
|
215
|
-
: "write";
|
|
216
|
-
|
|
217
|
-
const drifted = [];
|
|
218
|
-
const skipped = [];
|
|
219
|
-
let added = 0;
|
|
220
|
-
|
|
221
|
-
for (const f of files) {
|
|
222
|
-
const raw = readFileSync(f, "utf8");
|
|
223
|
-
const rel = relative(ROOT, f);
|
|
224
|
-
const lines = raw.split("\n");
|
|
225
|
-
const { heads } = sections(lines);
|
|
226
|
-
// Fewer than three sections is a file that does not need a map of itself.
|
|
227
|
-
if (heads.length < 3) {
|
|
228
|
-
skipped.push(`${rel} (${heads.length} section(s))`);
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
const block = render(heads);
|
|
232
|
-
|
|
233
|
-
const start = lines.indexOf(MARK_OPEN);
|
|
234
|
-
let next;
|
|
235
|
-
if (start !== -1) {
|
|
236
|
-
const end = lines.indexOf(MARK_CLOSE, start);
|
|
237
|
-
if (end === -1) {
|
|
238
|
-
drifted.push(`${rel}: ${MARK_OPEN} with no ${MARK_CLOSE}`);
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
const current = lines.slice(start, end + 1).join("\n");
|
|
242
|
-
if (current === block) continue;
|
|
243
|
-
next = [...lines.slice(0, start), ...block.split("\n"), ...lines.slice(end + 1)];
|
|
244
|
-
drifted.push(`${rel}: contents no longer match the headings`);
|
|
245
|
-
} else {
|
|
246
|
-
const at = insertionPoint(lines);
|
|
247
|
-
next = [...lines.slice(0, at), "", ...block.split("\n"), ...lines.slice(at)];
|
|
248
|
-
drifted.push(`${rel}: no table of contents`);
|
|
249
|
-
}
|
|
250
|
-
const out = next.join("\n");
|
|
251
|
-
added += out.length - raw.length;
|
|
252
|
-
if (mode === "write") writeFileSync(f, out);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (mode === "stats") {
|
|
256
|
-
console.log(`eligible: ${files.length} ref files over ${MIN_LINES} lines`);
|
|
257
|
-
console.log(`excluded as loaded-whole-by-contract: ${excluded.length}`);
|
|
258
|
-
console.log(`skipped (under 3 sections): ${skipped.length}`);
|
|
259
|
-
console.log(
|
|
260
|
-
`would add: ${added} bytes total, ~${Math.round(added / 4)} tokens if every one were read`,
|
|
261
|
-
);
|
|
262
|
-
process.exit(0);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (mode === "check") {
|
|
266
|
-
if (drifted.length === 0) {
|
|
267
|
-
console.log(`gen-ref-toc: ${files.length} long ref(s) carry a current table of contents`);
|
|
268
|
-
process.exit(0);
|
|
269
|
-
}
|
|
270
|
-
console.log(
|
|
271
|
-
`gen-ref-toc: ${drifted.length} file(s) need \`node pipeline/scripts/gen-ref-toc.mjs\`:`,
|
|
272
|
-
);
|
|
273
|
-
for (const d of drifted) console.log(` ${d}`);
|
|
274
|
-
process.exit(1);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
console.log(
|
|
278
|
-
`gen-ref-toc: ${drifted.length} file(s) updated, ${skipped.length} skipped, ${added} bytes added`,
|
|
279
|
-
);
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* make-manifest.mjs - a digest of everything the package ships.
|
|
4
|
-
*
|
|
5
|
-
* "Is my install the one that was published, or has something edited it" is a
|
|
6
|
-
* question this tree could not answer. The install is a COPY: `install.js`
|
|
7
|
-
* writes into ~/.claude, ~/.copilot and ~/.codex, and from that moment the two
|
|
8
|
-
* halves drift independently. An edit made in the installed copy looks like a
|
|
9
|
-
* feature nobody can find the source of; a file the installer failed to write
|
|
10
|
-
* looks like a bug in a script that was never there.
|
|
11
|
-
*
|
|
12
|
-
* The manifest turns both into one command. It records a SHA-256 for every
|
|
13
|
-
* file the package ships, the version, and the commit it was built from, and
|
|
14
|
-
* `multi-agent-pipeline verify` compares an installed tree against it, file by
|
|
15
|
-
* file.
|
|
16
|
-
*
|
|
17
|
-
* The file set is not guessed: it comes from `npm pack --dry-run --json`, so
|
|
18
|
-
* it is by construction the same set npm will publish, `files` globs and all.
|
|
19
|
-
* `--ignore-scripts` is on the inner call because this script runs FROM
|
|
20
|
-
* prepack, and without it npm would run prepack again.
|
|
21
|
-
*
|
|
22
|
-
* Two things the manifest cannot cover, said here rather than discovered later:
|
|
23
|
-
* it cannot hash itself (it does not exist when the list is taken), and a
|
|
24
|
-
* signature over it does not authenticate the tarball - the verifier and the
|
|
25
|
-
* public key travel inside the same tarball, so anyone able to rewrite one can
|
|
26
|
-
* rewrite the others. What a signature DOES catch is a manifest damaged in
|
|
27
|
-
* transit or edited after install, which is the failure this file exists for.
|
|
28
|
-
* Provenance belongs to npm's own integrity field, not to us.
|
|
29
|
-
*
|
|
30
|
-
* Usage:
|
|
31
|
-
* make-manifest.mjs write manifest.json at the repo root
|
|
32
|
-
* make-manifest.mjs --sign also write manifest.sig (needs a signing key)
|
|
33
|
-
* make-manifest.mjs --check recompute and report drift, write nothing
|
|
34
|
-
* make-manifest.mjs --json
|
|
35
|
-
*
|
|
36
|
-
* Exit codes:
|
|
37
|
-
* 0 written, or --check found no drift
|
|
38
|
-
* 1 --check found drift
|
|
39
|
-
* 2 usage or environment error
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
import { execFileSync } from "node:child_process";
|
|
43
|
-
import { createHash, createPrivateKey, sign as cryptoSign } from "node:crypto";
|
|
44
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
45
|
-
import { dirname, join } from "node:path";
|
|
46
|
-
import { fileURLToPath } from "node:url";
|
|
47
|
-
import { runMain } from "../lib/fatal.mjs";
|
|
48
|
-
|
|
49
|
-
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
50
|
-
const MANIFEST = join(ROOT, "manifest.json");
|
|
51
|
-
|
|
52
|
-
const argv = process.argv.slice(2);
|
|
53
|
-
const has = (f) => argv.includes(f);
|
|
54
|
-
|
|
55
|
-
/** The exact set npm would publish, in npm's own words. */
|
|
56
|
-
function shippedFiles() {
|
|
57
|
-
const out = execFileSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], {
|
|
58
|
-
cwd: ROOT,
|
|
59
|
-
encoding: "utf-8",
|
|
60
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
61
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
62
|
-
});
|
|
63
|
-
const parsed = JSON.parse(out);
|
|
64
|
-
const files = parsed?.[0]?.files;
|
|
65
|
-
if (!Array.isArray(files) || !files.length) {
|
|
66
|
-
throw new Error("npm pack --dry-run returned no file list");
|
|
67
|
-
}
|
|
68
|
-
return files
|
|
69
|
-
.map((f) => f.path)
|
|
70
|
-
.filter((p) => p !== "manifest.json" && p !== "manifest.sig")
|
|
71
|
-
.sort();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function sha256(path) {
|
|
75
|
-
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function gitCommit() {
|
|
79
|
-
try {
|
|
80
|
-
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
81
|
-
cwd: ROOT,
|
|
82
|
-
encoding: "utf-8",
|
|
83
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
84
|
-
}).trim();
|
|
85
|
-
} catch {
|
|
86
|
-
// A tarball unpacked outside a checkout has no git. The manifest is still
|
|
87
|
-
// valid; only the provenance line is unknown, and saying so beats writing
|
|
88
|
-
// a placeholder that reads like a real commit.
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function build() {
|
|
94
|
-
const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
|
|
95
|
-
const files = {};
|
|
96
|
-
for (const rel of shippedFiles()) {
|
|
97
|
-
const abs = join(ROOT, rel);
|
|
98
|
-
if (!existsSync(abs)) continue;
|
|
99
|
-
files[rel] = sha256(abs);
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
name: pkg.name,
|
|
103
|
-
version: pkg.version,
|
|
104
|
-
commit: gitCommit(),
|
|
105
|
-
generatedAt: new Date().toISOString(),
|
|
106
|
-
algorithm: "sha256",
|
|
107
|
-
fileCount: Object.keys(files).length,
|
|
108
|
-
files,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Sign the manifest with an ed25519 key. Optional, and silent about its absence
|
|
114
|
-
* on the common path: a package with no signature still verifies on hashes
|
|
115
|
-
* alone, which is the property that actually gets used.
|
|
116
|
-
*
|
|
117
|
-
* The key is read through the credential store, never taken from argv and never
|
|
118
|
-
* printed. MULTI_AGENT_SIGNING_KEY is the fallback for a build host that has no
|
|
119
|
-
* store (CI), and it holds the PEM itself.
|
|
120
|
-
*
|
|
121
|
-
* @param {string} manifestJson exact bytes that were written to disk
|
|
122
|
-
* @returns {string|null} base64 signature, or null when no key is configured
|
|
123
|
-
*/
|
|
124
|
-
function sign(manifestJson) {
|
|
125
|
-
let pem = process.env.MULTI_AGENT_SIGNING_KEY || "";
|
|
126
|
-
if (!pem) {
|
|
127
|
-
try {
|
|
128
|
-
pem = execFileSync(
|
|
129
|
-
join(ROOT, "pipeline", "lib", "credential-store.sh"),
|
|
130
|
-
["get", "pipeline_signing"],
|
|
131
|
-
{
|
|
132
|
-
encoding: "utf-8",
|
|
133
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
134
|
-
},
|
|
135
|
-
).trim();
|
|
136
|
-
} catch {
|
|
137
|
-
pem = "";
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (!pem) {
|
|
141
|
-
process.stderr.write(
|
|
142
|
-
"make-manifest: no signing key configured - manifest written unsigned (hashes still verify)\n",
|
|
143
|
-
);
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
const privateKey = createPrivateKey(pem);
|
|
147
|
-
return cryptoSign(null, Buffer.from(manifestJson), privateKey).toString("base64");
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function main() {
|
|
151
|
-
if (has("--check")) {
|
|
152
|
-
if (!existsSync(MANIFEST)) {
|
|
153
|
-
process.stderr.write("make-manifest: no manifest.json to check\n");
|
|
154
|
-
process.exit(2);
|
|
155
|
-
}
|
|
156
|
-
const stored = JSON.parse(readFileSync(MANIFEST, "utf-8"));
|
|
157
|
-
const fresh = build();
|
|
158
|
-
const changed = [];
|
|
159
|
-
for (const [rel, hash] of Object.entries(fresh.files)) {
|
|
160
|
-
if (stored.files[rel] !== hash) changed.push(rel);
|
|
161
|
-
}
|
|
162
|
-
const gone = Object.keys(stored.files).filter((r) => !(r in fresh.files));
|
|
163
|
-
const report = { changed, gone, stale: changed.length + gone.length > 0 };
|
|
164
|
-
if (has("--json")) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
165
|
-
else if (!report.stale)
|
|
166
|
-
process.stdout.write(`manifest.json is current (${fresh.fileCount} files)\n`);
|
|
167
|
-
else {
|
|
168
|
-
process.stdout.write(
|
|
169
|
-
`manifest.json is stale: ${changed.length} changed, ${gone.length} gone\n`,
|
|
170
|
-
);
|
|
171
|
-
for (const r of [...changed, ...gone].slice(0, 20)) process.stdout.write(` ${r}\n`);
|
|
172
|
-
}
|
|
173
|
-
process.exitCode = report.stale ? 1 : 0;
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const manifest = build();
|
|
178
|
-
const json = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
179
|
-
writeFileSync(MANIFEST, json);
|
|
180
|
-
if (has("--sign")) {
|
|
181
|
-
const sig = sign(json);
|
|
182
|
-
if (sig) writeFileSync(join(ROOT, "manifest.sig"), `${sig}\n`);
|
|
183
|
-
}
|
|
184
|
-
if (has("--json"))
|
|
185
|
-
process.stdout.write(
|
|
186
|
-
`${JSON.stringify({ written: MANIFEST, ...manifest, files: undefined }, null, 2)}\n`,
|
|
187
|
-
);
|
|
188
|
-
else
|
|
189
|
-
// STDERR, not stdout. This runs as `prepack`, and npm passes a prepack
|
|
190
|
-
// script's stdout straight through - so one friendly line here lands INSIDE
|
|
191
|
-
// `npm pack --dry-run --json` and every consumer that parses that JSON
|
|
192
|
-
// fails with "Unexpected token 'm'". Two gates caught it within minutes;
|
|
193
|
-
// a publish would not have.
|
|
194
|
-
process.stderr.write(
|
|
195
|
-
`manifest.json: ${manifest.fileCount} files, version ${manifest.version}, commit ${manifest.commit?.slice(0, 12) ?? "unknown"}\n`,
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
runMain("make-manifest", main);
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* scorecard-snapshot.mjs - keep what the scorecard said, and say what moved.
|
|
4
|
-
*
|
|
5
|
-
* The scorecard answers "does every mechanical claim hold right now". It
|
|
6
|
-
* cannot answer "is this better or worse than last week", and that second
|
|
7
|
-
* question is the one that catches slow rot: a metric that has been failing
|
|
8
|
-
* for a month reads identically to one that broke an hour ago.
|
|
9
|
-
*
|
|
10
|
-
* WHAT THIS DELIBERATELY DOES NOT DO: collapse the result into a 0-100 score.
|
|
11
|
-
* ruflo's scorecard does, and the discipline worth taking from it is
|
|
12
|
-
* "measurable and comparable over time", not the number. The scorecard already
|
|
13
|
-
* reports twelve measured metrics AND four it refuses to measure - a single
|
|
14
|
-
* figure would hide both halves, and an unmeasured category would silently
|
|
15
|
-
* count as zero or as full marks depending on an arithmetic choice nobody
|
|
16
|
-
* would ever read. A diff keeps every metric answering for itself.
|
|
17
|
-
*
|
|
18
|
-
* Snapshots live in ~/.claude/state/scorecard/<iso>.json and are never pruned
|
|
19
|
-
* by this script: they are small, and a history that deletes itself cannot
|
|
20
|
-
* answer the question it was kept for.
|
|
21
|
-
*
|
|
22
|
-
* Usage:
|
|
23
|
-
* scorecard-snapshot.mjs --save run the scorecard, store a snapshot
|
|
24
|
-
* scorecard-snapshot.mjs --diff latest vs the one before it
|
|
25
|
-
* scorecard-snapshot.mjs --diff a.json b.json two snapshots by path
|
|
26
|
-
* scorecard-snapshot.mjs --list what is stored
|
|
27
|
-
*
|
|
28
|
-
* Exit codes:
|
|
29
|
-
* 0 - nothing regressed (or a save succeeded)
|
|
30
|
-
* 1 - at least one metric that used to pass now fails
|
|
31
|
-
* 2 - usage error, or not enough snapshots to compare
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
import { execFileSync } from "node:child_process";
|
|
35
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
36
|
-
import { homedir } from "node:os";
|
|
37
|
-
import { dirname, join } from "node:path";
|
|
38
|
-
import { fileURLToPath } from "node:url";
|
|
39
|
-
import { runMain } from "../lib/fatal.mjs";
|
|
40
|
-
|
|
41
|
-
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
42
|
-
const STORE = process.env.SCORECARD_STORE || join(homedir(), ".claude", "state", "scorecard");
|
|
43
|
-
|
|
44
|
-
/** Metric identity. Category plus metric name, because neither is unique alone. */
|
|
45
|
-
const keyOf = (r) => `${r.category} :: ${r.metric}`;
|
|
46
|
-
|
|
47
|
-
function runScorecard() {
|
|
48
|
-
// `--json` prints the report on stdout; a failing metric is a non-zero exit
|
|
49
|
-
// and is exactly the case worth snapshotting, so the status is captured
|
|
50
|
-
// rather than thrown.
|
|
51
|
-
try {
|
|
52
|
-
return JSON.parse(
|
|
53
|
-
execFileSync("node", ["pipeline/scripts/scorecard.mjs", "--json"], {
|
|
54
|
-
cwd: ROOT,
|
|
55
|
-
encoding: "utf-8",
|
|
56
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
57
|
-
}),
|
|
58
|
-
);
|
|
59
|
-
} catch (err) {
|
|
60
|
-
const out = err?.stdout;
|
|
61
|
-
if (typeof out === "string" && out.trim().startsWith("{")) return JSON.parse(out);
|
|
62
|
-
throw new Error(`scorecard did not produce JSON: ${err?.message ?? err}`, { cause: err });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function snapshots() {
|
|
67
|
-
if (!existsSync(STORE)) return [];
|
|
68
|
-
return (
|
|
69
|
-
readdirSync(STORE)
|
|
70
|
-
.filter((f) => f.endsWith(".json"))
|
|
71
|
-
// ISO-8601 sorts lexically in time order, which is the whole reason the
|
|
72
|
-
// file is named after the timestamp rather than a counter.
|
|
73
|
-
.sort()
|
|
74
|
-
.map((f) => join(STORE, f))
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function save() {
|
|
79
|
-
const report = runScorecard();
|
|
80
|
-
mkdirSync(STORE, { recursive: true });
|
|
81
|
-
const at = new Date().toISOString().replace(/[:.]/g, "-");
|
|
82
|
-
const path = join(STORE, `${at}.json`);
|
|
83
|
-
writeFileSync(path, JSON.stringify({ at: new Date().toISOString(), ...report }, null, 2) + "\n");
|
|
84
|
-
process.stdout.write(`scorecard snapshot: ${path}\n`);
|
|
85
|
-
process.stdout.write(` ${report.passed} passed, ${report.failed} failed\n`);
|
|
86
|
-
return 0;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function diff(aPath, bPath) {
|
|
90
|
-
const a = JSON.parse(readFileSync(aPath, "utf-8"));
|
|
91
|
-
const b = JSON.parse(readFileSync(bPath, "utf-8"));
|
|
92
|
-
// UNMEASURED rows carry no `ok` at all - they are the categories the
|
|
93
|
-
// scorecard refuses to score. Comparing verdicts across them would invent
|
|
94
|
-
// one, which is the thing that section exists to avoid.
|
|
95
|
-
const measured = (rs) => (rs || []).filter((r) => r.kind !== "UNMEASURED");
|
|
96
|
-
const ma = new Map(measured(a.results).map((r) => [keyOf(r), r]));
|
|
97
|
-
const mb = new Map(measured(b.results).map((r) => [keyOf(r), r]));
|
|
98
|
-
|
|
99
|
-
const regressed = [];
|
|
100
|
-
const fixed = [];
|
|
101
|
-
const added = [];
|
|
102
|
-
const removed = [];
|
|
103
|
-
const changed = [];
|
|
104
|
-
|
|
105
|
-
for (const [k, rb] of mb) {
|
|
106
|
-
const ra = ma.get(k);
|
|
107
|
-
if (!ra) {
|
|
108
|
-
added.push(rb);
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (ra.ok && !rb.ok) regressed.push({ k, ra, rb });
|
|
112
|
-
else if (!ra.ok && rb.ok) fixed.push({ k, ra, rb });
|
|
113
|
-
// A metric whose verdict held but whose DETAIL moved is the early warning:
|
|
114
|
-
// coverage sliding from 72 to 69 passes the floor and is still the thing
|
|
115
|
-
// that will fail next month.
|
|
116
|
-
else if (ra.detail !== rb.detail) changed.push({ k, ra, rb });
|
|
117
|
-
}
|
|
118
|
-
for (const [k, ra] of ma) if (!mb.has(k)) removed.push({ k, ra });
|
|
119
|
-
|
|
120
|
-
const out = [];
|
|
121
|
-
out.push(`scorecard diff - ${a.at ?? aPath} → ${b.at ?? bPath}`);
|
|
122
|
-
out.push(` ${a.passed}/${a.passed + a.failed} → ${b.passed}/${b.passed + b.failed} passing`);
|
|
123
|
-
const section = (title, rows, fmt) => {
|
|
124
|
-
if (!rows.length) return;
|
|
125
|
-
out.push("");
|
|
126
|
-
out.push(`${title} (${rows.length})`);
|
|
127
|
-
for (const r of rows) out.push(` ${fmt(r)}`);
|
|
128
|
-
};
|
|
129
|
-
section("REGRESSED", regressed, ({ k, rb }) => `${k}\n now: ${rb.detail ?? "(no detail)"}`);
|
|
130
|
-
section("FIXED", fixed, ({ k, rb }) => `${k}\n now: ${rb.detail ?? "(no detail)"}`);
|
|
131
|
-
section("NEW METRIC", added, (r) => `${keyOf(r)} ${r.ok ? "passing" : "FAILING"}`);
|
|
132
|
-
section("METRIC GONE", removed, ({ k }) => `${k} - no longer measured`);
|
|
133
|
-
section(
|
|
134
|
-
"SAME VERDICT, DIFFERENT NUMBERS",
|
|
135
|
-
changed,
|
|
136
|
-
({ k, ra, rb }) =>
|
|
137
|
-
`${k}\n was: ${ra.detail ?? "(none)"}\n now: ${rb.detail ?? "(none)"}`,
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
if (!regressed.length && !fixed.length && !added.length && !removed.length && !changed.length) {
|
|
141
|
-
out.push("");
|
|
142
|
-
out.push(" nothing moved");
|
|
143
|
-
}
|
|
144
|
-
process.stdout.write(out.join("\n") + "\n");
|
|
145
|
-
return regressed.length ? 1 : 0;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function main() {
|
|
149
|
-
const args = process.argv.slice(2);
|
|
150
|
-
if (args.includes("--save")) return save();
|
|
151
|
-
if (args.includes("--list")) {
|
|
152
|
-
const s = snapshots();
|
|
153
|
-
process.stdout.write(s.length ? s.join("\n") + "\n" : `no snapshots under ${STORE}\n`);
|
|
154
|
-
return 0;
|
|
155
|
-
}
|
|
156
|
-
if (args.includes("--diff")) {
|
|
157
|
-
const paths = args.filter((a) => !a.startsWith("--"));
|
|
158
|
-
if (paths.length === 2) return diff(paths[0], paths[1]);
|
|
159
|
-
const s = snapshots();
|
|
160
|
-
if (s.length < 2) {
|
|
161
|
-
process.stderr.write(
|
|
162
|
-
`need two snapshots to compare, found ${s.length} under ${STORE}\n` +
|
|
163
|
-
` take one with: node pipeline/scripts/scorecard-snapshot.mjs --save\n`,
|
|
164
|
-
);
|
|
165
|
-
return 2;
|
|
166
|
-
}
|
|
167
|
-
return diff(s[s.length - 2], s[s.length - 1]);
|
|
168
|
-
}
|
|
169
|
-
process.stderr.write("usage: scorecard-snapshot.mjs --save | --diff [a.json b.json] | --list\n");
|
|
170
|
-
return 2;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// `process.exitCode`, never `process.exit()`: stdout to a pipe is asynchronous
|
|
174
|
-
// and exit() discards whatever has not drained, which truncated two scripts in
|
|
175
|
-
// this repo at a buffer boundary and only ever when piped.
|
|
176
|
-
runMain("scorecard-snapshot", () => {
|
|
177
|
-
process.exitCode = main();
|
|
178
|
-
});
|