@clue-ai/cli 0.0.3 → 0.0.5

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.
@@ -4,8 +4,7 @@ Run the Clue SDK initialization tool with structured inputs.
4
4
 
5
5
  Required fields:
6
6
 
7
- - `project_key`
8
- - `service_key`
7
+ - `project_key` as an environment variable reference or placeholder, not a pasted secret value
9
8
  - `framework`
10
9
  - `backend_root_path`
11
10
  - `environment`
@@ -19,9 +18,12 @@ Required secrets:
19
18
 
20
19
  Behavior:
21
20
 
22
- 1. Collect the required fields as structured input.
23
- 2. Build the canonical init request.
24
- 3. Run `clue-ai init --request <generated-request.json> --repo .`.
25
- 4. Show the generated report and low-confidence review points.
21
+ 1. Follow the installed `clue-setup-orchestrator` skill and its monitoring gates.
22
+ 2. Collect the required fields as structured input without asking for secret values.
23
+ 3. Derive `service_key` from `backend_root_path` and repository structure; do not ask the user to type it.
24
+ 4. Build the canonical init request.
25
+ 5. Run `clue-ai init --request <generated-request.json> --repo .`.
26
+ 6. Show the generated report and low-confidence review points.
26
27
 
27
28
  Do not ask the user to write a free-form setup prompt.
29
+ Do not ask the user to paste project key, API key, service key, token, or environment values.
@@ -4,8 +4,7 @@ Run the Clue SDK initialization tool with structured inputs.
4
4
 
5
5
  Required fields:
6
6
 
7
- - `project_key`
8
- - `service_key`
7
+ - `project_key` as an environment variable reference or placeholder, not a pasted secret value
9
8
  - `framework`
10
9
  - `backend_root_path`
11
10
  - `environment`
@@ -19,9 +18,12 @@ Required secrets:
19
18
 
20
19
  Behavior:
21
20
 
22
- 1. Collect the required fields as structured input.
23
- 2. Build the canonical init request.
24
- 3. Run `clue-ai init --request <generated-request.json> --repo .`.
25
- 4. Show the generated report and low-confidence review points.
21
+ 1. Follow the installed `clue-setup-orchestrator` skill and its monitoring gates.
22
+ 2. Collect the required fields as structured input without asking for secret values.
23
+ 3. Derive `service_key` from `backend_root_path` and repository structure; do not ask the user to type it.
24
+ 4. Build the canonical init request.
25
+ 5. Run `clue-ai init --request <generated-request.json> --repo .`.
26
+ 6. Show the generated report and low-confidence review points.
26
27
 
27
28
  Do not ask the user to write a free-form setup prompt.
29
+ Do not ask the user to paste project key, API key, service key, token, or environment values.
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@clue-ai/cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "bin": {
6
- "clue-ai": "bin/clue-tool.mjs"
6
+ "clue-ai": "bin/clue-cli.mjs"
7
7
  },
