@dev.fast/whiteboard 0.0.0-stage → 0.2.1-preview.20261005.98
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/LICENSE +21 -0
- package/README.md +146 -2
- package/THIRD_PARTY_NOTICES.md +93 -0
- package/dist/account-alias-DOHN1RSH.js +973 -0
- package/dist/agent-cli-ChOq0ZuO.js +117 -0
- package/dist/agent-cli-DzWfZr6G.js +2 -0
- package/dist/agent-client-CPQo7iTI.js +305 -0
- package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
- package/dist/build-info.json +1 -0
- package/dist/cli-hVgHcdsv.js +47 -0
- package/dist/cli-runner-D8y2_luU.js +5095 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -0
- package/dist/client-DFoh50W3.js +950 -0
- package/dist/desktop-discovery-DBO-PM2V.js +164 -0
- package/dist/error-message-OtiDonty.js +6 -0
- package/dist/fs-utils-BMPLt0cr.js +22 -0
- package/dist/fuzzy-match-BoAmcyak.js +30 -0
- package/dist/headless-host-BJxf4dUU.js +94 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/local-data-CxLk75rx.js +4491 -0
- package/dist/mcp-QeF8vkdf.js +140 -0
- package/dist/package-paths-B6-zxvIO.js +47 -0
- package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
- package/dist/profile-4ry2f3AM.js +76 -0
- package/dist/profile-Bh5FIrmf.js +2 -0
- package/dist/request-origin-D_4QIdoN.js +811 -0
- package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
- package/dist/review-home-paths-6zZH-1c9.js +2560 -0
- package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +2 -0
- package/dist/s3-SzLpeCG6.js +345 -0
- package/dist/s3-config-BgxkSoOy.js +2 -0
- package/dist/s3-config-CAxhO9u_.js +676 -0
- package/dist/server/desktop-host.d.ts +4 -0
- package/dist/server/desktop-host.js +1702 -0
- package/dist/server-discovery-Dbzn3w6Z.js +66 -0
- package/dist/sharing/index.d.ts +1977 -0
- package/dist/sharing/index.js +2 -0
- package/dist/src-CGP5ytbV.js +283 -0
- package/dist/src-CmdiBL20.js +3676 -0
- package/dist/src-DnwdaQ2r.js +865 -0
- package/dist/src-X9phtB2j.js +68 -0
- package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
- package/dist/tool-failure-C8zB73HV.js +98 -0
- package/dist/tutorial-trace-DrBvkaeL.js +43 -0
- package/instructions/authoring.md +36 -0
- package/instructions/file-lenses.md +13 -0
- package/instructions/scratchpad.md +23 -0
- package/instructions/trace-archaeology.md +113 -0
- package/onboarding.md +8 -0
- package/package.json +106 -3
- package/src/agent-selection.ts +100 -0
- package/src/agent-session-ref.ts +80 -0
- package/src/ask/agents.ts +351 -0
- package/src/ask/checkout-files.ts +47 -0
- package/src/ask/file-refs.ts +111 -0
- package/src/ask/pi-mcp.ts +154 -0
- package/src/ask/protocol.ts +244 -0
- package/src/ask/thread-state.ts +350 -0
- package/src/ask/thread.ts +1400 -0
- package/src/ask/threads.ts +219 -0
- package/src/ask/watch.ts +79 -0
- package/src/cli-install.ts +1041 -0
- package/src/cli-runner.ts +1461 -0
- package/src/cli-runtime-info.ts +35 -0
- package/src/cli.ts +222 -0
- package/src/connect-prompts.ts +248 -0
- package/src/cursor-deeplink.ts +11 -0
- package/src/desktop-discovery.ts +355 -0
- package/src/diff-selection-migration.ts +63 -0
- package/src/embedded-posthog-key.ts +6 -0
- package/src/error-telemetry.ts +247 -0
- package/src/evidence.ts +14 -0
- package/src/exception-telemetry.ts +126 -0
- package/src/fixtures/blocks/call_stack_diff.json +27 -0
- package/src/fixtures/blocks/callout.json +11 -0
- package/src/fixtures/blocks/code.json +9 -0
- package/src/fixtures/blocks/code_peek.json +8 -0
- package/src/fixtures/blocks/database_lens.json +51 -0
- package/src/fixtures/blocks/divider.json +1 -0
- package/src/fixtures/blocks/fixtures.ts +33 -0
- package/src/fixtures/blocks/flow_diagram.json +36 -0
- package/src/fixtures/blocks/ids.ts +8 -0
- package/src/fixtures/blocks/image.json +9 -0
- package/src/fixtures/blocks/markdown.json +7 -0
- package/src/fixtures/blocks/section.json +11 -0
- package/src/fixtures/blocks/sequence.json +31 -0
- package/src/fixtures/blocks/software_map.json +7 -0
- package/src/fixtures/blocks/trace_quote.json +9 -0
- package/src/fixtures/blocks/tutorial.json +51 -0
- package/src/fs-utils.ts +32 -0
- package/src/fuzzy-match.ts +94 -0
- package/src/install.ts +29 -0
- package/src/legacy-skills.ts +133 -0
- package/src/lens-selection.ts +230 -0
- package/src/markdown-latex-math.ts +230 -0
- package/src/markdown.ts +81 -0
- package/src/package-paths.ts +53 -0
- package/src/posthog-capture-client.ts +610 -0
- package/src/review-api/README.md +295 -0
- package/src/review-api/activity.ts +340 -0
- package/src/review-api/agent-cli.ts +219 -0
- package/src/review-api/agent-client.ts +188 -0
- package/src/review-api/anchor-quotes.ts +88 -0
- package/src/review-api/ask-history.ts +242 -0
- package/src/review-api/authoring-tools.ts +211 -0
- package/src/review-api/blocks/call_stack_diff.ts +75 -0
- package/src/review-api/blocks/callout.ts +24 -0
- package/src/review-api/blocks/code.ts +9 -0
- package/src/review-api/blocks/code_peek.ts +13 -0
- package/src/review-api/blocks/database_lens.ts +150 -0
- package/src/review-api/blocks/definition.ts +40 -0
- package/src/review-api/blocks/divider.ts +6 -0
- package/src/review-api/blocks/flow_diagram.ts +111 -0
- package/src/review-api/blocks/image.ts +10 -0
- package/src/review-api/blocks/index.ts +90 -0
- package/src/review-api/blocks/markdown.ts +17 -0
- package/src/review-api/blocks/section.ts +24 -0
- package/src/review-api/blocks/sequence.ts +56 -0
- package/src/review-api/blocks/software_map.ts +9 -0
- package/src/review-api/blocks/trace_quote.ts +10 -0
- package/src/review-api/blocks/tutorial.ts +39 -0
- package/src/review-api/checkout-fs.ts +14 -0
- package/src/review-api/client.ts +1 -0
- package/src/review-api/comparison-coverage.ts +304 -0
- package/src/review-api/component-reference.ts +30 -0
- package/src/review-api/diff-lenses.ts +175 -0
- package/src/review-api/document-headings.ts +51 -0
- package/src/review-api/document-text.ts +200 -0
- package/src/review-api/document.ts +906 -0
- package/src/review-api/file-lenses.ts +100 -0
- package/src/review-api/http.ts +1877 -0
- package/src/review-api/image-decode.ts +29 -0
- package/src/review-api/input-error.ts +9 -0
- package/src/review-api/instructions.ts +89 -0
- package/src/review-api/lens-alignment.ts +52 -0
- package/src/review-api/local-data.ts +1837 -0
- package/src/review-api/map-input.ts +154 -0
- package/src/review-api/mcp-client-agent.ts +32 -0
- package/src/review-api/mcp.ts +240 -0
- package/src/review-api/origin.ts +43 -0
- package/src/review-api/profile.ts +146 -0
- package/src/review-api/public-tools.ts +102 -0
- package/src/review-api/pull-request.ts +389 -0
- package/src/review-api/read-schemas.ts +85 -0
- package/src/review-api/recovery.ts +2 -0
- package/src/review-api/request-origin.ts +33 -0
- package/src/review-api/review-progress.ts +382 -0
- package/src/review-api/status-tool.ts +10 -0
- package/src/review-api/store-schema.ts +22 -0
- package/src/review-api/store.ts +1647 -0
- package/src/review-api/tool-failure.ts +64 -0
- package/src/review-api/trace-schema.ts +13 -0
- package/src/review-api/traces.ts +119 -0
- package/src/review-api/unsupported-files.integration.ts +91 -0
- package/src/review-api/workspaces.ts +692 -0
- package/src/review-api/worktree-source.ts +170 -0
- package/src/review-api/worktree-structural.integration.ts +320 -0
- package/src/review-app-launcher.ts +432 -0
- package/src/review-app-picker.ts +168 -0
- package/src/review-app.ts +134 -0
- package/src/review-bundled-tools.ts +249 -0
- package/src/review-checkout-paths.ts +37 -0
- package/src/review-diff-files.ts +89 -0
- package/src/review-head-checkout.ts +300 -0
- package/src/review-home-paths.ts +73 -0
- package/src/review-info.ts +59 -0
- package/src/review-instances.ts +113 -0
- package/src/review-logger.ts +183 -0
- package/src/review-preferences.ts +88 -0
- package/src/review-prepare.ts +291 -0
- package/src/review-stack.ts +112 -0
- package/src/review-telemetry.ts +1066 -0
- package/src/runtime.ts +74 -0
- package/src/server/account-alias.ts +30 -0
- package/src/server/bounded-stream.ts +34 -0
- package/src/server/bug-report.ts +301 -0
- package/src/server/client-error-budget.ts +45 -0
- package/src/server/crash-report.ts +273 -0
- package/src/server/desktop-host-shutdown.ts +64 -0
- package/src/server/desktop-host.ts +190 -0
- package/src/server/desktop-server.ts +703 -0
- package/src/server/diffr-config.ts +509 -0
- package/src/server/diffr-languages.ts +97 -0
- package/src/server/global-verb-relay.ts +190 -0
- package/src/server/headless-host.ts +149 -0
- package/src/server/hono-http.ts +162 -0
- package/src/server/http-json.ts +24 -0
- package/src/server/json-review-reporting.ts +174 -0
- package/src/server/process-error-telemetry.ts +174 -0
- package/src/server/review-api-parsers.ts +90 -0
- package/src/server/review-lifecycle-telemetry.ts +73 -0
- package/src/server/review-open-watchdog.ts +46 -0
- package/src/server/review-server-core.ts +290 -0
- package/src/server/structural-comparisons.ts +153 -0
- package/src/server/structural-diff.ts +186 -0
- package/src/server/tutorial-service.ts +241 -0
- package/src/server/ui-telemetry.ts +198 -0
- package/src/server-discovery.ts +95 -0
- package/src/session-markers.ts +132 -0
- package/src/sharing/auth.ts +41 -0
- package/src/sharing/cli.ts +83 -0
- package/src/sharing/client.ts +334 -0
- package/src/sharing/export.ts +195 -0
- package/src/sharing/host.ts +316 -0
- package/src/sharing/import.ts +787 -0
- package/src/sharing/index.ts +16 -0
- package/src/sharing/repository.ts +145 -0
- package/src/sharing/routes.ts +34 -0
- package/src/slug.ts +23 -0
- package/src/software-map-diff-counts.ts +517 -0
- package/src/software-map-model.ts +1147 -0
- package/src/software-map-topology-diff.ts +260 -0
- package/src/source.ts +92 -0
- package/src/startup-trace.ts +232 -0
- package/src/stored-document-migration.ts +151 -0
- package/src/telemetry-clean-text.ts +257 -0
- package/src/telemetry-config.ts +298 -0
- package/src/telemetry-debug-sink.ts +38 -0
- package/src/telemetry.ts +13 -0
- package/src/trace-cli.ts +156 -0
- package/src/trace-storage-cli.ts +509 -0
- package/src/tutorial-conversation.ts +18 -0
- package/src/ui-telemetry-events.ts +765 -0
- package/src/unified-diff.ts +71 -0
- package/src/viewed-coverage.ts +259 -0
- package/src/windows-cli.ts +125 -0
- package/tutorial/document.json +275 -0
- package/tutorial/runtime-manifest.json +12 -0
- package/tutorial/sample-service/package.json +9 -0
- package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
- package/tutorial/sample-service/src/app.ts +26 -0
- package/tutorial/sample-service/src/database/schema.ts +10 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
- package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
- package/tutorial/sample-service/src/orders/order-service.ts +34 -0
- package/tutorial/sample-service/src/orders/order.ts +21 -0
- package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
- package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
- package/tutorial/sample-service/tsconfig.json +12 -0
- package/tutorial/software-map.json +139 -0
- package/tutorial/trace.json +20 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type JsonValue, isJsonObject, isStringValue } from "@dev.fast/json";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AuthoringTool,
|
|
5
|
+
ToolText,
|
|
6
|
+
callAuthoringTool,
|
|
7
|
+
} from "./agent-client.js";
|
|
8
|
+
import type { ReviewApiClient } from "./client.js";
|
|
9
|
+
|
|
10
|
+
function publicDescription(text: string): string {
|
|
11
|
+
return text
|
|
12
|
+
.replace(/\breview_(\w+)/g, "session_$1")
|
|
13
|
+
.replace(/\breviewId\b/g, "sessionId")
|
|
14
|
+
.replace(/\bReview\b/g, "Whiteboard")
|
|
15
|
+
.replace("The result carries review,", "The result carries session,");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Rewrite schema prose without changing property names, enum values or examples. */
|
|
19
|
+
function translateSchemaDescriptions(value: JsonValue): void {
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
value.forEach(translateSchemaDescriptions);
|
|
22
|
+
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!isJsonObject(value)) return;
|
|
27
|
+
|
|
28
|
+
for (const [key, item] of Object.entries(value)) {
|
|
29
|
+
if (key === "description" && isStringValue(item))
|
|
30
|
+
value[key] = publicDescription(item);
|
|
31
|
+
else if (!["examples", "default", "const", "enum"].includes(key))
|
|
32
|
+
translateSchemaDescriptions(item);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Public vocabulary belongs to the agent boundary, not stored documents. */
|
|
37
|
+
export function publicTool(tool: AuthoringTool): AuthoringTool {
|
|
38
|
+
const inputSchema = structuredClone(tool.inputSchema);
|
|
39
|
+
|
|
40
|
+
if (isJsonObject(inputSchema)) translateSchemaDescriptions(inputSchema);
|
|
41
|
+
|
|
42
|
+
inputSchema.properties = Object.fromEntries(
|
|
43
|
+
Object.entries(inputSchema.properties ?? {}).map(([key, value]) => [
|
|
44
|
+
key === "reviewId" ? "sessionId" : key,
|
|
45
|
+
value,
|
|
46
|
+
]),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (inputSchema.required)
|
|
50
|
+
inputSchema.required = inputSchema.required.map((key) =>
|
|
51
|
+
key === "reviewId" ? "sessionId" : key,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
...tool,
|
|
56
|
+
name: tool.name.replace(/^review_/, "session_"),
|
|
57
|
+
description: publicDescription(tool.description),
|
|
58
|
+
inputSchema,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Translate known response envelopes only; authored content is opaque. */
|
|
63
|
+
export function publicResult(value: JsonValue): JsonValue {
|
|
64
|
+
if (Array.isArray(value)) return value.map(publicResult);
|
|
65
|
+
|
|
66
|
+
if (!isJsonObject(value)) return value;
|
|
67
|
+
|
|
68
|
+
return Object.fromEntries(
|
|
69
|
+
Object.entries(value).map(([key, item]) => {
|
|
70
|
+
const name =
|
|
71
|
+
key === "reviewId"
|
|
72
|
+
? "sessionId"
|
|
73
|
+
: key === "review"
|
|
74
|
+
? "session"
|
|
75
|
+
: key === "reviews"
|
|
76
|
+
? "sessions"
|
|
77
|
+
: key;
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
name,
|
|
81
|
+
key === "review" || key === "reviews" ? publicResult(item) : item,
|
|
82
|
+
];
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function callPublicTool(
|
|
88
|
+
client: ReviewApiClient,
|
|
89
|
+
tool: AuthoringTool,
|
|
90
|
+
input: Parameters<typeof callAuthoringTool>[2],
|
|
91
|
+
signal?: AbortSignal,
|
|
92
|
+
) {
|
|
93
|
+
if ("reviewId" in input) throw new Error("Use sessionId with session tools.");
|
|
94
|
+
const { sessionId, ...rest } = input;
|
|
95
|
+
|
|
96
|
+
const fields: Parameters<typeof callAuthoringTool>[2] = { ...rest };
|
|
97
|
+
|
|
98
|
+
if (sessionId !== undefined) fields.reviewId = sessionId;
|
|
99
|
+
const result = await callAuthoringTool(client, tool, fields, signal);
|
|
100
|
+
|
|
101
|
+
return result instanceof ToolText ? result : publicResult(result);
|
|
102
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
import { type LocalVcsKind, parseGitRemote } from "@dev.fast/local-vcs";
|
|
5
|
+
import { errorMessage } from "@dev.fast/trace-core";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
import { ReviewInputError } from "./document.js";
|
|
9
|
+
|
|
10
|
+
/** Runs one subprocess and resolves its stdout; rejects on failure or timeout. */
|
|
11
|
+
export type RunCommand = (
|
|
12
|
+
file: string,
|
|
13
|
+
args: string[],
|
|
14
|
+
options: { cwd?: string; timeoutMs: number },
|
|
15
|
+
) => Promise<string>;
|
|
16
|
+
|
|
17
|
+
export interface PullRequestDeps {
|
|
18
|
+
run: RunCommand;
|
|
19
|
+
fetch: typeof fetch;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** What GitHub says about a PR; the commits are fetched separately. */
|
|
23
|
+
export interface PullRequestRecord {
|
|
24
|
+
host: string;
|
|
25
|
+
slug: string;
|
|
26
|
+
number: number;
|
|
27
|
+
title: string;
|
|
28
|
+
baseRefName: string;
|
|
29
|
+
/** GitHub's base commit, frozen at the PR's last update. */
|
|
30
|
+
baseRefOid?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const GH_TIMEOUT_MS = 20_000;
|
|
34
|
+
|
|
35
|
+
const API_TIMEOUT_MS = 15_000;
|
|
36
|
+
|
|
37
|
+
const FETCH_TIMEOUT_MS = 120_000;
|
|
38
|
+
|
|
39
|
+
const LOCAL_TIMEOUT_MS = 30_000;
|
|
40
|
+
|
|
41
|
+
export const defaultPullRequestDeps: PullRequestDeps = {
|
|
42
|
+
run: (file, args, options) =>
|
|
43
|
+
new Promise((resolve, reject) => {
|
|
44
|
+
execFile(
|
|
45
|
+
file,
|
|
46
|
+
args,
|
|
47
|
+
{
|
|
48
|
+
cwd: options.cwd,
|
|
49
|
+
timeout: options.timeoutMs,
|
|
50
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
51
|
+
// Never wait on a credential or confirmation prompt nobody can see.
|
|
52
|
+
env: {
|
|
53
|
+
...process.env,
|
|
54
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
55
|
+
GH_PROMPT_DISABLED: "1",
|
|
56
|
+
GIT_SSH_COMMAND:
|
|
57
|
+
process.env.GIT_SSH_COMMAND ?? "ssh -o BatchMode=yes",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
(error, stdout, stderr) => {
|
|
61
|
+
if (!error) return resolve(stdout);
|
|
62
|
+
|
|
63
|
+
const reason = error.killed
|
|
64
|
+
? `timed out after ${options.timeoutMs / 1000}s`
|
|
65
|
+
: String(stderr).trim() || error.message;
|
|
66
|
+
|
|
67
|
+
reject(new Error(`${file} ${args[0]}: ${reason}`));
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
}),
|
|
71
|
+
fetch: (input, init) => fetch(input, init),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Host, owner/repo and number from a canonical PR URL (validated by the command schema). */
|
|
75
|
+
export function pullRequestAddress(url: string) {
|
|
76
|
+
const { hostname, pathname } = new URL(url);
|
|
77
|
+
const [, owner, repo, , number] = pathname.split("/");
|
|
78
|
+
|
|
79
|
+
return { host: hostname, slug: `${owner}/${repo}`, number: Number(number) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const metadataSchema = z.object({
|
|
83
|
+
number: z.number().int().positive(),
|
|
84
|
+
title: z.string(),
|
|
85
|
+
baseRefName: z.string().min(1),
|
|
86
|
+
baseRefOid: z.string().optional(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/** gh first (it carries the user's auth), then, for github.com only, GitHub's
|
|
90
|
+
* public REST API: Enterprise hosts require gh. */
|
|
91
|
+
export async function readPullRequest(
|
|
92
|
+
url: string,
|
|
93
|
+
deps: PullRequestDeps,
|
|
94
|
+
): Promise<PullRequestRecord> {
|
|
95
|
+
const { host, slug, number } = pullRequestAddress(url);
|
|
96
|
+
let ghFailure: string;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const stdout = await deps.run(
|
|
100
|
+
"gh",
|
|
101
|
+
[
|
|
102
|
+
"pr",
|
|
103
|
+
"view",
|
|
104
|
+
String(number),
|
|
105
|
+
"--repo",
|
|
106
|
+
`${host}/${slug}`,
|
|
107
|
+
"--json",
|
|
108
|
+
"number,title,baseRefName,baseRefOid",
|
|
109
|
+
],
|
|
110
|
+
{ timeoutMs: GH_TIMEOUT_MS },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return { host, slug, ...metadataSchema.parse(JSON.parse(stdout)) };
|
|
114
|
+
} catch (error) {
|
|
115
|
+
ghFailure = firstLine(errorMessage(error));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (host !== "github.com")
|
|
119
|
+
throw new ReviewInputError(
|
|
120
|
+
`Could not read ${url}: gh failed (${ghFailure}). Run \`gh auth login --hostname ${host}\` with an account that can read the repository, then retry.`,
|
|
121
|
+
409,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
let response: Response;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
response = await deps.fetch(
|
|
128
|
+
`https://api.github.com/repos/${slug}/pulls/${number}`,
|
|
129
|
+
{
|
|
130
|
+
headers: {
|
|
131
|
+
Accept: "application/vnd.github+json",
|
|
132
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
133
|
+
},
|
|
134
|
+
signal: AbortSignal.timeout(API_TIMEOUT_MS),
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw unreadable(
|
|
139
|
+
url,
|
|
140
|
+
ghFailure,
|
|
141
|
+
`the GitHub API is unreachable (${firstLine(errorMessage(error))})`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (response.status === 404)
|
|
146
|
+
throw new ReviewInputError(
|
|
147
|
+
`${url} was not found: it does not exist, or it is private and gh is not signed in to an account that can read it (run \`gh auth status\`). gh said: ${ghFailure}`,
|
|
148
|
+
404,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (response.status === 403 || response.status === 429)
|
|
152
|
+
throw unreadable(
|
|
153
|
+
url,
|
|
154
|
+
ghFailure,
|
|
155
|
+
response.headers.get("x-ratelimit-remaining") === "0" ||
|
|
156
|
+
response.status === 429
|
|
157
|
+
? "the unauthenticated GitHub API rate limit is exhausted"
|
|
158
|
+
: "the GitHub API refused the request",
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (!response.ok)
|
|
162
|
+
throw unreadable(
|
|
163
|
+
url,
|
|
164
|
+
ghFailure,
|
|
165
|
+
`the GitHub API returned HTTP ${response.status}`,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const parsed = z
|
|
169
|
+
.object({
|
|
170
|
+
number: z.number(),
|
|
171
|
+
title: z.string(),
|
|
172
|
+
base: z.object({ ref: z.string().min(1), sha: z.string() }),
|
|
173
|
+
})
|
|
174
|
+
.safeParse(await response.json().catch(() => undefined));
|
|
175
|
+
|
|
176
|
+
if (!parsed.success)
|
|
177
|
+
throw unreadable(url, ghFailure, "the GitHub API response was malformed");
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
host,
|
|
181
|
+
slug,
|
|
182
|
+
number: parsed.data.number,
|
|
183
|
+
title: parsed.data.title,
|
|
184
|
+
baseRefName: parsed.data.base.ref,
|
|
185
|
+
baseRefOid: parsed.data.base.sha,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Remotes by the host and owner/repo their configured URL names. The
|
|
190
|
+
* configured URL, not the insteadOf rewrite: a mirror still names its repo. */
|
|
191
|
+
export async function githubRemotes(
|
|
192
|
+
gitDir: string,
|
|
193
|
+
deps: PullRequestDeps,
|
|
194
|
+
): Promise<{ name: string; host: string; slug: string }[]> {
|
|
195
|
+
const stdout = await deps
|
|
196
|
+
.run(
|
|
197
|
+
"git",
|
|
198
|
+
[
|
|
199
|
+
"--git-dir",
|
|
200
|
+
gitDir,
|
|
201
|
+
"config",
|
|
202
|
+
"--get-regexp",
|
|
203
|
+
String.raw`^remote\..*\.url$`,
|
|
204
|
+
],
|
|
205
|
+
{ timeoutMs: LOCAL_TIMEOUT_MS },
|
|
206
|
+
)
|
|
207
|
+
.catch(() => "");
|
|
208
|
+
|
|
209
|
+
return stdout.split("\n").flatMap((line) => {
|
|
210
|
+
const match = /^remote\.(.+)\.url\s+(\S+)/.exec(line.trim());
|
|
211
|
+
const remote = match && parseGitRemote(match[2]!);
|
|
212
|
+
|
|
213
|
+
return match && remote
|
|
214
|
+
? [{ name: match[1]!, host: remote.host, slug: remote.slug }]
|
|
215
|
+
: [];
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Refs Review owns for one PR; the user's branches and bookmarks never move. */
|
|
220
|
+
export function pullRequestRefs(pr: {
|
|
221
|
+
host: string;
|
|
222
|
+
slug: string;
|
|
223
|
+
number: number;
|
|
224
|
+
}) {
|
|
225
|
+
// github.com keeps the host-less namespace existing checkouts already hold.
|
|
226
|
+
const repository = `${pr.host === "github.com" ? "" : `${pr.host}/`}${pr.slug}`;
|
|
227
|
+
const prefix = `refs/review/github/${repository.toLowerCase()}/pull/${pr.number}`;
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
head: `${prefix}/head`,
|
|
231
|
+
base: `${prefix}/base`,
|
|
232
|
+
frozenBase: `${prefix}/frozen-base`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Fetch the PR head (refs/pull/N/head, so fork PRs work) and its base branch
|
|
238
|
+
* into Review's own namespace, and return the comparison GitHub shows: the
|
|
239
|
+
* head, and the merge base of the head with the base branch.
|
|
240
|
+
*
|
|
241
|
+
* A PR merged with a merge commit is absorbed: merge-base(base branch, head)
|
|
242
|
+
* is the head itself and the diff is empty. GitHub freezes the base commit
|
|
243
|
+
* (baseRefOid) at the PR's last update, so its merge base with the head is
|
|
244
|
+
* the fork point GitHub diffs against, and it survives base branches that are
|
|
245
|
+
* force-rebuilt or deleted.
|
|
246
|
+
*/
|
|
247
|
+
export async function fetchPullRequest(
|
|
248
|
+
input: {
|
|
249
|
+
rootPath: string;
|
|
250
|
+
gitDir: string;
|
|
251
|
+
kind: LocalVcsKind;
|
|
252
|
+
remote: string;
|
|
253
|
+
pullRequest: PullRequestRecord;
|
|
254
|
+
},
|
|
255
|
+
deps: PullRequestDeps,
|
|
256
|
+
): Promise<{ head: string; base: string }> {
|
|
257
|
+
const { pullRequest: pr } = input;
|
|
258
|
+
const refs = pullRequestRefs(pr);
|
|
259
|
+
|
|
260
|
+
const git = (args: string[], timeoutMs = LOCAL_TIMEOUT_MS) =>
|
|
261
|
+
deps
|
|
262
|
+
.run("git", ["--git-dir", input.gitDir, ...args], { timeoutMs })
|
|
263
|
+
.then((stdout) => stdout.trim());
|
|
264
|
+
|
|
265
|
+
const fetchRefs = (...refspecs: string[]) =>
|
|
266
|
+
git(
|
|
267
|
+
[
|
|
268
|
+
"fetch",
|
|
269
|
+
"--no-tags",
|
|
270
|
+
"--no-write-fetch-head",
|
|
271
|
+
"--quiet",
|
|
272
|
+
input.remote,
|
|
273
|
+
...refspecs,
|
|
274
|
+
],
|
|
275
|
+
FETCH_TIMEOUT_MS,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const commit = (ref: string) =>
|
|
279
|
+
git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).catch(
|
|
280
|
+
() => undefined,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const mergeBase = (left: string, right: string) =>
|
|
284
|
+
git(["merge-base", left, right]).catch(() => undefined);
|
|
285
|
+
|
|
286
|
+
const frozenBase = async () => {
|
|
287
|
+
if (!pr.baseRefOid) return undefined;
|
|
288
|
+
|
|
289
|
+
if (!(await commit(pr.baseRefOid)))
|
|
290
|
+
await fetchRefs(`+${pr.baseRefOid}:${refs.frozenBase}`).catch(
|
|
291
|
+
() => undefined,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
return commit(pr.baseRefOid);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
let baseTip: string | undefined;
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
await fetchRefs(
|
|
301
|
+
`+refs/pull/${pr.number}/head:${refs.head}`,
|
|
302
|
+
`+refs/heads/${pr.baseRefName}:${refs.base}`,
|
|
303
|
+
);
|
|
304
|
+
baseTip = await commit(refs.base);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
// The base branch may be gone; the head and the frozen base suffice.
|
|
307
|
+
try {
|
|
308
|
+
await fetchRefs(`+refs/pull/${pr.number}/head:${refs.head}`);
|
|
309
|
+
} catch {
|
|
310
|
+
throw new ReviewInputError(
|
|
311
|
+
`Could not fetch PR #${pr.number} from remote "${input.remote}" (${pr.slug}). Check \`git fetch ${input.remote}\` works with your Git credentials, then retry. Git said: ${firstLine(errorMessage(error))}`,
|
|
312
|
+
409,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const head = await commit(refs.head);
|
|
318
|
+
|
|
319
|
+
baseTip ??= await frozenBase();
|
|
320
|
+
|
|
321
|
+
if (!head || !baseTip)
|
|
322
|
+
throw new ReviewInputError(
|
|
323
|
+
`Fetched PR #${pr.number} but could not resolve its ${head ? `base branch ${pr.baseRefName}` : "head"}.`,
|
|
324
|
+
409,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
let base = await mergeBase(baseTip, head);
|
|
328
|
+
|
|
329
|
+
if (!base || base === head) {
|
|
330
|
+
const frozen = await frozenBase();
|
|
331
|
+
|
|
332
|
+
if (frozen && frozen !== baseTip)
|
|
333
|
+
base = (await mergeBase(frozen, head)) ?? base;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!base)
|
|
337
|
+
throw new ReviewInputError(
|
|
338
|
+
`PR #${pr.number}'s head shares no history with ${pr.baseRefName}.`,
|
|
339
|
+
409,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
if (input.kind === "jj")
|
|
343
|
+
await indexForJj(input.rootPath, [base, head], git, deps);
|
|
344
|
+
|
|
345
|
+
return { head, base };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* jj resolves only commits in its index, and `jj git import` indexes only
|
|
350
|
+
* branches, remote branches and tags. Import the commits through a
|
|
351
|
+
* transient tag, then drop it: the commits stay indexed (hidden, resolvable
|
|
352
|
+
* by commit id) and no bookmark or tag remains.
|
|
353
|
+
*/
|
|
354
|
+
async function indexForJj(
|
|
355
|
+
rootPath: string,
|
|
356
|
+
commits: string[],
|
|
357
|
+
git: (args: string[]) => Promise<string>,
|
|
358
|
+
deps: PullRequestDeps,
|
|
359
|
+
) {
|
|
360
|
+
const tags = commits.map(() => `refs/tags/review-index-${randomUUID()}`);
|
|
361
|
+
|
|
362
|
+
const jjImport = () =>
|
|
363
|
+
deps.run(
|
|
364
|
+
"jj",
|
|
365
|
+
["-R", rootPath, "git", "import", "--ignore-working-copy", "--quiet"],
|
|
366
|
+
{ cwd: rootPath, timeoutMs: LOCAL_TIMEOUT_MS },
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
for (const [index, tag] of tags.entries())
|
|
371
|
+
await git(["update-ref", tag, commits[index]!]);
|
|
372
|
+
await jjImport();
|
|
373
|
+
} finally {
|
|
374
|
+
for (const tag of tags)
|
|
375
|
+
await git(["update-ref", "-d", tag]).catch(() => undefined);
|
|
376
|
+
await jjImport().catch(() => undefined);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function unreadable(url: string, ghFailure: string, fallback: string) {
|
|
381
|
+
return new ReviewInputError(
|
|
382
|
+
`Could not read ${url}: gh failed (${ghFailure}) and ${fallback}. Run \`gh auth status\` and sign in with an account that can read the repository, then retry.`,
|
|
383
|
+
409,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function firstLine(text: string) {
|
|
388
|
+
return text.split("\n")[0]!.slice(0, 300);
|
|
389
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { SourcePins } from "@review/source.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import { ReviewInputError } from "./input-error.js";
|
|
5
|
+
|
|
6
|
+
// Query schemas for the read routes Desktop and review_get call. http.ts
|
|
7
|
+
// parses query strings with them, and authoring-tools.ts publishes get's.
|
|
8
|
+
const version = z
|
|
9
|
+
.union([
|
|
10
|
+
z.literal("").transform(() => undefined),
|
|
11
|
+
z.coerce.number().int().nonnegative(),
|
|
12
|
+
])
|
|
13
|
+
.optional();
|
|
14
|
+
|
|
15
|
+
const commit = z.string().min(1).optional();
|
|
16
|
+
|
|
17
|
+
const side = z.enum(["base", "head"]);
|
|
18
|
+
|
|
19
|
+
/** Read at a reference's own pins instead of the review's target. Flat, so
|
|
20
|
+
* the same shape serves query strings and tool inputs. */
|
|
21
|
+
const anchor = {
|
|
22
|
+
repositoryId: z.string().min(1).optional(),
|
|
23
|
+
head: z.string().min(1).optional(),
|
|
24
|
+
base: z.string().min(1).optional(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function queryAnchor(input: {
|
|
28
|
+
repositoryId?: string;
|
|
29
|
+
head?: string;
|
|
30
|
+
base?: string;
|
|
31
|
+
}) {
|
|
32
|
+
if (!input.repositoryId && !input.head && !input.base) return undefined;
|
|
33
|
+
|
|
34
|
+
if (!input.repositoryId || !input.head)
|
|
35
|
+
throw new ReviewInputError(
|
|
36
|
+
"Reading at explicit pins needs both repositoryId and head.",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const pins: SourcePins = {
|
|
40
|
+
repositoryId: input.repositoryId,
|
|
41
|
+
head: input.head,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (input.base) pins.base = input.base;
|
|
45
|
+
|
|
46
|
+
return pins;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const inspectQuerySchema = z.strictObject({
|
|
50
|
+
version,
|
|
51
|
+
targetId: z.string().min(1).optional(),
|
|
52
|
+
full: z.union([z.literal(true), z.literal("true")]).optional(),
|
|
53
|
+
format: z.enum(["text", "json"]).default("text"),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const readQuerySchemas = {
|
|
57
|
+
get: z.strictObject({
|
|
58
|
+
version,
|
|
59
|
+
targetId: z.string().min(1).optional(),
|
|
60
|
+
full: z.union([z.literal(true), z.literal("true")]).optional(),
|
|
61
|
+
}),
|
|
62
|
+
maps: z.strictObject({ version }),
|
|
63
|
+
tree: z.strictObject({
|
|
64
|
+
version,
|
|
65
|
+
commit,
|
|
66
|
+
...anchor,
|
|
67
|
+
side: side.default("head"),
|
|
68
|
+
path: z.string().default(""),
|
|
69
|
+
}),
|
|
70
|
+
file: z.strictObject({
|
|
71
|
+
version,
|
|
72
|
+
commit,
|
|
73
|
+
...anchor,
|
|
74
|
+
side,
|
|
75
|
+
file: z.string().min(1),
|
|
76
|
+
}),
|
|
77
|
+
diff: z.strictObject({ version, commit, ...anchor }),
|
|
78
|
+
structuralDiff: z.strictObject({
|
|
79
|
+
version,
|
|
80
|
+
commit,
|
|
81
|
+
...anchor,
|
|
82
|
+
file: z.string().min(1).optional(),
|
|
83
|
+
}),
|
|
84
|
+
commits: z.strictObject({ version }),
|
|
85
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export const RECOVERY =
|
|
2
|
+
"Whiteboard is not running, so its tools and guidance are unavailable. Start Whiteboard Desktop (or `whiteboard server start` for headless use). If Whiteboard's tools still do not appear, reconnect the Whiteboard MCP server or start a new agent session.";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ReviewSessionAgent,
|
|
3
|
+
SESSION_AGENT_KIND,
|
|
4
|
+
} from "@review/ui-telemetry-events.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Headers the agent CLI sets so the server can say where a review came from.
|
|
9
|
+
* Both are telemetry only and parse to closed enums: anything else a caller
|
|
10
|
+
* sends becomes "other".
|
|
11
|
+
*/
|
|
12
|
+
export const REVIEW_VIA_HEADER = "x-review-via";
|
|
13
|
+
|
|
14
|
+
export const REVIEW_AGENT_HEADER = "x-review-agent";
|
|
15
|
+
|
|
16
|
+
export type ReviewRequestVia = "api" | "mcp" | "other";
|
|
17
|
+
|
|
18
|
+
const viaSchema = z.enum(["api", "mcp", "other"]).catch("other");
|
|
19
|
+
|
|
20
|
+
const agentSchema = z.enum(SESSION_AGENT_KIND).optional().catch("other");
|
|
21
|
+
|
|
22
|
+
export function reviewRequestOrigin(headers: Headers): {
|
|
23
|
+
via: ReviewRequestVia;
|
|
24
|
+
agentKind?: ReviewSessionAgent;
|
|
25
|
+
} {
|
|
26
|
+
const via = viaSchema.parse(headers.get(REVIEW_VIA_HEADER));
|
|
27
|
+
|
|
28
|
+
const agentKind = agentSchema.parse(
|
|
29
|
+
headers.get(REVIEW_AGENT_HEADER) ?? undefined,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return agentKind ? { via, agentKind } : { via };
|
|
33
|
+
}
|