agentxchain 0.8.8 → 2.1.1

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.
Files changed (74) hide show
  1. package/README.md +126 -142
  2. package/bin/agentxchain.js +186 -5
  3. package/dashboard/app.js +305 -0
  4. package/dashboard/components/blocked.js +145 -0
  5. package/dashboard/components/cross-repo.js +126 -0
  6. package/dashboard/components/gate.js +311 -0
  7. package/dashboard/components/hooks.js +177 -0
  8. package/dashboard/components/initiative.js +147 -0
  9. package/dashboard/components/ledger.js +165 -0
  10. package/dashboard/components/timeline.js +222 -0
  11. package/dashboard/index.html +352 -0
  12. package/package.json +14 -6
  13. package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
  14. package/scripts/publish-from-tag.sh +88 -0
  15. package/scripts/release-postflight.sh +231 -0
  16. package/scripts/release-preflight.sh +167 -0
  17. package/src/commands/accept-turn.js +160 -0
  18. package/src/commands/approve-completion.js +80 -0
  19. package/src/commands/approve-transition.js +85 -0
  20. package/src/commands/dashboard.js +70 -0
  21. package/src/commands/init.js +516 -0
  22. package/src/commands/migrate.js +348 -0
  23. package/src/commands/multi.js +549 -0
  24. package/src/commands/plugin.js +157 -0
  25. package/src/commands/reject-turn.js +204 -0
  26. package/src/commands/resume.js +389 -0
  27. package/src/commands/status.js +196 -3
  28. package/src/commands/step.js +947 -0
  29. package/src/commands/template-list.js +33 -0
  30. package/src/commands/template-set.js +279 -0
  31. package/src/commands/validate.js +20 -11
  32. package/src/commands/verify.js +71 -0
  33. package/src/lib/adapters/api-proxy-adapter.js +1076 -0
  34. package/src/lib/adapters/local-cli-adapter.js +337 -0
  35. package/src/lib/adapters/manual-adapter.js +169 -0
  36. package/src/lib/blocked-state.js +94 -0
  37. package/src/lib/config.js +97 -1
  38. package/src/lib/context-compressor.js +121 -0
  39. package/src/lib/context-section-parser.js +220 -0
  40. package/src/lib/coordinator-acceptance.js +428 -0
  41. package/src/lib/coordinator-config.js +461 -0
  42. package/src/lib/coordinator-dispatch.js +276 -0
  43. package/src/lib/coordinator-gates.js +487 -0
  44. package/src/lib/coordinator-hooks.js +239 -0
  45. package/src/lib/coordinator-recovery.js +523 -0
  46. package/src/lib/coordinator-state.js +365 -0
  47. package/src/lib/cross-repo-context.js +247 -0
  48. package/src/lib/dashboard/bridge-server.js +284 -0
  49. package/src/lib/dashboard/file-watcher.js +93 -0
  50. package/src/lib/dashboard/state-reader.js +96 -0
  51. package/src/lib/dispatch-bundle.js +568 -0
  52. package/src/lib/dispatch-manifest.js +252 -0
  53. package/src/lib/gate-evaluator.js +285 -0
  54. package/src/lib/governed-state.js +2139 -0
  55. package/src/lib/governed-templates.js +145 -0
  56. package/src/lib/hook-runner.js +788 -0
  57. package/src/lib/normalized-config.js +539 -0
  58. package/src/lib/plugin-config-schema.js +192 -0
  59. package/src/lib/plugins.js +692 -0
  60. package/src/lib/protocol-conformance.js +291 -0
  61. package/src/lib/reference-conformance-adapter.js +717 -0
  62. package/src/lib/repo-observer.js +597 -0
  63. package/src/lib/repo.js +0 -31
  64. package/src/lib/schema.js +121 -0
  65. package/src/lib/schemas/turn-result.schema.json +205 -0
  66. package/src/lib/token-budget.js +206 -0
  67. package/src/lib/token-counter.js +27 -0
  68. package/src/lib/turn-paths.js +67 -0
  69. package/src/lib/turn-result-validator.js +496 -0
  70. package/src/lib/validation.js +137 -0
  71. package/src/templates/governed/api-service.json +31 -0
  72. package/src/templates/governed/cli-tool.json +30 -0
  73. package/src/templates/governed/generic.json +10 -0
  74. package/src/templates/governed/web-app.json +30 -0
