@clue-ai/cli 0.0.22 → 0.0.24

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.
@@ -39,6 +39,8 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
39
39
  "For Next.js browser/client code, use only `NEXT_PUBLIC_CLUE_PROJECT_KEY`",
40
40
  "Do not read `process.env.CLUE_PROJECT_KEY`",
41
41
  "CLUE_API_BASE_URL` is not part of backend SDK initialization",
42
+ "FastAPI `clue_init_fastapi` must pass `service_key`",
43
+ "Do not use `os.environ[\"CLUE_*\"]`",
42
44
  "Install Clue SDK dependencies through the latest channel",
43
45
  "`@clue-ai/browser-sdk` must use `latest`",
44
46
  "Python backend SDK dependencies must not be pinned",
@@ -49,6 +51,7 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
49
51
  "Do not forward `origin`, `projectKey`, or `environment` from JSON/body payload fields under server `CLUE_API_KEY`",
50
52
  "Frontend SDK adapter code is contract-owned Clue setup wiring",
51
53
  "Do not derive it from `NEXT_PUBLIC_API_URL`",
54
+ "Next.js browser SDK adapter files must start with `\"use client\"`",
52
55
  "Do not call `ClueInit` with empty-string fallbacks",
53
56
  "do not set `initialized = true` before `ClueInit`",
54
57
  "For Django code, use `clue-django-sdk` only after package-manager or registry verification confirms it is installable",
@@ -58,9 +61,13 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
58
61
  "Reject Django SDK setup when `clue-django-sdk` installability has not been verified",
59
62
  "Reject ClueTrack instrumentation unless the user explicitly requested product event tracking",
60
63
  "Reject Next.js browser/client code that reads non-public `process.env.CLUE_*` variables",
64
+ "Reject backend Clue setup code that uses `os.environ[\"CLUE_*\"]` required indexing instead of non-crashing env reads",
61
65
  "Reject browser token proxy code that forwards origin, projectKey, or environment from request JSON/body under server `CLUE_API_KEY`",
66
+ "Reject browser token proxy code that sends `CLUE_API_KEY` as `Authorization: Bearer`",
67
+ "Reject browser token proxy code that imports undeclared HTTP client dependencies",
62
68
  "Reject frontend browser token providers that derive the Clue proxy URL from `NEXT_PUBLIC_API_URL`",
63
69
  "Reject frontend adapters that mix stale browser-token paths",
70
+ "Reject Next.js browser SDK adapter files that omit `\"use client\"`",
64
71
  "Reject frontend adapters that set `initialized = true` before calling `ClueInit`",
65
72
  "Reject Clue SDK dependency entries that pin stale fixed versions",
66
73
  "Audit the setup diff against the Clue setup contract even when the code was written by another agent",
@@ -71,6 +78,7 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
71
78
  "clue-local-verification": [
72
79
  "`setup-check --require-sdk-lifecycle` is a static source check only",
73
80
  "`setup-doctor --local` API connectivity preflight",
81
+ "setup-agent quality gates must be reported honestly",
74
82
  "Verify frontend SDK installability/import",
75
83
  "Verify backend SDK installability/import",
76
84
  "Do not run `npx -y @clue-ai/cli setup-watch --local` automatically",
@@ -88,6 +96,7 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
88
96
  "clue-setup-report": [
89
97
  "Never claim `setup completed` from `setup-check --require-sdk-lifecycle` alone",
90
98
  "Completion requires all applicable evidence",
99
+ "setup-agent quality gates must be reported honestly",
91
100
  "For every completion claim, include the evidence source",
92
101
  ],
93
102
  };
@@ -504,6 +513,9 @@ const sourceIsUnderAnyRoot = (source, roots) =>
504
513
  );
505
514
  });
506
515
 
