@nick848/fet 1.0.3 → 1.0.4

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
@@ -5,11 +5,12 @@ import {
5
5
  } from "../chunk-FZOVNHE7.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 stat2 } from "fs/promises";
13
+ import { join as join7 } from "path";
13
14
 
14
15
  // src/fs/atomic-write.ts
15
16
  import { dirname } from "path";
@@ -115,6 +116,48 @@ async function writeInitJournal(projectRoot, journal) {
115
116
  );
116
117
  }
117
118
 
119
+ // src/gitnexus.ts
120
+ import { execFile } from "child_process";
121
+ import { promisify } from "util";
122
+ var execFileAsync = promisify(execFile);
123
+ async function detectGitNexus(env = process.env) {
124
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
125
+ const executable = env.FET_GITNEXUS_EXECUTABLE?.trim() || "gitnexus";
126
+ try {
127
+ const { stdout, stderr } = await execFileAsync(executable, ["--version"], { shell: process.platform === "win32" });
128
+ return {
129
+ installed: true,
130
+ executablePath: executable,
131
+ version: (stdout.trim() || stderr.trim() || "unknown").split(/\r?\n/)[0] ?? "unknown",
132
+ checkedAt
133
+ };
134
+ } catch (error) {
135
+ return {
136
+ installed: false,
137
+ executablePath: executable,
138
+ version: null,
139
+ checkedAt,
140
+ error: error instanceof Error ? error.message : String(error)
141
+ };
142
+ }
143
+ }
144
+ function toGitNexusState(detection, previous) {
145
+ return {
146
+ provider: "gitnexus",
147
+ installed: detection.installed,
148
+ executablePath: detection.installed ? detection.executablePath : null,
149
+ version: detection.version,
150
+ checkedAt: detection.checkedAt,
151
+ recommendationShownAt: previous?.recommendationShownAt ?? null
152
+ };
153
+ }
154
+ function renderGitNexusRecommendation(state) {
155
+ if (state.installed) {
156
+ 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.`;
157
+ }
158
+ return "Optional GitNexus code graph support is not installed. Consider installing GitNexus later to speed up OpenSpec artifact generation and improve code insertion context.";
159
+ }
160
+
118
161
  // src/version.ts
119
162
  import { existsSync, readFileSync } from "fs";
120
163
  import { dirname as dirname4, join as join4, parse } from "path";
@@ -144,6 +187,7 @@ var AUTO_BEGIN = "<!-- FET:BEGIN AUTO -->";
144
187
  var AUTO_END = "<!-- FET:END AUTO -->";
145
188
  var USER_BEGIN = "<!-- FET:BEGIN USER -->";
146
189
  var USER_END = "<!-- FET:END USER -->";
190
+ var LLM_PLACEHOLDER_PATTERN = /\[NEEDS? LLM INPUT\]/;
147
191
  function hasManagedAutoRegion(content) {
148
192
  return count(content, AUTO_BEGIN) === 1 && count(content, AUTO_END) === 1 && content.indexOf(AUTO_BEGIN) < content.indexOf(AUTO_END);
149
193
  }
@@ -163,11 +207,41 @@ function replaceManagedRegion(existing, generated) {
163
207
  }
164
208
  const before = existing.slice(0, start);
165
209
  const after = existing.slice(end + AUTO_END.length);
210
+ const existingAuto = extractAuto(existing);
166
211
  const generatedAuto = extractAuto(generated);
167
212
  return `${before}${AUTO_BEGIN}
168
- ${generatedAuto}
213
+ ${mergeAutoRegion(existingAuto, generatedAuto)}
169
214
  ${AUTO_END}${after}`;
170
215
  }
216
+ function mergeAutoRegion(existingAuto, generatedAuto) {
217
+ const generatedSections = splitMarkdownSections(generatedAuto);
218
+ const existingSections = new Map(splitMarkdownSections(existingAuto).map((section) => [section.heading, section]));
219
+ if (!generatedSections.length || !existingSections.size) {
220
+ return generatedAuto;
221
+ }
222
+ return generatedSections.map((section) => {
223
+ const existing = existingSections.get(section.heading);
224
+ if (existing && LLM_PLACEHOLDER_PATTERN.test(section.body) && !LLM_PLACEHOLDER_PATTERN.test(existing.body)) {
225
+ return existing.raw.trim();
226
+ }
227
+ return section.raw.trim();
228
+ }).join("\n\n");
229
+ }
230
+ function splitMarkdownSections(content) {
231
+ const matches = [...content.matchAll(/^## .+$/gm)];
232
+ if (!matches.length) {
233
+ return [];
234
+ }
235
+ return matches.map((match, index) => {
236
+ const start = match.index ?? 0;
237
+ const end = matches[index + 1]?.index ?? content.length;
238
+ const raw = content.slice(start, end).trim();
239
+ const newline = raw.indexOf("\n");
240
+ const heading = newline === -1 ? raw.trim() : raw.slice(0, newline).trim();
241
+ const body = newline === -1 ? "" : raw.slice(newline + 1).trim();
242
+ return { heading, body, raw };
243
+ });
244
+ }
171
245
  function extractAuto(content) {
172
246
  const start = content.indexOf(AUTO_BEGIN);
173
247
  const end = content.indexOf(AUTO_END);
@@ -325,8 +399,8 @@ ${block}
325
399
  }
326
400
 
327
401
  // src/commands/update-context.ts
328
- import { readFile as readFile4 } from "fs/promises";
329
- import { join as join5 } from "path";
402
+ import { readFile as readFile5 } from "fs/promises";
403
+ import { join as join6 } from "path";
330
404
 
331
405
  // src/config/yaml.ts
332
406
  import { readFile as readFile3 } from "fs/promises";
@@ -345,23 +419,43 @@ async function mergeFetConfig(configPath, renderedFetYaml) {
345
419
  return doc.toString();
346
420
  }
347
421
 
422
+ // src/context-placeholders.ts
423
+ import { readFile as readFile4 } from "fs/promises";
424
+ import { join as join5 } from "path";
425
+ var AGENTS_LLM_PLACEHOLDER_PATTERN = /\[NEEDS? LLM INPUT\]/g;
426
+ async function countAgentsLlmPlaceholders(projectRoot) {
427
+ try {
428
+ const content = await readFile4(join5(projectRoot, "AGENTS.md"), "utf8");
429
+ return [...content.matchAll(AGENTS_LLM_PLACEHOLDER_PATTERN)].length;
430
+ } catch {
431
+ return 0;
432
+ }
433
+ }
434
+ function renderAgentsPlaceholderWarning(count2) {
435
+ return `AGENTS.md still contains ${count2} LLM placeholder(s). Run fet fill-context first so your IDE AI can replace them. Continuing current command.`;
436
+ }
437
+
348
438
  // src/commands/update-context.ts
349
439
  async function updateContextCommand(ctx) {
440
+ let contextResult = { warnings: [] };
350
441
  await withProjectLock(
351
442
  ctx.projectRoot,
352
443
  { command: "update-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
353
- async () => updateContextFiles(ctx)
444
+ async () => {
445
+ contextResult = await updateContextFiles(ctx);
446
+ }
354
447
  );
355
448
  ctx.output.result({
356
449
  ok: true,
357
450
  command: "update-context",
358
- summary: "\u5DF2\u66F4\u65B0 AGENTS.md \u4E0E openspec/config.yaml \u7684 FET \u6258\u7BA1\u533A\u57DF\u3002"
451
+ summary: "\u5DF2\u66F4\u65B0 AGENTS.md \u4E0E openspec/config.yaml \u7684 FET \u6258\u7BA1\u533A\u57DF\u3002",
452
+ warnings: contextResult.warnings
359
453
  });
360
454
  }
361
455
  async function updateContextFiles(ctx) {
362
456
  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");
457
+ const agentsPath = join6(ctx.projectRoot, "AGENTS.md");
458
+ const configPath = join6(ctx.projectRoot, "openspec", "config.yaml");
365
459
  const existingAgents = await readOptional(agentsPath);
366
460
  const warnings = [...scan.warnings];
367
461
  if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
@@ -388,6 +482,10 @@ async function updateContextFiles(ctx) {
388
482
  }
389
483
  await atomicWrite(agentsPath, replaceManagedRegion(existingAgents, renderAgentsMd(scan)));
390
484
  await atomicWrite(configPath, await mergeFetConfig(configPath, renderFetConfig(scan)));
485
+ const placeholderCount = await countAgentsLlmPlaceholders(ctx.projectRoot);
486
+ if (placeholderCount > 0) {
487
+ warnings.push(renderAgentsPlaceholderWarning(placeholderCount));
488
+ }
391
489
  const state = await ctx.stateStore.getOrCreateGlobal();
392
490
  state.context = {
393
491
  agentsMdUpdatedAt: scan.generatedAt,
@@ -399,7 +497,7 @@ async function updateContextFiles(ctx) {
399
497
  }
400
498
  async function readOptional(path) {
401
499
  try {
402
- return await readFile4(path, "utf8");
500
+ return await readFile5(path, "utf8");
403
501
  } catch {
404
502
  return null;
405
503
  }
@@ -407,7 +505,8 @@ async function readOptional(path) {
407
505
 
408
506
  // src/commands/init.ts
409
507
  async function initCommand(ctx) {
410
- const alreadyInitialized = await exists(join6(ctx.projectRoot, "openspec", "config.yaml"));
508
+ const alreadyInitialized = await exists(join7(ctx.projectRoot, "openspec", "config.yaml"));
509
+ let warnings = [];
411
510
  await withProjectLock(
412
511
  ctx.projectRoot,
413
512
  { command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
@@ -423,9 +522,17 @@ async function initCommand(ctx) {
423
522
  }
424
523
  }
425
524
  const contextResult = await updateContextFiles(ctx);
525
+ warnings = contextResult.warnings;
426
526
  await ensureGitignore(ctx);
427
527
  const state = await ctx.stateStore.getOrCreateGlobal();
428
528
  state.openspec = identity;
529
+ state.graph ??= {};
530
+ const gitnexus = toGitNexusState(await detectGitNexus(), state.graph.gitnexus);
531
+ if (!gitnexus.installed && !gitnexus.recommendationShownAt) {
532
+ warnings.push(renderGitNexusRecommendation(gitnexus));
533
+ gitnexus.recommendationShownAt = (/* @__PURE__ */ new Date()).toISOString();
534
+ }
535
+ state.graph.gitnexus = gitnexus;
429
536
  for (const adapter of ctx.toolAdapters) {
430
537
  const plan = await adapter.planInstall(ctx.projectRoot);
431
538
  const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
@@ -439,26 +546,24 @@ async function initCommand(ctx) {
439
546
  journal.completedAt = (/* @__PURE__ */ new Date()).toISOString();
440
547
  await writeInitJournal(ctx.projectRoot, journal);
441
548
  await ctx.stateStore.writeGlobal(state);
442
- for (const warning of contextResult.warnings) {
443
- ctx.output.warn(warning);
444
- }
445
549
  }
446
550
  );
447
551
  ctx.output.result({
448
552
  ok: true,
449
553
  command: "init",
450
554
  summary: "FET \u521D\u59CB\u5316\u5B8C\u6210\u3002",
555
+ warnings,
451
556
  nextSteps: ["\u4F7F\u7528 fet propose/new \u521B\u5EFA OpenSpec change", "\u4F7F\u7528 fet doctor \u68C0\u67E5\u9879\u76EE\u72B6\u6001"]
452
557
  });
453
558
  }
454
559
  async function ensureGitignore(ctx) {
455
- const gitignorePath = join6(ctx.projectRoot, ".gitignore");
560
+ const gitignorePath = join7(ctx.projectRoot, ".gitignore");
456
561
  const existing = await readOptional2(gitignorePath);
457
562
  await atomicWrite(gitignorePath, mergeGitignore(existing));
458
563
  }
459
564
  async function readOptional2(path) {
460
565
  try {
461
- return await readFile5(path, "utf8");
566
+ return await readFile6(path, "utf8");
462
567
  } catch {
463
568
  return null;
464
569
  }
@@ -473,19 +578,20 @@ async function exists(path) {
473
578
  }
474
579
 
475
580
  // src/commands/doctor.ts
476
- import { readFile as readFile6, stat as stat3 } from "fs/promises";
477
- import { join as join7 } from "path";
581
+ import { readFile as readFile7, stat as stat3 } from "fs/promises";
582
+ import { join as join8 } from "path";
478
583
  async function doctorCommand(ctx, options = {}) {
479
584
  const checks = [];
480
585
  checks.push(await checkOpenSpec(ctx));
481
586
  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")));
587
+ checks.push(await checkFile("agents", join8(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
588
+ checks.push(await checkFile("config", join8(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
589
+ checks.push(await checkPlaceholders(ctx.projectRoot));
590
+ checks.push(await checkGitNexus(ctx));
485
591
  for (const adapter of ctx.toolAdapters) {
486
592
  checks.push(...await adapter.doctor(ctx.projectRoot));
487
593
  }
488
- const lockPath = join7(ctx.projectRoot, "openspec", ".fet.lock");
594
+ const lockPath = join8(ctx.projectRoot, "openspec", ".fet.lock");
489
595
  if (await exists2(lockPath)) {
490
596
  if (options.fixLock) {
491
597
  await clearLock(ctx.projectRoot);
@@ -503,6 +609,25 @@ async function doctorCommand(ctx, options = {}) {
503
609
  data: checks
504
610
  });
505
611
  }
612
+ async function checkGitNexus(ctx) {
613
+ const global = await ctx.stateStore.readGlobal();
614
+ const state = toGitNexusState(await detectGitNexus(), global?.graph?.gitnexus);
615
+ if (global) {
616
+ global.graph ??= {};
617
+ global.graph.gitnexus = state;
618
+ await ctx.stateStore.writeGlobal(global);
619
+ }
620
+ return state.installed ? {
621
+ id: "gitnexus",
622
+ status: "pass",
623
+ message: `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"})`
624
+ } : {
625
+ id: "gitnexus",
626
+ status: "warn",
627
+ message: "Optional GitNexus code graph support is not installed",
628
+ suggestedCommand: "Install GitNexus later if you want OpenSpec artifacts to prefer a repository graph"
629
+ };
630
+ }
506
631
  async function checkOpenSpec(ctx) {
507
632
  try {
508
633
  const identity = await ctx.openSpec.resolveExecutable();
@@ -522,10 +647,10 @@ async function checkState(ctx) {
522
647
  async function checkFile(id, path, missing, suggestedCommand) {
523
648
  return await exists2(path) ? { id, status: "pass", message: `${id} \u5B58\u5728` } : { id, status: "warn", message: missing, suggestedCommand };
524
649
  }
525
- async function checkPlaceholders(path) {
650
+ async function checkPlaceholders(projectRoot) {
526
651
  try {
527
- const content = await readFile6(path, "utf8");
528
- const count2 = [...content.matchAll(/\[NEEDS? LLM INPUT\]/g)].length;
652
+ await readFile7(join8(projectRoot, "AGENTS.md"), "utf8");
653
+ const count2 = await countAgentsLlmPlaceholders(projectRoot);
529
654
  return count2 ? {
530
655
  id: "context-placeholders",
531
656
  status: "warn",
@@ -546,15 +671,14 @@ async function exists2(path) {
546
671
  }
547
672
 
548
673
  // 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;
674
+ import { mkdir as mkdir3 } from "fs/promises";
675
+ import { dirname as dirname5, join as join9 } from "path";
552
676
  async function fillContextCommand(ctx) {
553
677
  await withProjectLock(
554
678
  ctx.projectRoot,
555
679
  { command: "fill-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
556
680
  async () => {
557
- const handoffPath = join8(ctx.projectRoot, ".fet", "fill-context.md");
681
+ const handoffPath = join9(ctx.projectRoot, ".fet", "fill-context.md");
558
682
  await mkdir3(dirname5(handoffPath), { recursive: true });
559
683
  await atomicWrite(handoffPath, renderGenericHandoff());
560
684
  for (const adapter of ctx.toolAdapters) {
@@ -573,7 +697,7 @@ async function fillContextCommand(ctx) {
573
697
  }
574
698
  }
575
699
  );
576
- const placeholders = await countPlaceholders(join8(ctx.projectRoot, "AGENTS.md"));
700
+ const placeholders = await countAgentsLlmPlaceholders(ctx.projectRoot);
577
701
  ctx.output.result({
578
702
  ok: true,
579
703
  command: "fill-context",
@@ -608,23 +732,15 @@ Use the IDE AI to complete FET-generated placeholders.
608
732
  6. Run \`fet doctor\` and confirm no AGENTS.md placeholder warning remains.
609
733
  `;
610
734
  }
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;
617
- }
618
- }
619
735
 
620
736
  // src/commands/proxy.ts
621
737
  import { readFile as readFile10 } from "fs/promises";
622
- import { join as join10 } from "path";
738
+ import { join as join11 } from "path";
623
739
 
624
740
  // src/state/project.ts
625
- import { execFile } from "child_process";
626
- import { promisify } from "util";
627
- var execFileAsync = promisify(execFile);
741
+ import { execFile as execFile2 } from "child_process";
742
+ import { promisify as promisify2 } from "util";
743
+ var execFileAsync2 = promisify2(execFile2);
628
744
  async function detectProjectIdentity(projectRoot) {
629
745
  const [gitRoot, branch, headCommit] = await Promise.all([
630
746
  git(projectRoot, ["rev-parse", "--show-toplevel"]),
@@ -640,7 +756,7 @@ async function detectProjectIdentity(projectRoot) {
640
756
  }
641
757
  async function git(cwd, args) {
642
758
  try {
643
- const { stdout } = await execFileAsync("git", args, { cwd });
759
+ const { stdout } = await execFileAsync2("git", args, { cwd });
644
760
  return stdout.trim() || null;
645
761
  } catch {
646
762
  return null;
@@ -649,7 +765,7 @@ async function git(cwd, args) {
649
765
 
650
766
  // src/state/store.ts
651
767
  import { mkdir as mkdir4, readFile as readFile8 } from "fs/promises";
652
- import { join as join9 } from "path";
768
+ import { join as join10 } from "path";
653
769
 
654
770
  // src/state/schema.ts
655
771
  var phases = ["explore", "propose", "implement", "verify", "sync", "archive"];
@@ -670,6 +786,7 @@ function createGlobalState(fetVersion, project) {
670
786
  scannerVersion: 1
671
787
  },
672
788
  toolAdapters: {},
789
+ graph: {},
673
790
  verifyAuthorization: null,
674
791
  lastDoctor: null
675
792
  };
@@ -757,7 +874,7 @@ var StateStore = class {
757
874
  }
758
875
  async writeGlobal(state) {
759
876
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
760
- await mkdir4(join9(this.projectRoot, "openspec"), { recursive: true });
877
+ await mkdir4(join10(this.projectRoot, "openspec"), { recursive: true });
761
878
  await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
762
879
  `);
763
880
  }
@@ -778,15 +895,15 @@ var StateStore = class {
778
895
  }
779
896
  async writeChange(state) {
780
897
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
781
- await mkdir4(join9(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
898
+ await mkdir4(join10(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
782
899
  await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
783
900
  `);
784
901
  }
785
902
  globalPath() {
786
- return join9(this.projectRoot, "openspec", "fet-state.json");
903
+ return join10(this.projectRoot, "openspec", "fet-state.json");
787
904
  }
788
905
  changePath(changeId) {
789
- return join9(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
906
+ return join10(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
790
907
  }
791
908
  };
792
909
  function isNotFound(error) {
@@ -903,7 +1020,7 @@ async function createChangelogEntry(projectRoot, changeId) {
903
1020
  };
904
1021
  }
905
1022
  async function appendChangelog(projectRoot, entry) {
906
- const changelogPath = join10(projectRoot, "CHANGELOG.md");
1023
+ const changelogPath = join11(projectRoot, "CHANGELOG.md");
907
1024
  const existing = await readOptional3(changelogPath);
908
1025
  const block = `updateTime: ${entry.updateTime}
909
1026
  \u66F4\u65B0\u5185\u5BB9:${entry.content}
@@ -914,12 +1031,12 @@ ${block}` : block;
914
1031
  await atomicWrite(changelogPath, next);
915
1032
  }
916
1033
  async function readChangeRequirement(projectRoot, changeId) {
917
- const changeRoot = join10(projectRoot, "openspec", "changes", changeId);
918
- const proposal = await readOptional3(join10(changeRoot, "proposal.md"));
1034
+ const changeRoot = join11(projectRoot, "openspec", "changes", changeId);
1035
+ const proposal = await readOptional3(join11(changeRoot, "proposal.md"));
919
1036
  if (proposal) {
920
1037
  return summarizeMarkdown(proposal);
921
1038
  }
922
- const readme = await readOptional3(join10(changeRoot, "README.md"));
1039
+ const readme = await readOptional3(join11(changeRoot, "README.md"));
923
1040
  if (readme) {
924
1041
  return summarizeMarkdown(readme);
925
1042
  }
@@ -1064,7 +1181,7 @@ async function assertVerified(ctx) {
1064
1181
  // src/commands/verify.ts
1065
1182
  import { createHash } from "crypto";
1066
1183
  import { mkdir as mkdir5, readFile as readFile11, stat as stat4 } from "fs/promises";
1067
- import { join as join11 } from "path";
1184
+ import { join as join12 } from "path";
1068
1185
  async function verifyCommand(ctx, options) {
1069
1186
  if (options.auto) {
1070
1187
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
@@ -1131,8 +1248,8 @@ async function verifyCommand(ctx, options) {
1131
1248
  async function writeInstructions(ctx, changeId) {
1132
1249
  await assertChangeExists(ctx, changeId);
1133
1250
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1134
- const dir = join11(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
1135
- const instructionsPath = join11(dir, "verify-instructions.md");
1251
+ const dir = join12(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
1252
+ const instructionsPath = join12(dir, "verify-instructions.md");
1136
1253
  await mkdir5(dir, { recursive: true });
1137
1254
  await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
1138
1255
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
@@ -1149,7 +1266,7 @@ async function writeInstructions(ctx, changeId) {
1149
1266
  async function markDone(ctx, changeId) {
1150
1267
  await assertChangeExists(ctx, changeId);
1151
1268
  const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
1152
- const instructionsPath = join11(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
1269
+ const instructionsPath = join12(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
1153
1270
  const instructions = await readInstructions(instructionsPath, changeId);
1154
1271
  const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
1155
1272
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
@@ -1235,13 +1352,76 @@ async function resolveChangeId(ctx) {
1235
1352
  });
1236
1353
  }
1237
1354
 
1355
+ // src/model-policy.ts
1356
+ var HIGH_COST_MODEL_PATTERNS = [
1357
+ /gpt[-_ ]?5\.5/i,
1358
+ /glm[-_ ]?5(?:\.1)?/i,
1359
+ /claude.*opus/i,
1360
+ /opus/i,
1361
+ /claude.*sonnet/i,
1362
+ /sonnet/i
1363
+ ];
1364
+ var MODEL_ENV_KEYS = ["FET_IDE_MODEL", "FET_MODEL", "CODEX_MODEL", "CURSOR_MODEL", "OPENCODE_MODEL", "OPENAI_MODEL", "ANTHROPIC_MODEL"];
1365
+ function detectCurrentModel(env = process.env) {
1366
+ for (const key of MODEL_ENV_KEYS) {
1367
+ const value = env[key]?.trim();
1368
+ if (value) {
1369
+ return { source: key, name: value };
1370
+ }
1371
+ }
1372
+ return null;
1373
+ }
1374
+ function isHighCostModel(model) {
1375
+ return HIGH_COST_MODEL_PATTERNS.some((pattern) => pattern.test(model));
1376
+ }
1377
+ function getCommandModelPolicyMismatch(command, env = process.env) {
1378
+ if (env.FET_MODEL_POLICY === "off" || env.FET_SKIP_MODEL_POLICY === "1") {
1379
+ return null;
1380
+ }
1381
+ const detected = detectCurrentModel(env);
1382
+ if (!detected) {
1383
+ return null;
1384
+ }
1385
+ const highCost = isHighCostModel(detected.name);
1386
+ if (command === "apply") {
1387
+ if (!highCost) {
1388
+ return {
1389
+ command,
1390
+ detected,
1391
+ recommended: "high-cost",
1392
+ reason: "fet apply is the implementation phase and is recommended to use a high-capability/high-cost model."
1393
+ };
1394
+ }
1395
+ return null;
1396
+ }
1397
+ if (highCost) {
1398
+ return {
1399
+ command,
1400
+ detected,
1401
+ recommended: "low-cost",
1402
+ reason: `fet ${command} is not the implementation phase and is recommended to use a lower-cost model.`
1403
+ };
1404
+ }
1405
+ return null;
1406
+ }
1407
+ function formatModelPolicyMismatch(mismatch) {
1408
+ 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.";
1409
+ return `${mismatch.reason} Detected ${mismatch.detected.source}="${mismatch.detected.name}". ${switchHint}`;
1410
+ }
1411
+ function renderIdeModelPolicy(command) {
1412
+ if (command === "apply") {
1413
+ 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.";
1414
+ }
1415
+ 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.";
1416
+ }
1417
+
1238
1418
  // src/cli/context.ts
1239
1419
  import { resolve } from "path";
1240
1420
 
1241
1421
  // src/adapters/codex/index.ts
1242
1422
  import { mkdir as mkdir6, readFile as readFile12, stat as stat5 } from "fs/promises";
1243
1423
  import { homedir } from "os";
1244
- import { dirname as dirname6, join as join12 } from "path";
1424
+ import { dirname as dirname6, join as join13 } from "path";
1245
1425
 
1246
1426
  // src/adapters/commands.ts
1247
1427
  var FET_WORKFLOW_COMMANDS = [
@@ -1278,6 +1458,8 @@ Before doing FET or OpenSpec work in Codex, read:
1278
1458
  - openspec/config.yaml
1279
1459
  - the active change files under openspec/changes/<change-id>/, when a change is selected
1280
1460
 
1461
+ 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.
1462
+
1281
1463
  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
1464
 
1283
1465
  Command guides live in .codex/fet/commands/.
@@ -1313,8 +1495,12 @@ FET:END -->
1313
1495
 
1314
1496
  # fet ${command}
1315
1497
 
1498
+ ${renderIdeModelPolicy(command)}
1499
+
1316
1500
  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
1501
 
1502
+ 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.
1503
+
1318
1504
  Then run:
1319
1505
 
1320
1506
  \`\`\`sh
@@ -1337,8 +1523,12 @@ FET:END -->
1337
1523
 
1338
1524
  # fet passthrough
1339
1525
 
1526
+ ${renderIdeModelPolicy("passthrough")}
1527
+
1340
1528
  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
1529
 
1530
+ 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.
1531
+
1342
1532
  Then run:
1343
1533
 
1344
1534
  \`\`\`sh
@@ -1405,6 +1595,8 @@ Use FET as the entry point for this OpenSpec workflow.
1405
1595
 
1406
1596
  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
1597
 
1598
+ 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.
1599
+
1408
1600
  Run:
1409
1601
 
1410
1602
  \`\`\`sh
@@ -1425,8 +1617,12 @@ FET:END -->
1425
1617
 
1426
1618
  # fet fill-context
1427
1619
 
1620
+ ${renderIdeModelPolicy("fill-context")}
1621
+
1428
1622
  Use this command to complete FET-generated project context placeholders with Codex.
1429
1623
 
1624
+ 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.
1625
+
1430
1626
  First run:
1431
1627
 
1432
1628
  \`\`\`sh
@@ -1809,6 +2005,7 @@ Output:
1809
2005
  );
1810
2006
  }
1811
2007
  function renderManagedSlashPrompt(command, description, body) {
2008
+ const policyCommand = command.split(/\s+/)[1] ?? command;
1812
2009
  return `<!-- FET:MANAGED
1813
2010
  schemaVersion: 1
1814
2011
  fetVersion: ${FET_VERSION}
@@ -1822,6 +2019,10 @@ description: ${description}
1822
2019
  argument-hint: command arguments
1823
2020
  ---
1824
2021
 
2022
+ ${renderIdeModelPolicy(policyCommand)}
2023
+
2024
+ 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.
2025
+
1825
2026
  ${body}
1826
2027
  `;
1827
2028
  }
@@ -1832,7 +2033,7 @@ var CodexAdapter = class {
1832
2033
  adapterVersion = 1;
1833
2034
  async detect(projectRoot) {
1834
2035
  return {
1835
- detected: await exists3(join12(projectRoot, ".codex")) || await exists3(join12(projectRoot, "AGENTS.md")),
2036
+ detected: await exists3(join13(projectRoot, ".codex")) || await exists3(join13(projectRoot, "AGENTS.md")),
1836
2037
  reason: "Codex adapter is available for projects that use AGENTS.md"
1837
2038
  };
1838
2039
  }
@@ -1898,9 +2099,9 @@ var CodexAdapter = class {
1898
2099
  };
1899
2100
  function resolveTarget(projectRoot, file) {
1900
2101
  if (file.root === "codex-home") {
1901
- return join12(resolveCodexHome(), file.path);
2102
+ return join13(resolveCodexHome(), file.path);
1902
2103
  }
1903
- return join12(projectRoot, file.path);
2104
+ return join13(projectRoot, file.path);
1904
2105
  }
1905
2106
  function displayPathFor(file) {
1906
2107
  if (file.root === "codex-home") {
@@ -1909,7 +2110,7 @@ function displayPathFor(file) {
1909
2110
  return file.path;
1910
2111
  }
1911
2112
  function resolveCodexHome() {
1912
- return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join12(homedir(), ".codex");
2113
+ return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join13(homedir(), ".codex");
1913
2114
  }
1914
2115
  async function readExisting(path) {
1915
2116
  try {
@@ -1929,7 +2130,7 @@ async function exists3(path) {
1929
2130
 
1930
2131
  // src/adapters/cursor/index.ts
1931
2132
  import { mkdir as mkdir7, readFile as readFile13, stat as stat6 } from "fs/promises";
1932
- import { dirname as dirname7, join as join13 } from "path";
2133
+ import { dirname as dirname7, join as join14 } from "path";
1933
2134
 
1934
2135
  // src/adapters/cursor/templates.ts
1935
2136
  function cursorSkillFiles() {
@@ -1957,6 +2158,7 @@ alwaysApply: false
1957
2158
 
1958
2159
  - AGENTS.md
1959
2160
  - openspec/config.yaml
2161
+ - GitNexus code graph context, when available. Prefer it before broad repository scans; if it is unavailable, continue normally.
1960
2162
  - \u5F53\u524D change \u76EE\u5F55\u4E0B\u7684 OpenSpec \u89C4\u5212\u4EA7\u7269
1961
2163
 
1962
2164
  \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
@@ -1982,6 +2184,10 @@ disable-model-invocation: false
1982
2184
 
1983
2185
  Run \`fet fill-context\` first if the IDE commands need refreshing.
1984
2186
 
2187
+ ${renderIdeModelPolicy(command)}
2188
+
2189
+ If GitNexus code graph context is available in Cursor, prefer it before broad repository scans. If it is unavailable, continue normally.
2190
+
1985
2191
  Then read:
1986
2192
 
1987
2193
  - AGENTS.md
@@ -2004,6 +2210,10 @@ description: Run FET-managed OpenSpec ${command} workflow from the terminal
2004
2210
  disable-model-invocation: true
2005
2211
  ---
2006
2212
 
2213
+ ${renderIdeModelPolicy(command)}
2214
+
2215
+ If GitNexus code graph context is available in Cursor, prefer it before broad repository scans. If it is unavailable, continue normally.
2216
+
2007
2217
  \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
2218
 
2009
2219
  \u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
@@ -2022,7 +2232,7 @@ var CursorAdapter = class {
2022
2232
  adapterVersion = 1;
2023
2233
  async detect(projectRoot) {
2024
2234
  return {
2025
- detected: await exists4(join13(projectRoot, ".cursor")),
2235
+ detected: await exists4(join14(projectRoot, ".cursor")),
2026
2236
  reason: "Cursor adapter is available for any project"
2027
2237
  };
2028
2238
  }
@@ -2039,7 +2249,7 @@ var CursorAdapter = class {
2039
2249
  const written = [];
2040
2250
  const skipped = [];
2041
2251
  for (const file of plan.files) {
2042
- const target = join13(projectRoot, file.path);
2252
+ const target = join14(projectRoot, file.path);
2043
2253
  const existing = await readExisting2(target);
2044
2254
  if (existing && !existing.includes("FET:MANAGED") && !force) {
2045
2255
  throw new FetError({
@@ -2062,7 +2272,7 @@ var CursorAdapter = class {
2062
2272
  const plan = await this.planInstall(projectRoot);
2063
2273
  const checks = [];
2064
2274
  for (const file of plan.files) {
2065
- const target = join13(projectRoot, file.path);
2275
+ const target = join14(projectRoot, file.path);
2066
2276
  const content = await readExisting2(target);
2067
2277
  const managed = Boolean(content?.includes("FET:MANAGED"));
2068
2278
  const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
@@ -2093,17 +2303,17 @@ async function exists4(path) {
2093
2303
  }
2094
2304
 
2095
2305
  // src/openspec/adapter.ts
2096
- import { execFile as execFile3 } from "child_process";
2097
- import { promisify as promisify3 } from "util";
2306
+ import { execFile as execFile4 } from "child_process";
2307
+ import { promisify as promisify4 } from "util";
2098
2308
 
2099
2309
  // src/openspec/inspector.ts
2100
2310
  import { readdir, stat as stat7 } from "fs/promises";
2101
- import { join as join14 } from "path";
2311
+ import { join as join15 } from "path";
2102
2312
  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");
2313
+ const openspecPath = join15(projectRoot, "openspec");
2314
+ const changesPath = join15(openspecPath, "changes");
2315
+ const legacyArchivePath = join15(openspecPath, "archive");
2316
+ const changesArchivePath = join15(changesPath, "archive");
2107
2317
  return {
2108
2318
  exists: await exists5(openspecPath),
2109
2319
  changes: await listDirectories(changesPath, { exclude: ["archive"] }),
@@ -2111,13 +2321,13 @@ async function inspectOpenSpecProject(projectRoot) {
2111
2321
  };
2112
2322
  }
2113
2323
  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");
2324
+ const changePath = join15(projectRoot, "openspec", "changes", changeId);
2325
+ const tasksPath = join15(changePath, "tasks.md");
2326
+ const specsPath = join15(changePath, "specs");
2117
2327
  return {
2118
2328
  changeId,
2119
2329
  exists: await exists5(changePath),
2120
- hasProposal: await exists5(join14(changePath, "proposal.md")),
2330
+ hasProposal: await exists5(join15(changePath, "proposal.md")),
2121
2331
  hasTasks: await exists5(tasksPath),
2122
2332
  hasSpecs: await exists5(specsPath),
2123
2333
  tasksPath,
@@ -2143,9 +2353,9 @@ async function exists5(path) {
2143
2353
  }
2144
2354
 
2145
2355
  // src/openspec/resolver.ts
2146
- import { execFile as execFile2 } from "child_process";
2147
- import { promisify as promisify2 } from "util";
2148
- var execFileAsync2 = promisify2(execFile2);
2356
+ import { execFile as execFile3 } from "child_process";
2357
+ import { promisify as promisify3 } from "util";
2358
+ var execFileAsync3 = promisify3(execFile3);
2149
2359
  async function resolveOpenSpecExecutable() {
2150
2360
  const executablePath = await findExecutable();
2151
2361
  const version = await readVersion(executablePath);
@@ -2192,7 +2402,7 @@ async function readVersion(executablePath) {
2192
2402
  }
2193
2403
  }
2194
2404
  function exec(command, args) {
2195
- return execFileAsync2(command, args, { shell: process.platform === "win32" });
2405
+ return execFileAsync3(command, args, { shell: process.platform === "win32" });
2196
2406
  }
2197
2407
 
2198
2408
  // src/openspec/runner.ts
@@ -2238,7 +2448,7 @@ async function runOpenSpec(executablePath, command, args, options) {
2238
2448
  }
2239
2449
 
2240
2450
  // src/openspec/adapter.ts
2241
- var execFileAsync3 = promisify3(execFile3);
2451
+ var execFileAsync4 = promisify4(execFile4);
2242
2452
  var DefaultOpenSpecAdapter = class {
2243
2453
  identity;
2244
2454
  async resolveExecutable() {
@@ -2250,7 +2460,7 @@ var DefaultOpenSpecAdapter = class {
2250
2460
  const executable = identity.executablePath === "npx openspec" ? "npx" : identity.executablePath;
2251
2461
  const args = identity.executablePath === "npx openspec" ? ["openspec", "--help"] : ["--help"];
2252
2462
  try {
2253
- const { stdout } = await execFileAsync3(executable, args, { shell: process.platform === "win32" });
2463
+ const { stdout } = await execFileAsync4(executable, args, { shell: process.platform === "win32" });
2254
2464
  return {
2255
2465
  version: identity.version,
2256
2466
  commands: parseCommands(stdout),
@@ -2295,11 +2505,11 @@ function parseCommands(help) {
2295
2505
 
2296
2506
  // src/scanner/package.ts
2297
2507
  import { readFile as readFile14, stat as stat8 } from "fs/promises";
2298
- import { join as join15 } from "path";
2508
+ import { join as join16 } from "path";
2299
2509
  import { parse as parse2 } from "yaml";
2300
2510
  async function readPackageJson(projectRoot) {
2301
2511
  try {
2302
- return JSON.parse(await readFile14(join15(projectRoot, "package.json"), "utf8"));
2512
+ return JSON.parse(await readFile14(join16(projectRoot, "package.json"), "utf8"));
2303
2513
  } catch {
2304
2514
  return null;
2305
2515
  }
@@ -2365,7 +2575,7 @@ function detectFramework(pkg) {
2365
2575
  }
2366
2576
  async function detectLanguage(projectRoot, pkg) {
2367
2577
  const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
2368
- if (deps.typescript || await exists6(join15(projectRoot, "tsconfig.json"))) {
2578
+ if (deps.typescript || await exists6(join16(projectRoot, "tsconfig.json"))) {
2369
2579
  return "typescript";
2370
2580
  }
2371
2581
  return "javascript";
@@ -2380,7 +2590,7 @@ async function detectWorkspaces(projectRoot, pkg) {
2380
2590
  return packageWorkspaces;
2381
2591
  }
2382
2592
  try {
2383
- const workspace = parse2(await readFile14(join15(projectRoot, "pnpm-workspace.yaml"), "utf8"));
2593
+ const workspace = parse2(await readFile14(join16(projectRoot, "pnpm-workspace.yaml"), "utf8"));
2384
2594
  return (workspace?.packages ?? []).map((path) => ({
2385
2595
  name: path,
2386
2596
  path,
@@ -2400,7 +2610,7 @@ async function detectLockManagers(projectRoot) {
2400
2610
  ];
2401
2611
  const found = [];
2402
2612
  for (const [file, manager] of lockFiles) {
2403
- if (await exists6(join15(projectRoot, file))) {
2613
+ if (await exists6(join16(projectRoot, file))) {
2404
2614
  found.push(manager);
2405
2615
  }
2406
2616
  }
@@ -2426,12 +2636,12 @@ async function exists6(path) {
2426
2636
 
2427
2637
  // src/scanner/routes.ts
2428
2638
  import { readdir as readdir2, stat as stat9 } from "fs/promises";
2429
- import { join as join16, relative, sep } from "path";
2639
+ import { join as join17, relative, sep } from "path";
2430
2640
  async function scanRoutes(projectRoot) {
2431
2641
  const candidates = ["src/routes", "src/pages", "app", "pages"];
2432
2642
  const routes = [];
2433
2643
  for (const candidate of candidates) {
2434
- const root = join16(projectRoot, candidate);
2644
+ const root = join17(projectRoot, candidate);
2435
2645
  if (!await exists7(root)) {
2436
2646
  continue;
2437
2647
  }
@@ -2459,7 +2669,7 @@ async function listFiles(root) {
2459
2669
  const entries = await readdir2(root, { withFileTypes: true });
2460
2670
  const files = [];
2461
2671
  for (const entry of entries) {
2462
- const path = join16(root, entry.name);
2672
+ const path = join17(root, entry.name);
2463
2673
  if (entry.isDirectory()) {
2464
2674
  files.push(...await listFiles(path));
2465
2675
  } else {
@@ -2523,6 +2733,11 @@ var OutputWriter = class {
2523
2733
  }
2524
2734
  }
2525
2735
  warn(message, details) {
2736
+ if (this.json) {
2737
+ process.stderr.write(`${JSON.stringify({ ok: true, warning: message, details }, null, 2)}
2738
+ `);
2739
+ return;
2740
+ }
2526
2741
  if (!this.json) {
2527
2742
  process.stderr.write(`\u8B66\u544A\uFF1A${message}${formatDetails(details)}
2528
2743
  `);
@@ -2633,6 +2848,8 @@ function wrap(command, handler) {
2633
2848
  const opts = isCommandLike(maybeCommand) ? { ...maybeCommand.parent?.opts(), ...maybeCommand.opts() } : program.opts();
2634
2849
  const ctx = await createCommandContext(command, { ...opts, ...extractGlobalOptions(args) });
2635
2850
  try {
2851
+ await confirmModelPolicyRecommendation(ctx);
2852
+ await warnIfContextPlaceholdersRemain(ctx);
2636
2853
  await handler(ctx, ...args);
2637
2854
  } catch (error) {
2638
2855
  const fetError = toFetError(error);
@@ -2641,6 +2858,40 @@ function wrap(command, handler) {
2641
2858
  }
2642
2859
  };
2643
2860
  }
2861
+ async function confirmModelPolicyRecommendation(ctx) {
2862
+ const mismatch = getCommandModelPolicyMismatch(ctx.command);
2863
+ if (!mismatch) {
2864
+ return;
2865
+ }
2866
+ const warning = formatModelPolicyMismatch(mismatch);
2867
+ ctx.output.warn(`${warning} You can stop now to switch models, or continue this command.`);
2868
+ if (ctx.yes || ctx.json || !process.stdin.isTTY || !process.stderr.isTTY) {
2869
+ return;
2870
+ }
2871
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
2872
+ try {
2873
+ const answer = (await rl.question("Continue anyway? [y/N] ")).trim().toLowerCase();
2874
+ if (answer !== "y" && answer !== "yes") {
2875
+ throw new FetError({
2876
+ code: "USER_CANCELLED" /* UserCancelled */,
2877
+ message: "Command cancelled so you can switch IDE model.",
2878
+ details: { command: ctx.command, detected: mismatch.detected, recommended: mismatch.recommended },
2879
+ suggestedCommand: `Switch IDE model, then rerun fet ${ctx.command}.`
2880
+ });
2881
+ }
2882
+ } finally {
2883
+ rl.close();
2884
+ }
2885
+ }
2886
+ async function warnIfContextPlaceholdersRemain(ctx) {
2887
+ if (["init", "update-context", "fill-context", "doctor"].includes(ctx.command)) {
2888
+ return;
2889
+ }
2890
+ const count2 = await countAgentsLlmPlaceholders(ctx.projectRoot);
2891
+ if (count2 > 0) {
2892
+ ctx.output.warn(renderAgentsPlaceholderWarning(count2));
2893
+ }
2894
+ }
2644
2895
  function isCommandLike(value) {
2645
2896
  return value instanceof Command;
2646
2897
  }