@@ -0,0 +1,33 @@
1
+ import chalk from 'chalk';
2
+ import { loadAllGovernedTemplates, VALID_GOVERNED_TEMPLATE_IDS } from '../lib/governed-templates.js';
3
+
4
+ export function templateListCommand(opts) {
5
+ if (opts.json) {
6
+ const templates = loadAllGovernedTemplates();
7
+ const output = templates.map((t) => ({
8
+ id: t.id,
9
+ display_name: t.display_name,
10
+ description: t.description,
11
+ planning_artifacts: (t.planning_artifacts || []).map((a) => a.filename),
12
+ prompt_overrides: Object.keys(t.prompt_overrides || {}),
13
+ acceptance_hints: t.acceptance_hints || [],
14
+ }));
15
+ console.log(JSON.stringify(output, null, 2));
16
+ return;
17
+ }
18
+
19
+ console.log(chalk.bold('\n Available governed templates:\n'));
20
+ const templates = loadAllGovernedTemplates();
21
+ for (const t of templates) {
22
+ const artifacts = (t.planning_artifacts || []).map((a) => a.filename);
23
+ console.log(` ${chalk.cyan(t.id)} — ${t.description}`);
24
+ if (artifacts.length > 0) {
25
+ console.log(` Planning artifacts: ${artifacts.join(', ')}`);
26
+ }
27
+ if (t.prompt_overrides && Object.keys(t.prompt_overrides).length > 0) {
28
+ console.log(` Prompt overrides: ${Object.keys(t.prompt_overrides).join(', ')}`);
29
+ }
30
+ console.log('');
31
+ }
32
+ console.log(chalk.dim(` Usage: agentxchain template set <id>\n`));
33
+ }
@@ -0,0 +1,279 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { CONFIG_FILE } from '../lib/config.js';
5
+ import { loadGovernedTemplate, VALID_GOVERNED_TEMPLATE_IDS } from '../lib/governed-templates.js';
6
+
7
+ const LEDGER_PATH = '.agentxchain/decision-ledger.jsonl';
8
+ const PROMPT_OVERRIDE_SEPARATOR = '## Project-Type-Specific Guidance';
9
+ const ACCEPTANCE_HINTS_SEPARATOR = '## Template Guidance';
10
+
11
+ function interpolateTemplateContent(contentTemplate, projectName) {
12
+ return contentTemplate.replaceAll('{{project_name}}', projectName);
13
+ }
14
+
15
+ function appendJsonl(root, relPath, entry) {
16
+ const filePath = join(root, relPath);
17
+ mkdirSync(dirname(filePath), { recursive: true });
18
+ const line = JSON.stringify(entry) + '\n';
19
+ writeFileSync(filePath, line, { flag: 'a' });
20
+ }
21
+
22
+ export async function templateSetCommand(templateId, opts) {
23
+ const root = process.cwd();
24
+ const governedWorkspacePath = join(root, '.agentxchain');
25
+
26
+ // ── Validate governed project ──────────────────────────────────────────
27
+ const configPath = join(root, CONFIG_FILE);
28
+ if (!existsSync(configPath)) {
29
+ console.error(chalk.red(' Error: No agentxchain.json found.'));
30
+ process.exit(1);
31
+ }
32
+
33
+ let config;
34
+ try {
35
+ config = JSON.parse(readFileSync(configPath, 'utf8'));
36
+ } catch (err) {
37
+ console.error(chalk.red(` Error: Failed to read agentxchain.json: ${err.message}`));
38
+ process.exit(1);
39
+ }
40
+
41
+ if (config.schema_version !== '1.0' && config.schema_version !== '1.1') {
42
+ console.error(chalk.red(' Error: This is not a governed project. template set requires a governed project (schema_version 1.0 or 1.1).'));
43
+ process.exit(1);
44
+ }
45
+
46
+ if (!existsSync(governedWorkspacePath)) {
47
+ console.error(chalk.red(' Error: Governed workspace missing. template set requires an existing .agentxchain/ directory.'));
48
+ process.exit(1);
49
+ }
50
+
51
+ // ── Validate template ID ──────────────────────────────────────────────
52
+ if (!VALID_GOVERNED_TEMPLATE_IDS.includes(templateId)) {
53
+ console.error(chalk.red(` Error: Unknown template "${templateId}".`));
54
+ console.error(` Available templates: ${VALID_GOVERNED_TEMPLATE_IDS.join(', ')}`);
55
+ process.exit(1);
56
+ }
57
+
58
+ // ── Same template = no-op ─────────────────────────────────────────────
59
+ const previousTemplate = config.template || 'generic';
60
+ if (previousTemplate === templateId) {
61
+ console.log(chalk.green(` Already set to "${templateId}". No changes.`));
62
+ process.exit(0);
63
+ }
64
+
65
+ // ── Load manifest ─────────────────────────────────────────────────────
66
+ const manifest = loadGovernedTemplate(templateId);
67
+ const projectName = config.project?.name || 'Untitled';
68
+
69
+ // ── Build mutation plan ───────────────────────────────────────────────
70
+ const plan = {
71
+ config_update: true,
72
+ files_created: [],
73
+ files_skipped: [],
74
+ prompts_appended: [],
75
+ prompts_existing_guidance: [],
76
+ prompts_missing_paths: [],
77
+ prompts_missing_files: [],
78
+ acceptance_hints_status: 'none',
79
+ };
80
+
81
+ // Planning artifacts
82
+ for (const artifact of manifest.planning_artifacts || []) {
83
+ const artifactPath = join(root, '.planning', artifact.filename);
84
+ if (existsSync(artifactPath)) {
85
+ plan.files_skipped.push(artifact.filename);
86
+ } else {
87
+ plan.files_created.push(artifact.filename);
88
+ }
89
+ }
90
+
91
+ // Prompt overrides
92
+ const promptOverrides = manifest.prompt_overrides || {};
93
+ for (const roleId of Object.keys(promptOverrides)) {
94
+ const promptRelPath = config.prompts?.[roleId];
95
+ if (!promptRelPath) {
96
+ plan.prompts_missing_paths.push(roleId);
97
+ continue;
98
+ }
99
+ const promptPath = join(root, promptRelPath);
100
+ if (!existsSync(promptPath)) {
101
+ plan.prompts_missing_files.push({
102
+ role_id: roleId,
103
+ path: promptRelPath,
104
+ });
105
+ continue;
106
+ }
107
+ const content = readFileSync(promptPath, 'utf8');
108
+ if (content.includes(PROMPT_OVERRIDE_SEPARATOR)) {
109
+ plan.prompts_existing_guidance.push(roleId);
110
+ } else {
111
+ plan.prompts_appended.push(roleId);
112
+ }
113
+ }
114
+
115
+ // Acceptance hints
116
+ const acceptanceMatrixPath = join(root, '.planning', 'acceptance-matrix.md');
117
+ if (Array.isArray(manifest.acceptance_hints) && manifest.acceptance_hints.length > 0) {
118
+ if (existsSync(acceptanceMatrixPath)) {
119
+ const matrixContent = readFileSync(acceptanceMatrixPath, 'utf8');
120
+ if (matrixContent.includes(ACCEPTANCE_HINTS_SEPARATOR)) {
121
+ plan.acceptance_hints_status = 'existing_guidance';
122
+ } else {
123
+ plan.acceptance_hints_status = 'append';
124
+ }
125
+ } else {
126
+ plan.acceptance_hints_status = 'missing_file';
127
+ }
128
+ }
129
+
130
+ // ── Dry run: print plan and exit ──────────────────────────────────────
131
+ if (opts.dryRun) {
132
+ console.log(chalk.bold(`\n Template: ${previousTemplate} → ${templateId}\n`));
133
+ console.log(' Config:');
134
+ console.log(` agentxchain.json: template field will be updated`);
135
+ console.log('\n Planning artifacts:');
136
+ for (const f of plan.files_created) {
137
+ console.log(` .planning/${f}: ${chalk.green('WILL CREATE')}`);
138
+ }
139
+ for (const f of plan.files_skipped) {
140
+ console.log(` .planning/${f}: ${chalk.dim('EXISTS (skip)')}`);
141
+ }
142
+ if (plan.files_created.length === 0 && plan.files_skipped.length === 0) {
143
+ console.log(` ${chalk.dim('(none)')}`);
144
+ }
145
+ console.log('\n Prompts:');
146
+ for (const r of plan.prompts_appended) {
147
+ const p = config.prompts?.[r] || `<unknown path for ${r}>`;
148
+ console.log(` ${p}: ${chalk.green('WILL APPEND override')}`);
149
+ }
150
+ for (const r of plan.prompts_existing_guidance) {
151
+ const p = config.prompts?.[r] || `<unknown path for ${r}>`;
152
+ console.log(` ${p}: ${chalk.dim('ALREADY HAS guidance (skip)')}`);
153
+ }
154
+ for (const r of plan.prompts_missing_paths) {
155
+ console.log(` <no configured path for ${r}>: ${chalk.yellow('NO PROMPT PATH (skip)')}`);
156
+ }
157
+ for (const prompt of plan.prompts_missing_files) {
158
+ console.log(` ${prompt.path}: ${chalk.yellow('MISSING FILE (skip)')}`);
159
+ }
160
+ if (
161
+ plan.prompts_appended.length === 0
162
+ && plan.prompts_existing_guidance.length === 0
163
+ && plan.prompts_missing_paths.length === 0
164
+ && plan.prompts_missing_files.length === 0
165
+ ) {
166
+ console.log(` ${chalk.dim('(none)')}`);
167
+ }
168
+ console.log('\n Acceptance hints:');
169
+ if (plan.acceptance_hints_status === 'append') {
170
+ console.log(` .planning/acceptance-matrix.md: ${chalk.green('WILL APPEND template guidance')}`);
171
+ } else if (plan.acceptance_hints_status === 'existing_guidance') {
172
+ console.log(` .planning/acceptance-matrix.md: ${chalk.dim('ALREADY HAS guidance (skip)')}`);
173
+ } else if (plan.acceptance_hints_status === 'missing_file') {
174
+ console.log(` .planning/acceptance-matrix.md: ${chalk.yellow('MISSING FILE (skip)')}`);
175
+ } else {
176
+ console.log(` ${chalk.dim('(none)')}`);
177
+ }
178
+ console.log(chalk.dim('\n No changes written. Use without --dry-run to apply.\n'));
179
+ process.exit(0);
180
+ }
181
+
182
+ // ── Confirmation prompt ───────────────────────────────────────────────
183
+ if (!opts.yes) {
184
+ console.log(chalk.bold(`\n Template: ${previousTemplate} → ${templateId}`));
185
+ console.log(` ${plan.files_created.length} files to create, ${plan.prompts_appended.length} prompts to append.\n`);
186
+ const readline = await import('node:readline');
187
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
188
+ const answer = await new Promise((resolve) => {
189
+ rl.question(' Apply changes? [y/N] ', resolve);
190
+ });
191
+ rl.close();
192
+ if (answer.trim().toLowerCase() !== 'y') {
193
+ console.log(chalk.dim(' Aborted.'));
194
+ process.exit(0);
195
+ }
196
+ }
197
+
198
+ // ── Execute mutations ─────────────────────────────────────────────────
199
+
200
+ // 1. Update config
201
+ config.template = templateId;
202
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
203
+
204
+ // 2. Create planning artifacts
205
+ mkdirSync(join(root, '.planning'), { recursive: true });
206
+ for (const artifact of manifest.planning_artifacts || []) {
207
+ const artifactPath = join(root, '.planning', artifact.filename);
208
+ if (!existsSync(artifactPath)) {
209
+ writeFileSync(
210
+ artifactPath,
211
+ interpolateTemplateContent(artifact.content_template, projectName)
212
+ );
213
+ }
214
+ }
215
+
216
+ // 3. Append prompt overrides
217
+ for (const roleId of plan.prompts_appended) {
218
+ const promptPath = join(root, config.prompts[roleId]);
219
+ const content = readFileSync(promptPath, 'utf8');
220
+ const override = promptOverrides[roleId];
221
+ const appended = `${content}\n\n---\n\n${PROMPT_OVERRIDE_SEPARATOR}\n\n${override.trim()}\n`;
222
+ writeFileSync(promptPath, appended);
223
+ }
224
+ // Warn about skipped prompts when switching templates
225
+ for (const roleId of plan.prompts_existing_guidance) {
226
+ console.log(chalk.yellow(` Warning: Prompt for ${roleId} already has project-type guidance. Skipping. Edit manually if you want the new template's guidance.`));
227
+ }
228
+ for (const roleId of plan.prompts_missing_paths) {
229
+ console.log(chalk.yellow(` Warning: Prompt for ${roleId} has no configured path in agentxchain.json. Skipping template guidance.`));
230
+ }
231
+ for (const prompt of plan.prompts_missing_files) {
232
+ console.log(chalk.yellow(` Warning: Prompt file for ${prompt.role_id} not found at ${prompt.path}. Skipping template guidance.`));
233
+ }
234
+
235
+ // 4. Append acceptance hints
236
+ if (plan.acceptance_hints_status === 'append' && existsSync(acceptanceMatrixPath)) {
237
+ const matrixContent = readFileSync(acceptanceMatrixPath, 'utf8');
238
+ const hintLines = manifest.acceptance_hints.map((hint) => `- [ ] ${hint}`).join('\n');
239
+ const appended = `${matrixContent}\n\n${ACCEPTANCE_HINTS_SEPARATOR}\n\n${hintLines}\n`;
240
+ writeFileSync(acceptanceMatrixPath, appended);
241
+ } else if (plan.acceptance_hints_status === 'missing_file') {
242
+ console.log(chalk.yellow(' Warning: .planning/acceptance-matrix.md not found. Skipping template guidance hints.'));
243
+ }
244
+
245
+ // 5. Decision ledger
246
+ const ledgerEntry = {
247
+ type: 'template_set',
248
+ timestamp: new Date().toISOString(),
249
+ previous_template: previousTemplate,
250
+ new_template: templateId,
251
+ files_created: plan.files_created,
252
+ files_skipped: plan.files_skipped,
253
+ prompts_appended: plan.prompts_appended,
254
+ prompts_skipped: [
255
+ ...plan.prompts_existing_guidance,
256
+ ...plan.prompts_missing_paths,
257
+ ...plan.prompts_missing_files.map((prompt) => prompt.role_id),
258
+ ],
259
+ prompt_missing_paths: plan.prompts_missing_paths,
260
+ prompt_missing_files: plan.prompts_missing_files,
261
+ acceptance_hints_appended: plan.acceptance_hints_status === 'append',
262
+ acceptance_hints_skipped_reason: plan.acceptance_hints_status === 'append' ? null : plan.acceptance_hints_status,
263
+ operator: 'human',
264
+ };
265
+ appendJsonl(root, LEDGER_PATH, ledgerEntry);
266
+
267
+ // ── Summary ───────────────────────────────────────────────────────────
268
+ console.log(chalk.green(`\n Template set to "${templateId}".`));
269
+ if (plan.files_created.length > 0) {
270
+ console.log(` Created: ${plan.files_created.map(f => `.planning/${f}`).join(', ')}`);
271
+ }
272
+ if (plan.prompts_appended.length > 0) {
273
+ console.log(` Appended guidance to: ${plan.prompts_appended.join(', ')} prompts`);
274
+ }
275
+ if (plan.acceptance_hints_status === 'append') {
276
+ console.log(` Appended template guidance to acceptance-matrix.md`);
277
+ }
278
+ console.log('');
279
+ }
@@ -1,28 +1,37 @@
1
1
  import chalk from 'chalk';
