@cccarv82/freya 1.0.52 → 1.0.54

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.
@@ -25,6 +25,12 @@ Do not invent status updates.
25
25
  * **Keywords:** "Tarefa", "Task", "To-Do", "Fazer", "Agenda", "Delegado".
26
26
  * **Target File:** `data/tasks/task-log.json`.
27
27
  * **Action:** Read file directly.
28
+ * **If Daily Log Query:**
29
+ * **Keywords:** "log diário", "diário", "daily log", "anotações", "o que anotei", "ontem", "hoje", "semana passada".
30
+ * **Target Folder:** `logs/daily/`.
31
+ * **If date provided:** Read `logs/daily/YYYY-MM-DD.md`.
32
+ * **If date is relative (hoje/ontem):** Resolve to an exact date and read the matching file.
33
+ * **If no specific date or file missing:** Route to **Search** across `logs/daily/` (or ask the user to refine the date). If Search is unavailable, say you have no records for that date and offer to list available log dates.
28
34
  * **If Project Query:**
29
35
  * Proceed to Project Lookup (Glob search).
30
36
  * **Strategy:** Search recursively in `data/Clients`.
@@ -46,7 +52,13 @@ Do not invent status updates.
46
52
  * **Context:** "Aqui estão suas tarefas pendentes [{Category}]:"
47
53
  * **List:** Bullet points.
48
54
  * Format: `* [ID-Short] {Description} ({Priority})`
49
- * **Empty:** "Você não tem tarefas pendentes nesta categoria."
55
+ * **Empty:** "Você não tem tarefas pendentes nesta categoria."
56
+
57
+ * **Daily Log Logic:**
58
+ * Read the Markdown file from `logs/daily/YYYY-MM-DD.md`.
59
+ * Return a concise excerpt (first few meaningful lines or bullet points).
60
+ * If the file is empty, say: "Log registrado sem detalhes para essa data."
61
+ * If the file does not exist: "Não encontrei log diário para esta data."
50
62
 
51
63
  * **Project Logic:**
52
64
  * If matches found: Read the `status.json` file(s).
package/README.md CHANGED
@@ -8,7 +8,7 @@ F.R.E.Y.A. é um sistema de agentes de IA projetado para organizar seu trabalho,
8
8
 
9
9
  * **Ingestão Universal:** Registre updates, blockers e notas mentais em linguagem natural.
10
10
  * **Gestão de Tarefas:** Crie, liste e conclua tarefas ("Lembre-me de fazer X", "Minhas tarefas", "Terminei X").
11
- * **Oráculo:** Pergunte sobre o status de qualquer projeto ("Como está o projeto X?").
11
+ * **Oráculo:** Pergunte sobre o status de qualquer projeto ("Como está o projeto X?") e recupere logs diários ("O que anotei ontem?").
12
12
  * **Career Coach:** Gere "Brag Sheets" automáticas para suas avaliações de desempenho.
13
13
  * **Relatórios Automatizados:** Gere resumos semanais, dailies, relatório de Scrum Master e relatórios executivos.
14
14
  * **Blockers & Riscos:** Gere um relatório rápido de blockers priorizados por severidade.
