@nick848/fet 1.0.8 → 1.0.10

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/cli/index.js CHANGED
@@ -179,7 +179,12 @@ function toGitNexusState(detection, previous) {
179
179
  setupHandoffPath: previous?.setupHandoffPath ?? null,
180
180
  setupHandoffUpdatedAt: previous?.setupHandoffUpdatedAt ?? null,
181
181
  handoffPath: previous?.handoffPath ?? null,
182
- handoffUpdatedAt: previous?.handoffUpdatedAt ?? null
182
+ handoffUpdatedAt: previous?.handoffUpdatedAt ?? null,
183
+ projectContextPath: previous?.projectContextPath ?? null,
184
+ projectContextUpdatedAt: previous?.projectContextUpdatedAt ?? null,
185
+ workflowContextPath: previous?.workflowContextPath ?? null,
186
+ workflowContextUpdatedAt: previous?.workflowContextUpdatedAt ?? null,
187
+ lastWorkflowGraphQuery: previous?.lastWorkflowGraphQuery ?? null
183
188
  };
184
189
  }
185
190
  async function inspectGitNexusGraph(projectRoot, env = process.env) {
@@ -248,7 +253,8 @@ function resolveGitNexusCommand(env) {
248
253
  const raw = env.FET_GITNEXUS_COMMAND?.trim() || env.FET_GITNEXUS_EXECUTABLE?.trim() || "gitnexus";
249
254
  const parts = splitCommand(raw);
250
255
  const [file = "gitnexus", ...args] = parts;
251
- return { file, args, label: raw };
256
+ const resolvedFile = process.platform === "win32" && raw === "gitnexus" ? "gitnexus.cmd" : file;
257
+ return { file: resolvedFile, args, label: raw };
252
258
  }
253
259
  function splitCommand(value) {
254
260
  const matches = value.match(/"[^"]+"|'[^']+'|\S+/g);
@@ -442,8 +448,313 @@ FET:END -->
442
448
  }
443
449
 
444
450
  // src/commands/graph.ts
445
- import { mkdir as mkdir4 } from "fs/promises";
451
+ import { mkdir as mkdir5 } from "fs/promises";
452
+ import { dirname as dirname6, join as join9 } from "path";
453
+
454
+ // src/graph-context.ts
455
+ import { mkdir as mkdir4, readdir, readFile as readFile5 } from "fs/promises";
446
456
  import { dirname as dirname5, join as join8 } from "path";
457
+ var MAX_SOURCE_CONTEXT = 8e3;
458
+ var MAX_GRAPH_OUTPUT = 2e4;
459
+ async function buildProjectGraphContext(ctx, state, trigger) {
460
+ if (!isGraphReadable(state)) {
461
+ return {
462
+ generated: false,
463
+ path: null,
464
+ query: null,
465
+ warnings: ["GitNexus graph exists check did not pass; project graph context was not generated."]
466
+ };
467
+ }
468
+ const query = "FET OpenSpec workflow architecture commands adapters graph integration project structure";
469
+ const goal = "Summarize the repository modules, workflow entry points, and likely insertion points for future FET/OpenSpec work.";
470
+ const graphQuery = await runGitNexus(["query", query, "--goal", goal, "--limit", "8"], { cwd: ctx.projectRoot });
471
+ const status = await runGitNexus(["status"], { cwd: ctx.projectRoot });
472
+ const warnings = commandWarnings([["gitnexus query", graphQuery], ["gitnexus status", status]]);
473
+ const relativePath = ".fet/graph-context/project.md";
474
+ await writeGraphContext(
475
+ join8(ctx.projectRoot, relativePath),
476
+ renderProjectContext({
477
+ trigger,
478
+ state,
479
+ query,
480
+ goal,
481
+ status: commandText(status),
482
+ graphOutput: commandText(graphQuery),
483
+ warnings
484
+ })
485
+ );
486
+ return {
487
+ generated: true,
488
+ path: relativePath,
489
+ query,
490
+ warnings
491
+ };
492
+ }
493
+ async function buildWorkflowGraphContext(ctx, options) {
494
+ const existing = await ctx.stateStore.getOrCreateGlobal();
495
+ if (!existing.graph?.gitnexus?.graphExists) {
496
+ return {
497
+ generated: false,
498
+ path: null,
499
+ query: null,
500
+ warnings: []
501
+ };
502
+ }
503
+ const state = await refreshGitNexusState(ctx);
504
+ if (!isGraphReadable(state)) {
505
+ return {
506
+ generated: false,
507
+ path: null,
508
+ query: null,
509
+ warnings: []
510
+ };
511
+ }
512
+ const sourceContext = await collectOpenSpecContext(ctx.projectRoot, options.changeId);
513
+ const query = buildWorkflowQuery(options, sourceContext);
514
+ const goal = workflowGoal(options.command);
515
+ const graphQuery = await runGitNexus(["query", query, "--goal", goal, "--limit", "8"], { cwd: ctx.projectRoot });
516
+ const detectChanges = shouldDetectChanges(options.command) ? await runGitNexus(["detect-changes", "--scope", "all"], { cwd: ctx.projectRoot }) : null;
517
+ const warnings = commandWarnings([
518
+ ["gitnexus query", graphQuery],
519
+ ...detectChanges ? [["gitnexus detect-changes", detectChanges]] : []
520
+ ]);
521
+ const relativePath = `.fet/graph-context/${sanitizePathPart(options.changeId ?? options.command)}.md`;
522
+ await writeGraphContext(
523
+ join8(ctx.projectRoot, relativePath),
524
+ renderWorkflowContext({
525
+ state,
526
+ command: options.command,
527
+ args: options.args,
528
+ changeId: options.changeId,
529
+ query,
530
+ goal,
531
+ sourceContext,
532
+ graphOutput: commandText(graphQuery),
533
+ detectChanges: detectChanges ? commandText(detectChanges) : null,
534
+ warnings
535
+ })
536
+ );
537
+ const global = await ctx.stateStore.getOrCreateGlobal();
538
+ global.graph ??= {};
539
+ global.graph.gitnexus = {
540
+ ...state,
541
+ workflowContextPath: relativePath,
542
+ workflowContextUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(),
543
+ lastWorkflowGraphQuery: query
544
+ };
545
+ await ctx.stateStore.writeGlobal(global);
546
+ return {
547
+ generated: true,
548
+ path: relativePath,
549
+ query,
550
+ warnings
551
+ };
552
+ }
553
+ async function refreshGitNexusState(ctx) {
554
+ const global = await ctx.stateStore.getOrCreateGlobal();
555
+ global.graph ??= {};
556
+ const detection = await detectGitNexus();
557
+ const graph2 = await inspectGitNexusGraph(ctx.projectRoot);
558
+ const state = mergeGitNexusGraphInfo(toGitNexusState(detection, global.graph.gitnexus), graph2);
559
+ global.graph.gitnexus = state;
560
+ await ctx.stateStore.writeGlobal(global);
561
+ return state;
562
+ }
563
+ function isGraphReadable(state) {
564
+ return Boolean(state.installed && state.graphExists);
565
+ }
566
+ async function writeGraphContext(path, content) {
567
+ await mkdir4(dirname5(path), { recursive: true });
568
+ await atomicWrite(path, content);
569
+ }
570
+ function renderProjectContext(options) {
571
+ return `<!-- FET:MANAGED
572
+ schemaVersion: 1
573
+ generator: graph-context
574
+ scope: project
575
+ FET:END -->
576
+
577
+ # FET GitNexus Project Graph Context
578
+
579
+ Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
580
+ Trigger: ${options.trigger}
581
+
582
+ Use this file before broad repository scans in FET/OpenSpec work. Treat it as graph-derived context: confirm concrete behavior by reading the source files it points to.
583
+
584
+ ## Graph State
585
+
586
+ - Provider: GitNexus
587
+ - Installed: ${options.state.installed ? "yes" : "no"}
588
+ - Version: ${options.state.version ?? "unknown"}
589
+ - Graph path: ${options.state.graphPath ?? ".gitnexus"}
590
+ - Graph exists: ${options.state.graphExists ? "yes" : "no"}
591
+ - Last indexed at: ${options.state.lastIndexedAt ?? "unknown"}
592
+
593
+ ## GitNexus Status
594
+
595
+ \`\`\`text
596
+ ${clip(options.status, MAX_GRAPH_OUTPUT)}
597
+ \`\`\`
598
+
599
+ ## Project Query
600
+
601
+ - Query: ${options.query}
602
+ - Goal: ${options.goal}
603
+
604
+ \`\`\`text
605
+ ${clip(options.graphOutput, MAX_GRAPH_OUTPUT)}
606
+ \`\`\`
607
+ ${renderWarnings(options.warnings)}
608
+ `;
609
+ }
610
+ function renderWorkflowContext(options) {
611
+ return `<!-- FET:MANAGED
612
+ schemaVersion: 1
613
+ generator: graph-context
614
+ scope: workflow
615
+ FET:END -->
616
+
617
+ # FET GitNexus Workflow Graph Context
618
+
619
+ Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
620
+
621
+ Read this before editing or generating OpenSpec artifacts for this workflow. Use the graph output to narrow likely files, symbols, dependencies, and impact areas. If it conflicts with OpenSpec artifacts or source code, OpenSpec artifacts and source code win.
622
+
623
+ ## Workflow
624
+
625
+ - FET command: ${options.command}
626
+ - Args: ${options.args.length ? options.args.join(" ") : "(none)"}
627
+ - Change: ${options.changeId ?? "(none)"}
628
+ - Graph path: ${options.state.graphPath ?? ".gitnexus"}
629
+ - Last indexed at: ${options.state.lastIndexedAt ?? "unknown"}
630
+
631
+ ## OpenSpec Context Used For Query
632
+
633
+ \`\`\`text
634
+ ${clip(options.sourceContext || "(no change artifacts found yet)", MAX_SOURCE_CONTEXT)}
635
+ \`\`\`
636
+
637
+ ## GitNexus Query
638
+
639
+ - Query: ${options.query}
640
+ - Goal: ${options.goal}
641
+
642
+ \`\`\`text
643
+ ${clip(options.graphOutput, MAX_GRAPH_OUTPUT)}
644
+ \`\`\`
645
+ ${options.detectChanges ? `
646
+ ## GitNexus Change Impact
647
+
648
+ \`\`\`text
649
+ ${clip(options.detectChanges, MAX_GRAPH_OUTPUT)}
650
+ \`\`\`
651
+ ` : ""}
652
+ ${renderWarnings(options.warnings)}
653
+ `;
654
+ }
655
+ function renderWarnings(warnings) {
656
+ if (!warnings.length) {
657
+ return "";
658
+ }
659
+ return `
660
+ ## Warnings
661
+
662
+ ${warnings.map((warning) => `- ${warning}`).join("\n")}
663
+ `;
664
+ }
665
+ async function collectOpenSpecContext(projectRoot, changeId) {
666
+ if (!changeId) {
667
+ return "";
668
+ }
669
+ const changeRoot = join8(projectRoot, "openspec", "changes", changeId);
670
+ const chunks = [];
671
+ for (const file of ["proposal.md", "design.md", "tasks.md", "README.md"]) {
672
+ const content = await readOptional(join8(changeRoot, file));
673
+ if (content) {
674
+ chunks.push(`## ${file}
675
+ ${content}`);
676
+ }
677
+ }
678
+ const specsRoot = join8(changeRoot, "specs");
679
+ for (const spec of await listSpecFiles(specsRoot)) {
680
+ const content = await readOptional(spec.path);
681
+ if (content) {
682
+ chunks.push(`## ${spec.label}
683
+ ${content}`);
684
+ }
685
+ }
686
+ return clip(chunks.join("\n\n"), MAX_SOURCE_CONTEXT);
687
+ }
688
+ async function listSpecFiles(specsRoot) {
689
+ try {
690
+ const capabilities = await readdir(specsRoot, { withFileTypes: true });
691
+ return capabilities.filter((entry) => entry.isDirectory()).map((entry) => ({
692
+ path: join8(specsRoot, entry.name, "spec.md"),
693
+ label: `specs/${entry.name}/spec.md`
694
+ }));
695
+ } catch {
696
+ return [];
697
+ }
698
+ }
699
+ async function readOptional(path) {
700
+ try {
701
+ return await readFile5(path, "utf8");
702
+ } catch {
703
+ return null;
704
+ }
705
+ }
706
+ function buildWorkflowQuery(options, sourceContext) {
707
+ const artifactTerms = normalizeWhitespace(sourceContext).slice(0, 1200);
708
+ return normalizeWhitespace(
709
+ [
710
+ `FET OpenSpec ${options.command}`,
711
+ options.changeId ? `change ${options.changeId}` : "",
712
+ options.args.join(" "),
713
+ artifactTerms
714
+ ].join(" ")
715
+ );
716
+ }
717
+ function workflowGoal(command) {
718
+ if (command === "apply") {
719
+ return "Find implementation files, symbols, dependencies, and likely blast radius for the current OpenSpec tasks.";
720
+ }
721
+ if (command === "verify") {
722
+ return "Find affected flows and source areas that should be checked while verifying this OpenSpec change.";
723
+ }
724
+ if (["explore", "propose", "new", "continue", "ff"].includes(command)) {
725
+ return "Find relevant modules, entry points, and existing behavior to make OpenSpec artifacts precise.";
726
+ }
727
+ return "Find relevant repository context for this FET/OpenSpec workflow command.";
728
+ }
729
+ function shouldDetectChanges(command) {
730
+ return ["apply", "verify", "sync"].includes(command);
731
+ }
732
+ function commandText(result) {
733
+ const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
734
+ return output || `exit ${result.exitCode}`;
735
+ }
736
+ function commandWarnings(results) {
737
+ return results.filter(([, result]) => result.exitCode !== 0).map(([label, result]) => `${label} exited with ${result.exitCode}: ${firstLine(commandText(result))}`);
738
+ }
739
+ function firstLine(value) {
740
+ return value.trim().split(/\r?\n/)[0]?.trim() || "no output";
741
+ }
742
+ function clip(value, max) {
743
+ if (value.length <= max) {
744
+ return value;
745
+ }
746
+ return `${value.slice(0, max)}
747
+
748
+ [truncated ${value.length - max} characters]`;
749
+ }
750
+ function normalizeWhitespace(value) {
751
+ return value.replace(/\s+/g, " ").trim();
752
+ }
753
+ function sanitizePathPart(value) {
754
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "workflow";
755
+ }
756
+
757
+ // src/commands/graph.ts
447
758
  async function graphCommand(ctx, action, args = []) {
448
759
  switch (action) {
449
760
  case "status":
@@ -498,7 +809,7 @@ async function graphDoctorCommand(ctx) {
498
809
  }
499
810
  async function graphSetupCommand(ctx) {
500
811
  let result;
501
- const handoffPath = join8(ctx.projectRoot, ".fet", "graph-setup.md");
812
+ const handoffPath = join9(ctx.projectRoot, ".fet", "graph-setup.md");
502
813
  const installCommand = process.env.FET_GITNEXUS_INSTALL_COMMAND?.trim() || null;
503
814
  await withProjectLock(ctx.projectRoot, { command: "graph setup", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
504
815
  result = await refreshGraphState(ctx, { write: false });
@@ -534,7 +845,7 @@ async function graphSetupCommand(ctx) {
534
845
  }
535
846
  async function graphHandoffCommand(ctx) {
536
847
  let result;
537
- const handoffPath = join8(ctx.projectRoot, ".fet", "graph-handoff.md");
848
+ const handoffPath = join9(ctx.projectRoot, ".fet", "graph-handoff.md");
538
849
  await withProjectLock(ctx.projectRoot, { command: "graph handoff", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
539
850
  result = await refreshGraphState(ctx, { runStatus: true, write: false });
540
851
  await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state, ctx.language));
@@ -581,11 +892,14 @@ async function graphAnalyzeCommand(ctx, mode, args) {
581
892
  });
582
893
  }
583
894
  const result = await refreshGraphState(ctx, { write: false });
895
+ const graphContext = await buildProjectGraphContext(ctx, result.state, `fet graph ${mode}`);
584
896
  const global = await ctx.stateStore.getOrCreateGlobal();
585
897
  global.graph ??= {};
586
898
  global.graph.gitnexus = {
587
899
  ...result.state,
588
- lastRefreshAt: (/* @__PURE__ */ new Date()).toISOString()
900
+ lastRefreshAt: (/* @__PURE__ */ new Date()).toISOString(),
901
+ projectContextPath: graphContext.path,
902
+ projectContextUpdatedAt: graphContext.generated ? (/* @__PURE__ */ new Date()).toISOString() : result.state.projectContextUpdatedAt ?? null
589
903
  };
590
904
  await ctx.stateStore.writeGlobal(global);
591
905
  ctx.output.result({
@@ -595,9 +909,10 @@ async function graphAnalyzeCommand(ctx, mode, args) {
595
909
  warnings: result.state.graphExists ? [] : [
596
910
  ctx.language === "en" ? "GitNexus analyze completed, but the configured graph directory was not found." : "GitNexus analyze \u5DF2\u5B8C\u6210\uFF0C\u4F46\u672A\u53D1\u73B0\u914D\u7F6E\u7684\u4EE3\u7801\u56FE\u76EE\u5F55\u3002"
597
911
  ],
598
- nextSteps: ctx.language === "en" ? ["Run fet graph status", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"] : ["\u8FD0\u884C fet graph status", "\u4F7F\u7528 .fet/graph-handoff.md \u6216\u751F\u6210\u7684 IDE \u63D0\u793A\uFF0C\u4F18\u5148\u53C2\u8003\u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587"],
912
+ nextSteps: ctx.language === "en" ? ["Run fet graph status", "Read .fet/graph-context/project.md before broad scans", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"] : ["\u8FD0\u884C fet graph status", "\u4F7F\u7528 .fet/graph-handoff.md \u6216\u751F\u6210\u7684 IDE \u63D0\u793A\uFF0C\u4F18\u5148\u53C2\u8003\u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587"],
599
913
  data: {
600
914
  gitnexus: global.graph.gitnexus,
915
+ graphContext,
601
916
  run: {
602
917
  command: run.command,
603
918
  stdout: run.stdout.trim(),
@@ -617,7 +932,7 @@ async function refreshGraphState(ctx, options = {}) {
617
932
  gitnexusStatus = await runGitNexus(["status"], { cwd: ctx.projectRoot });
618
933
  state = {
619
934
  ...state,
620
- lastStatus: firstLine(gitnexusStatus.stdout) || firstLine(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
935
+ lastStatus: firstLine2(gitnexusStatus.stdout) || firstLine2(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
621
936
  };
622
937
  }
623
938
  if (options.write ?? true) {
@@ -635,7 +950,7 @@ async function refreshGraphState(ctx, options = {}) {
635
950
  };
636
951
  }
637
952
  async function writeHandoffFile(path, content) {
638
- await mkdir4(dirname5(path), { recursive: true });
953
+ await mkdir5(dirname6(path), { recursive: true });
639
954
  await atomicWrite(path, content);
640
955
  }
641
956
  function renderGraphSetupHandoff(state, options) {
@@ -780,24 +1095,24 @@ FET:END -->
780
1095
  - \u6240\u6709\u751F\u6210\u4EA7\u7269\u4ECD\u5199\u5165\u6B63\u5E38\u7684 OpenSpec change \u76EE\u5F55\u3002
781
1096
  `;
782
1097
  }
