cc-devflow 4.5.1 → 4.5.2

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 (36) hide show
  1. package/.claude/skills/cc-act/CHANGELOG.md +14 -0
  2. package/.claude/skills/cc-act/PLAYBOOK.md +26 -1
  3. package/.claude/skills/cc-act/SKILL.md +36 -7
  4. package/.claude/skills/cc-act/assets/PR_BRIEF_TEMPLATE.md +20 -0
  5. package/.claude/skills/cc-act/references/closure-contract.md +8 -0
  6. package/.claude/skills/cc-act/scripts/cc-act-common.sh +6 -1
  7. package/.claude/skills/cc-act/scripts/render-pr-brief.sh +99 -0
  8. package/.claude/skills/cc-act/scripts/verify-act-gate.sh +17 -1
  9. package/.claude/skills/cc-check/CHANGELOG.md +14 -0
  10. package/.claude/skills/cc-check/PLAYBOOK.md +101 -1
  11. package/.claude/skills/cc-check/SKILL.md +128 -7
  12. package/.claude/skills/cc-check/assets/REPORT_CARD_TEMPLATE.json +121 -1
  13. package/.claude/skills/cc-check/references/review-contract.md +88 -0
  14. package/.claude/skills/cc-check/scripts/render-report-card.js +172 -5
  15. package/.claude/skills/cc-check/scripts/verify-gate.sh +21 -0
  16. package/.claude/skills/cc-investigate/CHANGELOG.md +13 -0
  17. package/.claude/skills/cc-investigate/PLAYBOOK.md +105 -4
  18. package/.claude/skills/cc-investigate/SKILL.md +185 -8
  19. package/.claude/skills/cc-investigate/assets/ANALYSIS_TEMPLATE.md +77 -3
  20. package/.claude/skills/cc-investigate/assets/TASKS_TEMPLATE.md +10 -3
  21. package/.claude/skills/cc-investigate/assets/TASK_MANIFEST_TEMPLATE.json +102 -1
  22. package/.claude/skills/cc-investigate/references/investigation-contract.md +146 -0
  23. package/.claude/skills/cc-simplify/CHANGELOG.md +15 -0
  24. package/.claude/skills/cc-simplify/SKILL.md +255 -35
  25. package/CHANGELOG.md +16 -0
  26. package/docs/examples/example-bindings.json +3 -3
  27. package/docs/examples/full-design-blocked/README.md +1 -1
  28. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/review/report-card.json +140 -3
  29. package/docs/examples/local-handoff/README.md +1 -1
  30. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/review/report-card.json +92 -0
  31. package/docs/examples/pdca-loop/README.md +1 -1
  32. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/handoff/pr-brief.md +20 -0
  33. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/review/report-card.json +92 -0
  34. package/lib/skill-runtime/review.js +64 -1
  35. package/lib/skill-runtime/schemas.js +150 -3
  36. package/package.json +1 -1
@@ -68,6 +68,18 @@ function deriveVerdict(manifest, quickGates, strictGates, review) {
68
68
  return 'fail';
69
69
  }
70
70
 
71
+ if ([...quickGates, ...strictGates].some((gate) => ['blocked', 'pending'].includes(gate.status))) {
72
+ return 'blocked';
73
+ }
74
+
75
+ if (review.qa?.status === 'fail') {
76
+ return 'fail';
77
+ }
78
+
79
+ if (['blocked', 'pending'].includes(review.qa?.status)) {
80
+ return 'blocked';
81
+ }
82
+
71
83
  if (review.status === 'blocked') {
72
84
  return 'blocked';
73
85
  }
@@ -76,6 +88,20 @@ function deriveVerdict(manifest, quickGates, strictGates, review) {
76
88
  return 'fail';
77
89
  }
78
90
 
91
+ const freshness = buildReviewFreshness(review).status;
92
+ if (review.status === 'pass' && !['fresh', 'not-applicable'].includes(freshness)) {
93
+ return 'blocked';
94
+ }
95
+
96
+ const openOwnedFailures = (review.runtime?.failureOwnership || []).some((item) => {
97
+ const classification = item.classification || '';
98
+ const status = item.status || 'open';
99
+ return ['in-branch', 'ambiguous'].includes(classification) && !['resolved', 'closed'].includes(status);
100
+ });
101
+ if (openOwnedFailures) {
102
+ return 'blocked';
103
+ }
104
+
79
105
  return 'pass';
80
106
  }
81
107
 
@@ -116,10 +142,139 @@ function buildSummary({ quickGates, strictGates, review, verdict }) {
116
142
  ].join(' ');
117
143
  }
118
144
 
