nothumanallowed 14.1.80 → 14.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "14.1.80",
3
+ "version": "14.2.0",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '14.1.80';
8
+ export const VERSION = '14.2.0';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -381,6 +381,26 @@ const SkillStore = {
381
381
  if (fs.existsSync(abs)) fs.unlinkSync(abs);
382
382
  },
383
383
 
384
+ /** Ensure memory.md, skills.md, and provider.md always exist for a project. */
385
+ ensureDefaults(projectName, config) {
386
+ const dir = ensureDir(this.dir(projectName));
387
+ const provider = config?.llm?.provider || 'nha';
388
+ const model = config?.llm?.model || '';
389
+
390
+ const memFile = path.join(dir, 'memory.md');
391
+ if (!fs.existsSync(memFile)) {
392
+ fs.writeFileSync(memFile, `# ${projectName} — Project Memory\n\n_Architectural decisions, preferences, and notes._\n`, 'utf-8');
393
+ }
394
+ const skillsFile = path.join(dir, 'skills.md');
395
+ if (!fs.existsSync(skillsFile)) {
396
+ fs.writeFileSync(skillsFile, `# ${projectName} — Skills\n\n_Coding patterns, best practices, and conventions for this project._\n`, 'utf-8');
397
+ }
398
+ const providerFile = path.join(dir, `${provider}.md`);
399
+ if (!fs.existsSync(providerFile)) {
400
+ fs.writeFileSync(providerFile, `# ${provider.toUpperCase()} — ${model || 'Default'}\n\n_Model-specific notes, prompt tips, and configuration._\n`, 'utf-8');
401
+ }
402
+ },
403
+
384
404
  context(projectName) {
385
405
  const skills = this.list(projectName);
386
406
  if (skills.length === 0) return '';
@@ -455,165 +475,259 @@ const ChatStore = {
455
475
  * Uses structured SSE events: { type: 'text', token } | { type: 'tool', ... } | { type: 'done', changed }
456
476
  */
457
477
  async function runWebCraftAgent(config, projectName, message, attachments, emit) {
478
+ const MAX_STEPS = 8; // max agentic loop iterations
458
479
  const dir = ProjectStore.dir(projectName);
459
480
  if (!fs.existsSync(dir)) { emit({ type: 'error', msg: 'Project not found' }); return; }
460
481
 
461
- const files = _listProjectFiles(dir);
462
- const skillCtx = SkillStore.context(projectName);
482
+ SkillStore.ensureDefaults(projectName, config);
463
483
 
464
- const fileIndex = files.map((f) => `- ${f}`).join('\n');
465
484
  const today = new Date().toISOString().slice(0, 10);
466
485
  const LANG_MAP = { en:'English',it:'Italian',es:'Spanish',fr:'French',de:'German',pt:'Portuguese' };
467
486
  const language = LANG_MAP[(config?.language||'it').slice(0,2)] || 'Italian';
468
487
 
469
- // Build context: include files mentioned in the message + key project files
470
- const mentionedFiles = files.filter((f) => message.toLowerCase().includes(f.toLowerCase().split('/').pop() ?? ''));
471
- // Always include key structural files
472
- const keyFiles = files.filter((f) => /^(server|app|index)\.(js|mjs|ts)$/.test(f) || f === 'package.json' || f.includes('routes/index'));
473
- const contextFiles = [...new Set([...mentionedFiles, ...keyFiles, ...files.slice(0, 12)])].slice(0, 15);
474
- const fileContents = contextFiles.map((rel) => {
475
- try {
476
- const content = fs.readFileSync(path.join(dir, rel), 'utf-8');
477
- return `### FILE: ${rel}\n\`\`\`\n${content.slice(0, 8000)}\n\`\`\``;
478
- } catch { return ''; }
479
- }).filter(Boolean).join('\n\n');
480
-
481
488
  const toolSpec = `
482
- AVAILABLE TOOLS (use XML tags exactly as shown):
489
+ AVAILABLE TOOLS (use exactly ONE tool per <tool> tag):
490
+
491
+ 1. Read a file (to see its current content before editing):
492
+ <tool>{"op":"read","path":"relative/path.js"}</tool>
493
+
494
+ 2. Edit a file (surgical replacement of exact code):
495
+ <tool>{"op":"edit","path":"relative/path.js","old":"EXACT_EXISTING_CODE","new":"REPLACEMENT_CODE"}</tool>
483
496
 
484
- 1. Read a file:
485
- <tool>{"op":"read","path":"filename.js"}</tool>
497
+ 3. Write/create a file (complete content):
498
+ <tool>{"op":"write","path":"relative/path.js","content":"FULL_FILE_CONTENT"}</tool>
486
499
 
487
- 2. Edit a file (replace a snippet):
488
- <tool>{"op":"edit","path":"filename.js","old":"EXACT_EXISTING_CODE","new":"REPLACEMENT_CODE"}</tool>
500
+ 4. Check syntax of a JS file:
501
+ <tool>{"op":"check","path":"relative/path.js"}</tool>
489
502
 
490
- 3. Write a complete file (full content):
491
- <tool>{"op":"write","path":"filename.js","content":"FULL_FILE_CONTENT"}</tool>
503
+ WORKFLOW follow this for every change:
504
+ 1. Read the file(s) you need to modify
505
+ 2. Make your changes with edit or write
506
+ 3. Use check to verify JS files have no syntax errors
507
+ 4. If check fails, read the file again and fix
508
+ 5. When ALL changes are complete and verified, output: <done/>
492
509
 
493
510
  RULES:
494
- - Always use edit for small targeted changes (preferredfaster and safer)
495
- - Use write only when creating a new file or doing a complete rewrite
496
- - "old" must be an EXACT verbatim match of the existing code — no paraphrasing
497
- - Never apply a tool to a file outside the project scope
498
- - After each tool use, continue explaining what you did
511
+ - "old" must be an EXACT verbatim copy from the file copy-paste, no paraphrasing
512
+ - Use edit for targeted changes, write for new files or complete rewrites
513
+ - ALWAYS read before edit if you haven't seen the file content yet
514
+ - ALWAYS check JS files after modifications
515
+ - Output <done/> when you are completely finished this is MANDATORY
499
516
  `;
500
517
 
501
- // Load memory.md and skills.md for context
502
- const ctxDir = SkillStore.dir(projectName);
503
- let skillContext = '';
504
- try {
505
- const memPath = path.join(ctxDir, 'memory.md');
506
- const skillsPath = path.join(ctxDir, 'skills.md');
507
- const providerPath = path.join(ctxDir, `${config.llm?.provider || 'nha'}.md`);
518
+ // Build system prompt with fresh file list each step
519
+ function buildSystemPrompt() {
520
+ const files = _listProjectFiles(dir);
521
+ const fileIndex = files.map((f) => `- ${f}`).join('\n');
522
+ const skillCtx = SkillStore.context(projectName);
523
+ const ctxDir = SkillStore.dir(projectName);
524
+ let skillContext = '';
525
+ try {
526
+ for (const name of ['memory.md', 'skills.md', `${config.llm?.provider || 'nha'}.md`]) {
527
+ const p = path.join(ctxDir, name);
528
+ if (fs.existsSync(p)) skillContext += `\n### ${name}:\n${fs.readFileSync(p, 'utf-8')}\n`;
529
+ }
530
+ } catch {}
508
531
 
509
- if (fs.existsSync(memPath)) {
510
- const memContent = fs.readFileSync(memPath, 'utf-8');
511
- skillContext += `\n### MEMORY:\n${memContent}\n`;
512
- }
513
- if (fs.existsSync(skillsPath)) {
514
- const skillsContent = fs.readFileSync(skillsPath, 'utf-8');
515
- skillContext += `\n### SKILLS:\n${skillsContent}\n`;
516
- }
517
- if (fs.existsSync(providerPath)) {
518
- const providerContent = fs.readFileSync(providerPath, 'utf-8');
519
- skillContext += `\n### MODEL INFO:\n${providerContent}\n`;
520
- }
521
- } catch {}
532
+ // Key files: load full content for server, package.json, index
533
+ const keyFiles = files.filter((f) =>
534
+ /^(server|app|index)\.(js|mjs|ts)$/.test(f) || f === 'package.json' || f.includes('routes/index')
535
+ );
536
+ const mentionedFiles = files.filter((f) =>
537
+ message.toLowerCase().includes((f.split('/').pop() || '').toLowerCase())
538
+ );
539
+ const contextFiles = [...new Set([...mentionedFiles, ...keyFiles])].slice(0, 10);
540
+ const fileContents = contextFiles.map((rel) => {
541
+ try {
542
+ const content = fs.readFileSync(path.join(dir, rel), 'utf-8');
543
+ return `### FILE: ${rel}\n\`\`\`\n${content}\n\`\`\``;
544
+ } catch { return ''; }
545
+ }).filter(Boolean).join('\n\n');
546
+
547
+ return [
548
+ `You are WebCraft Agent — an elite AI coding assistant. Today: ${today}. Language: ${language}.`,
549
+ `\nYou control the project IDE. You MUST use tools to implement changes — never just explain.`,
550
+ `\nYour workflow: read → plan → edit/write → check → verify → done.`,
551
+ `\nAfter EVERY tool use, you will receive the result. Based on the result, decide what to do next.`,
552
+ `\nWhen finished, output <done/> to signal completion.`,
553
+ `\n\n## PROJECT: ${projectName}`,
554
+ `\n## FILE TREE:\n${fileIndex}`,
555
+ skillContext,
556
+ skillCtx ? `\n\n## PROJECT KNOWLEDGE:\n${skillCtx}` : '',
557
+ attachments?.length ? `\n\n## ATTACHMENTS: ${attachments.map((a) => a.name).join(', ')}` : '',
558
+ fileContents ? `\n\n## LOADED FILES:\n${fileContents}` : '',
559
+ `\n\n${toolSpec}`,
560
+ ].join('');
561
+ }
522
562
 
523
- const systemPrompt = [
524
- `You are WebCraft Agent — a team of 200 senior developers working as one entity. Today is ${today}. Respond in ${language}.`,
525
- `\nYou have FULL control of the project IDE. You read files, edit them surgically, and write new ones.`,
526
- `\nYour edits MUST be enterprise-grade: security, error handling, responsive design, accessibility.`,
527
- `\nWhen the user asks for changes, you MUST use tools to implement them — never just explain. ACT, don't talk.`,
528
- `\nAfter each tool use, briefly explain what changed and why.`,
529
- `\n\n## PROJECT: ${projectName}`,
530
- `\n## FILES:\n${fileIndex}`,
531
- skillContext,
532
- skillCtx ? `\n\n## ADDITIONAL CONTEXT:\n${skillCtx}` : '',
533
- attachments?.length ? `\n\n## ATTACHMENTS: ${attachments.map((a) => a.name).join(', ')}` : '',
534
- `\n\n## CURRENT FILE CONTENTS:\n${fileContents}`,
535
- `\n\n${toolSpec}`,
536
- ].join('');
537
-
538
- // Prepare user content (text + images if any)
563
+ // Prepare user content
539
564
  const userContent = attachments?.length
540
565
  ? _buildMultimodalContent(message, attachments)
541
566
  : message;
542
567
 
543
- let fullResponse = '';
568
+ // ── Agentic loop ───────────────────────────────────────────────────────────
544
569
  let hasChanges = false;
570
+ const modifiedFiles = new Set();
571
+ let conversationHistory = [
572
+ { role: 'user', content: userContent },
573
+ ];
574
+
575
+ for (let step = 0; step < MAX_STEPS; step++) {
576
+ const systemPrompt = buildSystemPrompt();
577
+ emit({ type: 'step', step: step + 1, max: MAX_STEPS });
578
+
579
+ // Build user message from conversation history
580
+ // callLLMStream takes a single string, so we concatenate the conversation
581
+ let stepResponse = '';
582
+ const userMsg = conversationHistory.map((m) =>
583
+ m.role === 'user' ? m.content : `[ASSISTANT RESPONSE]\n${m.content.slice(0, 6000)}`
584
+ ).join('\n\n---\n\n');
585
+
586
+ await callLLMStream(config, systemPrompt, userMsg, (token) => {
587
+ stepResponse += token;
588
+ // Stream visible text (suppress tool tags)
589
+ const visible = token.replace(/<tool>[\s\S]*?<\/tool>/g, '').replace(/<done\s*\/>/g, '');
590
+ if (visible) emit({ type: 'text', token: visible });
591
+ }, { max_tokens: 16384 });
592
+
593
+ // Check if agent signaled completion
594
+ const isDone = stepResponse.includes('<done/>') || stepResponse.includes('<done />');
595
+
596
+ // Extract and execute ALL tool calls from this step
597
+ const toolRegex = /<tool>([\s\S]*?)<\/tool>/g;
598
+ let match;
599
+ const toolResults = [];
600
+
601
+ while ((match = toolRegex.exec(stepResponse)) !== null) {
602
+ let toolCall;
603
+ try {
604
+ // Fix common JSON issues from LLM
605
+ let raw = match[1].trim();
606
+ raw = raw.replace(/,\s*}/g, '}').replace(/,\s*]/g, ']');
607
+ toolCall = JSON.parse(raw);
608
+ } catch {
609
+ toolResults.push({ op: 'error', result: 'JSON parse failed' });
610
+ emit({ type: 'tool', op: 'parse_error', path: '', result: 'json_parse_failed' });
611
+ continue;
612
+ }
545
613
 
546
- await callLLMStream(config, systemPrompt, userContent, (token) => {
547
- fullResponse += token;
548
- // Suppress raw <tool> blocks from text stream — only emit visible text
549
- const visibleToken = token.replace(/<tool>[\s\S]*?<\/tool>/g, '');
550
- if (visibleToken) emit({ type: 'text', token: visibleToken });
551
- }, { max_tokens: 16384 });
552
-
553
- // ── Execute all tool calls found in the response ───────────────────────────
554
- const toolRegex = /<tool>([\s\S]*?)<\/tool>/g;
555
- let match;
556
- while ((match = toolRegex.exec(fullResponse)) !== null) {
557
- let toolCall;
558
- try { toolCall = JSON.parse(match[1].trim()); } catch {
559
- emit({ type: 'tool', op: 'parse_error', path: '', result: 'json_parse_failed' });
560
- continue;
561
- }
562
-
563
- const { op, path: relPath, old: oldStr, new: newStr, content } = toolCall;
564
- if (!relPath || !_isSafePath(relPath)) {
565
- emit({ type: 'tool', op, path: relPath ?? '', result: 'unsafe_path' });
566
- continue;
567
- }
568
-
569
- if (op === 'read') {
570
- const src = ProjectStore.readFile(projectName, relPath);
571
- emit({ type: 'tool', op: 'read', path: relPath, result: src !== null ? 'ok' : 'not_found' });
614
+ const { op, path: relPath, old: oldStr, new: newStr, content } = toolCall;
615
+ if (!relPath || !_isSafePath(relPath)) {
616
+ toolResults.push({ op, path: relPath, result: 'unsafe_path' });
617
+ emit({ type: 'tool', op, path: relPath ?? '', result: 'unsafe_path' });
618
+ continue;
619
+ }
572
620
 
573
- } else if (op === 'edit') {
574
- const src = ProjectStore.readFile(projectName, relPath);
575
- if (src === null) {
576
- emit({ type: 'tool', op: 'edit', path: relPath, result: 'file_not_found' });
577
- } else if (!src.includes(oldStr)) {
578
- // Fallback: try LLM-assisted repair
579
- const repaired = await _attemptEditRepair(config, relPath, src, oldStr, newStr);
580
- if (repaired) {
581
- ProjectStore.writeFile(projectName, relPath, repaired);
621
+ if (op === 'read') {
622
+ const src = ProjectStore.readFile(projectName, relPath);
623
+ const result = src !== null ? 'ok' : 'not_found';
624
+ toolResults.push({ op: 'read', path: relPath, result, content: src?.slice(0, 12000) });
625
+ emit({ type: 'tool', op: 'read', path: relPath, result });
626
+
627
+ } else if (op === 'check') {
628
+ const src = ProjectStore.readFile(projectName, relPath);
629
+ let checkResult = 'ok';
630
+ if (!src) { checkResult = 'file_not_found'; }
631
+ else if (relPath.endsWith('.js') || relPath.endsWith('.mjs')) {
632
+ try { new Function(src); } catch (e) { checkResult = `syntax_error: ${e.message.replace(/\n.*/s, '')}`; }
633
+ } else if (relPath.endsWith('.json')) {
634
+ try { JSON.parse(src); } catch (e) { checkResult = `json_error: ${e.message}`; }
635
+ } else if (relPath.endsWith('.html')) {
636
+ checkResult = src.includes('</html>') ? 'ok' : 'missing_closing_html_tag';
637
+ }
638
+ toolResults.push({ op: 'check', path: relPath, result: checkResult });
639
+ emit({ type: 'tool', op: 'check', path: relPath, result: checkResult });
640
+
641
+ } else if (op === 'edit') {
642
+ const src = ProjectStore.readFile(projectName, relPath);
643
+ if (src === null) {
644
+ toolResults.push({ op: 'edit', path: relPath, result: 'file_not_found' });
645
+ emit({ type: 'tool', op: 'edit', path: relPath, result: 'file_not_found' });
646
+ } else if (!src.includes(oldStr)) {
647
+ const repaired = await _attemptEditRepair(config, relPath, src, oldStr, newStr);
648
+ if (repaired) {
649
+ ProjectStore.writeFile(projectName, relPath, repaired);
650
+ hasChanges = true;
651
+ modifiedFiles.add(relPath);
652
+ toolResults.push({ op: 'edit', path: relPath, result: 'ok_repaired' });
653
+ emit({ type: 'tool', op: 'edit', path: relPath, result: 'ok_repaired', oldSnippet: oldStr.slice(0, 300), newSnippet: newStr?.slice(0, 300) });
654
+ } else {
655
+ toolResults.push({ op: 'edit', path: relPath, result: 'old_not_found', hint: 'Use read to see current content, then retry with exact text' });
656
+ emit({ type: 'tool', op: 'edit', path: relPath, result: 'old_not_found', oldSnippet: oldStr.slice(0, 200) });
657
+ }
658
+ } else {
659
+ const newSrc = src.replace(oldStr, newStr ?? '');
660
+ ProjectStore.writeFile(projectName, relPath, newSrc);
582
661
  hasChanges = true;
583
- emit({ type: 'tool', op: 'edit', path: relPath, result: 'ok_repaired', oldSnippet: oldStr.slice(0, 300), newSnippet: newStr.slice(0, 300) });
662
+ modifiedFiles.add(relPath);
663
+ toolResults.push({ op: 'edit', path: relPath, result: 'ok' });
664
+ emit({ type: 'tool', op: 'edit', path: relPath, result: 'ok', oldSnippet: oldStr.slice(0, 300), newSnippet: newStr?.slice(0, 300) ?? '' });
665
+ }
666
+
667
+ } else if (op === 'write') {
668
+ if (content === undefined) {
669
+ toolResults.push({ op: 'write', path: relPath, result: 'missing_content' });
670
+ emit({ type: 'tool', op: 'write', path: relPath, result: 'missing_content' });
584
671
  } else {
585
- emit({ type: 'tool', op: 'edit', path: relPath, result: 'old_not_found', oldSnippet: oldStr.slice(0, 200) });
672
+ ProjectStore.writeFile(projectName, relPath, content);
673
+ hasChanges = true;
674
+ modifiedFiles.add(relPath);
675
+ toolResults.push({ op: 'write', path: relPath, result: 'ok' });
676
+ emit({ type: 'tool', op: 'write', path: relPath, result: 'ok' });
586
677
  }
587
- } else {
588
- const newSrc = src.replace(oldStr, newStr ?? '');
589
- ProjectStore.writeFile(projectName, relPath, newSrc);
590
- hasChanges = true;
591
- emit({ type: 'tool', op: 'edit', path: relPath, result: 'ok', oldSnippet: oldStr.slice(0, 300), newSnippet: newStr?.slice(0, 300) ?? '' });
592
678
  }
679
+ }
680
+
681
+ // If agent said done or no tools were called, break
682
+ if (isDone || toolResults.length === 0) break;
683
+
684
+ // Build tool results feedback for next iteration
685
+ const feedbackParts = toolResults.map((r) => {
686
+ let msg = `[${r.op}] ${r.path || ''}: ${r.result}`;
687
+ if (r.op === 'read' && r.content) msg += `\n\`\`\`\n${r.content}\n\`\`\``;
688
+ if (r.hint) msg += ` — ${r.hint}`;
689
+ return msg;
690
+ });
691
+
692
+ // Add assistant response + tool results to conversation
693
+ conversationHistory.push({ role: 'assistant', content: stepResponse });
694
+ conversationHistory.push({ role: 'user', content: `TOOL RESULTS:\n${feedbackParts.join('\n\n')}\n\nContinue your work. When done, output <done/>` });
593
695
 
594
- } else if (op === 'write') {
595
- if (content === undefined) {
596
- emit({ type: 'tool', op: 'write', path: relPath, result: 'missing_content' });
597
- } else {
598
- ProjectStore.writeFile(projectName, relPath, content);
599
- hasChanges = true;
600
- emit({ type: 'tool', op: 'write', path: relPath, result: 'ok' });
696
+ // Trim conversation to avoid context overflow (keep first user msg + last 4 exchanges)
697
+ if (conversationHistory.length > 10) {
698
+ conversationHistory = [conversationHistory[0], ...conversationHistory.slice(-6)];
699
+ }
700
+ }
701
+
702
+ // ── Post-edit: syntax check all modified JS files ──────────────────────────
703
+ const syntaxErrors = [];
704
+ for (const relPath of modifiedFiles) {
705
+ if (relPath.endsWith('.js') || relPath.endsWith('.mjs')) {
706
+ const src = ProjectStore.readFile(projectName, relPath);
707
+ if (src) {
708
+ try { new Function(src); } catch (e) {
709
+ syntaxErrors.push({ file: relPath, error: e.message.replace(/\n.*/s, '') });
710
+ }
601
711
  }
602
712
  }
603
713
  }
714
+ if (syntaxErrors.length > 0) {
715
+ emit({ type: 'syntax_errors', errors: syntaxErrors });
716
+ }
604
717
 
605
- // Log chat interaction to changes.log.md
718
+ // Log chat interaction
606
719
  if (hasChanges) {
607
720
  try {
608
- const ctxDir = SkillStore.dir(projectName);
609
- const logFile = path.join(ctxDir, 'changes.log.md');
610
- const timestamp = new Date().toISOString();
611
- const logEntry = `\n## ${timestamp.slice(0, 16).replace('T', ' ')} Chat modification\n- User: ${message.slice(0, 100)}${message.length > 100 ? '...' : ''}\n- Files modified: ${fullResponse.match(/<tool>[\s\S]*?"op":"(write|edit)"[\s\S]*?"path":"([^"]+)"[\s\S]*?<\/tool>/g)?.map(m => m.match(/"path":"([^"]+)"/)?.[1]).filter(Boolean).join(', ') || 'none'}\n`;
612
- fs.writeFileSync(logFile, (fs.existsSync(logFile) ? fs.readFileSync(logFile, 'utf-8') : '') + logEntry, 'utf-8');
721
+ const logFile = path.join(SkillStore.dir(projectName), 'changes.log.md');
722
+ const ts = new Date().toISOString().slice(0, 16).replace('T', ' ');
723
+ const entry = `\n## ${ts} — Chat modification\n- User: ${message.slice(0, 100)}${message.length > 100 ? '...' : ''}\n- Files: ${[...modifiedFiles].join(', ')}\n`;
724
+ fs.writeFileSync(logFile, (fs.existsSync(logFile) ? fs.readFileSync(logFile, 'utf-8') : '') + entry, 'utf-8');
613
725
  } catch {}
614
726
  }
615
727
 
616
- emit({ type: 'done', changed: hasChanges });
728
+ // Notify about modified files so frontend can reload them
729
+ emit({ type: 'files_changed', files: [...modifiedFiles] });
730
+ emit({ type: 'done', changed: hasChanges, syntaxErrors: syntaxErrors.length });
617
731
  }
618
732
 
619
733
  // ── Generation pipeline (SSE) ─────────────────────────────────────────────────
@@ -970,17 +1084,15 @@ Continue from here:`;
970
1084
  };
971
1085
  fs.writeFileSync(ProjectStore.metaPath(projectName), JSON.stringify(meta, null, 2), 'utf-8');
972
1086
 
973
- // Initialize skill context files
974
- const ctxDir = ensureDir(SkillStore.dir(projectName));
975
- const memFile = path.join(ctxDir, 'memory.md');
976
- if (!fs.existsSync(memFile)) {
977
- fs.writeFileSync(memFile, `# ${projectName} — Project Memory\n\n_Add architectural decisions, preferences, and notes here._\n`, 'utf-8');
978
- }
1087
+ // Initialize skill context files (memory.md, skills.md, provider.md)
1088
+ SkillStore.ensureDefaults(projectName, config);
1089
+ const ctxDir = SkillStore.dir(projectName);
979
1090
 
980
- // Generate skills.md with project context knowledge structure
1091
+ // Generate detailed skills.md with project context knowledge structure (first time only)
981
1092
  const skillsFile = path.join(ctxDir, 'skills.md');
982
- if (!fs.existsSync(skillsFile)) {
983
- const skillsContent = `# ${projectName} — Skills & Knowledge Structure
1093
+ const skillsContent = fs.existsSync(skillsFile) ? fs.readFileSync(skillsFile, 'utf-8') : '';
1094
+ if (skillsContent.length < 100) {
1095
+ const detailedSkills = `# ${projectName} — Skills & Knowledge Structure
984
1096
 
985
1097
  ## Context Discovery Strategy
986
1098
 
@@ -1018,36 +1130,11 @@ Agents score context relevance based on:
1018
1130
 
1019
1131
  This approach ensures agents have the right context without being overwhelmed by irrelevant information.
1020
1132
  `;
1021
- fs.writeFileSync(skillsFile, skillsContent, 'utf-8');
1133
+ fs.writeFileSync(skillsFile, detailedSkills, 'utf-8');
1022
1134
  }
1023
- // Initialize provider-specific file (liara.md, claude.md, etc.)
1135
+
1024
1136
  const provider = config.llm?.provider || 'nha';
1025
1137
  const model = config.llm?.model || '';
1026
- const providerFile = path.join(ctxDir, `${provider}.md`);
1027
- if (!fs.existsSync(providerFile)) {
1028
- const providerContent = `# ${provider.toUpperCase()} Model Configuration
1029
-
1030
- ## Current Model: ${model || 'Default'}
1031
-
1032
- ### Model Characteristics
1033
- - **Provider**: ${provider}
1034
- - **Model**: ${model || 'Default model for this provider'}
1035
- - **Context Window**: Varies by model
1036
- - **Strengths**: Add specific strengths of this model
1037
- - **Limitations**: Add specific limitations to be aware of
1038
-
1039
- ### Best Practices for This Model
1040
- - Write specific coding patterns this model excels at
1041
- - Note any formatting preferences
1042
- - Document prompt engineering tips that work well
1043
-
1044
- ### Configuration Notes
1045
- - Add any specific configuration notes for this provider
1046
- - Document any rate limits or special considerations
1047
- `;
1048
- fs.writeFileSync(providerFile, providerContent, 'utf-8');
1049
- }
1050
-
1051
1138
  const logFile = path.join(ctxDir, 'changes.log.md');
1052
1139
  const logEntry = `## ${new Date().toISOString().slice(0, 10)} — Initial generation\n- Generated ${generatedFiles.length} files\n- Tokens in: ${totalTokensIn} / out: ${totalTokensOut}\n- Description: ${description}\n- Provider: ${provider} (${model || 'default'})\n`;
1053
1140
  fs.writeFileSync(logFile, (fs.existsSync(logFile) ? fs.readFileSync(logFile, 'utf-8') : '') + logEntry, 'utf-8');
@@ -1160,6 +1247,8 @@ export function register(router) {
1160
1247
  // ── Skills get ────────────────────────────────────────────────────────────
1161
1248
  router.get(/^\/api\/studio\/webcraft\/skills\/(?<name>[^/?]+)(?:\?|$)/, (req, res) => {
1162
1249
  const projectName = decodeURIComponent(req.params.name ?? '');
1250
+ // Always ensure defaults exist when loading skills
1251
+ SkillStore.ensureDefaults(projectName, loadConfig());
1163
1252
  sendJSON(res, 200, { skills: SkillStore.list(projectName) });
1164
1253
  });
1165
1254