@rmrdeveloper/sideroom-pi 8.10.0 → 8.12.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 (37) hide show
  1. package/README.md +63 -28
  2. package/assets/artifacts/GUIDELINES_TEMPLATE.md +3 -3
  3. package/extensions/ask/index.ts +3 -2
  4. package/extensions/done/guard.ts +42 -37
  5. package/extensions/done/index.ts +3 -2
  6. package/extensions/done/model.ts +1 -5
  7. package/extensions/explain/guard.ts +13 -11
  8. package/extensions/explain/model.ts +2 -25
  9. package/extensions/guidelines/review.ts +31 -13
  10. package/extensions/jev/client.ts +27 -2
  11. package/extensions/jev/context.ts +360 -0
  12. package/extensions/jev/guard.ts +116 -34
  13. package/extensions/jev/model.ts +175 -23
  14. package/extensions/jev/workflow.ts +188 -0
  15. package/extensions/persona/catalog.ts +2 -2
  16. package/extensions/persona/execute.ts +4 -6
  17. package/extensions/persona/index.ts +3 -2
  18. package/extensions/persona/model.ts +1 -31
  19. package/extensions/persona/prompt.ts +5 -3
  20. package/extensions/shared/settle.ts +63 -13
  21. package/package.json +1 -1
  22. package/skills/sideroom-architecture/SKILL.md +17 -8
  23. package/skills/sideroom-codebase-design/LICENSE.txt +21 -0
  24. package/skills/sideroom-codebase-design/SKILL.md +151 -0
  25. package/skills/sideroom-codebase-design/references/growth.md +62 -0
  26. package/skills/sideroom-domain-modeling/SKILL.md +1 -1
  27. package/skills/sideroom-domain-scaffold/SKILL.md +6 -4
  28. package/skills/sideroom-grill/SKILL.md +16 -6
  29. package/skills/sideroom-guidelines/SKILL.md +9 -7
  30. package/skills/sideroom-guidelines/references/languages/go.md +3 -3
  31. package/skills/sideroom-guidelines/references/languages/java.md +3 -3
  32. package/skills/sideroom-guidelines/references/languages/javascript.md +25 -3
  33. package/skills/sideroom-guidelines/references/languages/php-laravel.md +3 -3
  34. package/skills/sideroom-guidelines/references/languages/python.md +3 -3
  35. package/skills/sideroom-guidelines/references/languages/rust.md +3 -3
  36. package/skills/sideroom-guidelines/references/languages/typescript.md +27 -3
  37. package/skills/sideroom-persona/SKILL.md +43 -25