145
+ function claimFromGate(gate) {
146
+ const name = String(gate.name || '').toLowerCase();
147
+ if (/test|spec/.test(name)) {
148
+ return 'tests-pass';
149
+ }
150
+ if (/lint/.test(name)) {
151
+ return 'lint-clean';
152
+ }
153
+ if (/type/.test(name)) {
154
+ return 'typecheck-clean';
155
+ }
156
+ if (/build|compile/.test(name)) {
157
+ return 'build-succeeds';
158
+ }
159
+ return `gate-${gate.name || 'unknown'}`;
160
+ }
161
+
162
+ function buildClaimEvidence({ manifest, quickGates, strictGates, review }) {
163
+ const gateClaims = [...quickGates, ...strictGates].map((gate) => ({
164
+ claim: claimFromGate(gate),
165
+ requiredProof: 'fresh command output with exit status and key observation',
166
+ commandOrArtifact: gate.command || gate.name || '',
167
+ exitStatus: gate.exitStatus ?? null,
168
+ keyObservation: gate.summary || gate.details || '',
169
+ status: gate.status || 'blocked'
170
+ }));
171
+
172
+ const openTasks = (manifest.tasks || []).filter((task) => task.status !== 'done' && task.status !== 'completed');
173
+ gateClaims.push({
174
+ claim: 'requirements-met',
175
+ requiredProof: 'line-by-line planning/tasks.md and task-manifest.json checklist',
176
+ commandOrArtifact: 'planning/tasks.md + planning/task-manifest.json',
177
+ exitStatus: null,
178
+ keyObservation: openTasks.length === 0 ? 'no open task gaps recorded' : `${openTasks.length} open task gaps recorded`,
179
+ status: openTasks.length === 0 && review.status === 'pass' ? 'pass' : 'blocked'
180
+ });
181
+
182
+ return [...(review.claimEvidence || []), ...gateClaims];
183
+ }
184
+
185
+ function buildQa(review) {
186
+ return {
187
+ status: review.qa?.status || 'skipped',
188
+ regressionProof: review.qa?.regressionProof || [],
189
+ testQuality: review.qa?.testQuality || [],
190
+ coverageAudit: review.qa?.coverageAudit || {
191
+ status: 'skipped',
192
+ coveragePct: null,
193
+ pathMap: [],
194
+ gaps: [],
195
+ testsAdded: [],
196
+ e2eRequired: false,
197
+ evalRequired: false,
198
+ qualityStars: ''
199
+ },
200
+ browserEvidence: review.qa?.browserEvidence || {
201
+ status: 'skipped',
202
+ mode: 'not-applicable',
203
+ affectedRoutes: [],
204
+ screenshots: [],
205
+ consoleErrors: [],
206
+ healthScore: null,
207
+ issues: [],
208
+ skipReason: 'not recorded'
209
+ },
210
+ tddException: review.qa?.tddException || null
211
+ };
212
+ }
213
+
214
+ function buildRuntime(review) {
215
+ const failureOwnership = review.runtime?.failureOwnership || [];
216
+ const hasOpenOwnedFailure = failureOwnership.some((item) => {
217
+ const classification = item.classification || '';
218
+ const status = item.status || 'open';
219
+ return ['in-branch', 'ambiguous'].includes(classification) && !['resolved', 'closed'].includes(status);
220
+ });
221
+
222
+ return {
223
+ status: review.runtime?.status || (hasOpenOwnedFailure ? 'blocked' : 'pass'),
224
+ failureOwnership
225
+ };
226
+ }
227
+
228
+ function firstReviewHead(review) {
229
+ return [
230
+ review.taskReviews?.reviewPacket?.headSha,
231
+ review.diffReview?.reviewPacket?.headSha
232
+ ].find((value) => typeof value === 'string' && value.length > 0) || '';
233
+ }
234
+
235
+ function buildReviewFreshness(review) {
236
+ if (review.freshness) {
237
+ return review.freshness;
238
+ }
239
+
240
+ const headSha = firstReviewHead(review);
241
+ if (!headSha) {
242
+ return {
243
+ status: 'unknown',
244
+ reviewedCommit: '',
245
+ currentCommit: '',
246
+ commitsSinceReview: null,
247
+ staleReason: 'review headSha is not recorded'
248
+ };
249
+ }
250
+
251
+ return {
252
+ status: 'fresh',
253
+ reviewedCommit: headSha,
254
+ currentCommit: headSha,
255
+ commitsSinceReview: 0,
256
+ staleReason: ''
257
+ };
258
+ }
259
+
260
+ function buildReview(review) {
261
+ return {
262
+ ...review,
263
+ freshness: buildReviewFreshness(review),
264
+ qualityScore: review.qualityScore ?? null,
265
+ specialistReviews: review.specialistReviews || []
266
+ };
267
+ }
268
+
269
+ function isFindingOpen(item) {
270
+ const status = item.status || item.triageStatus || '';
271
+ return !['resolved', 'accepted', 'informational', 'accepted-fixed', 'rejected-with-evidence', 'deferred-minor'].includes(status);
272
+ }
273
+
119
274
  function summarizeOpenReviewFindings(findings = []) {
120
275
  return findings
121
- .filter((item) => item.status !== 'resolved' && item.status !== 'accepted' && item.status !== 'informational')
122
- .map((item) => `${item.source}: ${item.summary}`);
276
+ .filter(isFindingOpen)
277
+ .map((item) => `${item.source || 'review'}: ${item.summary || item.evidence || 'open finding'}`);
123
278
  }
