@spec-wave/cli 0.23.0 → 0.24.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/README.md +25 -5
- package/package.json +1 -1
- package/src/agent/anthropic-agent.mjs +40 -19
- package/src/agent/index.mjs +104 -38
- package/src/commands/doctor.mjs +38 -12
- package/src/commands/init.mjs +5 -0
- package/src/config.mjs +36 -5
- package/src/lib/claude.mjs +13 -3
- package/src/plugin/.claude-plugin/plugin.json +1 -1
- package/src/plugin/skills/doctor/SKILL.md +1 -1
- package/src/plugin/skills/setup/SKILL.md +1 -1
- package/src/templates/skill/SKILL.md +1 -1
- package/src/templates/workflows/critique.yml +1 -0
- package/src/templates/workflows/decompose.yml +1 -0
- package/src/templates/workflows/generate-bug.yml +1 -0
- package/src/templates/workflows/generate-plan.yml +1 -0
- package/src/templates/workflows/generate-spec.yml +1 -0
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ Arquivo em `.github/config/tech_context.yml` que descreve a stack tecnológica d
|
|
|
108
108
|
```bash
|
|
109
109
|
gh auth refresh --scopes project,repo,workflow
|
|
110
110
|
```
|
|
111
|
-
- Secret no repositório: `ANTHROPIC_API_KEY` ou `OPENROUTER_API_KEY` (Settings → Secrets → Actions)
|
|
111
|
+
- Secret no repositório, conforme o provider: `ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN` (assinatura Claude Pro/Max) ou `OPENROUTER_API_KEY` (Settings → Secrets → Actions)
|
|
112
112
|
- Para repositórios em organizações: criar PAT com escopo `project` e adicionar como secret `GH_PROJECT_TOKEN`
|
|
113
113
|
|
|
114
114
|
---
|
|
@@ -191,10 +191,10 @@ npx @spec-wave/cli@latest init --repo acme/loja --project-title "Loja — Spec W
|
|
|
191
191
|
O `init` cria:
|
|
192
192
|
- GitHub Project v2 com 12 colunas Kanban e campos personalizados (Work Item Type, Priority, Story Points, Area)
|
|
193
193
|
- 20+ labels de tipo, prioridade e gatilho
|
|
194
|
-
-
|
|
194
|
+
- 8 GitHub Actions workflows em `.github/workflows/`
|
|
195
195
|
- `.spec-wave.json` com os IDs do Project
|
|
196
196
|
|
|
197
|
-
Adicionar o secret de IA no GitHub: **Settings → Secrets → Actions
|
|
197
|
+
Adicionar o secret de IA no GitHub: **Settings → Secrets → Actions** — `ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN` ou `OPENROUTER_API_KEY`, conforme o provider escolhido no `init`.
|
|
198
198
|
|
|
199
199
|
---
|
|
200
200
|
|
|
@@ -408,14 +408,34 @@ Isso atualiza apenas os arquivos de workflow sem recriar o Project ou as labels.
|
|
|
408
408
|
|
|
409
409
|
| Provider | Secret | Modelo padrão |
|
|
410
410
|
|----------|--------|---------------|
|
|
411
|
-
|
|
|
412
|
-
|
|
|
411
|
+
| `anthropic` | `ANTHROPIC_API_KEY` | `claude-sonnet-4-6` |
|
|
412
|
+
| `claude-oauth` | `CLAUDE_CODE_OAUTH_TOKEN` | `claude-sonnet-4-6` |
|
|
413
|
+
| `openrouter` | `OPENROUTER_API_KEY` | `anthropic/claude-3.7-sonnet` |
|
|
413
414
|
|
|
414
415
|
Configurar no `init`:
|
|
415
416
|
```bash
|
|
416
417
|
npx @spec-wave/cli@latest init --repo owner/repo --provider openrouter --model anthropic/claude-3.7-sonnet
|
|
417
418
|
```
|
|
418
419
|
|
|
420
|
+
### `claude-oauth` — rodar tudo pela assinatura Claude Pro/Max
|
|
421
|
+
|
|
422
|
+
Mesmo motor do provider `anthropic`, credencial diferente: em vez de uma chave
|
|
423
|
+
de API cobrada por token, o token da sua assinatura. Os cinco workflows de IA
|
|
424
|
+
(spec, plan, bug, decompose, critique) rodam assim, inclusive nos GitHub Actions
|
|
425
|
+
— o binário do Claude Code vem embarcado na CLI, então o runner não precisa de
|
|
426
|
+
nada além do `setup-node` que os workflows já fazem.
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
claude setup-token # gere o token (escopo só de inferência)
|
|
430
|
+
# cole o valor em Settings → Secrets → Actions → CLAUDE_CODE_OAUTH_TOKEN
|
|
431
|
+
|
|
432
|
+
npx @spec-wave/cli@latest init --repo owner/repo --provider claude-oauth
|
|
433
|
+
# ou, num repo já configurado, troque "provider" no .spec-wave.json e rode `update`
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
O consumo passa a sair do limite do seu plano — sob rate limit da assinatura o
|
|
437
|
+
job falha por indisponibilidade, não por erro de configuração.
|
|
438
|
+
|
|
419
439
|
---
|
|
420
440
|
|
|
421
441
|
## Licença
|
package/package.json
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
// e da execução das tools. Por isso não há dependência direta de
|
|
7
7
|
// `@anthropic-ai/sdk` aqui — não reintroduza uma.
|
|
8
8
|
//
|
|
9
|
-
// Consequência operacional que vale lembrar:
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// Consequência operacional que vale lembrar: o binário vem EMBARCADO no SDK
|
|
10
|
+
// (dependência opcional por plataforma), então `npm install -g @spec-wave/cli`
|
|
11
|
+
// basta — inclusive no runner do GitHub Actions, que só tem `setup-node`. Quem
|
|
12
|
+
// confere isso antes da primeira chamada é `assertBackendRunnable` em index.mjs.
|
|
13
13
|
//
|
|
14
14
|
// Tudo que o processo pai aprende chega por dois canais, e os dois viram
|
|
15
15
|
// observações do Langfuse manualmente:
|
|
@@ -31,6 +31,18 @@ import { TruncatedOutputError } from './errors.mjs';
|
|
|
31
31
|
const DEFAULT_TOOLS = ['Read', 'Glob', 'Grep'];
|
|
32
32
|
const MAX_TURNS = 25;
|
|
33
33
|
|
|
34
|
+
// Variáveis de credencial cujo valor vazio deve ser tratado como AUSENTE.
|
|
35
|
+
const CREDENTIAL_VARS = ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN'];
|
|
36
|
+
|
|
37
|
+
/** Cópia do ambiente sem credenciais de valor vazio (função PURA). */
|
|
38
|
+
export function withoutBlankCredentials(env) {
|
|
39
|
+
const copy = { ...env };
|
|
40
|
+
for (const key of CREDENTIAL_VARS) {
|
|
41
|
+
if (copy[key] !== undefined && String(copy[key]).trim() === '') delete copy[key];
|
|
42
|
+
}
|
|
43
|
+
return copy;
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
/** Base de comparação da allowlist: absoluto resolvido, minúsculo no win32. */
|
|
35
47
|
function normalizeWritePath(p) {
|
|
36
48
|
const resolved = path.resolve(p);
|
|
@@ -45,22 +57,24 @@ function normalizeWritePath(p) {
|
|
|
45
57
|
* @returns {Promise<import('./run-types.mjs').AgentRunResult>}
|
|
46
58
|
*/
|
|
47
59
|
export async function runAnthropicAgent(prompt, options) {
|
|
48
|
-
if (options.responseSchema) {
|
|
49
|
-
// O subprocesso não expõe tool_choice forçado, então não há como garantir
|
|
50
|
-
// a tool call única que a saída estruturada exige. Falhar aqui é melhor do
|
|
51
|
-
// que devolver texto livre que o chamador tentaria parsear "na tolerância"
|
|
52
|
-
// — foi exatamente esse caminho que rebaixava um finding grave a menor.
|
|
53
|
-
throw new Error(
|
|
54
|
-
'Saída estruturada não é suportada no backend anthropic (o subprocesso do ' +
|
|
55
|
-
'Claude Code não expõe tool_choice forçado). Use o provider openrouter para ' +
|
|
56
|
-
'ações com schema — hoje, a crítica adversarial.',
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
60
|
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// Saída estruturada: `outputFormat` faz o CLI garantir um payload conforme o
|
|
63
|
+
// schema (ele mesmo repete o turno enquanto não bater) e devolvê-lo em
|
|
64
|
+
// `structured_output` no result. Não existe equivalente ao `strict` do
|
|
65
|
+
// OpenRouter aqui — a garantia é o loop de retry do CLI, não o tipo do
|
|
66
|
+
// endpoint; por isso `responseSchema.strict` é ignorado neste backend.
|
|
67
|
+
const schema = options.responseSchema ?? null;
|
|
68
|
+
// Com schema o modelo não explora nada: a resposta é UM payload. Misturar as
|
|
69
|
+
// duas coisas daria a saída de escape de "chamar Read de novo" em vez de
|
|
70
|
+
// fechar o contrato — mesma regra do backend openrouter.
|
|
71
|
+
//
|
|
72
|
+
// O que NÃO se copia de lá é o `maxTurns: 1`: aqui o turno extra é do próprio
|
|
73
|
+
// CLI refazendo a resposta até casar com o schema (um run que deu certo
|
|
74
|
+
// reportou 2 turnos). Apertar o teto trocaria esse retry por error_max_turns.
|
|
75
|
+
const tools = schema ? [] : (options.tools ?? DEFAULT_TOOLS);
|
|
76
|
+
// `^()$` casaria com nome vazio; sem tools, o matcher não pode casar nada.
|
|
77
|
+
const toolMatcher = tools.length > 0 ? `^(${tools.join('|')})$` : '(?!)';
|
|
64
78
|
const allowedWritePaths = options.allowedWritePaths?.map(normalizeWritePath) ?? null;
|
|
65
79
|
const runResult = {
|
|
66
80
|
resultSubtype: null,
|
|
@@ -98,6 +112,7 @@ export async function runAnthropicAgent(prompt, options) {
|
|
|
98
112
|
const queryOptions = {
|
|
99
113
|
model: options.model,
|
|
100
114
|
maxTurns: options.maxTurns ?? MAX_TURNS,
|
|
115
|
+
...(schema ? { outputFormat: { type: 'json_schema', schema: schema.jsonSchema } } : {}),
|
|
101
116
|
// `tools` restringe o que o modelo sequer VÊ; `allowedTools` é só a
|
|
102
117
|
// lista de auto-aprovação sobre essa superfície. Sem `tools`, as ~31
|
|
103
118
|
// tools do Claude Code aparecem e são apenas aprovadas/negadas por
|
|
@@ -123,7 +138,12 @@ export async function runAnthropicAgent(prompt, options) {
|
|
|
123
138
|
],
|
|
124
139
|
}
|
|
125
140
|
: {}),
|
|
126
|
-
env
|
|
141
|
+
// `env` SUBSTITUI o ambiente do subprocesso, então o spread é obrigatório
|
|
142
|
+
// (PATH, HOME). O saneamento existe porque `${{ secrets.X }}` de um
|
|
143
|
+
// secret inexistente injeta string VAZIA: um CLAUDE_CODE_OAUTH_TOKEN=""
|
|
144
|
+
// chegando ao CLI é ambiguidade gratuita entre "não configurado" e
|
|
145
|
+
// "configurado errado".
|
|
146
|
+
env: withoutBlankCredentials(process.env),
|
|
127
147
|
// Desliga o carregamento de settings do filesystem (~/.claude,
|
|
128
148
|
// .claude/settings.json) para o subprocesso não herdar hooks ou
|
|
129
149
|
// permissões de fora deste `queryOptions`.
|
|
@@ -290,6 +310,7 @@ export async function runAnthropicAgent(prompt, options) {
|
|
|
290
310
|
if (message.subtype === 'success') {
|
|
291
311
|
output = message.result;
|
|
292
312
|
runResult.outputText = message.result;
|
|
313
|
+
if (schema) runResult.structured = message.structured_output ?? null;
|
|
293
314
|
if (!quiet && !printedAnyText && message.result.trim() !== '') {
|
|
294
315
|
console.log(`\nClaude: ${message.result}`);
|
|
295
316
|
}
|
package/src/agent/index.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
// Registro de backends: transforma um
|
|
1
|
+
// Registro de backends: transforma um backend num runner concreto.
|
|
2
2
|
// Porte de `agent-cli/src/workflow/backends.ts` + `src/lib.ts`.
|
|
3
3
|
//
|
|
4
|
-
// É o ÚNICO módulo que sabe que os dois backends existem, então um
|
|
5
|
-
//
|
|
4
|
+
// É o ÚNICO módulo que sabe que os dois backends existem, então um MOTOR novo
|
|
5
|
+
// significa mexer só aqui. Um provider novo que reusa um motor existente
|
|
6
|
+
// (`claude-oauth` sobre o backend anthropic) não passa por este arquivo — vive
|
|
7
|
+
// inteiro em config.mjs.
|
|
8
|
+
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
6
10
|
|
|
7
11
|
import { runAnthropicAgent } from './anthropic-agent.mjs';
|
|
8
12
|
import { runOpenRouterAgent } from './openrouter-agent.mjs';
|
|
@@ -12,7 +16,13 @@ export { ENV_FILE_PATTERN, createToolContext, executeTool, toolDefinitionsFor, K
|
|
|
12
16
|
export { initTelemetry, shutdownTelemetry, telemetryConfigured } from './telemetry.mjs';
|
|
13
17
|
export { runAnthropicAgent, runOpenRouterAgent };
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
// Este módulo fala em BACKEND, não em provider: quem escolhe entre `anthropic`,
|
|
20
|
+
// `claude-oauth` e `openrouter` é o config, e src/lib/claude.mjs traduz essa
|
|
21
|
+
// escolha para o backend antes de chegar aqui. Assim um provider novo que reusa
|
|
22
|
+
// um backend existente (foi o caso do claude-oauth) não encosta em src/agent/.
|
|
23
|
+
export const BACKENDS = ['anthropic', 'openrouter'];
|
|
24
|
+
/** @deprecated nome antigo de BACKENDS — mantido para não quebrar importadores. */
|
|
25
|
+
export const PROVIDERS = BACKENDS;
|
|
16
26
|
|
|
17
27
|
const RUNNERS = {
|
|
18
28
|
anthropic: runAnthropicAgent,
|
|
@@ -20,16 +30,16 @@ const RUNNERS = {
|
|
|
20
30
|
};
|
|
21
31
|
|
|
22
32
|
/**
|
|
23
|
-
* Runner do
|
|
33
|
+
* Runner do backend (função PURA).
|
|
24
34
|
*
|
|
25
|
-
* @param {'anthropic'|'openrouter'}
|
|
35
|
+
* @param {'anthropic'|'openrouter'} backend
|
|
26
36
|
* @returns {(prompt: string, options: object) => Promise<object>}
|
|
27
37
|
*/
|
|
28
|
-
export function runnerFor(
|
|
29
|
-
const runner = RUNNERS[
|
|
38
|
+
export function runnerFor(backend) {
|
|
39
|
+
const runner = RUNNERS[backend];
|
|
30
40
|
if (!runner) {
|
|
31
41
|
throw new Error(
|
|
32
|
-
`
|
|
42
|
+
`Backend desconhecido: "${backend}" (esperado um de: ${BACKENDS.join(', ')}).`,
|
|
33
43
|
);
|
|
34
44
|
}
|
|
35
45
|
return runner;
|
|
@@ -39,20 +49,31 @@ export function runnerFor(provider) {
|
|
|
39
49
|
* Confere credenciais ANTES de qualquer chamada de modelo — um fluxo que morre
|
|
40
50
|
* no meio por falta de chave já gastou dinheiro de verdade.
|
|
41
51
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
52
|
+
* A checagem é por BACKEND, e o backend anthropic aceita qualquer uma das duas
|
|
53
|
+
* credenciais: chave de API (provider `anthropic`) ou token da assinatura
|
|
54
|
+
* (provider `claude-oauth`). Quem exige a credencial CERTA para o provider
|
|
55
|
+
* escolhido é o `doctor`, que sabe qual dos dois o repo configurou.
|
|
56
|
+
*
|
|
57
|
+
* Fora do CI o backend anthropic passa mesmo sem nada no ambiente: quem está
|
|
58
|
+
* logado no Claude Code (`claude /login`) tem a credencial guardada pelo
|
|
59
|
+
* próprio CLI, não em variável, e barrar aí rejeitaria uma sessão que
|
|
60
|
+
* funciona. No Actions não existe login interativo, então lá a ausência
|
|
61
|
+
* continua sendo erro imediato.
|
|
44
62
|
*
|
|
45
|
-
* @param {'anthropic'|'openrouter'}
|
|
63
|
+
* @param {'anthropic'|'openrouter'} backend
|
|
46
64
|
* @param {object} [env]
|
|
47
65
|
*/
|
|
48
|
-
export function assertCredentials(
|
|
49
|
-
if (
|
|
66
|
+
export function assertCredentials(backend, env = process.env) {
|
|
67
|
+
if (
|
|
68
|
+
backend === 'anthropic' && env.GITHUB_ACTIONS === 'true' &&
|
|
69
|
+
!env.ANTHROPIC_API_KEY && !env.CLAUDE_CODE_OAUTH_TOKEN
|
|
70
|
+
) {
|
|
50
71
|
throw new Error(
|
|
51
72
|
'Faltam credenciais: defina ANTHROPIC_API_KEY ou CLAUDE_CODE_OAUTH_TOKEN ' +
|
|
52
73
|
'(secret do repositório, em Settings → Secrets → Actions).',
|
|
53
74
|
);
|
|
54
75
|
}
|
|
55
|
-
if (
|
|
76
|
+
if (backend === 'openrouter' && !env.OPENROUTER_API_KEY) {
|
|
56
77
|
throw new Error(
|
|
57
78
|
'Faltam credenciais: defina OPENROUTER_API_KEY ' +
|
|
58
79
|
'(secret do repositório, em Settings → Secrets → Actions).',
|
|
@@ -60,49 +81,94 @@ export function assertCredentials(provider, env = process.env) {
|
|
|
60
81
|
}
|
|
61
82
|
}
|
|
62
83
|
|
|
84
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Pacotes que podem carregar o binário nativo do Claude Code nesta plataforma.
|
|
88
|
+
*
|
|
89
|
+
* No linux há duas variantes de libc e o npm instala só a que casa com o host;
|
|
90
|
+
* aceitar as duas evita um falso negativo em imagem musl (Alpine).
|
|
91
|
+
*
|
|
92
|
+
* @param {NodeJS.Platform} [platform]
|
|
93
|
+
* @param {string} [arch]
|
|
94
|
+
* @returns {string[]}
|
|
95
|
+
*/
|
|
96
|
+
export function nativeBinaryPackages(platform = process.platform, arch = process.arch) {
|
|
97
|
+
const base = `@anthropic-ai/claude-agent-sdk-${platform}-${arch}`;
|
|
98
|
+
return platform === 'linux' ? [base, `${base}-musl`] : [base];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* O binário nativo do Claude Code está instalado?
|
|
103
|
+
*
|
|
104
|
+
* @param {(id: string) => string} [resolve] injetável para teste
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
export function nativeBinaryAvailable(resolve = id => requireFromHere.resolve(id)) {
|
|
108
|
+
return nativeBinaryPackages().some(pkg => {
|
|
109
|
+
try {
|
|
110
|
+
resolve(`${pkg}/package.json`);
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
63
118
|
/**
|
|
64
|
-
* O backend pode rodar NESTE ambiente?
|
|
119
|
+
* O backend pode rodar NESTE ambiente?
|
|
65
120
|
*
|
|
66
121
|
* O backend anthropic não chama a API: `query()` sobe o **Claude Code CLI como
|
|
67
|
-
* processo filho**.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
122
|
+
* processo filho**. O que decide se isso funciona é a presença do binário — e
|
|
123
|
+
* ele vem EMBARCADO no `@anthropic-ai/claude-agent-sdk`, como dependência
|
|
124
|
+
* opcional por plataforma (`@anthropic-ai/claude-agent-sdk-linux-x64` e
|
|
125
|
+
* companhia). Ou seja: `npm install -g @spec-wave/cli` já o traz, inclusive no
|
|
126
|
+
* runner do GitHub Actions, que só tem `setup-node`.
|
|
127
|
+
*
|
|
128
|
+
* Isto já foi um bloqueio incondicional no Actions, escrito quando o SDK ainda
|
|
129
|
+
* exigia o Claude Code instalado à parte. Checar o binário em vez do ambiente
|
|
130
|
+
* cobre a falha de verdade — `npm install --omit=optional`, plataforma sem
|
|
131
|
+
* build publicado — e libera a assinatura (provider `claude-oauth`) no CI.
|
|
132
|
+
*
|
|
133
|
+
* Fora do alcance daqui: binário presente mas com a libc errada (musl num host
|
|
134
|
+
* glibc). Aí quem falha é o spawn, e a mensagem do próprio SDK já nomeia a causa.
|
|
71
135
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
136
|
+
* `SPEC_WAVE_ALLOW_ANTHROPIC_IN_CI=1` segue aceito como bypass — hoje inútil,
|
|
137
|
+
* mas está em workflows de quem seguiu a orientação antiga.
|
|
74
138
|
*
|
|
75
|
-
* @param {'anthropic'|'openrouter'}
|
|
139
|
+
* @param {'anthropic'|'openrouter'} backend
|
|
76
140
|
* @param {object} [env]
|
|
77
141
|
*/
|
|
78
|
-
export function assertBackendRunnable(
|
|
79
|
-
if (
|
|
80
|
-
if (env.GITHUB_ACTIONS !== 'true') return;
|
|
142
|
+
export function assertBackendRunnable(backend, env = process.env) {
|
|
143
|
+
if (backend !== 'anthropic') return;
|
|
81
144
|
if (env.SPEC_WAVE_ALLOW_ANTHROPIC_IN_CI) return;
|
|
145
|
+
if (nativeBinaryAvailable()) return;
|
|
82
146
|
throw new Error(
|
|
83
|
-
'O
|
|
84
|
-
|
|
147
|
+
'O backend `anthropic` sobe o Claude Code CLI como subprocesso, e o binário nativo ' +
|
|
148
|
+
`não está instalado (procurado em: ${nativeBinaryPackages().join(', ')}).\n\n` +
|
|
85
149
|
'Saídas:\n' +
|
|
86
|
-
' 1.
|
|
87
|
-
'
|
|
88
|
-
' 2. ou
|
|
89
|
-
'
|
|
150
|
+
' 1. Reinstale o CLI SEM `--omit=optional` (o binário é uma dependência opcional ' +
|
|
151
|
+
'por plataforma do @anthropic-ai/claude-agent-sdk);\n' +
|
|
152
|
+
' 2. ou troque para o OpenRouter no .spec-wave.json: "ai": { "provider": "openrouter", ' +
|
|
153
|
+
'"model": "anthropic/claude-opus-4.8" } e adicione o secret OPENROUTER_API_KEY.',
|
|
90
154
|
);
|
|
91
155
|
}
|
|
92
156
|
|
|
93
157
|
/**
|
|
94
|
-
* Executa uma ação de IA no
|
|
158
|
+
* Executa uma ação de IA no backend escolhido.
|
|
95
159
|
*
|
|
96
160
|
* É o ponto único por onde `lib/claude.mjs` fala com o motor: os dois backends
|
|
97
|
-
* implementam o mesmo contrato, então quem chama nunca ramifica por
|
|
161
|
+
* implementam o mesmo contrato, então quem chama nunca ramifica por backend.
|
|
162
|
+
* O campo continua se chamando `provider` por compatibilidade com o contrato do
|
|
163
|
+
* agent-cli de origem, mas o valor esperado é o BACKEND.
|
|
98
164
|
*
|
|
99
165
|
* @param {string} prompt conteúdo do usuário
|
|
100
166
|
* @param {import('./run-types.mjs').AgentRunOptions & {provider: string}} options
|
|
101
167
|
* @returns {Promise<import('./run-types.mjs').AgentRunResult>}
|
|
102
168
|
*/
|
|
103
169
|
export async function runAgent(prompt, options) {
|
|
104
|
-
const { provider, ...rest } = options;
|
|
105
|
-
assertBackendRunnable(
|
|
106
|
-
assertCredentials(
|
|
107
|
-
return runnerFor(
|
|
170
|
+
const { provider: backend, ...rest } = options;
|
|
171
|
+
assertBackendRunnable(backend);
|
|
172
|
+
assertCredentials(backend);
|
|
173
|
+
return runnerFor(backend)(prompt, rest);
|
|
108
174
|
}
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from '../api/auth.mjs';
|
|
15
15
|
import { getProjectSnapshot, listSubIssues } from '../api/github-graphql.mjs';
|
|
16
16
|
import {
|
|
17
|
-
CONFIG_FILE, WORKFLOW_FILES, getProvider, DEFAULT_PROVIDER, STATUS_OPTIONS,
|
|
17
|
+
CONFIG_FILE, WORKFLOW_FILES, getProvider, DEFAULT_PROVIDER, AI_PROVIDERS, STATUS_OPTIONS,
|
|
18
18
|
RETIRED_STAGES, ALL_LABELS, allLabelsFor, LABEL_NEEDS_HUMAN, MODEL_LABEL_PREFIX,
|
|
19
19
|
DEFAULT_MAX_CRITIQUE_ATTEMPTS, STAGE_TRACKS, AI_ACTIONS, recommendedModelAliases,
|
|
20
20
|
modelLabels,
|
|
@@ -679,8 +679,9 @@ async function checkAi(ctx) {
|
|
|
679
679
|
`${Object.entries(aliases).map(([a, m]) => `${a}=${m}`).join(', ')}.`
|
|
680
680
|
);
|
|
681
681
|
// Slug da OpenRouter tem "/" (anthropic/claude-…); id da Anthropic, não.
|
|
682
|
+
// O critério é o BACKEND: `claude-oauth` fala com a mesma API do `anthropic`.
|
|
682
683
|
const wrongShape = Object.entries(aliases).filter(([, m]) => (
|
|
683
|
-
provider.
|
|
684
|
+
provider.backend === 'openrouter' ? !String(m).includes('/') : String(m).includes('/')
|
|
684
685
|
));
|
|
685
686
|
if (wrongShape.length > 0) {
|
|
686
687
|
status = 'warn';
|
|
@@ -700,7 +701,7 @@ async function checkAi(ctx) {
|
|
|
700
701
|
|
|
701
702
|
// Saída estruturada da crítica: sem structured output confiável, a validação
|
|
702
703
|
// do schema queima os retries antes de falhar.
|
|
703
|
-
if (provider.
|
|
704
|
+
if (provider.backend === 'openrouter' && !fileAi.models?.critique) {
|
|
704
705
|
status = 'warn';
|
|
705
706
|
notes.push(
|
|
706
707
|
`Saída estruturada da crítica: o provider é openrouter e \`ai.models.critique\` não está ` +
|
|
@@ -708,21 +709,27 @@ async function checkAi(ctx) {
|
|
|
708
709
|
'e a crítica falha na primeira execução. Aponte `ai.models.critique` para um modelo com ' +
|
|
709
710
|
'saída estruturada.'
|
|
710
711
|
);
|
|
712
|
+
} else if (provider.backend === 'anthropic') {
|
|
713
|
+
// No backend anthropic quem garante o schema é o próprio Claude Code
|
|
714
|
+
// (`outputFormat: json_schema`), repetindo o turno até casar — não há
|
|
715
|
+
// tool call forçado nem `strict` de endpoint para reportar.
|
|
716
|
+
notes.push(
|
|
717
|
+
`Saída estruturada da crítica: json_schema do Claude Code (${provider.value}) ` +
|
|
718
|
+
'— o CLI repete o turno até a resposta casar com o schema.'
|
|
719
|
+
);
|
|
711
720
|
} else {
|
|
712
721
|
notes.push(
|
|
713
722
|
`Saída estruturada da crítica: tool call forçado (${provider.value}) ` +
|
|
714
723
|
`· strict=${supportsStrictSchema(critiqueModel) ? 'sim' : 'não'} neste modelo.`
|
|
715
724
|
);
|
|
716
725
|
}
|
|
717
|
-
// O backend anthropic
|
|
718
|
-
//
|
|
719
|
-
if (provider.
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
'
|
|
723
|
-
'
|
|
724
|
-
'OPENROUTER_API_KEY), ou instale o Claude Code no workflow e defina ' +
|
|
725
|
-
'SPEC_WAVE_ALLOW_ANTHROPIC_IN_CI=1. Localmente o anthropic funciona.'
|
|
726
|
+
// O backend anthropic sobe o Claude Code CLI como subprocesso — e o binário
|
|
727
|
+
// vem embarcado no SDK, então o runner não precisa de instalação extra.
|
|
728
|
+
if (provider.backend === 'anthropic') {
|
|
729
|
+
notes.push(
|
|
730
|
+
`Backend anthropic (${provider.value}): o Claude Code sobe como subprocesso a partir do ` +
|
|
731
|
+
'binário embarcado no @anthropic-ai/claude-agent-sdk — nada a instalar no runner além do ' +
|
|
732
|
+
'próprio CLI. Só falha se o pacote for instalado com `--omit=optional`.'
|
|
726
733
|
);
|
|
727
734
|
}
|
|
728
735
|
|
|
@@ -955,10 +962,19 @@ async function checkWorkflows(ctx) {
|
|
|
955
962
|
// comportamento de pipelines já em andamento.
|
|
956
963
|
const unpinned = [];
|
|
957
964
|
const otherVersion = [];
|
|
965
|
+
// O secret do provider escolhido chega ao runner? A YAML é que decide: um
|
|
966
|
+
// repo configurado com um provider cujo secret o workflow não encaminha morre
|
|
967
|
+
// com "Faltam credenciais" no meio do fluxo, e o único conserto é `update`.
|
|
968
|
+
const provider = getProvider(ctx.cfg?.ai?.provider) || getProvider(DEFAULT_PROVIDER);
|
|
969
|
+
const missingSecret = [];
|
|
958
970
|
for (const file of WORKFLOW_FILES) {
|
|
959
971
|
const content = readFileSync(path.join(dir, file), 'utf-8');
|
|
960
972
|
if (/@spec-wave\/cli@latest/.test(content)) unpinned.push(file);
|
|
961
973
|
else if (!content.includes(`@spec-wave/cli@${CLI_VERSION}`)) otherVersion.push(file);
|
|
974
|
+
// Identifica os workflows de IA pelo que eles próprios declaram, em vez de
|
|
975
|
+
// manter uma segunda lista para sair de sincronia com WORKFLOW_FILES.
|
|
976
|
+
const usesAi = AI_PROVIDERS.some(pr => content.includes(`secrets.${pr.secret}`));
|
|
977
|
+
if (usesAi && !content.includes(`secrets.${provider.secret}`)) missingSecret.push(file);
|
|
962
978
|
}
|
|
963
979
|
const notes = [`Os ${WORKFLOW_FILES.length} workflows do spec-wave estão presentes.`];
|
|
964
980
|
let status = 'ok';
|
|
@@ -976,6 +992,16 @@ async function checkWorkflows(ctx) {
|
|
|
976
992
|
'para fazer o bump explícito.'
|
|
977
993
|
);
|
|
978
994
|
}
|
|
995
|
+
if (missingSecret.length > 0) {
|
|
996
|
+
status = 'fail';
|
|
997
|
+
notes.push(
|
|
998
|
+
`Não encaminham \`${provider.secret}\` (secret do provider ${provider.value}): ` +
|
|
999
|
+
`${missingSecret.join(', ')}. Esses workflows vão falhar com "Faltam credenciais" — ` +
|
|
1000
|
+
'rode `npx @spec-wave/cli@latest update`.'
|
|
1001
|
+
);
|
|
1002
|
+
} else {
|
|
1003
|
+
notes.push(`Todos encaminham \`${provider.secret}\` (provider ${provider.value}).`);
|
|
1004
|
+
}
|
|
979
1005
|
return { name, status, detail: notes.join('\n') };
|
|
980
1006
|
}
|
|
981
1007
|
|
package/src/commands/init.mjs
CHANGED
|
@@ -220,6 +220,11 @@ export async function init(options) {
|
|
|
220
220
|
? ` 1. Commite o ${CONFIG_FILE} quando quiser (git add ${CONFIG_FILE} && git commit)\n`
|
|
221
221
|
: '') +
|
|
222
222
|
` ${configWritten ? '2' : '1'}. Adicione ${providerMeta.secret} como secret no repositório (provider: ${providerMeta.label})\n` +
|
|
223
|
+
// O valor do token da assinatura não é copiável de lugar nenhum: só existe
|
|
224
|
+
// depois de rodar o comando que o gera.
|
|
225
|
+
(providerMeta.secret === 'CLAUDE_CODE_OAUTH_TOKEN'
|
|
226
|
+
? ` ${chalk.dim('Gere o valor com: claude setup-token')}\n`
|
|
227
|
+
: '') +
|
|
223
228
|
` ${configWritten ? '3' : '2'}. Configure o board view para agrupar por "Etapa"\n` +
|
|
224
229
|
` ${configWritten ? '4' : '3'}. Crie uma Feature com o prefixo [FEATURE] no título\n` +
|
|
225
230
|
` ${configWritten ? '5' : '4'}. Use a skill spec-wave para guiar o fluxo\n\n` +
|
package/src/config.mjs
CHANGED
|
@@ -11,17 +11,36 @@ export const PORTAL_URL = 'https://spec-wave.astratech.net.br';
|
|
|
11
11
|
// O provider e o modelo escolhidos no `init` são persistidos em .spec-wave.json
|
|
12
12
|
// (bloco `ai`) e lidos em runtime por src/lib/claude.mjs. Cada provider declara
|
|
13
13
|
// o secret do GitHub Actions de onde a chave é lida.
|
|
14
|
+
//
|
|
15
|
+
// `value` é o que o usuário escolhe; `backend` é QUEM executa (src/agent/).
|
|
16
|
+
// Os dois não coincidem: `anthropic` e `claude-oauth` rodam no mesmo backend e
|
|
17
|
+
// diferem só na credencial — chave de API por token vs. token da assinatura.
|
|
18
|
+
// Separar os campos evita que cada consumidor tenha que decorar essa relação.
|
|
14
19
|
export const AI_PROVIDERS = [
|
|
15
20
|
{
|
|
16
21
|
value: 'anthropic',
|
|
22
|
+
backend: 'anthropic',
|
|
17
23
|
label: 'Anthropic (API direta)',
|
|
18
24
|
hint: 'Usa o secret ANTHROPIC_API_KEY',
|
|
19
25
|
secret: 'ANTHROPIC_API_KEY',
|
|
20
26
|
defaultModel: 'claude-sonnet-4-6',
|
|
21
27
|
modelHint: 'ex.: claude-sonnet-4-6, claude-opus-4-1',
|
|
22
28
|
},
|
|
29
|
+
{
|
|
30
|
+
// Mesmo backend do `anthropic`, credencial diferente: o token da assinatura
|
|
31
|
+
// Claude Pro/Max (`claude setup-token`), com escopo só de inferência. O
|
|
32
|
+
// consumo sai do limite do plano, não de crédito por token.
|
|
33
|
+
value: 'claude-oauth',
|
|
34
|
+
backend: 'anthropic',
|
|
35
|
+
label: 'Claude (assinatura Pro/Max via OAuth)',
|
|
36
|
+
hint: 'Usa o secret CLAUDE_CODE_OAUTH_TOKEN — gere com `claude setup-token`',
|
|
37
|
+
secret: 'CLAUDE_CODE_OAUTH_TOKEN',
|
|
38
|
+
defaultModel: 'claude-sonnet-4-6',
|
|
39
|
+
modelHint: 'ex.: claude-sonnet-4-6, claude-opus-4-1',
|
|
40
|
+
},
|
|
23
41
|
{
|
|
24
42
|
value: 'openrouter',
|
|
43
|
+
backend: 'openrouter',
|
|
25
44
|
label: 'OpenRouter (multi-modelo)',
|
|
26
45
|
hint: 'Usa o secret OPENROUTER_API_KEY',
|
|
27
46
|
secret: 'OPENROUTER_API_KEY',
|
|
@@ -49,10 +68,12 @@ export const MODEL_LABEL_PREFIX = 'spec-wave:model:';
|
|
|
49
68
|
// `spec-wave:model:<apelido>` cubra "roda de novo num modelo mais forte" sem
|
|
50
69
|
// obrigar cada projeto a inventar a própria nomenclatura.
|
|
51
70
|
//
|
|
52
|
-
// O mapa é POR
|
|
53
|
-
// slug com "/" (anthropic/claude-opus-5) e a
|
|
54
|
-
// (claude-opus-5). Sugerir o formato errado geraria
|
|
55
|
-
// "apelido com formato incompatível" que o doctor emite
|
|
71
|
+
// O mapa é POR BACKEND, não por provider, porque o que ele distingue é o
|
|
72
|
+
// formato do id: a OpenRouter usa slug com "/" (anthropic/claude-opus-5) e a
|
|
73
|
+
// Anthropic usa o id nu (claude-opus-5). Sugerir o formato errado geraria
|
|
74
|
+
// exatamente o aviso de "apelido com formato incompatível" que o doctor emite
|
|
75
|
+
// na linha seguinte. `anthropic` e `claude-oauth` compartilham o mesmo mapa
|
|
76
|
+
// porque compartilham o backend.
|
|
56
77
|
export const RECOMMENDED_MODEL_ALIASES = {
|
|
57
78
|
openrouter: {
|
|
58
79
|
opus: 'anthropic/claude-opus-5',
|
|
@@ -78,7 +99,17 @@ export const RECOMMENDED_MODEL_ALIASES = {
|
|
|
78
99
|
* @returns {Record<string,string>}
|
|
79
100
|
*/
|
|
80
101
|
export function recommendedModelAliases(provider) {
|
|
81
|
-
return RECOMMENDED_MODEL_ALIASES[provider]
|
|
102
|
+
return RECOMMENDED_MODEL_ALIASES[providerBackend(provider)];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Backend que executa um provider (função PURA).
|
|
107
|
+
*
|
|
108
|
+
* @param {string} [value] valor de `ai.provider`
|
|
109
|
+
* @returns {'anthropic'|'openrouter'}
|
|
110
|
+
*/
|
|
111
|
+
export function providerBackend(value) {
|
|
112
|
+
return (getProvider(value) || getProvider(DEFAULT_PROVIDER)).backend;
|
|
82
113
|
}
|
|
83
114
|
|
|
84
115
|
// Quantas críticas seguidas podem reprovar a mesma issue antes de exigir revisão
|
package/src/lib/claude.mjs
CHANGED
|
@@ -109,6 +109,9 @@ export function resolveAiConfig({ env = {}, fileAi = {}, action, labels = [], mo
|
|
|
109
109
|
|
|
110
110
|
return {
|
|
111
111
|
provider: meta.value,
|
|
112
|
+
// Quem executa. `anthropic` e `claude-oauth` caem no mesmo backend e
|
|
113
|
+
// diferem só na credencial — src/agent/ nunca vê essa distinção.
|
|
114
|
+
backend: meta.backend,
|
|
112
115
|
model: rawModel.trim(),
|
|
113
116
|
modelSource,
|
|
114
117
|
labelAlias,
|
|
@@ -359,7 +362,7 @@ export async function generateDocument(systemPrompt, userContent, opts = {}) {
|
|
|
359
362
|
// Extraída do generateDocument para o generateStructured usar a mesma contagem.
|
|
360
363
|
function recordUsageEntry(opts, ai, { inputTokens, outputTokens, cost }) {
|
|
361
364
|
if (!Array.isArray(opts.usage)) return;
|
|
362
|
-
const finalCost = cost === null && ai.
|
|
365
|
+
const finalCost = cost === null && ai.backend === 'anthropic'
|
|
363
366
|
? computeCost({ model: ai.model, inputTokens, outputTokens, pricing: ai.pricing })
|
|
364
367
|
: cost;
|
|
365
368
|
opts.usage.push({
|
|
@@ -507,7 +510,7 @@ async function generateViaEngine(
|
|
|
507
510
|
systemPrompt, userContent, ai, { temperature, maxTokens, maxTurns, schema, strict, action, tools },
|
|
508
511
|
) {
|
|
509
512
|
const result = await runAgent(userContent, {
|
|
510
|
-
provider: ai.
|
|
513
|
+
provider: ai.backend,
|
|
511
514
|
model: ai.model,
|
|
512
515
|
systemPromptAppend: systemPrompt,
|
|
513
516
|
// Sessão/usuário alimentam o agrupamento das traces no Langfuse. Sem
|
|
@@ -536,7 +539,14 @@ async function generateViaEngine(
|
|
|
536
539
|
if (result.structured === null || result.structured === undefined) {
|
|
537
540
|
const err = new Error(
|
|
538
541
|
`O modelo ${ai.model} não devolveu a saída estruturada "${schema.name}" ` +
|
|
539
|
-
`(subtype=${result.resultSubtype}).`
|
|
542
|
+
`(subtype=${result.resultSubtype}).` +
|
|
543
|
+
(result.resultSubtype === 'error_max_structured_output_retries'
|
|
544
|
+
// Subtype só do backend anthropic: o CLI repetiu o turno e a saída
|
|
545
|
+
// nunca bateu com o schema. Repetir tende a resolver; se insistir, o
|
|
546
|
+
// modelo é que não dá conta do contrato.
|
|
547
|
+
? ' O Claude Code esgotou as tentativas de casar a resposta com o schema —' +
|
|
548
|
+
' se repetir, aponte `ai.models.critique` para um modelo mais forte.'
|
|
549
|
+
: '')
|
|
540
550
|
);
|
|
541
551
|
err.transient = true; // repetir a mesma requisição costuma resolver
|
|
542
552
|
throw err;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-wave",
|
|
3
3
|
"displayName": "Spec Wave",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.24.0",
|
|
5
5
|
"description": "Fluxo spec-driven no GitHub (RFC-001): Projects v2, labels de gatilho, spec/plan gerados por Action, decomposição em duas etapas e implementação orientada a Stories/Tasks.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Astratech",
|
|
@@ -43,7 +43,7 @@ npx @spec-wave/cli@latest doctor
|
|
|
43
43
|
- escopo faltando → o usuário roda `!gh auth refresh --scopes project,repo,workflow` (interativo, ele executa)
|
|
44
44
|
- `.spec-wave.json` dessincronizado → `npx @spec-wave/cli@latest refresh --config`
|
|
45
45
|
- workflows/labels divergentes → skill **update**
|
|
46
|
-
- secret de IA ausente → Settings → Secrets → Actions (`ANTHROPIC_API_KEY` ou `OPENROUTER_API_KEY
|
|
46
|
+
- secret de IA ausente → Settings → Secrets → Actions (`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN` ou `OPENROUTER_API_KEY`, conforme o provider)
|
|
47
47
|
- `specKit.command` ausente → configure antes de usar a skill **implement**
|
|
48
48
|
3. Trate `!` como "não deu para verificar", não como falha.
|
|
49
49
|
4. Se tudo passar e o problema original persistir, aí sim investigue a superfície específica (Action, issue, board).
|
|
@@ -58,7 +58,7 @@ Você dirige o `init` **com flags**. Nunca rode `npx @spec-wave/cli@latest init`
|
|
|
58
58
|
|
|
59
59
|
8. **Adapte o `tech_context.yml`** — o scaffold vem com dados de exemplo e a qualidade do `plan.md` depende dele. Ofereça ajustá-lo agora; o passo a passo está na skill **plan** (`reference/tech-context.md`).
|
|
60
60
|
|
|
61
|
-
9. **Secret de IA:** instrua a adicionar em Settings → Secrets → Actions a
|
|
61
|
+
9. **Secret de IA:** instrua a adicionar em Settings → Secrets → Actions a credencial do provider escolhido: `ANTHROPIC_API_KEY` (Anthropic), `CLAUDE_CODE_OAUTH_TOKEN` (assinatura Claude Pro/Max — gere com `claude setup-token`) ou `OPENROUTER_API_KEY` (OpenRouter).
|
|
62
62
|
|
|
63
63
|
10. **Próximo passo:** criar a primeira Feature — skill **issue**.
|
|
64
64
|
|
|
@@ -459,7 +459,7 @@ Configura o spec-wave no repositório. Você dirige o `init` com flags — **nun
|
|
|
459
459
|
Use `--skip-project` / `--skip-labels` / `--skip-files` **apenas** para re-rodar uma fase específica que falhou antes.
|
|
460
460
|
7. O `init` cria o Project, as labels, os workflows, um **scaffold de `.github/config/tech_context.yml`** (só se ainda não existir) e grava `.spec-wave.json`. Oriente o usuário a fazer `git pull` para trazer os arquivos ao checkout local.
|
|
461
461
|
8. **Adapte o `tech_context.yml`**: o scaffold vem com dados de exemplo. Ofereça ajustá-lo à stack real do repo seguindo a seção **Tech Context** (perto do comando `/spec-wave plan`) — isso melhora muito a qualidade do `plan.md`.
|
|
462
|
-
9. Instrua o usuário a adicionar a
|
|
462
|
+
9. Instrua o usuário a adicionar a credencial de IA como secret no repositório (Settings → Secrets → Actions): `ANTHROPIC_API_KEY` (Anthropic), `CLAUDE_CODE_OAUTH_TOKEN` (assinatura Claude Pro/Max — gere com `claude setup-token`) ou `OPENROUTER_API_KEY` (OpenRouter), conforme o provider escolhido no `init`.
|
|
463
463
|
|
|
464
464
|
---
|
|
465
465
|
|
|
@@ -51,5 +51,6 @@ jobs:
|
|
|
51
51
|
env:
|
|
52
52
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
53
53
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
54
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
54
55
|
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
55
56
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
@@ -76,5 +76,6 @@ jobs:
|
|
|
76
76
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
77
77
|
PROJECT_TOKEN: ${{ secrets.GH_PROJECT_TOKEN }}
|
|
78
78
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
79
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
79
80
|
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
80
81
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
@@ -56,5 +56,6 @@ jobs:
|
|
|
56
56
|
env:
|
|
57
57
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
58
58
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
59
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
59
60
|
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
60
61
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
@@ -64,5 +64,6 @@ jobs:
|
|
|
64
64
|
env:
|
|
65
65
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
66
66
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
67
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
67
68
|
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
68
69
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
@@ -64,5 +64,6 @@ jobs:
|
|
|
64
64
|
env:
|
|
65
65
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
66
66
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
67
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
67
68
|
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
68
69
|
GITHUB_REPOSITORY: ${{ github.repository }}
|