@tavaressan/vetor 0.1.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.
Files changed (78) hide show
  1. package/README.md +42 -0
  2. package/bin/vetor.js +6 -0
  3. package/lib/banner.js +35 -0
  4. package/lib/commands/install.js +71 -0
  5. package/lib/commands/status.js +59 -0
  6. package/lib/commands/uninstall.js +119 -0
  7. package/lib/commands/update.js +63 -0
  8. package/lib/installer/command-exists.js +30 -0
  9. package/lib/installer/cursor-hooks.js +181 -0
  10. package/lib/installer/detector.js +79 -0
  11. package/lib/installer/manifest.js +76 -0
  12. package/lib/installer/prompts.js +97 -0
  13. package/lib/installer/writer.js +382 -0
  14. package/lib/router.js +50 -0
  15. package/package.json +39 -0
  16. package/templates/.gitkeep +0 -0
  17. package/templates/agents/code-review/agent.json +27 -0
  18. package/templates/agents/code-review/codex.toml +37 -0
  19. package/templates/agents/code-review.md +99 -0
  20. package/templates/agents/issue-worker/agent.json +33 -0
  21. package/templates/agents/issue-worker/codex.toml +57 -0
  22. package/templates/agents/issue-worker.md +112 -0
  23. package/templates/hooks/hooks-codex.json +48 -0
  24. package/templates/hooks/hooks.json +62 -0
  25. package/templates/opencode/agent/code-review.md +73 -0
  26. package/templates/opencode/agent/issue-coordinator.md +521 -0
  27. package/templates/opencode/agent/issue-worker.md +64 -0
  28. package/templates/opencode/mcp.jsonc +39 -0
  29. package/templates/opencode/plugin/vetor.ts +207 -0
  30. package/templates/opencode/scripts/agent-registration_test.ts +92 -0
  31. package/templates/opencode/scripts/check-edit.ts +147 -0
  32. package/templates/opencode/scripts/ensure-external-directory-permission.ts +110 -0
  33. package/templates/opencode/scripts/ensure-external-directory-permission_test.ts +142 -0
  34. package/templates/opencode/scripts/lib/guard.ts +45 -0
  35. package/templates/opencode/scripts/lib/model-health.ts +133 -0
  36. package/templates/opencode/scripts/lib/model-health_test.ts +181 -0
  37. package/templates/opencode/scripts/lib/project.ts +240 -0
  38. package/templates/opencode/scripts/lib/project_test.ts +45 -0
  39. package/templates/opencode/scripts/lib/status.ts +69 -0
  40. package/templates/opencode/scripts/lib/worktree.ts +41 -0
  41. package/templates/opencode/scripts/model-health.ts +50 -0
  42. package/templates/opencode/scripts/model-health_test.ts +80 -0
  43. package/templates/opencode/scripts/resolve-model.ts +112 -0
  44. package/templates/opencode/scripts/resolve-model_test.ts +185 -0
  45. package/templates/opencode/scripts/safety-check.ts +203 -0
  46. package/templates/opencode/scripts/vetor-checks.sh +217 -0
  47. package/templates/opencode/scripts/vetor-status.sh +99 -0
  48. package/templates/skills/architecture-review/SKILL.md +187 -0
  49. package/templates/skills/backlog-ideator/SKILL.md +277 -0
  50. package/templates/skills/design/SKILL.md +468 -0
  51. package/templates/skills/design/examples/design-contract-example.md +46 -0
  52. package/templates/skills/design/examples/prototype-handoff-example.md +142 -0
  53. package/templates/skills/fix-loop-agent/SKILL.md +255 -0
  54. package/templates/skills/guardian/SKILL.md +343 -0
  55. package/templates/skills/issue-coordinator/SKILL.md +596 -0
  56. package/templates/skills/retro/SKILL.md +156 -0
  57. package/templates/skills/shared/references/agent-status.template.md +68 -0
  58. package/templates/skills/shared/references/codebase-design-vocabulary.md +54 -0
  59. package/templates/skills/shared/references/conflict-resolution.md +94 -0
  60. package/templates/skills/shared/references/delegate-to-runtime.md +239 -0
  61. package/templates/skills/shared/references/design-vocabulary.md +508 -0
  62. package/templates/skills/shared/references/evidence-state.md +365 -0
  63. package/templates/skills/shared/references/frontend-design-enforcement.md +33 -0
  64. package/templates/skills/shared/references/grilling-conventions.md +64 -0
  65. package/templates/skills/shared/references/knowledge-provider-contract.md +150 -0
  66. package/templates/skills/shared/references/mcp-availability.md +104 -0
  67. package/templates/skills/shared/references/module-test-map.template.md +72 -0
  68. package/templates/skills/shared/references/planning-conventions.md +97 -0
  69. package/templates/skills/shared/references/project-conventions.md +63 -0
  70. package/templates/skills/shared/references/tdd-conventions.md +81 -0
  71. package/templates/skills/shared/references/touched-files-cache.md +30 -0
  72. package/templates/skills/spec/SKILL.md +524 -0
  73. package/templates/skills/spec-validate/SKILL.md +195 -0
  74. package/templates/skills/spec-validate/references/traceability.md +169 -0
  75. package/templates/skills/stack-practices/SKILL.md +151 -0
  76. package/templates/skills/vetor/SKILL.md +174 -0
  77. package/templates/skills/worktree-create/SKILL.md +142 -0
  78. package/templates/skills/worktree-ship/SKILL.md +394 -0
