@xelth/eck-snapshot 4.0.0 → 4.2.0

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.
@@ -39,131 +39,201 @@ function buildAgentDefinitions(executionAgents) {
39
39
  return definitions;
40
40
  }
41
41
 
42
- function buildEckManifestSection(eckManifest) {
43
- if (!eckManifest) {
44
- return '';
45
- }
46
-
47
- let section = '\n## Project-Specific Manifest (.eck Directory)\n\n';
48
- section += 'This project includes a `.eck` directory with specific context and configuration:\n\n';
49
-
50
- if (eckManifest.context) {
51
- section += '### Project Context\n\n';
52
- section += eckManifest.context + '\n\n';
42
+ /**
43
+ * Parse journal entries from JOURNAL.md content
44
+ * @param {string} journalContent - Raw content of JOURNAL.md
45
+ * @returns {Array} Array of parsed journal entries
46
+ */
47
+ function parseJournalEntries(journalContent) {
48
+ if (!journalContent || typeof journalContent !== 'string') {
49
+ return [];
53
50
  }
54
51
 
55
- if (eckManifest.operations) {
56
- section += '### Operations Guide\n\n';
57
- section += eckManifest.operations + '\n\n';
52
+ // Split by --- separators, filter empty blocks
53
+ const blocks = journalContent.split(/^---$/m).filter(b => b.trim());
54
+ const entries = [];
55
+
56
+ for (let i = 0; i < blocks.length; i += 2) {
57
+ const frontmatter = blocks[i];
58
+ const body = blocks[i + 1] || '';
59
+
60
+ // Parse frontmatter
61
+ const typeMatch = frontmatter.match(/^type:\s*(.+)$/m);
62
+ const scopeMatch = frontmatter.match(/^scope:\s*(.+)$/m);
63
+ const summaryMatch = frontmatter.match(/^summary:\s*(.+)$/m);
64
+ const dateMatch = frontmatter.match(/^(?:date|timestamp):\s*(.+)$/m);
65
+ const taskIdMatch = frontmatter.match(/^task_id:\s*(.+)$/m);
66
+
67
+ // Extract title from body (first # heading)
68
+ const titleMatch = body.match(/^#\s+(.+)$/m);
69
+
70
+ entries.push({
71
+ type: typeMatch ? typeMatch[1].trim() : 'unknown',
72
+ scope: scopeMatch ? scopeMatch[1].trim() : '',
73
+ summary: summaryMatch ? summaryMatch[1].trim() : (titleMatch ? titleMatch[1].trim() : ''),
74
+ date: dateMatch ? dateMatch[1].trim() : '',
75
+ taskId: taskIdMatch ? taskIdMatch[1].trim() : '',
76
+ body: body.trim()
77
+ });
58
78
  }
59
79
 
60
- if (eckManifest.journal) {
61
- section += '### Development Journal\n\n';
62
- section += eckManifest.journal + '\n\n';
80
+ return entries;
81
+ }
82
+
83
+ /**
84
+ * Build a compact journal summary for the architect
85
+ * Shows: last entry (full) + 5 previous (headers only) + total count
86
+ */
87
+ function buildJournalSummary(journalContent) {
88
+ const entries = parseJournalEntries(journalContent);
89
+
90
+ if (entries.length === 0) {
91
+ return 'No journal entries found.';
92
+ }
93
+
94
+ let summary = '';
95
+
96
+ // Last entry - show full details
97
+ const lastEntry = entries[0];
98
+ summary += `**Latest Entry** (${lastEntry.date || 'no date'}):\n`;
99
+ summary += `- Type: \`${lastEntry.type}\` | Scope: \`${lastEntry.scope}\`\n`;
100
+ summary += `- ${lastEntry.summary}\n`;
101
+ if (lastEntry.body) {
102
+ // Include body but limit to first 3 lines
103
+ const bodyLines = lastEntry.body.split('\n').filter(l => l.trim()).slice(0, 4);
104
+ summary += bodyLines.map(l => ` ${l}`).join('\n') + '\n';
63
105
  }
64
106
 
65
- if (Object.keys(eckManifest.environment).length > 0) {
66
- section += '### Environment Overrides\n\n';
67
- section += 'The following environment settings override auto-detected values:\n\n';
68
- for (const [key, value] of Object.entries(eckManifest.environment)) {
69
- section += `- **${key}**: ${value}\n`;
107
+ // Previous 5 entries - headers only
108
+ if (entries.length > 1) {
109
+ summary += '\n**Previous entries:**\n';
110
+ const previousEntries = entries.slice(1, 6);
111
+ for (const entry of previousEntries) {
112
+ summary += `- \`${entry.type}(${entry.scope})\`: ${entry.summary}\n`;
70
113
  }
71
- section += '\n';
72
114
  }
73
115
 
74
- section += '**Important**: Use this manifest information when formulating technical plans and briefing execution agents. The context, operations guide, and journal provide crucial project-specific knowledge that should inform your decisions.\n\n';
116
+ // Total count
117
+ if (entries.length > 6) {
118
+ summary += `\n*...and ${entries.length - 6} more entries in .eck/JOURNAL.md*\n`;
119
+ }
120
+
121
+ return summary;
122
+ }
123
+
124
+ function buildEckManifestSection(eckManifest) {
125
+ if (!eckManifest) {
126
+ return '';
127
+ }
128
+
129
+ let section = '\n## Project Context (.eck Directory)\n\n';
130
+ section += 'This project has a `.eck/` directory with project-specific context files.\n';
131
+ section += 'The coder agent can read these files when needed. Available files:\n\n';
132
+ section += '- `CONTEXT.md` - Project overview and architecture\n';
133
+ section += '- `OPERATIONS.md` - Common commands and workflows\n';
134
+ section += '- `JOURNAL.md` - Development history\n';
135
+ section += '- `ROADMAP.md` - Planned features\n';
136
+ section += '- `TECH_DEBT.md` - Known issues and refactoring needs\n';
137
+ section += '- `ENVIRONMENT.md` - Environment-specific settings\n\n';
138
+
139
+ // Add journal summary (compact view for architect)
140
+ if (eckManifest.journal) {
141
+ section += '### Recent Development Activity\n\n';
142
+ section += buildJournalSummary(eckManifest.journal) + '\n';
143
+ }
144
+
75
145
  section += '---\n\n';
76
146
 
77
- return section;
78
- }
79
-
80
- function extractMeaningfulLine(block) {
81
- if (!block || typeof block !== 'string') {
82
- return null;
83
- }
84
-
85
- const lines = block.split(/\r?\n/);
86
- for (const line of lines) {
87
- const trimmed = line.trim();
88
- if (!trimmed || trimmed.startsWith('#')) {
89
- continue;
90
- }
91
- const withoutBullet = trimmed.replace(/^[-*]\s*/, '').trim();
92
- if (withoutBullet) {
93
- return withoutBullet.replace(/\s+/g, ' ');
94
- }
95
- }
96
- return null;
97
- }
98
-
99
- function extractDescriptionFromManifest(eckManifest) {
100
- if (!eckManifest) {
101
- return null;
102
- }
103
-
104
- if (typeof eckManifest.description === 'string' && eckManifest.description.trim()) {
105
- return eckManifest.description.trim();
106
- }
107
-
108
- if (eckManifest.project && typeof eckManifest.project.description === 'string' && eckManifest.project.description.trim()) {
109
- return eckManifest.project.description.trim();
110
- }
111
-
112
- if (typeof eckManifest.context === 'string' && eckManifest.context.trim()) {
113
- const sectionMatch = eckManifest.context.match(/##\s*Description\s*([\s\S]*?)(?=^##\s|^#\s|\Z)/im);
114
- if (sectionMatch && sectionMatch[1]) {
115
- const meaningful = extractMeaningfulLine(sectionMatch[1]);
116
- if (meaningful) {
117
- return meaningful;
118
- }
119
- }
120
-
121
- const fallback = extractMeaningfulLine(eckManifest.context);
122
- if (fallback) {
123
- return fallback;
124
- }
125
- }
126
-
127
- return null;
128
- }
129
-
130
- async function resolveProjectDescription(context) {
131
- const defaultDescription = 'Project description not provided.';
132
-
133
- const manifestDescription = extractDescriptionFromManifest(context.eckManifest);
134
- if (manifestDescription) {
135
- const normalized = manifestDescription.trim();
136
- const genericPatterns = [
137
- /^brief description of what this project does/i,
138
- /^no project context provided/i
139
- ];
140
- const isGeneric = genericPatterns.some(pattern => pattern.test(normalized));
141
- if (!isGeneric) {
142
- return normalized;
143
- }
144
- }
145
-
146
- if (context.repoPath) {
147
- try {
148
- const packageJsonPath = path.join(context.repoPath, 'package.json');
149
- const pkgRaw = await fs.readFile(packageJsonPath, 'utf-8');
150
- const pkg = JSON.parse(pkgRaw);
151
- if (typeof pkg.description === 'string' && pkg.description.trim()) {
152
- return pkg.description.trim();
153
- }
154
- } catch (error) {
155
- // Ignore errors - package.json may not exist or be readable
156
- }
157
- }
158
-
159
- return defaultDescription;
160
- }
161
-
162
- export async function generateEnhancedAIHeader(context, isGitRepo = false) {
163
- try {
164
- const setupConfig = await loadSetupConfig();
165
- const { aiInstructions } = setupConfig;
166
- const { architectPersona, executionAgents, promptTemplates } = aiInstructions;
147
+ return section;
148
+ }
149
+
150
+ function extractMeaningfulLine(block) {
151
+ if (!block || typeof block !== 'string') {
152
+ return null;
153
+ }
154
+
155
+ const lines = block.split(/\r?\n/);
156
+ for (const line of lines) {
157
+ const trimmed = line.trim();
158
+ if (!trimmed || trimmed.startsWith('#')) {
159
+ continue;
160
+ }
161
+ const withoutBullet = trimmed.replace(/^[-*]\s*/, '').trim();
162
+ if (withoutBullet) {
163
+ return withoutBullet.replace(/\s+/g, ' ');
164
+ }
165
+ }
166
+ return null;
167
+ }
168
+
169
+ function extractDescriptionFromManifest(eckManifest) {
170
+ if (!eckManifest) {
171
+ return null;
172
+ }
173
+
174
+ if (typeof eckManifest.description === 'string' && eckManifest.description.trim()) {
175
+ return eckManifest.description.trim();
176
+ }
177
+
178
+ if (eckManifest.project && typeof eckManifest.project.description === 'string' && eckManifest.project.description.trim()) {
179
+ return eckManifest.project.description.trim();
180
+ }
181
+
182
+ if (typeof eckManifest.context === 'string' && eckManifest.context.trim()) {
183
+ const sectionMatch = eckManifest.context.match(/##\s*Description\s*([\s\S]*?)(?=^##\s|^#\s|\Z)/im);
184
+ if (sectionMatch && sectionMatch[1]) {
185
+ const meaningful = extractMeaningfulLine(sectionMatch[1]);
186
+ if (meaningful) {
187
+ return meaningful;
188
+ }
189
+ }
190
+
191
+ const fallback = extractMeaningfulLine(eckManifest.context);
192
+ if (fallback) {
193
+ return fallback;
194
+ }
195
+ }
196
+
197
+ return null;
198
+ }
199
+
200
+ async function resolveProjectDescription(context) {
201
+ const defaultDescription = 'Project description not provided.';
202
+
203
+ const manifestDescription = extractDescriptionFromManifest(context.eckManifest);
204
+ if (manifestDescription) {
205
+ const normalized = manifestDescription.trim();
206
+ const genericPatterns = [
207
+ /^brief description of what this project does/i,
208
+ /^no project context provided/i
209
+ ];
210
+ const isGeneric = genericPatterns.some(pattern => pattern.test(normalized));
211
+ if (!isGeneric) {
212
+ return normalized;
213
+ }
214
+ }
215
+
216
+ if (context.repoPath) {
217
+ try {
218
+ const packageJsonPath = path.join(context.repoPath, 'package.json');
219
+ const pkgRaw = await fs.readFile(packageJsonPath, 'utf-8');
220
+ const pkg = JSON.parse(pkgRaw);
221
+ if (typeof pkg.description === 'string' && pkg.description.trim()) {
222
+ return pkg.description.trim();
223
+ }
224
+ } catch (error) {
225
+ // Ignore errors - package.json may not exist or be readable
226
+ }
227
+ }
228
+
229
+ return defaultDescription;
230
+ }
231
+
232
+ export async function generateEnhancedAIHeader(context, isGitRepo = false) {
233
+ try {
234
+ const setupConfig = await loadSetupConfig();
235
+ const { aiInstructions } = setupConfig;
236
+ const { architectPersona, executionAgents, promptTemplates } = aiInstructions;
167
237
 
168
238
  // Helper function to read a template file or return the string if it's not a path
169
239
  const loadTemplate = async (templatePathOrString) => {
@@ -205,11 +275,11 @@ export async function generateEnhancedAIHeader(context, isGitRepo = false) {
205
275
  }
206
276
 
207
277
  // --- Build common context sections ---
208
- const projectDescription = await resolveProjectDescription(context);
209
- const projectOverview = `### PROJECT OVERVIEW
210
- - **Project:** ${context.repoName || 'Unknown'}
211
- - **Description:** ${projectDescription}
212
- `;
278
+ const projectDescription = await resolveProjectDescription(context);
279
+ const projectOverview = `### PROJECT OVERVIEW
280
+ - **Project:** ${context.repoName || 'Unknown'}
281
+ - **Description:** ${projectDescription}
282
+ `;
213
283
  const normalizedEck = normalizeManifest(context.eckManifest);
214
284
  let eckManifestSection = '';
215
285
  if (normalizedEck) {
@@ -243,49 +313,205 @@ ${eckManifestSection}
243
313
  return agentHeader;
244
314
  }
245
315
 
246
- // --- This is the main/Senior Architect prompt logic ---
247
- let template;
248
- if (context.mode === 'vector') {
249
- template = await loadTemplate(promptTemplates.vectorMode);
250
- // Inject context for vector mode
251
- template = template.replace('{{multiAgentSection}}', `
252
- ${projectOverview}
253
- ${eckManifestSection}
254
- `);
316
+ // --- Determine Workflow Content based on JA Flag ---
317
+ const withJa = context.options && context.options.withJa;
318
+ let hierarchicalWorkflow = '';
319
+ let commandFormats = '';
320
+
321
+ if (withJa) {
322
+ hierarchicalWorkflow = `### HIERARCHICAL AGENT WORKFLOW
323
+
324
+ Your primary role is **Senior Architect**. You formulate high-level strategy. For complex code implementation, you will delegate to a **Junior Architect** agent (\`gemini_wsl\`), who has a detailed (\`_ja.md\`) snapshot and the ability to command a **Coder** agent (\`claude\`).
325
+
326
+ - **Senior Architect (You):** Sets strategy, defines high-level tasks.
327
+ - **Junior Architect (\`gemini_wsl\`):** Receives strategic tasks, analyzes the \`_ja.md\` snapshot, breaks the task down, and commands the Coder.
328
+ - **Coder (\`claude\`):** Receives small, precise coding tasks from the Junior Architect. **Claude is responsible for keeping the .eck/ manifest files accurate and synchronized with the code.**`;
329
+
330
+ commandFormats = `### COMMAND FORMATS
331
+
332
+ You MUST use one of two JSON command formats based on your target:
333
+
334
+ **1. For Coders (\`local_dev\`, \`production_server\`, \`android_wsl_dev\`, \`gemini_windows\`) - LOW-LEVEL EXECUTION:**
335
+ Use \`apply_code_changes\` for simple, direct tasks where you provide all details.
336
+
337
+ \`\`\`json
338
+ {
339
+ "target_agent": "local_dev",
340
+ "agent_environment": "Development environment with full GUI support and development tools",
341
+ "command_for_agent": "apply_code_changes",
342
+ "task_id": "unique-task-id",
343
+ "payload": {
344
+ "objective": "Brief, clear task description",
345
+ "context": "Why this change is needed - include relevant .eck manifest context",
346
+ "files_to_modify": [
347
+ {
348
+ "path": "exact/file/path.js",
349
+ "action": "specific action (add, modify, replace, delete)",
350
+ "location": "line numbers, function name, or search pattern",
351
+ "details": "precise description of the change"
352
+ }
353
+ ],
354
+ "new_files": [
355
+ {
356
+ "path": "path/to/new/file.js",
357
+ "content_type": "javascript/json/markdown/config",
358
+ "purpose": "why this file is needed"
359
+ }
360
+ ],
361
+ "dependencies": {
362
+ "install": ["package-name@version"],
363
+ "remove": ["old-package-name"]
364
+ },
365
+ "validation_steps": [
366
+ "npm run test",
367
+ "node index.js --help",
368
+ "specific command to verify functionality"
369
+ ],
370
+ "expected_outcome": "what should work after changes",
371
+ "post_execution_steps": {
372
+ "journal_entry": {
373
+ "type": "feat",
374
+ "scope": "authentication",
375
+ "summary": "Brief description of what was accomplished",
376
+ "details": "Detailed explanation of changes, impacts, and technical notes"
377
+ },
378
+ "mcp_feedback": {
379
+ "success": true,
380
+ "errors": [],
381
+ "mcp_version": "1.0"
382
+ }
383
+ }
384
+ }
385
+ }
386
+ \`\`\`
387
+
388
+ **2. For Junior Architects (\`gemini_wsl\`) - HIGH-LEVEL DELEGATION:**
389
+ Use \`execute_strategic_task\` for complex features. The JA will use its own snapshot and Coder agent to complete the task.
390
+
391
+ \`\`\`json
392
+ {
393
+ "target_agent": "gemini_wsl",
394
+ "command_for_agent": "execute_strategic_task",
395
+ "payload": {
396
+ "objective": "Implement the user authentication feature",
397
+ "context": "This is a high-level task. Use your _ja.md snapshot to analyze the codebase. Use your 'claude (delegate)' capability to implement the necessary code across all required files (routes, controllers, services).",
398
+ "constraints": [
399
+ "Must use JWT for tokens",
400
+ "Add new routes to \`routes/api.js\`",
401
+ "Ensure all new code is covered by tests"
402
+ ],
403
+ "validation_steps": [
404
+ "npm run test"
405
+ ]
406
+ }
407
+ }
408
+ \`\`\``;
255
409
  } else {
256
- template = await loadTemplate(promptTemplates.multiAgent);
257
- // --- INJECT DYNAMIC CONTEXT ---
258
- template = template.replace('{{projectOverview}}', projectOverview);
259
- template = template.replace('{{eckManifestSection}}', eckManifestSection);
260
- // --- END INJECT ---
410
+ hierarchicalWorkflow = `### AGENT WORKFLOW
411
+
412
+ Your role is **Architect**. You formulate technical plans and delegate code implementation tasks directly to the **Coder** agents.
413
+
414
+ **Your secondary duty is DOCUMENTATION INTEGRITY.** You must ensure the Coder updates .eck/ files whenever the project structure, roadmap, or debt changes.
415
+
416
+ - **Architect (You):** Sets strategy, defines tasks, enforces manifest maintenance.
417
+ - **Coder (e.g., \`local_dev\`):** Receives precise coding tasks and executes them, including manifest updates.`;
418
+
419
+ commandFormats = `### COMMAND FORMATS (Eck-Protocol v2)
420
+
421
+ You MUST use the **Eck-Protocol v2** format for all code execution tasks. This format combines Markdown for analysis, XML tags for file operations, and JSON for routing metadata.
422
+
423
+ **CRITICAL DISPLAY RULE (THE 4-BACKTICK WRAPPER):**
424
+ To ensure your command is copy-pasteable without breaking UI rendering, you **MUST** wrap the ENTIRE protocol output in a \`text\` block using **QUADRUPLE BACKTICKS** (\` \`\`\`\` \`).
425
+
426
+ **Why?** Your command contains internal code blocks with 3 backticks. To escape them, the outer container needs 4.
427
+
428
+ **Required Output Format:**
429
+
430
+ \`\`\`\`text
431
+ # Analysis
432
+ [Your reasoning...]
433
+
434
+ ## Changes
435
+ <file path="example.js" action="replace">
436
+ \\\`\\\`\\\`javascript
437
+ // Internal code block uses 3 backticks
438
+ const x = 1;
439
+ \\\`\\\`\\\`
440
+ </file>
441
+
442
+ ## Metadata
443
+ \\\`\\\`\\\`json
444
+ { ... }
445
+ \\\`\\\`\\\`
446
+ \`\`\`\`
447
+
448
+ **File Actions:**
449
+ - \`create\`: Create a new file (requires full content)
450
+ - \`replace\`: Overwrite existing file (requires full content)
451
+ - \`modify\`: Replace specific sections (provide context)
452
+ - \`delete\`: Delete the file
453
+ `;
261
454
  }
262
455
 
263
- const agentDefinitions = buildAgentDefinitions(executionAgents);
456
+ // --- This is the main/Senior Architect prompt logic ---
457
+ let template;
458
+ template = await loadTemplate(promptTemplates.multiAgent);
459
+ // --- INJECT DYNAMIC CONTEXT ---
460
+ template = template.replace('{{projectOverview}}', projectOverview);
461
+ template = template.replace('{{eckManifestSection}}', eckManifestSection);
462
+ // --- END INJECT ---
463
+
464
+ // Filter out gemini agents if not in JA mode
465
+ const filteredExecutionAgents = {};
466
+ for (const [key, agent] of Object.entries(executionAgents)) {
467
+ const isGeminiAgent = key.includes('gemini') || (agent.name && agent.name.toLowerCase().includes('gemini'));
468
+ if (isGeminiAgent) {
469
+ if (withJa && agent.active) filteredExecutionAgents[key] = agent;
470
+ } else {
471
+ if (agent.active) filteredExecutionAgents[key] = agent;
472
+ }
473
+ }
474
+
475
+ const agentDefinitions = buildAgentDefinitions(filteredExecutionAgents);
264
476
 
265
477
  const data = {
266
478
  ...context,
267
- timestamp: new Date().toISOString(),
479
+ timestamp: new Date().toLocaleString(),
268
480
  architectPersona,
269
- agentDefinitions
481
+ agentDefinitions,
482
+ hierarchicalWorkflow,
483
+ commandFormats
270
484
  };
271
485
 
272
486
  let renderedTemplate = render(template, data);
273
-
487
+
488
+ // Inject skeleton mode instructions if enabled
489
+ if (context.options && context.options.skeleton) {
490
+ try {
491
+ const skeletonInstructionPath = path.join(__dirname, '..', 'templates', 'skeleton-instruction.md');
492
+ const skeletonInstructions = await fs.readFile(skeletonInstructionPath, 'utf-8');
493
+ renderedTemplate += '\n\n' + skeletonInstructions + '\n\n';
494
+ } catch (e) {
495
+ console.warn('Warning: Could not load skeleton-instruction.md', e.message);
496
+ }
497
+ }
498
+
274
499
  // Inject dynamic profile context if a profile is active
275
500
  if (context.options && context.options.profile && context.repoPath) {
276
501
  let metadataHeader = '\n\n## Partial Snapshot Context\n';
277
502
  metadataHeader += `- **Profile(s) Active:** ${context.options.profile}\n`;
278
503
  try {
279
- const allProfiles = await getAllProfiles(context.repoPath);
280
- const activeProfileNames = context.options.profile.split(',').map(p => p.trim().replace(/^-/, ''));
281
- const allProfileNames = Object.keys(allProfiles).filter(p => !activeProfileNames.includes(p));
282
- if (allProfileNames.length > 0) {
283
- metadataHeader += `- **Other Available Profiles:** ${allProfileNames.join(', ')}\n`;
284
- }
504
+ const allProfiles = await getAllProfiles(context.repoPath);
505
+ const activeProfileNames = context.options.profile.split(',').map(p => p.trim().replace(/^-/, ''));
506
+ const allProfileNames = Object.keys(allProfiles).filter(p => !activeProfileNames.includes(p));
507
+ if (allProfileNames.length > 0) {
508
+ metadataHeader += `- **Other Available Profiles:** ${allProfileNames.join(', ')}\n`;
509
+ }
285
510
  } catch (e) { /* fail silently on metadata generation */ }
286
-
287
- const insertMarker = "### HIERARCHICAL AGENT WORKFLOW"; // Use our new marker
288
- renderedTemplate = renderedTemplate.replace(insertMarker, metadataHeader + '\n' + insertMarker);
511
+
512
+ const insertMarker = "### "; // Generic marker since we change the H1s
513
+ // Insert before first H3 (WORKFLOW usually)
514
+ renderedTemplate = renderedTemplate.replace(/### /, metadataHeader + '\n### ');
289
515
  }
290
516
 
291
517
  return renderedTemplate;
@@ -300,4 +526,4 @@ Generated: ${new Date().toISOString()}
300
526
 
301
527
  `;
302
528
  }
303
- }
529
+ }