@nick848/sf-cli 1.0.19 → 1.0.21

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/dist/index.js CHANGED
@@ -254,23 +254,38 @@ ${resource.analysis}`;
254
254
  lines.push("");
255
255
  lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 6/9: OpenSpec \u89C4\u683C \u2501\u2501\u2501"));
256
256
  lines.push("");
257
- activeSession.specItems = generateSpecItems(
258
- activeSession.refinedRequirement,
259
- activeSession.context,
260
- activeSession.bddScenarios,
261
- activeSession.clarificationQuestions,
262
- activeSession.referenceResources
263
- );
257
+ const loader = new LoadingIndicator("AI \u6B63\u5728\u62C6\u5206\u89C4\u683C");
258
+ loader.start();
259
+ try {
260
+ activeSession.specItems = await generateSpecItemsWithAI(
261
+ activeSession.refinedRequirement,
262
+ activeSession.context,
263
+ activeSession.bddScenarios,
264
+ activeSession.clarificationQuestions,
265
+ activeSession.referenceResources,
266
+ ctx
267
+ );
268
+ loader.stop();
269
+ } catch {
270
+ loader.stop(chalk9__default.default.yellow(" \u26A0 \u4F7F\u7528\u57FA\u7840\u89C4\u683C\u62C6\u5206"));
271
+ activeSession.specItems = generateSpecItems(
272
+ activeSession.refinedRequirement,
273
+ activeSession.context,
274
+ activeSession.bddScenarios,
275
+ activeSession.clarificationQuestions,
276
+ activeSession.referenceResources
277
+ );
278
+ }
264
279
  const specPath = await saveSpecFile(ctx.options.workingDirectory, activeSession);
265
280
  lines.push(chalk9__default.default.green(" \u2713 \u89C4\u683C\u6587\u4EF6\u5DF2\u751F\u6210"));
266
281
  lines.push(chalk9__default.default.gray(` \u8DEF\u5F84: ${specPath}`));
267
282
  lines.push("");
268
283
  lines.push(chalk9__default.default.cyan(" \u4EFB\u52A1\u6982\u89C8:"));
269
- for (const item of activeSession.specItems.slice(0, 5)) {
284
+ for (const item of activeSession.specItems.slice(0, 8)) {
270
285
  const icon = item.priority === "high" ? "\u{1F534}" : item.priority === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
271
286
  lines.push(chalk9__default.default.gray(` ${icon} [${item.id}] ${item.title}`));
272
287
  }
273
- if (activeSession.specItems.length > 5) {
288
+ if (activeSession.specItems.length > 8) {
274
289
  lines.push(chalk9__default.default.gray(` ... \u5171 ${activeSession.specItems.length} \u4E2A\u4EFB\u52A1`));
275
290
  }
276
291
  lines.push("");
@@ -417,13 +432,15 @@ async function handleWorkflowInput(input, ctx) {
417
432
  activeSession.bddScenarios = generateBDDScenarios(
418
433
  activeSession.refinedRequirement,
419
434
  activeSession.context,
420
- activeSession.clarificationQuestions
435
+ activeSession.clarificationQuestions,
436
+ activeSession.referenceResources
421
437
  );
422
438
  activeSession.specItems = generateSpecItems(
423
439
  activeSession.refinedRequirement,
424
440
  activeSession.context,
425
441
  activeSession.bddScenarios,
426
- activeSession.clarificationQuestions
442
+ activeSession.clarificationQuestions,
443
+ activeSession.referenceResources
427
444
  );
428
445
  const specPath = await saveSpecFile(ctx.options.workingDirectory, activeSession);
429
446
  return {
@@ -582,107 +599,115 @@ function getCategoryLabel(category) {
582
599
  async function executeDevelopment(ctx, session) {
583
600
  const workingDir = ctx.options.workingDirectory;
584
601
  const files = [];
585
- const loader = new LoadingIndicator("AI \u6B63\u5728\u751F\u6210\u4EE3\u7801");
586
- loader.start();
587
- try {
588
- const systemPrompt = buildDevelopmentPrompt(session);
589
- const messages = [
590
- {
591
- role: "system",
592
- content: `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u524D\u7AEF\u5F00\u53D1\u5DE5\u7A0B\u5E08\u3002\u8BF7\u6839\u636E\u9700\u6C42\u89C4\u683C\u751F\u6210\u4EE3\u7801\u5B9E\u73B0\u3002
602
+ console.log("");
603
+ for (let i = 0; i < session.specItems.length; i++) {
604
+ const item = session.specItems[i];
605
+ const prefix = `[${i + 1}/${session.specItems.length}]`;
606
+ if (item.title.includes("\u6D4B\u8BD5") || item.title.includes("test")) {
607
+ continue;
608
+ }
609
+ const loader = new LoadingIndicator(`${prefix} \u751F\u6210: ${item.title.slice(0, 20)}...`);
610
+ loader.start();
611
+ try {
612
+ const taskPrompt = buildTaskPrompt(session, item, i);
613
+ const messages = [
614
+ {
615
+ role: "system",
616
+ content: `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u524D\u7AEF\u5F00\u53D1\u5DE5\u7A0B\u5E08\u3002\u8BF7\u6839\u636E\u4EFB\u52A1\u63CF\u8FF0\u751F\u6210\u4EE3\u7801\u5B9E\u73B0\u3002
593
617
 
594
618
  \u26A0\uFE0F \u91CD\u8981\u89C4\u5219\uFF1A
595
- 1. \u6280\u672F\u5B9E\u73B0\u5FC5\u987B\u4E25\u683C\u9075\u5FAA\u9879\u76EE\u73B0\u6709\u7684\u5F00\u53D1\u89C4\u8303
596
- 2. \u53C2\u8003\u8D44\u6E90\uFF08\u5982\u679C\u6709\uFF09\u4EC5\u7528\u4E8E\u7406\u89E3\u4E1A\u52A1\u529F\u80FD\uFF0C\u4E0D\u8981\u590D\u5236\u5176\u6280\u672F\u5B9E\u73B0
597
- 3. \u4F7F\u7528\u9879\u76EE\u6307\u5B9A\u7684\u6280\u672F\u6808\u548C\u6846\u67B6
598
-
599
- \u4EE3\u7801\u8981\u6C42\uFF1A
600
- 1. \u751F\u6210\u5B8C\u6574\u3001\u53EF\u8FD0\u884C\u7684\u4EE3\u7801
601
- 2. \u9075\u5FAA\u9879\u76EE\u73B0\u6709\u7684\u4EE3\u7801\u98CE\u683C\u548C\u89C4\u8303
602
- 3. \u4F7F\u7528\u9879\u76EE\u6307\u5B9A\u7684\u6280\u672F\u6808
603
- 4. \u4EE3\u7801\u8981\u6709\u9002\u5F53\u7684\u6CE8\u91CA
604
- 5. \u8FD4\u56DE\u683C\u5F0F\uFF1A\u6BCF\u4E2A\u6587\u4EF6\u7528 \`\`\`filename \u4EE3\u7801 \`\`\` \u5305\u88F9
619
+ 1. \u53EA\u751F\u6210\u5F53\u524D\u4EFB\u52A1\u76F8\u5173\u7684\u4EE3\u7801\uFF0C\u4E0D\u8981\u751F\u6210\u5176\u4ED6\u4EFB\u52A1\u7684\u4EE3\u7801
620
+ 2. \u6280\u672F\u5B9E\u73B0\u5FC5\u987B\u4E25\u683C\u9075\u5FAA\u9879\u76EE\u73B0\u6709\u7684\u5F00\u53D1\u89C4\u8303
621
+ 3. \u751F\u6210\u5B8C\u6574\u3001\u53EF\u8FD0\u884C\u7684\u4EE3\u7801
622
+ 4. \u8FD4\u56DE\u683C\u5F0F\uFF1A\u6BCF\u4E2A\u6587\u4EF6\u7528 \`\`\`filename \u4EE3\u7801 \`\`\` \u5305\u88F9
605
623
 
606
624
  \u9879\u76EE\u4FE1\u606F\uFF1A
607
625
  - \u540D\u79F0: ${session.context?.name}
608
626
  - \u6846\u67B6: ${session.context?.framework || "\u672A\u6307\u5B9A"}
609
627
  - \u6280\u672F\u6808: ${session.context?.techStack.join(", ") || "\u672A\u6307\u5B9A"}
610
628
 
611
- ${session.context?.devStandards ? `\u3010\u5FC5\u987B\u9075\u5FAA\u7684\u5F00\u53D1\u89C4\u8303\u3011
612
- ${session.context.devStandards.slice(0, 2500)}` : ""}`
613
- },
614
- {
615
- role: "user",
616
- content: systemPrompt
629
+ ${session.context?.devStandards ? `\u3010\u5F00\u53D1\u89C4\u8303\u3011
630
+ ${session.context.devStandards.slice(0, 2e3)}` : ""}`
631
+ },
632
+ {
633
+ role: "user",
634
+ content: taskPrompt
635
+ }
636
+ ];
637
+ const response = await ctx.modelService.sendMessage(messages, {
638
+ temperature: 0.3,
639
+ maxTokens: 4e3,
640
+ // 单个任务减少 token
641
+ agent: "frontend-dev",
642
+ timeout: 12e4
643
+ // 2分钟超时
644
+ });
645
+ const codeBlocks = parseCodeBlocks(response.content);
646
+ if (codeBlocks.length > 0) {
647
+ for (const block of codeBlocks) {
648
+ const filePath = path4__namespace.join(workingDir, block.filename);
649
+ const dir = path4__namespace.dirname(filePath);
650
+ await fs4__namespace.mkdir(dir, { recursive: true });
651
+ await fs4__namespace.writeFile(filePath, block.code, "utf-8");
652
+ files.push(block.filename);
653
+ }
654
+ loader.stop(chalk9__default.default.green(`${prefix} \u2713 ${item.title.slice(0, 25)} (${codeBlocks.length} \u4E2A\u6587\u4EF6)`));
655
+ } else {
656
+ const implDir = path4__namespace.join(workingDir, "src");
657
+ await fs4__namespace.mkdir(implDir, { recursive: true });
658
+ const fileName = `${item.title.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, "_")}.ts`;
659
+ const filePath = path4__namespace.join(implDir, fileName);
660
+ await fs4__namespace.writeFile(filePath, `// TODO: ${item.title}
661
+ // ${item.description}
662
+ `, "utf-8");
663
+ files.push(`src/${fileName}`);
664
+ loader.stop(chalk9__default.default.yellow(`${prefix} \u26A0 \u751F\u6210\u57FA\u7840\u6A21\u677F: ${item.title.slice(0, 20)}`));
665
+ }
666
+ if (i < session.specItems.length - 1) {
667
+ await new Promise((resolve4) => setTimeout(resolve4, 500));
617
668
  }
618
- ];
619
- loader.update("\u6B63\u5728\u8C03\u7528 AI \u6A21\u578B");
620
- const response = await ctx.modelService.sendMessage(messages, {
621
- temperature: 0.3,
622
- maxTokens: 8e3,
623
- agent: "frontend-dev",
624
- timeout: 18e4
625
- // 3 分钟超时
626
- });
627
- loader.update("\u6B63\u5728\u89E3\u6790\u4EE3\u7801");
628
- const codeBlocks = parseCodeBlocks(response.content);
629
- for (const block of codeBlocks) {
630
- const filePath = path4__namespace.join(workingDir, block.filename);
631
- const dir = path4__namespace.dirname(filePath);
632
- await fs4__namespace.mkdir(dir, { recursive: true });
633
- await fs4__namespace.writeFile(filePath, block.code, "utf-8");
634
- files.push(block.filename);
635
- }
636
- if (files.length === 0) {
637
- const implDir = path4__namespace.join(workingDir, "src", "features");
638
- await fs4__namespace.mkdir(implDir, { recursive: true });
639
- const featureName = session.specItems[0]?.title || "feature";
640
- const fileName = `${featureName.replace(/[^a-zA-Z0-9]/g, "_")}.ts`;
641
- const filePath = path4__namespace.join(implDir, fileName);
642
- const stubCode = `/**
643
- * ${session.requirement}
644
- *
645
- * TODO: \u6B64\u6587\u4EF6\u7531 AI \u81EA\u52A8\u751F\u6210\uFF0C\u8BF7\u6839\u636E\u9700\u6C42\u5B8C\u5584\u5B9E\u73B0
646
- */
647
-
648
- export function ${featureName.replace(/[^a-zA-Z0-9]/g, "")}() {
649
- // TODO: \u5B9E\u73B0\u529F\u80FD
650
- console.log('${featureName} - \u5F85\u5B9E\u73B0');
651
- }
652
- `;
653
- await fs4__namespace.writeFile(filePath, stubCode, "utf-8");
654
- files.push(`src/features/${fileName}`);
669
+ } catch (error) {
670
+ loader.stop(chalk9__default.default.red(`${prefix} \u2717 \u5931\u8D25: ${error.message.slice(0, 40)}`));
655
671
  }
656
- loader.stop(chalk9__default.default.green(` \u2713 \u5DF2\u751F\u6210 ${files.length} \u4E2A\u6587\u4EF6`));
672
+ }
673
+ if (files.length > 0) {
657
674
  return { success: true, files };
658
- } catch (error) {
659
- loader.stop();
675
+ } else {
660
676
  return {
661
677
  success: false,
662
678
  files: [],
663
- error: error.message
679
+ error: "\u6240\u6709\u4EFB\u52A1\u4EE3\u7801\u751F\u6210\u5931\u8D25"
664
680
  };
665
681
  }
666
682
  }
667
- function buildDevelopmentPrompt(session) {
683
+ function buildTaskPrompt(session, item, index) {
668
684
  const lines = [];
669
- lines.push("## \u9700\u6C42\u63CF\u8FF0");
670
- lines.push(session.refinedRequirement);
671
- lines.push("");
672
- lines.push("## BDD \u573A\u666F");
673
- for (const scenario of session.bddScenarios) {
674
- lines.push(`### ${scenario.feature}`);
675
- for (const s of scenario.scenarios) {
676
- lines.push(`- ${s.name}`);
685
+ lines.push(`## \u5F53\u524D\u4EFB\u52A1 (#${index + 1})`);
686
+ lines.push(`**\u6807\u9898**: ${item.title}`);
687
+ lines.push(`**\u63CF\u8FF0**: ${item.description}`);
688
+ lines.push(`**\u4F18\u5148\u7EA7**: ${item.priority}`);
689
+ if (item.tests && item.tests.length > 0) {
690
+ lines.push(`**\u9A8C\u6536\u6807\u51C6**:`);
691
+ for (const t of item.tests) {
692
+ lines.push(`- ${t}`);
677
693
  }
678
694
  }
679
695
  lines.push("");
680
- lines.push("## \u4EFB\u52A1\u5217\u8868");
681
- for (const item of session.specItems) {
682
- lines.push(`- [${item.id}] ${item.title}: ${item.description}`);
696
+ lines.push(`## \u6574\u4F53\u9700\u6C42\u80CC\u666F`);
697
+ lines.push(session.requirement);
698
+ const relatedScenario = session.bddScenarios.find(
699
+ (s) => item.title.includes(s.feature) || s.feature.includes(item.title)
700
+ );
701
+ if (relatedScenario) {
702
+ lines.push("");
703
+ lines.push(`## \u76F8\u5173 BDD \u573A\u666F`);
704
+ lines.push(`Feature: ${relatedScenario.feature}`);
705
+ for (const s of relatedScenario.scenarios.slice(0, 2)) {
706
+ lines.push(`- ${s.name}`);
707
+ }
683
708
  }
684
709
  lines.push("");
685
- lines.push("\u8BF7\u6839\u636E\u4EE5\u4E0A\u9700\u6C42\u89C4\u683C\u751F\u6210\u4EE3\u7801\u5B9E\u73B0\u3002");
710
+ lines.push("\u8BF7\u751F\u6210\u5B9E\u73B0\u6B64\u4EFB\u52A1\u7684\u4EE3\u7801\u3002\u53EA\u751F\u6210\u5F53\u524D\u4EFB\u52A1\u9700\u8981\u7684\u6587\u4EF6\u3002");
686
711
  return lines.join("\n");
687
712
  }
688
713
  function parseCodeBlocks(content) {
@@ -1149,6 +1174,111 @@ function generateSpecItems(requirement, context, bddScenarios, questions, refere
1149
1174
  });
1150
1175
  return items;
1151
1176
  }
1177
+ async function generateSpecItemsWithAI(requirement, context, bddScenarios, questions, references, ctx) {
1178
+ const prompt2 = `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u9879\u76EE\u7ECF\u7406\u548C\u6280\u672F\u67B6\u6784\u5E08\u3002\u8BF7\u5C06\u4EE5\u4E0B\u9700\u6C42\u62C6\u5206\u4E3A\u7CBE\u7EC6\u5316\u7684\u5F00\u53D1\u4EFB\u52A1\u3002
1179
+
1180
+ ## \u9700\u6C42\u63CF\u8FF0
1181
+ ${requirement}
1182
+
1183
+ ## \u9879\u76EE\u4E0A\u4E0B\u6587
1184
+ - \u6280\u672F\u6808: ${context.techStack?.join(", ") || "TypeScript"}
1185
+ - \u6846\u67B6: ${context.framework || "\u672A\u6307\u5B9A"}
1186
+ ${context.devStandards ? `
1187
+ ## \u5F00\u53D1\u89C4\u8303\uFF08\u5FC5\u987B\u9075\u5FAA\uFF09
1188
+ ${context.devStandards.slice(0, 2e3)}
1189
+ ` : ""}
1190
+
1191
+ ## BDD \u573A\u666F\u53C2\u8003
1192
+ ${bddScenarios.map((s) => `- Feature: ${s.feature} (${s.scenarios.length} \u4E2A\u573A\u666F)`).join("\n")}
1193
+
1194
+ ## \u6F84\u6E05\u4FE1\u606F
1195
+ ${questions.filter((q) => q.answered).map((q) => `- ${q.question}: ${q.answer}`).join("\n") || "\u65E0"}
1196
+
1197
+ ## \u62C6\u5206\u8981\u6C42
1198
+
1199
+ \u8BF7\u5C06\u9700\u6C42\u62C6\u5206\u4E3A **\u7EC6\u7C92\u5EA6\u7684\u5F00\u53D1\u4EFB\u52A1**\uFF0C\u6BCF\u4E2A\u4EFB\u52A1\u5E94\u8BE5\uFF1A
1200
+ 1. **\u5355\u4E00\u804C\u8D23** - \u4E00\u4E2A\u4EFB\u52A1\u53EA\u505A\u4E00\u4EF6\u4E8B
1201
+ 2. **\u53EF\u72EC\u7ACB\u6D4B\u8BD5** - \u6709\u660E\u786E\u7684\u9A8C\u6536\u6807\u51C6
1202
+ 3. **2-4\u5C0F\u65F6\u53EF\u5B8C\u6210** - \u5982\u679C\u4EFB\u52A1\u592A\u5927\uFF0C\u7EE7\u7EED\u62C6\u5206
1203
+ 4. **\u6709\u660E\u786E\u7684\u8F93\u5165\u8F93\u51FA** - \u6E05\u695A\u77E5\u9053\u9700\u8981\u4EC0\u4E48\u3001\u4EA7\u51FA\u4EC0\u4E48
1204
+
1205
+ ## \u8F93\u51FA\u683C\u5F0F (JSON)
1206
+ \`\`\`json
1207
+ [
1208
+ {
1209
+ "id": "T001",
1210
+ "title": "\u4EFB\u52A1\u6807\u9898\uFF08\u7B80\u77ED\u660E\u786E\uFF09",
1211
+ "description": "\u8BE6\u7EC6\u63CF\u8FF0\uFF1A\u8981\u505A\u4EC0\u4E48\u3001\u5982\u4F55\u505A\u3001\u9A8C\u6536\u6807\u51C6",
1212
+ "priority": "high",
1213
+ "estimatedHours": 2,
1214
+ "dependencies": [],
1215
+ "acceptanceCriteria": ["\u9A8C\u6536\u6807\u51C61", "\u9A8C\u6536\u6807\u51C62"]
1216
+ }
1217
+ ]
1218
+ \`\`\`
1219
+
1220
+ ## \u62C6\u5206\u5EFA\u8BAE
1221
+ \u5BF9\u4E8E\u590D\u6742\u529F\u80FD\uFF08\u5982\u7B97\u6CD5\u7C7B\uFF09\uFF0C\u5E94\u8BE5\u62C6\u5206\u4E3A\uFF1A
1222
+ - \u6570\u636E\u7ED3\u6784/\u6A21\u578B\u5B9A\u4E49
1223
+ - \u6838\u5FC3\u7B97\u6CD5\u5206\u6B65\u5B9E\u73B0\uFF08\u6BCF\u4E2A\u8BA1\u7B97\u6B65\u9AA4\u4E00\u4E2A\u4EFB\u52A1\uFF09
1224
+ - \u8F93\u5165\u9A8C\u8BC1
1225
+ - \u7ED3\u679C\u683C\u5F0F\u5316
1226
+ - UI \u5C55\u793A\u7EC4\u4EF6
1227
+ - \u96C6\u6210\u6D4B\u8BD5
1228
+
1229
+ \u8BF7\u76F4\u63A5\u8F93\u51FA JSON \u6570\u7EC4\u3002`;
1230
+ const loader = new LoadingIndicator("AI \u6B63\u5728\u62C6\u5206\u89C4\u683C");
1231
+ loader.start();
1232
+ try {
1233
+ const response = await ctx.modelService.sendMessage([
1234
+ { role: "user", content: prompt2 }
1235
+ ], {
1236
+ temperature: 0.3,
1237
+ maxTokens: 4e3,
1238
+ timeout: 18e4
1239
+ // 增加到3分钟
1240
+ });
1241
+ const jsonMatch = response.content.match(/```json\s*([\s\S]*?)```/);
1242
+ if (jsonMatch) {
1243
+ try {
1244
+ const parsed = JSON.parse(jsonMatch[1].trim());
1245
+ loader.stop(chalk9__default.default.green(` \u2713 \u5DF2\u62C6\u5206 ${parsed.length} \u4E2A\u4EFB\u52A1`));
1246
+ return parsed.map((item, index) => ({
1247
+ id: item.id || `T${String(index + 1).padStart(3, "0")}`,
1248
+ title: item.title,
1249
+ description: item.description,
1250
+ priority: item.priority || "medium",
1251
+ files: [],
1252
+ tests: item.acceptanceCriteria || []
1253
+ }));
1254
+ } catch (parseError) {
1255
+ loader.stop(chalk9__default.default.yellow(` \u26A0 JSON \u89E3\u6790\u5931\u8D25: ${parseError.message.slice(0, 50)}`));
1256
+ return generateSpecItems(requirement, context, bddScenarios, questions, references);
1257
+ }
1258
+ }
1259
+ try {
1260
+ const parsed = JSON.parse(response.content);
1261
+ if (Array.isArray(parsed)) {
1262
+ loader.stop(chalk9__default.default.green(` \u2713 \u5DF2\u62C6\u5206 ${parsed.length} \u4E2A\u4EFB\u52A1`));
1263
+ return parsed.map((item, index) => ({
1264
+ id: item.id || `T${String(index + 1).padStart(3, "0")}`,
1265
+ title: item.title,
1266
+ description: item.description,
1267
+ priority: item.priority || "medium",
1268
+ files: [],
1269
+ tests: item.acceptanceCriteria || []
1270
+ }));
1271
+ }
1272
+ } catch {
1273
+ }
1274
+ loader.stop(chalk9__default.default.yellow(" \u26A0 \u672A\u80FD\u89E3\u6790 AI \u54CD\u5E94\uFF0C\u4F7F\u7528\u57FA\u7840\u62C6\u5206"));
1275
+ return generateSpecItems(requirement, context, bddScenarios, questions, references);
1276
+ } catch (error) {
1277
+ const errMsg = error.message.slice(0, 80);
1278
+ loader.stop(chalk9__default.default.yellow(` \u26A0 AI \u8C03\u7528\u5931\u8D25: ${errMsg}`));
1279
+ return generateSpecItems(requirement, context, bddScenarios, questions, references);
1280
+ }
1281
+ }
1152
1282
  async function saveSpecFile(workingDir, session) {
1153
1283
  const specDir = path4__namespace.join(workingDir, "openspec", "changes");
1154
1284
  await fs4__namespace.mkdir(specDir, { recursive: true });