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.
- package/AGENTS.md +2 -0
- package/README.md +11 -0
- package/bin/README.md +3 -3
- package/bin/commands/fork.js +562 -0
- package/bin/commands/init.js +62 -1
- package/bin/lib/cli/program.js +27 -0
- package/bin/lib/core/company-profile.js +92 -0
- package/bin/lib/core/sync-engine.js +164 -1
- package/package.json +2 -2
- package/rules/AGENTS.md +1 -0
- package/rules/company-overlay-rules.md +15 -0
- package/skills/AGENTS.md +3 -0
- package/skills/SKILLS-ROADMAP.md +5 -0
- package/skills/jarvis-fork/SKILL.md +184 -0
- package/skills/jarvis-init/SKILL.md +11 -2
- package/templates/ENV-template.md +11 -0
package/bin/commands/init.js
CHANGED
|
@@ -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
|
}
|
package/bin/lib/cli/program.js
CHANGED
|
@@ -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.
|
|
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` |
|
package/skills/SKILLS-ROADMAP.md
CHANGED
|
@@ -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`
|