@spotpatch/dev-server 0.8.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.");
@@ -2483,6 +2533,9 @@ function createSourceRegistry(options = {}) {
2483
2533
  resolve(fileId) {
2484
2534
  return idToPath.get(fileId);
2485
2535
  },
2536
+ findRegisteredId(absolutePath) {
2537
+ return pathToId.get(normalizeAbsolutePath(absolutePath));
2538
+ },
2486
2539
  resolveDataFlowComponent(componentSourceId) {
2487
2540
  return componentAnchors.get(componentSourceId);
2488
2541
  },
@@ -2495,21 +2548,1342 @@ function createSourceRegistry(options = {}) {
2495
2548
  });
2496
2549
  }
2497
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
+
2498
3655
  // src/server/middleware.ts
2499
3656
  import {
2500
3657
  ERROR_CODES as ERROR_CODES16,
2501
3658
  SPOTPATCH_API_BASE,
2502
- SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS4,
3659
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS5,
2503
3660
  SpotPatchError as SpotPatchError16,
2504
3661
  openEditorRequestSchema,
2505
3662
  sourceContextRequestSchema
2506
3663
  } from "@spotpatch/shared";
2507
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
+
2508
3882
  // src/external-handoff/browser-http.ts
2509
3883
  import {
2510
3884
  ERROR_CODES as ERROR_CODES10,
2511
3885
  EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS5,
2512
- SPOTPATCH_ENDPOINTS,
3886
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
2513
3887
  SpotPatchError as SpotPatchError10,
2514
3888
  externalHandoffCapabilityRequestSchema,
2515
3889
  externalHandoffPublishRequestSchema,
@@ -2518,8 +3892,8 @@ import {
2518
3892
  } from "@spotpatch/shared";
2519
3893
 
2520
3894
  // src/server/annotation-authorizer.ts
2521
- import { realpath as realpath5 } from "fs/promises";
2522
- import path7 from "path";
3895
+ import { realpath as realpath6 } from "fs/promises";
3896
+ import path8 from "path";
2523
3897
  import {
2524
3898
  ERROR_CODES as ERROR_CODES9,
2525
3899
  SpotPatchError as SpotPatchError9,
@@ -2528,8 +3902,8 @@ import {
2528
3902
  } from "@spotpatch/shared";
2529
3903
 
2530
3904
  // src/server/source-context.ts
2531
- import { readFile as readFile3, realpath as realpath4 } from "fs/promises";
2532
- import path6 from "path";
3905
+ import { readFile as readFile3, realpath as realpath5 } from "fs/promises";
3906
+ import path7 from "path";
2533
3907
  import {
2534
3908
  ERROR_CODES as ERROR_CODES8,
2535
3909
  SpotPatchError as SpotPatchError8
@@ -2537,8 +3911,8 @@ import {
2537
3911
 
2538
3912
  // src/server/extract-code-context.ts
2539
3913
  import {
2540
- parseSync,
2541
- Visitor
3914
+ parseSync as parseSync2,
3915
+ Visitor as Visitor2
2542
3916
  } from "oxc-parser";
2543
3917
  function isComponentName(name) {
2544
3918
  return /^[A-Z]/u.test(name);
@@ -2622,7 +3996,7 @@ function findComponentSpan(options) {
2622
3996
  }
2623
3997
  let parseResult;
2624
3998
  try {
2625
- parseResult = parseSync(options.sourcePath, options.source, {
3999
+ parseResult = parseSync2(options.sourcePath, options.source, {
2626
4000
  sourceType: "module"
2627
4001
  });
2628
4002
  } catch {
@@ -2633,7 +4007,7 @@ function findComponentSpan(options) {
2633
4007
  }
2634
4008
  const jsxNodes = [];
2635
4009
  const components = [];
2636
- const visitor = new Visitor({
4010
+ const visitor = new Visitor2({
2637
4011
  JSXElement(node) {
2638
4012
  jsxNodes.push(node);
2639
4013
  },
@@ -2765,8 +4139,8 @@ function extractCodeContext(options) {
2765
4139
  }
2766
4140
 
2767
4141
  // src/server/source-file.ts
2768
- import { realpath as realpath3, stat as stat2 } from "fs/promises";
2769
- import path5 from "path";
4142
+ import { realpath as realpath4, stat as stat2 } from "fs/promises";
4143
+ import path6 from "path";
2770
4144
  import { ERROR_CODES as ERROR_CODES7, SpotPatchError as SpotPatchError7 } from "@spotpatch/shared";
2771
4145
  var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
2772
4146
  function isMissingFileError(error) {
@@ -2777,8 +4151,8 @@ async function assertInsideRoot(root, candidate) {
2777
4151
  let realCandidate;
2778
4152
  try {
2779
4153
  [realRoot, realCandidate] = await Promise.all([
2780
- realpath3(root),
2781
- realpath3(candidate)
4154
+ realpath4(root),
4155
+ realpath4(candidate)
2782
4156
  ]);
2783
4157
  } catch (error) {
2784
4158
  if (isMissingFileError(error)) {
@@ -2788,8 +4162,8 @@ async function assertInsideRoot(root, candidate) {
2788
4162
  }
2789
4163
  throw error;
2790
4164
  }
2791
- const relative = path5.relative(realRoot, realCandidate);
2792
- 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);
2793
4167
  if (outside) {
2794
4168
  throw new SpotPatchError7(ERROR_CODES7.SOURCE_OUTSIDE_ROOT);
2795
4169
  }
@@ -2801,7 +4175,7 @@ async function resolveSourceFile(options) {
2801
4175
  throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
2802
4176
  }
2803
4177
  const sourcePath = await assertInsideRoot(options.root, registeredPath);
2804
- if (!ALLOWED_EXTENSIONS.has(path5.extname(sourcePath).toLowerCase())) {
4178
+ if (!ALLOWED_EXTENSIONS.has(path6.extname(sourcePath).toLowerCase())) {
2805
4179
  throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
2806
4180
  }
2807
4181
  let sourceStat;
@@ -2826,7 +4200,7 @@ async function resolveSourceFile(options) {
2826
4200
 
2827
4201
  // src/server/source-context.ts
2828
4202
  function toDisplayPath(root, sourcePath) {
2829
- return path6.relative(root, sourcePath).split(path6.sep).join("/");
4203
+ return path7.relative(root, sourcePath).split(path7.sep).join("/");
2830
4204
  }
2831
4205
  async function readSourceContext(options) {
2832
4206
  const sourcePath = await resolveSourceFile({
@@ -2849,11 +4223,11 @@ async function readSourceContext(options) {
2849
4223
  if (options.request.line > lines.length) {
2850
4224
  throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
2851
4225
  }
2852
- const extension = path6.extname(sourcePath).toLowerCase();
4226
+ const extension = path7.extname(sourcePath).toLowerCase();
2853
4227
  return extractCodeContext({
2854
4228
  source,
2855
4229
  sourcePath,
2856
- relativePath: toDisplayPath(await realpath4(options.root), sourcePath),
4230
+ relativePath: toDisplayPath(await realpath5(options.root), sourcePath),
2857
4231
  language: extension === ".tsx" ? "tsx" : "jsx",
2858
4232
  line: options.request.line,
2859
4233
  column: options.request.column,
@@ -2895,7 +4269,7 @@ async function authorizeSourceRef(source, registry, root) {
2895
4269
  return compactSourceRef(source);
2896
4270
  }
2897
4271
  const sourcePath = await resolveSourceFile({ fileId: source.fileId, registry, root });
2898
- 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("/");
2899
4273
  if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
2900
4274
  throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
2901
4275
  }
@@ -2998,11 +4372,11 @@ async function authorizeAnnotation(input) {
2998
4372
  }
2999
4373
 
3000
4374
  // src/external-handoff/browser-http.ts
3001
- function matchExternalHandoffBrowserPath(path9) {
3002
- if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffCapability) return "capability";
3003
- if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffPublish) return "publish";
3004
- if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffStatus) return "status";
3005
- 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) {
3006
4380
  return "resolve-delivery";
3007
4381
  }
3008
4382
  return void 0;
@@ -3082,7 +4456,7 @@ async function handleExternalHandoffBrowserRequest(request, response, route, opt
3082
4456
  import {
3083
4457
  ERROR_CODES as ERROR_CODES11,
3084
4458
  EXTERNAL_AGENT_CONTROL_LIMITS,
3085
- SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
4459
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS3,
3086
4460
  SpotPatchError as SpotPatchError11,
3087
4461
  externalAgentControlCancelRequestSchema,
3088
4462
  externalAgentControlConnectRequestSchema,
@@ -3094,15 +4468,15 @@ import {
3094
4468
  externalAgentManagedResultSchema,
3095
4469
  externalAgentResultRequestSchema
3096
4470
  } from "@spotpatch/shared";
3097
- function matchExternalAgentBrowserPath(path9) {
3098
- if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlStatus) return "status";
3099
- if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlConnect) return "connect";
3100
- 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) {
3101
4475
  return "disconnect";
3102
4476
  }
3103
- if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlCancel) return "cancel";
3104
- if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentEvents) return "events";
3105
- 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";
3106
4480
  return void 0;
3107
4481
  }
3108
4482
  function writeEvent(response, event) {
@@ -3241,7 +4615,7 @@ function createExternalAgentBrowserController(port) {
3241
4615
  // src/server/agent-http.ts
3242
4616
  import {
3243
4617
  ERROR_CODES as ERROR_CODES12,
3244
- SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS3,
4618
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS4,
3245
4619
  SpotPatchError as SpotPatchError12,
3246
4620
  agentCapabilityRequestSchema,
3247
4621
  agentJobActionRequestSchema,
@@ -3286,21 +4660,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
3286
4660
  "reverted",
3287
4661
  "failed"
3288
4662
  ]);
3289
- function matchAgentRequestPath(path9) {
3290
- if (path9 === SPOTPATCH_ENDPOINTS3.agentCapability) {
4663
+ function matchAgentRequestPath(path10) {
4664
+ if (path10 === SPOTPATCH_ENDPOINTS4.agentCapability) {
3291
4665
  return Object.freeze({ kind: "capability" });
3292
4666
  }
3293
- if (path9 === SPOTPATCH_ENDPOINTS3.agentWorkspaceHealth) {
4667
+ if (path10 === SPOTPATCH_ENDPOINTS4.agentWorkspaceHealth) {
3294
4668
  return Object.freeze({ kind: "workspace-health" });
3295
4669
  }
3296
- if (path9 === SPOTPATCH_ENDPOINTS3.agentJobs) {
4670
+ if (path10 === SPOTPATCH_ENDPOINTS4.agentJobs) {
3297
4671
  return Object.freeze({ kind: "create-job" });
3298
4672
  }
3299
- const prefix = `${SPOTPATCH_ENDPOINTS3.agentJobs}/`;
3300
- if (!path9.startsWith(prefix)) {
4673
+ const prefix = `${SPOTPATCH_ENDPOINTS4.agentJobs}/`;
4674
+ if (!path10.startsWith(prefix)) {
3301
4675
  return void 0;
3302
4676
  }
3303
- const segments = path9.slice(prefix.length).split("/");
4677
+ const segments = path10.slice(prefix.length).split("/");
3304
4678
  const jobId = segments[0];
3305
4679
  const action = segments[1];
3306
4680
  if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
@@ -3494,13 +4868,13 @@ function detectIntegratedEditor(environment) {
3494
4868
  function editorCommand(editor) {
3495
4869
  return editor === "cursor" ? "cursor" : "code";
3496
4870
  }
3497
- var DEFAULT_DEPENDENCIES2 = Object.freeze({
4871
+ var DEFAULT_DEPENDENCIES3 = Object.freeze({
3498
4872
  environment: process.env,
3499
4873
  fallbackLauncher: launchEditor,
3500
4874
  processSpawner: (command, arguments_, options) => spawn(command, [...arguments_], options),
3501
4875
  startupGraceMs: EDITOR_STARTUP_GRACE_MS
3502
4876
  });
3503
- function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
4877
+ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES3) {
3504
4878
  return (target, configuredEditor) => {
3505
4879
  const integratedEditor = detectIntegratedEditor(dependencies.environment);
3506
4880
  const resolvedEditor = configuredEditor === "auto" ? integratedEditor : configuredEditor;
@@ -3555,7 +4929,7 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
3555
4929
  var launchConfiguredEditor = createEditorLauncher();
3556
4930
 
3557
4931
  // src/server/data-flow-http.ts
3558
- import { createHash as createHash3 } from "crypto";
4932
+ import { createHash as createHash5 } from "crypto";
3559
4933
  import {
3560
4934
  createStaticDataFlowAnalyzer
3561
4935
  } from "@spotpatch/analyzer";
@@ -3684,7 +5058,7 @@ async function handlePageDataFlowReport(request, analyzer, options) {
3684
5058
  const analyzedVersions = new Set(
3685
5059
  componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
3686
5060
  );
3687
- 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)}`;
3688
5062
  const complete = componentReports.every((report2) => report2.completeness.complete);
3689
5063
  const report = Object.freeze({
3690
5064
  schemaVersion: DATA_FLOW_SCHEMA_VERSION,
@@ -3777,6 +5151,9 @@ function parseOrigin(value) {
3777
5151
  return void 0;
3778
5152
  }
3779
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
+ }
3780
5157
  function assertRequestAuthorized(request, options) {
3781
5158
  const actualToken = getSingleHeader(request, SPOTPATCH_TOKEN_HEADER);
3782
5159
  if (!tokensMatch2(actualToken, options.sessionToken)) {
@@ -3786,10 +5163,16 @@ function assertRequestAuthorized(request, options) {
3786
5163
  const originHeader = getSingleHeader(request, "origin");
3787
5164
  const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
3788
5165
  const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
3789
- if (host === void 0 || origin === void 0) {
5166
+ if (host === void 0) {
3790
5167
  throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
3791
5168
  }
3792
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
+ }
3793
5176
  const originIsLoopback = isLoopbackHostname(origin.hostname);
3794
5177
  if (!options.allowLan) {
3795
5178
  if (!hostIsLoopback || !originIsLoopback) {
@@ -3862,7 +5245,7 @@ async function readRuntimeBootstrap(request, options) {
3862
5245
  }
3863
5246
 
3864
5247
  // src/server/middleware.ts
3865
- var STATUS_BY_ERROR = Object.freeze({
5248
+ var STATUS_BY_ERROR2 = Object.freeze({
3866
5249
  [ERROR_CODES16.INVALID_REQUEST]: 400,
3867
5250
  [ERROR_CODES16.INVALID_TOKEN]: 401,
3868
5251
  [ERROR_CODES16.ORIGIN_NOT_ALLOWED]: 403,
@@ -3918,7 +5301,7 @@ var STATUS_BY_ERROR = Object.freeze({
3918
5301
  [ERROR_CODES16.APPLY_CONFLICT]: 409,
3919
5302
  [ERROR_CODES16.INTERNAL_ERROR]: 500
3920
5303
  });
3921
- var PUBLIC_MESSAGES = Object.freeze({
5304
+ var PUBLIC_MESSAGES2 = Object.freeze({
3922
5305
  [ERROR_CODES16.INVALID_REQUEST]: "The request is invalid.",
3923
5306
  [ERROR_CODES16.INVALID_TOKEN]: "The session token is invalid.",
3924
5307
  [ERROR_CODES16.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
@@ -3974,7 +5357,7 @@ var PUBLIC_MESSAGES = Object.freeze({
3974
5357
  [ERROR_CODES16.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
3975
5358
  [ERROR_CODES16.INTERNAL_ERROR]: "The request could not be completed."
3976
5359
  });
3977
- function writeJson2(response, status, payload) {
5360
+ function writeJson3(response, status, payload) {
3978
5361
  response.statusCode = status;
3979
5362
  response.setHeader("Cache-Control", "no-store");
3980
5363
  response.setHeader("Content-Type", "application/json; charset=utf-8");
@@ -3983,16 +5366,16 @@ function writeJson2(response, status, payload) {
3983
5366
  function asSpotPatchError(error) {
3984
5367
  return error instanceof SpotPatchError16 ? error : new SpotPatchError16(ERROR_CODES16.INTERNAL_ERROR, void 0, { cause: error });
3985
5368
  }
3986
- function writeError(response, error, logger) {
5369
+ function writeError2(response, error, logger) {
3987
5370
  const normalized = asSpotPatchError(error);
3988
5371
  if (normalized.code === ERROR_CODES16.INTERNAL_ERROR) {
3989
5372
  logger?.warn("[spotpatch:server] Internal request failure.");
3990
5373
  }
3991
- writeJson2(response, STATUS_BY_ERROR[normalized.code], {
5374
+ writeJson3(response, STATUS_BY_ERROR2[normalized.code], {
3992
5375
  ok: false,
3993
5376
  error: {
3994
5377
  code: normalized.code,
3995
- message: PUBLIC_MESSAGES[normalized.code]
5378
+ message: PUBLIC_MESSAGES2[normalized.code]
3996
5379
  }
3997
5380
  });
3998
5381
  }
@@ -4048,44 +5431,55 @@ function createSpotPatchMiddleware(options) {
4048
5431
  const dataFlowAnalyzer = createDataFlowAnalyzer(options);
4049
5432
  const externalAgentController = options.externalAgentControl === void 0 ? void 0 : createExternalAgentBrowserController(options.externalAgentControl);
4050
5433
  const middleware = (request, response, next) => {
4051
- const path9 = requestPath(request);
4052
- const agentRoute = matchAgentRequestPath(path9);
4053
- const externalAgentRoute = matchExternalAgentBrowserPath(path9);
4054
- const externalHandoffRoute = matchExternalHandoffBrowserPath(path9);
4055
- 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}/`)) {
4056
5440
  next();
4057
5441
  return;
4058
5442
  }
4059
5443
  const handle = async () => {
4060
- if (path9 === SPOTPATCH_ENDPOINTS4.bootstrap && bootstrap !== void 0) {
5444
+ if (path10 === SPOTPATCH_ENDPOINTS5.bootstrap && bootstrap !== void 0) {
4061
5445
  const data = await readRuntimeBootstrap(
4062
5446
  request,
4063
5447
  bootstrap
4064
5448
  );
4065
- writeJson2(response, 200, { ok: true, data });
5449
+ writeJson3(response, 200, { ok: true, data });
4066
5450
  return;
4067
5451
  }
4068
5452
  assertRequestAuthorized(request, {
4069
5453
  allowLan: options.options.allowLan,
4070
5454
  sessionToken: options.session.token
4071
5455
  });
4072
- 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) {
4073
5467
  if (request.method !== "POST") {
4074
5468
  throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
4075
5469
  }
4076
5470
  const data = await handleSourceContext(request, options);
4077
- writeJson2(response, 200, { ok: true, data });
5471
+ writeJson3(response, 200, { ok: true, data });
4078
5472
  return;
4079
5473
  }
4080
- if (path9 === SPOTPATCH_ENDPOINTS4.openEditor) {
5474
+ if (path10 === SPOTPATCH_ENDPOINTS5.openEditor) {
4081
5475
  if (request.method !== "POST") {
4082
5476
  throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
4083
5477
  }
4084
5478
  const data = await handleOpenEditor(request, options);
4085
- writeJson2(response, 200, { ok: true, data });
5479
+ writeJson3(response, 200, { ok: true, data });
4086
5480
  return;
4087
5481
  }
4088
- if (path9 === SPOTPATCH_ENDPOINTS4.dataFlowComponentReport) {
5482
+ if (path10 === SPOTPATCH_ENDPOINTS5.dataFlowComponentReport) {
4089
5483
  if (request.method !== "POST") {
4090
5484
  throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
4091
5485
  }
@@ -4094,15 +5488,15 @@ function createSpotPatchMiddleware(options) {
4094
5488
  dataFlowAnalyzer,
4095
5489
  options
4096
5490
  );
4097
- writeJson2(response, 200, { ok: true, data });
5491
+ writeJson3(response, 200, { ok: true, data });
4098
5492
  return;
4099
5493
  }
4100
- if (path9 === SPOTPATCH_ENDPOINTS4.dataFlowPageReport) {
5494
+ if (path10 === SPOTPATCH_ENDPOINTS5.dataFlowPageReport) {
4101
5495
  if (request.method !== "POST") {
4102
5496
  throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
4103
5497
  }
4104
5498
  const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
4105
- writeJson2(response, 200, { ok: true, data });
5499
+ writeJson3(response, 200, { ok: true, data });
4106
5500
  return;
4107
5501
  }
4108
5502
  if (externalAgentRoute !== void 0) {
@@ -4114,7 +5508,7 @@ function createSpotPatchMiddleware(options) {
4114
5508
  response,
4115
5509
  externalAgentRoute,
4116
5510
  (target, status, data) => {
4117
- writeJson2(target, status, { ok: true, data });
5511
+ writeJson3(target, status, { ok: true, data });
4118
5512
  }
4119
5513
  );
4120
5514
  return;
@@ -4131,7 +5525,7 @@ function createSpotPatchMiddleware(options) {
4131
5525
  ...options.externalHandoffService === void 0 ? {} : { service: options.externalHandoffService }
4132
5526
  },
4133
5527
  (target, status, data) => {
4134
- writeJson2(target, status, { ok: true, data });
5528
+ writeJson3(target, status, { ok: true, data });
4135
5529
  }
4136
5530
  );
4137
5531
  return;
@@ -4145,12 +5539,12 @@ function createSpotPatchMiddleware(options) {
4145
5539
  options,
4146
5540
  agentRoute,
4147
5541
  (target, status, data) => {
4148
- writeJson2(target, status, { ok: true, data });
5542
+ writeJson3(target, status, { ok: true, data });
4149
5543
  }
4150
5544
  );
4151
5545
  };
4152
5546
  void handle().catch((error) => {
4153
- writeError(response, error, options.logger);
5547
+ writeError2(response, error, options.logger);
4154
5548
  });
4155
5549
  };
4156
5550
  return Object.assign(middleware, {
@@ -4162,8 +5556,8 @@ function createSpotPatchMiddleware(options) {
4162
5556
 
4163
5557
  // src/server/source-registration.ts
4164
5558
  import { timingSafeEqual as timingSafeEqual3 } from "crypto";
4165
- import { lstat as lstat4, readFile as readFile4, realpath as realpath6 } from "fs/promises";
4166
- import path8 from "path";
5559
+ import { lstat as lstat5, readFile as readFile4, realpath as realpath7 } from "fs/promises";
5560
+ import path9 from "path";
4167
5561
  import {
4168
5562
  createDataFlowSourceFilter,
4169
5563
  createDataFlowSourceVersion,
@@ -4203,13 +5597,13 @@ function identitiesMatch(actual, expected) {
4203
5597
  return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual3(actualBytes, expectedBytes);
4204
5598
  }
4205
5599
  function isWithinRoot(root, candidate) {
4206
- const relative = path8.relative(root, candidate);
4207
- 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);
4208
5602
  }
4209
5603
  function hasForbiddenSegment(root, candidate) {
4210
- 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));
4211
5605
  }
4212
- function writeJson3(response, statusCode, payload) {
5606
+ function writeJson4(response, statusCode, payload) {
4213
5607
  const body = JSON.stringify(payload);
4214
5608
  response.statusCode = statusCode;
4215
5609
  response.setHeader("Cache-Control", "no-store");
@@ -4229,15 +5623,15 @@ function requestComesFromLoopbackWorker(request) {
4229
5623
  }
4230
5624
  }
4231
5625
  async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
4232
- if (!path8.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
5626
+ if (!path9.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
4233
5627
  return void 0;
4234
5628
  }
4235
5629
  try {
4236
- const sourceStat = await lstat4(requestedPath);
5630
+ const sourceStat = await lstat5(requestedPath);
4237
5631
  if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
4238
5632
  return void 0;
4239
5633
  }
4240
- const resolvedPath = await realpath6(requestedPath);
5634
+ const resolvedPath = await realpath7(requestedPath);
4241
5635
  if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
4242
5636
  return void 0;
4243
5637
  }
@@ -4250,7 +5644,7 @@ async function createSourceRegistrationService(input) {
4250
5644
  if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
4251
5645
  throw new TypeError("The source registration identity is invalid.");
4252
5646
  }
4253
- const root = await realpath6(input.root);
5647
+ const root = await realpath7(input.root);
4254
5648
  const sourceFilter = createSourceFilter(root, input.options);
4255
5649
  const dataFlowFilter = createDataFlowSourceFilter(root, input.options);
4256
5650
  const registrationQueues = /* @__PURE__ */ new Map();
@@ -4286,14 +5680,14 @@ async function createSourceRegistrationService(input) {
4286
5680
  getSingleHeader3(request, INTERNAL_SECRET_HEADER),
4287
5681
  input.internalSecret
4288
5682
  )) {
4289
- writeJson3(response, 403, { ok: false });
5683
+ writeJson4(response, 403, { ok: false });
4290
5684
  return;
4291
5685
  }
4292
5686
  const parsed = registrationRequestSchema.safeParse(
4293
5687
  await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
4294
5688
  );
4295
5689
  if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
4296
- writeJson3(response, 400, { ok: false });
5690
+ writeJson4(response, 400, { ok: false });
4297
5691
  return;
4298
5692
  }
4299
5693
  const sourcePath = await resolveAuthorizedSource(
@@ -4302,19 +5696,19 @@ async function createSourceRegistrationService(input) {
4302
5696
  (absolutePath) => parsed.data.dataFlow === void 0 ? sourceFilter.shouldTransform(absolutePath, "<") : dataFlowFilter.shouldTransform(absolutePath, "")
4303
5697
  );
4304
5698
  if (sourcePath === void 0) {
4305
- writeJson3(response, 403, { ok: false });
5699
+ writeJson4(response, 403, { ok: false });
4306
5700
  return;
4307
5701
  }
4308
5702
  const fileId = await registerCurrentSource(sourcePath, parsed.data.dataFlow);
4309
5703
  if (fileId === void 0) {
4310
- writeJson3(response, 409, { ok: false });
5704
+ writeJson4(response, 409, { ok: false });
4311
5705
  return;
4312
5706
  }
4313
- writeJson3(response, 200, { epoch: input.registryEpoch, fileId });
5707
+ writeJson4(response, 200, { epoch: input.registryEpoch, fileId });
4314
5708
  };
4315
5709
  void handle().catch(() => {
4316
5710
  if (!response.headersSent) {
4317
- writeJson3(response, 400, { ok: false });
5711
+ writeJson4(response, 400, { ok: false });
4318
5712
  } else {
4319
5713
  response.destroy();
4320
5714
  }
@@ -4324,11 +5718,11 @@ async function createSourceRegistrationService(input) {
4324
5718
  }
4325
5719
 
4326
5720
  // src/session/session.ts
4327
- import { randomBytes as randomBytes8 } from "crypto";
5721
+ import { randomBytes as randomBytes10 } from "crypto";
4328
5722
  function createSession() {
4329
5723
  return Object.freeze({
4330
- id: randomBytes8(16).toString("base64url"),
4331
- token: randomBytes8(16).toString("base64url")
5724
+ id: randomBytes10(16).toString("base64url"),
5725
+ token: randomBytes10(16).toString("base64url")
4332
5726
  });
4333
5727
  }
4334
5728
 
@@ -4339,6 +5733,7 @@ var OPTION_KEYS = Object.freeze([
4339
5733
  "budget",
4340
5734
  "debug",
4341
5735
  "dataFlow",
5736
+ "contextualAsk",
4342
5737
  "editor",
4343
5738
  "enabled",
4344
5739
  "externalAgent",
@@ -4430,6 +5825,9 @@ function serializeResolvedSpotPatchOptions(options) {
4430
5825
  dataFlow: options.dataFlow.enabled ? Object.freeze({
4431
5826
  runtime: options.dataFlow.runtime
4432
5827
  }) : false,
5828
+ contextualAsk: options.contextualAsk.enabled ? Object.freeze({
5829
+ ...options.contextualAsk.defaultExecutor === void 0 ? {} : { defaultExecutor: options.contextualAsk.defaultExecutor }
5830
+ }) : false,
4433
5831
  editor: options.editor,
4434
5832
  enabled: options.enabled,
4435
5833
  externalAgent: options.externalAgent.enabled,
@@ -4482,6 +5880,33 @@ function parseDataFlow(value) {
4482
5880
  runtime: value.runtime
4483
5881
  });
4484
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
+ }
4485
5910
  function parseSerializedSpotPatchOptions(value) {
4486
5911
  if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
4487
5912
  throw new TypeError("The SpotPatch options transport is invalid.");
@@ -4496,6 +5921,7 @@ function parseSerializedSpotPatchOptions(value) {
4496
5921
  budget: parseBudget(value.budget),
4497
5922
  debug: value.debug,
4498
5923
  dataFlow: parseDataFlow(value.dataFlow),
5924
+ contextualAsk: parseContextualAsk(value.contextualAsk),
4499
5925
  editor: value.editor,
4500
5926
  enabled: value.enabled,
4501
5927
  externalAgent: value.externalAgent,
@@ -4513,10 +5939,15 @@ function parseSerializedSpotPatchOptions(value) {
4513
5939
  }
4514
5940
  }
4515
5941
  export {
5942
+ ContextualAskError,
4516
5943
  DEFAULT_EXCLUDE,
4517
5944
  DEFAULT_OPTIONS,
4518
5945
  applyIntegrationPlan,
5946
+ captureAskReadSnapshot,
5947
+ composeContextualAskExecutors,
4519
5948
  createAgentJobManager,
5949
+ createConfiguredKeyAskExecutors,
5950
+ createContextualAskManager,
4520
5951
  createExternalHandoffService,
4521
5952
  createIntegrationFileChange,
4522
5953
  createRuntimeAiConfig,
@@ -4525,9 +5956,12 @@ export {
4525
5956
  createSourceRegistrationService,
4526
5957
  createSourceRegistry,
4527
5958
  createSpotPatchMiddleware,
5959
+ createWorkspaceActivityCoordinator,
4528
5960
  discoverProjectValidationCheck,
5961
+ handleContextualAskRequest,
4529
5962
  integrationPathExists,
4530
5963
  isLoopbackHostname,
5964
+ matchContextualAskPath,
4531
5965
  parseSerializedSpotPatchOptions,
4532
5966
  readIntegrationFile,
4533
5967
  readJsonRequestBody,