@@ -0,0 +1,185 @@
1
+ // Teste de ponta-a-ponta do CLI (issue #84): com uma entrada `degraded` sintética em
2
+ // model-health.json para o modelo preferencial, confirma que a escolha cai para o próximo
3
+ // modelo/provedor saudável da lista de fallback — e que, com todos degraded, o script sai com
4
+ // código 1 (o issue-coordinator interpreta isso como "mantenha o grupo QUEUED").
5
+
6
+ import { assertEquals, assertStringIncludes } from "@std/assert";
7
+
8
+ const SCRIPT = new URL("./resolve-model.ts", import.meta.url).pathname;
9
+
10
+ async function git(args: string[], cwd: string): Promise<void> {
11
+ await new Deno.Command("git", { args, cwd, stdout: "null", stderr: "null" }).output();
12
+ }
13
+
14
+ async function makeRepo(): Promise<string> {
15
+ const dir = await Deno.makeTempDir();
16
+ await git(["init", "-q", "-b", "main"], dir);
17
+ await git(["config", "user.email", "test@example.com"], dir);
18
+ await git(["config", "user.name", "Test"], dir);
19
+ await git(["commit", "-q", "--allow-empty", "-m", "init"], dir);
20
+ return dir;
21
+ }
22
+
23
+ async function writeModelHealth(repo: string, content: unknown): Promise<void> {
24
+ const dir = `${repo}/.claude/vetor/status`;
25
+ await Deno.mkdir(dir, { recursive: true });
26
+ await Deno.writeTextFile(`${dir}/model-health.json`, JSON.stringify(content));
27
+ }
28
+
29
+ async function runCli(
30
+ input: unknown,
31
+ ): Promise<{ code: number; stdout: string; stderr: string }> {
32
+ const command = new Deno.Command("deno", {
33
+ args: ["run", "-A", SCRIPT],
34
+ stdin: "piped",
35
+ stdout: "piped",
36
+ stderr: "piped",
37
+ });
38
+ const child = command.spawn();
39
+ const writer = child.stdin.getWriter();
40
+ await writer.write(new TextEncoder().encode(JSON.stringify(input)));
41
+ await writer.close();
42
+ const out = await child.output();
43
+ return {
44
+ code: out.code,
45
+ stdout: new TextDecoder().decode(out.stdout).trim(),
46
+ stderr: new TextDecoder().decode(out.stderr).trim(),
47
+ };
48
+ }
49
+
50
+ Deno.test("resolve-model CLI - sem model-health.json escolhe o primeiro da lista", async () => {
51
+ const repo = await makeRepo();
52
+ const result = await runCli({
53
+ fallback: ["anthropic/claude-haiku-4-5", "anthropic/claude-sonnet-4-5"],
54
+ cwd: repo,
55
+ });
56
+ assertEquals(result.code, 0);
57
+ assertEquals(result.stdout, "anthropic/claude-haiku-4-5");
58
+ });
59
+
60
+ Deno.test("resolve-model CLI - preferencial degraded (sintético) cai para o próximo", async () => {
61
+ const repo = await makeRepo();
62
+ await writeModelHealth(repo, {
63
+ "anthropic/claude-haiku-4-5": {
64
+ status: "degraded",
65
+ until: Date.now() + 60_000,
66
+ lastError: "HTTP 429",
67
+ },
68
+ });
69
+
70
+ const result = await runCli({
71
+ fallback: ["anthropic/claude-haiku-4-5", "anthropic/claude-sonnet-4-5"],
72
+ cwd: repo,
73
+ });
74
+
75
+ assertEquals(result.code, 0);
76
+ assertEquals(result.stdout, "anthropic/claude-sonnet-4-5");
77
+ });
78
+
79
+ Deno.test("resolve-model CLI - todos degraded sai com código 1 e sem stdout", async () => {
80
+ const repo = await makeRepo();
81
+ const until = Date.now() + 60_000;
82
+ await writeModelHealth(repo, {
83
+ "anthropic/claude-haiku-4-5": { status: "degraded", until, lastError: "HTTP 429" },
84
+ "anthropic/claude-sonnet-4-5": { status: "degraded", until, lastError: "HTTP 429" },
85
+ });
86
+
87
+ const result = await runCli({
88
+ fallback: ["anthropic/claude-haiku-4-5", "anthropic/claude-sonnet-4-5"],
89
+ cwd: repo,
90
+ });
91
+
92
+ assertEquals(result.code, 1);
93
+ assertEquals(result.stdout, "");
94
+ assertStringIncludes(result.stderr, "degraded");
95
+ });
96
+
97
+ // Issue #307: no Git Bash do Windows, `$(pwd)` produz um path POSIX-style (`/c/Users/...`), não
98
+ // nativo (`C:\Users\...`). `resolveWorktree()` roda `git` com esse cwd via `Deno.Command`, que no
99
+ // Windows falha ao spawnar (`Failed to spawn ... No such cwd`) para paths desse formato — cai
100
+ // silenciosamente no `DEFAULT_MODEL_FALLBACK` embutido em vez do `config.json` real.
101
+ function toPosixPath(nativePath: string): string {
102
+ return nativePath.replace(/^([A-Za-z]):\\/, (_, d) => `/${d.toLowerCase()}/`).replaceAll(
103
+ "\\",
104
+ "/",
105
+ );
106
+ }
107
+
108
+ Deno.test({
109
+ name: "resolve-model CLI - resolve config.json real mesmo com cwd POSIX-style (Git Bash Windows)",
110
+ ignore: Deno.build.os !== "windows",
111
+ fn: async () => {
112
+ const repo = await makeRepo();
113
+ await Deno.mkdir(`${repo}/.claude/vetor`, { recursive: true });
114
+ await Deno.writeTextFile(
115
+ `${repo}/.claude/vetor/config.json`,
116
+ JSON.stringify({
117
+ modelFallback: { simple: ["openai/gpt-5-mini", "anthropic/claude-haiku-4-5"] },
118
+ }),
119
+ );
120
+
121
+ const result = await runCli({ tier: "simple", cwd: toPosixPath(repo) });
122
+
123
+ assertEquals(result.code, 0);
124
+ assertEquals(result.stdout, "openai/gpt-5-mini");
125
+ },
126
+ });
127
+
128
+ Deno.test("resolve-model CLI - usa tier de config.json quando fallback não é passado", async () => {
129
+ const repo = await makeRepo();
130
+ await Deno.mkdir(`${repo}/.claude/vetor`, { recursive: true });
131
+ await Deno.writeTextFile(
132
+ `${repo}/.claude/vetor/config.json`,
133
+ JSON.stringify({
134
+ modelFallback: { simple: ["openai/gpt-5-mini", "anthropic/claude-haiku-4-5"] },
135
+ }),
136
+ );
137
+
138
+ const result = await runCli({ tier: "simple", cwd: repo });
139
+
140
+ assertEquals(result.code, 0);
141
+ assertEquals(result.stdout, "openai/gpt-5-mini");
142
+ });
143
+
144
+ // Issue #312: o default embutido anterior assumia provider `anthropic` direto
145
+ // (`anthropic/claude-haiku-4-5`), que falha com "Unexpected server error" do opencode real em
146
+ // qualquer ambiente configurado só com outro provider (ex.: OpenRouter) — sem heurística confiável
147
+ // para detectar o provider certo, a escolha (opção b da issue) é falhar cedo com mensagem acionável
148
+ // em vez de devolver um modelo que pode não existir no ambiente do usuário.
149
+ //
150
+ // Código de saída 2, não 1: "não configurado" é um erro de configuração permanente, diferente de
151
+ // "todos degraded" (transitório, código 1, que o coordinator interpreta como "mantenha QUEUED e
152
+ // tente de novo"). Se "não configurado" também saísse com 1, o coordinator ficaria retentando um
153
+ // grupo indefinidamente sem nunca mostrar a mensagem acionável — pior que o erro genérico que
154
+ // substituiu.
155
+ Deno.test(
156
+ "resolve-model CLI - sem config.json e sem fallback explícito falha cedo (código 2) com mensagem acionável",
157
+ async () => {
158
+ const repo = await makeRepo();
159
+
160
+ const result = await runCli({ tier: "simple", cwd: repo });
161
+
162
+ assertEquals(result.code, 2);
163
+ assertEquals(result.stdout, "");
164
+ assertStringIncludes(result.stderr, "modelFallback.simple");
165
+ assertStringIncludes(result.stderr, "config.json");
166
+ },
167
+ );
168
+
169
+ Deno.test(
170
+ "resolve-model CLI - config.json presente mas sem modelFallback para o tier falha cedo (código 2)",
171
+ async () => {
172
+ const repo = await makeRepo();
173
+ await Deno.mkdir(`${repo}/.claude/vetor`, { recursive: true });
174
+ await Deno.writeTextFile(
175
+ `${repo}/.claude/vetor/config.json`,
176
+ JSON.stringify({ runtime: "deno" }),
177
+ );
178
+
179
+ const result = await runCli({ tier: "complex", cwd: repo });
180
+
181
+ assertEquals(result.code, 2);
182
+ assertEquals(result.stdout, "");
183
+ assertStringIncludes(result.stderr, "modelFallback.complex");
184
+ },
185
+ );
@@ -0,0 +1,203 @@
1
+ // Safety gate do Vetor para o Claude Code (hook PreToolUse, matcher Bash|Edit|Write).
2
+ //
3
+ // Substitui scripts/cc-safety-hook.sh, que dependia de `python3` no PATH — ausente no
4
+ // Windows, onde o interpretador costuma se chamar `python`. Sob `set -euo pipefail`
5
+ // aquele hook morria em silêncio e o gate não era aplicado.
6
+ //
7
+ // Contrato do Claude Code: recebe o evento em JSON no stdin; exit 2 BLOQUEIA a chamada.
8
+ // Espelha a política de scripts/safety-check.sh, que segue servindo ao Antigravity —
9
+ // exceto pelo guard de escrita, que o Antigravity não tem como aplicar (lá o matcher só
10
+ // cobre run_command).
11
+ //
12
+ // Três políticas:
13
+ // Worktree — dentro de um worktree, o cwd precisa estar em .claude/worktrees/ da raiz
14
+ // e continuar registrado em `git worktree list` (não pode ser stale).
15
+ // Bash — não empurrar para branch protegida; não fazer push/PR de worker não-GREEN.
16
+ // Edit/Write — dentro de um worktree, não escrever fora dele; um agente vetor:issue-worker
17
+ // nunca deveria escrever com cwd resolvendo para a raiz do projeto (fora de
18
+ // qualquer worktree linkado) — se acontecer, é sinal de cwd mal resolvido pelo
19
+ // harness, não uma escrita legítima (ver issue #57).
20
+ // Binding — segunda camada, independente do guard acima: com múltiplos workers em
21
+ // paralelo (ver issue #63), o cwd recebido no payload pode contaminar entre
22
+ // subagentes — o cwd resolve para um worktree real, só que de OUTRO worker
23
+ // ativo na mesma sessão. `isLinked` sozinho não pega esse caso. Aqui
24
+ // correlacionamos `agent_id` (estável por instância de subagente, diferente
25
+ // de `agent_type`) com o worktree resolvido na primeira chamada; uma mudança
26
+ // de worktree para o mesmo agent_id é bloqueada.
27
+
28
+ import { isWriteAllowed } from "./lib/guard.ts";
29
+ import { run } from "./lib/project.ts";
30
+ import {
31
+ agentBindingPath,
32
+ readStatus,
33
+ resolveWorktree,
34
+ statusFilePath,
35
+ type WorktreeInfo,
36
+ } from "./lib/status.ts";
37
+ import { evaluateFreshness } from "./lib/worktree.ts";
38
+
39
+ const PROTECTED_BRANCHES = ["main", "master", "production"];
40
+
41
+ interface HookInput {
42
+ tool_name?: string;
43
+ tool_input?: { command?: string; file_path?: string };
44
+ cwd?: string;
45
+ /** Presente quando o hook dispara dentro de um subagente (ex.: "vetor:issue-worker"). */
46
+ agent_type?: string;
47
+ /** Identificador único da instância do subagente — estável entre chamadas, ao contrário de agent_type. */
48
+ agent_id?: string;
49
+ }
50
+
51
+ function blocked(message: string): never {
52
+ console.error(message);
53
+ Deno.exit(2);
54
+ }
55
+
56
+ /** Extrai a branch de destino de um `git push [flags] [remote] <branch>[:<remote-branch>]`. */
57
+ function pushDestination(command: string): string | null {
58
+ const push = command.match(/git push[^&|;]*/)?.[0];
59
+ if (!push) return null;
60
+
61
+ const last = push.trim().split(/\s+/).pop();
62
+ if (!last || last.startsWith("-")) return null;
63
+ return last.split(":")[0];
64
+ }
65
+
66
+ /**
67
+ * Só se aplica dentro de um worktree linkado (`wt.isLinked`): a raiz do repositório
68
+ * principal não tem essa restrição, senão o próprio uso legítimo do hook lá quebraria.
69
+ */
70
+ async function checkFreshness(wt: WorktreeInfo): Promise<void> {
71
+ const list = await run("git", ["worktree", "list", "--porcelain"], wt.root);
72
+ const message = evaluateFreshness(wt.toplevel, wt.root, list.stdout);
73
+ if (message) blocked(message);
74
+ }
75
+
76
+ function checkBash(command: string, wt: WorktreeInfo | null): void {
77
+ const dest = pushDestination(command);
78
+ if (dest && PROTECTED_BRANCHES.includes(dest)) {
79
+ blocked(
80
+ "ERROR: Push to protected branches (main, master, production) is prohibited by Vetor Safety Hook.",
81
+ );
82
+ }
83
+
84
+ if (!/git push|gh pr (create|ready|merge)/.test(command)) return;
85
+ if (!wt?.isLinked) return;
86
+
87
+ const status = readStatus(statusFilePath(wt.root, wt.branch));
88
+ // Sem status file, a branch não pertence ao fluxo do coordinator: não interfere.
89
+ if (status === null) return;
90
+
91
+ if (status !== "GREEN") {
92
+ blocked(
93
+ `ERROR: worker não-GREEN (Status: ${
94
+ status || "desconhecido"
95
+ }) — push/PR bloqueado pelo Vetor Safety Hook.\n` +
96
+ "Registre BLOCKED_WAITING no status file se precisar de intervenção; o worktree-ship faz a entrega após GREEN.",
97
+ );
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Segunda camada de defesa contra cwd contaminado (issue #63): sem `agent_id` não há como
103
+ * correlacionar, então não se aplica — não é regressão, é a mesma cobertura de antes.
104
+ * Na primeira chamada de um `agent_id`, grava o worktree resolvido; em chamadas seguintes,
105
+ * uma mudança de worktree para o MESMO agent_id indica cwd contaminado — bloqueia.
106
+ */
107
+ function checkAgentBinding(root: string, agentId: string | undefined, toplevel: string): void {
108
+ if (!agentId) return;
109
+
110
+ const path = agentBindingPath(root, agentId);
111
+ let bound: string | null = null;
112
+ try {
113
+ bound = Deno.readTextFileSync(path).trim();
114
+ } catch {
115
+ // Primeira chamada deste agent_id: nada gravado ainda.
116
+ }
117
+
118
+ if (bound && bound !== toplevel) {
119
+ blocked(
120
+ `ERROR: cwd contaminado detectado pelo Vetor Safety Hook (issue #63): agent_id ${agentId} ` +
121
+ `já estava vinculado ao worktree ${bound}, mas este evento resolveu para ${toplevel}.\n` +
122
+ "Isso indica cwd entregue incorretamente pelo harness durante execução paralela de " +
123
+ "subagentes — a escrita foi bloqueada para não vazar entre workers.",
124
+ );
125
+ }
126
+
127
+ if (!bound) {
128
+ try {
129
+ Deno.mkdirSync(`${root}/.claude/vetor/status/.agent-cwd`, { recursive: true });
130
+ Deno.writeTextFileSync(path, `${toplevel}\n`);
131
+ } catch {
132
+ // Sem conseguir gravar o vínculo, a checagem principal (isWriteAllowed) já cobre
133
+ // a política de escrita — a ausência do vínculo só reduz a segunda camada.
134
+ }
135
+ }
136
+ }
137
+
138
+ async function checkWrite(
139
+ filePath: string,
140
+ cwd: string,
141
+ agentType?: string,
142
+ agentId?: string,
143
+ ): Promise<void> {
144
+ const wt = await resolveWorktree(cwd);
145
+
146
+ if (!wt?.isLinked) {
147
+ // Um vetor:issue-worker deveria estar sempre dentro do seu worktree isolado. cwd
148
+ // resolvendo para fora de um worktree linkado (ex.: a raiz do projeto) indica cwd
149
+ // incorreto entregue pelo harness — deixar passar contamina a raiz compartilhada por
150
+ // todos os workers em paralelo (reprodução real: issue #57).
151
+ if (agentType === "vetor:issue-worker") {
152
+ blocked(
153
+ `ERROR: vetor:issue-worker escrevendo com cwd fora de um worktree linkado: ${filePath}\n` +
154
+ `cwd resolvido: ${cwd}${wt ? ` (branch: ${wt.branch})` : ""}. Um issue-worker só ` +
155
+ "deveria escrever dentro do seu próprio worktree — bloqueado pelo Vetor Safety Hook.",
156
+ );
157
+ }
158
+ return;
159
+ }
160
+
161
+ checkAgentBinding(wt.root, agentId, wt.toplevel);
162
+
163
+ if (!isWriteAllowed(filePath, wt.toplevel, wt.root)) {
164
+ blocked(
165
+ `ERROR: escrita fora do worktree bloqueada pelo Vetor Safety Hook: ${filePath}\n` +
166
+ `O worker só escreve dentro de ${wt.toplevel} (e no seu status file). Editar a raiz ` +
167
+ "contamina os demais workers em paralelo.",
168
+ );
169
+ }
170
+ }
171
+
172
+ async function main() {
173
+ const raw = new TextDecoder().decode(await new Response(Deno.stdin.readable).arrayBuffer());
174
+
175
+ let input: HookInput;
176
+ try {
177
+ input = JSON.parse(raw);
178
+ } catch {
179
+ // Entrada ilegível não deve travar a sessão do usuário.
180
+ Deno.exit(0);
181
+ }
182
+
183
+ const cwd = input.cwd ?? Deno.cwd();
184
+ const wt = await resolveWorktree(cwd);
185
+
186
+ // A checagem de frescor só faz sentido dentro de um worktree linkado — na raiz do
187
+ // repositório principal (isLinked === false) o hook segue liberando normalmente.
188
+ if (wt?.isLinked) {
189
+ await checkFreshness(wt);
190
+ }
191
+
192
+ if (input.tool_name === "Edit" || input.tool_name === "Write") {
193
+ const filePath = input.tool_input?.file_path;
194
+ if (filePath) await checkWrite(filePath, cwd, input.agent_type, input.agent_id);
195
+ Deno.exit(0);
196
+ }
197
+
198
+ const command = input.tool_input?.command;
199
+ if (command) checkBash(command, wt);
200
+ Deno.exit(0);
201
+ }
202
+
203
+ await main();
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env bash
2
+ # Checagens determinísticas compartilhadas pelas skills do Vetor.
3
+ #
4
+ # Uso: vetor-checks.sh <subcomando> [args]
5
+ # default-branch imprime a branch default do repositório (nunca assume master)
6
+ # in-worktree exit 0 se o cwd é um worktree linkado; exit 1 se é o root
7
+ # migrations exit 1 se há versões de migration duplicadas (convenção Flyway)
8
+ # debug-scan <base-branch> exit 1 se o diff vs. a base contém padrões de debug/teste exclusivo
9
+ # validate-issue-ref <valor> exit 1 se valor não for inteiro positivo; exit 0 caso contrário
10
+ # safe-remove-worktree <path> remove o worktree somente se não houver worktree filho ativo
11
+ # sync-root tenta retornar o repositório principal para a branch default de forma segura
12
+ # worktree-audit lista worktrees linkados (exceto o root) com idade/tamanho/uncommitted
13
+ # find-orphan-status [dir] lista status files sem worktree correspondente (default: .claude/vetor/status)
14
+ # archive-orphan-status <path> move um status file órfão para <dir>/archive/
15
+ #
16
+ # Exit codes: 0 = passou; 1 = checagem falhou (a skill deve parar e mostrar a saída); 2 = uso incorreto.
17
+
18
+ set -uo pipefail
19
+
20
+ cmd="${1:-}"
21
+
22
+ case "$cmd" in
23
+ default-branch)
24
+ DEFAULT_BRANCH=$(git symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')
25
+ [ -z "$DEFAULT_BRANCH" ] && DEFAULT_BRANCH=$(git remote show origin 2>/dev/null | sed -n '/HEAD branch/s/.*: //p')
26
+ [ -z "$DEFAULT_BRANCH" ] && DEFAULT_BRANCH=master
27
+ echo "$DEFAULT_BRANCH"
28
+ ;;
29
+
30
+ in-worktree)
31
+ # Contrato estável para uso EXTERNO ao plugin (issue #129): hooks/scripts de projetos
32
+ # consumidores podem chamar `vetor-checks.sh in-worktree` diretamente para decidir se o
33
+ # cwd é um worktree linkado (exit 0) ou o repositório principal (exit 1), sem stdout.
34
+ # Não reimplemente essa comparação com `pwd` vs `git worktree list` — os formatos de path
35
+ # nunca coincidem no Windows/Git Bash, o que faz a comparação ingênua concluir "worktree"
36
+ # mesmo estando no root.
37
+ git_dir=$(git rev-parse --git-dir 2>/dev/null) || { echo "não é um repositório git" >&2; exit 1; }
38
+ common_dir=$(git rev-parse --git-common-dir 2>/dev/null)
39
+ if [ "$(cd "$git_dir" && pwd)" = "$(cd "$common_dir" && pwd)" ]; then
40
+ echo "cwd é o root do repositório, não um worktree linkado" >&2
41
+ exit 1
42
+ fi
43
+ ;;
44
+
45
+ migrations)
46
+ # Colisão semântica invisível ao git: dois arquivos com a mesma versão (V13__a.sql, V13__b.sql).
47
+ dups=$(git ls-files '*/db/migration/V*__*.sql' \
48
+ | sed -E 's#.*/V([0-9]+)__.*#\1#' | sort | uniq -d)
49
+ if [ -n "$dups" ]; then
50
+ echo "FALHA: colisão de versão de migration — versões duplicadas:" >&2
51
+ for v in $dups; do
52
+ echo " V$v:" >&2
53
+ git ls-files "*/db/migration/V${v}__*.sql" | sed 's/^/ /' >&2
54
+ done
55
+ echo "Renumere a migration deste worker para a próxima versão livre antes de prosseguir." >&2
56
+ exit 1
57
+ fi
58
+ ;;
59
+
60
+ debug-scan)
61
+ base="${2:?uso: vetor-checks.sh debug-scan <base-branch>}"
62
+ # Grep only added lines from diff (lines starting with +), not the entire file content.
63
+ # The caller MUST pass origin/$DEFAULT_BRANCH (not local $DEFAULT_BRANCH) to avoid
64
+ # Stale branch references in worktrees (issue #70).
65
+ # Exclude script/test files that define or test the regex to avoid false positives (issue #108, #109)
66
+ hits=$(git diff "$base" -U0 -- '*.ts' '*.sh' '*.js' '*.tsx' '*.jsx' \
67
+ ':!scripts/vetor-checks.sh' ':!scripts/tests/vetor-checks_test.ts' ':!skills/**/*.md' \
68
+ ':!.opencode/scripts/vetor-checks.sh' ':!.opencode/skills/**/*.md' \
69
+ ':!opencode/scripts/vetor-checks.sh' ':!opencode/skills/**/*.md' 2>/dev/null \
70
+ | grep -E '^\+' | grep -vE '^\+\+\+' \
71
+ | grep -nE 'console\.log|var_dump|fit\(|fdescribe\(|it\.only' 2>/dev/null || true)
72
+ if [ -n "$hits" ]; then
73
+ echo "FALHA: padrões de debug/teste exclusivo no diff (remova antes do push):" >&2
74
+ echo "$hits" >&2
75
+ exit 1
76
+ fi
77
+ ;;
78
+
79
+ validate-issue-ref)
80
+ valor="${2:?uso: vetor-checks.sh validate-issue-ref <valor>}"
81
+ if ! [[ "$valor" =~ ^[1-9][0-9]*$ ]]; then
82
+ echo "ERRO: issue# deve ser um inteiro positivo, recebido \"$valor\"" >&2
83
+ exit 1
84
+ fi
85
+ ;;
86
+
87
+ safe-remove-worktree)
88
+ target="${2:?uso: vetor-checks.sh safe-remove-worktree <path>}"
89
+ target=$(cd "$target" && pwd -P) || {
90
+ echo "ERRO: worktree para cleanup não encontrado: $target" >&2
91
+ exit 1
92
+ }
93
+
94
+ children=()
95
+ while IFS= read -r line; do
96
+ case "$line" in
97
+ "worktree "*)
98
+ candidate="${line#worktree }"
99
+ candidate=$(cd "$candidate" 2>/dev/null && pwd -P) || continue
100
+ case "$candidate" in
101
+ "$target"/*) children+=("$candidate") ;;
102
+ esac
103
+ ;;
104
+ esac
105
+ done < <(git worktree list --porcelain)
106
+
107
+ if [ "${#children[@]}" -gt 0 ]; then
108
+ echo "FALHA: cleanup bloqueado; o worktree $target contém worktree(s) ativo(s):" >&2
109
+ printf ' %s\n' "${children[@]}" >&2
110
+ echo "Remova ou realoque os worktrees filhos antes de remover o pai." >&2
111
+ exit 1
112
+ fi
113
+
114
+ git worktree remove "$target"
115
+ ;;
116
+
117
+ sync-root)
118
+ # Tenta retornar a raiz do repo para a branch default se a branch atual estiver limpa
119
+ # e sem commits locais pendentes vs remote (ou sem remote tracker caso já deletada).
120
+ ROOT=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null | xargs dirname)
121
+ [ -z "$ROOT" ] && exit 0
122
+ cd "$ROOT" || exit 0
123
+
124
+ DEFAULT_BRANCH=$("$0" default-branch)
125
+ current=$(git branch --show-current 2>/dev/null)
126
+ [ -z "$current" ] || [ "$current" = "$DEFAULT_BRANCH" ] && exit 0
127
+
128
+ # 1. Verifica se tem uncommitted changes
129
+ if ! git diff-index --quiet HEAD --; then
130
+ echo "AVISO: root tem mudanças pendentes na branch $current. Não mudando para $DEFAULT_BRANCH." >&2
131
+ exit 0
132
+ fi
133
+
134
+ # 2. Verifica se a branch tem commits que não estão na default
135
+ if ! git merge-base --is-ancestor HEAD "origin/$DEFAULT_BRANCH" 2>/dev/null; then
136
+ echo "AVISO: root está na branch $current que possui commits não integrados em origin/$DEFAULT_BRANCH. Não mudando para $DEFAULT_BRANCH." >&2
137
+ exit 0
138
+ fi
139
+
140
+ git checkout "$DEFAULT_BRANCH" >/dev/null 2>&1
141
+ git pull origin "$DEFAULT_BRANCH" >/dev/null 2>&1
142
+ echo "Root sincronizado com $DEFAULT_BRANCH (branch anterior: $current estava limpa e mesclada)."
143
+ ;;
144
+
145
+ worktree-audit)
146
+ # Emite uma linha por worktree linkado (exclui o root) no formato:
147
+ # <path>|<branch>|<age_days>|<size_kb>|<uncommitted:yes/no>
148
+ # "age_days" é medido a partir do timestamp do último commit do worktree (proxy de
149
+ # staleness — evita depender de mtime de diretório, que muda a qualquer escrita).
150
+ main_worktree=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null | xargs dirname)
151
+ now_ts=$(date +%s)
152
+ path=""
153
+ branch=""
154
+ while IFS= read -r line; do
155
+ case "$line" in
156
+ "worktree "*)
157
+ path="${line#worktree }"
158
+ branch=""
159
+ ;;
160
+ "branch refs/heads/"*)
161
+ branch="${line#branch refs/heads/}"
162
+ ;;
163
+ "")
164
+ if [ -n "$path" ] && [ "$path" != "$main_worktree" ] && [ -d "$path" ]; then
165
+ last_commit_ts=$(git -C "$path" log -1 --format=%ct 2>/dev/null || echo "$now_ts")
166
+ age_days=$(( (now_ts - last_commit_ts) / 86400 ))
167
+ size_kb=$(du -sk "$path" 2>/dev/null | cut -f1)
168
+ if [ -n "$(git -C "$path" status --porcelain 2>/dev/null)" ]; then
169
+ uncommitted="yes"
170
+ else
171
+ uncommitted="no"
172
+ fi
173
+ echo "${path}|${branch:-detached}|${age_days}|${size_kb:-0}|${uncommitted}"
174
+ fi
175
+ path=""
176
+ branch=""
177
+ ;;
178
+ esac
179
+ done < <(git worktree list --porcelain; echo "")
180
+ ;;
181
+
182
+ find-orphan-status)
183
+ # Lista status files (`.claude/vetor/status/*.md`) cujo worktree correspondente não
184
+ # existe mais em `git worktree list`. Report-only — não move nem apaga nada.
185
+ status_dir="${2:-.claude/vetor/status}"
186
+ [ -d "$status_dir" ] || exit 0
187
+ active=$(git worktree list --porcelain | sed -n 's#^branch refs/heads/##p' | tr '/' '-')
188
+ for f in "$status_dir"/*.md; do
189
+ [ -e "$f" ] || continue
190
+ name=$(basename "$f" .md)
191
+ if ! printf '%s\n' "$active" | grep -qx "$name"; then
192
+ echo "$f"
193
+ fi
194
+ done
195
+ ;;
196
+
197
+ archive-orphan-status)
198
+ # Move um status file órfão para <dir>/archive/ (não apaga — recolhimento reversível).
199
+ target="${2:?uso: vetor-checks.sh archive-orphan-status <path-do-status-file>}"
200
+ [ -f "$target" ] || {
201
+ echo "ERRO: status file não encontrado: $target" >&2
202
+ exit 1
203
+ }
204
+ status_dir=$(dirname "$target")
205
+ archive_dir="$status_dir/archive"
206
+ mkdir -p "$archive_dir"
207
+ mv "$target" "$archive_dir/"
208
+ echo "Arquivado: $archive_dir/$(basename "$target")"
209
+ ;;
210
+
211
+ *)
212
+ echo "uso: vetor-checks.sh <default-branch|in-worktree|migrations|debug-scan <base-branch>|validate-issue-ref <valor>|safe-remove-worktree <path>|sync-root|worktree-audit|find-orphan-status [dir]|archive-orphan-status <path>>" >&2
213
+ exit 2
214
+ ;;
215
+ esac
216
+
217
+ exit 0