@plannotator/pi-extension 0.27.5 → 0.27.7
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.
- package/README.md +2 -0
- package/generated/ai/providers/child-io.ts +93 -0
- package/generated/ai/providers/codex-app-server.ts +38 -2
- package/generated/ai/providers/pi-sdk-node.ts +46 -4
- package/generated/ai/providers/pi-sdk.ts +54 -6
- package/generated/annotate-args.ts +25 -3
- package/generated/bridge-script.ts +4590 -0
- package/generated/call-flow-types.ts +18 -5
- package/generated/config.ts +2 -1
- package/generated/jj-core.ts +51 -3
- package/generated/live-probe.ts +133 -0
- package/generated/live-proxy-core.ts +555 -0
- package/generated/live-proxy-node.ts +419 -0
- package/generated/prompts.ts +1 -0
- package/generated/review-core.ts +11 -0
- package/index.ts +86 -15
- package/package.json +6 -1
- package/plannotator-browser.ts +22 -1
- package/plannotator.html +3 -3
- package/review-editor.html +3 -3
- package/server/serverAnnotate-live.test.ts +273 -0
- package/server/serverAnnotate.ts +146 -10
- package/skills/plannotator/SKILL.md +185 -0
- package/skills/plannotator/agents/openai.yaml +9 -0
|
@@ -161,7 +161,7 @@ export interface ParsedCallDiffWorkerResult {
|
|
|
161
161
|
diagnostics: CallFlowDiagnostic[];
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
const MAX_TREES =
|
|
164
|
+
const MAX_TREES = 2_000;
|
|
165
165
|
const MAX_NODES = 5_000;
|
|
166
166
|
const MAX_TREE_DEPTH = 32;
|
|
167
167
|
const MAX_DIAGNOSTICS = 100;
|
|
@@ -218,9 +218,16 @@ export function parseCallDiffWorkerResult(value: unknown): ParsedCallDiffWorkerR
|
|
|
218
218
|
if (!isRecord(value.result) || !Array.isArray(value.result.trees)) {
|
|
219
219
|
throw new Error("CallDiff worker response is missing its diff trees.");
|
|
220
220
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
// A large-but-valid result (many small entry trees — common in Swift, TS,
|
|
222
|
+
// and other per-function languages) degrades instead of failing: keep the
|
|
223
|
+
// first MAX_TREES trees and say so in diagnostics (#1351). The caps that
|
|
224
|
+
// guard against genuinely unbounded worker output (total nodes, depth, raw
|
|
225
|
+
// length) still throw below.
|
|
226
|
+
const totalTreeCount = value.result.trees.length;
|
|
227
|
+
const truncatedTrees = totalTreeCount > MAX_TREES;
|
|
228
|
+
const boundedTreeCandidates = truncatedTrees
|
|
229
|
+
? value.result.trees.slice(0, MAX_TREES)
|
|
230
|
+
: value.result.trees;
|
|
224
231
|
|
|
225
232
|
let nodeCount = 0;
|
|
226
233
|
const parseNode = (candidate: unknown, depth: number): CallFlowNode => {
|
|
@@ -254,7 +261,7 @@ export function parseCallDiffWorkerResult(value: unknown): ParsedCallDiffWorkerR
|
|
|
254
261
|
};
|
|
255
262
|
|
|
256
263
|
const raw = boundedRaw(value.result.ascii);
|
|
257
|
-
const trees =
|
|
264
|
+
const trees = boundedTreeCandidates.map((candidate): CallFlowTree => {
|
|
258
265
|
if (!isRecord(candidate)) throw new Error("CallDiff worker returned an invalid tree.");
|
|
259
266
|
const entry = boundedString(candidate.entry, "tree entry");
|
|
260
267
|
const tree = parseNode(candidate.tree, 0);
|
|
@@ -287,6 +294,12 @@ export function parseCallDiffWorkerResult(value: unknown): ParsedCallDiffWorkerR
|
|
|
287
294
|
});
|
|
288
295
|
}
|
|
289
296
|
}
|
|
297
|
+
if (truncatedTrees) {
|
|
298
|
+
diagnostics.push({
|
|
299
|
+
level: "warning",
|
|
300
|
+
message: `Showing the first ${MAX_TREES.toLocaleString("en-US")} of ${totalTreeCount.toLocaleString("en-US")} call trees; the rest were truncated to bound the result.`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
290
303
|
|
|
291
304
|
return {
|
|
292
305
|
version: boundedString(value.version, "version", 100),
|
package/generated/config.ts
CHANGED
package/generated/jj-core.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type JjEvoLogEntry,
|
|
10
10
|
JJ_TRUNK_REVSET,
|
|
11
11
|
jjLineBaseRevset,
|
|
12
|
+
parseRemoteBookmark,
|
|
12
13
|
validateFilePath,
|
|
13
14
|
} from "./review-core.ts";
|
|
14
15
|
|
|
@@ -27,6 +28,31 @@ export interface ReviewJjRuntime {
|
|
|
27
28
|
) => Promise<GitCommandResult>;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// `reachable(@, mutable())` is JJ's definition of the stack being worked on.
|
|
32
|
+
// Its root parents are where that line diverged from immutable history.
|
|
33
|
+
//
|
|
34
|
+
// `latest(..., 1)` is what keeps the query single-record. A criss-cross history
|
|
35
|
+
// can leave several fork points, and the parser below reads one record only, so
|
|
36
|
+
// the tie-break belongs in the revset where it is deliberate and testable
|
|
37
|
+
// rather than in a silent "first row wins" slice. It also matters for
|
|
38
|
+
// correctness: bookmark preference (remote before local) is only meaningful
|
|
39
|
+
// within one commit, so a multi-row answer could otherwise pick a remote
|
|
40
|
+
// bookmark from one commit over a local bookmark on a nearer one.
|
|
41
|
+
const JJ_LINE_BASE_REVSET = "latest(fork_point(roots(reachable(@, mutable()))-), 1)";
|
|
42
|
+
|
|
43
|
+
// `jj git push --change` mints bookmarks under `git.push-bookmark-prefix`
|
|
44
|
+
// (default `push-`). They name one change, not a line of work, so they are
|
|
45
|
+
// never a useful review base. They reach the fork point in practice: a
|
|
46
|
+
// colleague's pushed change bookmark arrives as an untracked remote bookmark,
|
|
47
|
+
// which makes its commit immutable and therefore a candidate base, and the
|
|
48
|
+
// reviewer would be told they are comparing against `push-vmopwunwxopv@origin`.
|
|
49
|
+
const JJ_GENERATED_PUSH_BOOKMARK_PREFIX = "push-";
|
|
50
|
+
|
|
51
|
+
// `commit_id` renders in full, so an all-zero id is the virtual root commit.
|
|
52
|
+
// A repo with no immutable history forks there, and a 40-zero hash means
|
|
53
|
+
// nothing to a reviewer, so keep the `trunk()` sentinel for that case.
|
|
54
|
+
const JJ_ROOT_COMMIT_ID = /^0+$/;
|
|
55
|
+
|
|
30
56
|
export async function detectJjWorkspace(
|
|
31
57
|
runtime: ReviewJjRuntime,
|
|
32
58
|
cwd?: string,
|
|
@@ -369,13 +395,35 @@ export async function selectDefaultJjCompareTarget(
|
|
|
369
395
|
"log",
|
|
370
396
|
"--no-graph",
|
|
371
397
|
"-r",
|
|
372
|
-
|
|
398
|
+
JJ_LINE_BASE_REVSET,
|
|
373
399
|
"-T",
|
|
374
|
-
|
|
400
|
+
'json(bookmarks) ++ "\\t" ++ commit_id ++ "\\n"',
|
|
375
401
|
], { cwd });
|
|
402
|
+
// Every unresolvable case falls back to `trunk()`, which is what this
|
|
403
|
+
// returned before the line-of-work base was inferred at all. The only live
|
|
404
|
+
// caller is `getJjContext`, which runs on the review startup path with no
|
|
405
|
+
// handler above it, so throwing here does not report a problem: it aborts
|
|
406
|
+
// `plannotator review` with a stack trace before the server is built. That
|
|
407
|
+
// also covers a `jj` too old for `fork_point`/`reachable`, where the revset
|
|
408
|
+
// itself fails and the previous default is still perfectly serviceable.
|
|
376
409
|
if (result.exitCode !== 0) return JJ_TRUNK_REVSET;
|
|
377
410
|
|
|
378
|
-
|
|
411
|
+
const [record] = splitJjTemplateRecords(result.stdout);
|
|
412
|
+
if (!record) return JJ_TRUNK_REVSET;
|
|
413
|
+
|
|
414
|
+
const fields = splitJjTemplateFields(record);
|
|
415
|
+
const bookmark = parseJjResolvedBookmarks(fields?.[0] ?? record)
|
|
416
|
+
.find((name) => !isGeneratedPushBookmark(name));
|
|
417
|
+
if (bookmark) return bookmark;
|
|
418
|
+
|
|
419
|
+
const commitId = fields?.[1]?.trim();
|
|
420
|
+
if (commitId && !JJ_ROOT_COMMIT_ID.test(commitId)) return commitId;
|
|
421
|
+
return JJ_TRUNK_REVSET;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function isGeneratedPushBookmark(target: string): boolean {
|
|
425
|
+
const name = parseRemoteBookmark(target)?.name ?? target;
|
|
426
|
+
return name.startsWith(JJ_GENERATED_PUSH_BOOKMARK_PREFIX);
|
|
379
427
|
}
|
|
380
428
|
|
|
381
429
|
function parseJjResolvedBookmarks(value: string): string[] {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// @generated — DO NOT EDIT. Source: packages/shared/live-probe.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared live-app eligibility probe for annotate URL targets.
|
|
4
|
+
*
|
|
5
|
+
* One implementation of the "does this loopback URL front a live dev app?"
|
|
6
|
+
* decision, used by the Bun CLI's annotate resolution
|
|
7
|
+
* (apps/hook/server/annotate-resolution.ts) and the Pi extension's annotate
|
|
8
|
+
* command (vendored to generated/). Both hosts must probe identically: same
|
|
9
|
+
* 3s timeout, same `accept: text/html` request, same "any status < 500"
|
|
10
|
+
* gate, same final-response redirect rule — a drifting copy here would make
|
|
11
|
+
* the same URL open live on one agent and static on another.
|
|
12
|
+
*
|
|
13
|
+
* Pure Web-platform code: global fetch + AbortSignal.timeout, no Bun or
|
|
14
|
+
* node:http APIs.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { isLoopbackHostname } from "./live-proxy-core.ts";
|
|
18
|
+
|
|
19
|
+
export const LIVE_PROBE_TIMEOUT_MS = 3000;
|
|
20
|
+
|
|
21
|
+
export interface LiveAppCandidate {
|
|
22
|
+
parsed: URL | null;
|
|
23
|
+
/** The URL parsed and its hostname is loopback (localhost / ::1 / 127/8). */
|
|
24
|
+
loopback: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Parse an annotate URL target and classify its live-mode candidacy. */
|
|
28
|
+
export function classifyLiveAppCandidate(url: string): LiveAppCandidate {
|
|
29
|
+
let parsed: URL | null = null;
|
|
30
|
+
try {
|
|
31
|
+
parsed = new URL(url);
|
|
32
|
+
} catch {
|
|
33
|
+
parsed = null;
|
|
34
|
+
}
|
|
35
|
+
return { parsed, loopback: parsed !== null && isLoopbackHostname(parsed.hostname) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface LiveAppProbeResult {
|
|
39
|
+
liveEligible: boolean;
|
|
40
|
+
/** Fetch-level failure message, when the probe never completed. */
|
|
41
|
+
probeError: string | null;
|
|
42
|
+
/** Final URL when the probe redirected off the target's loopback origin. */
|
|
43
|
+
probeRedirectedTo: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Probe a loopback http URL for live eligibility. Live eligibility is judged
|
|
48
|
+
* on the FINAL response after redirects: a loopback URL that 302s off its
|
|
49
|
+
* own origin (auth portal, tunnel splash page, another local service) must
|
|
50
|
+
* not open a live session whose iframe immediately navigates off the proxy;
|
|
51
|
+
* same-server redirects (/ to /login) stay live-eligible. Any status < 500
|
|
52
|
+
* with an HTML content type qualifies, so a login page or SPA 404 still
|
|
53
|
+
* opens live.
|
|
54
|
+
*/
|
|
55
|
+
export async function probeLiveAppTarget(
|
|
56
|
+
targetUrl: string,
|
|
57
|
+
parsedUrl: URL,
|
|
58
|
+
fetchImpl: typeof fetch = fetch,
|
|
59
|
+
): Promise<LiveAppProbeResult> {
|
|
60
|
+
let liveEligible = false;
|
|
61
|
+
let probeError: string | null = null;
|
|
62
|
+
let probeRedirectedTo: string | null = null;
|
|
63
|
+
try {
|
|
64
|
+
const probe = await fetchImpl(targetUrl, {
|
|
65
|
+
headers: { accept: "text/html" },
|
|
66
|
+
redirect: "follow",
|
|
67
|
+
signal: AbortSignal.timeout(LIVE_PROBE_TIMEOUT_MS),
|
|
68
|
+
});
|
|
69
|
+
const contentType = probe.headers.get("content-type") ?? "";
|
|
70
|
+
let finalOnOrigin = false;
|
|
71
|
+
try {
|
|
72
|
+
const finalUrl = new URL(probe.url || targetUrl);
|
|
73
|
+
finalOnOrigin =
|
|
74
|
+
finalUrl.protocol === "http:"
|
|
75
|
+
&& isLoopbackHostname(finalUrl.hostname)
|
|
76
|
+
&& (finalUrl.port || "80") === (parsedUrl.port || "80");
|
|
77
|
+
if (!finalOnOrigin) probeRedirectedTo = probe.url;
|
|
78
|
+
} catch {
|
|
79
|
+
finalOnOrigin = false;
|
|
80
|
+
}
|
|
81
|
+
liveEligible = probe.status < 500 && contentType.includes("text/html") && finalOnOrigin;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
probeError = err instanceof Error ? err.message : String(err);
|
|
84
|
+
}
|
|
85
|
+
return { liveEligible, probeError, probeRedirectedTo };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Shared user-facing messages ---------------------------------------------
|
|
89
|
+
// Wording is shared so a Pi user and a CLI user read the SAME explanation for
|
|
90
|
+
// the same condition; hosts differ only in the channel (stderr vs notify).
|
|
91
|
+
|
|
92
|
+
/** --app given with a file/folder target. */
|
|
93
|
+
export const LIVE_APP_REQUIRES_URL_MESSAGE =
|
|
94
|
+
"--app requires a URL target (a running local app, e.g. http://localhost:5173)";
|
|
95
|
+
|
|
96
|
+
/** --app given with a non-loopback URL. */
|
|
97
|
+
export const LIVE_APP_REQUIRES_LOOPBACK_MESSAGE = "--app requires a localhost/loopback URL";
|
|
98
|
+
|
|
99
|
+
/** --app given with an https URL (the live proxy is http-only). */
|
|
100
|
+
export const LIVE_APP_REQUIRES_HTTP_MESSAGE =
|
|
101
|
+
"--app requires an http:// URL (the live app proxy does not support https upstreams)";
|
|
102
|
+
|
|
103
|
+
/** Live sessions are hard-off in remote mode; no override env var exists. */
|
|
104
|
+
export const LIVE_APP_REMOTE_MESSAGE =
|
|
105
|
+
"Live app annotation is unavailable in remote mode (PLANNOTATOR_REMOTE). Run locally, or use --static to annotate a converted snapshot of the page.";
|
|
106
|
+
|
|
107
|
+
/** The --app forced-live failure, keyed on what the probe found. */
|
|
108
|
+
export function buildForceAppFailureMessage(
|
|
109
|
+
targetUrl: string,
|
|
110
|
+
probe: LiveAppProbeResult,
|
|
111
|
+
): string {
|
|
112
|
+
if (probe.probeError !== null) {
|
|
113
|
+
return `--app: could not reach ${targetUrl}: ${probe.probeError}`;
|
|
114
|
+
}
|
|
115
|
+
if (probe.probeRedirectedTo !== null) {
|
|
116
|
+
return `--app: ${targetUrl} redirected off its loopback origin (${probe.probeRedirectedTo}); live mode requires the app to serve HTML from the probed origin`;
|
|
117
|
+
}
|
|
118
|
+
return `--app: ${targetUrl} did not return an HTML page`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Fallback notice when a live-eligible-looking URL probed as unreachable and
|
|
123
|
+
* the session degrades to static conversion. A dev server still starting up
|
|
124
|
+
* probes as unreachable, and a silent downgrade to static conversion reads
|
|
125
|
+
* as a broken live session, so the notice names the condition and the
|
|
126
|
+
* retry-with---app escape hatch.
|
|
127
|
+
*/
|
|
128
|
+
export function buildLiveProbeFallbackNotice(targetUrl: string, probeError: string): string {
|
|
129
|
+
return (
|
|
130
|
+
`Live probe failed for ${targetUrl} (${probeError}); using static conversion. `
|
|
131
|
+
+ `If a dev server is still starting up, retry with --app to force live mode.`
|
|
132
|
+
);
|
|
133
|
+
}
|