jarvis-ai-framework 5.6.0 → 5.7.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/AGENTS.md +1 -0
- package/bin/commands/daily-report.js +90 -0
- package/bin/lib/cli/program.js +9 -0
- package/bin/lib/core/session-scan.js +266 -0
- package/package.json +2 -2
- package/rules-on-demand/engineering/eng.transitions-rules.md +2 -2
- package/templates/engineering/architecture-template.md +9 -8
- package/templates/engineering/daily-template.md +1 -1
- package/workflows/engineering/eng.daily-report.md +131 -0
package/AGENTS.md
CHANGED
|
@@ -199,6 +199,7 @@ jarvis list # Listar agents, skills e wo
|
|
|
199
199
|
jarvis install-rtk # Instalar RTK (token killer — economia 60-90% em tokens de shell)
|
|
200
200
|
jarvis tokens [--hub X --position Y --area Z --squad W] [--rtk] [--json] # Estimar o que carrega no início da sessão (perfil do ENV.md se sem flags)
|
|
201
201
|
jarvis map <nome> [--depth N] [--reverse] # Mapa de chamadas de um workflow, skill ou agente (lê só as tabelas padronizadas)
|
|
202
|
+
jarvis daily-report [--days N] [--json] # Junta os daily.md das sessões por dia (usado pelo /eng.daily-report)
|
|
202
203
|
```
|
|
203
204
|
|
|
204
205
|
Quando atuar em um projeto alvo, detectar comandos em:
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
2
|
+
import { scanSessions, parseDays } from "../lib/core/session-scan.js";
|
|
3
|
+
import { resolveEnvPath, loadEnv } from "../lib/env-loader.js";
|
|
4
|
+
import { logger } from "../lib/utils/logger.js";
|
|
5
|
+
import { GREEN, YELLOW, CYAN, DIM, NC } from "../lib/utils/ui.js";
|
|
6
|
+
|
|
7
|
+
const USAGE = "Uso: jarvis daily-report [--days 1|3|7|15|30|60] [--json]";
|
|
8
|
+
const DEFAULTS = {
|
|
9
|
+
DOCS_FOLDER: "docs",
|
|
10
|
+
JARVIS_DOCS: "$DOCS_FOLDER/jarvis",
|
|
11
|
+
ENG_DOCS: "$JARVIS_DOCS/engineering",
|
|
12
|
+
SESSIONS_DIR: ".jarvis/sessions",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Expande `$VAR` com os valores do ENV (e os padrões acima), em até 5 passadas. */
|
|
16
|
+
export function expandVars(value, env) {
|
|
17
|
+
let out = value;
|
|
18
|
+
for (let i = 0; i < 5 && /\$[A-Z_]+/.test(out); i++) {
|
|
19
|
+
out = out.replace(/\$([A-Z_][A-Z0-9_]*)/g, (m, name) => env[name] ?? DEFAULTS[name] ?? m);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveDir(root, value) {
|
|
25
|
+
return isAbsolute(value) ? value : join(root, value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Lê as sessões do workspace e devolve o JSON do relatório. Separado de `dailyReport` para ser testável.
|
|
30
|
+
* @param {Record<string, unknown>} flags - `days`, `ide`, `env-file`
|
|
31
|
+
* @param {string} [cwd]
|
|
32
|
+
* @param {Date} [now]
|
|
33
|
+
* @returns {{ code: number, json?: object, lines?: string[] }}
|
|
34
|
+
*/
|
|
35
|
+
export function runDailyReport(flags = {}, cwd = process.cwd(), now = new Date()) {
|
|
36
|
+
const parsed = parseDays(flags.days);
|
|
37
|
+
if (parsed.error) return { code: 1, lines: [`❌ ${parsed.error}`, USAGE] };
|
|
38
|
+
|
|
39
|
+
let envPath;
|
|
40
|
+
let root;
|
|
41
|
+
try {
|
|
42
|
+
({ path: envPath, root } = resolveEnvPath(cwd, flags));
|
|
43
|
+
} catch (err) {
|
|
44
|
+
return { code: 1, lines: [`❌ ${err.message}`, USAGE] };
|
|
45
|
+
}
|
|
46
|
+
root = root || cwd;
|
|
47
|
+
const env = { ...DEFAULTS, ...loadEnv(cwd, envPath) };
|
|
48
|
+
|
|
49
|
+
const sessionsDir = join(resolveDir(root, expandVars(env.SESSIONS_DIR, env)), "eng");
|
|
50
|
+
const reportsDir = join(resolveDir(root, expandVars(env.ENG_DOCS, env)), "daily-reports");
|
|
51
|
+
|
|
52
|
+
const result = scanSessions(sessionsDir, { days: parsed.days, now });
|
|
53
|
+
const reportPathAbs = join(reportsDir, result.fileName);
|
|
54
|
+
return {
|
|
55
|
+
code: 0,
|
|
56
|
+
json: {
|
|
57
|
+
...result,
|
|
58
|
+
workspace: root,
|
|
59
|
+
sessionsDir: relative(root, sessionsDir) || ".",
|
|
60
|
+
reportPath: relative(root, reportPathAbs),
|
|
61
|
+
reportPathAbs,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function dailyReport(flags) {
|
|
67
|
+
const { code, json, lines } = runDailyReport(flags);
|
|
68
|
+
if (code !== 0) {
|
|
69
|
+
for (const line of lines) logger.error(line);
|
|
70
|
+
process.exit(code);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (flags.json) {
|
|
74
|
+
process.stdout.write(`${JSON.stringify(json, null, 2)}\n`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
logger.info(`${CYAN}🗓️ Registros dos últimos ${json.days} dia(s)${NC}`);
|
|
79
|
+
logger.info(`${DIM}Janela: ${json.window.from} a ${json.window.to} (fuso local)${NC}`);
|
|
80
|
+
logger.info(
|
|
81
|
+
`Sessões lidas: ${json.sessionsScanned} · na janela: ${json.sessionsInWindow} · dias com registro: ${json.byDate.length}`
|
|
82
|
+
);
|
|
83
|
+
logger.info(
|
|
84
|
+
json.alerts.length
|
|
85
|
+
? `${YELLOW}⚠️ ${json.alerts.length} sessão(ões) sem daily.md: ${json.alerts.map((a) => a.key).join(", ")}${NC}`
|
|
86
|
+
: `${GREEN}✅ Todas as sessões da janela têm daily.md${NC}`
|
|
87
|
+
);
|
|
88
|
+
for (const w of json.warnings) logger.info(`${DIM}aviso: ${w}${NC}`);
|
|
89
|
+
logger.info(`${DIM}Relatório (escrito pelo /eng.daily-report): ${json.reportPath}${NC}`);
|
|
90
|
+
}
|
package/bin/lib/cli/program.js
CHANGED
|
@@ -26,6 +26,7 @@ import { qaSignoff } from "../../commands/qa-signoff.js";
|
|
|
26
26
|
import { installRtk } from "../../commands/install-rtk.js";
|
|
27
27
|
import { tokens } from "../../commands/tokens.js";
|
|
28
28
|
import { map } from "../../commands/map.js";
|
|
29
|
+
import { dailyReport } from "../../commands/daily-report.js";
|
|
29
30
|
import { runCreateMerge } from "../vcs/create-merge.js";
|
|
30
31
|
import { runCreateIssue } from "../vcs/create-issue.js";
|
|
31
32
|
import { runComment } from "../tasks/comment.js";
|
|
@@ -187,6 +188,14 @@ export function buildProgram() {
|
|
|
187
188
|
.option("--reverse", "Mostra quem chama, em vez do que é chamado")
|
|
188
189
|
.action((name, options) => map(toLegacyFlags(options, { _: [name].filter(Boolean) })));
|
|
189
190
|
|
|
191
|
+
cmd(program, "daily-report")
|
|
192
|
+
.description("Junta os daily.md das sessões por dia (base do /eng.daily-report)")
|
|
193
|
+
.option("--days <n>", "Janela em dias: 1, 3, 7, 15, 30 ou 60 (padrão: 7)")
|
|
194
|
+
.option("--json", "Saída em JSON")
|
|
195
|
+
.option("--ide <ide>")
|
|
196
|
+
.option("--env-file <path>")
|
|
197
|
+
.action((options) => dailyReport(toLegacyFlags(options)));
|
|
198
|
+
|
|
190
199
|
const vcs = cmd(program, "vcs").description("Adapters de Git host (GitLab/GitHub/Bitbucket)");
|
|
191
200
|
|
|
192
201
|
cmd(vcs, "create-merge")
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Varredura das sessões de engenharia para o /eng.daily-report: junta as seções `## AAAA-MM-DD`
|
|
2
|
+
// dos `daily.md` por dia e aponta as sessões que deveriam ter `daily.md` e não têm.
|
|
3
|
+
// Só lê: não escreve nas sessões nem cria o relatório (quem escreve é o workflow).
|
|
4
|
+
// As datas são sempre do calendário LOCAL (o `.timestamp_start` é gravado em UTC).
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
export const ALLOWED_DAYS = [1, 3, 7, 15, 30, 60];
|
|
9
|
+
export const DEFAULT_DAYS = 7;
|
|
10
|
+
|
|
11
|
+
// Dia em que o `daily.md` passou a existir (gerado pelo eng.pr). Sessões sem `daily.md` que
|
|
12
|
+
// começaram antes disso só geram alerta se chegaram ao eng.pr (têm `pull-request.md`).
|
|
13
|
+
export const DAILY_SINCE = "2026-10-15";
|
|
14
|
+
|
|
15
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
16
|
+
const EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
17
|
+
|
|
18
|
+
/** Valida a janela. Sem valor: o padrão (7). */
|
|
19
|
+
export function parseDays(value) {
|
|
20
|
+
if (value === undefined || value === null || value === "" || value === true)
|
|
21
|
+
return { days: DEFAULT_DAYS };
|
|
22
|
+
const n = Number(String(value).trim());
|
|
23
|
+
if (!ALLOWED_DAYS.includes(n)) {
|
|
24
|
+
return {
|
|
25
|
+
error: `Janela inválida: ${value}. Valores aceitos: ${ALLOWED_DAYS.join(", ")}`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return { days: n };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `AAAA-MM-DD` no fuso local (sem `toISOString`, que é UTC). */
|
|
32
|
+
export function localDate(date) {
|
|
33
|
+
const y = date.getFullYear();
|
|
34
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
35
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
36
|
+
return `${y}-${m}-${d}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Hoje e os `days - 1` dias anteriores, do mais recente para o mais antigo. */
|
|
40
|
+
export function buildWindow(days, now = new Date()) {
|
|
41
|
+
const dates = [];
|
|
42
|
+
for (let i = 0; i < days; i++) {
|
|
43
|
+
dates.push(localDate(new Date(now.getFullYear(), now.getMonth(), now.getDate() - i)));
|
|
44
|
+
}
|
|
45
|
+
return { from: dates[dates.length - 1], to: dates[0], dates };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function reportFileName(today, days) {
|
|
49
|
+
return `daily-report-${today}-${days}d.md`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function redactEmails(text) {
|
|
53
|
+
return typeof text === "string" ? text.replace(EMAIL_RE, "[e-mail removido]") : text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readText(path) {
|
|
57
|
+
try {
|
|
58
|
+
return readFileSync(path, "utf8");
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function unquote(value) {
|
|
65
|
+
const v = value.trim();
|
|
66
|
+
return /^(["']).*\1$/.test(v) ? v.slice(1, -1) : v;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Frontmatter `chave: valor` simples (ignora comentários). */
|
|
70
|
+
function parseFrontmatter(text) {
|
|
71
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
72
|
+
const data = {};
|
|
73
|
+
if (!m) return { data, body: text };
|
|
74
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
75
|
+
if (/^\s*#/.test(line)) continue;
|
|
76
|
+
const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
|
|
77
|
+
if (kv) data[kv[1]] = unquote(kv[2]);
|
|
78
|
+
}
|
|
79
|
+
return { data, body: text.slice(m[0].length) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Primeira linha não vazia depois de `### Timesheet`, sem crases nem cercas de código. */
|
|
83
|
+
function extractTimesheet(section) {
|
|
84
|
+
const m = section.match(/^###\s+Timesheet\s*$([\s\S]*?)(?=^###?\s|(?![\s\S]))/m);
|
|
85
|
+
if (!m) return null;
|
|
86
|
+
const line = m[1]
|
|
87
|
+
.split(/\r?\n/)
|
|
88
|
+
.map((l) => l.trim())
|
|
89
|
+
.find((l) => l && !/^```/.test(l));
|
|
90
|
+
return line ? line.replace(/^`+|`+$/g, "").trim() : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function extractDescription(section) {
|
|
94
|
+
const m = section.match(/^###\s+Descrição detalhada\s*$([\s\S]*?)(?=^###\s|(?![\s\S]))/m);
|
|
95
|
+
return m ? m[1].trim() : "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Lê um `daily.md`: frontmatter (`task_key`, `card_url`, `pr`, `branch`) e uma entrada por
|
|
100
|
+
* seção `## AAAA-MM-DD`, com a descrição detalhada e a linha de timesheet.
|
|
101
|
+
*/
|
|
102
|
+
export function parseDaily(text) {
|
|
103
|
+
const { data, body } = parseFrontmatter(text);
|
|
104
|
+
const sections = [];
|
|
105
|
+
const parts = body.split(/^##\s+(\d{4}-\d{2}-\d{2})\s*$/m);
|
|
106
|
+
for (let i = 1; i < parts.length; i += 2) {
|
|
107
|
+
sections.push({
|
|
108
|
+
date: parts[i],
|
|
109
|
+
description: redactEmails(extractDescription(parts[i + 1])),
|
|
110
|
+
timesheet: redactEmails(extractTimesheet(parts[i + 1])),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
taskKey: data.task_key || null,
|
|
115
|
+
cardUrl: data.card_url || null,
|
|
116
|
+
pr: data.pr || null,
|
|
117
|
+
branch: data.branch || null,
|
|
118
|
+
sections,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function firstHeading(text) {
|
|
123
|
+
const m = text && text.match(/^#\s+(.+)$/m);
|
|
124
|
+
return m ? m[1].trim() : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Linha `Issue:` do cabeçalho do architecture.md (com ou sem negrito e `>`). */
|
|
128
|
+
function issueLine(text) {
|
|
129
|
+
const m = text && text.match(/^[>\s]*\**Issue:\**\s*(.+?)\s*$/m);
|
|
130
|
+
return m ? m[1].trim() : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function prNumberFrom(text) {
|
|
134
|
+
if (!text) return null;
|
|
135
|
+
const m = text.match(/\/(?:pull|merge_requests)\/(\d+)/) || text.match(/\b(?:PR|MR)\s*#(\d+)/);
|
|
136
|
+
return m ? `#${m[1]}` : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Data e atividade de uma sessão sem `daily.md`, só com fontes locais (o vendor fica com o
|
|
141
|
+
* workflow). Ordem da data: `Criado em:` > `.timestamp_start` (UTC → local) > modificação da pasta.
|
|
142
|
+
*/
|
|
143
|
+
export function sessionInfo(dir) {
|
|
144
|
+
const arch = readText(join(dir, "architecture.md"));
|
|
145
|
+
const plan = readText(join(dir, "plan.md"));
|
|
146
|
+
const pr = readText(join(dir, "pull-request.md"));
|
|
147
|
+
|
|
148
|
+
let date = null;
|
|
149
|
+
let dateSource = null;
|
|
150
|
+
const created = arch && arch.match(/Criado em:\**\s*(\d{4}-\d{2}-\d{2})/);
|
|
151
|
+
if (created) {
|
|
152
|
+
date = created[1];
|
|
153
|
+
dateSource = "architecture";
|
|
154
|
+
}
|
|
155
|
+
if (!date) {
|
|
156
|
+
const ts = (readText(join(dir, ".timestamp_start")) || "").trim();
|
|
157
|
+
const parsed = ts ? new Date(ts) : null;
|
|
158
|
+
if (parsed && !Number.isNaN(parsed.getTime())) {
|
|
159
|
+
date = localDate(parsed);
|
|
160
|
+
dateSource = "timestamp_start";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!date) {
|
|
164
|
+
date = localDate(statSync(dir).mtime);
|
|
165
|
+
dateSource = "mtime";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const issue = issueLine(arch);
|
|
169
|
+
const issueKey = issue ? (issue.match(/#\d+|\b[A-Z][A-Z0-9]+-\d+\b/) || [null])[0] : null;
|
|
170
|
+
const issueTitle = issue ? (issue.match(/\s[—–-]\s+(.+)$/) || [null, null])[1] : null;
|
|
171
|
+
const archTitle = firstHeading(arch);
|
|
172
|
+
const usefulArchTitle =
|
|
173
|
+
archTitle && !/^architecture(\.md)?\s*[:—-]?\s*[#\w-]*$/i.test(archTitle) ? archTitle : null;
|
|
174
|
+
|
|
175
|
+
const activity = issueTitle || firstHeading(plan) || firstHeading(pr) || usefulArchTitle || null;
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
date,
|
|
179
|
+
dateSource,
|
|
180
|
+
activity: redactEmails(activity),
|
|
181
|
+
issueKey,
|
|
182
|
+
prNumber: prNumberFrom(pr),
|
|
183
|
+
hasPullRequest: pr !== null,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Sessão sem `daily.md` gera alerta se chegou ao eng.pr ou começou depois do `daily.md` existir. */
|
|
188
|
+
export function needsAlert({ hasDaily, hasPullRequest, date }) {
|
|
189
|
+
if (hasDaily) return false;
|
|
190
|
+
return hasPullRequest || date >= DAILY_SINCE;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const byKey = (a, b) => a.key.localeCompare(b.key, "pt-BR", { numeric: true });
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Varre `sessionsDir` (o `$SESSIONS_DIR/eng`) e devolve o que entra na janela.
|
|
197
|
+
* @param {string} sessionsDir
|
|
198
|
+
* @param {{ days?: number, now?: Date }} [options]
|
|
199
|
+
*/
|
|
200
|
+
export function scanSessions(sessionsDir, { days = DEFAULT_DAYS, now = new Date() } = {}) {
|
|
201
|
+
const window = buildWindow(days, now);
|
|
202
|
+
const inWindow = new Set(window.dates);
|
|
203
|
+
const warnings = [];
|
|
204
|
+
const perDate = new Map();
|
|
205
|
+
const alerts = [];
|
|
206
|
+
const touched = new Set();
|
|
207
|
+
|
|
208
|
+
let keys = [];
|
|
209
|
+
if (existsSync(sessionsDir)) {
|
|
210
|
+
keys = readdirSync(sessionsDir, { withFileTypes: true })
|
|
211
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
212
|
+
.map((e) => e.name);
|
|
213
|
+
} else {
|
|
214
|
+
warnings.push(`pasta de sessões não encontrada: ${sessionsDir}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const key of keys) {
|
|
218
|
+
const dir = join(sessionsDir, key);
|
|
219
|
+
const dailyText = readText(join(dir, "daily.md"));
|
|
220
|
+
|
|
221
|
+
if (dailyText !== null) {
|
|
222
|
+
const daily = parseDaily(dailyText);
|
|
223
|
+
for (const s of daily.sections) {
|
|
224
|
+
if (!DATE_RE.test(s.date) || !inWindow.has(s.date)) continue;
|
|
225
|
+
if (!perDate.has(s.date)) perDate.set(s.date, []);
|
|
226
|
+
perDate.get(s.date).push({
|
|
227
|
+
key,
|
|
228
|
+
taskKey: daily.taskKey,
|
|
229
|
+
cardUrl: daily.cardUrl,
|
|
230
|
+
pr: daily.pr,
|
|
231
|
+
branch: daily.branch,
|
|
232
|
+
description: s.description,
|
|
233
|
+
timesheet: s.timesheet,
|
|
234
|
+
});
|
|
235
|
+
touched.add(key);
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const info = sessionInfo(dir);
|
|
241
|
+
if (!inWindow.has(info.date)) continue;
|
|
242
|
+
if (!needsAlert({ hasDaily: false, hasPullRequest: info.hasPullRequest, date: info.date }))
|
|
243
|
+
continue;
|
|
244
|
+
if (info.dateSource === "mtime")
|
|
245
|
+
warnings.push(`sessão ${key}: data tirada da modificação da pasta (sem outra fonte)`);
|
|
246
|
+
alerts.push({ key, ...info });
|
|
247
|
+
touched.add(key);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const byDate = [...perDate.entries()]
|
|
251
|
+
.sort(([a], [b]) => (a < b ? 1 : -1))
|
|
252
|
+
.map(([date, tasks]) => ({ date, tasks: tasks.sort(byKey) }));
|
|
253
|
+
alerts.sort((a, b) => (a.date === b.date ? byKey(a, b) : a.date < b.date ? 1 : -1));
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
days,
|
|
257
|
+
today: window.to,
|
|
258
|
+
window: { from: window.from, to: window.to },
|
|
259
|
+
fileName: reportFileName(window.to, days),
|
|
260
|
+
sessionsScanned: keys.length,
|
|
261
|
+
sessionsInWindow: touched.size,
|
|
262
|
+
byDate,
|
|
263
|
+
alerts,
|
|
264
|
+
warnings,
|
|
265
|
+
};
|
|
266
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jarvis-ai-framework",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.7.0",
|
|
4
4
|
"description": "Jarvis framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"prepublishOnly": "echo 'Publishing jarvis-framework...'",
|
|
12
12
|
"publish:gitlab": "npm publish",
|
|
13
13
|
"test:comandos": "node test/comandos-docs.test.js",
|
|
14
|
-
"test:filtro": "node --test test/profile-filter.test.js test/token-report.test.js test/flow-map.test.js test/env-loader.test.js test/sync-engine.test.js test/vcs-api.test.js test/fetch-file.test.js test/publish-file.test.js test/docs-storage.test.js test/env-upgrade.test.js test/docs-migrate.test.js test/init-upgrade.test.js test/workspace-mode.test.js test/stale-skills.test.js",
|
|
14
|
+
"test:filtro": "node --test test/profile-filter.test.js test/token-report.test.js test/flow-map.test.js test/env-loader.test.js test/sync-engine.test.js test/vcs-api.test.js test/fetch-file.test.js test/publish-file.test.js test/docs-storage.test.js test/env-upgrade.test.js test/docs-migrate.test.js test/init-upgrade.test.js test/workspace-mode.test.js test/stale-skills.test.js test/session-scan.test.js",
|
|
15
15
|
"lint": "eslint bin/ test/",
|
|
16
16
|
"lint:fix": "eslint bin/ test/ --fix",
|
|
17
17
|
"format": "prettier --write bin/ test/",
|
|
@@ -18,12 +18,12 @@ Lida pelos workflows `eng.start`, `eng.plan`, `eng.work`, `eng.pre-pr` e `eng.pr
|
|
|
18
18
|
2. **Formato:** `AskUserQuestion`, uma pergunta por decisão, no máximo 4 opções. **Não liste "Outros"**: o `AskUserQuestion` já o acrescenta sozinho para o texto livre, e listá-lo gasta uma das 4 vagas. Sem `AskUserQuestion`, use a tabela de opções de `prod-rules.md`.
|
|
19
19
|
3. **A resposta é a aprovação.** "Aprovar e seguir" ou "Aprovar e parar aqui" valem como a aprovação explícita que as rules de cada comando exigem. Sem uma dessas respostas, nada avança.
|
|
20
20
|
4. **Seguir para o próximo comando:** se a IDE permite chamar outro comando (no Claude Code, pelo Skill tool), chame-o com o mesmo `{TASK_MANAGER_KEY}`; senão, mostre o comando pronto para colar.
|
|
21
|
-
5. **Consolidado de links, acumulado a cada etapa.** Ao terminar e antes de cada pergunta que
|
|
21
|
+
5. **Consolidado de links, acumulado a cada etapa.** Ao terminar e antes de **cada pergunta de transição** (todas as da tabela "Transições", inclusive as do início, como "O que fazer com o PR?", e mesmo quando a pergunta é repetida), cada comando mostra o bloco "📄 Arquivos da sessão" com **todos** os arquivos que já existem na sessão, e não só os da etapa, para que a lista fique sempre visível até o `eng.spec-consolidate`:
|
|
22
22
|
- Ordem do ciclo: `context.md`, `architecture.md`, `plan.md`, `gate-checklist.md`, `pull-request.md`, `daily.md`, as versões `{nome}.public.md` (se houver) e os arquivos de `consolidation/` (do `eng.spec-consolidate`). Só entra o que existe.
|
|
23
23
|
- Cada item é um link markdown com caminho relativo ao workspace (ex.: `[architecture.md](.jarvis/sessions/eng/TASK-123/architecture.md)`), seguido de uma **nota curta de estado** entre parênteses: `novo` ou `atualizado` quando foi gerado ou alterado nesta etapa, mais o que mudou ou o que o arquivo contém (ex.: `plan.md (atualizado, com comentários por fase)`, `architecture.md (aprovado; 5 questões decididas)`, `daily.md (novo; aprovado, com timesheet)`). Os arquivos que não mudaram na etapa aparecem sem nota ou com o estado em que ficaram.
|
|
24
24
|
- A lista é **reimpressa inteira** a cada etapa, sem esconder os arquivos anteriores.
|
|
25
25
|
- Logo abaixo, "🔗 Links" com o card/issue e, quando existir, o PR/MR.
|
|
26
|
-
- **Antes de cada pergunta que depende de um arquivo
|
|
26
|
+
- **Antes de cada pergunta de transição**, escreva primeiro essa mensagem em markdown, com o resumo, e só então abra o `AskUserQuestion`. Isso vale também para as perguntas que não dependem de um arquivo: a lista é o mapa de onde a tarefa está no ponto de decisão. Na pergunta que depende de um arquivo (`architecture.md`, `plan.md`, `gate-checklist.md`, `pull-request.md`, `daily.md`), o resumo é o desse arquivo. A pergunta fica curta, só com a decisão e o nome do arquivo em texto simples; ela espera a resposta enquanto a pessoa abre e lê com calma. Não dependa do link dentro do `AskUserQuestion`.
|
|
27
27
|
6. **Limites que nenhuma escolha dispensa:** `MAX_AI_EXECUTION_PERCENTAGE`, testes e lint passando, e a parada em questão nova em aberto.
|
|
28
28
|
|
|
29
29
|
### Proibido
|
|
@@ -251,10 +251,11 @@ graph TD
|
|
|
251
251
|
> Tudo que for **diferente do código ou ambíguo no card** vira uma questão aqui, detalhada a ponto de outro agente entendê-la sem reconstruir a análise. No mínimo 3 opções, mais a **4 — Outra**.
|
|
252
252
|
> **Com Advisor** (cabeçalho), a sugestão do Advisor prevalece e é a decisão adotada; o agente principal não a substitui nem contesta. **Sem Advisor**, "Sugestão do Sub-Agent (Advisor)" e "Divergência" ficam "N/A (Advisor não utilizado)", a regra em destaque é omitida e a decisão é a do usuário ao revisar o documento.
|
|
253
253
|
> Questão levantada depois do `eng.start` (consulta por exceção) entra aqui, numerada em sequência, marcada "levantada no `eng.plan`" ou "no `eng.work`". Sem questão em aberto, escreva "Nenhuma" e o motivo em uma frase.
|
|
254
|
+
> **Marcação para leitura rápida:** decidida a questão, a opção escolhida ganha ✅ e "(escolhida)" no título, e as demais ganham ❌, inclusive a **4 — Outra**. A coluna "Opção escolhida" da tabela repete a escolha (`✅ {n} — {nome curto}`). Enquanto a questão estiver 🔄 Aberta, as opções ficam sem marcação e a coluna fica "—".
|
|
254
255
|
|
|
255
|
-
| ID | Questão | Responsável | Deadline | Status |
|
|
256
|
-
| -- | ---------------------------- | --------------------------- | -------- | -------------------------- |
|
|
257
|
-
| Q1 | {resumo objetivo da questão} | {Advisor \| nome do usuário} | {— \| data} | {✅ Decidida \| 🔄 Aberta} |
|
|
256
|
+
| ID | Questão | Opção escolhida | Responsável | Deadline | Status |
|
|
257
|
+
| -- | ---------------------------- | ----------------------------------- | --------------------------- | -------- | -------------------------- |
|
|
258
|
+
| Q1 | {resumo objetivo da questão} | {✅ {n} — {nome curto} \| —} | {Advisor \| nome do usuário} | {— \| data} | {✅ Decidida \| 🔄 Aberta} |
|
|
258
259
|
|
|
259
260
|
### Q1 — {Título da questão}
|
|
260
261
|
|
|
@@ -269,17 +270,17 @@ graph TD
|
|
|
269
270
|
|
|
270
271
|
**Opções:**
|
|
271
272
|
|
|
272
|
-
|
|
273
|
+
**✅ 1. {Opção 1} (escolhida)**
|
|
273
274
|
* Como funciona: {descrição}
|
|
274
275
|
* Vantagens: {vantagens}
|
|
275
276
|
* Desvantagens: {desvantagens}
|
|
276
277
|
* Impacto: {impacto}
|
|
277
278
|
|
|
278
|
-
|
|
279
|
+
**❌ 2. {Opção 2}** (mesmo formato)
|
|
279
280
|
|
|
280
|
-
|
|
281
|
+
**❌ 3. {Opção 3}** (mesmo formato)
|
|
281
282
|
|
|
282
|
-
|
|
283
|
+
**❌ 4. Outra**
|
|
283
284
|
* Alternativa adicional identificada pelo Sub-Agent/Advisor, detalhada caso ele entenda que nenhuma das opções anteriores é a melhor abordagem.
|
|
284
285
|
|
|
285
286
|
**Sugestão do Agente Principal (Architect):**
|
|
@@ -289,7 +290,7 @@ graph TD
|
|
|
289
290
|
{recomendação independente do Advisor + justificativa técnica}
|
|
290
291
|
|
|
291
292
|
**Decisão adotada:**
|
|
292
|
-
**{recomendação do Sub-Agent/Advisor}**
|
|
293
|
+
✅ **{n} — {recomendação do Sub-Agent/Advisor}**
|
|
293
294
|
|
|
294
295
|
> **Regra:** a recomendação do Advisor sempre prevalece e deve ser adotada na implementação, independentemente de ser igual ou diferente da recomendação do Agente Principal.
|
|
295
296
|
|
|
@@ -30,7 +30,7 @@ updated_at: {YYYY-MM-DD}
|
|
|
30
30
|
|
|
31
31
|
# Daily — {TASK_KEY}
|
|
32
32
|
|
|
33
|
-
> Registro pessoal do esforço: narrativa auditável e linha pronta para o timesheet. Fica só na sessão, nunca é publicado. Uma seção por data (`## AAAA-MM-DD`), no formato abaixo, para
|
|
33
|
+
> Registro pessoal do esforço: narrativa auditável e linha pronta para o timesheet. Fica só na sessão, nunca é publicado. Uma seção por data (`## AAAA-MM-DD`), no formato abaixo, para o `/eng.daily-report` juntar os registros por dia.
|
|
34
34
|
|
|
35
35
|
## {AAAA-MM-DD}
|
|
36
36
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Junta os daily.md das sessões em um relatório por dia (mapa mental), com alertas das sessões sem daily.md
|
|
3
|
+
auto_execution_mode: 3
|
|
4
|
+
recommended_model: sonnet
|
|
5
|
+
model_tier: medium
|
|
6
|
+
model_justification: Leitura de poucos arquivos e síntese curta; a janela, as datas e os alertas já vêm prontos do CLI
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Relatório dos daily.md
|
|
10
|
+
|
|
11
|
+
Este comando junta o que foi feito nos últimos `N` dias, lendo os `daily.md` que o `/eng.pr` grava em cada sessão. O resultado é um relatório em markdown, do dia mais recente para o mais antigo, em lista aninhada (tarefa > frentes de trabalho > detalhes), com a linha de timesheet de cada tarefa e um bloco de **alertas** das sessões que deveriam ter `daily.md` e não têm.
|
|
12
|
+
|
|
13
|
+
> 🔒 **Privado**: o relatório reaproveita a narrativa e o timesheet pessoais do `daily.md`. Ele fica em `$ENG_DOCS/daily-reports/` (dentro de `docs/jarvis/`, fora do versionamento) e **nunca** é publicado: nem no Central docs, nem no card.
|
|
14
|
+
|
|
15
|
+
### Skills invocados durante o workflow
|
|
16
|
+
|
|
17
|
+
| Passo | Skill | Condição |
|
|
18
|
+
|-------|-------|----------|
|
|
19
|
+
| — | Nenhuma — workflow autocontido | — |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Argumento
|
|
24
|
+
|
|
25
|
+
<arguments>
|
|
26
|
+
$ARGUMENTS
|
|
27
|
+
</arguments>
|
|
28
|
+
|
|
29
|
+
`{N}` é a janela em dias: **1, 3, 7, 15, 30 ou 60** (padrão **7**). A janela é do calendário local e inclui hoje: `1` = só hoje; `7` = hoje e os 6 dias anteriores.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Passo 1: Ler as sessões
|
|
34
|
+
|
|
35
|
+
Rode o helper do CLI (ele lê o `ENV.md` do workspace, mesmo se você estiver dentro de um repositório filho):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
jarvis daily-report --days {N} --json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- **Saída com erro** (código 1, por exemplo `N` inválido): mostre a mensagem, que já lista os valores aceitos, e **pare**. Não crie arquivo.
|
|
42
|
+
- **Saída JSON**: use os campos abaixo e não refaça as contas (janela, fuso, ordem e alertas já estão certos).
|
|
43
|
+
|
|
44
|
+
| Campo | O que é |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `days`, `window.from`, `window.to`, `today` | Janela pedida, no calendário local |
|
|
47
|
+
| `sessionsScanned`, `sessionsInWindow` | Sessões lidas em `$SESSIONS_DIR/eng/` e quantas entram na janela |
|
|
48
|
+
| `byDate[]` | Um item por dia com registro, do mais recente para o mais antigo; em cada um, `tasks[]` com `key`, `taskKey`, `cardUrl`, `pr`, `description` e `timesheet` (seção `## AAAA-MM-DD` do `daily.md`) |
|
|
49
|
+
| `alerts[]` | Sessões sem `daily.md` que deveriam ter (chegaram ao `eng.pr` ou começaram a partir de 2026-10-15), com `date`, `dateSource`, `activity`, `issueKey`, `prNumber` |
|
|
50
|
+
| `warnings[]` | Avisos (por exemplo, data tirada da modificação da pasta) |
|
|
51
|
+
| `reportPath`, `reportPathAbs` | Onde escrever o relatório |
|
|
52
|
+
|
|
53
|
+
Os textos já vêm sem e-mails.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Passo 2: Completar os alertas pelo vendor
|
|
58
|
+
|
|
59
|
+
Para cada item de `alerts` com `prNumber` ou `issueKey`, consulte o vendor de `VERSION_CONTROL`/`TASK_MANAGER` (no GitHub: `gh pr view {n} --json title,createdAt` e `gh issue view {n} --json title`):
|
|
60
|
+
|
|
61
|
+
- **Título:** use o do vendor quando `activity` vier vazia.
|
|
62
|
+
- **Data:** se houver PR, a data de criação dele (em fuso local) vale mais que a data local do helper. Se ela cair fora da janela, tire o alerta.
|
|
63
|
+
- Sem vendor, sem acesso ou com falha: siga com o que veio do helper, sem perguntar.
|
|
64
|
+
- Sem nenhuma fonte de atividade: escreva "não identificada", só com o ID da sessão.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Passo 3: Escrever o relatório
|
|
69
|
+
|
|
70
|
+
Crie a pasta de `reportPathAbs`, se não existir, e escreva o arquivo. Se já existir um relatório com a mesma janela no mesmo dia, ele é **sobrescrito**. Janelas diferentes no mesmo dia convivem.
|
|
71
|
+
|
|
72
|
+
```markdown
|
|
73
|
+
# Daily report — {window.from} a {window.to} ({N} dias)
|
|
74
|
+
|
|
75
|
+
> Gerado em {AAAA-MM-DD HH:MM} · {sessionsScanned} sessões lidas · {sessionsInWindow} na janela · privado, não publicar
|
|
76
|
+
|
|
77
|
+
## ⚠️ Alertas: sessões sem daily.md
|
|
78
|
+
|
|
79
|
+
| Data | ID da sessão | Atividade | Status |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| {date} | {key} | {activity} ({issueKey}, PR {prNumber}) | sem daily.md{; data pela modificação da pasta, se dateSource = mtime} |
|
|
82
|
+
|
|
83
|
+
## {AAAA-MM-DD} ({dia da semana})
|
|
84
|
+
|
|
85
|
+
- **{taskKey} — {título curto}** ([card]({cardUrl}), [PR {n}]({url do PR}))
|
|
86
|
+
- {frente de trabalho 1}
|
|
87
|
+
- {detalhe}
|
|
88
|
+
- {frente de trabalho 2}
|
|
89
|
+
- {detalhe}
|
|
90
|
+
- 🕒 `{timesheet}`
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- **Alertas** vêm antes dos dias. Sem alertas, troque a tabela por "✅ Todas as sessões da janela têm `daily.md`."
|
|
94
|
+
- **Um bloco `##` por data de `byDate`, na mesma ordem** (decrescente). Dia sem registro não aparece.
|
|
95
|
+
- **Síntese por tarefa:** de 3 a 5 frentes de trabalho, cada uma com detalhes curtos, tirados **só** da `description` (contexto, o que foi feito, decisões; *post mortem* quando houver). Não acrescente fatos. O título curto vem da linha de timesheet ou do card.
|
|
96
|
+
- **Timesheet literal:** a linha `timesheet` entra sem alteração, para copiar.
|
|
97
|
+
- Links: `cardUrl` e o PR de `pr` (o campo traz `#n` e a URL). Faltando, omita o link.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Passo 4: Mostrar o resultado
|
|
102
|
+
|
|
103
|
+
Mostre, em markdown:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
✅ Relatório gerado: [{nome do arquivo}]({reportPath})
|
|
107
|
+
|
|
108
|
+
🗓️ Janela: {window.from} a {window.to} ({N} dias)
|
|
109
|
+
📚 Sessões: {sessionsScanned} lidas · {sessionsInWindow} na janela · {dias com registro} dias com registro
|
|
110
|
+
⚠️ Alertas: {quantidade} ({IDs}) — rode o /eng.pr nessas sessões (Fase 7.2) para gerar o daily.md
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
O link usa o caminho relativo ao workspace (`reportPath`).
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Regras
|
|
118
|
+
|
|
119
|
+
### ✅ Faça
|
|
120
|
+
|
|
121
|
+
- Use o JSON do `jarvis daily-report` como fonte da janela, das datas, da ordem e dos alertas
|
|
122
|
+
- Mantenha a linha de timesheet literal
|
|
123
|
+
- Sobrescreva o relatório da mesma janela no mesmo dia
|
|
124
|
+
|
|
125
|
+
### ⛔ Não faça
|
|
126
|
+
|
|
127
|
+
- Não publique o relatório (Central docs, card, PR) nem o copie para fora de `docs/jarvis/`
|
|
128
|
+
- Não inclua e-mails, tokens nem valores de variáveis do `ENV.md`
|
|
129
|
+
- Não acrescente fatos além do `daily.md` e do vendor
|
|
130
|
+
- Não altere os `daily.md` nem outros arquivos das sessões
|
|
131
|
+
- Não gere arquivo quando a janela for inválida
|