@spec-wave/cli 0.20.0 → 0.23.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/bin/spec-wave.mjs +2 -2
- package/package.json +1 -1
- package/src/agent/anthropic-agent.mjs +3 -1
- package/src/agent/errors.mjs +59 -13
- package/src/agent/openrouter-agent.mjs +5 -1
- package/src/api/github-graphql.mjs +127 -7
- package/src/api/github-rest.mjs +9 -2
- package/src/commands/code-review.mjs +15 -4
- package/src/commands/decompose.mjs +172 -23
- package/src/commands/doctor.mjs +88 -8
- package/src/commands/move.mjs +58 -1
- package/src/commands/order.mjs +200 -7
- package/src/commands/qa.mjs +8 -2
- package/src/commands/repair-stage.mjs +6 -1
- package/src/commands/story.mjs +6 -1
- package/src/commands/task.mjs +6 -1
- package/src/commands/triage.mjs +4 -1
- package/src/commands/validate.mjs +39 -18
- package/src/config.mjs +47 -3
- package/src/lib/board.mjs +87 -3
- package/src/lib/bug-doc.mjs +71 -0
- package/src/lib/claude.mjs +23 -6
- package/src/lib/decomposition-doc.mjs +66 -14
- package/src/lib/dependencies.mjs +14 -4
- package/src/lib/implement-board.mjs +15 -11
- package/src/plugin/.claude-plugin/plugin.json +1 -1
- package/src/plugin/skills/decompose/SKILL.md +3 -3
- package/src/plugin/skills/order/SKILL.md +8 -4
- package/src/plugin/skills/ready/SKILL.md +1 -1
- package/src/templates/skill/SKILL.md +4 -4
- package/src/templates/workflows/code-review.yml +9 -2
- package/src/templates/workflows/critique.yml +19 -2
- package/src/templates/workflows/decompose.yml +19 -2
- package/src/templates/workflows/generate-bug.yml +19 -2
- package/src/templates/workflows/generate-plan.yml +19 -2
- package/src/templates/workflows/generate-spec.yml +19 -2
- package/src/templates/workflows/qa.yml +5 -1
- package/src/templates/workflows/validate.yml +19 -2
package/bin/spec-wave.mjs
CHANGED
|
@@ -263,8 +263,8 @@ program
|
|
|
263
263
|
|
|
264
264
|
program
|
|
265
265
|
.command('order')
|
|
266
|
-
.description('Ordena as Stories
|
|
267
|
-
.argument('
|
|
266
|
+
.description('Ordena as Stories pelas dependências (topológica). Sem argumento, o mapa de todas as Features com trabalho')
|
|
267
|
+
.argument('[feature]', 'Número da issue da Feature, ex.: 12 ou #12. Omitido: todas as Features abertas fora de 🎉 Done')
|
|
268
268
|
.action(async (feature) => {
|
|
269
269
|
const { order } = await import('../src/commands/order.mjs');
|
|
270
270
|
await order({ feature }).catch(err => { console.error(err.message); process.exit(1); });
|
package/package.json
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import path from 'node:path';
|
|
26
26
|
import { withTrace, startObservation, updateSpan, endObservation } from './tracing.mjs';
|
|
27
27
|
import { ENV_FILE_PATTERN } from './tools.mjs';
|
|
28
|
-
import { summarizeToolCalls } from './errors.mjs';
|
|
28
|
+
import { summarizeToolCalls, toolSignature } from './errors.mjs';
|
|
29
29
|
import { TruncatedOutputError } from './errors.mjs';
|
|
30
30
|
|
|
31
31
|
const DEFAULT_TOOLS = ['Read', 'Glob', 'Grep'];
|
|
@@ -72,6 +72,7 @@ export async function runAnthropicAgent(prompt, options) {
|
|
|
72
72
|
// Simétrico ao backend openrouter: sem isto, um run que estoura o teto de
|
|
73
73
|
// turnos não deixa registro do que o modelo esteve fazendo.
|
|
74
74
|
toolCalls: [],
|
|
75
|
+
toolSignatures: [],
|
|
75
76
|
};
|
|
76
77
|
const quiet = options.quiet !== false;
|
|
77
78
|
|
|
@@ -159,6 +160,7 @@ export async function runAnthropicAgent(prompt, options) {
|
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
runResult.toolCalls.push(input.tool_name);
|
|
163
|
+
runResult.toolSignatures.push(toolSignature(input.tool_name, input.tool_input));
|
|
162
164
|
if (options.verbose) {
|
|
163
165
|
console.log(`\n[Tool] ${input.tool_name}(${JSON.stringify(input.tool_input)})`);
|
|
164
166
|
}
|
package/src/agent/errors.mjs
CHANGED
|
@@ -45,34 +45,80 @@ export class TruncatedOutputError extends Error {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Assinatura de uma tool call: nome + ALVO (função PURA).
|
|
50
|
+
*
|
|
51
|
+
* `Read×59` não distingue "leu 59 arquivos diferentes" de "leu o mesmo arquivo
|
|
52
|
+
* 59 vezes", e essas duas situações pedem remédios opostos. O alvo é o que
|
|
53
|
+
* separa as duas, e é o único dado do input que entra aqui — nada de conteúdo.
|
|
54
|
+
*/
|
|
55
|
+
export function toolSignature(name, input) {
|
|
56
|
+
const alvo = input && typeof input === 'object'
|
|
57
|
+
? input.file_path ?? input.path ?? input.pattern ?? input.query ?? input.command ?? null
|
|
58
|
+
: null;
|
|
59
|
+
return alvo ? `${name}:${String(alvo)}` : String(name);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* O modelo estava EXPLORANDO (e não preso num loop)? (função PURA)
|
|
64
|
+
*
|
|
65
|
+
* Explorar = chamadas majoritariamente distintas: leu muitos arquivos, varreu
|
|
66
|
+
* muitos padrões e o orçamento acabou antes de escrever. Isso VARIA entre
|
|
67
|
+
* execuções — repetir a mesma requisição costuma passar. Loop degenerado é o
|
|
68
|
+
* contrário: a mesma chamada repetida, que repetir só encarece.
|
|
69
|
+
*
|
|
70
|
+
* Amostra pequena não sustenta nenhuma das duas conclusões, e o default é o
|
|
71
|
+
* conservador (não repetir).
|
|
72
|
+
*/
|
|
73
|
+
export function looksLikeExploration(signatures = []) {
|
|
74
|
+
if (!Array.isArray(signatures) || signatures.length < 4) return false;
|
|
75
|
+
const distintas = new Set(signatures).size;
|
|
76
|
+
return distintas / signatures.length >= 0.5;
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
// Teto de turnos esgotado: o modelo respondeu com tool calls em TODOS os turnos
|
|
49
|
-
// e nunca produziu o documento.
|
|
50
|
-
//
|
|
80
|
+
// e nunca produziu o documento.
|
|
81
|
+
//
|
|
82
|
+
// NÃO é uma falha só, são duas, e a distinção é o que decide se repetir vale:
|
|
83
|
+
//
|
|
84
|
+
// • LOOP DEGENERADO (a mesma chamada de novo e de novo) é determinístico —
|
|
85
|
+
// repetir custou 55 minutos de Action num caso real: três tentativas de 25
|
|
86
|
+
// turnos (4min → 19min → 32min) para a mesma falha, que devia ter aparecido
|
|
87
|
+
// na primeira. Aqui não se repete: troca-se o modelo ou sobe-se o teto.
|
|
51
88
|
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
89
|
+
// • EXPLORAÇÃO (chamadas distintas: Read×59 em 59 arquivos, Glob×20) é
|
|
90
|
+
// VARIÂNCIA, não limite estrutural — o orçamento acabou antes de escrever.
|
|
91
|
+
// Tratar isso como determinístico mandava trocar de modelo por nada: em
|
|
92
|
+
// quatro ocorrências reais, três reaplicações da MESMA label no MESMO modelo
|
|
93
|
+
// passaram na segunda tentativa, uma delas depois de um run de 20min46s que
|
|
94
|
+
// uma repetição de ~2min teria evitado.
|
|
55
95
|
export class MaxTurnsError extends Error {
|
|
56
|
-
constructor({ provider, model, turns, action, toolCalls = [] }) {
|
|
57
|
-
// O que o modelo ficou fazendo é a informação que decide o remédio: muitas
|
|
58
|
-
// leituras distintas sugerem teto baixo; a mesma chamada repetida é loop
|
|
59
|
-
// degenerado, e aí subir o teto só encarece.
|
|
96
|
+
constructor({ provider, model, turns, action, toolCalls = [], toolSignatures = [] }) {
|
|
60
97
|
const resumo = toolCalls.length > 0
|
|
61
98
|
? ` Ferramentas mais chamadas: ${summarizeToolCalls(toolCalls)}.`
|
|
62
99
|
: '';
|
|
100
|
+
const exploracao = looksLikeExploration(toolSignatures);
|
|
101
|
+
const remedio = exploracao
|
|
102
|
+
? 'As chamadas são majoritariamente DISTINTAS — o modelo gastou o orçamento explorando, ' +
|
|
103
|
+
'o que varia entre execuções. Uma repetição costuma bastar (a CLI já faz UMA, ' +
|
|
104
|
+
'automaticamente); se insistir, suba `maxTurns` no prompt em vez de trocar o modelo.'
|
|
105
|
+
: 'As chamadas se REPETEM — é loop degenerado, e repetir reproduz a mesma perambulação ' +
|
|
106
|
+
'mais cara. Troque o modelo (por `ai.models` ou pela label ' +
|
|
107
|
+
'`spec-wave:model:<apelido>`) ou suba o teto de turnos.';
|
|
63
108
|
super(
|
|
64
109
|
`O modelo esgotou o teto de ${turns} turnos sem produzir o documento ` +
|
|
65
110
|
`(${provider} · ${model}${action ? ` · ação=${action}` : ''}): respondeu com chamadas de ` +
|
|
66
|
-
`ferramenta em todos eles.${resumo} `
|
|
67
|
-
'Repetir não ajuda — o erro é determinístico. Ou o modelo não fecha o loop de ' +
|
|
68
|
-
'ferramentas nesta tarefa (troque-o, por `ai.models` ou pela label ' +
|
|
69
|
-
'`spec-wave:model:<apelido>`), ou a exploração precisa de mais turnos.'
|
|
111
|
+
`ferramenta em todos eles.${resumo} ${remedio}`
|
|
70
112
|
);
|
|
71
113
|
this.name = 'MaxTurnsError';
|
|
72
114
|
this.maxTurns = true;
|
|
115
|
+
// Quem decide o retry (lib/claude.mjs) lê esta flag — a classificação mora
|
|
116
|
+
// aqui, junto dos dados que a sustentam.
|
|
117
|
+
this.exploration = exploracao;
|
|
73
118
|
this.turns = turns;
|
|
74
119
|
this.action = action || null;
|
|
75
120
|
this.toolCalls = toolCalls;
|
|
121
|
+
this.toolSignatures = toolSignatures;
|
|
76
122
|
}
|
|
77
123
|
}
|
|
78
124
|
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
toolDefinitionsFor,
|
|
23
23
|
} from './tools.mjs';
|
|
24
24
|
import { withTrace, startObservation, updateSpan, endObservation } from './tracing.mjs';
|
|
25
|
-
import { TruncatedOutputError, isTruncationReason, summarizeToolCalls } from './errors.mjs';
|
|
25
|
+
import { TruncatedOutputError, isTruncationReason, summarizeToolCalls, toolSignature } from './errors.mjs';
|
|
26
26
|
|
|
27
27
|
const DEFAULT_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
28
28
|
const DEFAULT_TOOLS = ['Read', 'Glob', 'Grep'];
|
|
@@ -184,6 +184,7 @@ export async function runOpenRouterAgent(prompt, options) {
|
|
|
184
184
|
// turnos sem escrever nada, o log não dizia no que ele se perdeu, e o
|
|
185
185
|
// diagnóstico exigiu reconstruir o run à mão.
|
|
186
186
|
toolCalls: [],
|
|
187
|
+
toolSignatures: [],
|
|
187
188
|
};
|
|
188
189
|
const quiet = options.quiet !== false;
|
|
189
190
|
|
|
@@ -333,6 +334,9 @@ export async function runOpenRouterAgent(prompt, options) {
|
|
|
333
334
|
}
|
|
334
335
|
|
|
335
336
|
runResult.toolCalls.push(call.function.name);
|
|
337
|
+
// Assinatura (nome + alvo) separa exploração de loop degenerado —
|
|
338
|
+
// é o que decide se o teto de turnos merece uma repetição.
|
|
339
|
+
runResult.toolSignatures.push(toolSignature(call.function.name, parsedInput));
|
|
336
340
|
if (options.verbose) console.log(`\n[Tool] ${call.function.name}(${call.function.arguments})`);
|
|
337
341
|
|
|
338
342
|
const observation = startObservation(
|
|
@@ -179,17 +179,137 @@ export async function getProjectSnapshot(token, projectId) {
|
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
/**
|
|
183
|
+
* O item deste conteúdo que JÁ está no project (função de rede, sem mutação).
|
|
184
|
+
*
|
|
185
|
+
* Vai pelo conteúdo (`node(contentId).projectItems`), não pelos itens do
|
|
186
|
+
* project: um board com milhares de itens paginaria por todos eles para achar
|
|
187
|
+
* um; uma issue está em poucos projects.
|
|
188
|
+
*
|
|
189
|
+
* @returns {Promise<string|null>} id do item, ou null se não estiver no project
|
|
190
|
+
*/
|
|
191
|
+
export async function findProjectItem(token, projectId, contentId) {
|
|
184
192
|
const client = makeClient(token);
|
|
185
193
|
const result = await client(`
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
194
|
+
query FindItem($contentId: ID!) {
|
|
195
|
+
node(id: $contentId) {
|
|
196
|
+
... on Issue { projectItems(first: 50) { nodes { id project { id } } } }
|
|
197
|
+
... on PullRequest { projectItems(first: 50) { nodes { id project { id } } } }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
`, { contentId });
|
|
201
|
+
const nodes = result?.node?.projectItems?.nodes || [];
|
|
202
|
+
return nodes.find(n => n?.project?.id === projectId)?.id ?? null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Adiciona uma issue/PR (pelo node id do conteúdo) ao Project e devolve o id do
|
|
207
|
+
* item. IDEMPOTENTE: item já presente devolve o id existente.
|
|
208
|
+
*
|
|
209
|
+
* `addProjectV2ItemById` é documentado como idempotente, mas na prática responde
|
|
210
|
+
* `Content already exists in this project` quando o item entrou por outro
|
|
211
|
+
* caminho entre a leitura e a escrita — a automação nativa "auto-add" do
|
|
212
|
+
* project, ou um run concorrente do próprio spec-wave. Como esta é a PRIMEIRA
|
|
213
|
+
* chamada de advanceToStage(), o erro abortava tudo o que vinha depois: o item
|
|
214
|
+
* ficava no board com o Status que a automação do project escreve e SEM Etapa —
|
|
215
|
+
* exatamente o estado que some de todas as telas (caso da #281). Traduzir o erro
|
|
216
|
+
* numa busca pelo item existente é o que torna a escrita seguinte possível.
|
|
217
|
+
*/
|
|
218
|
+
export async function addProjectItem(token, projectId, contentId) {
|
|
219
|
+
const client = makeClient(token);
|
|
220
|
+
try {
|
|
221
|
+
const result = await client(`
|
|
222
|
+
mutation AddItem($projectId: ID!, $contentId: ID!) {
|
|
223
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
|
|
224
|
+
item { id }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
`, { projectId, contentId });
|
|
228
|
+
return result.addProjectV2ItemById.item.id;
|
|
229
|
+
} catch (err) {
|
|
230
|
+
if (!isAlreadyInProjectError(err)) throw err;
|
|
231
|
+
const existing = await findProjectItem(token, projectId, contentId);
|
|
232
|
+
// Sem item correspondente, o erro não era duplicidade — devolvê-lo é mais
|
|
233
|
+
// honesto que um null que estouraria adiante como "field id inválido".
|
|
234
|
+
if (!existing) throw err;
|
|
235
|
+
return existing;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* O erro é "esse conteúdo já está no project"? (função PURA)
|
|
241
|
+
*
|
|
242
|
+
* A mensagem vem de fora e pode mudar; o teste é por substring e não por
|
|
243
|
+
* igualdade justamente por isso. Errar para o lado de NÃO reconhecer só
|
|
244
|
+
* restaura o comportamento antigo (o erro sobe).
|
|
245
|
+
*/
|
|
246
|
+
export function isAlreadyInProjectError(err) {
|
|
247
|
+
const msg = String(err?.message || '');
|
|
248
|
+
return /already exists in this project/i.test(msg)
|
|
249
|
+
|| /content already exists/i.test(msg);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Todos os itens do Project, com os campos SINGLE_SELECT resolvidos por NOME.
|
|
254
|
+
*
|
|
255
|
+
* Uma query paginada no lugar de duas chamadas POR ITEM
|
|
256
|
+
* (`addProjectItem` + `getItemSingleSelectValue`), que é como o `order` lia a
|
|
257
|
+
* Etapa de cada Story. Com nove Features na tela isso passava de cem chamadas
|
|
258
|
+
* para montar um mapa que cabe em duas.
|
|
259
|
+
*
|
|
260
|
+
* Só itens cujo conteúdo é Issue entram (draft e PR ficam de fora).
|
|
261
|
+
*
|
|
262
|
+
* @returns {Promise<Array<{number, title, state, nodeId, itemId, fields: Record<string,string>}>>}
|
|
263
|
+
*/
|
|
264
|
+
export async function listProjectItems(token, projectId) {
|
|
265
|
+
const client = makeClient(token);
|
|
266
|
+
const itens = [];
|
|
267
|
+
let after = null;
|
|
268
|
+
do {
|
|
269
|
+
const result = await client(`
|
|
270
|
+
query ProjectItems($id: ID!, $after: String) {
|
|
271
|
+
node(id: $id) {
|
|
272
|
+
... on ProjectV2 {
|
|
273
|
+
items(first: 100, after: $after) {
|
|
274
|
+
pageInfo { hasNextPage endCursor }
|
|
275
|
+
nodes {
|
|
276
|
+
id
|
|
277
|
+
fieldValues(first: 20) {
|
|
278
|
+
nodes {
|
|
279
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
280
|
+
name
|
|
281
|
+
field { ... on ProjectV2SingleSelectField { name } }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
content { ... on Issue { number title state id } }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
`, { id: projectId, after });
|
|
292
|
+
const page = result.node?.items;
|
|
293
|
+
for (const node of page?.nodes || []) {
|
|
294
|
+
const issue = node?.content;
|
|
295
|
+
if (!issue?.number) continue;
|
|
296
|
+
const fields = {};
|
|
297
|
+
for (const fv of node.fieldValues?.nodes || []) {
|
|
298
|
+
const nome = fv?.field?.name;
|
|
299
|
+
if (nome && fv.name) fields[nome] = fv.name;
|
|
189
300
|
}
|
|
301
|
+
itens.push({
|
|
302
|
+
number: issue.number,
|
|
303
|
+
title: issue.title,
|
|
304
|
+
state: issue.state,
|
|
305
|
+
nodeId: issue.id,
|
|
306
|
+
itemId: node.id,
|
|
307
|
+
fields,
|
|
308
|
+
});
|
|
190
309
|
}
|
|
191
|
-
|
|
192
|
-
|
|
310
|
+
after = page?.pageInfo?.hasNextPage ? page.pageInfo.endCursor : null;
|
|
311
|
+
} while (after);
|
|
312
|
+
return itens;
|
|
193
313
|
}
|
|
194
314
|
|
|
195
315
|
// Cria a relação de sub-issue nativa do GitHub (parent → child). Ambos os IDs
|
package/src/api/github-rest.mjs
CHANGED
|
@@ -281,9 +281,16 @@ export async function ensurePullRequest(
|
|
|
281
281
|
|
|
282
282
|
// `id` é o database id da issue — exigido pela API de dependências
|
|
283
283
|
// (blocked_by), que não aceita number nem node id.
|
|
284
|
-
|
|
284
|
+
//
|
|
285
|
+
// `milestone` é o NÚMERO do milestone no repo (não o título nem o node id) e só
|
|
286
|
+
// entra no payload quando existe: a API rejeita `milestone: null` com 422 em vez
|
|
287
|
+
// de tratar como "sem milestone".
|
|
288
|
+
export async function createIssue(token, owner, repo, title, body, labels, { milestone } = {}) {
|
|
285
289
|
const octokit = makeOctokit(token);
|
|
286
|
-
const res = await octokit.rest.issues.create({
|
|
290
|
+
const res = await octokit.rest.issues.create({
|
|
291
|
+
owner, repo, title, body, labels,
|
|
292
|
+
...(milestone ? { milestone } : {}),
|
|
293
|
+
});
|
|
287
294
|
return { number: res.data.number, nodeId: res.data.node_id, url: res.data.html_url, id: res.data.id };
|
|
288
295
|
}
|
|
289
296
|
|
|
@@ -282,6 +282,9 @@ export async function codeReview({ prNumber, resolveOnly = false }) {
|
|
|
282
282
|
// Resolve campos uma vez, reutiliza em todas as Features.
|
|
283
283
|
const etapaField = await resolveField(projectToken, project, 'Etapa').catch(() => null);
|
|
284
284
|
const statusField = await resolveField(projectToken, project, 'Status').catch(() => null);
|
|
285
|
+
// Repara o Work Item Type vazio de passagem (o apply nunca o escreveu).
|
|
286
|
+
// Só preenche o vazio e nunca derruba o movimento — ver ensureWorkItemType.
|
|
287
|
+
const typeField = await resolveField(projectToken, project, 'Work Item Type').catch(() => null);
|
|
285
288
|
|
|
286
289
|
const seen = new Set();
|
|
287
290
|
const updated = [];
|
|
@@ -312,7 +315,9 @@ export async function codeReview({ prNumber, resolveOnly = false }) {
|
|
|
312
315
|
if (seen.has(n)) continue;
|
|
313
316
|
seen.add(n);
|
|
314
317
|
try {
|
|
315
|
-
const moved = await advanceToStage(
|
|
318
|
+
const moved = await advanceToStage(
|
|
319
|
+
projectToken, project, etapaField, statusField, info.nodeId, DONE_STAGE, DONE_STATUS,
|
|
320
|
+
{ typeField, itemType: 'Task' });
|
|
316
321
|
if (moved) {
|
|
317
322
|
updated.push(`#${n} ${info.title} → ${DONE_STAGE}`);
|
|
318
323
|
console.log(`#${n} → "${DONE_STAGE}" / Status "${DONE_STATUS}".`);
|
|
@@ -329,7 +334,9 @@ export async function codeReview({ prNumber, resolveOnly = false }) {
|
|
|
329
334
|
if (seen.has(n)) continue;
|
|
330
335
|
seen.add(n);
|
|
331
336
|
try {
|
|
332
|
-
const moved = await advanceToStage(
|
|
337
|
+
const moved = await advanceToStage(
|
|
338
|
+
projectToken, project, etapaField, statusField, info.nodeId, CODE_REVIEW_STAGE, TODO_STATUS,
|
|
339
|
+
{ typeField, itemType: 'Story' });
|
|
333
340
|
if (moved) {
|
|
334
341
|
updated.push(`#${n} ${info.title} → ${CODE_REVIEW_STAGE}`);
|
|
335
342
|
console.log(`#${n} → "${CODE_REVIEW_STAGE}" / Status "${TODO_STATUS}".`);
|
|
@@ -346,7 +353,9 @@ export async function codeReview({ prNumber, resolveOnly = false }) {
|
|
|
346
353
|
if (seen.has(n)) continue;
|
|
347
354
|
seen.add(n);
|
|
348
355
|
try {
|
|
349
|
-
const moved = await advanceToStage(
|
|
356
|
+
const moved = await advanceToStage(
|
|
357
|
+
projectToken, project, etapaField, statusField, info.nodeId, CODE_REVIEW_STAGE, TODO_STATUS,
|
|
358
|
+
{ typeField, itemType: 'Bug' });
|
|
350
359
|
if (moved) {
|
|
351
360
|
updated.push(`#${n} ${info.title} → ${CODE_REVIEW_STAGE}`);
|
|
352
361
|
console.log(`Bug #${n} → "${CODE_REVIEW_STAGE}" / Status "${TODO_STATUS}".`);
|
|
@@ -367,7 +376,9 @@ export async function codeReview({ prNumber, resolveOnly = false }) {
|
|
|
367
376
|
console.log(`Feature #${feature.number} mantida em desenvolvimento — ainda há Stories pendentes (fora de "${CODE_REVIEW_STAGE}").`);
|
|
368
377
|
} else if (!seen.has(feature.number)) {
|
|
369
378
|
seen.add(feature.number);
|
|
370
|
-
const moved = await advanceToStage(
|
|
379
|
+
const moved = await advanceToStage(
|
|
380
|
+
projectToken, project, etapaField, statusField, feature.nodeId, CODE_REVIEW_STAGE, TODO_STATUS,
|
|
381
|
+
{ typeField, itemType: 'Feature' });
|
|
371
382
|
if (moved) {
|
|
372
383
|
updated.push(`#${feature.number} ${feature.title} (Feature) → ${CODE_REVIEW_STAGE}`);
|
|
373
384
|
console.log(`Feature #${feature.number} → "${CODE_REVIEW_STAGE}" (todas as Stories concluídas).`);
|