@open-agent-toolkit/cli 0.2.77 → 0.2.79

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.
@@ -21,7 +21,7 @@ Reject an incomplete or contradictory assignment before reading sources. Never
21
21
  request credentials, mutate an investigated source, broaden scope, or choose an
22
22
  alternate write path.
23
23
 
24
- The manifest's ten wave modes map to the worker's closed seven-mode vocabulary:
24
+ The manifest's nine wave modes map to the worker's closed six-mode vocabulary:
25
25
 
26
26
  | Manifest wave mode | Worker assignment mode |
27
27
  | -------------------------- | ---------------------- |
@@ -31,7 +31,6 @@ The manifest's ten wave modes map to the worker's closed seven-mode vocabulary:
31
31
  | `semantic-verification` | `verify` |
32
32
  | `adversarial` | `adversary` |
33
33
  | `coverage` | `coverage` |
34
- | `reconciliation` | `reconcile` |
35
34
  | `redundant-gather` | `gather` |
36
35
  | `redundant-verification` | `verify` |
37
36
  | `contradiction-resolution` | `adversary` |
@@ -47,17 +46,16 @@ wave target; the worker neither selects nor upgrades it.
47
46
  - `gather`: inspect assigned sources and emit source-grounded findings with
48
47
  typed locators, minimal display excerpts, uncertainty, and contradictions.
49
48
  - `compile`: consume only designated dossiers and create a provisional claim
50
- ledger candidate. Deduplicate without inventing evidence.
49
+ ledger candidate. Deduplicate without inventing evidence. Display excerpts
50
+ must be exact contiguous source substrings, except for the explicit
51
+ `redacted-exact` representation; never paraphrase an excerpt.
51
52
  - `verify`: consume an immutable verification brief, reopen only its declared
52
53
  sources, test locators and claim semantics, and report claim dispositions.
53
54
  - `adversary`: consume scope, questions, and provisional statements only; seek
54
55
  counterevidence, unsupported inference, and missing alternatives.
55
56
  - `coverage`: compare declared scope and questions with ledger coverage without
56
57
  reading gatherer reasoning.
57
- - `reconcile`: apply review dispositions and contradiction outcomes to a new
58
- ledger candidate. Preserve prior revisions and never invent evidence.
59
-
60
- No other mode is valid.
58
+ No other mode is valid.
61
59
 
62
60
  ## Output Contract
63
61
 
@@ -83,11 +81,93 @@ Persist minimal excerpts only. Detect and redact secret spans before writing;
83
81
  never persist the secret or its sensitive-span digest. Finish by returning the
84
82
  artifact path and compact outcome only.
85
83
 
84
+ Before returning, run the supplied deterministic validator against the sole
85
+ unpromoted `writePath`. Correct an invalid candidate within the same accepted
86
+ task and validate again. A terminal invalid candidate is `PASS_FAILED`; it does
87
+ not authorize controller retry or replacement.
88
+
89
+ Closed review-result examples (all omitted arrays are still required as shown):
90
+
91
+ ```json
92
+ {
93
+ "kind": "recon.review-result",
94
+ "schemaVersion": 1,
95
+ "id": "review-semantic",
96
+ "runId": "run-1",
97
+ "reviewKind": "semantic",
98
+ "reviewerLane": "lane-semantic",
99
+ "status": "complete",
100
+ "brief": {
101
+ "path": "reviews/briefs/verify.json",
102
+ "digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
103
+ },
104
+ "permittedInputs": [],
105
+ "excludedInputs": [],
106
+ "dispositions": [],
107
+ "newEvidence": [],
108
+ "evidenceAssociations": [],
109
+ "coverageFindings": [],
110
+ "unresolvedIssues": []
111
+ }
112
+ ```
113
+
114
+ ```json
115
+ {
116
+ "kind": "recon.review-result",
117
+ "schemaVersion": 1,
118
+ "id": "review-adversarial",
119
+ "runId": "run-1",
120
+ "reviewKind": "adversarial",
121
+ "reviewerLane": "lane-adversarial",
122
+ "status": "complete",
123
+ "brief": {
124
+ "path": "reviews/briefs/adversary.json",
125
+ "digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
126
+ },
127
+ "permittedInputs": [],
128
+ "excludedInputs": [],
129
+ "dispositions": [],
130
+ "newEvidence": [],
131
+ "evidenceAssociations": [],
132
+ "coverageFindings": [],
133
+ "unresolvedIssues": []
134
+ }
135
+ ```
136
+
137
+ ```json
138
+ {
139
+ "kind": "recon.review-result",
140
+ "schemaVersion": 1,
141
+ "id": "review-coverage",
142
+ "runId": "run-1",
143
+ "reviewKind": "coverage",
144
+ "reviewerLane": "lane-coverage",
145
+ "status": "complete",
146
+ "brief": {
147
+ "path": "reviews/briefs/coverage.json",
148
+ "digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
149
+ },
150
+ "permittedInputs": [],
151
+ "excludedInputs": [],
152
+ "dispositions": [],
153
+ "newEvidence": [],
154
+ "evidenceAssociations": [],
155
+ "coverageFindings": [],
156
+ "unresolvedIssues": []
157
+ }
158
+ ```
159
+
86
160
  The controller validates the candidate against `packet-contract.md` with the