8
8
  "files": [
9
9
  "bin/",
@@ -7,7 +7,6 @@ export const REQUIRED_SECRET_NAMES = [
7
7
 
8
8
  export const CLUE_INIT_COMMAND_FIELDS = [
9
9
  "project_key",
10
- "service_key",
11
10
  "framework",
12
11
  "backend_root_path",
13
12
  "environment",
@@ -45,15 +44,35 @@ const normalizeStringArray = (value, fallback, field) => {
45
44
  .map((entry) => entry.trim());
46
45
  };
47
46
 
47
+ const deriveServiceKeyFromBackendRootPath = (backendRootPath) => {
48
+ const segment = backendRootPath
49
+ .split("/")
50
+ .map((part) => part.trim())
51
+ .filter(Boolean)
52
+ .at(-1);
53
+ const normalized = segment
54
+ ?.toLowerCase()
55
+ .replace(/[^a-z0-9_-]+/g, "-")
56
+ .replace(/^-+|-+$/g, "");
57
+ if (!normalized) {
58
+ throw new Error(`service_key cannot be derived from backend_root_path for ${CLUE_INIT_COMMAND_NAME}`);
59
+ }
60
+ return normalized;
61
+ };
62
+
48
63
  export const buildClueInitRequestFromCommandInput = ({
49
64
  targetTool,
50
65
  input,
51
66
  }) => {
52
67
  const backendRootPath = requireField(input, "backend_root_path");
68
+ const serviceKey =
69
+ typeof input?.service_key === "string" && input.service_key.trim()
70
+ ? input.service_key.trim()
71
+ : deriveServiceKeyFromBackendRootPath(backendRootPath);
53
72
  return {
54
73
  target_tool: targetTool,
55
74
  project_key: requireField(input, "project_key"),
56
- service_key: requireField(input, "service_key"),
75
+ service_key: serviceKey,
57
76
  framework: requireField(input, "framework"),
58
77
  environment: requireField(input, "environment"),
59
78
  allowed_source_paths: normalizeStringArray(
package/src/contracts.mjs CHANGED
@@ -7,6 +7,7 @@ const {
7
7
  clueInitToolRequestSchema,
8
8
  clueInitToolReportSchema,
9
9
  semanticSnapshotRequestSchema,
10
+ semanticSnapshotResponseSchema,
10
11
  } = schemaPackage;
11
12
 
12
13
  const formatSchemaError = (result, field) => {
@@ -122,3 +123,7 @@ export const buildOperationSourceKey = (method, pathTemplate) =>
122
123
  export const validateSemanticSnapshotRequest = (input) => {
123
124
  return parseWithSchema(semanticSnapshotRequestSchema, input, "semantic snapshot");
124
125
  };
126
+
127
+ export const validateSemanticSnapshotResponse = (input) => {
128
+ return parseWithSchema(semanticSnapshotResponseSchema, input, "semantic snapshot response");
129
+ };
@@ -13,6 +13,7 @@ const FUNCTION_PATTERN = /(?:async\s+def|def)\s+(?<name>[A-Za-z_][A-Za-z0-9_]*)\
13
13
  const ROUTER_ASSIGNMENT = /\b(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*APIRouter\s*\(/g;
14
14
  const INCLUDE_ROUTER_CALL = /\b(?<owner>[A-Za-z_][A-Za-z0-9_]*)\.include_router\s*\(/g;
15
15
  const FROM_IMPORT = /^\s*from\s+(?<module>[A-Za-z0-9_.]+|\.+[A-Za-z0-9_.]*)\s+import\s+(?<names>[A-Za-z0-9_,\s]+)$/gm;
16
+ const IMPORT_MODULE = /^\s*import\s+(?<modules>[A-Za-z0-9_.,\s]+)$/gm;
16
17
 
17
18
  const sha256 = (value) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
18
19
 
@@ -125,6 +126,41 @@ const resolveImportFile = ({ filesByRelativePath, currentRelativePath, moduleNam
125
126
  return null;
126
127
  };
127
128
 
129
+ const resolveFromImportEntry = ({
130
+ filesByRelativePath,
131
+ currentRelativePath,
132
+ moduleName,
133
+ importedName,
134
+ }) => {
135
+ const childModuleName =
136
+ moduleName.endsWith(".") || moduleName === "."
137
+ ? `${moduleName}${importedName}`
138
+ : `${moduleName}.${importedName}`;
139
+ const childFile = resolveImportFile({
140
+ filesByRelativePath,
141
+ currentRelativePath,
142
+ moduleName: childModuleName,
143
+ });
144
+ if (childFile) {
145
+ return {
146
+ file: childFile,
147
+ name: undefined,
148
+ };
149
+ }
150
+ const moduleFile = resolveImportFile({
151
+ filesByRelativePath,
152
+ currentRelativePath,
153
+ moduleName,
154
+ });
155
+ if (!moduleFile) {
156
+ return null;
157
+ }
158
+ return {
159
+ file: moduleFile,
160
+ name: importedName,
161
+ };
162
+ };
163
+
128
164
  const parseRouterAssignments = (source, relativePath) => {
129
165
  const prefixes = new Map();
130
166
  for (const match of source.matchAll(ROUTER_ASSIGNMENT)) {
@@ -141,44 +177,99 @@ const parseRouterAssignments = (source, relativePath) => {
141
177
  const parseImports = ({ source, currentRelativePath, filesByRelativePath }) => {
142
178
  const imports = new Map();
143
179
  for (const match of source.matchAll(FROM_IMPORT)) {
144
- const targetFile = resolveImportFile({
145
- filesByRelativePath,
146
- currentRelativePath,
147
- moduleName: match.groups.module,
148
- });
149
- if (!targetFile) {
150
- continue;
151
- }
152
180
  for (const entry of parseImportNames(match.groups.names)) {
181
+ const resolved = resolveFromImportEntry({
182
+ filesByRelativePath,
183
+ currentRelativePath,
184
+ moduleName: match.groups.module,
185
+ importedName: entry.importedName,
186
+ });
187
+ if (!resolved) {
188
+ continue;
189
+ }
153
190
  imports.set(entry.localName, {
191
+ file: resolved.file,
192
+ name: resolved.name,
193
+ });
194
+ }
195
+ }
196
+ for (const match of source.matchAll(IMPORT_MODULE)) {
197
+ for (const entry of parseImportNames(match.groups.modules)) {
198
+ const targetFile = resolveImportFile({
199
+ filesByRelativePath,
200
+ currentRelativePath,
201
+ moduleName: entry.importedName,
202
+ });
203
+ if (!targetFile) {
204
+ continue;
205
+ }
206
+ imports.set(entry.localName.split(".").at(-1), {
154
207
  file: targetFile,
155
- name: entry.importedName,
208
+ name: undefined,
156
209
  });
157
210
  }
158
211
  }
159
212
  return imports;
160
213
  };
161
214
 
215
+ const routerKey = (file, routerName) => `${file}::${routerName}`;
216
+
217
+ const resolveRouterTarget = ({ target, relativePath, imports }) => {
218
+ const parts = target.split(".");
219
+ if (parts.length === 1) {
220
+ const imported = imports.get(target);
221
+ return {
222
+ file: imported?.file ?? relativePath,
223
+ routerName: imported?.name ?? target,
224
+ };
225
+ }
226
+ const imported = imports.get(parts[0]);
227
+ if (!imported?.file || imported.name) {
228
+ throw new Error(`${relativePath}: include_router dotted target cannot be analyzed safely`);
229
+ }
230
+ if (parts.length !== 2) {
231
+ throw new Error(`${relativePath}: include_router dotted target cannot be analyzed safely`);
232
+ }
233
+ return {
234
+ file: imported.file,
235
+ routerName: parts[1],
236
+ };
237
+ };
238
+
162
239
  const parseIncludeRouters = ({ source, relativePath, imports }) => {
163
240
  const includes = [];
164
241
  for (const match of source.matchAll(INCLUDE_ROUTER_CALL)) {
165
242
  const openParenIndex = match.index + match[0].length - 1;
166
243
  const args = readCallArgs(source, openParenIndex);
167
- const routerMatch = /^\s*(?<router>[A-Za-z_][A-Za-z0-9_]*)/.exec(args);
244
+ const routerMatch = /^\s*(?<router>[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)/.exec(args);
168
245
  if (!routerMatch?.groups?.router) {
169
246
  throw new Error(`${relativePath}: include_router target cannot be analyzed safely`);
170
247
  }
171
- const routerName = routerMatch.groups.router;
172
- const imported = imports.get(routerName);
248
+ const resolved = resolveRouterTarget({
249
+ target: routerMatch.groups.router,
250
+ relativePath,
251
+ imports,
252
+ });
173
253
  includes.push({
174
- file: imported?.file ?? relativePath,
175
- routerName: imported?.name ?? routerName,
254
+ ownerName: match.groups.owner,
255
+ file: resolved.file,
256
+ routerName: resolved.routerName,
176
257
  prefix: extractPrefix({ args, relativePath, callName: "include_router" }),
177
258
  });
178
259
  }
179
260
  return includes;
180
261
  };
181
262
 
263
+ const addIncludePrefix = (prefixesByRouterKey, key, prefix) => {
264
+ const current = prefixesByRouterKey.get(key) ?? [];
265
+ if (!current.includes(prefix)) {
266
+ current.push(prefix);
267
+ prefixesByRouterKey.set(key, current);
268
+ return true;
269
+ }
270
+ return false;
271
+ };
272
+
182
273
  const findNextFunction = (source, fromIndex) => {
183
274
  FUNCTION_PATTERN.lastIndex = fromIndex;
184
275
  const match = FUNCTION_PATTERN.exec(source);
@@ -270,6 +361,14 @@ export const analyzeFastApiRoutes = async ({ repoRoot, files }) => {
270
361
  });
271
362
  }
272
363
 
364
+ const routerPrefixesByKey = new Map();
365
+ for (const record of records) {
366
+ for (const [routerName, prefix] of record.routerPrefixes.entries()) {
367
+ routerPrefixesByKey.set(routerKey(record.relativePath, routerName), prefix);
368
+ }
369
+ }
370
+
371
+ const includeEdges = [];
273
372
  const includePrefixes = new Map();
274
373
  for (const record of records) {
275
374
  for (const include of parseIncludeRouters({
@@ -277,10 +376,42 @@ export const analyzeFastApiRoutes = async ({ repoRoot, files }) => {
277
376
  relativePath: record.relativePath,
278
377
  imports: record.imports,
279
378
  })) {
280
- const key = `${include.file}::${include.routerName}`;
281
- const current = includePrefixes.get(key) ?? [];
282
- current.push(include.prefix);
283
- includePrefixes.set(key, current);
379
+ const parentKey = record.routerPrefixes.has(include.ownerName)
380
+ ? routerKey(record.relativePath, include.ownerName)
381
+ : null;
382
+ includeEdges.push({
383
+ parentKey,
384
+ targetKey: routerKey(include.file, include.routerName),
385
+ prefix: include.prefix,
386
+ });
387
+ if (!parentKey) {
388
+ addIncludePrefix(
389
+ includePrefixes,
390
+ routerKey(include.file, include.routerName),
391
+ include.prefix,
392
+ );
393
+ }
394
+ }
395
+ }
396
+ let changed = true;
397
+ while (changed) {
398
+ changed = false;
399
+ for (const edge of includeEdges) {
400
+ if (!edge.parentKey) {
401
+ continue;
402
+ }
403
+ for (const parentPrefix of includePrefixes.get(edge.parentKey) ?? []) {
404
+ changed =
405
+ addIncludePrefix(
406
+ includePrefixes,
407
+ edge.targetKey,
408
+ combinePaths(
409
+ parentPrefix,
410
+ routerPrefixesByKey.get(edge.parentKey) ?? "",
411
+ edge.prefix,
412
+ ),
413
+ ) || changed;
414
+ }
284
415
  }
285
416
  }
286
417
 
@@ -289,7 +420,7 @@ export const analyzeFastApiRoutes = async ({ repoRoot, files }) => {
289
420
  const buildForDecorator = ({ match, method, decoratorPath }) => {
290
421
  const routerName = match.groups.router;
291
422
  const functionInfo = findNextFunction(record.source, match.index + match[0].length);
292
- const key = `${record.relativePath}::${routerName}`;
423
+ const key = routerKey(record.relativePath, routerName);
293
424
  const localRouterPrefix = record.routerPrefixes.get(routerName) ?? "";
294
425
  const prefixes = includePrefixes.get(key) ?? [""];
295
426
  for (const includePrefix of prefixes) {
package/src/init-tool.mjs CHANGED
@@ -1,86 +1,185 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { buildInitReport, validateInitRequest } from "./contracts.mjs";
4
- import { applyLifecyclePlan, planLifecycleInsertions } from "./lifecycle-init.mjs";
4
+ import {
5
+ applyLifecyclePlan,
6
+ planLifecycleInsertions,
7
+ } from "./lifecycle-init.mjs";
8
+
9
+ const DEFAULT_SEMANTIC_WORKFLOW_PATH =
10
+ ".github/workflows/clue-semantic-snapshot.yml";
11
+
12
+ const nonEmpty = (value, field) => {
13
+ if (typeof value !== "string" || value.trim() === "") {
14
+ throw new Error(`${field} is required`);
15
+ }
16
+ return value.trim();
17
+ };
18
+
19
+ const normalizeStringArray = (value, field, { min = 0 } = {}) => {
20
+ if (!Array.isArray(value)) {
21
+ throw new Error(`${field} must be an array`);
22
+ }
23
+ const result = value
24
+ .filter((entry) => typeof entry === "string" && entry.trim())
25
+ .map((entry) => entry.trim());
26
+ if (result.length < min) {
27
+ throw new Error(`${field} must include at least ${min} item(s)`);
28
+ }
29
+ return [...new Set(result)];
30
+ };
31
+
32
+ const splitCsv = (value) =>
33
+ typeof value === "string"
34
+ ? value
35
+ .split(",")
36
+ .map((entry) => entry.trim())
37
+ .filter(Boolean)
38
+ : [];
39
+
40
+ const deriveServiceKeyFromPath = (backendRootPath) => {
41
+ const segment = backendRootPath
42
+ .split("/")
43
+ .map((part) => part.trim())
44
+ .filter(Boolean)
45
+ .at(-1);
46
+ const normalized = segment
47
+ ?.toLowerCase()
48
+ .replace(/[^a-z0-9_-]+/g, "-")
49
+ .replace(/^-+|-+$/g, "");
50
+ if (!normalized) {
51
+ throw new Error("service-key cannot be derived from backend-root-path");
52
+ }
53
+ return normalized;
54
+ };
55
+
56
+ export const buildSemanticWorkflowRequestFromFlags = (flags) => {
57
+ const backendRootPath = nonEmpty(flags.backendRootPath, "backend-root-path");
58
+ const allowedSourcePaths = splitCsv(flags.allowedSourcePaths);
59
+ return {
60
+ ci_workflow_path:
61
+ typeof flags.workflowPath === "string" && flags.workflowPath.trim()
62
+ ? flags.workflowPath.trim()
63
+ : DEFAULT_SEMANTIC_WORKFLOW_PATH,
64
+ service_key:
65
+ typeof flags.serviceKey === "string" && flags.serviceKey.trim()
66
+ ? flags.serviceKey.trim()
67
+ : deriveServiceKeyFromPath(backendRootPath),
68
+ framework: nonEmpty(flags.framework, "framework"),
69
+ allowed_source_paths: normalizeStringArray(
70
+ allowedSourcePaths.length ? allowedSourcePaths : [backendRootPath],
71
+ "allowed-source-paths",
72
+ { min: 1 },
73
+ ),
74
+ excluded_source_paths: normalizeStringArray(
75
+ splitCsv(flags.excludedSourcePaths),
76
+ "excluded-source-paths",
77
+ ),
78
+ };
79
+ };
5
80
 
6
81
  const workflowRequestPayload = (request) =>
7
- JSON.stringify(
8
- {
9
- project_key: "${{ vars.CLUE_PROJECT_KEY }}",
10
- environment: "${{ vars.CLUE_ENVIRONMENT }}",
11
- service_key: request.service_key,
12
- repository: {
13
- provider: "github",
14
- repository_id: "${{ github.repository_id }}",
15
- owner: "${{ github.repository_owner }}",
16
- name: "${{ github.event.repository.name }}",
17
- default_branch: "${{ github.event.repository.default_branch }}",
18
- merge_commit: "${{ github.sha }}",
19
- workflow_run_id: "${{ github.run_id }}",
20
- },
21
- service: {
22
- service_key: request.service_key,
23
- root_path: request.allowed_source_paths[0],
24
- framework: request.framework,
25
- language: "python",
26
- },
27
- allowed_source_paths: request.allowed_source_paths,
28
- excluded_source_paths: request.excluded_source_paths,
29
- clue_api_base_url: "${{ vars.CLUE_API_BASE_URL }}",
30
- ai_model: "${{ vars.CLUE_AI_MODEL }}",
31
- },
32
- null,
33
- 10,
34
- ).trim();
82
+ JSON.stringify(
83
+ {
84
+ project_key: "${{ vars.CLUE_PROJECT_KEY }}",
85
+ environment: "${{ vars.CLUE_ENVIRONMENT }}",
86
+ service_key: request.service_key,
87
+ repository: {
88
+ provider: "github",
89
+ repository_id: "${{ github.repository_id }}",
90
+ merge_commit: "${{ github.sha }}",
91
+ workflow_run_id: "${{ github.run_id }}",
92
+ },
93
+ service: {
94
+ service_key: request.service_key,
95
+ root_path: request.allowed_source_paths[0],
96
+ framework: request.framework,
97
+ language: "python",
98
+ },
99
+ allowed_source_paths: request.allowed_source_paths,
100
+ excluded_source_paths: request.excluded_source_paths,
101
+ clue_api_base_url: "${{ vars.CLUE_API_BASE_URL }}",
102
+ ai_model: "${{ vars.CLUE_AI_MODEL }}",
103
+ },
104
+ null,
105
+ 10,
106
+ ).trim();
107
+
108
+ const indentMultiline = (value, spaces) => {
109
+ const prefix = " ".repeat(spaces);
110
+ return value
111
+ .split("\n")
112
+ .map((line) => `${prefix}${line}`)
113
+ .join("\n");
114
+ };
35
115
 
36
116
  const workflowTemplate = (request) => `name: Clue Semantic Snapshot
37
117
 
38
118
  on:
39
119
  push:
40
120
 
121
+ permissions:
122
+ contents: read
123
+
41
124
  jobs:
42
125
  semantic-snapshot:
43
126
  if: github.ref_name == github.event.repository.default_branch
44
127
  runs-on: ubuntu-latest
45
128
  steps:
46
129
  - uses: actions/checkout@v4
130
+ with:
131
+ persist-credentials: false
47
132
  - uses: actions/setup-node@v4
48
133
  with:
49
134
  node-version: "20"
50
135
  - name: Run Clue semantic generation
51
- continue-on-error: true
52
136
  env:
53
137
  CLUE_API_KEY: \${{ secrets.CLUE_API_KEY }}
54
138
  AI_PROVIDER_API_KEY: \${{ secrets.AI_PROVIDER_API_KEY }}
139
+ CLUE_SEMANTIC_REQUEST_JSON: |
140
+ ${indentMultiline(workflowRequestPayload(request), 12)}
55
141
  run: |
56
- mkdir -p .clue
57
- cat > .clue/semantic-request.runtime.json <<'JSON'
58
- ${workflowRequestPayload(request)}
59
- JSON
60
- npx @clue-ai/cli semantic-ci --request .clue/semantic-request.runtime.json --repo .
142
+ npx @clue-ai/cli semantic-ci --request-env CLUE_SEMANTIC_REQUEST_JSON --repo .
61
143
  `;
62
144
 
145
+ export const writeSemanticWorkflow = async ({ repoRoot, request }) => {
146
+ const workflowPath = join(repoRoot, request.ci_workflow_path);
147
+ await mkdir(dirname(workflowPath), { recursive: true });
148
+ await writeFile(workflowPath, workflowTemplate(request), "utf8");
149
+ return {
150
+ ci_workflow_path: request.ci_workflow_path,
151
+ ci_workflow_added: true,
152
+ required_secrets: ["CLUE_API_KEY", "AI_PROVIDER_API_KEY"],
153
+ required_variables: [
154
+ "CLUE_PROJECT_KEY",
155
+ "CLUE_ENVIRONMENT",
156
+ "CLUE_API_BASE_URL",
157
+ "CLUE_AI_MODEL",
158
+ ],
159
+ runtime_request_committed: false,
160
+ semantic_generation_timing: "after_merge_ci",
161
+ allowed_source_paths: request.allowed_source_paths,
162
+ excluded_source_paths: request.excluded_source_paths,
163
+ };
164
+ };
165
+
63
166
  export const runInitTool = async ({
64
- repoRoot,
65
- request: rawRequest,
66
- env = process.env,
67
- lifecyclePlanner = planLifecycleInsertions,
167
+ repoRoot,
168
+ request: rawRequest,
169
+ env = process.env,
170
+ lifecyclePlanner = planLifecycleInsertions,
68
171
  }) => {
69
- const request = validateInitRequest(rawRequest);
70
- const workflowPath = join(repoRoot, request.ci_workflow_path);
71
- await mkdir(dirname(workflowPath), { recursive: true });
72
- await writeFile(workflowPath, workflowTemplate(request), "utf8");
73
- const lifecyclePlan = await lifecyclePlanner({ repoRoot, request, env });
74
- const lifecycleResult = await applyLifecyclePlan({
75
- repoRoot,
76
- plan: lifecyclePlan,
77
- });
172
+ const request = validateInitRequest(rawRequest);
173
+ await writeSemanticWorkflow({ repoRoot, request });
174
+ const lifecyclePlan = await lifecyclePlanner({ repoRoot, request, env });
175
+ const lifecycleResult = await applyLifecyclePlan({
176
+ repoRoot,
177
+ plan: lifecyclePlan,
178
+ });
78
179
 
79
- return buildInitReport({
80
- request,
81
- lifecycleInsertions: lifecycleResult.lifecycleInsertions,
82
- warnings: [
83
- ...lifecycleResult.warnings,
84
- ],
85
- });
180
+ return buildInitReport({
181
+ request,
182
+ lifecycleInsertions: lifecycleResult.lifecycleInsertions,
183
+ warnings: [...lifecycleResult.warnings],
184
+ });
86
185
  };