@nick848/fet 1.0.1 → 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/README.md +27 -21
- package/dist/cli/index.js +954 -92
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -182,7 +182,7 @@ function count(content, needle) {
|
|
|
182
182
|
|
|
183
183
|
// src/templates/agents-md.ts
|
|
184
184
|
function renderAgentsMd(scan) {
|
|
185
|
-
const
|
|
185
|
+
const commands = Object.entries(scan.commands).map(([name, command]) => `| ${name} | \`${command.command}\` | ${command.source} |`).join("\n");
|
|
186
186
|
const routes = scan.routes.map((route) => `| ${route.path} | ${route.source} | ${route.confidence}${route.inferred ? " inferred" : ""} |`).join("\n");
|
|
187
187
|
const workspaces = scan.project.workspaces.map((workspace) => `| ${workspace.name} | ${workspace.path} | ${workspace.source} |`).join("\n");
|
|
188
188
|
return `# Project Context
|
|
@@ -206,7 +206,7 @@ ${workspaces || "| root | . | inferred |"}
|
|
|
206
206
|
|
|
207
207
|
| Name | Command | Source |
|
|
208
208
|
|------|---------|--------|
|
|
209
|
-
${
|
|
209
|
+
${commands || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | [NEEDS LLM INPUT] |"}
|
|
210
210
|
|
|
211
211
|
## Structure
|
|
212
212
|
|
|
@@ -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
|
|
561
|
-
import { join as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
786
|
+
return join9(this.projectRoot, "openspec", "fet-state.json");
|
|
696
787
|
}
|
|
697
788
|
changePath(changeId) {
|
|
698
|
-
return
|
|
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
|
|
797
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
707
798
|
async function readCompletedTaskIds(tasksPath) {
|
|
708
799
|
let content;
|
|
709
800
|
try {
|
|
710
|
-
content = await
|
|
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 (
|
|
762
|
-
state.activeChangeId
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
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:
|
|
844
|
-
message: "OpenSpec 1.2.0
|
|
845
|
-
suggestedCommand: "
|
|
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: "
|
|
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
|
|
910
|
-
import { join as
|
|
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 =
|
|
978
|
-
const instructionsPath =
|
|
979
|
-
await
|
|
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 =
|
|
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
|
|
1188
|
+
const content = await readFile11(path, "utf8");
|
|
1032
1189
|
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
1033
1190
|
if (fileChangeId !== changeId) {
|
|
1034
1191
|
throw new FetError({
|
|
@@ -1081,12 +1238,13 @@ async function resolveChangeId(ctx) {
|
|
|
1081
1238
|
// src/cli/context.ts
|
|
1082
1239
|
import { resolve } from "path";
|
|
1083
1240
|
|
|
1084
|
-
// src/adapters/
|
|
1085
|
-
import { mkdir as
|
|
1086
|
-
import {
|
|
1241
|
+
// src/adapters/codex/index.ts
|
|
1242
|
+
import { mkdir as mkdir6, readFile as readFile12, stat as stat5 } from "fs/promises";
|
|
1243
|
+
import { homedir } from "os";
|
|
1244
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
1087
1245
|
|
|
1088
|
-
// src/adapters/
|
|
1089
|
-
var
|
|
1246
|
+
// src/adapters/commands.ts
|
|
1247
|
+
var FET_WORKFLOW_COMMANDS = [
|
|
1090
1248
|
"explore",
|
|
1091
1249
|
"propose",
|
|
1092
1250
|
"new",
|
|
@@ -1099,8 +1257,683 @@ var commands = [
|
|
|
1099
1257
|
"bulk-archive",
|
|
1100
1258
|
"onboard"
|
|
1101
1259
|
];
|
|
1260
|
+
var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "fill-context", "passthrough"];
|
|
1261
|
+
|
|
1262
|
+
// src/adapters/codex/templates.ts
|
|
1263
|
+
function codexGuideFile() {
|
|
1264
|
+
return {
|
|
1265
|
+
path: ".codex/fet/context.md",
|
|
1266
|
+
content: `<!-- FET:MANAGED
|
|
1267
|
+
schemaVersion: 1
|
|
1268
|
+
fetVersion: ${FET_VERSION}
|
|
1269
|
+
generator: codex-adapter
|
|
1270
|
+
adapterVersion: 1
|
|
1271
|
+
FET:END -->
|
|
1272
|
+
|
|
1273
|
+
# FET For Codex
|
|
1274
|
+
|
|
1275
|
+
Before doing FET or OpenSpec work in Codex, read:
|
|
1276
|
+
|
|
1277
|
+
- AGENTS.md
|
|
1278
|
+
- openspec/config.yaml
|
|
1279
|
+
- the active change files under openspec/changes/<change-id>/, when a change is selected
|
|
1280
|
+
|
|
1281
|
+
Use the terminal command \`fet <command>\` as the source of truth for workflow transitions. These files are Codex-readable guidance; they do not register native slash commands.
|
|
1282
|
+
|
|
1283
|
+
Command guides live in .codex/fet/commands/.
|
|
1284
|
+
`
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
function codexCommandFiles() {
|
|
1288
|
+
return FET_ADAPTER_COMMANDS.map((command) => ({
|
|
1289
|
+
path: `.codex/fet/commands/${command}.md`,
|
|
1290
|
+
content: renderCommand(command)
|
|
1291
|
+
}));
|
|
1292
|
+
}
|
|
1293
|
+
function codexSlashPromptFiles() {
|
|
1294
|
+
return FET_ADAPTER_COMMANDS.map((command) => ({
|
|
1295
|
+
path: `prompts/fet-${command}.md`,
|
|
1296
|
+
content: renderSlashPrompt(command)
|
|
1297
|
+
}));
|
|
1298
|
+
}
|
|
1299
|
+
function renderCommand(command) {
|
|
1300
|
+
if (command === "fill-context") {
|
|
1301
|
+
return renderFillContextCommand();
|
|
1302
|
+
}
|
|
1303
|
+
if (command === "passthrough") {
|
|
1304
|
+
return renderPassthroughCommand();
|
|
1305
|
+
}
|
|
1306
|
+
return `<!-- FET:MANAGED
|
|
1307
|
+
schemaVersion: 1
|
|
1308
|
+
fetVersion: ${FET_VERSION}
|
|
1309
|
+
generator: codex-adapter
|
|
1310
|
+
adapterVersion: 1
|
|
1311
|
+
command: fet ${command}
|
|
1312
|
+
FET:END -->
|
|
1313
|
+
|
|
1314
|
+
# fet ${command}
|
|
1315
|
+
|
|
1316
|
+
When the user asks Codex to run the FET ${command} workflow, first make sure the project context is loaded from AGENTS.md and openspec/config.yaml.
|
|
1317
|
+
|
|
1318
|
+
Then run:
|
|
1319
|
+
|
|
1320
|
+
\`\`\`sh
|
|
1321
|
+
fet ${command}
|
|
1322
|
+
\`\`\`
|
|
1323
|
+
|
|
1324
|
+
If the command needs a change id, pass it with \`--change <change-id>\` or use the active OpenSpec change from the user's request.
|
|
1325
|
+
|
|
1326
|
+
After the command completes, report the important next steps from the FET output and keep any generated OpenSpec artifacts in the normal project workflow.
|
|
1327
|
+
`;
|
|
1328
|
+
}
|
|
1329
|
+
function renderPassthroughCommand() {
|
|
1330
|
+
return `<!-- FET:MANAGED
|
|
1331
|
+
schemaVersion: 1
|
|
1332
|
+
fetVersion: ${FET_VERSION}
|
|
1333
|
+
generator: codex-adapter
|
|
1334
|
+
adapterVersion: 1
|
|
1335
|
+
command: fet passthrough <openspec-command>
|
|
1336
|
+
FET:END -->
|
|
1337
|
+
|
|
1338
|
+
# fet passthrough
|
|
1339
|
+
|
|
1340
|
+
When the user asks Codex to run an OpenSpec command that FET does not manage as a first-class workflow command, use FET passthrough instead of calling OpenSpec directly.
|
|
1341
|
+
|
|
1342
|
+
Then run:
|
|
1343
|
+
|
|
1344
|
+
\`\`\`sh
|
|
1345
|
+
fet passthrough <openspec-command> [...args]
|
|
1346
|
+
\`\`\`
|
|
1347
|
+
|
|
1348
|
+
This preserves the FET entry point while allowing access to unmanaged or newly added OpenSpec commands. Passthrough does not update FET lifecycle state.
|
|
1349
|
+
`;
|
|
1350
|
+
}
|
|
1351
|
+
function renderSlashPrompt(command) {
|
|
1352
|
+
if (command === "continue") {
|
|
1353
|
+
return renderContinueSlashPrompt();
|
|
1354
|
+
}
|
|
1355
|
+
if (command === "ff" || command === "propose") {
|
|
1356
|
+
return renderFastForwardSlashPrompt(command);
|
|
1357
|
+
}
|
|
1358
|
+
if (command === "explore") {
|
|
1359
|
+
return renderExploreSlashPrompt();
|
|
1360
|
+
}
|
|
1361
|
+
if (command === "new") {
|
|
1362
|
+
return renderNewSlashPrompt();
|
|
1363
|
+
}
|
|
1364
|
+
if (command === "apply") {
|
|
1365
|
+
return renderApplySlashPrompt();
|
|
1366
|
+
}
|
|
1367
|
+
if (command === "verify") {
|
|
1368
|
+
return renderVerifySlashPrompt();
|
|
1369
|
+
}
|
|
1370
|
+
if (command === "sync") {
|
|
1371
|
+
return renderSyncSlashPrompt();
|
|
1372
|
+
}
|
|
1373
|
+
if (command === "archive") {
|
|
1374
|
+
return renderArchiveSlashPrompt();
|
|
1375
|
+
}
|
|
1376
|
+
if (command === "bulk-archive") {
|
|
1377
|
+
return renderBulkArchiveSlashPrompt();
|
|
1378
|
+
}
|
|
1379
|
+
if (command === "onboard") {
|
|
1380
|
+
return renderOnboardSlashPrompt();
|
|
1381
|
+
}
|
|
1382
|
+
if (command === "fill-context") {
|
|
1383
|
+
return renderFillContextSlashPrompt();
|
|
1384
|
+
}
|
|
1385
|
+
if (command === "passthrough") {
|
|
1386
|
+
return renderPassthroughSlashPrompt();
|
|
1387
|
+
}
|
|
1388
|
+
const usage = command === "passthrough" ? "fet passthrough <openspec-command> [...args]" : `fet ${command} [...args]`;
|
|
1389
|
+
const shellCommand = command === "passthrough" ? "fet passthrough $ARGUMENTS" : `fet ${command} $ARGUMENTS`;
|
|
1390
|
+
const description = command === "passthrough" ? "Run an unmanaged OpenSpec command through FET passthrough" : `Run the FET-managed OpenSpec ${command} workflow`;
|
|
1391
|
+
return `<!-- FET:MANAGED
|
|
1392
|
+
schemaVersion: 1
|
|
1393
|
+
fetVersion: ${FET_VERSION}
|
|
1394
|
+
generator: codex-adapter
|
|
1395
|
+
adapterVersion: 1
|
|
1396
|
+
command: ${usage}
|
|
1397
|
+
FET:END -->
|
|
1398
|
+
|
|
1399
|
+
---
|
|
1400
|
+
description: ${description}
|
|
1401
|
+
argument-hint: command arguments
|
|
1402
|
+
---
|
|
1403
|
+
|
|
1404
|
+
Use FET as the entry point for this OpenSpec workflow.
|
|
1405
|
+
|
|
1406
|
+
Before running the command, make sure the relevant project context is loaded from AGENTS.md and openspec/config.yaml. If a change id is needed and was not provided, infer it from the active FET/OpenSpec state when unambiguous; otherwise ask the user for the change id.
|
|
1407
|
+
|
|
1408
|
+
Run:
|
|
1409
|
+
|
|
1410
|
+
\`\`\`sh
|
|
1411
|
+
${shellCommand}
|
|
1412
|
+
\`\`\`
|
|
1413
|
+
|
|
1414
|
+
After it completes, summarize the important FET output and next steps.
|
|
1415
|
+
`;
|
|
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
|
+
}
|
|
1473
|
+
function renderNewSlashPrompt() {
|
|
1474
|
+
return renderManagedSlashPrompt(
|
|
1475
|
+
"fet new [...args]",
|
|
1476
|
+
"Create a new FET/OpenSpec change scaffold",
|
|
1477
|
+
`Create a new FET-managed OpenSpec change scaffold.
|
|
1478
|
+
|
|
1479
|
+
Input after the slash command should be a kebab-case change id or a short description.
|
|
1480
|
+
|
|
1481
|
+
Steps:
|
|
1482
|
+
|
|
1483
|
+
1. Load project context from AGENTS.md and openspec/config.yaml.
|
|
1484
|
+
2. If no input was provided, ask what change the user wants to build or fix.
|
|
1485
|
+
3. Derive a kebab-case change id when the user provided a description.
|
|
1486
|
+
4. Create the change through FET:
|
|
1487
|
+
\`\`\`sh
|
|
1488
|
+
fet new <change-id>
|
|
1489
|
+
\`\`\`
|
|
1490
|
+
5. Show current artifact status:
|
|
1491
|
+
\`\`\`sh
|
|
1492
|
+
fet passthrough status --change <change-id>
|
|
1493
|
+
\`\`\`
|
|
1494
|
+
6. Get the first ready artifact instructions:
|
|
1495
|
+
\`\`\`sh
|
|
1496
|
+
fet continue <first-ready-artifact-id> --change <change-id>
|
|
1497
|
+
\`\`\`
|
|
1498
|
+
|
|
1499
|
+
Guardrails:
|
|
1500
|
+
- Do not create artifact files in /prompts:fet-new.
|
|
1501
|
+
- If the change already exists, suggest /prompts:fet-continue <change-id>.
|
|
1502
|
+
- Show the change location and the next command to create the first artifact.`
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
function renderApplySlashPrompt() {
|
|
1506
|
+
return renderManagedSlashPrompt(
|
|
1507
|
+
"fet apply [...args]",
|
|
1508
|
+
"Implement tasks from a FET/OpenSpec change",
|
|
1509
|
+
`Implement a FET-managed OpenSpec change.
|
|
1510
|
+
|
|
1511
|
+
Input after the slash command should identify the change, for example a change id or --change <change-id>.
|
|
1512
|
+
|
|
1513
|
+
Steps:
|
|
1514
|
+
|
|
1515
|
+
1. Resolve the change id. If ambiguous, ask the user.
|
|
1516
|
+
2. Run the native OpenSpec apply flow through FET:
|
|
1517
|
+
\`\`\`sh
|
|
1518
|
+
fet apply --change <change-id> --json
|
|
1519
|
+
\`\`\`
|
|
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>/.
|
|
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>.
|
|
1522
|
+
5. Implement pending tasks one by one:
|
|
1523
|
+
- Keep code changes minimal and scoped to the task.
|
|
1524
|
+
- Follow proposal, specs, design, and tasks.
|
|
1525
|
+
- Mark each completed task checkbox in tasks.md from \`- [ ]\` to \`- [x]\`.
|
|
1526
|
+
- Pause and ask if a task is ambiguous or reveals a design conflict.
|
|
1527
|
+
6. After completing or pausing, summarize completed tasks, remaining tasks, and blockers.
|
|
1528
|
+
|
|
1529
|
+
Guardrails:
|
|
1530
|
+
- Never skip reading OpenSpec artifacts before implementation.
|
|
1531
|
+
- Do not mark a task complete until the code change is actually done.
|
|
1532
|
+
- Do not run sync or archive from apply.`
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
function renderVerifySlashPrompt() {
|
|
1536
|
+
return renderManagedSlashPrompt(
|
|
1537
|
+
"fet verify [...args]",
|
|
1538
|
+
"Verify a FET/OpenSpec change before sync or archive",
|
|
1539
|
+
`Verify a FET-managed OpenSpec change.
|
|
1540
|
+
|
|
1541
|
+
Input after the slash command should identify the change. If the user passes --done, declare verification complete only after checks have been performed or explicitly accepted by the user.
|
|
1542
|
+
|
|
1543
|
+
Steps:
|
|
1544
|
+
|
|
1545
|
+
1. Resolve the change id. If ambiguous, ask the user.
|
|
1546
|
+
2. Generate FET verification instructions:
|
|
1547
|
+
\`\`\`sh
|
|
1548
|
+
fet verify --change <change-id>
|
|
1549
|
+
\`\`\`
|
|
1550
|
+
3. Read openspec/changes/<change-id>/.fet/verify-instructions.md.
|
|
1551
|
+
4. Read available artifacts for the change: proposal.md, design.md, tasks.md, and delta specs.
|
|
1552
|
+
5. Verify:
|
|
1553
|
+
- Completeness: tasks and required artifacts are present and checked off where appropriate.
|
|
1554
|
+
- Correctness: implementation evidence matches specs and proposal.
|
|
1555
|
+
- Coherence: code follows design decisions and project patterns.
|
|
1556
|
+
6. Report critical issues, warnings, and suggestions with file references.
|
|
1557
|
+
7. If there are no critical issues, or the user explicitly accepts the remaining risk, mark FET verification done:
|
|
1558
|
+
\`\`\`sh
|
|
1559
|
+
fet verify --done --change <change-id>
|
|
1560
|
+
\`\`\`
|
|
1561
|
+
|
|
1562
|
+
Guardrails:
|
|
1563
|
+
- Do not run --done before producing a verification assessment.
|
|
1564
|
+
- Treat incomplete tasks or missing required behavior as critical unless user explicitly accepts them.
|
|
1565
|
+
- Suggest /prompts:fet-sync <change-id> and /prompts:fet-archive <change-id> only after verification is done.`
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
function renderSyncSlashPrompt() {
|
|
1569
|
+
return renderManagedSlashPrompt(
|
|
1570
|
+
"fet sync [...args]",
|
|
1571
|
+
"Sync delta specs and validate a FET/OpenSpec change",
|
|
1572
|
+
`Sync a FET-managed OpenSpec change.
|
|
1573
|
+
|
|
1574
|
+
Input after the slash command should identify the change.
|
|
1575
|
+
|
|
1576
|
+
Steps:
|
|
1577
|
+
|
|
1578
|
+
1. Resolve the change id. If ambiguous, ask the user.
|
|
1579
|
+
2. Confirm FET verification is complete or run /prompts:fet-verify <change-id> first.
|
|
1580
|
+
3. Find delta specs under openspec/changes/<change-id>/specs/*/spec.md.
|
|
1581
|
+
4. If delta specs exist, intelligently merge them into openspec/specs/<capability>/spec.md:
|
|
1582
|
+
- Add ADDED requirements.
|
|
1583
|
+
- Apply MODIFIED requirements without deleting unrelated existing scenarios.
|
|
1584
|
+
- Remove REMOVED requirements.
|
|
1585
|
+
- Apply RENAMED requirements.
|
|
1586
|
+
- Preserve main-spec content not mentioned in the delta.
|
|
1587
|
+
5. If no delta specs exist, state that there is nothing to merge.
|
|
1588
|
+
6. Run the FET sync gate and strict OpenSpec validation:
|
|
1589
|
+
\`\`\`sh
|
|
1590
|
+
fet sync --change <change-id>
|
|
1591
|
+
\`\`\`
|
|
1592
|
+
7. Summarize updated capabilities and validation result.
|
|
1593
|
+
|
|
1594
|
+
Guardrails:
|
|
1595
|
+
- Read both delta and main specs before editing.
|
|
1596
|
+
- Make sync idempotent where possible.
|
|
1597
|
+
- If FET reports verify is not done, stop and run/ask for verification instead of bypassing the gate.`
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
function renderArchiveSlashPrompt() {
|
|
1601
|
+
return renderManagedSlashPrompt(
|
|
1602
|
+
"fet archive [...args]",
|
|
1603
|
+
"Archive a verified FET/OpenSpec change",
|
|
1604
|
+
`Archive a FET-managed OpenSpec change.
|
|
1605
|
+
|
|
1606
|
+
Input after the slash command should identify the change.
|
|
1607
|
+
|
|
1608
|
+
Steps:
|
|
1609
|
+
|
|
1610
|
+
1. Resolve the change id. If ambiguous, ask the user.
|
|
1611
|
+
2. Check artifact and task status:
|
|
1612
|
+
\`\`\`sh
|
|
1613
|
+
fet passthrough status --change <change-id> --json
|
|
1614
|
+
\`\`\`
|
|
1615
|
+
3. If tasks or required artifacts are incomplete, show the warning and ask whether to continue.
|
|
1616
|
+
4. If delta specs exist, assess whether they have been synced. If not, recommend /prompts:fet-sync <change-id> before archiving.
|
|
1617
|
+
5. Confirm FET verification is complete. If not, run or suggest /prompts:fet-verify <change-id>.
|
|
1618
|
+
6. Archive through FET:
|
|
1619
|
+
\`\`\`sh
|
|
1620
|
+
fet archive --change <change-id>
|
|
1621
|
+
\`\`\`
|
|
1622
|
+
7. Report archive result, sync status, and any warnings.
|
|
1623
|
+
|
|
1624
|
+
Guardrails:
|
|
1625
|
+
- Do not move change directories manually; use fet archive.
|
|
1626
|
+
- Do not bypass the FET verify gate.
|
|
1627
|
+
- Ask before archiving with incomplete tasks or unsynced delta specs.`
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
function renderBulkArchiveSlashPrompt() {
|
|
1631
|
+
return renderManagedSlashPrompt(
|
|
1632
|
+
"fet bulk-archive [...args]",
|
|
1633
|
+
"Archive multiple FET/OpenSpec changes safely",
|
|
1634
|
+
`Bulk archive FET-managed OpenSpec changes.
|
|
1635
|
+
|
|
1636
|
+
Steps:
|
|
1637
|
+
|
|
1638
|
+
1. List active changes:
|
|
1639
|
+
\`\`\`sh
|
|
1640
|
+
fet passthrough status --json
|
|
1641
|
+
\`\`\`
|
|
1642
|
+
2. Ask the user which changes to archive. Do not auto-select.
|
|
1643
|
+
3. For each selected change:
|
|
1644
|
+
- Check artifact/task status.
|
|
1645
|
+
- Confirm verification is done.
|
|
1646
|
+
- Recommend sync if delta specs are unsynced.
|
|
1647
|
+
- Run \`fet archive --change <change-id>\`.
|
|
1648
|
+
4. If \`fet bulk-archive\` reports that the current OpenSpec version does not support a native bulk command, archive selected changes one by one through \`fet archive --change <change-id>\`.
|
|
1649
|
+
5. Summarize successes, skipped changes, and failures.
|
|
1650
|
+
|
|
1651
|
+
Guardrails:
|
|
1652
|
+
- Never archive all changes without explicit user selection.
|
|
1653
|
+
- Do not bypass verify or warnings for individual changes.
|
|
1654
|
+
- Continue with remaining selected changes if one archive fails, then report the failure clearly.`
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
function renderOnboardSlashPrompt() {
|
|
1658
|
+
return renderManagedSlashPrompt(
|
|
1659
|
+
"fet onboard [...args]",
|
|
1660
|
+
"Load FET/OpenSpec onboarding context",
|
|
1661
|
+
`Onboard the user or Codex into the FET/OpenSpec workflow for this project.
|
|
1662
|
+
|
|
1663
|
+
Steps:
|
|
1664
|
+
|
|
1665
|
+
1. Read AGENTS.md and openspec/config.yaml.
|
|
1666
|
+
2. Run FET onboarding:
|
|
1667
|
+
\`\`\`sh
|
|
1668
|
+
fet onboard $ARGUMENTS
|
|
1669
|
+
\`\`\`
|
|
1670
|
+
3. Summarize:
|
|
1671
|
+
- Project context.
|
|
1672
|
+
- Available active changes.
|
|
1673
|
+
- Core commands: new, continue, ff, apply, verify, sync, archive.
|
|
1674
|
+
- Where Codex prompts live.
|
|
1675
|
+
|
|
1676
|
+
Guardrails:
|
|
1677
|
+
- Do not create or modify artifacts during onboard.
|
|
1678
|
+
- Use this command to orient the session, then suggest the next concrete FET command.`
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
function renderPassthroughSlashPrompt() {
|
|
1682
|
+
return renderManagedSlashPrompt(
|
|
1683
|
+
"fet passthrough <openspec-command> [...args]",
|
|
1684
|
+
"Run an unmanaged OpenSpec command through FET",
|
|
1685
|
+
`Run an OpenSpec command that FET does not manage as a first-class workflow command.
|
|
1686
|
+
|
|
1687
|
+
Steps:
|
|
1688
|
+
|
|
1689
|
+
1. Identify the OpenSpec command and arguments from the user's input.
|
|
1690
|
+
2. Run it through FET:
|
|
1691
|
+
\`\`\`sh
|
|
1692
|
+
fet passthrough <openspec-command> [...args]
|
|
1693
|
+
\`\`\`
|
|
1694
|
+
3. Report the output and whether FET lifecycle state was updated.
|
|
1695
|
+
|
|
1696
|
+
Guardrails:
|
|
1697
|
+
- Do not call openspec directly unless FET passthrough itself is unavailable.
|
|
1698
|
+
- Remember that passthrough does not update FET lifecycle state.
|
|
1699
|
+
- For managed workflows, prefer the specific FET prompt instead of passthrough.`
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
function renderExploreSlashPrompt() {
|
|
1703
|
+
return renderManagedSlashPrompt(
|
|
1704
|
+
"fet explore [...args]",
|
|
1705
|
+
"Explore requirements for a FET/OpenSpec change",
|
|
1706
|
+
`Enter exploration mode for a FET-managed OpenSpec change.
|
|
1707
|
+
|
|
1708
|
+
Use this command for thinking and proposal shaping, not application implementation.
|
|
1709
|
+
|
|
1710
|
+
Input after the slash command may be a change id, a feature idea, or a problem description.
|
|
1711
|
+
|
|
1712
|
+
Steps:
|
|
1713
|
+
|
|
1714
|
+
1. Load project context from AGENTS.md and openspec/config.yaml.
|
|
1715
|
+
2. Check existing changes:
|
|
1716
|
+
\`\`\`sh
|
|
1717
|
+
fet passthrough status --json
|
|
1718
|
+
\`\`\`
|
|
1719
|
+
3. If the input names an existing change, read its current artifacts under openspec/changes/<change-id>/.
|
|
1720
|
+
4. If the input is a new idea and the user wants to capture it, derive a kebab-case change id and create the change:
|
|
1721
|
+
\`\`\`sh
|
|
1722
|
+
fet new <change-id>
|
|
1723
|
+
\`\`\`
|
|
1724
|
+
5. Run the native OpenSpec exploration flow through FET so clarification prompts stay interactive:
|
|
1725
|
+
\`\`\`sh
|
|
1726
|
+
fet explore --change <change-id>
|
|
1727
|
+
\`\`\`
|
|
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.
|
|
1729
|
+
|
|
1730
|
+
Guardrails:
|
|
1731
|
+
- Do not write application code in explore mode.
|
|
1732
|
+
- Ask a clarifying question if the proposal would otherwise be mostly guesswork.
|
|
1733
|
+
- Creating or updating OpenSpec artifacts is allowed when the user asks to capture the thinking.
|
|
1734
|
+
- After creating proposal.md, show the path and suggest /prompts:fet-continue <change-id> for the next artifact.`
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
function renderContinueSlashPrompt() {
|
|
1738
|
+
return renderManagedSlashPrompt(
|
|
1739
|
+
"fet continue [...args]",
|
|
1740
|
+
"Create the next FET/OpenSpec artifact",
|
|
1741
|
+
`Continue a FET-managed OpenSpec change by creating exactly one ready artifact.
|
|
1742
|
+
|
|
1743
|
+
Input after the slash command should be a change id, optionally followed by an artifact id.
|
|
1744
|
+
|
|
1745
|
+
Steps:
|
|
1746
|
+
|
|
1747
|
+
1. Load project context from AGENTS.md and openspec/config.yaml.
|
|
1748
|
+
2. Resolve the change id. If it is missing and cannot be inferred unambiguously, ask the user.
|
|
1749
|
+
3. Check status:
|
|
1750
|
+
\`\`\`sh
|
|
1751
|
+
fet passthrough status --change <change-id> --json
|
|
1752
|
+
\`\`\`
|
|
1753
|
+
4. Pick the first artifact whose status is ready, unless the user specified an artifact id.
|
|
1754
|
+
5. Run the native OpenSpec continue flow through FET:
|
|
1755
|
+
\`\`\`sh
|
|
1756
|
+
fet continue <artifact-id> --change <change-id> --json
|
|
1757
|
+
\`\`\`
|
|
1758
|
+
6. Follow the native continue output. When it provides template, instruction, dependencies, and outputPath, use those fields.
|
|
1759
|
+
7. Read dependency files before writing.
|
|
1760
|
+
8. Create the artifact file at outputPath. Do not copy context/rules wrapper text into the artifact; use those fields only as constraints.
|
|
1761
|
+
9. Verify the file exists, then run:
|
|
1762
|
+
\`\`\`sh
|
|
1763
|
+
fet passthrough status --change <change-id>
|
|
1764
|
+
\`\`\`
|
|
1765
|
+
|
|
1766
|
+
Output:
|
|
1767
|
+
- State which artifact was created.
|
|
1768
|
+
- Show the file path.
|
|
1769
|
+
- Show the current status and what to run next.
|
|
1770
|
+
|
|
1771
|
+
Guardrails:
|
|
1772
|
+
- Create one artifact per invocation.
|
|
1773
|
+
- Never skip dependency order.
|
|
1774
|
+
- Ask the user if instructions are ambiguous enough that a useful artifact cannot be written.`
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
function renderFastForwardSlashPrompt(command) {
|
|
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>";
|
|
1780
|
+
return renderManagedSlashPrompt(
|
|
1781
|
+
`fet ${command} [...args]`,
|
|
1782
|
+
command === "propose" ? "Create a change and generate required OpenSpec artifacts" : "Generate required OpenSpec artifacts for a change",
|
|
1783
|
+
`${title}.
|
|
1784
|
+
|
|
1785
|
+
Input after the slash command may be a change id or a description of what the user wants to build.
|
|
1786
|
+
|
|
1787
|
+
Steps:
|
|
1788
|
+
|
|
1789
|
+
1. Load project context from AGENTS.md and openspec/config.yaml.
|
|
1790
|
+
2. Resolve the change id or description. If ambiguous, ask the user.
|
|
1791
|
+
3. Run the native OpenSpec ${command} flow through FET:
|
|
1792
|
+
\`\`\`sh
|
|
1793
|
+
${commandLine}
|
|
1794
|
+
\`\`\`
|
|
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.
|
|
1797
|
+
|
|
1798
|
+
Artifact rules:
|
|
1799
|
+
- Follow the instruction field from OpenSpec/FET for each artifact.
|
|
1800
|
+
- Use template as structure, filling it with concrete project-specific content.
|
|
1801
|
+
- Do not copy context/rules wrapper text into artifact files.
|
|
1802
|
+
- Verify each file exists after writing.
|
|
1803
|
+
|
|
1804
|
+
Output:
|
|
1805
|
+
- Change id and location.
|
|
1806
|
+
- Artifacts created.
|
|
1807
|
+
- Current status.
|
|
1808
|
+
- Next recommended command, usually /prompts:fet-apply <change-id>.`
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
function renderManagedSlashPrompt(command, description, body) {
|
|
1812
|
+
return `<!-- FET:MANAGED
|
|
1813
|
+
schemaVersion: 1
|
|
1814
|
+
fetVersion: ${FET_VERSION}
|
|
1815
|
+
generator: codex-adapter
|
|
1816
|
+
adapterVersion: 1
|
|
1817
|
+
command: ${command}
|
|
1818
|
+
FET:END -->
|
|
1819
|
+
|
|
1820
|
+
---
|
|
1821
|
+
description: ${description}
|
|
1822
|
+
argument-hint: command arguments
|
|
1823
|
+
---
|
|
1824
|
+
|
|
1825
|
+
${body}
|
|
1826
|
+
`;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// src/adapters/codex/index.ts
|
|
1830
|
+
var CodexAdapter = class {
|
|
1831
|
+
tool = "codex";
|
|
1832
|
+
adapterVersion = 1;
|
|
1833
|
+
async detect(projectRoot) {
|
|
1834
|
+
return {
|
|
1835
|
+
detected: await exists3(join12(projectRoot, ".codex")) || await exists3(join12(projectRoot, "AGENTS.md")),
|
|
1836
|
+
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
async planInstall(_projectRoot) {
|
|
1840
|
+
return {
|
|
1841
|
+
tool: this.tool,
|
|
1842
|
+
files: [
|
|
1843
|
+
...[codexGuideFile(), ...codexCommandFiles()].map((file) => ({
|
|
1844
|
+
...file,
|
|
1845
|
+
managed: true,
|
|
1846
|
+
root: "project"
|
|
1847
|
+
})),
|
|
1848
|
+
...codexSlashPromptFiles().map((file) => ({
|
|
1849
|
+
...file,
|
|
1850
|
+
managed: true,
|
|
1851
|
+
root: "codex-home"
|
|
1852
|
+
}))
|
|
1853
|
+
]
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
async install(projectRoot, plan, force = false) {
|
|
1857
|
+
const written = [];
|
|
1858
|
+
const skipped = [];
|
|
1859
|
+
for (const file of plan.files) {
|
|
1860
|
+
const target = resolveTarget(projectRoot, file);
|
|
1861
|
+
const displayPath = displayPathFor(file);
|
|
1862
|
+
const existing = await readExisting(target);
|
|
1863
|
+
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
1864
|
+
throw new FetError({
|
|
1865
|
+
code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
|
|
1866
|
+
message: "Codex adapter file already exists and is not managed by FET",
|
|
1867
|
+
details: { path: displayPath },
|
|
1868
|
+
suggestedCommand: "fet init --yes"
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
1872
|
+
await createBackup(target);
|
|
1873
|
+
}
|
|
1874
|
+
await mkdir6(dirname6(target), { recursive: true });
|
|
1875
|
+
await atomicWrite(target, file.content);
|
|
1876
|
+
written.push(displayPath);
|
|
1877
|
+
}
|
|
1878
|
+
return { tool: this.tool, written, skipped };
|
|
1879
|
+
}
|
|
1880
|
+
async doctor(projectRoot) {
|
|
1881
|
+
const plan = await this.planInstall(projectRoot);
|
|
1882
|
+
const checks = [];
|
|
1883
|
+
for (const file of plan.files) {
|
|
1884
|
+
const target = resolveTarget(projectRoot, file);
|
|
1885
|
+
const displayPath = displayPathFor(file);
|
|
1886
|
+
const content = await readExisting(target);
|
|
1887
|
+
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
1888
|
+
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
1889
|
+
checks.push({
|
|
1890
|
+
id: `codex:${displayPath}`,
|
|
1891
|
+
status: !content ? "warn" : managed && versionMatches ? "pass" : "warn",
|
|
1892
|
+
message: !content ? `${displayPath} is missing` : !managed ? `${displayPath} exists but is not managed by FET` : !versionMatches ? `${displayPath} adapterVersion is stale` : `${displayPath} is managed by FET`,
|
|
1893
|
+
suggestedCommand: !content || !managed || !versionMatches ? "fet init" : void 0
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
return checks;
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
function resolveTarget(projectRoot, file) {
|
|
1900
|
+
if (file.root === "codex-home") {
|
|
1901
|
+
return join12(resolveCodexHome(), file.path);
|
|
1902
|
+
}
|
|
1903
|
+
return join12(projectRoot, file.path);
|
|
1904
|
+
}
|
|
1905
|
+
function displayPathFor(file) {
|
|
1906
|
+
if (file.root === "codex-home") {
|
|
1907
|
+
return `$CODEX_HOME/${file.path.replaceAll("\\", "/")}`;
|
|
1908
|
+
}
|
|
1909
|
+
return file.path;
|
|
1910
|
+
}
|
|
1911
|
+
function resolveCodexHome() {
|
|
1912
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join12(homedir(), ".codex");
|
|
1913
|
+
}
|
|
1914
|
+
async function readExisting(path) {
|
|
1915
|
+
try {
|
|
1916
|
+
return await readFile12(path, "utf8");
|
|
1917
|
+
} catch {
|
|
1918
|
+
return null;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
async function exists3(path) {
|
|
1922
|
+
try {
|
|
1923
|
+
await stat5(path);
|
|
1924
|
+
return true;
|
|
1925
|
+
} catch {
|
|
1926
|
+
return false;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// src/adapters/cursor/index.ts
|
|
1931
|
+
import { mkdir as mkdir7, readFile as readFile13, stat as stat6 } from "fs/promises";
|
|
1932
|
+
import { dirname as dirname7, join as join13 } from "path";
|
|
1933
|
+
|
|
1934
|
+
// src/adapters/cursor/templates.ts
|
|
1102
1935
|
function cursorSkillFiles() {
|
|
1103
|
-
return
|
|
1936
|
+
return FET_ADAPTER_COMMANDS.map((command) => ({
|
|
1104
1937
|
path: `.cursor/skills/fet-${command}/SKILL.md`,
|
|
1105
1938
|
content: renderSkill(command)
|
|
1106
1939
|
}));
|
|
@@ -1131,12 +1964,38 @@ alwaysApply: false
|
|
|
1131
1964
|
};
|
|
1132
1965
|
}
|
|
1133
1966
|
function renderSkill(command) {
|
|
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
|
+
}
|
|
1134
1993
|
return `<!-- FET:MANAGED
|
|
1135
1994
|
schemaVersion: 1
|
|
1136
1995
|
fetVersion: ${FET_VERSION}
|
|
1137
1996
|
generator: cursor-adapter
|
|
1138
1997
|
adapterVersion: 1
|
|
1139
|
-
command:
|
|
1998
|
+
command: ${usage}
|
|
1140
1999
|
FET:END -->
|
|
1141
2000
|
|
|
1142
2001
|
---
|
|
@@ -1150,7 +2009,7 @@ disable-model-invocation: true
|
|
|
1150
2009
|
\u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
|
|
1151
2010
|
|
|
1152
2011
|
\`\`\`sh
|
|
1153
|
-
|
|
2012
|
+
${usage}
|
|
1154
2013
|
\`\`\`
|
|
1155
2014
|
|
|
1156
2015
|
\u6267\u884C\u524D\u8BF7\u786E\u8BA4\u5DF2\u9605\u8BFB AGENTS.md \u4E0E openspec/config.yaml\u3002
|
|
@@ -1163,7 +2022,7 @@ var CursorAdapter = class {
|
|
|
1163
2022
|
adapterVersion = 1;
|
|
1164
2023
|
async detect(projectRoot) {
|
|
1165
2024
|
return {
|
|
1166
|
-
detected: await
|
|
2025
|
+
detected: await exists4(join13(projectRoot, ".cursor")),
|
|
1167
2026
|
reason: "Cursor adapter is available for any project"
|
|
1168
2027
|
};
|
|
1169
2028
|
}
|
|
@@ -1180,8 +2039,8 @@ var CursorAdapter = class {
|
|
|
1180
2039
|
const written = [];
|
|
1181
2040
|
const skipped = [];
|
|
1182
2041
|
for (const file of plan.files) {
|
|
1183
|
-
const target =
|
|
1184
|
-
const existing = await
|
|
2042
|
+
const target = join13(projectRoot, file.path);
|
|
2043
|
+
const existing = await readExisting2(target);
|
|
1185
2044
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
1186
2045
|
throw new FetError({
|
|
1187
2046
|
code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
|
|
@@ -1193,7 +2052,7 @@ var CursorAdapter = class {
|
|
|
1193
2052
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
1194
2053
|
await createBackup(target);
|
|
1195
2054
|
}
|
|
1196
|
-
await
|
|
2055
|
+
await mkdir7(dirname7(target), { recursive: true });
|
|
1197
2056
|
await atomicWrite(target, file.content);
|
|
1198
2057
|
written.push(file.path);
|
|
1199
2058
|
}
|
|
@@ -1203,8 +2062,8 @@ var CursorAdapter = class {
|
|
|
1203
2062
|
const plan = await this.planInstall(projectRoot);
|
|
1204
2063
|
const checks = [];
|
|
1205
2064
|
for (const file of plan.files) {
|
|
1206
|
-
const target =
|
|
1207
|
-
const content = await
|
|
2065
|
+
const target = join13(projectRoot, file.path);
|
|
2066
|
+
const content = await readExisting2(target);
|
|
1208
2067
|
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
1209
2068
|
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
1210
2069
|
checks.push({
|
|
@@ -1217,16 +2076,16 @@ var CursorAdapter = class {
|
|
|
1217
2076
|
return checks;
|
|
1218
2077
|
}
|
|
1219
2078
|
};
|
|
1220
|
-
async function
|
|
2079
|
+
async function readExisting2(path) {
|
|
1221
2080
|
try {
|
|
1222
|
-
return await
|
|
2081
|
+
return await readFile13(path, "utf8");
|
|
1223
2082
|
} catch {
|
|
1224
2083
|
return null;
|
|
1225
2084
|
}
|
|
1226
2085
|
}
|
|
1227
|
-
async function
|
|
2086
|
+
async function exists4(path) {
|
|
1228
2087
|
try {
|
|
1229
|
-
await
|
|
2088
|
+
await stat6(path);
|
|
1230
2089
|
return true;
|
|
1231
2090
|
} catch {
|
|
1232
2091
|
return false;
|
|
@@ -1238,43 +2097,45 @@ import { execFile as execFile3 } from "child_process";
|
|
|
1238
2097
|
import { promisify as promisify3 } from "util";
|
|
1239
2098
|
|
|
1240
2099
|
// src/openspec/inspector.ts
|
|
1241
|
-
import { readdir, stat as
|
|
1242
|
-
import { join as
|
|
2100
|
+
import { readdir, stat as stat7 } from "fs/promises";
|
|
2101
|
+
import { join as join14 } from "path";
|
|
1243
2102
|
async function inspectOpenSpecProject(projectRoot) {
|
|
1244
|
-
const openspecPath =
|
|
1245
|
-
const changesPath =
|
|
1246
|
-
const
|
|
2103
|
+
const openspecPath = join14(projectRoot, "openspec");
|
|
2104
|
+
const changesPath = join14(openspecPath, "changes");
|
|
2105
|
+
const legacyArchivePath = join14(openspecPath, "archive");
|
|
2106
|
+
const changesArchivePath = join14(changesPath, "archive");
|
|
1247
2107
|
return {
|
|
1248
|
-
exists: await
|
|
1249
|
-
changes: await listDirectories(changesPath),
|
|
1250
|
-
archived: await listDirectories(
|
|
2108
|
+
exists: await exists5(openspecPath),
|
|
2109
|
+
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
2110
|
+
archived: [.../* @__PURE__ */ new Set([...await listDirectories(legacyArchivePath), ...await listDirectories(changesArchivePath)])]
|
|
1251
2111
|
};
|
|
1252
2112
|
}
|
|
1253
2113
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
1254
|
-
const changePath =
|
|
1255
|
-
const tasksPath =
|
|
1256
|
-
const specsPath =
|
|
2114
|
+
const changePath = join14(projectRoot, "openspec", "changes", changeId);
|
|
2115
|
+
const tasksPath = join14(changePath, "tasks.md");
|
|
2116
|
+
const specsPath = join14(changePath, "specs");
|
|
1257
2117
|
return {
|
|
1258
2118
|
changeId,
|
|
1259
|
-
exists: await
|
|
1260
|
-
hasProposal: await
|
|
1261
|
-
hasTasks: await
|
|
1262
|
-
hasSpecs: await
|
|
2119
|
+
exists: await exists5(changePath),
|
|
2120
|
+
hasProposal: await exists5(join14(changePath, "proposal.md")),
|
|
2121
|
+
hasTasks: await exists5(tasksPath),
|
|
2122
|
+
hasSpecs: await exists5(specsPath),
|
|
1263
2123
|
tasksPath,
|
|
1264
2124
|
changePath
|
|
1265
2125
|
};
|
|
1266
2126
|
}
|
|
1267
|
-
async function listDirectories(path) {
|
|
2127
|
+
async function listDirectories(path, options = {}) {
|
|
1268
2128
|
try {
|
|
1269
2129
|
const entries = await readdir(path, { withFileTypes: true });
|
|
1270
|
-
|
|
2130
|
+
const excluded = new Set(options.exclude ?? []);
|
|
2131
|
+
return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
|
|
1271
2132
|
} catch {
|
|
1272
2133
|
return [];
|
|
1273
2134
|
}
|
|
1274
2135
|
}
|
|
1275
|
-
async function
|
|
2136
|
+
async function exists5(path) {
|
|
1276
2137
|
try {
|
|
1277
|
-
await
|
|
2138
|
+
await stat7(path);
|
|
1278
2139
|
return true;
|
|
1279
2140
|
} catch {
|
|
1280
2141
|
return false;
|
|
@@ -1433,12 +2294,12 @@ function parseCommands(help) {
|
|
|
1433
2294
|
}
|
|
1434
2295
|
|
|
1435
2296
|
// src/scanner/package.ts
|
|
1436
|
-
import { readFile as
|
|
1437
|
-
import { join as
|
|
2297
|
+
import { readFile as readFile14, stat as stat8 } from "fs/promises";
|
|
2298
|
+
import { join as join15 } from "path";
|
|
1438
2299
|
import { parse as parse2 } from "yaml";
|
|
1439
2300
|
async function readPackageJson(projectRoot) {
|
|
1440
2301
|
try {
|
|
1441
|
-
return JSON.parse(await
|
|
2302
|
+
return JSON.parse(await readFile14(join15(projectRoot, "package.json"), "utf8"));
|
|
1442
2303
|
} catch {
|
|
1443
2304
|
return null;
|
|
1444
2305
|
}
|
|
@@ -1504,7 +2365,7 @@ function detectFramework(pkg) {
|
|
|
1504
2365
|
}
|
|
1505
2366
|
async function detectLanguage(projectRoot, pkg) {
|
|
1506
2367
|
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
1507
|
-
if (deps.typescript || await
|
|
2368
|
+
if (deps.typescript || await exists6(join15(projectRoot, "tsconfig.json"))) {
|
|
1508
2369
|
return "typescript";
|
|
1509
2370
|
}
|
|
1510
2371
|
return "javascript";
|
|
@@ -1519,7 +2380,7 @@ async function detectWorkspaces(projectRoot, pkg) {
|
|
|
1519
2380
|
return packageWorkspaces;
|
|
1520
2381
|
}
|
|
1521
2382
|
try {
|
|
1522
|
-
const workspace = parse2(await
|
|
2383
|
+
const workspace = parse2(await readFile14(join15(projectRoot, "pnpm-workspace.yaml"), "utf8"));
|
|
1523
2384
|
return (workspace?.packages ?? []).map((path) => ({
|
|
1524
2385
|
name: path,
|
|
1525
2386
|
path,
|
|
@@ -1539,7 +2400,7 @@ async function detectLockManagers(projectRoot) {
|
|
|
1539
2400
|
];
|
|
1540
2401
|
const found = [];
|
|
1541
2402
|
for (const [file, manager] of lockFiles) {
|
|
1542
|
-
if (await
|
|
2403
|
+
if (await exists6(join15(projectRoot, file))) {
|
|
1543
2404
|
found.push(manager);
|
|
1544
2405
|
}
|
|
1545
2406
|
}
|
|
@@ -1554,9 +2415,9 @@ function normalizeWorkspaces(workspaces) {
|
|
|
1554
2415
|
function scriptCommand(packageManager, name) {
|
|
1555
2416
|
return packageManager === "npm" ? `npm run ${name}` : `${packageManager} ${name}`;
|
|
1556
2417
|
}
|
|
1557
|
-
async function
|
|
2418
|
+
async function exists6(path) {
|
|
1558
2419
|
try {
|
|
1559
|
-
await
|
|
2420
|
+
await stat8(path);
|
|
1560
2421
|
return true;
|
|
1561
2422
|
} catch {
|
|
1562
2423
|
return false;
|
|
@@ -1564,14 +2425,14 @@ async function exists5(path) {
|
|
|
1564
2425
|
}
|
|
1565
2426
|
|
|
1566
2427
|
// src/scanner/routes.ts
|
|
1567
|
-
import { readdir as readdir2, stat as
|
|
1568
|
-
import { join as
|
|
2428
|
+
import { readdir as readdir2, stat as stat9 } from "fs/promises";
|
|
2429
|
+
import { join as join16, relative, sep } from "path";
|
|
1569
2430
|
async function scanRoutes(projectRoot) {
|
|
1570
2431
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
1571
2432
|
const routes = [];
|
|
1572
2433
|
for (const candidate of candidates) {
|
|
1573
|
-
const root =
|
|
1574
|
-
if (!await
|
|
2434
|
+
const root = join16(projectRoot, candidate);
|
|
2435
|
+
if (!await exists7(root)) {
|
|
1575
2436
|
continue;
|
|
1576
2437
|
}
|
|
1577
2438
|
for (const file of await listFiles(root)) {
|
|
@@ -1598,7 +2459,7 @@ async function listFiles(root) {
|
|
|
1598
2459
|
const entries = await readdir2(root, { withFileTypes: true });
|
|
1599
2460
|
const files = [];
|
|
1600
2461
|
for (const entry of entries) {
|
|
1601
|
-
const path =
|
|
2462
|
+
const path = join16(root, entry.name);
|
|
1602
2463
|
if (entry.isDirectory()) {
|
|
1603
2464
|
files.push(...await listFiles(path));
|
|
1604
2465
|
} else {
|
|
@@ -1607,9 +2468,9 @@ async function listFiles(root) {
|
|
|
1607
2468
|
}
|
|
1608
2469
|
return files;
|
|
1609
2470
|
}
|
|
1610
|
-
async function
|
|
2471
|
+
async function exists7(path) {
|
|
1611
2472
|
try {
|
|
1612
|
-
await
|
|
2473
|
+
await stat9(path);
|
|
1613
2474
|
return true;
|
|
1614
2475
|
} catch {
|
|
1615
2476
|
return false;
|
|
@@ -1741,7 +2602,7 @@ async function createCommandContext(command, options) {
|
|
|
1741
2602
|
stateStore: new StateStore(projectRoot, FET_VERSION, project),
|
|
1742
2603
|
openSpec: new DefaultOpenSpecAdapter(),
|
|
1743
2604
|
scanner: new ProjectScanner(),
|
|
1744
|
-
toolAdapters: [new CursorAdapter()]
|
|
2605
|
+
toolAdapters: [new CursorAdapter(), new CodexAdapter()]
|
|
1745
2606
|
};
|
|
1746
2607
|
}
|
|
1747
2608
|
|
|
@@ -1750,6 +2611,7 @@ var program = new Command();
|
|
|
1750
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");
|
|
1751
2612
|
addGlobalOptions(program.command("init")).description("\u521D\u59CB\u5316 FET + OpenSpec").action(wrap("init", initCommand));
|
|
1752
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));
|
|
1753
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(
|
|
1754
2616
|
wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
|
|
1755
2617
|
);
|