@davesheffer/hunch 1.8.2 → 1.8.3
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 +4 -0
- package/dist/cli/index.js +17 -0
- package/dist/constitution/experiment.js +90 -0
- package/dist/constitution/service.js +21 -4
- package/package.json +9 -2
- package/tooling/competitive-watch.mjs +108 -0
package/README.md
CHANGED
|
@@ -13,6 +13,9 @@ relevant context before it makes a change.
|
|
|
13
13
|
Memory starts **advisory**. Nothing blocks until you explicitly trust a precise rule and choose
|
|
14
14
|
strict enforcement.
|
|
15
15
|
|
|
16
|
+
**Memory is the input. The product boundary is the receipt:** relevant evidence before an edit,
|
|
17
|
+
then a deterministic check of the change against the rules your team has explicitly trusted.
|
|
18
|
+
|
|
16
19
|
## Start in five minutes
|
|
17
20
|
|
|
18
21
|
Requires Node 22.13+ and a git repository.
|
|
@@ -104,5 +107,6 @@ Local tools see the combined graph; public CI and committed documentation stay p
|
|
|
104
107
|
- [VS Code extension](vscode-extension/README.md)
|
|
105
108
|
- [Contributing](CONTRIBUTING.md)
|
|
106
109
|
- [Architecture benchmark](bench/architectural-conformance.md)
|
|
110
|
+
- [Competitive landscape (dated; re-verify before quoting)](docs/competitive-landscape.md)
|
|
107
111
|
|
|
108
112
|
Apache-2.0
|
package/dist/cli/index.js
CHANGED
|
@@ -1957,6 +1957,23 @@ experimentCmd
|
|
|
1957
1957
|
store.close();
|
|
1958
1958
|
}
|
|
1959
1959
|
});
|
|
1960
|
+
experimentCmd
|
|
1961
|
+
.command("qualify")
|
|
1962
|
+
.description("Record a passing excluded comprehension check before an EXP-03 revision-2 timed review.")
|
|
1963
|
+
.argument("<file>", "reviewer qualification JSON")
|
|
1964
|
+
.action((file) => {
|
|
1965
|
+
const { store, root } = storeFor();
|
|
1966
|
+
try {
|
|
1967
|
+
const input = JSON.parse(readFileSync(resolve(file), "utf8"));
|
|
1968
|
+
console.log(JSON.stringify(new ConstitutionService(store, root).qualifyExperimentReviewer(input), null, 2));
|
|
1969
|
+
}
|
|
1970
|
+
catch (e) {
|
|
1971
|
+
fail(e.message);
|
|
1972
|
+
}
|
|
1973
|
+
finally {
|
|
1974
|
+
store.close();
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1960
1977
|
experimentCmd
|
|
1961
1978
|
.command("next")
|
|
1962
1979
|
.description("Start or resume the next randomized EXP-03 human review and return only its assigned treatment.")
|
|
@@ -245,6 +245,46 @@ export function assignmentTreatment(bank, run, assignment) {
|
|
|
245
245
|
throw new Error(`assignment ${assignment.id} treatment hash mismatch`);
|
|
246
246
|
return treatment;
|
|
247
247
|
}
|
|
248
|
+
/** Human-facing help is deliberately outside the hash-bound treatment. It may
|
|
249
|
+
* explain the review task, but must never interpret the assigned evidence. */
|
|
250
|
+
export function experimentReviewGuide(arm) {
|
|
251
|
+
const notEnough = {
|
|
252
|
+
value: "uncompilable",
|
|
253
|
+
label: "Not enough information",
|
|
254
|
+
use_when: "The requirement does not clearly say which code relationship must always hold.",
|
|
255
|
+
};
|
|
256
|
+
if (arm === "A") {
|
|
257
|
+
return {
|
|
258
|
+
title: "Write one code rule from the requirement",
|
|
259
|
+
question: "Can one exact code rule be written from the requirement without guessing?",
|
|
260
|
+
action: "If yes, write one sentence naming the code elements, required or forbidden relationship, direct or transitive meaning, and file scope. If no, choose Not enough information.",
|
|
261
|
+
answer_template: "<subject> must <directly or transitively> <required or forbidden relationship> <target>; scope: <file or component>",
|
|
262
|
+
warning: "Use only the stated requirement. Current code may confirm names, but cannot add intent.",
|
|
263
|
+
choices: [
|
|
264
|
+
{ value: "accepted_precise", label: "Rule written", use_when: "Your sentence expresses one exact rule fully supported by the requirement." },
|
|
265
|
+
notEnough,
|
|
266
|
+
],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const choices = [
|
|
270
|
+
{ value: "accepted_precise", label: "Yes — exact match", use_when: "The proposed rule says exactly what the requirement says." },
|
|
271
|
+
{ value: "accepted_edited", label: "Needs editing", use_when: "The requirement supports one rule, but the proposal needs a specific correction." },
|
|
272
|
+
{ value: "rejected", label: "Unsupported", use_when: "The proposal adds or changes meaning that the requirement does not support." },
|
|
273
|
+
notEnough,
|
|
274
|
+
];
|
|
275
|
+
return {
|
|
276
|
+
title: arm === "C" ? "Check the proposed rule and its proof card" : "Check the proposed rule",
|
|
277
|
+
question: "Does the proposed rule say exactly what the requirement says?",
|
|
278
|
+
action: arm === "C"
|
|
279
|
+
? "Compare the requirement with the proposed rule, then use the proof card only to check that the named code targets are bound correctly."
|
|
280
|
+
: "Compare the requirement with the proposed rule. Check the relationship, direction, direct or transitive meaning, and scope.",
|
|
281
|
+
answer_template: "Choose one plain-language option; include corrected rule text only for Needs editing.",
|
|
282
|
+
warning: arm === "C"
|
|
283
|
+
? "The proof card can verify code bindings, but it cannot add intent missing from the requirement."
|
|
284
|
+
: "Do not infer intent from the current implementation or from nearby code.",
|
|
285
|
+
choices,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
248
288
|
export function experimentRunContentHash(run) {
|
|
249
289
|
const { id: _id, content_hash: _hash, ...body } = run;
|
|
250
290
|
return canonicalHash(body);
|
|
@@ -537,6 +577,38 @@ export function compileExperimentReviewStart(run, assignment, reviewer, opts = {
|
|
|
537
577
|
const contentHash = canonicalHash(body);
|
|
538
578
|
return ExperimentReviewStartSchema.parse({ id: `expreview_${shortHash(contentHash)}`, content_hash: contentHash, ...body });
|
|
539
579
|
}
|
|
580
|
+
export const ExperimentReviewerQualificationSchema = z.object({
|
|
581
|
+
id: z.string().regex(/^expreviewqual_[a-f0-9]{10}$/),
|
|
582
|
+
content_hash: z.string().regex(HASH),
|
|
583
|
+
preregistration_id: z.string().regex(/^expreg_[a-f0-9]{10}$/),
|
|
584
|
+
preregistration_hash: z.string().regex(HASH),
|
|
585
|
+
reviewer: z.string().regex(/^human:[^\s]+$/i),
|
|
586
|
+
protocol: z.literal("exp03-plain-language-comprehension-v2"),
|
|
587
|
+
cases_hash: z.string().regex(HASH),
|
|
588
|
+
passed: z.literal(true),
|
|
589
|
+
reason: z.string().trim().min(1).max(4000),
|
|
590
|
+
data_class: z.literal("private"),
|
|
591
|
+
authority: z.literal("none"),
|
|
592
|
+
recorded_at: z.string().datetime({ offset: true }),
|
|
593
|
+
}).strict();
|
|
594
|
+
export function experimentReviewerQualificationContentHash(record) {
|
|
595
|
+
const { id: _id, content_hash: _hash, ...body } = record;
|
|
596
|
+
return canonicalHash(body);
|
|
597
|
+
}
|
|
598
|
+
export function compileExperimentReviewerQualification(input, preregistration, opts = {}) {
|
|
599
|
+
if (preregistration.experiment !== "EXP-03" || preregistration.revision < 2)
|
|
600
|
+
throw new Error("plain-language reviewer qualification requires EXP-03 revision 2 or later");
|
|
601
|
+
if (input.preregistration_id !== preregistration.id || input.preregistration_hash !== preregistration.content_hash)
|
|
602
|
+
throw new Error("reviewer qualification must bind the exact current preregistration");
|
|
603
|
+
const body = {
|
|
604
|
+
...input,
|
|
605
|
+
data_class: "private",
|
|
606
|
+
authority: "none",
|
|
607
|
+
recorded_at: opts.now ?? new Date().toISOString(),
|
|
608
|
+
};
|
|
609
|
+
const contentHash = canonicalHash(body);
|
|
610
|
+
return ExperimentReviewerQualificationSchema.parse({ id: `expreviewqual_${shortHash(contentHash)}`, content_hash: contentHash, ...body });
|
|
611
|
+
}
|
|
540
612
|
export const ExperimentFollowupSchema = z.object({
|
|
541
613
|
id: z.string().regex(/^expfollow_[a-f0-9]{10}$/),
|
|
542
614
|
content_hash: z.string().regex(HASH),
|
|
@@ -943,6 +1015,24 @@ export class ExperimentRepository {
|
|
|
943
1015
|
this.put("experiment-review-starts", parsed.id, parsed);
|
|
944
1016
|
return parsed;
|
|
945
1017
|
}
|
|
1018
|
+
listReviewerQualifications() {
|
|
1019
|
+
return this.load("experiment-review-qualifications", "expreviewqual_", (raw) => {
|
|
1020
|
+
const parsed = ExperimentReviewerQualificationSchema.parse(raw);
|
|
1021
|
+
if (experimentReviewerQualificationContentHash(parsed) !== parsed.content_hash || parsed.id !== `expreviewqual_${shortHash(parsed.content_hash)}`)
|
|
1022
|
+
throw new Error(`experiment reviewer qualification ${parsed.id} content hash mismatch`);
|
|
1023
|
+
return parsed;
|
|
1024
|
+
}).sort((a, b) => a.id.localeCompare(b.id));
|
|
1025
|
+
}
|
|
1026
|
+
putReviewerQualification(record) {
|
|
1027
|
+
const parsed = ExperimentReviewerQualificationSchema.parse(record);
|
|
1028
|
+
if (experimentReviewerQualificationContentHash(parsed) !== parsed.content_hash || parsed.id !== `expreviewqual_${shortHash(parsed.content_hash)}`)
|
|
1029
|
+
throw new Error(`experiment reviewer qualification ${parsed.id} content hash mismatch`);
|
|
1030
|
+
const incumbent = this.listReviewerQualifications().find((item) => item.preregistration_id === parsed.preregistration_id && item.reviewer === parsed.reviewer);
|
|
1031
|
+
if (incumbent)
|
|
1032
|
+
return incumbent;
|
|
1033
|
+
this.put("experiment-review-qualifications", parsed.id, parsed);
|
|
1034
|
+
return parsed;
|
|
1035
|
+
}
|
|
946
1036
|
listFollowups() {
|
|
947
1037
|
const records = this.load("experiment-followups", "expfollow_", (raw) => {
|
|
948
1038
|
const parsed = ExperimentFollowupSchema.parse(raw);
|
|
@@ -31,7 +31,7 @@ import { evaluateExecutableBehaviorPolicy } from "./behaviorEvaluator.js";
|
|
|
31
31
|
import { executeG2OperationalDrill } from "./g2Drills.js";
|
|
32
32
|
import { G3_REQUIRED_EXPERIMENTS, G3EvidenceRepository, compileExperimentPreregistration, compileG3Plan, compileProofReviewMeasurement, scoreG3Readiness, } from "./g3.js";
|
|
33
33
|
import { executeG3AdapterConformance, g3ConformanceSourceHash } from "./g3Conformance.js";
|
|
34
|
-
import { ExperimentRepository, assignmentTreatment, buildExperimentReport, compileExperimentCaseBank, compileExperimentFollowup, compileExperimentOutcome, compileExperimentReviewStart, compileExperimentRun, compileExperimentStop, compileExp03ReviewResponse, currentExperimentOutcomes, normalizedEditDistance, } from "./experiment.js";
|
|
34
|
+
import { ExperimentRepository, assignmentTreatment, buildExperimentReport, compileExperimentCaseBank, compileExperimentFollowup, compileExperimentOutcome, compileExperimentReviewStart, compileExperimentReviewerQualification, compileExperimentRun, compileExperimentStop, compileExp03ReviewResponse, currentExperimentOutcomes, experimentReviewGuide, normalizedEditDistance, } from "./experiment.js";
|
|
35
35
|
import { executeExp01Assignment } from "./experimentRunner.js";
|
|
36
36
|
function relationSummary(policy) {
|
|
37
37
|
return {
|
|
@@ -492,11 +492,22 @@ export class ConstitutionService {
|
|
|
492
492
|
const bank = this.experimentRepository.listCaseBanks().find((item) => item.id === run.case_bank_id);
|
|
493
493
|
if (!bank)
|
|
494
494
|
throw new Error(`run ${run.id} is missing exact case bank ${run.case_bank_id}`);
|
|
495
|
+
const preregistration = this.g3Repository.listExperiments().find((item) => item.id === run.preregistration_id && item.content_hash === run.preregistration_hash);
|
|
496
|
+
const usesPlainLanguageReview = bank.cases.some((item) => "required_relationship" in item);
|
|
497
|
+
if (usesPlainLanguageReview && !preregistration)
|
|
498
|
+
throw new Error(`run ${run.id} is missing exact preregistration ${run.preregistration_id}`);
|
|
499
|
+
if (preregistration && preregistration.revision >= 2) {
|
|
500
|
+
const qualification = this.experimentRepository.listReviewerQualifications().find((item) => item.preregistration_id === preregistration.id && item.preregistration_hash === preregistration.content_hash && item.reviewer === reviewer);
|
|
501
|
+
if (!qualification)
|
|
502
|
+
throw new Error(`${reviewer} must pass the excluded plain-language comprehension check before a revision-${preregistration.revision} timed review`);
|
|
503
|
+
const targetReviewers = new Set(bank.cases.map((item) => item.strata.target_reviewer).filter(Boolean));
|
|
504
|
+
if (targetReviewers.has(reviewer) || targetReviewers.has(reviewer.replace(/^human:/i, "")))
|
|
505
|
+
throw new Error(`${reviewer} cannot perform timed reviews because the same actor labeled revision-${preregistration.revision} targets`);
|
|
506
|
+
}
|
|
495
507
|
// Single-operator mitigation (expreg_9c9617cd13, revision >= 3): at least 48 hours
|
|
496
508
|
// must separate the case-bank lock from the FIRST review start — enforced, not
|
|
497
509
|
// merely auditable, so a violation is impossible rather than post-hoc visible.
|
|
498
|
-
|
|
499
|
-
if (prereg && prereg.revision >= 3) {
|
|
510
|
+
if (preregistration && preregistration.revision >= 3) {
|
|
500
511
|
const elapsed = Date.parse(opts.now ?? new Date().toISOString()) - Date.parse(bank.locked_at);
|
|
501
512
|
const hasStart = this.experimentRepository.listReviewStarts().some((item) => item.run_id === run.id);
|
|
502
513
|
if (!hasStart && elapsed < 48 * 3_600_000) {
|
|
@@ -518,7 +529,13 @@ export class ConstitutionService {
|
|
|
518
529
|
if (!assignment)
|
|
519
530
|
throw new Error(`no unreviewed EXP-03 assignment is available for ${reviewer}`);
|
|
520
531
|
const start = existing ?? this.experimentRepository.putReviewStart(compileExperimentReviewStart(run, assignment, reviewer, opts));
|
|
521
|
-
return { start, assignment, treatment: assignmentTreatment(bank, run, assignment) };
|
|
532
|
+
return { start, assignment, treatment: assignmentTreatment(bank, run, assignment), review_guide: experimentReviewGuide(assignment.arm) };
|
|
533
|
+
}
|
|
534
|
+
qualifyExperimentReviewer(input, opts = {}) {
|
|
535
|
+
const preregistration = this.g3Repository.currentExperiments().find((item) => item.experiment === "EXP-03");
|
|
536
|
+
if (!preregistration)
|
|
537
|
+
throw new Error("no current EXP-03 preregistration");
|
|
538
|
+
return this.experimentRepository.putReviewerQualification(compileExperimentReviewerQualification(input, preregistration, opts));
|
|
522
539
|
}
|
|
523
540
|
/** Resolve an EXP-03 run/assignment/case triple (the shared lookup for both
|
|
524
541
|
* review-submission dialects). */
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.3",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
6
|
-
"description": "
|
|
6
|
+
"description": "Engineering memory and a deterministic Change Gate for AI-assisted codebases: decisions, rejected approaches, constraints, and bug lineage become portable context and opt-in enforcement for every MCP assistant.",
|
|
7
7
|
"homepage": "https://hunch-pi.vercel.app",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"dist/**/*.js",
|
|
21
21
|
"bench/constitution-exp03-v1.json",
|
|
22
|
+
"tooling/competitive-watch.mjs",
|
|
22
23
|
"LICENSE",
|
|
23
24
|
"NOTICE"
|
|
24
25
|
],
|
|
@@ -33,7 +34,12 @@
|
|
|
33
34
|
"windsurf",
|
|
34
35
|
"antigravity",
|
|
35
36
|
"mcp",
|
|
37
|
+
"coding-agents",
|
|
38
|
+
"agent-memory",
|
|
36
39
|
"engineering-memory",
|
|
40
|
+
"architectural-conformance",
|
|
41
|
+
"change-gate",
|
|
42
|
+
"code-governance",
|
|
37
43
|
"knowledge-graph",
|
|
38
44
|
"code-intelligence",
|
|
39
45
|
"ai",
|
|
@@ -52,6 +58,7 @@
|
|
|
52
58
|
"rehearse:constitution": "npm run build && node tooling/constitution-clean-rehearsal.mjs",
|
|
53
59
|
"gate:release": "node tooling/release-gate.mjs",
|
|
54
60
|
"site:proof": "npm run build && node tooling/generate-public-proof.mjs",
|
|
61
|
+
"research:competitors": "node tooling/competitive-watch.mjs",
|
|
55
62
|
"prepublishOnly": "npm run build"
|
|
56
63
|
},
|
|
57
64
|
"dependencies": {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const repos = [
|
|
4
|
+
"davesheffer/hunch",
|
|
5
|
+
"gitmem-dev/gitmem",
|
|
6
|
+
"ismaelkedir/knowit",
|
|
7
|
+
"weigibbor/mnemo",
|
|
8
|
+
"oldskultxo/aictx",
|
|
9
|
+
"riponcm/projectmem",
|
|
10
|
+
"Cranot/roam-code",
|
|
11
|
+
"blackwell-systems/knowing",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const distinctivePhrases = [
|
|
15
|
+
"Causal Merge Verdict",
|
|
16
|
+
"corrections become enforced",
|
|
17
|
+
"content-matched constraints",
|
|
18
|
+
"deterministic Change Gate for AI-assisted codebases",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const token = process.env.GITHUB_TOKEN?.trim();
|
|
22
|
+
const headers = {
|
|
23
|
+
Accept: "application/vnd.github+json",
|
|
24
|
+
"User-Agent": "hunch-competitive-watch",
|
|
25
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
26
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
async function github(path) {
|
|
30
|
+
const response = await fetch(`https://api.github.com${path}`, { headers });
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const detail = (await response.text()).slice(0, 300).replaceAll("\n", " ");
|
|
33
|
+
throw new Error(`GitHub ${response.status} for ${path}: ${detail}`);
|
|
34
|
+
}
|
|
35
|
+
return response.json();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function repoSnapshot(repo) {
|
|
39
|
+
const data = await github(`/repos/${repo}`);
|
|
40
|
+
return {
|
|
41
|
+
repo,
|
|
42
|
+
created: data.created_at,
|
|
43
|
+
pushed: data.pushed_at,
|
|
44
|
+
stars: data.stargazers_count,
|
|
45
|
+
forks: data.forks_count,
|
|
46
|
+
issues: data.open_issues_count,
|
|
47
|
+
url: data.html_url,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function phraseSnapshot(phrase) {
|
|
52
|
+
const query = encodeURIComponent(`\"${phrase}\"`);
|
|
53
|
+
const data = await github(`/search/code?q=${query}&per_page=100`);
|
|
54
|
+
const external = data.items
|
|
55
|
+
.filter((item) => !item.repository.full_name.startsWith("davesheffer/"))
|
|
56
|
+
.map((item) => ({
|
|
57
|
+
repo: item.repository.full_name,
|
|
58
|
+
path: item.path,
|
|
59
|
+
url: item.html_url,
|
|
60
|
+
}));
|
|
61
|
+
return { phrase, total: data.total_count, external };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function render(snapshot, phrases) {
|
|
65
|
+
const lines = [
|
|
66
|
+
`# Competitive watch — ${new Date().toISOString()}`,
|
|
67
|
+
"",
|
|
68
|
+
"## Public repository signals",
|
|
69
|
+
"",
|
|
70
|
+
"| Repository | Created | Last push | Stars | Forks | Open issues |",
|
|
71
|
+
"| --- | --- | --- | ---: | ---: | ---: |",
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
for (const item of snapshot) {
|
|
75
|
+
lines.push(
|
|
76
|
+
`| [${item.repo}](${item.url}) | ${item.created.slice(0, 10)} | ${item.pushed.slice(0, 10)} | ${item.stars} | ${item.forks} | ${item.issues} |`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
lines.push("", "## Distinctive phrase search", "");
|
|
81
|
+
if (!token) {
|
|
82
|
+
lines.push("Skipped: set `GITHUB_TOKEN` to enable authenticated GitHub code search.");
|
|
83
|
+
} else {
|
|
84
|
+
for (const result of phrases) {
|
|
85
|
+
lines.push(`- **${result.phrase}** — ${result.external.length} external indexed match(es)`);
|
|
86
|
+
for (const match of result.external) {
|
|
87
|
+
lines.push(` - [${match.repo} · ${match.path}](${match.url})`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
lines.push(
|
|
93
|
+
"",
|
|
94
|
+
"> Signals are leads, not copying findings. Re-check chronology and substantial similarity before drawing a conclusion.",
|
|
95
|
+
);
|
|
96
|
+
return `${lines.join("\n")}\n`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const snapshot = await Promise.all(repos.map(repoSnapshot));
|
|
101
|
+
const phrases = token
|
|
102
|
+
? await Promise.all(distinctivePhrases.map(phraseSnapshot))
|
|
103
|
+
: [];
|
|
104
|
+
process.stdout.write(render(snapshot, phrases));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
process.stderr.write(`competitive-watch: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|