add-coder 0.3.24 → 0.3.25-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -10
- package/dist/index.js +66 -6
- package/package.json +1 -1
- package/templates/.add-coder-src-hash.json +9 -7
- package/templates/adapters/codex/config.toml.example +20 -0
- package/templates/core/scripts/db-ensure.sh +8 -0
- package/templates/core/scripts/mcp-server/shared/redact.ts +28 -0
- package/templates/core/scripts/mcp-server/shared/response.ts +3 -2
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +69 -2
- package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +1 -1
- package/templates/core/scripts/mcp-server/tools/index.ts +66 -1
- package/templates/core/scripts/mcp-server.ts +2 -1
package/README.md
CHANGED
|
@@ -220,21 +220,47 @@ Plan 里的 Task 不应该停留在文档里。add-coder 将 tasks.md 末尾的
|
|
|
220
220
|
tasks.md §IDE JSON → TodoWrite → IDE 面板
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
### ⑩
|
|
223
|
+
### ⑩ 并发契约体系:协作层 + 进程层双层
|
|
224
224
|
|
|
225
|
-
多个 Agent 同时改一个仓库,没有契约必然冲突——改同一批文件、审计归因混乱。add-coder
|
|
225
|
+
多个 Agent 同时改一个仓库,没有契约必然冲突——改同一批文件、审计归因混乱。add-coder 把并行协作变成**经 HITL 审批的契约体系**,分两层:
|
|
226
226
|
|
|
227
|
-
|
|
|
228
|
-
|
|
229
|
-
|
|
|
230
|
-
|
|
|
231
|
-
| **仲裁链路** | 跨边界修改走 BOUNDARY_REQUEST → Lead 裁决 → 落库可查 |
|
|
232
|
-
| **审计分桶** | 每个专家独立 planKeyword,query_audit_logs 各域各查 |
|
|
227
|
+
| 层 | 契约 | 版本 | 职责 |
|
|
228
|
+
|----|------|------|------|
|
|
229
|
+
| **协作层** | 并发协作契约(collab-contract) | v1(v0.3.18) | 多智能体协作秩序:总控 Plan + N 个子 Plan / 文件边界 / 仲裁链路 / 审计分桶 |
|
|
230
|
+
| **进程层** | [多 IDE 进程并发契约](./docs/multi-ide-concurrency-contract.md) | v2(v0.3.25) | MCP Server 并发行为承诺:连接模型 / 幂等键 / PROJECT_ID 校验 / 断开隔离四态 / 生命周期拆分 / client 编排差异矩阵 |
|
|
233
231
|
|
|
234
|
-
>
|
|
232
|
+
> 协作层模板:`templates/core/templates/collab-contract-template.md`(契约新建/重大变更走 `COLLAB_CONTRACT` 审批)。
|
|
233
|
+
> 进程层文档:`docs/multi-ide-concurrency-contract.md`(Codex Parallel MCP / TAgent / Claude Code 并发行为对齐基准)。
|
|
235
234
|
>
|
|
236
235
|
> 📜 溯源:并发契约原创时间戳 → [CHANGELOG v0.3.18「并发协作契约」](https://github.com/xiaomingming92/add-coder/blob/main/CHANGELOG.md#0318---2026-08-05);"酷"的工程学定义 → [what-makes-software-cool.md](https://github.com/xiaomingming92/add-coder/blob/main/docs/what-makes-software-cool.md)——契约的审计分桶与完成判定(DPS ≥ 80)正长在"熵值管控"四维上。
|
|
237
236
|
|
|
237
|
+
### ⑪ Codex MCP 原生接入(v0.3.25)
|
|
238
|
+
|
|
239
|
+
> **状态区分**:"已生成 Codex 模板" ≠ "Codex MCP 端到端已验证"——以下 6 步是**已验证闭环**,不写自定义脚本即可完成接入。
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
# 1. 安装 add-coder(已安装可跳过)
|
|
243
|
+
npm i -g add-coder
|
|
244
|
+
|
|
245
|
+
# 2. 初始化 Codex 适配(hooks 模板 + config.toml 真源)
|
|
246
|
+
add-coder init --adapter=codex
|
|
247
|
+
|
|
248
|
+
# 3. 输出可直接使用的 config.toml 片段(不写盘,不初始化项目)
|
|
249
|
+
add-coder init --adapter=codex --print-mcp-config
|
|
250
|
+
|
|
251
|
+
# 4a. 粘贴片段到 ~/.codex/config.toml(Windows: %USERPROFILE%\.codex\config.toml)
|
|
252
|
+
# 4b. 或自动写入(显式确认 + 先备份 + 防重复)
|
|
253
|
+
add-coder init --adapter=codex --write-user-config
|
|
254
|
+
|
|
255
|
+
# 5. 重启 Codex(App/CLI/IDE 扩展通用,修改 config.toml 后需重启生效)
|
|
256
|
+
|
|
257
|
+
# 6. 验证:Codex 中发现 add_coder MCP Server,完整工具集可调用(29 tools)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Windows 分支**:`--print-mcp-config` 在 win32 平台自动输出 `cmd /c npx.cmd` 启动分支(原生 PowerShell 场景,不依赖 WSL)。
|
|
261
|
+
|
|
262
|
+
**命名兼容**:MCP Server ID 归一化为 `add_coder`(连字符→下划线,Codex 约束);`env.PROJECT_ROOT` 由渲染时注入,多项目粘贴错误配置时 mcp-server 启动即校验退出(进程层契约 §4)。
|
|
263
|
+
|
|
238
264
|
---
|
|
239
265
|
|
|
240
266
|
## 📖 案例示范:老安卓设备续命工程
|
|
@@ -351,7 +377,7 @@ npx add-coder init
|
|
|
351
377
|
| `.qoder/` | Qoder 适配(hooks、settings.json、mcp.json) |
|
|
352
378
|
| `.vscode/` | VS Code 适配(settings.json、tasks.json) |
|
|
353
379
|
| `.trae/` | Trae 适配(hooks.json、settings.json) |
|
|
354
|
-
| `.codex/` | Codex 适配(hooks.json、settings.json) |
|
|
380
|
+
| `.codex/` | Codex 适配(hooks.json、settings.json、config.toml.example) |
|
|
355
381
|
|
|
356
382
|
## MCP 审计工具链
|
|
357
383
|
|
package/dist/index.js
CHANGED
|
@@ -623,8 +623,18 @@ function renderAdapter4(config, targetDir, dryRun, magicDir) {
|
|
|
623
623
|
}
|
|
624
624
|
|
|
625
625
|
// src/adapters/codex/renderer.ts
|
|
626
|
+
function codexCommandFor(platform) {
|
|
627
|
+
return platform === "win32" ? "cmd /c npx.cmd" : "npx";
|
|
628
|
+
}
|
|
626
629
|
function renderAdapter5(config, targetDir, dryRun, magicDir) {
|
|
627
|
-
|
|
630
|
+
const files = renderAdapterBase(config, magicDir, magicDir === ".vscode", dryRun);
|
|
631
|
+
const codexCommand = codexCommandFor(process.platform);
|
|
632
|
+
for (const [rel, content] of files) {
|
|
633
|
+
if (rel.endsWith("config.toml.example") || rel.endsWith("config.toml")) {
|
|
634
|
+
files.set(rel, content.replaceAll("{{codexCommand}}", codexCommand));
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return files;
|
|
628
638
|
}
|
|
629
639
|
|
|
630
640
|
// src/lib/utils.ts
|
|
@@ -1366,7 +1376,8 @@ async function injectPrisma2(projectRoot, options = {}) {
|
|
|
1366
1376
|
// src/cli/commands/init.ts
|
|
1367
1377
|
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync11, mkdirSync as mkdirSync6, copyFileSync as copyFileSync2, readdirSync as readdirSync4 } from "fs";
|
|
1368
1378
|
import { createHash } from "crypto";
|
|
1369
|
-
import { resolve as resolve8 } from "path";
|
|
1379
|
+
import { resolve as resolve8, dirname as dirname5, join as pathJoin } from "path";
|
|
1380
|
+
import { homedir as homedir2 } from "os";
|
|
1370
1381
|
import { createConnection as createConnection2 } from "net";
|
|
1371
1382
|
|
|
1372
1383
|
// src/lib/model-predownload.ts
|
|
@@ -1455,6 +1466,10 @@ var ADAPTER_RENDERERS = {
|
|
|
1455
1466
|
};
|
|
1456
1467
|
var MAGIC_DIR_MAP = { claude: ".claude", qoder: ".qoder", vscode: ".vscode", trae: ".trae", codex: ".codex" };
|
|
1457
1468
|
async function initCommand(options) {
|
|
1469
|
+
if (options.printMcpConfig || options.writeUserConfig) {
|
|
1470
|
+
await handleCodexMcpConfig(options);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1458
1473
|
const ctx = await prepare(options);
|
|
1459
1474
|
writeComposeEnv(ctx);
|
|
1460
1475
|
if (!options.dryRun) {
|
|
@@ -1477,6 +1492,51 @@ async function initCommand(options) {
|
|
|
1477
1492
|
}
|
|
1478
1493
|
}
|
|
1479
1494
|
}
|
|
1495
|
+
async function handleCodexMcpConfig(options) {
|
|
1496
|
+
const projectRoot = process.cwd();
|
|
1497
|
+
const magicDir = MAGIC_DIR_MAP[options.adapter ?? "codex"] ?? ".codex";
|
|
1498
|
+
const config = await loadConfig(projectRoot, options.config, { force: true });
|
|
1499
|
+
config.projectRoot = projectRoot;
|
|
1500
|
+
config.magicDir = magicDir;
|
|
1501
|
+
const files = renderAdapter5(config, projectRoot, !!options.dryRun, magicDir);
|
|
1502
|
+
const tomlEntry = [...files.entries()].find(
|
|
1503
|
+
([rel]) => rel.endsWith("config.toml.example") || rel.endsWith("config.toml")
|
|
1504
|
+
);
|
|
1505
|
+
if (!tomlEntry) {
|
|
1506
|
+
console.error("\u2717 config.toml \u6E32\u67D3\u4EA7\u7269\u7F3A\u5931\uFF08\u68C0\u67E5 templates/adapters/codex/config.toml.example\uFF09");
|
|
1507
|
+
process.exit(1);
|
|
1508
|
+
}
|
|
1509
|
+
const toml = tomlEntry[1];
|
|
1510
|
+
if (options.printMcpConfig) {
|
|
1511
|
+
console.log("\n# \u5C06\u4EE5\u4E0B\u7247\u6BB5\u7C98\u8D34\u5230 ~/.codex/config.toml\uFF08\u6216\u4F7F\u7528 --write-user-config \u81EA\u52A8\u5199\u5165\uFF09");
|
|
1512
|
+
console.log(toml);
|
|
1513
|
+
}
|
|
1514
|
+
if (options.writeUserConfig) {
|
|
1515
|
+
const userConfigPath = pathJoin(homedir2(), ".codex", "config.toml");
|
|
1516
|
+
if (options.dryRun) {
|
|
1517
|
+
console.log(`[dry-run] \u5C06\u5199\u5165 ${userConfigPath}`);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
if (existsSync11(userConfigPath) && readFileSync9(userConfigPath, "utf-8").includes("mcp_servers.add_coder")) {
|
|
1521
|
+
console.log(`\u26A0\uFE0F ${userConfigPath} \u5DF2\u5B58\u5728 mcp_servers.add_coder \u914D\u7F6E\uFF0C\u8DF3\u8FC7\uFF08\u907F\u514D\u91CD\u590D\uFF09`);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
const answer = await ask(`\u5199\u5165 ${userConfigPath}\uFF1F\uFF08y/N\uFF09 \u2192 `);
|
|
1525
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
1526
|
+
console.log("\u5DF2\u53D6\u6D88\u5199\u5165");
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
if (existsSync11(userConfigPath)) {
|
|
1530
|
+
const backup = `${userConfigPath}.bak-${Date.now()}`;
|
|
1531
|
+
copyFileSync2(userConfigPath, backup);
|
|
1532
|
+
console.log(`\u5DF2\u5907\u4EFD\u539F\u914D\u7F6E \u2192 ${backup}`);
|
|
1533
|
+
}
|
|
1534
|
+
mkdirSync6(dirname5(userConfigPath), { recursive: true });
|
|
1535
|
+
writeFileSync6(userConfigPath, `
|
|
1536
|
+
${toml}`, { flag: "a" });
|
|
1537
|
+
console.log(`\u2705 \u5DF2\u5199\u5165 ${userConfigPath}\uFF0C\u91CD\u542F Codex \u751F\u6548`);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1480
1540
|
async function resolveAdapter(projectRoot, specified) {
|
|
1481
1541
|
if (specified) {
|
|
1482
1542
|
if (!MAGIC_DIR_MAP[specified]) throw new Error(`\u672A\u77E5 adapter: ${specified}`);
|
|
@@ -1899,7 +1959,7 @@ function finalize(ctx, result, dbFail) {
|
|
|
1899
1959
|
|
|
1900
1960
|
// src/cli/commands/sync.ts
|
|
1901
1961
|
import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
|
|
1902
|
-
import { resolve as resolve9, dirname as
|
|
1962
|
+
import { resolve as resolve9, dirname as dirname6 } from "path";
|
|
1903
1963
|
import { createHash as createHash2 } from "crypto";
|
|
1904
1964
|
|
|
1905
1965
|
// src/lib/path-normalize.ts
|
|
@@ -2144,7 +2204,7 @@ async function syncCommand(options = {}) {
|
|
|
2144
2204
|
}
|
|
2145
2205
|
function printMigrateGuidance(targetPath, changed) {
|
|
2146
2206
|
const g = SYNC_PRISMA_CONFIG.POST_SYNC;
|
|
2147
|
-
const isManaged = existsSync12(resolve9(
|
|
2207
|
+
const isManaged = existsSync12(resolve9(dirname6(targetPath), "migrations"));
|
|
2148
2208
|
const actions = isManaged ? g.MANAGED_ACTIONS : g.UNMANAGED_ACTIONS;
|
|
2149
2209
|
console.log(` \u25B6 \u5DF2\u5199\u5165 ${changed} \u5904\u53D8\u66F4\uFF0C${g.HEADER}`);
|
|
2150
2210
|
for (const a of actions) {
|
|
@@ -2338,7 +2398,7 @@ async function checkPrismaDiff(projectRoot, options) {
|
|
|
2338
2398
|
} else {
|
|
2339
2399
|
console.log(`
|
|
2340
2400
|
\u76EE\u6807 schema \u6587\u4EF6\u4E0D\u5B58\u5728: ${result.targetPath}`);
|
|
2341
|
-
mkdirSync7(
|
|
2401
|
+
mkdirSync7(dirname6(targetPath), { recursive: true });
|
|
2342
2402
|
writeFileSync7(targetPath, "// add.prisma \u2014 ADD \u6CBB\u7406\u6A21\u578B\n\n", "utf-8");
|
|
2343
2403
|
const n = injectMissingModels(targetPath, result.missing);
|
|
2344
2404
|
console.log(` \u2705 \u5DF2\u521B\u5EFA\u5E76\u6CE8\u5165 ${n} \u4E2A\u6A21\u578B/\u679A\u4E3E`);
|
|
@@ -2668,7 +2728,7 @@ async function modelDownloadCommand(options) {
|
|
|
2668
2728
|
}
|
|
2669
2729
|
var program = new Command();
|
|
2670
2730
|
program.name("add-coder").description("\u521D\u59CB\u5316 ADD \u8303\u5F0F\u5DE5\u4F5C\u6D41\u6A21\u677F").version(version);
|
|
2671
|
-
program.command("init").description("\u521D\u59CB\u5316 ADD \u6A21\u677F\u5230\u5F53\u524D\u9879\u76EE").option("--adapter <type>", "\u76EE\u6807 IDE: claude | qoder | vscode | trae | codex").option("--config <path>", "\u6307\u5B9A\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84").option("--force", "\u8986\u76D6\u5DF2\u6709\u6587\u4EF6\uFF0C\u4E0D\u4EA4\u4E92").option("--dry-run", "\u9884\u89C8\u6A21\u5F0F\uFF0C\u4E0D\u5B9E\u9645\u5199\u5165").option("--stack <name>", "\u6280\u672F\u6808\u7EA6\u675F profile \u540D\uFF08\u5982 machineserver\uFF0C\u53EF\u9009\uFF09").option("--skip-model", "\u8DF3\u8FC7 embedding \u6A21\u578B\u9884\u4E0B\u8F7D").action(initCommand);
|
|
2731
|
+
program.command("init").description("\u521D\u59CB\u5316 ADD \u6A21\u677F\u5230\u5F53\u524D\u9879\u76EE").option("--adapter <type>", "\u76EE\u6807 IDE: claude | qoder | vscode | trae | codex").option("--config <path>", "\u6307\u5B9A\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84").option("--force", "\u8986\u76D6\u5DF2\u6709\u6587\u4EF6\uFF0C\u4E0D\u4EA4\u4E92").option("--dry-run", "\u9884\u89C8\u6A21\u5F0F\uFF0C\u4E0D\u5B9E\u9645\u5199\u5165").option("--stack <name>", "\u6280\u672F\u6808\u7EA6\u675F profile \u540D\uFF08\u5982 machineserver\uFF0C\u53EF\u9009\uFF09").option("--skip-model", "\u8DF3\u8FC7 embedding \u6A21\u578B\u9884\u4E0B\u8F7D").option("--print-mcp-config", "Codex: stdout \u8F93\u51FA config.toml \u7247\u6BB5\uFF08\u4E0D\u5199\u76D8\uFF0C\u4E0D\u521D\u59CB\u5316\u9879\u76EE\uFF09").option("--write-user-config", "Codex: \u663E\u5F0F\u786E\u8BA4\u540E\u5199\u5165 ~/.codex/config.toml\uFF08\u5148\u5907\u4EFD\uFF09").action(initCommand);
|
|
2672
2732
|
program.command("sync").description("\u589E\u91CF\u540C\u6B65\u7F3A\u5931\u6587\u4EF6\uFF08--patch \u8986\u76D6\u5DF2\u6709\u6A21\u677F\uFF09").option("--adapter <type>", "\u76EE\u6807 IDE: claude | qoder | vscode | trae | codex").option("--patch", "\u8986\u76D6\u5DF2\u6709\u6A21\u677F\u6587\u4EF6\uFF08\u4E0D\u78B0 plans/specs/reviews\uFF09").option("-i, --interactive", "\u4EA4\u4E92\u5F0F\u9009\u62E9\u8981\u540C\u6B65\u7684\u6587\u4EF6").option("--model", "\u68C0\u6D4B\u5230\u7F3A\u5931\u65F6\u4E0B\u8F7D embedding \u6A21\u578B").action(syncCommand);
|
|
2673
2733
|
program.command("status").description("\u68C0\u67E5 ADD \u6A21\u677F\u5B8C\u6574\u6027").action(statusCommand);
|
|
2674
2734
|
program.command("stack").description("\u7BA1\u7406\u6280\u672F\u6808\u7EA6\u675F profile\uFF08list / set <name> / show / --clear\uFF09").argument("[sub]", "list | set | show").argument("[name]", "set <name>: profile \u540D\uFF08\u5185\u7F6E\u6216\u81EA\u5B9A\u4E49\uFF09").option("--adapter <type>", "\u76EE\u6807 IDE: claude | qoder | vscode | trae | codex").option("--clear", "\u6E05\u9664\u6280\u672F\u6808\u8BBE\u7F6E\uFF08\u4E2D\u6027\uFF09").action(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "add-coder",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.25-0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "[codein2027](https://github.com/xiaomingming92/codein2027) - A complete scaffolding for building the ADD programming paradigm, the implementation layer for AI code governance. Core principle: Audit as Infrastructure. Breaks the black-box programming process and cross-session amnesia, evolving programming paradigms into an auditable, traceable, and convergent new era. npx-ready.\n\n[codein2027](https://github.com/xiaomingming92/codein2027) 快速构建 ADD 编程范式的完整脚手架——AI 代码治理的落地方案。以「审计即基础设施」为核心,彻底打破编程过程黑盒与跨轮失忆,让编程范式进化为可审计、可追溯、可收敛的新时代。npx 即用,人人可体验。",
|
|
6
6
|
"repository": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_version": "0.3.
|
|
2
|
+
"_version": "0.3.25-0",
|
|
3
3
|
"adapters/claude/hooks/doc-format-guard.sh": "ec59ad3f",
|
|
4
4
|
"adapters/claude/hooks/lib/common.sh": "4f3e64ad",
|
|
5
5
|
"adapters/claude/hooks/lib/context-inject.sh": "cfc8c65e",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"adapters/claude/hooks/subagent-stop.sh": "0f89e28c",
|
|
27
27
|
"adapters/claude/mcp.json": "4ddc5120",
|
|
28
28
|
"adapters/claude/settings.json": "067ccc45",
|
|
29
|
+
"adapters/codex/config.toml.example": "de1634aa",
|
|
29
30
|
"adapters/codex/hooks/doc-format-guard.sh": "8e134e78",
|
|
30
31
|
"adapters/codex/hooks/lib/common.sh": "4f3e64ad",
|
|
31
32
|
"adapters/codex/hooks/lib/context-inject.sh": "cfc8c65e",
|
|
@@ -180,7 +181,7 @@
|
|
|
180
181
|
"core/rules/profiles/webapp-profile.md": "e493ae7b",
|
|
181
182
|
"core/rules/project_rules.md": "82b25e08",
|
|
182
183
|
"core/rules/theory-practice-map.toml": "b1db99fd",
|
|
183
|
-
"core/scripts/db-ensure.sh": "
|
|
184
|
+
"core/scripts/db-ensure.sh": "94a5ff92",
|
|
184
185
|
"core/scripts/gen-plan-index.sh": "bfc6ad3e",
|
|
185
186
|
"core/scripts/mcp-server/elicitation/confirm.ts": "924857b8",
|
|
186
187
|
"core/scripts/mcp-server/elicitation/index.ts": "44c5dc3f",
|
|
@@ -202,7 +203,8 @@
|
|
|
202
203
|
"core/scripts/mcp-server/shared/package.json": "e3d5a4a2",
|
|
203
204
|
"core/scripts/mcp-server/shared/prisma.ts": "a9618f99",
|
|
204
205
|
"core/scripts/mcp-server/shared/project-root-strategy.ts": "1a5bdc57",
|
|
205
|
-
"core/scripts/mcp-server/shared/
|
|
206
|
+
"core/scripts/mcp-server/shared/redact.ts": "22e008aa",
|
|
207
|
+
"core/scripts/mcp-server/shared/response.ts": "0d1539a8",
|
|
206
208
|
"core/scripts/mcp-server/shared/run-command.ts": "42caf972",
|
|
207
209
|
"core/scripts/mcp-server/tasks/index.ts": "c040eb75",
|
|
208
210
|
"core/scripts/mcp-server/tasks/runner.ts": "d84bd35d",
|
|
@@ -214,20 +216,20 @@
|
|
|
214
216
|
"core/scripts/mcp-server/tools/gateway/check_add_route_completeness.ts": "b58e76ee",
|
|
215
217
|
"core/scripts/mcp-server/tools/gateway/check_add_route_status.ts": "23fbbb6b",
|
|
216
218
|
"core/scripts/mcp-server/tools/gateway/check_dps.ts": "4d599b38",
|
|
217
|
-
"core/scripts/mcp-server/tools/gateway/check_rahs.ts": "
|
|
218
|
-
"core/scripts/mcp-server/tools/gateway/check_spec_sync.ts": "
|
|
219
|
+
"core/scripts/mcp-server/tools/gateway/check_rahs.ts": "3ce99c7c",
|
|
220
|
+
"core/scripts/mcp-server/tools/gateway/check_spec_sync.ts": "8d6efd83",
|
|
219
221
|
"core/scripts/mcp-server/tools/gateway/helpers.ts": "f8794a92",
|
|
220
222
|
"core/scripts/mcp-server/tools/gateway/index.ts": "559acb75",
|
|
221
223
|
"core/scripts/mcp-server/tools/gateway.backup": "65eae393",
|
|
222
224
|
"core/scripts/mcp-server/tools/hitl.ts": "f447345f",
|
|
223
225
|
"core/scripts/mcp-server/tools/hook-event-report.ts": "3f21836e",
|
|
224
|
-
"core/scripts/mcp-server/tools/index.ts": "
|
|
226
|
+
"core/scripts/mcp-server/tools/index.ts": "3e6b6135",
|
|
225
227
|
"core/scripts/mcp-server/tools/plan.ts": "477fd80d",
|
|
226
228
|
"core/scripts/mcp-server/tools/quality.ts": "c4634453",
|
|
227
229
|
"core/scripts/mcp-server/tools/registrar.ts": "efd81b3a",
|
|
228
230
|
"core/scripts/mcp-server/tools/review.ts": "370f84f2",
|
|
229
231
|
"core/scripts/mcp-server/types.ts": "6c5e60bc",
|
|
230
|
-
"core/scripts/mcp-server.ts": "
|
|
232
|
+
"core/scripts/mcp-server.ts": "48b1fd69",
|
|
231
233
|
"core/skills/add-paradigm/SKILL.md": "5d4fa389",
|
|
232
234
|
"core/skills/session-init/SKILL.md": "bb1205b0",
|
|
233
235
|
"core/specs/add-coder-npm-package/checklist.md": "ed92032c",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Codex MCP 配置模板(真源)
|
|
2
|
+
# 渲染后分发到 .codex/ 与各 magic dirs templates/,供 --print-mcp-config 输出
|
|
3
|
+
# 使用方式:
|
|
4
|
+
# npx add-coder init --adapter=codex --print-mcp-config # stdout 输出片段
|
|
5
|
+
# npx add-coder init --adapter=codex --write-user-config # 显式确认后写入 ~/.codex/config.toml
|
|
6
|
+
#
|
|
7
|
+
# Windows 分支(renderer 按平台注入,无需手动修改):
|
|
8
|
+
# win32: command = "cmd /c" args = ["npx.cmd", "tsx", "..."] # .cmd 分支
|
|
9
|
+
# posix: command = "npx" args = ["tsx", "..."]
|
|
10
|
+
#
|
|
11
|
+
# 安全约定:
|
|
12
|
+
# - env 只引用环境变量({{projectRoot}} 由渲染时注入,密码/连接串禁止写入模板)
|
|
13
|
+
# - DATABASE_URL 请通过消费方 .env 或 IDE 环境传入,模板不包含任何连接串
|
|
14
|
+
|
|
15
|
+
[mcp_servers.add_coder]
|
|
16
|
+
command = "{{codexCommand}}"
|
|
17
|
+
args = ["tsx", "{{magicDir}}/scripts/mcp-server.ts"]
|
|
18
|
+
env = {
|
|
19
|
+
PROJECT_ROOT = "{{projectRoot}}",
|
|
20
|
+
}
|
|
@@ -156,6 +156,14 @@ apply_atlas_diff() {
|
|
|
156
156
|
# ⑦ Atlas 同步主流程(探测 → dev-url → 目标 → baseline → diff → apply)
|
|
157
157
|
atlas_sync() {
|
|
158
158
|
[ "$ENGINE" = "sqlite" ] && return 0
|
|
159
|
+
# 迁移锁(进程层契约 v2 §6):多 IDE 并发 init 时仅一个进程执行迁移
|
|
160
|
+
# 非阻塞拿锁(pg_try_advisory_lock),会话级锁——脚本退出连接断开自动释放
|
|
161
|
+
LOCK_KEY="0xADD001"
|
|
162
|
+
LOCKED="$(podman exec "${PROJECT_NAME:-add-project}-postgres" psql -U "${DATABASE_USER:-admin}" -d "${PROJECT_NAME:-add-project}" -tAc "SELECT pg_try_advisory_lock(${LOCK_KEY});" 2>/dev/null || true)"
|
|
163
|
+
if [ "$LOCKED" != "t" ]; then
|
|
164
|
+
echo "!!! 另一个进程正在迁移,请稍后重试"
|
|
165
|
+
return 1
|
|
166
|
+
fi
|
|
159
167
|
if ! atlas_cmd version > /dev/null 2>&1; then
|
|
160
168
|
echo "!!! Atlas 不可用。add-coder sync --patch 可自动安装 @ariga/atlas;或手动: pnpm add -D @ariga/atlas"
|
|
161
169
|
echo " 降级路径: prisma-diff(免 shadow)→ db-push + 强制备份;文档: README「Atlas 数据库同步能力」"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Author : xiaomingming wujixmm@gmail.com
|
|
3
|
+
* @Date : 2026-08-10 18:41:34
|
|
4
|
+
* @LastEditors : xiaomingming wujixmm@gmail.com
|
|
5
|
+
* @LastEditTime : 2026-08-10 18:41:35
|
|
6
|
+
* @FilePath : /farm-agent/home/xmm/ai/add-coder/templates/core/scripts/mcp-server/shared/redact.ts
|
|
7
|
+
* @Description :
|
|
8
|
+
*/
|
|
9
|
+
// 敏感信息脱敏(进程层并发契约 v2:日志/错误不泄露 DATABASE_URL/密码)
|
|
10
|
+
// 统一入口:response.ts 的 textResponse/errorResponse 内部调用,一处封装全覆盖所有工具
|
|
11
|
+
|
|
12
|
+
const CONNECTION_URL_RE =
|
|
13
|
+
/(postgres(?:ql)?|mysql|redis|mongodb(?:\+srv)?):\/\/[^\s"'`]+/gi
|
|
14
|
+
|
|
15
|
+
export function redactUrl(url: string): string {
|
|
16
|
+
// postgres://user:pass@host:port/db → postgres://user:****@host:port/db
|
|
17
|
+
return url.replace(/(:\/\/[^:/\s]+:)([^@/\s]+)(@)/, "$1****$3")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function redact(text: string): string {
|
|
21
|
+
return text.replace(CONNECTION_URL_RE, (m) => {
|
|
22
|
+
try {
|
|
23
|
+
return redactUrl(m)
|
|
24
|
+
} catch {
|
|
25
|
+
return m
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { ToolResponse } from "../types.js"
|
|
2
|
+
import { redact } from "./redact.js"
|
|
2
3
|
|
|
3
4
|
export function textResponse(text: string): { content: ToolResponse } {
|
|
4
|
-
return { content: [{ type: "text", text }] }
|
|
5
|
+
return { content: [{ type: "text", text: redact(text) }] }
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export function errorResponse(message: string): { content: ToolResponse; isError: boolean } {
|
|
8
|
-
return { content: [{ type: "text", text: message }], isError: true }
|
|
9
|
+
return { content: [{ type: "text", text: redact(message) }], isError: true }
|
|
9
10
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import * as z from "zod/v4";
|
|
10
10
|
import type { ToolRegistrar } from "../registrar.js";
|
|
11
11
|
import { existsSync } from "fs";
|
|
12
|
-
import { join } from "path";
|
|
12
|
+
import { join, basename } from "path";
|
|
13
13
|
import { textResponse, errorResponse } from "../../shared/response.js";
|
|
14
14
|
import {
|
|
15
15
|
readFileSafe,
|
|
@@ -42,7 +42,9 @@ export function registerCheckRahs(server: ToolRegistrar) {
|
|
|
42
42
|
);
|
|
43
43
|
const pm = apf.find(
|
|
44
44
|
(f) =>
|
|
45
|
-
f.toLowerCase().includes(pp.toLowerCase()) &&
|
|
45
|
+
f.toLowerCase().includes(pp.toLowerCase()) &&
|
|
46
|
+
f.includes("-plan-v") &&
|
|
47
|
+
!f.includes(".hitl"),
|
|
46
48
|
);
|
|
47
49
|
if (!pm)
|
|
48
50
|
return errorResponse(`未找到匹配的 Plan 文件(关键词: ${pp})`);
|
|
@@ -80,6 +82,71 @@ export function registerCheckRahs(server: ToolRegistrar) {
|
|
|
80
82
|
})) as Array<{ id: string }>;
|
|
81
83
|
auditScore = Math.min(100, logs.length * 10);
|
|
82
84
|
} catch {}
|
|
85
|
+
// ── 范围保真度 / Spec 合规 / 阶段对称性:从静态基线改为动态计算(修复:
|
|
86
|
+
// 三个维度原为固定 80,任何 Plan 的 RAHS 上限 88 永远无法通过)──
|
|
87
|
+
try {
|
|
88
|
+
const arFile = apf.find(
|
|
89
|
+
(f) =>
|
|
90
|
+
f.toLowerCase().includes(pp.toLowerCase()) &&
|
|
91
|
+
f.toLowerCase().includes("add-route"),
|
|
92
|
+
);
|
|
93
|
+
if (arFile) {
|
|
94
|
+
const arContent =
|
|
95
|
+
(await readFileSafe(join(plansDir, arFile))) || "";
|
|
96
|
+
const checks = arContent.match(/^- \[[ x]\]/gm) || [];
|
|
97
|
+
const done = checks.filter((c) => c.includes("[x]")).length;
|
|
98
|
+
// 阶段对称性:add-route Step 产出项勾选率(Handoff/Report Closure 等
|
|
99
|
+
// 条件项未勾选时按比例折算,不阻塞)
|
|
100
|
+
symScore = checks.length > 0
|
|
101
|
+
? Math.max(70, Math.round((done / checks.length) * 100))
|
|
102
|
+
: 70;
|
|
103
|
+
}
|
|
104
|
+
} catch {}
|
|
105
|
+
try {
|
|
106
|
+
// Spec 合规:checklist [T] 项勾选率([R] 运行时项不计入)
|
|
107
|
+
const specDir = join(PROJECT_ROOT, MAGIC_DIR, "specs");
|
|
108
|
+
const sn = basename(pm, ".md").replace(/-plan-v\d+$/i, "");
|
|
109
|
+
const clPath = join(specDir, sn, "checklist.md");
|
|
110
|
+
if (existsSync(clPath)) {
|
|
111
|
+
const cl = (await readFileSafe(clPath)) || "";
|
|
112
|
+
const tItems = cl.match(/^- \[[ x]\] \[T\]/gm) || [];
|
|
113
|
+
const tDone = tItems.filter((c) => c.includes("[x]")).length;
|
|
114
|
+
specScore = tItems.length > 0
|
|
115
|
+
? Math.max(70, Math.round((tDone / tItems.length) * 100))
|
|
116
|
+
: 70;
|
|
117
|
+
}
|
|
118
|
+
} catch {}
|
|
119
|
+
try {
|
|
120
|
+
// 范围保真度:git diff 变更文件与 add-route 附录清单匹配率
|
|
121
|
+
const arFile = apf.find(
|
|
122
|
+
(f) =>
|
|
123
|
+
f.toLowerCase().includes(pp.toLowerCase()) &&
|
|
124
|
+
f.toLowerCase().includes("add-route"),
|
|
125
|
+
);
|
|
126
|
+
if (arFile) {
|
|
127
|
+
const arContent =
|
|
128
|
+
(await readFileSafe(join(plansDir, arFile))) || "";
|
|
129
|
+
const appendix = (
|
|
130
|
+
arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json|yml|yaml)`/g) ||
|
|
131
|
+
[]
|
|
132
|
+
).map((f: string) => f.replace(/`/g, "").toLowerCase());
|
|
133
|
+
const diff = runCommand("git", ["diff", "--name-only"], {
|
|
134
|
+
cwd: PROJECT_ROOT,
|
|
135
|
+
timeout: 5000,
|
|
136
|
+
});
|
|
137
|
+
const diffFiles = diff.stdout
|
|
138
|
+
.trim()
|
|
139
|
+
.split("\n")
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.map((f: string) => f.toLowerCase());
|
|
142
|
+
if (appendix.length > 0 && diffFiles.length > 0) {
|
|
143
|
+
const matched = diffFiles.filter((f) =>
|
|
144
|
+
appendix.some((a) => f === a || f.endsWith(`/${a}`)),
|
|
145
|
+
).length;
|
|
146
|
+
scopeScore = Math.max(70, Math.round((matched / diffFiles.length) * 100));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch {}
|
|
83
150
|
const parts = [
|
|
84
151
|
"=== RAHS:Runtime Architecture Health Score ===",
|
|
85
152
|
`Plan 关键词: "${pp}"`,
|
|
@@ -69,7 +69,7 @@ export function registerCheckSpecSync(server: ToolRegistrar) {
|
|
|
69
69
|
lines.push(`add-route: ${arFile}`);
|
|
70
70
|
const arContent = (await readFileSafe(join(plansDir, arFile))) || "";
|
|
71
71
|
// 提取 add-route 附录文件清单
|
|
72
|
-
const appendixFiles = (arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json)`/g) || [])
|
|
72
|
+
const appendixFiles = (arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json|yml|yaml)`/g) || [])
|
|
73
73
|
.map((f: string) => f.replace(/`/g, ""));
|
|
74
74
|
lines.push(`附录文件: ${appendixFiles.length} 个`);
|
|
75
75
|
|
|
@@ -12,6 +12,61 @@ import { registerReviewTools } from "./review.js"
|
|
|
12
12
|
import { PROJECT_ID } from "../shared/env.js"
|
|
13
13
|
import type { ToolRegistrar } from "./registrar.js"
|
|
14
14
|
|
|
15
|
+
// ── 读写分级信号量节流(进程层并发契约 v2 §7:Codex Parallel MCP 真并行兑底) ──
|
|
16
|
+
// 读工具共享 8 并发;写工具共享 4 并发;超限请求排队(MCP 协议允许延迟响应,排队即天然反压)
|
|
17
|
+
const READ_MAX = 8
|
|
18
|
+
const WRITE_MAX = 4
|
|
19
|
+
|
|
20
|
+
const READ_TOOLS = new Set([
|
|
21
|
+
"get_project_context", "find_related_docs", "get_db_schema", "query_audit_logs",
|
|
22
|
+
"plan_status", "review_status", "status_hitl", "contract_status",
|
|
23
|
+
"check_dps", "check_rahs", "check_add_route_status", "check_spec_sync",
|
|
24
|
+
"check_add_route_completeness", "check_phase_symmetry", "check_failure_path",
|
|
25
|
+
"check_add_compliance", "get_hook_events",
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
function createSemaphore(limit: number) {
|
|
29
|
+
let active = 0
|
|
30
|
+
const queue: (() => void)[] = []
|
|
31
|
+
const acquire = () =>
|
|
32
|
+
new Promise<void>((resolve) => {
|
|
33
|
+
if (active < limit) {
|
|
34
|
+
active++
|
|
35
|
+
resolve()
|
|
36
|
+
} else {
|
|
37
|
+
queue.push(resolve)
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
const release = () => {
|
|
41
|
+
active--
|
|
42
|
+
const next = queue.shift()
|
|
43
|
+
if (next) {
|
|
44
|
+
active++
|
|
45
|
+
next()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { acquire, release }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 429 指数退避重试(3 次,250ms 起步,倍率 2x):上游限流不直接透传
|
|
52
|
+
async function withRetry<T>(fn: () => Promise<T>): Promise<T> {
|
|
53
|
+
let lastErr: unknown
|
|
54
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
55
|
+
try {
|
|
56
|
+
return await fn()
|
|
57
|
+
} catch (e) {
|
|
58
|
+
lastErr = e
|
|
59
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
60
|
+
if (!/429|rate.?limit|限流/i.test(msg)) throw e
|
|
61
|
+
await new Promise((r) => setTimeout(r, 250 * 2 ** (attempt - 1)))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw lastErr
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const readSem = createSemaphore(READ_MAX)
|
|
68
|
+
const writeSem = createSemaphore(WRITE_MAX)
|
|
69
|
+
|
|
15
70
|
export function registerAllTools(server: McpServer) {
|
|
16
71
|
// D9 多 MCP 路由安全:派生装饰版 registrar,为所有工具 description 注入项目身份前缀
|
|
17
72
|
const origRegister = server.registerTool.bind(server)
|
|
@@ -20,10 +75,20 @@ export function registerAllTools(server: McpServer) {
|
|
|
20
75
|
type ToolCb = Parameters<RegisterTool>[2]
|
|
21
76
|
const registerTool = ((name: string, config: ToolConfig, cb: ToolCb) => {
|
|
22
77
|
const c = config as { description?: string }
|
|
78
|
+
// 读写分级节流 + 429 退避(进程层契约 v2:Codex 并行调用兜底)
|
|
79
|
+
const sem = READ_TOOLS.has(name) ? readSem : writeSem
|
|
80
|
+
const throttledCb = (async (...args: unknown[]) => {
|
|
81
|
+
await sem.acquire()
|
|
82
|
+
try {
|
|
83
|
+
return await withRetry(async () => cb(...(args as Parameters<ToolCb>)))
|
|
84
|
+
} finally {
|
|
85
|
+
sem.release()
|
|
86
|
+
}
|
|
87
|
+
}) as ToolCb
|
|
23
88
|
return origRegister(
|
|
24
89
|
name,
|
|
25
90
|
{ ...config, description: `[项目: ${PROJECT_ID}] ${c.description ?? ""}` },
|
|
26
|
-
|
|
91
|
+
throttledCb,
|
|
27
92
|
)
|
|
28
93
|
}) as RegisterTool
|
|
29
94
|
const registrar: ToolRegistrar = { registerTool }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/server"
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"
|
|
3
3
|
import { registerAll } from "./mcp-server/index.js"
|
|
4
|
+
import { redact } from "./mcp-server/shared/redact.js"
|
|
4
5
|
|
|
5
6
|
async function main() {
|
|
6
7
|
const server = new McpServer(
|
|
@@ -14,6 +15,6 @@ async function main() {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
main().catch((error) => {
|
|
17
|
-
console.error("[ADD-MCP] Fatal error:", error)
|
|
18
|
+
console.error("[ADD-MCP] Fatal error:", redact(error instanceof Error ? error.message : String(error)))
|
|
18
19
|
process.exit(1)
|
|
19
20
|
})
|