sillyspec 3.25.4 → 3.25.6
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-brainstorm/SKILL.md +27 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +56 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +26 -0
- package/.claude/skills/sillyspec-propose/SKILL.md +26 -0
- package/.claude/skills/sillyspec-scan/SKILL.md +12 -7
- package/.claude/skills/sillyspec-verify/SKILL.md +2 -2
- package/package.json +1 -1
- package/src/change-list.js +114 -12
- package/src/change-risk-profile.js +36 -1
- package/src/constants.js +11 -0
- package/src/index.js +4 -1
- package/src/machine-interface.js +5 -1
- package/src/progress/stage-machine.js +17 -14
- package/src/run/command.js +58 -3
- package/src/run/complete-handlers.js +9 -3
- package/src/run/complete.js +130 -33
- package/src/run/gates.js +37 -0
- package/src/run/prompt.js +20 -0
- package/src/run/quick-audit.js +8 -1
- package/src/run/scan-profile.js +29 -10
- package/src/run/shared.js +161 -4
- package/src/run/stage.js +4 -0
- package/src/scan-postcheck.js +25 -11
- package/src/stage-contract.js +39 -8
- package/src/stage-review.js +16 -5
- package/src/stages/archive.js +13 -9
- package/src/stages/brainstorm-auto.js +3 -2
- package/src/stages/brainstorm.js +13 -16
- package/src/stages/execute.js +4 -3
- package/src/stages/plan-postcheck.js +1 -1
- package/src/stages/plan.js +13 -0
- package/src/stages/propose.js +3 -2
- package/src/stages/quick.js +31 -31
- package/src/stages/scan.js +2 -1
- package/src/stages/status.js +2 -2
- package/src/stages/verify.js +19 -8
- package/src/task-review.js +308 -3
- package/src/verify-postcheck.js +353 -38
- package/src/worktree-apply.js +134 -67
- package/src/worktree.js +106 -32
- package/templates/prompts/verify-probes.md +13 -2
package/src/run/command.js
CHANGED
|
@@ -23,7 +23,7 @@ import { join, resolve, dirname } from 'node:path'
|
|
|
23
23
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
|
|
24
24
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
25
25
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
26
|
-
import { resolveSpecDir, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, didYouMean, assertSafeChangeName } from './shared.js'
|
|
26
|
+
import { resolveSpecDir, countAncestorSpecDirs, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift } from './shared.js'
|
|
27
27
|
import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
28
28
|
import { outputStep } from './prompt.js'
|
|
29
29
|
import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
|
|
@@ -195,6 +195,20 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
195
195
|
console.error(' 这些动作互斥,请只保留其中一个再重试。')
|
|
196
196
|
process.exit(2)
|
|
197
197
|
}
|
|
198
|
+
// scan profile flag 互斥:--quick/--standard/--deep 同时给≥2 个语义矛盾(computeScanProfile
|
|
199
|
+
// 只取首个命中、其余静默忽略 → agent 以为生效的其实没生效,与 STEP_ACTIONS 同类风险)。
|
|
200
|
+
const PROFILE_FLAGS = [
|
|
201
|
+
['--quick', flags.includes('--quick'), 'quick profile(快速接入,4 份核心文档)'],
|
|
202
|
+
['--standard', flags.includes('--standard'), 'standard profile(压缩步骤)'],
|
|
203
|
+
['--deep', flags.includes('--deep'), 'deep profile(完整扫描)'],
|
|
204
|
+
]
|
|
205
|
+
const activeProfiles = PROFILE_FLAGS.filter(([, v]) => v)
|
|
206
|
+
if (activeProfiles.length >= 2) {
|
|
207
|
+
console.error('❌ scan profile 参数冲突(同时只能指定一个):')
|
|
208
|
+
for (const [name, , desc] of activeProfiles) console.error(` ${name} — ${desc}`)
|
|
209
|
+
console.error(' 这三档互斥,请只保留其中一个再重试。')
|
|
210
|
+
process.exit(2)
|
|
211
|
+
}
|
|
198
212
|
// 注:--non-interactive 与 --interactive 的冲突在 index.js 检测(--interactive 被 index.js
|
|
199
213
|
// 作为全局 flag 吞掉、不透传到此处,故 isInteractive 在此恒为 false)。
|
|
200
214
|
const waitReason = getFlagValue('--reason')
|
|
@@ -279,6 +293,17 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
279
293
|
// runCommand 后续所有 .sillyspec/ 操作必须用 specBase
|
|
280
294
|
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
281
295
|
|
|
296
|
+
// 漂移提醒:cwd 祖先链 ≥2 个 .sillyspec = monorepo 多实例,当前命中的「最近」实例
|
|
297
|
+
// 可能不是用户意图的项目(如 cd 进被独立 scan 的子项目跑测试后忘回根)。
|
|
298
|
+
// 平台模式 / 显式 --spec-dir 跳过(已明确指定,无歧义)。仅提醒不阻断。
|
|
299
|
+
if (!platformOpts.specRoot && !specDir) {
|
|
300
|
+
const ancestorCount = countAncestorSpecDirs(cwd)
|
|
301
|
+
if (ancestorCount >= 2) {
|
|
302
|
+
console.warn(`⚠️ 检测到祖先链有 ${ancestorCount} 个 .sillyspec 实例(monorepo 多实例),当前使用: ${specBase}`)
|
|
303
|
+
console.warn(` 若意图是另一个项目:cd 回该项目根,或用 --spec-dir <根>/.sillyspec 显式指定。`)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
282
307
|
// 平台模式:首次接入时清理旧版本残留的 cwd/.sillyspec/(防止源码污染)。
|
|
283
308
|
// ⚠️ 同 init.js:必须保护真实资产(changes/、projects/、sillyspec.db)。
|
|
284
309
|
// 只在「首次」执行一次——用 cwd 下的 .sillyspec-platform-cleaned 标记文件记录已处理,
|
|
@@ -437,7 +462,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
437
462
|
'--files', '--allow-new', '--force-baseline', '--force-rescan',
|
|
438
463
|
'--json', '--dir', '--help',
|
|
439
464
|
'--reopen', '--from-step', '--mode',
|
|
440
|
-
'--deep', //
|
|
465
|
+
'--deep', '--quick', '--standard', // scan profile 三档显式选择(scan-profile.js 从 argv 读;互斥见下方 PROFILE_FLAGS 检测)
|
|
441
466
|
])
|
|
442
467
|
for (let i = 0; i < flags.length; i++) {
|
|
443
468
|
const f = flags[i]
|
|
@@ -497,6 +522,21 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
497
522
|
process.exit(2) // 用法错(execute 必须显式 --change)→ exit 2
|
|
498
523
|
}
|
|
499
524
|
|
|
525
|
+
// worktree 副本漂移硬守卫(坑 worktree-execute-spec-drift):specBase 命中 worktree 内 .sillyspec
|
|
526
|
+
// checkout 副本 → 进度/产出会写分裂库。必须在 validateChangeExists 之前——副本里 change 目录真实
|
|
527
|
+
// 存在,存在性校验会被骗放行。覆盖 plan/execute/verify/archive(validateChangeExists 同 Set):
|
|
528
|
+
// 都要求 change 已存在、都被副本骗、都写 .sillyspec 资产(archive 尤甚:在副本里移动 change+commit)。
|
|
529
|
+
// 平台模式/显式 --spec-dir 跳过(已明确指定,与 line~285 warn / ~529 quick 守卫条件对齐)。
|
|
530
|
+
if (!platformOpts.specRoot && !specDir
|
|
531
|
+
&& ['plan', 'execute', 'verify', 'archive'].includes(stageName)) {
|
|
532
|
+
const wt = detectWorktreeSpecDrift(specBase)
|
|
533
|
+
if (wt) {
|
|
534
|
+
console.error(`❌ ${wt.message}`)
|
|
535
|
+
console.error(` 当前 cwd=${cwd},命中的 spec=${specBase}。`)
|
|
536
|
+
process.exit(2) // 环境错(cwd/spec 漂移)→ exit 2
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
500
540
|
// --change 变更名存在性校验(治 cwd 漂移误匹配,缺陷 execute-in-place-windows-pitfalls 坑5):
|
|
501
541
|
// 必须在 pm.read/initChange 之前——initChange 会为 --change 指定的新名字创建 changes/ 目录,
|
|
502
542
|
// 校验若在其后则目录已存在、永远通过。用原始 changeName(--change 参数),覆盖 plan/execute/
|
|
@@ -509,6 +549,21 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
509
549
|
process.exit(2) // 环境错(cwd/spec 漂移)→ exit 2
|
|
510
550
|
}
|
|
511
551
|
|
|
552
|
+
// quick 专用 cwd 漂移 fail-fast 守卫(坑 quick-cwd-drift-splits-specdir):
|
|
553
|
+
// quick 被 validateChangeExists 的 sessionId 豁免(quick-<8hex> 不在 changes/ 下),漂移时除上方
|
|
554
|
+
// countAncestorSpecDirs 的 warn 外无硬守卫 → 无声分裂(progress/artifact/QUICKLOG 落子项目、根
|
|
555
|
+
// 会话停滞)。这里补:当前 specBase 无本 session guard、但祖先链别处有 = 跨 specDir 漂移 → fail-fast。
|
|
556
|
+
// 平台模式/显式 --spec-dir 跳过(specBase 已明确,与 line~285 warn 条件对齐)。
|
|
557
|
+
if (stageName === 'quick' && changeName && /^quick-[0-9a-f]{8}$/.test(changeName)
|
|
558
|
+
&& !platformOpts.specRoot && !specDir) {
|
|
559
|
+
const drift = detectQuickSessionDrift(cwd, specBase, changeName)
|
|
560
|
+
if (drift) {
|
|
561
|
+
console.error(`❌ ${drift.message}`)
|
|
562
|
+
console.error(` 当前 cwd=${cwd},命中的 spec=${specBase}。`)
|
|
563
|
+
process.exit(2) // 环境错(cwd/spec 漂移)→ exit 2
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
512
567
|
let progress = await pm.read(cwd, changeName)
|
|
513
568
|
|
|
514
569
|
if (!progress) {
|
|
@@ -884,7 +939,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
884
939
|
// Auto 模式下 brainstorm 使用 artifact-first 步骤
|
|
885
940
|
if (stage === 'brainstorm') {
|
|
886
941
|
const existingSteps = progress.stages?.brainstorm?.steps
|
|
887
|
-
const isAutoModeSteps = existingSteps?.length === 4 && existingSteps?.[0]?.name === '
|
|
942
|
+
const isAutoModeSteps = existingSteps?.length === 4 && existingSteps?.[0]?.name === '进度确认与上下文加载'
|
|
888
943
|
if (!isAutoModeSteps) {
|
|
889
944
|
if (!progress.stages) progress.stages = {}
|
|
890
945
|
progress.stages.brainstorm = {
|
|
@@ -29,6 +29,7 @@ import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
|
|
|
29
29
|
import { printQuickAuditReview } from './quick-audit.js'
|
|
30
30
|
import { validateQuickResult, allocateQuicklogEntry, findQuicklogEntry, completeQuicklogEntry } from '../quicklog.js'
|
|
31
31
|
import { getRule } from '../stage-contract-spec.js'
|
|
32
|
+
import { archiveDestDirName } from '../stage-contract.js'
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* 清洗项目名:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符。
|
|
@@ -101,7 +102,8 @@ async function archiveChangeDirectory(pm, cwd, progress, specBase) {
|
|
|
101
102
|
const archiveDir = join(changesDir, 'archive')
|
|
102
103
|
const srcDir = join(changesDir, archiveChangeName)
|
|
103
104
|
const date = new Date().toISOString().slice(0, 10)
|
|
104
|
-
const
|
|
105
|
+
const destName = archiveDestDirName(date, archiveChangeName)
|
|
106
|
+
const destDir = join(archiveDir, destName)
|
|
105
107
|
|
|
106
108
|
if (!existsSync(srcDir)) {
|
|
107
109
|
console.error(`❌ 归档失败:源目录不存在 ${srcDir}`)
|
|
@@ -157,7 +159,7 @@ async function archiveChangeDirectory(pm, cwd, progress, specBase) {
|
|
|
157
159
|
console.warn(`⚠️ 归档 worktree 清理失败(不阻断归档): ${e.message}`)
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
console.log(`📦 已归档:${archiveChangeName} → archive/${
|
|
162
|
+
console.log(`📦 已归档:${archiveChangeName} → archive/${destName}/`)
|
|
161
163
|
return destDir
|
|
162
164
|
}
|
|
163
165
|
/**
|
|
@@ -767,6 +769,9 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
|
|
|
767
769
|
source_commit_error: sourceCommit === null ? (sourceCommitError || 'unknown') : undefined,
|
|
768
770
|
generated_at: new Date().toISOString(),
|
|
769
771
|
schema_version: 1,
|
|
772
|
+
scan_profile: stageData.scanProfile
|
|
773
|
+
? { mode: stageData.scanProfile.mode, reason: stageData.scanProfile.reason }
|
|
774
|
+
: null,
|
|
770
775
|
postcheck_result_path: null,
|
|
771
776
|
workflow_runs_dir: platformOpts.runtimeRoot
|
|
772
777
|
? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown', 'workflow-runs')
|
|
@@ -795,6 +800,7 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
|
|
|
795
800
|
projectListParsed: stageData.scanMeta?.projectListParsed ?? null,
|
|
796
801
|
manifestWritten: stageData.scanMeta?.manifestWritten ?? null,
|
|
797
802
|
},
|
|
803
|
+
scanProfile: stageData.scanProfile || null,
|
|
798
804
|
})
|
|
799
805
|
printScanPostCheckResult(postResult)
|
|
800
806
|
|
|
@@ -869,7 +875,7 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
|
|
|
869
875
|
// 非 platform 模式 scan 也做轻量 post-check + 结构化输出
|
|
870
876
|
if (stageName === 'scan' && !platformOpts.specRoot && !platformOpts.runtimeRoot) {
|
|
871
877
|
const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult } = await import('../scan-postcheck.js')
|
|
872
|
-
const postResult = runScanPostCheck({ cwd, specDir: null, outputText })
|
|
878
|
+
const postResult = runScanPostCheck({ cwd, specDir: null, outputText, scanProfile: stageData.scanProfile || null })
|
|
873
879
|
printScanPostCheckResult(postResult)
|
|
874
880
|
// 结构化结果写入 .sillyspec/.runtime/
|
|
875
881
|
const structured = formatStructuredResult(postResult, { source_root: cwd })
|
package/src/run/complete.js
CHANGED
|
@@ -18,7 +18,7 @@ import { join } from 'node:path'
|
|
|
18
18
|
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, appendFileSync, statSync } from 'node:fs'
|
|
19
19
|
import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions } from './shared.js'
|
|
20
20
|
import { outputStep } from './prompt.js'
|
|
21
|
-
import { enforceDepsGate, runStageCompletionGates } from './gates.js'
|
|
21
|
+
import { enforceDepsGate, enforceReviewJsonGate, runStageCompletionGates } from './gates.js'
|
|
22
22
|
import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact, handleExecuteWorktreeCleanup, handleScanStageCompleted } from './complete-handlers.js'
|
|
23
23
|
import { stageRegistry } from '../stages/index.js'
|
|
24
24
|
import { formatExecuteSummary } from '../worktree-apply.js'
|
|
@@ -174,6 +174,9 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
174
174
|
|
|
175
175
|
// ── execute deps 验证硬门(change 2026-06-28-worktree-deps-provision)──
|
|
176
176
|
await enforceDepsGate(stageName, cwd, changeName, steps[currentIdx], steps, currentIdx, specBase, platformOpts)
|
|
177
|
+
// review.json 字段硬门(坑 review-json-field-gap):已勾 [x] task 的 review.json 必须 schema 完整,
|
|
178
|
+
// 提前到每次 --done 校验(而非等 Task Review Gate 在整阶段收尾才暴露,迫使用户事后批量补)。
|
|
179
|
+
await enforceReviewJsonGate(stageName, cwd, changeName, steps[currentIdx], steps, currentIdx, specBase, platformOpts)
|
|
177
180
|
|
|
178
181
|
steps[currentIdx].status = 'completed'
|
|
179
182
|
steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
@@ -211,6 +214,39 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
211
214
|
// execute Wave artifact(W6 Step6c 抽至 complete-handlers.js handleExecuteWaveArtifact)
|
|
212
215
|
await handleExecuteWaveArtifact({ stageName, steps, currentIdx, changeName, specBase, cwd })
|
|
213
216
|
|
|
217
|
+
// execute 批量完成检测:plan 全勾 + 代码客观核验通过 → 剩余 step 一次性标 completed,
|
|
218
|
+
// 使本次 --done 直接进入阶段完成分支(治"3 Wave 做完仍逐次 +1、需重走 7 次 --done")。
|
|
219
|
+
// 先按 review.json pass 自动勾 plan checkbox(复用 continueStep 同源逻辑),再判批量条件。
|
|
220
|
+
if (stageName === 'execute' && changeName) {
|
|
221
|
+
const _ac = await autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts })
|
|
222
|
+
if (_ac.autoChecked) {
|
|
223
|
+
console.log(` ✅ 自动勾选 ${_ac.checkedCount} 个 task checkbox(基于 review.json pass)`)
|
|
224
|
+
}
|
|
225
|
+
if (_ac.skippedCount > 0) {
|
|
226
|
+
console.warn(` ⚠️ ${_ac.skippedCount} 个 task 未勾(review.json 缺失/fail)→ 批量完成条件不满足,仍按单步推进`)
|
|
227
|
+
}
|
|
228
|
+
const _bf = await detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, steps })
|
|
229
|
+
if (_bf.batched && _bf.aligned > 0) {
|
|
230
|
+
console.log(`\n🚀 execute 批量完成:plan 全勾 + 代码核验通过,一次性补完 ${_bf.aligned} 个剩余 step → 进入阶段完成分支`)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// per-task review 草稿兜底(坑 worktree-execute-apply-friction 坑2):主 agent 直接实现模式
|
|
234
|
+
// 无子代理 review 落盘 → Task Review Gate 报缺 review.json 阻断。每次 execute --done 跑(幂等,
|
|
235
|
+
// 已存在跳过),据 git diff base..head 按 allowed_paths 归属生成 cannot_verify 草稿,进 gate 前就位。
|
|
236
|
+
try {
|
|
237
|
+
const { generateTaskReviewDrafts } = await import('../task-review.js')
|
|
238
|
+
const _drafts = await generateTaskReviewDrafts({ changeName, cwd, platformOpts })
|
|
239
|
+
if (_drafts.generated > 0) {
|
|
240
|
+
console.log(' 📄 自动补写 ' + _drafts.generated + ' 个 per-task review.json 草稿(cannot_verify,主 agent 实现模式兜底,需 agent 复核后升级为 pass/fail)')
|
|
241
|
+
if (_drafts.unattributed && _drafts.unattributed.length > 0) {
|
|
242
|
+
console.warn(' ⚠️ ' + _drafts.unattributed.length + ' 个变更文件未归属任何 task(顺带修复/非源码),草稿未覆盖:' + _drafts.unattributed.join(', '))
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} catch (e) {
|
|
246
|
+
console.warn(' ⚠️ per-task review 草稿生成失败(不阻断 execute 完成):' + (e && e.message ? e.message : e))
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
214
250
|
const nextPendingIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
215
251
|
|
|
216
252
|
if (nextPendingIdx === -1) {
|
|
@@ -349,6 +385,92 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
349
385
|
}
|
|
350
386
|
|
|
351
387
|
// ── Step 调度兄弟函数(W6 Step7b-2 从 run.js 搬入:skip/wait/continue + formatWaitHistory)──
|
|
388
|
+
/**
|
|
389
|
+
* plan.md checkbox 自动勾选:execute 完成 + 各 task review.json 双 verdict 非 fail → 自动勾选。
|
|
390
|
+
* 治本于"plan 全靠手动勾、易遗漏"——以 review.json pass 为客观依据勾选。供 continueStep 完成段
|
|
391
|
+
* 与 completeStep 的 execute 批量完成检测复用(避免两处复制)。
|
|
392
|
+
*
|
|
393
|
+
* @returns {{autoChecked:boolean, checkedCount:number, skippedCount:number, planTotal:number, planChecked:number}}
|
|
394
|
+
* planTotal/planChecked 在调用方于本函数之后用 readPlanCheckboxStatus 重读(勾选已落盘)。
|
|
395
|
+
*/
|
|
396
|
+
async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts }) {
|
|
397
|
+
if (stageName !== 'execute' || !changeName) {
|
|
398
|
+
return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
const specBaseLc = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
402
|
+
const changeDir = join(specBaseLc, 'changes', changeName)
|
|
403
|
+
const planPath = join(changeDir, 'plan.md')
|
|
404
|
+
const runtimeRoot = platformOpts?.runtimeRoot || join(specBaseLc, '.runtime')
|
|
405
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
406
|
+
if (!existsSync(planPath) || !existsSync(runIdFile)) {
|
|
407
|
+
return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
|
|
408
|
+
}
|
|
409
|
+
const executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
410
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
411
|
+
const { readReview } = await import('../task-review.js')
|
|
412
|
+
let checkedCount = 0
|
|
413
|
+
let skippedCount = 0
|
|
414
|
+
const updated = planContent.replace(/^(\s*[-*]\s*\[)\s(\]\s*task-\d+)/gim, (match, p1, p2) => {
|
|
415
|
+
const taskNum = match.match(/task-(\d+)/)[1].padStart(2, '0')
|
|
416
|
+
const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', `task-${taskNum}`, 'review.json')
|
|
417
|
+
const r = readReview(reviewPath)
|
|
418
|
+
if (r.ok && r.review?.specVerdict !== 'fail' && r.review?.qualityVerdict !== 'fail') {
|
|
419
|
+
checkedCount++
|
|
420
|
+
return `${p1}x${p2}` // 勾选
|
|
421
|
+
}
|
|
422
|
+
skippedCount++
|
|
423
|
+
return match // 不勾
|
|
424
|
+
})
|
|
425
|
+
if (checkedCount > 0) {
|
|
426
|
+
writeFileSync(planPath, updated)
|
|
427
|
+
}
|
|
428
|
+
return { autoChecked: checkedCount > 0, checkedCount, skippedCount }
|
|
429
|
+
} catch {
|
|
430
|
+
return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* execute 批量完成检测:plan.md 所有 task checkbox 已勾 + 代码客观核验非零变更 →
|
|
436
|
+
* 把剩余 pending/in-progress step 一次性标 completed,使 completeStep 本次 --done 即进入阶段完成
|
|
437
|
+
* 分支(而非逐次 +1)。治"3 Wave 全做完仍显示未开工、需重走 7 次 --done"。
|
|
438
|
+
* 复用现成能力:readPlanCheckboxStatus(plan 勾选状态)+ checkExecuteCodeEvidence(代码客观核验,
|
|
439
|
+
* 与 doctor --align-execute-progress 同源,D-002/D-004)。仅在 stageName==='execute' && changeName 触发。
|
|
440
|
+
*
|
|
441
|
+
* 安全门:plan 零 checkbox / 未全勾 / 代码零变更(unchanged)均不批量——信任 plan 声明但用代码核验兜底。
|
|
442
|
+
* @returns {Promise<{batched:boolean, aligned:number, reason?:string}>}
|
|
443
|
+
*/
|
|
444
|
+
async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, steps }) {
|
|
445
|
+
if (stageName !== 'execute' || !changeName) return { batched: false, aligned: 0 }
|
|
446
|
+
try {
|
|
447
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
448
|
+
const { total: planTotal, checked: planChecked } = pm.readPlanCheckboxStatus(changeDir)
|
|
449
|
+
if (planTotal === 0) return { batched: false, aligned: 0, reason: 'plan.md 无 task checkbox' }
|
|
450
|
+
if (planChecked < planTotal) {
|
|
451
|
+
return { batched: false, aligned: 0, reason: `plan 未全勾(${planChecked}/${planTotal})` }
|
|
452
|
+
}
|
|
453
|
+
// plan 全勾 → 代码客观核验(防 plan 被手动勾伪造导致空完成)
|
|
454
|
+
const { checkExecuteCodeEvidence } = await import('../stage-contract.js')
|
|
455
|
+
const evidence = checkExecuteCodeEvidence(cwd, changeName)
|
|
456
|
+
if (evidence.status === 'unchanged') {
|
|
457
|
+
return { batched: false, aligned: 0, reason: `代码零变更(${evidence.detail})` }
|
|
458
|
+
}
|
|
459
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
460
|
+
let aligned = 0
|
|
461
|
+
for (const step of steps) {
|
|
462
|
+
if (step.status === 'pending' || step.status === 'in-progress') {
|
|
463
|
+
step.status = 'completed'
|
|
464
|
+
step.completedAt = now
|
|
465
|
+
aligned++
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return { batched: true, aligned }
|
|
469
|
+
} catch {
|
|
470
|
+
return { batched: false, aligned: 0 }
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
352
474
|
function formatWaitHistory(step) {
|
|
353
475
|
const answers = Array.isArray(step.waitAnswers) ? step.waitAnswers : []
|
|
354
476
|
if (answers.length === 0) return null
|
|
@@ -621,38 +743,13 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
621
743
|
console.log(` ⚠️ 若 worktree 改动还没 apply 到主工作区,先:sillyspec worktree apply ${changeName}`)
|
|
622
744
|
console.log(` (apply 不需要先 commit,支持 working tree 未提交改动)`)
|
|
623
745
|
// plan.md checkbox auto-check:execute 完成 + review.json pass → 自动勾选(治本,比警告可靠)
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
632
|
-
const planContent = readFileSync(planPath, 'utf8')
|
|
633
|
-
const { readReview } = await import('../task-review.js')
|
|
634
|
-
let checkedCount = 0
|
|
635
|
-
let skippedCount = 0
|
|
636
|
-
const updated = planContent.replace(/^(\s*[-*]\s*\[)\s(\]\s*task-\d+)/gim, (match, p1, p2) => {
|
|
637
|
-
const taskNum = match.match(/task-(\d+)/)[1].padStart(2, '0')
|
|
638
|
-
const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', `task-${taskNum}`, 'review.json')
|
|
639
|
-
const r = readReview(reviewPath)
|
|
640
|
-
if (r.ok && r.review?.specVerdict !== 'fail' && r.review?.qualityVerdict !== 'fail') {
|
|
641
|
-
checkedCount++
|
|
642
|
-
return `${p1}x${p2}` // 勾选
|
|
643
|
-
}
|
|
644
|
-
skippedCount++
|
|
645
|
-
return match // 不勾
|
|
646
|
-
})
|
|
647
|
-
if (checkedCount > 0) {
|
|
648
|
-
writeFileSync(planPath, updated)
|
|
649
|
-
console.log(` ✅ 自动勾选 ${checkedCount} 个 task checkbox(基于 review.json pass)`)
|
|
650
|
-
}
|
|
651
|
-
if (skippedCount > 0) {
|
|
652
|
-
console.warn(` ⚠️ ${skippedCount} 个 task 未勾(review.json 缺失/fail)→ archive 会拦。补 review 后重跑 execute --done 触发自动勾`)
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
} catch {}
|
|
746
|
+
const _ac = await autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts })
|
|
747
|
+
if (_ac.autoChecked) {
|
|
748
|
+
console.log(` ✅ 自动勾选 ${_ac.checkedCount} 个 task checkbox(基于 review.json pass)`)
|
|
749
|
+
}
|
|
750
|
+
if (_ac.skippedCount > 0) {
|
|
751
|
+
console.warn(` ⚠️ ${_ac.skippedCount} 个 task 未勾(review.json 缺失/fail)→ archive 会拦。补 review 后重跑 execute --done 触发自动勾`)
|
|
752
|
+
}
|
|
656
753
|
}
|
|
657
754
|
}
|
|
658
755
|
return { stageCompleted: true, currentIdx, nextPendingIdx: -1 }
|
package/src/run/gates.js
CHANGED
|
@@ -96,6 +96,39 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
|
|
|
96
96
|
}
|
|
97
97
|
process.exit(1)
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* execute --done 硬门:已勾 [x] task 的 review.json 必须 schema 完整(坑 review-json-field-gap)。
|
|
102
|
+
*
|
|
103
|
+
* Task Review Gate(validateTaskReviews)只在 execute 整阶段完成时跑(complete.js 阶段完成分支的
|
|
104
|
+
* actualCompleted===actualTotal 守卫),单 task --done 不校验 → 子代理勾 checkbox 却漏写/漏字段
|
|
105
|
+
* review.json,要到收尾才暴露,用户被迫事后批量补。本门提前到每次 --done:校验 plan 里所有已勾
|
|
106
|
+
* task 的 review.json,缺字段/不存在/JSON 坏 → 置 step=blocked + exit(1),与 enforceDepsGate 同范式。
|
|
107
|
+
* 未勾 task 不校验(还没做)。平台模式/无 marker/无 plan 时放行(下游 Task Review Gate 兜底)。
|
|
108
|
+
*/
|
|
109
|
+
export async function enforceReviewJsonGate(stageName, cwd, changeName, step, steps, currentIdx, specBase, platformOpts) {
|
|
110
|
+
if (stageName !== 'execute' || !changeName) return true
|
|
111
|
+
const runtimeRoot = platformOpts?.runtimeRoot || join(specBase, '.runtime')
|
|
112
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
113
|
+
const planPath = join(specBase, 'changes', changeName, 'plan.md')
|
|
114
|
+
if (!existsSync(runIdFile) || !existsSync(planPath)) return true
|
|
115
|
+
const executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
116
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
117
|
+
const { validateCheckedTaskReviews } = await import('../task-review.js')
|
|
118
|
+
const result = validateCheckedTaskReviews({ planContent, runtimeRoot, executeRunId })
|
|
119
|
+
if (result.ok) return true
|
|
120
|
+
if (steps && steps[currentIdx]) steps[currentIdx].status = 'blocked'
|
|
121
|
+
console.error('❌ ── review.json 字段校验阻断(本次 --done 未完成,进度未推进)──')
|
|
122
|
+
console.error(' 已勾选 [x] 的 task review.json 不完整(铁律:勾 checkbox 前必须先写完整 review.json):')
|
|
123
|
+
for (const f of result.failures) {
|
|
124
|
+
const kindLabel = f.kind === 'missing' ? 'review.json 不存在' : (f.kind === 'parseError' ? 'JSON 解析失败' : '字段缺失')
|
|
125
|
+
console.error(` • ${f.taskId}(${kindLabel}): ${f.reviewPath}`)
|
|
126
|
+
for (const e of f.errors) console.error(` - ${e}`)
|
|
127
|
+
}
|
|
128
|
+
console.error(' 修复:补全上述 review.json 字段后重跑 execute --done。')
|
|
129
|
+
process.exit(1)
|
|
130
|
+
}
|
|
131
|
+
|
|
99
132
|
/**
|
|
100
133
|
* 阶段完成校验失败时回滚状态。
|
|
101
134
|
*
|
|
@@ -183,6 +216,10 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
183
216
|
const { runVerifyParityCheck, printVerifyParityCheck } = await import('../verify-postcheck.js')
|
|
184
217
|
const parityCheck = runVerifyParityCheck({ cwd, specBase, changeName, runtimeRoot: platformOpts?.runtimeRoot })
|
|
185
218
|
printVerifyParityCheck(parityCheck)
|
|
219
|
+
// ── 删除探针(advisory,不阻断):切斯特顿栅栏护栏,用 git 事实对账静默删除代码 ──
|
|
220
|
+
const { runVerifyDeletionCheck, printVerifyDeletionCheck } = await import('../verify-postcheck.js')
|
|
221
|
+
const deletionCheck = runVerifyDeletionCheck({ cwd, specBase, changeName })
|
|
222
|
+
printVerifyDeletionCheck(deletionCheck)
|
|
186
223
|
console.log('\n✅ 验证通过,下一步:sillyspec run archive')
|
|
187
224
|
}
|
|
188
225
|
|
package/src/run/prompt.js
CHANGED
|
@@ -517,6 +517,26 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
517
517
|
}
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
+
// archive Step1「任务完成度检查」客观真相源注入:以 review.json verdict 算完成度,
|
|
521
|
+
// 替代「机械数 plan.md checkbox」(checkbox 依赖 autoCheckPlanFromReviews 回填,runId marker /
|
|
522
|
+
// review 缺失时回填静默 no-op,会停在未勾态导致完成度失真 → archive 误判「全未完成」)。
|
|
523
|
+
// summarizeTaskCompletion 内部已 fail-safe 降级(无 plan / 无 runId → checkbox 统计 + 标注 source),
|
|
524
|
+
// 此处仅兜底注入异常,绝不阻断 archive。
|
|
525
|
+
if (stageName === 'archive' && promptText.includes('{TASK_COMPLETION_REPORT}')) {
|
|
526
|
+
try {
|
|
527
|
+
const { summarizeTaskCompletion } = await import('../task-review.js')
|
|
528
|
+
const tcrSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
529
|
+
const tcrRuntimeRoot = platformOpts?.runtimeRoot || join(tcrSpecBase, '.runtime')
|
|
530
|
+
const tcrChangeDir = changeName ? join(tcrSpecBase, 'changes', changeName) : null
|
|
531
|
+
const summary = tcrChangeDir
|
|
532
|
+
? summarizeTaskCompletion({ changeDir: tcrChangeDir, runtimeRoot: tcrRuntimeRoot, changeName })
|
|
533
|
+
: null
|
|
534
|
+
promptText = promptText.split('{TASK_COMPLETION_REPORT}').join(summary ? summary.report : '(无法计算完成度:变更目录缺失)')
|
|
535
|
+
} catch (e) {
|
|
536
|
+
promptText = promptText.split('{TASK_COMPLETION_REPORT}').join('(完成度计算异常:' + e.message + ',请手动核对 plan.md 与 .runtime/execute-runs/*/tasks/task-NN/review.json)')
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
520
540
|
// 注入模块上下文(brainstorm/plan/execute 阶段,基于 Module Context Index)
|
|
521
541
|
if (['brainstorm', 'plan', 'execute'].includes(stageName) && projectName) {
|
|
522
542
|
const effectiveSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
package/src/run/quick-audit.js
CHANGED
|
@@ -34,7 +34,14 @@ export function printQuickAuditReview(review) {
|
|
|
34
34
|
console.warn(` - ${r}`)
|
|
35
35
|
}
|
|
36
36
|
} else {
|
|
37
|
-
|
|
37
|
+
// 区分「本轮新增」(changedFiles,已扣前序 baseline)vs「累计暂存」(stagedTotal,全部未提交)。
|
|
38
|
+
// 仅当存在前序 baseline 残留(累计 > 本轮新增)时才追加括注,避免普通场景冗余。
|
|
39
|
+
const stagedTotal = typeof review.stagedTotal === 'number' ? review.stagedTotal : review.changedFiles.length
|
|
40
|
+
if (stagedTotal > review.changedFiles.length) {
|
|
41
|
+
console.log(`\n✅ quick 变更边界审计 — SAFE (本轮新增变更 ${review.changedFiles.length} 个文件;累计暂存 ${stagedTotal} 个,含前序 baseline ${stagedTotal - review.changedFiles.length} 个未计入本轮)`)
|
|
42
|
+
} else {
|
|
43
|
+
console.log(`\n✅ quick 变更边界审计 — SAFE (本轮新增变更 ${review.changedFiles.length} 个文件)`)
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
|
package/src/run/scan-profile.js
CHANGED
|
@@ -15,23 +15,36 @@ import { safeGit } from './shared.js'
|
|
|
15
15
|
/**
|
|
16
16
|
* 估算源码规模(文件数 + 字节数)—— 跨平台 Node 遍历,替代 Unix `find`(原生 Windows 无 find,
|
|
17
17
|
* 原 find 失效会让所有 Windows 项目永远 fallback 到 standard profile)。
|
|
18
|
-
*
|
|
19
|
-
* (超限时 fileCount 已远超 quick/standard 阈值,不影响 profile
|
|
18
|
+
* 扩展名限定源码后缀;skipDirs 排除依赖/构建产物/覆盖率等非源码目录;maxFiles 封顶避免超大仓库遍历过久
|
|
19
|
+
* (超限时 fileCount 已远超 quick/standard 阈值,不影响 profile 判定);maxDepth 兜底防极深嵌套产物。
|
|
20
20
|
*/
|
|
21
|
-
export function estimateSourceSize(cwd, maxFiles = 5000) {
|
|
21
|
+
export function estimateSourceSize(cwd, maxFiles = 5000, maxDepth = 6) {
|
|
22
22
|
const sourceExts = new Set(['.js','.ts','.tsx','.py','.java','.go','.rs','.rb','.php','.c','.cpp','.h','.jsx','.vue','.svelte'])
|
|
23
|
-
|
|
23
|
+
// skipDirs:排除依赖、版本控制、构建产物、覆盖率、临时目录等非源码目录。
|
|
24
|
+
// 原清单只排了 node_modules/.git/dist/build 等,漏了 .next/.nuxt/coverage/.svelte-kit 等构建产物,
|
|
25
|
+
// 导致带构建产物的小项目被产物里的海量 .js 拉高 fileCount/sourceBytes → 误判 deep。
|
|
26
|
+
const skipDirs = new Set([
|
|
27
|
+
'node_modules', '.git', 'dist', 'build', 'out', 'target',
|
|
28
|
+
'__pycache__', '.venv', 'venv',
|
|
29
|
+
'.next', '.nuxt', '.output', '.svelte-kit', '.astro', '.turbo', '.parcel-cache',
|
|
30
|
+
'coverage', '.nyc_output', '.vitest', '.cache',
|
|
31
|
+
'vendor', 'bower_components',
|
|
32
|
+
'dist-types',
|
|
33
|
+
'.sillyspec', '.claude',
|
|
34
|
+
'tmp', 'temp', '.tmp', 'logs',
|
|
35
|
+
])
|
|
24
36
|
let fileCount = 0
|
|
25
37
|
let sourceBytes = 0
|
|
26
|
-
|
|
38
|
+
// stack 存 [dir, depth]:maxDepth 兜底,防极深嵌套的产物/生成目录在 skipDirs 漏网时被全遍历。
|
|
39
|
+
const stack = [[cwd, 0]]
|
|
27
40
|
while (stack.length) {
|
|
28
|
-
const dir = stack.pop()
|
|
41
|
+
const [dir, depth] = stack.pop()
|
|
29
42
|
let entries
|
|
30
43
|
try { entries = readdirSync(dir, { withFileTypes: true }) }
|
|
31
44
|
catch { continue }
|
|
32
45
|
for (const e of entries) {
|
|
33
46
|
if (e.isDirectory()) {
|
|
34
|
-
if (!skipDirs.has(e.name)) stack.push(join(dir, e.name))
|
|
47
|
+
if (!skipDirs.has(e.name) && depth < maxDepth) stack.push([join(dir, e.name), depth + 1])
|
|
35
48
|
} else if (e.isFile() && sourceExts.has(extname(e.name))) {
|
|
36
49
|
fileCount++
|
|
37
50
|
try { sourceBytes += statSync(join(dir, e.name)).size } catch {}
|
|
@@ -49,8 +62,14 @@ export function estimateSourceSize(cwd, maxFiles = 5000) {
|
|
|
49
62
|
* deep: 大项目或 --deep → 完整流程
|
|
50
63
|
*/
|
|
51
64
|
export function computeScanProfile(cwd, platformOpts) {
|
|
52
|
-
//
|
|
65
|
+
// 显式 profile flag 优先于自动判定(三档互斥由 command.js 检测;此处取首个命中即可)
|
|
53
66
|
const flags = process.argv.slice(2)
|
|
67
|
+
if (flags.includes('--quick')) {
|
|
68
|
+
return { mode: 'quick', reason: '用户指定 --quick', maxAgentCalls: 0, maxDocs: 5 }
|
|
69
|
+
}
|
|
70
|
+
if (flags.includes('--standard')) {
|
|
71
|
+
return { mode: 'standard', reason: '用户指定 --standard', maxAgentCalls: 1, maxDocs: 8 }
|
|
72
|
+
}
|
|
54
73
|
if (flags.includes('--deep')) {
|
|
55
74
|
return { mode: 'deep', reason: '用户指定 --deep', maxAgentCalls: 4, maxDocs: 99 }
|
|
56
75
|
}
|
|
@@ -115,7 +134,7 @@ export function applyScanProfileSteps(stageData, profile, cwd, platformOpts) {
|
|
|
115
134
|
status: 'pending',
|
|
116
135
|
prompt: `## Quick Scan — 核心文档生成
|
|
117
136
|
|
|
118
|
-
|
|
137
|
+
当前为 quick profile(小项目自动判定 或 用户显式 --quick),一次性生成核心文档用于快速接入。
|
|
119
138
|
|
|
120
139
|
### 操作
|
|
121
140
|
1. 读取项目结构和关键文件(package.json / pyproject.toml / README / 入口文件)
|
|
@@ -126,7 +145,7 @@ export function applyScanProfileSteps(stageData, profile, cwd, platformOpts) {
|
|
|
126
145
|
- **STRUCTURE.md** — 目录树 + 模块说明
|
|
127
146
|
3. 如发现子项目,注册到 \`{PROJECTS_ROOT}/\` 下
|
|
128
147
|
|
|
129
|
-
|
|
148
|
+
每份文档 frontmatter 必须包含:\`author\`、\`created_at\`、\`scan_depth: quick\`(标记快速接入的浅层版本;后续深度扫描 --deep 会识别此标记并覆盖升级为完整文档)。
|
|
130
149
|
|
|
131
150
|
### ⛔ 硬约束
|
|
132
151
|
- **严禁使用子代理(Agent/Task 工具)。** 所有文档在一个 turn 内完成。
|