@spec-wave/cli 0.5.10 → 0.5.11
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
CHANGED
|
@@ -54,24 +54,27 @@ async function resolveFeatureIssue(token, owner, repo, issueNumber) {
|
|
|
54
54
|
return null;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// Coleta a "unidade de review" de uma issue referenciada no PR
|
|
58
|
-
//
|
|
59
|
-
//
|
|
57
|
+
// Coleta a "unidade de review" de uma issue referenciada no PR. Separa a
|
|
58
|
+
// Feature dos demais itens, porque a Feature tem regra própria (só avança
|
|
59
|
+
// quando TODAS as suas Stories estiverem em Code Review). Retorna
|
|
60
|
+
// { feature: {number,nodeId,title}|null, items: Map(number -> {nodeId,title}) }
|
|
61
|
+
// onde `items` são as Stories + Tasks que andam juntas neste PR.
|
|
60
62
|
async function collectReviewUnit(token, owner, repo, issueNumber) {
|
|
61
|
-
const
|
|
62
|
-
const add = (n, nodeId, title) => { if (n && nodeId && !
|
|
63
|
+
const items = new Map();
|
|
64
|
+
const add = (n, nodeId, title) => { if (n && nodeId && !items.has(n)) items.set(n, { nodeId, title }); };
|
|
63
65
|
|
|
64
66
|
let issue;
|
|
65
|
-
try { issue = await getIssue(token, owner, repo, issueNumber); } catch { return
|
|
67
|
+
try { issue = await getIssue(token, owner, repo, issueNumber); } catch { return { feature: null, items }; }
|
|
66
68
|
const type = detectIssueType(issue);
|
|
67
|
-
if (type !== 'Feature' && type !== 'Story' && type !== 'Task') return
|
|
69
|
+
if (type !== 'Feature' && type !== 'Story' && type !== 'Task') return { feature: null, items };
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
const feature =
|
|
71
|
-
|
|
71
|
+
const featureIssue = await resolveFeatureIssue(token, owner, repo, issueNumber);
|
|
72
|
+
const feature = featureIssue
|
|
73
|
+
? { number: featureIssue.number, nodeId: featureIssue.node_id, title: featureIssue.title }
|
|
74
|
+
: null;
|
|
72
75
|
|
|
73
76
|
if (type === 'Feature') {
|
|
74
|
-
//
|
|
77
|
+
// Referência direta à Feature: toda a subárvore (Stories + Tasks).
|
|
75
78
|
const stories = await listSubIssues(token, issue.node_id).catch(() => []);
|
|
76
79
|
for (const st of stories) {
|
|
77
80
|
add(st.number, st.nodeId, st.title);
|
|
@@ -91,7 +94,24 @@ async function collectReviewUnit(token, owner, repo, issueNumber) {
|
|
|
91
94
|
for (const t of tasks) add(t.number, t.nodeId, t.title);
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
|
-
return
|
|
97
|
+
return { feature, items };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// A Feature só avança quando TODAS as suas Stories já estiverem em Code Review
|
|
101
|
+
// (ou etapa posterior). readToken lê as issues; projToken opera no Project.
|
|
102
|
+
async function allStoriesReadyForReview(readToken, projToken, project, etapaField, featureNodeId) {
|
|
103
|
+
if (!etapaField?.id) return true; // sem campo Etapa não há como checar — assume ok
|
|
104
|
+
const subs = await listSubIssues(readToken, featureNodeId).catch(() => []);
|
|
105
|
+
const stories = subs.filter(s => detectIssueType({ title: s.title }) === 'Story');
|
|
106
|
+
if (stories.length === 0) return true;
|
|
107
|
+
const tgtIdx = STAGE_ORDER.indexOf(CODE_REVIEW_STAGE);
|
|
108
|
+
for (const st of stories) {
|
|
109
|
+
const itemId = await addProjectItem(projToken, project.id, st.nodeId);
|
|
110
|
+
const current = await getItemSingleSelectValue(projToken, itemId, etapaField.id).catch(() => null);
|
|
111
|
+
const idx = current ? STAGE_ORDER.indexOf(current) : -1;
|
|
112
|
+
if (idx === -1 || idx < tgtIdx) return false; // há Story pendente
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
95
115
|
}
|
|
96
116
|
|
|
97
117
|
// Avança um item do board para a Etapa "👀 Code Review" e reinicia o Status
|
|
@@ -168,12 +188,14 @@ export async function codeReview({ prNumber }) {
|
|
|
168
188
|
|
|
169
189
|
const seen = new Set();
|
|
170
190
|
const updated = [];
|
|
191
|
+
const featuresChecked = new Set();
|
|
171
192
|
|
|
172
|
-
// Para cada issue referenciada
|
|
173
|
-
//
|
|
193
|
+
// Para cada issue referenciada: move a Story + Tasks para Code Review. A
|
|
194
|
+
// Feature só avança quando TODAS as suas Stories já estiverem em Code Review.
|
|
174
195
|
for (const num of issueNums) {
|
|
175
|
-
const
|
|
176
|
-
|
|
196
|
+
const { feature, items } = await collectReviewUnit(token, owner, repo, num);
|
|
197
|
+
|
|
198
|
+
for (const [n, info] of items) {
|
|
177
199
|
if (seen.has(n)) continue;
|
|
178
200
|
seen.add(n);
|
|
179
201
|
try {
|
|
@@ -188,13 +210,35 @@ export async function codeReview({ prNumber }) {
|
|
|
188
210
|
console.warn(`Falha ao atualizar #${n}: ${err.message}`);
|
|
189
211
|
}
|
|
190
212
|
}
|
|
213
|
+
|
|
214
|
+
// Feature: só avança se todas as suas Stories já estão em Code Review+.
|
|
215
|
+
if (feature && !featuresChecked.has(feature.number)) {
|
|
216
|
+
featuresChecked.add(feature.number);
|
|
217
|
+
try {
|
|
218
|
+
const ready = await allStoriesReadyForReview(token, projectToken, project, etapaField, feature.nodeId);
|
|
219
|
+
if (!ready) {
|
|
220
|
+
console.log(`Feature #${feature.number} mantida em desenvolvimento — ainda há Stories pendentes (fora de "${CODE_REVIEW_STAGE}").`);
|
|
221
|
+
} else if (!seen.has(feature.number)) {
|
|
222
|
+
seen.add(feature.number);
|
|
223
|
+
const moved = await setCodeReview(projectToken, project, etapaField, statusField, feature.nodeId);
|
|
224
|
+
if (moved) {
|
|
225
|
+
updated.push(`#${feature.number} ${feature.title} (Feature)`);
|
|
226
|
+
console.log(`Feature #${feature.number} → "${CODE_REVIEW_STAGE}" (todas as Stories concluídas).`);
|
|
227
|
+
} else {
|
|
228
|
+
console.log(`Feature #${feature.number} já está em "${CODE_REVIEW_STAGE}" ou etapa posterior.`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.warn(`Falha ao avaliar a Feature #${feature.number}: ${err.message}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
191
235
|
}
|
|
192
236
|
|
|
193
237
|
if (updated.length > 0) {
|
|
194
238
|
await commentOnIssue(
|
|
195
239
|
token, owner, repo, parseInt(prNumber, 10),
|
|
196
240
|
`🔍 **Code Review iniciado**\n\n` +
|
|
197
|
-
`Movidos para **${CODE_REVIEW_STAGE}** (Feature
|
|
241
|
+
`Movidos para **${CODE_REVIEW_STAGE}** (a Feature só avança quando todas as suas Stories concluírem):\n\n` +
|
|
198
242
|
updated.map(f => `- ${f}`).join('\n')
|
|
199
243
|
).catch(() => {});
|
|
200
244
|
}
|
|
@@ -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('');
|
|
@@ -86,11 +86,19 @@ function buildContext({ type, issue, tasks, feature, spec, plan, specPath, planP
|
|
|
86
86
|
lines.push(' 1. Faça o **commit** de todas as mudanças da implementação.');
|
|
87
87
|
lines.push(` 2. Abra o **Pull Request** da Story #${issue.number}.`);
|
|
88
88
|
lines.push(
|
|
89
|
-
` 3. **Avance a Etapa
|
|
90
|
-
`
|
|
91
|
-
`
|
|
92
|
-
'Tudo anda junto com a Feature e sempre para frente.'
|
|
89
|
+
` 3. **Avance a Etapa da Story #${issue.number} e de todas as suas ${tasks.length} Task(s) ` +
|
|
90
|
+
`(${tasks.map(t => `#${t.number}`).join(', ')}) para ${STAGE_CODE_REVIEW}**, reiniciando o Status ` +
|
|
91
|
+
`de cada uma para ${PROGRESS_TODO}. (Story e Tasks andam juntas.)`
|
|
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
103
|
lines.push(
|
|
96
104
|
`Esta Task #${issue.number} está na Etapa **${STAGE_DEVELOPMENT}**. Acompanhe o progresso pelo ` +
|
|
@@ -212,7 +220,7 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
212
220
|
}
|
|
213
221
|
|
|
214
222
|
// 4. Resolve a Feature (pai na cadeia) — para spec.md/plan.md e para as
|
|
215
|
-
// instruções de fim de Story (
|
|
223
|
+
// instruções de fim de Story (avançar a Etapa para Code Review).
|
|
216
224
|
const feature = await resolveFeature(token, issue.node_id);
|
|
217
225
|
let featureDir = featureDirOpt;
|
|
218
226
|
if (!featureDir && feature?.title) {
|
|
@@ -227,8 +235,19 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
227
235
|
p.log.warn('Não foi possível resolver a Feature; seguindo só com as tasks (use --feature-dir).');
|
|
228
236
|
}
|
|
229
237
|
|
|
238
|
+
// 4b. Stories irmãs da Feature — a Feature só avança para Code Review quando
|
|
239
|
+
// TODAS as suas Stories estiverem implementadas. Lista as outras para o agente
|
|
240
|
+
// verificar antes de mover a Feature.
|
|
241
|
+
let siblingStories = [];
|
|
242
|
+
if (type === 'Story' && feature?.nodeId) {
|
|
243
|
+
const featureSubs = await listSubIssues(token, feature.nodeId).catch(() => []);
|
|
244
|
+
siblingStories = featureSubs
|
|
245
|
+
.filter(s => detectIssueType({ title: s.title }) === 'Story' && s.number !== issue.number)
|
|
246
|
+
.map(s => ({ number: s.number, title: s.title }));
|
|
247
|
+
}
|
|
248
|
+
|
|
230
249
|
// 5. Monta e grava o arquivo de contexto.
|
|
231
|
-
const context = buildContext({ type, issue, tasks, feature, ...specPlan });
|
|
250
|
+
const context = buildContext({ type, issue, tasks, feature, siblingStories, ...specPlan });
|
|
232
251
|
mkdirSync(WORK_DIR, { recursive: true });
|
|
233
252
|
const tasksFile = path.join(WORK_DIR, `implement-${issueNumber}.md`);
|
|
234
253
|
writeFileSync(tasksFile, context);
|
|
@@ -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**, usando o campo **Status** (Todo → In Progress → Done) para o progresso *dentro* da Etapa 🚧 Desenvolvimento — sem trocar a Etapa das tasks (nunca duas com Status "In Progress" ao mesmo tempo). **Ao concluir toda a Story**: fazer o commit, abrir o PR e **avançar a Etapa** da **
|
|
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**, usando o campo **Status** (Todo → In Progress → Done) para o progresso *dentro* da Etapa 🚧 Desenvolvimento — sem trocar a Etapa das tasks (nunca duas com Status "In Progress" ao mesmo tempo). **Ao concluir toda a Story**: fazer o commit, abrir o PR e **avançar a Etapa** da **Story e de todas as suas Tasks** para **👀 Code Review** (Status → Todo). 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, use o campo **Status** (In Progress ao começar → Done ao concluir) *dentro* da Etapa 🚧 Desenvolvimento — não troque a Etapa da task. Atualize os campos via `gh`. **Ao concluir toda a Story**: faça o commit, abra o PR e **avance a Etapa** da **
|
|
399
|
+
4. **Se você (agente) for implementar diretamente** (sem `specKit.command`): siga o contexto task por task. Para cada task, use o campo **Status** (In Progress ao começar → Done ao concluir) *dentro* da Etapa 🚧 Desenvolvimento — não troque a Etapa da task. Atualize os campos via `gh`. **Ao concluir toda a Story**: faça o commit, abra o PR e **avance a Etapa** da **Story e de todas as suas Tasks** para **👀 Code Review** (Status → Todo). 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 (Story
|
|
407
|
+
7. Ao final (Story + Tasks 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
|
|