@nick848/fet 1.1.11 → 1.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -3000,6 +3000,390 @@ When the artifact is \`specs/<capability>/spec.md\` (or you edit spec files in t
3000
3000
  3. \u82F1\u6587\u89C4\u8303\u53E5\u6709\u4EFB\u4F55\u53D8\u52A8\u65F6\uFF0C**\u540C\u4E00\u6B21\u7F16\u8F91**\u5FC5\u987B\u540C\u6B65\u66F4\u65B0\u5BF9\u5E94\u4E2D\u6587\u6CE8\u91CA\u3002${uiBlock}`;
3001
3001
  }
3002
3002
 
3003
+ // src/templates/write-boundary.ts
3004
+ var WRITE_BOUNDARY_ALLOW_PREFIXES = ["src/", "openspec/"];
3005
+ var WRITE_BOUNDARY_ROOT_CONFIG_EXACT = [
3006
+ ".gitignore",
3007
+ ".gitattributes",
3008
+ ".stylelintignore",
3009
+ ".eslintignore",
3010
+ ".prettierignore",
3011
+ ".editorconfig",
3012
+ ".npmrc",
3013
+ ".nvmrc",
3014
+ ".node-version",
3015
+ ".checkrc.js",
3016
+ "package.json",
3017
+ "package-lock.json",
3018
+ "pnpm-lock.yaml",
3019
+ "yarn.lock",
3020
+ "bun.lockb",
3021
+ "npm-shrinkwrap.json"
3022
+ ];
3023
+ var WRITE_BOUNDARY_ROOT_CONFIG_BASENAME_PREFIXES = [
3024
+ ".eslintrc",
3025
+ ".stylelintrc",
3026
+ ".prettierrc",
3027
+ ".stylelint",
3028
+ "eslint.config",
3029
+ "stylelint.config",
3030
+ "prettier.config",
3031
+ "tsconfig",
3032
+ "vitest.config",
3033
+ "vite.config",
3034
+ "tsup.config",
3035
+ "jest.config",
3036
+ "rollup.config",
3037
+ "webpack.config",
3038
+ "babel.config",
3039
+ "biome.json",
3040
+ "deno.json",
3041
+ "deno.jsonc"
3042
+ ];
3043
+ function renderRootConfigPathList(language) {
3044
+ const samples = language === "en" ? "`.gitignore`, `.stylelintignore`, `.stylelintrc*`, `.eslintrc*`, `eslint.config.*`, `.checkrc.js`, `package.json`, `package-lock.json`, `tsconfig*.json`, `.prettierrc*`, tool configs at repo root (`vitest.config.*`, `vite.config.*`, \u2026)" : "`.gitignore`\u3001`.stylelintignore`\u3001`.stylelintrc*`\u3001`.eslintrc*`\u3001`eslint.config.*`\u3001`.checkrc.js`\u3001`package.json`\u3001`package-lock.json`\u3001`tsconfig*.json`\u3001`.prettierrc*`\u3001\u4EE5\u53CA\u4ED3\u5E93\u6839\u76EE\u5F55\u4E0B\u7684 `vitest.config.*`\u3001`vite.config.*` \u7B49\u5DE5\u5177\u914D\u7F6E";
3045
+ return samples;
3046
+ }
3047
+ function renderWriteBoundaryGuardrail(language) {
3048
+ const rootList = renderRootConfigPathList(language);
3049
+ if (language === "en") {
3050
+ return `- Default write scope: \`src/**\`, \`openspec/**\`, and any \`**/.fet/**\`. All other paths need explicit user approval first.
3051
+ - **Repo-root config is high risk**: ${rootList} \u2014 list each file and why before editing; never change these silently.`;
3052
+ }
3053
+ return `- \u9ED8\u8BA4\u53EA\u5141\u8BB8\u4FEE\u6539 \`src/**\`\u3001\`openspec/**\` \u53CA\u4EFB\u610F \`**/.fet/**\`\uFF1B\u5176\u4F59\u8DEF\u5F84\u987B\u5148\u83B7\u7528\u6237\u660E\u786E\u540C\u610F\u3002
3054
+ - **\u4ED3\u5E93\u6839\u76EE\u5F55\u914D\u7F6E\u6587\u4EF6\u9AD8\u98CE\u9669**\uFF1A${rootList} \u2014 \u4FEE\u6539\u524D\u987B\u5217\u51FA\u6587\u4EF6\u4E0E\u539F\u56E0\uFF0C\u7981\u6B62\u64C5\u81EA\u6539\u52A8\u3002`;
3055
+ }
3056
+ function renderWriteBoundaryPolicyBody(language) {
3057
+ if (language === "en") {
3058
+ return `## Default allowed write scope
3059
+
3060
+ - \`src/**\` \u2014 application/library source
3061
+ - \`openspec/**\` \u2014 OpenSpec specs and change artifacts
3062
+ - \`**/.fet/**\` \u2014 per-change FET handoff files (including \`openspec/changes/<id>/.fet/\`)
3063
+
3064
+ ## Ask the user first
3065
+
3066
+ Before creating, editing, or deleting files **outside** the allowed scope:
3067
+
3068
+ 1. List every path you need to touch and why.
3069
+ 2. Wait for explicit user approval (do not assume silence means yes).
3070
+ 3. Prefer minimal diffs.
3071
+
3072
+ Common paths that need approval: \`tests/**\`, \`.github/**\`, \`.workflow/**\`, \`.cursor/**\`, \`.codex/**\`, \`AGENTS.md\`, \`README*\`, \`.env*\`.
3073
+
3074
+ ## Repo-root config (always ask \u2014 never silent edit)
3075
+
3076
+ These live at the **repository root** (no subdirectory). Treat every change as infrastructure impact:
3077
+
3078
+ ${renderRootConfigPathList(language)}
3079
+
3080
+ Workflow:
3081
+
3082
+ 1. State **exact filenames** (e.g. \`package.json\`, \`.eslintrc.js\`, \`.gitignore\`).
3083
+ 2. Explain **why** each file must change.
3084
+ 3. Wait for **explicit user approval** before writing.
3085
+ 4. Do not \u201Cfix lint/format\u201D by editing root configs unless the user requested that scope.
3086
+
3087
+ ## Forbidden without approval
3088
+
3089
+ - Dependency or lockfile changes (\`package.json\`, \`package-lock.json\`, etc.) unless the user asked
3090
+ - Lint/format/tooling config at repo root (\`.eslintrc*\`, \`.stylelint*\`, \`.checkrc.js\`, \`.gitignore\`, \u2026) unless the user asked
3091
+ - Secrets or credential files
3092
+ - Using shell redirects or destructive git commands to bypass this policy
3093
+
3094
+ ## Cursor enforcement
3095
+
3096
+ When \`.cursor/hooks/fet-guard-write-paths.mjs\` is installed, out-of-scope **write tools** trigger an approval prompt (\`permission: ask\`). Shell writes are gated by \`fet-guard-shell-writes.mjs\`. Rules alone are not sufficient\u2014keep hooks enabled after \`fet init\`.`;
3097
+ }
3098
+ return `## \u9ED8\u8BA4\u5141\u8BB8\u4FEE\u6539\u7684\u8303\u56F4
3099
+
3100
+ - \`src/**\` \u2014 \u4E1A\u52A1/\u5E93\u6E90\u7801
3101
+ - \`openspec/**\` \u2014 OpenSpec \u89C4\u8303\u4E0E change \u4EA7\u7269
3102
+ - \`**/.fet/**\` \u2014 \u5404 change \u7684 FET \u4EA4\u63A5\u6587\u4EF6\uFF08\u542B \`openspec/changes/<id>/.fet/\`\uFF09
3103
+
3104
+ ## \u987B\u5148\u5F81\u5F97\u7528\u6237\u540C\u610F
3105
+
3106
+ \u5728**\u5141\u8BB8\u8303\u56F4\u5916**\u521B\u5EFA\u3001\u4FEE\u6539\u6216\u5220\u9664\u6587\u4EF6\u4E4B\u524D\uFF1A
3107
+
3108
+ 1. \u5217\u51FA\u5C06\u8981\u4FEE\u6539\u7684\u8DEF\u5F84\u53CA\u539F\u56E0\u3002
3109
+ 2. \u7B49\u5F85\u7528\u6237\u660E\u786E\u540C\u610F\uFF08\u4E0D\u8981\u9ED8\u8BA4\u6C89\u9ED8\u5373\u540C\u610F\uFF09\u3002
3110
+ 3. \u4FDD\u6301\u6700\u5C0F\u6539\u52A8\u3002
3111
+
3112
+ \u5E38\u89C1\u9700\u5BA1\u6279\u8DEF\u5F84\uFF1A\`tests/**\`\u3001\`.github/**\`\u3001\`.workflow/**\`\u3001\`.cursor/**\`\u3001\`.codex/**\`\u3001\`AGENTS.md\`\u3001\`README*\`\u3001\`.env*\`\u3002
3113
+
3114
+ ## \u4ED3\u5E93\u6839\u76EE\u5F55\u914D\u7F6E\uFF08\u59CB\u7EC8\u987B\u8BE2\u95EE\uFF0C\u7981\u6B62\u9759\u9ED8\u4FEE\u6539\uFF09
3115
+
3116
+ \u4EE5\u4E0B\u6587\u4EF6\u4F4D\u4E8E**\u9879\u76EE\u6839\u76EE\u5F55**\uFF08\u8DEF\u5F84\u4E2D\u65E0\u5B50\u76EE\u5F55\uFF09\uFF0C\u4E00\u5F8B\u89C6\u4E3A\u57FA\u7840\u8BBE\u65BD\u7EA7\u6539\u52A8\uFF1A
3117
+
3118
+ ${renderRootConfigPathList(language)}
3119
+
3120
+ \u6D41\u7A0B\uFF1A
3121
+
3122
+ 1. \u660E\u786E\u5217\u51FA**\u5B8C\u6574\u6587\u4EF6\u540D**\uFF08\u5982 \`package.json\`\u3001\`.eslintrc.js\`\u3001\`.gitignore\`\uFF09\u3002
3123
+ 2. \u8BF4\u660E**\u6BCF\u9879\u4FEE\u6539\u7684\u539F\u56E0**\u3002
3124
+ 3. \u83B7\u5F97\u7528\u6237**\u660E\u786E\u540C\u610F**\u540E\u518D\u5199\u5165\u3002
3125
+ 4. \u4E0D\u8981\u4EE5\u300C\u987A\u4FBF\u4FEE lint/\u683C\u5F0F\u300D\u4E3A\u7531\u64C5\u81EA\u6539\u6839\u76EE\u5F55\u914D\u7F6E\u3002
3126
+
3127
+ ## \u672A\u7ECF\u540C\u610F\u7981\u6B62
3128
+
3129
+ - \u64C5\u81EA\u6539\u4F9D\u8D56\u6216\u9501\u6587\u4EF6\uFF08\`package.json\`\u3001\`package-lock.json\` \u7B49\uFF09\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42
3130
+ - \u64C5\u81EA\u6539\u6839\u76EE\u5F55 lint/\u683C\u5F0F\u5316/\u5DE5\u5177\u94FE\u914D\u7F6E\uFF08\`.eslintrc*\`\u3001\`.stylelint*\`\u3001\`.checkrc.js\`\u3001\`.gitignore\` \u7B49\uFF09\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42
3131
+ - \u4FEE\u6539\u5BC6\u94A5\u6216\u51ED\u8BC1\u6587\u4EF6
3132
+ - \u7528 shell \u91CD\u5B9A\u5411\u6216\u7834\u574F\u6027 git \u547D\u4EE4\u7ED5\u8FC7\u672C\u7B56\u7565
3133
+
3134
+ ## Cursor \u786C\u95E8\u7981
3135
+
3136
+ \u5B89\u88C5 \`.cursor/hooks/fet-guard-write-paths.mjs\` \u540E\uFF0C\u8D85\u51FA\u8303\u56F4\u7684**\u5199\u5DE5\u5177**\u4F1A\u5F39\u51FA\u5BA1\u6279\uFF08\`permission: ask\`\uFF09\uFF1B\`fet-guard-shell-writes.mjs\` \u7EA6\u675F\u53EF\u80FD\u5199\u6587\u4EF6\u7684 shell\u3002\u4EC5\u89C4\u5219\u4E0D\u591F\u2014\u2014\`fet init\` \u540E\u8BF7\u4FDD\u6301 hooks \u542F\u7528\u3002`;
3137
+ }
3138
+ function renderCursorWriteBoundaryRule(language) {
3139
+ const description = language === "en" ? "FET write boundary: default edits only under src/, openspec/, and .fet/" : "FET \u5199\u8DEF\u5F84\u8FB9\u754C\uFF1A\u9ED8\u8BA4\u4EC5\u53EF\u6539 src/\u3001openspec/\u3001.fet/";
3140
+ return `<!-- FET:MANAGED
3141
+ schemaVersion: 1
3142
+ fetVersion: ${FET_VERSION}
3143
+ generator: cursor-adapter
3144
+ adapterVersion: 1
3145
+ FET:END -->
3146
+
3147
+ ---
3148
+ description: ${description}
3149
+ alwaysApply: true
3150
+ ---
3151
+
3152
+ ${renderWriteBoundaryPolicyBody(language)}
3153
+
3154
+ ${renderWriteBoundaryGuardrail(language)}
3155
+ `;
3156
+ }
3157
+ function renderCodexWriteBoundaryGuide(language) {
3158
+ return `<!-- FET:MANAGED
3159
+ schemaVersion: 1
3160
+ fetVersion: ${FET_VERSION}
3161
+ generator: codex-adapter
3162
+ adapterVersion: 1
3163
+ FET:END -->
3164
+
3165
+ # ${language === "en" ? "Write path boundary (Codex)" : "\u5199\u8DEF\u5F84\u8FB9\u754C\uFF08Codex\uFF09"}
3166
+
3167
+ ${renderWriteBoundaryPolicyBody(language)}
3168
+
3169
+ ${renderWriteBoundaryGuardrail(language)}
3170
+ `;
3171
+ }
3172
+ function renderCursorWritePathsHookMjs() {
3173
+ const allowPrefixes = [...WRITE_BOUNDARY_ALLOW_PREFIXES];
3174
+ const rootExact = [...WRITE_BOUNDARY_ROOT_CONFIG_EXACT];
3175
+ const rootPrefixes = [...WRITE_BOUNDARY_ROOT_CONFIG_BASENAME_PREFIXES];
3176
+ return `#!/usr/bin/env node
3177
+ /**
3178
+ * FET:MANAGED
3179
+ * adapterVersion: 1
3180
+ * write-path guard for Cursor preToolUse (Write/StrReplace/EditNotebook/Delete).
3181
+ */
3182
+ import { readFileSync } from "node:fs";
3183
+
3184
+ const ALLOW_PREFIXES = ${JSON.stringify(allowPrefixes)};
3185
+ const ROOT_CONFIG_EXACT = ${JSON.stringify(rootExact)};
3186
+ const ROOT_CONFIG_PREFIXES = ${JSON.stringify(rootPrefixes)};
3187
+
3188
+ function normalizePath(path) {
3189
+ return String(path ?? "")
3190
+ .replaceAll("\\\\", "/")
3191
+ .replace(/^\\.\\/+/, "")
3192
+ .replace(/^\\/+/u, "");
3193
+ }
3194
+
3195
+ function isRootConfigPath(path) {
3196
+ const normalized = normalizePath(path);
3197
+ if (!normalized || normalized.includes("/")) return false;
3198
+ const base = normalized.toLowerCase();
3199
+ if (ROOT_CONFIG_EXACT.includes(base)) return true;
3200
+ return ROOT_CONFIG_PREFIXES.some((prefix) => base === prefix || base.startsWith(prefix));
3201
+ }
3202
+
3203
+ function classifyPath(path) {
3204
+ const normalized = normalizePath(path);
3205
+ if (!normalized) return "ask";
3206
+ for (const prefix of ALLOW_PREFIXES) {
3207
+ const bare = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
3208
+ if (normalized === bare || normalized.startsWith(prefix)) return "allow";
3209
+ }
3210
+ if (normalized.startsWith(".fet/") || normalized.includes("/.fet/")) return "allow";
3211
+ if (isRootConfigPath(normalized)) return "root_config";
3212
+ return "ask";
3213
+ }
3214
+
3215
+ function extractPaths(payload) {
3216
+ const paths = [];
3217
+ const tool = payload?.tool_name ?? payload?.toolName ?? "";
3218
+ const input = payload?.tool_input ?? payload?.toolInput ?? payload?.input ?? {};
3219
+ if (tool === "Write" || tool === "StrReplace" || tool === "Delete") {
3220
+ if (input.path) paths.push(input.path);
3221
+ }
3222
+ if (tool === "EditNotebook" && input.target_notebook) {
3223
+ paths.push(input.target_notebook);
3224
+ }
3225
+ return paths;
3226
+ }
3227
+
3228
+ function respond(decision, paths) {
3229
+ if (decision === "allow") {
3230
+ process.stdout.write(JSON.stringify({ permission: "allow" }));
3231
+ return;
3232
+ }
3233
+ const list = paths.map((p) => normalizePath(p)).filter(Boolean).join(", ") || "(unknown path)";
3234
+ const hasRootConfig = paths.some((p) => classifyPath(p) === "root_config");
3235
+ const msg = hasRootConfig
3236
+ ? "FET: repo-root config file(s) require your explicit approval (.gitignore, package.json, .eslintrc*, .stylelint*, .checkrc.js, lockfiles, etc.). Approve only if you requested this tooling change."
3237
+ : "FET write boundary: this edit is outside src/, openspec/, or **/.fet/. Approve only if you intend to modify protected paths.";
3238
+ process.stdout.write(
3239
+ JSON.stringify({
3240
+ permission: "ask",
3241
+ user_message: msg + " Paths: " + list,
3242
+ agent_message: hasRootConfig
3243
+ ? "Repo-root config edit blocked. Explain why each root config file must change and wait for user approval."
3244
+ : "Out-of-scope write blocked pending user approval. List why each path is needed, then retry after approval."
3245
+ })
3246
+ );
3247
+ }
3248
+
3249
+ const raw = readFileSync(0, "utf8");
3250
+ let payload = {};
3251
+ try {
3252
+ payload = JSON.parse(raw || "{}");
3253
+ } catch {
3254
+ process.stdout.write(JSON.stringify({ permission: "ask", user_message: "FET write guard: invalid hook payload." }));
3255
+ process.exit(0);
3256
+ }
3257
+
3258
+ const paths = extractPaths(payload);
3259
+ if (paths.length === 0) {
3260
+ respond("allow", paths);
3261
+ process.exit(0);
3262
+ }
3263
+
3264
+ const decisions = paths.map((p) => classifyPath(p));
3265
+ if (decisions.every((d) => d === "allow")) {
3266
+ respond("allow", paths);
3267
+ } else {
3268
+ respond("ask", paths);
3269
+ }
3270
+ process.exit(0);
3271
+ `;
3272
+ }
3273
+ function renderCursorShellWriteHookMjs() {
3274
+ return `#!/usr/bin/env node
3275
+ /**
3276
+ * FET:MANAGED
3277
+ * adapterVersion: 1
3278
+ * shell guard for commands that may write outside the FET write boundary.
3279
+ */
3280
+ import { readFileSync } from "node:fs";
3281
+
3282
+ const REDIRECT = />\\s*[^\\s|&;]+/;
3283
+ const GIT_WRITE = /\\bgit\\s+(checkout|restore|reset|clean|apply)\\b/i;
3284
+ const PKG_WRITE = /\\b(npm|pnpm|yarn|bun)\\s+(install|ci|add|remove|update)\\b/i;
3285
+ const STREAM_EDIT = /\\b(sed|perl)\\s+[^\\n]*-i\\b/i;
3286
+ const TEE = /\\btee\\s+[^|;&\\n]+/i;
3287
+
3288
+ function respondAsk(command, reason) {
3289
+ process.stdout.write(
3290
+ JSON.stringify({
3291
+ permission: "ask",
3292
+ user_message: "FET shell guard: " + reason,
3293
+ agent_message: "Shell command may modify files outside src/openspec/.fet. Command: " + command
3294
+ })
3295
+ );
3296
+ }
3297
+
3298
+ const raw = readFileSync(0, "utf8");
3299
+ let payload = {};
3300
+ try {
3301
+ payload = JSON.parse(raw || "{}");
3302
+ } catch {
3303
+ respondAsk("", "invalid hook payload");
3304
+ process.exit(0);
3305
+ }
3306
+
3307
+ const command = String(payload?.command ?? "");
3308
+ if (!command.trim()) {
3309
+ process.stdout.write(JSON.stringify({ permission: "allow" }));
3310
+ process.exit(0);
3311
+ }
3312
+
3313
+ if (
3314
+ REDIRECT.test(command) ||
3315
+ GIT_WRITE.test(command) ||
3316
+ PKG_WRITE.test(command) ||
3317
+ STREAM_EDIT.test(command) ||
3318
+ TEE.test(command)
3319
+ ) {
3320
+ respondAsk(command, "command may write or replace project files outside the default FET scope");
3321
+ process.exit(0);
3322
+ }
3323
+
3324
+ process.stdout.write(JSON.stringify({ permission: "allow" }));
3325
+ process.exit(0);
3326
+ `;
3327
+ }
3328
+ function renderCursorHooksJson() {
3329
+ return JSON.stringify(
3330
+ {
3331
+ version: 1,
3332
+ _fet: {
3333
+ writeBoundary: true,
3334
+ fetVersion: FET_VERSION
3335
+ },
3336
+ hooks: {
3337
+ preToolUse: [
3338
+ {
3339
+ command: ".cursor/hooks/fet-guard-write-paths.mjs",
3340
+ matcher: "Write|StrReplace|EditNotebook|Delete",
3341
+ failClosed: true
3342
+ }
3343
+ ],
3344
+ beforeShellExecution: [
3345
+ {
3346
+ command: ".cursor/hooks/fet-guard-shell-writes.mjs",
3347
+ failClosed: true
3348
+ }
3349
+ ]
3350
+ }
3351
+ },
3352
+ null,
3353
+ 2
3354
+ );
3355
+ }
3356
+ function mergeCursorHooksJson(existingContent, fetContent) {
3357
+ const fet = JSON.parse(fetContent);
3358
+ if (!existingContent?.trim()) {
3359
+ return fetContent;
3360
+ }
3361
+ let existing;
3362
+ try {
3363
+ existing = JSON.parse(existingContent);
3364
+ } catch {
3365
+ return fetContent;
3366
+ }
3367
+ const merged = {
3368
+ version: existing.version ?? fet.version ?? 1,
3369
+ _fet: { ...existing._fet, ...fet._fet, writeBoundary: true },
3370
+ hooks: { ...existing.hooks }
3371
+ };
3372
+ for (const [event, fetEntries] of Object.entries(fet.hooks ?? {})) {
3373
+ const current = [...merged.hooks?.[event] ?? []];
3374
+ for (const entry of fetEntries) {
3375
+ const duplicate = current.some((item) => item.command === entry.command);
3376
+ if (!duplicate) {
3377
+ current.push(entry);
3378
+ }
3379
+ }
3380
+ merged.hooks = merged.hooks ?? {};
3381
+ merged.hooks[event] = current;
3382
+ }
3383
+ return `${JSON.stringify(merged, null, 2)}
3384
+ `;
3385
+ }
3386
+
3003
3387
  // src/commands/update-context.ts
