@spotpatch/dev-server 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -169,6 +169,10 @@ function createAgentJobManager(options) {
169
169
  job.updatedAt = dependencies.now();
170
170
  emitSnapshot(job);
171
171
  emitPhase(job, phaseMessage);
172
+ if (!isActive(status)) {
173
+ job.lease?.release();
174
+ job.lease = void 0;
175
+ }
172
176
  };
173
177
  const probeResolved = async (selection, signal) => {
174
178
  const key = capabilityCacheKey(selection.provider, selection.model);
@@ -358,6 +362,7 @@ function createAgentJobManager(options) {
358
362
  [...jobs.values()].map((job) => job.runPromise).filter((promise) => promise !== void 0)
359
363
  );
360
364
  capabilityCache.clear();
365
+ for (const job of jobs.values()) job.lease?.release();
361
366
  jobs.clear();
362
367
  },
363
368
  create(request) {
@@ -382,8 +387,13 @@ function createAgentJobManager(options) {
382
387
  request.providerProfileId,
383
388
  request.modelProfileId
384
389
  );
390
+ const lease = options.coordinator?.acquire("change");
391
+ if (options.coordinator !== void 0 && lease === void 0) {
392
+ throw new SpotPatchError(ERROR_CODES.AGENT_BUSY);
393
+ }
385
394
  const id = dependencies.createJobId();
386
395
  if (!JOB_ID_PATTERN.test(id) || jobs.has(id)) {
396
+ lease?.release();
387
397
  throw new SpotPatchError(ERROR_CODES.INTERNAL_ERROR);
388
398
  }
389
399
  const timestamp = dependencies.now();
@@ -397,6 +407,7 @@ function createAgentJobManager(options) {
397
407
  events: [],
398
408
  id,
399
409
  listeners: /* @__PURE__ */ new Set(),
410
+ lease,
400
411
  model: selection.model,
401
412
  phaseMessage: "Agent job queued.",
402
413
  preparedChange: void 0,
@@ -439,6 +450,11 @@ function createAgentJobManager(options) {
439
450
  hasActiveJob(job.id) ? ERROR_CODES.AGENT_BUSY : ERROR_CODES.APPLY_CONFLICT
440
451
  );
441
452
  }
453
+ const lease = options.coordinator?.acquire("change");
454
+ if (options.coordinator !== void 0 && lease === void 0) {
455
+ throw new SpotPatchError(ERROR_CODES.AGENT_BUSY);
456
+ }
457
+ job.lease = lease;
442
458
  transition(job, "reverting", "Reverting the applied Agent change.");
443
459
  try {
444
460
  await dependencies.revertChange(job.preparedChange);
@@ -1249,12 +1265,12 @@ function createExternalHandoffStore(options) {
1249
1265
  }
1250
1266
  return current.snapshot;
1251
1267
  };
1252
- const findReplay = (requestId, fingerprint) => {
1268
+ const findReplay = (requestId, fingerprint2) => {
1253
1269
  requireOpen();
1254
1270
  sweep();
1255
1271
  const record = idempotency.get(requestId);
1256
1272
  if (record === void 0) return void 0;
1257
- if (record.fingerprint !== fingerprint) {
1273
+ if (record.fingerprint !== fingerprint2) {
1258
1274
  throw new SpotPatchError5(ERROR_CODES5.HANDOFF_VALIDATION_FAILED);
1259
1275
  }
1260
1276
  return replayResult(record);
@@ -1499,12 +1515,12 @@ function createExternalHandoffService(options) {
1499
1515
  },
1500
1516
  async publish(request, authorize) {
1501
1517
  requireReady();
1502
- const fingerprint = fingerprintExternalHandoffAnnotation(request.annotation);
1503
- const replayed = store.replay(request.requestId, fingerprint);
1518
+ const fingerprint2 = fingerprintExternalHandoffAnnotation(request.annotation);
1519
+ const replayed = store.replay(request.requestId, fingerprint2);
1504
1520
  if (replayed !== void 0) return replayed;
1505
1521
  const pending = inFlight.get(request.requestId);
1506
1522
  if (pending !== void 0) {
1507
- if (pending.fingerprint !== fingerprint) {
1523
+ if (pending.fingerprint !== fingerprint2) {
1508
1524
  throw new SpotPatchError6(ERROR_CODES6.HANDOFF_VALIDATION_FAILED);
1509
1525
  }
1510
1526
  return asReplay(await pending.promise);
@@ -1514,12 +1530,12 @@ function createExternalHandoffService(options) {
1514
1530
  const annotation = await authorize(request.annotation);
1515
1531
  return store.publish({
1516
1532
  annotation,
1517
- fingerprint,
1533
+ fingerprint: fingerprint2,
1518
1534
  requestId: request.requestId,
1519
1535
  reserve: activeRegistry.reserve
1520
1536
  });
1521
1537
  })();
1522
- const activePublish = Object.freeze({ fingerprint, promise });
1538
+ const activePublish = Object.freeze({ fingerprint: fingerprint2, promise });
1523
1539
  inFlight.set(request.requestId, activePublish);
1524
1540
  try {
1525
1541
  return await promise;
@@ -1840,6 +1856,8 @@ import {
1840
1856
  MAX_ANNOTATION_TARGETS,
1841
1857
  SPOTPATCH_EDITOR_PREFERENCES,
1842
1858
  SPOTPATCH_LOCALE_PREFERENCES,
1859
+ contextualAskOptionsSchema,
1860
+ CONTEXTUAL_ASK_LIMITS,
1843
1861
  DEFAULT_DATA_FLOW_LIMITS
1844
1862
  } from "@spotpatch/shared";
1845
1863
  import { z } from "zod";
@@ -1878,7 +1896,8 @@ var DEFAULT_OPTIONS = Object.freeze({
1878
1896
  runtime: "dispatch",
1879
1897
  limits: DEFAULT_DATA_FLOW_LIMITS
1880
1898
  }),
1881
- externalAgent: Object.freeze({ enabled: false })
1899
+ externalAgent: Object.freeze({ enabled: false }),
1900
+ contextualAsk: Object.freeze({ enabled: false })
1882
1901
  });
1883
1902
  var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
1884
1903
  var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
@@ -2188,6 +2207,35 @@ function resolveDataFlowOptions(options) {
2188
2207
  limits: DEFAULT_DATA_FLOW_LIMITS
2189
2208
  });
2190
2209
  }
2210
+ function resolveContextualAskOptions(value, ai) {
2211
+ if (value === void 0 || value === false) return DEFAULT_OPTIONS.contextualAsk;
2212
+ const parsed = contextualAskOptionsSchema.safeParse(value);
2213
+ if (!parsed.success) {
2214
+ throw new RangeError("SpotPatch contextualAsk configuration is invalid.");
2215
+ }
2216
+ const configured = typeof parsed.data === "boolean" ? {} : parsed.data;
2217
+ const preference = configured.defaultExecutor;
2218
+ if (preference?.kind === "configured-key") {
2219
+ const provider = ai === false ? void 0 : ai.providers[preference.providerProfileId];
2220
+ if (provider?.models[preference.modelProfileId] === void 0) {
2221
+ throw new RangeError(
2222
+ "SpotPatch contextualAsk default Key executor must reference a configured AI model."
2223
+ );
2224
+ }
2225
+ }
2226
+ if (ai !== false && Object.values(ai.providers).reduce(
2227
+ (total, provider) => total + Object.keys(provider.models).length,
2228
+ 0
2229
+ ) >= CONTEXTUAL_ASK_LIMITS.maximumExecutors) {
2230
+ throw new RangeError(
2231
+ "SpotPatch contextualAsk configuration exceeds the executor limit."
2232
+ );
2233
+ }
2234
+ return Object.freeze({
2235
+ enabled: true,
2236
+ ...preference === void 0 ? {} : { defaultExecutor: Object.freeze({ ...preference }) }
2237
+ });
2238
+ }
2191
2239
  function createRuntimeDataFlowConfig(options) {
2192
2240
  return Object.freeze({
2193
2241
  enabled: options.enabled,
@@ -2226,6 +2274,7 @@ function resolveOptions(options = {}, environmentAi) {
2226
2274
  `SpotPatch maxTargets must be an integer between 1 and ${String(MAX_ANNOTATION_TARGETS)}.`
2227
2275
  );
2228
2276
  }
2277
+ const ai = resolveAiOptions(options.ai ?? environmentAi);
2229
2278
  const resolved = {
2230
2279
  enabled: options.enabled ?? DEFAULT_OPTIONS.enabled,
2231
2280
  include: Object.freeze([...options.include ?? DEFAULT_OPTIONS.include]),
@@ -2238,11 +2287,12 @@ function resolveOptions(options = {}, environmentAi) {
2238
2287
  debug: options.debug ?? DEFAULT_OPTIONS.debug,
2239
2288
  locale,
2240
2289
  maxTargets,
2241
- ai: resolveAiOptions(options.ai ?? environmentAi),
2290
+ ai,
2242
2291
  dataFlow: resolveDataFlowOptions(options.dataFlow),
2243
2292
  externalAgent: Object.freeze({
2244
2293
  enabled: options.externalAgent ?? DEFAULT_OPTIONS.externalAgent.enabled
2245
- })
2294
+ }),
2295
+ contextualAsk: resolveContextualAskOptions(options.contextualAsk, ai)
2246
2296
  };
2247
2297
  if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
2248
2298
  throw new RangeError("SpotPatch shortcut is invalid.");
@@ -2478,10 +2528,14 @@ function createSourceRegistry(options = {}) {
2478
2528
  );
2479
2529
  }
2480
2530
  componentIdsByPath.set(normalizedPath, currentIds);
2531
+ return fileId;
2481
2532
  },
2482
2533
  resolve(fileId) {
2483
2534
  return idToPath.get(fileId);
2484
2535
  },
2536
+ findRegisteredId(absolutePath) {
2537
+ return pathToId.get(normalizeAbsolutePath(absolutePath));
2538
+ },
2485
2539
  resolveDataFlowComponent(componentSourceId) {
2486
2540
  return componentAnchors.get(componentSourceId);
2487
2541
  },
@@ -2494,21 +2548,1342 @@ function createSourceRegistry(options = {}) {
2494
2548
  });
2495
2549
  }
2496
2550
 
