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
package/src/run/complete.js
CHANGED
|
@@ -29,11 +29,25 @@ import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProject
|
|
|
29
29
|
import { formatExecuteSummary } from '../worktree-apply.js'
|
|
30
30
|
import { isEndToEndTaskText } from '../change-risk-profile.js'
|
|
31
31
|
import { deriveTitleFromLinkedChange } from '../quicklog.js'
|
|
32
|
+
import { gitQuiet } from '../git-helper.js'
|
|
32
33
|
|
|
33
34
|
// validateMetadata / readDesignScale / validateFileLocations 已迁至 ./gates.js(completeStageGates
|
|
34
35
|
// 共享收尾管线,消除 noAI 末步 / continueStep 完成分支绕过 gate 的 S1/S2/S3 不对称)。
|
|
35
36
|
// completeStep 阶段完成分支(task-02 接入 completeStageGates)+ brainstorm 下一步提示改 import 自 gates.js。
|
|
36
37
|
|
|
38
|
+
// 完整流程 change title 刷新:从 proposal/design 首个 # 标题提取中文描述写 changes.title(人类可读
|
|
39
|
+
// 展示元信息)。proposal/design 落盘后的**任意步骤持久化点**都调用——单步 --done(brainstorm
|
|
40
|
+
// step6 design.md 已落盘但阶段未完)与阶段完成同样刷新,治「brainstorm 全程 title 存英文 autoName
|
|
41
|
+
// 兜底」。quick-<hex> 无 proposal/design 目录,deriveTitleFromLinkedChange 返回 '' 不刷新
|
|
42
|
+
// (quick 走 handleQuickStageCompletion 的 extractTitleFromResult)。失败静默(不阻断流程)。
|
|
43
|
+
function refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName) {
|
|
44
|
+
if (!changeName || /^quick-[0-9a-f]{8}$/.test(changeName)) return
|
|
45
|
+
try {
|
|
46
|
+
const refinedTitle = deriveTitleFromLinkedChange(specBase, changeName)
|
|
47
|
+
if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle })
|
|
48
|
+
} catch { /* title 刷新失败不阻断 */ }
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
/**
|
|
38
52
|
* 坑1:用 --done --answer 解掉「已 waiting 的步骤」。
|
|
39
53
|
*
|
|
@@ -114,8 +128,28 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
114
128
|
console.log(`⚠️ Step "${steps[_resolvedWaitIdx].name}" 此前处于 waiting,--done --answer 已补回答并拉回待完成。`)
|
|
115
129
|
}
|
|
116
130
|
if (currentIdx === -1) {
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
// ── reopen stale 收尾路径(坑 reopen-done-escape-hatch-unreachable)──
|
|
132
|
+
// 全 completed+stale(无 pending/in-progress)时若只有 exit(1),--done --confirm 逃生门
|
|
133
|
+
// 永远到不了下方回填门控。修复:有 stale 时——无 confirm 报拦截指引(与门控同文案);
|
|
134
|
+
// 带 confirm 把首个 stale 拉回 pending 走完整完成管线,末步后的回填门控 confirm 分支
|
|
135
|
+
// 收尾其余 stale(--confirm 语义 = 全部 stale 一并确认)。
|
|
136
|
+
const _staleIdx = steps.findIndex(s => s.status === 'stale')
|
|
137
|
+
if (_staleIdx !== -1) {
|
|
138
|
+
const _staleNames = steps.filter(s => s.status === 'stale').map(s => s.name)
|
|
139
|
+
if (!confirm) {
|
|
140
|
+
console.error(`\n⏸️ 检测到 ${_staleNames.length} 个 stale 步骤(reopen 后未执行):${_staleNames.join('、')}`)
|
|
141
|
+
console.error(` 两条出路:`)
|
|
142
|
+
console.error(` ① sillyspec run ${stageName}${changeName ? ` --change ${changeName}` : ''} 逐个真实执行(stale 会被转为 pending)`)
|
|
143
|
+
console.error(` ② 确认方案未变,一次性回填收尾:sillyspec run ${stageName} --done --confirm${changeName ? ` --change ${changeName}` : ''} --output "..."`)
|
|
144
|
+
process.exit(1)
|
|
145
|
+
}
|
|
146
|
+
steps[_staleIdx].status = 'pending'
|
|
147
|
+
currentIdx = _staleIdx
|
|
148
|
+
console.log(`⚠️ --confirm 收尾:把 stale 步骤 "${steps[_staleIdx].name}" 拉回当前步骤走完成管线,其余 stale 由回填门控一并确认。`)
|
|
149
|
+
} else {
|
|
150
|
+
console.error(`没有待完成的步骤(阶段 ${stageName} 已无 pending/in-progress 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
|
|
151
|
+
process.exit(1)
|
|
152
|
+
}
|
|
119
153
|
}
|
|
120
154
|
|
|
121
155
|
// ── requiresWait 硬门控 ──
|
|
@@ -285,15 +319,40 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
285
319
|
// completed,否则进度数字与实际步骤明细矛盾(阶段已完成 + 6/8)。
|
|
286
320
|
// 安全边界:仅进入阶段完成分支时才回填(stale 本就被跳过、不回填也照常标 completed),
|
|
287
321
|
// 不改变单步推进行为;stale 若确需重跑,运行流(stage.js:141-148)会先于本分支转 pending 执行。
|
|
322
|
+
// FR-01 门控:无 --confirm 时不回填,阻断并给两条出路;带 --confirm 时回填并审计。
|
|
288
323
|
const staleSteps = steps.filter(s => s.status === 'stale')
|
|
289
324
|
if (staleSteps.length > 0) {
|
|
325
|
+
if (!confirm) {
|
|
326
|
+
// 无 confirm:不回填,阶段不完成,返回 staleBlocked 标记
|
|
327
|
+
const staleNames = staleSteps.map(s => `Step ${steps.indexOf(s) + 1}(${s.name})`).join(', ')
|
|
328
|
+
console.error(`⏸️ 检测到 ${staleSteps.length} 个 stale 步骤(reopen 后未执行)——两条出路:`)
|
|
329
|
+
console.error(` ① sillyspec run ${stageName} 逐个真实执行(首个 stale 自动转 pending)`)
|
|
330
|
+
console.error(` ② 确认方案未变用 --done --confirm 一次性回填收尾`)
|
|
331
|
+
console.error(` stale 步骤:${staleNames}`)
|
|
332
|
+
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
333
|
+
pm._write(cwd, progress, changeName)
|
|
334
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: -1, staleBlocked: true }
|
|
335
|
+
}
|
|
336
|
+
// 带 confirm:按现行回填逻辑 + 审计记录
|
|
290
337
|
const nowStr = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
291
338
|
for (const st of staleSteps) {
|
|
292
339
|
st.status = 'completed'
|
|
293
340
|
st.completedAt = st.completedAt || nowStr
|
|
294
341
|
}
|
|
295
342
|
pm._write(cwd, progress, changeName)
|
|
296
|
-
console.log(` ⚠️ 同步回填 ${staleSteps.length} 个 stale 步骤为 completed(reopen --from-step N 后 --done,方案未变)`)
|
|
343
|
+
console.log(` ⚠️ 同步回填 ${staleSteps.length} 个 stale 步骤为 completed(reopen --from-step N 后 --done --confirm,方案未变)`)
|
|
344
|
+
// 审计记录 reopen-stale-backfill
|
|
345
|
+
try {
|
|
346
|
+
pm._appendAuditLog(cwd, {
|
|
347
|
+
action: 'reopen-stale-backfill',
|
|
348
|
+
stage: stageName,
|
|
349
|
+
change: changeName,
|
|
350
|
+
steps: staleSteps.map(s => s.name),
|
|
351
|
+
confirmedAt: nowStr
|
|
352
|
+
})
|
|
353
|
+
} catch (e) {
|
|
354
|
+
console.warn('⚠️ 回填审计日志写入失败(不阻断):', e.message)
|
|
355
|
+
}
|
|
297
356
|
}
|
|
298
357
|
|
|
299
358
|
stageData.status = 'completed'
|
|
@@ -331,15 +390,8 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
331
390
|
if (_stageGatesResult?.stageCompleted === false) process.exitCode = 1
|
|
332
391
|
if (_stageGatesResult) return _stageGatesResult
|
|
333
392
|
|
|
334
|
-
// 完整流程 change title
|
|
335
|
-
|
|
336
|
-
// deriveTitleFromLinkedChange 返回 '' 不刷新(quick 走 handleQuickStageCompletion 的 extractTitleFromResult)。
|
|
337
|
-
if (changeName && !/^quick-[0-9a-f]{8}$/.test(changeName)) {
|
|
338
|
-
try {
|
|
339
|
-
const refinedTitle = deriveTitleFromLinkedChange(specBase, changeName)
|
|
340
|
-
if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle })
|
|
341
|
-
} catch { /* title 刷新失败不阻断完成 */ }
|
|
342
|
-
}
|
|
393
|
+
// 完整流程 change title 刷新(阶段完成时;proposal/design 已落盘,机制见 refreshChangeTitleFromArtifacts)
|
|
394
|
+
refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName)
|
|
343
395
|
|
|
344
396
|
// gate 全过:persist completed(task-01 移后)。此处到 _write 之间若崩,DB 仍 in-progress(内存已 completed 但未落盘),下次进 CLI 读 DB 即 in-progress,不产生"假 completed"。
|
|
345
397
|
pm._write(cwd, progress, changeName)
|
|
@@ -404,6 +456,9 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
404
456
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
405
457
|
pm._write(cwd, progress, changeName)
|
|
406
458
|
triggerSync(cwd, changeName, platformOpts)
|
|
459
|
+
// 单步完成也刷新 change title:design.md 在 brainstorm step6 落盘,此后每次 --done 都该让
|
|
460
|
+
// changes.title 反映中文描述,不等阶段收尾(机制见 refreshChangeTitleFromArtifacts)。
|
|
461
|
+
refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName)
|
|
407
462
|
|
|
408
463
|
// Append to user-inputs.md
|
|
409
464
|
if (outputText) {
|
|
@@ -453,21 +508,90 @@ function readTaskCardText(changeDir, taskNum) {
|
|
|
453
508
|
try { return readFileSync(join(changeDir, 'tasks', `task-${taskNum}.md`), 'utf8') } catch { return '' }
|
|
454
509
|
}
|
|
455
510
|
|
|
511
|
+
/**
|
|
512
|
+
* W2 task-04/05 共用 helper:构造草稿零 diff 校验用的 ctx(与 task-review.js generateTaskReviewDrafts 同源)。
|
|
513
|
+
* 用于 autoCheckPlanFromReviews(勾选层)和 detectExecuteBatchFinish(批量层)。
|
|
514
|
+
* 失败返回 null(调用方降级:勾选层不勾、批量层不过滤——向后兼容)。
|
|
515
|
+
*
|
|
516
|
+
* @returns {Promise<{gitDir:string, base:string, head:string}|null>}
|
|
517
|
+
*/
|
|
518
|
+
async function buildDraftContext(cwd, changeName) {
|
|
519
|
+
try {
|
|
520
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
521
|
+
const meta = new WorktreeManager({ cwd }).getMeta(changeName)
|
|
522
|
+
const base = meta?.baselineCommit || meta?.baseHash || null
|
|
523
|
+
let gitDir = cwd
|
|
524
|
+
let head = null
|
|
525
|
+
if (meta?.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
526
|
+
gitDir = meta.worktreePath
|
|
527
|
+
}
|
|
528
|
+
if (base) {
|
|
529
|
+
try {
|
|
530
|
+
head = gitQuiet(gitDir, ['rev-parse', 'HEAD'])
|
|
531
|
+
} catch (e) {
|
|
532
|
+
console.warn(`⚠️ ctx 构造失败(git rev-parse HEAD 失败:${e && e.message ? e.message : e}),跳过草稿 diff 校验`)
|
|
533
|
+
return null
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (base && head && gitDir) {
|
|
537
|
+
return { gitDir, base, head }
|
|
538
|
+
}
|
|
539
|
+
return null
|
|
540
|
+
} catch (e) {
|
|
541
|
+
console.warn(`⚠️ ctx 构造失败(${e && e.message ? e.message : e}),跳过草稿 diff 校验`)
|
|
542
|
+
return null
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
456
546
|
/**
|
|
457
547
|
* 判定 task 是否该被 autoCheckPlanFromReviews 自动勾选。
|
|
458
548
|
* 端到端/deployment-critical task 要求 review spec+quality 双 pass(cannot_verify 不算,防批量完成
|
|
459
549
|
* 放行未真验的端到端 task);普通 task 非 fail 即可(保主 agent 直接实现模式体验,其 cannot_verify
|
|
460
550
|
* 草稿仍可批量收尾)。坑 execute-batch-complete-endtoend-checkbox。
|
|
461
|
-
*
|
|
551
|
+
*
|
|
552
|
+
* W2 task-04 FR-03:草稿勾选层零 diff 守卫——review 为自动草稿且 ctx 给定时,额外要求
|
|
553
|
+
* changedFiles 非空且 git diff 实测非空。防 allowed_paths 误归属他人 diff 或陈旧 review.json 导致
|
|
554
|
+
* 空任务被自动勾选/批量放行。ctx 缺省时保持现行判定(向后兼容)。
|
|
555
|
+
*
|
|
556
|
+
* @param {{ok?:boolean, review?:{specVerdict?:string, qualityVerdict?:string, reviewerNotes?:string, changedFiles?:string[]}}} r readReview 结果
|
|
462
557
|
* @param {boolean} endToEnd 是否端到端/deployment-critical task
|
|
558
|
+
* @param {{gitDir?:string, base?:string, head?:string}|null} [ctx] 实测 diff 上下文;缺省保持现行判定
|
|
463
559
|
* @returns {boolean}
|
|
464
560
|
*/
|
|
465
|
-
export function shouldAutoCheckTask(r, endToEnd) {
|
|
561
|
+
export function shouldAutoCheckTask(r, endToEnd, ctx = null) {
|
|
466
562
|
if (!r?.ok) return false
|
|
467
563
|
const spec = r.review?.specVerdict
|
|
468
564
|
const quality = r.review?.qualityVerdict
|
|
469
565
|
if (spec === 'fail' || quality === 'fail') return false
|
|
470
566
|
if (endToEnd) return spec === 'pass' && quality === 'pass'
|
|
567
|
+
|
|
568
|
+
// W2 task-04 草稿零 diff 守卫(FR-03)
|
|
569
|
+
// ctx 给定且 review 为自动草稿时,要求 changedFiles 非空且实测 diff 非空
|
|
570
|
+
if (ctx && r.review?.reviewerNotes && r.review.reviewerNotes.includes('auto-generated draft')) {
|
|
571
|
+
const changedFiles = r.review.changedFiles || []
|
|
572
|
+
if (changedFiles.length === 0) {
|
|
573
|
+
console.warn(`⚠️ 草稿 review changedFiles 为空,跳过自动勾选`)
|
|
574
|
+
return false
|
|
575
|
+
}
|
|
576
|
+
if (!ctx.gitDir || !ctx.base || !ctx.head) {
|
|
577
|
+
console.warn(`⚠️ ctx 信息不完整(gitDir=${ctx.gitDir}, base=${ctx.base}, head=${ctx.head}),跳过草稿 diff 校验,保守不勾`)
|
|
578
|
+
return false
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
// 实测 diff:git diff --name-only <base>..<head> -- <changedFiles>
|
|
582
|
+
// 用数组参数避免 shell 拆词,复用 git-helper 安全模式
|
|
583
|
+
const diffResult = gitQuiet(ctx.gitDir, ['diff', '--name-only', `${ctx.base}..${ctx.head}`, '--', ...changedFiles], { trim: true })
|
|
584
|
+
if (!diffResult || diffResult.trim() === '') {
|
|
585
|
+
console.warn(`⚠️ 草稿 review 实测 diff 为空(base=${ctx.base.slice(0,8)}, head=${ctx.head.slice(0,8)}, files=${changedFiles.length}),跳过自动勾选`)
|
|
586
|
+
return false
|
|
587
|
+
}
|
|
588
|
+
console.log(` ✓ 草稿 diff 校验通过(${diffResult.trim().split('\n').length} 个文件有改动)`)
|
|
589
|
+
} catch (e) {
|
|
590
|
+
console.warn(`⚠️ 草稿 diff 校验失败(${e && e.message ? e.message : e}),保守不勾`)
|
|
591
|
+
return false
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
471
595
|
return true
|
|
472
596
|
}
|
|
473
597
|
async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts }) {
|
|
@@ -491,6 +615,10 @@ async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOp
|
|
|
491
615
|
return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
|
|
492
616
|
}
|
|
493
617
|
const executeRunId = c
|
|
618
|
+
|
|
619
|
+
// W2 task-04 FR-03:构造 ctx 供草稿零 diff 守卫用(与 task-review.js generateTaskReviewDrafts 同源)
|
|
620
|
+
const ctx = await buildDraftContext(cwd, changeName)
|
|
621
|
+
|
|
494
622
|
// plan.md 是 agent 与 CLI 都会写的共享文件(agent 勾 checkbox、此处 autoCheck 也勾选)。
|
|
495
623
|
// 读-改-写必须整体持锁(withFileLock 串行化多进程),否则并发 execute --done / 手动勾选互相覆盖
|
|
496
624
|
// (后到者覆盖先到者);写入用 writeAtomicSync(tmp+rename 原子),防 Windows 整文件覆盖被读半截
|
|
@@ -506,7 +634,7 @@ async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOp
|
|
|
506
634
|
const r = readReview(reviewPath)
|
|
507
635
|
// 端到端 task:必须 pass(cannot_verify 不算);普通 task:非 fail 即可(坑 execute-batch-complete-endtoend-checkbox)
|
|
508
636
|
const endToEnd = isEndToEndTaskText(match + ' ' + readTaskCardText(changeDir, taskNum))
|
|
509
|
-
if (shouldAutoCheckTask(r, endToEnd)) {
|
|
637
|
+
if (shouldAutoCheckTask(r, endToEnd, ctx)) {
|
|
510
638
|
checkedCount++
|
|
511
639
|
return `${p1}x${p2}` // 勾选
|
|
512
640
|
}
|
|
@@ -530,8 +658,11 @@ async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOp
|
|
|
530
658
|
* 复用现成能力:readPlanCheckboxStatus(plan 勾选状态)+ checkExecuteCodeEvidence(代码客观核验,
|
|
531
659
|
* 与 doctor --align-execute-progress 同源,D-002/D-004)。仅在 stageName==='execute' && changeName 触发。
|
|
532
660
|
*
|
|
661
|
+
* W2 task-05 FR-04:plan 全勾后逐 task 复核——review 缺失或草稿零 diff → 阻断批量,
|
|
662
|
+
* 返回 blockedTasks 列表(task id 数组),调用方打印 reason。
|
|
663
|
+
*
|
|
533
664
|
* 安全门:plan 零 checkbox / 未全勾 / 代码零变更(unchanged)均不批量——信任 plan 声明但用代码核验兜底。
|
|
534
|
-
* @returns {Promise<{batched:boolean, aligned:number, reason?:string}>}
|
|
665
|
+
* @returns {Promise<{batched:boolean, aligned:number, reason?:string, blockedTasks?:string[]}>}
|
|
535
666
|
*/
|
|
536
667
|
async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, steps }) {
|
|
537
668
|
if (stageName !== 'execute' || !changeName) return { batched: false, aligned: 0 }
|
|
@@ -548,6 +679,78 @@ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBa
|
|
|
548
679
|
if (evidence.status === 'unchanged') {
|
|
549
680
|
return { batched: false, aligned: 0, reason: `代码零变更(${evidence.detail})` }
|
|
550
681
|
}
|
|
682
|
+
|
|
683
|
+
// W2 task-05 FR-04:逐 task 复核(草稿零 diff 守卫——批量层)
|
|
684
|
+
// 读取 plan.md 提取已勾 task id 列表(正则 `- [x] task-NN`)
|
|
685
|
+
const planPath = join(changeDir, 'plan.md')
|
|
686
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
687
|
+
const taskCheckboxRegex = /-\s\[x\]\s+task-(\d+)/gi
|
|
688
|
+
const taskMatches = [...planContent.matchAll(taskCheckboxRegex)]
|
|
689
|
+
const taskIds = taskMatches.map(m => `task-${m[1].padStart(2, '0')}`)
|
|
690
|
+
|
|
691
|
+
if (taskIds.length === 0) {
|
|
692
|
+
return { batched: false, aligned: 0, reason: 'plan.md 无已勾 task checkbox' }
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// 构造 ctx 供草稿零 diff 校验用(与 task-04 同源)
|
|
696
|
+
const ctx = await buildDraftContext(cwd, changeName)
|
|
697
|
+
const runtimeRoot = resolveRuntimeRoot({ specRoot: specBase }, specBase)
|
|
698
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
699
|
+
const blockedTasks = []
|
|
700
|
+
|
|
701
|
+
for (const taskId of taskIds) {
|
|
702
|
+
let review = null
|
|
703
|
+
try {
|
|
704
|
+
const { readReview, isValidExecuteRunId } = await import('../task-review.js')
|
|
705
|
+
const c = readFileSync(runIdFile, 'utf8').trim()
|
|
706
|
+
if (!c || !isValidExecuteRunId(c)) {
|
|
707
|
+
blockedTasks.push(taskId)
|
|
708
|
+
continue
|
|
709
|
+
}
|
|
710
|
+
const executeRunId = c
|
|
711
|
+
const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId, 'review.json')
|
|
712
|
+
const r = readReview(reviewPath)
|
|
713
|
+
if (!r?.ok) {
|
|
714
|
+
blockedTasks.push(taskId)
|
|
715
|
+
continue
|
|
716
|
+
}
|
|
717
|
+
review = r.review
|
|
718
|
+
} catch {
|
|
719
|
+
blockedTasks.push(taskId)
|
|
720
|
+
continue
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// review 存在:判定是否为自动草稿且零 diff
|
|
724
|
+
if (review?.reviewerNotes && review.reviewerNotes.includes('auto-generated draft')) {
|
|
725
|
+
const changedFiles = review.changedFiles || []
|
|
726
|
+
// 草稿且 changedFiles 为空 → 阻断
|
|
727
|
+
if (changedFiles.length === 0) {
|
|
728
|
+
blockedTasks.push(taskId)
|
|
729
|
+
continue
|
|
730
|
+
}
|
|
731
|
+
// 草稿且有 changedFiles → 实测 diff 校验(ctx 与 task-04 同源)
|
|
732
|
+
if (ctx && ctx.gitDir && ctx.base && ctx.head) {
|
|
733
|
+
try {
|
|
734
|
+
const diffResult = gitQuiet(ctx.gitDir, ['diff', '--name-only', `${ctx.base}..${ctx.head}`, '--', ...changedFiles], { trim: true })
|
|
735
|
+
if (!diffResult || diffResult.trim() === '') {
|
|
736
|
+
blockedTasks.push(taskId)
|
|
737
|
+
}
|
|
738
|
+
} catch (e) {
|
|
739
|
+
// diff 校验失败保守处理:阻断(避免误批量)
|
|
740
|
+
blockedTasks.push(taskId)
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// blockedTasks 非空 → 阻断批量
|
|
747
|
+
if (blockedTasks.length > 0) {
|
|
748
|
+
const reason = `以下 task 复核未过(review 缺失或草稿零 diff):${blockedTasks.join(', ')}`
|
|
749
|
+
console.warn(`⚠️ 批量完成被阻断:${reason}`)
|
|
750
|
+
return { batched: false, aligned: 0, reason, blockedTasks }
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// 全部复核通过 → 批量标 completed
|
|
551
754
|
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
552
755
|
let aligned = 0
|
|
553
756
|
for (const step of steps) {
|
|
@@ -558,8 +761,9 @@ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBa
|
|
|
558
761
|
}
|
|
559
762
|
}
|
|
560
763
|
return { batched: true, aligned }
|
|
561
|
-
} catch {
|
|
562
|
-
|
|
764
|
+
} catch (e) {
|
|
765
|
+
// 异常 fail-open:不批量,reason 带错误信息
|
|
766
|
+
return { batched: false, aligned: 0, reason: `批量完成检测异常:${e && e.message ? e.message : e}` }
|
|
563
767
|
}
|
|
564
768
|
}
|
|
565
769
|
|
|
@@ -759,6 +963,8 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
759
963
|
progress.lastActive = now
|
|
760
964
|
pm._write(cwd, progress, changeName)
|
|
761
965
|
triggerSync(cwd, changeName, platformOpts)
|
|
966
|
+
// wait 解除持久化点(含 repeatable 多轮)同样刷新 change title——多轮确认期间 design.md 已存在
|
|
967
|
+
refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName)
|
|
762
968
|
|
|
763
969
|
console.log(`✅ Step ${currentIdx + 1}/${stageData.steps.length} 已继续:${currentStep.name}`)
|
|
764
970
|
console.log(` 回答:${answer}`)
|
|
@@ -815,12 +1021,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
815
1021
|
if (_stageGatesResult?.stageCompleted === false) process.exitCode = 1
|
|
816
1022
|
if (_stageGatesResult) return _stageGatesResult
|
|
817
1023
|
// 完整流程 change title 刷新(同 completeStep 完成分支,wait 解除后阶段完成也刷新)。
|
|
818
|
-
|
|
819
|
-
try {
|
|
820
|
-
const refinedTitle = deriveTitleFromLinkedChange(specBase, changeName)
|
|
821
|
-
if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle })
|
|
822
|
-
} catch { /* title 刷新失败不阻断完成 */ }
|
|
823
|
-
}
|
|
1024
|
+
refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName)
|
|
824
1025
|
// gate 全过:persist completed(task-03 移后;此处无 triggerSync)。
|
|
825
1026
|
pm._write(cwd, progress, changeName)
|
|
826
1027
|
console.log(`\n✅ ${stageName} 阶段已完成(${stageData.steps.length}/${stageData.steps.length} 步)`)
|