@nick848/fet 1.0.5 → 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/README.md +83 -183
- package/README_en.md +196 -0
- package/dist/cli/index.js +1447 -900
- package/dist/cli/index.js.map +1 -1
- package/package.json +4 -2
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/
|
|
12
|
-
import { readFile as
|
|
13
|
-
import { join as
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
101
|
-
import { readFile as
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 "
|
|
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,836 +255,1078 @@ function splitCommand(value) {
|
|
|
230
255
|
return (matches?.length ? matches : [value]).map((part) => part.replace(/^["']|["']$/g, ""));
|
|
231
256
|
}
|
|
232
257
|
|
|
233
|
-
// src/
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
326
|
-
|
|
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/
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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 -->
|
|
362
402
|
|
|
363
|
-
|
|
364
|
-
|-------|--------|------------|
|
|
365
|
-
${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
|
|
403
|
+
# FET Fill Context
|
|
366
404
|
|
|
367
|
-
|
|
405
|
+
Use the IDE AI to complete FET-generated placeholders.
|
|
368
406
|
|
|
369
|
-
|
|
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 -->
|
|
370
420
|
|
|
371
|
-
|
|
421
|
+
# FET \u586B\u5145\u4E0A\u4E0B\u6587
|
|
372
422
|
|
|
373
|
-
|
|
374
|
-
- FET Version: ${FET_VERSION}
|
|
375
|
-
- Scanner Version: ${scan.scannerVersion}
|
|
376
|
-
- Warnings: ${scan.warnings.length ? scan.warnings.join("; ") : "none"}
|
|
377
|
-
${AUTO_END}
|
|
423
|
+
\u4F7F\u7528 IDE AI \u8865\u9F50 FET \u751F\u6210\u7684\u9879\u76EE\u4E0A\u4E0B\u6587\u5360\u4F4D\u7B26\u3002
|
|
378
424
|
|
|
379
|
-
|
|
380
|
-
|
|
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
|
+
}
|
|
381
434
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
+
}
|
|
385
459
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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()
|
|
413
581
|
}
|
|
414
582
|
}
|
|
415
583
|
});
|
|
416
584
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
|
421
619
|
schemaVersion: 1
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
changeId: ${changeId}
|
|
425
|
-
purpose: manual-verify
|
|
426
|
-
---
|
|
620
|
+
generator: graph-setup
|
|
621
|
+
FET:END -->
|
|
427
622
|
|
|
428
|
-
#
|
|
623
|
+
# FET Graph Setup
|
|
429
624
|
|
|
430
|
-
|
|
625
|
+
GitNexus graph support is optional. FET does not install GitNexus automatically and does not require graph support for OpenSpec workflows.
|
|
431
626
|
|
|
432
|
-
|
|
433
|
-
2. \u6309\u9879\u76EE\u7EA6\u5B9A\u8FD0\u884C lint\u3001typecheck\u3001test\u3002
|
|
434
|
-
3. \u68C0\u67E5\u672C\u6B21 change \u7684 \`tasks.md\` \u662F\u5426\u4E0E\u5B9E\u73B0\u72B6\u6001\u4E00\u81F4\u3002
|
|
627
|
+
Current status:
|
|
435
628
|
|
|
436
|
-
|
|
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"}
|
|
437
634
|
|
|
438
|
-
|
|
439
|
-
fet verify --done --change ${changeId}
|
|
440
|
-
\`\`\`
|
|
441
|
-
`;
|
|
442
|
-
}
|
|
635
|
+
Suggested setup flow:
|
|
443
636
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
"openspec/fet-state.json",
|
|
449
|
-
"openspec/.fet.lock",
|
|
450
|
-
"openspec/.fet-init-journal.json",
|
|
451
|
-
"openspec/changes/*/fet-state.json",
|
|
452
|
-
"openspec/changes/*/.fet/",
|
|
453
|
-
".gitnexus/"
|
|
454
|
-
];
|
|
455
|
-
function mergeGitignore(existing) {
|
|
456
|
-
const block = `${BEGIN}
|
|
457
|
-
${RULES.join("\n")}
|
|
458
|
-
${END}`;
|
|
459
|
-
if (!existing || !existing.trim()) {
|
|
460
|
-
return `${block}
|
|
461
|
-
`;
|
|
462
|
-
}
|
|
463
|
-
const start = existing.indexOf(BEGIN);
|
|
464
|
-
const end = existing.indexOf(END);
|
|
465
|
-
if (start !== -1 && end !== -1 && end > start) {
|
|
466
|
-
return `${existing.slice(0, start)}${block}${existing.slice(end + END.length)}`;
|
|
467
|
-
}
|
|
468
|
-
return `${existing.replace(/\s*$/, "")}
|
|
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.
|
|
469
641
|
|
|
470
|
-
|
|
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.
|
|
471
647
|
`;
|
|
472
648
|
}
|
|
649
|
+
function renderGraphUsageHandoff(state) {
|
|
650
|
+
return `<!-- FET:MANAGED
|
|
651
|
+
schemaVersion: 1
|
|
652
|
+
generator: graph-handoff
|
|
653
|
+
FET:END -->
|
|
473
654
|
|
|
474
|
-
|
|
475
|
-
import { readFile as readFile5 } from "fs/promises";
|
|
476
|
-
import { join as join7 } from "path";
|
|
655
|
+
# FET Graph Handoff
|
|
477
656
|
|
|
478
|
-
|
|
479
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
480
|
-
import { parseDocument } from "yaml";
|
|
481
|
-
async function mergeFetConfig(configPath, renderedFetYaml) {
|
|
482
|
-
const fetDoc = parseDocument(renderedFetYaml);
|
|
483
|
-
const nextFet = fetDoc.get("fet", true);
|
|
484
|
-
let existing = "";
|
|
485
|
-
try {
|
|
486
|
-
existing = await readFile3(configPath, "utf8");
|
|
487
|
-
} catch {
|
|
488
|
-
return renderedFetYaml;
|
|
489
|
-
}
|
|
490
|
-
const doc = parseDocument(existing || "{}");
|
|
491
|
-
doc.set("fet", nextFet);
|
|
492
|
-
return doc.toString();
|
|
493
|
-
}
|
|
657
|
+
Use GitNexus graph context as an optional first pass before broad repository scans.
|
|
494
658
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
+
`;
|
|
506
680
|
}
|
|
507
|
-
function
|
|
508
|
-
return
|
|
681
|
+
function firstLine(value) {
|
|
682
|
+
return value.trim().split(/\r?\n/)[0]?.trim() || null;
|
|
509
683
|
}
|
|
510
684
|
|
|
511
|
-
// src/commands/
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
const configPath = join7(ctx.projectRoot, "openspec", "config.yaml");
|
|
532
|
-
const existingAgents = await readOptional(agentsPath);
|
|
533
|
-
const warnings = [...scan.warnings];
|
|
534
|
-
if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
|
|
535
|
-
throw new FetError({
|
|
536
|
-
code: "CONFIG_INVALID" /* ConfigInvalid */,
|
|
537
|
-
message: "AGENTS.md \u7684 FET \u6258\u7BA1\u6807\u8BB0\u635F\u574F\u6216\u91CD\u590D",
|
|
538
|
-
details: { path: "AGENTS.md" },
|
|
539
|
-
suggestedCommand: "\u624B\u52A8\u4FEE\u590D FET:BEGIN AUTO / FET:END AUTO \u6807\u8BB0\u540E\u91CD\u8BD5"
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
if (existingAgents && !hasManagedAutoRegion(existingAgents)) {
|
|
543
|
-
if (!ctx.yes) {
|
|
544
|
-
throw new FetError({
|
|
545
|
-
code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
|
|
546
|
-
message: "AGENTS.md \u5DF2\u5B58\u5728\u4E14\u4E0D\u5305\u542B FET \u6258\u7BA1\u533A\u57DF",
|
|
547
|
-
details: { path: "AGENTS.md" },
|
|
548
|
-
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"
|
|
549
|
-
});
|
|
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;
|
|
703
|
+
}
|
|
704
|
+
throw new Error(`package.json \u7F3A\u5C11\u6709\u6548\u7684 version \u5B57\u6BB5: ${packageJsonPath}`);
|
|
550
705
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
warnings.push(`\u5DF2\u5907\u4EFD\u975E\u6258\u7BA1 AGENTS.md \u5230 ${backupPath}`);
|
|
706
|
+
if (currentDir === root) {
|
|
707
|
+
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D FET package.json");
|
|
554
708
|
}
|
|
555
|
-
|
|
556
|
-
await atomicWrite(agentsPath, replaceManagedRegion(existingAgents, renderAgentsMd(scan)));
|
|
557
|
-
await atomicWrite(configPath, await mergeFetConfig(configPath, renderFetConfig(scan)));
|
|
558
|
-
const placeholderCount = await countAgentsLlmPlaceholders(ctx.projectRoot);
|
|
559
|
-
if (placeholderCount > 0) {
|
|
560
|
-
warnings.push(renderAgentsPlaceholderWarning(placeholderCount));
|
|
561
|
-
}
|
|
562
|
-
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
563
|
-
state.context = {
|
|
564
|
-
agentsMdUpdatedAt: scan.generatedAt,
|
|
565
|
-
configUpdatedAt: scan.generatedAt,
|
|
566
|
-
scannerVersion: scan.scannerVersion
|
|
567
|
-
};
|
|
568
|
-
await ctx.stateStore.writeGlobal(state);
|
|
569
|
-
return { warnings };
|
|
570
|
-
}
|
|
571
|
-
async function readOptional(path) {
|
|
572
|
-
try {
|
|
573
|
-
return await readFile5(path, "utf8");
|
|
574
|
-
} catch {
|
|
575
|
-
return null;
|
|
709
|
+
currentDir = dirname6(currentDir);
|
|
576
710
|
}
|
|
577
711
|
}
|
|
578
712
|
|
|
579
|
-
// src/
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const journal = createInitJournal(ctx.fetVersion);
|
|
588
|
-
await writeInitJournal(ctx.projectRoot, journal);
|
|
589
|
-
const identity = await ctx.openSpec.resolveExecutable();
|
|
590
|
-
if (!alreadyInitialized) {
|
|
591
|
-
const result = await ctx.openSpec.run("init", ["--tools", "none"], { cwd: ctx.projectRoot, stdio: "inherit" });
|
|
592
|
-
if (result.exitCode !== 0) {
|
|
593
|
-
process.exitCode = result.exitCode;
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
const contextResult = await updateContextFiles(ctx);
|
|
598
|
-
warnings = contextResult.warnings;
|
|
599
|
-
await ensureGitignore(ctx);
|
|
600
|
-
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
601
|
-
state.openspec = identity;
|
|
602
|
-
state.graph ??= {};
|
|
603
|
-
const gitnexus = mergeGitNexusGraphInfo(
|
|
604
|
-
toGitNexusState(await detectGitNexus(), state.graph.gitnexus),
|
|
605
|
-
await inspectGitNexusGraph(ctx.projectRoot)
|
|
606
|
-
);
|
|
607
|
-
if (!gitnexus.installed && !gitnexus.recommendationShownAt) {
|
|
608
|
-
warnings.push(renderGitNexusRecommendation(gitnexus));
|
|
609
|
-
gitnexus.recommendationShownAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
610
|
-
}
|
|
611
|
-
state.graph.gitnexus = gitnexus;
|
|
612
|
-
for (const adapter of ctx.toolAdapters) {
|
|
613
|
-
const plan = await adapter.planInstall(ctx.projectRoot);
|
|
614
|
-
const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
|
|
615
|
-
state.toolAdapters[adapter.tool] = {
|
|
616
|
-
adapterVersion: adapter.adapterVersion,
|
|
617
|
-
installed: true,
|
|
618
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
619
|
-
};
|
|
620
|
-
journal.steps.push(...result.written.map((path) => ({ operation: "write", path, status: "done" })));
|
|
621
|
-
}
|
|
622
|
-
journal.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
623
|
-
await writeInitJournal(ctx.projectRoot, journal);
|
|
624
|
-
await ctx.stateStore.writeGlobal(state);
|
|
625
|
-
}
|
|
626
|
-
);
|
|
627
|
-
ctx.output.result({
|
|
628
|
-
ok: true,
|
|
629
|
-
command: "init",
|
|
630
|
-
summary: "FET \u521D\u59CB\u5316\u5B8C\u6210\u3002",
|
|
631
|
-
warnings,
|
|
632
|
-
nextSteps: ["\u4F7F\u7528 fet propose/new \u521B\u5EFA OpenSpec change", "\u4F7F\u7528 fet doctor \u68C0\u67E5\u9879\u76EE\u72B6\u6001"]
|
|
633
|
-
});
|
|
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);
|
|
634
721
|
}
|
|
635
|
-
|
|
636
|
-
const
|
|
637
|
-
const
|
|
638
|
-
|
|
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);
|
|
639
726
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
return
|
|
643
|
-
} catch {
|
|
644
|
-
return null;
|
|
727
|
+
function replaceManagedRegion(existing, generated) {
|
|
728
|
+
if (!existing) {
|
|
729
|
+
return generated;
|
|
645
730
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
return true;
|
|
651
|
-
} catch {
|
|
652
|
-
return false;
|
|
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;
|
|
653
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}`;
|
|
654
743
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const checks = [];
|
|
661
|
-
checks.push(await checkOpenSpec(ctx));
|
|
662
|
-
checks.push(await checkState(ctx));
|
|
663
|
-
checks.push(await checkFile("agents", join9(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
|
|
664
|
-
checks.push(await checkFile("config", join9(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
|
|
665
|
-
checks.push(await checkPlaceholders(ctx.projectRoot));
|
|
666
|
-
checks.push(await checkGitNexus(ctx));
|
|
667
|
-
for (const adapter of ctx.toolAdapters) {
|
|
668
|
-
checks.push(...await adapter.doctor(ctx.projectRoot));
|
|
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;
|
|
669
749
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
if (
|
|
673
|
-
|
|
674
|
-
checks.push({ id: "lock", status: "pass", message: "\u5DF2\u6E05\u7406 openspec/.fet.lock" });
|
|
675
|
-
} else {
|
|
676
|
-
checks.push({ id: "lock", status: "warn", message: "\u5B58\u5728 openspec/.fet.lock", suggestedCommand: "fet doctor --fix-lock" });
|
|
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();
|
|
677
754
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
ctx.output.result({
|
|
681
|
-
ok: true,
|
|
682
|
-
command: "doctor",
|
|
683
|
-
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",
|
|
684
|
-
warnings,
|
|
685
|
-
data: checks
|
|
686
|
-
});
|
|
755
|
+
return section.raw.trim();
|
|
756
|
+
}).join("\n\n");
|
|
687
757
|
}
|
|
688
|
-
|
|
689
|
-
const
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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"
|
|
708
779
|
};
|
|
780
|
+
return aliases[normalized] ?? normalized;
|
|
709
781
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
return
|
|
714
|
-
} catch (error) {
|
|
715
|
-
return { id: "openspec", status: "fail", message: error instanceof Error ? error.message : "OpenSpec \u68C0\u6D4B\u5931\u8D25" };
|
|
782
|
+
function splitMarkdownSections(content) {
|
|
783
|
+
const matches = [...content.matchAll(/^## .+$/gm)];
|
|
784
|
+
if (!matches.length) {
|
|
785
|
+
return [];
|
|
716
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 };
|
|
795
|
+
});
|
|
717
796
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
return { id: "state", status: "fail", message: error instanceof Error ? error.message : "FET \u72B6\u6001\u8BFB\u53D6\u5931\u8D25" };
|
|
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();
|
|
724
802
|
}
|
|
803
|
+
return content.slice(start + AUTO_BEGIN.length, end).trim();
|
|
725
804
|
}
|
|
726
|
-
|
|
727
|
-
return
|
|
805
|
+
function count(content, needle) {
|
|
806
|
+
return content.split(needle).length - 1;
|
|
728
807
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
return
|
|
734
|
-
id: "context-placeholders",
|
|
735
|
-
status: "warn",
|
|
736
|
-
message: `AGENTS.md has ${count2} LLM placeholder(s)`,
|
|
737
|
-
suggestedCommand: "fet fill-context"
|
|
738
|
-
} : { id: "context-placeholders", status: "pass", message: "AGENTS.md placeholders resolved" };
|
|
739
|
-
} catch {
|
|
740
|
-
return { id: "context-placeholders", status: "warn", message: "AGENTS.md missing", suggestedCommand: "fet update-context" };
|
|
808
|
+
|
|
809
|
+
// src/templates/agents-md.ts
|
|
810
|
+
function renderAgentsMd(scan, language = "zh-CN") {
|
|
811
|
+
if (language === "en") {
|
|
812
|
+
return renderAgentsMdEn(scan);
|
|
741
813
|
}
|
|
814
|
+
return renderAgentsMdZh(scan);
|
|
742
815
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
|
821
|
+
|
|
822
|
+
${AUTO_BEGIN}
|
|
823
|
+
## \u9879\u76EE\u5FEB\u7167
|
|
824
|
+
|
|
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"}
|
|
830
|
+
|
|
831
|
+
## \u5DE5\u4F5C\u533A
|
|
832
|
+
|
|
833
|
+
| \u540D\u79F0 | \u8DEF\u5F84 | \u6765\u6E90 |
|
|
834
|
+
|------|------|--------|
|
|
835
|
+
${workspaces || "| root | . | inferred |"}
|
|
836
|
+
|
|
837
|
+
## \u547D\u4EE4
|
|
838
|
+
|
|
839
|
+
| \u540D\u79F0 | \u547D\u4EE4 | \u6765\u6E90 |
|
|
840
|
+
|------|---------|--------|
|
|
841
|
+
${commands || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | [NEEDS LLM INPUT] |"}
|
|
842
|
+
|
|
843
|
+
## \u7ED3\u6784
|
|
844
|
+
|
|
845
|
+
[NEEDS LLM INPUT]
|
|
846
|
+
|
|
847
|
+
## \u8DEF\u7531
|
|
848
|
+
|
|
849
|
+
| \u8DEF\u7531 | \u6765\u6E90 | \u7F6E\u4FE1\u5EA6 |
|
|
850
|
+
|-------|--------|------------|
|
|
851
|
+
${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
|
|
852
|
+
|
|
853
|
+
## \u7EA6\u5B9A
|
|
854
|
+
|
|
855
|
+
[NEEDS LLM INPUT]
|
|
856
|
+
|
|
857
|
+
## AI \u5DE5\u4F5C\u6307\u5357
|
|
858
|
+
|
|
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
|
|
863
|
+
|
|
864
|
+
## \u626B\u63CF\u5143\u6570\u636E
|
|
865
|
+
|
|
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}
|
|
871
|
+
|
|
872
|
+
${USER_BEGIN}
|
|
873
|
+
## \u7ED9 AI \u7684\u5907\u6CE8
|
|
874
|
+
|
|
875
|
+
[NEEDS LLM INPUT]
|
|
876
|
+
${USER_END}
|
|
877
|
+
`;
|
|
750
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
|
|
751
884
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
885
|
+
${AUTO_BEGIN}
|
|
886
|
+
## Project Snapshot
|
|
887
|
+
|
|
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"}
|
|
893
|
+
|
|
894
|
+
## Workspaces
|
|
895
|
+
|
|
896
|
+
| Name | Path | Source |
|
|
897
|
+
|------|------|--------|
|
|
898
|
+
${workspaces || "| root | . | inferred |"}
|
|
899
|
+
|
|
900
|
+
## Commands
|
|
901
|
+
|
|
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
|
+
`;
|
|
940
|
+
}
|
|
941
|
+
|
|
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
|
|
776
969
|
}
|
|
777
970
|
}
|
|
778
|
-
);
|
|
779
|
-
const placeholders = await countAgentsLlmPlaceholders(ctx.projectRoot);
|
|
780
|
-
ctx.output.result({
|
|
781
|
-
ok: true,
|
|
782
|
-
command: "fill-context",
|
|
783
|
-
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.",
|
|
784
|
-
nextSteps: placeholders ? [
|
|
785
|
-
"Cursor: run /fet-fill-context",
|
|
786
|
-
"Codex: run /prompts:fet-fill-context",
|
|
787
|
-
"OpenCode or other IDEs: open .fet/fill-context.md or run fet fill-context for handoff instructions"
|
|
788
|
-
] : ["Run fet doctor to confirm project context health"],
|
|
789
|
-
data: {
|
|
790
|
-
placeholders,
|
|
791
|
-
cursorCommand: "/fet-fill-context",
|
|
792
|
-
codexCommand: "/prompts:fet-fill-context"
|
|
793
|
-
}
|
|
794
971
|
});
|
|
795
972
|
}
|
|
796
|
-
|
|
973
|
+
|
|
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
|
+
`;
|
|
983
|
+
}
|
|
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)}`;
|
|
988
|
+
}
|
|
989
|
+
return `${existing.replace(/\s*$/, "")}
|
|
990
|
+
|
|
991
|
+
${block}
|
|
992
|
+
`;
|
|
993
|
+
}
|
|
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
|
+
`;
|
|
1007
|
+
}
|
|
1008
|
+
function renderKarpathyFetHandoff(language = "zh-CN") {
|
|
797
1009
|
return `<!-- FET:MANAGED
|
|
798
1010
|
schemaVersion: 1
|
|
799
|
-
generator:
|
|
1011
|
+
generator: karpathy-skills
|
|
800
1012
|
FET:END -->
|
|
801
1013
|
|
|
802
|
-
#
|
|
803
|
-
|
|
804
|
-
Use the IDE AI to complete FET-generated placeholders.
|
|
1014
|
+
# ${language === "en" ? "Andrej Karpathy Inspired Coding Guidelines" : "\u53D7 Andrej Karpathy \u542F\u53D1\u7684\u7F16\u7801\u6307\u5357"}
|
|
805
1015
|
|
|
806
|
-
|
|
807
|
-
2. Inspect README files, package scripts, routes, tests, source layout, and project conventions.
|
|
808
|
-
3. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete project-specific content.
|
|
809
|
-
4. Preserve FET managed markers.
|
|
810
|
-
5. Do not modify business code.
|
|
811
|
-
6. Run \`fet doctor\` and confirm no AGENTS.md placeholder warning remains.
|
|
1016
|
+
${renderKarpathyGuidelinesBody(language)}
|
|
812
1017
|
`;
|
|
813
1018
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
async function graphCommand(ctx, action, args = []) {
|
|
819
|
-
switch (action) {
|
|
820
|
-
case "status":
|
|
821
|
-
await graphStatusCommand(ctx);
|
|
822
|
-
return;
|
|
823
|
-
case "doctor":
|
|
824
|
-
await graphDoctorCommand(ctx);
|
|
825
|
-
return;
|
|
826
|
-
case "setup":
|
|
827
|
-
await graphSetupCommand(ctx);
|
|
828
|
-
return;
|
|
829
|
-
case "handoff":
|
|
830
|
-
await graphHandoffCommand(ctx);
|
|
831
|
-
return;
|
|
832
|
-
case "init":
|
|
833
|
-
await graphAnalyzeCommand(ctx, "init", args);
|
|
834
|
-
return;
|
|
835
|
-
case "refresh":
|
|
836
|
-
await graphAnalyzeCommand(ctx, "refresh", args);
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
async function graphStatusCommand(ctx) {
|
|
841
|
-
const result = await refreshGraphState(ctx, { runStatus: true });
|
|
842
|
-
const warnings = result.state.installed ? [] : ["GitNexus is not installed. Run fet graph setup for installation handoff instructions."];
|
|
843
|
-
ctx.output.result({
|
|
844
|
-
ok: true,
|
|
845
|
-
command: "graph status",
|
|
846
|
-
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.",
|
|
847
|
-
warnings,
|
|
848
|
-
nextSteps: result.state.installed && !result.state.graphExists ? ["Run fet graph init to build the first GitNexus graph"] : void 0,
|
|
849
|
-
data: result
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
async function graphDoctorCommand(ctx) {
|
|
853
|
-
const result = await refreshGraphState(ctx, { runStatus: true });
|
|
854
|
-
const warnings = [
|
|
855
|
-
...!result.state.installed ? ["GitNexus is not installed."] : [],
|
|
856
|
-
...result.state.installed && !result.state.graphExists ? ["GitNexus is installed but no graph directory was found."] : [],
|
|
857
|
-
...!result.state.handoffPath ? ["Graph handoff instructions have not been generated."] : []
|
|
858
|
-
];
|
|
859
|
-
ctx.output.result({
|
|
860
|
-
ok: true,
|
|
861
|
-
command: "graph doctor",
|
|
862
|
-
summary: warnings.length ? `Graph doctor completed with ${warnings.length} warning(s).` : "Graph doctor completed without warnings.",
|
|
863
|
-
warnings,
|
|
864
|
-
nextSteps: warnings.length ? ["Run fet graph setup", "Run fet graph handoff", "Run fet graph init when GitNexus is installed"] : void 0,
|
|
865
|
-
data: result
|
|
866
|
-
});
|
|
867
|
-
}
|
|
868
|
-
async function graphSetupCommand(ctx) {
|
|
869
|
-
let result;
|
|
870
|
-
const handoffPath = join11(ctx.projectRoot, ".fet", "graph-setup.md");
|
|
871
|
-
await withProjectLock(ctx.projectRoot, { command: "graph setup", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
872
|
-
result = await refreshGraphState(ctx, { write: false });
|
|
873
|
-
await writeHandoffFile(handoffPath, renderGraphSetupHandoff(result.state));
|
|
874
|
-
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
875
|
-
global.graph ??= {};
|
|
876
|
-
global.graph.gitnexus = {
|
|
877
|
-
...result.state,
|
|
878
|
-
setupHandoffPath: ".fet/graph-setup.md",
|
|
879
|
-
setupHandoffUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
880
|
-
};
|
|
881
|
-
await ctx.stateStore.writeGlobal(global);
|
|
882
|
-
});
|
|
883
|
-
ctx.output.result({
|
|
884
|
-
ok: true,
|
|
885
|
-
command: "graph setup",
|
|
886
|
-
summary: "GitNexus setup handoff generated.",
|
|
887
|
-
warnings: result.state.installed ? [] : ["GitNexus is not installed. The handoff explains installation and IDE-assisted setup options."],
|
|
888
|
-
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"],
|
|
889
|
-
data: {
|
|
890
|
-
path: ".fet/graph-setup.md",
|
|
891
|
-
gitnexus: result.state
|
|
892
|
-
}
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
async function graphHandoffCommand(ctx) {
|
|
896
|
-
let result;
|
|
897
|
-
const handoffPath = join11(ctx.projectRoot, ".fet", "graph-handoff.md");
|
|
898
|
-
await withProjectLock(ctx.projectRoot, { command: "graph handoff", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
899
|
-
result = await refreshGraphState(ctx, { runStatus: true, write: false });
|
|
900
|
-
await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state));
|
|
901
|
-
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
902
|
-
global.graph ??= {};
|
|
903
|
-
global.graph.gitnexus = {
|
|
904
|
-
...result.state,
|
|
905
|
-
handoffPath: ".fet/graph-handoff.md",
|
|
906
|
-
handoffUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
907
|
-
};
|
|
908
|
-
await ctx.stateStore.writeGlobal(global);
|
|
909
|
-
});
|
|
910
|
-
ctx.output.result({
|
|
911
|
-
ok: true,
|
|
912
|
-
command: "graph handoff",
|
|
913
|
-
summary: "GitNexus graph usage handoff generated.",
|
|
914
|
-
warnings: result.state.installed ? [] : ["GitNexus is not installed. The handoff still documents the fallback behavior."],
|
|
915
|
-
nextSteps: ["Cursor/Codex/OpenCode: read .fet/graph-handoff.md before broad repository scans"],
|
|
916
|
-
data: {
|
|
917
|
-
path: ".fet/graph-handoff.md",
|
|
918
|
-
gitnexus: result.state
|
|
919
|
-
}
|
|
920
|
-
});
|
|
1019
|
+
function renderManagedBlock(content) {
|
|
1020
|
+
return `${BEGIN}
|
|
1021
|
+
${content}
|
|
1022
|
+
${END}`;
|
|
921
1023
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
code: "GRAPH_PROVIDER_NOT_FOUND" /* GraphProviderNotFound */,
|
|
927
|
-
message: "GitNexus is not installed or is not available on PATH.",
|
|
928
|
-
details: { executable: detection.executablePath, error: detection.error },
|
|
929
|
-
suggestedCommand: "fet graph setup"
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
const run = await runGitNexus(["analyze", ...args], { cwd: ctx.projectRoot });
|
|
933
|
-
if (run.exitCode !== 0) {
|
|
934
|
-
throw new FetError({
|
|
935
|
-
code: "GRAPH_COMMAND_FAILED" /* GraphCommandFailed */,
|
|
936
|
-
message: "GitNexus analyze failed.",
|
|
937
|
-
details: { command: run.command.join(" "), exitCode: run.exitCode, stdout: run.stdout, stderr: run.stderr },
|
|
938
|
-
suggestedCommand: "fet graph doctor"
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
const result = await refreshGraphState(ctx, { write: false });
|
|
942
|
-
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
943
|
-
global.graph ??= {};
|
|
944
|
-
global.graph.gitnexus = {
|
|
945
|
-
...result.state,
|
|
946
|
-
lastRefreshAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
947
|
-
};
|
|
948
|
-
await ctx.stateStore.writeGlobal(global);
|
|
949
|
-
ctx.output.result({
|
|
950
|
-
ok: true,
|
|
951
|
-
command: `graph ${mode}`,
|
|
952
|
-
summary: mode === "init" ? "GitNexus graph initialized." : "GitNexus graph refreshed.",
|
|
953
|
-
warnings: result.state.graphExists ? [] : ["GitNexus analyze completed, but the configured graph directory was not found."],
|
|
954
|
-
nextSteps: ["Run fet graph status", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"],
|
|
955
|
-
data: {
|
|
956
|
-
gitnexus: global.graph.gitnexus,
|
|
957
|
-
run: {
|
|
958
|
-
command: run.command,
|
|
959
|
-
stdout: run.stdout.trim(),
|
|
960
|
-
stderr: run.stderr.trim()
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
});
|
|
1024
|
+
function renderKarpathyClaudeGuidelines() {
|
|
1025
|
+
return `# Andrej Karpathy Inspired Coding Guidelines
|
|
1026
|
+
|
|
1027
|
+
${renderKarpathyGuidelinesBody()}`;
|
|
964
1028
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
const detection = await detectGitNexus();
|
|
969
|
-
const graph2 = await inspectGitNexusGraph(ctx.projectRoot);
|
|
970
|
-
let state = mergeGitNexusGraphInfo(toGitNexusState(detection, global.graph.gitnexus), graph2);
|
|
971
|
-
let gitnexusStatus = null;
|
|
972
|
-
if (options.runStatus && detection.installed) {
|
|
973
|
-
gitnexusStatus = await runGitNexus(["status"], { cwd: ctx.projectRoot });
|
|
974
|
-
state = {
|
|
975
|
-
...state,
|
|
976
|
-
lastStatus: firstLine(gitnexusStatus.stdout) || firstLine(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
if (options.write ?? true) {
|
|
980
|
-
global.graph.gitnexus = state;
|
|
981
|
-
await ctx.stateStore.writeGlobal(global);
|
|
1029
|
+
function renderKarpathyGuidelinesBody(language = "zh-CN") {
|
|
1030
|
+
if (language === "en") {
|
|
1031
|
+
return renderKarpathyGuidelinesBodyEn();
|
|
982
1032
|
}
|
|
983
|
-
return {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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`;
|
|
992
1066
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
|
1094
|
+
|
|
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.`;
|
|
996
1101
|
}
|
|
997
|
-
function renderGraphSetupHandoff(state) {
|
|
998
|
-
return `<!-- FET:MANAGED
|
|
999
|
-
schemaVersion: 1
|
|
1000
|
-
generator: graph-setup
|
|
1001
|
-
FET:END -->
|
|
1002
1102
|
|
|
1003
|
-
|
|
1103
|
+
// src/templates/verify-instructions.ts
|
|
1104
|
+
function renderVerifyInstructions(changeId, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
1105
|
+
return `---
|
|
1106
|
+
schemaVersion: 1
|
|
1107
|
+
fetVersion: ${FET_VERSION}
|
|
1108
|
+
generatedAt: ${generatedAt}
|
|
1109
|
+
changeId: ${changeId}
|
|
1110
|
+
purpose: manual-verify
|
|
1111
|
+
---
|
|
1004
1112
|
|
|
1005
|
-
|
|
1113
|
+
# Verify Instructions
|
|
1006
1114
|
|
|
1007
|
-
|
|
1115
|
+
\u8BF7\u6309\u987A\u5E8F\u5B8C\u6210\u4EE5\u4E0B\u68C0\u67E5\uFF1A
|
|
1008
1116
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
- Graph path: ${state.graphPath ?? ".gitnexus"}
|
|
1013
|
-
- Graph exists: ${state.graphExists ? "yes" : "no"}
|
|
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
|
|
1014
1120
|
|
|
1015
|
-
|
|
1121
|
+
\u5B8C\u6210\u540E\u8FD0\u884C\uFF1A
|
|
1016
1122
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1123
|
+
\`\`\`sh
|
|
1124
|
+
fet verify --done --change ${changeId}
|
|
1125
|
+
\`\`\`
|
|
1126
|
+
`;
|
|
1127
|
+
}
|
|
1021
1128
|
|
|
1022
|
-
|
|
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
|
+
`;
|
|
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*$/, "")}
|
|
1023
1154
|
|
|
1024
|
-
|
|
1025
|
-
- Do not generate or modify application code during setup.
|
|
1026
|
-
- Do not run global IDE configuration commands unless the user explicitly approves them.
|
|
1155
|
+
${block}
|
|
1027
1156
|
`;
|
|
1028
1157
|
}
|
|
1029
|
-
function renderGraphUsageHandoff(state) {
|
|
1030
|
-
return `<!-- FET:MANAGED
|
|
1031
|
-
schemaVersion: 1
|
|
1032
|
-
generator: graph-handoff
|
|
1033
|
-
FET:END -->
|
|
1034
|
-
|
|
1035
|
-
# FET Graph Handoff
|
|
1036
|
-
|
|
1037
|
-
Use GitNexus graph context as an optional first pass before broad repository scans.
|
|
1038
1158
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
- Graph path: ${state.graphPath ?? ".gitnexus"}
|
|
1043
|
-
- Graph exists: ${state.graphExists ? "yes" : "no"}
|
|
1044
|
-
- Last indexed at: ${state.lastIndexedAt ?? "unknown"}
|
|
1045
|
-
- Last status: ${state.lastStatus ?? "unknown"}
|
|
1046
|
-
|
|
1047
|
-
When graph context is available:
|
|
1159
|
+
// src/commands/update-context.ts
|
|
1160
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1161
|
+
import { join as join10 } from "path";
|
|
1048
1162
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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();
|
|
1178
|
+
}
|
|
1053
1179
|
|
|
1054
|
-
|
|
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);
|
|
1185
|
+
});
|
|
1186
|
+
ctx.output.result({
|
|
1187
|
+
ok: true,
|
|
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
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
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}`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
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
|
+
);
|
|
1236
|
+
}
|
|
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
|
|
1246
|
+
};
|
|
1247
|
+
await ctx.stateStore.writeGlobal(state);
|
|
1248
|
+
return { warnings };
|
|
1249
|
+
}
|
|
1250
|
+
async function readOptional(path) {
|
|
1251
|
+
try {
|
|
1252
|
+
return await readFile6(path, "utf8");
|
|
1253
|
+
} catch {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1055
1257
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
+
});
|
|
1060
1310
|
}
|
|
1061
|
-
function
|
|
1062
|
-
|
|
1311
|
+
async function ensureGitignore(ctx) {
|
|
1312
|
+
const gitignorePath = join11(ctx.projectRoot, ".gitignore");
|
|
1313
|
+
const existing = await readOptional2(gitignorePath);
|
|
1314
|
+
await atomicWrite(gitignorePath, mergeGitignore(existing));
|
|
1315
|
+
}
|
|
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
|
+
}
|
|
1063
1330
|
}
|
|
1064
1331
|
|
|
1065
1332
|
// src/commands/proxy.ts
|
|
@@ -1096,6 +1363,22 @@ async function git(cwd, args) {
|
|
|
1096
1363
|
import { mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
|
|
1097
1364
|
import { join as join12 } from "path";
|
|
1098
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
|
+
|
|
1099
1382
|
// src/state/schema.ts
|
|
1100
1383
|
var phases = ["explore", "propose", "implement", "verify", "sync", "archive"];
|
|
1101
1384
|
function createGlobalState(fetVersion, project) {
|
|
@@ -1106,6 +1389,10 @@ function createGlobalState(fetVersion, project) {
|
|
|
1106
1389
|
createdAt: now,
|
|
1107
1390
|
updatedAt: now,
|
|
1108
1391
|
project,
|
|
1392
|
+
language: {
|
|
1393
|
+
current: DEFAULT_LANGUAGE,
|
|
1394
|
+
updatedAt: now
|
|
1395
|
+
},
|
|
1109
1396
|
openspec: null,
|
|
1110
1397
|
activeChangeId: null,
|
|
1111
1398
|
openChangeIds: [],
|
|
@@ -1129,9 +1416,7 @@ function createChangeState(fetVersion, changeId, phase) {
|
|
|
1129
1416
|
createdAt: now,
|
|
1130
1417
|
updatedAt: now,
|
|
1131
1418
|
currentPhase: phase,
|
|
1132
|
-
phases: Object.fromEntries(
|
|
1133
|
-
phases.map((item) => [item, { status: item === phase ? "in_progress" : "not_started" }])
|
|
1134
|
-
),
|
|
1419
|
+
phases: Object.fromEntries(phases.map((item) => [item, { status: item === phase ? "in_progress" : "not_started" }])),
|
|
1135
1420
|
tasks: {
|
|
1136
1421
|
source: "tasks.md",
|
|
1137
1422
|
completedIds: [],
|
|
@@ -1146,6 +1431,12 @@ function assertGlobalState(value) {
|
|
|
1146
1431
|
if (!isRecord(value) || value.schemaVersion !== 1) {
|
|
1147
1432
|
throw unsupportedSchema("\u5168\u5C40\u72B6\u6001 schema \u4E0D\u53D7\u652F\u6301");
|
|
1148
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
|
+
}
|
|
1149
1440
|
if (typeof value.fetVersion !== "string" || !isRecord(value.project)) {
|
|
1150
1441
|
throw corruptedState("\u5168\u5C40\u72B6\u6001\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5");
|
|
1151
1442
|
}
|
|
@@ -1427,14 +1718,16 @@ function stripFetOptions(args) {
|
|
|
1427
1718
|
async function mapOpenSpecCommand(ctx, command, args) {
|
|
1428
1719
|
switch (command) {
|
|
1429
1720
|
case "propose":
|
|
1430
|
-
case "continue":
|
|
1431
|
-
case "ff":
|
|
1432
|
-
case "apply":
|
|
1433
|
-
case "sync":
|
|
1434
1721
|
case "bulk-archive":
|
|
1435
1722
|
case "explore":
|
|
1436
1723
|
case "onboard":
|
|
1437
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) };
|
|
1438
1731
|
case "new":
|
|
1439
1732
|
return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
|
|
1440
1733
|
case "archive":
|
|
@@ -1458,6 +1751,18 @@ async function mapOpenSpecCommand(ctx, command, args) {
|
|
|
1458
1751
|
function withGlobalChange(ctx, args) {
|
|
1459
1752
|
return ctx.changeId ? ["--change", ctx.changeId, ...args] : args;
|
|
1460
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
|
+
}
|
|
1461
1766
|
async function requireChangeId(ctx) {
|
|
1462
1767
|
if (ctx.changeId) {
|
|
1463
1768
|
return ctx.changeId;
|
|
@@ -1703,8 +2008,18 @@ function detectCurrentModel(env = process.env) {
|
|
|
1703
2008
|
function isHighCostModel(model) {
|
|
1704
2009
|
return HIGH_COST_MODEL_PATTERNS.some((pattern) => pattern.test(model));
|
|
1705
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
|
+
}
|
|
1706
2021
|
function getCommandModelPolicyMismatch(command, env = process.env) {
|
|
1707
|
-
if (env
|
|
2022
|
+
if (getModelPolicyMode(env) === "off") {
|
|
1708
2023
|
return null;
|
|
1709
2024
|
}
|
|
1710
2025
|
const detected = detectCurrentModel(env);
|
|
@@ -1733,15 +2048,26 @@ function getCommandModelPolicyMismatch(command, env = process.env) {
|
|
|
1733
2048
|
}
|
|
1734
2049
|
return null;
|
|
1735
2050
|
}
|
|
1736
|
-
function formatModelPolicyMismatch(mismatch) {
|
|
1737
|
-
|
|
1738
|
-
|
|
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}`;
|
|
1739
2059
|
}
|
|
1740
|
-
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
|
+
}
|
|
1741
2067
|
if (command === "apply") {
|
|
1742
|
-
return "
|
|
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";
|
|
1743
2069
|
}
|
|
1744
|
-
return "
|
|
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";
|
|
1745
2071
|
}
|
|
1746
2072
|
|
|
1747
2073
|
// src/cli/context.ts
|
|
@@ -1780,7 +2106,7 @@ function renderFetAdapterUsage(command, args = "[...args]") {
|
|
|
1780
2106
|
}
|
|
1781
2107
|
|
|
1782
2108
|
// src/adapters/codex/templates.ts
|
|
1783
|
-
function codexGuideFile() {
|
|
2109
|
+
function codexGuideFile(language = DEFAULT_LANGUAGE) {
|
|
1784
2110
|
return {
|
|
1785
2111
|
path: ".codex/fet/context.md",
|
|
1786
2112
|
content: `<!-- FET:MANAGED
|
|
@@ -1792,10 +2118,15 @@ FET:END -->
|
|
|
1792
2118
|
|
|
1793
2119
|
# FET For Codex
|
|
1794
2120
|
|
|
2121
|
+
## \u8BED\u8A00
|
|
2122
|
+
|
|
2123
|
+
${languageInstruction(language)}
|
|
2124
|
+
|
|
1795
2125
|
Before doing FET or OpenSpec work in Codex, read:
|
|
1796
2126
|
|
|
1797
2127
|
- AGENTS.md
|
|
1798
2128
|
- openspec/config.yaml
|
|
2129
|
+
- .codex/fet/karpathy-guidelines.md
|
|
1799
2130
|
- the active change files under openspec/changes/<change-id>/, when a change is selected
|
|
1800
2131
|
|
|
1801
2132
|
If GitNexus code graph context is available in the IDE or MCP tools, prefer it before broad repository scans. Use it to identify relevant modules, dependencies, and insertion points, then read only the concrete source files needed. If GitNexus is unavailable, continue with the normal FET/OpenSpec workflow.
|
|
@@ -1806,27 +2137,49 @@ Command guides live in .codex/fet/commands/.
|
|
|
1806
2137
|
`
|
|
1807
2138
|
};
|
|
1808
2139
|
}
|
|
1809
|
-
function codexCommandFiles() {
|
|
1810
|
-
return
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
2140
|
+
function codexCommandFiles(language = DEFAULT_LANGUAGE) {
|
|
2141
|
+
return [
|
|
2142
|
+
codexKarpathyGuidelinesFile(language),
|
|
2143
|
+
...FET_ADAPTER_COMMANDS.map((command) => ({
|
|
2144
|
+
path: `.codex/fet/commands/${command}.md`,
|
|
2145
|
+
content: renderCommand(command, language)
|
|
2146
|
+
}))
|
|
2147
|
+
];
|
|
1814
2148
|
}
|
|
1815
|
-
function codexSlashPromptFiles() {
|
|
2149
|
+
function codexSlashPromptFiles(language = DEFAULT_LANGUAGE) {
|
|
1816
2150
|
return FET_ADAPTER_COMMANDS.map((command) => ({
|
|
1817
2151
|
path: `prompts/fet-${command}.md`,
|
|
1818
|
-
content: renderSlashPrompt(command)
|
|
2152
|
+
content: renderSlashPrompt(command, language)
|
|
1819
2153
|
}));
|
|
1820
2154
|
}
|
|
1821
|
-
function
|
|
2155
|
+
function codexKarpathyGuidelinesFile(language) {
|
|
2156
|
+
return {
|
|
2157
|
+
path: ".codex/fet/karpathy-guidelines.md",
|
|
2158
|
+
content: `<!-- FET:MANAGED
|
|
2159
|
+
schemaVersion: 1
|
|
2160
|
+
fetVersion: ${FET_VERSION}
|
|
2161
|
+
generator: codex-adapter
|
|
2162
|
+
adapterVersion: 1
|
|
2163
|
+
FET:END -->
|
|
2164
|
+
|
|
2165
|
+
# Andrej Karpathy Inspired Coding Guidelines
|
|
2166
|
+
|
|
2167
|
+
${renderKarpathyGuidelinesBody(language)}
|
|
2168
|
+
`
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
function renderCommand(command, language) {
|
|
2172
|
+
if (language !== "en") {
|
|
2173
|
+
return renderCommandZh(command);
|
|
2174
|
+
}
|
|
1822
2175
|
if (command === "fill-context") {
|
|
1823
|
-
return renderFillContextCommand();
|
|
2176
|
+
return renderFillContextCommand(language);
|
|
1824
2177
|
}
|
|
1825
2178
|
if (command === "passthrough") {
|
|
1826
|
-
return renderPassthroughCommand();
|
|
2179
|
+
return renderPassthroughCommand(language);
|
|
1827
2180
|
}
|
|
1828
2181
|
if (command.startsWith("graph-")) {
|
|
1829
|
-
return renderGraphCommand(command);
|
|
2182
|
+
return renderGraphCommand(command, language);
|
|
1830
2183
|
}
|
|
1831
2184
|
const usage = renderFetAdapterUsage(command, "");
|
|
1832
2185
|
return `<!-- FET:MANAGED
|
|
@@ -1839,10 +2192,14 @@ FET:END -->
|
|
|
1839
2192
|
|
|
1840
2193
|
# ${usage}
|
|
1841
2194
|
|
|
1842
|
-
${renderIdeModelPolicy(command)}
|
|
2195
|
+
${renderIdeModelPolicy(command, language)}
|
|
2196
|
+
|
|
2197
|
+
${languageInstruction(language)}
|
|
1843
2198
|
|
|
1844
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.
|
|
1845
2200
|
|
|
2201
|
+
Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
|
|
2202
|
+
|
|
1846
2203
|
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.
|
|
1847
2204
|
|
|
1848
2205
|
Then run:
|
|
@@ -1856,7 +2213,41 @@ If the command needs a change id, pass it with \`--change <change-id>\` or use t
|
|
|
1856
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.
|
|
1857
2214
|
`;
|
|
1858
2215
|
}
|
|
1859
|
-
function
|
|
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) {
|
|
1860
2251
|
return `<!-- FET:MANAGED
|
|
1861
2252
|
schemaVersion: 1
|
|
1862
2253
|
fetVersion: ${FET_VERSION}
|
|
@@ -1867,10 +2258,14 @@ FET:END -->
|
|
|
1867
2258
|
|
|
1868
2259
|
# fet passthrough
|
|
1869
2260
|
|
|
1870
|
-
${renderIdeModelPolicy("passthrough")}
|
|
2261
|
+
${renderIdeModelPolicy("passthrough", language)}
|
|
2262
|
+
|
|
2263
|
+
${languageInstruction(language)}
|
|
1871
2264
|
|
|
1872
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.
|
|
1873
2266
|
|
|
2267
|
+
Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
|
|
2268
|
+
|
|
1874
2269
|
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.
|
|
1875
2270
|
|
|
1876
2271
|
Then run:
|
|
@@ -1882,7 +2277,7 @@ fet passthrough <openspec-command> [...args]
|
|
|
1882
2277
|
This preserves the FET entry point while allowing access to unmanaged or newly added OpenSpec commands. Passthrough does not update FET lifecycle state.
|
|
1883
2278
|
`;
|
|
1884
2279
|
}
|
|
1885
|
-
function renderGraphCommand(command) {
|
|
2280
|
+
function renderGraphCommand(command, language) {
|
|
1886
2281
|
const usage = renderFetAdapterUsage(command, "");
|
|
1887
2282
|
const subcommand = command.slice("graph-".length);
|
|
1888
2283
|
return `<!-- FET:MANAGED
|
|
@@ -1895,10 +2290,14 @@ FET:END -->
|
|
|
1895
2290
|
|
|
1896
2291
|
# ${usage}
|
|
1897
2292
|
|
|
1898
|
-
${renderIdeModelPolicy(command)}
|
|
2293
|
+
${renderIdeModelPolicy(command, language)}
|
|
2294
|
+
|
|
2295
|
+
${languageInstruction(language)}
|
|
1899
2296
|
|
|
1900
2297
|
When the user asks Codex to work with optional GitNexus graph support, use FET as the entry point.
|
|
1901
2298
|
|
|
2299
|
+
Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
|
|
2300
|
+
|
|
1902
2301
|
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.
|
|
1903
2302
|
|
|
1904
2303
|
Run:
|
|
@@ -1912,42 +2311,45 @@ For graph init or refresh, pass extra GitNexus analyze arguments only when the u
|
|
|
1912
2311
|
After the command completes, report the GitNexus state, generated handoff files, and next steps.
|
|
1913
2312
|
`;
|
|
1914
2313
|
}
|
|
1915
|
-
function renderSlashPrompt(command) {
|
|
2314
|
+
function renderSlashPrompt(command, language) {
|
|
2315
|
+
if (language !== "en") {
|
|
2316
|
+
return renderSlashPromptZh(command);
|
|
2317
|
+
}
|
|
1916
2318
|
if (command === "continue") {
|
|
1917
|
-
return renderContinueSlashPrompt();
|
|
2319
|
+
return renderContinueSlashPrompt(language);
|
|
1918
2320
|
}
|
|
1919
2321
|
if (command === "ff" || command === "propose") {
|
|
1920
|
-
return renderFastForwardSlashPrompt(command);
|
|
2322
|
+
return renderFastForwardSlashPrompt(command, language);
|
|
1921
2323
|
}
|
|
1922
2324
|
if (command === "explore") {
|
|
1923
|
-
return renderExploreSlashPrompt();
|
|
2325
|
+
return renderExploreSlashPrompt(language);
|
|
1924
2326
|
}
|
|
1925
2327
|
if (command === "new") {
|
|
1926
|
-
return renderNewSlashPrompt();
|
|
2328
|
+
return renderNewSlashPrompt(language);
|
|
1927
2329
|
}
|
|
1928
2330
|
if (command === "apply") {
|
|
1929
|
-
return renderApplySlashPrompt();
|
|
2331
|
+
return renderApplySlashPrompt(language);
|
|
1930
2332
|
}
|
|
1931
2333
|
if (command === "verify") {
|
|
1932
|
-
return renderVerifySlashPrompt();
|
|
2334
|
+
return renderVerifySlashPrompt(language);
|
|
1933
2335
|
}
|
|
1934
2336
|
if (command === "sync") {
|
|
1935
|
-
return renderSyncSlashPrompt();
|
|
2337
|
+
return renderSyncSlashPrompt(language);
|
|
1936
2338
|
}
|
|
1937
2339
|
if (command === "archive") {
|
|
1938
|
-
return renderArchiveSlashPrompt();
|
|
2340
|
+
return renderArchiveSlashPrompt(language);
|
|
1939
2341
|
}
|
|
1940
2342
|
if (command === "bulk-archive") {
|
|
1941
|
-
return renderBulkArchiveSlashPrompt();
|
|
2343
|
+
return renderBulkArchiveSlashPrompt(language);
|
|
1942
2344
|
}
|
|
1943
2345
|
if (command === "onboard") {
|
|
1944
|
-
return renderOnboardSlashPrompt();
|
|
2346
|
+
return renderOnboardSlashPrompt(language);
|
|
1945
2347
|
}
|
|
1946
2348
|
if (command === "fill-context") {
|
|
1947
|
-
return renderFillContextSlashPrompt();
|
|
2349
|
+
return renderFillContextSlashPrompt(language);
|
|
1948
2350
|
}
|
|
1949
2351
|
if (command === "passthrough") {
|
|
1950
|
-
return renderPassthroughSlashPrompt();
|
|
2352
|
+
return renderPassthroughSlashPrompt(language);
|
|
1951
2353
|
}
|
|
1952
2354
|
const usage = renderFetAdapterUsage(command);
|
|
1953
2355
|
const isGraph = command.startsWith("graph-");
|
|
@@ -1963,13 +2365,14 @@ FET:END -->
|
|
|
1963
2365
|
|
|
1964
2366
|
---
|
|
1965
2367
|
description: ${description}
|
|
1966
|
-
argument-hint: command arguments
|
|
1967
2368
|
---
|
|
1968
2369
|
|
|
1969
2370
|
Use FET as the entry point for this OpenSpec workflow.
|
|
1970
2371
|
|
|
1971
2372
|
Before running the command, make sure the relevant project context is loaded from AGENTS.md and openspec/config.yaml. If a change id is needed and was not provided, infer it from the active FET/OpenSpec state when unambiguous; otherwise ask the user for the change id.
|
|
1972
2373
|
|
|
2374
|
+
Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
|
|
2375
|
+
|
|
1973
2376
|
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.
|
|
1974
2377
|
|
|
1975
2378
|
Run:
|
|
@@ -1981,7 +2384,89 @@ ${shellCommand}
|
|
|
1981
2384
|
After it completes, summarize the important FET output and next steps.
|
|
1982
2385
|
`;
|
|
1983
2386
|
}
|
|
1984
|
-
function
|
|
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) {
|
|
1985
2470
|
return `<!-- FET:MANAGED
|
|
1986
2471
|
schemaVersion: 1
|
|
1987
2472
|
fetVersion: ${FET_VERSION}
|
|
@@ -1992,10 +2477,14 @@ FET:END -->
|
|
|
1992
2477
|
|
|
1993
2478
|
# fet fill-context
|
|
1994
2479
|
|
|
1995
|
-
${renderIdeModelPolicy("fill-context")}
|
|
2480
|
+
${renderIdeModelPolicy("fill-context", language)}
|
|
2481
|
+
|
|
2482
|
+
${languageInstruction(language)}
|
|
1996
2483
|
|
|
1997
2484
|
Use this command to complete FET-generated project context placeholders with Codex.
|
|
1998
2485
|
|
|
2486
|
+
Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
|
|
2487
|
+
|
|
1999
2488
|
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.
|
|
2000
2489
|
|
|
2001
2490
|
First run:
|
|
@@ -2007,7 +2496,7 @@ fet fill-context
|
|
|
2007
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.
|
|
2008
2497
|
`;
|
|
2009
2498
|
}
|
|
2010
|
-
function renderFillContextSlashPrompt() {
|
|
2499
|
+
function renderFillContextSlashPrompt(language) {
|
|
2011
2500
|
return renderManagedSlashPrompt(
|
|
2012
2501
|
"fet fill-context",
|
|
2013
2502
|
"Fill FET AGENTS.md placeholders with Codex",
|
|
@@ -2020,16 +2509,17 @@ Steps:
|
|
|
2020
2509
|
fet fill-context
|
|
2021
2510
|
\`\`\`
|
|
2022
2511
|
2. Read AGENTS.md and openspec/config.yaml.
|
|
2023
|
-
3.
|
|
2512
|
+
3. Read .codex/fet/karpathy-guidelines.md.
|
|
2513
|
+
4. Inspect the project to understand:
|
|
2024
2514
|
- source structure and major modules
|
|
2025
2515
|
- framework and routing conventions
|
|
2026
2516
|
- scripts, test commands, and build commands
|
|
2027
2517
|
- coding conventions and project-specific patterns
|
|
2028
2518
|
- important docs such as README files
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2519
|
+
5. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete, concise project-specific content.
|
|
2520
|
+
6. Preserve FET managed markers such as \`FET:BEGIN AUTO\`, \`FET:END AUTO\`, \`FET:BEGIN USER\`, and \`FET:END USER\`.
|
|
2521
|
+
7. Do not modify business code.
|
|
2522
|
+
8. Run:
|
|
2033
2523
|
\`\`\`sh
|
|
2034
2524
|
fet doctor
|
|
2035
2525
|
\`\`\`
|
|
@@ -2038,10 +2528,12 @@ Steps:
|
|
|
2038
2528
|
Guardrails:
|
|
2039
2529
|
- Do not invent facts that cannot be inferred from the repo.
|
|
2040
2530
|
- Use [UNKNOWN] only when the repository does not contain enough evidence.
|
|
2041
|
-
- 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
|
|
2042
2534
|
);
|
|
2043
2535
|
}
|
|
2044
|
-
function renderNewSlashPrompt() {
|
|
2536
|
+
function renderNewSlashPrompt(language) {
|
|
2045
2537
|
return renderManagedSlashPrompt(
|
|
2046
2538
|
"fet new [...args]",
|
|
2047
2539
|
"Create a new FET/OpenSpec change scaffold",
|
|
@@ -2070,20 +2562,22 @@ Steps:
|
|
|
2070
2562
|
Guardrails:
|
|
2071
2563
|
- Do not create artifact files in /prompts:fet-new.
|
|
2072
2564
|
- If the change already exists, suggest /prompts:fet-continue <change-id>.
|
|
2073
|
-
- 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
|
|
2074
2568
|
);
|
|
2075
2569
|
}
|
|
2076
|
-
function renderApplySlashPrompt() {
|
|
2570
|
+
function renderApplySlashPrompt(language) {
|
|
2077
2571
|
return renderManagedSlashPrompt(
|
|
2078
2572
|
"fet apply [...args]",
|
|
2079
2573
|
"Implement tasks from a FET/OpenSpec change",
|
|
2080
2574
|
`Implement a FET-managed OpenSpec change.
|
|
2081
2575
|
|
|
2082
|
-
Input after the slash command
|
|
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.
|
|
2083
2577
|
|
|
2084
2578
|
Steps:
|
|
2085
2579
|
|
|
2086
|
-
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.
|
|
2087
2581
|
2. Run the native OpenSpec apply flow through FET:
|
|
2088
2582
|
\`\`\`sh
|
|
2089
2583
|
fet apply --change <change-id> --json
|
|
@@ -2100,20 +2594,22 @@ Steps:
|
|
|
2100
2594
|
Guardrails:
|
|
2101
2595
|
- Never skip reading OpenSpec artifacts before implementation.
|
|
2102
2596
|
- Do not mark a task complete until the code change is actually done.
|
|
2103
|
-
- Do not run sync or archive from apply
|
|
2597
|
+
- Do not run sync or archive from apply.`,
|
|
2598
|
+
void 0,
|
|
2599
|
+
language
|
|
2104
2600
|
);
|
|
2105
2601
|
}
|
|
2106
|
-
function renderVerifySlashPrompt() {
|
|
2602
|
+
function renderVerifySlashPrompt(language) {
|
|
2107
2603
|
return renderManagedSlashPrompt(
|
|
2108
2604
|
"fet verify [...args]",
|
|
2109
2605
|
"Verify a FET/OpenSpec change before sync or archive",
|
|
2110
2606
|
`Verify a FET-managed OpenSpec change.
|
|
2111
2607
|
|
|
2112
|
-
Input after the slash command
|
|
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.
|
|
2113
2609
|
|
|
2114
2610
|
Steps:
|
|
2115
2611
|
|
|
2116
|
-
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.
|
|
2117
2613
|
2. Generate FET verification instructions:
|
|
2118
2614
|
\`\`\`sh
|
|
2119
2615
|
fet verify --change <change-id>
|
|
@@ -2133,20 +2629,22 @@ Steps:
|
|
|
2133
2629
|
Guardrails:
|
|
2134
2630
|
- Do not run --done before producing a verification assessment.
|
|
2135
2631
|
- Treat incomplete tasks or missing required behavior as critical unless user explicitly accepts them.
|
|
2136
|
-
- 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
|
|
2137
2635
|
);
|
|
2138
2636
|
}
|
|
2139
|
-
function renderSyncSlashPrompt() {
|
|
2637
|
+
function renderSyncSlashPrompt(language) {
|
|
2140
2638
|
return renderManagedSlashPrompt(
|
|
2141
2639
|
"fet sync [...args]",
|
|
2142
2640
|
"Sync delta specs and validate a FET/OpenSpec change",
|
|
2143
2641
|
`Sync a FET-managed OpenSpec change.
|
|
2144
2642
|
|
|
2145
|
-
Input after the slash command
|
|
2643
|
+
Input after the slash command may identify the change. If omitted, use the active OpenSpec change when it is unambiguous.
|
|
2146
2644
|
|
|
2147
2645
|
Steps:
|
|
2148
2646
|
|
|
2149
|
-
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.
|
|
2150
2648
|
2. Confirm FET verification is complete or run /prompts:fet-verify <change-id> first.
|
|
2151
2649
|
3. Find delta specs under openspec/changes/<change-id>/specs/*/spec.md.
|
|
2152
2650
|
4. If delta specs exist, intelligently merge them into openspec/specs/<capability>/spec.md:
|
|
@@ -2165,20 +2663,22 @@ Steps:
|
|
|
2165
2663
|
Guardrails:
|
|
2166
2664
|
- Read both delta and main specs before editing.
|
|
2167
2665
|
- Make sync idempotent where possible.
|
|
2168
|
-
- 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
|
|
2169
2669
|
);
|
|
2170
2670
|
}
|
|
2171
|
-
function renderArchiveSlashPrompt() {
|
|
2671
|
+
function renderArchiveSlashPrompt(language) {
|
|
2172
2672
|
return renderManagedSlashPrompt(
|
|
2173
2673
|
"fet archive [...args]",
|
|
2174
2674
|
"Archive a verified FET/OpenSpec change",
|
|
2175
2675
|
`Archive a FET-managed OpenSpec change.
|
|
2176
2676
|
|
|
2177
|
-
Input after the slash command
|
|
2677
|
+
Input after the slash command may identify the change. If omitted, use the active OpenSpec change when it is unambiguous.
|
|
2178
2678
|
|
|
2179
2679
|
Steps:
|
|
2180
2680
|
|
|
2181
|
-
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.
|
|
2182
2682
|
2. Check artifact and task status:
|
|
2183
2683
|
\`\`\`sh
|
|
2184
2684
|
fet passthrough status --change <change-id> --json
|
|
@@ -2195,10 +2695,12 @@ Steps:
|
|
|
2195
2695
|
Guardrails:
|
|
2196
2696
|
- Do not move change directories manually; use fet archive.
|
|
2197
2697
|
- Do not bypass the FET verify gate.
|
|
2198
|
-
- 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
|
|
2199
2701
|
);
|
|
2200
2702
|
}
|
|
2201
|
-
function renderBulkArchiveSlashPrompt() {
|
|
2703
|
+
function renderBulkArchiveSlashPrompt(language) {
|
|
2202
2704
|
return renderManagedSlashPrompt(
|
|
2203
2705
|
"fet bulk-archive [...args]",
|
|
2204
2706
|
"Archive multiple FET/OpenSpec changes safely",
|
|
@@ -2222,10 +2724,12 @@ Steps:
|
|
|
2222
2724
|
Guardrails:
|
|
2223
2725
|
- Never archive all changes without explicit user selection.
|
|
2224
2726
|
- Do not bypass verify or warnings for individual changes.
|
|
2225
|
-
- 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
|
|
2226
2730
|
);
|
|
2227
2731
|
}
|
|
2228
|
-
function renderOnboardSlashPrompt() {
|
|
2732
|
+
function renderOnboardSlashPrompt(language) {
|
|
2229
2733
|
return renderManagedSlashPrompt(
|
|
2230
2734
|
"fet onboard [...args]",
|
|
2231
2735
|
"Load FET/OpenSpec onboarding context",
|
|
@@ -2236,7 +2740,7 @@ Steps:
|
|
|
2236
2740
|
1. Read AGENTS.md and openspec/config.yaml.
|
|
2237
2741
|
2. Run FET onboarding:
|
|
2238
2742
|
\`\`\`sh
|
|
2239
|
-
fet onboard
|
|
2743
|
+
fet onboard
|
|
2240
2744
|
\`\`\`
|
|
2241
2745
|
3. Summarize:
|
|
2242
2746
|
- Project context.
|
|
@@ -2246,10 +2750,12 @@ Steps:
|
|
|
2246
2750
|
|
|
2247
2751
|
Guardrails:
|
|
2248
2752
|
- Do not create or modify artifacts during onboard.
|
|
2249
|
-
- 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
|
|
2250
2756
|
);
|
|
2251
2757
|
}
|
|
2252
|
-
function renderPassthroughSlashPrompt() {
|
|
2758
|
+
function renderPassthroughSlashPrompt(language) {
|
|
2253
2759
|
return renderManagedSlashPrompt(
|
|
2254
2760
|
"fet passthrough <openspec-command> [...args]",
|
|
2255
2761
|
"Run an unmanaged OpenSpec command through FET",
|
|
@@ -2267,10 +2773,12 @@ Steps:
|
|
|
2267
2773
|
Guardrails:
|
|
2268
2774
|
- Do not call openspec directly unless FET passthrough itself is unavailable.
|
|
2269
2775
|
- Remember that passthrough does not update FET lifecycle state.
|
|
2270
|
-
- 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
|
|
2271
2779
|
);
|
|
2272
2780
|
}
|
|
2273
|
-
function renderExploreSlashPrompt() {
|
|
2781
|
+
function renderExploreSlashPrompt(language) {
|
|
2274
2782
|
return renderManagedSlashPrompt(
|
|
2275
2783
|
"fet explore [...args]",
|
|
2276
2784
|
"Explore requirements for a FET/OpenSpec change",
|
|
@@ -2302,21 +2810,23 @@ Guardrails:
|
|
|
2302
2810
|
- Do not write application code in explore mode.
|
|
2303
2811
|
- Ask a clarifying question if the proposal would otherwise be mostly guesswork.
|
|
2304
2812
|
- Creating or updating OpenSpec artifacts is allowed when the user asks to capture the thinking.
|
|
2305
|
-
- 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
|
|
2306
2816
|
);
|
|
2307
2817
|
}
|
|
2308
|
-
function renderContinueSlashPrompt() {
|
|
2818
|
+
function renderContinueSlashPrompt(language) {
|
|
2309
2819
|
return renderManagedSlashPrompt(
|
|
2310
2820
|
"fet continue [...args]",
|
|
2311
2821
|
"Create the next FET/OpenSpec artifact",
|
|
2312
2822
|
`Continue a FET-managed OpenSpec change by creating exactly one ready artifact.
|
|
2313
2823
|
|
|
2314
|
-
Input after the slash command
|
|
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.
|
|
2315
2825
|
|
|
2316
2826
|
Steps:
|
|
2317
2827
|
|
|
2318
2828
|
1. Load project context from AGENTS.md and openspec/config.yaml.
|
|
2319
|
-
2. Resolve the change id
|
|
2829
|
+
2. Resolve the change id from the input, active FET/OpenSpec state, or the only open change. If ambiguous, ask the user.
|
|
2320
2830
|
3. Check status:
|
|
2321
2831
|
\`\`\`sh
|
|
2322
2832
|
fet passthrough status --change <change-id> --json
|
|
@@ -2324,7 +2834,7 @@ Steps:
|
|
|
2324
2834
|
4. Pick the first artifact whose status is ready, unless the user specified an artifact id.
|
|
2325
2835
|
5. Run the native OpenSpec continue flow through FET:
|
|
2326
2836
|
\`\`\`sh
|
|
2327
|
-
fet continue
|
|
2837
|
+
fet continue [artifact-id] --change <change-id> --json
|
|
2328
2838
|
\`\`\`
|
|
2329
2839
|
6. Follow the native continue output. When it provides template, instruction, dependencies, and outputPath, use those fields.
|
|
2330
2840
|
7. Read dependency files before writing.
|
|
@@ -2342,10 +2852,12 @@ Output:
|
|
|
2342
2852
|
Guardrails:
|
|
2343
2853
|
- Create one artifact per invocation.
|
|
2344
2854
|
- Never skip dependency order.
|
|
2345
|
-
- 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
|
|
2346
2858
|
);
|
|
2347
2859
|
}
|
|
2348
|
-
function renderFastForwardSlashPrompt(command) {
|
|
2860
|
+
function renderFastForwardSlashPrompt(command, language) {
|
|
2349
2861
|
const title = command === "propose" ? "Propose a new FET/OpenSpec change" : "Fast-forward FET/OpenSpec artifact creation";
|
|
2350
2862
|
const commandLine = command === "propose" ? "fet propose <change-id-or-description>" : "fet ff --change <change-id>";
|
|
2351
2863
|
return renderManagedSlashPrompt(
|
|
@@ -2353,7 +2865,7 @@ function renderFastForwardSlashPrompt(command) {
|
|
|
2353
2865
|
command === "propose" ? "Create a change and generate required OpenSpec artifacts" : "Generate required OpenSpec artifacts for a change",
|
|
2354
2866
|
`${title}.
|
|
2355
2867
|
|
|
2356
|
-
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.
|
|
2357
2869
|
|
|
2358
2870
|
Steps:
|
|
2359
2871
|
|
|
@@ -2376,11 +2888,15 @@ Output:
|
|
|
2376
2888
|
- Change id and location.
|
|
2377
2889
|
- Artifacts created.
|
|
2378
2890
|
- Current status.
|
|
2379
|
-
- Next recommended command, usually /prompts:fet-apply <change-id
|
|
2891
|
+
- Next recommended command, usually /prompts:fet-apply <change-id>.`,
|
|
2892
|
+
void 0,
|
|
2893
|
+
language
|
|
2380
2894
|
);
|
|
2381
2895
|
}
|
|
2382
|
-
function renderManagedSlashPrompt(command, description, body) {
|
|
2896
|
+
function renderManagedSlashPrompt(command, description, body, argumentHint, language = DEFAULT_LANGUAGE) {
|
|
2383
2897
|
const policyCommand = command.split(/\s+/)[1] ?? command;
|
|
2898
|
+
const argumentHintLine = argumentHint ? `argument-hint: ${argumentHint}
|
|
2899
|
+
` : "";
|
|
2384
2900
|
return `<!-- FET:MANAGED
|
|
2385
2901
|
schemaVersion: 1
|
|
2386
2902
|
fetVersion: ${FET_VERSION}
|
|
@@ -2391,10 +2907,11 @@ FET:END -->
|
|
|
2391
2907
|
|
|
2392
2908
|
---
|
|
2393
2909
|
description: ${description}
|
|
2394
|
-
|
|
2395
|
-
|
|
2910
|
+
${argumentHintLine}---
|
|
2911
|
+
|
|
2912
|
+
${renderIdeModelPolicy(policyCommand, language)}
|
|
2396
2913
|
|
|
2397
|
-
${
|
|
2914
|
+
${languageInstruction(language)}
|
|
2398
2915
|
|
|
2399
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.
|
|
2400
2917
|
|
|
@@ -2412,16 +2929,16 @@ var CodexAdapter = class {
|
|
|
2412
2929
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
2413
2930
|
};
|
|
2414
2931
|
}
|
|
2415
|
-
async planInstall(_projectRoot) {
|
|
2932
|
+
async planInstall(_projectRoot, language) {
|
|
2416
2933
|
return {
|
|
2417
2934
|
tool: this.tool,
|
|
2418
2935
|
files: [
|
|
2419
|
-
...[codexGuideFile(), ...codexCommandFiles()].map((file) => ({
|
|
2936
|
+
...[codexGuideFile(language), ...codexCommandFiles(language)].map((file) => ({
|
|
2420
2937
|
...file,
|
|
2421
2938
|
managed: true,
|
|
2422
2939
|
root: "project"
|
|
2423
2940
|
})),
|
|
2424
|
-
...codexSlashPromptFiles().map((file) => ({
|
|
2941
|
+
...codexSlashPromptFiles(language).map((file) => ({
|
|
2425
2942
|
...file,
|
|
2426
2943
|
managed: true,
|
|
2427
2944
|
root: "codex-home"
|
|
@@ -2508,13 +3025,13 @@ import { mkdir as mkdir8, readFile as readFile13, stat as stat7 } from "fs/promi
|
|
|
2508
3025
|
import { dirname as dirname8, join as join16 } from "path";
|
|
2509
3026
|
|
|
2510
3027
|
// src/adapters/cursor/templates.ts
|
|
2511
|
-
function cursorSkillFiles() {
|
|
3028
|
+
function cursorSkillFiles(language = DEFAULT_LANGUAGE) {
|
|
2512
3029
|
return FET_ADAPTER_COMMANDS.map((command) => ({
|
|
2513
3030
|
path: `.cursor/skills/fet-${command}/SKILL.md`,
|
|
2514
|
-
content: renderSkill(command)
|
|
3031
|
+
content: renderSkill(command, language)
|
|
2515
3032
|
}));
|
|
2516
3033
|
}
|
|
2517
|
-
function cursorRuleFile() {
|
|
3034
|
+
function cursorRuleFile(language = DEFAULT_LANGUAGE) {
|
|
2518
3035
|
return {
|
|
2519
3036
|
path: ".cursor/rules/fet-context.mdc",
|
|
2520
3037
|
content: `<!-- FET:MANAGED
|
|
@@ -2525,22 +3042,24 @@ adapterVersion: 1
|
|
|
2525
3042
|
FET:END -->
|
|
2526
3043
|
|
|
2527
3044
|
---
|
|
2528
|
-
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"}
|
|
2529
3046
|
alwaysApply: false
|
|
2530
3047
|
---
|
|
2531
3048
|
|
|
3049
|
+
${languageInstruction(language)}
|
|
3050
|
+
|
|
2532
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
|
|
2533
3052
|
|
|
2534
3053
|
- AGENTS.md
|
|
2535
3054
|
- openspec/config.yaml
|
|
2536
|
-
- GitNexus
|
|
2537
|
-
- \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
|
|
2538
3057
|
|
|
2539
|
-
\u5982\u679C\u7528\u6237\u8F93\u5165\u7C7B\u4F3C \`/fet apply\` \u7684\u8BF7\u6C42\
|
|
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
|
|
2540
3059
|
`
|
|
2541
3060
|
};
|
|
2542
3061
|
}
|
|
2543
|
-
function renderSkill(command) {
|
|
3062
|
+
function renderSkill(command, language) {
|
|
2544
3063
|
const usage = renderFetAdapterUsage(command, command === "passthrough" ? "[...args]" : "");
|
|
2545
3064
|
if (command === "fill-context") {
|
|
2546
3065
|
return `<!-- FET:MANAGED
|
|
@@ -2553,22 +3072,22 @@ FET:END -->
|
|
|
2553
3072
|
|
|
2554
3073
|
---
|
|
2555
3074
|
name: fet-fill-context
|
|
2556
|
-
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"}
|
|
2557
3076
|
disable-model-invocation: false
|
|
2558
3077
|
---
|
|
2559
3078
|
|
|
2560
|
-
|
|
3079
|
+
${renderIdeModelPolicy(command, language)}
|
|
2561
3080
|
|
|
2562
|
-
${
|
|
3081
|
+
${languageInstruction(language)}
|
|
2563
3082
|
|
|
2564
|
-
|
|
3083
|
+
\u5982\u679C IDE \u547D\u4EE4\u9700\u8981\u5237\u65B0\uFF0C\u5148\u8FD0\u884C \`fet fill-context\`\u3002
|
|
2565
3084
|
|
|
2566
|
-
|
|
3085
|
+
\u7136\u540E\u9605\u8BFB\uFF1A
|
|
2567
3086
|
|
|
2568
3087
|
- AGENTS.md
|
|
2569
3088
|
- openspec/config.yaml
|
|
2570
3089
|
|
|
2571
|
-
|
|
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
|
|
2572
3091
|
`;
|
|
2573
3092
|
}
|
|
2574
3093
|
return `<!-- FET:MANAGED
|
|
@@ -2581,15 +3100,15 @@ FET:END -->
|
|
|
2581
3100
|
|
|
2582
3101
|
---
|
|
2583
3102
|
name: fet-${command}
|
|
2584
|
-
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`}
|
|
2585
3104
|
disable-model-invocation: true
|
|
2586
3105
|
---
|
|
2587
3106
|
|
|
2588
|
-
${renderIdeModelPolicy(command)}
|
|
3107
|
+
${renderIdeModelPolicy(command, language)}
|
|
2589
3108
|
|
|
2590
|
-
|
|
3109
|
+
${languageInstruction(language)}
|
|
2591
3110
|
|
|
2592
|
-
\
|
|
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
|
|
2593
3112
|
|
|
2594
3113
|
\u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
|
|
2595
3114
|
|
|
@@ -2597,7 +3116,7 @@ If GitNexus code graph context is available in Cursor, prefer it before broad re
|
|
|
2597
3116
|
${usage}
|
|
2598
3117
|
\`\`\`
|
|
2599
3118
|
|
|
2600
|
-
\u6267\u884C\u524D\u8BF7\u786E\u8BA4\u5DF2\u9605\u8BFB AGENTS.md \
|
|
3119
|
+
\u6267\u884C\u524D\u8BF7\u786E\u8BA4\u5DF2\u9605\u8BFB AGENTS.md \u548C openspec/config.yaml\u3002
|
|
2601
3120
|
`;
|
|
2602
3121
|
}
|
|
2603
3122
|
|
|
@@ -2611,10 +3130,10 @@ var CursorAdapter = class {
|
|
|
2611
3130
|
reason: "Cursor adapter is available for any project"
|
|
2612
3131
|
};
|
|
2613
3132
|
}
|
|
2614
|
-
async planInstall(_projectRoot) {
|
|
3133
|
+
async planInstall(_projectRoot, language) {
|
|
2615
3134
|
return {
|
|
2616
3135
|
tool: this.tool,
|
|
2617
|
-
files: [...cursorSkillFiles(), cursorRuleFile()].map((file) => ({
|
|
3136
|
+
files: [...cursorSkillFiles(language), cursorRuleFile(language)].map((file) => ({
|
|
2618
3137
|
...file,
|
|
2619
3138
|
managed: true
|
|
2620
3139
|
}))
|
|
@@ -2629,7 +3148,7 @@ var CursorAdapter = class {
|
|
|
2629
3148
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
2630
3149
|
throw new FetError({
|
|
2631
3150
|
code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
|
|
2632
|
-
message: "Cursor \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\u4E14\u4E0D\
|
|
3151
|
+
message: "Cursor \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u4E14\u4E0D\u7531 FET \u7BA1\u7406\u3002",
|
|
2633
3152
|
details: { path: file.path },
|
|
2634
3153
|
suggestedCommand: "fet init --yes"
|
|
2635
3154
|
});
|
|
@@ -2654,7 +3173,7 @@ var CursorAdapter = class {
|
|
|
2654
3173
|
checks.push({
|
|
2655
3174
|
id: `cursor:${file.path}`,
|
|
2656
3175
|
status: !content ? "warn" : managed && versionMatches ? "pass" : "warn",
|
|
2657
|
-
message: !content ? `${file.path} \u7F3A\u5931` : !managed ? `${file.path} \u5B58\u5728\u4F46\u4E0D\
|
|
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`,
|
|
2658
3177
|
suggestedCommand: !content || !managed || !versionMatches ? "fet init" : void 0
|
|
2659
3178
|
});
|
|
2660
3179
|
}
|
|
@@ -3097,10 +3616,12 @@ var ProjectScanner = class {
|
|
|
3097
3616
|
|
|
3098
3617
|
// src/cli/output.ts
|
|
3099
3618
|
var OutputWriter = class {
|
|
3100
|
-
constructor(json) {
|
|
3619
|
+
constructor(json, language = "zh-CN") {
|
|
3101
3620
|
this.json = json;
|
|
3621
|
+
this.language = language;
|
|
3102
3622
|
}
|
|
3103
3623
|
json;
|
|
3624
|
+
language;
|
|
3104
3625
|
info(message, details) {
|
|
3105
3626
|
if (!this.json) {
|
|
3106
3627
|
process.stdout.write(`${message}${formatDetails(details)}
|
|
@@ -3113,10 +3634,8 @@ var OutputWriter = class {
|
|
|
3113
3634
|
`);
|
|
3114
3635
|
return;
|
|
3115
3636
|
}
|
|
3116
|
-
|
|
3117
|
-
process.stderr.write(`\u8B66\u544A\uFF1A${message}${formatDetails(details)}
|
|
3637
|
+
process.stderr.write(`${this.language === "en" ? "Warning" : "\u8B66\u544A"}\uFF1A${message}${formatDetails(details)}
|
|
3118
3638
|
`);
|
|
3119
|
-
}
|
|
3120
3639
|
}
|
|
3121
3640
|
error(error) {
|
|
3122
3641
|
if (this.json) {
|
|
@@ -3124,17 +3643,17 @@ var OutputWriter = class {
|
|
|
3124
3643
|
`);
|
|
3125
3644
|
return;
|
|
3126
3645
|
}
|
|
3127
|
-
process.stderr.write(
|
|
3646
|
+
process.stderr.write(`${this.language === "en" ? "FET cannot continue" : "FET \u65E0\u6CD5\u7EE7\u7EED"}\uFF1A${error.message}
|
|
3128
3647
|
`);
|
|
3129
3648
|
if (error.details !== void 0) {
|
|
3130
3649
|
process.stderr.write(`
|
|
3131
|
-
\u8BE6\u60C5\uFF1A
|
|
3650
|
+
${this.language === "en" ? "Details" : "\u8BE6\u60C5"}\uFF1A
|
|
3132
3651
|
${formatBlock(error.details)}
|
|
3133
3652
|
`);
|
|
3134
3653
|
}
|
|
3135
3654
|
if (error.suggestedCommand) {
|
|
3136
3655
|
process.stderr.write(`
|
|
3137
|
-
\u5EFA\u8BAE\uFF1A
|
|
3656
|
+
${this.language === "en" ? "Suggestion" : "\u5EFA\u8BAE"}\uFF1A
|
|
3138
3657
|
${error.suggestedCommand}
|
|
3139
3658
|
`);
|
|
3140
3659
|
}
|
|
@@ -3148,11 +3667,13 @@ ${formatBlock(error.details)}
|
|
|
3148
3667
|
process.stdout.write(`${result.summary}
|
|
3149
3668
|
`);
|
|
3150
3669
|
for (const warning of result.warnings ?? []) {
|
|
3151
|
-
process.stdout.write(
|
|
3670
|
+
process.stdout.write(`${this.language === "en" ? "Warning" : "\u8B66\u544A"}\uFF1A${warning}
|
|
3152
3671
|
`);
|
|
3153
3672
|
}
|
|
3154
3673
|
if (result.nextSteps?.length) {
|
|
3155
|
-
process.stdout.write(
|
|
3674
|
+
process.stdout.write(`
|
|
3675
|
+
${this.language === "en" ? "Next steps" : "\u4E0B\u4E00\u6B65"}\uFF1A
|
|
3676
|
+
`);
|
|
3156
3677
|
for (const step of result.nextSteps) {
|
|
3157
3678
|
process.stdout.write(` ${step}
|
|
3158
3679
|
`);
|
|
@@ -3177,7 +3698,10 @@ function formatBlock(details) {
|
|
|
3177
3698
|
async function createCommandContext(command, options) {
|
|
3178
3699
|
const projectRoot = resolve(options.cwd ?? process.cwd());
|
|
3179
3700
|
const project = await detectProjectIdentity(projectRoot);
|
|
3180
|
-
const
|
|
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);
|
|
3181
3705
|
return {
|
|
3182
3706
|
command,
|
|
3183
3707
|
cwd: projectRoot,
|
|
@@ -3187,9 +3711,11 @@ async function createCommandContext(command, options) {
|
|
|
3187
3711
|
verbose: Boolean(options.verbose),
|
|
3188
3712
|
yes: Boolean(options.yes),
|
|
3189
3713
|
changeId: options.change,
|
|
3714
|
+
language,
|
|
3715
|
+
explicitLanguage: options.lang !== void 0,
|
|
3190
3716
|
fetVersion: FET_VERSION,
|
|
3191
3717
|
output,
|
|
3192
|
-
stateStore
|
|
3718
|
+
stateStore,
|
|
3193
3719
|
openSpec: new DefaultOpenSpecAdapter(),
|
|
3194
3720
|
scanner: new ProjectScanner(),
|
|
3195
3721
|
toolAdapters: [new CursorAdapter(), new CodexAdapter()]
|
|
@@ -3198,25 +3724,33 @@ async function createCommandContext(command, options) {
|
|
|
3198
3724
|
|
|
3199
3725
|
// src/cli/index.ts
|
|
3200
3726
|
var program = new Command();
|
|
3201
|
-
program.name("fet").description("
|
|
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");
|
|
3202
3728
|
addGlobalOptions(program.command("init")).description("\u521D\u59CB\u5316 FET + OpenSpec").action(wrap("init", initCommand));
|
|
3203
3729
|
addGlobalOptions(program.command("update-context")).description("\u66F4\u65B0\u9879\u76EE\u4E0A\u4E0B\u6587").action(wrap("update-context", updateContextCommand));
|
|
3204
|
-
addGlobalOptions(program.command("fill-context")).description("
|
|
3205
|
-
var graph = addGlobalOptions(program.command("graph").description("
|
|
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"));
|
|
3206
3732
|
for (const action of ["status", "setup", "doctor", "handoff"]) {
|
|
3207
|
-
addGlobalOptions(graph.command(action).description(
|
|
3733
|
+
addGlobalOptions(graph.command(action).description(`\u8FD0\u884C fet graph ${action}`)).action(wrap("graph", (ctx) => graphCommand(ctx, action)));
|
|
3208
3734
|
}
|
|
3209
3735
|
for (const action of ["init", "refresh"]) {
|
|
3210
|
-
addGlobalOptions(
|
|
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)));
|
|
3211
3739
|
}
|
|
3212
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(
|
|
3213
3741
|
wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
|
|
3214
3742
|
);
|
|
3215
|
-
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(
|
|
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
|
+
);
|
|
3216
3746
|
for (const command of ["explore", "propose", "new", "continue", "ff", "apply", "sync", "archive", "bulk-archive", "onboard"]) {
|
|
3217
|
-
addGlobalOptions(program.command(`${command} [args...]`).description(`\u4EE3\u7406\u6267\u884C openspec ${command}`).allowUnknownOption(true).passThroughOptions()).action(
|
|
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
|
+
);
|
|
3218
3750
|
}
|
|
3219
|
-
addGlobalOptions(program.command("passthrough <command> [args...]").description("\u900F\u4F20\u6682\u672A\u63A5\u7BA1\u7684 OpenSpec \u547D\u4EE4").allowUnknownOption(true).passThroughOptions()).action(
|
|
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
|
+
);
|
|
3220
3754
|
program.parseAsync(process.argv).catch((error) => {
|
|
3221
3755
|
const json = process.argv.includes("--json");
|
|
3222
3756
|
const output = new OutputWriter(json);
|
|
@@ -3230,7 +3764,7 @@ function wrap(command, handler) {
|
|
|
3230
3764
|
const opts = isCommandLike(maybeCommand) ? { ...maybeCommand.parent?.opts(), ...maybeCommand.opts() } : program.opts();
|
|
3231
3765
|
const ctx = await createCommandContext(command, { ...opts, ...extractGlobalOptions(args) });
|
|
3232
3766
|
try {
|
|
3233
|
-
await
|
|
3767
|
+
await handleModelPolicyRecommendation(ctx);
|
|
3234
3768
|
await warnIfContextPlaceholdersRemain(ctx);
|
|
3235
3769
|
await handler(ctx, ...args);
|
|
3236
3770
|
} catch (error) {
|
|
@@ -3240,45 +3774,53 @@ function wrap(command, handler) {
|
|
|
3240
3774
|
}
|
|
3241
3775
|
};
|
|
3242
3776
|
}
|
|
3243
|
-
async function
|
|
3777
|
+
async function handleModelPolicyRecommendation(ctx) {
|
|
3244
3778
|
const mismatch = getCommandModelPolicyMismatch(ctx.command);
|
|
3245
3779
|
if (!mismatch) {
|
|
3246
3780
|
return;
|
|
3247
3781
|
}
|
|
3248
|
-
const warning = formatModelPolicyMismatch(mismatch);
|
|
3249
|
-
|
|
3250
|
-
|
|
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) {
|
|
3251
3786
|
return;
|
|
3252
3787
|
}
|
|
3253
3788
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3254
3789
|
try {
|
|
3255
|
-
const
|
|
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();
|
|
3256
3792
|
if (answer !== "y" && answer !== "yes") {
|
|
3257
3793
|
throw new FetError({
|
|
3258
3794
|
code: "USER_CANCELLED" /* UserCancelled */,
|
|
3259
|
-
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",
|
|
3260
3796
|
details: { command: ctx.command, detected: mismatch.detected, recommended: mismatch.recommended },
|
|
3261
|
-
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`
|
|
3262
3798
|
});
|
|
3263
3799
|
}
|
|
3264
3800
|
} finally {
|
|
3265
3801
|
rl.close();
|
|
3266
3802
|
}
|
|
3267
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
|
+
}
|
|
3268
3810
|
async function warnIfContextPlaceholdersRemain(ctx) {
|
|
3269
3811
|
if (["init", "update-context", "fill-context", "doctor"].includes(ctx.command)) {
|
|
3270
3812
|
return;
|
|
3271
3813
|
}
|
|
3272
3814
|
const count2 = await countAgentsLlmPlaceholders(ctx.projectRoot);
|
|
3273
3815
|
if (count2 > 0) {
|
|
3274
|
-
ctx.output.warn(renderAgentsPlaceholderWarning(count2));
|
|
3816
|
+
ctx.output.warn(renderAgentsPlaceholderWarning(count2, ctx.language));
|
|
3275
3817
|
}
|
|
3276
3818
|
}
|
|
3277
3819
|
function isCommandLike(value) {
|
|
3278
3820
|
return value instanceof Command;
|
|
3279
3821
|
}
|
|
3280
3822
|
function addGlobalOptions(command) {
|
|
3281
|
-
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");
|
|
3282
3824
|
}
|
|
3283
3825
|
function extractGlobalOptions(args) {
|
|
3284
3826
|
const values = args.flatMap((arg) => Array.isArray(arg) ? arg : []);
|
|
@@ -3298,6 +3840,11 @@ function extractGlobalOptions(args) {
|
|
|
3298
3840
|
index += 1;
|
|
3299
3841
|
} else if (value.startsWith("--change=")) {
|
|
3300
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);
|
|
3301
3848
|
} else if (value === "--yes") {
|
|
3302
3849
|
options.yes = true;
|
|
3303
3850
|
} else if (value === "--json") {
|