@event4u/agent-config 7.0.1 → 7.0.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +21 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +22 -4
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +33 -8
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +3 -3
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/mcp/registry-manifest.json +2 -2
- package/package.json +1 -1
- package/src/scripts/rdp_gate_classify.ts +131 -0
- package/src/scripts/rdp_quality_eval.ts +437 -0
package/CHANGELOG.md
CHANGED
|
@@ -948,6 +948,27 @@ our recommendation order, not its support status.
|
|
|
948
948
|
> that forces a new era split (`# Era: 7.1.x`, etc.) — see
|
|
949
949
|
> [`docs/contracts/CHANGELOG-conventions.md § Era splits`](docs/contracts/CHANGELOG-conventions.md).
|
|
950
950
|
|
|
951
|
+
## [7.0.2](https://github.com/event4u-app/agent-config/compare/7.0.1...7.0.2) (2026-06-22)
|
|
952
|
+
|
|
953
|
+
### Bug Fixes
|
|
954
|
+
|
|
955
|
+
* **roadmaps:** regen entry-guard fires under symlinked invocation ([244e4e4](https://github.com/event4u-app/agent-config/commit/244e4e45a42a6b3d4793ac192e0f912d71954ba4))
|
|
956
|
+
* **rdp-eval:** harden index access for strict typecheck (noUncheckedIndexedAccess) ([041b21e](https://github.com/event4u-app/agent-config/commit/041b21e4877361b1499328216242fa6ec595ff99))
|
|
957
|
+
* **rdp-eval:** satisfy eslint on the new runners ([3c418e1](https://github.com/event4u-app/agent-config/commit/3c418e16faf340b8367148f6788835e400241e4e))
|
|
958
|
+
|
|
959
|
+
### Documentation
|
|
960
|
+
|
|
961
|
+
* **rdp-eval:** keep-scoped L6 verdict — scope orchestrator to multi-step work ([61173ca](https://github.com/event4u-app/agent-config/commit/61173ca899442b81159e4f3efe586251dcea4459))
|
|
962
|
+
|
|
963
|
+
### Tests
|
|
964
|
+
|
|
965
|
+
* **rdp-eval:** capture larger-N L6 + gate-classification data ([f92aad3](https://github.com/event4u-app/agent-config/commit/f92aad32620af27c5cf4d000859adeeb9894a819))
|
|
966
|
+
* **rdp-eval:** port quality+L6 eval runners to TypeScript (fetch-based) ([9c4ec60](https://github.com/event4u-app/agent-config/commit/9c4ec604ca6fbb82d796cf2d65626d381a854142))
|
|
967
|
+
|
|
968
|
+
### CI
|
|
969
|
+
|
|
970
|
+
* **consistency:** trigger on src/** and dist/agent-src/** paths ([6a5e5e7](https://github.com/event4u-app/agent-config/commit/6a5e5e76cea3d5f879f9fcda6e510cc73039826e))
|
|
971
|
+
|
|
951
972
|
## [7.0.1](https://github.com/event4u-app/agent-config/compare/7.0.0...7.0.1) (2026-06-22)
|
|
952
973
|
|
|
953
974
|
### Bug Fixes
|
|
@@ -784,10 +784,28 @@ function main(argv?: readonly string[]): number {
|
|
|
784
784
|
return 0;
|
|
785
785
|
}
|
|
786
786
|
|
|
787
|
-
|
|
788
|
-
process.argv[1]
|
|
789
|
-
|
|
790
|
-
|
|
787
|
+
function _isCliEntry(): boolean {
|
|
788
|
+
if (process.argv[1] === undefined) {
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
792
|
+
if (import.meta.url === argvUrl) {
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
// A symlinked invocation (e.g. via `.augment/scripts` → `dist/agent-src/scripts`,
|
|
796
|
+
// or macOS /var → /private/var temp dirs) makes the raw URLs differ:
|
|
797
|
+
// import.meta.url is the resolved real path while argv[1] keeps the symlink
|
|
798
|
+
// path. Compare realpaths so the entry guard still fires (without this the
|
|
799
|
+
// dashboard regen silently no-ops when run through the symlink).
|
|
800
|
+
try {
|
|
801
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
802
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
803
|
+
return here === argv;
|
|
804
|
+
} catch {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
791
809
|
try {
|
|
792
810
|
process.exitCode = main();
|
|
793
811
|
} catch (exc) {
|
|
@@ -34,12 +34,29 @@ coordinates); on a standard host run the full chain.
|
|
|
34
34
|
|
|
35
35
|
## When to use
|
|
36
36
|
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
Scope (eval-calibrated, L6): engage only for **interdependent multi-step** work
|
|
38
|
+
— steps whose ordering/handoffs matter (schema → API → job → UI; staged
|
|
39
|
+
migration; cross-cutting refactor). **Not** for single-turn analysis (explain /
|
|
40
|
+
name / yes-no / pick-X-or-Y / one-shot edit): there the ordered chain is a no-op,
|
|
41
|
+
only adds tokens.
|
|
42
|
+
|
|
43
|
+
- Interdependent **multi-step** task on a standard host. Analyze the dependency
|
|
44
|
+
structure (what blocks what) before acting.
|
|
45
|
+
- Work where a missed/out-of-order link (grounding, verification) compounds
|
|
46
|
+
downstream.
|
|
47
|
+
|
|
48
|
+
Do NOT use for trivial / linear / fully-specified tasks **or single-turn
|
|
49
|
+
analysis** (the gate filters these); don't duplicate the work of the skills it
|
|
50
|
+
coordinates.
|
|
51
|
+
|
|
52
|
+
> **Why scoped (L6, 2026-06-22).** Controlled distributed-vs-orchestrated eval
|
|
53
|
+
> (N=16, independent rater): ordered chain gains **+19.2%** on multi-step,
|
|
54
|
+
> **−1.1%** (no-op) on single-turn. Standard host classifies multi-step vs
|
|
55
|
+
> single-turn at gate time 16/16; misclassification degrades gracefully (wrong
|
|
56
|
+
> arm still functional). Revert trigger = re-run `tests/reasoning-layer-eval`
|
|
57
|
+
> (eval is the telemetry — no-runtime package); RDP flip conditions judged
|
|
58
|
+
> per-mechanism, never univariate aggregate. Evidence:
|
|
59
|
+
> `tests/reasoning-layer-eval/RESULTS-L6-largeN-2026-06-22.md`.
|
|
43
60
|
|
|
44
61
|
## When the agent should load this
|
|
45
62
|
|
|
@@ -77,8 +94,16 @@ not exclusivity.
|
|
|
77
94
|
|
|
78
95
|
## Output
|
|
79
96
|
|
|
80
|
-
The task's actual deliverable
|
|
81
|
-
|
|
97
|
+
The task's actual deliverable, produced **through the chain** (analysis before
|
|
98
|
+
action — grounding + load-bearing-unknown resolution precede any edit):
|
|
99
|
+
|
|
100
|
+
1. **Outcome first** — response leads with the result + its evidence.
|
|
101
|
+
2. **Reasoning stays in notes** — hypotheses / predictions / decisions in the
|
|
102
|
+
notes file, never dumped into the response.
|
|
103
|
+
3. **Chain honoured in order** — ground → intent → notes → resolve-hardest-first
|
|
104
|
+
→ audit → verify ran as ordered links (with handoffs), not a buffet.
|
|
105
|
+
4. **Verified before "done"** — no completion claim without the verifier link's
|
|
106
|
+
evidence.
|
|
82
107
|
|
|
83
108
|
## Do NOT
|
|
84
109
|
|
|
@@ -10357,7 +10357,7 @@
|
|
|
10357
10357
|
},
|
|
10358
10358
|
{
|
|
10359
10359
|
"category": "skill",
|
|
10360
|
-
"checksum": "sha256:
|
|
10360
|
+
"checksum": "sha256:c7a74836f37f983ea6432ce8773fd03df7c20bdb616616db4852364406b4ad78",
|
|
10361
10361
|
"install": {
|
|
10362
10362
|
"default": true,
|
|
10363
10363
|
"removable": false
|
|
@@ -11742,7 +11742,7 @@
|
|
|
11742
11742
|
]
|
|
11743
11743
|
}
|
|
11744
11744
|
],
|
|
11745
|
-
"checksum": "sha256:
|
|
11745
|
+
"checksum": "sha256:f5d227f1f899e88091f76a952cdfadda24da8bcc233811a2c55a8ebfdf564fa7",
|
|
11746
11746
|
"deprecations": [
|
|
11747
11747
|
{
|
|
11748
11748
|
"key": "tier",
|
|
@@ -11880,7 +11880,7 @@
|
|
|
11880
11880
|
"reason": "scaffold template for ticket-bundle tickets (ADR-101); carries no runtime frontmatter — emit-tickets fills it in per ticket"
|
|
11881
11881
|
}
|
|
11882
11882
|
],
|
|
11883
|
-
"generated_at": "2026-06-
|
|
11883
|
+
"generated_at": "2026-06-22T07:13:57Z",
|
|
11884
11884
|
"packs": [
|
|
11885
11885
|
{
|
|
11886
11886
|
"artefact_count": 93,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
a8b3e192c4c3462c185328180eed27e001de5e3f87feba346a63e42c6d6ebcb0 discovery-manifest.json
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"checksum": "sha256:
|
|
3
|
-
"generated_at": "2026-06-
|
|
2
|
+
"checksum": "sha256:f5d227f1f899e88091f76a952cdfadda24da8bcc233811a2c55a8ebfdf564fa7",
|
|
3
|
+
"generated_at": "2026-06-22T07:13:57Z",
|
|
4
4
|
"packs": [
|
|
5
5
|
{
|
|
6
6
|
"always_on": false,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"checksum": "sha256:
|
|
3
|
-
"generated_at": "2026-06-
|
|
2
|
+
"checksum": "sha256:f5d227f1f899e88091f76a952cdfadda24da8bcc233811a2c55a8ebfdf564fa7",
|
|
3
|
+
"generated_at": "2026-06-22T07:13:57Z",
|
|
4
4
|
"scanner_version": "47c9a32fb265",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
{
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"artefact_count": 501,
|
|
4
4
|
"scanner_version": "47c9a32fb265"
|
|
5
5
|
},
|
|
6
|
-
"generated_at": "2026-06-
|
|
6
|
+
"generated_at": "2026-06-22",
|
|
7
7
|
"package": {
|
|
8
8
|
"description": "Universal AI Agent OS \u2014 audited skills, governance rules, commands, and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
|
|
9
9
|
"homepage": "https://github.com/event4u-app/agent-config#readme",
|
|
10
10
|
"name": "@event4u/agent-config",
|
|
11
11
|
"repository": "https://github.com/event4u-app/agent-config",
|
|
12
|
-
"version": "7.0.
|
|
12
|
+
"version": "7.0.2"
|
|
13
13
|
},
|
|
14
14
|
"registries": [
|
|
15
15
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.2",
|
|
4
4
|
"description": "Universal AI Agent OS — audited skills, governance rules, commands, and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* RDP gate-time classification validation (road-to-rdp-frontier-polish Phase 1).
|
|
4
|
+
*
|
|
5
|
+
* The council's blocker on "keep-scoped" (2026-06-22): scoping the orchestrator
|
|
6
|
+
* to interdependent-multi-step work via agent self-assessment is only honest if
|
|
7
|
+
* the agent can classify a task AT GATE TIME — from the prompt alone, before
|
|
8
|
+
* doing the work. This script measures exactly that: the standard host classifies
|
|
9
|
+
* each labelled corpus prompt as multi-step vs single-turn, scored against the
|
|
10
|
+
* ground-truth `mechanism` label. High accuracy → the self-assessment gate is a
|
|
11
|
+
* real control, not vibes. Low accuracy → keep-scoped is not implementable.
|
|
12
|
+
*
|
|
13
|
+
* Direct Messages API via fetch (no SDK dep), reuses the key gate + cost
|
|
14
|
+
* estimate from skill_trigger_eval.ts. Dry-run by default; --confirm to spend
|
|
15
|
+
* (+ RDP_EVAL_ALLOW_NONTTY=1 on a non-tty caller that has confirmed cost).
|
|
16
|
+
*
|
|
17
|
+
* Usage: ./scripts-run src/scripts/rdp_gate_classify --corpus <file> --confirm
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from 'node:fs';
|
|
20
|
+
import { estimate_cost, load_anthropic_key } from './skill_trigger_eval.js';
|
|
21
|
+
|
|
22
|
+
/** stdout helper — the lint config forbids `console.log` (allows warn/error). */
|
|
23
|
+
const emit = (s: string): void => {
|
|
24
|
+
process.stdout.write(`${s}\n`);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
|
|
28
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
29
|
+
|
|
30
|
+
const GATE_SYSTEM = `You are an agent deciding, at the START of a task (before doing any work), whether
|
|
31
|
+
to engage a heavyweight multi-step reasoning orchestrator. You see ONLY the task
|
|
32
|
+
prompt. Classify the task into exactly one bucket using the rdp-gate criterion:
|
|
33
|
+
|
|
34
|
+
- "multi-step": interdependent multi-step work where the ordering and handoffs
|
|
35
|
+
between steps matter (e.g. schema → API → job → UI; a migration with stages; a
|
|
36
|
+
cross-cutting refactor). The orchestrator helps here.
|
|
37
|
+
- "single-turn": a single analytical or lookup question, a naming/explain/yes-no
|
|
38
|
+
ask, or a one-shot edit — work that does NOT decompose into ordered
|
|
39
|
+
interdependent steps. The orchestrator over-processes here.
|
|
40
|
+
|
|
41
|
+
Respond with STRICT JSON only: {"bucket":"multi-step"|"single-turn","confidence":"high"|"medium"|"low"}`;
|
|
42
|
+
|
|
43
|
+
interface Slot {
|
|
44
|
+
n: string;
|
|
45
|
+
slug: string;
|
|
46
|
+
mechanism?: string;
|
|
47
|
+
band: string;
|
|
48
|
+
prompt: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function classify(apiKey: string, model: string, prompt: string): Promise<{ bucket: string; confidence: string } | null> {
|
|
52
|
+
const resp = await fetch(ANTHROPIC_URL, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
'x-api-key': apiKey,
|
|
56
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
57
|
+
'content-type': 'application/json',
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
model,
|
|
61
|
+
max_tokens: 60,
|
|
62
|
+
system: GATE_SYSTEM,
|
|
63
|
+
messages: [{ role: 'user', content: `Task prompt:\n${prompt}` }],
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
if (!resp.ok) throw new Error(`API ${resp.status}: ${(await resp.text()).slice(0, 300)}`);
|
|
67
|
+
const data = (await resp.json()) as {
|
|
68
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
69
|
+
usage?: { input_tokens?: number; output_tokens?: number };
|
|
70
|
+
};
|
|
71
|
+
const text = (data.content ?? []).filter((b) => b.type === 'text').map((b) => b.text ?? '').join('');
|
|
72
|
+
const m = text.match(/\{[\s\S]*\}/);
|
|
73
|
+
if (!m) return null;
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(m[0]) as { bucket: string; confidence: string };
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function expectedBucket(mechanism: string | undefined): string {
|
|
82
|
+
return mechanism === 'multi-stage' ? 'multi-step' : 'single-turn';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function main(): Promise<number> {
|
|
86
|
+
const argv = process.argv.slice(2);
|
|
87
|
+
const args: Record<string, string | boolean> = {};
|
|
88
|
+
for (let i = 0; i < argv.length; i++) {
|
|
89
|
+
const t = argv[i];
|
|
90
|
+
if (t === undefined) continue;
|
|
91
|
+
if (t === '--confirm') args.confirm = true;
|
|
92
|
+
else if (t.startsWith('--')) {
|
|
93
|
+
const next = argv[i + 1];
|
|
94
|
+
if (next && !next.startsWith('--')) { args[t.slice(2)] = next; i++; } else args[t.slice(2)] = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const corpusPath = (args.corpus as string) ?? '';
|
|
98
|
+
const model = (args.model as string) ?? 'claude-haiku-4-5-20251001';
|
|
99
|
+
if (!corpusPath) throw new Error('--corpus <file> required');
|
|
100
|
+
const slots = (JSON.parse(fs.readFileSync(corpusPath, 'utf-8')) as { slots: Slot[] }).slots;
|
|
101
|
+
|
|
102
|
+
const est = slots.reduce((p) => p + estimate_cost(model, 250, 60), 0);
|
|
103
|
+
emit(`rdp-gate-classify · ${slots.length} prompts · ${model} · ~$${est.toFixed(4)}`);
|
|
104
|
+
if (!args.confirm) {
|
|
105
|
+
emit('DRY-RUN — no spend. Re-run with --confirm.');
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
if (!process.stdin.isTTY && process.env.RDP_EVAL_ALLOW_NONTTY !== '1') {
|
|
109
|
+
throw new Error('Non-tty: set RDP_EVAL_ALLOW_NONTTY=1 only after confirming cost.');
|
|
110
|
+
}
|
|
111
|
+
const apiKey = load_anthropic_key();
|
|
112
|
+
let correct = 0;
|
|
113
|
+
const rows: Array<Record<string, unknown>> = [];
|
|
114
|
+
for (const s of slots) {
|
|
115
|
+
const r = await classify(apiKey, model, s.prompt);
|
|
116
|
+
const exp = expectedBucket(s.mechanism);
|
|
117
|
+
const got = r?.bucket ?? 'parse-error';
|
|
118
|
+
const ok = got === exp;
|
|
119
|
+
if (ok) correct++;
|
|
120
|
+
console.error(` ${s.n} ${s.slug}: expected ${exp} · got ${got} (${r?.confidence ?? '-'}) ${ok ? 'OK' : 'MISS'}`);
|
|
121
|
+
rows.push({ slot: s.n, slug: s.slug, expected: exp, got, confidence: r?.confidence ?? null, ok });
|
|
122
|
+
}
|
|
123
|
+
const acc = Math.round((correct / slots.length) * 1000) / 10;
|
|
124
|
+
const out = (args.results as string) ?? '/tmp/rdp-gate-classify.json';
|
|
125
|
+
fs.writeFileSync(out, JSON.stringify({ model, n: slots.length, correct, accuracy_pct: acc, rows }, null, 2), 'utf-8');
|
|
126
|
+
emit(`\nGATE-TIME CLASSIFICATION ACCURACY: ${correct}/${slots.length} = ${acc}% (model ${model})`);
|
|
127
|
+
emit(`Results: ${out}`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main().then((c) => process.exit(c), (e) => { console.error(e instanceof Error ? e.message : String(e)); process.exit(1); });
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* RDP quality-layer + L6-isolation eval runner (TypeScript).
|
|
4
|
+
*
|
|
5
|
+
* Ports the Python `run_quality_eval.py` removed in the py2ts teardown. The
|
|
6
|
+
* Anthropic SDK is not a dependency in this repo (the TS trigger runner throws
|
|
7
|
+
* on the live path); this runner therefore calls the Messages API directly via
|
|
8
|
+
* Node's global `fetch` — no new dependency. It reuses the 0600-key gate, the
|
|
9
|
+
* cost-estimate, and the price tables exported by `skill_trigger_eval.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Two modes (controlled system-prompt differential — the differing block is the
|
|
12
|
+
* only variable, so the delta isolates that block's effect):
|
|
13
|
+
* --mode quality : baseline (no RDP) vs treatment (+RDP layer) [L8]
|
|
14
|
+
* --mode l6 : distributed-only (RDP buffet) vs orchestrated (ordered) [L6]
|
|
15
|
+
*
|
|
16
|
+
* The measured model runs with the system prompt this runner supplies, so a
|
|
17
|
+
* baseline is NOT contaminated by the calling agent's own active rules.
|
|
18
|
+
*
|
|
19
|
+
* Optional `--score-with <model>` adds an independent model rater (rater 2):
|
|
20
|
+
* after capture, each variant transcript is scored 0–3 on the 4 rubric dims by
|
|
21
|
+
* a separate model call, recorded alongside the transcript.
|
|
22
|
+
*
|
|
23
|
+
* Cost discipline mirrors skill_trigger_eval.ts: dry-run by default; a billable
|
|
24
|
+
* run needs --confirm, and on a non-tty stdin also RDP_EVAL_ALLOW_NONTTY=1
|
|
25
|
+
* (set only when the caller has already confirmed the cost).
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* ./scripts-run src/scripts/rdp_quality_eval --mode l6 --corpus <file>
|
|
29
|
+
* ./scripts-run src/scripts/rdp_quality_eval --mode l6 --corpus <file> --confirm --score-with claude-sonnet-4-5
|
|
30
|
+
*/
|
|
31
|
+
import * as fs from 'node:fs';
|
|
32
|
+
import * as path from 'node:path';
|
|
33
|
+
import { estimate_cost, load_anthropic_key } from './skill_trigger_eval.js';
|
|
34
|
+
|
|
35
|
+
/** stdout helper — the lint config forbids `console.log` (allows warn/error). */
|
|
36
|
+
const out = (s: string): void => {
|
|
37
|
+
process.stdout.write(`${s}\n`);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const REPO_ROOT = path.resolve(import.meta.dirname, '..', '..');
|
|
41
|
+
const EVAL_DIR = path.join(REPO_ROOT, 'tests', 'reasoning-layer-eval');
|
|
42
|
+
const GT_DIR = path.join(EVAL_DIR, 'golden-transcripts');
|
|
43
|
+
const DEFAULT_CORPUS = path.join(GT_DIR, 'corpus-prompts.json');
|
|
44
|
+
|
|
45
|
+
const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
|
|
46
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
47
|
+
|
|
48
|
+
// ---- the system-prompt blocks (verbatim port of the Python harness) ---------
|
|
49
|
+
const BASE_SYSTEM = `You are a senior software engineering assistant working inside a real codebase.
|
|
50
|
+
|
|
51
|
+
Operating posture:
|
|
52
|
+
- Keep diffs minimal and scoped to the stated task; no drive-by refactors.
|
|
53
|
+
- When a requirement is ambiguous, you may ask one focused clarifying question.
|
|
54
|
+
- Do not claim work is complete without the evidence that proves it.
|
|
55
|
+
- Be direct and concise. No flattery, no filler.`;
|
|
56
|
+
|
|
57
|
+
const RDP_BLOCK = `## Reasoning Discipline Protocol
|
|
58
|
+
|
|
59
|
+
Engage the protocol below only where it pays: skip it on trivial, short, fully
|
|
60
|
+
specified tasks (rename, one-liner, list files); apply it on complex, ambiguous,
|
|
61
|
+
multi-component, stateful, or irreversible tasks. If you are a strong-reasoning
|
|
62
|
+
model that already self-coordinates, apply it lightly.
|
|
63
|
+
|
|
64
|
+
1. GROUND BEFORE DESIGNING. Enumerate the constraints, available facts, and the
|
|
65
|
+
information gaps the task leaves open. Close the load-bearing gaps (by asking
|
|
66
|
+
or by stating an explicit assumption) BEFORE proposing a solution. Never design
|
|
67
|
+
against unstated assumptions.
|
|
68
|
+
|
|
69
|
+
2. INFER THE REAL GOAL. When the literal request and the underlying goal may
|
|
70
|
+
differ, state the inferred goal in one line, then give ONE recommendation —
|
|
71
|
+
not a spread of framings.
|
|
72
|
+
|
|
73
|
+
3. COMPLEXITY-FIRST SEQUENCING. For multi-step work, resolve the hardest /
|
|
74
|
+
most load-bearing unknown FIRST, before dependent work. Do not build the easy
|
|
75
|
+
parts first and rework later. Name what you would tackle first and why.
|
|
76
|
+
|
|
77
|
+
4. NOTES-FIRST OUTPUT. Keep multi-hypothesis reasoning, predictions, and
|
|
78
|
+
decisions in a clearly delimited "## Working notes" section. Your "## Answer"
|
|
79
|
+
section carries CONCLUSIONS + EVIDENCE only — never a raw chain-of-thought
|
|
80
|
+
dump. The answer must be readable by someone who saw none of the working
|
|
81
|
+
thread: outcome-first, no arrow-chain shorthand.
|
|
82
|
+
|
|
83
|
+
5. VERIFIER GATE (risky change). When the task shows two or more of {branching /
|
|
84
|
+
conditional logic, three or more explicit must/must-not constraints, stateful
|
|
85
|
+
operations, irreversibility}, explicitly name what must be verified and how
|
|
86
|
+
BEFORE treating the change as done. Surface the irreversible step for
|
|
87
|
+
confirmation rather than executing it blind.
|
|
88
|
+
|
|
89
|
+
6. PREDICTIONS + DECISIONS (calibration / ledger). When you make an estimate,
|
|
90
|
+
log it as a prediction with a confidence level so it can be checked against
|
|
91
|
+
the actual outcome later. When you choose between alternatives, record the
|
|
92
|
+
decision, the alternatives, the reason, and what would make you revisit it —
|
|
93
|
+
in the Working notes, so a later session can reuse it instead of re-deriving.
|
|
94
|
+
|
|
95
|
+
7. ADAPTIVE EFFORT. Scale effort to difficulty; stop when marginal evidence
|
|
96
|
+
drops rather than over-elaborating.`;
|
|
97
|
+
|
|
98
|
+
const ORCHESTRATOR_PREAMBLE = `## Reasoning orchestration (run the disciplines as ONE ordered chain)
|
|
99
|
+
|
|
100
|
+
Do not treat the protocol below as an optional buffet. Run it as a single
|
|
101
|
+
coordinated chain, in order, with explicit handoffs between links — a skipped or
|
|
102
|
+
out-of-order link compounds downstream:
|
|
103
|
+
|
|
104
|
+
ground → infer intent → write working notes → resolve the load-bearing unknown
|
|
105
|
+
first → audit progress against real evidence → verify before claiming done.
|
|
106
|
+
|
|
107
|
+
Coordinate the links; do not let later steps run before earlier ones. On a
|
|
108
|
+
trivial or fully-specified task, do NOT force the chain (that is over-process) —
|
|
109
|
+
engage it only where the task is genuinely complex/ambiguous/interdependent.`;
|
|
110
|
+
|
|
111
|
+
interface Slot {
|
|
112
|
+
n: string;
|
|
113
|
+
slug: string;
|
|
114
|
+
family?: string;
|
|
115
|
+
band: string;
|
|
116
|
+
discipline?: string;
|
|
117
|
+
mechanism?: string;
|
|
118
|
+
prompt: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface Variant {
|
|
122
|
+
model: string;
|
|
123
|
+
text: string;
|
|
124
|
+
input_tokens: number;
|
|
125
|
+
output_tokens: number;
|
|
126
|
+
cost_usd: number;
|
|
127
|
+
score?: Record<string, unknown>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildArms(mode: string): Array<[string, string]> {
|
|
131
|
+
const treat = `${BASE_SYSTEM}\n\n${RDP_BLOCK}`;
|
|
132
|
+
if (mode === 'l6') {
|
|
133
|
+
return [
|
|
134
|
+
['distributed', treat],
|
|
135
|
+
['orchestrated', `${treat}\n\n${ORCHESTRATOR_PREAMBLE}`],
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
return [
|
|
139
|
+
['baseline', BASE_SYSTEM],
|
|
140
|
+
['treatment', treat],
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function approxTokens(text: string): number {
|
|
145
|
+
return Math.max(1, Math.floor(text.length / 4));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function modelFor(band: string, standard: string, strong: string): string {
|
|
149
|
+
return band === 'strong' ? strong : standard;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface ApiResult {
|
|
153
|
+
text: string;
|
|
154
|
+
input_tokens: number;
|
|
155
|
+
output_tokens: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function callAnthropic(
|
|
159
|
+
apiKey: string,
|
|
160
|
+
model: string,
|
|
161
|
+
system: string,
|
|
162
|
+
userContent: string,
|
|
163
|
+
maxTokens: number,
|
|
164
|
+
): Promise<ApiResult> {
|
|
165
|
+
const resp = await fetch(ANTHROPIC_URL, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: {
|
|
168
|
+
'x-api-key': apiKey,
|
|
169
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
170
|
+
'content-type': 'application/json',
|
|
171
|
+
},
|
|
172
|
+
body: JSON.stringify({
|
|
173
|
+
model,
|
|
174
|
+
max_tokens: maxTokens,
|
|
175
|
+
system,
|
|
176
|
+
messages: [{ role: 'user', content: userContent }],
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
if (!resp.ok) {
|
|
180
|
+
const body = await resp.text();
|
|
181
|
+
throw new Error(`Anthropic API ${resp.status}: ${body.slice(0, 400)}`);
|
|
182
|
+
}
|
|
183
|
+
const data = (await resp.json()) as {
|
|
184
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
185
|
+
usage?: { input_tokens?: number; output_tokens?: number };
|
|
186
|
+
};
|
|
187
|
+
const text = (data.content ?? [])
|
|
188
|
+
.filter((b) => b.type === 'text')
|
|
189
|
+
.map((b) => b.text ?? '')
|
|
190
|
+
.join('');
|
|
191
|
+
return {
|
|
192
|
+
text,
|
|
193
|
+
input_tokens: data.usage?.input_tokens ?? 0,
|
|
194
|
+
output_tokens: data.usage?.output_tokens ?? 0,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const SCORER_SYSTEM = `You are a strict, impartial evaluator. Score one assistant transcript against a
|
|
199
|
+
4-dimension rubric (0–3 each; 3 = excellent, 0 = absent/harmful). Dimensions:
|
|
200
|
+
1 notes-first adherence — reasoning kept out of the answer proper; answer = conclusions + evidence (not a raw chain-of-thought dump).
|
|
201
|
+
2 grounding — environment/constraints/info-gaps closed (or explicitly assumed) before designing; no jumping to a solution against assumptions.
|
|
202
|
+
3 premature-solution avoidance — the hardest/load-bearing unknown resolved before dependent work; no over-production of code/answer before grounding.
|
|
203
|
+
4 coherence / re-grounded summary — final answer readable by someone who saw none of the working thread; outcome-first; no arrow-chain shorthand.
|
|
204
|
+
Also report reasoning_extraction_refusal: true ONLY if the model refused due to a meta/extraction instruction.
|
|
205
|
+
Respond with STRICT JSON only, no prose: {"dim1":N,"dim2":N,"dim3":N,"dim4":N,"reasoning_extraction_refusal":false,"note":"<=120 chars"}`;
|
|
206
|
+
|
|
207
|
+
async function scoreTranscript(
|
|
208
|
+
apiKey: string,
|
|
209
|
+
scorerModel: string,
|
|
210
|
+
prompt: string,
|
|
211
|
+
transcript: string,
|
|
212
|
+
): Promise<Record<string, unknown> | null> {
|
|
213
|
+
const user = `## Task prompt given to the assistant\n${prompt}\n\n## Assistant transcript to score\n${transcript}\n\nScore now as strict JSON.`;
|
|
214
|
+
const r = await callAnthropic(apiKey, scorerModel, SCORER_SYSTEM, user, 300);
|
|
215
|
+
const m = r.text.match(/\{[\s\S]*\}/);
|
|
216
|
+
if (!m) return null;
|
|
217
|
+
try {
|
|
218
|
+
return JSON.parse(m[0]) as Record<string, unknown>;
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function loadSlots(corpusPath: string, selected: Set<string> | null): Slot[] {
|
|
225
|
+
const data = JSON.parse(fs.readFileSync(corpusPath, 'utf-8')) as { slots: Slot[] };
|
|
226
|
+
let slots = data.slots;
|
|
227
|
+
if (selected) slots = slots.filter((s) => selected.has(s.n));
|
|
228
|
+
return slots;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function parseArgs(argv: string[]): Record<string, string | boolean> {
|
|
232
|
+
const a: Record<string, string | boolean> = {};
|
|
233
|
+
for (let i = 0; i < argv.length; i++) {
|
|
234
|
+
const t = argv[i];
|
|
235
|
+
if (t === undefined) continue;
|
|
236
|
+
if (t === '--confirm') a.confirm = true;
|
|
237
|
+
else if (t.startsWith('--')) {
|
|
238
|
+
const key = t.slice(2);
|
|
239
|
+
const next = argv[i + 1];
|
|
240
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
241
|
+
a[key] = next;
|
|
242
|
+
i++;
|
|
243
|
+
} else a[key] = true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return a;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function meanDims(score: Record<string, unknown> | undefined): number | null {
|
|
250
|
+
if (!score) return null;
|
|
251
|
+
const ds = ['dim1', 'dim2', 'dim3', 'dim4'].map((k) => Number(score[k]));
|
|
252
|
+
if (ds.some((x) => Number.isNaN(x))) return null;
|
|
253
|
+
return ds.reduce((p, c) => p + c, 0) / 4;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function main(): Promise<number> {
|
|
257
|
+
const args = parseArgs(process.argv.slice(2));
|
|
258
|
+
const mode = (args.mode as string) ?? 'quality';
|
|
259
|
+
const corpusPath = (args.corpus as string) ?? DEFAULT_CORPUS;
|
|
260
|
+
const standardModel = (args['standard-model'] as string) ?? 'claude-haiku-4-5-20251001';
|
|
261
|
+
const strongModel = (args['strong-model'] as string) ?? 'claude-sonnet-4-5';
|
|
262
|
+
const maxTokens = args['max-tokens'] ? Number(args['max-tokens']) : 1600;
|
|
263
|
+
const scoreWith = args['score-with'] as string | undefined;
|
|
264
|
+
const selected = args.slots
|
|
265
|
+
? new Set(String(args.slots).split(',').map((s) => s.trim().padStart(2, '0')))
|
|
266
|
+
: null;
|
|
267
|
+
const resultsPath =
|
|
268
|
+
(args.results as string) ??
|
|
269
|
+
path.join(GT_DIR, mode === 'l6' ? 'l6-results.json' : 'results.json');
|
|
270
|
+
|
|
271
|
+
const slots = loadSlots(corpusPath, selected);
|
|
272
|
+
const arms = buildArms(mode);
|
|
273
|
+
const variantNames = arms.map((a) => a[0]);
|
|
274
|
+
|
|
275
|
+
// ---- cost preview --------------------------------------------------------
|
|
276
|
+
let total = 0;
|
|
277
|
+
const byModel: Record<string, number> = {};
|
|
278
|
+
for (const s of slots) {
|
|
279
|
+
const model = modelFor(s.band, standardModel, strongModel);
|
|
280
|
+
for (const [, sys] of arms) {
|
|
281
|
+
const tin = approxTokens(sys) + approxTokens(s.prompt);
|
|
282
|
+
const c = estimate_cost(model, tin, maxTokens);
|
|
283
|
+
total += c;
|
|
284
|
+
byModel[model] = (byModel[model] ?? 0) + c;
|
|
285
|
+
}
|
|
286
|
+
if (scoreWith) {
|
|
287
|
+
// 2 scoring calls per slot (one per variant)
|
|
288
|
+
const c = estimate_cost(scoreWith, 2000, 300) * 2;
|
|
289
|
+
total += c;
|
|
290
|
+
byModel[`${scoreWith} (scorer)`] = (byModel[`${scoreWith} (scorer)`] ?? 0) + c;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const nCalls = slots.length * arms.length + (scoreWith ? slots.length * 2 : 0);
|
|
294
|
+
out(
|
|
295
|
+
`rdp-eval · mode=${mode} · ${slots.length} slots × ${arms.length} variants (${variantNames.join('/')}) ` +
|
|
296
|
+
`${scoreWith ? `+ scorer(${scoreWith})` : ''} = ${nCalls} calls`,
|
|
297
|
+
);
|
|
298
|
+
for (const [m, c] of Object.entries(byModel)) out(` ${m}: ~$${c.toFixed(4)}`);
|
|
299
|
+
out(` EXPECTED TOTAL (worst-case): ~$${total.toFixed(4)}`);
|
|
300
|
+
|
|
301
|
+
if (!args.confirm) {
|
|
302
|
+
out('\nDRY-RUN — no spend. Re-run with --confirm to capture transcripts.');
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
if (!process.stdin.isTTY && process.env.RDP_EVAL_ALLOW_NONTTY !== '1') {
|
|
306
|
+
throw new Error(
|
|
307
|
+
'Refusing a billable run on non-tty stdin. Run interactively, or set ' +
|
|
308
|
+
'RDP_EVAL_ALLOW_NONTTY=1 if the caller has already confirmed the cost.',
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const apiKey = load_anthropic_key();
|
|
313
|
+
const ts = (args.date as string) ?? '0000-00-00';
|
|
314
|
+
let actual = 0;
|
|
315
|
+
const results: Array<Record<string, unknown>> = [];
|
|
316
|
+
|
|
317
|
+
for (const s of slots) {
|
|
318
|
+
const model = modelFor(s.band, standardModel, strongModel);
|
|
319
|
+
const variants: Record<string, Variant> = {};
|
|
320
|
+
for (const [variant, sys] of arms) {
|
|
321
|
+
console.error(` → slot ${s.n} ${variant} (${model}) …`);
|
|
322
|
+
const r = await callAnthropic(apiKey, model, sys, s.prompt, maxTokens);
|
|
323
|
+
const cost = estimate_cost(model, r.input_tokens, r.output_tokens);
|
|
324
|
+
actual += cost;
|
|
325
|
+
const v: Variant = {
|
|
326
|
+
model,
|
|
327
|
+
text: r.text,
|
|
328
|
+
input_tokens: r.input_tokens,
|
|
329
|
+
output_tokens: r.output_tokens,
|
|
330
|
+
cost_usd: cost,
|
|
331
|
+
};
|
|
332
|
+
if (scoreWith) {
|
|
333
|
+
console.error(` scoring ${variant} via ${scoreWith} …`);
|
|
334
|
+
const sc = await scoreTranscript(apiKey, scoreWith, s.prompt, r.text);
|
|
335
|
+
actual += estimate_cost(scoreWith, 2000, 300);
|
|
336
|
+
if (sc) v.score = sc;
|
|
337
|
+
}
|
|
338
|
+
variants[variant] = v;
|
|
339
|
+
}
|
|
340
|
+
const [v0, v1] = variantNames;
|
|
341
|
+
const va = v0 !== undefined ? variants[v0] : undefined;
|
|
342
|
+
const vb = v1 !== undefined ? variants[v1] : undefined;
|
|
343
|
+
if (v0 === undefined || v1 === undefined || va === undefined || vb === undefined) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const aOut = va.output_tokens;
|
|
347
|
+
const bOut = vb.output_tokens;
|
|
348
|
+
const overhead = aOut ? Math.round(((bOut - aOut) / aOut) * 1000) / 10 : null;
|
|
349
|
+
const m0 = meanDims(va.score);
|
|
350
|
+
const m1 = meanDims(vb.score);
|
|
351
|
+
results.push({
|
|
352
|
+
slot: s.n,
|
|
353
|
+
slug: s.slug,
|
|
354
|
+
band: s.band,
|
|
355
|
+
mechanism: s.mechanism ?? null,
|
|
356
|
+
discipline: s.discipline ?? null,
|
|
357
|
+
prompt: s.prompt,
|
|
358
|
+
model,
|
|
359
|
+
variants,
|
|
360
|
+
output_token_overhead_pct: overhead,
|
|
361
|
+
rater2_mean: { [v0]: m0, [v1]: m1 },
|
|
362
|
+
rater2_delta: m0 !== null && m1 !== null ? Math.round((m1 - m0) * 100) / 100 : null,
|
|
363
|
+
});
|
|
364
|
+
writeTranscript(s, variants, ts, overhead, variantNames, mode);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
fs.writeFileSync(
|
|
368
|
+
resultsPath,
|
|
369
|
+
JSON.stringify(
|
|
370
|
+
{
|
|
371
|
+
date: ts,
|
|
372
|
+
mode,
|
|
373
|
+
standard_model: standardModel,
|
|
374
|
+
strong_model: strongModel,
|
|
375
|
+
scorer_model: scoreWith ?? null,
|
|
376
|
+
actual_cost_usd: Math.round(actual * 10000) / 10000,
|
|
377
|
+
results,
|
|
378
|
+
},
|
|
379
|
+
null,
|
|
380
|
+
2,
|
|
381
|
+
),
|
|
382
|
+
'utf-8',
|
|
383
|
+
);
|
|
384
|
+
out(`\nDONE · mode=${mode} · actual ~$${(Math.round(actual * 10000) / 10000).toFixed(4)}`);
|
|
385
|
+
out(`Results JSON: ${resultsPath}`);
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function writeTranscript(
|
|
390
|
+
slot: Slot,
|
|
391
|
+
variants: Record<string, Variant>,
|
|
392
|
+
ts: string,
|
|
393
|
+
overhead: number | null,
|
|
394
|
+
variantNames: string[],
|
|
395
|
+
mode: string,
|
|
396
|
+
): void {
|
|
397
|
+
const prefix = mode === 'l6' ? 'l6n-' : '';
|
|
398
|
+
const p = path.join(GT_DIR, `${prefix}${slot.n}-${slot.slug}.md`);
|
|
399
|
+
const L: string[] = [
|
|
400
|
+
`# Transcript — slot ${slot.n}: ${slot.slug}`,
|
|
401
|
+
'',
|
|
402
|
+
`- **Band:** ${slot.band}${slot.mechanism ? ` · **Mechanism:** ${slot.mechanism}` : ''}`,
|
|
403
|
+
`- **Captured:** ${ts} (controlled system-prompt differential; rater 2 = model scorer)`,
|
|
404
|
+
'',
|
|
405
|
+
'## Prompt',
|
|
406
|
+
'',
|
|
407
|
+
slot.prompt,
|
|
408
|
+
'',
|
|
409
|
+
];
|
|
410
|
+
for (const variant of variantNames) {
|
|
411
|
+
const v = variants[variant];
|
|
412
|
+
if (v === undefined) continue;
|
|
413
|
+
const sc = v.score ? ` · rater2 ${JSON.stringify(v.score)}` : '';
|
|
414
|
+
L.push(
|
|
415
|
+
`## Transcript — ${variant} (${v.model})`,
|
|
416
|
+
'',
|
|
417
|
+
'~~~text',
|
|
418
|
+
v.text.trimEnd(),
|
|
419
|
+
'~~~',
|
|
420
|
+
'',
|
|
421
|
+
`**Tokens:** in ${v.input_tokens} / out ${v.output_tokens} / est $${v.cost_usd}${sc}`,
|
|
422
|
+
'',
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
if (overhead !== null) {
|
|
426
|
+
L.push(`**Output-token overhead (${variantNames[1]} vs ${variantNames[0]}):** ${overhead > 0 ? '+' : ''}${overhead}%`, '');
|
|
427
|
+
}
|
|
428
|
+
fs.writeFileSync(p, L.join('\n'), 'utf-8');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
main().then(
|
|
432
|
+
(code) => process.exit(code),
|
|
433
|
+
(err) => {
|
|
434
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
435
|
+
process.exit(1);
|
|
436
|
+
},
|
|
437
|
+
);
|