add-coder 0.3.34 → 0.3.37
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.en.md +101 -42
- package/README.md +63 -18
- package/dist/index.js +24 -6
- package/package.json +2 -2
- package/templates/.add-coder-src-hash.json +94 -39
- package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/claude/hooks/session-start.mjs +65 -1
- package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/codex/hooks/session-start.mjs +65 -1
- package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/qoder/hooks/session-start.mjs +67 -1
- package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/trae/hooks/session-start.mjs +65 -1
- package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/vscode/hooks/session-start.mjs +65 -1
- package/templates/core/governance/doc-format-guard.ts +29 -112
- package/templates/core/governance/post-tool-router.ts +33 -1
- package/templates/core/governance/prompt-router.ts +47 -0
- package/templates/core/governance/session-start-guard.ts +48 -1
- package/templates/core/prisma/add.prisma +203 -0
- package/templates/core/scripts/db-ensure.sh +92 -2
- package/templates/core/scripts/mcp-server/shared/db-types.ts +119 -0
- package/templates/core/scripts/mcp-server/shared/hitl-create-policy.ts +27 -0
- package/templates/core/scripts/mcp-server/shared/hitl-proposal-content.ts +110 -0
- package/templates/core/scripts/mcp-server/shared/hitl-widget-instance.ts +85 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/batch-fit.ts +250 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/feedback-stats.ts +101 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/unit-state.ts +224 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/conflicts.ts +59 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/dedup.ts +45 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/errors.ts +33 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/handoff-digest.ts +92 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/metric-candidate.ts +79 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/scope.ts +94 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/secrets.ts +50 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/state-machine.ts +90 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/index.ts +117 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/local-onnx.ts +105 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/openai-compatible.ts +87 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/consolidation.ts +226 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts +153 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +114 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-recall.ts +134 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-writer.ts +217 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts +69 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts +89 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +139 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +29 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +106 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fusion.ts +43 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts +285 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +31 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/recall-writer.ts +87 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/reranker.ts +116 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/types.ts +52 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/pgvector.ts +143 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/sqlite-vec.ts +118 -0
- package/templates/core/scripts/mcp-server/shared/memory/switches.ts +39 -0
- package/templates/core/scripts/mcp-server/shared/review-files.ts +22 -0
- package/templates/core/scripts/mcp-server/shared/runtime-freshness.ts +235 -0
- package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +37 -0
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +40 -1
- package/templates/core/scripts/mcp-server/tools/hitl.ts +108 -42
- package/templates/core/scripts/mcp-server/tools/index.ts +7 -1
- package/templates/core/scripts/mcp-server/tools/memory-compat.ts +258 -0
- package/templates/core/scripts/mcp-server/tools/memory.ts +654 -0
- package/templates/core/scripts/mcp-server/tools/plan.ts +8 -3
- package/templates/core/scripts/mcp-server/tools/review.ts +10 -7
- package/templates/core/scripts/mcp-server.ts +36 -0
- package/templates/core/templates/checklist-template.md +13 -0
- package/templates/core/templates/review-implementation-template.md +24 -0
- package/templates/core/templates/review-template.md +16 -0
- package/templates/core/validation/index.ts +136 -0
- package/templates/core/validation/policy.ts +91 -0
- package/templates/core/validation/registry.ts +61 -0
- package/templates/core/validation/schema-validator.ts +277 -0
- package/templates/core/validation/validators/add-route.ts +32 -0
- package/templates/core/validation/validators/checklist.ts +48 -0
- package/templates/core/validation/validators/handoff.ts +46 -0
- package/templates/core/validation/validators/hitl.ts +22 -0
- package/templates/core/validation/validators/index.ts +52 -0
- package/templates/core/validation/validators/plan.ts +20 -0
- package/templates/core/validation/validators/report.ts +16 -0
- package/templates/core/validation/validators/review.ts +30 -0
- package/templates/core/validation/validators/spec.ts +25 -0
- package/templates/core/validation/validators/tasks.ts +40 -0
- package/templates/core/validation/validators/types.ts +32 -0
- package/templates/core/vocabulary/add-governance-vocabulary.md +18 -0
|
@@ -450,7 +450,45 @@ function writeHookEvent(hook, decision, cmd, reason, plan = "unknown", status =
|
|
|
450
450
|
appendFileSync(file, line);
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
+
// templates/core/scripts/mcp-server/shared/memory/switches.ts
|
|
454
|
+
function recallMode(env = process.env) {
|
|
455
|
+
const v = (env.ADD_MEMORY_RECALL_MODE ?? "shadow").toLowerCase();
|
|
456
|
+
return v === "off" || v === "inject" ? v : "shadow";
|
|
457
|
+
}
|
|
458
|
+
var L1_SNAPSHOT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
459
|
+
|
|
460
|
+
// templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts
|
|
461
|
+
var STAGE_LABEL = {
|
|
462
|
+
"plan-start": "Plan \u8D77\u8349",
|
|
463
|
+
"spec-start": "Spec \u8D77\u8349",
|
|
464
|
+
dps: "DPS \u95E8\u7981",
|
|
465
|
+
rahs: "RAHS \u95E8\u7981",
|
|
466
|
+
handoff: "Handoff \u4EA4\u63A5"
|
|
467
|
+
};
|
|
468
|
+
var STAGE_PATTERNS = [
|
|
469
|
+
{ stage: "handoff", re: /handoff|交接(手册|文档|说明)?|交接给/i },
|
|
470
|
+
{ stage: "rahs", re: /rahs|注意力漂移|执行健康度/i },
|
|
471
|
+
{ stage: "dps", re: /dps|文档质量闸门|质量闸门|门禁(评分|检查)?/i },
|
|
472
|
+
{ stage: "spec-start", re: /(生成|写|新建|起草|补)\s*(spec|规格|三元组)|specs?\s*三元组|WHEN-?THEN/i },
|
|
473
|
+
{ stage: "plan-start", re: /(生成|写|新建|起草|补)\s*(plan|计划|方案)|plan\s*阶段|规划阶段/i }
|
|
474
|
+
];
|
|
475
|
+
function detectRecallStage(prompt) {
|
|
476
|
+
if (!prompt) return null;
|
|
477
|
+
for (const { stage, re } of STAGE_PATTERNS) {
|
|
478
|
+
if (re.test(prompt)) return stage;
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
function buildStageRecallHint(stage, mode) {
|
|
483
|
+
if (mode === "off") return null;
|
|
484
|
+
const label = STAGE_LABEL[stage];
|
|
485
|
+
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";
|
|
486
|
+
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}
|
|
487
|
+
`;
|
|
488
|
+
}
|
|
489
|
+
|
|
453
490
|
// templates/core/governance/prompt-router.ts
|
|
491
|
+
var MEMORY_RECALL_HINT_RE = /之前|上次|还记得|记得吗|为什么当时|类似问题|历史决策|以前的方案|曾经怎么/i;
|
|
454
492
|
var PromptRouter = class {
|
|
455
493
|
magicDir;
|
|
456
494
|
constructor(magicDir) {
|
|
@@ -513,6 +551,38 @@ var PromptRouter = class {
|
|
|
513
551
|
/** 前置注入(core: 无;qoder: 无条件 "ADD workflow active" JSON) */
|
|
514
552
|
preamble(_input) {
|
|
515
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* 显式记忆回忆提示(Spec §10 / Plan §9.1 补充入口):
|
|
556
|
+
* 命中回忆词且 ADD_MEMORY_RECALL_MODE != off → 提示调用 recall_memory。
|
|
557
|
+
* 不做自动召回(同步 Hook ≤200ms 无 DB);shadow 模式下提示语明示"仅显式调用"。
|
|
558
|
+
*/
|
|
559
|
+
maybeMemoryHint(prompt) {
|
|
560
|
+
try {
|
|
561
|
+
if (recallMode() === "off") return;
|
|
562
|
+
if (!MEMORY_RECALL_HINT_RE.test(prompt)) return;
|
|
563
|
+
const mode = recallMode();
|
|
564
|
+
const suffix = mode === "shadow" ? "\uFF08\u5F53\u524D shadow \u6A21\u5F0F\uFF1A\u53EC\u56DE\u5C06\u843D\u5BA1\u8BA1\uFF0CHook \u4E0D\u81EA\u52A8\u6CE8\u5165\uFF09" : "";
|
|
565
|
+
process.stdout.write(
|
|
566
|
+
`[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
|
|
567
|
+
`
|
|
568
|
+
);
|
|
569
|
+
} catch {
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* 阶段确定性召回提示(Spec §2 §DeterministicRecall,Plan §3.1 方案 B1 的 Hook 侧):
|
|
574
|
+
* 命中 ADD 阶段词 → 提示到工具侧做确定性召回(plan-start/spec-start/dps/rahs/handoff)。
|
|
575
|
+
* Hook 同步路径无 DB:只输出提示文本,不召回、不写审计;任何异常 fail-open。
|
|
576
|
+
*/
|
|
577
|
+
maybeStageHint(prompt) {
|
|
578
|
+
try {
|
|
579
|
+
const stage = detectRecallStage(prompt);
|
|
580
|
+
if (!stage) return;
|
|
581
|
+
const hint = buildStageRecallHint(stage, recallMode());
|
|
582
|
+
if (hint) process.stdout.write(hint);
|
|
583
|
+
} catch {
|
|
584
|
+
}
|
|
585
|
+
}
|
|
516
586
|
/** Layer 3 输出形态(core: 纯文本逐行;qoder: hookSpecificOutput JSON 包) */
|
|
517
587
|
layer3Json() {
|
|
518
588
|
return false;
|
|
@@ -544,6 +614,8 @@ var PromptRouter = class {
|
|
|
544
614
|
for (const m of matched) process.stdout.write(m + "\n");
|
|
545
615
|
return 0;
|
|
546
616
|
}
|
|
617
|
+
this.maybeMemoryHint(prompt);
|
|
618
|
+
this.maybeStageHint(prompt);
|
|
547
619
|
const devKw = loadDevKeywords();
|
|
548
620
|
if (devKw.length === 0) return 0;
|
|
549
621
|
if (!this.devKwMatched(prompt, devKw)) return 0;
|
|
@@ -296,7 +296,7 @@ function projectHash() {
|
|
|
296
296
|
var DEV_FLAG = `/tmp/add_dev_${projectHash()}`;
|
|
297
297
|
|
|
298
298
|
// templates/core/governance/session-start-guard.ts
|
|
299
|
-
import { existsSync as existsSync3, readdirSync, statSync } from "node:fs";
|
|
299
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
300
300
|
import { join as join3 } from "node:path";
|
|
301
301
|
|
|
302
302
|
// templates/core/governance/preload-templates.ts
|
|
@@ -487,6 +487,30 @@ var PreloadTemplates = class _PreloadTemplates {
|
|
|
487
487
|
}
|
|
488
488
|
};
|
|
489
489
|
|
|
490
|
+
// templates/core/scripts/mcp-server/shared/memory/switches.ts
|
|
491
|
+
function recallMode(env = process.env) {
|
|
492
|
+
const v = (env.ADD_MEMORY_RECALL_MODE ?? "shadow").toLowerCase();
|
|
493
|
+
return v === "off" || v === "inject" ? v : "shadow";
|
|
494
|
+
}
|
|
495
|
+
function memoryMaxTokens(env = process.env) {
|
|
496
|
+
const n = Number(env.ADD_MEMORY_MAX_TOKENS ?? 600);
|
|
497
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 600;
|
|
498
|
+
}
|
|
499
|
+
var MEMORY_DIR_NAME = "memory";
|
|
500
|
+
var L1_SNAPSHOT_FILE = "l1-context.md";
|
|
501
|
+
var L1_SNAPSHOT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
502
|
+
|
|
503
|
+
// templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts
|
|
504
|
+
function estimateTokens(text) {
|
|
505
|
+
let cjk = 0;
|
|
506
|
+
let other = 0;
|
|
507
|
+
for (const ch of text) {
|
|
508
|
+
if (/[-ヿ㐀-䶿一-鿿豈--]/.test(ch)) cjk++;
|
|
509
|
+
else other++;
|
|
510
|
+
}
|
|
511
|
+
return cjk + Math.ceil(other / 4);
|
|
512
|
+
}
|
|
513
|
+
|
|
490
514
|
// templates/core/governance/session-start-guard.ts
|
|
491
515
|
var SessionStartGuard = class {
|
|
492
516
|
projectDir;
|
|
@@ -506,6 +530,7 @@ var SessionStartGuard = class {
|
|
|
506
530
|
this.emitIndex();
|
|
507
531
|
this.emitTodoReminder(state);
|
|
508
532
|
this.emitHitlPending();
|
|
533
|
+
this.emitMemoryL1();
|
|
509
534
|
return 0;
|
|
510
535
|
}
|
|
511
536
|
// ─────────────────────────── 扩展点 ───────────────────────────
|
|
@@ -554,6 +579,45 @@ var SessionStartGuard = class {
|
|
|
554
579
|
}
|
|
555
580
|
}
|
|
556
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* ⑤ Memory L1 快照注入(ADD_MEMORY_RECALL_MODE 三态):
|
|
584
|
+
* off → 不输出
|
|
585
|
+
* shadow → 仅提示快照存在(召回可执行并落审计,但不注入上下文)
|
|
586
|
+
* inject → 读预计算快照注入(新鲜度 ≤7 天,token 预算截断,带来源边界标签)
|
|
587
|
+
* 同步 Hook ≤200ms 约束:只读文件,任何异常 fail-open 静默跳过。
|
|
588
|
+
*/
|
|
589
|
+
emitMemoryL1() {
|
|
590
|
+
try {
|
|
591
|
+
const mode = recallMode();
|
|
592
|
+
if (mode === "off" || !this.magicDir) return;
|
|
593
|
+
const file = join3(this.projectDir, this.magicDir, MEMORY_DIR_NAME, L1_SNAPSHOT_FILE);
|
|
594
|
+
if (!existsSync3(file)) return;
|
|
595
|
+
if (mode === "shadow") {
|
|
596
|
+
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}
|
|
597
|
+
`);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (Date.now() - statSync(file).mtimeMs > L1_SNAPSHOT_TTL_MS) return;
|
|
601
|
+
const text = readFileSync3(file, "utf-8");
|
|
602
|
+
const budget = memoryMaxTokens();
|
|
603
|
+
if (estimateTokens(text) <= budget) {
|
|
604
|
+
process.stdout.write(text);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const lines = text.split("\n");
|
|
608
|
+
const kept = [];
|
|
609
|
+
let used = 0;
|
|
610
|
+
for (const line of lines) {
|
|
611
|
+
const t = estimateTokens(line);
|
|
612
|
+
if (used + t > budget) break;
|
|
613
|
+
kept.push(line);
|
|
614
|
+
used += t;
|
|
615
|
+
}
|
|
616
|
+
kept.push(`[Memory L1] \u26A0\uFE0F \u8D85\u51FA\u9884\u7B97 ${budget} tokens\uFF0C\u5DF2\u622A\u65AD\uFF08\u5B8C\u6574\u5FEB\u7167: ${file}\uFF09`);
|
|
617
|
+
process.stdout.write(kept.join("\n") + "\n");
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
}
|
|
557
621
|
};
|
|
558
622
|
|
|
559
623
|
// templates/adapters/codex/hooks/session-start.ts
|
|
@@ -500,6 +500,156 @@ function writeHookEvent(hook, decision, cmd, reason, plan = "unknown", status =
|
|
|
500
500
|
appendFileSync(file, line);
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
// templates/core/validation/schema-validator.ts
|
|
504
|
+
function countOccurrences(haystack, needle) {
|
|
505
|
+
if (!needle) return 0;
|
|
506
|
+
const h = normalizeWidth(haystack);
|
|
507
|
+
const n = normalizeWidth(needle);
|
|
508
|
+
let count = 0;
|
|
509
|
+
let idx = h.indexOf(n);
|
|
510
|
+
while (idx !== -1) {
|
|
511
|
+
count++;
|
|
512
|
+
idx = h.indexOf(n, idx + n.length);
|
|
513
|
+
}
|
|
514
|
+
return count;
|
|
515
|
+
}
|
|
516
|
+
var FULLWIDTH_START = 65281;
|
|
517
|
+
var FULLWIDTH_END = 65374;
|
|
518
|
+
var FULLWIDTH_TO_HALF = 65248;
|
|
519
|
+
var WIDTH_FOLD_EXTRA = { "\u3000": " " };
|
|
520
|
+
function normalizeWidth(text) {
|
|
521
|
+
let out = "";
|
|
522
|
+
for (const ch of text) {
|
|
523
|
+
const code = ch.codePointAt(0);
|
|
524
|
+
if (code >= FULLWIDTH_START && code <= FULLWIDTH_END) {
|
|
525
|
+
out += String.fromCharCode(code - FULLWIDTH_TO_HALF);
|
|
526
|
+
} else {
|
|
527
|
+
out += WIDTH_FOLD_EXTRA[ch] ?? ch;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return out;
|
|
531
|
+
}
|
|
532
|
+
function stripFencedBlocks(content) {
|
|
533
|
+
return content.replace(/```[\s\S]*?```/g, "\n");
|
|
534
|
+
}
|
|
535
|
+
function structText(content, groupColumn) {
|
|
536
|
+
let text = (content.match(/^#{2,}\s.*$/gm) ?? []).join("\n");
|
|
537
|
+
const col = typeof groupColumn === "number" ? groupColumn : Number(groupColumn);
|
|
538
|
+
if (Number.isFinite(col) && col > 0) {
|
|
539
|
+
const cells = content.split("\n").map((line) => {
|
|
540
|
+
const cells2 = line.split("|");
|
|
541
|
+
return cells2.length > col ? (cells2[col + 1] ?? "").trim() : "";
|
|
542
|
+
}).filter(Boolean);
|
|
543
|
+
text += "\n" + cells.join("\n");
|
|
544
|
+
}
|
|
545
|
+
return text;
|
|
546
|
+
}
|
|
547
|
+
function validateAnchors(content, schema, templateContent) {
|
|
548
|
+
const issues = [];
|
|
549
|
+
const nContent = normalizeWidth(content);
|
|
550
|
+
for (const section of schema.sections) {
|
|
551
|
+
if (!section.anchor) continue;
|
|
552
|
+
const refAnchor = normalizeWidth(section.anchor);
|
|
553
|
+
const refLine = templateContent.split("\n").find((l) => normalizeWidth(l).includes(refAnchor));
|
|
554
|
+
if (!refLine) continue;
|
|
555
|
+
const tokens = [
|
|
556
|
+
...new Set(
|
|
557
|
+
normalizeWidth(refLine).replace(/[#*`|(){]/g, " ").split(/\s+/).filter((t) => t !== "" && !t.includes("{"))
|
|
558
|
+
)
|
|
559
|
+
];
|
|
560
|
+
if (tokens.length === 0) continue;
|
|
561
|
+
let scope = nContent;
|
|
562
|
+
if (section.within) {
|
|
563
|
+
const startIdx = nContent.indexOf(normalizeWidth(section.within));
|
|
564
|
+
if (startIdx < 0) continue;
|
|
565
|
+
const endIdx = nContent.indexOf("\n## ", startIdx + 1);
|
|
566
|
+
scope = nContent.slice(startIdx, endIdx === -1 ? void 0 : endIdx);
|
|
567
|
+
}
|
|
568
|
+
const missTokens = tokens.filter((tok) => !scope.includes(tok));
|
|
569
|
+
if (missTokens.length > 0) {
|
|
570
|
+
issues.push({
|
|
571
|
+
code: "ANCHOR_MISS",
|
|
572
|
+
detail: `\u7F3A\u951A\u70B9(${section.id}): ${missTokens.join(" ")}`,
|
|
573
|
+
expected: section.anchor
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return issues;
|
|
578
|
+
}
|
|
579
|
+
function inferRoundHeading(schema) {
|
|
580
|
+
const round = schema.sections.find((s) => s.heading?.includes("<\u7B2CN\u8F6E>"));
|
|
581
|
+
return round?.heading ?? null;
|
|
582
|
+
}
|
|
583
|
+
function countRounds(content, roundHeading) {
|
|
584
|
+
const heading = normalizeWidth(roundHeading);
|
|
585
|
+
if (!heading.includes("<\u7B2CN\u8F6E>")) return 0;
|
|
586
|
+
const pattern = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace("<\u7B2CN\u8F6E>", "\u7B2C\\s*\\d+\\s*\u8F6E");
|
|
587
|
+
const re = new RegExp(`^${pattern}`, "gm");
|
|
588
|
+
return (normalizeWidth(content).match(re) ?? []).length;
|
|
589
|
+
}
|
|
590
|
+
function validateAgainstSchema(content, schema, opts = {}) {
|
|
591
|
+
const issues = [];
|
|
592
|
+
const proseOnly = stripFencedBlocks(content);
|
|
593
|
+
const contentNormalized = normalizeWidth(content);
|
|
594
|
+
const roundHeading = opts.roundHeading ?? inferRoundHeading(schema);
|
|
595
|
+
for (const section of schema.sections) {
|
|
596
|
+
if (!section.heading) continue;
|
|
597
|
+
const isRoundSection = roundHeading !== null && section.heading === roundHeading;
|
|
598
|
+
if (isRoundSection) {
|
|
599
|
+
const need = opts.expectRounds ?? 1;
|
|
600
|
+
const found = countRounds(content, roundHeading);
|
|
601
|
+
if (found < need) {
|
|
602
|
+
issues.push({
|
|
603
|
+
code: "ROUND_COUNT_SHORT",
|
|
604
|
+
detail: `\u8F6E\u6B21\u7AE0\u8282\u4E0D\u8DB3\uFF1A\u671F\u671B ${need} \u8F6E\uFF0C\u5B9E\u9645 ${found} \u8F6E`,
|
|
605
|
+
expected: roundHeading
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
for (const sub of section.subsections ?? []) {
|
|
609
|
+
if (countOccurrences(content, sub.heading) < need) {
|
|
610
|
+
issues.push({
|
|
611
|
+
code: "MISSING_SUBSECTION",
|
|
612
|
+
detail: `\u8F6E\u6B21\u5B50\u7AE0\u8282\u7F3A\u5931\u6216\u4E0D\u8DB3\uFF08\u9700\u8981 ${need} \u4EFD\uFF09\uFF1A${sub.heading}`,
|
|
613
|
+
expected: sub.heading
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (section.required && countOccurrences(content, section.heading) === 0) {
|
|
620
|
+
issues.push({ code: "MISSING_SECTION", detail: `\u7F3A\u5C11\u5FC5\u9700\u7AE0\u8282\uFF1A${section.heading}`, expected: section.heading });
|
|
621
|
+
}
|
|
622
|
+
for (const sub of section.subsections ?? []) {
|
|
623
|
+
const need = isRoundSection ? opts.expectRounds ?? 1 : 1;
|
|
624
|
+
if (countOccurrences(content, sub.heading) < need) {
|
|
625
|
+
issues.push({
|
|
626
|
+
code: "MISSING_SUBSECTION",
|
|
627
|
+
detail: `\u5B50\u7AE0\u8282\u7F3A\u5931\u6216\u4E0D\u8DB3\uFF08\u9700\u8981 ${need} \u4EFD\uFF09\uFF1A${sub.heading}`,
|
|
628
|
+
expected: sub.heading
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
for (const ph of schema.placeholders ?? []) {
|
|
634
|
+
if (!ph) continue;
|
|
635
|
+
const nPh = normalizeWidth(ph);
|
|
636
|
+
const idx = contentNormalized.indexOf(nPh);
|
|
637
|
+
if (idx === -1) continue;
|
|
638
|
+
const hit = nPh === ph ? ph : `${content.slice(idx, idx + nPh.length)}\uFF08\u5BBD\u5EA6\u7B49\u4EF7\u4E8E ${ph}\uFF09`;
|
|
639
|
+
issues.push({ code: "PLACEHOLDER_LEFT", detail: `\u5360\u4F4D\u7B26\u672A\u66FF\u6362\uFF1A${hit}`, expected: ph });
|
|
640
|
+
}
|
|
641
|
+
const struct = normalizeWidth(structText(content, schema.groupColumn));
|
|
642
|
+
for (const term of schema.forbidden_terms ?? []) {
|
|
643
|
+
if (term && struct.includes(normalizeWidth(term))) {
|
|
644
|
+
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 });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (opts.templateContent) {
|
|
648
|
+
issues.push(...validateAnchors(content, schema, opts.templateContent));
|
|
649
|
+
}
|
|
650
|
+
return issues;
|
|
651
|
+
}
|
|
652
|
+
|
|
503
653
|
// templates/core/governance/doc-format-guard.ts
|
|
504
654
|
function extractContent(input) {
|
|
505
655
|
const ti = input.tool_input;
|
|
@@ -595,90 +745,28 @@ var DocFormatGuard = class {
|
|
|
595
745
|
}
|
|
596
746
|
/** 章节/锚定/占位符/禁词校验,返回 struct 统计 */
|
|
597
747
|
runSchemaChecks(schema, templateName, content, isSearchReplace) {
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const
|
|
603
|
-
const
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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 };
|
|
748
|
+
const templatePath = join3(this.projectDir, this.magicDir, "templates", templateName);
|
|
749
|
+
const templateContent = existsSync3(templatePath) ? readFileSync2(templatePath, "utf-8") : "";
|
|
750
|
+
const all = validateAgainstSchema(content, schema, { templateContent });
|
|
751
|
+
const relevant = isSearchReplace ? all.filter((i) => i.code === "PLACEHOLDER_LEFT" || i.code === "FORBIDDEN_TERM") : all;
|
|
752
|
+
for (const i of relevant) this.issues.push(` ${i.detail}`);
|
|
753
|
+
const missed = relevant.length;
|
|
754
|
+
const declared = this.countDeclaredRules(schema, isSearchReplace);
|
|
755
|
+
return {
|
|
756
|
+
applied: Math.max(missed, declared),
|
|
757
|
+
missed,
|
|
758
|
+
anchorHit: !relevant.some((i) => i.code === "ANCHOR_MISS")
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
/** 已声明规则数(用于 struct_score 分母;口径与原内联实现一致:按规则条数计) */
|
|
762
|
+
countDeclaredRules(schema, isSearchReplace) {
|
|
763
|
+
const placeholders = (schema.placeholders ?? []).length;
|
|
764
|
+
const terms = (schema.forbidden_terms ?? []).length;
|
|
765
|
+
if (isSearchReplace) return placeholders + terms;
|
|
766
|
+
const anchors = schema.sections.filter((s) => s.anchor).length;
|
|
767
|
+
const required = schema.sections.filter((s) => s.required === true && s.heading).length;
|
|
768
|
+
const subs = schema.sections.flatMap((s) => s.subsections ?? []).filter((s) => s.heading).length;
|
|
769
|
+
return anchors + required + subs + placeholders + terms;
|
|
682
770
|
}
|
|
683
771
|
/** 算法化规则校验(真源: [doc.anti_cheat] + HITL 表非空 + handoff 冲突)
|
|
684
772
|
* protected: adapter 子类可 override(如 claude 版无算法规则段) */
|
|
@@ -278,7 +278,7 @@ function projectHash() {
|
|
|
278
278
|
var DEV_FLAG = `/tmp/add_dev_${projectHash()}`;
|
|
279
279
|
|
|
280
280
|
// templates/core/governance/post-tool-router.ts
|
|
281
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
281
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, appendFileSync as appendFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
282
282
|
import { join as join3, basename as basename2, dirname as dirname2 } from "node:path";
|
|
283
283
|
|
|
284
284
|
// templates/core/governance/notify.ts
|
|
@@ -343,6 +343,37 @@ var AuditBridge = class {
|
|
|
343
343
|
}
|
|
344
344
|
};
|
|
345
345
|
|
|
346
|
+
// templates/core/scripts/mcp-server/shared/memory/switches.ts
|
|
347
|
+
function recallMode(env = process.env) {
|
|
348
|
+
const v = (env.ADD_MEMORY_RECALL_MODE ?? "shadow").toLowerCase();
|
|
349
|
+
return v === "off" || v === "inject" ? v : "shadow";
|
|
350
|
+
}
|
|
351
|
+
function evidenceEnabled(env = process.env) {
|
|
352
|
+
return (env.ADD_MEMORY_EVIDENCE ?? "on").toLowerCase() !== "off";
|
|
353
|
+
}
|
|
354
|
+
var MEMORY_DIR_NAME = "memory";
|
|
355
|
+
var EVIDENCE_QUEUE_FILE = "evidence-queue.jsonl";
|
|
356
|
+
var L1_SNAPSHOT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
357
|
+
|
|
358
|
+
// templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts
|
|
359
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
360
|
+
function classifyEvidenceSource(filePath) {
|
|
361
|
+
const p = filePath.replace(/\\/g, "/");
|
|
362
|
+
if (/(^|\/)plans\/[^/]*-plan-v\d+\.md$/.test(p)) return "PLAN";
|
|
363
|
+
if (/(^|\/)plans\/[^/]*-add-route-v\d+\.md$/.test(p)) return "PLAN";
|
|
364
|
+
if (/(^|\/)specs\/.+\.md$/.test(p)) return "SPEC";
|
|
365
|
+
if (/handoff[^/]*\.md$/i.test(p)) return "HANDOFF";
|
|
366
|
+
if (/(^|\/)reviews\/.+\.md$/.test(p)) return "DEV_OPERATION";
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
function buildEvidenceEvent(filePath, excerpt, occurredAt = /* @__PURE__ */ new Date()) {
|
|
370
|
+
const sourceType = classifyEvidenceSource(filePath);
|
|
371
|
+
if (!sourceType) throw new Error(`\u975E\u767D\u540D\u5355\u8DEF\u5F84: ${filePath}`);
|
|
372
|
+
const sourceRef = filePath;
|
|
373
|
+
const dedupKey = createHash2("sha256").update(`${sourceType}|${sourceRef}|${excerpt}`, "utf8").digest("hex").slice(0, 32);
|
|
374
|
+
return { dedupKey, sourceType, sourceRef, excerpt: excerpt.slice(0, 500), occurredAt: occurredAt.toISOString() };
|
|
375
|
+
}
|
|
376
|
+
|
|
346
377
|
// templates/core/governance/post-tool-router.ts
|
|
347
378
|
function extractOutputText(raw) {
|
|
348
379
|
const m1 = raw.match(/"output"\s*:\s*"([^"]*)"/);
|
|
@@ -473,6 +504,7 @@ var PostToolRouter = class {
|
|
|
473
504
|
this.postDocSections(filePath);
|
|
474
505
|
this.emitLine(this.emitAuditReminder(filePath));
|
|
475
506
|
this.emitWriteEvent(filePath);
|
|
507
|
+
this.emitMemoryEvidence(filePath);
|
|
476
508
|
} else if (toolName2 === "Bash") {
|
|
477
509
|
this.emitLine(this.emitBashDone());
|
|
478
510
|
} else {
|
|
@@ -500,6 +532,32 @@ var PostToolRouter = class {
|
|
|
500
532
|
emitWriteEvent(filePath) {
|
|
501
533
|
this.auditBridge.emit(filePath);
|
|
502
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* §2g Memory 采证入队(Spec §10 PostToolUse 白名单,扩展点):
|
|
537
|
+
* 白名单路径(plans/specs/reviews/handoff)→ 追加 evidence-queue.jsonl(幂等 dedupKey),
|
|
538
|
+
* 由 consolidation job 异步落库。ADD_MEMORY_EVIDENCE=off 或 RECALL_MODE=off 时关闭。
|
|
539
|
+
* fail-open:任何异常不阻断文件写入主路径(Plan §9.3)。
|
|
540
|
+
*/
|
|
541
|
+
emitMemoryEvidence(filePath) {
|
|
542
|
+
try {
|
|
543
|
+
if (!evidenceEnabled() || recallMode() === "off") return;
|
|
544
|
+
if (!classifyEvidenceSource(filePath)) return;
|
|
545
|
+
if (!this.magicDir) return;
|
|
546
|
+
let excerpt = "";
|
|
547
|
+
try {
|
|
548
|
+
excerpt = readFileSync2(filePath, "utf-8").slice(0, 500);
|
|
549
|
+
} catch {
|
|
550
|
+
excerpt = basename2(filePath);
|
|
551
|
+
}
|
|
552
|
+
const ev = buildEvidenceEvent(filePath, excerpt);
|
|
553
|
+
const dir = join3(this.projectDir, this.magicDir, MEMORY_DIR_NAME);
|
|
554
|
+
mkdirSync2(dir, { recursive: true });
|
|
555
|
+
appendFileSync2(join3(dir, EVIDENCE_QUEUE_FILE), JSON.stringify(ev) + "\n", "utf-8");
|
|
556
|
+
this.emitLine(`[ADD PostToolUse] \u{1F9E0} \u8BB0\u5FC6\u91C7\u8BC1\u5165\u961F: ${filePath}\uFF08consolidation \u65F6\u843D\u5E93\uFF09
|
|
557
|
+
`);
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
560
|
+
}
|
|
503
561
|
/** §3 Bash 增强(core: lint/tsc;codex 子类: lint/typecheck/test) */
|
|
504
562
|
emitBashDone() {
|
|
505
563
|
return "[ADD PostToolUse] \u547D\u4EE4\u6267\u884C\u5B8C\u6210\u3002\u5982\u6709 lint/tsc \u9519\u8BEF\u8BF7\u4FEE\u590D\u3002\n";
|
|
@@ -447,7 +447,45 @@ function writeHookEvent(hook, decision, cmd, reason, plan = "unknown", status =
|
|
|
447
447
|
appendFileSync(file, line);
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
+
// templates/core/scripts/mcp-server/shared/memory/switches.ts
|
|
451
|
+
function recallMode(env = process.env) {
|
|
452
|
+
const v = (env.ADD_MEMORY_RECALL_MODE ?? "shadow").toLowerCase();
|
|
453
|
+
return v === "off" || v === "inject" ? v : "shadow";
|
|
454
|
+
}
|
|
455
|
+
var L1_SNAPSHOT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
456
|
+
|
|
457
|
+
// templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts
|
|
458
|
+
var STAGE_LABEL = {
|
|
459
|
+
"plan-start": "Plan \u8D77\u8349",
|
|
460
|
+
"spec-start": "Spec \u8D77\u8349",
|
|
461
|
+
dps: "DPS \u95E8\u7981",
|
|
462
|
+
rahs: "RAHS \u95E8\u7981",
|
|
463
|
+
handoff: "Handoff \u4EA4\u63A5"
|
|
464
|
+
};
|
|
465
|
+
var STAGE_PATTERNS = [
|
|
466
|
+
{ stage: "handoff", re: /handoff|交接(手册|文档|说明)?|交接给/i },
|
|
467
|
+
{ stage: "rahs", re: /rahs|注意力漂移|执行健康度/i },
|
|
468
|
+
{ stage: "dps", re: /dps|文档质量闸门|质量闸门|门禁(评分|检查)?/i },
|
|
469
|
+
{ stage: "spec-start", re: /(生成|写|新建|起草|补)\s*(spec|规格|三元组)|specs?\s*三元组|WHEN-?THEN/i },
|
|
470
|
+
{ stage: "plan-start", re: /(生成|写|新建|起草|补)\s*(plan|计划|方案)|plan\s*阶段|规划阶段/i }
|
|
471
|
+
];
|
|
472
|
+
function detectRecallStage(prompt) {
|
|
473
|
+
if (!prompt) return null;
|
|
474
|
+
for (const { stage, re } of STAGE_PATTERNS) {
|
|
475
|
+
if (re.test(prompt)) return stage;
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
function buildStageRecallHint(stage, mode) {
|
|
480
|
+
if (mode === "off") return null;
|
|
481
|
+
const label = STAGE_LABEL[stage];
|
|
482
|
+
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";
|
|
483
|
+
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}
|
|
484
|
+
`;
|
|
485
|
+
}
|
|
486
|
+
|
|
450
487
|
// templates/core/governance/prompt-router.ts
|
|
488
|
+
var MEMORY_RECALL_HINT_RE = /之前|上次|还记得|记得吗|为什么当时|类似问题|历史决策|以前的方案|曾经怎么/i;
|
|
451
489
|
var PromptRouter = class {
|
|
452
490
|
magicDir;
|
|
453
491
|
constructor(magicDir) {
|
|
@@ -510,6 +548,38 @@ var PromptRouter = class {
|
|
|
510
548
|
/** 前置注入(core: 无;qoder: 无条件 "ADD workflow active" JSON) */
|
|
511
549
|
preamble(_input) {
|
|
512
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* 显式记忆回忆提示(Spec §10 / Plan §9.1 补充入口):
|
|
553
|
+
* 命中回忆词且 ADD_MEMORY_RECALL_MODE != off → 提示调用 recall_memory。
|
|
554
|
+
* 不做自动召回(同步 Hook ≤200ms 无 DB);shadow 模式下提示语明示"仅显式调用"。
|
|
555
|
+
*/
|
|
556
|
+
maybeMemoryHint(prompt) {
|
|
557
|
+
try {
|
|
558
|
+
if (recallMode() === "off") return;
|
|
559
|
+
if (!MEMORY_RECALL_HINT_RE.test(prompt)) return;
|
|
560
|
+
const mode = recallMode();
|
|
561
|
+
const suffix = mode === "shadow" ? "\uFF08\u5F53\u524D shadow \u6A21\u5F0F\uFF1A\u53EC\u56DE\u5C06\u843D\u5BA1\u8BA1\uFF0CHook \u4E0D\u81EA\u52A8\u6CE8\u5165\uFF09" : "";
|
|
562
|
+
process.stdout.write(
|
|
563
|
+
`[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
|
|
564
|
+
`
|
|
565
|
+
);
|
|
566
|
+
} catch {
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* 阶段确定性召回提示(Spec §2 §DeterministicRecall,Plan §3.1 方案 B1 的 Hook 侧):
|
|
571
|
+
* 命中 ADD 阶段词 → 提示到工具侧做确定性召回(plan-start/spec-start/dps/rahs/handoff)。
|
|
572
|
+
* Hook 同步路径无 DB:只输出提示文本,不召回、不写审计;任何异常 fail-open。
|
|
573
|
+
*/
|
|
574
|
+
maybeStageHint(prompt) {
|
|
575
|
+
try {
|
|
576
|
+
const stage = detectRecallStage(prompt);
|
|
577
|
+
if (!stage) return;
|
|
578
|
+
const hint = buildStageRecallHint(stage, recallMode());
|
|
579
|
+
if (hint) process.stdout.write(hint);
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
}
|
|
513
583
|
/** Layer 3 输出形态(core: 纯文本逐行;qoder: hookSpecificOutput JSON 包) */
|
|
514
584
|
layer3Json() {
|
|
515
585
|
return false;
|
|
@@ -541,6 +611,8 @@ var PromptRouter = class {
|
|
|
541
611
|
for (const m of matched) process.stdout.write(m + "\n");
|
|
542
612
|
return 0;
|
|
543
613
|
}
|
|
614
|
+
this.maybeMemoryHint(prompt);
|
|
615
|
+
this.maybeStageHint(prompt);
|
|
544
616
|
const devKw = loadDevKeywords();
|
|
545
617
|
if (devKw.length === 0) return 0;
|
|
546
618
|
if (!this.devKwMatched(prompt, devKw)) return 0;
|