@clue-ai/cli 0.0.9 → 0.0.10

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.
@@ -1,9 +1,15 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
- import { join, relative, resolve } from "node:path";
2
+ import { dirname, join, relative, resolve } from "node:path";
3
3
  import { validateSemanticCiRequest } from "./contracts.mjs";
4
4
  import {
5
+ SEMANTIC_GEN_WORKFLOW_COMMAND,
6
+ workflowHasCompatibleSemanticGenCommand,
7
+ } from "./cli-invocation.mjs";
8
+ import {
9
+ extractExecutableModuleStatements,
5
10
  findLifecycleCallApiNames,
6
11
  findLifecycleGuardViolations,
12
+ stripSourceNoise,
7
13
  } from "./lifecycle-guard.mjs";
8
14
  import { listAllowedSourceFiles } from "./path-policy.mjs";
9
15
  import { runSemanticInventory } from "./semantic-ci.mjs";
@@ -21,6 +27,45 @@ const SETUP_SKILLS = [
21
27
  "clue-local-verification",
22
28
  "clue-setup-report",
23
29
  ];
30
+ const SETUP_SKILL_CONTENT_VERSION = "2026-05-10.lifecycle-placement-only.v1";
31
+ const REQUIRED_SETUP_SKILL_PHRASES = {
32
+ "clue-sdk-instrumentation": [
33
+ "Do not create no-op wrappers",
34
+ "Lifecycle calls must resolve to real Clue SDK imports",
35
+ "add the real `@clue-ai/browser-sdk` dependency",
36
+ "Do not invent `clue-js-sdk`",
37
+ "The implementation scope is only ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout placement",
38
+ "For Django code, use `clue-django-sdk` only after package-manager or registry verification confirms it is installable",
39
+ ],
40
+ "clue-setup-audit": [
41
+ "Reject wrong SDK package names",
42
+ "Reject Django SDK setup when `clue-django-sdk` installability has not been verified",
43
+ "Reject ClueTrack instrumentation unless the user explicitly requested product event tracking",
44
+ "Reject unrelated refactors, renames, file moves",
45
+ "Execution agents must not approve, certify, or mark their own work complete",
46
+ ],
47
+ "clue-local-verification": [
48
+ "`setup-check --require-sdk-lifecycle` is a static source check only",
49
+ "Verify frontend SDK installability/import",
50
+ "Verify backend SDK installability/import",
51
+ "Do not run `npx -y @clue-ai/cli setup-watch --local` automatically",
52
+ "user_verification_pending",
53
+ `Confirm \`.github/workflows/clue-semantic-snapshot.yml\` calls \`${SEMANTIC_GEN_WORKFLOW_COMMAND}\``,
54
+ ],
55
+ "clue-setup-orchestrator": [
56
+ "Clue CLI public npm package: `@clue-ai/cli`",
57
+ "help --json",
58
+ "Required implementation agent: SDK Lifecycle Placement Agent only",
59
+ "Treat semantic snapshot readiness and semantic CI as generated/static verification surfaces",
60
+ "Do not search for a global `clue-ai` binary",
61
+ "Read `.clue/setup-manifest.json` `cli_invocation`",
62
+ ],
63
+ "clue-setup-report": [
64
+ "Never claim `setup completed` from `setup-check --require-sdk-lifecycle` alone",
65
+ "Completion requires all applicable evidence",
66
+ "For every completion claim, include the evidence source",
67
+ ],
68
+ };
24
69
  const TARGET_SKILL_ROOTS = {
25
70
  codex: [".agents", "skills"],
26
71
  claude_code: [".claude", "skills"],
@@ -50,14 +95,25 @@ const semanticRequestFromWorkflow = (workflow) => {
50
95
  return null;
51
96
  }
52
97
  };
53
- const BACKEND_SDK_MARKERS = [
54
- "clue-fastapi-sdk",
55
- "clue-django-sdk",
56
- "clue-python-sdk-core",
57
- "clue_fastapi_sdk",
58
- "clue_django_sdk",
59
- "clue_python_sdk_core",
60
- ];
98
+ const FRONTEND_SDK_PACKAGE = "@clue-ai/browser-sdk";
99
+ const WRONG_FRONTEND_SDK_PACKAGES = ["clue-js-sdk", "@clue/browser-sdk"];
100
+ const BACKEND_SDK_BY_FRAMEWORK = {
101
+ fastapi: {
102
+ packages: ["clue-fastapi-sdk"],
103
+ imports: ["clue_fastapi_sdk"],
104
+ initPattern: /clue_init_fastapi|CluePythonBootstrapConfig/,
105
+ installabilityStatus: "published",
106
+ },
107
+ django: {
108
+ packages: ["clue-django-sdk"],
109
+ imports: ["clue_django_sdk"],
110
+ initPattern:
111
+ /clue_init_django|configure_settings|CluePythonBootstrapConfig/,
112
+ installabilityStatus: "unverified",
113
+ blocker:
114
+ "clue-django-sdk installability has not been verified; Django setup must remain blocked until the package is published and import-checked",
115
+ },
116
+ };
61
117
  const DEPENDENCY_FILE_CANDIDATES = [
62
118
  "package.json",
63
119
  "pnpm-lock.yaml",
@@ -78,6 +134,124 @@ const exists = async (path) => {
78
134
  }
79
135
  };