3004
3388
  async function updateContextCommand(ctx) {
3005
3389
  let contextResult = { warnings: [] };
@@ -6385,6 +6769,7 @@ Before doing FET or OpenSpec work in Codex, read:
6385
6769
  - .codex/fet/spec-language.md when writing or updating OpenSpec specs
6386
6770
  - openspec/changes/<change-id>/.fet/figma-apply-instructions.md before UI work when FET apply reports Figma links
6387
6771
  - .codex/fet/ui-display-contract.md when UI binds API data; openspec/changes/<change-id>/.fet/ui-display-contract.yaml when present
6772
+ - .codex/fet/write-boundary.md for default edit scope (src/, openspec/, .fet/ only; ask before other paths)
6388
6773
  - the active change files under openspec/changes/<change-id>/, when a change is selected
6389
6774
 
6390
6775
  If GitNexus code graph context is available in the IDE or MCP tools, prefer it before broad repository scans. Use it to identify relevant modules, dependencies, and insertion points, then read only the concrete source files needed. If GitNexus is unavailable, continue with the normal FET/OpenSpec workflow.
@@ -6406,6 +6791,7 @@ ${languageInstruction(language)}
6406
6791
  - \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9605\u8BFB .codex/fet/figma-stop.md\uFF1B\u6709 figma-apply-instructions.md \u65F6\u5FC5\u987B\u5728\u6539 UI \u524D\u5B8C\u6574\u6267\u884C
6407
6792
  - \u7F16\u5199\u6216\u66F4\u65B0 spec \u65F6\u9605\u8BFB .codex/fet/spec-language.md\uFF08\u82F1\u6587\u89C4\u8303 + \u540C\u6B21\u7F16\u8F91\u7EF4\u62A4\u4E2D\u6587\u6CE8\u91CA\uFF09
6408
6793
  - \u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u9605\u8BFB .codex/fet/ui-display-contract.md\uFF1B\u5B58\u5728 ui-display-contract.yaml \u65F6\u5B9E\u65BD\u524D\u987B\u786E\u8BA4 displayFields
6794
+ - \u4FEE\u6539\u6587\u4EF6\u524D\u9605\u8BFB .codex/fet/write-boundary.md\uFF08\u9ED8\u8BA4\u4EC5 src/\u3001openspec/\u3001.fet/\uFF1B\u5176\u5B83\u8DEF\u5F84\u987B\u7528\u6237\u660E\u786E\u540C\u610F\uFF09
6409
6795
  - \u5982\u679C\u5DF2\u9009\u62E9 change\uFF0C\u9605\u8BFB openspec/changes/<change-id>/ \u4E0B\u7684\u5F53\u524D\u4EA7\u7269
6410
6796
 
6411
6797
  \u5982\u679C IDE \u6216 MCP \u5DE5\u5177\u4E2D\u53EF\u7528 GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\uFF0C\u5148\u7528\u5B83\u7F29\u5C0F\u4ED3\u5E93\u626B\u63CF\u8303\u56F4\uFF1B\u7528\u56FE\u8BC6\u522B\u76F8\u5173\u6A21\u5757\u3001\u4F9D\u8D56\u548C\u63D2\u5165\u70B9\uFF0C\u518D\u53EA\u8BFB\u53D6\u9700\u8981\u786E\u8BA4\u884C\u4E3A\u7684\u5177\u4F53\u6E90\u7801\u6587\u4EF6\u3002GitNexus \u4E0D\u53EF\u7528\u65F6\uFF0C\u6309\u666E\u901A FET/OpenSpec \u5DE5\u4F5C\u6D41\u7EE7\u7EED\u3002
@@ -6444,12 +6830,19 @@ function codexUiDisplayContractFile(language = DEFAULT_LANGUAGE) {
6444
6830
  content: renderCodexUiDisplayContractGuide(language)
6445
6831
  };
6446
6832
  }
