sillyspec 3.28.0 → 3.28.3
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-auto/SKILL.md +54 -93
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-continue/SKILL.md +17 -47
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -214
- package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -270
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-propose/SKILL.md +16 -87
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -70
- package/.claude/skills/sillyspec-state/SKILL.md +25 -64
- package/.claude/skills/sillyspec-workspace/SKILL.md +33 -175
- package/package.json +1 -1
- package/src/archive-delta.js +462 -0
- package/src/constants.js +6 -0
- package/src/design-facts.js +418 -0
- package/src/dispatch/backends/sillyhub-mcp.js +17 -5
- package/src/docs-check.js +197 -34
- package/src/docs-migrate.js +124 -0
- package/src/doctor-diagnostics.js +11 -2
- package/src/endpoint-baseline.js +109 -0
- package/src/git-helper.js +31 -0
- package/src/index.js +381 -22
- package/src/modules.js +41 -12
- package/src/progress/change-registry.js +39 -0
- package/src/progress/consistency-doctor.js +124 -5
- package/src/progress/stage-machine.js +13 -2
- package/src/progress.js +5 -0
- package/src/quicklog.js +51 -0
- package/src/run/command.js +199 -5
- package/src/run/complete-handlers.js +60 -4
- package/src/run/complete.js +105 -6
- package/src/run/gates.js +353 -2
- package/src/run/progress-confirm.js +66 -0
- package/src/run/prompt.js +193 -15
- package/src/run/quick-audit.js +12 -5
- package/src/run/scan-profile.js +117 -7
- package/src/run/shared.js +22 -6
- package/src/run/stage.js +14 -1
- package/src/scan-diff.js +54 -1
- package/src/scan-facts.js +173 -0
- package/src/scan-postcheck.js +114 -3
- package/src/spec-sync.js +19 -2
- package/src/stage-review.js +62 -3
- package/src/stages/archive.js +5 -5
- package/src/stages/brainstorm-auto.js +2 -1
- package/src/stages/brainstorm.js +12 -20
- package/src/stages/execute.js +19 -21
- package/src/stages/plan-postcheck.js +194 -1
- package/src/stages/plan.js +8 -3
- package/src/stages/quick.js +4 -2
- package/src/stages/scan.js +44 -102
- package/src/stages/verify.js +18 -18
- package/src/task-review.js +200 -0
- package/src/taskcard.js +8 -1
- package/src/verify-postcheck.js +771 -29
- package/src/verify-probes.js +89 -10
- package/src/workflow.js +30 -0
- package/templates/agents-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +5 -0
- package/templates/workflows/scan-docs.yaml +6 -0
- package/.claude/skills/verify-per-user/SKILL.md +0 -6
package/src/modules.js
CHANGED
|
@@ -212,21 +212,42 @@ export function splitChangelog(cwd, { force = false } = {}) {
|
|
|
212
212
|
for (const f of cards) {
|
|
213
213
|
const cardPath = join(modulesDir, f);
|
|
214
214
|
const content = readFileSync(cardPath, 'utf8');
|
|
215
|
-
// 提取「## 变更索引」节(CRLF 归一后行级匹配,与 modules.js
|
|
215
|
+
// 提取「## 变更索引」节(CRLF 归一后行级匹配,与 modules.js 其余解析同防线)。
|
|
216
|
+
// 标题正则允许后缀(如「(表格,初始为空)」——2026-09-07 实证 core-engine 形态漏迁);
|
|
217
|
+
// miss 时再试 MANUAL_NOTES 区(stages/runtime 形态:<!-- MANUAL_NOTES_END --> 前的尾部
|
|
218
|
+
// 行块——全或无:块内行须全部为「- <变更名> | 摘要」形态才整体迁出,防误迁散文)
|
|
216
219
|
const lines = content.replace(/\r\n/g, '\n').split('\n');
|
|
217
|
-
const startIdx = lines.findIndex(l => /^##\s
|
|
218
|
-
|
|
219
|
-
let
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
const startIdx = lines.findIndex(l => /^##\s*变更索引/.test(l));
|
|
221
|
+
let kind = 'index';
|
|
222
|
+
let segStart = -1, segEnd = -1, body = '';
|
|
223
|
+
if (startIdx >= 0) {
|
|
224
|
+
let endIdx = lines.length;
|
|
225
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
226
|
+
if (/^##\s/.test(lines[i])) { endIdx = i; break; }
|
|
227
|
+
}
|
|
228
|
+
const b = lines.slice(startIdx + 1, endIdx).join('\n').trim();
|
|
229
|
+
// 已迁出(只剩指针行)或本就为空 → 跳过
|
|
230
|
+
if (!b || /^见\s/.test(b) || b.includes('.changelog.md')) { skipped++; continue; }
|
|
231
|
+
kind = 'index'; segStart = startIdx + 1; segEnd = endIdx; body = b;
|
|
232
|
+
} else {
|
|
233
|
+
const endMark = lines.findIndex(l => l.includes('<!-- MANUAL_NOTES_END -->'));
|
|
234
|
+
if (endMark < 0) { skipped++; continue; }
|
|
235
|
+
// 块首 = END 标记向上、以空行为界的连续内容块起点
|
|
236
|
+
let bs = endMark - 1;
|
|
237
|
+
while (bs >= 0 && lines[bs].trim() !== '') bs--;
|
|
238
|
+
// 块内剔除 HTML 注释行(区开标记 <!-- MANUAL_NOTES ... --> 等)后须全为变更行形态
|
|
239
|
+
const block = lines.slice(bs + 1, endMark).filter(l => {
|
|
240
|
+
const t = l.trim()
|
|
241
|
+
return t !== '' && !/^<!--.*-->$/.test(t)
|
|
242
|
+
});
|
|
243
|
+
const CHG_LINE = /^- [A-Za-z0-9][\w\-./]* \| /;
|
|
244
|
+
if (block.length === 0 || !block.every(l => CHG_LINE.test(l))) { skipped++; continue; }
|
|
245
|
+
kind = 'manual-notes'; segStart = bs + 1; segEnd = endMark; body = block.join('\n');
|
|
222
246
|
}
|
|
223
|
-
const body = lines.slice(startIdx + 1, endIdx).join('\n').trim();
|
|
224
|
-
// 已迁出(只剩指针行)或本就为空 → 跳过
|
|
225
|
-
if (!body || /^见\s/.test(body) || body.includes('.changelog.md')) { skipped++; continue; }
|
|
226
247
|
const bytesMoved = Buffer.byteLength(body, 'utf8');
|
|
227
248
|
const stem = f.replace(/\.md$/, '');
|
|
228
249
|
const sidecarPath = join(modulesDir, `${stem}.changelog.md`);
|
|
229
|
-
results.push({ card: cardPath, sidecar: sidecarPath, bytesMoved, _lines: lines,
|
|
250
|
+
results.push({ card: cardPath, sidecar: sidecarPath, bytesMoved, kind, _lines: lines, _segStart: segStart, _segEnd: segEnd, _body: body });
|
|
230
251
|
}
|
|
231
252
|
}
|
|
232
253
|
|
|
@@ -253,9 +274,17 @@ export function splitChangelog(cwd, { force = false } = {}) {
|
|
|
253
274
|
sidecarContent = header + '\n' + r._body + '\n';
|
|
254
275
|
}
|
|
255
276
|
writeFileSync(r.sidecar, sidecarContent, 'utf8');
|
|
256
|
-
//
|
|
277
|
+
// 卡内替换(只动该段,其余逐字保留;_lines 已是 LF 归一):
|
|
278
|
+
// index → 节体换指针行;manual-notes → 尾部块清空 + END 标记前留迁移指针注释
|
|
257
279
|
const stem = r.card.split(/[\\/]/).pop().replace(/\.md$/, '');
|
|
258
|
-
|
|
280
|
+
let newLines;
|
|
281
|
+
if (r.kind === 'manual-notes') {
|
|
282
|
+
newLines = [...r._lines.slice(0, r._segStart),
|
|
283
|
+
`<!-- MANUAL_NOTES 区变更条目已迁出至 ${stem}.changelog.md(split-changelog);新条目追加 sidecar,勿写回本卡 -->`,
|
|
284
|
+
...r._lines.slice(r._segEnd)];
|
|
285
|
+
} else {
|
|
286
|
+
newLines = [...r._lines.slice(0, r._segStart), '', `见 \`${stem}.changelog.md\`——历史条目已迁出;新条目直接追加 sidecar,勿写回本卡。`, '', ...r._lines.slice(r._segEnd)];
|
|
287
|
+
}
|
|
259
288
|
writeFileSync(r.card, newLines.join('\n'), 'utf8');
|
|
260
289
|
console.log(`✅ ${r.card} → ${r.sidecar}(迁出 ${(r.bytesMoved / 1024).toFixed(1)}KB,卡内留指针)`);
|
|
261
290
|
}
|
|
@@ -19,6 +19,25 @@ export class ChangeRegistry {
|
|
|
19
19
|
return rows.map(r => r.name);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* 读变更创建时间(IR 严格模式闸门判别源,change: 2026-09-07-ir-hardening D-001@v1)。
|
|
24
|
+
* 只读不抛(与 listChanges 同款 DB 访问):变更不存在 / db 异常 → null——调用方
|
|
25
|
+
* (isStrictChange)按 fail-open 落存量豁免,不误伤。
|
|
26
|
+
* @param {string} cwd
|
|
27
|
+
* @param {string} changeName
|
|
28
|
+
* @returns {string|null} ISO 时间字符串或 null
|
|
29
|
+
*/
|
|
30
|
+
getChangeCreatedAt(cwd, changeName) {
|
|
31
|
+
if (!changeName || typeof changeName !== 'string') return null;
|
|
32
|
+
try {
|
|
33
|
+
const db = this.pm._ensureDB(cwd);
|
|
34
|
+
const row = db.getDb().prepare('SELECT created_at FROM changes WHERE name = ?').get(changeName);
|
|
35
|
+
return row && typeof row.created_at === 'string' ? row.created_at : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
22
41
|
/**
|
|
23
42
|
* 查询单个变更的流程阶段与状态(quick 轻量归档阶段闸用)。
|
|
24
43
|
* 与 readChangeIsolation 不同(展示读,catch → null 宽容):本方法服务权限判定,读失败
|
|
@@ -126,6 +145,26 @@ export class ChangeRegistry {
|
|
|
126
145
|
}
|
|
127
146
|
}
|
|
128
147
|
|
|
148
|
+
/**
|
|
149
|
+
* 读取变更回填的 quicklog_id(quick 启动时 stage.js 写入的分配 ql-ID)。
|
|
150
|
+
* quick --done 兜底路径复用启动 ql-ID 用(坑 platform-takeover-phantom-progress-db
|
|
151
|
+
* 同日变体:guard 缺失时补分配新号制造引用劈叉——落码注释/模块文档的 ql-ID 是启动
|
|
152
|
+
* 时就给出的,兜底换号必然劈叉;启动 ID 进度库里可查)。
|
|
153
|
+
* @param {string} cwd
|
|
154
|
+
* @param {string} changeName
|
|
155
|
+
* @returns {string|null} 行缺失/未回填/读取失败返回 null
|
|
156
|
+
*/
|
|
157
|
+
getQuicklogId(cwd, changeName) {
|
|
158
|
+
if (!changeName) return null;
|
|
159
|
+
try {
|
|
160
|
+
const db = this.pm._ensureDB(cwd);
|
|
161
|
+
const row = db.getDb().prepare('SELECT quicklog_id FROM changes WHERE name = ?').get(changeName);
|
|
162
|
+
return (row && typeof row.quicklog_id === 'string' && row.quicklog_id) || null;
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
129
168
|
/**
|
|
130
169
|
* 读取变更的隔离状态
|
|
131
170
|
* @param {string} cwd - 项目根目录
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// W6 Step9a: Revision v1 状态一致性检查 + 修复 + --force 审计日志。
|
|
2
2
|
// 从 src/progress.js 单体 ProgressManager 抽出。持有 pm 引用(构造注入),调 pm.read / pm._write /
|
|
3
3
|
// pm._ensureRuntimeDir / pm._runtimePath(persistence-core 留 facade 本体)。
|
|
4
|
-
import { appendFileSync, existsSync, readdirSync } from 'fs';
|
|
5
|
-
import { join } from 'path';
|
|
4
|
+
import { appendFileSync, existsSync, readFileSync, readdirSync } from 'fs';
|
|
5
|
+
import { join, resolve } from 'path';
|
|
6
|
+
import { DB } from '../db.js';
|
|
6
7
|
import { summarizeTaskCompletion } from '../task-review.js';
|
|
7
8
|
import { STAGE_ORDER, MAIN_FLOW_ORDER } from './shared.js';
|
|
8
9
|
|
|
@@ -11,6 +12,104 @@ export class ConsistencyDoctor {
|
|
|
11
12
|
this.pm = pm;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* 双进度库分裂探测(坑 progress-repair-dual-library-blind,2026-09-04 用户实证②:
|
|
17
|
+
* 接管指针切换库后 progress repair 只看当前库,旧库残留的活跃变更进度完全不可见,
|
|
18
|
+
* 报告「未发现问题,无需修复」的假阴性)。
|
|
19
|
+
*
|
|
20
|
+
* 判据:cwd 存在平台接管指针(.sillyspec-platform.json 指向库 A = 当前修复目标),
|
|
21
|
+
* 同时 cwd/.sillyspec/.runtime/sillyspec.db(库 B = 切库前旧库)也存在且 ≠ 库 A
|
|
22
|
+
* → 双库并存。自指指针(specRoot 解析回本地 .sillyspec)不算。
|
|
23
|
+
*
|
|
24
|
+
* 只读探测:另一库仅在 sillyspec.db 已存在时直读(DB 类 + 存在性前置检查,绝不新建库文件);
|
|
25
|
+
* 读失败 fail-open 返回库存在但活跃清单不可读,不阻断 check/repair 主流程。
|
|
26
|
+
*
|
|
27
|
+
* @param {string} cwd - 进度库锚定 cwd(index.js 传入 progDir)
|
|
28
|
+
* @param {string|null} [changeName] - check/repair 的 --change 目标(判「操作错了库」用)
|
|
29
|
+
* @returns {{ split: boolean, dualDb: boolean, bothActive: boolean, targetInOther: boolean, lines: string[] }}
|
|
30
|
+
*/
|
|
31
|
+
detectLibrarySplit(cwd, changeName = null) {
|
|
32
|
+
const out = { split: false, dualDb: false, bothActive: false, targetInOther: false, lines: [] };
|
|
33
|
+
const pointerPath = join(cwd, '.sillyspec-platform.json');
|
|
34
|
+
if (!existsSync(pointerPath)) return out;
|
|
35
|
+
let ptr = null;
|
|
36
|
+
try {
|
|
37
|
+
ptr = JSON.parse(readFileSync(pointerPath, 'utf8'));
|
|
38
|
+
} catch {
|
|
39
|
+
return out; // 指针损坏由 resolvePlatformSpecDir fail-closed 负责,此处不重复报
|
|
40
|
+
}
|
|
41
|
+
if (!ptr.specRoot) return out;
|
|
42
|
+
|
|
43
|
+
const localRoot = join(cwd, '.sillyspec');
|
|
44
|
+
const localDbPath = join(localRoot, '.runtime', 'sillyspec.db');
|
|
45
|
+
if (resolve(ptr.specRoot) === resolve(localRoot)) return out; // 自指/同库
|
|
46
|
+
if (!existsSync(localDbPath)) return out; // 旧库无 DB → 无分裂
|
|
47
|
+
|
|
48
|
+
out.dualDb = true;
|
|
49
|
+
const currentSpecDir = this.pm._getSpecDir(cwd);
|
|
50
|
+
// 当前库:DB 已存在才读(readGlobal 走 pm 连接池,不新开连接;不存在时 readGlobal 会
|
|
51
|
+
// 隐式建库,须前置 existsSync 挡住——探测绝不产生写副作用)
|
|
52
|
+
let current = null;
|
|
53
|
+
if (existsSync(join(currentSpecDir, '.runtime', 'sillyspec.db'))) {
|
|
54
|
+
try {
|
|
55
|
+
const g = this.pm.readGlobal(cwd);
|
|
56
|
+
current = { activeChanges: (g && g.activeChanges) || [] };
|
|
57
|
+
} catch (e) {
|
|
58
|
+
current = { activeChanges: [], unreadable: true, error: e.message };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const other = this._readActiveQuiet(localRoot, cwd);
|
|
62
|
+
const currentActive = current?.activeChanges || [];
|
|
63
|
+
const otherActive = other?.activeChanges || [];
|
|
64
|
+
|
|
65
|
+
out.lines.push(
|
|
66
|
+
`双进度库并存(接管指针 ${pointerPath} → ${currentSpecDir};切库前旧库残留 ${localRoot})` +
|
|
67
|
+
`——本次 check/repair 只作用于当前库,旧库状态不可见`
|
|
68
|
+
);
|
|
69
|
+
out.lines.push(
|
|
70
|
+
` 当前库活跃变更: ${currentActive.length > 0 ? currentActive.join('、') : '(无)'}${current?.unreadable ? '(读取失败:' + current.error + ')' : ''}`
|
|
71
|
+
);
|
|
72
|
+
out.lines.push(
|
|
73
|
+
` 旧库活跃变更: ${otherActive.length > 0 ? otherActive.join('、') : '(无)'}${other?.unreadable ? '(读取失败:' + other.error + ')' : ''}`
|
|
74
|
+
);
|
|
75
|
+
out.lines.push(
|
|
76
|
+
` 处理:确认权威库后收尾/归档弃用库的活跃变更;或 sillyspec platform pointer --cleanup 删指针后重跑平台 scan 指向正确库;临时读旧库用 --spec-dir ${localRoot}`
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (currentActive.length > 0 && otherActive.length > 0) {
|
|
80
|
+
out.bothActive = true;
|
|
81
|
+
out.split = true;
|
|
82
|
+
}
|
|
83
|
+
// 目标变更只在旧库活跃(当前库查无)→ 本次操作几乎必然找错了库,最强信号
|
|
84
|
+
if (changeName && otherActive.includes(changeName) && !currentActive.includes(changeName)) {
|
|
85
|
+
out.targetInOther = true;
|
|
86
|
+
out.split = true;
|
|
87
|
+
out.lines.push(
|
|
88
|
+
` ⚠️ 目标变更 "${changeName}" 不在当前库,但旧库有同名活跃变更——本次操作错了库,修复应带上 --spec-dir ${localRoot}(或修正指针)`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 只读读一个库的活跃变更清单(detectLibrarySplit 专用)。
|
|
96
|
+
* sillyspec.db 不存在返回 null(绝不新建库);打开/读取失败返回 { unreadable, error }
|
|
97
|
+
* 不上抛——分裂探测 fail-open,不阻断主流程。
|
|
98
|
+
*/
|
|
99
|
+
_readActiveQuiet(specDir, cwd) {
|
|
100
|
+
const dbPath = join(specDir, '.runtime', 'sillyspec.db');
|
|
101
|
+
if (!existsSync(dbPath)) return null;
|
|
102
|
+
let db = null;
|
|
103
|
+
try {
|
|
104
|
+
db = new DB(dbPath);
|
|
105
|
+
db.init();
|
|
106
|
+
const rows = db.getDb().prepare("SELECT name FROM changes WHERE status = 'active' ORDER BY name").all();
|
|
107
|
+
return { activeChanges: rows.map(r => r.name) };
|
|
108
|
+
} catch (e) {
|
|
109
|
+
return { activeChanges: [], unreadable: true, error: e.message };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
14
113
|
/**
|
|
15
114
|
* 强制状态变更的审计记录(--force 逃生口专用)。
|
|
16
115
|
* 追加到 .runtime/audit.log,供人工/doctor 追溯"谁在什么时候绕过了校验"。
|
|
@@ -36,12 +135,26 @@ export class ConsistencyDoctor {
|
|
|
36
135
|
checkConsistency(cwd, changeName = null) {
|
|
37
136
|
const data = this.pm.read(cwd, changeName);
|
|
38
137
|
if (!data) {
|
|
39
|
-
|
|
138
|
+
// 双库分裂探测不依赖单变更 progress 可读(read 失败时更要看见另一库,坑
|
|
139
|
+
// progress-repair-dual-library-blind):无 progress 也要报分裂,不能只报「无法读取」
|
|
140
|
+
const splitNoData = this.detectLibrarySplit(cwd, changeName);
|
|
141
|
+
const issuesNoData = ['无法读取进度数据'];
|
|
142
|
+
if (splitNoData.targetInOther || splitNoData.bothActive) issuesNoData.push(...splitNoData.lines);
|
|
143
|
+
return { ok: false, issues: issuesNoData, warnings: splitNoData.dualDb && !splitNoData.split ? splitNoData.lines : [] };
|
|
40
144
|
}
|
|
41
145
|
|
|
42
146
|
const issues = [];
|
|
43
147
|
const warnings = [];
|
|
44
148
|
|
|
149
|
+
// 双库分裂(2026-09-04 ②):目标变更只在旧库 / 两库皆有活跃 → issue;
|
|
150
|
+
// 仅双库并存(单侧活跃)→ warning(平台模式本地库保留真实资产是容忍态,避免常态误报)
|
|
151
|
+
const libSplit = this.detectLibrarySplit(cwd, changeName);
|
|
152
|
+
if (libSplit.targetInOther || libSplit.bothActive) {
|
|
153
|
+
issues.push(...libSplit.lines);
|
|
154
|
+
} else if (libSplit.dualDb) {
|
|
155
|
+
warnings.push(...libSplit.lines);
|
|
156
|
+
}
|
|
157
|
+
|
|
45
158
|
for (const stageName of STAGE_ORDER) {
|
|
46
159
|
const sd = data.stages[stageName];
|
|
47
160
|
if (!sd) continue;
|
|
@@ -186,14 +299,20 @@ export class ConsistencyDoctor {
|
|
|
186
299
|
repairConsistency(cwd, opts = {}) {
|
|
187
300
|
const { apply = false, changeName = null } = opts;
|
|
188
301
|
|
|
302
|
+
// 双库分裂(2026-09-04 ②):repair 修不了「哪边是权威库」,只能把另一库的存在与活跃
|
|
303
|
+
// 变更亮出来进 manual 清单。放 read 之前——多活跃/读不了库时 read 早退,探测不能跟着瞎。
|
|
304
|
+
const libSplit = this.detectLibrarySplit(cwd, changeName);
|
|
305
|
+
const splitManual = libSplit.dualDb ? [...libSplit.lines] : [];
|
|
306
|
+
|
|
189
307
|
const data = this.pm.read(cwd, changeName);
|
|
190
308
|
if (!data) {
|
|
191
309
|
console.log('❌ 无法读取进度数据');
|
|
192
|
-
|
|
310
|
+
for (const m of splitManual) console.log(` - ${m}`);
|
|
311
|
+
return { fixable: [], manual: ['无法读取进度数据', ...splitManual], applied: [] };
|
|
193
312
|
}
|
|
194
313
|
|
|
195
314
|
const fixable = []; // { stage, action, description, apply: (data) => void }
|
|
196
|
-
const manual = []; // string
|
|
315
|
+
const manual = [...splitManual]; // string
|
|
197
316
|
|
|
198
317
|
const now = new Date().toLocaleString('zh-CN', { hour12: false });
|
|
199
318
|
|
|
@@ -7,8 +7,19 @@ import { mkdirSync, existsSync, readdirSync, readFileSync } from 'fs';
|
|
|
7
7
|
import { join, resolve, basename } from 'path';
|
|
8
8
|
import { writeAtomicSync } from '../fs-atomic.js';
|
|
9
9
|
import { runValidators } from '../stage-contract.js';
|
|
10
|
+
import { QUICK_SID_RE } from '../run/shared.js';
|
|
10
11
|
import { VALID_STAGES, STAGE_LABELS, STAGE_ORDER, MAIN_FLOW_ORDER, SPEC_DIR_NAME, CURRENT_VERSION, STALL_WARN_DAYS, emptyStage } from './shared.js';
|
|
11
12
|
|
|
13
|
+
// quick 会话行(quick-<8hex>)按设计无实体 changes/ 目录(progress.js initChange 跳过建目录),
|
|
14
|
+
// 「DB active + changes/ 无同名目录」的 ghost 判定对它是类别错误——进行中的 quick 从写库那
|
|
15
|
+
// 一刻起即命中误报(2026-09-07 quick-inflight-ghost-misjudge,面板「一键清理」清了又长:
|
|
16
|
+
// 每次清理都成功,新 ghost 是清理后才启动的 quick)。show 与 overview 统一经此判定排除。
|
|
17
|
+
// 注意:排除的只是「判定」,doctor --cleanup-ghosts 仍保留 quick 行归档能力(QUICKLOG 已
|
|
18
|
+
// 完成但 DB 行未注销的收尾中断形态,那是兜底出口,不在此排除)。
|
|
19
|
+
function _isGhostChange(changesRoot, cn) {
|
|
20
|
+
return !QUICK_SID_RE.test(cn) && !existsSync(join(changesRoot, cn));
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
/**
|
|
13
24
|
* 归一化步骤的已记录等待回答(show 渲染用,跨会话恢复数据源)。
|
|
14
25
|
* waitAnswers 走 pm.read 时已 JSON.parse,但容错字符串/损坏;单值列 wait_answer(旧路径)并入第 1 轮。
|
|
@@ -209,7 +220,7 @@ export class StageMachine {
|
|
|
209
220
|
const pendingConflicts = this._listPendingConflicts(this.pm._getSpecDir(cwd));
|
|
210
221
|
for (const cn of changes) {
|
|
211
222
|
const data = this.pm.read(cwd, cn);
|
|
212
|
-
const dirMissing =
|
|
223
|
+
const dirMissing = _isGhostChange(changesRoot, cn);
|
|
213
224
|
if (!data) {
|
|
214
225
|
console.log(` 📂 ${cn} — (无法读取)${dirMissing ? ' ⚠️ 目录缺失(残留记录,sillyspec doctor --cleanup-ghosts --confirm 可归档清理)' : ''}`);
|
|
215
226
|
continue;
|
|
@@ -272,7 +283,7 @@ export class StageMachine {
|
|
|
272
283
|
const list = [];
|
|
273
284
|
for (const cn of changes) {
|
|
274
285
|
const data = this.pm.read(cwd, cn);
|
|
275
|
-
const dirMissing =
|
|
286
|
+
const dirMissing = _isGhostChange(changesRoot, cn);
|
|
276
287
|
const entry = {
|
|
277
288
|
name: cn,
|
|
278
289
|
readable: !!data,
|
package/src/progress.js
CHANGED
|
@@ -833,6 +833,8 @@ export class ProgressManager {
|
|
|
833
833
|
// ── 变更管理 ──
|
|
834
834
|
|
|
835
835
|
listChanges(cwd) { return this._changeRegistry.listChanges(cwd); }
|
|
836
|
+
/** 读变更创建时间(IR 严格模式闸门判别源,2026-09-07-ir-hardening D-001)——只读不抛,缺行/异常 → null */
|
|
837
|
+
getChangeCreatedAt(cwd, changeName) { return this._changeRegistry.getChangeCreatedAt(cwd, changeName); }
|
|
836
838
|
|
|
837
839
|
// 单变更阶段查询(quick 轻量归档阶段闸):无行 null,读失败抛(调用方 fail-closed)
|
|
838
840
|
getChangeStage(cwd, changeName) { return this._changeRegistry.getChangeStage(cwd, changeName); }
|
|
@@ -868,6 +870,9 @@ export class ProgressManager {
|
|
|
868
870
|
|
|
869
871
|
updateChangeMeta(cwd, changeName, meta) { return this._changeRegistry.updateChangeMeta(cwd, changeName, meta); }
|
|
870
872
|
|
|
873
|
+
// quick --done 兜底复用启动 ql-ID(坑 platform-takeover-phantom-progress-db 同日变体)
|
|
874
|
+
getQuicklogId(cwd, changeName) { return this._changeRegistry.getQuicklogId(cwd, changeName); }
|
|
875
|
+
|
|
871
876
|
// ── CLI 命令 ──
|
|
872
877
|
|
|
873
878
|
init(cwd) {
|
package/src/quicklog.js
CHANGED
|
@@ -713,6 +713,57 @@ export async function allocateQuicklogEntry(specBase, gitUser, { description, li
|
|
|
713
713
|
})
|
|
714
714
|
}
|
|
715
715
|
|
|
716
|
+
/**
|
|
717
|
+
* 以**给定 ql-ID** 追加一条「进行中」骨架条目(不分配新 ID)。
|
|
718
|
+
*
|
|
719
|
+
* 坑 platform-takeover-phantom-progress-db 同日变体:quick --done 兜底复用启动分配的
|
|
720
|
+
* ql-ID(进度库 changes.quicklog_id 可查)时,该 ID 的条目可能不在当前 specBase 的
|
|
721
|
+
* QUICKLOG 文件里(启动与 --done 落到不同库/文件的分裂形态)——用原 ID 补建骨架再走
|
|
722
|
+
* 完成翻态,保证「同一次会话只有一个 ql-ID」。条目已存在则幂等跳过(不重复追加)。
|
|
723
|
+
* 与 allocateQuicklogEntry 同锁同格式同平台推送,仅跳过 ID 分配段。
|
|
724
|
+
*/
|
|
725
|
+
export async function appendQuicklogEntryWithId(specBase, gitUser, qlId, { description = '(guard 缺失补建)', linkedChanges = [], allowedFiles = [] } = {}) {
|
|
726
|
+
if (!qlId || typeof qlId !== 'string') throw new Error('appendQuicklogEntryWithId: qlId 必填')
|
|
727
|
+
if (findQuicklogEntry(specBase, gitUser, qlId)) return { qlId, existed: true }
|
|
728
|
+
const quicklogDir = join(specBase, 'quicklog')
|
|
729
|
+
mkdirSync(quicklogDir, { recursive: true })
|
|
730
|
+
const user = sanitizeQuicklogUser(gitUser) || 'unknown'
|
|
731
|
+
const userFile = join(quicklogDir, `QUICKLOG-${user}.md`)
|
|
732
|
+
const lockPath = join(quicklogDir, `.QUICKLOG-${user}.md.lock`)
|
|
733
|
+
const desc = sanitizeDesc(description)
|
|
734
|
+
const linked = Array.isArray(linkedChanges) ? linkedChanges : []
|
|
735
|
+
const files = Array.isArray(allowedFiles) ? allowedFiles : []
|
|
736
|
+
|
|
737
|
+
return await withFileLock(lockPath, async () => {
|
|
738
|
+
// 双检(锁内):并发兜底同 ID 只落一条
|
|
739
|
+
if (findQuicklogEntry(specBase, gitUser, qlId)) return { qlId, existed: true }
|
|
740
|
+
await rotateIfNeeded(userFile, user)
|
|
741
|
+
const entry = [
|
|
742
|
+
'',
|
|
743
|
+
`## ${qlId} | ${nowDatetime()} | ${desc}`,
|
|
744
|
+
'状态:进行中',
|
|
745
|
+
`关联变更:${linked.length > 0 ? linked.join(', ') : '(无)'}`,
|
|
746
|
+
`文件:${files.length > 0 ? files.join(', ') : '(见实际改动)'}`,
|
|
747
|
+
'',
|
|
748
|
+
].join('\n')
|
|
749
|
+
appendFileSync(userFile, entry)
|
|
750
|
+
for (const c of linked) await appendTaskCheckbox(specBase, c, qlId, desc)
|
|
751
|
+
await pushQuicklogEntryToPlatform(specBase, {
|
|
752
|
+
ql_id: qlId,
|
|
753
|
+
timestamp: nowDatetime(),
|
|
754
|
+
title: desc,
|
|
755
|
+
status: 'in_progress',
|
|
756
|
+
status_note: null,
|
|
757
|
+
author_raw: user,
|
|
758
|
+
linked_changes: linked.filter(c => /^\d{4}-\d{2}-\d{2}-/.test(c)),
|
|
759
|
+
files: files.map(f => ({ path: f, note: null })),
|
|
760
|
+
body_sections: {},
|
|
761
|
+
raw_block: entry,
|
|
762
|
+
}).catch(() => {})
|
|
763
|
+
return { qlId, existed: false }
|
|
764
|
+
})
|
|
765
|
+
}
|
|
766
|
+
|
|
716
767
|
/**
|
|
717
768
|
* 翻某 qlId 条目为「已完成」+ 追加结果 + 勾选关联 tasks.md。持锁。
|
|
718
769
|
*/
|