dd-harness 0.39.0 → 0.40.1

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.
@@ -21,6 +21,8 @@ export type Item = {
21
21
  * maquina, nao por projeto).
22
22
  */
23
23
  export declare function diagnosticaAtualizacao(raiz: string, versaoInstalada: string, versaoPublicada: string | null): Promise<Item[]>;
24
+ /** Reescreve em disco os ponteiros das skills que o serviço tem. */
25
+ export declare function reescrevePonteiros(raiz: string): Promise<string>;
24
26
  /**
25
27
  * Incorpora ao projeto as skills iniciais escolhidas pelo usuario.
26
28
  *
package/dist/atualizar.js CHANGED
@@ -140,7 +140,7 @@ export async function diagnosticaAtualizacao(raiz, versaoInstalada, versaoPublic
140
140
  return itens;
141
141
  }
142
142
  /** Reescreve em disco os ponteiros das skills que o serviço tem. */
143
- async function reescrevePonteiros(raiz) {
143
+ export async function reescrevePonteiros(raiz) {
144
144
  const destinos = [...new Set((await hostsInstalados(raiz))
145
145
  .map(h => h === "claude" ? ".claude" : ".agents"))];
146
146
  const r = await escrevePonteirosDeSkills(raiz, await leSkills(raiz), destinos.length ? destinos : [".claude"]);
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { diagnostico } from "./diagnostico.js";
3
- import { aplicaAtualizacao, diagnosticaAtualizacao, versaoInstalada, versaoPublicada } from "./atualizar.js";
3
+ import { aplicaAtualizacao, diagnosticaAtualizacao, reescrevePonteiros, versaoInstalada, versaoPublicada } from "./atualizar.js";
4
4
  import { instalaHosts, selecionaHosts } from "./hosts.js";
5
5
  import { achaRaiz } from "./config.js";
6
6
  import { abreSessao, guardaSessao, saidaDaGuarda, saidaDoBoot } from "./sessao.js";
@@ -27,7 +27,7 @@ import { pergunta, escolha, fechaPerguntas } from "./pergunta.js";
27
27
  import { buscaPolitica } from "./politica.js";
28
28
  import { avisoDeOrdem, blocoDeSessao, criaFase, editaFase, formataChangelog, formataRoadmap, leFases, } from "./roadmap.js";
29
29
  import { decideDoHook, guardaCache } from "./cinto.js";
30
- import { escrevePonteirosDeSkills, leSkill, leSkills, semeiaSkills } from "./skill.js";
30
+ import { criaSkill, editaSkill, escrevePonteirosDeSkills, leSkill, leSkills, semeiaSkills } from "./skill.js";
31
31
  import { SKILLS_INICIAIS } from "./skills-iniciais.js";
32
32
  import { busca } from "./buscar.js";
33
33
  import { apaga, arquiva, edita, le, promove } from "./curar.js";
@@ -80,7 +80,7 @@ const AJUDA = `dd-harness — a política e o Brain do projeto, no serviço
80
80
  projeto do espaço, inclusive os futuros
81
81
  dd-harness despromover <pasta>/<slug>
82
82
  traz de volta ao alcance dos vínculos
83
- dd-harness apagar <pasta>/<slug> [--confirmar "<frase devolvida>"]
83
+ dd-harness apagar <pasta>/<slug> [--confirmar "<frase devolvida>"]
84
84
  apaga de vez, em cascata — sem desfazer.
85
85
  Para tirar de circulação guardando o
86
86
  conteúdo, use "arquivar"
@@ -101,6 +101,11 @@ const AJUDA = `dd-harness — a política e o Brain do projeto, no serviço
101
101
  dd-harness skills as skills deste projeto e quando cada uma serve
102
102
  dd-harness skill <nome> o procedimento de uma delas (o mesmo que o
103
103
  agente recebe ao invocá-la)
104
+ dd-harness skill criar <nome> --descricao "<quando invocar>" [--conteudo <arquivo.md>]
105
+ [--so-por-comando true|false]
106
+ dd-harness skill editar <nome> [--descricao "<d>"] [--conteudo <arquivo.md>]
107
+ [--so-por-comando true|false]
108
+ grava no serviço e reescreve o ponteiro em disco
104
109
  dd-harness roadmap as fases abertas: a atual inteira, as próximas
105
110
  por título (opcional — projeto sem fase não tem)
106
111
  dd-harness changelog [--versao <v>]
@@ -140,11 +145,9 @@ function argumento(argv, nome) {
140
145
  const API_PADRAO = "https://dd-harness.vercel.app";
141
146
  // --- roadmap e changelog ---
142
147
  /**
143
- * `skills` e `skill <slug>` — so LEITURA.
144
- *
145
- * Criar e editar ficam na interface de proposito: escrever procedimento e trabalho de
146
- * texto longo, e um editor de verdade e melhor que um flag de linha de comando. O que o
147
- * terminal precisa e ver o que existe e conferir um procedimento sem trocar de janela.
148
+ * `skills` e `skill <slug>` — leitura. Criar e editar sao `skill criar|editar`, com o
149
+ * procedimento vindo de um .md (`--conteudo`), como em `fase` e `gravar`: sem isso a skill
150
+ * `escrever-skill` redigia o texto e nao tinha como grava-lo fora da interface.
148
151
  */
149
152
  async function comandoSkills() {
150
153
  const skills = await leSkills(process.cwd());
@@ -160,10 +163,44 @@ async function comandoSkills() {
160
163
  }
161
164
  console.log(`${skills.length} skill(s). \`dd-harness skill <nome>\` mostra o procedimento.`);
162
165
  }
166
+ /** `--so-por-comando true|false`: flag solta seria so "ligar", e desligar tambem e edicao. */
167
+ function soPorComandoDoArgv(argv) {
168
+ const v = argumento(argv, "so-por-comando");
169
+ if (v === undefined)
170
+ return undefined;
171
+ if (v !== "true" && v !== "false")
172
+ throw new Error("--so-por-comando: use true ou false.");
173
+ return v === "true";
174
+ }
175
+ /**
176
+ * `skill criar` e `skill editar`. O ponteiro em disco e reescrito em seguida: descricao e
177
+ * "so por comando" moram no frontmatter dele, e o host so le o disco.
178
+ */
179
+ async function comandoSkillEscrita(acao, argv) {
180
+ const [slug, ...opcoes] = argv;
181
+ if (!slug || slug.startsWith("--"))
182
+ throw new Error(`skill ${acao} exige o nome da skill.`);
183
+ const campos = {
184
+ descricao: argumento(opcoes, "descricao"),
185
+ conteudo: await conteudoDoArgv(opcoes),
186
+ so_por_comando: soPorComandoDoArgv(opcoes),
187
+ };
188
+ if (acao === "criar") {
189
+ if (!campos.descricao)
190
+ throw new Error('skill criar exige --descricao "<quando invocar>".');
191
+ await criaSkill(process.cwd(), { ...campos, slug, descricao: campos.descricao, conteudo: campos.conteudo ?? "" });
192
+ }
193
+ else {
194
+ await editaSkill(process.cwd(), slug, campos);
195
+ }
196
+ console.log(`Skill ${slug} ${acao === "criar" ? "criada" : "atualizada"}; ${await reescrevePonteiros(process.cwd())}.`);
197
+ }
163
198
  async function comandoSkill(argv) {
164
199
  const slug = argv[0];
200
+ if (slug === "criar" || slug === "editar")
201
+ return comandoSkillEscrita(slug, argv.slice(1));
165
202
  if (!slug || slug.startsWith("-")) {
166
- throw new Error("uso: dd-harness skill <nome>");
203
+ throw new Error("uso: dd-harness skill <nome> | dd-harness skill criar|editar <nome> [...]");
167
204
  }
168
205
  const s = await leSkill(process.cwd(), slug);
169
206
  console.log(`# ${s.slug}
package/dist/sessao.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
- import { join } from "node:path";
3
+ import { join, resolve } from "node:path";
4
4
  import { credencial } from "./api.js";
5
5
  import { achaRaiz } from "./config.js";
6
6
  import { decideDoHook, guardaCache, leCache } from "./cinto.js";
7
7
  import { edicoesDaChamada, normalizaEntrada } from "./edicoes.js";
8
- import { estadoDoRepo, escreveAtomico, revisaoDoContexto } from "./estado-local.js";
8
+ import { diretorioDoUsuario, estadoDoRepo, escreveAtomico, revisaoDoContexto } from "./estado-local.js";
9
9
  import { buscaPolitica } from "./politica.js";
10
10
  import { blocoDeSessao } from "./roadmap.js";
11
11
  import { leSkills, escrevePonteirosDeSkills } from "./skill.js";
@@ -14,21 +14,38 @@ import { subiuOWorker } from "./worker.js";
14
14
  const PRAZO = 4 * 60 * 60 * 1000;
15
15
  const hash = (s) => createHash("sha256").update(s).digest("hex");
16
16
  const arquivo = (raiz, sessao, host) => join(estadoDoRepo(raiz), `sessao-${hash(host + sessao)}.json`);
17
+ const arquivoDaRaiz = (sessao, host) => join(diretorioDoUsuario(), "sessoes", `${hash(host + sessao)}.json`);
18
+ async function raizDaSessao(sessao, host) {
19
+ try {
20
+ const lido = JSON.parse(await readFile(arquivoDaRaiz(sessao, host), "utf8"));
21
+ return typeof lido.raiz === "string" ? lido.raiz : null;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ const mesmaRaiz = (a, b) => process.platform === "win32" ? resolve(a).toLowerCase() === resolve(b).toLowerCase() : resolve(a) === resolve(b);
17
28
  async function identidade(raiz) {
18
29
  const c = await credencial(raiz);
19
30
  return hash(JSON.stringify(c)); // inclui token sem expô-lo em disco/log.
20
31
  }
21
- async function estadoAtual(raiz, sessao, host) {
32
+ async function estadoConhecido(raiz, sessao, host) {
22
33
  try {
23
34
  const e = JSON.parse(await readFile(arquivo(raiz, sessao, host), "utf8"));
24
- if (e.estado === "ok" && e.revisao !== await revisaoDoContexto(raiz))
25
- return null;
26
- return e.identidade === await identidade(raiz) && Date.now() >= e.em && Date.now() - e.em < PRAZO ? e : null;
35
+ return e.identidade === await identidade(raiz) && Date.now() >= e.em ? e : null;
27
36
  }
28
37
  catch {
29
38
  return null;
30
39
  }
31
40
  }
41
+ async function estadoAtual(raiz, sessao, host) {
42
+ const e = await estadoConhecido(raiz, sessao, host);
43
+ if (!e || Date.now() - e.em >= PRAZO)
44
+ return null;
45
+ if (e.estado === "ok" && e.revisao !== await revisaoDoContexto(raiz))
46
+ return null;
47
+ return e;
48
+ }
32
49
  export function saidaDoBoot(host, contexto, ok) {
33
50
  return host === "antigravity"
34
51
  ? { injectSteps: contexto ? [{ ephemeralMessage: contexto }] : [] }
@@ -39,7 +56,9 @@ export async function abreSessao(host, entrada) {
39
56
  const c = normalizaEntrada(host, entrada);
40
57
  if (!c.sessao)
41
58
  throw new Error("hook sem identificador de sessão: não é possível validar o boot");
42
- const raiz = await achaRaiz(c.raiz);
59
+ const raiz = await achaRaiz(c.raiz).catch(() => raizDaSessao(c.sessao, host));
60
+ if (!raiz)
61
+ throw new Error("dd-harness: projeto não encontrado. Abra a sessão a partir de um projeto configurado.");
43
62
  const anterior = await estadoAtual(raiz, c.sessao, host);
44
63
  // A mensagem do Antigravity é efêmera: não presumir que sobreviva à próxima inferência.
45
64
  const inicio = host !== "antigravity" || Number(entrada.invocationNum) === 0 || anterior?.estado !== "ok";
@@ -85,6 +104,7 @@ export async function abreSessao(host, entrada) {
85
104
  contexto = `PARE: não foi possível carregar a política e o briefing. ${r.motivo}. Corrija a conexão/configuração e reabra a sessão.`;
86
105
  await escreveAtomico(arquivo(raiz, c.sessao, host), JSON.stringify({ identidade: id,
87
106
  revisao, ciclo, estado: r.estado, em: Date.now() }));
107
+ await escreveAtomico(arquivoDaRaiz(c.sessao, host), JSON.stringify({ raiz }));
88
108
  return saidaDoBoot(host, contexto, r.estado !== "inalcancavel");
89
109
  }
90
110
  export function saidaDaGuarda(host, motivo, contexto) {
@@ -113,44 +133,64 @@ const LEITURA_MCP = /^mcp__dd[-_]harness__(ler_artefato|ler_roadmap|ler_changelo
113
133
  const ONBOARDING = /^(ToolSearch|mcp__dd[-_]harness__(escrever_artefato|criar_pasta|criar_projeto))$/;
114
134
  export async function guardaSessao(host, entrada) {
115
135
  const c = normalizaEntrada(host, entrada);
116
- const raiz = await achaRaiz(c.raiz);
117
- const e = c.sessao ? await estadoAtual(raiz, c.sessao, host) : null;
136
+ const vinculada = c.sessao ? await raizDaSessao(c.sessao, host) : null;
137
+ const atual = await achaRaiz(c.raiz).catch(() => null);
138
+ if (vinculada && atual && !mesmaRaiz(vinculada, atual)) {
139
+ return saidaDaGuarda(host, "dd-harness: esta sessão pertence a outro projeto. Abra uma sessão neste projeto para carregar sua política e seu briefing.");
140
+ }
141
+ const raiz = atual ?? vinculada;
142
+ if (!raiz) {
143
+ if (LEITURA.test(c.ferramenta) || LEITURA_MCP.test(c.ferramenta))
144
+ return saidaDaGuarda(host);
145
+ return saidaDaGuarda(host, "dd-harness: esta sessão não tem projeto associado. Volte ao projeto configurado e abra uma sessão nele; não rode `dd-harness start` na pasta de rascunho.");
146
+ }
147
+ const edicoes = edicoesDaChamada(c);
148
+ for (const edicao of edicoes) {
149
+ const projetoDoArquivo = await achaRaiz(edicao.arquivo).catch(() => null);
150
+ if (projetoDoArquivo && !mesmaRaiz(projetoDoArquivo, raiz)) {
151
+ return saidaDaGuarda(host, "dd-harness: o arquivo pertence a outro projeto. Abra uma sessão nesse projeto antes de editá-lo.");
152
+ }
153
+ }
154
+ let e = c.sessao ? await estadoAtual(raiz, c.sessao, host) : null;
155
+ let contextoAtualizado = "";
118
156
  if (e?.estado !== "ok") {
119
157
  if (LEITURA.test(c.ferramenta) || LEITURA_MCP.test(c.ferramenta))
120
158
  return saidaDaGuarda(host);
121
- // O dia zero de um projeto novo trava num ciclo: gravar o briefing exige a guarda
122
- // liberada, e a guarda so libera com briefing gravado. `estadoAtual` devolve `null`
123
- // tanto para "projeto novo" quanto para "sessao expirada", entao o estado local nao
124
- // distingue os dois — quem sabe e o SERVICO. Se ele diz que nao ha politica, este
125
- // projeto ainda esta em onboarding e as ferramentas de escreve-lo passam.
159
+ const anterior = c.sessao ? await estadoConhecido(raiz, c.sessao, host) : null;
160
+ // Projeto novo so pode completar o briefing se as ferramentas que o escrevem passarem.
161
+ // Quem distingue onboarding de contexto indisponivel e o servico.
126
162
  //
127
163
  // Nao e afrouxamento: o que passa aqui e so o que CONSTROI o contexto obrigatorio, e
128
164
  // so enquanto ele nao existe. Na primeira politica gravada, este caminho fecha sozinho.
165
+ const revisao = await revisaoDoContexto(raiz);
129
166
  const r = await buscaPolitica(raiz).catch(() => null);
130
167
  if (ONBOARDING.test(c.ferramenta) && r?.estado === "sem-politica")
131
168
  return saidaDaGuarda(host);
132
- // O briefing acabou de ser gravado NESTA sessao: o servico ja tem os dois, mas o boot
133
- // desta sessao rodou quando nao havia nada, e o estado local ficou velho. O agente sai
134
- // do onboarding direto para um segundo bloqueio, sem nada indicando o que fazer —
135
- // medido no dd-playwright, onde ele concluiu sozinho que precisava reiniciar e pediu
136
- // ao usuario.
137
- //
138
- // Continua NEGANDO, e de proposito: e o SessionStart que entrega politica e briefing ao
139
- // modelo, e liberar aqui daria trabalho sem o contexto que a guarda existe para
140
- // garantir. O que muda e a mensagem — quem le passa a saber que o projeto esta pronto e
141
- // que falta so reabrir, em vez de procurar o que ainda estaria faltando.
142
- if (r?.estado === "ok") {
169
+ if (r?.estado === "ok" && (anterior?.estado === "ok" || anterior?.estado === "sem-politica")) {
170
+ if (revisao !== await revisaoDoContexto(raiz)) {
171
+ return saidaDaGuarda(host, "dd-harness: o contexto mudou durante a atualização. Repita a chamada para carregar a versão atual.");
172
+ }
173
+ await guardaCache(raiz, { memorias: r.memorias ?? [] });
174
+ e = { identidade: anterior.identidade, revisao, ciclo: randomUUID(), estado: "ok", em: Date.now() };
175
+ contextoAtualizado = "dd-harness: política e briefing atualizados nesta sessão. Este contexto substitui as versões anteriores:\n\n" +
176
+ r.conteudo + blocoDeSessao(r.roadmap);
177
+ }
178
+ else if (r?.estado === "ok") {
143
179
  return saidaDaGuarda(host, "dd-harness: política e briefing JÁ EXISTEM no serviço, mas o boot " +
144
- "desta sessão rodou antes de eles existirem — por isso o contexto não chegou ao modelo. " +
180
+ "desta sessão não foi validado — por isso o contexto não chegou ao modelo. " +
145
181
  "Nada falta gravar: **reabra a sessão** para o boot carregá-los. " +
146
182
  "Enquanto isso, leitura (Read/Glob/Grep) e as ferramentas de leitura do dd-harness continuam liberadas.");
147
183
  }
148
- return saidaDaGuarda(host, "dd-harness: política e briefing não foram carregados nesta sessão/projeto. " +
149
- "Se este projeto é novo, conduza o briefing e grave os dois com `escrever_artefato`; " +
150
- "se não, reabra a sessão para o boot rodar.");
184
+ else if (r?.estado === "inalcancavel") {
185
+ return saidaDaGuarda(host, `dd-harness: não foi possível atualizar política e briefing: ${r.motivo}. Confira a conexão e repita a chamada.`);
186
+ }
187
+ else {
188
+ return saidaDaGuarda(host, "dd-harness: política e briefing não foram carregados nesta sessão/projeto. " +
189
+ "Se este projeto é novo, conduza o briefing e grave os dois com `escrever_artefato`; " +
190
+ "se não, confira a conexão e reabra a sessão para o boot rodar.");
191
+ }
151
192
  }
152
- const edicoes = edicoesDaChamada(c);
153
- const avisos = [];
193
+ const avisos = contextoAtualizado ? [contextoAtualizado] : [];
154
194
  if (edicoes.length && !await leCache(raiz)) {
155
195
  const r = await buscaPolitica(raiz);
156
196
  if (r.estado !== "ok") {
@@ -167,5 +207,11 @@ export async function guardaSessao(host, entrada) {
167
207
  if (r.hookSpecificOutput.additionalContext)
168
208
  avisos.push(r.hookSpecificOutput.additionalContext);
169
209
  }
210
+ if (contextoAtualizado) {
211
+ if (e.revisao !== await revisaoDoContexto(raiz)) {
212
+ return saidaDaGuarda(host, "dd-harness: o contexto mudou durante a atualização. Repita a chamada para carregar a versão atual.");
213
+ }
214
+ await escreveAtomico(arquivo(raiz, c.sessao, host), JSON.stringify(e));
215
+ }
170
216
  return saidaDaGuarda(host, undefined, avisos.join("\n\n"));
171
217
  }
package/dist/skill.d.ts CHANGED
@@ -31,6 +31,11 @@ export declare function leSkill(raiz: string, slug: string): Promise<SkillComple
31
31
  export declare function criaSkill(raiz: string, dados: NovaSkillInicial & Partial<Pick<Skill, "so_por_comando" | "ferramentas" | "caminhos" | "dica_de_argumento">>): Promise<{
32
32
  skill: Skill;
33
33
  }>;
34
+ /** Só o que muda: o que não vier fica como está no serviço. */
35
+ export type EdicaoDeSkill = Partial<Pick<SkillCompleta, "descricao" | "conteudo" | "so_por_comando" | "ferramentas" | "caminhos" | "dica_de_argumento">>;
36
+ export declare function editaSkill(raiz: string, slug: string, dados: EdicaoDeSkill): Promise<{
37
+ skill: Skill;
38
+ }>;
34
39
  /**
35
40
  * O SKILL.md que vai para o disco: frontmatter de verdade, corpo que aponta para o MCP.
36
41
  *
package/dist/skill.js CHANGED
@@ -38,6 +38,17 @@ dados) {
38
38
  await recusa(resposta);
39
39
  return (await jsonDaEscrita(resposta));
40
40
  }
41
+ export async function editaSkill(raiz, slug, dados) {
42
+ const { config, token } = await credencial(raiz);
43
+ const resposta = await pede(`${config.api}/api/v1/skills/${encodeURIComponent(slug)}`, {
44
+ method: "PATCH",
45
+ headers: cabecalhos(token, true),
46
+ body: JSON.stringify({ tenant: config.tenant, projeto: config.projeto, ...dados }),
47
+ });
48
+ if (!resposta.ok)
49
+ await recusa(resposta);
50
+ return (await jsonDaEscrita(resposta));
51
+ }
41
52
  // --- o ponteiro em disco (puro, para ser testado sem rede) ---
42
53
  /**
43
54
  * Escapa o que quebraria o YAML do frontmatter.