@nick848/fet 1.0.6 → 1.0.8

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
@@ -8,9 +8,28 @@ import {
8
8
  import { createInterface } from "readline/promises";
9
9
  import { Command } from "commander";
10
10
 
11
- // src/commands/init.ts
12
- import { readFile as readFile6, stat as stat3 } from "fs/promises";
13
- import { join as join8 } from "path";
11
+ // src/commands/doctor.ts
12
+ import { readFile as readFile4, stat as stat3 } from "fs/promises";
13
+ import { join as join6 } from "path";
14
+
15
+ // src/context-placeholders.ts
16
+ import { readFile } from "fs/promises";
17
+ import { join } from "path";
18
+ var AGENTS_LLM_PLACEHOLDER_PATTERN = /\[NEEDS? LLM INPUT\]/g;
19
+ async function countAgentsLlmPlaceholders(projectRoot) {
20
+ try {
21
+ const content = await readFile(join(projectRoot, "AGENTS.md"), "utf8");
22
+ return [...content.matchAll(AGENTS_LLM_PLACEHOLDER_PATTERN)].length;
23
+ } catch {
24
+ return 0;
25
+ }
26
+ }
27
+ function renderAgentsPlaceholderWarning(count2, language = "zh-CN") {
28
+ if (language === "en") {
29
+ return `AGENTS.md still contains ${count2} LLM placeholder(s). Run fet fill-context first so your IDE AI can replace them. Continuing current command.`;
30
+ }
31
+ return `AGENTS.md \u4ECD\u6709 ${count2} \u4E2A LLM \u5360\u4F4D\u7B26\u3002\u5EFA\u8BAE\u5148\u8FD0\u884C fet fill-context\uFF0C\u8BA9 IDE AI \u8865\u9F50\u9879\u76EE\u4E0A\u4E0B\u6587\uFF1B\u5F53\u524D\u547D\u4EE4\u4F1A\u7EE7\u7EED\u6267\u884C\u3002`;
32
+ }
14
33
 
15
34
  // src/fs/atomic-write.ts
16
35
  import { dirname } from "path";
@@ -31,7 +50,7 @@ async function atomicWrite(targetPath, content) {
31
50
 
32
51
  // src/fs/backup.ts
33
52
  import { copyFile, stat } from "fs/promises";
34
- import { basename, dirname as dirname2, join } from "path";
53
+ import { basename, dirname as dirname2, join as join2 } from "path";
35
54
  async function createBackup(filePath) {
36
55
  try {
37
56
  await stat(filePath);
@@ -39,17 +58,17 @@ async function createBackup(filePath) {
39
58
  return null;
40
59
  }
41
60
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "-");
42
- const backupPath = join(dirname2(filePath), `${basename(filePath)}.fet-backup-${timestamp}`);
61
+ const backupPath = join2(dirname2(filePath), `${basename(filePath)}.fet-backup-${timestamp}`);
43
62
  await copyFile(filePath, backupPath);
44
63
  return backupPath;
45
64
  }
46
65
 
47
66
  // src/fs/lock.ts
48
67
  import { hostname } from "os";
49
- import { dirname as dirname3, join as join2 } from "path";
50
- import { mkdir as mkdir2, open as open2, readFile, rm } from "fs/promises";
68
+ import { dirname as dirname3, join as join3 } from "path";
69
+ import { mkdir as mkdir2, open as open2, readFile as readFile2, rm } from "fs/promises";
51
70
  async function withProjectLock(projectRoot, metadata, fn) {
52
- const lockPath = join2(projectRoot, "openspec", ".fet.lock");
71
+ const lockPath = join3(projectRoot, "openspec", ".fet.lock");
53
72
  const lock = {
54
73
  pid: process.pid,
55
74
  hostname: hostname(),
@@ -81,13 +100,13 @@ async function withProjectLock(projectRoot, metadata, fn) {
81
100
  }
82
101
  async function readExistingLock(lockPath) {
83
102
  try {
84
- return JSON.parse(await readFile(lockPath, "utf8"));
103
+ return JSON.parse(await readFile2(lockPath, "utf8"));
85
104
  } catch {
86
105
  return { path: lockPath };
87
106
  }
88
107
  }
89
108
  async function clearLock(projectRoot) {
90
- const lockPath = join2(projectRoot, "openspec", ".fet.lock");
109
+ const lockPath = join3(projectRoot, "openspec", ".fet.lock");
91
110
  try {
92
111
  await rm(lockPath);
93
112
  return true;
@@ -97,8 +116,8 @@ async function clearLock(projectRoot) {
97
116
  }
98
117
 
99
118
  // src/fs/journal.ts
100
- import { join as join3 } from "path";
101
- import { readFile as readFile2 } from "fs/promises";
119
+ import { join as join4 } from "path";
120
+ import { readFile as readFile3 } from "fs/promises";
102
121
  function createInitJournal(fetVersion) {
103
122
  return {
104
123
  schemaVersion: 1,
@@ -110,7 +129,7 @@ function createInitJournal(fetVersion) {
110
129
  }
111
130
  async function writeInitJournal(projectRoot, journal) {
112
131
  await atomicWrite(
113
- join3(projectRoot, "openspec", ".fet-init-journal.json"),
132
+ join4(projectRoot, "openspec", ".fet-init-journal.json"),
114
133
  `${JSON.stringify(journal, null, 2)}
115
134
  `
116
135
  );
@@ -119,7 +138,7 @@ async function writeInitJournal(projectRoot, journal) {
119
138
  // src/gitnexus.ts
120
139
  import { execFile } from "child_process";
121
140
  import { stat as stat2 } from "fs/promises";
122
- import { join as join4 } from "path";
141
+ import { join as join5 } from "path";
123
142
  import { promisify } from "util";
124
143
  var execFileAsync = promisify(execFile);
125
144
  var DEFAULT_GRAPH_PATH = ".gitnexus";
@@ -165,7 +184,7 @@ function toGitNexusState(detection, previous) {
165
184
  }
166
185
  async function inspectGitNexusGraph(projectRoot, env = process.env) {
167
186
  const relative2 = env.FET_GITNEXUS_GRAPH_PATH?.trim() || DEFAULT_GRAPH_PATH;
168
- const graphPath = join4(projectRoot, relative2);
187
+ const graphPath = join5(projectRoot, relative2);
169
188
  try {
170
189
  const info = await stat2(graphPath);
171
190
  return {
@@ -213,11 +232,17 @@ function mergeGitNexusGraphInfo(state, graph2) {
213
232
  lastIndexedAt: graph2.lastIndexedAt
214
233
  };
215
234
  }
216
- function renderGitNexusRecommendation(state) {
235
+ function renderGitNexusRecommendation(state, language = "zh-CN") {
236
+ if (language === "en") {
237
+ if (state.installed) {
238
+ return `Optional GitNexus detected (${state.version ?? "unknown"}). You can generate a code graph after init; future OpenSpec artifacts should prefer the graph when it is available.`;
239
+ }
240
+ return "Optional GitNexus code graph support is not installed. Consider installing GitNexus later to speed up OpenSpec artifact generation and improve code insertion context.";
241
+ }
217
242
  if (state.installed) {
218
- return `Optional GitNexus detected (${state.version ?? "unknown"}). You can generate a code graph after init; future OpenSpec artifacts should prefer the graph when it is available.`;
243
+ return `\u68C0\u6D4B\u5230\u53EF\u9009 GitNexus\uFF08${state.version ?? "unknown"}\uFF09\u3002\u521D\u59CB\u5316\u540E\u53EF\u4EE5\u751F\u6210\u4EE3\u7801\u56FE\uFF1B\u540E\u7EED OpenSpec \u4EA7\u7269\u5728\u56FE\u53EF\u7528\u65F6\u5E94\u4F18\u5148\u53C2\u8003\u5B83\u3002`;
219
244
  }
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.";
245
+ return "\u672A\u5B89\u88C5\u53EF\u9009\u7684 GitNexus \u4EE3\u7801\u56FE\u652F\u6301\u3002\u53EF\u4EE5\u7A0D\u540E\u5B89\u88C5 GitNexus\uFF0C\u4EE5\u52A0\u901F OpenSpec \u4EA7\u7269\u751F\u6210\u5E76\u6539\u5584\u4EE3\u7801\u63D2\u5165\u4E0A\u4E0B\u6587\u3002";
221
246
  }
222
247
  function resolveGitNexusCommand(env) {
223
248
  const raw = env.FET_GITNEXUS_COMMAND?.trim() || env.FET_GITNEXUS_EXECUTABLE?.trim() || "gitnexus";
@@ -230,946 +255,1180 @@ function splitCommand(value) {
230
255
  return (matches?.length ? matches : [value]).map((part) => part.replace(/^["']|["']$/g, ""));
231
256
  }
232
257
 
233
- // src/version.ts
234
- import { existsSync, readFileSync } from "fs";
235
- import { dirname as dirname4, join as join5, parse } from "path";
236
- import { fileURLToPath } from "url";
237
- var FET_VERSION = readPackageVersion();
238
- function readPackageVersion() {
239
- let currentDir = dirname4(fileURLToPath(import.meta.url));
240
- const root = parse(currentDir).root;
241
- while (true) {
242
- const packageJsonPath = join5(currentDir, "package.json");
243
- if (existsSync(packageJsonPath)) {
244
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
245
- if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
246
- return packageJson.version;
247
- }
248
- throw new Error(`package.json \u7F3A\u5C11\u6709\u6548\u7684 version \u5B57\u6BB5: ${packageJsonPath}`);
249
- }
250
- if (currentDir === root) {
251
- throw new Error("\u65E0\u6CD5\u5B9A\u4F4D FET package.json");
258
+ // src/commands/doctor.ts
259
+ async function doctorCommand(ctx, options = {}) {
260
+ const checks = [];
261
+ checks.push(await checkOpenSpec(ctx));
262
+ checks.push(await checkState(ctx));
263
+ checks.push(await checkFile("agents", join6(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
264
+ checks.push(await checkFile("config", join6(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
265
+ checks.push(await checkPlaceholders(ctx));
266
+ checks.push(await checkGitNexus(ctx));
267
+ for (const adapter of ctx.toolAdapters) {
268
+ checks.push(...await adapter.doctor(ctx.projectRoot));
269
+ }
270
+ const lockPath = join6(ctx.projectRoot, "openspec", ".fet.lock");
271
+ if (await exists(lockPath)) {
272
+ if (options.fixLock) {
273
+ await clearLock(ctx.projectRoot);
274
+ checks.push({ id: "lock", status: "pass", message: "\u5DF2\u6E05\u7406 openspec/.fet.lock" });
275
+ } else {
276
+ checks.push({ id: "lock", status: "warn", message: "\u5B58\u5728 openspec/.fet.lock", suggestedCommand: "fet doctor --fix-lock" });
252
277
  }
253
- currentDir = dirname4(currentDir);
254
278
  }
279
+ const warnings = checks.filter((check) => check.status !== "pass").map((check) => check.message);
280
+ ctx.output.result({
281
+ ok: true,
282
+ command: "doctor",
283
+ summary: warnings.length ? `\u8BCA\u65AD\u5B8C\u6210\uFF0C\u53D1\u73B0 ${warnings.length} \u4E2A\u9700\u8981\u5173\u6CE8\u7684\u95EE\u9898\u3002` : "\u8BCA\u65AD\u5B8C\u6210\uFF0C\u672A\u53D1\u73B0\u660E\u663E\u95EE\u9898\u3002",
284
+ warnings,
285
+ data: checks
286
+ });
255
287
  }
256
-
257
- // src/templates/markers.ts
258
- var AUTO_BEGIN = "<!-- FET:BEGIN AUTO -->";
259
- var AUTO_END = "<!-- FET:END AUTO -->";
260
- var USER_BEGIN = "<!-- FET:BEGIN USER -->";
261
- var USER_END = "<!-- FET:END USER -->";
262
- var LLM_PLACEHOLDER_PATTERN = /\[NEEDS? LLM INPUT\]/;
263
- function hasManagedAutoRegion(content) {
264
- return count(content, AUTO_BEGIN) === 1 && count(content, AUTO_END) === 1 && content.indexOf(AUTO_BEGIN) < content.indexOf(AUTO_END);
265
- }
266
- function hasInvalidManagedAutoRegion(content) {
267
- const beginCount = count(content, AUTO_BEGIN);
268
- const endCount = count(content, AUTO_END);
269
- return beginCount !== endCount || beginCount > 1 || endCount > 1 || beginCount === 1 && content.indexOf(AUTO_BEGIN) > content.indexOf(AUTO_END);
270
- }
271
- function replaceManagedRegion(existing, generated) {
272
- if (!existing) {
273
- return generated;
274
- }
275
- const start = existing.indexOf(AUTO_BEGIN);
276
- const end = existing.indexOf(AUTO_END);
277
- if (start === -1 || end === -1 || end < start) {
278
- return generated;
288
+ async function checkGitNexus(ctx) {
289
+ const global = await ctx.stateStore.readGlobal();
290
+ const state = mergeGitNexusGraphInfo(
291
+ toGitNexusState(await detectGitNexus(), global?.graph?.gitnexus),
292
+ await inspectGitNexusGraph(ctx.projectRoot)
293
+ );
294
+ if (global) {
295
+ global.graph ??= {};
296
+ global.graph.gitnexus = state;
297
+ await ctx.stateStore.writeGlobal(global);
279
298
  }
280
- const before = existing.slice(0, start);
281
- const after = existing.slice(end + AUTO_END.length);
282
- const existingAuto = extractAuto(existing);
283
- const generatedAuto = extractAuto(generated);
284
- return `${before}${AUTO_BEGIN}
285
- ${mergeAutoRegion(existingAuto, generatedAuto)}
286
- ${AUTO_END}${after}`;
299
+ return state.installed ? {
300
+ id: "gitnexus",
301
+ status: "pass",
302
+ message: ctx.language === "en" ? `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"}), graph ${state.graphExists ? "found" : "not found"}` : `\u68C0\u6D4B\u5230 GitNexus\uFF1A${state.executablePath ?? "gitnexus"}\uFF08${state.version ?? "unknown"}\uFF09\uFF0C\u4EE3\u7801\u56FE${state.graphExists ? "\u5DF2\u627E\u5230" : "\u672A\u627E\u5230"}`
303
+ } : {
304
+ id: "gitnexus",
305
+ status: "warn",
306
+ message: ctx.language === "en" ? "Optional GitNexus code graph support is not installed" : "\u5C1A\u672A\u5B89\u88C5\u53EF\u9009\u7684 GitNexus \u4EE3\u7801\u56FE\u652F\u6301",
307
+ suggestedCommand: ctx.language === "en" ? "Install GitNexus later if you want OpenSpec artifacts to prefer a repository graph" : "\u5982\u679C\u5E0C\u671B OpenSpec \u4EA7\u7269\u4F18\u5148\u53C2\u8003\u4ED3\u5E93\u4EE3\u7801\u56FE\uFF0C\u53EF\u4EE5\u7A0D\u540E\u5B89\u88C5 GitNexus"
308
+ };
287
309
  }
288
- function mergeAutoRegion(existingAuto, generatedAuto) {
289
- const generatedSections = splitMarkdownSections(generatedAuto);
290
- const existingSections = new Map(splitMarkdownSections(existingAuto).map((section) => [section.heading, section]));
291
- if (!generatedSections.length || !existingSections.size) {
292
- return generatedAuto;
310
+ async function checkOpenSpec(ctx) {
311
+ try {
312
+ const identity = await ctx.openSpec.resolveExecutable();
313
+ return { id: "openspec", status: "pass", message: `OpenSpec: ${identity.executablePath} (${identity.version})` };
314
+ } catch (error) {
315
+ return { id: "openspec", status: "fail", message: error instanceof Error ? error.message : "OpenSpec \u68C0\u6D4B\u5931\u8D25" };
293
316
  }
294
- return generatedSections.map((section) => {
295
- const existing = existingSections.get(section.heading);
296
- if (existing && LLM_PLACEHOLDER_PATTERN.test(section.body) && !LLM_PLACEHOLDER_PATTERN.test(existing.body)) {
297
- return existing.raw.trim();
298
- }
299
- return section.raw.trim();
300
- }).join("\n\n");
301
317
  }
302
- function splitMarkdownSections(content) {
303
- const matches = [...content.matchAll(/^## .+$/gm)];
304
- if (!matches.length) {
305
- return [];
318
+ async function checkState(ctx) {
319
+ try {
320
+ const state = await ctx.stateStore.readGlobal();
321
+ return state ? { id: "state", status: "pass", message: "FET \u5168\u5C40\u72B6\u6001\u53EF\u8BFB\u53D6" } : { id: "state", status: "warn", message: "FET \u5168\u5C40\u72B6\u6001\u5C1A\u672A\u521D\u59CB\u5316", suggestedCommand: "fet init" };
322
+ } catch (error) {
323
+ return { id: "state", status: "fail", message: error instanceof Error ? error.message : "FET \u72B6\u6001\u8BFB\u53D6\u5931\u8D25" };
306
324
  }
307
- return matches.map((match, index) => {
308
- const start = match.index ?? 0;
309
- const end = matches[index + 1]?.index ?? content.length;
310
- const raw = content.slice(start, end).trim();
311
- const newline = raw.indexOf("\n");
312
- const heading = newline === -1 ? raw.trim() : raw.slice(0, newline).trim();
313
- const body = newline === -1 ? "" : raw.slice(newline + 1).trim();
314
- return { heading, body, raw };
315
- });
316
325
  }
317
- function extractAuto(content) {
318
- const start = content.indexOf(AUTO_BEGIN);
319
- const end = content.indexOf(AUTO_END);
320
- if (start === -1 || end === -1 || end < start) {
321
- return content.trim();
326
+ async function checkFile(id, path, missing, suggestedCommand) {
327
+ return await exists(path) ? { id, status: "pass", message: `${id} \u5B58\u5728` } : { id, status: "warn", message: missing, suggestedCommand };
328
+ }
329
+ async function checkPlaceholders(ctx) {
330
+ try {
331
+ await readFile4(join6(ctx.projectRoot, "AGENTS.md"), "utf8");
332
+ const count2 = await countAgentsLlmPlaceholders(ctx.projectRoot);
333
+ return count2 ? {
334
+ id: "context-placeholders",
335
+ status: "warn",
336
+ message: ctx.language === "en" ? `AGENTS.md has ${count2} LLM placeholder(s)` : `AGENTS.md \u4ECD\u6709 ${count2} \u4E2A LLM \u5360\u4F4D\u7B26`,
337
+ suggestedCommand: "fet fill-context"
338
+ } : {
339
+ id: "context-placeholders",
340
+ status: "pass",
341
+ message: ctx.language === "en" ? "AGENTS.md placeholders resolved" : "AGENTS.md \u5360\u4F4D\u7B26\u5DF2\u5904\u7406"
342
+ };
343
+ } catch {
344
+ return {
345
+ id: "context-placeholders",
346
+ status: "warn",
347
+ message: ctx.language === "en" ? "AGENTS.md missing" : "AGENTS.md \u7F3A\u5931",
348
+ suggestedCommand: "fet update-context"
349
+ };
322
350
  }
323
- return content.slice(start + AUTO_BEGIN.length, end).trim();
324
351
  }
325
- function count(content, needle) {
326
- return content.split(needle).length - 1;
352
+ async function exists(path) {
353
+ try {
354
+ await stat3(path);
355
+ return true;
356
+ } catch {
357
+ return false;
358
+ }
327
359
  }
328
360
 
329
- // src/templates/agents-md.ts
330
- function renderAgentsMd(scan) {
331
- const commands = Object.entries(scan.commands).map(([name, command]) => `| ${name} | \`${command.command}\` | ${command.source} |`).join("\n");
332
- const routes = scan.routes.map((route) => `| ${route.path} | ${route.source} | ${route.confidence}${route.inferred ? " inferred" : ""} |`).join("\n");
333
- const workspaces = scan.project.workspaces.map((workspace) => `| ${workspace.name} | ${workspace.path} | ${workspace.source} |`).join("\n");
334
- return `# Project Context
335
-
336
- ${AUTO_BEGIN}
337
- ## Project Snapshot
338
-
339
- - Name: ${scan.project.name}
340
- - Package Manager: ${scan.project.packageManager} (${scan.project.packageManagerConfidence})
341
- - Framework: ${scan.project.framework.name} (${scan.project.framework.confidence})
342
- - Language: ${scan.project.language}
343
- - Monorepo: ${scan.project.monorepo ? "yes" : "no"}
344
-
345
- ## Workspaces
346
-
347
- | Name | Path | Source |
348
- |------|------|--------|
349
- ${workspaces || "| root | . | inferred |"}
350
-
351
- ## Commands
352
-
353
- | Name | Command | Source |
354
- |------|---------|--------|
355
- ${commands || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | [NEEDS LLM INPUT] |"}
356
-
357
- ## Structure
358
-
359
- [NEEDS LLM INPUT]
360
-
361
- ## Routes
361
+ // src/commands/fill-context.ts
362
+ import { mkdir as mkdir3 } from "fs/promises";
363
+ import { dirname as dirname4, join as join7 } from "path";
364
+ async function fillContextCommand(ctx) {
365
+ await withProjectLock(ctx.projectRoot, { command: "fill-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
366
+ const handoffPath = join7(ctx.projectRoot, ".fet", "fill-context.md");
367
+ await mkdir3(dirname4(handoffPath), { recursive: true });
368
+ await atomicWrite(handoffPath, renderGenericHandoff(ctx.language));
369
+ for (const adapter of ctx.toolAdapters) {
370
+ const plan = await adapter.planInstall(ctx.projectRoot, ctx.language);
371
+ const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
372
+ const state = await ctx.stateStore.getOrCreateGlobal();
373
+ state.toolAdapters[adapter.tool] = {
374
+ adapterVersion: adapter.adapterVersion,
375
+ installed: true,
376
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
377
+ };
378
+ await ctx.stateStore.writeGlobal(state);
379
+ if (ctx.verbose) {
380
+ ctx.output.info(ctx.language === "en" ? `Updated ${adapter.tool} adapter` : `\u5DF2\u66F4\u65B0 ${adapter.tool} \u9002\u914D\u5668`, { written: result.written });
381
+ }
382
+ }
383
+ });
384
+ const placeholders = await countAgentsLlmPlaceholders(ctx.projectRoot);
385
+ ctx.output.result({
386
+ ok: true,
387
+ command: "fill-context",
388
+ summary: ctx.language === "en" ? placeholders ? `Found ${placeholders} AGENTS.md placeholder(s). Use your IDE AI to fill them.` : "No AGENTS.md placeholders found. IDE fill-context commands were refreshed." : placeholders ? `\u53D1\u73B0 ${placeholders} \u4E2A AGENTS.md \u5360\u4F4D\u7B26\u3002\u8BF7\u4F7F\u7528 IDE AI \u8865\u9F50\u3002` : "\u672A\u53D1\u73B0 AGENTS.md \u5360\u4F4D\u7B26\uFF0C\u5DF2\u5237\u65B0 IDE fill-context \u547D\u4EE4\u3002",
389
+ nextSteps: ctx.language === "en" ? placeholders ? [
390
+ "Cursor: run /fet-fill-context",
391
+ "Codex: run /prompts:fet-fill-context",
392
+ "OpenCode or other IDEs: open .fet/fill-context.md or run fet fill-context for handoff instructions"
393
+ ] : ["Run fet doctor to confirm project context health"] : placeholders ? [
394
+ "Cursor\uFF1A\u8FD0\u884C /fet-fill-context",
395
+ "Codex\uFF1A\u8FD0\u884C /prompts:fet-fill-context",
396
+ "OpenCode \u6216\u5176\u4ED6 IDE\uFF1A\u6253\u5F00 .fet/fill-context.md\uFF0C\u6216\u8FD0\u884C fet fill-context \u67E5\u770B\u4EA4\u63A5\u8BF4\u660E"
397
+ ] : ["\u8FD0\u884C fet doctor \u786E\u8BA4\u9879\u76EE\u4E0A\u4E0B\u6587\u72B6\u6001"],
398
+ data: {
399
+ placeholders,
400
+ cursorCommand: "/fet-fill-context",
401
+ codexCommand: "/prompts:fet-fill-context"
402
+ }
403
+ });
404
+ }
405
+ function renderGenericHandoff(language) {
406
+ if (language === "en") {
407
+ return `<!-- FET:MANAGED
408
+ schemaVersion: 1
409
+ generator: fill-context
410
+ FET:END -->
362
411
 
363
- | Route | Source | Confidence |
364
- |-------|--------|------------|
365
- ${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
412
+ # FET Fill Context
366
413
 
367
- ## Conventions
414
+ Use the IDE AI to complete FET-generated placeholders.
368
415
 
369
- [NEEDS LLM INPUT]
416
+ 1. Read AGENTS.md and openspec/config.yaml.
417
+ 2. Read .fet/karpathy-guidelines.md when it exists. For Codex, also read .codex/fet/karpathy-guidelines.md when it exists.
418
+ 3. Inspect README files, package scripts, routes, tests, source layout, and project conventions.
419
+ 4. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete project-specific content.
420
+ 5. Preserve FET managed markers.
421
+ 6. Do not modify business code.
422
+ 7. Run \`fet doctor\` and confirm no AGENTS.md placeholder warning remains.
423
+ `;
424
+ }
425
+ return `<!-- FET:MANAGED
426
+ schemaVersion: 1
427
+ generator: fill-context
428
+ FET:END -->
370
429
 
371
- ## AI Work Guidelines
430
+ # FET \u586B\u5145\u4E0A\u4E0B\u6587
372
431
 
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.
432
+ \u4F7F\u7528 IDE AI \u8865\u9F50 FET \u751F\u6210\u7684\u9879\u76EE\u4E0A\u4E0B\u6587\u5360\u4F4D\u7B26\u3002
376
433
 
377
- ## Scanner Metadata
434
+ 1. \u9605\u8BFB AGENTS.md \u548C openspec/config.yaml\u3002
435
+ 2. \u5982\u679C\u5B58\u5728 .fet/karpathy-guidelines.md\uFF0C\u8BF7\u4E00\u5E76\u9605\u8BFB\u3002\u5BF9 Codex\uFF0C\u5982\u679C\u5B58\u5728 .codex/fet/karpathy-guidelines.md\uFF0C\u4E5F\u8981\u9605\u8BFB\u3002
436
+ 3. \u68C0\u67E5 README\u3001package scripts\u3001\u8DEF\u7531\u3001\u6D4B\u8BD5\u3001\u6E90\u7801\u7ED3\u6784\u548C\u9879\u76EE\u7EA6\u5B9A\u3002
437
+ 4. \u5C06 AGENTS.md \u4E2D\u6BCF\u4E2A \`[NEEDS LLM INPUT]\` \u6216 \`[NEED LLM INPUT]\` \u5360\u4F4D\u7B26\u66FF\u6362\u4E3A\u5177\u4F53\u3001\u9879\u76EE\u76F8\u5173\u7684\u5185\u5BB9\u3002
438
+ 5. \u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\u3002
439
+ 6. \u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
440
+ 7. \u8FD0\u884C \`fet doctor\`\uFF0C\u786E\u8BA4\u4E0D\u518D\u6709 AGENTS.md \u5360\u4F4D\u7B26\u8B66\u544A\u3002
441
+ `;
442
+ }
378
443
 
379
- - Generated At: ${scan.generatedAt}
380
- - FET Version: ${FET_VERSION}
381
- - Scanner Version: ${scan.scannerVersion}
382
- - Warnings: ${scan.warnings.length ? scan.warnings.join("; ") : "none"}
383
- ${AUTO_END}
384
-
385
- ${USER_BEGIN}
386
- ## Notes For AI
387
-
388
- [NEEDS LLM INPUT]
389
- ${USER_END}
390
- `;
444
+ // src/commands/graph.ts
445
+ import { mkdir as mkdir4 } from "fs/promises";
446
+ import { dirname as dirname5, join as join8 } from "path";
447
+ async function graphCommand(ctx, action, args = []) {
448
+ switch (action) {
449
+ case "status":
450
+ await graphStatusCommand(ctx);
451
+ return;
452
+ case "doctor":
453
+ await graphDoctorCommand(ctx);
454
+ return;
455
+ case "setup":
456
+ await graphSetupCommand(ctx);
457
+ return;
458
+ case "handoff":
459
+ await graphHandoffCommand(ctx);
460
+ return;
461
+ case "init":
462
+ await graphAnalyzeCommand(ctx, "init", args);
463
+ return;
464
+ case "refresh":
465
+ await graphAnalyzeCommand(ctx, "refresh", args);
466
+ return;
467
+ }
391
468
  }
392
-
393
- // src/templates/config-yaml.ts
394
- import { stringify } from "yaml";
395
- function renderFetConfig(scan) {
396
- return stringify({
397
- fet: {
398
- schemaVersion: 1,
399
- generatedAt: scan.generatedAt,
400
- fetVersion: FET_VERSION,
401
- scannerVersion: scan.scannerVersion,
402
- project: {
403
- packageManager: scan.project.packageManager,
404
- packageManagerConfidence: scan.project.packageManagerConfidence,
405
- framework: scan.project.framework,
406
- language: scan.project.language,
407
- monorepo: scan.project.monorepo,
408
- workspaces: scan.project.workspaces
409
- },
410
- commands: scan.commands,
411
- validation: {
412
- monorepo: scan.project.monorepo,
413
- missing: {
414
- lint: "warn",
415
- typecheck: "warn",
416
- test: "warn"
417
- },
418
- workspaces: scan.project.workspaces
469
+ async function graphStatusCommand(ctx) {
470
+ const result = await refreshGraphState(ctx, { runStatus: true });
471
+ const warnings = result.state.installed ? [] : [
472
+ ctx.language === "en" ? "GitNexus is not installed. Run fet graph setup for installation handoff instructions." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002\u8FD0\u884C fet graph setup \u83B7\u53D6\u5B89\u88C5\u4EA4\u63A5\u8BF4\u660E\u3002"
473
+ ];
474
+ ctx.output.result({
475
+ ok: true,
476
+ command: "graph status",
477
+ summary: ctx.language === "en" ? 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." : result.state.installed ? `\u5DF2\u68C0\u67E5 GitNexus \u4EE3\u7801\u56FE\u72B6\u6001\u3002\u4EE3\u7801\u56FE${result.state.graphExists ? "\u5B58\u5728" : "\u4E0D\u5B58\u5728"}\uFF0C\u8DEF\u5F84\u4E3A ${result.state.graphPath ?? ".gitnexus"}\u3002` : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002\u4EE3\u7801\u56FE\u80FD\u529B\u4FDD\u6301\u53EF\u9009\u3002",
478
+ warnings,
479
+ nextSteps: result.state.installed && !result.state.graphExists ? [ctx.language === "en" ? "Run fet graph init to build the first GitNexus graph" : "\u8FD0\u884C fet graph init \u751F\u6210\u7B2C\u4E00\u4EFD GitNexus \u4EE3\u7801\u56FE"] : void 0,
480
+ data: result
481
+ });
482
+ }
483
+ async function graphDoctorCommand(ctx) {
484
+ const result = await refreshGraphState(ctx, { runStatus: true });
485
+ const warnings = [
486
+ ...!result.state.installed ? [ctx.language === "en" ? "GitNexus is not installed." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002"] : [],
487
+ ...result.state.installed && !result.state.graphExists ? [ctx.language === "en" ? "GitNexus is installed but no graph directory was found." : "\u5DF2\u5B89\u88C5 GitNexus\uFF0C\u4F46\u672A\u53D1\u73B0\u4EE3\u7801\u56FE\u76EE\u5F55\u3002"] : [],
488
+ ...!result.state.handoffPath ? [ctx.language === "en" ? "Graph handoff instructions have not been generated." : "\u5C1A\u672A\u751F\u6210\u4EE3\u7801\u56FE\u4F7F\u7528\u4EA4\u63A5\u8BF4\u660E\u3002"] : []
489
+ ];
490
+ ctx.output.result({
491
+ ok: true,
492
+ command: "graph doctor",
493
+ summary: ctx.language === "en" ? warnings.length ? `Graph doctor completed with ${warnings.length} warning(s).` : "Graph doctor completed without warnings." : warnings.length ? `\u4EE3\u7801\u56FE\u8BCA\u65AD\u5B8C\u6210\uFF0C\u53D1\u73B0 ${warnings.length} \u4E2A\u8B66\u544A\u3002` : "\u4EE3\u7801\u56FE\u8BCA\u65AD\u5B8C\u6210\uFF0C\u672A\u53D1\u73B0\u8B66\u544A\u3002",
494
+ warnings,
495
+ nextSteps: warnings.length ? ctx.language === "en" ? ["Run fet graph setup", "Run fet graph handoff", "Run fet graph init when GitNexus is installed"] : ["\u8FD0\u884C fet graph setup", "\u8FD0\u884C fet graph handoff", "\u5B89\u88C5 GitNexus \u540E\u8FD0\u884C fet graph init"] : void 0,
496
+ data: result
497
+ });
498
+ }
499
+ async function graphSetupCommand(ctx) {
500
+ let result;
501
+ const handoffPath = join8(ctx.projectRoot, ".fet", "graph-setup.md");
502
+ const installCommand = process.env.FET_GITNEXUS_INSTALL_COMMAND?.trim() || null;
503
+ await withProjectLock(ctx.projectRoot, { command: "graph setup", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
504
+ result = await refreshGraphState(ctx, { write: false });
505
+ await writeHandoffFile(handoffPath, renderGraphSetupHandoff(result.state, { installCommand, language: ctx.language }));
506
+ const global = await ctx.stateStore.getOrCreateGlobal();
507
+ global.graph ??= {};
508
+ global.graph.gitnexus = {
509
+ ...result.state,
510
+ setupHandoffPath: ".fet/graph-setup.md",
511
+ setupHandoffUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
512
+ };
513
+ await ctx.stateStore.writeGlobal(global);
514
+ });
515
+ ctx.output.result({
516
+ ok: true,
517
+ command: "graph setup",
518
+ summary: ctx.language === "en" ? "GitNexus IDE-assisted setup playbook generated." : "\u5DF2\u751F\u6210 GitNexus IDE LLM \u8F85\u52A9\u5B89\u88C5\u4EFB\u52A1\u4E66\u3002",
519
+ warnings: result.state.installed ? [] : [
520
+ ctx.language === "en" ? "GitNexus is not installed. The playbook lets the current IDE LLM guide installation with user confirmation before risky commands." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002\u4EFB\u52A1\u4E66\u4F1A\u6307\u5BFC\u5F53\u524D IDE LLM \u63A8\u8FDB\u5B89\u88C5\uFF0C\u5E76\u5728\u9AD8\u98CE\u9669\u547D\u4EE4\u524D\u8BF7\u6C42\u7528\u6237\u786E\u8BA4\u3002"
521
+ ],
522
+ nextSteps: result.state.installed ? [
523
+ ctx.language === "en" ? "Ask your IDE AI to read .fet/graph-setup.md and run the optional gitnexus setup flow if you want IDE/MCP integration" : "\u8BA9\u5F53\u524D IDE AI \u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u5728\u9700\u8981 IDE/MCP \u96C6\u6210\u65F6\u6309\u786E\u8BA4\u6D41\u7A0B\u8FD0\u884C gitnexus setup",
524
+ "fet graph init"
525
+ ] : [
526
+ ctx.language === "en" ? "Ask your current IDE AI to read .fet/graph-setup.md and follow the installation playbook" : "\u8BA9\u5F53\u524D IDE AI \u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u6309\u5B89\u88C5\u4EFB\u52A1\u4E66\u63A8\u8FDB"
527
+ ],
528
+ data: {
529
+ path: ".fet/graph-setup.md",
530
+ installCommandConfigured: Boolean(installCommand),
531
+ gitnexus: result.state
532
+ }
533
+ });
534
+ }
535
+ async function graphHandoffCommand(ctx) {
536
+ let result;
537
+ const handoffPath = join8(ctx.projectRoot, ".fet", "graph-handoff.md");
538
+ await withProjectLock(ctx.projectRoot, { command: "graph handoff", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
539
+ result = await refreshGraphState(ctx, { runStatus: true, write: false });
540
+ await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state, ctx.language));
541
+ const global = await ctx.stateStore.getOrCreateGlobal();
542
+ global.graph ??= {};
543
+ global.graph.gitnexus = {
544
+ ...result.state,
545
+ handoffPath: ".fet/graph-handoff.md",
546
+ handoffUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
547
+ };
548
+ await ctx.stateStore.writeGlobal(global);
549
+ });
550
+ ctx.output.result({
551
+ ok: true,
552
+ command: "graph handoff",
553
+ summary: ctx.language === "en" ? "GitNexus graph usage handoff generated." : "\u5DF2\u751F\u6210 GitNexus \u4EE3\u7801\u56FE\u4F7F\u7528\u4EA4\u63A5\u8BF4\u660E\u3002",
554
+ warnings: result.state.installed ? [] : [
555
+ ctx.language === "en" ? "GitNexus is not installed. The handoff still documents the fallback behavior." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002\u4EA4\u63A5\u8BF4\u660E\u4ECD\u4F1A\u8BB0\u5F55\u56DE\u9000\u884C\u4E3A\u3002"
556
+ ],
557
+ nextSteps: ctx.language === "en" ? ["Cursor/Codex/OpenCode: read .fet/graph-handoff.md before broad repository scans"] : ["Cursor/Codex/OpenCode\uFF1A\u5927\u8303\u56F4\u626B\u63CF\u4ED3\u5E93\u524D\u5148\u9605\u8BFB .fet/graph-handoff.md"],
558
+ data: {
559
+ path: ".fet/graph-handoff.md",
560
+ gitnexus: result.state
561
+ }
562
+ });
563
+ }
564
+ async function graphAnalyzeCommand(ctx, mode, args) {
565
+ const detection = await detectGitNexus();
566
+ if (!detection.installed) {
567
+ throw new FetError({
568
+ code: "GRAPH_PROVIDER_NOT_FOUND" /* GraphProviderNotFound */,
569
+ message: ctx.language === "en" ? "GitNexus is not installed or is not available on PATH." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\uFF0C\u6216 GitNexus \u4E0D\u5728 PATH \u4E2D\u3002",
570
+ details: { executable: detection.executablePath, error: detection.error },
571
+ suggestedCommand: "fet graph setup"
572
+ });
573
+ }
574
+ const run = await runGitNexus(["analyze", ...args], { cwd: ctx.projectRoot });
575
+ if (run.exitCode !== 0) {
576
+ throw new FetError({
577
+ code: "GRAPH_COMMAND_FAILED" /* GraphCommandFailed */,
578
+ message: ctx.language === "en" ? "GitNexus analyze failed." : "GitNexus analyze \u6267\u884C\u5931\u8D25\u3002",
579
+ details: { command: run.command.join(" "), exitCode: run.exitCode, stdout: run.stdout, stderr: run.stderr },
580
+ suggestedCommand: "fet graph doctor"
581
+ });
582
+ }
583
+ const result = await refreshGraphState(ctx, { write: false });
584
+ const global = await ctx.stateStore.getOrCreateGlobal();
585
+ global.graph ??= {};
586
+ global.graph.gitnexus = {
587
+ ...result.state,
588
+ lastRefreshAt: (/* @__PURE__ */ new Date()).toISOString()
589
+ };
590
+ await ctx.stateStore.writeGlobal(global);
591
+ ctx.output.result({
592
+ ok: true,
593
+ command: `graph ${mode}`,
594
+ summary: ctx.language === "en" ? mode === "init" ? "GitNexus graph initialized." : "GitNexus graph refreshed." : mode === "init" ? "\u5DF2\u521D\u59CB\u5316 GitNexus \u4EE3\u7801\u56FE\u3002" : "\u5DF2\u5237\u65B0 GitNexus \u4EE3\u7801\u56FE\u3002",
595
+ warnings: result.state.graphExists ? [] : [
596
+ ctx.language === "en" ? "GitNexus analyze completed, but the configured graph directory was not found." : "GitNexus analyze \u5DF2\u5B8C\u6210\uFF0C\u4F46\u672A\u53D1\u73B0\u914D\u7F6E\u7684\u4EE3\u7801\u56FE\u76EE\u5F55\u3002"
597
+ ],
598
+ nextSteps: ctx.language === "en" ? ["Run fet graph status", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"] : ["\u8FD0\u884C fet graph status", "\u4F7F\u7528 .fet/graph-handoff.md \u6216\u751F\u6210\u7684 IDE \u63D0\u793A\uFF0C\u4F18\u5148\u53C2\u8003\u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587"],
599
+ data: {
600
+ gitnexus: global.graph.gitnexus,
601
+ run: {
602
+ command: run.command,
603
+ stdout: run.stdout.trim(),
604
+ stderr: run.stderr.trim()
419
605
  }
420
606
  }
421
607
  });
422
608
  }
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
- `;
609
+ async function refreshGraphState(ctx, options = {}) {
610
+ const global = await ctx.stateStore.getOrCreateGlobal();
611
+ global.graph ??= {};
612
+ const detection = await detectGitNexus();
613
+ const graph2 = await inspectGitNexusGraph(ctx.projectRoot);
614
+ let state = mergeGitNexusGraphInfo(toGitNexusState(detection, global.graph.gitnexus), graph2);
615
+ let gitnexusStatus = null;
616
+ if (options.runStatus && detection.installed) {
617
+ gitnexusStatus = await runGitNexus(["status"], { cwd: ctx.projectRoot });
618
+ state = {
619
+ ...state,
620
+ lastStatus: firstLine(gitnexusStatus.stdout) || firstLine(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
621
+ };
433
622
  }
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)}`;
623
+ if (options.write ?? true) {
624
+ global.graph.gitnexus = state;
625
+ await ctx.stateStore.writeGlobal(global);
438
626
  }
439
- return `${existing.replace(/\s*$/, "")}
440
-
441
- ${block}
442
- `;
627
+ return {
628
+ state,
629
+ gitnexusStatus: gitnexusStatus ? {
630
+ exitCode: gitnexusStatus.exitCode,
631
+ command: gitnexusStatus.command,
632
+ stdout: gitnexusStatus.stdout.trim(),
633
+ stderr: gitnexusStatus.stderr.trim()
634
+ } : null
635
+ };
443
636
  }
444
- function renderKarpathyCursorRule() {
445
- return `<!-- FET:MANAGED
637
+ async function writeHandoffFile(path, content) {
638
+ await mkdir4(dirname5(path), { recursive: true });
639
+ await atomicWrite(path, content);
640
+ }
641
+ function renderGraphSetupHandoff(state, options) {
642
+ if (options.language === "en") {
643
+ return `<!-- FET:MANAGED
446
644
  schemaVersion: 1
447
- generator: karpathy-skills
645
+ generator: graph-setup
448
646
  FET:END -->
449
647
 
450
- ---
451
- description: Andrej Karpathy inspired coding guidelines
452
- alwaysApply: true
453
- ---
648
+ # FET GitNexus IDE Setup Playbook
649
+
650
+ This file is written for the current IDE LLM. Use it to help the user install and verify optional GitNexus graph support. GitNexus is optional; FET/OpenSpec workflows must continue to work when it is unavailable.
651
+
652
+ Current status:
653
+
654
+ - Installed: ${state.installed ? "yes" : "no"}
655
+ - Executable: ${state.executablePath ?? "gitnexus"}
656
+ - Version: ${state.version ?? "unknown"}
657
+ - Graph path: ${state.graphPath ?? ".gitnexus"}
658
+ - Graph exists: ${state.graphExists ? "yes" : "no"}
659
+ - Configured install command: ${options.installCommand ?? "none"}
660
+
661
+ IDE LLM setup flow:
454
662
 
455
- ${renderKarpathyGuidelinesBody()}
663
+ 1. Check the shell, operating system, package managers, and PATH. Run only read-only detection commands at first.
664
+ 2. Run \`gitnexus --version\` or the executable shown above. If it succeeds, skip installation and continue to verification.
665
+ 3. If GitNexus is missing and \`FET_GITNEXUS_INSTALL_COMMAND\` is configured, explain that command to the user and ask for approval before running it.
666
+ 4. If no install command is configured, find the official GitNexus installation instructions or ask the user for the preferred install method. Do not invent an installer.
667
+ 5. Before running any command that downloads software, installs globally, changes PATH, modifies user home files, or changes IDE/MCP configuration, show the exact command and wait for user approval.
668
+ 6. After installation, verify with \`gitnexus --version\`.
669
+ 7. If the user wants IDE/MCP integration, run \`gitnexus setup\` only after explaining what it may change and receiving approval.
670
+ 8. Return to this project and run \`fet graph init\` to build the first graph.
671
+ 9. Run \`fet graph handoff\` so future IDE AI work can prefer graph context before broad repository scans.
672
+
673
+ Guardrails:
674
+
675
+ - Do not block FET/OpenSpec commands when GitNexus is unavailable.
676
+ - Do not generate or modify application code during setup.
677
+ - Do not silently install software or modify global/user-level configuration.
678
+ - If installation fails, summarize the failing command, stderr, and the next manual step.
456
679
  `;
457
- }
458
- function renderKarpathyFetHandoff() {
680
+ }
459
681
  return `<!-- FET:MANAGED
460
682
  schemaVersion: 1
461
- generator: karpathy-skills
683
+ generator: graph-setup
462
684
  FET:END -->
463
685
 
464
- # Andrej Karpathy Inspired Coding Guidelines
686
+ # FET GitNexus IDE \u5B89\u88C5\u4EFB\u52A1\u4E66
465
687
 
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
688
+ \u672C\u6587\u6863\u5199\u7ED9\u5F53\u524D IDE LLM\u3002\u8BF7\u7528\u5B83\u5E2E\u52A9\u7528\u6237\u5B89\u88C5\u5E76\u9A8C\u8BC1\u53EF\u9009\u7684 GitNexus \u4EE3\u7801\u56FE\u80FD\u529B\u3002GitNexus \u662F\u53EF\u9009\u80FD\u529B\uFF1B\u5373\u4F7F\u4E0D\u53EF\u7528\uFF0CFET/OpenSpec \u4E3B\u6D41\u7A0B\u4E5F\u5FC5\u987B\u7EE7\u7EED\u53EF\u7528\u3002
476
689
 
477
- ${renderKarpathyGuidelinesBody()}`;
478
- }
479
- function renderKarpathyGuidelinesBody() {
480
- return `Source: ${KARPATHY_SKILLS_SOURCE}
690
+ \u5F53\u524D\u72B6\u6001\uFF1A
481
691
 
482
- Use these project-level guidelines together with AGENTS.md, OpenSpec artifacts, and the user's latest request.
692
+ - \u5DF2\u5B89\u88C5\uFF1A${state.installed ? "\u662F" : "\u5426"}
693
+ - \u53EF\u6267\u884C\u6587\u4EF6\uFF1A${state.executablePath ?? "gitnexus"}
694
+ - \u7248\u672C\uFF1A${state.version ?? "unknown"}
695
+ - \u56FE\u8DEF\u5F84\uFF1A${state.graphPath ?? ".gitnexus"}
696
+ - \u56FE\u5DF2\u5B58\u5728\uFF1A${state.graphExists ? "\u662F" : "\u5426"}
697
+ - \u5DF2\u914D\u7F6E\u5B89\u88C5\u547D\u4EE4\uFF1A${options.installCommand ?? "\u65E0"}
483
698
 
484
- ## Think Before Coding
699
+ IDE LLM \u5B89\u88C5\u6D41\u7A0B\uFF1A
485
700
 
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.
701
+ 1. \u5148\u68C0\u6D4B shell\u3001\u64CD\u4F5C\u7CFB\u7EDF\u3001\u5305\u7BA1\u7406\u5668\u548C PATH\u3002\u8D77\u6B65\u9636\u6BB5\u53EA\u8FD0\u884C\u53EA\u8BFB\u68C0\u6D4B\u547D\u4EE4\u3002
702
+ 2. \u8FD0\u884C \`gitnexus --version\` \u6216\u4E0A\u65B9\u663E\u793A\u7684\u53EF\u6267\u884C\u6587\u4EF6\u3002\u5982\u679C\u6210\u529F\uFF0C\u8DF3\u8FC7\u5B89\u88C5\u5E76\u8FDB\u5165\u9A8C\u8BC1\u3002
703
+ 3. \u5982\u679C GitNexus \u7F3A\u5931\u4E14\u5DF2\u914D\u7F6E \`FET_GITNEXUS_INSTALL_COMMAND\`\uFF0C\u5148\u5411\u7528\u6237\u89E3\u91CA\u8BE5\u547D\u4EE4\uFF0C\u518D\u7B49\u5F85\u7528\u6237\u6279\u51C6\u540E\u6267\u884C\u3002
704
+ 4. \u5982\u679C\u6CA1\u6709\u914D\u7F6E\u5B89\u88C5\u547D\u4EE4\uFF0C\u67E5\u627E GitNexus \u5B98\u65B9\u5B89\u88C5\u8BF4\u660E\uFF0C\u6216\u8BE2\u95EE\u7528\u6237\u5E0C\u671B\u4F7F\u7528\u7684\u5B89\u88C5\u65B9\u5F0F\u3002\u4E0D\u8981\u81C6\u9020\u5B89\u88C5\u547D\u4EE4\u3002
705
+ 5. \u4EFB\u4F55\u4F1A\u4E0B\u8F7D\u8F6F\u4EF6\u3001\u5168\u5C40\u5B89\u88C5\u3001\u4FEE\u6539 PATH\u3001\u5199\u5165\u7528\u6237\u76EE\u5F55\u6216\u4FEE\u6539 IDE/MCP \u914D\u7F6E\u7684\u547D\u4EE4\uFF0C\u6267\u884C\u524D\u90FD\u8981\u5C55\u793A\u5B8C\u6574\u547D\u4EE4\u5E76\u7B49\u5F85\u7528\u6237\u786E\u8BA4\u3002
706
+ 6. \u5B89\u88C5\u540E\u8FD0\u884C \`gitnexus --version\` \u9A8C\u8BC1\u3002
707
+ 7. \u5982\u679C\u7528\u6237\u9700\u8981 IDE/MCP \u96C6\u6210\uFF0C\u5148\u8BF4\u660E \`gitnexus setup\` \u53EF\u80FD\u4FEE\u6539\u7684\u5185\u5BB9\uFF0C\u83B7\u5F97\u786E\u8BA4\u540E\u518D\u8FD0\u884C\u3002
708
+ 8. \u56DE\u5230\u672C\u9879\u76EE\u8FD0\u884C \`fet graph init\`\uFF0C\u751F\u6210\u7B2C\u4E00\u4EFD\u4EE3\u7801\u56FE\u3002
709
+ 9. \u8FD0\u884C \`fet graph handoff\`\uFF0C\u8BA9\u540E\u7EED IDE AI \u5728\u5927\u8303\u56F4\u626B\u63CF\u524D\u4F18\u5148\u4F7F\u7528\u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u3002
490
710
 
491
- ## Simplicity First
711
+ \u7EA6\u675F\uFF1A
492
712
 
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.
713
+ - GitNexus \u4E0D\u53EF\u7528\u65F6\uFF0C\u4E0D\u8981\u963B\u585E FET/OpenSpec \u547D\u4EE4\u3002
714
+ - \u5B89\u88C5\u8FC7\u7A0B\u4E2D\u4E0D\u8981\u751F\u6210\u6216\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
715
+ - \u4E0D\u8981\u9759\u9ED8\u5B89\u88C5\u8F6F\u4EF6\uFF0C\u4E5F\u4E0D\u8981\u9759\u9ED8\u4FEE\u6539\u5168\u5C40\u6216\u7528\u6237\u7EA7\u914D\u7F6E\u3002
716
+ - \u5982\u679C\u5B89\u88C5\u5931\u8D25\uFF0C\u6C47\u603B\u5931\u8D25\u547D\u4EE4\u3001stderr \u548C\u4E0B\u4E00\u6B65\u4EBA\u5DE5\u5904\u7406\u5EFA\u8BAE\u3002
717
+ `;
718
+ }
719
+ function renderGraphUsageHandoff(state, language) {
720
+ if (language === "en") {
721
+ return `<!-- FET:MANAGED
722
+ schemaVersion: 1
723
+ generator: graph-handoff
724
+ FET:END -->
497
725
 
498
- ## Precise Edits
726
+ # FET Graph Handoff
499
727
 
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.
728
+ Use GitNexus graph context as an optional first pass before broad repository scans.
504
729
 
505
- ## Goal-Driven Execution
730
+ Current status:
506
731
 
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.
732
+ - Installed: ${state.installed ? "yes" : "no"}
733
+ - Graph path: ${state.graphPath ?? ".gitnexus"}
734
+ - Graph exists: ${state.graphExists ? "yes" : "no"}
735
+ - Last indexed at: ${state.lastIndexedAt ?? "unknown"}
736
+ - Last status: ${state.lastStatus ?? "unknown"}
511
737
 
512
- These guidelines intentionally favor caution over speed for non-trivial work. For obvious one-line fixes, use judgment and stay lightweight.`;
513
- }
738
+ When graph context is available:
514
739
 
515
- // src/templates/verify-instructions.ts
516
- function renderVerifyInstructions(changeId, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
517
- return `---
740
+ 1. Use the graph to identify likely modules, dependencies, and insertion points.
741
+ 2. Read only the concrete source files needed to confirm behavior.
742
+ 3. Prefer OpenSpec artifacts and AGENTS.md over graph guesses when they conflict.
743
+ 4. Fall back to normal repository inspection if the graph is missing, stale, or incomplete.
744
+
745
+ When producing OpenSpec artifacts:
746
+
747
+ - Use graph context to make proposal, design, specs, and tasks more precise.
748
+ - Avoid large repository scans when the graph already narrows the relevant area.
749
+ - Keep all generated artifacts in the normal OpenSpec change directory.
750
+ `;
751
+ }
752
+ return `<!-- FET:MANAGED
518
753
  schemaVersion: 1
519
- fetVersion: ${FET_VERSION}
520
- generatedAt: ${generatedAt}
521
- changeId: ${changeId}
522
- purpose: manual-verify
523
- ---
754
+ generator: graph-handoff
755
+ FET:END -->
524
756
 
525
- # Verify Instructions
757
+ # FET \u4EE3\u7801\u56FE\u4EA4\u63A5\u8BF4\u660E
526
758
 
527
- \u8BF7\u6309\u987A\u5E8F\u5B8C\u6210\u4EE5\u4E0B\u68C0\u67E5\uFF1A
759
+ \u5728\u5927\u8303\u56F4\u626B\u63CF\u4ED3\u5E93\u524D\uFF0C\u4F18\u5148\u628A GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u4F5C\u4E3A\u53EF\u9009\u7684\u7B2C\u4E00\u8F6E\u7EBF\u7D22\u3002
528
760
 
529
- 1. \u8FD0\u884C OpenSpec \u89C4\u8303\u6821\u9A8C\uFF1A\`openspec verify\`
530
- 2. \u6309\u9879\u76EE\u7EA6\u5B9A\u8FD0\u884C lint\u3001typecheck\u3001test\u3002
531
- 3. \u68C0\u67E5\u672C\u6B21 change \u7684 \`tasks.md\` \u662F\u5426\u4E0E\u5B9E\u73B0\u72B6\u6001\u4E00\u81F4\u3002
761
+ \u5F53\u524D\u72B6\u6001\uFF1A
532
762
 
533
- \u5B8C\u6210\u540E\u8FD0\u884C\uFF1A
763
+ - \u5DF2\u5B89\u88C5\uFF1A${state.installed ? "\u662F" : "\u5426"}
764
+ - \u56FE\u8DEF\u5F84\uFF1A${state.graphPath ?? ".gitnexus"}
765
+ - \u56FE\u5DF2\u5B58\u5728\uFF1A${state.graphExists ? "\u662F" : "\u5426"}
766
+ - \u6700\u540E\u7D22\u5F15\u65F6\u95F4\uFF1A${state.lastIndexedAt ?? "unknown"}
767
+ - \u6700\u540E\u72B6\u6001\uFF1A${state.lastStatus ?? "unknown"}
534
768
 
535
- \`\`\`sh
536
- fet verify --done --change ${changeId}
537
- \`\`\`
538
- `;
539
- }
769
+ \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u53EF\u7528\u65F6\uFF1A
540
770
 
541
- // src/templates/gitignore.ts
542
- var BEGIN2 = "# FET:BEGIN LOCAL STATE";
543
- var END2 = "# FET:END LOCAL STATE";
544
- var RULES = [
545
- "openspec/fet-state.json",
546
- "openspec/.fet.lock",
547
- "openspec/.fet-init-journal.json",
548
- "openspec/changes/*/fet-state.json",
549
- "openspec/changes/*/.fet/",
550
- ".gitnexus/"
551
- ];
552
- function mergeGitignore(existing) {
553
- const block = `${BEGIN2}
554
- ${RULES.join("\n")}
555
- ${END2}`;
556
- if (!existing || !existing.trim()) {
557
- return `${block}
558
- `;
559
- }
560
- const start = existing.indexOf(BEGIN2);
561
- const end = existing.indexOf(END2);
562
- if (start !== -1 && end !== -1 && end > start) {
563
- return `${existing.slice(0, start)}${block}${existing.slice(end + END2.length)}`;
564
- }
565
- return `${existing.replace(/\s*$/, "")}
771
+ 1. \u7528\u4EE3\u7801\u56FE\u8BC6\u522B\u53EF\u80FD\u76F8\u5173\u7684\u6A21\u5757\u3001\u4F9D\u8D56\u548C\u63D2\u5165\u70B9\u3002
772
+ 2. \u53EA\u8BFB\u53D6\u9700\u8981\u786E\u8BA4\u884C\u4E3A\u7684\u5177\u4F53\u6E90\u7801\u6587\u4EF6\u3002
773
+ 3. \u5F53\u4EE3\u7801\u56FE\u63A8\u65AD\u4E0E OpenSpec \u4EA7\u7269\u6216 AGENTS.md \u51B2\u7A81\u65F6\uFF0C\u4F18\u5148\u76F8\u4FE1 OpenSpec \u4EA7\u7269\u548C AGENTS.md\u3002
774
+ 4. \u5982\u679C\u4EE3\u7801\u56FE\u7F3A\u5931\u3001\u8FC7\u671F\u6216\u4E0D\u5B8C\u6574\uFF0C\u56DE\u9000\u5230\u666E\u901A\u4ED3\u5E93\u68C0\u67E5\u3002
566
775
 
567
- ${block}
776
+ \u751F\u6210 OpenSpec \u4EA7\u7269\u65F6\uFF1A
777
+
778
+ - \u7528\u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u8BA9 proposal\u3001design\u3001specs \u548C tasks \u66F4\u7CBE\u786E\u3002
779
+ - \u5F53\u4EE3\u7801\u56FE\u5DF2\u7ECF\u7F29\u5C0F\u76F8\u5173\u8303\u56F4\u65F6\uFF0C\u907F\u514D\u5927\u8303\u56F4\u4ED3\u5E93\u626B\u63CF\u3002
780
+ - \u6240\u6709\u751F\u6210\u4EA7\u7269\u4ECD\u5199\u5165\u6B63\u5E38\u7684 OpenSpec change \u76EE\u5F55\u3002
568
781
  `;
569
782
  }
783
+ function firstLine(value) {
784
+ return value.trim().split(/\r?\n/)[0]?.trim() || null;
785
+ }
570
786
 
571
- // src/commands/update-context.ts
572
- import { readFile as readFile5 } from "fs/promises";
573
- import { join as join7 } from "path";
787
+ // src/commands/init.ts
788
+ import { readFile as readFile7, stat as stat4 } from "fs/promises";
789
+ import { join as join11 } from "path";
574
790
 
575
- // src/config/yaml.ts
576
- import { readFile as readFile3 } from "fs/promises";
577
- import { parseDocument } from "yaml";
578
- async function mergeFetConfig(configPath, renderedFetYaml) {
579
- const fetDoc = parseDocument(renderedFetYaml);
580
- const nextFet = fetDoc.get("fet", true);
581
- let existing = "";
582
- try {
583
- existing = await readFile3(configPath, "utf8");
584
- } catch {
585
- return renderedFetYaml;
791
+ // src/version.ts
792
+ import { existsSync, readFileSync } from "fs";
793
+ import { dirname as dirname6, join as join9, parse } from "path";
794
+ import { fileURLToPath } from "url";
795
+ var FET_VERSION = readPackageVersion();
796
+ function readPackageVersion() {
797
+ let currentDir = dirname6(fileURLToPath(import.meta.url));
798
+ const root = parse(currentDir).root;
799
+ while (true) {
800
+ const packageJsonPath = join9(currentDir, "package.json");
801
+ if (existsSync(packageJsonPath)) {
802
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
803
+ if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
804
+ return packageJson.version;
805
+ }
806
+ throw new Error(`package.json \u7F3A\u5C11\u6709\u6548\u7684 version \u5B57\u6BB5: ${packageJsonPath}`);
807
+ }
808
+ if (currentDir === root) {
809
+ throw new Error("\u65E0\u6CD5\u5B9A\u4F4D FET package.json");
810
+ }
811
+ currentDir = dirname6(currentDir);
586
812
  }
587
- const doc = parseDocument(existing || "{}");
588
- doc.set("fet", nextFet);
589
- return doc.toString();
590
813
  }
591
814
 
592
- // src/context-placeholders.ts
593
- import { readFile as readFile4 } from "fs/promises";
594
- import { join as join6 } from "path";
595
- var AGENTS_LLM_PLACEHOLDER_PATTERN = /\[NEEDS? LLM INPUT\]/g;
596
- async function countAgentsLlmPlaceholders(projectRoot) {
597
- try {
598
- const content = await readFile4(join6(projectRoot, "AGENTS.md"), "utf8");
599
- return [...content.matchAll(AGENTS_LLM_PLACEHOLDER_PATTERN)].length;
600
- } catch {
601
- return 0;
602
- }
815
+ // src/templates/markers.ts
816
+ var AUTO_BEGIN = "<!-- FET:BEGIN AUTO -->";
817
+ var AUTO_END = "<!-- FET:END AUTO -->";
818
+ var USER_BEGIN = "<!-- FET:BEGIN USER -->";
819
+ var USER_END = "<!-- FET:END USER -->";
820
+ var LLM_PLACEHOLDER_PATTERN = /\[NEEDS? LLM INPUT\]/;
821
+ function hasManagedAutoRegion(content) {
822
+ return count(content, AUTO_BEGIN) === 1 && count(content, AUTO_END) === 1 && content.indexOf(AUTO_BEGIN) < content.indexOf(AUTO_END);
603
823
  }
604
- function renderAgentsPlaceholderWarning(count2) {
605
- return `AGENTS.md still contains ${count2} LLM placeholder(s). Run fet fill-context first so your IDE AI can replace them. Continuing current command.`;
824
+ function hasInvalidManagedAutoRegion(content) {
825
+ const beginCount = count(content, AUTO_BEGIN);
826
+ const endCount = count(content, AUTO_END);
827
+ return beginCount !== endCount || beginCount > 1 || endCount > 1 || beginCount === 1 && content.indexOf(AUTO_BEGIN) > content.indexOf(AUTO_END);
606
828
  }
607
-
608
- // src/commands/update-context.ts
609
- async function updateContextCommand(ctx) {
610
- let contextResult = { warnings: [] };
611
- await withProjectLock(
612
- ctx.projectRoot,
613
- { command: "update-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
614
- async () => {
615
- contextResult = await updateContextFiles(ctx);
616
- }
617
- );
618
- ctx.output.result({
619
- ok: true,
620
- command: "update-context",
621
- summary: "\u5DF2\u66F4\u65B0 AGENTS.md \u4E0E openspec/config.yaml \u7684 FET \u6258\u7BA1\u533A\u57DF\u3002",
622
- warnings: contextResult.warnings
623
- });
829
+ function replaceManagedRegion(existing, generated) {
830
+ if (!existing) {
831
+ return generated;
832
+ }
833
+ const start = existing.indexOf(AUTO_BEGIN);
834
+ const end = existing.indexOf(AUTO_END);
835
+ if (start === -1 || end === -1 || end < start) {
836
+ return generated;
837
+ }
838
+ const before = existing.slice(0, start);
839
+ const after = existing.slice(end + AUTO_END.length);
840
+ const existingAuto = extractAuto(existing);
841
+ const generatedAuto = extractAuto(generated);
842
+ return `${before}${AUTO_BEGIN}
843
+ ${mergeAutoRegion(existingAuto, generatedAuto)}
844
+ ${AUTO_END}${after}`;
624
845
  }
625
- async function updateContextFiles(ctx) {
626
- const scan = await ctx.scanner.scan(ctx.projectRoot, {});
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");
632
- const existingAgents = await readOptional(agentsPath);
633
- const existingClaude = await readOptional(claudePath);
634
- const existingKarpathyCursor = await readOptional(karpathyCursorPath);
635
- const warnings = [...scan.warnings];
636
- if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
637
- throw new FetError({
638
- code: "CONFIG_INVALID" /* ConfigInvalid */,
639
- message: "AGENTS.md \u7684 FET \u6258\u7BA1\u6807\u8BB0\u635F\u574F\u6216\u91CD\u590D",
640
- details: { path: "AGENTS.md" },
641
- suggestedCommand: "\u624B\u52A8\u4FEE\u590D FET:BEGIN AUTO / FET:END AUTO \u6807\u8BB0\u540E\u91CD\u8BD5"
642
- });
846
+ function mergeAutoRegion(existingAuto, generatedAuto) {
847
+ const generatedSections = splitMarkdownSections(generatedAuto);
848
+ const existingSections = new Map(splitMarkdownSections(existingAuto).map((section) => [sectionKey(section.heading), section]));
849
+ if (!generatedSections.length || !existingSections.size) {
850
+ return generatedAuto;
643
851
  }
644
- if (existingAgents && !hasManagedAutoRegion(existingAgents)) {
645
- if (!ctx.yes) {
646
- throw new FetError({
647
- code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
648
- message: "AGENTS.md \u5DF2\u5B58\u5728\u4E14\u4E0D\u5305\u542B FET \u6258\u7BA1\u533A\u57DF",
649
- details: { path: "AGENTS.md" },
650
- suggestedCommand: ctx.command === "init" ? "\u786E\u8BA4\u53EF\u5907\u4EFD\u5E76\u66FF\u6362\u540E\u8FD0\u884C fet init --yes" : "\u786E\u8BA4\u53EF\u5907\u4EFD\u5E76\u66FF\u6362\u540E\u8FD0\u884C fet update-context --yes"
651
- });
652
- }
653
- const backupPath = await createBackup(agentsPath);
654
- if (backupPath) {
655
- warnings.push(`\u5DF2\u5907\u4EFD\u975E\u6258\u7BA1 AGENTS.md \u5230 ${backupPath}`);
852
+ return generatedSections.map((section) => {
853
+ const existing = existingSections.get(sectionKey(section.heading));
854
+ if (existing && LLM_PLACEHOLDER_PATTERN.test(section.body) && !LLM_PLACEHOLDER_PATTERN.test(existing.body)) {
855
+ return existing.raw.trim();
656
856
  }
857
+ return section.raw.trim();
858
+ }).join("\n\n");
859
+ }
860
+ function sectionKey(heading) {
861
+ const normalized = heading.replace(/^##\s+/, "").trim().toLowerCase();
862
+ const aliases = {
863
+ "project snapshot": "snapshot",
864
+ "\u9879\u76EE\u5FEB\u7167": "snapshot",
865
+ workspaces: "workspaces",
866
+ "\u5DE5\u4F5C\u533A": "workspaces",
867
+ commands: "commands",
868
+ "\u547D\u4EE4": "commands",
869
+ structure: "structure",
870
+ "\u7ED3\u6784": "structure",
871
+ routes: "routes",
872
+ "\u8DEF\u7531": "routes",
873
+ conventions: "conventions",
874
+ "\u7EA6\u5B9A": "conventions",
875
+ "ai work guidelines": "ai-guidelines",
876
+ "ai \u5DE5\u4F5C\u6307\u5357": "ai-guidelines",
877
+ "scanner metadata": "metadata",
878
+ "\u626B\u63CF\u5143\u6570\u636E": "metadata",
879
+ "notes for ai": "notes",
880
+ "\u7ED9 ai \u7684\u5907\u6CE8": "notes"
881
+ };
882
+ return aliases[normalized] ?? normalized;
883
+ }
884
+ function splitMarkdownSections(content) {
885
+ const matches = [...content.matchAll(/^## .+$/gm)];
886
+ if (!matches.length) {
887
+ return [];
657
888
  }
658
- await atomicWrite(agentsPath, replaceManagedRegion(existingAgents, renderAgentsMd(scan)));
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
- }
667
- const placeholderCount = await countAgentsLlmPlaceholders(ctx.projectRoot);
668
- if (placeholderCount > 0) {
669
- warnings.push(renderAgentsPlaceholderWarning(placeholderCount));
889
+ return matches.map((match, index) => {
890
+ const start = match.index ?? 0;
891
+ const end = matches[index + 1]?.index ?? content.length;
892
+ const raw = content.slice(start, end).trim();
893
+ const newline = raw.indexOf("\n");
894
+ const heading = newline === -1 ? raw.trim() : raw.slice(0, newline).trim();
895
+ const body = newline === -1 ? "" : raw.slice(newline + 1).trim();
896
+ return { heading, body, raw };
897
+ });
898
+ }
899
+ function extractAuto(content) {
900
+ const start = content.indexOf(AUTO_BEGIN);
901
+ const end = content.indexOf(AUTO_END);
902
+ if (start === -1 || end === -1 || end < start) {
903
+ return content.trim();
670
904
  }
671
- const state = await ctx.stateStore.getOrCreateGlobal();
672
- state.context = {
673
- agentsMdUpdatedAt: scan.generatedAt,
674
- configUpdatedAt: scan.generatedAt,
675
- scannerVersion: scan.scannerVersion
676
- };
677
- await ctx.stateStore.writeGlobal(state);
678
- return { warnings };
905
+ return content.slice(start + AUTO_BEGIN.length, end).trim();
679
906
  }
680
- async function readOptional(path) {
681
- try {
682
- return await readFile5(path, "utf8");
683
- } catch {
684
- return null;
907
+ function count(content, needle) {
908
+ return content.split(needle).length - 1;
909
+ }
910
+
911
+ // src/templates/agents-md.ts
912
+ function renderAgentsMd(scan, language = "zh-CN") {
913
+ if (language === "en") {
914
+ return renderAgentsMdEn(scan);
685
915
  }
916
+ return renderAgentsMdZh(scan);
686
917
  }
918
+ function renderAgentsMdZh(scan) {
919
+ const commands = Object.entries(scan.commands).map(([name, command]) => `| ${name} | \`${command.command}\` | ${command.source} |`).join("\n");
920
+ const routes = scan.routes.map((route) => `| ${route.path} | ${route.source} | ${route.confidence}${route.inferred ? " inferred" : ""} |`).join("\n");
921
+ const workspaces = scan.project.workspaces.map((workspace) => `| ${workspace.name} | ${workspace.path} | ${workspace.source} |`).join("\n");
922
+ return `# \u9879\u76EE\u4E0A\u4E0B\u6587
687
923
 
688
- // src/commands/init.ts
689
- async function initCommand(ctx) {
690
- const alreadyInitialized = await exists(join8(ctx.projectRoot, "openspec", "config.yaml"));
691
- let warnings = [];
692
- await withProjectLock(
693
- ctx.projectRoot,
694
- { command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
695
- async () => {
696
- const journal = createInitJournal(ctx.fetVersion);
697
- await writeInitJournal(ctx.projectRoot, journal);
698
- const identity = await ctx.openSpec.resolveExecutable();
699
- if (!alreadyInitialized) {
700
- const result = await ctx.openSpec.run("init", ["--tools", "none"], { cwd: ctx.projectRoot, stdio: "inherit" });
701
- if (result.exitCode !== 0) {
702
- process.exitCode = result.exitCode;
703
- return;
704
- }
705
- }
706
- const contextResult = await updateContextFiles(ctx);
707
- warnings = contextResult.warnings;
708
- await ensureGitignore(ctx);
709
- const state = await ctx.stateStore.getOrCreateGlobal();
710
- state.openspec = identity;
711
- state.graph ??= {};
712
- const gitnexus = mergeGitNexusGraphInfo(
713
- toGitNexusState(await detectGitNexus(), state.graph.gitnexus),
714
- await inspectGitNexusGraph(ctx.projectRoot)
715
- );
716
- if (!gitnexus.installed && !gitnexus.recommendationShownAt) {
717
- warnings.push(renderGitNexusRecommendation(gitnexus));
718
- gitnexus.recommendationShownAt = (/* @__PURE__ */ new Date()).toISOString();
719
- }
720
- state.graph.gitnexus = gitnexus;
721
- for (const adapter of ctx.toolAdapters) {
722
- const plan = await adapter.planInstall(ctx.projectRoot);
723
- const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
724
- state.toolAdapters[adapter.tool] = {
725
- adapterVersion: adapter.adapterVersion,
726
- installed: true,
727
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
728
- };
729
- journal.steps.push(...result.written.map((path) => ({ operation: "write", path, status: "done" })));
730
- }
731
- journal.completedAt = (/* @__PURE__ */ new Date()).toISOString();
732
- await writeInitJournal(ctx.projectRoot, journal);
733
- await ctx.stateStore.writeGlobal(state);
734
- }
735
- );
736
- ctx.output.result({
737
- ok: true,
738
- command: "init",
739
- summary: "FET \u521D\u59CB\u5316\u5B8C\u6210\u3002",
740
- warnings,
741
- nextSteps: ["\u4F7F\u7528 fet propose/new \u521B\u5EFA OpenSpec change", "\u4F7F\u7528 fet doctor \u68C0\u67E5\u9879\u76EE\u72B6\u6001"]
742
- });
743
- }
744
- async function ensureGitignore(ctx) {
745
- const gitignorePath = join8(ctx.projectRoot, ".gitignore");
746
- const existing = await readOptional2(gitignorePath);
747
- await atomicWrite(gitignorePath, mergeGitignore(existing));
748
- }
749
- async function readOptional2(path) {
750
- try {
751
- return await readFile6(path, "utf8");
752
- } catch {
753
- return null;
754
- }
755
- }
756
- async function exists(path) {
757
- try {
758
- await stat3(path);
759
- return true;
760
- } catch {
761
- return false;
762
- }
763
- }
924
+ ${AUTO_BEGIN}
925
+ ## \u9879\u76EE\u5FEB\u7167
764
926
 
765
- // src/commands/doctor.ts
766
- import { readFile as readFile7, stat as stat4 } from "fs/promises";
767
- import { join as join9 } from "path";
768
- async function doctorCommand(ctx, options = {}) {
769
- const checks = [];
770
- checks.push(await checkOpenSpec(ctx));
771
- checks.push(await checkState(ctx));
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"));
774
- checks.push(await checkPlaceholders(ctx.projectRoot));
775
- checks.push(await checkGitNexus(ctx));
776
- for (const adapter of ctx.toolAdapters) {
777
- checks.push(...await adapter.doctor(ctx.projectRoot));
778
- }
779
- const lockPath = join9(ctx.projectRoot, "openspec", ".fet.lock");
780
- if (await exists2(lockPath)) {
781
- if (options.fixLock) {
782
- await clearLock(ctx.projectRoot);
783
- checks.push({ id: "lock", status: "pass", message: "\u5DF2\u6E05\u7406 openspec/.fet.lock" });
784
- } else {
785
- checks.push({ id: "lock", status: "warn", message: "\u5B58\u5728 openspec/.fet.lock", suggestedCommand: "fet doctor --fix-lock" });
786
- }
787
- }
788
- const warnings = checks.filter((check) => check.status !== "pass").map((check) => check.message);
789
- ctx.output.result({
790
- ok: true,
791
- command: "doctor",
792
- summary: warnings.length ? `\u8BCA\u65AD\u5B8C\u6210\uFF0C\u53D1\u73B0 ${warnings.length} \u4E2A\u9700\u8981\u5173\u6CE8\u7684\u95EE\u9898\u3002` : "\u8BCA\u65AD\u5B8C\u6210\uFF0C\u672A\u53D1\u73B0\u660E\u663E\u95EE\u9898\u3002",
793
- warnings,
794
- data: checks
795
- });
796
- }
797
- async function checkGitNexus(ctx) {
798
- const global = await ctx.stateStore.readGlobal();
799
- const state = mergeGitNexusGraphInfo(
800
- toGitNexusState(await detectGitNexus(), global?.graph?.gitnexus),
801
- await inspectGitNexusGraph(ctx.projectRoot)
802
- );
803
- if (global) {
804
- global.graph ??= {};
805
- global.graph.gitnexus = state;
806
- await ctx.stateStore.writeGlobal(global);
807
- }
808
- return state.installed ? {
809
- id: "gitnexus",
810
- status: "pass",
811
- message: `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"}), graph ${state.graphExists ? "found" : "not found"}`
812
- } : {
813
- id: "gitnexus",
814
- status: "warn",
815
- message: "Optional GitNexus code graph support is not installed",
816
- suggestedCommand: "Install GitNexus later if you want OpenSpec artifacts to prefer a repository graph"
817
- };
818
- }
819
- async function checkOpenSpec(ctx) {
820
- try {
821
- const identity = await ctx.openSpec.resolveExecutable();
822
- return { id: "openspec", status: "pass", message: `OpenSpec: ${identity.executablePath} (${identity.version})` };
823
- } catch (error) {
824
- return { id: "openspec", status: "fail", message: error instanceof Error ? error.message : "OpenSpec \u68C0\u6D4B\u5931\u8D25" };
825
- }
826
- }
827
- async function checkState(ctx) {
828
- try {
829
- const state = await ctx.stateStore.readGlobal();
830
- return state ? { id: "state", status: "pass", message: "FET \u5168\u5C40\u72B6\u6001\u53EF\u8BFB\u53D6" } : { id: "state", status: "warn", message: "FET \u5168\u5C40\u72B6\u6001\u5C1A\u672A\u521D\u59CB\u5316", suggestedCommand: "fet init" };
831
- } catch (error) {
832
- return { id: "state", status: "fail", message: error instanceof Error ? error.message : "FET \u72B6\u6001\u8BFB\u53D6\u5931\u8D25" };
833
- }
834
- }
835
- async function checkFile(id, path, missing, suggestedCommand) {
836
- return await exists2(path) ? { id, status: "pass", message: `${id} \u5B58\u5728` } : { id, status: "warn", message: missing, suggestedCommand };
837
- }
838
- async function checkPlaceholders(projectRoot) {
839
- try {
840
- await readFile7(join9(projectRoot, "AGENTS.md"), "utf8");
841
- const count2 = await countAgentsLlmPlaceholders(projectRoot);
842
- return count2 ? {
843
- id: "context-placeholders",
844
- status: "warn",
845
- message: `AGENTS.md has ${count2} LLM placeholder(s)`,
846
- suggestedCommand: "fet fill-context"
847
- } : { id: "context-placeholders", status: "pass", message: "AGENTS.md placeholders resolved" };
848
- } catch {
849
- return { id: "context-placeholders", status: "warn", message: "AGENTS.md missing", suggestedCommand: "fet update-context" };
850
- }
851
- }
852
- async function exists2(path) {
853
- try {
854
- await stat4(path);
855
- return true;
856
- } catch {
857
- return false;
858
- }
859
- }
927
+ - \u540D\u79F0\uFF1A${scan.project.name}
928
+ - \u5305\u7BA1\u7406\u5668\uFF1A${scan.project.packageManager}\uFF08${scan.project.packageManagerConfidence}\uFF09
929
+ - \u6846\u67B6\uFF1A${scan.project.framework.name}\uFF08${scan.project.framework.confidence}\uFF09
930
+ - \u8BED\u8A00\uFF1A${scan.project.language}
931
+ - Monorepo\uFF1A${scan.project.monorepo ? "\u662F" : "\u5426"}
860
932
 
861
- // src/commands/fill-context.ts
862
- import { mkdir as mkdir3 } from "fs/promises";
863
- import { dirname as dirname5, join as join10 } from "path";
864
- async function fillContextCommand(ctx) {
865
- await withProjectLock(
866
- ctx.projectRoot,
867
- { command: "fill-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
868
- async () => {
869
- const handoffPath = join10(ctx.projectRoot, ".fet", "fill-context.md");
870
- await mkdir3(dirname5(handoffPath), { recursive: true });
871
- await atomicWrite(handoffPath, renderGenericHandoff());
872
- for (const adapter of ctx.toolAdapters) {
873
- const plan = await adapter.planInstall(ctx.projectRoot);
874
- const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
875
- const state = await ctx.stateStore.getOrCreateGlobal();
876
- state.toolAdapters[adapter.tool] = {
877
- adapterVersion: adapter.adapterVersion,
878
- installed: true,
879
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
880
- };
881
- await ctx.stateStore.writeGlobal(state);
882
- if (ctx.verbose) {
883
- ctx.output.info(`Updated ${adapter.tool} adapter`, { written: result.written });
884
- }
885
- }
886
- }
887
- );
888
- const placeholders = await countAgentsLlmPlaceholders(ctx.projectRoot);
889
- ctx.output.result({
890
- ok: true,
891
- command: "fill-context",
892
- summary: placeholders ? `Found ${placeholders} AGENTS.md placeholder(s). Use your IDE AI to fill them.` : "No AGENTS.md placeholders found. IDE fill-context commands were refreshed.",
893
- nextSteps: placeholders ? [
894
- "Cursor: run /fet-fill-context",
895
- "Codex: run /prompts:fet-fill-context",
896
- "OpenCode or other IDEs: open .fet/fill-context.md or run fet fill-context for handoff instructions"
897
- ] : ["Run fet doctor to confirm project context health"],
898
- data: {
899
- placeholders,
900
- cursorCommand: "/fet-fill-context",
901
- codexCommand: "/prompts:fet-fill-context"
902
- }
903
- });
904
- }
905
- function renderGenericHandoff() {
906
- return `<!-- FET:MANAGED
907
- schemaVersion: 1
908
- generator: fill-context
909
- FET:END -->
933
+ ## \u5DE5\u4F5C\u533A
910
934
 
911
- # FET Fill Context
935
+ | \u540D\u79F0 | \u8DEF\u5F84 | \u6765\u6E90 |
936
+ |------|------|--------|
937
+ ${workspaces || "| root | . | inferred |"}
912
938
 
913
- Use the IDE AI to complete FET-generated placeholders.
939
+ ## \u547D\u4EE4
914
940
 
915
- 1. Read AGENTS.md and openspec/config.yaml.
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.
941
+ | \u540D\u79F0 | \u547D\u4EE4 | \u6765\u6E90 |
942
+ |------|---------|--------|
943
+ ${commands || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | [NEEDS LLM INPUT] |"}
944
+
945
+ ## \u7ED3\u6784
946
+
947
+ [NEEDS LLM INPUT]
948
+
949
+ ## \u8DEF\u7531
950
+
951
+ | \u8DEF\u7531 | \u6765\u6E90 | \u7F6E\u4FE1\u5EA6 |
952
+ |-------|--------|------------|
953
+ ${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
954
+
955
+ ## \u7EA6\u5B9A
956
+
957
+ [NEEDS LLM INPUT]
958
+
959
+ ## AI \u5DE5\u4F5C\u6307\u5357
960
+
961
+ - \u4F7F\u7528 FET \u6258\u7BA1\u7684 IDE \u5DE5\u4F5C\u6D41\u65F6\uFF0C\u4F18\u5148\u53C2\u8003 .fet/karpathy-guidelines.md \u4E2D\u7684\u9879\u76EE\u7EA7\u6307\u5357\u3002
962
+ - \u5BF9 Codex\uFF0C\u8FD8\u5E94\u5728\u5B58\u5728\u65F6\u9605\u8BFB .codex/fet/karpathy-guidelines.md\u3002
963
+ - \u8FD9\u4E9B\u6307\u5357\u4F4E\u4E8E\u7528\u6237\u6700\u65B0\u8BF7\u6C42\u548C\u660E\u786E\u7684 OpenSpec \u4EA7\u7269\u3002
964
+ - \u9ED8\u8BA4\u4F7F\u7528\u4E2D\u6587\u4EA7\u51FA\u4EA4\u4E92\u4FE1\u606F\u3001\u8BA1\u5212\u6587\u6863\u548C\u5B9E\u73B0\u8BF4\u660E\uFF0C\u9664\u975E\u7528\u6237\u53E6\u6709\u660E\u786E\u8981\u6C42\u3002
965
+
966
+ ## \u626B\u63CF\u5143\u6570\u636E
967
+
968
+ - \u751F\u6210\u65F6\u95F4\uFF1A${scan.generatedAt}
969
+ - FET \u7248\u672C\uFF1A${FET_VERSION}
970
+ - \u626B\u63CF\u5668\u7248\u672C\uFF1A${scan.scannerVersion}
971
+ - \u8B66\u544A\uFF1A${scan.warnings.length ? scan.warnings.join("; ") : "\u65E0"}
972
+ ${AUTO_END}
973
+
974
+ ${USER_BEGIN}
975
+ ## \u7ED9 AI \u7684\u5907\u6CE8
976
+
977
+ [NEEDS LLM INPUT]
978
+ ${USER_END}
922
979
  `;
923
980
  }
981
+ function renderAgentsMdEn(scan) {
982
+ const commands = Object.entries(scan.commands).map(([name, command]) => `| ${name} | \`${command.command}\` | ${command.source} |`).join("\n");
983
+ const routes = scan.routes.map((route) => `| ${route.path} | ${route.source} | ${route.confidence}${route.inferred ? " inferred" : ""} |`).join("\n");
984
+ const workspaces = scan.project.workspaces.map((workspace) => `| ${workspace.name} | ${workspace.path} | ${workspace.source} |`).join("\n");
985
+ return `# Project Context
924
986
 
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
- });
987
+ ${AUTO_BEGIN}
988
+ ## Project Snapshot
989
+
990
+ - Name: ${scan.project.name}
991
+ - Package Manager: ${scan.project.packageManager} (${scan.project.packageManagerConfidence})
992
+ - Framework: ${scan.project.framework.name} (${scan.project.framework.confidence})
993
+ - Language: ${scan.project.language}
994
+ - Monorepo: ${scan.project.monorepo ? "yes" : "no"}
995
+
996
+ ## Workspaces
997
+
998
+ | Name | Path | Source |
999
+ |------|------|--------|
1000
+ ${workspaces || "| root | . | inferred |"}
1001
+
1002
+ ## Commands
1003
+
1004
+ | Name | Command | Source |
1005
+ |------|---------|--------|
1006
+ ${commands || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | [NEEDS LLM INPUT] |"}
1007
+
1008
+ ## Structure
1009
+
1010
+ [NEEDS LLM INPUT]
1011
+
1012
+ ## Routes
1013
+
1014
+ | Route | Source | Confidence |
1015
+ |-------|--------|------------|
1016
+ ${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
1017
+
1018
+ ## Conventions
1019
+
1020
+ [NEEDS LLM INPUT]
1021
+
1022
+ ## AI Work Guidelines
1023
+
1024
+ - Prefer the project-level Andrej Karpathy inspired guidelines in .fet/karpathy-guidelines.md when using FET-managed IDE workflows.
1025
+ - For Codex, also read .codex/fet/karpathy-guidelines.md when present.
1026
+ - Treat those guidelines as secondary to the user's latest request and explicit OpenSpec artifacts.
1027
+
1028
+ ## Scanner Metadata
1029
+
1030
+ - Generated At: ${scan.generatedAt}
1031
+ - FET Version: ${FET_VERSION}
1032
+ - Scanner Version: ${scan.scannerVersion}
1033
+ - Warnings: ${scan.warnings.length ? scan.warnings.join("; ") : "none"}
1034
+ ${AUTO_END}
1035
+
1036
+ ${USER_BEGIN}
1037
+ ## Notes For AI
1038
+
1039
+ [NEEDS LLM INPUT]
1040
+ ${USER_END}
1041
+ `;
1031
1042
  }
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()
1043
+
1044
+ // src/templates/config-yaml.ts
1045
+ import { stringify } from "yaml";
1046
+ function renderFetConfig(scan, language = "zh-CN") {
1047
+ return stringify({
1048
+ fet: {
1049
+ schemaVersion: 1,
1050
+ generatedAt: scan.generatedAt,
1051
+ fetVersion: FET_VERSION,
1052
+ language,
1053
+ scannerVersion: scan.scannerVersion,
1054
+ project: {
1055
+ packageManager: scan.project.packageManager,
1056
+ packageManagerConfidence: scan.project.packageManagerConfidence,
1057
+ framework: scan.project.framework,
1058
+ language: scan.project.language,
1059
+ monorepo: scan.project.monorepo,
1060
+ workspaces: scan.project.workspaces
1061
+ },
1062
+ commands: scan.commands,
1063
+ validation: {
1064
+ monorepo: scan.project.monorepo,
1065
+ missing: {
1066
+ lint: "warn",
1067
+ typecheck: "warn",
1068
+ test: "warn"
1069
+ },
1070
+ workspaces: scan.project.workspaces
1071
1071
  }
1072
1072
  }
1073
1073
  });
1074
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
- };
1075
+
1076
+ // src/templates/karpathy-skills.ts
1077
+ var KARPATHY_SKILLS_SOURCE = "https://github.com/forrestchang/andrej-karpathy-skills";
1078
+ var BEGIN = "<!-- FET:BEGIN ANDREJ-KARPATHY-SKILLS -->";
1079
+ var END = "<!-- FET:END ANDREJ-KARPATHY-SKILLS -->";
1080
+ function mergeKarpathyClaudeMd(existing, language = "zh-CN") {
1081
+ const block = renderManagedBlock(renderKarpathyClaudeGuidelines(language));
1082
+ if (!existing || !existing.trim()) {
1083
+ return `${block}
1084
+ `;
1088
1085
  }
1089
- if (options.write ?? true) {
1090
- global.graph.gitnexus = state;
1091
- await ctx.stateStore.writeGlobal(global);
1086
+ const start = existing.indexOf(BEGIN);
1087
+ const end = existing.indexOf(END);
1088
+ if (start !== -1 && end !== -1 && end > start) {
1089
+ return `${existing.slice(0, start)}${block}${existing.slice(end + END.length)}`;
1092
1090
  }
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
- };
1091
+ return `${existing.replace(/\s*$/, "")}
1092
+
1093
+ ${block}
1094
+ `;
1102
1095
  }
1103
- async function writeHandoffFile(path, content) {
1104
- await mkdir4(dirname6(path), { recursive: true });
1105
- await atomicWrite(path, content);
1096
+ function renderKarpathyCursorRule(language = "zh-CN") {
1097
+ return `<!-- FET:MANAGED
1098
+ schemaVersion: 1
1099
+ generator: karpathy-skills
1100
+ FET:END -->
1101
+
1102
+ ---
1103
+ description: ${language === "en" ? "Andrej Karpathy inspired coding guidelines" : "\u53D7 Andrej Karpathy \u542F\u53D1\u7684\u7F16\u7801\u6307\u5357"}
1104
+ alwaysApply: true
1105
+ ---
1106
+
1107
+ ${renderKarpathyGuidelinesBody(language)}
1108
+ `;
1106
1109
  }
1107
- function renderGraphSetupHandoff(state) {
1110
+ function renderKarpathyFetHandoff(language = "zh-CN") {
1108
1111
  return `<!-- FET:MANAGED
1109
1112
  schemaVersion: 1
1110
- generator: graph-setup
1113
+ generator: karpathy-skills
1111
1114
  FET:END -->
1112
1115
 
1113
- # FET Graph Setup
1116
+ # ${language === "en" ? "Andrej Karpathy Inspired Coding Guidelines" : "\u53D7 Andrej Karpathy \u542F\u53D1\u7684\u7F16\u7801\u6307\u5357"}
1114
1117
 
1115
- GitNexus graph support is optional. FET does not install GitNexus automatically and does not require graph support for OpenSpec workflows.
1118
+ ${renderKarpathyGuidelinesBody(language)}
1119
+ `;
1120
+ }
1121
+ function renderManagedBlock(content) {
1122
+ return `${BEGIN}
1123
+ ${content}
1124
+ ${END}`;
1125
+ }
1126
+ function renderKarpathyClaudeGuidelines(language) {
1127
+ return `# ${language === "en" ? "Andrej Karpathy Inspired Coding Guidelines" : "\u53D7 Andrej Karpathy \u542F\u53D1\u7684\u7F16\u7801\u6307\u5357"}
1116
1128
 
1117
- Current status:
1129
+ ${renderKarpathyGuidelinesBody(language)}`;
1130
+ }
1131
+ function renderKarpathyGuidelinesBody(language = "zh-CN") {
1132
+ if (language === "en") {
1133
+ return renderKarpathyGuidelinesBodyEn();
1134
+ }
1135
+ return `\u6765\u6E90\uFF1A${KARPATHY_SKILLS_SOURCE}
1118
1136
 
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"}
1137
+ \u5C06\u8FD9\u4E9B\u9879\u76EE\u7EA7\u6307\u5357\u4E0E AGENTS.md\u3001OpenSpec \u4EA7\u7269\u548C\u7528\u6237\u6700\u65B0\u8BF7\u6C42\u4E00\u8D77\u4F7F\u7528\u3002
1124
1138
 
1125
- Suggested setup flow:
1139
+ ## \u7F16\u7801\u524D\u5148\u601D\u8003
1126
1140
 
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.
1141
+ - \u7F16\u8F91\u524D\u8BF4\u660E\u91CD\u8981\u5047\u8BBE\u3002
1142
+ - \u5F53\u6B67\u4E49\u4F1A\u6539\u53D8\u5B9E\u73B0\u65F6\uFF0C\u5148\u6F84\u6E05\u3002
1143
+ - \u4E3B\u52A8\u5448\u73B0\u53D6\u820D\uFF0C\u4E0D\u8981\u9759\u9ED8\u9009\u62E9\u9AD8\u98CE\u9669\u8DEF\u5F84\u3002
1144
+ - \u5F53\u66F4\u7B80\u5355\u7684\u65B9\u6848\u66F4\u9002\u5408\u8BF7\u6C42\u65F6\uFF0C\u660E\u786E\u63D0\u51FA\u3002
1131
1145
 
1132
- Guardrails:
1146
+ ## \u7B80\u6D01\u4F18\u5148
1133
1147
 
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.
1137
- `;
1148
+ - \u7528\u6700\u5C0F\u4E14\u6E05\u6670\u7684\u6539\u52A8\u89E3\u51B3\u8BF7\u6C42\u7684\u95EE\u9898\u3002
1149
+ - \u907F\u514D\u731C\u6D4B\u6027\u529F\u80FD\u3001\u914D\u7F6E\u6216\u62BD\u8C61\u3002
1150
+ - \u4E0D\u8981\u4E3A\u4E00\u6B21\u6027\u4EE3\u7801\u521B\u5EFA\u62BD\u8C61\u3002
1151
+ - \u4F18\u5148\u5220\u9664\u81EA\u5DF1\u6539\u52A8\u5F15\u5165\u7684\u590D\u6742\u5EA6\uFF0C\u800C\u4E0D\u662F\u7EE7\u7EED\u5806\u7ED3\u6784\u3002
1152
+
1153
+ ## \u7CBE\u51C6\u7F16\u8F91
1154
+
1155
+ - \u53EA\u4FEE\u6539\u76F4\u63A5\u670D\u52A1\u4E8E\u4EFB\u52A1\u7684\u6587\u4EF6\u548C\u884C\u3002
1156
+ - \u4FDD\u6301\u73B0\u6709\u98CE\u683C\uFF0C\u5373\u4FBF\u4F60\u4E2A\u4EBA\u66F4\u504F\u597D\u53E6\u4E00\u79CD\u6A21\u5F0F\u3002
1157
+ - \u9664\u975E\u4EFB\u52A1\u9700\u8981\uFF0C\u4E0D\u8981\u987A\u624B\u91CD\u6784\u9644\u8FD1\u4EE3\u7801\u3001\u6CE8\u91CA\u6216\u683C\u5F0F\u3002
1158
+ - \u53EA\u79FB\u9664\u56E0\u81EA\u5DF1\u6539\u52A8\u800C\u8FC7\u65F6\u7684\u6B7B\u5BFC\u5165\u3001\u53D8\u91CF\u6216 helper\u3002
1159
+
1160
+ ## \u76EE\u6807\u9A71\u52A8\u6267\u884C
1161
+
1162
+ - \u628A\u6A21\u7CCA\u5DE5\u4F5C\u8F6C\u6210\u5177\u4F53\u6210\u529F\u6807\u51C6\u3002
1163
+ - \u5BF9 bug\uFF0C\u4F18\u5148\u51C6\u5907\u590D\u73B0\u6D4B\u8BD5\u6216\u6E05\u6670\u9A8C\u8BC1\uFF0C\u518D\u505A\u4FEE\u590D\u3002
1164
+ - \u5BF9\u591A\u6B65\u9AA4\u5DE5\u4F5C\uFF0C\u4FDD\u7559\u7B80\u77ED\u8BA1\u5212\u5E76\u9A8C\u8BC1\u6BCF\u4E2A\u6709\u610F\u4E49\u7684\u6B65\u9AA4\u3002
1165
+ - \u6301\u7EED\u8FED\u4EE3\uFF0C\u76F4\u5230\u8FBE\u5230\u6210\u529F\u6807\u51C6\u6216\u660E\u786E\u9047\u5230\u963B\u585E\u3002
1166
+
1167
+ \u8FD9\u4E9B\u6307\u5357\u5728\u975E\u5E73\u51E1\u5DE5\u4F5C\u4E2D\u523B\u610F\u504F\u5411\u8C28\u614E\u800C\u4E0D\u662F\u901F\u5EA6\u3002\u660E\u663E\u7684\u4E00\u884C\u4FEE\u590D\u53EF\u4EE5\u7075\u6D3B\u5224\u65AD\uFF0C\u4FDD\u6301\u8F7B\u91CF\u3002`;
1138
1168
  }
1139
- function renderGraphUsageHandoff(state) {
1140
- return `<!-- FET:MANAGED
1169
+ function renderKarpathyGuidelinesBodyEn() {
1170
+ return `Source: ${KARPATHY_SKILLS_SOURCE}
1171
+
1172
+ Use these project-level guidelines together with AGENTS.md, OpenSpec artifacts, and the user's latest request.
1173
+
1174
+ ## Think Before Coding
1175
+
1176
+ - State important assumptions before editing.
1177
+ - Ask for clarification when ambiguity would change the implementation.
1178
+ - Surface tradeoffs instead of silently choosing a risky path.
1179
+ - Push back when a simpler approach better fits the request.
1180
+
1181
+ ## Simplicity First
1182
+
1183
+ - Solve the requested problem with the smallest clear change.
1184
+ - Avoid speculative features, configuration, or abstraction.
1185
+ - Do not create abstractions for one-off code.
1186
+ - Prefer deleting complexity introduced by your own change over adding more structure.
1187
+
1188
+ ## Precise Edits
1189
+
1190
+ - Touch only files and lines that directly serve the task.
1191
+ - Preserve existing style even when you personally prefer another pattern.
1192
+ - Do not refactor nearby code, comments, or formatting unless the task requires it.
1193
+ - Remove only dead imports, variables, or helpers made obsolete by your own change.
1194
+
1195
+ ## Goal-Driven Execution
1196
+
1197
+ - Convert vague work into concrete success criteria.
1198
+ - For bugs, prefer a reproducing test or clear verification before the fix.
1199
+ - For multi-step work, keep a short plan and verify each meaningful step.
1200
+ - Continue iterating until the success criteria are met or a blocker is explicit.
1201
+
1202
+ These guidelines intentionally favor caution over speed for non-trivial work. For obvious one-line fixes, use judgment and stay lightweight.`;
1203
+ }
1204
+
1205
+ // src/templates/verify-instructions.ts
1206
+ function renderVerifyInstructions(changeId, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
1207
+ return `---
1141
1208
  schemaVersion: 1
1142
- generator: graph-handoff
1143
- FET:END -->
1209
+ fetVersion: ${FET_VERSION}
1210
+ generatedAt: ${generatedAt}
1211
+ changeId: ${changeId}
1212
+ purpose: manual-verify
1213
+ ---
1144
1214
 
1145
- # FET Graph Handoff
1215
+ # Verify Instructions
1146
1216
 
1147
- Use GitNexus graph context as an optional first pass before broad repository scans.
1217
+ \u8BF7\u6309\u987A\u5E8F\u5B8C\u6210\u4EE5\u4E0B\u68C0\u67E5\uFF1A
1148
1218
 
1149
- Current status:
1219
+ 1. \u8FD0\u884C OpenSpec \u89C4\u8303\u6821\u9A8C\uFF1A\`openspec verify\`
1220
+ 2. \u6309\u9879\u76EE\u7EA6\u5B9A\u8FD0\u884C lint\u3001typecheck\u3001test\u3002
1221
+ 3. \u68C0\u67E5\u672C\u6B21 change \u7684 \`tasks.md\` \u662F\u5426\u4E0E\u5B9E\u73B0\u72B6\u6001\u4E00\u81F4\u3002
1150
1222
 
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"}
1223
+ \u5B8C\u6210\u540E\u8FD0\u884C\uFF1A
1156
1224
 
1157
- When graph context is available:
1225
+ \`\`\`sh
1226
+ fet verify --done --change ${changeId}
1227
+ \`\`\`
1228
+ `;
1229
+ }
1158
1230
 
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.
1231
+ // src/templates/gitignore.ts
1232
+ var BEGIN2 = "# FET:BEGIN LOCAL STATE";
1233
+ var END2 = "# FET:END LOCAL STATE";
1234
+ var RULES = [
1235
+ "openspec/fet-state.json",
1236
+ "openspec/.fet.lock",
1237
+ "openspec/.fet-init-journal.json",
1238
+ "openspec/changes/*/fet-state.json",
1239
+ "openspec/changes/*/.fet/",
1240
+ ".gitnexus/"
1241
+ ];
1242
+ function mergeGitignore(existing) {
1243
+ const block = `${BEGIN2}
1244
+ ${RULES.join("\n")}
1245
+ ${END2}`;
1246
+ if (!existing || !existing.trim()) {
1247
+ return `${block}
1248
+ `;
1249
+ }
1250
+ const start = existing.indexOf(BEGIN2);
1251
+ const end = existing.indexOf(END2);
1252
+ if (start !== -1 && end !== -1 && end > start) {
1253
+ return `${existing.slice(0, start)}${block}${existing.slice(end + END2.length)}`;
1254
+ }
1255
+ return `${existing.replace(/\s*$/, "")}
1163
1256
 
1164
- When producing OpenSpec artifacts:
1257
+ ${block}
1258
+ `;
1259
+ }
1260
+
1261
+ // src/commands/update-context.ts
1262
+ import { readFile as readFile6 } from "fs/promises";
1263
+ import { join as join10 } from "path";
1264
+
1265
+ // src/config/yaml.ts
1266
+ import { readFile as readFile5 } from "fs/promises";
1267
+ import { parseDocument } from "yaml";
1268
+ async function mergeFetConfig(configPath, renderedFetYaml) {
1269
+ const fetDoc = parseDocument(renderedFetYaml);
1270
+ const nextFet = fetDoc.get("fet", true);
1271
+ let existing = "";
1272
+ try {
1273
+ existing = await readFile5(configPath, "utf8");
1274
+ } catch {
1275
+ return renderedFetYaml;
1276
+ }
1277
+ const doc = parseDocument(existing || "{}");
1278
+ doc.set("fet", nextFet);
1279
+ return doc.toString();
1280
+ }
1281
+
1282
+ // src/commands/update-context.ts
1283
+ async function updateContextCommand(ctx) {
1284
+ let contextResult = { warnings: [] };
1285
+ await withProjectLock(ctx.projectRoot, { command: "update-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
1286
+ contextResult = await updateContextFiles(ctx);
1287
+ });
1288
+ ctx.output.result({
1289
+ ok: true,
1290
+ command: "update-context",
1291
+ summary: ctx.language === "en" ? "Updated FET-managed regions in AGENTS.md and openspec/config.yaml." : "\u5DF2\u66F4\u65B0 AGENTS.md \u4E0E openspec/config.yaml \u4E2D\u7684 FET \u6258\u7BA1\u533A\u57DF\u3002",
1292
+ warnings: contextResult.warnings
1293
+ });
1294
+ }
1295
+ async function updateContextFiles(ctx) {
1296
+ const scan = await ctx.scanner.scan(ctx.projectRoot, {});
1297
+ const agentsPath = join10(ctx.projectRoot, "AGENTS.md");
1298
+ const configPath = join10(ctx.projectRoot, "openspec", "config.yaml");
1299
+ const claudePath = join10(ctx.projectRoot, "CLAUDE.md");
1300
+ const karpathyHandoffPath = join10(ctx.projectRoot, ".fet", "karpathy-guidelines.md");
1301
+ const karpathyCursorPath = join10(ctx.projectRoot, ".cursor", "rules", "karpathy-guidelines.mdc");
1302
+ const existingAgents = await readOptional(agentsPath);
1303
+ const existingClaude = await readOptional(claudePath);
1304
+ const existingKarpathyCursor = await readOptional(karpathyCursorPath);
1305
+ const warnings = [...scan.warnings];
1306
+ if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
1307
+ throw new FetError({
1308
+ code: "CONFIG_INVALID" /* ConfigInvalid */,
1309
+ message: ctx.language === "en" ? "AGENTS.md FET managed markers are broken or duplicated." : "AGENTS.md \u7684 FET \u6258\u7BA1\u6807\u8BB0\u635F\u574F\u6216\u91CD\u590D\u3002",
1310
+ details: { path: "AGENTS.md" },
1311
+ suggestedCommand: ctx.language === "en" ? "Manually repair FET:BEGIN AUTO / FET:END AUTO markers, then rerun." : "\u624B\u52A8\u4FEE\u590D FET:BEGIN AUTO / FET:END AUTO \u6807\u8BB0\u540E\u91CD\u8BD5\u3002"
1312
+ });
1313
+ }
1314
+ if (existingAgents && !hasManagedAutoRegion(existingAgents)) {
1315
+ if (!ctx.yes) {
1316
+ throw new FetError({
1317
+ code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
1318
+ message: ctx.language === "en" ? "AGENTS.md already exists and does not contain a FET-managed region." : "AGENTS.md \u5DF2\u5B58\u5728\uFF0C\u4E14\u4E0D\u5305\u542B FET \u6258\u7BA1\u533A\u57DF\u3002",
1319
+ details: { path: "AGENTS.md" },
1320
+ suggestedCommand: ctx.command === "init" ? ctx.language === "en" ? "Confirm it can be backed up and replaced, then run fet init --yes." : "\u786E\u8BA4\u53EF\u5907\u4EFD\u5E76\u66FF\u6362\u540E\u8FD0\u884C fet init --yes\u3002" : ctx.language === "en" ? "Confirm it can be backed up and replaced, then run fet update-context --yes." : "\u786E\u8BA4\u53EF\u5907\u4EFD\u5E76\u66FF\u6362\u540E\u8FD0\u884C fet update-context --yes\u3002"
1321
+ });
1322
+ }
1323
+ const backupPath = await createBackup(agentsPath);
1324
+ if (backupPath) {
1325
+ warnings.push(ctx.language === "en" ? `Backed up unmanaged AGENTS.md to ${backupPath}` : `\u5DF2\u5C06\u975E\u6258\u7BA1 AGENTS.md \u5907\u4EFD\u5230 ${backupPath}`);
1326
+ }
1327
+ }
1328
+ await atomicWrite(agentsPath, replaceManagedRegion(existingAgents, renderAgentsMd(scan, ctx.language)));
1329
+ await atomicWrite(configPath, await mergeFetConfig(configPath, renderFetConfig(scan, ctx.language)));
1330
+ await atomicWrite(claudePath, mergeKarpathyClaudeMd(existingClaude, ctx.language));
1331
+ await atomicWrite(karpathyHandoffPath, renderKarpathyFetHandoff(ctx.language));
1332
+ if (!existingKarpathyCursor || existingKarpathyCursor.includes("FET:MANAGED")) {
1333
+ await atomicWrite(karpathyCursorPath, renderKarpathyCursorRule(ctx.language));
1334
+ } else {
1335
+ warnings.push(
1336
+ ctx.language === "en" ? ".cursor/rules/karpathy-guidelines.mdc exists and is not managed by FET; leaving it unchanged." : ".cursor/rules/karpathy-guidelines.mdc \u5DF2\u5B58\u5728\u4E14\u4E0D\u7531 FET \u6258\u7BA1\uFF0C\u5DF2\u4FDD\u6301\u4E0D\u53D8\u3002"
1337
+ );
1338
+ }
1339
+ const placeholderCount = await countAgentsLlmPlaceholders(ctx.projectRoot);
1340
+ if (placeholderCount > 0) {
1341
+ warnings.push(renderAgentsPlaceholderWarning(placeholderCount, ctx.language));
1342
+ }
1343
+ const state = await ctx.stateStore.getOrCreateGlobal();
1344
+ state.context = {
1345
+ agentsMdUpdatedAt: scan.generatedAt,
1346
+ configUpdatedAt: scan.generatedAt,
1347
+ scannerVersion: scan.scannerVersion
1348
+ };
1349
+ await ctx.stateStore.writeGlobal(state);
1350
+ return { warnings };
1351
+ }
1352
+ async function readOptional(path) {
1353
+ try {
1354
+ return await readFile6(path, "utf8");
1355
+ } catch {
1356
+ return null;
1357
+ }
1358
+ }
1165
1359
 
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
- `;
1360
+ // src/commands/init.ts
1361
+ async function initCommand(ctx) {
1362
+ const alreadyInitialized = await exists2(join11(ctx.projectRoot, "openspec", "config.yaml"));
1363
+ let warnings = [];
1364
+ await withProjectLock(ctx.projectRoot, { command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
1365
+ const journal = createInitJournal(ctx.fetVersion);
1366
+ await writeInitJournal(ctx.projectRoot, journal);
1367
+ const identity = await ctx.openSpec.resolveExecutable();
1368
+ if (!alreadyInitialized) {
1369
+ const result = await ctx.openSpec.run("init", ["--tools", "none"], { cwd: ctx.projectRoot, stdio: "inherit" });
1370
+ if (result.exitCode !== 0) {
1371
+ process.exitCode = result.exitCode;
1372
+ return;
1373
+ }
1374
+ }
1375
+ const contextResult = await updateContextFiles(ctx);
1376
+ warnings = contextResult.warnings;
1377
+ await ensureGitignore(ctx);
1378
+ const state = await ctx.stateStore.getOrCreateGlobal();
1379
+ state.openspec = identity;
1380
+ state.language = {
1381
+ current: ctx.language,
1382
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1383
+ };
1384
+ state.graph ??= {};
1385
+ const gitnexus = mergeGitNexusGraphInfo(toGitNexusState(await detectGitNexus(), state.graph.gitnexus), await inspectGitNexusGraph(ctx.projectRoot));
1386
+ if (!gitnexus.installed && !gitnexus.recommendationShownAt) {
1387
+ warnings.push(renderGitNexusRecommendation(gitnexus, ctx.language));
1388
+ gitnexus.recommendationShownAt = (/* @__PURE__ */ new Date()).toISOString();
1389
+ }
1390
+ state.graph.gitnexus = gitnexus;
1391
+ for (const adapter of ctx.toolAdapters) {
1392
+ const plan = await adapter.planInstall(ctx.projectRoot, ctx.language);
1393
+ const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
1394
+ state.toolAdapters[adapter.tool] = {
1395
+ adapterVersion: adapter.adapterVersion,
1396
+ installed: true,
1397
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1398
+ };
1399
+ journal.steps.push(...result.written.map((path) => ({ operation: "write", path, status: "done" })));
1400
+ }
1401
+ journal.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1402
+ await writeInitJournal(ctx.projectRoot, journal);
1403
+ await ctx.stateStore.writeGlobal(state);
1404
+ });
1405
+ ctx.output.result({
1406
+ ok: true,
1407
+ command: "init",
1408
+ summary: ctx.language === "en" ? "FET initialization completed." : "FET \u521D\u59CB\u5316\u5B8C\u6210\u3002",
1409
+ warnings,
1410
+ nextSteps: ctx.language === "en" ? ["Use fet propose/new to create an OpenSpec change", "Use fet doctor to check project health"] : ["\u4F7F\u7528 fet propose/new \u521B\u5EFA OpenSpec change", "\u4F7F\u7528 fet doctor \u68C0\u67E5\u9879\u76EE\u72B6\u6001"]
1411
+ });
1170
1412
  }
1171
- function firstLine(value) {
1172
- return value.trim().split(/\r?\n/)[0]?.trim() || null;
1413
+ async function ensureGitignore(ctx) {
1414
+ const gitignorePath = join11(ctx.projectRoot, ".gitignore");
1415
+ const existing = await readOptional2(gitignorePath);
1416
+ await atomicWrite(gitignorePath, mergeGitignore(existing));
1417
+ }
1418
+ async function readOptional2(path) {
1419
+ try {
1420
+ return await readFile7(path, "utf8");
1421
+ } catch {
1422
+ return null;
1423
+ }
1424
+ }
1425
+ async function exists2(path) {
1426
+ try {
1427
+ await stat4(path);
1428
+ return true;
1429
+ } catch {
1430
+ return false;
1431
+ }
1173
1432
  }
1174
1433
 
1175
1434
  // src/commands/proxy.ts
@@ -1206,6 +1465,22 @@ async function git(cwd, args) {
1206
1465
  import { mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
1207
1466
  import { join as join12 } from "path";
1208
1467
 
1468
+ // src/language.ts
1469
+ var DEFAULT_LANGUAGE = "zh-CN";
1470
+ function normalizeLanguage(value) {
1471
+ const normalized = value?.trim().toLowerCase();
1472
+ if (!normalized) {
1473
+ return DEFAULT_LANGUAGE;
1474
+ }
1475
+ if (["en", "en-us", "english"].includes(normalized)) {
1476
+ return "en";
1477
+ }
1478
+ return DEFAULT_LANGUAGE;
1479
+ }
1480
+ function languageInstruction(language) {
1481
+ return language === "en" ? "Use English for FET interaction messages, generated handoff documents, and IDE prompt output unless the user asks otherwise." : "\u9664\u975E\u7528\u6237\u53E6\u6709\u660E\u786E\u8981\u6C42\uFF0CFET \u7684\u4EA4\u4E92\u4FE1\u606F\u3001\u751F\u6210\u7684\u4EA4\u63A5\u6587\u6863\u548C IDE \u63D0\u793A\u4EA7\u51FA\u5747\u4F7F\u7528\u4E2D\u6587\u3002";
1482
+ }
1483
+
1209
1484
  // src/state/schema.ts
1210
1485
  var phases = ["explore", "propose", "implement", "verify", "sync", "archive"];
1211
1486
  function createGlobalState(fetVersion, project) {
@@ -1216,6 +1491,10 @@ function createGlobalState(fetVersion, project) {
1216
1491
  createdAt: now,
1217
1492
  updatedAt: now,
1218
1493
  project,
1494
+ language: {
1495
+ current: DEFAULT_LANGUAGE,
1496
+ updatedAt: now
1497
+ },
1219
1498
  openspec: null,
1220
1499
  activeChangeId: null,
1221
1500
  openChangeIds: [],
@@ -1239,9 +1518,7 @@ function createChangeState(fetVersion, changeId, phase) {
1239
1518
  createdAt: now,
1240
1519
  updatedAt: now,
1241
1520
  currentPhase: phase,
1242
- phases: Object.fromEntries(
1243
- phases.map((item) => [item, { status: item === phase ? "in_progress" : "not_started" }])
1244
- ),
1521
+ phases: Object.fromEntries(phases.map((item) => [item, { status: item === phase ? "in_progress" : "not_started" }])),
1245
1522
  tasks: {
1246
1523
  source: "tasks.md",
1247
1524
  completedIds: [],
@@ -1256,6 +1533,12 @@ function assertGlobalState(value) {
1256
1533
  if (!isRecord(value) || value.schemaVersion !== 1) {
1257
1534
  throw unsupportedSchema("\u5168\u5C40\u72B6\u6001 schema \u4E0D\u53D7\u652F\u6301");
1258
1535
  }
1536
+ if (!isRecord(value.language)) {
1537
+ value.language = {
1538
+ current: DEFAULT_LANGUAGE,
1539
+ updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
1540
+ };
1541
+ }
1259
1542
  if (typeof value.fetVersion !== "string" || !isRecord(value.project)) {
1260
1543
  throw corruptedState("\u5168\u5C40\u72B6\u6001\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5");
1261
1544
  }
@@ -1537,14 +1820,16 @@ function stripFetOptions(args) {
1537
1820
  async function mapOpenSpecCommand(ctx, command, args) {
1538
1821
  switch (command) {
1539
1822
  case "propose":
1540
- case "continue":
1541
- case "ff":
1542
- case "apply":
1543
- case "sync":
1544
1823
  case "bulk-archive":
1545
1824
  case "explore":
1546
1825
  case "onboard":
1547
1826
  return { command, args: withGlobalChange(ctx, args) };
1827
+ case "continue":
1828
+ case "ff":
1829
+ return { command, args: await withDefaultChange(ctx, args, true) };
1830
+ case "apply":
1831
+ case "sync":
1832
+ return { command, args: await withDefaultChange(ctx, args) };
1548
1833
  case "new":
1549
1834
  return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
1550
1835
  case "archive":
@@ -1568,6 +1853,18 @@ async function mapOpenSpecCommand(ctx, command, args) {
1568
1853
  function withGlobalChange(ctx, args) {
1569
1854
  return ctx.changeId ? ["--change", ctx.changeId, ...args] : args;
1570
1855
  }
1856
+ async function withDefaultChange(ctx, args, allowWithArgs = false) {
1857
+ if (ctx.changeId) {
1858
+ return ["--change", ctx.changeId, ...args];
1859
+ }
1860
+ if (args.includes("--change") || args.some((arg) => arg.startsWith("--change="))) {
1861
+ return args;
1862
+ }
1863
+ if (args.length > 0 && !allowWithArgs) {
1864
+ return args;
1865
+ }
1866
+ return ["--change", await requireChangeId(ctx), ...args];
1867
+ }
1571
1868
  async function requireChangeId(ctx) {
1572
1869
  if (ctx.changeId) {
1573
1870
  return ctx.changeId;
@@ -1813,8 +2110,18 @@ function detectCurrentModel(env = process.env) {
1813
2110
  function isHighCostModel(model) {
1814
2111
  return HIGH_COST_MODEL_PATTERNS.some((pattern) => pattern.test(model));
1815
2112
  }
2113
+ function getModelPolicyMode(env = process.env) {
2114
+ const value = env.FET_MODEL_POLICY?.trim().toLowerCase();
2115
+ if (value === "off" || env.FET_SKIP_MODEL_POLICY === "1") {
2116
+ return "off";
2117
+ }
2118
+ if (value === "warn") {
2119
+ return "warn";
2120
+ }
2121
+ return "confirm";
2122
+ }
1816
2123
  function getCommandModelPolicyMismatch(command, env = process.env) {
1817
- if (env.FET_MODEL_POLICY === "off" || env.FET_SKIP_MODEL_POLICY === "1") {
2124
+ if (getModelPolicyMode(env) === "off") {
1818
2125
  return null;
1819
2126
  }
1820
2127
  const detected = detectCurrentModel(env);
@@ -1843,15 +2150,26 @@ function getCommandModelPolicyMismatch(command, env = process.env) {
1843
2150
  }
1844
2151
  return null;
1845
2152
  }
1846
- function formatModelPolicyMismatch(mismatch) {
1847
- const switchHint = mismatch.recommended === "high-cost" ? "Recommended models include GPT-5.5, GLM-5.1, GLM-5, Claude Opus, or Claude Sonnet." : "Recommended action: switch to a lower-cost model and reserve high-cost models for fet apply.";
1848
- return `${mismatch.reason} Detected ${mismatch.detected.source}="${mismatch.detected.name}". ${switchHint}`;
2153
+ function formatModelPolicyMismatch(mismatch, language = "zh-CN") {
2154
+ if (language === "en") {
2155
+ const switchHint2 = mismatch.recommended === "high-cost" ? "Recommended models include GPT-5.5, GLM-5.1, GLM-5, Claude Opus, or Claude Sonnet." : "Recommended action: switch to a lower-cost model and reserve high-cost models for fet apply.";
2156
+ return `${mismatch.reason} Detected ${mismatch.detected.source}="${mismatch.detected.name}". ${switchHint2}`;
2157
+ }
2158
+ const switchHint = mismatch.recommended === "high-cost" ? "\u5EFA\u8BAE\u6A21\u578B\u5305\u62EC GPT-5.5\u3001GLM-5.1\u3001GLM-5\u3001Claude Opus \u6216 Claude Sonnet\u3002" : "\u5EFA\u8BAE\u5207\u6362\u5230\u4F4E\u6210\u672C\u6A21\u578B\uFF0C\u628A\u9AD8\u6210\u672C\u6A21\u578B\u7559\u7ED9 fet apply\u3002";
2159
+ const reason = mismatch.recommended === "high-cost" ? "fet apply \u5C5E\u4E8E\u5B9E\u65BD\u9636\u6BB5\uFF0C\u5EFA\u8BAE\u4F7F\u7528\u9AD8\u80FD\u529B/\u9AD8\u6210\u672C\u6A21\u578B\u3002" : `fet ${mismatch.command} \u4E0D\u662F\u5B9E\u65BD\u9636\u6BB5\uFF0C\u5EFA\u8BAE\u4F7F\u7528\u4F4E\u6210\u672C\u6A21\u578B\u3002`;
2160
+ return `${reason} \u68C0\u6D4B\u5230 ${mismatch.detected.source}="${mismatch.detected.name}"\u3002${switchHint}`;
1849
2161
  }
1850
- function renderIdeModelPolicy(command) {
2162
+ function renderIdeModelPolicy(command, language = "zh-CN") {
2163
+ if (language === "en") {
2164
+ if (command === "apply") {
2165
+ return "Model policy: this command is recommended to run with a high-capability/high-cost model such as GPT-5.5, GLM-5.1, GLM-5, Claude Opus, or Claude Sonnet. If the current IDE model is lower-cost, tell the user and ask whether to stop for a model switch or continue anyway. Continue only after the user chooses.";
2166
+ }
2167
+ return "Model policy: this command is recommended to run with a low-cost model. If the current IDE model is GPT-5.5, GLM-5.1, GLM-5, Claude Opus, Claude Sonnet, or another high-cost model, tell the user and ask whether to stop for a model switch or continue anyway. Continue only after the user chooses.";
2168
+ }
1851
2169
  if (command === "apply") {
1852
- return "Model policy: this command is recommended to run with a high-capability/high-cost model such as GPT-5.5, GLM-5.1, GLM-5, Claude Opus, or Claude Sonnet. If the current IDE model is lower-cost, tell the user and ask whether to stop for a model switch or continue anyway.";
2170
+ return "\u6A21\u578B\u7B56\u7565\uFF1A\u8BE5\u547D\u4EE4\u5EFA\u8BAE\u4F7F\u7528\u9AD8\u80FD\u529B/\u9AD8\u6210\u672C\u6A21\u578B\u8FD0\u884C\uFF0C\u4F8B\u5982 GPT-5.5\u3001GLM-5.1\u3001GLM-5\u3001Claude Opus \u6216 Claude Sonnet\u3002\u82E5\u5F53\u524D IDE \u6A21\u578B\u80FD\u529B\u8F83\u4F4E\uFF0C\u8BF7\u544A\u77E5\u7528\u6237\u5E76\u8BE2\u95EE\u662F\u505C\u6B62\u540E\u5207\u6362\u6A21\u578B\uFF0C\u8FD8\u662F\u7EE7\u7EED\u6267\u884C\u5F53\u524D\u547D\u4EE4\uFF1B\u53EA\u6709\u5728\u7528\u6237\u9009\u62E9\u540E\u624D\u7EE7\u7EED\u3002";
1853
2171
  }
1854
- return "Model policy: this command is recommended to run with a low-cost model. If the current IDE model is GPT-5.5, GLM-5.1, GLM-5, Claude Opus, Claude Sonnet, or another high-cost model, tell the user and ask whether to stop for a model switch or continue anyway.";
2172
+ return "\u6A21\u578B\u7B56\u7565\uFF1A\u8BE5\u547D\u4EE4\u5EFA\u8BAE\u4F7F\u7528\u4F4E\u6210\u672C\u6A21\u578B\u8FD0\u884C\u3002\u82E5\u5F53\u524D IDE \u6A21\u578B\u662F GPT-5.5\u3001GLM-5.1\u3001GLM-5\u3001Claude Opus\u3001Claude Sonnet \u6216\u5176\u4ED6\u9AD8\u6210\u672C\u6A21\u578B\uFF0C\u8BF7\u544A\u77E5\u7528\u6237\u5E76\u8BE2\u95EE\u662F\u505C\u6B62\u540E\u5207\u6362\u6A21\u578B\uFF0C\u8FD8\u662F\u7EE7\u7EED\u6267\u884C\u5F53\u524D\u547D\u4EE4\uFF1B\u53EA\u6709\u5728\u7528\u6237\u9009\u62E9\u540E\u624D\u7EE7\u7EED\u3002";
1855
2173
  }
1856
2174
 
1857
2175
  // src/cli/context.ts
@@ -1890,17 +2208,12 @@ function renderFetAdapterUsage(command, args = "[...args]") {
1890
2208
  }
1891
2209
 
1892
2210
  // src/adapters/codex/templates.ts
1893
- function codexGuideFile() {
1894
- return {
1895
- path: ".codex/fet/context.md",
1896
- content: `<!-- FET:MANAGED
1897
- schemaVersion: 1
1898
- fetVersion: ${FET_VERSION}
1899
- generator: codex-adapter
1900
- adapterVersion: 1
1901
- FET:END -->
2211
+ function codexGuideFile(language = DEFAULT_LANGUAGE) {
2212
+ const body = language === "en" ? `# FET For Codex
2213
+
2214
+ ## Language
1902
2215
 
1903
- # FET For Codex
2216
+ ${languageInstruction(language)}
1904
2217
 
1905
2218
  Before doing FET or OpenSpec work in Codex, read:
1906
2219
 
@@ -1914,25 +2227,53 @@ If GitNexus code graph context is available in the IDE or MCP tools, prefer it b
1914
2227
  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.
1915
2228
 
1916
2229
  Command guides live in .codex/fet/commands/.
1917
- `
2230
+ ` : `# Codex \u7684 FET \u4F7F\u7528\u6307\u5357
2231
+
2232
+ ## \u8BED\u8A00
2233
+
2234
+ ${languageInstruction(language)}
2235
+
2236
+ \u5728 Codex \u4E2D\u6267\u884C FET \u6216 OpenSpec \u5DE5\u4F5C\u524D\uFF0C\u5148\u9605\u8BFB\uFF1A
2237
+
2238
+ - AGENTS.md
2239
+ - openspec/config.yaml
2240
+ - .codex/fet/karpathy-guidelines.md
2241
+ - \u5982\u679C\u5DF2\u9009\u62E9 change\uFF0C\u9605\u8BFB openspec/changes/<change-id>/ \u4E0B\u7684\u5F53\u524D\u4EA7\u7269
2242
+
2243
+ \u5982\u679C IDE \u6216 MCP \u5DE5\u5177\u4E2D\u53EF\u7528 GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\uFF0C\u5148\u7528\u5B83\u7F29\u5C0F\u4ED3\u5E93\u626B\u63CF\u8303\u56F4\uFF1B\u7528\u56FE\u8BC6\u522B\u76F8\u5173\u6A21\u5757\u3001\u4F9D\u8D56\u548C\u63D2\u5165\u70B9\uFF0C\u518D\u53EA\u8BFB\u53D6\u9700\u8981\u786E\u8BA4\u884C\u4E3A\u7684\u5177\u4F53\u6E90\u7801\u6587\u4EF6\u3002GitNexus \u4E0D\u53EF\u7528\u65F6\uFF0C\u6309\u666E\u901A FET/OpenSpec \u5DE5\u4F5C\u6D41\u7EE7\u7EED\u3002
2244
+
2245
+ \u5DE5\u4F5C\u6D41\u6D41\u8F6C\u4EE5\u7EC8\u7AEF\u547D\u4EE4 \`fet <command>\` \u4E3A\u51C6\u3002\u8FD9\u4E9B\u6587\u4EF6\u662F\u7ED9 Codex \u9605\u8BFB\u7684\u6307\u5BFC\uFF0C\u4E0D\u6CE8\u518C\u539F\u751F slash command\u3002
2246
+
2247
+ \u547D\u4EE4\u6307\u5357\u4F4D\u4E8E .codex/fet/commands/\u3002
2248
+ `;
2249
+ return {
2250
+ path: ".codex/fet/context.md",
2251
+ content: `<!-- FET:MANAGED
2252
+ schemaVersion: 1
2253
+ fetVersion: ${FET_VERSION}
2254
+ generator: codex-adapter
2255
+ adapterVersion: 1
2256
+ FET:END -->
2257
+
2258
+ ${body}`
1918
2259
  };
1919
2260
  }
1920
- function codexCommandFiles() {
2261
+ function codexCommandFiles(language = DEFAULT_LANGUAGE) {
1921
2262
  return [
1922
- codexKarpathyGuidelinesFile(),
2263
+ codexKarpathyGuidelinesFile(language),
1923
2264
  ...FET_ADAPTER_COMMANDS.map((command) => ({
1924
2265
  path: `.codex/fet/commands/${command}.md`,
1925
- content: renderCommand(command)
2266
+ content: renderCommand(command, language)
1926
2267
  }))
1927
2268
  ];
1928
2269
  }
1929
- function codexSlashPromptFiles() {
2270
+ function codexSlashPromptFiles(language = DEFAULT_LANGUAGE) {
1930
2271
  return FET_ADAPTER_COMMANDS.map((command) => ({
1931
2272
  path: `prompts/fet-${command}.md`,
1932
- content: renderSlashPrompt(command)
2273
+ content: renderSlashPrompt(command, language)
1933
2274
  }));
1934
2275
  }
1935
- function codexKarpathyGuidelinesFile() {
2276
+ function codexKarpathyGuidelinesFile(language) {
1936
2277
  return {
1937
2278
  path: ".codex/fet/karpathy-guidelines.md",
1938
2279
  content: `<!-- FET:MANAGED
@@ -1942,21 +2283,24 @@ generator: codex-adapter
1942
2283
  adapterVersion: 1
1943
2284
  FET:END -->
1944
2285
 
1945
- # Andrej Karpathy Inspired Coding Guidelines
2286
+ # ${language === "en" ? "Andrej Karpathy Inspired Coding Guidelines" : "\u53D7 Andrej Karpathy \u542F\u53D1\u7684\u7F16\u7801\u6307\u5357"}
1946
2287
 
1947
- ${renderKarpathyGuidelinesBody()}
2288
+ ${renderKarpathyGuidelinesBody(language)}
1948
2289
  `
1949
2290
  };
1950
2291
  }
1951
- function renderCommand(command) {
2292
+ function renderCommand(command, language) {
2293
+ if (language !== "en") {
2294
+ return renderCommandZh(command);
2295
+ }
1952
2296
  if (command === "fill-context") {
1953
- return renderFillContextCommand();
2297
+ return renderFillContextCommand(language);
1954
2298
  }
1955
2299
  if (command === "passthrough") {
1956
- return renderPassthroughCommand();
2300
+ return renderPassthroughCommand(language);
1957
2301
  }
1958
2302
  if (command.startsWith("graph-")) {
1959
- return renderGraphCommand(command);
2303
+ return renderGraphCommand(command, language);
1960
2304
  }
1961
2305
  const usage = renderFetAdapterUsage(command, "");
1962
2306
  return `<!-- FET:MANAGED
@@ -1969,7 +2313,9 @@ FET:END -->
1969
2313
 
1970
2314
  # ${usage}
1971
2315
 
1972
- ${renderIdeModelPolicy(command)}
2316
+ ${renderIdeModelPolicy(command, language)}
2317
+
2318
+ ${languageInstruction(language)}
1973
2319
 
1974
2320
  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.
1975
2321
 
@@ -1988,7 +2334,69 @@ If the command needs a change id, pass it with \`--change <change-id>\` or use t
1988
2334
  After the command completes, report the important next steps from the FET output and keep any generated OpenSpec artifacts in the normal project workflow.
1989
2335
  `;
1990
2336
  }
1991
- function renderPassthroughCommand() {
2337
+ function renderCommandZh(command) {
2338
+ const usage = renderFetAdapterUsage(command, command === "fill-context" ? "" : command === "passthrough" ? "<openspec-command> [...args]" : "");
2339
+ const title = commandTitleZh(command);
2340
+ if (command === "graph-setup") {
2341
+ return `<!-- FET:MANAGED
2342
+ schemaVersion: 1
2343
+ fetVersion: ${FET_VERSION}
2344
+ generator: codex-adapter
2345
+ adapterVersion: 1
2346
+ command: ${usage}
2347
+ FET:END -->
2348
+
2349
+ # ${usage}
2350
+
2351
+ ${renderIdeModelPolicy(command, "zh-CN")}
2352
+
2353
+ ${languageInstruction("zh-CN")}
2354
+
2355
+ \u7528\u4E8E\u6307\u5BFC\u5F53\u524D IDE LLM \u5728\u7528\u6237\u786E\u8BA4\u4E0B\u5B89\u88C5 GitNexus\u3002
2356
+
2357
+ \u5148\u8FD0\u884C\uFF1A
2358
+
2359
+ \`\`\`sh
2360
+ ${usage}
2361
+ \`\`\`
2362
+
2363
+ \u7136\u540E\u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u4EE5\u5B83\u4F5C\u4E3A\u5B89\u88C5\u4EFB\u52A1\u4E66\u3002\u53EA\u8BFB\u68C0\u6D4B\u547D\u4EE4\u53EF\u4EE5\u76F4\u63A5\u8FD0\u884C\uFF1B\u4EFB\u4F55\u4F1A\u4E0B\u8F7D\u8F6F\u4EF6\u3001\u5168\u5C40\u5B89\u88C5\u3001\u4FEE\u6539 PATH\u3001\u5199\u5165\u7528\u6237\u7EA7\u6587\u4EF6\u6216\u4FEE\u6539 IDE/MCP \u914D\u7F6E\u7684\u547D\u4EE4\uFF0C\u6267\u884C\u524D\u90FD\u8981\u5C55\u793A\u5B8C\u6574\u547D\u4EE4\u5E76\u7B49\u5F85\u7528\u6237\u786E\u8BA4\u3002
2364
+
2365
+ \u5B89\u88C5\u540E\u7528 \`gitnexus --version\` \u9A8C\u8BC1\u3002\u5408\u9002\u65F6\u7EE7\u7EED\u8FD0\u884C \`fet graph init\` \u548C \`fet graph handoff\`\u3002\u5982\u679C\u5B89\u88C5\u5931\u8D25\uFF0C\u62A5\u544A\u5931\u8D25\u547D\u4EE4\u3001stderr \u548C\u4E0B\u4E00\u6B65\u4EBA\u5DE5\u5904\u7406\u5EFA\u8BAE\u3002
2366
+ `;
2367
+ }
2368
+ return `<!-- FET:MANAGED
2369
+ schemaVersion: 1
2370
+ fetVersion: ${FET_VERSION}
2371
+ generator: codex-adapter
2372
+ adapterVersion: 1
2373
+ command: ${usage}
2374
+ FET:END -->
2375
+
2376
+ # ${usage}
2377
+
2378
+ ${renderIdeModelPolicy(command, "zh-CN")}
2379
+
2380
+ ${languageInstruction("zh-CN")}
2381
+
2382
+ ## \u7528\u9014
2383
+
2384
+ ${title}
2385
+
2386
+ ## \u6267\u884C\u65B9\u5F0F
2387
+
2388
+ \u5728\u6267\u884C\u524D\u9605\u8BFB AGENTS.md\u3001openspec/config.yaml\u3001.codex/fet/karpathy-guidelines.md\uFF0C\u4EE5\u53CA\u5F53\u524D change \u76EE\u5F55\u4E0B\u5DF2\u6709\u7684 OpenSpec \u4EA7\u7269\u3002
2389
+
2390
+ \`\`\`sh
2391
+ ${usage}
2392
+ \`\`\`
2393
+
2394
+ \u5982\u679C\u547D\u4EE4\u9700\u8981 change id\uFF0C\u4F18\u5148\u4F7F\u7528\u7528\u6237\u8F93\u5165\u3001\`--change <change-id>\`\u3001FET active change \u6216\u552F\u4E00\u6253\u5F00\u7684 change\u3002\u5B58\u5728\u6B67\u4E49\u65F6\u5148\u8BE2\u95EE\u7528\u6237\u3002
2395
+
2396
+ \u6267\u884C\u5B8C\u6210\u540E\uFF0C\u7528\u4E2D\u6587\u603B\u7ED3\u5173\u952E\u8F93\u51FA\u3001\u751F\u6210\u6216\u66F4\u65B0\u7684\u6587\u4EF6\uFF0C\u4EE5\u53CA\u4E0B\u4E00\u6B65\u5EFA\u8BAE\u3002
2397
+ `;
2398
+ }
2399
+ function renderPassthroughCommand(language) {
1992
2400
  return `<!-- FET:MANAGED
1993
2401
  schemaVersion: 1
1994
2402
  fetVersion: ${FET_VERSION}
@@ -1999,7 +2407,9 @@ FET:END -->
1999
2407
 
2000
2408
  # fet passthrough
2001
2409
 
2002
- ${renderIdeModelPolicy("passthrough")}
2410
+ ${renderIdeModelPolicy("passthrough", language)}
2411
+
2412
+ ${languageInstruction(language)}
2003
2413
 
2004
2414
  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.
2005
2415
 
@@ -2016,8 +2426,36 @@ fet passthrough <openspec-command> [...args]
2016
2426
  This preserves the FET entry point while allowing access to unmanaged or newly added OpenSpec commands. Passthrough does not update FET lifecycle state.
2017
2427
  `;
2018
2428
  }
2019
- function renderGraphCommand(command) {
2429
+ function renderGraphCommand(command, language) {
2020
2430
  const usage = renderFetAdapterUsage(command, "");
2431
+ if (command === "graph-setup") {
2432
+ return `<!-- FET:MANAGED
2433
+ schemaVersion: 1
2434
+ fetVersion: ${FET_VERSION}
2435
+ generator: codex-adapter
2436
+ adapterVersion: 1
2437
+ command: ${usage}
2438
+ FET:END -->
2439
+
2440
+ # ${usage}
2441
+
2442
+ ${renderIdeModelPolicy(command, language)}
2443
+
2444
+ ${languageInstruction(language)}
2445
+
2446
+ Use this command to guide IDE-assisted GitNexus installation with user approval.
2447
+
2448
+ Run:
2449
+
2450
+ \`\`\`sh
2451
+ ${usage}
2452
+ \`\`\`
2453
+
2454
+ Then read .fet/graph-setup.md and follow it as the source of truth. You may run read-only detection commands directly. Before downloading software, installing globally, changing PATH, writing user-level files, or modifying IDE/MCP configuration, show the exact command and wait for user approval.
2455
+
2456
+ After installation, verify \`gitnexus --version\`. If appropriate, continue with \`fet graph init\` and \`fet graph handoff\`. If installation fails, report the failing command, stderr, and next manual step.
2457
+ `;
2458
+ }
2021
2459
  const subcommand = command.slice("graph-".length);
2022
2460
  return `<!-- FET:MANAGED
2023
2461
  schemaVersion: 1
@@ -2029,7 +2467,9 @@ FET:END -->
2029
2467
 
2030
2468
  # ${usage}
2031
2469
 
2032
- ${renderIdeModelPolicy(command)}
2470
+ ${renderIdeModelPolicy(command, language)}
2471
+
2472
+ ${languageInstruction(language)}
2033
2473
 
2034
2474
  When the user asks Codex to work with optional GitNexus graph support, use FET as the entry point.
2035
2475
 
@@ -2048,42 +2488,48 @@ For graph init or refresh, pass extra GitNexus analyze arguments only when the u
2048
2488
  After the command completes, report the GitNexus state, generated handoff files, and next steps.
2049
2489
  `;
2050
2490
  }
2051
- function renderSlashPrompt(command) {
2491
+ function renderSlashPrompt(command, language) {
2492
+ if (language !== "en") {
2493
+ return renderSlashPromptZh(command);
2494
+ }
2052
2495
  if (command === "continue") {
2053
- return renderContinueSlashPrompt();
2496
+ return renderContinueSlashPrompt(language);
2054
2497
  }
2055
2498
  if (command === "ff" || command === "propose") {
2056
- return renderFastForwardSlashPrompt(command);
2499
+ return renderFastForwardSlashPrompt(command, language);
2057
2500
  }
2058
2501
  if (command === "explore") {
2059
- return renderExploreSlashPrompt();
2502
+ return renderExploreSlashPrompt(language);
2060
2503
  }
2061
2504
  if (command === "new") {
2062
- return renderNewSlashPrompt();
2505
+ return renderNewSlashPrompt(language);
2063
2506
  }
2064
2507
  if (command === "apply") {
2065
- return renderApplySlashPrompt();
2508
+ return renderApplySlashPrompt(language);
2066
2509
  }
2067
2510
  if (command === "verify") {
2068
- return renderVerifySlashPrompt();
2511
+ return renderVerifySlashPrompt(language);
2069
2512
  }
2070
2513
  if (command === "sync") {
2071
- return renderSyncSlashPrompt();
2514
+ return renderSyncSlashPrompt(language);
2072
2515
  }
2073
2516
  if (command === "archive") {
2074
- return renderArchiveSlashPrompt();
2517
+ return renderArchiveSlashPrompt(language);
2075
2518
  }
2076
2519
  if (command === "bulk-archive") {
2077
- return renderBulkArchiveSlashPrompt();
2520
+ return renderBulkArchiveSlashPrompt(language);
2078
2521
  }
2079
2522
  if (command === "onboard") {
2080
- return renderOnboardSlashPrompt();
2523
+ return renderOnboardSlashPrompt(language);
2081
2524
  }
2082
2525
  if (command === "fill-context") {
2083
- return renderFillContextSlashPrompt();
2526
+ return renderFillContextSlashPrompt(language);
2084
2527
  }
2085
2528
  if (command === "passthrough") {
2086
- return renderPassthroughSlashPrompt();
2529
+ return renderPassthroughSlashPrompt(language);
2530
+ }
2531
+ if (command === "graph-setup") {
2532
+ return renderGraphSetupSlashPrompt(language);
2087
2533
  }
2088
2534
  const usage = renderFetAdapterUsage(command);
2089
2535
  const isGraph = command.startsWith("graph-");
@@ -2099,7 +2545,6 @@ FET:END -->
2099
2545
 
2100
2546
  ---
2101
2547
  description: ${description}
2102
- argument-hint: command arguments
2103
2548
  ---
2104
2549
 
2105
2550
  Use FET as the entry point for this OpenSpec workflow.
@@ -2119,7 +2564,92 @@ ${shellCommand}
2119
2564
  After it completes, summarize the important FET output and next steps.
2120
2565
  `;
2121
2566
  }
2122
- function renderFillContextCommand() {
2567
+ function renderSlashPromptZh(command) {
2568
+ if (command === "graph-setup") {
2569
+ return renderGraphSetupSlashPrompt("zh-CN");
2570
+ }
2571
+ const usage = renderFetAdapterUsage(command, command === "fill-context" ? "" : command === "passthrough" ? "<openspec-command> [...args]" : "[...args]");
2572
+ const argumentHint = command === "passthrough" ? "openspec-command [...args]" : void 0;
2573
+ const argumentHintLine = argumentHint ? `argument-hint: ${argumentHint}
2574
+ ` : "";
2575
+ return `<!-- FET:MANAGED
2576
+ schemaVersion: 1
2577
+ fetVersion: ${FET_VERSION}
2578
+ generator: codex-adapter
2579
+ adapterVersion: 1
2580
+ command: ${usage}
2581
+ FET:END -->
2582
+
2583
+ ---
2584
+ description: ${commandTitleZh(command)}
2585
+ ${argumentHintLine}---
2586
+
2587
+ ${renderIdeModelPolicy(command, "zh-CN")}
2588
+
2589
+ ${languageInstruction("zh-CN")}
2590
+
2591
+ ## \u76EE\u6807
2592
+
2593
+ ${commandGoalZh(command)}
2594
+
2595
+ ## \u6B65\u9AA4
2596
+
2597
+ 1. \u9605\u8BFB AGENTS.md\u3001openspec/config.yaml \u548C .codex/fet/karpathy-guidelines.md\u3002
2598
+ 2. \u5982\u679C\u5B58\u5728\u5F53\u524D OpenSpec change\uFF0C\u9605\u8BFB openspec/changes/<change-id>/ \u4E0B\u7684\u76F8\u5173\u4EA7\u7269\u3002
2599
+ 3. \u8FD0\u884C FET \u547D\u4EE4\uFF1A
2600
+ \`\`\`sh
2601
+ ${usage}
2602
+ \`\`\`
2603
+ 4. \u6309 FET/OpenSpec \u8F93\u51FA\u7EE7\u7EED\uFF1B\u9700\u8981\u5199\u4EA7\u7269\u65F6\uFF0C\u53EA\u5199\u5230 OpenSpec \u6307\u5B9A\u8DEF\u5F84\uFF0C\u4E0D\u8981\u628A\u63D0\u793A\u5305\u88C5\u6587\u672C\u590D\u5236\u8FDB\u4EA7\u7269\u3002
2604
+ 5. \u7528\u4E2D\u6587\u603B\u7ED3\u672C\u6B21\u52A8\u4F5C\u3001\u6587\u4EF6\u8DEF\u5F84\u3001\u72B6\u6001\u548C\u4E0B\u4E00\u6B65\u3002
2605
+
2606
+ ## \u7EA6\u675F
2607
+
2608
+ - \u9ED8\u8BA4\u4F7F\u7528\u4E2D\u6587\u4EA7\u51FA\u3002
2609
+ - \u4E0D\u8981\u7ED5\u8FC7 FET \u76F4\u63A5\u8C03\u7528 openspec\uFF0C\u9664\u975E FET \u547D\u4EE4\u672C\u8EAB\u4E0D\u53EF\u7528\u3002
2610
+ - change \u4E0D\u660E\u786E\u65F6\u5148\u8BE2\u95EE\u7528\u6237\u3002
2611
+ ${command === "fill-context" ? "- \u66FF\u6362 AGENTS.md \u4E2D\u6BCF\u4E2A `[NEEDS LLM INPUT]` \u6216 `[NEED LLM INPUT]` \u5360\u4F4D\u7B26\uFF0C\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002\n" : ""}${command === "continue" ? "- \u4E00\u6B21\u53EA\u521B\u5EFA\u4E00\u4E2A ready artifact\uFF0C\u5E76\u5728\u5199\u5165\u524D\u9605\u8BFB\u4F9D\u8D56\u6587\u4EF6\u3002\n" : ""}${command === "apply" ? "- \u4E0D\u8981\u5728\u672A\u5B8C\u6210\u771F\u5B9E\u5B9E\u73B0\u524D\u52FE\u9009 tasks.md\uFF1B\u4E0D\u8981\u4ECE apply \u9636\u6BB5\u76F4\u63A5 sync \u6216 archive\u3002\n" : ""}`;
2612
+ }
2613
+ function commandTitleZh(command) {
2614
+ const titles = {
2615
+ explore: "\u63A2\u7D22 FET/OpenSpec \u9700\u6C42",
2616
+ propose: "\u521B\u5EFA\u5E76\u8865\u9F50 FET/OpenSpec \u63D0\u6848\u4EA7\u7269",
2617
+ new: "\u521B\u5EFA\u65B0\u7684 FET/OpenSpec change \u9AA8\u67B6",
2618
+ continue: "\u63A8\u8FDB\u5F53\u524D FET/OpenSpec change \u7684\u4E0B\u4E00\u4E2A\u4EA7\u7269",
2619
+ ff: "\u5FEB\u901F\u751F\u6210 FET/OpenSpec \u6240\u9700\u4EA7\u7269",
2620
+ apply: "\u5B9E\u65BD FET/OpenSpec change \u4E2D\u7684\u4EFB\u52A1",
2621
+ verify: "\u9A8C\u8BC1 FET/OpenSpec change",
2622
+ sync: "\u540C\u6B65 delta specs \u5230\u4E3B\u89C4\u8303",
2623
+ archive: "\u5F52\u6863\u5DF2\u9A8C\u8BC1\u7684 FET/OpenSpec change",
2624
+ "bulk-archive": "\u6279\u91CF\u5F52\u6863 FET/OpenSpec changes",
2625
+ onboard: "\u52A0\u8F7D FET/OpenSpec \u9879\u76EE\u4E0A\u4E0B\u6587",
2626
+ "fill-context": "\u586B\u5145 FET AGENTS.md \u5360\u4F4D\u7B26",
2627
+ passthrough: "\u901A\u8FC7 FET \u900F\u4F20\u672A\u63A5\u7BA1\u7684 OpenSpec \u547D\u4EE4",
2628
+ "graph-status": "\u67E5\u770B GitNexus \u4EE3\u7801\u56FE\u72B6\u6001",
2629
+ "graph-setup": "\u751F\u6210 GitNexus \u5B89\u88C5\u4EA4\u63A5\u8BF4\u660E",
2630
+ "graph-init": "\u521D\u59CB\u5316 GitNexus \u4EE3\u7801\u56FE",
2631
+ "graph-refresh": "\u5237\u65B0 GitNexus \u4EE3\u7801\u56FE",
2632
+ "graph-doctor": "\u8BCA\u65AD GitNexus \u4EE3\u7801\u56FE",
2633
+ "graph-handoff": "\u751F\u6210 GitNexus \u4EE3\u7801\u56FE\u4F7F\u7528\u8BF4\u660E"
2634
+ };
2635
+ return titles[command] ?? `\u8FD0\u884C FET ${command} \u5DE5\u4F5C\u6D41`;
2636
+ }
2637
+ function commandGoalZh(command) {
2638
+ if (command === "fill-context") {
2639
+ return "\u8865\u9F50 FET \u81EA\u52A8\u751F\u6210\u7684\u9879\u76EE\u4E0A\u4E0B\u6587\uFF0C\u8BA9\u540E\u7EED AI \u7F16\u7801\u548C OpenSpec \u5DE5\u4F5C\u6D41\u62E5\u6709\u7A33\u5B9A\u7684\u9879\u76EE\u4E8B\u5B9E\u3002";
2640
+ }
2641
+ if (command === "continue") {
2642
+ return "\u57FA\u4E8E active change \u6216\u7528\u6237\u6307\u5B9A\u7684 change\uFF0C\u521B\u5EFA\u4E0B\u4E00\u4E2A ready \u7684 OpenSpec \u89C4\u5212\u4EA7\u7269\u3002";
2643
+ }
2644
+ if (command === "apply") {
2645
+ return "\u8BFB\u53D6 OpenSpec \u4EA7\u7269\u5E76\u6309 tasks.md \u5B9E\u65BD\u4EE3\u7801\u53D8\u66F4\u3002";
2646
+ }
2647
+ if (command.startsWith("graph-")) {
2648
+ return "\u7BA1\u7406\u53EF\u9009\u7684 GitNexus \u4EE3\u7801\u56FE\uFF0C\u8BA9 AI \u5728\u5927\u8303\u56F4\u626B\u63CF\u524D\u4F18\u5148\u83B7\u5F97\u7ED3\u6784\u5316\u4EE3\u7801\u4E0A\u4E0B\u6587\u3002";
2649
+ }
2650
+ return commandTitleZh(command);
2651
+ }
2652
+ function renderFillContextCommand(language) {
2123
2653
  return `<!-- FET:MANAGED
2124
2654
  schemaVersion: 1
2125
2655
  fetVersion: ${FET_VERSION}
@@ -2130,7 +2660,9 @@ FET:END -->
2130
2660
 
2131
2661
  # fet fill-context
2132
2662
 
2133
- ${renderIdeModelPolicy("fill-context")}
2663
+ ${renderIdeModelPolicy("fill-context", language)}
2664
+
2665
+ ${languageInstruction(language)}
2134
2666
 
2135
2667
  Use this command to complete FET-generated project context placeholders with Codex.
2136
2668
 
@@ -2147,7 +2679,7 @@ fet fill-context
2147
2679
  Then read AGENTS.md and openspec/config.yaml, inspect the project, and replace every [NEEDS LLM INPUT] or [NEED LLM INPUT] placeholder in AGENTS.md with concrete project-specific content. Preserve FET managed markers and do not modify business code.
2148
2680
  `;
2149
2681
  }
2150
- function renderFillContextSlashPrompt() {
2682
+ function renderFillContextSlashPrompt(language) {
2151
2683
  return renderManagedSlashPrompt(
2152
2684
  "fet fill-context",
2153
2685
  "Fill FET AGENTS.md placeholders with Codex",
@@ -2179,10 +2711,12 @@ Steps:
2179
2711
  Guardrails:
2180
2712
  - Do not invent facts that cannot be inferred from the repo.
2181
2713
  - Use [UNKNOWN] only when the repository does not contain enough evidence.
2182
- - Keep generated context stable and useful for future AI coding sessions.`
2714
+ - Keep generated context stable and useful for future AI coding sessions.`,
2715
+ void 0,
2716
+ language
2183
2717
  );
2184
2718
  }
2185
- function renderNewSlashPrompt() {
2719
+ function renderNewSlashPrompt(language) {
2186
2720
  return renderManagedSlashPrompt(
2187
2721
  "fet new [...args]",
2188
2722
  "Create a new FET/OpenSpec change scaffold",
@@ -2211,20 +2745,22 @@ Steps:
2211
2745
  Guardrails:
2212
2746
  - Do not create artifact files in /prompts:fet-new.
2213
2747
  - If the change already exists, suggest /prompts:fet-continue <change-id>.
2214
- - Show the change location and the next command to create the first artifact.`
2748
+ - Show the change location and the next command to create the first artifact.`,
2749
+ void 0,
2750
+ language
2215
2751
  );
2216
2752
  }
2217
- function renderApplySlashPrompt() {
2753
+ function renderApplySlashPrompt(language) {
2218
2754
  return renderManagedSlashPrompt(
2219
2755
  "fet apply [...args]",
2220
2756
  "Implement tasks from a FET/OpenSpec change",
2221
2757
  `Implement a FET-managed OpenSpec change.
2222
2758
 
2223
- Input after the slash command should identify the change, for example a change id or --change <change-id>.
2759
+ Input after the slash command may identify the change, for example a change id or --change <change-id>. If omitted, use the active OpenSpec change when it is unambiguous.
2224
2760
 
2225
2761
  Steps:
2226
2762
 
2227
- 1. Resolve the change id. If ambiguous, ask the user.
2763
+ 1. Resolve the change id from the input, active FET/OpenSpec state, or the only open change. If ambiguous, ask the user.
2228
2764
  2. Run the native OpenSpec apply flow through FET:
2229
2765
  \`\`\`sh
2230
2766
  fet apply --change <change-id> --json
@@ -2241,20 +2777,22 @@ Steps:
2241
2777
  Guardrails:
2242
2778
  - Never skip reading OpenSpec artifacts before implementation.
2243
2779
  - Do not mark a task complete until the code change is actually done.
2244
- - Do not run sync or archive from apply.`
2780
+ - Do not run sync or archive from apply.`,
2781
+ void 0,
2782
+ language
2245
2783
  );
2246
2784
  }
2247
- function renderVerifySlashPrompt() {
2785
+ function renderVerifySlashPrompt(language) {
2248
2786
  return renderManagedSlashPrompt(
2249
2787
  "fet verify [...args]",
2250
2788
  "Verify a FET/OpenSpec change before sync or archive",
2251
2789
  `Verify a FET-managed OpenSpec change.
2252
2790
 
2253
- 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.
2791
+ Input after the slash command may identify the change. If omitted, use the active OpenSpec change when it is unambiguous. If the user passes --done, declare verification complete only after checks have been performed or explicitly accepted by the user.
2254
2792
 
2255
2793
  Steps:
2256
2794
 
2257
- 1. Resolve the change id. If ambiguous, ask the user.
2795
+ 1. Resolve the change id from the input, active FET/OpenSpec state, or the only open change. If ambiguous, ask the user.
2258
2796
  2. Generate FET verification instructions:
2259
2797
  \`\`\`sh
2260
2798
  fet verify --change <change-id>
@@ -2274,20 +2812,22 @@ Steps:
2274
2812
  Guardrails:
2275
2813
  - Do not run --done before producing a verification assessment.
2276
2814
  - Treat incomplete tasks or missing required behavior as critical unless user explicitly accepts them.
2277
- - Suggest /prompts:fet-sync <change-id> and /prompts:fet-archive <change-id> only after verification is done.`
2815
+ - Suggest /prompts:fet-sync <change-id> and /prompts:fet-archive <change-id> only after verification is done.`,
2816
+ void 0,
2817
+ language
2278
2818
  );
2279
2819
  }
2280
- function renderSyncSlashPrompt() {
2820
+ function renderSyncSlashPrompt(language) {
2281
2821
  return renderManagedSlashPrompt(
2282
2822
  "fet sync [...args]",
2283
2823
  "Sync delta specs and validate a FET/OpenSpec change",
2284
2824
  `Sync a FET-managed OpenSpec change.
2285
2825
 
2286
- Input after the slash command should identify the change.
2826
+ Input after the slash command may identify the change. If omitted, use the active OpenSpec change when it is unambiguous.
2287
2827
 
2288
2828
  Steps:
2289
2829
 
2290
- 1. Resolve the change id. If ambiguous, ask the user.
2830
+ 1. Resolve the change id from the input, active FET/OpenSpec state, or the only open change. If ambiguous, ask the user.
2291
2831
  2. Confirm FET verification is complete or run /prompts:fet-verify <change-id> first.
2292
2832
  3. Find delta specs under openspec/changes/<change-id>/specs/*/spec.md.
2293
2833
  4. If delta specs exist, intelligently merge them into openspec/specs/<capability>/spec.md:
@@ -2306,20 +2846,22 @@ Steps:
2306
2846
  Guardrails:
2307
2847
  - Read both delta and main specs before editing.
2308
2848
  - Make sync idempotent where possible.
2309
- - If FET reports verify is not done, stop and run/ask for verification instead of bypassing the gate.`
2849
+ - If FET reports verify is not done, stop and run/ask for verification instead of bypassing the gate.`,
2850
+ void 0,
2851
+ language
2310
2852
  );
2311
2853
  }
2312
- function renderArchiveSlashPrompt() {
2854
+ function renderArchiveSlashPrompt(language) {
2313
2855
  return renderManagedSlashPrompt(
2314
2856
  "fet archive [...args]",
2315
2857
  "Archive a verified FET/OpenSpec change",
2316
2858
  `Archive a FET-managed OpenSpec change.
2317
2859
 
2318
- Input after the slash command should identify the change.
2860
+ Input after the slash command may identify the change. If omitted, use the active OpenSpec change when it is unambiguous.
2319
2861
 
2320
2862
  Steps:
2321
2863
 
2322
- 1. Resolve the change id. If ambiguous, ask the user.
2864
+ 1. Resolve the change id from the input, active FET/OpenSpec state, or the only open change. If ambiguous, ask the user.
2323
2865
  2. Check artifact and task status:
2324
2866
  \`\`\`sh
2325
2867
  fet passthrough status --change <change-id> --json
@@ -2336,10 +2878,12 @@ Steps:
2336
2878
  Guardrails:
2337
2879
  - Do not move change directories manually; use fet archive.
2338
2880
  - Do not bypass the FET verify gate.
2339
- - Ask before archiving with incomplete tasks or unsynced delta specs.`
2881
+ - Ask before archiving with incomplete tasks or unsynced delta specs.`,
2882
+ void 0,
2883
+ language
2340
2884
  );
2341
2885
  }
2342
- function renderBulkArchiveSlashPrompt() {
2886
+ function renderBulkArchiveSlashPrompt(language) {
2343
2887
  return renderManagedSlashPrompt(
2344
2888
  "fet bulk-archive [...args]",
2345
2889
  "Archive multiple FET/OpenSpec changes safely",
@@ -2363,10 +2907,12 @@ Steps:
2363
2907
  Guardrails:
2364
2908
  - Never archive all changes without explicit user selection.
2365
2909
  - Do not bypass verify or warnings for individual changes.
2366
- - Continue with remaining selected changes if one archive fails, then report the failure clearly.`
2910
+ - Continue with remaining selected changes if one archive fails, then report the failure clearly.`,
2911
+ void 0,
2912
+ language
2367
2913
  );
2368
2914
  }
2369
- function renderOnboardSlashPrompt() {
2915
+ function renderOnboardSlashPrompt(language) {
2370
2916
  return renderManagedSlashPrompt(
2371
2917
  "fet onboard [...args]",
2372
2918
  "Load FET/OpenSpec onboarding context",
@@ -2377,7 +2923,7 @@ Steps:
2377
2923
  1. Read AGENTS.md and openspec/config.yaml.
2378
2924
  2. Run FET onboarding:
2379
2925
  \`\`\`sh
2380
- fet onboard $ARGUMENTS
2926
+ fet onboard
2381
2927
  \`\`\`
2382
2928
  3. Summarize:
2383
2929
  - Project context.
@@ -2387,10 +2933,12 @@ Steps:
2387
2933
 
2388
2934
  Guardrails:
2389
2935
  - Do not create or modify artifacts during onboard.
2390
- - Use this command to orient the session, then suggest the next concrete FET command.`
2936
+ - Use this command to orient the session, then suggest the next concrete FET command.`,
2937
+ void 0,
2938
+ language
2391
2939
  );
2392
2940
  }
2393
- function renderPassthroughSlashPrompt() {
2941
+ function renderPassthroughSlashPrompt(language) {
2394
2942
  return renderManagedSlashPrompt(
2395
2943
  "fet passthrough <openspec-command> [...args]",
2396
2944
  "Run an unmanaged OpenSpec command through FET",
@@ -2408,10 +2956,68 @@ Steps:
2408
2956
  Guardrails:
2409
2957
  - Do not call openspec directly unless FET passthrough itself is unavailable.
2410
2958
  - Remember that passthrough does not update FET lifecycle state.
2411
- - For managed workflows, prefer the specific FET prompt instead of passthrough.`
2959
+ - For managed workflows, prefer the specific FET prompt instead of passthrough.`,
2960
+ "openspec-command [...args]",
2961
+ language
2962
+ );
2963
+ }
2964
+ function renderGraphSetupSlashPrompt(language) {
2965
+ if (language === "en") {
2966
+ return renderManagedSlashPrompt(
2967
+ "fet graph setup",
2968
+ "Guide IDE-assisted GitNexus installation with user approval",
2969
+ `Guide optional GitNexus installation for this project.
2970
+
2971
+ Steps:
2972
+
2973
+ 1. Run:
2974
+ \`\`\`sh
2975
+ fet graph setup
2976
+ \`\`\`
2977
+ 2. Read .fet/graph-setup.md and follow it as the source of truth.
2978
+ 3. Run read-only detection commands as needed, such as checking the shell, PATH, package managers, and \`gitnexus --version\`.
2979
+ 4. If GitNexus is missing, use the configured \`FET_GITNEXUS_INSTALL_COMMAND\` when present; otherwise find the official GitNexus installation instructions or ask the user for the preferred method.
2980
+ 5. Before any command that downloads software, installs globally, changes PATH, writes user-level files, or modifies IDE/MCP settings, show the exact command and wait for user approval.
2981
+ 6. Verify installation with \`gitnexus --version\`.
2982
+ 7. If the user wants IDE/MCP integration, explain \`gitnexus setup\` and run it only after approval.
2983
+ 8. Run \`fet graph init\` and then \`fet graph handoff\` when GitNexus is available.
2984
+
2985
+ Guardrails:
2986
+ - GitNexus is optional; do not block FET/OpenSpec workflows if installation fails.
2987
+ - Do not modify application code during setup.
2988
+ - Report the failing command, stderr, and next manual step if blocked.`,
2989
+ void 0,
2990
+ language
2991
+ );
2992
+ }
2993
+ return renderManagedSlashPrompt(
2994
+ "fet graph setup",
2995
+ "\u6307\u5BFC IDE LLM \u5728\u7528\u6237\u786E\u8BA4\u4E0B\u5B89\u88C5 GitNexus",
2996
+ `\u4E3A\u5F53\u524D\u9879\u76EE\u5F15\u5BFC\u53EF\u9009\u7684 GitNexus \u5B89\u88C5\u3002
2997
+
2998
+ \u6B65\u9AA4\uFF1A
2999
+
3000
+ 1. \u8FD0\u884C\uFF1A
3001
+ \`\`\`sh
3002
+ fet graph setup
3003
+ \`\`\`
3004
+ 2. \u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u4EE5\u5B83\u4F5C\u4E3A\u5B89\u88C5\u4EFB\u52A1\u4E66\u3002
3005
+ 3. \u6309\u9700\u8FD0\u884C\u53EA\u8BFB\u68C0\u6D4B\u547D\u4EE4\uFF0C\u4F8B\u5982\u68C0\u67E5 shell\u3001PATH\u3001\u5305\u7BA1\u7406\u5668\u548C \`gitnexus --version\`\u3002
3006
+ 4. \u5982\u679C GitNexus \u7F3A\u5931\uFF0C\u4F18\u5148\u4F7F\u7528\u5DF2\u914D\u7F6E\u7684 \`FET_GITNEXUS_INSTALL_COMMAND\`\uFF1B\u6CA1\u6709\u914D\u7F6E\u65F6\uFF0C\u67E5\u627E GitNexus \u5B98\u65B9\u5B89\u88C5\u8BF4\u660E\uFF0C\u6216\u8BE2\u95EE\u7528\u6237\u5E0C\u671B\u4F7F\u7528\u7684\u5B89\u88C5\u65B9\u5F0F\u3002
3007
+ 5. \u4EFB\u4F55\u4F1A\u4E0B\u8F7D\u8F6F\u4EF6\u3001\u5168\u5C40\u5B89\u88C5\u3001\u4FEE\u6539 PATH\u3001\u5199\u5165\u7528\u6237\u7EA7\u6587\u4EF6\u6216\u4FEE\u6539 IDE/MCP \u8BBE\u7F6E\u7684\u547D\u4EE4\uFF0C\u6267\u884C\u524D\u90FD\u8981\u5C55\u793A\u5B8C\u6574\u547D\u4EE4\u5E76\u7B49\u5F85\u7528\u6237\u786E\u8BA4\u3002
3008
+ 6. \u7528 \`gitnexus --version\` \u9A8C\u8BC1\u5B89\u88C5\u3002
3009
+ 7. \u5982\u679C\u7528\u6237\u9700\u8981 IDE/MCP \u96C6\u6210\uFF0C\u5148\u8BF4\u660E \`gitnexus setup\`\uFF0C\u83B7\u5F97\u786E\u8BA4\u540E\u518D\u8FD0\u884C\u3002
3010
+ 8. GitNexus \u53EF\u7528\u540E\u8FD0\u884C \`fet graph init\`\uFF0C\u518D\u8FD0\u884C \`fet graph handoff\`\u3002
3011
+
3012
+ \u7EA6\u675F\uFF1A
3013
+ - GitNexus \u662F\u53EF\u9009\u80FD\u529B\uFF1B\u5B89\u88C5\u5931\u8D25\u65F6\u4E0D\u8981\u963B\u585E FET/OpenSpec \u4E3B\u6D41\u7A0B\u3002
3014
+ - \u5B89\u88C5\u8FC7\u7A0B\u4E2D\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
3015
+ - \u5982\u679C\u53D7\u963B\uFF0C\u62A5\u544A\u5931\u8D25\u547D\u4EE4\u3001stderr \u548C\u4E0B\u4E00\u6B65\u4EBA\u5DE5\u5904\u7406\u5EFA\u8BAE\u3002`,
3016
+ void 0,
3017
+ language
2412
3018
  );
2413
3019
  }
2414
- function renderExploreSlashPrompt() {
3020
+ function renderExploreSlashPrompt(language) {
2415
3021
  return renderManagedSlashPrompt(
2416
3022
  "fet explore [...args]",
2417
3023
  "Explore requirements for a FET/OpenSpec change",
@@ -2443,21 +3049,23 @@ Guardrails:
2443
3049
  - Do not write application code in explore mode.
2444
3050
  - Ask a clarifying question if the proposal would otherwise be mostly guesswork.
2445
3051
  - Creating or updating OpenSpec artifacts is allowed when the user asks to capture the thinking.
2446
- - After creating proposal.md, show the path and suggest /prompts:fet-continue <change-id> for the next artifact.`
3052
+ - After creating proposal.md, show the path and suggest /prompts:fet-continue <change-id> for the next artifact.`,
3053
+ void 0,
3054
+ language
2447
3055
  );
2448
3056
  }
2449
- function renderContinueSlashPrompt() {
3057
+ function renderContinueSlashPrompt(language) {
2450
3058
  return renderManagedSlashPrompt(
2451
3059
  "fet continue [...args]",
2452
3060
  "Create the next FET/OpenSpec artifact",
2453
3061
  `Continue a FET-managed OpenSpec change by creating exactly one ready artifact.
2454
3062
 
2455
- Input after the slash command should be a change id, optionally followed by an artifact id.
3063
+ Input after the slash command may be a change id, optionally followed by an artifact id. If omitted, use the active OpenSpec change and continue the next ready artifact.
2456
3064
 
2457
3065
  Steps:
2458
3066
 
2459
3067
  1. Load project context from AGENTS.md and openspec/config.yaml.
2460
- 2. Resolve the change id. If it is missing and cannot be inferred unambiguously, ask the user.
3068
+ 2. Resolve the change id from the input, active FET/OpenSpec state, or the only open change. If ambiguous, ask the user.
2461
3069
  3. Check status:
2462
3070
  \`\`\`sh
2463
3071
  fet passthrough status --change <change-id> --json
@@ -2465,7 +3073,7 @@ Steps:
2465
3073
  4. Pick the first artifact whose status is ready, unless the user specified an artifact id.
2466
3074
  5. Run the native OpenSpec continue flow through FET:
2467
3075
  \`\`\`sh
2468
- fet continue <artifact-id> --change <change-id> --json
3076
+ fet continue [artifact-id] --change <change-id> --json
2469
3077
  \`\`\`
2470
3078
  6. Follow the native continue output. When it provides template, instruction, dependencies, and outputPath, use those fields.
2471
3079
  7. Read dependency files before writing.
@@ -2483,10 +3091,12 @@ Output:
2483
3091
  Guardrails:
2484
3092
  - Create one artifact per invocation.
2485
3093
  - Never skip dependency order.
2486
- - Ask the user if instructions are ambiguous enough that a useful artifact cannot be written.`
3094
+ - Ask the user if instructions are ambiguous enough that a useful artifact cannot be written.`,
3095
+ void 0,
3096
+ language
2487
3097
  );
2488
3098
  }
2489
- function renderFastForwardSlashPrompt(command) {
3099
+ function renderFastForwardSlashPrompt(command, language) {
2490
3100
  const title = command === "propose" ? "Propose a new FET/OpenSpec change" : "Fast-forward FET/OpenSpec artifact creation";
2491
3101
  const commandLine = command === "propose" ? "fet propose <change-id-or-description>" : "fet ff --change <change-id>";
2492
3102
  return renderManagedSlashPrompt(
@@ -2494,7 +3104,7 @@ function renderFastForwardSlashPrompt(command) {
2494
3104
  command === "propose" ? "Create a change and generate required OpenSpec artifacts" : "Generate required OpenSpec artifacts for a change",
2495
3105
  `${title}.
2496
3106
 
2497
- Input after the slash command may be a change id or a description of what the user wants to build.
3107
+ Input after the slash command may be a change id or a description of what the user wants to build. For ff, it may be omitted when the active OpenSpec change is unambiguous.
2498
3108
 
2499
3109
  Steps:
2500
3110
 
@@ -2517,11 +3127,16 @@ Output:
2517
3127
  - Change id and location.
2518
3128
  - Artifacts created.
2519
3129
  - Current status.
2520
- - Next recommended command, usually /prompts:fet-apply <change-id>.`
3130
+ - Next recommended command, usually /prompts:fet-apply <change-id>.`,
3131
+ void 0,
3132
+ language
2521
3133
  );
2522
3134
  }
2523
- function renderManagedSlashPrompt(command, description, body) {
3135
+ function renderManagedSlashPrompt(command, description, body, argumentHint, language = DEFAULT_LANGUAGE) {
2524
3136
  const policyCommand = command.split(/\s+/)[1] ?? command;
3137
+ const argumentHintLine = argumentHint ? `argument-hint: ${argumentHint}
3138
+ ` : "";
3139
+ const graphContextInstruction = language === "en" ? "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." : "\u5982\u679C GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u53EF\u7528\uFF0C\u5148\u7528\u5B83\u7F29\u5C0F\u6E90\u7801\u626B\u63CF\u8303\u56F4\uFF1B\u4E0D\u53EF\u7528\u65F6\u6309\u666E\u901A\u6D41\u7A0B\u7EE7\u7EED\u3002";
2525
3140
  return `<!-- FET:MANAGED
2526
3141
  schemaVersion: 1
2527
3142
  fetVersion: ${FET_VERSION}
@@ -2532,12 +3147,13 @@ FET:END -->
2532
3147
 
2533
3148
  ---
2534
3149
  description: ${description}
2535
- argument-hint: command arguments
2536
- ---
3150
+ ${argumentHintLine}---
2537
3151
 
2538
- ${renderIdeModelPolicy(policyCommand)}
3152
+ ${renderIdeModelPolicy(policyCommand, language)}
2539
3153
 
2540
- 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.
3154
+ ${languageInstruction(language)}
3155
+
3156
+ ${graphContextInstruction}
2541
3157
 
2542
3158
  ${body}
2543
3159
  `;
@@ -2553,16 +3169,16 @@ var CodexAdapter = class {
2553
3169
  reason: "Codex adapter is available for projects that use AGENTS.md"
2554
3170
  };
2555
3171
  }
2556
- async planInstall(_projectRoot) {
3172
+ async planInstall(_projectRoot, language) {
2557
3173
  return {
2558
3174
  tool: this.tool,
2559
3175
  files: [
2560
- ...[codexGuideFile(), ...codexCommandFiles()].map((file) => ({
3176
+ ...[codexGuideFile(language), ...codexCommandFiles(language)].map((file) => ({
2561
3177
  ...file,
2562
3178
  managed: true,
2563
3179
  root: "project"
2564
3180
  })),
2565
- ...codexSlashPromptFiles().map((file) => ({
3181
+ ...codexSlashPromptFiles(language).map((file) => ({
2566
3182
  ...file,
2567
3183
  managed: true,
2568
3184
  root: "codex-home"
@@ -2649,13 +3265,13 @@ import { mkdir as mkdir8, readFile as readFile13, stat as stat7 } from "fs/promi
2649
3265
  import { dirname as dirname8, join as join16 } from "path";
2650
3266
 
2651
3267
  // src/adapters/cursor/templates.ts
2652
- function cursorSkillFiles() {
3268
+ function cursorSkillFiles(language = DEFAULT_LANGUAGE) {
2653
3269
  return FET_ADAPTER_COMMANDS.map((command) => ({
2654
3270
  path: `.cursor/skills/fet-${command}/SKILL.md`,
2655
- content: renderSkill(command)
3271
+ content: renderSkill(command, language)
2656
3272
  }));
2657
3273
  }
2658
- function cursorRuleFile() {
3274
+ function cursorRuleFile(language = DEFAULT_LANGUAGE) {
2659
3275
  return {
2660
3276
  path: ".cursor/rules/fet-context.mdc",
2661
3277
  content: `<!-- FET:MANAGED
@@ -2666,22 +3282,24 @@ adapterVersion: 1
2666
3282
  FET:END -->
2667
3283
 
2668
3284
  ---
2669
- description: Load FET project context for implementation tasks
3285
+ description: ${language === "en" ? "Load FET project context for implementation tasks" : "\u4E3A\u5B9E\u73B0\u4EFB\u52A1\u52A0\u8F7D FET \u9879\u76EE\u4E0A\u4E0B\u6587"}
2670
3286
  alwaysApply: false
2671
3287
  ---
2672
3288
 
3289
+ ${languageInstruction(language)}
3290
+
2673
3291
  \u5F53\u7528\u6237\u8BF7\u6C42\u4FEE\u6539\u9879\u76EE\u3001\u5B9E\u73B0 OpenSpec change\u3001\u8FD0\u884C FET \u5DE5\u4F5C\u6D41\u6216\u89E3\u91CA\u9879\u76EE\u7ED3\u6784\u65F6\uFF0C\u4F18\u5148\u9605\u8BFB\uFF1A
2674
3292
 
2675
3293
  - AGENTS.md
2676
3294
  - openspec/config.yaml
2677
- - GitNexus code graph context, when available. Prefer it before broad repository scans; if it is unavailable, continue normally.
2678
- - \u5F53\u524D change \u76EE\u5F55\u4E0B\u7684 OpenSpec \u89C4\u5212\u4EA7\u7269
3295
+ - \u53EF\u7528\u65F6\u7684 GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u3002\u4F18\u5148\u7528\u5B83\u7F29\u5C0F\u8303\u56F4\uFF1B\u4E0D\u53EF\u7528\u65F6\u6309\u666E\u901A FET/OpenSpec \u5DE5\u4F5C\u6D41\u7EE7\u7EED\u3002
3296
+ - \u5F53\u524D change \u76EE\u5F55\u4E0B\u7684 OpenSpec \u89C4\u5212\u4EA7\u7269\u3002
2679
3297
 
2680
- \u5982\u679C\u7528\u6237\u8F93\u5165\u7C7B\u4F3C \`/fet apply\` \u7684\u8BF7\u6C42\uFF0CCursor \u5F53\u524D\u7248\u672C\u672A\u5FC5\u4F1A\u628A\u672C\u6587\u4EF6\u6CE8\u518C\u4E3A\u539F\u751F slash command\u3002\u6B64\u65F6\u8BF7\u628A\u5B83\u5F53\u4F5C\u5DE5\u4F5C\u6D41\u610F\u56FE\uFF0C\u5E76\u63D0\u793A\u7528\u6237\u5728\u7EC8\u7AEF\u6267\u884C\u5BF9\u5E94\u7684 \`fet <cmd>\` \u547D\u4EE4\u3002
3298
+ \u5982\u679C\u7528\u6237\u8F93\u5165\u7C7B\u4F3C \`/fet apply\` \u7684\u8BF7\u6C42\uFF0C\u8BF7\u628A\u5B83\u89C6\u4E3A\u5DE5\u4F5C\u6D41\u610F\u56FE\uFF0C\u5E76\u63D0\u793A\u6216\u6267\u884C\u5BF9\u5E94\u7684\u7EC8\u7AEF\u547D\u4EE4 \`fet <cmd>\`\u3002
2681
3299
  `
2682
3300
  };
2683
3301
  }
2684
- function renderSkill(command) {
3302
+ function renderSkill(command, language) {
2685
3303
  const usage = renderFetAdapterUsage(command, command === "passthrough" ? "[...args]" : "");
2686
3304
  if (command === "fill-context") {
2687
3305
  return `<!-- FET:MANAGED
@@ -2694,22 +3312,52 @@ FET:END -->
2694
3312
 
2695
3313
  ---
2696
3314
  name: fet-fill-context
2697
- description: Fill FET AGENTS.md placeholders with Cursor AI
3315
+ description: ${language === "en" ? "Fill FET AGENTS.md placeholders with Cursor AI" : "\u4F7F\u7528 Cursor AI \u586B\u5145 FET AGENTS.md \u5360\u4F4D\u7B26"}
2698
3316
  disable-model-invocation: false
2699
3317
  ---
2700
3318
 
2701
- Run \`fet fill-context\` first if the IDE commands need refreshing.
3319
+ ${renderIdeModelPolicy(command, language)}
2702
3320
 
2703
- ${renderIdeModelPolicy(command)}
3321
+ ${languageInstruction(language)}
2704
3322
 
2705
- If GitNexus code graph context is available in Cursor, prefer it before broad repository scans. If it is unavailable, continue normally.
3323
+ \u5982\u679C IDE \u547D\u4EE4\u9700\u8981\u5237\u65B0\uFF0C\u5148\u8FD0\u884C \`fet fill-context\`\u3002
2706
3324
 
2707
- Then read:
3325
+ \u7136\u540E\u9605\u8BFB\uFF1A
2708
3326
 
2709
3327
  - AGENTS.md
2710
3328
  - openspec/config.yaml
2711
3329
 
2712
- Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete project-specific content. Inspect README files, package scripts, routes, tests, source layout, and existing conventions before writing. Preserve FET managed markers and do not modify business code.
3330
+ \u68C0\u67E5 README\u3001package scripts\u3001\u8DEF\u7531\u3001\u6D4B\u8BD5\u3001\u6E90\u7801\u7ED3\u6784\u548C\u73B0\u6709\u7EA6\u5B9A\u540E\uFF0C\u628A AGENTS.md \u4E2D\u6BCF\u4E2A \`[NEEDS LLM INPUT]\` \u6216 \`[NEED LLM INPUT]\` \u5360\u4F4D\u7B26\u66FF\u6362\u4E3A\u5177\u4F53\u3001\u7B80\u6D01\u3001\u9879\u76EE\u76F8\u5173\u7684\u5185\u5BB9\u3002\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
3331
+ `;
3332
+ }
3333
+ if (command === "graph-setup") {
3334
+ return `<!-- FET:MANAGED
3335
+ schemaVersion: 1
3336
+ fetVersion: ${FET_VERSION}
3337
+ generator: cursor-adapter
3338
+ adapterVersion: 1
3339
+ command: fet graph setup
3340
+ FET:END -->
3341
+
3342
+ ---
3343
+ name: fet-graph-setup
3344
+ description: ${language === "en" ? "Guide IDE-assisted GitNexus installation with user approval" : "\u6307\u5BFC IDE LLM \u5728\u7528\u6237\u786E\u8BA4\u4E0B\u5B89\u88C5 GitNexus"}
3345
+ disable-model-invocation: false
3346
+ ---
3347
+
3348
+ ${renderIdeModelPolicy(command, language)}
3349
+
3350
+ ${languageInstruction(language)}
3351
+
3352
+ ${language === "en" ? `Run \`fet graph setup\`, then read .fet/graph-setup.md and follow it as an installation playbook.
3353
+
3354
+ You may run read-only detection commands without extra confirmation. Before any command that downloads software, installs globally, changes PATH, writes user-level config, or modifies IDE/MCP settings, show the exact command and wait for user approval.
3355
+
3356
+ After installation, verify with \`gitnexus --version\`, then run \`fet graph init\` and \`fet graph handoff\` when appropriate. If installation fails, summarize the failing command and next manual step.` : `\u5148\u8FD0\u884C \`fet graph setup\`\uFF0C\u518D\u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u628A\u5B83\u4F5C\u4E3A\u5B89\u88C5\u4EFB\u52A1\u4E66\u6267\u884C\u3002
3357
+
3358
+ \u53EA\u8BFB\u68C0\u6D4B\u547D\u4EE4\u53EF\u4EE5\u76F4\u63A5\u8FD0\u884C\u3002\u4EFB\u4F55\u4F1A\u4E0B\u8F7D\u8F6F\u4EF6\u3001\u5168\u5C40\u5B89\u88C5\u3001\u4FEE\u6539 PATH\u3001\u5199\u5165\u7528\u6237\u7EA7\u914D\u7F6E\u6216\u4FEE\u6539 IDE/MCP \u8BBE\u7F6E\u7684\u547D\u4EE4\uFF0C\u6267\u884C\u524D\u90FD\u8981\u5C55\u793A\u5B8C\u6574\u547D\u4EE4\u5E76\u7B49\u5F85\u7528\u6237\u786E\u8BA4\u3002
3359
+
3360
+ \u5B89\u88C5\u540E\u7528 \`gitnexus --version\` \u9A8C\u8BC1\uFF1B\u5408\u9002\u65F6\u7EE7\u7EED\u8FD0\u884C \`fet graph init\` \u548C \`fet graph handoff\`\u3002\u5982\u679C\u5B89\u88C5\u5931\u8D25\uFF0C\u6C47\u603B\u5931\u8D25\u547D\u4EE4\u548C\u4E0B\u4E00\u6B65\u4EBA\u5DE5\u5904\u7406\u5EFA\u8BAE\u3002`}
2713
3361
  `;
2714
3362
  }
2715
3363
  return `<!-- FET:MANAGED
@@ -2722,15 +3370,15 @@ FET:END -->
2722
3370
 
2723
3371
  ---
2724
3372
  name: fet-${command}
2725
- description: Run FET-managed OpenSpec ${command} workflow from the terminal
3373
+ description: ${language === "en" ? `Run FET-managed OpenSpec ${command} workflow from the terminal` : `\u4ECE\u7EC8\u7AEF\u8FD0\u884C FET \u6258\u7BA1\u7684 OpenSpec ${command} \u5DE5\u4F5C\u6D41`}
2726
3374
  disable-model-invocation: true
2727
3375
  ---
2728
3376
 
2729
- ${renderIdeModelPolicy(command)}
3377
+ ${renderIdeModelPolicy(command, language)}
2730
3378
 
2731
- If GitNexus code graph context is available in Cursor, prefer it before broad repository scans. If it is unavailable, continue normally.
3379
+ ${languageInstruction(language)}
2732
3380
 
2733
- \u6CE8\u610F\uFF1A\u6B64\u6587\u4EF6\u91C7\u7528 Cursor Skill \u76EE\u5F55\u7ED3\u6784\u3002\u5B83\u63D0\u4F9B \`/fet-${command}\` \u98CE\u683C\u7684\u5DE5\u4F5C\u6D41\u8BF4\u660E\uFF0C\u4E0D\u627F\u8BFA\u6CE8\u518C \`/fet ${command}\` \u8FD9\u79CD\u5E26\u7A7A\u683C\u7684\u539F\u751F slash command\u3002
3381
+ \u6B64\u6587\u4EF6\u91C7\u7528 Cursor Skill \u76EE\u5F55\u7ED3\u6784\uFF0C\u63D0\u4F9B \`/fet-${command}\` \u98CE\u683C\u7684\u5DE5\u4F5C\u6D41\u8BF4\u660E\uFF0C\u4E0D\u627F\u8BFA\u6CE8\u518C \`/fet ${command}\` \u8FD9\u79CD\u5E26\u7A7A\u683C\u7684\u539F\u751F slash command\u3002
2734
3382
 
2735
3383
  \u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
2736
3384
 
@@ -2738,7 +3386,7 @@ If GitNexus code graph context is available in Cursor, prefer it before broad re
2738
3386
  ${usage}
2739
3387
  \`\`\`
2740
3388
 
2741
- \u6267\u884C\u524D\u8BF7\u786E\u8BA4\u5DF2\u9605\u8BFB AGENTS.md \u4E0E openspec/config.yaml\u3002
3389
+ \u6267\u884C\u524D\u8BF7\u786E\u8BA4\u5DF2\u9605\u8BFB AGENTS.md \u548C openspec/config.yaml\u3002
2742
3390
  `;
2743
3391
  }
2744
3392
 
@@ -2752,10 +3400,10 @@ var CursorAdapter = class {
2752
3400
  reason: "Cursor adapter is available for any project"
2753
3401
  };
2754
3402
  }
2755
- async planInstall(_projectRoot) {
3403
+ async planInstall(_projectRoot, language) {
2756
3404
  return {
2757
3405
  tool: this.tool,
2758
- files: [...cursorSkillFiles(), cursorRuleFile()].map((file) => ({
3406
+ files: [...cursorSkillFiles(language), cursorRuleFile(language)].map((file) => ({
2759
3407
  ...file,
2760
3408
  managed: true
2761
3409
  }))
@@ -2770,7 +3418,7 @@ var CursorAdapter = class {
2770
3418
  if (existing && !existing.includes("FET:MANAGED") && !force) {
2771
3419
  throw new FetError({
2772
3420
  code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
2773
- message: "Cursor \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\u4E14\u4E0D\u5F52 FET \u7BA1\u7406",
3421
+ message: "Cursor \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u4E14\u4E0D\u7531 FET \u7BA1\u7406\u3002",
2774
3422
  details: { path: file.path },
2775
3423
  suggestedCommand: "fet init --yes"
2776
3424
  });
@@ -2795,7 +3443,7 @@ var CursorAdapter = class {
2795
3443
  checks.push({
2796
3444
  id: `cursor:${file.path}`,
2797
3445
  status: !content ? "warn" : managed && versionMatches ? "pass" : "warn",
2798
- message: !content ? `${file.path} \u7F3A\u5931` : !managed ? `${file.path} \u5B58\u5728\u4F46\u4E0D\u5F52 FET \u7BA1\u7406` : !versionMatches ? `${file.path} adapterVersion \u5DF2\u8FC7\u671F` : `${file.path} \u5B58\u5728\u4E14\u7248\u672C\u5339\u914D`,
3446
+ message: !content ? `${file.path} \u7F3A\u5931` : !managed ? `${file.path} \u5DF2\u5B58\u5728\uFF0C\u4F46\u4E0D\u7531 FET \u7BA1\u7406` : !versionMatches ? `${file.path} adapterVersion \u5DF2\u8FC7\u671F` : `${file.path} \u5DF2\u5B58\u5728\u4E14\u7248\u672C\u5339\u914D`,
2799
3447
  suggestedCommand: !content || !managed || !versionMatches ? "fet init" : void 0
2800
3448
  });
2801
3449
  }
@@ -3238,10 +3886,12 @@ var ProjectScanner = class {
3238
3886
 
3239
3887
  // src/cli/output.ts
3240
3888
  var OutputWriter = class {
3241
- constructor(json) {
3889
+ constructor(json, language = "zh-CN") {
3242
3890
  this.json = json;
3891
+ this.language = language;
3243
3892
  }
3244
3893
  json;
3894
+ language;
3245
3895
  info(message, details) {
3246
3896
  if (!this.json) {
3247
3897
  process.stdout.write(`${message}${formatDetails(details)}
@@ -3254,10 +3904,8 @@ var OutputWriter = class {
3254
3904
  `);
3255
3905
  return;
3256
3906
  }
3257
- if (!this.json) {
3258
- process.stderr.write(`\u8B66\u544A\uFF1A${message}${formatDetails(details)}
3907
+ process.stderr.write(`${this.language === "en" ? "Warning" : "\u8B66\u544A"}\uFF1A${message}${formatDetails(details)}
3259
3908
  `);
3260
- }
3261
3909
  }
3262
3910
  error(error) {
3263
3911
  if (this.json) {
@@ -3265,17 +3913,17 @@ var OutputWriter = class {
3265
3913
  `);
3266
3914
  return;
3267
3915
  }
3268
- process.stderr.write(`FET \u65E0\u6CD5\u7EE7\u7EED\uFF1A${error.message}
3916
+ process.stderr.write(`${this.language === "en" ? "FET cannot continue" : "FET \u65E0\u6CD5\u7EE7\u7EED"}\uFF1A${error.message}
3269
3917
  `);
3270
3918
  if (error.details !== void 0) {
3271
3919
  process.stderr.write(`
3272
- \u8BE6\u60C5\uFF1A
3920
+ ${this.language === "en" ? "Details" : "\u8BE6\u60C5"}\uFF1A
3273
3921
  ${formatBlock(error.details)}
3274
3922
  `);
3275
3923
  }
3276
3924
  if (error.suggestedCommand) {
3277
3925
  process.stderr.write(`
3278
- \u5EFA\u8BAE\uFF1A
3926
+ ${this.language === "en" ? "Suggestion" : "\u5EFA\u8BAE"}\uFF1A
3279
3927
  ${error.suggestedCommand}
3280
3928
  `);
3281
3929
  }
@@ -3289,11 +3937,13 @@ ${formatBlock(error.details)}
3289
3937
  process.stdout.write(`${result.summary}
3290
3938
  `);
3291
3939
  for (const warning of result.warnings ?? []) {
3292
- process.stdout.write(`\u8B66\u544A\uFF1A${warning}
3940
+ process.stdout.write(`${this.language === "en" ? "Warning" : "\u8B66\u544A"}\uFF1A${warning}
3293
3941
  `);
3294
3942
  }
3295
3943
  if (result.nextSteps?.length) {
3296
- process.stdout.write("\n\u4E0B\u4E00\u6B65\uFF1A\n");
3944
+ process.stdout.write(`
3945
+ ${this.language === "en" ? "Next steps" : "\u4E0B\u4E00\u6B65"}\uFF1A
3946
+ `);
3297
3947
  for (const step of result.nextSteps) {
3298
3948
  process.stdout.write(` ${step}
3299
3949
  `);
@@ -3318,7 +3968,10 @@ function formatBlock(details) {
3318
3968
  async function createCommandContext(command, options) {
3319
3969
  const projectRoot = resolve(options.cwd ?? process.cwd());
3320
3970
  const project = await detectProjectIdentity(projectRoot);
3321
- const output = new OutputWriter(Boolean(options.json));
3971
+ const stateStore = new StateStore(projectRoot, FET_VERSION, project);
3972
+ const savedState = await stateStore.readGlobal();
3973
+ const language = normalizeLanguage(options.lang ?? savedState?.language.current);
3974
+ const output = new OutputWriter(Boolean(options.json), language);
3322
3975
  return {
3323
3976
  command,
3324
3977
  cwd: projectRoot,
@@ -3328,9 +3981,11 @@ async function createCommandContext(command, options) {
3328
3981
  verbose: Boolean(options.verbose),
3329
3982
  yes: Boolean(options.yes),
3330
3983
  changeId: options.change,
3984
+ language,
3985
+ explicitLanguage: options.lang !== void 0,
3331
3986
  fetVersion: FET_VERSION,
3332
3987
  output,
3333
- stateStore: new StateStore(projectRoot, FET_VERSION, project),
3988
+ stateStore,
3334
3989
  openSpec: new DefaultOpenSpecAdapter(),
3335
3990
  scanner: new ProjectScanner(),
3336
3991
  toolAdapters: [new CursorAdapter(), new CodexAdapter()]
@@ -3339,25 +3994,33 @@ async function createCommandContext(command, options) {
3339
3994
 
3340
3995
  // src/cli/index.ts
3341
3996
  var program = new Command();
3342
- program.name("fet").description("Frontend workflow orchestration tool built around OpenSpec.").enablePositionalOptions().version(FET_VERSION).option("--cwd <path>", "\u6307\u5B9A\u9879\u76EE\u6839\u76EE\u5F55").option("--change <id>", "\u6307\u5B9A OpenSpec change").option("--yes", "\u5BF9\u4F4E\u98CE\u9669\u786E\u8BA4\u4F7F\u7528\u9ED8\u8BA4\u540C\u610F").option("--json", "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB JSON").option("--verbose", "\u8F93\u51FA\u8BCA\u65AD\u7EC6\u8282").option("--no-color", "\u7981\u7528\u7EC8\u7AEF\u989C\u8272");
3997
+ program.name("fet").description("\u56F4\u7ED5 OpenSpec \u7684\u524D\u7AEF\u5F00\u53D1\u5DE5\u4F5C\u6D41\u7F16\u6392\u5DE5\u5177\u3002").enablePositionalOptions().version(FET_VERSION).option("--cwd <path>", "\u6307\u5B9A\u9879\u76EE\u6839\u76EE\u5F55").option("--change <id>", "\u6307\u5B9A OpenSpec change").option("--lang <language>", "\u6307\u5B9A FET \u4EA4\u4E92\u4FE1\u606F\u548C\u751F\u6210\u4EA7\u7269\u8BED\u8A00\uFF0C\u9ED8\u8BA4 zh-CN").option("--yes", "\u5BF9\u4F4E\u98CE\u9669\u786E\u8BA4\u4F7F\u7528\u9ED8\u8BA4\u540C\u610F").option("--json", "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB JSON").option("--verbose", "\u8F93\u51FA\u8BCA\u65AD\u7EC6\u8282").option("--no-color", "\u7981\u7528\u7EC8\u7AEF\u989C\u8272");
3343
3998
  addGlobalOptions(program.command("init")).description("\u521D\u59CB\u5316 FET + OpenSpec").action(wrap("init", initCommand));
3344
3999
  addGlobalOptions(program.command("update-context")).description("\u66F4\u65B0\u9879\u76EE\u4E0A\u4E0B\u6587").action(wrap("update-context", updateContextCommand));
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"));
4000
+ addGlobalOptions(program.command("fill-context")).description("\u5237\u65B0 IDE \u586B\u5145 AGENTS.md \u5360\u4F4D\u7B26\u7684\u63D0\u793A\u6587\u4EF6").action(wrap("fill-context", fillContextCommand));
4001
+ var graph = addGlobalOptions(program.command("graph").description("\u7BA1\u7406\u53EF\u9009\u7684 GitNexus \u4EE3\u7801\u56FE\u652F\u6301"));
3347
4002
  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)));
4003
+ addGlobalOptions(graph.command(action).description(`\u8FD0\u884C fet graph ${action}`)).action(wrap("graph", (ctx) => graphCommand(ctx, action)));
3349
4004
  }
3350
4005
  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)));
4006
+ addGlobalOptions(
4007
+ graph.command(`${action} [args...]`).description(`\u901A\u8FC7 GitNexus analyze \u6267\u884C graph ${action}`).allowUnknownOption(true).passThroughOptions()
4008
+ ).action(wrap("graph", (ctx, args = []) => graphCommand(ctx, action, args)));
3352
4009
  }
3353
4010
  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(
3354
4011
  wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
3355
4012
  );
3356
- addGlobalOptions(program.command("verify").description("\u6700\u7EC8\u8D28\u91CF\u9A8C\u8BC1").option("--done", "\u58F0\u660E\u624B\u52A8\u9A8C\u8BC1\u5DF2\u5B8C\u6210").option("--auto", "\u751F\u6210\u6216\u6267\u884C\u81EA\u52A8\u9A8C\u8BC1\u8BA1\u5212")).action(wrap("verify", verifyCommand));
4013
+ addGlobalOptions(program.command("verify").description("\u6700\u7EC8\u8D28\u91CF\u9A8C\u8BC1").option("--done", "\u58F0\u660E\u624B\u52A8\u9A8C\u8BC1\u5DF2\u5B8C\u6210").option("--auto", "\u751F\u6210\u6216\u6267\u884C\u81EA\u52A8\u9A8C\u8BC1\u8BA1\u5212")).action(
4014
+ wrap("verify", verifyCommand)
4015
+ );
3357
4016
  for (const command of ["explore", "propose", "new", "continue", "ff", "apply", "sync", "archive", "bulk-archive", "onboard"]) {
3358
- addGlobalOptions(program.command(`${command} [args...]`).description(`\u4EE3\u7406\u6267\u884C openspec ${command}`).allowUnknownOption(true).passThroughOptions()).action(wrap(command, (ctx, args = []) => proxyCommand(ctx, command, args)));
4017
+ addGlobalOptions(program.command(`${command} [args...]`).description(`\u4EE3\u7406\u6267\u884C openspec ${command}`).allowUnknownOption(true).passThroughOptions()).action(
4018
+ wrap(command, (ctx, args = []) => proxyCommand(ctx, command, args))
4019
+ );
3359
4020
  }
3360
- addGlobalOptions(program.command("passthrough <command> [args...]").description("\u900F\u4F20\u6682\u672A\u63A5\u7BA1\u7684 OpenSpec \u547D\u4EE4").allowUnknownOption(true).passThroughOptions()).action(wrap("passthrough", (ctx, command, args = []) => passthroughCommand(ctx, command, args)));
4021
+ addGlobalOptions(program.command("passthrough <command> [args...]").description("\u900F\u4F20\u6682\u672A\u63A5\u7BA1\u7684 OpenSpec \u547D\u4EE4").allowUnknownOption(true).passThroughOptions()).action(
4022
+ wrap("passthrough", (ctx, command, args = []) => passthroughCommand(ctx, command, args))
4023
+ );
3361
4024
  program.parseAsync(process.argv).catch((error) => {
3362
4025
  const json = process.argv.includes("--json");
3363
4026
  const output = new OutputWriter(json);
@@ -3371,7 +4034,7 @@ function wrap(command, handler) {
3371
4034
  const opts = isCommandLike(maybeCommand) ? { ...maybeCommand.parent?.opts(), ...maybeCommand.opts() } : program.opts();
3372
4035
  const ctx = await createCommandContext(command, { ...opts, ...extractGlobalOptions(args) });
3373
4036
  try {
3374
- await confirmModelPolicyRecommendation(ctx);
4037
+ await handleModelPolicyRecommendation(ctx);
3375
4038
  await warnIfContextPlaceholdersRemain(ctx);
3376
4039
  await handler(ctx, ...args);
3377
4040
  } catch (error) {
@@ -3381,45 +4044,53 @@ function wrap(command, handler) {
3381
4044
  }
3382
4045
  };
3383
4046
  }
3384
- async function confirmModelPolicyRecommendation(ctx) {
4047
+ async function handleModelPolicyRecommendation(ctx) {
3385
4048
  const mismatch = getCommandModelPolicyMismatch(ctx.command);
3386
4049
  if (!mismatch) {
3387
4050
  return;
3388
4051
  }
3389
- const warning = formatModelPolicyMismatch(mismatch);
3390
- ctx.output.warn(`${warning} You can stop now to switch models, or continue this command.`);
3391
- if (ctx.yes || ctx.json || !process.stdin.isTTY || !process.stderr.isTTY) {
4052
+ const warning = formatModelPolicyMismatch(mismatch, ctx.language);
4053
+ const policyMode = getModelPolicyMode();
4054
+ ctx.output.warn(`${warning} ${renderModelPolicyActionHint(policyMode, ctx.language)}`);
4055
+ if (policyMode !== "confirm" || ctx.yes || ctx.json || !process.stdin.isTTY || !process.stderr.isTTY) {
3392
4056
  return;
3393
4057
  }
3394
4058
  const rl = createInterface({ input: process.stdin, output: process.stderr });
3395
4059
  try {
3396
- const answer = (await rl.question("Continue anyway? [y/N] ")).trim().toLowerCase();
4060
+ const question = ctx.language === "en" ? "Continue anyway? [y/N] " : "\u4ECD\u7136\u7EE7\u7EED\uFF1F[y/N] ";
4061
+ const answer = (await rl.question(question)).trim().toLowerCase();
3397
4062
  if (answer !== "y" && answer !== "yes") {
3398
4063
  throw new FetError({
3399
4064
  code: "USER_CANCELLED" /* UserCancelled */,
3400
- message: "Command cancelled so you can switch IDE model.",
4065
+ message: ctx.language === "en" ? "Command cancelled so you can switch IDE model." : "\u547D\u4EE4\u5DF2\u53D6\u6D88\uFF0C\u4F60\u53EF\u4EE5\u5148\u5207\u6362 IDE \u6A21\u578B\u3002",
3401
4066
  details: { command: ctx.command, detected: mismatch.detected, recommended: mismatch.recommended },
3402
- suggestedCommand: `Switch IDE model, then rerun fet ${ctx.command}.`
4067
+ suggestedCommand: ctx.language === "en" ? `Switch IDE model, then rerun fet ${ctx.command}.` : `\u5207\u6362 IDE \u6A21\u578B\u540E\u91CD\u65B0\u8FD0\u884C fet ${ctx.command}\u3002`
3403
4068
  });
3404
4069
  }
3405
4070
  } finally {
3406
4071
  rl.close();
3407
4072
  }
3408
4073
  }
4074
+ function renderModelPolicyActionHint(policyMode, language) {
4075
+ if (policyMode === "confirm") {
4076
+ return language === "en" ? "Choose whether to continue this command or stop and switch models." : "\u8BF7\u9009\u62E9\u7EE7\u7EED\u6267\u884C\u672C\u547D\u4EE4\uFF0C\u6216\u505C\u6B62\u540E\u624B\u52A8\u5207\u6362\u6A21\u578B\u3002";
4077
+ }
4078
+ return language === "en" ? "This is advisory because FET_MODEL_POLICY=warn; the command will continue." : "\u5F53\u524D\u8BBE\u7F6E FET_MODEL_POLICY=warn\uFF0C\u8BE5\u63D0\u9192\u4EC5\u4F5C\u4E3A\u5EFA\u8BAE\uFF0C\u547D\u4EE4\u4F1A\u7EE7\u7EED\u6267\u884C\u3002";
4079
+ }
3409
4080
  async function warnIfContextPlaceholdersRemain(ctx) {
3410
4081
  if (["init", "update-context", "fill-context", "doctor"].includes(ctx.command)) {
3411
4082
  return;
3412
4083
  }
3413
4084
  const count2 = await countAgentsLlmPlaceholders(ctx.projectRoot);
3414
4085
  if (count2 > 0) {
3415
- ctx.output.warn(renderAgentsPlaceholderWarning(count2));
4086
+ ctx.output.warn(renderAgentsPlaceholderWarning(count2, ctx.language));
3416
4087
  }
3417
4088
  }
3418
4089
  function isCommandLike(value) {
3419
4090
  return value instanceof Command;
3420
4091
  }
3421
4092
  function addGlobalOptions(command) {
3422
- return command.option("--cwd <path>", "\u6307\u5B9A\u9879\u76EE\u6839\u76EE\u5F55").option("--change <id>", "\u6307\u5B9A OpenSpec change").option("--yes", "\u5BF9\u4F4E\u98CE\u9669\u786E\u8BA4\u4F7F\u7528\u9ED8\u8BA4\u540C\u610F").option("--json", "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB JSON").option("--verbose", "\u8F93\u51FA\u8BCA\u65AD\u7EC6\u8282").option("--no-color", "\u7981\u7528\u7EC8\u7AEF\u989C\u8272");
4093
+ return command.option("--cwd <path>", "\u6307\u5B9A\u9879\u76EE\u6839\u76EE\u5F55").option("--change <id>", "\u6307\u5B9A OpenSpec change").option("--lang <language>", "\u6307\u5B9A FET \u4EA4\u4E92\u4FE1\u606F\u548C\u751F\u6210\u4EA7\u7269\u8BED\u8A00\uFF0C\u9ED8\u8BA4 zh-CN").option("--yes", "\u5BF9\u4F4E\u98CE\u9669\u786E\u8BA4\u4F7F\u7528\u9ED8\u8BA4\u540C\u610F").option("--json", "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB JSON").option("--verbose", "\u8F93\u51FA\u8BCA\u65AD\u7EC6\u8282").option("--no-color", "\u7981\u7528\u7EC8\u7AEF\u989C\u8272");
3423
4094
  }
3424
4095
  function extractGlobalOptions(args) {
3425
4096
  const values = args.flatMap((arg) => Array.isArray(arg) ? arg : []);
@@ -3439,6 +4110,11 @@ function extractGlobalOptions(args) {
3439
4110
  index += 1;
3440
4111
  } else if (value.startsWith("--change=")) {
3441
4112
  options.change = value.slice("--change=".length);
4113
+ } else if (value === "--lang" && typeof values[index + 1] === "string") {
4114
+ options.lang = values[index + 1];
4115
+ index += 1;
4116
+ } else if (value.startsWith("--lang=")) {
4117
+ options.lang = value.slice("--lang=".length);
3442
4118
  } else if (value === "--yes") {
3443
4119
  options.yes = true;
3444
4120
  } else if (value === "--json") {