dd-harness 0.13.0 → 0.15.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/dist/escreve-config.d.ts +17 -6
- package/dist/escreve-config.js +25 -16
- package/dist/index.js +105 -11
- package/dist/politica.d.ts +7 -0
- package/dist/politica.js +2 -1
- package/dist/roadmap.d.ts +91 -0
- package/dist/roadmap.js +153 -0
- package/package.json +1 -1
package/dist/escreve-config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Escrita de `.claude/settings.json`, `.mcp.json` e `AGENTS.md`, com merge.
|
|
2
|
+
* Escrita de `.claude/settings.json`, `.mcp.json`, `CLAUDE.md` e `AGENTS.md`, com merge.
|
|
3
3
|
*
|
|
4
4
|
* `init` (o comando antigo) so SUGERE esses arquivos — decisao de proposito, porque
|
|
5
5
|
* mesclar JSON alheio e onde falha silenciosa nasce. `start` (o wizard) reverte essa
|
|
@@ -31,11 +31,22 @@ export declare function escreveHook(raiz: string): Promise<ResultadoDaEscrita<"c
|
|
|
31
31
|
/** Acrescenta o servidor MCP a `.mcp.json`, sem tocar em outros servidores. */
|
|
32
32
|
export declare function escreveMcp(raiz: string): Promise<ResultadoDaEscrita<"criado" | "ja-tinha" | "acrescentado">>;
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
34
|
+
* Os dois pontos de entrada do repositorio — `CLAUDE.md` (Claude Code) e `AGENTS.md`
|
|
35
|
+
* (Codex, Cursor, Gemini CLI, Windsurf) — sao ESPELHOS: o mesmo apontamento para o
|
|
36
|
+
* dd-harness, sem regra propria. Nenhum dos dois e opcional, e nao ha pergunta sobre
|
|
37
|
+
* "qual ferramenta voce usa": quem abre o repositorio encontra o protocolo pela porta
|
|
38
|
+
* que a SUA ferramenta le, e as duas dizem a mesma coisa. Regra fica no servico; aqui
|
|
39
|
+
* fica so o ponteiro — por isso espelhar nao duplica nada que possa divergir.
|
|
40
|
+
*
|
|
41
|
+
* Markdown e livre, nao JSON: nao ha "mesclar" de verdade. Se o arquivo ja existe (com
|
|
42
|
+
* qualquer conteudo), so acrescenta a instrucao no fim; nunca sobrescreve o que a pessoa
|
|
43
|
+
* escreveu, porque markdown alheio e mais dificil de separar do nosso do que uma chave
|
|
44
|
+
* de objeto.
|
|
38
45
|
*/
|
|
39
|
-
export declare function
|
|
46
|
+
export declare function escrevePonteiro(raiz: string, arquivo: "AGENTS.md" | "CLAUDE.md"): Promise<{
|
|
47
|
+
estado: "criado" | "ja-tinha" | "acrescentado";
|
|
48
|
+
}>;
|
|
49
|
+
/** Compatibilidade: `init` ainda chama pelo nome antigo. */
|
|
50
|
+
export declare const escreveAgents: (raiz: string) => Promise<{
|
|
40
51
|
estado: "criado" | "ja-tinha" | "acrescentado";
|
|
41
52
|
}>;
|
package/dist/escreve-config.js
CHANGED
|
@@ -63,13 +63,20 @@ export async function escreveMcp(raiz) {
|
|
|
63
63
|
return { ok: true, estado: eraVazio ? "criado" : "acrescentado" };
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
66
|
+
* Os dois pontos de entrada do repositorio — `CLAUDE.md` (Claude Code) e `AGENTS.md`
|
|
67
|
+
* (Codex, Cursor, Gemini CLI, Windsurf) — sao ESPELHOS: o mesmo apontamento para o
|
|
68
|
+
* dd-harness, sem regra propria. Nenhum dos dois e opcional, e nao ha pergunta sobre
|
|
69
|
+
* "qual ferramenta voce usa": quem abre o repositorio encontra o protocolo pela porta
|
|
70
|
+
* que a SUA ferramenta le, e as duas dizem a mesma coisa. Regra fica no servico; aqui
|
|
71
|
+
* fica so o ponteiro — por isso espelhar nao duplica nada que possa divergir.
|
|
72
|
+
*
|
|
73
|
+
* Markdown e livre, nao JSON: nao ha "mesclar" de verdade. Se o arquivo ja existe (com
|
|
74
|
+
* qualquer conteudo), so acrescenta a instrucao no fim; nunca sobrescreve o que a pessoa
|
|
75
|
+
* escreveu, porque markdown alheio e mais dificil de separar do nosso do que uma chave
|
|
76
|
+
* de objeto.
|
|
70
77
|
*/
|
|
71
|
-
export async function
|
|
72
|
-
const caminho = join(raiz,
|
|
78
|
+
export async function escrevePonteiro(raiz, arquivo) {
|
|
79
|
+
const caminho = join(raiz, arquivo);
|
|
73
80
|
const instrucao = SUGESTAO_AGENTS;
|
|
74
81
|
let atual = null;
|
|
75
82
|
try {
|
|
@@ -88,14 +95,16 @@ export async function escreveAgents(raiz) {
|
|
|
88
95
|
await writeFile(caminho, `${atual}${separador}${instrucao}\n`, "utf8");
|
|
89
96
|
return { estado: "acrescentado" };
|
|
90
97
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
de
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
/** Compatibilidade: `init` ainda chama pelo nome antigo. */
|
|
99
|
+
export const escreveAgents = (raiz) => escrevePonteiro(raiz, "AGENTS.md");
|
|
100
|
+
const SUGESTAO_AGENTS = `## Protocolo do dd-harness
|
|
101
|
+
|
|
102
|
+
Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
|
|
103
|
+
|
|
104
|
+
**ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
|
|
105
|
+
\`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
|
|
106
|
+
de ler código, responder ou planejar.
|
|
107
|
+
|
|
108
|
+
- Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
|
|
109
|
+
avise o usuário e **não modifique nada** até ele resolver.
|
|
101
110
|
- Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.`;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { access } from "node:fs/promises";
|
|
3
|
+
import { access, readFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
// DEP0190: Node imprime esse warning direto no stderr no instante em que
|
|
6
6
|
// `spawn(..., { shell: true })` recebe um array de args — de forma sincrona, ANTES de
|
|
@@ -14,11 +14,12 @@ process.removeAllListeners("warning");
|
|
|
14
14
|
import { termosDaConsulta } from "./argv.js";
|
|
15
15
|
import { check, status } from "./check.js";
|
|
16
16
|
import { CAMINHO_CONFIG, guardaConfigDaMaquina, guardaToken, leConfigDaMaquina, leConfigDoRepo, leToken, } from "./config.js";
|
|
17
|
-
import {
|
|
17
|
+
import { escreveHook, escreveMcp, escrevePonteiro } from "./escreve-config.js";
|
|
18
18
|
import { grava } from "./gravar.js";
|
|
19
19
|
import { init, SUGESTAO_AGENTS, SUGESTAO_HOOK, SUGESTAO_MCP } from "./init.js";
|
|
20
20
|
import { pergunta, escolha, fechaPerguntas } from "./pergunta.js";
|
|
21
21
|
import { buscaPolitica } from "./politica.js";
|
|
22
|
+
import { blocoDeSessao, criaFase, editaFase, formataChangelog, formataRoadmap, leFases, } from "./roadmap.js";
|
|
22
23
|
import { busca } from "./buscar.js";
|
|
23
24
|
import { arquiva, edita, le } from "./curar.js";
|
|
24
25
|
import { criaPasta } from "./pasta.js";
|
|
@@ -61,6 +62,16 @@ const AJUDA = `dd-harness — a política e o Brain do projeto, no serviço
|
|
|
61
62
|
1 = não consegui buscar
|
|
62
63
|
--hook: fala o protocolo do SessionStart do
|
|
63
64
|
Claude Code, para pôr a política no contexto
|
|
65
|
+
dd-harness roadmap as fases abertas: a atual inteira, as próximas
|
|
66
|
+
por título (opcional — projeto sem fase não tem)
|
|
67
|
+
dd-harness changelog [--versao <v>]
|
|
68
|
+
o que já foi concluído, agrupado por versão
|
|
69
|
+
dd-harness fase criar --titulo "<t>" [--conteudo <arquivo.md>] [--versao <v>]
|
|
70
|
+
[--status ideia|aberta|concluida] [--slug <s>]
|
|
71
|
+
dd-harness fase editar <slug> [--titulo "<t>"] [--conteudo <arquivo.md>]
|
|
72
|
+
[--versao <v> | --sem-versao] [--status <s>] [--ordem <n>]
|
|
73
|
+
concluir = --status concluida: a fase sai do
|
|
74
|
+
roadmap e entra no changelog, nada migra
|
|
64
75
|
dd-harness --help
|
|
65
76
|
dd-harness --version qual binário está instalado nesta máquina
|
|
66
77
|
|
|
@@ -88,6 +99,77 @@ function argumento(argv, nome) {
|
|
|
88
99
|
return i >= 0 ? argv[i + 1] : undefined;
|
|
89
100
|
}
|
|
90
101
|
const API_PADRAO = "https://dd-harness.vercel.app";
|
|
102
|
+
// --- roadmap e changelog ---
|
|
103
|
+
async function comandoRoadmap() {
|
|
104
|
+
console.log(formataRoadmap(await leFases(process.cwd())));
|
|
105
|
+
}
|
|
106
|
+
async function comandoChangelog(argv) {
|
|
107
|
+
const versao = argumento(argv, "versao");
|
|
108
|
+
const fases = await leFases(process.cwd(), { status: "concluida", ...(versao ? { versao } : {}) });
|
|
109
|
+
console.log(formataChangelog(fases));
|
|
110
|
+
}
|
|
111
|
+
const STATUS_DE_FASE = ["ideia", "aberta", "concluida", "descartada"];
|
|
112
|
+
function statusDoArgv(argv) {
|
|
113
|
+
const s = argumento(argv, "status");
|
|
114
|
+
if (s === undefined)
|
|
115
|
+
return undefined;
|
|
116
|
+
if (!STATUS_DE_FASE.includes(s)) {
|
|
117
|
+
throw new Error(`--status: use ${STATUS_DE_FASE.join(", ")}.`);
|
|
118
|
+
}
|
|
119
|
+
return s;
|
|
120
|
+
}
|
|
121
|
+
/** `--conteudo` aponta para um .md, como `gravar` e `editar` — markdown de verdade nao cabe em flag. */
|
|
122
|
+
async function conteudoDoArgv(argv) {
|
|
123
|
+
const caminho = argumento(argv, "conteudo");
|
|
124
|
+
return caminho === undefined ? undefined : readFile(caminho, "utf8");
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* `fase criar` e `fase editar`. Concluir uma fase e `fase editar <slug> --status concluida`
|
|
128
|
+
* — nao ha verbo proprio de proposito: e uma edicao de status como qualquer outra, e e o
|
|
129
|
+
* banco que a transforma em changelog.
|
|
130
|
+
*/
|
|
131
|
+
async function comandoFase(argv) {
|
|
132
|
+
const [acao, ...resto] = argv;
|
|
133
|
+
if (acao === "criar") {
|
|
134
|
+
const titulo = argumento(resto, "titulo");
|
|
135
|
+
if (!titulo)
|
|
136
|
+
throw new Error('fase criar exige --titulo "<título>".');
|
|
137
|
+
const status = statusDoArgv(resto);
|
|
138
|
+
if (status === "descartada")
|
|
139
|
+
throw new Error("não se cria uma fase já descartada.");
|
|
140
|
+
const { fase } = await criaFase(process.cwd(), {
|
|
141
|
+
titulo,
|
|
142
|
+
conteudo: await conteudoDoArgv(resto),
|
|
143
|
+
versao: argumento(resto, "versao"),
|
|
144
|
+
status,
|
|
145
|
+
slug: argumento(resto, "slug"),
|
|
146
|
+
});
|
|
147
|
+
console.log(`Fase ${fase.slug} criada — ordem ${fase.ordem}, ${fase.status}` +
|
|
148
|
+
(fase.versao ? `, versão ${fase.versao}` : "") +
|
|
149
|
+
".");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (acao === "editar") {
|
|
153
|
+
const [slug, ...opcoes] = resto;
|
|
154
|
+
if (!slug || slug.startsWith("--"))
|
|
155
|
+
throw new Error("fase editar exige o slug da fase.");
|
|
156
|
+
const ordemBruta = argumento(opcoes, "ordem");
|
|
157
|
+
const status = statusDoArgv(opcoes);
|
|
158
|
+
const { fase } = await editaFase(process.cwd(), slug, {
|
|
159
|
+
titulo: argumento(opcoes, "titulo"),
|
|
160
|
+
conteudo: await conteudoDoArgv(opcoes),
|
|
161
|
+
versao: opcoes.includes("--sem-versao") ? null : argumento(opcoes, "versao"),
|
|
162
|
+
status,
|
|
163
|
+
ordem: ordemBruta === undefined ? undefined : Number(ordemBruta),
|
|
164
|
+
});
|
|
165
|
+
console.log(`Fase ${fase.slug} atualizada — ${fase.status}` +
|
|
166
|
+
(fase.versao ? `, versão ${fase.versao}` : "") +
|
|
167
|
+
(status === "concluida" ? " (saiu do roadmap, entrou no changelog)" : "") +
|
|
168
|
+
".");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
throw new Error('uso: dd-harness fase criar --titulo "<t>" [...] | dd-harness fase editar <slug> [...]');
|
|
172
|
+
}
|
|
91
173
|
/**
|
|
92
174
|
* Forca o `npx` a baixar e cachear `dd-harness-mcp` AGORA, dentro do wizard — nunca na
|
|
93
175
|
* primeira conexao do Claude Code. Sem isto, a primeira instalacao acontecia so quando
|
|
@@ -287,15 +369,18 @@ async function comandoStart() {
|
|
|
287
369
|
"conexão do Claude Code pode demorar ou falhar; se falhar, abra uma nova sessão " +
|
|
288
370
|
"e tente de novo.");
|
|
289
371
|
}
|
|
290
|
-
// 6. AGENTS.md —
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
372
|
+
// 6. CLAUDE.md e AGENTS.md — os dois SEMPRE, sem perguntar. Sao espelhos: o mesmo
|
|
373
|
+
// apontamento para o servico, cada um na porta que uma familia de ferramenta le. A
|
|
374
|
+
// pergunta "usa outra ferramenta?" saiu porque a resposta nao muda nada que valha a
|
|
375
|
+
// pena — quem responde "nao" hoje abre o repositorio no Cursor semana que vem e nao
|
|
376
|
+
// encontra protocolo nenhum, e o custo de ja ter o arquivo e um ponteiro de 8 linhas.
|
|
377
|
+
console.log("");
|
|
378
|
+
for (const arquivo of ["CLAUDE.md", "AGENTS.md"]) {
|
|
379
|
+
const r = await escrevePonteiro(process.cwd(), arquivo);
|
|
380
|
+
console.log({ criado: `criado ${arquivo}`,
|
|
381
|
+
"ja-tinha": `mantido ${arquivo} — já apontava para a política`,
|
|
382
|
+
acrescentado: `atualizado ${arquivo} (apontamento acrescentado ao que já existia)`,
|
|
383
|
+
}[r.estado]);
|
|
299
384
|
}
|
|
300
385
|
// 7. Onde esta o monorepo do worker nesta maquina? So pergunta uma vez, e so importa
|
|
301
386
|
// se houver memoria na fila agora — pular aqui nao trava nada, so avisa mais vezes.
|
|
@@ -585,6 +670,9 @@ function contextoDaSessao(r, worker) {
|
|
|
585
670
|
additionalContext: "# Política deste projeto (carregada do dd-harness)\n\n" +
|
|
586
671
|
"As regras abaixo valem para esta sessão inteira.\n\n" +
|
|
587
672
|
r.conteudo +
|
|
673
|
+
// Depois da politica, antes dos avisos operacionais: o roadmap e contexto de
|
|
674
|
+
// trabalho, a fila e ruido de infraestrutura. Vazio quando nao ha fase aberta.
|
|
675
|
+
blocoDeSessao(r.roadmap) +
|
|
588
676
|
fila,
|
|
589
677
|
};
|
|
590
678
|
}
|
|
@@ -799,6 +887,12 @@ async function principal() {
|
|
|
799
887
|
return comandoStatus();
|
|
800
888
|
case "politica":
|
|
801
889
|
return comandoPolitica(resto);
|
|
890
|
+
case "roadmap":
|
|
891
|
+
return comandoRoadmap();
|
|
892
|
+
case "changelog":
|
|
893
|
+
return comandoChangelog(resto);
|
|
894
|
+
case "fase":
|
|
895
|
+
return comandoFase(resto);
|
|
802
896
|
case "--version":
|
|
803
897
|
case "-V":
|
|
804
898
|
case "version":
|
package/dist/politica.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RoadmapDaSessao } from "./roadmap.js";
|
|
1
2
|
/**
|
|
2
3
|
* A politica do projeto, buscada no servico na hora.
|
|
3
4
|
*
|
|
@@ -30,5 +31,11 @@ export type ResultadoDaPolitica = ({
|
|
|
30
31
|
* faz esta chamada, entao saber o estado da fila custa zero requisicao.
|
|
31
32
|
*/
|
|
32
33
|
esperandoIndexacao?: number;
|
|
34
|
+
/**
|
|
35
|
+
* O roadmap ja fatiado para a sessao (fase atual inteira, proximas por titulo). Tambem de
|
|
36
|
+
* carona no mesmo payload — e opcional: `agora` nulo e "projeto sem roadmap", e o hook
|
|
37
|
+
* nao imprime nada.
|
|
38
|
+
*/
|
|
39
|
+
roadmap?: RoadmapDaSessao;
|
|
33
40
|
};
|
|
34
41
|
export declare function buscaPolitica(raiz: string): Promise<ResultadoDaPolitica>;
|
package/dist/politica.js
CHANGED
|
@@ -24,6 +24,7 @@ export async function buscaPolitica(raiz) {
|
|
|
24
24
|
const payload = (await resposta.json());
|
|
25
25
|
const conteudo = payload.politica?.trim();
|
|
26
26
|
const esperandoIndexacao = payload.esperando_indexacao ?? 0;
|
|
27
|
+
const roadmap = payload.roadmap;
|
|
27
28
|
// `briefado` e do SERVICO, nao inferido aqui: exige politica E briefing, os dois
|
|
28
29
|
// obrigatorios. So checar `politica` vazia deixava passar o caso de politica
|
|
29
30
|
// existir sem briefing — a sessao seguia como "ok" com metade do briefing faltando,
|
|
@@ -31,7 +32,7 @@ export async function buscaPolitica(raiz) {
|
|
|
31
32
|
if (!conteudo || payload.briefado === false) {
|
|
32
33
|
return { estado: "sem-politica", esperandoIndexacao };
|
|
33
34
|
}
|
|
34
|
-
return { estado: "ok", conteudo, esperandoIndexacao };
|
|
35
|
+
return { estado: "ok", conteudo, esperandoIndexacao, roadmap };
|
|
35
36
|
}
|
|
36
37
|
catch (erro) {
|
|
37
38
|
return { estado: "inalcancavel", motivo: mensagem(erro) };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roadmap e changelog: o mesmo dado, lido de dois jeitos.
|
|
3
|
+
*
|
|
4
|
+
* Uma fase e uma linha com `status`. Roadmap e o que esta aberto; changelog e o que foi
|
|
5
|
+
* concluido, agrupado por versao. Concluir e mudar um status — nada migra, e e isso que
|
|
6
|
+
* impede o roadmap de virar o cemiterio que o ROADMAP.md file-based virou (395 linhas em
|
|
7
|
+
* "Agora", seis delas ja feitas, porque mover texto e um ato que ninguem lembra de fazer).
|
|
8
|
+
*
|
|
9
|
+
* As funcoes de rede falam com `/api/v1/roadmap`. As de formatacao sao puras, para o hook,
|
|
10
|
+
* o terminal e o MCP imprimirem a mesma coisa — e para serem testadas sem rede.
|
|
11
|
+
*/
|
|
12
|
+
export type StatusDaFase = "ideia" | "aberta" | "concluida" | "descartada";
|
|
13
|
+
export type Fase = {
|
|
14
|
+
slug: string;
|
|
15
|
+
titulo: string;
|
|
16
|
+
conteudo: string;
|
|
17
|
+
status: StatusDaFase;
|
|
18
|
+
versao: string | null;
|
|
19
|
+
ordem: number;
|
|
20
|
+
concluida_em: string | null;
|
|
21
|
+
atualizada_em: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* O que o payload de `/api/v1/artefatos` traz ja fatiado para a sessao: a fase atual
|
|
25
|
+
* inteira, os titulos das proximas, e quantas ja foram concluidas. `agora` nulo significa
|
|
26
|
+
* "este projeto nao tem roadmap" — e o hook cala.
|
|
27
|
+
*/
|
|
28
|
+
export type RoadmapDaSessao = {
|
|
29
|
+
agora: {
|
|
30
|
+
slug: string;
|
|
31
|
+
titulo: string;
|
|
32
|
+
versao: string | null;
|
|
33
|
+
conteudo: string;
|
|
34
|
+
} | null;
|
|
35
|
+
a_seguir: {
|
|
36
|
+
slug: string;
|
|
37
|
+
titulo: string;
|
|
38
|
+
versao: string | null;
|
|
39
|
+
}[];
|
|
40
|
+
concluidas: number;
|
|
41
|
+
};
|
|
42
|
+
export type NovaFase = {
|
|
43
|
+
titulo: string;
|
|
44
|
+
conteudo?: string;
|
|
45
|
+
versao?: string | null;
|
|
46
|
+
status?: Exclude<StatusDaFase, "descartada">;
|
|
47
|
+
slug?: string;
|
|
48
|
+
};
|
|
49
|
+
export type EdicaoDeFase = {
|
|
50
|
+
titulo?: string;
|
|
51
|
+
conteudo?: string;
|
|
52
|
+
versao?: string | null;
|
|
53
|
+
status?: StatusDaFase;
|
|
54
|
+
ordem?: number;
|
|
55
|
+
};
|
|
56
|
+
export declare function leFases(raiz: string, filtro?: {
|
|
57
|
+
status?: StatusDaFase;
|
|
58
|
+
versao?: string;
|
|
59
|
+
}): Promise<Fase[]>;
|
|
60
|
+
export declare function leFase(raiz: string, slug: string): Promise<Fase>;
|
|
61
|
+
export declare function criaFase(raiz: string, dados: NovaFase): Promise<{
|
|
62
|
+
fase: Fase;
|
|
63
|
+
criou: boolean;
|
|
64
|
+
}>;
|
|
65
|
+
export declare function editaFase(raiz: string, slug: string, dados: EdicaoDeFase): Promise<{
|
|
66
|
+
fase: Fase;
|
|
67
|
+
}>;
|
|
68
|
+
/** Rotulo do grupo de fases concluidas sem versao: o "Unreleased" do Keep a Changelog. */
|
|
69
|
+
export declare const SEM_VERSAO = "Sem vers\u00E3o (ainda n\u00E3o lan\u00E7ado)";
|
|
70
|
+
/**
|
|
71
|
+
* O bloco que entra no contexto da sessao, colado depois da politica. Vazio quando o
|
|
72
|
+
* projeto nao tem fase aberta: aviso sem motivo em toda sessao e o que ensina a ignorar
|
|
73
|
+
* aviso — e roadmap e opcional.
|
|
74
|
+
*
|
|
75
|
+
* So a fase atual vai em texto integral. As proximas vao por titulo, e as concluidas nem
|
|
76
|
+
* isso — so a contagem. E a reducao de tokens que o modelo file-based nao tinha como
|
|
77
|
+
* fazer: la o arquivo entrava inteiro, feito e por fazer.
|
|
78
|
+
*/
|
|
79
|
+
export declare function blocoDeSessao(r: RoadmapDaSessao | null | undefined): string;
|
|
80
|
+
/**
|
|
81
|
+
* O roadmap para o terminal e para `ler_roadmap`: recebe TODAS as fases do projeto e
|
|
82
|
+
* separa por status. Concluidas nao aparecem aqui — sao changelog.
|
|
83
|
+
*/
|
|
84
|
+
export declare function formataRoadmap(fases: Fase[]): string;
|
|
85
|
+
/**
|
|
86
|
+
* O changelog: fases concluidas agrupadas por versao, na ordem em que a API devolveu (mais
|
|
87
|
+
* recente primeiro). Grupo sem versao recebe `SEM_VERSAO`. A ordem dos grupos e a da
|
|
88
|
+
* primeira aparicao — versao e rotulo livre, entao nao ha como ordena-la melhor que pelo
|
|
89
|
+
* tempo em que suas fases foram concluidas.
|
|
90
|
+
*/
|
|
91
|
+
export declare function formataChangelog(concluidas: Fase[]): string;
|
package/dist/roadmap.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
2
|
+
// --- rede ---
|
|
3
|
+
export async function leFases(raiz, filtro = {}) {
|
|
4
|
+
const { config, token } = await credencial(raiz);
|
|
5
|
+
const url = new URL(`${config.api}/api/v1/roadmap`);
|
|
6
|
+
url.searchParams.set("tenant", config.tenant);
|
|
7
|
+
url.searchParams.set("projeto", config.projeto);
|
|
8
|
+
if (filtro.status)
|
|
9
|
+
url.searchParams.set("status", filtro.status);
|
|
10
|
+
if (filtro.versao)
|
|
11
|
+
url.searchParams.set("versao", filtro.versao);
|
|
12
|
+
const resposta = await pede(url, { headers: cabecalhos(token) });
|
|
13
|
+
if (!resposta.ok)
|
|
14
|
+
await recusa(resposta);
|
|
15
|
+
return (await resposta.json()).fases;
|
|
16
|
+
}
|
|
17
|
+
export async function leFase(raiz, slug) {
|
|
18
|
+
const { config, token } = await credencial(raiz);
|
|
19
|
+
const url = new URL(`${config.api}/api/v1/roadmap/${encodeURIComponent(slug)}`);
|
|
20
|
+
url.searchParams.set("tenant", config.tenant);
|
|
21
|
+
url.searchParams.set("projeto", config.projeto);
|
|
22
|
+
const resposta = await pede(url, { headers: cabecalhos(token) });
|
|
23
|
+
if (!resposta.ok)
|
|
24
|
+
await recusa(resposta);
|
|
25
|
+
return (await resposta.json()).fase;
|
|
26
|
+
}
|
|
27
|
+
export async function criaFase(raiz, dados) {
|
|
28
|
+
const { config, token } = await credencial(raiz);
|
|
29
|
+
const resposta = await pede(`${config.api}/api/v1/roadmap`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: cabecalhos(token, true),
|
|
32
|
+
body: JSON.stringify({ tenant: config.tenant, projeto: config.projeto, ...dados }),
|
|
33
|
+
});
|
|
34
|
+
if (!resposta.ok)
|
|
35
|
+
await recusa(resposta);
|
|
36
|
+
return (await resposta.json());
|
|
37
|
+
}
|
|
38
|
+
export async function editaFase(raiz, slug, dados) {
|
|
39
|
+
const { config, token } = await credencial(raiz);
|
|
40
|
+
const resposta = await pede(`${config.api}/api/v1/roadmap/${encodeURIComponent(slug)}`, {
|
|
41
|
+
method: "PATCH",
|
|
42
|
+
headers: cabecalhos(token, true),
|
|
43
|
+
body: JSON.stringify({ tenant: config.tenant, projeto: config.projeto, ...dados }),
|
|
44
|
+
});
|
|
45
|
+
if (!resposta.ok)
|
|
46
|
+
await recusa(resposta);
|
|
47
|
+
return (await resposta.json());
|
|
48
|
+
}
|
|
49
|
+
// --- formatacao (pura) ---
|
|
50
|
+
/** Rotulo do grupo de fases concluidas sem versao: o "Unreleased" do Keep a Changelog. */
|
|
51
|
+
export const SEM_VERSAO = "Sem versão (ainda não lançado)";
|
|
52
|
+
const rotuloDeVersao = (versao) => (versao ? ` (${versao})` : "");
|
|
53
|
+
/**
|
|
54
|
+
* O bloco que entra no contexto da sessao, colado depois da politica. Vazio quando o
|
|
55
|
+
* projeto nao tem fase aberta: aviso sem motivo em toda sessao e o que ensina a ignorar
|
|
56
|
+
* aviso — e roadmap e opcional.
|
|
57
|
+
*
|
|
58
|
+
* So a fase atual vai em texto integral. As proximas vao por titulo, e as concluidas nem
|
|
59
|
+
* isso — so a contagem. E a reducao de tokens que o modelo file-based nao tinha como
|
|
60
|
+
* fazer: la o arquivo entrava inteiro, feito e por fazer.
|
|
61
|
+
*/
|
|
62
|
+
export function blocoDeSessao(r) {
|
|
63
|
+
if (!r?.agora)
|
|
64
|
+
return "";
|
|
65
|
+
const linhas = [
|
|
66
|
+
"",
|
|
67
|
+
"",
|
|
68
|
+
"---",
|
|
69
|
+
"",
|
|
70
|
+
"# Roadmap deste projeto (dd-harness)",
|
|
71
|
+
"",
|
|
72
|
+
`## Agora — ${r.agora.titulo}${rotuloDeVersao(r.agora.versao)}`,
|
|
73
|
+
"",
|
|
74
|
+
r.agora.conteudo.trim() || "_(fase sem descrição — pergunte ao usuário o que ela cobre.)_",
|
|
75
|
+
];
|
|
76
|
+
if (r.a_seguir.length > 0) {
|
|
77
|
+
linhas.push("", "## A seguir");
|
|
78
|
+
for (const f of r.a_seguir)
|
|
79
|
+
linhas.push(`- ${f.titulo}${rotuloDeVersao(f.versao)}`);
|
|
80
|
+
}
|
|
81
|
+
linhas.push("", r.concluidas > 0
|
|
82
|
+
? `${r.concluidas} fase(s) já concluída(s) — \`ler_changelog\` mostra o que foi entregue.`
|
|
83
|
+
: "Nenhuma fase concluída ainda.", "", "É da fase **Agora** que saem os passos desta sessão. Quando ela terminar, proponha ao " +
|
|
84
|
+
"usuário marcá-la concluída (`editar_fase` com `status: \"concluida\"`) — ela sai do " +
|
|
85
|
+
"roadmap e entra no changelog sozinha. O roadmap guarda o quê e quando; o porquê das " +
|
|
86
|
+
"decisões vai ao Brain, pelos três filtros, e fase concluída não gera memória por padrão.");
|
|
87
|
+
return linhas.join("\n");
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* O roadmap para o terminal e para `ler_roadmap`: recebe TODAS as fases do projeto e
|
|
91
|
+
* separa por status. Concluidas nao aparecem aqui — sao changelog.
|
|
92
|
+
*/
|
|
93
|
+
export function formataRoadmap(fases) {
|
|
94
|
+
const porOrdem = (a, b) => a.ordem - b.ordem;
|
|
95
|
+
const abertas = fases.filter((f) => f.status === "aberta").sort(porOrdem);
|
|
96
|
+
const ideias = fases.filter((f) => f.status === "ideia").sort(porOrdem);
|
|
97
|
+
const concluidas = fases.filter((f) => f.status === "concluida").length;
|
|
98
|
+
if (abertas.length === 0 && ideias.length === 0) {
|
|
99
|
+
return ("Este projeto não tem roadmap." +
|
|
100
|
+
(concluidas > 0 ? ` (${concluidas} fase(s) concluída(s) no changelog.)` : "") +
|
|
101
|
+
"\nCrie a primeira fase: `dd-harness fase criar --titulo \"...\"`.");
|
|
102
|
+
}
|
|
103
|
+
const linhas = [];
|
|
104
|
+
const [agora, ...aSeguir] = abertas;
|
|
105
|
+
if (agora) {
|
|
106
|
+
linhas.push(`## Agora — ${agora.titulo}${rotuloDeVersao(agora.versao)} [${agora.slug}]`, "");
|
|
107
|
+
linhas.push(agora.conteudo.trim() || "_(sem descrição)_");
|
|
108
|
+
}
|
|
109
|
+
if (aSeguir.length > 0) {
|
|
110
|
+
linhas.push("", "## A seguir");
|
|
111
|
+
for (const f of aSeguir)
|
|
112
|
+
linhas.push(`- ${f.titulo}${rotuloDeVersao(f.versao)} [${f.slug}]`);
|
|
113
|
+
}
|
|
114
|
+
if (ideias.length > 0) {
|
|
115
|
+
linhas.push("", "## Ideias (não comprometidas)");
|
|
116
|
+
for (const f of ideias)
|
|
117
|
+
linhas.push(`- ${f.titulo}${rotuloDeVersao(f.versao)} [${f.slug}]`);
|
|
118
|
+
}
|
|
119
|
+
// Neutro de proposito: este texto sai igual no terminal e na ferramenta MCP, e cada
|
|
120
|
+
// porta tem o seu jeito de abrir o changelog.
|
|
121
|
+
linhas.push("", concluidas > 0
|
|
122
|
+
? `${concluidas} fase(s) concluída(s) — estão no changelog.`
|
|
123
|
+
: "Nenhuma fase concluída ainda.");
|
|
124
|
+
return linhas.join("\n");
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* O changelog: fases concluidas agrupadas por versao, na ordem em que a API devolveu (mais
|
|
128
|
+
* recente primeiro). Grupo sem versao recebe `SEM_VERSAO`. A ordem dos grupos e a da
|
|
129
|
+
* primeira aparicao — versao e rotulo livre, entao nao ha como ordena-la melhor que pelo
|
|
130
|
+
* tempo em que suas fases foram concluidas.
|
|
131
|
+
*/
|
|
132
|
+
export function formataChangelog(concluidas) {
|
|
133
|
+
if (concluidas.length === 0)
|
|
134
|
+
return "Nenhuma fase concluída ainda.";
|
|
135
|
+
const grupos = new Map();
|
|
136
|
+
for (const f of concluidas) {
|
|
137
|
+
const chave = f.versao ?? SEM_VERSAO;
|
|
138
|
+
const grupo = grupos.get(chave) ?? [];
|
|
139
|
+
grupo.push(f);
|
|
140
|
+
grupos.set(chave, grupo);
|
|
141
|
+
}
|
|
142
|
+
const linhas = [];
|
|
143
|
+
for (const [versao, fases] of grupos) {
|
|
144
|
+
if (linhas.length > 0)
|
|
145
|
+
linhas.push("");
|
|
146
|
+
linhas.push(`## ${versao}`);
|
|
147
|
+
for (const f of fases) {
|
|
148
|
+
const data = f.concluida_em ? f.concluida_em.slice(0, 10) : "";
|
|
149
|
+
linhas.push(`- ${f.titulo}${data ? ` — ${data}` : ""} [${f.slug}]`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return linhas.join("\n");
|
|
153
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Cliente do dd-harness: politica no inicio da sessao, e memoria por busca — nada em disco. Sem dependencia: fetch, crypto e fs sao do Node.",
|
|
6
6
|
"license": "UNLICENSED",
|