2
- import { loadConfig } from '../lib/config.js';
3
- import { validateProject } from '../lib/validation.js';
2
+ import { loadConfig, loadProjectContext } from '../lib/config.js';
3
+ import { validateGovernedProject, validateProject } from '../lib/validation.js';
4
4
 
5
5
  export async function validateCommand(opts) {
6
- const result = loadConfig();
7
- if (!result) {
6
+ const context = loadProjectContext();
7
+ if (!context) {
8
8
  console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
9
9
  process.exit(1);
10
10
  }
11
11
 
12
- const { root, config } = result;
13
12
  const mode = opts.mode || 'full';
14
- const validation = validateProject(root, config, {
15
- mode,
16
- expectedAgent: opts.agent || null
17
- });
13
+ const validation = context.config.protocol_mode === 'governed'
14
+ ? validateGovernedProject(context.root, context.rawConfig, context.config, {
15
+ mode,
16
+ expectedAgent: opts.agent || null,
17
+ })
18
+ : validateProject(context.root, loadConfig()?.config || context.rawConfig, {
19
+ mode,
20
+ expectedAgent: opts.agent || null,
21
+ });
18
22
 
19
23
  if (opts.json) {
20
- console.log(JSON.stringify(validation, null, 2));
24
+ console.log(JSON.stringify({
25
+ ...validation,
26
+ protocol_mode: context.config.protocol_mode,
27
+ version: context.version,
28
+ }, null, 2));
21
29
  } else {
22
30
  console.log('');
23
31
  console.log(chalk.bold(` AgentXchain Validate (${mode})`));
24
32
  console.log(chalk.dim(' ' + '─'.repeat(44)));
25
- console.log(chalk.dim(` Root: ${root}`));
33
+ console.log(chalk.dim(` Root: ${context.root}`));
34
+ console.log(chalk.dim(` Protocol: ${context.config.protocol_mode} (v${context.version})`));
26
35
  console.log('');
27
36
 
28
37
  if (validation.ok) {
@@ -0,0 +1,71 @@
1
+ import chalk from 'chalk';
2
+ import { resolve } from 'node:path';
3
+ import { verifyProtocolConformance } from '../lib/protocol-conformance.js';
4
+
5
+ export async function verifyProtocolCommand(opts) {
6
+ const target = opts.target ? resolve(opts.target) : process.cwd();
7
+ const requestedTier = Number.parseInt(String(opts.tier || '1'), 10);
8
+ const format = opts.format || 'text';
9
+
10
+ let result;
11
+ try {
12
+ result = verifyProtocolConformance({
13
+ targetRoot: target,
14
+ requestedTier,
15
+ surface: opts.surface || null,
16
+ });
17
+ } catch (error) {
18
+ if (format === 'json') {
19
+ console.log(JSON.stringify({
20
+ overall: 'error',
21
+ message: error.message,
22
+ }, null, 2));
23
+ } else {
24
+ console.log(chalk.red(`Protocol verification failed: ${error.message}`));
25
+ }
26
+ process.exit(2);
27
+ }
28
+
29
+ if (format === 'json') {
30
+ console.log(JSON.stringify(result.report, null, 2));
31
+ } else {
32
+ printProtocolReport(result.report);
33
+ }
34
+
35
+ process.exit(result.exitCode);
36
+ }
37
+
38
+ function printProtocolReport(report) {
39
+ console.log('');
40
+ console.log(chalk.bold(' AgentXchain Protocol Conformance'));
41
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
42
+ console.log(chalk.dim(` Target: ${report.target_root}`));
43
+ console.log(chalk.dim(` Implementation: ${report.implementation}`));
44
+ console.log(chalk.dim(` Tier requested: ${report.tier_requested}`));
45
+ console.log('');
46
+
47
+ const overallLabel = report.overall === 'pass'
48
+ ? chalk.green('PASS')
49
+ : report.overall === 'fail'
50
+ ? chalk.red('FAIL')
51
+ : chalk.red('ERROR');
52
+ console.log(` Overall: ${overallLabel}`);
53
+
54
+ for (const [tierKey, tier] of Object.entries(report.results)) {
55
+ const label = tier.status === 'pass'
56
+ ? chalk.green('pass')
57
+ : tier.status === 'skipped'
58
+ ? chalk.yellow('skipped')
59
+ : chalk.red(tier.status);
60
+ console.log(` ${tierKey}: ${label} (${tier.fixtures_passed}/${tier.fixtures_run} passed)`);
61
+
62
+ for (const failure of tier.failures || []) {
63
+ console.log(chalk.red(` - ${failure.fixture_id}: ${failure.message}`));
64
+ }
65
+ for (const error of tier.errors || []) {
66
+ console.log(chalk.red(` - ${error.fixture_id}: ${error.message}`));
67
+ }
68
+ }
69
+
70
+ console.log('');
71
+ }