@guilhermefsousa/open-spec-kit 0.0.2 → 0.0.4
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/package.json +11 -4
- package/src/commands/doctor.js +65 -48
- package/src/commands/init.js +82 -78
- package/src/commands/validate.js +10 -8
- package/src/utils/http.js +54 -0
- package/src/utils/mcp-detect.js +131 -0
- package/templates/agents/scripts/notify-gchat.sh +12 -1
- package/templates/agents/skills/discovery/SKILL.md +3 -1
- package/templates/agents/skills/setup-project/SKILL.md +18 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guilhermefsousa/open-spec-kit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "CLI para spec-driven development com suporte a Claude Code e GitHub Copilot",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,15 +16,18 @@
|
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
18
|
"start": "node bin/open-spec-kit.js",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"test:coverage": "vitest run --coverage",
|
|
19
22
|
"prepublishOnly": "node bin/open-spec-kit.js --version"
|
|
20
23
|
},
|
|
21
24
|
"dependencies": {
|
|
25
|
+
"chalk": "^5.4.0",
|
|
22
26
|
"commander": "^13.0.0",
|
|
23
27
|
"inquirer": "^12.0.0",
|
|
24
|
-
"chalk": "^5.4.0",
|
|
25
28
|
"ora": "^8.0.0",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
29
|
+
"yaml": "^2.7.0",
|
|
30
|
+
"zod": "^3.24.0"
|
|
28
31
|
},
|
|
29
32
|
"engines": {
|
|
30
33
|
"node": ">=18.0.0"
|
|
@@ -47,5 +50,9 @@
|
|
|
47
50
|
"homepage": "https://github.com/guilhermefsousa/open-spec-kit#readme",
|
|
48
51
|
"bugs": {
|
|
49
52
|
"url": "https://github.com/guilhermefsousa/open-spec-kit/issues"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@vitest/coverage-v8": "^4.1.4",
|
|
56
|
+
"vitest": "^4.1.4"
|
|
50
57
|
}
|
|
51
58
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -3,6 +3,8 @@ import { readFile, access } from 'fs/promises';
|
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import yaml from 'yaml';
|
|
6
|
+
import { detectMcpRunner, getInstallInstructions } from '../utils/mcp-detect.js';
|
|
7
|
+
import { confluenceRequest, figmaRequest } from '../utils/http.js';
|
|
6
8
|
|
|
7
9
|
export async function doctorCommand() {
|
|
8
10
|
console.log(chalk.bold('\n open-spec-kit doctor\n'));
|
|
@@ -159,22 +161,29 @@ export async function doctorCommand() {
|
|
|
159
161
|
fail++;
|
|
160
162
|
}
|
|
161
163
|
|
|
162
|
-
// Check 7: MCP CLI available —
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
console.log(chalk.green(
|
|
164
|
+
// Check 7: MCP CLI available — detect best runner (direct, uvx, pipx)
|
|
165
|
+
const mcpRunner = detectMcpRunner();
|
|
166
|
+
if (mcpRunner.method !== 'pip-fallback') {
|
|
167
|
+
const via = mcpRunner.method === 'direct' ? 'PATH' : mcpRunner.method;
|
|
168
|
+
console.log(chalk.green(` ✓ mcp-atlassian disponível via ${via} (${mcpRunner.command}${mcpRunner.prefix.length > 0 ? ' ' + mcpRunner.prefix.join(' ') : ''})`));
|
|
167
169
|
pass++;
|
|
168
|
-
}
|
|
169
|
-
console.log(chalk.yellow(' ⚠ mcp-atlassian
|
|
170
|
-
|
|
170
|
+
} else {
|
|
171
|
+
console.log(chalk.yellow(' ⚠ mcp-atlassian não encontrado (nem direto, nem via uvx/pipx)'));
|
|
172
|
+
for (const line of getInstallInstructions()) {
|
|
173
|
+
console.log(chalk.dim(` ${line}`));
|
|
174
|
+
}
|
|
171
175
|
fail++;
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
// Check 7b: Python version >= 3.10 (required by mcp-atlassian)
|
|
179
|
+
// Try python3 first (Linux/macOS default), fall back to python (Windows default)
|
|
175
180
|
try {
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
let pyVersion;
|
|
182
|
+
try {
|
|
183
|
+
pyVersion = execSync('python3 --version', { stdio: 'pipe', timeout: 5000 }).toString().trim();
|
|
184
|
+
} catch {
|
|
185
|
+
pyVersion = execSync('python --version', { stdio: 'pipe', timeout: 5000 }).toString().trim();
|
|
186
|
+
}
|
|
178
187
|
const match = pyVersion.match(/(\d+)\.(\d+)/);
|
|
179
188
|
if (match) {
|
|
180
189
|
const major = Number(match[1]);
|
|
@@ -229,13 +238,8 @@ export async function doctorCommand() {
|
|
|
229
238
|
if (hasFigma) {
|
|
230
239
|
if (claudeMcpContent) {
|
|
231
240
|
if (claudeMcpContent.includes('"figma"')) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
fail++;
|
|
235
|
-
} else {
|
|
236
|
-
console.log(chalk.green(' ✓ .mcp.json (Claude) tem servidor Figma configurado'));
|
|
237
|
-
pass++;
|
|
238
|
-
}
|
|
241
|
+
console.log(chalk.green(' ✓ .mcp.json (Claude) tem servidor Figma configurado'));
|
|
242
|
+
pass++;
|
|
239
243
|
} else {
|
|
240
244
|
console.log(chalk.yellow(' ⚠ projects.yml tem figma: mas .mcp.json não tem servidor figma'));
|
|
241
245
|
fail++;
|
|
@@ -252,62 +256,75 @@ export async function doctorCommand() {
|
|
|
252
256
|
}
|
|
253
257
|
}
|
|
254
258
|
|
|
255
|
-
//
|
|
259
|
+
// Parse .env once for credential checks (11 + 12)
|
|
260
|
+
let envVars = {};
|
|
256
261
|
try {
|
|
257
262
|
const envContent = await readFile(join(cwd, '.env'), 'utf-8');
|
|
258
|
-
|
|
263
|
+
envVars = Object.fromEntries(
|
|
259
264
|
envContent.split('\n')
|
|
260
265
|
.filter(l => l.includes('=') && !l.startsWith('#'))
|
|
261
266
|
.map(l => { const idx = l.indexOf('='); return [l.slice(0, idx).trim(), l.slice(idx + 1).trim()]; }),
|
|
262
267
|
);
|
|
268
|
+
} catch {
|
|
269
|
+
// .env not found — already warned by MCP config check
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const isPlaceholder = (v) => !v || v.includes('seu-') || v.includes('SEU-') || v === 'seu-api-token-aqui' || v === 'seu-figma-api-key';
|
|
263
273
|
|
|
274
|
+
// Check 11: Confluence credentials — make real HTTP call to validate token
|
|
275
|
+
{
|
|
264
276
|
const confUrl = envVars.CONFLUENCE_URL;
|
|
265
|
-
const confUser = envVars.
|
|
277
|
+
const confUser = envVars.CONFLUENCE_USERNAME;
|
|
266
278
|
const confToken = envVars.CONFLUENCE_API_TOKEN;
|
|
267
279
|
|
|
268
|
-
const isPlaceholder = (v) => !v || v.includes('seu-') || v.includes('SEU-') || v === 'seu-api-token-aqui';
|
|
269
|
-
|
|
270
280
|
if (isPlaceholder(confUrl) || isPlaceholder(confUser) || isPlaceholder(confToken)) {
|
|
271
281
|
console.log(chalk.yellow(' ⚠ .env não configurado — pular validação de credenciais Confluence'));
|
|
272
|
-
console.log(chalk.dim(' Configure CONFLUENCE_URL,
|
|
282
|
+
console.log(chalk.dim(' Configure CONFLUENCE_URL, CONFLUENCE_USERNAME e CONFLUENCE_API_TOKEN no .env'));
|
|
273
283
|
fail++;
|
|
274
284
|
} else {
|
|
275
|
-
const basicAuth = Buffer.from(`${confUser}:${confToken}`).toString('base64');
|
|
276
|
-
const controller = new AbortController();
|
|
277
|
-
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
278
|
-
|
|
279
285
|
try {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (res.ok) {
|
|
287
|
-
console.log(chalk.green(' ✓ Credenciais Confluence válidas (token ativo)'));
|
|
288
|
-
pass++;
|
|
289
|
-
} else if (res.status === 401 || res.status === 403) {
|
|
290
|
-
console.log(chalk.red(' ✗ Token Confluence inválido ou expirado (HTTP ' + res.status + ')'));
|
|
286
|
+
await confluenceRequest(confUrl, '/rest/api/space?limit=1', confUser, confToken, 10000);
|
|
287
|
+
console.log(chalk.green(' ✓ Credenciais Confluence válidas (token ativo)'));
|
|
288
|
+
pass++;
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (err.status === 401 || err.status === 403) {
|
|
291
|
+
console.log(chalk.red(` ✗ Token Confluence inválido ou expirado (HTTP ${err.status})`));
|
|
291
292
|
console.log(chalk.dim(' Renove em: https://id.atlassian.com/manage-profile/security/api-tokens'));
|
|
292
293
|
console.log(chalk.dim(' Atualize CONFLUENCE_API_TOKEN no .env'));
|
|
293
294
|
fail++;
|
|
294
295
|
} else {
|
|
295
|
-
console.log(chalk.yellow(` ⚠
|
|
296
|
+
console.log(chalk.yellow(` ⚠ Não foi possível verificar credenciais Confluence: ${err.message}`));
|
|
296
297
|
fail++;
|
|
297
298
|
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check 12: Figma credentials — make real HTTP call to validate token
|
|
304
|
+
if (hasFigma) {
|
|
305
|
+
const figmaKey = envVars.FIGMA_API_KEY;
|
|
306
|
+
|
|
307
|
+
if (isPlaceholder(figmaKey)) {
|
|
308
|
+
console.log(chalk.yellow(' ⚠ FIGMA_API_KEY não configurado no .env'));
|
|
309
|
+
console.log(chalk.dim(' Gere em: Figma > Account Settings > Personal Access Tokens'));
|
|
310
|
+
fail++;
|
|
311
|
+
} else {
|
|
312
|
+
try {
|
|
313
|
+
const result = await figmaRequest(figmaKey, 10000);
|
|
314
|
+
console.log(chalk.green(` ✓ Token Figma válido (usuário: ${result.data.handle || result.data.email || 'OK'})`));
|
|
315
|
+
pass++;
|
|
316
|
+
} catch (err) {
|
|
317
|
+
if (err.status === 401 || err.status === 403) {
|
|
318
|
+
console.log(chalk.red(` ✗ Token Figma inválido ou expirado (HTTP ${err.status})`));
|
|
319
|
+
console.log(chalk.dim(' Renove em: Figma > Account Settings > Personal Access Tokens'));
|
|
320
|
+
console.log(chalk.dim(' Atualize FIGMA_API_KEY no .env'));
|
|
321
|
+
fail++;
|
|
302
322
|
} else {
|
|
303
|
-
console.log(chalk.yellow(` ⚠ Não foi possível verificar
|
|
323
|
+
console.log(chalk.yellow(` ⚠ Não foi possível verificar token Figma: ${err.message}`));
|
|
324
|
+
fail++;
|
|
304
325
|
}
|
|
305
|
-
// Network errors are WARN not FAIL — offline dev should not be blocked
|
|
306
|
-
fail++;
|
|
307
326
|
}
|
|
308
327
|
}
|
|
309
|
-
} catch {
|
|
310
|
-
// .env not found — already warned by MCP config check
|
|
311
328
|
}
|
|
312
329
|
|
|
313
330
|
// Summary
|
package/src/commands/init.js
CHANGED
|
@@ -5,8 +5,8 @@ import { writeFile, mkdir, access, cp } from 'fs/promises';
|
|
|
5
5
|
import { join, dirname } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
|
-
import
|
|
9
|
-
import
|
|
8
|
+
import { detectMcpRunner, detectNpxRunner, tryInstallMcpAtlassian, getInstallInstructions } from '../utils/mcp-detect.js';
|
|
9
|
+
import { confluenceRequest, figmaRequest } from '../utils/http.js';
|
|
10
10
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
const TEMPLATES_DIR = join(__dirname, '..', '..', 'templates');
|
|
@@ -48,7 +48,7 @@ const KNOWN_TECH_KEYWORDS = [
|
|
|
48
48
|
];
|
|
49
49
|
|
|
50
50
|
// ──────────────────────────────────────────────────────
|
|
51
|
-
//
|
|
51
|
+
// SSL error detection (corporate proxy workaround)
|
|
52
52
|
// ──────────────────────────────────────────────────────
|
|
53
53
|
const SSL_ERROR_CODES = new Set([
|
|
54
54
|
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
|
@@ -67,48 +67,6 @@ function isSslError(err) {
|
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function confluenceRequest(baseUrl, path, user, token, timeoutMs = 15000, allowInsecure = false) {
|
|
71
|
-
return new Promise((resolve, reject) => {
|
|
72
|
-
try {
|
|
73
|
-
const fullUrl = new URL(baseUrl.replace(/\/$/, '') + path);
|
|
74
|
-
const mod = fullUrl.protocol === 'https:' ? https : http;
|
|
75
|
-
const auth = Buffer.from(`${user}:${token}`).toString('base64');
|
|
76
|
-
const options = {
|
|
77
|
-
headers: {
|
|
78
|
-
'Authorization': `Basic ${auth}`,
|
|
79
|
-
'Accept': 'application/json',
|
|
80
|
-
},
|
|
81
|
-
timeout: timeoutMs,
|
|
82
|
-
...(allowInsecure && fullUrl.protocol === 'https:'
|
|
83
|
-
? { agent: new https.Agent({ rejectUnauthorized: false }) }
|
|
84
|
-
: {}),
|
|
85
|
-
};
|
|
86
|
-
const req = mod.get(fullUrl, options, (res) => {
|
|
87
|
-
let data = '';
|
|
88
|
-
res.on('data', (chunk) => { data += chunk; });
|
|
89
|
-
res.on('end', () => {
|
|
90
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
91
|
-
try {
|
|
92
|
-
resolve({ status: res.statusCode, data: JSON.parse(data) });
|
|
93
|
-
} catch {
|
|
94
|
-
resolve({ status: res.statusCode, data });
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
reject({ status: res.statusCode, message: `HTTP ${res.statusCode}` });
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
req.on('error', (err) => reject({ status: 0, message: err.message, code: err.code }));
|
|
102
|
-
req.on('timeout', () => {
|
|
103
|
-
req.destroy();
|
|
104
|
-
reject({ status: 0, message: 'Timeout (15s)' });
|
|
105
|
-
});
|
|
106
|
-
} catch (err) {
|
|
107
|
-
reject({ status: 0, message: err.message, code: err.code });
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
70
|
function stripHtml(html) {
|
|
113
71
|
if (!html) return '';
|
|
114
72
|
return html
|
|
@@ -419,13 +377,13 @@ Thumbs.db
|
|
|
419
377
|
|
|
420
378
|
function generateEnvFile(config) {
|
|
421
379
|
let content = `CONFLUENCE_URL=${config.confluenceUrl}
|
|
422
|
-
|
|
380
|
+
CONFLUENCE_USERNAME=${config.confluenceUser}
|
|
423
381
|
CONFLUENCE_API_TOKEN=${config.confluenceToken}
|
|
424
382
|
GCHAT_WEBHOOK_URL=${config.gchatWebhookUrl || ''}
|
|
425
383
|
`;
|
|
426
384
|
|
|
427
385
|
if (config.hasFigma && config.figmaFileUrl) {
|
|
428
|
-
content +=
|
|
386
|
+
content += `FIGMA_API_KEY=${config.figmaToken}\n`;
|
|
429
387
|
}
|
|
430
388
|
|
|
431
389
|
return content;
|
|
@@ -435,7 +393,7 @@ function generateEnvExample(config) {
|
|
|
435
393
|
let content = `# Confluence credentials
|
|
436
394
|
# Obtenha o token em: https://id.atlassian.com/manage-profile/security/api-tokens
|
|
437
395
|
CONFLUENCE_URL=https://seu-dominio.atlassian.net/wiki
|
|
438
|
-
|
|
396
|
+
CONFLUENCE_USERNAME=seu-email@empresa.com
|
|
439
397
|
CONFLUENCE_API_TOKEN=seu-token-aqui
|
|
440
398
|
|
|
441
399
|
# Google Chat webhook (opcional)
|
|
@@ -454,41 +412,49 @@ FIGMA_API_KEY=seu-figma-api-key
|
|
|
454
412
|
}
|
|
455
413
|
|
|
456
414
|
function generateClaudeMcp(config) {
|
|
415
|
+
const runner = detectMcpRunner();
|
|
416
|
+
const confluenceArgs = [
|
|
417
|
+
...runner.prefix,
|
|
418
|
+
'--confluence-url', '${CONFLUENCE_URL}',
|
|
419
|
+
'--confluence-username', '${CONFLUENCE_USERNAME}',
|
|
420
|
+
'--confluence-token', '${CONFLUENCE_API_TOKEN}'
|
|
421
|
+
];
|
|
457
422
|
const servers = {
|
|
458
423
|
confluence: {
|
|
459
|
-
command:
|
|
460
|
-
args:
|
|
461
|
-
'--confluence-url', '${CONFLUENCE_URL}',
|
|
462
|
-
'--confluence-username', '${CONFLUENCE_USER}',
|
|
463
|
-
'--confluence-token', '${CONFLUENCE_API_TOKEN}'
|
|
464
|
-
]
|
|
424
|
+
command: runner.command,
|
|
425
|
+
args: confluenceArgs,
|
|
465
426
|
}
|
|
466
427
|
};
|
|
467
428
|
if (config.hasFigma && config.figmaFileUrl) {
|
|
429
|
+
const npx = detectNpxRunner();
|
|
468
430
|
servers.figma = {
|
|
469
|
-
command:
|
|
470
|
-
args: [
|
|
431
|
+
command: npx.command,
|
|
432
|
+
args: [...npx.prefix, FIGMA_MCP_PACKAGE, '--figma-api-key', '${FIGMA_API_KEY}']
|
|
471
433
|
};
|
|
472
434
|
}
|
|
473
435
|
return JSON.stringify({ mcpServers: servers }, null, 2) + '\n';
|
|
474
436
|
}
|
|
475
437
|
|
|
476
438
|
function generateCopilotMcp(config) {
|
|
439
|
+
const runner = detectMcpRunner();
|
|
440
|
+
const confluenceArgs = [
|
|
441
|
+
...runner.prefix,
|
|
442
|
+
'--env-file', '.env',
|
|
443
|
+
'--no-confluence-ssl-verify'
|
|
444
|
+
];
|
|
477
445
|
const servers = {
|
|
478
446
|
confluence: {
|
|
479
447
|
type: 'stdio',
|
|
480
|
-
command:
|
|
481
|
-
args:
|
|
482
|
-
'--env-file', '.env',
|
|
483
|
-
'--no-confluence-ssl-verify'
|
|
484
|
-
]
|
|
448
|
+
command: runner.command,
|
|
449
|
+
args: confluenceArgs,
|
|
485
450
|
}
|
|
486
451
|
};
|
|
487
452
|
if (config.hasFigma && config.figmaFileUrl) {
|
|
453
|
+
const npx = detectNpxRunner();
|
|
488
454
|
servers.figma = {
|
|
489
455
|
type: 'stdio',
|
|
490
|
-
command:
|
|
491
|
-
args: [
|
|
456
|
+
command: npx.command,
|
|
457
|
+
args: [...npx.prefix, FIGMA_MCP_PACKAGE, '--figma-api-key', '${input:figma-api-key}']
|
|
492
458
|
};
|
|
493
459
|
}
|
|
494
460
|
return JSON.stringify({ servers }, null, 2) + '\n';
|
|
@@ -771,8 +737,22 @@ export async function initCommand() {
|
|
|
771
737
|
{
|
|
772
738
|
type: 'input',
|
|
773
739
|
name: 'figmaFileUrl',
|
|
774
|
-
message: 'URL do arquivo Figma (
|
|
775
|
-
when: (answers) => answers.hasFigma
|
|
740
|
+
message: 'URL do arquivo Figma (ex: https://www.figma.com/design/ABC123/Nome):',
|
|
741
|
+
when: (answers) => answers.hasFigma,
|
|
742
|
+
validate: v => {
|
|
743
|
+
const trimmed = v.trim();
|
|
744
|
+
if (!trimmed) return 'Obrigatório — se não tem URL, responda "Não" na pergunta anterior';
|
|
745
|
+
if (!trimmed.includes('figma.com')) return 'URL deve ser do Figma (figma.com)';
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
type: 'password',
|
|
751
|
+
name: 'figmaToken',
|
|
752
|
+
message: 'Figma API token (Personal Access Token):',
|
|
753
|
+
mask: '*',
|
|
754
|
+
when: (answers) => answers.hasFigma && answers.figmaFileUrl?.trim(),
|
|
755
|
+
validate: v => v.trim().length > 0 || 'Obrigatório — gere em: Figma > Account Settings > Personal Access Tokens'
|
|
776
756
|
},
|
|
777
757
|
{
|
|
778
758
|
type: 'confirm',
|
|
@@ -789,6 +769,29 @@ export async function initCommand() {
|
|
|
789
769
|
phase3.figmaFileUrl = '';
|
|
790
770
|
}
|
|
791
771
|
|
|
772
|
+
// Validate Figma token
|
|
773
|
+
let figmaTokenValidated = false;
|
|
774
|
+
if (phase3.hasFigma && phase3.figmaToken) {
|
|
775
|
+
const figmaSpinner = ora('Validando token do Figma...').start();
|
|
776
|
+
try {
|
|
777
|
+
const result = await figmaRequest(phase3.figmaToken.trim());
|
|
778
|
+
figmaSpinner.succeed(`Token Figma validado — usuário: ${result.data.handle || result.data.email || 'OK'}`);
|
|
779
|
+
figmaTokenValidated = true;
|
|
780
|
+
} catch (err) {
|
|
781
|
+
if (err.status === 401 || err.status === 403) {
|
|
782
|
+
figmaSpinner.fail(`Token Figma inválido (HTTP ${err.status}). Verifique o Personal Access Token.`);
|
|
783
|
+
console.log(chalk.dim(' Gere em: Figma > Account Settings > Personal Access Tokens'));
|
|
784
|
+
console.log(chalk.yellow(' Continuando sem Figma...\n'));
|
|
785
|
+
phase3.hasFigma = false;
|
|
786
|
+
phase3.figmaFileUrl = '';
|
|
787
|
+
phase3.figmaToken = '';
|
|
788
|
+
} else {
|
|
789
|
+
figmaSpinner.warn(`Não foi possível validar o token Figma: ${err.message}`);
|
|
790
|
+
console.log(chalk.yellow(' Token será salvo, mas verifique com "open-spec-kit doctor".\n'));
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
792
795
|
// B3: se .git já existe, git init é false independente do prompt (que foi pulado)
|
|
793
796
|
const initGitFinal = gitAlreadyExists ? false : (phase3.initGit ?? false);
|
|
794
797
|
|
|
@@ -812,6 +815,7 @@ export async function initCommand() {
|
|
|
812
815
|
gchatWebhookUrl: (phase3.gchatWebhookUrl || '').trim(),
|
|
813
816
|
hasFigma: phase3.hasFigma,
|
|
814
817
|
figmaFileUrl: (phase3.figmaFileUrl || '').trim(),
|
|
818
|
+
figmaToken: (phase3.figmaToken || '').trim(),
|
|
815
819
|
initGit: initGitFinal,
|
|
816
820
|
};
|
|
817
821
|
|
|
@@ -833,7 +837,10 @@ export async function initCommand() {
|
|
|
833
837
|
console.log(` VCS: ${vcsDisplay}`);
|
|
834
838
|
console.log(` Confluence: ${chalk.cyan(config.confluenceRef)} (${config.confluenceLayout === 'space' ? 'space key' : 'page ID'}) @ ${chalk.cyan(config.confluenceUrl)}`);
|
|
835
839
|
console.log(` GChat: ${chalk.cyan(config.gchatWebhookUrl ? '(webhook configurado)' : chalk.dim('Não configurado'))}`);
|
|
836
|
-
|
|
840
|
+
const figmaStatus = config.hasFigma
|
|
841
|
+
? `${config.figmaFileUrl} (${figmaTokenValidated ? 'token validado' : 'token salvo, não validado'})`
|
|
842
|
+
: 'Não';
|
|
843
|
+
console.log(` Figma: ${chalk.cyan(figmaStatus)}`);
|
|
837
844
|
if (gitAlreadyExists) {
|
|
838
845
|
console.log(` Git init: ${chalk.dim('Pulado (.git já existe)')}`);
|
|
839
846
|
} else {
|
|
@@ -913,19 +920,16 @@ export async function initCommand() {
|
|
|
913
920
|
|
|
914
921
|
genSpinner.succeed('Estrutura gerada com sucesso!');
|
|
915
922
|
|
|
916
|
-
// Install mcp-atlassian
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
pipSpinner.succeed('mcp-atlassian já está instalado.');
|
|
923
|
+
// Install mcp-atlassian — cross-platform detection (uvx > pipx > pip)
|
|
924
|
+
const mcpSpinner = ora('Verificando mcp-atlassian...').start();
|
|
925
|
+
const installResult = tryInstallMcpAtlassian();
|
|
926
|
+
if (installResult.installed) {
|
|
927
|
+
mcpSpinner.succeed(`mcp-atlassian disponível via ${installResult.method}`);
|
|
928
|
+
} else {
|
|
929
|
+
mcpSpinner.warn(`${installResult.message}. Instale manualmente:`);
|
|
930
|
+
for (const line of getInstallInstructions()) {
|
|
931
|
+
console.log(chalk.dim(` ${line}`));
|
|
926
932
|
}
|
|
927
|
-
} catch {
|
|
928
|
-
pipSpinner.warn('Não foi possível instalar mcp-atlassian. Instale manualmente: pip install mcp-atlassian');
|
|
929
933
|
}
|
|
930
934
|
|
|
931
935
|
// Git init
|
package/src/commands/validate.js
CHANGED
|
@@ -27,7 +27,7 @@ function parseBrief(content) {
|
|
|
27
27
|
lineCount: content.split('\n').length,
|
|
28
28
|
hasProblema: !!findSection(sections, /problema|contexto|visão\s*geral|overview|background|objetivo\s+do\s+produto|situação\s*atual|descri[çc][aã]o|identifica[çc][aã]o/i),
|
|
29
29
|
hasForaDeEscopo: !!findSection(sections, /fora\s+de\s+escopo/i),
|
|
30
|
-
reqIds:
|
|
30
|
+
reqIds: content.match(/REQ-\w+/g) || [],
|
|
31
31
|
rawContent: content,
|
|
32
32
|
};
|
|
33
33
|
}
|
|
@@ -35,7 +35,8 @@ function parseBrief(content) {
|
|
|
35
35
|
function parseScenarios(content) {
|
|
36
36
|
const sections = parseSections(content);
|
|
37
37
|
const CT_HEADING_RE = /^CT-(\d{3}-\d{2}):\s*(.+?)(?:\s*\(REQ-(\d{3})\))?$/;
|
|
38
|
-
|
|
38
|
+
// Broad regex: capture all CT-like IDs (including malformed) so rule07 can flag them
|
|
39
|
+
const CT_ID_RE = /CT-\d+-\d+/g;
|
|
39
40
|
const REQ_REF_RE = /REQ-\d{3}/g;
|
|
40
41
|
|
|
41
42
|
const allCtIds = [];
|
|
@@ -429,12 +430,7 @@ export async function validateCommand(options = {}) {
|
|
|
429
430
|
const contracts = contractsRaw ? parseContracts(contractsRaw) : null;
|
|
430
431
|
const tasks = tasksRaw ? parseTasks(tasksRaw) : null;
|
|
431
432
|
|
|
432
|
-
//
|
|
433
|
-
if (brief && scenarios) {
|
|
434
|
-
brief.reqIds = [...new Set([...brief.reqIds, ...scenarios.allReqRefs])];
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Rules 2-6: Brief
|
|
433
|
+
// Rules 2-6: Brief (run BEFORE enrich so rule06 sees raw duplicates)
|
|
438
434
|
if (brief) {
|
|
439
435
|
results.push(rules.rule02_briefMaxLines(brief));
|
|
440
436
|
results.push(rules.rule03_briefHasProblema(brief));
|
|
@@ -443,6 +439,12 @@ export async function validateCommand(options = {}) {
|
|
|
443
439
|
results.push(rules.rule06_noDuplicateReqIds(brief));
|
|
444
440
|
}
|
|
445
441
|
|
|
442
|
+
// Enrich brief with REQs from scenarios (union) — covers specs where brief doesn't list REQs
|
|
443
|
+
// Runs AFTER rule05/rule06 so those rules see the original brief REQs (with duplicates)
|
|
444
|
+
if (brief && scenarios) {
|
|
445
|
+
brief.reqIds = [...new Set([...brief.reqIds, ...scenarios.allReqRefs])];
|
|
446
|
+
}
|
|
447
|
+
|
|
446
448
|
// Rule 7: CT IDs
|
|
447
449
|
if (scenarios) {
|
|
448
450
|
results.push(rules.rule07_ctIdFormat(scenarios));
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Simple HTTP GET that returns parsed JSON. Used by init and doctor
|
|
6
|
+
* to validate Confluence and Figma credentials.
|
|
7
|
+
*/
|
|
8
|
+
function httpGetJson(url, headers, { timeoutMs = 15000, allowInsecure = false } = {}) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
try {
|
|
11
|
+
const fullUrl = new URL(url);
|
|
12
|
+
const mod = fullUrl.protocol === 'https:' ? https : http;
|
|
13
|
+
const options = {
|
|
14
|
+
headers: { ...headers, 'Accept': 'application/json' },
|
|
15
|
+
timeout: timeoutMs,
|
|
16
|
+
...(allowInsecure && fullUrl.protocol === 'https:'
|
|
17
|
+
? { agent: new https.Agent({ rejectUnauthorized: false }) }
|
|
18
|
+
: {}),
|
|
19
|
+
};
|
|
20
|
+
const req = mod.get(fullUrl, options, (res) => {
|
|
21
|
+
let data = '';
|
|
22
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
23
|
+
res.on('end', () => {
|
|
24
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
25
|
+
try {
|
|
26
|
+
resolve({ status: res.statusCode, data: JSON.parse(data) });
|
|
27
|
+
} catch {
|
|
28
|
+
resolve({ status: res.statusCode, data });
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
reject({ status: res.statusCode, message: `HTTP ${res.statusCode}` });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
req.on('error', (err) => reject({ status: 0, message: err.message, code: err.code }));
|
|
36
|
+
req.on('timeout', () => {
|
|
37
|
+
req.destroy();
|
|
38
|
+
reject({ status: 0, message: `Timeout (${timeoutMs / 1000}s)` });
|
|
39
|
+
});
|
|
40
|
+
} catch (err) {
|
|
41
|
+
reject({ status: 0, message: err.message, code: err.code });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function confluenceRequest(baseUrl, path, user, token, timeoutMs = 15000, allowInsecure = false) {
|
|
47
|
+
const url = baseUrl.replace(/\/$/, '') + path;
|
|
48
|
+
const auth = Buffer.from(`${user}:${token}`).toString('base64');
|
|
49
|
+
return httpGetJson(url, { 'Authorization': `Basic ${auth}` }, { timeoutMs, allowInsecure });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function figmaRequest(token, timeoutMs = 15000) {
|
|
53
|
+
return httpGetJson('https://api.figma.com/v1/me', { 'X-Figma-Token': token }, { timeoutMs });
|
|
54
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Runner Detection — cross-platform detection of the best way to run MCP servers.
|
|
3
|
+
*
|
|
4
|
+
* Detects whether mcp-atlassian is directly on PATH, or available via uvx/pipx.
|
|
5
|
+
* Used by both init.js (config generation) and doctor.js (health checks).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
|
|
10
|
+
const WHERE_CMD = process.platform === 'win32' ? 'where' : 'which';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if a command exists on PATH.
|
|
14
|
+
* @param {string} cmd
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
function commandExists(cmd) {
|
|
18
|
+
try {
|
|
19
|
+
execSync(`${WHERE_CMD} ${cmd}`, { stdio: 'pipe', timeout: 5000 });
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Detect the best way to run mcp-atlassian.
|
|
28
|
+
* Priority: direct PATH > uvx > pipx > pip-fallback
|
|
29
|
+
*
|
|
30
|
+
* @returns {{ command: string, prefix: string[], method: 'direct'|'uvx'|'pipx'|'pip-fallback' }}
|
|
31
|
+
* - command: the executable to run
|
|
32
|
+
* - prefix: args to prepend before mcp-atlassian's own args
|
|
33
|
+
* - method: how it was detected (for logging)
|
|
34
|
+
*/
|
|
35
|
+
export function detectMcpRunner() {
|
|
36
|
+
if (commandExists('mcp-atlassian')) {
|
|
37
|
+
return { command: 'mcp-atlassian', prefix: [], method: 'direct' };
|
|
38
|
+
}
|
|
39
|
+
if (commandExists('uvx')) {
|
|
40
|
+
return { command: 'uvx', prefix: ['mcp-atlassian'], method: 'uvx' };
|
|
41
|
+
}
|
|
42
|
+
if (commandExists('pipx')) {
|
|
43
|
+
return { command: 'pipx', prefix: ['run', 'mcp-atlassian'], method: 'pipx' };
|
|
44
|
+
}
|
|
45
|
+
return { command: 'mcp-atlassian', prefix: [], method: 'pip-fallback' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Detect the best way to run npx (for Figma MCP).
|
|
50
|
+
* @returns {{ command: string, prefix: string[], method: 'npx'|'npm-exec'|'fallback' }}
|
|
51
|
+
*/
|
|
52
|
+
export function detectNpxRunner() {
|
|
53
|
+
const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
54
|
+
if (commandExists(npxCmd)) {
|
|
55
|
+
return { command: npxCmd === 'npx.cmd' ? 'npx' : npxCmd, prefix: ['-y'], method: 'npx' };
|
|
56
|
+
}
|
|
57
|
+
if (commandExists('npm')) {
|
|
58
|
+
return { command: 'npm', prefix: ['exec', '--yes', '--'], method: 'npm-exec' };
|
|
59
|
+
}
|
|
60
|
+
return { command: 'npx', prefix: ['-y'], method: 'fallback' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Try to install mcp-atlassian using the best available package manager.
|
|
65
|
+
* @returns {{ installed: boolean, method: string, message: string }}
|
|
66
|
+
*/
|
|
67
|
+
export function tryInstallMcpAtlassian() {
|
|
68
|
+
// Already available — no install needed
|
|
69
|
+
const runner = detectMcpRunner();
|
|
70
|
+
if (runner.method !== 'pip-fallback') {
|
|
71
|
+
return { installed: true, method: runner.method, message: `mcp-atlassian acessível via ${runner.method}` };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Try uv tool install
|
|
75
|
+
if (commandExists('uv')) {
|
|
76
|
+
try {
|
|
77
|
+
execSync('uv tool install mcp-atlassian', { stdio: 'pipe', timeout: 120000 });
|
|
78
|
+
return { installed: true, method: 'uv-tool', message: 'Instalado via uv tool install' };
|
|
79
|
+
} catch { /* failed */ }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Try pipx install
|
|
83
|
+
if (commandExists('pipx')) {
|
|
84
|
+
try {
|
|
85
|
+
execSync('pipx install mcp-atlassian', { stdio: 'pipe', timeout: 120000 });
|
|
86
|
+
return { installed: true, method: 'pipx', message: 'Instalado via pipx install' };
|
|
87
|
+
} catch { /* failed */ }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Try pip install (legacy fallback)
|
|
91
|
+
const pipCmd = process.platform === 'win32' ? 'pip' : 'pip3';
|
|
92
|
+
if (commandExists(pipCmd)) {
|
|
93
|
+
try {
|
|
94
|
+
execSync(`${pipCmd} install mcp-atlassian`, { stdio: 'pipe', timeout: 120000 });
|
|
95
|
+
// Verify it landed on PATH
|
|
96
|
+
if (commandExists('mcp-atlassian')) {
|
|
97
|
+
return { installed: true, method: 'pip', message: `Instalado via ${pipCmd}` };
|
|
98
|
+
}
|
|
99
|
+
return { installed: false, method: 'pip-no-path', message: `${pipCmd} install executado mas mcp-atlassian não está no PATH` };
|
|
100
|
+
} catch { /* failed */ }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { installed: false, method: 'none', message: 'Nenhum gerenciador de pacotes Python encontrado' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get platform-specific install instructions for mcp-atlassian.
|
|
108
|
+
* @returns {string[]} Array of instruction lines
|
|
109
|
+
*/
|
|
110
|
+
export function getInstallInstructions() {
|
|
111
|
+
const platform = process.platform;
|
|
112
|
+
if (platform === 'win32') {
|
|
113
|
+
return [
|
|
114
|
+
'pip install mcp-atlassian',
|
|
115
|
+
'OU: instale uv (https://docs.astral.sh/uv/) e rode: uv tool install mcp-atlassian',
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
if (platform === 'darwin') {
|
|
119
|
+
return [
|
|
120
|
+
'brew install uv && uv tool install mcp-atlassian',
|
|
121
|
+
'OU: pip3 install mcp-atlassian',
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
// linux
|
|
125
|
+
return [
|
|
126
|
+
'curl -LsSf https://astral.sh/uv/install.sh | sh && uv tool install mcp-atlassian',
|
|
127
|
+
'OU: pip3 install mcp-atlassian',
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { commandExists };
|
|
@@ -54,7 +54,18 @@ else
|
|
|
54
54
|
fi
|
|
55
55
|
|
|
56
56
|
# Build and send via Python — avoids bash UTF-8 corruption
|
|
57
|
-
|
|
57
|
+
# Use python3 if available (Linux/macOS default), fall back to python (Windows)
|
|
58
|
+
PYTHON_CMD="python3"
|
|
59
|
+
if ! command -v python3 &>/dev/null; then
|
|
60
|
+
PYTHON_CMD="python"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
if ! command -v "$PYTHON_CMD" &>/dev/null; then
|
|
64
|
+
echo "[notify-gchat] Python não encontrado (nem python3, nem python) — notificação ignorada" >&2
|
|
65
|
+
exit 0
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
PYTHONIOENCODING=utf-8 $PYTHON_CMD -c "
|
|
58
69
|
import json, sys, subprocess
|
|
59
70
|
|
|
60
71
|
card_mode = sys.argv[1] == 'true'
|
|
@@ -101,7 +101,9 @@ This ensures the PRD is a **complete description of the new feature** with expli
|
|
|
101
101
|
1. `projects.yml` has a `figma:` section with `file_url` filled
|
|
102
102
|
2. The Figma MCP is available (`figma` server configured)
|
|
103
103
|
|
|
104
|
-
If
|
|
104
|
+
If `projects.yml` has `figma.file_url` but the MCP is not available, **warn the user**: "⚠ Figma configurado em projects.yml mas MCP Figma não disponível. Execute 'open-spec-kit doctor' para diagnosticar. Continuando sem contexto do Figma."
|
|
105
|
+
|
|
106
|
+
If `projects.yml` does NOT have a `figma:` section, skip silently (Figma is optional).
|
|
105
107
|
|
|
106
108
|
**If available:**
|
|
107
109
|
1. Extract the file key from the URL in `projects.yml` → `figma.file_url` (segment after `/design/` or `/file/`)
|
|
@@ -106,7 +106,9 @@ The setup supports two Confluence layouts:
|
|
|
106
106
|
1. `projects.yml` has a `figma:` section with `file_url` filled
|
|
107
107
|
2. The Figma MCP is available (`figma` server configured in `.mcp.json`)
|
|
108
108
|
|
|
109
|
-
If
|
|
109
|
+
If `projects.yml` has `figma.file_url` but the MCP is not available, **warn the user**: "⚠ Figma configurado em projects.yml mas MCP Figma não disponível. Execute 'open-spec-kit doctor' para diagnosticar. Continuando sem contexto do Figma."
|
|
110
|
+
|
|
111
|
+
If `projects.yml` does NOT have a `figma:` section, skip silently (Figma is optional).
|
|
110
112
|
|
|
111
113
|
**If available:**
|
|
112
114
|
1. Extract the file key from the URL in `projects.yml` → `figma.file_url`
|
|
@@ -129,9 +131,11 @@ When generating the living docs (step 3), reference Figma screens where relevant
|
|
|
129
131
|
|
|
130
132
|
### 3. Generate living docs on Confluence
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
**CRITICAL: All pages in this section MUST be created as children of `ROOT_ID` (the project root page), NOT as children of `DEMANDAS_ID`. `Demandas/` is a SIBLING — the generated pages go NEXT TO it, not INSIDE it. Use `ROOT_ID` as the parent_id for every `confluence_create_page` call in this step.**
|
|
135
|
+
|
|
136
|
+
Via MCP Confluence, create these pages **under the project root (`ROOT_ID`)**, in this order.
|
|
133
137
|
|
|
134
|
-
**Title prefix (multi-project)**: when the layout is multi-project (argument is a numeric page ID), ALL created page titles must be prefixed with the project acronym (`sigla` field in projects.yml). This avoids title conflicts within the same Confluence space. The project root page does NOT get a prefix (it's already unique by definition).
|
|
138
|
+
**Title prefix (multi-project)**: when the layout is multi-project (argument is a numeric page ID), ALL created page titles must be prefixed with the project acronym (`sigla` field in projects.yml). **Read the sigla from `projects.yml` field `project.sigla` — NEVER derive or invent a new one.** This avoids title conflicts within the same Confluence space. The project root page does NOT get a prefix (it's already unique by definition).
|
|
135
139
|
|
|
136
140
|
Title format:
|
|
137
141
|
- Domain pages: `{SIGLA} — Visão do Produto`, `{SIGLA} — Glossário`, etc.
|
|
@@ -147,8 +151,8 @@ In simple layout (1 space = 1 project), the prefix is optional — no conflict r
|
|
|
147
151
|
- `🔀 {SIGLA} — Fluxos`
|
|
148
152
|
- `📊 {SIGLA} — Tabelas de Referência`
|
|
149
153
|
- `🔌 {SIGLA} — Integrações`
|
|
150
|
-
- `🚀 Features`
|
|
151
|
-
- `📦 Arquivados`
|
|
154
|
+
- `🚀 {SIGLA} — Features`
|
|
155
|
+
- `📦 {SIGLA} — Arquivados`
|
|
152
156
|
- Business features: `🚀 {SIGLA}-NNN - Feature name`
|
|
153
157
|
- Infrastructure feature: `⚙️ {SIGLA}-000 - Infraestrutura e Scaffold`
|
|
154
158
|
|
|
@@ -246,8 +250,8 @@ Minimum expected terms for any project:
|
|
|
246
250
|
- External integrations: list or write "Nenhuma na v1"
|
|
247
251
|
|
|
248
252
|
**Step 3.4 — Structure pages:**
|
|
249
|
-
- Create `
|
|
250
|
-
- Create `
|
|
253
|
+
- Create `🚀 {SIGLA} — Features` parent page under the project root if it doesn't exist
|
|
254
|
+
- Create `📦 {SIGLA} — Arquivados` parent page under the project root if it doesn't exist
|
|
251
255
|
|
|
252
256
|
**Step 3.5 — Create initial feature pages:**
|
|
253
257
|
|
|
@@ -290,9 +294,11 @@ All subsequent features ({SIGLA}-001 onwards) have an implicit dependency on {SI
|
|
|
290
294
|
|
|
291
295
|
**Step 3.5b — Business features:**
|
|
292
296
|
|
|
297
|
+
**Reminder: feature pages are children of `{SIGLA} — Features` (which is a child of `ROOT_ID`). Do NOT create features inside `Demandas/`.**
|
|
298
|
+
|
|
293
299
|
Analyze the PO documents in `Demandas/` and identify distinct features/capabilities described. For EACH feature identified:
|
|
294
300
|
|
|
295
|
-
1. Create a child page under `Features
|
|
301
|
+
1. Create a child page under `{SIGLA} — Features` with title: `🚀 {SIGLA}-NNN - Feature name` (numbering sequential: 001, 002, ...). E.g., `🚀 FT-001 - Cadastro e Autenticação`, `🚀 TD-003 - Gerenciamento de Tarefas`
|
|
296
302
|
2. Content MUST start with a metadata table:
|
|
297
303
|
```
|
|
298
304
|
| Campo | Valor |
|
|
@@ -315,7 +321,7 @@ In the local spec repo:
|
|
|
315
321
|
|
|
316
322
|
**Fill `projects.yml`:**
|
|
317
323
|
- Project name, domain, status, team size
|
|
318
|
-
- **Sigla**: `sigla: XX` — short abbreviation (2-4 letters) used as prefix in Confluence titles and feature IDs.
|
|
324
|
+
- **Sigla**: `sigla: XX` — short abbreviation (2-4 letters) used as prefix in Confluence titles and feature IDs. **MUST read from `projects.yml` field `project.sigla`** — this was already set by the user during `open-spec-kit init`. NEVER derive or invent a new sigla. If `projects.yml` has no sigla, ask the dev.
|
|
319
325
|
- Stack (from PO's docs or ask the dev)
|
|
320
326
|
- **VCS**: `vcs: github | gitlab` and `vcs_org: {org/group}` — ask the dev if not obvious from context
|
|
321
327
|
- **Repos**: detect from the PRD what repos will be needed. If the PRD mentions "API .NET", declare a `project-api` repo with stack `dotnet` and status `planned`. If it mentions "React frontend", declare a `project-front` repo with stack `nodejs` and status `planned`. Do NOT leave repos commented out — declare them with `status: planned`.
|
|
@@ -443,9 +449,9 @@ The output is considered good when:
|
|
|
443
449
|
- [ ] Fluxos: all flows have happy path + exceptions + Mermaid flowchart TD
|
|
444
450
|
- [ ] Tabelas de Referencia: all enums defined, state transitions documented + Mermaid stateDiagram-v2
|
|
445
451
|
- [ ] Integracoes: all events have producer + consumer + expected payload
|
|
446
|
-
- [ ] Features
|
|
447
|
-
- [ ] Initial feature pages created under Features
|
|
448
|
-
- [ ] Arquivados
|
|
452
|
+
- [ ] {SIGLA} — Features parent page created
|
|
453
|
+
- [ ] Initial feature pages created under {SIGLA} — Features (with label `em-discovery`)
|
|
454
|
+
- [ ] {SIGLA} — Arquivados parent page created
|
|
449
455
|
|
|
450
456
|
### Spec repo updated
|
|
451
457
|
- [ ] projects.yml filled with all detected repos (status: planned, not commented)
|