add-coder 0.1.15 → 0.2.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.
Files changed (102) hide show
  1. package/README.en.md +13 -3
  2. package/README.md +9 -8
  3. package/dist/index.js +73 -99
  4. package/package.json +5 -1
  5. package/templates/adapters/claude/hooks/doc-format-guard.sh +164 -9
  6. package/templates/adapters/claude/hooks/notification.sh +29 -0
  7. package/templates/adapters/claude/hooks/permission-denied.sh +42 -0
  8. package/templates/adapters/claude/hooks/post-tool-use.sh +25 -8
  9. package/templates/adapters/claude/hooks/pre-compact.sh +31 -8
  10. package/templates/adapters/claude/hooks/pre-tool-use.sh +63 -13
  11. package/templates/adapters/claude/hooks/prompt-submit.sh +70 -8
  12. package/templates/adapters/claude/hooks/session-end.sh +21 -0
  13. package/templates/adapters/claude/hooks/session-start.sh +26 -13
  14. package/templates/adapters/claude/hooks/stop-check.sh +40 -5
  15. package/templates/adapters/claude/hooks/stop-failure.sh +44 -0
  16. package/templates/adapters/claude/hooks/subagent-guard.sh +29 -8
  17. package/templates/adapters/claude/hooks/subagent-stop.sh +20 -0
  18. package/templates/adapters/codex/hooks/notification.sh +38 -0
  19. package/templates/adapters/codex/hooks/post-tool-use.sh +35 -0
  20. package/templates/adapters/codex/hooks/pre-tool-use.sh +78 -0
  21. package/templates/adapters/codex/hooks/prompt-submit.sh +78 -0
  22. package/templates/adapters/codex/hooks/session-start.sh +36 -0
  23. package/templates/adapters/codex/hooks/stop-check.sh +45 -0
  24. package/templates/adapters/codex/hooks.json +1 -0
  25. package/templates/adapters/codex/settings.json +1 -0
  26. package/templates/adapters/qoder/hooks/doc-format-guard.sh +9 -1
  27. package/templates/adapters/qoder/hooks/lib/vocabulary.sh +1 -1
  28. package/templates/adapters/qoder/hooks/notification.sh +8 -5
  29. package/templates/adapters/qoder/hooks/post-tool-use.sh +21 -11
  30. package/templates/adapters/qoder/hooks/pre-compact.sh +31 -6
  31. package/templates/adapters/qoder/hooks/pre-tool-use.sh +66 -64
  32. package/templates/adapters/qoder/hooks/prompt-submit.sh +12 -2
  33. package/templates/adapters/qoder/hooks/session-end.sh +24 -0
  34. package/templates/adapters/qoder/hooks/session-start.sh +22 -7
  35. package/templates/adapters/qoder/hooks/stop-check.sh +18 -41
  36. package/templates/adapters/qoder/hooks/subagent-guard.sh +22 -6
  37. package/templates/adapters/qoder/hooks/subagent-stop.sh +26 -0
  38. package/templates/adapters/qoder/settings.json +13 -13
  39. package/templates/adapters/trae/hooks/notification.sh +38 -0
  40. package/templates/adapters/trae/hooks/post-tool-failure.sh +10 -0
  41. package/templates/adapters/trae/hooks/post-tool-use.sh +35 -0
  42. package/templates/adapters/trae/hooks/pre-compact.sh +37 -0
  43. package/templates/adapters/trae/hooks/pre-tool-use.sh +78 -0
  44. package/templates/adapters/trae/hooks/prompt-submit.sh +78 -0
  45. package/templates/adapters/trae/hooks/session-end.sh +21 -0
  46. package/templates/adapters/trae/hooks/session-start.sh +36 -0
  47. package/templates/adapters/trae/hooks/stop-check.sh +45 -0
  48. package/templates/adapters/trae/hooks/subagent-guard.sh +36 -0
  49. package/templates/adapters/trae/hooks/subagent-stop.sh +20 -0
  50. package/templates/adapters/trae/hooks.json +1 -0
  51. package/templates/adapters/trae/settings.json +1 -0
  52. package/templates/adapters/vscode/.github/hooks/error-occurred.json +1 -0
  53. package/templates/adapters/vscode/.github/hooks/post-tool-use.json +1 -0
  54. package/templates/adapters/vscode/.github/hooks/pre-compact.json +1 -0
  55. package/templates/adapters/vscode/.github/hooks/pre-tool-use.json +14 -0
  56. package/templates/adapters/vscode/.github/hooks/session-end.json +1 -0
  57. package/templates/adapters/vscode/.github/hooks/session-start.json +14 -0
  58. package/templates/adapters/vscode/.github/hooks/stop-check.json +1 -0
  59. package/templates/adapters/vscode/.github/hooks/subagent-start.json +1 -0
  60. package/templates/adapters/vscode/.github/hooks/subagent-stop.json +1 -0
  61. package/templates/adapters/vscode/.github/hooks/user-prompt-submit.json +14 -0
  62. package/templates/adapters/vscode/hooks/notification.sh +37 -0
  63. package/templates/adapters/vscode/hooks/post-tool-failure.sh +28 -0
  64. package/templates/adapters/vscode/hooks/post-tool-use.sh +35 -0
  65. package/templates/adapters/vscode/hooks/pre-compact.sh +37 -0
  66. package/templates/adapters/vscode/hooks/pre-tool-use.sh +78 -0
  67. package/templates/adapters/vscode/hooks/prompt-submit.sh +78 -0
  68. package/templates/adapters/vscode/hooks/session-end.sh +13 -0
  69. package/templates/adapters/vscode/hooks/session-start.sh +35 -0
  70. package/templates/adapters/vscode/hooks/stop-check.sh +53 -0
  71. package/templates/adapters/vscode/hooks/subagent-guard.sh +37 -0
  72. package/templates/adapters/vscode/hooks/subagent-stop.sh +12 -0
  73. package/templates/adapters/vscode/launch.json +1 -1
  74. package/templates/adapters/vscode/settings.json +2 -3
  75. package/templates/adapters/vscode/tasks.json +3 -3
  76. package/templates/core/docs/ADD-governance-claude-code.md +121 -0
  77. package/templates/core/docs/ADD-governance-codex.md +75 -0
  78. package/templates/core/docs/ADD-governance-qoder-cn.md +99 -0
  79. package/templates/core/docs/ADD-governance-trae.md +63 -0
  80. package/templates/core/docs/ADD-governance-vscode-copilot.md +132 -0
  81. package/templates/core/hooks/doc-format-guard.sh +164 -9
  82. package/templates/core/hooks/lib/common.sh +261 -0
  83. package/templates/core/hooks/lib/preload-templates.sh +187 -0
  84. package/templates/core/hooks/lib/session-end.sh +76 -0
  85. package/templates/core/hooks/lib/subagent-stop.sh +90 -0
  86. package/templates/core/hooks/notification.sh +29 -0
  87. package/templates/core/hooks/post-tool-use.sh +25 -8
  88. package/templates/core/hooks/pre-compact.sh +31 -8
  89. package/templates/core/hooks/pre-tool-use.sh +63 -13
  90. package/templates/core/hooks/prompt-submit.sh +70 -8
  91. package/templates/core/hooks/session-end.sh +38 -0
  92. package/templates/core/hooks/session-start.sh +26 -13
  93. package/templates/core/hooks/stop-check.sh +40 -5
  94. package/templates/core/hooks/subagent-stop.sh +47 -0
  95. package/templates/core/rules/project_rules.md +1 -1
  96. package/templates/core/scripts/db-ensure.sh +6 -5
  97. package/templates/core/scripts/mcp-server.ts +38 -120
  98. package/templates/core/skills/add-paradigm/SKILL.md +24 -0
  99. package/templates/core/templates/standard-plan-template.md +39 -13
  100. package/templates/core/hooks/lib/context-inject.sh +0 -96
  101. package/templates/core/hooks/lib/state-detect.sh +0 -104
  102. package/templates/core/hooks/lib/vocabulary.sh +0 -49
