add-coder 0.3.34 → 0.3.35

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 (147) hide show
  1. package/dist/index.js +21 -3
  2. package/package.json +3 -3
  3. package/templates/.add-coder-src-hash.json +144 -90
  4. package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -88
  5. package/templates/adapters/claude/hooks/notification.mjs +0 -4
  6. package/templates/adapters/claude/hooks/permission-denied.mjs +0 -4
  7. package/templates/adapters/claude/hooks/permission-gate.mjs +0 -4
  8. package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -5
  9. package/templates/adapters/claude/hooks/pre-compact.mjs +0 -4
  10. package/templates/adapters/claude/hooks/pre-tool-use.mjs +0 -4
  11. package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -4
  12. package/templates/adapters/claude/hooks/session-end.mjs +0 -4
  13. package/templates/adapters/claude/hooks/session-start.mjs +65 -5
  14. package/templates/adapters/claude/hooks/stop-check.mjs +6 -87
  15. package/templates/adapters/claude/hooks/stop-failure.mjs +0 -4
  16. package/templates/adapters/claude/hooks/subagent-guard.mjs +0 -4
  17. package/templates/adapters/claude/hooks/subagent-stop.mjs +0 -4
  18. package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -88
  19. package/templates/adapters/codex/hooks/notification.mjs +0 -4
  20. package/templates/adapters/codex/hooks/permission-gate.mjs +0 -4
  21. package/templates/adapters/codex/hooks/post-tool-failure.mjs +0 -4
  22. package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -5
  23. package/templates/adapters/codex/hooks/pre-compact.mjs +0 -4
  24. package/templates/adapters/codex/hooks/pre-tool-use.mjs +0 -4
  25. package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -4
  26. package/templates/adapters/codex/hooks/session-end.mjs +0 -4
  27. package/templates/adapters/codex/hooks/session-start.mjs +65 -5
  28. package/templates/adapters/codex/hooks/stop-check.mjs +6 -87
  29. package/templates/adapters/codex/hooks/subagent-guard.mjs +0 -4
  30. package/templates/adapters/codex/hooks/subagent-stop.mjs +0 -4
  31. package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -88
  32. package/templates/adapters/qoder/hooks/notification.mjs +0 -4
  33. package/templates/adapters/qoder/hooks/permission-gate.mjs +0 -4
  34. package/templates/adapters/qoder/hooks/post-tool-failure.mjs +0 -4
  35. package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -5
  36. package/templates/adapters/qoder/hooks/pre-compact.mjs +0 -4
  37. package/templates/adapters/qoder/hooks/pre-tool-use.mjs +0 -4
  38. package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -4
  39. package/templates/adapters/qoder/hooks/review-checklist.mjs +0 -4
  40. package/templates/adapters/qoder/hooks/session-end.mjs +0 -4
  41. package/templates/adapters/qoder/hooks/session-start.mjs +67 -5
  42. package/templates/adapters/qoder/hooks/stop-check.mjs +6 -87
  43. package/templates/adapters/qoder/hooks/subagent-guard.mjs +0 -4
  44. package/templates/adapters/qoder/hooks/subagent-stop.mjs +0 -4
  45. package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -88
  46. package/templates/adapters/trae/hooks/notification.mjs +0 -4
  47. package/templates/adapters/trae/hooks/permission-gate.mjs +0 -4
  48. package/templates/adapters/trae/hooks/post-tool-failure.mjs +0 -4
  49. package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -5
  50. package/templates/adapters/trae/hooks/pre-compact.mjs +0 -4
  51. package/templates/adapters/trae/hooks/pre-tool-use.mjs +0 -4
  52. package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -4
  53. package/templates/adapters/trae/hooks/session-end.mjs +0 -4
  54. package/templates/adapters/trae/hooks/session-start.mjs +65 -5
  55. package/templates/adapters/trae/hooks/stop-check.mjs +6 -87
  56. package/templates/adapters/trae/hooks/subagent-guard.mjs +0 -4
  57. package/templates/adapters/trae/hooks/subagent-stop.mjs +0 -4
  58. package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -88
  59. package/templates/adapters/vscode/hooks/notification.mjs +0 -4
  60. package/templates/adapters/vscode/hooks/permission-gate.mjs +0 -4
  61. package/templates/adapters/vscode/hooks/post-tool-failure.mjs +0 -4
  62. package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -5
  63. package/templates/adapters/vscode/hooks/pre-compact.mjs +0 -4
  64. package/templates/adapters/vscode/hooks/pre-tool-use.mjs +0 -4
  65. package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -4
  66. package/templates/adapters/vscode/hooks/session-end.mjs +0 -4
  67. package/templates/adapters/vscode/hooks/session-start.mjs +65 -5
  68. package/templates/adapters/vscode/hooks/stop-check.mjs +6 -87
  69. package/templates/adapters/vscode/hooks/subagent-guard.mjs +0 -4
  70. package/templates/adapters/vscode/hooks/subagent-stop.mjs +0 -4
  71. package/templates/core/governance/doc-format-guard.ts +29 -112
  72. package/templates/core/governance/post-tool-router.ts +33 -1
  73. package/templates/core/governance/prompt-router.ts +47 -0
  74. package/templates/core/governance/rules.ts +0 -4
  75. package/templates/core/governance/session-start-guard.ts +48 -1
  76. package/templates/core/governance/stop-router.ts +7 -101
  77. package/templates/core/prisma/add.prisma +203 -0
  78. package/templates/core/scripts/db-ensure.sh +61 -2
  79. package/templates/core/scripts/mcp-server/shared/db-types.ts +119 -0
  80. package/templates/core/scripts/mcp-server/shared/hitl-create-policy.ts +27 -0
  81. package/templates/core/scripts/mcp-server/shared/hitl-proposal-content.ts +110 -0
  82. package/templates/core/scripts/mcp-server/shared/hitl-widget-instance.ts +85 -0
  83. package/templates/core/scripts/mcp-server/shared/memory/calibration/batch-fit.ts +250 -0
  84. package/templates/core/scripts/mcp-server/shared/memory/calibration/feedback-stats.ts +101 -0
  85. package/templates/core/scripts/mcp-server/shared/memory/calibration/unit-state.ts +224 -0
  86. package/templates/core/scripts/mcp-server/shared/memory/domain/conflicts.ts +59 -0
  87. package/templates/core/scripts/mcp-server/shared/memory/domain/dedup.ts +45 -0
  88. package/templates/core/scripts/mcp-server/shared/memory/domain/errors.ts +33 -0
  89. package/templates/core/scripts/mcp-server/shared/memory/domain/handoff-digest.ts +92 -0
  90. package/templates/core/scripts/mcp-server/shared/memory/domain/metric-candidate.ts +79 -0
  91. package/templates/core/scripts/mcp-server/shared/memory/domain/scope.ts +94 -0
  92. package/templates/core/scripts/mcp-server/shared/memory/domain/secrets.ts +50 -0
  93. package/templates/core/scripts/mcp-server/shared/memory/domain/state-machine.ts +90 -0
  94. package/templates/core/scripts/mcp-server/shared/memory/embedding/index.ts +117 -0
  95. package/templates/core/scripts/mcp-server/shared/memory/embedding/local-onnx.ts +105 -0
  96. package/templates/core/scripts/mcp-server/shared/memory/embedding/openai-compatible.ts +87 -0
  97. package/templates/core/scripts/mcp-server/shared/memory/jobs/consolidation.ts +226 -0
  98. package/templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts +153 -0
  99. package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +114 -0
  100. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-recall.ts +134 -0
  101. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-writer.ts +217 -0
  102. package/templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts +69 -0
  103. package/templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts +89 -0
  104. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +139 -0
  105. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +29 -0
  106. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +106 -0
  107. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fusion.ts +43 -0
  108. package/templates/core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts +285 -0
  109. package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +31 -0
  110. package/templates/core/scripts/mcp-server/shared/memory/retrieval/recall-writer.ts +87 -0
  111. package/templates/core/scripts/mcp-server/shared/memory/retrieval/reranker.ts +116 -0
  112. package/templates/core/scripts/mcp-server/shared/memory/retrieval/types.ts +52 -0
  113. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/pgvector.ts +143 -0
  114. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/sqlite-vec.ts +118 -0
  115. package/templates/core/scripts/mcp-server/shared/memory/switches.ts +39 -0
  116. package/templates/core/scripts/mcp-server/shared/review-files.ts +22 -0
  117. package/templates/core/scripts/mcp-server/shared/runtime-freshness.ts +235 -0
  118. package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +41 -3
  119. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +40 -1
  120. package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +2 -2
  121. package/templates/core/scripts/mcp-server/tools/hitl.ts +108 -42
  122. package/templates/core/scripts/mcp-server/tools/index.ts +7 -1
  123. package/templates/core/scripts/mcp-server/tools/memory-compat.ts +258 -0
  124. package/templates/core/scripts/mcp-server/tools/memory.ts +654 -0
  125. package/templates/core/scripts/mcp-server/tools/plan.ts +8 -3
  126. package/templates/core/scripts/mcp-server/tools/review.ts +10 -7
  127. package/templates/core/scripts/mcp-server.ts +36 -0
  128. package/templates/core/templates/checklist-template.md +13 -0
  129. package/templates/core/templates/review-implementation-template.md +24 -0
  130. package/templates/core/templates/review-template.md +16 -0
  131. package/templates/core/validation/index.ts +136 -0
  132. package/templates/core/validation/policy.ts +91 -0
  133. package/templates/core/validation/registry.ts +61 -0
  134. package/templates/core/validation/schema-validator.ts +277 -0
  135. package/templates/core/validation/validators/add-route.ts +32 -0
  136. package/templates/core/validation/validators/checklist.ts +48 -0
  137. package/templates/core/validation/validators/handoff.ts +46 -0
  138. package/templates/core/validation/validators/hitl.ts +22 -0
  139. package/templates/core/validation/validators/index.ts +52 -0
  140. package/templates/core/validation/validators/plan.ts +20 -0
  141. package/templates/core/validation/validators/report.ts +16 -0
  142. package/templates/core/validation/validators/review.ts +30 -0
  143. package/templates/core/validation/validators/spec.ts +25 -0
  144. package/templates/core/validation/validators/tasks.ts +40 -0
  145. package/templates/core/validation/validators/types.ts +32 -0
  146. package/templates/core/vocabulary/add-governance-vocabulary.md +18 -0
  147. package/templates/core/scripts/mcp-server/shared/dps-spec-ref.ts +0 -17
