@clue-ai/cli 0.0.18 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/clue-cli.mjs +15 -0
- package/package.json +1 -1
- package/src/ai-provider.mjs +92 -2
- package/src/fastapi-analyzer.mjs +36 -2
- package/src/lifecycle-init.mjs +225 -14
- package/src/public-schema.cjs +1 -0
- package/src/semantic-ci.mjs +89 -9
- package/src/setup-agent.mjs +448 -0
- package/src/setup-ai-contract.mjs +2 -1
- package/src/setup-check.mjs +91 -8
- package/src/setup-help.mjs +28 -1
- package/src/setup-tool.mjs +8 -6
package/src/semantic-ci.mjs
CHANGED
|
@@ -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:
|
|
2079
|
-
previous_route_input_hash:
|
|
2080
|
-
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
|
-
|
|
2090
|
+
effectivePlan.origin === "changed_route_needs_review" &&
|
|
2091
|
+
aiRoute?.semantics
|
|
2083
2092
|
? "changed_route_needs_review"
|
|
2084
|
-
:
|
|
2085
|
-
semantic_change_reason:
|
|
2093
|
+
: effectivePlan.origin,
|
|
2094
|
+
semantic_change_reason: effectivePlan.semantic_change_reason,
|
|
2086
2095
|
previous_semantic_snapshot_version:
|
|
2087
|
-
|
|
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
|
|
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,448 @@
|
|
|
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 DEPENDENCY_WRITE_FILE_CANDIDATES = [
|
|
14
|
+
"package.json",
|
|
15
|
+
"package-lock.json",
|
|
16
|
+
"pnpm-lock.yaml",
|
|
17
|
+
"yarn.lock",
|
|
18
|
+
"bun.lock",
|
|
19
|
+
"requirements.txt",
|
|
20
|
+
"requirements-dev.txt",
|
|
21
|
+
"pyproject.toml",
|
|
22
|
+
"Pipfile",
|
|
23
|
+
"Pipfile.lock",
|
|
24
|
+
"poetry.lock",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const optionalString = (value) =>
|
|
28
|
+
typeof value === "string" && value.trim() ? value.trim() : null;
|
|
29
|
+
|
|
30
|
+
const readJson = async (path) => JSON.parse(await readFile(path, "utf8"));
|
|
31
|
+
|
|
32
|
+
const parseEnvGuide = (text) => {
|
|
33
|
+
const values = {};
|
|
34
|
+
for (const line of text.split(/\r?\n/)) {
|
|
35
|
+
const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)\s*$/.exec(line);
|
|
36
|
+
if (!match) continue;
|
|
37
|
+
const value = match[2].trim().replace(/^(['"])(.*)\1$/, "$2");
|
|
38
|
+
if (/^<[^>]+>$/.test(value)) continue;
|
|
39
|
+
values[match[1]] = value;
|
|
40
|
+
}
|
|
41
|
+
return values;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const readEnvGuideIfPresent = async ({ repoRoot }) => {
|
|
45
|
+
try {
|
|
46
|
+
return parseEnvGuide(
|
|
47
|
+
await readFile(resolve(repoRoot, DEFAULT_ENV_GUIDE_PATH), "utf8"),
|
|
48
|
+
);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error?.code === "ENOENT") return {};
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const exists = async (path) => {
|
|
56
|
+
try {
|
|
57
|
+
await access(path);
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const normalizePositiveInteger = (value, fallback) => {
|
|
65
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
66
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const aiProviderEnvFromFlags = ({ env, flags }) => {
|
|
70
|
+
const aiProvider = optionalString(flags.get("ai-provider"));
|
|
71
|
+
const aiProviderApiKey = optionalString(flags.get("ai-provider-api-key"));
|
|
72
|
+
const aiModel = optionalString(flags.get("ai-model"));
|
|
73
|
+
return {
|
|
74
|
+
...env,
|
|
75
|
+
...(aiProvider ? { CLUE_AI_PROVIDER: aiProvider } : {}),
|
|
76
|
+
...(aiProviderApiKey
|
|
77
|
+
? { CLUE_AI_PROVIDER_API_KEY: aiProviderApiKey }
|
|
78
|
+
: {}),
|
|
79
|
+
...(aiModel ? { CLUE_AI_MODEL: aiModel } : {}),
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const requireManifest = async ({ manifestPath, repoRoot }) => {
|
|
84
|
+
try {
|
|
85
|
+
return await readJson(resolve(repoRoot, manifestPath));
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error?.code === "ENOENT") {
|
|
88
|
+
const blocker = new Error(
|
|
89
|
+
`setup-agent requires ${manifestPath}; run setup first`,
|
|
90
|
+
);
|
|
91
|
+
blocker.code = "CLUE_SETUP_MANIFEST_MISSING";
|
|
92
|
+
throw blocker;
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const sourcePathsFromManifest = (manifest) => {
|
|
99
|
+
const watchTargets = Array.isArray(
|
|
100
|
+
manifest.lifecycle_verification?.watch_targets,
|
|
101
|
+
)
|
|
102
|
+
? manifest.lifecycle_verification.watch_targets
|
|
103
|
+
: [];
|
|
104
|
+
return [
|
|
105
|
+
...new Set(
|
|
106
|
+
[
|
|
107
|
+
manifest.detected?.backend_root_path,
|
|
108
|
+
...watchTargets.map((target) => target?.root_path),
|
|
109
|
+
]
|
|
110
|
+
.map(optionalString)
|
|
111
|
+
.filter(Boolean),
|
|
112
|
+
),
|
|
113
|
+
];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const dependencyCandidateRoots = (sourcePaths) => [
|
|
117
|
+
".",
|
|
118
|
+
...sourcePaths.flatMap((root) => {
|
|
119
|
+
const normalized = root.replace(/\/+$/, "");
|
|
120
|
+
return normalized.endsWith("/src") || normalized.endsWith("/app")
|
|
121
|
+
? [normalized, dirname(normalized)]
|
|
122
|
+
: [normalized];
|
|
123
|
+
}),
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const discoveredDependencyWritePaths = async ({ repoRoot, sourcePaths }) => {
|
|
127
|
+
const candidates = dependencyCandidateRoots(sourcePaths).flatMap((root) =>
|
|
128
|
+
DEPENDENCY_WRITE_FILE_CANDIDATES.map((file) =>
|
|
129
|
+
root === "." ? file : join(root, file),
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
const discovered = [];
|
|
133
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
134
|
+
if (await exists(resolve(repoRoot, candidate))) {
|
|
135
|
+
discovered.push(candidate);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return discovered;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const allowedWritePathsFromRequest = async ({ repoRoot, request }) => [
|
|
142
|
+
...new Set([
|
|
143
|
+
...request.allowed_source_paths,
|
|
144
|
+
...(await discoveredDependencyWritePaths({
|
|
145
|
+
repoRoot,
|
|
146
|
+
sourcePaths: request.allowed_source_paths,
|
|
147
|
+
})),
|
|
148
|
+
]),
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const buildLifecycleRequestFromManifest = ({ manifest, target }) => {
|
|
152
|
+
if (manifest?.status !== "ready_for_ai") {
|
|
153
|
+
throw new Error("setup-agent requires a ready_for_ai setup manifest");
|
|
154
|
+
}
|
|
155
|
+
const framework = optionalString(manifest.detected?.framework);
|
|
156
|
+
const backendRootPath = optionalString(manifest.detected?.backend_root_path);
|
|
157
|
+
const serviceKey = optionalString(manifest.detected?.service_key);
|
|
158
|
+
const sourcePaths = sourcePathsFromManifest(manifest);
|
|
159
|
+
if (!framework || !backendRootPath || !serviceKey || sourcePaths.length === 0) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
"setup-agent manifest is missing framework, backend root, service key, or source paths",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
target_tool: optionalString(target) ?? optionalString(manifest.target) ?? "codex",
|
|
166
|
+
framework,
|
|
167
|
+
project_key: "CLUE_PROJECT_KEY",
|
|
168
|
+
environment: "CLUE_ENVIRONMENT",
|
|
169
|
+
service_key: serviceKey,
|
|
170
|
+
allowed_source_paths: sourcePaths,
|
|
171
|
+
excluded_source_paths: [],
|
|
172
|
+
backend_root_path: backendRootPath,
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const failedChecks = (setupCheck) =>
|
|
177
|
+
Array.isArray(setupCheck?.checks)
|
|
178
|
+
? setupCheck.checks
|
|
179
|
+
.filter((check) => !check.passed)
|
|
180
|
+
.map((check) => ({
|
|
181
|
+
id: check.id,
|
|
182
|
+
summary: check.summary ?? null,
|
|
183
|
+
details: {
|
|
184
|
+
missing_apis: check.missing_apis ?? null,
|
|
185
|
+
backend_lifecycle: check.backend_lifecycle ?? null,
|
|
186
|
+
frontend_lifecycle: check.frontend_lifecycle ?? null,
|
|
187
|
+
findings: check.findings ?? null,
|
|
188
|
+
},
|
|
189
|
+
}))
|
|
190
|
+
: [];
|
|
191
|
+
|
|
192
|
+
const runStaticSetupCheck = async ({
|
|
193
|
+
manifest,
|
|
194
|
+
repoRoot,
|
|
195
|
+
request,
|
|
196
|
+
setupChecker,
|
|
197
|
+
target,
|
|
198
|
+
}) =>
|
|
199
|
+
setupChecker({
|
|
200
|
+
repoRoot,
|
|
201
|
+
request: {
|
|
202
|
+
framework: request.framework,
|
|
203
|
+
backend_root_path: request.backend_root_path,
|
|
204
|
+
allowed_source_paths: request.allowed_source_paths,
|
|
205
|
+
excluded_source_paths: request.excluded_source_paths,
|
|
206
|
+
service_key: request.service_key,
|
|
207
|
+
workflow_path: manifest.artifacts?.ci_workflow_path,
|
|
208
|
+
},
|
|
209
|
+
target,
|
|
210
|
+
requireSdkLifecycle: true,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const missingInputsFromDoctor = (report) => [
|
|
214
|
+
...new Set(
|
|
215
|
+
(report?.checks ?? [])
|
|
216
|
+
.flatMap((check) => {
|
|
217
|
+
const match = /^missing required input: (?<names>.+)$/.exec(
|
|
218
|
+
check.error ?? "",
|
|
219
|
+
);
|
|
220
|
+
return match?.groups?.names
|
|
221
|
+
? match.groups.names.split(",").map((entry) => entry.trim())
|
|
222
|
+
: [];
|
|
223
|
+
})
|
|
224
|
+
.filter(Boolean),
|
|
225
|
+
),
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
const runOptionalSetupDoctor = async ({ env, flags, repoRoot, setupDoctor }) => {
|
|
229
|
+
if (flags.has("skip-setup-doctor")) {
|
|
230
|
+
return {
|
|
231
|
+
status: "skipped",
|
|
232
|
+
reason: "skip-setup-doctor flag was provided",
|
|
233
|
+
missing_inputs: [],
|
|
234
|
+
report: null,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const report = await setupDoctor({ env, flags, repoRoot });
|
|
238
|
+
const missingInputs = missingInputsFromDoctor(report);
|
|
239
|
+
if (!report.passed) {
|
|
240
|
+
const failedChecks = Array.isArray(report?.checks)
|
|
241
|
+
? report.checks.filter((check) => !check.passed)
|
|
242
|
+
: [];
|
|
243
|
+
const allFailuresAreMissingInputs =
|
|
244
|
+
failedChecks.length > 0 &&
|
|
245
|
+
failedChecks.every((check) =>
|
|
246
|
+
/^missing required input: .+$/.test(check.error ?? ""),
|
|
247
|
+
);
|
|
248
|
+
if (allFailuresAreMissingInputs) {
|
|
249
|
+
return {
|
|
250
|
+
status: "blocked_missing_inputs",
|
|
251
|
+
reason: "setup-doctor required inputs are missing",
|
|
252
|
+
missing_inputs: missingInputs,
|
|
253
|
+
report,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
status: "failed",
|
|
258
|
+
reason: "setup-doctor connectivity preflight failed",
|
|
259
|
+
missing_inputs: [],
|
|
260
|
+
report,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
status: "passed",
|
|
265
|
+
reason: null,
|
|
266
|
+
missing_inputs: [],
|
|
267
|
+
report,
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const attemptFailureContext = ({ attempt, error, setupCheck, stage }) => ({
|
|
272
|
+
attempt,
|
|
273
|
+
stage,
|
|
274
|
+
error:
|
|
275
|
+
error === undefined
|
|
276
|
+
? null
|
|
277
|
+
: error instanceof Error
|
|
278
|
+
? error.message
|
|
279
|
+
: String(error),
|
|
280
|
+
failed_checks: failedChecks(setupCheck),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
export const runSetupAgent = async ({
|
|
284
|
+
env = process.env,
|
|
285
|
+
flags = new Map(),
|
|
286
|
+
lifecycleApplier = applyLifecyclePlan,
|
|
287
|
+
lifecyclePlanner = planLifecycleInsertionsStrict,
|
|
288
|
+
repoRoot = ".",
|
|
289
|
+
setupChecker = runSetupCheck,
|
|
290
|
+
setupDoctor = runSetupDoctor,
|
|
291
|
+
}) => {
|
|
292
|
+
const resolvedRepoRoot = resolve(repoRoot ?? ".");
|
|
293
|
+
const manifestPath = String(
|
|
294
|
+
flags.get("manifest") || DEFAULT_SETUP_MANIFEST_PATH,
|
|
295
|
+
);
|
|
296
|
+
let manifest;
|
|
297
|
+
try {
|
|
298
|
+
manifest = await requireManifest({
|
|
299
|
+
manifestPath,
|
|
300
|
+
repoRoot: resolvedRepoRoot,
|
|
301
|
+
});
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (error?.code === "CLUE_SETUP_MANIFEST_MISSING") {
|
|
304
|
+
return {
|
|
305
|
+
status: "blocked",
|
|
306
|
+
code: "CLUE_SETUP_MANIFEST_MISSING",
|
|
307
|
+
manifest_path: manifestPath,
|
|
308
|
+
attempts: [],
|
|
309
|
+
setup_check: null,
|
|
310
|
+
setup_doctor: {
|
|
311
|
+
status: "skipped",
|
|
312
|
+
reason: "setup manifest is missing",
|
|
313
|
+
missing_inputs: [],
|
|
314
|
+
report: null,
|
|
315
|
+
},
|
|
316
|
+
blockers: [
|
|
317
|
+
{
|
|
318
|
+
reason: error.message,
|
|
319
|
+
evidence: manifestPath,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
user_verification: {
|
|
323
|
+
setup_watch_owner: "user",
|
|
324
|
+
setup_watch_auto_run: false,
|
|
325
|
+
status: "not_reached",
|
|
326
|
+
required_command: "npx -y @clue-ai/cli setup-watch --local",
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
const target = optionalString(flags.get("target")) ?? manifest.target;
|
|
333
|
+
const mergedEnv = {
|
|
334
|
+
...(await readEnvGuideIfPresent({ repoRoot: resolvedRepoRoot })),
|
|
335
|
+
...env,
|
|
336
|
+
};
|
|
337
|
+
const request = buildLifecycleRequestFromManifest({ manifest, target });
|
|
338
|
+
const allowedWritePaths = await allowedWritePathsFromRequest({
|
|
339
|
+
repoRoot: resolvedRepoRoot,
|
|
340
|
+
request,
|
|
341
|
+
});
|
|
342
|
+
const maxAttempts = normalizePositiveInteger(
|
|
343
|
+
flags.get("max-attempts"),
|
|
344
|
+
DEFAULT_MAX_ATTEMPTS,
|
|
345
|
+
);
|
|
346
|
+
const aiEnv = aiProviderEnvFromFlags({ env: mergedEnv, flags });
|
|
347
|
+
const attempts = [];
|
|
348
|
+
let failureContext = null;
|
|
349
|
+
let lastSetupCheck = null;
|
|
350
|
+
|
|
351
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
352
|
+
try {
|
|
353
|
+
const plan = await lifecyclePlanner({
|
|
354
|
+
repoRoot: resolvedRepoRoot,
|
|
355
|
+
request,
|
|
356
|
+
env: aiEnv,
|
|
357
|
+
failureContext,
|
|
358
|
+
});
|
|
359
|
+
const lifecycleResult = await lifecycleApplier({
|
|
360
|
+
repoRoot: resolvedRepoRoot,
|
|
361
|
+
plan,
|
|
362
|
+
allowedWritePaths,
|
|
363
|
+
});
|
|
364
|
+
const setupCheck = await runStaticSetupCheck({
|
|
365
|
+
manifest,
|
|
366
|
+
repoRoot: resolvedRepoRoot,
|
|
367
|
+
request,
|
|
368
|
+
setupChecker,
|
|
369
|
+
target,
|
|
370
|
+
});
|
|
371
|
+
lastSetupCheck = setupCheck;
|
|
372
|
+
attempts.push({
|
|
373
|
+
attempt,
|
|
374
|
+
lifecycle_insertions: lifecycleResult.lifecycleInsertions,
|
|
375
|
+
warnings: lifecycleResult.warnings,
|
|
376
|
+
setup_check_passed: setupCheck.passed,
|
|
377
|
+
failed_checks: failedChecks(setupCheck),
|
|
378
|
+
});
|
|
379
|
+
if (setupCheck.passed) {
|
|
380
|
+
const setupDoctorResult = await runOptionalSetupDoctor({
|
|
381
|
+
env: mergedEnv,
|
|
382
|
+
flags,
|
|
383
|
+
repoRoot: resolvedRepoRoot,
|
|
384
|
+
setupDoctor,
|
|
385
|
+
});
|
|
386
|
+
return {
|
|
387
|
+
status:
|
|
388
|
+
setupDoctorResult.status === "passed" ||
|
|
389
|
+
setupDoctorResult.status === "skipped"
|
|
390
|
+
? "user_verification_pending"
|
|
391
|
+
: "blocked",
|
|
392
|
+
manifest_path: manifestPath,
|
|
393
|
+
attempts,
|
|
394
|
+
setup_check: setupCheck,
|
|
395
|
+
setup_doctor: setupDoctorResult,
|
|
396
|
+
user_verification: {
|
|
397
|
+
setup_watch_owner: "user",
|
|
398
|
+
setup_watch_auto_run: false,
|
|
399
|
+
status: "user_verification_pending",
|
|
400
|
+
required_command: "npx -y @clue-ai/cli setup-watch --local",
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
failureContext = attemptFailureContext({
|
|
405
|
+
attempt,
|
|
406
|
+
setupCheck,
|
|
407
|
+
stage: "setup_check",
|
|
408
|
+
});
|
|
409
|
+
} catch (error) {
|
|
410
|
+
failureContext = attemptFailureContext({
|
|
411
|
+
attempt,
|
|
412
|
+
error,
|
|
413
|
+
stage: "lifecycle_apply_or_plan",
|
|
414
|
+
});
|
|
415
|
+
attempts.push({
|
|
416
|
+
attempt,
|
|
417
|
+
setup_check_passed: false,
|
|
418
|
+
error: failureContext.error,
|
|
419
|
+
failed_checks: [],
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
status: "blocked",
|
|
426
|
+
manifest_path: manifestPath,
|
|
427
|
+
attempts,
|
|
428
|
+
setup_check: lastSetupCheck,
|
|
429
|
+
setup_doctor: {
|
|
430
|
+
status: "skipped",
|
|
431
|
+
reason: "setup-check did not pass",
|
|
432
|
+
missing_inputs: [],
|
|
433
|
+
report: null,
|
|
434
|
+
},
|
|
435
|
+
blockers: [
|
|
436
|
+
{
|
|
437
|
+
reason: "setup-agent retry budget exhausted",
|
|
438
|
+
evidence: failureContext,
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
user_verification: {
|
|
442
|
+
setup_watch_owner: "user",
|
|
443
|
+
setup_watch_auto_run: false,
|
|
444
|
+
status: "not_reached",
|
|
445
|
+
required_command: "npx -y @clue-ai/cli setup-watch --local",
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const AI_SETUP_CONTRACT_VERSION =
|
|
2
|
-
"2026-05-10.
|
|
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",
|