jarvis-ai-framework 5.1.0 → 5.5.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.
Files changed (58) hide show
  1. package/AGENTS.md +1 -0
  2. package/README.md +14 -1
  3. package/agents/product/prod.discovery-interviewer.md +18 -83
  4. package/bin/commands/init.js +149 -2
  5. package/bin/commands/tokens.js +9 -3
  6. package/bin/lib/cli/program.js +14 -2
  7. package/bin/lib/core/docs-migrate.js +259 -0
  8. package/bin/lib/core/env-upgrade.js +84 -0
  9. package/bin/lib/core/profile-filter.js +15 -3
  10. package/bin/lib/core/sync-engine.js +5 -3
  11. package/bin/lib/core/token-report.js +1 -0
  12. package/bin/lib/docs/storage/build-index.js +2 -1
  13. package/bin/lib/vcs/api.js +13 -1
  14. package/bin/lib/vcs/create-issue.js +17 -3
  15. package/package.json +2 -2
  16. package/rules/AGENTS.md +7 -3
  17. package/rules/artifact-style-rules.md +53 -0
  18. package/rules/product/prod-rules.md +4 -2
  19. package/rules/product/prod.spec-rules.md +4 -4
  20. package/rules-on-demand/engineering/eng.plan-rules.md +1 -1
  21. package/rules-on-demand/engineering/eng.pr-rules.md +4 -3
  22. package/rules-on-demand/engineering/eng.start-rules.md +2 -2
  23. package/rules-on-demand/engineering/eng.transitions-rules.md +58 -0
  24. package/rules-on-demand/engineering/eng.work-rules.md +9 -3
  25. package/rules-on-demand/warm-up-consume-rules.md +30 -0
  26. package/skills/AGENTS.md +1 -1
  27. package/skills/eng-global-pr/SKILL.md +3 -1
  28. package/skills/eng-global-task-comment/SKILL.md +2 -0
  29. package/skills/jarvis-docs-central/SKILL.md +1 -1
  30. package/skills/jarvis-init/SKILL.md +60 -17
  31. package/skills/jarvis-report-issue/SKILL.md +64 -9
  32. package/templates/AGENTS.md +2 -0
  33. package/templates/ENV-template.md +11 -1
  34. package/templates/cdd-mind-map-template.md +26 -0
  35. package/templates/engineering/architecture-template.md +2 -1
  36. package/templates/engineering/plan-template.md +2 -0
  37. package/templates/product/prod-discovery-template.md +39 -71
  38. package/templates/product/prod-epic-template.md +1 -1
  39. package/templates/product/prod-frd-template.md +1 -1
  40. package/templates/product/prod-issue-template.md +1 -1
  41. package/templates/product/prod-prd-template.md +1 -1
  42. package/templates/warm-up-template.md +45 -0
  43. package/workflows/engineering/eng.plan.md +2 -1
  44. package/workflows/engineering/eng.pr.md +39 -6
  45. package/workflows/engineering/eng.pre-pr.md +2 -1
  46. package/workflows/engineering/eng.spec-consolidate.md +3 -1
  47. package/workflows/engineering/eng.start.md +12 -2
  48. package/workflows/engineering/eng.work.md +22 -5
  49. package/workflows/product/prod.spec.ard.md +2 -0
  50. package/workflows/product/prod.spec.breakdown.md +2 -0
  51. package/workflows/product/prod.spec.clarify.md +2 -0
  52. package/workflows/product/prod.spec.discovery.md +56 -116
  53. package/workflows/product/prod.spec.epic.md +2 -0
  54. package/workflows/product/prod.spec.frd.md +2 -0
  55. package/workflows/product/prod.spec.issue.md +2 -0
  56. package/workflows/product/prod.spec.md +2 -0
  57. package/workflows/product/prod.spec.prd.md +2 -0
  58. package/workflows/warm-up.md +68 -483