package/README.en.md CHANGED
@@ -67,7 +67,15 @@ Hooks are not "notification push" — they are the **IDE runtime interception la
67
67
  | PreCompact | Forces retention of critical document paths during cross-session context compression |
68
68
  | PromptSubmit | Injects ADD vocabulary triggers, ensuring zero-latency LLM response to commands like "acceptance" and "gateway" |
69
69
 
70
- Each IDE (Claude/Qoder/VS Code) has its own hook implementation, but the **governance logic is unified** — the architecture is consistent, only the adapter layer differs.
70
+ Each IDE(Claude Code / Qoder CN / VS Code Copilot / Trae / Codex)has its own hook implementation, but the **governance logic is unified** — the architecture is consistent, only the adapter layer differs.
71
+
72
+ | IDE | Governance Doc | Events Covered | Hook Config |
73
+ |---|---|---|---|
74
+ | Claude Code | [ADD-governance-claude-code.md](./ADD-governance-claude-code.md) | 14/17 | `.claude/hooks/*.sh` |
75
+ | Qoder CN | [ADD-governance-qoder-cn.md](./ADD-governance-qoder-cn.md) | 10/17 | `.qoder/hooks/*.sh` |
76
+ | VS Code Copilot | [ADD-governance-vscode-copilot.md](./ADD-governance-vscode-copilot.md) | 10/17 | `.github/hooks/*.json` → `.vscode/hooks/*.sh` |
77
+ | Trae | [ADD-governance-trae.md](./ADD-governance-trae.md) | 6/17 | `hooks.json` → `.trae/hooks/*.sh` |
78
+ | Codex | [ADD-governance-codex.md](./ADD-governance-codex.md) | 0 native / 14 (via Claude import) | `.codex/hooks.json` |
71
79
 
72
80
  ---
73
81
 
@@ -116,7 +124,7 @@ npx add-coder init
116
124
 
117
125
  | Option | Description |
118
126
  |--------|-------------|
119
- | `--adapter <type>` | Target IDE: claude / qoder / vscode / auto (default) |
127
+ | `--adapter <type>` | Target IDE: claude / qoder / vscode / trae / codex / auto (default) |
120
128
  | `--config <path>` | Specify config file |
121
129
  | `--yes` | Skip interactions, create new files only |
122
130
  | `--force` | Overwrite existing files |
@@ -127,9 +135,11 @@ npx add-coder init
127
135
  | Directory | Content |
128
136
  |-----------|---------|
129
137
  | `.add/` | ADD shared core (skills, agents, docs, scripts, rules, etc.) |
130
- | `.claude/` | Claude Code adapter (hooks, settings.json) |
138
+ | `.claude/` | Claude Code adapter (hooks, settings.json, mcp.json) |
131
139
  | `.qoder/` | Qoder adapter (hooks, settings.json, mcp.json) |
132
140
  | `.vscode/` | VS Code adapter (settings.json, tasks.json) |
141
+ | `.trae/` | Trae adapter (hooks.json, settings.json) |
142
+ | `.codex/` | Codex adapter (hooks.json, settings.json) |
133
143
 
134
144
  ## MCP Audit Toolchain
135
145
 
package/README.md CHANGED
@@ -58,16 +58,17 @@ AI 对话的致命缺陷:上次讨论的架构决策、已修复的 Bug、达
58
58
 
59
59
  ### ⑤ 多 IDE 的 Hook 即治理层
60
60
 
61
- hook 不是「通知推送」,而是 **IDE 运行时拦截层**:
61
+ hook 不是「通知推送」,而是 **ADD 范式在 IDE agent 生命周期中的 17 个确定性治理卡位**。每个 IDE(Claude Code / Qoder CN / VS Code Copilot / Trae / Codex)有各自的 hook 机制,但治理逻辑统一——架构一致,适配层不同。
62
62
 
