@clue-ai/cli 0.0.5 → 0.0.6

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,8 +1,8 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
2
  import { join, relative, resolve } from "node:path";
3
3
  import {
4
- findLifecycleCallApiNames,
5
- findLifecycleGuardViolations,
4
+ findLifecycleCallApiNames,
5
+ findLifecycleGuardViolations,
6
6
  } from "./lifecycle-guard.mjs";
7
7
  import { listAllowedSourceFiles } from "./path-policy.mjs";
8
8
  import { runSemanticInventory } from "./semantic-ci.mjs";
@@ -10,426 +10,427 @@ import { runSemanticInventory } from "./semantic-ci.mjs";
10
10
  const DEFAULT_WORKFLOW_PATH = ".github/workflows/clue-semantic-snapshot.yml";
11
11
 
12
12
  const disallowedWorkflowMetadataPattern =
13
- /github\.(actor|triggering_actor|repository_owner)|github\.event\.sender|github\.event\.repository\.name|"default_branch"\s*:/;
13
+ /github\.(actor|triggering_actor|repository_owner)|github\.event\.sender|github\.event\.repository\.name|"default_branch"\s*:/;
14
14
  const SETUP_SKILLS = [
15
- "clue-setup-orchestrator",
16
- "clue-route-semantic-snapshot",
17
- "clue-semantic-ci",
18
- "clue-sdk-instrumentation",
19
- "clue-setup-audit",
20
- "clue-local-verification",
21
- "clue-setup-report",
15
+ "clue-setup-orchestrator",
16
+ "clue-route-semantic-snapshot",
17
+ "clue-semantic-ci",
18
+ "clue-sdk-instrumentation",
19
+ "clue-setup-audit",
20
+ "clue-local-verification",
21
+ "clue-setup-report",
22
22
  ];
23
23
  const TARGET_SKILL_ROOTS = {
24
- codex: [".agents", "skills"],
25
- claude_code: [".claude", "skills"],
24
+ codex: [".agents", "skills"],
25
+ claude_code: [".claude", "skills"],
26
26
  };
