add-coder 0.3.38 → 0.3.40

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.
Files changed (78) hide show
  1. package/README.en.md +16 -3
  2. package/README.md +16 -3
  3. package/dist/index.js +421 -70
  4. package/package.json +3 -2
  5. package/templates/.add-coder-src-hash.json +74 -61
  6. package/templates/adapters/claude/hooks/post-tool-use.mjs +1 -1
  7. package/templates/adapters/claude/hooks/prompt-submit.mjs +1 -1
  8. package/templates/adapters/claude/hooks/session-start.mjs +25 -5
  9. package/templates/adapters/claude/hooks/stop-check.mjs +2 -2
  10. package/templates/adapters/codex/hooks/post-tool-use.mjs +1 -1
  11. package/templates/adapters/codex/hooks/prompt-submit.mjs +1 -1
  12. package/templates/adapters/codex/hooks/session-start.mjs +25 -5
  13. package/templates/adapters/codex/hooks/stop-check.mjs +2 -2
  14. package/templates/adapters/qoder/hooks/post-tool-use.mjs +1 -1
  15. package/templates/adapters/qoder/hooks/prompt-submit.mjs +1 -1
  16. package/templates/adapters/qoder/hooks/session-start.mjs +25 -5
  17. package/templates/adapters/qoder/hooks/stop-check.mjs +2 -2
  18. package/templates/adapters/trae/hooks/post-tool-use.mjs +1 -1
  19. package/templates/adapters/trae/hooks/prompt-submit.mjs +1 -1
  20. package/templates/adapters/trae/hooks/session-start.mjs +25 -5
  21. package/templates/adapters/trae/hooks/stop-check.mjs +2 -2
  22. package/templates/adapters/vscode/hooks/post-tool-use.mjs +1 -1
  23. package/templates/adapters/vscode/hooks/prompt-submit.mjs +1 -1
  24. package/templates/adapters/vscode/hooks/session-start.mjs +25 -5
  25. package/templates/adapters/vscode/hooks/stop-check.mjs +2 -2
  26. package/templates/core/AGENTS.md +46 -0
  27. package/templates/core/docs/ADD-governance-claude-code.md +52 -0
  28. package/templates/core/docs/ADD-governance-codex.md +62 -0
  29. package/templates/core/docs/ADD-governance-qoder-cn.md +30 -0
  30. package/templates/core/docs/ADD-governance-trae.md +42 -0
  31. package/templates/core/docs/ADD-governance-vscode-copilot.md +88 -0
  32. package/templates/core/governance/rules.ts +2 -2
  33. package/templates/core/governance/session-start-guard.ts +28 -4
  34. package/templates/core/prisma/add.prisma +4 -0
  35. package/templates/core/scripts/mcp-server/resources/hitl-approval-widget.ts +4 -2
  36. package/templates/core/scripts/mcp-server/resources/index.ts +2 -0
  37. package/templates/core/scripts/mcp-server/resources/widget-probe.ts +53 -0
  38. package/templates/core/scripts/mcp-server/shared/db-types.ts +5 -1
  39. package/templates/core/scripts/mcp-server/shared/dev-operation.ts +97 -0
  40. package/templates/core/scripts/mcp-server/shared/hitl-ui.ts +46 -1
  41. package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +9 -0
  42. package/templates/core/scripts/mcp-server/shared/memory/retrieval/cjk-segmenter.ts +140 -0
  43. package/templates/core/scripts/mcp-server/shared/memory/retrieval/cjk-tokenize.ts +62 -0
  44. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +68 -16
  45. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +3 -3
  46. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +23 -6
  47. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts-fingerprint.ts +116 -0
  48. package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +9 -25
  49. package/templates/core/scripts/mcp-server/shared/memory/switches.ts +4 -2
  50. package/templates/core/scripts/mcp-server/shared/plan-lifecycle.ts +16 -3
  51. package/templates/core/scripts/mcp-server/shared/server-capabilities.ts +65 -0
  52. package/templates/core/scripts/mcp-server/tools/audit.ts +16 -34
  53. package/templates/core/scripts/mcp-server/tools/gateway/check_add_route_completeness.ts +17 -4
  54. package/templates/core/scripts/mcp-server/tools/gateway/check_add_route_status.ts +34 -6
  55. package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +15 -25
  56. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +23 -26
  57. package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +56 -38
  58. package/templates/core/scripts/mcp-server/tools/gateway/plan-resolve.ts +270 -0
  59. package/templates/core/scripts/mcp-server/tools/hitl.ts +18 -8
  60. package/templates/core/scripts/mcp-server/tools/index.ts +7 -2
  61. package/templates/core/scripts/mcp-server/tools/memory.ts +235 -1
  62. package/templates/core/scripts/mcp-server/tools/plan-lifecycle.ts +219 -0
  63. package/templates/core/scripts/mcp-server/tools/plan.ts +2 -0
  64. package/templates/core/scripts/mcp-server/tools/widget-probe.ts +53 -0
  65. package/templates/core/scripts/mcp-server.ts +5 -1
  66. package/templates/core/scripts/memory/backfill-search-text.ts +132 -0
  67. package/templates/core/scripts/memory/memory-jobs.ts +124 -0
  68. package/templates/core/scripts/plan-close.ts +85 -0
  69. package/templates/core/skills/add-paradigm/SKILL.md +19 -4
  70. package/templates/core/skills/session-init/SKILL.md +27 -0
  71. package/templates/core/templates/add-route-template-heavyweight.md +2 -0
  72. package/templates/core/templates/add-route-template.md +4 -0
  73. package/templates/core/templates/checklist-template.md +2 -0
  74. package/templates/core/templates/handoff-multi-round-template.md +2 -0
  75. package/templates/core/templates/review-implementation-template.md +18 -0
  76. package/templates/core/templates/review-implementation-template.schema.json +7 -1
  77. package/templates/core/templates/simple-plan-template.md +4 -0
  78. package/templates/core/vocabulary/add-governance-vocabulary.md +2 -0
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { readFileSync as readFileSync14 } from "fs";
4
+ import { readFileSync as readFileSync17 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/caijuehub/strategies/detect.strategy.ts
@@ -1548,7 +1548,7 @@ async function injectPrisma2(projectRoot, options = {}) {
1548
1548
  }
1549
1549
 
1550
1550
  // src/cli/commands/init.ts
1551
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, existsSync as existsSync13, mkdirSync as mkdirSync6, copyFileSync as copyFileSync2, readdirSync as readdirSync5 } from "fs";
1551
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, existsSync as existsSync14, mkdirSync as mkdirSync6, copyFileSync as copyFileSync2, readdirSync as readdirSync5 } from "fs";
1552
1552
  import { createHash } from "crypto";
1553
1553
  import { resolve as resolve9, dirname as dirname3, join as pathJoin } from "path";
1554
1554
  import { homedir as homedir2 } from "os";
@@ -1666,6 +1666,68 @@ function applyMemoryExpectedState(opts) {
1666
1666
  }
1667
1667
  }
1668
1668
 