6833
+ function codexWriteBoundaryFile(language = DEFAULT_LANGUAGE) {
6834
+ return {
6835
+ path: ".codex/fet/write-boundary.md",
6836
+ content: renderCodexWriteBoundaryGuide(language)
6837
+ };
6838
+ }
6447
6839
  function codexCommandFiles(language = DEFAULT_LANGUAGE) {
6448
6840
  return [
6449
6841
  codexKarpathyGuidelinesFile(language),
6450
6842
  codexFigmaStopFile(language),
6451
6843
  codexUiDisplayContractFile(language),
6452
6844
  codexSpecLanguageFile(language),
6845
+ codexWriteBoundaryFile(language),
6453
6846
  ...FET_ADAPTER_COMMANDS.map((command) => ({
6454
6847
  path: `.codex/fet/commands/${command}.md`,
6455
6848
  content: renderCommand(command, language)
@@ -6491,6 +6884,9 @@ function renderCommand(command, language) {
6491
6884
  if (command.startsWith("graph-")) {
6492
6885
  return renderGraphCommand(command, language);
6493
6886
  }
6887
+ if (command === "tdd" || command === "test" || command === "visual") {
6888
+ return renderStandaloneWorkflowCommand(command, language);
6889
+ }
6494
6890
  const usage = renderFetAdapterUsage(command, "");
6495
6891
  return `<!-- FET:MANAGED
6496
6892
  schemaVersion: 1
@@ -6521,11 +6917,16 @@ ${usage}
6521
6917
  If the command needs a change id, pass it with \`--change <change-id>\` or use the active OpenSpec change from the user's request.
6522
6918
 
6523
6919
  After the command completes, report the important next steps from the FET output and keep any generated OpenSpec artifacts in the normal project workflow.
6920
+
6921
+ ${renderWriteBoundaryGuardrail(language)}
6524
6922
  `;
6525
6923
  }
6526
6924
  function renderCommandZh(command) {
6527
6925
  const usage = renderFetAdapterUsage(command, command === "fill-context" ? "" : command === "passthrough" ? "<openspec-command> [...args]" : "");
6528
6926
  const title = commandTitleZh(command);
6927
+ if (command === "tdd" || command === "test" || command === "visual") {
6928
+ return renderStandaloneWorkflowCommand(command, "zh-CN");
6929
+ }
6529
6930
  if (command === "graph-setup") {
6530
6931
  return `<!-- FET:MANAGED
6531
6932
  schemaVersion: 1
@@ -6583,6 +6984,8 @@ ${usage}
6583
6984
  \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
6584
6985
 
6585
6986
  \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
6987
+
6988
+ ${renderWriteBoundaryGuardrail("zh-CN")}
6586
6989
  `;
6587
6990
  }
6588
6991
  function renderPassthroughCommand(language) {
@@ -6699,6 +7102,9 @@ function renderSlashPrompt(command, language) {
6699
7102
  if (command === "apply") {
6700
7103
  return renderApplySlashPrompt(language);
6701
7104
  }
7105
+ if (command === "tdd" || command === "test" || command === "visual") {
7106
+ return renderStandaloneWorkflowSlashPrompt(command, language);
7107
+ }
6702
7108
  if (command === "verify") {
6703
7109
  return renderVerifySlashPrompt(language);
6704
7110
  }
@@ -6760,6 +7166,9 @@ function renderSlashPromptZh(command) {
6760
7166
  if (command === "graph-setup") {
6761
7167
  return renderGraphSetupSlashPrompt("zh-CN");
6762
7168
  }
7169
+ if (command === "tdd" || command === "test" || command === "visual") {
7170
+ return renderStandaloneWorkflowSlashPrompt(command, "zh-CN");
7171
+ }
6763
7172
  const usage = renderFetAdapterUsage(command, command === "fill-context" ? "" : command === "passthrough" ? "<openspec-command> [...args]" : "[...args]");
6764
7173
  const argumentHint = command === "passthrough" ? "openspec-command [...args]" : void 0;
6765
7174
  const argumentHintLine = argumentHint ? `argument-hint: ${argumentHint}
@@ -6800,9 +7209,10 @@ ${commandGoalZh(command)}
6800
7209
  - \u9ED8\u8BA4\u4F7F\u7528\u4E2D\u6587\u4EA7\u51FA\u3002
6801
7210
  - \u4E0D\u8981\u7ED5\u8FC7 FET \u76F4\u63A5\u8C03\u7528 openspec\uFF0C\u9664\u975E FET \u547D\u4EE4\u672C\u8EAB\u4E0D\u53EF\u7528\u3002
6802
7211
  - change \u4E0D\u660E\u786E\u65F6\u5148\u8BE2\u95EE\u7528\u6237\u3002
6803
- ${command === "fill-context" ? "- fet fill-context \u53EF\u80FD\u5DF2\u5199\u5165\u5C0F\u7A0B\u5E8F\u5305\u4F53\u79EF\u4E0E 2MB \u7EA6\u675F\uFF0C\u4E0D\u8981\u8986\u76D6\u8BE5\u8282\u4E2D\u7684\u626B\u63CF\u7ED3\u679C\uFF0C\u9664\u975E\u4ED3\u5E93\u7ED3\u6784\u5DF2\u53D8\u3002\n- \u66FF\u6362 AGENTS.md \u4E2D\u5176\u4F59 [NEEDS 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 === "propose" || command === "continue" || command === "ff" ? "- \u4E00\u6B21\u53EA\u521B\u5EFA\u4E00\u4E2A ready artifact\uFF0C\u5E76\u5728\u5199\u5165\u524D\u9605\u8BFB\u4F9D\u8D56\u6587\u4EF6\u3002\n" : ""}${command === "propose" || command === "continue" || command === "ff" ? "- \u4E0D\u8981\u5728\u7528\u6237\u5BA1\u9605\u5F53\u524D\u4EA7\u7269\u524D\u81EA\u52A8\u8FD0\u884C fet continue\u3001fet ff \u6216\u5FAA\u73AF\u751F\u6210\u540E\u7EED\u4EA7\u7269\uFF1B\u9700\u8981\u7528\u6237\u660E\u786E\u786E\u8BA4\u540E\u518D\u63A8\u8FDB\u3002\n" : ""}${command === "propose" || command === "continue" || command === "ff" || command === "sync" ? `${renderSpecArtifactGuardrail("zh-CN")}
7212
+ ${renderWriteBoundaryGuardrail("zh-CN")}
7213
+ ${command === "fill-context" ? "- fet fill-context \u53EF\u80FD\u5DF2\u5199\u5165\u5C0F\u7A0B\u5E8F\u5305\u4F53\u79EF\u4E0E 2MB \u7EA6\u675F\uFF0C\u4E0D\u8981\u8986\u76D6\u8BE5\u8282\u4E2D\u7684\u626B\u63CF\u7ED3\u679C\uFF0C\u9664\u975E\u4ED3\u5E93\u7ED3\u6784\u5DF2\u53D8\u3002\n- \u66FF\u6362 AGENTS.md \u4E2D\u5176\u4F59 [NEEDS LLM INPUT] \u5360\u4F4D\u7B26\uFF0C\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\uFF1B\u4FEE\u6539 AGENTS.md \u5C5E\u4E8E\u5141\u8BB8\u8303\u56F4\u5916\u8DEF\u5F84\uFF0C\u987B\u5728\u672C\u8F6E\u5DF2\u83B7\u5F97\u7528\u6237\u540C\u610F\u3002\n" : ""}${command === "propose" || command === "continue" || command === "ff" ? "- \u4E00\u6B21\u53EA\u521B\u5EFA\u4E00\u4E2A ready artifact\uFF0C\u5E76\u5728\u5199\u5165\u524D\u9605\u8BFB\u4F9D\u8D56\u6587\u4EF6\u3002\n" : ""}${command === "propose" || command === "continue" || command === "ff" ? "- \u4E0D\u8981\u5728\u7528\u6237\u5BA1\u9605\u5F53\u524D\u4EA7\u7269\u524D\u81EA\u52A8\u8FD0\u884C fet continue\u3001fet ff \u6216\u5FAA\u73AF\u751F\u6210\u540E\u7EED\u4EA7\u7269\uFF1B\u9700\u8981\u7528\u6237\u660E\u786E\u786E\u8BA4\u540E\u518D\u63A8\u8FDB\u3002\n" : ""}${command === "propose" || command === "continue" || command === "ff" || command === "sync" ? `${renderSpecArtifactGuardrail("zh-CN")}
6804
7214
  ` : ""}${command === "propose" || command === "continue" || command === "ff" ? `${renderUiDisplayContractGuardrail("zh-CN")}
6805
- ` : ""}${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- \u82E5\u5B58\u5728 openspec/changes/<change-id>/.fet/figma-apply-instructions.md\uFF0C\u5FC5\u987B\u5148\u8BFB Figma\uFF08MCP/API\uFF09\u518D\u6539 UI\uFF1B\u5931\u8D25\u5219\u505C\u4E0B\u95EE\u7528\u6237\uFF0C\u7981\u6B62\u731C\u6837\u5F0F\u3002\n- \u82E5\u5B58\u5728 openspec/changes/<change-id>/.fet/ui-display-contract.yaml\uFF0C\u5B9E\u65BD\u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u524D\u987B\u786E\u8BA4 displayFields\uFF0C\u7981\u6B62\u6309 OpenAPI \u5168\u91CF\u5C55\u793A\u3002\n" : ""}`;
7215
+ ` : ""}${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- \u5B9E\u65BD\u524D\u987B\u5DF2\u8FD0\u884C fet tdd\uFF1B\u5B58\u5728 tdd-manifest.yaml \u65F6\u6309\u6E05\u5355\u7F16\u5199\u6D4B\u8BD5\u4E0E\u5B9E\u73B0\uFF0Cfet verify \u524D\u5148 fet test\u3002\n- \u82E5\u5B58\u5728 openspec/changes/<change-id>/.fet/figma-apply-instructions.md\uFF0C\u5FC5\u987B\u5148\u8BFB Figma\uFF08MCP/API\uFF09\u518D\u6539 UI\uFF1B\u5931\u8D25\u5219\u505C\u4E0B\u95EE\u7528\u6237\uFF0C\u7981\u6B62\u731C\u6837\u5F0F\u3002\n- \u82E5\u5B58\u5728 openspec/changes/<change-id>/.fet/ui-display-contract.yaml\uFF0C\u5B9E\u65BD\u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u524D\u987B\u786E\u8BA4 displayFields\uFF0C\u7981\u6B62\u6309 OpenAPI \u5168\u91CF\u5C55\u793A\u3002\n" : ""}`;
6806
7216
  }
6807
7217
  function commandTitleZh(command) {
6808
7218
  const titles = {
@@ -6876,7 +7286,9 @@ First run:
6876
7286
  fet fill-context
6877
7287
  \`\`\`
6878
7288
 
6879
- 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.
7289
+ 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. Editing AGENTS.md is outside the default write scope\u2014confirm user approval first.
7290
+
7291
+ ${renderWriteBoundaryGuardrail(language)}
6880
7292
  `;
6881
7293
  }
6882
7294
  function renderFillContextSlashPrompt(language) {
@@ -6912,7 +7324,9 @@ Steps:
6912
7324
  Guardrails:
6913
7325
  - Do not invent facts that cannot be inferred from the repo.
6914
7326
  - Use [UNKNOWN] only when the repository does not contain enough evidence.
6915
- - Keep generated context stable and useful for future AI coding sessions.`,
7327
+ - Keep generated context stable and useful for future AI coding sessions.
7328
+ - Editing AGENTS.md is outside the default write scope; confirm user approval first (see .codex/fet/write-boundary.md).
7329
+ ${renderWriteBoundaryGuardrail(language)}`,
6916
7330
  void 0,
6917
7331
  language
6918
7332
  );
@@ -6981,14 +7395,16 @@ Steps:
6981
7395
  - Follow proposal, specs, design, and tasks.
6982
7396
  - Mark each completed task checkbox in tasks.md from \`- [ ]\` to \`- [x]\`.
6983
7397
  - Pause and ask if a task is ambiguous or reveals a design conflict.
6984
- 8. After completing or pausing, summarize completed tasks, remaining tasks, and blockers.
7398
+ 8. Run \`fet tdd --change <change-id>\` before implementation when no \`tdd-manifest.yaml\` exists yet; after coding run \`fet test --change <change-id>\` before \`fet verify\`.
7399
+ 9. After completing or pausing, summarize completed tasks, remaining tasks, and blockers.
6985
7400
 
6986
7401
  Guardrails:
6987
7402
  - Never skip reading OpenSpec artifacts before implementation.
6988
7403
  - When Figma links exist for this change, never implement or restyle UI without reading Figma first.
6989
7404
  - When ui-display-contract.yaml exists, API schemas are not UI checklists\u2014only displayFields may render.
6990
7405
  - Do not mark a task complete until the code change is actually done.
6991
- - Do not run sync or archive from apply.`,
7406
+ - Do not run sync or archive from apply.
7407
+ ${renderWriteBoundaryGuardrail(language)}`,
6992
7408
  void 0,
6993
7409
  language
6994
7410
  );
@@ -7415,8 +7831,97 @@ ${languageInstruction(language)}
7415
7831
  ${graphContextInstruction}
7416
7832
 
7417
7833
  ${body}
7834
+
7835
+ ${renderWriteBoundaryGuardrail(language)}
7418
7836
  `;
7419
7837
  }
7838
+ function renderStandaloneWorkflowCommand(command, language) {
7839
+ const usage = renderFetAdapterUsage(command, "");
7840
+ const { title, body } = standaloneWorkflowCopy(command, language);
7841
+ return `<!-- FET:MANAGED
7842
+ schemaVersion: 1
7843
+ fetVersion: ${FET_VERSION}
7844
+ generator: codex-adapter
7845
+ adapterVersion: 1
7846
+ command: ${usage}
7847
+ FET:END -->
7848
+
7849
+ # ${usage}
7850
+
7851
+ ${renderIdeModelPolicy(command, language)}
7852
+
7853
+ ${languageInstruction(language)}
7854
+
7855
+ ## ${language === "en" ? "Purpose" : "\u7528\u9014"}
7856
+
7857
+ ${title}
7858
+
7859
+ ## ${language === "en" ? "Workflow" : "\u5DE5\u4F5C\u6D41"}
7860
+
7861
+ ${body}
7862
+
7863
+ ${renderWriteBoundaryGuardrail(language)}
7864
+ `;
7865
+ }
7866
+ function renderStandaloneWorkflowSlashPrompt(command, language) {
7867
+ const usage = renderFetAdapterUsage(command, "[...args]");
7868
+ const { title, body, description } = standaloneWorkflowCopy(command, language);
7869
+ return renderManagedSlashPrompt(
7870
+ usage,
7871
+ description,
7872
+ `${title}
7873
+
7874
+ Steps:
7875
+
7876
+ 1. Read AGENTS.md, openspec/config.yaml, .codex/fet/karpathy-guidelines.md, and .codex/fet/write-boundary.md.
7877
+ 2. Resolve the change id when needed (\`--change <change-id>\` or active change).
7878
+ 3. Run:
7879
+ \`\`\`sh
7880
+ ${renderFetAdapterUsage(command, "")}
7881
+ \`\`\`
7882
+ 4. ${body}
7883
+ 5. Summarize outputs, paths written under openspec/changes/<change-id>/.fet/, and next steps.
7884
+
7885
+ Guardrails:
7886
+ - ${body.split("\n")[0]}
7887
+ ${renderWriteBoundaryGuardrail(language)}`,
7888
+ void 0,
7889
+ language
7890
+ );
7891
+ }
7892
+ function standaloneWorkflowCopy(command, language) {
7893
+ if (command === "tdd") {
7894
+ return language === "en" ? {
7895
+ description: "Generate per-change TDD manifest and test instructions",
7896
+ title: "Generate the TDD manifest before implementation.",
7897
+ body: "After planning artifacts exist, run this before `fet apply`. It writes `openspec/changes/<change-id>/.fet/tdd-manifest.yaml`, `tdd-spec.md`, and `tdd-instructions.md`. Then add failing tests in the repo before implementation (tests/ edits need user approval per write-boundary)."
7898
+ } : {
7899
+ description: "\u751F\u6210\u672C change \u7684 TDD \u6E05\u5355\u4E0E\u6D4B\u8BD5\u6307\u5F15",
7900
+ title: "\u5728\u5B9E\u65BD\u524D\u751F\u6210 TDD \u6E05\u5355\u3002",
7901
+ body: "\u89C4\u5212\u4EA7\u7269\u5C31\u7EEA\u540E\u3001\u6267\u884C `fet apply` \u4E4B\u524D\u8FD0\u884C\u3002\u4F1A\u5199\u5165 `openspec/changes/<change-id>/.fet/tdd-manifest.yaml`\u3001`tdd-spec.md`\u3001`tdd-instructions.md`\uFF0C\u518D\u5728\u4ED3\u5E93\u4E2D\u7F16\u5199\u9884\u671F\u5931\u8D25\u7684\u6D4B\u8BD5\uFF08\u4FEE\u6539 tests/ \u987B\u7B26\u5408 write-boundary\uFF0C\u5148\u83B7\u7528\u6237\u540C\u610F\uFF09\u3002"
7902
+ };
7903
+ }
7904
+ if (command === "test") {
7905
+ return language === "en" ? {
7906
+ description: "Run unit tests scoped to the change TDD manifest",
7907
+ title: "Run change-scoped tests after implementation.",
7908
+ body: "Requires `tdd-manifest.yaml`. Records pass/fail in FET state; `fet verify` is blocked until this passes (unless configured to skip)."
7909
+ } : {
7910
+ description: "\u6309 change TDD \u6E05\u5355\u8FD0\u884C\u5355\u6D4B",
7911
+ title: "\u5B9E\u73B0\u5B8C\u6210\u540E\u6309 change \u8FD0\u884C\u6D4B\u8BD5\u3002",
7912
+ body: "\u9700\u8981\u6709\u6548\u7684 `tdd-manifest.yaml`\u3002\u7ED3\u679C\u5199\u5165 FET \u72B6\u6001\uFF1B\u672A\u901A\u8FC7\u524D `fet verify` \u4F1A\u88AB\u62E6\u622A\uFF08\u9664\u975E\u914D\u7F6E\u4E3A skip\uFF09\u3002"
7913
+ };
7914
+ }
7915
+ return language === "en" ? {
7916
+ description: "Layout-only visual verification for a change",
7917
+ title: "Run layout-only visual checks for the change.",
7918
+ body: "Default `fet visual` refreshes the manifest, captures with Playwright (`--base-url` required), and runs layout-only checks (no pixel match on dynamic API content). Use `--plan`, `--capture-only`, or `--check-layout-only` only when debugging."
7919
+ } : {
7920
+ description: "change \u7EA7 layout-only \u89C6\u89C9\u9A8C\u6536",
7921
+ title: "\u5BF9 change \u505A layout-only \u89C6\u89C9\u9A8C\u6536\u3002",
7922
+ body: "\u9ED8\u8BA4 `fet visual` \u4F1A\u66F4\u65B0\u6E05\u5355\u3001Playwright \u622A\u56FE\uFF08\u9700 `--base-url`\uFF09\u5E76\u505A layout-only \u68C0\u67E5\uFF08\u4E0D\u5BF9\u52A8\u6001\u63A5\u53E3\u5185\u5BB9\u505A\u50CF\u7D20\u5BF9\u6BD4\uFF09\u3002\u4EC5\u5728\u8C03\u8BD5\u65F6\u4F7F\u7528 `--plan`\u3001`--capture-only`\u3001`--check-layout-only`\u3002"
7923
+ };
7924
+ }
7420
7925
 
7421
7926
  // src/adapters/codex/index.ts
7422
7927
  var CodexAdapter = class {
@@ -7536,6 +8041,28 @@ function cursorSpecLanguageRuleFile(language = DEFAULT_LANGUAGE) {
7536
8041
  content: renderCursorSpecLanguageRule(language)
7537
8042
  };
7538
8043
  }
8044
+ function cursorWriteBoundaryRuleFile(language = DEFAULT_LANGUAGE) {
8045
+ return {
8046
+ path: ".cursor/rules/fet-write-boundary.mdc",
8047
+ content: renderCursorWriteBoundaryRule(language)
8048
+ };
8049
+ }
8050
+ function cursorHookFiles() {
8051
+ return [
8052
+ {
8053
+ path: ".cursor/hooks/fet-guard-write-paths.mjs",
8054
+ content: renderCursorWritePathsHookMjs()
8055
+ },
8056
+ {
8057
+ path: ".cursor/hooks/fet-guard-shell-writes.mjs",
8058
+ content: renderCursorShellWriteHookMjs()
8059
+ },
8060
+ {
8061
+ path: ".cursor/hooks.json",
8062
+ content: renderCursorHooksJson()
8063
+ }
8064
+ ];
8065
+ }
7539
8066
  function cursorUiDisplayContractRuleFile(language = DEFAULT_LANGUAGE) {
7540
8067
  return {
7541
8068
  path: ".cursor/rules/fet-ui-display-contract.mdc",
@@ -7545,6 +8072,7 @@ function cursorUiDisplayContractRuleFile(language = DEFAULT_LANGUAGE) {
7545
8072
  function cursorRuleFiles(language = DEFAULT_LANGUAGE) {
7546
8073
  return [
7547
8074
  cursorRuleFile(language),
8075
+ cursorWriteBoundaryRuleFile(language),
7548
8076
  cursorFigmaStopRuleFile(language),
7549
8077
  cursorUiDisplayContractRuleFile(language),
7550
8078
  cursorSpecLanguageRuleFile(language)
@@ -7582,6 +8110,7 @@ ${languageInstruction(language)}
7582
8110
  - \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9075\u5B88 \`.cursor/rules/fet-figma-stop.mdc\`\uFF1B\u82E5\u5B58\u5728 \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\` \u5FC5\u987B\u5728\u6539 UI \u524D\u5B8C\u6574\u6267\u884C\uFF1B\u540C\u76EE\u5F55 \`figma-stop.md\` \u542B\u94FE\u63A5\u4E0E\u505C\u6B62\u89C4\u5219\u3002
7583
8111
  - \u7F16\u5199\u6216\u4FEE\u6539 OpenSpec \`specs/**/spec.md\` \u65F6\u9075\u5B88 \`.cursor/rules/fet-spec-language.mdc\`\uFF08\u82F1\u6587\u89C4\u8303 + \`<!-- \u4E2D\u6587\uFF1A... -->\`\uFF0C\u540C\u6B21\u7F16\u8F91\u540C\u6B65\u66F4\u65B0\u4E2D\u6587\u8BF4\u660E\uFF09\u3002
7584
8112
  - \u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u9075\u5B88 \`.cursor/rules/fet-ui-display-contract.mdc\`\uFF1B\u5B58\u5728 \`openspec/changes/<change-id>/.fet/ui-display-contract.yaml\` \u65F6\u5B9E\u65BD\u524D\u987B\u786E\u8BA4 displayFields\u3002
8113
+ - \u4FEE\u6539\u6587\u4EF6\u65F6\u9075\u5B88 \`.cursor/rules/fet-write-boundary.mdc\`\uFF08\u9ED8\u8BA4\u4EC5 \`src/**\`\u3001\`openspec/**\`\u3001\`**/.fet/**\`\uFF1B\u5176\u5B83\u8DEF\u5F84\u987B\u7528\u6237\u660E\u786E\u540C\u610F\uFF09\u3002Cursor hooks \u4F1A\u5BF9\u8D8A\u754C\u5199\u5165\u5F39\u51FA\u5BA1\u6279\u3002
7585
8114
 
7586
8115
  \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
7587
8116
  `
@@ -7617,7 +8146,9 @@ ${languageInstruction(language)}
7617
8146
 
7618
8147
  \`fet fill-context\` \u53EF\u80FD\u5DF2\u5199\u5165\u5C0F\u7A0B\u5E8F\u5305\u4F53\u79EF\u8868\u4E0E 2MB \u5F00\u53D1\u7EA6\u675F\uFF0C\u4E0D\u8981\u8986\u76D6 AGENTS.md\u300C\u5C0F\u7A0B\u5E8F\u300D\u8282\u4E2D\u7684\u626B\u63CF\u7ED3\u679C\uFF0C\u9664\u975E\u4ED3\u5E93\u7ED3\u6784\u5DF2\u53D8\u3002
7619
8148
 
7620
- \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**\u5176\u4F59** \`[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
8149
+ \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**\u5176\u4F59** \`[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\u4FEE\u6539 \`AGENTS.md\` \u4E0D\u5728\u9ED8\u8BA4\u5199\u8DEF\u5F84\u5185\uFF0C\u987B\u786E\u8BA4\u7528\u6237\u5DF2\u540C\u610F\uFF08\u6216\u7531 hooks \u5BA1\u6279\uFF09\u3002
8150
+
8151
+ ${renderWriteBoundaryGuardrail(language)}
7621
8152
  `;
7622
8153
  }
7623
8154
  if (command === "graph-setup") {
@@ -7723,6 +8254,8 @@ ${renderSpecArtifactGuardrail(language)}
7723
8254
 
7724
8255
  ${renderUiDisplayContractGuardrail(language)}
7725
8256
 
8257
+ ${renderWriteBoundaryGuardrail(language)}
8258
+
7726
8259
  \u6267\u884C\u524D\u8BF7\u9605\u8BFB AGENTS.md\u3001openspec/config.yaml\uFF08\u542B \`fet.specLanguage\`\uFF09\u4E0E\u5F53\u524D change \u5DF2\u6709\u4EA7\u7269\u3002
7727
8260
  `;
7728
8261
  }
@@ -7778,6 +8311,8 @@ ${uiContractBlock}
7778
8311
  \u6267\u884C\u524D\u8BF7\u9605\u8BFB AGENTS.md\u3001openspec/config.yaml \u4E0E\u5F53\u524D change \u4E0B\u7684 OpenSpec \u4EA7\u7269\u3002
7779
8312
 
7780
8313
  \u5B9E\u65BD\u524D\u987B\u5DF2\u8FD0\u884C \`fet tdd\`\uFF1B\u5B58\u5728 \`openspec/changes/<change-id>/.fet/tdd-manifest.yaml\` \u65F6\u6309\u6E05\u5355\u7F16\u5199\u6D4B\u8BD5\u4E0E\u5B9E\u73B0\uFF0C\u5E76\u5728 \`fet verify\` \u524D\u5148 \`fet test\`\u3002
8314
+
8315
+ ${renderWriteBoundaryGuardrail(language)}
7781
8316
  `;
7782
8317
  }
7783
8318
  function renderVisualSkill(usage, language) {
@@ -7853,7 +8388,7 @@ var CursorAdapter = class {
7853
8388
  async planInstall(_projectRoot, language) {
7854
8389
  return {
7855
8390
  tool: this.tool,
7856
- files: [...cursorSkillFiles(language), ...cursorRuleFiles(language)].map((file) => ({
8391
+ files: [...cursorSkillFiles(language), ...cursorRuleFiles(language), ...cursorHookFiles()].map((file) => ({
7857
8392
  ...file,
7858
8393
  managed: true
7859
8394
  }))
@@ -7865,6 +8400,12 @@ var CursorAdapter = class {
7865
8400
  for (const file of plan.files) {
7866
8401
  const target = join31(projectRoot, file.path);
7867
8402
  const existing = await readExisting2(target);
8403
+ if (file.path === ".cursor/hooks.json") {
8404
+ await mkdir13(dirname12(target), { recursive: true });
8405
+ await atomicWrite(target, mergeCursorHooksJson(existing, file.content));
8406
+ written.push(file.path);
8407
+ continue;
8408
+ }
7868
8409
  if (existing && !existing.includes("FET:MANAGED") && !force) {
7869
8410
  throw new FetError({
7870
8411
  code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
@@ -7888,12 +8429,14 @@ var CursorAdapter = class {
7888
8429
  for (const file of plan.files) {
7889
8430
  const target = join31(projectRoot, file.path);
7890
8431
  const content = await readExisting2(target);
7891
- const managed = Boolean(content?.includes("FET:MANAGED"));
7892
- const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
8432
+ const hooksManaged = file.path === ".cursor/hooks.json" && Boolean(content?.includes('"writeBoundary"'));
8433
+ const hookScript = file.path.startsWith(".cursor/hooks/fet-guard-") && file.path.endsWith(".mjs");
8434
+ const managed = hooksManaged || hookScript || Boolean(content?.includes("FET:MANAGED"));
8435
+ const versionMatches = file.path === ".cursor/hooks.json" ? Boolean(content?.includes('"writeBoundary"')) : hookScript ? Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`)) : Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
7893
8436
  checks.push({
7894
8437
  id: `cursor:${file.path}`,
7895
8438
  status: !content ? "warn" : managed && versionMatches ? "pass" : "warn",
7896
- 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`,
8439
+ message: !content ? `${file.path} \u7F3A\u5931` : !managed ? `${file.path} \u5DF2\u5B58\u5728\uFF0C\u4F46\u4E0D\u7531 FET \u7BA1\u7406` : !versionMatches ? `${file.path} \u7248\u672C\u5DF2\u8FC7\u671F\uFF08\u8BF7\u8FD0\u884C fet init \u66F4\u65B0\uFF09` : `${file.path} \u5DF2\u5B58\u5728\u4E14\u7248\u672C\u5339\u914D`,
7897
8440
  suggestedCommand: !content || !managed || !versionMatches ? "fet init" : void 0
7898
8441
  });
7899
8442
  }