@dev.fast/whiteboard 0.3.1 → 0.3.3-preview.20261018.123

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 (42) hide show
  1. package/dist/{account-alias-D_mqo_Jk.js → account-alias-xOdmMnDH.js} +2 -2
  2. package/dist/{agent-cli-Cp7ufSvb.js → agent-cli-DT7MdaqL.js} +25 -6
  3. package/dist/agent-cli-DeCrUC2s.js +2 -0
  4. package/dist/{agent-client-DjXQy6CL.js → agent-client-BRBzv3AV.js} +3 -3
  5. package/dist/{authoring-tools-ByMdW_S8.js → authoring-tools-DGSEag8y.js} +2 -2
  6. package/dist/{background-server-CVL7QpsL.js → background-server-C_REWFkG.js} +1 -1
  7. package/dist/{background-server-ONy-xKJ1.js → background-server-WKzI_a98.js} +2 -2
  8. package/dist/build-info.json +1 -1
  9. package/dist/{cli-Cko4zfif.js → cli-BBSDi_dX.js} +1 -1
  10. package/dist/{cli-runner-2sJKjaRX.js → cli-runner-DshTBske.js} +17 -17
  11. package/dist/cli.js +2 -2
  12. package/dist/{client-CLqKkzOc.js → client-BN_FDxFr.js} +2 -2
  13. package/dist/{fuzzy-match-C9olHRCK.js → fuzzy-match-bFX6wm8j.js} +1 -1
  14. package/dist/{headless-host-CnCDrHTx.js → headless-host-CS4Gv0c5.js} +58 -11
  15. package/dist/{headless-host-DRwHcAuF.js → headless-host-CzvYpKOX.js} +1 -1
  16. package/dist/{local-data-DKmj2ygd.js → local-data-DtQTIzHX.js} +16 -3
  17. package/dist/{mcp-D-7jkRTU.js → mcp-DpKnnmHH.js} +3 -3
  18. package/dist/profile-Cz7L781D.js +2 -0
  19. package/dist/{profile-B58hz5n7.js → profile-DaqmODhc.js} +1 -1
  20. package/dist/{remote-attach-DJxW9402.js → remote-attach-DCngg8cl.js} +3 -3
  21. package/dist/{remote-language-server-CGsCaqro.js → remote-language-server-UWQgpoh-.js} +1 -1
  22. package/dist/{remote-uninstall-DYmNOetF.js → remote-uninstall-CcDOcGyi.js} +3 -3
  23. package/dist/{request-origin-BSwTNS8K.js → request-origin-Zcn_CjX4.js} +2 -2
  24. package/dist/{review-app-launcher-i1hmxOZA.js → review-app-launcher-tqSQPlbu.js} +2 -2
  25. package/dist/{review-telemetry-ClhP_5lW.js → review-telemetry-BHWJsnoD.js} +2 -2
  26. package/dist/server/desktop-host.js +11 -11
  27. package/dist/{server-discovery-DVqrcIDT.js → server-discovery-BVqrE1_-.js} +1 -1
  28. package/dist/sharing/index.d.ts +2 -0
  29. package/dist/sharing/index.js +1 -1
  30. package/dist/{src-DZG1xAqW.js → src-DsM3jO8G.js} +61 -1
  31. package/dist/{stored-document-migration-DLBhkSfp.js → stored-document-migration-CChxtFAy.js} +1 -1
  32. package/dist/{thread-state-DZihwhfu.js → thread-state-5pYr6_zM.js} +1 -1
  33. package/dist/{tool-failure-CVJrfi5s.js → tool-failure-BFt2xj9i.js} +1 -1
  34. package/package.json +5 -5
  35. package/src/review-api/agent-cli.ts +2 -0
  36. package/src/review-api/http.ts +4 -0
  37. package/src/review-api/install-update.ts +27 -0
  38. package/src/review-api/store.ts +20 -0
  39. package/src/server/feedback.ts +80 -0
  40. package/src/server/review-server-core.ts +2 -0
  41. package/dist/agent-cli-2D4icmBR.js +0 -2
  42. package/dist/profile-D1rScZ-E.js +0 -2
@@ -1,10 +1,10 @@
1
1
  import { n as isJsonObject } from "./src-X9phtB2j.js";
2
- import { w as ReviewCliInstallStampSchema } from "./src-DZG1xAqW.js";
2
+ import { w as ReviewCliInstallStampSchema } from "./src-DsM3jO8G.js";
3
3
  import { C as disableTraceMachine, E as traceMachineStatus, G as listTraceRepositoryRoots, K as traceRepositoryStatus, Q as removeAgentTraceHook, S as configureTraceMachine, T as traceMachineEnabled, U as disableAllTraceRepositories, W as enableTraceRepository, X as describeTraceHookOwners, Y as AGENT_TRACE_HOOK_AGENTS, Z as installHarnessHooks, dt as StoreClient, n as reviewDesktopStateDir, nt as traceScope, ot as readStoreAuth } from "./review-home-paths-Lc02WFuX.js";
4
4
  import { A as withFileLock, F as writeFileAtomicAsync, I as writePrivateJsonAtomic, M as devReviewHome } from "./s3-config-DDWWMh5x.js";
5
5
  import { i as readReviewPackageVersion } from "./package-paths-jMg6JxDP.js";
6
6
  import { n as isFile, t as isDirectory } from "./fs-utils-BMPLt0cr.js";
7
- import { s as findExecutable } from "./fuzzy-match-C9olHRCK.js";
7
+ import { s as findExecutable } from "./fuzzy-match-bFX6wm8j.js";
8
8
  import { createHash } from "node:crypto";
9
9
  import { constants } from "node:fs";
