@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,207 @@
1
+ // Safety gate do Vetor para o OpenCode (hooks tool.execute.before / tool.execute.after).
2
+ //
3
+ // Ao contrário do Claude Code e do Codex, o OpenCode não tem hook declarativo em JSON —
4
+ // um plugin é código TS/JS que recebe (input, output) e bloqueia lançando Error. Também não
5
+ // existe `$CLAUDE_PLUGIN_ROOT`/`$PLUGIN_ROOT`: por isso este plugin resolve os scripts como
6
+ // caminho relativo a si mesmo (`../scripts/...`), dentro da mesma pasta `opencode/` copiada
7
+ // para o projeto-alvo — ver README, seção "Compatibilidade com OpenCode".
8
+ //
9
+ // A lógica de segurança em si NÃO foi duplicada: este plugin só traduz o payload do OpenCode
10
+ // (`{tool, sessionID, args, agent}`) para o formato que scripts/safety-check.ts e
11
+ // scripts/check-edit.ts já esperam no stdin (o mesmo usado pelo Claude Code e pelo Codex) e
12
+ // invoca os scripts Deno originais via `deno run -A`. Fonte única de verdade continua em
13
+ // scripts/lib/*.ts.
14
+ //
15
+ // Payload confirmado contra o SDK instalado (@opencode-ai/plugin, v1.18.4) em 2026-07-21 —
16
+ // diferente do que foi possível fazer para o Codex (só doc pública, sem CLI real disponível).
17
+
18
+ import { spawn } from "node:child_process";
19
+ import { fileURLToPath } from "node:url";
20
+ import { dirname, join } from "node:path";
21
+
22
+ const HERE = dirname(fileURLToPath(import.meta.url));
23
+ const SCRIPTS_DIR = join(HERE, "..", "scripts");
24
+
25
+ interface ToolExecuteBeforeInput {
26
+ tool: string;
27
+ sessionID: string;
28
+ callID: string;
29
+ agent?: string;
30
+ }
31
+
32
+ interface ToolExecuteBeforeOutput {
33
+ args: Record<string, unknown>;
34
+ }
35
+
36
+ /** Mesmo shape que scripts/safety-check.ts e scripts/check-edit.ts já esperam no stdin. */
37
+ interface HookInput {
38
+ tool_name?: string;
39
+ tool_input?: { command?: string; file_path?: string };
40
+ cwd?: string;
41
+ agent_type?: string;
42
+ agent_id?: string;
43
+ }
44
+
45
+ const TOOL_NAME_MAP: Record<string, string> = {
46
+ bash: "Bash",
47
+ edit: "Edit",
48
+ write: "Write",
49
+ };
50
+
51
+ /**
52
+ * agent_id do Claude Code é estável por instância de subagente (issue #63: cwd contaminado
53
+ * entre workers paralelos). No modelo recomendado para o OpenCode (ver README — cada worker
54
+ * é um processo `opencode run --dir <worktree>` isolado, não a tool `task` in-process), essa
55
+ * classe de bug não deveria ocorrer: cada processo já nasce com cwd fixo no seu worktree.
56
+ * Mesmo assim, sessionID identifica a sessão/processo de forma estável e serve como proxy
57
+ * de agent_id para reaproveitar a segunda camada de defesa do safety-check.ts sem custo.
58
+ */
59
+ function toHookInput(
60
+ input: ToolExecuteBeforeInput,
61
+ output: ToolExecuteBeforeOutput,
62
+ cwd: string,
63
+ ): HookInput {
64
+ const toolName = TOOL_NAME_MAP[input.tool];
65
+ const toolInput = toolName === "Bash"
66
+ ? { command: String(output.args.command ?? "") }
67
+ : { file_path: String(output.args.filePath ?? output.args.file_path ?? "") };
68
+
69
+ return {
70
+ tool_name: toolName,
71
+ tool_input: toolInput,
72
+ cwd,
73
+ agent_type: input.agent,
74
+ agent_id: input.sessionID,
75
+ };
76
+ }
77
+
78
+ function runDenoScript(
79
+ script: string,
80
+ payload: HookInput | Record<string, unknown>,
81
+ timeoutMs: number,
82
+ ): Promise<{ code: number; stdout: string; stderr: string }> {
83
+ return new Promise((resolve) => {
84
+ const child = spawn("deno", ["run", "-A", join(SCRIPTS_DIR, script)], {
85
+ stdio: ["pipe", "pipe", "pipe"],
86
+ });
87
+
88
+ let stdout = "";
89
+ let stderr = "";
90
+ const timer = setTimeout(() => child.kill(), timeoutMs);
91
+
92
+ child.stdout.on("data", (chunk) => (stdout += chunk));
93
+ child.stderr.on("data", (chunk) => (stderr += chunk));
94
+ child.on("close", (code) => {
95
+ clearTimeout(timer);
96
+ resolve({ code: code ?? 0, stdout, stderr });
97
+ });
98
+ child.on("error", () => {
99
+ clearTimeout(timer);
100
+ // deno ausente do PATH: mesmo pré-requisito documentado no README para as outras
101
+ // plataformas. Fail-open — não travar a sessão do usuário por dependência faltando.
102
+ resolve({ code: 0, stdout: "", stderr: "" });
103
+ });
104
+
105
+ child.stdin.write(JSON.stringify(payload));
106
+ child.stdin.end();
107
+ });
108
+ }
109
+
110
+ /** Payload mínimo de `ApiError` (@opencode-ai/sdk v1.18.4, dist/gen/types.gen.d.ts:86). */
111
+ interface ApiErrorLike {
112
+ name?: string;
113
+ data?: {
114
+ message?: string;
115
+ statusCode?: number;
116
+ isRetryable?: boolean;
117
+ responseHeaders?: Record<string, string>;
118
+ };
119
+ }
120
+
121
+ /** `EventSessionError.properties` (types.gen.d.ts:518) — sem provider/model, só sessionID. */
122
+ interface SessionErrorEvent {
123
+ type: string;
124
+ properties?: {
125
+ sessionID?: string;
126
+ error?: ApiErrorLike;
127
+ };
128
+ }
129
+
130
+ // deno-lint-ignore require-await -- assinatura exigida pela API de plugin do OpenCode (async).
131
+ export const VetorPlugin = async ({ directory, worktree }: {
132
+ directory: string;
133
+ worktree: string;
134
+ }) => {
135
+ const cwd = worktree || directory;
136
+
137
+ // `session.error` (issue #83) não carrega provider/model — só `sessionID`. `chat.params`
138
+ // roda antes de cada chamada ao provedor e já recebe `model: { providerID, id }`; mantemos
139
+ // esse mapa em memória (por processo — cada worker `opencode run --dir` já é isolado) para
140
+ // resolver a chave "<provider>/<model>" quando o erro chegar depois na mesma sessão.
141
+ const sessionModel = new Map<string, string>();
142
+
143
+ return {
144
+ // deno-lint-ignore require-await -- assinatura exigida pela API de plugin do OpenCode (async).
145
+ "chat.params": async (
146
+ input: { sessionID: string; model: { providerID: string; id: string } },
147
+ _output: unknown,
148
+ ) => {
149
+ sessionModel.set(input.sessionID, `${input.model.providerID}/${input.model.id}`);
150
+ },
151
+
152
+ "event": async ({ event }: { event: SessionErrorEvent }) => {
153
+ if (event.type !== "session.error") return;
154
+
155
+ const error = event.properties?.error;
156
+ if (!error || error.name !== "APIError") return;
157
+
158
+ const statusCode = error.data?.statusCode;
159
+ if (statusCode === undefined) return;
160
+
161
+ const sessionID = event.properties?.sessionID;
162
+ const model = sessionID ? sessionModel.get(sessionID) : undefined;
163
+
164
+ await runDenoScript("model-health.ts", {
165
+ model,
166
+ statusCode,
167
+ responseHeaders: error.data?.responseHeaders,
168
+ message: error.data?.message,
169
+ cwd,
170
+ }, 20_000);
171
+ },
172
+
173
+ "tool.execute.before": async (
174
+ input: ToolExecuteBeforeInput,
175
+ output: ToolExecuteBeforeOutput,
176
+ ) => {
177
+ if (!["bash", "edit", "write"].includes(input.tool)) return;
178
+
179
+ const payload = toHookInput(input, output, cwd);
180
+ const { code, stderr } = await runDenoScript("safety-check.ts", payload, 300_000);
181
+ if (code === 2) {
182
+ throw new Error(stderr.trim() || "Bloqueado pelo Vetor Safety Hook.");
183
+ }
184
+ },
185
+
186
+ "tool.execute.after": async (
187
+ input: { tool: string; sessionID: string; callID: string; args: Record<string, unknown> },
188
+ output: { title: string; output: string; metadata: Record<string, unknown> },
189
+ ) => {
190
+ if (!["edit", "write"].includes(input.tool)) return;
191
+
192
+ const payload = toHookInput(input, { args: input.args }, cwd);
193
+ const { stdout } = await runDenoScript("check-edit.ts", payload, 20_000);
194
+ if (!stdout) return;
195
+
196
+ try {
197
+ const parsed = JSON.parse(stdout);
198
+ const context = parsed?.hookSpecificOutput?.additionalContext;
199
+ if (context) {
200
+ output.metadata = { ...output.metadata, vetorDiagnostics: context };
201
+ }
202
+ } catch {
203
+ // Saída não-JSON do check-edit.ts (não deveria acontecer): ignora silenciosamente.
204
+ }
205
+ },
206
+ };
207
+ };
@@ -0,0 +1,92 @@
1
+ // Reprodução da issue #306: `opencode agent list` só reconhece agents definidos em
2
+ // .opencode/agent/*.md. `issue-coordinator` vivia em .opencode/skills/issue-coordinator/SKILL.md —
3
+ // um diretório diferente, descoberto como skill, não como agent — e por isso nunca era listado nem
4
+ // invocável via `opencode run --agent issue-coordinator "<label>"` (caía silenciosamente no agent
5
+ // `build` default).
6
+ //
7
+ // Roda contra o binário `opencode` real quando disponível no PATH; do contrário, pula (o CLI é uma
8
+ // dependência externa opcional para dev/CI local, não travável via deno.lock).
9
+
10
+ import { assertMatch } from "@std/assert";
11
+
12
+ const OPENCODE_SRC = `${import.meta.dirname}/../`;
13
+
14
+ async function opencodeAvailable(): Promise<boolean> {
15
+ try {
16
+ const out = await new Deno.Command("opencode", {
17
+ args: ["--version"],
18
+ stdout: "null",
19
+ stderr: "null",
20
+ }).output();
21
+ return out.success;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ async function copyDir(src: string, dest: string): Promise<void> {
28
+ await Deno.mkdir(dest, { recursive: true });
29
+ for await (const entry of Deno.readDir(src)) {
30
+ const srcPath = `${src}/${entry.name}`;
31
+ const destPath = `${dest}/${entry.name}`;
32
+ if (entry.isDirectory) {
33
+ await copyDir(srcPath, destPath);
34
+ } else {
35
+ await Deno.copyFile(srcPath, destPath);
36
+ }
37
+ }
38
+ }
39
+
40
+ const AVAILABLE = await opencodeAvailable();
41
+
42
+ Deno.test({
43
+ name: "opencode agent list reconhece issue-coordinator (CLI real)",
44
+ ignore: !AVAILABLE,
45
+ fn: async () => {
46
+ const dir = await Deno.makeTempDir();
47
+ await copyDir(OPENCODE_SRC, `${dir}/.opencode`);
48
+
49
+ const out = await new Deno.Command("opencode", {
50
+ args: ["agent", "list"],
51
+ cwd: dir,
52
+ stdout: "piped",
53
+ stderr: "piped",
54
+ }).output();
55
+
56
+ const stdout = new TextDecoder().decode(out.stdout);
57
+ // Linha de agente real, ex. "issue-coordinator (primary)" — não basta checar substring: o
58
+ // JSON de permissões impresso logo abaixo de cada agente contém paths de external_directory que
59
+ // incluem "issue-coordinator" mesmo quando ele só existe como skill (falso positivo). O `mode`
60
+ // também é checado: a issue #306 pede explicitamente `mode: primary` (é o agent de entrada,
61
+ // invocado diretamente pelo usuário — diferente de `issue-worker`/`code-review`, despachados
62
+ // programaticamente pelo próprio coordinator).
63
+ assertMatch(stdout, /^issue-coordinator \(primary\)/m);
64
+ },
65
+ });
66
+
67
+ // Reprodução da issue #311: issue-worker e code-review tinham `mode: subagent` no frontmatter, mas
68
+ // nunca são invocados via `task` in-process do OpenCode (não existe esse mecanismo com isolamento de
69
+ // cwd) — o único uso real deles é como processo `opencode run --dir ... --agent <nome>` isolado,
70
+ // disparado programaticamente pelo issue-coordinator/worktree-ship. Invocar um agent `mode: subagent`
71
+ // diretamente via `--agent` cai silenciosamente no agent `build` default, ignorando por completo o
72
+ // corpo do agent pedido. `mode: primary` (mesmo valor de issue-coordinator, pelo mesmo motivo) é o
73
+ // frontmatter correto.
74
+ Deno.test({
75
+ name: "opencode agent list reconhece issue-worker e code-review como primary (CLI real)",
76
+ ignore: !AVAILABLE,
77
+ fn: async () => {
78
+ const dir = await Deno.makeTempDir();
79
+ await copyDir(OPENCODE_SRC, `${dir}/.opencode`);
80
+
81
+ const out = await new Deno.Command("opencode", {
82
+ args: ["agent", "list"],
83
+ cwd: dir,
84
+ stdout: "piped",
85
+ stderr: "piped",
86
+ }).output();
87
+
88
+ const stdout = new TextDecoder().decode(out.stdout);
89
+ assertMatch(stdout, /^issue-worker \(primary\)/m);
90
+ assertMatch(stdout, /^code-review \(primary\)/m);
91
+ },
92
+ });
@@ -0,0 +1,147 @@
1
+ // Diagnostics por edição (hook PostToolUse, matcher Edit|Write).
2
+ //
3
+ // Motivação (issue #36): sem isto, o issue-worker só descobre erro de tipo, import quebrado
4
+ // ou símbolo inexistente RODANDO O TESTE — e cada descoberta dessas queima uma das 5
5
+ // iterações do fix-loop. Aqui o erro volta junto com o resultado do próprio Edit, e o
6
+ // worker corrige na hora, sem gastar iteração.
7
+ //
8
+ // Contrato do Claude Code: exit 0 + `hookSpecificOutput.additionalContext` injeta o texto
9
+ // ao lado do resultado da tool. Hooks disparam dentro de subagentes, então isto alcança o
10
+ // worker. Sem erro → silêncio absoluto: ruído a cada edição custaria mais do que resolve.
11
+
12
+ import { detectProject } from "./lib/project.ts";
13
+ import { isWithin } from "./lib/guard.ts";
14
+
15
+ const CHECKABLE = /\.(ts|tsx|mts|cts)$/;
16
+ const TIMEOUT_MS = 20_000;
17
+ const MAX_OUTPUT = 4_000;
18
+
19
+ interface HookInput {
20
+ tool_input?: { file_path?: string };
21
+ cwd?: string;
22
+ }
23
+
24
+ function quiet(): never {
25
+ Deno.exit(0);
26
+ }
27
+
28
+ function emit(diagnostics: string): never {
29
+ const text = diagnostics.length > MAX_OUTPUT
30
+ ? `${diagnostics.slice(0, MAX_OUTPUT)}\n[...saída truncada]`
31
+ : diagnostics;
32
+
33
+ console.log(JSON.stringify({
34
+ hookSpecificOutput: {
35
+ hookEventName: "PostToolUse",
36
+ additionalContext:
37
+ `Typecheck do arquivo editado falhou. Corrija antes de rodar os testes:\n\n${text}`,
38
+ },
39
+ }));
40
+ Deno.exit(0);
41
+ }
42
+
43
+ /** Diretório do projeto mais próximo do arquivo — cobre monorepo, sem sair do cwd. */
44
+ function nearestProjectDir(filePath: string, cwd: string): string | null {
45
+ let dir = filePath.replaceAll("\\", "/").split("/").slice(0, -1).join("/");
46
+
47
+ while (dir && isWithin(dir, cwd)) {
48
+ if (detectProject(dir).runtime !== "unknown") return dir;
49
+ const parent = dir.split("/").slice(0, -1).join("/");
50
+ if (parent === dir) break;
51
+ dir = parent;
52
+ }
53
+
54
+ return null;
55
+ }
56
+
57
+ /** Um typecheck lento não pode segurar a edição: estourou o tempo, não diz nada. */
58
+ async function runWithTimeout(
59
+ cmd: string,
60
+ args: string[],
61
+ cwd: string,
62
+ ): Promise<{ code: number; output: string } | null> {
63
+ const timer = new AbortController();
64
+ const timeout = setTimeout(() => timer.abort(), TIMEOUT_MS);
65
+
66
+ try {
67
+ const result = await new Deno.Command(cmd, {
68
+ args,
69
+ cwd,
70
+ // Sem NO_COLOR a saída vem cheia de escapes ANSI, que só ocupariam contexto do worker.
71
+ env: { NO_COLOR: "1" },
72
+ stdout: "piped",
73
+ stderr: "piped",
74
+ signal: timer.signal,
75
+ }).output();
76
+
77
+ const decoder = new TextDecoder();
78
+ return {
79
+ code: result.code,
80
+ output: (decoder.decode(result.stdout) + decoder.decode(result.stderr)).trim(),
81
+ };
82
+ } catch {
83
+ // Binário ausente ou timeout: sem diagnóstico a reportar.
84
+ return null;
85
+ } finally {
86
+ clearTimeout(timeout);
87
+ }
88
+ }
89
+
90
+ function exists(path: string): boolean {
91
+ try {
92
+ Deno.statSync(path);
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ /** tsc local do projeto. Sem ele instalado, não há typecheck a fazer. */
100
+ function localTsc(dir: string): string | null {
101
+ const bin = Deno.build.os === "windows"
102
+ ? `${dir}/node_modules/.bin/tsc.cmd`
103
+ : `${dir}/node_modules/.bin/tsc`;
104
+ return exists(bin) ? bin : null;
105
+ }
106
+
107
+ async function main() {
108
+ const raw = new TextDecoder().decode(await new Response(Deno.stdin.readable).arrayBuffer());
109
+
110
+ let input: HookInput;
111
+ try {
112
+ input = JSON.parse(raw);
113
+ } catch {
114
+ quiet();
115
+ }
116
+
117
+ const filePath = input.tool_input?.file_path;
118
+ const cwd = input.cwd ?? Deno.cwd();
119
+ if (!filePath || !CHECKABLE.test(filePath) || !isWithin(filePath, cwd)) quiet();
120
+
121
+ const dir = nearestProjectDir(filePath, cwd);
122
+ if (!dir) quiet();
123
+
124
+ const { runtime } = detectProject(dir);
125
+
126
+ if (runtime === "deno") {
127
+ const result = await runWithTimeout("deno", ["check", filePath], dir);
128
+ if (result && result.code !== 0 && result.output) emit(result.output);
129
+ quiet();
130
+ }
131
+
132
+ if (runtime === "node") {
133
+ // Sem tsconfig o tsc não sabe as opções do projeto; um check com defaults produziria
134
+ // erros que não são erros do projeto.
135
+ if (!exists(`${dir}/tsconfig.json`)) quiet();
136
+ const tsc = localTsc(dir);
137
+ if (!tsc) quiet();
138
+
139
+ const result = await runWithTimeout(tsc, ["--noEmit"], dir);
140
+ if (result && result.code !== 0 && result.output) emit(result.output);
141
+ quiet();
142
+ }
143
+
144
+ quiet();
145
+ }
146
+
147
+ await main();
@@ -0,0 +1,110 @@
1
+ // CLI invocado pelo issue-coordinator portado (opencode/agent/issue-coordinator.md, issue #313)
2
+ // antes do primeiro `opencode run --dir <worktree>` da sessão.
3
+ //
4
+ // Causa raiz da #313: `issue-worker`/`code-review` rodam com `--dir <worktree>` (cwd fixado no
5
+ // worktree), mas `config.json`/`module-test-map.md`/status file vivem em
6
+ // `<repo-root>/.claude/vetor/`, fora da árvore do worktree — por desenho, é assim que o
7
+ // issue-coordinator agrega progresso de múltiplos processos `opencode` isolados via polling de
8
+ // arquivo. O OpenCode trata esse acesso como `external_directory` (default `"*": "ask"`); em
9
+ // `opencode run` não-interativo (sem TTY) uma permissão `ask` auto-rejeita em vez de bloquear
10
+ // esperando input.
11
+ //
12
+ // Confirmado contra o CLI `opencode` real (v1.18.32): uma regra `permission.external_directory`
13
+ // em `<repo-root>/opencode.json` resolve tanto leitura quanto escrita, mesmo com o worktree dois
14
+ // níveis abaixo (`<repo-root>/.claude/worktrees/<slug>`) — e a resolução do OpenCode é um walk-up
15
+ // de diretório a partir do cwd, independente de git: o arquivo não precisa estar rastreado/
16
+ // commitado para ser encontrado a partir de dentro do worktree (diferente de `.opencode/agent/*.md`,
17
+ // que só chega ao worktree se estiver commitado — `git worktree add` só propaga arquivos
18
+ // rastreados). Por isso este script grava um `opencode.json` **local, não versionado** na raiz do
19
+ // repositório principal, em vez de reescrever um `.md` de agent compartilhado (opção 1 da issue,
20
+ // descartada por risco de corrida entre workers paralelos editando o mesmo arquivo).
21
+ //
22
+ // Idempotente por desenho: seguro de chamar a cada dispatch (Fase 3 e Fase 4, inclusive
23
+ // redespacho/`--resume`, onde a Fase 3 não roda de novo) sem the coordinator precisar rastrear se
24
+ // já rodou. Nunca sobrescreve um `opencode.json` existente às cegas — mescla preservando outras
25
+ // chaves (ex.: `mcp`, outras regras de `permission.external_directory`) e recusa (código 1) se o
26
+ // arquivo existente não for JSON válido, em vez de arriscar destruir configuração do usuário.
27
+ //
28
+ // Contrato de stdin:
29
+ // { cwd?: string }
30
+ // Sem `cwd`, usa `Deno.cwd()`.
31
+ //
32
+ // Códigos de saída:
33
+ // 0 - regra presente (criada, mesclada, ou já estava correta) — stdout traz o path do
34
+ // opencode.json.
35
+ // 1 - opencode.json existente não é JSON válido; nada foi escrito.
36
+
37
+ import { normalizeCwd, readJson } from "./lib/project.ts";
38
+ import { resolveWorktree } from "./lib/status.ts";
39
+
40
+ interface Payload {
41
+ cwd?: string;
42
+ }
43
+
44
+ interface OpencodeConfig {
45
+ "$schema"?: string;
46
+ permission?: {
47
+ external_directory?: Record<string, string>;
48
+ [key: string]: unknown;
49
+ };
50
+ [key: string]: unknown;
51
+ }
52
+
53
+ function exists(path: string): boolean {
54
+ try {
55
+ Deno.statSync(path);
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ async function main() {
63
+ const raw = new TextDecoder().decode(await new Response(Deno.stdin.readable).arrayBuffer());
64
+
65
+ let input: Payload;
66
+ try {
67
+ input = raw.trim() ? JSON.parse(raw) : {};
68
+ } catch {
69
+ input = {};
70
+ }
71
+
72
+ const cwd = normalizeCwd(input.cwd ?? Deno.cwd());
73
+ const worktree = await resolveWorktree(cwd);
74
+ const root = worktree?.root ?? cwd;
75
+
76
+ const configPath = `${root}/opencode.json`;
77
+ const rulePath = `${root}/.claude/vetor/**`;
78
+
79
+ let config: OpencodeConfig;
80
+ if (exists(configPath)) {
81
+ try {
82
+ config = readJson(configPath) as OpencodeConfig;
83
+ } catch {
84
+ console.error(
85
+ `${configPath} existe mas não é JSON válido — recuse sobrescrever. Adicione manualmente ` +
86
+ `"permission.external_directory": { "${rulePath}": "allow" } a esse arquivo.`,
87
+ );
88
+ Deno.exit(1);
89
+ }
90
+ } else {
91
+ config = { "$schema": "https://opencode.ai/config.json" };
92
+ }
93
+
94
+ const currentRule = config.permission?.external_directory?.[rulePath];
95
+ if (currentRule === "allow") {
96
+ return;
97
+ }
98
+
99
+ config.permission = {
100
+ ...config.permission,
101
+ external_directory: {
102
+ ...config.permission?.external_directory,
103
+ [rulePath]: "allow",
104
+ },
105
+ };
106
+
107
+ await Deno.writeTextFile(configPath, JSON.stringify(config, null, 2) + "\n");
108
+ }
109
+
110
+ await main();