@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec-wave/cli",
3
- "version": "0.5.10",
3
+ "version": "0.5.11",
4
4
  "description": "Setup spec-driven GitHub workflow with Projects v2, labels, issue templates, and AI-powered Actions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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: a Feature
58
- // ancestral + a Story + as Tasks dessa Story tudo anda junto com a Feature.
59
- // Retorna Map number -> { nodeId, title }.
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 unit = new Map();
62
- const add = (n, nodeId, title) => { if (n && nodeId && !unit.has(n)) unit.set(n, { nodeId, title }); };
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 unit; }
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 unit;
69
+ if (type !== 'Feature' && type !== 'Story' && type !== 'Task') return { feature: null, items };
68
70
 
69
- // Feature ancestral (ou a própria).
70
- const feature = await resolveFeatureIssue(token, owner, repo, issueNumber);
71
- if (feature) add(feature.number, feature.node_id, feature.title);
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
- // Toda a subárvore da Feature: Stories + Tasks.
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 unit;
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, move a unidade inteira (Feature + Story +
173
- // Tasks) para Code Review tudo anda junto com a Feature.
193
+ // Para cada issue referenciada: move a Story + Tasks para Code Review. A
194
+ // Feature avança quando TODAS as suas Stories estiverem em Code Review.
174
195
  for (const num of issueNums) {
175
- const unit = await collectReviewUnit(token, owner, repo, num);
176
- for (const [n, info] of unit) {
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 + Story + Tasks):\n\n` +
241
+ `Movidos para **${CODE_REVIEW_STAGE}** (a Feature 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 — em conjunto — ${feature ? `da Feature #${feature.number}` : 'da Feature (issue pai da Story)'}, ` +
90
- `a Story #${issue.number} e todas as ${tasks.length} Task(s) (${tasks.map(t => `#${t.number}`).join(', ')}) ` +
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.'
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 (mover Feature + Story para Code Review).
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 **Feature, Story e todas as Tasks juntas** para **👀 Code Review**, reiniciando o **Status** de cada uma para **Todo**. Etapa só avança (nunca volta); Status mede o progresso dentro da etapa.
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 **Feature, Story e todas as Tasks juntas** para **👀 Code Review**, reiniciando o **Status** de cada uma para **Todo**. Lembre: Etapa só avança (nunca volta); Status é o progresso dentro da etapa.
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 implementada, commit feito, PR aberto e Feature + Story + Tasks em **👀 Code Review**): confirme o resultado com o usuário e oriente a revisão do PR.
407
+ 7. Ao final (Story + Tasks em **👀 Code Review**; a Feature 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