@zhuan-ai/zhuanspec 2.16.2 → 2.16.4

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.
@@ -38,6 +38,66 @@ export declare function appendAccuracyDebugLog(changeDir: string, entry: Accurac
38
38
  * 判断是否为代码文件(需纳入准确率统计)
39
39
  */
40
40
  export declare function isCodeFile(filePath: string): boolean;
41
+ /**
42
+ * Review 阶段自动修复 Skill 列表。
43
+ * 这些 Skill 在 review 阶段触发的代码编辑属于自动修复,
44
+ * 不应计入用户纠正(不影响 apply 阶段准确率)。
45
+ *
46
+ * 匹配规则:skill name 包含列表中任一子串即视为 autofix skill。
47
+ */
48
+ export declare const REVIEW_AUTOFIX_SKILLS: string[];
49
+ /**
50
+ * 纯确认/招呼类 prompt,不具备纠偏意图,不应触发用户纠正归因。
51
+ * 与 src/core/corrections/select-candidates.ts 中的 TRIVIAL_ACK_SET 保持同步。
52
+ */
53
+ export declare const TRIVIAL_ACK_WORDS: ReadonlySet<string>;
54
+ /**
55
+ * 判定 prompt 是否为琐碎确认/招呼(如"继续"、"好的"、"ok"等)。
56
+ *
57
+ * 规则:
58
+ * 1. 空字符串视为琐碎;
59
+ * 2. 规范化后命中 TRIVIAL_ACK_WORDS 视为琐碎;
60
+ * 3. 规范化后字符长度 ≤ 2 视为琐碎(如"好。"、"!!")。
61
+ *
62
+ * 注:斜杠/感叹号开头的命令词(/zhuanspec:、!ls 等)已由 user-input-hook
63
+ * 在写入 correctionContext 前过滤,无需在此重复处理。
64
+ */
65
+ export declare function isTrivialAckPrompt(text: string | null | undefined): boolean;
66
+ /**
67
+ * 判断 correctionContext 是否代表一次有意义的用户纠正(非琐碎确认)。
68
+ *
69
+ * 接受的 source:
70
+ * - correction-keyword:命中显式纠偏关键词,始终视为用户纠正;
71
+ * - post-baseline-prompt:baseline 后任何用户提示,但 promptSnippet
72
+ * 必须不是琐碎确认词(如"继续"、"好的")。
73
+ *
74
+ * 该函数用于 review 阶段判定 "用户提示词触发的纠正",使其覆盖范围与 apply 阶段一致。
75
+ */
76
+ export declare function isMeaningfulUserCorrection(correctionContext?: {
77
+ active: boolean;
78
+ source?: string;
79
+ promptSnippet?: string;
80
+ } | null): boolean;
81
+ /**
82
+ * 判定当前 review 阶段的编辑是否属于 Skill 自动修复(CR/单测),不应计入准确率。
83
+ *
84
+ * 判定逻辑:取最近一次 review 阶段的 skill 调用,若 skill name 匹配 REVIEW_AUTOFIX_SKILLS
85
+ * 且该 skill 调用晚于最近的有意义用户纠偏上下文(correctionContext.resolvedAt),则视为自动修复。
86
+ *
87
+ * "有意义的用户纠偏" 定义:
88
+ * - source = 'correction-keyword',或
89
+ * - source = 'post-baseline-prompt' 且 promptSnippet 不是琐碎确认词
90
+ */
91
+ export declare function isReviewAutoFixActive(skillCalls: Array<{
92
+ skill: string;
93
+ triggeredAt: string;
94
+ phase: string;
95
+ }>, correctionContext?: {
96
+ active: boolean;
97
+ resolvedAt?: string;
98
+ source?: string;
99
+ promptSnippet?: string;
100
+ }): boolean;
41
101
  export type TrackedPhase = 'techDesign' | 'propose' | 'apply' | 'review';
42
102
  export type TrackedKind = 'techSpec' | 'proposalDoc' | 'code';
43
103
  /**
@@ -150,6 +210,22 @@ export declare function snapshotAiBaseline(progress: ProgressData, changeDir: st
150
210
  * @returns true 表示本次调用创建了新快照,false 表示快照已存在或跳过
151
211
  */
152
212
  export declare function ensureAccuracySnapshot(changeDir: string): Promise<boolean>;
213
+ /**
214
+ * 同 (promptId + filePath + phase) 历史 record 中的最大 editLines。
215
+ *
216
+ * 背景:用户一次 prompt 可能触发 AI 多次 Edit(重复修正同一区域)。
217
+ * 直接累加会造成虚高;取 max 反映“最终保留下来的最大一次改动”。
218
+ *
219
+ * promptId 为空时返回 0(冷启动,当前是首条,取 0 后累加全量 changeLines)。
220
+ */
221
+ export declare function computePromptFileMaxLines(correctionEdits: ReadonlyArray<{
222
+ filePath: string;
223
+ editLines: number;
224
+ phase?: string;
225
+ triggeredBy?: {
226
+ promptId?: string;
227
+ };
228
+ }>, promptId: string | undefined, filePath: string, phase: TrackedPhase): number;
153
229
  /**
154
230
  * 用户纠正行数累计(legacy——新代码优先用 record-progress.ts 内联的 correctionContext 逻辑)。
155
231
  *
@@ -62,6 +62,110 @@ export function isCodeFile(filePath) {
62
62
  const ext = path.extname(filePath).toLowerCase();
63
63
  return CODE_FILE_EXTENSIONS.has(ext);
64
64
  }
65
+ // ============================================================
66
+ // Review 阶段自动修复 Skill 白名单
67
+ // ============================================================
68
+ /**
69
+ * Review 阶段自动修复 Skill 列表。
70
+ * 这些 Skill 在 review 阶段触发的代码编辑属于自动修复,
71
+ * 不应计入用户纠正(不影响 apply 阶段准确率)。
72
+ *
73
+ * 匹配规则:skill name 包含列表中任一子串即视为 autofix skill。
74
+ */
75
+ export const REVIEW_AUTOFIX_SKILLS = [
76
+ 'code-review-expert',
77
+ 'generate-mockito-unit-test',
78
+ ];
79
+ // ============================================================
80
+ // 琐碎确认词识别(与 select-candidates.ts 保持一致)
81
+ // ============================================================
82
+ /**
83
+ * 纯确认/招呼类 prompt,不具备纠偏意图,不应触发用户纠正归因。
84
+ * 与 src/core/corrections/select-candidates.ts 中的 TRIVIAL_ACK_SET 保持同步。
85
+ */
86
+ export const TRIVIAL_ACK_WORDS = new Set([
87
+ '是', '是的', '好', '好的', '嗯', '嗯嗯', '对', '行', '可以', '确认',
88
+ '同意', '继续', '没问题', '明白', '了解', '知道了', '收到', '辛苦', '辛苦了',
89
+ 'ok', 'okay', 'k', 'yes', 'y', 'yep', 'yeah', 'sure', 'fine', 'cool',
90
+ 'go', 'go on', 'plan approved', 'continue', 'thanks', 'thx',
91
+ ]);
92
+ /** 规范化文本:小写 + 中英文标点/空白统一压成单空格。 */
93
+ function normalizeAckText(text) {
94
+ return text
95
+ .toLowerCase()
96
+ .replace(/[\s\p{P}\p{S}]+/gu, ' ')
97
+ .trim();
98
+ }
99
+ /**
100
+ * 判定 prompt 是否为琐碎确认/招呼(如"继续"、"好的"、"ok"等)。
101
+ *
102
+ * 规则:
103
+ * 1. 空字符串视为琐碎;
104
+ * 2. 规范化后命中 TRIVIAL_ACK_WORDS 视为琐碎;
105
+ * 3. 规范化后字符长度 ≤ 2 视为琐碎(如"好。"、"!!")。
106
+ *
107
+ * 注:斜杠/感叹号开头的命令词(/zhuanspec:、!ls 等)已由 user-input-hook
108
+ * 在写入 correctionContext 前过滤,无需在此重复处理。
109
+ */
110
+ export function isTrivialAckPrompt(text) {
111
+ if (!text)
112
+ return true;
113
+ const norm = normalizeAckText(text);
114
+ if (norm.length === 0)
115
+ return true;
116
+ if (TRIVIAL_ACK_WORDS.has(norm))
117
+ return true;
118
+ if (norm.length <= 2)
119
+ return true;
120
+ return false;
121
+ }
122
+ /**
123
+ * 判断 correctionContext 是否代表一次有意义的用户纠正(非琐碎确认)。
124
+ *
125
+ * 接受的 source:
126
+ * - correction-keyword:命中显式纠偏关键词,始终视为用户纠正;
127
+ * - post-baseline-prompt:baseline 后任何用户提示,但 promptSnippet
128
+ * 必须不是琐碎确认词(如"继续"、"好的")。
129
+ *
130
+ * 该函数用于 review 阶段判定 "用户提示词触发的纠正",使其覆盖范围与 apply 阶段一致。
131
+ */
132
+ export function isMeaningfulUserCorrection(correctionContext) {
133
+ if (!correctionContext?.active)
134
+ return false;
135
+ const source = correctionContext.source;
136
+ if (source === 'correction-keyword')
137
+ return true;
138
+ if (source === 'post-baseline-prompt') {
139
+ return !isTrivialAckPrompt(correctionContext.promptSnippet);
140
+ }
141
+ return false;
142
+ }
143
+ /**
144
+ * 判定当前 review 阶段的编辑是否属于 Skill 自动修复(CR/单测),不应计入准确率。
145
+ *
146
+ * 判定逻辑:取最近一次 review 阶段的 skill 调用,若 skill name 匹配 REVIEW_AUTOFIX_SKILLS
147
+ * 且该 skill 调用晚于最近的有意义用户纠偏上下文(correctionContext.resolvedAt),则视为自动修复。
148
+ *
149
+ * "有意义的用户纠偏" 定义:
150
+ * - source = 'correction-keyword',或
151
+ * - source = 'post-baseline-prompt' 且 promptSnippet 不是琐碎确认词
152
+ */
153
+ export function isReviewAutoFixActive(skillCalls, correctionContext) {
154
+ // 取 review 阶段中匹配 autofix skill 的最近一次调用
155
+ const recentAutofix = [...skillCalls]
156
+ .reverse()
157
+ .find(sc => sc.phase === 'review'
158
+ && REVIEW_AUTOFIX_SKILLS.some(s => sc.skill.includes(s)));
159
+ if (!recentAutofix)
160
+ return false;
161
+ // 如果存在有意义的用户纠偏上下文且 resolvedAt 晚于 skill 触发时间,说明用户已介入
162
+ if (isMeaningfulUserCorrection(correctionContext)
163
+ && correctionContext?.resolvedAt
164
+ && correctionContext.resolvedAt > recentAutofix.triggeredAt) {
165
+ return false;
166
+ }
167
+ return true;
168
+ }
65
169
  /**
66
170
  * 判定 filePath 在当前 phase 下是否属于白名单,并返回文件分类。
67
171
  *
@@ -322,6 +426,32 @@ export async function ensureAccuracySnapshot(changeDir) {
322
426
  }
323
427
  }
324
428
  // ============================================================
429
+ // 纠偏去重计算(同 promptId+filePath+phase 取 max)
430
+ // ============================================================
431
+ /**
432
+ * 同 (promptId + filePath + phase) 历史 record 中的最大 editLines。
433
+ *
434
+ * 背景:用户一次 prompt 可能触发 AI 多次 Edit(重复修正同一区域)。
435
+ * 直接累加会造成虚高;取 max 反映“最终保留下来的最大一次改动”。
436
+ *
437
+ * promptId 为空时返回 0(冷启动,当前是首条,取 0 后累加全量 changeLines)。
438
+ */
439
+ export function computePromptFileMaxLines(correctionEdits, promptId, filePath, phase) {
440
+ if (!promptId || !filePath)
441
+ return 0;
442
+ let maxLines = 0;
443
+ for (const e of correctionEdits) {
444
+ if (e.phase === phase
445
+ && e.filePath === filePath
446
+ && e.triggeredBy?.promptId === promptId
447
+ && typeof e.editLines === 'number'
448
+ && e.editLines > maxLines) {
449
+ maxLines = e.editLines;
450
+ }
451
+ }
452
+ return maxLines;
453
+ }
454
+ // ============================================================
325
455
  // 用户纠正行数累计
326
456
  // ============================================================
327
457
  /**
@@ -85,5 +85,32 @@ export interface CodexHookSlot {
85
85
  * the same event (matching Codex's array-of-tables semantics).
86
86
  */
87
87
  export declare function getCodexDefaultHookSlots(): readonly CodexHookSlot[];
88
+ /**
89
+ * Compute the content-trust SHA-256 hash for a hook handler, replicating
90
+ * the algorithm from Codex CLI's `codex-rs/hooks/src/engine/discovery.rs`:
91
+ *
92
+ * Codex hashes a `NormalizedHookIdentity` struct, which is serialized to
93
+ * TOML then converted to canonical JSON. The structure is:
94
+ *
95
+ * { event_name, matcher?, hooks: [{ type, command, async, timeout, statusMessage? }] }
96
+ *
97
+ * Key normalization details:
98
+ * - `timeout` defaults to 600 when absent in source TOML
99
+ * - `async` is always false (we don't support async hooks)
100
+ * - `command_windows` (None) is omitted in TOML serialization
101
+ * - `matcher` is only included when present
102
+ * - Keys are recursively sorted (canonical JSON) then SHA-256 hashed
103
+ *
104
+ * This allows ZhuanSpec to pre-seed `trusted_hash` entries directly without
105
+ * relying on cross-project hash grafting, eliminating the "Hooks need review"
106
+ * prompt on first use or after upgrades.
107
+ */
108
+ export declare function computeHookContentHash(entry: CodexHookEntry): string;
109
+ /**
110
+ * Get the computed content-trust hashes for all default hook handlers.
111
+ * Returns a map from slot suffix (`<event_snake>:<group>:<handler>`) to
112
+ * the SHA-256 hash string that Codex CLI would compute.
113
+ */
114
+ export declare function getCodexDefaultHookHashes(): Map<string, string>;
88
115
  export {};
89
116
  //# sourceMappingURL=codex-hooks-template.d.ts.map
@@ -25,6 +25,7 @@
25
25
  * ZhuanSpec markers of `<repo>/.codex/config.toml` so that `zhuanspec update`
26
26
  * can regenerate it idempotently without clobbering user-owned config.
27
27
  */
28
+ import { createHash } from 'crypto';
28
29
  /**
29
30
  * ZhuanSpec default hooks, in logical order. Keep this list in sync with the
30
31
  * Claude settings.json generator in init.ts so behavior stays consistent
@@ -142,4 +143,84 @@ export function getCodexDefaultHookSlots() {
142
143
  return { event: entry.event, eventSnake, groupIndex, handlerIndex: 0 };
143
144
  });
144
145
  }
146
+ /**
147
+ * Compute the content-trust SHA-256 hash for a hook handler, replicating
148
+ * the algorithm from Codex CLI's `codex-rs/hooks/src/engine/discovery.rs`:
149
+ *
150
+ * Codex hashes a `NormalizedHookIdentity` struct, which is serialized to
151
+ * TOML then converted to canonical JSON. The structure is:
152
+ *
153
+ * { event_name, matcher?, hooks: [{ type, command, async, timeout, statusMessage? }] }
154
+ *
155
+ * Key normalization details:
156
+ * - `timeout` defaults to 600 when absent in source TOML
157
+ * - `async` is always false (we don't support async hooks)
158
+ * - `command_windows` (None) is omitted in TOML serialization
159
+ * - `matcher` is only included when present
160
+ * - Keys are recursively sorted (canonical JSON) then SHA-256 hashed
161
+ *
162
+ * This allows ZhuanSpec to pre-seed `trusted_hash` entries directly without
163
+ * relying on cross-project hash grafting, eliminating the "Hooks need review"
164
+ * prompt on first use or after upgrades.
165
+ */
166
+ export function computeHookContentHash(entry) {
167
+ // Build the normalized handler object matching Codex's HookHandlerConfig::Command
168
+ // after normalization (timeout defaults to 600, async always present).
169
+ const handlerObj = {
170
+ type: 'command',
171
+ command: entry.command,
172
+ async: false,
173
+ timeout: 600,
174
+ };
175
+ if (entry.statusMessage) {
176
+ handlerObj.statusMessage = entry.statusMessage;
177
+ }
178
+ // Build the NormalizedHookIdentity: event_name + flattened MatcherGroup
179
+ const identity = {
180
+ event_name: toEventSnake(entry.event),
181
+ hooks: [handlerObj],
182
+ };
183
+ if (entry.matcher) {
184
+ identity.matcher = entry.matcher;
185
+ }
186
+ const canonical = canonicalJson(identity);
187
+ const bytes = Buffer.from(JSON.stringify(canonical), 'utf-8');
188
+ const hex = createHash('sha256').update(bytes).digest('hex');
189
+ return `sha256:${hex}`;
190
+ }
191
+ /**
192
+ * Recursively sort object keys alphabetically to produce canonical JSON,
193
+ * matching Codex CLI's `canonical_json()` in fingerprint.rs.
194
+ */
195
+ function canonicalJson(value) {
196
+ if (value === null || value === undefined)
197
+ return value;
198
+ if (Array.isArray(value))
199
+ return value.map(canonicalJson);
200
+ if (typeof value === 'object') {
201
+ const obj = value;
202
+ const sorted = {};
203
+ for (const key of Object.keys(obj).sort()) {
204
+ sorted[key] = canonicalJson(obj[key]);
205
+ }
206
+ return sorted;
207
+ }
208
+ return value;
209
+ }
210
+ /**
211
+ * Get the computed content-trust hashes for all default hook handlers.
212
+ * Returns a map from slot suffix (`<event_snake>:<group>:<handler>`) to
213
+ * the SHA-256 hash string that Codex CLI would compute.
214
+ */
215
+ export function getCodexDefaultHookHashes() {
216
+ const hashes = new Map();
217
+ const slots = getCodexDefaultHookSlots();
218
+ for (let i = 0; i < CODEX_DEFAULT_HOOKS.length; i++) {
219
+ const entry = CODEX_DEFAULT_HOOKS[i];
220
+ const slot = slots[i];
221
+ const suffix = `${slot.eventSnake}:${slot.groupIndex}:${slot.handlerIndex}`;
222
+ hashes.set(suffix, computeHookContentHash(entry));
223
+ }
224
+ return hashes;
225
+ }
145
226
  //# sourceMappingURL=codex-hooks-template.js.map
@@ -432,6 +432,9 @@ const applySteps = `**步骤**
432
432
  - 阅读 \`changes/<id>/proposal.md\`、\`design.md\`(如果存在)和 \`tasks.md\` 以确认范围和验收标准。
433
433
  2. **Wave 并行执行(默认且唯一执行模式)**
434
434
  - **执行模型**:按 Wave 编号串行,Wave 内任务并行启动 subagent。
435
+ - **⚠️ 每个 Wave 启动前 MUST 重读核心机制(防长任务指令衰减)**:在为新 Wave 启动 subagent 前,编排 Agent **必须**在自己的输出里**逐字复述**以下两句话,作为执行前置自检(缺失即流程故障):
436
+ 1. "本 Wave 的每个任务 MUST 通过 Agent tool / spawn_agent 启动 subagent,禁止主线程串行手写代码或手写报告。"
437
+ 2. "任务报告必须由 subagent 生成,且必须符合 .claude/agents/tdd-apply-agent.md 或 apply-agent.md 的模板章节。"
435
438
  - **并行度限制**:每个 Wave 内最多同时启动 3 个 subagent。超过 3 个任务时,按批次(batch)执行,每批最多 3 个并行 subagent,批次间串行等待。
436
439
  - **Subagent 调用 @skill 后的行为**(任务标注 \`@skill:<skill-name>\` 时):
437
440
  1. 执行 @skill 标注的 Skill
@@ -512,6 +515,15 @@ const applySteps = `**步骤**
512
515
  - 同 Wave 所有任务(含阻塞任务)全部处理完成后 → 进入集成测试阶段
513
516
 
514
517
  **B. 集成测试阶段(MANDATORY,同 Wave 所有任务处理完成后)**:
518
+ B0. **任务报告 schema 校验(MANDATORY GATE,最先执行,不得跳过)**:
519
+ - 运行 \`zhuanspec validate-reports <change-id> --json\` 校验本 Wave 所有 \`reports/task-*.md\`
520
+ - 检查内容(基于 \`.claude/agents/tdd-apply-agent.md\` / \`apply-agent.md\` 模板):
521
+ * 公共字段:\`## 状态报告\` / Status / Task ID / Report File / \`### Agent 选择决策\` / Agent 类型
522
+ * tddApplyAgent 报告必须有:TDD Phase / \`### 上下文就绪摘要\` / \`### 验收点映射表\` / \`#### Verify RED\` / \`#### Verify GREEN\` / \`#### Mock Gate\` / \`### 测试运行结果\` / \`### Self-Review 发现\` / \`### Issues/Concerns\`
523
+ * applyAgent 报告必须有:\`### 实施内容\` / \`### 修改文件\` / \`### Self-Review 发现\` / \`### Issues/Concerns\`
524
+ * 交叉对账:tasks.md 中带 \`@test-case:TC-XXX\` 的任务,报告必须为 tddApplyAgent,否则必须显式声明 "TDD 适用性: 不适合(降级原因:...)"
525
+ - 任意报告校验失败 → **该任务强制视为 BLOCKED**,按 B5 流程以 Execution Mode = RETRY 重新 spawn subagent 产出合规报告;禁止跳过本步直接进入 B1
526
+ - 校验失败的根因通常是:编排 Agent 在主线程手写代码 + 手写偷工报告,没有真的 spawn subagent。修复方式:通过 Agent tool / spawn_agent 重新下发任务
515
527
  B1. **编译检查**:运行项目编译命令,确保无编译错误
516
528
  - Java: \`mvn compile -q\` / \`gradle compileJava\`
517
529
  - TypeScript: \`tsc --noEmit\`
@@ -588,6 +600,7 @@ const applySteps = `**步骤**
588
600
  B6. **生成集成测试报告(MANDATORY,不得跳过)**:
589
601
  - 报告路径:\`changes/<change-id>/reports/wave-{N}-integration-report.md\`
590
602
  - 记录所有任务执行结果、编译检查结果、阻塞任务重试结果
603
+ - **MUST 记录 B0 报告 schema 校验结果**(通过/失败任务列表,失败时附 \`validate-reports --json\` 输出)
591
604
  - **MUST 包含 Concerns 追踪章节**,列出所有 DONE_WITH_CONCERNS 和 BLOCKED 任务及其疑虑
592
605
  - **MUST 包含 \`## 🔁 待重试任务清单\` 段**(即使为空也要写 \`— 无需重跑\`),与 B4.1 第五步、B5 重试结果汇总一致
593
606
  - 整体状态:所有检查通过且无未解决的阻塞任务 → PASS
@@ -713,6 +726,7 @@ const archiveReferences = `**参考**
713
726
  - **AI 手动**(archive 命令前):调用 \`zhuanspec:knowledge\` skill 从会话记忆补充隐式约定、调试发现等。**必须在 \`zhuanspec archive\` 命令之前**调用,否则补充内容仅落本地、不会进入远端推送
714
727
  - 会话分析 (\`session-analytics\`):记录本次会话的效率指标,用于改进工作流。`;
715
728
  const designGuardrails = `${baseGuardrails}\n- **独立设计阶段**:techDesign 命令用于在 proposal 之前生成技术设计请求文档,不依赖变更提案。设计文档可作为后续提案的输入。
729
+ - **Skill 调用连续性(红线)**:在 techDesign 流程中调用任何辅助类 Skill(如 \`@skill:load-project-knowledge\`)后,**必须立即推进到下一编号步骤**,禁止把 Skill 的输出当作流程终态,禁止停顿等待用户输入“继续/下一步”等确认词。只有在显式标注的 AskUserQuestion 步骤(如开发范围确认、需求来源确认、外部依赖确认)才允许暂停等待用户。
716
730
  - **需求澄清优先**:在生成设计请求前,必须确认需求来源(大神页面、需求描述文本等)。
717
731
  - **开发范围前置确认(硬约束)**:在调用技术方案 Skill 前,**必须**先通过 AskUserQuestion 确认开发范围是“仅后端开发”还是“全栈开发”,根据答复选择对应 Skill(仅后端=\`generate-tech-spec-md-skill\`,全栈=\`generate-fullstack-tech-spec-skill\`),禁止默认或跳过此确认环节。
718
732
  - **Skill 可用性前置检查(硬约束)**:在实际调用技术方案 Skill 前,**必须**先校验目标 Skill 是否已安装且可用;**若不可用,立即中断流程**并提示用户到 Skill 市场安装对应 Skill,禁止以人工编写/其他 Skill 代替。
@@ -722,8 +736,13 @@ const designGuardrails = `${baseGuardrails}\n- **独立设计阶段**:techDesi
722
736
  - **禁止创建提案文件**:禁止创建 .tech-design、design.md、proposal.md、tasks.md、specs/ 等。
723
737
  - **Proposal 复用**:proposal 阶段通过 progress.json 的 phase 字段识别 techDesign 目录。`;
724
738
  const designSteps = `**步骤**
725
- 0. **检查知识库并生成 change-id**:
739
+ 0. **检查知识库、加载项目知识并生成 change-id**:
726
740
  - 运行 \`rg "[需求关键词]" zhuanspec/knowledge/\` 搜索相关陷阱和最佳实践,避免重复踩坑。阅读 \`zhuanspec/knowledge/index.md\` 了解项目级知识摘要。
741
+ - **加载项目知识(必选)**:调用 \`@skill:load-project-knowledge\` 进行渐进式加载:
742
+ * 输入:domain(当前项目)、keywords(从需求描述提取的核心关键词)
743
+ * 获取:matched_services(涉及的服务列表)、search_priority(检索优先路径)、architecture_constraints(架构约束)
744
+ * 使用:后续技术方案设计需引用 matched_services 作为服务定位,方案设计需检查 architecture_constraints
745
+ * ⚠️ **连续性约束(红线)**:\`load-project-knowledge\` 完成并输出 matched_services 表后,**必须立即继续执行下面的“生成 change-id”子步骤以及步骤 1(Phase 初始化)**,禁止停顿等待用户输入“继续”。该 Skill 只是辅助加载,不是流程门禁。
727
746
  - **生成 change-id**(与 proposal 阶段命名规则一致):
728
747
  * 从用户需求描述中提取核心动词和关键词
729
748
  * 格式:动词开头 + kebab-case 关词组合
@@ -191,18 +191,24 @@ Answer: <!-- user answer -->
191
191
 
192
192
  ### Wave 1(底层:DAO / 外部 Assemble)
193
193
 
194
+ > ⚠️ **MUST (subagent gate)**: 本 Wave 每个任务必须通过 Agent tool / spawn_agent 启动 subagent 执行;主线程直接施工产生的报告会被 B0 schema 校验(\`zhuanspec validate-reports <change-id>\`)拦截并触发 RETRY。
195
+
194
196
  <!-- Wave 1: Tasks with no dependencies, 层归属限定 dao/assemble/ddl/config -->
195
197
 
196
198
  - [ ] 1.1 <!-- Task description --> @layer:dao @skill:none <!-- 纯配置变更或手动操作 -->
197
199
 
198
200
  ### Wave 2(中间层:Domain / Application)
199
201
 
202
+ > ⚠️ **MUST (subagent gate)**: 本 Wave 每个任务必须通过 Agent tool / spawn_agent 启动 subagent 执行;主线程直接施工产生的报告会被 B0 schema 校验(\`zhuanspec validate-reports <change-id>\`)拦截并触发 RETRY。
203
+
200
204
  <!-- Wave 2: Tasks depending on Wave 1, 层归属限定 domain/application -->
201
205
 
202
206
  - [ ] 2.1 <!-- Task description --> @depends:1.1 @layer:domain @skill:none <!-- 无需特定 skill -->
203
207
 
204
208
  ### Wave 3(顶层:SCF / MQ / 定时任务 / 前端)
205
209
 
210
+ > ⚠️ **MUST (subagent gate)**: 本 Wave 每个任务必须通过 Agent tool / spawn_agent 启动 subagent 执行;主线程直接施工产生的报告会被 B0 schema 校验(\`zhuanspec validate-reports <change-id>\`)拦截并触发 RETRY。
211
+
206
212
  <!-- Wave 3: Tasks depending on Wave 2, 层归属限定 entry-scf/entry-mq-*/entry-job/fe -->
207
213
 
208
214
  - [ ] 3.1 <!-- Task description --> @depends:2.1 @layer:entry-scf @skill:none <!-- 无需特定 skill -->
@@ -153,6 +153,8 @@ Answer: <!-- user answer -->
153
153
 
154
154
  ### Wave 2
155
155
 
156
+ > ⚠️ **MUST (subagent gate)**: 本 Wave 每个任务必须通过 Agent tool / spawn_agent 启动 subagent(优先 tddApplyAgent)执行;主线程直接施工产生的报告会被 B0 schema 校验(\`zhuanspec validate-reports <change-id>\`)拦截并触发 RETRY。
157
+
156
158
  <!-- Wave 2: Red Phase - Write failing tests that define expected behavior -->
157
159
  <!-- 先写测试,测试应当初始失败 — 证明测试确实在验证有意义的行为 -->
158
160
  <!-- 每个测试任务必须使用类型A/B模板,明确测试文件路径、测试方法签名、断言内容 -->
@@ -162,6 +164,8 @@ Answer: <!-- user answer -->
162
164
 
163
165
  ### Wave 3
164
166
 
167
+ > ⚠️ **MUST (subagent gate)**: 本 Wave 每个任务必须通过 Agent tool / spawn_agent 启动 subagent(优先 tddApplyAgent)执行;主线程直接施工产生的报告会被 B0 schema 校验(\`zhuanspec validate-reports <change-id>\`)拦截并触发 RETRY。
168
+
165
169
  <!-- Wave 3: Green Phase - Write minimal code to make tests pass -->
166
170
  <!-- 编写最少代码使测试通过,每个任务必须使用类型A(新增)或类型B(修改)模板 -->
167
171
 
@@ -170,6 +174,8 @@ Answer: <!-- user answer -->
170
174
 
171
175
  ### Wave 4
172
176
 
177
+ > ⚠️ **MUST (subagent gate)**: 本 Wave 每个任务必须通过 Agent tool / spawn_agent 启动 subagent(优先 tddApplyAgent)执行;主线程直接施工产生的报告会被 B0 schema 校验(\`zhuanspec validate-reports <change-id>\`)拦截并触发 RETRY。
178
+
173
179
  <!-- Wave 4: Refactor Phase - Clean up code while keeping tests green -->
174
180
  <!-- 在保持测试通过的前提下重构,每个任务使用类型B模板标明修改位置 -->
175
181
 
@@ -178,6 +184,8 @@ Answer: <!-- user answer -->
178
184
 
179
185
  ### Wave 5
180
186
 
187
+ > ⚠️ **MUST (subagent gate)**: 本 Wave 每个任务必须通过 Agent tool / spawn_agent 启动 subagent 执行;主线程直接施工产生的报告会被 B0 schema 校验(\`zhuanspec validate-reports <change-id>\`)拦截并触发 RETRY。
188
+
181
189
  <!-- Wave 5: Documentation - Document the implemented feature -->
182
190
 
183
191
  - [ ] 5.1 <!-- 更新 API 文档 --> @depends:4.2 @skill:none <!-- justification: documentation task -->
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Apply 阶段任务报告 schema 校验。
3
+ *
4
+ * 背景:编排 Agent 在长任务尾部容易"指令衰减",跳过 Agent tool / spawn_agent
5
+ * 的 subagent 调用,自己在主线程顺手把任务做掉再补一份偷工的报告。这种偷工
6
+ * 报告会缺失 tdd-apply-agent.md / apply-agent.md 模板里强制的章节
7
+ * (TDD Phase / Verify RED / Verify GREEN / Mock Gate / Skill 调用记录 ...)。
8
+ *
9
+ * 本模块对 `zhuanspec/changes/<id>/reports/task-*.md` 做轻量结构校验:
10
+ * - 必备公共字段:`## 状态报告` / Status / Task ID / Report File / Agent 选择决策 / Agent 类型
11
+ * - 当 Agent 类型 = `tddApplyAgent`:再校验 TDD Phase / 上下文就绪摘要 /
12
+ * 验收点映射表 / TDD 执行过程(Verify RED + Verify GREEN)/ 测试运行结果 /
13
+ * Self-Review 发现 / Issues
14
+ * - 当 Agent 类型 = `applyAgent`:校验实施内容 / 修改文件 / Self-Review 发现 / Issues
15
+ *
16
+ * 与 tasks.md 交叉对账:若任务在 tasks.md 里带 `@test-case:TC-XXX`,则该任务的
17
+ * 报告原则上必须由 `tddApplyAgent` 产出(除非报告里显式声明降级原因,对应
18
+ * apply-agent.md 中的"降级使用 applyAgent"路径)。
19
+ */
20
+ export type ReportAgentType = 'tddApplyAgent' | 'applyAgent' | 'unknown';
21
+ export interface ReportIssue {
22
+ level: 'ERROR' | 'WARNING';
23
+ taskId: string;
24
+ message: string;
25
+ }
26
+ export interface SingleReportResult {
27
+ /** Task id parsed from filename, e.g. "1.1". */
28
+ taskId: string;
29
+ /** Absolute path to the report file. */
30
+ filePath: string;
31
+ /** Agent type declared inside report, or 'unknown' when absent. */
32
+ declaredAgentType: ReportAgentType;
33
+ /** Agent type expected from tasks.md (`@test-case` ⇒ tddApplyAgent). */
34
+ expectedAgentType: ReportAgentType;
35
+ /** Whether the report has a downgrade justification (i.e. claims "降级"/"不适合 TDD"). */
36
+ hasDowngradeJustification: boolean;
37
+ /** Section / field names that were required but not found. */
38
+ missing: string[];
39
+ /** Issues collected for this report. */
40
+ issues: ReportIssue[];
41
+ /** Whether the report passes schema validation. */
42
+ valid: boolean;
43
+ }
44
+ export interface ReportSchemaSummary {
45
+ changeId: string;
46
+ reportsDir: string;
47
+ /** Total reports inspected. */
48
+ totalReports: number;
49
+ /** Reports passing all schema checks. */
50
+ passedReports: number;
51
+ /** Reports failing schema checks. */
52
+ failedReports: number;
53
+ /** Tasks in tasks.md that have no report file at all. */
54
+ missingReports: string[];
55
+ /** Per-report results. */
56
+ reports: SingleReportResult[];
57
+ /** Overall validity (no ERROR-level issues + no missing reports). */
58
+ valid: boolean;
59
+ }
60
+ /**
61
+ * Inspect a single report file. Pure string-level checks — no side effects.
62
+ *
63
+ * @param taskId The task id (derived from filename).
64
+ * @param content The raw markdown content of the report file.
65
+ * @param expected The agent type expected from tasks.md (default 'unknown').
66
+ */
67
+ export declare function checkReportContent(taskId: string, filePath: string, content: string, expected: ReportAgentType): SingleReportResult;
68
+ /**
69
+ * Validate all task reports under `<changeDir>/reports/`.
70
+ * Cross-checks against `<changeDir>/tasks.md` to detect missing reports and
71
+ * agent-type mismatches.
72
+ */
73
+ export declare function validateTaskReports(changeDir: string): Promise<ReportSchemaSummary>;
74
+ //# sourceMappingURL=report-schema.d.ts.map