1669
+ // src/lib/memory-snapshot-install.ts
1670
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
1671
+ import { join as join10 } from "path";
1672
+ var ENV_FILE_CANDIDATES = [".env.development.local", ".env.development", ".env.local", ".env"];
1673
+ function readDatabaseUrlFromEnvFiles(projectRoot) {
1674
+ for (const f of ENV_FILE_CANDIDATES) {
1675
+ const p = join10(projectRoot, f);
1676
+ if (!existsSync13(p)) continue;
1677
+ const text = readFileSync10(p, "utf-8");
1678
+ const m = text.match(/^\s*DATABASE_URL\s*=\s*"?([^"\n\r]+)"?\s*$/m);
1679
+ if (m?.[1]) return m[1].trim();
1680
+ }
1681
+ return void 0;
1682
+ }
1683
+ function memoryJobsScriptRelPath(magicDir) {
1684
+ return `${magicDir}/scripts/memory/memory-jobs.ts`;
1685
+ }
1686
+ function refreshL1SnapshotOnInstall(opts) {
1687
+ const relScript = memoryJobsScriptRelPath(opts.magicDir);
1688
+ const absScript = join10(opts.projectRoot, opts.magicDir, "scripts", "memory", "memory-jobs.ts");
1689
+ const cmd = `npx tsx ${relScript} refresh-l1`;
1690
+ if (!existsSync13(absScript)) {
1691
+ return {
1692
+ ok: false,
1693
+ relScript,
1694
+ cmd,
1695
+ detail: `\u8BB0\u5FC6\u5165\u53E3\u811A\u672C\u7F3A\u5931: ${relScript}\uFF08\u6A21\u677F\u672A\u5C31\u7EEA\uFF1A\u5148\u6267\u884C add-coder sync\uFF0C\u6216\u786E\u8BA4 add-coder \u7248\u672C \u2265 \u672C Plan \u7248\u672C\uFF09`
1696
+ };
1697
+ }
1698
+ const env = opts.env ?? process.env;
1699
+ const databaseUrl = env.DATABASE_URL || readDatabaseUrlFromEnvFiles(opts.projectRoot);
1700
+ if (!databaseUrl) {
1701
+ return {
1702
+ ok: false,
1703
+ relScript,
1704
+ cmd,
1705
+ detail: `\u672A\u627E\u5230 DATABASE_URL\uFF08\u68C0\u67E5 ${ENV_FILE_CANDIDATES.join(" / ")}\uFF0C\u6216\u5728\u73AF\u5883\u4E2D\u663E\u5F0F\u5BFC\u51FA\uFF09`
1706
+ };
1707
+ }
1708
+ const runner = opts.run ?? ((c, a, o) => runCommand(c, a, o));
1709
+ const result = runner(
1710
+ "npx",
1711
+ ["tsx", absScript, "refresh-l1"],
1712
+ {
1713
+ cwd: opts.projectRoot,
1714
+ env: { ...env, PROJECT_ROOT: opts.projectRoot, MAGIC_DIR: opts.magicDir, DATABASE_URL: databaseUrl },
1715
+ timeout: opts.timeoutMs ?? 12e4
1716
+ }
1717
+ );
1718
+ if (result.status !== 0) {
1719
+ const why = (result.stderr || "").trim().split("\n").slice(0, 4).join(" | ");
1720
+ return {
1721
+ ok: false,
1722
+ relScript,
1723
+ cmd,
1724
+ detail: `memory-jobs refresh-l1 \u9000\u51FA\u7801 ${result.status}${why ? ` \u2014\u2014 ${why}` : "\uFF08\u65E0 stderr \u8F93\u51FA\uFF09"}`,
1725
+ stdout: result.stdout
1726
+ };
1727
+ }
1728
+ return { ok: true, relScript, cmd, stdout: result.stdout };
1729
+ }
1730
+
1669
1731
  // src/cli/commands/init.ts
