@nick848/fet 1.0.2 → 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,18 +578,20 @@ async function exists(path) {
473
578
  }
474
579
 
475
580
  // src/commands/doctor.ts
476
- import { 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"));
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));
484
591
  for (const adapter of ctx.toolAdapters) {
485
592
  checks.push(...await adapter.doctor(ctx.projectRoot));
486
593
  }
487
- const lockPath = join7(ctx.projectRoot, "openspec", ".fet.lock");
594
+ const lockPath = join8(ctx.projectRoot, "openspec", ".fet.lock");
488
595
  if (await exists2(lockPath)) {
489
596
  if (options.fixLock) {
490
597
  await clearLock(ctx.projectRoot);
@@ -502,6 +609,25 @@ async function doctorCommand(ctx, options = {}) {
502
609
  data: checks
503
610
  });
504
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
+ }
505
631
  async function checkOpenSpec(ctx) {
506
632
  try {
507
633
  const identity = await ctx.openSpec.resolveExecutable();
@@ -521,6 +647,20 @@ async function checkState(ctx) {
521
647
  async function checkFile(id, path, missing, suggestedCommand) {
522
648
  return await exists2(path) ? { id, status: "pass", message: `${id} \u5B58\u5728` } : { id, status: "warn", message: missing, suggestedCommand };
523
649
  }
650
+ async function checkPlaceholders(projectRoot) {
651
+ try {
652
+ await readFile7(join8(projectRoot, "AGENTS.md"), "utf8");
653
+ const count2 = await countAgentsLlmPlaceholders(projectRoot);
654
+ return count2 ? {
655
+ id: "context-placeholders",
656
+ status: "warn",
657
+ message: `AGENTS.md has ${count2} LLM placeholder(s)`,
658
+ suggestedCommand: "fet fill-context"
659
+ } : { id: "context-placeholders", status: "pass", message: "AGENTS.md placeholders resolved" };
660
+ } catch {
661
+ return { id: "context-placeholders", status: "warn", message: "AGENTS.md missing", suggestedCommand: "fet update-context" };
662
+ }
663
+ }
524
664
  async function exists2(path) {
525
665
  try {
526
666
  await stat3(path);
@@ -530,10 +670,77 @@ async function exists2(path) {
530
670
  }
531
671
  }
532
672
 
673
+ // src/commands/fill-context.ts
674
+ import { mkdir as mkdir3 } from "fs/promises";
675
+ import { dirname as dirname5, join as join9 } from "path";
676
+ async function fillContextCommand(ctx) {
677
+ await withProjectLock(
678
+ ctx.projectRoot,
679
+ { command: "fill-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
680
+ async () => {
681
+ const handoffPath = join9(ctx.projectRoot, ".fet", "fill-context.md");
682
+ await mkdir3(dirname5(handoffPath), { recursive: true });
683
+ await atomicWrite(handoffPath, renderGenericHandoff());
684
+ for (const adapter of ctx.toolAdapters) {
685
+ const plan = await adapter.planInstall(ctx.projectRoot);
686
+ const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
687
+ const state = await ctx.stateStore.getOrCreateGlobal();
688
+ state.toolAdapters[adapter.tool] = {
689
+ adapterVersion: adapter.adapterVersion,
690
+ installed: true,
691
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
692
+ };
693
+ await ctx.stateStore.writeGlobal(state);
694
+ if (ctx.verbose) {
695
+ ctx.output.info(`Updated ${adapter.tool} adapter`, { written: result.written });
696
+ }
697
+ }
698
+ }
699
+ );
700
+ const placeholders = await countAgentsLlmPlaceholders(ctx.projectRoot);
701
+ ctx.output.result({
702
+ ok: true,
703
+ command: "fill-context",
704
+ summary: placeholders ? `Found ${placeholders} AGENTS.md placeholder(s). Use your IDE AI to fill them.` : "No AGENTS.md placeholders found. IDE fill-context commands were refreshed.",
705
+ nextSteps: placeholders ? [
706
+ "Cursor: run /fet-fill-context",
707
+ "Codex: run /prompts:fet-fill-context",
708
+ "OpenCode or other IDEs: open .fet/fill-context.md or run fet fill-context for handoff instructions"
709
+ ] : ["Run fet doctor to confirm project context health"],
710
+ data: {
711
+ placeholders,
712
+ cursorCommand: "/fet-fill-context",
713
+ codexCommand: "/prompts:fet-fill-context"
714
+ }
715
+ });
716
+ }
717
+ function renderGenericHandoff() {
718
+ return `<!-- FET:MANAGED
719
+ schemaVersion: 1
720
+ generator: fill-context
721
+ FET:END -->
722
+
723
+ # FET Fill Context
724
+
725
+ Use the IDE AI to complete FET-generated placeholders.
726
+
727
+ 1. Read AGENTS.md and openspec/config.yaml.
728
+ 2. Inspect README files, package scripts, routes, tests, source layout, and project conventions.
729
+ 3. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete project-specific content.
730
+ 4. Preserve FET managed markers.
731
+ 5. Do not modify business code.
732
+ 6. Run \`fet doctor\` and confirm no AGENTS.md placeholder warning remains.
733
+ `;
734
+ }
735
+
736
+ // src/commands/proxy.ts
737
+ import { readFile as readFile10 } from "fs/promises";
738
+ import { join as join11 } from "path";
739
+
533
740
  // src/state/project.ts
534
- import { execFile } from "child_process";
535
- import { promisify } from "util";
536
- var execFileAsync = promisify(execFile);
741
+ import { execFile as execFile2 } from "child_process";
742
+ import { promisify as promisify2 } from "util";
743
+ var execFileAsync2 = promisify2(execFile2);
537
744
  async function detectProjectIdentity(projectRoot) {
538
745
  const [gitRoot, branch, headCommit] = await Promise.all([
539
746
  git(projectRoot, ["rev-parse", "--show-toplevel"]),
@@ -549,7 +756,7 @@ async function detectProjectIdentity(projectRoot) {
549
756
  }
550
757
  async function git(cwd, args) {
551
758
  try {
552
- const { stdout } = await execFileAsync("git", args, { cwd });
759
+ const { stdout } = await execFileAsync2("git", args, { cwd });
553
760
  return stdout.trim() || null;
554
761
  } catch {
555
762
  return null;
@@ -557,8 +764,8 @@ async function git(cwd, args) {
557
764
  }
558
765
 
559
766
  // src/state/store.ts
560
- import { mkdir as mkdir3, readFile as readFile6 } from "fs/promises";
561
- import { join as join8 } from "path";
767
+ import { mkdir as mkdir4, readFile as readFile8 } from "fs/promises";
768
+ import { join as join10 } from "path";
562
769
 
563
770
  // src/state/schema.ts
564
771
  var phases = ["explore", "propose", "implement", "verify", "sync", "archive"];
@@ -579,6 +786,7 @@ function createGlobalState(fetVersion, project) {
579
786
  scannerVersion: 1
580
787
  },
581
788
  toolAdapters: {},
789
+ graph: {},
582
790
  verifyAuthorization: null,
583
791
  lastDoctor: null
584
792
  };
@@ -651,7 +859,7 @@ var StateStore = class {
651
859
  project;
652
860
  async readGlobal() {
653
861
  try {
654
- const value = JSON.parse(await readFile6(this.globalPath(), "utf8"));
862
+ const value = JSON.parse(await readFile8(this.globalPath(), "utf8"));
655
863
  assertGlobalState(value);
656
864
  return value;
657
865
  } catch (error) {
@@ -666,13 +874,13 @@ var StateStore = class {
666
874
  }
667
875
  async writeGlobal(state) {
668
876
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
669
- await mkdir3(join8(this.projectRoot, "openspec"), { recursive: true });
877
+ await mkdir4(join10(this.projectRoot, "openspec"), { recursive: true });
670
878
  await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
671
879
  `);
672
880
  }
673
881
  async readChange(changeId) {
674
882
  try {
675
- const value = JSON.parse(await readFile6(this.changePath(changeId), "utf8"));
883
+ const value = JSON.parse(await readFile8(this.changePath(changeId), "utf8"));
676
884
  assertChangeState(value);
677
885
  return value;
678
886
  } catch (error) {
@@ -687,15 +895,15 @@ var StateStore = class {
687
895
  }
688
896
  async writeChange(state) {
689
897
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
690
- await mkdir3(join8(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
898
+ await mkdir4(join10(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
691
899
  await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
692
900
  `);
693
901
  }
694
902
  globalPath() {
695
- return join8(this.projectRoot, "openspec", "fet-state.json");
903
+ return join10(this.projectRoot, "openspec", "fet-state.json");
696
904
  }
697
905
  changePath(changeId) {
698
- return join8(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
906
+ return join10(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
699
907
  }
700
908
  };
701
909
  function isNotFound(error) {
@@ -703,11 +911,11 @@ function isNotFound(error) {
703
911
  }
704
912
 
705
913
  // src/state/tasks.ts
706
- import { readFile as readFile7 } from "fs/promises";
914
+ import { readFile as readFile9 } from "fs/promises";
707
915
  async function readCompletedTaskIds(tasksPath) {
708
916
  let content;
709
917
  try {
710
- content = await readFile7(tasksPath, "utf8");
918
+ content = await readFile9(tasksPath, "utf8");
711
919
  } catch {
712
920
  return [];
713
921
  }
@@ -746,6 +954,8 @@ async function proxyCommand(ctx, command, args) {
746
954
  await assertVerified(ctx);
747
955
  }
748
956
  const mapped = await mapOpenSpecCommand(ctx, command, openSpecArgs);
957
+ const targetChangeId = command === "archive" ? mapped.args[0] ?? ctx.changeId ?? null : ctx.changeId ?? null;
958
+ const changelogEntry = command === "archive" && targetChangeId ? await createChangelogEntry(ctx.projectRoot, targetChangeId) : null;
749
959
  const result = await ctx.openSpec.run(mapped.command, mapped.args, { cwd: ctx.projectRoot, stdio: ctx.json ? "pipe" : "inherit" });
750
960
  if (result.exitCode !== 0) {
751
961
  throw new FetError({
@@ -755,15 +965,25 @@ async function proxyCommand(ctx, command, args) {
755
965
  recoverable: true
756
966
  });
757
967
  }
968
+ if (changelogEntry) {
969
+ await appendChangelog(ctx.projectRoot, changelogEntry);
970
+ }
758
971
  const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
759
972
  const state = await ctx.stateStore.getOrCreateGlobal();
760
973
  state.openChangeIds = inspection.changes;
761
- if (ctx.changeId && inspection.changes.includes(ctx.changeId)) {
762
- state.activeChangeId = ctx.changeId;
763
- } else if (state.activeChangeId && !inspection.changes.includes(state.activeChangeId)) {
764
- state.activeChangeId = inspection.changes.length === 1 ? inspection.changes[0] ?? null : null;
765
- } else if (!state.activeChangeId && inspection.changes.length === 1) {
766
- state.activeChangeId = inspection.changes[0] ?? null;
974
+ if (command === "archive") {
975
+ if (!state.activeChangeId || state.activeChangeId === targetChangeId || !inspection.changes.includes(state.activeChangeId)) {
976
+ state.activeChangeId = null;
977
+ }
978
+ state.verifyAuthorization = null;
979
+ } else {
980
+ if (ctx.changeId && inspection.changes.includes(ctx.changeId)) {
981
+ state.activeChangeId = ctx.changeId;
982
+ } else if (state.activeChangeId && !inspection.changes.includes(state.activeChangeId)) {
983
+ state.activeChangeId = inspection.changes.length === 1 ? inspection.changes[0] ?? null : null;
984
+ } else if (!state.activeChangeId && inspection.changes.length === 1) {
985
+ state.activeChangeId = inspection.changes[0] ?? null;
986
+ }
767
987
  }
768
988
  await ctx.stateStore.writeGlobal(state);
769
989
  const changeId = ctx.changeId ?? state.activeChangeId;
@@ -793,6 +1013,58 @@ async function proxyCommand(ctx, command, args) {
793
1013
  summary: `fet ${command} \u5B8C\u6210\u3002`
794
1014
  });
795
1015
  }
1016
+ async function createChangelogEntry(projectRoot, changeId) {
1017
+ return {
1018
+ updateTime: formatLocalTimestamp(/* @__PURE__ */ new Date()),
1019
+ content: await readChangeRequirement(projectRoot, changeId)
1020
+ };
1021
+ }
1022
+ async function appendChangelog(projectRoot, entry) {
1023
+ const changelogPath = join11(projectRoot, "CHANGELOG.md");
1024
+ const existing = await readOptional3(changelogPath);
1025
+ const block = `updateTime: ${entry.updateTime}
1026
+ \u66F4\u65B0\u5185\u5BB9:${entry.content}
1027
+ `;
1028
+ const next = existing?.trimEnd() ? `${existing.trimEnd()}
1029
+
1030
+ ${block}` : block;
1031
+ await atomicWrite(changelogPath, next);
1032
+ }
1033
+ async function readChangeRequirement(projectRoot, changeId) {
1034
+ const changeRoot = join11(projectRoot, "openspec", "changes", changeId);
1035
+ const proposal = await readOptional3(join11(changeRoot, "proposal.md"));
1036
+ if (proposal) {
1037
+ return summarizeMarkdown(proposal);
1038
+ }
1039
+ const readme = await readOptional3(join11(changeRoot, "README.md"));
1040
+ if (readme) {
1041
+ return summarizeMarkdown(readme);
1042
+ }
1043
+ return changeId;
1044
+ }
1045
+ function summarizeMarkdown(content) {
1046
+ const normalized = content.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("<!--") && !line.startsWith("---")).join(" ");
1047
+ return normalized || "\u672A\u63D0\u4F9B\u53D8\u66F4\u9700\u6C42";
1048
+ }
1049
+ async function readOptional3(path) {
1050
+ try {
1051
+ return await readFile10(path, "utf8");
1052
+ } catch {
1053
+ return null;
1054
+ }
1055
+ }
1056
+ function formatLocalTimestamp(date) {
1057
+ const year = date.getFullYear();
1058
+ const month = pad(date.getMonth() + 1);
1059
+ const day = pad(date.getDate());
1060
+ const hours = pad(date.getHours());
1061
+ const minutes = pad(date.getMinutes());
1062
+ const seconds = pad(date.getSeconds());
1063
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
1064
+ }
1065
+ function pad(value) {
1066
+ return String(value).padStart(2, "0");
1067
+ }
796
1068
  async function passthroughCommand(ctx, command, args) {
797
1069
  const result = await ctx.openSpec.run(command, stripFetOptions(args), { cwd: ctx.projectRoot, stdio: ctx.json ? "pipe" : "inherit" });
798
1070
  if (result.exitCode !== 0) {
@@ -826,32 +1098,37 @@ function stripFetOptions(args) {
826
1098
  async function mapOpenSpecCommand(ctx, command, args) {
827
1099
  switch (command) {
828
1100
  case "propose":
829
- case "new":
830
- return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
831
1101
  case "continue":
832
- return { command: "instructions", args: [...withoutUndefined(args[0] ? [args[0]] : ["proposal"]), "--change", await requireChangeId(ctx)] };
833
1102
  case "ff":
834
- return { command: "status", args: ["--change", await requireChangeId(ctx)] };
835
1103
  case "apply":
836
- return { command: "instructions", args: ["apply", "--change", await requireChangeId(ctx)] };
837
1104
  case "sync":
838
- return { command: "validate", args: [ctx.changeId ?? args[0] ?? await requireChangeId(ctx), "--type", "change", "--strict"] };
1105
+ case "bulk-archive":
1106
+ case "explore":
1107
+ case "onboard":
1108
+ return { command, args: withGlobalChange(ctx, args) };
1109
+ case "new":
1110
+ return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
839
1111
  case "archive":
840
1112
  return { command: "archive", args: [ctx.changeId ?? args[0] ?? await requireChangeId(ctx), ...args.slice(ctx.changeId ? 0 : 1)] };
1113
+ /*
841
1114
  case "bulk-archive":
842
1115
  throw new FetError({
843
- code: "INVALID_ARGUMENTS" /* InvalidArguments */,
844
- message: "OpenSpec 1.2.0 \u4E0D\u63D0\u4F9B bulk-archive \u9876\u5C42\u547D\u4EE4",
845
- suggestedCommand: "\u9010\u4E2A\u6267\u884C fet archive --change <change-id>"
1116
+ code: ErrorCode.InvalidArguments,
1117
+ message: "OpenSpec 1.2.0 不提供 bulk-archive 顶层命令",
1118
+ suggestedCommand: "逐个执行 fet archive --change <change-id>"
846
1119
  });
847
1120
  case "explore":
848
- return { command: "instructions", args: ["proposal", "--change", await requireChangeId(ctx)] };
1121
+ return { command: "explore", args: ctx.changeId ? ["--change", ctx.changeId, ...args] : args };
849
1122
  case "onboard":
850
1123
  return { command: "instructions", args: [] };
1124
+ */
851
1125
  default:
852
1126
  return { command, args };
853
1127
  }
854
1128
  }
1129
+ function withGlobalChange(ctx, args) {
1130
+ return ctx.changeId ? ["--change", ctx.changeId, ...args] : args;
1131
+ }
855
1132
  async function requireChangeId(ctx) {
856
1133
  if (ctx.changeId) {
857
1134
  return ctx.changeId;
@@ -871,9 +1148,6 @@ async function requireChangeId(ctx) {
871
1148
  suggestedCommand: "\u6DFB\u52A0 --change <change-id>"
872
1149
  });
873
1150
  }
874
- function withoutUndefined(values) {
875
- return values.filter(Boolean);
876
- }
877
1151
  async function assertVerified(ctx) {
878
1152
  const global = await ctx.stateStore.getOrCreateGlobal();
879
1153
  const changeId = ctx.changeId ?? global.activeChangeId;
@@ -906,8 +1180,8 @@ async function assertVerified(ctx) {
906
1180
 
907
1181
  // src/commands/verify.ts
908
1182
  import { createHash } from "crypto";
909
- import { mkdir as mkdir4, readFile as readFile8, stat as stat4 } from "fs/promises";
910
- import { join as join9 } from "path";
1183
+ import { mkdir as mkdir5, readFile as readFile11, stat as stat4 } from "fs/promises";
1184
+ import { join as join12 } from "path";
911
1185
  async function verifyCommand(ctx, options) {
912
1186
  if (options.auto) {
913
1187
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
@@ -974,9 +1248,9 @@ async function verifyCommand(ctx, options) {
974
1248
  async function writeInstructions(ctx, changeId) {
975
1249
  await assertChangeExists(ctx, changeId);
976
1250
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
977
- const dir = join9(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
978
- const instructionsPath = join9(dir, "verify-instructions.md");
979
- await mkdir4(dir, { recursive: true });
1251
+ const dir = join12(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
1252
+ const instructionsPath = join12(dir, "verify-instructions.md");
1253
+ await mkdir5(dir, { recursive: true });
980
1254
  await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
981
1255
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
982
1256
  state.currentPhase = "verify";
@@ -992,7 +1266,7 @@ async function writeInstructions(ctx, changeId) {
992
1266
  async function markDone(ctx, changeId) {
993
1267
  await assertChangeExists(ctx, changeId);
994
1268
  const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
995
- const instructionsPath = join9(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
1269
+ const instructionsPath = join12(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
996
1270
  const instructions = await readInstructions(instructionsPath, changeId);
997
1271
  const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
998
1272
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
@@ -1028,7 +1302,7 @@ async function assertChangeExists(ctx, changeId) {
1028
1302
  async function readInstructions(path, changeId) {
1029
1303
  try {
1030
1304
  await stat4(path);
1031
- const content = await readFile8(path, "utf8");
1305
+ const content = await readFile11(path, "utf8");
1032
1306
  const fileChangeId = readFrontMatterValue(content, "changeId");
1033
1307
  if (fileChangeId !== changeId) {
1034
1308
  throw new FetError({
@@ -1078,13 +1352,76 @@ async function resolveChangeId(ctx) {
1078
1352
  });
1079
1353
  }
1080
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
+
1081
1418
  // src/cli/context.ts
1082
1419
  import { resolve } from "path";
1083
1420
 
1084
1421
  // src/adapters/codex/index.ts
1085
- import { mkdir as mkdir5, readFile as readFile9, stat as stat5 } from "fs/promises";
1422
+ import { mkdir as mkdir6, readFile as readFile12, stat as stat5 } from "fs/promises";
1086
1423
  import { homedir } from "os";
1087
- import { dirname as dirname5, join as join10 } from "path";
1424
+ import { dirname as dirname6, join as join13 } from "path";
1088
1425
 
1089
1426
  // src/adapters/commands.ts
1090
1427
  var FET_WORKFLOW_COMMANDS = [
@@ -1100,7 +1437,7 @@ var FET_WORKFLOW_COMMANDS = [
1100
1437
  "bulk-archive",
1101
1438
  "onboard"
1102
1439
  ];
1103
- var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "passthrough"];
1440
+ var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "fill-context", "passthrough"];
1104
1441
 
1105
1442
  // src/adapters/codex/templates.ts
1106
1443
  function codexGuideFile() {
@@ -1121,6 +1458,8 @@ Before doing FET or OpenSpec work in Codex, read:
1121
1458
  - openspec/config.yaml
1122
1459
  - the active change files under openspec/changes/<change-id>/, when a change is selected
1123
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
+
1124
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.
1125
1464
 
1126
1465
  Command guides live in .codex/fet/commands/.
@@ -1140,6 +1479,9 @@ function codexSlashPromptFiles() {
1140
1479
  }));
1141
1480
  }
1142
1481
  function renderCommand(command) {
1482
+ if (command === "fill-context") {
1483
+ return renderFillContextCommand();
1484
+ }
1143
1485
  if (command === "passthrough") {
1144
1486
  return renderPassthroughCommand();
1145
1487
  }
@@ -1153,8 +1495,12 @@ FET:END -->
1153
1495
 
1154
1496
  # fet ${command}
1155
1497
 
1498
+ ${renderIdeModelPolicy(command)}
1499
+
1156
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.
1157
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
+
1158
1504
  Then run:
1159
1505
 
1160
1506
  \`\`\`sh
@@ -1177,8 +1523,12 @@ FET:END -->
1177
1523
 
1178
1524
  # fet passthrough
1179
1525
 
1526
+ ${renderIdeModelPolicy("passthrough")}
1527
+
1180
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.
1181
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
+
1182
1532
  Then run:
1183
1533
 
1184
1534
  \`\`\`sh
@@ -1219,6 +1569,9 @@ function renderSlashPrompt(command) {
1219
1569
  if (command === "onboard") {
1220
1570
  return renderOnboardSlashPrompt();
1221
1571
  }
1572
+ if (command === "fill-context") {
1573
+ return renderFillContextSlashPrompt();
1574
+ }
1222
1575
  if (command === "passthrough") {
1223
1576
  return renderPassthroughSlashPrompt();
1224
1577
  }
@@ -1242,6 +1595,8 @@ Use FET as the entry point for this OpenSpec workflow.
1242
1595
 
1243
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.
1244
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
+
1245
1600
  Run:
1246
1601
 
1247
1602
  \`\`\`sh
@@ -1251,6 +1606,66 @@ ${shellCommand}
1251
1606
  After it completes, summarize the important FET output and next steps.
1252
1607
  `;
1253
1608
  }
1609
+ function renderFillContextCommand() {
1610
+ return `<!-- FET:MANAGED
1611
+ schemaVersion: 1
1612
+ fetVersion: ${FET_VERSION}
1613
+ generator: codex-adapter
1614
+ adapterVersion: 1
1615
+ command: fet fill-context
1616
+ FET:END -->
1617
+
1618
+ # fet fill-context
1619
+
1620
+ ${renderIdeModelPolicy("fill-context")}
1621
+
1622
+ Use this command to complete FET-generated project context placeholders with Codex.
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
+
1626
+ First run:
1627
+
1628
+ \`\`\`sh
1629
+ fet fill-context
1630
+ \`\`\`
1631
+
1632
+ Then read AGENTS.md and openspec/config.yaml, inspect the project, and replace every [NEEDS LLM INPUT] or [NEED LLM INPUT] placeholder in AGENTS.md with concrete project-specific content. Preserve FET managed markers and do not modify business code.
1633
+ `;
1634
+ }
1635
+ function renderFillContextSlashPrompt() {
1636
+ return renderManagedSlashPrompt(
1637
+ "fet fill-context",
1638
+ "Fill FET AGENTS.md placeholders with Codex",
1639
+ `Complete FET-generated project context placeholders.
1640
+
1641
+ Steps:
1642
+
1643
+ 1. Refresh FET IDE handoff files:
1644
+ \`\`\`sh
1645
+ fet fill-context
1646
+ \`\`\`
1647
+ 2. Read AGENTS.md and openspec/config.yaml.
1648
+ 3. Inspect the project to understand:
1649
+ - source structure and major modules
1650
+ - framework and routing conventions
1651
+ - scripts, test commands, and build commands
1652
+ - coding conventions and project-specific patterns
1653
+ - important docs such as README files
1654
+ 4. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete, concise project-specific content.
1655
+ 5. Preserve FET managed markers such as \`FET:BEGIN AUTO\`, \`FET:END AUTO\`, \`FET:BEGIN USER\`, and \`FET:END USER\`.
1656
+ 6. Do not modify business code.
1657
+ 7. Run:
1658
+ \`\`\`sh
1659
+ fet doctor
1660
+ \`\`\`
1661
+ Confirm that no AGENTS.md placeholder warning remains.
1662
+
1663
+ Guardrails:
1664
+ - Do not invent facts that cannot be inferred from the repo.
1665
+ - Use [UNKNOWN] only when the repository does not contain enough evidence.
1666
+ - Keep generated context stable and useful for future AI coding sessions.`
1667
+ );
1668
+ }
1254
1669
  function renderNewSlashPrompt() {
1255
1670
  return renderManagedSlashPrompt(
1256
1671
  "fet new [...args]",
@@ -1294,11 +1709,11 @@ Input after the slash command should identify the change, for example a change i
1294
1709
  Steps:
1295
1710
 
1296
1711
  1. Resolve the change id. If ambiguous, ask the user.
1297
- 2. Get FET-managed apply instructions:
1712
+ 2. Run the native OpenSpec apply flow through FET:
1298
1713
  \`\`\`sh
1299
1714
  fet apply --change <change-id> --json
1300
1715
  \`\`\`
1301
- 3. Read all context files named by the instructions output. If JSON output is unavailable, read the files referenced by the terminal output and the artifacts under openspec/changes/<change-id>/.
1716
+ 3. Follow the native apply output. If JSON output is unavailable, read the files referenced by the terminal output and the artifacts under openspec/changes/<change-id>/.
1302
1717
  4. If apply is blocked because required artifacts are missing, stop and suggest /prompts:fet-continue <change-id> or /prompts:fet-ff <change-id>.
1303
1718
  5. Implement pending tasks one by one:
1304
1719
  - Keep code changes minimal and scoped to the task.
@@ -1502,11 +1917,11 @@ Steps:
1502
1917
  \`\`\`sh
1503
1918
  fet new <change-id>
1504
1919
  \`\`\`
1505
- 5. Get proposal instructions through FET:
1920
+ 5. Run the native OpenSpec exploration flow through FET so clarification prompts stay interactive:
1506
1921
  \`\`\`sh
1507
1922
  fet explore --change <change-id>
1508
1923
  \`\`\`
1509
- 6. If the user asks to generate or capture the proposal, create openspec/changes/<change-id>/proposal.md using the instruction/template/output path from FET/OpenSpec. Fill the proposal from the conversation and project context.
1924
+ 6. If OpenSpec or the user asks to generate or capture the proposal, create openspec/changes/<change-id>/proposal.md using the interactive answers, conversation, and project context.
1510
1925
 
1511
1926
  Guardrails:
1512
1927
  - Do not write application code in explore mode.
@@ -1532,11 +1947,11 @@ Steps:
1532
1947
  fet passthrough status --change <change-id> --json
1533
1948
  \`\`\`
1534
1949
  4. Pick the first artifact whose status is ready, unless the user specified an artifact id.
1535
- 5. Get FET-managed instructions:
1950
+ 5. Run the native OpenSpec continue flow through FET:
1536
1951
  \`\`\`sh
1537
1952
  fet continue <artifact-id> --change <change-id> --json
1538
1953
  \`\`\`
1539
- 6. Parse the instructions output. Use its template, instruction, dependencies, and outputPath.
1954
+ 6. Follow the native continue output. When it provides template, instruction, dependencies, and outputPath, use those fields.
1540
1955
  7. Read dependency files before writing.
1541
1956
  8. Create the artifact file at outputPath. Do not copy context/rules wrapper text into the artifact; use those fields only as constraints.
1542
1957
  9. Verify the file exists, then run:
@@ -1557,6 +1972,7 @@ Guardrails:
1557
1972
  }
1558
1973
  function renderFastForwardSlashPrompt(command) {
1559
1974
  const title = command === "propose" ? "Propose a new FET/OpenSpec change" : "Fast-forward FET/OpenSpec artifact creation";
1975
+ const commandLine = command === "propose" ? "fet propose <change-id-or-description>" : "fet ff --change <change-id>";
1560
1976
  return renderManagedSlashPrompt(
1561
1977
  `fet ${command} [...args]`,
1562
1978
  command === "propose" ? "Create a change and generate required OpenSpec artifacts" : "Generate required OpenSpec artifacts for a change",
@@ -1567,22 +1983,13 @@ Input after the slash command may be a change id or a description of what the us
1567
1983
  Steps:
1568
1984
 
1569
1985
  1. Load project context from AGENTS.md and openspec/config.yaml.
1570
- 2. Resolve or create the change:
1571
- - If this is a new change, derive a kebab-case id and run \`fet new <change-id>\`.
1572
- - If the change already exists, continue it instead of recreating it.
1573
- 3. Check artifact status:
1986
+ 2. Resolve the change id or description. If ambiguous, ask the user.
1987
+ 3. Run the native OpenSpec ${command} flow through FET:
1574
1988
  \`\`\`sh
1575
- fet passthrough status --change <change-id> --json
1989
+ ${commandLine}
1576
1990
  \`\`\`
1577
- 4. Loop until the change is apply-ready:
1578
- - Pick the first artifact whose status is ready.
1579
- - Run \`fet continue <artifact-id> --change <change-id> --json\`.
1580
- - Parse template, instruction, dependencies, and outputPath.
1581
- - Read dependency files.
1582
- - Write the artifact file at outputPath.
1583
- - Re-run \`fet passthrough status --change <change-id> --json\`.
1584
- - Stop when all apply-required artifacts are done, or when no artifact is ready.
1585
- 5. If context is unclear, ask one concise question, then continue.
1991
+ 4. Follow the native output. If it asks for clarification, ask the user rather than inventing details.
1992
+ 5. If the native output includes artifact paths or templates to write, create only those files and preserve OpenSpec structure.
1586
1993
 
1587
1994
  Artifact rules:
1588
1995
  - Follow the instruction field from OpenSpec/FET for each artifact.
@@ -1598,6 +2005,7 @@ Output:
1598
2005
  );
1599
2006
  }
1600
2007
  function renderManagedSlashPrompt(command, description, body) {
2008
+ const policyCommand = command.split(/\s+/)[1] ?? command;
1601
2009
  return `<!-- FET:MANAGED
1602
2010
  schemaVersion: 1
1603
2011
  fetVersion: ${FET_VERSION}
@@ -1611,6 +2019,10 @@ description: ${description}
1611
2019
  argument-hint: command arguments
1612
2020
  ---
1613
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
+
1614
2026
  ${body}
1615
2027
  `;
1616
2028
  }
@@ -1621,7 +2033,7 @@ var CodexAdapter = class {
1621
2033
  adapterVersion = 1;
1622
2034
  async detect(projectRoot) {
1623
2035
  return {
1624
- detected: await exists3(join10(projectRoot, ".codex")) || await exists3(join10(projectRoot, "AGENTS.md")),
2036
+ detected: await exists3(join13(projectRoot, ".codex")) || await exists3(join13(projectRoot, "AGENTS.md")),
1625
2037
  reason: "Codex adapter is available for projects that use AGENTS.md"
1626
2038
  };
1627
2039
  }
@@ -1660,7 +2072,7 @@ var CodexAdapter = class {
1660
2072
  if (existing && !existing.includes("FET:MANAGED") && force) {
1661
2073
  await createBackup(target);
1662
2074
  }
1663
- await mkdir5(dirname5(target), { recursive: true });
2075
+ await mkdir6(dirname6(target), { recursive: true });
1664
2076
  await atomicWrite(target, file.content);
1665
2077
  written.push(displayPath);
1666
2078
  }
@@ -1687,9 +2099,9 @@ var CodexAdapter = class {
1687
2099
  };
1688
2100
  function resolveTarget(projectRoot, file) {
1689
2101
  if (file.root === "codex-home") {
1690
- return join10(resolveCodexHome(), file.path);
2102
+ return join13(resolveCodexHome(), file.path);
1691
2103
  }
1692
- return join10(projectRoot, file.path);
2104
+ return join13(projectRoot, file.path);
1693
2105
  }
1694
2106
  function displayPathFor(file) {
1695
2107
  if (file.root === "codex-home") {
@@ -1698,11 +2110,11 @@ function displayPathFor(file) {
1698
2110
  return file.path;
1699
2111
  }
1700
2112
  function resolveCodexHome() {
1701
- return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join10(homedir(), ".codex");
2113
+ return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join13(homedir(), ".codex");
1702
2114
  }
1703
2115
  async function readExisting(path) {
1704
2116
  try {
1705
- return await readFile9(path, "utf8");
2117
+ return await readFile12(path, "utf8");
1706
2118
  } catch {
1707
2119
  return null;
1708
2120
  }
@@ -1717,8 +2129,8 @@ async function exists3(path) {
1717
2129
  }
1718
2130
 
1719
2131
  // src/adapters/cursor/index.ts
1720
- import { mkdir as mkdir6, readFile as readFile10, stat as stat6 } from "fs/promises";
1721
- import { dirname as dirname6, join as join11 } from "path";
2132
+ import { mkdir as mkdir7, readFile as readFile13, stat as stat6 } from "fs/promises";
2133
+ import { dirname as dirname7, join as join14 } from "path";
1722
2134
 
1723
2135
  // src/adapters/cursor/templates.ts
1724
2136
  function cursorSkillFiles() {
@@ -1746,6 +2158,7 @@ alwaysApply: false
1746
2158
 
1747
2159
  - AGENTS.md
1748
2160
  - openspec/config.yaml
2161
+ - GitNexus code graph context, when available. Prefer it before broad repository scans; if it is unavailable, continue normally.
1749
2162
  - \u5F53\u524D change \u76EE\u5F55\u4E0B\u7684 OpenSpec \u89C4\u5212\u4EA7\u7269
1750
2163
 
1751
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
@@ -1754,6 +2167,35 @@ alwaysApply: false
1754
2167
  }
1755
2168
  function renderSkill(command) {
1756
2169
  const usage = command === "passthrough" ? "fet passthrough <openspec-command> [...args]" : `fet ${command}`;
2170
+ if (command === "fill-context") {
2171
+ return `<!-- FET:MANAGED
2172
+ schemaVersion: 1
2173
+ fetVersion: ${FET_VERSION}
2174
+ generator: cursor-adapter
2175
+ adapterVersion: 1
2176
+ command: fet fill-context
2177
+ FET:END -->
2178
+
2179
+ ---
2180
+ name: fet-fill-context
2181
+ description: Fill FET AGENTS.md placeholders with Cursor AI
2182
+ disable-model-invocation: false
2183
+ ---
2184
+
2185
+ Run \`fet fill-context\` first if the IDE commands need refreshing.
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
+
2191
+ Then read:
2192
+
2193
+ - AGENTS.md
2194
+ - openspec/config.yaml
2195
+
2196
+ Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete project-specific content. Inspect README files, package scripts, routes, tests, source layout, and existing conventions before writing. Preserve FET managed markers and do not modify business code.
2197
+ `;
2198
+ }
1757
2199
  return `<!-- FET:MANAGED
1758
2200
  schemaVersion: 1
1759
2201
  fetVersion: ${FET_VERSION}
@@ -1768,6 +2210,10 @@ description: Run FET-managed OpenSpec ${command} workflow from the terminal
1768
2210
  disable-model-invocation: true
1769
2211
  ---
1770
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
+
1771
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
1772
2218
 
1773
2219
  \u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
@@ -1786,7 +2232,7 @@ var CursorAdapter = class {
1786
2232
  adapterVersion = 1;
1787
2233
  async detect(projectRoot) {
1788
2234
  return {
1789
- detected: await exists4(join11(projectRoot, ".cursor")),
2235
+ detected: await exists4(join14(projectRoot, ".cursor")),
1790
2236
  reason: "Cursor adapter is available for any project"
1791
2237
  };
1792
2238
  }
@@ -1803,7 +2249,7 @@ var CursorAdapter = class {
1803
2249
  const written = [];
1804
2250
  const skipped = [];
1805
2251
  for (const file of plan.files) {
1806
- const target = join11(projectRoot, file.path);
2252
+ const target = join14(projectRoot, file.path);
1807
2253
  const existing = await readExisting2(target);
1808
2254
  if (existing && !existing.includes("FET:MANAGED") && !force) {
1809
2255
  throw new FetError({
@@ -1816,7 +2262,7 @@ var CursorAdapter = class {
1816
2262
  if (existing && !existing.includes("FET:MANAGED") && force) {
1817
2263
  await createBackup(target);
1818
2264
  }
1819
- await mkdir6(dirname6(target), { recursive: true });
2265
+ await mkdir7(dirname7(target), { recursive: true });
1820
2266
  await atomicWrite(target, file.content);
1821
2267
  written.push(file.path);
1822
2268
  }
@@ -1826,7 +2272,7 @@ var CursorAdapter = class {
1826
2272
  const plan = await this.planInstall(projectRoot);
1827
2273
  const checks = [];
1828
2274
  for (const file of plan.files) {
1829
- const target = join11(projectRoot, file.path);
2275
+ const target = join14(projectRoot, file.path);
1830
2276
  const content = await readExisting2(target);
1831
2277
  const managed = Boolean(content?.includes("FET:MANAGED"));
1832
2278
  const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
@@ -1842,7 +2288,7 @@ var CursorAdapter = class {
1842
2288
  };
1843
2289
  async function readExisting2(path) {
1844
2290
  try {
1845
- return await readFile10(path, "utf8");
2291
+ return await readFile13(path, "utf8");
1846
2292
  } catch {
1847
2293
  return null;
1848
2294
  }
@@ -1857,40 +2303,42 @@ async function exists4(path) {
1857
2303
  }
1858
2304
 
1859
2305
  // src/openspec/adapter.ts
1860
- import { execFile as execFile3 } from "child_process";
1861
- import { promisify as promisify3 } from "util";
2306
+ import { execFile as execFile4 } from "child_process";
2307
+ import { promisify as promisify4 } from "util";
1862
2308
 
1863
2309
  // src/openspec/inspector.ts
1864
2310
  import { readdir, stat as stat7 } from "fs/promises";
1865
- import { join as join12 } from "path";
2311
+ import { join as join15 } from "path";
1866
2312
  async function inspectOpenSpecProject(projectRoot) {
1867
- const openspecPath = join12(projectRoot, "openspec");
1868
- const changesPath = join12(openspecPath, "changes");
1869
- const archivePath = join12(openspecPath, "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");
1870
2317
  return {
1871
2318
  exists: await exists5(openspecPath),
1872
- changes: await listDirectories(changesPath),
1873
- archived: await listDirectories(archivePath)
2319
+ changes: await listDirectories(changesPath, { exclude: ["archive"] }),
2320
+ archived: [.../* @__PURE__ */ new Set([...await listDirectories(legacyArchivePath), ...await listDirectories(changesArchivePath)])]
1874
2321
  };
1875
2322
  }
1876
2323
  async function inspectOpenSpecChange(projectRoot, changeId) {
1877
- const changePath = join12(projectRoot, "openspec", "changes", changeId);
1878
- const tasksPath = join12(changePath, "tasks.md");
1879
- const specsPath = join12(changePath, "specs");
2324
+ const changePath = join15(projectRoot, "openspec", "changes", changeId);
2325
+ const tasksPath = join15(changePath, "tasks.md");
2326
+ const specsPath = join15(changePath, "specs");
1880
2327
  return {
1881
2328
  changeId,
1882
2329
  exists: await exists5(changePath),
1883
- hasProposal: await exists5(join12(changePath, "proposal.md")),
2330
+ hasProposal: await exists5(join15(changePath, "proposal.md")),
1884
2331
  hasTasks: await exists5(tasksPath),
1885
2332
  hasSpecs: await exists5(specsPath),
1886
2333
  tasksPath,
1887
2334
  changePath
1888
2335
  };
1889
2336
  }
1890
- async function listDirectories(path) {
2337
+ async function listDirectories(path, options = {}) {
1891
2338
  try {
1892
2339
  const entries = await readdir(path, { withFileTypes: true });
1893
- return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2340
+ const excluded = new Set(options.exclude ?? []);
2341
+ return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
1894
2342
  } catch {
1895
2343
  return [];
1896
2344
  }
@@ -1905,9 +2353,9 @@ async function exists5(path) {
1905
2353
  }
1906
2354
 
1907
2355
  // src/openspec/resolver.ts
1908
- import { execFile as execFile2 } from "child_process";
1909
- import { promisify as promisify2 } from "util";
1910
- var execFileAsync2 = promisify2(execFile2);
2356
+ import { execFile as execFile3 } from "child_process";
2357
+ import { promisify as promisify3 } from "util";
2358
+ var execFileAsync3 = promisify3(execFile3);
1911
2359
  async function resolveOpenSpecExecutable() {
1912
2360
  const executablePath = await findExecutable();
1913
2361
  const version = await readVersion(executablePath);
@@ -1954,7 +2402,7 @@ async function readVersion(executablePath) {
1954
2402
  }
1955
2403
  }
1956
2404
  function exec(command, args) {
1957
- return execFileAsync2(command, args, { shell: process.platform === "win32" });
2405
+ return execFileAsync3(command, args, { shell: process.platform === "win32" });
1958
2406
  }
1959
2407
 
1960
2408
  // src/openspec/runner.ts
@@ -2000,7 +2448,7 @@ async function runOpenSpec(executablePath, command, args, options) {
2000
2448
  }
2001
2449
 
2002
2450
  // src/openspec/adapter.ts
2003
- var execFileAsync3 = promisify3(execFile3);
2451
+ var execFileAsync4 = promisify4(execFile4);
2004
2452
  var DefaultOpenSpecAdapter = class {
2005
2453
  identity;
2006
2454
  async resolveExecutable() {
@@ -2012,7 +2460,7 @@ var DefaultOpenSpecAdapter = class {
2012
2460
  const executable = identity.executablePath === "npx openspec" ? "npx" : identity.executablePath;
2013
2461
  const args = identity.executablePath === "npx openspec" ? ["openspec", "--help"] : ["--help"];
2014
2462
  try {
2015
- const { stdout } = await execFileAsync3(executable, args, { shell: process.platform === "win32" });
2463
+ const { stdout } = await execFileAsync4(executable, args, { shell: process.platform === "win32" });
2016
2464
  return {
2017
2465
  version: identity.version,
2018
2466
  commands: parseCommands(stdout),
@@ -2056,12 +2504,12 @@ function parseCommands(help) {
2056
2504
  }
2057
2505
 
2058
2506
  // src/scanner/package.ts
2059
- import { readFile as readFile11, stat as stat8 } from "fs/promises";
2060
- import { join as join13 } from "path";
2507
+ import { readFile as readFile14, stat as stat8 } from "fs/promises";
2508
+ import { join as join16 } from "path";
2061
2509
  import { parse as parse2 } from "yaml";
2062
2510
  async function readPackageJson(projectRoot) {
2063
2511
  try {
2064
- return JSON.parse(await readFile11(join13(projectRoot, "package.json"), "utf8"));
2512
+ return JSON.parse(await readFile14(join16(projectRoot, "package.json"), "utf8"));
2065
2513
  } catch {
2066
2514
  return null;
2067
2515
  }
@@ -2127,7 +2575,7 @@ function detectFramework(pkg) {
2127
2575
  }
2128
2576
  async function detectLanguage(projectRoot, pkg) {
2129
2577
  const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
2130
- if (deps.typescript || await exists6(join13(projectRoot, "tsconfig.json"))) {
2578
+ if (deps.typescript || await exists6(join16(projectRoot, "tsconfig.json"))) {
2131
2579
  return "typescript";
2132
2580
  }
2133
2581
  return "javascript";
@@ -2142,7 +2590,7 @@ async function detectWorkspaces(projectRoot, pkg) {
2142
2590
  return packageWorkspaces;
2143
2591
  }
2144
2592
  try {
2145
- const workspace = parse2(await readFile11(join13(projectRoot, "pnpm-workspace.yaml"), "utf8"));
2593
+ const workspace = parse2(await readFile14(join16(projectRoot, "pnpm-workspace.yaml"), "utf8"));
2146
2594
  return (workspace?.packages ?? []).map((path) => ({
2147
2595
  name: path,
2148
2596
  path,
@@ -2162,7 +2610,7 @@ async function detectLockManagers(projectRoot) {
2162
2610
  ];
2163
2611
  const found = [];
2164
2612
  for (const [file, manager] of lockFiles) {
2165
- if (await exists6(join13(projectRoot, file))) {
2613
+ if (await exists6(join16(projectRoot, file))) {
2166
2614
  found.push(manager);
2167
2615
  }
2168
2616
  }
@@ -2188,12 +2636,12 @@ async function exists6(path) {
2188
2636
 
2189
2637
  // src/scanner/routes.ts
2190
2638
  import { readdir as readdir2, stat as stat9 } from "fs/promises";
2191
- import { join as join14, relative, sep } from "path";
2639
+ import { join as join17, relative, sep } from "path";
2192
2640
  async function scanRoutes(projectRoot) {
2193
2641
  const candidates = ["src/routes", "src/pages", "app", "pages"];
2194
2642
  const routes = [];
2195
2643
  for (const candidate of candidates) {
2196
- const root = join14(projectRoot, candidate);
2644
+ const root = join17(projectRoot, candidate);
2197
2645
  if (!await exists7(root)) {
2198
2646
  continue;
2199
2647
  }
@@ -2221,7 +2669,7 @@ async function listFiles(root) {
2221
2669
  const entries = await readdir2(root, { withFileTypes: true });
2222
2670
  const files = [];
2223
2671
  for (const entry of entries) {
2224
- const path = join14(root, entry.name);
2672
+ const path = join17(root, entry.name);
2225
2673
  if (entry.isDirectory()) {
2226
2674
  files.push(...await listFiles(path));
2227
2675
  } else {
@@ -2285,6 +2733,11 @@ var OutputWriter = class {
2285
2733
  }
2286
2734
  }
2287
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
+ }
2288
2741
  if (!this.json) {
2289
2742
  process.stderr.write(`\u8B66\u544A\uFF1A${message}${formatDetails(details)}
2290
2743
  `);
@@ -2373,6 +2826,7 @@ var program = new Command();
2373
2826
  program.name("fet").description("Frontend workflow orchestration tool built around OpenSpec.").enablePositionalOptions().version(FET_VERSION).option("--cwd <path>", "\u6307\u5B9A\u9879\u76EE\u6839\u76EE\u5F55").option("--change <id>", "\u6307\u5B9A OpenSpec change").option("--yes", "\u5BF9\u4F4E\u98CE\u9669\u786E\u8BA4\u4F7F\u7528\u9ED8\u8BA4\u540C\u610F").option("--json", "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB JSON").option("--verbose", "\u8F93\u51FA\u8BCA\u65AD\u7EC6\u8282").option("--no-color", "\u7981\u7528\u7EC8\u7AEF\u989C\u8272");
2374
2827
  addGlobalOptions(program.command("init")).description("\u521D\u59CB\u5316 FET + OpenSpec").action(wrap("init", initCommand));
2375
2828
  addGlobalOptions(program.command("update-context")).description("\u66F4\u65B0\u9879\u76EE\u4E0A\u4E0B\u6587").action(wrap("update-context", updateContextCommand));
2829
+ addGlobalOptions(program.command("fill-context")).description("Refresh IDE prompts for filling AGENTS.md placeholders").action(wrap("fill-context", fillContextCommand));
2376
2830
  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(
2377
2831
  wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
2378
2832
  );
@@ -2394,6 +2848,8 @@ function wrap(command, handler) {
2394
2848
  const opts = isCommandLike(maybeCommand) ? { ...maybeCommand.parent?.opts(), ...maybeCommand.opts() } : program.opts();
2395
2849
  const ctx = await createCommandContext(command, { ...opts, ...extractGlobalOptions(args) });
2396
2850
  try {
2851
+ await confirmModelPolicyRecommendation(ctx);
2852
+ await warnIfContextPlaceholdersRemain(ctx);
2397
2853
  await handler(ctx, ...args);
2398
2854
  } catch (error) {
2399
2855
  const fetError = toFetError(error);
@@ -2402,6 +2858,40 @@ function wrap(command, handler) {
2402
2858
  }
2403
2859
  };
2404
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
+ }
2405
2895
  function isCommandLike(value) {
2406
2896
  return value instanceof Command;
2407
2897
  }