sillyspec 3.25.7 → 3.25.8
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/package.json +1 -1
- package/src/progress/consistency-doctor.js +41 -2
- package/src/progress/stage-machine.js +3 -2
- package/src/quicklog.js +51 -3
- package/src/run/complete.js +17 -0
- package/src/run/gates.js +2 -0
- package/src/stage-review.js +22 -1
- package/src/stages/archive.js +3 -0
- package/src/stages/execute.js +7 -0
- package/src/stages/plan-postcheck.js +5 -0
- package/src/stages/plan.js +1 -0
- package/src/stages/verify.js +10 -13
- package/src/task-review.js +35 -16
- package/src/worktree-apply.js +25 -3
- package/templates/claude-instruction.md +1 -1
- package/templates/prompts/verify-probes.md +1 -0
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// 从 src/progress.js 单体 ProgressManager 抽出。持有 pm 引用(构造注入),调 pm.read / pm._write /
|
|
3
3
|
// pm._ensureRuntimeDir / pm._runtimePath(persistence-core 留 facade 本体)。
|
|
4
4
|
import { appendFileSync } from 'fs';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
5
6
|
import { STAGE_ORDER } from './shared.js';
|
|
6
7
|
|
|
7
8
|
export class ConsistencyDoctor {
|
|
@@ -193,8 +194,46 @@ export class ConsistencyDoctor {
|
|
|
193
194
|
});
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
//
|
|
197
|
-
|
|
197
|
+
// Fix e: execute 阶段 completed stage 有 pending/stale/in-progress step,但 review.json 客观产出已全通过 → 状态脱钩自动修
|
|
198
|
+
// (坑 verify-archive-flow-pitfalls 坑1+坑5:plan 加 Wave / execute Wave step 未走 --done,但 task 实际有 review.json verdict 且非 fail)
|
|
199
|
+
// 安全边界:仅当 changeName 有效、source=review.json(客观源可用)且 pending=0(所有 task verdict 通过)才自动修,
|
|
200
|
+
// 否则回落到 Manual a(保守不动)。不碰非 execute 阶段。
|
|
201
|
+
let executeAutoFixed = false;
|
|
202
|
+
if (stageName === 'execute' && changeName && sd.status === 'completed' && sd.steps) {
|
|
203
|
+
const badSteps = sd.steps.filter(st => ['pending', 'stale', 'in-progress'].includes(st.status));
|
|
204
|
+
if (badSteps.length > 0) {
|
|
205
|
+
try {
|
|
206
|
+
const changeDir = this.pm._changePath(cwd, changeName);
|
|
207
|
+
if (changeDir && existsSync(changeDir)) {
|
|
208
|
+
const runtimeRoot = this.pm._runtimePath(cwd);
|
|
209
|
+
const { summarizeTaskCompletion } = await import('../task-review.js');
|
|
210
|
+
const summary = summarizeTaskCompletion({ changeDir, runtimeRoot, changeName });
|
|
211
|
+
if (summary.source === 'review.json' && summary.total > 0 && summary.pending.length === 0) {
|
|
212
|
+
const desc = `execute: ${badSteps.length} 个 step 状态脱钩(${badSteps.map(st => st.name).join(', ')})——review.json 客观产出全通过(${summary.completed}/${summary.total}),自动标 completed`;
|
|
213
|
+
fixable.push({
|
|
214
|
+
stage: stageName,
|
|
215
|
+
action: 'align_execute_steps_to_reviews',
|
|
216
|
+
description: desc,
|
|
217
|
+
apply: (d) => {
|
|
218
|
+
for (const st of d.stages[stageName].steps) {
|
|
219
|
+
if (['pending', 'stale', 'in-progress'].includes(st.status)) {
|
|
220
|
+
st.status = 'completed';
|
|
221
|
+
st.completedAt = st.completedAt || now;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
executeAutoFixed = true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch (e) {
|
|
230
|
+
console.warn(`⚠️ execute 产出核验异常(回落 manual): ${e.message}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Manual a: completed stage 里有 pending/stale/in-progress steps(execute 已由 Fix e 自动修则跳过)
|
|
236
|
+
if (!executeAutoFixed && sd.status === 'completed' && sd.steps) {
|
|
198
237
|
const badSteps = sd.steps.filter(s => ['pending', 'stale', 'in-progress'].includes(s.status));
|
|
199
238
|
for (const step of badSteps) {
|
|
200
239
|
manual.push(`${stageName}/${step.name}: step 状态为 ${step.status},但 stage 状态为 completed(需手动确认)`);
|
|
@@ -97,7 +97,7 @@ export class StageMachine {
|
|
|
97
97
|
if (stageRow && stageRow.length > 0 && stageRow[0].values.length > 0) {
|
|
98
98
|
const stageId = stageRow[0].values[0][0];
|
|
99
99
|
sqlDb.run(
|
|
100
|
-
'UPDATE steps SET status = "completed", completed_at = ? WHERE stage_id = ? AND status
|
|
100
|
+
'UPDATE steps SET status = "completed", completed_at = ? WHERE stage_id = ? AND status IN ("pending", "stale")', // stale 一并回填(reopen --done 场景,坑 brainstorm-reopen-step-state-desync)
|
|
101
101
|
[now, stageId]
|
|
102
102
|
);
|
|
103
103
|
}
|
|
@@ -295,11 +295,12 @@ export class StageMachine {
|
|
|
295
295
|
// 改为:不要求 steps(空=该开始了);upstream 必须全 completed(pending 不算就绪)——
|
|
296
296
|
// archive 只在 scan/brainstorm/plan/execute/verify 全完成后才可能被推荐。
|
|
297
297
|
for (const s of STAGE_ORDER) {
|
|
298
|
+
if (s === 'scan') continue // scan 是 auxiliary(按需显式跑),不作"下一步"推荐——否则未 completed 时恒处 STAGE_ORDER 首位且 upstream 空→恒就绪→误推 scan(回头路,与 complete.js brainstorm/quick 专属分支同类根因)。prompt-control-debt plan-c。
|
|
298
299
|
const sd = data.stages[s];
|
|
299
300
|
if (!sd) continue;
|
|
300
301
|
if (sd.status === 'completed' || sd.status === 'skipped') continue;
|
|
301
302
|
const idx = STAGE_ORDER.indexOf(s);
|
|
302
|
-
const upstream = STAGE_ORDER.slice(0, idx);
|
|
303
|
+
const upstream = STAGE_ORDER.slice(0, idx).filter(us => us !== 'scan'); // scan(auxiliary)不算上游——否则 scan 未 completed 会阻塞所有主流程阶段,与上面跳过 scan 的迭代矛盾
|
|
303
304
|
const upstreamOk = upstream.every(us => data.stages[us]?.status === 'completed');
|
|
304
305
|
if (upstreamOk) {
|
|
305
306
|
return {
|
package/src/quicklog.js
CHANGED
|
@@ -248,6 +248,42 @@ async function checkTaskCheckbox(specBase, change, qlId) {
|
|
|
248
248
|
// 可能是 CRLF,split('\n') 后每行带 \r。原代码 `lines[i] === '状态:进行中'` 精确匹配
|
|
249
249
|
// 恒失败 → 走 splice「兜底插入」→ 条目内同时出现「状态:已完成」+「状态:进行中」。
|
|
250
250
|
// 状态行匹配与「已完成」判断统一用行首前缀匹配,容忍行尾 \r;写入保持 CRLF 不扩大改动。
|
|
251
|
+
// 单行四字段(需求:/根因:/方案:/结果:)切段。先按「字段边界」严格扫描:真实标签 = 上一标签之后首次
|
|
252
|
+
// 出现的、前导是串首/空白/句末标点(。;!?)的对应标签——字段正文引用标签字样(根因写「双层「结果:」
|
|
253
|
+
// 前缀」或正则 split(/(?=需求:|根因:|方案:|结果:)/))因前导是「/|( 等非边界字符而跳过。严格失败
|
|
254
|
+
// (如真实标签前导是「,」这类弱标点)退回宽松顺序扫描(上一标签之后首次出现)。缺标签返回 null → 落单行
|
|
255
|
+
// 兜底(--done 契约校验仍会拦缺字段)。残余边界:正文引用标签且前导恰好是空白/句末标点时仍可能错位。
|
|
256
|
+
function isFieldBoundary(body, idx) {
|
|
257
|
+
if (idx <= 0) return true
|
|
258
|
+
const prev = body[idx - 1]
|
|
259
|
+
return /\s/.test(prev) || '。;!?'.includes(prev)
|
|
260
|
+
}
|
|
261
|
+
function findBoundaryLabel(body, label, from) {
|
|
262
|
+
let idx = body.indexOf(label, from)
|
|
263
|
+
while (idx !== -1 && !isFieldBoundary(body, idx)) {
|
|
264
|
+
idx = body.indexOf(label, idx + label.length)
|
|
265
|
+
}
|
|
266
|
+
return idx
|
|
267
|
+
}
|
|
268
|
+
function scanFields(body, findLabel) {
|
|
269
|
+
const labels = ['需求:', '根因:', '方案:', '结果:']
|
|
270
|
+
const positions = []
|
|
271
|
+
let cursor = 0
|
|
272
|
+
for (const label of labels) {
|
|
273
|
+
const idx = findLabel(body, label, cursor)
|
|
274
|
+
if (idx === -1) return null
|
|
275
|
+
positions.push(idx)
|
|
276
|
+
cursor = idx + label.length
|
|
277
|
+
}
|
|
278
|
+
return positions.map((start, i) => {
|
|
279
|
+
const end = i + 1 < positions.length ? positions[i + 1] : body.length
|
|
280
|
+
return body.slice(start, end).trim()
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
function splitSingleLineFields(body) {
|
|
284
|
+
return scanFields(body, findBoundaryLabel) ?? scanFields(body, (b, l, f) => b.indexOf(l, f))
|
|
285
|
+
}
|
|
286
|
+
|
|
251
287
|
function flipEntryInContent(content, qlId, result, changedFiles = []) {
|
|
252
288
|
const lines = content.split('\n')
|
|
253
289
|
const startIdx = lines.findIndex(l => l.startsWith(`## ${qlId} |`))
|
|
@@ -280,13 +316,25 @@ function flipEntryInContent(content, qlId, result, changedFiles = []) {
|
|
|
280
316
|
endIdx += 1
|
|
281
317
|
}
|
|
282
318
|
if (result && !hasResult) {
|
|
319
|
+
// quick step3 --output 常被压成单行「需求:…根因:…方案:…结果:…」(agent 未加换行),
|
|
320
|
+
// 直接落盘会得到双层前缀「结果:需求:…结果:…」。把单行四字段归一为多行字段块,省 agent 手工
|
|
321
|
+
// 拆行精修(prompt-control-debt quick-①)。多行 / 单句结果不受影响。
|
|
322
|
+
let body = result
|
|
323
|
+
if (!/\r?\n/.test(body) && /^需求:/.test(body)) {
|
|
324
|
+
// 单行四字段归一为多行。不能 split(/(?=需求:|根因:|方案:|结果:)/)——正文引用字段标签字样
|
|
325
|
+
// (如根因里写「双层「结果:」前缀」)会被任意位置误切。改按序扫描:真实标签 = 上一标签之后
|
|
326
|
+
// 首次出现,字段正文引用更靠后的标签不误断(quick-① 2026-08-04 实证补丁)。缺标签返回 null
|
|
327
|
+
// → 落单行兜底(--done 契约校验仍会拦缺字段)。
|
|
328
|
+
const segs = splitSingleLineFields(body)
|
|
329
|
+
if (segs) body = segs.join('\n')
|
|
330
|
+
}
|
|
283
331
|
// 多行结果(结构化 需求/根因/方案/结果 字段块):逐行插入为独立字段行。
|
|
284
332
|
// 单行结果:保持「结果:<一句话>」一行,向后兼容简单用例。
|
|
285
|
-
if (/\r?\n/.test(
|
|
286
|
-
const resultLines =
|
|
333
|
+
if (/\r?\n/.test(body)) {
|
|
334
|
+
const resultLines = body.split(/\r?\n/).filter(l => l.trim() !== '')
|
|
287
335
|
lines.splice(endIdx, 0, ...resultLines)
|
|
288
336
|
} else {
|
|
289
|
-
lines.splice(endIdx, 0, `结果:${
|
|
337
|
+
lines.splice(endIdx, 0, `结果:${body}`)
|
|
290
338
|
}
|
|
291
339
|
}
|
|
292
340
|
return lines.join('\n')
|
package/src/run/complete.js
CHANGED
|
@@ -320,6 +320,23 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
320
320
|
// quick 收尾(W6 Step6b 抽至 complete-handlers.js handleQuickStageCompletion)
|
|
321
321
|
await handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, platformOpts })
|
|
322
322
|
|
|
323
|
+
// ── reopen --done 回填(坑 brainstorm-reopen-step-state-desync)──
|
|
324
|
+
// nextPendingIdx === -1 且无 waiting,说明要进阶段完成分支。此时若存在 stale 步骤
|
|
325
|
+
// (reopen --from-step N 把 N+1..end 置 stale,但方案未变、无需重跑),需同步回填为
|
|
326
|
+
// completed,否则进度数字与实际步骤明细矛盾(阶段已完成 + 6/8)。
|
|
327
|
+
// 安全边界:仅进入阶段完成分支时才回填(stale 本就被跳过、不回填也照常标 completed),
|
|
328
|
+
// 不改变单步推进行为;stale 若确需重跑,运行流(stage.js:141-148)会先于本分支转 pending 执行。
|
|
329
|
+
const staleSteps = steps.filter(s => s.status === 'stale')
|
|
330
|
+
if (staleSteps.length > 0) {
|
|
331
|
+
const nowStr = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
332
|
+
for (const st of staleSteps) {
|
|
333
|
+
st.status = 'completed'
|
|
334
|
+
st.completedAt = st.completedAt || nowStr
|
|
335
|
+
}
|
|
336
|
+
await pm._write(cwd, progress, changeName)
|
|
337
|
+
console.log(` ⚠️ 同步回填 ${staleSteps.length} 个 stale 步骤为 completed(reopen --from-step N 后 --done,方案未变)`)
|
|
338
|
+
}
|
|
339
|
+
|
|
323
340
|
stageData.status = 'completed'
|
|
324
341
|
stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
325
342
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
package/src/run/gates.js
CHANGED
|
@@ -204,6 +204,8 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
204
204
|
// 自报告 PASS 但实测失败 → 阻断(防止"文案通过"绕过验证)。
|
|
205
205
|
if (stageName === 'verify') {
|
|
206
206
|
const { runVerifyTestCheck, printVerifyTestCheck } = await import('../verify-postcheck.js')
|
|
207
|
+
// 测试实测是同步 execSync,长套件可跑 2~10min 且中途无输出——先预告避免 agent 误判卡死
|
|
208
|
+
console.log(`\n⏳ Verify 测试对账:CLI 亲自执行 local.yaml 的 commands.test(同步,耗时可能较长,请等待…)`)
|
|
207
209
|
const testCheck = runVerifyTestCheck({ cwd, specBase, changeName })
|
|
208
210
|
printVerifyTestCheck(testCheck)
|
|
209
211
|
if (testCheck.status === 'failed') {
|
package/src/stage-review.js
CHANGED
|
@@ -270,7 +270,13 @@ export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
|
|
|
270
270
|
const markerPath = stageReviewMarkerPath(runtimeRoot, stage, changeName)
|
|
271
271
|
if (existsSync(markerPath)) {
|
|
272
272
|
const content = readFileSync(markerPath, 'utf8').trim()
|
|
273
|
-
|
|
273
|
+
// marker 内容格式校验(execute 复盘 b2):必须 review- 前缀(generateStageReviewRunId 格式)。
|
|
274
|
+
// agent 手写 marker 可能误填 execute 的 exec- 前缀 → 按 stage-reviews/<stage>-<runId> 拼目录必找不到,
|
|
275
|
+
// 与其读坏 ID 报误导错误,不如显式 warn 并退回目录扫描。
|
|
276
|
+
if (content) {
|
|
277
|
+
if (/^review-/.test(content)) return content
|
|
278
|
+
console.warn(`⚠️ stage-review marker ${markerPath} 内容 "${content.slice(0, 40)}" 不是 review- 前缀(应为 generateStageReviewRunId 格式,勿填 execute 的 exec- ID),忽略并退回目录扫描`)
|
|
279
|
+
}
|
|
274
280
|
}
|
|
275
281
|
} catch {}
|
|
276
282
|
|
|
@@ -284,6 +290,21 @@ export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
|
|
|
284
290
|
.sort()
|
|
285
291
|
.reverse()
|
|
286
292
|
if (!entries[0]) return null
|
|
293
|
+
// cross-change 防护(execute 复盘 b1):changeName 提供时按 review.json 的 reviewedFiles[0] 归属过滤。
|
|
294
|
+
// 契约 reviewedFiles[0] = `changes/<change>/<mainDoc>`(renderReviewJsonContract),据此排除他变更的 review,
|
|
295
|
+
// 否则批量完成 marker 缺失时可能读到 proxy 等其他变更的 acceptance review 报错误导。
|
|
296
|
+
if (changeName) {
|
|
297
|
+
const matching = entries.find(e => {
|
|
298
|
+
try {
|
|
299
|
+
const rj = JSON.parse(readFileSync(join(dir, e, 'review.json'), 'utf8'))
|
|
300
|
+
const r0 = Array.isArray(rj.reviewedFiles) ? String(rj.reviewedFiles[0]) : ''
|
|
301
|
+
return r0.includes(`changes/${changeName}/`)
|
|
302
|
+
} catch { return false }
|
|
303
|
+
})
|
|
304
|
+
if (matching) return matching.slice(stage.length + 1)
|
|
305
|
+
console.warn(`⚠️ 无 ${stage} 的 review marker,且 stage-reviews/ 下无归属变更 ${changeName} 的 review(reviewedFiles 应含 changes/${changeName}/);为避免跨变更串台不再取最新,返回 null(fail-closed,由调用方报缺 review.json)`)
|
|
306
|
+
return null
|
|
307
|
+
}
|
|
287
308
|
// entries[0] = "plan-review-2026-...",剥掉 `${stage}-` 前缀得到 runId "review-2026-..."
|
|
288
309
|
return entries[0].slice(stage.length + 1)
|
|
289
310
|
} catch {
|
package/src/stages/archive.js
CHANGED
|
@@ -59,6 +59,9 @@ module-impact.md 第一行标题必须用中文:# 模块影响分析(Module
|
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
name: 'sync-module-docs',
|
|
62
|
+
// 坑 verify-archive-flow-pitfalls 坑4:缺 requiresWait 时 --continue --answer 会直接标 completed、
|
|
63
|
+
// agent 无机会按 module-impact.md 写入模块卡片。加 requiresWait 使 --continue 确认后回到本步执行写入
|
|
64
|
+
requiresWait: true,
|
|
62
65
|
prompt: `根据 module-impact.md 同步更新模块索引和卡片文档。
|
|
63
66
|
|
|
64
67
|
### ⚠️ 核心原则:结构化事实改 _module-map.yaml,语义解释改模块卡片
|
package/src/stages/execute.js
CHANGED
|
@@ -596,6 +596,13 @@ ${taskSummary}
|
|
|
596
596
|
- 🔥 热上下文:design.md 非目标/兼容策略 + 当前 Wave 任务(必须加载)
|
|
597
597
|
- 🌡️ 温上下文:CONVENTIONS.md + ARCHITECTURE.md(需要时加载)
|
|
598
598
|
- ❄️ 冷上下文:其他变更的 design.md、历史 plan.md(不要主动加载,除非明确需要)
|
|
599
|
+
|
|
600
|
+
### 中断续跑(如曾中断恢复)
|
|
601
|
+
execute 按 Wave 持久化进度,task 级进度靠 plan.md checkbox 勾选。若本 Wave 曾因 429/API 配额/崩溃中断:
|
|
602
|
+
- plan.md 中**已勾选 \`- [x]\` 的 task 已完成,跳过不重跑**(子代理也可能在完成前中断,重跑前先确认该 task 产出文件是否完整)
|
|
603
|
+
- 用 \`sillyspec status\` 查当前进度,重新 \`sillyspec run execute\` 会回到当前 Wave step 继续,**不要从零重置或重跑已完成 Wave**
|
|
604
|
+
- 本 Wave 已完成但不完整(产出缺文件)的 task 补做,不牵连其他 task
|
|
605
|
+
|
|
599
606
|
${contractInjection}${prototypeInjection}
|
|
600
607
|
### 本 Wave 任务
|
|
601
608
|
${taskList}
|
|
@@ -532,6 +532,11 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
|
|
|
532
532
|
|
|
533
533
|
if (!taskId) errors.push(fsRule.data.messageId.replaceAll('${id}', file))
|
|
534
534
|
if (!title) errors.push(fsRule.data.messageTitle.replaceAll('${id}', file))
|
|
535
|
+
// title_zh(中文标题)完整性:子代理常为压 plan.js 的 20~40 行硬上限合并 title/title_zh(静默丢失),
|
|
536
|
+
// postcheck 不校验行数(那是 prompt 劝说)但强制 frontmatter 字段齐全(enforcement 优于放宽劝说)。
|
|
537
|
+
// 无对应 manifest 文案,内联(其余字段文案走 fsRule.data.messageXxx)。prompt-control-debt plan-b。
|
|
538
|
+
const titleZh = (fm.match(/^title_zh:\s*(.+)/m)?.[1] || '').trim()
|
|
539
|
+
if (!titleZh) errors.push(`${taskId || file}: frontmatter 缺少 title_zh(中文标题)——不得为压行数合并/删除字段`)
|
|
535
540
|
if (taskId) allTaskIds.push(taskId)
|
|
536
541
|
|
|
537
542
|
// 2. allowed_paths 不为空
|
package/src/stages/plan.js
CHANGED
package/src/stages/verify.js
CHANGED
|
@@ -38,7 +38,7 @@ export const definition = {
|
|
|
38
38
|
{
|
|
39
39
|
name: '进度确认',
|
|
40
40
|
migratedFrom: ['状态检查'],
|
|
41
|
-
prompt: `> 💡 先说清楚:\`sillyspec run verify\`(不带 --done
|
|
41
|
+
prompt: `> 💡 先说清楚:\`sillyspec run verify\`(不带 --done)**只下发执行指令,不会替你跑测试/构建**——真正的测试由 CLI 在最后 --done 时统一执行(local.yaml 的 commands.test,同步对账可能耗时较长);「运行测试和质量扫描」那步**不需要你重复手动跑全量**(避免与 CLI 对账重复耗时),只做 lint/静态检查 + 可选快速冒烟。别以为敲了 run verify 就自动验证了。
|
|
42
42
|
|
|
43
43
|
检查当前进度,确认可以执行 verify。
|
|
44
44
|
|
|
@@ -156,28 +156,25 @@ export const definition = {
|
|
|
156
156
|
},
|
|
157
157
|
{
|
|
158
158
|
name: '运行测试和质量扫描',
|
|
159
|
-
prompt:
|
|
159
|
+
prompt: `运行代码质量扫描(测试实测统一由 CLI 对账执行,本步不重复手动跑全量)。
|
|
160
160
|
|
|
161
161
|
### 操作
|
|
162
|
-
1. 读取 \`.sillyspec/local.yaml\`
|
|
163
|
-
2.
|
|
164
|
-
3. 如果 local.yaml 无 test 命令,根据项目类型选择:
|
|
162
|
+
1. 读取 \`.sillyspec/local.yaml\` 获取构建、测试和 lint 命令
|
|
163
|
+
2. **不要手动重复跑 commands.test**——CLI 会在最终 --done 时统一执行一次(按变更命中模块子集),本步再跑 = 与 CLI 对账重复耗时(实测 198s×2)。如为提前发现实现问题,可对变更模块做**针对性快速冒烟**(可选,非必需):
|
|
165
164
|
- Maven:\`mvn test -pl <变更模块> -am\`(仅编译变更模块及其依赖)
|
|
166
165
|
- Gradle:\`./gradlew :<模块>:test\`
|
|
167
166
|
- npm/pnpm:\`pnpm test --filter=<包名>\` 或 \`npm test -- --testPathPattern=<相关文件>\`
|
|
168
167
|
- Python:\`pytest <变更模块路径>/\`
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
6. 如果 local.yaml 有 lint 命令,运行 lint 检查
|
|
168
|
+
3. 如果 local.yaml 有 lint 命令,运行 lint 检查
|
|
169
|
+
4. 搜索技术债务:grep TODO/FIXME/HACK/XXX(仅限变更文件)
|
|
172
170
|
|
|
173
171
|
### 注意
|
|
174
|
-
-
|
|
175
|
-
-
|
|
176
|
-
- **CLI 对账机制**:verify 阶段最终 --done 时,CLI 会亲自执行 local.yaml 的 commands.test 并与你的报告对账;实测失败会直接阻断 verify 完成,谎报测试结果没有意义
|
|
172
|
+
- **CLI 对账机制**:verify 阶段最终 --done 时,CLI 会亲自执行 local.yaml 的 commands.test(同步,耗时可能较长);实测失败会直接阻断 verify 完成,谎报测试结果没有意义
|
|
173
|
+
- 冒烟测试非必需:全量/模块实测结果以 CLI 对账为准,本步跑的结果仅供你提前发现问题并写入验证报告
|
|
177
174
|
|
|
178
175
|
### 输出
|
|
179
|
-
|
|
180
|
-
outputHint: '
|
|
176
|
+
质量扫描结果 + 技术债务标记`,
|
|
177
|
+
outputHint: '质量扫描结果 + 技术债务',
|
|
181
178
|
optional: false
|
|
182
179
|
},
|
|
183
180
|
{
|
package/src/task-review.js
CHANGED
|
@@ -460,6 +460,21 @@ function runGit(gitDir, args) {
|
|
|
460
460
|
}).trim()
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
// porcelain 状态行 → 变更文件路径(对齐 shared.js:343 parsePorcelainPath:`XY path`、引号包裹、
|
|
464
|
+
// rename `R old -> new` 取箭头后)。未 commit 的 working-tree 改动对账用。
|
|
465
|
+
function parsePorcelainFiles(statusOut) {
|
|
466
|
+
const files = []
|
|
467
|
+
for (const line of String(statusOut).split('\n')) {
|
|
468
|
+
if (line.trim() === '') continue
|
|
469
|
+
let p = line.slice(3).trim()
|
|
470
|
+
if (p.length >= 2 && p.startsWith('"') && p.endsWith('"')) p = p.slice(1, -1).replace(/\\(.)/g, (_, c) => c)
|
|
471
|
+
const arrow = p.indexOf(' -> ')
|
|
472
|
+
if (arrow !== -1) p = p.slice(arrow + 4)
|
|
473
|
+
if (p) files.push(p.replace(/\\/g, '/'))
|
|
474
|
+
}
|
|
475
|
+
return files
|
|
476
|
+
}
|
|
477
|
+
|
|
463
478
|
/**
|
|
464
479
|
* 校验 review.json 的 base/head 是否指向真实 git 提交,且 diff 非空。
|
|
465
480
|
*
|
|
@@ -514,23 +529,27 @@ export function verifyReviewGitEvidence(review, gitDir) {
|
|
|
514
529
|
return { ok: true, emptyDiff: false, errors, warnings, unavailable: false }
|
|
515
530
|
}
|
|
516
531
|
|
|
517
|
-
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
532
|
+
// 并入 working-tree 未提交改动(execute 复盘 a):子代理默认不 commit,base..head 无 commit diff 时
|
|
533
|
+
// diffFiles 留空 → 下方交叉比对拿空集对非空 changedFiles 必判「完全不相交」伪造,逼 agent 强制 commit
|
|
534
|
+
// + 改 7 个 review head。对齐 checkExecuteCodeEvidence(stage-contract.js 同时查 working-tree)语义,
|
|
535
|
+
// 未提交改动也算有效对账源。working-tree 有改动时无条件并入(覆盖「部分 commit + 未提交」)。
|
|
536
|
+
const commitDiffFiles = diffFiles.slice()
|
|
537
|
+
try {
|
|
538
|
+
const wtStatus = runGit(gitDir, ['status', '--porcelain'])
|
|
539
|
+
if (wtStatus && wtStatus.trim().length > 0) {
|
|
540
|
+
const wtFiles = parsePorcelainFiles(wtStatus)
|
|
541
|
+
if (wtFiles.length > 0) diffFiles = diffFiles.concat(wtFiles)
|
|
542
|
+
if (commitDiffFiles.length === 0) {
|
|
543
|
+
warnings.push(`base..head 无 commit diff(${review.base.slice(0, 8)}..${review.head.slice(0, 8)}),交叉对账并入 working-tree 未提交改动 ${wtFiles.length} 个文件 —— 按有效改动处理,不判零改动伪造`)
|
|
544
|
+
}
|
|
528
545
|
}
|
|
529
|
-
}
|
|
530
|
-
|
|
546
|
+
} catch {}
|
|
547
|
+
diffFiles = [...new Set(diffFiles)].filter(Boolean)
|
|
548
|
+
|
|
549
|
+
const emptyDiff = diffFiles.length === 0
|
|
531
550
|
|
|
532
|
-
// changedFiles 交叉比对:完全不相交 = review 描述的改动与实际 diff
|
|
533
|
-
if (!
|
|
551
|
+
// changedFiles 交叉比对:完全不相交 = review 描述的改动与实际 diff 无关(diffFiles 已含 working-tree)
|
|
552
|
+
if (!emptyDiff && Array.isArray(review.changedFiles) && review.changedFiles.length > 0) {
|
|
534
553
|
const normalize = (p) => String(p).replace(/^\.\//, '')
|
|
535
554
|
const diffSet = new Set(diffFiles.map(normalize))
|
|
536
555
|
const hasOverlap = review.changedFiles.some(f => {
|
|
@@ -543,7 +562,7 @@ export function verifyReviewGitEvidence(review, gitDir) {
|
|
|
543
562
|
}
|
|
544
563
|
}
|
|
545
564
|
|
|
546
|
-
return { ok: errors.length === 0, emptyDiff
|
|
565
|
+
return { ok: errors.length === 0, emptyDiff, errors, warnings, unavailable: false }
|
|
547
566
|
}
|
|
548
567
|
|
|
549
568
|
/**
|
package/src/worktree-apply.js
CHANGED
|
@@ -94,6 +94,29 @@ function getBlobHashMap(cwd, treeish, files) {
|
|
|
94
94
|
return map;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* apply 的文件清单 = design §6 清单 ∪ plan TaskCard allowed_paths(execute 复盘 c)。
|
|
99
|
+
*
|
|
100
|
+
* design §6 常只列源码、漏测试/产物文件,而 task allowed_paths(plan 阶段产出)已含——apply 若只认
|
|
101
|
+
* design 清单会在测试/产物文件上误拦(assess 用 task allowed_paths 已放行、apply 用 design 清单又拦,
|
|
102
|
+
* 两 gate 口径不一致)。union 后两源并集为准;plan 已过 validateDesignFileCoverage 单向校验(design ⊆
|
|
103
|
+
* plan),union 不会放开 design/plan 之外的任意文件(仍拦完全越界文件)。
|
|
104
|
+
*
|
|
105
|
+
* @param {string} projectRoot - 主仓库根
|
|
106
|
+
* @param {string} changeName - 变更名
|
|
107
|
+
* @returns {Set<string>} 并集清单(无 design 清单且无 task 卡片时为空集)
|
|
108
|
+
*/
|
|
109
|
+
export function resolveApplyAllowSet(projectRoot, changeName) {
|
|
110
|
+
const allowSet = parseFileChangeList(join(projectRoot, CHANGES_REL, changeName, 'design.md'));
|
|
111
|
+
const tasksDir = join(projectRoot, CHANGES_REL, changeName, 'tasks');
|
|
112
|
+
if (existsSync(tasksDir)) {
|
|
113
|
+
for (const tf of readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f))) {
|
|
114
|
+
for (const p of parseAllowedPaths(readFileSync(join(tasksDir, tf), 'utf8'))) allowSet.add(p);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return allowSet;
|
|
118
|
+
}
|
|
119
|
+
|
|
97
120
|
/**
|
|
98
121
|
* apply worktree 变更到主工作区
|
|
99
122
|
*
|
|
@@ -178,9 +201,8 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
178
201
|
return result;
|
|
179
202
|
}
|
|
180
203
|
|
|
181
|
-
// --- 3. 解析 design
|
|
182
|
-
const
|
|
183
|
-
const allowSet = parseFileChangeList(designPath);
|
|
204
|
+
// --- 3. 解析 apply 文件清单(design §6 清单 ∪ plan TaskCard allowed_paths,execute 复盘 c) ---
|
|
205
|
+
const allowSet = resolveApplyAllowSet(projectRoot, changeName);
|
|
184
206
|
const hasAllowList = allowSet.size > 0;
|
|
185
207
|
|
|
186
208
|
// --- 4. 校验:变更文件 ⊆ 清单(无清单则跳过)---
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
5. **执行顺序**:文档 → 读代码 → 写测试 → 写实现 → 跑测试 → 验收 → 更新文档。
|
|
12
12
|
6. **判规模选档**:≤3 文件、范围明确走 `quick`;多阶段 / 架构级走完整流程。
|
|
13
13
|
7. **代码先行不补流程(倒推 B 模式)**:代码若已先写好,**不回头补 brainstorm/plan 装样子**——用 `quick --done` 收尾 + 补 quicklog 条目,把已落盘改动如实登记进进度库。
|
|
14
|
-
8. **实证核验再 `--done`**:`quick --done`
|
|
14
|
+
8. **实证核验再 `--done`**:`quick --done` 前,触及 `src`/`test` 的改动先 `npm test` + `npm run lint`;纯 doc/配置改动可跳过(lint 不扫 docs/,空转无信息),以落盘文件与测试结果为准,不信口头"已完成"。
|
|
15
15
|
9. **中途停下用 `sillyspec status` / `sillyspec resume` 存进度**,不直接 commit 半成品。
|
|
16
16
|
10. **实现完成后对照文档验收**(design.md / 模块文档),并检查是否影响已有测试。
|
|
17
17
|
11. **非测试逻辑本身有误时,禁止改测试来"通过"**——修逻辑,不修测试。
|
|
@@ -20,6 +20,7 @@ grep -rl "<关键词>" <源码目录>/ --include="*.java" --include="*.js" --inc
|
|
|
20
20
|
1. 读取变更目录下的 tasks.md,提取所有 checkbox 任务
|
|
21
21
|
2. 对每个 task,**递归**检查对应模块目录及其**所有子目录**下是否存在测试文件(*test*、*spec*、*Test*、*Spec*)——包括模块内 co-located 的 tests/ 目录(如 `backend/app/modules/*/tests/`),**不要只看顶层 `tests/`**,否则会把测试就在模块内部的项目误判为“无测试”
|
|
22
22
|
3. 没有测试文件的 task 标记为 ⚠️ 缺少测试
|
|
23
|
+
4. **集成盲区提示**:测试文件存在 ≠ 集成正确——路由/layout 守卫重定向、跨模块装配这类集成 bug 组件单测覆盖不到,只有集成/冒烟/E2E 才暴露。对路由/layout/跨进程装配敏感的 task,额外检查是否有集成冒烟覆盖;无则标 ⚠️ 集成层未验证(需人工或部署确认)
|
|
23
24
|
|
|
24
25
|
**探针 4:决策追踪覆盖探针(如存在 decisions.md)**
|
|
25
26
|
1. 从 decisions.md 提取所有当前版本 D-xxx@vN
|