80
136
 
137
+ const readSetupManifest = async (repoRoot) => {
138
+ const manifestPath = join(repoRoot, ".clue", "setup-manifest.json");
139
+ if (!(await exists(manifestPath))) return undefined;
140
+ try {
141
+ return JSON.parse(await readFile(manifestPath, "utf8"));
142
+ } catch {
143
+ return undefined;
144
+ }
145
+ };
146
+
147
+ const validateSetupManifestContract = (manifest) => {
148
+ if (!manifest || typeof manifest !== "object") {
149
+ return {
150
+ checked: false,
151
+ findings: [],
152
+ };
153
+ }
154
+ const findings = [];
155
+ if (
156
+ manifest.cli_invocation?.ai_help_command !==
157
+ "npx -y @clue-ai/cli help --json"
158
+ ) {
159
+ findings.push("cli_invocation.ai_help_command is missing or stale");
160
+ }
161
+ if (manifest.lifecycle_verification?.owner !== "user") {
162
+ findings.push("lifecycle_verification.owner must be user");
163
+ }
164
+ if (
165
+ manifest.lifecycle_verification?.ai_agent_must_run_setup_watch !== false
166
+ ) {
167
+ findings.push(
168
+ "lifecycle_verification.ai_agent_must_run_setup_watch must be false",
169
+ );
170
+ }
171
+ if (
172
+ !String(manifest.lifecycle_verification?.rule ?? "").includes(
173
+ "AI implementation agents must not run setup-watch automatically",
174
+ )
175
+ ) {
176
+ findings.push("lifecycle_verification.rule must prohibit AI setup-watch");
177
+ }
178
+ if (
179
+ !Array.isArray(manifest.ai_owned_workstreams) ||
180
+ manifest.ai_owned_workstreams.length !== 1 ||
181
+ manifest.ai_owned_workstreams[0] !== "sdk_lifecycle_placement"
182
+ ) {
183
+ findings.push("ai_owned_workstreams must be sdk_lifecycle_placement only");
184
+ }
185
+ const lifecycleApis = manifest.ai_implementation_scope?.lifecycle_apis;
186
+ if (
187
+ !Array.isArray(lifecycleApis) ||
188
+ lifecycleApis.length !== REQUIRED_LIFECYCLE_APIS.length ||
189
+ !REQUIRED_LIFECYCLE_APIS.every((apiName) => lifecycleApis.includes(apiName))
190
+ ) {
191
+ findings.push(
192
+ "ai_implementation_scope.lifecycle_apis must contain only ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout",
193
+ );
194
+ }
195
+ if (
196
+ !Array.isArray(manifest.ai_implementation_scope?.out_of_scope_by_default) ||
197
+ !manifest.ai_implementation_scope.out_of_scope_by_default.includes(
198
+ "ClueTrack",
199
+ )
200
+ ) {
201
+ findings.push("ai_implementation_scope must mark ClueTrack out of scope");
202
+ }
203
+ const localEventDelivery = Array.isArray(manifest.required_final_verification)
204
+ ? manifest.required_final_verification.find(
205
+ (entry) => entry?.id === "local_event_delivery",
206
+ )
207
+ : null;
208
+ if (
209
+ localEventDelivery?.command !==
210
+ "user runs npx -y @clue-ai/cli setup-watch --local"
211
+ ) {
212
+ findings.push(
213
+ "required_final_verification.local_event_delivery must be user-operated",
214
+ );
215
+ }
216
+ if (
217
+ !String(localEventDelivery?.completion_meaning ?? "").includes(
218
+ "user_verification_pending",
219
+ )
220
+ ) {
221
+ findings.push(
222
+ "required_final_verification.local_event_delivery must report user_verification_pending without user evidence",
223
+ );
224
+ }
225
+ return {
226
+ checked: true,
227
+ findings,
228
+ };
229
+ };
230
+
231
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
232
+
233
+ const validateSetupSkillContent = async ({ skillRoot, rootParts }) => {
234
+ const missingRequiredPhrases = [];
235
+ for (const skillName of SETUP_SKILLS) {
236
+ const skillPath = join(skillRoot, skillName, "SKILL.md");
237
+ if (!(await exists(skillPath))) continue;
238
+ const text = await readFile(skillPath, "utf8");
239
+ const requiredPhrases = [
240
+ `setup_skill_version: ${SETUP_SKILL_CONTENT_VERSION}`,
241
+ ...(REQUIRED_SETUP_SKILL_PHRASES[skillName] ?? []),
242
+ ];
243
+ for (const phrase of requiredPhrases) {
244
+ if (!text.includes(phrase)) {
245
+ missingRequiredPhrases.push({
246
+ file_path: join(...rootParts, skillName, "SKILL.md"),
247
+ phrase,
248
+ });
249
+ }
250
+ }
251
+ }
252
+ return missingRequiredPhrases;
253
+ };
254
+
81
255
  const normalizeTarget = (target) => {
82
256
  if (typeof target !== "string" || target.trim() === "") return undefined;
83
257
  const normalized = target
@@ -116,9 +290,12 @@ const readAllowedSourceText = async ({
116
290
  };
117
291
 
118
292
  const readDependencyText = async ({ repoRoot, roots }) => {
293
+ const expandedRoots = roots.flatMap((root) =>
294
+ root.endsWith("/src") ? [root, dirname(root)] : [root],
295
+ );
119
296
  const candidatePaths = [
120
297
  ...DEPENDENCY_FILE_CANDIDATES,
121
- ...roots.flatMap((root) =>
298
+ ...expandedRoots.flatMap((root) =>
122
299
  DEPENDENCY_FILE_CANDIDATES.map((file) => join(root, file)),
123
300
  ),
124
301
  ];
@@ -174,26 +351,342 @@ const findSecretLeaks = (sources) =>
174
351
  const startsWithRoot = (filePath, root) =>
175
352
  filePath === root || filePath.startsWith(`${root.replace(/\/+$/, "")}/`);
176
353
 
177
- const hasAnyMarker = (text, markers) =>
178
- markers.some((marker) => text.includes(marker));
354
+ const packageJsonDependencyNames = (text) => {
355
+ try {
356
+ const parsed = JSON.parse(text);
357
+ return [
358
+ "dependencies",
359
+ "devDependencies",
360
+ "optionalDependencies",
361
+ "peerDependencies",
362
+ ].flatMap((field) =>
363
+ parsed && typeof parsed[field] === "object" && parsed[field] !== null
364
+ ? Object.keys(parsed[field])
365
+ : [],
366
+ );
367
+ } catch {
368
+ return [];
369
+ }
370
+ };
371
+
372
+ const dependencySourceHasPackage = (source, packageName) => {
373
+ if (source.file_path.endsWith("package.json")) {
374
+ return packageJsonDependencyNames(source.text).includes(packageName);
375
+ }
376
+ const packagePattern = new RegExp(
377
+ `(^|[\\s"'=,{\\[]+)${escapeRegex(packageName)}($|[\\s"'=<>~!,}\\]]+)`,
378
+ "i",
379
+ );
380
+ return source.text
381
+ .split(/\r?\n/)
382
+ .map((line) => line.trim())
383
+ .filter((line) => line && !line.startsWith("#"))
384
+ .some((line) => packagePattern.test(line));
385
+ };
386
+
387
+ const dependencyHasAnyPackage = (dependencySources, packageNames) =>
388
+ dependencySources.some((source) =>
389
+ packageNames.some((packageName) =>
390
+ dependencySourceHasPackage(source, packageName),
391
+ ),
392
+ );
393
+
394
+ const sourceHasPythonImport = (source, importName) => {
395
+ const importPattern = new RegExp(
396
+ `(^|\\n)\\s*(?:from\\s+${escapeRegex(importName)}\\b|import\\s+${escapeRegex(importName)}\\b)`,
397
+ );
398
+ return importPattern.test(
399
+ stripSourceNoise(source.text, { stripStrings: true }),
400
+ );
401
+ };
402
+
403
+ const sourcesHavePythonImport = (sources, importNames) =>
404
+ sources.some((source) =>
405
+ importNames.some((importName) => sourceHasPythonImport(source, importName)),
406
+ );
407
+
408
+ const sourcesHaveFrontendSdkImport = (sources) =>
409
+ sources.some((source) => sourceImportsFrontendSdk(source));
410
+
411
+ const sourceImportsFrontendSdk = (source) =>
412
+ sourceTextImportsFrontendSdk(source.text);
413
+
414
+ const sourceTextImportsFrontendSdk = (text) =>
415
+ extractExecutableModuleStatements(text).some((statement) =>
416
+ new RegExp(
417
+ `(?:from\\s*["']${escapeRegex(FRONTEND_SDK_PACKAGE)}["']|import\\s*["']${escapeRegex(FRONTEND_SDK_PACKAGE)}["'])`,
418
+ ).test(statement),
419
+ );
420
+
421
+ const parseNamedSpecifiers = (specifiers) =>
422
+ specifiers
423
+ .split(",")
424
+ .map((specifier) => specifier.trim())
425
+ .filter(Boolean)
426
+ .map((specifier) => {
427
+ const match = /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/.exec(
428
+ specifier,
429
+ );
430
+ if (!match) return null;
431
+ return {
432
+ imported: match[1],
433
+ local: match[2] ?? match[1],
434
+ exported: match[2] ?? match[1],
435
+ };
436
+ })
437
+ .filter(Boolean);
438
+
439
+ const sourceDirectlyProvidesApiFromFrontendSdk = (text, apiName) => {
440
+ const statements = extractExecutableModuleStatements(text);
441
+ for (const match of statements
442
+ .join("\n")
443
+ .matchAll(/import\s*{([^}]+)}\s*from\s*["']([^"']+)["']/g)) {
444
+ if (match[2] !== FRONTEND_SDK_PACKAGE) continue;
445
+ if (
446
+ parseNamedSpecifiers(match[1]).some(
447
+ (specifier) =>
448
+ specifier.imported === apiName && specifier.local === apiName,
449
+ )
450
+ ) {
451
+ return true;
452
+ }
453
+ }
454
+ for (const match of statements
455
+ .join("\n")
456
+ .matchAll(/export\s*{([^}]+)}\s*from\s*["']([^"']+)["']/g)) {
457
+ if (match[2] !== FRONTEND_SDK_PACKAGE) continue;
458
+ if (
459
+ parseNamedSpecifiers(match[1]).some(
460
+ (specifier) =>
461
+ specifier.imported === apiName && specifier.exported === apiName,
462
+ )
463
+ ) {
464
+ return true;
465
+ }
466
+ }
467
+ return false;
468
+ };
469
+
470
+ const frontendSdkImportedLocalsForApi = (text, apiName) => {
471
+ const locals = [];
472
+ for (const match of extractExecutableModuleStatements(text)
473
+ .join("\n")
474
+ .matchAll(/import\s*{([^}]+)}\s*from\s*["']([^"']+)["']/g)) {
475
+ if (match[2] !== FRONTEND_SDK_PACKAGE) continue;
476
+ for (const specifier of parseNamedSpecifiers(match[1])) {
477
+ if (specifier.imported === apiName) {
478
+ locals.push(specifier.local);
479
+ }
480
+ }
481
+ }
482
+ return locals;
483
+ };
484
+
485
+ const frontendSdkNamespaces = (text) =>
486
+ [
487
+ ...extractExecutableModuleStatements(text)
488
+ .join("\n")
489
+ .matchAll(
490
+ /import\s*\*\s*as\s*([A-Za-z_$][\w$]*)\s*from\s*["']([^"']+)["']/g,
491
+ ),
492
+ ]
493
+ .filter((match) => match[2] === FRONTEND_SDK_PACKAGE)
494
+ .map((match) => match[1]);
495
+
496
+ const sourceExportsLocalAsApi = (text, localName, apiName) => {
497
+ const statements = extractExecutableModuleStatements(text).join("\n");
498
+ for (const match of statements.matchAll(
499
+ /export\s*{([^}]+)}(?:\s*from\s*["'][^"']+["'])?/g,
500
+ )) {
501
+ if (match[0].includes(" from ")) continue;
502
+ if (
503
+ parseNamedSpecifiers(match[1]).some(
504
+ (specifier) =>
505
+ specifier.imported === localName && specifier.exported === apiName,
506
+ )
507
+ ) {
508
+ return true;
509
+ }
510
+ }
511
+ return new RegExp(
512
+ `export\\s+const\\s+${escapeRegex(apiName)}\\s*=\\s*${escapeRegex(localName)}\\b`,
513
+ ).test(statements);
514
+ };
515
+
516
+ const sourceForwardsFrontendSdkApi = (source, apiName) => {
517
+ const text = source.text;
518
+ if (sourceDirectlyProvidesApiFromFrontendSdk(text, apiName)) return true;
519
+ const importedLocals = frontendSdkImportedLocalsForApi(text, apiName);
520
+ if (
521
+ importedLocals.some((localName) =>
522
+ sourceExportsLocalAsApi(text, localName, apiName),
523
+ )
524
+ ) {
525
+ return true;
526
+ }
527
+ return frontendSdkNamespaces(text).some((namespaceName) =>
528
+ new RegExp(
529
+ `export\\s+const\\s+${escapeRegex(apiName)}\\s*=\\s*${escapeRegex(namespaceName)}\\.${escapeRegex(apiName)}\\b`,
530
+ ).test(text),
531
+ );
532
+ };
533
+
534
+ const localImportSpecifiersForApi = (text, apiName) =>
535
+ [
536
+ ...extractExecutableModuleStatements(text)
537
+ .join("\n")
538
+ .matchAll(/import\s*{([^}]+)}\s*from\s*["']([^"']+)["']/g),
539
+ ]
540
+ .filter((match) => match[2].startsWith(".") || match[2].startsWith("@/"))
541
+ .flatMap((match) =>
542
+ parseNamedSpecifiers(match[1])
543
+ .filter(
544
+ (specifier) =>
545
+ specifier.imported === apiName && specifier.local === apiName,
546
+ )
547
+ .map(() => match[2]),
548
+ );
549
+
550
+ const importSpecifiers = (text) =>
551
+ [
552
+ ...extractExecutableModuleStatements(text)
553
+ .join("\n")
554
+ .matchAll(/import\s+(?:[\s\S]*?\s+from\s+)?["']([^"']+)["']/g),
555
+ ]
556
+ .map((match) => match[1] ?? match[2])
557
+ .filter(Boolean);
558
+
559
+ const candidateSourcePaths = (basePath) => [
560
+ basePath,
561
+ ...SOURCE_EXTENSIONS.map((extension) => `${basePath}${extension}`),
562
+ ...SOURCE_EXTENSIONS.map((extension) => join(basePath, `index${extension}`)),
563
+ ];
564
+
565
+ const resolveLocalSource = ({ importerPath, sourceByPath, specifier }) => {
566
+ const candidates = [];
567
+ if (specifier.startsWith(".")) {
568
+ candidates.push(
569
+ ...candidateSourcePaths(join(dirname(importerPath), specifier)),
570
+ );
571
+ }
572
+ if (specifier.startsWith("@/")) {
573
+ const srcIndex = importerPath.lastIndexOf("/src/");
574
+ if (srcIndex >= 0) {
575
+ candidates.push(
576
+ ...candidateSourcePaths(
577
+ join(
578
+ importerPath.slice(0, srcIndex + "/src".length),
579
+ specifier.slice(2),
580
+ ),
581
+ ),
582
+ );
583
+ }
584
+ for (const root of [
585
+ "src",
586
+ "frontend/src",
587
+ "apps/web/src",
588
+ "apps/admin/src",
589
+ "apps/visitor/src",
590
+ ]) {
591
+ candidates.push(...candidateSourcePaths(join(root, specifier.slice(2))));
592
+ }
593
+ }
594
+ return candidates
595
+ .map((candidate) => sourceByPath.get(candidate))
596
+ .find(Boolean);
597
+ };
598
+
599
+ const sourceHasVerifiedFrontendSdkAccess = ({
600
+ apiNames,
601
+ source,
602
+ sourceByPath,
603
+ }) =>
604
+ apiNames.every((apiName) => {
605
+ if (sourceDirectlyProvidesApiFromFrontendSdk(source.text, apiName)) {
606
+ return true;
607
+ }
608
+ return localImportSpecifiersForApi(source.text, apiName).some(
609
+ (specifier) => {
610
+ const importedSource = resolveLocalSource({
611
+ importerPath: source.file_path,
612
+ sourceByPath,
613
+ specifier,
614
+ });
615
+ return importedSource
616
+ ? sourceForwardsFrontendSdkApi(importedSource, apiName)
617
+ : false;
618
+ },
619
+ );
620
+ });
621
+
622
+ const findWrongFrontendSdkPackages = ({ sources, dependencySources }) => {
623
+ const combined = [...sources, ...dependencySources]
624
+ .map((source) => stripSourceNoise(source.text))
625
+ .join("\n");
626
+ return WRONG_FRONTEND_SDK_PACKAGES.filter((packageName) =>
627
+ combined.includes(packageName),
628
+ );
629
+ };
630
+
631
+ const backendSdkSpec = (framework) =>
632
+ BACKEND_SDK_BY_FRAMEWORK[String(framework ?? "").toLowerCase()] ?? {
633
+ packages: Object.values(BACKEND_SDK_BY_FRAMEWORK).flatMap(
634
+ (spec) => spec.packages,
635
+ ),
636
+ imports: Object.values(BACKEND_SDK_BY_FRAMEWORK).flatMap(
637
+ (spec) => spec.imports,
638
+ ),
639
+ initPattern:
640
+ /clue_init_fastapi|clue_init_django|configure_settings|CluePythonBootstrapConfig/,
641
+ installabilityStatus: "framework_unverified",
642
+ };
179
643
 