516
+ const hasUseClientDirective = (text) =>
517
+ /^\s*(?:"use client"|'use client')\s*;?/.test(text);
518
+
507
519
  const dependencySourceHasPackage = (source, packageName) => {
508
520
  if (source.file_path.endsWith("package.json")) {
509
521
  return packageJsonDependencyNames(source.text).includes(packageName);
@@ -920,6 +932,25 @@ const findNextBrowserTokenProviderMissingPublicEndpointFiles = ({
920
932
  .map((source) => source.file_path);
921
933
  };
922
934
 
935
+ const findNextLifecycleMissingUseClientFiles = ({
936
+ dependencySources,
937
+ frontendSources,
938
+ }) => {
939
+ const roots = nextPackageRoots(dependencySources);
940
+ if (roots.length === 0) return [];
941
+ return frontendSources
942
+ .filter((source) => sourceIsUnderAnyRoot(source, roots))
943
+ .filter(
944
+ (source) =>
945
+ sourceImportsFrontendSdk(source) ||
946
+ /\bClueInit\s*\(/.test(
947
+ stripSourceNoise(source.text, { stripStrings: true }),
948
+ ),
949
+ )
950
+ .filter((source) => !hasUseClientDirective(source.text))
951
+ .map((source) => source.file_path);
952
+ };
953
+
923
954
  const browserTokenPathLiterals = (text) =>
924
955
  [
925
956
  ...stripSourceNoise(text).matchAll(
@@ -990,6 +1021,35 @@ const sourceLooksLikeBrowserTokenProxy = (source) => {
990
1021
  );
991
1022
  };
992
1023
 
1024
+ const findBackendSdkInitMissingServiceKeyFiles = ({
1025
+ backendSources,
1026
+ framework,
1027
+ }) => {
1028
+ if (framework !== "fastapi") return [];
1029
+ return backendSources
1030
+ .filter((source) =>
1031
+ /\bclue_init_fastapi\s*\(/.test(
1032
+ stripSourceNoise(source.text, { stripStrings: true }),
1033
+ ),
1034
+ )
1035
+ .filter(
1036
+ (source) =>
1037
+ !/\bclue_init_fastapi\s*\([\s\S]{0,1600}\bservice_key\s*=/.test(
1038
+ stripSourceNoise(source.text, { stripStrings: true }),
1039
+ ),
1040
+ )
1041
+ .map((source) => source.file_path);
1042
+ };
1043
+
1044
+ const findBackendClueEnvRequiredIndexFiles = (backendSources) =>
1045
+ backendSources
1046
+ .filter((source) =>
1047
+ /os\.environ\s*\[\s*["']CLUE_[A-Z0-9_]+["']\s*\]/.test(
1048
+ stripSourceNoise(source.text),
1049
+ ),
1050
+ )
1051
+ .map((source) => source.file_path);
1052
+
993
1053
  const findBrowserTokenProxyUsingBackendServiceKeyFiles = (backendSources) =>
994
1054
  backendSources
995
1055
  .filter(sourceLooksLikeBrowserTokenProxy)
@@ -1004,18 +1064,18 @@ const findBrowserTokenProxyUsingBackendServiceKeyFiles = (backendSources) =>
1004
1064
  .map((source) => source.file_path);
1005
1065
 
1006
1066
  const bodyOriginPatterns = [
1007
- /\b(?:payload|body|requestBody|request_body|data|input)\.origin\b/i,
1008
- /\b(?:payload|body|requestBody|request_body|data|input)\s*\[\s*["']origin["']\s*\]/i,
1009
- /\b(?:payload|body|requestBody|request_body|data|input)\.get\s*\(\s*["']origin["']/i,
1010
- /["']origin["']\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
1011
- /\borigin\s*=\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
1067
+ /\b(?:body|requestBody|request_body|data|input)\.origin\b/i,
1068
+ /\b(?:body|requestBody|request_body|data|input)\s*\[\s*["']origin["']\s*\]/i,
1069
+ /\b(?:body|requestBody|request_body|data|input)\.get\s*\(\s*["']origin["']/i,
1070
+ /["']origin["']\s*:\s*(?:body|requestBody|request_body|data|input)\b/i,
1071
+ /\borigin\s*=\s*(?:body|requestBody|request_body|data|input)\b/i,
1012
1072
  ];
1013
1073
 
1014
1074
  const bodyProjectEnvironmentPatterns = [
1015
- /\b(?:projectKey|project_key)\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
1016
- /["'](?:projectKey|project_key)["']\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
1017
- /\benvironment\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
1018
- /["']environment["']\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
1075
+ /\b(?:projectKey|project_key)\s*:\s*(?:body|requestBody|request_body|data|input)\b/i,
1076
+ /["'](?:projectKey|project_key)["']\s*:\s*(?:body|requestBody|request_body|data|input)\b/i,
1077
+ /\benvironment\s*:\s*(?:body|requestBody|request_body|data|input)\b/i,
1078
+ /["']environment["']\s*:\s*(?:body|requestBody|request_body|data|input)\b/i,
1019
1079
  ];
1020
1080
 
1021
1081
  const findBrowserTokenProxyTrustingBodyOriginFiles = (backendSources) =>
@@ -1040,6 +1100,37 @@ const findBrowserTokenProxyTrustingBodyProjectEnvironmentFiles = (
1040
1100
  })
1041
1101
  .map((source) => source.file_path);
1042
1102
 
1103
+ const findBrowserTokenProxyMissingApiKeyHeaderFiles = (backendSources) =>
1104
+ backendSources
1105
+ .filter(sourceLooksLikeBrowserTokenProxy)
1106
+ .filter(
1107
+ (source) =>
1108
+ !/["']x-clue-api-key["']\s*:/.test(stripSourceNoise(source.text)),
1109
+ )
1110
+ .map((source) => source.file_path);
1111
+
1112
+ const findBrowserTokenProxyUndeclaredHttpClientFiles = ({
1113
+ backendSources,
1114
+ dependencySources,
1115
+ }) => {
1116
+ const declaredHttpClients = ["httpx", "requests"].filter((packageName) =>
1117
+ dependencyHasAnyPackage(dependencySources, [packageName]),
1118
+ );
1119
+ return backendSources
1120
+ .filter(sourceLooksLikeBrowserTokenProxy)
1121
+ .filter((source) => {
1122
+ const text = stripSourceNoise(source.text, { stripStrings: true });
1123
+ const importsHttpx = /(?:^|\n)\s*import\s+httpx\b/.test(text);
1124
+ const importsRequests =
1125
+ /(?:^|\n)\s*(?:import\s+requests\b|from\s+requests\b)/.test(text);
1126
+ return (
1127
+ (importsHttpx && !declaredHttpClients.includes("httpx")) ||
1128
+ (importsRequests && !declaredHttpClients.includes("requests"))
1129
+ );
1130
+ })
1131
+ .map((source) => source.file_path);
1132
+ };
1133
+
1043
1134
  const backendSdkSpec = (framework) =>
1044
1135
  BACKEND_SDK_BY_FRAMEWORK[String(framework ?? "").toLowerCase()] ?? {
1045
1136
  packages: Object.values(BACKEND_SDK_BY_FRAMEWORK).flatMap(
@@ -1143,6 +1234,13 @@ const checkSdkLifecycle = ({
1143
1234
  !backendPresent || backendSpec.installabilityStatus !== "unverified";
1144
1235
  const backendInitPresent =
1145
1236
  !backendPresent || backendSpec.initPattern.test(backendCombined);
1237
+ const backendSdkInitMissingServiceKeyFiles =
1238
+ findBackendSdkInitMissingServiceKeyFiles({
1239
+ backendSources,
1240
+ framework,
1241
+ });
1242
+ const backendClueEnvRequiredIndexFiles =
1243
+ findBackendClueEnvRequiredIndexFiles(backendSources);
1146
1244
  const frontendLifecyclePresent = frontendFoundApiNames.length > 0;
1147
1245
  const frontendSdkDependencyPresent =
1148
1246
  !frontendLifecyclePresent ||
@@ -1182,12 +1280,24 @@ const checkSdkLifecycle = ({
1182
1280
  dependencySources,
1183
1281
  frontendSources,
1184
1282
  });
1283
+ const nextLifecycleMissingUseClientFiles =
1284
+ findNextLifecycleMissingUseClientFiles({
1285
+ dependencySources,
1286
+ frontendSources,
1287
+ });
1185
1288
  const browserTokenProxyUsingBackendServiceKeyFiles =
1186
1289
  findBrowserTokenProxyUsingBackendServiceKeyFiles(backendSources);
1187
1290
  const browserTokenProxyTrustingBodyOriginFiles =
1188
1291
  findBrowserTokenProxyTrustingBodyOriginFiles(backendSources);
1189
1292
  const browserTokenProxyTrustingBodyProjectEnvironmentFiles =
1190
1293
  findBrowserTokenProxyTrustingBodyProjectEnvironmentFiles(backendSources);
1294
+ const browserTokenProxyMissingApiKeyHeaderFiles =
1295
+ findBrowserTokenProxyMissingApiKeyHeaderFiles(backendSources);
1296
+ const browserTokenProxyUndeclaredHttpClientFiles =
1297
+ findBrowserTokenProxyUndeclaredHttpClientFiles({
1298
+ backendSources,
1299
+ dependencySources,
1300
+ });
1191
1301
  const backendIdentityRequired =
1192
1302
  backendPresent &&
1193
1303
  /\b(login|signin|sign_in|auth|token|session)\b/i.test(backendCombined);
@@ -1228,6 +1338,8 @@ const checkSdkLifecycle = ({
1228
1338
  sdk_import_present: backendSdkImportPresent,
1229
1339
  sdk_dependency_or_import_present: backendSdkPresent,
1230
1340
  sdk_init_present: backendInitPresent,
1341
+ sdk_init_missing_service_key_files: backendSdkInitMissingServiceKeyFiles,
1342
+ clue_env_required_index_files: backendClueEnvRequiredIndexFiles,
1231
1343
  required_apis: [
1232
1344
  ...(backendIdentityRequired ? ["ClueIdentify"] : []),
1233
1345
  ...(backendAccountRequired ? ["ClueSetAccount"] : []),
@@ -1259,12 +1371,18 @@ const checkSdkLifecycle = ({
1259
1371
  nextBrowserTokenProviderMissingPublicServiceKeyFiles,
1260
1372
  next_browser_token_provider_missing_public_endpoint_files:
1261
1373
  nextBrowserTokenProviderMissingPublicEndpointFiles,
1374
+ next_lifecycle_missing_use_client_files:
1375
+ nextLifecycleMissingUseClientFiles,
1262
1376
  browser_token_proxy_uses_backend_service_key_files:
1263
1377
  browserTokenProxyUsingBackendServiceKeyFiles,
1264
1378
  browser_token_proxy_trusts_body_origin_files:
1265
1379
  browserTokenProxyTrustingBodyOriginFiles,
1266
1380
  browser_token_proxy_trusts_body_project_environment_files:
1267
1381
  browserTokenProxyTrustingBodyProjectEnvironmentFiles,
1382
+ browser_token_proxy_missing_api_key_header_files:
1383
+ browserTokenProxyMissingApiKeyHeaderFiles,
1384
+ browser_token_proxy_undeclared_http_client_files:
1385
+ browserTokenProxyUndeclaredHttpClientFiles,
1268
1386
  },
1269
1387
  has_noop_wrapper: noOpPattern.test(combined),
1270
1388
  component_lifecycle_init_files: componentLifecycleInitFiles,
@@ -1278,6 +1396,8 @@ const checkSdkLifecycle = ({
1278
1396
  backendSdkInstallabilityVerified &&
1279
1397
  pinnedBackendSdkDependencyFiles.length === 0 &&
1280
1398
  backendInitPresent &&
1399
+ backendSdkInitMissingServiceKeyFiles.length === 0 &&
1400
+ backendClueEnvRequiredIndexFiles.length === 0 &&
1281
1401
  backendMissingApis.length === 0 &&
1282
1402
  frontendSdkPresent &&
1283
1403
  frontendLifecycleFilesWithoutVerifiedSdk.length === 0 &&
@@ -1291,9 +1411,12 @@ const checkSdkLifecycle = ({
1291
1411
  frontendInitBeforeClueInitFiles.length === 0 &&
1292
1412
  nextBrowserTokenProviderMissingPublicServiceKeyFiles.length === 0 &&
1293
1413
  nextBrowserTokenProviderMissingPublicEndpointFiles.length === 0 &&
1414
+ nextLifecycleMissingUseClientFiles.length === 0 &&
1294
1415
  browserTokenProxyUsingBackendServiceKeyFiles.length === 0 &&
1295
1416
  browserTokenProxyTrustingBodyOriginFiles.length === 0 &&
1296
1417
  browserTokenProxyTrustingBodyProjectEnvironmentFiles.length === 0 &&
1418
+ browserTokenProxyMissingApiKeyHeaderFiles.length === 0 &&
1419
+ browserTokenProxyUndeclaredHttpClientFiles.length === 0 &&
1297
1420
  !noOpPattern.test(combined) &&
1298
1421
  componentLifecycleInitFiles.length === 0 &&
1299
1422
  blockingLifecycleFiles.length === 0,
@@ -34,6 +34,23 @@ const commonDirectory = (paths) => {
34
34
  return common.length ? common.join("/") : ".";
35
35
  };
36
36
 
37
+ const isPathInside = (childPath, parentPath) =>
38
+ childPath === parentPath || childPath.startsWith(`${parentPath}/`);
39
+
40
+ const findFastApiAppRoot = async ({ repoRoot, files, routeFiles }) => {
41
+ const appRootCandidates = [];
42
+ for (const absolutePath of files) {
43
+ const relativePath = relative(repoRoot, absolutePath);
44
+ const source = await readFile(absolutePath, "utf8");
45
+ if (!/\bFastAPI\s*\(/.test(source)) continue;
46
+ const candidate = dirname(relativePath);
47
+ if (routeFiles.every((routeFile) => isPathInside(routeFile, candidate))) {
48
+ appRootCandidates.push(candidate);
49
+ }
50
+ }
51
+ return appRootCandidates.sort((left, right) => right.length - left.length)[0] ?? null;
52
+ };
53
+
37
54
  const ignoredPackageDirs = new Set([
38
55
  ".git",
39
56
  ".next",
@@ -162,7 +179,12 @@ export const runSetupDetect = async ({ repoRoot, excludedSourcePaths = [] }) =>
162
179
  fastApiRoutes.map((route) => route.ai_context.relative_path).filter(Boolean),
163
180
  ),
164
181
  ].sort();
165
- const backendRootPath = commonDirectory(routeFiles);
182
+ const backendRootPath =
183
+ (await findFastApiAppRoot({
184
+ repoRoot: resolvedRepoRoot,
185
+ files,
186
+ routeFiles,
187
+ })) ?? commonDirectory(routeFiles);
166
188
  const backendServiceKey = deriveServiceKeyFromPath(backendRootPath);
167
189
  return {
168
190
  detected: true,
@@ -8,6 +8,7 @@ export const CORE_SETUP_DOCUMENT_IDS = [
8
8
  "clue-boundary",
9
9
  "environment-and-secrets",
10
10
  "find-integration-points",
11
+ "official-sdk-contract",
11
12
  "browser-token-endpoint",
12
13
  "clue-init",
13
14
  "clue-identify",
@@ -96,4 +97,3 @@ export const buildSetupDocumentationContract = ({
96
97
  "Read the relevant Clue setup documents before editing and list consulted_document_ids in the final report.",
97
98
  };
98
99
  };
99
-
@@ -8,6 +8,7 @@ import {
8
8
  DETERMINISTIC_CONTROL_MODEL,
9
9
  FRONTEND_ADAPTER_CONTRACT,
10
10
  SETUP_DOCTRINE,
11
+ SETUP_AGENT_QUALITY_GATES,
11
12
  } from "./setup-ai-contract.mjs";
12
13
  import { buildSetupDocumentationContract } from "./setup-documents.mjs";
13
14
 
@@ -116,6 +117,7 @@ export const buildAiSetupHelp = () => ({
116
117
  "OpenAI uses Responses API forced function tools with strict schema and parallel_tool_calls false. Anthropic uses Messages API tool_choice with the same input schema.",
117
118
  setup_watch_auto_run: false,
118
119
  },
120
+ setup_agent_quality_gates: SETUP_AGENT_QUALITY_GATES,
119
121
  completion_boundary: {
120
122
  ai_may_claim: [
121
123
  "Clue setup code changes were applied",
@@ -10,6 +10,7 @@ import {
10
10
  import {
11
11
  AI_SETUP_CONTRACT_VERSION,
12
12
  API_CONNECTIVITY_CONTRACT,
13
+ SETUP_AGENT_QUALITY_GATES,
13
14
  setupDoctrineSkillLines,
14
15
  } from "./setup-ai-contract.mjs";
15
16
  import { buildSetupDocumentationContract } from "./setup-documents.mjs";
@@ -54,6 +55,9 @@ const skillBody = (name, { documentsUrl } = {}) => {
54
55
  )
55
56
  .map(([framework, docId]) => `${framework} -> ${docId}`)
56
57
  .join(", ");
58
+ const setupAgentQualityGateIds = SETUP_AGENT_QUALITY_GATES.map(
59
+ (gate) => gate.id,
60
+ ).join(", ");
57
61
  const descriptions = {
58
62
  "clue-setup-orchestrator":
59
63
  "Use first when running Clue setup so lifecycle placement remains the only implementation workstream and read-only checks stay separate.",
@@ -260,7 +264,7 @@ const skillBody = (name, { documentsUrl } = {}) => {
260
264
  "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`.",
261
265
  "Read all ids in `documentation.required_doc_ids` that apply to the detected repository.",
262
266
  "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.",
263
- "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.",
267
+ "Minimum lifecycle docs before implementation: ai-setup-order, environment-and-secrets, official-sdk-contract, browser-token-endpoint, clue-init, clue-identify, clue-set-account, clue-logout, setup-verification, forbidden-patterns.",
264
268
  "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.",
265
269
  "Return `consulted_document_ids` to the orchestrator and final report.",
266
270
  "If a document contradicts `.clue/setup-manifest.json` or generated skills, stop and report a blocker instead of choosing behavior silently.",
@@ -316,6 +320,8 @@ const skillBody = (name, { documentsUrl } = {}) => {
316
320
  "Do not add per-call try/catch, try/except, `.catch`, or custom safe wrappers solely around official Clue SDK public lifecycle calls.",
317
321
  "Never await a Clue lifecycle call in a way that can block login, logout, account selection, request handling, page rendering, or API responses.",
318
322
  "Add or report the required SDK dependency instead of fabricating lifecycle APIs.",
323
+ "A clean customer repository is expected to have no existing Clue SDK imports, dependencies, browserTokenProvider wiring, or Clue adapter. Do not treat that absence as a blocker; add the minimal official Clue setup wiring.",
324
+ "When no existing Clue adapter or browser-token proxy module exists, creating a minimal Clue-owned setup file under the existing source root is allowed. Do not use new files for unrelated host app abstractions.",
319
325
  "For frontend code, add the real `@clue-ai/browser-sdk` dependency when missing. Use the latest channel (`@clue-ai/browser-sdk@latest`) so setup receives current SDK fixes. Do not invent `clue-js-sdk`, `@clue/browser-sdk`, local placeholder modules, or dynamic imports that hide a missing SDK.",
320
326
  `When lifecycle edits are clear, write an exact replacement plan to a temporary local JSON file and apply it with \`${clueCliCommand("lifecycle-apply --plan <plan-file> --repo .")}\`.`,
321
327
  "If npm/npx cannot fetch the Clue CLI package, report a blocker with the exact command and error instead of manually applying replacement plans.",
@@ -325,6 +331,7 @@ const skillBody = (name, { documentsUrl } = {}) => {
325
331
  "For Next.js browser/client code, use only `NEXT_PUBLIC_CLUE_PROJECT_KEY`, `NEXT_PUBLIC_CLUE_ENVIRONMENT`, `NEXT_PUBLIC_CLUE_SERVICE_KEY`, `NEXT_PUBLIC_CLUE_INGEST_ENDPOINT`, and `NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT` from the frontend `.env.local` block.",
326
332
  "Do not read `process.env.CLUE_PROJECT_KEY`, `process.env.CLUE_ENVIRONMENT`, `process.env.CLUE_SERVICE_KEY`, or `process.env.CLUE_INGEST_ENDPOINT` in Next.js browser/client code, and do not add non-public `CLUE_*` fallbacks there.",
327
333
  "Frontend SDK adapter code is contract-owned Clue setup wiring. The AI may choose the existing import/mount point, but must not invent token URL, env, or initialization semantics.",
334
+ "Next.js browser SDK adapter files must start with `\"use client\"` so Clue browser SDK code is not imported through a server component module.",
328
335
  "For Next.js frontend adapters, read the full customer-backend browser-token proxy URL from `NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT`. Do not derive it from `NEXT_PUBLIC_API_URL`, generic app API env names, detected backend ports, or relative frontend-origin paths.",
329
336
  "Do not mix stale browser-token paths such as `/api/clue/browser-token`, `/clue/browser-tokens`, or `/browser-tokens` with the canonical `/api/v1/clue/browser-tokens` path.",
330
337
  "Do not call `ClueInit` with empty-string fallbacks for required `NEXT_PUBLIC_CLUE_*` values. If required Clue env is absent, skip initialization and report the missing env names.",
@@ -338,8 +345,12 @@ const skillBody = (name, { documentsUrl } = {}) => {
338
345
  "The browser token request must include the frontend service key used by `ClueInit`. Project key and environment may be included only as public consistency hints; the backend must use server configuration or validate them against server configuration before calling Clue.",
339
346
  "The backend browser token proxy must derive origin from trusted request headers or server request metadata. Do not forward `origin`, `projectKey`, or `environment` from JSON/body payload fields under server `CLUE_API_KEY`.",
340
347
  "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`.",
348
+ "When the backend browser token proxy calls Clue, send the server `CLUE_API_KEY` as the `x-clue-api-key` header. Do not use `Authorization: Bearer`, query parameters, or JSON body fields for the Clue API key.",
349
+ "Do not introduce non-Clue HTTP client dependencies for browser-token proxy code unless they already exist in dependency files. For Python, prefer an existing HTTP client or standard library instead of importing undeclared `httpx` or `requests`.",
341
350
  "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.",
342
351
  "For FastAPI code, add unpinned `clue-fastapi-sdk` to the backend dependency file when missing so pip resolves the latest release, 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.",
352
+ "Do not use `os.environ[\"CLUE_*\"]` required indexing for Clue env values; Clue setup must not crash the host service when env is missing.",
353
+ "FastAPI `clue_init_fastapi` must pass `service_key` from the setup manifest/`CLUE_SERVICE_KEY`; do not rely on `service_name` alone because setup-watch targets are keyed by service key.",
343
354
  "`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.",
344
355
  "Python backend SDK dependencies must not be pinned.",
345
356
  "`@clue-ai/browser-sdk` must use `latest`.",
@@ -372,11 +383,15 @@ const skillBody = (name, { documentsUrl } = {}) => {
372
383
  "Reject wrong SDK package names. Frontend must use `@clue-ai/browser-sdk`; FastAPI must use `clue-fastapi-sdk`; Django must use `clue-django-sdk`.",
373
384
  "Reject Django SDK setup when `clue-django-sdk` installability has not been verified.",
374
385
  "Reject backend setup when backend routes exist but no backend Clue SDK dependency/import/init was added.",
386
+ "Reject backend Clue setup code that uses `os.environ[\"CLUE_*\"]` required indexing instead of non-crashing env reads.",
375
387
  "Reject awaited lifecycle calls that can block host service behavior.",
376
388
  "Reject browser token proxy code that forwards origin, projectKey, or environment from request JSON/body under server `CLUE_API_KEY`.",
389
+ "Reject browser token proxy code that sends `CLUE_API_KEY` as `Authorization: Bearer`, query parameters, or JSON body fields instead of the `x-clue-api-key` header.",
390
+ "Reject browser token proxy code that imports undeclared HTTP client dependencies such as `httpx` or `requests`.",
377
391
  "Reject frontend browser token providers that derive the Clue proxy URL from `NEXT_PUBLIC_API_URL`, generic app API env names, detected backend ports, or non-Clue routing assumptions.",
378
392
  "Reject frontend adapters that mix stale browser-token paths such as `/api/clue/browser-token`, `/clue/browser-tokens`, or `/browser-tokens` with the canonical `/api/v1/clue/browser-tokens` path.",
379
393
  "Reject frontend adapters that set `initialized = true` before calling `ClueInit`, or pass empty-string fallbacks for required `NEXT_PUBLIC_CLUE_*` values into `ClueInit`.",
394
+ "Reject Next.js browser SDK adapter files that omit `\"use client\"`.",
380
395
  "Audit the setup diff against the Clue setup contract even when the code was written by another agent or an earlier pass. Ownership of authorship is irrelevant to approval.",
381
396
  "Reject setup that covers only one login path when multiple login success paths are clearly present.",
382
397
  "Reject ClueInit inside React component lifecycle hooks, page components, sidebars, login/register success callbacks, or any repeated user interaction path.",
@@ -483,6 +498,7 @@ const skillBody = (name, { documentsUrl } = {}) => {
483
498
  "- The full setup must start with `clue-setup-orchestrator`.",
484
499
  "- The implementation agent owns only lifecycle placement; monitoring agents review one surface at a time and report P0/P1 issues.",
485
500
  "- Do not continue past a P0/P1 monitoring finding until it is fixed or explicitly reported as blocked.",
501
+ `- setup-agent quality gates must be reported honestly when available: ${setupAgentQualityGateIds}. A skipped setup-doctor gate is not API connectivity verification.`,
486
502
  "- If subagents are unavailable, run the same structure as separate named review passes and say so in the final report.",
487
503
  "- Do not expose project keys, API keys, secrets, tokens, or environment variable values.",
488
504
  "- Do not ask the user to paste secret values.",