@nick848/fet 1.0.7 → 1.0.9

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);
@@ -262,7 +268,7 @@ async function doctorCommand(ctx, options = {}) {
262
268
  checks.push(await checkState(ctx));
263
269
  checks.push(await checkFile("agents", join6(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
264
270
  checks.push(await checkFile("config", join6(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
265
- checks.push(await checkPlaceholders(ctx.projectRoot));
271
+ checks.push(await checkPlaceholders(ctx));
266
272
  checks.push(await checkGitNexus(ctx));
267
273
  for (const adapter of ctx.toolAdapters) {
268
274
  checks.push(...await adapter.doctor(ctx.projectRoot));
@@ -299,12 +305,12 @@ async function checkGitNexus(ctx) {
299
305
  return state.installed ? {
300
306
  id: "gitnexus",
301
307
  status: "pass",
302
- message: `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"}), graph ${state.graphExists ? "found" : "not found"}`
308
+ message: ctx.language === "en" ? `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"}), graph ${state.graphExists ? "found" : "not found"}` : `\u68C0\u6D4B\u5230 GitNexus\uFF1A${state.executablePath ?? "gitnexus"}\uFF08${state.version ?? "unknown"}\uFF09\uFF0C\u4EE3\u7801\u56FE${state.graphExists ? "\u5DF2\u627E\u5230" : "\u672A\u627E\u5230"}`
303
309
  } : {
304
310
  id: "gitnexus",
305
311
  status: "warn",
306
- message: "Optional GitNexus code graph support is not installed",
307
- suggestedCommand: "Install GitNexus later if you want OpenSpec artifacts to prefer a repository graph"
312
+ message: ctx.language === "en" ? "Optional GitNexus code graph support is not installed" : "\u5C1A\u672A\u5B89\u88C5\u53EF\u9009\u7684 GitNexus \u4EE3\u7801\u56FE\u652F\u6301",
313
+ suggestedCommand: ctx.language === "en" ? "Install GitNexus later if you want OpenSpec artifacts to prefer a repository graph" : "\u5982\u679C\u5E0C\u671B OpenSpec \u4EA7\u7269\u4F18\u5148\u53C2\u8003\u4ED3\u5E93\u4EE3\u7801\u56FE\uFF0C\u53EF\u4EE5\u7A0D\u540E\u5B89\u88C5 GitNexus"
308
314
  };
309
315
  }
310
316
  async function checkOpenSpec(ctx) {
@@ -326,18 +332,27 @@ async function checkState(ctx) {
326
332
  async function checkFile(id, path, missing, suggestedCommand) {
327
333
  return await exists(path) ? { id, status: "pass", message: `${id} \u5B58\u5728` } : { id, status: "warn", message: missing, suggestedCommand };
328
334
  }
329
- async function checkPlaceholders(projectRoot) {
335
+ async function checkPlaceholders(ctx) {
330
336
  try {
331
- await readFile4(join6(projectRoot, "AGENTS.md"), "utf8");
332
- const count2 = await countAgentsLlmPlaceholders(projectRoot);
337
+ await readFile4(join6(ctx.projectRoot, "AGENTS.md"), "utf8");
338
+ const count2 = await countAgentsLlmPlaceholders(ctx.projectRoot);
333
339
  return count2 ? {
334
340
  id: "context-placeholders",
335
341
  status: "warn",
336
- message: `AGENTS.md has ${count2} LLM placeholder(s)`,
342
+ message: ctx.language === "en" ? `AGENTS.md has ${count2} LLM placeholder(s)` : `AGENTS.md \u4ECD\u6709 ${count2} \u4E2A LLM \u5360\u4F4D\u7B26`,
337
343
  suggestedCommand: "fet fill-context"
338
- } : { id: "context-placeholders", status: "pass", message: "AGENTS.md placeholders resolved" };
344
+ } : {
345
+ id: "context-placeholders",
346
+ status: "pass",
347
+ message: ctx.language === "en" ? "AGENTS.md placeholders resolved" : "AGENTS.md \u5360\u4F4D\u7B26\u5DF2\u5904\u7406"
348
+ };
339
349
  } catch {
340
- return { id: "context-placeholders", status: "warn", message: "AGENTS.md missing", suggestedCommand: "fet update-context" };
350
+ return {
351
+ id: "context-placeholders",
352
+ status: "warn",
353
+ message: ctx.language === "en" ? "AGENTS.md missing" : "AGENTS.md \u7F3A\u5931",
354
+ suggestedCommand: "fet update-context"
355
+ };
341
356
  }
342
357
  }
343
358
  async function exists(path) {
@@ -433,8 +448,313 @@ FET:END -->
433
448
  }
434
449
 
435
450
  // src/commands/graph.ts
436
- 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";
437
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
438
758
  async function graphCommand(ctx, action, args = []) {
439
759
  switch (action) {
440
760
  case "status":
@@ -459,38 +779,41 @@ async function graphCommand(ctx, action, args = []) {
459
779
  }
460
780
  async function graphStatusCommand(ctx) {
461
781
  const result = await refreshGraphState(ctx, { runStatus: true });
462
- const warnings = result.state.installed ? [] : ["GitNexus is not installed. Run fet graph setup for installation handoff instructions."];
782
+ const warnings = result.state.installed ? [] : [
783
+ ctx.language === "en" ? "GitNexus is not installed. Run fet graph setup for installation handoff instructions." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002\u8FD0\u884C fet graph setup \u83B7\u53D6\u5B89\u88C5\u4EA4\u63A5\u8BF4\u660E\u3002"
784
+ ];
463
785
  ctx.output.result({
464
786
  ok: true,
465
787
  command: "graph status",
466
- summary: result.state.installed ? `GitNexus graph status checked. Graph ${result.state.graphExists ? "exists" : "does not exist"} at ${result.state.graphPath ?? ".gitnexus"}.` : "GitNexus is not installed. Graph support remains optional.",
788
+ summary: ctx.language === "en" ? result.state.installed ? `GitNexus graph status checked. Graph ${result.state.graphExists ? "exists" : "does not exist"} at ${result.state.graphPath ?? ".gitnexus"}.` : "GitNexus is not installed. Graph support remains optional." : result.state.installed ? `\u5DF2\u68C0\u67E5 GitNexus \u4EE3\u7801\u56FE\u72B6\u6001\u3002\u4EE3\u7801\u56FE${result.state.graphExists ? "\u5B58\u5728" : "\u4E0D\u5B58\u5728"}\uFF0C\u8DEF\u5F84\u4E3A ${result.state.graphPath ?? ".gitnexus"}\u3002` : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002\u4EE3\u7801\u56FE\u80FD\u529B\u4FDD\u6301\u53EF\u9009\u3002",
467
789
  warnings,
468
- nextSteps: result.state.installed && !result.state.graphExists ? ["Run fet graph init to build the first GitNexus graph"] : void 0,
790
+ nextSteps: result.state.installed && !result.state.graphExists ? [ctx.language === "en" ? "Run fet graph init to build the first GitNexus graph" : "\u8FD0\u884C fet graph init \u751F\u6210\u7B2C\u4E00\u4EFD GitNexus \u4EE3\u7801\u56FE"] : void 0,
469
791
  data: result
470
792
  });
471
793
  }
472
794
  async function graphDoctorCommand(ctx) {
473
795
  const result = await refreshGraphState(ctx, { runStatus: true });
474
796
  const warnings = [
475
- ...!result.state.installed ? ["GitNexus is not installed."] : [],
476
- ...result.state.installed && !result.state.graphExists ? ["GitNexus is installed but no graph directory was found."] : [],
477
- ...!result.state.handoffPath ? ["Graph handoff instructions have not been generated."] : []
797
+ ...!result.state.installed ? [ctx.language === "en" ? "GitNexus is not installed." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002"] : [],
798
+ ...result.state.installed && !result.state.graphExists ? [ctx.language === "en" ? "GitNexus is installed but no graph directory was found." : "\u5DF2\u5B89\u88C5 GitNexus\uFF0C\u4F46\u672A\u53D1\u73B0\u4EE3\u7801\u56FE\u76EE\u5F55\u3002"] : [],
799
+ ...!result.state.handoffPath ? [ctx.language === "en" ? "Graph handoff instructions have not been generated." : "\u5C1A\u672A\u751F\u6210\u4EE3\u7801\u56FE\u4F7F\u7528\u4EA4\u63A5\u8BF4\u660E\u3002"] : []
478
800
  ];
479
801
  ctx.output.result({
480
802
  ok: true,
481
803
  command: "graph doctor",
482
- summary: warnings.length ? `Graph doctor completed with ${warnings.length} warning(s).` : "Graph doctor completed without warnings.",
804
+ summary: ctx.language === "en" ? warnings.length ? `Graph doctor completed with ${warnings.length} warning(s).` : "Graph doctor completed without warnings." : warnings.length ? `\u4EE3\u7801\u56FE\u8BCA\u65AD\u5B8C\u6210\uFF0C\u53D1\u73B0 ${warnings.length} \u4E2A\u8B66\u544A\u3002` : "\u4EE3\u7801\u56FE\u8BCA\u65AD\u5B8C\u6210\uFF0C\u672A\u53D1\u73B0\u8B66\u544A\u3002",
483
805
  warnings,
484
- nextSteps: warnings.length ? ["Run fet graph setup", "Run fet graph handoff", "Run fet graph init when GitNexus is installed"] : void 0,
806
+ nextSteps: warnings.length ? ctx.language === "en" ? ["Run fet graph setup", "Run fet graph handoff", "Run fet graph init when GitNexus is installed"] : ["\u8FD0\u884C fet graph setup", "\u8FD0\u884C fet graph handoff", "\u5B89\u88C5 GitNexus \u540E\u8FD0\u884C fet graph init"] : void 0,
485
807
  data: result
486
808
  });
487
809
  }
488
810
  async function graphSetupCommand(ctx) {
489
811
  let result;
490
- const handoffPath = join8(ctx.projectRoot, ".fet", "graph-setup.md");
812
+ const handoffPath = join9(ctx.projectRoot, ".fet", "graph-setup.md");
813
+ const installCommand = process.env.FET_GITNEXUS_INSTALL_COMMAND?.trim() || null;
491
814
  await withProjectLock(ctx.projectRoot, { command: "graph setup", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
492
815
  result = await refreshGraphState(ctx, { write: false });
493
- await writeHandoffFile(handoffPath, renderGraphSetupHandoff(result.state));
816
+ await writeHandoffFile(handoffPath, renderGraphSetupHandoff(result.state, { installCommand, language: ctx.language }));
494
817
  const global = await ctx.stateStore.getOrCreateGlobal();
495
818
  global.graph ??= {};
496
819
  global.graph.gitnexus = {
@@ -503,21 +826,29 @@ async function graphSetupCommand(ctx) {
503
826
  ctx.output.result({
504
827
  ok: true,
505
828
  command: "graph setup",
506
- summary: "GitNexus setup handoff generated.",
507
- warnings: result.state.installed ? [] : ["GitNexus is not installed. The handoff explains installation and IDE-assisted setup options."],
508
- nextSteps: result.state.installed ? ["Run gitnexus setup if you want to configure IDE/MCP integrations", "Run fet graph init"] : ["Open .fet/graph-setup.md in your IDE AI"],
829
+ summary: ctx.language === "en" ? "GitNexus IDE-assisted setup playbook generated." : "\u5DF2\u751F\u6210 GitNexus IDE LLM \u8F85\u52A9\u5B89\u88C5\u4EFB\u52A1\u4E66\u3002",
830
+ warnings: result.state.installed ? [] : [
831
+ ctx.language === "en" ? "GitNexus is not installed. The playbook lets the current IDE LLM guide installation with user confirmation before risky commands." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002\u4EFB\u52A1\u4E66\u4F1A\u6307\u5BFC\u5F53\u524D IDE LLM \u63A8\u8FDB\u5B89\u88C5\uFF0C\u5E76\u5728\u9AD8\u98CE\u9669\u547D\u4EE4\u524D\u8BF7\u6C42\u7528\u6237\u786E\u8BA4\u3002"
832
+ ],
833
+ nextSteps: result.state.installed ? [
834
+ ctx.language === "en" ? "Ask your IDE AI to read .fet/graph-setup.md and run the optional gitnexus setup flow if you want IDE/MCP integration" : "\u8BA9\u5F53\u524D IDE AI \u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u5728\u9700\u8981 IDE/MCP \u96C6\u6210\u65F6\u6309\u786E\u8BA4\u6D41\u7A0B\u8FD0\u884C gitnexus setup",
835
+ "fet graph init"
836
+ ] : [
837
+ ctx.language === "en" ? "Ask your current IDE AI to read .fet/graph-setup.md and follow the installation playbook" : "\u8BA9\u5F53\u524D IDE AI \u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u6309\u5B89\u88C5\u4EFB\u52A1\u4E66\u63A8\u8FDB"
838
+ ],
509
839
  data: {
510
840
  path: ".fet/graph-setup.md",
841
+ installCommandConfigured: Boolean(installCommand),
511
842
  gitnexus: result.state
512
843
  }
513
844
  });
514
845
  }
515
846
  async function graphHandoffCommand(ctx) {
516
847
  let result;
517
- const handoffPath = join8(ctx.projectRoot, ".fet", "graph-handoff.md");
848
+ const handoffPath = join9(ctx.projectRoot, ".fet", "graph-handoff.md");
518
849
  await withProjectLock(ctx.projectRoot, { command: "graph handoff", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
519
850
  result = await refreshGraphState(ctx, { runStatus: true, write: false });
520
- await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state));
851
+ await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state, ctx.language));
521
852
  const global = await ctx.stateStore.getOrCreateGlobal();
522
853
  global.graph ??= {};
523
854
  global.graph.gitnexus = {
@@ -530,9 +861,11 @@ async function graphHandoffCommand(ctx) {
530
861
  ctx.output.result({
531
862
  ok: true,
532
863
  command: "graph handoff",
533
- summary: "GitNexus graph usage handoff generated.",
534
- warnings: result.state.installed ? [] : ["GitNexus is not installed. The handoff still documents the fallback behavior."],
535
- nextSteps: ["Cursor/Codex/OpenCode: read .fet/graph-handoff.md before broad repository scans"],
864
+ summary: ctx.language === "en" ? "GitNexus graph usage handoff generated." : "\u5DF2\u751F\u6210 GitNexus \u4EE3\u7801\u56FE\u4F7F\u7528\u4EA4\u63A5\u8BF4\u660E\u3002",
865
+ warnings: result.state.installed ? [] : [
866
+ ctx.language === "en" ? "GitNexus is not installed. The handoff still documents the fallback behavior." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002\u4EA4\u63A5\u8BF4\u660E\u4ECD\u4F1A\u8BB0\u5F55\u56DE\u9000\u884C\u4E3A\u3002"
867
+ ],
868
+ nextSteps: ctx.language === "en" ? ["Cursor/Codex/OpenCode: read .fet/graph-handoff.md before broad repository scans"] : ["Cursor/Codex/OpenCode\uFF1A\u5927\u8303\u56F4\u626B\u63CF\u4ED3\u5E93\u524D\u5148\u9605\u8BFB .fet/graph-handoff.md"],
536
869
  data: {
537
870
  path: ".fet/graph-handoff.md",
538
871
  gitnexus: result.state
@@ -544,7 +877,7 @@ async function graphAnalyzeCommand(ctx, mode, args) {
544
877
  if (!detection.installed) {
545
878
  throw new FetError({
546
879
  code: "GRAPH_PROVIDER_NOT_FOUND" /* GraphProviderNotFound */,
547
- message: "GitNexus is not installed or is not available on PATH.",
880
+ message: ctx.language === "en" ? "GitNexus is not installed or is not available on PATH." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\uFF0C\u6216 GitNexus \u4E0D\u5728 PATH \u4E2D\u3002",
548
881
  details: { executable: detection.executablePath, error: detection.error },
549
882
  suggestedCommand: "fet graph setup"
550
883
  });
@@ -553,27 +886,33 @@ async function graphAnalyzeCommand(ctx, mode, args) {
553
886
  if (run.exitCode !== 0) {
554
887
  throw new FetError({
555
888
  code: "GRAPH_COMMAND_FAILED" /* GraphCommandFailed */,
556
- message: "GitNexus analyze failed.",
889
+ message: ctx.language === "en" ? "GitNexus analyze failed." : "GitNexus analyze \u6267\u884C\u5931\u8D25\u3002",
557
890
  details: { command: run.command.join(" "), exitCode: run.exitCode, stdout: run.stdout, stderr: run.stderr },
558
891
  suggestedCommand: "fet graph doctor"
559
892
  });
560
893
  }
561
894
  const result = await refreshGraphState(ctx, { write: false });
895
+ const graphContext = await buildProjectGraphContext(ctx, result.state, `fet graph ${mode}`);
562
896
  const global = await ctx.stateStore.getOrCreateGlobal();
563
897
  global.graph ??= {};
564
898
  global.graph.gitnexus = {
565
899
  ...result.state,
566
- 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
567
903
  };
568
904
  await ctx.stateStore.writeGlobal(global);
569
905
  ctx.output.result({
570
906
  ok: true,
571
907
  command: `graph ${mode}`,
572
- summary: mode === "init" ? "GitNexus graph initialized." : "GitNexus graph refreshed.",
573
- warnings: result.state.graphExists ? [] : ["GitNexus analyze completed, but the configured graph directory was not found."],
574
- nextSteps: ["Run fet graph status", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"],
908
+ summary: ctx.language === "en" ? mode === "init" ? "GitNexus graph initialized." : "GitNexus graph refreshed." : mode === "init" ? "\u5DF2\u521D\u59CB\u5316 GitNexus \u4EE3\u7801\u56FE\u3002" : "\u5DF2\u5237\u65B0 GitNexus \u4EE3\u7801\u56FE\u3002",
909
+ warnings: result.state.graphExists ? [] : [
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"
911
+ ],
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"],
575
913
  data: {
576
914
  gitnexus: global.graph.gitnexus,
915
+ graphContext,
577
916
  run: {
578
917
  command: run.command,
579
918
  stdout: run.stdout.trim(),
@@ -593,7 +932,7 @@ async function refreshGraphState(ctx, options = {}) {
593
932
  gitnexusStatus = await runGitNexus(["status"], { cwd: ctx.projectRoot });
594
933
  state = {
595
934
  ...state,
596
- lastStatus: firstLine(gitnexusStatus.stdout) || firstLine(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
935
+ lastStatus: firstLine2(gitnexusStatus.stdout) || firstLine2(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
597
936
  };
598
937
  }
599
938
  if (options.write ?? true) {
@@ -611,18 +950,19 @@ async function refreshGraphState(ctx, options = {}) {
611
950
  };
612
951
  }
613
952
  async function writeHandoffFile(path, content) {
614
- await mkdir4(dirname5(path), { recursive: true });
953
+ await mkdir5(dirname6(path), { recursive: true });
615
954
  await atomicWrite(path, content);
616
955
  }
617
- function renderGraphSetupHandoff(state) {
618
- return `<!-- FET:MANAGED
956
+ function renderGraphSetupHandoff(state, options) {
957
+ if (options.language === "en") {
958
+ return `<!-- FET:MANAGED
619
959
  schemaVersion: 1
620
960
  generator: graph-setup
621
961
  FET:END -->
622
962
 
623
- # FET Graph Setup
963
+ # FET GitNexus IDE Setup Playbook
624
964
 
625
- GitNexus graph support is optional. FET does not install GitNexus automatically and does not require graph support for OpenSpec workflows.
965
+ This file is written for the current IDE LLM. Use it to help the user install and verify optional GitNexus graph support. GitNexus is optional; FET/OpenSpec workflows must continue to work when it is unavailable.
626
966
 
627
967
  Current status:
628
968
 
@@ -631,24 +971,70 @@ Current status:
631
971
  - Version: ${state.version ?? "unknown"}
632
972
  - Graph path: ${state.graphPath ?? ".gitnexus"}
633
973
  - Graph exists: ${state.graphExists ? "yes" : "no"}
974
+ - Configured install command: ${options.installCommand ?? "none"}
634
975
 
635
- Suggested setup flow:
976
+ IDE LLM setup flow:
636
977
 
637
- 1. If GitNexus is not installed, install it using the method recommended by the GitNexus project.
638
- 2. If you want GitNexus MCP or IDE integration, run \`gitnexus setup\` yourself after reviewing what it changes.
639
- 3. Return to this project and run \`fet graph init\` to build the first graph.
640
- 4. Run \`fet graph handoff\` so IDE AI can prefer graph context before broad repository scans.
978
+ 1. Check the shell, operating system, package managers, and PATH. Run only read-only detection commands at first.
979
+ 2. Run \`gitnexus --version\` or the executable shown above. If it succeeds, skip installation and continue to verification.
980
+ 3. If GitNexus is missing and \`FET_GITNEXUS_INSTALL_COMMAND\` is configured, explain that command to the user and ask for approval before running it.
981
+ 4. If no install command is configured, find the official GitNexus installation instructions or ask the user for the preferred install method. Do not invent an installer.
982
+ 5. Before running any command that downloads software, installs globally, changes PATH, modifies user home files, or changes IDE/MCP configuration, show the exact command and wait for user approval.
983
+ 6. After installation, verify with \`gitnexus --version\`.
984
+ 7. If the user wants IDE/MCP integration, run \`gitnexus setup\` only after explaining what it may change and receiving approval.
985
+ 8. Return to this project and run \`fet graph init\` to build the first graph.
986
+ 9. Run \`fet graph handoff\` so future IDE AI work can prefer graph context before broad repository scans.
641
987
 
642
988
  Guardrails:
643
989
 
644
990
  - Do not block FET/OpenSpec commands when GitNexus is unavailable.
645
991
  - Do not generate or modify application code during setup.
646
- - Do not run global IDE configuration commands unless the user explicitly approves them.
992
+ - Do not silently install software or modify global/user-level configuration.
993
+ - If installation fails, summarize the failing command, stderr, and the next manual step.
647
994
  `;
648
- }
649
- function renderGraphUsageHandoff(state) {
995
+ }
650
996
  return `<!-- FET:MANAGED
651
997
  schemaVersion: 1
998
+ generator: graph-setup
999
+ FET:END -->
1000
+
1001
+ # FET GitNexus IDE \u5B89\u88C5\u4EFB\u52A1\u4E66
1002
+
1003
+ \u672C\u6587\u6863\u5199\u7ED9\u5F53\u524D IDE LLM\u3002\u8BF7\u7528\u5B83\u5E2E\u52A9\u7528\u6237\u5B89\u88C5\u5E76\u9A8C\u8BC1\u53EF\u9009\u7684 GitNexus \u4EE3\u7801\u56FE\u80FD\u529B\u3002GitNexus \u662F\u53EF\u9009\u80FD\u529B\uFF1B\u5373\u4F7F\u4E0D\u53EF\u7528\uFF0CFET/OpenSpec \u4E3B\u6D41\u7A0B\u4E5F\u5FC5\u987B\u7EE7\u7EED\u53EF\u7528\u3002
1004
+
1005
+ \u5F53\u524D\u72B6\u6001\uFF1A
1006
+
1007
+ - \u5DF2\u5B89\u88C5\uFF1A${state.installed ? "\u662F" : "\u5426"}
1008
+ - \u53EF\u6267\u884C\u6587\u4EF6\uFF1A${state.executablePath ?? "gitnexus"}
1009
+ - \u7248\u672C\uFF1A${state.version ?? "unknown"}
1010
+ - \u56FE\u8DEF\u5F84\uFF1A${state.graphPath ?? ".gitnexus"}
1011
+ - \u56FE\u5DF2\u5B58\u5728\uFF1A${state.graphExists ? "\u662F" : "\u5426"}
1012
+ - \u5DF2\u914D\u7F6E\u5B89\u88C5\u547D\u4EE4\uFF1A${options.installCommand ?? "\u65E0"}
1013
+
1014
+ IDE LLM \u5B89\u88C5\u6D41\u7A0B\uFF1A
1015
+
1016
+ 1. \u5148\u68C0\u6D4B shell\u3001\u64CD\u4F5C\u7CFB\u7EDF\u3001\u5305\u7BA1\u7406\u5668\u548C PATH\u3002\u8D77\u6B65\u9636\u6BB5\u53EA\u8FD0\u884C\u53EA\u8BFB\u68C0\u6D4B\u547D\u4EE4\u3002
1017
+ 2. \u8FD0\u884C \`gitnexus --version\` \u6216\u4E0A\u65B9\u663E\u793A\u7684\u53EF\u6267\u884C\u6587\u4EF6\u3002\u5982\u679C\u6210\u529F\uFF0C\u8DF3\u8FC7\u5B89\u88C5\u5E76\u8FDB\u5165\u9A8C\u8BC1\u3002
1018
+ 3. \u5982\u679C GitNexus \u7F3A\u5931\u4E14\u5DF2\u914D\u7F6E \`FET_GITNEXUS_INSTALL_COMMAND\`\uFF0C\u5148\u5411\u7528\u6237\u89E3\u91CA\u8BE5\u547D\u4EE4\uFF0C\u518D\u7B49\u5F85\u7528\u6237\u6279\u51C6\u540E\u6267\u884C\u3002
1019
+ 4. \u5982\u679C\u6CA1\u6709\u914D\u7F6E\u5B89\u88C5\u547D\u4EE4\uFF0C\u67E5\u627E GitNexus \u5B98\u65B9\u5B89\u88C5\u8BF4\u660E\uFF0C\u6216\u8BE2\u95EE\u7528\u6237\u5E0C\u671B\u4F7F\u7528\u7684\u5B89\u88C5\u65B9\u5F0F\u3002\u4E0D\u8981\u81C6\u9020\u5B89\u88C5\u547D\u4EE4\u3002
1020
+ 5. \u4EFB\u4F55\u4F1A\u4E0B\u8F7D\u8F6F\u4EF6\u3001\u5168\u5C40\u5B89\u88C5\u3001\u4FEE\u6539 PATH\u3001\u5199\u5165\u7528\u6237\u76EE\u5F55\u6216\u4FEE\u6539 IDE/MCP \u914D\u7F6E\u7684\u547D\u4EE4\uFF0C\u6267\u884C\u524D\u90FD\u8981\u5C55\u793A\u5B8C\u6574\u547D\u4EE4\u5E76\u7B49\u5F85\u7528\u6237\u786E\u8BA4\u3002
1021
+ 6. \u5B89\u88C5\u540E\u8FD0\u884C \`gitnexus --version\` \u9A8C\u8BC1\u3002
1022
+ 7. \u5982\u679C\u7528\u6237\u9700\u8981 IDE/MCP \u96C6\u6210\uFF0C\u5148\u8BF4\u660E \`gitnexus setup\` \u53EF\u80FD\u4FEE\u6539\u7684\u5185\u5BB9\uFF0C\u83B7\u5F97\u786E\u8BA4\u540E\u518D\u8FD0\u884C\u3002
1023
+ 8. \u56DE\u5230\u672C\u9879\u76EE\u8FD0\u884C \`fet graph init\`\uFF0C\u751F\u6210\u7B2C\u4E00\u4EFD\u4EE3\u7801\u56FE\u3002
1024
+ 9. \u8FD0\u884C \`fet graph handoff\`\uFF0C\u8BA9\u540E\u7EED IDE AI \u5728\u5927\u8303\u56F4\u626B\u63CF\u524D\u4F18\u5148\u4F7F\u7528\u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u3002
1025
+
1026
+ \u7EA6\u675F\uFF1A
1027
+
1028
+ - GitNexus \u4E0D\u53EF\u7528\u65F6\uFF0C\u4E0D\u8981\u963B\u585E FET/OpenSpec \u547D\u4EE4\u3002
1029
+ - \u5B89\u88C5\u8FC7\u7A0B\u4E2D\u4E0D\u8981\u751F\u6210\u6216\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
1030
+ - \u4E0D\u8981\u9759\u9ED8\u5B89\u88C5\u8F6F\u4EF6\uFF0C\u4E5F\u4E0D\u8981\u9759\u9ED8\u4FEE\u6539\u5168\u5C40\u6216\u7528\u6237\u7EA7\u914D\u7F6E\u3002
1031
+ - \u5982\u679C\u5B89\u88C5\u5931\u8D25\uFF0C\u6C47\u603B\u5931\u8D25\u547D\u4EE4\u3001stderr \u548C\u4E0B\u4E00\u6B65\u4EBA\u5DE5\u5904\u7406\u5EFA\u8BAE\u3002
1032
+ `;
1033
+ }
1034
+ function renderGraphUsageHandoff(state, language) {
1035
+ if (language === "en") {
1036
+ return `<!-- FET:MANAGED
1037
+ schemaVersion: 1
652
1038
  generator: graph-handoff
653
1039
  FET:END -->
654
1040
 
@@ -676,26 +1062,57 @@ When producing OpenSpec artifacts:
676
1062
  - Use graph context to make proposal, design, specs, and tasks more precise.
677
1063
  - Avoid large repository scans when the graph already narrows the relevant area.
678
1064
  - Keep all generated artifacts in the normal OpenSpec change directory.
1065
+ `;
1066
+ }
1067
+ return `<!-- FET:MANAGED
1068
+ schemaVersion: 1
1069
+ generator: graph-handoff
1070
+ FET:END -->
1071
+
1072
+ # FET \u4EE3\u7801\u56FE\u4EA4\u63A5\u8BF4\u660E
1073
+
1074
+ \u5728\u5927\u8303\u56F4\u626B\u63CF\u4ED3\u5E93\u524D\uFF0C\u4F18\u5148\u628A GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u4F5C\u4E3A\u53EF\u9009\u7684\u7B2C\u4E00\u8F6E\u7EBF\u7D22\u3002
1075
+
1076
+ \u5F53\u524D\u72B6\u6001\uFF1A
1077
+
1078
+ - \u5DF2\u5B89\u88C5\uFF1A${state.installed ? "\u662F" : "\u5426"}
1079
+ - \u56FE\u8DEF\u5F84\uFF1A${state.graphPath ?? ".gitnexus"}
1080
+ - \u56FE\u5DF2\u5B58\u5728\uFF1A${state.graphExists ? "\u662F" : "\u5426"}
1081
+ - \u6700\u540E\u7D22\u5F15\u65F6\u95F4\uFF1A${state.lastIndexedAt ?? "unknown"}
1082
+ - \u6700\u540E\u72B6\u6001\uFF1A${state.lastStatus ?? "unknown"}
1083
+
1084
+ \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u53EF\u7528\u65F6\uFF1A
1085
+
1086
+ 1. \u7528\u4EE3\u7801\u56FE\u8BC6\u522B\u53EF\u80FD\u76F8\u5173\u7684\u6A21\u5757\u3001\u4F9D\u8D56\u548C\u63D2\u5165\u70B9\u3002
1087
+ 2. \u53EA\u8BFB\u53D6\u9700\u8981\u786E\u8BA4\u884C\u4E3A\u7684\u5177\u4F53\u6E90\u7801\u6587\u4EF6\u3002
1088
+ 3. \u5F53\u4EE3\u7801\u56FE\u63A8\u65AD\u4E0E OpenSpec \u4EA7\u7269\u6216 AGENTS.md \u51B2\u7A81\u65F6\uFF0C\u4F18\u5148\u76F8\u4FE1 OpenSpec \u4EA7\u7269\u548C AGENTS.md\u3002
1089
+ 4. \u5982\u679C\u4EE3\u7801\u56FE\u7F3A\u5931\u3001\u8FC7\u671F\u6216\u4E0D\u5B8C\u6574\uFF0C\u56DE\u9000\u5230\u666E\u901A\u4ED3\u5E93\u68C0\u67E5\u3002
1090
+
1091
+ \u751F\u6210 OpenSpec \u4EA7\u7269\u65F6\uFF1A
1092
+
1093
+ - \u7528\u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u8BA9 proposal\u3001design\u3001specs \u548C tasks \u66F4\u7CBE\u786E\u3002
1094
+ - \u5F53\u4EE3\u7801\u56FE\u5DF2\u7ECF\u7F29\u5C0F\u76F8\u5173\u8303\u56F4\u65F6\uFF0C\u907F\u514D\u5927\u8303\u56F4\u4ED3\u5E93\u626B\u63CF\u3002
1095
+ - \u6240\u6709\u751F\u6210\u4EA7\u7269\u4ECD\u5199\u5165\u6B63\u5E38\u7684 OpenSpec change \u76EE\u5F55\u3002
679
1096
  `;
680
1097
  }
681
- function firstLine(value) {
1098
+ function firstLine2(value) {
682
1099
  return value.trim().split(/\r?\n/)[0]?.trim() || null;
683
1100
  }
684
1101
 
685
1102
  // src/commands/init.ts
686
- import { readFile as readFile7, stat as stat4 } from "fs/promises";
687
- import { join as join11 } from "path";
1103
+ import { readFile as readFile8, stat as stat4 } from "fs/promises";
1104
+ import { join as join12 } from "path";
688
1105
 
689
1106
  // src/version.ts
690
1107
  import { existsSync, readFileSync } from "fs";
691
- import { dirname as dirname6, join as join9, parse } from "path";
1108
+ import { dirname as dirname7, join as join10, parse } from "path";
692
1109
  import { fileURLToPath } from "url";
693
1110
  var FET_VERSION = readPackageVersion();
694
1111
  function readPackageVersion() {
695
- let currentDir = dirname6(fileURLToPath(import.meta.url));
1112
+ let currentDir = dirname7(fileURLToPath(import.meta.url));
696
1113
  const root = parse(currentDir).root;
697
1114
  while (true) {
698
- const packageJsonPath = join9(currentDir, "package.json");
1115
+ const packageJsonPath = join10(currentDir, "package.json");
699
1116
  if (existsSync(packageJsonPath)) {
700
1117
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
701
1118
  if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
@@ -706,7 +1123,7 @@ function readPackageVersion() {
706
1123
  if (currentDir === root) {
707
1124
  throw new Error("\u65E0\u6CD5\u5B9A\u4F4D FET package.json");
708
1125
  }
709
- currentDir = dirname6(currentDir);
1126
+ currentDir = dirname7(currentDir);
710
1127
  }
711
1128
  }
712
1129
 
@@ -975,8 +1392,8 @@ function renderFetConfig(scan, language = "zh-CN") {
975
1392
  var KARPATHY_SKILLS_SOURCE = "https://github.com/forrestchang/andrej-karpathy-skills";
976
1393
  var BEGIN = "<!-- FET:BEGIN ANDREJ-KARPATHY-SKILLS -->";
977
1394
  var END = "<!-- FET:END ANDREJ-KARPATHY-SKILLS -->";
978
- function mergeKarpathyClaudeMd(existing) {
979
- const block = renderManagedBlock(renderKarpathyClaudeGuidelines());
1395
+ function mergeKarpathyClaudeMd(existing, language = "zh-CN") {
1396
+ const block = renderManagedBlock(renderKarpathyClaudeGuidelines(language));
980
1397
  if (!existing || !existing.trim()) {
981
1398
  return `${block}
982
1399
  `;
@@ -1021,10 +1438,10 @@ function renderManagedBlock(content) {
1021
1438
  ${content}
1022
1439
  ${END}`;
1023
1440
  }
1024
- function renderKarpathyClaudeGuidelines() {
1025
- return `# Andrej Karpathy Inspired Coding Guidelines
1441
+ function renderKarpathyClaudeGuidelines(language) {
1442
+ return `# ${language === "en" ? "Andrej Karpathy Inspired Coding Guidelines" : "\u53D7 Andrej Karpathy \u542F\u53D1\u7684\u7F16\u7801\u6307\u5357"}
1026
1443
 
1027
- ${renderKarpathyGuidelinesBody()}`;
1444
+ ${renderKarpathyGuidelinesBody(language)}`;
1028
1445
  }
1029
1446
  function renderKarpathyGuidelinesBody(language = "zh-CN") {
1030
1447
  if (language === "en") {
@@ -1157,18 +1574,18 @@ ${block}
1157
1574
  }
1158
1575
 
1159
1576
  // src/commands/update-context.ts
1160
- import { readFile as readFile6 } from "fs/promises";
1161
- import { join as join10 } from "path";
1577
+ import { readFile as readFile7 } from "fs/promises";
1578
+ import { join as join11 } from "path";
1162
1579
 
1163
1580
  // src/config/yaml.ts
1164
- import { readFile as readFile5 } from "fs/promises";
1581
+ import { readFile as readFile6 } from "fs/promises";
1165
1582
  import { parseDocument } from "yaml";
1166
1583
  async function mergeFetConfig(configPath, renderedFetYaml) {
1167
1584
  const fetDoc = parseDocument(renderedFetYaml);
1168
1585
  const nextFet = fetDoc.get("fet", true);
1169
1586
  let existing = "";
1170
1587
  try {
1171
- existing = await readFile5(configPath, "utf8");
1588
+ existing = await readFile6(configPath, "utf8");
1172
1589
  } catch {
1173
1590
  return renderedFetYaml;
1174
1591
  }
@@ -1192,14 +1609,14 @@ async function updateContextCommand(ctx) {
1192
1609
  }
1193
1610
  async function updateContextFiles(ctx) {
1194
1611
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
1195
- const agentsPath = join10(ctx.projectRoot, "AGENTS.md");
1196
- const configPath = join10(ctx.projectRoot, "openspec", "config.yaml");
1197
- const claudePath = join10(ctx.projectRoot, "CLAUDE.md");
1198
- const karpathyHandoffPath = join10(ctx.projectRoot, ".fet", "karpathy-guidelines.md");
1199
- const karpathyCursorPath = join10(ctx.projectRoot, ".cursor", "rules", "karpathy-guidelines.mdc");
1200
- const existingAgents = await readOptional(agentsPath);
1201
- const existingClaude = await readOptional(claudePath);
1202
- 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);
1203
1620
  const warnings = [...scan.warnings];
1204
1621
  if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
1205
1622
  throw new FetError({
@@ -1225,7 +1642,7 @@ async function updateContextFiles(ctx) {
1225
1642
  }
1226
1643
  await atomicWrite(agentsPath, replaceManagedRegion(existingAgents, renderAgentsMd(scan, ctx.language)));
1227
1644
  await atomicWrite(configPath, await mergeFetConfig(configPath, renderFetConfig(scan, ctx.language)));
1228
- await atomicWrite(claudePath, mergeKarpathyClaudeMd(existingClaude));
1645
+ await atomicWrite(claudePath, mergeKarpathyClaudeMd(existingClaude, ctx.language));
1229
1646
  await atomicWrite(karpathyHandoffPath, renderKarpathyFetHandoff(ctx.language));
1230
1647
  if (!existingKarpathyCursor || existingKarpathyCursor.includes("FET:MANAGED")) {
1231
1648
  await atomicWrite(karpathyCursorPath, renderKarpathyCursorRule(ctx.language));
@@ -1247,9 +1664,9 @@ async function updateContextFiles(ctx) {
1247
1664
  await ctx.stateStore.writeGlobal(state);
1248
1665
  return { warnings };
1249
1666
  }
1250
- async function readOptional(path) {
1667
+ async function readOptional2(path) {
1251
1668
  try {
1252
- return await readFile6(path, "utf8");
1669
+ return await readFile7(path, "utf8");
1253
1670
  } catch {
1254
1671
  return null;
1255
1672
  }
@@ -1257,7 +1674,7 @@ async function readOptional(path) {
1257
1674
 
1258
1675
  // src/commands/init.ts
1259
1676
  async function initCommand(ctx) {
1260
- const alreadyInitialized = await exists2(join11(ctx.projectRoot, "openspec", "config.yaml"));
1677
+ const alreadyInitialized = await exists2(join12(ctx.projectRoot, "openspec", "config.yaml"));
1261
1678
  let warnings = [];
1262
1679
  await withProjectLock(ctx.projectRoot, { command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
1263
1680
  const journal = createInitJournal(ctx.fetVersion);
@@ -1309,13 +1726,13 @@ async function initCommand(ctx) {
1309
1726
  });
1310
1727
  }
1311
1728
  async function ensureGitignore(ctx) {
1312
- const gitignorePath = join11(ctx.projectRoot, ".gitignore");
1313
- const existing = await readOptional2(gitignorePath);
1729
+ const gitignorePath = join12(ctx.projectRoot, ".gitignore");
1730
+ const existing = await readOptional3(gitignorePath);
1314
1731
  await atomicWrite(gitignorePath, mergeGitignore(existing));
1315
1732
  }
1316
- async function readOptional2(path) {
1733
+ async function readOptional3(path) {
1317
1734
  try {
1318
- return await readFile7(path, "utf8");
1735
+ return await readFile8(path, "utf8");
1319
1736
  } catch {
1320
1737
  return null;
1321
1738
  }
@@ -1330,8 +1747,8 @@ async function exists2(path) {
1330
1747
  }
1331
1748
 
1332
1749
  // src/commands/proxy.ts
1333
- import { readFile as readFile10 } from "fs/promises";
1334
- import { join as join13 } from "path";
1750
+ import { readFile as readFile11 } from "fs/promises";
1751
+ import { join as join14 } from "path";
1335
1752
 
1336
1753
  // src/state/project.ts
1337
1754
  import { execFile as execFile2 } from "child_process";
@@ -1360,8 +1777,8 @@ async function git(cwd, args) {
1360
1777
  }
1361
1778
 
1362
1779
  // src/state/store.ts
1363
- import { mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
1364
- import { join as join12 } from "path";
1780
+ import { mkdir as mkdir6, readFile as readFile9 } from "fs/promises";
1781
+ import { join as join13 } from "path";
1365
1782
 
1366
1783
  // src/language.ts
1367
1784
  var DEFAULT_LANGUAGE = "zh-CN";
@@ -1479,7 +1896,7 @@ var StateStore = class {
1479
1896
  project;
1480
1897
  async readGlobal() {
1481
1898
  try {
1482
- const value = JSON.parse(await readFile8(this.globalPath(), "utf8"));
1899
+ const value = JSON.parse(await readFile9(this.globalPath(), "utf8"));
1483
1900
  assertGlobalState(value);
1484
1901
  return value;
1485
1902
  } catch (error) {
@@ -1494,13 +1911,13 @@ var StateStore = class {
1494
1911
  }
1495
1912
  async writeGlobal(state) {
1496
1913
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1497
- await mkdir5(join12(this.projectRoot, "openspec"), { recursive: true });
1914
+ await mkdir6(join13(this.projectRoot, "openspec"), { recursive: true });
1498
1915
  await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
1499
1916
  `);
1500
1917
  }
1501
1918
  async readChange(changeId) {
1502
1919
  try {
1503
- const value = JSON.parse(await readFile8(this.changePath(changeId), "utf8"));
1920
+ const value = JSON.parse(await readFile9(this.changePath(changeId), "utf8"));
1504
1921
  assertChangeState(value);
1505
1922
  return value;
1506
1923
  } catch (error) {
@@ -1515,15 +1932,15 @@ var StateStore = class {
1515
1932
  }
1516
1933
  async writeChange(state) {
1517
1934
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1518
- await mkdir5(join12(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
1935
+ await mkdir6(join13(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
1519
1936
  await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
1520
1937
  `);
1521
1938
  }
1522
1939
  globalPath() {
1523
- return join12(this.projectRoot, "openspec", "fet-state.json");
1940
+ return join13(this.projectRoot, "openspec", "fet-state.json");
1524
1941
  }
1525
1942
  changePath(changeId) {
1526
- return join12(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
1943
+ return join13(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
1527
1944
  }
1528
1945
  };
1529
1946
  function isNotFound(error) {
@@ -1531,11 +1948,11 @@ function isNotFound(error) {
1531
1948
  }
1532
1949
 
1533
1950
  // src/state/tasks.ts
1534
- import { readFile as readFile9 } from "fs/promises";
1951
+ import { readFile as readFile10 } from "fs/promises";
1535
1952
  async function readCompletedTaskIds(tasksPath) {
1536
1953
  let content;
1537
1954
  try {
1538
- content = await readFile9(tasksPath, "utf8");
1955
+ content = await readFile10(tasksPath, "utf8");
1539
1956
  } catch {
1540
1957
  return [];
1541
1958
  }
@@ -1566,71 +1983,76 @@ var phaseByCommand = {
1566
1983
  };
1567
1984
  async function proxyCommand(ctx, command, args) {
1568
1985
  const openSpecArgs = stripFetOptions(args);
1569
- await withProjectLock(
1570
- ctx.projectRoot,
1571
- { command, cwd: ctx.cwd, fetVersion: ctx.fetVersion },
1572
- async () => {
1573
- if (["sync", "archive", "bulk-archive"].includes(command)) {
1574
- await assertVerified(ctx);
1575
- }
1576
- const mapped = await mapOpenSpecCommand(ctx, command, openSpecArgs);
1577
- const targetChangeId = command === "archive" ? mapped.args[0] ?? ctx.changeId ?? null : ctx.changeId ?? null;
1578
- const changelogEntry = command === "archive" && targetChangeId ? await createChangelogEntry(ctx.projectRoot, targetChangeId) : null;
1579
- const result = await ctx.openSpec.run(mapped.command, mapped.args, { cwd: ctx.projectRoot, stdio: ctx.json ? "pipe" : "inherit" });
1580
- if (result.exitCode !== 0) {
1581
- throw new FetError({
1582
- code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
1583
- message: `OpenSpec ${command} \u6267\u884C\u5931\u8D25`,
1584
- details: result,
1585
- recoverable: true
1586
- });
1587
- }
1588
- if (changelogEntry) {
1589
- await appendChangelog(ctx.projectRoot, changelogEntry);
1590
- }
1591
- const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
1592
- const state = await ctx.stateStore.getOrCreateGlobal();
1593
- state.openChangeIds = inspection.changes;
1594
- if (command === "archive") {
1595
- if (!state.activeChangeId || state.activeChangeId === targetChangeId || !inspection.changes.includes(state.activeChangeId)) {
1596
- state.activeChangeId = null;
1597
- }
1598
- state.verifyAuthorization = null;
1599
- } else {
1600
- if (ctx.changeId && inspection.changes.includes(ctx.changeId)) {
1601
- state.activeChangeId = ctx.changeId;
1602
- } else if (state.activeChangeId && !inspection.changes.includes(state.activeChangeId)) {
1603
- state.activeChangeId = inspection.changes.length === 1 ? inspection.changes[0] ?? null : null;
1604
- } else if (!state.activeChangeId && inspection.changes.length === 1) {
1605
- state.activeChangeId = inspection.changes[0] ?? null;
1606
- }
1607
- }
1608
- await ctx.stateStore.writeGlobal(state);
1609
- const changeId = ctx.changeId ?? state.activeChangeId;
1610
- if (changeId && inspection.changes.includes(changeId)) {
1611
- const changeInspection = await ctx.openSpec.inspectChange(ctx.projectRoot, changeId);
1612
- const changeState = await ctx.stateStore.getOrCreateChange(changeId, phaseByCommand[command] ?? "propose");
1613
- changeState.currentPhase = phaseByCommand[command] ?? changeState.currentPhase;
1614
- changeState.phases[changeState.currentPhase] = {
1615
- status: "done",
1616
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1617
- };
1618
- changeState.lastOpenSpecCommand = {
1619
- command: mapped.command,
1620
- args: mapped.args,
1621
- exitCode: result.exitCode,
1622
- ranAt: (/* @__PURE__ */ new Date()).toISOString()
1623
- };
1624
- changeState.tasks.completedIds = await readCompletedTaskIds(changeInspection.tasksPath);
1625
- changeState.tasks.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
1626
- 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
+ const mappedChangeId = extractChangeId(mapped.args);
1993
+ const targetChangeId = command === "archive" ? mapped.args[0] ?? ctx.changeId ?? mappedChangeId : ctx.changeId ?? mappedChangeId;
1994
+ runState.graphContext = await buildWorkflowGraphContext(ctx, {
1995
+ command,
1996
+ args: mapped.args,
1997
+ changeId: targetChangeId
1998
+ });
1999
+ const changelogEntry = command === "archive" && targetChangeId ? await createChangelogEntry(ctx.projectRoot, targetChangeId) : null;
2000
+ const result = await ctx.openSpec.run(mapped.command, mapped.args, { cwd: ctx.projectRoot, stdio: ctx.json ? "pipe" : "inherit" });
2001
+ if (result.exitCode !== 0) {
2002
+ throw new FetError({
2003
+ code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
2004
+ message: `OpenSpec ${command} failed.`,
2005
+ details: result,
2006
+ recoverable: true
2007
+ });
2008
+ }
2009
+ if (changelogEntry) {
2010
+ await appendChangelog(ctx.projectRoot, changelogEntry);
2011
+ }
2012
+ const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
2013
+ const state = await ctx.stateStore.getOrCreateGlobal();
2014
+ state.openChangeIds = inspection.changes;
2015
+ if (command === "archive") {
2016
+ if (!state.activeChangeId || state.activeChangeId === targetChangeId || !inspection.changes.includes(state.activeChangeId)) {
2017
+ state.activeChangeId = null;
1627
2018
  }
2019
+ state.verifyAuthorization = null;
2020
+ } else if (ctx.changeId && inspection.changes.includes(ctx.changeId)) {
2021
+ state.activeChangeId = ctx.changeId;
2022
+ } else if (state.activeChangeId && !inspection.changes.includes(state.activeChangeId)) {
2023
+ state.activeChangeId = inspection.changes.length === 1 ? inspection.changes[0] ?? null : null;
2024
+ } else if (!state.activeChangeId && inspection.changes.length === 1) {
2025
+ state.activeChangeId = inspection.changes[0] ?? null;
1628
2026
  }
1629
- );
2027
+ await ctx.stateStore.writeGlobal(state);
2028
+ const changeId = ctx.changeId ?? state.activeChangeId;
2029
+ if (changeId && inspection.changes.includes(changeId)) {
2030
+ const changeInspection = await ctx.openSpec.inspectChange(ctx.projectRoot, changeId);
2031
+ const changeState = await ctx.stateStore.getOrCreateChange(changeId, phaseByCommand[command] ?? "propose");
2032
+ changeState.currentPhase = phaseByCommand[command] ?? changeState.currentPhase;
2033
+ changeState.phases[changeState.currentPhase] = {
2034
+ status: "done",
2035
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2036
+ };
2037
+ changeState.lastOpenSpecCommand = {
2038
+ command: mapped.command,
2039
+ args: mapped.args,
2040
+ exitCode: result.exitCode,
2041
+ ranAt: (/* @__PURE__ */ new Date()).toISOString()
2042
+ };
2043
+ changeState.tasks.completedIds = await readCompletedTaskIds(changeInspection.tasksPath);
2044
+ changeState.tasks.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
2045
+ await ctx.stateStore.writeChange(changeState);
2046
+ }
2047
+ });
2048
+ const graphContext = runState.graphContext;
1630
2049
  ctx.output.result({
1631
2050
  ok: true,
1632
2051
  command,
1633
- summary: `fet ${command} \u5B8C\u6210\u3002`
2052
+ summary: `fet ${command} completed.`,
2053
+ warnings: graphContext?.warnings,
2054
+ nextSteps: graphContext?.generated && graphContext.path ? [`Read ${graphContext.path} before broad source scans or implementation decisions.`] : void 0,
2055
+ data: graphContext ? { graphContext } : void 0
1634
2056
  });
1635
2057
  }
1636
2058
  async function createChangelogEntry(projectRoot, changeId) {
@@ -1640,10 +2062,12 @@ async function createChangelogEntry(projectRoot, changeId) {
1640
2062
  };
1641
2063
  }
1642
2064
  async function appendChangelog(projectRoot, entry) {
1643
- const changelogPath = join13(projectRoot, "CHANGELOG.md");
1644
- const existing = await readOptional3(changelogPath);
2065
+ const changelogPath = join14(projectRoot, "CHANGELOG.md");
2066
+ const existing = await readOptional4(changelogPath);
2067
+ const legacyContentLabel = "\u66F4\u65B0\u5185\u5BB9";
1645
2068
  const block = `updateTime: ${entry.updateTime}
1646
- \u66F4\u65B0\u5185\u5BB9:${entry.content}
2069
+ changeRequirement:${entry.content}
2070
+ ${legacyContentLabel}:${entry.content}
1647
2071
  `;
1648
2072
  const next = existing?.trimEnd() ? `${existing.trimEnd()}
1649
2073
 
@@ -1651,12 +2075,12 @@ ${block}` : block;
1651
2075
  await atomicWrite(changelogPath, next);
1652
2076
  }
1653
2077
  async function readChangeRequirement(projectRoot, changeId) {
1654
- const changeRoot = join13(projectRoot, "openspec", "changes", changeId);
1655
- const proposal = await readOptional3(join13(changeRoot, "proposal.md"));
2078
+ const changeRoot = join14(projectRoot, "openspec", "changes", changeId);
2079
+ const proposal = await readOptional4(join14(changeRoot, "proposal.md"));
1656
2080
  if (proposal) {
1657
2081
  return summarizeMarkdown(proposal);
1658
2082
  }
1659
- const readme = await readOptional3(join13(changeRoot, "README.md"));
2083
+ const readme = await readOptional4(join14(changeRoot, "README.md"));
1660
2084
  if (readme) {
1661
2085
  return summarizeMarkdown(readme);
1662
2086
  }
@@ -1664,11 +2088,11 @@ async function readChangeRequirement(projectRoot, changeId) {
1664
2088
  }
1665
2089
  function summarizeMarkdown(content) {
1666
2090
  const normalized = content.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("<!--") && !line.startsWith("---")).join(" ");
1667
- return normalized || "\u672A\u63D0\u4F9B\u53D8\u66F4\u9700\u6C42";
2091
+ return normalized || "No change requirement found.";
1668
2092
  }
1669
- async function readOptional3(path) {
2093
+ async function readOptional4(path) {
1670
2094
  try {
1671
- return await readFile10(path, "utf8");
2095
+ return await readFile11(path, "utf8");
1672
2096
  } catch {
1673
2097
  return null;
1674
2098
  }
@@ -1690,7 +2114,7 @@ async function passthroughCommand(ctx, command, args) {
1690
2114
  if (result.exitCode !== 0) {
1691
2115
  throw new FetError({
1692
2116
  code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
1693
- message: `OpenSpec ${command} \u6267\u884C\u5931\u8D25`,
2117
+ message: `OpenSpec ${command} failed.`,
1694
2118
  details: result
1695
2119
  });
1696
2120
  }
@@ -1732,18 +2156,6 @@ async function mapOpenSpecCommand(ctx, command, args) {
1732
2156
  return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
1733
2157
  case "archive":
1734
2158
  return { command: "archive", args: [ctx.changeId ?? args[0] ?? await requireChangeId(ctx), ...args.slice(ctx.changeId ? 0 : 1)] };
1735
- /*
1736
- case "bulk-archive":
1737
- throw new FetError({
1738
- code: ErrorCode.InvalidArguments,
1739
- message: "OpenSpec 1.2.0 不提供 bulk-archive 顶层命令",
1740
- suggestedCommand: "逐个执行 fet archive --change <change-id>"
1741
- });
1742
- case "explore":
1743
- return { command: "explore", args: ctx.changeId ? ["--change", ctx.changeId, ...args] : args };
1744
- case "onboard":
1745
- return { command: "instructions", args: [] };
1746
- */
1747
2159
  default:
1748
2160
  return { command, args };
1749
2161
  }
@@ -1763,6 +2175,18 @@ async function withDefaultChange(ctx, args, allowWithArgs = false) {
1763
2175
  }
1764
2176
  return ["--change", await requireChangeId(ctx), ...args];
1765
2177
  }
2178
+ function extractChangeId(args) {
2179
+ for (let index = 0; index < args.length; index += 1) {
2180
+ const arg = args[index];
2181
+ if (arg === "--change") {
2182
+ return args[index + 1] ?? null;
2183
+ }
2184
+ if (arg?.startsWith("--change=")) {
2185
+ return arg.slice("--change=".length) || null;
2186
+ }
2187
+ }
2188
+ return null;
2189
+ }
1766
2190
  async function requireChangeId(ctx) {
1767
2191
  if (ctx.changeId) {
1768
2192
  return ctx.changeId;
@@ -1777,9 +2201,9 @@ async function requireChangeId(ctx) {
1777
2201
  }
1778
2202
  throw new FetError({
1779
2203
  code: "INVALID_ARGUMENTS" /* InvalidArguments */,
1780
- message: "\u8BE5\u547D\u4EE4\u9700\u8981\u660E\u786E\u7684 change",
2204
+ message: "No unambiguous OpenSpec change id was found.",
1781
2205
  details: { openChangeIds: inspection.changes },
1782
- suggestedCommand: "\u6DFB\u52A0 --change <change-id>"
2206
+ suggestedCommand: "Pass --change <change-id>."
1783
2207
  });
1784
2208
  }
1785
2209
  async function assertVerified(ctx) {
@@ -1788,7 +2212,7 @@ async function assertVerified(ctx) {
1788
2212
  if (!changeId) {
1789
2213
  throw new FetError({
1790
2214
  code: "INVALID_ARGUMENTS" /* InvalidArguments */,
1791
- message: "\u672A\u6307\u5B9A change\uFF0C\u65E0\u6CD5\u68C0\u67E5 verify \u72B6\u6001",
2215
+ message: "A change id is required before this command can check FET verification.",
1792
2216
  suggestedCommand: "fet verify --done --change <change-id>"
1793
2217
  });
1794
2218
  }
@@ -1797,7 +2221,7 @@ async function assertVerified(ctx) {
1797
2221
  if (!inspection.changes.includes(changeId)) {
1798
2222
  throw new FetError({
1799
2223
  code: "INVALID_ARGUMENTS" /* InvalidArguments */,
1800
- message: "\u6307\u5B9A\u7684 change \u4E0D\u5B58\u5728\u6216\u5DF2\u5F52\u6863",
2224
+ message: "The selected change does not exist in openspec/changes.",
1801
2225
  details: { changeId, openChangeIds: inspection.changes },
1802
2226
  suggestedCommand: "fet doctor"
1803
2227
  });
@@ -1805,7 +2229,7 @@ async function assertVerified(ctx) {
1805
2229
  if (change?.manualVerify?.status !== "declared_done") {
1806
2230
  throw new FetError({
1807
2231
  code: "STATE_CORRUPTED" /* StateCorrupted */,
1808
- message: "\u5F53\u524D change \u5C1A\u672A\u901A\u8FC7 FET verify",
2232
+ message: "This change has not been marked verified by FET.",
1809
2233
  details: { changeId },
1810
2234
  suggestedCommand: `fet verify --change ${changeId}`
1811
2235
  });
@@ -1814,8 +2238,8 @@ async function assertVerified(ctx) {
1814
2238
 
1815
2239
  // src/commands/verify.ts
1816
2240
  import { createHash } from "crypto";
1817
- import { mkdir as mkdir6, readFile as readFile11, stat as stat5 } from "fs/promises";
1818
- import { join as join14 } from "path";
2241
+ import { mkdir as mkdir7, readFile as readFile12, stat as stat5 } from "fs/promises";
2242
+ import { join as join15 } from "path";
1819
2243
  async function verifyCommand(ctx, options) {
1820
2244
  if (options.auto) {
1821
2245
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
@@ -1882,9 +2306,9 @@ async function verifyCommand(ctx, options) {
1882
2306
  async function writeInstructions(ctx, changeId) {
1883
2307
  await assertChangeExists(ctx, changeId);
1884
2308
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1885
- const dir = join14(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
1886
- const instructionsPath = join14(dir, "verify-instructions.md");
1887
- await mkdir6(dir, { recursive: true });
2309
+ const dir = join15(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
2310
+ const instructionsPath = join15(dir, "verify-instructions.md");
2311
+ await mkdir7(dir, { recursive: true });
1888
2312
  await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
1889
2313
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
1890
2314
  state.currentPhase = "verify";
@@ -1900,7 +2324,7 @@ async function writeInstructions(ctx, changeId) {
1900
2324
  async function markDone(ctx, changeId) {
1901
2325
  await assertChangeExists(ctx, changeId);
1902
2326
  const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
1903
- const instructionsPath = join14(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
2327
+ const instructionsPath = join15(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
1904
2328
  const instructions = await readInstructions(instructionsPath, changeId);
1905
2329
  const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
1906
2330
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
@@ -1936,7 +2360,7 @@ async function assertChangeExists(ctx, changeId) {
1936
2360
  async function readInstructions(path, changeId) {
1937
2361
  try {
1938
2362
  await stat5(path);
1939
- const content = await readFile11(path, "utf8");
2363
+ const content = await readFile12(path, "utf8");
1940
2364
  const fileChangeId = readFrontMatterValue(content, "changeId");
1941
2365
  if (fileChangeId !== changeId) {
1942
2366
  throw new FetError({
@@ -2013,10 +2437,10 @@ function getModelPolicyMode(env = process.env) {
2013
2437
  if (value === "off" || env.FET_SKIP_MODEL_POLICY === "1") {
2014
2438
  return "off";
2015
2439
  }
2016
- if (value === "confirm") {
2017
- return "confirm";
2440
+ if (value === "warn") {
2441
+ return "warn";
2018
2442
  }
2019
- return "warn";
2443
+ return "confirm";
2020
2444
  }
2021
2445
  function getCommandModelPolicyMismatch(command, env = process.env) {
2022
2446
  if (getModelPolicyMode(env) === "off") {
@@ -2060,23 +2484,23 @@ function formatModelPolicyMismatch(mismatch, language = "zh-CN") {
2060
2484
  function renderIdeModelPolicy(command, language = "zh-CN") {
2061
2485
  if (language === "en") {
2062
2486
  if (command === "apply") {
2063
- return "Model policy: this command is recommended to run with a high-capability/high-cost model such as GPT-5.5, GLM-5.1, GLM-5, Claude Opus, or Claude Sonnet. Treat this as advisory: do not interrupt the workflow solely to ask about switching models. Run the FET command unless the user explicitly asks to pause for a model switch, and report any FET model warning in the summary.";
2487
+ return "Model policy: this command is recommended to run with a high-capability/high-cost model such as GPT-5.5, GLM-5.1, GLM-5, Claude Opus, or Claude Sonnet. If the current IDE model is lower-cost, tell the user and ask whether to stop for a model switch or continue anyway. Continue only after the user chooses.";
2064
2488
  }
2065
- return "Model policy: this command is recommended to run with a low-cost model. If the current IDE model is GPT-5.5, GLM-5.1, GLM-5, Claude Opus, Claude Sonnet, or another high-cost model, treat it as an advisory warning: do not interrupt the workflow solely to ask about switching models. Run the FET command unless the user explicitly asks to pause for a model switch, and report any FET model warning in the summary.";
2489
+ return "Model policy: this command is recommended to run with a low-cost model. If the current IDE model is GPT-5.5, GLM-5.1, GLM-5, Claude Opus, Claude Sonnet, or another high-cost model, tell the user and ask whether to stop for a model switch or continue anyway. Continue only after the user chooses.";
2066
2490
  }
2067
2491
  if (command === "apply") {
2068
- return "\u6A21\u578B\u7B56\u7565\uFF1A\u8BE5\u547D\u4EE4\u5EFA\u8BAE\u4F7F\u7528\u9AD8\u80FD\u529B/\u9AD8\u6210\u672C\u6A21\u578B\u8FD0\u884C\uFF0C\u4F8B\u5982 GPT-5.5\u3001GLM-5.1\u3001GLM-5\u3001Claude Opus \u6216 Claude Sonnet\u3002\u6B64\u63D0\u793A\u4EC5\u4F5C\u4E3A\u5EFA\u8BAE\uFF0C\u4E0D\u8981\u4EC5\u56E0\u6A21\u578B\u7B56\u7565\u800C\u6253\u65AD\u6D41\u7A0B\u8BE2\u95EE\u662F\u5426\u5207\u6362\u6A21\u578B\uFF1B\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u5148\u5207\u6362\u6A21\u578B\uFF0C\u5426\u5219\u7EE7\u7EED\u8FD0\u884C FET \u547D\u4EE4\uFF0C\u5E76\u5728\u603B\u7ED3\u4E2D\u8BF4\u660E FET \u8F93\u51FA\u7684\u6A21\u578B\u63D0\u9192\u3002";
2492
+ return "\u6A21\u578B\u7B56\u7565\uFF1A\u8BE5\u547D\u4EE4\u5EFA\u8BAE\u4F7F\u7528\u9AD8\u80FD\u529B/\u9AD8\u6210\u672C\u6A21\u578B\u8FD0\u884C\uFF0C\u4F8B\u5982 GPT-5.5\u3001GLM-5.1\u3001GLM-5\u3001Claude Opus \u6216 Claude Sonnet\u3002\u82E5\u5F53\u524D IDE \u6A21\u578B\u80FD\u529B\u8F83\u4F4E\uFF0C\u8BF7\u544A\u77E5\u7528\u6237\u5E76\u8BE2\u95EE\u662F\u505C\u6B62\u540E\u5207\u6362\u6A21\u578B\uFF0C\u8FD8\u662F\u7EE7\u7EED\u6267\u884C\u5F53\u524D\u547D\u4EE4\uFF1B\u53EA\u6709\u5728\u7528\u6237\u9009\u62E9\u540E\u624D\u7EE7\u7EED\u3002";
2069
2493
  }
2070
- return "\u6A21\u578B\u7B56\u7565\uFF1A\u8BE5\u547D\u4EE4\u5EFA\u8BAE\u4F7F\u7528\u4F4E\u6210\u672C\u6A21\u578B\u8FD0\u884C\u3002\u82E5\u5F53\u524D IDE \u6A21\u578B\u662F GPT-5.5\u3001GLM-5.1\u3001GLM-5\u3001Claude Opus\u3001Claude Sonnet \u6216\u5176\u4ED6\u9AD8\u6210\u672C\u6A21\u578B\uFF0C\u6B64\u63D0\u793A\u4EC5\u4F5C\u4E3A\u5EFA\u8BAE\uFF1B\u4E0D\u8981\u4EC5\u56E0\u6A21\u578B\u7B56\u7565\u800C\u6253\u65AD\u6D41\u7A0B\u8BE2\u95EE\u662F\u5426\u5207\u6362\u6A21\u578B\u3002\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u5148\u5207\u6362\u6A21\u578B\uFF0C\u5426\u5219\u7EE7\u7EED\u8FD0\u884C FET \u547D\u4EE4\uFF0C\u5E76\u5728\u603B\u7ED3\u4E2D\u8BF4\u660E FET \u8F93\u51FA\u7684\u6A21\u578B\u63D0\u9192\u3002";
2494
+ return "\u6A21\u578B\u7B56\u7565\uFF1A\u8BE5\u547D\u4EE4\u5EFA\u8BAE\u4F7F\u7528\u4F4E\u6210\u672C\u6A21\u578B\u8FD0\u884C\u3002\u82E5\u5F53\u524D IDE \u6A21\u578B\u662F GPT-5.5\u3001GLM-5.1\u3001GLM-5\u3001Claude Opus\u3001Claude Sonnet \u6216\u5176\u4ED6\u9AD8\u6210\u672C\u6A21\u578B\uFF0C\u8BF7\u544A\u77E5\u7528\u6237\u5E76\u8BE2\u95EE\u662F\u505C\u6B62\u540E\u5207\u6362\u6A21\u578B\uFF0C\u8FD8\u662F\u7EE7\u7EED\u6267\u884C\u5F53\u524D\u547D\u4EE4\uFF1B\u53EA\u6709\u5728\u7528\u6237\u9009\u62E9\u540E\u624D\u7EE7\u7EED\u3002";
2071
2495
  }
2072
2496
 
2073
2497
  // src/cli/context.ts
2074
2498
  import { resolve } from "path";
2075
2499
 
2076
2500
  // src/adapters/codex/index.ts
2077
- import { mkdir as mkdir7, readFile as readFile12, stat as stat6 } from "fs/promises";
2501
+ import { mkdir as mkdir8, readFile as readFile13, stat as stat6 } from "fs/promises";
2078
2502
  import { homedir } from "os";
2079
- import { dirname as dirname7, join as join15 } from "path";
2503
+ import { dirname as dirname8, join as join16 } from "path";
2080
2504
 
2081
2505
  // src/adapters/commands.ts
2082
2506
  var FET_WORKFLOW_COMMANDS = [
@@ -2107,18 +2531,9 @@ function renderFetAdapterUsage(command, args = "[...args]") {
2107
2531
 
2108
2532
  // src/adapters/codex/templates.ts
2109
2533
  function codexGuideFile(language = DEFAULT_LANGUAGE) {
2110
- return {
2111
- path: ".codex/fet/context.md",
2112
- content: `<!-- FET:MANAGED
2113
- schemaVersion: 1
2114
- fetVersion: ${FET_VERSION}
2115
- generator: codex-adapter
2116
- adapterVersion: 1
2117
- FET:END -->
2534
+ const body = language === "en" ? `# FET For Codex
2118
2535
 
2119
- # FET For Codex
2120
-
2121
- ## \u8BED\u8A00
2536
+ ## Language
2122
2537
 
2123
2538
  ${languageInstruction(language)}
2124
2539
 
@@ -2134,7 +2549,35 @@ If GitNexus code graph context is available in the IDE or MCP tools, prefer it b
2134
2549
  Use the terminal command \`fet <command>\` as the source of truth for workflow transitions. These files are Codex-readable guidance; they do not register native slash commands.
2135
2550
 
2136
2551
  Command guides live in .codex/fet/commands/.
2137
- `
2552
+ ` : `# Codex \u7684 FET \u4F7F\u7528\u6307\u5357
2553
+
2554
+ ## \u8BED\u8A00
2555
+
2556
+ ${languageInstruction(language)}
2557
+
2558
+ \u5728 Codex \u4E2D\u6267\u884C FET \u6216 OpenSpec \u5DE5\u4F5C\u524D\uFF0C\u5148\u9605\u8BFB\uFF1A
2559
+
2560
+ - AGENTS.md
2561
+ - openspec/config.yaml
2562
+ - .codex/fet/karpathy-guidelines.md
2563
+ - \u5982\u679C\u5DF2\u9009\u62E9 change\uFF0C\u9605\u8BFB openspec/changes/<change-id>/ \u4E0B\u7684\u5F53\u524D\u4EA7\u7269
2564
+
2565
+ \u5982\u679C IDE \u6216 MCP \u5DE5\u5177\u4E2D\u53EF\u7528 GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\uFF0C\u5148\u7528\u5B83\u7F29\u5C0F\u4ED3\u5E93\u626B\u63CF\u8303\u56F4\uFF1B\u7528\u56FE\u8BC6\u522B\u76F8\u5173\u6A21\u5757\u3001\u4F9D\u8D56\u548C\u63D2\u5165\u70B9\uFF0C\u518D\u53EA\u8BFB\u53D6\u9700\u8981\u786E\u8BA4\u884C\u4E3A\u7684\u5177\u4F53\u6E90\u7801\u6587\u4EF6\u3002GitNexus \u4E0D\u53EF\u7528\u65F6\uFF0C\u6309\u666E\u901A FET/OpenSpec \u5DE5\u4F5C\u6D41\u7EE7\u7EED\u3002
2566
+
2567
+ \u5DE5\u4F5C\u6D41\u6D41\u8F6C\u4EE5\u7EC8\u7AEF\u547D\u4EE4 \`fet <command>\` \u4E3A\u51C6\u3002\u8FD9\u4E9B\u6587\u4EF6\u662F\u7ED9 Codex \u9605\u8BFB\u7684\u6307\u5BFC\uFF0C\u4E0D\u6CE8\u518C\u539F\u751F slash command\u3002
2568
+
2569
+ \u547D\u4EE4\u6307\u5357\u4F4D\u4E8E .codex/fet/commands/\u3002
2570
+ `;
2571
+ return {
2572
+ path: ".codex/fet/context.md",
2573
+ content: `<!-- FET:MANAGED
2574
+ schemaVersion: 1
2575
+ fetVersion: ${FET_VERSION}
2576
+ generator: codex-adapter
2577
+ adapterVersion: 1
2578
+ FET:END -->
2579
+
2580
+ ${body}`
2138
2581
  };
2139
2582
  }
2140
2583
  function codexCommandFiles(language = DEFAULT_LANGUAGE) {
@@ -2162,7 +2605,7 @@ generator: codex-adapter
2162
2605
  adapterVersion: 1
2163
2606
  FET:END -->
2164
2607
 
2165
- # Andrej Karpathy Inspired Coding Guidelines
2608
+ # ${language === "en" ? "Andrej Karpathy Inspired Coding Guidelines" : "\u53D7 Andrej Karpathy \u542F\u53D1\u7684\u7F16\u7801\u6307\u5357"}
2166
2609
 
2167
2610
  ${renderKarpathyGuidelinesBody(language)}
2168
2611
  `
@@ -2216,6 +2659,34 @@ After the command completes, report the important next steps from the FET output
2216
2659
  function renderCommandZh(command) {
2217
2660
  const usage = renderFetAdapterUsage(command, command === "fill-context" ? "" : command === "passthrough" ? "<openspec-command> [...args]" : "");
2218
2661
  const title = commandTitleZh(command);
2662
+ if (command === "graph-setup") {
2663
+ return `<!-- FET:MANAGED
2664
+ schemaVersion: 1
2665
+ fetVersion: ${FET_VERSION}
2666
+ generator: codex-adapter
2667
+ adapterVersion: 1
2668
+ command: ${usage}
2669
+ FET:END -->
2670
+
2671
+ # ${usage}
2672
+
2673
+ ${renderIdeModelPolicy(command, "zh-CN")}
2674
+
2675
+ ${languageInstruction("zh-CN")}
2676
+
2677
+ \u7528\u4E8E\u6307\u5BFC\u5F53\u524D IDE LLM \u5728\u7528\u6237\u786E\u8BA4\u4E0B\u5B89\u88C5 GitNexus\u3002
2678
+
2679
+ \u5148\u8FD0\u884C\uFF1A
2680
+
2681
+ \`\`\`sh
2682
+ ${usage}
2683
+ \`\`\`
2684
+
2685
+ \u7136\u540E\u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u4EE5\u5B83\u4F5C\u4E3A\u5B89\u88C5\u4EFB\u52A1\u4E66\u3002\u53EA\u8BFB\u68C0\u6D4B\u547D\u4EE4\u53EF\u4EE5\u76F4\u63A5\u8FD0\u884C\uFF1B\u4EFB\u4F55\u4F1A\u4E0B\u8F7D\u8F6F\u4EF6\u3001\u5168\u5C40\u5B89\u88C5\u3001\u4FEE\u6539 PATH\u3001\u5199\u5165\u7528\u6237\u7EA7\u6587\u4EF6\u6216\u4FEE\u6539 IDE/MCP \u914D\u7F6E\u7684\u547D\u4EE4\uFF0C\u6267\u884C\u524D\u90FD\u8981\u5C55\u793A\u5B8C\u6574\u547D\u4EE4\u5E76\u7B49\u5F85\u7528\u6237\u786E\u8BA4\u3002
2686
+
2687
+ \u5B89\u88C5\u540E\u7528 \`gitnexus --version\` \u9A8C\u8BC1\u3002\u5408\u9002\u65F6\u7EE7\u7EED\u8FD0\u884C \`fet graph init\` \u548C \`fet graph handoff\`\u3002\u5982\u679C\u5B89\u88C5\u5931\u8D25\uFF0C\u62A5\u544A\u5931\u8D25\u547D\u4EE4\u3001stderr \u548C\u4E0B\u4E00\u6B65\u4EBA\u5DE5\u5904\u7406\u5EFA\u8BAE\u3002
2688
+ `;
2689
+ }
2219
2690
  return `<!-- FET:MANAGED
2220
2691
  schemaVersion: 1
2221
2692
  fetVersion: ${FET_VERSION}
@@ -2279,6 +2750,34 @@ This preserves the FET entry point while allowing access to unmanaged or newly a
2279
2750
  }
2280
2751
  function renderGraphCommand(command, language) {
2281
2752
  const usage = renderFetAdapterUsage(command, "");
2753
+ if (command === "graph-setup") {
2754
+ return `<!-- FET:MANAGED
2755
+ schemaVersion: 1
2756
+ fetVersion: ${FET_VERSION}
2757
+ generator: codex-adapter
2758
+ adapterVersion: 1
2759
+ command: ${usage}
2760
+ FET:END -->
2761
+
2762
+ # ${usage}
2763
+
2764
+ ${renderIdeModelPolicy(command, language)}
2765
+
2766
+ ${languageInstruction(language)}
2767
+
2768
+ Use this command to guide IDE-assisted GitNexus installation with user approval.
2769
+
2770
+ Run:
2771
+
2772
+ \`\`\`sh
2773
+ ${usage}
2774
+ \`\`\`
2775
+
2776
+ Then read .fet/graph-setup.md and follow it as the source of truth. You may run read-only detection commands directly. Before downloading software, installing globally, changing PATH, writing user-level files, or modifying IDE/MCP configuration, show the exact command and wait for user approval.
2777
+
2778
+ After installation, verify \`gitnexus --version\`. If appropriate, continue with \`fet graph init\` and \`fet graph handoff\`. If installation fails, report the failing command, stderr, and next manual step.
2779
+ `;
2780
+ }
2282
2781
  const subcommand = command.slice("graph-".length);
2283
2782
  return `<!-- FET:MANAGED
2284
2783
  schemaVersion: 1
@@ -2351,6 +2850,9 @@ function renderSlashPrompt(command, language) {
2351
2850
  if (command === "passthrough") {
2352
2851
  return renderPassthroughSlashPrompt(language);
2353
2852
  }
2853
+ if (command === "graph-setup") {
2854
+ return renderGraphSetupSlashPrompt(language);
2855
+ }
2354
2856
  const usage = renderFetAdapterUsage(command);
2355
2857
  const isGraph = command.startsWith("graph-");
2356
2858
  const shellCommand = isGraph ? `${renderFetAdapterUsage(command, "")} $ARGUMENTS` : `fet ${command} $ARGUMENTS`;
@@ -2385,6 +2887,9 @@ After it completes, summarize the important FET output and next steps.
2385
2887
  `;
2386
2888
  }
2387
2889
  function renderSlashPromptZh(command) {
2890
+ if (command === "graph-setup") {
2891
+ return renderGraphSetupSlashPrompt("zh-CN");
2892
+ }
2388
2893
  const usage = renderFetAdapterUsage(command, command === "fill-context" ? "" : command === "passthrough" ? "<openspec-command> [...args]" : "[...args]");
2389
2894
  const argumentHint = command === "passthrough" ? "openspec-command [...args]" : void 0;
2390
2895
  const argumentHintLine = argumentHint ? `argument-hint: ${argumentHint}
@@ -2778,6 +3283,62 @@ Guardrails:
2778
3283
  language
2779
3284
  );
2780
3285
  }
3286
+ function renderGraphSetupSlashPrompt(language) {
3287
+ if (language === "en") {
3288
+ return renderManagedSlashPrompt(
3289
+ "fet graph setup",
3290
+ "Guide IDE-assisted GitNexus installation with user approval",
3291
+ `Guide optional GitNexus installation for this project.
3292
+
3293
+ Steps:
3294
+
3295
+ 1. Run:
3296
+ \`\`\`sh
3297
+ fet graph setup
3298
+ \`\`\`
3299
+ 2. Read .fet/graph-setup.md and follow it as the source of truth.
3300
+ 3. Run read-only detection commands as needed, such as checking the shell, PATH, package managers, and \`gitnexus --version\`.
3301
+ 4. If GitNexus is missing, use the configured \`FET_GITNEXUS_INSTALL_COMMAND\` when present; otherwise find the official GitNexus installation instructions or ask the user for the preferred method.
3302
+ 5. Before any command that downloads software, installs globally, changes PATH, writes user-level files, or modifies IDE/MCP settings, show the exact command and wait for user approval.
3303
+ 6. Verify installation with \`gitnexus --version\`.
3304
+ 7. If the user wants IDE/MCP integration, explain \`gitnexus setup\` and run it only after approval.
3305
+ 8. Run \`fet graph init\` and then \`fet graph handoff\` when GitNexus is available.
3306
+
3307
+ Guardrails:
3308
+ - GitNexus is optional; do not block FET/OpenSpec workflows if installation fails.
3309
+ - Do not modify application code during setup.
3310
+ - Report the failing command, stderr, and next manual step if blocked.`,
3311
+ void 0,
3312
+ language
3313
+ );
3314
+ }
3315
+ return renderManagedSlashPrompt(
3316
+ "fet graph setup",
3317
+ "\u6307\u5BFC IDE LLM \u5728\u7528\u6237\u786E\u8BA4\u4E0B\u5B89\u88C5 GitNexus",
3318
+ `\u4E3A\u5F53\u524D\u9879\u76EE\u5F15\u5BFC\u53EF\u9009\u7684 GitNexus \u5B89\u88C5\u3002
3319
+
3320
+ \u6B65\u9AA4\uFF1A
3321
+
3322
+ 1. \u8FD0\u884C\uFF1A
3323
+ \`\`\`sh
3324
+ fet graph setup
3325
+ \`\`\`
3326
+ 2. \u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u4EE5\u5B83\u4F5C\u4E3A\u5B89\u88C5\u4EFB\u52A1\u4E66\u3002
3327
+ 3. \u6309\u9700\u8FD0\u884C\u53EA\u8BFB\u68C0\u6D4B\u547D\u4EE4\uFF0C\u4F8B\u5982\u68C0\u67E5 shell\u3001PATH\u3001\u5305\u7BA1\u7406\u5668\u548C \`gitnexus --version\`\u3002
3328
+ 4. \u5982\u679C GitNexus \u7F3A\u5931\uFF0C\u4F18\u5148\u4F7F\u7528\u5DF2\u914D\u7F6E\u7684 \`FET_GITNEXUS_INSTALL_COMMAND\`\uFF1B\u6CA1\u6709\u914D\u7F6E\u65F6\uFF0C\u67E5\u627E GitNexus \u5B98\u65B9\u5B89\u88C5\u8BF4\u660E\uFF0C\u6216\u8BE2\u95EE\u7528\u6237\u5E0C\u671B\u4F7F\u7528\u7684\u5B89\u88C5\u65B9\u5F0F\u3002
3329
+ 5. \u4EFB\u4F55\u4F1A\u4E0B\u8F7D\u8F6F\u4EF6\u3001\u5168\u5C40\u5B89\u88C5\u3001\u4FEE\u6539 PATH\u3001\u5199\u5165\u7528\u6237\u7EA7\u6587\u4EF6\u6216\u4FEE\u6539 IDE/MCP \u8BBE\u7F6E\u7684\u547D\u4EE4\uFF0C\u6267\u884C\u524D\u90FD\u8981\u5C55\u793A\u5B8C\u6574\u547D\u4EE4\u5E76\u7B49\u5F85\u7528\u6237\u786E\u8BA4\u3002
3330
+ 6. \u7528 \`gitnexus --version\` \u9A8C\u8BC1\u5B89\u88C5\u3002
3331
+ 7. \u5982\u679C\u7528\u6237\u9700\u8981 IDE/MCP \u96C6\u6210\uFF0C\u5148\u8BF4\u660E \`gitnexus setup\`\uFF0C\u83B7\u5F97\u786E\u8BA4\u540E\u518D\u8FD0\u884C\u3002
3332
+ 8. GitNexus \u53EF\u7528\u540E\u8FD0\u884C \`fet graph init\`\uFF0C\u518D\u8FD0\u884C \`fet graph handoff\`\u3002
3333
+
3334
+ \u7EA6\u675F\uFF1A
3335
+ - GitNexus \u662F\u53EF\u9009\u80FD\u529B\uFF1B\u5B89\u88C5\u5931\u8D25\u65F6\u4E0D\u8981\u963B\u585E FET/OpenSpec \u4E3B\u6D41\u7A0B\u3002
3336
+ - \u5B89\u88C5\u8FC7\u7A0B\u4E2D\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
3337
+ - \u5982\u679C\u53D7\u963B\uFF0C\u62A5\u544A\u5931\u8D25\u547D\u4EE4\u3001stderr \u548C\u4E0B\u4E00\u6B65\u4EBA\u5DE5\u5904\u7406\u5EFA\u8BAE\u3002`,
3338
+ void 0,
3339
+ language
3340
+ );
3341
+ }
2781
3342
  function renderExploreSlashPrompt(language) {
2782
3343
  return renderManagedSlashPrompt(
2783
3344
  "fet explore [...args]",
@@ -2897,6 +3458,7 @@ function renderManagedSlashPrompt(command, description, body, argumentHint, lang
2897
3458
  const policyCommand = command.split(/\s+/)[1] ?? command;
2898
3459
  const argumentHintLine = argumentHint ? `argument-hint: ${argumentHint}
2899
3460
  ` : "";
3461
+ const graphContextInstruction = language === "en" ? "If GitNexus graph context is available, consult it before broad source scans and use it to narrow the files you read. If it is unavailable, continue normally." : "\u5982\u679C GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u53EF\u7528\uFF0C\u5148\u7528\u5B83\u7F29\u5C0F\u6E90\u7801\u626B\u63CF\u8303\u56F4\uFF1B\u4E0D\u53EF\u7528\u65F6\u6309\u666E\u901A\u6D41\u7A0B\u7EE7\u7EED\u3002";
2900
3462
  return `<!-- FET:MANAGED
2901
3463
  schemaVersion: 1
2902
3464
  fetVersion: ${FET_VERSION}
@@ -2913,7 +3475,7 @@ ${renderIdeModelPolicy(policyCommand, language)}
2913
3475
 
2914
3476
  ${languageInstruction(language)}
2915
3477
 
2916
- If GitNexus graph context is available, consult it before broad source scans and use it to narrow the files you read. If it is unavailable, continue normally.
3478
+ ${graphContextInstruction}
2917
3479
 
2918
3480
  ${body}
2919
3481
  `;
@@ -2925,7 +3487,7 @@ var CodexAdapter = class {
2925
3487
  adapterVersion = 1;
2926
3488
  async detect(projectRoot) {
2927
3489
  return {
2928
- detected: await exists3(join15(projectRoot, ".codex")) || await exists3(join15(projectRoot, "AGENTS.md")),
3490
+ detected: await exists3(join16(projectRoot, ".codex")) || await exists3(join16(projectRoot, "AGENTS.md")),
2929
3491
  reason: "Codex adapter is available for projects that use AGENTS.md"
2930
3492
  };
2931
3493
  }
@@ -2964,7 +3526,7 @@ var CodexAdapter = class {
2964
3526
  if (existing && !existing.includes("FET:MANAGED") && force) {
2965
3527
  await createBackup(target);
2966
3528
  }
2967
- await mkdir7(dirname7(target), { recursive: true });
3529
+ await mkdir8(dirname8(target), { recursive: true });
2968
3530
  await atomicWrite(target, file.content);
2969
3531
  written.push(displayPath);
2970
3532
  }
@@ -2991,9 +3553,9 @@ var CodexAdapter = class {
2991
3553
  };
2992
3554
  function resolveTarget(projectRoot, file) {
2993
3555
  if (file.root === "codex-home") {
2994
- return join15(resolveCodexHome(), file.path);
3556
+ return join16(resolveCodexHome(), file.path);
2995
3557
  }
2996
- return join15(projectRoot, file.path);
3558
+ return join16(projectRoot, file.path);
2997
3559
  }
2998
3560
  function displayPathFor(file) {
2999
3561
  if (file.root === "codex-home") {
@@ -3002,11 +3564,11 @@ function displayPathFor(file) {
3002
3564
  return file.path;
3003
3565
  }
3004
3566
  function resolveCodexHome() {
3005
- return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join15(homedir(), ".codex");
3567
+ return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join16(homedir(), ".codex");
3006
3568
  }
3007
3569
  async function readExisting(path) {
3008
3570
  try {
3009
- return await readFile12(path, "utf8");
3571
+ return await readFile13(path, "utf8");
3010
3572
  } catch {
3011
3573
  return null;
3012
3574
  }
@@ -3021,8 +3583,8 @@ async function exists3(path) {
3021
3583
  }
3022
3584
 
3023
3585
  // src/adapters/cursor/index.ts
3024
- import { mkdir as mkdir8, readFile as readFile13, stat as stat7 } from "fs/promises";
3025
- import { dirname as dirname8, join as join16 } from "path";
3586
+ import { mkdir as mkdir9, readFile as readFile14, stat as stat7 } from "fs/promises";
3587
+ import { dirname as dirname9, join as join17 } from "path";
3026
3588
 
3027
3589
  // src/adapters/cursor/templates.ts
3028
3590
  function cursorSkillFiles(language = DEFAULT_LANGUAGE) {
@@ -3088,6 +3650,36 @@ ${languageInstruction(language)}
3088
3650
  - openspec/config.yaml
3089
3651
 
3090
3652
  \u68C0\u67E5 README\u3001package scripts\u3001\u8DEF\u7531\u3001\u6D4B\u8BD5\u3001\u6E90\u7801\u7ED3\u6784\u548C\u73B0\u6709\u7EA6\u5B9A\u540E\uFF0C\u628A AGENTS.md \u4E2D\u6BCF\u4E2A \`[NEEDS LLM INPUT]\` \u6216 \`[NEED LLM INPUT]\` \u5360\u4F4D\u7B26\u66FF\u6362\u4E3A\u5177\u4F53\u3001\u7B80\u6D01\u3001\u9879\u76EE\u76F8\u5173\u7684\u5185\u5BB9\u3002\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
3653
+ `;
3654
+ }
3655
+ if (command === "graph-setup") {
3656
+ return `<!-- FET:MANAGED
3657
+ schemaVersion: 1
3658
+ fetVersion: ${FET_VERSION}
3659
+ generator: cursor-adapter
3660
+ adapterVersion: 1
3661
+ command: fet graph setup
3662
+ FET:END -->
3663
+
3664
+ ---
3665
+ name: fet-graph-setup
3666
+ description: ${language === "en" ? "Guide IDE-assisted GitNexus installation with user approval" : "\u6307\u5BFC IDE LLM \u5728\u7528\u6237\u786E\u8BA4\u4E0B\u5B89\u88C5 GitNexus"}
3667
+ disable-model-invocation: false
3668
+ ---
3669
+
3670
+ ${renderIdeModelPolicy(command, language)}
3671
+
3672
+ ${languageInstruction(language)}
3673
+
3674
+ ${language === "en" ? `Run \`fet graph setup\`, then read .fet/graph-setup.md and follow it as an installation playbook.
3675
+
3676
+ You may run read-only detection commands without extra confirmation. Before any command that downloads software, installs globally, changes PATH, writes user-level config, or modifies IDE/MCP settings, show the exact command and wait for user approval.
3677
+
3678
+ After installation, verify with \`gitnexus --version\`, then run \`fet graph init\` and \`fet graph handoff\` when appropriate. If installation fails, summarize the failing command and next manual step.` : `\u5148\u8FD0\u884C \`fet graph setup\`\uFF0C\u518D\u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u628A\u5B83\u4F5C\u4E3A\u5B89\u88C5\u4EFB\u52A1\u4E66\u6267\u884C\u3002
3679
+
3680
+ \u53EA\u8BFB\u68C0\u6D4B\u547D\u4EE4\u53EF\u4EE5\u76F4\u63A5\u8FD0\u884C\u3002\u4EFB\u4F55\u4F1A\u4E0B\u8F7D\u8F6F\u4EF6\u3001\u5168\u5C40\u5B89\u88C5\u3001\u4FEE\u6539 PATH\u3001\u5199\u5165\u7528\u6237\u7EA7\u914D\u7F6E\u6216\u4FEE\u6539 IDE/MCP \u8BBE\u7F6E\u7684\u547D\u4EE4\uFF0C\u6267\u884C\u524D\u90FD\u8981\u5C55\u793A\u5B8C\u6574\u547D\u4EE4\u5E76\u7B49\u5F85\u7528\u6237\u786E\u8BA4\u3002
3681
+
3682
+ \u5B89\u88C5\u540E\u7528 \`gitnexus --version\` \u9A8C\u8BC1\uFF1B\u5408\u9002\u65F6\u7EE7\u7EED\u8FD0\u884C \`fet graph init\` \u548C \`fet graph handoff\`\u3002\u5982\u679C\u5B89\u88C5\u5931\u8D25\uFF0C\u6C47\u603B\u5931\u8D25\u547D\u4EE4\u548C\u4E0B\u4E00\u6B65\u4EBA\u5DE5\u5904\u7406\u5EFA\u8BAE\u3002`}
3091
3683
  `;
3092
3684
  }
3093
3685
  return `<!-- FET:MANAGED
@@ -3126,7 +3718,7 @@ var CursorAdapter = class {
3126
3718
  adapterVersion = 1;
3127
3719
  async detect(projectRoot) {
3128
3720
  return {
3129
- detected: await exists4(join16(projectRoot, ".cursor")),
3721
+ detected: await exists4(join17(projectRoot, ".cursor")),
3130
3722
  reason: "Cursor adapter is available for any project"
3131
3723
  };
3132
3724
  }
@@ -3143,7 +3735,7 @@ var CursorAdapter = class {
3143
3735
  const written = [];
3144
3736
  const skipped = [];
3145
3737
  for (const file of plan.files) {
3146
- const target = join16(projectRoot, file.path);
3738
+ const target = join17(projectRoot, file.path);
3147
3739
  const existing = await readExisting2(target);
3148
3740
  if (existing && !existing.includes("FET:MANAGED") && !force) {
3149
3741
  throw new FetError({
@@ -3156,7 +3748,7 @@ var CursorAdapter = class {
3156
3748
  if (existing && !existing.includes("FET:MANAGED") && force) {
3157
3749
  await createBackup(target);
3158
3750
  }
3159
- await mkdir8(dirname8(target), { recursive: true });
3751
+ await mkdir9(dirname9(target), { recursive: true });
3160
3752
  await atomicWrite(target, file.content);
3161
3753
  written.push(file.path);
3162
3754
  }
@@ -3166,7 +3758,7 @@ var CursorAdapter = class {
3166
3758
  const plan = await this.planInstall(projectRoot);
3167
3759
  const checks = [];
3168
3760
  for (const file of plan.files) {
3169
- const target = join16(projectRoot, file.path);
3761
+ const target = join17(projectRoot, file.path);
3170
3762
  const content = await readExisting2(target);
3171
3763
  const managed = Boolean(content?.includes("FET:MANAGED"));
3172
3764
  const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
@@ -3182,7 +3774,7 @@ var CursorAdapter = class {
3182
3774
  };
3183
3775
  async function readExisting2(path) {
3184
3776
  try {
3185
- return await readFile13(path, "utf8");
3777
+ return await readFile14(path, "utf8");
3186
3778
  } catch {
3187
3779
  return null;
3188
3780
  }
@@ -3201,13 +3793,13 @@ import { execFile as execFile4 } from "child_process";
3201
3793
  import { promisify as promisify4 } from "util";
3202
3794
 
3203
3795
  // src/openspec/inspector.ts
3204
- import { readdir, stat as stat8 } from "fs/promises";
3205
- import { join as join17 } from "path";
3796
+ import { readdir as readdir2, stat as stat8 } from "fs/promises";
3797
+ import { join as join18 } from "path";
3206
3798
  async function inspectOpenSpecProject(projectRoot) {
3207
- const openspecPath = join17(projectRoot, "openspec");
3208
- const changesPath = join17(openspecPath, "changes");
3209
- const legacyArchivePath = join17(openspecPath, "archive");
3210
- const changesArchivePath = join17(changesPath, "archive");
3799
+ const openspecPath = join18(projectRoot, "openspec");
3800
+ const changesPath = join18(openspecPath, "changes");
3801
+ const legacyArchivePath = join18(openspecPath, "archive");
3802
+ const changesArchivePath = join18(changesPath, "archive");
3211
3803
  return {
3212
3804
  exists: await exists5(openspecPath),
3213
3805
  changes: await listDirectories(changesPath, { exclude: ["archive"] }),
@@ -3215,13 +3807,13 @@ async function inspectOpenSpecProject(projectRoot) {
3215
3807
  };
3216
3808
  }
3217
3809
  async function inspectOpenSpecChange(projectRoot, changeId) {
3218
- const changePath = join17(projectRoot, "openspec", "changes", changeId);
3219
- const tasksPath = join17(changePath, "tasks.md");
3220
- const specsPath = join17(changePath, "specs");
3810
+ const changePath = join18(projectRoot, "openspec", "changes", changeId);
3811
+ const tasksPath = join18(changePath, "tasks.md");
3812
+ const specsPath = join18(changePath, "specs");
3221
3813
  return {
3222
3814
  changeId,
3223
3815
  exists: await exists5(changePath),
3224
- hasProposal: await exists5(join17(changePath, "proposal.md")),
3816
+ hasProposal: await exists5(join18(changePath, "proposal.md")),
3225
3817
  hasTasks: await exists5(tasksPath),
3226
3818
  hasSpecs: await exists5(specsPath),
3227
3819
  tasksPath,
@@ -3230,7 +3822,7 @@ async function inspectOpenSpecChange(projectRoot, changeId) {
3230
3822
  }
3231
3823
  async function listDirectories(path, options = {}) {
3232
3824
  try {
3233
- const entries = await readdir(path, { withFileTypes: true });
3825
+ const entries = await readdir2(path, { withFileTypes: true });
3234
3826
  const excluded = new Set(options.exclude ?? []);
3235
3827
  return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
3236
3828
  } catch {
@@ -3398,12 +3990,12 @@ function parseCommands(help) {
3398
3990
  }
3399
3991
 
3400
3992
  // src/scanner/package.ts
3401
- import { readFile as readFile14, stat as stat9 } from "fs/promises";
3402
- import { join as join18 } from "path";
3993
+ import { readFile as readFile15, stat as stat9 } from "fs/promises";
3994
+ import { join as join19 } from "path";
3403
3995
  import { parse as parse2 } from "yaml";
3404
3996
  async function readPackageJson(projectRoot) {
3405
3997
  try {
3406
- return JSON.parse(await readFile14(join18(projectRoot, "package.json"), "utf8"));
3998
+ return JSON.parse(await readFile15(join19(projectRoot, "package.json"), "utf8"));
3407
3999
  } catch {
3408
4000
  return null;
3409
4001
  }
@@ -3469,7 +4061,7 @@ function detectFramework(pkg) {
3469
4061
  }
3470
4062
  async function detectLanguage(projectRoot, pkg) {
3471
4063
  const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
3472
- if (deps.typescript || await exists6(join18(projectRoot, "tsconfig.json"))) {
4064
+ if (deps.typescript || await exists6(join19(projectRoot, "tsconfig.json"))) {
3473
4065
  return "typescript";
3474
4066
  }
3475
4067
  return "javascript";
@@ -3484,7 +4076,7 @@ async function detectWorkspaces(projectRoot, pkg) {
3484
4076
  return packageWorkspaces;
3485
4077
  }
3486
4078
  try {
3487
- const workspace = parse2(await readFile14(join18(projectRoot, "pnpm-workspace.yaml"), "utf8"));
4079
+ const workspace = parse2(await readFile15(join19(projectRoot, "pnpm-workspace.yaml"), "utf8"));
3488
4080
  return (workspace?.packages ?? []).map((path) => ({
3489
4081
  name: path,
3490
4082
  path,
@@ -3504,7 +4096,7 @@ async function detectLockManagers(projectRoot) {
3504
4096
  ];
3505
4097
  const found = [];
3506
4098
  for (const [file, manager] of lockFiles) {
3507
- if (await exists6(join18(projectRoot, file))) {
4099
+ if (await exists6(join19(projectRoot, file))) {
3508
4100
  found.push(manager);
3509
4101
  }
3510
4102
  }
@@ -3529,13 +4121,13 @@ async function exists6(path) {
3529
4121
  }
3530
4122
 
3531
4123
  // src/scanner/routes.ts
3532
- import { readdir as readdir2, stat as stat10 } from "fs/promises";
3533
- import { join as join19, relative, sep } from "path";
4124
+ import { readdir as readdir3, stat as stat10 } from "fs/promises";
4125
+ import { join as join20, relative, sep } from "path";
3534
4126
  async function scanRoutes(projectRoot) {
3535
4127
  const candidates = ["src/routes", "src/pages", "app", "pages"];
3536
4128
  const routes = [];
3537
4129
  for (const candidate of candidates) {
3538
- const root = join19(projectRoot, candidate);
4130
+ const root = join20(projectRoot, candidate);
3539
4131
  if (!await exists7(root)) {
3540
4132
  continue;
3541
4133
  }
@@ -3560,10 +4152,10 @@ function inferRoutePath(relativePath) {
3560
4152
  return `/${withoutIndex}`.replace(/\/+/g, "/");
3561
4153
  }
3562
4154
  async function listFiles(root) {
3563
- const entries = await readdir2(root, { withFileTypes: true });
4155
+ const entries = await readdir3(root, { withFileTypes: true });
3564
4156
  const files = [];
3565
4157
  for (const entry of entries) {
3566
- const path = join19(root, entry.name);
4158
+ const path = join20(root, entry.name);
3567
4159
  if (entry.isDirectory()) {
3568
4160
  files.push(...await listFiles(path));
3569
4161
  } else {
@@ -3805,7 +4397,7 @@ function renderModelPolicyActionHint(policyMode, language) {
3805
4397
  if (policyMode === "confirm") {
3806
4398
  return language === "en" ? "Choose whether to continue this command or stop and switch models." : "\u8BF7\u9009\u62E9\u7EE7\u7EED\u6267\u884C\u672C\u547D\u4EE4\uFF0C\u6216\u505C\u6B62\u540E\u624B\u52A8\u5207\u6362\u6A21\u578B\u3002";
3807
4399
  }
3808
- return language === "en" ? "This is advisory; the command will continue. Set FET_MODEL_POLICY=confirm if you want an explicit stop/continue prompt." : "\u8BE5\u63D0\u9192\u4EC5\u4F5C\u4E3A\u5EFA\u8BAE\uFF0C\u547D\u4EE4\u4F1A\u7EE7\u7EED\u6267\u884C\u3002\u5982\u9700\u663E\u5F0F\u9009\u62E9\u505C\u6B62\u6216\u7EE7\u7EED\uFF0C\u53EF\u8BBE\u7F6E FET_MODEL_POLICY=confirm\u3002";
4400
+ return language === "en" ? "This is advisory because FET_MODEL_POLICY=warn; the command will continue." : "\u5F53\u524D\u8BBE\u7F6E FET_MODEL_POLICY=warn\uFF0C\u8BE5\u63D0\u9192\u4EC5\u4F5C\u4E3A\u5EFA\u8BAE\uFF0C\u547D\u4EE4\u4F1A\u7EE7\u7EED\u6267\u884C\u3002";
3809
4401
  }
3810
4402
  async function warnIfContextPlaceholdersRemain(ctx) {
3811
4403
  if (["init", "update-context", "fill-context", "doctor"].includes(ctx.command)) {