87
161
  bundled deterministic artifact validator. A validation failure quarantines the
88
162
  candidate; it never authorizes the worker to rewrite a shared artifact, retry,
89
163
  or launch a replacement.
90
164
 
165
+ For an accepted launch, the approved-path artifact is authoritative over the
166
+ return transport. If its identity, schema, bytes, and digest validate, a later
167
+ stream-close diagnostic is non-material and the lane is complete. Missing or
168
+ invalid output remains terminal `PASS_FAILED`; neither outcome authorizes a
169
+ replacement launch.
170
+
91
171
  ## Invariants
92
172
 
93
173
  - Read only the assignment's allowed inputs using its source-read authority.
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { realpathSync } from 'node:fs';
4
3
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
5
4
  import { dirname, relative, resolve } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
5
 
8
6
  import { canonicalJson, hashFile } from './lib/canonical-json.mjs';
7
+ import { isDirectExecution } from './lib/cli-entry.mjs';
9
8
  import { issue, isObject, validateArtifactShape } from './lib/contracts.mjs';
10
9
  import { assertSafeOutputPath } from './lib/safe-path.mjs';
11
10
 
@@ -442,29 +441,7 @@ async function main(argv) {
442
441
  process.stdout.write(`${JSON.stringify(reference, null, 2)}\n`);
443
442
  }
444
443
 