27
27
  const SOURCE_EXTENSIONS = [".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
28
28
  const REQUIRED_LIFECYCLE_APIS = [
29
- "ClueInit",
30
- "ClueIdentify",
31
- "ClueSetAccount",
32
- "ClueLogout",
29
+ "ClueInit",
30
+ "ClueIdentify",
31
+ "ClueSetAccount",
32
+ "ClueLogout",
33
33
  ];
34
34
  const BACKEND_SDK_MARKERS = [
35
- "clue-fastapi-sdk",
36
- "clue-django-sdk",
37
- "clue-python-sdk-core",
38
- "clue_fastapi_sdk",
39
- "clue_django_sdk",
40
- "clue_python_sdk_core",
35
+ "clue-fastapi-sdk",
36
+ "clue-django-sdk",
37
+ "clue-python-sdk-core",
38
+ "clue_fastapi_sdk",
39
+ "clue_django_sdk",
40
+ "clue_python_sdk_core",
41
41
  ];
42
42
  const DEPENDENCY_FILE_CANDIDATES = [
43
- "package.json",
44
- "pnpm-lock.yaml",
45
- "package-lock.json",
46
- "yarn.lock",
47
- "requirements.txt",
48
- "requirements-dev.txt",
49
- "pyproject.toml",
50
- "poetry.lock",
51
- "Pipfile",
43
+ "package.json",
44
+ "pnpm-lock.yaml",
45
+ "package-lock.json",
46
+ "yarn.lock",
47
+ "requirements.txt",
48
+ "requirements-dev.txt",
49
+ "pyproject.toml",
50
+ "poetry.lock",
51
+ "Pipfile",
52
52
  ];
53
53
  const exists = async (path) => {
54
- try {
55
- await access(path);
56
- return true;
57
- } catch {
58
- return false;
59
- }
54
+ try {
55
+ await access(path);
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
60
  };
61
61
 
62
62
  const normalizeTarget = (target) => {
63
- if (typeof target !== "string" || target.trim() === "") return undefined;
64
- const normalized = target
65
- .trim()
66
- .toLowerCase()
67
- .replace(/[\s-]+/g, "_");
68
- if (!TARGET_SKILL_ROOTS[normalized]) {
69
- throw new Error("target must be codex or claude_code");
70
- }
71
- return normalized;
63
+ if (typeof target !== "string" || target.trim() === "") return undefined;
64
+ const normalized = target
65
+ .trim()
66
+ .toLowerCase()
67
+ .replace(/[\s-]+/g, "_");
68
+ if (!TARGET_SKILL_ROOTS[normalized]) {
69
+ throw new Error("target must be codex or claude_code");
70
+ }
71
+ return normalized;
72
72
  };
73
73
 
74
74
  const addCheck = (checks, id, passed, summary, details = {}) => {
75
- checks.push({ id, passed, summary, ...details });
75
+ checks.push({ id, passed, summary, ...details });
76
76
  };
77
77
 
78
78
  const readAllowedSourceText = async ({
79
- repoRoot,
80
- allowedSourcePaths,
81
- excludedSourcePaths,
79
+ repoRoot,
80
+ allowedSourcePaths,
81
+ excludedSourcePaths,
82
82
  }) => {
83
- const files = await listAllowedSourceFiles({
84
- repoRoot,
85
- allowedSourcePaths,
86
- excludedSourcePaths,
87
- extensions: SOURCE_EXTENSIONS,
88
- });
89
- const sources = [];
90
- for (const absolutePath of files) {
91
- sources.push({
92
- file_path: relative(resolve(repoRoot), absolutePath),
93
- text: await readFile(absolutePath, "utf8"),
94
- });
95
- }
96
- return sources;
83
+ const files = await listAllowedSourceFiles({
84
+ repoRoot,
85
+ allowedSourcePaths,
86
+ excludedSourcePaths,
87
+ extensions: SOURCE_EXTENSIONS,
88
+ });
89
+ const sources = [];
90
+ for (const absolutePath of files) {
91
+ sources.push({
92
+ file_path: relative(resolve(repoRoot), absolutePath),
93
+ text: await readFile(absolutePath, "utf8"),
94
+ });
95
+ }
96
+ return sources;
97
97
  };
98
98
 
99
99
  const readDependencyText = async ({ repoRoot, roots }) => {
100
- const candidatePaths = [
101
- ...DEPENDENCY_FILE_CANDIDATES,
102
- ...roots.flatMap((root) =>
103
- DEPENDENCY_FILE_CANDIDATES.map((file) => join(root, file)),
104
- ),
105
- ];
106
- const sources = [];
107
- for (const path of [...new Set(candidatePaths)]) {
108
- const absolutePath = join(repoRoot, path);
109
- if (!(await exists(absolutePath))) continue;
110
- sources.push({
111
- file_path: path,
112
- text: await readFile(absolutePath, "utf8"),
113
- });
114
- }
115
- return sources;
100
+ const candidatePaths = [
101
+ ...DEPENDENCY_FILE_CANDIDATES,
102
+ ...roots.flatMap((root) =>
103
+ DEPENDENCY_FILE_CANDIDATES.map((file) => join(root, file)),
104
+ ),
105
+ ];
106
+ const sources = [];
107
+ for (const path of [...new Set(candidatePaths)]) {
108
+ const absolutePath = join(repoRoot, path);
109
+ if (!(await exists(absolutePath))) continue;
110
+ sources.push({
111
+ file_path: path,
112
+ text: await readFile(absolutePath, "utf8"),
113
+ });
114
+ }
115
+ return sources;
116
116
  };
117
117
 
118
118
  const setupSourcePaths = async ({ repoRoot, request, includeFrontend }) => {
119
- const requested = request?.allowed_source_paths?.length
120
- ? request.allowed_source_paths
121
- : ["."];
122
- if (!includeFrontend) return requested;
123
- const candidates = [
124
- ...requested,
125
- "frontend/src",
126
- "src",
127
- "app",
128
- "apps/web/src",
129
- "packages/frontend",
130
- ];
131
- const existing = [];
132
- for (const candidate of [...new Set(candidates)]) {
133
- if (await exists(join(repoRoot, candidate))) {
134
- existing.push(candidate);
135
- }
136
- }
137
- return existing.length ? existing : requested;
119
+ const requested = request?.allowed_source_paths?.length
120
+ ? request.allowed_source_paths
121
+ : ["."];
122
+ if (!includeFrontend) return requested;
123
+ const candidates = [
124
+ ...requested,
125
+ "frontend/src",
126
+ "src",
127
+ "app",
128
+ "apps/web/src",
129
+ "packages/frontend",
130
+ ];
131
+ const existing = [];
132
+ for (const candidate of [...new Set(candidates)]) {
133
+ if (await exists(join(repoRoot, candidate))) {
134
+ existing.push(candidate);
135
+ }
136
+ }
137
+ return existing.length ? existing : requested;
138
138
  };
139
139
 
140
140
  const secretLeakPatterns = [
141
- /pk_(live|test)_[A-Za-z0-9_-]+/,
142
- /sk_(live|test)_[A-Za-z0-9_-]+/,
143
- /npm_[A-Za-z0-9]{20,}/,
144
- /CLUE_API_KEY\s*[:=]\s*["'][^"']+["']/,
145
- /AI_PROVIDER_API_KEY\s*[:=]\s*["'][^"']+["']/,
141
+ /pk_(live|test)_[A-Za-z0-9_-]+/,
142
+ /sk_(live|test)_[A-Za-z0-9_-]+/,
143
+ /npm_[A-Za-z0-9]{20,}/,
144
+ /CLUE_API_KEY\s*[:=]\s*["'][^"']+["']/,
145
+ /CLUE_AI_PROVIDER_API_KEY\s*[:=]\s*["'][^"']+["']/,
146
146
  ];
147
147
 
148
148
  const findSecretLeaks = (sources) =>
149
- sources.flatMap((source) =>
150
- secretLeakPatterns.some((pattern) => pattern.test(source.text))
151
- ? [source.file_path]
152
- : [],
153
- );
149
+ sources.flatMap((source) =>
150
+ secretLeakPatterns.some((pattern) => pattern.test(source.text))
151
+ ? [source.file_path]
152
+ : [],
153
+ );
154
154
 
155
155
  const startsWithRoot = (filePath, root) =>
156
- filePath === root || filePath.startsWith(`${root.replace(/\/+$/, "")}/`);
156
+ filePath === root || filePath.startsWith(`${root.replace(/\/+$/, "")}/`);
157
157
 
158
158
  const hasAnyMarker = (text, markers) =>
159
- markers.some((marker) => text.includes(marker));
159
+ markers.some((marker) => text.includes(marker));
160
160
 
161
161
  const checkSdkLifecycle = ({
162
- backendRootPaths = [],
163
- dependencySources = [],
164
- sources,
162
+ backendRootPaths = [],
163
+ dependencySources = [],
164
+ sources,
165
165
  }) => {
166
- const combined = sources.map((source) => source.text).join("\n");
167
- const backendSources = sources.filter((source) =>
168
- backendRootPaths.some((root) => startsWithRoot(source.file_path, root)),
169
- );
170
- const backendCombined = backendSources
171
- .map((source) => source.text)
172
- .join("\n");
173
- const dependencyCombined = dependencySources
174
- .map((source) => source.text)
175
- .join("\n");
176
- const foundApiNames = findLifecycleCallApiNames(combined);
177
- const backendFoundApiNames = findLifecycleCallApiNames(backendCombined);
178
- const foundApis = REQUIRED_LIFECYCLE_APIS.filter((api) =>
179
- foundApiNames.includes(api),
180
- );
181
- const missingApis = REQUIRED_LIFECYCLE_APIS.filter(
182
- (api) => !foundApis.includes(api),
183
- );
184
- const noOpPattern =
185
- /window\.Clue(?:Init|Identify|SetAccount|Logout)|(?:function|const|let|var)\s+Clue(?:Init|Identify|SetAccount|Logout)\b/;
186
- const componentLifecycleInitFiles = sources
187
- .filter((source) =>
188
- /useEffect\s*\([\s\S]{0,1200}ClueInit/.test(source.text),
189
- )
190
- .map((source) => source.file_path);
191
- const unguardedLifecycleCalls = sources.flatMap((source) =>
192
- findLifecycleGuardViolations(source.text).map((violation) => ({
193
- file_path: source.file_path,
194
- ...violation,
195
- })),
196
- );
197
- const unguardedLifecycleFiles = [
198
- ...new Set(unguardedLifecycleCalls.map((violation) => violation.file_path)),
199
- ];
200
- const backendPresent = backendSources.length > 0;
201
- const backendSdkPresent =
202
- !backendPresent ||
203
- hasAnyMarker(
204
- `${backendCombined}\n${dependencyCombined}`,
205
- BACKEND_SDK_MARKERS,
206
- );
207
- const backendInitPresent =
208
- !backendPresent ||
209
- /clue_init_fastapi|clue_init_django|configure_settings|CluePythonBootstrapConfig/.test(
210
- backendCombined,
211
- );
212
- const backendIdentityRequired =
213
- backendPresent &&
214
- /\b(login|signin|sign_in|auth|token|session)\b/i.test(backendCombined);
215
- const backendLogoutRequired =
216
- backendPresent && /\b(logout|signout|sign_out)\b/i.test(backendCombined);
217
- const backendAccountRequired =
218
- backendPresent &&
219
- /\b(account|workspace|organization|tenant)\b/i.test(backendCombined);
220
- const backendMissingApis = [
221
- ...(backendIdentityRequired &&
222
- !backendFoundApiNames.includes("ClueIdentify")
223
- ? ["ClueIdentify"]
224
- : []),
225
- ...(backendLogoutRequired && !backendFoundApiNames.includes("ClueLogout")
226
- ? ["ClueLogout"]
227
- : []),
228
- ...(backendAccountRequired &&
229
- !backendFoundApiNames.includes("ClueSetAccount")
230
- ? ["ClueSetAccount"]
231
- : []),
232
- ];
233
- return {
234
- foundApis,
235
- missingApis,
236
- backend_lifecycle: {
237
- backend_present: backendPresent,
238
- backend_root_paths: backendRootPaths,
239
- dependency_files: dependencySources.map((source) => source.file_path),
240
- sdk_dependency_or_import_present: backendSdkPresent,
241
- sdk_init_present: backendInitPresent,
242
- required_apis: [
243
- ...(backendIdentityRequired ? ["ClueIdentify"] : []),
244
- ...(backendAccountRequired ? ["ClueSetAccount"] : []),
245
- ...(backendLogoutRequired ? ["ClueLogout"] : []),
246
- ],
247
- missing_apis: backendMissingApis,
248
- },
249
- has_noop_wrapper: noOpPattern.test(combined),
250
- component_lifecycle_init_files: componentLifecycleInitFiles,
251
- unguarded_lifecycle_files: unguardedLifecycleFiles,
252
- unguarded_lifecycle_calls: unguardedLifecycleCalls,
253
- passed:
254
- missingApis.length === 0 &&
255
- backendSdkPresent &&
256
- backendInitPresent &&
257
- backendMissingApis.length === 0 &&
258
- !noOpPattern.test(combined) &&
259
- componentLifecycleInitFiles.length === 0 &&
260
- unguardedLifecycleFiles.length === 0,
261
- };
166
+ const combined = sources.map((source) => source.text).join("\n");
167
+ const backendSources = sources.filter((source) =>
168
+ backendRootPaths.some((root) => startsWithRoot(source.file_path, root)),
169
+ );
170
+ const backendCombined = backendSources
171
+ .map((source) => source.text)
172
+ .join("\n");
173
+ const dependencyCombined = dependencySources
174
+ .map((source) => source.text)
175
+ .join("\n");
176
+ const foundApiNames = findLifecycleCallApiNames(combined);
177
+ const backendFoundApiNames = findLifecycleCallApiNames(backendCombined);
178
+ const foundApis = REQUIRED_LIFECYCLE_APIS.filter((api) =>
179
+ foundApiNames.includes(api),
180
+ );
181
+ const missingApis = REQUIRED_LIFECYCLE_APIS.filter(
182
+ (api) => !foundApis.includes(api),
183
+ );
184
+ const noOpPattern =
185
+ /window\.Clue(?:Init|Identify|SetAccount|Logout)|(?:function|const|let|var)\s+Clue(?:Init|Identify|SetAccount|Logout)\b/;
186
+ const componentLifecycleInitFiles = sources
187
+ .filter((source) =>
188
+ /useEffect\s*\([\s\S]{0,1200}ClueInit/.test(source.text),
189
+ )
190
+ .map((source) => source.file_path);
191
+ const unguardedLifecycleCalls = sources.flatMap((source) =>
192
+ findLifecycleGuardViolations(source.text).map((violation) => ({
193
+ file_path: source.file_path,
194
+ ...violation,
195
+ })),
196
+ );
197
+ const unguardedLifecycleFiles = [
198
+ ...new Set(unguardedLifecycleCalls.map((violation) => violation.file_path)),
199
+ ];
200
+ const backendPresent = backendSources.length > 0;
201
+ const backendSdkPresent =
202
+ !backendPresent ||
203
+ hasAnyMarker(
204
+ `${backendCombined}\n${dependencyCombined}`,
205
+ BACKEND_SDK_MARKERS,
206
+ );
207
+ const backendInitPresent =
208
+ !backendPresent ||
209
+ /clue_init_fastapi|clue_init_django|configure_settings|CluePythonBootstrapConfig/.test(
210
+ backendCombined,
211
+ );
212
+ const backendIdentityRequired =
213
+ backendPresent &&
214
+ /\b(login|signin|sign_in|auth|token|session)\b/i.test(backendCombined);
215
+ const backendLogoutRequired =
216
+ backendPresent && /\b(logout|signout|sign_out)\b/i.test(backendCombined);
217
+ const backendAccountRequired =
218
+ backendPresent &&
219
+ /\b(account|workspace|organization|tenant)\b/i.test(backendCombined);
220
+ const backendMissingApis = [
221
+ ...(backendIdentityRequired &&
222
+ !backendFoundApiNames.includes("ClueIdentify")
223
+ ? ["ClueIdentify"]
224
+ : []),
225
+ ...(backendLogoutRequired && !backendFoundApiNames.includes("ClueLogout")
226
+ ? ["ClueLogout"]
227
+ : []),
228
+ ...(backendAccountRequired &&
229
+ !backendFoundApiNames.includes("ClueSetAccount")
230
+ ? ["ClueSetAccount"]
231
+ : []),
232
+ ];
233
+ return {
234
+ foundApis,
235
+ missingApis,
236
+ backend_lifecycle: {
237
+ backend_present: backendPresent,
238
+ backend_root_paths: backendRootPaths,
239
+ dependency_files: dependencySources.map((source) => source.file_path),
240
+ sdk_dependency_or_import_present: backendSdkPresent,
241
+ sdk_init_present: backendInitPresent,
242
+ required_apis: [
243
+ ...(backendIdentityRequired ? ["ClueIdentify"] : []),
244
+ ...(backendAccountRequired ? ["ClueSetAccount"] : []),
245
+ ...(backendLogoutRequired ? ["ClueLogout"] : []),
246
+ ],
247
+ missing_apis: backendMissingApis,
248
+ },
249
+ has_noop_wrapper: noOpPattern.test(combined),
250
+ component_lifecycle_init_files: componentLifecycleInitFiles,
251
+ unguarded_lifecycle_files: unguardedLifecycleFiles,
252
+ unguarded_lifecycle_calls: unguardedLifecycleCalls,
253
+ passed:
254
+ missingApis.length === 0 &&
255
+ backendSdkPresent &&
256
+ backendInitPresent &&
257
+ backendMissingApis.length === 0 &&
258
+ !noOpPattern.test(combined) &&
259
+ componentLifecycleInitFiles.length === 0 &&
260
+ unguardedLifecycleFiles.length === 0,
261
+ };
262
262
  };
263
263
 
264
264
  export const runSetupCheck = async ({
265
- repoRoot,
266
- target,
267
- request,
268
- requireSdkLifecycle = false,
265
+ repoRoot,
266
+ target,
267
+ request,
268
+ requireSdkLifecycle = false,
269
269
  }) => {
270
- const resolvedRepoRoot = resolve(repoRoot ?? ".");
271
- const checks = [];
272
- const normalizedTarget = normalizeTarget(target);
270
+ const resolvedRepoRoot = resolve(repoRoot ?? ".");
271
+ const checks = [];
272
+ const normalizedTarget = normalizeTarget(target);
273
273
 
274
- if (normalizedTarget) {
275
- const skillRoot = join(
276
- resolvedRepoRoot,
277
- ...TARGET_SKILL_ROOTS[normalizedTarget],
278
- );
279
- const missingSkills = [];
280
- for (const skillName of SETUP_SKILLS) {
281
- const skillPath = join(skillRoot, skillName, "SKILL.md");
282
- if (!(await exists(skillPath))) {
283
- missingSkills.push(
284
- join(...TARGET_SKILL_ROOTS[normalizedTarget], skillName, "SKILL.md"),
285
- );
286
- }
287
- }
288
- addCheck(
289
- checks,
290
- "setup_skills",
291
- missingSkills.length === 0,
292
- missingSkills.length === 0
293
- ? "setup skills are installed"
294
- : "setup skills are missing",
295
- { missing_files: missingSkills },
296
- );
297
- }
274
+ if (normalizedTarget) {
275
+ const skillRoot = join(
276
+ resolvedRepoRoot,
277
+ ...TARGET_SKILL_ROOTS[normalizedTarget],
278
+ );
279
+ const missingSkills = [];
280
+ for (const skillName of SETUP_SKILLS) {
281
+ const skillPath = join(skillRoot, skillName, "SKILL.md");
282
+ if (!(await exists(skillPath))) {
283
+ missingSkills.push(
284
+ join(...TARGET_SKILL_ROOTS[normalizedTarget], skillName, "SKILL.md"),
285
+ );
286
+ }
287
+ }
288
+ addCheck(
289
+ checks,
290
+ "setup_skills",
291
+ missingSkills.length === 0,
292
+ missingSkills.length === 0
293
+ ? "setup skills are installed"
294
+ : "setup skills are missing",
295
+ { missing_files: missingSkills },
296
+ );
297
+ }
298
298
 
299
- const workflowPath = request?.ci_workflow_path ?? DEFAULT_WORKFLOW_PATH;
300
- const absoluteWorkflowPath = join(resolvedRepoRoot, workflowPath);
301
- if (await exists(absoluteWorkflowPath)) {
302
- const workflow = await readFile(absoluteWorkflowPath, "utf8");
303
- addCheck(
304
- checks,
305
- "semantic_workflow",
306
- workflow.includes(
307
- "npx @clue-ai/cli semantic-ci --request-env CLUE_SEMANTIC_REQUEST_JSON --repo .",
308
- ) &&
309
- workflow.includes("CLUE_SEMANTIC_REQUEST_JSON: |") &&
310
- workflow.includes("CLUE_API_KEY: ${{ secrets.CLUE_API_KEY }}") &&
311
- workflow.includes(
312
- "AI_PROVIDER_API_KEY: ${{ secrets.AI_PROVIDER_API_KEY }}",
313
- ) &&
314
- workflow.includes("permissions:\n contents: read") &&
315
- workflow.includes("persist-credentials: false") &&
316
- !disallowedWorkflowMetadataPattern.test(workflow),
317
- "semantic workflow uses the canonical env runtime request, least-privilege checkout, and privacy-minimized GitHub metadata",
318
- { workflow_path: workflowPath },
319
- );
320
- } else {
321
- addCheck(
322
- checks,
323
- "semantic_workflow",
324
- false,
325
- "semantic workflow is missing",
326
- {
327
- workflow_path: workflowPath,
328
- },
329
- );
330
- }
299
+ const workflowPath = request?.ci_workflow_path ?? DEFAULT_WORKFLOW_PATH;
300
+ const absoluteWorkflowPath = join(resolvedRepoRoot, workflowPath);
301
+ if (await exists(absoluteWorkflowPath)) {
302
+ const workflow = await readFile(absoluteWorkflowPath, "utf8");
303
+ addCheck(
304
+ checks,
305
+ "semantic_workflow",
306
+ workflow.includes(
307
+ "npx @clue-ai/cli semantic-ci --request-env CLUE_SEMANTIC_REQUEST_JSON --repo .",
308
+ ) &&
309
+ workflow.includes("CLUE_SEMANTIC_REQUEST_JSON: |") &&
310
+ workflow.includes("CLUE_API_KEY: ${{ secrets.CLUE_API_KEY }}") &&
311
+ workflow.includes(
312
+ "CLUE_AI_PROVIDER_API_KEY: ${{ secrets.CLUE_AI_PROVIDER_API_KEY }}",
313
+ ) &&
314
+ workflow.includes("CLUE_AI_PROVIDER: ${{ vars.CLUE_AI_PROVIDER }}") &&
315
+ workflow.includes("permissions:\n contents: read") &&
316
+ workflow.includes("persist-credentials: false") &&
317
+ !disallowedWorkflowMetadataPattern.test(workflow),
318
+ "semantic workflow uses the canonical env runtime request, least-privilege checkout, and privacy-minimized GitHub metadata",
319
+ { workflow_path: workflowPath },
320
+ );
321
+ } else {
322
+ addCheck(
323
+ checks,
324
+ "semantic_workflow",
325
+ false,
326
+ "semantic workflow is missing",
327
+ {
328
+ workflow_path: workflowPath,
329
+ },
330
+ );
331
+ }
331
332
 
332
- addCheck(
333
- checks,
334
- "runtime_request_not_committed",
335
- !(await exists(
336
- join(resolvedRepoRoot, ".clue", "semantic-request.runtime.json"),
337
- )),
338
- ".clue/semantic-request.runtime.json is not present in the repository",
339
- );
333
+ addCheck(
334
+ checks,
335
+ "runtime_request_not_committed",
336
+ !(await exists(
337
+ join(resolvedRepoRoot, ".clue", "semantic-request.runtime.json"),
338
+ )),
339
+ ".clue/semantic-request.runtime.json is not present in the repository",
340
+ );
340
341
 
341
- if (request) {
342
- try {
343
- const inventory = await runSemanticInventory({
344
- repoRoot: resolvedRepoRoot,
345
- request,
346
- });
347
- addCheck(
348
- checks,
349
- "semantic_inventory",
350
- true,
351
- "semantic inventory discovers routes",
352
- {
353
- route_count: inventory.route_count,
354
- },
355
- );
356
- } catch (error) {
357
- addCheck(
358
- checks,
359
- "semantic_inventory",
360
- false,
361
- "semantic inventory failed",
362
- {
363
- error: error instanceof Error ? error.message : String(error),
364
- },
365
- );
366
- }
367
- }
342
+ if (request) {
343
+ try {
344
+ const inventory = await runSemanticInventory({
345
+ repoRoot: resolvedRepoRoot,
346
+ request,
347
+ });
348
+ addCheck(
349
+ checks,
350
+ "semantic_inventory",
351
+ true,
352
+ "semantic inventory discovers routes",
353
+ {
354
+ route_count: inventory.route_count,
355
+ },
356
+ );
357
+ } catch (error) {
358
+ addCheck(
359
+ checks,
360
+ "semantic_inventory",
361
+ false,
362
+ "semantic inventory failed",
363
+ {
364
+ error: error instanceof Error ? error.message : String(error),
365
+ },
366
+ );
367
+ }
368
+ }
368
369
 
369
- const sourcePaths = await setupSourcePaths({
370
- repoRoot: resolvedRepoRoot,
371
- request,
372
- includeFrontend: requireSdkLifecycle,
373
- });
374
- const excludedPaths = request?.excluded_source_paths ?? [];
375
- const sources = await readAllowedSourceText({
376
- repoRoot: resolvedRepoRoot,
377
- allowedSourcePaths: sourcePaths,
378
- excludedSourcePaths: excludedPaths,
379
- });
380
- const dependencySources = await readDependencyText({
381
- repoRoot: resolvedRepoRoot,
382
- roots: request?.allowed_source_paths ?? [],
383
- });
384
- const secretLeaks = findSecretLeaks([
385
- ...sources,
386
- ...dependencySources,
387
- ...((await exists(absoluteWorkflowPath))
388
- ? [
389
- {
390
- file_path: workflowPath,
391
- text: await readFile(absoluteWorkflowPath, "utf8"),
392
- },
393
- ]
394
- : []),
395
- ]);
396
- addCheck(
397
- checks,
398
- "no_secret_values",
399
- secretLeaks.length === 0,
400
- "no obvious secret values found",
401
- {
402
- files: secretLeaks,
403
- },
404
- );
370
+ const sourcePaths = await setupSourcePaths({
371
+ repoRoot: resolvedRepoRoot,
372
+ request,
373
+ includeFrontend: requireSdkLifecycle,
374
+ });
375
+ const excludedPaths = request?.excluded_source_paths ?? [];
376
+ const sources = await readAllowedSourceText({
377
+ repoRoot: resolvedRepoRoot,
378
+ allowedSourcePaths: sourcePaths,
379
+ excludedSourcePaths: excludedPaths,
380
+ });
381
+ const dependencySources = await readDependencyText({
382
+ repoRoot: resolvedRepoRoot,
383
+ roots: request?.allowed_source_paths ?? [],
384
+ });
385
+ const secretLeaks = findSecretLeaks([
386
+ ...sources,
387
+ ...dependencySources,
388
+ ...((await exists(absoluteWorkflowPath))
389
+ ? [
390
+ {
391
+ file_path: workflowPath,
392
+ text: await readFile(absoluteWorkflowPath, "utf8"),
393
+ },
394
+ ]
395
+ : []),
396
+ ]);
397
+ addCheck(
398
+ checks,
399
+ "no_secret_values",
400
+ secretLeaks.length === 0,
401
+ "no obvious secret values found",
402
+ {
403
+ files: secretLeaks,
404
+ },
405
+ );
405
406
 
406
- if (requireSdkLifecycle) {
407
- const sdkLifecycle = checkSdkLifecycle({
408
- backendRootPaths: request?.allowed_source_paths ?? [],
409
- dependencySources,
410
- sources,
411
- });
412
- addCheck(
413
- checks,
414
- "sdk_lifecycle",
415
- sdkLifecycle.passed,
416
- "SDK lifecycle calls resolve to real source code instead of no-op globals",
417
- {
418
- found_apis: sdkLifecycle.foundApis,
419
- missing_apis: sdkLifecycle.missingApis,
420
- backend_lifecycle: sdkLifecycle.backend_lifecycle,
421
- has_noop_wrapper: sdkLifecycle.has_noop_wrapper,
422
- component_lifecycle_init_files:
423
- sdkLifecycle.component_lifecycle_init_files,
424
- unguarded_lifecycle_files: sdkLifecycle.unguarded_lifecycle_files,
425
- unguarded_lifecycle_calls: sdkLifecycle.unguarded_lifecycle_calls,
426
- },
427
- );
428
- }
407
+ if (requireSdkLifecycle) {
408
+ const sdkLifecycle = checkSdkLifecycle({
409
+ backendRootPaths: request?.allowed_source_paths ?? [],
410
+ dependencySources,
411
+ sources,
412
+ });
413
+ addCheck(
414
+ checks,
415
+ "sdk_lifecycle",
416
+ sdkLifecycle.passed,
417
+ "SDK lifecycle calls resolve to real source code instead of no-op globals",
418
+ {
419
+ found_apis: sdkLifecycle.foundApis,
420
+ missing_apis: sdkLifecycle.missingApis,
421
+ backend_lifecycle: sdkLifecycle.backend_lifecycle,
422
+ has_noop_wrapper: sdkLifecycle.has_noop_wrapper,
423
+ component_lifecycle_init_files:
424
+ sdkLifecycle.component_lifecycle_init_files,
425
+ unguarded_lifecycle_files: sdkLifecycle.unguarded_lifecycle_files,
426
+ unguarded_lifecycle_calls: sdkLifecycle.unguarded_lifecycle_calls,
427
+ },
428
+ );
429
+ }
429
430
 
430
- const passed = checks.every((check) => check.passed);
431
- return {
432
- passed,
433
- checks,
434
- };
431
+ const passed = checks.every((check) => check.passed);
432
+ return {
433
+ passed,
434
+ checks,
435
+ };
435
436
  };