@@ -0,0 +1,360 @@
1
+ import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import { extname, join, posix, relative, sep } from 'node:path';
3
+ import ignore from 'ignore';
4
+ import { LANGUAGE, languageForPath } from '../rules/catalog.ts';
5
+ import {
6
+ buildFileState,
7
+ isTestPath,
8
+ type JevFileState,
9
+ type JevRelatedFile,
10
+ stateFitsBudget,
11
+ } from './model.ts';
12
+
13
+ const MAX_RELATED_FILES = 4;
14
+ const MAX_SCAN_FILES = 1_024;
15
+ const MAX_SCAN_DEPTH = 10;
16
+ const MAX_CANDIDATE_BYTES = 32_000;
17
+ const IGNORE_FILES = ['.gitignore', '.ignore', '.fdignore'] as const;
18
+ const SENSITIVE_NAME =
19
+ /secret|credential|password|token|private.?key|api.?key|auth.?key|(?:^|[._-])(?:env|private|key)(?:[._-]|$)/iu;
20
+ const SKIP_DIRECTORIES = new Set(['node_modules', 'dist', 'build', 'coverage']);
21
+
22
+ interface Candidate {
23
+ readonly path: string;
24
+ readonly body: string;
25
+ }
26
+
27
+ interface ScanResult {
28
+ readonly candidates: readonly Candidate[];
29
+ readonly partial: boolean;
30
+ }
31
+
32
+ // Keep the edited file complete. Other files are included whole or not at all.
33
+ export function buildContextualState(
34
+ cwd: string,
35
+ path: string,
36
+ body: string,
37
+ kind: string,
38
+ lines: Parameters<typeof buildFileState>[3],
39
+ ): JevFileState | undefined {
40
+ const base = buildFileState(path, body, kind, lines);
41
+ if (!stateFitsBudget(base)) {
42
+ return undefined;
43
+ }
44
+
45
+ const scan = scanSourceFiles(cwd, path);
46
+ const imported = references(path, body);
47
+ const related: JevRelatedFile[] = [];
48
+ let partial = scan.partial;
49
+ const matches = scan.candidates
50
+ .flatMap((candidate) => {
51
+ const importedByTarget = matchesReference(imported, candidate.path);
52
+ const consumesTarget = matchesReference(
53
+ references(candidate.path, candidate.body),
54
+ path,
55
+ );
56
+ const companionTest = isCompanionTest(path, candidate.path);
57
+ const testedSource = isCompanionTest(candidate.path, path);
58
+ if (
59
+ !importedByTarget &&
60
+ !consumesTarget &&
61
+ !companionTest &&
62
+ !testedSource
63
+ ) {
64
+ return [];
65
+ }
66
+ return [
67
+ {
68
+ ...candidate,
69
+ relation: isTestPath(candidate.path)
70
+ ? 'test'
71
+ : importedByTarget || testedSource
72
+ ? 'imported'
73
+ : 'consumer',
74
+ } as const,
75
+ ];
76
+ })
77
+ .sort((left, right) => {
78
+ if (left.relation !== right.relation) {
79
+ const priority = { test: 0, consumer: 1, imported: 2 };
80
+ return priority[left.relation] - priority[right.relation];
81
+ }
82
+ return compareText(left.path, right.path);
83
+ });
84
+
85
+ for (const candidate of matches) {
86
+ if (related.length >= MAX_RELATED_FILES) {
87
+ partial = true;
88
+ break;
89
+ }
90
+ const next = [...related, candidate];
91
+ if (!stateFitsBudget(buildFileState(path, body, kind, lines, next))) {
92
+ partial = true;
93
+ continue;
94
+ }
95
+ related.push(candidate);
96
+ }
97
+ if (
98
+ [...imported].some(
99
+ (name) =>
100
+ !related.some((file) => matchesReference(new Set([name]), file.path)),
101
+ )
102
+ ) {
103
+ partial = true;
104
+ }
105
+ while (
106
+ related.length > 0 &&
107
+ !stateFitsBudget(buildFileState(path, body, kind, lines, related, partial))
108
+ ) {
109
+ related.pop();
110
+ partial = true;
111
+ }
112
+ const state = buildFileState(path, body, kind, lines, related, partial);
113
+ return stateFitsBudget(state) ? state : undefined;
114
+ }
115
+
116
+ function scanSourceFiles(cwd: string, editedPath: string): ScanResult {
117
+ const root = realpathSync(cwd);
118
+ const candidates: Candidate[] = [];
119
+ let visited = 0;
120
+ let partial = false;
121
+
122
+ function visit(
123
+ directory: string,
124
+ depth: number,
125
+ patterns: readonly string[],
126
+ ): void {
127
+ if (depth > MAX_SCAN_DEPTH || visited >= MAX_SCAN_FILES) {
128
+ partial = true;
129
+ return;
130
+ }
131
+ const localPatterns = [...patterns, ...readIgnorePatterns(directory, root)];
132
+ const matcher = ignore().add(localPatterns);
133
+ const entries = readdirSync(directory, { withFileTypes: true }).sort(
134
+ (a, b) => compareText(a.name, b.name),
135
+ );
136
+ for (const entry of entries) {
137
+ if (visited >= MAX_SCAN_FILES) {
138
+ partial = true;
139
+ return;
140
+ }
141
+ const absolute = join(directory, entry.name);
142
+ const path = relative(root, absolute).split(sep).join('/');
143
+ if (entry.name.startsWith('.') || SENSITIVE_NAME.test(entry.name)) {
144
+ continue;
145
+ }
146
+ if (entry.isDirectory()) {
147
+ if (!SKIP_DIRECTORIES.has(entry.name) && !matcher.ignores(`${path}/`)) {
148
+ visit(absolute, depth + 1, localPatterns);
149
+ }
150
+ continue;
151
+ }
152
+ if (
153
+ !entry.isFile() ||
154
+ matcher.ignores(path) ||
155
+ path === editedPath ||
156
+ languageForPath(path) === LANGUAGE.generic
157
+ ) {
158
+ continue;
159
+ }
160
+ visited += 1;
161
+ if (statSync(absolute).size > MAX_CANDIDATE_BYTES) {
162
+ partial = true;
163
+ continue;
164
+ }
165
+ candidates.push({ path, body: readFileSync(absolute, 'utf8') });
166
+ }
167
+ }
168
+
169
+ visit(root, 0, []);
170
+ return { candidates, partial };
171
+ }
172
+
173
+ function readIgnorePatterns(directory: string, root: string): string[] {
174
+ const prefix = relative(root, directory).split(sep).join('/');
175
+ const rules: string[] = [];
176
+ for (const name of IGNORE_FILES) {
177
+ const file = join(directory, name);
178
+ if (!exists(file)) {
179
+ continue;
180
+ }
181
+ for (const line of readFileSync(file, 'utf8').split(/\r?\n/u)) {
182
+ const trimmed = line.trim();
183
+ if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('\\#'))) {
184
+ continue;
185
+ }
186
+ const negative = line.startsWith('!');
187
+ const pattern = (negative ? line.slice(1) : line).replace(/^\//u, '');
188
+ rules.push(
189
+ `${negative ? '!' : ''}${prefix ? `${prefix}/` : ''}${pattern}`,
190
+ );
191
+ }
192
+ }
193
+ return rules;
194
+ }
195
+
196
+ function exists(path: string): boolean {
197
+ try {
198
+ statSync(path);
199
+ return true;
200
+ } catch (error) {
201
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
202
+ return false;
203
+ }
204
+ throw error;
205
+ }
206
+ }
207
+
208
+ function modulePath(path: string): string {
209
+ const extension = extname(path);
210
+ const withoutExtension = extension ? path.slice(0, -extension.length) : path;
211
+ return withoutExtension.endsWith('/index')
212
+ ? withoutExtension.slice(0, -'/index'.length)
213
+ : withoutExtension;
214
+ }
215
+
216
+ function references(path: string, body: string): Set<string> {
217
+ const names = new Set<string>();
218
+ const language = languageForPath(path);
219
+ const code = body.replace(/\/\*[\s\S]*?\*\/|^[ \t]*\/\/[^\n]*$/gmu, '');
220
+ if (language === LANGUAGE.typescript || language === LANGUAGE.javascript) {
221
+ const imports = [
222
+ /^[ \t]*(?:import|export)\s+(?:\{[^}]*\}|[^\n;]+)\s*from\s*['"](\.[^'"]+)['"]/gmu,
223
+ /^[ \t]*import\s*['"](\.[^'"]+)['"]/gmu,
224
+ /^[ \t]*(?:(?:const|let|var)\s+[\w{} ,]+\s*=\s*)?require\s*\(\s*['"](\.[^'"]+)['"]/gmu,
225
+ ];
226
+ for (const pattern of imports) {
227
+ for (const match of code.matchAll(pattern)) {
228
+ const specifier = match[1];
229
+ if (specifier) {
230
+ names.add(
231
+ modulePath(
232
+ posix.normalize(posix.join(posix.dirname(path), specifier)),
233
+ ),
234
+ );
235
+ }
236
+ }
237
+ }
238
+ }
239
+ if (language === LANGUAGE.python) {
240
+ for (const match of code.matchAll(
241
+ /^[ \t]*from\s+(\.+)([\w.]*)\s+import\b/gmu,
242
+ )) {
243
+ const dots = match[1]?.length ?? 0;
244
+ const specifier = match[2]?.replaceAll('.', '/') ?? '';
245
+ const parent = posix.resolve(
246
+ '/',
247
+ posix.dirname(path),
248
+ ...Array(dots - 1).fill('..'),
249
+ );
250
+ names.add(modulePath(posix.join(parent.slice(1), specifier)));
251
+ }
252
+ }
253
+ if (language === LANGUAGE.rust) {
254
+ let sourceRoot = posix.dirname(path);
255
+ if (path.startsWith('src/')) {
256
+ sourceRoot = 'src';
257
+ } else if (path.includes('/src/')) {
258
+ sourceRoot = path.slice(0, path.indexOf('/src/') + '/src'.length);
259
+ }
260
+ for (const match of code.matchAll(
261
+ /^[ \t]*(?:pub\s+)?(?:use|mod)\s+(crate::)?([\w:]+)\s*[;{]/gmu,
262
+ )) {
263
+ const specifier = match[2]?.replaceAll('::', '/');
264
+ if (specifier) {
265
+ const from = match[1] ? sourceRoot : posix.dirname(path);
266
+ names.add(modulePath(posix.join(from, specifier)));
267
+ }
268
+ }
269
+ }
270
+ if (language === LANGUAGE.php) {
271
+ for (const match of code.matchAll(
272
+ /^[ \t]*(?:require|include)(?:_once)?\s*(?:\(?\s*__DIR__\s*\.\s*)?['"](\.[^'"]+|\/[^'"]+)['"]/gmu,
273
+ )) {
274
+ const specifier = match[1];
275
+ if (specifier) {
276
+ names.add(
277
+ modulePath(
278
+ posix.normalize(posix.join(posix.dirname(path), specifier)),
279
+ ),
280
+ );
281
+ }
282
+ }
283
+ }
284
+ if (language === LANGUAGE.java) {
285
+ for (const match of code.matchAll(
286
+ /^[ \t]*import\s+(?:static\s+)?([\w.]+)\s*;/gmu,
287
+ )) {
288
+ const specifier = match[1]?.replaceAll('.', '/');
289
+ if (specifier) {
290
+ names.add(specifier);
291
+ }
292
+ }
293
+ }
294
+ if (language === LANGUAGE.go) {
295
+ for (const declaration of code.matchAll(
296
+ /^[ \t]*import\s*(?:\(([^)]*)\)|"([^"\n]+)")/gmu,
297
+ )) {
298
+ const quoted = declaration[1] ?? `"${declaration[2] ?? ''}"`;
299
+ for (const match of quoted.matchAll(/"([^"\n]+)"/gu)) {
300
+ if (match[1]) {
301
+ names.add(match[1]);
302
+ }
303
+ }
304
+ }
305
+ }
306
+ return names;
307
+ }
308
+
309
+ function compareText(left: string, right: string): number {
310
+ if (left < right) {
311
+ return -1;
312
+ }
313
+ if (left > right) {
314
+ return 1;
315
+ }
316
+ return 0;
317
+ }
318
+
319
+ function matchesReference(names: ReadonlySet<string>, path: string): boolean {
320
+ const target = modulePath(path);
321
+ if (names.has(target)) {
322
+ return true;
323
+ }
324
+ for (const name of names) {
325
+ if (path.endsWith('.java') && target.endsWith(`/${name}`)) {
326
+ return true;
327
+ }
328
+ if (path.endsWith('.go')) {
329
+ const packageParts = name.split('/');
330
+ const packageSuffix = packageParts.slice(-2).join('/');
331
+ if (
332
+ packageParts.length >= 2 &&
333
+ posix.dirname(path).endsWith(`/${packageSuffix}`)
334
+ ) {
335
+ return true;
336
+ }
337
+ }
338
+ }
339
+ return false;
340
+ }
341
+
342
+ // Naming is evidence only for a sibling test, never for a repository-wide basename match.
343
+ function isCompanionTest(source: string, candidate: string): boolean {
344
+ if (
345
+ isTestPath(source) ||
346
+ !isTestPath(candidate) ||
347
+ posix.dirname(source) !== posix.dirname(candidate)
348
+ ) {
349
+ return false;
350
+ }
351
+ const stem = posix.basename(source, extname(source));
352
+ const testStem = posix.basename(candidate, extname(candidate));
353
+ return (
354
+ testStem === `${stem}.test` ||
355
+ testStem === `${stem}.spec` ||
356
+ testStem === `${stem}_test` ||
357
+ testStem === `test_${stem}` ||
358
+ testStem === `${stem}Test`
359
+ );
360
+ }
@@ -6,6 +6,7 @@ import {
6
6
  isWriteToolResult,
7
7
  type ToolCallEvent,
8
8
  } from '@earendil-works/pi-coding-agent';
