@nick848/fet 1.0.6 → 1.0.7

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,1078 @@ 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.projectRoot));
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: `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"}), graph ${state.graphExists ? "found" : "not found"}`
303
+ } : {
304
+ id: "gitnexus",
305
+ status: "warn",
306
+ message: "Optional GitNexus code graph support is not installed",
307
+ suggestedCommand: "Install GitNexus later if you want OpenSpec artifacts to prefer a repository graph"
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(projectRoot) {
330
+ try {
331
+ await readFile4(join6(projectRoot, "AGENTS.md"), "utf8");
332
+ const count2 = await countAgentsLlmPlaceholders(projectRoot);
333
+ return count2 ? {
334
+ id: "context-placeholders",
335
+ status: "warn",
336
+ message: `AGENTS.md has ${count2} LLM placeholder(s)`,
337
+ suggestedCommand: "fet fill-context"
338
+ } : { id: "context-placeholders", status: "pass", message: "AGENTS.md placeholders resolved" };
339
+ } catch {
340
+ return { id: "context-placeholders", status: "warn", message: "AGENTS.md missing", suggestedCommand: "fet update-context" };
322
341
  }
323
- return content.slice(start + AUTO_BEGIN.length, end).trim();
324
342
  }
325
- function count(content, needle) {
326
- return content.split(needle).length - 1;
343
+ async function exists(path) {
344
+ try {
345
+ await stat3(path);
346
+ return true;
347
+ } catch {
348
+ return false;
349
+ }
327
350
  }
328
351
 
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]
352
+ // src/commands/fill-context.ts
353
+ import { mkdir as mkdir3 } from "fs/promises";
354
+ import { dirname as dirname4, join as join7 } from "path";
355
+ async function fillContextCommand(ctx) {
356
+ await withProjectLock(ctx.projectRoot, { command: "fill-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
357
+ const handoffPath = join7(ctx.projectRoot, ".fet", "fill-context.md");
358
+ await mkdir3(dirname4(handoffPath), { recursive: true });
359
+ await atomicWrite(handoffPath, renderGenericHandoff(ctx.language));
360
+ for (const adapter of ctx.toolAdapters) {
361
+ const plan = await adapter.planInstall(ctx.projectRoot, ctx.language);
362
+ const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
363
+ const state = await ctx.stateStore.getOrCreateGlobal();
364
+ state.toolAdapters[adapter.tool] = {
365
+ adapterVersion: adapter.adapterVersion,
366
+ installed: true,
367
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
368
+ };
369
+ await ctx.stateStore.writeGlobal(state);
370
+ if (ctx.verbose) {
371
+ ctx.output.info(ctx.language === "en" ? `Updated ${adapter.tool} adapter` : `\u5DF2\u66F4\u65B0 ${adapter.tool} \u9002\u914D\u5668`, { written: result.written });
372
+ }
373
+ }
374
+ });
375
+ const placeholders = await countAgentsLlmPlaceholders(ctx.projectRoot);
376
+ ctx.output.result({
377
+ ok: true,
378
+ command: "fill-context",
379
+ 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",
380
+ nextSteps: ctx.language === "en" ? placeholders ? [
381
+ "Cursor: run /fet-fill-context",
382
+ "Codex: run /prompts:fet-fill-context",
383
+ "OpenCode or other IDEs: open .fet/fill-context.md or run fet fill-context for handoff instructions"
384
+ ] : ["Run fet doctor to confirm project context health"] : placeholders ? [
385
+ "Cursor\uFF1A\u8FD0\u884C /fet-fill-context",
386
+ "Codex\uFF1A\u8FD0\u884C /prompts:fet-fill-context",
387
+ "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"
388
+ ] : ["\u8FD0\u884C fet doctor \u786E\u8BA4\u9879\u76EE\u4E0A\u4E0B\u6587\u72B6\u6001"],
389
+ data: {
390
+ placeholders,
391
+ cursorCommand: "/fet-fill-context",
392
+ codexCommand: "/prompts:fet-fill-context"
393
+ }
394
+ });
395
+ }
396
+ function renderGenericHandoff(language) {
397
+ if (language === "en") {
398
+ return `<!-- FET:MANAGED
399
+ schemaVersion: 1
400
+ generator: fill-context
401
+ FET:END -->
360
402
 
361
- ## Routes
403
+ # FET Fill Context
362
404
 
363
- | Route | Source | Confidence |
364
- |-------|--------|------------|
365
- ${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
405
+ Use the IDE AI to complete FET-generated placeholders.
366
406
 
367
- ## Conventions
407
+ 1. Read AGENTS.md and openspec/config.yaml.
408
+ 2. Read .fet/karpathy-guidelines.md when it exists. For Codex, also read .codex/fet/karpathy-guidelines.md when it exists.
409
+ 3. Inspect README files, package scripts, routes, tests, source layout, and project conventions.
410
+ 4. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete project-specific content.
411
+ 5. Preserve FET managed markers.
412
+ 6. Do not modify business code.
413
+ 7. Run \`fet doctor\` and confirm no AGENTS.md placeholder warning remains.
414
+ `;
415
+ }
416
+ return `<!-- FET:MANAGED
417
+ schemaVersion: 1
418
+ generator: fill-context
419
+ FET:END -->
368
420
 
369
- [NEEDS LLM INPUT]
421
+ # FET \u586B\u5145\u4E0A\u4E0B\u6587
370
422
 
371
- ## AI Work Guidelines
423
+ \u4F7F\u7528 IDE AI \u8865\u9F50 FET \u751F\u6210\u7684\u9879\u76EE\u4E0A\u4E0B\u6587\u5360\u4F4D\u7B26\u3002
372
424
 
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.
425
+ 1. \u9605\u8BFB AGENTS.md \u548C openspec/config.yaml\u3002
426
+ 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
427
+ 3. \u68C0\u67E5 README\u3001package scripts\u3001\u8DEF\u7531\u3001\u6D4B\u8BD5\u3001\u6E90\u7801\u7ED3\u6784\u548C\u9879\u76EE\u7EA6\u5B9A\u3002
428
+ 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
429
+ 5. \u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\u3002
430
+ 6. \u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
431
+ 7. \u8FD0\u884C \`fet doctor\`\uFF0C\u786E\u8BA4\u4E0D\u518D\u6709 AGENTS.md \u5360\u4F4D\u7B26\u8B66\u544A\u3002
432
+ `;
433
+ }
376
434
 
377
- ## Scanner Metadata
435
+ // src/commands/graph.ts
436
+ import { mkdir as mkdir4 } from "fs/promises";
437
+ import { dirname as dirname5, join as join8 } from "path";
438
+ async function graphCommand(ctx, action, args = []) {
439
+ switch (action) {
440
+ case "status":
441
+ await graphStatusCommand(ctx);
442
+ return;
443
+ case "doctor":
444
+ await graphDoctorCommand(ctx);
445
+ return;
446
+ case "setup":
447
+ await graphSetupCommand(ctx);
448
+ return;
449
+ case "handoff":
450
+ await graphHandoffCommand(ctx);
451
+ return;
452
+ case "init":
453
+ await graphAnalyzeCommand(ctx, "init", args);
454
+ return;
455
+ case "refresh":
456
+ await graphAnalyzeCommand(ctx, "refresh", args);
457
+ return;
458
+ }
459
+ }
460
+ async function graphStatusCommand(ctx) {
461
+ const result = await refreshGraphState(ctx, { runStatus: true });
462
+ const warnings = result.state.installed ? [] : ["GitNexus is not installed. Run fet graph setup for installation handoff instructions."];
463
+ ctx.output.result({
464
+ ok: true,
465
+ command: "graph status",
466
+ 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.",
467
+ warnings,
468
+ nextSteps: result.state.installed && !result.state.graphExists ? ["Run fet graph init to build the first GitNexus graph"] : void 0,
469
+ data: result
470
+ });
471
+ }
472
+ async function graphDoctorCommand(ctx) {
473
+ const result = await refreshGraphState(ctx, { runStatus: true });
474
+ const warnings = [
475
+ ...!result.state.installed ? ["GitNexus is not installed."] : [],
476
+ ...result.state.installed && !result.state.graphExists ? ["GitNexus is installed but no graph directory was found."] : [],
477
+ ...!result.state.handoffPath ? ["Graph handoff instructions have not been generated."] : []
478
+ ];
479
+ ctx.output.result({
480
+ ok: true,
481
+ command: "graph doctor",
482
+ summary: warnings.length ? `Graph doctor completed with ${warnings.length} warning(s).` : "Graph doctor completed without warnings.",
483
+ warnings,
484
+ nextSteps: warnings.length ? ["Run fet graph setup", "Run fet graph handoff", "Run fet graph init when GitNexus is installed"] : void 0,
485
+ data: result
486
+ });
487
+ }
488
+ async function graphSetupCommand(ctx) {
489
+ let result;
490
+ const handoffPath = join8(ctx.projectRoot, ".fet", "graph-setup.md");
491
+ await withProjectLock(ctx.projectRoot, { command: "graph setup", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
492
+ result = await refreshGraphState(ctx, { write: false });
493
+ await writeHandoffFile(handoffPath, renderGraphSetupHandoff(result.state));
494
+ const global = await ctx.stateStore.getOrCreateGlobal();
495
+ global.graph ??= {};
496
+ global.graph.gitnexus = {
497
+ ...result.state,
498
+ setupHandoffPath: ".fet/graph-setup.md",
499
+ setupHandoffUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
500
+ };
501
+ await ctx.stateStore.writeGlobal(global);
502
+ });
503
+ ctx.output.result({
504
+ ok: true,
505
+ command: "graph setup",
506
+ summary: "GitNexus setup handoff generated.",
507
+ warnings: result.state.installed ? [] : ["GitNexus is not installed. The handoff explains installation and IDE-assisted setup options."],
508
+ 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"],
509
+ data: {
510
+ path: ".fet/graph-setup.md",
511
+ gitnexus: result.state
512
+ }
513
+ });
514
+ }
515
+ async function graphHandoffCommand(ctx) {
516
+ let result;
517
+ const handoffPath = join8(ctx.projectRoot, ".fet", "graph-handoff.md");
518
+ await withProjectLock(ctx.projectRoot, { command: "graph handoff", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
519
+ result = await refreshGraphState(ctx, { runStatus: true, write: false });
520
+ await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state));
521
+ const global = await ctx.stateStore.getOrCreateGlobal();
522
+ global.graph ??= {};
523
+ global.graph.gitnexus = {
524
+ ...result.state,
525
+ handoffPath: ".fet/graph-handoff.md",
526
+ handoffUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
527
+ };
528
+ await ctx.stateStore.writeGlobal(global);
529
+ });
530
+ ctx.output.result({
531
+ ok: true,
532
+ command: "graph handoff",
533
+ summary: "GitNexus graph usage handoff generated.",
534
+ warnings: result.state.installed ? [] : ["GitNexus is not installed. The handoff still documents the fallback behavior."],
535
+ nextSteps: ["Cursor/Codex/OpenCode: read .fet/graph-handoff.md before broad repository scans"],
536
+ data: {
537
+ path: ".fet/graph-handoff.md",
538
+ gitnexus: result.state
539
+ }
540
+ });
541
+ }
542
+ async function graphAnalyzeCommand(ctx, mode, args) {
543
+ const detection = await detectGitNexus();
544
+ if (!detection.installed) {
545
+ throw new FetError({
546
+ code: "GRAPH_PROVIDER_NOT_FOUND" /* GraphProviderNotFound */,
547
+ message: "GitNexus is not installed or is not available on PATH.",
548
+ details: { executable: detection.executablePath, error: detection.error },
549
+ suggestedCommand: "fet graph setup"
550
+ });
551
+ }
552
+ const run = await runGitNexus(["analyze", ...args], { cwd: ctx.projectRoot });
553
+ if (run.exitCode !== 0) {
554
+ throw new FetError({
555
+ code: "GRAPH_COMMAND_FAILED" /* GraphCommandFailed */,
556
+ message: "GitNexus analyze failed.",
557
+ details: { command: run.command.join(" "), exitCode: run.exitCode, stdout: run.stdout, stderr: run.stderr },
558
+ suggestedCommand: "fet graph doctor"
559
+ });
560
+ }
561
+ const result = await refreshGraphState(ctx, { write: false });
562
+ const global = await ctx.stateStore.getOrCreateGlobal();
563
+ global.graph ??= {};
564
+ global.graph.gitnexus = {
565
+ ...result.state,
566
+ lastRefreshAt: (/* @__PURE__ */ new Date()).toISOString()
567
+ };
568
+ await ctx.stateStore.writeGlobal(global);
569
+ ctx.output.result({
570
+ ok: true,
571
+ command: `graph ${mode}`,
572
+ summary: mode === "init" ? "GitNexus graph initialized." : "GitNexus graph refreshed.",
573
+ warnings: result.state.graphExists ? [] : ["GitNexus analyze completed, but the configured graph directory was not found."],
574
+ nextSteps: ["Run fet graph status", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"],
575
+ data: {
576
+ gitnexus: global.graph.gitnexus,
577
+ run: {
578
+ command: run.command,
579
+ stdout: run.stdout.trim(),
580
+ stderr: run.stderr.trim()
581
+ }
582
+ }
583
+ });
584
+ }
585
+ async function refreshGraphState(ctx, options = {}) {
586
+ const global = await ctx.stateStore.getOrCreateGlobal();
587
+ global.graph ??= {};
588
+ const detection = await detectGitNexus();
589
+ const graph2 = await inspectGitNexusGraph(ctx.projectRoot);
590
+ let state = mergeGitNexusGraphInfo(toGitNexusState(detection, global.graph.gitnexus), graph2);
591
+ let gitnexusStatus = null;
592
+ if (options.runStatus && detection.installed) {
593
+ gitnexusStatus = await runGitNexus(["status"], { cwd: ctx.projectRoot });
594
+ state = {
595
+ ...state,
596
+ lastStatus: firstLine(gitnexusStatus.stdout) || firstLine(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
597
+ };
598
+ }
599
+ if (options.write ?? true) {
600
+ global.graph.gitnexus = state;
601
+ await ctx.stateStore.writeGlobal(global);
602
+ }
603
+ return {
604
+ state,
605
+ gitnexusStatus: gitnexusStatus ? {
606
+ exitCode: gitnexusStatus.exitCode,
607
+ command: gitnexusStatus.command,
608
+ stdout: gitnexusStatus.stdout.trim(),
609
+ stderr: gitnexusStatus.stderr.trim()
610
+ } : null
611
+ };
612
+ }
613
+ async function writeHandoffFile(path, content) {
614
+ await mkdir4(dirname5(path), { recursive: true });
615
+ await atomicWrite(path, content);
616
+ }
617
+ function renderGraphSetupHandoff(state) {
618
+ return `<!-- FET:MANAGED
619
+ schemaVersion: 1
620
+ generator: graph-setup
621
+ FET:END -->
378
622
 
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}
623
+ # FET Graph Setup
384
624
 
385
- ${USER_BEGIN}
386
- ## Notes For AI
625
+ GitNexus graph support is optional. FET does not install GitNexus automatically and does not require graph support for OpenSpec workflows.
387
626
 
388
- [NEEDS LLM INPUT]
389
- ${USER_END}
627
+ Current status:
628
+
629
+ - Installed: ${state.installed ? "yes" : "no"}
630
+ - Executable: ${state.executablePath ?? "gitnexus"}
631
+ - Version: ${state.version ?? "unknown"}
632
+ - Graph path: ${state.graphPath ?? ".gitnexus"}
633
+ - Graph exists: ${state.graphExists ? "yes" : "no"}
634
+
635
+ Suggested setup flow:
636
+
637
+ 1. If GitNexus is not installed, install it using the method recommended by the GitNexus project.
638
+ 2. If you want GitNexus MCP or IDE integration, run \`gitnexus setup\` yourself after reviewing what it changes.
639
+ 3. Return to this project and run \`fet graph init\` to build the first graph.
640
+ 4. Run \`fet graph handoff\` so IDE AI can prefer graph context before broad repository scans.
641
+
642
+ Guardrails:
643
+
644
+ - Do not block FET/OpenSpec commands when GitNexus is unavailable.
645
+ - Do not generate or modify application code during setup.
646
+ - Do not run global IDE configuration commands unless the user explicitly approves them.
390
647
  `;
391
648
  }
649
+ function renderGraphUsageHandoff(state) {
650
+ return `<!-- FET:MANAGED
651
+ schemaVersion: 1
652
+ generator: graph-handoff
653
+ FET:END -->
392
654
 
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
655
+ # FET Graph Handoff
656
+
657
+ Use GitNexus graph context as an optional first pass before broad repository scans.
658
+
659
+ Current status:
660
+
661
+ - Installed: ${state.installed ? "yes" : "no"}
662
+ - Graph path: ${state.graphPath ?? ".gitnexus"}
663
+ - Graph exists: ${state.graphExists ? "yes" : "no"}
664
+ - Last indexed at: ${state.lastIndexedAt ?? "unknown"}
665
+ - Last status: ${state.lastStatus ?? "unknown"}
666
+
667
+ When graph context is available:
668
+
669
+ 1. Use the graph to identify likely modules, dependencies, and insertion points.
670
+ 2. Read only the concrete source files needed to confirm behavior.
671
+ 3. Prefer OpenSpec artifacts and AGENTS.md over graph guesses when they conflict.
672
+ 4. Fall back to normal repository inspection if the graph is missing, stale, or incomplete.
673
+
674
+ When producing OpenSpec artifacts:
675
+
676
+ - Use graph context to make proposal, design, specs, and tasks more precise.
677
+ - Avoid large repository scans when the graph already narrows the relevant area.
678
+ - Keep all generated artifacts in the normal OpenSpec change directory.
679
+ `;
680
+ }
681
+ function firstLine(value) {
682
+ return value.trim().split(/\r?\n/)[0]?.trim() || null;
683
+ }
684
+
685
+ // src/commands/init.ts
686
+ import { readFile as readFile7, stat as stat4 } from "fs/promises";
687
+ import { join as join11 } from "path";
688
+
689
+ // src/version.ts
690
+ import { existsSync, readFileSync } from "fs";
691
+ import { dirname as dirname6, join as join9, parse } from "path";
692
+ import { fileURLToPath } from "url";
693
+ var FET_VERSION = readPackageVersion();
694
+ function readPackageVersion() {
695
+ let currentDir = dirname6(fileURLToPath(import.meta.url));
696
+ const root = parse(currentDir).root;
697
+ while (true) {
698
+ const packageJsonPath = join9(currentDir, "package.json");
699
+ if (existsSync(packageJsonPath)) {
700
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
701
+ if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
702
+ return packageJson.version;
419
703
  }
704
+ throw new Error(`package.json \u7F3A\u5C11\u6709\u6548\u7684 version \u5B57\u6BB5: ${packageJsonPath}`);
420
705
  }
706
+ if (currentDir === root) {
707
+ throw new Error("\u65E0\u6CD5\u5B9A\u4F4D FET package.json");
708
+ }
709
+ currentDir = dirname6(currentDir);
710
+ }
711
+ }
712
+
713
+ // src/templates/markers.ts
714
+ var AUTO_BEGIN = "<!-- FET:BEGIN AUTO -->";
715
+ var AUTO_END = "<!-- FET:END AUTO -->";
716
+ var USER_BEGIN = "<!-- FET:BEGIN USER -->";
717
+ var USER_END = "<!-- FET:END USER -->";
718
+ var LLM_PLACEHOLDER_PATTERN = /\[NEEDS? LLM INPUT\]/;
719
+ function hasManagedAutoRegion(content) {
720
+ return count(content, AUTO_BEGIN) === 1 && count(content, AUTO_END) === 1 && content.indexOf(AUTO_BEGIN) < content.indexOf(AUTO_END);
721
+ }
722
+ function hasInvalidManagedAutoRegion(content) {
723
+ const beginCount = count(content, AUTO_BEGIN);
724
+ const endCount = count(content, AUTO_END);
725
+ return beginCount !== endCount || beginCount > 1 || endCount > 1 || beginCount === 1 && content.indexOf(AUTO_BEGIN) > content.indexOf(AUTO_END);
726
+ }
727
+ function replaceManagedRegion(existing, generated) {
728
+ if (!existing) {
729
+ return generated;
730
+ }
731
+ const start = existing.indexOf(AUTO_BEGIN);
732
+ const end = existing.indexOf(AUTO_END);
733
+ if (start === -1 || end === -1 || end < start) {
734
+ return generated;
735
+ }
736
+ const before = existing.slice(0, start);
737
+ const after = existing.slice(end + AUTO_END.length);
738
+ const existingAuto = extractAuto(existing);
739
+ const generatedAuto = extractAuto(generated);
740
+ return `${before}${AUTO_BEGIN}
741
+ ${mergeAutoRegion(existingAuto, generatedAuto)}
742
+ ${AUTO_END}${after}`;
743
+ }
744
+ function mergeAutoRegion(existingAuto, generatedAuto) {
745
+ const generatedSections = splitMarkdownSections(generatedAuto);
746
+ const existingSections = new Map(splitMarkdownSections(existingAuto).map((section) => [sectionKey(section.heading), section]));
747
+ if (!generatedSections.length || !existingSections.size) {
748
+ return generatedAuto;
749
+ }
750
+ return generatedSections.map((section) => {
751
+ const existing = existingSections.get(sectionKey(section.heading));
752
+ if (existing && LLM_PLACEHOLDER_PATTERN.test(section.body) && !LLM_PLACEHOLDER_PATTERN.test(existing.body)) {
753
+ return existing.raw.trim();
754
+ }
755
+ return section.raw.trim();
756
+ }).join("\n\n");
757
+ }
758
+ function sectionKey(heading) {
759
+ const normalized = heading.replace(/^##\s+/, "").trim().toLowerCase();
760
+ const aliases = {
761
+ "project snapshot": "snapshot",
762
+ "\u9879\u76EE\u5FEB\u7167": "snapshot",
763
+ workspaces: "workspaces",
764
+ "\u5DE5\u4F5C\u533A": "workspaces",
765
+ commands: "commands",
766
+ "\u547D\u4EE4": "commands",
767
+ structure: "structure",
768
+ "\u7ED3\u6784": "structure",
769
+ routes: "routes",
770
+ "\u8DEF\u7531": "routes",
771
+ conventions: "conventions",
772
+ "\u7EA6\u5B9A": "conventions",
773
+ "ai work guidelines": "ai-guidelines",
774
+ "ai \u5DE5\u4F5C\u6307\u5357": "ai-guidelines",
775
+ "scanner metadata": "metadata",
776
+ "\u626B\u63CF\u5143\u6570\u636E": "metadata",
777
+ "notes for ai": "notes",
778
+ "\u7ED9 ai \u7684\u5907\u6CE8": "notes"
779
+ };
780
+ return aliases[normalized] ?? normalized;
781
+ }
782
+ function splitMarkdownSections(content) {
783
+ const matches = [...content.matchAll(/^## .+$/gm)];
784
+ if (!matches.length) {
785
+ return [];
786
+ }
787
+ return matches.map((match, index) => {
788
+ const start = match.index ?? 0;
789
+ const end = matches[index + 1]?.index ?? content.length;
790
+ const raw = content.slice(start, end).trim();
791
+ const newline = raw.indexOf("\n");
792
+ const heading = newline === -1 ? raw.trim() : raw.slice(0, newline).trim();
793
+ const body = newline === -1 ? "" : raw.slice(newline + 1).trim();
794
+ return { heading, body, raw };
421
795
  });
422
796
  }
423
-
424
- // src/templates/karpathy-skills.ts
425
- var KARPATHY_SKILLS_SOURCE = "https://github.com/forrestchang/andrej-karpathy-skills";
426
- var BEGIN = "<!-- FET:BEGIN ANDREJ-KARPATHY-SKILLS -->";
427
- var END = "<!-- FET:END ANDREJ-KARPATHY-SKILLS -->";
428
- function mergeKarpathyClaudeMd(existing) {
429
- const block = renderManagedBlock(renderKarpathyClaudeGuidelines());
430
- if (!existing || !existing.trim()) {
431
- return `${block}
432
- `;
433
- }
434
- const start = existing.indexOf(BEGIN);
435
- const end = existing.indexOf(END);
436
- if (start !== -1 && end !== -1 && end > start) {
437
- return `${existing.slice(0, start)}${block}${existing.slice(end + END.length)}`;
797
+ function extractAuto(content) {
798
+ const start = content.indexOf(AUTO_BEGIN);
799
+ const end = content.indexOf(AUTO_END);
800
+ if (start === -1 || end === -1 || end < start) {
801
+ return content.trim();
438
802
  }
439
- return `${existing.replace(/\s*$/, "")}
440
-
441
- ${block}
442
- `;
803
+ return content.slice(start + AUTO_BEGIN.length, end).trim();
443
804
  }
444
- function renderKarpathyCursorRule() {
445
- return `<!-- FET:MANAGED
446
- schemaVersion: 1
447
- generator: karpathy-skills
448
- FET:END -->
449
-
450
- ---
451
- description: Andrej Karpathy inspired coding guidelines
452
- alwaysApply: true
453
- ---
454
-
455
- ${renderKarpathyGuidelinesBody()}
456
- `;
805
+ function count(content, needle) {
806
+ return content.split(needle).length - 1;
457
807
  }
458
- function renderKarpathyFetHandoff() {
459
- return `<!-- FET:MANAGED
460
- schemaVersion: 1
461
- generator: karpathy-skills
462
- FET:END -->
463
808
 
464
- # Andrej Karpathy Inspired Coding Guidelines
465
-
466
- ${renderKarpathyGuidelinesBody()}
467
- `;
468
- }
469
- function renderManagedBlock(content) {
470
- return `${BEGIN}
471
- ${content}
472
- ${END}`;
809
+ // src/templates/agents-md.ts
810
+ function renderAgentsMd(scan, language = "zh-CN") {
811
+ if (language === "en") {
812
+ return renderAgentsMdEn(scan);
813
+ }
814
+ return renderAgentsMdZh(scan);
473
815
  }
474
- function renderKarpathyClaudeGuidelines() {
475
- return `# Andrej Karpathy Inspired Coding Guidelines
816
+ function renderAgentsMdZh(scan) {
817
+ const commands = Object.entries(scan.commands).map(([name, command]) => `| ${name} | \`${command.command}\` | ${command.source} |`).join("\n");
818
+ const routes = scan.routes.map((route) => `| ${route.path} | ${route.source} | ${route.confidence}${route.inferred ? " inferred" : ""} |`).join("\n");
819
+ const workspaces = scan.project.workspaces.map((workspace) => `| ${workspace.name} | ${workspace.path} | ${workspace.source} |`).join("\n");
820
+ return `# \u9879\u76EE\u4E0A\u4E0B\u6587
476
821
 
477
- ${renderKarpathyGuidelinesBody()}`;
478
- }
479
- function renderKarpathyGuidelinesBody() {
480
- return `Source: ${KARPATHY_SKILLS_SOURCE}
822
+ ${AUTO_BEGIN}
823
+ ## \u9879\u76EE\u5FEB\u7167
481
824
 
482
- Use these project-level guidelines together with AGENTS.md, OpenSpec artifacts, and the user's latest request.
825
+ - \u540D\u79F0\uFF1A${scan.project.name}
826
+ - \u5305\u7BA1\u7406\u5668\uFF1A${scan.project.packageManager}\uFF08${scan.project.packageManagerConfidence}\uFF09
827
+ - \u6846\u67B6\uFF1A${scan.project.framework.name}\uFF08${scan.project.framework.confidence}\uFF09
828
+ - \u8BED\u8A00\uFF1A${scan.project.language}
829
+ - Monorepo\uFF1A${scan.project.monorepo ? "\u662F" : "\u5426"}
483
830
 
484
- ## Think Before Coding
831
+ ## \u5DE5\u4F5C\u533A
485
832
 
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.
833
+ | \u540D\u79F0 | \u8DEF\u5F84 | \u6765\u6E90 |
834
+ |------|------|--------|
835
+ ${workspaces || "| root | . | inferred |"}
490
836
 
491
- ## Simplicity First
837
+ ## \u547D\u4EE4
492
838
 
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.
839
+ | \u540D\u79F0 | \u547D\u4EE4 | \u6765\u6E90 |
840
+ |------|---------|--------|
841
+ ${commands || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | [NEEDS LLM INPUT] |"}
497
842
 
498
- ## Precise Edits
843
+ ## \u7ED3\u6784
499
844
 
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.
845
+ [NEEDS LLM INPUT]
504
846
 
505
- ## Goal-Driven Execution
847
+ ## \u8DEF\u7531
506
848
 
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.
849
+ | \u8DEF\u7531 | \u6765\u6E90 | \u7F6E\u4FE1\u5EA6 |
850
+ |-------|--------|------------|
851
+ ${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
511
852
 
512
- These guidelines intentionally favor caution over speed for non-trivial work. For obvious one-line fixes, use judgment and stay lightweight.`;
513
- }
853
+ ## \u7EA6\u5B9A
514
854
 
515
- // src/templates/verify-instructions.ts
516
- function renderVerifyInstructions(changeId, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
517
- return `---
518
- schemaVersion: 1
519
- fetVersion: ${FET_VERSION}
520
- generatedAt: ${generatedAt}
521
- changeId: ${changeId}
522
- purpose: manual-verify
523
- ---
855
+ [NEEDS LLM INPUT]
524
856
 
525
- # Verify Instructions
857
+ ## AI \u5DE5\u4F5C\u6307\u5357
526
858
 
527
- \u8BF7\u6309\u987A\u5E8F\u5B8C\u6210\u4EE5\u4E0B\u68C0\u67E5\uFF1A
859
+ - \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
860
+ - \u5BF9 Codex\uFF0C\u8FD8\u5E94\u5728\u5B58\u5728\u65F6\u9605\u8BFB .codex/fet/karpathy-guidelines.md\u3002
861
+ - \u8FD9\u4E9B\u6307\u5357\u4F4E\u4E8E\u7528\u6237\u6700\u65B0\u8BF7\u6C42\u548C\u660E\u786E\u7684 OpenSpec \u4EA7\u7269\u3002
862
+ - \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
528
863
 
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
864
+ ## \u626B\u63CF\u5143\u6570\u636E
532
865
 
533
- \u5B8C\u6210\u540E\u8FD0\u884C\uFF1A
866
+ - \u751F\u6210\u65F6\u95F4\uFF1A${scan.generatedAt}
867
+ - FET \u7248\u672C\uFF1A${FET_VERSION}
868
+ - \u626B\u63CF\u5668\u7248\u672C\uFF1A${scan.scannerVersion}
869
+ - \u8B66\u544A\uFF1A${scan.warnings.length ? scan.warnings.join("; ") : "\u65E0"}
870
+ ${AUTO_END}
534
871
 
535
- \`\`\`sh
536
- fet verify --done --change ${changeId}
537
- \`\`\`
872
+ ${USER_BEGIN}
873
+ ## \u7ED9 AI \u7684\u5907\u6CE8
874
+
875
+ [NEEDS LLM INPUT]
876
+ ${USER_END}
538
877
  `;
539
878
  }
879
+ function renderAgentsMdEn(scan) {
880
+ const commands = Object.entries(scan.commands).map(([name, command]) => `| ${name} | \`${command.command}\` | ${command.source} |`).join("\n");
881
+ const routes = scan.routes.map((route) => `| ${route.path} | ${route.source} | ${route.confidence}${route.inferred ? " inferred" : ""} |`).join("\n");
882
+ const workspaces = scan.project.workspaces.map((workspace) => `| ${workspace.name} | ${workspace.path} | ${workspace.source} |`).join("\n");
883
+ return `# Project Context
540
884
 
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*$/, "")}
885
+ ${AUTO_BEGIN}
886
+ ## Project Snapshot
566
887
 
567
- ${block}
568
- `;
569
- }
888
+ - Name: ${scan.project.name}
889
+ - Package Manager: ${scan.project.packageManager} (${scan.project.packageManagerConfidence})
890
+ - Framework: ${scan.project.framework.name} (${scan.project.framework.confidence})
891
+ - Language: ${scan.project.language}
892
+ - Monorepo: ${scan.project.monorepo ? "yes" : "no"}
570
893
 
571
- // src/commands/update-context.ts
572
- import { readFile as readFile5 } from "fs/promises";
573
- import { join as join7 } from "path";
894
+ ## Workspaces
574
895
 
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;
586
- }
587
- const doc = parseDocument(existing || "{}");
588
- doc.set("fet", nextFet);
589
- return doc.toString();
590
- }
896
+ | Name | Path | Source |
897
+ |------|------|--------|
898
+ ${workspaces || "| root | . | inferred |"}
591
899
 
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
- }
603
- }
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.`;
606
- }
900
+ ## Commands
607
901
 
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
- });
624
- }
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
- });
643
- }
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}`);
656
- }
657
- }
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));
670
- }
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 };
679
- }
680
- async function readOptional(path) {
681
- try {
682
- return await readFile5(path, "utf8");
683
- } catch {
684
- return null;
685
- }
902
+ | Name | Command | Source |
903
+ |------|---------|--------|
904
+ ${commands || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | [NEEDS LLM INPUT] |"}
905
+
906
+ ## Structure
907
+
908
+ [NEEDS LLM INPUT]
909
+
910
+ ## Routes
911
+
912
+ | Route | Source | Confidence |
913
+ |-------|--------|------------|
914
+ ${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
915
+
916
+ ## Conventions
917
+
918
+ [NEEDS LLM INPUT]
919
+
920
+ ## AI Work Guidelines
921
+
922
+ - Prefer the project-level Andrej Karpathy inspired guidelines in .fet/karpathy-guidelines.md when using FET-managed IDE workflows.
923
+ - For Codex, also read .codex/fet/karpathy-guidelines.md when present.
924
+ - Treat those guidelines as secondary to the user's latest request and explicit OpenSpec artifacts.
925
+
926
+ ## Scanner Metadata
927
+
928
+ - Generated At: ${scan.generatedAt}
929
+ - FET Version: ${FET_VERSION}
930
+ - Scanner Version: ${scan.scannerVersion}
931
+ - Warnings: ${scan.warnings.length ? scan.warnings.join("; ") : "none"}
932
+ ${AUTO_END}
933
+
934
+ ${USER_BEGIN}
935
+ ## Notes For AI
936
+
937
+ [NEEDS LLM INPUT]
938
+ ${USER_END}
939
+ `;
686
940
  }
687
941
 
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" })));
942
+ // src/templates/config-yaml.ts
943
+ import { stringify } from "yaml";
944
+ function renderFetConfig(scan, language = "zh-CN") {
945
+ return stringify({
946
+ fet: {
947
+ schemaVersion: 1,
948
+ generatedAt: scan.generatedAt,
949
+ fetVersion: FET_VERSION,
950
+ language,
951
+ scannerVersion: scan.scannerVersion,
952
+ project: {
953
+ packageManager: scan.project.packageManager,
954
+ packageManagerConfidence: scan.project.packageManagerConfidence,
955
+ framework: scan.project.framework,
956
+ language: scan.project.language,
957
+ monorepo: scan.project.monorepo,
958
+ workspaces: scan.project.workspaces
959
+ },
960
+ commands: scan.commands,
961
+ validation: {
962
+ monorepo: scan.project.monorepo,
963
+ missing: {
964
+ lint: "warn",
965
+ typecheck: "warn",
966
+ test: "warn"
967
+ },
968
+ workspaces: scan.project.workspaces
730
969
  }
731
- journal.completedAt = (/* @__PURE__ */ new Date()).toISOString();
732
- await writeInitJournal(ctx.projectRoot, journal);
733
- await ctx.stateStore.writeGlobal(state);
734
970
  }
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
971
  });
743
972
  }
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
- }
764
973
 
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
- }
974
+ // src/templates/karpathy-skills.ts
975
+ var KARPATHY_SKILLS_SOURCE = "https://github.com/forrestchang/andrej-karpathy-skills";
976
+ var BEGIN = "<!-- FET:BEGIN ANDREJ-KARPATHY-SKILLS -->";
977
+ var END = "<!-- FET:END ANDREJ-KARPATHY-SKILLS -->";
978
+ function mergeKarpathyClaudeMd(existing) {
979
+ const block = renderManagedBlock(renderKarpathyClaudeGuidelines());
980
+ if (!existing || !existing.trim()) {
981
+ return `${block}
982
+ `;
787
983
  }
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);
984
+ const start = existing.indexOf(BEGIN);
985
+ const end = existing.indexOf(END);
986
+ if (start !== -1 && end !== -1 && end > start) {
987
+ return `${existing.slice(0, start)}${block}${existing.slice(end + END.length)}`;
807
988
  }
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
- };
989
+ return `${existing.replace(/\s*$/, "")}
990
+
991
+ ${block}
992
+ `;
818
993
  }
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
- }
994
+ function renderKarpathyCursorRule(language = "zh-CN") {
995
+ return `<!-- FET:MANAGED
996
+ schemaVersion: 1
997
+ generator: karpathy-skills
998
+ FET:END -->
999
+
1000
+ ---
1001
+ description: ${language === "en" ? "Andrej Karpathy inspired coding guidelines" : "\u53D7 Andrej Karpathy \u542F\u53D1\u7684\u7F16\u7801\u6307\u5357"}
1002
+ alwaysApply: true
1003
+ ---
1004
+
1005
+ ${renderKarpathyGuidelinesBody(language)}
1006
+ `;
826
1007
  }
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
- }
1008
+ function renderKarpathyFetHandoff(language = "zh-CN") {
1009
+ return `<!-- FET:MANAGED
1010
+ schemaVersion: 1
1011
+ generator: karpathy-skills
1012
+ FET:END -->
1013
+
1014
+ # ${language === "en" ? "Andrej Karpathy Inspired Coding Guidelines" : "\u53D7 Andrej Karpathy \u542F\u53D1\u7684\u7F16\u7801\u6307\u5357"}
1015
+
1016
+ ${renderKarpathyGuidelinesBody(language)}
1017
+ `;
834
1018
  }
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 };
1019
+ function renderManagedBlock(content) {
1020
+ return `${BEGIN}
1021
+ ${content}
1022
+ ${END}`;
837
1023
  }
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
- }
1024
+ function renderKarpathyClaudeGuidelines() {
1025
+ return `# Andrej Karpathy Inspired Coding Guidelines
1026
+
1027
+ ${renderKarpathyGuidelinesBody()}`;
851
1028
  }
852
- async function exists2(path) {
853
- try {
854
- await stat4(path);
855
- return true;
856
- } catch {
857
- return false;
1029
+ function renderKarpathyGuidelinesBody(language = "zh-CN") {
1030
+ if (language === "en") {
1031
+ return renderKarpathyGuidelinesBodyEn();
858
1032
  }
1033
+ return `\u6765\u6E90\uFF1A${KARPATHY_SKILLS_SOURCE}
1034
+
1035
+ \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
1036
+
1037
+ ## \u7F16\u7801\u524D\u5148\u601D\u8003
1038
+
1039
+ - \u7F16\u8F91\u524D\u8BF4\u660E\u91CD\u8981\u5047\u8BBE\u3002
1040
+ - \u5F53\u6B67\u4E49\u4F1A\u6539\u53D8\u5B9E\u73B0\u65F6\uFF0C\u5148\u6F84\u6E05\u3002
1041
+ - \u4E3B\u52A8\u5448\u73B0\u53D6\u820D\uFF0C\u4E0D\u8981\u9759\u9ED8\u9009\u62E9\u9AD8\u98CE\u9669\u8DEF\u5F84\u3002
1042
+ - \u5F53\u66F4\u7B80\u5355\u7684\u65B9\u6848\u66F4\u9002\u5408\u8BF7\u6C42\u65F6\uFF0C\u660E\u786E\u63D0\u51FA\u3002
1043
+
1044
+ ## \u7B80\u6D01\u4F18\u5148
1045
+
1046
+ - \u7528\u6700\u5C0F\u4E14\u6E05\u6670\u7684\u6539\u52A8\u89E3\u51B3\u8BF7\u6C42\u7684\u95EE\u9898\u3002
1047
+ - \u907F\u514D\u731C\u6D4B\u6027\u529F\u80FD\u3001\u914D\u7F6E\u6216\u62BD\u8C61\u3002
1048
+ - \u4E0D\u8981\u4E3A\u4E00\u6B21\u6027\u4EE3\u7801\u521B\u5EFA\u62BD\u8C61\u3002
1049
+ - \u4F18\u5148\u5220\u9664\u81EA\u5DF1\u6539\u52A8\u5F15\u5165\u7684\u590D\u6742\u5EA6\uFF0C\u800C\u4E0D\u662F\u7EE7\u7EED\u5806\u7ED3\u6784\u3002
1050
+
1051
+ ## \u7CBE\u51C6\u7F16\u8F91
1052
+
1053
+ - \u53EA\u4FEE\u6539\u76F4\u63A5\u670D\u52A1\u4E8E\u4EFB\u52A1\u7684\u6587\u4EF6\u548C\u884C\u3002
1054
+ - \u4FDD\u6301\u73B0\u6709\u98CE\u683C\uFF0C\u5373\u4FBF\u4F60\u4E2A\u4EBA\u66F4\u504F\u597D\u53E6\u4E00\u79CD\u6A21\u5F0F\u3002
1055
+ - \u9664\u975E\u4EFB\u52A1\u9700\u8981\uFF0C\u4E0D\u8981\u987A\u624B\u91CD\u6784\u9644\u8FD1\u4EE3\u7801\u3001\u6CE8\u91CA\u6216\u683C\u5F0F\u3002
1056
+ - \u53EA\u79FB\u9664\u56E0\u81EA\u5DF1\u6539\u52A8\u800C\u8FC7\u65F6\u7684\u6B7B\u5BFC\u5165\u3001\u53D8\u91CF\u6216 helper\u3002
1057
+
1058
+ ## \u76EE\u6807\u9A71\u52A8\u6267\u884C
1059
+
1060
+ - \u628A\u6A21\u7CCA\u5DE5\u4F5C\u8F6C\u6210\u5177\u4F53\u6210\u529F\u6807\u51C6\u3002
1061
+ - \u5BF9 bug\uFF0C\u4F18\u5148\u51C6\u5907\u590D\u73B0\u6D4B\u8BD5\u6216\u6E05\u6670\u9A8C\u8BC1\uFF0C\u518D\u505A\u4FEE\u590D\u3002
1062
+ - \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
1063
+ - \u6301\u7EED\u8FED\u4EE3\uFF0C\u76F4\u5230\u8FBE\u5230\u6210\u529F\u6807\u51C6\u6216\u660E\u786E\u9047\u5230\u963B\u585E\u3002
1064
+
1065
+ \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`;
859
1066
  }
1067
+ function renderKarpathyGuidelinesBodyEn() {
1068
+ return `Source: ${KARPATHY_SKILLS_SOURCE}
1069
+
1070
+ Use these project-level guidelines together with AGENTS.md, OpenSpec artifacts, and the user's latest request.
1071
+
1072
+ ## Think Before Coding
1073
+
1074
+ - State important assumptions before editing.
1075
+ - Ask for clarification when ambiguity would change the implementation.
1076
+ - Surface tradeoffs instead of silently choosing a risky path.
1077
+ - Push back when a simpler approach better fits the request.
1078
+
1079
+ ## Simplicity First
1080
+
1081
+ - Solve the requested problem with the smallest clear change.
1082
+ - Avoid speculative features, configuration, or abstraction.
1083
+ - Do not create abstractions for one-off code.
1084
+ - Prefer deleting complexity introduced by your own change over adding more structure.
1085
+
1086
+ ## Precise Edits
1087
+
1088
+ - Touch only files and lines that directly serve the task.
1089
+ - Preserve existing style even when you personally prefer another pattern.
1090
+ - Do not refactor nearby code, comments, or formatting unless the task requires it.
1091
+ - Remove only dead imports, variables, or helpers made obsolete by your own change.
1092
+
1093
+ ## Goal-Driven Execution
860
1094
 
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
- });
1095
+ - Convert vague work into concrete success criteria.
1096
+ - For bugs, prefer a reproducing test or clear verification before the fix.
1097
+ - For multi-step work, keep a short plan and verify each meaningful step.
1098
+ - Continue iterating until the success criteria are met or a blocker is explicit.
1099
+
1100
+ These guidelines intentionally favor caution over speed for non-trivial work. For obvious one-line fixes, use judgment and stay lightweight.`;
904
1101
  }
905
- function renderGenericHandoff() {
906
- return `<!-- FET:MANAGED
1102
+
1103
+ // src/templates/verify-instructions.ts
1104
+ function renderVerifyInstructions(changeId, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
1105
+ return `---
907
1106
  schemaVersion: 1
908
- generator: fill-context
909
- FET:END -->
1107
+ fetVersion: ${FET_VERSION}
1108
+ generatedAt: ${generatedAt}
1109
+ changeId: ${changeId}
1110
+ purpose: manual-verify
1111
+ ---
910
1112
 
911
- # FET Fill Context
1113
+ # Verify Instructions
912
1114
 
913
- Use the IDE AI to complete FET-generated placeholders.
1115
+ \u8BF7\u6309\u987A\u5E8F\u5B8C\u6210\u4EE5\u4E0B\u68C0\u67E5\uFF1A
914
1116
 
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.
1117
+ 1. \u8FD0\u884C OpenSpec \u89C4\u8303\u6821\u9A8C\uFF1A\`openspec verify\`
1118
+ 2. \u6309\u9879\u76EE\u7EA6\u5B9A\u8FD0\u884C lint\u3001typecheck\u3001test\u3002
1119
+ 3. \u68C0\u67E5\u672C\u6B21 change \u7684 \`tasks.md\` \u662F\u5426\u4E0E\u5B9E\u73B0\u72B6\u6001\u4E00\u81F4\u3002
1120
+
1121
+ \u5B8C\u6210\u540E\u8FD0\u884C\uFF1A
1122
+
1123
+ \`\`\`sh
1124
+ fet verify --done --change ${changeId}
1125
+ \`\`\`
922
1126
  `;
923
1127
  }
924
1128
 
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;
1129
+ // src/templates/gitignore.ts
1130
+ var BEGIN2 = "# FET:BEGIN LOCAL STATE";
1131
+ var END2 = "# FET:END LOCAL STATE";
1132
+ var RULES = [
1133
+ "openspec/fet-state.json",
1134
+ "openspec/.fet.lock",
1135
+ "openspec/.fet-init-journal.json",
1136
+ "openspec/changes/*/fet-state.json",
1137
+ "openspec/changes/*/.fet/",
1138
+ ".gitnexus/"
1139
+ ];
1140
+ function mergeGitignore(existing) {
1141
+ const block = `${BEGIN2}
1142
+ ${RULES.join("\n")}
1143
+ ${END2}`;
1144
+ if (!existing || !existing.trim()) {
1145
+ return `${block}
1146
+ `;
948
1147
  }
1148
+ const start = existing.indexOf(BEGIN2);
1149
+ const end = existing.indexOf(END2);
1150
+ if (start !== -1 && end !== -1 && end > start) {
1151
+ return `${existing.slice(0, start)}${block}${existing.slice(end + END2.length)}`;
1152
+ }
1153
+ return `${existing.replace(/\s*$/, "")}
1154
+
1155
+ ${block}
1156
+ `;
949
1157
  }
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
- });
1158
+
1159
+ // src/commands/update-context.ts
1160
+ import { readFile as readFile6 } from "fs/promises";
1161
+ import { join as join10 } from "path";
1162
+
1163
+ // src/config/yaml.ts
1164
+ import { readFile as readFile5 } from "fs/promises";
1165
+ import { parseDocument } from "yaml";
1166
+ async function mergeFetConfig(configPath, renderedFetYaml) {
1167
+ const fetDoc = parseDocument(renderedFetYaml);
1168
+ const nextFet = fetDoc.get("fet", true);
1169
+ let existing = "";
1170
+ try {
1171
+ existing = await readFile5(configPath, "utf8");
1172
+ } catch {
1173
+ return renderedFetYaml;
1174
+ }
1175
+ const doc = parseDocument(existing || "{}");
1176
+ doc.set("fet", nextFet);
1177
+ return doc.toString();
1004
1178
  }
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);
1179
+
1180
+ // src/commands/update-context.ts
1181
+ async function updateContextCommand(ctx) {
1182
+ let contextResult = { warnings: [] };
1183
+ await withProjectLock(ctx.projectRoot, { command: "update-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
1184
+ contextResult = await updateContextFiles(ctx);
1019
1185
  });
1020
1186
  ctx.output.result({
1021
1187
  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
- }
1188
+ command: "update-context",
1189
+ 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",
1190
+ warnings: contextResult.warnings
1030
1191
  });