1670
1732
  var ADAPTER_RENDERERS = {
1671
1733
  claude: renderAdapter,
@@ -1697,6 +1759,17 @@ async function initCommand(options) {
1697
1759
  const dbFail = await deployDatabase(ctx);
1698
1760
  ensurePortsContract(ctx.projectRoot, ctx.config, !!options.dryRun);
1699
1761
  deployDocs(ctx);
1762
+ if (!options.dryRun) {
1763
+ const snap = refreshL1SnapshotOnInstall({ projectRoot: ctx.projectRoot, magicDir: ctx.magicDir });
1764
+ if (!snap.ok) {
1765
+ console.error(`
1766
+ \u2717 \u8BB0\u5FC6\u5FEB\u7167\u672A\u5C31\u7EEA: ${snap.detail}`);
1767
+ console.error(` \u590D\u73B0\u547D\u4EE4: ${snap.cmd}`);
1768
+ console.error(" \u4FEE\u590D\u540E\u91CD\u65B0\u8FD0\u884C add-coder init\uFF08\u8BE5\u6B65\u9AA4\u4E3A\u786C\u95E8\u7981\uFF09\u3002");
1769
+ process.exit(1);
1770
+ }
1771
+ console.log(`\u2705 \u8BB0\u5FC6 L1 \u5FEB\u7167\u5DF2\u5237\u65B0: ${snap.relScript}`);
1772
+ }
1700
1773
  finalize(ctx, result, dbFail);
1701
1774
  if (!options.dryRun) {
1702
1775
  try {
@@ -1732,7 +1805,7 @@ async function handleCodexMcpConfig(options) {
1732
1805
  console.log(`[dry-run] \u5C06\u5199\u5165 ${userConfigPath}`);
1733
1806
  return;
1734
1807
  }
1735
- if (existsSync13(userConfigPath) && readFileSync10(userConfigPath, "utf-8").includes("mcp_servers.add_coder")) {
1808
+ if (existsSync14(userConfigPath) && readFileSync11(userConfigPath, "utf-8").includes("mcp_servers.add_coder")) {
1736
1809
  console.log(`\u26A0\uFE0F ${userConfigPath} \u5DF2\u5B58\u5728 mcp_servers.add_coder \u914D\u7F6E\uFF0C\u8DF3\u8FC7\uFF08\u907F\u514D\u91CD\u590D\uFF09`);
1737
1810
  return;
1738
1811
  }
@@ -1741,7 +1814,7 @@ async function handleCodexMcpConfig(options) {
1741
1814
  console.log("\u5DF2\u53D6\u6D88\u5199\u5165");
1742
1815
  return;
1743
1816
  }
1744
- if (existsSync13(userConfigPath)) {
1817
+ if (existsSync14(userConfigPath)) {
1745
1818
  const backup = `${userConfigPath}.bak-${Date.now()}`;
1746
1819
  copyFileSync2(userConfigPath, backup);
1747
1820
  console.log(`\u5DF2\u5907\u4EFD\u539F\u914D\u7F6E \u2192 ${backup}`);
@@ -1917,18 +1990,18 @@ main().catch((e) => { console.error(e); process.exit(1); });
1917
1990
  console.log(`[dry-run] \u5C06\u5199\u5165 ${scriptPath}`);
1918
1991
  return;
1919
1992
  }
1920
- if (!existsSync13(scriptsDir)) mkdirSync6(scriptsDir, { recursive: true });
1993
+ if (!existsSync14(scriptsDir)) mkdirSync6(scriptsDir, { recursive: true });
1921
1994
  writeFileSync6(scriptPath, content, "utf-8");
1922
1995
  console.log("\u5DF2\u751F\u6210 scripts/export-db.ts");
1923
1996
  }
1924
1997
  function injectDbExportScript(projectRoot, dryRun) {
1925
1998
  const pkgPath = resolve9(projectRoot, "package.json");
1926
- if (!existsSync13(pkgPath)) return;
1999
+ if (!existsSync14(pkgPath)) return;
1927
2000
  if (dryRun) {
1928
2001
  console.log("[dry-run] \u5C06\u6CE8\u5165 db:export");
1929
2002
  return;
1930
2003
  }
1931
- const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
2004
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
1932
2005
  if (!pkg.scripts) pkg.scripts = {};
1933
2006
  if (!pkg.scripts["db:export"]) {
1934
2007
  pkg.scripts["db:export"] = "npx tsx scripts/export-db.ts";
@@ -1976,14 +2049,14 @@ function writeComposeEnv(ctx) {
1976
2049
  if (!db.reuseExisting) {
1977
2050
  const composeName = db.container === "podman" ? "podman-compose.add.yml" : "docker-compose.add.yml";
1978
2051
  const composePath = resolve9(projectRoot, composeName);
1979
- if (!options.dryRun && (!existsSync13(composePath) || options.force)) {
2052
+ if (!options.dryRun && (!existsSync14(composePath) || options.force)) {
1980
2053
  writeFileSync6(composePath, composeContent(config.projectName || "add-project"), "utf-8");
1981
2054
  console.log(`\u5DF2\u521B\u5EFA ${composeName}`);
1982
2055
  }
1983
2056
  }
1984
2057
  const devEnvPath = resolve9(projectRoot, ".env.development");
1985
- if (!options.dryRun && existsSync13(devEnvPath)) {
1986
- const existing = readFileSync10(devEnvPath, "utf-8");
2058
+ if (!options.dryRun && existsSync14(devEnvPath)) {
2059
+ const existing = readFileSync11(devEnvPath, "utf-8");
1987
2060
  if (!/^DATABASE_USER=/m.test(existing)) {
1988
2061
  writeFileSync6(devEnvPath, existing + `
1989
2062
  DATABASE_USER=${db.user || "admin"}
@@ -2030,7 +2103,7 @@ async function renderAndWrite(ctx) {
2030
2103
  const reviewTargets = target === "codex" ? [magicDir] : [ADD_DIR, magicDir];
2031
2104
  for (const d of reviewTargets) {
2032
2105
  const reviewsDir = resolve9(projectRoot, d, "reviews");
2033
- if (!existsSync13(reviewsDir)) {
2106
+ if (!existsSync14(reviewsDir)) {
2034
2107
  if (dry) {
2035
2108
  console.log(`[dry-run] \u5C06\u521B\u5EFA ${reviewsDir}/`);
2036
2109
  } else {
@@ -2042,7 +2115,7 @@ async function renderAndWrite(ctx) {
2042
2115
  const hashMap = {};
2043
2116
  let npmVer = "";
2044
2117
  try {
2045
- npmVer = JSON.parse(readFileSync10(resolve9(projectRoot, "node_modules", "add-coder", "templates", ".add-coder-src-hash.json"), "utf-8"))._version ?? "";
2118
+ npmVer = JSON.parse(readFileSync11(resolve9(projectRoot, "node_modules", "add-coder", "templates", ".add-coder-src-hash.json"), "utf-8"))._version ?? "";
2046
2119
  } catch {
2047
2120
  }
2048
2121
  for (const [rp, c] of allFiles) {
@@ -2081,7 +2154,7 @@ ${bashRun.stderr.trim().split("\n").slice(0, 5).join("\n")}` : ""}`;
2081
2154
  }
2082
2155
  if (db.engine === "postgresql" && db.container === "manual") {
2083
2156
  const dbScript = resolve9(projectRoot, magicDir, "scripts", "db-ensure.sh");
2084
- if (existsSync13(dbScript)) {
2157
+ if (existsSync14(dbScript)) {
2085
2158
  try {
2086
2159
  const bashRun = runCommand("bash", [dbScript, "postgresql", "manual"], { cwd: projectRoot, stdio: "inherit" });
2087
2160
  if (bashRun.status !== 0) fail = `db-ensure.sh \u9000\u51FA\u7801: ${bashRun.status}${bashRun.stderr ? `
@@ -2139,12 +2212,12 @@ function deployDocs(ctx) {
2139
2212
  for (const d of ["00-\u9700\u6C42", "01-\u67B6\u6784", "02-\u89C4\u8303"]) {
2140
2213
  const srcDir = resolve9(groundingSrc, d);
2141
2214
  const destDir = resolve9(docsBase, d);
2142
- if (!existsSync13(destDir)) mkdirSync6(destDir, { recursive: true });
2143
- if (!existsSync13(srcDir)) continue;
2215
+ if (!existsSync14(destDir)) mkdirSync6(destDir, { recursive: true });
2216
+ if (!existsSync14(srcDir)) continue;
2144
2217
  for (const f of readdirSync5(srcDir)) {
2145
2218
  const src = resolve9(srcDir, f);
2146
2219
  const dest = resolve9(destDir, f);
2147
- if (existsSync13(dest)) continue;
2220
+ if (existsSync14(dest)) continue;
2148
2221
  try {
2149
2222
  copyFileSync2(src, dest);
2150
2223
  } catch {
@@ -2160,7 +2233,7 @@ function finalize(ctx, result, dbFail) {
2160
2233
  return;
2161
2234
  }
2162
2235
  if (db.engine === "sqlite") console.log("\u6570\u636E\u5907\u4EFD: npm run db:export \u2192 data/exports/");
2163
- const pkg = JSON.parse(readFileSync10(resolve9(import.meta.dirname, "../package.json"), "utf-8"));
2236
+ const pkg = JSON.parse(readFileSync11(resolve9(import.meta.dirname, "../package.json"), "utf-8"));
2164
2237
  const peerNames = Object.keys(pkg.peerDependencies || {});
2165
2238
  if (peerNames.length > 0) {
2166
2239
  console.log(`
@@ -2193,7 +2266,7 @@ function finalize(ctx, result, dbFail) {
2193
2266
  }
2194
2267
 
2195
2268
  // src/cli/commands/sync.ts
2196
- import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
2269
+ import { existsSync as existsSync15, readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
2197
2270
  import { resolve as resolve10, dirname as dirname4 } from "path";
2198
2271
  import { createHash as createHash2 } from "crypto";
2199
2272
 
@@ -2267,7 +2340,7 @@ function resolvePatchConflictMode(yes = false, isTTY = Boolean(process.stdin.isT
2267
2340
  }
2268
2341
  function loadHashFile(root, magic) {
2269
2342
  try {
2270
- const raw = JSON.parse(readFileSync11(resolve10(root, magic, SYNC_CONFIG.HASH_OUTPUT_FILE), "utf-8"));
2343
+ const raw = JSON.parse(readFileSync12(resolve10(root, magic, SYNC_CONFIG.HASH_OUTPUT_FILE), "utf-8"));
2271
2344
  const normalized = {};
2272
2345
  for (const [k, v] of Object.entries(raw)) normalized[normalizeRelPath(k)] = v;
2273
2346
  return normalized;
@@ -2277,7 +2350,7 @@ function loadHashFile(root, magic) {
2277
2350
  }
2278
2351
  function loadVersionFile(root, magic) {
2279
2352
  try {
2280
- return readFileSync11(resolve10(root, magic, SYNC_CONFIG.VERSION_SENTINEL), "utf-8").trim();
2353
+ return readFileSync12(resolve10(root, magic, SYNC_CONFIG.VERSION_SENTINEL), "utf-8").trim();
2281
2354
  } catch {
2282
2355
  return "";
2283
2356
  }
@@ -2293,7 +2366,7 @@ function loadTemplateVersion(root) {
2293
2366
  ];
2294
2367
  for (const candidate of candidates) {
2295
2368
  try {
2296
- return JSON.parse(readFileSync11(candidate, "utf-8"))._version ?? "";
2369
+ return JSON.parse(readFileSync12(candidate, "utf-8"))._version ?? "";
2297
2370
  } catch {
2298
2371
  }
2299
2372
  }
@@ -2404,13 +2477,13 @@ async function syncCommand(options = {}) {
2404
2477
  for (const [relPath, content] of candidates) {
2405
2478
  const key = normalizeRelPath(relPath);
2406
2479
  const absPath = resolve10(projectRoot, relPath);
2407
- if (!existsSync14(absPath)) {
2480
+ if (!existsSync15(absPath)) {
2408
2481
  missingFiles.set(key, content);
2409
2482
  } else if (establishBaseline) {
2410
2483
  missingFiles.set(key, content);
2411
2484
  } else {
2412
2485
  const classification = classifyPatchCandidate(
2413
- readFileSync11(absPath, "utf-8"),
2486
+ readFileSync12(absPath, "utf-8"),
2414
2487
  content,
2415
2488
  outHash[key]
2416
2489
  );
@@ -2451,7 +2524,7 @@ async function syncCommand(options = {}) {
2451
2524
  let finalHash = mergeFullHash(
2452
2525
  outHash,
2453
2526
  [...candidates].map(([relPath]) => ({ relPath, absPath: resolve10(projectRoot, relPath) })),
2454
- (absPath) => existsSync14(absPath) ? hash8(readFileSync11(absPath, "utf-8")) : null,
2527
+ (absPath) => existsSync15(absPath) ? hash8(readFileSync12(absPath, "utf-8")) : null,
2455
2528
  unresolvedConflictKeys
2456
2529
  );
2457
2530
  if (target === "codex") {
@@ -2462,16 +2535,18 @@ async function syncCommand(options = {}) {
2462
2535
  await checkPrismaDiff(projectRoot, options);
2463
2536
  ensurePortsContract(projectRoot, config);
2464
2537
  await maybeModelDownload(options);
2538
+ refreshSnapshotOrExit(projectRoot, magicDir);
2465
2539
  return;
2466
2540
  }
2467
2541
  const missing = /* @__PURE__ */ new Map();
2468
2542
  for (const [relPath, content] of allFiles) {
2469
- if (!existsSync14(resolve10(projectRoot, relPath))) {
2543
+ if (!existsSync15(resolve10(projectRoot, relPath))) {
2470
2544
  missing.set(relPath, content);
2471
2545
  }
2472
2546
  }
2473
2547
  if (missing.size === 0) {
2474
2548
  console.log(SYNC_CONFIG.PROMPT_FULL);
2549
+ refreshSnapshotOrExit(projectRoot, magicDir);
2475
2550
  return;
2476
2551
  }
2477
2552
  let filesToWrite = missing;
@@ -2487,10 +2562,22 @@ async function syncCommand(options = {}) {
2487
2562
  await checkPrismaDiff(projectRoot, options);
2488
2563
  ensurePortsContract(projectRoot, config);
2489
2564
  await maybeModelDownload(options);
2565
+ refreshSnapshotOrExit(projectRoot, magicDir);
2566
+ }
2567
+ function refreshSnapshotOrExit(projectRoot, magicDir) {
2568
+ const snap = refreshL1SnapshotOnInstall({ projectRoot, magicDir });
2569
+ if (!snap.ok) {
2570
+ console.error(`
2571
+ \u2717 \u8BB0\u5FC6\u5FEB\u7167\u672A\u5C31\u7EEA: ${snap.detail}`);
2572
+ console.error(` \u590D\u73B0\u547D\u4EE4: ${snap.cmd}`);
2573
+ console.error(" \u4FEE\u590D\u540E\u91CD\u65B0\u8FD0\u884C add-coder sync\uFF08\u8BE5\u6B65\u9AA4\u4E3A\u786C\u95E8\u7981\uFF09\u3002");
2574
+ process.exit(1);
2575
+ }
2576
+ console.log(`\u2705 \u8BB0\u5FC6 L1 \u5FEB\u7167\u5DF2\u5237\u65B0: ${snap.relScript}`);
2490
2577
  }
2491
2578
  function printMigrateGuidance(targetPath, changed) {
2492
2579
  const g = SYNC_PRISMA_CONFIG.POST_SYNC;
2493
- const isManaged = existsSync14(resolve10(dirname4(targetPath), "migrations"));
2580
+ const isManaged = existsSync15(resolve10(dirname4(targetPath), "migrations"));
2494
2581
  const actions = isManaged ? g.MANAGED_ACTIONS : g.UNMANAGED_ACTIONS;
2495
2582
  console.log(` \u25B6 \u5DF2\u5199\u5165 ${changed} \u5904\u53D8\u66F4\uFF0C${g.HEADER}`);
2496
2583
  for (const a of actions) {
@@ -2529,7 +2616,7 @@ async function ensureAtlasCapability(projectRoot, yes = false) {
2529
2616
  console.log(" \u6587\u6863: README.md \u2192 \u7AE0\u8282\u300CAtlas \u6570\u636E\u5E93\u540C\u6B65\u80FD\u529B\u300D/ DEVELOPMENT.md \xA7\u4E5D");
2530
2617
  return;
2531
2618
  }
2532
- const pm = existsSync14(resolve10(projectRoot, "pnpm-lock.yaml")) ? "pnpm" : "npm";
2619
+ const pm = existsSync15(resolve10(projectRoot, "pnpm-lock.yaml")) ? "pnpm" : "npm";
2533
2620
  console.log(` \u5B89\u88C5 ${pm} add -D @ariga/atlas ...`);
2534
2621
  const r = runCommand(pm, ["add", "-D", "@ariga/atlas"], { cwd: projectRoot, timeout: 18e4 });
2535
2622
  if (r.status !== 0) {
@@ -2542,8 +2629,8 @@ async function ensureAtlasCapability(projectRoot, yes = false) {
2542
2629
  }
2543
2630
  function checkHostAtlasSegment(projectRoot) {
2544
2631
  const script = resolve10(projectRoot, "scripts", "db-ensure.sh");
2545
- if (!existsSync14(script)) return;
2546
- const content = readFileSync11(script, "utf-8");
2632
+ if (!existsSync15(script)) return;
2633
+ const content = readFileSync12(script, "utf-8");
2547
2634
  if (content.includes("atlas_sync")) return;
2548
2635
  console.warn(" \u26A0\uFE0F \u5BBF\u4E3B scripts/db-ensure.sh \u672A\u5305\u542B Atlas \u540C\u6B65\u6BB5\uFF08\u65E5\u5E38 db-ensure \u5C06\u7F3A\u5C11 ADD \u6CBB\u7406\u6A21\u578B\u540C\u6B65\uFF09");
2549
2636
  console.warn(" \u804C\u8D23\u8FB9\u754C: add-coder \u53EA\u540C\u6B65 ADD \u6CBB\u7406\u6A21\u578B(7 \u8868)\uFF1B\u5BBF\u4E3B\u4E1A\u52A1\u8868 diff \u63A8\u8350 Atlas \u4F46\u4E0D\u5F3A\u6C42");
@@ -2557,7 +2644,7 @@ async function checkPrismaDiff(projectRoot, options) {
2557
2644
  if (!options.patch) return;
2558
2645
  const basePath = resolve10(projectRoot, SYNC_PRISMA_CONFIG.BASE_SCHEMA);
2559
2646
  const targetPath = resolve10(projectRoot, SYNC_PRISMA_CONFIG.TARGET_PATTERN);
2560
- if (!existsSync14(basePath)) {
2647
+ if (!existsSync15(basePath)) {
2561
2648
  console.log(`
2562
2649
  \u26A0\uFE0F \u57FA\u51C6 schema \u4E0D\u5B58\u5728: ${basePath}`);
2563
2650
  console.log(` \u8BF7\u786E\u4FDD add-coder \u5DF2\u6B63\u786E\u5B89\u88C5\u3002`);
@@ -2571,7 +2658,7 @@ async function checkPrismaDiff(projectRoot, options) {
2571
2658
  \u2705 Prisma schema \u4E0E add-coder \u6807\u51C6\u4E00\u81F4\uFF0C\u65E0\u9700\u540C\u6B65\u3002`);
2572
2659
  return;
2573
2660
  }
2574
- const targetExists = existsSync14(targetPath);
2661
+ const targetExists = existsSync15(targetPath);
2575
2662
  let modifiedCount = 0;
2576
2663
  console.log(`
2577
2664
  \u26A0\uFE0F Prisma schema \u5DEE\u5F02\u68C0\u6D4B:`);
@@ -2722,7 +2809,7 @@ async function handleDiffAction(action, ctx) {
2722
2809
  }
2723
2810
  }
2724
2811
  function injectMissingModels(targetPath, models) {
2725
- let content = readFileSync11(targetPath, "utf-8");
2812
+ let content = readFileSync12(targetPath, "utf-8");
2726
2813
  content = content.replace(/\n*$/, "\n");
2727
2814
  content += `
2728
2815
  // ===== \u7531 add-coder sync --patch \u81EA\u52A8\u6CE8\u5165 (${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}) =====
@@ -2735,7 +2822,7 @@ function injectMissingModels(targetPath, models) {
2735
2822
  return models.length;
2736
2823
  }
2737
2824
  function getBaseFieldLines(basePath, modelName) {
2738
- const content = readFileSync11(basePath, "utf-8");
2825
+ const content = readFileSync12(basePath, "utf-8");
2739
2826
  const blocks = parseSchemaBlocks(content);
2740
2827
  const block = blocks.get(`model:${modelName}`) ?? blocks.get(`enum:${modelName}`);
2741
2828
  if (!block) return {};
@@ -2758,7 +2845,7 @@ function injectFieldLines(targetPath, basePath, modelName, fieldKeys) {
2758
2845
  console.warn(`\u26A0\uFE0F \u6CE8\u5165\u5931\u8D25\uFF1A${modelName} \u5728\u57FA\u51C6\u4E2D\u672A\u627E\u5230\u5B57\u6BB5\u5B9A\u4E49\uFF08${fieldKeys.length} \u4E2A\u5B57\u6BB5\u672A\u5199\u5165\uFF09`);
2759
2846
  return 0;
2760
2847
  }
2761
- const content = readFileSync11(targetPath, "utf-8");
2848
+ const content = readFileSync12(targetPath, "utf-8");
2762
2849
  const lines = content.split("\n");
2763
2850
  const blocks = parseSchemaBlocks(content);
2764
2851
  const target = blocks.get(`model:${modelName}`) ?? blocks.get(`enum:${modelName}`);
@@ -2805,7 +2892,7 @@ function injectFieldLines(targetPath, basePath, modelName, fieldKeys) {
2805
2892
  function overwriteFieldLines(targetPath, basePath, modelName, conflicts) {
2806
2893
  const baseFields = getBaseFieldLines(basePath, modelName);
2807
2894
  if (Object.keys(baseFields).length === 0) return 0;
2808
- let content = readFileSync11(targetPath, "utf-8");
2895
+ let content = readFileSync12(targetPath, "utf-8");
2809
2896
  let count = 0;
2810
2897
  for (const { fieldName } of conflicts) {
2811
2898
  const baseLine = baseFields[fieldName];
@@ -2821,8 +2908,176 @@ function overwriteFieldLines(targetPath, basePath, modelName, conflicts) {
2821
2908
  }
2822
2909
 
2823
2910
  // src/cli/commands/status.ts
2824
- import { existsSync as existsSync15 } from "fs";
2825
- import { resolve as resolve11 } from "path";
2911
+ import { existsSync as existsSync16, readFileSync as readFileSync13 } from "fs";
2912
+ import { homedir as homedir3 } from "os";
2913
+ import { join as join11, resolve as resolve11 } from "path";
2914
+ var VIRTUAL_TOOLS_SETTING_KEY = "github.copilot.chat.virtualTools.threshold";
2915
+ function stripJsonComments(text) {
2916
+ let out = "";
2917
+ let inString = false;
2918
+ let escaped = false;
2919
+ for (let i = 0; i < text.length; i++) {
2920
+ const ch = text[i];
2921
+ if (inString) {
2922
+ out += ch;
2923
+ if (escaped) escaped = false;
2924
+ else if (ch === "\\") escaped = true;
2925
+ else if (ch === '"') inString = false;
2926
+ continue;
2927
+ }
2928
+ if (ch === '"') {
2929
+ inString = true;
2930
+ out += ch;
2931
+ continue;
2932
+ }
2933
+ if (ch === "/" && text[i + 1] === "/") {
2934
+ while (i < text.length && text[i] !== "\n") i++;
2935
+ out += "\n";
2936
+ continue;
2937
+ }
2938
+ if (ch === "/" && text[i + 1] === "*") {
2939
+ i += 2;
2940
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
2941
+ i++;
2942
+ out += " ";
2943
+ continue;
2944
+ }
2945
+ out += ch;
2946
+ }
2947
+ return out;
2948
+ }
2949
+ function stripTrailingCommas(text) {
2950
+ let out = "";
2951
+ let inString = false;
2952
+ let escaped = false;
2953
+ for (let i = 0; i < text.length; i++) {
2954
+ const ch = text[i];
2955
+ if (inString) {
2956
+ out += ch;
2957
+ if (escaped) escaped = false;
2958
+ else if (ch === "\\") escaped = true;
2959
+ else if (ch === '"') inString = false;
2960
+ continue;
2961
+ }
2962
+ if (ch === '"') {
2963
+ inString = true;
2964
+ out += ch;
2965
+ continue;
2966
+ }
2967
+ if (ch === ",") {
2968
+ let j = i + 1;
2969
+ while (j < text.length && /\s/.test(text[j])) j++;
2970
+ if (text[j] === "}" || text[j] === "]") continue;
2971
+ }
2972
+ out += ch;
2973
+ }
2974
+ return out;
2975
+ }
2976
+ function parseJsonc(text) {
2977
+ return JSON.parse(stripTrailingCommas(stripJsonComments(text)));
2978
+ }
2979
+ function vscodeSettingsCandidates(input) {
2980
+ const platform = input.platform ?? process.platform;
2981
+ const home = input.homeDir ?? homedir3();
2982
+ const env = input.env ?? process.env;
2983
+ const variants = ["Code", "Code - Insiders"];
2984
+ let userRoots;
2985
+ if (platform === "win32") {
2986
+ const appData = env.APPDATA || join11(home, "AppData", "Roaming");
2987
+ userRoots = variants.map((v) => join11(appData, v, "User"));
2988
+ } else if (platform === "darwin") {
2989
+ userRoots = variants.map((v) => join11(home, "Library", "Application Support", v, "User"));
2990
+ } else {
2991
+ userRoots = variants.map((v) => join11(home, ".config", v, "User"));
2992
+ }
2993
+ return [
2994
+ join11(input.projectRoot, ".vscode", "settings.json"),
2995
+ ...userRoots.map((root) => join11(root, "settings.json"))
2996
+ ];
2997
+ }
2998
+ function checkVsCodeVirtualTools(input) {
2999
+ const candidates = vscodeSettingsCandidates(input).map((path) => {
3000
+ if (!existsSync16(path)) return { path, exists: false, hasKey: false, threshold: null };
3001
+ let parsed;
3002
+ try {
3003
+ parsed = parseJsonc(readFileSync13(path, "utf-8"));
3004
+ } catch (e) {
3005
+ return {
3006
+ path,
3007
+ exists: true,
3008
+ hasKey: false,
3009
+ threshold: null,
3010
+ parseError: e instanceof Error ? e.message : String(e)
3011
+ };
3012
+ }
3013
+ if (typeof parsed !== "object" || parsed === null) {
3014
+ return { path, exists: true, hasKey: false, threshold: null, parseError: "\u9876\u5C42\u4E0D\u662F JSON \u5BF9\u8C61" };
3015
+ }
3016
+ const raw = parsed[VIRTUAL_TOOLS_SETTING_KEY];
3017
+ if (raw === void 0) return { path, exists: true, hasKey: false, threshold: null };
3018
+ return {
3019
+ path,
3020
+ exists: true,
3021
+ hasKey: true,
3022
+ threshold: typeof raw === "number" ? raw : null
3023
+ };
3024
+ });
3025
+ const ok = candidates.some((c) => c.threshold === 0);
3026
+ const state = ok ? "ok" : candidates.some((c) => c.hasKey) ? "non-zero" : "missing";
3027
+ return { state, candidates };
3028
+ }
3029
+ function isVsCodeTarget(config, projectRoot) {
3030
+ if (config.magicDir === ".vscode" || config.adapters.includes("vscode")) return true;
3031
+ const root = projectRoot || config.projectRoot;
3032
+ if (!root) return false;
3033
+ if (existsSync16(join11(root, ".vscode", "mcp.json"))) return true;
3034
+ const settingsPath = join11(root, ".vscode", "settings.json");
3035
+ if (!existsSync16(settingsPath)) return false;
3036
+ try {
3037
+ const parsed = parseJsonc(readFileSync13(settingsPath, "utf-8"));
3038
+ if (typeof parsed !== "object" || parsed === null) return false;
3039
+ const obj = parsed;
3040
+ if (obj.mcp !== void 0) return true;
3041
+ return Object.keys(obj).some((k) => k.startsWith("github.copilot.chat."));
3042
+ } catch {
3043
+ return false;
3044
+ }
3045
+ }
3046
+ function virtualToolsNoticeLines(check, ctx) {
3047
+ const lines = [];
3048
+ const describe = (c) => {
3049
+ const where = c.path === join11(ctx.projectRoot, ".vscode", "settings.json") ? "\u5DE5\u4F5C\u533A" : "\u7528\u6237\u7EA7";
3050
+ if (!c.exists) return `${where}: ${c.path}\uFF08\u6587\u4EF6\u4E0D\u5B58\u5728\uFF09`;
3051
+ if (c.parseError) return `${where}: ${c.path}\uFF08\u89E3\u6790\u5931\u8D25: ${c.parseError}\uFF09`;
3052
+ if (!c.hasKey) return `${where}: ${c.path}\uFF08\u952E\u7F3A\u5931\uFF09`;
3053
+ return `${where}: ${c.path}\uFF08\u5F53\u524D\u503C ${c.threshold ?? "\u975E\u6570\u503C"}\uFF09`;
3054
+ };
3055
+ if (check.state === "ok") {
3056
+ const hit = check.candidates.find((c) => c.threshold === 0);
3057
+ lines.push(` \u2705 VS Code \u865A\u62DF\u5DE5\u5177\u6298\u53E0\u5DF2\u5173\u95ED\uFF08${hit ? hit.path : VIRTUAL_TOOLS_SETTING_KEY + " = 0"}\uFF09`);
3058
+ return lines;
3059
+ }
3060
+ lines.push(` \u26A0\uFE0F VS Code \u5BBF\u4E3B\u9002\u914D\uFF1A\u672A\u68C0\u6D4B\u5230 "${VIRTUAL_TOOLS_SETTING_KEY}": 0`);
3061
+ check.candidates.forEach((c) => lines.push(` - ${describe(c)}`));
3062
+ if (check.state === "non-zero") {
3063
+ lines.push(" \u8BE5\u952E\u5B58\u5728\u4F46\u53D6\u503C\u975E 0 \u21D2 \u6298\u53E0\u4ECD\u4F1A\u89E6\u53D1\uFF08\u89E6\u53D1\u7EBF = \u6240\u6709 MCP \u670D\u52A1\u5668\u5DE5\u5177\u603B\u6570 \u2265 \u9608\u503C/2\uFF0C\u9ED8\u8BA4 64\uFF09");
3064
+ } else {
3065
+ lines.push(" \u82E5\u4F60\u7684 Copilot / VS Code \u7248\u672C\u5DF2\u4E0D\u518D\u63D0\u4F9B\u8BE5\u8BBE\u7F6E\u9879\uFF0C\u53EF\u5FFD\u7565\u672C\u544A\u8B66\uFF08\u672C\u9879\u4E3A\u5EFA\u8BAE\u6027\u68C0\u67E5\uFF0C\u4E0D\u5F71\u54CD\u9000\u51FA\u7801\uFF09");
3066
+ }
3067
+ lines.push(" \u4FEE\u590D: \u5728\u7528\u6237\u7EA7\u6216\u5DE5\u4F5C\u533A\u7EA7 settings.json \u52A0\u5165\u4E0B\u9762\u8FD9\u884C\uFF0C\u7136\u540E\u300C\u91CD\u8F7D VS Code \u7A97\u53E3\u300D\uFF08Developer: Reload Window\uFF09");
3068
+ lines.push(` "${VIRTUAL_TOOLS_SETTING_KEY}": 0`);
3069
+ lines.push(' \u539F\u56E0: \u5DE5\u5177\u88AB\u5BBF\u4E3B\u6298\u53E0\u65F6\uFF0C\u672A\u6FC0\u6D3B\u4EE3\u7406\u5C31\u8C03\u7528\u4F1A\u8BEF\u62A5 "currently disabled by the user"\uFF0C');
3070
+ lines.push(" HITL \u5BA1\u6279\u94FE\uFF08update_hitl/status_hitl\uFF09\u4E0E ADD-7 \u5BA1\u8BA1\u94FE\uFF08record_dev_operation/query_audit_logs\uFF09\u4F1A\u540C\u65F6\u4E2D\u65AD");
3071
+ lines.push(` \u8BF4\u660E: ${ctx.docPath}\uFF08\u672C\u9879\u4E3A\u5EFA\u8BAE\u6027\u68C0\u67E5\uFF0C\u4E0D\u4F1A\u4FEE\u6539\u4F60\u7684\u8BBE\u7F6E\u6587\u4EF6\uFF09`);
3072
+ return lines;
3073
+ }
3074
+ function printVirtualToolsCheck(config) {
3075
+ const check = checkVsCodeVirtualTools({ projectRoot: config.projectRoot });
3076
+ const docPath = join11(config.magicDir || ".vscode", "docs", "ADD-governance-vscode-copilot.md");
3077
+ virtualToolsNoticeLines(check, { projectRoot: config.projectRoot, docPath }).forEach(
3078
+ (line) => console.log(line)
3079
+ );
3080
+ }
2826
3081
  async function statusCommand() {
2827
3082
  const projectRoot = process.cwd();
2828
3083
  const config = await loadConfig(projectRoot);
@@ -2831,7 +3086,7 @@ async function statusCommand() {
2831
3086
  const missing = [];
2832
3087
  const present = [];
2833
3088
  for (const [relPath] of coreFiles) {
2834
- if (existsSync15(resolve11(projectRoot, relPath))) {
3089
+ if (existsSync16(resolve11(projectRoot, relPath))) {
2835
3090
  present.push(relPath);
2836
3091
  } else {
2837
3092
  missing.push(relPath);
@@ -2842,15 +3097,22 @@ async function statusCommand() {
2842
3097
  if (missing.length > 0) {
2843
3098
  console.log(` \u7F3A\u5931: ${missing.length} \u6587\u4EF6`);
2844
3099
  missing.forEach((f) => console.log(` - ${f}`));
2845
- process.exit(1);
2846
3100
  } else {
2847
3101
  console.log(" \u6240\u6709\u6587\u4EF6\u5B8C\u6574\u3002");
2848
3102
  }
3103
+ if (isVsCodeTarget(config, projectRoot)) {
3104
+ console.log("");
3105
+ console.log("\u5BBF\u4E3B\u9002\u914D\u81EA\u68C0\uFF08VS Code Copilot\uFF09:");
3106
+ printVirtualToolsCheck(config);
3107
+ }
3108
+ if (missing.length > 0) {
3109
+ process.exit(1);
3110
+ }
2849
3111
  }
2850
3112
 
2851
3113
  // src/cli/commands/stack.ts
2852
- import { readdirSync as readdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync16, mkdirSync as mkdirSync8 } from "fs";
2853
- import { resolve as resolve12, join as join10 } from "path";
3114
+ import { readdirSync as readdirSync6, readFileSync as readFileSync14, writeFileSync as writeFileSync8, existsSync as existsSync17, mkdirSync as mkdirSync8 } from "fs";
3115
+ import { resolve as resolve12, join as join12 } from "path";
2854
3116
  import { createHash as createHash3 } from "crypto";
2855
3117
  var HASH_OUTPUT_FILE = ".add-coder-hash.json";
2856
3118
  function hash82(c) {
@@ -2866,13 +3128,13 @@ function resolveMagicDir2(projectRoot, specified) {
2866
3128
  }
2867
3129
  function listCustomProfiles(projectRoot, magicDir, registryNames) {
2868
3130
  const dir = resolve12(projectRoot, magicDir, "rules", "profiles");
2869
- if (!existsSync16(dir)) return [];
3131
+ if (!existsSync17(dir)) return [];
2870
3132
  return readdirSync6(dir).filter((f) => f.endsWith(".md") && !registryNames.has(f.replace(/-profile\.md$/, ""))).sort();
2871
3133
  }
2872
3134
  function profileExists(projectRoot, magicDir, name) {
2873
3135
  const registry = loadProfileRegistry();
2874
3136
  if (registry.some((p) => p.name === name)) return true;
2875
- return existsSync16(resolve12(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`));
3137
+ return existsSync17(resolve12(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`));
2876
3138
  }
2877
3139
  function stackCommand(sub, name, options = {}) {
2878
3140
  const projectRoot = process.cwd();
@@ -2910,11 +3172,11 @@ function stackCommand(sub, name, options = {}) {
2910
3172
  return;
2911
3173
  }
2912
3174
  const profilePath = resolve12(projectRoot, magicDir, "rules", "profiles", `${current}-profile.md`);
2913
- const stat = existsSync16(profilePath) ? readFileSync12(profilePath, "utf-8").length : 0;
3175
+ const stat = existsSync17(profilePath) ? readFileSync14(profilePath, "utf-8").length : 0;
2914
3176
  console.log(`\u6280\u672F\u6808: ${current}`);
2915
- console.log(`profile \u6587\u4EF6: ${profilePath}${existsSync16(profilePath) ? ` (${stat} \u5B57\u7B26)` : "\uFF08\u7F3A\u5931\uFF09"}`);
3177
+ console.log(`profile \u6587\u4EF6: ${profilePath}${existsSync17(profilePath) ? ` (${stat} \u5B57\u7B26)` : "\uFF08\u7F3A\u5931\uFF09"}`);
2916
3178
  try {
2917
- const raw = JSON.parse(readFileSync12(resolve12(projectRoot, magicDir, "stack.json"), "utf-8"));
3179
+ const raw = JSON.parse(readFileSync14(resolve12(projectRoot, magicDir, "stack.json"), "utf-8"));
2918
3180
  if (raw.updatedAt) console.log(`\u66F4\u65B0\u65F6\u95F4: ${raw.updatedAt}`);
2919
3181
  } catch {
2920
3182
  }
@@ -2940,8 +3202,8 @@ function buildConfig(projectRoot, magicDir, stack) {
2940
3202
  };
2941
3203
  try {
2942
3204
  const pkgPath = resolve12(projectRoot, "package.json");
2943
- if (existsSync16(pkgPath)) {
2944
- const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3205
+ if (existsSync17(pkgPath)) {
3206
+ const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
2945
3207
  if (pkg.name) config.projectName = pkg.name;
2946
3208
  }
2947
3209
  } catch {
@@ -2968,13 +3230,13 @@ function applyStack(projectRoot, magicDir, name) {
2968
3230
  }
2969
3231
  const hashMap = {};
2970
3232
  try {
2971
- Object.assign(hashMap, JSON.parse(readFileSync12(resolve12(projectRoot, magicDir, HASH_OUTPUT_FILE), "utf-8")));
3233
+ Object.assign(hashMap, JSON.parse(readFileSync14(resolve12(projectRoot, magicDir, HASH_OUTPUT_FILE), "utf-8")));
2972
3234
  } catch {
2973
3235
  }
2974
3236
  let written = 0;
2975
3237
  for (const [relPath, content] of stackRelated) {
2976
3238
  const targetPath = resolve12(projectRoot, relPath);
2977
- mkdirSync8(join10(targetPath, ".."), { recursive: true });
3239
+ mkdirSync8(join12(targetPath, ".."), { recursive: true });
2978
3240
  writeFileSync8(targetPath, content, "utf-8");
2979
3241
  hashMap[relPath] = hash82(content);
2980
3242
  written++;
@@ -2988,21 +3250,26 @@ function applyStack(projectRoot, magicDir, name) {
2988
3250
  const profilePathMagic = resolve12(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`);
2989
3251
  const profilePathAdd = resolve12(projectRoot, ADD_DIR, "rules", "profiles", `${name}-profile.md`);
2990
3252
  const projectRulesPath = resolve12(projectRoot, magicDir, "rules", "project_rules.md");
2991
- const projectRulesContent = existsSync16(projectRulesPath) ? readFileSync12(projectRulesPath, "utf-8") : "";
3253
+ const projectRulesContent = existsSync17(projectRulesPath) ? readFileSync14(projectRulesPath, "utf-8") : "";
2992
3254
  if (written === 0) fail(`\u672A\u6E32\u67D3\u4EFB\u4F55 stack \u76F8\u5173\u6587\u4EF6\uFF08${name}\uFF09\u2014\u2014Windows \u8DEF\u5F84\u5339\u914D\u5931\u6548\u9057\u7559\u95EE\u9898`);
2993
- if (magicDir !== magicDirFor("codex") && isBuiltin && !existsSync16(profilePathAdd)) fail(`profile \u672A\u5199\u5165 ${ADD_DIR}: ${profilePathAdd}`);
2994
- if (!existsSync16(profilePathMagic)) fail(`profile \u672A\u5199\u5165 ${magicDir}: ${profilePathMagic}`);
3255
+ if (magicDir !== magicDirFor("codex") && isBuiltin && !existsSync17(profilePathAdd)) fail(`profile \u672A\u5199\u5165 ${ADD_DIR}: ${profilePathAdd}`);
3256
+ if (!existsSync17(profilePathMagic)) fail(`profile \u672A\u5199\u5165 ${magicDir}: ${profilePathMagic}`);
2995
3257
  if (!projectRulesContent.includes("**\u5F53\u524D\u6280\u672F\u6808**") || !projectRulesContent.includes(name)) fail(`project_rules.md \u672A\u5305\u542B ${name} \u5F15\u7528`);
2996
3258
  writeFileSync8(resolve12(projectRoot, magicDir, HASH_OUTPUT_FILE), JSON.stringify(hashMap, null, 2) + "\n", "utf-8");
2997
3259
  console.log(`\u2705 \u6280\u672F\u6808\u5DF2\u8BBE\u7F6E\u4E3A ${name}`);
2998
3260
  console.log(` ${magicDir}/stack.json \u2192 ${name}`);
2999
- console.log(` ${magicDir}/rules/profiles/${name}-profile.md ${existsSync16(resolve12(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`)) ? "\u5DF2\u5C31\u4F4D" : "\uFF08\u81EA\u5B9A\u4E49 profile\uFF0C\u9879\u76EE\u4FA7\u6587\u4EF6\uFF09"}`);
3261
+ console.log(` ${magicDir}/rules/profiles/${name}-profile.md ${existsSync17(resolve12(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`)) ? "\u5DF2\u5C31\u4F4D" : "\uFF08\u81EA\u5B9A\u4E49 profile\uFF0C\u9879\u76EE\u4FA7\u6587\u4EF6\uFF09"}`);
3000
3262
  console.log(` project_rules.md \u5F15\u7528\u884C\u5DF2\u66F4\u65B0 + hash \u5DF2\u5237\u65B0\uFF08${written} \u4E2A\u6587\u4EF6\uFF09`);
3001
3263
  }
3002
3264
 
3003
3265
  // src/lib/memory-fts-objects.ts
3004
3266
  var PG_FTS_OBJECTS = [
3005
3267
  { name: "pg_trgm", kind: "extension", ddl: `CREATE EXTENSION IF NOT EXISTS pg_trgm;` },
3268
+ {
3269
+ name: "AddMemory_searchText_tsv_idx",
3270
+ kind: "index",
3271
+ ddl: `CREATE INDEX IF NOT EXISTS "AddMemory_searchText_tsv_idx" ON "public"."AddMemory" USING GIN (to_tsvector('simple', "searchText"));`
3272
+ },
3006
3273
  {
3007
3274
  name: "AddMemory_topic_trgm_idx",
3008
3275
  kind: "index",
@@ -3023,17 +3290,17 @@ var SQLITE_FTS_OBJECTS = [
3023
3290
  {
3024
3291
  name: "add_memory_fts",
3025
3292
  kind: "table",
3026
- ddl: `CREATE VIRTUAL TABLE IF NOT EXISTS add_memory_fts USING fts5(memory_id UNINDEXED, topic, content, tokenize = 'trigram');`
3293
+ ddl: `CREATE VIRTUAL TABLE IF NOT EXISTS add_memory_fts USING fts5(memory_id UNINDEXED, searchText, tokenize = 'unicode61');`
3027
3294
  },
3028
3295
  {
3029
3296
  name: "add_memory_fts_ai",
3030
3297
  kind: "trigger",
3031
- ddl: `CREATE TRIGGER IF NOT EXISTS add_memory_fts_ai AFTER INSERT ON "AddMemory" BEGIN INSERT INTO add_memory_fts(memory_id, topic, content) VALUES (new.id, new.topic, new.content); END;`
3298
+ ddl: `CREATE TRIGGER IF NOT EXISTS add_memory_fts_ai AFTER INSERT ON "AddMemory" BEGIN INSERT INTO add_memory_fts(memory_id, searchText) VALUES (new.id, new.searchText); END;`
3032
3299
  },
3033
3300
  {
3034
3301
  name: "add_memory_fts_au",
3035
3302
  kind: "trigger",
3036
- ddl: `CREATE TRIGGER IF NOT EXISTS add_memory_fts_au AFTER UPDATE ON "AddMemory" BEGIN DELETE FROM add_memory_fts WHERE memory_id = old.id; INSERT INTO add_memory_fts(memory_id, topic, content) VALUES (new.id, new.topic, new.content); END;`
3303
+ ddl: `CREATE TRIGGER IF NOT EXISTS add_memory_fts_au AFTER UPDATE ON "AddMemory" BEGIN DELETE FROM add_memory_fts WHERE memory_id = old.id; INSERT INTO add_memory_fts(memory_id, searchText) VALUES (new.id, new.searchText); END;`
3037
3304
  },
3038
3305
  {
3039
3306
  name: "add_memory_fts_ad",
@@ -3081,12 +3348,73 @@ function parsePgProbeMissing(stderr, expected) {
3081
3348
  return tail.split(/[\s"',\n]+/).map((s) => s.trim()).filter((s) => known.has(s));
3082
3349
  }
3083
3350
 
3084
- // src/lib/memory-fts-runtime.ts
3085
- import { existsSync as existsSync17, mkdtempSync, readFileSync as readFileSync13, rmSync as rmSync2, writeFileSync as writeFileSync9 } from "fs";
3351
+ // templates/core/scripts/mcp-server/shared/memory/retrieval/fts-fingerprint.ts
3352
+ import { createHash as createHash4 } from "crypto";
3353
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
3086
3354
  import { createRequire } from "module";
3355
+ import { dirname as dirname5, join as join13 } from "path";
3356
+ var TOKENIZATION_CONTRACT_VERSION = 1;
3357
+ function computeFtsFingerprint(input) {
3358
+ const userDictPath = input.userDictPath ?? join13(input.projectRoot, input.magicDir, "data", "jieba", "userdict.txt");
3359
+ let userDict = "none";
3360
+ if (existsSync18(userDictPath)) {
3361
+ userDict = createHash4("sha256").update(readFileSync15(userDictPath)).digest("hex").slice(0, 12);
3362
+ }
3363
+ const jieba = input.jiebaVersion ?? detectJiebaVersion();
3364
+ const value = createHash4("sha256").update(`tokenization-v${TOKENIZATION_CONTRACT_VERSION}|${jieba ?? "bigram"}|${userDict}`).digest("hex").slice(0, 12);
3365
+ return { value, parts: { jieba: jieba ?? "bigram", userDict, contract: TOKENIZATION_CONTRACT_VERSION } };
3366
+ }
3367
+ function detectJiebaVersion() {
3368
+ try {
3369
+ const req = createRequire(import.meta.url);
3370
+ const pkg = req("@node-rs/jieba/package.json");
3371
+ return pkg.version ?? "unknown";
3372
+ } catch {
3373
+ return null;
3374
+ }
3375
+ }
3376
+ function fingerprintMarkerPath(projectRoot, magicDir) {
3377
+ return join13(projectRoot, magicDir, "memory", "fts-fingerprint.json");
3378
+ }
3379
+ function readRecordedFingerprint(projectRoot, magicDir) {
3380
+ const p = fingerprintMarkerPath(projectRoot, magicDir);
3381
+ if (!existsSync18(p)) return null;
3382
+ try {
3383
+ return JSON.parse(readFileSync15(p, "utf-8"));
3384
+ } catch {
3385
+ return null;
3386
+ }
3387
+ }
3388
+ function writeRecordedFingerprint(projectRoot, magicDir, fingerprint, now = /* @__PURE__ */ new Date()) {
3389
+ const p = fingerprintMarkerPath(projectRoot, magicDir);
3390
+ mkdirSync9(dirname5(p), { recursive: true });
3391
+ const payload = {
3392
+ fingerprint: fingerprint.value,
3393
+ recordedAt: now.toISOString(),
3394
+ parts: fingerprint.parts
3395
+ };
3396
+ writeFileSync9(p, JSON.stringify(payload, null, 2) + "\n", "utf-8");
3397
+ return p;
3398
+ }
3399
+ function checkFtsFingerprint(projectRoot, magicDir) {
3400
+ const current = computeFtsFingerprint({ projectRoot, magicDir });
3401
+ const recorded = readRecordedFingerprint(projectRoot, magicDir);
3402
+ if (!recorded) {
3403
+ return { requiresReindex: true, reason: "\u672A\u8BB0\u5F55\u6307\u7EB9\uFF08\u65E0\u6CD5\u8BC1\u660E\u5386\u53F2 searchText \u4E0E\u5F53\u524D\u5206\u8BCD\u5668\u540C\u6E90\uFF09", current, recorded };
3404
+ }
3405
+ if (recorded.fingerprint !== current.value) {
3406
+ const diff = ["jieba", "userDict", "contract"].filter((k) => recorded.parts?.[k] !== current.parts[k]).map((k) => `${k}: ${String(recorded.parts?.[k])} \u2192 ${String(current.parts[k])}`).join("; ");
3407
+ return { requiresReindex: true, reason: `\u6307\u7EB9\u4E0D\u4E00\u81F4\uFF08${diff}\uFF09`, current, recorded };
3408
+ }
3409
+ return { requiresReindex: false, reason: "\u6307\u7EB9\u4E00\u81F4", current, recorded };
3410
+ }
3411
+
3412
+ // src/lib/memory-fts-runtime.ts
3413
+ import { existsSync as existsSync19, mkdtempSync, readFileSync as readFileSync16, rmSync as rmSync2, writeFileSync as writeFileSync10 } from "fs";
3414
+ import { createRequire as createRequire2 } from "module";
3087
3415
  import { tmpdir } from "os";
3088
- import { isAbsolute, join as join11, resolve as resolve13 } from "path";
3089
- var requireBuiltin = createRequire(import.meta.url);
3416
+ import { isAbsolute, join as join14, resolve as resolve13 } from "path";
3417
+ var requireBuiltin = createRequire2(import.meta.url);
3090
3418
  function loadNodeSqlite() {
3091
3419
  return requireBuiltin("node:sqlite");
3092
3420
  }
@@ -3126,7 +3454,7 @@ function sqlitePathCandidates(databaseUrl, projectRoot) {
3126
3454
  }
3127
3455
  function createSqliteAdapter(opts) {
3128
3456
  const candidates = opts.dbPath ? [opts.dbPath] : sqlitePathCandidates(opts.databaseUrl, opts.projectRoot);
3129
- const dbPath = candidates.find((p) => existsSync17(p));
3457
+ const dbPath = candidates.find((p) => existsSync19(p));
3130
3458
  const source = dbPath ?? `${opts.databaseUrl}\uFF08\u672A\u627E\u5230\u5E93\u6587\u4EF6\uFF0C\u5DF2\u8BD5\uFF1A${candidates.join(" / ")}\uFF09`;
3131
3459
  function withDb(fn) {
3132
3460
  if (!dbPath) {
@@ -3168,8 +3496,8 @@ function createSqliteAdapter(opts) {
3168
3496
  function readPrismaDatasourceProvider(projectRoot) {
3169
3497
  for (const rel of ["prisma/schema.prisma", "prisma/add.prisma"]) {
3170
3498
  const p = resolve13(projectRoot, rel);
3171
- if (!existsSync17(p)) continue;
3172
- const m = readFileSync13(p, "utf-8").match(/datasource\s+\w+\s*\{[\s\S]*?provider\s*=\s*"([^"]+)"/);
3499
+ if (!existsSync19(p)) continue;
3500
+ const m = readFileSync16(p, "utf-8").match(/datasource\s+\w+\s*\{[\s\S]*?provider\s*=\s*"([^"]+)"/);
3173
3501
  if (m) return m[1];
3174
3502
  }
3175
3503
  return null;
@@ -3184,8 +3512,8 @@ function resolveProjectDatabaseUrl(projectRoot, env = process.env) {
3184
3512
  if (env.DATABASE_URL) return env.DATABASE_URL;
3185
3513
  for (const rel of [".env.development", ".env"]) {
3186
3514
  const p = resolve13(projectRoot, rel);
3187
- if (!existsSync17(p)) continue;
3188
- const m = readFileSync13(p, "utf-8").match(/^\s*DATABASE_URL\s*=\s*["']?([^"'\n]+)["']?\s*$/m);
3515
+ if (!existsSync19(p)) continue;
3516
+ const m = readFileSync16(p, "utf-8").match(/^\s*DATABASE_URL\s*=\s*["']?([^"'\n]+)["']?\s*$/m);
3189
3517
  if (m) return m[1].trim();
3190
3518
  }
3191
3519
  return null;
@@ -3197,9 +3525,9 @@ function createPrismaCliAdapter(opts) {
3197
3525
  const run = opts.run ?? ((argv, cwd) => runCommand(pm, argv, { cwd, timeout: opts.timeoutMs ?? 12e4 }));
3198
3526
  const source = opts.describeSource ?? "prisma db execute\uFF08\u9879\u76EE DATABASE_URL\uFF09";
3199
3527
  function executeSql(sql, tag) {
3200
- const dir = mkdtempSync(join11(tmpdir(), "add-coder-fts-"));
3201
- const file = join11(dir, `${tag}.sql`);
3202
- writeFileSync9(file, sql, "utf-8");
3528
+ const dir = mkdtempSync(join14(tmpdir(), "add-coder-fts-"));
3529
+ const file = join14(dir, `${tag}.sql`);
3530
+ writeFileSync10(file, sql, "utf-8");
3203
3531
  try {
3204
3532
  const r = run([...base, "db", "execute", "--file", file], opts.projectRoot);
3205
3533
  return { status: r.status, stderr: r.stderr ?? "" };
@@ -3272,6 +3600,18 @@ function printReport(report, mode, io) {
3272
3600
  const head = mode === "apply" ? "\u8BB0\u5FC6 FTS \u671F\u671B\u6001 \xB7 \u91CD\u5EFA" : "\u8BB0\u5FC6 FTS \u671F\u671B\u6001 \xB7 \u63A2\u6D4B";
3273
3601
  io.log(`${head}\uFF08backend=${report.backend}\uFF0Csource=${report.source}\uFF09`);
3274
3602
  io.log(` \u5B8C\u6210\u5EA6 ${report.present}/${report.total}\uFF08${report.progress}%\uFF09`);
3603
+ try {
3604
+ const projectRoot = process.cwd();
3605
+ const check = checkFtsFingerprint(projectRoot, resolveMagicDir(projectRoot));
3606
+ if (check.requiresReindex) {
3607
+ io.log(` \u26A0\uFE0F \u68C0\u7D22\u6307\u7EB9\uFF1A\u9700\u91CD\u7D22\u5F15 \u2014\u2014 ${check.reason}`);
3608
+ io.log(" \u2192 \u5904\u7F6E\uFF1Aadd-coder memory:reindex --apply \u540E\u7531\u56DE\u586B\u811A\u672C\u91CD\u7B97 searchText\uFF08\u6307\u7EB9\u968F\u91CD\u5EFA\u5199\u5165\uFF09");
3609
+ } else {
3610
+ io.log(` \u2705 \u68C0\u7D22\u6307\u7EB9\u4E00\u81F4\uFF08${check.current.value}\uFF09`);
3611
+ }
3612
+ } catch (error) {
3613
+ io.log(` \u26A0\uFE0F \u68C0\u7D22\u6307\u7EB9\uFF1A\u65E0\u6CD5\u5224\u5B9A\uFF08${error instanceof Error ? error.message : String(error)}\uFF09`);
3614
+ }
3275
3615
  if (mode === "apply" && report.rebuilt.length > 0) {
3276
3616
  io.log(` \u672C\u6B21\u91CD\u5EFA\uFF08${report.rebuilt.length}\uFF09\uFF1A${report.rebuilt.join(", ")}`);
3277
3617
  }
@@ -3293,8 +3633,19 @@ async function memoryReindexCommand(options, deps = {}) {
3293
3633
  try {
3294
3634
  const adapter = resolveAdapter3(projectRoot, options, deps);
3295
3635
  const report = mode === "apply" ? await reindexVia(adapter) : await probeVia(adapter);
3636
+ let fingerprintNote = null;
3637
+ if (mode === "apply" && report.missing.length === 0) {
3638
+ try {
3639
+ const magicDir = resolveMagicDir(projectRoot);
3640
+ const path = writeRecordedFingerprint(projectRoot, magicDir, computeFtsFingerprint({ projectRoot, magicDir }));
3641
+ fingerprintNote = ` \u68C0\u7D22\u6307\u7EB9\uFF1A\u5DF2\u8BB0\u5F55\uFF08${path}\uFF09`;
3642
+ } catch (e) {
3643
+ fingerprintNote = ` \u26A0\uFE0F \u68C0\u7D22\u6307\u7EB9\u5199\u5165\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}\uFF08\u4E0B\u6B21 probe \u4ECD\u4F1A\u63D0\u793A\u9700\u91CD\u7D22\u5F15\uFF09`;
3644
+ }
3645
+ }
3296
3646
  if (options.json) io.log(JSON.stringify(report, null, 2));
3297
3647
  else printReport(report, mode, io);
3648
+ if (fingerprintNote && !options.json) io.log(fingerprintNote);
3298
3649
  if (mode === "probe") return 0;
3299
3650
  return report.missing.length > 0 ? 1 : 0;
3300
3651
  } catch (e) {
@@ -3309,7 +3660,7 @@ async function memoryReindexCommand(options, deps = {}) {
3309
3660
 
3310
3661
  // src/cli/index.ts
3311
3662
  var { version } = JSON.parse(
3312
- readFileSync14(new URL("../package.json", import.meta.url), "utf-8")
3663
+ readFileSync17(new URL("../package.json", import.meta.url), "utf-8")
3313
3664
  );
3314
3665
  async function modelDownloadCommand(options) {
3315
3666
  try {