783
- function firstLine(value) {
1098
+ function firstLine2(value) {
784
1099
  return value.trim().split(/\r?\n/)[0]?.trim() || null;
785
1100
  }
786
1101
 
787
1102
  // src/commands/init.ts
788
- import { readFile as readFile7, stat as stat4 } from "fs/promises";
789
- import { join as join11 } from "path";
1103
+ import { readFile as readFile8, stat as stat4 } from "fs/promises";
1104
+ import { join as join12 } from "path";
790
1105
 
791
1106
  // src/version.ts
792
1107
  import { existsSync, readFileSync } from "fs";
793
- import { dirname as dirname6, join as join9, parse } from "path";
1108
+ import { dirname as dirname7, join as join10, parse } from "path";
794
1109
  import { fileURLToPath } from "url";
795
1110
  var FET_VERSION = readPackageVersion();
796
1111
  function readPackageVersion() {
797
- let currentDir = dirname6(fileURLToPath(import.meta.url));
1112
+ let currentDir = dirname7(fileURLToPath(import.meta.url));
798
1113
  const root = parse(currentDir).root;
799
1114
  while (true) {
800
- const packageJsonPath = join9(currentDir, "package.json");
1115
+ const packageJsonPath = join10(currentDir, "package.json");
801
1116
  if (existsSync(packageJsonPath)) {
802
1117
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
803
1118
  if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
@@ -808,7 +1123,7 @@ function readPackageVersion() {
808
1123
  if (currentDir === root) {
809
1124
  throw new Error("\u65E0\u6CD5\u5B9A\u4F4D FET package.json");
810
1125
  }
811
- currentDir = dirname6(currentDir);
1126
+ currentDir = dirname7(currentDir);
812
1127
  }
813
1128
  }
814
1129
 
@@ -1259,18 +1574,18 @@ ${block}
1259
1574
  }
1260
1575
 
1261
1576
  // src/commands/update-context.ts
1262
- import { readFile as readFile6 } from "fs/promises";
1263
- import { join as join10 } from "path";
1577
+ import { readFile as readFile7 } from "fs/promises";
1578
+ import { join as join11 } from "path";
1264
1579
 
1265
1580
  // src/config/yaml.ts
1266
- import { readFile as readFile5 } from "fs/promises";
1581
+ import { readFile as readFile6 } from "fs/promises";
1267
1582
  import { parseDocument } from "yaml";
1268
1583
  async function mergeFetConfig(configPath, renderedFetYaml) {
1269
1584
  const fetDoc = parseDocument(renderedFetYaml);
1270
1585
  const nextFet = fetDoc.get("fet", true);
1271
1586
  let existing = "";
1272
1587
  try {
1273
- existing = await readFile5(configPath, "utf8");
1588
+ existing = await readFile6(configPath, "utf8");
1274
1589
  } catch {
1275
1590
  return renderedFetYaml;
1276
1591
  }
@@ -1294,14 +1609,14 @@ async function updateContextCommand(ctx) {
1294
1609
  }
1295
1610
  async function updateContextFiles(ctx) {
1296
1611
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
1297
- const agentsPath = join10(ctx.projectRoot, "AGENTS.md");
1298
- const configPath = join10(ctx.projectRoot, "openspec", "config.yaml");
1299
- const claudePath = join10(ctx.projectRoot, "CLAUDE.md");
1300
- const karpathyHandoffPath = join10(ctx.projectRoot, ".fet", "karpathy-guidelines.md");
1301
- const karpathyCursorPath = join10(ctx.projectRoot, ".cursor", "rules", "karpathy-guidelines.mdc");
1302
- const existingAgents = await readOptional(agentsPath);
1303
- const existingClaude = await readOptional(claudePath);
1304
- const existingKarpathyCursor = await readOptional(karpathyCursorPath);
1612
+ const agentsPath = join11(ctx.projectRoot, "AGENTS.md");
1613
+ const configPath = join11(ctx.projectRoot, "openspec", "config.yaml");
1614
+ const claudePath = join11(ctx.projectRoot, "CLAUDE.md");
1615
+ const karpathyHandoffPath = join11(ctx.projectRoot, ".fet", "karpathy-guidelines.md");
1616
+ const karpathyCursorPath = join11(ctx.projectRoot, ".cursor", "rules", "karpathy-guidelines.mdc");
1617
+ const existingAgents = await readOptional2(agentsPath);
1618
+ const existingClaude = await readOptional2(claudePath);
1619
+ const existingKarpathyCursor = await readOptional2(karpathyCursorPath);
1305
1620
  const warnings = [...scan.warnings];
1306
1621
  if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
1307
1622
  throw new FetError({
@@ -1349,9 +1664,9 @@ async function updateContextFiles(ctx) {
1349
1664
  await ctx.stateStore.writeGlobal(state);
1350
1665
  return { warnings };
1351
1666
  }
1352
- async function readOptional(path) {
1667
+ async function readOptional2(path) {
1353
1668
  try {
1354
- return await readFile6(path, "utf8");
1669
+ return await readFile7(path, "utf8");
1355
1670
  } catch {
1356
1671
  return null;
1357
1672
  }
@@ -1359,7 +1674,7 @@ async function readOptional(path) {
1359
1674
 
1360
1675
  // src/commands/init.ts
1361
1676
  async function initCommand(ctx) {
1362
- const alreadyInitialized = await exists2(join11(ctx.projectRoot, "openspec", "config.yaml"));
1677
+ const alreadyInitialized = await exists2(join12(ctx.projectRoot, "openspec", "config.yaml"));
1363
1678
  let warnings = [];
1364
1679
  await withProjectLock(ctx.projectRoot, { command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
1365
1680
  const journal = createInitJournal(ctx.fetVersion);
@@ -1411,13 +1726,13 @@ async function initCommand(ctx) {
1411
1726
  });
1412
1727
  }
1413
1728
  async function ensureGitignore(ctx) {
1414
- const gitignorePath = join11(ctx.projectRoot, ".gitignore");
1415
- const existing = await readOptional2(gitignorePath);
1729
+ const gitignorePath = join12(ctx.projectRoot, ".gitignore");
1730
+ const existing = await readOptional3(gitignorePath);
1416
1731
  await atomicWrite(gitignorePath, mergeGitignore(existing));
1417
1732
  }
1418
- async function readOptional2(path) {
1733
+ async function readOptional3(path) {
1419
1734
  try {
1420
- return await readFile7(path, "utf8");
1735
+ return await readFile8(path, "utf8");
1421
1736
  } catch {
1422
1737
  return null;
1423
1738
  }
@@ -1432,8 +1747,8 @@ async function exists2(path) {
1432
1747
  }
1433
1748
 
1434
1749
  // src/commands/proxy.ts
1435
- import { readFile as readFile10 } from "fs/promises";
1436
- import { join as join13 } from "path";
1750
+ import { readFile as readFile11 } from "fs/promises";
1751
+ import { join as join14 } from "path";
1437
1752
 
1438
1753
  // src/state/project.ts
1439
1754
  import { execFile as execFile2 } from "child_process";
@@ -1462,8 +1777,8 @@ async function git(cwd, args) {
1462
1777
  }
1463
1778
 
1464
1779
  // src/state/store.ts
1465
- import { mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
1466
- import { join as join12 } from "path";
1780
+ import { mkdir as mkdir6, readFile as readFile9 } from "fs/promises";
1781
+ import { join as join13 } from "path";
1467
1782
 
1468
1783
  // src/language.ts
1469
1784
  var DEFAULT_LANGUAGE = "zh-CN";
@@ -1581,7 +1896,7 @@ var StateStore = class {
1581
1896
  project;
1582
1897
  async readGlobal() {
1583
1898
  try {
1584
- const value = JSON.parse(await readFile8(this.globalPath(), "utf8"));
1899
+ const value = JSON.parse(await readFile9(this.globalPath(), "utf8"));
1585
1900
  assertGlobalState(value);
1586
1901
  return value;
1587
1902
  } catch (error) {
@@ -1596,13 +1911,13 @@ var StateStore = class {
1596
1911
  }
1597
1912
  async writeGlobal(state) {
1598
1913
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1599
- await mkdir5(join12(this.projectRoot, "openspec"), { recursive: true });
1914
+ await mkdir6(join13(this.projectRoot, "openspec"), { recursive: true });
1600
1915
  await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
1601
1916
  `);
1602
1917
  }
1603
1918
  async readChange(changeId) {
1604
1919
  try {
1605
- const value = JSON.parse(await readFile8(this.changePath(changeId), "utf8"));
1920
+ const value = JSON.parse(await readFile9(this.changePath(changeId), "utf8"));
1606
1921
  assertChangeState(value);
1607
1922
  return value;
1608
1923
  } catch (error) {
@@ -1617,15 +1932,15 @@ var StateStore = class {
1617
1932
  }
1618
1933
  async writeChange(state) {
1619
1934
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1620
- await mkdir5(join12(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
1935
+ await mkdir6(join13(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
1621
1936
  await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
1622
1937
  `);
1623
1938
  }
1624
1939
  globalPath() {
1625
- return join12(this.projectRoot, "openspec", "fet-state.json");
1940
+ return join13(this.projectRoot, "openspec", "fet-state.json");
1626
1941
  }
1627
1942
  changePath(changeId) {
1628
- return join12(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
1943
+ return join13(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
1629
1944
  }
1630
1945
  };
1631
1946
  function isNotFound(error) {
@@ -1633,11 +1948,11 @@ function isNotFound(error) {
1633
1948
  }
1634
1949
 
1635
1950
  // src/state/tasks.ts
1636
- import { readFile as readFile9 } from "fs/promises";
1951
+ import { readFile as readFile10 } from "fs/promises";
1637
1952
  async function readCompletedTaskIds(tasksPath) {
1638
1953
  let content;
1639
1954
  try {
1640
- content = await readFile9(tasksPath, "utf8");
1955
+ content = await readFile10(tasksPath, "utf8");
1641
1956
  } catch {
1642
1957
  return [];
1643
1958
  }
@@ -1668,71 +1983,94 @@ var phaseByCommand = {
1668
1983
  };
1669
1984
  async function proxyCommand(ctx, command, args) {
1670
1985
  const openSpecArgs = stripFetOptions(args);
1671
- await withProjectLock(
1672
- ctx.projectRoot,
1673
- { command, cwd: ctx.cwd, fetVersion: ctx.fetVersion },
1674
- async () => {
1675
- if (["sync", "archive", "bulk-archive"].includes(command)) {
1676
- await assertVerified(ctx);
1677
- }
1678
- const mapped = await mapOpenSpecCommand(ctx, command, openSpecArgs);
1679
- const targetChangeId = command === "archive" ? mapped.args[0] ?? ctx.changeId ?? null : ctx.changeId ?? null;
1680
- const changelogEntry = command === "archive" && targetChangeId ? await createChangelogEntry(ctx.projectRoot, targetChangeId) : null;
1681
- const result = await ctx.openSpec.run(mapped.command, mapped.args, { cwd: ctx.projectRoot, stdio: ctx.json ? "pipe" : "inherit" });
1682
- if (result.exitCode !== 0) {
1683
- throw new FetError({
1684
- code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
1685
- message: `OpenSpec ${command} \u6267\u884C\u5931\u8D25`,
1686
- details: result,
1687
- recoverable: true
1688
- });
1689
- }
1690
- if (changelogEntry) {
1691
- await appendChangelog(ctx.projectRoot, changelogEntry);
1692
- }
1693
- const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
1694
- const state = await ctx.stateStore.getOrCreateGlobal();
1695
- state.openChangeIds = inspection.changes;
1696
- if (command === "archive") {
1697
- if (!state.activeChangeId || state.activeChangeId === targetChangeId || !inspection.changes.includes(state.activeChangeId)) {
1698
- state.activeChangeId = null;
1699
- }
1700
- state.verifyAuthorization = null;
1701
- } else {
1702
- if (ctx.changeId && inspection.changes.includes(ctx.changeId)) {
1703
- state.activeChangeId = ctx.changeId;
1704
- } else if (state.activeChangeId && !inspection.changes.includes(state.activeChangeId)) {
1705
- state.activeChangeId = inspection.changes.length === 1 ? inspection.changes[0] ?? null : null;
1706
- } else if (!state.activeChangeId && inspection.changes.length === 1) {
1707
- state.activeChangeId = inspection.changes[0] ?? null;
1708
- }
1709
- }
1710
- await ctx.stateStore.writeGlobal(state);
1711
- const changeId = ctx.changeId ?? state.activeChangeId;
1712
- if (changeId && inspection.changes.includes(changeId)) {
1713
- const changeInspection = await ctx.openSpec.inspectChange(ctx.projectRoot, changeId);
1714
- const changeState = await ctx.stateStore.getOrCreateChange(changeId, phaseByCommand[command] ?? "propose");
1715
- changeState.currentPhase = phaseByCommand[command] ?? changeState.currentPhase;
1716
- changeState.phases[changeState.currentPhase] = {
1717
- status: "done",
1718
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1719
- };
1720
- changeState.lastOpenSpecCommand = {
1721
- command: mapped.command,
1722
- args: mapped.args,
1723
- exitCode: result.exitCode,
1724
- ranAt: (/* @__PURE__ */ new Date()).toISOString()
1725
- };
1726
- changeState.tasks.completedIds = await readCompletedTaskIds(changeInspection.tasksPath);
1727
- changeState.tasks.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
1728
- await ctx.stateStore.writeChange(changeState);
1986
+ const runState = {};
1987
+ await withProjectLock(ctx.projectRoot, { command, cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
1988
+ if (["sync", "archive", "bulk-archive"].includes(command)) {
1989
+ await assertVerified(ctx);
1990
+ }
1991
+ const mapped = await mapOpenSpecCommand(ctx, command, openSpecArgs);
1992
+ await assertOpenSpecCommandSupported(ctx, mapped.command, command);
1993
+ const mappedChangeId = extractChangeId(mapped.args);
1994
+ const targetChangeId = command === "archive" ? mapped.args[0] ?? ctx.changeId ?? mappedChangeId : ctx.changeId ?? mappedChangeId;
1995
+ runState.graphContext = await buildWorkflowGraphContext(ctx, {
1996
+ command,
1997
+ args: mapped.args,
1998
+ changeId: targetChangeId
1999
+ });
2000
+ const changelogEntry = command === "archive" && targetChangeId ? await createChangelogEntry(ctx.projectRoot, targetChangeId) : null;
2001
+ const result = await ctx.openSpec.run(mapped.command, mapped.args, { cwd: ctx.projectRoot, stdio: ctx.json ? "pipe" : "inherit" });
2002
+ if (result.exitCode !== 0) {
2003
+ throw new FetError({
2004
+ code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
2005
+ message: `OpenSpec ${command} failed.`,
2006
+ details: result,
2007
+ recoverable: true
2008
+ });
2009
+ }
2010
+ if (changelogEntry) {
2011
+ await appendChangelog(ctx.projectRoot, changelogEntry);
2012
+ }
2013
+ const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
2014
+ const state = await ctx.stateStore.getOrCreateGlobal();
2015
+ state.openChangeIds = inspection.changes;
2016
+ if (command === "archive") {
2017
+ if (!state.activeChangeId || state.activeChangeId === targetChangeId || !inspection.changes.includes(state.activeChangeId)) {
2018
+ state.activeChangeId = null;
1729
2019
  }
2020
+ state.verifyAuthorization = null;
2021
+ } else if (ctx.changeId && inspection.changes.includes(ctx.changeId)) {
2022
+ state.activeChangeId = ctx.changeId;
2023
+ } else if (state.activeChangeId && !inspection.changes.includes(state.activeChangeId)) {
2024
+ state.activeChangeId = inspection.changes.length === 1 ? inspection.changes[0] ?? null : null;
2025
+ } else if (!state.activeChangeId && inspection.changes.length === 1) {
2026
+ state.activeChangeId = inspection.changes[0] ?? null;
1730
2027
  }
1731
- );
2028
+ await ctx.stateStore.writeGlobal(state);
2029
+ const changeId = ctx.changeId ?? state.activeChangeId;
2030
+ if (changeId && inspection.changes.includes(changeId)) {
2031
+ const changeInspection = await ctx.openSpec.inspectChange(ctx.projectRoot, changeId);
2032
+ const changeState = await ctx.stateStore.getOrCreateChange(changeId, phaseByCommand[command] ?? "propose");
2033
+ changeState.currentPhase = phaseByCommand[command] ?? changeState.currentPhase;
2034
+ changeState.phases[changeState.currentPhase] = {
2035
+ status: "done",
2036
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2037
+ };
2038
+ changeState.lastOpenSpecCommand = {
2039
+ command: mapped.command,
2040
+ args: mapped.args,
2041
+ exitCode: result.exitCode,
2042
+ ranAt: (/* @__PURE__ */ new Date()).toISOString()
2043
+ };
2044
+ changeState.tasks.completedIds = await readCompletedTaskIds(changeInspection.tasksPath);
2045
+ changeState.tasks.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
2046
+ await ctx.stateStore.writeChange(changeState);
2047
+ }
2048
+ });
2049
+ const graphContext = runState.graphContext;
1732
2050
  ctx.output.result({
1733
2051
  ok: true,
1734
2052
  command,
1735
- summary: `fet ${command} \u5B8C\u6210\u3002`
2053
+ summary: `fet ${command} completed.`,
2054
+ warnings: graphContext?.warnings,
2055
+ nextSteps: graphContext?.generated && graphContext.path ? [`Read ${graphContext.path} before broad source scans or implementation decisions.`] : void 0,
2056
+ data: graphContext ? { graphContext } : void 0
2057
+ });
2058
+ }
2059
+ async function assertOpenSpecCommandSupported(ctx, openSpecCommand, fetCommand) {
2060
+ const capabilities = await ctx.openSpec.getCapabilities();
2061
+ if (capabilities.commands.includes(openSpecCommand)) {
2062
+ return;
2063
+ }
2064
+ throw new FetError({
2065
+ code: "OPENSPEC_UNSUPPORTED_VERSION" /* OpenSpecUnsupportedVersion */,
2066
+ message: `OpenSpec CLI ${capabilities.version} does not expose command "${openSpecCommand}" required by "fet ${fetCommand}". FET will not substitute another workflow command automatically.`,
2067
+ details: {
2068
+ openSpecVersion: capabilities.version,
2069
+ requiredCommand: openSpecCommand,
2070
+ availableCommands: capabilities.commands,
2071
+ supported: capabilities.supported
2072
+ },
2073
+ suggestedCommand: "Upgrade OpenSpec to a version that supports this command, then rerun FET. Try: npm install -g @fission-ai/openspec@latest && fet doctor. If your OpenSpec version intentionally removed this command, pause and choose a compatible FET workflow instead of running ff automatically."
1736
2074
  });
1737
2075
  }
1738
2076
  async function createChangelogEntry(projectRoot, changeId) {
@@ -1742,10 +2080,12 @@ async function createChangelogEntry(projectRoot, changeId) {
1742
2080
  };
1743
2081
  }
1744
2082
  async function appendChangelog(projectRoot, entry) {
1745
- const changelogPath = join13(projectRoot, "CHANGELOG.md");
1746
- const existing = await readOptional3(changelogPath);
2083
+ const changelogPath = join14(projectRoot, "CHANGELOG.md");
2084
+ const existing = await readOptional4(changelogPath);
2085
+ const legacyContentLabel = "\u66F4\u65B0\u5185\u5BB9";
1747
2086
  const block = `updateTime: ${entry.updateTime}
1748
- \u66F4\u65B0\u5185\u5BB9:${entry.content}
2087
+ changeRequirement:${entry.content}
2088
+ ${legacyContentLabel}:${entry.content}
1749
2089
  `;
1750
2090
  const next = existing?.trimEnd() ? `${existing.trimEnd()}
1751
2091
 
@@ -1753,12 +2093,12 @@ ${block}` : block;
1753
2093
  await atomicWrite(changelogPath, next);
1754
2094
  }
1755
2095
  async function readChangeRequirement(projectRoot, changeId) {
1756
- const changeRoot = join13(projectRoot, "openspec", "changes", changeId);
1757
- const proposal = await readOptional3(join13(changeRoot, "proposal.md"));
2096
+ const changeRoot = join14(projectRoot, "openspec", "changes", changeId);
2097
+ const proposal = await readOptional4(join14(changeRoot, "proposal.md"));
1758
2098
  if (proposal) {
1759
2099
  return summarizeMarkdown(proposal);
1760
2100
  }
1761
- const readme = await readOptional3(join13(changeRoot, "README.md"));
2101
+ const readme = await readOptional4(join14(changeRoot, "README.md"));
1762
2102
  if (readme) {
1763
2103
  return summarizeMarkdown(readme);
1764
2104
  }
@@ -1766,11 +2106,11 @@ async function readChangeRequirement(projectRoot, changeId) {
1766
2106
  }
1767
2107
  function summarizeMarkdown(content) {
1768
2108
  const normalized = content.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("<!--") && !line.startsWith("---")).join(" ");
1769
- return normalized || "\u672A\u63D0\u4F9B\u53D8\u66F4\u9700\u6C42";
2109
+ return normalized || "No change requirement found.";
1770
2110
  }
1771
- async function readOptional3(path) {
2111
+ async function readOptional4(path) {
1772
2112
  try {
1773
- return await readFile10(path, "utf8");
2113
+ return await readFile11(path, "utf8");
1774
2114
  } catch {
1775
2115
  return null;
1776
2116
  }
@@ -1792,7 +2132,7 @@ async function passthroughCommand(ctx, command, args) {
1792
2132
  if (result.exitCode !== 0) {
1793
2133
  throw new FetError({
1794
2134
  code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
1795
- message: `OpenSpec ${command} \u6267\u884C\u5931\u8D25`,
2135
+ message: `OpenSpec ${command} failed.`,
1796
2136
  details: result
1797
2137
  });
1798
2138
  }
@@ -1834,18 +2174,6 @@ async function mapOpenSpecCommand(ctx, command, args) {
1834
2174
  return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
1835
2175
  case "archive":
1836
2176
  return { command: "archive", args: [ctx.changeId ?? args[0] ?? await requireChangeId(ctx), ...args.slice(ctx.changeId ? 0 : 1)] };
1837
- /*
1838
- case "bulk-archive":
1839
- throw new FetError({
1840
- code: ErrorCode.InvalidArguments,
1841
- message: "OpenSpec 1.2.0 不提供 bulk-archive 顶层命令",
1842
- suggestedCommand: "逐个执行 fet archive --change <change-id>"
1843
- });
1844
- case "explore":
1845
- return { command: "explore", args: ctx.changeId ? ["--change", ctx.changeId, ...args] : args };
1846
- case "onboard":
1847
- return { command: "instructions", args: [] };
1848
- */
1849
2177
  default:
1850
2178
  return { command, args };
1851
2179
  }
@@ -1865,6 +2193,18 @@ async function withDefaultChange(ctx, args, allowWithArgs = false) {
1865
2193
  }
1866
2194
  return ["--change", await requireChangeId(ctx), ...args];
1867
2195
  }
2196
+ function extractChangeId(args) {
2197
+ for (let index = 0; index < args.length; index += 1) {
2198
+ const arg = args[index];
2199
+ if (arg === "--change") {
2200
+ return args[index + 1] ?? null;
2201
+ }
2202
+ if (arg?.startsWith("--change=")) {
2203
+ return arg.slice("--change=".length) || null;
2204
+ }
2205
+ }
2206
+ return null;
2207
+ }
1868
2208
  async function requireChangeId(ctx) {
1869
2209
  if (ctx.changeId) {
1870
2210
  return ctx.changeId;
@@ -1879,9 +2219,9 @@ async function requireChangeId(ctx) {
1879
2219
  }
1880
2220
  throw new FetError({
1881
2221
  code: "INVALID_ARGUMENTS" /* InvalidArguments */,
1882
- message: "\u8BE5\u547D\u4EE4\u9700\u8981\u660E\u786E\u7684 change",
2222
+ message: "No unambiguous OpenSpec change id was found.",
1883
2223
  details: { openChangeIds: inspection.changes },
1884
- suggestedCommand: "\u6DFB\u52A0 --change <change-id>"
2224
+ suggestedCommand: "Pass --change <change-id>."
1885
2225
  });
1886
2226
  }
1887
2227
  async function assertVerified(ctx) {
@@ -1890,7 +2230,7 @@ async function assertVerified(ctx) {
1890
2230
  if (!changeId) {
1891
2231
  throw new FetError({
1892
2232
  code: "INVALID_ARGUMENTS" /* InvalidArguments */,
1893
- message: "\u672A\u6307\u5B9A change\uFF0C\u65E0\u6CD5\u68C0\u67E5 verify \u72B6\u6001",
2233
+ message: "A change id is required before this command can check FET verification.",
1894
2234
  suggestedCommand: "fet verify --done --change <change-id>"
1895
2235
  });
1896
2236
  }
@@ -1899,7 +2239,7 @@ async function assertVerified(ctx) {
1899
2239
  if (!inspection.changes.includes(changeId)) {
1900
2240
  throw new FetError({
1901
2241
  code: "INVALID_ARGUMENTS" /* InvalidArguments */,
1902
- message: "\u6307\u5B9A\u7684 change \u4E0D\u5B58\u5728\u6216\u5DF2\u5F52\u6863",
2242
+ message: "The selected change does not exist in openspec/changes.",
1903
2243
  details: { changeId, openChangeIds: inspection.changes },
1904
2244
  suggestedCommand: "fet doctor"
1905
2245
  });
@@ -1907,7 +2247,7 @@ async function assertVerified(ctx) {
1907
2247
  if (change?.manualVerify?.status !== "declared_done") {
1908
2248
  throw new FetError({
1909
2249
  code: "STATE_CORRUPTED" /* StateCorrupted */,
1910
- message: "\u5F53\u524D change \u5C1A\u672A\u901A\u8FC7 FET verify",
2250
+ message: "This change has not been marked verified by FET.",
1911
2251
  details: { changeId },
1912
2252
  suggestedCommand: `fet verify --change ${changeId}`
1913
2253
  });
@@ -1916,8 +2256,8 @@ async function assertVerified(ctx) {
1916
2256
 
1917
2257
  // src/commands/verify.ts
1918
2258
  import { createHash } from "crypto";
1919
- import { mkdir as mkdir6, readFile as readFile11, stat as stat5 } from "fs/promises";
1920
- import { join as join14 } from "path";
2259
+ import { mkdir as mkdir7, readFile as readFile12, stat as stat5 } from "fs/promises";
2260
+ import { join as join15 } from "path";
1921
2261
  async function verifyCommand(ctx, options) {
1922
2262
  if (options.auto) {
1923
2263
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
@@ -1984,9 +2324,9 @@ async function verifyCommand(ctx, options) {
1984
2324
  async function writeInstructions(ctx, changeId) {
1985
2325
  await assertChangeExists(ctx, changeId);
1986
2326
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1987
- const dir = join14(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
1988
- const instructionsPath = join14(dir, "verify-instructions.md");
1989
- await mkdir6(dir, { recursive: true });
2327
+ const dir = join15(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
2328
+ const instructionsPath = join15(dir, "verify-instructions.md");
2329
+ await mkdir7(dir, { recursive: true });
1990
2330
  await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
1991
2331
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
1992
2332
  state.currentPhase = "verify";
@@ -2002,7 +2342,7 @@ async function writeInstructions(ctx, changeId) {
2002
2342
  async function markDone(ctx, changeId) {
2003
2343
  await assertChangeExists(ctx, changeId);
2004
2344
  const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
2005
- const instructionsPath = join14(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
2345
+ const instructionsPath = join15(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
2006
2346
  const instructions = await readInstructions(instructionsPath, changeId);
2007
2347
  const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
2008
2348
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
@@ -2038,7 +2378,7 @@ async function assertChangeExists(ctx, changeId) {
2038
2378
  async function readInstructions(path, changeId) {
2039
2379
  try {
2040
2380
  await stat5(path);
2041
- const content = await readFile11(path, "utf8");
2381
+ const content = await readFile12(path, "utf8");
2042
2382
  const fileChangeId = readFrontMatterValue(content, "changeId");
2043
2383
  if (fileChangeId !== changeId) {
2044
2384
  throw new FetError({
@@ -2176,9 +2516,9 @@ function renderIdeModelPolicy(command, language = "zh-CN") {
2176
2516
  import { resolve } from "path";
2177
2517
 
2178
2518
  // src/adapters/codex/index.ts
2179
- import { mkdir as mkdir7, readFile as readFile12, stat as stat6 } from "fs/promises";
2519
+ import { mkdir as mkdir8, readFile as readFile13, stat as stat6 } from "fs/promises";
2180
2520
  import { homedir } from "os";
2181
- import { dirname as dirname7, join as join15 } from "path";
2521
+ import { dirname as dirname8, join as join16 } from "path";
2182
2522
 
2183
2523
  // src/adapters/commands.ts
2184
2524
  var FET_WORKFLOW_COMMANDS = [
@@ -2489,15 +2829,15 @@ After the command completes, report the GitNexus state, generated handoff files,
2489
2829
  `;
2490
2830
  }
2491
2831
  function renderSlashPrompt(command, language) {
2832
+ if (command === "ff" || command === "propose") {
2833
+ return renderFastForwardSlashPrompt(command, language);
2834
+ }
2492
2835
  if (language !== "en") {
2493
2836
  return renderSlashPromptZh(command);
2494
2837
  }
2495
2838
  if (command === "continue") {
2496
2839
  return renderContinueSlashPrompt(language);
2497
2840
  }
2498
- if (command === "ff" || command === "propose") {
2499
- return renderFastForwardSlashPrompt(command, language);
2500
- }
2501
2841
  if (command === "explore") {
2502
2842
  return renderExploreSlashPrompt(language);
2503
2843
  }
@@ -3097,11 +3437,11 @@ Guardrails:
3097
3437
  );
3098
3438
  }
3099
3439
  function renderFastForwardSlashPrompt(command, language) {
3100
- const title = command === "propose" ? "Propose a new FET/OpenSpec change" : "Fast-forward FET/OpenSpec artifact creation";
3440
+ const title = language === "en" ? command === "propose" ? "Propose a new FET/OpenSpec change" : "Fast-forward FET/OpenSpec artifact creation" : command === "propose" ? "\u521B\u5EFA\u5E76\u8865\u9F50 FET/OpenSpec \u63D0\u6848\u4EA7\u7269" : "\u5FEB\u901F\u751F\u6210 FET/OpenSpec \u6240\u9700\u4EA7\u7269";
3101
3441
  const commandLine = command === "propose" ? "fet propose <change-id-or-description>" : "fet ff --change <change-id>";
3102
3442
  return renderManagedSlashPrompt(
3103
3443
  `fet ${command} [...args]`,
3104
- command === "propose" ? "Create a change and generate required OpenSpec artifacts" : "Generate required OpenSpec artifacts for a change",
3444
+ language === "en" ? command === "propose" ? "Create a change and generate required OpenSpec artifacts" : "Generate required OpenSpec artifacts for a change" : command === "propose" ? "\u521B\u5EFA\u5E76\u8865\u9F50 FET/OpenSpec \u63D0\u6848\u4EA7\u7269" : "\u5FEB\u901F\u751F\u6210 FET/OpenSpec \u6240\u9700\u4EA7\u7269",
3105
3445
  `${title}.
3106
3446
 
3107
3447
  Input after the slash command may be a change id or a description of what the user wants to build. For ff, it may be omitted when the active OpenSpec change is unambiguous.
@@ -3116,6 +3456,7 @@ Steps:
3116
3456
  \`\`\`
3117
3457
  4. Follow the native output. If it asks for clarification, ask the user rather than inventing details.
3118
3458
  5. If the native output includes artifact paths or templates to write, create only those files and preserve OpenSpec structure.
3459
+ 6. If FET reports that the OpenSpec CLI does not expose the requested command, stop immediately. Do not run \`fet ff\`, \`openspec ff\`, \`openspec change\`, or any alternative workflow command unless the user explicitly chooses that fallback after seeing the error.
3119
3460
 
3120
3461
  Artifact rules:
3121
3462
  - Follow the instruction field from OpenSpec/FET for each artifact.
@@ -3127,7 +3468,11 @@ Output:
3127
3468
  - Change id and location.
3128
3469
  - Artifacts created.
3129
3470
  - Current status.
3130
- - Next recommended command, usually /prompts:fet-apply <change-id>.`,
3471
+ - Next recommended command, usually /prompts:fet-apply <change-id>.
3472
+
3473
+ Guardrails:
3474
+ - Do not substitute one FET/OpenSpec workflow command for another after a command-not-found or unsupported-version error.
3475
+ - If OpenSpec appears outdated or incompatible, report the detected version and suggest \`npm install -g @fission-ai/openspec@latest\` or \`fet doctor\`, then wait for the user's decision.`,
3131
3476
  void 0,
3132
3477
  language
3133
3478
  );
@@ -3165,7 +3510,7 @@ var CodexAdapter = class {
3165
3510
  adapterVersion = 1;
3166
3511
  async detect(projectRoot) {
3167
3512
  return {
3168
- detected: await exists3(join15(projectRoot, ".codex")) || await exists3(join15(projectRoot, "AGENTS.md")),
3513
+ detected: await exists3(join16(projectRoot, ".codex")) || await exists3(join16(projectRoot, "AGENTS.md")),
3169
3514
  reason: "Codex adapter is available for projects that use AGENTS.md"
3170
3515
  };
3171
3516
  }
@@ -3204,7 +3549,7 @@ var CodexAdapter = class {
3204
3549
  if (existing && !existing.includes("FET:MANAGED") && force) {
3205
3550
  await createBackup(target);
3206
3551
  }
3207
- await mkdir7(dirname7(target), { recursive: true });
3552
+ await mkdir8(dirname8(target), { recursive: true });
3208
3553
  await atomicWrite(target, file.content);
3209
3554
  written.push(displayPath);
3210
3555
  }
@@ -3231,9 +3576,9 @@ var CodexAdapter = class {
3231
3576
  };
3232
3577
  function resolveTarget(projectRoot, file) {
3233
3578
  if (file.root === "codex-home") {
3234
- return join15(resolveCodexHome(), file.path);
3579
+ return join16(resolveCodexHome(), file.path);
3235
3580
  }
3236
- return join15(projectRoot, file.path);
3581
+ return join16(projectRoot, file.path);
3237
3582
  }
3238
3583
  function displayPathFor(file) {
3239
3584
  if (file.root === "codex-home") {
@@ -3242,11 +3587,11 @@ function displayPathFor(file) {
3242
3587
  return file.path;
3243
3588
  }
3244
3589
  function resolveCodexHome() {
3245
- return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join15(homedir(), ".codex");
3590
+ return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join16(homedir(), ".codex");
3246
3591
  }
3247
3592
  async function readExisting(path) {
3248
3593
  try {
3249
- return await readFile12(path, "utf8");
3594
+ return await readFile13(path, "utf8");
3250
3595
  } catch {
3251
3596
  return null;
3252
3597
  }
@@ -3261,8 +3606,8 @@ async function exists3(path) {
3261
3606
  }
3262
3607
 
3263
3608
  // src/adapters/cursor/index.ts
3264
- import { mkdir as mkdir8, readFile as readFile13, stat as stat7 } from "fs/promises";
3265
- import { dirname as dirname8, join as join16 } from "path";
3609
+ import { mkdir as mkdir9, readFile as readFile14, stat as stat7 } from "fs/promises";
3610
+ import { dirname as dirname9, join as join17 } from "path";
3266
3611
 
3267
3612
  // src/adapters/cursor/templates.ts
3268
3613
  function cursorSkillFiles(language = DEFAULT_LANGUAGE) {
@@ -3396,7 +3741,7 @@ var CursorAdapter = class {
3396
3741
  adapterVersion = 1;
3397
3742
  async detect(projectRoot) {
3398
3743
  return {
3399
- detected: await exists4(join16(projectRoot, ".cursor")),
3744
+ detected: await exists4(join17(projectRoot, ".cursor")),
3400
3745
  reason: "Cursor adapter is available for any project"
3401
3746
  };
3402
3747
  }
@@ -3413,7 +3758,7 @@ var CursorAdapter = class {
3413
3758
  const written = [];
3414
3759
  const skipped = [];
3415
3760
  for (const file of plan.files) {
3416
- const target = join16(projectRoot, file.path);
3761
+ const target = join17(projectRoot, file.path);
3417
3762
  const existing = await readExisting2(target);
3418
3763
  if (existing && !existing.includes("FET:MANAGED") && !force) {
3419
3764
  throw new FetError({
@@ -3426,7 +3771,7 @@ var CursorAdapter = class {
3426
3771
  if (existing && !existing.includes("FET:MANAGED") && force) {
3427
3772
  await createBackup(target);
3428
3773
  }
3429
- await mkdir8(dirname8(target), { recursive: true });
3774
+ await mkdir9(dirname9(target), { recursive: true });
3430
3775
  await atomicWrite(target, file.content);
3431
3776
  written.push(file.path);
3432
3777
  }
@@ -3436,7 +3781,7 @@ var CursorAdapter = class {
3436
3781
  const plan = await this.planInstall(projectRoot);
3437
3782
  const checks = [];
3438
3783
  for (const file of plan.files) {
3439
- const target = join16(projectRoot, file.path);
3784
+ const target = join17(projectRoot, file.path);
3440
3785
  const content = await readExisting2(target);
3441
3786
  const managed = Boolean(content?.includes("FET:MANAGED"));
3442
3787
  const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
@@ -3452,7 +3797,7 @@ var CursorAdapter = class {
3452
3797
  };
3453
3798
  async function readExisting2(path) {
3454
3799
  try {
3455
- return await readFile13(path, "utf8");
3800
+ return await readFile14(path, "utf8");
3456
3801
  } catch {
3457
3802
  return null;
3458
3803
  }
@@ -3471,13 +3816,13 @@ import { execFile as execFile4 } from "child_process";
3471
3816
  import { promisify as promisify4 } from "util";
3472
3817
 
3473
3818
  // src/openspec/inspector.ts
3474
- import { readdir, stat as stat8 } from "fs/promises";
3475
- import { join as join17 } from "path";
3819
+ import { readdir as readdir2, stat as stat8 } from "fs/promises";
3820
+ import { join as join18 } from "path";
3476
3821
  async function inspectOpenSpecProject(projectRoot) {
3477
- const openspecPath = join17(projectRoot, "openspec");
3478
- const changesPath = join17(openspecPath, "changes");
3479
- const legacyArchivePath = join17(openspecPath, "archive");
3480
- const changesArchivePath = join17(changesPath, "archive");
3822
+ const openspecPath = join18(projectRoot, "openspec");
3823
+ const changesPath = join18(openspecPath, "changes");
3824
+ const legacyArchivePath = join18(openspecPath, "archive");
3825
+ const changesArchivePath = join18(changesPath, "archive");
3481
3826
  return {
3482
3827
  exists: await exists5(openspecPath),
3483
3828
  changes: await listDirectories(changesPath, { exclude: ["archive"] }),
@@ -3485,13 +3830,13 @@ async function inspectOpenSpecProject(projectRoot) {
3485
3830
  };
3486
3831
  }
3487
3832
  async function inspectOpenSpecChange(projectRoot, changeId) {
3488
- const changePath = join17(projectRoot, "openspec", "changes", changeId);
3489
- const tasksPath = join17(changePath, "tasks.md");
3490
- const specsPath = join17(changePath, "specs");
3833
+ const changePath = join18(projectRoot, "openspec", "changes", changeId);
3834
+ const tasksPath = join18(changePath, "tasks.md");
3835
+ const specsPath = join18(changePath, "specs");
3491
3836
  return {
3492
3837
  changeId,
3493
3838
  exists: await exists5(changePath),
3494
- hasProposal: await exists5(join17(changePath, "proposal.md")),
3839
+ hasProposal: await exists5(join18(changePath, "proposal.md")),
3495
3840
  hasTasks: await exists5(tasksPath),
3496
3841
  hasSpecs: await exists5(specsPath),
3497
3842
  tasksPath,
@@ -3500,7 +3845,7 @@ async function inspectOpenSpecChange(projectRoot, changeId) {
3500
3845
  }
3501
3846
  async function listDirectories(path, options = {}) {
3502
3847
  try {
3503
- const entries = await readdir(path, { withFileTypes: true });
3848
+ const entries = await readdir2(path, { withFileTypes: true });
3504
3849
  const excluded = new Set(options.exclude ?? []);
3505
3850
  return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
3506
3851
  } catch {
@@ -3533,7 +3878,7 @@ async function findExecutable() {
3533
3878
  const command = process.platform === "win32" ? "where.exe" : "which";
3534
3879
  try {
3535
3880
  const { stdout } = await exec(command, ["openspec"]);
3536
- const first = stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
3881
+ const first = stdout.split(/\r?\n/).map((line) => line.trim()).sort((left, right) => executablePreference(left) - executablePreference(right)).find(Boolean);
3537
3882
  if (first) {
3538
3883
  return first;
3539
3884
  }
@@ -3550,6 +3895,12 @@ async function findExecutable() {
3550
3895
  });
3551
3896
  }
3552
3897
  }
3898
+ function executablePreference(path) {
3899
+ if (process.platform === "win32" && path.toLowerCase().endsWith(".cmd")) {
3900
+ return 0;
3901
+ }
3902
+ return 1;
3903
+ }
3553
3904
  async function readVersion(executablePath) {
3554
3905
  const command = executablePath === "npx openspec" ? "npx" : executablePath;
3555
3906
  const args = executablePath === "npx openspec" ? ["openspec", "--version"] : ["--version"];
@@ -3664,16 +4015,19 @@ function parseCommands(help) {
3664
4015
  "bulk-archive",
3665
4016
  "onboard"
3666
4017
  ];
3667
- return known.filter((command) => help.includes(command));
4018
+ return known.filter((command) => new RegExp(`\\b${escapeRegExp(command)}\\b`).test(help));
4019
+ }
4020
+ function escapeRegExp(value) {
4021
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3668
4022
  }
3669
4023
 
3670
4024
  // src/scanner/package.ts
3671
- import { readFile as readFile14, stat as stat9 } from "fs/promises";
3672
- import { join as join18 } from "path";
4025
+ import { readFile as readFile15, stat as stat9 } from "fs/promises";
4026
+ import { join as join19 } from "path";
3673
4027
  import { parse as parse2 } from "yaml";
3674
4028
  async function readPackageJson(projectRoot) {
3675
4029
  try {
3676
- return JSON.parse(await readFile14(join18(projectRoot, "package.json"), "utf8"));
4030
+ return JSON.parse(await readFile15(join19(projectRoot, "package.json"), "utf8"));
3677
4031
  } catch {
3678
4032
  return null;
3679
4033
  }
@@ -3739,7 +4093,7 @@ function detectFramework(pkg) {
3739
4093
  }
3740
4094
  async function detectLanguage(projectRoot, pkg) {
3741
4095
  const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
3742
- if (deps.typescript || await exists6(join18(projectRoot, "tsconfig.json"))) {
4096
+ if (deps.typescript || await exists6(join19(projectRoot, "tsconfig.json"))) {
3743
4097
  return "typescript";
3744
4098
  }
3745
4099
  return "javascript";
@@ -3754,7 +4108,7 @@ async function detectWorkspaces(projectRoot, pkg) {
3754
4108
  return packageWorkspaces;
3755
4109
  }
3756
4110
  try {
3757
- const workspace = parse2(await readFile14(join18(projectRoot, "pnpm-workspace.yaml"), "utf8"));
4111
+ const workspace = parse2(await readFile15(join19(projectRoot, "pnpm-workspace.yaml"), "utf8"));
3758
4112
  return (workspace?.packages ?? []).map((path) => ({
3759
4113
  name: path,
3760
4114
  path,
@@ -3774,7 +4128,7 @@ async function detectLockManagers(projectRoot) {
3774
4128
  ];
3775
4129
  const found = [];
3776
4130
  for (const [file, manager] of lockFiles) {
3777
- if (await exists6(join18(projectRoot, file))) {
4131
+ if (await exists6(join19(projectRoot, file))) {
3778
4132
  found.push(manager);
3779
4133
  }
3780
4134
  }
@@ -3799,13 +4153,13 @@ async function exists6(path) {
3799
4153
  }
3800
4154
 
3801
4155
  // src/scanner/routes.ts
3802
- import { readdir as readdir2, stat as stat10 } from "fs/promises";
3803
- import { join as join19, relative, sep } from "path";
4156
+ import { readdir as readdir3, stat as stat10 } from "fs/promises";
4157
+ import { join as join20, relative, sep } from "path";
3804
4158
  async function scanRoutes(projectRoot) {
3805
4159
  const candidates = ["src/routes", "src/pages", "app", "pages"];
3806
4160
  const routes = [];
3807
4161
  for (const candidate of candidates) {
3808
- const root = join19(projectRoot, candidate);
4162
+ const root = join20(projectRoot, candidate);
3809
4163
  if (!await exists7(root)) {
3810
4164
  continue;
3811
4165
  }
@@ -3830,10 +4184,10 @@ function inferRoutePath(relativePath) {
3830
4184
  return `/${withoutIndex}`.replace(/\/+/g, "/");
3831
4185
  }
3832
4186
  async function listFiles(root) {
3833
- const entries = await readdir2(root, { withFileTypes: true });
4187
+ const entries = await readdir3(root, { withFileTypes: true });
3834
4188
  const files = [];
3835
4189
  for (const entry of entries) {
3836
- const path = join19(root, entry.name);
4190
+ const path = join20(root, entry.name);
3837
4191
  if (entry.isDirectory()) {
3838
4192
  files.push(...await listFiles(path));
3839
4193
  } else {