63
- | Hook 类型 | 作用 |
64
- |----------|------|
65
- | PreToolUse | 工具调用前校验白名单、注入上下文、DPS 条件检查 |
66
- | PostToolUse | 自动审计记录、Plan 同步检测、格式守卫 |
67
- | PreCompact | 跨轮上下文压缩时强制保留关键文档路径 |
68
- | PromptSubmit | 注入 ADD 词汇表触发词,确保 LLM 对「验收」「门禁」等命令零延迟响应 |
63
+ | IDE | 治理文档 | 覆盖事件 | Hook 配置 |
64
+ |---|---|---|---|
65
+ | Claude Code | [ADD-governance-claude-code.md](./ADD-governance-claude-code.md) | 14/17 | `.claude/hooks/*.sh` |
66
+ | Qoder CN | [ADD-governance-qoder-cn.md](./ADD-governance-qoder-cn.md) | 10/17 | `.qoder/hooks/*.sh` |
67
+ | VS Code Copilot | [ADD-governance-vscode-copilot.md](./ADD-governance-vscode-copilot.md) | 10/17 | `.github/hooks/*.json` → `.vscode/hooks/*.sh` |
68
+ | Trae | [ADD-governance-trae.md](./ADD-governance-trae.md) | 6/17 | `hooks.json` → `.trae/hooks/*.sh` |
69
+ | Codex | [ADD-governance-codex.md](./ADD-governance-codex.md) | 0 (原生) / 14 (导入 Claude) | `.codex/hooks.json` |
69
70
 
70
- 每个 IDE(Claude/Qoder/VS Code)有各自的 hook 实现,但**治理逻辑统一** 架构一致,适配层不同。
71
+ > 实施 Plan: [add-coder-hook-full-alignment-plan-v1](./.qoder/plans/2026-07/17/add-coder-hook-full-alignment-plan-v1.md) | 触发源: [GitHub Issue #6](https://github.com/xiaomingming92/add-coder/issues/6)
71
72
 
72
73
  ---
73
74
 
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 readFileSync8 } from "fs";
4
+ import { readFileSync as readFileSync5 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/caijuehub/strategies/detect.strategy.ts
@@ -51,7 +51,7 @@ import { join as join2 } from "path";
51
51
 
52
52
  // src/config/schema.ts
53
53
  import { z } from "zod";
54
- var AdapterEnum = z.enum(["claude", "qoder", "vscode"]);
54
+ var AdapterEnum = z.enum(["claude", "qoder", "vscode", "trae", "codex"]);
55
55
  var AddCoderConfigSchema = z.object({
56
56
  projectName: z.string().min(1, "\u9879\u76EE\u540D\u4E0D\u80FD\u4E3A\u7A7A"),
57
57
  projectRoot: z.string().default(""),
@@ -230,6 +230,9 @@ function renderCore(config, dryRun) {
230
230
  const rendered = render(content, config);
231
231
  const targetRel = join4(CORE_TARGET, relative(CORE_DIR, full));
232
232
  files.set(targetRel, rendered);
233
+ if (relative(CORE_DIR, full).startsWith("docs/")) {
234
+ files.set(name, rendered);
235
+ }
233
236
  }
234
237
  }
235
238
  }
@@ -239,107 +242,70 @@ function renderCore(config, dryRun) {
239
242
  }
240
243
  return files;
241
244
  }
