sillyspec 3.28.3 → 3.28.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
- package/.claude/skills/sillyspec-state/SKILL.md +25 -25
- package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
- package/AGENTS.md +25 -0
- package/INSTRUCTIONS.md +12 -0
- package/package.json +1 -1
- package/results-review-tier.md +18 -0
- package/src/agent-session-log.js +4 -3
- package/src/archive-delta.js +82 -0
- package/src/change-delete.js +2 -1
- package/src/change-list.js +18 -3
- package/src/change-risk-profile.js +77 -6
- package/src/commit-suggest.js +52 -1
- package/src/config-cat.js +17 -1
- package/src/constants.js +3 -1
- package/src/decision-distill.js +45 -5
- package/src/decisions-io.js +96 -0
- package/src/design-facts.js +3 -0
- package/src/dispatch/probe.js +31 -8
- package/src/docs-check.js +102 -3
- package/src/doctor-diagnostics.js +441 -5
- package/src/foreign-declared.js +53 -2
- package/src/index.js +606 -22
- package/src/mcp-server.js +144 -0
- package/src/module-impact.js +84 -12
- package/src/modules.js +46 -34
- package/src/plan-adopt-waves.js +13 -5
- package/src/progress/change-registry.js +34 -0
- package/src/progress.js +75 -2
- package/src/quick-recommend.js +6 -1
- package/src/quicklog.js +70 -8
- package/src/review-dispatch.js +377 -0
- package/src/run/archive-distill.js +48 -0
- package/src/run/command.js +48 -13
- package/src/run/complete-handlers.js +215 -9
- package/src/run/complete.js +184 -2
- package/src/run/gate-snapshot.js +88 -0
- package/src/run/gates.js +206 -58
- package/src/run/prompt.js +150 -14
- package/src/run/quick-audit.js +38 -13
- package/src/run/shared.js +183 -12
- package/src/run/stage.js +52 -2
- package/src/run/verify-quality-scan.js +336 -0
- package/src/runtime-hygiene.js +84 -0
- package/src/scope-audit.js +735 -0
- package/src/sillyhub-mcp/client.js +58 -3
- package/src/spec-sync.js +63 -8
- package/src/stage-contract.js +83 -3
- package/src/stage-review-checklist.js +44 -0
- package/src/stage-review.js +125 -4
- package/src/stages/archive.js +60 -167
- package/src/stages/brainstorm.js +7 -17
- package/src/stages/doctor.js +41 -515
- package/src/stages/execute.js +13 -10
- package/src/stages/plan-postcheck.js +191 -7
- package/src/stages/plan.js +7 -35
- package/src/stages/quick.js +11 -11
- package/src/stages/verify.js +24 -39
- package/src/sync-noise.js +158 -0
- package/src/sync.js +93 -27
- package/src/task-review.js +133 -17
- package/src/taskcard.js +46 -3
- package/src/validate-artifacts.js +163 -0
- package/src/verify-facts-schema.js +141 -0
- package/src/verify-postcheck.js +347 -29
- package/src/verify-probes.js +275 -24
- package/src/workflow.js +6 -0
- package/src/worktree-apply.js +167 -24
- package/src/worktree.js +5 -1
- package/templates/agents-instruction.md +1 -0
package/src/run/complete.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { join } from 'node:path'
|
|
20
20
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
|
|
21
21
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
22
|
+
import { gitQuiet } from '../git-helper.js'
|
|
22
23
|
import { withFileLock } from '../quicklog.js'
|
|
23
24
|
import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir } from './shared.js'
|
|
24
25
|
|
|
@@ -48,7 +49,6 @@ import { formatExecuteSummary } from '../worktree-apply.js'
|
|
|
48
49
|
import { validateDecisionModuleRefs } from '../design-facts.js'
|
|
49
50
|
import { isEndToEndTaskText } from '../change-risk-profile.js'
|
|
50
51
|
import { deriveTitleFromLinkedChange } from '../quicklog.js'
|
|
51
|
-
import { gitQuiet } from '../git-helper.js'
|
|
52
52
|
|
|
53
53
|
// validateMetadata / readDesignScale / validateFileLocations 已迁至 ./gates.js(completeStageGates
|
|
54
54
|
// 共享收尾管线,消除 noAI 末步 / continueStep 完成分支绕过 gate 的 S1/S2/S3 不对称)。
|
|
@@ -97,6 +97,26 @@ export function resolveWaitingStepWithAnswer(steps, doneAnswer, nowStr) {
|
|
|
97
97
|
return waitIdx
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* 事实性步骤摘要合成(P0-2,noai-ir-roadmap §3):--output 省略时由 CLI 按既有事实合成
|
|
102
|
+
* ——步骤名 + 变更窗口(git porcelain 计数与前几个路径)+ 门禁以 CLI 校验输出为准。
|
|
103
|
+
* 纯事实零编造(无「通过/成功」等判断词——判断在 gate,不在摘要);语义性说明(方案
|
|
104
|
+
* 取舍/用户反馈)不合成,摘要自带手写指引。长度控制在 MAX_OUTPUT(200) 截断线内。
|
|
105
|
+
*/
|
|
106
|
+
export function synthesizeStepOutput({ stageName, stepName, cwd }) {
|
|
107
|
+
let windowNote = ''
|
|
108
|
+
try {
|
|
109
|
+
const porcelain = gitQuiet(cwd, ['status', '--porcelain'])
|
|
110
|
+
if (porcelain !== null) {
|
|
111
|
+
const paths = porcelain.split('\n').filter(Boolean).map((l) => (l.slice(3) || '').trim()).filter(Boolean)
|
|
112
|
+
windowNote = paths.length === 0
|
|
113
|
+
? ';工作区无未提交变更'
|
|
114
|
+
: `;变更窗口 ${paths.length} 文件(${paths.slice(0, 3).map((p) => p.split(/[\\/]/).pop()).join('、')}${paths.length > 3 ? ' 等' : ''})`
|
|
115
|
+
}
|
|
116
|
+
} catch { /* git 不可读 → 窗口段缺省(摘要仍成立) */ }
|
|
117
|
+
return `【CLI 合成】步骤「${stepName}」完成${windowNote};门禁以本次 CLI 校验输出为准(方案取舍等语义说明需 --output 手写)`
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
101
121
|
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false, quickFiles = [] } = options
|
|
102
122
|
// specRoot(平台) > specDriftAnchor(worktree 漂移锚定主仓) > cwd/.sillyspec(本地)——
|
|
@@ -158,6 +178,16 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
158
178
|
currentIdx = _resolvedWaitIdx
|
|
159
179
|
console.log(`⚠️ Step "${steps[_resolvedWaitIdx].name}" 此前处于 waiting,--done --answer 已补回答并拉回待完成。`)
|
|
160
180
|
}
|
|
181
|
+
|
|
182
|
+
// ── P0-2(noai-ir-roadmap §3):--output 省略 → CLI 按事实合成步骤摘要 ──
|
|
183
|
+
// agent 手打摘要是每步命中的幻觉源;事实面(步骤名 + 变更窗口 + 门禁以 CLI 校验为准)
|
|
184
|
+
// CLI 本就持有,纯事实合成零编造(判断词留给 gate)。语义性说明(方案取舍/用户反馈)仍应
|
|
185
|
+
// --output 手写——合成摘要自带该指引。quick 阶段除外(末步四字段硬契约
|
|
186
|
+
// validateQuickResult,合成文本过不了四字段校验;quick 的省略提示由其阶段文案承担)。
|
|
187
|
+
if (!outputText && stageName !== 'quick' && currentIdx !== -1 && steps[currentIdx]) {
|
|
188
|
+
outputText = synthesizeStepOutput({ stageName, stepName: steps[currentIdx].name, cwd })
|
|
189
|
+
console.log(`🤖 --output 未提供——CLI 已按事实合成步骤摘要(gate 校验照常;方案取舍等语义说明下次带 --output 手写)。`)
|
|
190
|
+
}
|
|
161
191
|
// ── waiting 前置守卫(坑 archive-step3-wait-answer-hint-late)──
|
|
162
192
|
// 普通 --done(无 --answer)时若存在 waiting 步骤:currentIdx 会跳过它推进后续步骤
|
|
163
193
|
//(findIndex 只查 pending/in-progress),步骤被静默越过 + --answer 要求要到别处报错才暴露。
|
|
@@ -380,11 +410,59 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
380
410
|
} else if (_cliAction === 'progressConfirm') {
|
|
381
411
|
// 与 stage.js noAI 分支同语义(brainstorm/execute/verify step1 进度确认)
|
|
382
412
|
executeProgressConfirm({ stageName, cwd, stageData, changeName, pm })
|
|
413
|
+
} else if (_cliAction === 'verifyRunQualityScan') {
|
|
414
|
+
// 与 stage.js noAI 分支同语义(P0-1 noai-ir-roadmap §3:verify 测试步实测提前 + 指纹复用;
|
|
415
|
+
// 失败 throw → completeStep 不推进,步骤保持 pending)
|
|
416
|
+
const { executeVerifyQualityScan } = await import('./verify-quality-scan.js')
|
|
417
|
+
await executeVerifyQualityScan({ cwd, specBase, changeName, platformOpts })
|
|
418
|
+
} else if (_cliAction === 'archiveDistill') {
|
|
419
|
+
// 与 stage.js noAI 分支同语义(P0-4 安全变体:distill 提炼 CLI 机械执行,needsWait 裁决
|
|
420
|
+
// 收敛到「确认归档 --confirm」;全路径不抛)
|
|
421
|
+
const { executeArchiveDistill } = await import('./archive-distill.js')
|
|
422
|
+
await executeArchiveDistill({ cwd, specBase, changeName })
|
|
423
|
+
} else if (_cliAction === 'doctorRunDiagnostics') {
|
|
424
|
+
// 2026-09-09-doctor-noai:与 stage.js 分支同语义(诊断+渲染+落盘)
|
|
425
|
+
const { runDoctorDiagnostics, renderDoctorSummary, writeDoctorDiagnosis } = await import('../doctor-diagnostics.js')
|
|
426
|
+
const diag = await runDoctorDiagnostics({ cwd })
|
|
427
|
+
console.log(renderDoctorSummary(diag))
|
|
428
|
+
try { writeDoctorDiagnosis(diag, (platformOpts?.specRoot || join(cwd, '.sillyspec'))) } catch { /* 落盘 fail-soft */ }
|
|
383
429
|
} else {
|
|
384
430
|
throw new Error(`noAI 步骤 ${steps[currentIdx].name} 的未知 _cliAction: ${_cliAction}——请在 complete.js 注册对应分支`)
|
|
385
431
|
}
|
|
386
432
|
}
|
|
387
433
|
|
|
434
|
+
// ── 并发 --done 防护(坑 execute-concurrent-done-skips-next-wave,2026-09-08 实证)──
|
|
435
|
+
// 会话 B 先 --done 推进了 Wave N,会话 A 随后的 --done 会落到**新的当前步 = Wave N+1**,
|
|
436
|
+
// 用 Wave N 的摘要静默完成 Wave N+1(产物未落地却标完成)。CLI 短进程无法读心,两层防护:
|
|
437
|
+
// ① 醒目前置横幅(邻步 60s 内刚被完成 = 疑似并行会话刚推进)——操作者在看到「✅ Step N
|
|
438
|
+
// 完成:Wave X」与自己所做不符时可立即 --reopen,不静默;
|
|
439
|
+
// ② 写前重读校验(本命令读库→落盘窗口内的真实竞态):重读 DB,若本命令要完成的步骤
|
|
440
|
+
// 已被并行进程标 completed/skipped,拒绝覆盖式推进(防双写交错把状态机写花)。
|
|
441
|
+
if (stageName === 'execute' && currentIdx > 0) {
|
|
442
|
+
const prev = steps[currentIdx - 1]
|
|
443
|
+
const _prevDoneAgoSec = prev?.completedAt ? Math.round((Date.now() - new Date(prev.completedAt.replace(/(\d{4})\/(\d{2})\/(\d{2})/, '$1-$2-$3')).getTime()) / 1000) : null
|
|
444
|
+
if (_prevDoneAgoSec !== null && Number.isFinite(_prevDoneAgoSec) && _prevDoneAgoSec >= 0 && _prevDoneAgoSec <= 60) {
|
|
445
|
+
console.warn('')
|
|
446
|
+
console.warn(`⚠️ 并发防护:上一步「${prev.name}」${_prevDoneAgoSec}s 前刚被(可能另一会话的)--done 完成——本次将完成的是「${steps[currentIdx].name}」。`)
|
|
447
|
+
console.warn(` 若与你刚做完的 Wave 不符,立即停手:sillyspec run ${stageName} --reopen --from-step ${currentIdx + 1}${changeName ? ` --change ${changeName}` : ''} 退回本步,勿继续下一 Wave。`)
|
|
448
|
+
console.warn('')
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
let _dupCompletedByPeer = false
|
|
452
|
+
try {
|
|
453
|
+
const _fresh = pm.read(cwd, changeName)
|
|
454
|
+
const _freshSteps = _fresh?.stages?.[stageName]?.steps
|
|
455
|
+
if (Array.isArray(_freshSteps) && _freshSteps[currentIdx] && _freshSteps[currentIdx].name === steps[currentIdx].name
|
|
456
|
+
&& (_freshSteps[currentIdx].status === 'completed' || _freshSteps[currentIdx].status === 'skipped')) {
|
|
457
|
+
_dupCompletedByPeer = true
|
|
458
|
+
}
|
|
459
|
+
} catch { /* 重读失败(平台模式瞬态等)不阻断——横幅防护仍在 */ }
|
|
460
|
+
if (_dupCompletedByPeer) {
|
|
461
|
+
console.error(`❌ 步骤「${steps[currentIdx].name}」刚被并行会话完成(本命令读库后被推进),拒绝重复推进——防把下一步误标完成。`)
|
|
462
|
+
console.error(` 排查:sillyspec progress show${changeName ? ` --change ${changeName}` : ''} 确认当前步;你若也完成了同一 Wave,无需再次 --done;若要完成的是下一步,重跑 --done(此时它会落在正确的当前步)。`)
|
|
463
|
+
process.exit(1)
|
|
464
|
+
}
|
|
465
|
+
|
|
388
466
|
steps[currentIdx].status = 'completed'
|
|
389
467
|
steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
390
468
|
if (outputText) {
|
|
@@ -400,6 +478,13 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
400
478
|
mkdirSync(artifactBase, { recursive: true })
|
|
401
479
|
const ts = new Date().toISOString().slice(0,19).replace(/[-T:]/g, '')
|
|
402
480
|
writeFileSync(join(artifactBase, `${changeName || 'unknown'}-${stageName}-step${currentIdx + 1}-${ts}.txt`), outputText)
|
|
481
|
+
// 写入侧滚动裁剪(runtime-hygiene.js,2026-09-08 系统性回收):artifacts/ 只写不收,
|
|
482
|
+
// 实证本仓 534 份累积。文件名时间戳在尾部、前缀是变更名(长变更晚收尾会乱序)→ 按
|
|
483
|
+
// mtime 排序(write-once 本地审计文件,mtime 可靠)。fail-open 静默。
|
|
484
|
+
try {
|
|
485
|
+
const { pruneTimestampedEntries } = await import('../runtime-hygiene.js')
|
|
486
|
+
pruneTimestampedEntries({ dir: artifactBase, keep: 100, orderBy: 'mtime' })
|
|
487
|
+
} catch { /* 卫生动作 fail-open */ }
|
|
403
488
|
} else {
|
|
404
489
|
steps[currentIdx].output = outputText
|
|
405
490
|
}
|
|
@@ -577,6 +662,10 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
577
662
|
const total = steps.length
|
|
578
663
|
console.log(`✅ ${stageName} 阶段已完成(${total}/${total} 步)`)
|
|
579
664
|
|
|
665
|
+
// task-04(2026-09-10-change-scope-audit):execute 完成打范围对账全表+落 .runtime 快照 /
|
|
666
|
+
// verify 完成出一行漂移确认。advisory fail-soft(D-006)——helper 内部全兜,绝不阻断完成。
|
|
667
|
+
await printStageCompletionScopeAudit({ stageName, cwd, changeName, specBase, platformOpts })
|
|
668
|
+
|
|
580
669
|
if (stageName === 'execute') {
|
|
581
670
|
// execute run summary:展示真实可得的结构化信息
|
|
582
671
|
try {
|
|
@@ -621,6 +710,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
621
710
|
// 走下方 _getNextSuggestion 会因 scan 是 STAGE_ORDER 首位、永未完成而误推 scan(回头路),
|
|
622
711
|
// 与 brainstorm 同类问题(见上注释),故给 quick 专属分支。
|
|
623
712
|
console.log('\n👉 quick 已完成。下一步:提交本次改动(git commit / sillyspec-commit),或 sillyspec run <stage> 继续其他阶段。')
|
|
713
|
+
console.log(' ⚠️ 多会话共享仓禁目录级 git add / git add -A(会夹带并行会话文件,2026-09-10 实证误删已提交文档):用显式 pathspec(git add -- 文件…),提交前 git status 核对暂存面。')
|
|
624
714
|
} else {
|
|
625
715
|
// D1 暗衔接修:阶段刚完成,CLI 本就知道状态机下一步是哪个阶段(_getNextSuggestion),
|
|
626
716
|
// 却只丢一句「下一步由你决定」让 agent 自己猜命令。改为按 progress 实际状态给出精确命令。
|
|
@@ -684,6 +774,95 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
684
774
|
return { stageCompleted: false, currentIdx, nextPendingIdx }
|
|
685
775
|
}
|
|
686
776
|
|
|
777
|
+
// ── 变更范围对账 advisory 注入(2026-09-10-change-scope-audit task-04,FR-03)──
|
|
778
|
+
// execute 完成打全表+落快照、verify 完成出一行漂移确认;completeStep 与 continueStep(wait
|
|
779
|
+
// 解除完成)两路径收敛点同调 printStageCompletionScopeAudit(Grill G-2:单锚 completeStep 会漏
|
|
780
|
+
// wait 解除路径致 verify 假报无快照)。全程 fail-soft(D-006):任何异常只打单行 ⚠️,不阻断
|
|
781
|
+
// 阶段完成、不进任何门禁判定。scope-audit.js 经动态 import 接入(本文件 runtime-hygiene /
|
|
782
|
+
// design-facts 等同款先例)——只读纯函数模块,模块缓存下零重复加载。
|
|
783
|
+
// - execute:renderScopeAuditTable 全表(maxRows 60 截断 + 完整表跑 scope-audit 指引 +
|
|
784
|
+
// ⚠️ 出口指引行——计划外补 design.md 声明或 --output 注明原因,均 renderer 内置)+ 落
|
|
785
|
+
// .runtime/scope-audit-<change>.json 快照(结构即 ScopeAuditResult + savedAt 落盘时间戳,
|
|
786
|
+
// 只写 .runtime 运行时产物区不动 git 状态)。
|
|
787
|
+
// - verify:读快照对比文件集差集出一行「变更范围:N 文件 +X/-Y(vs execute 时点:一致|漂移
|
|
788
|
+
// M 文件:<列表截断>)」。漂移过滤面 = filterDeliverableFiles 结果(resolveReconcileActualFiles
|
|
789
|
+
// 已过、本就保留 docs 子树)再排除 .sillyspec/docs/**——verify 阶段合法文档同步不计漂移
|
|
790
|
+
// (Grill C-5/P2-②:组合过滤在本侧做,不动 worktree-apply.js 过滤器本体)。快照缺失/降级 →
|
|
791
|
+
// 跳过对比只打当前 totals 一行。
|
|
792
|
+
|
|
793
|
+
/** 漂移对比排除面:verify 阶段合法文档同步子树(posix 归一后前缀匹配) */
|
|
794
|
+
const SCOPE_DRIFT_EXCLUDE_RE = /^\.sillyspec\/docs\//
|
|
795
|
+
|
|
796
|
+
/** 快照/当前结果 → 漂移对比用文件集(posix 归一 + docs 子树排除);畸形结构防御式得空集 */
|
|
797
|
+
function scopeAuditPathSet(result) {
|
|
798
|
+
const rows = Array.isArray(result?.rows) ? result.rows : []
|
|
799
|
+
const set = new Set()
|
|
800
|
+
for (const r of rows) {
|
|
801
|
+
const p = r && typeof r.path === 'string' ? r.path.replace(/\\/g, '/') : ''
|
|
802
|
+
if (p && !SCOPE_DRIFT_EXCLUDE_RE.test(p)) set.add(p)
|
|
803
|
+
}
|
|
804
|
+
return set
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** execute 完成路径:全表 + 快照落盘(快照写失败只提示,verify 对比按无快照降级) */
|
|
808
|
+
async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts, computeChangeScopeAudit, renderScopeAuditTable }) {
|
|
809
|
+
const result = await computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts })
|
|
810
|
+
console.log(`\n${renderScopeAuditTable(result, { maxRows: 60 })}`)
|
|
811
|
+
try {
|
|
812
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
813
|
+
const snapPath = join(runtimeRoot, `scope-audit-${changeName}.json`)
|
|
814
|
+
mkdirSync(runtimeRoot, { recursive: true })
|
|
815
|
+
writeFileSync(snapPath, JSON.stringify({ ...result, savedAt: new Date().toISOString() }, null, 2) + '\n')
|
|
816
|
+
} catch (e) {
|
|
817
|
+
console.warn(` ⚠️ 范围对账快照写入失败(不阻断,verify 漂移对比将按无快照降级):${e && e.message ? String(e.message).split('\n')[0] : e}`)
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** verify 完成路径:读 execute 时点快照对比文件集差集,一行结论(行数不比——execute 形态 A 锚
|
|
822
|
+
* worktree meta、verify 形态 B 锚 merge-base,基点不同行数天然抖动,比行数必假阳) */
|
|
823
|
+
async function printVerifyScopeDrift({ cwd, changeName, specBase, platformOpts, computeChangeScopeAudit }) {
|
|
824
|
+
const result = await computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts })
|
|
825
|
+
const totals = result?.totals && typeof result.totals === 'object' ? result.totals : {}
|
|
826
|
+
const scopeLine = `变更范围:${totals.files ?? (Array.isArray(result?.rows) ? result.rows.length : 0)} 文件 +${totals.additions ?? 0}/-${totals.deletions ?? 0}`
|
|
827
|
+
if (result?.ok !== true) {
|
|
828
|
+
console.log(` ⚠️ ${scopeLine}(对账降级:${result?.degradedReason || '实际侧文件集解析失败'}——advisory,不影响 verify 完成)`)
|
|
829
|
+
return
|
|
830
|
+
}
|
|
831
|
+
let snapshot = null
|
|
832
|
+
try {
|
|
833
|
+
snapshot = JSON.parse(readFileSync(join(resolveRuntimeRoot(platformOpts, specBase), `scope-audit-${changeName}.json`), 'utf8'))
|
|
834
|
+
} catch { /* 快照缺失/损坏 → 跳过对比只打当前 totals */ }
|
|
835
|
+
if (!snapshot || snapshot.ok !== true) {
|
|
836
|
+
console.log(` ${scopeLine}(无 execute 时点快照可对比——完整表跑 sillyspec scope-audit --change ${changeName})`)
|
|
837
|
+
return
|
|
838
|
+
}
|
|
839
|
+
const current = scopeAuditPathSet(result)
|
|
840
|
+
const baseline = scopeAuditPathSet(snapshot)
|
|
841
|
+
const drift = [...new Set([...current, ...baseline])].filter(p => current.has(p) !== baseline.has(p)).sort()
|
|
842
|
+
if (drift.length === 0) {
|
|
843
|
+
console.log(` ${scopeLine}(vs execute 时点:一致)`)
|
|
844
|
+
} else {
|
|
845
|
+
const shown = drift.slice(0, 5).join(', ')
|
|
846
|
+
const tail = drift.length > 5 ? ` 等 ${drift.length} 个` : ''
|
|
847
|
+
console.log(` ${scopeLine}(vs execute 时点:漂移 ${drift.length} 文件:${shown}${tail}——advisory 不阻断,请确认 verify 期改动并按需补 design.md 声明)`)
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** 两完成路径公共入口(stage 派发 + 动态 import + fail-soft 兜底,D-006) */
|
|
852
|
+
async function printStageCompletionScopeAudit({ stageName, cwd, changeName, specBase, platformOpts }) {
|
|
853
|
+
try {
|
|
854
|
+
if (!changeName || (stageName !== 'execute' && stageName !== 'verify')) return
|
|
855
|
+
const { computeChangeScopeAudit, renderScopeAuditTable } = await import('../scope-audit.js')
|
|
856
|
+
if (stageName === 'execute') {
|
|
857
|
+
await printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts, computeChangeScopeAudit, renderScopeAuditTable })
|
|
858
|
+
} else {
|
|
859
|
+
await printVerifyScopeDrift({ cwd, changeName, specBase, platformOpts, computeChangeScopeAudit })
|
|
860
|
+
}
|
|
861
|
+
} catch (e) {
|
|
862
|
+
console.warn(`⚠️ 变更范围对账输出异常(advisory,不阻断 ${stageName} 阶段完成):${e && e.message ? String(e.message).split('\n')[0] : e}`)
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
687
866
|
// ── Step 调度兄弟函数(W6 Step7b-2 从 run.js 搬入:skip/wait/continue + formatWaitHistory)──
|
|
688
867
|
/**
|
|
689
868
|
* plan.md checkbox 自动勾选:execute 完成 + 各 task review.json 双 verdict 非 fail → 自动勾选。
|
|
@@ -813,7 +992,7 @@ export function shouldAutoCheckTask(r, endToEnd, ctx = null) {
|
|
|
813
992
|
|
|
814
993
|
return true
|
|
815
994
|
}
|
|
816
|
-
async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts }) {
|
|
995
|
+
export async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts }) {
|
|
817
996
|
if (stageName !== 'execute' || !changeName) {
|
|
818
997
|
return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
|
|
819
998
|
}
|
|
@@ -1292,6 +1471,9 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
1292
1471
|
// gate 全过:persist completed(task-03 移后;此处无 triggerSync)。
|
|
1293
1472
|
pm._write(cwd, progress, changeName)
|
|
1294
1473
|
console.log(`\n✅ ${stageName} 阶段已完成(${stageData.steps.length}/${stageData.steps.length} 步)`)
|
|
1474
|
+
// task-04:wait 解除完成路径与 completeStep 完成分支同源注入(Grill G-2:本路径不漏,
|
|
1475
|
+
// 否则 execute 快照缺失 → verify 假报「无快照可对比」)。
|
|
1476
|
+
await printStageCompletionScopeAudit({ stageName, cwd, changeName, specBase, platformOpts })
|
|
1295
1477
|
// 阶段完成后明确下一步(agent 常卡:stageData completed 但不知要 run <下一阶段> 推进 currentStage)
|
|
1296
1478
|
const nextStageHint = { brainstorm: 'plan', plan: 'execute', execute: 'verify', verify: 'archive' }[stageName]
|
|
1297
1479
|
if (nextStageHint) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gate-snapshot.js — quick --done test+lint 门禁的隔离快照执行(2026-09-10 驾驭小结第六批②,
|
|
3
|
+
* 用户实证「lint 实测对账在主仓跑,被并行会话的脏文件拦门」)。
|
|
4
|
+
*
|
|
5
|
+
* 问题:门禁在主仓工作区跑 commands.test/lint——多会话共享仓里并行会话的未提交 src/test
|
|
6
|
+
* 改动(语法错误/半成品/锚漂)会污染实测结果,把无辜会话的 --done 拦在门上(只能走
|
|
7
|
+
* advisory 逃生口)。
|
|
8
|
+
*
|
|
9
|
+
* 修法:HEAD 干净快照 + 会话文件 overlay——`git worktree add --detach <tmp> HEAD` 取干净
|
|
10
|
+
* 基线(并行脏文件天然不在),把本会话审计/声明的变更文件覆盖进快照,node_modules 经
|
|
11
|
+
* junction 复用主仓依赖(零拷贝),local.yaml 从主仓复制(gitignore 不进 HEAD),在快照
|
|
12
|
+
* cwd 跑对账。快照基建任何失败 → 返回 null,调用方回退主仓现行为(零回归兜底)。
|
|
13
|
+
*
|
|
14
|
+
* 快照生命周期:一次门禁一批(create → run → cleanup);崩溃残留由 git worktree prune /
|
|
15
|
+
* 临时目录自然回收(OS tmp 清理),不进主仓 .runtime。
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from 'node:child_process'
|
|
18
|
+
import { mkdtempSync, mkdirSync, copyFileSync, writeFileSync, rmSync, existsSync, readdirSync, statSync, symlinkSync, readFileSync } from 'node:fs'
|
|
19
|
+
import { join, dirname, relative, resolve } from 'node:path'
|
|
20
|
+
import { tmpdir } from 'node:os'
|
|
21
|
+
|
|
22
|
+
function git(cwd, args) {
|
|
23
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60000 }).trim()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 创建隔离快照:HEAD worktree + 会话文件 overlay + node_modules junction + local.yaml。
|
|
28
|
+
* @param {{ cwd: string, files: string[] }} opts cwd=主仓根;files=本会话变更文件(仓库根相对 POSIX)
|
|
29
|
+
* @returns {{ snapshotRoot: string, cleanup: () => void, reason?: string }|null} 失败返回 null(调用方回退主仓)
|
|
30
|
+
*/
|
|
31
|
+
export function createGateSnapshot({ cwd, files }) {
|
|
32
|
+
let snapshotRoot = null
|
|
33
|
+
try {
|
|
34
|
+
// 前置:主仓须是 git 仓且有 HEAD(无 git 环境回退主仓现行为)
|
|
35
|
+
git(cwd, ['rev-parse', 'HEAD'])
|
|
36
|
+
|
|
37
|
+
snapshotRoot = mkdtempSync(join(tmpdir(), 'sillyspec-gate-'))
|
|
38
|
+
git(cwd, ['worktree', 'add', '--detach', '--quiet', snapshotRoot, 'HEAD'])
|
|
39
|
+
|
|
40
|
+
// 会话文件 overlay:主仓工作区版本覆盖进快照(本会话的最新态;文件不存在=已删,跳过)。
|
|
41
|
+
// .sillyspec/ 跳过面收窄(P2-d 落地实证的快照盲区):原一刀切跳过整个 .sillyspec/ ——
|
|
42
|
+
// 会话声明的 tracked docs(module-map 补录/知识库/模块卡)进不了快照,门禁读到 HEAD
|
|
43
|
+
// 旧版恒误报(src 未录 module-map 的修复在主仓生效、快照里仍然失败)。真正需要隔离的
|
|
44
|
+
// 是共享运行时面:.runtime/(sqlite 锁/在途 marker)与 quicklog/(跨会话共享账本);
|
|
45
|
+
// local.yaml 由下方 cfg 段显式复制(单一来源,不经 overlay)。
|
|
46
|
+
let overlaid = 0
|
|
47
|
+
for (const f of files) {
|
|
48
|
+
const isSillyspecRuntime = typeof f === 'string' && (f.startsWith('.sillyspec/.runtime/') || f.startsWith('.sillyspec/quicklog/') || f === '.sillyspec/local.yaml' || f === '.sillyspec/.sillyspec-platform.json')
|
|
49
|
+
if (typeof f !== 'string' || f.includes('..') || f.startsWith('/') || isSillyspecRuntime) continue
|
|
50
|
+
const src = join(cwd, f)
|
|
51
|
+
if (!existsSync(src)) continue
|
|
52
|
+
const dst = join(snapshotRoot, f)
|
|
53
|
+
mkdirSync(dirname(dst), { recursive: true })
|
|
54
|
+
copyFileSync(src, dst)
|
|
55
|
+
overlaid++
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// node_modules junction(依赖主仓安装态;junction Windows 无需特权,POSIX 用 symlink)
|
|
59
|
+
const nmSrc = join(cwd, 'node_modules')
|
|
60
|
+
if (existsSync(nmSrc)) {
|
|
61
|
+
try { symlinkSync(nmSrc, join(snapshotRoot, 'node_modules'), 'junction') }
|
|
62
|
+
catch { /* 依赖链接失败:test/lint 可能跑不起来 → 仍返回快照,命令层报错走 fallback */ }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// local.yaml(gitignore 不进 HEAD;门禁命令配置来源)+ package-lock 保持 HEAD 版(npm test 不装新依赖)
|
|
66
|
+
for (const cfg of [join('.sillyspec', 'local.yaml')]) {
|
|
67
|
+
const src = join(cwd, cfg)
|
|
68
|
+
if (existsSync(src)) {
|
|
69
|
+
mkdirSync(dirname(join(snapshotRoot, cfg)), { recursive: true })
|
|
70
|
+
copyFileSync(src, join(snapshotRoot, cfg))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const cleanup = () => {
|
|
75
|
+
try { git(cwd, ['worktree', 'remove', '--force', '--quiet', snapshotRoot]) } catch {
|
|
76
|
+
try { rmSync(snapshotRoot, { recursive: true, force: true }) } catch { /* 残留交 OS tmp 清理 */ }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { snapshotRoot, cleanup, overlaid }
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// 基建失败(worktree add 拒绝/磁盘满/…):尽力清理后回退主仓
|
|
82
|
+
if (snapshotRoot) {
|
|
83
|
+
try { git(cwd, ['worktree', 'remove', '--force', '--quiet', snapshotRoot]) } catch {}
|
|
84
|
+
try { rmSync(snapshotRoot, { recursive: true, force: true }) } catch {}
|
|
85
|
+
}
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
}
|