@nick848/fet 1.0.2 → 1.0.3

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
@@ -473,7 +473,7 @@ async function exists(path) {
473
473
  }
474
474
 
475
475
  // src/commands/doctor.ts
476
- import { stat as stat3 } from "fs/promises";
476
+ import { readFile as readFile6, stat as stat3 } from "fs/promises";
477
477
  import { join as join7 } from "path";
478
478
  async function doctorCommand(ctx, options = {}) {
479
479
  const checks = [];
@@ -481,6 +481,7 @@ async function doctorCommand(ctx, options = {}) {
481
481
  checks.push(await checkState(ctx));
482
482
  checks.push(await checkFile("agents", join7(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
483
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")));
484
485
  for (const adapter of ctx.toolAdapters) {
485
486
  checks.push(...await adapter.doctor(ctx.projectRoot));
486
487
  }
@@ -521,6 +522,20 @@ async function checkState(ctx) {
521
522
  async function checkFile(id, path, missing, suggestedCommand) {
522
523
  return await exists2(path) ? { id, status: "pass", message: `${id} \u5B58\u5728` } : { id, status: "warn", message: missing, suggestedCommand };
523
524
  }
525
+ async function checkPlaceholders(path) {
526
+ try {
527
+ const content = await readFile6(path, "utf8");
528
+ const count2 = [...content.matchAll(/\[NEEDS? LLM INPUT\]/g)].length;
529
+ return count2 ? {
530
+ id: "context-placeholders",
531
+ status: "warn",
532
+ message: `AGENTS.md has ${count2} LLM placeholder(s)`,
533
+ suggestedCommand: "fet fill-context"
534
+ } : { id: "context-placeholders", status: "pass", message: "AGENTS.md placeholders resolved" };
535
+ } catch {
536
+ return { id: "context-placeholders", status: "warn", message: "AGENTS.md missing", suggestedCommand: "fet update-context" };
537
+ }
538
+ }
524
539
  async function exists2(path) {
525
540
  try {
526
541
  await stat3(path);
@@ -530,6 +545,82 @@ async function exists2(path) {
530
545
  }
531
546
  }
532
547
 
548
+ // 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;
552
+ async function fillContextCommand(ctx) {
553
+ await withProjectLock(
554
+ ctx.projectRoot,
555
+ { command: "fill-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
556
+ async () => {
557
+ const handoffPath = join8(ctx.projectRoot, ".fet", "fill-context.md");
558
+ await mkdir3(dirname5(handoffPath), { recursive: true });
559
+ await atomicWrite(handoffPath, renderGenericHandoff());
560
+ for (const adapter of ctx.toolAdapters) {
561
+ const plan = await adapter.planInstall(ctx.projectRoot);
562
+ const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
563
+ const state = await ctx.stateStore.getOrCreateGlobal();
564
+ state.toolAdapters[adapter.tool] = {
565
+ adapterVersion: adapter.adapterVersion,
566
+ installed: true,
567
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
568
+ };
569
+ await ctx.stateStore.writeGlobal(state);
570
+ if (ctx.verbose) {
571
+ ctx.output.info(`Updated ${adapter.tool} adapter`, { written: result.written });
572
+ }
573
+ }
574
+ }
575
+ );
576
+ const placeholders = await countPlaceholders(join8(ctx.projectRoot, "AGENTS.md"));
577
+ ctx.output.result({
578
+ ok: true,
579
+ command: "fill-context",
580
+ 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.",
581
+ nextSteps: placeholders ? [
582
+ "Cursor: run /fet-fill-context",
583
+ "Codex: run /prompts:fet-fill-context",
584
+ "OpenCode or other IDEs: open .fet/fill-context.md or run fet fill-context for handoff instructions"
585
+ ] : ["Run fet doctor to confirm project context health"],
586
+ data: {
587
+ placeholders,
588
+ cursorCommand: "/fet-fill-context",
589
+ codexCommand: "/prompts:fet-fill-context"
590
+ }
591
+ });
592
+ }
593
+ function renderGenericHandoff() {
594
+ return `<!-- FET:MANAGED
595
+ schemaVersion: 1
596
+ generator: fill-context
597
+ FET:END -->
598
+
599
+ # FET Fill Context
600
+
601
+ Use the IDE AI to complete FET-generated placeholders.
602
+
603
+ 1. Read AGENTS.md and openspec/config.yaml.
604
+ 2. Inspect README files, package scripts, routes, tests, source layout, and project conventions.
605
+ 3. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete project-specific content.
606
+ 4. Preserve FET managed markers.
607
+ 5. Do not modify business code.
608
+ 6. Run \`fet doctor\` and confirm no AGENTS.md placeholder warning remains.
609
+ `;
610
+ }
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
+
620
+ // src/commands/proxy.ts
621
+ import { readFile as readFile10 } from "fs/promises";
622
+ import { join as join10 } from "path";
623
+
533
624
  // src/state/project.ts
534
625
  import { execFile } from "child_process";
535
626
  import { promisify } from "util";
@@ -557,8 +648,8 @@ async function git(cwd, args) {
557
648
  }
558
649
 
559
650
  // src/state/store.ts
560
- import { mkdir as mkdir3, readFile as readFile6 } from "fs/promises";
561
- import { join as join8 } from "path";
651
+ import { mkdir as mkdir4, readFile as readFile8 } from "fs/promises";
652
+ import { join as join9 } from "path";
562
653
 
563
654
  // src/state/schema.ts
564
655
  var phases = ["explore", "propose", "implement", "verify", "sync", "archive"];
@@ -651,7 +742,7 @@ var StateStore = class {
651
742
  project;
652
743
  async readGlobal() {
653
744
  try {
654
- const value = JSON.parse(await readFile6(this.globalPath(), "utf8"));
745
+ const value = JSON.parse(await readFile8(this.globalPath(), "utf8"));
655
746
  assertGlobalState(value);
656
747
  return value;
657
748
  } catch (error) {
@@ -666,13 +757,13 @@ var StateStore = class {
666
757
  }
667
758
  async writeGlobal(state) {
668
759
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
669
- await mkdir3(join8(this.projectRoot, "openspec"), { recursive: true });
760
+ await mkdir4(join9(this.projectRoot, "openspec"), { recursive: true });
670
761
  await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
671
762
  `);
672
763
  }
673
764
  async readChange(changeId) {
674
765
  try {
675
- const value = JSON.parse(await readFile6(this.changePath(changeId), "utf8"));
766
+ const value = JSON.parse(await readFile8(this.changePath(changeId), "utf8"));
676
767
  assertChangeState(value);
677
768
  return value;
678
769
  } catch (error) {
@@ -687,15 +778,15 @@ var StateStore = class {
687
778
  }
688
779
  async writeChange(state) {
689
780
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
690
- await mkdir3(join8(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
781
+ await mkdir4(join9(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
691
782
  await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
692
783
  `);
693
784
  }
694
785
  globalPath() {
695
- return join8(this.projectRoot, "openspec", "fet-state.json");
786
+ return join9(this.projectRoot, "openspec", "fet-state.json");
696
787
  }
697
788
  changePath(changeId) {
698
- return join8(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
789
+ return join9(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
699
790
  }
700
791
  };
701
792
  function isNotFound(error) {
@@ -703,11 +794,11 @@ function isNotFound(error) {
703
794
  }
704
795
 
705
796
  // src/state/tasks.ts
706
- import { readFile as readFile7 } from "fs/promises";
797
+ import { readFile as readFile9 } from "fs/promises";
707
798
  async function readCompletedTaskIds(tasksPath) {
708
799
  let content;
709
800
  try {
710
- content = await readFile7(tasksPath, "utf8");
801
+ content = await readFile9(tasksPath, "utf8");
711
802
  } catch {
712
803
  return [];
713
804
  }
@@ -746,6 +837,8 @@ async function proxyCommand(ctx, command, args) {
746
837
  await assertVerified(ctx);
747
838
  }
748
839
  const mapped = await mapOpenSpecCommand(ctx, command, openSpecArgs);
840
+ const targetChangeId = command === "archive" ? mapped.args[0] ?? ctx.changeId ?? null : ctx.changeId ?? null;
841
+ const changelogEntry = command === "archive" && targetChangeId ? await createChangelogEntry(ctx.projectRoot, targetChangeId) : null;
749
842
  const result = await ctx.openSpec.run(mapped.command, mapped.args, { cwd: ctx.projectRoot, stdio: ctx.json ? "pipe" : "inherit" });
750
843
  if (result.exitCode !== 0) {
751
844
  throw new FetError({
@@ -755,15 +848,25 @@ async function proxyCommand(ctx, command, args) {
755
848
  recoverable: true
756
849
  });
757
850
  }
851
+ if (changelogEntry) {
852
+ await appendChangelog(ctx.projectRoot, changelogEntry);
853
+ }
758
854
  const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
759
855
  const state = await ctx.stateStore.getOrCreateGlobal();
760
856
  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;
857
+ if (command === "archive") {
858
+ if (!state.activeChangeId || state.activeChangeId === targetChangeId || !inspection.changes.includes(state.activeChangeId)) {
859
+ state.activeChangeId = null;
860
+ }
861
+ state.verifyAuthorization = null;
862
+ } else {
863
+ if (ctx.changeId && inspection.changes.includes(ctx.changeId)) {
864
+ state.activeChangeId = ctx.changeId;
865
+ } else if (state.activeChangeId && !inspection.changes.includes(state.activeChangeId)) {
866
+ state.activeChangeId = inspection.changes.length === 1 ? inspection.changes[0] ?? null : null;
867
+ } else if (!state.activeChangeId && inspection.changes.length === 1) {
868
+ state.activeChangeId = inspection.changes[0] ?? null;
869
+ }
767
870
  }
768
871
  await ctx.stateStore.writeGlobal(state);
769
872
  const changeId = ctx.changeId ?? state.activeChangeId;
@@ -793,6 +896,58 @@ async function proxyCommand(ctx, command, args) {
793
896
  summary: `fet ${command} \u5B8C\u6210\u3002`
794
897
  });
795
898
  }
899
+ async function createChangelogEntry(projectRoot, changeId) {
900
+ return {
901
+ updateTime: formatLocalTimestamp(/* @__PURE__ */ new Date()),
902
+ content: await readChangeRequirement(projectRoot, changeId)
903
+ };
904
+ }
905
+ async function appendChangelog(projectRoot, entry) {
906
+ const changelogPath = join10(projectRoot, "CHANGELOG.md");
907
+ const existing = await readOptional3(changelogPath);
908
+ const block = `updateTime: ${entry.updateTime}
909
+ \u66F4\u65B0\u5185\u5BB9:${entry.content}
910
+ `;
911
+ const next = existing?.trimEnd() ? `${existing.trimEnd()}
912
+
913
+ ${block}` : block;
914
+ await atomicWrite(changelogPath, next);
915
+ }
916
+ async function readChangeRequirement(projectRoot, changeId) {
917
+ const changeRoot = join10(projectRoot, "openspec", "changes", changeId);
918
+ const proposal = await readOptional3(join10(changeRoot, "proposal.md"));
919
+ if (proposal) {
920
+ return summarizeMarkdown(proposal);
921
+ }
922
+ const readme = await readOptional3(join10(changeRoot, "README.md"));
923
+ if (readme) {
924
+ return summarizeMarkdown(readme);
925
+ }
926
+ return changeId;
927
+ }
928
+ function summarizeMarkdown(content) {
929
+ const normalized = content.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("<!--") && !line.startsWith("---")).join(" ");
930
+ return normalized || "\u672A\u63D0\u4F9B\u53D8\u66F4\u9700\u6C42";
931
+ }
932
+ async function readOptional3(path) {
933
+ try {
934
+ return await readFile10(path, "utf8");
935
+ } catch {
936
+ return null;
937
+ }
938
+ }
939
+ function formatLocalTimestamp(date) {
940
+ const year = date.getFullYear();
941
+ const month = pad(date.getMonth() + 1);
942
+ const day = pad(date.getDate());
943
+ const hours = pad(date.getHours());
944
+ const minutes = pad(date.getMinutes());
945
+ const seconds = pad(date.getSeconds());
946
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
947
+ }
948
+ function pad(value) {
949
+ return String(value).padStart(2, "0");
950
+ }
796
951
  async function passthroughCommand(ctx, command, args) {
797
952
  const result = await ctx.openSpec.run(command, stripFetOptions(args), { cwd: ctx.projectRoot, stdio: ctx.json ? "pipe" : "inherit" });
798
953
  if (result.exitCode !== 0) {
@@ -826,32 +981,37 @@ function stripFetOptions(args) {
826
981
  async function mapOpenSpecCommand(ctx, command, args) {
827
982
  switch (command) {
828
983
  case "propose":
829
- case "new":
830
- return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
831
984
  case "continue":
832
- return { command: "instructions", args: [...withoutUndefined(args[0] ? [args[0]] : ["proposal"]), "--change", await requireChangeId(ctx)] };
833
985
  case "ff":
834
- return { command: "status", args: ["--change", await requireChangeId(ctx)] };
835
986
  case "apply":
836
- return { command: "instructions", args: ["apply", "--change", await requireChangeId(ctx)] };
837
987
  case "sync":
838
- return { command: "validate", args: [ctx.changeId ?? args[0] ?? await requireChangeId(ctx), "--type", "change", "--strict"] };
988
+ case "bulk-archive":
989
+ case "explore":
990
+ case "onboard":
991
+ return { command, args: withGlobalChange(ctx, args) };
992
+ case "new":
993
+ return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
839
994
  case "archive":
840
995
  return { command: "archive", args: [ctx.changeId ?? args[0] ?? await requireChangeId(ctx), ...args.slice(ctx.changeId ? 0 : 1)] };
996
+ /*
841
997
  case "bulk-archive":
842
998
  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>"
999
+ code: ErrorCode.InvalidArguments,
1000
+ message: "OpenSpec 1.2.0 不提供 bulk-archive 顶层命令",
1001
+ suggestedCommand: "逐个执行 fet archive --change <change-id>"
846
1002
  });
847
1003
  case "explore":
848
- return { command: "instructions", args: ["proposal", "--change", await requireChangeId(ctx)] };
1004
+ return { command: "explore", args: ctx.changeId ? ["--change", ctx.changeId, ...args] : args };
849
1005
  case "onboard":
850
1006
  return { command: "instructions", args: [] };
1007
+ */
851
1008
  default:
852
1009
  return { command, args };
853
1010
  }
854
1011
  }
1012
+ function withGlobalChange(ctx, args) {
1013
+ return ctx.changeId ? ["--change", ctx.changeId, ...args] : args;
1014
+ }
855
1015
  async function requireChangeId(ctx) {
856
1016
  if (ctx.changeId) {
857
1017
  return ctx.changeId;
@@ -871,9 +1031,6 @@ async function requireChangeId(ctx) {
871
1031
  suggestedCommand: "\u6DFB\u52A0 --change <change-id>"
872
1032
  });
873
1033
  }
874
- function withoutUndefined(values) {
875
- return values.filter(Boolean);
876
- }
877
1034
  async function assertVerified(ctx) {
878
1035
  const global = await ctx.stateStore.getOrCreateGlobal();
879
1036
  const changeId = ctx.changeId ?? global.activeChangeId;
@@ -906,8 +1063,8 @@ async function assertVerified(ctx) {
906
1063
 
907
1064
  // src/commands/verify.ts
908
1065
  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";
1066
+ import { mkdir as mkdir5, readFile as readFile11, stat as stat4 } from "fs/promises";
1067
+ import { join as join11 } from "path";
911
1068
  async function verifyCommand(ctx, options) {
912
1069
  if (options.auto) {
913
1070
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
@@ -974,9 +1131,9 @@ async function verifyCommand(ctx, options) {
974
1131
  async function writeInstructions(ctx, changeId) {
975
1132
  await assertChangeExists(ctx, changeId);
976
1133
  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 });
1134
+ const dir = join11(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
1135
+ const instructionsPath = join11(dir, "verify-instructions.md");
1136
+ await mkdir5(dir, { recursive: true });
980
1137
  await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
981
1138
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
982
1139
  state.currentPhase = "verify";
@@ -992,7 +1149,7 @@ async function writeInstructions(ctx, changeId) {
992
1149
  async function markDone(ctx, changeId) {
993
1150
  await assertChangeExists(ctx, changeId);
994
1151
  const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
995
- const instructionsPath = join9(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
1152
+ const instructionsPath = join11(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
996
1153
  const instructions = await readInstructions(instructionsPath, changeId);
997
1154
  const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
998
1155
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
@@ -1028,7 +1185,7 @@ async function assertChangeExists(ctx, changeId) {
1028
1185
  async function readInstructions(path, changeId) {
1029
1186
  try {
1030
1187
  await stat4(path);
1031
- const content = await readFile8(path, "utf8");
1188
+ const content = await readFile11(path, "utf8");
1032
1189
  const fileChangeId = readFrontMatterValue(content, "changeId");
1033
1190
  if (fileChangeId !== changeId) {
1034
1191
  throw new FetError({
@@ -1082,9 +1239,9 @@ async function resolveChangeId(ctx) {
1082
1239
  import { resolve } from "path";
1083
1240
 
1084
1241
  // src/adapters/codex/index.ts
1085
- import { mkdir as mkdir5, readFile as readFile9, stat as stat5 } from "fs/promises";
1242
+ import { mkdir as mkdir6, readFile as readFile12, stat as stat5 } from "fs/promises";
1086
1243
  import { homedir } from "os";
1087
- import { dirname as dirname5, join as join10 } from "path";
1244
+ import { dirname as dirname6, join as join12 } from "path";
1088
1245
 
1089
1246
  // src/adapters/commands.ts
1090
1247
  var FET_WORKFLOW_COMMANDS = [
@@ -1100,7 +1257,7 @@ var FET_WORKFLOW_COMMANDS = [
1100
1257
  "bulk-archive",
1101
1258
  "onboard"
1102
1259
  ];
1103
- var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "passthrough"];
1260
+ var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "fill-context", "passthrough"];
1104
1261
 
1105
1262
  // src/adapters/codex/templates.ts
1106
1263
  function codexGuideFile() {
@@ -1140,6 +1297,9 @@ function codexSlashPromptFiles() {
1140
1297
  }));
1141
1298
  }
1142
1299
  function renderCommand(command) {
1300
+ if (command === "fill-context") {
1301
+ return renderFillContextCommand();
1302
+ }
1143
1303
  if (command === "passthrough") {
1144
1304
  return renderPassthroughCommand();
1145
1305
  }
@@ -1219,6 +1379,9 @@ function renderSlashPrompt(command) {
1219
1379
  if (command === "onboard") {
1220
1380
  return renderOnboardSlashPrompt();
1221
1381
  }
1382
+ if (command === "fill-context") {
1383
+ return renderFillContextSlashPrompt();
1384
+ }
1222
1385
  if (command === "passthrough") {
1223
1386
  return renderPassthroughSlashPrompt();
1224
1387
  }
@@ -1251,6 +1414,62 @@ ${shellCommand}
1251
1414
  After it completes, summarize the important FET output and next steps.
1252
1415
  `;
1253
1416
  }
1417
+ function renderFillContextCommand() {
1418
+ return `<!-- FET:MANAGED
1419
+ schemaVersion: 1
1420
+ fetVersion: ${FET_VERSION}
1421
+ generator: codex-adapter
1422
+ adapterVersion: 1
1423
+ command: fet fill-context
1424
+ FET:END -->
1425
+
1426
+ # fet fill-context
1427
+
1428
+ Use this command to complete FET-generated project context placeholders with Codex.
1429
+
1430
+ First run:
1431
+
1432
+ \`\`\`sh
1433
+ fet fill-context
1434
+ \`\`\`
1435
+
1436
+ 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.
1437
+ `;
1438
+ }
1439
+ function renderFillContextSlashPrompt() {
1440
+ return renderManagedSlashPrompt(
1441
+ "fet fill-context",
1442
+ "Fill FET AGENTS.md placeholders with Codex",
1443
+ `Complete FET-generated project context placeholders.
1444
+
1445
+ Steps:
1446
+
1447
+ 1. Refresh FET IDE handoff files:
1448
+ \`\`\`sh
1449
+ fet fill-context
1450
+ \`\`\`
1451
+ 2. Read AGENTS.md and openspec/config.yaml.
1452
+ 3. Inspect the project to understand:
1453
+ - source structure and major modules
1454
+ - framework and routing conventions
1455
+ - scripts, test commands, and build commands
1456
+ - coding conventions and project-specific patterns
1457
+ - important docs such as README files
1458
+ 4. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete, concise project-specific content.
1459
+ 5. Preserve FET managed markers such as \`FET:BEGIN AUTO\`, \`FET:END AUTO\`, \`FET:BEGIN USER\`, and \`FET:END USER\`.
1460
+ 6. Do not modify business code.
1461
+ 7. Run:
1462
+ \`\`\`sh
1463
+ fet doctor
1464
+ \`\`\`
1465
+ Confirm that no AGENTS.md placeholder warning remains.
1466
+
1467
+ Guardrails:
1468
+ - Do not invent facts that cannot be inferred from the repo.
1469
+ - Use [UNKNOWN] only when the repository does not contain enough evidence.
1470
+ - Keep generated context stable and useful for future AI coding sessions.`
1471
+ );
1472
+ }
1254
1473
  function renderNewSlashPrompt() {
1255
1474
  return renderManagedSlashPrompt(
1256
1475
  "fet new [...args]",
@@ -1294,11 +1513,11 @@ Input after the slash command should identify the change, for example a change i
1294
1513
  Steps:
1295
1514
 
1296
1515
  1. Resolve the change id. If ambiguous, ask the user.
1297
- 2. Get FET-managed apply instructions:
1516
+ 2. Run the native OpenSpec apply flow through FET:
1298
1517
  \`\`\`sh
1299
1518
  fet apply --change <change-id> --json
1300
1519
  \`\`\`
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>/.
1520
+ 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
1521
  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
1522
  5. Implement pending tasks one by one:
1304
1523
  - Keep code changes minimal and scoped to the task.
@@ -1502,11 +1721,11 @@ Steps:
1502
1721
  \`\`\`sh
1503
1722
  fet new <change-id>
1504
1723
  \`\`\`
1505
- 5. Get proposal instructions through FET:
1724
+ 5. Run the native OpenSpec exploration flow through FET so clarification prompts stay interactive:
1506
1725
  \`\`\`sh
1507
1726
  fet explore --change <change-id>
1508
1727
  \`\`\`
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.
1728
+ 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
1729
 
1511
1730
  Guardrails:
1512
1731
  - Do not write application code in explore mode.
@@ -1532,11 +1751,11 @@ Steps:
1532
1751
  fet passthrough status --change <change-id> --json
1533
1752
  \`\`\`
1534
1753
  4. Pick the first artifact whose status is ready, unless the user specified an artifact id.
1535
- 5. Get FET-managed instructions:
1754
+ 5. Run the native OpenSpec continue flow through FET:
1536
1755
  \`\`\`sh
1537
1756
  fet continue <artifact-id> --change <change-id> --json
1538
1757
  \`\`\`
1539
- 6. Parse the instructions output. Use its template, instruction, dependencies, and outputPath.
1758
+ 6. Follow the native continue output. When it provides template, instruction, dependencies, and outputPath, use those fields.
1540
1759
  7. Read dependency files before writing.
1541
1760
  8. Create the artifact file at outputPath. Do not copy context/rules wrapper text into the artifact; use those fields only as constraints.
1542
1761
  9. Verify the file exists, then run:
@@ -1557,6 +1776,7 @@ Guardrails:
1557
1776
  }
1558
1777
  function renderFastForwardSlashPrompt(command) {
1559
1778
  const title = command === "propose" ? "Propose a new FET/OpenSpec change" : "Fast-forward FET/OpenSpec artifact creation";
1779
+ const commandLine = command === "propose" ? "fet propose <change-id-or-description>" : "fet ff --change <change-id>";
1560
1780
  return renderManagedSlashPrompt(
1561
1781
  `fet ${command} [...args]`,
1562
1782
  command === "propose" ? "Create a change and generate required OpenSpec artifacts" : "Generate required OpenSpec artifacts for a change",
@@ -1567,22 +1787,13 @@ Input after the slash command may be a change id or a description of what the us
1567
1787
  Steps:
1568
1788
 
1569
1789
  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:
1790
+ 2. Resolve the change id or description. If ambiguous, ask the user.
1791
+ 3. Run the native OpenSpec ${command} flow through FET:
1574
1792
  \`\`\`sh
1575
- fet passthrough status --change <change-id> --json
1793
+ ${commandLine}
1576
1794
  \`\`\`
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.
1795
+ 4. Follow the native output. If it asks for clarification, ask the user rather than inventing details.
1796
+ 5. If the native output includes artifact paths or templates to write, create only those files and preserve OpenSpec structure.
1586
1797
 
1587
1798
  Artifact rules:
1588
1799
  - Follow the instruction field from OpenSpec/FET for each artifact.
@@ -1621,7 +1832,7 @@ var CodexAdapter = class {
1621
1832
  adapterVersion = 1;
1622
1833
  async detect(projectRoot) {
1623
1834
  return {
1624
- detected: await exists3(join10(projectRoot, ".codex")) || await exists3(join10(projectRoot, "AGENTS.md")),
1835
+ detected: await exists3(join12(projectRoot, ".codex")) || await exists3(join12(projectRoot, "AGENTS.md")),
1625
1836
  reason: "Codex adapter is available for projects that use AGENTS.md"
1626
1837
  };
1627
1838
  }
@@ -1660,7 +1871,7 @@ var CodexAdapter = class {
1660
1871
  if (existing && !existing.includes("FET:MANAGED") && force) {
1661
1872
  await createBackup(target);
1662
1873
  }
1663
- await mkdir5(dirname5(target), { recursive: true });
1874
+ await mkdir6(dirname6(target), { recursive: true });
1664
1875
  await atomicWrite(target, file.content);
1665
1876
  written.push(displayPath);
1666
1877
  }
@@ -1687,9 +1898,9 @@ var CodexAdapter = class {
1687
1898
  };
1688
1899
  function resolveTarget(projectRoot, file) {
1689
1900
  if (file.root === "codex-home") {
1690
- return join10(resolveCodexHome(), file.path);
1901
+ return join12(resolveCodexHome(), file.path);
1691
1902
  }
1692
- return join10(projectRoot, file.path);
1903
+ return join12(projectRoot, file.path);
1693
1904
  }
1694
1905
  function displayPathFor(file) {
1695
1906
  if (file.root === "codex-home") {
@@ -1698,11 +1909,11 @@ function displayPathFor(file) {
1698
1909
  return file.path;
1699
1910
  }
1700
1911
  function resolveCodexHome() {
1701
- return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join10(homedir(), ".codex");
1912
+ return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join12(homedir(), ".codex");
1702
1913
  }
1703
1914
  async function readExisting(path) {
1704
1915
  try {
1705
- return await readFile9(path, "utf8");
1916
+ return await readFile12(path, "utf8");
1706
1917
  } catch {
1707
1918
  return null;
1708
1919
  }
@@ -1717,8 +1928,8 @@ async function exists3(path) {
1717
1928
  }
1718
1929
 
1719
1930
  // 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";
1931
+ import { mkdir as mkdir7, readFile as readFile13, stat as stat6 } from "fs/promises";
1932
+ import { dirname as dirname7, join as join13 } from "path";
1722
1933
 
1723
1934
  // src/adapters/cursor/templates.ts
1724
1935
  function cursorSkillFiles() {
@@ -1754,6 +1965,31 @@ alwaysApply: false
1754
1965
  }
1755
1966
  function renderSkill(command) {
1756
1967
  const usage = command === "passthrough" ? "fet passthrough <openspec-command> [...args]" : `fet ${command}`;
1968
+ if (command === "fill-context") {
1969
+ return `<!-- FET:MANAGED
1970
+ schemaVersion: 1
1971
+ fetVersion: ${FET_VERSION}
1972
+ generator: cursor-adapter
1973
+ adapterVersion: 1
1974
+ command: fet fill-context
1975
+ FET:END -->
1976
+
1977
+ ---
1978
+ name: fet-fill-context
1979
+ description: Fill FET AGENTS.md placeholders with Cursor AI
1980
+ disable-model-invocation: false
1981
+ ---
1982
+
1983
+ Run \`fet fill-context\` first if the IDE commands need refreshing.
1984
+
1985
+ Then read:
1986
+
1987
+ - AGENTS.md
1988
+ - openspec/config.yaml
1989
+
1990
+ 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.
1991
+ `;
1992
+ }
1757
1993
  return `<!-- FET:MANAGED
1758
1994
  schemaVersion: 1
1759
1995
  fetVersion: ${FET_VERSION}
@@ -1786,7 +2022,7 @@ var CursorAdapter = class {
1786
2022
  adapterVersion = 1;
1787
2023
  async detect(projectRoot) {
1788
2024
  return {
1789
- detected: await exists4(join11(projectRoot, ".cursor")),
2025
+ detected: await exists4(join13(projectRoot, ".cursor")),
1790
2026
  reason: "Cursor adapter is available for any project"
1791
2027
  };
1792
2028
  }
@@ -1803,7 +2039,7 @@ var CursorAdapter = class {
1803
2039
  const written = [];
1804
2040
  const skipped = [];
1805
2041
  for (const file of plan.files) {
1806
- const target = join11(projectRoot, file.path);
2042
+ const target = join13(projectRoot, file.path);
1807
2043
  const existing = await readExisting2(target);
1808
2044
  if (existing && !existing.includes("FET:MANAGED") && !force) {
1809
2045
  throw new FetError({
@@ -1816,7 +2052,7 @@ var CursorAdapter = class {
1816
2052
  if (existing && !existing.includes("FET:MANAGED") && force) {
1817
2053
  await createBackup(target);
1818
2054
  }
1819
- await mkdir6(dirname6(target), { recursive: true });
2055
+ await mkdir7(dirname7(target), { recursive: true });
1820
2056
  await atomicWrite(target, file.content);
1821
2057
  written.push(file.path);
1822
2058
  }
@@ -1826,7 +2062,7 @@ var CursorAdapter = class {
1826
2062
  const plan = await this.planInstall(projectRoot);
1827
2063
  const checks = [];
1828
2064
  for (const file of plan.files) {
1829
- const target = join11(projectRoot, file.path);
2065
+ const target = join13(projectRoot, file.path);
1830
2066
  const content = await readExisting2(target);
1831
2067
  const managed = Boolean(content?.includes("FET:MANAGED"));
1832
2068
  const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
@@ -1842,7 +2078,7 @@ var CursorAdapter = class {
1842
2078
  };
1843
2079
  async function readExisting2(path) {
1844
2080
  try {
1845
- return await readFile10(path, "utf8");
2081
+ return await readFile13(path, "utf8");
1846
2082
  } catch {
1847
2083
  return null;
1848
2084
  }
@@ -1862,35 +2098,37 @@ import { promisify as promisify3 } from "util";
1862
2098
 
1863
2099
  // src/openspec/inspector.ts
1864
2100
  import { readdir, stat as stat7 } from "fs/promises";
1865
- import { join as join12 } from "path";
2101
+ import { join as join14 } from "path";
1866
2102
  async function inspectOpenSpecProject(projectRoot) {
1867
- const openspecPath = join12(projectRoot, "openspec");
1868
- const changesPath = join12(openspecPath, "changes");
1869
- const archivePath = join12(openspecPath, "archive");
2103
+ const openspecPath = join14(projectRoot, "openspec");
2104
+ const changesPath = join14(openspecPath, "changes");
2105
+ const legacyArchivePath = join14(openspecPath, "archive");
2106
+ const changesArchivePath = join14(changesPath, "archive");
1870
2107
  return {
1871
2108
  exists: await exists5(openspecPath),
1872
- changes: await listDirectories(changesPath),
1873
- archived: await listDirectories(archivePath)
2109
+ changes: await listDirectories(changesPath, { exclude: ["archive"] }),
2110
+ archived: [.../* @__PURE__ */ new Set([...await listDirectories(legacyArchivePath), ...await listDirectories(changesArchivePath)])]
1874
2111
  };
1875
2112
  }
1876
2113
  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");
2114
+ const changePath = join14(projectRoot, "openspec", "changes", changeId);
2115
+ const tasksPath = join14(changePath, "tasks.md");
2116
+ const specsPath = join14(changePath, "specs");
1880
2117
  return {
1881
2118
  changeId,
1882
2119
  exists: await exists5(changePath),
1883
- hasProposal: await exists5(join12(changePath, "proposal.md")),
2120
+ hasProposal: await exists5(join14(changePath, "proposal.md")),
1884
2121
  hasTasks: await exists5(tasksPath),
1885
2122
  hasSpecs: await exists5(specsPath),
1886
2123
  tasksPath,
1887
2124
  changePath
1888
2125
  };
1889
2126
  }
1890
- async function listDirectories(path) {
2127
+ async function listDirectories(path, options = {}) {
1891
2128
  try {
1892
2129
  const entries = await readdir(path, { withFileTypes: true });
1893
- return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2130
+ const excluded = new Set(options.exclude ?? []);
2131
+ return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
1894
2132
  } catch {
1895
2133
  return [];
1896
2134
  }
@@ -2056,12 +2294,12 @@ function parseCommands(help) {
2056
2294
  }
2057
2295
 
2058
2296
  // src/scanner/package.ts
2059
- import { readFile as readFile11, stat as stat8 } from "fs/promises";
2060
- import { join as join13 } from "path";
2297
+ import { readFile as readFile14, stat as stat8 } from "fs/promises";
2298
+ import { join as join15 } from "path";
2061
2299
  import { parse as parse2 } from "yaml";
2062
2300
  async function readPackageJson(projectRoot) {
2063
2301
  try {
2064
- return JSON.parse(await readFile11(join13(projectRoot, "package.json"), "utf8"));
2302
+ return JSON.parse(await readFile14(join15(projectRoot, "package.json"), "utf8"));
2065
2303
  } catch {
2066
2304
  return null;
2067
2305
  }
@@ -2127,7 +2365,7 @@ function detectFramework(pkg) {
2127
2365
  }
2128
2366
  async function detectLanguage(projectRoot, pkg) {
2129
2367
  const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
2130
- if (deps.typescript || await exists6(join13(projectRoot, "tsconfig.json"))) {
2368
+ if (deps.typescript || await exists6(join15(projectRoot, "tsconfig.json"))) {
2131
2369
  return "typescript";
2132
2370
  }
2133
2371
  return "javascript";
@@ -2142,7 +2380,7 @@ async function detectWorkspaces(projectRoot, pkg) {
2142
2380
  return packageWorkspaces;
2143
2381
  }
2144
2382
  try {
2145
- const workspace = parse2(await readFile11(join13(projectRoot, "pnpm-workspace.yaml"), "utf8"));
2383
+ const workspace = parse2(await readFile14(join15(projectRoot, "pnpm-workspace.yaml"), "utf8"));
2146
2384
  return (workspace?.packages ?? []).map((path) => ({
2147
2385
  name: path,
2148
2386
  path,
@@ -2162,7 +2400,7 @@ async function detectLockManagers(projectRoot) {
2162
2400
  ];
2163
2401
  const found = [];
2164
2402
  for (const [file, manager] of lockFiles) {
2165
- if (await exists6(join13(projectRoot, file))) {
2403
+ if (await exists6(join15(projectRoot, file))) {
2166
2404
  found.push(manager);
2167
2405
  }
2168
2406
  }
@@ -2188,12 +2426,12 @@ async function exists6(path) {
2188
2426
 
2189
2427
  // src/scanner/routes.ts
2190
2428
  import { readdir as readdir2, stat as stat9 } from "fs/promises";
2191
- import { join as join14, relative, sep } from "path";
2429
+ import { join as join16, relative, sep } from "path";
2192
2430
  async function scanRoutes(projectRoot) {
2193
2431
  const candidates = ["src/routes", "src/pages", "app", "pages"];
2194
2432
  const routes = [];
2195
2433
  for (const candidate of candidates) {
2196
- const root = join14(projectRoot, candidate);
2434
+ const root = join16(projectRoot, candidate);
2197
2435
  if (!await exists7(root)) {
2198
2436
  continue;
2199
2437
  }
@@ -2221,7 +2459,7 @@ async function listFiles(root) {
2221
2459
  const entries = await readdir2(root, { withFileTypes: true });
2222
2460
  const files = [];
2223
2461
  for (const entry of entries) {
2224
- const path = join14(root, entry.name);
2462
+ const path = join16(root, entry.name);
2225
2463
  if (entry.isDirectory()) {
2226
2464
  files.push(...await listFiles(path));
2227
2465
  } else {
@@ -2373,6 +2611,7 @@ var program = new Command();
2373
2611
  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
2612
  addGlobalOptions(program.command("init")).description("\u521D\u59CB\u5316 FET + OpenSpec").action(wrap("init", initCommand));
2375
2613
  addGlobalOptions(program.command("update-context")).description("\u66F4\u65B0\u9879\u76EE\u4E0A\u4E0B\u6587").action(wrap("update-context", updateContextCommand));
2614
+ addGlobalOptions(program.command("fill-context")).description("Refresh IDE prompts for filling AGENTS.md placeholders").action(wrap("fill-context", fillContextCommand));
2376
2615
  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
2616
  wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
2378
2617
  );