sillyspec 3.33.0 → 3.33.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.
- package/.claude/skills/sillyspec-flow/SKILL.md +18 -10
- package/.github/workflows/ci.yml +11 -3
- package/AGENTS.md +3 -3
- package/README.md +2 -2
- package/assets/command-cards/flow.md +9 -5
- package/package.json +1 -1
- package/src/archive-delta.js +4 -3
- package/src/change-delete.js +6 -5
- package/src/contract-matrix.js +2 -1
- package/src/cross-placement.js +5 -1
- package/src/cross-repo-reconcile.js +4 -1
- package/src/db.js +8 -2
- package/src/decision-distill.js +22 -8
- package/src/decision-values.js +34 -0
- package/src/decisions-io.js +5 -2
- package/src/deleted-ledger.js +2 -1
- package/src/doctor-diagnostics.js +8 -3
- package/src/flow-draft.js +17 -10
- package/src/flow-parity.js +93 -5
- package/src/flow-review-pack.js +183 -0
- package/src/flow-review.js +45 -3
- package/src/flow.js +365 -109
- package/src/friction-ledger.js +2 -1
- package/src/friction-tally.js +8 -4
- package/src/handoff.js +2 -1
- package/src/hooks/worktree-guard.js +5 -5
- package/src/index.js +62 -21
- package/src/init.js +7 -30
- package/src/knowledge-hits.js +2 -1
- package/src/knowledge-vector.js +2 -1
- package/src/progress/consistency-doctor.js +5 -2
- package/src/progress/stage-machine.js +6 -2
- package/src/progress.js +19 -8
- package/src/quicklog.js +194 -9
- package/src/review-dispatch.js +4 -3
- package/src/review-tier.js +2 -1
- package/src/route-hindsight.js +4 -3
- package/src/run/bg-sync.js +10 -3
- package/src/run/command.js +14 -12
- package/src/run/complete-handlers.js +40 -37
- package/src/run/complete.js +12 -8
- package/src/run/gate-snapshot-ledger.js +2 -1
- package/src/run/gate-snapshot.js +102 -23
- package/src/run/gates.js +32 -8
- package/src/run/prompt.js +6 -5
- package/src/run/scan-profile.js +2 -1
- package/src/run/shared.js +109 -5
- package/src/run/stage.js +5 -4
- package/src/run/test-ledger.js +2 -1
- package/src/run/verify-quality-scan.js +11 -5
- package/src/runtime-paths.js +189 -0
- package/src/scan-refresh.js +4 -3
- package/src/scope-audit.js +13 -7
- package/src/sentinel-assertions.js +37 -0
- package/src/spec-sync.js +38 -22
- package/src/stage-contract.js +24 -2
- package/src/stage-review.js +3 -2
- package/src/stage-templates.js +3 -2
- package/src/stages/brainstorm.js +6 -5
- package/src/sync-noise.js +3 -2
- package/src/sync.js +71 -15
- package/src/task-done.js +2 -1
- package/src/task-review.js +7 -5
- package/src/task-tick.js +45 -1
- package/src/validate-artifacts.js +2 -1
- package/src/verify-draft.js +2 -1
- package/src/verify-postcheck.js +11 -10
- package/src/verify-probes.js +157 -52
- package/src/watcher.js +15 -9
- package/src/worktree-apply.js +2 -1
- package/src/write-audit.js +2 -1
- package/templates/agents-instruction.md +3 -3
package/src/quicklog.js
CHANGED
|
@@ -118,18 +118,74 @@ export function sanitizeDesc(description) {
|
|
|
118
118
|
return s.length > 120 ? s.slice(0, 120) + '…' : s
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
// ── 短标题提炼(2026-10-16-short-change-title,变更中心标题治理·工具侧)──
|
|
122
|
+
|
|
123
|
+
// 提炼参数:目标 20 字 / 上限 30 字 / 截点下限 6 字(更短的截点宁可不切——残词标题比
|
|
124
|
+
// 硬切长句更难读,如「修复,…」切出「修复」)。平台侧回填脚本须与此处口径对齐。
|
|
125
|
+
const SHORT_TITLE_DEFAULTS = Object.freeze({ target: 20, max: 30, floor: 6 })
|
|
126
|
+
// 全角/语义截点:无条件可切。ASCII 标点仅在后随空白/CJK/串尾时才算截点(防 3.5 / 1,000 /
|
|
127
|
+
// e.g. 误切——小数点后随数字是最常见误切源,rpc-budget 标题实证)。
|
|
128
|
+
const SHORT_TITLE_CJK_CUT = new Set([...'。!?;:、,…—~·'])
|
|
129
|
+
const SHORT_TITLE_ASCII_CUT = new Set([...'.,;:!?'])
|
|
130
|
+
const SHORT_TITLE_CJK_RE = /[\u3400-\u4dbf\u4e00-\u9fff]/
|
|
131
|
+
// 截断后尾部清理:剥句读尾标点(引号括号不入此集——「xxx」收尾是平衡结构不能剥);
|
|
132
|
+
// 再剥截点切进引号体时留下的悬空开口引号/括号。
|
|
133
|
+
const SHORT_TITLE_TRAIL_RE = /[\s。..!!??;;::,,、…—~~·]+$/u
|
|
134
|
+
const SHORT_TITLE_DANGLING_RE = /[「『(《"']+$/u
|
|
135
|
+
|
|
121
136
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* --title
|
|
137
|
+
* 短标题提炼(2026-10-16-short-change-title)。病根:deriveChangeTitle 旧口径对超长首行
|
|
138
|
+
* 50 字硬切加 …(残句难读),纯靠 agent 自觉写短首行(本地 20 条实证 7 条变更名兜底 +
|
|
139
|
+
* 8 条硬切残句)。提炼口径:超 max 长句在自然标点处截断——截点取「距 target 最近、
|
|
140
|
+
* 落在 [floor, max] 区间」者(同距取更短),截后剥尾部标点;无可用截点才硬切
|
|
141
|
+
* slice(0,max)+'…' 并标 hardCut=true(调用方可据此打 --title 软提示)。
|
|
142
|
+
* 返回 { title, hardCut };空输入 { title:'', hardCut:false }。
|
|
143
|
+
*/
|
|
144
|
+
export function distillShortTitleEx(raw, opts = {}) {
|
|
145
|
+
const { target, max, floor } = { ...SHORT_TITLE_DEFAULTS, ...opts }
|
|
146
|
+
const s = String(raw || '').replace(/\s+/g, ' ').trim()
|
|
147
|
+
if (!s) return { title: '', hardCut: false }
|
|
148
|
+
if (s.length <= max) {
|
|
149
|
+
return { title: s.replace(SHORT_TITLE_TRAIL_RE, ''), hardCut: false }
|
|
150
|
+
}
|
|
151
|
+
// 截点收集:候选标题=标点前缀(去重后按距 target 最近择优)
|
|
152
|
+
const seen = new Set()
|
|
153
|
+
const cands = []
|
|
154
|
+
for (let i = 0; i < s.length; i++) {
|
|
155
|
+
const ch = s[i]
|
|
156
|
+
let cut = SHORT_TITLE_CJK_CUT.has(ch)
|
|
157
|
+
if (!cut && SHORT_TITLE_ASCII_CUT.has(ch)) {
|
|
158
|
+
const nx = s[i + 1]
|
|
159
|
+
cut = nx === undefined || /\s/.test(nx) || SHORT_TITLE_CJK_RE.test(nx)
|
|
160
|
+
}
|
|
161
|
+
if (!cut) continue
|
|
162
|
+
const t = s.slice(0, i).replace(SHORT_TITLE_TRAIL_RE, '').replace(SHORT_TITLE_DANGLING_RE, '').trim()
|
|
163
|
+
if (t && !seen.has(t)) { seen.add(t); cands.push(t) }
|
|
164
|
+
}
|
|
165
|
+
const valid = cands.filter((t) => t.length >= floor && t.length <= max)
|
|
166
|
+
if (valid.length > 0) {
|
|
167
|
+
valid.sort((a, b) => (Math.abs(a.length - target) - Math.abs(b.length - target)) || (a.length - b.length))
|
|
168
|
+
return { title: valid[0], hardCut: false }
|
|
169
|
+
}
|
|
170
|
+
return { title: s.slice(0, max) + '…', hardCut: true }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** distillShortTitleEx 的字符串简版(不需要 hardCut 观测的调用方)。 */
|
|
174
|
+
export function distillShortTitle(raw, opts) {
|
|
175
|
+
return distillShortTitleEx(raw, opts).title
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 变更标题首行提取(不截断):取 --input 首个非空行(「成功标准:」引导行即止——那是验收
|
|
180
|
+
* 面不是标题素材),剥「需求:/动机:」类标签前缀,空白折叠单行。截断口径由此分治:
|
|
181
|
+
* deriveChangeTitle(≤50 硬切,模块摘要用)与 deriveShortChangeTitle(≤30 提炼,标题面用)
|
|
182
|
+
* 共享本提取,两个口径互不牵连。空输入返回 ''(调用方兜底变更名)。
|
|
127
183
|
* 段头行防御(2026-10-15-title-label-skip):过门格式示例曾被读成段头形态——agent 把
|
|
128
184
|
* 「动机与背景:」独立成行照抄,白名单无复合词致整行成标题(进度库 6 变更实证)。两道
|
|
129
185
|
* 防御:白名单补复合词(动机与背景/背景与动机);任意短段头行(≤12 字非冒号内容+行尾
|
|
130
186
|
* 冒号,如「变更背景:」「详细说明:」)整行跳过取次非空行——冒号后无内容不成标题。
|
|
131
187
|
*/
|
|
132
|
-
|
|
188
|
+
function deriveChangeTitleRaw(input) {
|
|
133
189
|
for (const l of String(input || '').replace(/\r\n/g, '\n').split('\n')) {
|
|
134
190
|
const t = l.trim()
|
|
135
191
|
if (!t) continue
|
|
@@ -137,12 +193,105 @@ export function deriveChangeTitle(input, max = 50) {
|
|
|
137
193
|
if (/^[^::]{1,12}[::]$/.test(t)) continue
|
|
138
194
|
const stripped = t.replace(/^(?:需求|动机|背景|任务|标题|动机与背景|背景与动机)\s*[::]\s*/, '').trim()
|
|
139
195
|
if (!stripped) continue
|
|
140
|
-
|
|
141
|
-
return one.length > max ? one.slice(0, max) + '…' : one
|
|
196
|
+
return stripped.replace(/\s+/g, ' ')
|
|
142
197
|
}
|
|
143
198
|
return ''
|
|
144
199
|
}
|
|
145
200
|
|
|
201
|
+
/**
|
|
202
|
+
* 变更标题推导(用户需求 2026-09-29:changes.title 应为中文描述,≤50 字、建议 ~20 字)。
|
|
203
|
+
* --title 显式覆盖一切推导。agent 在 --input 首行写一句中文概括即成标题(flow start 横幅
|
|
204
|
+
* 有指引)。**2026-10-16-short-change-title 起标题面改走 deriveShortChangeTitle 提炼口径
|
|
205
|
+
* (目标 20/上限 30,50 字硬切残句实证淘汰)**;本函数 ≤50 硬切口径保持不变——
|
|
206
|
+
* motiveFromInput 模块变更索引摘要与其同源(flow-parity),勿在此改截断行为。
|
|
207
|
+
*/
|
|
208
|
+
export function deriveChangeTitle(input, max = 50) {
|
|
209
|
+
const one = deriveChangeTitleRaw(input)
|
|
210
|
+
if (!one) return ''
|
|
211
|
+
return one.length > max ? one.slice(0, max) + '…' : one
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 变更标题推导·提炼口径(2026-10-16-short-change-title):deriveChangeTitleRaw 提取首行后
|
|
216
|
+
* 过 distillShortTitle——目标 20 字上限 30 字、自然标点截断、无截点才硬切。changes.title
|
|
217
|
+
* 落库面统一入口(flow start/resume/adopt、run initChange);--title 显式值由调用方直传,
|
|
218
|
+
* 不经本函数(用户原话不做二次加工)。
|
|
219
|
+
*/
|
|
220
|
+
export function deriveShortChangeTitleEx(input, opts) {
|
|
221
|
+
const raw = deriveChangeTitleRaw(input)
|
|
222
|
+
if (!raw) return { title: '', hardCut: false }
|
|
223
|
+
return distillShortTitleEx(raw, opts)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function deriveShortChangeTitle(input, opts) {
|
|
227
|
+
return deriveShortChangeTitleEx(input, opts).title
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── 标题硬区间门(2026-10-16-title-range-gate,用户硬性要求:10-30 字必须、建议 ~20、可强制命名)──
|
|
231
|
+
|
|
232
|
+
/** 标题是否落在硬区间(去首尾空白后按字符数计 10..30)。变更名兜底/短句/超长均 false。 */
|
|
233
|
+
export function isValidChangeTitle(title) {
|
|
234
|
+
const n = [...String(title || '').trim()].length
|
|
235
|
+
return n >= 10 && n <= 30
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* fresh 启动标题解析:--title 显式 > --input 首行提炼 > 变更名兜底。
|
|
240
|
+
* enforce=true(agent 命名时刻,flow start fresh 非平台 writer):不在区间/兜底 → ok=false,
|
|
241
|
+
* 调用方必须在任何状态写入前拒绝启动(强制 agent 命名)。
|
|
242
|
+
* enforce=false(平台 writer 预建空目录自动派发,无 agent 对话面可强制):恒 ok=true,
|
|
243
|
+
* 超界/兜底由调用方 ⚠️ 留痕(best-effort 标题优于阻断派发)。
|
|
244
|
+
*/
|
|
245
|
+
export function resolveStartTitle({ titleFlag, input, change, enforce = true }) {
|
|
246
|
+
if (titleFlag) {
|
|
247
|
+
return { title: titleFlag, ok: !enforce || isValidChangeTitle(titleFlag), kind: 'flag', hardCut: false }
|
|
248
|
+
}
|
|
249
|
+
const derived = deriveShortChangeTitleEx(input)
|
|
250
|
+
// enforce 下 hardCut(30 字+… 残句)与超区间同样拒绝:残句正是「让人认不出变更做什么」
|
|
251
|
+
// 的形态,CLI 提炼救不了就让 agent 自己命名(补标点/写短句/--title 三选一)。
|
|
252
|
+
// enforce=false(平台豁免)任何非空提炼值照用,超界/残句由调用方 ⚠️ 留痕。
|
|
253
|
+
if (derived.title && (!enforce || (!derived.hardCut && isValidChangeTitle(derived.title)))) {
|
|
254
|
+
return { title: derived.title, ok: true, kind: 'derived', hardCut: false }
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
title: change,
|
|
258
|
+
ok: !enforce,
|
|
259
|
+
kind: 'fallback',
|
|
260
|
+
hardCut: Boolean(derived && derived.hardCut),
|
|
261
|
+
rejectedTitle: (derived && derived.title) || null,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** hardCut 软提示(fresh/resume 共用单源)。文案口径=「6..30 字内无可用截点」——hardCut 判定
|
|
266
|
+
* 是候选截点经 [floor,max] 过滤后为空(标点存在但全在区间外也硬切),禁用「无自然断点」旧
|
|
267
|
+
* 措辞(2026-10-16-short-change-title 评审 P3 清偿)。 */
|
|
268
|
+
export const SHORT_TITLE_HARDCUT_HINT = '💡 标题超 30 字且 6..30 字内无可用自然标点截点,已硬切——重入 flow start --title "<10-30 字中文概括>" 可给更短标题(不阻断)'
|
|
269
|
+
|
|
270
|
+
/** 超界不阻断面的强提示行(adopt 收编/resume 补写/平台 writer 豁免共用单源)。 */
|
|
271
|
+
export function shortTitleRangeWarnLine(title) {
|
|
272
|
+
const n = [...String(title || '').trim()].length
|
|
273
|
+
return `⚠️ 标题「${title}」${n} 字不在 10-30 硬区间(建议 ~20 字)——重入 flow start --title "<10-30 字中文概括>" 可改(不阻断)`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** 硬门拒绝报错(flow start fresh exit 2 用)。状态/原因/修复一次给全(cli-copy-selfsufficient)。 */
|
|
277
|
+
export function shortTitleRejectMessage(title) {
|
|
278
|
+
const fix = [
|
|
279
|
+
' ① 重跑时把 --input 首行写成一句 10-30 字中文概括(首行即变更标题)',
|
|
280
|
+
' ② 或追加参数 --title "<10-30 字中文概括>"',
|
|
281
|
+
]
|
|
282
|
+
if (title == null || title === '') {
|
|
283
|
+
return [
|
|
284
|
+
'❌ 变更标题未命名——flow start 全新启动必须带 10-30 字标题(必须,建议 ~20 字):标题是变更中心展示面,须让人一眼认出本变更做什么。两条修复:',
|
|
285
|
+
...fix,
|
|
286
|
+
].join('\n')
|
|
287
|
+
}
|
|
288
|
+
const n = [...String(title).trim()].length
|
|
289
|
+
return [
|
|
290
|
+
`❌ 变更标题必须为 10-30 字(当前:${JSON.stringify(title)},${n} 字;必须,建议 ~20 字)——标题是变更中心展示面,须让人一眼认出本变更做什么。两条修复:`,
|
|
291
|
+
...fix,
|
|
292
|
+
].join('\n')
|
|
293
|
+
}
|
|
294
|
+
|
|
146
295
|
|
|
147
296
|
// ── 平台推送(best-effort,2026-08-16-change-center-quick-tab task-06 / D-003)──
|
|
148
297
|
|
|
@@ -337,6 +486,10 @@ function sanitizeQuicklogUser(user) {
|
|
|
337
486
|
// 从关联变更的 proposal/design 提取首个 # 标题(去「提案书(Proposal)— / 设计文档(Design)—」固定前缀)。
|
|
338
487
|
// 让 quick --linked-changes 启动时(用户常不带 --input)也能拿到语义标题,而非 (quick 任务) 占位。
|
|
339
488
|
// 读不到任何标题返回 ''(调用方再回退占位符,保持向后兼容)。
|
|
489
|
+
// 2026-10-16-short-change-title:① 返回前过提炼封顶 ≤30 字(超长 dash-desc 不直落标题);
|
|
490
|
+
// ② 两文档皆无语义标题(纯模板 H1,brainstorm 模板正文即「# 提案书(Proposal)」形态)时
|
|
491
|
+
// 回退读 proposal「## 动机」节首行过提炼——动机首句本是一句概括素材,旧口径放弃致标题
|
|
492
|
+
// 恒变更名兜底(平台变更中心 7/20 实证)。无动机节/无内容行仍返回 ''(空串不刷新契约不变)。
|
|
340
493
|
export function deriveTitleFromLinkedChange(specBase, change) {
|
|
341
494
|
if (!change) return ''
|
|
342
495
|
for (const f of ['proposal.md', 'design.md']) {
|
|
@@ -355,7 +508,39 @@ export function deriveTitleFromLinkedChange(specBase, change) {
|
|
|
355
508
|
if (!raw) continue
|
|
356
509
|
const dash = raw.match(/[—-]\s*(.+)$/)
|
|
357
510
|
const title = dash ? dash[1].trim() : raw
|
|
358
|
-
if (title) return title
|
|
511
|
+
if (title) return distillShortTitle(title)
|
|
512
|
+
}
|
|
513
|
+
const motive = extractMotivationFirstLine(specBase, change)
|
|
514
|
+
return motive ? distillShortTitle(motive) : ''
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// proposal「## 动机」节首个内容行(动机回退素材,2026-10-16-short-change-title)。
|
|
518
|
+
// 节标题形态对齐 stage-templates(## 动机)与 flow-draft SECTION_HEAD_RE:2-6 级标题行含
|
|
519
|
+
// 「动机」即命中(H1 本身是标题不参与)。内容行跳过空行/表格分隔行/HTML 注释(AGENT 槽)/
|
|
520
|
+
// 模板占位行(「为什么做、解决什么核心问题」),剥 markdown bullet 与「任务原话转写:」
|
|
521
|
+
// 前缀(flow-draft 机器稿动机段固定前缀,剥后即 input 首行,与 deriveShortChangeTitle 同源)。
|
|
522
|
+
// 读不到文件/无节/无合格内容行返回 ''。
|
|
523
|
+
function extractMotivationFirstLine(specBase, change) {
|
|
524
|
+
let content
|
|
525
|
+
try { content = readFileSync(join(specBase, 'changes', change, 'proposal.md'), 'utf8') } catch { return '' }
|
|
526
|
+
let inMotivation = false
|
|
527
|
+
for (const line of String(content).replace(/\r\n/g, '\n').split('\n')) {
|
|
528
|
+
const t = line.trim()
|
|
529
|
+
if (/^#{2,6}\s/.test(t)) {
|
|
530
|
+
if (inMotivation) return '' // 动机节内直到下一节标题都无合格内容行
|
|
531
|
+
inMotivation = /^#{2,6}\s*(?:\d+(?:[.、)]\s*)?)?[^\n]*动机/.test(t)
|
|
532
|
+
continue
|
|
533
|
+
}
|
|
534
|
+
if (!inMotivation || !t) continue
|
|
535
|
+
if (/^[\s|:-]+$/.test(t)) continue // 表格分隔/装饰行
|
|
536
|
+
if (/^<!--/.test(t)) continue // AGENT 槽等 HTML 注释
|
|
537
|
+
const stripped = t
|
|
538
|
+
.replace(/^(?:[-*+]|\d+[.、)])\s+/, '')
|
|
539
|
+
.replace(/^>\s*/, '')
|
|
540
|
+
.replace(/^任务原话转写[::]\s*/, '')
|
|
541
|
+
.trim()
|
|
542
|
+
if (!stripped || stripped === '(未提供 --input)' || stripped === '为什么做、解决什么核心问题') continue
|
|
543
|
+
return stripped
|
|
359
544
|
}
|
|
360
545
|
return ''
|
|
361
546
|
}
|
package/src/review-dispatch.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
|
|
21
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
21
22
|
import { join, dirname } from 'node:path'
|
|
22
23
|
import jsYaml from 'js-yaml'
|
|
23
24
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
@@ -41,7 +42,7 @@ const DISPATCH_STATES = ['dispatching', 'in-flight', 'completed', 'failed', 'aba
|
|
|
41
42
|
|
|
42
43
|
/** 在途记录路径:{SPEC_ROOT}/.runtime/review-dispatch-<change>.json */
|
|
43
44
|
function dispatchRecordPath(specBase, changeName) {
|
|
44
|
-
return join(specBase, '.runtime', `review-dispatch-${changeName}.json`)
|
|
45
|
+
return runtimeStatePath(join(specBase, '.runtime'), `review-dispatch-${changeName}.json`)
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/** 读在途记录(无/坏 → null,fail-open 调用方按无在途处理)。 */
|
|
@@ -459,7 +460,7 @@ function appendShadowLedgerSkip(runtimeRoot, skip) {
|
|
|
459
460
|
*/
|
|
460
461
|
function readCeremonyTierForShadow(runtimeRoot, changeName) {
|
|
461
462
|
try {
|
|
462
|
-
const p =
|
|
463
|
+
const p = runtimeStatePath(runtimeRoot, `ceremony-tier-${changeName}.json`)
|
|
463
464
|
if (!existsSync(p)) return null
|
|
464
465
|
const doc = JSON.parse(readFileSync(p, 'utf8'))
|
|
465
466
|
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return null
|
|
@@ -480,7 +481,7 @@ function generateShadowReviewRunId() {
|
|
|
480
481
|
// stage-review.js:751 的主线在途提示内联读的也是主线文件名,不会被影子记录污染)──
|
|
481
482
|
|
|
482
483
|
function shadowRecordPath(specBase, changeName) {
|
|
483
|
-
return join(specBase, '.runtime', `review-dispatch-shadow-${changeName}.json`)
|
|
484
|
+
return runtimeStatePath(join(specBase, '.runtime'), `review-dispatch-shadow-${changeName}.json`)
|
|
484
485
|
}
|
|
485
486
|
|
|
486
487
|
function readShadowRecord(specBase, changeName) {
|
package/src/review-tier.js
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { parseFileChangeList } from './change-list.js'
|
|
38
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
38
39
|
import { computeCeremonyTier, CEREMONY_TIERS } from './ceremony-tier.js'
|
|
39
40
|
import { readCeremonyPricingConfig } from './ceremony-config.js'
|
|
40
41
|
import { resolveChangeRisk, extractExplicitRiskLevel } from './change-risk-profile.js'
|
|
@@ -151,7 +152,7 @@ export function classifyReviewTier({ planLevel, designPath, riskDetection, frict
|
|
|
151
152
|
if (designPath) {
|
|
152
153
|
try {
|
|
153
154
|
const changeDir = dirname(designPath)
|
|
154
|
-
const tierPath = join(dirname(dirname(changeDir)), '.runtime', `ceremony-tier-${basename(changeDir)}.json`)
|
|
155
|
+
const tierPath = runtimeStatePath(join(dirname(dirname(changeDir)), '.runtime'), `ceremony-tier-${basename(changeDir)}.json`)
|
|
155
156
|
if (existsSync(tierPath)) {
|
|
156
157
|
const tf = JSON.parse(readFileSync(tierPath, 'utf8'))
|
|
157
158
|
if (tf && CEREMONY_TIERS.includes(tf.tier)) tierFileTier = tf.tier
|
package/src/route-hindsight.js
CHANGED
|
@@ -37,6 +37,7 @@ import { createHash } from 'node:crypto'
|
|
|
37
37
|
import { join, basename, dirname } from 'node:path'
|
|
38
38
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
39
39
|
import { computeEditRatio } from './flow-draft.js'
|
|
40
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
40
41
|
|
|
41
42
|
/** 超阈判定常量(可调;口径:比例两路为严格大于——恰等于阈值不算超,保守防误标 R-01;
|
|
42
43
|
* 计数两路为大于等于——2 维/2 次即系统性信号)。 */
|
|
@@ -107,7 +108,7 @@ function num(v) {
|
|
|
107
108
|
export function snapshotBaseline({ specBase, change, changeDir }) {
|
|
108
109
|
try {
|
|
109
110
|
const runtimeRoot = join(specBase, '.runtime')
|
|
110
|
-
const p =
|
|
111
|
+
const p = runtimeStatePath(runtimeRoot, `${BASELINE_PREFIX}${change}.json`)
|
|
111
112
|
if (existsSync(p)) return { captured: false, reason: 'already-exists(首写者胜)' }
|
|
112
113
|
const payload = {
|
|
113
114
|
schemaVersion: 1,
|
|
@@ -130,7 +131,7 @@ export function snapshotBaseline({ specBase, change, changeDir }) {
|
|
|
130
131
|
*/
|
|
131
132
|
export function baselineSha256({ specBase, change }) {
|
|
132
133
|
try {
|
|
133
|
-
const p = join(specBase, '.runtime', `${BASELINE_PREFIX}${change}.json`)
|
|
134
|
+
const p = runtimeStatePath(join(specBase, '.runtime'), `${BASELINE_PREFIX}${change}.json`)
|
|
134
135
|
if (!existsSync(p)) return null
|
|
135
136
|
return createHash('sha256').update(readTextSafe(p)).digest('hex')
|
|
136
137
|
} catch {
|
|
@@ -161,7 +162,7 @@ export function computeHindsightMetrics({ changeDir, reviewJson, flowState }) {
|
|
|
161
162
|
// ① 首版快照 → 终稿行级改写比
|
|
162
163
|
let designRewriteRatio = 0
|
|
163
164
|
let tasksRewriteRatio = 0
|
|
164
|
-
const baselineRaw = readTextSafe(join(specBase, '.runtime', `${BASELINE_PREFIX}${change}.json`))
|
|
165
|
+
const baselineRaw = readTextSafe(runtimeStatePath(join(specBase, '.runtime'), `${BASELINE_PREFIX}${change}.json`))
|
|
165
166
|
let baseline = null
|
|
166
167
|
if (baselineRaw != null) { try { const j = JSON.parse(baselineRaw); baseline = j && typeof j === 'object' ? j : null } catch { baseline = null } }
|
|
167
168
|
if (baseline) {
|
package/src/run/bg-sync.js
CHANGED
|
@@ -36,12 +36,18 @@
|
|
|
36
36
|
* 刻意不赌);父进程内 shared.js 已加载,动态 import 走缓存无额外成本。
|
|
37
37
|
*/
|
|
38
38
|
import { spawn } from 'child_process';
|
|
39
|
+
import { runtimeTempPath } from '../runtime-paths.js'
|
|
39
40
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'fs';
|
|
40
41
|
import { join } from 'path';
|
|
41
42
|
|
|
42
43
|
export const BG_LOCK_FILENAME = 'spec-sync-bg.lock';
|
|
43
44
|
export const BG_LOG_FILENAME = 'spec-sync-bg.log';
|
|
44
45
|
|
|
46
|
+
/** 后台同步日志路径(2026-10-18 布局分层:可随时删除的产物归 temp/)。 */
|
|
47
|
+
export function bgSyncLogPath(runtimeRoot) {
|
|
48
|
+
return runtimeTempPath(runtimeRoot, BG_LOG_FILENAME);
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
// 后台轮参数:单轮预算下限(与 env SILLYSPEC_SYNC_TIMEOUT_MS 取 max)、轮数与总预算帽
|
|
46
52
|
// (防慢平台下无限续轮);锁时效(pid 活性之外的第二道判死——pid 复用假活兜底,
|
|
47
53
|
// 子进程自身总预算 ~2.5min + 收尾余量远小于该值,超时必是异常残留)。
|
|
@@ -108,7 +114,7 @@ export function isBgSyncLockLive(lock, now = Date.now()) {
|
|
|
108
114
|
/** 子进程起步截尾日志(>1MB 保后 512KB,从整行边界起)。失败只影响日志体积。 */
|
|
109
115
|
function truncateLogIfNeeded(runtimeRoot) {
|
|
110
116
|
try {
|
|
111
|
-
const p =
|
|
117
|
+
const p = bgSyncLogPath(runtimeRoot);
|
|
112
118
|
if (statSync(p).size <= LOG_TRUNCATE_THRESHOLD) return;
|
|
113
119
|
const buf = readFileSync(p);
|
|
114
120
|
const keep = buf.subarray(Math.max(0, buf.length - LOG_TRUNCATE_KEEP));
|
|
@@ -143,15 +149,16 @@ export async function spawnBackgroundSync(cwd, changeName, platformOpts = {}, op
|
|
|
143
149
|
// 单飞合并:活后台轮在跑,把「还有新状态要同步」并入其下一轮。置位失败(并发写锁)
|
|
144
150
|
// 只损失合并信号——后台轮收尾锁自清,下一条命令重新 spawn,收敛不丢。
|
|
145
151
|
try { writeBgSyncLock(runtimeRoot, { ...lock, rerunQueued: true }); } catch { /* 见上 */ }
|
|
146
|
-
return { status: 'coalesced', logPath:
|
|
152
|
+
return { status: 'coalesced', logPath: bgSyncLogPath(runtimeRoot) };
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
// 日志 fd:子进程 stdout/stderr 直接 append 进日志文件(跨平台 fd 继承);开不了退 ignore
|
|
150
|
-
const logPath =
|
|
156
|
+
const logPath = bgSyncLogPath(runtimeRoot);
|
|
151
157
|
let stdio = ['ignore', 'ignore', 'ignore'];
|
|
152
158
|
let logFd = null;
|
|
153
159
|
try {
|
|
154
160
|
mkdirSync(runtimeRoot, { recursive: true });
|
|
161
|
+
mkdirSync(join(runtimeRoot, 'temp'), { recursive: true }); // temp/ 可被整体删除,写前重建
|
|
155
162
|
logFd = openSync(logPath, 'a');
|
|
156
163
|
stdio = ['ignore', logFd, logFd];
|
|
157
164
|
} catch {
|
package/src/run/command.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* './run/stage.js' → './shared.js' 等同级
|
|
21
21
|
*/
|
|
22
22
|
import { basename, join, resolve, dirname } from 'node:path'
|
|
23
|
+
import { runtimeStatePath } from '../runtime-paths.js'
|
|
23
24
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync, unlinkSync, statSync } from 'node:fs'
|
|
24
25
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
25
26
|
import { fileURLToPath } from 'node:url'
|
|
@@ -29,7 +30,7 @@ import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
|
29
30
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
30
31
|
import { completeStep, completeStepBurst, skipStep, waitStep, continueStep, synthesizeStepOutput } from './complete.js'
|
|
31
32
|
import { runStage } from './stage.js'
|
|
32
|
-
import { sanitizeDesc,
|
|
33
|
+
import { sanitizeDesc, deriveShortChangeTitle } from '../quicklog.js'
|
|
33
34
|
import { ProgressManager, resolveSessionIdentity } from '../progress.js'
|
|
34
35
|
import { validateChangeExists, checkTransition } from '../stage-contract.js'
|
|
35
36
|
import { READONLY_AUXILIARY_STAGES } from '../constants.js'
|
|
@@ -232,7 +233,7 @@ export async function ensureStageSteps(progress, stageName, cwd, specDir = null)
|
|
|
232
233
|
function emitReadonlyEmptyGuide() {
|
|
233
234
|
console.log('ℹ️ 未找到进度数据(只读查询不建变更)')
|
|
234
235
|
console.log(' → 可开始轻量变更(--input 引号内换行合法,下例可照抄):')
|
|
235
|
-
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
236
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
|
|
236
237
|
console.log('')
|
|
237
238
|
console.log(' 成功标准:')
|
|
238
239
|
console.log(' - <可验证标准>"')
|
|
@@ -858,7 +859,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
858
859
|
quickFallbackUsed = true
|
|
859
860
|
} else {
|
|
860
861
|
console.log(' 本仓无近期 quick 会话记录(current marker 与 quick-sessions 均空)——新工作请走轻量变更(--input 引号内换行合法,可照抄):')
|
|
861
|
-
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
862
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
|
|
862
863
|
console.log('')
|
|
863
864
|
console.log(' 成功标准:')
|
|
864
865
|
console.log(' - <可验证标准>"')
|
|
@@ -1301,11 +1302,12 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1301
1302
|
}
|
|
1302
1303
|
}
|
|
1303
1304
|
} catch { /* 守卫异常不拦正常注册(fail-open)*/ }
|
|
1304
|
-
// 创建时即写 title(与 name/KEY 同时落 db):--input
|
|
1305
|
-
//
|
|
1305
|
+
// 创建时即写 title(与 name/KEY 同时落 db):--input 首行过提炼 deriveShortChangeTitle
|
|
1306
|
+
// (目标 20/上限 30 自然标点截断;2026-10-16-short-change-title,50 字硬切残句淘汰)
|
|
1307
|
+
// 优先,长输入兜底 sanitizeDesc 压行,无则用 name 兜底。
|
|
1306
1308
|
// proposal 还没写、无权威标题来源,这是临时值;brainstorm/plan 完成 proposal/design 落盘后由
|
|
1307
1309
|
// complete.js 通用完成路径 deriveTitleFromLinkedChange 刷新为真实 # 标题(与 quick 启动 title 同源)。
|
|
1308
|
-
progress = pm.initChange(cwd, autoChange, { title: inputText ? (
|
|
1310
|
+
progress = pm.initChange(cwd, autoChange, { title: inputText ? (deriveShortChangeTitle(inputText) || sanitizeDesc(inputText)) : autoChange })
|
|
1309
1311
|
} else if (isAuxiliary) {
|
|
1310
1312
|
let autoName = changeName || resolveChangeNameAuto(cwd, specRoot) || 'default'
|
|
1311
1313
|
// archive 特例:归档后变更从活跃列表排除(listChanges WHERE status='active'),
|
|
@@ -1332,7 +1334,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1332
1334
|
changeName = autoName
|
|
1333
1335
|
if (!progress) {
|
|
1334
1336
|
// auxiliary 创建也写 title(--input 优先 / name 兜底),同上完整流程语义。
|
|
1335
|
-
progress = pm.initChange(cwd, autoName, { title: inputText ? (
|
|
1337
|
+
progress = pm.initChange(cwd, autoName, { title: inputText ? (deriveShortChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1336
1338
|
// initChange 可能因 project 表为空返回 null
|
|
1337
1339
|
if (!progress) {
|
|
1338
1340
|
progress = { currentStage: stageName, stages: {}, lastActive: new Date().toLocaleString('zh-CN', { hour12: false }), project: '' }
|
|
@@ -1354,7 +1356,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1354
1356
|
// initChangeIfNoActive 单语句原子抢占,败者走同一引导。
|
|
1355
1357
|
const date = new Date().toISOString().slice(0, 10)
|
|
1356
1358
|
const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
|
|
1357
|
-
const claim = pm.initChangeIfNoActive(cwd, autoName, { title: inputText ? (
|
|
1359
|
+
const claim = pm.initChangeIfNoActive(cwd, autoName, { title: inputText ? (deriveShortChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1358
1360
|
if (!claim.created) {
|
|
1359
1361
|
const activeChanges = claim.activeChanges || []
|
|
1360
1362
|
console.error(`❌ 已存在 ${activeChanges.length} 个活跃变更,run brainstorm 无 --change 不会自动创建新变更。`)
|
|
@@ -1377,7 +1379,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1377
1379
|
// 正是并发双建的温床)——1 活跃现与多活跃同走 exit 2 引导。
|
|
1378
1380
|
const date = new Date().toISOString().slice(0, 10)
|
|
1379
1381
|
const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
|
|
1380
|
-
const claim = pm.initChangeIfNoActive(cwd, autoName, { title: inputText ? (
|
|
1382
|
+
const claim = pm.initChangeIfNoActive(cwd, autoName, { title: inputText ? (deriveShortChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1381
1383
|
if (!claim.created) {
|
|
1382
1384
|
const activeCount = claim.activeChanges || []
|
|
1383
1385
|
console.error(`❌ 已存在 ${activeCount.length} 个活跃变更,run auto 无法自动定位目标。`)
|
|
@@ -1452,7 +1454,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1452
1454
|
if (!flags.includes('--upgrade-thick')) {
|
|
1453
1455
|
console.error(`⛔ 本变更在轻量跑道上(flow-state 在场)——升厚走完整流程需用户同意,agent 不得自行转道:`)
|
|
1454
1456
|
console.error(` · 征得用户同意后带 --upgrade-thick 重跑本命令(落痕留档,首次转厚即转)`)
|
|
1455
|
-
console.error(` · 用户未确认或选择轻量跑 →
|
|
1457
|
+
console.error(` · 用户未确认或选择轻量跑 → 继续轻量变更:先答 spec 面并过批准再干活(未批准先 flow approve),收口 sillyspec flow done --change ${effectiveChange}`)
|
|
1456
1458
|
process.exit(2)
|
|
1457
1459
|
}
|
|
1458
1460
|
writeFlowState(changeDir, { legacy_fallback: true, upgraded_by_consent: new Date().toISOString() })
|
|
@@ -2076,7 +2078,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
2076
2078
|
if (classification.mode === 'quick') {
|
|
2077
2079
|
console.log(`📊 auto 模式分类:${classification.reason} 判为小变更`)
|
|
2078
2080
|
console.log(` 小变更请走轻量变更(quick 通道已退役,v3.30.0 起拒绝新会话;--input 引号内换行合法,可照抄):`)
|
|
2079
|
-
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
2081
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
|
|
2080
2082
|
console.log('')
|
|
2081
2083
|
console.log(' 成功标准:')
|
|
2082
2084
|
console.log(' - <可验证标准>"')
|
|
@@ -2382,7 +2384,7 @@ function printAutoCompletionSummary(pm, cwd, changeName, flowStages, specBase) {
|
|
|
2382
2384
|
// last-delta 模块清单(sidecar 缺失跳过)
|
|
2383
2385
|
try {
|
|
2384
2386
|
const runtimeRoot = join((specBase || join(cwd, '.sillyspec')), '.runtime')
|
|
2385
|
-
const sidecarPath =
|
|
2387
|
+
const sidecarPath = runtimeStatePath(runtimeRoot, 'last-delta.json')
|
|
2386
2388
|
if (existsSync(sidecarPath)) {
|
|
2387
2389
|
const sidecar = JSON.parse(readFileSync(sidecarPath, 'utf8'))
|
|
2388
2390
|
if (Array.isArray(sidecar.affectedModules) && sidecar.affectedModules.length > 0) {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* (非被外层吞);process.exit 不可被 try 捕获 → 搬迁行为完全等价。
|
|
31
31
|
*/
|
|
32
32
|
import { basename, dirname, join, resolve, relative, isAbsolute } from 'node:path'
|
|
33
|
+
import { runtimeStatePath, runtimeStateFileNames } from '../runtime-paths.js'
|
|
33
34
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync, renameSync } from 'node:fs'
|
|
34
35
|
import { createHash } from 'node:crypto'
|
|
35
36
|
import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
|
|
@@ -161,8 +162,9 @@ function ownerFromReviewedFiles(reviewedFiles) {
|
|
|
161
162
|
* 归档 / change-delete 无 delta 同样可删。
|
|
162
163
|
*
|
|
163
164
|
* 归属 fail-closed(禁 mtime 猜、禁后缀匹配):
|
|
164
|
-
*
|
|
165
|
-
*
|
|
165
|
+
* state/ 全家族散文件 + 根目录同名兜底(runtime-paths.js runtimeStateFileNames 唯一清单:
|
|
166
|
+
* draft-ledger / watcher 三件套 / test 台账 / ceremony 档位 / 冲突标记 / marker 等,
|
|
167
|
+
* 含 apply-pathspec-<change>.txt、prompt-inject-<change>.json 两个历史登记家族)
|
|
166
168
|
* execute-runs/<runId>/ 仅当 `change` 戳全等(无戳旧 run 留下,不按覆盖度启发式删)
|
|
167
169
|
* stage-reviews/<dir>/ 仅当 review.json reviewedFiles 首段 === changeName
|
|
168
170
|
* verify-runs/<ts>/ 仅当目录内 JSON 的 change 字段集合 size=1 且等于本变更
|
|
@@ -193,33 +195,26 @@ export function pruneArchivedChangeRuntime(runtimeRoot, changeName) {
|
|
|
193
195
|
} catch { /* 单份失败不连坐,下次归档/删变更再收 */ }
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
// 2026-10-18 布局分层:per-change 散文件全部落 state/,回收名单从三家族扩到全家族
|
|
199
|
+
// (runtime-paths.js STATE_FAMILIES 唯一清单源,禁 glob 的精确名形态不变)。
|
|
200
|
+
// 每个精确名试双路径:state/(新布局)+ 根目录(未迁移旧仓/混版本迟到文件兜底——
|
|
201
|
+
// 迁移幂等收敛后根路径命中恒为零)。friction-tally 删前残余滚台账语义保留(见函数头)。
|
|
196
202
|
try {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
// 删前残余先滚台账(tax-governance FR-02 兜底滚动点,见函数头注释)。
|
|
213
|
-
try {
|
|
214
|
-
const fp = join(runtimeRoot, `friction-tally-${changeName}.json`)
|
|
215
|
-
if (existsSync(fp)) {
|
|
216
|
-
const merged = mergeFrictionEntrySync(runtimeRoot, {
|
|
217
|
-
change: changeName,
|
|
218
|
-
counts: residualFrictionCounts(fp),
|
|
219
|
-
archivedAt: new Date().toISOString(),
|
|
220
|
-
})
|
|
221
|
-
ledgerAppend = !!(merged && merged.ok && !merged.skipped)
|
|
222
|
-
gone(fp)
|
|
203
|
+
for (const name of runtimeStateFileNames(changeName)) {
|
|
204
|
+
for (const p of [runtimeStatePath(runtimeRoot, name), join(runtimeRoot, name)]) {
|
|
205
|
+
try {
|
|
206
|
+
if (!existsSync(p)) continue
|
|
207
|
+
if (name === `friction-tally-${changeName}.json`) {
|
|
208
|
+
const merged = mergeFrictionEntrySync(runtimeRoot, {
|
|
209
|
+
change: changeName,
|
|
210
|
+
counts: residualFrictionCounts(p),
|
|
211
|
+
archivedAt: new Date().toISOString(),
|
|
212
|
+
})
|
|
213
|
+
ledgerAppend = !!(merged && merged.ok && !merged.skipped)
|
|
214
|
+
}
|
|
215
|
+
gone(p)
|
|
216
|
+
} catch { /* 单路径失败不连坐 */ }
|
|
217
|
+
}
|
|
223
218
|
}
|
|
224
219
|
} catch {}
|
|
225
220
|
|
|
@@ -320,15 +315,22 @@ export async function archiveWorktreeCleanup(cwd, archiveChangeName, specBase, p
|
|
|
320
315
|
// 手工短名变更(如 login)时误删自动日期前缀变更(2026-08-01-login 天然以 '-login' 结尾)
|
|
321
316
|
// 的 marker。stage 集合与写入侧 stageReviewMarkerPath 的调用方一致。
|
|
322
317
|
const REVIEW_STAGES = ['brainstorm', 'plan', 'execute', 'verify', 'archive']
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
try {
|
|
318
|
+
// 2026-10-18 布局分层后 marker 落 state/;根目录扫描保留为旧布局兜底(迁移幂等收敛)
|
|
319
|
+
const markerDirs = [join(runtimeRoot, 'state'), runtimeRoot]
|
|
320
|
+
let markerCleaned = 0
|
|
321
|
+
for (const markerDir of markerDirs) {
|
|
322
|
+
let names = []
|
|
323
|
+
try { names = readdirSync(markerDir) } catch { continue }
|
|
324
|
+
const markers = names.filter((f) =>
|
|
325
|
+
f === `current-execute-run-id-${archiveChangeName}` ||
|
|
326
|
+
REVIEW_STAGES.some(st => f === `current-stage-review-run-id-${st}-${archiveChangeName}`),
|
|
327
|
+
)
|
|
328
|
+
for (const m of markers) {
|
|
329
|
+
try { unlinkSync(join(markerDir, m)); markerCleaned++ } catch {}
|
|
330
|
+
}
|
|
329
331
|
}
|
|
330
|
-
if (
|
|
331
|
-
console.log(`🧹 归档清理 ${
|
|
332
|
+
if (markerCleaned > 0) {
|
|
333
|
+
console.log(`🧹 归档清理 ${markerCleaned} 个 runId marker(execute/stage-review)`)
|
|
332
334
|
}
|
|
333
335
|
// 取证回收必须在下方 worktree 块的 `if (!meta) return` 之前:无 meta 早退是常态
|
|
334
336
|
// (apply 已 cleanup),漏接就会让 execute-runs/verify-runs 继续按变更数累积。
|
|
@@ -2803,7 +2805,8 @@ export async function handleExecuteWorktreeCleanup({ stageName, changeName, cwd
|
|
|
2803
2805
|
try {
|
|
2804
2806
|
const runtimeDir = join(cwd, '.sillyspec', '.runtime');
|
|
2805
2807
|
mkdirSync(runtimeDir, { recursive: true });
|
|
2806
|
-
|
|
2808
|
+
try { mkdirSync(join(runtimeDir, 'state'), { recursive: true }); } catch { /* 回执 fail-open */ }
|
|
2809
|
+
writeFileSync(runtimeStatePath(runtimeDir, `execute-cleanup-${changeName}.json`), JSON.stringify({
|
|
2807
2810
|
change: changeName, cleanedAt: new Date().toISOString(),
|
|
2808
2811
|
reason: 'execute-completed-no-unapplied-changes',
|
|
2809
2812
|
}, null, 2) + '\n', 'utf8');
|