180
644
  const checkSdkLifecycle = ({
181
645
  backendRootPaths = [],
182
646
  dependencySources = [],
647
+ framework,
183
648
  sources,
184
649
  }) => {
185
- const combined = sources.map((source) => source.text).join("\n");
650
+ const combined = sources
651
+ .map((source) => stripSourceNoise(source.text, { stripStrings: true }))
652
+ .join("\n");
186
653
  const backendSources = sources.filter((source) =>
187
654
  backendRootPaths.some((root) => startsWithRoot(source.file_path, root)),
188
655
  );
656
+ const frontendSources = sources.filter(
657
+ (source) =>
658
+ !backendRootPaths.some((root) => startsWithRoot(source.file_path, root)),
659
+ );
189
660
  const backendCombined = backendSources
190
- .map((source) => source.text)
661
+ .map((source) => stripSourceNoise(source.text, { stripStrings: true }))
191
662
  .join("\n");
192
- const dependencyCombined = dependencySources
193
- .map((source) => source.text)
663
+ const frontendCombined = frontendSources
664
+ .map((source) => stripSourceNoise(source.text, { stripStrings: true }))
194
665
  .join("\n");
195
666
  const foundApiNames = findLifecycleCallApiNames(combined);
196
667
  const backendFoundApiNames = findLifecycleCallApiNames(backendCombined);
668
+ const frontendFoundApiNames = findLifecycleCallApiNames(frontendCombined);
669
+ const sourceByPath = new Map(
670
+ sources.map((source) => [source.file_path, source]),
671
+ );
672
+ const frontendLifecycleFilesWithoutVerifiedSdk = frontendSources
673
+ .filter(
674
+ (source) =>
675
+ findLifecycleCallApiNames(
676
+ stripSourceNoise(source.text, { stripStrings: true }),
677
+ ).length > 0,
678
+ )
679
+ .filter((source) => {
680
+ const apiNames = findLifecycleCallApiNames(
681
+ stripSourceNoise(source.text, { stripStrings: true }),
682
+ );
683
+ return !sourceHasVerifiedFrontendSdkAccess({
684
+ apiNames,
685
+ source,
686
+ sourceByPath,
687
+ });
688
+ })
689
+ .map((source) => source.file_path);
197
690
  const foundApis = REQUIRED_LIFECYCLE_APIS.filter((api) =>
198
691
  foundApiNames.includes(api),
199
692
  );
@@ -204,30 +697,49 @@ const checkSdkLifecycle = ({
204
697
  /window\.Clue(?:Init|Identify|SetAccount|Logout)|(?:function|const|let|var)\s+Clue(?:Init|Identify|SetAccount|Logout)\b/;
205
698
  const componentLifecycleInitFiles = sources
206
699
  .filter((source) =>
207
- /useEffect\s*\([\s\S]{0,1200}ClueInit/.test(source.text),
700
+ /useEffect\s*\([\s\S]{0,1200}ClueInit/.test(
701
+ stripSourceNoise(source.text, { stripStrings: true }),
702
+ ),
208
703
  )
209
704
  .map((source) => source.file_path);
210
- const unguardedLifecycleCalls = sources.flatMap((source) =>
211
- findLifecycleGuardViolations(source.text).map((violation) => ({
705
+ const blockingLifecycleCalls = sources.flatMap((source) =>
706
+ findLifecycleGuardViolations(
707
+ stripSourceNoise(source.text, { stripStrings: true }),
708
+ ).map((violation) => ({
212
709
  file_path: source.file_path,
213
710
  ...violation,
214
711
  })),
215
712
  );
216
- const unguardedLifecycleFiles = [
217
- ...new Set(unguardedLifecycleCalls.map((violation) => violation.file_path)),
713
+ const blockingLifecycleFiles = [
714
+ ...new Set(blockingLifecycleCalls.map((violation) => violation.file_path)),
218
715
  ];
219
716
  const backendPresent = backendSources.length > 0;
220
- const backendSdkPresent =
717
+ const backendSpec = backendSdkSpec(framework);
718
+ const backendSdkDependencyPresent =
221
719
  !backendPresent ||
222
- hasAnyMarker(
223
- `${backendCombined}\n${dependencyCombined}`,
224
- BACKEND_SDK_MARKERS,
225
- );
226
- const backendInitPresent =
720
+ dependencyHasAnyPackage(dependencySources, backendSpec.packages);
721
+ const backendSdkImportPresent =
227
722
  !backendPresent ||
228
- /clue_init_fastapi|clue_init_django|configure_settings|CluePythonBootstrapConfig/.test(
229
- backendCombined,
230
- );
723
+ sourcesHavePythonImport(backendSources, backendSpec.imports);
724
+ const backendSdkPresent =
725
+ !backendPresent || (backendSdkDependencyPresent && backendSdkImportPresent);
726
+ const backendSdkInstallabilityVerified =
727
+ !backendPresent || backendSpec.installabilityStatus !== "unverified";
728
+ const backendInitPresent =
729
+ !backendPresent || backendSpec.initPattern.test(backendCombined);
730
+ const frontendLifecyclePresent = frontendFoundApiNames.length > 0;
731
+ const frontendSdkDependencyPresent =
732
+ !frontendLifecyclePresent ||
733
+ dependencyHasAnyPackage(dependencySources, [FRONTEND_SDK_PACKAGE]);
734
+ const frontendSdkImportPresent =
735
+ !frontendLifecyclePresent || sourcesHaveFrontendSdkImport(frontendSources);
736
+ const frontendSdkPresent =
737
+ !frontendLifecyclePresent ||
738
+ (frontendSdkDependencyPresent && frontendSdkImportPresent);
739
+ const wrongFrontendSdkPackages = findWrongFrontendSdkPackages({
740
+ sources: frontendSources,
741
+ dependencySources,
742
+ });
231
743
  const backendIdentityRequired =
232
744
  backendPresent &&
233
745
  /\b(login|signin|sign_in|auth|token|session)\b/i.test(backendCombined);
@@ -256,6 +768,15 @@ const checkSdkLifecycle = ({
256
768
  backend_present: backendPresent,
257
769
  backend_root_paths: backendRootPaths,
258
770
  dependency_files: dependencySources.map((source) => source.file_path),
771
+ expected_sdk_packages: backendSpec.packages,
772
+ expected_sdk_imports: backendSpec.imports,
773
+ sdk_installability_status: backendSpec.installabilityStatus,
774
+ sdk_installability_verified: backendSdkInstallabilityVerified,
775
+ sdk_blocker: backendSdkInstallabilityVerified
776
+ ? null
777
+ : backendSpec.blocker,
778
+ sdk_dependency_present: backendSdkDependencyPresent,
779
+ sdk_import_present: backendSdkImportPresent,
259
780
  sdk_dependency_or_import_present: backendSdkPresent,
260
781
  sdk_init_present: backendInitPresent,
261
782
  required_apis: [
@@ -265,18 +786,35 @@ const checkSdkLifecycle = ({
265
786
  ],
266
787
  missing_apis: backendMissingApis,
267
788
  },
789
+ frontend_lifecycle: {
790
+ lifecycle_present: frontendLifecyclePresent,
791
+ found_apis: frontendFoundApiNames,
792
+ expected_sdk_package: FRONTEND_SDK_PACKAGE,
793
+ sdk_dependency_present: frontendSdkDependencyPresent,
794
+ sdk_import_present: frontendSdkImportPresent,
795
+ sdk_dependency_or_import_present: frontendSdkPresent,
796
+ lifecycle_files_without_verified_sdk:
797
+ frontendLifecycleFilesWithoutVerifiedSdk,
798
+ wrong_sdk_packages: wrongFrontendSdkPackages,
799
+ },
268
800
  has_noop_wrapper: noOpPattern.test(combined),
269
801
  component_lifecycle_init_files: componentLifecycleInitFiles,
270
- unguarded_lifecycle_files: unguardedLifecycleFiles,
271
- unguarded_lifecycle_calls: unguardedLifecycleCalls,
802
+ blocking_lifecycle_files: blockingLifecycleFiles,
803
+ blocking_lifecycle_calls: blockingLifecycleCalls,
804
+ unguarded_lifecycle_files: blockingLifecycleFiles,
805
+ unguarded_lifecycle_calls: blockingLifecycleCalls,
272
806
  passed:
273
807
  missingApis.length === 0 &&
274
808
  backendSdkPresent &&
809
+ backendSdkInstallabilityVerified &&
275
810
  backendInitPresent &&
276
811
  backendMissingApis.length === 0 &&
812
+ frontendSdkPresent &&
813
+ frontendLifecycleFilesWithoutVerifiedSdk.length === 0 &&
814
+ wrongFrontendSdkPackages.length === 0 &&
277
815
  !noOpPattern.test(combined) &&
278
816
  componentLifecycleInitFiles.length === 0 &&
279
- unguardedLifecycleFiles.length === 0,
817
+ blockingLifecycleFiles.length === 0,
280
818
  };
281
819
  };
282
820
 
@@ -288,22 +826,39 @@ export const runSetupCheck = async ({
288
826
  }) => {
289
827
  const resolvedRepoRoot = resolve(repoRoot ?? ".");
290
828
  const checks = [];
291
- const normalizedTarget = normalizeTarget(target);
829
+ const setupManifest = await readSetupManifest(resolvedRepoRoot);
830
+ const manifestContract = validateSetupManifestContract(setupManifest);
831
+ addCheck(
832
+ checks,
833
+ "setup_manifest_contract",
834
+ !manifestContract.checked || manifestContract.findings.length === 0,
835
+ manifestContract.checked
836
+ ? manifestContract.findings.length === 0
837
+ ? "setup manifest contains current AI setup responsibility boundaries"
838
+ : "setup manifest is missing current AI setup responsibility boundaries"
839
+ : "setup manifest contract was not checked because .clue/setup-manifest.json is absent or unreadable",
840
+ {
841
+ checked: manifestContract.checked,
842
+ findings: manifestContract.findings,
843
+ },
844
+ );
845
+ const normalizedTarget =
846
+ normalizeTarget(target) ?? normalizeTarget(setupManifest?.target);
292
847
 
293
848
  if (normalizedTarget) {
294
- const skillRoot = join(
295
- resolvedRepoRoot,
296
- ...TARGET_SKILL_ROOTS[normalizedTarget],
297
- );
849
+ const rootParts = TARGET_SKILL_ROOTS[normalizedTarget];
850
+ const skillRoot = join(resolvedRepoRoot, ...rootParts);
298
851
  const missingSkills = [];
299
852
  for (const skillName of SETUP_SKILLS) {
300
853
  const skillPath = join(skillRoot, skillName, "SKILL.md");
301
854
  if (!(await exists(skillPath))) {
302
- missingSkills.push(
303
- join(...TARGET_SKILL_ROOTS[normalizedTarget], skillName, "SKILL.md"),
304
- );
855
+ missingSkills.push(join(...rootParts, skillName, "SKILL.md"));
305
856
  }
306
857
  }
858
+ const missingRequiredPhrases = await validateSetupSkillContent({
859
+ skillRoot,
860
+ rootParts,
861
+ });
307
862
  addCheck(
308
863
  checks,
309
864
  "setup_skills",
@@ -313,6 +868,36 @@ export const runSetupCheck = async ({
313
868
  : "setup skills are missing",
314
869
  { missing_files: missingSkills },
315
870
  );
871
+ addCheck(
872
+ checks,
873
+ "setup_skill_content",
874
+ missingSkills.length === 0 && missingRequiredPhrases.length === 0,
875
+ missingSkills.length === 0 && missingRequiredPhrases.length === 0
876
+ ? "setup skills contain current safety rules"
877
+ : "setup skills are stale or missing required safety rules",
878
+ {
879
+ expected_version: SETUP_SKILL_CONTENT_VERSION,
880
+ missing_required_phrases: missingRequiredPhrases,
881
+ },
882
+ );
883
+ } else {
884
+ addCheck(
885
+ checks,
886
+ "setup_skills",
887
+ false,
888
+ "setup target is required or must be inferable from .clue/setup-manifest.json",
889
+ { missing_target: true },
890
+ );
891
+ addCheck(
892
+ checks,
893
+ "setup_skill_content",
894
+ false,
895
+ "setup skills were not checked because setup target is missing",
896
+ {
897
+ expected_version: SETUP_SKILL_CONTENT_VERSION,
898
+ missing_required_phrases: [],
899
+ },
900
+ );
316
901
  }
317
902
 
318
903
  const workflowPath = request?.ci_workflow_path ?? DEFAULT_WORKFLOW_PATH;
@@ -323,9 +908,7 @@ export const runSetupCheck = async ({
323
908
  addCheck(
324
909
  checks,
325
910
  "semantic_workflow",
326
- workflow.includes(
327
- "npx @clue-ai/cli semantic-gen --request-env CLUE_SEMANTIC_REQUEST_JSON --repo .",
328
- ) &&
911
+ workflowHasCompatibleSemanticGenCommand(workflow) &&
329
912
  semanticRequest !== null &&
330
913
  workflow.includes("CLUE_SEMANTIC_REQUEST_JSON: |") &&
331
914
  workflow.includes("CLUE_API_KEY: ${{ secrets.CLUE_API_KEY }}") &&
@@ -341,7 +924,7 @@ export const runSetupCheck = async ({
341
924
  workflow.includes("permissions:\n contents: read") &&
342
925
  workflow.includes("persist-credentials: false") &&
343
926
  !disallowedWorkflowMetadataPattern.test(workflow),
344
- "semantic workflow uses the canonical env runtime request, least-privilege checkout, and privacy-minimized GitHub metadata",
927
+ "semantic workflow uses a compatible env runtime request, least-privilege checkout, and privacy-minimized GitHub metadata",
345
928
  { workflow_path: workflowPath },
346
929
  );
347
930
  } else {
@@ -406,7 +989,7 @@ export const runSetupCheck = async ({
406
989
  });
407
990
  const dependencySources = await readDependencyText({
408
991
  repoRoot: resolvedRepoRoot,
409
- roots: request?.allowed_source_paths ?? [],
992
+ roots: sourcePaths,
410
993
  });
411
994
  const secretLeaks = findSecretLeaks([
412
995
  ...sources,
@@ -434,22 +1017,26 @@ export const runSetupCheck = async ({
434
1017
  const sdkLifecycle = checkSdkLifecycle({
435
1018
  backendRootPaths: request?.allowed_source_paths ?? [],
436
1019
  dependencySources,
1020
+ framework: request?.framework,
437
1021
  sources,
438
1022
  });
439
1023
  addCheck(
440
1024
  checks,
441
1025
  "sdk_lifecycle",
442
1026
  sdkLifecycle.passed,
443
- "static SDK lifecycle references are present and guarded; dependency install, import, app startup, and event delivery are not verified by this check",
1027
+ "static SDK lifecycle references are present and non-blocking; dependency install, import, app startup, and event delivery are not verified by this check",
444
1028
  {
445
1029
  found_apis: sdkLifecycle.foundApis,
446
1030
  missing_apis: sdkLifecycle.missingApis,
447
1031
  verification_scope:
448
1032
  "static_source_only_not_dependency_install_or_runtime_event_delivery",
449
1033
  backend_lifecycle: sdkLifecycle.backend_lifecycle,
1034
+ frontend_lifecycle: sdkLifecycle.frontend_lifecycle,
450
1035
  has_noop_wrapper: sdkLifecycle.has_noop_wrapper,
451
1036
  component_lifecycle_init_files:
452
1037
  sdkLifecycle.component_lifecycle_init_files,
1038
+ blocking_lifecycle_files: sdkLifecycle.blocking_lifecycle_files,
1039
+ blocking_lifecycle_calls: sdkLifecycle.blocking_lifecycle_calls,
453
1040
  unguarded_lifecycle_files: sdkLifecycle.unguarded_lifecycle_files,
454
1041
  unguarded_lifecycle_calls: sdkLifecycle.unguarded_lifecycle_calls,
455
1042
  },
@@ -459,6 +1046,13 @@ export const runSetupCheck = async ({
459
1046
  const passed = checks.every((check) => check.passed);
460
1047
  return {
461
1048
  passed,
1049
+ static_passed: passed,
1050
+ completion_status:
1051
+ passed && requireSdkLifecycle
1052
+ ? "static_passed_runtime_verification_required"
1053
+ : passed
1054
+ ? "passed"
1055
+ : "failed",
462
1056
  checks,
463
1057
  };
464
1058
  };