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