@clue-ai/cli 0.0.18 → 0.0.20

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.
@@ -644,6 +644,9 @@ const generateAiRoutesPerRoute = async ({
644
644
  );
645
645
  }
646
646
  } catch (error) {
647
+ if (error instanceof SyntaxError) {
648
+ throw error;
649
+ }
647
650
  aiRoutes.set(
648
651
  route.operation_source_key,
649
652
  unavailableAiRoute(
@@ -2066,25 +2069,31 @@ const buildSnapshot = ({
2066
2069
  };
2067
2070
  }
2068
2071
 
2072
+ const effectivePlan =
2073
+ !aiRoute?.semantics && plan.active_semantic_source === "new_confirmed"
2074
+ ? unconfirmedFallbackPlan({ plan, reason: unavailableReason })
2075
+ : plan;
2076
+
2069
2077
  return withStabilityMetadata({
2070
2078
  currentRoute: route,
2071
- plan,
2079
+ plan: effectivePlan,
2072
2080
  route: {
2073
2081
  ...baseRoute,
2074
2082
  operation_effects: assignmentCollections.operationEffects,
2075
2083
  unresolved_operation_effects:
2076
2084
  assignmentCollections.unresolvedOperationEffects,
2077
2085
  source_evidence_refs: routeEvidenceRefs.map((entry) => entry.id),
2078
- route_input_hash: plan.route_input_hash,
2079
- previous_route_input_hash: plan.previous_route_input_hash,
2080
- previous_route_semantic_hash: plan.previous_route_semantic_hash,
2086
+ route_input_hash: effectivePlan.route_input_hash,
2087
+ previous_route_input_hash: effectivePlan.previous_route_input_hash,
2088
+ previous_route_semantic_hash: effectivePlan.previous_route_semantic_hash,
2081
2089
  semantic_origin:
2082
- plan.origin === "changed_route_needs_review" && aiRoute?.semantics
2090
+ effectivePlan.origin === "changed_route_needs_review" &&
2091
+ aiRoute?.semantics
2083
2092
  ? "changed_route_needs_review"
2084
- : plan.origin,
2085
- semantic_change_reason: plan.semantic_change_reason,
2093
+ : effectivePlan.origin,
2094
+ semantic_change_reason: effectivePlan.semantic_change_reason,
2086
2095
  previous_semantic_snapshot_version:
2087
- plan.previous_route?.semantic_snapshot_version,
2096
+ effectivePlan.previous_route?.semantic_snapshot_version,
2088
2097
  route_semantic_hash: routeSemanticHash({
2089
2098
  ...baseRoute,
2090
2099
  operation_effects: assignmentCollections.operationEffects,
@@ -2463,8 +2472,16 @@ const routeSourceForPolicy = (route) =>
2463
2472
  const classifyClueInfrastructureRoute = (route) => {
2464
2473
  const source = routeSourceForPolicy(route);
2465
2474
  const reservedPath = isReservedClueBrowserTokenPath(route.path_template);
2466
- const proxiesToClueBrowserTokenApi =
2475
+ const directlyCallsClueBrowserTokenApi =
2467
2476
  /\/(?:api\/v[0-9]+\/)?ingest\/browser-tokens\b/i.test(source);
2477
+ const usesClueBrowserTokenRequestContract =
2478
+ /\bCLUE_API_BASE_URL\b/.test(source) &&
2479
+ /\bCLUE_PROJECT_KEY\b/.test(source) &&
2480
+ /\bCLUE_ENVIRONMENT\b/.test(source) &&
2481
+ /\bserviceKey\b/.test(source) &&
2482
+ /\b(?:client|httpx)\.post\b/.test(source);
2483
+ const proxiesToClueBrowserTokenApi =
2484
+ directlyCallsClueBrowserTokenApi || usesClueBrowserTokenRequestContract;
2468
2485
  const usesServerSideClueApiKey = /\bCLUE_API_KEY\b|x-clue-api-key/i.test(
2469
2486
  source,
2470
2487
  );
@@ -2481,6 +2498,8 @@ const classifyClueInfrastructureRoute = (route) => {
2481
2498
  evidence: {
2482
2499
  reserved_clue_path: true,
2483
2500
  proxies_to_clue_browser_token_api: true,
2501
+ uses_clue_browser_token_request_contract:
2502
+ usesClueBrowserTokenRequestContract,
2484
2503
  uses_server_side_clue_api_key: true,
2485
2504
  },
2486
2505
  };
@@ -2491,6 +2510,8 @@ const classifyClueInfrastructureRoute = (route) => {
2491
2510
  evidence: {
2492
2511
  reserved_clue_path: reservedPath,
2493
2512
  proxies_to_clue_browser_token_api: proxiesToClueBrowserTokenApi,
2513
+ uses_clue_browser_token_request_contract:
2514
+ usesClueBrowserTokenRequestContract,
2494
2515
  uses_server_side_clue_api_key: usesServerSideClueApiKey,
2495
2516
  },
2496
2517
  };
@@ -2853,6 +2874,19 @@ const withStabilityMetadata = ({ route, currentRoute, plan }) => ({
2853
2874
  evidence_packet_summary: buildEvidencePacketSummary(currentRoute),
2854
2875
  });
2855
2876
 
2877
+ const unconfirmedFallbackPlan = ({ plan, reason }) => ({
2878
+ ...plan,
2879
+ origin: "fallback",
2880
+ purpose_change_state: "insufficient_evidence",
2881
+ active_semantic_source: "new_unconfirmed",
2882
+ stability_confidence: 0,
2883
+ stability_missing_context: [
2884
+ ...safeArray(plan.stability_missing_context),
2885
+ "AI route semantic generation did not produce usable active semantics.",
2886
+ ],
2887
+ semantic_change_reason: reason,
2888
+ });
2889
+
2856
2890
  const fieldPathMatchesKeys = ({
2857
2891
  fieldPath,
2858
2892
  routeKeys,
@@ -3246,6 +3280,31 @@ const sendSnapshot = async ({ request, env, snapshot }) => {
3246
3280
  }
3247
3281
  };
3248
3282
 
3283
+ const routeHasUsableActiveSemantics = (route) =>
3284
+ route.semantics.route_confidence > 0 ||
3285
+ safeArray(route.operation_effects).length > 0 ||
3286
+ ["previous_reused", "previous_kept_pending_review"].includes(
3287
+ route.active_semantic_source,
3288
+ );
3289
+
3290
+ const assertSemanticSnapshotUploadable = (snapshot) => {
3291
+ const authFailureRoute = snapshot.routes.find((route) =>
3292
+ /provider_(401|403)\b/.test(route.confidence_reason ?? ""),
3293
+ );
3294
+ if (authFailureRoute) {
3295
+ throw new Error(
3296
+ `semantic snapshot generation failed before upload: AI provider authentication failed for ${authFailureRoute.operation_source_key}`,
3297
+ );
3298
+ }
3299
+ const usableRouteCount = snapshot.routes.filter(routeHasUsableActiveSemantics)
3300
+ .length;
3301
+ if (snapshot.routes.length > 0 && usableRouteCount === 0) {
3302
+ throw new Error(
3303
+ "semantic snapshot generation produced zero usable route semantics; refusing upload",
3304
+ );
3305
+ }
3306
+ };
3307
+
3249
3308
  const assertSemanticSnapshotAudit = ({
3250
3309
  routes,
3251
3310
  snapshot,
@@ -3274,6 +3333,7 @@ const assertSemanticSnapshotAudit = ({
3274
3333
  "changed_route_semantic_reused",
3275
3334
  "changed_route_semantic_regenerated",
3276
3335
  "changed_route_needs_review",
3336
+ "fallback",
3277
3337
  ]);
3278
3338
  for (const route of auditedSnapshot.routes) {
3279
3339
  const currentRoute = routeByKey.get(route.operation_source_key);
@@ -3347,6 +3407,25 @@ const assertSemanticSnapshotAudit = ({
3347
3407
  `semantic snapshot audit found missing previous metadata for added purpose: ${route.operation_source_key}`,
3348
3408
  );
3349
3409
  }
3410
+ if (
3411
+ route.active_semantic_source === "new_confirmed" &&
3412
+ route.semantics.route_confidence === 0 &&
3413
+ safeArray(route.operation_effects).length === 0
3414
+ ) {
3415
+ throw new Error(
3416
+ `semantic snapshot audit found confirmed semantics without usable evidence: ${route.operation_source_key}`,
3417
+ );
3418
+ }
3419
+ if (
3420
+ route.active_semantic_source === "new_unconfirmed" &&
3421
+ (route.semantic_origin !== "fallback" ||
3422
+ route.purpose_change_state !== "insufficient_evidence" ||
3423
+ route.semantic_stability.confidence !== 0)
3424
+ ) {
3425
+ throw new Error(
3426
+ `semantic snapshot audit found inconsistent unconfirmed fallback semantics: ${route.operation_source_key}`,
3427
+ );
3428
+ }
3350
3429
  }
3351
3430
  if (
3352
3431
  (route.semantic_origin === "unchanged_route_reused" ||
@@ -3495,6 +3574,7 @@ export const runSemanticCi = async ({
3495
3574
  generationContract,
3496
3575
  aiRuntime,
3497
3576
  });
3577
+ assertSemanticSnapshotUploadable(snapshot);
3498
3578
  const upload = await sendSnapshot({ request, env, snapshot });
3499
3579
  return {
3500
3580
  accepted: upload.accepted === true,
@@ -0,0 +1,504 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import {
4
+ applyLifecyclePlan,
5
+ planLifecycleInsertionsStrict,
6
+ } from "./lifecycle-init.mjs";
7
+ import { runSetupCheck } from "./setup-check.mjs";
8
+ import { runSetupDoctor } from "./setup-doctor.mjs";
9
+
10
+ const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
11
+ const DEFAULT_ENV_GUIDE_PATH = ".env.clue";
12
+ const DEFAULT_MAX_ATTEMPTS = 3;
13
+ const REQUIRED_SETUP_AGENT_AI_ENV_NAMES = [
14
+ "CLUE_AI_PROVIDER",
15
+ "CLUE_AI_PROVIDER_API_KEY",
16
+ "CLUE_AI_MODEL",
17
+ ];
18
+ const DEPENDENCY_WRITE_FILE_CANDIDATES = [
19
+ "package.json",
20
+ "package-lock.json",
21
+ "pnpm-lock.yaml",
22
+ "yarn.lock",
23
+ "bun.lock",
24
+ "requirements.txt",
25
+ "requirements-dev.txt",
26
+ "pyproject.toml",
27
+ "Pipfile",
28
+ "Pipfile.lock",
29
+ "poetry.lock",
30
+ ];
31
+
32
+ const optionalString = (value) =>
33
+ typeof value === "string" && value.trim() ? value.trim() : null;
34
+
35
+ const readJson = async (path) => JSON.parse(await readFile(path, "utf8"));
36
+
37
+ const parseEnvGuide = (text) => {
38
+ const values = {};
39
+ for (const line of text.split(/\r?\n/)) {
40
+ const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)\s*$/.exec(line);
41
+ if (!match) continue;
42
+ const value = match[2].trim().replace(/^(['"])(.*)\1$/, "$2");
43
+ if (/^<[^>]+>$/.test(value)) continue;
44
+ values[match[1]] = value;
45
+ }
46
+ return values;
47
+ };
48
+
49
+ const readEnvGuideIfPresent = async ({ repoRoot }) => {
50
+ try {
51
+ return parseEnvGuide(
52
+ await readFile(resolve(repoRoot, DEFAULT_ENV_GUIDE_PATH), "utf8"),
53
+ );
54
+ } catch (error) {
55
+ if (error?.code === "ENOENT") return {};
56
+ throw error;
57
+ }
58
+ };
59
+
60
+ const exists = async (path) => {
61
+ try {
62
+ await access(path);
63
+ return true;
64
+ } catch {
65
+ return false;
66
+ }
67
+ };
68
+
69
+ const normalizePositiveInteger = (value, fallback) => {
70
+ const parsed = Number.parseInt(String(value ?? ""), 10);
71
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
72
+ };
73
+
74
+ const aiProviderEnvFromFlags = ({ env, flags }) => {
75
+ const aiProvider = optionalString(flags.get("ai-provider"));
76
+ const aiProviderApiKey = optionalString(flags.get("ai-provider-api-key"));
77
+ const aiModel = optionalString(flags.get("ai-model"));
78
+ return {
79
+ ...env,
80
+ ...(aiProvider ? { CLUE_AI_PROVIDER: aiProvider } : {}),
81
+ ...(aiProviderApiKey
82
+ ? { CLUE_AI_PROVIDER_API_KEY: aiProviderApiKey }
83
+ : {}),
84
+ ...(aiModel ? { CLUE_AI_MODEL: aiModel } : {}),
85
+ };
86
+ };
87
+
88
+ const missingSetupAgentAiEnvNames = (env) =>
89
+ REQUIRED_SETUP_AGENT_AI_ENV_NAMES.filter((name) => !optionalString(env[name]));
90
+
91
+ const blockedMissingAiProviderConfig = ({ manifestPath, missingInputs }) => ({
92
+ status: "blocked",
93
+ code: "CLUE_SETUP_AGENT_AI_CONFIG_MISSING",
94
+ manifest_path: manifestPath,
95
+ attempts: [],
96
+ setup_check: null,
97
+ setup_doctor: {
98
+ status: "skipped",
99
+ reason: "AI provider configuration is missing",
100
+ missing_inputs: missingInputs,
101
+ report: null,
102
+ },
103
+ blockers: [
104
+ {
105
+ reason:
106
+ "setup-agent requires CLUE_AI_PROVIDER, CLUE_AI_PROVIDER_API_KEY, and CLUE_AI_MODEL before lifecycle planning",
107
+ evidence: {
108
+ missing_inputs: missingInputs,
109
+ env_file_path: DEFAULT_ENV_GUIDE_PATH,
110
+ supported_providers: ["openai", "anthropic"],
111
+ cli_flags: [
112
+ "--ai-provider",
113
+ "--ai-provider-api-key",
114
+ "--ai-model",
115
+ ],
116
+ },
117
+ },
118
+ ],
119
+ next_steps: [
120
+ "Open .env.clue and replace CLUE_AI_PROVIDER_API_KEY with an OpenAI API key for codex or an Anthropic API key for claude_code.",
121
+ "Confirm CLUE_AI_PROVIDER and CLUE_AI_MODEL are set in .env.clue or process env.",
122
+ "Alternatively run setup-agent with --ai-provider, --ai-provider-api-key, and --ai-model.",
123
+ ],
124
+ user_verification: {
125
+ setup_watch_owner: "user",
126
+ setup_watch_auto_run: false,
127
+ status: "not_reached",
128
+ required_command: "npx -y @clue-ai/cli setup-watch --local",
129
+ },
130
+ });
131
+
132
+ const requireManifest = async ({ manifestPath, repoRoot }) => {
133
+ try {
134
+ return await readJson(resolve(repoRoot, manifestPath));
135
+ } catch (error) {
136
+ if (error?.code === "ENOENT") {
137
+ const blocker = new Error(
138
+ `setup-agent requires ${manifestPath}; run setup first`,
139
+ );
140
+ blocker.code = "CLUE_SETUP_MANIFEST_MISSING";
141
+ throw blocker;
142
+ }
143
+ throw error;
144
+ }
145
+ };
146
+
147
+ const sourcePathsFromManifest = (manifest) => {
148
+ const watchTargets = Array.isArray(
149
+ manifest.lifecycle_verification?.watch_targets,
150
+ )
151
+ ? manifest.lifecycle_verification.watch_targets
152
+ : [];
153
+ return [
154
+ ...new Set(
155
+ [
156
+ manifest.detected?.backend_root_path,
157
+ ...watchTargets.map((target) => target?.root_path),
158
+ ]
159
+ .map(optionalString)
160
+ .filter(Boolean),
161
+ ),
162
+ ];
163
+ };
164
+
165
+ const dependencyCandidateRoots = (sourcePaths) => [
166
+ ".",
167
+ ...sourcePaths.flatMap((root) => {
168
+ const normalized = root.replace(/\/+$/, "");
169
+ return normalized.endsWith("/src") || normalized.endsWith("/app")
170
+ ? [normalized, dirname(normalized)]
171
+ : [normalized];
172
+ }),
173
+ ];
174
+
175
+ const discoveredDependencyWritePaths = async ({ repoRoot, sourcePaths }) => {
176
+ const candidates = dependencyCandidateRoots(sourcePaths).flatMap((root) =>
177
+ DEPENDENCY_WRITE_FILE_CANDIDATES.map((file) =>
178
+ root === "." ? file : join(root, file),
179
+ ),
180
+ );
181
+ const discovered = [];
182
+ for (const candidate of [...new Set(candidates)]) {
183
+ if (await exists(resolve(repoRoot, candidate))) {
184
+ discovered.push(candidate);
185
+ }
186
+ }
187
+ return discovered;
188
+ };
189
+
190
+ const allowedWritePathsFromRequest = async ({ repoRoot, request }) => [
191
+ ...new Set([
192
+ ...request.allowed_source_paths,
193
+ ...(await discoveredDependencyWritePaths({
194
+ repoRoot,
195
+ sourcePaths: request.allowed_source_paths,
196
+ })),
197
+ ]),
198
+ ];
199
+
200
+ const buildLifecycleRequestFromManifest = ({ manifest, target }) => {
201
+ if (manifest?.status !== "ready_for_ai") {
202
+ throw new Error("setup-agent requires a ready_for_ai setup manifest");
203
+ }
204
+ const framework = optionalString(manifest.detected?.framework);
205
+ const backendRootPath = optionalString(manifest.detected?.backend_root_path);
206
+ const serviceKey = optionalString(manifest.detected?.service_key);
207
+ const sourcePaths = sourcePathsFromManifest(manifest);
208
+ if (!framework || !backendRootPath || !serviceKey || sourcePaths.length === 0) {
209
+ throw new Error(
210
+ "setup-agent manifest is missing framework, backend root, service key, or source paths",
211
+ );
212
+ }
213
+ return {
214
+ target_tool: optionalString(target) ?? optionalString(manifest.target) ?? "codex",
215
+ framework,
216
+ project_key: "CLUE_PROJECT_KEY",
217
+ environment: "CLUE_ENVIRONMENT",
218
+ service_key: serviceKey,
219
+ allowed_source_paths: sourcePaths,
220
+ excluded_source_paths: [],
221
+ backend_root_path: backendRootPath,
222
+ };
223
+ };
224
+
225
+ const failedChecks = (setupCheck) =>
226
+ Array.isArray(setupCheck?.checks)
227
+ ? setupCheck.checks
228
+ .filter((check) => !check.passed)
229
+ .map((check) => ({
230
+ id: check.id,
231
+ summary: check.summary ?? null,
232
+ details: {
233
+ missing_apis: check.missing_apis ?? null,
234
+ backend_lifecycle: check.backend_lifecycle ?? null,
235
+ frontend_lifecycle: check.frontend_lifecycle ?? null,
236
+ findings: check.findings ?? null,
237
+ },
238
+ }))
239
+ : [];
240
+
241
+ const runStaticSetupCheck = async ({
242
+ manifest,
243
+ repoRoot,
244
+ request,
245
+ setupChecker,
246
+ target,
247
+ }) =>
248
+ setupChecker({
249
+ repoRoot,
250
+ request: {
251
+ framework: request.framework,
252
+ backend_root_path: request.backend_root_path,
253
+ allowed_source_paths: request.allowed_source_paths,
254
+ excluded_source_paths: request.excluded_source_paths,
255
+ service_key: request.service_key,
256
+ workflow_path: manifest.artifacts?.ci_workflow_path,
257
+ },
258
+ target,
259
+ requireSdkLifecycle: true,
260
+ });
261
+
262
+ const missingInputsFromDoctor = (report) => [
263
+ ...new Set(
264
+ (report?.checks ?? [])
265
+ .flatMap((check) => {
266
+ const match = /^missing required input: (?<names>.+)$/.exec(
267
+ check.error ?? "",
268
+ );
269
+ return match?.groups?.names
270
+ ? match.groups.names.split(",").map((entry) => entry.trim())
271
+ : [];
272
+ })
273
+ .filter(Boolean),
274
+ ),
275
+ ];
276
+
277
+ const runOptionalSetupDoctor = async ({ env, flags, repoRoot, setupDoctor }) => {
278
+ if (flags.has("skip-setup-doctor")) {
279
+ return {
280
+ status: "skipped",
281
+ reason: "skip-setup-doctor flag was provided",
282
+ missing_inputs: [],
283
+ report: null,
284
+ };
285
+ }
286
+ const report = await setupDoctor({ env, flags, repoRoot });
287
+ const missingInputs = missingInputsFromDoctor(report);
288
+ if (!report.passed) {
289
+ const failedChecks = Array.isArray(report?.checks)
290
+ ? report.checks.filter((check) => !check.passed)
291
+ : [];
292
+ const allFailuresAreMissingInputs =
293
+ failedChecks.length > 0 &&
294
+ failedChecks.every((check) =>
295
+ /^missing required input: .+$/.test(check.error ?? ""),
296
+ );
297
+ if (allFailuresAreMissingInputs) {
298
+ return {
299
+ status: "blocked_missing_inputs",
300
+ reason: "setup-doctor required inputs are missing",
301
+ missing_inputs: missingInputs,
302
+ report,
303
+ };
304
+ }
305
+ return {
306
+ status: "failed",
307
+ reason: "setup-doctor connectivity preflight failed",
308
+ missing_inputs: [],
309
+ report,
310
+ };
311
+ }
312
+ return {
313
+ status: "passed",
314
+ reason: null,
315
+ missing_inputs: [],
316
+ report,
317
+ };
318
+ };
319
+
320
+ const attemptFailureContext = ({ attempt, error, setupCheck, stage }) => ({
321
+ attempt,
322
+ stage,
323
+ error:
324
+ error === undefined
325
+ ? null
326
+ : error instanceof Error
327
+ ? error.message
328
+ : String(error),
329
+ failed_checks: failedChecks(setupCheck),
330
+ });
331
+
332
+ export const runSetupAgent = async ({
333
+ env = process.env,
334
+ flags = new Map(),
335
+ lifecycleApplier = applyLifecyclePlan,
336
+ lifecyclePlanner = planLifecycleInsertionsStrict,
337
+ repoRoot = ".",
338
+ setupChecker = runSetupCheck,
339
+ setupDoctor = runSetupDoctor,
340
+ }) => {
341
+ const resolvedRepoRoot = resolve(repoRoot ?? ".");
342
+ const manifestPath = String(
343
+ flags.get("manifest") || DEFAULT_SETUP_MANIFEST_PATH,
344
+ );
345
+ let manifest;
346
+ try {
347
+ manifest = await requireManifest({
348
+ manifestPath,
349
+ repoRoot: resolvedRepoRoot,
350
+ });
351
+ } catch (error) {
352
+ if (error?.code === "CLUE_SETUP_MANIFEST_MISSING") {
353
+ return {
354
+ status: "blocked",
355
+ code: "CLUE_SETUP_MANIFEST_MISSING",
356
+ manifest_path: manifestPath,
357
+ attempts: [],
358
+ setup_check: null,
359
+ setup_doctor: {
360
+ status: "skipped",
361
+ reason: "setup manifest is missing",
362
+ missing_inputs: [],
363
+ report: null,
364
+ },
365
+ blockers: [
366
+ {
367
+ reason: error.message,
368
+ evidence: manifestPath,
369
+ },
370
+ ],
371
+ user_verification: {
372
+ setup_watch_owner: "user",
373
+ setup_watch_auto_run: false,
374
+ status: "not_reached",
375
+ required_command: "npx -y @clue-ai/cli setup-watch --local",
376
+ },
377
+ };
378
+ }
379
+ throw error;
380
+ }
381
+ const target = optionalString(flags.get("target")) ?? manifest.target;
382
+ const mergedEnv = {
383
+ ...(await readEnvGuideIfPresent({ repoRoot: resolvedRepoRoot })),
384
+ ...env,
385
+ };
386
+ const request = buildLifecycleRequestFromManifest({ manifest, target });
387
+ const allowedWritePaths = await allowedWritePathsFromRequest({
388
+ repoRoot: resolvedRepoRoot,
389
+ request,
390
+ });
391
+ const maxAttempts = normalizePositiveInteger(
392
+ flags.get("max-attempts"),
393
+ DEFAULT_MAX_ATTEMPTS,
394
+ );
395
+ const aiEnv = aiProviderEnvFromFlags({ env: mergedEnv, flags });
396
+ const missingAiInputs = missingSetupAgentAiEnvNames(aiEnv);
397
+ if (missingAiInputs.length > 0) {
398
+ return blockedMissingAiProviderConfig({
399
+ manifestPath,
400
+ missingInputs: missingAiInputs,
401
+ });
402
+ }
403
+ const attempts = [];
404
+ let failureContext = null;
405
+ let lastSetupCheck = null;
406
+
407
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
408
+ try {
409
+ const plan = await lifecyclePlanner({
410
+ repoRoot: resolvedRepoRoot,
411
+ request,
412
+ env: aiEnv,
413
+ failureContext,
414
+ });
415
+ const lifecycleResult = await lifecycleApplier({
416
+ repoRoot: resolvedRepoRoot,
417
+ plan,
418
+ allowedWritePaths,
419
+ });
420
+ const setupCheck = await runStaticSetupCheck({
421
+ manifest,
422
+ repoRoot: resolvedRepoRoot,
423
+ request,
424
+ setupChecker,
425
+ target,
426
+ });
427
+ lastSetupCheck = setupCheck;
428
+ attempts.push({
429
+ attempt,
430
+ lifecycle_insertions: lifecycleResult.lifecycleInsertions,
431
+ warnings: lifecycleResult.warnings,
432
+ setup_check_passed: setupCheck.passed,
433
+ failed_checks: failedChecks(setupCheck),
434
+ });
435
+ if (setupCheck.passed) {
436
+ const setupDoctorResult = await runOptionalSetupDoctor({
437
+ env: mergedEnv,
438
+ flags,
439
+ repoRoot: resolvedRepoRoot,
440
+ setupDoctor,
441
+ });
442
+ return {
443
+ status:
444
+ setupDoctorResult.status === "passed" ||
445
+ setupDoctorResult.status === "skipped"
446
+ ? "user_verification_pending"
447
+ : "blocked",
448
+ manifest_path: manifestPath,
449
+ attempts,
450
+ setup_check: setupCheck,
451
+ setup_doctor: setupDoctorResult,
452
+ user_verification: {
453
+ setup_watch_owner: "user",
454
+ setup_watch_auto_run: false,
455
+ status: "user_verification_pending",
456
+ required_command: "npx -y @clue-ai/cli setup-watch --local",
457
+ },
458
+ };
459
+ }
460
+ failureContext = attemptFailureContext({
461
+ attempt,
462
+ setupCheck,
463
+ stage: "setup_check",
464
+ });
465
+ } catch (error) {
466
+ failureContext = attemptFailureContext({
467
+ attempt,
468
+ error,
469
+ stage: "lifecycle_apply_or_plan",
470
+ });
471
+ attempts.push({
472
+ attempt,
473
+ setup_check_passed: false,
474
+ error: failureContext.error,
475
+ failed_checks: [],
476
+ });
477
+ }
478
+ }
479
+
480
+ return {
481
+ status: "blocked",
482
+ manifest_path: manifestPath,
483
+ attempts,
484
+ setup_check: lastSetupCheck,
485
+ setup_doctor: {
486
+ status: "skipped",
487
+ reason: "setup-check did not pass",
488
+ missing_inputs: [],
489
+ report: null,
490
+ },
491
+ blockers: [
492
+ {
493
+ reason: "setup-agent retry budget exhausted",
494
+ evidence: failureContext,
495
+ },
496
+ ],
497
+ user_verification: {
498
+ setup_watch_owner: "user",
499
+ setup_watch_auto_run: false,
500
+ status: "not_reached",
501
+ required_command: "npx -y @clue-ai/cli setup-watch --local",
502
+ },
503
+ };
504
+ };
@@ -1,5 +1,5 @@
1
1
  export const AI_SETUP_CONTRACT_VERSION =
2
- "2026-05-10.frontend-adapter-contract.v8";
2
+ "2026-05-10.latest-sdk-contract.v9";
3
3
 
4
4
  export const SETUP_DOCTRINE = {
5
5
  purpose:
@@ -26,6 +26,7 @@ export const DETERMINISTIC_CONTROL_MODEL = {
26
26
  ],
27
27
  cli_should_control: [
28
28
  "official SDK package names",
29
+ "latest-channel Clue SDK dependency declarations so setup agents cannot pin stale SDK versions",
29
30
  "official public SDK function names and supported lifecycle API set",
30
31
  "environment variable names produced by setup and consumed by setup code",
31
32
  "machine-owned semantic workflow generation and verification",