124
279
 
125
280
  function collectBlockingFindings({ manifest, quickGates, strictGates, review }) {
@@ -132,8 +287,8 @@ function collectBlockingFindings({ manifest, quickGates, strictGates, review })
132
287
  }
133
288
 
134
289
  for (const gate of [...quickGates, ...strictGates]) {
135
- if (gate.status === 'fail') {
136
- findings.push(`${gate.name}: ${gate.details}`);
290
+ if (['fail', 'blocked', 'pending'].includes(gate.status)) {
291
+ findings.push(`${gate.name}: ${gate.details || gate.summary || gate.status}`);
137
292
  }
138
293
  }
139
294
 
@@ -200,6 +355,15 @@ function main() {
200
355
  blockingFindings
201
356
  });
202
357
  const specSignals = deriveSpecSignals(manifest, verdict, review);
358
+ const claimEvidence = buildClaimEvidence({
359
+ manifest,
360
+ quickGates,
361
+ strictGates,
362
+ review
363
+ });
364
+ const runtime = buildRuntime(review);
365
+ const qa = buildQa(review);
366
+ const reviewReport = buildReview(review);
203
367
 
204
368
  const report = {
205
369
  changeId: args.changeId,
@@ -214,9 +378,12 @@ function main() {
214
378
  specAlignment: specSignals.specAlignment,
215
379
  specDeltaVerified: specSignals.specDeltaVerified,
216
380
  specSyncReady: specSignals.specSyncReady,
381
+ runtime,
382
+ claimEvidence,
383
+ qa,
217
384
  quickGates,
218
385
  strictGates,
219
- review,
386
+ review: reviewReport,
220
387
  blockingFindings,
221
388
  gaps: manifest.spec?.newGaps || [],
222
389
  reroute,
@@ -36,6 +36,10 @@ jq -e '
36
36
  .summary and
37
37
  .review and
38
38
  .blockingFindings and
39
+ (.runtime and (.runtime | type == "object")) and
40
+ (.runtime.status | IN("pass", "fail", "blocked", "skipped", "pending")) and
41
+ ((.runtime.failureOwnership? // []) | type == "array") and
42
+ ((.runtime.failureOwnership? // []) | all(.[]; (.classification? // "environment") | IN("in-branch", "pre-existing", "environment", "ambiguous"))) and
39
43
  (.specAlignment? // "blocked") and
40
44
  ((.specDeltaVerified? // false) | type == "boolean") and
41
45
  ((.specSyncReady? // false) | type == "boolean") and
@@ -45,6 +49,23 @@ jq -e '
45
49
  (.overall | IN("pass", "fail")) and
46
50
  (.reroute | IN("none", "cc-do", "cc-investigate", "cc-plan")) and
47
51
  (.review.status | IN("pass", "fail", "blocked", "skipped", "pending")) and
52
+ ((.review.freshness? // {"status":"unknown"}) | type == "object") and
53
+ ((.review.freshness.status? // "unknown") | IN("fresh", "stale", "unknown", "not-applicable")) and
54
+ ((.review.specialistReviews? // []) | type == "array") and
55
+ ((.claimEvidence? // []) | type == "array") and
56
+ ((.claimEvidence? // []) | all(.[];
57
+ (.claim and .requiredProof and .commandOrArtifact and .keyObservation and .status) and
58
+ (.status | IN("pass", "fail", "blocked", "skipped", "pending"))
59
+ )) and
60
+ ((.qa? // {"status":"skipped"}) | type == "object") and
61
+ ((.qa.status? // "skipped") | IN("pass", "fail", "blocked", "skipped", "pending")) and
62
+ ((.qa.coverageAudit? // {"status":"skipped"}) | type == "object") and
63
+ ((.qa.coverageAudit.status? // "skipped") | IN("pass", "fail", "blocked", "skipped", "pending")) and
64
+ ((.qa.browserEvidence? // {"status":"skipped"}) | type == "object") and
65
+ ((.qa.browserEvidence.status? // "skipped") | IN("pass", "fail", "blocked", "skipped", "pending")) and
66
+ ((.review.findings? // []) | all(.[]; ((.confidenceScore? // 7) | type == "number") and ((.displayTier? // "info") | IN("blocking", "warning", "info", "suppressed")))) and
67
+ ((.verdict != "pass") or ((.review.freshness.status? // "unknown") | IN("fresh", "not-applicable"))) and
68
+ ((.verdict != "pass") or (((.runtime.failureOwnership? // []) | map(select(((.classification? // "") | IN("in-branch", "ambiguous")) and ((.status? // "open") | IN("open", "pending")))) | length) == 0)) and
48
69
  ((.verdict == "pass" and .reroute == "none") or (.verdict != "pass" and .reroute != "none"))
49
70
  ' "$REPORT" >/dev/null
50
71
 
@@ -1,5 +1,18 @@
1
1
  # CC-Investigate Skill Changelog
2
2
 
3
+ ## v1.1.4 - 2026-04-28
4
+
5
+ - add boundary-probe, backward-trace, reference-comparison, diagnostic-instrumentation, and condition-wait investigation modes for multi-component, deep-stack, similar-path, and flaky failures
6
+ - require analysis templates to record boundary matrices, caller chains, working-vs-broken comparisons, probe cleanup, root-cause class, and no-code-root-cause evidence
7
+ - update tasks and manifest templates so repair handoffs preserve the probe/trace facts that prove the confirmed root cause
8
+
9
+ ## v1.1.3 - 2026-04-28
10
+
11
+ - add the explicit `NO REPAIR WITHOUT A FROZEN ROOT-CAUSE CONTRACT` iron law to keep investigation separate from implementation
12
+ - require prior investigation history, pattern analysis, falsification methods, sanitized external research, and escalation decisions before freezing a root cause
13
+ - add repair-boundary scope lock fields for affected module, allowed files, forbidden files, blast radius, and split-or-reroute decisions
14
+ - update analysis, tasks, and task-manifest templates with root-cause hypothesis and investigation metadata
15
+
3
16
  ## v1.1.2 - 2026-04-27
4
17
 
5
18
  - require investigation outputs to resolve the runtime output policy before writing analysis, task, or change metadata artifacts
@@ -16,6 +16,16 @@
16
16
  3. 先证伪假设,再冻结根因。
17
17
  4. `planning/analysis.md` 和 `planning/tasks.md` 必须足够让 `cc-do` 脱离当前会话继续工作。
18
18
  5. 调查失败三次后先重建入口,不准继续乱补。
19
+ 6. 没有 frozen root-cause contract,不准进入 repair task。
20
+ 7. 多组件、深层调用、flaky 问题必须先补边界探针、反向追踪或条件等待证据。
21
+
22
+ ## Iron Law
23
+
24
+ ```text
25
+ NO REPAIR WITHOUT A FROZEN ROOT-CAUSE CONTRACT
26
+ ```
27
+
28
+ root-cause contract 至少包含:稳定复现或缩小后的可验证症状、被破坏的代码 / 配置 / 数据 / 依赖契约、已证伪假设、最小修复边界。
19
29
 
20
30
  ## Required Outputs
21
31
 
@@ -26,10 +36,101 @@
26
36
  ## Investigation Standard
27
37
 
28
38
  1. 先收集 symptom、expected、actual、repro。
29
- 2. 先沿代码路径定位触点和最近变更。
30
- 3. 每个假设都要写支持证据和反证。
31
- 4. 只有被证据钉死的根因才能进入 repair contract。
32
- 5. repair contract 只讲最小修复边界,不顺手发明新范围。
39
+ 2. 先查 prior investigations、TODOS/backlog、report-card finding 和最近变更。
40
+ 3. 先沿代码路径定位触点和最近变更。
41
+ 4. 先做 pattern analysis,再形成 1-3 个可证伪假设。
42
+ 5. 每个假设都要写支持证据、反证、证伪方法、预期观察、实际观察。
43
+ 6. 只有被证据钉死的根因才能进入 repair contract。
44
+ 7. repair contract 只讲最小修复边界,不顺手发明新范围。
45
+
46
+ ## Investigation Modes
47
+
48
+ | Mode | 什么时候用 | 第一动作 |
49
+ | --- | --- | --- |
50
+ | `reproduce-first` | 症状真实但不稳定 | 缩小复现命令 / 手动路径 |
51
+ | `diff-trace` | 昨天可用、今天坏了 | `git log --oneline -20 -- <affected-files>` |
52
+ | `boundary-probe` | API -> service -> DB、CI -> build -> deploy 这类链路断裂 | 记录每层输入、输出、配置和状态 |
53
+ | `backward-trace` | 错误出现在深层堆栈或坏值来源不明 | 从 immediate failure site 反追 original trigger |
54
+ | `reference-compare` | 同仓库有相似可用路径 | 列出 working / broken 差异并逐项接受或排除 |
55
+ | `condition-wait` | flaky、sleep、timeout、重试后消失 | 找真实等待条件,不先加大延时 |
56
+ | `history-trace` | 同一区域反复坏 | 查历史 `analysis.md`、TODO、report-card finding |
57
+ | `pattern-research` | 陌生框架 / 依赖 / 平台错误 | 脱敏后查通用错误类型 |
58
+ | `contract-check` | 修复边界可能扩大 | 判定 implementation drift / missing spec truth / roadmap mismatch |
59
+
60
+ ## Pattern Analysis
61
+
62
+ 至少对照这些模式,不要直接猜:
63
+
64
+ - race condition:间歇性、时序相关、共享状态
65
+ - null propagation:TypeError / undefined / missing guard
66
+ - state corruption:数据不一致、部分更新、hook / transaction 顺序
67
+ - integration failure:timeout、协议不匹配、外部服务边界
68
+ - configuration drift:本地 / CI / 生产表现不同
69
+ - stale cache:清缓存后恢复或旧状态复现
70
+ - resource leak:OOM、句柄增长、生命周期未释放
71
+ - trust boundary drift:外部输入、LLM 输出、用户输入被当成可信
72
+ - timing guess / flaky wait:任意 sleep / timeout / setTimeout 掩盖真实条件
73
+
74
+ ## Boundary And Trace Evidence
75
+
76
+ 复杂链路必须在 `analysis.md` 写清:
77
+
78
+ - Boundary Probe Matrix:component boundary、input observed、output observed、config/env observed、state observed、verdict
79
+ - Backward Trace Chain:immediate failure site、caller chain、bad value origin、original trigger、why symptom-site fix is rejected
80
+ - Reference Comparison:similar working example、broken path、differences accepted / ruled out
81
+ - Diagnostic Instrumentation Plan:probe location、question answered、command、expected signal、cleanup requirement
82
+
83
+ 这些字段不是装饰。它们的作用是证明根因位于源头,而不是报错点。
84
+
85
+ ## Prior Investigation History
86
+
87
+ 形成根因前至少查:
88
+
89
+ 1. `git log --oneline -20 -- <affected-files>`
90
+ 2. `rg -n "<error-keyword>|<module>|<capability>" devflow/changes`
91
+ 3. `TODOS.md`、backlog、roadmap 中的相关项
92
+ 4. 既有 `planning/analysis.md` 和 `review/report-card.json`
93
+
94
+ 命中历史时,写入 `analysis.md` 的 `Prior Investigations`,说明这次是复发、同类结构味道,还是无关历史。
95
+
96
+ ## External Research Hygiene
97
+
98
+ 只有在本地证据不足、错误类型陌生、或像依赖 / 框架 / 平台问题时才做外部调研。
99
+
100
+ - 先删除 host、IP、token、customer id、内部路径、SQL、私有 repo 名。
101
+ - 只搜错误类别、框架 / 库名、版本、通用组件名。
102
+ - 调研结果只能进入候选假设,必须回到本仓库复现或代码证据验证。
103
+
104
+ ## No Code Root Cause
105
+
106
+ 如果结论是环境、外部服务或时序窗口:
107
+
108
+ - 写 `rootCauseClass`: `code` / `config` / `environment` / `external` / `timing`
109
+ - 写明为什么不是 code root cause
110
+ - 写明需要什么 monitoring / future evidence
111
+ - 写明 operator handling,不要把未知外因包装成代码修复
112
+
113
+ ## Scope Lock
114
+
115
+ 根因假设形成后,写清:
116
+
117
+ - affected module
118
+ - allowed files
119
+ - forbidden files
120
+ - blast radius estimate
121
+ - if touches >5 files: split / justify / reroute
122
+
123
+ 超过 5 个文件默认是调查信号,不是正常 bug-fix 规模。
124
+
125
+ ## Escalation
126
+
127
+ 三次假设失败后写 `Escalation Decision`:
128
+
129
+ - failed hypothesis count
130
+ - attempted evidence
131
+ - why current entry is suspect
132
+ - next option:continue / instrument-and-wait / human-review / reroute-cc-plan
133
+ - recommendation
33
134
 
34
135
  ## Local Kit
35
136
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cc-investigate
3
- version: 1.1.2
3
+ version: 1.1.4
4
4
  description: "Use when a bug, regression, broken task, or unexpected behavior needs root-cause investigation, reproducible evidence, and a frozen repair handoff before cc-do resumes coding."
5
5
  triggers:
6
6
  - "帮我查这个 bug"
@@ -34,9 +34,11 @@ entry_gate:
34
34
  - "Read the current bug report, existing requirement artifacts, relevant code, tests, and recent history before forming any hypothesis."
35
35
  - "Use a FIX-<number>-<description> change key for new bug-fix investigations."
36
36
  - "Reproduce or narrow the symptom first, then freeze the evidence chain before proposing repair tasks."
37
+ - "Search prior investigations, TODO/backlog signals, and recent fixes in the affected area before declaring the bug novel."
38
+ - "For multi-component, deep-stack, or flaky symptoms, record boundary probes, backward trace, or condition-wait evidence before declaring the root cause."
37
39
  - "Do not write production code here; this stage ends with planning/analysis.md plus executable repair tasks for cc-do."
38
40
  exit_criteria:
39
- - "planning/analysis.md records symptom, reproduction, evidence chain, confirmed root cause, and repair boundary."
41
+ - "planning/analysis.md records symptom, reproduction, evidence chain, boundary probes or backward trace when applicable, pattern analysis, tested hypotheses, confirmed root cause, and repair boundary."
40
42
  - "planning/tasks.md and planning/task-manifest.json are explicit enough that cc-do can repair the bug without chat memory."
41
43
  - "The honest next step is cc-do, cc-plan, or roadmap."
42
44
  reroutes:
@@ -106,6 +108,21 @@ tool_budget:
106
108
 
107
109
  如果问题其实是在问“应该做什么功能”或“范围是否要变”,别硬调试,回 `cc-plan`。
108
110
 
111
+ ## Iron Law
112
+
113
+ ```text
114
+ NO REPAIR WITHOUT A FROZEN ROOT-CAUSE CONTRACT
115
+ ```
116
+
117
+ `cc-investigate` 可以跑复现、读代码、查日志、加临时探针、证伪假设,但不能把“可能是”写成 repair task。
118
+
119
+ 根因合同必须同时回答:
120
+
121
+ 1. 症状如何稳定复现或被缩小到可验证范围。
122
+ 2. 哪条代码 / 配置 / 数据 / 依赖路径违反了什么契约。
123
+ 3. 哪些假设被证伪,为什么不是它们。
124
+ 4. 最小修复边界在哪里,哪些文件明确不该动。
125
+
109
126
  ## Quick Start
110
127
 
111
128
  先判断你面对的是哪一类调查现实:
@@ -114,7 +131,13 @@ tool_budget:
114
131
  | --- | --- |
115
132
  | 症状真实,但还没有稳定复现 | `reproduce-first`,先把现象钉死 |
116
133
  | 明显是 regression | `diff-trace`,先查最近变化 |
134
+ | 多组件链路断裂 | `boundary-probe`,先记录每个边界的输入、输出、配置和状态 |
135
+ | 报错点很深或坏值来源不明 | `backward-trace`,从 symptom site 一直追到 original trigger |
136
+ | 同仓库有相似可用路径 | `reference-compare`,先列出 working vs broken differences |
137
+ | flaky / sleep / timeout 类问题 | `condition-wait`,先找真实等待条件,不先加大延时 |
117
138
  | 症状已知,但修复边界可能扩大范围 | `contract-check`,先判是否还属于当前 requirement |
139
+ | 错误类型陌生,像框架 / 依赖 / 平台问题 | `pattern-research`,先做脱敏外部调研 |
140
+ | 同一区域反复坏 | `history-trace`,先查 prior investigations 和最近修复 |
118
141
  | 看起来像 bug,实则是未定义行为或新需求 | 直接 reroute 到 `cc-plan` |
119
142
 
120
143
  先说“这是什么类问题”,再说“我要怎么修”。没有分类的 debug,最后都会变成乱打补丁。
@@ -163,29 +186,181 @@ tool_budget:
163
186
  - 记录用户看见了什么
164
187
  - 记录预期与实际差异
165
188
  - 记录复现命令或手动路径
189
+ - 如果上下文缺失,只问一个最关键问题,不一次性抛出问题清单
166
190
  2. **Trace reality**
167
191
  - 沿着代码路径找触点
192
+ - 多组件系统先写 `Boundary Probe Matrix`:每个边界的输入、输出、配置 / 环境、状态和 pass/fail
193
+ - 深层报错先写 `Backward Trace Chain`:immediate failure site、caller chain、bad value origin、original trigger
168
194
  - 查最近提交和同类改动
195
+ - 查既有 `devflow/changes/*/planning/analysis.md`、`TODOS.md`、backlog、report-card finding
169
196
  - 找现有测试和断点证据
170
197
  - 判定偏离的是 capability boundary、invariant,还是只是实现细节
171
- 3. **Form hypotheses**
198
+ 3. **Classify pattern**
199
+ - 判定是否属于 race condition、null propagation、state corruption、integration failure、configuration drift、stale cache、resource leak、trust boundary drift、timing guess / flaky wait
200
+ - 如果有同仓库 working example,先写 `Reference Comparison`,列出 working path、broken path、差异和被接受 / 排除的假设
201
+ - 如果错误类型陌生,先做脱敏外部调研;只搜索通用错误类型、框架 / 库名和版本,不搜索 raw secret / path / customer data
202
+ 4. **Form hypotheses**
172
203
  - 只保留 1-3 个可被证伪的假设
173
204
  - 每个假设都写支持证据和反证
174
- 4. **Test hypotheses**
205
+ - 每个假设都写 `falsification method`、`expected observation`、`actual observation`
206
+ 5. **Test hypotheses**
175
207
  - 用复现、日志、断言、最小探针验证
176
- - 三次假设都失败,就停下重建上下文
177
- 5. **Freeze repair contract**
208
+ - 临时探针必须写 `Diagnostic Instrumentation Plan`:probe location、question answered、command、expected signal、cleanup requirement
209
+ - 三次假设都失败,就停下进入 escalation decision
210
+ 6. **Freeze repair contract**
178
211
  - 根因确认后,写进 `planning/analysis.md`
179
212
  - 只保留最小修复边界
213
+ - 写明 affected module、allowed files、forbidden files、blast radius estimate;超过 5 个文件默认拆分或 reroute
180
214
  - 输出 `planning/tasks.md` + `planning/task-manifest.json` + `change-meta.json`
181
- 6. **Hand off**
215
+ 7. **Hand off**
182
216
  - 下一步明确写成 `cc-do`
183
217
  - 如果 repair contract 越过当前 requirement,就 reroute 到 `cc-plan`
184
218
 
219
+ ## Pattern Analysis
220
+
221
+ 不要从空白猜测开始。先把 bug 放进一个可检查的模式:
222
+
223
+ | Pattern | Signature | First place to inspect |
224
+ | --- | --- | --- |
225
+ | race condition | 间歇性、时序相关、重试后消失 | 并发写、锁、队列、共享状态 |
226
+ | null propagation | TypeError / NoMethod / undefined access | nullable 输入、默认值、边界 guard |
227
+ | state corruption | 数据不一致、部分更新、顺序错乱 | transaction、callback、hook、副作用 |
228
+ | integration failure | timeout、unexpected response、协议不匹配 | API boundary、service config、schema |
229
+ | configuration drift | 本地可用、CI/生产失败 | env、feature flag、版本、路径、权限 |
230
+ | stale cache | 清缓存后恢复、旧状态复现 | browser / CDN / Redis / build cache |
231
+ | resource leak | OOM、句柄增长、慢性崩溃 | lifecycle、subscription、retention、cleanup |
232
+ | trust boundary drift | LLM / 用户输入 / 外部响应被当成可信 | validation、escaping、policy gate |
233
+ | timing guess / flaky wait | sleep / setTimeout / timeout 增大后偶尔通过 | 真实完成条件、事件、文件、状态或队列计数 |
234
+
235
+ 模式分析不是结论,只是定位第一批证据的索引。
236
+
237
+ ## Boundary Probe Matrix
238
+
239
+ 多组件链路不要先猜。先记录每个边界的事实:
240
+
241
+ - component boundary
242
+ - input observed
243
+ - output observed
244
+ - config / env observed
245
+ - state observed
246
+ - verdict: `pass` / `fail` / `unknown`
247
+
248
+ 只有一个边界先失败时,后续调查才收缩到那个组件。多个边界都异常时,优先找共同上游,不在下游堆补丁。
249
+
250
+ ## Backward Trace Chain
251
+
252
+ 报错点很深时,不准只在 symptom site 加 guard。`analysis.md` 必须追到:
253
+
254
+ - immediate failure site
255
+ - direct caller
256
+ - caller chain
257
+ - bad value origin
258
+ - original trigger
259
+ - why symptom-site fix is rejected
260
+
261
+ 找不到 original trigger 时,根因还没有冻结,只能继续调查或进入 escalation。
262
+
263
+ ## Reference Comparison
264
+
265
+ 同仓库或参考实现有相似可用路径时,先对照再假设:
266
+
267
+ - similar working example
268
+ - broken path
269
+ - differences found
270
+ - differences accepted as hypothesis
271
+ - differences ruled out
272
+
273
+ 不要用“看起来差不多”跳过差异。小差异也可能是根因。
274
+
275
+ ## Diagnostic Instrumentation
276
+
277
+ 临时日志、断言、探针只能用于回答一个明确问题:
278
+
279
+ - probe location
280
+ - question answered
281
+ - command to run
282
+ - expected signal
283
+ - actual signal
284
+ - cleanup requirement
285
+
286
+ 探针不能变成修复。进入 `cc-do` 前,要么删除,要么明确写入 repair task 的清理 / 转正方式。
287
+
288
+ ## Timing And Flaky Bugs
289
+
290
+ 遇到 flaky、sleep、timeout、重试后消失:
291
+
292
+ - 先找真实等待条件:event、state、file、count、queue empty、network response
293
+ - 任意 timeout 必须说明为什么这个时间是业务 / 协议事实,不是猜测
294
+ - 如果只能在并发或负载下复现,记录对应命令和环境
295
+ - 不把“加大 sleep”写成 repair contract,除非它本身就是被证实的协议等待窗口
296
+
297
+ ## No Code Root Cause
298
+
299
+ 如果调查证明是环境、外部服务或时序窗口,不要假装代码根因:
300
+
301
+ - `rootCauseClass`: `code` / `config` / `environment` / `external` / `timing`
302
+ - `why not code root cause`
303
+ - `monitoring or future evidence needed`
304
+ - `operator handling after fix`
305
+
306
+ 这类结论仍然需要本地证据支撑;“没有根因”通常只是调查不完整。
307
+
308
+ ## Prior Investigation History
309
+
310
+ 同一区域反复坏是架构味道,不是巧合。形成根因前至少查:
311
+
312
+ 1. `git log --oneline -20 -- <affected-files>`
313
+ 2. `rg -n "<error-keyword>|<module>|<capability>" devflow/changes`
314
+ 3. `TODOS.md`、backlog、roadmap 中的相关未解决项
315
+ 4. 既有 `report-card.json` finding 和 `planning/analysis.md`
316
+ 5. 可用时,查询项目记忆或历史调查摘要
317
+
318
+ 如果命中过往调查,写入 `analysis.md` 的 `Prior Investigations`,包括是否复发、根因是否同类、这次是否说明结构问题。
319
+
320
+ ## External Research Hygiene
321
+
322
+ 外部调研只在本地证据不足、错误类型陌生、或像依赖 / 框架 / 平台 bug 时使用。
323
+
324
+ 调研前必须脱敏:
325
+
326
+ - 删除 host、IP、token、customer id、内部路径、SQL 片段、私有 repo 名
327
+ - 搜索错误类别、框架 / 库名、版本和通用组件名
328
+ - 如果无法安全脱敏,就跳过外部搜索,并在 `researchEvidence` 写明原因
329
+
330
+ 调研结论只能作为候选假设,不能直接变成 confirmed root cause。必须回到本仓库复现或代码证据验证。
331
+
332
+ ## Scope Lock And Blast Radius
333
+
334
+ 形成根因假设后,先锁定最小调查 / 修复边界:
335
+
336
+ - `affectedModule`
337
+ - `allowedFiles`
338
+ - `forbiddenFiles`
339
+ - `blastRadiusFiles`
340
+ - `blastRadiusRisk`: `low` / `medium` / `high`
341
+
342
+ 如果修复预计触碰超过 5 个文件,默认说明这可能不是单点 bug:
343
+
344
+ 1. 能拆成 critical path + follow-up,就拆。
345
+ 2. 不能拆但仍是根因跨度,写明为什么。
346
+ 3. 如果已经变成设计 / 架构范围,reroute 到 `cc-plan`。
347
+
348
+ ## Escalation Decision
349
+
350
+ 三次假设失败后,不准继续硬猜。`analysis.md` 必须写:
351
+
352
+ - failedHypothesisCount
353
+ - what was attempted
354
+ - why current entry is suspect
355
+ - next option:`continue-with-new-hypothesis` / `instrument-and-wait` / `human-review` / `reroute-cc-plan`
356
+ - recommendation
357
+
185
358
  ## Good Output
186
359
 
187
360
  - 看完第一屏就知道 bug 是什么、怎么复现、为什么会坏
188
361
  - 根因不是感觉,而是被证据钉死的具体断点
362
+ - 假设不是列表装饰,而是带证伪方法和实际观察
363
+ - 历史调查、最近改动、模式分析没有被跳过
189
364
  - 修复边界清楚到 `cc-do` 不需要二次调查
190
365
  - `planning/tasks.md` 只包含修复任务,不夹带新需求
191
366
  - 如果应该回 `cc-plan`,理由写清楚,不假装还能继续 patch
@@ -207,7 +382,9 @@ tool_budget:
207
382
  4. `planning/tasks.md` 必须足够让 `cc-do` 在脱离当前对话后继续推进。
208
383
  5. 如果修复方案已经变成新 feature 设计,停止 debug,回 `cc-plan`。
209
384
  6. 三次假设失败后,默认说明你的调查入口错了,不准继续硬猜。
210
- 7. 好的调查不是“找了很多可能性”,而是把错误世界缩成一个可信的 repair contract。
385
+ 7. 外部调研必须先脱敏,调研结论必须回到本仓库证据验证。
386
+ 8. 修复触点超过 5 个文件时,默认先拆分或 reroute,不把大重构伪装成 bug fix。
387
+ 9. 好的调查不是“找了很多可能性”,而是把错误世界缩成一个可信的 repair contract。
211
388
 
212
389
  ## Exit Criteria
213
390