@nick848/fet 1.0.1 → 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/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 commands2 = Object.entries(scan.commands).map(([name, command]) => `| ${name} | \`${command.command}\` | ${command.source} |`).join("\n");
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
- ${commands2 || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | [NEEDS LLM INPUT] |"}
209
+ ${commands || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | [NEEDS LLM INPUT] |"}
210
210
 
211
211
  ## Structure
212
212
 
@@ -1081,12 +1081,13 @@ async function resolveChangeId(ctx) {
1081
1081
  // src/cli/context.ts
1082
1082
  import { resolve } from "path";
1083
1083
 
1084
- // src/adapters/cursor/index.ts
1084
+ // src/adapters/codex/index.ts
1085
1085
  import { mkdir as mkdir5, readFile as readFile9, stat as stat5 } from "fs/promises";
1086
+ import { homedir } from "os";
1086
1087
  import { dirname as dirname5, join as join10 } from "path";
1087
1088
 
1088
- // src/adapters/cursor/templates.ts
1089
- var commands = [
1089
+ // src/adapters/commands.ts
1090
+ var FET_WORKFLOW_COMMANDS = [
1090
1091
  "explore",
1091
1092
  "propose",
1092
1093
  "new",
@@ -1099,8 +1100,629 @@ var commands = [
1099
1100
  "bulk-archive",
1100
1101
  "onboard"
1101
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
1102
1724
  function cursorSkillFiles() {
1103
- return commands.map((command) => ({
1725
+ return FET_ADAPTER_COMMANDS.map((command) => ({
1104
1726
  path: `.cursor/skills/fet-${command}/SKILL.md`,
1105
1727
  content: renderSkill(command)
1106
1728
  }));
@@ -1131,12 +1753,13 @@ alwaysApply: false
1131
1753
  };
1132
1754
  }
1133
1755
  function renderSkill(command) {
1756
+ const usage = command === "passthrough" ? "fet passthrough <openspec-command> [...args]" : `fet ${command}`;
1134
1757
  return `<!-- FET:MANAGED
1135
1758
  schemaVersion: 1
1136
1759
  fetVersion: ${FET_VERSION}
1137
1760
  generator: cursor-adapter
1138
1761
  adapterVersion: 1
1139
- command: fet ${command}
1762
+ command: ${usage}
1140
1763
  FET:END -->
1141
1764
 
1142
1765
  ---
@@ -1150,7 +1773,7 @@ disable-model-invocation: true
1150
1773
  \u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
1151
1774
 
1152
1775
  \`\`\`sh
1153
- fet ${command}
1776
+ ${usage}
1154
1777
  \`\`\`
1155
1778
 
1156
1779
  \u6267\u884C\u524D\u8BF7\u786E\u8BA4\u5DF2\u9605\u8BFB AGENTS.md \u4E0E openspec/config.yaml\u3002
@@ -1163,7 +1786,7 @@ var CursorAdapter = class {
1163
1786
  adapterVersion = 1;
1164
1787
  async detect(projectRoot) {
1165
1788
  return {
1166
- detected: await exists3(join10(projectRoot, ".cursor")),
1789
+ detected: await exists4(join11(projectRoot, ".cursor")),
1167
1790
  reason: "Cursor adapter is available for any project"
1168
1791
  };
1169
1792
  }
@@ -1180,8 +1803,8 @@ var CursorAdapter = class {
1180
1803
  const written = [];
1181
1804
  const skipped = [];
1182
1805
  for (const file of plan.files) {
1183
- const target = join10(projectRoot, file.path);
1184
- const existing = await readExisting(target);
1806
+ const target = join11(projectRoot, file.path);
1807
+ const existing = await readExisting2(target);
1185
1808
  if (existing && !existing.includes("FET:MANAGED") && !force) {
1186
1809
  throw new FetError({
1187
1810
  code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
@@ -1193,7 +1816,7 @@ var CursorAdapter = class {
1193
1816
  if (existing && !existing.includes("FET:MANAGED") && force) {
1194
1817
  await createBackup(target);
1195
1818
  }
1196
- await mkdir5(dirname5(target), { recursive: true });
1819
+ await mkdir6(dirname6(target), { recursive: true });
1197
1820
  await atomicWrite(target, file.content);
1198
1821
  written.push(file.path);
1199
1822
  }
@@ -1203,8 +1826,8 @@ var CursorAdapter = class {
1203
1826
  const plan = await this.planInstall(projectRoot);
1204
1827
  const checks = [];
1205
1828
  for (const file of plan.files) {
1206
- const target = join10(projectRoot, file.path);
1207
- const content = await readExisting(target);
1829
+ const target = join11(projectRoot, file.path);
1830
+ const content = await readExisting2(target);
1208
1831
  const managed = Boolean(content?.includes("FET:MANAGED"));
1209
1832
  const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
1210
1833
  checks.push({
@@ -1217,16 +1840,16 @@ var CursorAdapter = class {
1217
1840
  return checks;
1218
1841
  }
1219
1842
  };
1220
- async function readExisting(path) {
1843
+ async function readExisting2(path) {
1221
1844
  try {
1222
- return await readFile9(path, "utf8");
1845
+ return await readFile10(path, "utf8");
1223
1846
  } catch {
1224
1847
  return null;
1225
1848
  }
1226
1849
  }
1227
- async function exists3(path) {
1850
+ async function exists4(path) {
1228
1851
  try {
1229
- await stat5(path);
1852
+ await stat6(path);
1230
1853
  return true;
1231
1854
  } catch {
1232
1855
  return false;
@@ -1238,28 +1861,28 @@ import { execFile as execFile3 } from "child_process";
1238
1861
  import { promisify as promisify3 } from "util";
1239
1862
 
1240
1863
  // src/openspec/inspector.ts
1241
- import { readdir, stat as stat6 } from "fs/promises";
1242
- import { join as join11 } from "path";
1864
+ import { readdir, stat as stat7 } from "fs/promises";
1865
+ import { join as join12 } from "path";
1243
1866
  async function inspectOpenSpecProject(projectRoot) {
1244
- const openspecPath = join11(projectRoot, "openspec");
1245
- const changesPath = join11(openspecPath, "changes");
1246
- const archivePath = join11(openspecPath, "archive");
1867
+ const openspecPath = join12(projectRoot, "openspec");
1868
+ const changesPath = join12(openspecPath, "changes");
1869
+ const archivePath = join12(openspecPath, "archive");
1247
1870
  return {
1248
- exists: await exists4(openspecPath),
1871
+ exists: await exists5(openspecPath),
1249
1872
  changes: await listDirectories(changesPath),
1250
1873
  archived: await listDirectories(archivePath)
1251
1874
  };
1252
1875
  }
1253
1876
  async function inspectOpenSpecChange(projectRoot, changeId) {
1254
- const changePath = join11(projectRoot, "openspec", "changes", changeId);
1255
- const tasksPath = join11(changePath, "tasks.md");
1256
- const specsPath = join11(changePath, "specs");
1877
+ const changePath = join12(projectRoot, "openspec", "changes", changeId);
1878
+ const tasksPath = join12(changePath, "tasks.md");
1879
+ const specsPath = join12(changePath, "specs");
1257
1880
  return {
1258
1881
  changeId,
1259
- exists: await exists4(changePath),
1260
- hasProposal: await exists4(join11(changePath, "proposal.md")),
1261
- hasTasks: await exists4(tasksPath),
1262
- hasSpecs: await exists4(specsPath),
1882
+ exists: await exists5(changePath),
1883
+ hasProposal: await exists5(join12(changePath, "proposal.md")),
1884
+ hasTasks: await exists5(tasksPath),
1885
+ hasSpecs: await exists5(specsPath),
1263
1886
  tasksPath,
1264
1887
  changePath
1265
1888
  };
@@ -1272,9 +1895,9 @@ async function listDirectories(path) {
1272
1895
  return [];
1273
1896
  }
1274
1897
  }
1275
- async function exists4(path) {
1898
+ async function exists5(path) {
1276
1899
  try {
1277
- await stat6(path);
1900
+ await stat7(path);
1278
1901
  return true;
1279
1902
  } catch {
1280
1903
  return false;
@@ -1433,12 +2056,12 @@ function parseCommands(help) {
1433
2056
  }
1434
2057
 
1435
2058
  // src/scanner/package.ts
1436
- import { readFile as readFile10, stat as stat7 } from "fs/promises";
1437
- import { join as join12 } from "path";
2059
+ import { readFile as readFile11, stat as stat8 } from "fs/promises";
2060
+ import { join as join13 } from "path";
1438
2061
  import { parse as parse2 } from "yaml";
1439
2062
  async function readPackageJson(projectRoot) {
1440
2063
  try {
1441
- return JSON.parse(await readFile10(join12(projectRoot, "package.json"), "utf8"));
2064
+ return JSON.parse(await readFile11(join13(projectRoot, "package.json"), "utf8"));
1442
2065
  } catch {
1443
2066
  return null;
1444
2067
  }
@@ -1504,7 +2127,7 @@ function detectFramework(pkg) {
1504
2127
  }
1505
2128
  async function detectLanguage(projectRoot, pkg) {
1506
2129
  const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
1507
- if (deps.typescript || await exists5(join12(projectRoot, "tsconfig.json"))) {
2130
+ if (deps.typescript || await exists6(join13(projectRoot, "tsconfig.json"))) {
1508
2131
  return "typescript";
1509
2132
  }
1510
2133
  return "javascript";
@@ -1519,7 +2142,7 @@ async function detectWorkspaces(projectRoot, pkg) {
1519
2142
  return packageWorkspaces;
1520
2143
  }
1521
2144
  try {
1522
- const workspace = parse2(await readFile10(join12(projectRoot, "pnpm-workspace.yaml"), "utf8"));
2145
+ const workspace = parse2(await readFile11(join13(projectRoot, "pnpm-workspace.yaml"), "utf8"));
1523
2146
  return (workspace?.packages ?? []).map((path) => ({
1524
2147
  name: path,
1525
2148
  path,
@@ -1539,7 +2162,7 @@ async function detectLockManagers(projectRoot) {
1539
2162
  ];
1540
2163
  const found = [];
1541
2164
  for (const [file, manager] of lockFiles) {
1542
- if (await exists5(join12(projectRoot, file))) {
2165
+ if (await exists6(join13(projectRoot, file))) {
1543
2166
  found.push(manager);
1544
2167
  }
1545
2168
  }
@@ -1554,9 +2177,9 @@ function normalizeWorkspaces(workspaces) {
1554
2177
  function scriptCommand(packageManager, name) {
1555
2178
  return packageManager === "npm" ? `npm run ${name}` : `${packageManager} ${name}`;
1556
2179
  }
1557
- async function exists5(path) {
2180
+ async function exists6(path) {
1558
2181
  try {
1559
- await stat7(path);
2182
+ await stat8(path);
1560
2183
  return true;
1561
2184
  } catch {
1562
2185
  return false;
@@ -1564,14 +2187,14 @@ async function exists5(path) {
1564
2187
  }
1565
2188
 
1566
2189
  // src/scanner/routes.ts
1567
- import { readdir as readdir2, stat as stat8 } from "fs/promises";
1568
- import { join as join13, relative, sep } from "path";
2190
+ import { readdir as readdir2, stat as stat9 } from "fs/promises";
2191
+ import { join as join14, relative, sep } from "path";
1569
2192
  async function scanRoutes(projectRoot) {
1570
2193
  const candidates = ["src/routes", "src/pages", "app", "pages"];
1571
2194
  const routes = [];
1572
2195
  for (const candidate of candidates) {
1573
- const root = join13(projectRoot, candidate);
1574
- if (!await exists6(root)) {
2196
+ const root = join14(projectRoot, candidate);
2197
+ if (!await exists7(root)) {
1575
2198
  continue;
1576
2199
  }
1577
2200
  for (const file of await listFiles(root)) {
@@ -1598,7 +2221,7 @@ async function listFiles(root) {
1598
2221
  const entries = await readdir2(root, { withFileTypes: true });
1599
2222
  const files = [];
1600
2223
  for (const entry of entries) {
1601
- const path = join13(root, entry.name);
2224
+ const path = join14(root, entry.name);
1602
2225
  if (entry.isDirectory()) {
1603
2226
  files.push(...await listFiles(path));
1604
2227
  } else {
@@ -1607,9 +2230,9 @@ async function listFiles(root) {
1607
2230
  }
1608
2231
  return files;
1609
2232
  }
1610
- async function exists6(path) {
2233
+ async function exists7(path) {
1611
2234
  try {
1612
- await stat8(path);
2235
+ await stat9(path);
1613
2236
  return true;
1614
2237
  } catch {
1615
2238
  return false;
@@ -1741,7 +2364,7 @@ async function createCommandContext(command, options) {
1741
2364
  stateStore: new StateStore(projectRoot, FET_VERSION, project),
1742
2365
  openSpec: new DefaultOpenSpecAdapter(),
1743
2366
  scanner: new ProjectScanner(),
1744
- toolAdapters: [new CursorAdapter()]
2367
+ toolAdapters: [new CursorAdapter(), new CodexAdapter()]
1745
2368
  };
1746
2369
  }
1747
2370