@cccarv82/freya 2.1.6 → 2.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/.agent/rules/freya/freya.mdc +8 -0
- package/cli/init.js +1 -1
- package/cli/web-ui.js +56 -0
- package/cli/web.js +124 -1
- package/package.json +3 -3
- package/scripts/validate-structure.js +151 -0
- package/templates/base/.agent/rules/freya/agents/ingestor.mdc +8 -0
- package/templates/base/.agent/rules/freya/agents/master.mdc +5 -0
- package/templates/base/docs/career/Career Hub.md +13 -0
- package/templates/base/docs/reports/Reports Hub.md +13 -0
- package/templates/base/docs/standards/Standards Hub.md +11 -0
- package/templates/base/scripts/validate-structure.js +151 -0
|
@@ -35,3 +35,11 @@ Como posso ajudar você hoje?
|
|
|
35
35
|
[4] General Assistance
|
|
36
36
|
```
|
|
37
37
|
</menu-display>
|
|
38
|
+
|
|
39
|
+
## Registro Padrao
|
|
40
|
+
|
|
41
|
+
- Logs diarios (raw input, notas cronologicas): `logs/daily/YYYY-MM-DD.md`
|
|
42
|
+
- Dados estruturados (status, tarefas, carreira): `data/**`
|
|
43
|
+
- Sintese e navegacao (hubs, reports): `docs/**`
|
|
44
|
+
|
|
45
|
+
Regra: nunca gravar logs diarios em data/ ou docs/. Nunca gravar dados estruturados em logs/.
|
package/cli/init.js
CHANGED
|
@@ -81,7 +81,7 @@ function ensurePackageJson(targetDir, force, summary) {
|
|
|
81
81
|
const existing = readJsonSafe(pkgPath);
|
|
82
82
|
|
|
83
83
|
const scriptsToEnsure = {
|
|
84
|
-
health: 'node scripts/validate-data.js',
|
|
84
|
+
health: 'node scripts/validate-data.js && node scripts/validate-structure.js',
|
|
85
85
|
migrate: 'node scripts/migrate-data.js',
|
|
86
86
|
report: 'node scripts/generate-weekly-report.js',
|
|
87
87
|
'sm-weekly': 'node scripts/generate-sm-weekly-report.js',
|
package/cli/web-ui.js
CHANGED
|
@@ -656,6 +656,48 @@
|
|
|
656
656
|
grid.appendChild(card); }
|
|
657
657
|
}
|
|
658
658
|
|
|
659
|
+
function renderProjects() {
|
|
660
|
+
const el = $('projectsGrid');
|
|
661
|
+
if (!el) return;
|
|
662
|
+
const filter = String(($('projectsFilter') && $('projectsFilter').value) || '').toLowerCase();
|
|
663
|
+
const items = Array.isArray(state.projects) ? state.projects : [];
|
|
664
|
+
const filtered = items.filter((p) => {
|
|
665
|
+
const hay = [p.client, p.program, p.stream, p.project, p.slug, (p.tags||[]).join(' ')].join(' ').toLowerCase();
|
|
666
|
+
return !filter || hay.includes(filter);
|
|
667
|
+
});
|
|
668
|
+
el.innerHTML = '';
|
|
669
|
+
for (const p of filtered) {
|
|
670
|
+
const card = document.createElement('div');
|
|
671
|
+
card.className = 'reportCard';
|
|
672
|
+
card.innerHTML = '<div class="reportHead">'
|
|
673
|
+
+ '<div><div class="reportTitle">' + escapeHtml(p.project || p.slug || 'Projeto') + '</div>'
|
|
674
|
+
+ '<div class="reportMeta">' + escapeHtml([p.client, p.program, p.stream].filter(Boolean).join(' · ')) + '</div></div>'
|
|
675
|
+
+ '<div class="reportActions">' + (p.active ? '<span class="pill ok">ativo</span>' : '<span class="pill warn">inativo</span>') + '</div>'
|
|
676
|
+
+ '</div>'
|
|
677
|
+
+ '<div class="help" style="margin-top:8px">' + escapeHtml(p.currentStatus || 'Sem status') + '</div>'
|
|
678
|
+
+ '<div class="reportMeta" style="margin-top:8px">Última atualização: ' + escapeHtml(p.lastUpdated || '—') + '</div>'
|
|
679
|
+
+ '<div class="reportMeta">Eventos: ' + escapeHtml(String(p.historyCount || 0)) + '</div>';
|
|
680
|
+
el.appendChild(card);
|
|
681
|
+
}
|
|
682
|
+
if (!filtered.length) {
|
|
683
|
+
const empty = document.createElement('div');
|
|
684
|
+
empty.className = 'help';
|
|
685
|
+
empty.textContent = 'Nenhum projeto encontrado.';
|
|
686
|
+
el.appendChild(empty);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function refreshProjects() {
|
|
691
|
+
try {
|
|
692
|
+
const r = await api('/api/projects/list', { dir: dirOrDefault() });
|
|
693
|
+
state.projects = r.projects || [];
|
|
694
|
+
renderProjects();
|
|
695
|
+
} catch (e) {
|
|
696
|
+
const el = $('projectsGrid');
|
|
697
|
+
if (el) el.textContent = 'Falha ao carregar projetos.';
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
659
701
|
async function refreshReportsPage() {
|
|
660
702
|
try {
|
|
661
703
|
setPill('run', 'carregando…');
|
|
@@ -680,6 +722,7 @@
|
|
|
680
722
|
function wireRailNav() {
|
|
681
723
|
const dash = $('railDashboard');
|
|
682
724
|
const rep = $('railReports');
|
|
725
|
+
const proj = $('railProjects');
|
|
683
726
|
const health = $('railCompanion');
|
|
684
727
|
if (dash) {
|
|
685
728
|
dash.onclick = () => {
|
|
@@ -698,6 +741,12 @@
|
|
|
698
741
|
if (!isReports) window.location.href = '/reports';
|
|
699
742
|
};
|
|
700
743
|
}
|
|
744
|
+
if (proj) {
|
|
745
|
+
proj.onclick = () => {
|
|
746
|
+
const isProjects = document.body && document.body.dataset && document.body.dataset.page === 'projects';
|
|
747
|
+
if (!isProjects) window.location.href = '/projects';
|
|
748
|
+
};
|
|
749
|
+
}
|
|
701
750
|
if (health) {
|
|
702
751
|
health.onclick = () => {
|
|
703
752
|
const isHealth = document.body && document.body.dataset && document.body.dataset.page === 'companion';
|
|
@@ -1314,6 +1363,7 @@
|
|
|
1314
1363
|
} catch {}
|
|
1315
1364
|
|
|
1316
1365
|
const isReportsPage = document.body && document.body.dataset && document.body.dataset.page === 'reports';
|
|
1366
|
+
const isProjectsPage = document.body && document.body.dataset && document.body.dataset.page === 'projects';
|
|
1317
1367
|
const isCompanionPage = document.body && document.body.dataset && document.body.dataset.page === 'companion';
|
|
1318
1368
|
|
|
1319
1369
|
// Load persisted settings from the workspace + bootstrap (auto-init + auto-health)
|
|
@@ -1342,6 +1392,11 @@
|
|
|
1342
1392
|
return;
|
|
1343
1393
|
}
|
|
1344
1394
|
|
|
1395
|
+
if (isProjectsPage) {
|
|
1396
|
+
await refreshProjects();
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1345
1400
|
if (isCompanionPage) {
|
|
1346
1401
|
await refreshHealthChecklist();
|
|
1347
1402
|
return;
|
|
@@ -1385,6 +1440,7 @@
|
|
|
1385
1440
|
window.renderReportsList = renderReportsList;
|
|
1386
1441
|
window.renderReportsPage = renderReportsPage;
|
|
1387
1442
|
window.refreshReportsPage = refreshReportsPage;
|
|
1443
|
+
window.refreshProjects = refreshProjects;
|
|
1388
1444
|
window.refreshBlockersInsights = refreshBlockersInsights;
|
|
1389
1445
|
window.refreshHealthChecklist = refreshHealthChecklist;
|
|
1390
1446
|
window.copyOut = copyOut;
|
package/cli/web.js
CHANGED
|
@@ -905,6 +905,7 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
905
905
|
<button class="railBtn active" id="railDashboard" type="button" title="Dashboard">D</button>
|
|
906
906
|
<button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
|
|
907
907
|
<button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
|
|
908
|
+
<button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
|
|
908
909
|
</div>
|
|
909
910
|
<div class="railBottom">
|
|
910
911
|
<div class="railStatus" id="railStatus" title="status"></div>
|
|
@@ -1155,6 +1156,7 @@ function buildReportsHtml(safeDefault, appVersion) {
|
|
|
1155
1156
|
<button class="railBtn" id="railDashboard" type="button" title="Dashboard">D</button>
|
|
1156
1157
|
<button class="railBtn active" id="railReports" type="button" title="Relatórios">R</button>
|
|
1157
1158
|
<button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
|
|
1159
|
+
<button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
|
|
1158
1160
|
</div>
|
|
1159
1161
|
<div class="railBottom">
|
|
1160
1162
|
<div class="railStatus" id="railStatus" title="status"></div>
|
|
@@ -1209,6 +1211,84 @@ function buildReportsHtml(safeDefault, appVersion) {
|
|
|
1209
1211
|
</html>`
|
|
1210
1212
|
}
|
|
1211
1213
|
|
|
1214
|
+
function buildProjectsHtml(safeDefault, appVersion) {
|
|
1215
|
+
const safeVersion = escapeHtml(appVersion || 'unknown');
|
|
1216
|
+
return `<!doctype html>
|
|
1217
|
+
<html>
|
|
1218
|
+
<head>
|
|
1219
|
+
<meta charset="utf-8" />
|
|
1220
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1221
|
+
<title>Projects</title>
|
|
1222
|
+
<link rel="stylesheet" href="/app.css" />
|
|
1223
|
+
</head>
|
|
1224
|
+
<body data-page="projects">
|
|
1225
|
+
<div class="app">
|
|
1226
|
+
<div class="frame">
|
|
1227
|
+
<div class="shell">
|
|
1228
|
+
|
|
1229
|
+
<aside class="rail">
|
|
1230
|
+
<div class="railTop">
|
|
1231
|
+
<div class="railLogo">F</div>
|
|
1232
|
+
</div>
|
|
1233
|
+
<div class="railNav">
|
|
1234
|
+
<button class="railBtn" id="railDashboard" type="button" title="Dashboard">D</button>
|
|
1235
|
+
<button class="railBtn" id="railReports" type="button" title="Relatorios">R</button>
|
|
1236
|
+
<button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
|
|
1237
|
+
<button class="railBtn active" id="railProjects" type="button" title="Projects">P</button>
|
|
1238
|
+
</div>
|
|
1239
|
+
<div class="railBottom">
|
|
1240
|
+
<div class="railStatus" id="railStatus" title="status"></div>
|
|
1241
|
+
</div>
|
|
1242
|
+
</aside>
|
|
1243
|
+
|
|
1244
|
+
<main class="center reportsPage" id="projectsPage">
|
|
1245
|
+
<div class="topbar">
|
|
1246
|
+
<div class="brandLine">
|
|
1247
|
+
<span class="spark"></span>
|
|
1248
|
+
<div class="brandStack">
|
|
1249
|
+
<div class="brand">FREYA</div>
|
|
1250
|
+
<div class="brandSub">Projects</div>
|
|
1251
|
+
</div>
|
|
1252
|
+
</div>
|
|
1253
|
+
<div class="topActions">
|
|
1254
|
+
<span class="chip" id="chipVersion">v${safeVersion}</span>
|
|
1255
|
+
<span class="chip" id="chipPort">127.0.0.1:3872</span>
|
|
1256
|
+
</div>
|
|
1257
|
+
</div>
|
|
1258
|
+
|
|
1259
|
+
<div class="centerBody">
|
|
1260
|
+
<input id="dir" type="hidden" />
|
|
1261
|
+
|
|
1262
|
+
<section class="reportsHeader">
|
|
1263
|
+
<div>
|
|
1264
|
+
<div class="reportsTitle">Projects</div>
|
|
1265
|
+
<div class="reportsSubtitle">Status por projeto com ultima atualizacao e riscos.</div>
|
|
1266
|
+
</div>
|
|
1267
|
+
<div class="reportsActions">
|
|
1268
|
+
<button class="btn small" type="button" onclick="refreshProjects()">Atualizar</button>
|
|
1269
|
+
</div>
|
|
1270
|
+
</section>
|
|
1271
|
+
|
|
1272
|
+
<section class="reportsTools">
|
|
1273
|
+
<input id="projectsFilter" placeholder="filtrar (cliente, projeto, tag)" oninput="renderProjects()" />
|
|
1274
|
+
</section>
|
|
1275
|
+
|
|
1276
|
+
<section class="reportsGrid" id="projectsGrid"></section>
|
|
1277
|
+
</div>
|
|
1278
|
+
</main>
|
|
1279
|
+
|
|
1280
|
+
</div>
|
|
1281
|
+
</div>
|
|
1282
|
+
</div>
|
|
1283
|
+
|
|
1284
|
+
<script>
|
|
1285
|
+
window.__FREYA_DEFAULT_DIR = "${safeDefault}";
|
|
1286
|
+
</script>
|
|
1287
|
+
<script src="/app.js"></script>
|
|
1288
|
+
</body>
|
|
1289
|
+
</html>`;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1212
1292
|
function buildCompanionHtml(safeDefault, appVersion) {
|
|
1213
1293
|
const safeVersion = escapeHtml(appVersion || 'unknown');
|
|
1214
1294
|
return `<!doctype html>
|
|
@@ -1232,6 +1312,7 @@ function buildCompanionHtml(safeDefault, appVersion) {
|
|
|
1232
1312
|
<button class="railBtn" id="railDashboard" type="button" title="Dashboard">D</button>
|
|
1233
1313
|
<button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
|
|
1234
1314
|
<button class="railBtn active" id="railCompanion" type="button" title="Companion">C</button>
|
|
1315
|
+
<button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
|
|
1235
1316
|
</div>
|
|
1236
1317
|
<div class="railBottom">
|
|
1237
1318
|
<div class="railStatus" id="railStatus" title="status"></div>
|
|
@@ -1616,6 +1697,14 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1616
1697
|
return;
|
|
1617
1698
|
}
|
|
1618
1699
|
|
|
1700
|
+
if (req.method === 'GET' && req.url === '/projects') {
|
|
1701
|
+
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
|
|
1702
|
+
const body = projectsHtml(dir || './freya');
|
|
1703
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1704
|
+
res.end(body);
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1619
1708
|
if (req.method === 'GET' && req.url === '/app.css') {
|
|
1620
1709
|
const css = fs.readFileSync(path.join(__dirname, 'web-ui.css'), 'utf8');
|
|
1621
1710
|
res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
@@ -1701,7 +1790,41 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1701
1790
|
return safeJson(res, 200, { ok: true, map: out });
|
|
1702
1791
|
}
|
|
1703
1792
|
|
|
1704
|
-
if (req.url === '/api/
|
|
1793
|
+
if (req.url === '/api/projects/list') {
|
|
1794
|
+
const base = path.join(workspaceDir, 'data', 'Clients');
|
|
1795
|
+
const items = [];
|
|
1796
|
+
if (exists(base)) {
|
|
1797
|
+
const stack = [base];
|
|
1798
|
+
while (stack.length) {
|
|
1799
|
+
const dirp = stack.pop();
|
|
1800
|
+
const entries = fs.readdirSync(dirp, { withFileTypes: true });
|
|
1801
|
+
for (const ent of entries) {
|
|
1802
|
+
const full = path.join(dirp, ent.name);
|
|
1803
|
+
if (ent.isDirectory()) stack.push(full);
|
|
1804
|
+
else if (ent.isFile() && ent.name === 'status.json') {
|
|
1805
|
+
const doc = readJsonOrNull(full) || {};
|
|
1806
|
+
const rel = path.relative(base, path.dirname(full)).replace(/\\/g, '/');
|
|
1807
|
+
items.push({
|
|
1808
|
+
slug: rel,
|
|
1809
|
+
client: doc.client || null,
|
|
1810
|
+
program: doc.program || null,
|
|
1811
|
+
stream: doc.stream || null,
|
|
1812
|
+
project: doc.project || null,
|
|
1813
|
+
active: doc.active !== false,
|
|
1814
|
+
currentStatus: doc.currentStatus || '',
|
|
1815
|
+
lastUpdated: doc.lastUpdated || '',
|
|
1816
|
+
tags: Array.isArray(doc.tags) ? doc.tags : [],
|
|
1817
|
+
historyCount: Array.isArray(doc.history) ? doc.history.length : 0
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
items.sort((a,b)=> String(b.lastUpdated||'').localeCompare(String(a.lastUpdated||'')));
|
|
1824
|
+
return safeJson(res, 200, { ok: true, projects: items });
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
if (req.url === '/api/reports/list') {
|
|
1705
1828
|
const reports = listReports(workspaceDir);
|
|
1706
1829
|
return safeJson(res, 200, { reports });
|
|
1707
1830
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cccarv82/freya",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Personal AI Assistant with local-first persistence",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"health": "node scripts/validate-data.js",
|
|
6
|
+
"health": "node scripts/validate-data.js && node scripts/validate-structure.js",
|
|
7
7
|
"migrate": "node scripts/migrate-data.js",
|
|
8
8
|
"report": "node scripts/generate-weekly-report.js",
|
|
9
9
|
"sm-weekly": "node scripts/generate-sm-weekly-report.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-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"
|
|
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 && node tests/unit/test-structure-validation.js"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [],
|
|
19
19
|
"author": "",
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const ROOT = path.join(__dirname, '..');
|
|
5
|
+
const LOGS_DIR = path.join(ROOT, 'logs', 'daily');
|
|
6
|
+
const DATA_DIR = path.join(ROOT, 'data');
|
|
7
|
+
const DOCS_DIR = path.join(ROOT, 'docs');
|
|
8
|
+
const CLIENTS_DIR = path.join(DATA_DIR, 'Clients');
|
|
9
|
+
|
|
10
|
+
const errors = [];
|
|
11
|
+
|
|
12
|
+
function exists(p) {
|
|
13
|
+
try { fs.accessSync(p); return true; } catch { return false; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readFileSafe(p) {
|
|
17
|
+
try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function walk(dir, out = []) {
|
|
21
|
+
if (!exists(dir)) return out;
|
|
22
|
+
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
23
|
+
const full = path.join(dir, ent.name);
|
|
24
|
+
if (ent.isDirectory()) walk(full, out);
|
|
25
|
+
else out.push(full);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseFrontmatter(text) {
|
|
31
|
+
if (!text) return null;
|
|
32
|
+
const lines = text.split(/\r?\n/);
|
|
33
|
+
if (!lines.length || lines[0].trim() !== '---') return null;
|
|
34
|
+
const fm = {};
|
|
35
|
+
for (let i = 1; i < lines.length; i++) {
|
|
36
|
+
const line = lines[i];
|
|
37
|
+
if (line.trim() === '---') break;
|
|
38
|
+
const idx = line.indexOf(':');
|
|
39
|
+
if (idx === -1) continue;
|
|
40
|
+
const key = line.slice(0, idx).trim();
|
|
41
|
+
const value = line.slice(idx + 1).trim();
|
|
42
|
+
fm[key] = value;
|
|
43
|
+
}
|
|
44
|
+
return fm;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateDailyLogs() {
|
|
48
|
+
if (!exists(LOGS_DIR)) return;
|
|
49
|
+
const files = fs.readdirSync(LOGS_DIR)
|
|
50
|
+
.filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f));
|
|
51
|
+
|
|
52
|
+
for (const name of files) {
|
|
53
|
+
const full = path.join(LOGS_DIR, name);
|
|
54
|
+
const body = readFileSafe(full);
|
|
55
|
+
const fm = parseFrontmatter(body);
|
|
56
|
+
const date = name.replace(/\.md$/, '');
|
|
57
|
+
if (!fm) {
|
|
58
|
+
errors.push(`Daily log missing frontmatter: ${path.relative(ROOT, full)}`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const type = String(fm.Type || '').toLowerCase();
|
|
62
|
+
const fmDate = String(fm.Date || '').trim();
|
|
63
|
+
if (type !== 'daily') {
|
|
64
|
+
errors.push(`Daily log frontmatter Type must be 'daily': ${path.relative(ROOT, full)}`);
|
|
65
|
+
}
|
|
66
|
+
if (fmDate !== date) {
|
|
67
|
+
errors.push(`Daily log frontmatter Date must match filename (${date}): ${path.relative(ROOT, full)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function collectProjectSlugs() {
|
|
73
|
+
if (!exists(CLIENTS_DIR)) return [];
|
|
74
|
+
const slugs = [];
|
|
75
|
+
const files = walk(CLIENTS_DIR).filter((f) => f.endsWith('status.json'));
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
const rel = path.relative(CLIENTS_DIR, path.dirname(file));
|
|
78
|
+
if (!rel) continue;
|
|
79
|
+
const slug = rel.split(path.sep).join('/').toLowerCase();
|
|
80
|
+
slugs.push(slug);
|
|
81
|
+
}
|
|
82
|
+
return Array.from(new Set(slugs));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateProjectStatusHistory() {
|
|
86
|
+
if (!exists(CLIENTS_DIR)) return;
|
|
87
|
+
const files = walk(CLIENTS_DIR).filter((f) => f.endsWith('status.json'));
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const raw = readFileSafe(file);
|
|
90
|
+
if (!raw) continue;
|
|
91
|
+
try {
|
|
92
|
+
const json = JSON.parse(raw);
|
|
93
|
+
if (!Array.isArray(json.history)) {
|
|
94
|
+
errors.push(`status.json must include history array: ${path.relative(ROOT, file)}`);
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
errors.push(`Invalid JSON in status.json: ${path.relative(ROOT, file)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateTaskProjectSlugs() {
|
|
103
|
+
const slugs = collectProjectSlugs();
|
|
104
|
+
if (!slugs.length) return; // no known slugs -> skip
|
|
105
|
+
|
|
106
|
+
const taskFile = path.join(DATA_DIR, 'tasks', 'task-log.json');
|
|
107
|
+
if (!exists(taskFile)) return;
|
|
108
|
+
|
|
109
|
+
let json;
|
|
110
|
+
try { json = JSON.parse(readFileSafe(taskFile) || '{}'); } catch { return; }
|
|
111
|
+
const tasks = Array.isArray(json.tasks) ? json.tasks : [];
|
|
112
|
+
|
|
113
|
+
for (const task of tasks) {
|
|
114
|
+
if (!task || typeof task !== 'object') continue;
|
|
115
|
+
const desc = String(task.description || '').toLowerCase();
|
|
116
|
+
if (!desc) continue;
|
|
117
|
+
const mentioned = slugs.find((slug) => desc.includes(slug));
|
|
118
|
+
if (mentioned && !task.projectSlug) {
|
|
119
|
+
errors.push(`Task missing projectSlug for mentioned project (${mentioned}): ${task.id || task.description}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function validateDocsHubs() {
|
|
125
|
+
const hubs = [
|
|
126
|
+
path.join(DOCS_DIR, 'reports', 'Reports Hub.md'),
|
|
127
|
+
path.join(DOCS_DIR, 'career', 'Career Hub.md'),
|
|
128
|
+
path.join(DOCS_DIR, 'standards', 'Standards Hub.md'),
|
|
129
|
+
];
|
|
130
|
+
for (const hub of hubs) {
|
|
131
|
+
if (!exists(hub)) {
|
|
132
|
+
errors.push(`Missing hub doc: ${path.relative(ROOT, hub)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function main() {
|
|
138
|
+
validateDailyLogs();
|
|
139
|
+
validateProjectStatusHistory();
|
|
140
|
+
validateTaskProjectSlugs();
|
|
141
|
+
validateDocsHubs();
|
|
142
|
+
|
|
143
|
+
if (errors.length) {
|
|
144
|
+
console.error('❌ Structure validation failed:');
|
|
145
|
+
for (const err of errors) console.error('-', err);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
console.log('✅ Structure validation passed');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
main();
|
|
@@ -14,6 +14,14 @@ Before ANY attempt to parse, classify, or understand the input, you MUST write t
|
|
|
14
14
|
This ensures no data is lost even if the subsequent steps fail.
|
|
15
15
|
</critical-rule>
|
|
16
16
|
|
|
17
|
+
<structure-guardrails>
|
|
18
|
+
**Pasta correta, sempre:**
|
|
19
|
+
- Logs diários brutos → `logs/daily/YYYY-MM-DD.md`
|
|
20
|
+
- Dados estruturados → `data/**` (tasks, career, Clients/.../status.json)
|
|
21
|
+
- Documentos de síntese/hubs/relatórios → `docs/**`
|
|
22
|
+
Nunca gravar dados estruturados em `logs/` e nunca colocar notas diárias em `docs/`.
|
|
23
|
+
</structure-guardrails>
|
|
24
|
+
|
|
17
25
|
<workflow>
|
|
18
26
|
1. **Receive Input:** The user provides text (status update, blocker, random thought, etc.).
|
|
19
27
|
2. **Safe Log (PRIORITY):**
|
|
@@ -81,6 +81,11 @@ You must fully embody this agent's persona and follow all activation instruction
|
|
|
81
81
|
- If user asks "Relatório Scrum Master", "SM weekly" or "weekly scrum" -> Execute `npm run sm-weekly` via the Shell tool.
|
|
82
82
|
- If user asks "Relatório de blockers", "blockers report", "riscos" -> Execute `npm run blockers` via the Shell tool.
|
|
83
83
|
- Inform the user where the file was saved when applicable.
|
|
84
|
+
- **Structure Guardrail (ALWAYS)**:
|
|
85
|
+
- Logs diários brutos → `logs/daily/`
|
|
86
|
+
- Dados estruturados → `data/`
|
|
87
|
+
- Hubs e relatórios → `docs/`
|
|
88
|
+
- Nunca misturar camadas.
|
|
84
89
|
- **Git Operations**: If user asks "Commit changes", "Save my work", or "Generate commit" ->
|
|
85
90
|
1. Execute `git status --porcelain` via Shell.
|
|
86
91
|
2. If output is empty, inform the user "No changes to commit".
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
Type: career-hub
|
|
3
|
+
Tags: [career, hub]
|
|
4
|
+
DataPath: data/career/career-log.json
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Career Hub
|
|
8
|
+
|
|
9
|
+
Centro de navegação para evolução de carreira dentro da FREYA.
|
|
10
|
+
|
|
11
|
+
## Como usar
|
|
12
|
+
- Registrar feedbacks e conquistas em `data/career/career-log.json`.
|
|
13
|
+
- Criar relatórios de carreira em `docs/reports`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
Type: reports-hub
|
|
3
|
+
Tags: [reports, hub]
|
|
4
|
+
DirPath: docs/reports
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Reports Hub
|
|
8
|
+
|
|
9
|
+
Ponto único para visualizar relatórios gerados pela FREYA.
|
|
10
|
+
|
|
11
|
+
## Como usar
|
|
12
|
+
- Relatórios gerados via scripts aparecem aqui.
|
|
13
|
+
- Mantenha links para relatórios importantes.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const ROOT = path.join(__dirname, '..');
|
|
5
|
+
const LOGS_DIR = path.join(ROOT, 'logs', 'daily');
|
|
6
|
+
const DATA_DIR = path.join(ROOT, 'data');
|
|
7
|
+
const DOCS_DIR = path.join(ROOT, 'docs');
|
|
8
|
+
const CLIENTS_DIR = path.join(DATA_DIR, 'Clients');
|
|
9
|
+
|
|
10
|
+
const errors = [];
|
|
11
|
+
|
|
12
|
+
function exists(p) {
|
|
13
|
+
try { fs.accessSync(p); return true; } catch { return false; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readFileSafe(p) {
|
|
17
|
+
try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function walk(dir, out = []) {
|
|
21
|
+
if (!exists(dir)) return out;
|
|
22
|
+
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
23
|
+
const full = path.join(dir, ent.name);
|
|
24
|
+
if (ent.isDirectory()) walk(full, out);
|
|
25
|
+
else out.push(full);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseFrontmatter(text) {
|
|
31
|
+
if (!text) return null;
|
|
32
|
+
const lines = text.split(/\r?\n/);
|
|
33
|
+
if (!lines.length || lines[0].trim() !== '---') return null;
|
|
34
|
+
const fm = {};
|
|
35
|
+
for (let i = 1; i < lines.length; i++) {
|
|
36
|
+
const line = lines[i];
|
|
37
|
+
if (line.trim() === '---') break;
|
|
38
|
+
const idx = line.indexOf(':');
|
|
39
|
+
if (idx === -1) continue;
|
|
40
|
+
const key = line.slice(0, idx).trim();
|
|
41
|
+
const value = line.slice(idx + 1).trim();
|
|
42
|
+
fm[key] = value;
|
|
43
|
+
}
|
|
44
|
+
return fm;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateDailyLogs() {
|
|
48
|
+
if (!exists(LOGS_DIR)) return;
|
|
49
|
+
const files = fs.readdirSync(LOGS_DIR)
|
|
50
|
+
.filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f));
|
|
51
|
+
|
|
52
|
+
for (const name of files) {
|
|
53
|
+
const full = path.join(LOGS_DIR, name);
|
|
54
|
+
const body = readFileSafe(full);
|
|
55
|
+
const fm = parseFrontmatter(body);
|
|
56
|
+
const date = name.replace(/\.md$/, '');
|
|
57
|
+
if (!fm) {
|
|
58
|
+
errors.push(`Daily log missing frontmatter: ${path.relative(ROOT, full)}`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const type = String(fm.Type || '').toLowerCase();
|
|
62
|
+
const fmDate = String(fm.Date || '').trim();
|
|
63
|
+
if (type !== 'daily') {
|
|
64
|
+
errors.push(`Daily log frontmatter Type must be 'daily': ${path.relative(ROOT, full)}`);
|
|
65
|
+
}
|
|
66
|
+
if (fmDate !== date) {
|
|
67
|
+
errors.push(`Daily log frontmatter Date must match filename (${date}): ${path.relative(ROOT, full)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function collectProjectSlugs() {
|
|
73
|
+
if (!exists(CLIENTS_DIR)) return [];
|
|
74
|
+
const slugs = [];
|
|
75
|
+
const files = walk(CLIENTS_DIR).filter((f) => f.endsWith('status.json'));
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
const rel = path.relative(CLIENTS_DIR, path.dirname(file));
|
|
78
|
+
if (!rel) continue;
|
|
79
|
+
const slug = rel.split(path.sep).join('/').toLowerCase();
|
|
80
|
+
slugs.push(slug);
|
|
81
|
+
}
|
|
82
|
+
return Array.from(new Set(slugs));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateProjectStatusHistory() {
|
|
86
|
+
if (!exists(CLIENTS_DIR)) return;
|
|
87
|
+
const files = walk(CLIENTS_DIR).filter((f) => f.endsWith('status.json'));
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const raw = readFileSafe(file);
|
|
90
|
+
if (!raw) continue;
|
|
91
|
+
try {
|
|
92
|
+
const json = JSON.parse(raw);
|
|
93
|
+
if (!Array.isArray(json.history)) {
|
|
94
|
+
errors.push(`status.json must include history array: ${path.relative(ROOT, file)}`);
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
errors.push(`Invalid JSON in status.json: ${path.relative(ROOT, file)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateTaskProjectSlugs() {
|
|
103
|
+
const slugs = collectProjectSlugs();
|
|
104
|
+
if (!slugs.length) return; // no known slugs -> skip
|
|
105
|
+
|
|
106
|
+
const taskFile = path.join(DATA_DIR, 'tasks', 'task-log.json');
|
|
107
|
+
if (!exists(taskFile)) return;
|
|
108
|
+
|
|
109
|
+
let json;
|
|
110
|
+
try { json = JSON.parse(readFileSafe(taskFile) || '{}'); } catch { return; }
|
|
111
|
+
const tasks = Array.isArray(json.tasks) ? json.tasks : [];
|
|
112
|
+
|
|
113
|
+
for (const task of tasks) {
|
|
114
|
+
if (!task || typeof task !== 'object') continue;
|
|
115
|
+
const desc = String(task.description || '').toLowerCase();
|
|
116
|
+
if (!desc) continue;
|
|
117
|
+
const mentioned = slugs.find((slug) => desc.includes(slug));
|
|
118
|
+
if (mentioned && !task.projectSlug) {
|
|
119
|
+
errors.push(`Task missing projectSlug for mentioned project (${mentioned}): ${task.id || task.description}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function validateDocsHubs() {
|
|
125
|
+
const hubs = [
|
|
126
|
+
path.join(DOCS_DIR, 'reports', 'Reports Hub.md'),
|
|
127
|
+
path.join(DOCS_DIR, 'career', 'Career Hub.md'),
|
|
128
|
+
path.join(DOCS_DIR, 'standards', 'Standards Hub.md'),
|
|
129
|
+
];
|
|
130
|
+
for (const hub of hubs) {
|
|
131
|
+
if (!exists(hub)) {
|
|
132
|
+
errors.push(`Missing hub doc: ${path.relative(ROOT, hub)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function main() {
|
|
138
|
+
validateDailyLogs();
|
|
139
|
+
validateProjectStatusHistory();
|
|
140
|
+
validateTaskProjectSlugs();
|
|
141
|
+
validateDocsHubs();
|
|
142
|
+
|
|
143
|
+
if (errors.length) {
|
|
144
|
+
console.error('❌ Structure validation failed:');
|
|
145
|
+
for (const err of errors) console.error('-', err);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
console.log('✅ Structure validation passed');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
main();
|