package/cli/web.js CHANGED
@@ -30,6 +30,12 @@ function escapeHtml(str) {
30
30
 
31
31
  const APP_VERSION = readAppVersion();
32
32
 
33
+ const CHAT_ID_PATTERNS = [
34
+ /\bPTI\d{4,}-\d+\b/gi,
35
+ /\bINC\d+\b/gi,
36
+ /\bCHG\d+\b/gi
37
+ ];
38
+
33
39
  function guessNpmCmd() {
34
40
  // We'll execute via cmd.exe on Windows for reliability.
35
41
  return process.platform === 'win32' ? 'npm' : 'npm';
@@ -637,6 +643,61 @@ function isAllowedChatSearchPath(relPath) {
637
643
  return relPath.startsWith('data/') || relPath.startsWith('logs/') || relPath.startsWith('docs/');
638
644
  }
639
645
 
646
+ function extractChatIds(text) {
647
+ const tokens = new Set();
648
+ const q = String(text || '');
649
+ for (const re of CHAT_ID_PATTERNS) {
650
+ const matches = q.match(re);
651
+ if (matches) {
652
+ for (const m of matches) tokens.add(m.toUpperCase());
653
+ }
654
+ }
655
+ return Array.from(tokens);
656
+ }
657
+
658
+ function extractProjectToken(text) {
659
+ const raw = String(text || '');
660
+ const m = raw.match(/project\s*[:=]\s*([A-Za-z0-9_\/-]+)/i);
661
+ if (m && m[1]) return m[1].trim();
662
+ const m2 = raw.match(/project\s*\(([^)]+)\)/i);
663
+ if (m2 && m2[1]) return m2[1].trim();
664
+ return '';
665
+ }
666
+
667
+ function projectFromPath(relPath) {
668
+ const p = String(relPath || '');
669
+ const m = p.match(/data\/Clients\/([^/]+)\/([^/]+)/i);
670
+ if (m && m[1] && m[2]) return `${m[1]}/${m[2]}`;
671
+ return '';
672
+ }
673
+
674
+ function matchKey(m) {
675
+ const ids = extractChatIds(`${m.file || ''} ${m.snippet || ''}`);
676
+ if (ids.length) return `id:${ids[0]}`;
677
+ const proj = projectFromPath(m.file) || extractProjectToken(`${m.file || ''} ${m.snippet || ''}`);
678
+ if (proj) return `proj:${proj.toLowerCase()}`;
679
+ return `file:${m.file || ''}`;
680
+ }
681
+
682
+ function mergeMatches(primary, secondary, limit = 8) {
683
+ const list = [];
684
+ const seen = new Set();
685
+ const push = (m) => {
686
+ if (!m || !m.file || !isAllowedChatSearchPath(m.file)) return;
687
+ const key = matchKey(m);
688
+ if (seen.has(key)) return;
689
+ seen.add(key);
690
+ list.push(m);
691
+ };
692
+
693
+ (primary || []).forEach(push);
694
+ (secondary || []).forEach(push);
695
+
696
+ const total = list.length;
697
+ const trimmed = list.slice(0, Math.max(1, Math.min(20, limit)));
698
+ return { matches: trimmed, total };
699
+ }
700
+
640
701
  async function copilotSearch(workspaceDir, query, opts = {}) {
641
702
  const q = String(query || '').trim();
642
703
  if (!q) return { ok: false, error: 'Missing query' };
@@ -723,8 +784,8 @@ async function copilotSearch(workspaceDir, query, opts = {}) {
723
784
  return { ok: true, answer, matches, evidence };
724
785
  }
725
786
 
726
- function buildChatAnswer(query, matches, summary, evidence, answer) {
727
- const count = matches.length;
787
+ function buildChatAnswer(query, matches, summary, evidence, answer, totalCount) {
788
+ const count = typeof totalCount === 'number' ? totalCount : matches.length;
728
789
  let summaryText = String(summary || '').trim();
729
790
  let answerText = String(answer || '').trim();
730
791
  if (!answerText) {
@@ -740,7 +801,7 @@ function buildChatAnswer(query, matches, summary, evidence, answer) {
740
801
 
741
802
  const lines = [];
742
803
  lines.push(`Encontrei ${count} registro(s).`);
743
- lines.push(`Resumo: ${answerText}`);
804
+ lines.push(`Resposta curta: ${answerText}`);
744
805
 
745
806
  const evidences = Array.isArray(evidence) && evidence.length
746
807
  ? evidence
@@ -749,15 +810,15 @@ function buildChatAnswer(query, matches, summary, evidence, answer) {
749
810
  if (!evidences.length) return lines.join('\n');
750
811
 
751
812
  lines.push('');
752
- lines.push('Principais evidências:');
813
+ lines.push('Detalhes:');
753
814
  for (const m of evidences.slice(0, 5)) {
754
- const parts = [];
755
- if (m.date) parts.push(`**${m.date}**`);
756
- if (m.file) parts.push('`' + m.file + '`');
757
- const prefix = parts.length ? parts.join(' — ') + ':' : '';
758
815
  const detail = (m.detail || m.snippet || '').toString().trim();
759
816
  if (!detail) continue;
760
- lines.push(`- ${prefix} ${detail}`);
817
+ const meta = [];
818
+ if (m.file) meta.push(m.file);
819
+ if (m.date) meta.push(m.date);
820
+ const suffix = meta.length ? ` (${meta.join(' · ')})` : '';
821
+ lines.push(`- ${detail}${suffix}`);
761
822
  }
762
823
 
763
824
  return lines.join('\n');
@@ -1781,24 +1842,27 @@ async function cmdWeb({ port, dir, open, dev }) {
1781
1842
  if (!query) return safeJson(res, 400, { error: 'Missing query' });
1782
1843
 
1783
1844
  const copilotResult = await copilotSearch(workspaceDir, query, { limit: 8 });
1845
+ const indexMatches = searchIndex(workspaceDir, query, { limit: 12 });
1846
+ const baseMatches = indexMatches.length
1847
+ ? indexMatches
1848
+ : searchWorkspace(workspaceDir, query, { limit: 12 });
1849
+
1784
1850
  if (copilotResult.ok) {
1785
- const matches = copilotResult.matches || [];
1851
+ const merged = mergeMatches(copilotResult.matches || [], baseMatches, 8);
1786
1852
  const answer = buildChatAnswer(
1787
1853
  query,
1788
- matches,
1854
+ merged.matches,
1789
1855
  copilotResult.summary,
1790
1856
  copilotResult.evidence,
1791
- copilotResult.answer
1857
+ copilotResult.answer,
1858
+ merged.total
1792
1859
  );
1793
- return safeJson(res, 200, { ok: true, sessionId, answer, matches });
1860
+ return safeJson(res, 200, { ok: true, sessionId, answer, matches: merged.matches });
1794
1861
  }
1795
1862
 
1796
- const indexMatches = searchIndex(workspaceDir, query, { limit: 8 });
1797
- const matches = indexMatches.length
1798
- ? indexMatches
1799
- : searchWorkspace(workspaceDir, query, { limit: 8 });
1800
- const answer = buildChatAnswer(query, matches, '');
1801
- return safeJson(res, 200, { ok: true, sessionId, answer, matches });
1863
+ const merged = mergeMatches(baseMatches, [], 8);
1864
+ const answer = buildChatAnswer(query, merged.matches, '', [], '', merged.total);
1865
+ return safeJson(res, 200, { ok: true, sessionId, answer, matches: merged.matches });
1802
1866
  }
1803
1867
 
1804
1868
  // Chat persistence (per session)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.52",
3
+ "version": "1.0.54",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",
@@ -13,7 +13,7 @@
13
13
  "export-obsidian": "node scripts/export-obsidian.js",
14
14
  "build-index": "node scripts/index/build-index.js",
15
15
  "update-index": "node scripts/index/update-index.js",
16
- "test": "node tests/unit/test-package-config.js && node tests/unit/test-cli-init.js && node tests/unit/test-cli-web-help.js && node tests/unit/test-web-static-assets.js && node tests/unit/test-fs-utils.js && node tests/unit/test-search-utils.js && node tests/unit/test-index-utils.js && node tests/unit/test-task-schema.js && node tests/unit/test-daily-generation.js && node tests/unit/test-report-generation.js && node tests/unit/test-oracle-retrieval.js && node tests/unit/test-task-completion.js && node tests/unit/test-migrate-data.js && node tests/unit/test-blockers-validation.js && node tests/unit/test-blockers-report.js && node tests/unit/test-sm-weekly-report.js && node tests/integration/test-ingestor-task.js"
16
+ "test": "node tests/unit/test-package-config.js && node tests/unit/test-cli-init.js && node tests/unit/test-cli-web-help.js && node tests/unit/test-web-static-assets.js && node tests/unit/test-fs-utils.js && node tests/unit/test-search-utils.js && node tests/unit/test-index-utils.js && node tests/unit/test-task-schema.js && node tests/unit/test-daily-generation.js && node tests/unit/test-report-generation.js && node tests/unit/test-executive-report-logs.js && node tests/unit/test-oracle-retrieval.js && node tests/unit/test-task-completion.js && node tests/unit/test-migrate-data.js && node tests/unit/test-blockers-validation.js && node tests/unit/test-blockers-report.js && node tests/unit/test-sm-weekly-report.js && node tests/integration/test-ingestor-task.js"
17
17
  },
18
18
  "keywords": [],
19
19
  "author": "",
@@ -9,7 +9,7 @@
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
 
12
- const { toIsoDate, isWithinRange } = require('./lib/date-utils');
12
+ const { toIsoDate, isWithinRange, safeParseToMs } = require('./lib/date-utils');
13
13
  const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
14
14
 
15
15
  // --- Configuration ---
@@ -17,6 +17,15 @@ const DATA_DIR = path.join(__dirname, '../data');
17
17
  const LOGS_DIR = path.join(__dirname, '../logs/daily');
18
18
  const OUTPUT_DIR = path.join(__dirname, '../docs/reports');
19
19
  const TASKS_FILE = path.join(DATA_DIR, 'tasks/task-log.json');
20
+ const BLOCKERS_FILE = path.join(DATA_DIR, 'blockers/blocker-log.json');
21
+
22
+ const RESOLVED_STATUSES = new Set(['RESOLVED', 'CLOSED', 'DONE', 'FIXED']);
23
+ const SEVERITY_ORDER = {
24
+ CRITICAL: 0,
25
+ HIGH: 1,
26
+ MEDIUM: 2,
27
+ LOW: 3
28
+ };
20
29
 
21
30
  // --- Helpers ---
22
31
  function getDateRange(period) {
@@ -152,6 +161,135 @@ function getDailyLogs(start, end) {
152
161
  return relevantLogs;
153
162
  }
154
163
 
164
+ function summarizeLogContent(content, maxLines = 3, maxChars = 280) {
165
+ if (!content) return null;
166
+ const lines = content
167
+ .split('\n')
168
+ .map(line => line.trim())
169
+ .filter(line => line && !line.startsWith('#'));
170
+
171
+ const highlights = [];
172
+ for (const line of lines) {
173
+ const cleaned = line.replace(/^[-*]\s+/, '').replace(/^>\s+/, '').trim();
174
+ if (!cleaned) continue;
175
+ highlights.push(cleaned);
176
+ if (highlights.length >= maxLines) break;
177
+ }
178
+
179
+ if (highlights.length === 0) return null;
180
+ let summary = highlights.join('; ');
181
+ if (summary.length > maxChars) {
182
+ summary = summary.slice(0, Math.max(0, maxChars - 3)).trim() + '...';
183
+ }
184
+ return summary;
185
+ }
186
+
187
+ function normalizeStatus(blocker) {
188
+ const raw = blocker.status || blocker.state || blocker.currentStatus;
189
+ if (!raw) return 'UNKNOWN';
190
+ return String(raw).trim().toUpperCase();
191
+ }
192
+
193
+ function normalizeSeverity(blocker) {
194
+ const raw = blocker.severity || blocker.priority || blocker.level;
195
+ if (!raw) return 'UNSPECIFIED';
196
+ const value = String(raw).trim().toUpperCase();
197
+ if (value.includes('CRIT')) return 'CRITICAL';
198
+ if (value.includes('HIGH')) return 'HIGH';
199
+ if (value.includes('MED')) return 'MEDIUM';
200
+ if (value.includes('LOW')) return 'LOW';
201
+ return value;
202
+ }
203
+
204
+ function getBlockerTitle(blocker) {
205
+ return (
206
+ blocker.title ||
207
+ blocker.summary ||
208
+ blocker.description ||
209
+ blocker.content ||
210
+ blocker.text ||
211
+ 'Untitled blocker'
212
+ );
213
+ }
214
+
215
+ function getCreatedAt(blocker) {
216
+ const candidates = [
217
+ blocker.createdAt,
218
+ blocker.created_at,
219
+ blocker.openedAt,
220
+ blocker.opened_at,
221
+ blocker.reportedAt,
222
+ blocker.reported_at,
223
+ blocker.date,
224
+ blocker.loggedAt
225
+ ];
226
+ for (const value of candidates) {
227
+ const ms = safeParseToMs(value);
228
+ if (Number.isFinite(ms)) return ms;
229
+ }
230
+ return NaN;
231
+ }
232
+
233
+ function getResolvedAt(blocker) {
234
+ const candidates = [
235
+ blocker.resolvedAt,
236
+ blocker.resolved_at,
237
+ blocker.closedAt,
238
+ blocker.closed_at,
239
+ blocker.completedAt
240
+ ];
241
+ for (const value of candidates) {
242
+ const ms = safeParseToMs(value);
243
+ if (Number.isFinite(ms)) return ms;
244
+ }
245
+ return NaN;
246
+ }
247
+
248
+ function isOpen(blocker) {
249
+ const status = normalizeStatus(blocker);
250
+ if (RESOLVED_STATUSES.has(status)) return false;
251
+ const resolvedAt = getResolvedAt(blocker);
252
+ return !Number.isFinite(resolvedAt);
253
+ }
254
+
255
+ function loadBlockers() {
256
+ if (!fs.existsSync(BLOCKERS_FILE)) return [];
257
+ const result = safeReadJson(BLOCKERS_FILE);
258
+ if (!result.ok) {
259
+ const relativePath = path.relative(DATA_DIR, BLOCKERS_FILE);
260
+ if (result.error.type === 'parse') {
261
+ quarantineCorruptedFile(BLOCKERS_FILE, result.error.message);
262
+ console.warn(`⚠️ [${relativePath}] JSON parse failed; quarantined to _corrupted.`);
263
+ } else {
264
+ console.error(`Error reading blockers: ${result.error.message}`);
265
+ }
266
+ return [];
267
+ }
268
+ return Array.isArray(result.json.blockers) ? result.json.blockers : [];
269
+ }
270
+
271
+ function getBlockers(start, end) {
272
+ const blockers = loadBlockers();
273
+ const open = blockers.filter(isOpen);
274
+ open.sort((a, b) => {
275
+ const severityA = normalizeSeverity(a);
276
+ const severityB = normalizeSeverity(b);
277
+ const rankA = SEVERITY_ORDER[severityA] ?? 99;
278
+ const rankB = SEVERITY_ORDER[severityB] ?? 99;
279
+ if (rankA !== rankB) return rankA - rankB;
280
+ const createdA = getCreatedAt(a);
281
+ const createdB = getCreatedAt(b);
282
+ const msA = Number.isFinite(createdA) ? createdA : Number.MAX_SAFE_INTEGER;
283
+ const msB = Number.isFinite(createdB) ? createdB : Number.MAX_SAFE_INTEGER;
284
+ return msA - msB;
285
+ });
286
+
287
+ const openedRecent = blockers.filter(blocker => isWithinRange(getCreatedAt(blocker), start, end));
288
+ const resolvedRecent = blockers.filter(blocker => isWithinRange(getResolvedAt(blocker), start, end));
289
+
290
+ return { open, openedRecent, resolvedRecent };
291
+ }
292
+
155
293
  // --- Report Generation ---
156
294
 
157
295
  function generateReport(period) {
@@ -162,7 +300,8 @@ function generateReport(period) {
162
300
 
163
301
  const tasks = getTasks(start, end);
164
302
  const projects = getProjectUpdates(start, end);
165
- // const logs = getDailyLogs(start, end); // Maybe too verbose to include raw logs, let's stick to summarized data
303
+ const logs = getDailyLogs(start, end);
304
+ const blockers = getBlockers(start, end);
166
305
 
167
306
  let md = `# Relatório de Status Profissional - ${dateStr}\n`;
168
307
  md += `**Período:** ${formatDate(start)} a ${formatDate(end)}\n\n`;
@@ -171,9 +310,25 @@ function generateReport(period) {
171
310
  md += `## 📋 Resumo Executivo\n`;
172
311
  const totalDone = tasks.completed.length;
173
312
  const activeProjects = projects.length;
174
- md += `Neste período, foram concluídas **${totalDone}** entregas focais. Atualmente há **${activeProjects}** projetos com atualizações recentes.\n\n`;
313
+ const logCount = logs.length;
314
+ const openBlockers = blockers.open.length;
315
+ md += `Neste período, foram concluídas **${totalDone}** entregas focais. Atualmente há **${activeProjects}** projetos com atualizações recentes. Foram registrados **${logCount}** logs diários e existem **${openBlockers}** blockers em aberto.\n\n`;
316
+
317
+ // 2. Contexto dos Logs Diários
318
+ md += `## 📝 Contexto dos Logs Diários\n`;
319
+ if (logs.length === 0) {
320
+ md += `*Sem logs diários no período.*\n\n`;
321
+ } else {
322
+ logs
323
+ .sort((a, b) => a.date.localeCompare(b.date))
324
+ .forEach(log => {
325
+ const summary = summarizeLogContent(log.content) || 'Log registrado sem destaques.';
326
+ md += `- **${log.date}:** ${summary}\n`;
327
+ });
328
+ md += `\n`;
329
+ }
175
330
 
176
- // 2. Principais Entregas
331
+ // 3. Principais Entregas
177
332
  md += `## ✅ Principais Entregas\n`;
178
333
  if (tasks.completed.length === 0) {
179
334
  md += `*Nenhuma entrega registrada no período.*\n`;
@@ -185,7 +340,7 @@ function generateReport(period) {
185
340
  }
186
341
  md += `\n`;
187
342
 
188
- // 3. Status dos Projetos
343
+ // 4. Status dos Projetos
189
344
  md += `## 🏗️ Status dos Projetos\n`;
190
345
  if (projects.length === 0) {
191
346
  md += `*Sem atualizações de projeto recentes.*\n`;
@@ -204,7 +359,41 @@ function generateReport(period) {
204
359
  });
205
360
  }
206
361
 
207
- // 4. Próximos Passos
362
+ // 5. Bloqueios
363
+ md += `## 🚧 Bloqueios\n`;
364
+ if (blockers.open.length === 0) {
365
+ md += `*Nenhum blocker em aberto registrado.*\n`;
366
+ } else {
367
+ md += `**Em aberto:**\n`;
368
+ blockers.open.forEach(blocker => {
369
+ const title = getBlockerTitle(blocker);
370
+ const severity = normalizeSeverity(blocker);
371
+ const createdAt = getCreatedAt(blocker);
372
+ const createdDate = Number.isFinite(createdAt) ? toIsoDate(createdAt) : 'Unknown';
373
+ const project = blocker.project || blocker.projectName || blocker.projectSlug;
374
+ const client = blocker.client || blocker.clientName || blocker.clientSlug;
375
+ const metaParts = [
376
+ `Severidade: ${severity}`,
377
+ project ? `Projeto: ${project}` : null,
378
+ client ? `Cliente: ${client}` : null,
379
+ `Aberto: ${createdDate}`
380
+ ].filter(Boolean);
381
+ md += `- ${title} (${metaParts.join('; ')})\n`;
382
+ });
383
+ }
384
+
385
+ if (blockers.resolvedRecent.length > 0) {
386
+ md += `\n**Resolvidos no período:**\n`;
387
+ blockers.resolvedRecent.forEach(blocker => {
388
+ const title = getBlockerTitle(blocker);
389
+ const resolvedAt = getResolvedAt(blocker);
390
+ const resolvedDate = Number.isFinite(resolvedAt) ? toIsoDate(resolvedAt) : 'Unknown';
391
+ md += `- ${title} (Resolvido: ${resolvedDate})\n`;
392
+ });
393
+ }
394
+ md += `\n\n`;
395
+
396
+ // 6. Próximos Passos
208
397
  md += `## 🚀 Próximos Passos\n`;
209
398
  if (tasks.pending.length === 0) {
210
399
  md += `*Sem itens prioritários na fila.*\n`;
@@ -25,6 +25,12 @@ Do not invent status updates.
25
25
  * **Keywords:** "Tarefa", "Task", "To-Do", "Fazer", "Agenda", "Delegado".
26
26
  * **Target File:** `data/tasks/task-log.json`.
27
27
  * **Action:** Read file directly.
28
+ * **If Daily Log Query:**
29
+ * **Keywords:** "log diário", "diário", "daily log", "anotações", "o que anotei", "ontem", "hoje", "semana passada".
30
+ * **Target Folder:** `logs/daily/`.
31
+ * **If date provided:** Read `logs/daily/YYYY-MM-DD.md`.
32
+ * **If date is relative (hoje/ontem):** Resolve to an exact date and read the matching file.
33
+ * **If no specific date or file missing:** Route to **Search** across `logs/daily/` (or ask the user to refine the date). If Search is unavailable, say you have no records for that date and offer to list available log dates.
28
34
  * **If Project Query:**
29
35
  * Proceed to Project Lookup (Glob search).
30
36
  * **Strategy:** Search recursively in `data/Clients`.
@@ -46,7 +52,13 @@ Do not invent status updates.
46
52
  * **Context:** "Aqui estão suas tarefas pendentes [{Category}]:"
47
53
  * **List:** Bullet points.
48
54
  * Format: `* [ID-Short] {Description} ({Priority})`
49
- * **Empty:** "Você não tem tarefas pendentes nesta categoria."
55
+ * **Empty:** "Você não tem tarefas pendentes nesta categoria."
56
+
57
+ * **Daily Log Logic:**
58
+ * Read the Markdown file from `logs/daily/YYYY-MM-DD.md`.
59
+ * Return a concise excerpt (first few meaningful lines or bullet points).
60
+ * If the file is empty, say: "Log registrado sem detalhes para essa data."
61
+ * If the file does not exist: "Não encontrei log diário para esta data."
50
62
 
51
63
  * **Project Logic:**
52
64
  * If matches found: Read the `status.json` file(s).
@@ -96,6 +108,7 @@ Em atraso devido a condições climáticas.
96
108
  <persona>
97
109
  Maintain the F.R.E.Y.A. persona defined in `master.mdc`.
98
110
  Tone: Analytical, Precise, Data-Driven.
111
+ Obsidian: Quando citar projetos/notas, prefira nomes e links em formato Obsidian (wikilinks `[[...]]`) quando aplicável.
99
112
  Signature:
100
113
  — FREYA
101
114
  Assistente Responsiva com Otimização Aprimorada
@@ -8,7 +8,7 @@ F.R.E.Y.A. é um sistema de agentes de IA projetado para organizar seu trabalho,
8
8
 
9
9
  * **Ingestão Universal:** Registre updates, blockers e notas mentais em linguagem natural.
10
10
  * **Gestão de Tarefas:** Crie, liste e conclua tarefas ("Lembre-me de fazer X", "Minhas tarefas", "Terminei X").
11
- * **Oráculo:** Pergunte sobre o status de qualquer projeto ("Como está o projeto X?").
11
+ * **Oráculo:** Pergunte sobre o status de qualquer projeto ("Como está o projeto X?") e recupere logs diários ("O que anotei ontem?").
12
12
  * **Career Coach:** Gere "Brag Sheets" automáticas para suas avaliações de desempenho.
13
13
  * **Relatórios Automatizados:** Gere resumos semanais, dailies, relatório de Scrum Master e relatórios executivos.
14
14
  * **Blockers & Riscos:** Gere um relatório rápido de blockers priorizados por severidade.
@@ -16,9 +16,68 @@ F.R.E.Y.A. é um sistema de agentes de IA projetado para organizar seu trabalho,
16
16
  * **Git Automation:** Gere commits inteligentes automaticamente. A Freya analisa suas mudanças e escreve a mensagem para você.
17
17
  * **Privacidade Total:** Seus dados (JSON e Markdown) ficam 100% locais na sua máquina.
18
18
 
19
+ ## 📦 Instalação (CLI)
20
+
21
+ Você pode usar a FREYA como um CLI para **inicializar uma workspace** completa (agents + scripts + data) em qualquer diretório.
22
+
23
+ ## 🚢 Publicação no npm (maintainers)
24
+
25
+ Este repositório suporta publicação automática via GitHub Actions.
26
+
27
+ ### Pré-requisitos
28
+ 1) Ter permissão de publish no pacote `@cccarv82/freya` no npm.
29
+ 2) Criar o secret no GitHub: `NPM_TOKEN` (Automation token do npm com permissão de publish).
30
+
31
+ ### Como publicar
32
+ 1) Atualize a versão e crie uma tag `vX.Y.Z`:
33
+ ```bash
34
+ npm version patch
35
+ # ou minor/major
36
+
37
+ git push --follow-tags
38
+ ```
39
+ 2) A Action `npm-publish` roda no push da tag e executa `npm publish --access public`.
40
+
41
+ ### Via npx (recomendado)
42
+ ```bash
43
+ npx @cccarv82/freya init
44
+ # cria ./freya
45
+ ```
46
+
47
+ ### Via instalação global
48
+ ```bash
49
+ npm i -g @cccarv82/freya
50
+ freya init
51
+ # cria ./freya
52
+ ```
53
+
54
+ ### Modos do `init`
55
+ ```bash
56
+ freya init # cria ./freya
57
+ freya init meu-projeto # cria ./meu-projeto
58
+ freya init --here # instala no diretório atual
59
+ ```
60
+
61
+ ### Atualizar uma workspace existente (sem perder dados)
62
+ Por padrão, ao rodar `init` em uma pasta existente, o CLI **preserva**:
63
+ - `data/**`
64
+ - `logs/**`
65
+
66
+ E atualiza/instala normalmente:
67
+ - `.agent/**`
68
+ - `scripts/**`
69
+ - `README.md`, `USER_GUIDE.md`
70
+ - `package.json` (merge de scripts)
71
+
72
+ Flags (use com cuidado):
73
+ ```bash
74
+ freya init --here --force-data # permite sobrescrever data/
75
+ freya init --here --force-logs # permite sobrescrever logs/
76
+ ```
77
+
19
78
  ## 🚀 Como Usar
20
79
 
21
- 1. Abra esta pasta na **sua IDE**.
80
+ 1. Abra a pasta da workspace gerada (ex.: `./freya`) na **sua IDE**.
22
81
  2. No chat da IDE (ex: Ctrl+L / Cmd+L), digite:
23
82
  > `@freya Ajuda`
24
83
  3. Siga as instruções da assistente.
@@ -5,6 +5,39 @@ Este sistema foi projetado para ser seu assistente pessoal de produtividade, ope
5
5
 
6
6
  ## 🚀 Como Iniciar
7
7
 
8
+ ### 1) Criar uma workspace (CLI)
9
+ Você pode inicializar uma workspace completa (agents + scripts + data) em qualquer diretório.
10
+
11
+ **Via npx (recomendado):**
12
+ ```bash
13
+ npx @cccarv82/freya init
14
+ # cria ./freya
15
+ ```
16
+
17
+ **Via instalação global:**
18
+ ```bash
19
+ npm i -g @cccarv82/freya
20
+ freya init
21
+ # cria ./freya
22
+ ```
23
+
24
+ **Modos do init:**
25
+ ```bash
26
+ freya init # cria ./freya
27
+ freya init meu-projeto # cria ./meu-projeto
28
+ freya init --here # instala no diretório atual
29
+ ```
30
+
31
+ **Upgrade sem perder dados (recomendado):**
32
+ Ao rodar `init` em uma pasta já existente, o CLI **preserva automaticamente** `data/**` e `logs/**` (se não estiverem vazios) e atualiza o restante.
33
+
34
+ Se você quiser sobrescrever explicitamente:
35
+ ```bash
36
+ freya init --here --force-data
37
+ freya init --here --force-logs
38
+ ```
39
+
40
+ ### 2) Interagir no chat da IDE
8
41
  Para interagir com a assistente, basta chamá-la no chat da sua IDE:
9
42
 
10
43
  > `@freya [sua mensagem]`
@@ -39,6 +72,10 @@ Recupere o contexto de qualquer projeto instantaneamente.
39
72
  > "Como está o projeto Vivo 5G?"
40
73
  * *Resultado:* Resumo executivo do status atual e das últimas 3 atualizações.
41
74
 
75
+ * **Consulta de Logs Diários:**
76
+ > "O que anotei ontem?"
77
+ * *Resultado:* Retorna um trecho do log diário em `logs/daily/YYYY-MM-DD.md` (ou direciona para busca quando a data não estiver clara).
78
+
42
79
  * **Anti-Alucinação:**
43
80
  A FREYA sempre citará a fonte da informação (ex: `(Source: data/Clients/vivo/5g/status.json)`). Se ela não souber, ela dirá explicitamente.
44
81
 
@@ -80,7 +117,7 @@ Transforme seus logs em relatórios úteis sem esforço. Peça à FREYA no chat
80
117
 
81
118
  * **Relatório de Status Profissional (Executivo):**
82
119
  > "Gerar status report", "Relatório Executivo"
83
- * *Resultado:* Gera um relatório Markdown completo com Resumo Executivo, Entregas, Status de Projetos e Bloqueios. Ideal para enviar stakeholders.
120
+ * *Resultado:* Gera um relatório Markdown completo com Resumo Executivo, Contexto dos Logs Diários, Entregas, Status de Projetos e Bloqueios. Ideal para enviar stakeholders.
84
121
  * *Manual:* `npm run status -- --period [daily|weekly]`
85
122
 
86
123
  * **Relatório Scrum Master (Semanal):**
@@ -9,7 +9,7 @@
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
 
12
- const { toIsoDate, isWithinRange } = require('./lib/date-utils');
12
+ const { toIsoDate, isWithinRange, safeParseToMs } = require('./lib/date-utils');
13
13
  const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
14
14
 
15
15
  // --- Configuration ---
@@ -17,6 +17,15 @@ const DATA_DIR = path.join(__dirname, '../data');
17
17
  const LOGS_DIR = path.join(__dirname, '../logs/daily');
18
18
  const OUTPUT_DIR = path.join(__dirname, '../docs/reports');
19
19
  const TASKS_FILE = path.join(DATA_DIR, 'tasks/task-log.json');
20
+ const BLOCKERS_FILE = path.join(DATA_DIR, 'blockers/blocker-log.json');
21
+
22
+ const RESOLVED_STATUSES = new Set(['RESOLVED', 'CLOSED', 'DONE', 'FIXED']);
23
+ const SEVERITY_ORDER = {
24
+ CRITICAL: 0,
25
+ HIGH: 1,
26
+ MEDIUM: 2,
27
+ LOW: 3
28
+ };
20
29
 
21
30
  // --- Helpers ---
22
31
  function getDateRange(period) {
@@ -152,6 +161,135 @@ function getDailyLogs(start, end) {
152
161
  return relevantLogs;
153
162
  }
154
163
 
164
+ function summarizeLogContent(content, maxLines = 3, maxChars = 280) {
165
+ if (!content) return null;
166
+ const lines = content
167
+ .split('\n')
168
+ .map(line => line.trim())
169
+ .filter(line => line && !line.startsWith('#'));
170
+
171
+ const highlights = [];
172
+ for (const line of lines) {
173
+ const cleaned = line.replace(/^[-*]\s+/, '').replace(/^>\s+/, '').trim();
174
+ if (!cleaned) continue;
175
+ highlights.push(cleaned);
176
+ if (highlights.length >= maxLines) break;
177
+ }
178
+
179
+ if (highlights.length === 0) return null;
180
+ let summary = highlights.join('; ');
181
+ if (summary.length > maxChars) {
182
+ summary = summary.slice(0, Math.max(0, maxChars - 3)).trim() + '...';
183
+ }
184
+ return summary;
185
+ }
186
+
187
+ function normalizeStatus(blocker) {
188
+ const raw = blocker.status || blocker.state || blocker.currentStatus;
189
+ if (!raw) return 'UNKNOWN';
190
+ return String(raw).trim().toUpperCase();
191
+ }
192
+
193
+ function normalizeSeverity(blocker) {
194
+ const raw = blocker.severity || blocker.priority || blocker.level;
195
+ if (!raw) return 'UNSPECIFIED';
196
+ const value = String(raw).trim().toUpperCase();
197
+ if (value.includes('CRIT')) return 'CRITICAL';
198
+ if (value.includes('HIGH')) return 'HIGH';
199
+ if (value.includes('MED')) return 'MEDIUM';
200
+ if (value.includes('LOW')) return 'LOW';
201
+ return value;
202
+ }
203
+
204
+ function getBlockerTitle(blocker) {
205
+ return (
206
+ blocker.title ||
207
+ blocker.summary ||
208
+ blocker.description ||
209
+ blocker.content ||
210
+ blocker.text ||
211
+ 'Untitled blocker'
212
+ );
213
+ }
214
+
215
+ function getCreatedAt(blocker) {
216
+ const candidates = [
217
+ blocker.createdAt,
218
+ blocker.created_at,
219
+ blocker.openedAt,
220
+ blocker.opened_at,
221
+ blocker.reportedAt,
222
+ blocker.reported_at,
223
+ blocker.date,
224
+ blocker.loggedAt
225
+ ];
226
+ for (const value of candidates) {
227
+ const ms = safeParseToMs(value);
228
+ if (Number.isFinite(ms)) return ms;
229
+ }
230
+ return NaN;
231
+ }
232
+
233
+ function getResolvedAt(blocker) {
234
+ const candidates = [
235
+ blocker.resolvedAt,
236
+ blocker.resolved_at,
237
+ blocker.closedAt,
238
+ blocker.closed_at,
239
+ blocker.completedAt
240
+ ];
241
+ for (const value of candidates) {
242
+ const ms = safeParseToMs(value);
243
+ if (Number.isFinite(ms)) return ms;
244
+ }
245
+ return NaN;
246
+ }
247
+
248
+ function isOpen(blocker) {
249
+ const status = normalizeStatus(blocker);
250
+ if (RESOLVED_STATUSES.has(status)) return false;
251
+ const resolvedAt = getResolvedAt(blocker);
252
+ return !Number.isFinite(resolvedAt);
253
+ }
254
+
255
+ function loadBlockers() {
256
+ if (!fs.existsSync(BLOCKERS_FILE)) return [];
257
+ const result = safeReadJson(BLOCKERS_FILE);
258
+ if (!result.ok) {
259
+ const relativePath = path.relative(DATA_DIR, BLOCKERS_FILE);
260
+ if (result.error.type === 'parse') {
261
+ quarantineCorruptedFile(BLOCKERS_FILE, result.error.message);
262
+ console.warn(`⚠️ [${relativePath}] JSON parse failed; quarantined to _corrupted.`);
263
+ } else {
264
+ console.error(`Error reading blockers: ${result.error.message}`);
265
+ }
266
+ return [];
267
+ }
268
+ return Array.isArray(result.json.blockers) ? result.json.blockers : [];
269
+ }
270
+
271
+ function getBlockers(start, end) {
272
+ const blockers = loadBlockers();
273
+ const open = blockers.filter(isOpen);
274
+ open.sort((a, b) => {
275
+ const severityA = normalizeSeverity(a);
276
+ const severityB = normalizeSeverity(b);
277
+ const rankA = SEVERITY_ORDER[severityA] ?? 99;
278
+ const rankB = SEVERITY_ORDER[severityB] ?? 99;
279
+ if (rankA !== rankB) return rankA - rankB;
280
+ const createdA = getCreatedAt(a);
281
+ const createdB = getCreatedAt(b);
282
+ const msA = Number.isFinite(createdA) ? createdA : Number.MAX_SAFE_INTEGER;
283
+ const msB = Number.isFinite(createdB) ? createdB : Number.MAX_SAFE_INTEGER;
284
+ return msA - msB;
285
+ });
286
+
287
+ const openedRecent = blockers.filter(blocker => isWithinRange(getCreatedAt(blocker), start, end));
288
+ const resolvedRecent = blockers.filter(blocker => isWithinRange(getResolvedAt(blocker), start, end));
289
+
290
+ return { open, openedRecent, resolvedRecent };
291
+ }
292
+
155
293
  // --- Report Generation ---
156
294
 
157
295
  function generateReport(period) {
@@ -162,7 +300,8 @@ function generateReport(period) {
162
300
 
163
301
  const tasks = getTasks(start, end);
164
302
  const projects = getProjectUpdates(start, end);
165
- // const logs = getDailyLogs(start, end); // Maybe too verbose to include raw logs, let's stick to summarized data
303
+ const logs = getDailyLogs(start, end);
304
+ const blockers = getBlockers(start, end);
166
305
 
167
306
  let md = `# Relatório de Status Profissional - ${dateStr}\n`;
168
307
  md += `**Período:** ${formatDate(start)} a ${formatDate(end)}\n\n`;
@@ -171,9 +310,25 @@ function generateReport(period) {
171
310
  md += `## 📋 Resumo Executivo\n`;
172
311
  const totalDone = tasks.completed.length;
173
312
  const activeProjects = projects.length;
174
- md += `Neste período, foram concluídas **${totalDone}** entregas focais. Atualmente há **${activeProjects}** projetos com atualizações recentes.\n\n`;
313
+ const logCount = logs.length;
314
+ const openBlockers = blockers.open.length;
315
+ md += `Neste período, foram concluídas **${totalDone}** entregas focais. Atualmente há **${activeProjects}** projetos com atualizações recentes. Foram registrados **${logCount}** logs diários e existem **${openBlockers}** blockers em aberto.\n\n`;
316
+
317
+ // 2. Contexto dos Logs Diários
318
+ md += `## 📝 Contexto dos Logs Diários\n`;
319
+ if (logs.length === 0) {
320
+ md += `*Sem logs diários no período.*\n\n`;
321
+ } else {
322
+ logs
323
+ .sort((a, b) => a.date.localeCompare(b.date))
324
+ .forEach(log => {
325
+ const summary = summarizeLogContent(log.content) || 'Log registrado sem destaques.';
326
+ md += `- **${log.date}:** ${summary}\n`;
327
+ });
328
+ md += `\n`;
329
+ }
175
330
 
176
- // 2. Principais Entregas
331
+ // 3. Principais Entregas
177
332
  md += `## ✅ Principais Entregas\n`;
178
333
  if (tasks.completed.length === 0) {
179
334
  md += `*Nenhuma entrega registrada no período.*\n`;
@@ -185,7 +340,7 @@ function generateReport(period) {
185
340
  }
186
341
  md += `\n`;
187
342
 
188
- // 3. Status dos Projetos
343
+ // 4. Status dos Projetos
189
344
  md += `## 🏗️ Status dos Projetos\n`;
190
345
  if (projects.length === 0) {
191
346
  md += `*Sem atualizações de projeto recentes.*\n`;
@@ -204,7 +359,41 @@ function generateReport(period) {
204
359
  });
205
360
  }
206
361
 
207
- // 4. Próximos Passos
362
+ // 5. Bloqueios
363
+ md += `## 🚧 Bloqueios\n`;
364
+ if (blockers.open.length === 0) {
365
+ md += `*Nenhum blocker em aberto registrado.*\n`;
366
+ } else {
367
+ md += `**Em aberto:**\n`;
368
+ blockers.open.forEach(blocker => {
369
+ const title = getBlockerTitle(blocker);
370
+ const severity = normalizeSeverity(blocker);
371
+ const createdAt = getCreatedAt(blocker);
372
+ const createdDate = Number.isFinite(createdAt) ? toIsoDate(createdAt) : 'Unknown';
373
+ const project = blocker.project || blocker.projectName || blocker.projectSlug;
374
+ const client = blocker.client || blocker.clientName || blocker.clientSlug;
375
+ const metaParts = [
376
+ `Severidade: ${severity}`,
377
+ project ? `Projeto: ${project}` : null,
378
+ client ? `Cliente: ${client}` : null,
379
+ `Aberto: ${createdDate}`
380
+ ].filter(Boolean);
381
+ md += `- ${title} (${metaParts.join('; ')})\n`;
382
+ });
383
+ }
384
+
385
+ if (blockers.resolvedRecent.length > 0) {
386
+ md += `\n**Resolvidos no período:**\n`;
387
+ blockers.resolvedRecent.forEach(blocker => {
388
+ const title = getBlockerTitle(blocker);
389
+ const resolvedAt = getResolvedAt(blocker);
390
+ const resolvedDate = Number.isFinite(resolvedAt) ? toIsoDate(resolvedAt) : 'Unknown';
391
+ md += `- ${title} (Resolvido: ${resolvedDate})\n`;
392
+ });
393
+ }
394
+ md += `\n\n`;
395
+
396
+ // 6. Próximos Passos
208
397
  md += `## 🚀 Próximos Passos\n`;
209
398
  if (tasks.pending.length === 0) {
210
399
  md += `*Sem itens prioritários na fila.*\n`;