2551
+ // src/contextual-ask/manager.ts
2552
+ import { createHash as createHash4, randomBytes as randomBytes9 } from "crypto";
2553
+ import {
2554
+ askAnswerDraftSchema,
2555
+ askAnswerResultSchema,
2556
+ askJobCreateRequestSchema,
2557
+ askJobEventSchema,
2558
+ askJobResultResponseSchema,
2559
+ askJobSnapshotSchema,
2560
+ contextualAskCapabilitySchema,
2561
+ contextualAskExecutorCapabilitySchema,
2562
+ CONTEXTUAL_ASK_LIMITS as CONTEXTUAL_ASK_LIMITS3,
2563
+ CONTEXTUAL_ASK_SCHEMA_VERSION
2564
+ } from "@spotpatch/shared";
2565
+
2566
+ // src/contextual-ask/error.ts
2567
+ import { ContextualAskExecutorError } from "@spotpatch/agent";
2568
+ var ContextualAskError = class extends Error {
2569
+ code;
2570
+ constructor(code, options = {}) {
2571
+ super(code, options.cause === void 0 ? void 0 : { cause: options.cause });
2572
+ this.name = "ContextualAskError";
2573
+ this.code = code;
2574
+ }
2575
+ };
2576
+ function asContextualAskError(error) {
2577
+ if (error instanceof ContextualAskError) return error;
2578
+ if (error instanceof ContextualAskExecutorError) {
2579
+ return new ContextualAskError(error.code, { cause: error });
2580
+ }
2581
+ return new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", { cause: error });
2582
+ }
2583
+
2584
+ // src/contextual-ask/read-snapshot.ts
2585
+ import { createHash as createHash3, randomBytes as randomBytes8 } from "crypto";
2586
+ import { constants } from "fs";
2587
+ import { lstat as lstat4, open as open2, realpath as realpath3 } from "fs/promises";
2588
+ import path5 from "path";
2589
+ import {
2590
+ CONTEXTUAL_ASK_LIMITS as CONTEXTUAL_ASK_LIMITS2
2591
+ } from "@spotpatch/shared";
2592
+ import { parseSync, Visitor } from "oxc-parser";
2593
+ var SOURCE_EXTENSIONS = Object.freeze([
2594
+ ".ts",
2595
+ ".tsx",
2596
+ ".js",
2597
+ ".jsx",
2598
+ ".mjs",
2599
+ ".cjs",
2600
+ ".css",
2601
+ ".scss",
2602
+ ".sass",
2603
+ ".less"
2604
+ ]);
2605
+ var PROTECTED_DIRECTORIES = /* @__PURE__ */ new Set([
2606
+ ".git",
2607
+ ".ssh",
2608
+ ".spotpatch",
2609
+ "coverage",
2610
+ "dist",
2611
+ "node_modules"
2612
+ ]);
2613
+ var PROTECTED_FILES = /* @__PURE__ */ new Set([
2614
+ ".gitmodules",
2615
+ ".git-credentials",
2616
+ ".netrc",
2617
+ ".npmrc",
2618
+ ".pnpmrc",
2619
+ ".pypirc",
2620
+ ".yarnrc",
2621
+ "bun.lock",
2622
+ "bun.lockb",
2623
+ "npm-shrinkwrap.json",
2624
+ "package-lock.json",
2625
+ "pnpm-lock.yaml",
2626
+ "yarn.lock"
2627
+ ]);
2628
+ var decoder = new TextDecoder("utf-8", { fatal: true });
2629
+ function deny(code) {
2630
+ throw new ContextualAskError(code);
2631
+ }
2632
+ function toPosixRelative(realRoot, absolutePath) {
2633
+ const relative = path5.relative(realRoot, absolutePath);
2634
+ if (relative.length === 0 || relative === ".." || relative.startsWith(`..${path5.sep}`) || path5.isAbsolute(relative)) {
2635
+ return deny("ASK_SOURCE_SCOPE_DENIED");
2636
+ }
2637
+ const normalized = relative.split(path5.sep).join("/");
2638
+ const segments = normalized.toLowerCase().split("/");
2639
+ const fileName = segments.at(-1) ?? "";
2640
+ if (segments.some((segment) => PROTECTED_DIRECTORIES.has(segment)) || PROTECTED_FILES.has(fileName) || fileName === ".env" || fileName === ".envrc" || fileName === ".dev.vars" || fileName.startsWith(".env.") || fileName.endsWith(".pem") || fileName.endsWith(".key") || fileName.endsWith(".p12") || fileName.endsWith(".pfx") || fileName.startsWith("id_rsa")) {
2641
+ return deny("ASK_SOURCE_SCOPE_DENIED");
2642
+ }
2643
+ return normalized;
2644
+ }
2645
+ async function assertNoSymlinkPath(realRoot, absolutePath) {
2646
+ const relative = toPosixRelative(realRoot, absolutePath);
2647
+ let current = realRoot;
2648
+ for (const segment of relative.split("/")) {
2649
+ current = path5.join(current, segment);
2650
+ const metadata = await lstat4(current).catch(() => void 0);
2651
+ if (metadata === void 0 || metadata.isSymbolicLink()) {
2652
+ deny("ASK_SOURCE_SCOPE_DENIED");
2653
+ }
2654
+ }
2655
+ }
2656
+ async function readSecureSource(realRoot, absolutePath) {
2657
+ const normalizedAbsolute = path5.normalize(path5.resolve(absolutePath));
2658
+ const relativePath = toPosixRelative(realRoot, normalizedAbsolute);
2659
+ await assertNoSymlinkPath(realRoot, normalizedAbsolute);
2660
+ const canonical = await realpath3(normalizedAbsolute).catch(() => void 0);
2661
+ if (canonical === void 0 || canonical !== normalizedAbsolute) {
2662
+ return deny("ASK_SOURCE_SCOPE_DENIED");
2663
+ }
2664
+ let handle;
2665
+ try {
2666
+ handle = await open2(normalizedAbsolute, constants.O_RDONLY | constants.O_NOFOLLOW);
2667
+ const before = await handle.stat();
2668
+ if (!before.isFile() || before.nlink !== 1) {
2669
+ return deny("ASK_SOURCE_SCOPE_DENIED");
2670
+ }
2671
+ if (before.size > CONTEXTUAL_ASK_LIMITS2.maximumReadBytesPerFile) {
2672
+ throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
2673
+ }
2674
+ const bytes = await handle.readFile();
2675
+ const after = await handle.stat();
2676
+ if (bytes.byteLength !== before.size || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || bytes.includes(0)) {
2677
+ return deny("ASK_SOURCE_SCOPE_DENIED");
2678
+ }
2679
+ let content;
2680
+ try {
2681
+ content = decoder.decode(bytes);
2682
+ } catch (error) {
2683
+ throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED", { cause: error });
2684
+ }
2685
+ return Object.freeze({
2686
+ content,
2687
+ contentHash: createHash3("sha256").update(bytes).digest("hex"),
2688
+ relativePath,
2689
+ size: bytes.byteLength
2690
+ });
2691
+ } finally {
2692
+ await handle?.close().catch(() => void 0);
2693
+ }
2694
+ }
2695
+ function confidence(value) {
2696
+ return value === "exact" || value === "probable" ? value : "approximate";
2697
+ }
2698
+ async function authorizeSeeds(selection, registry, root, realRoot) {
2699
+ const byPath = /* @__PURE__ */ new Map();
2700
+ for (const target of selection.targets) {
2701
+ let fileId = target.react.source?.fileId ?? target.source.fileId;
2702
+ let sourceVersion;
2703
+ if (target.react.componentSourceId !== void 0) {
2704
+ const anchor = registry.resolveDataFlowComponent(target.react.componentSourceId);
2705
+ if (anchor === void 0 || target.react.sourceVersion !== anchor.sourceVersion) {
2706
+ deny("ASK_SELECTION_STALE");
2707
+ }
2708
+ fileId = anchor.fileId;
2709
+ sourceVersion = anchor.sourceVersion;
2710
+ }
2711
+ if (fileId === void 0) continue;
2712
+ const registered = registry.resolve(fileId);
2713
+ if (registered === void 0) deny("ASK_SELECTION_STALE");
2714
+ const canonical = await realpath3(registered).catch(() => void 0);
2715
+ if (canonical === void 0) deny("ASK_SELECTION_STALE");
2716
+ const absolutePath = path5.normalize(canonical);
2717
+ const registeredRelative = toPosixRelative(realRoot, absolutePath);
2718
+ const claimedRelative = target.react.source?.relativePath ?? target.source.relativePath;
2719
+ if (claimedRelative !== void 0 && claimedRelative.split("\\").join("/") !== registeredRelative) {
2720
+ deny("ASK_SELECTION_STALE");
2721
+ }
2722
+ const existing = byPath.get(absolutePath);
2723
+ const targetIds = Object.freeze([
2724
+ ...existing?.targetIds ?? [],
2725
+ ...existing?.targetIds.includes(target.targetId) === true ? [] : [target.targetId]
2726
+ ]);
2727
+ byPath.set(
2728
+ absolutePath,
2729
+ Object.freeze({
2730
+ absolutePath,
2731
+ fileId,
2732
+ confidence: confidence(
2733
+ target.react.source?.confidence ?? target.source.confidence
2734
+ ),
2735
+ targetIds,
2736
+ ...sourceVersion === void 0 ? {} : { sourceVersion }
2737
+ })
2738
+ );
2739
+ }
2740
+ for (const target of selection.targets) {
2741
+ for (const rule of target.styles.matchedRules) {
2742
+ const claimed = rule.source;
2743
+ if (claimed === void 0 || claimed.startsWith("/") || claimed.includes("\\") || claimed.includes("\0") || claimed.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
2744
+ continue;
2745
+ }
2746
+ const registeredPath = path5.resolve(root, ...claimed.split("/"));
2747
+ const fileId = registry.findRegisteredId(registeredPath);
2748
+ if (fileId === void 0) continue;
2749
+ const canonical = await realpath3(registeredPath).catch(() => void 0);
2750
+ if (canonical === void 0) continue;
2751
+ const absolutePath = path5.normalize(canonical);
2752
+ toPosixRelative(realRoot, absolutePath);
2753
+ const existing = byPath.get(absolutePath);
2754
+ const targetIds = Object.freeze([
2755
+ ...existing?.targetIds ?? [],
2756
+ ...existing?.targetIds.includes(target.targetId) === true ? [] : [target.targetId]
2757
+ ]);
2758
+ byPath.set(
2759
+ absolutePath,
2760
+ Object.freeze({
2761
+ absolutePath,
2762
+ fileId,
2763
+ confidence: existing?.confidence ?? "probable",
2764
+ targetIds,
2765
+ ...existing?.sourceVersion === void 0 ? {} : { sourceVersion: existing.sourceVersion }
2766
+ })
2767
+ );
2768
+ }
2769
+ }
2770
+ if (byPath.size === 0) deny("ASK_SELECTION_REQUIRED");
2771
+ return [...byPath.values()];
2772
+ }
2773
+ function localSpecifiers(filePath, content) {
2774
+ const found = /* @__PURE__ */ new Set();
2775
+ if (/\.(?:css|scss|sass|less)$/iu.test(filePath)) {
2776
+ const pattern = /@import\s+(?:url\(\s*)?["']([^"']+)["']/gu;
2777
+ for (const match of content.matchAll(pattern)) {
2778
+ if (match[1]?.startsWith(".")) found.add(match[1]);
2779
+ }
2780
+ return [...found].sort();
2781
+ }
2782
+ try {
2783
+ const parsed = parseSync(filePath, content, { sourceType: "module" });
2784
+ if (parsed.errors.length > 0) return [];
2785
+ const add = (value) => {
2786
+ if (typeof value === "string" && value.startsWith(".")) found.add(value);
2787
+ };
2788
+ const visitor = new Visitor({
2789
+ ImportDeclaration(node) {
2790
+ add(node.source.value);
2791
+ },
2792
+ ExportNamedDeclaration(node) {
2793
+ add(node.source?.value);
2794
+ },
2795
+ ExportAllDeclaration(node) {
2796
+ add(node.source.value);
2797
+ },
2798
+ ImportExpression(node) {
2799
+ if ("value" in node.source) add(node.source.value);
2800
+ }
2801
+ });
2802
+ visitor.visit(parsed.program);
2803
+ } catch {
2804
+ return [];
2805
+ }
2806
+ return [...found].sort();
2807
+ }
2808
+ function importCandidates(importer, specifier) {
2809
+ const base = path5.resolve(path5.dirname(importer), specifier);
2810
+ if (path5.extname(base) !== "") return Object.freeze([base]);
2811
+ return Object.freeze([
2812
+ ...SOURCE_EXTENSIONS.map((extension) => `${base}${extension}`),
2813
+ ...SOURCE_EXTENSIONS.map((extension) => path5.join(base, `index${extension}`))
2814
+ ]);
2815
+ }
2816
+ function countLines(content) {
2817
+ return content.length === 0 ? 1 : content.split("\n").length;
2818
+ }
2819
+ function publicSourceEntry(source) {
2820
+ return Object.freeze({
2821
+ handleId: source.handleId,
2822
+ fileId: source.fileId,
2823
+ relativePath: source.relativePath,
2824
+ label: source.label,
2825
+ lineCount: source.lineCount,
2826
+ size: source.size,
2827
+ contentHash: source.contentHash,
2828
+ confidence: source.confidence,
2829
+ targetIds: source.targetIds,
2830
+ ...source.sourceVersion === void 0 ? {} : { sourceVersion: source.sourceVersion }
2831
+ });
2832
+ }
2833
+ function assertSelectionEvidenceCurrent(selection, captured) {
2834
+ for (const target of selection.targets) {
2835
+ if (target.code === void 0) continue;
2836
+ const source = captured.find(
2837
+ (candidate) => candidate.relativePath === target.code?.relativePath
2838
+ );
2839
+ if (source === void 0) deny("ASK_SELECTION_STALE");
2840
+ const lines = source.content.split(/\r?\n/u);
2841
+ if (target.code.endLine > lines.length) deny("ASK_SELECTION_STALE");
2842
+ const currentExcerpt = lines.slice(target.code.startLine - 1, target.code.endLine).join("\n");
2843
+ if (currentExcerpt !== target.code.excerpt) deny("ASK_SELECTION_STALE");
2844
+ }
2845
+ }
2846
+ async function captureAskReadSnapshot(options) {
2847
+ options.signal?.throwIfAborted();
2848
+ const realRoot = await realpath3(options.root).catch(() => void 0);
2849
+ if (realRoot === void 0) deny("ASK_SOURCE_SCOPE_DENIED");
2850
+ const seeds = await authorizeSeeds(
2851
+ options.selection,
2852
+ options.registry,
2853
+ options.root,
2854
+ realRoot
2855
+ );
2856
+ const createHandleId = options.createHandleId ?? (() => randomBytes8(16).toString("base64url"));
2857
+ const captured = [];
2858
+ const capturedPaths = /* @__PURE__ */ new Set();
2859
+ const queue = [...seeds];
2860
+ let totalBytes = 0;
2861
+ let truncated = false;
2862
+ while (queue.length > 0) {
2863
+ options.signal?.throwIfAborted();
2864
+ const candidate = queue.shift();
2865
+ if (candidate === void 0 || capturedPaths.has(candidate.absolutePath)) continue;
2866
+ if (captured.length >= CONTEXTUAL_ASK_LIMITS2.maximumReadFiles) {
2867
+ truncated = true;
2868
+ break;
2869
+ }
2870
+ let read;
2871
+ try {
2872
+ read = await readSecureSource(realRoot, candidate.absolutePath);
2873
+ options.signal?.throwIfAborted();
2874
+ } catch (error) {
2875
+ if (captured.length < seeds.length) throw error;
2876
+ truncated = true;
2877
+ continue;
2878
+ }
2879
+ if (totalBytes + read.size > CONTEXTUAL_ASK_LIMITS2.maximumReadBytesTotal) {
2880
+ if (captured.length < seeds.length)
2881
+ throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
2882
+ truncated = true;
2883
+ continue;
2884
+ }
2885
+ capturedPaths.add(candidate.absolutePath);
2886
+ totalBytes += read.size;
2887
+ const entry = Object.freeze({
2888
+ handleId: createHandleId(),
2889
+ fileId: candidate.fileId,
2890
+ relativePath: read.relativePath,
2891
+ label: path5.basename(read.relativePath),
2892
+ lineCount: countLines(read.content),
2893
+ size: read.size,
2894
+ contentHash: read.contentHash,
2895
+ confidence: candidate.confidence,
2896
+ targetIds: Object.freeze([...candidate.targetIds]),
2897
+ ...candidate.sourceVersion === void 0 ? {} : { sourceVersion: candidate.sourceVersion },
2898
+ absolutePath: candidate.absolutePath,
2899
+ content: read.content
2900
+ });
2901
+ captured.push(entry);
2902
+ for (const specifier of localSpecifiers(candidate.absolutePath, read.content)) {
2903
+ options.signal?.throwIfAborted();
2904
+ for (const importPath of importCandidates(candidate.absolutePath, specifier)) {
2905
+ if (capturedPaths.has(importPath)) continue;
2906
+ const metadata = await lstat4(importPath).catch(() => void 0);
2907
+ if (metadata?.isFile() !== true || metadata.isSymbolicLink()) continue;
2908
+ const fileId = options.registry.findRegisteredId(importPath) ?? options.registry.register(importPath);
2909
+ queue.push(
2910
+ Object.freeze({
2911
+ absolutePath: path5.normalize(importPath),
2912
+ fileId,
2913
+ confidence: "probable",
2914
+ targetIds: candidate.targetIds
2915
+ })
2916
+ );
2917
+ break;
2918
+ }
2919
+ }
2920
+ }
2921
+ assertSelectionEvidenceCurrent(options.selection, captured);
2922
+ const sourceByHandle = new Map(captured.map((source) => [source.handleId, source]));
2923
+ const publicEntries = Object.freeze(
2924
+ captured.map((source) => publicSourceEntry(source))
2925
+ );
2926
+ const contextHash = createHash3("sha256").update(
2927
+ JSON.stringify({
2928
+ selectionId: options.selection.selectionId,
2929
+ sources: publicEntries.map((source) => ({
2930
+ relativePath: source.relativePath,
2931
+ contentHash: source.contentHash,
2932
+ targetIds: source.targetIds,
2933
+ sourceVersion: source.sourceVersion ?? null
2934
+ }))
2935
+ })
2936
+ ).digest("hex");
2937
+ let disposed = false;
2938
+ const requireSource = (handleId) => {
2939
+ if (disposed) throw new ContextualAskError("ASK_RESULT_EXPIRED");
2940
+ const source = sourceByHandle.get(handleId);
2941
+ if (source === void 0) throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED");
2942
+ return source;
2943
+ };
2944
+ const snapshot2 = Object.freeze({
2945
+ manifest: () => {
2946
+ if (disposed) throw new ContextualAskError("ASK_RESULT_EXPIRED");
2947
+ return publicEntries;
2948
+ },
2949
+ read(handleId, range = {}) {
2950
+ const source = requireSource(handleId);
2951
+ const startLine = range.startLine ?? 1;
2952
+ const endLine = range.endLine ?? source.lineCount;
2953
+ if (!Number.isSafeInteger(startLine) || !Number.isSafeInteger(endLine) || startLine < 1 || endLine < startLine || endLine > source.lineCount) {
2954
+ throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED");
2955
+ }
2956
+ return Object.freeze({
2957
+ handleId,
2958
+ startLine,
2959
+ endLine,
2960
+ content: source.content.split("\n").slice(startLine - 1, endLine).join("\n")
2961
+ });
2962
+ },
2963
+ search(query) {
2964
+ if (query.length === 0 || query.length > CONTEXTUAL_ASK_LIMITS2.maximumSearchQueryCharacters || query.includes("\0")) {
2965
+ throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED");
2966
+ }
2967
+ const normalized = query.toLocaleLowerCase("en-US");
2968
+ const matches = [];
2969
+ for (const source of captured) {
2970
+ for (const [index, line] of source.content.split("\n").entries()) {
2971
+ if (line.toLocaleLowerCase("en-US").includes(normalized)) {
2972
+ matches.push(
2973
+ Object.freeze({
2974
+ handleId: source.handleId,
2975
+ line: index + 1,
2976
+ preview: line.slice(
2977
+ 0,
2978
+ CONTEXTUAL_ASK_LIMITS2.maximumSearchPreviewCharacters
2979
+ )
2980
+ })
2981
+ );
2982
+ if (matches.length >= CONTEXTUAL_ASK_LIMITS2.maximumSearchResults) {
2983
+ return Object.freeze(matches);
2984
+ }
2985
+ }
2986
+ }
2987
+ }
2988
+ return Object.freeze(matches);
2989
+ }
2990
+ });
2991
+ return Object.freeze({
2992
+ grant: Object.freeze({ contextHash, truncated, sources: publicEntries }),
2993
+ snapshot: snapshot2,
2994
+ dispose() {
2995
+ disposed = true;
2996
+ sourceByHandle.clear();
2997
+ captured.splice(0, captured.length);
2998
+ },
2999
+ async isStale() {
3000
+ for (const source of captured) {
3001
+ try {
3002
+ const current = await readSecureSource(realRoot, source.absolutePath);
3003
+ if (current.contentHash !== source.contentHash) return true;
3004
+ } catch {
3005
+ return true;
3006
+ }
3007
+ }
3008
+ return false;
3009
+ },
3010
+ projectCitation(handleId, startLine, endLine) {
3011
+ const source = requireSource(handleId);
3012
+ if (!Number.isSafeInteger(startLine) || !Number.isSafeInteger(endLine) || startLine < 1 || endLine < startLine || endLine > source.lineCount) {
3013
+ throw new ContextualAskError("ASK_ANSWER_INVALID");
3014
+ }
3015
+ return Object.freeze({ ...publicSourceEntry(source), startLine, endLine });
3016
+ }
3017
+ });
3018
+ }
3019
+
3020
+ // src/contextual-ask/manager.ts
3021
+ var ACTIVE_STATUSES = /* @__PURE__ */ new Set([
3022
+ "queued",
3023
+ "authorizing",
3024
+ "running",
3025
+ "cancelling"
3026
+ ]);
3027
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["answered", "cancelled", "failed"]);
3028
+ var DEFAULT_DEPENDENCIES2 = Object.freeze({
3029
+ captureSnapshot: captureAskReadSnapshot,
3030
+ createId: () => randomBytes9(16).toString("base64url"),
3031
+ now: () => /* @__PURE__ */ new Date()
3032
+ });
3033
+ function fingerprint(request) {
3034
+ return createHash4("sha256").update(
3035
+ JSON.stringify({
3036
+ envelope: request.envelope,
3037
+ executorId: request.executorId,
3038
+ providerDataConsent: request.providerDataConsent
3039
+ })
3040
+ ).digest("hex");
3041
+ }
3042
+ function fileCountBucket(count) {
3043
+ if (count <= 1) return "one";
3044
+ if (count <= 5) return "few";
3045
+ return "many";
3046
+ }
3047
+ function raceWithAbort(operation, signal) {
3048
+ const reason = () => signal.reason instanceof Error ? signal.reason : new ContextualAskError("ASK_CANCELLED");
3049
+ if (signal.aborted) return Promise.reject(reason());
3050
+ return new Promise((resolve, reject) => {
3051
+ const onAbort = () => {
3052
+ reject(reason());
3053
+ };
3054
+ signal.addEventListener("abort", onAbort, { once: true });
3055
+ operation.then(
3056
+ (value) => {
3057
+ signal.removeEventListener("abort", onAbort);
3058
+ resolve(value);
3059
+ },
3060
+ (error) => {
3061
+ signal.removeEventListener("abort", onAbort);
3062
+ reject(
3063
+ error instanceof Error ? error : new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", { cause: error })
3064
+ );
3065
+ }
3066
+ );
3067
+ });
3068
+ }
3069
+ function createContextualAskManager(options) {
3070
+ const dependencies = Object.freeze({
3071
+ ...DEFAULT_DEPENDENCIES2,
3072
+ ...options.dependencies
3073
+ });
3074
+ const executors = /* @__PURE__ */ new Map();
3075
+ for (const executor of options.executors ?? []) {
3076
+ if (executors.has(executor.executorId)) {
3077
+ throw new TypeError("Contextual Ask executor IDs must be unique.");
3078
+ }
3079
+ executors.set(executor.executorId, executor);
3080
+ }
3081
+ const jobs = /* @__PURE__ */ new Map();
3082
+ const requests = /* @__PURE__ */ new Map();
3083
+ const pendingRequests = /* @__PURE__ */ new Map();
3084
+ let closed = false;
3085
+ const isClosed = () => closed;
3086
+ const cleanupExpired = () => {
3087
+ const now = dependencies.now().getTime();
3088
+ for (const [jobId, job] of jobs) {
3089
+ if (job.expiresAt !== void 0 && now >= job.expiresAt) {
3090
+ job.snapshot?.dispose();
3091
+ jobs.delete(jobId);
3092
+ const request = requests.get(job.request.requestId);
3093
+ if (request?.jobId === jobId) requests.delete(job.request.requestId);
3094
+ }
3095
+ }
3096
+ };
3097
+ const requireJob = (jobId) => {
3098
+ cleanupExpired();
3099
+ const job = jobs.get(jobId);
3100
+ if (job === void 0) throw new ContextualAskError("ASK_RESULT_EXPIRED");
3101
+ return job;
3102
+ };
3103
+ const publicSnapshot = (job) => askJobSnapshotSchema.parse({
3104
+ schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
3105
+ jobId: job.id,
3106
+ selectionId: job.request.envelope.selection.selectionId,
3107
+ status: job.status,
3108
+ executor: {
3109
+ executorId: job.executorCapability.executorId,
3110
+ kind: job.executorCapability.kind,
3111
+ label: job.executorCapability.label,
3112
+ modelLabel: job.executorCapability.effectiveModelLabel
3113
+ },
3114
+ ...job.phaseMessage === void 0 ? {} : { phaseMessage: job.phaseMessage },
3115
+ createdAt: job.createdAt,
3116
+ updatedAt: job.updatedAt,
3117
+ canCancel: ["queued", "authorizing", "running"].includes(job.status),
3118
+ ...job.errorCode === void 0 ? {} : { errorCode: job.errorCode }
3119
+ });
3120
+ const appendEvent = (job, event) => {
3121
+ job.events.push(askJobEventSchema.parse(event));
3122
+ if (job.events.length > CONTEXTUAL_ASK_LIMITS3.maximumRetainedEvents) {
3123
+ job.events.splice(
3124
+ 0,
3125
+ job.events.length - CONTEXTUAL_ASK_LIMITS3.maximumRetainedEvents
3126
+ );
3127
+ }
3128
+ for (const listener of job.listeners) listener(event);
3129
+ };
3130
+ const eventBase = (job) => {
3131
+ job.sequence += 1;
3132
+ return {
3133
+ schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
3134
+ sequence: job.sequence,
3135
+ jobId: job.id,
3136
+ status: job.status,
3137
+ timestamp: dependencies.now().toISOString()
3138
+ };
3139
+ };
3140
+ const emitSnapshot = (job) => {
3141
+ appendEvent(job, {
3142
+ ...eventBase(job),
3143
+ type: "snapshot",
3144
+ snapshot: publicSnapshot(job)
3145
+ });
3146
+ };
3147
+ const emitPhase = (job, message) => {
3148
+ appendEvent(job, { ...eventBase(job), type: "phase", message });
3149
+ };
3150
+ const emitReadActivity = (job, entry, state) => {
3151
+ appendEvent(job, {
3152
+ ...eventBase(job),
3153
+ type: "read-activity",
3154
+ activity: entry === void 0 ? {
3155
+ kind: "file-count",
3156
+ bucket: fileCountBucket(job.snapshot?.grant.sources.length ?? 0)
3157
+ } : {
3158
+ kind: "source",
3159
+ sourceId: entry.fileId,
3160
+ relativePath: entry.relativePath
3161
+ },
3162
+ state
3163
+ });
3164
+ };
3165
+ const transition = (job, status, phaseMessage, errorCode) => {
3166
+ job.status = status;
3167
+ job.phaseMessage = phaseMessage;
3168
+ job.errorCode = errorCode;
3169
+ job.updatedAt = dependencies.now().toISOString();
3170
+ emitSnapshot(job);
3171
+ if (!TERMINAL_STATUSES.has(status)) emitPhase(job, phaseMessage);
3172
+ };
3173
+ const finish = (job) => {
3174
+ job.expiresAt = dependencies.now().getTime() + CONTEXTUAL_ASK_LIMITS3.resultTtlMs;
3175
+ job.lease.release();
3176
+ job.snapshot?.dispose();
3177
+ job.snapshot = void 0;
3178
+ };
3179
+ const fail = (job, error) => {
3180
+ if (TERMINAL_STATUSES.has(job.status)) return;
3181
+ const normalized = asContextualAskError(error);
3182
+ const abortReason = job.controller.signal.reason;
3183
+ const cancelled = normalized.code === "ASK_CANCELLED" || abortReason instanceof ContextualAskError && abortReason.code === "ASK_CANCELLED";
3184
+ transition(
3185
+ job,
3186
+ cancelled ? "cancelled" : "failed",
3187
+ cancelled ? "Question cancelled." : "Question failed.",
3188
+ cancelled ? "ASK_CANCELLED" : normalized.code
3189
+ );
3190
+ appendEvent(job, {
3191
+ ...eventBase(job),
3192
+ type: "error",
3193
+ errorCode: cancelled ? "ASK_CANCELLED" : normalized.code
3194
+ });
3195
+ finish(job);
3196
+ };
3197
+ const instrumentSnapshot = (job, snapshot2, onLimitExceeded) => {
3198
+ const byHandle = new Map(
3199
+ snapshot2.manifest().map((entry) => [entry.handleId, entry])
3200
+ );
3201
+ let readOperations = 0;
3202
+ const beginOperation = () => {
3203
+ readOperations += 1;
3204
+ if (readOperations > CONTEXTUAL_ASK_LIMITS3.maximumToolCalls) {
3205
+ onLimitExceeded();
3206
+ throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
3207
+ }
3208
+ };
3209
+ return Object.freeze({
3210
+ manifest: () => snapshot2.manifest(),
3211
+ read(handleId, range) {
3212
+ beginOperation();
3213
+ const entry = byHandle.get(handleId);
3214
+ if (job.status === "running") emitReadActivity(job, entry, "started");
3215
+ try {
3216
+ const result = snapshot2.read(handleId, range);
3217
+ if (job.status === "running") emitReadActivity(job, entry, "succeeded");
3218
+ return result;
3219
+ } catch (error) {
3220
+ if (job.status === "running") emitReadActivity(job, entry, "failed");
3221
+ throw error;
3222
+ }
3223
+ },
3224
+ search(query) {
3225
+ beginOperation();
3226
+ if (job.status === "running") emitReadActivity(job, void 0, "started");
3227
+ try {
3228
+ const result = snapshot2.search(query);
3229
+ if (job.status === "running") {
3230
+ emitReadActivity(job, void 0, "succeeded");
3231
+ }
3232
+ return result;
3233
+ } catch (error) {
3234
+ if (job.status === "running") emitReadActivity(job, void 0, "failed");
3235
+ throw error;
3236
+ }
3237
+ }
3238
+ });
3239
+ };
3240
+ const projectAnswer = async (job, draft, captured) => {
3241
+ const sourceIds = /* @__PURE__ */ new Map();
3242
+ const sources = /* @__PURE__ */ new Map();
3243
+ const projectCitations = (citations) => citations.map((citation) => {
3244
+ const key = `${citation.handleId}:${String(citation.startLine)}:${String(citation.endLine)}`;
3245
+ let sourceId = sourceIds.get(key);
3246
+ if (sourceId === void 0) {
3247
+ sourceId = dependencies.createId();
3248
+ sourceIds.set(key, sourceId);
3249
+ sources.set(
3250
+ sourceId,
3251
+ captured.projectCitation(
3252
+ citation.handleId,
3253
+ citation.startLine,
3254
+ citation.endLine
3255
+ )
3256
+ );
3257
+ }
3258
+ return sourceId;
3259
+ });
3260
+ const blocks = draft.blocks.map((block) => {
3261
+ if (block.kind === "list") {
3262
+ return {
3263
+ kind: "list",
3264
+ items: block.items.map((item) => ({
3265
+ text: item.text,
3266
+ sourceIds: projectCitations(item.citations)
3267
+ }))
3268
+ };
3269
+ }
3270
+ return Object.freeze({
3271
+ kind: block.kind,
3272
+ ...block.kind === "paragraph" ? { text: block.text } : { code: block.code },
3273
+ ...block.kind === "code" && block.language !== void 0 ? { language: block.language } : {},
3274
+ sourceIds: projectCitations(block.citations)
3275
+ });
3276
+ });
3277
+ const warnings = [...draft.warnings];
3278
+ if (captured.grant.truncated && !warnings.some((warning) => warning.code === "source-truncated")) {
3279
+ warnings.push(Object.freeze({ code: "source-truncated" }));
3280
+ }
3281
+ if (await captured.isStale() && !warnings.some((warning) => warning.code === "source-stale")) {
3282
+ warnings.push(Object.freeze({ code: "source-stale" }));
3283
+ }
3284
+ const created = dependencies.now();
3285
+ return askAnswerResultSchema.parse({
3286
+ schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
3287
+ jobId: job.id,
3288
+ selectionId: job.request.envelope.selection.selectionId,
3289
+ contextHash: captured.grant.contextHash,
3290
+ executor: {
3291
+ executorId: job.executorCapability.executorId,
3292
+ kind: job.executorCapability.kind,
3293
+ label: job.executorCapability.label,
3294
+ modelLabel: job.executorCapability.effectiveModelLabel
3295
+ },
3296
+ blocks,
3297
+ sources: [...sources].map(([sourceId, source]) => ({
3298
+ sourceId,
3299
+ label: source.label,
3300
+ relativePath: source.relativePath,
3301
+ fileId: source.fileId,
3302
+ startLine: source.startLine,
3303
+ endLine: source.endLine,
3304
+ confidence: source.confidence,
3305
+ targetIds: source.targetIds,
3306
+ ...source.sourceVersion === void 0 ? {} : { sourceVersion: source.sourceVersion },
3307
+ contentHash: source.contentHash
3308
+ })),
3309
+ warnings,
3310
+ createdAt: created.toISOString(),
3311
+ expiresAt: new Date(
3312
+ created.getTime() + CONTEXTUAL_ASK_LIMITS3.resultTtlMs
3313
+ ).toISOString()
3314
+ });
3315
+ };
3316
+ const run = async (job) => {
3317
+ const timeout = setTimeout(() => {
3318
+ job.controller.abort(new ContextualAskError("ASK_TIMEOUT"));
3319
+ }, CONTEXTUAL_ASK_LIMITS3.jobTimeoutMs);
3320
+ timeout.unref();
3321
+ try {
3322
+ transition(job, "authorizing", "Authorizing selected sources.");
3323
+ const captured = await dependencies.captureSnapshot({
3324
+ root: options.root,
3325
+ registry: options.registry,
3326
+ selection: job.request.envelope.selection,
3327
+ signal: job.controller.signal,
3328
+ createHandleId: dependencies.createId
3329
+ });
3330
+ if (job.controller.signal.aborted) {
3331
+ captured.dispose();
3332
+ }
3333
+ job.controller.signal.throwIfAborted();
3334
+ job.snapshot = captured;
3335
+ transition(job, "running", "Analyzing the authorized snapshot.");
3336
+ emitReadActivity(job, void 0, "succeeded");
3337
+ const readLimit = { exceeded: false };
3338
+ const execution = await raceWithAbort(
3339
+ job.executor.execute(
3340
+ {
3341
+ jobId: job.id,
3342
+ envelope: job.request.envelope,
3343
+ grant: captured.grant,
3344
+ snapshot: instrumentSnapshot(job, captured.snapshot, () => {
3345
+ readLimit.exceeded = true;
3346
+ })
3347
+ },
3348
+ job.controller.signal
3349
+ ),
3350
+ job.controller.signal
3351
+ );
3352
+ const draft = askAnswerDraftSchema.parse(execution);
3353
+ const effectiveModelLabel = job.executor.effectiveModelLabel?.();
3354
+ if (effectiveModelLabel !== void 0) {
3355
+ job.executorCapability = contextualAskExecutorCapabilitySchema.parse({
3356
+ ...job.executorCapability,
3357
+ effectiveModelLabel
3358
+ });
3359
+ }
3360
+ if (readLimit.exceeded) throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
3361
+ job.controller.signal.throwIfAborted();
3362
+ const result = await projectAnswer(job, draft, captured);
3363
+ job.controller.signal.throwIfAborted();
3364
+ job.result = result;
3365
+ transition(job, "answered", "Answer ready.");
3366
+ appendEvent(job, { ...eventBase(job), type: "answer-ready" });
3367
+ finish(job);
3368
+ } catch (error) {
3369
+ fail(
3370
+ job,
3371
+ error instanceof Error && error.name === "ZodError" ? new ContextualAskError("ASK_ANSWER_INVALID", { cause: error }) : asContextualAskError(error)
3372
+ );
3373
+ } finally {
3374
+ clearTimeout(timeout);
3375
+ }
3376
+ };
3377
+ return Object.freeze({
3378
+ async capability(signal) {
3379
+ if (!options.enabled || closed) {
3380
+ return contextualAskCapabilitySchema.parse({
3381
+ schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
3382
+ enabled: false,
3383
+ executors: [],
3384
+ safety: {
3385
+ selectionRequired: true,
3386
+ singleTurn: true,
3387
+ writesAllowed: false,
3388
+ historyStored: false
3389
+ },
3390
+ checkedAt: dependencies.now().toISOString()
3391
+ });
3392
+ }
3393
+ const capabilities = (await Promise.all(
3394
+ [...executors.values()].map(async (executor) => {
3395
+ try {
3396
+ return contextualAskExecutorCapabilitySchema.parse(
3397
+ await raceWithAbort(executor.capability(signal), signal)
3398
+ );
3399
+ } catch {
3400
+ return void 0;
3401
+ }
3402
+ })
3403
+ )).filter((capability) => capability !== void 0);
3404
+ return contextualAskCapabilitySchema.parse({
3405
+ schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
3406
+ enabled: true,
3407
+ executors: capabilities,
3408
+ safety: {
3409
+ selectionRequired: true,
3410
+ singleTurn: true,
3411
+ writesAllowed: false,
3412
+ historyStored: false
3413
+ },
3414
+ checkedAt: dependencies.now().toISOString()
3415
+ });
3416
+ },
3417
+ async create(rawRequest) {
3418
+ cleanupExpired();
3419
+ if (!options.enabled || closed) throw new ContextualAskError("ASK_DISABLED");
3420
+ const parsed = askJobCreateRequestSchema.safeParse(rawRequest);
3421
+ if (!parsed.success) throw new ContextualAskError("ASK_QUESTION_INVALID");
3422
+ const request = parsed.data;
3423
+ const requestFingerprint = fingerprint(request);
3424
+ const previous = requests.get(request.requestId);
3425
+ if (previous !== void 0) {
3426
+ if (previous.fingerprint !== requestFingerprint) {
3427
+ throw new ContextualAskError("ASK_IDEMPOTENCY_CONFLICT");
3428
+ }
3429
+ return publicSnapshot(requireJob(previous.jobId));
3430
+ }
3431
+ const pending = pendingRequests.get(request.requestId);
3432
+ if (pending !== void 0) {
3433
+ if (pending.fingerprint !== requestFingerprint) {
3434
+ throw new ContextualAskError("ASK_IDEMPOTENCY_CONFLICT");
3435
+ }
3436
+ return await pending.promise;
3437
+ }
3438
+ if ([...jobs.values()].some((job) => ACTIVE_STATUSES.has(job.status))) {
3439
+ throw new ContextualAskError("ASK_BUSY");
3440
+ }
3441
+ const executor = executors.get(request.executorId);
3442
+ if (executor === void 0)
3443
+ throw new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE");
3444
+ const lease = options.coordinator.acquire("ask");
3445
+ if (lease === void 0) throw new ContextualAskError("ASK_BUSY");
3446
+ const controller = new AbortController();
3447
+ let leaseTransferred = false;
3448
+ const creation = (async () => {
3449
+ try {
3450
+ const capabilitySignal = AbortSignal.any([
3451
+ controller.signal,
3452
+ AbortSignal.timeout(CONTEXTUAL_ASK_LIMITS3.capabilityTimeoutMs)
3453
+ ]);
3454
+ const capability = contextualAskExecutorCapabilitySchema.parse(
3455
+ await raceWithAbort(
3456
+ executor.capability(capabilitySignal),
3457
+ capabilitySignal
3458
+ )
3459
+ );
3460
+ if (isClosed()) throw new ContextualAskError("ASK_DISABLED");
3461
+ if (capability.executorId !== executor.executorId || capability.state !== "ready" || !capability.readOnlyProven) {
3462
+ throw new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE");
3463
+ }
3464
+ for (const [jobId, existing] of jobs) {
3465
+ if (TERMINAL_STATUSES.has(existing.status)) {
3466
+ existing.snapshot?.dispose();
3467
+ jobs.delete(jobId);
3468
+ requests.delete(existing.request.requestId);
3469
+ }
3470
+ }
3471
+ const createdAt = dependencies.now().toISOString();
3472
+ const job = {
3473
+ controller: new AbortController(),
3474
+ createdAt,
3475
+ events: [],
3476
+ executor,
3477
+ executorCapability: capability,
3478
+ fingerprint: requestFingerprint,
3479
+ id: dependencies.createId(),
3480
+ lease,
3481
+ listeners: /* @__PURE__ */ new Set(),
3482
+ request,
3483
+ errorCode: void 0,
3484
+ expiresAt: void 0,
3485
+ phaseMessage: "Question queued.",
3486
+ result: void 0,
3487
+ runPromise: void 0,
3488
+ sequence: 0,
3489
+ snapshot: void 0,
3490
+ status: "queued",
3491
+ updatedAt: createdAt
3492
+ };
3493
+ jobs.set(job.id, job);
3494
+ requests.set(request.requestId, {
3495
+ fingerprint: requestFingerprint,
3496
+ jobId: job.id
3497
+ });
3498
+ leaseTransferred = true;
3499
+ emitSnapshot(job);
3500
+ emitPhase(job, "Question queued.");
3501
+ job.runPromise = run(job);
3502
+ return publicSnapshot(job);
3503
+ } catch (error) {
3504
+ throw error instanceof ContextualAskError ? error : new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", {
3505
+ cause: error
3506
+ });
3507
+ } finally {
3508
+ if (!leaseTransferred) lease.release();
3509
+ pendingRequests.delete(request.requestId);
3510
+ }
3511
+ })();
3512
+ pendingRequests.set(request.requestId, {
3513
+ controller,
3514
+ fingerprint: requestFingerprint,
3515
+ promise: creation
3516
+ });
3517
+ return await creation;
3518
+ },
3519
+ cancel(jobId) {
3520
+ const job = requireJob(jobId);
3521
+ if (TERMINAL_STATUSES.has(job.status)) return publicSnapshot(job);
3522
+ if (job.status !== "cancelling") {
3523
+ transition(job, "cancelling", "Cancelling question.");
3524
+ job.controller.abort(new ContextualAskError("ASK_CANCELLED"));
3525
+ job.snapshot?.dispose();
3526
+ job.snapshot = void 0;
3527
+ }
3528
+ return publicSnapshot(job);
3529
+ },
3530
+ events(jobId, afterSequence = 0) {
3531
+ if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) {
3532
+ throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
3533
+ }
3534
+ return Object.freeze(
3535
+ requireJob(jobId).events.filter((event) => event.sequence > afterSequence)
3536
+ );
3537
+ },
3538
+ async result(jobId) {
3539
+ const job = requireJob(jobId);
3540
+ await job.runPromise;
3541
+ const current = requireJob(jobId);
3542
+ return askJobResultResponseSchema.parse({
3543
+ snapshot: publicSnapshot(current),
3544
+ ...current.result === void 0 ? {} : { result: current.result }
3545
+ });
3546
+ },
3547
+ snapshot(jobId) {
3548
+ return publicSnapshot(requireJob(jobId));
3549
+ },
3550
+ subscribe(jobId, listener) {
3551
+ const job = requireJob(jobId);
3552
+ if (job.listeners.size >= CONTEXTUAL_ASK_LIMITS3.maximumEventSubscribers) {
3553
+ throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
3554
+ }
3555
+ job.listeners.add(listener);
3556
+ return () => job.listeners.delete(listener);
3557
+ },
3558
+ async close() {
3559
+ if (closed) return;
3560
+ closed = true;
3561
+ for (const pending of pendingRequests.values()) {
3562
+ pending.controller.abort(new ContextualAskError("ASK_CANCELLED"));
3563
+ }
3564
+ await Promise.allSettled(
3565
+ [...pendingRequests.values()].map((pending) => pending.promise)
3566
+ );
3567
+ for (const job of jobs.values()) {
3568
+ if (ACTIVE_STATUSES.has(job.status)) {
3569
+ job.controller.abort(new ContextualAskError("ASK_CANCELLED"));
3570
+ }
3571
+ }
3572
+ await Promise.allSettled(
3573
+ [...jobs.values()].map((job) => job.runPromise ?? Promise.resolve())
3574
+ );
3575
+ for (const job of jobs.values()) {
3576
+ job.snapshot?.dispose();
3577
+ job.listeners.clear();
3578
+ job.lease.release();
3579
+ }
3580
+ jobs.clear();
3581
+ requests.clear();
3582
+ pendingRequests.clear();
3583
+ await Promise.allSettled(
3584
+ [...executors.values()].map(async (executor) => executor.dispose?.())
3585
+ );
3586
+ }
3587
+ });
3588
+ }
3589
+
3590
+ // src/contextual-ask/configured-key-executors.ts
3591
+ import {
3592
+ createConfiguredKeyAskExecutor,
3593
+ resolveProviderCredential as resolveProviderCredential2
3594
+ } from "@spotpatch/agent";
3595
+ function createConfiguredKeyAskExecutors(options) {
3596
+ const preferred = options.defaultExecutor?.kind === "configured-key" ? options.defaultExecutor : Object.freeze({
3597
+ kind: "configured-key",
3598
+ providerProfileId: options.ai.defaultProvider,
3599
+ modelProfileId: options.ai.providers[options.ai.defaultProvider]?.defaultModel ?? ""
3600
+ });
3601
+ const providers = Object.values(options.ai.providers).sort(
3602
+ (left, right) => left.id === preferred.providerProfileId ? -1 : right.id === preferred.providerProfileId ? 1 : 0
3603
+ );
3604
+ return Object.freeze(
3605
+ providers.flatMap((provider) => {
3606
+ const credential = resolveProviderCredential2(
3607
+ provider.apiKeyEnv,
3608
+ options.environment
3609
+ );
3610
+ const models = Object.values(provider.models).sort(
3611
+ (left, right) => provider.id !== preferred.providerProfileId ? 0 : left.id === preferred.modelProfileId ? -1 : right.id === preferred.modelProfileId ? 1 : 0
3612
+ );
3613
+ return models.map(
3614
+ (model) => createConfiguredKeyAskExecutor({
3615
+ provider,
3616
+ model,
3617
+ credential,
3618
+ limits: options.ai.execution.limits,
3619
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch }
3620
+ })
3621
+ );
3622
+ })
3623
+ );
3624
+ }
3625
+
3626
+ // src/contextual-ask/executors.ts
3627
+ function composeContextualAskExecutors(options) {
3628
+ return Object.freeze(
3629
+ options.defaultExecutor?.kind === "managed-codex" ? [options.managedCodex, ...options.configuredKey] : [...options.configuredKey, options.managedCodex]
3630
+ );
3631
+ }
3632
+
3633
+ // src/workspace/activity-coordinator.ts
3634
+ function createWorkspaceActivityCoordinator() {
3635
+ let active;
3636
+ return Object.freeze({
3637
+ acquire(kind) {
3638
+ if (active !== void 0) return void 0;
3639
+ active = kind;
3640
+ let released = false;
3641
+ return Object.freeze({
3642
+ release() {
3643
+ if (released) return;
3644
+ released = true;
3645
+ if (active === kind) active = void 0;
3646
+ }
3647
+ });
3648
+ },
3649
+ current() {
3650
+ return active;
3651
+ }
3652
+ });
3653
+ }
3654
+
2497
3655
  // src/server/middleware.ts
