@plannotator/pi-extension 0.17.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { execSync, spawnSync } from "node:child_process";
2
- import { readFileSync } from "node:fs";
2
+ import { readFileSync, existsSync } from "node:fs";
3
3
  import { createServer } from "node:http";
4
+ import os from "node:os";
4
5
 
5
6
  import { Readable } from "node:stream";
6
7
 
@@ -46,7 +47,7 @@ import {
46
47
  } from "./handlers.js";
47
48
  import { html, json, parseBody, requestUrl, toWebRequest } from "./helpers.js";
48
49
 
49
- import { listenOnPort } from "./network.js";
50
+ import { isRemoteSession, listenOnPort } from "./network.js";
50
51
  import {
51
52
  fetchPRContext,
52
53
  fetchPRFileContent,
@@ -71,10 +72,24 @@ import {
71
72
  transformClaudeFindings,
72
73
  } from "../generated/claude-review.js";
73
74
 
75
+ /** Detect if running inside WSL (Windows Subsystem for Linux) */
76
+ function detectWSL(): boolean {
77
+ if (process.platform !== "linux") return false;
78
+ if (os.release().toLowerCase().includes("microsoft")) return true;
79
+ try {
80
+ if (existsSync("/proc/version")) {
81
+ const content = readFileSync("/proc/version", "utf-8").toLowerCase();
82
+ return content.includes("wsl") || content.includes("microsoft");
83
+ }
84
+ } catch { /* ignore */ }
85
+ return false;
86
+ }
87
+
74
88
  export interface ReviewServerResult {
75
89
  port: number;
76
90
  portSource: "env" | "remote-default" | "random";
77
91
  url: string;
92
+ isRemote: boolean;
78
93
  waitForDecision: () => Promise<{
79
94
  approved: boolean;
80
95
  feedback: string;
@@ -84,7 +99,7 @@ export interface ReviewServerResult {
84
99
  stop: () => void;
85
100
  }
86
101
 
87
- const reviewRuntime: ReviewGitRuntime = {
102
+ export const reviewRuntime: ReviewGitRuntime = {
88
103
  async runGit(
89
104
  args: string[],
90
105
  options?: { cwd?: string },
@@ -132,11 +147,20 @@ export async function startReviewServer(options: {
132
147
  sharingEnabled?: boolean;
133
148
  shareBaseUrl?: string;
134
149
  prMetadata?: PRMetadata;
150
+ /** Working directory for agent processes (e.g., --local worktree). Independent of diff pipeline. */
151
+ agentCwd?: string;
152
+ /** Cleanup callback invoked when server stops (e.g., remove temp worktree) */
153
+ onCleanup?: () => void | Promise<void>;
154
+ /** Called when server starts with the URL, remote status, and port */
155
+ onReady?: (url: string, isRemote: boolean, port: number) => void;
135
156
  }): Promise<ReviewServerResult> {
136
157
  const gitUser = detectGitUser();
137
158
  const draftKey = contentHash(options.rawPatch);
138
159
  const prMeta = options.prMetadata;
139
160
  const isPRMode = !!prMeta;
161
+ const hasLocalAccess = !!options.gitContext;
162
+ const isRemote = isRemoteSession();
163
+ const wslFlag = detectWSL();
140
164
  const prRef = prMeta ? prRefFromMetadata(prMeta) : null;
141
165
  const platformUser = prRef ? await getPRUser(prRef) : null;
142
166
 
@@ -170,6 +194,7 @@ export async function startReviewServer(options: {
170
194
  let serverUrl = "";
171
195
  // Worktree-aware cwd resolver — shared by getCwd, buildCommand, and onJobComplete
172
196
  function resolveAgentCwd(): string {
197
+ if (options.agentCwd) return options.agentCwd;
173
198
  if (currentDiffType.startsWith("worktree:")) {
174
199
  const parsed = parseWorktreeDiffType(currentDiffType);
175
200
  if (parsed) return parsed.path;
@@ -183,11 +208,11 @@ export async function startReviewServer(options: {
183
208
 
184
209
  async buildCommand(provider) {
185
210
  const cwd = resolveAgentCwd();
186
- const hasLocalAccess = !!options.gitContext;
211
+ const hasAgentLocalAccess = !!options.agentCwd || !!options.gitContext;
187
212
  const userMessage = buildCodexReviewUserMessage(
188
213
  currentPatch,
189
214
  currentDiffType,
190
- { defaultBranch: options.gitContext?.defaultBranch, hasLocalAccess },
215
+ { defaultBranch: options.gitContext?.defaultBranch, hasLocalAccess: hasAgentLocalAccess },
191
216
  options.prMetadata,
192
217
  );
193
218
 
@@ -214,8 +239,11 @@ export async function startReviewServer(options: {
214
239
  const output = await parseCodexOutput(meta.outputPath);
215
240
  if (!output) return;
216
241
 
242
+ // Override verdict if there are blocking findings (P0/P1) — Codex's
243
+ // freeform correctness string can say "mostly correct" with real bugs.
244
+ const hasBlockingFindings = output.findings.some((f: any) => f.priority !== null && f.priority <= 1);
217
245
  job.summary = {
218
- correctness: output.overall_correctness,
246
+ correctness: hasBlockingFindings ? "Issues Found" : output.overall_correctness,
219
247
  explanation: output.overall_explanation,
220
248
  confidence: output.overall_confidence_score,
221
249
  };
@@ -369,13 +397,7 @@ export async function startReviewServer(options: {
369
397
  aiEndpoints = ai.createAIEndpoints({
370
398
  registry,
371
399
  sessionManager,
372
- getCwd: () => {
373
- if (currentDiffType.startsWith("worktree:")) {
374
- const parsed = parseWorktreeDiffType(currentDiffType);
375
- if (parsed) return parsed.path;
376
- }
377
- return options.gitContext?.cwd ?? process.cwd();
378
- },
400
+ getCwd: resolveAgentCwd,
379
401
  });
380
402
  aiSessionManager = sessionManager;
381
403
  aiRegistry = registry;
@@ -392,19 +414,21 @@ export async function startReviewServer(options: {
392
414
  rawPatch: currentPatch,
393
415
  gitRef: currentGitRef,
394
416
  origin: options.origin ?? "pi",
395
- diffType: isPRMode ? undefined : currentDiffType,
396
- gitContext: isPRMode ? undefined : options.gitContext,
417
+ diffType: hasLocalAccess ? currentDiffType : undefined,
418
+ gitContext: hasLocalAccess ? options.gitContext : undefined,
397
419
  sharingEnabled,
398
420
  shareBaseUrl,
399
421
  repoInfo,
422
+ isWSL: wslFlag,
423
+ ...(options.agentCwd && { agentCwd: options.agentCwd }),
400
424
  ...(isPRMode && { prMetadata: prMeta, platformUser }),
401
425
  ...(isPRMode && initialViewedFiles.length > 0 && { viewedFiles: initialViewedFiles }),
402
- ...(currentError ? { error: currentError } : {}),
426
+ ...(currentError && { error: currentError }),
403
427
  serverConfig: getServerConfig(gitUser),
404
428
  });
405
429
  } else if (url.pathname === "/api/diff/switch" && req.method === "POST") {
406
- if (isPRMode) {
407
- json(res, { error: "Not available for PR reviews" }, 400);
430
+ if (!hasLocalAccess) {
431
+ json(res, { error: "Not available without local file access" }, 400);
408
432
  return;
409
433
  }
410
434
  try {
@@ -456,23 +480,21 @@ export async function startReviewServer(options: {
456
480
  }
457
481
  try {
458
482
  const body = await parseBody(req);
483
+ const fileComments = (body.fileComments as PRReviewFileComment[]) || [];
484
+ console.error(`[pr-action] ${body.action} with ${fileComments.length} file comment(s), headSha=${prMeta.headSha}`);
459
485
  await submitPRReview(
460
486
  prRef,
461
487
  prMeta.headSha,
462
488
  body.action as "approve" | "comment",
463
489
  body.body as string,
464
- (body.fileComments as PRReviewFileComment[]) || [],
490
+ fileComments,
465
491
  );
492
+ console.error(`[pr-action] Success`);
466
493
  json(res, { ok: true, prUrl: prMeta.url });
467
494
  } catch (err) {
468
- json(
469
- res,
470
- {
471
- error:
472
- err instanceof Error ? err.message : "Failed to submit PR review",
473
- },
474
- 500,
475
- );
495
+ const message = err instanceof Error ? err.message : "Failed to submit PR review";
496
+ console.error(`[pr-action] Failed: ${message}`);
497
+ json(res, { error: message }, 500);
476
498
  }
477
499
  } else if (url.pathname === "/api/pr-viewed" && req.method === "POST") {
478
500
  if (!isPRMode || !prMeta || !prRef) {
@@ -498,14 +520,9 @@ export async function startReviewServer(options: {
498
520
  );
499
521
  json(res, { ok: true });
500
522
  } catch (err) {
501
- json(
502
- res,
503
- {
504
- error:
505
- err instanceof Error ? err.message : "Failed to update viewed state",
506
- },
507
- 500,
508
- );
523
+ const message = err instanceof Error ? err.message : "Failed to update viewed state";
524
+ console.error("[plannotator] /api/pr-viewed error:", message);
525
+ json(res, { error: message }, 500);
509
526
  }
510
527
  } else if (url.pathname === "/api/file-content" && req.method === "GET") {
511
528
  const filePath = url.searchParams.get("path");
@@ -529,6 +546,23 @@ export async function startReviewServer(options: {
529
546
  }
530
547
  }
531
548
 
549
+ // Local mode first (matches Bun server priority)
550
+ if (hasLocalAccess && !isPRMode) {
551
+ const defaultBranch = options.gitContext?.defaultBranch || "main";
552
+ const defaultCwd = options.gitContext?.cwd;
553
+ const result = await getFileContentsForDiffCore(
554
+ reviewRuntime,
555
+ currentDiffType,
556
+ defaultBranch,
557
+ filePath,
558
+ oldPath,
559
+ defaultCwd,
560
+ );
561
+ json(res, result);
562
+ return;
563
+ }
564
+
565
+ // PR mode: fetch from platform API using merge-base/head SHAs
532
566
  if (isPRMode && prRef && prMeta) {
533
567
  try {
534
568
  const oldSha = prMeta.mergeBaseSha ?? prMeta.baseSha;
@@ -552,17 +586,7 @@ export async function startReviewServer(options: {
552
586
  return;
553
587
  }
554
588
 
555
- const defaultBranch = options.gitContext?.defaultBranch || "main";
556
- const defaultCwd = options.gitContext?.cwd;
557
- const result = await getFileContentsForDiffCore(
558
- reviewRuntime,
559
- currentDiffType,
560
- defaultBranch,
561
- filePath,
562
- oldPath,
563
- defaultCwd,
564
- );
565
- json(res, result);
589
+ json(res, { error: "No file access available" }, 400);
566
590
  } else if (url.pathname === "/api/config" && req.method === "POST") {
567
591
  try {
568
592
  const body = (await parseBody(req)) as { displayName?: string; diffOptions?: Record<string, unknown> };
@@ -581,8 +605,14 @@ export async function startReviewServer(options: {
581
605
  } else if (url.pathname === "/api/agents" && req.method === "GET") {
582
606
  json(res, { agents: [] });
583
607
  } else if (url.pathname === "/api/git-add" && req.method === "POST") {
584
- if (isPRMode) {
585
- json(res, { error: "Not available for PR reviews" }, 400);
608
+ // Staging only available for local diff types that support it (not PR mode, not branch diffs).
609
+ // Worktree diff types use composite format "worktree:/path:uncommitted" — extract the base type.
610
+ const baseDiffType = currentDiffType.startsWith("worktree:")
611
+ ? (parseWorktreeDiffType(currentDiffType)?.subType ?? currentDiffType)
612
+ : currentDiffType;
613
+ const canStage = baseDiffType === "uncommitted" || baseDiffType === "unstaged";
614
+ if (isPRMode || !canStage) {
615
+ json(res, { error: "Staging not available" }, 400);
586
616
  return;
587
617
  }
588
618
  try {
@@ -608,7 +638,7 @@ export async function startReviewServer(options: {
608
638
  json(res, { ok: true });
609
639
  } catch (err) {
610
640
  const message =
611
- err instanceof Error ? err.message : "Failed to git add";
641
+ err instanceof Error ? err.message : "Failed to stage file";
612
642
  json(res, { error: message }, 500);
613
643
  }
614
644
  } else if (url.pathname === "/api/draft") {
@@ -674,10 +704,15 @@ export async function startReviewServer(options: {
674
704
  const exitHandler = () => agentJobs.killAll();
675
705
  process.once("exit", exitHandler);
676
706
 
707
+ if (options.onReady) {
708
+ options.onReady(serverUrl, isRemote, port);
709
+ }
710
+
677
711
  return {
678
712
  port,
679
713
  portSource,
680
714
  url: serverUrl,
715
+ isRemote,
681
716
  waitForDecision: () => decisionPromise,
682
717
  stop: () => {
683
718
  process.removeListener("exit", exitHandler);
@@ -685,6 +720,13 @@ export async function startReviewServer(options: {
685
720
  aiSessionManager?.disposeAll();
686
721
  aiRegistry?.disposeAll();
687
722
  server.close();
723
+ // Invoke cleanup callback (e.g., remove temp worktree)
724
+ if (options.onCleanup) {
725
+ try {
726
+ const result = options.onCleanup();
727
+ if (result instanceof Promise) result.catch(() => {});
728
+ } catch { /* best effort */ }
729
+ }
688
730
  },
689
731
  };
690
732
  }
package/server.ts CHANGED
@@ -21,6 +21,7 @@ export {
21
21
  } from "./server/serverPlan.js";
22
22
  export {
23
23
  getGitContext,
24
+ reviewRuntime,
24
25
  type ReviewServerResult,
25
26
  runGitDiff,
26
27
  startReviewServer,