dd-harness 0.14.0 → 0.16.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/check.d.ts CHANGED
@@ -29,6 +29,11 @@ export type ResultadoDoCheck = {
29
29
  * rename a cruzar, e a lista de ausentes continua sendo tudo o que se pode dizer.
30
30
  */
31
31
  reancoragens: Reancoragem[];
32
+ /**
33
+ * Pediram `--commit` e o cruzamento nao aconteceu — e por que. `undefined` quando
34
+ * aconteceu, ou quando nao se pediu commit nenhum.
35
+ */
36
+ semGit?: "sem-repositorio" | "commit-desconhecido" | "git-indisponivel";
32
37
  };
33
38
  export type ResultadoDoStatus = {
34
39
  /**
package/dist/check.js CHANGED
@@ -99,6 +99,7 @@ export async function check(raiz, commit) {
99
99
  fechadas: 0,
100
100
  tocadas,
101
101
  reancoragens,
102
+ semGit: mudancas.semGit,
102
103
  };
103
104
  }
104
105
  const envio = await pede(`${config.api}/api/v1/deriva`, {
@@ -127,5 +128,6 @@ export async function check(raiz, commit) {
127
128
  fechadas: julgamento.fechadas ?? 0,
128
129
  tocadas,
129
130
  reancoragens,
131
+ semGit: mudancas.semGit,
130
132
  };
131
133
  }
package/dist/diff.d.ts CHANGED
@@ -19,6 +19,16 @@ export type Rename = {
19
19
  export type MudancasDoCommit = {
20
20
  caminhos: string[];
21
21
  renames: Rename[];
22
+ /**
23
+ * Por que o cruzamento nao aconteceu. `undefined` = aconteceu (mesmo que o commit nao
24
+ * tenha tocado nada que uma ancora guarda).
25
+ *
26
+ * Sem isto, "o commit nao mexeu em nada indexado" e "o git nem rodou" saem identicos —
27
+ * uma lista vazia. Uma rodada de teste com agente-cobaia passou `--commit` com um SHA
28
+ * inventado, fora de um repositorio git, e recebeu exit 0 e a mesma saida de sempre: o
29
+ * cruzamento que ela queria exercitar simplesmente nao existiu, sem uma linha dizendo.
30
+ */
31
+ semGit?: "sem-repositorio" | "commit-desconhecido" | "git-indisponivel";
22
32
  };
23
33
  /**
24
34
  * O que um commit mudou: caminhos tocados e renames detectados.
package/dist/diff.js CHANGED
@@ -17,10 +17,34 @@ export async function caminhosDoCommit(raiz, commit) {
17
17
  const { stdout } = await roda("git", ["diff-tree", "--no-commit-id", "-r", "-M", "--name-status", commit], { cwd: raiz });
18
18
  return interpretaNameStatus(stdout);
19
19
  }
20
- catch {
21
- return { caminhos: [], renames: [] };
20
+ catch (erro) {
21
+ // Continua sem lancar o gancho `post-commit` nao pode falhar por causa de um aviso.
22
+ // O que muda e que a razao volta junto, para quem chamou poder dize-la em vez de
23
+ // mostrar "nenhuma mudanca" quando na verdade nada foi consultado.
24
+ return { caminhos: [], renames: [], semGit: classificaFalhaDoGit(erro) };
22
25
  }
23
26
  }
27
+ /**
28
+ * Traduz a falha do `git diff-tree` nos tres casos que pedem respostas diferentes de quem
29
+ * chamou: consertar o repositorio, conferir o SHA, ou instalar o git.
30
+ *
31
+ * A leitura e do stderr porque o `git` usa o mesmo codigo de saida (128) para "not a git
32
+ * repository" e para "bad object" — o texto e o unico jeito de separar os dois.
33
+ */
34
+ function classificaFalhaDoGit(erro) {
35
+ const e = erro;
36
+ if (e.code === "ENOENT")
37
+ return "git-indisponivel";
38
+ const stderr = e.stderr ?? "";
39
+ if (/not a git repository/i.test(stderr))
40
+ return "sem-repositorio";
41
+ if (/bad object|unknown revision|ambiguous argument/i.test(stderr)) {
42
+ return "commit-desconhecido";
43
+ }
44
+ // Falha que nao sabemos nomear: ainda e melhor avisar que o cruzamento nao aconteceu do
45
+ // que deixar passar como silencio.
46
+ return "git-indisponivel";
47
+ }
24
48
  /**
25
49
  * Le a saida de `--name-status`: uma letra de status, TAB, e um ou dois caminhos.
26
50
  *
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
@@ -19,6 +19,7 @@ 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
@@ -471,8 +553,29 @@ async function comandoReancorar(argv) {
471
553
  console.log(` para ${r.para}`);
472
554
  console.log(" Rode `dd-harness check --commit <sha>` para medir a base nova.");
473
555
  }
556
+ /**
557
+ * Pediram `--commit` e o cruzamento com o diff nao aconteceu. Avisa ANTES do resto: sem
558
+ * isto a saida e identica a de um commit que nao tocou em nada indexado, e quem pediu o
559
+ * cruzamento nao tem como saber que ele nao existiu.
560
+ *
561
+ * Aviso, nao erro: o gancho `post-commit` termina em sucesso mesmo assim, e a medicao das
562
+ * ancoras (que nao depende de git) continua valendo.
563
+ */
564
+ function avisaSemGit(motivo) {
565
+ const texto = {
566
+ "sem-repositorio": "esta pasta não é um repositório git — não há diff para cruzar com as âncoras.",
567
+ "commit-desconhecido": "o git não reconhece este commit — confira o SHA.",
568
+ "git-indisponivel": "não consegui rodar o git aqui.",
569
+ }[motivo];
570
+ console.log(`AVISO: ${texto}`);
571
+ console.log(" As âncoras foram medidas assim mesmo; só o cruzamento com o");
572
+ console.log(" commit ficou de fora.");
573
+ console.log("");
574
+ }
474
575
  async function comandoCheck(argv) {
475
576
  const r = await check(process.cwd(), argumento(argv, "commit"));
577
+ if (r.semGit)
578
+ avisaSemGit(r.semGit);
476
579
  if (r.medidas === 0) {
477
580
  console.log("nenhuma âncora para medir.");
478
581
  return;
@@ -588,6 +691,9 @@ function contextoDaSessao(r, worker) {
588
691
  additionalContext: "# Política deste projeto (carregada do dd-harness)\n\n" +
589
692
  "As regras abaixo valem para esta sessão inteira.\n\n" +
590
693
  r.conteudo +
694
+ // Depois da politica, antes dos avisos operacionais: o roadmap e contexto de
695
+ // trabalho, a fila e ruido de infraestrutura. Vazio quando nao ha fase aberta.
696
+ blocoDeSessao(r.roadmap) +
591
697
  fila,
592
698
  };
593
699
  }
@@ -802,6 +908,12 @@ async function principal() {
802
908
  return comandoStatus();
803
909
  case "politica":
804
910
  return comandoPolitica(resto);
911
+ case "roadmap":
912
+ return comandoRoadmap();
913
+ case "changelog":
914
+ return comandoChangelog(resto);
915
+ case "fase":
916
+ return comandoFase(resto);
805
917
  case "--version":
806
918
  case "-V":
807
919
  case "version":
@@ -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;
@@ -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.14.0",
3
+ "version": "0.16.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",