@@ -148,10 +148,6 @@ var protocol = {
148
148
  ".trae",
149
149
  ".codex"
150
150
  ]
151
- },
152
- "stop": {
153
- "max_prompt_per_context": 3,
154
- "window_minutes": 30
155
151
  }
156
152
  };
157
153
 
@@ -450,7 +446,45 @@ function writeHookEvent(hook, decision, cmd, reason, plan = "unknown", status =
450
446
  appendFileSync(file, line);
451
447
  }
452
448
 
449
+ // templates/core/scripts/mcp-server/shared/memory/switches.ts
450
+ function recallMode(env = process.env) {
451
+ const v = (env.ADD_MEMORY_RECALL_MODE ?? "shadow").toLowerCase();
452
+ return v === "off" || v === "inject" ? v : "shadow";
453
+ }
454
+ var L1_SNAPSHOT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
455
+
456
+ // templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts
457
+ var STAGE_LABEL = {
458
+ "plan-start": "Plan \u8D77\u8349",
459
+ "spec-start": "Spec \u8D77\u8349",
460
+ dps: "DPS \u95E8\u7981",
461
+ rahs: "RAHS \u95E8\u7981",
462
+ handoff: "Handoff \u4EA4\u63A5"
463
+ };
464
+ var STAGE_PATTERNS = [
465
+ { stage: "handoff", re: /handoff|交接(手册|文档|说明)?|交接给/i },
466
+ { stage: "rahs", re: /rahs|注意力漂移|执行健康度/i },
467
+ { stage: "dps", re: /dps|文档质量闸门|质量闸门|门禁(评分|检查)?/i },
468
+ { stage: "spec-start", re: /(生成|写|新建|起草|补)\s*(spec|规格|三元组)|specs?\s*三元组|WHEN-?THEN/i },
469
+ { stage: "plan-start", re: /(生成|写|新建|起草|补)\s*(plan|计划|方案)|plan\s*阶段|规划阶段/i }
470
+ ];
471
+ function detectRecallStage(prompt) {
472
+ if (!prompt) return null;
473
+ for (const { stage, re } of STAGE_PATTERNS) {
474
+ if (re.test(prompt)) return stage;
475
+ }
476
+ return null;
477
+ }
478
+ function buildStageRecallHint(stage, mode) {
479
+ if (mode === "off") return null;
480
+ const label = STAGE_LABEL[stage];
481
+ const tail = mode === "shadow" ? "\uFF08\u5F53\u524D shadow \u6A21\u5F0F\uFF1AHook \u4E0D\u6CE8\u5165\u4E0A\u4E0B\u6587\uFF0C\u53EC\u56DE\u7ED3\u679C\u9700\u663E\u5F0F\u6D88\u8D39\uFF09" : "\uFF08\u5F53\u524D inject \u6A21\u5F0F\uFF1A\u8C03\u7528\u540E\u4E0A\u4E0B\u6587\u5C06\u88AB\u6CE8\u5165\uFF09";
482
+ return `[Memory] \u68C0\u6D4B\u5230${label}\u9636\u6BB5 \u2192 \u5EFA\u8BAE\u8C03\u7528 recall_memory({ stage: "${stage}", query: <\u672C\u9636\u6BB5\u610F\u56FE>, planKeyword: <Plan \u5173\u952E\u8BCD> }) \u83B7\u53D6\u53D7\u6CBB\u7406\u7684\u5386\u53F2\u4E0A\u4E0B\u6587\uFF08\u542B\u6765\u6E90\u4E0E\u8BC4\u5206\uFF09\u3002${tail}
483
+ `;
484
+ }
485
+
453
486
  // templates/core/governance/prompt-router.ts