2498
3656
  import {
2499
3657
  ERROR_CODES as ERROR_CODES16,
2500
3658
  SPOTPATCH_API_BASE,
2501
- SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS4,
3659
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS5,
2502
3660
  SpotPatchError as SpotPatchError16,
2503
3661
  openEditorRequestSchema,
2504
3662
  sourceContextRequestSchema
2505
3663
  } from "@spotpatch/shared";
2506
3664
 
3665
+ // src/contextual-ask/http.ts
3666
+ import {
3667
+ askJobActionRequestSchema,
3668
+ askJobCreateRequestSchema as askJobCreateRequestSchema2,
3669
+ askJobEventsRequestSchema,
3670
+ CONTEXTUAL_ASK_LIMITS as CONTEXTUAL_ASK_LIMITS4,
3671
+ SPOTPATCH_ENDPOINTS
3672
+ } from "@spotpatch/shared";
3673
+ var STATUS_BY_ERROR = Object.freeze({
3674
+ ASK_DISABLED: 404,
3675
+ ASK_SELECTION_REQUIRED: 422,
3676
+ ASK_SELECTION_STALE: 409,
3677
+ ASK_QUESTION_INVALID: 422,
3678
+ ASK_EXECUTOR_UNAVAILABLE: 503,
3679
+ ASK_TIMEOUT: 504,
3680
+ ASK_CONSENT_REQUIRED: 403,
3681
+ ASK_BUSY: 409,
3682
+ ASK_IDEMPOTENCY_CONFLICT: 409,
3683
+ ASK_SOURCE_SCOPE_DENIED: 403,
3684
+ ASK_LIMIT_EXCEEDED: 413,
3685
+ ASK_ANSWER_INVALID: 502,
3686
+ ASK_WRITE_ATTEMPTED: 403,
3687
+ ASK_CANCELLED: 409,
3688
+ ASK_RESULT_EXPIRED: 410,
3689
+ ASK_PROTOCOL_INCOMPATIBLE: 400
3690
+ });
3691
+ var PUBLIC_MESSAGES = Object.freeze({
3692
+ ASK_DISABLED: "Contextual Ask is not enabled.",
3693
+ ASK_SELECTION_REQUIRED: "Select at least one source-backed element.",
3694
+ ASK_SELECTION_STALE: "The selected source is stale.",
3695
+ ASK_QUESTION_INVALID: "The question is invalid.",
3696
+ ASK_EXECUTOR_UNAVAILABLE: "The selected Ask executor is unavailable.",
3697
+ ASK_TIMEOUT: "The Ask executor did not answer before the deadline.",
3698
+ ASK_CONSENT_REQUIRED: "Provider data consent is required.",
3699
+ ASK_BUSY: "Another workspace task is active.",
3700
+ ASK_IDEMPOTENCY_CONFLICT: "The request ID was reused with different content.",
3701
+ ASK_SOURCE_SCOPE_DENIED: "The requested source scope was denied.",
3702
+ ASK_LIMIT_EXCEEDED: "The Ask request exceeded a safety limit.",
3703
+ ASK_ANSWER_INVALID: "The executor returned an invalid answer.",
3704
+ ASK_WRITE_ATTEMPTED: "Ask executors cannot write project files.",
3705
+ ASK_CANCELLED: "The Ask job was cancelled.",
3706
+ ASK_RESULT_EXPIRED: "The Ask result has expired.",
3707
+ ASK_PROTOCOL_INCOMPATIBLE: "The Ask protocol request is incompatible."
3708
+ });
3709
+ function writeJson2(response, status, data) {
3710
+ response.statusCode = status;
3711
+ response.setHeader("Cache-Control", "no-store");
3712
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
3713
+ response.end(JSON.stringify({ ok: true, data }));
3714
+ }
3715
+ function writeError(response, error, logger) {
3716
+ const normalized = error instanceof ContextualAskError ? error : new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", { cause: error });
3717
+ if (normalized.code === "ASK_EXECUTOR_UNAVAILABLE" && error !== normalized) {
3718
+ logger?.warn("[spotpatch:ask] Internal contextual Ask request failure.");
3719
+ }
3720
+ response.statusCode = STATUS_BY_ERROR[normalized.code];
3721
+ response.setHeader("Cache-Control", "no-store");
3722
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
3723
+ response.end(
3724
+ JSON.stringify({
3725
+ ok: false,
3726
+ error: {
3727
+ code: normalized.code,
3728
+ message: PUBLIC_MESSAGES[normalized.code]
3729
+ }
3730
+ })
3731
+ );
3732
+ }
3733
+ function matchContextualAskPath(pathname) {
3734
+ if (pathname === SPOTPATCH_ENDPOINTS.askCapability) {
3735
+ return Object.freeze({ kind: "capability" });
3736
+ }
3737
+ if (pathname === SPOTPATCH_ENDPOINTS.askJobs) {
3738
+ return Object.freeze({ kind: "create" });
3739
+ }
3740
+ const prefix = `${SPOTPATCH_ENDPOINTS.askJobs}/`;
3741
+ if (!pathname.startsWith(prefix)) return void 0;
3742
+ const segments = pathname.slice(prefix.length).split("/");
3743
+ if (segments.length !== 2) return void 0;
3744
+ let jobId;
3745
+ try {
3746
+ jobId = decodeURIComponent(segments[0] ?? "");
3747
+ } catch {
3748
+ return void 0;
3749
+ }
3750
+ const action = segments[1];
3751
+ if (!/^[A-Za-z0-9_-]{1,128}$/u.test(jobId) || action !== "events" && action !== "result" && action !== "cancel") {
3752
+ return void 0;
3753
+ }
3754
+ return Object.freeze({ kind: "job", action, jobId });
3755
+ }
3756
+ function parseAfterSequence(request) {
3757
+ let url;
3758
+ try {
3759
+ url = new URL(request.url ?? "/", "http://spotpatch.invalid");
3760
+ } catch {
3761
+ throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
3762
+ }
3763
+ if ([...url.searchParams.keys()].some((key) => key !== "afterSequence")) {
3764
+ throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
3765
+ }
3766
+ const raw = url.searchParams.get("afterSequence");
3767
+ if (raw === null) return 0;
3768
+ if (!/^\d+$/u.test(raw)) throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
3769
+ const parsed = askJobEventsRequestSchema.safeParse({ afterSequence: Number(raw) });
3770
+ if (!parsed.success || !Number.isSafeInteger(parsed.data.afterSequence)) {
3771
+ throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
3772
+ }
3773
+ return parsed.data.afterSequence ?? 0;
3774
+ }
3775
+ function streamEvents(request, response, manager, jobId) {
3776
+ const afterSequence = parseAfterSequence(request);
3777
+ response.statusCode = 200;
3778
+ response.setHeader("Cache-Control", "no-store");
3779
+ response.setHeader("Content-Type", "application/x-ndjson; charset=utf-8");
3780
+ response.setHeader("X-Content-Type-Options", "nosniff");
3781
+ let unsubscribe = () => void 0;
3782
+ const heartbeat = setInterval(
3783
+ () => response.write("\n"),
3784
+ CONTEXTUAL_ASK_LIMITS4.eventHeartbeatMs
3785
+ );
3786
+ heartbeat.unref();
3787
+ const cleanup = () => {
3788
+ clearInterval(heartbeat);
3789
+ unsubscribe();
3790
+ };
3791
+ unsubscribe = manager.subscribe(jobId, (event) => {
3792
+ response.write(`${JSON.stringify(event)}
3793
+ `);
3794
+ if (["answered", "cancelled", "failed"].includes(event.status)) {
3795
+ cleanup();
3796
+ response.end();
3797
+ }
3798
+ });
3799
+ const replay = manager.events(jobId, afterSequence);
3800
+ for (const event of replay) response.write(`${JSON.stringify(event)}
3801
+ `);
3802
+ const latest = replay.at(-1);
3803
+ const currentStatus = latest?.status ?? manager.snapshot(jobId).status;
3804
+ if (["answered", "cancelled", "failed"].includes(currentStatus)) {
3805
+ cleanup();
3806
+ response.end();
3807
+ return;
3808
+ }
3809
+ request.once("close", cleanup);
3810
+ response.once("close", cleanup);
3811
+ }
3812
+ function hasEmptySelection(value) {
3813
+ if (typeof value !== "object" || value === null) return false;
3814
+ const envelope = value.envelope;
3815
+ if (typeof envelope !== "object" || envelope === null) return false;
3816
+ const selection = envelope.selection;
3817
+ if (typeof selection !== "object" || selection === null) return false;
3818
+ return Array.isArray(selection.targets) && selection.targets.length === 0;
3819
+ }
3820
+ function lacksProviderDataConsent(value) {
3821
+ return typeof value === "object" && value !== null && value.providerDataConsent !== true;
3822
+ }
3823
+ async function handleContextualAskRequest(request, response, route, manager, logger) {
3824
+ try {
3825
+ if (manager === void 0) throw new ContextualAskError("ASK_DISABLED");
3826
+ if (route.kind === "capability") {
3827
+ if (request.method !== "GET")
3828
+ throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
3829
+ writeJson2(
3830
+ response,
3831
+ 200,
3832
+ await manager.capability(
3833
+ AbortSignal.timeout(CONTEXTUAL_ASK_LIMITS4.capabilityTimeoutMs)
3834
+ )
3835
+ );
3836
+ return;
3837
+ }
3838
+ if (route.kind === "create") {
3839
+ if (request.method !== "POST")
3840
+ throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
3841
+ const raw = await readJsonRequestBody(
3842
+ request,
3843
+ CONTEXTUAL_ASK_LIMITS4.maximumRequestBodyBytes
3844
+ ).catch((error) => {
3845
+ throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE", { cause: error });
3846
+ });
3847
+ const parsed = askJobCreateRequestSchema2.safeParse(raw);
3848
+ if (!parsed.success) {
3849
+ throw new ContextualAskError(
3850
+ hasEmptySelection(raw) ? "ASK_SELECTION_REQUIRED" : lacksProviderDataConsent(raw) ? "ASK_CONSENT_REQUIRED" : "ASK_PROTOCOL_INCOMPATIBLE"
3851
+ );
3852
+ }
3853
+ writeJson2(response, 202, await manager.create(parsed.data));
3854
+ return;
3855
+ }
3856
+ if (route.action === "events") {
3857
+ if (request.method !== "GET")
3858
+ throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
3859
+ streamEvents(request, response, manager, route.jobId);
3860
+ return;
3861
+ }
3862
+ if (route.action === "result") {
3863
+ if (request.method !== "GET")
3864
+ throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
3865
+ writeJson2(response, 200, await manager.result(route.jobId));
3866
+ return;
3867
+ }
3868
+ if (request.method !== "POST")
3869
+ throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
3870
+ const action = askJobActionRequestSchema.safeParse(
3871
+ await readJsonRequestBody(request).catch((error) => {
3872
+ throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE", { cause: error });
3873
+ })
3874
+ );
3875
+ if (!action.success) throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
3876
+ writeJson2(response, 200, manager.cancel(route.jobId));
3877
+ } catch (error) {
3878
+ writeError(response, error, logger);
3879
+ }
3880
+ }
3881
+
2507
3882
  // src/external-handoff/browser-http.ts
