jarvis-ai-framework 8.2.0 → 8.3.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.
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
2
2
  import { join, dirname, basename, resolve, sep } from "node:path";
3
3
  import { createInterface } from "node:readline";
4
- import { syncAssets } from "../lib/core/sync-engine.js";
4
+ import { syncAssets, applyRuleOverlay } from "../lib/core/sync-engine.js";
5
5
  import { discoverAll } from "../lib/core/scanner.js";
6
6
  import {
7
7
  IDES,
@@ -23,6 +23,7 @@ import {
23
23
  removeStaleSkills,
24
24
  } from "../lib/core/stale-skills.js";
25
25
  import { parseEnvEntries, planEnvUpgrade, applyEnvUpgrade } from "../lib/core/env-upgrade.js";
26
+ import { DEFAULT_COMPANY_PROFILE, isValidCompanyProfile } from "../lib/core/company-profile.js";
26
27
  import { checkCli, getVendor } from "../lib/integrations/cli-runner.js";
27
28
  import {
28
29
  expandEnv,
@@ -76,6 +77,32 @@ export function upgradePendencies({ pendingAxes, envPlan }) {
76
77
  return reasons;
77
78
  }
78
79
 
80
+ /**
81
+ * Pergunta do perfil de empresa no `--upgrade` (#199). Pura: recebe o `ask` (uma pergunta → a
82
+ * resposta) e o `warn` (um aviso na hora), e devolve o perfil próprio ou `null` para o default.
83
+ * Enter = default; nome inválido é perguntado mais uma vez; inválido de novo = default, com aviso.
84
+ * Testável sem terminal.
85
+ * @param {(question: string) => Promise<string>} ask
86
+ * @param {{ defaultProfile?: string, warn?: (message: string) => void }} [opts]
87
+ * @returns {Promise<string | null>}
88
+ */
89
+ export async function promptCompanyProfile(
90
+ ask,
91
+ { defaultProfile = DEFAULT_COMPANY_PROFILE, warn = () => {} } = {}
92
+ ) {
93
+ const prompt = `Perfil de empresa (COMPANY_PROFILE) [Enter = ${defaultProfile}]: `;
94
+ let answer = (await ask(prompt)).trim();
95
+ if (answer && !isValidCompanyProfile(answer)) {
96
+ warn(
97
+ "Nome inválido: use letras minúsculas, números e hífen, e não o nome de uma pasta do core. Tente de novo ou Enter para o default."
98
+ );
99
+ answer = (await ask(prompt)).trim();
100
+ }
101
+ if (answer && isValidCompanyProfile(answer)) return answer === defaultProfile ? null : answer;
102
+ if (answer) warn(`Nome inválido de novo: gravando o default ${defaultProfile}.`);
103
+ return null;
104
+ }
105
+
79
106
  /**
80
107
  * Leitor de linhas que não perde resposta: guarda as linhas que chegam antes da pergunta
81
108
  * (pipe com várias linhas, como `printf 's\n3\n'`) e devolve `null` quando a entrada fecha
@@ -564,11 +591,40 @@ async function runUpgrade(cwd, flags) {
564
591
  }
565
592
  }
566
593
 
594
+ // 3.2 Perfil de empresa (#199): a chave nova nasce com o default; com terminal e sem --yes,
595
+ // pergunta. Só o nome da chave e "default/definido" saem no resumo, nunca o valor.
596
+ let companyProfile = null;
597
+ if (addedKeys.includes("COMPANY_PROFILE")) {
598
+ if (!flags.yes && process.stdin.isTTY) {
599
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
600
+ companyProfile = await promptCompanyProfile((q) => ask(rl, q), {
601
+ warn: (m) => logger.info(`${YELLOW} ${m}${NC}`),
602
+ });
603
+ rl.close();
604
+ } else if (!flags.yes) {
605
+ logger.info(
606
+ `${DIM} Sem terminal interativo e sem --yes: COMPANY_PROFILE fica no default ${DEFAULT_COMPANY_PROFILE}.${NC}`
607
+ );
608
+ }
609
+ }
610
+
567
611
  const overrides =
568
612
  docsPlan.moves.length > 0 && !accepted ? declineEnvValues(docsPlan, addedKeys, env) : {};
613
+ if (companyProfile) overrides.COMPANY_PROFILE = companyProfile;
569
614
  const today = new Date().toISOString().slice(0, 10);
570
615
  applyEnvUpgrade(envPath, envPlan, today, overrides);
571
616
 
617
+ // 3.3 Perfil respondido agora: o sync acima rodou com o default. Reaplica o overlay do perfil
618
+ // gravado (pasta dele criada, rules dele por cima). A pasta do default criada antes fica: todo
619
+ // workspace a tem, e o upgrade não apaga nada em overlays/.
620
+ if (companyProfile) {
621
+ const again = { copied: [], skipped: [], errors: [] };
622
+ applyRuleOverlay(join(workspaceDir, `.${getIDEFolder(ide)}`), again);
623
+ for (const item of again.copied) logger.info(`${BLUE} ↳ ${item}${NC}`);
624
+ for (const item of again.skipped) logger.info(`${DIM} ↳ ${item}${NC}`);
625
+ for (const e of again.errors) logger.error(`${RED} ✗ ${e}${NC}`);
626
+ }
627
+
572
628
  // 3.1 Skills instalados que saíram do framework: só lista; remove com confirmação ou --remove-stale
573
629
  const ideSkillsDir = join(workspaceDir, `.${getIDEFolder(ide)}`, "skills");
574
630
  const stale = findStaleSkills({
@@ -619,6 +675,11 @@ async function runUpgrade(cwd, flags) {
619
675
  logger.info(
620
676
  ` • ENV.md: ${addedKeys.length ? `+ ${addedKeys.join(", ")}` : "nenhuma variável nova"}`
621
677
  );
678
+ if (addedKeys.includes("COMPANY_PROFILE")) {
679
+ logger.info(
680
+ ` • COMPANY_PROFILE: ${companyProfile ? "definido" : `default (${DEFAULT_COMPANY_PROFILE})`}`
681
+ );
682
+ }
622
683
  if (Object.keys(overrides).length) {
623
684
  logger.info(` • Mantidos no caminho antigo: ${Object.keys(overrides).join(", ")}`);
624
685
  }
@@ -25,6 +25,7 @@ import { docsPublish } from "../../commands/docs-publish.js";
25
25
  import { installRtk } from "../../commands/install-rtk.js";
26
26
  import { tokens } from "../../commands/tokens.js";
27
27
  import { map } from "../../commands/map.js";
28
+ import { fork } from "../../commands/fork.js";
28
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";
@@ -177,6 +178,32 @@ export function buildProgram() {
177
178
  .option("--env-file <path>")
178
179
  .action((options) => tokens(toLegacyFlags(options)));
179
180
 
181
+ cmd(program, "fork")
182
+ .description(
183
+ "Overlay de empresa: copia um artefato do core para $IDE/overlays/{COMPANY_PROFILE}/, com carimbo"
184
+ )
185
+ .argument(
186
+ "[nome]",
187
+ "Workflow, skill ou agente pelo nome, ou rules/…, rules-on-demand/…, templates/… por caminho"
188
+ )
189
+ .option(
190
+ "--list",
191
+ "Lista o que está forkado no overlay ativo (só lista; não compara com o core)"
192
+ )
193
+ .option(
194
+ "--update",
195
+ "Recarimba o fork com a versão atual do core (forked_from); o conteúdo não muda"
196
+ )
197
+ .option(
198
+ "--diff",
199
+ "Com --update: só mostra o diff fork × core atual, por seção (🔴 em possível mudança de contrato), sem gravar"
200
+ )
201
+ .option("--delete", "Remove o fork do overlay; rules voltam à versão do core na IDE")
202
+ .option("--candidate", "Marca o fork como candidato a core (core_candidate: true)")
203
+ .option("--ide <ide>")
204
+ .option("--env-file <path>")
205
+ .action((nome, options) => fork(toLegacyFlags(options, { _: [nome].filter(Boolean) })));
206
+
180
207
  cmd(program, "map")
181
208
  .description("Mapa de chamadas de um workflow, skill ou agente")
182
209
  .argument("[name]", "Nome do workflow, skill ou agente")
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @fileoverview Perfil de empresa (`COMPANY_PROFILE`, #199): nome do overlay ativo em
3
+ * `$IDE/overlays/{perfil}/`. A chave nunca fica vazia: o default é `stark-industries`,
4
+ * o overlay padrão do Jarvis. Um nome fora do padrão ou igual a uma pasta do core cai
5
+ * no default, com aviso. Este módulo só decide o nome; quem lê e grava arquivos é quem o chama.
6
+ * @module core/company-profile
7
+ */
8
+
9
+ /** @constant {string} Perfil default: o overlay padrão de todo workspace */
10
+ export const DEFAULT_COMPANY_PROFILE = "stark-industries";
11
+
12
+ /** @constant {RegExp} Mesmo padrão dos nomes de skill (minúsculas, números e hífen) */
13
+ export const COMPANY_PROFILE_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
14
+
15
+ /**
16
+ * Pastas do core que um perfil não pode usar como nome: o overlay espelha estas pastas,
17
+ * e `overlays/rules/` ficaria ambíguo com `rules/`.
18
+ * @constant {ReadonlyArray<string>}
19
+ */
20
+ export const CORE_FOLDERS = Object.freeze([
21
+ "agents",
22
+ "skills",
23
+ "workflows",
24
+ "commands",
25
+ "rules",
26
+ "rules-on-demand",
27
+ "templates",
28
+ "scripts",
29
+ "overlays",
30
+ ]);
31
+
32
+ /**
33
+ * @param {string} name
34
+ * @returns {boolean} true se o nome serve como perfil (padrão e fora das pastas do core)
35
+ */
36
+ export function isValidCompanyProfile(name) {
37
+ return COMPANY_PROFILE_PATTERN.test(name) && !CORE_FOLDERS.includes(name);
38
+ }
39
+
40
+ /**
41
+ * Resolve o perfil ativo a partir do valor lido do `ENV.md` (já sem aspas e comentário).
42
+ * Vazio ou ausente → default com aviso ℹ️ (ENV editado à mão ou anterior à #199);
43
+ * inválido → default com aviso ⚠️. O aviso vai no retorno, nunca em log: quem chama decide.
44
+ * @param {string | undefined | null} raw - valor de `COMPANY_PROFILE`
45
+ * @returns {{ profile: string, warning: string | null, isDefault: boolean }}
46
+ */
47
+ export function resolveCompanyProfile(raw) {
48
+ const value = (raw ?? "").trim();
49
+ if (!value) {
50
+ return {
51
+ profile: DEFAULT_COMPANY_PROFILE,
52
+ isDefault: true,
53
+ warning:
54
+ raw === undefined || raw === null
55
+ ? null
56
+ : `ℹ️ COMPANY_PROFILE vazio: usando o perfil default ${DEFAULT_COMPANY_PROFILE}. Rode /jarvis-init para definir um perfil próprio.`,
57
+ };
58
+ }
59
+ if (CORE_FOLDERS.includes(value)) {
60
+ return {
61
+ profile: DEFAULT_COMPANY_PROFILE,
62
+ isDefault: true,
63
+ warning: `⚠️ COMPANY_PROFILE não pode ser o nome de uma pasta do core (${CORE_FOLDERS.join(", ")}). Usando o perfil default ${DEFAULT_COMPANY_PROFILE}.`,
64
+ };
65
+ }
66
+ if (!COMPANY_PROFILE_PATTERN.test(value)) {
67
+ return {
68
+ profile: DEFAULT_COMPANY_PROFILE,
69
+ isDefault: true,
70
+ warning: `⚠️ COMPANY_PROFILE "${value}" inválido (use letras minúsculas, números e hífen). Usando o perfil default ${DEFAULT_COMPANY_PROFILE}.`,
71
+ };
72
+ }
73
+ return { profile: value, isDefault: value === DEFAULT_COMPANY_PROFILE, warning: null };
74
+ }
75
+
76
+ /**
77
+ * Lê `COMPANY_PROFILE=` de um texto de `ENV.md` (primeira ocorrência, sem aspas nem comentário
78
+ * de fim de linha) e resolve o perfil. ENV.md sem a chave (anterior à #199) → default com o ℹ️,
79
+ * como a chave vazia; sem ENV.md (`envText` ausente, primeiro `jarvis init`) → default sem aviso.
80
+ * @param {string | undefined} envText
81
+ * @returns {{ profile: string, warning: string | null, isDefault: boolean }}
82
+ */
83
+ export function readCompanyProfile(envText) {
84
+ if (envText === undefined || envText === null) return resolveCompanyProfile(undefined);
85
+ const match = /^[ \t]*COMPANY_PROFILE=(.*)$/m.exec(envText);
86
+ if (!match) return resolveCompanyProfile("");
87
+ const raw = match[1]
88
+ .replace(/\s+#.*$/, "")
89
+ .trim()
90
+ .replace(/^["']|["']$/g, "");
91
+ return resolveCompanyProfile(raw);
92
+ }
@@ -5,14 +5,16 @@ import {
5
5
  readdirSync,
6
6
  renameSync,
7
7
  readFileSync,
8
+ realpathSync,
8
9
  writeFileSync,
9
10
  rmSync,
10
11
  } from "node:fs";
11
12
  import { execSync } from "node:child_process";
12
- import { join, relative, basename } from "node:path";
13
+ import { join, relative, basename, dirname, sep } from "node:path";
13
14
  import { getFrameworkRoot } from "../utils/paths.js";
14
15
  import { getIDEFolder } from "../config/ide-config.js";
15
16
  import { extractFrontmatterBlock } from "../utils/frontmatter.js";
17
+ import { readCompanyProfile } from "./company-profile.js";
16
18
  import {
17
19
  SYNC_DIRS,
18
20
  SYNC_ROOT_FILES,
@@ -94,6 +96,9 @@ export function syncAssets(targetDir, ide, opts = {}) {
94
96
  }
95
97
  }
96
98
 
99
+ // RuleResolver (#199): rules/ e rules-on-demand/ do overlay do perfil ativo por cima das do core.
100
+ applyRuleOverlay(ideDir, result, { dryRun: opts.dryRun });
101
+
97
102
  // Copy root files to IDE folder
98
103
  for (const file of SYNC_ROOT_FILES) {
99
104
  const src = join(frameworkRoot, file);
@@ -198,6 +203,164 @@ export function syncAssets(targetDir, ide, opts = {}) {
198
203
  return result;
199
204
  }
200
205
 
206
+ /** Pastas do overlay que a IDE carrega sozinha e por isso são resolvidas aqui, na instalação. */
207
+ const OVERLAY_RULE_DIRS = Object.freeze(["rules", "rules-on-demand"]);
208
+
209
+ /**
210
+ * Como `collectMdFiles`, mas inclui links simbólicos para arquivo (o `Dirent` de um link não é
211
+ * `isFile()`), para o `realpath` decidir se o alvo está dentro do overlay. Link para pasta não é seguido.
212
+ */
213
+ function collectOverlayFiles(dir, acc) {
214
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
215
+ const full = join(dir, entry.name);
216
+ if (entry.isDirectory()) collectOverlayFiles(full, acc);
217
+ else if ((entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".md"))
218
+ acc.push(full);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Linha `> Overlay: {perfil} (forked_from …)` logo depois do frontmatter (ou na primeira linha,
224
+ * sem frontmatter): quem abre `.{ide}/rules/` vê que a cópia veio do overlay e onde editar.
225
+ * @param {string} content - Conteúdo do arquivo do overlay (não é alterado lá)
226
+ * @param {string} profile
227
+ * @returns {string}
228
+ */
229
+ export function withOverlayMarker(rawContent, profile) {
230
+ // CRLF (fork editado no Windows) vira LF: a IDE lê igual e o frontmatter é reconhecido.
231
+ const content = rawContent.replace(/\r\n?/g, "\n");
232
+ const block = extractFrontmatterBlock(content);
233
+ const forked = block === null ? null : /^forked_from:\s*(.+)$/m.exec(block);
234
+ const marker = `> Overlay: ${profile} (forked_from ${forked ? forked[1].trim() : "sem carimbo"})`;
235
+ if (block === null) return `${marker}\n\n${content}`;
236
+ const fenceEnd = content.indexOf("\n---", 3);
237
+ const lineEnd = content.indexOf("\n", fenceEnd + 1);
238
+ if (lineEnd === -1) return `${content}\n\n${marker}\n`;
239
+ const head = content.slice(0, lineEnd + 1);
240
+ const tail = content.slice(lineEnd + 1).replace(/^\n+/, "");
241
+ return `${head}\n${marker}\n\n${tail}`;
242
+ }
243
+
244
+ /** A marcação exatamente como `withOverlayMarker` a escreve; captura o perfil. */
245
+ const OVERLAY_MARKER = /^> Overlay: ([a-z0-9][a-z0-9-]*) \(forked_from .*\)$/;
246
+
247
+ /**
248
+ * Lê a marcação de overlay de uma cópia na IDE: só vale na posição em que `withOverlayMarker` a
249
+ * grava (primeira linha útil depois do frontmatter, ou a primeira linha do arquivo) e no formato
250
+ * exato. Uma rule própria que cite a marcação no corpo não é confundida com uma cópia do overlay.
251
+ * @param {string} rawContent
252
+ * @returns {string | null} o perfil da marcação, ou `null`
253
+ */
254
+ export function readOverlayMarker(rawContent) {
255
+ const content = rawContent.replace(/\r\n?/g, "\n");
256
+ let rest = content;
257
+ if (extractFrontmatterBlock(content) !== null) {
258
+ const fenceEnd = content.indexOf("\n---", 3);
259
+ const lineEnd = content.indexOf("\n", fenceEnd + 1);
260
+ rest = lineEnd === -1 ? "" : content.slice(lineEnd + 1);
261
+ }
262
+ const first = rest.split("\n").find((line) => line.trim() !== "");
263
+ const match = first ? OVERLAY_MARKER.exec(first) : null;
264
+ return match ? match[1] : null;
265
+ }
266
+
267
+ /**
268
+ * Cópia na IDE marcada `> Overlay: …` que o overlay **ativo** não tem é de outro perfil (ou saiu do
269
+ * overlay à mão). Sem isto, a IDE continuaria carregando a regra de uma empresa inativa, inclusive
270
+ * quando só este passo roda (`jarvis fork`, reaplicação do upgrade), sem o `cpSync` do core antes.
271
+ * Se o core tem o arquivo, a cópia volta a ser a do core; senão, é removida. Só a IDE é tocada.
272
+ */
273
+ function removeOrphanOverlayRules(ideDir, overlayDir, result, { dryRun, frameworkRoot }) {
274
+ for (const dir of OVERLAY_RULE_DIRS) {
275
+ const ideRules = join(ideDir, dir);
276
+ if (!existsSync(ideRules)) continue;
277
+ const files = [];
278
+ collectMdFiles(ideRules, files);
279
+ for (const file of files) {
280
+ const marker = readOverlayMarker(readFileSync(file, "utf-8"));
281
+ if (marker === null) continue;
282
+ const rel = relative(ideRules, file);
283
+ if (existsSync(join(overlayDir, dir, rel))) continue; // o overlay ativo a recopia em seguida
284
+ const shown = `${dir}/${rel.split(sep).join("/")}`;
285
+ const core = join(frameworkRoot, dir, rel);
286
+ if (existsSync(core)) {
287
+ if (!dryRun) cpSync(core, file);
288
+ result.copied.push(`(overlay) ${shown}: voltou ao core (era do perfil ${marker})`);
289
+ } else {
290
+ if (!dryRun) rmSync(file, { force: true });
291
+ result.copied.push(`(overlay) ${shown}: órfã removida da IDE (era do perfil ${marker})`);
292
+ }
293
+ }
294
+ }
295
+ }
296
+
297
+ /**
298
+ * RuleResolver (#199). Lê `COMPANY_PROFILE` do `.{ide}/ENV.md` (vazio, inválido ou sem ENV.md →
299
+ * `stark-industries`), garante a pasta `overlays/{perfil}/` e copia por cima de `.{ide}/rules/` e
300
+ * `.{ide}/rules-on-demand/` cada `.md` do overlay com o mesmo caminho relativo, marcado.
301
+ * Workflows, skills, agentes e templates não passam por aqui: a rule de precedência os resolve na chamada.
302
+ *
303
+ * Única escrita em `overlays/`: criar a pasta vazia do perfil. Nenhum arquivo é gravado, alterado
304
+ * ou apagado dentro dela. Caminho que resolve (realpath) para fora do overlay é ignorado com aviso.
305
+ * @param {string} ideDir - Pasta da IDE (ex.: `{workspace}/.claude`)
306
+ * @param {{ copied: string[], skipped: string[], errors: string[] }} result - Acumulador do sync
307
+ * @param {{ dryRun?: boolean }} [opts]
308
+ * @returns {string} O perfil ativo
309
+ */
310
+ export function applyRuleOverlay(ideDir, result, { dryRun = false, frameworkRoot } = {}) {
311
+ const envPath = join(ideDir, "ENV.md");
312
+ const envText = existsSync(envPath) ? readFileSync(envPath, "utf-8") : undefined;
313
+ const { profile, warning } = readCompanyProfile(envText);
314
+ if (warning) result.skipped.push(`(overlay) ${warning}`);
315
+
316
+ const overlayDir = join(ideDir, "overlays", profile);
317
+ removeOrphanOverlayRules(ideDir, overlayDir, result, {
318
+ dryRun,
319
+ frameworkRoot: frameworkRoot ?? getFrameworkRoot(),
320
+ });
321
+
322
+ if (!existsSync(overlayDir)) {
323
+ if (!dryRun) mkdirSync(overlayDir, { recursive: true });
324
+ result.copied.push(`Overlay ${profile}: pasta ${dryRun ? "seria criada" : "criada"}`);
325
+ return profile;
326
+ }
327
+
328
+ const overlayRoot = realpathSync(overlayDir);
329
+ for (const dir of OVERLAY_RULE_DIRS) {
330
+ const srcDir = join(overlayDir, dir);
331
+ if (!existsSync(srcDir)) continue;
332
+ const files = [];
333
+ collectOverlayFiles(srcDir, files);
334
+ for (const file of files) {
335
+ const rel = relative(srcDir, file);
336
+ const shown = `${dir}/${rel.split(sep).join("/")}`;
337
+ let real;
338
+ try {
339
+ real = realpathSync(file);
340
+ } catch {
341
+ result.skipped.push(`(overlay) ${shown}: link quebrado, ignorado`);
342
+ continue;
343
+ }
344
+ if (!real.startsWith(overlayRoot + sep)) {
345
+ result.skipped.push(`(overlay) ${shown}: aponta para fora do overlay, ignorado`);
346
+ continue;
347
+ }
348
+ try {
349
+ const content = readFileSync(file, "utf-8");
350
+ const dest = join(ideDir, dir, rel);
351
+ if (!dryRun) {
352
+ mkdirSync(dirname(dest), { recursive: true });
353
+ writeFileSync(dest, withOverlayMarker(content, profile), "utf-8");
354
+ }
355
+ result.copied.push(`(overlay) ${shown}`);
356
+ } catch (err) {
357
+ result.errors.push(`(overlay) ${shown}: ${err.message}`);
358
+ }
359
+ }
360
+ }
361
+ return profile;
362
+ }
363
+
201
364
  /**
202
365
  * Detecta RTK no PATH e reconcilia RTK_ENABLED no ENV.md + rtk-rules.md na IDE.
203
366
  * - RTK encontrado → RTK_ENABLED=true no ENV.md, mantém rule
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jarvis-ai-framework",
3
- "version": "8.2.0",
3
+ "version": "8.3.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 test/session-scan.test.js test/framework-refs.test.js test/validate-frontmatter.test.js test/mcp-config.test.js test/integrations-catalog.test.js test/cli-runner.test.js test/tasks-comment.test.js test/version.test.js test/gate-checklist-rodadas.test.js test/publish-comments-rules.test.js test/pre-pr-criterio.test.js test/nomes-soltos.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 test/framework-refs.test.js test/validate-frontmatter.test.js test/mcp-config.test.js test/integrations-catalog.test.js test/cli-runner.test.js test/tasks-comment.test.js test/version.test.js test/gate-checklist-rodadas.test.js test/publish-comments-rules.test.js test/pre-pr-criterio.test.js test/nomes-soltos.test.js test/company-profile.test.js test/company-overlay-rules.test.js test/fork.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
@@ -14,6 +14,7 @@ A pasta `rules/` contem **regras e diretrizes** que governam o comportamento do
14
14
 
15
15
  ```
16
16
  rules/ # Sempre carregadas no início da sessão
17
+ ├── company-overlay-rules.md # Overlay de empresa (COMPANY_PROFILE): overlay > especialização > core, conferido na chamada
17
18
  ├── engineering/ # Regras de engenharia
18
19
  │ ├── eng-rules.md # Regras gerais de engenharia
19
20
  │ ├── eng.bump-rules.md # Regras para versionamento
@@ -0,0 +1,15 @@
1
+ ---
2
+ env_file: "@/ENV.md"
3
+ ---
4
+
5
+ > **Applies to:** HUB: all | POSITION: all | AREA: all | SQUAD: all
6
+
7
+ # Overlay de empresa — precedência na chamada
8
+
9
+ Perfil ativo: `COMPANY_PROFILE` do `ENV.md` (vazio = `stark-industries`, o overlay padrão). O overlay fica em `$IDE/overlays/{COMPANY_PROFILE}/` e espelha o core: `workflows/`, `skills/`, `agents/`, `rules/`, `rules-on-demand/`, `templates/`.
10
+
11
+ **Antes de seguir um workflow, skill ou agente, ou de ler um template ou uma rule-on-demand por caminho**, confira `$IDE/overlays/{COMPANY_PROFILE}/{tipo}/{mesmo caminho}`. Existe → leia-o e siga-o **em vez** do do core. Não existe → siga o core, sem aviso.
12
+
13
+ Precedência: **overlay > especialização de stack > core.**
14
+
15
+ Não escreva no overlay: só o `/jarvis-fork` (CLI `jarvis fork`) grava lá, com confirmação. `rules/` não passa por aqui: a cópia em `$IDE/rules/` já vem resolvida na instalação (sync e Passo 8 do `/jarvis-init`), marcada com `> Overlay: …` quando é da empresa.
package/skills/AGENTS.md CHANGED
@@ -78,6 +78,8 @@ skills/
78
78
  │ └── assets/
79
79
  ├── jarvis-report-issue/ # Reportar bug no próprio Jarvis (JARVIS_PROJECT)
80
80
  │ └── SKILL.md
81
+ ├── jarvis-fork/ # Fork guiado de um artefato do core para o overlay da empresa (COMPANY_PROFILE; CLI jarvis fork)
82
+ │ └── SKILL.md
81
83
  ├── jarvis-context-detect/ # Detecta contexto do projeto/tarefa
82
84
  │ └── SKILL.md
83
85
  └── jarvis-docs-central/ # Sync/publish docs no central-docs (GitHub/GitLab)
@@ -241,6 +243,7 @@ Skills são invocados de formas diferentes dependendo da IDE:
241
243
  | Arquitetura C4 | `eng-backend-arch-c4` |
242
244
  | Listar especializações de stack (instaladas e registradas) | `jarvis-list-specializations` |
243
245
  | Criar uma especialização de stack (backend ou frontend) a partir do código do projeto | `jarvis-create-specialization` |
246
+ | Ajustar um comando do core a uma empresa sem editar o core (fork para `$IDE/overlays/{COMPANY_PROFILE}/`), listar, atualizar ou remover forks | `jarvis-fork` |
244
247
  | Decisão de arquitetura do Jarvis (agent, skill, workflow ou rule?), simplificação, tendências | `jarvis-evolution-architect` |
245
248
  | Mensageria, filas, RabbitMQ, events, consumers, producers | `eng-backend-rabbitmq` (especialização de backend: registrar em `BACKEND_SPECIALIZATIONS`) |
246
249
  | APIs, auth, workers, filas, caching, banco de dados | `eng-backend` (base) + `BACKEND_SPECIALIZATIONS` |
@@ -141,6 +141,11 @@ Removidas: `checkin`, `checkout`, `daily`, `task-log`, `send-priority`, `sent-pr
141
141
  - **Trigger**: auto-report de bugs no framework Jarvis pela CLI do host (`gh`), depois da pergunta
142
142
  - **Status**: `criado`
143
143
 
144
+ ### `jarvis-fork` ✅
145
+ - **Trigger**: um comando do core não serve a esta empresa; "customizar o Jarvis para este cliente"
146
+ - **Escopo**: conversa até achar a seção que incomoda, menor ajuste, simulação e fork no overlay (`$IDE/overlays/{COMPANY_PROFILE}/`) só com o Sim, pelo CLI `jarvis fork`; `--list`, `--update`, `--delete`; pergunta de alcance (só da empresa ou melhoria para o core via `jarvis-report-issue`)
147
+ - **Status**: `criado` (#199)
148
+
144
149
  ### `jarvis-docs-index` / `jarvis-docs-central` / `jarvis-context-detect` ✅
145
150
  - **Trigger**: indexação de docs, sync com repo central, detecção de contexto
146
151
  - **Status**: `criado`