487
+ var MEMORY_RECALL_HINT_RE = /之前|上次|还记得|记得吗|为什么当时|类似问题|历史决策|以前的方案|曾经怎么/i;
454
488
  var PromptRouter = class {
455
489
  magicDir;
456
490
  constructor(magicDir) {
@@ -513,6 +547,38 @@ var PromptRouter = class {
513
547
  /** 前置注入(core: 无;qoder: 无条件 "ADD workflow active" JSON) */
514
548
  preamble(_input) {
515
549
  }
550
+ /**
551
+ * 显式记忆回忆提示(Spec §10 / Plan §9.1 补充入口):
552
+ * 命中回忆词且 ADD_MEMORY_RECALL_MODE != off → 提示调用 recall_memory。
553
+ * 不做自动召回(同步 Hook ≤200ms 无 DB);shadow 模式下提示语明示"仅显式调用"。
554
+ */
555
+ maybeMemoryHint(prompt) {
556
+ try {
557
+ if (recallMode() === "off") return;
558
+ if (!MEMORY_RECALL_HINT_RE.test(prompt)) return;
559
+ const mode = recallMode();
560
+ const suffix = mode === "shadow" ? "\uFF08\u5F53\u524D shadow \u6A21\u5F0F\uFF1A\u53EC\u56DE\u5C06\u843D\u5BA1\u8BA1\uFF0CHook \u4E0D\u81EA\u52A8\u6CE8\u5165\uFF09" : "";
561
+ process.stdout.write(
562
+ `[Memory] \u68C0\u6D4B\u5230\u5386\u53F2\u56DE\u5FC6\u610F\u56FE${suffix}\u3002\u53EF\u8C03\u7528 recall_memory({ query: <\u4F60\u7684\u95EE\u9898>, stage: "prompt" }) \u83B7\u53D6\u53D7\u6CBB\u7406\u7684\u8BB0\u5FC6\u4E0A\u4E0B\u6587\uFF08\u542B\u6765\u6E90\u4E0E\u8BC4\u5206\uFF09\u3002
563
+ `
564
+ );
565
+ } catch {
566
+ }
567
+ }
568
+ /**
569
+ * 阶段确定性召回提示(Spec §2 §DeterministicRecall,Plan §3.1 方案 B1 的 Hook 侧):
570
+ * 命中 ADD 阶段词 → 提示到工具侧做确定性召回(plan-start/spec-start/dps/rahs/handoff)。
571
+ * Hook 同步路径无 DB:只输出提示文本,不召回、不写审计;任何异常 fail-open。
572
+ */
573
+ maybeStageHint(prompt) {
574
+ try {
575
+ const stage = detectRecallStage(prompt);
576
+ if (!stage) return;
577
+ const hint = buildStageRecallHint(stage, recallMode());
578
+ if (hint) process.stdout.write(hint);
579
+ } catch {
580
+ }
581
+ }
516
582
  /** Layer 3 输出形态(core: 纯文本逐行;qoder: hookSpecificOutput JSON 包) */
517
583
  layer3Json() {
518
584
  return false;
@@ -544,6 +610,8 @@ var PromptRouter = class {
544
610
  for (const m of matched) process.stdout.write(m + "\n");
545
611
  return 0;
546
612
  }
613
+ this.maybeMemoryHint(prompt);
614
+ this.maybeStageHint(prompt);
547
615
  const devKw = loadDevKeywords();
548
616
  if (devKw.length === 0) return 0;
549
617
  if (!this.devKwMatched(prompt, devKw)) return 0;
@@ -117,10 +117,6 @@ var protocol = {
117
117
  ".trae",
118
118
  ".codex"
119
119
  ]
120
- },
121
- "stop": {
122
- "max_prompt_per_context": 3,
123
- "window_minutes": 30
124
120
  }
125
121
  };
126
122
 
@@ -122,10 +122,6 @@ var protocol = {
122
122
  ".trae",
123
123
  ".codex"
124
124
  ]
125
- },
126
- "stop": {
127
- "max_prompt_per_context": 3,
128
- "window_minutes": 30
129
125
  }
130
126
  };
