@nick848/fet 1.0.4 → 1.0.6

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
@@ -2,15 +2,15 @@
2
2
  import {
3
3
  FetError,
4
4
  toFetError
5
- } from "../chunk-FZOVNHE7.js";
5
+ } from "../chunk-V4ZRBF5L.js";
6
6
 
7
7
  // src/cli/index.ts
8
8
  import { createInterface } from "readline/promises";
9
9
  import { Command } from "commander";
10
10
 
11
11
  // src/commands/init.ts
12
- import { readFile as readFile6, stat as stat2 } from "fs/promises";
13
- import { join as join7 } from "path";
12
+ import { readFile as readFile6, stat as stat3 } from "fs/promises";
13
+ import { join as join8 } from "path";
14
14
 
15
15
  // src/fs/atomic-write.ts
16
16
  import { dirname } from "path";
@@ -118,23 +118,26 @@ async function writeInitJournal(projectRoot, journal) {
118
118
 
119
119
  // src/gitnexus.ts
120
120
  import { execFile } from "child_process";
121
+ import { stat as stat2 } from "fs/promises";
122
+ import { join as join4 } from "path";
121
123
  import { promisify } from "util";
122
124
  var execFileAsync = promisify(execFile);
125
+ var DEFAULT_GRAPH_PATH = ".gitnexus";
123
126
  async function detectGitNexus(env = process.env) {
124
127
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
125
- const executable = env.FET_GITNEXUS_EXECUTABLE?.trim() || "gitnexus";
128
+ const command = resolveGitNexusCommand(env);
126
129
  try {
127
- const { stdout, stderr } = await execFileAsync(executable, ["--version"], { shell: process.platform === "win32" });
130
+ const { stdout, stderr } = await execFileAsync(command.file, [...command.args, "--version"], { shell: process.platform === "win32" });
128
131
  return {
129
132
  installed: true,
130
- executablePath: executable,
133
+ executablePath: command.label,
131
134
  version: (stdout.trim() || stderr.trim() || "unknown").split(/\r?\n/)[0] ?? "unknown",
132
135
  checkedAt
133
136
  };
134
137
  } catch (error) {
135
138
  return {
136
139
  installed: false,
137
- executablePath: executable,
140
+ executablePath: command.label,
138
141
  version: null,
139
142
  checkedAt,
140
143
  error: error instanceof Error ? error.message : String(error)
@@ -148,7 +151,66 @@ function toGitNexusState(detection, previous) {
148
151
  executablePath: detection.installed ? detection.executablePath : null,
149
152
  version: detection.version,
150
153
  checkedAt: detection.checkedAt,
151
- recommendationShownAt: previous?.recommendationShownAt ?? null
154
+ recommendationShownAt: previous?.recommendationShownAt ?? null,
155
+ graphPath: previous?.graphPath ?? null,
156
+ graphExists: previous?.graphExists ?? false,
157
+ lastIndexedAt: previous?.lastIndexedAt ?? null,
158
+ lastRefreshAt: previous?.lastRefreshAt ?? null,
159
+ lastStatus: previous?.lastStatus ?? null,
160
+ setupHandoffPath: previous?.setupHandoffPath ?? null,
161
+ setupHandoffUpdatedAt: previous?.setupHandoffUpdatedAt ?? null,
162
+ handoffPath: previous?.handoffPath ?? null,
163
+ handoffUpdatedAt: previous?.handoffUpdatedAt ?? null
164
+ };
165
+ }
166
+ async function inspectGitNexusGraph(projectRoot, env = process.env) {
167
+ const relative2 = env.FET_GITNEXUS_GRAPH_PATH?.trim() || DEFAULT_GRAPH_PATH;
168
+ const graphPath = join4(projectRoot, relative2);
169
+ try {
170
+ const info = await stat2(graphPath);
171
+ return {
172
+ graphPath: relative2,
173
+ graphExists: true,
174
+ lastIndexedAt: info.mtime.toISOString()
175
+ };
176
+ } catch {
177
+ return {
178
+ graphPath: relative2,
179
+ graphExists: false,
180
+ lastIndexedAt: null
181
+ };
182
+ }
183
+ }
184
+ async function runGitNexus(args, options) {
185
+ const command = resolveGitNexusCommand(options.env ?? process.env);
186
+ const fullCommand = [command.file, ...command.args, ...args];
187
+ try {
188
+ const { stdout, stderr } = await execFileAsync(command.file, [...command.args, ...args], {
189
+ cwd: options.cwd,
190
+ shell: process.platform === "win32"
191
+ });
192
+ return {
193
+ exitCode: 0,
194
+ stdout,
195
+ stderr,
196
+ command: fullCommand
197
+ };
198
+ } catch (error) {
199
+ const maybe = error;
200
+ return {
201
+ exitCode: typeof maybe.code === "number" ? maybe.code : 1,
202
+ stdout: maybe.stdout ?? "",
203
+ stderr: maybe.stderr ?? (error instanceof Error ? error.message : String(error)),
204
+ command: fullCommand
205
+ };
206
+ }
207
+ }
208
+ function mergeGitNexusGraphInfo(state, graph2) {
209
+ return {
210
+ ...state,
211
+ graphPath: graph2.graphPath,
212
+ graphExists: graph2.graphExists,
213
+ lastIndexedAt: graph2.lastIndexedAt
152
214
  };
153
215
  }
154
216
  function renderGitNexusRecommendation(state) {
@@ -157,17 +219,27 @@ function renderGitNexusRecommendation(state) {
157
219
  }
158
220
  return "Optional GitNexus code graph support is not installed. Consider installing GitNexus later to speed up OpenSpec artifact generation and improve code insertion context.";
159
221
  }
222
+ function resolveGitNexusCommand(env) {
223
+ const raw = env.FET_GITNEXUS_COMMAND?.trim() || env.FET_GITNEXUS_EXECUTABLE?.trim() || "gitnexus";
224
+ const parts = splitCommand(raw);
225
+ const [file = "gitnexus", ...args] = parts;
226
+ return { file, args, label: raw };
227
+ }
228
+ function splitCommand(value) {
229
+ const matches = value.match(/"[^"]+"|'[^']+'|\S+/g);
230
+ return (matches?.length ? matches : [value]).map((part) => part.replace(/^["']|["']$/g, ""));
231
+ }
160
232
 
161
233
  // src/version.ts
162
234
  import { existsSync, readFileSync } from "fs";
163
- import { dirname as dirname4, join as join4, parse } from "path";
235
+ import { dirname as dirname4, join as join5, parse } from "path";
164
236
  import { fileURLToPath } from "url";
165
237
  var FET_VERSION = readPackageVersion();
166
238
  function readPackageVersion() {
167
239
  let currentDir = dirname4(fileURLToPath(import.meta.url));
168
240
  const root = parse(currentDir).root;
169
241
  while (true) {
170
- const packageJsonPath = join4(currentDir, "package.json");
242
+ const packageJsonPath = join5(currentDir, "package.json");
171
243
  if (existsSync(packageJsonPath)) {
172
244
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
173
245
  if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
@@ -296,6 +368,12 @@ ${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
296
368
 
297
369
  [NEEDS LLM INPUT]
298
370
 
371
+ ## AI Work Guidelines
372
+
373
+ - Prefer the project-level Andrej Karpathy inspired guidelines in .fet/karpathy-guidelines.md when using FET-managed IDE workflows.
374
+ - For Codex, also read .codex/fet/karpathy-guidelines.md when present.
375
+ - Treat those guidelines as secondary to the user's latest request and explicit OpenSpec artifacts.
376
+
299
377
  ## Scanner Metadata
300
378
 
301
379
  - Generated At: ${scan.generatedAt}
@@ -343,6 +421,97 @@ function renderFetConfig(scan) {
343
421
  });
344
422
  }
345
423
 
424
+ // src/templates/karpathy-skills.ts
425
+ var KARPATHY_SKILLS_SOURCE = "https://github.com/forrestchang/andrej-karpathy-skills";
426
+ var BEGIN = "<!-- FET:BEGIN ANDREJ-KARPATHY-SKILLS -->";
427
+ var END = "<!-- FET:END ANDREJ-KARPATHY-SKILLS -->";
428
+ function mergeKarpathyClaudeMd(existing) {
429
+ const block = renderManagedBlock(renderKarpathyClaudeGuidelines());
430
+ if (!existing || !existing.trim()) {
431
+ return `${block}
432
+ `;
433
+ }
434
+ const start = existing.indexOf(BEGIN);
435
+ const end = existing.indexOf(END);
436
+ if (start !== -1 && end !== -1 && end > start) {
437
+ return `${existing.slice(0, start)}${block}${existing.slice(end + END.length)}`;
438
+ }
439
+ return `${existing.replace(/\s*$/, "")}
440
+
441
+ ${block}
442
+ `;
443
+ }
444
+ function renderKarpathyCursorRule() {
445
+ return `<!-- FET:MANAGED
446
+ schemaVersion: 1
447
+ generator: karpathy-skills
448
+ FET:END -->
449
+
450
+ ---
451
+ description: Andrej Karpathy inspired coding guidelines
452
+ alwaysApply: true
453
+ ---
454
+
455
+ ${renderKarpathyGuidelinesBody()}
456
+ `;
457
+ }
458
+ function renderKarpathyFetHandoff() {
459
+ return `<!-- FET:MANAGED
460
+ schemaVersion: 1
461
+ generator: karpathy-skills
462
+ FET:END -->
463
+
464
+ # Andrej Karpathy Inspired Coding Guidelines
465
+
466
+ ${renderKarpathyGuidelinesBody()}
467
+ `;
468
+ }
469
+ function renderManagedBlock(content) {
470
+ return `${BEGIN}
471
+ ${content}
472
+ ${END}`;
473
+ }
474
+ function renderKarpathyClaudeGuidelines() {
475
+ return `# Andrej Karpathy Inspired Coding Guidelines
476
+
477
+ ${renderKarpathyGuidelinesBody()}`;
478
+ }
479
+ function renderKarpathyGuidelinesBody() {
480
+ return `Source: ${KARPATHY_SKILLS_SOURCE}
481
+
482
+ Use these project-level guidelines together with AGENTS.md, OpenSpec artifacts, and the user's latest request.
483
+
484
+ ## Think Before Coding
485
+
486
+ - State important assumptions before editing.
487
+ - Ask for clarification when ambiguity would change the implementation.
488
+ - Surface tradeoffs instead of silently choosing a risky path.
489
+ - Push back when a simpler approach better fits the request.
490
+
491
+ ## Simplicity First
492
+
493
+ - Solve the requested problem with the smallest clear change.
494
+ - Avoid speculative features, configuration, or abstraction.
495
+ - Do not create abstractions for one-off code.
496
+ - Prefer deleting complexity introduced by your own change over adding more structure.
497
+
498
+ ## Precise Edits
499
+
500
+ - Touch only files and lines that directly serve the task.
501
+ - Preserve existing style even when you personally prefer another pattern.
502
+ - Do not refactor nearby code, comments, or formatting unless the task requires it.
503
+ - Remove only dead imports, variables, or helpers made obsolete by your own change.
504
+
505
+ ## Goal-Driven Execution
506
+
507
+ - Convert vague work into concrete success criteria.
508
+ - For bugs, prefer a reproducing test or clear verification before the fix.
509
+ - For multi-step work, keep a short plan and verify each meaningful step.
510
+ - Continue iterating until the success criteria are met or a blocker is explicit.
511
+
512
+ These guidelines intentionally favor caution over speed for non-trivial work. For obvious one-line fixes, use judgment and stay lightweight.`;
513
+ }
514
+
346
515
  // src/templates/verify-instructions.ts
347
516
  function renderVerifyInstructions(changeId, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
348
517
  return `---
@@ -370,27 +539,28 @@ fet verify --done --change ${changeId}
370
539
  }
371
540
 
372
541
  // src/templates/gitignore.ts
373
- var BEGIN = "# FET:BEGIN LOCAL STATE";
374
- var END = "# FET:END LOCAL STATE";
542
+ var BEGIN2 = "# FET:BEGIN LOCAL STATE";
543
+ var END2 = "# FET:END LOCAL STATE";
375
544
  var RULES = [
376
545
  "openspec/fet-state.json",
377
546
  "openspec/.fet.lock",
378
547
  "openspec/.fet-init-journal.json",
379
548
  "openspec/changes/*/fet-state.json",
380
- "openspec/changes/*/.fet/"
549
+ "openspec/changes/*/.fet/",
550
+ ".gitnexus/"
381
551
  ];
382
552
  function mergeGitignore(existing) {
383
- const block = `${BEGIN}
553
+ const block = `${BEGIN2}
384
554
  ${RULES.join("\n")}
385
- ${END}`;
555
+ ${END2}`;
386
556
  if (!existing || !existing.trim()) {
387
557
  return `${block}
388
558
  `;
389
559
  }
390
- const start = existing.indexOf(BEGIN);
391
- const end = existing.indexOf(END);
560
+ const start = existing.indexOf(BEGIN2);
561
+ const end = existing.indexOf(END2);
392
562
  if (start !== -1 && end !== -1 && end > start) {
393
- return `${existing.slice(0, start)}${block}${existing.slice(end + END.length)}`;
563
+ return `${existing.slice(0, start)}${block}${existing.slice(end + END2.length)}`;
394
564
  }
395
565
  return `${existing.replace(/\s*$/, "")}
396
566
 
@@ -400,7 +570,7 @@ ${block}
400
570
 
401
571
  // src/commands/update-context.ts
402
572
  import { readFile as readFile5 } from "fs/promises";
403
- import { join as join6 } from "path";
573
+ import { join as join7 } from "path";
404
574
 
405
575
  // src/config/yaml.ts
406
576
  import { readFile as readFile3 } from "fs/promises";
@@ -421,11 +591,11 @@ async function mergeFetConfig(configPath, renderedFetYaml) {
421
591
 
422
592
  // src/context-placeholders.ts
423
593
  import { readFile as readFile4 } from "fs/promises";
424
- import { join as join5 } from "path";
594
+ import { join as join6 } from "path";
425
595
  var AGENTS_LLM_PLACEHOLDER_PATTERN = /\[NEEDS? LLM INPUT\]/g;
426
596
  async function countAgentsLlmPlaceholders(projectRoot) {
427
597
  try {
428
- const content = await readFile4(join5(projectRoot, "AGENTS.md"), "utf8");
598
+ const content = await readFile4(join6(projectRoot, "AGENTS.md"), "utf8");
429
599
  return [...content.matchAll(AGENTS_LLM_PLACEHOLDER_PATTERN)].length;
430
600
  } catch {
431
601
  return 0;
@@ -454,9 +624,14 @@ async function updateContextCommand(ctx) {
454
624
  }
455
625
  async function updateContextFiles(ctx) {
456
626
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
457
- const agentsPath = join6(ctx.projectRoot, "AGENTS.md");
458
- const configPath = join6(ctx.projectRoot, "openspec", "config.yaml");
627
+ const agentsPath = join7(ctx.projectRoot, "AGENTS.md");
628
+ const configPath = join7(ctx.projectRoot, "openspec", "config.yaml");
629
+ const claudePath = join7(ctx.projectRoot, "CLAUDE.md");
630
+ const karpathyHandoffPath = join7(ctx.projectRoot, ".fet", "karpathy-guidelines.md");
631
+ const karpathyCursorPath = join7(ctx.projectRoot, ".cursor", "rules", "karpathy-guidelines.mdc");
459
632
  const existingAgents = await readOptional(agentsPath);
633
+ const existingClaude = await readOptional(claudePath);
634
+ const existingKarpathyCursor = await readOptional(karpathyCursorPath);
460
635
  const warnings = [...scan.warnings];
461
636
  if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
462
637
  throw new FetError({
@@ -482,6 +657,13 @@ async function updateContextFiles(ctx) {
482
657
  }
483
658
  await atomicWrite(agentsPath, replaceManagedRegion(existingAgents, renderAgentsMd(scan)));
484
659
  await atomicWrite(configPath, await mergeFetConfig(configPath, renderFetConfig(scan)));
660
+ await atomicWrite(claudePath, mergeKarpathyClaudeMd(existingClaude));
661
+ await atomicWrite(karpathyHandoffPath, renderKarpathyFetHandoff());
662
+ if (!existingKarpathyCursor || existingKarpathyCursor.includes("FET:MANAGED")) {
663
+ await atomicWrite(karpathyCursorPath, renderKarpathyCursorRule());
664
+ } else {
665
+ warnings.push(".cursor/rules/karpathy-guidelines.mdc exists and is not managed by FET; leaving it unchanged.");
666
+ }
485
667
  const placeholderCount = await countAgentsLlmPlaceholders(ctx.projectRoot);
486
668
  if (placeholderCount > 0) {
487
669
  warnings.push(renderAgentsPlaceholderWarning(placeholderCount));
@@ -505,7 +687,7 @@ async function readOptional(path) {
505
687
 
506
688
  // src/commands/init.ts
507
689
  async function initCommand(ctx) {
508
- const alreadyInitialized = await exists(join7(ctx.projectRoot, "openspec", "config.yaml"));
690
+ const alreadyInitialized = await exists(join8(ctx.projectRoot, "openspec", "config.yaml"));
509
691
  let warnings = [];
510
692
  await withProjectLock(
511
693
  ctx.projectRoot,
@@ -527,7 +709,10 @@ async function initCommand(ctx) {
527
709
  const state = await ctx.stateStore.getOrCreateGlobal();
528
710
  state.openspec = identity;
529
711
  state.graph ??= {};
530
- const gitnexus = toGitNexusState(await detectGitNexus(), state.graph.gitnexus);
712
+ const gitnexus = mergeGitNexusGraphInfo(
713
+ toGitNexusState(await detectGitNexus(), state.graph.gitnexus),
714
+ await inspectGitNexusGraph(ctx.projectRoot)
715
+ );
531
716
  if (!gitnexus.installed && !gitnexus.recommendationShownAt) {
532
717
  warnings.push(renderGitNexusRecommendation(gitnexus));
533
718
  gitnexus.recommendationShownAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -557,7 +742,7 @@ async function initCommand(ctx) {
557
742
  });
558
743
  }
559
744
  async function ensureGitignore(ctx) {
560
- const gitignorePath = join7(ctx.projectRoot, ".gitignore");
745
+ const gitignorePath = join8(ctx.projectRoot, ".gitignore");
561
746
  const existing = await readOptional2(gitignorePath);
562
747
  await atomicWrite(gitignorePath, mergeGitignore(existing));
563
748
  }
@@ -570,7 +755,7 @@ async function readOptional2(path) {
570
755
  }
571
756
  async function exists(path) {
572
757
  try {
573
- await stat2(path);
758
+ await stat3(path);
574
759
  return true;
575
760
  } catch {
576
761
  return false;
@@ -578,20 +763,20 @@ async function exists(path) {
578
763
  }
579
764
 
580
765
  // src/commands/doctor.ts
581
- import { readFile as readFile7, stat as stat3 } from "fs/promises";
582
- import { join as join8 } from "path";
766
+ import { readFile as readFile7, stat as stat4 } from "fs/promises";
767
+ import { join as join9 } from "path";
583
768
  async function doctorCommand(ctx, options = {}) {
584
769
  const checks = [];
585
770
  checks.push(await checkOpenSpec(ctx));
586
771
  checks.push(await checkState(ctx));
587
- checks.push(await checkFile("agents", join8(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
588
- checks.push(await checkFile("config", join8(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
772
+ checks.push(await checkFile("agents", join9(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
773
+ checks.push(await checkFile("config", join9(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
589
774
  checks.push(await checkPlaceholders(ctx.projectRoot));
590
775
  checks.push(await checkGitNexus(ctx));
591
776
  for (const adapter of ctx.toolAdapters) {
592
777
  checks.push(...await adapter.doctor(ctx.projectRoot));
593
778
  }
594
- const lockPath = join8(ctx.projectRoot, "openspec", ".fet.lock");
779
+ const lockPath = join9(ctx.projectRoot, "openspec", ".fet.lock");
595
780
  if (await exists2(lockPath)) {
596
781
  if (options.fixLock) {
597
782
  await clearLock(ctx.projectRoot);
@@ -611,7 +796,10 @@ async function doctorCommand(ctx, options = {}) {
611
796
  }
612
797
  async function checkGitNexus(ctx) {
613
798
  const global = await ctx.stateStore.readGlobal();
614
- const state = toGitNexusState(await detectGitNexus(), global?.graph?.gitnexus);
799
+ const state = mergeGitNexusGraphInfo(
800
+ toGitNexusState(await detectGitNexus(), global?.graph?.gitnexus),
801
+ await inspectGitNexusGraph(ctx.projectRoot)
802
+ );
615
803
  if (global) {
616
804
  global.graph ??= {};
617
805
  global.graph.gitnexus = state;
@@ -620,7 +808,7 @@ async function checkGitNexus(ctx) {
620
808
  return state.installed ? {
621
809
  id: "gitnexus",
622
810
  status: "pass",
623
- message: `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"})`
811
+ message: `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"}), graph ${state.graphExists ? "found" : "not found"}`
624
812
  } : {
625
813
  id: "gitnexus",
626
814
  status: "warn",
@@ -649,7 +837,7 @@ async function checkFile(id, path, missing, suggestedCommand) {
649
837
  }
650
838
  async function checkPlaceholders(projectRoot) {
651
839
  try {
652
- await readFile7(join8(projectRoot, "AGENTS.md"), "utf8");
840
+ await readFile7(join9(projectRoot, "AGENTS.md"), "utf8");
653
841
  const count2 = await countAgentsLlmPlaceholders(projectRoot);
654
842
  return count2 ? {
655
843
  id: "context-placeholders",
@@ -663,7 +851,7 @@ async function checkPlaceholders(projectRoot) {
663
851
  }
664
852
  async function exists2(path) {
665
853
  try {
666
- await stat3(path);
854
+ await stat4(path);
667
855
  return true;
668
856
  } catch {
669
857
  return false;
@@ -672,13 +860,13 @@ async function exists2(path) {
672
860
 
673
861
  // src/commands/fill-context.ts
674
862
  import { mkdir as mkdir3 } from "fs/promises";
675
- import { dirname as dirname5, join as join9 } from "path";
863
+ import { dirname as dirname5, join as join10 } from "path";
676
864
  async function fillContextCommand(ctx) {
677
865
  await withProjectLock(
678
866
  ctx.projectRoot,
679
867
  { command: "fill-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
680
868
  async () => {
681
- const handoffPath = join9(ctx.projectRoot, ".fet", "fill-context.md");
869
+ const handoffPath = join10(ctx.projectRoot, ".fet", "fill-context.md");
682
870
  await mkdir3(dirname5(handoffPath), { recursive: true });
683
871
  await atomicWrite(handoffPath, renderGenericHandoff());
684
872
  for (const adapter of ctx.toolAdapters) {
@@ -725,17 +913,268 @@ FET:END -->
725
913
  Use the IDE AI to complete FET-generated placeholders.
726
914
 
727
915
  1. Read AGENTS.md and openspec/config.yaml.
728
- 2. Inspect README files, package scripts, routes, tests, source layout, and project conventions.
729
- 3. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete project-specific content.
730
- 4. Preserve FET managed markers.
731
- 5. Do not modify business code.
732
- 6. Run \`fet doctor\` and confirm no AGENTS.md placeholder warning remains.
916
+ 2. Read .fet/karpathy-guidelines.md when it exists. For Codex, also read .codex/fet/karpathy-guidelines.md when it exists.
917
+ 3. Inspect README files, package scripts, routes, tests, source layout, and project conventions.
918
+ 4. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete project-specific content.
919
+ 5. Preserve FET managed markers.
920
+ 6. Do not modify business code.
921
+ 7. Run \`fet doctor\` and confirm no AGENTS.md placeholder warning remains.
922
+ `;
923
+ }
924
+
925
+ // src/commands/graph.ts
926
+ import { mkdir as mkdir4 } from "fs/promises";
927
+ import { dirname as dirname6, join as join11 } from "path";
928
+ async function graphCommand(ctx, action, args = []) {
929
+ switch (action) {
930
+ case "status":
931
+ await graphStatusCommand(ctx);
932
+ return;
933
+ case "doctor":
934
+ await graphDoctorCommand(ctx);
935
+ return;
936
+ case "setup":
937
+ await graphSetupCommand(ctx);
938
+ return;
939
+ case "handoff":
940
+ await graphHandoffCommand(ctx);
941
+ return;
942
+ case "init":
943
+ await graphAnalyzeCommand(ctx, "init", args);
944
+ return;
945
+ case "refresh":
946
+ await graphAnalyzeCommand(ctx, "refresh", args);
947
+ return;
948
+ }
949
+ }
950
+ async function graphStatusCommand(ctx) {
951
+ const result = await refreshGraphState(ctx, { runStatus: true });
952
+ const warnings = result.state.installed ? [] : ["GitNexus is not installed. Run fet graph setup for installation handoff instructions."];
953
+ ctx.output.result({
954
+ ok: true,
955
+ command: "graph status",
956
+ summary: result.state.installed ? `GitNexus graph status checked. Graph ${result.state.graphExists ? "exists" : "does not exist"} at ${result.state.graphPath ?? ".gitnexus"}.` : "GitNexus is not installed. Graph support remains optional.",
957
+ warnings,
958
+ nextSteps: result.state.installed && !result.state.graphExists ? ["Run fet graph init to build the first GitNexus graph"] : void 0,
959
+ data: result
960
+ });
961
+ }
962
+ async function graphDoctorCommand(ctx) {
963
+ const result = await refreshGraphState(ctx, { runStatus: true });
964
+ const warnings = [
965
+ ...!result.state.installed ? ["GitNexus is not installed."] : [],
966
+ ...result.state.installed && !result.state.graphExists ? ["GitNexus is installed but no graph directory was found."] : [],
967
+ ...!result.state.handoffPath ? ["Graph handoff instructions have not been generated."] : []
968
+ ];
969
+ ctx.output.result({
970
+ ok: true,
971
+ command: "graph doctor",
972
+ summary: warnings.length ? `Graph doctor completed with ${warnings.length} warning(s).` : "Graph doctor completed without warnings.",
973
+ warnings,
974
+ nextSteps: warnings.length ? ["Run fet graph setup", "Run fet graph handoff", "Run fet graph init when GitNexus is installed"] : void 0,
975
+ data: result
976
+ });
977
+ }
978
+ async function graphSetupCommand(ctx) {
979
+ let result;
980
+ const handoffPath = join11(ctx.projectRoot, ".fet", "graph-setup.md");
981
+ await withProjectLock(ctx.projectRoot, { command: "graph setup", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
982
+ result = await refreshGraphState(ctx, { write: false });
983
+ await writeHandoffFile(handoffPath, renderGraphSetupHandoff(result.state));
984
+ const global = await ctx.stateStore.getOrCreateGlobal();
985
+ global.graph ??= {};
986
+ global.graph.gitnexus = {
987
+ ...result.state,
988
+ setupHandoffPath: ".fet/graph-setup.md",
989
+ setupHandoffUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
990
+ };
991
+ await ctx.stateStore.writeGlobal(global);
992
+ });
993
+ ctx.output.result({
994
+ ok: true,
995
+ command: "graph setup",
996
+ summary: "GitNexus setup handoff generated.",
997
+ warnings: result.state.installed ? [] : ["GitNexus is not installed. The handoff explains installation and IDE-assisted setup options."],
998
+ nextSteps: result.state.installed ? ["Run gitnexus setup if you want to configure IDE/MCP integrations", "Run fet graph init"] : ["Open .fet/graph-setup.md in your IDE AI"],
999
+ data: {
1000
+ path: ".fet/graph-setup.md",
1001
+ gitnexus: result.state
1002
+ }
1003
+ });
1004
+ }
1005
+ async function graphHandoffCommand(ctx) {
1006
+ let result;
1007
+ const handoffPath = join11(ctx.projectRoot, ".fet", "graph-handoff.md");
1008
+ await withProjectLock(ctx.projectRoot, { command: "graph handoff", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
1009
+ result = await refreshGraphState(ctx, { runStatus: true, write: false });
1010
+ await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state));
1011
+ const global = await ctx.stateStore.getOrCreateGlobal();
1012
+ global.graph ??= {};
1013
+ global.graph.gitnexus = {
1014
+ ...result.state,
1015
+ handoffPath: ".fet/graph-handoff.md",
1016
+ handoffUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
1017
+ };
1018
+ await ctx.stateStore.writeGlobal(global);
1019
+ });
1020
+ ctx.output.result({
1021
+ ok: true,
1022
+ command: "graph handoff",
1023
+ summary: "GitNexus graph usage handoff generated.",
1024
+ warnings: result.state.installed ? [] : ["GitNexus is not installed. The handoff still documents the fallback behavior."],
1025
+ nextSteps: ["Cursor/Codex/OpenCode: read .fet/graph-handoff.md before broad repository scans"],
1026
+ data: {
1027
+ path: ".fet/graph-handoff.md",
1028
+ gitnexus: result.state
1029
+ }
1030
+ });
1031
+ }
1032
+ async function graphAnalyzeCommand(ctx, mode, args) {
1033
+ const detection = await detectGitNexus();
1034
+ if (!detection.installed) {
1035
+ throw new FetError({
1036
+ code: "GRAPH_PROVIDER_NOT_FOUND" /* GraphProviderNotFound */,
1037
+ message: "GitNexus is not installed or is not available on PATH.",
1038
+ details: { executable: detection.executablePath, error: detection.error },
1039
+ suggestedCommand: "fet graph setup"
1040
+ });
1041
+ }
1042
+ const run = await runGitNexus(["analyze", ...args], { cwd: ctx.projectRoot });
1043
+ if (run.exitCode !== 0) {
1044
+ throw new FetError({
1045
+ code: "GRAPH_COMMAND_FAILED" /* GraphCommandFailed */,
1046
+ message: "GitNexus analyze failed.",
1047
+ details: { command: run.command.join(" "), exitCode: run.exitCode, stdout: run.stdout, stderr: run.stderr },
1048
+ suggestedCommand: "fet graph doctor"
1049
+ });
1050
+ }
1051
+ const result = await refreshGraphState(ctx, { write: false });
1052
+ const global = await ctx.stateStore.getOrCreateGlobal();
1053
+ global.graph ??= {};
1054
+ global.graph.gitnexus = {
1055
+ ...result.state,
1056
+ lastRefreshAt: (/* @__PURE__ */ new Date()).toISOString()
1057
+ };
1058
+ await ctx.stateStore.writeGlobal(global);
1059
+ ctx.output.result({
1060
+ ok: true,
1061
+ command: `graph ${mode}`,
1062
+ summary: mode === "init" ? "GitNexus graph initialized." : "GitNexus graph refreshed.",
1063
+ warnings: result.state.graphExists ? [] : ["GitNexus analyze completed, but the configured graph directory was not found."],
1064
+ nextSteps: ["Run fet graph status", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"],
1065
+ data: {
1066
+ gitnexus: global.graph.gitnexus,
1067
+ run: {
1068
+ command: run.command,
1069
+ stdout: run.stdout.trim(),
1070
+ stderr: run.stderr.trim()
1071
+ }
1072
+ }
1073
+ });
1074
+ }
1075
+ async function refreshGraphState(ctx, options = {}) {
1076
+ const global = await ctx.stateStore.getOrCreateGlobal();
1077
+ global.graph ??= {};
1078
+ const detection = await detectGitNexus();
1079
+ const graph2 = await inspectGitNexusGraph(ctx.projectRoot);
1080
+ let state = mergeGitNexusGraphInfo(toGitNexusState(detection, global.graph.gitnexus), graph2);
1081
+ let gitnexusStatus = null;
1082
+ if (options.runStatus && detection.installed) {
1083
+ gitnexusStatus = await runGitNexus(["status"], { cwd: ctx.projectRoot });
1084
+ state = {
1085
+ ...state,
1086
+ lastStatus: firstLine(gitnexusStatus.stdout) || firstLine(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
1087
+ };
1088
+ }
1089
+ if (options.write ?? true) {
1090
+ global.graph.gitnexus = state;
1091
+ await ctx.stateStore.writeGlobal(global);
1092
+ }
1093
+ return {
1094
+ state,
1095
+ gitnexusStatus: gitnexusStatus ? {
1096
+ exitCode: gitnexusStatus.exitCode,
1097
+ command: gitnexusStatus.command,
1098
+ stdout: gitnexusStatus.stdout.trim(),
1099
+ stderr: gitnexusStatus.stderr.trim()
1100
+ } : null
1101
+ };
1102
+ }
1103
+ async function writeHandoffFile(path, content) {
1104
+ await mkdir4(dirname6(path), { recursive: true });
1105
+ await atomicWrite(path, content);
1106
+ }
1107
+ function renderGraphSetupHandoff(state) {
1108
+ return `<!-- FET:MANAGED
1109
+ schemaVersion: 1
1110
+ generator: graph-setup
1111
+ FET:END -->
1112
+
1113
+ # FET Graph Setup
1114
+
1115
+ GitNexus graph support is optional. FET does not install GitNexus automatically and does not require graph support for OpenSpec workflows.
1116
+
1117
+ Current status:
1118
+
1119
+ - Installed: ${state.installed ? "yes" : "no"}
1120
+ - Executable: ${state.executablePath ?? "gitnexus"}
1121
+ - Version: ${state.version ?? "unknown"}
1122
+ - Graph path: ${state.graphPath ?? ".gitnexus"}
1123
+ - Graph exists: ${state.graphExists ? "yes" : "no"}
1124
+
1125
+ Suggested setup flow:
1126
+
1127
+ 1. If GitNexus is not installed, install it using the method recommended by the GitNexus project.
1128
+ 2. If you want GitNexus MCP or IDE integration, run \`gitnexus setup\` yourself after reviewing what it changes.
1129
+ 3. Return to this project and run \`fet graph init\` to build the first graph.
1130
+ 4. Run \`fet graph handoff\` so IDE AI can prefer graph context before broad repository scans.
1131
+
1132
+ Guardrails:
1133
+
1134
+ - Do not block FET/OpenSpec commands when GitNexus is unavailable.
1135
+ - Do not generate or modify application code during setup.
1136
+ - Do not run global IDE configuration commands unless the user explicitly approves them.
733
1137
  `;
734
1138
  }
1139
+ function renderGraphUsageHandoff(state) {
1140
+ return `<!-- FET:MANAGED
1141
+ schemaVersion: 1
1142
+ generator: graph-handoff
1143
+ FET:END -->
1144
+
1145
+ # FET Graph Handoff
1146
+
1147
+ Use GitNexus graph context as an optional first pass before broad repository scans.
1148
+
1149
+ Current status:
1150
+
1151
+ - Installed: ${state.installed ? "yes" : "no"}
1152
+ - Graph path: ${state.graphPath ?? ".gitnexus"}
1153
+ - Graph exists: ${state.graphExists ? "yes" : "no"}
1154
+ - Last indexed at: ${state.lastIndexedAt ?? "unknown"}
1155
+ - Last status: ${state.lastStatus ?? "unknown"}
1156
+
1157
+ When graph context is available:
1158
+
1159
+ 1. Use the graph to identify likely modules, dependencies, and insertion points.
1160
+ 2. Read only the concrete source files needed to confirm behavior.
1161
+ 3. Prefer OpenSpec artifacts and AGENTS.md over graph guesses when they conflict.
1162
+ 4. Fall back to normal repository inspection if the graph is missing, stale, or incomplete.
1163
+
1164
+ When producing OpenSpec artifacts:
1165
+
1166
+ - Use graph context to make proposal, design, specs, and tasks more precise.
1167
+ - Avoid large repository scans when the graph already narrows the relevant area.
1168
+ - Keep all generated artifacts in the normal OpenSpec change directory.
1169
+ `;
1170
+ }
1171
+ function firstLine(value) {
1172
+ return value.trim().split(/\r?\n/)[0]?.trim() || null;
1173
+ }
735
1174
 
736
1175
  // src/commands/proxy.ts
737
1176
  import { readFile as readFile10 } from "fs/promises";
738
- import { join as join11 } from "path";
1177
+ import { join as join13 } from "path";
739
1178
 
740
1179
  // src/state/project.ts
741
1180
  import { execFile as execFile2 } from "child_process";
@@ -764,8 +1203,8 @@ async function git(cwd, args) {
764
1203
  }
765
1204
 
766
1205
  // src/state/store.ts
767
- import { mkdir as mkdir4, readFile as readFile8 } from "fs/promises";
768
- import { join as join10 } from "path";
1206
+ import { mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
1207
+ import { join as join12 } from "path";
769
1208
 
770
1209
  // src/state/schema.ts
771
1210
  var phases = ["explore", "propose", "implement", "verify", "sync", "archive"];
@@ -874,7 +1313,7 @@ var StateStore = class {
874
1313
  }
875
1314
  async writeGlobal(state) {
876
1315
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
877
- await mkdir4(join10(this.projectRoot, "openspec"), { recursive: true });
1316
+ await mkdir5(join12(this.projectRoot, "openspec"), { recursive: true });
878
1317
  await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
879
1318
  `);
880
1319
  }
@@ -895,15 +1334,15 @@ var StateStore = class {
895
1334
  }
896
1335
  async writeChange(state) {
897
1336
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
898
- await mkdir4(join10(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
1337
+ await mkdir5(join12(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
899
1338
  await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
900
1339
  `);
901
1340
  }
902
1341
  globalPath() {
903
- return join10(this.projectRoot, "openspec", "fet-state.json");
1342
+ return join12(this.projectRoot, "openspec", "fet-state.json");
904
1343
  }
905
1344
  changePath(changeId) {
906
- return join10(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
1345
+ return join12(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
907
1346
  }
908
1347
  };
909
1348
  function isNotFound(error) {
@@ -1020,7 +1459,7 @@ async function createChangelogEntry(projectRoot, changeId) {
1020
1459
  };
1021
1460
  }
1022
1461
  async function appendChangelog(projectRoot, entry) {
1023
- const changelogPath = join11(projectRoot, "CHANGELOG.md");
1462
+ const changelogPath = join13(projectRoot, "CHANGELOG.md");
1024
1463
  const existing = await readOptional3(changelogPath);
1025
1464
  const block = `updateTime: ${entry.updateTime}
1026
1465
  \u66F4\u65B0\u5185\u5BB9:${entry.content}
@@ -1031,12 +1470,12 @@ ${block}` : block;
1031
1470
  await atomicWrite(changelogPath, next);
1032
1471
  }
1033
1472
  async function readChangeRequirement(projectRoot, changeId) {
1034
- const changeRoot = join11(projectRoot, "openspec", "changes", changeId);
1035
- const proposal = await readOptional3(join11(changeRoot, "proposal.md"));
1473
+ const changeRoot = join13(projectRoot, "openspec", "changes", changeId);
1474
+ const proposal = await readOptional3(join13(changeRoot, "proposal.md"));
1036
1475
  if (proposal) {
1037
1476
  return summarizeMarkdown(proposal);
1038
1477
  }
1039
- const readme = await readOptional3(join11(changeRoot, "README.md"));
1478
+ const readme = await readOptional3(join13(changeRoot, "README.md"));
1040
1479
  if (readme) {
1041
1480
  return summarizeMarkdown(readme);
1042
1481
  }
@@ -1180,8 +1619,8 @@ async function assertVerified(ctx) {
1180
1619
 
1181
1620
  // src/commands/verify.ts
1182
1621
  import { createHash } from "crypto";
1183
- import { mkdir as mkdir5, readFile as readFile11, stat as stat4 } from "fs/promises";
1184
- import { join as join12 } from "path";
1622
+ import { mkdir as mkdir6, readFile as readFile11, stat as stat5 } from "fs/promises";
1623
+ import { join as join14 } from "path";
1185
1624
  async function verifyCommand(ctx, options) {
1186
1625
  if (options.auto) {
1187
1626
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
@@ -1248,9 +1687,9 @@ async function verifyCommand(ctx, options) {
1248
1687
  async function writeInstructions(ctx, changeId) {
1249
1688
  await assertChangeExists(ctx, changeId);
1250
1689
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1251
- const dir = join12(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
1252
- const instructionsPath = join12(dir, "verify-instructions.md");
1253
- await mkdir5(dir, { recursive: true });
1690
+ const dir = join14(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
1691
+ const instructionsPath = join14(dir, "verify-instructions.md");
1692
+ await mkdir6(dir, { recursive: true });
1254
1693
  await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
1255
1694
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
1256
1695
  state.currentPhase = "verify";
@@ -1266,7 +1705,7 @@ async function writeInstructions(ctx, changeId) {
1266
1705
  async function markDone(ctx, changeId) {
1267
1706
  await assertChangeExists(ctx, changeId);
1268
1707
  const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
1269
- const instructionsPath = join12(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
1708
+ const instructionsPath = join14(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
1270
1709
  const instructions = await readInstructions(instructionsPath, changeId);
1271
1710
  const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
1272
1711
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
@@ -1301,7 +1740,7 @@ async function assertChangeExists(ctx, changeId) {
1301
1740
  }
1302
1741
  async function readInstructions(path, changeId) {
1303
1742
  try {
1304
- await stat4(path);
1743
+ await stat5(path);
1305
1744
  const content = await readFile11(path, "utf8");
1306
1745
  const fileChangeId = readFrontMatterValue(content, "changeId");
1307
1746
  if (fileChangeId !== changeId) {
@@ -1419,9 +1858,9 @@ function renderIdeModelPolicy(command) {
1419
1858
  import { resolve } from "path";
1420
1859
 
1421
1860
  // src/adapters/codex/index.ts
1422
- import { mkdir as mkdir6, readFile as readFile12, stat as stat5 } from "fs/promises";
1861
+ import { mkdir as mkdir7, readFile as readFile12, stat as stat6 } from "fs/promises";
1423
1862
  import { homedir } from "os";
1424
- import { dirname as dirname6, join as join13 } from "path";
1863
+ import { dirname as dirname7, join as join15 } from "path";
1425
1864
 
1426
1865
  // src/adapters/commands.ts
1427
1866
  var FET_WORKFLOW_COMMANDS = [
@@ -1437,7 +1876,18 @@ var FET_WORKFLOW_COMMANDS = [
1437
1876
  "bulk-archive",
1438
1877
  "onboard"
1439
1878
  ];
1440
- var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "fill-context", "passthrough"];
1879
+ var FET_GRAPH_COMMANDS = ["graph-status", "graph-setup", "graph-init", "graph-refresh", "graph-doctor", "graph-handoff"];
1880
+ var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "fill-context", "passthrough", ...FET_GRAPH_COMMANDS];
1881
+ function renderFetAdapterUsage(command, args = "[...args]") {
1882
+ if (command.startsWith("graph-")) {
1883
+ const subcommand = command.slice("graph-".length);
1884
+ return `fet graph ${subcommand}${args ? ` ${args}` : ""}`;
1885
+ }
1886
+ if (command === "passthrough") {
1887
+ return `fet passthrough <openspec-command>${args ? ` ${args}` : ""}`;
1888
+ }
1889
+ return `fet ${command}${args ? ` ${args}` : ""}`;
1890
+ }
1441
1891
 
1442
1892
  // src/adapters/codex/templates.ts
1443
1893
  function codexGuideFile() {
@@ -1456,6 +1906,7 @@ Before doing FET or OpenSpec work in Codex, read:
1456
1906
 
1457
1907
  - AGENTS.md
1458
1908
  - openspec/config.yaml
1909
+ - .codex/fet/karpathy-guidelines.md
1459
1910
  - the active change files under openspec/changes/<change-id>/, when a change is selected
1460
1911
 
1461
1912
  If GitNexus code graph context is available in the IDE or MCP tools, prefer it before broad repository scans. Use it to identify relevant modules, dependencies, and insertion points, then read only the concrete source files needed. If GitNexus is unavailable, continue with the normal FET/OpenSpec workflow.
@@ -1467,10 +1918,13 @@ Command guides live in .codex/fet/commands/.
1467
1918
  };
1468
1919
  }
1469
1920
  function codexCommandFiles() {
1470
- return FET_ADAPTER_COMMANDS.map((command) => ({
1471
- path: `.codex/fet/commands/${command}.md`,
1472
- content: renderCommand(command)
1473
- }));
1921
+ return [
1922
+ codexKarpathyGuidelinesFile(),
1923
+ ...FET_ADAPTER_COMMANDS.map((command) => ({
1924
+ path: `.codex/fet/commands/${command}.md`,
1925
+ content: renderCommand(command)
1926
+ }))
1927
+ ];
1474
1928
  }
1475
1929
  function codexSlashPromptFiles() {
1476
1930
  return FET_ADAPTER_COMMANDS.map((command) => ({
@@ -1478,6 +1932,22 @@ function codexSlashPromptFiles() {
1478
1932
  content: renderSlashPrompt(command)
1479
1933
  }));
1480
1934
  }
1935
+ function codexKarpathyGuidelinesFile() {
1936
+ return {
1937
+ path: ".codex/fet/karpathy-guidelines.md",
1938
+ content: `<!-- FET:MANAGED
1939
+ schemaVersion: 1
1940
+ fetVersion: ${FET_VERSION}
1941
+ generator: codex-adapter
1942
+ adapterVersion: 1
1943
+ FET:END -->
1944
+
1945
+ # Andrej Karpathy Inspired Coding Guidelines
1946
+
1947
+ ${renderKarpathyGuidelinesBody()}
1948
+ `
1949
+ };
1950
+ }
1481
1951
  function renderCommand(command) {
1482
1952
  if (command === "fill-context") {
1483
1953
  return renderFillContextCommand();
@@ -1485,26 +1955,32 @@ function renderCommand(command) {
1485
1955
  if (command === "passthrough") {
1486
1956
  return renderPassthroughCommand();
1487
1957
  }
1958
+ if (command.startsWith("graph-")) {
1959
+ return renderGraphCommand(command);
1960
+ }
1961
+ const usage = renderFetAdapterUsage(command, "");
1488
1962
  return `<!-- FET:MANAGED
1489
1963
  schemaVersion: 1
1490
1964
  fetVersion: ${FET_VERSION}
1491
1965
  generator: codex-adapter
1492
1966
  adapterVersion: 1
1493
- command: fet ${command}
1967
+ command: ${usage}
1494
1968
  FET:END -->
1495
1969
 
1496
- # fet ${command}
1970
+ # ${usage}
1497
1971
 
1498
1972
  ${renderIdeModelPolicy(command)}
1499
1973
 
1500
1974
  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.
1501
1975
 
1976
+ Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
1977
+
1502
1978
  If GitNexus graph context is available, consult it before broad source scans and use it to narrow the files you read. If it is unavailable, continue normally.
1503
1979
 
1504
1980
  Then run:
1505
1981
 
1506
1982
  \`\`\`sh
1507
- fet ${command}
1983
+ ${usage}
1508
1984
  \`\`\`
1509
1985
 
1510
1986
  If the command needs a change id, pass it with \`--change <change-id>\` or use the active OpenSpec change from the user's request.
@@ -1527,6 +2003,8 @@ ${renderIdeModelPolicy("passthrough")}
1527
2003
 
1528
2004
  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.
1529
2005
 
2006
+ Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
2007
+
1530
2008
  If GitNexus graph context is available, consult it before broad source scans and use it to narrow the files you read. If it is unavailable, continue normally.
1531
2009
 
1532
2010
  Then run:
@@ -1538,6 +2016,38 @@ fet passthrough <openspec-command> [...args]
1538
2016
  This preserves the FET entry point while allowing access to unmanaged or newly added OpenSpec commands. Passthrough does not update FET lifecycle state.
1539
2017
  `;
1540
2018
  }
2019
+ function renderGraphCommand(command) {
2020
+ const usage = renderFetAdapterUsage(command, "");
2021
+ const subcommand = command.slice("graph-".length);
2022
+ return `<!-- FET:MANAGED
2023
+ schemaVersion: 1
2024
+ fetVersion: ${FET_VERSION}
2025
+ generator: codex-adapter
2026
+ adapterVersion: 1
2027
+ command: ${usage}
2028
+ FET:END -->
2029
+
2030
+ # ${usage}
2031
+
2032
+ ${renderIdeModelPolicy(command)}
2033
+
2034
+ When the user asks Codex to work with optional GitNexus graph support, use FET as the entry point.
2035
+
2036
+ Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
2037
+
2038
+ If GitNexus graph context is available, consult it before broad source scans and use it to narrow the files you read. If it is unavailable, continue normally.
2039
+
2040
+ Run:
2041
+
2042
+ \`\`\`sh
2043
+ ${usage}
2044
+ \`\`\`
2045
+
2046
+ For graph init or refresh, pass extra GitNexus analyze arguments only when the user provides them.
2047
+
2048
+ After the command completes, report the GitNexus state, generated handoff files, and next steps.
2049
+ `;
2050
+ }
1541
2051
  function renderSlashPrompt(command) {
1542
2052
  if (command === "continue") {
1543
2053
  return renderContinueSlashPrompt();
@@ -1575,9 +2085,10 @@ function renderSlashPrompt(command) {
1575
2085
  if (command === "passthrough") {
1576
2086
  return renderPassthroughSlashPrompt();
1577
2087
  }
1578
- const usage = command === "passthrough" ? "fet passthrough <openspec-command> [...args]" : `fet ${command} [...args]`;
1579
- const shellCommand = command === "passthrough" ? "fet passthrough $ARGUMENTS" : `fet ${command} $ARGUMENTS`;
1580
- const description = command === "passthrough" ? "Run an unmanaged OpenSpec command through FET passthrough" : `Run the FET-managed OpenSpec ${command} workflow`;
2088
+ const usage = renderFetAdapterUsage(command);
2089
+ const isGraph = command.startsWith("graph-");
2090
+ const shellCommand = isGraph ? `${renderFetAdapterUsage(command, "")} $ARGUMENTS` : `fet ${command} $ARGUMENTS`;
2091
+ const description = isGraph ? `Run optional GitNexus graph ${command.slice("graph-".length)} through FET` : `Run the FET-managed OpenSpec ${command} workflow`;
1581
2092
  return `<!-- FET:MANAGED
1582
2093
  schemaVersion: 1
1583
2094
  fetVersion: ${FET_VERSION}
@@ -1595,6 +2106,8 @@ Use FET as the entry point for this OpenSpec workflow.
1595
2106
 
1596
2107
  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.
1597
2108
 
2109
+ Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
2110
+
1598
2111
  If GitNexus graph context is available, consult it before broad source scans and use it to narrow the files you read. If it is unavailable, continue normally.
1599
2112
 
1600
2113
  Run:
@@ -1621,6 +2134,8 @@ ${renderIdeModelPolicy("fill-context")}
1621
2134
 
1622
2135
  Use this command to complete FET-generated project context placeholders with Codex.
1623
2136
 
2137
+ Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
2138
+
1624
2139
  If GitNexus graph context is available, consult it before broad source scans and use it to narrow the files you read. If it is unavailable, continue normally.
1625
2140
 
1626
2141
  First run:
@@ -1645,16 +2160,17 @@ Steps:
1645
2160
  fet fill-context
1646
2161
  \`\`\`
1647
2162
  2. Read AGENTS.md and openspec/config.yaml.
1648
- 3. Inspect the project to understand:
2163
+ 3. Read .codex/fet/karpathy-guidelines.md.
2164
+ 4. Inspect the project to understand:
1649
2165
  - source structure and major modules
1650
2166
  - framework and routing conventions
1651
2167
  - scripts, test commands, and build commands
1652
2168
  - coding conventions and project-specific patterns
1653
2169
  - important docs such as README files
1654
- 4. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete, concise project-specific content.
1655
- 5. Preserve FET managed markers such as \`FET:BEGIN AUTO\`, \`FET:END AUTO\`, \`FET:BEGIN USER\`, and \`FET:END USER\`.
1656
- 6. Do not modify business code.
1657
- 7. Run:
2170
+ 5. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete, concise project-specific content.
2171
+ 6. Preserve FET managed markers such as \`FET:BEGIN AUTO\`, \`FET:END AUTO\`, \`FET:BEGIN USER\`, and \`FET:END USER\`.
2172
+ 7. Do not modify business code.
2173
+ 8. Run:
1658
2174
  \`\`\`sh
1659
2175
  fet doctor
1660
2176
  \`\`\`
@@ -2033,7 +2549,7 @@ var CodexAdapter = class {
2033
2549
  adapterVersion = 1;
2034
2550
  async detect(projectRoot) {
2035
2551
  return {
2036
- detected: await exists3(join13(projectRoot, ".codex")) || await exists3(join13(projectRoot, "AGENTS.md")),
2552
+ detected: await exists3(join15(projectRoot, ".codex")) || await exists3(join15(projectRoot, "AGENTS.md")),
2037
2553
  reason: "Codex adapter is available for projects that use AGENTS.md"
2038
2554
  };
2039
2555
  }
@@ -2072,7 +2588,7 @@ var CodexAdapter = class {
2072
2588
  if (existing && !existing.includes("FET:MANAGED") && force) {
2073
2589
  await createBackup(target);
2074
2590
  }
2075
- await mkdir6(dirname6(target), { recursive: true });
2591
+ await mkdir7(dirname7(target), { recursive: true });
2076
2592
  await atomicWrite(target, file.content);
2077
2593
  written.push(displayPath);
2078
2594
  }
@@ -2099,9 +2615,9 @@ var CodexAdapter = class {
2099
2615
  };
2100
2616
  function resolveTarget(projectRoot, file) {
2101
2617
  if (file.root === "codex-home") {
2102
- return join13(resolveCodexHome(), file.path);
2618
+ return join15(resolveCodexHome(), file.path);
2103
2619
  }
2104
- return join13(projectRoot, file.path);
2620
+ return join15(projectRoot, file.path);
2105
2621
  }
2106
2622
  function displayPathFor(file) {
2107
2623
  if (file.root === "codex-home") {
@@ -2110,7 +2626,7 @@ function displayPathFor(file) {
2110
2626
  return file.path;
2111
2627
  }
2112
2628
  function resolveCodexHome() {
2113
- return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join13(homedir(), ".codex");
2629
+ return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join15(homedir(), ".codex");
2114
2630
  }
2115
2631
  async function readExisting(path) {
2116
2632
  try {
@@ -2121,7 +2637,7 @@ async function readExisting(path) {
2121
2637
  }
2122
2638
  async function exists3(path) {
2123
2639
  try {
2124
- await stat5(path);
2640
+ await stat6(path);
2125
2641
  return true;
2126
2642
  } catch {
2127
2643
  return false;
@@ -2129,8 +2645,8 @@ async function exists3(path) {
2129
2645
  }
2130
2646
 
2131
2647
  // src/adapters/cursor/index.ts
2132
- import { mkdir as mkdir7, readFile as readFile13, stat as stat6 } from "fs/promises";
2133
- import { dirname as dirname7, join as join14 } from "path";
2648
+ import { mkdir as mkdir8, readFile as readFile13, stat as stat7 } from "fs/promises";
2649
+ import { dirname as dirname8, join as join16 } from "path";
2134
2650
 
2135
2651
  // src/adapters/cursor/templates.ts
2136
2652
  function cursorSkillFiles() {
@@ -2166,7 +2682,7 @@ alwaysApply: false
2166
2682
  };
2167
2683
  }
2168
2684
  function renderSkill(command) {
2169
- const usage = command === "passthrough" ? "fet passthrough <openspec-command> [...args]" : `fet ${command}`;
2685
+ const usage = renderFetAdapterUsage(command, command === "passthrough" ? "[...args]" : "");
2170
2686
  if (command === "fill-context") {
2171
2687
  return `<!-- FET:MANAGED
2172
2688
  schemaVersion: 1
@@ -2232,7 +2748,7 @@ var CursorAdapter = class {
2232
2748
  adapterVersion = 1;
2233
2749
  async detect(projectRoot) {
2234
2750
  return {
2235
- detected: await exists4(join14(projectRoot, ".cursor")),
2751
+ detected: await exists4(join16(projectRoot, ".cursor")),
2236
2752
  reason: "Cursor adapter is available for any project"
2237
2753
  };
2238
2754
  }
@@ -2249,7 +2765,7 @@ var CursorAdapter = class {
2249
2765
  const written = [];
2250
2766
  const skipped = [];
2251
2767
  for (const file of plan.files) {
2252
- const target = join14(projectRoot, file.path);
2768
+ const target = join16(projectRoot, file.path);
2253
2769
  const existing = await readExisting2(target);
2254
2770
  if (existing && !existing.includes("FET:MANAGED") && !force) {
2255
2771
  throw new FetError({
@@ -2262,7 +2778,7 @@ var CursorAdapter = class {
2262
2778
  if (existing && !existing.includes("FET:MANAGED") && force) {
2263
2779
  await createBackup(target);
2264
2780
  }
2265
- await mkdir7(dirname7(target), { recursive: true });
2781
+ await mkdir8(dirname8(target), { recursive: true });
2266
2782
  await atomicWrite(target, file.content);
2267
2783
  written.push(file.path);
2268
2784
  }
@@ -2272,7 +2788,7 @@ var CursorAdapter = class {
2272
2788
  const plan = await this.planInstall(projectRoot);
2273
2789
  const checks = [];
2274
2790
  for (const file of plan.files) {
2275
- const target = join14(projectRoot, file.path);
2791
+ const target = join16(projectRoot, file.path);
2276
2792
  const content = await readExisting2(target);
2277
2793
  const managed = Boolean(content?.includes("FET:MANAGED"));
2278
2794
  const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
@@ -2295,7 +2811,7 @@ async function readExisting2(path) {
2295
2811
  }
2296
2812
  async function exists4(path) {
2297
2813
  try {
2298
- await stat6(path);
2814
+ await stat7(path);
2299
2815
  return true;
2300
2816
  } catch {
2301
2817
  return false;
@@ -2307,13 +2823,13 @@ import { execFile as execFile4 } from "child_process";
2307
2823
  import { promisify as promisify4 } from "util";
2308
2824
 
2309
2825
  // src/openspec/inspector.ts
2310
- import { readdir, stat as stat7 } from "fs/promises";
2311
- import { join as join15 } from "path";
2826
+ import { readdir, stat as stat8 } from "fs/promises";
2827
+ import { join as join17 } from "path";
2312
2828
  async function inspectOpenSpecProject(projectRoot) {
2313
- const openspecPath = join15(projectRoot, "openspec");
2314
- const changesPath = join15(openspecPath, "changes");
2315
- const legacyArchivePath = join15(openspecPath, "archive");
2316
- const changesArchivePath = join15(changesPath, "archive");
2829
+ const openspecPath = join17(projectRoot, "openspec");
2830
+ const changesPath = join17(openspecPath, "changes");
2831
+ const legacyArchivePath = join17(openspecPath, "archive");
2832
+ const changesArchivePath = join17(changesPath, "archive");
2317
2833
  return {
2318
2834
  exists: await exists5(openspecPath),
2319
2835
  changes: await listDirectories(changesPath, { exclude: ["archive"] }),
@@ -2321,13 +2837,13 @@ async function inspectOpenSpecProject(projectRoot) {
2321
2837
  };
2322
2838
  }
2323
2839
  async function inspectOpenSpecChange(projectRoot, changeId) {
2324
- const changePath = join15(projectRoot, "openspec", "changes", changeId);
2325
- const tasksPath = join15(changePath, "tasks.md");
2326
- const specsPath = join15(changePath, "specs");
2840
+ const changePath = join17(projectRoot, "openspec", "changes", changeId);
2841
+ const tasksPath = join17(changePath, "tasks.md");
2842
+ const specsPath = join17(changePath, "specs");
2327
2843
  return {
2328
2844
  changeId,
2329
2845
  exists: await exists5(changePath),
2330
- hasProposal: await exists5(join15(changePath, "proposal.md")),
2846
+ hasProposal: await exists5(join17(changePath, "proposal.md")),
2331
2847
  hasTasks: await exists5(tasksPath),
2332
2848
  hasSpecs: await exists5(specsPath),
2333
2849
  tasksPath,
@@ -2345,7 +2861,7 @@ async function listDirectories(path, options = {}) {
2345
2861
  }
2346
2862
  async function exists5(path) {
2347
2863
  try {
2348
- await stat7(path);
2864
+ await stat8(path);
2349
2865
  return true;
2350
2866
  } catch {
2351
2867
  return false;
@@ -2504,12 +3020,12 @@ function parseCommands(help) {
2504
3020
  }
2505
3021
 
2506
3022
  // src/scanner/package.ts
2507
- import { readFile as readFile14, stat as stat8 } from "fs/promises";
2508
- import { join as join16 } from "path";
3023
+ import { readFile as readFile14, stat as stat9 } from "fs/promises";
3024
+ import { join as join18 } from "path";
2509
3025
  import { parse as parse2 } from "yaml";
2510
3026
  async function readPackageJson(projectRoot) {
2511
3027
  try {
2512
- return JSON.parse(await readFile14(join16(projectRoot, "package.json"), "utf8"));
3028
+ return JSON.parse(await readFile14(join18(projectRoot, "package.json"), "utf8"));
2513
3029
  } catch {
2514
3030
  return null;
2515
3031
  }
@@ -2575,7 +3091,7 @@ function detectFramework(pkg) {
2575
3091
  }
2576
3092
  async function detectLanguage(projectRoot, pkg) {
2577
3093
  const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
2578
- if (deps.typescript || await exists6(join16(projectRoot, "tsconfig.json"))) {
3094
+ if (deps.typescript || await exists6(join18(projectRoot, "tsconfig.json"))) {
2579
3095
  return "typescript";
2580
3096
  }
2581
3097
  return "javascript";
@@ -2590,7 +3106,7 @@ async function detectWorkspaces(projectRoot, pkg) {
2590
3106
  return packageWorkspaces;
2591
3107
  }
2592
3108
  try {
2593
- const workspace = parse2(await readFile14(join16(projectRoot, "pnpm-workspace.yaml"), "utf8"));
3109
+ const workspace = parse2(await readFile14(join18(projectRoot, "pnpm-workspace.yaml"), "utf8"));
2594
3110
  return (workspace?.packages ?? []).map((path) => ({
2595
3111
  name: path,
2596
3112
  path,
@@ -2610,7 +3126,7 @@ async function detectLockManagers(projectRoot) {
2610
3126
  ];
2611
3127
  const found = [];
2612
3128
  for (const [file, manager] of lockFiles) {
2613
- if (await exists6(join16(projectRoot, file))) {
3129
+ if (await exists6(join18(projectRoot, file))) {
2614
3130
  found.push(manager);
2615
3131
  }
2616
3132
  }
@@ -2627,7 +3143,7 @@ function scriptCommand(packageManager, name) {
2627
3143
  }
2628
3144
  async function exists6(path) {
2629
3145
  try {
2630
- await stat8(path);
3146
+ await stat9(path);
2631
3147
  return true;
2632
3148
  } catch {
2633
3149
  return false;
@@ -2635,13 +3151,13 @@ async function exists6(path) {
2635
3151
  }
2636
3152
 
2637
3153
  // src/scanner/routes.ts
2638
- import { readdir as readdir2, stat as stat9 } from "fs/promises";
2639
- import { join as join17, relative, sep } from "path";
3154
+ import { readdir as readdir2, stat as stat10 } from "fs/promises";
3155
+ import { join as join19, relative, sep } from "path";
2640
3156
  async function scanRoutes(projectRoot) {
2641
3157
  const candidates = ["src/routes", "src/pages", "app", "pages"];
2642
3158
  const routes = [];
2643
3159
  for (const candidate of candidates) {
2644
- const root = join17(projectRoot, candidate);
3160
+ const root = join19(projectRoot, candidate);
2645
3161
  if (!await exists7(root)) {
2646
3162
  continue;
2647
3163
  }
@@ -2669,7 +3185,7 @@ async function listFiles(root) {
2669
3185
  const entries = await readdir2(root, { withFileTypes: true });
2670
3186
  const files = [];
2671
3187
  for (const entry of entries) {
2672
- const path = join17(root, entry.name);
3188
+ const path = join19(root, entry.name);
2673
3189
  if (entry.isDirectory()) {
2674
3190
  files.push(...await listFiles(path));
2675
3191
  } else {
@@ -2680,7 +3196,7 @@ async function listFiles(root) {
2680
3196
  }
2681
3197
  async function exists7(path) {
2682
3198
  try {
2683
- await stat9(path);
3199
+ await stat10(path);
2684
3200
  return true;
2685
3201
  } catch {
2686
3202
  return false;
@@ -2827,6 +3343,13 @@ program.name("fet").description("Frontend workflow orchestration tool built arou
2827
3343
  addGlobalOptions(program.command("init")).description("\u521D\u59CB\u5316 FET + OpenSpec").action(wrap("init", initCommand));
2828
3344
  addGlobalOptions(program.command("update-context")).description("\u66F4\u65B0\u9879\u76EE\u4E0A\u4E0B\u6587").action(wrap("update-context", updateContextCommand));
2829
3345
  addGlobalOptions(program.command("fill-context")).description("Refresh IDE prompts for filling AGENTS.md placeholders").action(wrap("fill-context", fillContextCommand));
3346
+ var graph = addGlobalOptions(program.command("graph").description("Manage optional GitNexus code graph support"));
3347
+ for (const action of ["status", "setup", "doctor", "handoff"]) {
3348
+ addGlobalOptions(graph.command(action).description(`Run fet graph ${action}`)).action(wrap("graph", (ctx) => graphCommand(ctx, action)));
3349
+ }
3350
+ for (const action of ["init", "refresh"]) {
3351
+ addGlobalOptions(graph.command(`${action} [args...]`).description(`Run GitNexus analyze for graph ${action}`).allowUnknownOption(true).passThroughOptions()).action(wrap("graph", (ctx, args = []) => graphCommand(ctx, action, args)));
3352
+ }
2830
3353
  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(
2831
3354
  wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
2832
3355
  );