1031
1192
  }
1032
- async function graphAnalyzeCommand(ctx, mode, args) {
1033
- const detection = await detectGitNexus();
1034
- if (!detection.installed) {
1035
- throw new FetError({
1036
- code: "GRAPH_PROVIDER_NOT_FOUND" /* GraphProviderNotFound */,
1037
- message: "GitNexus is not installed or is not available on PATH.",
1038
- details: { executable: detection.executablePath, error: detection.error },
1039
- suggestedCommand: "fet graph setup"
1040
- });
1041
- }
1042
- const run = await runGitNexus(["analyze", ...args], { cwd: ctx.projectRoot });
1043
- if (run.exitCode !== 0) {
1044
- throw new FetError({
1045
- code: "GRAPH_COMMAND_FAILED" /* GraphCommandFailed */,
1046
- message: "GitNexus analyze failed.",
1047
- details: { command: run.command.join(" "), exitCode: run.exitCode, stdout: run.stdout, stderr: run.stderr },
1048
- suggestedCommand: "fet graph doctor"
1049
- });
1050
- }
1051
- const result = await refreshGraphState(ctx, { write: false });
1052
- const global = await ctx.stateStore.getOrCreateGlobal();
1053
- global.graph ??= {};
1054
- global.graph.gitnexus = {
1055
- ...result.state,
1056
- lastRefreshAt: (/* @__PURE__ */ new Date()).toISOString()
1057
- };
1058
- await ctx.stateStore.writeGlobal(global);
1059
- ctx.output.result({
1060
- ok: true,
1061
- command: `graph ${mode}`,
1062
- summary: mode === "init" ? "GitNexus graph initialized." : "GitNexus graph refreshed.",
1063
- warnings: result.state.graphExists ? [] : ["GitNexus analyze completed, but the configured graph directory was not found."],
1064
- nextSteps: ["Run fet graph status", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"],
1065
- data: {
1066
- gitnexus: global.graph.gitnexus,
1067
- run: {
1068
- command: run.command,
1069
- stdout: run.stdout.trim(),
1070
- stderr: run.stderr.trim()
1071
- }
1193
+ async function updateContextFiles(ctx) {
1194
+ const scan = await ctx.scanner.scan(ctx.projectRoot, {});
1195
+ const agentsPath = join10(ctx.projectRoot, "AGENTS.md");
1196
+ const configPath = join10(ctx.projectRoot, "openspec", "config.yaml");
1197
+ const claudePath = join10(ctx.projectRoot, "CLAUDE.md");
1198
+ const karpathyHandoffPath = join10(ctx.projectRoot, ".fet", "karpathy-guidelines.md");
1199
+ const karpathyCursorPath = join10(ctx.projectRoot, ".cursor", "rules", "karpathy-guidelines.mdc");
1200
+ const existingAgents = await readOptional(agentsPath);
1201
+ const existingClaude = await readOptional(claudePath);
1202
+ const existingKarpathyCursor = await readOptional(karpathyCursorPath);
1203
+ const warnings = [...scan.warnings];
1204
+ if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
1205
+ throw new FetError({
1206
+ code: "CONFIG_INVALID" /* ConfigInvalid */,
1207
+ 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",
1208
+ details: { path: "AGENTS.md" },
1209
+ 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"
1210
+ });
1211
+ }
1212
+ if (existingAgents && !hasManagedAutoRegion(existingAgents)) {
1213
+ if (!ctx.yes) {
1214
+ throw new FetError({
1215
+ code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
1216
+ 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",
1217
+ details: { path: "AGENTS.md" },
1218
+ 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"
1219
+ });
1220
+ }
1221
+ const backupPath = await createBackup(agentsPath);
1222
+ if (backupPath) {
1223
+ warnings.push(ctx.language === "en" ? `Backed up unmanaged AGENTS.md to ${backupPath}` : `\u5DF2\u5C06\u975E\u6258\u7BA1 AGENTS.md \u5907\u4EFD\u5230 ${backupPath}`);
1072
1224
  }
1073
- });
1074
- }
1075
- async function refreshGraphState(ctx, options = {}) {
1076
- const global = await ctx.stateStore.getOrCreateGlobal();
1077
- global.graph ??= {};
1078
- const detection = await detectGitNexus();
1079
- const graph2 = await inspectGitNexusGraph(ctx.projectRoot);
1080
- let state = mergeGitNexusGraphInfo(toGitNexusState(detection, global.graph.gitnexus), graph2);
1081
- let gitnexusStatus = null;
1082
- if (options.runStatus && detection.installed) {
1083
- gitnexusStatus = await runGitNexus(["status"], { cwd: ctx.projectRoot });
1084
- state = {
1085
- ...state,
1086
- lastStatus: firstLine(gitnexusStatus.stdout) || firstLine(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
1087
- };
1088
1225
  }
1089
- if (options.write ?? true) {
1090
- global.graph.gitnexus = state;
1091
- await ctx.stateStore.writeGlobal(global);
1226
+ await atomicWrite(agentsPath, replaceManagedRegion(existingAgents, renderAgentsMd(scan, ctx.language)));
1227
+ await atomicWrite(configPath, await mergeFetConfig(configPath, renderFetConfig(scan, ctx.language)));
1228
+ await atomicWrite(claudePath, mergeKarpathyClaudeMd(existingClaude));
1229
+ await atomicWrite(karpathyHandoffPath, renderKarpathyFetHandoff(ctx.language));
1230
+ if (!existingKarpathyCursor || existingKarpathyCursor.includes("FET:MANAGED")) {
1231
+ await atomicWrite(karpathyCursorPath, renderKarpathyCursorRule(ctx.language));
1232
+ } else {
1233
+ warnings.push(
1234
+ 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"
1235
+ );
1092
1236
  }
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
1237
+ const placeholderCount = await countAgentsLlmPlaceholders(ctx.projectRoot);
1238
+ if (placeholderCount > 0) {
1239
+ warnings.push(renderAgentsPlaceholderWarning(placeholderCount, ctx.language));
1240
+ }
1241
+ const state = await ctx.stateStore.getOrCreateGlobal();
1242
+ state.context = {
1243
+ agentsMdUpdatedAt: scan.generatedAt,
1244
+ configUpdatedAt: scan.generatedAt,
1245
+ scannerVersion: scan.scannerVersion
1101
1246
  };
1247
+ await ctx.stateStore.writeGlobal(state);
1248
+ return { warnings };
1102
1249
  }
1103
- async function writeHandoffFile(path, content) {
1104
- await mkdir4(dirname6(path), { recursive: true });
1105
- await atomicWrite(path, content);
1250
+ async function readOptional(path) {
1251
+ try {
1252
+ return await readFile6(path, "utf8");
1253
+ } catch {
1254
+ return null;
1255
+ }
1106
1256
  }
1107
- function renderGraphSetupHandoff(state) {
1108
- return `<!-- FET:MANAGED
1109
- schemaVersion: 1
1110
- generator: graph-setup
1111
- FET:END -->
1112
-
1113
- # FET Graph Setup
1114
-
1115
- GitNexus graph support is optional. FET does not install GitNexus automatically and does not require graph support for OpenSpec workflows.
1116
-
1117
- Current status:
1118
-
1119
- - Installed: ${state.installed ? "yes" : "no"}
1120
- - Executable: ${state.executablePath ?? "gitnexus"}
1121
- - Version: ${state.version ?? "unknown"}
1122
- - Graph path: ${state.graphPath ?? ".gitnexus"}
1123
- - Graph exists: ${state.graphExists ? "yes" : "no"}
1124
-
1125
- Suggested setup flow:
1126
-
1127
- 1. If GitNexus is not installed, install it using the method recommended by the GitNexus project.
1128
- 2. If you want GitNexus MCP or IDE integration, run \`gitnexus setup\` yourself after reviewing what it changes.
1129
- 3. Return to this project and run \`fet graph init\` to build the first graph.
1130
- 4. Run \`fet graph handoff\` so IDE AI can prefer graph context before broad repository scans.
1131
-
1132
- Guardrails:
1133
1257
 
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
- `;
1258
+ // src/commands/init.ts
1259
+ async function initCommand(ctx) {
1260
+ const alreadyInitialized = await exists2(join11(ctx.projectRoot, "openspec", "config.yaml"));
1261
+ let warnings = [];
1262
+ await withProjectLock(ctx.projectRoot, { command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
1263
+ const journal = createInitJournal(ctx.fetVersion);
1264
+ await writeInitJournal(ctx.projectRoot, journal);
1265
+ const identity = await ctx.openSpec.resolveExecutable();
1266
+ if (!alreadyInitialized) {
1267
+ const result = await ctx.openSpec.run("init", ["--tools", "none"], { cwd: ctx.projectRoot, stdio: "inherit" });
1268
+ if (result.exitCode !== 0) {
1269
+ process.exitCode = result.exitCode;
1270
+ return;
1271
+ }
1272
+ }
1273
+ const contextResult = await updateContextFiles(ctx);
1274
+ warnings = contextResult.warnings;
1275
+ await ensureGitignore(ctx);
1276
+ const state = await ctx.stateStore.getOrCreateGlobal();
1277
+ state.openspec = identity;
1278
+ state.language = {
1279
+ current: ctx.language,
1280
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1281
+ };
1282
+ state.graph ??= {};
1283
+ const gitnexus = mergeGitNexusGraphInfo(toGitNexusState(await detectGitNexus(), state.graph.gitnexus), await inspectGitNexusGraph(ctx.projectRoot));
1284
+ if (!gitnexus.installed && !gitnexus.recommendationShownAt) {
1285
+ warnings.push(renderGitNexusRecommendation(gitnexus, ctx.language));
1286
+ gitnexus.recommendationShownAt = (/* @__PURE__ */ new Date()).toISOString();
1287
+ }
1288
+ state.graph.gitnexus = gitnexus;
1289
+ for (const adapter of ctx.toolAdapters) {
1290
+ const plan = await adapter.planInstall(ctx.projectRoot, ctx.language);
1291
+ const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
1292
+ state.toolAdapters[adapter.tool] = {
1293
+ adapterVersion: adapter.adapterVersion,
1294
+ installed: true,
1295
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1296
+ };
1297
+ journal.steps.push(...result.written.map((path) => ({ operation: "write", path, status: "done" })));
1298
+ }
1299
+ journal.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1300
+ await writeInitJournal(ctx.projectRoot, journal);
1301
+ await ctx.stateStore.writeGlobal(state);
1302
+ });
1303
+ ctx.output.result({
1304
+ ok: true,
1305
+ command: "init",
1306
+ summary: ctx.language === "en" ? "FET initialization completed." : "FET \u521D\u59CB\u5316\u5B8C\u6210\u3002",
1307
+ warnings,
1308
+ 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"]
1309
+ });
1138
1310
  }
1139
- function renderGraphUsageHandoff(state) {
1140
- return `<!-- FET:MANAGED
1141
- schemaVersion: 1
1142
- generator: graph-handoff
1143
- FET:END -->
1144
-
1145
- # FET Graph Handoff
1146
-
1147
- Use GitNexus graph context as an optional first pass before broad repository scans.
1148
-
1149
- Current status:
1150
-
1151
- - Installed: ${state.installed ? "yes" : "no"}
1152
- - Graph path: ${state.graphPath ?? ".gitnexus"}
1153
- - Graph exists: ${state.graphExists ? "yes" : "no"}
1154
- - Last indexed at: ${state.lastIndexedAt ?? "unknown"}
1155
- - Last status: ${state.lastStatus ?? "unknown"}
1156
-
1157
- When graph context is available:
1158
-
1159
- 1. Use the graph to identify likely modules, dependencies, and insertion points.
1160
- 2. Read only the concrete source files needed to confirm behavior.
1161
- 3. Prefer OpenSpec artifacts and AGENTS.md over graph guesses when they conflict.
1162
- 4. Fall back to normal repository inspection if the graph is missing, stale, or incomplete.
1163
-
1164
- When producing OpenSpec artifacts:
1165
-
1166
- - Use graph context to make proposal, design, specs, and tasks more precise.
1167
- - Avoid large repository scans when the graph already narrows the relevant area.
1168
- - Keep all generated artifacts in the normal OpenSpec change directory.
1169
- `;
1311
+ async function ensureGitignore(ctx) {
1312
+ const gitignorePath = join11(ctx.projectRoot, ".gitignore");
1313
+ const existing = await readOptional2(gitignorePath);
1314
+ await atomicWrite(gitignorePath, mergeGitignore(existing));
1170
1315
  }
1171
- function firstLine(value) {
1172
- return value.trim().split(/\r?\n/)[0]?.trim() || null;
1316
+ async function readOptional2(path) {
1317
+ try {
1318
+ return await readFile7(path, "utf8");
1319
+ } catch {
1320
+ return null;
1321
+ }
1322
+ }
1323
+ async function exists2(path) {
1324
+ try {
1325
+ await stat4(path);
1326
+ return true;
1327
+ } catch {
1328
+ return false;
1329
+ }
1173
1330
  }
1174
1331
 
1175
1332
  // src/commands/proxy.ts
@@ -1206,6 +1363,22 @@ async function git(cwd, args) {
1206
1363
  import { mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
1207
1364
  import { join as join12 } from "path";
1208
1365
 
1366
+ // src/language.ts
1367
+ var DEFAULT_LANGUAGE = "zh-CN";
1368
+ function normalizeLanguage(value) {
1369
+ const normalized = value?.trim().toLowerCase();
1370
+ if (!normalized) {
1371
+ return DEFAULT_LANGUAGE;
1372
+ }
1373
+ if (["en", "en-us", "english"].includes(normalized)) {
1374
+ return "en";
1375
+ }
1376
+ return DEFAULT_LANGUAGE;
1377
+ }
1378
+ function languageInstruction(language) {
1379
+ 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";
1380
+ }
1381
+
1209
1382
  // src/state/schema.ts
1210
1383
  var phases = ["explore", "propose", "implement", "verify", "sync", "archive"];
1211
1384
  function createGlobalState(fetVersion, project) {
@@ -1216,6 +1389,10 @@ function createGlobalState(fetVersion, project) {
1216
1389
  createdAt: now,
1217
1390
  updatedAt: now,
1218
1391
  project,
1392
+ language: {
1393
+ current: DEFAULT_LANGUAGE,
1394
+ updatedAt: now
1395
+ },
1219
1396
  openspec: null,
1220
1397
  activeChangeId: null,
1221
1398
  openChangeIds: [],
@@ -1239,9 +1416,7 @@ function createChangeState(fetVersion, changeId, phase) {
1239
1416
  createdAt: now,
1240
1417
  updatedAt: now,
1241
1418
  currentPhase: phase,
1242
- phases: Object.fromEntries(
1243
- phases.map((item) => [item, { status: item === phase ? "in_progress" : "not_started" }])
1244
- ),
1419
+ phases: Object.fromEntries(phases.map((item) => [item, { status: item === phase ? "in_progress" : "not_started" }])),
1245
1420
  tasks: {
1246
1421
  source: "tasks.md",
1247
1422
  completedIds: [],
@@ -1256,6 +1431,12 @@ function assertGlobalState(value) {
1256
1431
  if (!isRecord(value) || value.schemaVersion !== 1) {
1257
1432
  throw unsupportedSchema("\u5168\u5C40\u72B6\u6001 schema \u4E0D\u53D7\u652F\u6301");
1258
1433
  }
1434
+ if (!isRecord(value.language)) {
1435
+ value.language = {
1436
+ current: DEFAULT_LANGUAGE,
1437
+ updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
1438
+ };
1439
+ }
1259
1440
  if (typeof value.fetVersion !== "string" || !isRecord(value.project)) {
1260
1441
  throw corruptedState("\u5168\u5C40\u72B6\u6001\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5");
1261
1442
  }
@@ -1537,14 +1718,16 @@ function stripFetOptions(args) {
1537
1718
  async function mapOpenSpecCommand(ctx, command, args) {
1538
1719
  switch (command) {
1539
1720
  case "propose":
1540
- case "continue":
1541
- case "ff":
1542
- case "apply":
1543
- case "sync":
1544
1721
  case "bulk-archive":
1545
1722
  case "explore":
1546
1723
  case "onboard":
1547
1724
  return { command, args: withGlobalChange(ctx, args) };
1725
+ case "continue":
1726
+ case "ff":
1727
+ return { command, args: await withDefaultChange(ctx, args, true) };
1728
+ case "apply":
1729
+ case "sync":
1730
+ return { command, args: await withDefaultChange(ctx, args) };
1548
1731
  case "new":
1549
1732
  return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
1550
1733
  case "archive":
@@ -1568,6 +1751,18 @@ async function mapOpenSpecCommand(ctx, command, args) {
1568
1751
  function withGlobalChange(ctx, args) {
1569
1752
  return ctx.changeId ? ["--change", ctx.changeId, ...args] : args;
1570
1753
  }
1754
+ async function withDefaultChange(ctx, args, allowWithArgs = false) {
1755
+ if (ctx.changeId) {
1756
+ return ["--change", ctx.changeId, ...args];
1757
+ }
1758
+ if (args.includes("--change") || args.some((arg) => arg.startsWith("--change="))) {
1759
+ return args;
1760
+ }
1761
+ if (args.length > 0 && !allowWithArgs) {
1762
+ return args;
1763
+ }
1764
+ return ["--change", await requireChangeId(ctx), ...args];
1765
+ }
1571
1766
  async function requireChangeId(ctx) {
1572
1767
  if (ctx.changeId) {
1573
1768
  return ctx.changeId;
@@ -1813,8 +2008,18 @@ function detectCurrentModel(env = process.env) {
1813
2008
  function isHighCostModel(model) {
1814
2009
  return HIGH_COST_MODEL_PATTERNS.some((pattern) => pattern.test(model));
1815
2010
  }
2011
+ function getModelPolicyMode(env = process.env) {
2012
+ const value = env.FET_MODEL_POLICY?.trim().toLowerCase();
2013
+ if (value === "off" || env.FET_SKIP_MODEL_POLICY === "1") {
2014
+ return "off";
2015
+ }
2016
+ if (value === "confirm") {
2017
+ return "confirm";
2018
+ }
2019
+ return "warn";
2020
+ }
1816
2021
  function getCommandModelPolicyMismatch(command, env = process.env) {
1817
- if (env.FET_MODEL_POLICY === "off" || env.FET_SKIP_MODEL_POLICY === "1") {
2022
+ if (getModelPolicyMode(env) === "off") {
1818
2023
  return null;
1819
2024
  }
1820
2025
  const detected = detectCurrentModel(env);
@@ -1843,15 +2048,26 @@ function getCommandModelPolicyMismatch(command, env = process.env) {
1843
2048
  }
1844
2049
  return null;
1845
2050
  }
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}`;
2051
+ function formatModelPolicyMismatch(mismatch, language = "zh-CN") {
2052
+ if (language === "en") {
2053
+ 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.";
2054
+ return `${mismatch.reason} Detected ${mismatch.detected.source}="${mismatch.detected.name}". ${switchHint2}`;
2055
+ }
2056
+ 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";
2057
+ 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`;
2058
+ return `${reason} \u68C0\u6D4B\u5230 ${mismatch.detected.source}="${mismatch.detected.name}"\u3002${switchHint}`;
1849
2059
  }
1850
- function renderIdeModelPolicy(command) {
2060
+ function renderIdeModelPolicy(command, language = "zh-CN") {
2061
+ if (language === "en") {
2062
+ if (command === "apply") {
2063
+ 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. Treat this as advisory: do not interrupt the workflow solely to ask about switching models. Run the FET command unless the user explicitly asks to pause for a model switch, and report any FET model warning in the summary.";
2064
+ }
2065
+ 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, treat it as an advisory warning: do not interrupt the workflow solely to ask about switching models. Run the FET command unless the user explicitly asks to pause for a model switch, and report any FET model warning in the summary.";
2066
+ }
1851
2067
  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.";
2068
+ 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\u6B64\u63D0\u793A\u4EC5\u4F5C\u4E3A\u5EFA\u8BAE\uFF0C\u4E0D\u8981\u4EC5\u56E0\u6A21\u578B\u7B56\u7565\u800C\u6253\u65AD\u6D41\u7A0B\u8BE2\u95EE\u662F\u5426\u5207\u6362\u6A21\u578B\uFF1B\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u5148\u5207\u6362\u6A21\u578B\uFF0C\u5426\u5219\u7EE7\u7EED\u8FD0\u884C FET \u547D\u4EE4\uFF0C\u5E76\u5728\u603B\u7ED3\u4E2D\u8BF4\u660E FET \u8F93\u51FA\u7684\u6A21\u578B\u63D0\u9192\u3002";
1853
2069
  }
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.";
2070
+ 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\u6B64\u63D0\u793A\u4EC5\u4F5C\u4E3A\u5EFA\u8BAE\uFF1B\u4E0D\u8981\u4EC5\u56E0\u6A21\u578B\u7B56\u7565\u800C\u6253\u65AD\u6D41\u7A0B\u8BE2\u95EE\u662F\u5426\u5207\u6362\u6A21\u578B\u3002\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u5148\u5207\u6362\u6A21\u578B\uFF0C\u5426\u5219\u7EE7\u7EED\u8FD0\u884C FET \u547D\u4EE4\uFF0C\u5E76\u5728\u603B\u7ED3\u4E2D\u8BF4\u660E FET \u8F93\u51FA\u7684\u6A21\u578B\u63D0\u9192\u3002";
1855
2071
  }
1856
2072
 
1857
2073
  // src/cli/context.ts
@@ -1890,7 +2106,7 @@ function renderFetAdapterUsage(command, args = "[...args]") {
1890
2106
  }
1891
2107
 
1892
2108
  // src/adapters/codex/templates.ts
1893
- function codexGuideFile() {
2109
+ function codexGuideFile(language = DEFAULT_LANGUAGE) {
1894
2110
  return {
1895
2111
  path: ".codex/fet/context.md",
1896
2112
  content: `<!-- FET:MANAGED
@@ -1902,6 +2118,10 @@ FET:END -->
1902
2118
 
1903
2119
  # FET For Codex
1904
2120
 
2121
+ ## \u8BED\u8A00
2122
+
2123
+ ${languageInstruction(language)}
2124
+
1905
2125
  Before doing FET or OpenSpec work in Codex, read:
1906
2126
 
1907
2127
  - AGENTS.md
@@ -1917,22 +2137,22 @@ Command guides live in .codex/fet/commands/.
1917
2137
  `
1918
2138
  };
1919
2139
  }
1920
- function codexCommandFiles() {
2140
+ function codexCommandFiles(language = DEFAULT_LANGUAGE) {
1921
2141
  return [
1922
- codexKarpathyGuidelinesFile(),
2142
+ codexKarpathyGuidelinesFile(language),
1923
2143
  ...FET_ADAPTER_COMMANDS.map((command) => ({
1924
2144
  path: `.codex/fet/commands/${command}.md`,
1925
- content: renderCommand(command)
2145
+ content: renderCommand(command, language)
1926
2146
  }))
1927
2147
  ];
1928
2148
  }
1929
- function codexSlashPromptFiles() {
2149
+ function codexSlashPromptFiles(language = DEFAULT_LANGUAGE) {
1930
2150
  return FET_ADAPTER_COMMANDS.map((command) => ({
1931
2151
  path: `prompts/fet-${command}.md`,
1932
- content: renderSlashPrompt(command)
2152
+ content: renderSlashPrompt(command, language)
1933
2153
  }));
1934
2154
  }
1935
- function codexKarpathyGuidelinesFile() {
2155
+ function codexKarpathyGuidelinesFile(language) {
1936
2156
  return {
1937
2157
  path: ".codex/fet/karpathy-guidelines.md",
1938
2158
  content: `<!-- FET:MANAGED
@@ -1944,19 +2164,22 @@ FET:END -->
1944
2164
 
1945
2165
  # Andrej Karpathy Inspired Coding Guidelines
1946
2166
 
1947
- ${renderKarpathyGuidelinesBody()}
2167
+ ${renderKarpathyGuidelinesBody(language)}
1948
2168
  `
1949
2169
  };
1950
2170
  }
1951
- function renderCommand(command) {
2171
+ function renderCommand(command, language) {
2172
+ if (language !== "en") {
2173
+ return renderCommandZh(command);
2174
+ }
1952
2175
  if (command === "fill-context") {
1953
- return renderFillContextCommand();
2176
+ return renderFillContextCommand(language);
1954
2177
  }
1955
2178
  if (command === "passthrough") {
1956
- return renderPassthroughCommand();
2179
+ return renderPassthroughCommand(language);
1957
2180
  }
1958
2181
  if (command.startsWith("graph-")) {
1959
- return renderGraphCommand(command);
2182
+ return renderGraphCommand(command, language);
1960
2183
  }
1961
2184
  const usage = renderFetAdapterUsage(command, "");
1962
2185
  return `<!-- FET:MANAGED
@@ -1969,7 +2192,9 @@ FET:END -->
1969
2192
 
1970
2193
  # ${usage}
1971
2194
 
1972
- ${renderIdeModelPolicy(command)}
2195
+ ${renderIdeModelPolicy(command, language)}
2196
+
2197
+ ${languageInstruction(language)}
1973
2198
 
1974
2199
  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
2200
 
@@ -1988,7 +2213,41 @@ If the command needs a change id, pass it with \`--change <change-id>\` or use t
1988
2213
  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
2214
  `;
1990
2215
  }
1991
- function renderPassthroughCommand() {
2216
+ function renderCommandZh(command) {
2217
+ const usage = renderFetAdapterUsage(command, command === "fill-context" ? "" : command === "passthrough" ? "<openspec-command> [...args]" : "");
2218
+ const title = commandTitleZh(command);
2219
+ return `<!-- FET:MANAGED
2220
+ schemaVersion: 1
2221
+ fetVersion: ${FET_VERSION}
2222
+ generator: codex-adapter
2223
+ adapterVersion: 1
2224
+ command: ${usage}
2225
+ FET:END -->
2226
+
2227
+ # ${usage}
2228
+
2229
+ ${renderIdeModelPolicy(command, "zh-CN")}
2230
+
2231
+ ${languageInstruction("zh-CN")}
2232
+
2233
+ ## \u7528\u9014
2234
+
2235
+ ${title}
2236
+
2237
+ ## \u6267\u884C\u65B9\u5F0F
2238
+
2239
+ \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
2240
+
2241
+ \`\`\`sh
2242
+ ${usage}
2243
+ \`\`\`
2244
+
2245
+ \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
2246
+
2247
+ \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
2248
+ `;
2249
+ }
2250
+ function renderPassthroughCommand(language) {
1992
2251
  return `<!-- FET:MANAGED
1993
2252
  schemaVersion: 1
1994
2253
  fetVersion: ${FET_VERSION}
@@ -1999,7 +2258,9 @@ FET:END -->
1999
2258
 
2000
2259
  # fet passthrough
2001
2260
 
2002
- ${renderIdeModelPolicy("passthrough")}
2261
+ ${renderIdeModelPolicy("passthrough", language)}
2262
+
2263
+ ${languageInstruction(language)}
2003
2264
 
2004
2265
  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
2266
 
@@ -2016,7 +2277,7 @@ fet passthrough <openspec-command> [...args]
2016
2277
  This preserves the FET entry point while allowing access to unmanaged or newly added OpenSpec commands. Passthrough does not update FET lifecycle state.
2017
2278
  `;
2018
2279
  }
2019
- function renderGraphCommand(command) {
2280
+ function renderGraphCommand(command, language) {
2020
2281
  const usage = renderFetAdapterUsage(command, "");
2021
2282
  const subcommand = command.slice("graph-".length);
2022
2283
  return `<!-- FET:MANAGED
@@ -2029,7 +2290,9 @@ FET:END -->
2029
2290
 
2030
2291
  # ${usage}
2031
2292
 
2032
- ${renderIdeModelPolicy(command)}
2293
+ ${renderIdeModelPolicy(command, language)}
2294
+
2295
+ ${languageInstruction(language)}
2033
2296
 
2034
2297
  When the user asks Codex to work with optional GitNexus graph support, use FET as the entry point.
2035
2298
 
@@ -2048,42 +2311,45 @@ For graph init or refresh, pass extra GitNexus analyze arguments only when the u
2048
2311
  After the command completes, report the GitNexus state, generated handoff files, and next steps.
2049
2312
  `;
2050
2313
  }
2051
- function renderSlashPrompt(command) {
2314
+ function renderSlashPrompt(command, language) {
2315
+ if (language !== "en") {
2316
+ return renderSlashPromptZh(command);
2317
+ }
2052
2318
  if (command === "continue") {
2053
- return renderContinueSlashPrompt();
2319
+ return renderContinueSlashPrompt(language);
2054
2320
  }
2055
2321
  if (command === "ff" || command === "propose") {
2056
- return renderFastForwardSlashPrompt(command);
2322
+ return renderFastForwardSlashPrompt(command, language);
2057
2323
  }
2058
2324
  if (command === "explore") {
2059
- return renderExploreSlashPrompt();
2325
+ return renderExploreSlashPrompt(language);
2060
2326
  }
2061
2327
  if (command === "new") {
2062
- return renderNewSlashPrompt();
2328
+ return renderNewSlashPrompt(language);
2063
2329
  }
2064
2330
  if (command === "apply") {
2065
- return renderApplySlashPrompt();
2331
+ return renderApplySlashPrompt(language);
2066
2332
  }
2067
2333
  if (command === "verify") {
2068
- return renderVerifySlashPrompt();
2334
+ return renderVerifySlashPrompt(language);
2069
2335
  }
2070
2336
  if (command === "sync") {
2071
- return renderSyncSlashPrompt();
2337
+ return renderSyncSlashPrompt(language);
2072
2338
  }
2073
2339
  if (command === "archive") {
2074
- return renderArchiveSlashPrompt();
2340
+ return renderArchiveSlashPrompt(language);
2075
2341
  }
2076
2342
  if (command === "bulk-archive") {
2077
- return renderBulkArchiveSlashPrompt();
2343
+ return renderBulkArchiveSlashPrompt(language);
2078
2344
  }
2079
2345
  if (command === "onboard") {
2080
- return renderOnboardSlashPrompt();
2346
+ return renderOnboardSlashPrompt(language);
2081
2347
  }
2082
2348
  if (command === "fill-context") {
2083
- return renderFillContextSlashPrompt();
2349
+ return renderFillContextSlashPrompt(language);
2084
2350
  }
2085
2351
  if (command === "passthrough") {
2086
- return renderPassthroughSlashPrompt();
2352
+ return renderPassthroughSlashPrompt(language);
2087
2353
  }
2088
2354
  const usage = renderFetAdapterUsage(command);
2089
2355
  const isGraph = command.startsWith("graph-");
@@ -2099,7 +2365,6 @@ FET:END -->
2099
2365
 
2100
2366
  ---
2101
2367
  description: ${description}
2102
- argument-hint: command arguments
2103
2368
  ---
2104
2369
 
2105
2370
  Use FET as the entry point for this OpenSpec workflow.
@@ -2119,7 +2384,89 @@ ${shellCommand}
2119
2384
  After it completes, summarize the important FET output and next steps.
2120
2385
  `;
2121
2386
  }
2122
- function renderFillContextCommand() {
2387
+ function renderSlashPromptZh(command) {
2388
+ const usage = renderFetAdapterUsage(command, command === "fill-context" ? "" : command === "passthrough" ? "<openspec-command> [...args]" : "[...args]");
2389
+ const argumentHint = command === "passthrough" ? "openspec-command [...args]" : void 0;
2390
+ const argumentHintLine = argumentHint ? `argument-hint: ${argumentHint}
2391
+ ` : "";
2392
+ return `<!-- FET:MANAGED
2393
+ schemaVersion: 1
2394
+ fetVersion: ${FET_VERSION}
2395
+ generator: codex-adapter
2396
+ adapterVersion: 1
2397
+ command: ${usage}
2398
+ FET:END -->
2399
+
2400
+ ---
2401
+ description: ${commandTitleZh(command)}
2402
+ ${argumentHintLine}---
2403
+
2404
+ ${renderIdeModelPolicy(command, "zh-CN")}
2405
+
2406
+ ${languageInstruction("zh-CN")}
2407
+
2408
+ ## \u76EE\u6807
2409
+
2410
+ ${commandGoalZh(command)}
2411
+
2412
+ ## \u6B65\u9AA4
2413
+
2414
+ 1. \u9605\u8BFB AGENTS.md\u3001openspec/config.yaml \u548C .codex/fet/karpathy-guidelines.md\u3002
2415
+ 2. \u5982\u679C\u5B58\u5728\u5F53\u524D OpenSpec change\uFF0C\u9605\u8BFB openspec/changes/<change-id>/ \u4E0B\u7684\u76F8\u5173\u4EA7\u7269\u3002
2416
+ 3. \u8FD0\u884C FET \u547D\u4EE4\uFF1A
2417
+ \`\`\`sh
2418
+ ${usage}
2419
+ \`\`\`
2420
+ 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
2421
+ 5. \u7528\u4E2D\u6587\u603B\u7ED3\u672C\u6B21\u52A8\u4F5C\u3001\u6587\u4EF6\u8DEF\u5F84\u3001\u72B6\u6001\u548C\u4E0B\u4E00\u6B65\u3002
2422
+
2423
+ ## \u7EA6\u675F
2424
+
2425
+ - \u9ED8\u8BA4\u4F7F\u7528\u4E2D\u6587\u4EA7\u51FA\u3002
2426
+ - \u4E0D\u8981\u7ED5\u8FC7 FET \u76F4\u63A5\u8C03\u7528 openspec\uFF0C\u9664\u975E FET \u547D\u4EE4\u672C\u8EAB\u4E0D\u53EF\u7528\u3002
2427
+ - change \u4E0D\u660E\u786E\u65F6\u5148\u8BE2\u95EE\u7528\u6237\u3002
2428
+ ${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" : ""}`;
2429
+ }
2430
+ function commandTitleZh(command) {
2431
+ const titles = {
2432
+ explore: "\u63A2\u7D22 FET/OpenSpec \u9700\u6C42",
2433
+ propose: "\u521B\u5EFA\u5E76\u8865\u9F50 FET/OpenSpec \u63D0\u6848\u4EA7\u7269",
2434
+ new: "\u521B\u5EFA\u65B0\u7684 FET/OpenSpec change \u9AA8\u67B6",
2435
+ continue: "\u63A8\u8FDB\u5F53\u524D FET/OpenSpec change \u7684\u4E0B\u4E00\u4E2A\u4EA7\u7269",
2436
+ ff: "\u5FEB\u901F\u751F\u6210 FET/OpenSpec \u6240\u9700\u4EA7\u7269",
2437
+ apply: "\u5B9E\u65BD FET/OpenSpec change \u4E2D\u7684\u4EFB\u52A1",
2438
+ verify: "\u9A8C\u8BC1 FET/OpenSpec change",
2439
+ sync: "\u540C\u6B65 delta specs \u5230\u4E3B\u89C4\u8303",
2440
+ archive: "\u5F52\u6863\u5DF2\u9A8C\u8BC1\u7684 FET/OpenSpec change",
2441
+ "bulk-archive": "\u6279\u91CF\u5F52\u6863 FET/OpenSpec changes",
2442
+ onboard: "\u52A0\u8F7D FET/OpenSpec \u9879\u76EE\u4E0A\u4E0B\u6587",
2443
+ "fill-context": "\u586B\u5145 FET AGENTS.md \u5360\u4F4D\u7B26",
2444
+ passthrough: "\u901A\u8FC7 FET \u900F\u4F20\u672A\u63A5\u7BA1\u7684 OpenSpec \u547D\u4EE4",
2445
+ "graph-status": "\u67E5\u770B GitNexus \u4EE3\u7801\u56FE\u72B6\u6001",
2446
+ "graph-setup": "\u751F\u6210 GitNexus \u5B89\u88C5\u4EA4\u63A5\u8BF4\u660E",
2447
+ "graph-init": "\u521D\u59CB\u5316 GitNexus \u4EE3\u7801\u56FE",
2448
+ "graph-refresh": "\u5237\u65B0 GitNexus \u4EE3\u7801\u56FE",
2449
+ "graph-doctor": "\u8BCA\u65AD GitNexus \u4EE3\u7801\u56FE",
2450
+ "graph-handoff": "\u751F\u6210 GitNexus \u4EE3\u7801\u56FE\u4F7F\u7528\u8BF4\u660E"
2451
+ };
2452
+ return titles[command] ?? `\u8FD0\u884C FET ${command} \u5DE5\u4F5C\u6D41`;
2453
+ }
2454
+ function commandGoalZh(command) {
2455
+ if (command === "fill-context") {
2456
+ 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";
2457
+ }
2458
+ if (command === "continue") {
2459
+ 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";
2460
+ }
2461
+ if (command === "apply") {
2462
+ return "\u8BFB\u53D6 OpenSpec \u4EA7\u7269\u5E76\u6309 tasks.md \u5B9E\u65BD\u4EE3\u7801\u53D8\u66F4\u3002";
2463
+ }
2464
+ if (command.startsWith("graph-")) {
2465
+ 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";
2466
+ }
2467
+ return commandTitleZh(command);
2468
+ }
2469
+ function renderFillContextCommand(language) {
2123
2470
  return `<!-- FET:MANAGED
2124
2471
  schemaVersion: 1
2125
2472
  fetVersion: ${FET_VERSION}
@@ -2130,7 +2477,9 @@ FET:END -->
2130
2477
 
2131
2478
  # fet fill-context
2132
2479
 
2133
- ${renderIdeModelPolicy("fill-context")}
2480
+ ${renderIdeModelPolicy("fill-context", language)}
2481
+
2482
+ ${languageInstruction(language)}
2134
2483
 
2135
2484
  Use this command to complete FET-generated project context placeholders with Codex.
2136
2485
 
@@ -2147,7 +2496,7 @@ fet fill-context
2147
2496
  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
2497
  `;
2149
2498
  }
2150
- function renderFillContextSlashPrompt() {
2499
+ function renderFillContextSlashPrompt(language) {
2151
2500
  return renderManagedSlashPrompt(
2152
2501
  "fet fill-context",
2153
2502
  "Fill FET AGENTS.md placeholders with Codex",
@@ -2179,10 +2528,12 @@ Steps:
2179
2528
  Guardrails:
2180
2529
  - Do not invent facts that cannot be inferred from the repo.
2181
2530
  - Use [UNKNOWN] only when the repository does not contain enough evidence.
2182
- - Keep generated context stable and useful for future AI coding sessions.`
2531
+ - Keep generated context stable and useful for future AI coding sessions.`,
2532
+ void 0,
2533
+ language
2183
2534
  );
2184
2535
  }
2185
- function renderNewSlashPrompt() {
2536
+ function renderNewSlashPrompt(language) {
2186
2537
  return renderManagedSlashPrompt(
2187
2538
  "fet new [...args]",
2188
2539
  "Create a new FET/OpenSpec change scaffold",
@@ -2211,20 +2562,22 @@ Steps:
2211
2562
  Guardrails:
2212
2563
  - Do not create artifact files in /prompts:fet-new.
2213
2564
  - 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.`
2565
+ - Show the change location and the next command to create the first artifact.`,
2566
+ void 0,
2567
+ language
2215
2568
  );
2216
2569
  }
2217
- function renderApplySlashPrompt() {
2570
+ function renderApplySlashPrompt(language) {
2218
2571
  return renderManagedSlashPrompt(
2219
2572
  "fet apply [...args]",
2220
2573
  "Implement tasks from a FET/OpenSpec change",
2221
2574
  `Implement a FET-managed OpenSpec change.
2222
2575
 
2223
- Input after the slash command should identify the change, for example a change id or --change <change-id>.
2576
+ 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
2577
 
2225
2578
  Steps:
2226
2579
 
2227
- 1. Resolve the change id. If ambiguous, ask the user.
2580
+ 1. Resolve the change id from the input, active FET/OpenSpec state, or the only open change. If ambiguous, ask the user.
2228
2581
  2. Run the native OpenSpec apply flow through FET:
2229
2582
  \`\`\`sh
2230
2583
  fet apply --change <change-id> --json
@@ -2241,20 +2594,22 @@ Steps:
2241
2594
  Guardrails:
2242
2595
  - Never skip reading OpenSpec artifacts before implementation.
2243
2596
  - Do not mark a task complete until the code change is actually done.
2244
- - Do not run sync or archive from apply.`
2597
+ - Do not run sync or archive from apply.`,
2598
+ void 0,
2599
+ language
2245
2600
  );
2246
2601
  }
2247
- function renderVerifySlashPrompt() {
2602
+ function renderVerifySlashPrompt(language) {
2248
2603
  return renderManagedSlashPrompt(
2249
2604
  "fet verify [...args]",
2250
2605
  "Verify a FET/OpenSpec change before sync or archive",
2251
2606
  `Verify a FET-managed OpenSpec change.
2252
2607
 
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.
2608
+ 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
2609
 
2255
2610
  Steps:
2256
2611
 
2257
- 1. Resolve the change id. If ambiguous, ask the user.
2612
+ 1. Resolve the change id from the input, active FET/OpenSpec state, or the only open change. If ambiguous, ask the user.
2258
2613
  2. Generate FET verification instructions:
2259
2614
  \`\`\`sh
2260
2615
  fet verify --change <change-id>
@@ -2274,20 +2629,22 @@ Steps:
2274
2629
  Guardrails:
2275
2630
  - Do not run --done before producing a verification assessment.
2276
2631
  - 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.`
2632
+ - Suggest /prompts:fet-sync <change-id> and /prompts:fet-archive <change-id> only after verification is done.`,
2633
+ void 0,
2634
+ language
2278
2635
  );
2279
2636
  }
2280
- function renderSyncSlashPrompt() {
2637
+ function renderSyncSlashPrompt(language) {
2281
2638
  return renderManagedSlashPrompt(
2282
2639
  "fet sync [...args]",
2283
2640
  "Sync delta specs and validate a FET/OpenSpec change",
2284
2641
  `Sync a FET-managed OpenSpec change.
2285
2642
 
2286
- Input after the slash command should identify the change.
2643
+ Input after the slash command may identify the change. If omitted, use the active OpenSpec change when it is unambiguous.
2287
2644
 
2288
2645
  Steps:
2289
2646
 
2290
- 1. Resolve the change id. If ambiguous, ask the user.
2647
+ 1. Resolve the change id from the input, active FET/OpenSpec state, or the only open change. If ambiguous, ask the user.
2291
2648
  2. Confirm FET verification is complete or run /prompts:fet-verify <change-id> first.
2292
2649
  3. Find delta specs under openspec/changes/<change-id>/specs/*/spec.md.
2293
2650
  4. If delta specs exist, intelligently merge them into openspec/specs/<capability>/spec.md:
@@ -2306,20 +2663,22 @@ Steps:
2306
2663
  Guardrails:
2307
2664
  - Read both delta and main specs before editing.
2308
2665
  - Make sync idempotent where possible.
2309
- - If FET reports verify is not done, stop and run/ask for verification instead of bypassing the gate.`
2666
+ - If FET reports verify is not done, stop and run/ask for verification instead of bypassing the gate.`,
2667
+ void 0,
2668
+ language
2310
2669
  );
2311
2670
  }
2312
- function renderArchiveSlashPrompt() {
2671
+ function renderArchiveSlashPrompt(language) {
2313
2672
  return renderManagedSlashPrompt(
2314
2673
  "fet archive [...args]",
2315
2674
  "Archive a verified FET/OpenSpec change",
2316
2675
  `Archive a FET-managed OpenSpec change.
2317
2676
 
2318
- Input after the slash command should identify the change.
2677
+ Input after the slash command may identify the change. If omitted, use the active OpenSpec change when it is unambiguous.
2319
2678
 
2320
2679
  Steps:
2321
2680
 
2322
- 1. Resolve the change id. If ambiguous, ask the user.
2681
+ 1. Resolve the change id from the input, active FET/OpenSpec state, or the only open change. If ambiguous, ask the user.
2323
2682
  2. Check artifact and task status:
2324
2683
  \`\`\`sh
2325
2684
  fet passthrough status --change <change-id> --json
@@ -2336,10 +2695,12 @@ Steps:
2336
2695
  Guardrails:
2337
2696
  - Do not move change directories manually; use fet archive.
2338
2697
  - Do not bypass the FET verify gate.
2339
- - Ask before archiving with incomplete tasks or unsynced delta specs.`
2698
+ - Ask before archiving with incomplete tasks or unsynced delta specs.`,
2699
+ void 0,
2700
+ language
2340
2701
  );
2341
2702
  }
2342
- function renderBulkArchiveSlashPrompt() {
2703
+ function renderBulkArchiveSlashPrompt(language) {
2343
2704
  return renderManagedSlashPrompt(
2344
2705
  "fet bulk-archive [...args]",
2345
2706
  "Archive multiple FET/OpenSpec changes safely",
@@ -2363,10 +2724,12 @@ Steps:
2363
2724
  Guardrails:
2364
2725
  - Never archive all changes without explicit user selection.
2365
2726
  - Do not bypass verify or warnings for individual changes.
2366
- - Continue with remaining selected changes if one archive fails, then report the failure clearly.`
2727
+ - Continue with remaining selected changes if one archive fails, then report the failure clearly.`,
2728
+ void 0,
2729
+ language
2367
2730
  );
2368
2731
  }
2369
- function renderOnboardSlashPrompt() {
2732
+ function renderOnboardSlashPrompt(language) {
2370
2733
  return renderManagedSlashPrompt(
2371
2734
  "fet onboard [...args]",
2372
2735
  "Load FET/OpenSpec onboarding context",
@@ -2377,7 +2740,7 @@ Steps:
2377
2740
  1. Read AGENTS.md and openspec/config.yaml.
2378
2741
  2. Run FET onboarding:
2379
2742
  \`\`\`sh
2380
- fet onboard $ARGUMENTS
2743
+ fet onboard
2381
2744
  \`\`\`
2382
2745
  3. Summarize:
2383
2746
  - Project context.
@@ -2387,10 +2750,12 @@ Steps:
2387
2750
 
2388
2751
  Guardrails:
2389
2752
  - Do not create or modify artifacts during onboard.
2390
- - Use this command to orient the session, then suggest the next concrete FET command.`
2753
+ - Use this command to orient the session, then suggest the next concrete FET command.`,
2754
+ void 0,
2755
+ language
2391
2756
  );
2392
2757
  }
2393
- function renderPassthroughSlashPrompt() {
2758
+ function renderPassthroughSlashPrompt(language) {
2394
2759
  return renderManagedSlashPrompt(
2395
2760
  "fet passthrough <openspec-command> [...args]",
2396
2761
  "Run an unmanaged OpenSpec command through FET",
@@ -2408,10 +2773,12 @@ Steps:
2408
2773
  Guardrails:
2409
2774
  - Do not call openspec directly unless FET passthrough itself is unavailable.
2410
2775
  - Remember that passthrough does not update FET lifecycle state.
2411
- - For managed workflows, prefer the specific FET prompt instead of passthrough.`
2776
+ - For managed workflows, prefer the specific FET prompt instead of passthrough.`,
2777
+ "openspec-command [...args]",
2778
+ language
2412
2779
  );
2413
2780
  }
2414
- function renderExploreSlashPrompt() {
2781
+ function renderExploreSlashPrompt(language) {
2415
2782
  return renderManagedSlashPrompt(
2416
2783
  "fet explore [...args]",
2417
2784
  "Explore requirements for a FET/OpenSpec change",
@@ -2443,21 +2810,23 @@ Guardrails:
2443
2810
  - Do not write application code in explore mode.
2444
2811
  - Ask a clarifying question if the proposal would otherwise be mostly guesswork.
2445
2812
  - 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.`
2813
+ - After creating proposal.md, show the path and suggest /prompts:fet-continue <change-id> for the next artifact.`,
2814
+ void 0,
2815
+ language
2447
2816
  );
2448
2817
  }
2449
- function renderContinueSlashPrompt() {
2818
+ function renderContinueSlashPrompt(language) {
2450
2819
  return renderManagedSlashPrompt(
2451
2820
  "fet continue [...args]",
2452
2821
  "Create the next FET/OpenSpec artifact",
2453
2822
  `Continue a FET-managed OpenSpec change by creating exactly one ready artifact.
2454
2823
 
2455
- Input after the slash command should be a change id, optionally followed by an artifact id.
2824
+ 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
2825
 
2457
2826
  Steps:
2458
2827
 
2459
2828
  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.
2829
+ 2. Resolve the change id from the input, active FET/OpenSpec state, or the only open change. If ambiguous, ask the user.
2461
2830
  3. Check status:
2462
2831
  \`\`\`sh
2463
2832
  fet passthrough status --change <change-id> --json
@@ -2465,7 +2834,7 @@ Steps:
2465
2834
  4. Pick the first artifact whose status is ready, unless the user specified an artifact id.
2466
2835
  5. Run the native OpenSpec continue flow through FET:
2467
2836
  \`\`\`sh
2468
- fet continue <artifact-id> --change <change-id> --json
2837
+ fet continue [artifact-id] --change <change-id> --json
2469
2838
  \`\`\`
2470
2839
  6. Follow the native continue output. When it provides template, instruction, dependencies, and outputPath, use those fields.
2471
2840
  7. Read dependency files before writing.
@@ -2483,10 +2852,12 @@ Output:
2483
2852
  Guardrails:
2484
2853
  - Create one artifact per invocation.
2485
2854
  - Never skip dependency order.
2486
- - Ask the user if instructions are ambiguous enough that a useful artifact cannot be written.`
2855
+ - Ask the user if instructions are ambiguous enough that a useful artifact cannot be written.`,
2856
+ void 0,
2857
+ language
2487
2858
  );
2488
2859
  }
2489
- function renderFastForwardSlashPrompt(command) {
2860
+ function renderFastForwardSlashPrompt(command, language) {
2490
2861
  const title = command === "propose" ? "Propose a new FET/OpenSpec change" : "Fast-forward FET/OpenSpec artifact creation";
2491
2862
  const commandLine = command === "propose" ? "fet propose <change-id-or-description>" : "fet ff --change <change-id>";
2492
2863
  return renderManagedSlashPrompt(
@@ -2494,7 +2865,7 @@ function renderFastForwardSlashPrompt(command) {
2494
2865
  command === "propose" ? "Create a change and generate required OpenSpec artifacts" : "Generate required OpenSpec artifacts for a change",
2495
2866
  `${title}.
2496
2867
 
2497
- Input after the slash command may be a change id or a description of what the user wants to build.
2868
+ 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
2869
 
2499
2870
  Steps:
2500
2871
 
@@ -2517,11 +2888,15 @@ Output:
2517
2888
  - Change id and location.
2518
2889
  - Artifacts created.
2519
2890
  - Current status.
2520
- - Next recommended command, usually /prompts:fet-apply <change-id>.`
2891
+ - Next recommended command, usually /prompts:fet-apply <change-id>.`,
2892
+ void 0,
2893
+ language
2521
2894
  );
2522
2895
  }
2523
- function renderManagedSlashPrompt(command, description, body) {
2896
+ function renderManagedSlashPrompt(command, description, body, argumentHint, language = DEFAULT_LANGUAGE) {
2524
2897
  const policyCommand = command.split(/\s+/)[1] ?? command;
2898
+ const argumentHintLine = argumentHint ? `argument-hint: ${argumentHint}
2899
+ ` : "";
2525
2900
  return `<!-- FET:MANAGED
2526
2901
  schemaVersion: 1
2527
2902
  fetVersion: ${FET_VERSION}
@@ -2532,10 +2907,11 @@ FET:END -->
2532
2907
 
2533
2908
  ---
2534
2909
  description: ${description}
2535
- argument-hint: command arguments
2536
- ---
2910
+ ${argumentHintLine}---
2911
+
2912
+ ${renderIdeModelPolicy(policyCommand, language)}
2537
2913
 
2538
- ${renderIdeModelPolicy(policyCommand)}
2914
+ ${languageInstruction(language)}
2539
2915
 
2540
2916
  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.
2541
2917
 
@@ -2553,16 +2929,16 @@ var CodexAdapter = class {
2553
2929
  reason: "Codex adapter is available for projects that use AGENTS.md"
2554
2930
  };
2555
2931
  }
2556
- async planInstall(_projectRoot) {
2932
+ async planInstall(_projectRoot, language) {
2557
2933
  return {
2558
2934
  tool: this.tool,
2559
2935
  files: [
2560
- ...[codexGuideFile(), ...codexCommandFiles()].map((file) => ({
2936
+ ...[codexGuideFile(language), ...codexCommandFiles(language)].map((file) => ({
2561
2937
  ...file,
2562
2938
  managed: true,
2563
2939
  root: "project"
2564
2940
  })),
2565
- ...codexSlashPromptFiles().map((file) => ({
2941
+ ...codexSlashPromptFiles(language).map((file) => ({
2566
2942
  ...file,
2567
2943
  managed: true,
2568
2944
  root: "codex-home"
@@ -2649,13 +3025,13 @@ import { mkdir as mkdir8, readFile as readFile13, stat as stat7 } from "fs/promi
2649
3025
  import { dirname as dirname8, join as join16 } from "path";
2650
3026
 
2651
3027
  // src/adapters/cursor/templates.ts
2652
- function cursorSkillFiles() {
3028
+ function cursorSkillFiles(language = DEFAULT_LANGUAGE) {
2653
3029
  return FET_ADAPTER_COMMANDS.map((command) => ({
2654
3030
  path: `.cursor/skills/fet-${command}/SKILL.md`,
2655
- content: renderSkill(command)
3031
+ content: renderSkill(command, language)
2656
3032
  }));
2657
3033
  }
2658
- function cursorRuleFile() {
3034
+ function cursorRuleFile(language = DEFAULT_LANGUAGE) {
2659
3035
  return {
2660
3036
  path: ".cursor/rules/fet-context.mdc",
2661
3037
  content: `<!-- FET:MANAGED
@@ -2666,22 +3042,24 @@ adapterVersion: 1
2666
3042
  FET:END -->
2667
3043
 
2668
3044
  ---
2669
- description: Load FET project context for implementation tasks
3045
+ description: ${language === "en" ? "Load FET project context for implementation tasks" : "\u4E3A\u5B9E\u73B0\u4EFB\u52A1\u52A0\u8F7D FET \u9879\u76EE\u4E0A\u4E0B\u6587"}
2670
3046
  alwaysApply: false
2671
3047
  ---
2672
3048
 
3049
+ ${languageInstruction(language)}
3050
+
2673
3051
  \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
3052
 
2675
3053
  - AGENTS.md
2676
3054
  - 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
3055
+ - \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
3056
+ - \u5F53\u524D change \u76EE\u5F55\u4E0B\u7684 OpenSpec \u89C4\u5212\u4EA7\u7269\u3002
2679
3057
 
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
3058
+ \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
3059
  `
2682
3060
  };
2683
3061
  }
2684
- function renderSkill(command) {
3062
+ function renderSkill(command, language) {
2685
3063
  const usage = renderFetAdapterUsage(command, command === "passthrough" ? "[...args]" : "");
2686
3064
  if (command === "fill-context") {
2687
3065
  return `<!-- FET:MANAGED
@@ -2694,22 +3072,22 @@ FET:END -->
2694
3072
 
2695
3073
  ---
2696
3074
  name: fet-fill-context
2697
- description: Fill FET AGENTS.md placeholders with Cursor AI
3075
+ description: ${language === "en" ? "Fill FET AGENTS.md placeholders with Cursor AI" : "\u4F7F\u7528 Cursor AI \u586B\u5145 FET AGENTS.md \u5360\u4F4D\u7B26"}
2698
3076
  disable-model-invocation: false
2699
3077
  ---
2700
3078
 
2701
- Run \`fet fill-context\` first if the IDE commands need refreshing.
3079
+ ${renderIdeModelPolicy(command, language)}
2702
3080
 
2703
- ${renderIdeModelPolicy(command)}
3081
+ ${languageInstruction(language)}
2704
3082
 
2705
- If GitNexus code graph context is available in Cursor, prefer it before broad repository scans. If it is unavailable, continue normally.
3083
+ \u5982\u679C IDE \u547D\u4EE4\u9700\u8981\u5237\u65B0\uFF0C\u5148\u8FD0\u884C \`fet fill-context\`\u3002
2706
3084
 
2707
- Then read:
3085
+ \u7136\u540E\u9605\u8BFB\uFF1A
2708
3086
 
2709
3087
  - AGENTS.md
2710
3088
  - openspec/config.yaml
2711
3089
 
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.
3090
+ \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
2713
3091
  `;
2714
3092
  }
2715
3093
  return `<!-- FET:MANAGED
@@ -2722,15 +3100,15 @@ FET:END -->
2722
3100
 
2723
3101
  ---
2724
3102
  name: fet-${command}
2725
- description: Run FET-managed OpenSpec ${command} workflow from the terminal
3103
+ 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
3104
  disable-model-invocation: true
2727
3105
  ---
2728
3106
 
2729
- ${renderIdeModelPolicy(command)}
3107
+ ${renderIdeModelPolicy(command, language)}
2730
3108
 
2731
- If GitNexus code graph context is available in Cursor, prefer it before broad repository scans. If it is unavailable, continue normally.
3109
+ ${languageInstruction(language)}
2732
3110
 
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
3111
+ \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
3112
 
2735
3113
  \u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
2736
3114
 
@@ -2738,7 +3116,7 @@ If GitNexus code graph context is available in Cursor, prefer it before broad re
2738
3116
  ${usage}
2739
3117
  \`\`\`
2740
3118
 
2741
- \u6267\u884C\u524D\u8BF7\u786E\u8BA4\u5DF2\u9605\u8BFB AGENTS.md \u4E0E openspec/config.yaml\u3002
3119
+ \u6267\u884C\u524D\u8BF7\u786E\u8BA4\u5DF2\u9605\u8BFB AGENTS.md \u548C openspec/config.yaml\u3002
2742
3120
  `;
2743
3121
  }
2744
3122
 
@@ -2752,10 +3130,10 @@ var CursorAdapter = class {
2752
3130
  reason: "Cursor adapter is available for any project"
2753
3131
  };
2754
3132
  }
2755
- async planInstall(_projectRoot) {
3133
+ async planInstall(_projectRoot, language) {
2756
3134
  return {
2757
3135
  tool: this.tool,
2758
- files: [...cursorSkillFiles(), cursorRuleFile()].map((file) => ({
3136
+ files: [...cursorSkillFiles(language), cursorRuleFile(language)].map((file) => ({
2759
3137
  ...file,
2760
3138
  managed: true
2761
3139
  }))
@@ -2770,7 +3148,7 @@ var CursorAdapter = class {
2770
3148
  if (existing && !existing.includes("FET:MANAGED") && !force) {
2771
3149
  throw new FetError({
2772
3150
  code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
2773
- message: "Cursor \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\u4E14\u4E0D\u5F52 FET \u7BA1\u7406",
3151
+ message: "Cursor \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u4E14\u4E0D\u7531 FET \u7BA1\u7406\u3002",
2774
3152
  details: { path: file.path },
2775
3153
  suggestedCommand: "fet init --yes"
2776
3154
  });
@@ -2795,7 +3173,7 @@ var CursorAdapter = class {
2795
3173
  checks.push({
2796
3174
  id: `cursor:${file.path}`,
2797
3175
  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`,
3176
+ 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
3177
  suggestedCommand: !content || !managed || !versionMatches ? "fet init" : void 0
2800
3178
  });
2801
3179
  }
@@ -3238,10 +3616,12 @@ var ProjectScanner = class {
3238
3616
 
3239
3617
  // src/cli/output.ts
3240
3618
  var OutputWriter = class {
3241
- constructor(json) {
3619
+ constructor(json, language = "zh-CN") {
3242
3620
  this.json = json;
3621
+ this.language = language;
3243
3622
  }
3244
3623
  json;
3624
+ language;
3245
3625
  info(message, details) {
3246
3626
  if (!this.json) {
3247
3627
  process.stdout.write(`${message}${formatDetails(details)}
@@ -3254,10 +3634,8 @@ var OutputWriter = class {
3254
3634
  `);
3255
3635
  return;
3256
3636
  }
3257
- if (!this.json) {
3258
- process.stderr.write(`\u8B66\u544A\uFF1A${message}${formatDetails(details)}
3637
+ process.stderr.write(`${this.language === "en" ? "Warning" : "\u8B66\u544A"}\uFF1A${message}${formatDetails(details)}
3259
3638
  `);
3260
- }
3261
3639
  }
3262
3640
  error(error) {
3263
3641
  if (this.json) {
@@ -3265,17 +3643,17 @@ var OutputWriter = class {
3265
3643
  `);
3266
3644
  return;
3267
3645
  }
3268
- process.stderr.write(`FET \u65E0\u6CD5\u7EE7\u7EED\uFF1A${error.message}
3646
+ process.stderr.write(`${this.language === "en" ? "FET cannot continue" : "FET \u65E0\u6CD5\u7EE7\u7EED"}\uFF1A${error.message}
3269
3647
  `);
3270
3648
  if (error.details !== void 0) {
3271
3649
  process.stderr.write(`
3272
- \u8BE6\u60C5\uFF1A
3650
+ ${this.language === "en" ? "Details" : "\u8BE6\u60C5"}\uFF1A
3273
3651
  ${formatBlock(error.details)}
3274
3652
  `);
3275
3653
  }
3276
3654
  if (error.suggestedCommand) {
3277
3655
  process.stderr.write(`
3278
- \u5EFA\u8BAE\uFF1A
3656
+ ${this.language === "en" ? "Suggestion" : "\u5EFA\u8BAE"}\uFF1A
3279
3657
  ${error.suggestedCommand}
3280
3658
  `);
3281
3659
  }
@@ -3289,11 +3667,13 @@ ${formatBlock(error.details)}
3289
3667
  process.stdout.write(`${result.summary}
3290
3668
  `);
3291
3669
  for (const warning of result.warnings ?? []) {
3292
- process.stdout.write(`\u8B66\u544A\uFF1A${warning}
3670
+ process.stdout.write(`${this.language === "en" ? "Warning" : "\u8B66\u544A"}\uFF1A${warning}
3293
3671
  `);
3294
3672
  }
3295
3673
  if (result.nextSteps?.length) {
3296
- process.stdout.write("\n\u4E0B\u4E00\u6B65\uFF1A\n");
3674
+ process.stdout.write(`
3675
+ ${this.language === "en" ? "Next steps" : "\u4E0B\u4E00\u6B65"}\uFF1A
3676
+ `);
3297
3677
  for (const step of result.nextSteps) {
3298
3678
  process.stdout.write(` ${step}
3299
3679
  `);
@@ -3318,7 +3698,10 @@ function formatBlock(details) {
3318
3698
  async function createCommandContext(command, options) {
3319
3699
  const projectRoot = resolve(options.cwd ?? process.cwd());
3320
3700
  const project = await detectProjectIdentity(projectRoot);
3321
- const output = new OutputWriter(Boolean(options.json));
3701
+ const stateStore = new StateStore(projectRoot, FET_VERSION, project);
3702
+ const savedState = await stateStore.readGlobal();
3703
+ const language = normalizeLanguage(options.lang ?? savedState?.language.current);
3704
+ const output = new OutputWriter(Boolean(options.json), language);
3322
3705
  return {
3323
3706
  command,
3324
3707
  cwd: projectRoot,
@@ -3328,9 +3711,11 @@ async function createCommandContext(command, options) {
3328
3711
  verbose: Boolean(options.verbose),
3329
3712
  yes: Boolean(options.yes),
3330
3713
  changeId: options.change,
3714
+ language,
3715
+ explicitLanguage: options.lang !== void 0,
3331
3716
  fetVersion: FET_VERSION,
3332
3717
  output,
3333
- stateStore: new StateStore(projectRoot, FET_VERSION, project),
3718
+ stateStore,
3334
3719
  openSpec: new DefaultOpenSpecAdapter(),
3335
3720
  scanner: new ProjectScanner(),
3336
3721
  toolAdapters: [new CursorAdapter(), new CodexAdapter()]
@@ -3339,25 +3724,33 @@ async function createCommandContext(command, options) {
3339
3724
 
3340
3725
  // src/cli/index.ts
3341
3726
  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");
3727
+ 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
3728
  addGlobalOptions(program.command("init")).description("\u521D\u59CB\u5316 FET + OpenSpec").action(wrap("init", initCommand));
3344
3729
  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"));
3730
+ 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));
3731
+ var graph = addGlobalOptions(program.command("graph").description("\u7BA1\u7406\u53EF\u9009\u7684 GitNexus \u4EE3\u7801\u56FE\u652F\u6301"));
3347
3732
  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)));
3733
+ addGlobalOptions(graph.command(action).description(`\u8FD0\u884C fet graph ${action}`)).action(wrap("graph", (ctx) => graphCommand(ctx, action)));
3349
3734
  }
3350
3735
  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)));
3736
+ addGlobalOptions(
3737
+ graph.command(`${action} [args...]`).description(`\u901A\u8FC7 GitNexus analyze \u6267\u884C graph ${action}`).allowUnknownOption(true).passThroughOptions()
3738
+ ).action(wrap("graph", (ctx, args = []) => graphCommand(ctx, action, args)));
3352
3739
  }
3353
3740
  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
3741
  wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
3355
3742
  );
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));
3743
+ 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(
3744
+ wrap("verify", verifyCommand)
3745
+ );
3357
3746
  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)));
3747
+ addGlobalOptions(program.command(`${command} [args...]`).description(`\u4EE3\u7406\u6267\u884C openspec ${command}`).allowUnknownOption(true).passThroughOptions()).action(
3748
+ wrap(command, (ctx, args = []) => proxyCommand(ctx, command, args))
3749
+ );
3359
3750
  }
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)));
3751
+ addGlobalOptions(program.command("passthrough <command> [args...]").description("\u900F\u4F20\u6682\u672A\u63A5\u7BA1\u7684 OpenSpec \u547D\u4EE4").allowUnknownOption(true).passThroughOptions()).action(
3752
+ wrap("passthrough", (ctx, command, args = []) => passthroughCommand(ctx, command, args))
3753
+ );
3361
3754
  program.parseAsync(process.argv).catch((error) => {
3362
3755
  const json = process.argv.includes("--json");
3363
3756
  const output = new OutputWriter(json);
@@ -3371,7 +3764,7 @@ function wrap(command, handler) {
3371
3764
  const opts = isCommandLike(maybeCommand) ? { ...maybeCommand.parent?.opts(), ...maybeCommand.opts() } : program.opts();
3372
3765
  const ctx = await createCommandContext(command, { ...opts, ...extractGlobalOptions(args) });
3373
3766
  try {
3374
- await confirmModelPolicyRecommendation(ctx);
3767
+ await handleModelPolicyRecommendation(ctx);
3375
3768
  await warnIfContextPlaceholdersRemain(ctx);
3376
3769
  await handler(ctx, ...args);
3377
3770
  } catch (error) {
@@ -3381,45 +3774,53 @@ function wrap(command, handler) {
3381
3774
  }
3382
3775
  };
3383
3776
  }
3384
- async function confirmModelPolicyRecommendation(ctx) {
3777
+ async function handleModelPolicyRecommendation(ctx) {
3385
3778
  const mismatch = getCommandModelPolicyMismatch(ctx.command);
3386
3779
  if (!mismatch) {
3387
3780
  return;
3388
3781
  }
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) {
3782
+ const warning = formatModelPolicyMismatch(mismatch, ctx.language);
3783
+ const policyMode = getModelPolicyMode();
3784
+ ctx.output.warn(`${warning} ${renderModelPolicyActionHint(policyMode, ctx.language)}`);
3785
+ if (policyMode !== "confirm" || ctx.yes || ctx.json || !process.stdin.isTTY || !process.stderr.isTTY) {
3392
3786
  return;
3393
3787
  }
3394
3788
  const rl = createInterface({ input: process.stdin, output: process.stderr });
3395
3789
  try {
3396
- const answer = (await rl.question("Continue anyway? [y/N] ")).trim().toLowerCase();
3790
+ const question = ctx.language === "en" ? "Continue anyway? [y/N] " : "\u4ECD\u7136\u7EE7\u7EED\uFF1F[y/N] ";
3791
+ const answer = (await rl.question(question)).trim().toLowerCase();
3397
3792
  if (answer !== "y" && answer !== "yes") {
3398
3793
  throw new FetError({
3399
3794
  code: "USER_CANCELLED" /* UserCancelled */,
3400
- message: "Command cancelled so you can switch IDE model.",
3795
+ 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
3796
  details: { command: ctx.command, detected: mismatch.detected, recommended: mismatch.recommended },
3402
- suggestedCommand: `Switch IDE model, then rerun fet ${ctx.command}.`
3797
+ 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
3798
  });
3404
3799
  }
3405
3800
  } finally {
3406
3801
  rl.close();
3407
3802
  }
3408
3803
  }
3804
+ function renderModelPolicyActionHint(policyMode, language) {
3805
+ if (policyMode === "confirm") {
3806
+ 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";
3807
+ }
3808
+ return language === "en" ? "This is advisory; the command will continue. Set FET_MODEL_POLICY=confirm if you want an explicit stop/continue prompt." : "\u8BE5\u63D0\u9192\u4EC5\u4F5C\u4E3A\u5EFA\u8BAE\uFF0C\u547D\u4EE4\u4F1A\u7EE7\u7EED\u6267\u884C\u3002\u5982\u9700\u663E\u5F0F\u9009\u62E9\u505C\u6B62\u6216\u7EE7\u7EED\uFF0C\u53EF\u8BBE\u7F6E FET_MODEL_POLICY=confirm\u3002";
3809
+ }
3409
3810
  async function warnIfContextPlaceholdersRemain(ctx) {
3410
3811
  if (["init", "update-context", "fill-context", "doctor"].includes(ctx.command)) {
3411
3812
  return;
3412
3813
  }
3413
3814
  const count2 = await countAgentsLlmPlaceholders(ctx.projectRoot);
3414
3815
  if (count2 > 0) {
3415
- ctx.output.warn(renderAgentsPlaceholderWarning(count2));
3816
+ ctx.output.warn(renderAgentsPlaceholderWarning(count2, ctx.language));
3416
3817
  }
3417
3818
  }
3418
3819
  function isCommandLike(value) {
3419
3820
  return value instanceof Command;
3420
3821
  }
3421
3822
  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");
3823
+ 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
3824
  }
3424
3825
  function extractGlobalOptions(args) {
3425
3826
  const values = args.flatMap((arg) => Array.isArray(arg) ? arg : []);
@@ -3439,6 +3840,11 @@ function extractGlobalOptions(args) {
3439
3840
  index += 1;
3440
3841
  } else if (value.startsWith("--change=")) {
3441
3842
  options.change = value.slice("--change=".length);
3843
+ } else if (value === "--lang" && typeof values[index + 1] === "string") {
3844
+ options.lang = values[index + 1];
3845
+ index += 1;
3846
+ } else if (value.startsWith("--lang=")) {
3847
+ options.lang = value.slice("--lang=".length);
3442
3848
  } else if (value === "--yes") {
3443
3849
  options.yes = true;
3444
3850
  } else if (value === "--json") {