@payconductor/houndsman 0.2.0 → 0.4.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 (107) hide show
  1. package/README.md +18 -6
  2. package/package.json +1 -1
  3. package/src/cli/arg-parser.ts +8 -0
  4. package/src/cli/commands/command.ts +23 -0
  5. package/src/cli/commands/context-command.ts +40 -0
  6. package/src/cli/commands/doctor-command.ts +60 -0
  7. package/src/cli/commands/findings-command.ts +38 -0
  8. package/src/cli/commands/help-command.ts +13 -0
  9. package/src/cli/commands/registry.ts +34 -0
  10. package/src/cli/commands/review-command.ts +21 -0
  11. package/src/cli/commands/server-command.ts +37 -0
  12. package/src/cli/commands/setup-command.ts +34 -0
  13. package/src/cli/commands/watchdog-command.ts +34 -0
  14. package/src/cli/help.ts +47 -0
  15. package/src/cli/index.ts +60 -268
  16. package/src/core/arg-parser.ts +48 -0
  17. package/src/core/commands/context.ts +29 -13
  18. package/src/core/commands/findings.ts +185 -260
  19. package/src/core/commands/review.ts +21 -39
  20. package/src/core/commands/scaffold.ts +189 -80
  21. package/src/core/commands/setup.ts +16 -11
  22. package/src/core/commands/update.ts +7 -7
  23. package/src/core/commands/watchdog.ts +8 -80
  24. package/src/core/config.ts +24 -25
  25. package/src/core/defaults.ts +20 -0
  26. package/src/core/diff-source.ts +1 -13
  27. package/src/core/doc-discoverer.ts +25 -15
  28. package/src/core/domain/budget.ts +42 -0
  29. package/src/core/domain/classification.ts +94 -0
  30. package/src/core/domain/diff.ts +53 -0
  31. package/src/core/domain/watchdog.ts +96 -0
  32. package/src/core/engine/carryover.ts +67 -0
  33. package/src/core/engine/houndsman-engine.ts +65 -0
  34. package/src/core/engine/review-scope.ts +95 -0
  35. package/src/core/engine/review-session.ts +79 -0
  36. package/src/core/file-classifier.ts +11 -67
  37. package/src/core/findings-state.ts +193 -100
  38. package/src/core/git-revision.ts +14 -40
  39. package/src/core/git-runner.ts +46 -0
  40. package/src/core/github-review.ts +4 -33
  41. package/src/core/jev/jev-candidates.ts +49 -35
  42. package/src/core/orchestrator.ts +247 -421
  43. package/src/core/paths.ts +26 -11
  44. package/src/core/pr-context.ts +39 -51
  45. package/src/core/project-config.ts +56 -0
  46. package/src/core/renderers/batch-presenter.ts +40 -0
  47. package/src/core/renderers/brief-renderer.ts +23 -0
  48. package/src/core/renderers/findings-presenter.ts +74 -0
  49. package/src/core/renderers/manifest-writer.ts +123 -0
  50. package/src/core/renderers/report-renderer.ts +66 -0
  51. package/src/core/renderers/verify-presenter.ts +50 -0
  52. package/src/core/renderers/watchdog-presenter.ts +100 -0
  53. package/src/core/report-builder.ts +12 -7
  54. package/src/core/run-layout.ts +8 -0
  55. package/src/core/run-status.ts +29 -59
  56. package/src/core/services/clock.ts +12 -0
  57. package/src/core/services/file-classifier.ts +34 -0
  58. package/src/core/services/fix-batch-builder.ts +13 -0
  59. package/src/core/services/gh-client.ts +54 -0
  60. package/src/core/services/git-repository.ts +139 -0
  61. package/src/core/services/project-files.ts +91 -0
  62. package/src/core/services/verifier.ts +58 -0
  63. package/src/core/storage/factory.ts +24 -0
  64. package/src/core/storage/file/file-storage.ts +269 -0
  65. package/src/core/storage/paths.ts +64 -0
  66. package/src/core/storage/repo-identity.ts +70 -0
  67. package/src/core/storage/sqlite/mappers.ts +160 -0
  68. package/src/core/storage/sqlite/schema.ts +108 -0
  69. package/src/core/storage/sqlite/sqlite-storage.ts +427 -0
  70. package/src/core/storage/types.ts +143 -0
  71. package/src/core/storage/work-area.ts +191 -0
  72. package/src/core/verify.ts +2 -1
  73. package/src/core/watchdog-registry.ts +58 -16
  74. package/src/index.ts +57 -6
  75. package/src/server/artifacts.ts +8 -61
  76. package/src/server/client/ArtifactsPanel.tsx +137 -23
  77. package/src/server/client/FindingCard.tsx +37 -6
  78. package/src/server/client/MarkdownText.tsx +101 -0
  79. package/src/server/client/SubmitModal.tsx +9 -4
  80. package/src/server/client/code-highlight.ts +422 -0
  81. package/src/server/client/constants.ts +130 -24
  82. package/src/server/client/json-highlight.ts +55 -0
  83. package/src/server/client/markdown.ts +153 -0
  84. package/src/server/client/types.ts +23 -0
  85. package/src/server/index.ts +155 -245
  86. package/src/server/run-repository.ts +299 -0
  87. package/src/skill/OUTPUT-FORMAT.md +6 -4
  88. package/src/skill/README.md +22 -14
  89. package/src/skill/SETUP.md +4 -4
  90. package/src/skill/fix.md +5 -5
  91. package/src/skill/hunt.md +27 -18
  92. package/src/skill/train.md +1 -1
  93. package/src/skill/verify.md +5 -5
  94. package/src/tests/__snapshots__/baseline.test.ts.snap +825 -0
  95. package/src/tests/baseline.test.ts +555 -493
  96. package/src/tests/cli.test.ts +40 -19
  97. package/src/tests/code-highlight.test.ts +123 -0
  98. package/src/tests/domain.test.ts +328 -0
  99. package/src/tests/findings-commands.test.ts +153 -131
  100. package/src/tests/findings-state.test.ts +6 -3
  101. package/src/tests/fixtures/fake-project.ts +0 -1
  102. package/src/tests/infrastructure.test.ts +205 -0
  103. package/src/tests/markdown.test.ts +89 -0
  104. package/src/tests/review-scripts.test.ts +8 -5
  105. package/src/tests/run-status.test.ts +33 -23
  106. package/src/tests/server.test.ts +108 -75
  107. package/src/tests/storage.test.ts +386 -0
