@spec-wave/cli 0.5.10 → 0.6.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/package.json +1 -1
- package/src/commands/code-review.mjs +111 -45
- package/src/commands/implement.mjs +40 -26
- package/src/config.mjs +1 -0
- package/src/templates/skill/SKILL.md +3 -3
package/package.json
CHANGED
|
@@ -4,11 +4,14 @@ import { resolveToken } from '../api/auth.mjs';
|
|
|
4
4
|
import { getIssue, getPR, commentOnIssue } from '../api/github-rest.mjs';
|
|
5
5
|
import { addProjectItem, setItemSingleSelect, getSingleSelectField, getIssueParent, listSubIssues, getItemSingleSelectValue } from '../api/github-graphql.mjs';
|
|
6
6
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
7
|
-
import { CONFIG_FILE, STATUS_OPTIONS, STAGE_ORDER, PROGRESS_TODO } from '../config.mjs';
|
|
7
|
+
import { CONFIG_FILE, STATUS_OPTIONS, STAGE_ORDER, STAGE_DONE, PROGRESS_TODO, PROGRESS_DONE } from '../config.mjs';
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// Ao abrir o PR: Stories/Feature → Etapa "👀 Code Review" (Status "Todo");
|
|
10
|
+
// Tasks → Etapa "🎉 Done" (Status "Done"), pois a implementação da task terminou.
|
|
10
11
|
const CODE_REVIEW_STAGE = STATUS_OPTIONS.find(s => s.name.includes('Code Review'))?.name;
|
|
12
|
+
const DONE_STAGE = STAGE_DONE;
|
|
11
13
|
const TODO_STATUS = PROGRESS_TODO;
|
|
14
|
+
const DONE_STATUS = PROGRESS_DONE;
|
|
12
15
|
|
|
13
16
|
// Extrai números de issues referenciadas no corpo do PR (Closes #N, Fixes #N, #N solto).
|
|
14
17
|
function extractIssueNumbers(body) {
|
|
@@ -54,66 +57,86 @@ async function resolveFeatureIssue(token, owner, repo, issueNumber) {
|
|
|
54
57
|
return null;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
// Coleta a "unidade de review" de uma issue referenciada no PR
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
+
// Coleta a "unidade de review" de uma issue referenciada no PR, separando por
|
|
61
|
+
// tipo porque cada um tem destino próprio: Tasks → Done, Stories → Code Review,
|
|
62
|
+
// Feature → Code Review (só quando todas as Stories estiverem prontas). Retorna
|
|
63
|
+
// { feature:{number,nodeId,title}|null, stories:Map, tasks:Map }.
|
|
60
64
|
async function collectReviewUnit(token, owner, repo, issueNumber) {
|
|
61
|
-
const
|
|
62
|
-
const
|
|
65
|
+
const stories = new Map();
|
|
66
|
+
const tasks = new Map();
|
|
67
|
+
const addStory = (n, nodeId, title) => { if (n && nodeId && !stories.has(n)) stories.set(n, { nodeId, title }); };
|
|
68
|
+
const addTask = (n, nodeId, title) => { if (n && nodeId && !tasks.has(n)) tasks.set(n, { nodeId, title }); };
|
|
63
69
|
|
|
64
70
|
let issue;
|
|
65
|
-
try { issue = await getIssue(token, owner, repo, issueNumber); } catch { return
|
|
71
|
+
try { issue = await getIssue(token, owner, repo, issueNumber); } catch { return { feature: null, stories, tasks }; }
|
|
66
72
|
const type = detectIssueType(issue);
|
|
67
|
-
if (type !== 'Feature' && type !== 'Story' && type !== 'Task') return
|
|
73
|
+
if (type !== 'Feature' && type !== 'Story' && type !== 'Task') return { feature: null, stories, tasks };
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
const feature =
|
|
71
|
-
|
|
75
|
+
const featureIssue = await resolveFeatureIssue(token, owner, repo, issueNumber);
|
|
76
|
+
const feature = featureIssue
|
|
77
|
+
? { number: featureIssue.number, nodeId: featureIssue.node_id, title: featureIssue.title }
|
|
78
|
+
: null;
|
|
72
79
|
|
|
73
80
|
if (type === 'Feature') {
|
|
74
|
-
//
|
|
75
|
-
const
|
|
76
|
-
for (const st of
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
for (const t of
|
|
81
|
+
// Referência direta à Feature: toda a subárvore (Stories + Tasks).
|
|
82
|
+
const subs = await listSubIssues(token, issue.node_id).catch(() => []);
|
|
83
|
+
for (const st of subs) {
|
|
84
|
+
addStory(st.number, st.nodeId, st.title);
|
|
85
|
+
const tks = await listSubIssues(token, st.nodeId).catch(() => []);
|
|
86
|
+
for (const t of tks) addTask(t.number, t.nodeId, t.title);
|
|
80
87
|
}
|
|
81
88
|
} else if (type === 'Story') {
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
for (const t of
|
|
89
|
+
addStory(issue.number, issue.node_id, issue.title);
|
|
90
|
+
const tks = await listSubIssues(token, issue.node_id).catch(() => []);
|
|
91
|
+
for (const t of tks) addTask(t.number, t.nodeId, t.title);
|
|
85
92
|
} else { // Task → inclui a Story pai e as Tasks irmãs.
|
|
86
|
-
|
|
93
|
+
addTask(issue.number, issue.node_id, issue.title);
|
|
87
94
|
const parent = await getIssueParent(token, issue.node_id).catch(() => null);
|
|
88
95
|
if (parent && detectIssueType({ title: parent.title }) === 'Story') {
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
for (const t of
|
|
96
|
+
addStory(parent.number, parent.nodeId, parent.title);
|
|
97
|
+
const tks = await listSubIssues(token, parent.nodeId).catch(() => []);
|
|
98
|
+
for (const t of tks) addTask(t.number, t.nodeId, t.title);
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
|
-
return
|
|
101
|
+
return { feature, stories, tasks };
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
//
|
|
98
|
-
// (
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
// A Feature só avança quando TODAS as suas Stories já estiverem em Code Review
|
|
105
|
+
// (ou etapa posterior). readToken lê as issues; projToken opera no Project.
|
|
106
|
+
async function allStoriesReadyForReview(readToken, projToken, project, etapaField, featureNodeId) {
|
|
107
|
+
if (!etapaField?.id) return true; // sem campo Etapa não há como checar — assume ok
|
|
108
|
+
const subs = await listSubIssues(readToken, featureNodeId).catch(() => []);
|
|
109
|
+
const stories = subs.filter(s => detectIssueType({ title: s.title }) === 'Story');
|
|
110
|
+
if (stories.length === 0) return true;
|
|
111
|
+
const tgtIdx = STAGE_ORDER.indexOf(CODE_REVIEW_STAGE);
|
|
112
|
+
for (const st of stories) {
|
|
113
|
+
const itemId = await addProjectItem(projToken, project.id, st.nodeId);
|
|
114
|
+
const current = await getItemSingleSelectValue(projToken, itemId, etapaField.id).catch(() => null);
|
|
115
|
+
const idx = current ? STAGE_ORDER.indexOf(current) : -1;
|
|
116
|
+
if (idx === -1 || idx < tgtIdx) return false; // há Story pendente
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Avança um item do board para `targetStage` (Etapa) e define o Status para
|
|
122
|
+
// `targetStatus`. Uma issue só AVANÇA: se já estiver em `targetStage` ou em uma
|
|
123
|
+
// etapa posterior, não é tocada (retorna false). Retorna true se avançou.
|
|
124
|
+
async function advanceToStage(token, project, etapaField, statusField, nodeId, targetStage, targetStatus) {
|
|
101
125
|
const itemId = await addProjectItem(token, project.id, nodeId);
|
|
102
126
|
|
|
103
|
-
if (etapaField?.id &&
|
|
127
|
+
if (etapaField?.id && targetStage) {
|
|
104
128
|
// Nunca retroceder: compara a etapa atual com a de destino na ordem canônica.
|
|
105
129
|
const current = await getItemSingleSelectValue(token, itemId, etapaField.id).catch(() => null);
|
|
106
130
|
const curIdx = current ? STAGE_ORDER.indexOf(current) : -1;
|
|
107
|
-
const tgtIdx = STAGE_ORDER.indexOf(
|
|
131
|
+
const tgtIdx = STAGE_ORDER.indexOf(targetStage);
|
|
108
132
|
if (curIdx !== -1 && tgtIdx !== -1 && curIdx >= tgtIdx) {
|
|
109
|
-
return false; // já está
|
|
133
|
+
return false; // já está nessa etapa ou adiante — não retrocede
|
|
110
134
|
}
|
|
111
|
-
const optionId = etapaField.options?.[
|
|
135
|
+
const optionId = etapaField.options?.[targetStage];
|
|
112
136
|
if (optionId) await setItemSingleSelect(token, project.id, itemId, etapaField.id, optionId);
|
|
113
137
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const optionId = statusField.options?.[TODO_STATUS];
|
|
138
|
+
if (statusField?.id && targetStatus) {
|
|
139
|
+
const optionId = statusField.options?.[targetStatus];
|
|
117
140
|
if (optionId) await setItemSingleSelect(token, project.id, itemId, statusField.id, optionId);
|
|
118
141
|
}
|
|
119
142
|
return true;
|
|
@@ -168,18 +191,39 @@ export async function codeReview({ prNumber }) {
|
|
|
168
191
|
|
|
169
192
|
const seen = new Set();
|
|
170
193
|
const updated = [];
|
|
194
|
+
const featuresChecked = new Set();
|
|
171
195
|
|
|
172
|
-
//
|
|
173
|
-
//
|
|
196
|
+
// Regras ao abrir o PR: Tasks → 🎉 Done (implementação concluída);
|
|
197
|
+
// Stories → 👀 Code Review; Feature → Code Review só quando TODAS as suas
|
|
198
|
+
// Stories já estiverem em Code Review.
|
|
174
199
|
for (const num of issueNums) {
|
|
175
|
-
const
|
|
176
|
-
|
|
200
|
+
const { feature, stories, tasks } = await collectReviewUnit(token, owner, repo, num);
|
|
201
|
+
|
|
202
|
+
// Tasks → Done (Status Done).
|
|
203
|
+
for (const [n, info] of tasks) {
|
|
204
|
+
if (seen.has(n)) continue;
|
|
205
|
+
seen.add(n);
|
|
206
|
+
try {
|
|
207
|
+
const moved = await advanceToStage(projectToken, project, etapaField, statusField, info.nodeId, DONE_STAGE, DONE_STATUS);
|
|
208
|
+
if (moved) {
|
|
209
|
+
updated.push(`#${n} ${info.title} → ${DONE_STAGE}`);
|
|
210
|
+
console.log(`#${n} → "${DONE_STAGE}" / Status "${DONE_STATUS}".`);
|
|
211
|
+
} else {
|
|
212
|
+
console.log(`#${n} já está em "${DONE_STAGE}" ou etapa posterior — mantido (não retrocede).`);
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.warn(`Falha ao atualizar #${n}: ${err.message}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Stories → Code Review (Status Todo).
|
|
220
|
+
for (const [n, info] of stories) {
|
|
177
221
|
if (seen.has(n)) continue;
|
|
178
222
|
seen.add(n);
|
|
179
223
|
try {
|
|
180
|
-
const moved = await
|
|
224
|
+
const moved = await advanceToStage(projectToken, project, etapaField, statusField, info.nodeId, CODE_REVIEW_STAGE, TODO_STATUS);
|
|
181
225
|
if (moved) {
|
|
182
|
-
updated.push(`#${n} ${info.title}`);
|
|
226
|
+
updated.push(`#${n} ${info.title} → ${CODE_REVIEW_STAGE}`);
|
|
183
227
|
console.log(`#${n} → "${CODE_REVIEW_STAGE}" / Status "${TODO_STATUS}".`);
|
|
184
228
|
} else {
|
|
185
229
|
console.log(`#${n} já está em "${CODE_REVIEW_STAGE}" ou etapa posterior — mantido (não retrocede).`);
|
|
@@ -188,16 +232,38 @@ export async function codeReview({ prNumber }) {
|
|
|
188
232
|
console.warn(`Falha ao atualizar #${n}: ${err.message}`);
|
|
189
233
|
}
|
|
190
234
|
}
|
|
235
|
+
|
|
236
|
+
// Feature: só avança se todas as suas Stories já estão em Code Review+.
|
|
237
|
+
if (feature && !featuresChecked.has(feature.number)) {
|
|
238
|
+
featuresChecked.add(feature.number);
|
|
239
|
+
try {
|
|
240
|
+
const ready = await allStoriesReadyForReview(token, projectToken, project, etapaField, feature.nodeId);
|
|
241
|
+
if (!ready) {
|
|
242
|
+
console.log(`Feature #${feature.number} mantida em desenvolvimento — ainda há Stories pendentes (fora de "${CODE_REVIEW_STAGE}").`);
|
|
243
|
+
} else if (!seen.has(feature.number)) {
|
|
244
|
+
seen.add(feature.number);
|
|
245
|
+
const moved = await advanceToStage(projectToken, project, etapaField, statusField, feature.nodeId, CODE_REVIEW_STAGE, TODO_STATUS);
|
|
246
|
+
if (moved) {
|
|
247
|
+
updated.push(`#${feature.number} ${feature.title} (Feature) → ${CODE_REVIEW_STAGE}`);
|
|
248
|
+
console.log(`Feature #${feature.number} → "${CODE_REVIEW_STAGE}" (todas as Stories concluídas).`);
|
|
249
|
+
} else {
|
|
250
|
+
console.log(`Feature #${feature.number} já está em "${CODE_REVIEW_STAGE}" ou etapa posterior.`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.warn(`Falha ao avaliar a Feature #${feature.number}: ${err.message}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
191
257
|
}
|
|
192
258
|
|
|
193
259
|
if (updated.length > 0) {
|
|
194
260
|
await commentOnIssue(
|
|
195
261
|
token, owner, repo, parseInt(prNumber, 10),
|
|
196
262
|
`🔍 **Code Review iniciado**\n\n` +
|
|
197
|
-
`
|
|
263
|
+
`Board atualizado (Tasks → **${DONE_STAGE}**; Story → **${CODE_REVIEW_STAGE}**; Feature só avança quando todas as Stories concluírem):\n\n` +
|
|
198
264
|
updated.map(f => `- ${f}`).join('\n')
|
|
199
265
|
).catch(() => {});
|
|
200
266
|
}
|
|
201
267
|
|
|
202
|
-
console.log(`code-review: ${updated.length} item(ns)
|
|
268
|
+
console.log(`code-review: ${updated.length} item(ns) atualizado(s) no board.`);
|
|
203
269
|
}
|
|
@@ -5,7 +5,7 @@ import { execSync } from 'node:child_process';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { resolveToken } from '../api/auth.mjs';
|
|
7
7
|
import {
|
|
8
|
-
CONFIG_FILE, STAGE_DEVELOPMENT, STAGE_CODE_REVIEW,
|
|
8
|
+
CONFIG_FILE, STAGE_DEVELOPMENT, STAGE_CODE_REVIEW, STAGE_DONE,
|
|
9
9
|
PROGRESS_TODO, PROGRESS_IN_PROGRESS, PROGRESS_DONE,
|
|
10
10
|
} from '../config.mjs';
|
|
11
11
|
import { getIssue } from '../api/github-rest.mjs';
|
|
@@ -26,7 +26,7 @@ async function resolveFeature(token, startNodeId) {
|
|
|
26
26
|
const parent = await getIssueParent(token, current);
|
|
27
27
|
if (!parent) return null;
|
|
28
28
|
if (detectIssueType({ title: parent.title }) === 'Feature') {
|
|
29
|
-
return { number: parent.number, title: parent.title };
|
|
29
|
+
return { number: parent.number, title: parent.title, nodeId: parent.nodeId };
|
|
30
30
|
}
|
|
31
31
|
current = parent.nodeId;
|
|
32
32
|
}
|
|
@@ -46,7 +46,7 @@ function readSpecPlan(featureDir) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// Monta o markdown de contexto que será entregue ao spec-kit implement.
|
|
49
|
-
function buildContext({ type, issue, tasks, feature, spec, plan, specPath, planPath }) {
|
|
49
|
+
function buildContext({ type, issue, tasks, feature, siblingStories = [], spec, plan, specPath, planPath }) {
|
|
50
50
|
const lines = [];
|
|
51
51
|
lines.push(`# Contexto de implementação — ${type} #${issue.number}`);
|
|
52
52
|
lines.push('');
|
|
@@ -57,56 +57,59 @@ function buildContext({ type, issue, tasks, feature, spec, plan, specPath, planP
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// Modelo do board: "Etapa" (coluna do kanban) = DIREÇÃO, só avança; "Status"
|
|
60
|
-
// (Todo/In Progress/Done) = PROGRESSO dentro da etapa
|
|
61
|
-
//
|
|
62
|
-
//
|
|
60
|
+
// (Todo/In Progress/Done) = PROGRESSO dentro da etapa. O desenvolvimento de
|
|
61
|
+
// cada Task acontece na Etapa Desenvolvimento (Status In Progress); ao concluir,
|
|
62
|
+
// a Task avança para a Etapa Done (Status Done). A Story avança para Code Review.
|
|
63
63
|
lines.push('');
|
|
64
64
|
lines.push('## Instruções de execução (uma task por vez, sequencial)');
|
|
65
65
|
lines.push('');
|
|
66
66
|
lines.push(
|
|
67
67
|
'Há **dois campos** no board com papéis diferentes — não os confunda:\n' +
|
|
68
|
-
`- **Etapa** (Backlog → … → ${STAGE_DEVELOPMENT} → ${STAGE_CODE_REVIEW} → …): a DIREÇÃO no kanban. Uma issue só **avança**, **nunca** volta para uma etapa anterior.\n` +
|
|
69
|
-
`- **Status** (${PROGRESS_TODO} → ${PROGRESS_IN_PROGRESS} → ${PROGRESS_DONE}): o **progresso dentro da etapa atual**. Ao avançar de etapa, o Status
|
|
68
|
+
`- **Etapa** (Backlog → … → ${STAGE_DEVELOPMENT} → ${STAGE_CODE_REVIEW} → … → ${STAGE_DONE}): a DIREÇÃO no kanban. Uma issue só **avança**, **nunca** volta para uma etapa anterior.\n` +
|
|
69
|
+
`- **Status** (${PROGRESS_TODO} → ${PROGRESS_IN_PROGRESS} → ${PROGRESS_DONE}): o **progresso dentro da etapa atual**. Ao avançar de etapa, o Status reinicia em ${PROGRESS_TODO} — exceto ao chegar na Etapa ${STAGE_DONE}, onde o Status fica **${PROGRESS_DONE}**.`
|
|
70
70
|
);
|
|
71
71
|
lines.push('');
|
|
72
72
|
if (type === 'Story') {
|
|
73
73
|
lines.push(
|
|
74
74
|
`Nesta fase, a Story #${issue.number} e suas Tasks estão na Etapa **${STAGE_DEVELOPMENT}**. ` +
|
|
75
|
-
|
|
75
|
+
`Durante o desenvolvimento de cada Task, mexa no **Status**; ao **concluir** a Task, ela ` +
|
|
76
|
+
`**avança para a Etapa ${STAGE_DONE}** (com Status ${PROGRESS_DONE}).`
|
|
76
77
|
);
|
|
77
78
|
lines.push('');
|
|
78
79
|
lines.push(`1. Garanta que a Story #${issue.number} está na Etapa **${STAGE_DEVELOPMENT}** com Status **${PROGRESS_IN_PROGRESS}**, e que as Tasks estão nessa Etapa com Status **${PROGRESS_TODO}**.`);
|
|
79
80
|
lines.push(`2. Implemente as ${tasks.length} task(s) **uma de cada vez, na ordem abaixo**. É PROIBIDO ter mais de uma Task com Status **${PROGRESS_IN_PROGRESS}** ao mesmo tempo. Para **cada Task**, na ordem:`);
|
|
80
81
|
lines.push(` 1. **Ao começar:** Status da Task → **${PROGRESS_IN_PROGRESS}** (a Etapa continua ${STAGE_DEVELOPMENT}).`);
|
|
81
82
|
lines.push(' 2. **Implemente** a Task por completo.');
|
|
82
|
-
lines.push(` 3. **Ao concluir:**
|
|
83
|
+
lines.push(` 3. **Ao concluir:** **avance a Task para a Etapa ${STAGE_DONE}** com Status **${PROGRESS_DONE}**.`);
|
|
83
84
|
lines.push(' 4. Só então avance para a próxima Task.');
|
|
84
85
|
lines.push('');
|
|
85
|
-
lines.push(`3. **Ao concluir TODA a Story** (todas as Tasks
|
|
86
|
+
lines.push(`3. **Ao concluir TODA a Story** (todas as Tasks na Etapa ${STAGE_DONE}):`);
|
|
86
87
|
lines.push(' 1. Faça o **commit** de todas as mudanças da implementação.');
|
|
87
88
|
lines.push(` 2. Abra o **Pull Request** da Story #${issue.number}.`);
|
|
88
89
|
lines.push(
|
|
89
|
-
` 3. **Avance a Etapa
|
|
90
|
-
`
|
|
91
|
-
`para ${STAGE_CODE_REVIEW}**, e **reinicie o Status de cada um para ${PROGRESS_TODO}**. ` +
|
|
92
|
-
'Tudo anda junto com a Feature e sempre para frente.'
|
|
90
|
+
` 3. **Avance a Etapa da Story #${issue.number} para ${STAGE_CODE_REVIEW}** ` +
|
|
91
|
+
`(reinicie o Status para ${PROGRESS_TODO}). As Tasks já estão em ${STAGE_DONE}.`
|
|
93
92
|
);
|
|
93
|
+
// A Feature só avança quando TODAS as suas Stories estiverem implementadas.
|
|
94
|
+
lines.push(` 4. **A Feature${feature ? ` #${feature.number}` : ' (issue pai da Story)'} só avança para ${STAGE_CODE_REVIEW} quando TODAS as suas Stories estiverem implementadas:**`);
|
|
95
|
+
if (siblingStories.length === 0) {
|
|
96
|
+
lines.push(` - Esta é a **única Story** da Feature → avance também a Feature${feature ? ` #${feature.number}` : ''} para ${STAGE_CODE_REVIEW} (Status → ${PROGRESS_TODO}).`);
|
|
97
|
+
} else {
|
|
98
|
+
lines.push(` - Outras Stories desta Feature: ${siblingStories.map(s => `#${s.number}`).join(', ')}.`);
|
|
99
|
+
lines.push(` - Verifique a Etapa de cada uma. Avance a Feature para ${STAGE_CODE_REVIEW} **somente se TODAS** já estiverem em ${STAGE_CODE_REVIEW} (ou etapa posterior).`);
|
|
100
|
+
lines.push(` - Se **qualquer** Story ainda estiver pendente (antes de ${STAGE_CODE_REVIEW}), **NÃO mova a Feature** — deixe-a em ${STAGE_DEVELOPMENT} até a última Story ser concluída.`);
|
|
101
|
+
}
|
|
94
102
|
} else {
|
|
95
|
-
lines.push(
|
|
96
|
-
`Esta Task #${issue.number} está na Etapa **${STAGE_DEVELOPMENT}**. Acompanhe o progresso pelo ` +
|
|
97
|
-
'**Status** — **não** mude a Etapa aqui:'
|
|
98
|
-
);
|
|
103
|
+
lines.push(`Esta Task #${issue.number} está na Etapa **${STAGE_DEVELOPMENT}**:`);
|
|
99
104
|
lines.push('');
|
|
100
|
-
lines.push(`1. **Ao começar:** Status da Task #${issue.number} → **${PROGRESS_IN_PROGRESS}
|
|
105
|
+
lines.push(`1. **Ao começar:** Status da Task #${issue.number} → **${PROGRESS_IN_PROGRESS}** (Etapa continua ${STAGE_DEVELOPMENT}).`);
|
|
101
106
|
lines.push('2. **Implemente** a Task por completo.');
|
|
102
|
-
lines.push(`3. **Ao concluir:**
|
|
103
|
-
lines.push('');
|
|
104
|
-
lines.push(`> A Etapa avança para **${STAGE_CODE_REVIEW}** (Status reinicia em ${PROGRESS_TODO}) junto com a Story/Feature quando o PR é aberto — nunca volta para uma etapa anterior.`);
|
|
107
|
+
lines.push(`3. **Ao concluir:** **avance a Task #${issue.number} para a Etapa ${STAGE_DONE}** com Status **${PROGRESS_DONE}**.`);
|
|
105
108
|
}
|
|
106
109
|
lines.push('');
|
|
107
110
|
lines.push(
|
|
108
111
|
`> **Regra do board:** a **Etapa** só avança (nunca retrocede); o **Status** (${PROGRESS_TODO}/${PROGRESS_IN_PROGRESS}/${PROGRESS_DONE}) ` +
|
|
109
|
-
|
|
112
|
+
`mede o progresso dentro da etapa atual e reinicia a cada avanço (na Etapa ${STAGE_DONE}, o Status fica ${PROGRESS_DONE}).`
|
|
110
113
|
);
|
|
111
114
|
|
|
112
115
|
lines.push('');
|
|
@@ -212,7 +215,7 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
212
215
|
}
|
|
213
216
|
|
|
214
217
|
// 4. Resolve a Feature (pai na cadeia) — para spec.md/plan.md e para as
|
|
215
|
-
// instruções de fim de Story (
|
|
218
|
+
// instruções de fim de Story (avançar a Etapa para Code Review).
|
|
216
219
|
const feature = await resolveFeature(token, issue.node_id);
|
|
217
220
|
let featureDir = featureDirOpt;
|
|
218
221
|
if (!featureDir && feature?.title) {
|
|
@@ -227,8 +230,19 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
227
230
|
p.log.warn('Não foi possível resolver a Feature; seguindo só com as tasks (use --feature-dir).');
|
|
228
231
|
}
|
|
229
232
|
|
|
233
|
+
// 4b. Stories irmãs da Feature — a Feature só avança para Code Review quando
|
|
234
|
+
// TODAS as suas Stories estiverem implementadas. Lista as outras para o agente
|
|
235
|
+
// verificar antes de mover a Feature.
|
|
236
|
+
let siblingStories = [];
|
|
237
|
+
if (type === 'Story' && feature?.nodeId) {
|
|
238
|
+
const featureSubs = await listSubIssues(token, feature.nodeId).catch(() => []);
|
|
239
|
+
siblingStories = featureSubs
|
|
240
|
+
.filter(s => detectIssueType({ title: s.title }) === 'Story' && s.number !== issue.number)
|
|
241
|
+
.map(s => ({ number: s.number, title: s.title }));
|
|
242
|
+
}
|
|
243
|
+
|
|
230
244
|
// 5. Monta e grava o arquivo de contexto.
|
|
231
|
-
const context = buildContext({ type, issue, tasks, feature, ...specPlan });
|
|
245
|
+
const context = buildContext({ type, issue, tasks, feature, siblingStories, ...specPlan });
|
|
232
246
|
mkdirSync(WORK_DIR, { recursive: true });
|
|
233
247
|
const tasksFile = path.join(WORK_DIR, `implement-${issueNumber}.md`);
|
|
234
248
|
writeFileSync(tasksFile, context);
|
package/src/config.mjs
CHANGED
|
@@ -60,6 +60,7 @@ export const STATUS_OPTIONS = [
|
|
|
60
60
|
// Etapas (campo Etapa) referenciadas pelo fluxo de implementação.
|
|
61
61
|
export const STAGE_DEVELOPMENT = STATUS_OPTIONS.find(s => s.name.includes('Desenvolvimento')).name;
|
|
62
62
|
export const STAGE_CODE_REVIEW = STATUS_OPTIONS.find(s => s.name.includes('Code Review')).name;
|
|
63
|
+
export const STAGE_DONE = STATUS_OPTIONS.find(s => s.name.includes('Done')).name;
|
|
63
64
|
// Ordem canônica das etapas — usada para garantir que uma issue só AVANÇA.
|
|
64
65
|
export const STAGE_ORDER = STATUS_OPTIONS.map(s => s.name);
|
|
65
66
|
|
|
@@ -166,7 +166,7 @@ Mesmas flags do `issue` (exceto `--type`, fixo em `feature`). Mantido para o flu
|
|
|
166
166
|
| `--feature-dir <path>` | string | Caminho `docs/features/<slug>` para anexar `spec.md`/`plan.md` como contexto (sobrescreve a resolução automática). |
|
|
167
167
|
| `--dry-run` | flag | Monta o contexto e imprime o comando do spec-kit **sem executar**. |
|
|
168
168
|
|
|
169
|
-
> Diferente dos quatro acima, `implement` roda **localmente** (lê `.spec-wave.json`, como `issue`), não por Action. Detecta o tipo da issue: **Story** → coleta todas as Tasks (sub-issues) e aciona o spec-kit uma única vez; **Task** → só aquela task. Monta o contexto em `.spec-wave/implement-<n>.md` e chama o comando configurado em `specKit.command` (no `.spec-wave.json`) ou na env `SPEC_WAVE_IMPLEMENT_CMD`. Placeholders disponíveis no template: `{tasksFile} {specFile} {planFile} {issue} {type} {title}`. Se nada estiver configurado, ele apenas monta o contexto e mostra como configurar (não executa). O contexto inclui instruções para o agente implementar as Tasks **sequencialmente, uma por vez
|
|
169
|
+
> Diferente dos quatro acima, `implement` roda **localmente** (lê `.spec-wave.json`, como `issue`), não por Action. Detecta o tipo da issue: **Story** → coleta todas as Tasks (sub-issues) e aciona o spec-kit uma única vez; **Task** → só aquela task. Monta o contexto em `.spec-wave/implement-<n>.md` e chama o comando configurado em `specKit.command` (no `.spec-wave.json`) ou na env `SPEC_WAVE_IMPLEMENT_CMD`. Placeholders disponíveis no template: `{tasksFile} {specFile} {planFile} {issue} {type} {title}`. Se nada estiver configurado, ele apenas monta o contexto e mostra como configurar (não executa). O contexto inclui instruções para o agente implementar as Tasks **sequencialmente, uma por vez** (nunca duas com Status "In Progress" ao mesmo tempo): cada Task usa o **Status** (In Progress) *dentro* da Etapa 🚧 Desenvolvimento e, **ao concluir, avança para a Etapa 🎉 Done com Status Done**. **Ao concluir toda a Story**: fazer o commit, abrir o PR e **avançar a Etapa da Story para 👀 Code Review** (Status → Todo) — as Tasks já estão em 🎉 Done. A **Feature só avança** para Code Review quando **TODAS as suas Stories** já estiverem em Code Review — enquanto houver Story pendente, a Feature fica em 🚧 Desenvolvimento. Etapa só avança (nunca volta); Status mede o progresso dentro da etapa.
|
|
170
170
|
|
|
171
171
|
---
|
|
172
172
|
|
|
@@ -396,7 +396,7 @@ Aciona o spec-kit para implementar uma **Story** (todas as suas Tasks) ou uma **
|
|
|
396
396
|
npx @spec-wave/cli implement <número> --dry-run
|
|
397
397
|
```
|
|
398
398
|
3. Mostre ao usuário o contexto montado em `.spec-wave/implement-<número>.md` e o comando. Esse arquivo contém as **instruções de execução sequencial**: implemente as Tasks **uma por vez** — mova a task para **🚧 Desenvolvimento** só ao iniciá-la e para **🎉 Done** ao concluí-la, antes de passar para a próxima. **Nunca** coloque várias tasks em "in progress" ao mesmo tempo.
|
|
399
|
-
4. **Se você (agente) for implementar diretamente** (sem `specKit.command`): siga o contexto task por task. Para cada task
|
|
399
|
+
4. **Se você (agente) for implementar diretamente** (sem `specKit.command`): siga o contexto task por task. Para cada task: Status → In Progress *dentro* da Etapa 🚧 Desenvolvimento e, **ao concluir, avance a task para a Etapa 🎉 Done com Status Done**. Atualize os campos via `gh`. **Ao concluir toda a Story**: faça o commit, abra o PR e **avance a Etapa da Story para 👀 Code Review** (Status → Todo) — as Tasks já estão em 🎉 Done. A **Feature só avança** quando **TODAS as suas Stories** já estiverem em Code Review — se houver Story pendente, deixe a Feature em 🚧 Desenvolvimento. Lembre: Etapa só avança (nunca volta); Status é o progresso dentro da etapa.
|
|
400
400
|
5. Se o usuário aprovar e o spec-kit estiver configurado, rode sem `--dry-run`:
|
|
401
401
|
```bash
|
|
402
402
|
npx @spec-wave/cli implement <número>
|
|
@@ -404,7 +404,7 @@ Aciona o spec-kit para implementar uma **Story** (todas as suas Tasks) ou uma **
|
|
|
404
404
|
- Se o spec-kit **não** estiver configurado, o comando só monta o contexto e mostra como configurar (`specKit.command` / `SPEC_WAVE_IMPLEMENT_CMD`). Ajude o usuário a definir o template (placeholders: `{tasksFile} {specFile} {planFile} {issue} {type} {title}`).
|
|
405
405
|
- Use `--feature-dir docs/features/<slug>` se a resolução automática da Feature falhar (a skill avisa com warning) e você quiser anexar `spec.md`/`plan.md` como contexto.
|
|
406
406
|
6. Se a issue **não** for Story nem Task (ex.: Feature, Bug), o comando recusa — oriente o usuário: Features se decompõem (`/spec-wave decompose`); implemente as Stories/Tasks resultantes.
|
|
407
|
-
7. Ao final (
|
|
407
|
+
7. Ao final (Tasks em **🎉 Done**, Story em **👀 Code Review**; a Feature só vai para Code Review quando a última Story concluir): confirme o resultado com o usuário e oriente a revisão do PR.
|
|
408
408
|
|
|
409
409
|
---
|
|
410
410
|
|