445
- /**
446
- * Direct invocation, compared as canonical paths on both sides. Comparing a raw
447
- * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
448
- * that exits 0 whenever the skill is reached through a symlinked install root,
449
- * and canonicalizing only one side has the same effect under
450
- * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
451
- * A path that cannot be canonicalized is not a module Node loaded as the entry
452
- * point, so a thrown `realpathSync` means "not invoked directly" and returns
453
- * `false`; it never masks a direct run.
454
- */
455
- function isDirectInvocation(invokedPath) {
456
- if (!invokedPath) return false;
457
- try {
458
- return (
459
- realpathSync(fileURLToPath(import.meta.url)) ===
460
- realpathSync(resolve(invokedPath))
461
- );
462
- } catch {
463
- return false;
464
- }
465
- }
466
-
467
- if (isDirectInvocation(process.argv[1])) {
444
+ if (isDirectExecution(import.meta.url)) {
468
445
  main(process.argv.slice(2)).catch((error) => {
469
446
  process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
470
447
  process.exitCode = 1;
@@ -0,0 +1,21 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ /**
6
+ * Compare canonical paths on both sides so direct execution works through a
7
+ * symlinked install root, including with `--preserve-symlinks-main`. A path
8
+ * that cannot be canonicalized is not the loaded entry module, so resolution
9
+ * failure returns false without masking a direct run.
10
+ */
11
+ export function isDirectExecution(moduleUrl, entryPath = process.argv[1]) {
12
+ if (!entryPath) return false;
13
+ try {
14
+ return (
15
+ realpathSync(fileURLToPath(moduleUrl)) ===
16
+ realpathSync(resolve(entryPath))
17
+ );
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
@@ -33,7 +33,6 @@ export const workerModes = [
33
33
  'verify',
34
34
  'adversary',
35
35
  'coverage',
36
- 'reconcile',
37
36
  ];
38
37
  export const evidenceRelations = [
39
38
  'supports',
@@ -49,7 +48,6 @@ export const waveModes = [
49
48
  'semantic-verification',
50
49
  'adversarial',
51
50
  'coverage',
52
- 'reconciliation',
53
51
  'redundant-gather',
54
52
  'redundant-verification',
55
53
  'contradiction-resolution',
@@ -67,6 +65,18 @@ export const conditionPredicates = [
67
65
  'unresolved-material-challenge',
68
66
  ];
69
67
 
68
+ const reconciliationReviewPaths = Object.freeze({
69
+ 'semantic-verification': 'reviews/semantic.json',
70
+ adversarial: 'reviews/adversarial.json',
71
+ coverage: 'reviews/coverage.json',
72
+ 'redundant-verification': 'reviews/redundant-verification.json',
73
+ 'contradiction-resolution': 'reviews/contradiction-resolution.json',
74
+ });
75
+
76
+ const singletonReviewWaveModes = new Set(
77
+ Object.keys(reconciliationReviewPaths),
78
+ );
79
+
70
80
  function profilePolicy(
71
81
  orderedSingletonWaveModes,
72
82
  allowedWaveModes,
@@ -100,7 +110,6 @@ export const profileRoutingPolicy = Object.freeze({
100
110
  'semantic-verification',
101
111
  'adversarial',
102
112
  'coverage',
103
- 'reconciliation',
104
113
  ],
105
114
  [
106
115
  'map',
@@ -109,7 +118,6 @@ export const profileRoutingPolicy = Object.freeze({
109
118
  'semantic-verification',
110
119
  'adversarial',
111
120
  'coverage',
112
- 'reconciliation',
113
121
  'contradiction-resolution',
114
122
  ],
115
123
  [
@@ -131,7 +139,6 @@ export const profileRoutingPolicy = Object.freeze({
131
139
  'adversarial',
132
140
  'coverage',
133
141
  'redundant-verification',
134
- 'reconciliation',
135
142
  ],
136
143
  [
137
144
  'map',
@@ -142,7 +149,6 @@ export const profileRoutingPolicy = Object.freeze({
142
149
  'coverage',
143
150
  'redundant-gather',
144
151
  'redundant-verification',
145
- 'reconciliation',
146
152
  'contradiction-resolution',
147
153
  ],
148
154
  [
@@ -154,7 +160,7 @@ export const profileRoutingPolicy = Object.freeze({
154
160
  'redundant-verification',
155
161
  'contradiction-resolution',
156
162
  ],
157
- { lanes: 20, concurrency: 8, conditions: 2 },
163
+ { lanes: 20, concurrency: 8, conditions: 1 },
158
164
  ),
159
165
  });
160
166
 
@@ -359,6 +365,7 @@ const executionV2Keys = new Set([
359
365
  'retryLimit',
360
366
  'waves',
361
367
  'conditions',
368
+ 'reconciliation',
362
369
  'approval',
363
370
  ]);
364
371
 
@@ -410,6 +417,57 @@ function validateExecutionV2(value, errors, path) {
410
417
  requiredObject(value, 'approval', errors, path);
411
418
  validateApprovalEvidence(value.approval, errors, `${path}.approval`);
412
419
 
420
+ if (Object.hasOwn(value, 'reconciliation')) {
421
+ const reconciliation = value.reconciliation;
422
+ requiredObject(value, 'reconciliation', errors, path);
423
+ if (isObject(reconciliation)) {
424
+ closedObject(
425
+ reconciliation,
426
+ new Set([
427
+ 'producer',
428
+ 'inputLedger',
429
+ 'outputLedger',
430
+ 'outputReview',
431
+ 'requiredReviews',
432
+ 'conditionalReviews',
433
+ ]),
434
+ errors,
435
+ `${path}.reconciliation`,
436
+ );
437
+ for (const key of [
438
+ 'producer',
439
+ 'inputLedger',
440
+ 'outputLedger',
441
+ 'outputReview',
442
+ ]) {
443
+ requiredString(reconciliation, key, errors, `${path}.reconciliation`);
444
+ }
445
+ if (reconciliation.producer !== 'controller:reconcile-ledger-v1') {
446
+ errors.push(
447
+ issue(
448
+ 'INVALID_RECONCILIATION_PRODUCER',
449
+ 'Controller reconciliation must use controller:reconcile-ledger-v1',
450
+ `${path}.reconciliation.producer`,
451
+ ),
452
+ );
453
+ }
454
+ for (const key of ['requiredReviews', 'conditionalReviews']) {
455
+ requiredArray(reconciliation, key, errors, `${path}.reconciliation`);
456
+ for (const [index, item] of (reconciliation[key] ?? []).entries()) {
457
+ if (typeof item !== 'string' || item.length === 0) {
458
+ errors.push(
459
+ issue(
460
+ 'INVALID_RECONCILIATION_PATH',
461
+ `${key} must contain non-empty paths`,
462
+ `${path}.reconciliation.${key}[${index}]`,
463
+ ),
464
+ );
465
+ }
466
+ }
467
+ }
468
+ }
469
+ }
470
+
413
471
  const waves = Array.isArray(value.waves) ? value.waves : [];
414
472
  const waveIds = new Set();
415
473
  const laneIds = new Set();
@@ -624,7 +682,8 @@ export function validateV2ProfileTopology(
624
682
  for (const [waveIndex, wave] of waves.entries()) {
625
683
  if (
626
684
  policy.allowedWaveModes.includes(wave?.mode) &&
627
- !policy.countedLaneModes.includes(wave.mode) &&
685
+ (!policy.countedLaneModes.includes(wave.mode) ||
686
+ singletonReviewWaveModes.has(wave.mode)) &&
628
687
  Array.isArray(wave?.lanes) &&
629
688
  wave.lanes.length !== 1
630
689
  ) {
@@ -673,25 +732,77 @@ export function validateV2ProfileTopology(
673
732
  ),
674
733
  );
675
734
  }
676
- const reconciliationIndexes = waves
677
- .map((wave, index) => (wave?.mode === 'reconciliation' ? index : -1))
678
- .filter((index) => index !== -1);
679
735
  const requiresReconciliation = ['standard', 'thorough'].includes(
680
736
  requestedProfile,
681
737
  );
682
- if (
683
- reconciliationIndexes.length > 1 ||
684
- (requiresReconciliation && reconciliationIndexes.length !== 1) ||
685
- reconciliationIndexes.some((index) => index !== waves.length - 1) ||
686
- reconciliationIndexes.some((index) => waves[index]?.conditional === true)
687
- ) {
738
+ if (requiresReconciliation !== isObject(execution.reconciliation)) {
688
739
  errors.push(
689
740
  issue(
690
741
  'INVALID_TERMINAL_TOPOLOGY',
691
- 'Standard and thorough routing require exactly one non-conditional terminal reconciliation wave',
692
- `${path}.waves`,
742
+ 'Standard and thorough routing require exactly one controller reconciliation declaration',
743
+ `${path}.reconciliation`,
693
744
  ),
694
745
  );
746
+ } else if (requiresReconciliation) {
747
+ const reconciliation = execution.reconciliation;
748
+ const expected = {
749
+ producer: 'controller:reconcile-ledger-v1',
750
+ inputLedger: 'raw/drafts/claims-v1.json',
751
+ outputLedger: 'raw/drafts/claims-v2.json',
752
+ outputReview: 'reviews/reconciliation.json',
753
+ requiredReviews: [
754
+ reconciliationReviewPaths['semantic-verification'],
755
+ reconciliationReviewPaths.adversarial,
756
+ reconciliationReviewPaths.coverage,
757
+ ],
758
+ conditionalReviews: [
759
+ reconciliationReviewPaths['redundant-verification'],
760
+ reconciliationReviewPaths['contradiction-resolution'],
761
+ ],
762
+ };
763
+ const matchesExpected =
764
+ ['producer', 'inputLedger', 'outputLedger', 'outputReview'].every(
765
+ (key) => reconciliation[key] === expected[key],
766
+ ) &&
767
+ ['requiredReviews', 'conditionalReviews'].every(
768
+ (key) =>
769
+ Array.isArray(reconciliation[key]) &&
770
+ reconciliation[key].length === expected[key].length &&
771
+ reconciliation[key].every(
772
+ (value, index) => value === expected[key][index],
773
+ ),
774
+ );
775
+ if (!matchesExpected) {
776
+ errors.push(
777
+ issue(
778
+ 'INVALID_RECONCILIATION_PATH',
779
+ 'Controller reconciliation paths and review sets must exactly match the closed manifest contract',
780
+ `${path}.reconciliation`,
781
+ ),
782
+ );
783
+ }
784
+ for (const [mode, reviewPath] of Object.entries(
785
+ reconciliationReviewPaths,
786
+ )) {
787
+ const matchingWaves = waves.filter((wave) => wave?.mode === mode);
788
+ if (matchingWaves.length !== 1 || matchingWaves[0].lanes?.length !== 1) {
789
+ continue;
790
+ }
791
+ const writeRoot = matchingWaves[0].lanes[0].writeRoot;
792
+ if (
793
+ typeof writeRoot === 'string' &&
794
+ reviewPath !== writeRoot &&
795
+ !reviewPath.startsWith(`${writeRoot}/`)
796
+ ) {
797
+ errors.push(
798
+ issue(
799
+ 'INVALID_RECONCILIATION_PATH',
800
+ `${mode} output ${reviewPath} must be inside its approved write root`,
801
+ `${path}.waves`,
802
+ ),
803
+ );
804
+ }
805
+ }
695
806
  }
696
807
 
697
808
  const waveIndexes = new Map(
@@ -708,7 +819,6 @@ export function validateV2ProfileTopology(
708
819
  'predicate',
709
820
  'maxActivations',
710
821
  ]);
711
- const terminalIndex = reconciliationIndexes[0] ?? -1;
712
822
  for (const [index, condition] of conditions.entries()) {
713
823
  const conditionPath = `${path}.conditions[${index}]`;
714
824
  if (
@@ -852,24 +962,10 @@ export function validateV2ProfileTopology(
852
962
  );
853
963
  }
854
964
  }
855
- if (
856
- terminalIndex !== -1 &&
857
- destinationIndex !== undefined &&
858
- destinationIndex >= terminalIndex
859
- ) {
860
- errors.push(
861
- issue(
862
- 'INVALID_TERMINAL_TOPOLOGY',
863
- 'Conditional evidence waves must complete before the one terminal reconciliation',
864
- `${conditionPath}.destinationWaveId`,
865
- ),
866
- );
867
- }
868
965
  }
869
966
  for (const wave of waves) {
870
967
  if (
871
968
  wave?.conditional === true &&
872
- wave.mode !== 'reconciliation' &&
873
969
  !policy.orderedSingletonWaveModes.includes(wave.mode) &&
874
970
  !conditionDestinations.has(wave.waveId)
875
971
  ) {
@@ -927,10 +1023,7 @@ export function validateV2ProfileTopology(
927
1023
  );
928
1024
  }
929
1025
  const conditionalRequired = stageIndexes.find(
930
- ({ mode, indexes }) =>
931
- mode !== 'reconciliation' &&
932
- indexes.length === 1 &&
933
- waves[indexes[0]]?.conditional,
1026
+ ({ indexes }) => indexes.length === 1 && waves[indexes[0]]?.conditional,
934
1027
  );
935
1028
  if (conditionalRequired) {
936
1029
  errors.push(
@@ -2192,6 +2285,19 @@ function validateReviewResult(value, errors) {
2192
2285
  ]) {
2193
2286
  requiredArray(value, key, errors);
2194
2287
  }
2288
+ if (Array.isArray(value.unresolvedIssues)) {
2289
+ for (const [index, unresolvedIssue] of value.unresolvedIssues.entries()) {
2290
+ if (typeof unresolvedIssue !== 'string') {
2291
+ errors.push(
2292
+ issue(
2293
+ 'INVALID_UNRESOLVED_ISSUE',
2294
+ 'unresolvedIssues members must be strings',
2295
+ `$.unresolvedIssues[${index}]`,
2296
+ ),
2297
+ );
2298
+ }
2299
+ }
2300
+ }
2195
2301
  if (
2196
2302
  ![
2197
2303
  'semantic',
@@ -52,11 +52,6 @@ const economicalDefaults = Object.freeze({
52
52
  'Compare evidence with an explicit question or source inventory.',
53
53
  taskClass: 'mechanical-recon',
54
54
  }),
55
- reconciliation: Object.freeze({
56
- assignment:
57
- 'Assemble agreement and disagreement without deciding implications.',
58
- taskClass: 'mechanical-recon',
59
- }),
60
55
  'contradiction-resolution': Object.freeze({
61
56
  assignment: 'Seek discriminating evidence for a named contradiction.',
62
57
  taskClass: 'mechanical-recon',
@@ -172,6 +167,7 @@ function assertProposalExecution(manifest) {
172
167
  'retryLimit',
173
168
  'waves',
174
169
  'conditions',
170
+ 'reconciliation',
175
171
  'approval',
176
172
  ]),
177
173
  'INVALID_ROUTING_PROPOSAL',
@@ -377,6 +373,7 @@ export function normalizeManifestRouting(manifest) {
377
373
  target: resolveEffectiveWaveTarget(execution, wave),
378
374
  })),
379
375
  conditions: clone(execution.conditions),
376
+ reconciliation: clone(execution.reconciliation ?? null),
380
377
  approval: clone(execution.approval),
381
378
  };
382
379
  return deepFreeze(routing);
@@ -58,7 +58,7 @@ export function createValidatedRun({
58
58
  passes: normalizePasses(passes),
59
59
  achievedProfile,
60
60
  assuranceReviewIds: [...assuranceReviewIds].sort(),
61
- terminalReconciliation: reconciliationContext?.reconciliation
61
+ controllerReconciliation: reconciliationContext?.reconciliation
62
62
  ? clone(reconciliationContext.reconciliation)
63
63
  : null,
64
64
  priorLedger: reconciliationContext?.priorLedger
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { realpathSync } from 'node:fs';
4
3
  import { readFile } from 'node:fs/promises';
5
- import { resolve } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
4
 
5
+ import { isDirectExecution } from './lib/cli-entry.mjs';
8
6
  import {
9
7
  checkApprovedWaveTarget,
10
8
  createRoutingPreview,
@@ -81,29 +79,7 @@ export async function main(argv = process.argv.slice(2)) {
81
79
  );
82
80
  }
83
81
 
84
- /**
85
- * Direct invocation, compared as canonical paths on both sides. Comparing a raw
86
- * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
87
- * that exits 0 whenever the skill is reached through a symlinked install root,
88
- * and canonicalizing only one side has the same effect under
89
- * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
90
- * A path that cannot be canonicalized is not a module Node loaded as the entry
91
- * point, so a thrown `realpathSync` means "not invoked directly" and returns
92
- * `false`; it never masks a direct run.
93
- */
94
- function isDirectInvocation(invokedPath) {
95
- if (!invokedPath) return false;
96
- try {
97
- return (
98
- realpathSync(fileURLToPath(import.meta.url)) ===
99
- realpathSync(resolve(invokedPath))
100
- );
101
- } catch {
102
- return false;
103
- }
104
- }
105
-
106
- if (isDirectInvocation(process.argv[1])) {
82
+ if (isDirectExecution(import.meta.url)) {
107
83
  main().catch((error) => {
108
84
  const code = error instanceof RoutingContractError ? error.code : 'ERROR';
109
85
  process.stderr.write(`${code}: ${error.message}\n`);