@spec-wave/cli 0.5.11 → 0.7.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 +37 -0
- package/package.json +4 -1
- package/src/api/github-rest.mjs +45 -1
- package/src/commands/code-review.mjs +66 -83
- package/src/commands/decompose.mjs +278 -61
- package/src/commands/doctor.mjs +415 -0
- package/src/commands/generate-plan.mjs +58 -3
- package/src/commands/generate-spec.mjs +35 -2
- package/src/commands/implement.mjs +134 -24
- package/src/commands/init.mjs +2 -2
- package/src/commands/order.mjs +172 -0
- package/src/commands/qa.mjs +8 -46
- package/src/commands/story.mjs +128 -0
- package/src/commands/task.mjs +183 -0
- package/src/commands/validate.mjs +20 -3
- package/src/config.mjs +38 -0
- package/src/lib/board.mjs +106 -0
- package/src/lib/claude.mjs +71 -11
- package/src/lib/code-digest.mjs +183 -0
- package/src/lib/critique.mjs +158 -0
- package/src/lib/dependencies.mjs +92 -0
- package/src/lib/output-lint.mjs +92 -0
- package/src/setup/files.mjs +8 -20
- package/src/templates/skill/SKILL.md +105 -13
- package/src/templates/workflows/code-review.yml +4 -0
- package/src/templates/workflows/decompose.yml +7 -0
- package/src/templates/workflows/generate-plan.yml +4 -0
- package/src/templates/workflows/generate-spec.yml +4 -0
- package/src/templates/workflows/qa.yml +4 -0
- package/src/templates/workflows/validate.yml +4 -0
- package/src/ui/wizard.mjs +2 -2
package/bin/spec-wave.mjs
CHANGED
|
@@ -193,4 +193,41 @@ program
|
|
|
193
193
|
await implement({ issue, ...options }).catch(err => { console.error(err.message); process.exit(1); });
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
+
program
|
|
197
|
+
.command('order')
|
|
198
|
+
.description('Ordena as Stories de uma Feature pelas dependências (topológica)')
|
|
199
|
+
.argument('<feature>', 'Número da issue da Feature, ex.: 12 ou #12')
|
|
200
|
+
.action(async (feature) => {
|
|
201
|
+
const { order } = await import('../src/commands/order.mjs');
|
|
202
|
+
await order({ feature }).catch(err => { console.error(err.message); process.exit(1); });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
program
|
|
206
|
+
.command('task')
|
|
207
|
+
.description('Gerencia uma Task no board: start (Status "In Progress") ou done (Done)')
|
|
208
|
+
.argument('<action>', 'Ação: start ou done')
|
|
209
|
+
.argument('<n>', 'Número da issue da Task, ex.: 12 ou #12')
|
|
210
|
+
.action(async (action, n) => {
|
|
211
|
+
const { task } = await import('../src/commands/task.mjs');
|
|
212
|
+
await task({ action, issue: n }).catch(err => { console.error(err.message); process.exit(1); });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
program
|
|
216
|
+
.command('story')
|
|
217
|
+
.description('Gerencia uma Story no board: review (move para Code Review)')
|
|
218
|
+
.argument('<action>', 'Ação: review')
|
|
219
|
+
.argument('<n>', 'Número da issue da Story, ex.: 12 ou #12')
|
|
220
|
+
.action(async (action, n) => {
|
|
221
|
+
const { story } = await import('../src/commands/story.mjs');
|
|
222
|
+
await story({ action, issue: n }).catch(err => { console.error(err.message); process.exit(1); });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
program
|
|
226
|
+
.command('doctor')
|
|
227
|
+
.description('Diagnostica a configuração do spec-wave no repositório atual')
|
|
228
|
+
.action(async () => {
|
|
229
|
+
const { doctor } = await import('../src/commands/doctor.mjs');
|
|
230
|
+
await doctor().catch(err => { console.error(err.message); process.exit(1); });
|
|
231
|
+
});
|
|
232
|
+
|
|
196
233
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spec-wave/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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": {
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"bin",
|
|
14
14
|
"src"
|
|
15
15
|
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test test/*.test.mjs"
|
|
18
|
+
},
|
|
16
19
|
"engines": {
|
|
17
20
|
"node": ">=20"
|
|
18
21
|
},
|
package/src/api/github-rest.mjs
CHANGED
|
@@ -87,10 +87,12 @@ export async function upsertFile(token, owner, repo, path, content, message) {
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// `id` é o database id da issue — exigido pela API de dependências
|
|
91
|
+
// (blocked_by), que não aceita number nem node id.
|
|
90
92
|
export async function createIssue(token, owner, repo, title, body, labels) {
|
|
91
93
|
const octokit = makeOctokit(token);
|
|
92
94
|
const res = await octokit.rest.issues.create({ owner, repo, title, body, labels });
|
|
93
|
-
return { number: res.data.number, nodeId: res.data.node_id, url: res.data.html_url };
|
|
95
|
+
return { number: res.data.number, nodeId: res.data.node_id, url: res.data.html_url, id: res.data.id };
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
export async function getIssue(token, owner, repo, issueNumber) {
|
|
@@ -148,6 +150,48 @@ export async function removeLabel(token, owner, repo, issueNumber, labelName) {
|
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
|
|
153
|
+
// Lista todos os comentários de uma issue/PR: [{ author, body, createdAt }].
|
|
154
|
+
// Paginado — traz o histórico completo (usado pela crítica adversarial).
|
|
155
|
+
export async function listIssueComments(token, owner, repo, issueNumber) {
|
|
156
|
+
const octokit = makeOctokit(token);
|
|
157
|
+
const comments = await octokit.paginate(octokit.rest.issues.listComments, {
|
|
158
|
+
owner,
|
|
159
|
+
repo,
|
|
160
|
+
issue_number: issueNumber,
|
|
161
|
+
per_page: 100,
|
|
162
|
+
});
|
|
163
|
+
return comments.map(c => ({
|
|
164
|
+
author: c.user?.login || '',
|
|
165
|
+
body: c.body || '',
|
|
166
|
+
createdAt: c.created_at,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Marca uma issue como bloqueada por outra (relação nativa do GitHub).
|
|
171
|
+
// `blockingIssueId` é o DATABASE id da issue bloqueadora (não o number nem o
|
|
172
|
+
// node id — ver createIssue). Erros propagam: o chamador usa como fallback.
|
|
173
|
+
export async function addBlockedBy(token, owner, repo, issueNumber, blockingIssueId) {
|
|
174
|
+
const octokit = makeOctokit(token);
|
|
175
|
+
await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by', {
|
|
176
|
+
owner,
|
|
177
|
+
repo,
|
|
178
|
+
issue_number: issueNumber,
|
|
179
|
+
issue_id: blockingIssueId,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Lista as issues que bloqueiam `issueNumber`: [{ number, title, state, id }].
|
|
184
|
+
export async function listBlockedBy(token, owner, repo, issueNumber) {
|
|
185
|
+
const octokit = makeOctokit(token);
|
|
186
|
+
const issues = await octokit.paginate('GET /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by', {
|
|
187
|
+
owner,
|
|
188
|
+
repo,
|
|
189
|
+
issue_number: issueNumber,
|
|
190
|
+
per_page: 100,
|
|
191
|
+
});
|
|
192
|
+
return issues.map(i => ({ number: i.number, title: i.title, state: i.state, id: i.id }));
|
|
193
|
+
}
|
|
194
|
+
|
|
151
195
|
export async function commentOnIssue(token, owner, repo, issueNumber, body) {
|
|
152
196
|
const octokit = makeOctokit(token);
|
|
153
197
|
await octokit.rest.issues.createComment({
|
|
@@ -2,13 +2,17 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { resolveToken } from '../api/auth.mjs';
|
|
4
4
|
import { getIssue, getPR, commentOnIssue } from '../api/github-rest.mjs';
|
|
5
|
-
import { addProjectItem,
|
|
5
|
+
import { addProjectItem, getIssueParent, listSubIssues, getItemSingleSelectValue } from '../api/github-graphql.mjs';
|
|
6
6
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { loadProjectConfig, resolveField, advanceToStage } from '../lib/board.mjs';
|
|
8
|
+
import { CONFIG_FILE, STATUS_OPTIONS, STAGE_ORDER, STAGE_DONE, PROGRESS_TODO, PROGRESS_DONE, isManualStageType } from '../config.mjs';
|
|
8
9
|
|
|
9
|
-
//
|
|
10
|
+
// Ao abrir o PR: Stories/Feature → Etapa "👀 Code Review" (Status "Todo");
|
|
11
|
+
// Tasks → Etapa "🎉 Done" (Status "Done"), pois a implementação da task terminou.
|
|
10
12
|
const CODE_REVIEW_STAGE = STATUS_OPTIONS.find(s => s.name.includes('Code Review'))?.name;
|
|
13
|
+
const DONE_STAGE = STAGE_DONE;
|
|
11
14
|
const TODO_STATUS = PROGRESS_TODO;
|
|
15
|
+
const DONE_STATUS = PROGRESS_DONE;
|
|
12
16
|
|
|
13
17
|
// Extrai números de issues referenciadas no corpo do PR (Closes #N, Fixes #N, #N solto).
|
|
14
18
|
function extractIssueNumbers(body) {
|
|
@@ -22,15 +26,6 @@ function extractIssueNumbers(body) {
|
|
|
22
26
|
return [...nums];
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
// Resolve o campo SINGLE_SELECT pelo nome: usa .spec-wave.json, legado ou API.
|
|
26
|
-
async function resolveField(token, project, name) {
|
|
27
|
-
if (project.fields?.[name]) return project.fields[name];
|
|
28
|
-
if (name === 'Etapa' && project.etapaFieldId) {
|
|
29
|
-
return { id: project.etapaFieldId, options: project.stageOptions || {} };
|
|
30
|
-
}
|
|
31
|
-
return await getSingleSelectField(token, project.id, name);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
29
|
// A partir de qualquer issue (Feature/Story/Task), sobe a hierarquia e retorna a Feature.
|
|
35
30
|
async function resolveFeatureIssue(token, owner, repo, issueNumber) {
|
|
36
31
|
let issue;
|
|
@@ -54,47 +49,52 @@ async function resolveFeatureIssue(token, owner, repo, issueNumber) {
|
|
|
54
49
|
return null;
|
|
55
50
|
}
|
|
56
51
|
|
|
57
|
-
// Coleta a "unidade de review" de uma issue referenciada no PR
|
|
58
|
-
//
|
|
59
|
-
// quando
|
|
60
|
-
// { feature:
|
|
61
|
-
// onde `items` são as Stories + Tasks que andam juntas neste PR.
|
|
52
|
+
// Coleta a "unidade de review" de uma issue referenciada no PR, separando por
|
|
53
|
+
// tipo porque cada um tem destino próprio: Tasks → Done, Stories → Code Review,
|
|
54
|
+
// Feature → Code Review (só quando todas as Stories estiverem prontas). Retorna
|
|
55
|
+
// { feature:{number,nodeId,title}|null, stories:Map, tasks:Map }.
|
|
62
56
|
async function collectReviewUnit(token, owner, repo, issueNumber) {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
57
|
+
const stories = new Map();
|
|
58
|
+
const tasks = new Map();
|
|
59
|
+
const addStory = (n, nodeId, title) => { if (n && nodeId && !stories.has(n)) stories.set(n, { nodeId, title }); };
|
|
60
|
+
const addTask = (n, nodeId, title) => { if (n && nodeId && !tasks.has(n)) tasks.set(n, { nodeId, title }); };
|
|
65
61
|
|
|
66
62
|
let issue;
|
|
67
|
-
try { issue = await getIssue(token, owner, repo, issueNumber); } catch { return { feature: null,
|
|
63
|
+
try { issue = await getIssue(token, owner, repo, issueNumber); } catch { return { feature: null, stories, tasks }; }
|
|
68
64
|
const type = detectIssueType(issue);
|
|
69
|
-
if (type !== 'Feature' && type !== 'Story' && type !== 'Task') return { feature: null,
|
|
65
|
+
if (type !== 'Feature' && type !== 'Story' && type !== 'Task') return { feature: null, stories, tasks };
|
|
70
66
|
|
|
71
67
|
const featureIssue = await resolveFeatureIssue(token, owner, repo, issueNumber);
|
|
72
68
|
const feature = featureIssue
|
|
73
69
|
? { number: featureIssue.number, nodeId: featureIssue.node_id, title: featureIssue.title }
|
|
74
70
|
: null;
|
|
75
71
|
|
|
72
|
+
// Spikes (tipos manuais) nunca são movidos automaticamente — pulados aqui.
|
|
73
|
+
const isManual = (sub) => isManualStageType(detectIssueType({ title: sub.title, labels: sub.labels }));
|
|
74
|
+
|
|
76
75
|
if (type === 'Feature') {
|
|
77
76
|
// Referência direta à Feature: toda a subárvore (Stories + Tasks).
|
|
78
|
-
const
|
|
79
|
-
for (const st of
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
const subs = await listSubIssues(token, issue.node_id).catch(() => []);
|
|
78
|
+
for (const st of subs) {
|
|
79
|
+
if (isManual(st)) continue;
|
|
80
|
+
addStory(st.number, st.nodeId, st.title);
|
|
81
|
+
const tks = await listSubIssues(token, st.nodeId).catch(() => []);
|
|
82
|
+
for (const t of tks) { if (isManual(t)) continue; addTask(t.number, t.nodeId, t.title); }
|
|
83
83
|
}
|
|
84
84
|
} else if (type === 'Story') {
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
for (const t of
|
|
85
|
+
addStory(issue.number, issue.node_id, issue.title);
|
|
86
|
+
const tks = await listSubIssues(token, issue.node_id).catch(() => []);
|
|
87
|
+
for (const t of tks) { if (isManual(t)) continue; addTask(t.number, t.nodeId, t.title); }
|
|
88
88
|
} else { // Task → inclui a Story pai e as Tasks irmãs.
|
|
89
|
-
|
|
89
|
+
addTask(issue.number, issue.node_id, issue.title);
|
|
90
90
|
const parent = await getIssueParent(token, issue.node_id).catch(() => null);
|
|
91
91
|
if (parent && detectIssueType({ title: parent.title }) === 'Story') {
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
for (const t of
|
|
92
|
+
addStory(parent.number, parent.nodeId, parent.title);
|
|
93
|
+
const tks = await listSubIssues(token, parent.nodeId).catch(() => []);
|
|
94
|
+
for (const t of tks) { if (isManual(t)) continue; addTask(t.number, t.nodeId, t.title); }
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
return { feature,
|
|
97
|
+
return { feature, stories, tasks };
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// A Feature só avança quando TODAS as suas Stories já estiverem em Code Review
|
|
@@ -114,31 +114,6 @@ async function allStoriesReadyForReview(readToken, projToken, project, etapaFiel
|
|
|
114
114
|
return true;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
// Avança um item do board para a Etapa "👀 Code Review" e reinicia o Status
|
|
118
|
-
// (nativo) para "Todo". Uma issue só AVANÇA: se já estiver em Code Review ou em
|
|
119
|
-
// uma etapa posterior, não é tocada (retorna false). Retorna true se avançou.
|
|
120
|
-
async function setCodeReview(token, project, etapaField, statusField, nodeId) {
|
|
121
|
-
const itemId = await addProjectItem(token, project.id, nodeId);
|
|
122
|
-
|
|
123
|
-
if (etapaField?.id && CODE_REVIEW_STAGE) {
|
|
124
|
-
// Nunca retroceder: compara a etapa atual com a de destino na ordem canônica.
|
|
125
|
-
const current = await getItemSingleSelectValue(token, itemId, etapaField.id).catch(() => null);
|
|
126
|
-
const curIdx = current ? STAGE_ORDER.indexOf(current) : -1;
|
|
127
|
-
const tgtIdx = STAGE_ORDER.indexOf(CODE_REVIEW_STAGE);
|
|
128
|
-
if (curIdx !== -1 && tgtIdx !== -1 && curIdx >= tgtIdx) {
|
|
129
|
-
return false; // já está em Code Review ou adiante — não retrocede
|
|
130
|
-
}
|
|
131
|
-
const optionId = etapaField.options?.[CODE_REVIEW_STAGE];
|
|
132
|
-
if (optionId) await setItemSingleSelect(token, project.id, itemId, etapaField.id, optionId);
|
|
133
|
-
}
|
|
134
|
-
// Ao avançar de etapa, o Status reinicia em "Todo".
|
|
135
|
-
if (statusField?.id) {
|
|
136
|
-
const optionId = statusField.options?.[TODO_STATUS];
|
|
137
|
-
if (optionId) await setItemSingleSelect(token, project.id, itemId, statusField.id, optionId);
|
|
138
|
-
}
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
117
|
export async function codeReview({ prNumber }) {
|
|
143
118
|
const token = await resolveToken();
|
|
144
119
|
const projectToken = process.env.PROJECT_TOKEN || token;
|
|
@@ -165,20 +140,9 @@ export async function codeReview({ prNumber }) {
|
|
|
165
140
|
}
|
|
166
141
|
|
|
167
142
|
// Carrega projeto do .spec-wave.json
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
170
|
-
console.warn(`${
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
let project;
|
|
174
|
-
try {
|
|
175
|
-
project = JSON.parse(readFileSync(configPath, 'utf-8')).project || {};
|
|
176
|
-
} catch (err) {
|
|
177
|
-
console.warn(`${CONFIG_FILE} corrompido (${err.message}) — board não atualizado.`);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (!project.id) {
|
|
181
|
-
console.warn(`Project não configurado em ${CONFIG_FILE} — board não atualizado.`);
|
|
143
|
+
const { project, error: projectError } = loadProjectConfig();
|
|
144
|
+
if (projectError) {
|
|
145
|
+
console.warn(`${projectError} — board não atualizado.`);
|
|
182
146
|
return;
|
|
183
147
|
}
|
|
184
148
|
|
|
@@ -190,18 +154,37 @@ export async function codeReview({ prNumber }) {
|
|
|
190
154
|
const updated = [];
|
|
191
155
|
const featuresChecked = new Set();
|
|
192
156
|
|
|
193
|
-
//
|
|
194
|
-
//
|
|
157
|
+
// Regras ao abrir o PR: Tasks → 🎉 Done (implementação concluída);
|
|
158
|
+
// Stories → 👀 Code Review; Feature → Code Review só quando TODAS as suas
|
|
159
|
+
// Stories já estiverem em Code Review.
|
|
195
160
|
for (const num of issueNums) {
|
|
196
|
-
const { feature,
|
|
161
|
+
const { feature, stories, tasks } = await collectReviewUnit(token, owner, repo, num);
|
|
162
|
+
|
|
163
|
+
// Tasks → Done (Status Done).
|
|
164
|
+
for (const [n, info] of tasks) {
|
|
165
|
+
if (seen.has(n)) continue;
|
|
166
|
+
seen.add(n);
|
|
167
|
+
try {
|
|
168
|
+
const moved = await advanceToStage(projectToken, project, etapaField, statusField, info.nodeId, DONE_STAGE, DONE_STATUS);
|
|
169
|
+
if (moved) {
|
|
170
|
+
updated.push(`#${n} ${info.title} → ${DONE_STAGE}`);
|
|
171
|
+
console.log(`#${n} → "${DONE_STAGE}" / Status "${DONE_STATUS}".`);
|
|
172
|
+
} else {
|
|
173
|
+
console.log(`#${n} já está em "${DONE_STAGE}" ou etapa posterior — mantido (não retrocede).`);
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.warn(`Falha ao atualizar #${n}: ${err.message}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
197
179
|
|
|
198
|
-
|
|
180
|
+
// Stories → Code Review (Status Todo).
|
|
181
|
+
for (const [n, info] of stories) {
|
|
199
182
|
if (seen.has(n)) continue;
|
|
200
183
|
seen.add(n);
|
|
201
184
|
try {
|
|
202
|
-
const moved = await
|
|
185
|
+
const moved = await advanceToStage(projectToken, project, etapaField, statusField, info.nodeId, CODE_REVIEW_STAGE, TODO_STATUS);
|
|
203
186
|
if (moved) {
|
|
204
|
-
updated.push(`#${n} ${info.title}`);
|
|
187
|
+
updated.push(`#${n} ${info.title} → ${CODE_REVIEW_STAGE}`);
|
|
205
188
|
console.log(`#${n} → "${CODE_REVIEW_STAGE}" / Status "${TODO_STATUS}".`);
|
|
206
189
|
} else {
|
|
207
190
|
console.log(`#${n} já está em "${CODE_REVIEW_STAGE}" ou etapa posterior — mantido (não retrocede).`);
|
|
@@ -220,9 +203,9 @@ export async function codeReview({ prNumber }) {
|
|
|
220
203
|
console.log(`Feature #${feature.number} mantida em desenvolvimento — ainda há Stories pendentes (fora de "${CODE_REVIEW_STAGE}").`);
|
|
221
204
|
} else if (!seen.has(feature.number)) {
|
|
222
205
|
seen.add(feature.number);
|
|
223
|
-
const moved = await
|
|
206
|
+
const moved = await advanceToStage(projectToken, project, etapaField, statusField, feature.nodeId, CODE_REVIEW_STAGE, TODO_STATUS);
|
|
224
207
|
if (moved) {
|
|
225
|
-
updated.push(`#${feature.number} ${feature.title} (Feature)`);
|
|
208
|
+
updated.push(`#${feature.number} ${feature.title} (Feature) → ${CODE_REVIEW_STAGE}`);
|
|
226
209
|
console.log(`Feature #${feature.number} → "${CODE_REVIEW_STAGE}" (todas as Stories concluídas).`);
|
|
227
210
|
} else {
|
|
228
211
|
console.log(`Feature #${feature.number} já está em "${CODE_REVIEW_STAGE}" ou etapa posterior.`);
|
|
@@ -238,10 +221,10 @@ export async function codeReview({ prNumber }) {
|
|
|
238
221
|
await commentOnIssue(
|
|
239
222
|
token, owner, repo, parseInt(prNumber, 10),
|
|
240
223
|
`🔍 **Code Review iniciado**\n\n` +
|
|
241
|
-
`
|
|
224
|
+
`Board atualizado (Tasks → **${DONE_STAGE}**; Story → **${CODE_REVIEW_STAGE}**; Feature só avança quando todas as Stories concluírem):\n\n` +
|
|
242
225
|
updated.map(f => `- ${f}`).join('\n')
|
|
243
226
|
).catch(() => {});
|
|
244
227
|
}
|
|
245
228
|
|
|
246
|
-
console.log(`code-review: ${updated.length} item(ns)
|
|
229
|
+
console.log(`code-review: ${updated.length} item(ns) atualizado(s) no board.`);
|
|
247
230
|
}
|