9
+ import { commandMatches, detectCheckCommand } from '../done/detect.ts';
9
10
  import {
10
11
  projectRelativePath,
11
12
  resolveFileToolPath,
@@ -14,23 +15,35 @@ import { readFileIfExists } from '../shared/read-file.ts';
14
15
  import { REVIEW_NOTE_EVENT, type ReviewNote } from '../shared/review-note.ts';
15
16
  import { startsUserTurn } from '../shared/settle.ts';
16
17
  import {
17
- askJev,
18
+ askJevRequest,
18
19
  JEV_FAILURE,
19
20
  type JevFailure,
20
21
  type JevTransport,
21
22
  } from './client.ts';
23
+ import { buildContextualState } from './context.ts';
22
24
  import {
23
25
  type AddedLine,
24
26
  addedLinesForEdit,
25
27
  addedLinesForWrite,
26
- buildFileState,
28
+ buildRequestBody,
27
29
  findingKey,
28
30
  formatFindings,
29
31
  isReviewablePath,
30
32
  type JevFinding,
31
- stateFitsBudget,
33
+ type JevRequestBody,
32
34
  } from './model.ts';
33
35
 
36
+ import {
37
+ buildAskReview,
38
+ buildBoardReview,
39
+ completedItems,
40
+ completionIds,
41
+ createWorkflowEvidence,
42
+ recordFileEvidence,
43
+ resetWorkflowEvidence,
44
+ type WorkflowEvidence,
45
+ } from './workflow.ts';
46
+
34
47
  export const JEV_TRANSIENT_FAILURE_LIMIT = 3;
35
48
 
36
49
  const MUTATION_KIND = {
@@ -65,6 +78,7 @@ export interface JevGuardState {
65
78
  rateLimitedThisTurn: boolean;
66
79
  lastModel: string | undefined;
67
80
  calls: number;
81
+ readonly evidence: WorkflowEvidence;
68
82
  readonly pending: Map<string, PendingMutation>;
69
83
  readonly notedThisTurn: Set<string>;
70
84
  }
@@ -82,6 +96,7 @@ export function createJevGuardState(): JevGuardState {
82
96
  rateLimitedThisTurn: false,
83
97
  lastModel: undefined,
84
98
  calls: 0,
99
+ evidence: createWorkflowEvidence(),
85
100
  pending: new Map(),
86
101
  notedThisTurn: new Set(),
87
102
  };
@@ -89,6 +104,7 @@ export function createJevGuardState(): JevGuardState {
89
104
 
90
105
  // The F10 screen calls this after the user stores a key or asks to retry.
91
106
  export function resetJevGuard(state: JevGuardState): void {
107
+ resetWorkflowEvidence(state.evidence);
92
108
  state.halted = undefined;
93
109
  state.transientFailures = 0;
94
110
  state.rateLimitedThisTurn = false;
@@ -97,6 +113,7 @@ export function resetJevGuard(state: JevGuardState): void {
97
113
  // Usage and per-turn memory belong to the active session: a new session starts
98
114
  // counting from zero. The breaker stays, because it tracks the key.
99
115
  export function resetJevSession(state: JevGuardState): void {
116
+ resetWorkflowEvidence(state.evidence);
100
117
  state.calls = 0;
101
118
  state.rateLimitedThisTurn = false;
102
119
  state.pending.clear();
@@ -132,6 +149,7 @@ export function registerJevGuard(
132
149
  if (!startsUserTurn(event)) {
133
150
  return;
134
151
  }
152
+ resetWorkflowEvidence(state.evidence);
135
153
  state.notedThisTurn.clear();
136
154
  state.rateLimitedThisTurn = false;
137
155
  });
@@ -146,7 +164,7 @@ export function registerJevGuard(
146
164
  // the previous content is gone. The body is read after, from disk.
147
165
  pi.on('tool_call', (event, ctx) => {
148
166
  const mutation = extractMutation(event, ctx.cwd);
149
- if (mutation === undefined || !isWorthAsking(mutation)) {
167
+ if (mutation === undefined || !isReviewablePath(mutation.relativePath)) {
150
168
  return undefined;
151
169
  }
152
170
  state.pending.set(event.toolCallId, mutation);
@@ -156,44 +174,108 @@ export function registerJevGuard(
156
174
  pi.on('tool_result', async (event, ctx) => {
157
175
  const pending = state.pending.get(event.toolCallId);
158
176
  state.pending.delete(event.toolCallId);
159
- if (pending === undefined || event.isError) {
160
- return undefined;
161
- }
162
- if (!isWriteToolResult(event) && !isEditToolResult(event)) {
163
- return undefined;
164
- }
165
-
166
177
  const apiKey = dependencies.apiKey();
167
- if (apiKey === undefined) {
168
- dependencies.report(JEV_STATUS.noKey, ctx);
178
+ if (
179
+ apiKey === undefined ||
180
+ !shouldCallJev(state) ||
181
+ ctx.signal?.aborted === true
182
+ ) {
183
+ if (!event.isError && pending !== undefined && isWorthAsking(pending)) {
184
+ dependencies.report(
185
+ apiKey === undefined ? JEV_STATUS.noKey : guardStatus(state),
186
+ ctx,
187
+ );
188
+ }
169
189
  return undefined;
170
190
  }
171
- if (!shouldCallJev(state)) {
172
- dependencies.report(guardStatus(state), ctx);
191
+ if (event.isError) {
192
+ if (
193
+ event.toolName === 'bash' &&
194
+ typeof event.input.command === 'string'
195
+ ) {
196
+ const check = detectCheckCommand(ctx.cwd);
197
+ if (check !== undefined && commandMatches(event.input.command, check)) {
198
+ state.evidence.checks = 'failed';
199
+ }
200
+ }
173
201
  return undefined;
174
202
  }
175
-
176
- const body = readFileIfExists(pending.absolutePath);
177
- if (body === undefined) {
203
+ if (event.toolName === 'bash' && typeof event.input.command === 'string') {
204
+ const check = detectCheckCommand(ctx.cwd);
205
+ if (check !== undefined && commandMatches(event.input.command, check)) {
206
+ state.evidence.checks = 'passed';
207
+ }
178
208
  return undefined;
179
209
  }
180
- const fileState = buildFileState(
181
- pending.relativePath,
182
- body,
183
- pending.kind,
184
- pending.lines,
185
- );
186
- if (!stateFitsBudget(fileState) || ctx.signal?.aborted === true) {
210
+ let requestBody: JevRequestBody | undefined;
211
+ let subject: string;
212
+ if (event.toolName === 'sideroom_ask') {
213
+ if (
214
+ typeof event.details !== 'object' ||
215
+ event.details === null ||
216
+ !('cancelled' in event.details) ||
217
+ event.details.cancelled !== false
218
+ ) {
219
+ return undefined;
220
+ }
221
+ requestBody = buildAskReview(event.input);
222
+ subject = 'sideroom_ask';
223
+ } else if (event.toolName === 'sideroom_todo') {
224
+ const items = completedItems(event.details);
225
+ if (items === undefined) {
226
+ return undefined;
227
+ }
228
+ requestBody = buildBoardReview({
229
+ items,
230
+ evidence: state.evidence,
231
+ changedIds: completionIds(event.input),
232
+ });
233
+ subject = 'sideroom_todo';
234
+ } else {
235
+ if (
236
+ pending === undefined ||
237
+ (!isWriteToolResult(event) && !isEditToolResult(event))
238
+ ) {
239
+ return undefined;
240
+ }
241
+ subject = pending.relativePath;
242
+ state.evidence.checks = 'stale';
243
+ // Replace cached evidence even when this version no longer fits the budget.
244
+ state.evidence.files.delete(subject);
245
+ if (!isWorthAsking(pending)) {
246
+ state.evidence.partial = true;
247
+ return undefined;
248
+ }
249
+ const body = readFileIfExists(pending.absolutePath);
250
+ if (body === undefined) {
251
+ state.evidence.partial = true;
252
+ return undefined;
253
+ }
254
+ const fileState = buildContextualState(
255
+ ctx.cwd,
256
+ subject,
257
+ body,
258
+ pending.kind,
259
+ pending.lines,
260
+ );
261
+ if (fileState === undefined) {
262
+ state.evidence.partial = true;
263
+ return undefined;
264
+ }
265
+ recordFileEvidence(state.evidence, fileState);
266
+ requestBody = buildRequestBody(fileState);
267
+ }
268
+ if (requestBody === undefined) {
187
269
  return undefined;
188
270
  }
189
271
 
190
272
  state.calls += 1;
191
- const attempt = await askJev(
192
- dependencies.transport,
273
+ const attempt = await askJevRequest({
274
+ transport: dependencies.transport,
193
275
  apiKey,
194
- fileState,
195
- ctx.signal,
196
- );
276
+ requestBody,
277
+ signal: ctx.signal,
278
+ });
197
279
  if (attempt.failure !== undefined) {
198
280
  registerFailure(state, attempt.failure);
199
281
  dependencies.report(guardStatus(state), ctx);
@@ -208,14 +290,14 @@ export function registerJevGuard(
208
290
 
209
291
  const findings = attempt.findings ?? [];
210
292
  const fresh = findings.filter((finding) =>
211
- isUnnoted(state, pending.relativePath, finding),
293
+ isUnnoted(state, subject, finding),
212
294
  );
213
295
  if (fresh.length === 0) {
214
296
  return undefined;
215
297
  }
216
- markNoted(state, pending.relativePath, fresh);
298
+ markNoted(state, subject, fresh);
217
299
  const note: ReviewNote = {
218
- text: formatFindings(pending.relativePath, fresh),
300
+ text: formatFindings(subject, fresh),
219
301
  };
220
302
  pi.events.emit(REVIEW_NOTE_EVENT, note);
221
303
  return undefined;
@@ -269,7 +351,7 @@ function registerSuccess(state: JevGuardState): void {
269
351
  state.transientFailures = 0;
270
352
  }
271
353
 
272
- // A deletion-only edit leaves nothing for the six questions to read, and only
354
+ // A deletion-only edit leaves nothing for the questions to read, and only
273
355
  // the project-relative path of a supported language may leave the machine.
274
356
  function isWorthAsking(mutation: PendingMutation): boolean {
275
357
  return mutation.lines.length > 0 && isReviewablePath(mutation.relativePath);