@spec-wave/cli 0.26.0 → 0.28.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/api/github-rest.mjs +52 -0
- package/src/cli.mjs +12 -0
- package/src/commands/decompose.mjs +166 -39
- package/src/commands/doctor.mjs +214 -3
- package/src/commands/generate-bug.mjs +22 -16
- package/src/commands/generate-plan.mjs +72 -23
- package/src/commands/generate-spec.mjs +19 -15
- package/src/commands/implement.mjs +47 -22
- package/src/commands/install-skill.mjs +18 -8
- package/src/commands/preflight.mjs +322 -0
- package/src/commands/run.mjs +51 -30
- package/src/commands/update.mjs +143 -12
- package/src/commands/validate.mjs +84 -17
- package/src/config.mjs +18 -0
- package/src/lib/artifact-pr.mjs +272 -0
- package/src/lib/artifact-publish.mjs +169 -0
- package/src/lib/doc-availability.mjs +23 -1
- package/src/lib/doc-source.mjs +162 -0
- package/src/lib/flow-run.mjs +9 -218
- package/src/lib/next-step.mjs +27 -4
- package/src/lib/pr-branch.mjs +106 -7
- package/src/lib/repo-links.mjs +8 -2
- package/src/plugin/.claude-plugin/plugin.json +1 -1
- package/src/plugin/README.md +5 -0
- package/src/plugin/skills/bug/SKILL.md +2 -2
- package/src/plugin/skills/decompose/SKILL.md +4 -4
- package/src/plugin/skills/plan/SKILL.md +1 -1
- package/src/plugin/skills/preparar-feature/SKILL.md +245 -0
- package/src/plugin/skills/preparar-feature/reference/critica.md +88 -0
- package/src/plugin/skills/preparar-specs/SKILL.md +171 -0
- package/src/plugin/skills/preparar-specs/reference/armadilhas.md +209 -0
- package/src/plugin/skills/preparar-specs/reference/revisao.md +107 -0
- package/src/plugin/skills/run/SKILL.md +3 -1
- package/src/plugin/skills/spec/SKILL.md +4 -4
- package/src/plugin/skills/update/SKILL.md +10 -4
- package/src/plugin/skills/workflow/SKILL.md +8 -3
- package/src/templates/skill/SKILL.md +13 -10
- package/src/templates/workflows/code-review.yml +13 -2
- package/src/templates/workflows/critique.yml +1 -1
- package/src/templates/workflows/decompose.yml +13 -2
- package/src/templates/workflows/generate-bug.yml +17 -6
- package/src/templates/workflows/generate-plan.yml +20 -7
- package/src/templates/workflows/generate-spec.yml +20 -7
- package/src/templates/workflows/qa.yml +13 -0
package/src/commands/doctor.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import * as p from '@clack/prompts';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import { Octokit } from '@octokit/rest';
|
|
11
|
+
import yaml from 'js-yaml';
|
|
11
12
|
import {
|
|
12
13
|
resolveToken, verifyTokenScopes, describeTokenSource, activeGhAccount,
|
|
13
14
|
tokenMismatchWarning, parseActiveAccount,
|
|
@@ -15,7 +16,8 @@ import {
|
|
|
15
16
|
import { getProjectSnapshot, listSubIssues } from '../api/github-graphql.mjs';
|
|
16
17
|
import { getRepoVariable } from '../api/github-rest.mjs';
|
|
17
18
|
import {
|
|
18
|
-
CONFIG_FILE, WORKFLOW_FILES, getProvider, DEFAULT_PROVIDER,
|
|
19
|
+
CONFIG_FILE, WORKFLOW_FILES, ARTIFACT_WORKFLOW_FILES, getProvider, DEFAULT_PROVIDER,
|
|
20
|
+
AI_PROVIDERS, STATUS_OPTIONS,
|
|
19
21
|
RETIRED_STAGES, ALL_LABELS, allLabelsFor, LABEL_NEEDS_HUMAN, MODEL_LABEL_PREFIX,
|
|
20
22
|
DEFAULT_MAX_CRITIQUE_ATTEMPTS, STAGE_TRACKS, AI_ACTIONS, recommendedModelAliases,
|
|
21
23
|
modelLabels,
|
|
@@ -903,10 +905,21 @@ async function checkDecompositions(ctx) {
|
|
|
903
905
|
);
|
|
904
906
|
}
|
|
905
907
|
|
|
908
|
+
// Esta checagem lê o DISCO de propósito: ela audita registro consolidado, e um
|
|
909
|
+
// rascunho ainda em Pull Request não é registro. A consequência a declarar é
|
|
910
|
+
// que rascunhos não mergeados ficam FORA da conferência de deriva — não é
|
|
911
|
+
// omissão, é o recorte; mas quem lê o relatório precisa saber.
|
|
912
|
+
const nota = 'Rascunhos ainda em Pull Request não entram nesta conferência — ' +
|
|
913
|
+
'só o que está na branch base é registro.';
|
|
914
|
+
|
|
906
915
|
if (aplicados === 0) {
|
|
907
|
-
return {
|
|
916
|
+
return {
|
|
917
|
+
name,
|
|
918
|
+
status: 'ok',
|
|
919
|
+
detail: `${arquivos.length} rascunho(s) ainda não aplicado(s) — nada a conferir. ${nota}`,
|
|
920
|
+
};
|
|
908
921
|
}
|
|
909
|
-
return { name, status, detail: notes.join('\n') };
|
|
922
|
+
return { name, status, detail: [...notes, nota].join('\n') };
|
|
910
923
|
}
|
|
911
924
|
|
|
912
925
|
export function checkSpecKit(ctx) {
|
|
@@ -967,6 +980,203 @@ async function checkExecutionMode(ctx) {
|
|
|
967
980
|
return { name, status, detail: [estado.summary, ...estado.notes, ...estado.fixes].join('\n') };
|
|
968
981
|
}
|
|
969
982
|
|
|
983
|
+
/**
|
|
984
|
+
* Veredito sobre a capacidade de publicar documentos por Pull Request (função PURA).
|
|
985
|
+
*
|
|
986
|
+
* Três coisas precisam ser verdade, e as três falham de formas diferentes:
|
|
987
|
+
*
|
|
988
|
+
* • **Permissões no YAML.** Sem `pull-requests: write`, o commit é criado e o PR
|
|
989
|
+
* não — o documento fica numa branch que ninguém vê. Sem `contents: write`, a
|
|
990
|
+
* Git Data API responde 403 e nada é publicado.
|
|
991
|
+
* • **O Actions pode abrir PR.** "Allow GitHub Actions to create and approve
|
|
992
|
+
* pull requests" vem DESLIGADO em muitas organizações, e é a falha mais
|
|
993
|
+
* provável deste fluxo. Warn e não fail: a consulta exige `administration:
|
|
994
|
+
* read`, que o GITHUB_TOKEN não tem, então "não consegui ver" é comum e não
|
|
995
|
+
* pode virar vermelho.
|
|
996
|
+
* • **Status checks obrigatórios na branch default.** Este é FAIL, e o motivo é
|
|
997
|
+
* contraintuitivo: PR aberto pelo GITHUB_TOKEN não dispara workflow nenhum,
|
|
998
|
+
* logo o check obrigatório nunca sai de "expected" e NENHUM PR de documento
|
|
999
|
+
* fica mergeável. O fluxo trava no primeiro passo, sem erro visível em lugar
|
|
1000
|
+
* nenhum — a issue simplesmente para.
|
|
1001
|
+
*
|
|
1002
|
+
* @param {object} params
|
|
1003
|
+
* @param {boolean|null} [params.canCreatePr] null = não foi possível consultar
|
|
1004
|
+
* @param {Record<string, {contents?: string, pullRequests?: string}>} [params.workflowPerms]
|
|
1005
|
+
* @param {string[]|null} [params.requiredChecks] null = não foi possível consultar
|
|
1006
|
+
* @param {boolean|null} [params.prTokenPresent] o secret alternativo existe no repo
|
|
1007
|
+
* @param {string} [params.prTokenSecret] nome do secret alternativo
|
|
1008
|
+
* @returns {{status: 'ok'|'warn'|'fail', notes: string[]}}
|
|
1009
|
+
*/
|
|
1010
|
+
export function inspectPrPublishing({
|
|
1011
|
+
canCreatePr = null, workflowPerms = {}, requiredChecks = null,
|
|
1012
|
+
prTokenPresent = null, prTokenSecret = 'GH_PR_TOKEN',
|
|
1013
|
+
} = {}) {
|
|
1014
|
+
const notes = [];
|
|
1015
|
+
let status = 'ok';
|
|
1016
|
+
const piora = (novo) => {
|
|
1017
|
+
if (novo === 'fail' || status === 'fail') status = 'fail';
|
|
1018
|
+
else if (novo === 'warn') status = 'warn';
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
const semPr = Object.entries(workflowPerms)
|
|
1022
|
+
.filter(([, perm]) => perm?.pullRequests !== 'write').map(([f]) => f);
|
|
1023
|
+
const semContents = Object.entries(workflowPerms)
|
|
1024
|
+
.filter(([, perm]) => perm?.contents !== 'write').map(([f]) => f);
|
|
1025
|
+
|
|
1026
|
+
if (semPr.length > 0) {
|
|
1027
|
+
piora('fail');
|
|
1028
|
+
notes.push(
|
|
1029
|
+
`Sem \`pull-requests: write\`: ${semPr.join(', ')}. O commit é criado e o Pull ` +
|
|
1030
|
+
'Request não — o documento fica numa branch que ninguém vê. Rode `update`.'
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
if (semContents.length > 0) {
|
|
1034
|
+
piora('fail');
|
|
1035
|
+
notes.push(
|
|
1036
|
+
`Sem \`contents: write\`: ${semContents.join(', ')}. O commit vai por Git Data API, ` +
|
|
1037
|
+
'que é autorizada por essa permissão. Rode `update`.'
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (canCreatePr === false && !prTokenPresent) {
|
|
1042
|
+
piora('warn');
|
|
1043
|
+
notes.push(
|
|
1044
|
+
'O GitHub Actions está proibido de criar Pull Requests neste repositório. ' +
|
|
1045
|
+
'Ligue Settings → Actions → General → "Allow GitHub Actions to create and approve ' +
|
|
1046
|
+
`pull requests", ou defina o secret \`${prTokenSecret}\` com um PAT.`
|
|
1047
|
+
);
|
|
1048
|
+
} else if (canCreatePr === false) {
|
|
1049
|
+
// Mitigado: o PR é aberto com o PAT, não com o GITHUB_TOKEN. Continuar
|
|
1050
|
+
// pedindo a configuração que a pessoa ACABOU de fazer é o jeito mais rápido
|
|
1051
|
+
// de ensinar que o doctor pode ser ignorado.
|
|
1052
|
+
notes.push(
|
|
1053
|
+
`O Actions não pode abrir PR com o GITHUB_TOKEN, mas \`${prTokenSecret}\` está ` +
|
|
1054
|
+
'definido e é ele que será usado — só o NOME é verificável daqui, não o valor ' +
|
|
1055
|
+
'nem as permissões.'
|
|
1056
|
+
);
|
|
1057
|
+
} else if (canCreatePr === null) {
|
|
1058
|
+
notes.push(
|
|
1059
|
+
'Não foi possível verificar se o Actions pode criar Pull Requests (a consulta ' +
|
|
1060
|
+
'exige `administration: read`). Se a publicação falhar com 403, é aqui.'
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (requiredChecks?.length && !prTokenPresent) {
|
|
1065
|
+
piora('fail');
|
|
1066
|
+
notes.push(
|
|
1067
|
+
`A branch default exige status check(s) obrigatório(s): ${requiredChecks.join(', ')}. ` +
|
|
1068
|
+
'Pull Request aberto pelo GITHUB_TOKEN NÃO dispara workflow, então esses checks ' +
|
|
1069
|
+
'nunca ficam verdes e nenhum PR de documento se torna mergeável — o fluxo trava no ' +
|
|
1070
|
+
`primeiro passo. Use um PAT no secret \`${prTokenSecret}\` (PR aberto por PAT dispara ` +
|
|
1071
|
+
'os workflows), ou dispense os checks para as branches `spec-wave/*`.'
|
|
1072
|
+
);
|
|
1073
|
+
} else if (requiredChecks?.length) {
|
|
1074
|
+
notes.push(
|
|
1075
|
+
`A branch default exige ${requiredChecks.join(', ')}; como o PR é aberto por ` +
|
|
1076
|
+
`\`${prTokenSecret}\`, os workflows disparam e os checks podem ficar verdes.`
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if (status === 'ok' && notes.length === 0) {
|
|
1081
|
+
notes.push('Os workflows podem publicar documentos por Pull Request.');
|
|
1082
|
+
}
|
|
1083
|
+
return { status, notes };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Permissões de PR declaradas nos workflows instalados (I/O — nunca lança).
|
|
1088
|
+
*
|
|
1089
|
+
* O job MAIS RESTRITIVO manda: basta um sem a permissão para o fluxo quebrar.
|
|
1090
|
+
*
|
|
1091
|
+
* @param {string} dir diretório `.github/workflows`
|
|
1092
|
+
* @returns {Record<string, {contents?: string, pullRequests?: string}>}
|
|
1093
|
+
*/
|
|
1094
|
+
export function readWorkflowPrPermissions(dir) {
|
|
1095
|
+
const workflowPerms = {};
|
|
1096
|
+
for (const file of ARTIFACT_WORKFLOW_FILES) {
|
|
1097
|
+
const caminho = path.join(dir, file);
|
|
1098
|
+
if (!existsSync(caminho)) continue; // ausência já é reportada por checkWorkflows
|
|
1099
|
+
let wf;
|
|
1100
|
+
try {
|
|
1101
|
+
wf = yaml.load(readFileSync(caminho, 'utf-8'));
|
|
1102
|
+
} catch {
|
|
1103
|
+
continue; // YAML ilegível é problema de outro check
|
|
1104
|
+
}
|
|
1105
|
+
for (const job of Object.values(wf?.jobs || {})) {
|
|
1106
|
+
const atual = workflowPerms[file];
|
|
1107
|
+
const perm = {
|
|
1108
|
+
contents: job?.permissions?.contents,
|
|
1109
|
+
pullRequests: job?.permissions?.['pull-requests'],
|
|
1110
|
+
};
|
|
1111
|
+
if (!atual || perm.pullRequests !== 'write' || perm.contents !== 'write') {
|
|
1112
|
+
workflowPerms[file] = perm;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return workflowPerms;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Coleta o contexto que `inspectPrPublishing` julga (I/O — nunca lança).
|
|
1121
|
+
*
|
|
1122
|
+
* Separado do check para o `preflight` poder fazer a MESMA pergunta sem
|
|
1123
|
+
* reimplementá-la: as três consultas têm cada uma o seu motivo para falhar sem
|
|
1124
|
+
* que isso signifique "está errado", e duplicar esse tratamento em dois comandos
|
|
1125
|
+
* é como as duas respostas passam a divergir.
|
|
1126
|
+
*
|
|
1127
|
+
* @param {{token?: string, owner?: string, repo?: string, root?: string}} params
|
|
1128
|
+
* @returns {Promise<{canCreatePr: boolean|null, workflowPerms: object,
|
|
1129
|
+
* requiredChecks: string[]|null, prTokenPresent: boolean|null}>}
|
|
1130
|
+
*/
|
|
1131
|
+
export async function readPrPublishingContext({ token, owner, repo, root }) {
|
|
1132
|
+
const workflowPerms = readWorkflowPrPermissions(path.join(root || process.cwd(), '.github', 'workflows'));
|
|
1133
|
+
|
|
1134
|
+
let canCreatePr = null;
|
|
1135
|
+
let requiredChecks = null;
|
|
1136
|
+
let prTokenPresent = null;
|
|
1137
|
+
if (token && owner && repo) {
|
|
1138
|
+
const octokit = makeOctokit(token);
|
|
1139
|
+
try {
|
|
1140
|
+
const res = await octokit.request('GET /repos/{owner}/{repo}/actions/secrets',
|
|
1141
|
+
{ owner, repo });
|
|
1142
|
+
prTokenPresent = (res.data.secrets || []).some(sec => sec.name === 'GH_PR_TOKEN');
|
|
1143
|
+
} catch {
|
|
1144
|
+
prTokenPresent = null; // sem permissão de ler secrets — não afirmar ausência
|
|
1145
|
+
}
|
|
1146
|
+
try {
|
|
1147
|
+
const res = await octokit.request('GET /repos/{owner}/{repo}/actions/permissions/workflow',
|
|
1148
|
+
{ owner, repo });
|
|
1149
|
+
canCreatePr = res.data?.can_approve_pull_request_reviews ?? null;
|
|
1150
|
+
} catch {
|
|
1151
|
+
canCreatePr = null; // exige administration:read — ausência é comum, não é erro
|
|
1152
|
+
}
|
|
1153
|
+
try {
|
|
1154
|
+
const info = await octokit.rest.repos.get({ owner, repo });
|
|
1155
|
+
const branch = info.data.default_branch;
|
|
1156
|
+
const prot = await octokit.rest.repos.getBranchProtection({ owner, repo, branch });
|
|
1157
|
+
requiredChecks = prot.data?.required_status_checks?.contexts || [];
|
|
1158
|
+
} catch {
|
|
1159
|
+
requiredChecks = null; // sem proteção (404) ou sem permissão — nos dois casos, não afirmar
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return { canCreatePr, workflowPerms, requiredChecks, prTokenPresent };
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
async function checkPrPublishing(ctx) {
|
|
1166
|
+
const name = 'Publicação por Pull Request';
|
|
1167
|
+
const cfg = ctx.cfg || {};
|
|
1168
|
+
const contexto = await readPrPublishingContext({
|
|
1169
|
+
token: ctx.token, owner: cfg.owner, repo: cfg.repo, root: ctx.root || ctx.cwd,
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
if (Object.keys(contexto.workflowPerms).length === 0) {
|
|
1173
|
+
return { name, status: 'warn', detail: 'Workflows não encontrados — rode `init` ou `update`.' };
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const { status, notes } = inspectPrPublishing(contexto);
|
|
1177
|
+
return { name, status, detail: notes.join('\n ') };
|
|
1178
|
+
}
|
|
1179
|
+
|
|
970
1180
|
async function checkWorkflows(ctx) {
|
|
971
1181
|
const name = 'Workflows do Actions';
|
|
972
1182
|
// Ancorado na raiz do projeto, não no cwd: rodar o doctor de um subdiretório
|
|
@@ -1068,6 +1278,7 @@ export async function doctor() {
|
|
|
1068
1278
|
checkDecompositions,
|
|
1069
1279
|
checkSpecKit,
|
|
1070
1280
|
checkWorkflows,
|
|
1281
|
+
checkPrPublishing,
|
|
1071
1282
|
checkExecutionMode,
|
|
1072
1283
|
];
|
|
1073
1284
|
const results = [];
|
|
@@ -18,8 +18,9 @@ import { recordUsage } from '../lib/usage-report.mjs';
|
|
|
18
18
|
import { loadPrompt, systemPromptWithTools } from '../lib/prompt-loader.mjs';
|
|
19
19
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
20
20
|
import { bugDocPaths } from '../lib/bug-doc.mjs';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
21
|
+
import { resolveFlowContext } from '../lib/flow-run.mjs';
|
|
22
|
+
import { publishArtifact } from '../lib/artifact-publish.mjs';
|
|
23
|
+
import { renderPrLine } from '../lib/artifact-pr.mjs';
|
|
23
24
|
import {
|
|
24
25
|
runCritique, resolveCritiqueAttempt, renderNeedsHumanComment,
|
|
25
26
|
} from '../lib/critique.mjs';
|
|
@@ -77,7 +78,7 @@ export async function generateBug({ issueNumber }) {
|
|
|
77
78
|
return;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
const {
|
|
81
|
+
const { fileRel } = bugDocPaths(issue.title, root);
|
|
81
82
|
|
|
82
83
|
const comments = await listIssueComments(token, owner, repo, n).catch(() => []);
|
|
83
84
|
const report = buildReport(issue, comments);
|
|
@@ -111,16 +112,19 @@ export async function generateBug({ issueNumber }) {
|
|
|
111
112
|
// ver lib/unwrap-doc.mjs. O que vai para o repositório é o documento.
|
|
112
113
|
const content = unwrapGeneratedDoc(bruto);
|
|
113
114
|
|
|
114
|
-
// Publica pelo mesmo caminho do generate-spec/plan/decompose:
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
// Publica pelo mesmo caminho do generate-spec/plan/decompose: branch própria
|
|
116
|
+
// do documento, commit único e Pull Request. O bloco de git solto que
|
|
117
|
+
// existia aqui commitava direto na branch default sem sequer escopar o
|
|
118
|
+
// commit ao arquivo — varria para dentro qualquer coisa no index.
|
|
119
|
+
const published = await publishArtifact({
|
|
120
|
+
token, owner, repo,
|
|
121
|
+
doc: 'bug',
|
|
122
|
+
issueNumber: n,
|
|
123
|
+
issueTitle: issue.title,
|
|
124
|
+
issueUrl: issue.html_url,
|
|
125
|
+
pathRel: fileRel,
|
|
121
126
|
content,
|
|
122
|
-
|
|
123
|
-
mode,
|
|
127
|
+
nextLabel: 'spec-wave:ready',
|
|
124
128
|
});
|
|
125
129
|
if (published.warning) console.warn(`⚠️ ${published.warning}`);
|
|
126
130
|
|
|
@@ -136,14 +140,16 @@ export async function generateBug({ issueNumber }) {
|
|
|
136
140
|
await commentOnIssue(
|
|
137
141
|
token, owner, repo, n,
|
|
138
142
|
'🐞 **bug.md gerado automaticamente!**\n\n' +
|
|
139
|
-
`📄 Arquivo: [\`${fileRel}\`](${
|
|
140
|
-
|
|
141
|
-
'a
|
|
143
|
+
`📄 Arquivo: [\`${fileRel}\`](${published.blobUrl})\n\n` +
|
|
144
|
+
`${renderPrLine(published)}\n\n` +
|
|
145
|
+
'Revise a **causa raiz** e o **teste de regressão** no Pull Request — são as duas seções ' +
|
|
146
|
+
'que decidem se a correção ataca o defeito ou o sintoma. Depois do merge, valide com:\n' +
|
|
142
147
|
`\`\`\`\ngh issue edit ${n} --add-label "spec-wave:ready"\n\`\`\`` +
|
|
143
148
|
(critique?.blocked ? '\n\n⛔ A crítica adversarial encontrou problemas graves (acima).' : '')
|
|
144
149
|
);
|
|
145
150
|
|
|
146
|
-
console.log(`bug.md
|
|
151
|
+
console.log(`bug.md publicado em ${published.branch} (${fileRel}).`);
|
|
152
|
+
return { pr: published.pr, branch: published.branch };
|
|
147
153
|
} catch (err) {
|
|
148
154
|
// Mesmo motivo do generate-spec: sem remover a label, re-aplicá-la não
|
|
149
155
|
// emite evento e a issue vira beco sem saída.
|
|
@@ -2,13 +2,13 @@ import path from 'node:path';
|
|
|
2
2
|
import { readFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { resolveToken } from '../api/auth.mjs';
|
|
4
4
|
import {
|
|
5
|
-
getIssue, removeLabel, addLabel, commentOnIssue, listIssueComments,
|
|
5
|
+
getIssue, removeLabel, addLabel, commentOnIssue, listIssueComments, getRepoDefaultBranch,
|
|
6
6
|
} from '../api/github-rest.mjs';
|
|
7
7
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
8
8
|
import {
|
|
9
9
|
allowsSpecPlan, SPEC_PLAN_EXCLUDED_TYPES, TARGET_LANGUAGE,
|
|
10
10
|
LABEL_CRITIQUE, LABEL_CRITIQUE_FAILED, LABEL_NEEDS_HUMAN, LABEL_RISK_ACCEPTED,
|
|
11
|
-
DEFAULT_MAX_CRITIQUE_ATTEMPTS, labelNames,
|
|
11
|
+
LABEL_SPEC, DEFAULT_MAX_CRITIQUE_ATTEMPTS, labelNames,
|
|
12
12
|
} from '../config.mjs';
|
|
13
13
|
import { generateDocument } from '../lib/claude.mjs';
|
|
14
14
|
import { unwrapGeneratedDoc } from '../lib/unwrap-doc.mjs';
|
|
@@ -18,9 +18,11 @@ import {
|
|
|
18
18
|
} from '../lib/critique.mjs';
|
|
19
19
|
import { recordUsage } from '../lib/usage-report.mjs';
|
|
20
20
|
import { slugify } from '../lib/slugify.mjs';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
21
|
+
import { resolveFlowContext } from '../lib/flow-run.mjs';
|
|
22
|
+
import { publishArtifact } from '../lib/artifact-publish.mjs';
|
|
23
|
+
import { loadArtifact } from '../lib/doc-source.mjs';
|
|
24
|
+
import { awaitingMergeBlock, renderPrLine } from '../lib/artifact-pr.mjs';
|
|
25
|
+
import { isAwaitingMerge } from '../lib/doc-source.mjs';
|
|
24
26
|
import { buildTechContext } from '../lib/tech-context.mjs';
|
|
25
27
|
import { loadPrompt, systemPromptWithTools } from '../lib/prompt-loader.mjs';
|
|
26
28
|
|
|
@@ -155,13 +157,39 @@ export async function generatePlan({ issueNumber }) {
|
|
|
155
157
|
// Caminho RELATIVO para links/commit; ABSOLUTO ancorado na raiz para o fs — o
|
|
156
158
|
// config é procurado subindo na árvore, e os documentos moram junto dele.
|
|
157
159
|
const featureRel = `docs/features/${slug}`;
|
|
158
|
-
const featureDir = resolveFromRoot(root, featureRel);
|
|
159
|
-
const filePath = path.join(featureDir, 'plan.md');
|
|
160
160
|
const fileRel = `${featureRel}/plan.md`;
|
|
161
161
|
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
const
|
|
162
|
+
// A spec é a ENTRADA do plano, e agora ela pode estar em três lugares: no
|
|
163
|
+
// clone, na branch base, ou num Pull Request que ninguém mergeou ainda.
|
|
164
|
+
const specRel = `${featureRel}/spec.md`;
|
|
165
|
+
const base = await getRepoDefaultBranch(token, owner, repo).catch(() => null);
|
|
166
|
+
const spec = await loadArtifact({
|
|
167
|
+
token, owner, repo, root, pathRel: specRel, doc: 'spec',
|
|
168
|
+
issueNumber: parseInt(issueNumber, 10), base,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Portão duro. Este comando TOLERAVA a spec ausente e seguia com um
|
|
172
|
+
// placeholder no payload — o que, com a publicação por Pull Request, viraria o
|
|
173
|
+
// modo de falha caro: PR da spec aberto, plano gerado sobre o vazio, nenhum
|
|
174
|
+
// erro. Degradar em silêncio custa uma chamada de IA e produz um documento que
|
|
175
|
+
// parece bom. Recusar custa uma label.
|
|
176
|
+
if (isAwaitingMerge(spec.state) || spec.content == null) {
|
|
177
|
+
const n = parseInt(issueNumber, 10);
|
|
178
|
+
const motivo = isAwaitingMerge(spec.state)
|
|
179
|
+
? awaitingMergeBlock({ pathRel: specRel, state: spec.state, pr: spec.pr, branch: spec.ref })
|
|
180
|
+
: {
|
|
181
|
+
message: `\`${specRel}\` não existe — o plano depende da spec.`,
|
|
182
|
+
unblock: `Aplique \`${LABEL_SPEC}\` para gerar a spec primeiro.`,
|
|
183
|
+
};
|
|
184
|
+
await removeLabel(token, owner, repo, n, 'spec-wave:plan').catch(() => {});
|
|
185
|
+
await commentOnIssue(token, owner, repo, n,
|
|
186
|
+
`⏸️ **plan.md não gerado:** ${motivo.message}\n\n${motivo.unblock}\n\n` +
|
|
187
|
+
'Gerar o plano sem a spec produziria um documento plausível e errado, ' +
|
|
188
|
+
'com a chamada de IA já paga.'
|
|
189
|
+
).catch(() => {});
|
|
190
|
+
throw new Error(motivo.message);
|
|
191
|
+
}
|
|
192
|
+
const specContent = spec.content;
|
|
165
193
|
|
|
166
194
|
// Tech context (RFC-002 §4): estático + dinâmico + override do corpo da issue.
|
|
167
195
|
const tech = buildTechContext({ issueBody: issue.body || '' });
|
|
@@ -199,11 +227,16 @@ export async function generatePlan({ issueNumber }) {
|
|
|
199
227
|
// ver lib/unwrap-doc.mjs. O que vai para o repositório é o documento.
|
|
200
228
|
const content = unwrapGeneratedDoc(bruto);
|
|
201
229
|
|
|
202
|
-
const published =
|
|
203
|
-
|
|
230
|
+
const published = await publishArtifact({
|
|
231
|
+
token, owner, repo,
|
|
232
|
+
doc: 'plan',
|
|
233
|
+
issueNumber: parseInt(issueNumber, 10),
|
|
234
|
+
issueTitle: issue.title,
|
|
235
|
+
issueUrl: issue.html_url,
|
|
236
|
+
pathRel: fileRel,
|
|
204
237
|
content,
|
|
205
|
-
|
|
206
|
-
|
|
238
|
+
base,
|
|
239
|
+
nextLabel: 'spec-wave:ready',
|
|
207
240
|
});
|
|
208
241
|
if (published.warning) console.warn(`⚠️ ${published.warning}`);
|
|
209
242
|
|
|
@@ -214,8 +247,9 @@ export async function generatePlan({ issueNumber }) {
|
|
|
214
247
|
await commentOnIssue(
|
|
215
248
|
token, owner, repo, parseInt(issueNumber, 10),
|
|
216
249
|
`📋 **plan.md gerado automaticamente!**\n\n` +
|
|
217
|
-
`📄 Arquivo: [\`${fileRel}\`](${
|
|
218
|
-
|
|
250
|
+
`📄 Arquivo: [\`${fileRel}\`](${published.blobUrl})\n\n` +
|
|
251
|
+
`${renderPrLine(published)}\n\n` +
|
|
252
|
+
`Revise o plano **no Pull Request** e faça o merge. Depois, valide a Feature: mova o card para **✅ Ready** ou use:\n` +
|
|
219
253
|
`\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:ready"\n\`\`\`` +
|
|
220
254
|
formatLintWarning(lintFindings)
|
|
221
255
|
);
|
|
@@ -229,7 +263,8 @@ export async function generatePlan({ issueNumber }) {
|
|
|
229
263
|
labels: issueLabels, config, usage: usageEntries,
|
|
230
264
|
});
|
|
231
265
|
|
|
232
|
-
console.log(`plan.md
|
|
266
|
+
console.log(`plan.md publicado em ${published.branch} (${fileRel}).`);
|
|
267
|
+
return { pr: published.pr, branch: published.branch };
|
|
233
268
|
} catch (err) {
|
|
234
269
|
// Mesmo beco sem saída do generate-spec: o gatilho é `issues: [labeled]`,
|
|
235
270
|
// então com a label ainda aplicada re-adicioná-la não dispara nada. Remove
|
|
@@ -361,9 +396,20 @@ export async function critique({ issueNumber, file, kind, failOnGrave = false })
|
|
|
361
396
|
return;
|
|
362
397
|
}
|
|
363
398
|
|
|
364
|
-
const
|
|
365
|
-
const
|
|
366
|
-
|
|
399
|
+
const featureRel = `docs/features/${slugify(issue.title)}`;
|
|
400
|
+
const planRel = `${featureRel}/plan.md`;
|
|
401
|
+
const specRel = `${featureRel}/spec.md`;
|
|
402
|
+
const base = await getRepoDefaultBranch(token, owner, repo).catch(() => null);
|
|
403
|
+
const ler = (doc, pathRel) => loadArtifact({
|
|
404
|
+
token, owner, repo, root, pathRel, doc, issueNumber: n, base,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Este é o caminho de RECUPERAÇÃO do `critique-failed`: o revisor corrige o
|
|
408
|
+
// plano e pede outra crítica. Com a publicação por Pull Request, o plano que
|
|
409
|
+
// ele acabou de corrigir está na branch do PR — ler só o disco responderia
|
|
410
|
+
// "nada a criticar" e removeria o gatilho, fechando a única saída do ciclo.
|
|
411
|
+
const plan = await ler('plan', planRel);
|
|
412
|
+
if (plan.content == null) {
|
|
367
413
|
console.log('plan.md ainda não existe — gere o plano antes de criticá-lo.');
|
|
368
414
|
await removeLabel(token, owner, repo, n, LABEL_CRITIQUE).catch(() => {});
|
|
369
415
|
await commentOnIssue(token, owner, repo, n,
|
|
@@ -371,13 +417,16 @@ export async function critique({ issueNumber, file, kind, failOnGrave = false })
|
|
|
371
417
|
'Aplique `spec-wave:plan` para gerá-lo.').catch(() => {});
|
|
372
418
|
return;
|
|
373
419
|
}
|
|
374
|
-
|
|
420
|
+
if (plan.state === 'pending-pr') {
|
|
421
|
+
console.log(`Criticando o plan.md do PR #${plan.pr?.number} (ainda não mergeado).`);
|
|
422
|
+
}
|
|
423
|
+
const spec = await ler('spec', specRel);
|
|
375
424
|
|
|
376
425
|
const issueLabels = labelNames(issue.labels || []);
|
|
377
426
|
await critiquePlan({
|
|
378
427
|
token, owner, repo, issueNumber: n,
|
|
379
|
-
spec:
|
|
380
|
-
plan:
|
|
428
|
+
spec: spec.content,
|
|
429
|
+
plan: plan.content,
|
|
381
430
|
techContextYaml: buildTechContext({ issueBody: issue.body || '' }).yaml,
|
|
382
431
|
labels: issueLabels, config, usage: [],
|
|
383
432
|
});
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { resolveToken } from '../api/auth.mjs';
|
|
3
2
|
import { getIssue, removeLabel, commentOnIssue } from '../api/github-rest.mjs';
|
|
4
3
|
import { generateDocument } from '../lib/claude.mjs';
|
|
5
4
|
import { unwrapGeneratedDoc } from '../lib/unwrap-doc.mjs';
|
|
6
5
|
import { recordUsage } from '../lib/usage-report.mjs';
|
|
7
6
|
import { slugify } from '../lib/slugify.mjs';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
7
|
+
import { resolveFlowContext } from '../lib/flow-run.mjs';
|
|
8
|
+
import { publishArtifact } from '../lib/artifact-publish.mjs';
|
|
9
|
+
import { renderPrLine } from '../lib/artifact-pr.mjs';
|
|
11
10
|
import { loadPrompt, systemPromptWithTools } from '../lib/prompt-loader.mjs';
|
|
12
11
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
13
12
|
import {
|
|
@@ -56,11 +55,9 @@ export async function generateSpec({ issueNumber }) {
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
const slug = slugify(issue.title);
|
|
59
|
-
//
|
|
60
|
-
//
|
|
58
|
+
// Só o caminho RELATIVO: a publicação é via API, e nada é gravado no working
|
|
59
|
+
// tree — ver o cabeçalho de lib/artifact-publish.mjs.
|
|
61
60
|
const featureRel = `docs/features/${slug}`;
|
|
62
|
-
const featureDir = resolveFromRoot(root, featureRel);
|
|
63
|
-
const filePath = path.join(featureDir, 'spec.md');
|
|
64
61
|
const fileRel = `${featureRel}/spec.md`;
|
|
65
62
|
|
|
66
63
|
// Payload estruturado (RFC-002 §5.1): metadata + entrada de negócio.
|
|
@@ -96,11 +93,15 @@ export async function generateSpec({ issueNumber }) {
|
|
|
96
93
|
// ver lib/unwrap-doc.mjs. O que vai para o repositório é o documento.
|
|
97
94
|
const content = unwrapGeneratedDoc(bruto);
|
|
98
95
|
|
|
99
|
-
const published =
|
|
100
|
-
|
|
96
|
+
const published = await publishArtifact({
|
|
97
|
+
token, owner, repo,
|
|
98
|
+
doc: 'spec',
|
|
99
|
+
issueNumber: parseInt(issueNumber, 10),
|
|
100
|
+
issueTitle: issue.title,
|
|
101
|
+
issueUrl: issue.html_url,
|
|
102
|
+
pathRel: fileRel,
|
|
101
103
|
content,
|
|
102
|
-
|
|
103
|
-
mode,
|
|
104
|
+
nextLabel: 'spec-wave:plan',
|
|
104
105
|
});
|
|
105
106
|
if (published.warning) console.warn(`⚠️ ${published.warning}`);
|
|
106
107
|
|
|
@@ -111,13 +112,16 @@ export async function generateSpec({ issueNumber }) {
|
|
|
111
112
|
await commentOnIssue(
|
|
112
113
|
token, owner, repo, parseInt(issueNumber, 10),
|
|
113
114
|
`📋 **spec.md gerado automaticamente!**\n\n` +
|
|
114
|
-
`📄 Arquivo: [\`${fileRel}\`](${
|
|
115
|
-
|
|
115
|
+
`📄 Arquivo: [\`${fileRel}\`](${published.blobUrl})\n\n` +
|
|
116
|
+
`${renderPrLine(published)}\n\n` +
|
|
117
|
+
`Revise a especificação **no Pull Request** e faça o merge. Depois, gere o plano técnico: mova o card para **📋 Plan** ou use:\n` +
|
|
116
118
|
`\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:plan"\n\`\`\`` +
|
|
117
119
|
formatLintWarning(lintFindings)
|
|
118
120
|
);
|
|
119
121
|
|
|
120
|
-
console.log(`spec.md
|
|
122
|
+
console.log(`spec.md publicado em ${published.branch} (${fileRel}).`);
|
|
123
|
+
// Devolvido para o `run` saber que o próximo passo depende de um merge.
|
|
124
|
+
return { pr: published.pr, branch: published.branch };
|
|
121
125
|
} catch (err) {
|
|
122
126
|
// Sem isto a label de gatilho fica aplicada — e como o workflow dispara em
|
|
123
127
|
// `issues: [labeled]`, re-adicionar uma label já presente não emite evento:
|