10
10
  import { access, chmod, lstat, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
@@ -1,11 +1,29 @@
1
1
  import { n as isJsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
2
2
  import { T as traceMachineEnabled } from "./review-home-paths-Lc02WFuX.js";
3
- import { a as toolResultText, i as connectReviewInstance, s as ReviewApiError } from "./agent-client-DjXQy6CL.js";
3
+ import { a as toolResultText, i as connectReviewInstance, s as ReviewApiError } from "./agent-client-BRBzv3AV.js";
4
4
  import { M as devReviewHome } from "./s3-config-DDWWMh5x.js";
5
- import { b as selectReviewInstance, u as REVIEW_INSTANCE_ENV } from "./server-discovery-DVqrcIDT.js";
6
- import { n as reviewSessionAgent } from "./review-telemetry-ClhP_5lW.js";
7
- import { n as REVIEW_VIA_HEADER, t as REVIEW_AGENT_HEADER } from "./request-origin-BSwTNS8K.js";
8
- import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-CVJrfi5s.js";
5
+ import { n as readBuildCommit } from "./package-paths-jMg6JxDP.js";
6
+ import { b as selectReviewInstance, u as REVIEW_INSTANCE_ENV } from "./server-discovery-BVqrE1_-.js";
7
+ import { n as reviewSessionAgent } from "./review-telemetry-BHWJsnoD.js";
8
+ import { n as REVIEW_VIA_HEADER, t as REVIEW_AGENT_HEADER } from "./request-origin-Zcn_CjX4.js";
9
+ import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-BFt2xj9i.js";
10
+ import { existsSync } from "node:fs";
11
+ import path from "node:path";
12
+ //#region src/review-api/install-update.ts
13
+ /** Exits once an update replaces this install, or the Windows updater marks it. */
14
+ function exitOnInstallUpdate(stderr) {
15
+ const commit = readBuildCommit(import.meta.url);
16
+ if (!commit) return;
17
+ const marker = path.join(path.dirname(process.execPath), "applying_update");
18
+ const staleMarker = existsSync(marker);
19
+ const timer = setInterval(() => {
20
+ if (!staleMarker && existsSync(marker) || readBuildCommit(import.meta.url) !== commit) {
21
+ clearInterval(timer);
22
+ stderr.write("Whiteboard was updated. Restart its MCP server.\n", () => process.exit(75));
23
+ }
24
+ }, 1e3).unref();
25
+ }
26
+ //#endregion
9
27
  //#region src/review-api/agent-cli.ts
10
28
  const reviewAgentCliHelp = "whiteboard api tools (one line per tool)\nwhiteboard api tools <tool-name> (its description and input schema)\nwhiteboard api <tool-name> '<json>'\nwhiteboard api <tool-name> - (read JSON from stdin)\nwhiteboard mcp (stdio MCP adapter; Whiteboard Desktop or whiteboard server start must be running)\nSelect headless state with DEV_REVIEW_SERVER_DIR or whiteboard --state-dir <path> api/mcp.\n";
11
29
  async function runReviewAgentCli(input) {
@@ -20,7 +38,8 @@ async function runReviewAgentCli(input) {
20
38
  if (extra.length || mode === "mcp" && name) throw new Error("Unexpected arguments. Use whiteboard api --help.");
21
39
  const headers = Object.fromEntries([[REVIEW_VIA_HEADER, mode === "mcp" ? "mcp" : "api"], [REVIEW_AGENT_HEADER, reviewSessionAgent(env)]]);
22
40
  if (mode === "mcp") {
23
- const { serveReviewMcp } = await import("./mcp-D-7jkRTU.js");
41
+ const { serveReviewMcp } = await import("./mcp-DpKnnmHH.js");
42
+ exitOnInstallUpdate(input.stderr);
24
43
  await serveReviewMcp(async (key, agentKind) => {
25
44
  const connected = await connectReviewInstance(key ? {
26
45
  ...env,
@@ -0,0 +1,2 @@
1
+ import { n as runReviewAgentCli } from "./agent-cli-DT7MdaqL.js";
2
+ export { runReviewAgentCli };
@@ -1,7 +1,7 @@
1
1
  import { d as isBooleanValue, f as isNumberValue, m as isStringValue, u as parseJsonText } from "./src-X9phtB2j.js";
2
- import "./src-DZG1xAqW.js";
2
+ import "./src-DsM3jO8G.js";
3
3
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
4
- import { b as selectReviewInstance, c as reviewServerStateDir, d as ReviewInstanceUnavailableError, i as readReviewServerDiscovery, l as serverNotReady, m as healthyReviewInstance, s as reviewServerIsHealthy, y as reviewInstanceUnavailable } from "./server-discovery-DVqrcIDT.js";
4
+ import { b as selectReviewInstance, c as reviewServerStateDir, d as ReviewInstanceUnavailableError, i as readReviewServerDiscovery, l as serverNotReady, m as healthyReviewInstance, s as reviewServerIsHealthy, y as reviewInstanceUnavailable } from "./server-discovery-BVqrE1_-.js";
5
5
  //#region ../review-protocol/src/review-api-client.ts
6
6
  const defaultRequest = (url, init) => fetch(url, init);
7
7
  /** A non-2xx reply; the status tells a caller whether retrying can help. */
@@ -249,7 +249,7 @@ async function connectReviewInstance(env = process.env, headers = {}, options =
249
249
  const selection = await selectReviewInstance({ env });
250
250
  const discovery = healthyReviewInstance(selection);
251
251
  if (!discovery) {
252
- const { ensureServerWithoutDesktop } = await import("./background-server-CVL7QpsL.js");
252
+ const { ensureServerWithoutDesktop } = await import("./background-server-C_REWFkG.js");
253
253
  const server = await ensureServerWithoutDesktop({
254
254
  selection,
255
255
  env,
@@ -1,5 +1,5 @@
1
- import { c as activityEndSchema, i as commandSchema, l as activityUpdateSchema, n as uploadSchema, s as activityBeginSchema } from "./local-data-DKmj2ygd.js";
2
- import { S as publishedEditSchema, y as pathTargetSchema } from "./stored-document-migration-DLBhkSfp.js";
1
+ import { c as activityEndSchema, i as commandSchema, l as activityUpdateSchema, n as uploadSchema, s as activityBeginSchema } from "./local-data-DtQTIzHX.js";
2
+ import { S as publishedEditSchema, y as pathTargetSchema } from "./stored-document-migration-CChxtFAy.js";
3
3
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
4
4
  import { t as findReviewPackageRoot } from "./package-paths-jMg6JxDP.js";
5
5
  import { z } from "zod";
@@ -1,2 +1,2 @@
1
- import { a as headlessServerOwner, c as stopBackgroundServer, i as ensureServerWithoutDesktop, r as ensureBackgroundServer } from "./background-server-ONy-xKJ1.js";
1
+ import { a as headlessServerOwner, c as stopBackgroundServer, i as ensureServerWithoutDesktop, r as ensureBackgroundServer } from "./background-server-WKzI_a98.js";
2
2
  export { ensureBackgroundServer, ensureServerWithoutDesktop, headlessServerOwner, stopBackgroundServer };
@@ -1,8 +1,8 @@
1
1
  import "./review-home-paths-Lc02WFuX.js";
2
2
  import { D as liveLockOwner, O as processIsAlive } from "./s3-config-DDWWMh5x.js";
3
3
  import { t as findReviewPackageRoot } from "./package-paths-jMg6JxDP.js";
4
- import { a as readReviewServerHealth, c as reviewServerStateDir, i as readReviewServerDiscovery, n as desktopOwnsHome, p as desktopUsesHome, r as headlessServerLockPath } from "./server-discovery-DVqrcIDT.js";
5
- import { t as desktopApplicationInstalled } from "./review-app-launcher-i1hmxOZA.js";
4
+ import { a as readReviewServerHealth, c as reviewServerStateDir, i as readReviewServerDiscovery, n as desktopOwnsHome, p as desktopUsesHome, r as headlessServerLockPath } from "./server-discovery-BVqrE1_-.js";
5
+ import { t as desktopApplicationInstalled } from "./review-app-launcher-tqSQPlbu.js";
6
6
  import { mkdir, open, readFile, realpath, stat } from "node:fs/promises";
7
7
  import path from "node:path";
8
8
  import { spawn } from "node:child_process";
@@ -1 +1 @@
1
- {"version":"0.3.1","commit":"674dfbb9b0de56eb16c9ac015f2f0b2b3fa01980","dirty":true,"builtAt":"2026-10-16T20:29:45.789Z"}
1
+ {"version":"0.3.3-preview.20261018.123","commit":"5b96280747a70c90fa74e7eb2d71cccf8cf05ad7","dirty":true,"builtAt":"2026-10-18T16:53:01.255Z"}
@@ -1,4 +1,4 @@
1
- import { r as connectReviewApi } from "./agent-client-DjXQy6CL.js";
1
+ import { r as connectReviewApi } from "./agent-client-BRBzv3AV.js";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { z } from "zod";
4
4
  //#region src/sharing/cli.ts
@@ -1,18 +1,18 @@
1
1
  import { C as setLocalVcsCommandObserver, E as __toESM, T as __require, w as __commonJSMin } from "./src-BMPuxWv4.js";
2
2
  import { c as jsonString, n as isJsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
3
- import { _ as REVIEW_REMOTE_AGENT_IDS, p as REVIEW_CLI_INSTALL_TARGET_LABELS, v as REVIEW_REMOTE_ATTACH_BEGIN, y as REVIEW_REMOTE_ATTACH_END } from "./src-DZG1xAqW.js";
3
+ import { _ as REVIEW_REMOTE_AGENT_IDS, p as REVIEW_CLI_INSTALL_TARGET_LABELS, v as REVIEW_REMOTE_ATTACH_BEGIN, y as REVIEW_REMOTE_ATTACH_END } from "./src-DsM3jO8G.js";
4
4
  import { $ as resolveTraceCommand, A as runTraceDeny, E as traceMachineStatus, F as describeSelection, H as resolveTraceRepositoryTarget, J as HOSTED_CAPTURE_SCOPE_DESCRIPTION, K as traceRepositoryStatus, L as loadS3TraceStorage, M as runTraceSessions, N as runTraceStoreDelete, P as runTraceStoreInfo, T as traceMachineEnabled, V as requireTraceConsent, _ as runTraceRepair, b as runTraceGitHook, c as registerTraceCommands, ct as runStoreLogout, d as runTraceList$1, dt as StoreClient, f as runTracePull$1, ft as withStoreAuthorization, g as runTraceInstallMachine, gt as jsonRequestedInArgv, h as runTraceEnable, ht as humanStream, j as runTraceOnboard, k as runTraceAllow, l as resolveTraceReadStorage, lt as runStoreWhoami, m as runTraceDisable, mt as failWithJsonError, nt as traceScope, ot as readStoreAuth, p as runTraceShow, pt as emitJsonEvent, q as readTraceUserConfig, rt as DEFAULT_STORE_ORIGIN, st as runStoreLogin, tt as traceHomeDir, u as runTraceBlame, ut as StoreApiError, v as runTraceStatus, w as readLegacyCaptureSettings, x as runTraceHook, y as runTraceSync, z as selectTraceStorage } from "./review-home-paths-Lc02WFuX.js";
5
- import { o as ReviewApiClient, s as ReviewApiError } from "./agent-client-DjXQy6CL.js";
5
+ import { o as ReviewApiClient, s as ReviewApiError } from "./agent-client-BRBzv3AV.js";
6
6
  import { t as resolveReviewRoot } from "./runtime-CKiWhGrf.js";
7
7
  import { C as s3ProfileSchema, E as writeTraceConfigFile, M as devReviewHome, N as normalizeStoreOrigin, S as readTraceConfigFile, T as sameS3Profile, _ as emptyTraceConfig, c as resolveS3Setup, g as currentStore, h as TraceConfigurationError, i as isS3MockMode, n as clearTraceEnvCache, o as readTraceEnvFile, p as traceSettingsPath, w as s3Store } from "./s3-config-DDWWMh5x.js";
8
8
  import { t as errorMessage } from "./error-message-OtiDonty.js";
9
9
  import { i as readReviewPackageVersion, r as readBuildInfo, t as findReviewPackageRoot } from "./package-paths-jMg6JxDP.js";
10
- import { a as readReviewServerHealth, b as selectReviewInstance, c as reviewServerStateDir, d as ReviewInstanceUnavailableError, f as clearDefaultReviewInstance, g as requireHealthyReviewDesktop, h as readDefaultReviewInstance, i as readReviewServerDiscovery, l as serverNotReady, m as healthyReviewInstance, p as desktopUsesHome, t as DESKTOP_TAKEOVER_SIGNAL, u as REVIEW_INSTANCE_ENV, x as writeDefaultReviewInstance, y as reviewInstanceUnavailable } from "./server-discovery-DVqrcIDT.js";
11
- import { t as ReviewTelemetry } from "./review-telemetry-ClhP_5lW.js";
12
- import { t as fuzzyRank } from "./fuzzy-match-C9olHRCK.js";
13
- import { _ as isInstallTarget, a as isOwnedShim, f as windowsInstallerCommand, g as ALL_INSTALL_TARGETS, h as detectConnectAgents, m as connectAgent, o as pathShimPath, p as scanLegacySkills, t as aliasInstallationToAccount, v as connectPrompts } from "./account-alias-D_mqo_Jk.js";
14
- import { n as focusReviewDesktop, r as runReviewAppLaunch } from "./review-app-launcher-i1hmxOZA.js";
15
- import { t as reviewAgentCliHelp } from "./agent-cli-Cp7ufSvb.js";
10
+ import { a as readReviewServerHealth, b as selectReviewInstance, c as reviewServerStateDir, d as ReviewInstanceUnavailableError, f as clearDefaultReviewInstance, g as requireHealthyReviewDesktop, h as readDefaultReviewInstance, i as readReviewServerDiscovery, l as serverNotReady, m as healthyReviewInstance, p as desktopUsesHome, t as DESKTOP_TAKEOVER_SIGNAL, u as REVIEW_INSTANCE_ENV, x as writeDefaultReviewInstance, y as reviewInstanceUnavailable } from "./server-discovery-BVqrE1_-.js";
11
+ import { t as ReviewTelemetry } from "./review-telemetry-BHWJsnoD.js";
12
+ import { t as fuzzyRank } from "./fuzzy-match-bFX6wm8j.js";
13
+ import { _ as isInstallTarget, a as isOwnedShim, f as windowsInstallerCommand, g as ALL_INSTALL_TARGETS, h as detectConnectAgents, m as connectAgent, o as pathShimPath, p as scanLegacySkills, t as aliasInstallationToAccount, v as connectPrompts } from "./account-alias-xOdmMnDH.js";
14
+ import { n as focusReviewDesktop, r as runReviewAppLaunch } from "./review-app-launcher-tqSQPlbu.js";
15
+ import { t as reviewAgentCliHelp } from "./agent-cli-DT7MdaqL.js";
16
16
  import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
17
17
  import { realpath } from "node:fs/promises";
18
18
  import path from "node:path";
@@ -4297,7 +4297,7 @@ async function runReviewCli(input) {
4297
4297
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new ReviewCliUsageError("--port must be an integer between 0 and 65535.");
4298
4298
  const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
4299
4299
  if (options.detach) {
4300
- const { ensureBackgroundServer } = await import("./background-server-CVL7QpsL.js");
4300
+ const { ensureBackgroundServer } = await import("./background-server-C_REWFkG.js");
4301
4301
  const { discovery, started } = await ensureBackgroundServer({
4302
4302
  stateDir,
4303
4303
  env,
@@ -4319,7 +4319,7 @@ async function runReviewCli(input) {
4319
4319
  process.once("SIGINT", stop);
4320
4320
  process.once("SIGTERM", stop);
4321
4321
  process.once(DESKTOP_TAKEOVER_SIGNAL, takeover);
4322
- const { HeadlessServerBusyError, runHeadlessServer } = await import("./headless-host-DRwHcAuF.js");
4322
+ const { HeadlessServerBusyError, runHeadlessServer } = await import("./headless-host-CzvYpKOX.js");
4323
4323
  try {
4324
4324
  await runHeadlessServer({
4325
4325
  stateDir,
@@ -4356,7 +4356,7 @@ async function runReviewCli(input) {
4356
4356
  configureJsonOutput(serverCommand.command("stop").description("Stop a background server started by --detach, the CLI, or Desktop").option("--state-dir <path>", "directory selected when starting the server"), "plain").action(async (_options, command) => {
4357
4357
  const options = command.optsWithGlobals();
4358
4358
  const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
4359
- const { headlessServerOwner, stopBackgroundServer } = await import("./background-server-CVL7QpsL.js");
4359
+ const { headlessServerOwner, stopBackgroundServer } = await import("./background-server-C_REWFkG.js");
4360
4360
  const owner = await headlessServerOwner(stateDir);
4361
4361
  const discovery = await readReviewServerDiscovery(stateDir);
4362
4362
  if (owner === void 0) {
@@ -4384,7 +4384,7 @@ async function runReviewCli(input) {
4384
4384
  if (!existsSync(path.join(stateDir, "review-api.db"))) throw new Error(`No saved reviews in ${stateDir}, so there is no server id to reset. Check --state-dir.`);
4385
4385
  const inUse = `A Whiteboard server is using ${stateDir}. Stop it first, then run whiteboard server reset-id again.`;
4386
4386
  if (await desktopUsesHome(stateDir, env)) return refuse(inUse, options.json);
4387
- const [{ openReviewProfile }, { withHeadlessServerLock }] = await Promise.all([import("./profile-D1rScZ-E.js"), import("./headless-host-DRwHcAuF.js")]);
4387
+ const [{ openReviewProfile }, { withHeadlessServerLock }] = await Promise.all([import("./profile-Cz7L781D.js"), import("./headless-host-CzvYpKOX.js")]);
4388
4388
  const reset = await withHeadlessServerLock(await realpath(stateDir), async () => {
4389
4389
  const local = await openReviewProfile(stateDir, { manageWorkspaces: false });
4390
4390
  try {
@@ -4406,7 +4406,7 @@ async function runReviewCli(input) {
4406
4406
  configureJsonOutput(remote.command("attach").description("Start a background server if none is healthy and report how to reach it").option("--state-dir <path>", "directory for saved reviews and server discovery").option("--groups <groups>", "comma-separated optional extension groups the Desktop has enabled, such as go").option("--replace", "stop a running server of another version that the CLI or Desktop started, and start this one"), "plain").action(async (_options, command) => {
4407
4407
  const options = command.optsWithGlobals();
4408
4408
  const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
4409
- const { remoteAttach } = await import("./remote-attach-DJxW9402.js");
4409
+ const { remoteAttach } = await import("./remote-attach-DCngg8cl.js");
4410
4410
  let attach;
4411
4411
  try {
4412
4412
  attach = await remoteAttach({
@@ -4448,7 +4448,7 @@ async function runReviewCli(input) {
4448
4448
  configureJsonOutput(remote.command("uninstall").description("Remove what Whiteboard Desktop installed on this host, stopping a server it started").addOption(new Option("--keep-reviews", "leave the saved reviews in place").conflicts("deleteReviews")).addOption(new Option("--delete-reviews", "also delete the saved reviews")).option("--state-dir <path>", "directory for saved reviews and server discovery"), "plain").action(async (_options, command) => {
4449
4449
  const options = command.optsWithGlobals();
4450
4450
  const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
4451
- const { remoteUninstall } = await import("./remote-uninstall-DYmNOetF.js");
4451
+ const { remoteUninstall } = await import("./remote-uninstall-CcDOcGyi.js");
4452
4452
  let deleteReviews = options.deleteReviews ? true : options.keepReviews ? false : void 0;
4453
4453
  if (deleteReviews === void 0 && !options.json && input.stdin?.isTTY) {
4454
4454
  const prompt = createInterface$1({
@@ -4670,14 +4670,14 @@ async function runReviewCli(input) {
4670
4670
  command.showHelpAfterError(false).error("Markdown review migration is no longer supported. Ask your agent to migrate your old Whiteboard reviews.");
4671
4671
  });
4672
4672
  configureJsonOutput(configureJsonOutput(program.command("share").description("Upload an immutable review snapshot and return its share link"), "plain").option("--review <id>", "review ID").option("--version <number>", "Saved version to share").option("--preview", "Open the share link in Whiteboard Preview by default").option("--request-id <uuid>", "Reuse this ID when retrying the same immutable upload").action(async (options) => {
4673
- const { runShareCli } = await import("./cli-Cko4zfif.js");
4673
+ const { runShareCli } = await import("./cli-BBSDi_dX.js");
4674
4674
  state.exitCode = await runShareCli({
4675
4675
  ...input,
4676
4676
  ...options,
4677
4677
  env: authoringEnv()
4678
4678
  });
4679
4679
  }).command("revoke <share-id>").description("Revoke future downloads of a share"), "plain").action(async (shareId, options) => {
4680
- const { runShareCli } = await import("./cli-Cko4zfif.js");
4680
+ const { runShareCli } = await import("./cli-BBSDi_dX.js");
4681
4681
  state.exitCode = await runShareCli({
4682
4682
  ...input,
4683
4683
  ...options,
@@ -4758,7 +4758,7 @@ async function runReviewCli(input) {
4758
4758
  return commands;
4759
4759
  } });
4760
4760
  for (const [name, description] of [["api", "Call a JSON review authoring tool on the running server"], ["mcp", "Serve the JSON review authoring tools over stdio MCP"]]) configureOutput(program.command(name).description(description).argument("[args...]", "tool name and JSON input").allowUnknownOption().allowExcessArguments().helpOption(false).passThroughOptions().addHelpText("after", `\n${reviewAgentCliHelp}`), "plain").action(async (args) => {
4761
- const { runReviewAgentCli } = await import("./agent-cli-2D4icmBR.js");
4761
+ const { runReviewAgentCli } = await import("./agent-cli-DeCrUC2s.js");
4762
4762
  state.exitCode = await runReviewAgentCli({
4763
4763
  ...input,
4764
4764
  env: authoringEnv(),
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as jsonNumber, c as jsonString, o as jsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
3
- import "./src-DZG1xAqW.js";
3
+ import "./src-DsM3jO8G.js";
4
4
  import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
5
5
  import path from "node:path";
6
6
  import os from "node:os";
@@ -17,7 +17,7 @@ async function runCli(effectivePath) {
17
17
  `);
18
18
  return 1;
19
19
  }
20
- const { runReviewCli } = await import("./cli-runner-2sJKjaRX.js");
20
+ const { runReviewCli } = await import("./cli-runner-DshTBske.js");
21
21
  return runReviewCli({
22
22
  argv,
23
23
  cliPaths: {
@@ -1,7 +1,7 @@
1
1
  import { f as gitAt } from "./src-BMPuxWv4.js";
2
2
  import { n as isJsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
3
- import "./src-DZG1xAqW.js";
4
- import { C as resourceReference, D as sourceReferences, F as lensSchema, H as markdownNodes, I as lensSelections, U as parseMarkdown, Y as sourceAnchors, _ as hasCodeReferences, c as traceSchema, h as elements, k as checkReferences, l as textIncludesQuote, n as migrateStoredDocument, o as normalizedSoftwareElementSchema, p as documentSchema, r as migrateStoredLenses, s as normalizedSoftwareRelationshipSchema, t as liftFileLenses, u as anchorPins, w as resourceReferences, x as pinsSchema } from "./stored-document-migration-DLBhkSfp.js";
3
+ import "./src-DsM3jO8G.js";
4
+ import { C as resourceReference, D as sourceReferences, F as lensSchema, H as markdownNodes, I as lensSelections, U as parseMarkdown, Y as sourceAnchors, _ as hasCodeReferences, c as traceSchema, h as elements, k as checkReferences, l as textIncludesQuote, n as migrateStoredDocument, o as normalizedSoftwareElementSchema, p as documentSchema, r as migrateStoredLenses, s as normalizedSoftwareRelationshipSchema, t as liftFileLenses, u as anchorPins, w as resourceReferences, x as pinsSchema } from "./stored-document-migration-CChxtFAy.js";
5
5
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  import { z } from "zod";
@@ -1,6 +1,6 @@
1
1
  import "./review-home-paths-Lc02WFuX.js";
2
2
  import { M as devReviewHome } from "./s3-config-DDWWMh5x.js";
3
- import { n as askAgentIds } from "./thread-state-DZihwhfu.js";
3
+ import { n as askAgentIds } from "./thread-state-5pYr6_zM.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { constants } from "node:fs";
6
6
  import { access, chmod, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
@@ -1,21 +1,21 @@
1
1
  import { d as git, r as defaultBranch } from "./src-BMPuxWv4.js";
2
2
  import { c as jsonString, m as isStringValue, n as isJsonObject, p as isObjectValue, u as parseJsonText } from "./src-X9phtB2j.js";
3
- import { a as sharedReviewId, c as verifyShareRepository, d as StreamLimitError, f as readBoundedStream, l as exportShare, m as shareIdSchema, n as ShareClient, p as parseShareLink, r as SharePreflightError, s as readShareRepository, t as ShareAuthError, u as validateShareSources } from "./client-CLqKkzOc.js";
4
- import { a as inspectSnapshot, d as invalidateStructuralComparisons, f as diffrExecutable, i as commandSchema, p as diffrMissingError, r as SCRATCHPAD_ID } from "./local-data-DKmj2ygd.js";
5
- import { A as reviewDiffrSummarizerInputSchema, D as ReviewOnboardingRunRequestSchema, a as parseReviewVerbRequest, g as REVIEW_HOST_HEADER, i as parseReviewDesktopVerbResult, j as decodeStructuralDiffEvent } from "./src-DZG1xAqW.js";
3
+ import { a as sharedReviewId, c as verifyShareRepository, d as StreamLimitError, f as readBoundedStream, l as exportShare, m as shareIdSchema, n as ShareClient, p as parseShareLink, r as SharePreflightError, s as readShareRepository, t as ShareAuthError, u as validateShareSources } from "./client-BN_FDxFr.js";
4
+ import { a as inspectSnapshot, d as invalidateStructuralComparisons, f as diffrExecutable, i as commandSchema, p as diffrMissingError, r as SCRATCHPAD_ID } from "./local-data-DtQTIzHX.js";
5
+ import { D as ReviewFeedbackSchema, E as ReviewFeedbackContactSchema, M as reviewDiffrSummarizerInputSchema, N as decodeStructuralDiffEvent, a as parseReviewVerbRequest, g as REVIEW_HOST_HEADER, i as parseReviewDesktopVerbResult, k as ReviewOnboardingRunRequestSchema } from "./src-DsM3jO8G.js";
6
6
  import { B as TraceStorageDeniedError, D as listReviewTraceSessions, I as isTraceStorageConfigured, O as loadReviewAgentTrace, R as resolveTraceStorage, T as traceMachineEnabled, at as openUrlInBrowser, et as shellQuote, it as clearStoreAuth, ot as readStoreAuth, rt as DEFAULT_STORE_ORIGIN, st as runStoreLogin, z as selectTraceStorage } from "./review-home-paths-Lc02WFuX.js";
7
- import { E as selectionReferences, G as comparisonKey, I as lensSelections, J as selectionKey, K as formatAnchor, M as UNCATEGORIZED_LENS_ID, Q as emptyCoverage, W as anchorSelection, Z as coverageSources, et as scopedCoverage, h as elements, j as stepSources, nt as unionIntervals, q as resolveDiffSelection, tt as subtractIntervals, u as anchorPins, z as fileLineRangeSchema } from "./stored-document-migration-DLBhkSfp.js";
7
+ import { E as selectionReferences, G as comparisonKey, I as lensSelections, J as selectionKey, K as formatAnchor, M as UNCATEGORIZED_LENS_ID, Q as emptyCoverage, W as anchorSelection, Z as coverageSources, et as scopedCoverage, h as elements, j as stepSources, nt as unionIntervals, q as resolveDiffSelection, tt as subtractIntervals, u as anchorPins, z as fileLineRangeSchema } from "./stored-document-migration-CChxtFAy.js";
8
8
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
9
9
  import { A as withFileLock, I as writePrivateJsonAtomic, M as devReviewHome, h as TraceConfigurationError, i as isS3MockMode } from "./s3-config-DDWWMh5x.js";
10
10
  import { t as errorMessage } from "./error-message-OtiDonty.js";
11
11
  import { i as readReviewPackageVersion, n as readBuildCommit, t as findReviewPackageRoot } from "./package-paths-jMg6JxDP.js";
12
- import { i as readReviewServerDiscovery, n as desktopOwnsHome, o as reviewServerDiscoveryPath, p as desktopUsesHome, r as headlessServerLockPath, t as DESKTOP_TAKEOVER_SIGNAL } from "./server-discovery-DVqrcIDT.js";
13
- import { c as askPicksSchema, d as selectionMarkdown, l as askQuestionSchema, n as askAgentIds, r as askChoiceKinds, s as askPermissionsSchema, t as applyAskChange, u as AgentSelectionSchema } from "./thread-state-DZihwhfu.js";
14
- import { t as openReviewProfile } from "./profile-B58hz5n7.js";
15
- import { a as REVIEW_APP_SESSION_ID_HEADER, c as sanitizeUiTelemetryEvent, i as BUNDLE_FRAME_PATTERN, l as cleanTelemetryText, r as reviewRequestOrigin, s as isReportableCleanedMessage } from "./request-origin-BSwTNS8K.js";
16
- import { a as defaultAskPermissions, c as launchAskAgent, i as askAgents, n as askAgentSettings, o as detectAskAgents, r as askAgentTakesMcp, t as fuzzyRank } from "./fuzzy-match-C9olHRCK.js";
17
- import { a as scratchpadAvailable, c as readQuerySchemas, i as renderInstructions, o as inspectQuerySchema, r as instructionsQuerySchema, s as queryAnchor, t as authoringTools } from "./authoring-tools-ByMdW_S8.js";
18
- import { a as headlessServerOwner, c as stopBackgroundServer } from "./background-server-ONy-xKJ1.js";
12
+ import { _ as reviewInstanceIdentity, i as readReviewServerDiscovery, n as desktopOwnsHome, o as reviewServerDiscoveryPath, p as desktopUsesHome, r as headlessServerLockPath, t as DESKTOP_TAKEOVER_SIGNAL } from "./server-discovery-BVqrE1_-.js";
13
+ import { c as askPicksSchema, d as selectionMarkdown, l as askQuestionSchema, n as askAgentIds, r as askChoiceKinds, s as askPermissionsSchema, t as applyAskChange, u as AgentSelectionSchema } from "./thread-state-5pYr6_zM.js";
14
+ import { t as openReviewProfile } from "./profile-DaqmODhc.js";
15
+ import { a as REVIEW_APP_SESSION_ID_HEADER, c as sanitizeUiTelemetryEvent, i as BUNDLE_FRAME_PATTERN, l as cleanTelemetryText, r as reviewRequestOrigin, s as isReportableCleanedMessage } from "./request-origin-Zcn_CjX4.js";
16
+ import { a as defaultAskPermissions, c as launchAskAgent, i as askAgents, n as askAgentSettings, o as detectAskAgents, r as askAgentTakesMcp, t as fuzzyRank } from "./fuzzy-match-bFX6wm8j.js";
17
+ import { a as scratchpadAvailable, c as readQuerySchemas, i as renderInstructions, o as inspectQuerySchema, r as instructionsQuerySchema, s as queryAnchor, t as authoringTools } from "./authoring-tools-DGSEag8y.js";
18
+ import { a as headlessServerOwner, c as stopBackgroundServer } from "./background-server-WKzI_a98.js";
19
19
  import crypto$1, { createHash, randomBytes, randomUUID } from "node:crypto";
20
20
  import { z } from "zod";
21
21
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
@@ -3566,6 +3566,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3566
3566
  await ensureScratchpad();
3567
3567
  return context.json(catalog());
3568
3568
  });
3569
+ app.get("/feedback-eligibility", (context) => context.json({ eligible: store.feedbackEligible() }));
3569
3570
  const instructionContext = async () => ({
3570
3571
  desktopAvailable: Boolean(open),
3571
3572
  scratchpadEnabled: scratchpadEnabled(),
@@ -4482,6 +4483,51 @@ function watch(lines, subscribe) {
4482
4483
  } });
4483
4484
  }
4484
4485
  //#endregion
4486
+ //#region src/server/feedback.ts
4487
+ const FEEDBACK_URL = "https://dev.fast/api/feedback";
4488
+ /** Runs behind the local API's auth middleware. No telemetry envelope is sent. */
4489
+ function mountFeedback(app) {
4490
+ const localUrl = process.env.DEV_FAST_REVIEW_FEEDBACK_URL;
4491
+ const receiver = localUrl && reviewInstanceIdentity(process.env).channel !== "stable" && [
4492
+ "localhost",
4493
+ "127.0.0.1",
4494
+ "[::1]"
4495
+ ].includes(new URL(localUrl).hostname) ? localUrl : FEEDBACK_URL;
4496
+ for (const [path, schema] of [["/feedback", ReviewFeedbackSchema], ["/feedback/contact", ReviewFeedbackContactSchema]]) app.post(path, async (context) => {
4497
+ const input = schema.safeParse(await readBoundedRequestJson(context.req.raw, 65536));
4498
+ if (!input.success) return context.json({
4499
+ ok: false,
4500
+ error: "Invalid feedback."
4501
+ }, 400);
4502
+ const signal = AbortSignal.any([context.req.raw.signal, AbortSignal.timeout(5e3)]);
4503
+ try {
4504
+ const response = await fetch(receiver, {
4505
+ method: "POST",
4506
+ headers: { "content-type": "application/json" },
4507
+ body: JSON.stringify(input.data),
4508
+ signal,
4509
+ redirect: "error"
4510
+ });
4511
+ const body = await response.json().catch((error) => {
4512
+ if (signal.aborted || error instanceof TypeError) throw error;
4513
+ return null;
4514
+ });
4515
+ if (!response.ok || body?.ok !== true) return context.json({
4516
+ ok: false,
4517
+ code: "receiver",
4518
+ error: "Feedback could not be saved. Please try again."
4519
+ }, 502);
4520
+ return context.json({ ok: true });
4521
+ } catch {
4522
+ return context.json({
4523
+ ok: false,
4524
+ code: "network",
4525
+ error: "Feedback could not be saved. Please try again."
4526
+ }, 502);
4527
+ }
4528
+ });
4529
+ }
4530
+ //#endregion
4485
4531
  //#region src/server/review-server-core.ts
4486
4532
  const version = readReviewPackageVersion(import.meta.url);
4487
4533
  const commit = readBuildCommit(import.meta.url);
@@ -4519,6 +4565,7 @@ function createReviewServerApp(input) {
4519
4565
  await next();
4520
4566
  });
4521
4567
  app.get("/control", (context) => openControlEvents(context, input.relay));
4568
+ mountFeedback(app);
4522
4569
  app.post("/control/result", async (context) => {
4523
4570
  const accepted = input.relay.acceptResult(await readBoundedRequestJson(context.req.raw));
4524
4571
  return serverJson(accepted ? 200 : 404, { ok: accepted });
@@ -1,2 +1,2 @@
1
- import { i as withHeadlessServerLock, n as runHeadlessServer, r as takeOverServerHome, t as HeadlessServerBusyError } from "./headless-host-CnCDrHTx.js";
1
+ import { i as withHeadlessServerLock, n as runHeadlessServer, r as takeOverServerHome, t as HeadlessServerBusyError } from "./headless-host-CS4Gv0c5.js";
2
2
  export { HeadlessServerBusyError, runHeadlessServer, takeOverServerHome, withHeadlessServerLock };
@@ -1,13 +1,13 @@
1
1
  import { _ as parseGitRemote, a as diff, b as resolveRepoContextSync, c as diffFileSummariesWorkingTree, d as git, g as listTrackedFilesAtCommit, h as listCommitRange, i as detectLocalVcs, l as diffTrees, m as jjRevisionIsConflicted, n as createBlobBatchReader, p as gitCommonDir, s as diffFileSummariesTrees, t as LocalVcsToolsMissingError, u as diffWorkingTree, v as readFileAtCommit, x as resolveRevision, y as resolveRepoContext } from "./src-BMPuxWv4.js";
2
2
  import { c as jsonString, o as jsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
3
- import { M as structuralChangeCounts, j as decodeStructuralDiffEvent, k as SCRATCHPAD_REVIEW_ID } from "./src-DZG1xAqW.js";
3
+ import { N as decodeStructuralDiffEvent, P as structuralChangeCounts, j as SCRATCHPAD_REVIEW_ID } from "./src-DsM3jO8G.js";
4
4
  import { s as safeStorageSegment } from "./review-home-paths-Lc02WFuX.js";
5
- import { $ as intersectIntervals, A as documentAdvice, B as requireVisibleSource, C as resourceReference, D as sourceReferences, I as lensSelections, L as SourceRangeError, N as applyLensEdit, O as summarizeEdit, P as lensEditSchema, Q as emptyCoverage, R as checkSourcePath, T as reviewTargetSchema, V as sliceSourceRange, X as coverageSchema, Y as sourceAnchors, a as defineSoftwareMap, b as pinnedRepositoryIds, c as traceSchema, d as applyEdit, f as assignFreshIds, g as explicitPins, h as elements, i as SoftwareModelValidationError, k as checkReferences, l as textIncludesQuote, m as editSchema, n as migrateStoredDocument, nt as unionIntervals, p as documentSchema, r as migrateStoredLenses, rt as updateCoverage, t as liftFileLenses, tt as subtractIntervals, u as anchorPins, v as isUnit, w as resourceReferences, x as pinsSchema, z as fileLineRangeSchema } from "./stored-document-migration-DLBhkSfp.js";
5
+ import { $ as intersectIntervals, A as documentAdvice, B as requireVisibleSource, C as resourceReference, D as sourceReferences, I as lensSelections, L as SourceRangeError, N as applyLensEdit, O as summarizeEdit, P as lensEditSchema, Q as emptyCoverage, R as checkSourcePath, T as reviewTargetSchema, V as sliceSourceRange, X as coverageSchema, Y as sourceAnchors, a as defineSoftwareMap, b as pinnedRepositoryIds, c as traceSchema, d as applyEdit, f as assignFreshIds, g as explicitPins, h as elements, i as SoftwareModelValidationError, k as checkReferences, l as textIncludesQuote, m as editSchema, n as migrateStoredDocument, nt as unionIntervals, p as documentSchema, r as migrateStoredLenses, rt as updateCoverage, t as liftFileLenses, tt as subtractIntervals, u as anchorPins, v as isUnit, w as resourceReferences, x as pinsSchema, z as fileLineRangeSchema } from "./stored-document-migration-CChxtFAy.js";
6
6
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
7
7
  import { A as withFileLock, I as writePrivateJsonAtomic, O as processIsAlive } from "./s3-config-DDWWMh5x.js";
8
8
  import { t as errorMessage } from "./error-message-OtiDonty.js";
9
9
  import { r as isMissingFileError } from "./fs-utils-BMPLt0cr.js";
10
- import { a as askHistoryEntrySchema, i as askEntrySchema, o as askOfferSchema, s as askPermissionsSchema } from "./thread-state-DZihwhfu.js";
10
+ import { a as askHistoryEntrySchema, i as askEntrySchema, o as askOfferSchema, s as askPermissionsSchema } from "./thread-state-5pYr6_zM.js";
11
11
  import { createRequire } from "node:module";
12
12
  import crypto, { createHash, randomUUID } from "node:crypto";
13
13
  import { z } from "zod";
@@ -2552,6 +2552,19 @@ var ReviewStore = class {
2552
2552
  if (!this.has(SCRATCHPAD_ID)) throw error;
2553
2553
  }
2554
2554
  }
2555
+ /** Two saved board/day pairs in the last fourteen days. */
2556
+ feedbackEligible(now = Date.now()) {
2557
+ const row = this.db.prepare(`SELECT count(*) AS pairs FROM (
2558
+ SELECT DISTINCT review_id, date(json_extract(snapshot, '$.createdAt')) AS utc_date
2559
+ FROM versions
2560
+ WHERE (version = 0 OR json_type(snapshot, '$.lastEdit') = 'object')
2561
+ AND json_extract(snapshot, '$.kind') IS NOT 'scratchpad'
2562
+ AND json_extract(snapshot, '$.origin.tutorial') IS NOT 1
2563
+ AND julianday(json_extract(snapshot, '$.createdAt')) BETWEEN julianday(?) AND julianday(?)
2564
+ LIMIT 2
2565
+ )`).get((/* @__PURE__ */ new Date(now - 12096e5)).toISOString(), new Date(now).toISOString());
2566
+ return Number(row?.pairs) >= 2;
2567
+ }
2555
2568
  history(id) {
2556
2569
  return this.db.prepare("SELECT version,json_extract(snapshot,'$.title') AS title,json_extract(snapshot,'$.createdAt') AS created_at FROM versions WHERE review_id=? ORDER BY version").all(id).map((row) => ({
2557
2570
  version: Number(row.version),
@@ -1,7 +1,7 @@
1
- import { a as toolResultText, s as ReviewApiError } from "./agent-client-DjXQy6CL.js";
1
+ import { a as toolResultText, s as ReviewApiError } from "./agent-client-BRBzv3AV.js";
2
2
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
3
- import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-ByMdW_S8.js";
4
- import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-CVJrfi5s.js";
3
+ import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-DGSEag8y.js";
4
+ import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-BFt2xj9i.js";
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -0,0 +1,2 @@
1
+ import { t as openReviewProfile } from "./profile-DaqmODhc.js";
2
+ export { openReviewProfile };
@@ -1,4 +1,4 @@
1
- import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-DKmj2ygd.js";
1
+ import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-DtQTIzHX.js";
2
2
  import "./review-home-paths-Lc02WFuX.js";
3
3
  import { A as withFileLock } from "./s3-config-DDWWMh5x.js";
4
4
  import { access, mkdir } from "node:fs/promises";
@@ -1,7 +1,7 @@
1
1
  import { i as readReviewPackageVersion } from "./package-paths-jMg6JxDP.js";
2
- import { a as readReviewServerHealth, l as serverNotReady } from "./server-discovery-DVqrcIDT.js";
3
- import { c as stopBackgroundServer, o as recordedBackgroundServer, r as ensureBackgroundServer } from "./background-server-ONy-xKJ1.js";
4
- import { t as ensureRemoteLanguageServer } from "./remote-language-server-CGsCaqro.js";
2
+ import { a as readReviewServerHealth, l as serverNotReady } from "./server-discovery-BVqrE1_-.js";
3
+ import { c as stopBackgroundServer, o as recordedBackgroundServer, r as ensureBackgroundServer } from "./background-server-WKzI_a98.js";
4
+ import { t as ensureRemoteLanguageServer } from "./remote-language-server-UWQgpoh-.js";
5
5
  import path from "node:path";
6
6
  import { spawn } from "node:child_process";
7
7
  import { gt, valid } from "semver";
@@ -1,7 +1,7 @@
1
1
  import "./review-home-paths-Lc02WFuX.js";
2
2
  import { A as withFileLock, k as processStartIdentity } from "./s3-config-DDWWMh5x.js";
3
3
  import { t as findReviewPackageRoot } from "./package-paths-jMg6JxDP.js";
4
- import { n as cliSpawn, s as spawnDetached } from "./background-server-ONy-xKJ1.js";
4
+ import { n as cliSpawn, s as spawnDetached } from "./background-server-WKzI_a98.js";
5
5
  import { r as remoteServerPaths, t as ensureRemoteExtensions } from "./remote-extensions-BCAbQZTv.js";
6
6
  import { randomBytes } from "node:crypto";
7
7
  import { z } from "zod";
@@ -1,9 +1,9 @@
1
- import { b as REVIEW_REMOTE_INSTALL_LOCK, x as REVIEW_REMOTE_WRAPPER_MARK } from "./src-DZG1xAqW.js";
1
+ import { b as REVIEW_REMOTE_INSTALL_LOCK, x as REVIEW_REMOTE_WRAPPER_MARK } from "./src-DsM3jO8G.js";
2
2
  import "./review-home-paths-Lc02WFuX.js";
3
3
  import { j as DEV_REVIEW_HOME_ENV } from "./s3-config-DDWWMh5x.js";
4
- import { c as stopBackgroundServer, o as recordedBackgroundServer } from "./background-server-ONy-xKJ1.js";
4
+ import { c as stopBackgroundServer, o as recordedBackgroundServer } from "./background-server-WKzI_a98.js";
5
5
  import { i as whiteboardRemoteHome } from "./remote-extensions-BCAbQZTv.js";
6
- import { n as remoteLanguageServerGroups, r as stopProcessGroup } from "./remote-language-server-CGsCaqro.js";
6
+ import { n as remoteLanguageServerGroups, r as stopProcessGroup } from "./remote-language-server-UWQgpoh-.js";
7
7
  import { existsSync } from "node:fs";
8
8
  import { lstat, mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
9
9
  import path from "node:path";
@@ -1,6 +1,6 @@
1
1
  import { a as jsonNumber, c as jsonString, i as jsonBoolean, n as isJsonObject } from "./src-X9phtB2j.js";
2
- import { S as REVIEW_THEME_CHOICES, h as REVIEW_DIFF_LAYOUT_SCOPES, m as REVIEW_DIFF_LAYOUTS } from "./src-DZG1xAqW.js";
3
- import { n as askAgentIds } from "./thread-state-DZihwhfu.js";
2
+ import { S as REVIEW_THEME_CHOICES, h as REVIEW_DIFF_LAYOUT_SCOPES, m as REVIEW_DIFF_LAYOUTS } from "./src-DsM3jO8G.js";
3
+ import { n as askAgentIds } from "./thread-state-5pYr6_zM.js";
4
4
  import { z } from "zod";
5
5
  //#region src/telemetry-clean-text.ts
6
6
  const USER_DATA_REGEXES = [
@@ -1,5 +1,5 @@
1
- import { o as parseReviewVerbResponse } from "./src-DZG1xAqW.js";
2
- import { b as selectReviewInstance, m as healthyReviewInstance, v as reviewInstanceStartHint, y as reviewInstanceUnavailable } from "./server-discovery-DVqrcIDT.js";
1
+ import { o as parseReviewVerbResponse } from "./src-DsM3jO8G.js";
2
+ import { b as selectReviewInstance, m as healthyReviewInstance, v as reviewInstanceStartHint, y as reviewInstanceUnavailable } from "./server-discovery-BVqrE1_-.js";
3
3
  import { closeSync, existsSync, fstatSync, mkdtempSync, openSync, readSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { homedir, tmpdir } from "node:os";
@@ -1,9 +1,9 @@
1
1
  import { c as jsonString, l as jsonValueSchema, o as jsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
2
- import "./src-DZG1xAqW.js";
2
+ import "./src-DsM3jO8G.js";
3
3
  import "./review-home-paths-Lc02WFuX.js";
4
4
  import { A as withFileLock, M as devReviewHome, O as processIsAlive, P as writeFileAtomic } from "./s3-config-DDWWMh5x.js";
5
5
  import { i as readReviewPackageVersion, t as findReviewPackageRoot } from "./package-paths-jMg6JxDP.js";
6
- import "./request-origin-BSwTNS8K.js";
6
+ import "./request-origin-Zcn_CjX4.js";
7
7
  import { createHmac, randomUUID } from "node:crypto";
8
8
  import { z } from "zod";
9
9
  import { existsSync, readFileSync, rmSync } from "node:fs";
@@ -1,22 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import { S as splitGitPatchFiles, a as diff, o as diffFileSummaries } from "../src-BMPuxWv4.js";
3
3
  import { c as jsonString, i as jsonBoolean, n as isJsonObject, p as isObjectValue, s as jsonProperty, u as parseJsonText } from "../src-X9phtB2j.js";
4
- import { d as StreamLimitError, f as readBoundedStream, i as SharedReviewStore } from "../client-CLqKkzOc.js";
5
- import { u as pullRequestProvider } from "../local-data-DKmj2ygd.js";
6
- import { A as reviewDiffrSummarizerInputSchema, E as ReviewGatewayHostSchema, T as ReviewConnectRequestSchema, c as ReviewBugReportMetaV2Schema, d as REVIEW_CLIENT_HEADER, f as REVIEW_CLIENT_REMOTE, g as REVIEW_HOST_HEADER, l as ReviewBugReportRequestSchema, r as parseReviewDesktopVerbFrame, s as parseZod, t as parseReviewCliInstallApplyRequest, u as parseReviewBugReportResponse } from "../src-DZG1xAqW.js";
4
+ import { d as StreamLimitError, f as readBoundedStream, i as SharedReviewStore } from "../client-BN_FDxFr.js";
5
+ import { u as pullRequestProvider } from "../local-data-DtQTIzHX.js";
6
+ import { M as reviewDiffrSummarizerInputSchema, O as ReviewGatewayHostSchema, T as ReviewConnectRequestSchema, c as ReviewBugReportMetaV2Schema, d as REVIEW_CLIENT_HEADER, f as REVIEW_CLIENT_REMOTE, g as REVIEW_HOST_HEADER, l as ReviewBugReportRequestSchema, r as parseReviewDesktopVerbFrame, s as parseZod, t as parseReviewCliInstallApplyRequest, u as parseReviewBugReportResponse } from "../src-DsM3jO8G.js";
7
7
  import { i as reviewInstanceDiscoveryPath, o as reviewLegacyDiscoveryPath } from "../review-home-paths-Lc02WFuX.js";
8
- import { w as resourceReferences } from "../stored-document-migration-DLBhkSfp.js";
8
+ import { w as resourceReferences } from "../stored-document-migration-CChxtFAy.js";
9
9
  import { t as ReviewInputError } from "../input-error-OLgB_h31.js";
10
10
  import { I as writePrivateJsonAtomic, M as devReviewHome } from "../s3-config-DDWWMh5x.js";
11
11
  import { t as errorMessage } from "../error-message-OtiDonty.js";
12
12
  import { i as readReviewPackageVersion, t as findReviewPackageRoot } from "../package-paths-jMg6JxDP.js";
13
- import { _ as reviewInstanceIdentity, u as REVIEW_INSTANCE_ENV } from "../server-discovery-DVqrcIDT.js";
14
- import { t as openReviewProfile } from "../profile-B58hz5n7.js";
15
- import { r as reviewTelemetryChannel, t as ReviewTelemetry } from "../review-telemetry-ClhP_5lW.js";
16
- import { o as REVIEW_TELEMETRY_TABS } from "../request-origin-BSwTNS8K.js";
17
- import { c as removeLegacyReviewSkills, d as skipCliInstall, h as detectConnectAgents, i as finishCliInstallUpdate, l as resetCliInstall, m as connectAgent, n as applyCliInstall, o as pathShimPath, r as declineCliInstall, s as removeCliInstall, t as aliasInstallationToAccount, u as resolveCliInstallStatus } from "../account-alias-D_mqo_Jk.js";
18
- import { C as saveDiffrSummarizer, E as cliAskTools, S as readDiffrConfig, T as testDiffrSummarizer, _ as DEFAULT_MAX_REQUEST_BYTES, a as drainServerCrashReport, b as GlobalReviewDesktopVerbRelay, c as answerErrorsAsJson, d as legacyModeSchema, f as captureSanitizedUiTelemetry, g as readBoundedRequestJson, h as createNodeRequestListener, l as createWhiteboardCore, m as recordClientError, o as installProcessErrorTelemetry, p as clientErrorsForSession, r as takeOverServerHome, s as serverCrashReportPath, u as serverJson, v as HttpJsonError, w as setDiffrConfigValue, x as migrateDiffrConfig, y as ReviewServerError } from "../headless-host-CnCDrHTx.js";
19
- import { c as readQuerySchemas } from "../authoring-tools-ByMdW_S8.js";
13
+ import { _ as reviewInstanceIdentity, u as REVIEW_INSTANCE_ENV } from "../server-discovery-BVqrE1_-.js";
14
+ import { t as openReviewProfile } from "../profile-DaqmODhc.js";
15
+ import { r as reviewTelemetryChannel, t as ReviewTelemetry } from "../review-telemetry-BHWJsnoD.js";
16
+ import { o as REVIEW_TELEMETRY_TABS } from "../request-origin-Zcn_CjX4.js";
17
+ import { c as removeLegacyReviewSkills, d as skipCliInstall, h as detectConnectAgents, i as finishCliInstallUpdate, l as resetCliInstall, m as connectAgent, n as applyCliInstall, o as pathShimPath, r as declineCliInstall, s as removeCliInstall, t as aliasInstallationToAccount, u as resolveCliInstallStatus } from "../account-alias-xOdmMnDH.js";
18
+ import { C as saveDiffrSummarizer, E as cliAskTools, S as readDiffrConfig, T as testDiffrSummarizer, _ as DEFAULT_MAX_REQUEST_BYTES, a as drainServerCrashReport, b as GlobalReviewDesktopVerbRelay, c as answerErrorsAsJson, d as legacyModeSchema, f as captureSanitizedUiTelemetry, g as readBoundedRequestJson, h as createNodeRequestListener, l as createWhiteboardCore, m as recordClientError, o as installProcessErrorTelemetry, p as clientErrorsForSession, r as takeOverServerHome, s as serverCrashReportPath, u as serverJson, v as HttpJsonError, w as setDiffrConfigValue, x as migrateDiffrConfig, y as ReviewServerError } from "../headless-host-CS4Gv0c5.js";
19
+ import { c as readQuerySchemas } from "../authoring-tools-DGSEag8y.js";
20
20
  import crypto, { createHash, randomBytes } from "node:crypto";
21
21
  import { z } from "zod";
22
22
  import fs, { existsSync, readFileSync } from "node:fs";
@@ -1,5 +1,5 @@
1
1
  import { a as jsonNumber, n as isJsonObject, s as jsonProperty, u as parseJsonText } from "./src-X9phtB2j.js";
2
- import { n as parseReviewDesktopDiscovery } from "./src-DZG1xAqW.js";
2
+ import { n as parseReviewDesktopDiscovery } from "./src-DsM3jO8G.js";
3
3
  import { a as reviewInstancesDir, o as reviewLegacyDiscoveryPath, r as reviewDevInstanceKey, t as reviewDefaultInstancePath } from "./review-home-paths-Lc02WFuX.js";
4
4
  import { M as devReviewHome, O as processIsAlive, j as DEV_REVIEW_HOME_ENV } from "./s3-config-DDWWMh5x.js";
5
5
  import { z } from "zod";
@@ -1484,6 +1484,8 @@ declare class ReviewStore {
1484
1484
  /** The one scratchpad, made on first use. A host that loses the race to
1485
1485
  * another on the same home finds it made. */
1486
1486
  ensureScratchpad(): Promise<void>;
1487
+ /** Two saved board/day pairs in the last fourteen days. */
1488
+ feedbackEligible(now?: number): boolean;
1487
1489
  history(id: string): {
1488
1490
  version: number;
1489
1491
  title: string;
@@ -1,2 +1,2 @@
1
- import { a as sharedReviewId, c as verifyShareRepository, i as SharedReviewStore, l as exportShare, n as ShareClient, o as validateShareBundle } from "../client-CLqKkzOc.js";
1
+ import { a as sharedReviewId, c as verifyShareRepository, i as SharedReviewStore, l as exportShare, n as ShareClient, o as validateShareBundle } from "../client-BN_FDxFr.js";
2
2
  export { ShareClient, SharedReviewStore, exportShare, sharedReviewId, validateShareBundle, verifyShareRepository };
@@ -831,6 +831,66 @@ z.discriminatedUnion("ok", [z.strictObject({
831
831
  toolCalls: z.number(),
832
832
  events: z.array(ReviewAgentTraceEventSchema)
833
833
  }), ReviewErrorResponseSchema]);
834
+ const REVIEW_FEEDBACK_CAMPAIGN = "whiteboard-feedback-2026-10";
835
+ /** The host supplies its persistent installation ID, not a form or launch ID. */
836
+ const feedbackId = z.string().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/);
837
+ const feedbackSelections = (values) => z.array(z.enum(values)).max(values.length).refine((items) => new Set(items).size === items.length);
838
+ /** Required survey answers and optional contact are separate submissions. */
839
+ const ReviewFeedbackSchema = z.strictObject({
840
+ id: feedbackId,
841
+ campaign: z.literal(REVIEW_FEEDBACK_CAMPAIGN),
842
+ environment: z.enum([
843
+ "production",
844
+ "preview",
845
+ "development",
846
+ "test"
847
+ ]).optional(),
848
+ contexts: feedbackSelections([
849
+ "work",
850
+ "personal_projects",
851
+ "open_source_projects",
852
+ "other"
853
+ ]).refine((items) => items.length > 0),
854
+ contextsOther: z.string().trim().max(500).optional(),
855
+ benefit: z.string().trim().min(1).max(4e3),
856
+ feature: z.enum([
857
+ "code_understanding",
858
+ "planning_editing",
859
+ "collaboration",
860
+ "other"
861
+ ]),
862
+ featureOther: z.string().trim().max(500).optional(),
863
+ rating: z.number().int().min(1).max(5),
864
+ roles: feedbackSelections([
865
+ "developer",
866
+ "engineering_manager",
867
+ "founder",
868
+ "other"
869
+ ]).refine((items) => items.length > 0),
870
+ rolesOther: z.string().trim().max(500).optional()
871
+ }).superRefine((response, context) => {
872
+ for (const [selected, other] of [
873
+ [response.contexts.includes("other"), "contextsOther"],
874
+ [response.feature === "other", "featureOther"],
875
+ [response.roles.includes("other"), "rolesOther"]
876
+ ]) if (selected ? !response[other]?.trim() : response[other] !== void 0) context.addIssue({
877
+ code: "custom",
878
+ path: [other],
879
+ message: selected ? "Describe Other." : "Select Other before supplying its text."
880
+ });
881
+ });
882
+ const ReviewFeedbackContactSchema = z.strictObject({
883
+ kind: z.literal("contact"),
884
+ id: feedbackId,
885
+ campaign: z.literal(REVIEW_FEEDBACK_CAMPAIGN),
886
+ environment: z.enum([
887
+ "production",
888
+ "preview",
889
+ "development",
890
+ "test"
891
+ ]).optional(),
892
+ email: z.email().max(254)
893
+ });
834
894
  //#endregion
835
895
  //#region ../review-protocol/src/bug-report.ts
836
896
  const requiredString = z.string({ error: "must be a string" }).refine((value) => value.trim().length > 0, "must be a string");
@@ -979,4 +1039,4 @@ function formatIssuePath(path) {
979
1039
  return output;
980
1040
  }
981
1041
  //#endregion
982
- export { reviewDiffrSummarizerInputSchema as A, ReviewApiSelectionSourceSchema as C, ReviewOnboardingRunRequestSchema as D, ReviewGatewayHostSchema as E, structuralChangeCounts as M, ReviewSelectedDiffSchema as O, REVIEW_THEME_CHOICES as S, ReviewConnectRequestSchema as T, REVIEW_REMOTE_AGENT_IDS as _, parseReviewVerbRequest as a, REVIEW_REMOTE_INSTALL_LOCK as b, ReviewBugReportMetaV2Schema as c, REVIEW_CLIENT_HEADER as d, REVIEW_CLIENT_REMOTE as f, REVIEW_HOST_HEADER as g, REVIEW_DIFF_LAYOUT_SCOPES as h, parseReviewDesktopVerbResult as i, decodeStructuralDiffEvent as j, SCRATCHPAD_REVIEW_ID as k, ReviewBugReportRequestSchema as l, REVIEW_DIFF_LAYOUTS as m, parseReviewDesktopDiscovery as n, parseReviewVerbResponse as o, REVIEW_CLI_INSTALL_TARGET_LABELS as p, parseReviewDesktopVerbFrame as r, parseZod as s, parseReviewCliInstallApplyRequest as t, parseReviewBugReportResponse as u, REVIEW_REMOTE_ATTACH_BEGIN as v, ReviewCliInstallStampSchema as w, REVIEW_REMOTE_WRAPPER_MARK as x, REVIEW_REMOTE_ATTACH_END as y };
1042
+ export { ReviewSelectedDiffSchema as A, ReviewApiSelectionSourceSchema as C, ReviewFeedbackSchema as D, ReviewFeedbackContactSchema as E, reviewDiffrSummarizerInputSchema as M, decodeStructuralDiffEvent as N, ReviewGatewayHostSchema as O, structuralChangeCounts as P, REVIEW_THEME_CHOICES as S, ReviewConnectRequestSchema as T, REVIEW_REMOTE_AGENT_IDS as _, parseReviewVerbRequest as a, REVIEW_REMOTE_INSTALL_LOCK as b, ReviewBugReportMetaV2Schema as c, REVIEW_CLIENT_HEADER as d, REVIEW_CLIENT_REMOTE as f, REVIEW_HOST_HEADER as g, REVIEW_DIFF_LAYOUT_SCOPES as h, parseReviewDesktopVerbResult as i, SCRATCHPAD_REVIEW_ID as j, ReviewOnboardingRunRequestSchema as k, ReviewBugReportRequestSchema as l, REVIEW_DIFF_LAYOUTS as m, parseReviewDesktopDiscovery as n, parseReviewVerbResponse as o, REVIEW_CLI_INSTALL_TARGET_LABELS as p, parseReviewDesktopVerbFrame as r, parseZod as s, parseReviewCliInstallApplyRequest as t, parseReviewBugReportResponse as u, REVIEW_REMOTE_ATTACH_BEGIN as v, ReviewCliInstallStampSchema as w, REVIEW_REMOTE_WRAPPER_MARK as x, REVIEW_REMOTE_ATTACH_END as y };
@@ -1,5 +1,5 @@
1
1
  import { f as isNumberValue, l as jsonValueSchema, m as isStringValue, n as isJsonObject } from "./src-X9phtB2j.js";
2
- import "./src-DZG1xAqW.js";
2
+ import "./src-DsM3jO8G.js";
3
3
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
4
4
  import { z } from "zod";
5
5
  import { fromMarkdown } from "mdast-util-from-markdown";
@@ -1,4 +1,4 @@
1
- import { C as ReviewApiSelectionSourceSchema, O as ReviewSelectedDiffSchema } from "./src-DZG1xAqW.js";
1
+ import { A as ReviewSelectedDiffSchema, C as ReviewApiSelectionSourceSchema } from "./src-DsM3jO8G.js";
2
2
  import { z } from "zod";
3
3
  /** A semantic selection, independent of comment/thread creation. */
4
4
  const AgentSelectionSchema = z.strictObject({
@@ -1,5 +1,5 @@
1
1
  import { m as isStringValue, n as isJsonObject } from "./src-X9phtB2j.js";
2
- import { n as callAuthoringTool, s as ReviewApiError, t as ToolText } from "./agent-client-DjXQy6CL.js";
2
+ import { n as callAuthoringTool, s as ReviewApiError, t as ToolText } from "./agent-client-BRBzv3AV.js";
3
3
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
4
4
  //#region src/review-api/public-tools.ts
5
5
  function publicDescription(text) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev.fast/whiteboard",
3
- "version": "0.3.1",
3
+ "version": "0.3.3-preview.20261018.123",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -74,11 +74,11 @@
74
74
  "tsx": "4.21.0",
75
75
  "typescript": "6.0.3",
76
76
  "vitest": "4.1.11",
77
- "@dev.fast/trace-core": "0.0.0",
77
+ "@dev.fast/json": "0.0.0",
78
78
  "@dev.fast/review-share-protocol": "0.1.2",
79
- "@dev.fast/review-protocol": "0.0.0",
79
+ "@dev.fast/trace-core": "0.0.0",
80
80
  "@dev.fast/trace-protocol": "0.5.0",
81
- "@dev.fast/json": "0.0.0"
81
+ "@dev.fast/review-protocol": "0.0.0"
82
82
  },
83
83
  "engines": {
84
84
  "node": ">=24 <25"
@@ -88,7 +88,7 @@
88
88
  "repo": "devdotfast/review",
89
89
  "docs": "packages/review/instructions/authoring.md"
90
90
  },
91
- "gitHead": "674dfbb9b0de56eb16c9ac015f2f0b2b3fa01980",
91
+ "gitHead": "5b96280747a70c90fa74e7eb2d71cccf8cf05ad7",
92
92
  "scripts": {
93
93
  "prebuild": "pnpm run build:workspace-deps",
94
94
  "build": "node --max-old-space-size=8192 node_modules/tsdown/dist/run.mjs",
@@ -17,6 +17,7 @@ import {
17
17
  toolResultText,
18
18
  } from "./agent-client.js";
19
19
  import { type ReviewApiClient, ReviewApiError } from "./client.js";
20
+ import { exitOnInstallUpdate } from "./install-update.js";
20
21
  import { callPublicTool, publicTool } from "./public-tools.js";
21
22
  import { RECOVERY } from "./recovery.js";
22
23
  import { REVIEW_AGENT_HEADER, REVIEW_VIA_HEADER } from "./request-origin.js";
@@ -70,6 +71,7 @@ export async function runReviewAgentCli(input: AgentCliInput): Promise<number> {
70
71
 
71
72
  if (mode === "mcp") {
72
73
  const { serveReviewMcp } = await import("./mcp.js");
74
+ exitOnInstallUpdate(input.stderr);
73
75
  await serveReviewMcp(
74
76
  async (key, agentKind) => {
75
77
  const connected = await connectReviewInstance(
@@ -317,6 +317,10 @@ export function createReviewApi(
317
317
  return context.json(catalog());
318
318
  });
319
319
 
320
+ app.get("/feedback-eligibility", (context) =>
321
+ context.json({ eligible: store.feedbackEligible() }),
322
+ );
323
+
320
324
  // Server-owned state only: asking the Desktop canvas would let a stalled
321
325
  // renderer block tool listing and the first instructions call.
322
326
  const instructionContext = async () => ({
@@ -0,0 +1,27 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { Writable } from "node:stream";
4
+
5
+ import { readBuildCommit } from "@review/package-paths.js";
6
+
7
+ /** Exits once an update replaces this install, or the Windows updater marks it. */
8
+ export function exitOnInstallUpdate(stderr: Writable): void {
9
+ const commit = readBuildCommit(import.meta.url);
10
+
11
+ if (!commit) return;
12
+
13
+ const marker = path.join(path.dirname(process.execPath), "applying_update");
14
+ const staleMarker = existsSync(marker);
15
+
16
+ const timer = setInterval(() => {
17
+ if (
18
+ (!staleMarker && existsSync(marker)) ||
19
+ readBuildCommit(import.meta.url) !== commit
20
+ ) {
21
+ clearInterval(timer);
22
+ stderr.write("Whiteboard was updated. Restart its MCP server.\n", () =>
23
+ process.exit(75),
24
+ );
25
+ }
26
+ }, 1000).unref();
27
+ }
@@ -1074,6 +1074,26 @@ export class ReviewStore {
1074
1074
  if (!this.has(SCRATCHPAD_ID)) throw error;
1075
1075
  }
1076
1076
  }
1077
+ /** Two saved board/day pairs in the last fourteen days. */
1078
+ feedbackEligible(now = Date.now()): boolean {
1079
+ const row = this.db
1080
+ .prepare(`SELECT count(*) AS pairs FROM (
1081
+ SELECT DISTINCT review_id, date(json_extract(snapshot, '$.createdAt')) AS utc_date
1082
+ FROM versions
1083
+ WHERE (version = 0 OR json_type(snapshot, '$.lastEdit') = 'object')
1084
+ AND json_extract(snapshot, '$.kind') IS NOT 'scratchpad'
1085
+ AND json_extract(snapshot, '$.origin.tutorial') IS NOT 1
1086
+ AND julianday(json_extract(snapshot, '$.createdAt')) BETWEEN julianday(?) AND julianday(?)
1087
+ LIMIT 2
1088
+ )`)
1089
+ .get(
1090
+ new Date(now - 14 * 24 * 60 * 60 * 1000).toISOString(),
1091
+ new Date(now).toISOString(),
1092
+ );
1093
+
1094
+ return Number(row?.pairs) >= 2;
1095
+ }
1096
+
1077
1097
  history(id: string) {
1078
1098
  return this.db
1079
1099
  .prepare(
@@ -0,0 +1,80 @@
1
+ import {
2
+ ReviewFeedbackContactSchema,
3
+ ReviewFeedbackSchema,
4
+ } from "@dev.fast/review-protocol";
5
+ import { reviewInstanceIdentity } from "@review/desktop-discovery";
6
+ import type { Hono } from "hono";
7
+
8
+ import { type ReviewHonoEnv, readBoundedRequestJson } from "./hono-http";
9
+
10
+ const FEEDBACK_URL = "https://dev.fast/api/feedback";
11
+
12
+ /** Runs behind the local API's auth middleware. No telemetry envelope is sent. */
13
+ export function mountFeedback(app: Hono<ReviewHonoEnv>) {
14
+ // Dev/Preview can exercise the form against a local receiver.
15
+ const localUrl = process.env.DEV_FAST_REVIEW_FEEDBACK_URL;
16
+
17
+ const receiver =
18
+ localUrl &&
19
+ reviewInstanceIdentity(process.env).channel !== "stable" &&
20
+ ["localhost", "127.0.0.1", "[::1]"].includes(new URL(localUrl).hostname)
21
+ ? localUrl
22
+ : FEEDBACK_URL;
23
+
24
+ for (const [path, schema] of [
25
+ ["/feedback", ReviewFeedbackSchema],
26
+ ["/feedback/contact", ReviewFeedbackContactSchema],
27
+ ] as const) {
28
+ app.post(path, async (context) => {
29
+ const input = schema.safeParse(
30
+ await readBoundedRequestJson(context.req.raw, 65_536),
31
+ );
32
+
33
+ if (!input.success)
34
+ return context.json({ ok: false, error: "Invalid feedback." }, 400);
35
+
36
+ const signal = AbortSignal.any([
37
+ context.req.raw.signal,
38
+ AbortSignal.timeout(5_000),
39
+ ]);
40
+
41
+ try {
42
+ const response = await fetch(receiver, {
43
+ method: "POST",
44
+ headers: { "content-type": "application/json" },
45
+ body: JSON.stringify(input.data),
46
+ signal,
47
+ redirect: "error",
48
+ });
49
+
50
+ const body = await response.json().catch((error) => {
51
+ if (signal.aborted || error instanceof TypeError) throw error;
52
+
53
+ return null;
54
+ });
55
+
56
+ if (!response.ok || body?.ok !== true) {
57
+ return context.json(
58
+ {
59
+ ok: false,
60
+ code: "receiver",
61
+ error: "Feedback could not be saved. Please try again.",
62
+ },
63
+ 502,
64
+ );
65
+ }
66
+
67
+ return context.json({ ok: true });
68
+ } catch {
69
+ return context.json(
70
+ {
71
+ ok: false,
72
+ code: "network",
73
+ error: "Feedback could not be saved. Please try again.",
74
+ },
75
+ 502,
76
+ );
77
+ }
78
+ });
79
+ }
80
+ }
@@ -30,6 +30,7 @@ import { streamSSE } from "hono/streaming";
30
30
  import type { ContentfulStatusCode } from "hono/utils/http-status";
31
31
  import { z } from "zod";
32
32
 
33
+ import { mountFeedback } from "./feedback";
33
34
  import type { ReviewDesktopVerbRelay } from "./global-verb-relay";
34
35
  import {
35
36
  type ReviewHonoEnv,
@@ -92,6 +93,7 @@ export function createReviewServerApp(input: {
92
93
  await next();
93
94
  });
94
95
  app.get("/control", (context) => openControlEvents(context, input.relay));
96
+ mountFeedback(app);
95
97
  app.post("/control/result", async (context) => {
96
98
  const accepted = input.relay.acceptResult(
97
99
  await readBoundedRequestJson(context.req.raw),
@@ -1,2 +0,0 @@
1
- import { n as runReviewAgentCli } from "./agent-cli-Cp7ufSvb.js";
2
- export { runReviewAgentCli };
@@ -1,2 +0,0 @@
1
- import { t as openReviewProfile } from "./profile-B58hz5n7.js";
2
- export { openReviewProfile };