@nick848/fet 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2,14 +2,15 @@
2
2
  import {
3
3
  FetError,
4
4
  toFetError
5
- } from "../chunk-FZOVNHE7.js";
5
+ } from "../chunk-V4ZRBF5L.js";
6
6
 
7
7
  // src/cli/index.ts
8
+ import { createInterface } from "readline/promises";
8
9
  import { Command } from "commander";
9
10
 
10
11
  // src/commands/init.ts
11
- import { readFile as readFile5, stat as stat2 } from "fs/promises";
12
- import { join as join6 } from "path";
12
+ import { readFile as readFile6, stat as stat3 } from "fs/promises";
13
+ import { join as join8 } from "path";
13
14
 
14
15
  // src/fs/atomic-write.ts
15
16
  import { dirname } from "path";
@@ -115,16 +116,130 @@ async function writeInitJournal(projectRoot, journal) {
115
116
  );
116
117
  }
117
118
 
119
+ // src/gitnexus.ts
120
+ import { execFile } from "child_process";
121
+ import { stat as stat2 } from "fs/promises";
122
+ import { join as join4 } from "path";
123
+ import { promisify } from "util";
124
+ var execFileAsync = promisify(execFile);
125
+ var DEFAULT_GRAPH_PATH = ".gitnexus";
126
+ async function detectGitNexus(env = process.env) {
127
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
128
+ const command = resolveGitNexusCommand(env);
129
+ try {
130
+ const { stdout, stderr } = await execFileAsync(command.file, [...command.args, "--version"], { shell: process.platform === "win32" });
131
+ return {
132
+ installed: true,
133
+ executablePath: command.label,
134
+ version: (stdout.trim() || stderr.trim() || "unknown").split(/\r?\n/)[0] ?? "unknown",
135
+ checkedAt
136
+ };
137
+ } catch (error) {
138
+ return {
139
+ installed: false,
140
+ executablePath: command.label,
141
+ version: null,
142
+ checkedAt,
143
+ error: error instanceof Error ? error.message : String(error)
144
+ };
145
+ }
146
+ }
147
+ function toGitNexusState(detection, previous) {
148
+ return {
149
+ provider: "gitnexus",
150
+ installed: detection.installed,
151
+ executablePath: detection.installed ? detection.executablePath : null,
152
+ version: detection.version,
153
+ checkedAt: detection.checkedAt,
154
+ recommendationShownAt: previous?.recommendationShownAt ?? null,
155
+ graphPath: previous?.graphPath ?? null,
156
+ graphExists: previous?.graphExists ?? false,
157
+ lastIndexedAt: previous?.lastIndexedAt ?? null,
158
+ lastRefreshAt: previous?.lastRefreshAt ?? null,
159
+ lastStatus: previous?.lastStatus ?? null,
160
+ setupHandoffPath: previous?.setupHandoffPath ?? null,
161
+ setupHandoffUpdatedAt: previous?.setupHandoffUpdatedAt ?? null,
162
+ handoffPath: previous?.handoffPath ?? null,
163
+ handoffUpdatedAt: previous?.handoffUpdatedAt ?? null
164
+ };
165
+ }
166
+ async function inspectGitNexusGraph(projectRoot, env = process.env) {
167
+ const relative2 = env.FET_GITNEXUS_GRAPH_PATH?.trim() || DEFAULT_GRAPH_PATH;
168
+ const graphPath = join4(projectRoot, relative2);
169
+ try {
170
+ const info = await stat2(graphPath);
171
+ return {
172
+ graphPath: relative2,
173
+ graphExists: true,
174
+ lastIndexedAt: info.mtime.toISOString()
175
+ };
176
+ } catch {
177
+ return {
178
+ graphPath: relative2,
179
+ graphExists: false,
180
+ lastIndexedAt: null
181
+ };
182
+ }
183
+ }
184
+ async function runGitNexus(args, options) {
185
+ const command = resolveGitNexusCommand(options.env ?? process.env);
186
+ const fullCommand = [command.file, ...command.args, ...args];
187
+ try {
188
+ const { stdout, stderr } = await execFileAsync(command.file, [...command.args, ...args], {
189
+ cwd: options.cwd,
190
+ shell: process.platform === "win32"
191
+ });
192
+ return {
193
+ exitCode: 0,
194
+ stdout,
195
+ stderr,
196
+ command: fullCommand
197
+ };
198
+ } catch (error) {
199
+ const maybe = error;
200
+ return {
201
+ exitCode: typeof maybe.code === "number" ? maybe.code : 1,
202
+ stdout: maybe.stdout ?? "",
203
+ stderr: maybe.stderr ?? (error instanceof Error ? error.message : String(error)),
204
+ command: fullCommand
205
+ };
206
+ }
207
+ }
208
+ function mergeGitNexusGraphInfo(state, graph2) {
209
+ return {
210
+ ...state,
211
+ graphPath: graph2.graphPath,
212
+ graphExists: graph2.graphExists,
213
+ lastIndexedAt: graph2.lastIndexedAt
214
+ };
215
+ }
216
+ function renderGitNexusRecommendation(state) {
217
+ if (state.installed) {
218
+ return `Optional GitNexus detected (${state.version ?? "unknown"}). You can generate a code graph after init; future OpenSpec artifacts should prefer the graph when it is available.`;
219
+ }
220
+ return "Optional GitNexus code graph support is not installed. Consider installing GitNexus later to speed up OpenSpec artifact generation and improve code insertion context.";
221
+ }
222
+ function resolveGitNexusCommand(env) {
223
+ const raw = env.FET_GITNEXUS_COMMAND?.trim() || env.FET_GITNEXUS_EXECUTABLE?.trim() || "gitnexus";
224
+ const parts = splitCommand(raw);
225
+ const [file = "gitnexus", ...args] = parts;
226
+ return { file, args, label: raw };
227
+ }
228
+ function splitCommand(value) {
229
+ const matches = value.match(/"[^"]+"|'[^']+'|\S+/g);
230
+ return (matches?.length ? matches : [value]).map((part) => part.replace(/^["']|["']$/g, ""));
231
+ }
232
+
118
233
  // src/version.ts
119
234
  import { existsSync, readFileSync } from "fs";
120
- import { dirname as dirname4, join as join4, parse } from "path";
235
+ import { dirname as dirname4, join as join5, parse } from "path";
121
236
  import { fileURLToPath } from "url";
122
237
  var FET_VERSION = readPackageVersion();
123
238
  function readPackageVersion() {
124
239
  let currentDir = dirname4(fileURLToPath(import.meta.url));
125
240
  const root = parse(currentDir).root;
126
241
  while (true) {
127
- const packageJsonPath = join4(currentDir, "package.json");
242
+ const packageJsonPath = join5(currentDir, "package.json");
128
243
  if (existsSync(packageJsonPath)) {
129
244
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
130
245
  if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
@@ -144,6 +259,7 @@ var AUTO_BEGIN = "<!-- FET:BEGIN AUTO -->";
144
259
  var AUTO_END = "<!-- FET:END AUTO -->";
145
260
  var USER_BEGIN = "<!-- FET:BEGIN USER -->";
146
261
  var USER_END = "<!-- FET:END USER -->";
262
+ var LLM_PLACEHOLDER_PATTERN = /\[NEEDS? LLM INPUT\]/;
147
263
  function hasManagedAutoRegion(content) {
148
264
  return count(content, AUTO_BEGIN) === 1 && count(content, AUTO_END) === 1 && content.indexOf(AUTO_BEGIN) < content.indexOf(AUTO_END);
149
265
  }
@@ -163,11 +279,41 @@ function replaceManagedRegion(existing, generated) {
163
279
  }
164
280
  const before = existing.slice(0, start);
165
281
  const after = existing.slice(end + AUTO_END.length);
282
+ const existingAuto = extractAuto(existing);
166
283
  const generatedAuto = extractAuto(generated);
167
284
  return `${before}${AUTO_BEGIN}
168
- ${generatedAuto}
285
+ ${mergeAutoRegion(existingAuto, generatedAuto)}
169
286
  ${AUTO_END}${after}`;
170
287
  }
288
+ function mergeAutoRegion(existingAuto, generatedAuto) {
289
+ const generatedSections = splitMarkdownSections(generatedAuto);
290
+ const existingSections = new Map(splitMarkdownSections(existingAuto).map((section) => [section.heading, section]));
291
+ if (!generatedSections.length || !existingSections.size) {
292
+ return generatedAuto;
293
+ }
294
+ return generatedSections.map((section) => {
295
+ const existing = existingSections.get(section.heading);
296
+ if (existing && LLM_PLACEHOLDER_PATTERN.test(section.body) && !LLM_PLACEHOLDER_PATTERN.test(existing.body)) {
297
+ return existing.raw.trim();
298
+ }
299
+ return section.raw.trim();
300
+ }).join("\n\n");
301
+ }
302
+ function splitMarkdownSections(content) {
303
+ const matches = [...content.matchAll(/^## .+$/gm)];
304
+ if (!matches.length) {
305
+ return [];
306
+ }
307
+ return matches.map((match, index) => {
308
+ const start = match.index ?? 0;
309
+ const end = matches[index + 1]?.index ?? content.length;
310
+ const raw = content.slice(start, end).trim();
311
+ const newline = raw.indexOf("\n");
312
+ const heading = newline === -1 ? raw.trim() : raw.slice(0, newline).trim();
313
+ const body = newline === -1 ? "" : raw.slice(newline + 1).trim();
314
+ return { heading, body, raw };
315
+ });
316
+ }
171
317
  function extractAuto(content) {
172
318
  const start = content.indexOf(AUTO_BEGIN);
173
319
  const end = content.indexOf(AUTO_END);
@@ -303,7 +449,8 @@ var RULES = [
303
449
  "openspec/.fet.lock",
304
450
  "openspec/.fet-init-journal.json",
305
451
  "openspec/changes/*/fet-state.json",
306
- "openspec/changes/*/.fet/"
452
+ "openspec/changes/*/.fet/",
453
+ ".gitnexus/"
307
454
  ];
308
455
  function mergeGitignore(existing) {
309
456
  const block = `${BEGIN}
@@ -325,8 +472,8 @@ ${block}
325
472
  }
326
473
 
327
474
  // src/commands/update-context.ts
328
- import { readFile as readFile4 } from "fs/promises";
329
- import { join as join5 } from "path";
475
+ import { readFile as readFile5 } from "fs/promises";
476
+ import { join as join7 } from "path";
330
477
 
331
478
  // src/config/yaml.ts
332
479
  import { readFile as readFile3 } from "fs/promises";
@@ -345,23 +492,43 @@ async function mergeFetConfig(configPath, renderedFetYaml) {
345
492
  return doc.toString();
346
493
  }
347
494
 
495
+ // src/context-placeholders.ts
496
+ import { readFile as readFile4 } from "fs/promises";
497
+ import { join as join6 } from "path";
498
+ var AGENTS_LLM_PLACEHOLDER_PATTERN = /\[NEEDS? LLM INPUT\]/g;
499
+ async function countAgentsLlmPlaceholders(projectRoot) {
500
+ try {
501
+ const content = await readFile4(join6(projectRoot, "AGENTS.md"), "utf8");
502
+ return [...content.matchAll(AGENTS_LLM_PLACEHOLDER_PATTERN)].length;
503
+ } catch {
504
+ return 0;
505
+ }
506
+ }
507
+ function renderAgentsPlaceholderWarning(count2) {
508
+ return `AGENTS.md still contains ${count2} LLM placeholder(s). Run fet fill-context first so your IDE AI can replace them. Continuing current command.`;
509
+ }
510
+
348
511
  // src/commands/update-context.ts
349
512
  async function updateContextCommand(ctx) {
513
+ let contextResult = { warnings: [] };
350
514
  await withProjectLock(
351
515
  ctx.projectRoot,
352
516
  { command: "update-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
353
- async () => updateContextFiles(ctx)
517
+ async () => {
518
+ contextResult = await updateContextFiles(ctx);
519
+ }
354
520
  );
355
521
  ctx.output.result({
356
522
  ok: true,
357
523
  command: "update-context",
358
- summary: "\u5DF2\u66F4\u65B0 AGENTS.md \u4E0E openspec/config.yaml \u7684 FET \u6258\u7BA1\u533A\u57DF\u3002"
524
+ summary: "\u5DF2\u66F4\u65B0 AGENTS.md \u4E0E openspec/config.yaml \u7684 FET \u6258\u7BA1\u533A\u57DF\u3002",
525
+ warnings: contextResult.warnings
359
526
  });
360
527
  }
361
528
  async function updateContextFiles(ctx) {
362
529
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
363
- const agentsPath = join5(ctx.projectRoot, "AGENTS.md");
364
- const configPath = join5(ctx.projectRoot, "openspec", "config.yaml");
530
+ const agentsPath = join7(ctx.projectRoot, "AGENTS.md");
531
+ const configPath = join7(ctx.projectRoot, "openspec", "config.yaml");
365
532
  const existingAgents = await readOptional(agentsPath);
366
533
  const warnings = [...scan.warnings];
367
534
  if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
@@ -388,6 +555,10 @@ async function updateContextFiles(ctx) {
388
555
  }
389
556
  await atomicWrite(agentsPath, replaceManagedRegion(existingAgents, renderAgentsMd(scan)));
390
557
  await atomicWrite(configPath, await mergeFetConfig(configPath, renderFetConfig(scan)));
558
+ const placeholderCount = await countAgentsLlmPlaceholders(ctx.projectRoot);
559
+ if (placeholderCount > 0) {
560
+ warnings.push(renderAgentsPlaceholderWarning(placeholderCount));
561
+ }
391
562
  const state = await ctx.stateStore.getOrCreateGlobal();
392
563
  state.context = {
393
564
  agentsMdUpdatedAt: scan.generatedAt,
@@ -399,7 +570,7 @@ async function updateContextFiles(ctx) {
399
570
  }
400
571
  async function readOptional(path) {
401
572
  try {
402
- return await readFile4(path, "utf8");
573
+ return await readFile5(path, "utf8");
403
574
  } catch {
404
575
  return null;
405
576
  }
@@ -407,7 +578,8 @@ async function readOptional(path) {
407
578
 
408
579
  // src/commands/init.ts
409
580
  async function initCommand(ctx) {
410
- const alreadyInitialized = await exists(join6(ctx.projectRoot, "openspec", "config.yaml"));
581
+ const alreadyInitialized = await exists(join8(ctx.projectRoot, "openspec", "config.yaml"));
582
+ let warnings = [];
411
583
  await withProjectLock(
412
584
  ctx.projectRoot,
413
585
  { command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
@@ -423,9 +595,20 @@ async function initCommand(ctx) {
423
595
  }
424
596
  }
425
597
  const contextResult = await updateContextFiles(ctx);
598
+ warnings = contextResult.warnings;
426
599
  await ensureGitignore(ctx);
427
600
  const state = await ctx.stateStore.getOrCreateGlobal();
428
601
  state.openspec = identity;
602
+ state.graph ??= {};
603
+ const gitnexus = mergeGitNexusGraphInfo(
604
+ toGitNexusState(await detectGitNexus(), state.graph.gitnexus),
605
+ await inspectGitNexusGraph(ctx.projectRoot)
606
+ );
607
+ if (!gitnexus.installed && !gitnexus.recommendationShownAt) {
608
+ warnings.push(renderGitNexusRecommendation(gitnexus));
609
+ gitnexus.recommendationShownAt = (/* @__PURE__ */ new Date()).toISOString();
610
+ }
611
+ state.graph.gitnexus = gitnexus;
429
612
  for (const adapter of ctx.toolAdapters) {
430
613
  const plan = await adapter.planInstall(ctx.projectRoot);
431
614
  const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
@@ -439,33 +622,31 @@ async function initCommand(ctx) {
439
622
  journal.completedAt = (/* @__PURE__ */ new Date()).toISOString();
440
623
  await writeInitJournal(ctx.projectRoot, journal);
441
624
  await ctx.stateStore.writeGlobal(state);
442
- for (const warning of contextResult.warnings) {
443
- ctx.output.warn(warning);
444
- }
445
625
  }
446
626
  );
447
627
  ctx.output.result({
448
628
  ok: true,
449
629
  command: "init",
450
630
  summary: "FET \u521D\u59CB\u5316\u5B8C\u6210\u3002",
631
+ warnings,
451
632
  nextSteps: ["\u4F7F\u7528 fet propose/new \u521B\u5EFA OpenSpec change", "\u4F7F\u7528 fet doctor \u68C0\u67E5\u9879\u76EE\u72B6\u6001"]
452
633
  });
453
634
  }
454
635
  async function ensureGitignore(ctx) {
455
- const gitignorePath = join6(ctx.projectRoot, ".gitignore");
636
+ const gitignorePath = join8(ctx.projectRoot, ".gitignore");
456
637
  const existing = await readOptional2(gitignorePath);
457
638
  await atomicWrite(gitignorePath, mergeGitignore(existing));
458
639
  }
459
640
  async function readOptional2(path) {
460
641
  try {
461
- return await readFile5(path, "utf8");
642
+ return await readFile6(path, "utf8");
462
643
  } catch {
463
644
  return null;
464
645
  }
465
646
  }
466
647
  async function exists(path) {
467
648
  try {
468
- await stat2(path);
649
+ await stat3(path);
469
650
  return true;
470
651
  } catch {
471
652
  return false;
@@ -473,19 +654,20 @@ async function exists(path) {
473
654
  }
474
655
 
475
656
  // src/commands/doctor.ts
476
- import { readFile as readFile6, stat as stat3 } from "fs/promises";
477
- import { join as join7 } from "path";
657
+ import { readFile as readFile7, stat as stat4 } from "fs/promises";
658
+ import { join as join9 } from "path";
478
659
  async function doctorCommand(ctx, options = {}) {
479
660
  const checks = [];
480
661
  checks.push(await checkOpenSpec(ctx));
481
662
  checks.push(await checkState(ctx));
482
- checks.push(await checkFile("agents", join7(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
483
- checks.push(await checkFile("config", join7(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
484
- checks.push(await checkPlaceholders(join7(ctx.projectRoot, "AGENTS.md")));
663
+ checks.push(await checkFile("agents", join9(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
664
+ checks.push(await checkFile("config", join9(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
665
+ checks.push(await checkPlaceholders(ctx.projectRoot));
666
+ checks.push(await checkGitNexus(ctx));
485
667
  for (const adapter of ctx.toolAdapters) {
486
668
  checks.push(...await adapter.doctor(ctx.projectRoot));
487
669
  }
488
- const lockPath = join7(ctx.projectRoot, "openspec", ".fet.lock");
670
+ const lockPath = join9(ctx.projectRoot, "openspec", ".fet.lock");
489
671
  if (await exists2(lockPath)) {
490
672
  if (options.fixLock) {
491
673
  await clearLock(ctx.projectRoot);
@@ -503,6 +685,28 @@ async function doctorCommand(ctx, options = {}) {
503
685
  data: checks
504
686
  });
505
687
  }
688
+ async function checkGitNexus(ctx) {
689
+ const global = await ctx.stateStore.readGlobal();
690
+ const state = mergeGitNexusGraphInfo(
691
+ toGitNexusState(await detectGitNexus(), global?.graph?.gitnexus),
692
+ await inspectGitNexusGraph(ctx.projectRoot)
693
+ );
694
+ if (global) {
695
+ global.graph ??= {};
696
+ global.graph.gitnexus = state;
697
+ await ctx.stateStore.writeGlobal(global);
698
+ }
699
+ return state.installed ? {
700
+ id: "gitnexus",
701
+ status: "pass",
702
+ message: `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"}), graph ${state.graphExists ? "found" : "not found"}`
703
+ } : {
704
+ id: "gitnexus",
705
+ status: "warn",
706
+ message: "Optional GitNexus code graph support is not installed",
707
+ suggestedCommand: "Install GitNexus later if you want OpenSpec artifacts to prefer a repository graph"
708
+ };
709
+ }
506
710
  async function checkOpenSpec(ctx) {
507
711
  try {
508
712
  const identity = await ctx.openSpec.resolveExecutable();
@@ -522,10 +726,10 @@ async function checkState(ctx) {
522
726
  async function checkFile(id, path, missing, suggestedCommand) {
523
727
  return await exists2(path) ? { id, status: "pass", message: `${id} \u5B58\u5728` } : { id, status: "warn", message: missing, suggestedCommand };
524
728
  }
525
- async function checkPlaceholders(path) {
729
+ async function checkPlaceholders(projectRoot) {
526
730
  try {
527
- const content = await readFile6(path, "utf8");
528
- const count2 = [...content.matchAll(/\[NEEDS? LLM INPUT\]/g)].length;
731
+ await readFile7(join9(projectRoot, "AGENTS.md"), "utf8");
732
+ const count2 = await countAgentsLlmPlaceholders(projectRoot);
529
733
  return count2 ? {
530
734
  id: "context-placeholders",
531
735
  status: "warn",
@@ -538,7 +742,7 @@ async function checkPlaceholders(path) {
538
742
  }
539
743
  async function exists2(path) {
540
744
  try {
541
- await stat3(path);
745
+ await stat4(path);
542
746
  return true;
543
747
  } catch {
544
748
  return false;
@@ -546,15 +750,14 @@ async function exists2(path) {
546
750
  }
547
751
 
548
752
  // src/commands/fill-context.ts
549
- import { mkdir as mkdir3, readFile as readFile7 } from "fs/promises";
550
- import { dirname as dirname5, join as join8 } from "path";
551
- var placeholderPattern = /\[NEEDS? LLM INPUT\]/g;
753
+ import { mkdir as mkdir3 } from "fs/promises";
754
+ import { dirname as dirname5, join as join10 } from "path";
552
755
  async function fillContextCommand(ctx) {
553
756
  await withProjectLock(
554
757
  ctx.projectRoot,
555
758
  { command: "fill-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
556
759
  async () => {
557
- const handoffPath = join8(ctx.projectRoot, ".fet", "fill-context.md");
760
+ const handoffPath = join10(ctx.projectRoot, ".fet", "fill-context.md");
558
761
  await mkdir3(dirname5(handoffPath), { recursive: true });
559
762
  await atomicWrite(handoffPath, renderGenericHandoff());
560
763
  for (const adapter of ctx.toolAdapters) {
@@ -573,7 +776,7 @@ async function fillContextCommand(ctx) {
573
776
  }
574
777
  }
575
778
  );
576
- const placeholders = await countPlaceholders(join8(ctx.projectRoot, "AGENTS.md"));
779
+ const placeholders = await countAgentsLlmPlaceholders(ctx.projectRoot);
577
780
  ctx.output.result({
578
781
  ok: true,
579
782
  command: "fill-context",
@@ -608,23 +811,265 @@ Use the IDE AI to complete FET-generated placeholders.
608
811
  6. Run \`fet doctor\` and confirm no AGENTS.md placeholder warning remains.
609
812
  `;
610
813
  }
611
- async function countPlaceholders(path) {
612
- try {
613
- const content = await readFile7(path, "utf8");
614
- return [...content.matchAll(placeholderPattern)].length;
615
- } catch {
616
- return 0;
814
+
815
+ // src/commands/graph.ts
816
+ import { mkdir as mkdir4 } from "fs/promises";
817
+ import { dirname as dirname6, join as join11 } from "path";
818
+ async function graphCommand(ctx, action, args = []) {
819
+ switch (action) {
820
+ case "status":
821
+ await graphStatusCommand(ctx);
822
+ return;
823
+ case "doctor":
824
+ await graphDoctorCommand(ctx);
825
+ return;
826
+ case "setup":
827
+ await graphSetupCommand(ctx);
828
+ return;
829
+ case "handoff":
830
+ await graphHandoffCommand(ctx);
831
+ return;
832
+ case "init":
833
+ await graphAnalyzeCommand(ctx, "init", args);
834
+ return;
835
+ case "refresh":
836
+ await graphAnalyzeCommand(ctx, "refresh", args);
837
+ return;
617
838
  }
618
839
  }
840
+ async function graphStatusCommand(ctx) {
841
+ const result = await refreshGraphState(ctx, { runStatus: true });
842
+ const warnings = result.state.installed ? [] : ["GitNexus is not installed. Run fet graph setup for installation handoff instructions."];
843
+ ctx.output.result({
844
+ ok: true,
845
+ command: "graph status",
846
+ 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.",
847
+ warnings,
848
+ nextSteps: result.state.installed && !result.state.graphExists ? ["Run fet graph init to build the first GitNexus graph"] : void 0,
849
+ data: result
850
+ });
851
+ }
852
+ async function graphDoctorCommand(ctx) {
853
+ const result = await refreshGraphState(ctx, { runStatus: true });
854
+ const warnings = [
855
+ ...!result.state.installed ? ["GitNexus is not installed."] : [],
856
+ ...result.state.installed && !result.state.graphExists ? ["GitNexus is installed but no graph directory was found."] : [],
857
+ ...!result.state.handoffPath ? ["Graph handoff instructions have not been generated."] : []
858
+ ];
859
+ ctx.output.result({
860
+ ok: true,
861
+ command: "graph doctor",
862
+ summary: warnings.length ? `Graph doctor completed with ${warnings.length} warning(s).` : "Graph doctor completed without warnings.",
863
+ warnings,
864
+ nextSteps: warnings.length ? ["Run fet graph setup", "Run fet graph handoff", "Run fet graph init when GitNexus is installed"] : void 0,
865
+ data: result
866
+ });
867
+ }
868
+ async function graphSetupCommand(ctx) {
869
+ let result;
870
+ const handoffPath = join11(ctx.projectRoot, ".fet", "graph-setup.md");
871
+ await withProjectLock(ctx.projectRoot, { command: "graph setup", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
872
+ result = await refreshGraphState(ctx, { write: false });
873
+ await writeHandoffFile(handoffPath, renderGraphSetupHandoff(result.state));
874
+ const global = await ctx.stateStore.getOrCreateGlobal();
875
+ global.graph ??= {};
876
+ global.graph.gitnexus = {
877
+ ...result.state,
878
+ setupHandoffPath: ".fet/graph-setup.md",
879
+ setupHandoffUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
880
+ };
881
+ await ctx.stateStore.writeGlobal(global);
882
+ });
883
+ ctx.output.result({
884
+ ok: true,
885
+ command: "graph setup",
886
+ summary: "GitNexus setup handoff generated.",
887
+ warnings: result.state.installed ? [] : ["GitNexus is not installed. The handoff explains installation and IDE-assisted setup options."],
888
+ 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"],
889
+ data: {
890
+ path: ".fet/graph-setup.md",
891
+ gitnexus: result.state
892
+ }
893
+ });
894
+ }
895
+ async function graphHandoffCommand(ctx) {
896
+ let result;
897
+ const handoffPath = join11(ctx.projectRoot, ".fet", "graph-handoff.md");
898
+ await withProjectLock(ctx.projectRoot, { command: "graph handoff", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
899
+ result = await refreshGraphState(ctx, { runStatus: true, write: false });
900
+ await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state));
901
+ const global = await ctx.stateStore.getOrCreateGlobal();
902
+ global.graph ??= {};
903
+ global.graph.gitnexus = {
904
+ ...result.state,
905
+ handoffPath: ".fet/graph-handoff.md",
906
+ handoffUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
907
+ };
908
+ await ctx.stateStore.writeGlobal(global);
909
+ });
910
+ ctx.output.result({
911
+ ok: true,
912
+ command: "graph handoff",
913
+ summary: "GitNexus graph usage handoff generated.",
914
+ warnings: result.state.installed ? [] : ["GitNexus is not installed. The handoff still documents the fallback behavior."],
915
+ nextSteps: ["Cursor/Codex/OpenCode: read .fet/graph-handoff.md before broad repository scans"],
916
+ data: {
917
+ path: ".fet/graph-handoff.md",
918
+ gitnexus: result.state
919
+ }
920
+ });
921
+ }
922
+ async function graphAnalyzeCommand(ctx, mode, args) {
923
+ const detection = await detectGitNexus();
924
+ if (!detection.installed) {
925
+ throw new FetError({
926
+ code: "GRAPH_PROVIDER_NOT_FOUND" /* GraphProviderNotFound */,
927
+ message: "GitNexus is not installed or is not available on PATH.",
928
+ details: { executable: detection.executablePath, error: detection.error },
929
+ suggestedCommand: "fet graph setup"
930
+ });
931
+ }
932
+ const run = await runGitNexus(["analyze", ...args], { cwd: ctx.projectRoot });
933
+ if (run.exitCode !== 0) {
934
+ throw new FetError({
935
+ code: "GRAPH_COMMAND_FAILED" /* GraphCommandFailed */,
936
+ message: "GitNexus analyze failed.",
937
+ details: { command: run.command.join(" "), exitCode: run.exitCode, stdout: run.stdout, stderr: run.stderr },
938
+ suggestedCommand: "fet graph doctor"
939
+ });
940
+ }
941
+ const result = await refreshGraphState(ctx, { write: false });
942
+ const global = await ctx.stateStore.getOrCreateGlobal();
943
+ global.graph ??= {};
944
+ global.graph.gitnexus = {
945
+ ...result.state,
946
+ lastRefreshAt: (/* @__PURE__ */ new Date()).toISOString()
947
+ };
948
+ await ctx.stateStore.writeGlobal(global);
949
+ ctx.output.result({
950
+ ok: true,
951
+ command: `graph ${mode}`,
952
+ summary: mode === "init" ? "GitNexus graph initialized." : "GitNexus graph refreshed.",
953
+ warnings: result.state.graphExists ? [] : ["GitNexus analyze completed, but the configured graph directory was not found."],
954
+ nextSteps: ["Run fet graph status", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"],
955
+ data: {
956
+ gitnexus: global.graph.gitnexus,
957
+ run: {
958
+ command: run.command,
959
+ stdout: run.stdout.trim(),
960
+ stderr: run.stderr.trim()
961
+ }
962
+ }
963
+ });
964
+ }
965
+ async function refreshGraphState(ctx, options = {}) {
966
+ const global = await ctx.stateStore.getOrCreateGlobal();
967
+ global.graph ??= {};
968
+ const detection = await detectGitNexus();
969
+ const graph2 = await inspectGitNexusGraph(ctx.projectRoot);
970
+ let state = mergeGitNexusGraphInfo(toGitNexusState(detection, global.graph.gitnexus), graph2);
971
+ let gitnexusStatus = null;
972
+ if (options.runStatus && detection.installed) {
973
+ gitnexusStatus = await runGitNexus(["status"], { cwd: ctx.projectRoot });
974
+ state = {
975
+ ...state,
976
+ lastStatus: firstLine(gitnexusStatus.stdout) || firstLine(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
977
+ };
978
+ }
979
+ if (options.write ?? true) {
980
+ global.graph.gitnexus = state;
981
+ await ctx.stateStore.writeGlobal(global);
982
+ }
983
+ return {
984
+ state,
985
+ gitnexusStatus: gitnexusStatus ? {
986
+ exitCode: gitnexusStatus.exitCode,
987
+ command: gitnexusStatus.command,
988
+ stdout: gitnexusStatus.stdout.trim(),
989
+ stderr: gitnexusStatus.stderr.trim()
990
+ } : null
991
+ };
992
+ }
993
+ async function writeHandoffFile(path, content) {
994
+ await mkdir4(dirname6(path), { recursive: true });
995
+ await atomicWrite(path, content);
996
+ }
997
+ function renderGraphSetupHandoff(state) {
998
+ return `<!-- FET:MANAGED
999
+ schemaVersion: 1
1000
+ generator: graph-setup
1001
+ FET:END -->
1002
+
1003
+ # FET Graph Setup
1004
+
1005
+ GitNexus graph support is optional. FET does not install GitNexus automatically and does not require graph support for OpenSpec workflows.
1006
+
1007
+ Current status:
1008
+
1009
+ - Installed: ${state.installed ? "yes" : "no"}
1010
+ - Executable: ${state.executablePath ?? "gitnexus"}
1011
+ - Version: ${state.version ?? "unknown"}
1012
+ - Graph path: ${state.graphPath ?? ".gitnexus"}
1013
+ - Graph exists: ${state.graphExists ? "yes" : "no"}
1014
+
1015
+ Suggested setup flow:
1016
+
1017
+ 1. If GitNexus is not installed, install it using the method recommended by the GitNexus project.
1018
+ 2. If you want GitNexus MCP or IDE integration, run \`gitnexus setup\` yourself after reviewing what it changes.
1019
+ 3. Return to this project and run \`fet graph init\` to build the first graph.
1020
+ 4. Run \`fet graph handoff\` so IDE AI can prefer graph context before broad repository scans.
1021
+
1022
+ Guardrails:
1023
+
1024
+ - Do not block FET/OpenSpec commands when GitNexus is unavailable.
1025
+ - Do not generate or modify application code during setup.
1026
+ - Do not run global IDE configuration commands unless the user explicitly approves them.
1027
+ `;
1028
+ }
1029
+ function renderGraphUsageHandoff(state) {
1030
+ return `<!-- FET:MANAGED
1031
+ schemaVersion: 1
1032
+ generator: graph-handoff
1033
+ FET:END -->
1034
+
1035
+ # FET Graph Handoff
1036
+
1037
+ Use GitNexus graph context as an optional first pass before broad repository scans.
1038
+
1039
+ Current status:
1040
+
1041
+ - Installed: ${state.installed ? "yes" : "no"}
1042
+ - Graph path: ${state.graphPath ?? ".gitnexus"}
1043
+ - Graph exists: ${state.graphExists ? "yes" : "no"}
1044
+ - Last indexed at: ${state.lastIndexedAt ?? "unknown"}
1045
+ - Last status: ${state.lastStatus ?? "unknown"}
1046
+
1047
+ When graph context is available:
1048
+
1049
+ 1. Use the graph to identify likely modules, dependencies, and insertion points.
1050
+ 2. Read only the concrete source files needed to confirm behavior.
1051
+ 3. Prefer OpenSpec artifacts and AGENTS.md over graph guesses when they conflict.
1052
+ 4. Fall back to normal repository inspection if the graph is missing, stale, or incomplete.
1053
+
1054
+ When producing OpenSpec artifacts:
1055
+
1056
+ - Use graph context to make proposal, design, specs, and tasks more precise.
1057
+ - Avoid large repository scans when the graph already narrows the relevant area.
1058
+ - Keep all generated artifacts in the normal OpenSpec change directory.
1059
+ `;
1060
+ }
1061
+ function firstLine(value) {
1062
+ return value.trim().split(/\r?\n/)[0]?.trim() || null;
1063
+ }
619
1064
 
620
1065
  // src/commands/proxy.ts
621
1066
  import { readFile as readFile10 } from "fs/promises";
622
- import { join as join10 } from "path";
1067
+ import { join as join13 } from "path";
623
1068
 
624
1069
  // src/state/project.ts
625
- import { execFile } from "child_process";
626
- import { promisify } from "util";
627
- var execFileAsync = promisify(execFile);
1070
+ import { execFile as execFile2 } from "child_process";
1071
+ import { promisify as promisify2 } from "util";
1072
+ var execFileAsync2 = promisify2(execFile2);
628
1073
  async function detectProjectIdentity(projectRoot) {
629
1074
  const [gitRoot, branch, headCommit] = await Promise.all([
630
1075
  git(projectRoot, ["rev-parse", "--show-toplevel"]),
@@ -640,7 +1085,7 @@ async function detectProjectIdentity(projectRoot) {
640
1085
  }
641
1086
  async function git(cwd, args) {
642
1087
  try {
643
- const { stdout } = await execFileAsync("git", args, { cwd });
1088
+ const { stdout } = await execFileAsync2("git", args, { cwd });
644
1089
  return stdout.trim() || null;
645
1090
  } catch {
646
1091
  return null;
@@ -648,8 +1093,8 @@ async function git(cwd, args) {
648
1093
  }
649
1094
 
650
1095
  // src/state/store.ts
651
- import { mkdir as mkdir4, readFile as readFile8 } from "fs/promises";
652
- import { join as join9 } from "path";
1096
+ import { mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
1097
+ import { join as join12 } from "path";
653
1098
 
654
1099
  // src/state/schema.ts
655
1100
  var phases = ["explore", "propose", "implement", "verify", "sync", "archive"];
@@ -670,6 +1115,7 @@ function createGlobalState(fetVersion, project) {
670
1115
  scannerVersion: 1
671
1116
  },
672
1117
  toolAdapters: {},
1118
+ graph: {},
673
1119
  verifyAuthorization: null,
674
1120
  lastDoctor: null
675
1121
  };
@@ -757,7 +1203,7 @@ var StateStore = class {
757
1203
  }
758
1204
  async writeGlobal(state) {
759
1205
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
760
- await mkdir4(join9(this.projectRoot, "openspec"), { recursive: true });
1206
+ await mkdir5(join12(this.projectRoot, "openspec"), { recursive: true });
761
1207
  await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
762
1208
  `);
763
1209
  }
@@ -778,15 +1224,15 @@ var StateStore = class {
778
1224
  }
779
1225
  async writeChange(state) {
780
1226
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
781
- await mkdir4(join9(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
1227
+ await mkdir5(join12(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
782
1228
  await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
783
1229
  `);
784
1230
  }
785
1231
  globalPath() {
786
- return join9(this.projectRoot, "openspec", "fet-state.json");
1232
+ return join12(this.projectRoot, "openspec", "fet-state.json");
787
1233
  }
788
1234
  changePath(changeId) {
789
- return join9(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
1235
+ return join12(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
790
1236
  }
791
1237
  };
792
1238
  function isNotFound(error) {
@@ -903,7 +1349,7 @@ async function createChangelogEntry(projectRoot, changeId) {
903
1349
  };
904
1350
  }
905
1351
  async function appendChangelog(projectRoot, entry) {
906
- const changelogPath = join10(projectRoot, "CHANGELOG.md");
1352
+ const changelogPath = join13(projectRoot, "CHANGELOG.md");
907
1353
  const existing = await readOptional3(changelogPath);
908
1354
  const block = `updateTime: ${entry.updateTime}
909
1355
  \u66F4\u65B0\u5185\u5BB9:${entry.content}
@@ -914,12 +1360,12 @@ ${block}` : block;
914
1360
  await atomicWrite(changelogPath, next);
915
1361
  }
916
1362
  async function readChangeRequirement(projectRoot, changeId) {
917
- const changeRoot = join10(projectRoot, "openspec", "changes", changeId);
918
- const proposal = await readOptional3(join10(changeRoot, "proposal.md"));
1363
+ const changeRoot = join13(projectRoot, "openspec", "changes", changeId);
1364
+ const proposal = await readOptional3(join13(changeRoot, "proposal.md"));
919
1365
  if (proposal) {
920
1366
  return summarizeMarkdown(proposal);
921
1367
  }
922
- const readme = await readOptional3(join10(changeRoot, "README.md"));
1368
+ const readme = await readOptional3(join13(changeRoot, "README.md"));
923
1369
  if (readme) {
924
1370
  return summarizeMarkdown(readme);
925
1371
  }
@@ -1063,8 +1509,8 @@ async function assertVerified(ctx) {
1063
1509
 
1064
1510
  // src/commands/verify.ts
1065
1511
  import { createHash } from "crypto";
1066
- import { mkdir as mkdir5, readFile as readFile11, stat as stat4 } from "fs/promises";
1067
- import { join as join11 } from "path";
1512
+ import { mkdir as mkdir6, readFile as readFile11, stat as stat5 } from "fs/promises";
1513
+ import { join as join14 } from "path";
1068
1514
  async function verifyCommand(ctx, options) {
1069
1515
  if (options.auto) {
1070
1516
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
@@ -1131,9 +1577,9 @@ async function verifyCommand(ctx, options) {
1131
1577
  async function writeInstructions(ctx, changeId) {
1132
1578
  await assertChangeExists(ctx, changeId);
1133
1579
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1134
- const dir = join11(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
1135
- const instructionsPath = join11(dir, "verify-instructions.md");
1136
- await mkdir5(dir, { recursive: true });
1580
+ const dir = join14(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
1581
+ const instructionsPath = join14(dir, "verify-instructions.md");
1582
+ await mkdir6(dir, { recursive: true });
1137
1583
  await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
1138
1584
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
1139
1585
  state.currentPhase = "verify";
@@ -1149,7 +1595,7 @@ async function writeInstructions(ctx, changeId) {
1149
1595
  async function markDone(ctx, changeId) {
1150
1596
  await assertChangeExists(ctx, changeId);
1151
1597
  const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
1152
- const instructionsPath = join11(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
1598
+ const instructionsPath = join14(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
1153
1599
  const instructions = await readInstructions(instructionsPath, changeId);
1154
1600
  const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
1155
1601
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
@@ -1184,7 +1630,7 @@ async function assertChangeExists(ctx, changeId) {
1184
1630
  }
1185
1631
  async function readInstructions(path, changeId) {
1186
1632
  try {
1187
- await stat4(path);
1633
+ await stat5(path);
1188
1634
  const content = await readFile11(path, "utf8");
1189
1635
  const fileChangeId = readFrontMatterValue(content, "changeId");
1190
1636
  if (fileChangeId !== changeId) {
@@ -1235,13 +1681,76 @@ async function resolveChangeId(ctx) {
1235
1681
  });
1236
1682
  }
1237
1683
 
1684
+ // src/model-policy.ts
1685
+ var HIGH_COST_MODEL_PATTERNS = [
1686
+ /gpt[-_ ]?5\.5/i,
1687
+ /glm[-_ ]?5(?:\.1)?/i,
1688
+ /claude.*opus/i,
1689
+ /opus/i,
1690
+ /claude.*sonnet/i,
1691
+ /sonnet/i
1692
+ ];
1693
+ var MODEL_ENV_KEYS = ["FET_IDE_MODEL", "FET_MODEL", "CODEX_MODEL", "CURSOR_MODEL", "OPENCODE_MODEL", "OPENAI_MODEL", "ANTHROPIC_MODEL"];
1694
+ function detectCurrentModel(env = process.env) {
1695
+ for (const key of MODEL_ENV_KEYS) {
1696
+ const value = env[key]?.trim();
1697
+ if (value) {
1698
+ return { source: key, name: value };
1699
+ }
1700
+ }
1701
+ return null;
1702
+ }
1703
+ function isHighCostModel(model) {
1704
+ return HIGH_COST_MODEL_PATTERNS.some((pattern) => pattern.test(model));
1705
+ }
1706
+ function getCommandModelPolicyMismatch(command, env = process.env) {
1707
+ if (env.FET_MODEL_POLICY === "off" || env.FET_SKIP_MODEL_POLICY === "1") {
1708
+ return null;
1709
+ }
1710
+ const detected = detectCurrentModel(env);
1711
+ if (!detected) {
1712
+ return null;
1713
+ }
1714
+ const highCost = isHighCostModel(detected.name);
1715
+ if (command === "apply") {
1716
+ if (!highCost) {
1717
+ return {
1718
+ command,
1719
+ detected,
1720
+ recommended: "high-cost",
1721
+ reason: "fet apply is the implementation phase and is recommended to use a high-capability/high-cost model."
1722
+ };
1723
+ }
1724
+ return null;
1725
+ }
1726
+ if (highCost) {
1727
+ return {
1728
+ command,
1729
+ detected,
1730
+ recommended: "low-cost",
1731
+ reason: `fet ${command} is not the implementation phase and is recommended to use a lower-cost model.`
1732
+ };
1733
+ }
1734
+ return null;
1735
+ }
1736
+ function formatModelPolicyMismatch(mismatch) {
1737
+ const switchHint = mismatch.recommended === "high-cost" ? "Recommended models include GPT-5.5, GLM-5.1, GLM-5, Claude Opus, or Claude Sonnet." : "Recommended action: switch to a lower-cost model and reserve high-cost models for fet apply.";
1738
+ return `${mismatch.reason} Detected ${mismatch.detected.source}="${mismatch.detected.name}". ${switchHint}`;
1739
+ }
1740
+ function renderIdeModelPolicy(command) {
1741
+ if (command === "apply") {
1742
+ 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.";
1743
+ }
1744
+ 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.";
1745
+ }
1746
+
1238
1747
  // src/cli/context.ts
1239
1748
  import { resolve } from "path";
1240
1749
 
1241
1750
  // src/adapters/codex/index.ts
1242
- import { mkdir as mkdir6, readFile as readFile12, stat as stat5 } from "fs/promises";
1751
+ import { mkdir as mkdir7, readFile as readFile12, stat as stat6 } from "fs/promises";
1243
1752
  import { homedir } from "os";
1244
- import { dirname as dirname6, join as join12 } from "path";
1753
+ import { dirname as dirname7, join as join15 } from "path";
1245
1754
 
1246
1755
  // src/adapters/commands.ts
1247
1756
  var FET_WORKFLOW_COMMANDS = [
@@ -1257,7 +1766,18 @@ var FET_WORKFLOW_COMMANDS = [
1257
1766
  "bulk-archive",
1258
1767
  "onboard"
1259
1768
  ];
1260
- var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "fill-context", "passthrough"];
1769
+ var FET_GRAPH_COMMANDS = ["graph-status", "graph-setup", "graph-init", "graph-refresh", "graph-doctor", "graph-handoff"];
1770
+ var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "fill-context", "passthrough", ...FET_GRAPH_COMMANDS];
1771
+ function renderFetAdapterUsage(command, args = "[...args]") {
1772
+ if (command.startsWith("graph-")) {
1773
+ const subcommand = command.slice("graph-".length);
1774
+ return `fet graph ${subcommand}${args ? ` ${args}` : ""}`;
1775
+ }
1776
+ if (command === "passthrough") {
1777
+ return `fet passthrough <openspec-command>${args ? ` ${args}` : ""}`;
1778
+ }
1779
+ return `fet ${command}${args ? ` ${args}` : ""}`;
1780
+ }
1261
1781
 
1262
1782
  // src/adapters/codex/templates.ts
1263
1783
  function codexGuideFile() {
@@ -1278,6 +1798,8 @@ Before doing FET or OpenSpec work in Codex, read:
1278
1798
  - openspec/config.yaml
1279
1799
  - the active change files under openspec/changes/<change-id>/, when a change is selected
1280
1800
 
1801
+ If GitNexus code graph context is available in the IDE or MCP tools, prefer it before broad repository scans. Use it to identify relevant modules, dependencies, and insertion points, then read only the concrete source files needed. If GitNexus is unavailable, continue with the normal FET/OpenSpec workflow.
1802
+
1281
1803
  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.
1282
1804
 
1283
1805
  Command guides live in .codex/fet/commands/.
@@ -1303,22 +1825,30 @@ function renderCommand(command) {
1303
1825
  if (command === "passthrough") {
1304
1826
  return renderPassthroughCommand();
1305
1827
  }
1828
+ if (command.startsWith("graph-")) {
1829
+ return renderGraphCommand(command);
1830
+ }
1831
+ const usage = renderFetAdapterUsage(command, "");
1306
1832
  return `<!-- FET:MANAGED
1307
1833
  schemaVersion: 1
1308
1834
  fetVersion: ${FET_VERSION}
1309
1835
  generator: codex-adapter
1310
1836
  adapterVersion: 1
1311
- command: fet ${command}
1837
+ command: ${usage}
1312
1838
  FET:END -->
1313
1839
 
1314
- # fet ${command}
1840
+ # ${usage}
1841
+
1842
+ ${renderIdeModelPolicy(command)}
1315
1843
 
1316
1844
  When the user asks Codex to run the FET ${command} workflow, first make sure the project context is loaded from AGENTS.md and openspec/config.yaml.
1317
1845
 
1846
+ 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.
1847
+
1318
1848
  Then run:
1319
1849
 
1320
1850
  \`\`\`sh
1321
- fet ${command}
1851
+ ${usage}
1322
1852
  \`\`\`
1323
1853
 
1324
1854
  If the command needs a change id, pass it with \`--change <change-id>\` or use the active OpenSpec change from the user's request.
@@ -1337,8 +1867,12 @@ FET:END -->
1337
1867
 
1338
1868
  # fet passthrough
1339
1869
 
1870
+ ${renderIdeModelPolicy("passthrough")}
1871
+
1340
1872
  When the user asks Codex to run an OpenSpec command that FET does not manage as a first-class workflow command, use FET passthrough instead of calling OpenSpec directly.
1341
1873
 
1874
+ 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.
1875
+
1342
1876
  Then run:
1343
1877
 
1344
1878
  \`\`\`sh
@@ -1348,6 +1882,36 @@ fet passthrough <openspec-command> [...args]
1348
1882
  This preserves the FET entry point while allowing access to unmanaged or newly added OpenSpec commands. Passthrough does not update FET lifecycle state.
1349
1883
  `;
1350
1884
  }
1885
+ function renderGraphCommand(command) {
1886
+ const usage = renderFetAdapterUsage(command, "");
1887
+ const subcommand = command.slice("graph-".length);
1888
+ return `<!-- FET:MANAGED
1889
+ schemaVersion: 1
1890
+ fetVersion: ${FET_VERSION}
1891
+ generator: codex-adapter
1892
+ adapterVersion: 1
1893
+ command: ${usage}
1894
+ FET:END -->
1895
+
1896
+ # ${usage}
1897
+
1898
+ ${renderIdeModelPolicy(command)}
1899
+
1900
+ When the user asks Codex to work with optional GitNexus graph support, use FET as the entry point.
1901
+
1902
+ 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.
1903
+
1904
+ Run:
1905
+
1906
+ \`\`\`sh
1907
+ ${usage}
1908
+ \`\`\`
1909
+
1910
+ For graph init or refresh, pass extra GitNexus analyze arguments only when the user provides them.
1911
+
1912
+ After the command completes, report the GitNexus state, generated handoff files, and next steps.
1913
+ `;
1914
+ }
1351
1915
  function renderSlashPrompt(command) {
1352
1916
  if (command === "continue") {
1353
1917
  return renderContinueSlashPrompt();
@@ -1385,9 +1949,10 @@ function renderSlashPrompt(command) {
1385
1949
  if (command === "passthrough") {
1386
1950
  return renderPassthroughSlashPrompt();
1387
1951
  }
1388
- const usage = command === "passthrough" ? "fet passthrough <openspec-command> [...args]" : `fet ${command} [...args]`;
1389
- const shellCommand = command === "passthrough" ? "fet passthrough $ARGUMENTS" : `fet ${command} $ARGUMENTS`;
1390
- const description = command === "passthrough" ? "Run an unmanaged OpenSpec command through FET passthrough" : `Run the FET-managed OpenSpec ${command} workflow`;
1952
+ const usage = renderFetAdapterUsage(command);
1953
+ const isGraph = command.startsWith("graph-");
1954
+ const shellCommand = isGraph ? `${renderFetAdapterUsage(command, "")} $ARGUMENTS` : `fet ${command} $ARGUMENTS`;
1955
+ const description = isGraph ? `Run optional GitNexus graph ${command.slice("graph-".length)} through FET` : `Run the FET-managed OpenSpec ${command} workflow`;
1391
1956
  return `<!-- FET:MANAGED
1392
1957
  schemaVersion: 1
1393
1958
  fetVersion: ${FET_VERSION}
@@ -1405,6 +1970,8 @@ Use FET as the entry point for this OpenSpec workflow.
1405
1970
 
1406
1971
  Before running the command, make sure the relevant project context is loaded from AGENTS.md and openspec/config.yaml. If a change id is needed and was not provided, infer it from the active FET/OpenSpec state when unambiguous; otherwise ask the user for the change id.
1407
1972
 
1973
+ 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.
1974
+
1408
1975
  Run:
1409
1976
 
1410
1977
  \`\`\`sh
@@ -1425,8 +1992,12 @@ FET:END -->
1425
1992
 
1426
1993
  # fet fill-context
1427
1994
 
1995
+ ${renderIdeModelPolicy("fill-context")}
1996
+
1428
1997
  Use this command to complete FET-generated project context placeholders with Codex.
1429
1998
 
1999
+ 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.
2000
+
1430
2001
  First run:
1431
2002
 
1432
2003
  \`\`\`sh
@@ -1809,6 +2380,7 @@ Output:
1809
2380
  );
1810
2381
  }
1811
2382
  function renderManagedSlashPrompt(command, description, body) {
2383
+ const policyCommand = command.split(/\s+/)[1] ?? command;
1812
2384
  return `<!-- FET:MANAGED
1813
2385
  schemaVersion: 1
1814
2386
  fetVersion: ${FET_VERSION}
@@ -1822,6 +2394,10 @@ description: ${description}
1822
2394
  argument-hint: command arguments
1823
2395
  ---
1824
2396
 
2397
+ ${renderIdeModelPolicy(policyCommand)}
2398
+
2399
+ 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.
2400
+
1825
2401
  ${body}
1826
2402
  `;
1827
2403
  }
@@ -1832,7 +2408,7 @@ var CodexAdapter = class {
1832
2408
  adapterVersion = 1;
1833
2409
  async detect(projectRoot) {
1834
2410
  return {
1835
- detected: await exists3(join12(projectRoot, ".codex")) || await exists3(join12(projectRoot, "AGENTS.md")),
2411
+ detected: await exists3(join15(projectRoot, ".codex")) || await exists3(join15(projectRoot, "AGENTS.md")),
1836
2412
  reason: "Codex adapter is available for projects that use AGENTS.md"
1837
2413
  };
1838
2414
  }
@@ -1871,7 +2447,7 @@ var CodexAdapter = class {
1871
2447
  if (existing && !existing.includes("FET:MANAGED") && force) {
1872
2448
  await createBackup(target);
1873
2449
  }
1874
- await mkdir6(dirname6(target), { recursive: true });
2450
+ await mkdir7(dirname7(target), { recursive: true });
1875
2451
  await atomicWrite(target, file.content);
1876
2452
  written.push(displayPath);
1877
2453
  }
@@ -1898,9 +2474,9 @@ var CodexAdapter = class {
1898
2474
  };
1899
2475
  function resolveTarget(projectRoot, file) {
1900
2476
  if (file.root === "codex-home") {
1901
- return join12(resolveCodexHome(), file.path);
2477
+ return join15(resolveCodexHome(), file.path);
1902
2478
  }
1903
- return join12(projectRoot, file.path);
2479
+ return join15(projectRoot, file.path);
1904
2480
  }
1905
2481
  function displayPathFor(file) {
1906
2482
  if (file.root === "codex-home") {
@@ -1909,7 +2485,7 @@ function displayPathFor(file) {
1909
2485
  return file.path;
1910
2486
  }
1911
2487
  function resolveCodexHome() {
1912
- return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join12(homedir(), ".codex");
2488
+ return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join15(homedir(), ".codex");
1913
2489
  }
1914
2490
  async function readExisting(path) {
1915
2491
  try {
@@ -1920,7 +2496,7 @@ async function readExisting(path) {
1920
2496
  }
1921
2497
  async function exists3(path) {
1922
2498
  try {
1923
- await stat5(path);
2499
+ await stat6(path);
1924
2500
  return true;
1925
2501
  } catch {
1926
2502
  return false;
@@ -1928,8 +2504,8 @@ async function exists3(path) {
1928
2504
  }
1929
2505
 
1930
2506
  // src/adapters/cursor/index.ts
1931
- import { mkdir as mkdir7, readFile as readFile13, stat as stat6 } from "fs/promises";
1932
- import { dirname as dirname7, join as join13 } from "path";
2507
+ import { mkdir as mkdir8, readFile as readFile13, stat as stat7 } from "fs/promises";
2508
+ import { dirname as dirname8, join as join16 } from "path";
1933
2509
 
1934
2510
  // src/adapters/cursor/templates.ts
1935
2511
  function cursorSkillFiles() {
@@ -1957,6 +2533,7 @@ alwaysApply: false
1957
2533
 
1958
2534
  - AGENTS.md
1959
2535
  - openspec/config.yaml
2536
+ - GitNexus code graph context, when available. Prefer it before broad repository scans; if it is unavailable, continue normally.
1960
2537
  - \u5F53\u524D change \u76EE\u5F55\u4E0B\u7684 OpenSpec \u89C4\u5212\u4EA7\u7269
1961
2538
 
1962
2539
  \u5982\u679C\u7528\u6237\u8F93\u5165\u7C7B\u4F3C \`/fet apply\` \u7684\u8BF7\u6C42\uFF0CCursor \u5F53\u524D\u7248\u672C\u672A\u5FC5\u4F1A\u628A\u672C\u6587\u4EF6\u6CE8\u518C\u4E3A\u539F\u751F slash command\u3002\u6B64\u65F6\u8BF7\u628A\u5B83\u5F53\u4F5C\u5DE5\u4F5C\u6D41\u610F\u56FE\uFF0C\u5E76\u63D0\u793A\u7528\u6237\u5728\u7EC8\u7AEF\u6267\u884C\u5BF9\u5E94\u7684 \`fet <cmd>\` \u547D\u4EE4\u3002
@@ -1964,7 +2541,7 @@ alwaysApply: false
1964
2541
  };
1965
2542
  }
1966
2543
  function renderSkill(command) {
1967
- const usage = command === "passthrough" ? "fet passthrough <openspec-command> [...args]" : `fet ${command}`;
2544
+ const usage = renderFetAdapterUsage(command, command === "passthrough" ? "[...args]" : "");
1968
2545
  if (command === "fill-context") {
1969
2546
  return `<!-- FET:MANAGED
1970
2547
  schemaVersion: 1
@@ -1982,6 +2559,10 @@ disable-model-invocation: false
1982
2559
 
1983
2560
  Run \`fet fill-context\` first if the IDE commands need refreshing.
1984
2561
 
2562
+ ${renderIdeModelPolicy(command)}
2563
+
2564
+ If GitNexus code graph context is available in Cursor, prefer it before broad repository scans. If it is unavailable, continue normally.
2565
+
1985
2566
  Then read:
1986
2567
 
1987
2568
  - AGENTS.md
@@ -2004,6 +2585,10 @@ description: Run FET-managed OpenSpec ${command} workflow from the terminal
2004
2585
  disable-model-invocation: true
2005
2586
  ---
2006
2587
 
2588
+ ${renderIdeModelPolicy(command)}
2589
+
2590
+ If GitNexus code graph context is available in Cursor, prefer it before broad repository scans. If it is unavailable, continue normally.
2591
+
2007
2592
  \u6CE8\u610F\uFF1A\u6B64\u6587\u4EF6\u91C7\u7528 Cursor Skill \u76EE\u5F55\u7ED3\u6784\u3002\u5B83\u63D0\u4F9B \`/fet-${command}\` \u98CE\u683C\u7684\u5DE5\u4F5C\u6D41\u8BF4\u660E\uFF0C\u4E0D\u627F\u8BFA\u6CE8\u518C \`/fet ${command}\` \u8FD9\u79CD\u5E26\u7A7A\u683C\u7684\u539F\u751F slash command\u3002
2008
2593
 
2009
2594
  \u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
@@ -2022,7 +2607,7 @@ var CursorAdapter = class {
2022
2607
  adapterVersion = 1;
2023
2608
  async detect(projectRoot) {
2024
2609
  return {
2025
- detected: await exists4(join13(projectRoot, ".cursor")),
2610
+ detected: await exists4(join16(projectRoot, ".cursor")),
2026
2611
  reason: "Cursor adapter is available for any project"
2027
2612
  };
2028
2613
  }
@@ -2039,7 +2624,7 @@ var CursorAdapter = class {
2039
2624
  const written = [];
2040
2625
  const skipped = [];
2041
2626
  for (const file of plan.files) {
2042
- const target = join13(projectRoot, file.path);
2627
+ const target = join16(projectRoot, file.path);
2043
2628
  const existing = await readExisting2(target);
2044
2629
  if (existing && !existing.includes("FET:MANAGED") && !force) {
2045
2630
  throw new FetError({
@@ -2052,7 +2637,7 @@ var CursorAdapter = class {
2052
2637
  if (existing && !existing.includes("FET:MANAGED") && force) {
2053
2638
  await createBackup(target);
2054
2639
  }
2055
- await mkdir7(dirname7(target), { recursive: true });
2640
+ await mkdir8(dirname8(target), { recursive: true });
2056
2641
  await atomicWrite(target, file.content);
2057
2642
  written.push(file.path);
2058
2643
  }
@@ -2062,7 +2647,7 @@ var CursorAdapter = class {
2062
2647
  const plan = await this.planInstall(projectRoot);
2063
2648
  const checks = [];
2064
2649
  for (const file of plan.files) {
2065
- const target = join13(projectRoot, file.path);
2650
+ const target = join16(projectRoot, file.path);
2066
2651
  const content = await readExisting2(target);
2067
2652
  const managed = Boolean(content?.includes("FET:MANAGED"));
2068
2653
  const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
@@ -2085,7 +2670,7 @@ async function readExisting2(path) {
2085
2670
  }
2086
2671
  async function exists4(path) {
2087
2672
  try {
2088
- await stat6(path);
2673
+ await stat7(path);
2089
2674
  return true;
2090
2675
  } catch {
2091
2676
  return false;
@@ -2093,17 +2678,17 @@ async function exists4(path) {
2093
2678
  }
2094
2679
 
2095
2680
  // src/openspec/adapter.ts
2096
- import { execFile as execFile3 } from "child_process";
2097
- import { promisify as promisify3 } from "util";
2681
+ import { execFile as execFile4 } from "child_process";
2682
+ import { promisify as promisify4 } from "util";
2098
2683
 
2099
2684
  // src/openspec/inspector.ts
2100
- import { readdir, stat as stat7 } from "fs/promises";
2101
- import { join as join14 } from "path";
2685
+ import { readdir, stat as stat8 } from "fs/promises";
2686
+ import { join as join17 } from "path";
2102
2687
  async function inspectOpenSpecProject(projectRoot) {
2103
- const openspecPath = join14(projectRoot, "openspec");
2104
- const changesPath = join14(openspecPath, "changes");
2105
- const legacyArchivePath = join14(openspecPath, "archive");
2106
- const changesArchivePath = join14(changesPath, "archive");
2688
+ const openspecPath = join17(projectRoot, "openspec");
2689
+ const changesPath = join17(openspecPath, "changes");
2690
+ const legacyArchivePath = join17(openspecPath, "archive");
2691
+ const changesArchivePath = join17(changesPath, "archive");
2107
2692
  return {
2108
2693
  exists: await exists5(openspecPath),
2109
2694
  changes: await listDirectories(changesPath, { exclude: ["archive"] }),
@@ -2111,13 +2696,13 @@ async function inspectOpenSpecProject(projectRoot) {
2111
2696
  };
2112
2697
  }
2113
2698
  async function inspectOpenSpecChange(projectRoot, changeId) {
2114
- const changePath = join14(projectRoot, "openspec", "changes", changeId);
2115
- const tasksPath = join14(changePath, "tasks.md");
2116
- const specsPath = join14(changePath, "specs");
2699
+ const changePath = join17(projectRoot, "openspec", "changes", changeId);
2700
+ const tasksPath = join17(changePath, "tasks.md");
2701
+ const specsPath = join17(changePath, "specs");
2117
2702
  return {
2118
2703
  changeId,
2119
2704
  exists: await exists5(changePath),
2120
- hasProposal: await exists5(join14(changePath, "proposal.md")),
2705
+ hasProposal: await exists5(join17(changePath, "proposal.md")),
2121
2706
  hasTasks: await exists5(tasksPath),
2122
2707
  hasSpecs: await exists5(specsPath),
2123
2708
  tasksPath,
@@ -2135,7 +2720,7 @@ async function listDirectories(path, options = {}) {
2135
2720
  }
2136
2721
  async function exists5(path) {
2137
2722
  try {
2138
- await stat7(path);
2723
+ await stat8(path);
2139
2724
  return true;
2140
2725
  } catch {
2141
2726
  return false;
@@ -2143,9 +2728,9 @@ async function exists5(path) {
2143
2728
  }
2144
2729
 
2145
2730
  // src/openspec/resolver.ts
2146
- import { execFile as execFile2 } from "child_process";
2147
- import { promisify as promisify2 } from "util";
2148
- var execFileAsync2 = promisify2(execFile2);
2731
+ import { execFile as execFile3 } from "child_process";
2732
+ import { promisify as promisify3 } from "util";
2733
+ var execFileAsync3 = promisify3(execFile3);
2149
2734
  async function resolveOpenSpecExecutable() {
2150
2735
  const executablePath = await findExecutable();
2151
2736
  const version = await readVersion(executablePath);
@@ -2192,7 +2777,7 @@ async function readVersion(executablePath) {
2192
2777
  }
2193
2778
  }
2194
2779
  function exec(command, args) {
2195
- return execFileAsync2(command, args, { shell: process.platform === "win32" });
2780
+ return execFileAsync3(command, args, { shell: process.platform === "win32" });
2196
2781
  }
2197
2782
 
2198
2783
  // src/openspec/runner.ts
@@ -2238,7 +2823,7 @@ async function runOpenSpec(executablePath, command, args, options) {
2238
2823
  }
2239
2824
 
2240
2825
  // src/openspec/adapter.ts
2241
- var execFileAsync3 = promisify3(execFile3);
2826
+ var execFileAsync4 = promisify4(execFile4);
2242
2827
  var DefaultOpenSpecAdapter = class {
2243
2828
  identity;
2244
2829
  async resolveExecutable() {
@@ -2250,7 +2835,7 @@ var DefaultOpenSpecAdapter = class {
2250
2835
  const executable = identity.executablePath === "npx openspec" ? "npx" : identity.executablePath;
2251
2836
  const args = identity.executablePath === "npx openspec" ? ["openspec", "--help"] : ["--help"];
2252
2837
  try {
2253
- const { stdout } = await execFileAsync3(executable, args, { shell: process.platform === "win32" });
2838
+ const { stdout } = await execFileAsync4(executable, args, { shell: process.platform === "win32" });
2254
2839
  return {
2255
2840
  version: identity.version,
2256
2841
  commands: parseCommands(stdout),
@@ -2294,12 +2879,12 @@ function parseCommands(help) {
2294
2879
  }
2295
2880
 
2296
2881
  // src/scanner/package.ts
2297
- import { readFile as readFile14, stat as stat8 } from "fs/promises";
2298
- import { join as join15 } from "path";
2882
+ import { readFile as readFile14, stat as stat9 } from "fs/promises";
2883
+ import { join as join18 } from "path";
2299
2884
  import { parse as parse2 } from "yaml";
2300
2885
  async function readPackageJson(projectRoot) {
2301
2886
  try {
2302
- return JSON.parse(await readFile14(join15(projectRoot, "package.json"), "utf8"));
2887
+ return JSON.parse(await readFile14(join18(projectRoot, "package.json"), "utf8"));
2303
2888
  } catch {
2304
2889
  return null;
2305
2890
  }
@@ -2365,7 +2950,7 @@ function detectFramework(pkg) {
2365
2950
  }
2366
2951
  async function detectLanguage(projectRoot, pkg) {
2367
2952
  const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
2368
- if (deps.typescript || await exists6(join15(projectRoot, "tsconfig.json"))) {
2953
+ if (deps.typescript || await exists6(join18(projectRoot, "tsconfig.json"))) {
2369
2954
  return "typescript";
2370
2955
  }
2371
2956
  return "javascript";
@@ -2380,7 +2965,7 @@ async function detectWorkspaces(projectRoot, pkg) {
2380
2965
  return packageWorkspaces;
2381
2966
  }
2382
2967
  try {
2383
- const workspace = parse2(await readFile14(join15(projectRoot, "pnpm-workspace.yaml"), "utf8"));
2968
+ const workspace = parse2(await readFile14(join18(projectRoot, "pnpm-workspace.yaml"), "utf8"));
2384
2969
  return (workspace?.packages ?? []).map((path) => ({
2385
2970
  name: path,
2386
2971
  path,
@@ -2400,7 +2985,7 @@ async function detectLockManagers(projectRoot) {
2400
2985
  ];
2401
2986
  const found = [];
2402
2987
  for (const [file, manager] of lockFiles) {
2403
- if (await exists6(join15(projectRoot, file))) {
2988
+ if (await exists6(join18(projectRoot, file))) {
2404
2989
  found.push(manager);
2405
2990
  }
2406
2991
  }
@@ -2417,7 +3002,7 @@ function scriptCommand(packageManager, name) {
2417
3002
  }
2418
3003
  async function exists6(path) {
2419
3004
  try {
2420
- await stat8(path);
3005
+ await stat9(path);
2421
3006
  return true;
2422
3007
  } catch {
2423
3008
  return false;
@@ -2425,13 +3010,13 @@ async function exists6(path) {
2425
3010
  }
2426
3011
 
2427
3012
  // src/scanner/routes.ts
2428
- import { readdir as readdir2, stat as stat9 } from "fs/promises";
2429
- import { join as join16, relative, sep } from "path";
3013
+ import { readdir as readdir2, stat as stat10 } from "fs/promises";
3014
+ import { join as join19, relative, sep } from "path";
2430
3015
  async function scanRoutes(projectRoot) {
2431
3016
  const candidates = ["src/routes", "src/pages", "app", "pages"];
2432
3017
  const routes = [];
2433
3018
  for (const candidate of candidates) {
2434
- const root = join16(projectRoot, candidate);
3019
+ const root = join19(projectRoot, candidate);
2435
3020
  if (!await exists7(root)) {
2436
3021
  continue;
2437
3022
  }
@@ -2459,7 +3044,7 @@ async function listFiles(root) {
2459
3044
  const entries = await readdir2(root, { withFileTypes: true });
2460
3045
  const files = [];
2461
3046
  for (const entry of entries) {
2462
- const path = join16(root, entry.name);
3047
+ const path = join19(root, entry.name);
2463
3048
  if (entry.isDirectory()) {
2464
3049
  files.push(...await listFiles(path));
2465
3050
  } else {
@@ -2470,7 +3055,7 @@ async function listFiles(root) {
2470
3055
  }
2471
3056
  async function exists7(path) {
2472
3057
  try {
2473
- await stat9(path);
3058
+ await stat10(path);
2474
3059
  return true;
2475
3060
  } catch {
2476
3061
  return false;
@@ -2523,6 +3108,11 @@ var OutputWriter = class {
2523
3108
  }
2524
3109
  }
2525
3110
  warn(message, details) {
3111
+ if (this.json) {
3112
+ process.stderr.write(`${JSON.stringify({ ok: true, warning: message, details }, null, 2)}
3113
+ `);
3114
+ return;
3115
+ }
2526
3116
  if (!this.json) {
2527
3117
  process.stderr.write(`\u8B66\u544A\uFF1A${message}${formatDetails(details)}
2528
3118
  `);
@@ -2612,6 +3202,13 @@ program.name("fet").description("Frontend workflow orchestration tool built arou
2612
3202
  addGlobalOptions(program.command("init")).description("\u521D\u59CB\u5316 FET + OpenSpec").action(wrap("init", initCommand));
2613
3203
  addGlobalOptions(program.command("update-context")).description("\u66F4\u65B0\u9879\u76EE\u4E0A\u4E0B\u6587").action(wrap("update-context", updateContextCommand));
2614
3204
  addGlobalOptions(program.command("fill-context")).description("Refresh IDE prompts for filling AGENTS.md placeholders").action(wrap("fill-context", fillContextCommand));
3205
+ var graph = addGlobalOptions(program.command("graph").description("Manage optional GitNexus code graph support"));
3206
+ for (const action of ["status", "setup", "doctor", "handoff"]) {
3207
+ addGlobalOptions(graph.command(action).description(`Run fet graph ${action}`)).action(wrap("graph", (ctx) => graphCommand(ctx, action)));
3208
+ }
3209
+ for (const action of ["init", "refresh"]) {
3210
+ addGlobalOptions(graph.command(`${action} [args...]`).description(`Run GitNexus analyze for graph ${action}`).allowUnknownOption(true).passThroughOptions()).action(wrap("graph", (ctx, args = []) => graphCommand(ctx, action, args)));
3211
+ }
2615
3212
  addGlobalOptions(program.command("doctor").description("\u8BCA\u65AD\u72B6\u6001\u3001\u914D\u7F6E\u4E0E\u4E00\u81F4\u6027").option("--fix-lock", "\u6E05\u7406 FET \u9501\u6587\u4EF6")).action(
2616
3213
  wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
2617
3214
  );
@@ -2633,6 +3230,8 @@ function wrap(command, handler) {
2633
3230
  const opts = isCommandLike(maybeCommand) ? { ...maybeCommand.parent?.opts(), ...maybeCommand.opts() } : program.opts();
2634
3231
  const ctx = await createCommandContext(command, { ...opts, ...extractGlobalOptions(args) });
2635
3232
  try {
3233
+ await confirmModelPolicyRecommendation(ctx);
3234
+ await warnIfContextPlaceholdersRemain(ctx);
2636
3235
  await handler(ctx, ...args);
2637
3236
  } catch (error) {
2638
3237
  const fetError = toFetError(error);
@@ -2641,6 +3240,40 @@ function wrap(command, handler) {
2641
3240
  }
2642
3241
  };
2643
3242
  }
3243
+ async function confirmModelPolicyRecommendation(ctx) {
3244
+ const mismatch = getCommandModelPolicyMismatch(ctx.command);
3245
+ if (!mismatch) {
3246
+ return;
3247
+ }
3248
+ const warning = formatModelPolicyMismatch(mismatch);
3249
+ ctx.output.warn(`${warning} You can stop now to switch models, or continue this command.`);
3250
+ if (ctx.yes || ctx.json || !process.stdin.isTTY || !process.stderr.isTTY) {
3251
+ return;
3252
+ }
3253
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
3254
+ try {
3255
+ const answer = (await rl.question("Continue anyway? [y/N] ")).trim().toLowerCase();
3256
+ if (answer !== "y" && answer !== "yes") {
3257
+ throw new FetError({
3258
+ code: "USER_CANCELLED" /* UserCancelled */,
3259
+ message: "Command cancelled so you can switch IDE model.",
3260
+ details: { command: ctx.command, detected: mismatch.detected, recommended: mismatch.recommended },
3261
+ suggestedCommand: `Switch IDE model, then rerun fet ${ctx.command}.`
3262
+ });
3263
+ }
3264
+ } finally {
3265
+ rl.close();
3266
+ }
3267
+ }
3268
+ async function warnIfContextPlaceholdersRemain(ctx) {
3269
+ if (["init", "update-context", "fill-context", "doctor"].includes(ctx.command)) {
3270
+ return;
3271
+ }
3272
+ const count2 = await countAgentsLlmPlaceholders(ctx.projectRoot);
3273
+ if (count2 > 0) {
3274
+ ctx.output.warn(renderAgentsPlaceholderWarning(count2));
3275
+ }
3276
+ }
2644
3277
  function isCommandLike(value) {
2645
3278
  return value instanceof Command;
2646
3279
  }