242
-
243
- // src/adapters/claude/renderer.ts
244
- import { readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
245
- import { join as join5, relative as relative2, dirname as dirname3 } from "path";
246
- import { fileURLToPath as fileURLToPath2 } from "url";
247
- var __filename2 = fileURLToPath2(import.meta.url);
248
- var __dirname2 = dirname3(__filename2);
249
- var TEMPLATES_ROOT2 = join5(__dirname2, "../templates");
250
- var ADAPTER_DIR = join5(TEMPLATES_ROOT2, "adapters", "claude");
251
- var TARGET_MAGIC_PATH = ".claude";
252
- function renderAdapter(config, targetDir, dryRun) {
245
+ function renderAdapterBase(config, magicPath, githubHooksToRoot, dryRun) {
253
246
  const files = /* @__PURE__ */ new Map();
254
- function walk(dir, base) {
255
- for (const name of readdirSync2(dir)) {
256
- const full = join5(dir, name);
257
- if (statSync2(full).isDirectory()) {
258
- walk(full, join5(base, name));
247
+ const adapterDir = join4(TEMPLATES_ROOT, "adapters", magicPath.replace(".", ""));
248
+ const coreHooksLib = join4(TEMPLATES_ROOT, "core", "hooks", "lib");
249
+ function walk(dir, base, targetPrefix) {
250
+ for (const name of readdirSync(dir)) {
251
+ const full = join4(dir, name);
252
+ if (statSync(full).isDirectory()) {
253
+ walk(full, join4(base, name), targetPrefix);
259
254
  } else {
260
- const content = readFileSync3(full, "utf-8");
255
+ const content = readFileSync2(full, "utf-8");
261
256
  const rendered = render(content, config);
262
- const targetRel = join5(TARGET_MAGIC_PATH, relative2(ADAPTER_DIR, full));
257
+ let targetRel;
258
+ if (targetPrefix) {
259
+ targetRel = join4(targetPrefix, relative(dir, full));
260
+ } else if (githubHooksToRoot && relative(adapterDir, full).startsWith(".github/")) {
261
+ targetRel = relative(adapterDir, full);
262
+ } else {
263
+ targetRel = join4(magicPath, relative(adapterDir, full));
264
+ }
263
265
  files.set(targetRel, rendered);
264
266
  }
265
267
  }
266
268
  }
267
- walk(ADAPTER_DIR, "");
269
+ walk(adapterDir, "");
270
+ walk(coreHooksLib, "", join4(magicPath, "hooks", "lib"));
268
271
  if (dryRun) {
269
- console.log(`[dry-run] Claude adapter: ${files.size} files`);
272
+ console.log(`[dry-run] ${magicPath} adapter: ${files.size} files`);
270
273
  }
271
274
  return files;
272
275
  }
273
276
 
277
+ // src/adapters/claude/renderer.ts
278
+ function renderAdapter(config, targetDir, dryRun, magicDir) {
279
+ return renderAdapterBase(config, magicDir, magicDir === ".vscode", dryRun);
280
+ }
281
+
274
282
  // src/adapters/qoder/renderer.ts
275
- import { readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
276
- import { join as join6, relative as relative3, dirname as dirname4 } from "path";
277
- import { fileURLToPath as fileURLToPath3 } from "url";
278
- var __filename3 = fileURLToPath3(import.meta.url);
279
- var __dirname3 = dirname4(__filename3);
280
- var TEMPLATES_ROOT3 = join6(__dirname3, "../templates");
281
- var ADAPTER_DIR2 = join6(TEMPLATES_ROOT3, "adapters", "qoder");
282
- var TARGET_MAGIC_PATH2 = ".qoder";
283
- function renderAdapter2(config, targetDir, dryRun) {
284
- const files = /* @__PURE__ */ new Map();
285
- function walk(dir, base) {
286
- for (const name of readdirSync3(dir)) {
287
- const full = join6(dir, name);
288
- if (statSync3(full).isDirectory()) {
289
- walk(full, join6(base, name));
290
- } else {
291
- const content = readFileSync4(full, "utf-8");
292
- const rendered = render(content, config);
293
- const targetRel = join6(TARGET_MAGIC_PATH2, relative3(ADAPTER_DIR2, full));
294
- files.set(targetRel, rendered);
295
- }
296
- }
297
- }
298
- walk(ADAPTER_DIR2, "");
299
- if (dryRun) {
300
- console.log(`[dry-run] Qoder adapter: ${files.size} files`);
301
- }
302
- return files;
283
+ function renderAdapter2(config, targetDir, dryRun, magicDir) {
284
+ return renderAdapterBase(config, magicDir, magicDir === ".vscode", dryRun);
303
285
  }
304
286
 
305
287
  // src/adapters/vscode/renderer.ts
306
- import { readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
307
- import { join as join7, relative as relative4, dirname as dirname5 } from "path";
308
- import { fileURLToPath as fileURLToPath4 } from "url";
309
- var __filename4 = fileURLToPath4(import.meta.url);
310
- var __dirname4 = dirname5(__filename4);
311
- var TEMPLATES_ROOT4 = join7(__dirname4, "../templates");
312
- var ADAPTER_DIR3 = join7(TEMPLATES_ROOT4, "adapters", "vscode");
313
- var TARGET_MAGIC_PATH3 = ".vscode";
314
- function renderAdapter3(config, targetDir, dryRun) {
315
- const files = /* @__PURE__ */ new Map();
316
- function walk(dir, base) {
317
- for (const name of readdirSync4(dir)) {
318
- const full = join7(dir, name);
319
- if (statSync4(full).isDirectory()) {
320
- walk(full, join7(base, name));
321
- } else {
322
- const content = readFileSync5(full, "utf-8");
323
- const rendered = render(content, config);
324
- const targetRel = join7(TARGET_MAGIC_PATH3, relative4(ADAPTER_DIR3, full));
325
- files.set(targetRel, rendered);
326
- }
327
- }
328
- }
329
- walk(ADAPTER_DIR3, "");
330
- if (dryRun) {
331
- console.log(`[dry-run] VS Code adapter: ${files.size} files`);
332
- }
333
- return files;
288
+ function renderAdapter3(config, targetDir, dryRun, magicDir) {
289
+ return renderAdapterBase(config, magicDir, magicDir === ".vscode", dryRun);
290
+ }
291
+
292
+ // src/adapters/trae/renderer.ts
293
+ function renderAdapter4(config, targetDir, dryRun, magicDir) {
294
+ return renderAdapterBase(config, magicDir, magicDir === ".vscode", dryRun);
295
+ }
296
+
297
+ // src/adapters/codex/renderer.ts
298
+ function renderAdapter5(config, targetDir, dryRun, magicDir) {
299
+ return renderAdapterBase(config, magicDir, magicDir === ".vscode", dryRun);
334
300
  }
335
301
 
336
302
  // src/cli/prisma-injector.ts
337
- import { resolve as resolve3, dirname as dirname6 } from "path";
338
- import { fileURLToPath as fileURLToPath5 } from "url";
303
+ import { resolve as resolve3, dirname as dirname3 } from "path";
304
+ import { fileURLToPath as fileURLToPath2 } from "url";
339
305
 
340
306
  // src/caijuehub/strategies/prisma.strategy.ts
341
307
  import { spawnSync } from "child_process";
342
- import { copyFileSync, existsSync as existsSync5, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
308
+ import { copyFileSync, existsSync as existsSync5, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
343
309
  import { resolve as resolve2 } from "path";
344
310
  var PRISMA_CONFIG = {
345
311
  onMissing: "ask",
@@ -419,10 +385,10 @@ function postInitSetup(projectRoot, schemaPath, addPrismaTemplate, destPath) {
419
385
  const envPath = resolve2(projectRoot, ".env");
420
386
  const devEnvPath = resolve2(projectRoot, ".env.development");
421
387
  if (existsSync5(envPath)) {
422
- const envContent = readFileSync6(envPath, "utf-8");
388
+ const envContent = readFileSync3(envPath, "utf-8");
423
389
  const dbUrl = envContent.match(/DATABASE_URL=.*/);
424
390
  if (dbUrl) {
425
- const existing = existsSync5(devEnvPath) ? readFileSync6(devEnvPath, "utf-8") : "";
391
+ const existing = existsSync5(devEnvPath) ? readFileSync3(devEnvPath, "utf-8") : "";
426
392
  if (!existing.includes("DATABASE_URL=")) {
427
393
  writeFileSync2(devEnvPath, `${existing}${existing ? "\n" : ""}${dbUrl[0]}
428
394
  `, "utf-8");
@@ -476,8 +442,8 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
476
442
  console.log("\u8986\u76D6");
477
443
  } else if (choice === "d") {
478
444
  copyFileSync(destPath, destPath + ".bak");
479
- console.log("=== \u5F53\u524D\uFF08\u5DF2\u5907\u4EFD\uFF09===\n" + readFileSync6(destPath, "utf-8"));
480
- console.log("=== \u6A21\u677F ===\n" + readFileSync6(addPrismaTemplate, "utf-8"));
445
+ console.log("=== \u5F53\u524D\uFF08\u5DF2\u5907\u4EFD\uFF09===\n" + readFileSync3(destPath, "utf-8"));
446
+ console.log("=== \u6A21\u677F ===\n" + readFileSync3(addPrismaTemplate, "utf-8"));
481
447
  if (await ask("\u786E\u8BA4\u8986\u76D6\uFF1F[y/N] ") !== "y") {
482
448
  console.log("\u5DF2\u8DF3\u8FC7");
483
449
  return true;
@@ -524,24 +490,26 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
524
490
  }
525
491
 
526
492
  // src/cli/prisma-injector.ts
527
- var __filename5 = fileURLToPath5(import.meta.url);
528
- var __dirname5 = dirname6(__filename5);
529
- var ADD_PRISMA_TEMPLATE = resolve3(__dirname5, "../templates/core/prisma/add.prisma");
493
+ var __filename2 = fileURLToPath2(import.meta.url);
494
+ var __dirname2 = dirname3(__filename2);
495
+ var ADD_PRISMA_TEMPLATE = resolve3(__dirname2, "../templates/core/prisma/add.prisma");
530
496
  async function injectPrisma2(projectRoot, options = {}) {
531
497
  return injectPrisma(projectRoot, ADD_PRISMA_TEMPLATE, options);
532
498
  }
533
499
 
534
500
  // src/cli/commands/init.ts
535
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, copyFileSync as copyFileSync2, readdirSync as readdirSync5 } from "fs";
501
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, copyFileSync as copyFileSync2, readdirSync as readdirSync2 } from "fs";
536
502
  import { resolve as resolve4 } from "path";
537
503
  import { spawnSync as spawnSync2 } from "child_process";
538
504
  import { createConnection } from "net";
539
505
  var ADAPTER_RENDERERS = {
540
506
  claude: renderAdapter,
541
507
  qoder: renderAdapter2,
542
- vscode: renderAdapter3
508
+ vscode: renderAdapter3,
509
+ trae: renderAdapter4,
510
+ codex: renderAdapter5
543
511
  };
544
- var MAGIC_DIR_MAP = { claude: ".claude", qoder: ".qoder", vscode: ".vscode" };
512
+ var MAGIC_DIR_MAP = { claude: ".claude", qoder: ".qoder", vscode: ".vscode", trae: ".trae", codex: ".codex" };
545
513
  async function resolveAdapter(projectRoot, specified) {
546
514
  if (specified) {
547
515
  if (!MAGIC_DIR_MAP[specified]) throw new Error(`\u672A\u77E5 adapter: ${specified}`);
@@ -712,7 +680,7 @@ function injectDbExportScript(projectRoot, dryRun) {
712
680
  console.log("[dry-run] \u5C06\u6CE8\u5165 db:export");
713
681
  return;
714
682
  }
715
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
683
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
716
684
  if (!pkg.scripts) pkg.scripts = {};
717
685
  if (!pkg.scripts["db:export"]) {
718
686
  pkg.scripts["db:export"] = "npx tsx scripts/export-db.ts";
@@ -736,7 +704,7 @@ async function initCommand(options) {
736
704
  }
737
705
  if (db.engine === "postgresql" && db.container && db.container !== "manual") {
738
706
  if (!db.reuseExisting) {
739
- const composeName = db.container === "podman" ? "podman-compose.yml" : "docker-compose.yml";
707
+ const composeName = db.container === "podman" ? "podman-compose.add.yml" : "docker-compose.add.yml";
740
708
  const composePath = resolve4(projectRoot, composeName);
741
709
  if (!options.dryRun && (!existsSync6(composePath) || options.force)) {
742
710
  writeFileSync3(composePath, composeContent(config.projectName || "add-project"), "utf-8");
@@ -745,7 +713,7 @@ async function initCommand(options) {
745
713
  }
746
714
  const devEnvPath = resolve4(projectRoot, ".env.development");
747
715
  if (!options.dryRun && existsSync6(devEnvPath)) {
748
- const existing = readFileSync7(devEnvPath, "utf-8");
716
+ const existing = readFileSync4(devEnvPath, "utf-8");
749
717
  if (!/^DATABASE_USER=/m.test(existing)) {
750
718
  writeFileSync3(devEnvPath, existing + `
751
719
  DATABASE_USER=${db.user || "admin"}
@@ -767,14 +735,20 @@ PROJECT_NAME=${config.projectName || "add-project"}
767
735
  if (!allFiles.has(targetPath)) allFiles.set(targetPath, content);
768
736
  }
769
737
  }
770
- for (const adapter of resolveAdapters2(target)) {
738
+ const resolved = resolveAdapters2(target);
739
+ for (const adapter of resolved) {
771
740
  const renderFn = ADAPTER_RENDERERS[adapter];
772
741
  if (renderFn) {
773
- const adapterFiles = renderFn(config, projectRoot, !!options.dryRun);
742
+ const adapterFiles = renderFn(config, projectRoot, !!options.dryRun, magicDir);
774
743
  for (const [p, c] of adapterFiles) allFiles.set(p, c);
775
744
  console.log(`${adapter} adapter: ${adapterFiles.size} \u6587\u4EF6`);
776
745
  }
777
746
  }
747
+ if (resolved.includes("vscode") || resolved.includes("trae") || resolved.includes("codex")) {
748
+ const claudeFiles = renderAdapter(config, projectRoot, !!options.dryRun, ".claude");
749
+ for (const [p, c] of claudeFiles) allFiles.set(p, c);
750
+ console.log(`claude adapter (via Agent Host): ${claudeFiles.size} \u6587\u4EF6`);
751
+ }
778
752
  const result = await writeFiles2(projectRoot, allFiles, { force: options.force, dryRun: options.dryRun });
779
753
  if (db.engine === "postgresql" && db.container && db.container !== "manual") {
780
754
  if (!options.dryRun) {
@@ -822,7 +796,7 @@ PROJECT_NAME=${config.projectName || "add-project"}
822
796
  const destDir = resolve4(docsBase, d);
823
797
  if (!existsSync6(destDir)) mkdirSync3(destDir, { recursive: true });
824
798
  if (!existsSync6(srcDir)) continue;
825
- for (const f of readdirSync5(srcDir)) {
799
+ for (const f of readdirSync2(srcDir)) {
826
800
  const src = resolve4(srcDir, f);
827
801
  const dest = resolve4(destDir, f);
828
802
  if (existsSync6(dest)) continue;
@@ -837,7 +811,7 @@ PROJECT_NAME=${config.projectName || "add-project"}
837
811
  \u5B8C\u6210: \u65B0\u5EFA ${result.created}, \u8DF3\u8FC7 ${result.skipped}, \u8986\u76D6 ${result.overwritten}`);
838
812
  if (!options.dryRun) {
839
813
  if (db.engine === "sqlite") console.log("\u6570\u636E\u5907\u4EFD: npm run db:export \u2192 data/exports/");
840
- const pkg = JSON.parse(readFileSync7(resolve4(import.meta.dirname, "../package.json"), "utf-8"));
814
+ const pkg = JSON.parse(readFileSync4(resolve4(import.meta.dirname, "../package.json"), "utf-8"));
841
815
  const peerNames = Object.keys(pkg.peerDependencies || {});
842
816
  if (peerNames.length > 0) {
843
817
  console.log(`
@@ -902,7 +876,7 @@ async function statusCommand() {
902
876
 
903
877
  // src/cli/index.ts
904
878
  var { version } = JSON.parse(
905
- readFileSync8(new URL("../package.json", import.meta.url), "utf-8")
879
+ readFileSync5(new URL("../package.json", import.meta.url), "utf-8")
906
880
  );
907
881
  var program = new Command();
908
882
  program.name("add-coder").description("\u521D\u59CB\u5316 ADD \u8303\u5F0F\u5DE5\u4F5C\u6D41\u6A21\u677F").version(version);
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "add-coder",
3
- "version": "0.1.15",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "[codein2027](https://github.com/xiaomingming92/codein2027) 快速构建 ADD 编程范式的完整脚手架——AI 代码治理的落地方案。以「审计即基础设施」为核心,彻底打破编程过程黑盒与跨轮失忆,让编程范式进化为可审计、可追溯、可收敛的新时代。npx 即用,人人可体验。",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/xiaomingming92/add-coder.git"
9
+ },
6
10
  "type": "module",
7
11
  "bin": {
8
12
  "add-coder": "bin/add-coder.js"
@@ -1,17 +1,172 @@
1
1
  #!/bin/bash
2
- # doc-format-guard.sh — 文档格式守卫(Claude Code 适配)
2
+ # doc-format-guard.sh — schema.json 驱动的 ADD 文档格式守卫
3
3
  set -euo pipefail
4
4
 
5
- HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
6
- SHARED_LIB="$HOOK_DIR/../../shared/hooks-lib/common.sh"
7
- [ -f "$SHARED_LIB" ] && source "$SHARED_LIB"
5
+ input=$(cat)
8
6
 
9
- input=$(parse_input)
7
+ # DEBUG: dump stdin for investigation
8
+ mkdir -p .qoder/debug-dump
9
+ echo "=== $(date) ===" >> .qoder/debug-dump/stdin.log
10
+ echo "file_path: $(echo "$input" | jq -r '.tool_input.file_path // "EMPTY"')" >> .qoder/debug-dump/stdin.log
11
+ echo "has_file_content: $(echo "$input" | jq 'has("tool_input") and (.tool_input | has("file_content"))')" >> .qoder/debug-dump/stdin.log
12
+ echo "has_replacements: $(echo "$input" | jq 'has("tool_input") and (.tool_input | has("replacements"))')" >> .qoder/debug-dump/stdin.log
13
+ echo "top_keys: $(echo "$input" | jq -r 'keys | join(", ")')" >> .qoder/debug-dump/stdin.log
14
+ echo "tool_input_keys: $(echo "$input" | jq -r '.tool_input | keys | join(", ") // "NO_TOOL_INPUT"')" >> .qoder/debug-dump/stdin.log
15
+ echo "=== DONE ===" >> .qoder/debug-dump/stdin.log
10
16
  file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
17
+ # L17: 非文件工具事件(空 stdin)→ 不拦截(由 matcher 层过滤)
11
18
  [ -z "$file_path" ] && exit 0
12
19
 
13
- # 检查 ADD 文档格式
14
- if echo "$file_path" | grep -qE '\.qoder/(specs|plans|reviews)/.*\.md$'; then
15
- echo "[ADD DocFormatGuard] 检测到 ADD 文档变更,请确保格式符合模板规范。"
20
+ if ! echo "$file_path" | grep -qE '\.qoder/(plans|specs)/'; then
21
+ exit 0
16
22
  fi
17
- exit 0
23
+
24
+ CONTENT=$(echo "$input" | jq -r '
25
+ if .tool_input.file_content then .tool_input.file_content
26
+ elif .tool_input.content then .tool_input.content
27
+ elif .tool_input.replacements then .tool_input.replacements[0].new_text
28
+ else "" end')
29
+ # L24: PostToolUse 不可阻断(仅反馈),PreToolUse exit 2 + ask 可拦截
30
+ # L24: 文件在 .qoder/(plans|specs)/ 下但 Write 工具未传 content → 无法校验,阻断
31
+ if [ -z "$CONTENT" ]; then
32
+ echo "⛔ 拒绝:Write 工具未传 file_content,无法校验手写文档格式" >&2
33
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"Write 工具未传 file_content,无法校验手写文档。请用 SearchReplace 改写已有文件,或用 Write 工具重试。"}}'
34
+ exit 2
35
+ fi
36
+
37
+ PROJECT_DIR="${QODER_PROJECT_DIR:-${QODERCN_PROJECT_DIR:-$PWD}}"
38
+ TEMPLATES_DIR="$PROJECT_DIR/.qoder/templates"
39
+
40
+ # 通过 filename 匹配模板名
41
+ TEMPLATE_NAME=""
42
+ for tmpl in "$TEMPLATES_DIR"/*.md; do
43
+ base=$(basename "$tmpl")
44
+ if echo "$file_path" | grep -q "$base"; then
45
+ TEMPLATE_NAME="$base"
46
+ break
47
+ fi
48
+ done
49
+
50
+ # 退一步:根据文件内容特征猜测模板类型
51
+ if [ -z "$TEMPLATE_NAME" ]; then
52
+ if echo "$CONTENT" | grep -q "## 四、Handoff"; then
53
+ TEMPLATE_NAME="simple-standard-plan-template.md"
54
+ elif echo "$CONTENT" | grep -q "## PLAN 元信息"; then
55
+ TEMPLATE_NAME="standard-plan-template.md"
56
+ elif echo "$CONTENT" | grep -q "## Review 元信息"; then
57
+ if echo "$CONTENT" | grep -q "运行时验证"; then
58
+ TEMPLATE_NAME="review-runtime-template.md"
59
+ elif echo "$CONTENT" | grep -q "跨仓库格式契约"; then
60
+ TEMPLATE_NAME="review-implementation-template.md"
61
+ else
62
+ TEMPLATE_NAME="review-template.md"
63
+ fi
64
+ elif echo "$CONTENT" | grep -q "## Why"; then
65
+ TEMPLATE_NAME="spec-template.md"
66
+ elif echo "$CONTENT" | grep -q "## Preconditions"; then
67
+ TEMPLATE_NAME="tasks-template.md"
68
+ elif echo "$CONTENT" | grep -q "审计链(证据→devlog→checklist)"; then
69
+ TEMPLATE_NAME="checklist-template.md"
70
+ else
71
+ case "$file_path" in
72
+ *handoff*)
73
+ # ★ 按内容特征区分单/多轮 handoff,不依赖文件名
74
+ if echo "$CONTENT" | grep -qF "## 全局元信息"; then
75
+ TEMPLATE_NAME="handoff-multi-round-template.md"
76
+ elif echo "$CONTENT" | grep -qF "## 1. 交接前状态"; then
77
+ TEMPLATE_NAME="handoff-single-round-template.md"
78
+ else
79
+ echo "⛔ handoff 文件内容无法识别模板类型(缺 '## 全局元信息' 或 '## 1. 交接前状态'),拒绝写入" >&2
80
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"handoff 内容不符合 single/multi 模板规范"}}'
81
+ exit 2
82
+ fi
83
+ ;;
84
+ *plan*) TEMPLATE_NAME="standard-plan-template.md" ;;
85
+ *add-route*heavy*) TEMPLATE_NAME="add-route-template-heavyweight.md" ;;
86
+ *add-route*) TEMPLATE_NAME="add-route-template.md" ;;
87
+ *tasks*) TEMPLATE_NAME="tasks-template.md" ;;
88
+ *spec*) TEMPLATE_NAME="spec-template.md" ;;
89
+ *checklist*) TEMPLATE_NAME="checklist-template.md" ;;
90
+ *report*runtime*) TEMPLATE_NAME="runtime-report-template.md" ;;
91
+ *report*) TEMPLATE_NAME="report-template.md" ;;
92
+ *fix-verif*) TEMPLATE_NAME="fix-verification-template.md" ;;
93
+ *)
94
+ # 增量修订识别:包含 ~~删除线~~ / → 新增标记 / [修订日期] 任意一个 → 视为增量更新,放行
95
+ if echo "$CONTENT" | grep -qE '~~.+~~|→|\[[0-9]{4}-[0-9]{2}-[0-9]{2}\s+修订'; then
96
+ echo "[doc-format-guard] 检测到增量修订格式,跳过完整章节校验" >&2
97
+ exit 0
98
+ fi
99
+ echo "⛔ 拒绝:无法识别文档类型 (file_path: $file_path),缺少模板匹配规则" >&2
100
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"无法识别 ADD 文档类型,请联系管理员更新 doc-format-guard.sh"}}'
101
+ exit 2
102
+ ;;
103
+ esac
104
+ fi
105
+ fi
106
+
107
+ SCHEMA_FILE="$TEMPLATES_DIR/$(echo "${TEMPLATE_NAME%.md}" | sed 's/-template$//').schema.json"
108
+ # L84: schema 文件不存在 → 无校验规则,阻断(不允许无规则放行)
109
+ if [ ! -f "$SCHEMA_FILE" ]; then
110
+ echo "⛔ 阻断:模板 ${TEMPLATE_NAME} 缺少对应的 .schema.json 校验规则" >&2
111
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"缺少 .schema.json 校验规则文件,禁止无规则放行"}}'
112
+ exit 2
113
+ fi
114
+
115
+ # SearchReplace 只传 patch(replacements[].new_text),不传全文件 → 跳过章节校验
116
+ IS_SEARCH_REPLACE=$(echo "$input" | jq -r '.tool_input.replacements | length > 0 // false')
117
+
118
+ ISSUES=""
119
+
120
+ # ── 章节校验 ──
121
+ # 使用项目目录下的临时文件,避免 /tmp 在沙箱中不可写导致静默跳过
122
+ TMPFILE="$PROJECT_DIR/.qoder/.doc-guard-issues.tmp"
123
+ : > "$TMPFILE"
124
+ trap "rm -f $TMPFILE" EXIT
125
+ # SearchReplace 只传 patch → 跳过章节/子章节校验(无法从 patch 推断完整文档结构)
126
+ if [ "$IS_SEARCH_REPLACE" != "true" ]; then
127
+ jq -r '.sections[] | select(.required == true) | .heading' "$SCHEMA_FILE" 2>/dev/null | while IFS= read -r heading; do
128
+ if ! echo "$CONTENT" | grep -qF "$heading"; then
129
+ echo " 缺章节: $heading"
130
+ fi
131
+ done > "$TMPFILE"
132
+
133
+ # 子章节
134
+ jq -r '.sections[].subsections[]?.heading' "$SCHEMA_FILE" 2>/dev/null | while IFS= read -r sub; do
135
+ if ! echo "$CONTENT" | grep -qF "$sub"; then
136
+ echo " 缺子章节: $sub"
137
+ fi
138
+ done >> "$TMPFILE"
139
+ fi
140
+
141
+ # ── 占位符校验 ──
142
+ jq -r '.placeholders[]?' "$SCHEMA_FILE" 2>/dev/null | while IFS= read -r ph; do
143
+ if echo "$CONTENT" | grep -qF "$ph"; then
144
+ echo " 未替换占位符: $ph"
145
+ fi
146
+ done >> "$TMPFILE"
147
+
148
+ # ── 禁止词校验 ──
149
+ jq -r '.forbidden_terms[]?' "$SCHEMA_FILE" 2>/dev/null | while IFS= read -r term; do
150
+ if echo "$CONTENT" | grep -qw "$term"; then
151
+ echo " 禁止词: $term"
152
+ fi
153
+ done >> "$TMPFILE"
154
+
155
+ ISSUES=$(cat "$TMPFILE" 2>/dev/null)
156
+
157
+ # ── 阻断或放行 ──
158
+ if [ -n "$ISSUES" ]; then
159
+ echo "⛔ $TEMPLATE_NAME 校验不通过:
160
+ $ISSUES" >&2
161
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"ask\",\"permissionDecisionReason\":\"文档格式校验不通过,请对照模板修正\"}}"
162
+ exit 2
163
+ fi
164
+
165
+ # ── 自动更新 index.md ──
166
+ if echo "$file_path" | grep -q '\.qoder/plans/'; then
167
+ if [ -x "$PROJECT_DIR/scripts/gen-plan-index.sh" ]; then
168
+ "$PROJECT_DIR/scripts/gen-plan-index.sh" 2>/dev/null || true
169
+ fi
170
+ fi
171
+
172
+ exit 0
@@ -1,4 +1,33 @@
1
1
  #!/bin/bash
2
+ # Notification — Review 提醒 + Token 预警(Claude Code 适配)
3
+ # 治理卡位 #12: 开发提醒/Token 预警
4
+ set -euo pipefail
5
+
6
+ input=$(cat)
7
+ ntype=$(echo "$input" | jq -r '.notification_type // ""' 2>/dev/null || echo "")
8
+
9
+ if [ "$ntype" != "result" ]; then
10
+ exit 0
11
+ fi
12
+
13
+ HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
14
+ export CURRENT_MAGIC=$(basename "$(dirname "$HOOK_DIR")")
15
+ COMMON_LIB="$HOOK_DIR/lib/common.sh"
16
+ [ -f "$COMMON_LIB" ] && source "$COMMON_LIB"
17
+
18
+ export PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
19
+
20
+ state=$(detect_active_add 2>/dev/null || true)
21
+ [ -z "$state" ] && exit 0
22
+
23
+ IFS='::' read -r plan _ _ _ _ <<< "$state"
24
+
25
+ reviews_dir="${PROJECT_DIR}/.qoder/reviews"
26
+ if ls "$reviews_dir"/*.md >/dev/null 2>&1; then
27
+ echo "[ADD Notification] Plan: ${plan} — 请检查 Review 文档: ${reviews_dir}"
28
+ fi
29
+ exit 0
30
+ #!/bin/bash
2
31
  # Notification — 通知事件处理(Claude Code 适配)
3
32
  set -euo pipefail
4
33
 
@@ -0,0 +1,42 @@
1
+ #!/bin/bash
2
+ ###
3
+ # @Author : xiaomingming wujixmm@gmail.com
4
+ # @Date : 2026-07-17 13:36:32
5
+ # @LastEditors : xiaomingming wujixmm@gmail.com
6
+ # @LastEditTime : 2026-07-17 13:36:33
7
+ # @FilePath : /add-coder/templates/adapters/claude/hooks/permission-denied.sh
8
+ # @Description :
9
+ ###
10
+ # permission-denied.sh — Claude Code PermissionDenied:拒绝记录 + 替代方案
11
+ # 治理卡位 #14: 拒绝原因记录 + 替代方案注入
12
+ # Claude Code 独有事件
13
+ set -euo pipefail
14
+
15
+ HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
16
+ COMMON_LIB="$HOOK_DIR/lib/common.sh"
17
+ [ -f "$COMMON_LIB" ] && source "$COMMON_LIB"
18
+
19
+ input=$(parse_input)
20
+ tool_name=$(json_get "$input" "tool_name")
21
+ [ -z "$tool_name" ] && tool_name=$(echo "$input" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | sed 's/.*: *"\([^"]*\)".*/\1/' || echo "unknown")
22
+
23
+ reason=$(json_get "$input" "reason")
24
+ [ -z "$reason" ] && reason=$(echo "$input" | grep -o '"reason"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | sed 's/.*: *"\([^"]*\)".*/\1/' || echo "权限被拒绝")
25
+
26
+ # 记录拒绝
27
+ echo "[ADD PermissionDenied] ${tool_name} 被拒绝: ${reason}" >&2
28
+
29
+ # 按工具类型提供替代建议
30
+ case "$tool_name" in
31
+ Bash)
32
+ echo "[ADD PermissionDenied] 建议: 使用安全的等价命令,或通过 permission-gate.sh 白名单放行" >&2
33
+ ;;
34
+ Write|Edit)
35
+ echo "[ADD PermissionDenied] 建议: 检查目标路径是否在项目范围内,敏感文件(.env等)不可写入" >&2
36
+ ;;
37
+ Read)
38
+ echo "[ADD PermissionDenied] 建议: 该文件可能受读保护,尝试读取项目公有文件替代" >&2
39
+ ;;
40
+ esac
41
+
42
+ exit 0