@clue-ai/cli 0.0.14 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,16 +62,21 @@ leaks, and SDK lifecycle presence when requested. With
62
62
  installation, SDK imports in the target environments, app startup, and event
63
63
  delivery remain required before setup can be called complete.
64
64
 
65
- `npx -y @clue-ai/cli setup-watch` polls the Clue API setup-check endpoint while you operate
66
- the local service. `--clue-api-base-url` is the Clue API URL, not the customer
67
- frontend URL. Use `--watch-targets` to list every local frontend/backend app by
68
- service key and the lifecycle checks expected for that service, for example
65
+ `npx -y @clue-ai/cli setup-watch` polls the Clue API setup-check endpoint in
66
+ remote mode while you operate the service. `--clue-api-base-url` is the Clue API
67
+ URL, not the customer frontend URL. Use `--watch-targets` to list every
68
+ frontend/backend producer by service key and the lifecycle checks expected for
69
+ that service, for example
69
70
  `frontend:web[init,identify,set-account,event-sent]=<frontend-url>,backend:api[init,identify,set-account,logout,event-sent]=<backend-url>`.
70
71
  Do not assume localhost ports; use the actual URLs printed by the repository's
71
72
  dev scripts or configured in its local env.
72
73
  In `--local` mode, the watcher stays open until the expected lifecycle checks
73
- pass or you stop it with Ctrl+C. It prints a per-service Clue lifecycle
74
- checklist and only prints a new snapshot when the observed state changes.
74
+ pass or you stop it with Ctrl+C. The frontend/backend endpoint URLs printed by
75
+ the command are local Clue receiver endpoints; configure the customer's Clue SDK
76
+ ingest endpoint to those receiver URLs while testing. Local mode does not ask
77
+ for or health-check customer service URLs. It prints a per-service Clue
78
+ lifecycle checklist and only prints a new snapshot when the observed state
79
+ changes.
75
80
 
76
81
  `npx -y @clue-ai/cli setup` reads Clue values from the setup screen flags, detects local
77
82
  services, writes `.clue/setup-manifest.json`, and prints service-specific env
package/bin/clue-cli.mjs CHANGED
@@ -304,7 +304,18 @@ const maybeProtectEnvironmentGuide = async ({
304
304
  };
305
305
  };
306
306
 