131
127
 
@@ -296,7 +292,7 @@ function projectHash() {
296
292
  var DEV_FLAG = `/tmp/add_dev_${projectHash()}`;
297
293
 
298
294
  // templates/core/governance/session-start-guard.ts
299
- import { existsSync as existsSync3, readdirSync, statSync } from "node:fs";
295
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
300
296
  import { join as join3 } from "node:path";
301
297
 
302
298
  // templates/core/governance/preload-templates.ts
@@ -487,6 +483,30 @@ var PreloadTemplates = class _PreloadTemplates {
487
483
  }
488
484
  };
489
485
 
486
+ // templates/core/scripts/mcp-server/shared/memory/switches.ts
487
+ function recallMode(env = process.env) {
488
+ const v = (env.ADD_MEMORY_RECALL_MODE ?? "shadow").toLowerCase();
489
+ return v === "off" || v === "inject" ? v : "shadow";
490
+ }
491
+ function memoryMaxTokens(env = process.env) {
492
+ const n = Number(env.ADD_MEMORY_MAX_TOKENS ?? 600);
493
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 600;
494
+ }
495
+ var MEMORY_DIR_NAME = "memory";
496
+ var L1_SNAPSHOT_FILE = "l1-context.md";
497
+ var L1_SNAPSHOT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
498
+
499
+ // templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts
500
+ function estimateTokens(text) {
501
+ let cjk = 0;
502
+ let other = 0;
503
+ for (const ch of text) {
504
+ if (/[぀-ヿ㐀-䶿一-鿿豈-﫿＀-￯]/.test(ch)) cjk++;
505
+ else other++;
506
+ }
507
+ return cjk + Math.ceil(other / 4);
508
+ }
509
+
490
510
  // templates/core/governance/session-start-guard.ts
491
511
  var SessionStartGuard = class {
492
512
  projectDir;
@@ -506,6 +526,7 @@ var SessionStartGuard = class {
506
526
  this.emitIndex();
507
527
  this.emitTodoReminder(state);
508
528
  this.emitHitlPending();
529
+ this.emitMemoryL1();
509
530
  return 0;
510
531
  }
511
532
  // ─────────────────────────── 扩展点 ───────────────────────────
@@ -554,6 +575,45 @@ var SessionStartGuard = class {
554
575
  }
555
576
  }
556
577
  }
578
+ /**
579
+ * ⑤ Memory L1 快照注入(ADD_MEMORY_RECALL_MODE 三态):
580
+ * off → 不输出
581
+ * shadow → 仅提示快照存在(召回可执行并落审计,但不注入上下文)
582
+ * inject → 读预计算快照注入(新鲜度 ≤7 天,token 预算截断,带来源边界标签)
583
+ * 同步 Hook ≤200ms 约束:只读文件,任何异常 fail-open 静默跳过。
584
+ */
585
+ emitMemoryL1() {
586
+ try {
587
+ const mode = recallMode();
588
+ if (mode === "off" || !this.magicDir) return;
589
+ const file = join3(this.projectDir, this.magicDir, MEMORY_DIR_NAME, L1_SNAPSHOT_FILE);
590
+ if (!existsSync3(file)) return;
591
+ if (mode === "shadow") {
592
+ process.stdout.write(`[Memory] L1 \u5FEB\u7167\u5DF2\u751F\u6210\uFF08shadow \u6A21\u5F0F\u672A\u6CE8\u5165\uFF09\u3002\u9700\u8981\u65F6\u8C03\u7528 recall_memory \u663E\u5F0F\u53EC\u56DE: ${file}
593
+ `);
594
+ return;
595
+ }
596
+ if (Date.now() - statSync(file).mtimeMs > L1_SNAPSHOT_TTL_MS) return;
597
+ const text = readFileSync3(file, "utf-8");
598
+ const budget = memoryMaxTokens();
599
+ if (estimateTokens(text) <= budget) {
600
+ process.stdout.write(text);
601
+ return;
602
+ }
603
+ const lines = text.split("\n");
604
+ const kept = [];
605
+ let used = 0;
606
+ for (const line of lines) {
607
+ const t = estimateTokens(line);
608
+ if (used + t > budget) break;
609
+ kept.push(line);
610
+ used += t;
611
+ }
612
+ kept.push(`[Memory L1] \u26A0\uFE0F \u8D85\u51FA\u9884\u7B97 ${budget} tokens\uFF0C\u5DF2\u622A\u65AD\uFF08\u5B8C\u6574\u5FEB\u7167: ${file}\uFF09`);
613
+ process.stdout.write(kept.join("\n") + "\n");
614
+ } catch {
615
+ }
616
+ }
557
617
  };