package/src/cli/index.ts CHANGED
@@ -22,285 +22,77 @@
22
22
  * test Roda a suíte de testes da lib
23
23
  * watchdog <id> Contexto de um watchdog (`--rules`, `--json`)
24
24
  *
25
- * Flags: `--base <b>` · `--head <ref>` · `--branch <b>` · `--dir <d>` ·
26
- * `--config <path>` · `--clean` · `--full` · `--no-jev` · `--fail-on-blocking`.
25
+ * Flags: `--base <b>` · `--head <ref>` · `--branch <b>` · `--config <path>` ·
26
+ * `--clean` · `--full` · `--no-jev` · `--fail-on-blocking`.
27
27
  */
28
28
 
29
- import { type HoundsmanConfig, resolveArtifactDir } from "../core/config";
30
29
  import { loadConfig } from "../core/config-loader";
31
- import { relFromRoot } from "../core/paths";
32
-
33
- const HELP = `Uso: houndsman [comando] [flags]
34
-
35
- Comandos:
36
- (nenhum) Prepara a revisão (classification + briefs + manifesto)
37
- --finalize Consolida os findings em review.md
38
- context Coleta o contexto da PR via gh (Passo 0)
39
- findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · submit · path)
40
- server Sobe a UI local de triagem (--port, --host, --open)
41
- setup Cria a configuração inicial do projeto (config + skills)
42
- update Sincroniza os arquivos auto-gerados (.houndsman/core)
43
- doctor Valida a configuração, o ambiente e a credencial Jev
44
- test Roda a suíte de testes da lib
45
- watchdog <id> Contexto de um watchdog (--rules, --json)
46
-
47
- Flags:
48
- --config <path> Caminho do houndsman.config.* (default: detecta no cwd)
49
- --base <branch> Base do diff (default: main)
50
- --head <ref> Head do diff (default: HEAD)
51
- --branch <nome> Branch alvo (default: detecta do git)
52
- --dir <dir> Diretório dos artefatos
53
- --clean Descarta findings anteriores
54
- --full Força review completo (ignora o modo incremental)
55
- --no-jev Desativa a pré-análise Jev
56
- --fail-on-blocking Exit 1 se houver bloqueantes (com --finalize)
57
-
58
- Flags de findings:
59
- --decision <d> pending | execute | ignore
60
- --exec <e> pending | in_progress | done | stale
61
- --comment <text> Comentário/instrução adicional
62
- --watchdog <id> Filtra por watchdog (list)
63
- --severity <s> Filtra por severidade (list, batch)
64
- --file <text> Filtra por trecho do caminho (list)
65
- --all Inclui findings ausentes do último relatório
66
- --json Saída JSON
67
- --limit <n> Limita o nº de itens (list)
68
- --group <key> Grupo alvo no batch (default: o primeiro)
69
- --offset <n> Paginação dentro do grupo (batch)
70
- --include-done Inclui já concluídos no batch
71
- --apply (verify) Marca os resolvidos como done
72
- --fail-on-persisting (verify) Exit 1 se algum blocking persistir
73
- --include-sent (submit) Reenvia findings já enviados à PR
74
- --dry-run (submit) Só a prévia do envio, sem postar`;
75
-
76
- function readFlag(args: string[], flag: string): string | undefined {
77
- const index = args.indexOf(flag);
78
- return index >= 0 ? args[index + 1] : undefined;
79
- }
80
-
81
- /** Passo 0 — contexto da PR via `gh`. */
82
- async function runContext(config: HoundsmanConfig, args: string[]): Promise<void> {
83
- const { runContextCommand } = await import("../core/commands/context");
84
- const { paths, context } = runContextCommand(config, {
85
- branch: readFlag(args, "--branch"),
86
- dir: readFlag(args, "--dir"),
87
- });
88
-
89
- if (context.available) {
90
- const src = context.source;
91
- const label = [src?.number ? `#${src.number}` : "", src?.title ?? ""]
92
- .filter(Boolean)
93
- .join(" ");
94
- console.log(
95
- `[houndsman] PR ${label} (${src?.head} → ${src?.base}), ${context.stats?.files ?? 0} arquivo(s).`,
96
- );
97
- console.log(`[houndsman] contexto: ${relFromRoot(paths.context)}`);
98
- console.log(`[houndsman] brief: ${relFromRoot(paths.brief)}`);
99
- console.log(
100
- `[houndsman] próximo passo: subagente resume em ${relFromRoot(paths.summary)}`,
101
- );
102
- } else {
103
- console.log(
104
- `[houndsman] contexto indisponível (${context.reason}): ${context.detail}`,
105
- );
106
- console.log(`[houndsman] marcador: ${relFromRoot(paths.context)}`);
107
- }
108
- }
109
-
110
- /** Valida a configuração do projeto e o ambiente. */
111
- async function runDoctor(config: HoundsmanConfig): Promise<void> {
112
- const { checkRegistry } = await import("../core/registry-check");
113
- console.log("→ Validando configuração do projeto...");
114
- const { problems, warnings, jevQuestions } = checkRegistry(config);
115
-
116
- if (problems.length > 0) {
117
- console.error("\n❌ Problemas na configuração de revisão:\n");
118
- for (const problem of problems) console.error(` - ${problem}`);
119
- process.exit(1);
120
- }
121
- console.log(
122
- ` ✅ ${config.watchdogIds.length} watchdogs, ${jevQuestions} pergunta(s) Jev.`,
123
- );
124
-
125
- if (warnings.length > 0) {
126
- console.log(`\n⚠️ ${warnings.length} aviso(s) (não bloqueantes):`);
127
- for (const warning of warnings) console.log(` - ${warning}`);
128
- }
129
-
130
- console.log("\n→ Ambiente:");
131
- console.log(` ${Bun.which("git") ? "✅" : "❌"} git`);
132
- console.log(` ${Bun.which("gh") ? "✅" : "⚠️ "} gh (opcional — contexto da PR)`);
133
- await reportJevCredential();
30
+ import { HoundsmanEngine } from "../core/engine/houndsman-engine";
31
+ import { ArgParser } from "./arg-parser";
32
+ import type { CliContext } from "./commands/command";
33
+ import { ContextCommand } from "./commands/context-command";
34
+ import { DoctorCommand } from "./commands/doctor-command";
35
+ import { FindingsCommand } from "./commands/findings-command";
36
+ import { HelpCommand } from "./commands/help-command";
37
+ import { CommandRegistry } from "./commands/registry";
38
+ import { ReviewCommand } from "./commands/review-command";
39
+ import { ServerCommand } from "./commands/server-command";
40
+ import { SetupCommand, TestCommand, UpdateCommand } from "./commands/setup-command";
41
+ import { WatchdogCommand } from "./commands/watchdog-command";
42
+ import { HELP } from "./help";
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Registry de comandos
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /** Monta o registry com todos os comandos do binário. */
49
+ function buildRegistry(): CommandRegistry {
50
+ const doctor = new DoctorCommand();
51
+ return new CommandRegistry()
52
+ .register(new ReviewCommand(), { fallback: true })
53
+ .register(new HelpCommand())
54
+ .register(new ContextCommand())
55
+ .register(new FindingsCommand())
56
+ .register(new WatchdogCommand())
57
+ .register(new ServerCommand())
58
+ .register(new SetupCommand())
59
+ .register(new UpdateCommand())
60
+ .register(new TestCommand())
61
+ .register(doctor)
62
+ .registerAs("check", doctor);
134
63
  }
135
64
 
136
65
  /**
137
- * Reporta o estado da credencial Jev: presença e validade (chamada mínima).
138
- * Fail-open — nunca altera o exit code (`doctor` é diagnóstico).
139
- */
140
- async function reportJevCredential(): Promise<void> {
141
- const { JevClient } = await import("../core/jev");
142
- const client = new JevClient();
143
- if (!client.isConfigured()) {
144
- console.log(" ⚠️ TYPESAFE_API_KEY (opcional — pré-análise Jev)");
145
- return;
146
- }
147
- const check = await client.validateCredential();
148
- if (check.ok) {
149
- console.log(` ✅ Jev: credencial válida (modelo ${check.model}).`);
150
- } else if (check.kind === "invalid") {
151
- console.log(` ❌ Jev: credencial inválida — ${check.detail}`);
152
- } else {
153
- console.log(` ⚠️ Jev: não foi possível validar — ${check.detail}`);
154
- }
155
- }
156
-
157
- /** Contexto de um watchdog. */
158
- async function runWatchdog(config: HoundsmanConfig, args: string[]): Promise<void> {
159
- const { renderWatchdogContext } = await import("../core/commands/watchdog");
160
- const id = args.find((arg) => !arg.startsWith("-"));
161
-
162
- if (!id || renderWatchdogContext(config, id) === null) {
163
- console.error(
164
- id
165
- ? `Watchdog '${id}' não existe.`
166
- : "Informe o id do watchdog: houndsman watchdog <id>",
167
- );
168
- console.error(`Ids válidos: ${config.watchdogIds.join(", ")}`);
169
- process.exit(1);
170
- }
171
-
172
- console.log(
173
- renderWatchdogContext(config, id, {
174
- withRules: args.includes("--rules"),
175
- json: args.includes("--json"),
176
- }),
177
- );
178
- }
179
-
180
- /**
181
- * Subcomando `findings` — triagem/correção dos findings (list/show/set/done/reset/
182
- * sync/summary/batch/verify/path). Deixa o agente interagir com o estado sem abrir
183
- * o JSON. O `verify` é assíncrono (consulta opcional ao Jev) — roteado à parte.
184
- */
185
- async function runFindings(config: HoundsmanConfig, args: string[]): Promise<void> {
186
- const { runFindingsCommand, runSubmitCommand, runVerifyCommand } = await import(
187
- "../core/commands/findings"
188
- );
189
- const sub = args.find((arg) => !arg.startsWith("-"));
190
- const result =
191
- sub === "verify"
192
- ? await runVerifyCommand(config, args)
193
- : sub === "submit"
194
- ? await runSubmitCommand(config, args)
195
- : runFindingsCommand(config, args);
196
- if (result.exitCode !== 0) {
197
- console.error(result.output);
198
- process.exit(result.exitCode);
199
- }
200
- console.log(result.output);
201
- }
202
-
203
- /** Abre a URL no navegador padrão (best-effort, por plataforma). */
204
- function openBrowser(url: string): void {
205
- try {
206
- const command =
207
- process.platform === "win32"
208
- ? ["cmd", "/c", "start", "", url]
209
- : process.platform === "darwin"
210
- ? ["open", url]
211
- : ["xdg-open", url];
212
- Bun.spawn(command, { stdout: "ignore", stderr: "ignore" });
213
- } catch {
214
- // best-effort — se não abrir, o usuário copia a URL.
215
- }
216
- }
217
-
218
- /** Subcomando `server` — sobe a UI local de triagem. */
219
- async function runServer(config: HoundsmanConfig, args: string[]): Promise<void> {
220
- const { startServer } = await import("../server");
221
- const port = Number(readFlag(args, "--port")) || 7331;
222
- const host = readFlag(args, "--host") ?? "127.0.0.1";
223
- const { url } = startServer({ config, port, host });
224
- console.log(`[houndsman] servidor em ${url}`);
225
- console.log("[houndsman] Ctrl+C para encerrar.");
226
- if (args.includes("--open")) openBrowser(url);
227
- }
228
-
229
- /** Passo 1/3 — preparar ou finalizar. */
230
- async function runReview(config: HoundsmanConfig, args: string[]): Promise<void> {
231
- const [{ parseReviewArgs }, { finalize, prepare, resolveBranch }] = await Promise.all(
232
- [import("../core/commands/review"), import("../core/orchestrator")],
233
- );
234
- const opts = parseReviewArgs(args, resolveArtifactDir(config));
235
- const branch = resolveBranch(opts);
236
-
237
- if (opts.finalize) {
238
- const result = finalize(opts, branch, config);
239
- if (opts.failOnBlocking && result.blockers > 0) process.exit(1);
240
- } else {
241
- await prepare(opts, branch, config);
242
- }
243
- }
244
-
245
- /**
246
- * Executa um subcomando. `--config` sobrepõe a descoberta automática.
247
- * Comandos que não precisam de projeto (`test`, `setup`) são tratados antes do load.
66
+ * Bootstrap do CLI: resolve o comando no registry, carrega a config sob demanda
67
+ * e delega a execução (o comando decide log/exit).
248
68
  */
249
69
  export async function main(): Promise<void> {
250
70
  const argv = process.argv.slice(2);
251
- const first = argv[0];
252
- const command = first && !first.startsWith("-") ? first : undefined;
253
- const rest = command ? argv.slice(1) : argv;
254
-
255
- if (command === "test") {
256
- const { runLibTests } = await import("../core/commands/test");
257
- process.exit(runLibTests());
258
- }
259
- if (command === "setup") {
260
- const { runSetupCommand } = await import("../core/commands/setup");
261
- await runSetupCommand({ cwd: process.cwd(), args: rest });
262
- return;
263
- }
264
- if (command === "update") {
265
- const { runUpdateCommand } = await import("../core/commands/update");
266
- await runUpdateCommand({ cwd: process.cwd(), args: rest });
267
- return;
268
- }
269
-
270
- if (command === "help") {
271
- console.info(HELP);
272
- return;
71
+ const parser = ArgParser.from(argv);
72
+ const commandName = parser.command();
73
+ const registry = buildRegistry();
74
+ const command = registry.resolve(commandName);
75
+
76
+ if (!command) {
77
+ console.error(`Subcomando desconhecido: '${commandName}'.\n`);
78
+ console.error(HELP);
79
+ process.exit(1);
273
80
  }
274
81
 
275
- const { config } = await loadConfig({
276
- configPath: readFlag(argv, "--config"),
277
- });
278
-
279
- switch (command) {
280
- case "context":
281
- await runContext(config, rest);
282
- return;
283
- case "doctor":
284
- case "check":
285
- await runDoctor(config);
286
- return;
287
- case "watchdog":
288
- await runWatchdog(config, rest);
289
- return;
290
- case "findings":
291
- await runFindings(config, rest);
292
- return;
293
- case "server":
294
- await runServer(config, rest);
295
- return;
296
- case undefined:
297
- await runReview(config, rest);
298
- return;
299
- default:
300
- console.error(`Subcomando desconhecido: '${command}'.\n`);
301
- console.error(HELP);
302
- process.exit(1);
303
- }
82
+ const context: CliContext = {
83
+ cwd: process.cwd(),
84
+ out: (message) => console.log(message),
85
+ err: (message) => console.error(message),
86
+ engine: async () => {
87
+ const { config } = await loadConfig({
88
+ configPath: parser.value("--config"),
89
+ });
90
+ return new HoundsmanEngine(config);
91
+ },
92
+ };
93
+
94
+ const code = await command.run(parser.rest(), context);
95
+ if (code !== 0) process.exit(code);
304
96
  }
305
97
 
306
98
  if (import.meta.main) {
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Parser simples de argv (biblioteca) — compartilhado por CLI e comandos.
3
+ *
4
+ * Não valida flags: só lê valores/booleanos e separa o subcomando.
5
+ */
6
+
7
+ /** argv com leitura de flags por nome. */
8
+ export class ArgParser {
9
+ readonly argv: string[];
10
+
11
+ constructor(argv: readonly string[]) {
12
+ this.argv = [...argv];
13
+ }
14
+
15
+ static from(argv: readonly string[]): ArgParser {
16
+ return new ArgParser(argv);
17
+ }
18
+
19
+ /** Valor da flag (`--x y`), se presente. */
20
+ value(flag: string): string | undefined {
21
+ const index = this.argv.indexOf(flag);
22
+ return index >= 0 ? this.argv[index + 1] : undefined;
23
+ }
24
+
25
+ /** `true` se a flag está presente. */
26
+ has(flag: string): boolean {
27
+ return this.argv.includes(flag);
28
+ }
29
+
30
+ /** Primeiro token que não é flag (subcomando). */
31
+ command(): string | undefined {
32
+ const first = this.argv[0];
33
+ return first && !first.startsWith("-") ? first : undefined;
34
+ }
35
+
36
+ /** Argumentos sem o subcomando. */
37
+ rest(): string[] {
38
+ const command = this.command();
39
+ if (!command) return this.argv;
40
+ const index = this.argv.indexOf(command);
41
+ return [...this.argv.slice(0, index), ...this.argv.slice(index + 1)];
42
+ }
43
+
44
+ /** Todos os tokens que não começam com `-`. */
45
+ positionals(): string[] {
46
+ return this.argv.filter((arg) => !arg.startsWith("-"));
47
+ }
48
+ }
@@ -1,19 +1,20 @@
1
1
  /**
2
2
  * Subcomando `context` — coleta o contexto da PR via `gh` (Passo 0).
3
3
  *
4
- * Biblioteca: grava os artefatos e devolve os dados; o CLI faz o log/exit.
4
+ * Biblioteca: grava no staging da review + artefato no storage; o CLI faz o
5
+ * log/exit.
5
6
  */
6
7
 
7
- import { mkdirSync, writeFileSync } from "node:fs";
8
- import { type HoundsmanConfig, resolveArtifactDir } from "../config";
8
+ import type { HoundsmanConfig } from "../config";
9
9
  import {
10
- type ContextPaths,
11
10
  collectPrContext,
12
- contextPaths,
13
11
  currentBranch,
14
12
  type PrContextRaw,
15
13
  renderContextBrief,
16
14
  } from "../pr-context";
15
+ import { sanitizeBranch } from "../report-builder";
16
+ import type { RepoIdentity, StorageProvider } from "../storage/types";
17
+ import { type ContextPaths, ReviewWorkArea } from "../storage/work-area";
17
18
 
18
19
  export type ContextRunResult = {
19
20
  paths: ContextPaths;
@@ -22,18 +23,33 @@ export type ContextRunResult = {
22
23
 
23
24
  /** Coleta a PR atual e grava `context.json` + `context-brief.md`. */
24
25
  export function runContextCommand(
25
- config: HoundsmanConfig,
26
- opts: { branch?: string; dir?: string } = {},
26
+ _config: HoundsmanConfig,
27
+ storage: StorageProvider,
28
+ repo: RepoIdentity,
29
+ opts: { branch?: string } = {},
27
30
  ): ContextRunResult {
28
31
  const branch = opts.branch ?? currentBranch();
29
- const dir = opts.dir ?? resolveArtifactDir(config);
30
- const paths = contextPaths(dir, branch);
31
-
32
- mkdirSync(paths.dir, { recursive: true });
32
+ const review = storage.getOrCreateReview({
33
+ repo,
34
+ branch,
35
+ slug: sanitizeBranch(branch),
36
+ });
37
+ const work = new ReviewWorkArea(review);
38
+ const paths = work.contextPaths;
33
39
 
34
40
  const context = collectPrContext(branch);
35
- writeFileSync(paths.context, `${JSON.stringify(context, null, 2)}\n`);
36
- writeFileSync(paths.brief, `${renderContextBrief(context, branch, paths)}\n`);
41
+ const brief = renderContextBrief(context, branch, paths);
42
+ work.writeContext(context, brief);
43
+ storage.writeArtifact(review.id, {
44
+ name: "context.json",
45
+ kind: "context",
46
+ content: `${JSON.stringify(context, null, 2)}\n`,
47
+ });
48
+ storage.writeArtifact(review.id, {
49
+ name: "context-brief.md",
50
+ kind: "context-brief",
51
+ content: `${brief}\n`,
52
+ });
37
53
 
38
54
  return { paths, context };
39
55
  }