@@ -0,0 +1,259 @@
1
+ // Documentos de trabalho nos caminhos antigos → docs/jarvis/… (`jarvis init --upgrade`).
2
+ // Mesmas regras do item 8 do Upgrade (C) do skill jarvis-init: conflito no destino pula,
3
+ // pasta antiga removida só se ficar vazia, `git mv` quando versionado, `related_discovery`
4
+ // reescrito pelo nome do arquivo e, na recusa, só chaves novas recebem o caminho antigo.
5
+ import {
6
+ existsSync,
7
+ readdirSync,
8
+ statSync,
9
+ mkdirSync,
10
+ renameSync,
11
+ rmdirSync,
12
+ readFileSync,
13
+ writeFileSync,
14
+ appendFileSync,
15
+ } from "node:fs";
16
+ import { join, resolve, relative, dirname, basename, isAbsolute, sep } from "node:path";
17
+ import { execFileSync } from "node:child_process";
18
+
19
+ const ENG_SUBDIRS = ["ARD", "RFC", "qa", "swagger", "openapi", "robots", "c4"];
20
+ const LEGACY_ENG_SUBDIRS = ["robots", "swagger", "openapi", "c4"];
21
+
22
+ /**
23
+ * Resolve `$NOME` com as outras chaves do ENV, **em qualquer ordem**: upgrades antigos
24
+ * acrescentaram `JARVIS_DOCS` no fim do arquivo, depois do `PROD_DOCS` que a usa.
25
+ * Desconhecida (ex.: `$IDE`) ou ciclo fica literal.
26
+ * @param {Map<string, string>} entries
27
+ * @returns {Map<string, string>}
28
+ */
29
+ export function expandEnv(entries) {
30
+ const out = new Map();
31
+ const resolving = new Set();
32
+ const resolveKey = (key) => {
33
+ if (out.has(key)) return out.get(key);
34
+ if (!entries.has(key) || resolving.has(key)) return null;
35
+ resolving.add(key);
36
+ const value = entries
37
+ .get(key)
38
+ .replace(/\$([A-Z][A-Z0-9_]*)/g, (m, name) => resolveKey(name) ?? m);
39
+ resolving.delete(key);
40
+ out.set(key, value);
41
+ return value;
42
+ };
43
+ for (const key of entries.keys()) resolveKey(key);
44
+ return out;
45
+ }
46
+
47
+ const clean = (p) => p.replace(/^\.\//, "").replace(/\/+$/, "");
48
+
49
+ /** Pastas de documentos resolvidas, com os padrões do template quando faltam. */
50
+ export function docsFolders(env) {
51
+ const docs = clean(env.get("DOCS_FOLDER") || "docs");
52
+ const jarvis = clean(env.get("JARVIS_DOCS") || `${docs}/jarvis`);
53
+ const prod = clean(env.get("PROD_DOCS") || `${jarvis}/product`);
54
+ const eng = clean(env.get("ENG_DOCS") || `${jarvis}/engineering`);
55
+ const discoveries = clean(env.get("DISCOVERIES_DOCS") || `${jarvis}/discoveries`);
56
+ return { docs, jarvis, prod, eng, discoveries };
57
+ }
58
+
59
+ function inside(workspaceDir, rel) {
60
+ if (isAbsolute(rel)) return false;
61
+ const r = relative(workspaceDir, resolve(workspaceDir, rel));
62
+ return r !== "" && !r.startsWith("..") && !isAbsolute(r);
63
+ }
64
+
65
+ function listFiles(dir) {
66
+ const files = [];
67
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
68
+ const p = join(dir, entry.name);
69
+ if (entry.isDirectory()) files.push(...listFiles(p));
70
+ else if (entry.isFile()) files.push(p);
71
+ }
72
+ return files;
73
+ }
74
+
75
+ /** Pares pasta antiga → pasta nova, na ordem do item 8 + discoveries. */
76
+ function sourcePairs(f) {
77
+ const pairs = [
78
+ { from: `${f.docs}/product`, to: f.prod, discoveriesSub: true },
79
+ ...ENG_SUBDIRS.map((s) => ({ from: `${f.docs}/engineering/${s}`, to: `${f.eng}/${s}` })),
80
+ ...LEGACY_ENG_SUBDIRS.map((s) => ({ from: `docs/engineering/${s}`, to: `${f.eng}/${s}` })),
81
+ { from: `${f.prod}/discoveries`, to: f.discoveries, discovery: true },
82
+ ];
83
+ const seen = new Set();
84
+ return pairs.filter((p) => {
85
+ if (p.from === p.to || seen.has(p.from)) return false;
86
+ seen.add(p.from);
87
+ return true;
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Planeja a mudança de pasta (sem mexer em nada).
93
+ * @param {string} workspaceDir — pasta que contém `.{ide}/`
94
+ * @param {Map<string, string>} env — ENV já expandido (`expandEnv`)
95
+ * @returns {{ moves: {from: string, to: string, discovery: boolean}[],
96
+ * skipped: {from: string, to: string, reason: string}[], sources: string[] }}
97
+ * Caminhos relativos ao workspace.
98
+ */
99
+ export function planDocsMigration(workspaceDir, env) {
100
+ const f = docsFolders(env);
101
+ const moves = [];
102
+ const skipped = [];
103
+ const sources = [];
104
+
105
+ for (const pair of sourcePairs(f)) {
106
+ if (!inside(workspaceDir, pair.from) || !inside(workspaceDir, pair.to)) {
107
+ skipped.push({ from: pair.from, to: pair.to, reason: "fora do workspace" });
108
+ continue;
109
+ }
110
+ const fromAbs = join(workspaceDir, pair.from);
111
+ if (!existsSync(fromAbs) || !statSync(fromAbs).isDirectory()) continue;
112
+
113
+ let added = false;
114
+ for (const fileAbs of listFiles(fromAbs)) {
115
+ const rel = relative(fromAbs, fileAbs).split(sep).join("/");
116
+ // docs/product/discoveries/* (layout antigo) vai direto para a pasta dos discoveries
117
+ const toDiscoveries =
118
+ pair.discovery || (pair.discoveriesSub && rel.startsWith("discoveries/"));
119
+ const to = toDiscoveries
120
+ ? `${f.discoveries}/${pair.discovery ? rel : rel.slice("discoveries/".length)}`
121
+ : `${pair.to}/${rel}`;
122
+ const from = `${pair.from}/${rel}`;
123
+ if (from === to) continue;
124
+ if (existsSync(join(workspaceDir, to)) || moves.some((m) => m.to === to)) {
125
+ skipped.push({ from, to, reason: "conflito" });
126
+ continue;
127
+ }
128
+ moves.push({ from, to, discovery: toDiscoveries });
129
+ added = true;
130
+ }
131
+ if (added) sources.push(pair.from);
132
+ }
133
+ return { moves, skipped, sources };
134
+ }
135
+
136
+ function isTracked(fileAbs) {
137
+ try {
138
+ execFileSync("git", ["ls-files", "--error-unmatch", basename(fileAbs)], {
139
+ cwd: dirname(fileAbs),
140
+ stdio: "ignore",
141
+ });
142
+ return true;
143
+ } catch {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ function removeEmptyDirs(dirAbs, stopAbs) {
149
+ let dir = dirAbs;
150
+ while (dir.startsWith(stopAbs) && dir !== stopAbs && existsSync(dir)) {
151
+ if (readdirSync(dir).length > 0) break;
152
+ rmdirSync(dir);
153
+ dir = dirname(dir);
154
+ }
155
+ }
156
+
157
+ function emptyDirsUnder(dirAbs) {
158
+ if (!existsSync(dirAbs)) return;
159
+ for (const entry of readdirSync(dirAbs, { withFileTypes: true })) {
160
+ if (entry.isDirectory()) emptyDirsUnder(join(dirAbs, entry.name));
161
+ }
162
+ if (readdirSync(dirAbs).length === 0) rmdirSync(dirAbs);
163
+ }
164
+
165
+ /**
166
+ * Aplica o plano: `git mv` se o arquivo é versionado, senão `rename`. Remove as pastas
167
+ * antigas só se ficarem vazias e garante `{JARVIS_DOCS}/` no `.gitignore` do workspace.
168
+ * @returns {{ moved: {from: string, to: string, discovery: boolean}[], errors: string[] }}
169
+ */
170
+ export function applyDocsMigration(workspaceDir, plan, env) {
171
+ const moved = [];
172
+ const errors = [];
173
+ for (const m of plan.moves) {
174
+ const fromAbs = join(workspaceDir, m.from);
175
+ const toAbs = join(workspaceDir, m.to);
176
+ try {
177
+ mkdirSync(dirname(toAbs), { recursive: true });
178
+ if (isTracked(fromAbs)) {
179
+ execFileSync("git", ["mv", fromAbs, toAbs], { cwd: dirname(fromAbs), stdio: "ignore" });
180
+ } else {
181
+ renameSync(fromAbs, toAbs);
182
+ }
183
+ moved.push(m);
184
+ } catch (err) {
185
+ errors.push(`${m.from}: ${err.message.split("\n")[0]}`);
186
+ }
187
+ }
188
+
189
+ // Pastas antigas vazias saem; a pasta raiz dos documentos ($DOCS_FOLDER) sempre fica.
190
+ const docsAbs = join(workspaceDir, docsFolders(env).docs);
191
+ for (const src of plan.sources) {
192
+ const srcAbs = join(workspaceDir, src);
193
+ emptyDirsUnder(srcAbs);
194
+ removeEmptyDirs(dirname(srcAbs), docsAbs);
195
+ }
196
+
197
+ if (moved.length > 0) ensureGitignore(workspaceDir, `${docsFolders(env).jarvis}/`);
198
+ return { moved, errors };
199
+ }
200
+
201
+ function ensureGitignore(workspaceDir, line) {
202
+ const p = join(workspaceDir, ".gitignore");
203
+ const text = existsSync(p) ? readFileSync(p, "utf-8") : "";
204
+ const bare = line.replace(/\/$/, "");
205
+ if (text.split("\n").some((l) => [line, bare, `/${line}`, `/${bare}`].includes(l.trim()))) return;
206
+ appendFileSync(p, `${text && !text.endsWith("\n") ? "\n" : ""}${line}\n`);
207
+ }
208
+
209
+ /**
210
+ * Nos `.md` de `PROD_DOCS`, todo `related_discovery:` cujo nome de arquivo seja o de um
211
+ * discovery movido passa ao caminho novo — inclusive valores que já estavam quebrados.
212
+ * @returns {string[]} arquivos alterados (relativos ao workspace)
213
+ */
214
+ export function rewriteRelatedDiscovery(workspaceDir, env, moved) {
215
+ const byName = new Map(moved.filter((m) => m.discovery).map((m) => [basename(m.to), m.to]));
216
+ if (byName.size === 0) return [];
217
+ const prodAbs = join(workspaceDir, docsFolders(env).prod);
218
+ if (!existsSync(prodAbs)) return [];
219
+
220
+ const changed = [];
221
+ for (const fileAbs of listFiles(prodAbs).filter((p) => p.endsWith(".md"))) {
222
+ const text = readFileSync(fileAbs, "utf-8");
223
+ const next = text.replace(
224
+ /^(related_discovery:[ \t]*)(\S+)[ \t]*$/gm,
225
+ (line, prefix, value) => {
226
+ const to = byName.get(basename(value));
227
+ return to && value !== to ? `${prefix}${to}` : line;
228
+ }
229
+ );
230
+ if (next !== text) {
231
+ writeFileSync(fileAbs, next);
232
+ changed.push(relative(workspaceDir, fileAbs).split(sep).join("/"));
233
+ }
234
+ }
235
+ return changed;
236
+ }
237
+
238
+ /**
239
+ * Recusa em mover: o valor que as chaves **novas** recebem para continuar no caminho antigo.
240
+ * Chave que já existe no ENV não muda (o upgrade só acrescenta).
241
+ * @param {{moves: {from: string, discovery: boolean}[]}} plan
242
+ * @param {string[]} addedKeys — chaves que o upgrade vai acrescentar
243
+ * @returns {Record<string, string>}
244
+ */
245
+ export function declineEnvValues(plan, addedKeys, env) {
246
+ const added = new Set(addedKeys);
247
+ const f = docsFolders(env);
248
+ const out = {};
249
+ if (added.has("DISCOVERIES_DOCS") && plan.moves.some((m) => m.discovery)) {
250
+ out.DISCOVERIES_DOCS = "$PROD_DOCS/discoveries";
251
+ }
252
+ if (added.has("PROD_DOCS") && plan.moves.some((m) => m.from.startsWith(`${f.docs}/product/`))) {
253
+ out.PROD_DOCS = "$DOCS_FOLDER/product";
254
+ }
255
+ if (added.has("ENG_DOCS") && plan.moves.some((m) => m.from.includes("/engineering/"))) {
256
+ out.ENG_DOCS = "$DOCS_FOLDER/engineering";
257
+ }
258
+ return out;
259
+ }
@@ -0,0 +1,84 @@
1
+ // Upgrade do ENV.md (`jarvis init --upgrade`): acrescenta as chaves do template que faltam.
2
+ // Regra: só acrescenta. Nenhuma chave existente muda, preenchida, vazia ou com lista de opções.
3
+ // Mesmas regras do Upgrade (C) do skill jarvis-init (itens 4, 5, 9, 10 e 11).
4
+ import { readFileSync, writeFileSync } from "node:fs";
5
+
6
+ const KEY_LINE = /^([A-Z][A-Z0-9_]*)=(.*)$/;
7
+
8
+ // PROD_MODE nova: o valor vem da AREA (só no upgrade; a criação do ENV continua perguntando).
9
+ const PROD_MODE_BY_AREA = { ENGINEERING: "developer-guided", PRODUCT: "product-led" };
10
+
11
+ /**
12
+ * Lê as chaves do ENV na ordem do arquivo, **incluindo as vazias** (o `loadEnv`
13
+ * descarta as vazias, o que esconderia uma chave existente do diff).
14
+ * Linhas comentadas não contam.
15
+ * @returns {Map<string, string>}
16
+ */
17
+ export function parseEnvEntries(text) {
18
+ const entries = new Map();
19
+ for (const line of text.split("\n")) {
20
+ const m = line.trim().match(KEY_LINE);
21
+ if (m && !entries.has(m[1])) entries.set(m[1], m[2].trim());
22
+ }
23
+ return entries;
24
+ }
25
+
26
+ function isOptionList(value) {
27
+ return /^\[.*\]$/.test(value);
28
+ }
29
+
30
+ // Bloco "Data Engineering": DATA_* só entram com HUB=DATA. DATA_SPECIALIZATIONS é
31
+ // obrigatória em qualquer HUB (eng.specializations-rules.md) e nunca é excluída.
32
+ function isDataOnlyKey(key) {
33
+ return key.startsWith("DATA_") && key !== "DATA_SPECIALIZATIONS";
34
+ }
35
+
36
+ /**
37
+ * Calcula o que o upgrade acrescenta, sem gravar nada.
38
+ * @returns {{ added: {key: string, value: string}[], pending: string[], warnings: string[] }}
39
+ * `pending` e `warnings` trazem só nomes de chave, nunca valores do ENV.
40
+ */
41
+ export function planEnvUpgrade(envText, templateText) {
42
+ const env = parseEnvEntries(envText);
43
+ const template = parseEnvEntries(templateText);
44
+ const hub = (env.get("HUB") || "").toUpperCase();
45
+ const area = (env.get("AREA") || "").toUpperCase();
46
+
47
+ const added = [];
48
+ const pending = [];
49
+ const warnings = [];
50
+
51
+ for (const [key, templateValue] of template) {
52
+ if (env.has(key)) continue;
53
+ if (hub !== "DATA" && isDataOnlyKey(key)) continue;
54
+
55
+ let value = isOptionList(templateValue) ? "" : templateValue;
56
+ if (key === "PROD_MODE") {
57
+ value = PROD_MODE_BY_AREA[area] || "";
58
+ if (!value) pending.push("PROD_MODE");
59
+ }
60
+ added.push({ key, value });
61
+ }
62
+
63
+ if (env.has("PROD_MODE") && !env.get("PROD_MODE")) pending.push("PROD_MODE");
64
+
65
+ if (env.get("CENTRAL_DOCS_REPO") && !env.has("CENTRAL_DOCS_STORAGE")) {
66
+ warnings.push("CENTRAL_DOCS_REPO");
67
+ }
68
+
69
+ return { added, pending, warnings };
70
+ }
71
+
72
+ /**
73
+ * Grava as chaves novas no fim do ENV, num bloco datado. Nenhuma linha existente muda.
74
+ * `overrides` troca o valor de chaves novas (ex.: recusa em mover os discoveries).
75
+ * @returns {boolean} true se gravou
76
+ */
77
+ export function applyEnvUpgrade(envPath, plan, today, overrides = {}) {
78
+ if (plan.added.length === 0) return false;
79
+ const text = readFileSync(envPath, "utf-8");
80
+ const lines = plan.added.map(({ key, value }) => `${key}=${overrides[key] ?? value}`);
81
+ const sep = text.endsWith("\n") ? "" : "\n";
82
+ writeFileSync(envPath, `${text}${sep}\n# --- Upgrade ${today} ---\n${lines.join("\n")}\n`);
83
+ return true;
84
+ }
@@ -7,7 +7,9 @@
7
7
  * `rules/AGENTS.md`: ao mudar uma, mude as três.
8
8
  *
9
9
  * Regras:
10
- * - Os quatro eixos (HUB, POSITION, AREA, SQUAD) são combinados com AND.
10
+ * - Os eixos (HUB, POSITION, AREA, SQUAD e o opcional ARTIFACT_STYLE) são combinados com AND.
11
+ * - ARTIFACT_STYLE (5º campo, opcional): ausente no bloco = `all`; perfil sem valor =
12
+ * `ai-generated`. Sem coringa: FULLCYCLE e GENERALIST não o satisfazem (é escolha da pessoa).
11
13
  * - `all` casa qualquer valor; listas são separadas por vírgula.
12
14
  * - FULLCYCLE (HUB) satisfaz qualquer HUB.
13
15
  * - GENERALIST (POSITION) satisfaz qualquer POSITION e qualquer AREA.
@@ -20,8 +22,11 @@
20
22
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
21
23
  import { join, relative, sep } from "node:path";
22
24
 
25
+ // SQUAD para no próximo `|`: o 5º campo (ARTIFACT_STYLE) é opcional.
23
26
  const APPLIES_TO_RE =
24
- /> \*\*Applies to:\*\* HUB: ([^|]+)\| POSITION: ([^|]+)\| AREA: ([^|]+)\| SQUAD: (.+)/;
27
+ /> \*\*Applies to:\*\* HUB: ([^|]+)\| POSITION: ([^|]+)\| AREA: ([^|]+)\| SQUAD: ([^|\n]+)(?:\| ARTIFACT_STYLE: ([^|\n]+))?/;
28
+
29
+ const DEFAULT_ARTIFACT_STYLE = "ai-generated";
25
30
 
26
31
  /**
27
32
  * @typedef {Object} AppliesTo
@@ -29,6 +34,7 @@ const APPLIES_TO_RE =
29
34
  * @property {string} POSITION
30
35
  * @property {string} AREA
31
36
  * @property {string} SQUAD
37
+ * @property {string} ARTIFACT_STYLE - `all` quando o bloco não traz o campo
32
38
  */
33
39
 
34
40
  /**
@@ -37,6 +43,7 @@ const APPLIES_TO_RE =
37
43
  * @property {string} POSITION
38
44
  * @property {string} AREA
39
45
  * @property {string} SQUAD
46
+ * @property {string} [ARTIFACT_STYLE] - vazio = `ai-generated`
40
47
  */
41
48
 
42
49
  /**
@@ -52,6 +59,7 @@ export function parseAppliesTo(content) {
52
59
  POSITION: match[2].trim(),
53
60
  AREA: match[3].trim(),
54
61
  SQUAD: match[4].trim(),
62
+ ARTIFACT_STYLE: (match[5] || "all").trim(),
55
63
  };
56
64
  }
57
65
 
@@ -76,7 +84,11 @@ export function matchesProfile(appliesTo, profile) {
76
84
  const position = generalist || axisMatches(appliesTo.POSITION, profile.POSITION);
77
85
  const area = generalist || axisMatches(appliesTo.AREA, profile.AREA);
78
86
  const squad = axisMatches(appliesTo.SQUAD, profile.SQUAD);
79
- return hub && position && area && squad;
87
+ const style = axisMatches(
88
+ appliesTo.ARTIFACT_STYLE || "all",
89
+ profile.ARTIFACT_STYLE || DEFAULT_ARTIFACT_STYLE
90
+ );
91
+ return hub && position && area && squad && style;
80
92
  }
81
93
 
82
94
  function walkMarkdown(dir) {
@@ -149,7 +149,7 @@ export function syncAssets(targetDir, ide, opts = {}) {
149
149
 
150
150
  // RTK: detectar e reconciliar RTK_ENABLED + rtk-rules.md
151
151
  if (!opts.dryRun) {
152
- const rtkResult = reconcileRtk(ideDir, targetDir, ide);
152
+ const rtkResult = reconcileRtk(ideDir, targetDir, ide, { keepEnvValues: opts.keepEnvValues });
153
153
  if (rtkResult) result.copied.push(rtkResult);
154
154
  }
155
155
 
@@ -191,9 +191,11 @@ export function syncAssets(targetDir, ide, opts = {}) {
191
191
  * Detecta RTK no PATH e reconcilia RTK_ENABLED no ENV.md + rtk-rules.md na IDE.
192
192
  * - RTK encontrado → RTK_ENABLED=true no ENV.md, mantém rule
193
193
  * - RTK não encontrado → remove rtk-rules.md da IDE, mantém RTK_ENABLED=false
194
+ * - `keepEnvValues` (jarvis init --upgrade): não troca RTK_ENABLED=false por true, porque o
195
+ * upgrade só acrescenta chaves novas; acrescentar RTK_ENABLED quando ela não existe continua valendo
194
196
  * @returns {string|null} mensagem de log ou null se nenhuma ação
195
197
  */
196
- function reconcileRtk(ideDir, targetDir, ide) {
198
+ function reconcileRtk(ideDir, targetDir, ide, { keepEnvValues = false } = {}) {
197
199
  const rtkRulePath = join(ideDir, "rules", "rtk-rules.md");
198
200
  const envPath = join(ideDir, "ENV.md");
199
201
  const hasRtk = (() => {
@@ -209,7 +211,7 @@ function reconcileRtk(ideDir, targetDir, ide) {
209
211
  // Ativar RTK_ENABLED no ENV.md se existir e estiver false
210
212
  if (existsSync(envPath)) {
211
213
  let env = readFileSync(envPath, "utf-8");
212
- if (env.includes("RTK_ENABLED=false")) {
214
+ if (env.includes("RTK_ENABLED=false") && !keepEnvValues) {
213
215
  env = env.replace("RTK_ENABLED=false", "RTK_ENABLED=true");
214
216
  writeFileSync(envPath, env, "utf-8");
215
217
  return "(rtk) detectado — RTK_ENABLED=true no ENV.md";
@@ -91,6 +91,7 @@ export function buildReport(root, profile, opts = {}) {
91
91
  POSITION: profile.POSITION,
92
92
  AREA: profile.AREA,
93
93
  SQUAD: profile.SQUAD,
94
+ ARTIFACT_STYLE: profile.ARTIFACT_STYLE || "ai-generated",
94
95
  },
95
96
  rtkEnabled,
96
97
  estimate: `tokens = bytes ÷ ${BYTES_PER_TOKEN} (estimativa)`,
@@ -53,7 +53,8 @@ export function renderIndex(root, squad) {
53
53
  const areaDir = join(root, squad, area);
54
54
  const files = walkFiles(areaDir).filter((f) => {
55
55
  const name = f.split(sep).pop();
56
- return name.startsWith(prefix) && name.endsWith(".md");
56
+ // {nome}.public.md (versão humanizada, ARTIFACT_STYLE=human-generated) não entra no índice
57
+ return name.startsWith(prefix) && name.endsWith(".md") && !name.endsWith(".public.md");
57
58
  });
58
59
  out.push(`## ${area}`, "", `> ${title}`, "");
59
60
  if (files.length === 0) {
@@ -108,6 +108,14 @@ export async function createMergeRequest(repoUrl, opts) {
108
108
  };
109
109
  }
110
110
 
111
+ // Tipos de issue do Bitbucket (campo `kind`; ele não tem labels livres).
112
+ const BITBUCKET_KINDS = ["bug", "enhancement", "proposal", "task"];
113
+
114
+ /**
115
+ * @param {string} repoUrl
116
+ * @param {{ title: string, body: string, labels?: string[] }} opts - `labels` padrão: ["bug"].
117
+ * No Bitbucket, a primeira label vira o `kind` quando é um tipo válido; senão, `bug`.
118
+ */
111
119
  export async function createIssue(repoUrl, opts) {
112
120
  const { title, body, labels = ["bug"] } = opts;
113
121
  const repo = parseRepoUrl(repoUrl);
@@ -149,7 +157,11 @@ export async function createIssue(repoUrl, opts) {
149
157
  {
150
158
  method: "POST",
151
159
  headers,
152
- body: { title, content: { raw: body } },
160
+ body: {
161
+ title,
162
+ content: { raw: body },
163
+ kind: BITBUCKET_KINDS.includes(labels[0]) ? labels[0] : "bug",
164
+ },
153
165
  }
154
166
  );
155
167
  if (!ok || !json?.links?.html?.href) {
@@ -9,11 +9,25 @@ import { createIssue, parseRepoUrl } from "./api.js";
9
9
  * @param {string} options.repo - URL ou `org/repo` do repositório
10
10
  * @param {string} options.title - Título da issue
11
11
  * @param {string} options.bodyFile - Path de um arquivo com o corpo em markdown
12
+ * @param {string} [options.labels] - Labels separadas por vírgula (padrão: bug)
12
13
  */
13
- export async function runCreateIssue({ repo, title, bodyFile }) {
14
+ /**
15
+ * "enhancement, documentation" → ["enhancement", "documentation"]; vazio → undefined (padrão: bug).
16
+ * @param {string} [raw]
17
+ * @returns {string[] | undefined}
18
+ */
19
+ export function parseLabels(raw) {
20
+ const list = String(raw || "")
21
+ .split(",")
22
+ .map((l) => l.trim())
23
+ .filter(Boolean);
24
+ return list.length ? list : undefined;
25
+ }
26
+
27
+ export async function runCreateIssue({ repo, title, bodyFile, labels }) {
14
28
  if (!title || !bodyFile || !repo) {
15
29
  console.error(
16
- "Uso: jarvis vcs create-issue --repo <url-ou-path> --title <titulo> --body-file <path>"
30
+ "Uso: jarvis vcs create-issue --repo <url-ou-path> --title <titulo> --body-file <path> [--label <labels>]"
17
31
  );
18
32
  process.exit(1);
19
33
  }
@@ -33,7 +47,7 @@ export async function runCreateIssue({ repo, title, bodyFile }) {
33
47
  const body = existsSync(bodyFile) ? readFileSync(bodyFile, "utf-8") : "";
34
48
 
35
49
  try {
36
- const result = await createIssue(repoUrl, { title, body });
50
+ const result = await createIssue(repoUrl, { title, body, labels: parseLabels(labels) });
37
51
  parseRepoUrl(repoUrl);
38
52
  console.log(`✅ Issue aberto: ${result.url}`);
39
53
  console.log(JSON.stringify(result));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jarvis-ai-framework",
3
- "version": "5.1.0",
3
+ "version": "5.5.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",
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",
15
15
  "lint": "eslint bin/ test/",
16
16
  "lint:fix": "eslint bin/ test/ --fix",
17
17
  "format": "prettier --write bin/ test/",
package/rules/AGENTS.md CHANGED
@@ -32,8 +32,9 @@ rules-on-demand/ # Lidas pelo workflow que as usa (não carregadas
32
32
  │ ├── eng.pr-rules.md # Regras para /eng.pr
33
33
  │ ├── eng.tech-spec-rules.md # Regras para tech specs
34
34
  │ └── eng.breakdown-subtasks-rules.md # Regras para o breakdown de subtarefas
35
- └── product/
36
- └── prod.guided-rules.md # Modo Developer-Guided (lida só com PROD_MODE=developer-guided)
35
+ ├── product/
36
+ │ └── prod.guided-rules.md # Modo Developer-Guided (lida só com PROD_MODE=developer-guided)
37
+ └── warm-up-consume-rules.md # Consumo do /warm-up (lida pelo eng.start e pelos prod.spec* só quando há aquecimento)
37
38
  ```
38
39
 
39
40
  ### `rules/` x `rules-on-demand/`
@@ -74,6 +75,8 @@ Cada arquivo de rule declara para quais perfis se aplica via bloco `applies_to`,
74
75
  > **Applies to:** HUB: {valor ou all} | POSITION: {valor ou all} | AREA: {valor ou all} | SQUAD: {valor ou all}
75
76
  ```
76
77
 
78
+ Campo opcional no fim: `| ARTIFACT_STYLE: {ai-generated, human-generated ou all}` (ausente = `all`).
79
+
77
80
  **Eixos de filtragem:**
78
81
 
79
82
  | Eixo | Exemplos de valor | `all` significa |
@@ -82,13 +85,14 @@ Cada arquivo de rule declara para quais perfis se aplica via bloco `applies_to`,
82
85
  | POSITION | TECH LEAD, PM, QA-ENGINEER, SENIOR, GENERALIST | qualquer cargo |
83
86
  | AREA | ENGINEERING, PRODUCT | qualquer área |
84
87
  | SQUAD | CORE | qualquer squad |
88
+ | ARTIFACT_STYLE (opcional) | ai-generated, human-generated | qualquer estilo (ENV vazio = `ai-generated`; sem coringa) |
85
89
 
86
90
  **Regras:**
87
91
  - Uma rule é aplicada se **todos** os eixos forem satisfeitos (AND, não OR)
88
92
  - Arquivos sem bloco `applies_to` são considerados **universais** — copiados para qualquer perfil
89
93
  - `rules/AGENTS.md` nunca é filtrado — sempre copiado
90
94
 
91
- **Quem faz a filtragem:** o skill `/jarvis-init` (Passo 9 — Profile-Aware Rules Sync). Ao criar, atualizar ou fazer upgrade do ENV.md, copia para `$IDE/rules/` apenas as rules que batem com o perfil (HUB + POSITION + AREA + SQUAD) e deleta as que não batem mais.
95
+ **Quem faz a filtragem:** o skill `/jarvis-init` (Passo 8 — Profile-Aware Rules Sync). Ao criar, atualizar ou fazer upgrade do ENV.md, copia para `$IDE/rules/` apenas as rules que batem com o perfil (HUB + POSITION + AREA + SQUAD + ARTIFACT_STYLE) e deleta as que não batem mais.
92
96
 
93
97
  **Ao criar uma nova rule**, definir o bloco `applies_to` é obrigatório. Sem ele, a rule é tratada como universal — o que pode ser indesejado para rules domain-specific.
94
98
 
@@ -0,0 +1,53 @@
1
+ > **Applies to:** HUB: all | POSITION: all | AREA: all | SQUAD: all | ARTIFACT_STYLE: human-generated
2
+
3
+ # Artifact Style Rules (human-generated)
4
+
5
+ ## Objetivo
6
+
7
+ Com `ARTIFACT_STYLE=human-generated` no `ENV.md`, nada do que o Jarvis publica leva marca de IA, e todo documento gerado oferece uma versão humanizada para repassar a clientes ou stakeholders.
8
+
9
+ ## Escopo
10
+
11
+ Esta rule só é copiada para `$IDE/rules/` quando o perfil tem `ARTIFACT_STYLE=human-generated` (eixo do `Applies to`, Passo 8 do `/jarvis-init`). Vale para todo comando e para pedidos fora de workflow.
12
+
13
+ ## Regras
14
+
15
+ ### Obrigatório
16
+
17
+ 1. **Publicado pelo Jarvis: sempre sem marcas, sem perguntar.**
18
+ - Vale para: mensagem de commit, título e descrição do PR/MR, comentário no card, issue aberta (ex.: `jarvis-report-issue`).
19
+ - Esta rule **prevalece** sobre instruções de atribuição da IDE: não inclua `Co-Authored-By` de ferramenta nem "Generated with …".
20
+ - Antes do push, o `eng.pr` confere e limpa o que a IDE tiver posto (Fase 3 do `eng.pr`).
21
+ 2. **Documento gerado: grava o padrão e pergunta.**
22
+ - Vale para todo `.md` que um comando grava, inclusive os de sessão: PRD, FRD, épico, história, discovery, ARD, RFC, tech spec, Central docs, `architecture.md`, `plan.md`, `context.md`, `daily.md` (e a linha de timesheet), `gate-checklist.md`, `pull-request.md`.
23
+ - Depois de gravar o documento padrão (como hoje), pergunte com `AskUserQuestion`: **"Quer a versão humanizada deste documento?"** (**Não**, padrão / **Sim**).
24
+ - Com Sim, grave `{nome}.public.md` **ao lado** do original, passando no checklist abaixo. O original continua como fonte da verdade.
25
+ - Central docs (`feature-*`, `delivery-*`): o `.public.md` fica ao lado da consolidação, na sessão, e **não** é publicado.
26
+ 3. **Checklist "sem marcas de IA"** (versão humanizada e tudo o que é publicado). Não pode ter:
27
+ - `Co-Authored-By` de ferramenta, "Generated with …" ou links para a ferramenta;
28
+ - menções a IA, Claude ou outros modelos, assistente, agente, sub-agente ou Advisor;
29
+ - `recommended_model` nem comentários de frontmatter dirigidos à IA;
30
+ - "Reportado por: IA" ou qualquer autoria atribuída à ferramenta.
31
+ 4. **Estilo da versão humanizada:** texto corrido de pessoa para pessoa. Parágrafos onde cabe parágrafo; lista só quando há itens; sem emojis de status em excesso; sem fórmulas como "Em resumo", "Vale ressaltar". Mantenha o conteúdo, as decisões e os links úteis ao leitor (issue, PR, card).
32
+
33
+ ### Proibido
34
+
35
+ - Alterar o documento original para gerar a versão humanizada.
36
+ - Publicar commit, PR, comentário ou issue com qualquer item do checklist.
37
+ - Reescrever commits já enviados ao remoto.
38
+ - Contar `{nome}.public.md` na sequência de IDs (`prd-{id}`, `discovery-{id}`…) ou incluí-lo em índices.
39
+
40
+ ### Recomendado
41
+
42
+ - Com vários documentos gerados no mesmo comando, uma pergunta só, listando os documentos.
43
+
44
+ ## Exceções
45
+
46
+ - Com `ARTIFACT_STYLE=ai-generated` (ou vazio), esta rule não existe na sessão e nada muda.
47
+
48
+ ## Referências
49
+
50
+ - `templates/ENV-template.md` (`ARTIFACT_STYLE`)
51
+ - `skills/jarvis-init/SKILL.md` (Pergunta 9 e Passo 8)
52
+ - `workflows/engineering/eng.pr.md` (conferência antes do push)
53
+ - `rules/AGENTS.md` (eixo `ARTIFACT_STYLE` do `Applies to`)
@@ -40,12 +40,14 @@ PROD_RULES=$RULES_FOLDER/$PROD_FOLDER_NAME
40
40
  PROD_FLOWS=.$IDE/$FLOWS_FOLDER
41
41
  PROD_TEMPLATES=.$IDE/$TEMPLATES_FOLDER/$PROD_FOLDER_NAME
42
42
  PROD_DOCS=$DOCS_FOLDER/jarvis/$PROD_FOLDER_NAME
43
+ DISCOVERIES_DOCS=$DOCS_FOLDER/jarvis/discoveries
43
44
  ```
44
45
 
45
46
  Abaixo, segue uma descrição de algumas das variáveis mais importantes para as rotinas de produto:
46
47
 
47
48
  - $DOCS_FOLDER é o nome da pasta principal de documentações do projeto. Ela fica localizada na raiz do projeto.
48
- - $PROD_DOCS é a pasta dos documentos de produto gerados pelos comandos (PRD, FRD, épicos, histórias, discovery): `$JARVIS_DOCS/product` (`docs/jarvis/product`), nos dois modos de trabalho e fora do versionamento do código. Os de engenharia ficam em `$ENG_DOCS` (`docs/jarvis/engineering`).
49
+ - $PROD_DOCS é a pasta dos documentos de produto gerados pelos comandos (PRD, FRD, épicos, histórias): `$JARVIS_DOCS/product` (`docs/jarvis/product`), nos dois modos de trabalho e fora do versionamento do código. Os de engenharia ficam em `$ENG_DOCS` (`docs/jarvis/engineering`).
50
+ - $DISCOVERIES_DOCS é a pasta dos discoveries: `$JARVIS_DOCS/discoveries` (`docs/jarvis/discoveries`), fora de `product/`. ENV sem a variável: use `$PROD_DOCS/discoveries` se essa pasta existir, senão `$JARVIS_DOCS/discoveries`, e avise para rodar `jarvis init --upgrade`.
49
51
  - $PROD_FOLDER_NAME é o nome padrão da pasta de produto que será usada em diversos contextos do projeto.
50
52
  - $TEMPLATES_FOLDER é o nome da pasta principal de templates do projeto.
51
53
  - $PROD_TEMPLATES é o caminho completo para os templates de produto.
@@ -126,7 +128,7 @@ Dentro das pastas do projeto, os caminhos de pastas e o nome dos arquivos finais
126
128
  - Documento de Requisitos Funcionais: `$PROD_DOCS/prd-{id}-{nome-do-pai}/frd-{id}-{nome}.md`
127
129
  - Épico: `$PROD_DOCS/prd-{id}-{nome-do-pai}/issues/epic-{id}-{nome}.md`
128
130
  - História / tarefa: `$PROD_DOCS/prd-{id}-{nome-do-pai}/issues/{story|task}-{id}-{nome}.md`
129
- - Discovery (rascunho de uma ideia, anterior ao PRD): `$PROD_DOCS/discoveries/discovery-{id}-{nome}.md`. Usa `status` (`in_review` enquanto o rascunho existe; `cancelled` se o veredito for não seguir) e `verdict` (vazio durante a entrevista, `proceed`, `investigate_more` ou `do_not_proceed`). O veredito é uma recomendação; um PRD, FRD, épico ou issue criado depois prevalece como fonte da verdade.
131
+ - Discovery (rascunho de uma ideia, anterior ao PRD): `$DISCOVERIES_DOCS/discovery-{id}-{nome}.md`. Usa `status` (`in_review` enquanto o rascunho existe; `cancelled` se o veredito for não seguir) e `verdict` (vazio durante a entrevista, `proceed`, `investigate_more` ou `do_not_proceed`). O veredito é uma recomendação; um PRD, FRD, épico ou issue criado depois prevalece como fonte da verdade.
130
132
 
131
133
  O ID deve ser iterado nos novos arquivos seguindo a sequência existente.
132
134