2508
3883
  import {
2509
3884
  ERROR_CODES as ERROR_CODES10,
2510
3885
  EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS5,
2511
- SPOTPATCH_ENDPOINTS,
3886
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
2512
3887
  SpotPatchError as SpotPatchError10,
2513
3888
  externalHandoffCapabilityRequestSchema,
2514
3889
  externalHandoffPublishRequestSchema,
@@ -2517,8 +3892,8 @@ import {
2517
3892
  } from "@spotpatch/shared";
2518
3893
 
2519
3894
  // src/server/annotation-authorizer.ts
2520
- import { realpath as realpath5 } from "fs/promises";
2521
- import path7 from "path";
3895
+ import { realpath as realpath6 } from "fs/promises";
3896
+ import path8 from "path";
2522
3897
  import {
2523
3898
  ERROR_CODES as ERROR_CODES9,
2524
3899
  SpotPatchError as SpotPatchError9,
@@ -2527,8 +3902,8 @@ import {
2527
3902
  } from "@spotpatch/shared";
2528
3903
 
2529
3904
  // src/server/source-context.ts
2530
- import { readFile as readFile3, realpath as realpath4 } from "fs/promises";
2531
- import path6 from "path";
3905
+ import { readFile as readFile3, realpath as realpath5 } from "fs/promises";
3906
+ import path7 from "path";
2532
3907
  import {
2533
3908
  ERROR_CODES as ERROR_CODES8,
2534
3909
  SpotPatchError as SpotPatchError8
@@ -2536,8 +3911,8 @@ import {
2536
3911
 
2537
3912
  // src/server/extract-code-context.ts
2538
3913
  import {
2539
- parseSync,
2540
- Visitor
3914
+ parseSync as parseSync2,
3915
+ Visitor as Visitor2
2541
3916
  } from "oxc-parser";
2542
3917
  function isComponentName(name) {
2543
3918
  return /^[A-Z]/u.test(name);
@@ -2621,7 +3996,7 @@ function findComponentSpan(options) {
2621
3996
  }
2622
3997
  let parseResult;
2623
3998
  try {
2624
- parseResult = parseSync(options.sourcePath, options.source, {
3999
+ parseResult = parseSync2(options.sourcePath, options.source, {
2625
4000
  sourceType: "module"
2626
4001
  });
2627
4002
  } catch {
@@ -2632,7 +4007,7 @@ function findComponentSpan(options) {
2632
4007
  }
2633
4008
  const jsxNodes = [];
2634
4009
  const components = [];
2635
- const visitor = new Visitor({
4010
+ const visitor = new Visitor2({
2636
4011
  JSXElement(node) {
2637
4012
  jsxNodes.push(node);
2638
4013
  },
@@ -2764,8 +4139,8 @@ function extractCodeContext(options) {
2764
4139
  }
2765
4140
 
2766
4141
  // src/server/source-file.ts
2767
- import { realpath as realpath3, stat as stat2 } from "fs/promises";
2768
- import path5 from "path";
4142
+ import { realpath as realpath4, stat as stat2 } from "fs/promises";
4143
+ import path6 from "path";
2769
4144
  import { ERROR_CODES as ERROR_CODES7, SpotPatchError as SpotPatchError7 } from "@spotpatch/shared";
2770
4145
  var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
2771
4146
  function isMissingFileError(error) {
@@ -2776,8 +4151,8 @@ async function assertInsideRoot(root, candidate) {
2776
4151
  let realCandidate;
2777
4152
  try {
2778
4153
  [realRoot, realCandidate] = await Promise.all([
2779
- realpath3(root),
2780
- realpath3(candidate)
4154
+ realpath4(root),
4155
+ realpath4(candidate)
2781
4156
  ]);
2782
4157
  } catch (error) {
2783
4158
  if (isMissingFileError(error)) {
@@ -2787,8 +4162,8 @@ async function assertInsideRoot(root, candidate) {
2787
4162
  }
2788
4163
  throw error;
2789
4164
  }
2790
- const relative = path5.relative(realRoot, realCandidate);
2791
- const outside = relative.startsWith(`..${path5.sep}`) || relative === ".." || path5.isAbsolute(relative);
4165
+ const relative = path6.relative(realRoot, realCandidate);
4166
+ const outside = relative.startsWith(`..${path6.sep}`) || relative === ".." || path6.isAbsolute(relative);
2792
4167
  if (outside) {
2793
4168
  throw new SpotPatchError7(ERROR_CODES7.SOURCE_OUTSIDE_ROOT);
2794
4169
  }
@@ -2800,7 +4175,7 @@ async function resolveSourceFile(options) {
2800
4175
  throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
2801
4176
  }
2802
4177
  const sourcePath = await assertInsideRoot(options.root, registeredPath);
2803
- if (!ALLOWED_EXTENSIONS.has(path5.extname(sourcePath).toLowerCase())) {
4178
+ if (!ALLOWED_EXTENSIONS.has(path6.extname(sourcePath).toLowerCase())) {
2804
4179
  throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
2805
4180
  }
2806
4181
  let sourceStat;
@@ -2825,7 +4200,7 @@ async function resolveSourceFile(options) {
2825
4200
 
2826
4201
  // src/server/source-context.ts
2827
4202
  function toDisplayPath(root, sourcePath) {
2828
- return path6.relative(root, sourcePath).split(path6.sep).join("/");
4203
+ return path7.relative(root, sourcePath).split(path7.sep).join("/");
2829
4204
  }
2830
4205
  async function readSourceContext(options) {
2831
4206
  const sourcePath = await resolveSourceFile({
@@ -2848,11 +4223,11 @@ async function readSourceContext(options) {
2848
4223
  if (options.request.line > lines.length) {
2849
4224
  throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
2850
4225
  }
2851
- const extension = path6.extname(sourcePath).toLowerCase();
4226
+ const extension = path7.extname(sourcePath).toLowerCase();
2852
4227
  return extractCodeContext({
2853
4228
  source,
2854
4229
  sourcePath,
2855
- relativePath: toDisplayPath(await realpath4(options.root), sourcePath),
4230
+ relativePath: toDisplayPath(await realpath5(options.root), sourcePath),
2856
4231
  language: extension === ".tsx" ? "tsx" : "jsx",
2857
4232
  line: options.request.line,
2858
4233
  column: options.request.column,
@@ -2894,7 +4269,7 @@ async function authorizeSourceRef(source, registry, root) {
2894
4269
  return compactSourceRef(source);
2895
4270
  }
2896
4271
  const sourcePath = await resolveSourceFile({ fileId: source.fileId, registry, root });
2897
- const relativePath = path7.relative(await realpath5(root), sourcePath).split(path7.sep).join("/");
4272
+ const relativePath = path8.relative(await realpath6(root), sourcePath).split(path8.sep).join("/");
2898
4273
  if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
2899
4274
  throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
2900
4275
  }
@@ -2997,11 +4372,11 @@ async function authorizeAnnotation(input) {
2997
4372
  }
2998
4373
 
2999
4374
  // src/external-handoff/browser-http.ts
3000
- function matchExternalHandoffBrowserPath(path9) {
3001
- if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffCapability) return "capability";
3002
- if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffPublish) return "publish";
3003
- if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffStatus) return "status";
3004
- if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffResolveDelivery) {
4375
+ function matchExternalHandoffBrowserPath(path10) {
4376
+ if (path10 === SPOTPATCH_ENDPOINTS2.externalHandoffCapability) return "capability";
4377
+ if (path10 === SPOTPATCH_ENDPOINTS2.externalHandoffPublish) return "publish";
4378
+ if (path10 === SPOTPATCH_ENDPOINTS2.externalHandoffStatus) return "status";
4379
+ if (path10 === SPOTPATCH_ENDPOINTS2.externalHandoffResolveDelivery) {
3005
4380
  return "resolve-delivery";
3006
4381
  }
3007
4382
  return void 0;
@@ -3081,7 +4456,7 @@ async function handleExternalHandoffBrowserRequest(request, response, route, opt
3081
4456
  import {
3082
4457
  ERROR_CODES as ERROR_CODES11,
3083
4458
  EXTERNAL_AGENT_CONTROL_LIMITS,
3084
- SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
4459
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS3,
3085
4460
  SpotPatchError as SpotPatchError11,
3086
4461
  externalAgentControlCancelRequestSchema,
3087
4462
  externalAgentControlConnectRequestSchema,
@@ -3093,15 +4468,15 @@ import {
3093
4468
  externalAgentManagedResultSchema,
3094
4469
  externalAgentResultRequestSchema
3095
4470
  } from "@spotpatch/shared";
3096
- function matchExternalAgentBrowserPath(path9) {
3097
- if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlStatus) return "status";
3098
- if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlConnect) return "connect";
3099
- if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlDisconnect) {
4471
+ function matchExternalAgentBrowserPath(path10) {
4472
+ if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentControlStatus) return "status";
4473
+ if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentControlConnect) return "connect";
4474
+ if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentControlDisconnect) {
3100
4475
  return "disconnect";
3101
4476
  }
3102
- if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlCancel) return "cancel";
3103
- if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentEvents) return "events";
3104
- if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentResult) return "result";
4477
+ if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentControlCancel) return "cancel";
4478
+ if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentEvents) return "events";
4479
+ if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentResult) return "result";
3105
4480
  return void 0;
3106
4481
  }
3107
4482
  function writeEvent(response, event) {
@@ -3240,7 +4615,7 @@ function createExternalAgentBrowserController(port) {
3240
4615
  // src/server/agent-http.ts
3241
4616
  import {
3242
4617
  ERROR_CODES as ERROR_CODES12,
3243
- SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS3,
4618
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS4,
3244
4619
  SpotPatchError as SpotPatchError12,
3245
4620
  agentCapabilityRequestSchema,
3246
4621
  agentJobActionRequestSchema,
@@ -3285,21 +4660,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
3285
4660
  "reverted",
3286
4661
  "failed"
3287
4662
  ]);
3288
- function matchAgentRequestPath(path9) {
3289
- if (path9 === SPOTPATCH_ENDPOINTS3.agentCapability) {
4663
+ function matchAgentRequestPath(path10) {
4664
+ if (path10 === SPOTPATCH_ENDPOINTS4.agentCapability) {
3290
4665
  return Object.freeze({ kind: "capability" });
3291
4666
  }
3292
- if (path9 === SPOTPATCH_ENDPOINTS3.agentWorkspaceHealth) {
4667
+ if (path10 === SPOTPATCH_ENDPOINTS4.agentWorkspaceHealth) {
3293
4668
  return Object.freeze({ kind: "workspace-health" });
3294
4669
  }
3295
- if (path9 === SPOTPATCH_ENDPOINTS3.agentJobs) {
4670
+ if (path10 === SPOTPATCH_ENDPOINTS4.agentJobs) {
3296
4671
  return Object.freeze({ kind: "create-job" });
3297
4672
  }
3298
- const prefix = `${SPOTPATCH_ENDPOINTS3.agentJobs}/`;
3299
- if (!path9.startsWith(prefix)) {
4673
+ const prefix = `${SPOTPATCH_ENDPOINTS4.agentJobs}/`;
4674
+ if (!path10.startsWith(prefix)) {
3300
4675
  return void 0;
3301
4676
  }
3302
- const segments = path9.slice(prefix.length).split("/");
4677
+ const segments = path10.slice(prefix.length).split("/");
3303
4678
  const jobId = segments[0];
3304
4679
  const action = segments[1];
3305
4680
  if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
@@ -3493,13 +4868,13 @@ function detectIntegratedEditor(environment) {
3493
4868
  function editorCommand(editor) {
3494
4869
  return editor === "cursor" ? "cursor" : "code";
3495
4870
  }
3496
- var DEFAULT_DEPENDENCIES2 = Object.freeze({
4871
+ var DEFAULT_DEPENDENCIES3 = Object.freeze({
3497
4872
  environment: process.env,
3498
4873
  fallbackLauncher: launchEditor,
3499
4874
  processSpawner: (command, arguments_, options) => spawn(command, [...arguments_], options),
3500
4875
  startupGraceMs: EDITOR_STARTUP_GRACE_MS
3501
4876
  });
3502
- function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
4877
+ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES3) {
3503
4878
  return (target, configuredEditor) => {
3504
4879
  const integratedEditor = detectIntegratedEditor(dependencies.environment);
3505
4880
  const resolvedEditor = configuredEditor === "auto" ? integratedEditor : configuredEditor;
@@ -3554,7 +4929,7 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
3554
4929
  var launchConfiguredEditor = createEditorLauncher();
3555
4930
 
3556
4931
  // src/server/data-flow-http.ts
3557
- import { createHash as createHash3 } from "crypto";
4932
+ import { createHash as createHash5 } from "crypto";
3558
4933
  import {
3559
4934
  createStaticDataFlowAnalyzer
3560
4935
  } from "@spotpatch/analyzer";
@@ -3683,7 +5058,7 @@ async function handlePageDataFlowReport(request, analyzer, options) {
3683
5058
  const analyzedVersions = new Set(
3684
5059
  componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
3685
5060
  );
3686
- const reportId = `page_${createHash3("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
5061
+ const reportId = `page_${createHash5("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
3687
5062
  const complete = componentReports.every((report2) => report2.completeness.complete);
3688
5063
  const report = Object.freeze({
3689
5064
  schemaVersion: DATA_FLOW_SCHEMA_VERSION,
@@ -3776,6 +5151,9 @@ function parseOrigin(value) {
3776
5151
  return void 0;
3777
5152
  }
3778
5153
  }
5154
+ function isSameOriginBrowserGet(request) {
5155
+ return request.method === "GET" && getSingleHeader(request, "sec-fetch-site")?.toLowerCase() === "same-origin" && getSingleHeader(request, "sec-fetch-mode")?.toLowerCase() === "cors" && getSingleHeader(request, "sec-fetch-dest")?.toLowerCase() === "empty";
5156
+ }
3779
5157
  function assertRequestAuthorized(request, options) {
3780
5158
  const actualToken = getSingleHeader(request, SPOTPATCH_TOKEN_HEADER);
3781
5159
  if (!tokensMatch2(actualToken, options.sessionToken)) {
@@ -3785,10 +5163,16 @@ function assertRequestAuthorized(request, options) {
3785
5163
  const originHeader = getSingleHeader(request, "origin");
3786
5164
  const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
3787
5165
  const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
3788
- if (host === void 0 || origin === void 0) {
5166
+ if (host === void 0) {
3789
5167
  throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
3790
5168
  }
3791
5169
  const hostIsLoopback = isLoopbackHostname(host.hostname);
5170
+ if (origin === void 0) {
5171
+ if (!isSameOriginBrowserGet(request) || !options.allowLan && !hostIsLoopback) {
5172
+ throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
5173
+ }
5174
+ return;
5175
+ }
3792
5176
  const originIsLoopback = isLoopbackHostname(origin.hostname);
3793
5177
  if (!options.allowLan) {
3794
5178
  if (!hostIsLoopback || !originIsLoopback) {
@@ -3861,7 +5245,7 @@ async function readRuntimeBootstrap(request, options) {
3861
5245
  }
3862
5246
 
3863
5247
  // src/server/middleware.ts
3864
- var STATUS_BY_ERROR = Object.freeze({
5248
+ var STATUS_BY_ERROR2 = Object.freeze({
3865
5249
  [ERROR_CODES16.INVALID_REQUEST]: 400,
3866
5250
  [ERROR_CODES16.INVALID_TOKEN]: 401,
3867
5251
  [ERROR_CODES16.ORIGIN_NOT_ALLOWED]: 403,
@@ -3917,7 +5301,7 @@ var STATUS_BY_ERROR = Object.freeze({
3917
5301
  [ERROR_CODES16.APPLY_CONFLICT]: 409,
3918
5302
  [ERROR_CODES16.INTERNAL_ERROR]: 500
3919
5303
  });
3920
- var PUBLIC_MESSAGES = Object.freeze({
5304
+ var PUBLIC_MESSAGES2 = Object.freeze({
3921
5305
  [ERROR_CODES16.INVALID_REQUEST]: "The request is invalid.",
3922
5306
  [ERROR_CODES16.INVALID_TOKEN]: "The session token is invalid.",
3923
5307
  [ERROR_CODES16.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
@@ -3973,7 +5357,7 @@ var PUBLIC_MESSAGES = Object.freeze({
3973
5357
  [ERROR_CODES16.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
3974
5358
  [ERROR_CODES16.INTERNAL_ERROR]: "The request could not be completed."
3975
5359
  });
3976
- function writeJson2(response, status, payload) {
5360
+ function writeJson3(response, status, payload) {
3977
5361
  response.statusCode = status;
3978
5362
  response.setHeader("Cache-Control", "no-store");
3979
5363
  response.setHeader("Content-Type", "application/json; charset=utf-8");
@@ -3982,16 +5366,16 @@ function writeJson2(response, status, payload) {
3982
5366
  function asSpotPatchError(error) {
3983
5367
  return error instanceof SpotPatchError16 ? error : new SpotPatchError16(ERROR_CODES16.INTERNAL_ERROR, void 0, { cause: error });
3984
5368
  }
3985
- function writeError(response, error, logger) {
5369
+ function writeError2(response, error, logger) {
3986
5370
  const normalized = asSpotPatchError(error);
3987
5371
  if (normalized.code === ERROR_CODES16.INTERNAL_ERROR) {
3988
5372
  logger?.warn("[spotpatch:server] Internal request failure.");
3989
5373
  }
3990
- writeJson2(response, STATUS_BY_ERROR[normalized.code], {
5374
+ writeJson3(response, STATUS_BY_ERROR2[normalized.code], {
3991
5375
  ok: false,
3992
5376
  error: {
3993
5377
  code: normalized.code,
3994
- message: PUBLIC_MESSAGES[normalized.code]
5378
+ message: PUBLIC_MESSAGES2[normalized.code]
3995
5379
  }
3996
5380
  });
3997
5381
  }
@@ -4047,44 +5431,55 @@ function createSpotPatchMiddleware(options) {
4047
5431
  const dataFlowAnalyzer = createDataFlowAnalyzer(options);
4048
5432
  const externalAgentController = options.externalAgentControl === void 0 ? void 0 : createExternalAgentBrowserController(options.externalAgentControl);
4049
5433
  const middleware = (request, response, next) => {
4050
- const path9 = requestPath(request);
4051
- const agentRoute = matchAgentRequestPath(path9);
4052
- const externalAgentRoute = matchExternalAgentBrowserPath(path9);
4053
- const externalHandoffRoute = matchExternalHandoffBrowserPath(path9);
4054
- if (path9 !== SPOTPATCH_ENDPOINTS4.sourceContext && path9 !== SPOTPATCH_ENDPOINTS4.openEditor && path9 !== SPOTPATCH_ENDPOINTS4.dataFlowComponentReport && path9 !== SPOTPATCH_ENDPOINTS4.dataFlowPageReport && agentRoute === void 0 && externalAgentRoute === void 0 && externalHandoffRoute === void 0 && !path9.startsWith(`${SPOTPATCH_API_BASE}/`)) {
5434
+ const path10 = requestPath(request);
5435
+ const agentRoute = matchAgentRequestPath(path10);
5436
+ const askRoute = matchContextualAskPath(path10);
5437
+ const externalAgentRoute = matchExternalAgentBrowserPath(path10);
5438
+ const externalHandoffRoute = matchExternalHandoffBrowserPath(path10);
5439
+ if (path10 !== SPOTPATCH_ENDPOINTS5.sourceContext && path10 !== SPOTPATCH_ENDPOINTS5.openEditor && path10 !== SPOTPATCH_ENDPOINTS5.dataFlowComponentReport && path10 !== SPOTPATCH_ENDPOINTS5.dataFlowPageReport && agentRoute === void 0 && askRoute === void 0 && externalAgentRoute === void 0 && externalHandoffRoute === void 0 && !path10.startsWith(`${SPOTPATCH_API_BASE}/`)) {
4055
5440
  next();
4056
5441
  return;
4057
5442
  }
4058
5443
  const handle = async () => {
4059
- if (path9 === SPOTPATCH_ENDPOINTS4.bootstrap && bootstrap !== void 0) {
5444
+ if (path10 === SPOTPATCH_ENDPOINTS5.bootstrap && bootstrap !== void 0) {
4060
5445
  const data = await readRuntimeBootstrap(
4061
5446
  request,
4062
5447
  bootstrap
4063
5448
  );
4064
- writeJson2(response, 200, { ok: true, data });
5449
+ writeJson3(response, 200, { ok: true, data });
4065
5450
  return;
4066
5451
  }
4067
5452
  assertRequestAuthorized(request, {
4068
5453
  allowLan: options.options.allowLan,
4069
5454
  sessionToken: options.session.token
4070
5455
  });
4071
- if (path9 === SPOTPATCH_ENDPOINTS4.sourceContext) {
5456
+ if (askRoute !== void 0) {
5457
+ await handleContextualAskRequest(
5458
+ request,
5459
+ response,
5460
+ askRoute,
5461
+ options.contextualAskManager,
5462
+ options.logger
5463
+ );
5464
+ return;
5465
+ }
5466
+ if (path10 === SPOTPATCH_ENDPOINTS5.sourceContext) {
4072
5467
  if (request.method !== "POST") {
4073
5468
  throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
4074
5469
  }
4075
5470
  const data = await handleSourceContext(request, options);
4076
- writeJson2(response, 200, { ok: true, data });
5471
+ writeJson3(response, 200, { ok: true, data });
4077
5472
  return;
4078
5473
  }
4079
- if (path9 === SPOTPATCH_ENDPOINTS4.openEditor) {
5474
+ if (path10 === SPOTPATCH_ENDPOINTS5.openEditor) {
4080
5475
  if (request.method !== "POST") {
4081
5476
  throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
4082
5477
  }
4083
5478
  const data = await handleOpenEditor(request, options);
4084
- writeJson2(response, 200, { ok: true, data });
5479
+ writeJson3(response, 200, { ok: true, data });
4085
5480
  return;
4086
5481
  }
4087
- if (path9 === SPOTPATCH_ENDPOINTS4.dataFlowComponentReport) {
5482
+ if (path10 === SPOTPATCH_ENDPOINTS5.dataFlowComponentReport) {
4088
5483
  if (request.method !== "POST") {
4089
5484
  throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
4090
5485
  }
@@ -4093,15 +5488,15 @@ function createSpotPatchMiddleware(options) {
4093
5488
  dataFlowAnalyzer,
4094
5489
  options
4095
5490
  );
4096
- writeJson2(response, 200, { ok: true, data });
5491
+ writeJson3(response, 200, { ok: true, data });
4097
5492
  return;
4098
5493
  }
4099
- if (path9 === SPOTPATCH_ENDPOINTS4.dataFlowPageReport) {
5494
+ if (path10 === SPOTPATCH_ENDPOINTS5.dataFlowPageReport) {
4100
5495
  if (request.method !== "POST") {
4101
5496
  throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
4102
5497
  }
4103
5498
  const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
4104
- writeJson2(response, 200, { ok: true, data });
5499
+ writeJson3(response, 200, { ok: true, data });
4105
5500
  return;
4106
5501
  }
4107
5502
  if (externalAgentRoute !== void 0) {
@@ -4113,7 +5508,7 @@ function createSpotPatchMiddleware(options) {
4113
5508
  response,
4114
5509
  externalAgentRoute,
4115
5510
  (target, status, data) => {
4116
- writeJson2(target, status, { ok: true, data });
5511
+ writeJson3(target, status, { ok: true, data });
4117
5512
  }
4118
5513
  );
4119
5514
  return;
@@ -4130,7 +5525,7 @@ function createSpotPatchMiddleware(options) {
4130
5525
  ...options.externalHandoffService === void 0 ? {} : { service: options.externalHandoffService }
4131
5526
  },
4132
5527
  (target, status, data) => {
4133
- writeJson2(target, status, { ok: true, data });
5528
+ writeJson3(target, status, { ok: true, data });
4134
5529
  }
4135
5530
  );
4136
5531
  return;
@@ -4144,12 +5539,12 @@ function createSpotPatchMiddleware(options) {
4144
5539
  options,
4145
5540
  agentRoute,
4146
5541
  (target, status, data) => {
4147
- writeJson2(target, status, { ok: true, data });
5542
+ writeJson3(target, status, { ok: true, data });
4148
5543
  }
4149
5544
  );
4150
5545
  };
4151
5546
  void handle().catch((error) => {
4152
- writeError(response, error, options.logger);
5547
+ writeError2(response, error, options.logger);
4153
5548
  });
4154
5549
  };
4155
5550
  return Object.assign(middleware, {
@@ -4161,17 +5556,33 @@ function createSpotPatchMiddleware(options) {
4161
5556
 
4162
5557
  // src/server/source-registration.ts
4163
5558
  import { timingSafeEqual as timingSafeEqual3 } from "crypto";
4164
- import { lstat as lstat4, realpath as realpath6 } from "fs/promises";
4165
- import path8 from "path";
4166
- import { createSourceFilter } from "@spotpatch/compiler";
5559
+ import { lstat as lstat5, readFile as readFile4, realpath as realpath7 } from "fs/promises";
5560
+ import path9 from "path";
5561
+ import {
5562
+ createDataFlowSourceFilter,
5563
+ createDataFlowSourceVersion,
5564
+ createSourceFilter
5565
+ } from "@spotpatch/compiler";
5566
+ import { DEFAULT_DATA_FLOW_LIMITS as DEFAULT_DATA_FLOW_LIMITS2 } from "@spotpatch/shared";
4167
5567
  import { z as z2 } from "zod";
4168
- var REGISTRATION_BODY_LIMIT_BYTES = 4096;
5568
+ var REGISTRATION_BODY_LIMIT_BYTES = DEFAULT_DATA_FLOW_LIMITS2.protocolRequestMaxBytes;
4169
5569
  var REGISTRATION_IDENTITY_PATTERN = /^[A-Za-z0-9_-]{16,128}$/u;
5570
+ var DATA_FLOW_IDENTITY_PATTERN = /^[A-Za-z0-9_-]{1,128}$/u;
4170
5571
  var INTERNAL_SECRET_HEADER = "x-spotpatch-internal";
4171
5572
  var FORBIDDEN_SOURCE_SEGMENTS = /* @__PURE__ */ new Set([".next", "node_modules"]);
4172
5573
  var registrationRequestSchema = z2.strictObject({
4173
5574
  epoch: z2.string().regex(REGISTRATION_IDENTITY_PATTERN),
4174
- resourcePath: z2.string().min(1).max(3072)
5575
+ resourcePath: z2.string().min(1).max(3072),
5576
+ dataFlow: z2.strictObject({
5577
+ sourceVersion: z2.string().regex(DATA_FLOW_IDENTITY_PATTERN),
5578
+ components: z2.array(
5579
+ z2.strictObject({
5580
+ componentSourceId: z2.string().regex(DATA_FLOW_IDENTITY_PATTERN),
5581
+ line: z2.number().int().positive().max(1e7),
5582
+ column: z2.number().int().positive().max(1e7)
5583
+ })
5584
+ ).max(DEFAULT_DATA_FLOW_LIMITS2.sourceMaxComponents)
5585
+ }).optional()
4175
5586
  });
4176
5587
  function getSingleHeader3(request, name) {
4177
5588
  const value = request.headers[name.toLowerCase()];
@@ -4186,13 +5597,13 @@ function identitiesMatch(actual, expected) {
4186
5597
  return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual3(actualBytes, expectedBytes);
4187
5598
  }
4188
5599
  function isWithinRoot(root, candidate) {
4189
- const relative = path8.relative(root, candidate);
4190
- return relative === "" || !relative.startsWith(`..${path8.sep}`) && relative !== ".." && !path8.isAbsolute(relative);
5600
+ const relative = path9.relative(root, candidate);
5601
+ return relative === "" || !relative.startsWith(`..${path9.sep}`) && relative !== ".." && !path9.isAbsolute(relative);
4191
5602
  }
4192
5603
  function hasForbiddenSegment(root, candidate) {
4193
- return path8.relative(root, candidate).split(path8.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
5604
+ return path9.relative(root, candidate).split(path9.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
4194
5605
  }
4195
- function writeJson3(response, statusCode, payload) {
5606
+ function writeJson4(response, statusCode, payload) {
4196
5607
  const body = JSON.stringify(payload);
4197
5608
  response.statusCode = statusCode;
4198
5609
  response.setHeader("Cache-Control", "no-store");
@@ -4212,15 +5623,15 @@ function requestComesFromLoopbackWorker(request) {
4212
5623
  }
4213
5624
  }
4214
5625
  async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
4215
- if (!path8.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
5626
+ if (!path9.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
4216
5627
  return void 0;
4217
5628
  }
4218
5629
  try {
4219
- const sourceStat = await lstat4(requestedPath);
5630
+ const sourceStat = await lstat5(requestedPath);
4220
5631
  if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
4221
5632
  return void 0;
4222
5633
  }
4223
- const resolvedPath = await realpath6(requestedPath);
5634
+ const resolvedPath = await realpath7(requestedPath);
4224
5635
  if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
4225
5636
  return void 0;
4226
5637
  }
@@ -4233,8 +5644,35 @@ async function createSourceRegistrationService(input) {
4233
5644
  if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
4234
5645
  throw new TypeError("The source registration identity is invalid.");
4235
5646
  }
4236
- const root = await realpath6(input.root);
5647
+ const root = await realpath7(input.root);
4237
5648
  const sourceFilter = createSourceFilter(root, input.options);
5649
+ const dataFlowFilter = createDataFlowSourceFilter(root, input.options);
5650
+ const registrationQueues = /* @__PURE__ */ new Map();
5651
+ async function registerCurrentSource(sourcePath, dataFlow) {
5652
+ if (dataFlow === void 0) {
5653
+ return input.registry.register(sourcePath);
5654
+ }
5655
+ let fileId;
5656
+ const previous = registrationQueues.get(sourcePath) ?? Promise.resolve();
5657
+ const current = previous.catch(() => void 0).then(async () => {
5658
+ const currentSource = await readFile4(sourcePath, "utf8");
5659
+ if (createDataFlowSourceVersion(currentSource) !== dataFlow.sourceVersion) {
5660
+ return;
5661
+ }
5662
+ fileId = input.registry.registerDataFlowComponents(
5663
+ sourcePath,
5664
+ dataFlow.sourceVersion,
5665
+ dataFlow.components
5666
+ );
5667
+ });
5668
+ registrationQueues.set(sourcePath, current);
5669
+ await current.finally(() => {
5670
+ if (registrationQueues.get(sourcePath) === current) {
5671
+ registrationQueues.delete(sourcePath);
5672
+ }
5673
+ });
5674
+ return fileId;
5675
+ }
4238
5676
  const handler = (request, response) => {
4239
5677
  const handle = async () => {
4240
5678
  const contentType = getSingleHeader3(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
@@ -4242,33 +5680,35 @@ async function createSourceRegistrationService(input) {
4242
5680
  getSingleHeader3(request, INTERNAL_SECRET_HEADER),
4243
5681
  input.internalSecret
4244
5682
  )) {
4245
- writeJson3(response, 403, { ok: false });
5683
+ writeJson4(response, 403, { ok: false });
4246
5684
  return;
4247
5685
  }
4248
5686
  const parsed = registrationRequestSchema.safeParse(
4249
5687
  await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
4250
5688
  );
4251
5689
  if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
4252
- writeJson3(response, 400, { ok: false });
5690
+ writeJson4(response, 400, { ok: false });
4253
5691
  return;
4254
5692
  }
4255
5693
  const sourcePath = await resolveAuthorizedSource(
4256
5694
  root,
4257
5695
  parsed.data.resourcePath,
4258
- (absolutePath) => sourceFilter.shouldTransform(absolutePath, "<")
5696
+ (absolutePath) => parsed.data.dataFlow === void 0 ? sourceFilter.shouldTransform(absolutePath, "<") : dataFlowFilter.shouldTransform(absolutePath, "")
4259
5697
  );
4260
5698
  if (sourcePath === void 0) {
4261
- writeJson3(response, 403, { ok: false });
5699
+ writeJson4(response, 403, { ok: false });
4262
5700
  return;
4263
5701
  }
4264
- writeJson3(response, 200, {
4265
- epoch: input.registryEpoch,
4266
- fileId: input.registry.register(sourcePath)
4267
- });
5702
+ const fileId = await registerCurrentSource(sourcePath, parsed.data.dataFlow);
5703
+ if (fileId === void 0) {
5704
+ writeJson4(response, 409, { ok: false });
5705
+ return;
5706
+ }
5707
+ writeJson4(response, 200, { epoch: input.registryEpoch, fileId });
4268
5708
  };
4269
5709
  void handle().catch(() => {
4270
5710
  if (!response.headersSent) {
4271
- writeJson3(response, 400, { ok: false });
5711
+ writeJson4(response, 400, { ok: false });
4272
5712
  } else {
4273
5713
  response.destroy();
4274
5714
  }
@@ -4278,11 +5718,11 @@ async function createSourceRegistrationService(input) {
4278
5718
  }
4279
5719
 
4280
5720
  // src/session/session.ts
4281
- import { randomBytes as randomBytes8 } from "crypto";
5721
+ import { randomBytes as randomBytes10 } from "crypto";
4282
5722
  function createSession() {
4283
5723
  return Object.freeze({
4284
- id: randomBytes8(16).toString("base64url"),
4285
- token: randomBytes8(16).toString("base64url")
5724
+ id: randomBytes10(16).toString("base64url"),
5725
+ token: randomBytes10(16).toString("base64url")
4286
5726
  });
4287
5727
  }
4288
5728
 
@@ -4293,6 +5733,7 @@ var OPTION_KEYS = Object.freeze([
4293
5733
  "budget",
4294
5734
  "debug",
4295
5735
  "dataFlow",
5736
+ "contextualAsk",
4296
5737
  "editor",
4297
5738
  "enabled",
4298
5739
  "externalAgent",
@@ -4384,6 +5825,9 @@ function serializeResolvedSpotPatchOptions(options) {
4384
5825
  dataFlow: options.dataFlow.enabled ? Object.freeze({
4385
5826
  runtime: options.dataFlow.runtime
4386
5827
  }) : false,
5828
+ contextualAsk: options.contextualAsk.enabled ? Object.freeze({
5829
+ ...options.contextualAsk.defaultExecutor === void 0 ? {} : { defaultExecutor: options.contextualAsk.defaultExecutor }
5830
+ }) : false,
4387
5831
  editor: options.editor,
4388
5832
  enabled: options.enabled,
4389
5833
  externalAgent: options.externalAgent.enabled,
@@ -4436,6 +5880,33 @@ function parseDataFlow(value) {
4436
5880
  runtime: value.runtime
4437
5881
  });
4438
5882
  }
5883
+ function parseContextualAsk(value) {
5884
+ if (value === false || value === true) return value;
5885
+ if (!isRecord2(value)) {
5886
+ throw new TypeError("The SpotPatch contextual Ask transport is invalid.");
5887
+ }
5888
+ const keys = Object.keys(value);
5889
+ if (keys.length === 0) return Object.freeze({});
5890
+ if (!hasExactKeys(value, ["defaultExecutor"]) || !isRecord2(value.defaultExecutor)) {
5891
+ throw new TypeError("The SpotPatch contextual Ask transport is invalid.");
5892
+ }
5893
+ const preference = value.defaultExecutor;
5894
+ if (preference.kind === "managed-codex" && hasExactKeys(preference, ["kind"])) {
5895
+ return Object.freeze({
5896
+ defaultExecutor: Object.freeze({ kind: "managed-codex" })
5897
+ });
5898
+ }
5899
+ if (preference.kind === "configured-key" && hasExactKeys(preference, ["kind", "modelProfileId", "providerProfileId"]) && typeof preference.providerProfileId === "string" && typeof preference.modelProfileId === "string") {
5900
+ return Object.freeze({
5901
+ defaultExecutor: Object.freeze({
5902
+ kind: "configured-key",
5903
+ providerProfileId: preference.providerProfileId,
5904
+ modelProfileId: preference.modelProfileId
5905
+ })
5906
+ });
5907
+ }
5908
+ throw new TypeError("The SpotPatch contextual Ask transport is invalid.");
5909
+ }
4439
5910
  function parseSerializedSpotPatchOptions(value) {
4440
5911
  if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
4441
5912
  throw new TypeError("The SpotPatch options transport is invalid.");
@@ -4450,6 +5921,7 @@ function parseSerializedSpotPatchOptions(value) {
4450
5921
  budget: parseBudget(value.budget),
4451
5922
  debug: value.debug,
4452
5923
  dataFlow: parseDataFlow(value.dataFlow),
5924
+ contextualAsk: parseContextualAsk(value.contextualAsk),
4453
5925
  editor: value.editor,
4454
5926
  enabled: value.enabled,
4455
5927
  externalAgent: value.externalAgent,
@@ -4467,10 +5939,15 @@ function parseSerializedSpotPatchOptions(value) {
4467
5939
  }
4468
5940
  }
4469
5941
  export {
5942
+ ContextualAskError,
4470
5943
  DEFAULT_EXCLUDE,
4471
5944
  DEFAULT_OPTIONS,
4472
5945
  applyIntegrationPlan,
5946
+ captureAskReadSnapshot,
5947
+ composeContextualAskExecutors,
4473
5948
  createAgentJobManager,
5949
+ createConfiguredKeyAskExecutors,
5950
+ createContextualAskManager,
4474
5951
  createExternalHandoffService,
4475
5952
  createIntegrationFileChange,
4476
5953
  createRuntimeAiConfig,
@@ -4479,9 +5956,12 @@ export {
4479
5956
  createSourceRegistrationService,
4480
5957
  createSourceRegistry,
4481
5958
  createSpotPatchMiddleware,
5959
+ createWorkspaceActivityCoordinator,
4482
5960
  discoverProjectValidationCheck,
5961
+ handleContextualAskRequest,
4483
5962
  integrationPathExists,
4484
5963
  isLoopbackHostname,
5964
+ matchContextualAskPath,
4485
5965
  parseSerializedSpotPatchOptions,
4486
5966
  readIntegrationFile,
4487
5967
  readJsonRequestBody,