@nick848/fet 1.0.0 → 1.0.2
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 +207 -40
- package/dist/cli/index.js +716 -72
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { Command } from "commander";
|
|
|
9
9
|
|
|
10
10
|
// src/commands/init.ts
|
|
11
11
|
import { readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
12
|
-
import { join as
|
|
12
|
+
import { join as join6 } from "path";
|
|
13
13
|
|
|
14
14
|
// src/fs/atomic-write.ts
|
|
15
15
|
import { dirname } from "path";
|
|
@@ -116,7 +116,28 @@ async function writeInitJournal(projectRoot, journal) {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// src/version.ts
|
|
119
|
-
|
|
119
|
+
import { existsSync, readFileSync } from "fs";
|
|
120
|
+
import { dirname as dirname4, join as join4, parse } from "path";
|
|
121
|
+
import { fileURLToPath } from "url";
|
|
122
|
+
var FET_VERSION = readPackageVersion();
|
|
123
|
+
function readPackageVersion() {
|
|
124
|
+
let currentDir = dirname4(fileURLToPath(import.meta.url));
|
|
125
|
+
const root = parse(currentDir).root;
|
|
126
|
+
while (true) {
|
|
127
|
+
const packageJsonPath = join4(currentDir, "package.json");
|
|
128
|
+
if (existsSync(packageJsonPath)) {
|
|
129
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
130
|
+
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
131
|
+
return packageJson.version;
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`package.json \u7F3A\u5C11\u6709\u6548\u7684 version \u5B57\u6BB5: ${packageJsonPath}`);
|
|
134
|
+
}
|
|
135
|
+
if (currentDir === root) {
|
|
136
|
+
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D FET package.json");
|
|
137
|
+
}
|
|
138
|
+
currentDir = dirname4(currentDir);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
120
141
|
|
|
121
142
|
// src/templates/markers.ts
|
|
122
143
|
var AUTO_BEGIN = "<!-- FET:BEGIN AUTO -->";
|
|
@@ -161,7 +182,7 @@ function count(content, needle) {
|
|
|
161
182
|
|
|
162
183
|
// src/templates/agents-md.ts
|
|
163
184
|
function renderAgentsMd(scan) {
|
|
164
|
-
const
|
|
185
|
+
const commands = Object.entries(scan.commands).map(([name, command]) => `| ${name} | \`${command.command}\` | ${command.source} |`).join("\n");
|
|
165
186
|
const routes = scan.routes.map((route) => `| ${route.path} | ${route.source} | ${route.confidence}${route.inferred ? " inferred" : ""} |`).join("\n");
|
|
166
187
|
const workspaces = scan.project.workspaces.map((workspace) => `| ${workspace.name} | ${workspace.path} | ${workspace.source} |`).join("\n");
|
|
167
188
|
return `# Project Context
|
|
@@ -185,7 +206,7 @@ ${workspaces || "| root | . | inferred |"}
|
|
|
185
206
|
|
|
186
207
|
| Name | Command | Source |
|
|
187
208
|
|------|---------|--------|
|
|
188
|
-
${
|
|
209
|
+
${commands || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | [NEEDS LLM INPUT] |"}
|
|
189
210
|
|
|
190
211
|
## Structure
|
|
191
212
|
|
|
@@ -305,7 +326,7 @@ ${block}
|
|
|
305
326
|
|
|
306
327
|
// src/commands/update-context.ts
|
|
307
328
|
import { readFile as readFile4 } from "fs/promises";
|
|
308
|
-
import { join as
|
|
329
|
+
import { join as join5 } from "path";
|
|
309
330
|
|
|
310
331
|
// src/config/yaml.ts
|
|
311
332
|
import { readFile as readFile3 } from "fs/promises";
|
|
@@ -339,8 +360,8 @@ async function updateContextCommand(ctx) {
|
|
|
339
360
|
}
|
|
340
361
|
async function updateContextFiles(ctx) {
|
|
341
362
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
342
|
-
const agentsPath =
|
|
343
|
-
const configPath =
|
|
363
|
+
const agentsPath = join5(ctx.projectRoot, "AGENTS.md");
|
|
364
|
+
const configPath = join5(ctx.projectRoot, "openspec", "config.yaml");
|
|
344
365
|
const existingAgents = await readOptional(agentsPath);
|
|
345
366
|
const warnings = [...scan.warnings];
|
|
346
367
|
if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
|
|
@@ -357,7 +378,7 @@ async function updateContextFiles(ctx) {
|
|
|
357
378
|
code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
|
|
358
379
|
message: "AGENTS.md \u5DF2\u5B58\u5728\u4E14\u4E0D\u5305\u542B FET \u6258\u7BA1\u533A\u57DF",
|
|
359
380
|
details: { path: "AGENTS.md" },
|
|
360
|
-
suggestedCommand: "\u786E\u8BA4\u53EF\u5907\u4EFD\u5E76\u66FF\u6362\u540E\u8FD0\u884C fet update-context --yes"
|
|
381
|
+
suggestedCommand: ctx.command === "init" ? "\u786E\u8BA4\u53EF\u5907\u4EFD\u5E76\u66FF\u6362\u540E\u8FD0\u884C fet init --yes" : "\u786E\u8BA4\u53EF\u5907\u4EFD\u5E76\u66FF\u6362\u540E\u8FD0\u884C fet update-context --yes"
|
|
361
382
|
});
|
|
362
383
|
}
|
|
363
384
|
const backupPath = await createBackup(agentsPath);
|
|
@@ -386,7 +407,7 @@ async function readOptional(path) {
|
|
|
386
407
|
|
|
387
408
|
// src/commands/init.ts
|
|
388
409
|
async function initCommand(ctx) {
|
|
389
|
-
const alreadyInitialized = await exists(
|
|
410
|
+
const alreadyInitialized = await exists(join6(ctx.projectRoot, "openspec", "config.yaml"));
|
|
390
411
|
await withProjectLock(
|
|
391
412
|
ctx.projectRoot,
|
|
392
413
|
{ command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
|
|
@@ -431,7 +452,7 @@ async function initCommand(ctx) {
|
|
|
431
452
|
});
|
|
432
453
|
}
|
|
433
454
|
async function ensureGitignore(ctx) {
|
|
434
|
-
const gitignorePath =
|
|
455
|
+
const gitignorePath = join6(ctx.projectRoot, ".gitignore");
|
|
435
456
|
const existing = await readOptional2(gitignorePath);
|
|
436
457
|
await atomicWrite(gitignorePath, mergeGitignore(existing));
|
|
437
458
|
}
|
|
@@ -453,17 +474,17 @@ async function exists(path) {
|
|
|
453
474
|
|
|
454
475
|
// src/commands/doctor.ts
|
|
455
476
|
import { stat as stat3 } from "fs/promises";
|
|
456
|
-
import { join as
|
|
477
|
+
import { join as join7 } from "path";
|
|
457
478
|
async function doctorCommand(ctx, options = {}) {
|
|
458
479
|
const checks = [];
|
|
459
480
|
checks.push(await checkOpenSpec(ctx));
|
|
460
481
|
checks.push(await checkState(ctx));
|
|
461
|
-
checks.push(await checkFile("agents",
|
|
462
|
-
checks.push(await checkFile("config",
|
|
482
|
+
checks.push(await checkFile("agents", join7(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
|
|
483
|
+
checks.push(await checkFile("config", join7(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
|
|
463
484
|
for (const adapter of ctx.toolAdapters) {
|
|
464
485
|
checks.push(...await adapter.doctor(ctx.projectRoot));
|
|
465
486
|
}
|
|
466
|
-
const lockPath =
|
|
487
|
+
const lockPath = join7(ctx.projectRoot, "openspec", ".fet.lock");
|
|
467
488
|
if (await exists2(lockPath)) {
|
|
468
489
|
if (options.fixLock) {
|
|
469
490
|
await clearLock(ctx.projectRoot);
|
|
@@ -537,7 +558,7 @@ async function git(cwd, args) {
|
|
|
537
558
|
|
|
538
559
|
// src/state/store.ts
|
|
539
560
|
import { mkdir as mkdir3, readFile as readFile6 } from "fs/promises";
|
|
540
|
-
import { join as
|
|
561
|
+
import { join as join8 } from "path";
|
|
541
562
|
|
|
542
563
|
// src/state/schema.ts
|
|
543
564
|
var phases = ["explore", "propose", "implement", "verify", "sync", "archive"];
|
|
@@ -645,7 +666,7 @@ var StateStore = class {
|
|
|
645
666
|
}
|
|
646
667
|
async writeGlobal(state) {
|
|
647
668
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
648
|
-
await mkdir3(
|
|
669
|
+
await mkdir3(join8(this.projectRoot, "openspec"), { recursive: true });
|
|
649
670
|
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
650
671
|
`);
|
|
651
672
|
}
|
|
@@ -666,15 +687,15 @@ var StateStore = class {
|
|
|
666
687
|
}
|
|
667
688
|
async writeChange(state) {
|
|
668
689
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
669
|
-
await mkdir3(
|
|
690
|
+
await mkdir3(join8(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
670
691
|
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
671
692
|
`);
|
|
672
693
|
}
|
|
673
694
|
globalPath() {
|
|
674
|
-
return
|
|
695
|
+
return join8(this.projectRoot, "openspec", "fet-state.json");
|
|
675
696
|
}
|
|
676
697
|
changePath(changeId) {
|
|
677
|
-
return
|
|
698
|
+
return join8(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
678
699
|
}
|
|
679
700
|
};
|
|
680
701
|
function isNotFound(error) {
|
|
@@ -886,7 +907,7 @@ async function assertVerified(ctx) {
|
|
|
886
907
|
// src/commands/verify.ts
|
|
887
908
|
import { createHash } from "crypto";
|
|
888
909
|
import { mkdir as mkdir4, readFile as readFile8, stat as stat4 } from "fs/promises";
|
|
889
|
-
import { join as
|
|
910
|
+
import { join as join9 } from "path";
|
|
890
911
|
async function verifyCommand(ctx, options) {
|
|
891
912
|
if (options.auto) {
|
|
892
913
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
@@ -953,8 +974,8 @@ async function verifyCommand(ctx, options) {
|
|
|
953
974
|
async function writeInstructions(ctx, changeId) {
|
|
954
975
|
await assertChangeExists(ctx, changeId);
|
|
955
976
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
956
|
-
const dir =
|
|
957
|
-
const instructionsPath =
|
|
977
|
+
const dir = join9(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
978
|
+
const instructionsPath = join9(dir, "verify-instructions.md");
|
|
958
979
|
await mkdir4(dir, { recursive: true });
|
|
959
980
|
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
960
981
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -971,7 +992,7 @@ async function writeInstructions(ctx, changeId) {
|
|
|
971
992
|
async function markDone(ctx, changeId) {
|
|
972
993
|
await assertChangeExists(ctx, changeId);
|
|
973
994
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
974
|
-
const instructionsPath =
|
|
995
|
+
const instructionsPath = join9(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
975
996
|
const instructions = await readInstructions(instructionsPath, changeId);
|
|
976
997
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
977
998
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -1060,12 +1081,13 @@ async function resolveChangeId(ctx) {
|
|
|
1060
1081
|
// src/cli/context.ts
|
|
1061
1082
|
import { resolve } from "path";
|
|
1062
1083
|
|
|
1063
|
-
// src/adapters/
|
|
1084
|
+
// src/adapters/codex/index.ts
|
|
1064
1085
|
import { mkdir as mkdir5, readFile as readFile9, stat as stat5 } from "fs/promises";
|
|
1065
|
-
import {
|
|
1086
|
+
import { homedir } from "os";
|
|
1087
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
1066
1088
|
|
|
1067
|
-
// src/adapters/
|
|
1068
|
-
var
|
|
1089
|
+
// src/adapters/commands.ts
|
|
1090
|
+
var FET_WORKFLOW_COMMANDS = [
|
|
1069
1091
|
"explore",
|
|
1070
1092
|
"propose",
|
|
1071
1093
|
"new",
|
|
@@ -1078,8 +1100,629 @@ var commands = [
|
|
|
1078
1100
|
"bulk-archive",
|
|
1079
1101
|
"onboard"
|
|
1080
1102
|
];
|
|
1103
|
+
var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "passthrough"];
|
|
1104
|
+
|
|
1105
|
+
// src/adapters/codex/templates.ts
|
|
1106
|
+
function codexGuideFile() {
|
|
1107
|
+
return {
|
|
1108
|
+
path: ".codex/fet/context.md",
|
|
1109
|
+
content: `<!-- FET:MANAGED
|
|
1110
|
+
schemaVersion: 1
|
|
1111
|
+
fetVersion: ${FET_VERSION}
|
|
1112
|
+
generator: codex-adapter
|
|
1113
|
+
adapterVersion: 1
|
|
1114
|
+
FET:END -->
|
|
1115
|
+
|
|
1116
|
+
# FET For Codex
|
|
1117
|
+
|
|
1118
|
+
Before doing FET or OpenSpec work in Codex, read:
|
|
1119
|
+
|
|
1120
|
+
- AGENTS.md
|
|
1121
|
+
- openspec/config.yaml
|
|
1122
|
+
- the active change files under openspec/changes/<change-id>/, when a change is selected
|
|
1123
|
+
|
|
1124
|
+
Use the terminal command \`fet <command>\` as the source of truth for workflow transitions. These files are Codex-readable guidance; they do not register native slash commands.
|
|
1125
|
+
|
|
1126
|
+
Command guides live in .codex/fet/commands/.
|
|
1127
|
+
`
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
function codexCommandFiles() {
|
|
1131
|
+
return FET_ADAPTER_COMMANDS.map((command) => ({
|
|
1132
|
+
path: `.codex/fet/commands/${command}.md`,
|
|
1133
|
+
content: renderCommand(command)
|
|
1134
|
+
}));
|
|
1135
|
+
}
|
|
1136
|
+
function codexSlashPromptFiles() {
|
|
1137
|
+
return FET_ADAPTER_COMMANDS.map((command) => ({
|
|
1138
|
+
path: `prompts/fet-${command}.md`,
|
|
1139
|
+
content: renderSlashPrompt(command)
|
|
1140
|
+
}));
|
|
1141
|
+
}
|
|
1142
|
+
function renderCommand(command) {
|
|
1143
|
+
if (command === "passthrough") {
|
|
1144
|
+
return renderPassthroughCommand();
|
|
1145
|
+
}
|
|
1146
|
+
return `<!-- FET:MANAGED
|
|
1147
|
+
schemaVersion: 1
|
|
1148
|
+
fetVersion: ${FET_VERSION}
|
|
1149
|
+
generator: codex-adapter
|
|
1150
|
+
adapterVersion: 1
|
|
1151
|
+
command: fet ${command}
|
|
1152
|
+
FET:END -->
|
|
1153
|
+
|
|
1154
|
+
# fet ${command}
|
|
1155
|
+
|
|
1156
|
+
When the user asks Codex to run the FET ${command} workflow, first make sure the project context is loaded from AGENTS.md and openspec/config.yaml.
|
|
1157
|
+
|
|
1158
|
+
Then run:
|
|
1159
|
+
|
|
1160
|
+
\`\`\`sh
|
|
1161
|
+
fet ${command}
|
|
1162
|
+
\`\`\`
|
|
1163
|
+
|
|
1164
|
+
If the command needs a change id, pass it with \`--change <change-id>\` or use the active OpenSpec change from the user's request.
|
|
1165
|
+
|
|
1166
|
+
After the command completes, report the important next steps from the FET output and keep any generated OpenSpec artifacts in the normal project workflow.
|
|
1167
|
+
`;
|
|
1168
|
+
}
|
|
1169
|
+
function renderPassthroughCommand() {
|
|
1170
|
+
return `<!-- FET:MANAGED
|
|
1171
|
+
schemaVersion: 1
|
|
1172
|
+
fetVersion: ${FET_VERSION}
|
|
1173
|
+
generator: codex-adapter
|
|
1174
|
+
adapterVersion: 1
|
|
1175
|
+
command: fet passthrough <openspec-command>
|
|
1176
|
+
FET:END -->
|
|
1177
|
+
|
|
1178
|
+
# fet passthrough
|
|
1179
|
+
|
|
1180
|
+
When the user asks Codex to run an OpenSpec command that FET does not manage as a first-class workflow command, use FET passthrough instead of calling OpenSpec directly.
|
|
1181
|
+
|
|
1182
|
+
Then run:
|
|
1183
|
+
|
|
1184
|
+
\`\`\`sh
|
|
1185
|
+
fet passthrough <openspec-command> [...args]
|
|
1186
|
+
\`\`\`
|
|
1187
|
+
|
|
1188
|
+
This preserves the FET entry point while allowing access to unmanaged or newly added OpenSpec commands. Passthrough does not update FET lifecycle state.
|
|
1189
|
+
`;
|
|
1190
|
+
}
|
|
1191
|
+
function renderSlashPrompt(command) {
|
|
1192
|
+
if (command === "continue") {
|
|
1193
|
+
return renderContinueSlashPrompt();
|
|
1194
|
+
}
|
|
1195
|
+
if (command === "ff" || command === "propose") {
|
|
1196
|
+
return renderFastForwardSlashPrompt(command);
|
|
1197
|
+
}
|
|
1198
|
+
if (command === "explore") {
|
|
1199
|
+
return renderExploreSlashPrompt();
|
|
1200
|
+
}
|
|
1201
|
+
if (command === "new") {
|
|
1202
|
+
return renderNewSlashPrompt();
|
|
1203
|
+
}
|
|
1204
|
+
if (command === "apply") {
|
|
1205
|
+
return renderApplySlashPrompt();
|
|
1206
|
+
}
|
|
1207
|
+
if (command === "verify") {
|
|
1208
|
+
return renderVerifySlashPrompt();
|
|
1209
|
+
}
|
|
1210
|
+
if (command === "sync") {
|
|
1211
|
+
return renderSyncSlashPrompt();
|
|
1212
|
+
}
|
|
1213
|
+
if (command === "archive") {
|
|
1214
|
+
return renderArchiveSlashPrompt();
|
|
1215
|
+
}
|
|
1216
|
+
if (command === "bulk-archive") {
|
|
1217
|
+
return renderBulkArchiveSlashPrompt();
|
|
1218
|
+
}
|
|
1219
|
+
if (command === "onboard") {
|
|
1220
|
+
return renderOnboardSlashPrompt();
|
|
1221
|
+
}
|
|
1222
|
+
if (command === "passthrough") {
|
|
1223
|
+
return renderPassthroughSlashPrompt();
|
|
1224
|
+
}
|
|
1225
|
+
const usage = command === "passthrough" ? "fet passthrough <openspec-command> [...args]" : `fet ${command} [...args]`;
|
|
1226
|
+
const shellCommand = command === "passthrough" ? "fet passthrough $ARGUMENTS" : `fet ${command} $ARGUMENTS`;
|
|
1227
|
+
const description = command === "passthrough" ? "Run an unmanaged OpenSpec command through FET passthrough" : `Run the FET-managed OpenSpec ${command} workflow`;
|
|
1228
|
+
return `<!-- FET:MANAGED
|
|
1229
|
+
schemaVersion: 1
|
|
1230
|
+
fetVersion: ${FET_VERSION}
|
|
1231
|
+
generator: codex-adapter
|
|
1232
|
+
adapterVersion: 1
|
|
1233
|
+
command: ${usage}
|
|
1234
|
+
FET:END -->
|
|
1235
|
+
|
|
1236
|
+
---
|
|
1237
|
+
description: ${description}
|
|
1238
|
+
argument-hint: command arguments
|
|
1239
|
+
---
|
|
1240
|
+
|
|
1241
|
+
Use FET as the entry point for this OpenSpec workflow.
|
|
1242
|
+
|
|
1243
|
+
Before running the command, make sure the relevant project context is loaded from AGENTS.md and openspec/config.yaml. If a change id is needed and was not provided, infer it from the active FET/OpenSpec state when unambiguous; otherwise ask the user for the change id.
|
|
1244
|
+
|
|
1245
|
+
Run:
|
|
1246
|
+
|
|
1247
|
+
\`\`\`sh
|
|
1248
|
+
${shellCommand}
|
|
1249
|
+
\`\`\`
|
|
1250
|
+
|
|
1251
|
+
After it completes, summarize the important FET output and next steps.
|
|
1252
|
+
`;
|
|
1253
|
+
}
|
|
1254
|
+
function renderNewSlashPrompt() {
|
|
1255
|
+
return renderManagedSlashPrompt(
|
|
1256
|
+
"fet new [...args]",
|
|
1257
|
+
"Create a new FET/OpenSpec change scaffold",
|
|
1258
|
+
`Create a new FET-managed OpenSpec change scaffold.
|
|
1259
|
+
|
|
1260
|
+
Input after the slash command should be a kebab-case change id or a short description.
|
|
1261
|
+
|
|
1262
|
+
Steps:
|
|
1263
|
+
|
|
1264
|
+
1. Load project context from AGENTS.md and openspec/config.yaml.
|
|
1265
|
+
2. If no input was provided, ask what change the user wants to build or fix.
|
|
1266
|
+
3. Derive a kebab-case change id when the user provided a description.
|
|
1267
|
+
4. Create the change through FET:
|
|
1268
|
+
\`\`\`sh
|
|
1269
|
+
fet new <change-id>
|
|
1270
|
+
\`\`\`
|
|
1271
|
+
5. Show current artifact status:
|
|
1272
|
+
\`\`\`sh
|
|
1273
|
+
fet passthrough status --change <change-id>
|
|
1274
|
+
\`\`\`
|
|
1275
|
+
6. Get the first ready artifact instructions:
|
|
1276
|
+
\`\`\`sh
|
|
1277
|
+
fet continue <first-ready-artifact-id> --change <change-id>
|
|
1278
|
+
\`\`\`
|
|
1279
|
+
|
|
1280
|
+
Guardrails:
|
|
1281
|
+
- Do not create artifact files in /prompts:fet-new.
|
|
1282
|
+
- If the change already exists, suggest /prompts:fet-continue <change-id>.
|
|
1283
|
+
- Show the change location and the next command to create the first artifact.`
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
function renderApplySlashPrompt() {
|
|
1287
|
+
return renderManagedSlashPrompt(
|
|
1288
|
+
"fet apply [...args]",
|
|
1289
|
+
"Implement tasks from a FET/OpenSpec change",
|
|
1290
|
+
`Implement a FET-managed OpenSpec change.
|
|
1291
|
+
|
|
1292
|
+
Input after the slash command should identify the change, for example a change id or --change <change-id>.
|
|
1293
|
+
|
|
1294
|
+
Steps:
|
|
1295
|
+
|
|
1296
|
+
1. Resolve the change id. If ambiguous, ask the user.
|
|
1297
|
+
2. Get FET-managed apply instructions:
|
|
1298
|
+
\`\`\`sh
|
|
1299
|
+
fet apply --change <change-id> --json
|
|
1300
|
+
\`\`\`
|
|
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>/.
|
|
1302
|
+
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
|
+
5. Implement pending tasks one by one:
|
|
1304
|
+
- Keep code changes minimal and scoped to the task.
|
|
1305
|
+
- Follow proposal, specs, design, and tasks.
|
|
1306
|
+
- Mark each completed task checkbox in tasks.md from \`- [ ]\` to \`- [x]\`.
|
|
1307
|
+
- Pause and ask if a task is ambiguous or reveals a design conflict.
|
|
1308
|
+
6. After completing or pausing, summarize completed tasks, remaining tasks, and blockers.
|
|
1309
|
+
|
|
1310
|
+
Guardrails:
|
|
1311
|
+
- Never skip reading OpenSpec artifacts before implementation.
|
|
1312
|
+
- Do not mark a task complete until the code change is actually done.
|
|
1313
|
+
- Do not run sync or archive from apply.`
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
function renderVerifySlashPrompt() {
|
|
1317
|
+
return renderManagedSlashPrompt(
|
|
1318
|
+
"fet verify [...args]",
|
|
1319
|
+
"Verify a FET/OpenSpec change before sync or archive",
|
|
1320
|
+
`Verify a FET-managed OpenSpec change.
|
|
1321
|
+
|
|
1322
|
+
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.
|
|
1323
|
+
|
|
1324
|
+
Steps:
|
|
1325
|
+
|
|
1326
|
+
1. Resolve the change id. If ambiguous, ask the user.
|
|
1327
|
+
2. Generate FET verification instructions:
|
|
1328
|
+
\`\`\`sh
|
|
1329
|
+
fet verify --change <change-id>
|
|
1330
|
+
\`\`\`
|
|
1331
|
+
3. Read openspec/changes/<change-id>/.fet/verify-instructions.md.
|
|
1332
|
+
4. Read available artifacts for the change: proposal.md, design.md, tasks.md, and delta specs.
|
|
1333
|
+
5. Verify:
|
|
1334
|
+
- Completeness: tasks and required artifacts are present and checked off where appropriate.
|
|
1335
|
+
- Correctness: implementation evidence matches specs and proposal.
|
|
1336
|
+
- Coherence: code follows design decisions and project patterns.
|
|
1337
|
+
6. Report critical issues, warnings, and suggestions with file references.
|
|
1338
|
+
7. If there are no critical issues, or the user explicitly accepts the remaining risk, mark FET verification done:
|
|
1339
|
+
\`\`\`sh
|
|
1340
|
+
fet verify --done --change <change-id>
|
|
1341
|
+
\`\`\`
|
|
1342
|
+
|
|
1343
|
+
Guardrails:
|
|
1344
|
+
- Do not run --done before producing a verification assessment.
|
|
1345
|
+
- Treat incomplete tasks or missing required behavior as critical unless user explicitly accepts them.
|
|
1346
|
+
- Suggest /prompts:fet-sync <change-id> and /prompts:fet-archive <change-id> only after verification is done.`
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
function renderSyncSlashPrompt() {
|
|
1350
|
+
return renderManagedSlashPrompt(
|
|
1351
|
+
"fet sync [...args]",
|
|
1352
|
+
"Sync delta specs and validate a FET/OpenSpec change",
|
|
1353
|
+
`Sync a FET-managed OpenSpec change.
|
|
1354
|
+
|
|
1355
|
+
Input after the slash command should identify the change.
|
|
1356
|
+
|
|
1357
|
+
Steps:
|
|
1358
|
+
|
|
1359
|
+
1. Resolve the change id. If ambiguous, ask the user.
|
|
1360
|
+
2. Confirm FET verification is complete or run /prompts:fet-verify <change-id> first.
|
|
1361
|
+
3. Find delta specs under openspec/changes/<change-id>/specs/*/spec.md.
|
|
1362
|
+
4. If delta specs exist, intelligently merge them into openspec/specs/<capability>/spec.md:
|
|
1363
|
+
- Add ADDED requirements.
|
|
1364
|
+
- Apply MODIFIED requirements without deleting unrelated existing scenarios.
|
|
1365
|
+
- Remove REMOVED requirements.
|
|
1366
|
+
- Apply RENAMED requirements.
|
|
1367
|
+
- Preserve main-spec content not mentioned in the delta.
|
|
1368
|
+
5. If no delta specs exist, state that there is nothing to merge.
|
|
1369
|
+
6. Run the FET sync gate and strict OpenSpec validation:
|
|
1370
|
+
\`\`\`sh
|
|
1371
|
+
fet sync --change <change-id>
|
|
1372
|
+
\`\`\`
|
|
1373
|
+
7. Summarize updated capabilities and validation result.
|
|
1374
|
+
|
|
1375
|
+
Guardrails:
|
|
1376
|
+
- Read both delta and main specs before editing.
|
|
1377
|
+
- Make sync idempotent where possible.
|
|
1378
|
+
- If FET reports verify is not done, stop and run/ask for verification instead of bypassing the gate.`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
function renderArchiveSlashPrompt() {
|
|
1382
|
+
return renderManagedSlashPrompt(
|
|
1383
|
+
"fet archive [...args]",
|
|
1384
|
+
"Archive a verified FET/OpenSpec change",
|
|
1385
|
+
`Archive a FET-managed OpenSpec change.
|
|
1386
|
+
|
|
1387
|
+
Input after the slash command should identify the change.
|
|
1388
|
+
|
|
1389
|
+
Steps:
|
|
1390
|
+
|
|
1391
|
+
1. Resolve the change id. If ambiguous, ask the user.
|
|
1392
|
+
2. Check artifact and task status:
|
|
1393
|
+
\`\`\`sh
|
|
1394
|
+
fet passthrough status --change <change-id> --json
|
|
1395
|
+
\`\`\`
|
|
1396
|
+
3. If tasks or required artifacts are incomplete, show the warning and ask whether to continue.
|
|
1397
|
+
4. If delta specs exist, assess whether they have been synced. If not, recommend /prompts:fet-sync <change-id> before archiving.
|
|
1398
|
+
5. Confirm FET verification is complete. If not, run or suggest /prompts:fet-verify <change-id>.
|
|
1399
|
+
6. Archive through FET:
|
|
1400
|
+
\`\`\`sh
|
|
1401
|
+
fet archive --change <change-id>
|
|
1402
|
+
\`\`\`
|
|
1403
|
+
7. Report archive result, sync status, and any warnings.
|
|
1404
|
+
|
|
1405
|
+
Guardrails:
|
|
1406
|
+
- Do not move change directories manually; use fet archive.
|
|
1407
|
+
- Do not bypass the FET verify gate.
|
|
1408
|
+
- Ask before archiving with incomplete tasks or unsynced delta specs.`
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
function renderBulkArchiveSlashPrompt() {
|
|
1412
|
+
return renderManagedSlashPrompt(
|
|
1413
|
+
"fet bulk-archive [...args]",
|
|
1414
|
+
"Archive multiple FET/OpenSpec changes safely",
|
|
1415
|
+
`Bulk archive FET-managed OpenSpec changes.
|
|
1416
|
+
|
|
1417
|
+
Steps:
|
|
1418
|
+
|
|
1419
|
+
1. List active changes:
|
|
1420
|
+
\`\`\`sh
|
|
1421
|
+
fet passthrough status --json
|
|
1422
|
+
\`\`\`
|
|
1423
|
+
2. Ask the user which changes to archive. Do not auto-select.
|
|
1424
|
+
3. For each selected change:
|
|
1425
|
+
- Check artifact/task status.
|
|
1426
|
+
- Confirm verification is done.
|
|
1427
|
+
- Recommend sync if delta specs are unsynced.
|
|
1428
|
+
- Run \`fet archive --change <change-id>\`.
|
|
1429
|
+
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>\`.
|
|
1430
|
+
5. Summarize successes, skipped changes, and failures.
|
|
1431
|
+
|
|
1432
|
+
Guardrails:
|
|
1433
|
+
- Never archive all changes without explicit user selection.
|
|
1434
|
+
- Do not bypass verify or warnings for individual changes.
|
|
1435
|
+
- Continue with remaining selected changes if one archive fails, then report the failure clearly.`
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
function renderOnboardSlashPrompt() {
|
|
1439
|
+
return renderManagedSlashPrompt(
|
|
1440
|
+
"fet onboard [...args]",
|
|
1441
|
+
"Load FET/OpenSpec onboarding context",
|
|
1442
|
+
`Onboard the user or Codex into the FET/OpenSpec workflow for this project.
|
|
1443
|
+
|
|
1444
|
+
Steps:
|
|
1445
|
+
|
|
1446
|
+
1. Read AGENTS.md and openspec/config.yaml.
|
|
1447
|
+
2. Run FET onboarding:
|
|
1448
|
+
\`\`\`sh
|
|
1449
|
+
fet onboard $ARGUMENTS
|
|
1450
|
+
\`\`\`
|
|
1451
|
+
3. Summarize:
|
|
1452
|
+
- Project context.
|
|
1453
|
+
- Available active changes.
|
|
1454
|
+
- Core commands: new, continue, ff, apply, verify, sync, archive.
|
|
1455
|
+
- Where Codex prompts live.
|
|
1456
|
+
|
|
1457
|
+
Guardrails:
|
|
1458
|
+
- Do not create or modify artifacts during onboard.
|
|
1459
|
+
- Use this command to orient the session, then suggest the next concrete FET command.`
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
function renderPassthroughSlashPrompt() {
|
|
1463
|
+
return renderManagedSlashPrompt(
|
|
1464
|
+
"fet passthrough <openspec-command> [...args]",
|
|
1465
|
+
"Run an unmanaged OpenSpec command through FET",
|
|
1466
|
+
`Run an OpenSpec command that FET does not manage as a first-class workflow command.
|
|
1467
|
+
|
|
1468
|
+
Steps:
|
|
1469
|
+
|
|
1470
|
+
1. Identify the OpenSpec command and arguments from the user's input.
|
|
1471
|
+
2. Run it through FET:
|
|
1472
|
+
\`\`\`sh
|
|
1473
|
+
fet passthrough <openspec-command> [...args]
|
|
1474
|
+
\`\`\`
|
|
1475
|
+
3. Report the output and whether FET lifecycle state was updated.
|
|
1476
|
+
|
|
1477
|
+
Guardrails:
|
|
1478
|
+
- Do not call openspec directly unless FET passthrough itself is unavailable.
|
|
1479
|
+
- Remember that passthrough does not update FET lifecycle state.
|
|
1480
|
+
- For managed workflows, prefer the specific FET prompt instead of passthrough.`
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
function renderExploreSlashPrompt() {
|
|
1484
|
+
return renderManagedSlashPrompt(
|
|
1485
|
+
"fet explore [...args]",
|
|
1486
|
+
"Explore requirements for a FET/OpenSpec change",
|
|
1487
|
+
`Enter exploration mode for a FET-managed OpenSpec change.
|
|
1488
|
+
|
|
1489
|
+
Use this command for thinking and proposal shaping, not application implementation.
|
|
1490
|
+
|
|
1491
|
+
Input after the slash command may be a change id, a feature idea, or a problem description.
|
|
1492
|
+
|
|
1493
|
+
Steps:
|
|
1494
|
+
|
|
1495
|
+
1. Load project context from AGENTS.md and openspec/config.yaml.
|
|
1496
|
+
2. Check existing changes:
|
|
1497
|
+
\`\`\`sh
|
|
1498
|
+
fet passthrough status --json
|
|
1499
|
+
\`\`\`
|
|
1500
|
+
3. If the input names an existing change, read its current artifacts under openspec/changes/<change-id>/.
|
|
1501
|
+
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:
|
|
1502
|
+
\`\`\`sh
|
|
1503
|
+
fet new <change-id>
|
|
1504
|
+
\`\`\`
|
|
1505
|
+
5. Get proposal instructions through FET:
|
|
1506
|
+
\`\`\`sh
|
|
1507
|
+
fet explore --change <change-id>
|
|
1508
|
+
\`\`\`
|
|
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.
|
|
1510
|
+
|
|
1511
|
+
Guardrails:
|
|
1512
|
+
- Do not write application code in explore mode.
|
|
1513
|
+
- Ask a clarifying question if the proposal would otherwise be mostly guesswork.
|
|
1514
|
+
- Creating or updating OpenSpec artifacts is allowed when the user asks to capture the thinking.
|
|
1515
|
+
- After creating proposal.md, show the path and suggest /prompts:fet-continue <change-id> for the next artifact.`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
function renderContinueSlashPrompt() {
|
|
1519
|
+
return renderManagedSlashPrompt(
|
|
1520
|
+
"fet continue [...args]",
|
|
1521
|
+
"Create the next FET/OpenSpec artifact",
|
|
1522
|
+
`Continue a FET-managed OpenSpec change by creating exactly one ready artifact.
|
|
1523
|
+
|
|
1524
|
+
Input after the slash command should be a change id, optionally followed by an artifact id.
|
|
1525
|
+
|
|
1526
|
+
Steps:
|
|
1527
|
+
|
|
1528
|
+
1. Load project context from AGENTS.md and openspec/config.yaml.
|
|
1529
|
+
2. Resolve the change id. If it is missing and cannot be inferred unambiguously, ask the user.
|
|
1530
|
+
3. Check status:
|
|
1531
|
+
\`\`\`sh
|
|
1532
|
+
fet passthrough status --change <change-id> --json
|
|
1533
|
+
\`\`\`
|
|
1534
|
+
4. Pick the first artifact whose status is ready, unless the user specified an artifact id.
|
|
1535
|
+
5. Get FET-managed instructions:
|
|
1536
|
+
\`\`\`sh
|
|
1537
|
+
fet continue <artifact-id> --change <change-id> --json
|
|
1538
|
+
\`\`\`
|
|
1539
|
+
6. Parse the instructions output. Use its template, instruction, dependencies, and outputPath.
|
|
1540
|
+
7. Read dependency files before writing.
|
|
1541
|
+
8. Create the artifact file at outputPath. Do not copy context/rules wrapper text into the artifact; use those fields only as constraints.
|
|
1542
|
+
9. Verify the file exists, then run:
|
|
1543
|
+
\`\`\`sh
|
|
1544
|
+
fet passthrough status --change <change-id>
|
|
1545
|
+
\`\`\`
|
|
1546
|
+
|
|
1547
|
+
Output:
|
|
1548
|
+
- State which artifact was created.
|
|
1549
|
+
- Show the file path.
|
|
1550
|
+
- Show the current status and what to run next.
|
|
1551
|
+
|
|
1552
|
+
Guardrails:
|
|
1553
|
+
- Create one artifact per invocation.
|
|
1554
|
+
- Never skip dependency order.
|
|
1555
|
+
- Ask the user if instructions are ambiguous enough that a useful artifact cannot be written.`
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
function renderFastForwardSlashPrompt(command) {
|
|
1559
|
+
const title = command === "propose" ? "Propose a new FET/OpenSpec change" : "Fast-forward FET/OpenSpec artifact creation";
|
|
1560
|
+
return renderManagedSlashPrompt(
|
|
1561
|
+
`fet ${command} [...args]`,
|
|
1562
|
+
command === "propose" ? "Create a change and generate required OpenSpec artifacts" : "Generate required OpenSpec artifacts for a change",
|
|
1563
|
+
`${title}.
|
|
1564
|
+
|
|
1565
|
+
Input after the slash command may be a change id or a description of what the user wants to build.
|
|
1566
|
+
|
|
1567
|
+
Steps:
|
|
1568
|
+
|
|
1569
|
+
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:
|
|
1574
|
+
\`\`\`sh
|
|
1575
|
+
fet passthrough status --change <change-id> --json
|
|
1576
|
+
\`\`\`
|
|
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.
|
|
1586
|
+
|
|
1587
|
+
Artifact rules:
|
|
1588
|
+
- Follow the instruction field from OpenSpec/FET for each artifact.
|
|
1589
|
+
- Use template as structure, filling it with concrete project-specific content.
|
|
1590
|
+
- Do not copy context/rules wrapper text into artifact files.
|
|
1591
|
+
- Verify each file exists after writing.
|
|
1592
|
+
|
|
1593
|
+
Output:
|
|
1594
|
+
- Change id and location.
|
|
1595
|
+
- Artifacts created.
|
|
1596
|
+
- Current status.
|
|
1597
|
+
- Next recommended command, usually /prompts:fet-apply <change-id>.`
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
function renderManagedSlashPrompt(command, description, body) {
|
|
1601
|
+
return `<!-- FET:MANAGED
|
|
1602
|
+
schemaVersion: 1
|
|
1603
|
+
fetVersion: ${FET_VERSION}
|
|
1604
|
+
generator: codex-adapter
|
|
1605
|
+
adapterVersion: 1
|
|
1606
|
+
command: ${command}
|
|
1607
|
+
FET:END -->
|
|
1608
|
+
|
|
1609
|
+
---
|
|
1610
|
+
description: ${description}
|
|
1611
|
+
argument-hint: command arguments
|
|
1612
|
+
---
|
|
1613
|
+
|
|
1614
|
+
${body}
|
|
1615
|
+
`;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// src/adapters/codex/index.ts
|
|
1619
|
+
var CodexAdapter = class {
|
|
1620
|
+
tool = "codex";
|
|
1621
|
+
adapterVersion = 1;
|
|
1622
|
+
async detect(projectRoot) {
|
|
1623
|
+
return {
|
|
1624
|
+
detected: await exists3(join10(projectRoot, ".codex")) || await exists3(join10(projectRoot, "AGENTS.md")),
|
|
1625
|
+
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
async planInstall(_projectRoot) {
|
|
1629
|
+
return {
|
|
1630
|
+
tool: this.tool,
|
|
1631
|
+
files: [
|
|
1632
|
+
...[codexGuideFile(), ...codexCommandFiles()].map((file) => ({
|
|
1633
|
+
...file,
|
|
1634
|
+
managed: true,
|
|
1635
|
+
root: "project"
|
|
1636
|
+
})),
|
|
1637
|
+
...codexSlashPromptFiles().map((file) => ({
|
|
1638
|
+
...file,
|
|
1639
|
+
managed: true,
|
|
1640
|
+
root: "codex-home"
|
|
1641
|
+
}))
|
|
1642
|
+
]
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
async install(projectRoot, plan, force = false) {
|
|
1646
|
+
const written = [];
|
|
1647
|
+
const skipped = [];
|
|
1648
|
+
for (const file of plan.files) {
|
|
1649
|
+
const target = resolveTarget(projectRoot, file);
|
|
1650
|
+
const displayPath = displayPathFor(file);
|
|
1651
|
+
const existing = await readExisting(target);
|
|
1652
|
+
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
1653
|
+
throw new FetError({
|
|
1654
|
+
code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
|
|
1655
|
+
message: "Codex adapter file already exists and is not managed by FET",
|
|
1656
|
+
details: { path: displayPath },
|
|
1657
|
+
suggestedCommand: "fet init --yes"
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
1661
|
+
await createBackup(target);
|
|
1662
|
+
}
|
|
1663
|
+
await mkdir5(dirname5(target), { recursive: true });
|
|
1664
|
+
await atomicWrite(target, file.content);
|
|
1665
|
+
written.push(displayPath);
|
|
1666
|
+
}
|
|
1667
|
+
return { tool: this.tool, written, skipped };
|
|
1668
|
+
}
|
|
1669
|
+
async doctor(projectRoot) {
|
|
1670
|
+
const plan = await this.planInstall(projectRoot);
|
|
1671
|
+
const checks = [];
|
|
1672
|
+
for (const file of plan.files) {
|
|
1673
|
+
const target = resolveTarget(projectRoot, file);
|
|
1674
|
+
const displayPath = displayPathFor(file);
|
|
1675
|
+
const content = await readExisting(target);
|
|
1676
|
+
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
1677
|
+
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
1678
|
+
checks.push({
|
|
1679
|
+
id: `codex:${displayPath}`,
|
|
1680
|
+
status: !content ? "warn" : managed && versionMatches ? "pass" : "warn",
|
|
1681
|
+
message: !content ? `${displayPath} is missing` : !managed ? `${displayPath} exists but is not managed by FET` : !versionMatches ? `${displayPath} adapterVersion is stale` : `${displayPath} is managed by FET`,
|
|
1682
|
+
suggestedCommand: !content || !managed || !versionMatches ? "fet init" : void 0
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
return checks;
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
function resolveTarget(projectRoot, file) {
|
|
1689
|
+
if (file.root === "codex-home") {
|
|
1690
|
+
return join10(resolveCodexHome(), file.path);
|
|
1691
|
+
}
|
|
1692
|
+
return join10(projectRoot, file.path);
|
|
1693
|
+
}
|
|
1694
|
+
function displayPathFor(file) {
|
|
1695
|
+
if (file.root === "codex-home") {
|
|
1696
|
+
return `$CODEX_HOME/${file.path.replaceAll("\\", "/")}`;
|
|
1697
|
+
}
|
|
1698
|
+
return file.path;
|
|
1699
|
+
}
|
|
1700
|
+
function resolveCodexHome() {
|
|
1701
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join10(homedir(), ".codex");
|
|
1702
|
+
}
|
|
1703
|
+
async function readExisting(path) {
|
|
1704
|
+
try {
|
|
1705
|
+
return await readFile9(path, "utf8");
|
|
1706
|
+
} catch {
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
async function exists3(path) {
|
|
1711
|
+
try {
|
|
1712
|
+
await stat5(path);
|
|
1713
|
+
return true;
|
|
1714
|
+
} catch {
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// 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";
|
|
1722
|
+
|
|
1723
|
+
// src/adapters/cursor/templates.ts
|
|
1081
1724
|
function cursorSkillFiles() {
|
|
1082
|
-
return
|
|
1725
|
+
return FET_ADAPTER_COMMANDS.map((command) => ({
|
|
1083
1726
|
path: `.cursor/skills/fet-${command}/SKILL.md`,
|
|
1084
1727
|
content: renderSkill(command)
|
|
1085
1728
|
}));
|
|
@@ -1110,12 +1753,13 @@ alwaysApply: false
|
|
|
1110
1753
|
};
|
|
1111
1754
|
}
|
|
1112
1755
|
function renderSkill(command) {
|
|
1756
|
+
const usage = command === "passthrough" ? "fet passthrough <openspec-command> [...args]" : `fet ${command}`;
|
|
1113
1757
|
return `<!-- FET:MANAGED
|
|
1114
1758
|
schemaVersion: 1
|
|
1115
1759
|
fetVersion: ${FET_VERSION}
|
|
1116
1760
|
generator: cursor-adapter
|
|
1117
1761
|
adapterVersion: 1
|
|
1118
|
-
command:
|
|
1762
|
+
command: ${usage}
|
|
1119
1763
|
FET:END -->
|
|
1120
1764
|
|
|
1121
1765
|
---
|
|
@@ -1129,7 +1773,7 @@ disable-model-invocation: true
|
|
|
1129
1773
|
\u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
|
|
1130
1774
|
|
|
1131
1775
|
\`\`\`sh
|
|
1132
|
-
|
|
1776
|
+
${usage}
|
|
1133
1777
|
\`\`\`
|
|
1134
1778
|
|
|
1135
1779
|
\u6267\u884C\u524D\u8BF7\u786E\u8BA4\u5DF2\u9605\u8BFB AGENTS.md \u4E0E openspec/config.yaml\u3002
|
|
@@ -1142,7 +1786,7 @@ var CursorAdapter = class {
|
|
|
1142
1786
|
adapterVersion = 1;
|
|
1143
1787
|
async detect(projectRoot) {
|
|
1144
1788
|
return {
|
|
1145
|
-
detected: await
|
|
1789
|
+
detected: await exists4(join11(projectRoot, ".cursor")),
|
|
1146
1790
|
reason: "Cursor adapter is available for any project"
|
|
1147
1791
|
};
|
|
1148
1792
|
}
|
|
@@ -1159,8 +1803,8 @@ var CursorAdapter = class {
|
|
|
1159
1803
|
const written = [];
|
|
1160
1804
|
const skipped = [];
|
|
1161
1805
|
for (const file of plan.files) {
|
|
1162
|
-
const target =
|
|
1163
|
-
const existing = await
|
|
1806
|
+
const target = join11(projectRoot, file.path);
|
|
1807
|
+
const existing = await readExisting2(target);
|
|
1164
1808
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
1165
1809
|
throw new FetError({
|
|
1166
1810
|
code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
|
|
@@ -1172,7 +1816,7 @@ var CursorAdapter = class {
|
|
|
1172
1816
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
1173
1817
|
await createBackup(target);
|
|
1174
1818
|
}
|
|
1175
|
-
await
|
|
1819
|
+
await mkdir6(dirname6(target), { recursive: true });
|
|
1176
1820
|
await atomicWrite(target, file.content);
|
|
1177
1821
|
written.push(file.path);
|
|
1178
1822
|
}
|
|
@@ -1182,8 +1826,8 @@ var CursorAdapter = class {
|
|
|
1182
1826
|
const plan = await this.planInstall(projectRoot);
|
|
1183
1827
|
const checks = [];
|
|
1184
1828
|
for (const file of plan.files) {
|
|
1185
|
-
const target =
|
|
1186
|
-
const content = await
|
|
1829
|
+
const target = join11(projectRoot, file.path);
|
|
1830
|
+
const content = await readExisting2(target);
|
|
1187
1831
|
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
1188
1832
|
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
1189
1833
|
checks.push({
|
|
@@ -1196,16 +1840,16 @@ var CursorAdapter = class {
|
|
|
1196
1840
|
return checks;
|
|
1197
1841
|
}
|
|
1198
1842
|
};
|
|
1199
|
-
async function
|
|
1843
|
+
async function readExisting2(path) {
|
|
1200
1844
|
try {
|
|
1201
|
-
return await
|
|
1845
|
+
return await readFile10(path, "utf8");
|
|
1202
1846
|
} catch {
|
|
1203
1847
|
return null;
|
|
1204
1848
|
}
|
|
1205
1849
|
}
|
|
1206
|
-
async function
|
|
1850
|
+
async function exists4(path) {
|
|
1207
1851
|
try {
|
|
1208
|
-
await
|
|
1852
|
+
await stat6(path);
|
|
1209
1853
|
return true;
|
|
1210
1854
|
} catch {
|
|
1211
1855
|
return false;
|
|
@@ -1217,28 +1861,28 @@ import { execFile as execFile3 } from "child_process";
|
|
|
1217
1861
|
import { promisify as promisify3 } from "util";
|
|
1218
1862
|
|
|
1219
1863
|
// src/openspec/inspector.ts
|
|
1220
|
-
import { readdir, stat as
|
|
1221
|
-
import { join as
|
|
1864
|
+
import { readdir, stat as stat7 } from "fs/promises";
|
|
1865
|
+
import { join as join12 } from "path";
|
|
1222
1866
|
async function inspectOpenSpecProject(projectRoot) {
|
|
1223
|
-
const openspecPath =
|
|
1224
|
-
const changesPath =
|
|
1225
|
-
const archivePath =
|
|
1867
|
+
const openspecPath = join12(projectRoot, "openspec");
|
|
1868
|
+
const changesPath = join12(openspecPath, "changes");
|
|
1869
|
+
const archivePath = join12(openspecPath, "archive");
|
|
1226
1870
|
return {
|
|
1227
|
-
exists: await
|
|
1871
|
+
exists: await exists5(openspecPath),
|
|
1228
1872
|
changes: await listDirectories(changesPath),
|
|
1229
1873
|
archived: await listDirectories(archivePath)
|
|
1230
1874
|
};
|
|
1231
1875
|
}
|
|
1232
1876
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
1233
|
-
const changePath =
|
|
1234
|
-
const tasksPath =
|
|
1235
|
-
const specsPath =
|
|
1877
|
+
const changePath = join12(projectRoot, "openspec", "changes", changeId);
|
|
1878
|
+
const tasksPath = join12(changePath, "tasks.md");
|
|
1879
|
+
const specsPath = join12(changePath, "specs");
|
|
1236
1880
|
return {
|
|
1237
1881
|
changeId,
|
|
1238
|
-
exists: await
|
|
1239
|
-
hasProposal: await
|
|
1240
|
-
hasTasks: await
|
|
1241
|
-
hasSpecs: await
|
|
1882
|
+
exists: await exists5(changePath),
|
|
1883
|
+
hasProposal: await exists5(join12(changePath, "proposal.md")),
|
|
1884
|
+
hasTasks: await exists5(tasksPath),
|
|
1885
|
+
hasSpecs: await exists5(specsPath),
|
|
1242
1886
|
tasksPath,
|
|
1243
1887
|
changePath
|
|
1244
1888
|
};
|
|
@@ -1251,9 +1895,9 @@ async function listDirectories(path) {
|
|
|
1251
1895
|
return [];
|
|
1252
1896
|
}
|
|
1253
1897
|
}
|
|
1254
|
-
async function
|
|
1898
|
+
async function exists5(path) {
|
|
1255
1899
|
try {
|
|
1256
|
-
await
|
|
1900
|
+
await stat7(path);
|
|
1257
1901
|
return true;
|
|
1258
1902
|
} catch {
|
|
1259
1903
|
return false;
|
|
@@ -1412,12 +2056,12 @@ function parseCommands(help) {
|
|
|
1412
2056
|
}
|
|
1413
2057
|
|
|
1414
2058
|
// src/scanner/package.ts
|
|
1415
|
-
import { readFile as
|
|
1416
|
-
import { join as
|
|
1417
|
-
import { parse } from "yaml";
|
|
2059
|
+
import { readFile as readFile11, stat as stat8 } from "fs/promises";
|
|
2060
|
+
import { join as join13 } from "path";
|
|
2061
|
+
import { parse as parse2 } from "yaml";
|
|
1418
2062
|
async function readPackageJson(projectRoot) {
|
|
1419
2063
|
try {
|
|
1420
|
-
return JSON.parse(await
|
|
2064
|
+
return JSON.parse(await readFile11(join13(projectRoot, "package.json"), "utf8"));
|
|
1421
2065
|
} catch {
|
|
1422
2066
|
return null;
|
|
1423
2067
|
}
|
|
@@ -1483,7 +2127,7 @@ function detectFramework(pkg) {
|
|
|
1483
2127
|
}
|
|
1484
2128
|
async function detectLanguage(projectRoot, pkg) {
|
|
1485
2129
|
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
1486
|
-
if (deps.typescript || await
|
|
2130
|
+
if (deps.typescript || await exists6(join13(projectRoot, "tsconfig.json"))) {
|
|
1487
2131
|
return "typescript";
|
|
1488
2132
|
}
|
|
1489
2133
|
return "javascript";
|
|
@@ -1498,7 +2142,7 @@ async function detectWorkspaces(projectRoot, pkg) {
|
|
|
1498
2142
|
return packageWorkspaces;
|
|
1499
2143
|
}
|
|
1500
2144
|
try {
|
|
1501
|
-
const workspace =
|
|
2145
|
+
const workspace = parse2(await readFile11(join13(projectRoot, "pnpm-workspace.yaml"), "utf8"));
|
|
1502
2146
|
return (workspace?.packages ?? []).map((path) => ({
|
|
1503
2147
|
name: path,
|
|
1504
2148
|
path,
|
|
@@ -1518,7 +2162,7 @@ async function detectLockManagers(projectRoot) {
|
|
|
1518
2162
|
];
|
|
1519
2163
|
const found = [];
|
|
1520
2164
|
for (const [file, manager] of lockFiles) {
|
|
1521
|
-
if (await
|
|
2165
|
+
if (await exists6(join13(projectRoot, file))) {
|
|
1522
2166
|
found.push(manager);
|
|
1523
2167
|
}
|
|
1524
2168
|
}
|
|
@@ -1533,9 +2177,9 @@ function normalizeWorkspaces(workspaces) {
|
|
|
1533
2177
|
function scriptCommand(packageManager, name) {
|
|
1534
2178
|
return packageManager === "npm" ? `npm run ${name}` : `${packageManager} ${name}`;
|
|
1535
2179
|
}
|
|
1536
|
-
async function
|
|
2180
|
+
async function exists6(path) {
|
|
1537
2181
|
try {
|
|
1538
|
-
await
|
|
2182
|
+
await stat8(path);
|
|
1539
2183
|
return true;
|
|
1540
2184
|
} catch {
|
|
1541
2185
|
return false;
|
|
@@ -1543,14 +2187,14 @@ async function exists5(path) {
|
|
|
1543
2187
|
}
|
|
1544
2188
|
|
|
1545
2189
|
// src/scanner/routes.ts
|
|
1546
|
-
import { readdir as readdir2, stat as
|
|
1547
|
-
import { join as
|
|
2190
|
+
import { readdir as readdir2, stat as stat9 } from "fs/promises";
|
|
2191
|
+
import { join as join14, relative, sep } from "path";
|
|
1548
2192
|
async function scanRoutes(projectRoot) {
|
|
1549
2193
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
1550
2194
|
const routes = [];
|
|
1551
2195
|
for (const candidate of candidates) {
|
|
1552
|
-
const root =
|
|
1553
|
-
if (!await
|
|
2196
|
+
const root = join14(projectRoot, candidate);
|
|
2197
|
+
if (!await exists7(root)) {
|
|
1554
2198
|
continue;
|
|
1555
2199
|
}
|
|
1556
2200
|
for (const file of await listFiles(root)) {
|
|
@@ -1577,7 +2221,7 @@ async function listFiles(root) {
|
|
|
1577
2221
|
const entries = await readdir2(root, { withFileTypes: true });
|
|
1578
2222
|
const files = [];
|
|
1579
2223
|
for (const entry of entries) {
|
|
1580
|
-
const path =
|
|
2224
|
+
const path = join14(root, entry.name);
|
|
1581
2225
|
if (entry.isDirectory()) {
|
|
1582
2226
|
files.push(...await listFiles(path));
|
|
1583
2227
|
} else {
|
|
@@ -1586,9 +2230,9 @@ async function listFiles(root) {
|
|
|
1586
2230
|
}
|
|
1587
2231
|
return files;
|
|
1588
2232
|
}
|
|
1589
|
-
async function
|
|
2233
|
+
async function exists7(path) {
|
|
1590
2234
|
try {
|
|
1591
|
-
await
|
|
2235
|
+
await stat9(path);
|
|
1592
2236
|
return true;
|
|
1593
2237
|
} catch {
|
|
1594
2238
|
return false;
|
|
@@ -1720,7 +2364,7 @@ async function createCommandContext(command, options) {
|
|
|
1720
2364
|
stateStore: new StateStore(projectRoot, FET_VERSION, project),
|
|
1721
2365
|
openSpec: new DefaultOpenSpecAdapter(),
|
|
1722
2366
|
scanner: new ProjectScanner(),
|
|
1723
|
-
toolAdapters: [new CursorAdapter()]
|
|
2367
|
+
toolAdapters: [new CursorAdapter(), new CodexAdapter()]
|
|
1724
2368
|
};
|
|
1725
2369
|
}
|
|
1726
2370
|
|