@zhuan-ai/zhuanspec 2.15.7 → 2.16.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 (39) hide show
  1. package/dist/cli/hooks.d.ts +13 -1
  2. package/dist/cli/hooks.js +75 -2
  3. package/dist/cli/index.js +94 -4
  4. package/dist/commands/accuracy.js +5 -1
  5. package/dist/commands/knowledge.d.ts +21 -0
  6. package/dist/commands/knowledge.js +80 -0
  7. package/dist/commands/progress.d.ts +66 -0
  8. package/dist/commands/progress.js +306 -1
  9. package/dist/core/archive.js +28 -0
  10. package/dist/core/configurators/codex.d.ts +55 -1
  11. package/dist/core/configurators/codex.js +234 -2
  12. package/dist/core/hooks/collect-knowledge.js +66 -0
  13. package/dist/core/hooks/deviation-check.d.ts +26 -1
  14. package/dist/core/hooks/deviation-check.js +32 -184
  15. package/dist/core/hooks/knowledge-index.d.ts +84 -0
  16. package/dist/core/hooks/knowledge-index.js +270 -0
  17. package/dist/core/hooks/record-progress.d.ts +63 -1
  18. package/dist/core/hooks/record-progress.js +208 -54
  19. package/dist/core/hooks/summarize.js +90 -2
  20. package/dist/core/hooks/user-input-hook.d.ts +38 -2
  21. package/dist/core/hooks/user-input-hook.js +346 -21
  22. package/dist/core/init.d.ts +11 -0
  23. package/dist/core/init.js +43 -4
  24. package/dist/core/metrics/code-accuracy.d.ts +152 -3
  25. package/dist/core/metrics/code-accuracy.js +323 -19
  26. package/dist/core/templates/agents-root-stub.d.ts +1 -1
  27. package/dist/core/templates/agents-root-stub.js +1 -0
  28. package/dist/core/templates/agents-template.d.ts +1 -1
  29. package/dist/core/templates/agents-template.js +21 -4
  30. package/dist/core/templates/codex-hooks-template.d.ts +37 -0
  31. package/dist/core/templates/codex-hooks-template.js +33 -2
  32. package/dist/core/templates/slash-command-templates.js +144 -28
  33. package/dist/core/templates/tasks-template.js +66 -14
  34. package/dist/core/update.js +17 -0
  35. package/dist/core/validation/strict-rules.d.ts +28 -0
  36. package/dist/core/validation/strict-rules.js +284 -0
  37. package/dist/utils/hook-merge.d.ts +6 -0
  38. package/dist/utils/hook-merge.js +33 -2
  39. package/package.json +1 -1
@@ -13,12 +13,22 @@
13
13
  *
14
14
  * ~/.codex/ (or $CODEX_HOME)
15
15
  * config.toml - Per-project [projects."…"] trusted_level = "trusted"
16
- * block (wrapped in TRUST markers keyed by absolute path)
16
+ * block (wrapped in TRUST markers keyed by absolute path).
17
+ * Also receives [hooks.state."<abs>:<event>:<i>:<j>"]
18
+ * entries grafted from other already-trusted projects,
19
+ * wrapped in HOOK_STATE markers keyed by config.toml path.
17
20
  *
18
21
  * Codex does **not** load project-scoped hooks/subagents unless the project