558
618
 
559
619
  // templates/adapters/codex/hooks/session-start.ts
@@ -195,10 +195,6 @@ var protocol = {
195
195
  ".trae",
196
196
  ".codex"
197
197
  ]
198
- },
199
- "stop": {
200
- "max_prompt_per_context": 3,
201
- "window_minutes": 30
202
198
  }
203
199
  };
204
200
 
@@ -413,10 +409,6 @@ function buildStopContext(quadrant, info) {
413
409
  }
414
410
 
415
411
  // templates/core/governance/stop-router.ts
416
- import { createHash as createHash2 } from "node:crypto";
417
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
418
- import { tmpdir } from "node:os";
419
- import { join as join2 } from "node:path";
420
412
  var StopRouter = class {
421
413
  /** 主路由:返回 exit code(0 放行 / 2 阻断) */
422
414
  run() {
@@ -445,20 +437,14 @@ var StopRouter = class {
445
437
  }
446
438
  // ─────────────────────────── 扩展点 ───────────────────────────
447
439
  /**
448
- * Q4 验收决策(双维度组合——2026-08-14 Task 9.4.4④ 上提,回流: I2):
449
- * 维度 0(新增,临时方案): 弹框频控——同项目同 Plan 窗口内达上限降级放行
450
- * 维度 1(前置): DB 任务进度(step = done/total,数值且 done<total 未完成阻断提示)
451
- * 维度 2: checklist 质量(checkAddCompleteness 未闭环 阻断)
452
- * 互补非替代——codex 原 DB 进度分流语义上提 core,core checklist 质量语义保留。
453
- */
440
+ * Q4 验收决策(双维度组合——2026-08-14 Task 9.4.4④ 上提,回流: I2):
441
+ * 维度 1(前置): DB 任务进度(step = done/total,数值且 done<total 未完成阻断提示)
442
+ * 维度 2: checklist 质量(checkAddCompleteness 未闭环阻断)
443
+ * 互补非替代——codex DB 进度分流语义上提 core,core checklist 质量语义保留。
444
+ */
454
445
  q4Check(plan, rounds, step, handoff, addRoute) {
446
+ void plan;
455
447
  void rounds;
456
- const limited = this.stopPromptLimited(plan);
457
- if (limited) {
458
- process.stderr.write(`[ADD Stop] \u5DF2\u8FBE\u672C Plan \u5F39\u6846\u4E0A\u9650(${this.stopPromptMax()})\uFF0C\u672C\u6B21\u653E\u884C\u3002\u8BF7\u5728\u540E\u7EED\u5B9E\u65BD\u4E2D\u5B8C\u6210\u9A8C\u6536\u95ED\u73AF\uFF08devlog + handoff\uFF09\u540E\u505C\u6B62\u3002
459
- `);
460
- return 0;
461
- }
462
448
  const [donePart, totalPart] = step.split("/");
463
449
  if (/^\d+$/.test(donePart) && /^\d+$/.test(totalPart) && Number(donePart) < Number(totalPart)) {
464
450
  return this.emitQ4Unclosed(
@@ -475,73 +461,6 @@ var StopRouter = class {
475
461
  clearDevAction();
476
462
  return this.emitQ4Pass();
477
463
  }
478
- // ── 弹框频控(临时方案,规则真源: hook-protocol-rules.toml [protocol.stop])──
479
- // 计数隔离: 文件级 = PROJECT_DIR md5(跨项目不互扰);key 级 = planName(同项目跨 Plan 不互扰)
480
- // 故障降级: 哨兵读/写异常 → fail-open 放行(不阻塞主流程)
481
- /** 哨兵记录:同一项目同一 Plan 的弹框计数 */
482
- stopPromptMax() {
483
- const cfg = protocol.stop;
484
- return cfg?.max_prompt_per_context ?? 3;
485
- }
486
- /** 哨兵窗口(毫秒) */
487
- stopPromptWindowMs() {
488
- const cfg = protocol.stop;
489
- return (cfg?.window_minutes ?? 30) * 6e4;
490
- }
491
- /** 哨兵文件路径(项目隔离:PROJECT_DIR md5) */
492
- stopSentinelPath() {
493
- const projectDir = process.env.PROJECT_DIR || process.env.QODER_PROJECT_DIR || process.env.QODERCN_PROJECT_DIR || process.env.CLAUDE_PROJECT_DIR || process.cwd();
494
- const md5 = createHash2("md5").update(projectDir).digest("hex").slice(0, 8);
495
- return join2(tmpdir(), `add_stop_${md5}.json`);
496
- }
497
- /** 写哨兵(失败静默,fail-open) */
498
- writeStopPromptSentinel(data) {
499
- try {
500
- writeFileSync2(this.stopSentinelPath(), JSON.stringify(data), "utf-8");
501
- } catch {
502
- }
503
- }
504
- /**
505
- * 频控判定:返回 true = 已达上限应放行(不弹框)
506
- * 生成态缺失 protocol.stop(TOML 未同步)→ 跳过频控保持既有行为(生成链幂等校验兜底)
507
- */
508
- stopPromptLimited(plan) {
509
- try {
510
- const stopCfg = protocol.stop;
511
- if (!stopCfg) return false;
512
- const max = this.stopPromptMax();
513
- const windowMs = this.stopPromptWindowMs();
514
- const path3 = this.stopSentinelPath();
515
- let data = {};
516
- if (existsSync2(path3)) {
517
- try {
518
- data = JSON.parse(readFileSync2(path3, "utf-8")) ?? {};
519
- } catch {
520
- return true;
521
- }
522
- }
523
- const now = Date.now();
524
- const entry = data[plan];
525
- if (!entry) {
526
- data[plan] = { count: 1, lastPromptAt: now };
527
- this.writeStopPromptSentinel(data);
528
- return false;
529
- }
530
- if (now - entry.lastPromptAt > windowMs) {
531
- data[plan] = { count: 1, lastPromptAt: now };
532
- this.writeStopPromptSentinel(data);
533
- return false;
534
- }
535
- if (entry.count >= max) {
536
- return true;
537
- }
538
- data[plan] = { count: entry.count + 1, lastPromptAt: now };
539
- this.writeStopPromptSentinel(data);
540
- return false;
541
- } catch {
542
- return true;
543
- }
544
- }
545
464
  /** Q0: DB 不可用(core: stderr + 2) */
546
465
  emitQ0(reason) {
547
466
  process.stderr.write(
@@ -116,10 +116,6 @@ var protocol = {
116
116
  ".trae",
117
117
  ".codex"
118
118
  ]
119
- },
120
- "stop": {
121
- "max_prompt_per_context": 3,
122
- "window_minutes": 30
123
119
  }
124
120
  };
125
121
 
@@ -117,10 +117,6 @@ var protocol = {
117
117
  ".trae",
118
118
  ".codex"
119
119
  ]
120
- },
121
- "stop": {
122
- "max_prompt_per_context": 3,
123
- "window_minutes": 30
124
120
  }
125
121
  };
126
122
 
@@ -388,10 +388,6 @@ var protocol = {
388
388
  ".trae",
389
389
  ".codex"
390
390
  ]
391
- },
392
- "stop": {
393
- "max_prompt_per_context": 3,
394
- "window_minutes": 30
395
391
  }
396
392
  };
397
393
 
@@ -500,6 +496,156 @@ function writeHookEvent(hook, decision, cmd, reason, plan = "unknown", status =
500
496
  appendFileSync(file, line);
501
497
  }
502
498
 
499
+ // templates/core/validation/schema-validator.ts
500
+ function countOccurrences(haystack, needle) {
501
+ if (!needle) return 0;
502
+ const h = normalizeWidth(haystack);
503
+ const n = normalizeWidth(needle);
504
+ let count = 0;
505
+ let idx = h.indexOf(n);
506
+ while (idx !== -1) {
507
+ count++;
508
+ idx = h.indexOf(n, idx + n.length);
509
+ }
510
+ return count;
511
+ }
512
+ var FULLWIDTH_START = 65281;
513
+ var FULLWIDTH_END = 65374;
514
+ var FULLWIDTH_TO_HALF = 65248;
515
+ var WIDTH_FOLD_EXTRA = { "\u3000": " " };
516
+ function normalizeWidth(text) {
517
+ let out = "";
518
+ for (const ch of text) {
519
+ const code = ch.codePointAt(0);
520
+ if (code >= FULLWIDTH_START && code <= FULLWIDTH_END) {
521
+ out += String.fromCharCode(code - FULLWIDTH_TO_HALF);
522
+ } else {
523
+ out += WIDTH_FOLD_EXTRA[ch] ?? ch;
524
+ }
525
+ }
526
+ return out;
527
+ }
528
+ function stripFencedBlocks(content) {
529
+ return content.replace(/```[\s\S]*?```/g, "\n");
530
+ }
531
+ function structText(content, groupColumn) {
532
+ let text = (content.match(/^#{2,}\s.*$/gm) ?? []).join("\n");
533
+ const col = typeof groupColumn === "number" ? groupColumn : Number(groupColumn);
534
+ if (Number.isFinite(col) && col > 0) {
535
+ const cells = content.split("\n").map((line) => {
536
+ const cells2 = line.split("|");
537
+ return cells2.length > col ? (cells2[col + 1] ?? "").trim() : "";
538
+ }).filter(Boolean);
539
+ text += "\n" + cells.join("\n");
540
+ }
541
+ return text;
542
+ }
543
+ function validateAnchors(content, schema, templateContent) {
544
+ const issues = [];
545
+ const nContent = normalizeWidth(content);
546
+ for (const section of schema.sections) {
547
+ if (!section.anchor) continue;
548
+ const refAnchor = normalizeWidth(section.anchor);
549
+ const refLine = templateContent.split("\n").find((l) => normalizeWidth(l).includes(refAnchor));
550
+ if (!refLine) continue;
551
+ const tokens = [
552
+ ...new Set(
553
+ normalizeWidth(refLine).replace(/[#*`|(){]/g, " ").split(/\s+/).filter((t) => t !== "" && !t.includes("{"))
554
+ )
555
+ ];
556
+ if (tokens.length === 0) continue;
557
+ let scope = nContent;
558
+ if (section.within) {
559
+ const startIdx = nContent.indexOf(normalizeWidth(section.within));
560
+ if (startIdx < 0) continue;
561
+ const endIdx = nContent.indexOf("\n## ", startIdx + 1);
562
+ scope = nContent.slice(startIdx, endIdx === -1 ? void 0 : endIdx);
563
+ }
564
+ const missTokens = tokens.filter((tok) => !scope.includes(tok));
565
+ if (missTokens.length > 0) {
566
+ issues.push({
567
+ code: "ANCHOR_MISS",
568
+ detail: `\u7F3A\u951A\u70B9(${section.id}): ${missTokens.join(" ")}`,
569
+ expected: section.anchor
570
+ });
571
+ }
572
+ }
573
+ return issues;
574
+ }
575
+ function inferRoundHeading(schema) {
576
+ const round = schema.sections.find((s) => s.heading?.includes("<\u7B2CN\u8F6E>"));
577
+ return round?.heading ?? null;
578
+ }
579
+ function countRounds(content, roundHeading) {
580
+ const heading = normalizeWidth(roundHeading);
581
+ if (!heading.includes("<\u7B2CN\u8F6E>")) return 0;
582
+ const pattern = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace("<\u7B2CN\u8F6E>", "\u7B2C\\s*\\d+\\s*\u8F6E");
583
+ const re = new RegExp(`^${pattern}`, "gm");
584
+ return (normalizeWidth(content).match(re) ?? []).length;
585
+ }
586
+ function validateAgainstSchema(content, schema, opts = {}) {
587
+ const issues = [];
588
+ const proseOnly = stripFencedBlocks(content);
589
+ const contentNormalized = normalizeWidth(content);
590
+ const roundHeading = opts.roundHeading ?? inferRoundHeading(schema);
591
+ for (const section of schema.sections) {
592
+ if (!section.heading) continue;
593
+ const isRoundSection = roundHeading !== null && section.heading === roundHeading;
594
+ if (isRoundSection) {
595
+ const need = opts.expectRounds ?? 1;
596
+ const found = countRounds(content, roundHeading);
597
+ if (found < need) {
598
+ issues.push({
599
+ code: "ROUND_COUNT_SHORT",
600
+ detail: `\u8F6E\u6B21\u7AE0\u8282\u4E0D\u8DB3\uFF1A\u671F\u671B ${need} \u8F6E\uFF0C\u5B9E\u9645 ${found} \u8F6E`,
601
+ expected: roundHeading
602
+ });
603
+ }
604
+ for (const sub of section.subsections ?? []) {
605
+ if (countOccurrences(content, sub.heading) < need) {
606
+ issues.push({
607
+ code: "MISSING_SUBSECTION",
608
+ detail: `\u8F6E\u6B21\u5B50\u7AE0\u8282\u7F3A\u5931\u6216\u4E0D\u8DB3\uFF08\u9700\u8981 ${need} \u4EFD\uFF09\uFF1A${sub.heading}`,
609
+ expected: sub.heading
610
+ });
611
+ }
612
+ }
613
+ continue;
614
+ }
615
+ if (section.required && countOccurrences(content, section.heading) === 0) {
616
+ issues.push({ code: "MISSING_SECTION", detail: `\u7F3A\u5C11\u5FC5\u9700\u7AE0\u8282\uFF1A${section.heading}`, expected: section.heading });
617
+ }
618
+ for (const sub of section.subsections ?? []) {
619
+ const need = isRoundSection ? opts.expectRounds ?? 1 : 1;
620
+ if (countOccurrences(content, sub.heading) < need) {
621
+ issues.push({
622
+ code: "MISSING_SUBSECTION",
623
+ detail: `\u5B50\u7AE0\u8282\u7F3A\u5931\u6216\u4E0D\u8DB3\uFF08\u9700\u8981 ${need} \u4EFD\uFF09\uFF1A${sub.heading}`,
624
+ expected: sub.heading
625
+ });
626
+ }
627
+ }
628
+ }
629
+ for (const ph of schema.placeholders ?? []) {
630
+ if (!ph) continue;
631
+ const nPh = normalizeWidth(ph);
632
+ const idx = contentNormalized.indexOf(nPh);
633
+ if (idx === -1) continue;
634
+ const hit = nPh === ph ? ph : `${content.slice(idx, idx + nPh.length)}\uFF08\u5BBD\u5EA6\u7B49\u4EF7\u4E8E ${ph}\uFF09`;
635
+ issues.push({ code: "PLACEHOLDER_LEFT", detail: `\u5360\u4F4D\u7B26\u672A\u66FF\u6362\uFF1A${hit}`, expected: ph });
636
+ }
637
+ const struct = normalizeWidth(structText(content, schema.groupColumn));
638
+ for (const term of schema.forbidden_terms ?? []) {
639
+ if (term && struct.includes(normalizeWidth(term))) {
640
+ issues.push({ code: "FORBIDDEN_TERM", detail: `\u7ED3\u6784\u4F4D\u7981\u8BCD\uFF1A${term}\uFF08\u4EC5\u6807\u9898\u884C\u4E0E\u6307\u5B9A\u5217\u5224\u5B9A\uFF09`, expected: term });
641
+ }
642
+ }
643
+ if (opts.templateContent) {
644
+ issues.push(...validateAnchors(content, schema, opts.templateContent));
645
+ }
646
+ return issues;
647
+ }
648
+
503
649
  // templates/core/governance/doc-format-guard.ts
504
650
  function extractContent(input) {
505
651
  const ti = input.tool_input;
@@ -595,90 +741,28 @@ var DocFormatGuard = class {
595
741
  }
596
742
  /** 章节/锚定/占位符/禁词校验,返回 struct 统计 */
597
743
  runSchemaChecks(schema, templateName, content, isSearchReplace) {
598
- const templatesDir = join3(this.projectDir, this.magicDir, "templates");
599
- let applied = 0;
600
- let missed = 0;
601
- let anchorHit = true;
602
- const headings = schema.sections.filter((s) => s.heading).map((s) => s.heading);
603
- const requiredHeadings = schema.sections.filter((s) => s.required === true && s.heading).map((s) => s.heading);
604
- const subs = schema.sections.flatMap((s) => s.subsections ?? []).filter((s) => s.heading).map((s) => s.heading);
605
- const placeholders = schema.placeholders ?? [];
606
- const terms = schema.forbidden_terms ?? [];
607
- if (!isSearchReplace) {
608
- for (const section of schema.sections) {
609
- if (!section.anchor) continue;
610
- applied++;
611
- const templateContent = existsSync3(join3(templatesDir, templateName)) ? readFileSync2(join3(templatesDir, templateName), "utf-8") : "";
612
- const refLine = templateContent.split("\n").find((l) => l.includes(section.anchor));
613
- if (!refLine) {
614
- process.stderr.write(`[doc-format-guard] anchor_miss: schema ${section.id} \u58F0\u660E\u7684 anchor '${section.anchor}' \u5728 ${templateName} \u4E2D\u672A\u5B9A\u4F4D\uFF0C\u8DF3\u8FC7\u8BE5\u89C4\u5219\uFF08\u5192\u70DF\u5DE1\u68C0\u5151\u5E95\uFF09
615
- `);
616
- applied--;
617
- continue;
618
- }
619
- const tokens = [...new Set(refLine.replace(/[#*`|(){]/g, " ").split(/\s+/).filter((t) => t !== "" && !t.includes("{")))];
620
- if (tokens.length === 0) {
621
- applied--;
622
- continue;
623
- }
624
- let scope = content;
625
- if (section.within) {
626
- if (content.includes(section.within)) {
627
- const startIdx = content.indexOf(section.within);
628
- const endIdx = content.indexOf("\n## ", startIdx + 1);
629
- scope = content.slice(startIdx, endIdx === -1 ? void 0 : endIdx);
630
- } else {
631
- process.stderr.write(`[doc-format-guard] within_miss: schema ${section.id} \u7684 within '${section.within}' \u5728\u6587\u6863\u4E2D\u672A\u5B9A\u4F4D\uFF0C\u8DF3\u8FC7\u8BE5\u89C4\u5219
632
- `);
633
- applied--;
634
- continue;
635
- }
636
- }
637
- const missTokens = tokens.filter((tok) => !scope.includes(tok));
638
- if (missTokens.length > 0) {
639
- this.issues.push(` \u7F3A\u951A\u70B9(${section.id}): ${missTokens.join(" ")}`);
640
- missed++;
641
- anchorHit = false;
642
- }
643
- }
644
- for (const heading of requiredHeadings) {
645
- applied++;
646
- if (!content.includes(heading)) {
647
- this.issues.push(` \u7F3A\u7AE0\u8282: ${heading}`);
648
- missed++;
649
- }
650
- }
651
- for (const sub of subs) {
652
- applied++;
653
- if (!content.includes(sub)) {
654
- this.issues.push(` \u7F3A\u5B50\u7AE0\u8282: ${sub}`);
655
- missed++;
656
- }
657
- }
658
- }
659
- for (const ph of placeholders) {
660
- if (content.includes(ph)) {
661
- this.issues.push(` \u672A\u66FF\u6362\u5360\u4F4D\u7B26: ${ph}`);
662
- missed++;
663
- }
664
- }
665
- let structText = (content.match(/^#{2,}\s.*$/gm) ?? []).join("\n");
666
- const col = typeof schema.groupColumn === "number" ? schema.groupColumn : Number(schema.groupColumn);
667
- if (!Number.isNaN(col) && col > 0) {
668
- const colLines = content.split("\n").map((l) => {
669
- const cells = l.split("|");
670
- return cells.length > col ? (cells[col + 1] ?? "").trim() : "";
671
- }).filter(Boolean);
672
- structText += "\n" + colLines.join("\n");
673
- }
674
- for (const term of terms) {
675
- applied++;
676
- if (structText.includes(term)) {
677
- this.issues.push(` \u7ED3\u6784\u4F4D\u7981\u8BCD: ${term}`);
678
- missed++;
679
- }
680
- }
681
- return { applied, missed, anchorHit };
744
+ const templatePath = join3(this.projectDir, this.magicDir, "templates", templateName);
745
+ const templateContent = existsSync3(templatePath) ? readFileSync2(templatePath, "utf-8") : "";
746
+ const all = validateAgainstSchema(content, schema, { templateContent });
747
+ const relevant = isSearchReplace ? all.filter((i) => i.code === "PLACEHOLDER_LEFT" || i.code === "FORBIDDEN_TERM") : all;
748
+ for (const i of relevant) this.issues.push(` ${i.detail}`);
749
+ const missed = relevant.length;
750
+ const declared = this.countDeclaredRules(schema, isSearchReplace);
751
+ return {
752
+ applied: Math.max(missed, declared),
753
+ missed,
754
+ anchorHit: !relevant.some((i) => i.code === "ANCHOR_MISS")
755
+ };
756
+ }
757
+ /** 已声明规则数(用于 struct_score 分母;口径与原内联实现一致:按规则条数计) */
758
+ countDeclaredRules(schema, isSearchReplace) {
759
+ const placeholders = (schema.placeholders ?? []).length;
760
+ const terms = (schema.forbidden_terms ?? []).length;
761
+ if (isSearchReplace) return placeholders + terms;
762
+ const anchors = schema.sections.filter((s) => s.anchor).length;
763
+ const required = schema.sections.filter((s) => s.required === true && s.heading).length;
764
+ const subs = schema.sections.flatMap((s) => s.subsections ?? []).filter((s) => s.heading).length;
765
+ return anchors + required + subs + placeholders + terms;
682
766
  }
683
767
  /** 算法化规则校验(真源: [doc.anti_cheat] + HITL 表非空 + handoff 冲突)
684
768
  * protected: adapter 子类可 override(如 claude 版无算法规则段) */
@@ -116,10 +116,6 @@ var protocol = {
116
116
  ".trae",
117
117
  ".codex"
118
118
  ]
119
- },
120
- "stop": {
121
- "max_prompt_per_context": 3,
122
- "window_minutes": 30
123
119
  }
124
120
  };
125
121
 
@@ -116,10 +116,6 @@ var protocol = {
116
116
  ".trae",
117
117
  ".codex"
118
118
  ]
119
- },
120
- "stop": {
121
- "max_prompt_per_context": 3,
122
- "window_minutes": 30
123
119
  }
124
120
  };
125
121