sillyspec 3.26.10 → 3.26.11
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/.claude/skills/sillyspec-archive/SKILL.md +85 -85
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-commit/SKILL.md +109 -109
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
- package/.claude/skills/sillyspec-doctor/SKILL.md +76 -76
- package/.claude/skills/sillyspec-execute/SKILL.md +200 -200
- package/.claude/skills/sillyspec-explore/SKILL.md +95 -95
- package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
- package/.claude/skills/sillyspec-plan/SKILL.md +148 -148
- package/.claude/skills/sillyspec-quick/SKILL.md +111 -111
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
- package/.claude/skills/sillyspec-scan/SKILL.md +79 -79
- package/.claude/skills/sillyspec-state/SKILL.md +64 -64
- package/.claude/skills/sillyspec-status/SKILL.md +54 -54
- package/.claude/skills/sillyspec-verify/SKILL.md +83 -83
- package/.claude/skills/sillyspec-workspace/SKILL.md +169 -169
- package/.gitattributes +2 -0
- package/.husky/pre-push +20 -20
- package/.npmrc-publish +2 -0
- package/README.md +201 -201
- package/SKILL.md +90 -90
- package/package.json +39 -39
- package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/server/executor.js +86 -86
- package/packages/dashboard/server/index.js +601 -601
- package/packages/dashboard/server/parser.js +526 -526
- package/src/change-risk-profile.js +254 -254
- package/src/classify-change.js +109 -109
- package/src/config-schema.js +310 -310
- package/src/contract-matrix.js +379 -379
- package/src/db.js +350 -350
- package/src/dispatch/backends/local-agent.js +155 -155
- package/src/dispatch/backends/sillyhub-mcp.js +245 -245
- package/src/dispatch/probe.js +210 -210
- package/src/dispatch/strategy.js +121 -121
- package/src/docs-check.js +131 -4
- package/src/doctor-diagnostics.js +711 -711
- package/src/endpoint-extractor.js +401 -401
- package/src/fs-atomic.js +69 -69
- package/src/git-helper.js +79 -79
- package/src/hooks/claude-pre-tool-use.cjs +125 -125
- package/src/hooks/worktree-guard.js +848 -848
- package/src/index.js +90 -8
- package/src/init.js +623 -623
- package/src/knowledge-match.js +130 -130
- package/src/local-detect.js +120 -120
- package/src/machine-interface.js +543 -543
- package/src/migrate.js +127 -127
- package/src/progress/change-registry.js +297 -260
- package/src/progress/consistency-doctor.js +355 -355
- package/src/progress/shared.js +36 -36
- package/src/progress/stage-machine.js +29 -1
- package/src/progress/step-store.js +261 -261
- package/src/progress.js +1131 -1127
- package/src/quicklog.js +693 -693
- package/src/review-tier.js +77 -77
- package/src/run/command.js +1385 -1364
- package/src/run/complete-handlers.js +1443 -1399
- package/src/run/complete.js +225 -24
- package/src/run/gates.js +755 -755
- package/src/run/prompt.js +761 -761
- package/src/run/quick-audit.js +155 -146
- package/src/run/shared.js +1268 -1215
- package/src/run/stage.js +487 -479
- package/src/scan-postcheck.js +371 -371
- package/src/setup.js +416 -416
- package/src/sillyhub-mcp/client.js +529 -529
- package/src/spec-sync.js +323 -319
- package/src/stage-contract.js +897 -897
- package/src/stage-review.js +549 -549
- package/src/stages/archive.js +155 -155
- package/src/stages/brainstorm-auto.js +236 -236
- package/src/stages/brainstorm.js +596 -597
- package/src/stages/cmd-existence.js +145 -145
- package/src/stages/doctor.js +371 -371
- package/src/stages/execute.js +1003 -1003
- package/src/stages/explore.js +34 -34
- package/src/stages/index.js +29 -29
- package/src/stages/knowledge.js +498 -498
- package/src/stages/plan-postcheck.js +1302 -1302
- package/src/stages/plan.js +625 -623
- package/src/stages/quick.js +151 -151
- package/src/stages/scan.js +607 -607
- package/src/stages/status.js +64 -64
- package/src/stages/verify.js +264 -264
- package/src/sync.js +1223 -1135
- package/src/task-review.js +1019 -1019
- package/src/verify-postcheck.js +1186 -1186
- package/src/workflow.js +600 -600
- package/src/worktree-apply.js +1213 -1136
- package/src/worktree-deps.js +353 -353
- package/src/worktree.js +1481 -1481
- package/templates/workflows/archive-impact.yaml +79 -79
- package/templates/workflows/scan-docs.yaml +132 -132
|
@@ -1,355 +1,355 @@
|
|
|
1
|
-
// W6 Step9a: Revision v1 状态一致性检查 + 修复 + --force 审计日志。
|
|
2
|
-
// 从 src/progress.js 单体 ProgressManager 抽出。持有 pm 引用(构造注入),调 pm.read / pm._write /
|
|
3
|
-
// pm._ensureRuntimeDir / pm._runtimePath(persistence-core 留 facade 本体)。
|
|
4
|
-
import { appendFileSync, existsSync, readdirSync } from 'fs';
|
|
5
|
-
import { join } from 'path';
|
|
6
|
-
import { summarizeTaskCompletion } from '../task-review.js';
|
|
7
|
-
import { STAGE_ORDER, MAIN_FLOW_ORDER } from './shared.js';
|
|
8
|
-
|
|
9
|
-
export class ConsistencyDoctor {
|
|
10
|
-
constructor(pm) {
|
|
11
|
-
this.pm = pm;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 强制状态变更的审计记录(--force 逃生口专用)。
|
|
16
|
-
* 追加到 .runtime/audit.log,供人工/doctor 追溯"谁在什么时候绕过了校验"。
|
|
17
|
-
*/
|
|
18
|
-
_appendAuditLog(cwd, entry) {
|
|
19
|
-
try {
|
|
20
|
-
this.pm._ensureRuntimeDir(cwd);
|
|
21
|
-
const auditPath = this.pm._runtimePath(cwd, 'audit.log');
|
|
22
|
-
const line = JSON.stringify({ at: new Date().toISOString(), ...entry });
|
|
23
|
-
appendFileSync(auditPath, line + '\n');
|
|
24
|
-
} catch (e) {
|
|
25
|
-
console.warn(`⚠️ 审计日志写入失败: ${e.message}`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Revision v1 状态一致性检查
|
|
31
|
-
* 只报告,不自动修复。
|
|
32
|
-
* @param {string} cwd
|
|
33
|
-
* @param {string|null} changeName
|
|
34
|
-
* @returns {{ ok: boolean, issues: string[], warnings: string[] }}
|
|
35
|
-
*/
|
|
36
|
-
checkConsistency(cwd, changeName = null) {
|
|
37
|
-
const data = this.pm.read(cwd, changeName);
|
|
38
|
-
if (!data) {
|
|
39
|
-
return { ok: false, issues: ['无法读取进度数据'], warnings: [] };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const issues = [];
|
|
43
|
-
const warnings = [];
|
|
44
|
-
|
|
45
|
-
for (const stageName of STAGE_ORDER) {
|
|
46
|
-
const sd = data.stages[stageName];
|
|
47
|
-
if (!sd) continue;
|
|
48
|
-
|
|
49
|
-
// a. completed stage 不能有 pending/stale steps
|
|
50
|
-
if (sd.status === 'completed' && sd.steps) {
|
|
51
|
-
const badSteps = sd.steps.filter(s => ['pending', 'stale', 'in-progress'].includes(s.status));
|
|
52
|
-
for (const step of badSteps) {
|
|
53
|
-
issues.push(`${stageName}/${step.name}: step 状态为 ${step.status},但 stage 状态为 completed`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// b. revising stage 应有 revision > 0 或 reopenedFromStep
|
|
58
|
-
if (sd.status === 'revising') {
|
|
59
|
-
if (!sd.revision || sd.revision < 1) {
|
|
60
|
-
issues.push(`${stageName}: 状态为 revising 但 revision 缺失或为 0`);
|
|
61
|
-
}
|
|
62
|
-
if (!sd.reopenedFromStep) {
|
|
63
|
-
warnings.push(`${stageName}: 状态为 revising 但未记录 reopenedFromStep`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// c. stale stage 应有 staleReason
|
|
68
|
-
if (sd.status === 'stale') {
|
|
69
|
-
if (!sd.staleReason) {
|
|
70
|
-
warnings.push(`${stageName}: 状态为 stale 但缺少 staleReason`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// d. 下游 completed 不能出现在上游 stale/revising 之后
|
|
75
|
-
// 用 MAIN_FLOW_ORDER(不含 scan)算上下游:scan 是 auxiliary 永不要求 completed,
|
|
76
|
-
// 把 scan 当上游会让「scan stale/revising」误报 brainstorm 不该 completed(plan-c 同根因)。
|
|
77
|
-
const stageIdx = MAIN_FLOW_ORDER.indexOf(stageName);
|
|
78
|
-
for (let i = 0; i < stageIdx; i++) {
|
|
79
|
-
const upstream = MAIN_FLOW_ORDER[i];
|
|
80
|
-
const upData = data.stages[upstream];
|
|
81
|
-
if (upData && (upData.status === 'stale' || upData.status === 'revising')) {
|
|
82
|
-
if (sd.status === 'completed') {
|
|
83
|
-
issues.push(`${stageName}: 状态为 completed,但上游 ${upstream} 状态为 ${upData.status}(下游不应在上游修订/失效时保持 completed)`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// e. step stale 时 stage 不应是 completed
|
|
89
|
-
if (sd.status === 'completed' && sd.steps) {
|
|
90
|
-
const staleSteps = sd.steps.filter(s => s.status === 'stale');
|
|
91
|
-
for (const step of staleSteps) {
|
|
92
|
-
issues.push(`${stageName}/${step.name}: step 状态为 stale,但 stage 状态为 completed`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// FR-07 / AC-03 / design §7 / D-02:lost-update 间接信号
|
|
98
|
-
// (.runtime/worktrees/<change> 目录残留 vs DB current_stage≠execute)。
|
|
99
|
-
// 只读诊断并入 issues 报告,不写 DB / 不删 worktree / 不自动修复。
|
|
100
|
-
const lostUpdateSignals = this.detectLostUpdateSignals(cwd);
|
|
101
|
-
issues.push(...lostUpdateSignals);
|
|
102
|
-
|
|
103
|
-
// 输出报告
|
|
104
|
-
console.log('');
|
|
105
|
-
console.log(' ═══════════════════════════════════════');
|
|
106
|
-
console.log(' 状态一致性检查');
|
|
107
|
-
console.log(' ═══════════════════════════════════════');
|
|
108
|
-
|
|
109
|
-
if (issues.length === 0 && warnings.length === 0) {
|
|
110
|
-
console.log(' ✅ 未发现一致性问题');
|
|
111
|
-
} else {
|
|
112
|
-
if (issues.length > 0) {
|
|
113
|
-
console.log(`\n ❌ 问题 (${issues.length}):`);
|
|
114
|
-
for (const issue of issues) console.log(` - ${issue}`);
|
|
115
|
-
}
|
|
116
|
-
if (warnings.length > 0) {
|
|
117
|
-
console.log(`\n ⚠️ 警告 (${warnings.length}):`);
|
|
118
|
-
for (const w of warnings) console.log(` - ${w}`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
console.log('');
|
|
122
|
-
|
|
123
|
-
return { ok: issues.length === 0, issues, warnings };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* 检测 lost-update 间接信号(FR-07 / AC-03 / design §7 / D-02)。
|
|
128
|
-
*
|
|
129
|
-
* 判据:.runtime/worktrees/<change> 目录存在但 DB current_stage ≠ 'execute'
|
|
130
|
-
* → worktree 残留但进度被回退,是并发场景下 lost-update 的间接痕迹。
|
|
131
|
-
*
|
|
132
|
-
* 信号定义严格遵循 design §7 / FR-07:仅 current_stage !== 'execute' 判信号,
|
|
133
|
-
* 不扩展到 quick 等其它 stage;DB 无对应行(data=null)的 worktree 目录不算信号。
|
|
134
|
-
*
|
|
135
|
-
* 只读诊断:不写 DB、不删 worktree 目录、不自动修复
|
|
136
|
-
* (修复仍走 doctor --align / repairConsistency 既有逻辑)。
|
|
137
|
-
*
|
|
138
|
-
* @param {string} cwd
|
|
139
|
-
* @returns {string[]} issue 描述数组(每条含 change 名、实际 current_stage、worktree 目录路径)
|
|
140
|
-
*/
|
|
141
|
-
detectLostUpdateSignals(cwd) {
|
|
142
|
-
const worktreesRoot = this.pm._runtimePath(cwd, 'worktrees');
|
|
143
|
-
// 零信号兼容既有 fixture:worktrees 目录不存在或读取失败时直接返回空数组
|
|
144
|
-
if (!existsSync(worktreesRoot)) return [];
|
|
145
|
-
|
|
146
|
-
let entries;
|
|
147
|
-
try {
|
|
148
|
-
entries = readdirSync(worktreesRoot, { withFileTypes: true });
|
|
149
|
-
} catch {
|
|
150
|
-
return [];
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const issues = [];
|
|
154
|
-
for (const entry of entries) {
|
|
155
|
-
// Dirent.isDirectory 在 Windows / Linux / macOS 上一致判定目录项
|
|
156
|
-
if (!entry.isDirectory()) continue;
|
|
157
|
-
const changeName = entry.name;
|
|
158
|
-
const worktreeDir = join(worktreesRoot, changeName);
|
|
159
|
-
|
|
160
|
-
// 复用 pm.read(task-08 已同步化,直接拿返回值,无 await),不新开 DB 连接
|
|
161
|
-
const data = this.pm.read(cwd, changeName);
|
|
162
|
-
// DB 无对应行(data=null)不算信号,跳过
|
|
163
|
-
if (!data) continue;
|
|
164
|
-
// 仅 current_stage !== 'execute' 判信号
|
|
165
|
-
if (data.currentStage !== 'execute') {
|
|
166
|
-
issues.push(
|
|
167
|
-
`lost-update 信号: change "${changeName}" 的 worktree 目录仍存在(${worktreeDir}),` +
|
|
168
|
-
`但 current_stage 为 "${data.currentStage}"(非 execute,疑进度被回退)`
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return issues;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Revision v1.2 状态修复
|
|
177
|
-
* 默认 dry-run,--apply 才真正修改 DB。
|
|
178
|
-
* 只修安全项,不碰产物文件、不 reset/reopen stage。
|
|
179
|
-
*
|
|
180
|
-
* @param {string} cwd
|
|
181
|
-
* @param {object} opts
|
|
182
|
-
* @param {boolean} [opts.apply=false]
|
|
183
|
-
* @param {string|null} [opts.changeName]
|
|
184
|
-
* @returns {{ fixable: object[], manual: string[], applied: object[] }}
|
|
185
|
-
*/
|
|
186
|
-
repairConsistency(cwd, opts = {}) {
|
|
187
|
-
const { apply = false, changeName = null } = opts;
|
|
188
|
-
|
|
189
|
-
const data = this.pm.read(cwd, changeName);
|
|
190
|
-
if (!data) {
|
|
191
|
-
console.log('❌ 无法读取进度数据');
|
|
192
|
-
return { fixable: [], manual: ['无法读取进度数据'], applied: [] };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const fixable = []; // { stage, action, description, apply: (data) => void }
|
|
196
|
-
const manual = []; // string
|
|
197
|
-
|
|
198
|
-
const now = new Date().toLocaleString('zh-CN', { hour12: false });
|
|
199
|
-
|
|
200
|
-
for (const stageName of STAGE_ORDER) {
|
|
201
|
-
const sd = data.stages[stageName];
|
|
202
|
-
if (!sd) continue;
|
|
203
|
-
|
|
204
|
-
// Fix a: stale stage 缺 staleReason → 补默认原因
|
|
205
|
-
if (sd.status === 'stale' && !sd.staleReason) {
|
|
206
|
-
const reason = stageName === 'archive'
|
|
207
|
-
? 'upstream stage revised; existing archive artifacts are preserved but no longer trusted'
|
|
208
|
-
: 'unknown upstream revision';
|
|
209
|
-
fixable.push({
|
|
210
|
-
stage: stageName,
|
|
211
|
-
action: 'set_stale_reason',
|
|
212
|
-
description: `${stageName}: stale 缺 staleReason → 补 "${reason}"`,
|
|
213
|
-
apply: (d) => { d.stages[stageName].staleReason = reason; },
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Fix b: 上游 stale/revising,下游仍 completed → cascade stale(同 d,用 MAIN_FLOW_ORDER 排除 scan)
|
|
218
|
-
const stageIdx = MAIN_FLOW_ORDER.indexOf(stageName);
|
|
219
|
-
for (let i = 0; i < stageIdx; i++) {
|
|
220
|
-
const upstream = MAIN_FLOW_ORDER[i];
|
|
221
|
-
const upData = data.stages[upstream];
|
|
222
|
-
if (upData && (upData.status === 'stale' || upData.status === 'revising')) {
|
|
223
|
-
if (sd.status === 'completed') {
|
|
224
|
-
const upStatus = upData.status;
|
|
225
|
-
const reason = `upstream ${upstream} is ${upStatus}`;
|
|
226
|
-
fixable.push({
|
|
227
|
-
stage: stageName,
|
|
228
|
-
action: 'cascade_stale',
|
|
229
|
-
description: `${stageName}: completed → stale(上游 ${upstream} 为 ${upStatus})`,
|
|
230
|
-
apply: (d) => {
|
|
231
|
-
d.stages[stageName].status = 'stale';
|
|
232
|
-
d.stages[stageName].staleReason = reason;
|
|
233
|
-
d.stages[stageName].completedAt = null;
|
|
234
|
-
},
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Fix c: archive stale 缺 staleReason(专用文案)
|
|
241
|
-
if (stageName === 'archive' && sd.status === 'stale' && !sd.staleReason) {
|
|
242
|
-
// 已在 Fix a 中处理,这里不重复
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Fix d: revising stage 缺 reopenedAt → 补当前时间
|
|
246
|
-
if (sd.status === 'revising' && !sd.reopenedAt) {
|
|
247
|
-
fixable.push({
|
|
248
|
-
stage: stageName,
|
|
249
|
-
action: 'set_reopened_at',
|
|
250
|
-
description: `${stageName}: revising 缺 reopenedAt → 补当前时间`,
|
|
251
|
-
apply: (d) => { d.stages[stageName].reopenedAt = now; },
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Fix e: execute 阶段 completed stage 有 pending/stale/in-progress step,但 review.json 客观产出已全通过 → 状态脱钩自动修
|
|
256
|
-
// (坑 verify-archive-flow-pitfalls 坑1+坑5:plan 加 Wave / execute Wave step 未走 --done,但 task 实际有 review.json verdict 且非 fail)
|
|
257
|
-
// 安全边界:仅当 changeName 有效、source=review.json(客观源可用)且 pending=0(所有 task verdict 通过)才自动修,
|
|
258
|
-
// 否则回落到 Manual a(保守不动)。不碰非 execute 阶段。
|
|
259
|
-
let executeAutoFixed = false;
|
|
260
|
-
if (stageName === 'execute' && changeName && sd.status === 'completed' && sd.steps) {
|
|
261
|
-
const badSteps = sd.steps.filter(st => ['pending', 'stale', 'in-progress'].includes(st.status));
|
|
262
|
-
if (badSteps.length > 0) {
|
|
263
|
-
try {
|
|
264
|
-
const changeDir = this.pm._changePath(cwd, changeName);
|
|
265
|
-
if (changeDir && existsSync(changeDir)) {
|
|
266
|
-
const runtimeRoot = this.pm._runtimePath(cwd);
|
|
267
|
-
const summary = summarizeTaskCompletion({ changeDir, runtimeRoot, changeName });
|
|
268
|
-
if (summary.source === 'review.json' && summary.total > 0 && summary.pending.length === 0) {
|
|
269
|
-
const desc = `execute: ${badSteps.length} 个 step 状态脱钩(${badSteps.map(st => st.name).join(', ')})——review.json 客观产出全通过(${summary.completed}/${summary.total}),自动标 completed`;
|
|
270
|
-
fixable.push({
|
|
271
|
-
stage: stageName,
|
|
272
|
-
action: 'align_execute_steps_to_reviews',
|
|
273
|
-
description: desc,
|
|
274
|
-
apply: (d) => {
|
|
275
|
-
for (const st of d.stages[stageName].steps) {
|
|
276
|
-
if (['pending', 'stale', 'in-progress'].includes(st.status)) {
|
|
277
|
-
st.status = 'completed';
|
|
278
|
-
st.completedAt = st.completedAt || now;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
},
|
|
282
|
-
});
|
|
283
|
-
executeAutoFixed = true;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
} catch (e) {
|
|
287
|
-
console.warn(`⚠️ execute 产出核验异常(回落 manual): ${e.message}`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Manual a: completed stage 里有 pending/stale/in-progress steps(execute 已由 Fix e 自动修则跳过)
|
|
293
|
-
if (!executeAutoFixed && sd.status === 'completed' && sd.steps) {
|
|
294
|
-
const badSteps = sd.steps.filter(s => ['pending', 'stale', 'in-progress'].includes(s.status));
|
|
295
|
-
for (const step of badSteps) {
|
|
296
|
-
manual.push(`${stageName}/${step.name}: step 状态为 ${step.status},但 stage 状态为 completed(需手动确认)`);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Manual b: revising stage 缺 reopenedFromStep
|
|
301
|
-
if (sd.status === 'revising' && !sd.reopenedFromStep) {
|
|
302
|
-
manual.push(`${stageName}: revising 缺 reopenedFromStep(需手动确认修订起始步骤)`);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Manual c: steps 为空但 stage completed
|
|
306
|
-
if (sd.status === 'completed' && (!sd.steps || sd.steps.length === 0)) {
|
|
307
|
-
manual.push(`${stageName}: completed 但 steps 为空(需手动确认)`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// 输出报告
|
|
312
|
-
console.log('');
|
|
313
|
-
console.log(' ═══════════════════════════════════════');
|
|
314
|
-
console.log(` 状态修复 ${apply ? '(--apply 模式)' : '(dry-run 模式)'}`);
|
|
315
|
-
console.log(' ═══════════════════════════════════════');
|
|
316
|
-
|
|
317
|
-
if (fixable.length === 0 && manual.length === 0) {
|
|
318
|
-
console.log(' ✅ 未发现问题,无需修复');
|
|
319
|
-
console.log('');
|
|
320
|
-
return { fixable: [], manual: [], applied: [] };
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const applied = [];
|
|
324
|
-
|
|
325
|
-
if (fixable.length > 0) {
|
|
326
|
-
console.log(`\n 🔧 可自动修复 (${fixable.length}):`);
|
|
327
|
-
for (const item of fixable) {
|
|
328
|
-
console.log(` - ${item.description}`);
|
|
329
|
-
if (apply) {
|
|
330
|
-
item.apply(data);
|
|
331
|
-
applied.push({ stage: item.stage, action: item.action });
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
if (!apply) {
|
|
335
|
-
console.log('\n 💡 使用 --apply 执行修复');
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (manual.length > 0) {
|
|
340
|
-
console.log(`\n 👆 需手动处理 (${manual.length}):`);
|
|
341
|
-
for (const m of manual) console.log(` - ${m}`);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (apply && applied.length > 0) {
|
|
345
|
-
data.lastActive = now;
|
|
346
|
-
this.pm._write(cwd, data, changeName);
|
|
347
|
-
console.log(`\n ✅ 已修复 ${applied.length} 项`);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
console.log('');
|
|
351
|
-
|
|
352
|
-
return { fixable, manual, applied };
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
1
|
+
// W6 Step9a: Revision v1 状态一致性检查 + 修复 + --force 审计日志。
|
|
2
|
+
// 从 src/progress.js 单体 ProgressManager 抽出。持有 pm 引用(构造注入),调 pm.read / pm._write /
|
|
3
|
+
// pm._ensureRuntimeDir / pm._runtimePath(persistence-core 留 facade 本体)。
|
|
4
|
+
import { appendFileSync, existsSync, readdirSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { summarizeTaskCompletion } from '../task-review.js';
|
|
7
|
+
import { STAGE_ORDER, MAIN_FLOW_ORDER } from './shared.js';
|
|
8
|
+
|
|
9
|
+
export class ConsistencyDoctor {
|
|
10
|
+
constructor(pm) {
|
|
11
|
+
this.pm = pm;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 强制状态变更的审计记录(--force 逃生口专用)。
|
|
16
|
+
* 追加到 .runtime/audit.log,供人工/doctor 追溯"谁在什么时候绕过了校验"。
|
|
17
|
+
*/
|
|
18
|
+
_appendAuditLog(cwd, entry) {
|
|
19
|
+
try {
|
|
20
|
+
this.pm._ensureRuntimeDir(cwd);
|
|
21
|
+
const auditPath = this.pm._runtimePath(cwd, 'audit.log');
|
|
22
|
+
const line = JSON.stringify({ at: new Date().toISOString(), ...entry });
|
|
23
|
+
appendFileSync(auditPath, line + '\n');
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.warn(`⚠️ 审计日志写入失败: ${e.message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Revision v1 状态一致性检查
|
|
31
|
+
* 只报告,不自动修复。
|
|
32
|
+
* @param {string} cwd
|
|
33
|
+
* @param {string|null} changeName
|
|
34
|
+
* @returns {{ ok: boolean, issues: string[], warnings: string[] }}
|
|
35
|
+
*/
|
|
36
|
+
checkConsistency(cwd, changeName = null) {
|
|
37
|
+
const data = this.pm.read(cwd, changeName);
|
|
38
|
+
if (!data) {
|
|
39
|
+
return { ok: false, issues: ['无法读取进度数据'], warnings: [] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const issues = [];
|
|
43
|
+
const warnings = [];
|
|
44
|
+
|
|
45
|
+
for (const stageName of STAGE_ORDER) {
|
|
46
|
+
const sd = data.stages[stageName];
|
|
47
|
+
if (!sd) continue;
|
|
48
|
+
|
|
49
|
+
// a. completed stage 不能有 pending/stale steps
|
|
50
|
+
if (sd.status === 'completed' && sd.steps) {
|
|
51
|
+
const badSteps = sd.steps.filter(s => ['pending', 'stale', 'in-progress'].includes(s.status));
|
|
52
|
+
for (const step of badSteps) {
|
|
53
|
+
issues.push(`${stageName}/${step.name}: step 状态为 ${step.status},但 stage 状态为 completed`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// b. revising stage 应有 revision > 0 或 reopenedFromStep
|
|
58
|
+
if (sd.status === 'revising') {
|
|
59
|
+
if (!sd.revision || sd.revision < 1) {
|
|
60
|
+
issues.push(`${stageName}: 状态为 revising 但 revision 缺失或为 0`);
|
|
61
|
+
}
|
|
62
|
+
if (!sd.reopenedFromStep) {
|
|
63
|
+
warnings.push(`${stageName}: 状态为 revising 但未记录 reopenedFromStep`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// c. stale stage 应有 staleReason
|
|
68
|
+
if (sd.status === 'stale') {
|
|
69
|
+
if (!sd.staleReason) {
|
|
70
|
+
warnings.push(`${stageName}: 状态为 stale 但缺少 staleReason`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// d. 下游 completed 不能出现在上游 stale/revising 之后
|
|
75
|
+
// 用 MAIN_FLOW_ORDER(不含 scan)算上下游:scan 是 auxiliary 永不要求 completed,
|
|
76
|
+
// 把 scan 当上游会让「scan stale/revising」误报 brainstorm 不该 completed(plan-c 同根因)。
|
|
77
|
+
const stageIdx = MAIN_FLOW_ORDER.indexOf(stageName);
|
|
78
|
+
for (let i = 0; i < stageIdx; i++) {
|
|
79
|
+
const upstream = MAIN_FLOW_ORDER[i];
|
|
80
|
+
const upData = data.stages[upstream];
|
|
81
|
+
if (upData && (upData.status === 'stale' || upData.status === 'revising')) {
|
|
82
|
+
if (sd.status === 'completed') {
|
|
83
|
+
issues.push(`${stageName}: 状态为 completed,但上游 ${upstream} 状态为 ${upData.status}(下游不应在上游修订/失效时保持 completed)`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// e. step stale 时 stage 不应是 completed
|
|
89
|
+
if (sd.status === 'completed' && sd.steps) {
|
|
90
|
+
const staleSteps = sd.steps.filter(s => s.status === 'stale');
|
|
91
|
+
for (const step of staleSteps) {
|
|
92
|
+
issues.push(`${stageName}/${step.name}: step 状态为 stale,但 stage 状态为 completed`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// FR-07 / AC-03 / design §7 / D-02:lost-update 间接信号
|
|
98
|
+
// (.runtime/worktrees/<change> 目录残留 vs DB current_stage≠execute)。
|
|
99
|
+
// 只读诊断并入 issues 报告,不写 DB / 不删 worktree / 不自动修复。
|
|
100
|
+
const lostUpdateSignals = this.detectLostUpdateSignals(cwd);
|
|
101
|
+
issues.push(...lostUpdateSignals);
|
|
102
|
+
|
|
103
|
+
// 输出报告
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(' ═══════════════════════════════════════');
|
|
106
|
+
console.log(' 状态一致性检查');
|
|
107
|
+
console.log(' ═══════════════════════════════════════');
|
|
108
|
+
|
|
109
|
+
if (issues.length === 0 && warnings.length === 0) {
|
|
110
|
+
console.log(' ✅ 未发现一致性问题');
|
|
111
|
+
} else {
|
|
112
|
+
if (issues.length > 0) {
|
|
113
|
+
console.log(`\n ❌ 问题 (${issues.length}):`);
|
|
114
|
+
for (const issue of issues) console.log(` - ${issue}`);
|
|
115
|
+
}
|
|
116
|
+
if (warnings.length > 0) {
|
|
117
|
+
console.log(`\n ⚠️ 警告 (${warnings.length}):`);
|
|
118
|
+
for (const w of warnings) console.log(` - ${w}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
console.log('');
|
|
122
|
+
|
|
123
|
+
return { ok: issues.length === 0, issues, warnings };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 检测 lost-update 间接信号(FR-07 / AC-03 / design §7 / D-02)。
|
|
128
|
+
*
|
|
129
|
+
* 判据:.runtime/worktrees/<change> 目录存在但 DB current_stage ≠ 'execute'
|
|
130
|
+
* → worktree 残留但进度被回退,是并发场景下 lost-update 的间接痕迹。
|
|
131
|
+
*
|
|
132
|
+
* 信号定义严格遵循 design §7 / FR-07:仅 current_stage !== 'execute' 判信号,
|
|
133
|
+
* 不扩展到 quick 等其它 stage;DB 无对应行(data=null)的 worktree 目录不算信号。
|
|
134
|
+
*
|
|
135
|
+
* 只读诊断:不写 DB、不删 worktree 目录、不自动修复
|
|
136
|
+
* (修复仍走 doctor --align / repairConsistency 既有逻辑)。
|
|
137
|
+
*
|
|
138
|
+
* @param {string} cwd
|
|
139
|
+
* @returns {string[]} issue 描述数组(每条含 change 名、实际 current_stage、worktree 目录路径)
|
|
140
|
+
*/
|
|
141
|
+
detectLostUpdateSignals(cwd) {
|
|
142
|
+
const worktreesRoot = this.pm._runtimePath(cwd, 'worktrees');
|
|
143
|
+
// 零信号兼容既有 fixture:worktrees 目录不存在或读取失败时直接返回空数组
|
|
144
|
+
if (!existsSync(worktreesRoot)) return [];
|
|
145
|
+
|
|
146
|
+
let entries;
|
|
147
|
+
try {
|
|
148
|
+
entries = readdirSync(worktreesRoot, { withFileTypes: true });
|
|
149
|
+
} catch {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const issues = [];
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
// Dirent.isDirectory 在 Windows / Linux / macOS 上一致判定目录项
|
|
156
|
+
if (!entry.isDirectory()) continue;
|
|
157
|
+
const changeName = entry.name;
|
|
158
|
+
const worktreeDir = join(worktreesRoot, changeName);
|
|
159
|
+
|
|
160
|
+
// 复用 pm.read(task-08 已同步化,直接拿返回值,无 await),不新开 DB 连接
|
|
161
|
+
const data = this.pm.read(cwd, changeName);
|
|
162
|
+
// DB 无对应行(data=null)不算信号,跳过
|
|
163
|
+
if (!data) continue;
|
|
164
|
+
// 仅 current_stage !== 'execute' 判信号
|
|
165
|
+
if (data.currentStage !== 'execute') {
|
|
166
|
+
issues.push(
|
|
167
|
+
`lost-update 信号: change "${changeName}" 的 worktree 目录仍存在(${worktreeDir}),` +
|
|
168
|
+
`但 current_stage 为 "${data.currentStage}"(非 execute,疑进度被回退)`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return issues;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Revision v1.2 状态修复
|
|
177
|
+
* 默认 dry-run,--apply 才真正修改 DB。
|
|
178
|
+
* 只修安全项,不碰产物文件、不 reset/reopen stage。
|
|
179
|
+
*
|
|
180
|
+
* @param {string} cwd
|
|
181
|
+
* @param {object} opts
|
|
182
|
+
* @param {boolean} [opts.apply=false]
|
|
183
|
+
* @param {string|null} [opts.changeName]
|
|
184
|
+
* @returns {{ fixable: object[], manual: string[], applied: object[] }}
|
|
185
|
+
*/
|
|
186
|
+
repairConsistency(cwd, opts = {}) {
|
|
187
|
+
const { apply = false, changeName = null } = opts;
|
|
188
|
+
|
|
189
|
+
const data = this.pm.read(cwd, changeName);
|
|
190
|
+
if (!data) {
|
|
191
|
+
console.log('❌ 无法读取进度数据');
|
|
192
|
+
return { fixable: [], manual: ['无法读取进度数据'], applied: [] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const fixable = []; // { stage, action, description, apply: (data) => void }
|
|
196
|
+
const manual = []; // string
|
|
197
|
+
|
|
198
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false });
|
|
199
|
+
|
|
200
|
+
for (const stageName of STAGE_ORDER) {
|
|
201
|
+
const sd = data.stages[stageName];
|
|
202
|
+
if (!sd) continue;
|
|
203
|
+
|
|
204
|
+
// Fix a: stale stage 缺 staleReason → 补默认原因
|
|
205
|
+
if (sd.status === 'stale' && !sd.staleReason) {
|
|
206
|
+
const reason = stageName === 'archive'
|
|
207
|
+
? 'upstream stage revised; existing archive artifacts are preserved but no longer trusted'
|
|
208
|
+
: 'unknown upstream revision';
|
|
209
|
+
fixable.push({
|
|
210
|
+
stage: stageName,
|
|
211
|
+
action: 'set_stale_reason',
|
|
212
|
+
description: `${stageName}: stale 缺 staleReason → 补 "${reason}"`,
|
|
213
|
+
apply: (d) => { d.stages[stageName].staleReason = reason; },
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Fix b: 上游 stale/revising,下游仍 completed → cascade stale(同 d,用 MAIN_FLOW_ORDER 排除 scan)
|
|
218
|
+
const stageIdx = MAIN_FLOW_ORDER.indexOf(stageName);
|
|
219
|
+
for (let i = 0; i < stageIdx; i++) {
|
|
220
|
+
const upstream = MAIN_FLOW_ORDER[i];
|
|
221
|
+
const upData = data.stages[upstream];
|
|
222
|
+
if (upData && (upData.status === 'stale' || upData.status === 'revising')) {
|
|
223
|
+
if (sd.status === 'completed') {
|
|
224
|
+
const upStatus = upData.status;
|
|
225
|
+
const reason = `upstream ${upstream} is ${upStatus}`;
|
|
226
|
+
fixable.push({
|
|
227
|
+
stage: stageName,
|
|
228
|
+
action: 'cascade_stale',
|
|
229
|
+
description: `${stageName}: completed → stale(上游 ${upstream} 为 ${upStatus})`,
|
|
230
|
+
apply: (d) => {
|
|
231
|
+
d.stages[stageName].status = 'stale';
|
|
232
|
+
d.stages[stageName].staleReason = reason;
|
|
233
|
+
d.stages[stageName].completedAt = null;
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Fix c: archive stale 缺 staleReason(专用文案)
|
|
241
|
+
if (stageName === 'archive' && sd.status === 'stale' && !sd.staleReason) {
|
|
242
|
+
// 已在 Fix a 中处理,这里不重复
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Fix d: revising stage 缺 reopenedAt → 补当前时间
|
|
246
|
+
if (sd.status === 'revising' && !sd.reopenedAt) {
|
|
247
|
+
fixable.push({
|
|
248
|
+
stage: stageName,
|
|
249
|
+
action: 'set_reopened_at',
|
|
250
|
+
description: `${stageName}: revising 缺 reopenedAt → 补当前时间`,
|
|
251
|
+
apply: (d) => { d.stages[stageName].reopenedAt = now; },
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Fix e: execute 阶段 completed stage 有 pending/stale/in-progress step,但 review.json 客观产出已全通过 → 状态脱钩自动修
|
|
256
|
+
// (坑 verify-archive-flow-pitfalls 坑1+坑5:plan 加 Wave / execute Wave step 未走 --done,但 task 实际有 review.json verdict 且非 fail)
|
|
257
|
+
// 安全边界:仅当 changeName 有效、source=review.json(客观源可用)且 pending=0(所有 task verdict 通过)才自动修,
|
|
258
|
+
// 否则回落到 Manual a(保守不动)。不碰非 execute 阶段。
|
|
259
|
+
let executeAutoFixed = false;
|
|
260
|
+
if (stageName === 'execute' && changeName && sd.status === 'completed' && sd.steps) {
|
|
261
|
+
const badSteps = sd.steps.filter(st => ['pending', 'stale', 'in-progress'].includes(st.status));
|
|
262
|
+
if (badSteps.length > 0) {
|
|
263
|
+
try {
|
|
264
|
+
const changeDir = this.pm._changePath(cwd, changeName);
|
|
265
|
+
if (changeDir && existsSync(changeDir)) {
|
|
266
|
+
const runtimeRoot = this.pm._runtimePath(cwd);
|
|
267
|
+
const summary = summarizeTaskCompletion({ changeDir, runtimeRoot, changeName });
|
|
268
|
+
if (summary.source === 'review.json' && summary.total > 0 && summary.pending.length === 0) {
|
|
269
|
+
const desc = `execute: ${badSteps.length} 个 step 状态脱钩(${badSteps.map(st => st.name).join(', ')})——review.json 客观产出全通过(${summary.completed}/${summary.total}),自动标 completed`;
|
|
270
|
+
fixable.push({
|
|
271
|
+
stage: stageName,
|
|
272
|
+
action: 'align_execute_steps_to_reviews',
|
|
273
|
+
description: desc,
|
|
274
|
+
apply: (d) => {
|
|
275
|
+
for (const st of d.stages[stageName].steps) {
|
|
276
|
+
if (['pending', 'stale', 'in-progress'].includes(st.status)) {
|
|
277
|
+
st.status = 'completed';
|
|
278
|
+
st.completedAt = st.completedAt || now;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
executeAutoFixed = true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} catch (e) {
|
|
287
|
+
console.warn(`⚠️ execute 产出核验异常(回落 manual): ${e.message}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Manual a: completed stage 里有 pending/stale/in-progress steps(execute 已由 Fix e 自动修则跳过)
|
|
293
|
+
if (!executeAutoFixed && sd.status === 'completed' && sd.steps) {
|
|
294
|
+
const badSteps = sd.steps.filter(s => ['pending', 'stale', 'in-progress'].includes(s.status));
|
|
295
|
+
for (const step of badSteps) {
|
|
296
|
+
manual.push(`${stageName}/${step.name}: step 状态为 ${step.status},但 stage 状态为 completed(需手动确认)`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Manual b: revising stage 缺 reopenedFromStep
|
|
301
|
+
if (sd.status === 'revising' && !sd.reopenedFromStep) {
|
|
302
|
+
manual.push(`${stageName}: revising 缺 reopenedFromStep(需手动确认修订起始步骤)`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Manual c: steps 为空但 stage completed
|
|
306
|
+
if (sd.status === 'completed' && (!sd.steps || sd.steps.length === 0)) {
|
|
307
|
+
manual.push(`${stageName}: completed 但 steps 为空(需手动确认)`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 输出报告
|
|
312
|
+
console.log('');
|
|
313
|
+
console.log(' ═══════════════════════════════════════');
|
|
314
|
+
console.log(` 状态修复 ${apply ? '(--apply 模式)' : '(dry-run 模式)'}`);
|
|
315
|
+
console.log(' ═══════════════════════════════════════');
|
|
316
|
+
|
|
317
|
+
if (fixable.length === 0 && manual.length === 0) {
|
|
318
|
+
console.log(' ✅ 未发现问题,无需修复');
|
|
319
|
+
console.log('');
|
|
320
|
+
return { fixable: [], manual: [], applied: [] };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const applied = [];
|
|
324
|
+
|
|
325
|
+
if (fixable.length > 0) {
|
|
326
|
+
console.log(`\n 🔧 可自动修复 (${fixable.length}):`);
|
|
327
|
+
for (const item of fixable) {
|
|
328
|
+
console.log(` - ${item.description}`);
|
|
329
|
+
if (apply) {
|
|
330
|
+
item.apply(data);
|
|
331
|
+
applied.push({ stage: item.stage, action: item.action });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!apply) {
|
|
335
|
+
console.log('\n 💡 使用 --apply 执行修复');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (manual.length > 0) {
|
|
340
|
+
console.log(`\n 👆 需手动处理 (${manual.length}):`);
|
|
341
|
+
for (const m of manual) console.log(` - ${m}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (apply && applied.length > 0) {
|
|
345
|
+
data.lastActive = now;
|
|
346
|
+
this.pm._write(cwd, data, changeName);
|
|
347
|
+
console.log(`\n ✅ 已修复 ${applied.length} 项`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
console.log('');
|
|
351
|
+
|
|
352
|
+
return { fixable, manual, applied };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|