19
22
  * is explicitly marked trusted in the global config (see openai/codex#14601),
20
23
  * so we opportunistically write that trust entry during `zhuanspec init`.
21
24
  *
25
+ * Codex CLI >= 0.129 additionally requires each hook's content hash to be
26
+ * pre-approved (openai/codex#20321) or the user sees "N hooks need review".
27
+ * The `trusted_hash` in `[hooks.state.<key>]` depends only on normalised hook
28
+ * *content* (event + matcher + command), not on the key path, so we clone a
29
+ * previously approved hash to the current project's key to skip the manual
30
+ * review prompt in fresh workspaces.
31
+ *
22
32
  * Idempotency:
23
33
  * - Hooks block is regenerated inside ZHUANSPEC markers on every run.
24
34
  * - Trust entries are keyed by absolute path; repeated runs no-op.
@@ -28,7 +38,7 @@ import path from 'path';
28
38
  import os from 'os';
29
39
  import { promises as fs } from 'fs';
30
40
  import { FileSystemUtils } from '../../utils/file-system.js';
31
- import { getCodexHooksTomlBlock } from '../templates/codex-hooks-template.js';
41
+ import { getCodexHooksTomlBlock, getCodexDefaultHookSlots } from '../templates/codex-hooks-template.js';
32
42
  import { getCodexAgentTomls, } from '../templates/codex-agents-template.js';
33
43
  import { getCodexSkillFiles } from '../templates/codex-skills-template.js';
34
44
  import { CodexSlashCommandConfigurator } from './slash/codex.js';
@@ -51,6 +61,18 @@ function buildTrustMarkers(projectPath) {
51
61
  end: `# <!-- ZHUANSPEC:TRUST:END path=${projectPath} -->`,
52
62
  };
53
63
  }
64
+ /**
65
+ * Build the per-path HOOK_STATE marker pair for the global config.toml.
66
+ * Keyed by the **project config.toml absolute path** (not the project dir)
67
+ * so it aligns with the prefix of each `[hooks.state."<config-path>:..."]`
68
+ * key that Codex records.
69
+ */
70
+ function buildHookStateMarkers(projectConfigTomlPath) {
71
+ return {
72
+ start: `# <!-- ZHUANSPEC:HOOK_STATE:START path=${projectConfigTomlPath} -->`,
73
+ end: `# <!-- ZHUANSPEC:HOOK_STATE:END path=${projectConfigTomlPath} -->`,
74
+ };
75
+ }
54
76
  export class CodexConfigurator {
55
77
  name = 'Codex';
56
78
  /** Codex reads AGENTS.md for project instructions (no dedicated CODEX.md). */
@@ -80,6 +102,7 @@ export class CodexConfigurator {
80
102
  const skillsWritten = await this.mirrorSkills(projectPath);
81
103
  const configTomlPath = await this.writeProjectConfigToml(codexDir, agentFiles);
82
104
  const { globalConfigPath, trustEntryAdded } = await this.ensureGlobalTrustEntry(projectPath);
105
+ const hookTrust = await this.ensureHookTrustSeed(globalConfigPath, configTomlPath);
83
106
  const conflictingGlobalHooksJson = await this.detectGlobalHooksJsonConflict();
84
107
  // Codex CLI >= 0.117 no longer scans `~/.codex/prompts/`; delete any
85
108
  // ZhuanSpec-managed prompt left behind by pre-2.12.5 installs so the
@@ -94,6 +117,7 @@ export class CodexConfigurator {
94
117
  trustEntryAdded,
95
118
  conflictingGlobalHooksJson,
96
119
  legacyPromptsCleaned,
120
+ hookTrust,
97
121
  };
98
122
  }
99
123
  /**
@@ -292,5 +316,213 @@ export class CodexConfigurator {
292
316
  const block = this.buildTrustBlock(projectPath);
293
317
  return `${before}${block}${after}`;
294
318
  }
319
+ /**
320
+ * Graft `[hooks.state."<key>"] trusted_hash = "sha256:..."` entries for the
321
+ * current project from any other project that the user has already approved
322
+ * via Codex's `/hooks` TUI. The grafted block is wrapped in HOOK_STATE
323
+ * markers keyed by `configTomlPath` so it stays idempotent across runs and
324
+ * coexists with HOOK_STATE blocks for sibling projects in the same file.
325
+ *
326
+ * Strategy:
327
+ * - Strip our own existing block first to avoid self-referencing seeds.
328
+ * - Scan remaining `[hooks.state."<path>:<event>:<grp>:<hdl>"]` tables;
329
+ * index their `trusted_hash` by the `<event>:<grp>:<hdl>` suffix so the
330
+ * same logical hook in any other project can serve as a seed.
331
+ * - For each ZhuanSpec default slot:
332
+ * * If a native (unmanaged) entry already owns the same key in this
333
+ * project, skip to avoid TOML duplicate-key errors.
334
+ * * Else if a seed exists, emit a managed entry pointing the user's
335
+ * project key at the known-good hash.
336
+ * * Else record the slot in `skippedNoSeed` so callers can surface a
337
+ * "please approve once in Codex" hint.
338
+ *
339
+ * Failures are non-fatal: any unexpected error is swallowed and reported
340
+ * via an empty result with `seedAvailable=false`, since hook trust is a
341
+ * UX optimisation — missing it just means the user sees the regular
342
+ * Codex review prompt.
343
+ */
344
+ async ensureHookTrustSeed(globalConfigPath, configTomlPath) {
345
+ const slots = getCodexDefaultHookSlots();
346
+ const empty = {
347
+ written: [],
348
+ skippedNoSeed: slots.map(slotSuffix),
349
+ skippedNative: [],
350
+ seedAvailable: false,
351
+ };
352
+ try {
353
+ let existing = '';
354
+ if (await FileSystemUtils.fileExists(globalConfigPath)) {
355
+ existing = await FileSystemUtils.readFile(globalConfigPath);
356
+ }
357
+ // 1. Strip our own block so we don't seed from ourselves and so the
358
+ // "native conflict" check only sees user/legacy entries.
359
+ const stripped = stripHookStateBlock(existing, configTomlPath);
360
+ // 2. Build seed map (suffix → first hash seen anywhere except this project).
361
+ const allEntries = parseHookStateEntries(stripped);
362
+ const seedBySuffix = new Map();
363
+ const nativeOwnedSuffixes = new Set();
364
+ for (const entry of allEntries) {
365
+ const split = splitHookStateKey(entry.key);
366
+ if (!split)
367
+ continue;
368
+ if (split.pathPart === configTomlPath) {
369
+ // Native (unmanaged) entry already exists for this project's slot.
370
+ nativeOwnedSuffixes.add(split.suffix);
371
+ continue;
372
+ }
373
+ if (!seedBySuffix.has(split.suffix)) {
374
+ seedBySuffix.set(split.suffix, entry.hash);
375
+ }
376
+ }
377
+ const seedAvailable = seedBySuffix.size > 0;
378
+ // 3. Plan per-slot writes.
379
+ const written = [];
380
+ const skippedNoSeed = [];
381
+ const skippedNative = [];
382
+ const blockEntries = [];
383
+ for (const slot of slots) {
384
+ const suffix = slotSuffix(slot);
385
+ if (nativeOwnedSuffixes.has(suffix)) {
386
+ skippedNative.push(suffix);
387
+ continue;
388
+ }
389
+ const seed = seedBySuffix.get(suffix);
390
+ if (!seed) {
391
+ skippedNoSeed.push(suffix);
392
+ continue;
393
+ }
394
+ const fullKey = `${configTomlPath}:${suffix}`;
395
+ blockEntries.push({ key: fullKey, hash: seed });
396
+ written.push(suffix);
397
+ }
398
+ // 4. Persist. When nothing to write, ensure any stale managed block is
399
+ // removed (e.g. user revoked all hooks elsewhere); otherwise replace.
400
+ const finalContent = blockEntries.length === 0
401
+ ? stripped
402
+ : insertHookStateBlock(stripped, configTomlPath, buildHookStateBlockBody(configTomlPath, blockEntries));
403
+ if (finalContent !== existing) {
404
+ await FileSystemUtils.writeFile(globalConfigPath, finalContent);
405
+ }
406
+ return { written, skippedNoSeed, skippedNative, seedAvailable };
407
+ }
408
+ catch {
409
+ // Non-fatal: hook trust seeding is a UX optimisation.
410
+ return empty;
411
+ }
412
+ }
413
+ }
414
+ // ---------------------------------------------------------------------------
415
+ // Hook trust seed helpers (module-private).
416
+ //
417
+ // These intentionally avoid pulling in a TOML parser dependency — we only
418
+ // need to read/write a tightly scoped subset of `~/.codex/config.toml`
419
+ // (`[hooks.state."..."]` tables with a single `trusted_hash` value) and
420
+ // must round-trip the rest of the user's file byte-for-byte.
421
+ // ---------------------------------------------------------------------------
422
+ /** `<event>:<group>:<handler>` suffix used both for seed lookup and key building. */
423
+ function slotSuffix(slot) {
424
+ return `${slot.eventSnake}:${slot.groupIndex}:${slot.handlerIndex}`;
425
+ }
426
+ /**
427
+ * Parse `[hooks.state."<key>"] trusted_hash = "..."` entries from a TOML
428
+ * blob using a line walker (no full TOML grammar needed).
429
+ */
430
+ function parseHookStateEntries(content) {
431
+ const lines = content.split('\n');
432
+ const entries = [];
433
+ const headerRe = /^\[hooks\.state\."((?:[^"\\]|\\.)+)"\]\s*$/;
434
+ const hashRe = /^trusted_hash\s*=\s*"([^"]+)"\s*$/;
435
+ for (let i = 0; i < lines.length; i++) {
436
+ const m = lines[i].match(headerRe);
437
+ if (!m)
438
+ continue;
439
+ const rawKey = m[1];
440
+ // Walk forward until the next table header or EOF, capturing trusted_hash.
441
+ for (let j = i + 1; j < lines.length; j++) {
442
+ const trimmed = lines[j].trim();
443
+ if (trimmed === '' || trimmed.startsWith('#'))
444
+ continue;
445
+ if (trimmed.startsWith('['))
446
+ break;
447
+ const h = trimmed.match(hashRe);
448
+ if (h) {
449
+ entries.push({ key: unescapeTomlBasic(rawKey), hash: h[1] });
450
+ break;
451
+ }
452
+ }
453
+ }
454
+ return entries;
455
+ }
456
+ /**
457
+ * Split a hook-state key (`<config-toml-path>:<event>:<grp>:<hdl>`) into its
458
+ * path prefix and 3-field suffix. Handles paths containing `:` (Windows
459
+ * drives, etc.) by splitting on the **last** three colons.
460
+ */
461
+ function splitHookStateKey(key) {
462
+ const parts = key.split(':');
463
+ if (parts.length < 4)
464
+ return null;
465
+ const suffix = parts.slice(-3).join(':');
466
+ const pathPart = parts.slice(0, parts.length - 3).join(':');
467
+ if (!pathPart)
468
+ return null;
469
+ return { pathPart, suffix };
470
+ }
471
+ /** Reverse TOML basic-string escapes for `\\\\` and `\\"` (sufficient for paths). */
472
+ function unescapeTomlBasic(raw) {
473
+ return raw.replace(/\\\\/g, '\\').replace(/\\"/g, '"');
474
+ }
475
+ /** Forward TOML basic-string escape — mirrors `escapeBasic` used elsewhere. */
476
+ function escapeTomlBasic(raw) {
477
+ return raw.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
478
+ }
479
+ /** Build the body (between markers) of the HOOK_STATE managed block. */
480
+ function buildHookStateBlockBody(configTomlPath, entries) {
481
+ const { start, end } = buildHookStateMarkers(configTomlPath);
482
+ const tables = entries.map((e) => [`[hooks.state."${escapeTomlBasic(e.key)}"]`, `trusted_hash = "${e.hash}"`].join('\n'));
483
+ const header = [
484
+ '# ZhuanSpec hook trust seeds — grafted from previously approved projects.',
485
+ '# Managed block; regenerated by `zhuanspec init/update`. Do not edit by hand.',
486
+ ].join('\n');
487
+ return [start, header, '', tables.join('\n\n'), end].join('\n');
488
+ }
489
+ /** Remove our marker-wrapped HOOK_STATE block for `configTomlPath` from `content`. */
490
+ function stripHookStateBlock(content, configTomlPath) {
491
+ const { start, end } = buildHookStateMarkers(configTomlPath);
492
+ const startIdx = content.indexOf(start);
493
+ if (startIdx === -1)
494
+ return content;
495
+ const endIdx = content.indexOf(end, startIdx + start.length);
496
+ if (endIdx === -1)
497
+ return content;
498
+ const blockStart = startIdx;
499
+ let blockEnd = endIdx + end.length;
500
+ // Consume the trailing newline written together with the block so we
501
+ // don't leave an orphan blank line behind. Leave any leading newline
502
+ // intact so `insertHookStateBlock` produces a stable, idempotent layout.
503
+ if (content[blockEnd] === '\n')
504
+ blockEnd += 1;
505
+ return content.substring(0, blockStart) + content.substring(blockEnd);
506
+ }
507
+ /**
508
+ * Insert (or replace) the HOOK_STATE managed block for `configTomlPath` in
509
+ * `content`. When a previous block exists, it's swapped in-place to keep the
510
+ * file order stable. Otherwise the block is appended to EOF; we adjust the
511
+ * separator so a follow-up `strip + insert` round-trip produces byte-for-byte
512
+ * the same result (idempotency).
513
+ */
514
+ function insertHookStateBlock(content, configTomlPath, block) {
515
+ const { start, end } = buildHookStateMarkers(configTomlPath);
516
+ const startIdx = content.indexOf(start);
517
+ if (startIdx !== -1) {
518
+ const endIdx = content.indexOf(end, startIdx + start.length);
519
+ if (endIdx !== -1) {
520
+ const before = content.substring(0, startIdx);
521
+ const after = content.substring(endIdx + end.length);
522
+ return `${before}${block}${after}`;
523
+ }
524
+ }
525
+ const separator = content.length === 0 || content.endsWith('\n') ? '' : '\n';
526
+ return `${content}${separator}${block}\n`;
295
527
  }
296
528
  //# sourceMappingURL=codex.js.map
@@ -8,8 +8,56 @@
8
8
  * Design principle: no keyword matching, no auto-save — the LLM decides.
9
9
  */
10
10
  import path from 'path';
11
+ import { promises as fs } from 'fs';
11
12
  import { FileSystemUtils } from '../../utils/file-system.js';
12
13
  import { detectHookHost, sanitizeCodexEnvelope } from '../../utils/hook-host.js';
14
+ /** Level 2 兼容用的关键词(与 deviation-check 中 USER_CORRECTION_KEYWORDS/REQUIREMENT_CHANGE_KEYWORDS 保持同步) */
15
+ const FALLBACK_CORRECTION_KEYWORDS = ['不对', '错了', '不是这样', '改成', '重做', '错误', '有问题', '有 bug'];
16
+ const FALLBACK_REQUIREMENT_KEYWORDS = ['需求改', '需求变更', '需求调整', '新增需求', '补充需求'];
17
+ async function readCorrectionStats(cwd, changeId) {
18
+ if (!changeId)
19
+ return null;
20
+ const userInputsPath = path.join(cwd, 'zhuanspec', 'changes', changeId, 'metrics', 'user_inputs.json');
21
+ try {
22
+ const raw = await fs.readFile(userInputsPath, 'utf8');
23
+ const parsed = JSON.parse(raw);
24
+ const inputs = Array.isArray(parsed.inputs) ? parsed.inputs : [];
25
+ if (inputs.length === 0)
26
+ return { level: 3, hardCorrectionCount: 0, requirementChangeCount: 0 };
27
+ const hasSignal = inputs.some((i) => i && typeof i === 'object' && 'correctionSignal' in i);
28
+ if (hasSignal) {
29
+ let hard = 0;
30
+ let req = 0;
31
+ for (const entry of inputs) {
32
+ const sig = entry['correctionSignal'];
33
+ const postBaseline = entry['postBaseline'] === true;
34
+ if (!sig || sig.kind !== 'hard' || !postBaseline)
35
+ continue;
36
+ if (sig.isRequirementChange)
37
+ req += 1;
38
+ else
39
+ hard += 1;
40
+ }
41
+ return { level: 1, hardCorrectionCount: hard, requirementChangeCount: req };
42
+ }
43
+ // Level 2: 按 summary 关键词推断
44
+ let hard2 = 0;
45
+ let req2 = 0;
46
+ for (const entry of inputs) {
47
+ const summary = String(entry['summary'] || '').toLowerCase();
48
+ if (!summary)
49
+ continue;
50
+ if (FALLBACK_REQUIREMENT_KEYWORDS.some((k) => summary.includes(k.toLowerCase())))
51
+ req2 += 1;
52
+ else if (FALLBACK_CORRECTION_KEYWORDS.some((k) => summary.includes(k.toLowerCase())))
53
+ hard2 += 1;
54
+ }
55
+ return { level: 2, hardCorrectionCount: hard2, requirementChangeCount: req2 };
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
13
61
  export async function collectKnowledgeHook(options) {
14
62
  let stdinData = {};
15
63
  try {
@@ -61,6 +109,8 @@ export async function runCollectKnowledge(filePath) {
61
109
  const cwd = process.cwd();
62
110
  const knowledgeDir = path.join(cwd, 'zhuanspec', 'knowledge');
63
111
  const hasKnowledge = await FileSystemUtils.directoryExists(knowledgeDir);
112
+ // 读纠偏统计(三级 fallback,读失败不阻断主流程)
113
+ const stats = await readCorrectionStats(cwd, changeId);
64
114
  const knowledgeRef = hasKnowledge
65
115
  ? '参考 `zhuanspec/knowledge/` 下已有文件的格式(统一 Front Matter + 三类分目录)'
66
116
  : '将写入 `zhuanspec/knowledge/troubleshooting/`、`best-practices/` 或 `implicit-conventions/` 对应目录(仅此三类)';
@@ -84,9 +134,25 @@ export async function runCollectKnowledge(filePath) {
84
134
  '⚠️ **去重要求**:保存前先读取 `zhuanspec/knowledge/` 下已有文件,',
85
135
  '若已有同类记录,跳过或追加到已有文件,不要重复新建。',
86
136
  ];
137
+ // 纠偏累计提示(基于 user_inputs.json)
138
+ //
139
+ // 阈值选择的启发式意图:
140
+ // - hardCorrectionCount >= 2:一次纠偏不提示避免打扰,两次及以上视为"稳定坑",优先沉淀
141
+ // - requirementChangeCount >= 1:需求变更对设计决策影响通常较大,出现一次即提示
142
+ // 本处仅做"是否优先沉淀"的强度提示,具体三类目录分类一律交给 @skill:zhuanspec:knowledge 的分类决策树,不在本 hook 做分类预判。
143
+ const correctionHints = [];
144
+ if (stats && stats.level <= 2) {
145
+ if (stats.hardCorrectionCount >= 2) {
146
+ correctionHints.push(`⚠️ 本次 apply 已累计 ${stats.hardCorrectionCount} 次纠偏(数据源 Level ${stats.level}),若本次改动是对其中某条纠偏的解决,**请优先沉淀**(具体分类交由 @skill:zhuanspec:knowledge 决定)。`);
147
+ }
148
+ if (stats.requirementChangeCount >= 1) {
149
+ correctionHints.push(`⚠️ 本次 apply 涉及 ${stats.requirementChangeCount} 次需求变更,若本次改动属于其中的设计决策负载,**建议优先沉淀为项目知识**(具体分类交由 @skill:zhuanspec:knowledge 决定)。`);
150
+ }
151
+ }
87
152
  const additionalContext = [
88
153
  `💡 **知识沉淀提示**(针对刚才对 \`${filePath || '该文件'}\` 的改动)`,
89
154
  '',
155
+ ...(correctionHints.length > 0 ? [...correctionHints, ''] : []),
90
156
  ...phaseGuide,
91
157
  '',
92
158
  '询问方式:',
@@ -15,7 +15,7 @@ interface DeviationCheckOptions {
15
15
  json?: boolean;
16
16
  file?: string;
17
17
  tool?: string;
18
- trigger?: 'pre-tool' | 'post-prompt' | 'resume';
18
+ trigger?: 'pre-tool' | 'resume';
19
19
  prompt?: string;
20
20
  }
21
21
  declare const PITFALL_KEYWORDS: string[];
@@ -24,6 +24,14 @@ declare const IMPLICIT_CONVENTION_KEYWORDS: string[];
24
24
  declare const USER_CORRECTION_KEYWORDS: string[];
25
25
  declare const REQUIREMENT_CHANGE_KEYWORDS: string[];
26
26
  export declare function deviationCheckHook(options: DeviationCheckOptions): Promise<void>;
27
+ /**
28
+ * 识别 Codex CLI/App 由 Ambient Suggestions 等后台特性发出的系统内部 prompt。
29
+ * 这类 prompt 并非用户真实输入,不应触发纠偏/需求变更/范围检查。
30
+ *
31
+ * 参考:openai/codex#18541 —— Ambient Suggestions 会调用 UserPromptSubmit 钩子
32
+ * 但不调用对应的 Stop 钩子,且目前无法在项目级关闭。
33
+ */
34
+ export declare function isCodexInternalPrompt(prompt: string): boolean;
27
35
  /**
28
36
  * 从提案中提取类名 / 标识符等符号,用于 L2 符号级匹配。
29
37
  * 识别规则:
@@ -33,5 +41,22 @@ export declare function deviationCheckHook(options: DeviationCheckOptions): Prom
33
41
  * - 排除全小写且无下划线的短词(避免与英文动词擞车)
34
42
  */
35
43
  declare function parseEstimatedSymbols(proposalPath: string): Promise<string[]>;
44
+ export declare function extractProposalSummary(content: string, maxLength: number): string;
45
+ /**
46
+ * 构造纠偏四选一上下文(注入给主 agent)
47
+ * 要求主 agent:
48
+ * 1. 停止任何 Edit/Write
49
+ * 2. 用 askUserQuestion 让用户四选一
50
+ * 3. 按选择走对应路径
51
+ * 4. 路径完成后运行 resolve-correction 清理标记
52
+ */
53
+ export declare function buildCorrectionOptionsContext(proposalSummary: string, changeId: string): string;
54
+ /**
55
+ * 构造需求变更(T2)硬拦截的上下文。
56
+ * 与 buildCorrectionOptionsContext 的区别:
57
+ * - 仅提供 A / D 两个选项(需求变更必然要先改 Spec,无 Bug 豁免路径)
58
+ * - 文案强调“新增/变更功能范围”而非“AI 做错”
59
+ */
60
+ export declare function buildRequirementChangeContext(proposalSummary: string, changeId: string): string;
36
61
  export { PITFALL_KEYWORDS, USER_CORRECTION_KEYWORDS, BEST_PRACTICE_KEYWORDS, IMPLICIT_CONVENTION_KEYWORDS, REQUIREMENT_CHANGE_KEYWORDS, parseEstimatedSymbols };
37
62
  //# sourceMappingURL=deviation-check.d.ts.map