307
- const confirmTargetUrls = async ({ flags, watchTargets, env }) => {
307
+ const clearWatchTargetUrls = (watchTargets) =>
308
+ watchTargets.map((target) => ({
309
+ ...target,
310
+ url: null,
311
+ urlEnvName: null,
312
+ localUrlCandidates: [],
313
+ }));
314
+
315
+ const confirmTargetUrls = async ({ flags, localMode, watchTargets, env }) => {
316
+ if (localMode) {
317
+ return clearWatchTargetUrls(watchTargets);
318
+ }
308
319
  const initialTargets = watchTargets.map((target) => ({
309
320
  ...target,
310
321
  url: resolveTargetUrlFromEnv({ target, env }),
@@ -370,14 +381,20 @@ const checkTargetUrl = async (url) => {
370
381
  }
371
382
  };
372
383
 
373
- const evaluateWatchTargets = async ({ latest, watchTargets }) => {
384
+ const evaluateWatchTargets = async ({
385
+ latest,
386
+ requireTargetUrl = true,
387
+ watchTargets,
388
+ }) => {
374
389
  const producers = Array.isArray(latest?.producers) ? latest.producers : [];
375
390
  return Promise.all(
376
391
  watchTargets.map(async (target) => {
377
392
  const producer = producers.find(
378
393
  (entry) => entry.id === target.producerId,
379
394
  );
380
- const urlHealth = await checkTargetUrl(target.url);
395
+ const urlHealth = requireTargetUrl
396
+ ? await checkTargetUrl(target.url)
397
+ : { checked: false, reachable: true, status: null };
381
398
  const lifecycleStatus = {
382
399
  init: Boolean(producer?.clueInit ?? producer?.sdkInitialized),
383
400
  identify: Boolean(producer?.clueIdentify),
@@ -411,7 +428,7 @@ const evaluateWatchTargets = async ({ latest, watchTargets }) => {
411
428
  urlChecked: urlHealth.checked,
412
429
  urlReachable: urlHealth.reachable,
413
430
  urlStatus: urlHealth.status,
414
- passed: producerPassed && urlHealth.reachable,
431
+ passed: producerPassed && (!requireTargetUrl || urlHealth.reachable),
415
432
  };
416
433
  }),
417
434
  );
@@ -686,6 +703,7 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
686
703
  const explicitWatchTargets = parseWatchTargets(flags.get("watch-targets"));
687
704
  const watchTargets = await confirmTargetUrls({
688
705
  flags,
706
+ localMode,
689
707
  watchTargets:
690
708
  explicitWatchTargets.length > 0
691
709
  ? explicitWatchTargets
@@ -739,6 +757,7 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
739
757
  });
740
758
  const targetChecks = await evaluateWatchTargets({
741
759
  latest,
760
+ requireTargetUrl: false,
742
761
  watchTargets,
743
762
  });
744
763
  const targetChecksPassed =
@@ -829,7 +848,7 @@ const usage = () =>
829
848
  "",
830
849
  "Usage:",
831
850
  " /clue-init",
832
- ` ${clueCliCommand("setup --clue-api-key <key> --clue-api-base-url <url> --project-key <key> --environment dev")}`,
851
+ ` ${clueCliCommand("setup --clue-api-key <key> --clue-api-base-url <url> --project-key <key> --environment dev --documents-url <url>")}`,
833
852
  ` ${clueCliCommand("setup-detect --repo .")}`,
834
853
  ` ${clueCliCommand("semantic-inventory --framework fastapi --backend-root-path backend --repo .")}`,
835
854
  ` ${clueCliCommand("semantic-agent-skills --output .clue/semantic-agent-skills.json")}`,
@@ -903,6 +922,7 @@ const main = async () => {
903
922
  if (command === "setup") {
904
923
  const report = await installSetupSkills({
905
924
  repoRoot,
925
+ documentsUrl: flags.get("documents-url"),
906
926
  target:
907
927
  typeof flags.get("target") === "string"
908
928
  ? flags.get("target")
@@ -924,12 +944,13 @@ const main = async () => {
924
944
  repoRoot,
925
945
  target: report.target,
926
946
  skillRoot: report.skill_root,
927
- setupContext: {
928
- clueApiKey: flags.get("clue-api-key"),
929
- clueApiBaseUrl: flags.get("clue-api-base-url"),
930
- projectKey: flags.get("project-key"),
931
- environment: flags.get("environment"),
932
- },
947
+ setupContext: {
948
+ clueApiKey: flags.get("clue-api-key"),
949
+ clueApiBaseUrl: flags.get("clue-api-base-url"),
950
+ documentsUrl: flags.get("documents-url"),
951
+ projectKey: flags.get("project-key"),
952
+ environment: flags.get("environment"),
953
+ },
933
954
  });
934
955
  const environmentFileProtection =
935
956
  preEnvironmentFileProtection?.env_file_path ===
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clue-ai/cli",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "clue-ai": "bin/clue-cli.mjs"
@@ -575,7 +575,10 @@ const buildLifecyclePrompt = ({ request, files }) =>
575
575
  "Never place CLUE_API_KEY in frontend code, frontend env files, browser bundles, or client-readable config.",
576
576
  "When browser SDK ingest is configured, implement a backend-owned browser token endpoint that reads server-side CLUE_API_KEY and requests POST /api/v1/ingest/browser-tokens from Clue.",
577
577
  "Configure frontend ClueInit with browserTokenProvider that calls the local backend token endpoint and returns the token string.",
578
+ "The local backend token endpoint is part of the customer app, not the Clue API. It may use the customer's route convention, but it must call Clue server-side at /api/v1/ingest/browser-tokens.",
579
+ "The frontend browserTokenProvider must send the same service key used by ClueInit to the customer backend token endpoint. For Next.js this value comes from NEXT_PUBLIC_CLUE_SERVICE_KEY.",
578
580
  "The browser token request must include project key, environment, service key, and the current browser origin; the backend must attach x-clue-api-key server-side when calling Clue.",
581
+ "For browser token proxy code, the service key sent to Clue must be the frontend ClueInit serviceKey from the browser request, not the backend service's CLUE_SERVICE_KEY.",
579
582
  "If a backend-owned browser token endpoint is implemented, read CLUE_API_BASE_URL from the backend env block and normalize it so values with or without a trailing /api/v1 do not produce duplicate paths.",
580
583
  "Do not add @clue-ai/browser-sdk or backend SDK dependencies with * or latest; use a concrete published version or package-manager-resolved semver range and update the repository lockfile when one exists.",
581
584
  "Prefer minimal edits that engineers can review in one PR.",
@@ -845,9 +845,23 @@ const semanticSnapshotRequestSchema = zod_1.z
845
845
  ], "semantic_meaning_candidates[].selection_ai_inference_evidence_ref");
846
846
  });
847
847
  });
848
+ const semanticSnapshotResponseSchema = zod_1.z.object({
849
+ accepted: zod_1.z.literal(true),
850
+ duplicate: zod_1.z.boolean(),
851
+ semantic_snapshot_id: nonEmptyStringSchema,
852
+ route_count: zod_1.z.number().int().nonnegative(),
853
+ diff_summary: zod_1.z.object({
854
+ added_routes: zod_1.z.number().int().nonnegative(),
855
+ removed_routes: zod_1.z.number().int().nonnegative(),
856
+ changed_routes: zod_1.z.number().int().nonnegative(),
857
+ unchanged_routes: zod_1.z.number().int().nonnegative(),
858
+ low_confidence_routes: zod_1.z.number().int().nonnegative(),
859
+ }),
860
+ });
848
861
 
849
862
  return {
850
863
  semanticSnapshotRequestSchema,
864
+ semanticSnapshotResponseSchema,
851
865
  };
852
866
  })();
853
867
 
@@ -855,6 +869,7 @@ const schemaPackage = {
855
869
  clueInitToolRequestSchema: toolingSchemas.clueInitToolRequestSchema,
856
870
  clueInitToolReportSchema: toolingSchemas.clueInitToolReportSchema,
857
871
  semanticSnapshotRequestSchema: semanticSchemas.semanticSnapshotRequestSchema,
872
+ semanticSnapshotResponseSchema: semanticSchemas.semanticSnapshotResponseSchema,
858
873
  };
859
874
 
860
875
  module.exports = schemaPackage;
@@ -706,7 +706,7 @@ const FORBIDDEN_TARGET_TEXT_PATTERNS = [
706
706
  /\b[A-Za-z0-9_./-]+\.py\b/i,
707
707
  /\b(from\s+[A-Za-z_][A-Za-z0-9_.]*\s+import|import\s+[A-Za-z_][A-Za-z0-9_.]*)\b/i,
708
708
  /\b(class|def)\s+[A-Za-z_][A-Za-z0-9_]*/,
709
- /\b(select|insert|update|delete)\s+.+\b(from|into|set)\b/i,
709
+ /\b(?:select\s+(?:\*|[a-z0-9_.,\s]+)\s+from|insert\s+into|update\s+[a-z0-9_."-]+\s+set|delete\s+from)\b/i,
710
710
  /\b(system|user|assistant)\s*:\s*.+\b(prompt|completion|transcript)\b/i,
711
711
  /\b[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY)\b(?:\s*=\s*["']?[^"'\s,;}]+)?/,
712
712
  /\b[A-Z][A-Z0-9_]{2,}\s*=\s*["']?[^"'\s,;}]+/,
@@ -1358,6 +1358,15 @@ const unresolvedEffect = ({
1358
1358
  .map((entry) => sanitizeText(entry.trim(), route)),
1359
1359
  });
1360
1360
 
1361
+ const findCatalogEntryByTargetObjectKey = (catalogByGroup, targetObjectKey) => {
1362
+ for (const [mapKey, entry] of catalogByGroup.entries()) {
1363
+ if (entry?.target_object_key === targetObjectKey) {
1364
+ return { mapKey, entry };
1365
+ }
1366
+ }
1367
+ return null;
1368
+ };
1369
+
1361
1370
  const buildOperationAssignmentCollections = ({
1362
1371
  route,
1363
1372
  aiRoute,
@@ -1535,7 +1544,15 @@ const buildOperationAssignmentCollections = ({
1535
1544
  }
1536
1545
 
1537
1546
  const groupKey = `${concept.toLowerCase()}::${businessRole.toLowerCase()}`;
1538
- const existingCatalog = catalogByGroup.get(groupKey);
1547
+ const existingCatalogByTarget = findCatalogEntryByTargetObjectKey(
1548
+ catalogByGroup,
1549
+ targetKeyCandidate,
1550
+ );
1551
+ const existingCatalog =
1552
+ catalogByGroup.get(groupKey) ?? existingCatalogByTarget?.entry;
1553
+ const catalogMapKey = catalogByGroup.has(groupKey)
1554
+ ? groupKey
1555
+ : (existingCatalogByTarget?.mapKey ?? groupKey);
1539
1556
  const targetObjectKey =
1540
1557
  existingCatalog?.target_object_key ?? targetKeyCandidate;
1541
1558
  const operationEffectKey = `${targetObjectKey}.${effectAction}`;
@@ -1706,7 +1723,7 @@ const buildOperationAssignmentCollections = ({
1706
1723
  catalogEntry.grouping_evidence_refs.push(ref);
1707
1724
  }
1708
1725
  }
1709
- catalogByGroup.set(groupKey, catalogEntry);
1726
+ catalogByGroup.set(catalogMapKey, catalogEntry);
1710
1727
 
1711
1728
  mappings.push({
1712
1729
  id: mappingId,
@@ -1991,7 +2008,7 @@ const sanitizeText = (value, route) => {
1991
2008
  }
1992
2009
  result = result
1993
2010
  .replace(
1994
- /\b(select|insert|update|delete)\s+.+?\b(from|into|set)\b[^"',;}]+/gi,
2011
+ /\b(?:select\s+(?:\*|[a-z0-9_.,\s]+)\s+from|insert\s+into|update\s+[a-z0-9_."-]+\s+set|delete\s+from)\b[^"',;}]+/gi,
1995
2012
  "[sql]",
1996
2013
  )
1997
2014
  .replace(
@@ -2036,25 +2053,80 @@ const sanitizeSemantics = (value, route) => {
2036
2053
  record && typeof record === "object" && !Array.isArray(record)
2037
2054
  ? record
2038
2055
  : {};
2056
+ const routeConfidence = Number.isFinite(Number(safeRecord.route_confidence))
2057
+ ? Math.max(0, Math.min(1, Number(safeRecord.route_confidence)))
2058
+ : 0;
2039
2059
  return {
2040
2060
  route_summary:
2041
2061
  typeof safeRecord.route_summary === "string" &&
2042
2062
  safeRecord.route_summary.trim()
2043
2063
  ? safeRecord.route_summary
2044
2064
  : "unknown",
2045
- action_candidates: Array.isArray(safeRecord.action_candidates)
2046
- ? safeRecord.action_candidates
2047
- : [],
2048
- outcome_candidates: Array.isArray(safeRecord.outcome_candidates)
2049
- ? safeRecord.outcome_candidates
2050
- : [],
2065
+ action_candidates: sanitizeSemanticCandidates(
2066
+ safeRecord.action_candidates,
2067
+ route,
2068
+ routeConfidence,
2069
+ ),
2070
+ outcome_candidates: sanitizeSemanticCandidates(
2071
+ safeRecord.outcome_candidates,
2072
+ route,
2073
+ routeConfidence,
2074
+ ),
2051
2075
  value_event_candidates: [],
2052
- route_confidence: Number.isFinite(Number(safeRecord.route_confidence))
2053
- ? Math.max(0, Math.min(1, Number(safeRecord.route_confidence)))
2054
- : 0,
2076
+ route_confidence: routeConfidence,
2055
2077
  };
2056
2078
  };
2057
2079
 
2080
+ const sanitizeSemanticCandidates = (value, route, routeConfidence) => {
2081
+ if (!Array.isArray(value)) {
2082
+ return [];
2083
+ }
2084
+ return value
2085
+ .map((entry) => {
2086
+ if (typeof entry === "string") {
2087
+ const label = sanitizeText(entry, route).trim();
2088
+ return label
2089
+ ? {
2090
+ label,
2091
+ confidence: routeConfidence > 0 ? routeConfidence : 0.5,
2092
+ }
2093
+ : null;
2094
+ }
2095
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
2096
+ return null;
2097
+ }
2098
+ const label =
2099
+ typeof entry.label === "string" && entry.label.trim()
2100
+ ? sanitizeText(entry.label, route).trim()
2101
+ : "";
2102
+ if (!label) {
2103
+ return null;
2104
+ }
2105
+ const candidate = {
2106
+ label,
2107
+ confidence: Number.isFinite(Number(entry.confidence))
2108
+ ? Math.max(0, Math.min(1, Number(entry.confidence)))
2109
+ : routeConfidence > 0
2110
+ ? routeConfidence
2111
+ : 0.5,
2112
+ };
2113
+ for (const key of [
2114
+ "subject_type",
2115
+ "action_category",
2116
+ "outcome_kind",
2117
+ "value_kind",
2118
+ ]) {
2119
+ if (typeof entry[key] === "string" && entry[key].trim()) {
2120
+ candidate[key] = sanitizeText(entry[key], route).trim();
2121
+ } else if (entry[key] === null) {
2122
+ candidate[key] = null;
2123
+ }
2124
+ }
2125
+ return candidate;
2126
+ })
2127
+ .filter(Boolean);
2128
+ };
2129
+
2058
2130
  const sanitizeForClueStorage = (value, route) => {
2059
2131
  if (typeof value === "string") {
2060
2132
  return sanitizeText(value, route);
@@ -2152,6 +2224,10 @@ const previousRouteMap = (previousSnapshot) =>
2152
2224
  ]),
2153
2225
  );
2154
2226
 
2227
+ const hasReusablePreviousRouteSemantics = (route) =>
2228
+ Number(route?.semantics?.route_confidence ?? 0) > 0 &&
2229
+ route?.semantics?.route_summary !== "unknown";
2230
+
2155
2231
  const buildRouteEvidencePromptEntry = ({ route, hashScope }) => ({
2156
2232
  operation_source_key: route.operation_source_key,
2157
2233
  method: route.method,
@@ -2192,7 +2268,10 @@ const classifyRoutesForSnapshot = async ({
2192
2268
  continue;
2193
2269
  }
2194
2270
 
2195
- if (previousRoute.route_input_hash === currentHash) {
2271
+ if (
2272
+ previousRoute.route_input_hash === currentHash &&
2273
+ hasReusablePreviousRouteSemantics(previousRoute)
2274
+ ) {
2196
2275
  plans.set(route.operation_source_key, {
2197
2276
  origin: "unchanged_route_reused",
2198
2277
  route_input_hash: currentHash,
@@ -2206,6 +2285,21 @@ const classifyRoutesForSnapshot = async ({
2206
2285
  continue;
2207
2286
  }
2208
2287
 
2288
+ if (previousRoute.route_input_hash === currentHash) {
2289
+ plans.set(route.operation_source_key, {
2290
+ origin: "changed_route_semantic_regenerated",
2291
+ route_input_hash: currentHash,
2292
+ previous_route: previousRoute,
2293
+ previous_route_input_hash: previousRoute.route_input_hash,
2294
+ previous_route_semantic_hash:
2295
+ previousRoute.route_semantic_hash ?? routeSemanticHash(previousRoute),
2296
+ semantic_change_reason:
2297
+ "Previous route semantics had no reusable confidence, so the route was regenerated.",
2298
+ });
2299
+ routesRequiringGeneration.push(route);
2300
+ continue;
2301
+ }
2302
+
2209
2303
  const decision = await callAiReuseDecisionProvider({
2210
2304
  request,
2211
2305
  env,
@@ -2483,7 +2577,8 @@ const FORBIDDEN_CODE_STRUCTURE_PATTERNS = [
2483
2577
  },
2484
2578
  {
2485
2579
  label: "raw sql",
2486
- pattern: /\b(select|insert|update|delete)\s+.+\b(from|into|set)\b/i,
2580
+ pattern:
2581
+ /\b(?:select\s+(?:\*|[a-z0-9_.,\s]+)\s+from|insert\s+into|update\s+[a-z0-9_."-]+\s+set|delete\s+from)\b/i,
2487
2582
  },
2488
2583
  {
2489
2584
  label: "raw prompt",
@@ -2627,6 +2722,9 @@ const fetchLatestSnapshot = async ({ request, env }) => {
2627
2722
  );
2628
2723
  }
2629
2724
  const body = await response.json();
2725
+ if (body?.found === false && body?.snapshot === undefined) {
2726
+ return null;
2727
+ }
2630
2728
  const candidate = body?.snapshot ?? body;
2631
2729
  if (!candidate || !Array.isArray(candidate.routes)) {
2632
2730
  if (allowFullRegeneration) {
@@ -2664,7 +2762,16 @@ const sendSnapshot = async ({ request, env, snapshot }) => {
2664
2762
  body: JSON.stringify(validatedSnapshot),
2665
2763
  });
2666
2764
  if (!response.ok) {
2667
- throw new Error(`Clue semantic snapshot upload failed: ${response.status}`);
2765
+ const responseBody =
2766
+ typeof response.text === "function"
2767
+ ? await response.text().catch(() => "")
2768
+ : "";
2769
+ const responseDetail = responseBody.trim()
2770
+ ? `: ${responseBody.trim().slice(0, 1000)}`
2771
+ : "";
2772
+ throw new Error(
2773
+ `Clue semantic snapshot upload failed: ${response.status}${responseDetail}`,
2774
+ );
2668
2775
  }
2669
2776
  const body = await response.json();
2670
2777
  try {
@@ -2782,7 +2889,11 @@ export const runSemanticCi = async ({
2782
2889
  const previousRoutes = previousRouteMap(previousSnapshot);
2783
2890
  const routeNeedsAi = routes.some((route) => {
2784
2891
  const previousRoute = previousRoutes.get(route.operation_source_key);
2785
- return !previousRoute || previousRoute.route_input_hash !== routeInputHash(route);
2892
+ return (
2893
+ !previousRoute ||
2894
+ previousRoute.route_input_hash !== routeInputHash(route) ||
2895
+ !hasReusablePreviousRouteSemantics(previousRoute)
2896
+ );
2786
2897
  });
2787
2898
  if (routeNeedsAi && !env.CLUE_AI_PROVIDER_API_KEY) {
2788
2899
  throw new Error("CLUE_AI_PROVIDER_API_KEY is required for semantic generation");
@@ -27,7 +27,7 @@ const SETUP_SKILLS = [
27
27
  "clue-local-verification",
28
28
  "clue-setup-report",
29
29
  ];
30
- const SETUP_SKILL_CONTENT_VERSION = "2026-05-10.lifecycle-placement-only.v3";
30
+ const SETUP_SKILL_CONTENT_VERSION = "2026-05-10.lifecycle-placement-only.v4";
31
31
  const REQUIRED_SETUP_SKILL_PHRASES = {
32
32
  "clue-sdk-instrumentation": [
33
33
  "Do not create no-op wrappers",
@@ -40,6 +40,8 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
40
40
  "CLUE_API_BASE_URL` is not part of backend SDK initialization",
41
41
  "Do not add `@clue-ai/browser-sdk` or backend SDK dependencies with `*` or `latest`",
42
42
  "Whitespace-only changes are allowed only on lines directly changed for Clue SDK wiring",
43
+ "The local backend token endpoint is part of the customer app, not the Clue API",
44
+ "The frontend `browserTokenProvider` must send the same service key used by `ClueInit`",
43
45
  "For Django code, use `clue-django-sdk` only after package-manager or registry verification confirms it is installable",
44
46
  ],
45
47
  "clue-setup-audit": [
@@ -335,7 +337,9 @@ const readAllowedSourceText = async ({
335
337
 
336
338
  const readDependencyText = async ({ repoRoot, roots }) => {
337
339
  const expandedRoots = roots.flatMap((root) =>
338
- root.endsWith("/src") ? [root, dirname(root)] : [root],
340
+ root.endsWith("/src") || root.endsWith("/app")
341
+ ? [root, dirname(root)]
342
+ : [root],
339
343
  );
340
344
  const candidatePaths = [
341
345
  ...DEPENDENCY_FILE_CANDIDATES,
@@ -764,6 +768,65 @@ const findWildcardFrontendSdkDependencyFiles = (dependencySources) =>
764
768
  })
765
769
  .map((source) => source.file_path);
766
770
 
771
+ const sourceLooksLikeBrowserTokenProvider = (source) => {
772
+ const text = stripSourceNoise(source.text);
773
+ return (
774
+ sourceImportsFrontendSdk(source) &&
775
+ /browserTokenProvider|browser[-_]?tokens/i.test(text)
776
+ );
777
+ };
778
+
779
+ const findBrowserTokenProviderMissingServiceKeyFiles = (frontendSources) =>
780
+ frontendSources
781
+ .filter(sourceLooksLikeBrowserTokenProvider)
782
+ .filter((source) => {
783
+ const text = stripSourceNoise(source.text);
784
+ if (/body\s*:\s*JSON\.stringify\s*\(\s*{\s*}\s*\)/.test(text)) {
785
+ return true;
786
+ }
787
+ return !/body\s*:\s*JSON\.stringify\s*\(\s*{[^}]*\b(?:service_key|serviceKey)\b/.test(
788
+ text,
789
+ );
790
+ })
791
+ .map((source) => source.file_path);
792
+
793
+ const findNextBrowserTokenProviderMissingPublicServiceKeyFiles = ({
794
+ dependencySources,
795
+ frontendSources,
796
+ }) => {
797
+ const roots = nextPackageRoots(dependencySources);
798
+ if (roots.length === 0) return [];
799
+ return frontendSources
800
+ .filter((source) => sourceIsUnderAnyRoot(source, roots))
801
+ .filter(sourceLooksLikeBrowserTokenProvider)
802
+ .filter(
803
+ (source) =>
804
+ !/\bprocess\.env\.NEXT_PUBLIC_CLUE_SERVICE_KEY\b/.test(source.text),
805
+ )
806
+ .map((source) => source.file_path);
807
+ };
808
+
809
+ const sourceLooksLikeBrowserTokenProxy = (source) => {
810
+ const text = stripSourceNoise(source.text);
811
+ return (
812
+ /browser[-_]?tokens|ingest\/browser-tokens/i.test(text) &&
813
+ /CLUE_API_KEY|x-clue-api-key/i.test(text)
814
+ );
815
+ };
816
+
817
+ const findBrowserTokenProxyUsingBackendServiceKeyFiles = (backendSources) =>
818
+ backendSources
819
+ .filter(sourceLooksLikeBrowserTokenProxy)
820
+ .filter((source) => {
821
+ const text = stripSourceNoise(source.text);
822
+ return [
823
+ /["']service_key["']\s*:\s*[^,\n}]*CLUE_SERVICE_KEY\b/i,
824
+ /\bserviceKey\s*:\s*[^,\n}]*CLUE_SERVICE_KEY\b/i,
825
+ /\b(?:browser_)?service_key\s*=\s*[^,\n]*CLUE_SERVICE_KEY\b/i,
826
+ ].some((pattern) => pattern.test(text));
827
+ })
828
+ .map((source) => source.file_path);
829
+
767
830
  const backendSdkSpec = (framework) =>
768
831
  BACKEND_SDK_BY_FRAMEWORK[String(framework ?? "").toLowerCase()] ?? {
769
832
  packages: Object.values(BACKEND_SDK_BY_FRAMEWORK).flatMap(
@@ -882,6 +945,15 @@ const checkSdkLifecycle = ({
882
945
  });
883
946
  const wildcardFrontendSdkDependencyFiles =
884
947
  findWildcardFrontendSdkDependencyFiles(dependencySources);
948
+ const browserTokenProviderMissingServiceKeyFiles =
949
+ findBrowserTokenProviderMissingServiceKeyFiles(frontendSources);
950
+ const nextBrowserTokenProviderMissingPublicServiceKeyFiles =
951
+ findNextBrowserTokenProviderMissingPublicServiceKeyFiles({
952
+ dependencySources,
953
+ frontendSources,
954
+ });
955
+ const browserTokenProxyUsingBackendServiceKeyFiles =
956
+ findBrowserTokenProxyUsingBackendServiceKeyFiles(backendSources);
885
957
  const backendIdentityRequired =
886
958
  backendPresent &&
887
959
  /\b(login|signin|sign_in|auth|token|session)\b/i.test(backendCombined);
@@ -940,6 +1012,12 @@ const checkSdkLifecycle = ({
940
1012
  wrong_sdk_packages: wrongFrontendSdkPackages,
941
1013
  next_lifecycle_non_public_env_files: nextLifecycleNonPublicEnvFiles,
942
1014
  wildcard_sdk_dependency_files: wildcardFrontendSdkDependencyFiles,
1015
+ browser_token_provider_missing_service_key_files:
1016
+ browserTokenProviderMissingServiceKeyFiles,
1017
+ next_browser_token_provider_missing_public_service_key_files:
1018
+ nextBrowserTokenProviderMissingPublicServiceKeyFiles,
1019
+ browser_token_proxy_uses_backend_service_key_files:
1020
+ browserTokenProxyUsingBackendServiceKeyFiles,
943
1021
  },
944
1022
  has_noop_wrapper: noOpPattern.test(combined),
945
1023
  component_lifecycle_init_files: componentLifecycleInitFiles,
@@ -958,6 +1036,9 @@ const checkSdkLifecycle = ({
958
1036
  wrongFrontendSdkPackages.length === 0 &&
959
1037
  nextLifecycleNonPublicEnvFiles.length === 0 &&
960
1038
  wildcardFrontendSdkDependencyFiles.length === 0 &&
1039
+ browserTokenProviderMissingServiceKeyFiles.length === 0 &&
1040
+ nextBrowserTokenProviderMissingPublicServiceKeyFiles.length === 0 &&
1041
+ browserTokenProxyUsingBackendServiceKeyFiles.length === 0 &&
961
1042
  !noOpPattern.test(combined) &&
962
1043
  componentLifecycleInitFiles.length === 0 &&
963
1044
  blockingLifecycleFiles.length === 0,
@@ -0,0 +1,99 @@
1
+ export const SETUP_DOCUMENTATION_CONTRACT_VERSION =
2
+ "2026-05-10.setup-documents.v1";
3
+
4
+ export const DEFAULT_SETUP_DOCUMENTS_URL = "/documents";
5
+
6
+ export const CORE_SETUP_DOCUMENT_IDS = [
7
+ "ai-setup-order",
8
+ "clue-boundary",
9
+ "environment-and-secrets",
10
+ "find-integration-points",
11
+ "browser-token-endpoint",
12
+ "clue-init",
13
+ "clue-identify",
14
+ "clue-set-account",
15
+ "clue-logout",
16
+ "setup-verification",
17
+ "forbidden-patterns",
18
+ ];
19
+
20
+ export const FRAMEWORK_SETUP_DOCUMENT_IDS = {
21
+ angular: "framework-react-spa",
22
+ django: "framework-django",
23
+ fastapi: "framework-fastapi",
24
+ nextjs: "framework-nextjs",
25
+ react: "framework-react-spa",
26
+ vite: "framework-react-spa",
27
+ vue: "framework-react-spa",
28
+ };
29
+
30
+ const optionalString = (value) =>
31
+ typeof value === "string" && value.trim() ? value.trim() : null;
32
+
33
+ const normalizeDocumentsUrl = (documentsUrl) =>
34
+ optionalString(documentsUrl)?.replace(/\/+$/, "") ??
35
+ DEFAULT_SETUP_DOCUMENTS_URL;
36
+
37
+ const normalizeFrameworks = (frameworks = []) => [
38
+ ...new Set(
39
+ frameworks
40
+ .map((framework) =>
41
+ typeof framework === "string" && framework.trim()
42
+ ? framework.trim().toLowerCase()
43
+ : null,
44
+ )
45
+ .filter(Boolean),
46
+ ),
47
+ ];
48
+
49
+ const docUrlFor = ({ documentsUrl, docId }) => `${documentsUrl}#${docId}`;
50
+
51
+ export const frameworkDocIdsFor = (frameworks = []) =>
52
+ [
53
+ ...new Set(
54
+ normalizeFrameworks(frameworks)
55
+ .map((framework) => FRAMEWORK_SETUP_DOCUMENT_IDS[framework])
56
+ .filter(Boolean),
57
+ ),
58
+ ];
59
+
60
+ export const buildSetupDocumentationContract = ({
61
+ documentsUrl,
62
+ framework,
63
+ frameworks = [],
64
+ } = {}) => {
65
+ const normalizedDocumentsUrl = normalizeDocumentsUrl(documentsUrl);
66
+ const selectedFrameworks = normalizeFrameworks([
67
+ ...(framework ? [framework] : []),
68
+ ...frameworks,
69
+ ]);
70
+ const selectedFrameworkDocIds = frameworkDocIdsFor(selectedFrameworks);
71
+ const requiredDocIds = [
72
+ ...new Set([...CORE_SETUP_DOCUMENT_IDS, ...selectedFrameworkDocIds]),
73
+ ];
74
+
75
+ return {
76
+ version: SETUP_DOCUMENTATION_CONTRACT_VERSION,
77
+ documents_url: normalizedDocumentsUrl,
78
+ required_doc_ids: requiredDocIds,
79
+ framework_doc_ids_by_framework: FRAMEWORK_SETUP_DOCUMENT_IDS,
80
+ selected_frameworks: selectedFrameworks,
81
+ selected_framework_doc_ids: selectedFrameworkDocIds,
82
+ doc_urls: Object.fromEntries(
83
+ requiredDocIds.map((docId) => [
84
+ docId,
85
+ docUrlFor({ documentsUrl: normalizedDocumentsUrl, docId }),
86
+ ]),
87
+ ),
88
+ pre_editing_gate: [
89
+ "Open documents_url before editing when tool access allows it.",
90
+ "Read every required_doc_ids entry that applies to the detected framework.",
91
+ "If documents_url cannot be opened, continue only from the generated skills and manifest doc ids, and report documentation_access_blocked.",
92
+ "Do not implement by prompt memory alone when the manifest contains a documentation contract.",
93
+ ],
94
+ report_required_fields: ["consulted_document_ids"],
95
+ agent_rule:
96
+ "Read the relevant Clue setup documents before editing and list consulted_document_ids in the final report.",
97
+ };
98
+ };
99
+
@@ -2,8 +2,9 @@ import {
2
2
  CLUE_CLI_INVOCATION_CONTRACT,
3
3
  clueCliCommand,
4
4
  } from "./cli-invocation.mjs";
5
+ import { buildSetupDocumentationContract } from "./setup-documents.mjs";
5
6
 
6
- export const AI_SETUP_HELP_VERSION = "2026-05-10.lifecycle-placement-only.v3";
7
+ export const AI_SETUP_HELP_VERSION = "2026-05-10.lifecycle-placement-only.v4";
7
8
 
8
9
  export const buildAiSetupHelp = () => ({
9
10
  name: "@clue-ai/cli AI setup help",
@@ -55,9 +56,10 @@ export const buildAiSetupHelp = () => ({
55
56
  },
56
57
  backend_browser_token_proxy_env: {
57
58
  variables: ["CLUE_API_KEY", "CLUE_API_BASE_URL"],
58
- rule: "CLUE_API_KEY stays server-side. CLUE_API_BASE_URL is used only by backend-owned browser token proxy code and is not part of backend SDK initialization.",
59
+ rule: "CLUE_API_KEY stays server-side. CLUE_API_BASE_URL is used only by backend-owned browser token proxy code and is not part of backend SDK initialization. The proxy endpoint belongs to the customer backend, but it calls the Clue API server-side at /api/v1/ingest/browser-tokens. The frontend browserTokenProvider must send the frontend ClueInit serviceKey to that customer backend proxy, and the proxy must issue browser tokens for that frontend serviceKey, not the backend service key.",
59
60
  },
60
61
  },
62
+ documentation_contract: buildSetupDocumentationContract(),
61
63
  setup_watch: {
62
64
  owner: "user",
63
65
  ai_agent_must_run: false,
@@ -8,6 +8,7 @@ import {
8
8
  CLUE_CLI_INVOCATION_CONTRACT,
9
9
  clueCliCommand,
10
10
  } from "./cli-invocation.mjs";
11
+ import { buildSetupDocumentationContract } from "./setup-documents.mjs";
11
12
  import { runSetupDetect } from "./setup-detect.mjs";
12
13
 
13
14
  const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
@@ -167,6 +168,7 @@ const aiProviderGuideForTarget = (target) =>
167
168
  const setupContextFromInput = (input = {}) => ({
168
169
  clue_api_key: optionalString(input.clueApiKey),
169
170
  clue_api_base_url: optionalString(input.clueApiBaseUrl),
171
+ documents_url: optionalString(input.documentsUrl),
170
172
  project_key: optionalString(input.projectKey),
171
173
  environment: optionalString(input.environment),
172
174
  });
@@ -386,10 +388,22 @@ export const runSetupPrepare = async ({
386
388
  const detection = await runSetupDetect({ repoRoot: resolvedRepoRoot });
387
389
  const { candidate, blockers } = firstCandidateOrBlocker(detection);
388
390
  if (!candidate) {
391
+ const detectedFrameworks = [
392
+ ...(Array.isArray(detection.services?.frontend)
393
+ ? detection.services.frontend.map((service) => service.framework)
394
+ : []),
395
+ ...(Array.isArray(detection.services?.backend)
396
+ ? detection.services.backend.map((service) => service.framework)
397
+ : []),
398
+ ];
389
399
  const manifest = {
390
400
  status: "blocked",
391
401
  target,
392
402
  skill_root: skillRoot,
403
+ documentation: buildSetupDocumentationContract({
404
+ documentsUrl: setupContext.documents_url,
405
+ frameworks: detectedFrameworks,
406
+ }),
393
407
  blockers,
394
408
  detection,
395
409
  ai_next_scope: "blocked_until_backend_routes_are_detected",
@@ -435,6 +449,10 @@ export const runSetupPrepare = async ({
435
449
  request,
436
450
  });
437
451
  const watchTargets = buildWatchTargets(detection, candidate);
452
+ const detectedFrameworks = [
453
+ candidate.framework,
454
+ ...watchTargets.map((target) => target.framework),
455
+ ];
438
456
  const frontendRuntimeEnvNames = requiredFrontendEnvNames(watchTargets);
439
457
  const backendRuntimeEnvNames = requiredBackendEnvNames(watchTargets);
440
458
  const serviceRuntimeEnvNames = [
@@ -450,6 +468,11 @@ export const runSetupPrepare = async ({
450
468
  backend_root_path: candidate.backend_root_path,
451
469
  service_key: candidate.service_key,
452
470
  },
471
+ documentation: buildSetupDocumentationContract({
472
+ documentsUrl: setupContext.documents_url,
473
+ framework: candidate.framework,
474
+ frameworks: detectedFrameworks,
475
+ }),
453
476
  service_identity: {
454
477
  canonical_field: "service_key",
455
478
  backend_env_name: "CLUE_SERVICE_KEY",
@@ -7,9 +7,11 @@ import {
7
7
  CLUE_CLI_RECOMMENDED_PREFIX,
8
8
  clueCliCommand,
9
9
  } from "./cli-invocation.mjs";
10
+ import { buildSetupDocumentationContract } from "./setup-documents.mjs";
10
11
 
11
12
  const SKILL_NAMES = [
12
13
  "clue-setup-orchestrator",
14
+ "clue-setup-reference",
13
15
  "clue-route-semantic-snapshot",
14
16
  "clue-semantic-gen",
15
17
  "clue-sdk-instrumentation",
@@ -17,7 +19,7 @@ const SKILL_NAMES = [
17
19
  "clue-local-verification",
18
20
  "clue-setup-report",
19
21
  ];
20
- const SETUP_SKILL_CONTENT_VERSION = "2026-05-10.lifecycle-placement-only.v3";
22
+ const SETUP_SKILL_CONTENT_VERSION = "2026-05-10.lifecycle-placement-only.v4";
21
23
 
22
24
  const TARGETS = new Set(["codex", "claude_code"]);
23
25
 
@@ -37,10 +39,21 @@ const normalizeTarget = (target) => {
37
39
  return normalized;
38
40
  };
39
41
 
40
- const skillBody = (name) => {
42
+ const skillBody = (name, { documentsUrl } = {}) => {
43
+ const documentationContract = buildSetupDocumentationContract({
44
+ documentsUrl,
45
+ });
46
+ const requiredDocIds = documentationContract.required_doc_ids.join(", ");
47
+ const frameworkDocIds = Object.entries(
48
+ documentationContract.framework_doc_ids_by_framework,
49
+ )
50
+ .map(([framework, docId]) => `${framework} -> ${docId}`)
51
+ .join(", ");
41
52
  const descriptions = {
42
53
  "clue-setup-orchestrator":
43
54
  "Use first when running Clue setup so lifecycle placement remains the only implementation workstream and read-only checks stay separate.",
55
+ "clue-setup-reference":
56
+ "Use before Clue setup implementation to read the public setup documents contract, select framework-specific docs, and record consulted document ids.",
44
57
  "clue-route-semantic-snapshot":
45
58
  "Use when checking backend route coverage and semantic snapshot readiness without hand-authoring generated snapshot files.",
46
59
  "clue-semantic-gen":
@@ -58,6 +71,8 @@ const skillBody = (name) => {
58
71
  const agentRoles = {
59
72
  "clue-setup-orchestrator":
60
73
  "Coordinator agent. Owns sequencing, agent assignment, gates, and blocker handling. It must not edit product code directly.",
74
+ "clue-setup-reference":
75
+ "Documentation reference agent. Owns reading the Clue setup docs contract, selecting relevant document ids, and reporting documentation blockers. It must not edit product code.",
61
76
  "clue-route-semantic-snapshot":
62
77
  "Semantic route readiness agent. Owns backend route inventory/readiness validation only. It must not author generated snapshot content or SDK code.",
63
78
  "clue-semantic-gen":
@@ -75,10 +90,18 @@ const skillBody = (name) => {
75
90
  const owns = {
76
91
  "clue-setup-orchestrator": [
77
92
  "read `.clue/setup-manifest.json` first",
93
+ "require `clue-setup-reference` before any SDK lifecycle placement",
78
94
  "assign exactly one implementation agent for SDK lifecycle placement",
79
95
  "assign multiple read-only monitoring agents",
80
96
  "stop on manifest blockers or P0/P1 findings",
81
97
  ],
98
+ "clue-setup-reference": [
99
+ "read `.clue/setup-manifest.json` documentation contract",
100
+ "open the public setup documents URL when tool access allows it",
101
+ "select required lifecycle docs and the detected framework doc",
102
+ "report documentation_access_blocked when docs cannot be opened",
103
+ "produce consulted_document_ids for downstream implementation and final report",
104
+ ],
82
105
  "clue-route-semantic-snapshot": [
83
106
  "backend route discovery readiness",
84
107
  "route coverage gaps and unsupported-framework blockers",
@@ -122,6 +145,12 @@ const skillBody = (name) => {
122
145
  "merge workstreams into one broad task",
123
146
  "continue after a blocker without reporting it",
124
147
  ],
148
+ "clue-setup-reference": [
149
+ "edit product code",
150
+ "infer setup behavior from memory when the docs contract is available",
151
+ "skip framework-specific docs when a framework doc id is available",
152
+ "claim docs were read without listing consulted_document_ids",
153
+ ],
125
154
  "clue-route-semantic-snapshot": [
126
155
  "create SDK lifecycle calls",
127
156
  "create or edit the CI workflow",
@@ -161,6 +190,12 @@ const skillBody = (name) => {
161
190
  "agent plan with execution agents, monitoring agents, file ownership, and gate order",
162
191
  "blocker list if setup cannot proceed",
163
192
  ],
193
+ "clue-setup-reference": [
194
+ "documentation readiness result: ready or blocked",
195
+ "documents_url used",
196
+ "consulted_document_ids or documentation_access_blocked reason",
197
+ "framework document id selected from the manifest",
198
+ ],
164
199
  "clue-route-semantic-snapshot": [
165
200
  "route readiness result: ready or blocked",
166
201
  "route coverage evidence and blocker details",
@@ -198,18 +233,32 @@ const skillBody = (name) => {
198
233
  "Use multiple monitoring agents for read-only checks, or named review passes if subagents are unavailable.",
199
234
  `The initial \`${clueCliCommand("setup --clue-api-key <key> --clue-api-base-url <url> --project-key <key> --environment <environment>")}\` command already performs repository discovery, semantic CI workflow generation, setup manifest generation, and writes service-specific environment guidance to \`.env.clue\` when backend routes can be detected.`,
200
235
  "Before implementation, read `.clue/setup-manifest.json` and treat it as the mechanical setup source of truth.",
236
+ "Read `.clue/setup-manifest.json` `documentation` and run `clue-setup-reference` before assigning SDK lifecycle placement.",
201
237
  "Read `.clue/setup-manifest.json` `cli_invocation` before running any Clue CLI subcommand.",
202
238
  `Before editing a customer repository, run \`${clueCliCommand("help --json")}\` and follow its setup_execution_contract.`,
203
239
  "Use the service keys and watch targets from `.clue/setup-manifest.json`; do not invent service keys.",
204
240
  "If `.clue/setup-manifest.json` has status `blocked`, stop and report its blockers instead of guessing.",
205
241
  "Treat semantic snapshot readiness and semantic CI as generated/static verification surfaces, not AI implementation workstreams.",
206
242
  "Do not implement or refresh semantic snapshot CI during lifecycle placement; report a blocker if generated semantic artifacts are missing or stale.",
243
+ "Before lifecycle placement, require `clue-setup-reference` to produce consulted_document_ids.",
207
244
  "Before lifecycle placement, read and apply `clue-sdk-instrumentation`.",
208
245
  "After lifecycle placement, run a monitoring check with `clue-setup-audit` before continuing.",
209
246
  "For final local verification, read and apply `clue-local-verification`.",
210
247
  "For the final report, read and apply `clue-setup-report`.",
211
248
  "Do not continue past P0/P1 monitoring findings until fixed or reported as blocked.",
212
249
  ],
250
+ "clue-setup-reference": [
251
+ "Read `.clue/setup-manifest.json` first.",
252
+ "Find the `documentation` object. If it is missing, run `npx -y @clue-ai/cli help --json` and use `setup_execution_contract.documentation_contract` as the fallback contract.",
253
+ "Use `documentation.documents_url` as the public docs page.",
254
+ "Open the docs page when the AI tool has browser or HTTP access. If it cannot be opened, continue only from the generated skill text and manifest doc ids, then report `documentation_access_blocked`.",
255
+ "Read all ids in `documentation.required_doc_ids` that apply to the detected repository.",
256
+ "For framework-specific implementation, select ids from `documentation.selected_framework_doc_ids` first. If empty, use `documentation.framework_doc_ids_by_framework` for the detected framework.",
257
+ "Minimum lifecycle docs before implementation: ai-setup-order, environment-and-secrets, browser-token-endpoint, clue-init, clue-identify, clue-set-account, clue-logout, setup-verification, forbidden-patterns.",
258
+ "For Next.js, read framework-nextjs. For React, Vite, Vue, or Angular browser apps, read framework-react-spa. For FastAPI, read framework-fastapi. For Django, read framework-django.",
259
+ "Return `consulted_document_ids` to the orchestrator and final report.",
260
+ "If a document contradicts `.clue/setup-manifest.json` or generated skills, stop and report a blocker instead of choosing behavior silently.",
261
+ ],
213
262
  "clue-route-semantic-snapshot": [
214
263
  "Use this skill as the source of truth for semantic snapshot readiness and route coverage verification.",
215
264
  "Do not hand-author semantic snapshot content files.",
@@ -251,6 +300,7 @@ const skillBody = (name) => {
251
300
  ],
252
301
  "clue-sdk-instrumentation": [
253
302
  "Use this skill as the source of truth for Clue SDK lifecycle implementation.",
303
+ "Before editing, confirm `clue-setup-reference` has produced consulted_document_ids for the relevant framework and lifecycle docs.",
254
304
  "The implementation scope is only ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout placement in existing lifecycle boundaries.",
255
305
  "Keep SDK lifecycle implementation separate from semantic snapshot and CI workflow work.",
256
306
  "Do not create semantic snapshot content or semantic snapshot CI workflow files from this skill.",
@@ -272,7 +322,10 @@ const skillBody = (name) => {
272
322
  "Never put `CLUE_API_KEY` in frontend code, frontend env files, browser bundles, or client-readable config.",
273
323
  "When browser SDK ingest is configured, implement a backend-owned browser token endpoint that reads server-side `CLUE_API_KEY` and requests `POST /api/v1/ingest/browser-tokens` from Clue.",
274
324
  "Configure frontend `ClueInit` with `browserTokenProvider` that calls the local backend token endpoint and returns the token string.",
325
+ "The local backend token endpoint is part of the customer app, not the Clue API. It may be named with the customer app's route convention, but it must call Clue server-side at `/api/v1/ingest/browser-tokens`.",
326
+ "The frontend `browserTokenProvider` must send the same service key used by `ClueInit` to the customer backend token endpoint. For Next.js this value comes from `NEXT_PUBLIC_CLUE_SERVICE_KEY`.",
275
327
  "The browser token request must include project key, environment, service key, and the current browser origin; the backend must attach `x-clue-api-key` server-side when calling Clue.",
328
+ "For browser token proxy code, the service key sent to Clue must be the frontend `ClueInit` serviceKey from the browser request, not the backend service's `CLUE_SERVICE_KEY`.",
276
329
  "If a backend-owned browser token endpoint is implemented, read `CLUE_API_BASE_URL` from the backend env block and normalize it so values with or without a trailing `/api/v1` do not produce duplicate paths.",
277
330
  "For FastAPI code, add `clue-fastapi-sdk` to the backend dependency file when missing, import `clue_init_fastapi` plus `ClueIdentify`, `ClueSetAccount`, and `ClueLogout` where needed, and use `CLUE_PROJECT_KEY`, `CLUE_ENVIRONMENT`, `CLUE_API_KEY`, and `CLUE_INGEST_ENDPOINT` from the backend env block.",
278
331
  "`CLUE_API_BASE_URL` is not part of backend SDK initialization. Use it only when the application backend owns a browser-token proxy for a frontend service.",
@@ -294,6 +347,7 @@ const skillBody = (name) => {
294
347
  ],
295
348
  "clue-setup-audit": [
296
349
  "Act as a read-only monitoring agent, not the execution agent.",
350
+ "Confirm the final report draft includes consulted_document_ids and that lifecycle edits do not contradict the referenced docs.",
297
351
  "Check one completed workstream at a time and report P0/P1 issues before more implementation continues.",
298
352
  "Review changed files line by line.",
299
353
  "Verify semantic snapshot, semantic CI, and SDK lifecycle responsibilities did not bleed into each other.",
@@ -318,6 +372,7 @@ const skillBody = (name) => {
318
372
  ],
319
373
  "clue-local-verification": [
320
374
  "Act as a read-only monitoring agent for local verification evidence.",
375
+ "Confirm documentation_access_blocked is reported if docs could not be opened, and do not treat that as event delivery verification.",
321
376
  "Verify each workstream independently before the final setup report.",
322
377
  "Confirm generated skill files exist.",
323
378
  "Confirm workflow files and SDK lifecycle imports/calls exist when those phases have run.",
@@ -341,6 +396,7 @@ const skillBody = (name) => {
341
396
  "clue-setup-report": [
342
397
  "Use this skill only after execution and monitoring passes are finished.",
343
398
  "Never claim `setup completed` from `setup-check --require-sdk-lifecycle` alone. That check is static and does not verify dependency installation, imports, app startup, or event delivery.",
399
+ "Include `consulted_document_ids` from `clue-setup-reference`; if docs could not be opened, include `documentation_access_blocked` with the reason.",
344
400
  "Completion requires all applicable evidence: SDK dependencies install successfully, SDK imports work in the target frontend/backend environments, the app starts, `setup-check --require-sdk-lifecycle` passes, and user-provided `setup-watch --local` or Clue setup screen evidence confirms expected event delivery.",
345
401
  "If any SDK package is unpublished, install/import checks were not run, app startup was not verified, or user-operated setup-watch/setup-screen evidence was not provided, the final status must be `blocked`, `partially complete`, or `user_verification_pending`; it must not be `complete`.",
346
402
  "For every completion claim, include the evidence source: command, exit status, output summary, file path, runtime URL, user-provided setup-watch result, or user-provided setup-screen result.",
@@ -386,11 +442,16 @@ const skillBody = (name) => {
386
442
  `- Use \`${CLUE_CLI_RECOMMENDED_PREFIX} <command>\` for Clue CLI commands unless \`.clue/setup-manifest.json\` explicitly provides a different invocation.`,
387
443
  `- If checking Clue CLI availability, run \`${CLUE_CLI_RECOMMENDED_PREFIX} --version\` or \`${CLUE_CLI_RECOMMENDED_PREFIX} --help\` and report the exact fetch/runtime error if it fails.`,
388
444
  `- Before editing a customer repository, run \`${clueCliCommand("help --json")}\` and follow its setup_execution_contract.`,
445
+ `- Public Clue setup docs: \`${documentationContract.documents_url}\`.`,
446
+ `- Required setup doc ids: ${requiredDocIds}.`,
447
+ `- Framework setup doc mapping: ${frameworkDocIds}.`,
448
+ "- Before editing, read `.clue/setup-manifest.json` `documentation`, run `clue-setup-reference`, and carry `consulted_document_ids` into the final report.",
389
449
  `- Do not search for a global \`${CLUE_CLI_BINARY_NAME}\` binary or block on \`which ${CLUE_CLI_BINARY_NAME}\`; missing global binary is normal.`,
390
450
  "- The AI implementation task is only to decide where ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout belong in existing code and apply the minimal SDK wiring for those calls.",
391
451
  "- Only exact changes required to place ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout are allowed. Do not perform ClueTrack instrumentation unless the user explicitly requested product event tracking.",
392
452
  "- Do not perform unrelated refactors, renames, file moves, formatting churn, broad cleanup, business logic rewrites, auth/session rewrites beyond minimal Clue hook insertion, UI changes unrelated to Clue setup, or unrelated dependency upgrades.",
393
453
  "- Do not run broad formatters or make whitespace-only cleanup. Keep formatting changes limited to lines directly touched for Clue SDK wiring.",
454
+ "- A customer backend browser-token endpoint is only a proxy for browser SDK token issuance. It is not the Clue API itself; the backend must call Clue server-side at `/api/v1/ingest/browser-tokens` with the frontend `ClueInit` service key supplied by the browser token provider.",
394
455
  "- Do not implement or refresh semantic snapshot CI during lifecycle placement; report a blocker if generated semantic artifacts are missing or stale.",
395
456
  `- Do not run \`${clueCliCommand("setup-watch --local")}\` automatically. setup-watch and the Clue setup screen are user-operated verification steps, not implementation-agent responsibility.`,
396
457
  "- The full setup must start with `clue-setup-orchestrator`.",
@@ -432,6 +493,7 @@ const askTarget = async ({
432
493
  export const installSetupSkills = async ({
433
494
  repoRoot,
434
495
  target,
496
+ documentsUrl,
435
497
  input,
436
498
  output,
437
499
  } = {}) => {
@@ -449,7 +511,7 @@ export const installSetupSkills = async ({
449
511
  const skillDir = join(skillRoot, skillName);
450
512
  const skillPath = join(skillDir, "SKILL.md");
451
513
  await mkdir(skillDir, { recursive: true });
452
- await writeFile(skillPath, skillBody(skillName), "utf8");
514
+ await writeFile(skillPath, skillBody(skillName, { documentsUrl }), "utf8");
453
515
  installed.push(skillPath);
454
516
  }
455
517