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 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
+ }
@@ -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.6.0",
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 depende de um arquivo, 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`:
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** (`architecture.md`, `plan.md`, `gate-checklist.md`, `pull-request.md`, `daily.md`), escreva primeiro essa mensagem em markdown, com o resumo, e só então abra o `AskUserQuestion`. 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`.
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
- **1. {Opção 1}**
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
- **2. {Opção 2}** (mesmo formato)
279
+ **❌ 2. {Opção 2}** (mesmo formato)
279
280
 
280
- **3. {Opção 3}** (mesmo formato)
281
+ **❌ 3. {Opção 3}** (mesmo formato)
281
282
 
282
- **4. Outra**
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 um comando futuro juntar os registros de um dia.
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