@xelth/eck-snapshot 4.1.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,39 +39,109 @@ function buildAgentDefinitions(executionAgents) {
39
39
  return definitions;
40
40
  }
41
41
 
42
- function buildEckManifestSection(eckManifest) {
43
- if (!eckManifest) {
44
- return '';
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 [];
45
50
  }
46
51
 
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';
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
+ });
53
78
  }
54
79
 
55
- if (eckManifest.operations) {
56
- section += '### Operations Guide\n\n';
57
- section += eckManifest.operations + '\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.';
58
92
  }
59
93
 
60
- if (eckManifest.journal) {
61
- section += '### Development Journal\n\n';
62
- section += eckManifest.journal + '\n\n';
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
147
  return section;
@@ -255,7 +325,7 @@ Your primary role is **Senior Architect**. You formulate high-level strategy. Fo
255
325
 
256
326
  - **Senior Architect (You):** Sets strategy, defines high-level tasks.
257
327
  - **Junior Architect (\`gemini_wsl\`):** Receives strategic tasks, analyzes the \`_ja.md\` snapshot, breaks the task down, and commands the Coder.
258
- - **Coder (\`claude\`):** Receives small, precise coding tasks from the Junior Architect. **Claude is highly trained for code generation and should be used for all primary code-writing tasks**, while \`gemini_wsl\` can use its own tools for analysis, validation, and running shell commands.`;
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.**`;
259
329
 
260
330
  commandFormats = `### COMMAND FORMATS
261
331
 
@@ -339,92 +409,74 @@ Use \`execute_strategic_task\` for complex features. The JA will use its own sna
339
409
  } else {
340
410
  hierarchicalWorkflow = `### AGENT WORKFLOW
341
411
 
342
- Your role is **Architect**. You formulate technical plans and delegate code implementation tasks directly to the **Coder** agents (e.g., \`local_dev\`).
412
+ Your role is **Architect**. You formulate technical plans and delegate code implementation tasks directly to the **Coder** agents.
343
413
 
344
- - **Architect (You):** Sets strategy, defines tasks.
345
- - **Coder (e.g., \`local_dev\`):** Receives precise coding tasks and executes them.`;
414
+ **Your secondary duty is DOCUMENTATION INTEGRITY.** You must ensure the Coder updates .eck/ files whenever the project structure, roadmap, or debt changes.
346
415
 
347
- commandFormats = `### COMMAND FORMATS
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.`;
348
418
 
349
- You MUST use the following JSON command format for Coders:
419
+ commandFormats = `### COMMAND FORMATS (Eck-Protocol v2)
350
420
 
351
- **For Coders (\`local_dev\`, \`production_server\`, \`android_wsl_dev\`, \`gemini_windows\`):**
352
- Use \`apply_code_changes\` for direct tasks where you provide all details.
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.
353
422
 
354
- \`\`\`json
355
- {
356
- "target_agent": "local_dev",
357
- "agent_environment": "Development environment with full GUI support and development tools",
358
- "command_for_agent": "apply_code_changes",
359
- "task_id": "unique-task-id",
360
- "payload": {
361
- "objective": "Brief, clear task description",
362
- "context": "Why this change is needed - include relevant .eck manifest context",
363
- "files_to_modify": [
364
- {
365
- "path": "exact/file/path.js",
366
- "action": "specific action (add, modify, replace, delete)",
367
- "location": "line numbers, function name, or search pattern",
368
- "details": "precise description of the change"
369
- }
370
- ],
371
- "new_files": [
372
- {
373
- "path": "path/to/new/file.js",
374
- "content_type": "javascript/json/markdown/config",
375
- "purpose": "why this file is needed"
376
- }
377
- ],
378
- "dependencies": {
379
- "install": ["package-name@version"],
380
- "remove": ["old-package-name"]
381
- },
382
- "validation_steps": [
383
- "npm run test",
384
- "node index.js --help",
385
- "specific command to verify functionality"
386
- ],
387
- "expected_outcome": "what should work after changes",
388
- "post_execution_steps": {
389
- "journal_entry": {
390
- "type": "feat",
391
- "scope": "authentication",
392
- "summary": "Brief description of what was accomplished",
393
- "details": "Detailed explanation of changes, impacts, and technical notes"
394
- },
395
- "mcp_feedback": {
396
- "success": true,
397
- "errors": [],
398
- "mcp_version": "1.0"
399
- }
400
- }
401
- }
402
- }
403
- \`\`\``;
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
+ `;
404
454
  }
405
455
 
406
456
  // --- This is the main/Senior Architect prompt logic ---
407
457
  let template;
408
- if (context.mode === 'vector') {
409
- template = await loadTemplate(promptTemplates.vectorMode);
410
- // Inject context for vector mode
411
- template = template.replace('{{multiAgentSection}}', `
412
- ${projectOverview}
413
- ${eckManifestSection}
414
- `);
415
- } else {
416
- template = await loadTemplate(promptTemplates.multiAgent);
417
- // --- INJECT DYNAMIC CONTEXT ---
418
- template = template.replace('{{projectOverview}}', projectOverview);
419
- template = template.replace('{{eckManifestSection}}', eckManifestSection);
420
- // --- END INJECT ---
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
+ }
421
473
  }
422
474
 
423
- const agentDefinitions = buildAgentDefinitions(executionAgents);
475
+ const agentDefinitions = buildAgentDefinitions(filteredExecutionAgents);
424
476
 
425
477
  const data = {
426
478
  ...context,
427
- timestamp: new Date().toISOString(),
479
+ timestamp: new Date().toLocaleString(),
428
480
  architectPersona,
429
481
  agentDefinitions,
430
482
  hierarchicalWorkflow,
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Eck-Protocol v2 Parser
3
+ *
4
+ * Parses hybrid Markdown/XML/JSON format for agent communication.
5
+ * This is a pure text parser - no shell commands are involved.
6
+ *
7
+ * Format specification:
8
+ * - Markdown for human-readable analysis/thinking
9
+ * - XML-like <file> tags for code changes (with code in standard fences)
10
+ * - JSON in fenced blocks for structured metadata
11
+ */
12
+
13
+ /**
14
+ * Parses Eck-Protocol v2 response from an agent.
15
+ * @param {string} text - Raw text response from the agent
16
+ * @returns {object} Parsed structure with thought, files, and metadata
17
+ */
18
+ export function parseEckResponse(text) {
19
+ const result = {
20
+ thought: '',
21
+ files: [],
22
+ metadata: {},
23
+ raw: text
24
+ };
25
+
26
+ if (!text || typeof text !== 'string') {
27
+ return result;
28
+ }
29
+
30
+ result.files = extractFiles(text);
31
+ result.metadata = extractMetadata(text);
32
+ result.thought = extractThought(text);
33
+
34
+ return result;
35
+ }
36
+
37
+ /**
38
+ * Extracts file changes from <file> tags.
39
+ * @param {string} text - Raw text
40
+ * @returns {Array<{path: string, action: string, content: string}>}
41
+ */
42
+ function extractFiles(text) {
43
+ const files = [];
44
+
45
+ // Pattern: <file path="..." action="..."> content </file>
46
+ const fileRegex = /<file\s+path=["']([^"']+)["']\s+action=["']([^"']+)["']>\s*([\s\S]*?)\s*<\/file>/gi;
47
+
48
+ let match;
49
+ while ((match = fileRegex.exec(text)) !== null) {
50
+ const path = match[1];
51
+ const action = match[2];
52
+ let content = match[3].trim();
53
+
54
+ // Strip markdown code fences if present
55
+ const fenceMatch = content.match(/^```[\w]*\s*\n?([\s\S]*?)\n?\s*```$/);
56
+ if (fenceMatch) {
57
+ content = fenceMatch[1];
58
+ }
59
+
60
+ content = content.replace(/\r\n/g, '\n');
61
+ files.push({ path, action, content });
62
+ }
63
+
64
+ return files;
65
+ }
66
+
67
+ /**
68
+ * Extracts metadata from JSON fenced blocks.
69
+ * @param {string} text - Raw text
70
+ * @returns {object} Parsed metadata object
71
+ */
72
+ function extractMetadata(text) {
73
+ let metadata = {};
74
+
75
+ // Try: ## Metadata section with JSON block
76
+ const metadataSectionMatch = text.match(/##\s*Metadata\s*\n+```json\s*\n([\s\S]*?)\n```/i);
77
+ if (metadataSectionMatch) {
78
+ try {
79
+ metadata = JSON.parse(metadataSectionMatch[1].trim());
80
+ return metadata;
81
+ } catch (e) {
82
+ console.warn('Failed to parse Metadata JSON:', e.message);
83
+ }
84
+ }
85
+
86
+ // Try: <journal> tag with JSON
87
+ const journalMatch = text.match(/<journal>\s*```json\s*\n?([\s\S]*?)\n?\s*```\s*<\/journal>/i);
88
+ if (journalMatch) {
89
+ try {
90
+ metadata.journal = JSON.parse(journalMatch[1].trim());
91
+ return metadata;
92
+ } catch (e) {
93
+ console.warn('Failed to parse journal JSON:', e.message);
94
+ }
95
+ }
96
+
97
+ // Try: simple <journal type="..." scope="...">summary</journal>
98
+ const simpleJournalMatch = text.match(/<journal\s+type=["']([^"']+)["']\s+scope=["']([^"']+)["']>([^<]+)<\/journal>/i);
99
+ if (simpleJournalMatch) {
100
+ metadata.journal = {
101
+ type: simpleJournalMatch[1],
102
+ scope: simpleJournalMatch[2],
103
+ summary: simpleJournalMatch[3].trim()
104
+ };
105
+ }
106
+
107
+ return metadata;
108
+ }
109
+
110
+ /**
111
+ * Extracts the thought/analysis section.
112
+ * @param {string} text - Raw text
113
+ * @returns {string} The thought/analysis content
114
+ */
115
+ function extractThought(text) {
116
+ const changesIndex = text.search(/##\s*Changes/i);
117
+ const fileIndex = text.search(/<file\s+path=/i);
118
+
119
+ let endIndex = text.length;
120
+ if (changesIndex !== -1 && fileIndex !== -1) {
121
+ endIndex = Math.min(changesIndex, fileIndex);
122
+ } else if (changesIndex !== -1) {
123
+ endIndex = changesIndex;
124
+ } else if (fileIndex !== -1) {
125
+ endIndex = fileIndex;
126
+ }
127
+
128
+ let thought = text.substring(0, endIndex).trim();
129
+ thought = thought.replace(/^#\s*(Analysis|Thinking|Plan)\s*\n*/i, '').trim();
130
+ return thought;
131
+ }
132
+
133
+ /**
134
+ * Validates if a response contains valid Eck-Protocol v2 structure.
135
+ * @param {string} text - Raw text to validate
136
+ * @returns {{valid: boolean, hasFiles: boolean, hasMetadata: boolean, errors: string[]}}
137
+ */
138
+ export function validateEckResponse(text) {
139
+ const result = {
140
+ valid: true,
141
+ hasFiles: false,
142
+ hasMetadata: false,
143
+ errors: []
144
+ };
145
+
146
+ if (!text || typeof text !== 'string') {
147
+ result.valid = false;
148
+ result.errors.push('Response is empty or not a string');
149
+ return result;
150
+ }
151
+
152
+ const hasFileTags = /<file\s+path=/.test(text);
153
+ if (hasFileTags) {
154
+ result.hasFiles = true;
155
+ const openTags = (text.match(/<file\s+/g) || []).length;
156
+ const closeTags = (text.match(/<\/file>/g) || []).length;
157
+ if (openTags !== closeTags) {
158
+ result.valid = false;
159
+ result.errors.push(`Mismatched file tags: ${openTags} opening, ${closeTags} closing`);
160
+ }
161
+ }
162
+
163
+ const hasMetadata = /##\s*Metadata|<journal/i.test(text);
164
+ if (hasMetadata) {
165
+ result.hasMetadata = true;
166
+ }
167
+
168
+ return result;
169
+ }
170
+
171
+ /**
172
+ * Parses response with fallback to legacy JSON format.
173
+ * @param {string} text - Raw response text
174
+ * @returns {object} Parsed result with format indicator
175
+ */
176
+ export function parseWithFallback(text) {
177
+ const validation = validateEckResponse(text);
178
+
179
+ if (validation.hasFiles || validation.hasMetadata) {
180
+ const parsed = parseEckResponse(text);
181
+ return { format: 'eck-v2', ...parsed };
182
+ }
183
+
184
+ // Fallback: try to parse as JSON
185
+ try {
186
+ const jsonMatch = text.match(/```json\s*\n([\s\S]*?)\n```/);
187
+ if (jsonMatch) {
188
+ const jsonData = JSON.parse(jsonMatch[1]);
189
+ return {
190
+ format: 'legacy-json',
191
+ thought: text.split('```json')[0].trim(),
192
+ data: jsonData,
193
+ files: [],
194
+ metadata: {}
195
+ };
196
+ }
197
+
198
+ const jsonData = JSON.parse(text.trim());
199
+ return {
200
+ format: 'pure-json',
201
+ thought: '',
202
+ data: jsonData,
203
+ files: [],
204
+ metadata: {}
205
+ };
206
+ } catch (e) {
207
+ return {
208
+ format: 'plain-text',
209
+ thought: text,
210
+ files: [],
211
+ metadata: {},
212
+ raw: text
213
+ };
214
+ }
215
+ }
216
+
217
+ export default {
218
+ parseEckResponse,
219
+ validateEckResponse,
220
+ parseWithFallback
221
+ };