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.
- package/AGENTS.md +1 -0
- package/README.md +14 -1
- package/agents/product/prod.discovery-interviewer.md +18 -83
- package/bin/commands/init.js +149 -2
- package/bin/commands/tokens.js +9 -3
- package/bin/lib/cli/program.js +14 -2
- package/bin/lib/core/docs-migrate.js +259 -0
- package/bin/lib/core/env-upgrade.js +84 -0
- package/bin/lib/core/profile-filter.js +15 -3
- package/bin/lib/core/sync-engine.js +5 -3
- package/bin/lib/core/token-report.js +1 -0
- package/bin/lib/docs/storage/build-index.js +2 -1
- package/bin/lib/vcs/api.js +13 -1
- package/bin/lib/vcs/create-issue.js +17 -3
- package/package.json +2 -2
- package/rules/AGENTS.md +7 -3
- package/rules/artifact-style-rules.md +53 -0
- package/rules/product/prod-rules.md +4 -2
- package/rules/product/prod.spec-rules.md +4 -4
- package/rules-on-demand/engineering/eng.plan-rules.md +1 -1
- package/rules-on-demand/engineering/eng.pr-rules.md +4 -3
- package/rules-on-demand/engineering/eng.start-rules.md +2 -2
- package/rules-on-demand/engineering/eng.transitions-rules.md +58 -0
- package/rules-on-demand/engineering/eng.work-rules.md +9 -3
- package/rules-on-demand/warm-up-consume-rules.md +30 -0
- package/skills/AGENTS.md +1 -1
- package/skills/eng-global-pr/SKILL.md +3 -1
- package/skills/eng-global-task-comment/SKILL.md +2 -0
- package/skills/jarvis-docs-central/SKILL.md +1 -1
- package/skills/jarvis-init/SKILL.md +60 -17
- package/skills/jarvis-report-issue/SKILL.md +64 -9
- package/templates/AGENTS.md +2 -0
- package/templates/ENV-template.md +11 -1
- package/templates/cdd-mind-map-template.md +26 -0
- package/templates/engineering/architecture-template.md +2 -1
- package/templates/engineering/plan-template.md +2 -0
- package/templates/product/prod-discovery-template.md +39 -71
- package/templates/product/prod-epic-template.md +1 -1
- package/templates/product/prod-frd-template.md +1 -1
- package/templates/product/prod-issue-template.md +1 -1
- package/templates/product/prod-prd-template.md +1 -1
- package/templates/warm-up-template.md +45 -0
- package/workflows/engineering/eng.plan.md +2 -1
- package/workflows/engineering/eng.pr.md +39 -6
- package/workflows/engineering/eng.pre-pr.md +2 -1
- package/workflows/engineering/eng.spec-consolidate.md +3 -1
- package/workflows/engineering/eng.start.md +12 -2
- package/workflows/engineering/eng.work.md +22 -5
- package/workflows/product/prod.spec.ard.md +2 -0
- package/workflows/product/prod.spec.breakdown.md +2 -0
- package/workflows/product/prod.spec.clarify.md +2 -0
- package/workflows/product/prod.spec.discovery.md +56 -116
- package/workflows/product/prod.spec.epic.md +2 -0
- package/workflows/product/prod.spec.frd.md +2 -0
- package/workflows/product/prod.spec.issue.md +2 -0
- package/workflows/product/prod.spec.md +2 -0
- package/workflows/product/prod.spec.prd.md +2 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/bin/lib/vcs/api.js
CHANGED
|
@@ -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: {
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
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
|
|
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): `$
|
|
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
|
|