sillyspec 3.29.4 → 3.30.0
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 +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +18 -116
- package/AGENTS.md +38 -20
- package/SKILL.md +9 -3
- package/assets/command-cards/flow.md +32 -0
- package/assets/command-cards/run-archive.md +26 -0
- package/assets/command-cards/run-brainstorm.md +33 -0
- package/assets/command-cards/run-execute.md +29 -0
- package/assets/command-cards/run-plan.md +27 -0
- package/assets/command-cards/run-quick.md +17 -0
- package/assets/command-cards/run-verify.md +29 -0
- package/assets/command-cards/status.md +27 -0
- package/package.json +2 -1
- package/packages/dashboard/server/executor.js +2 -1
- package/packages/dashboard/server/parser.js +7 -7
- package/r7-test-env/README-R7-TEST.md +93 -0
- package/r7-test-env/demo-project/lib/calc.js +6 -0
- package/r7-test-env/demo-project/package.json +1 -0
- package/r7-test-env/demo-project/test.js +10 -0
- package/r7-test-env/prompt-R7-SMOKE.md +41 -0
- package/r7-test-env/sillyspec-r7.cmd +3 -0
- package/round4/account.mjs +98 -0
- package/round4/b1-analyze-task08.mjs +40 -0
- package/round4/b1-b1-target-validation.md +32 -0
- package/round4/b3a-prompt-audit-result.md +66 -0
- package/round4/b3a-prompt-audit.mjs +89 -0
- package/round4/brief-autocompact.md +34 -0
- package/round4/brief-session-replay.md +50 -0
- package/round4/deep-read-synthesis.md +38 -0
- package/round4/extract-full.mjs +38 -0
- package/round4/extract-timeline.mjs +43 -0
- package/round4/find-sessions.mjs +13 -0
- package/round4/full-r4-l.md +4707 -0
- package/round4/full-r4-o-l.md +4300 -0
- package/round4/full-r4-os.md +1681 -0
- package/round4/full-r4-sf.md +4255 -0
- package/round4/full-r4-sq.md +1734 -0
- package/round4/gate-audit-relay-receipt.md +30 -0
- package/round4/gate-value-audit.md +52 -0
- package/round4/optimization-plan.md +165 -0
- package/round4/prereg.md +67 -0
- package/round4/prompt-R4-L.md +63 -0
- package/round4/prompt-R4-O-L.md +59 -0
- package/round4/prompt-R4-O-S.md +56 -0
- package/round4/prompt-R4-S-F.md +60 -0
- package/round4/prompt-R4-S-Q.md +66 -0
- package/round4/r4-final-report.html +347 -0
- package/round4/r4-os-timeline.txt +297 -0
- package/round4/r4-sf-timeline.txt +738 -0
- package/round4/r4-sq-timeline.txt +322 -0
- package/round4/r4l-timeline.txt +793 -0
- package/round4/r4ol-timeline.txt +729 -0
- package/round4/r4sf-timeline.txt +852 -0
- package/round4/r5-scenario-matrix.md +46 -0
- package/round4/result-S.md +171 -0
- package/round4/result-final.md +46 -0
- package/round4/result-quality.md +63 -0
- package/src/change-list.js +9 -0
- package/src/classify-change.js +6 -0
- package/src/command-cards.js +74 -0
- package/src/commit-guard.js +2 -0
- package/src/config-schema.js +64 -12
- package/src/contract-matrix.js +26 -5
- package/src/db.js +35 -4
- package/src/decision-distill.js +9 -3
- package/src/diagnostic-codes.js +2 -1
- package/src/docs-check.js +1 -1
- package/src/doctor-diagnostics.js +44 -2
- package/src/flow-draft.js +748 -0
- package/src/flow-parity.js +236 -0
- package/src/flow-review.js +184 -0
- package/src/flow.js +1492 -0
- package/src/foreign-declared.js +17 -1
- package/src/fr-index.js +396 -22
- package/src/fs-atomic.js +5 -1
- package/src/git-helper.js +2 -2
- package/src/greenfield-bootstrap.js +98 -0
- package/src/handoff.js +120 -7
- package/src/hunk-attribution.js +191 -0
- package/src/index.js +602 -21
- package/src/init.js +25 -8
- package/src/knowledge-classify.js +47 -1
- package/src/knowledge-digest.js +162 -0
- package/src/knowledge-quicklog.js +246 -0
- package/src/machine-draft.js +128 -0
- package/src/machine-interface.js +194 -7
- package/src/mcp-server.js +1 -0
- package/src/module-impact.js +51 -22
- package/src/module-resolve.js +3 -1
- package/src/preview-progress.js +160 -0
- package/src/progress/change-registry.js +11 -3
- package/src/progress/shared.js +3 -2
- package/src/progress.js +56 -13
- package/src/quicklog.js +44 -6
- package/src/redomain.js +147 -0
- package/src/review-material-pack.js +204 -1
- package/src/run/archive-distill.js +4 -2
- package/src/run/bg-sync.js +67 -0
- package/src/run/command.js +127 -10
- package/src/run/complete-handlers.js +363 -73
- package/src/run/complete.js +356 -74
- package/src/run/gate-snapshot-ledger.js +213 -0
- package/src/run/gate-snapshot.js +260 -38
- package/src/run/gates.js +482 -268
- package/src/run/green-cache.js +123 -0
- package/src/run/junction-rm.js +70 -0
- package/src/run/next.js +14 -0
- package/src/run/prompt.js +185 -55
- package/src/run/quick-audit.js +213 -14
- package/src/run/shared.js +123 -4
- package/src/run/stage.js +182 -216
- package/src/run/test-ledger.js +264 -0
- package/src/run/verify-quality-scan.js +84 -7
- package/src/scope-audit.js +6 -1
- package/src/sentinel-assertions.js +114 -0
- package/src/setup.js +3 -3
- package/src/spec-sync.js +41 -1
- package/src/stage-contract.js +26 -8
- package/src/stage-review-checklist.js +2 -0
- package/src/stage-review.js +7 -2
- package/src/stage-templates.js +157 -0
- package/src/stages/archive.js +2 -0
- package/src/stages/brainstorm.js +24 -106
- package/src/stages/execute.js +222 -85
- package/src/stages/knowledge.js +74 -1
- package/src/stages/plan-postcheck.js +385 -20
- package/src/stages/plan.js +23 -1
- package/src/stages/quick.js +10 -2
- package/src/stages/verify.js +4 -2
- package/src/sync.js +81 -23
- package/src/task-done.js +181 -0
- package/src/task-review.js +8 -55
- package/src/taskcard-frontmatter.js +57 -0
- package/src/test-bindings.js +549 -0
- package/src/timeline.js +218 -0
- package/src/ui-visual.js +175 -0
- package/src/verify-draft.js +214 -0
- package/src/verify-postcheck.js +919 -159
- package/src/verify-probes.js +103 -25
- package/src/watcher.js +1031 -0
- package/src/worktree-apply.js +44 -4
- package/src/worktree-deps.js +22 -8
- package/src/worktree-policy.js +42 -0
- package/src/worktree.js +102 -15
- package/templates/agents-instruction.md +37 -18
package/src/run/complete.js
CHANGED
|
@@ -45,12 +45,13 @@ import { executeProgressConfirm } from './progress-confirm.js'
|
|
|
45
45
|
import { AUXILIARY_STAGES } from '../constants.js'
|
|
46
46
|
import { executePlanPostcheck as runPlanPostcheckLib, parseRepoRegistry } from '../stages/plan-postcheck.js'
|
|
47
47
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
48
|
-
import { enforceDepsGate,
|
|
48
|
+
import { enforceDepsGate, enforceSymbolImpactGate, warnMissingUiPrototype, completeStageGates, readDesignScale } from './gates.js'
|
|
49
49
|
import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact, assertWaveTasksComplete } from './complete-handlers.js'
|
|
50
50
|
import { formatExecuteSummary } from '../worktree-apply.js'
|
|
51
51
|
import { validateDecisionModuleRefs } from '../design-facts.js'
|
|
52
52
|
import { isEndToEndTaskText } from '../change-risk-profile.js'
|
|
53
53
|
import { deriveTitleFromLinkedChange } from '../quicklog.js'
|
|
54
|
+
import { stageRegistry } from '../stages/index.js' // hasPendingVerifyScanStep 判定面(cli-protocol-trust;缺 import 时 ReferenceError 被 fail-open catch 吞成恒 false——本行即其修复)
|
|
54
55
|
|
|
55
56
|
// validateMetadata / readDesignScale / validateFileLocations 已迁至 ./gates.js(completeStageGates
|
|
56
57
|
// 共享收尾管线,消除 noAI 末步 / continueStep 完成分支绕过 gate 的 S1/S2/S3 不对称)。
|
|
@@ -61,6 +62,34 @@ import { deriveTitleFromLinkedChange } from '../quicklog.js'
|
|
|
61
62
|
// step6 design.md 已落盘但阶段未完)与阶段完成同样刷新,治「brainstorm 全程 title 存英文 autoName
|
|
62
63
|
// 兜底」。quick-<hex> 无 proposal/design 目录,deriveTitleFromLinkedChange 返回 '' 不刷新
|
|
63
64
|
// (quick 走 handleQuickStageCompletion 的 extractTitleFromResult)。失败静默(不阻断流程)。
|
|
65
|
+
// ── Wave 边界 handoff 纪律(2026-09-21 R5 接线 ql-20260921-007)──
|
|
66
|
+
// rollout 实证(sess_418fc2bb,2026-09-21):单会话跨 4 Wave 背 ~130K 上下文 ×176 请求,
|
|
67
|
+
// ≈1/4 墙钟是肥上下文结构税(缓存 99% 救账单不救墙钟)。B4a 交接块已在场,缺的只是纪律——
|
|
68
|
+
// 本 helper 维护「变更 × 会话 → 已完成 Wave 步计数」账本,同会话跨 ≥2 Wave 时升级提示语气。
|
|
69
|
+
// 纯函数(IO 由调用方做):返回更新后的账本 + 是否达 advisory 阈值。
|
|
70
|
+
export function updateWaveSessionLedger(ledger, sessionId, waveName) {
|
|
71
|
+
const sid = String(sessionId || 'anon')
|
|
72
|
+
const prev = ledger && ledger.sessionId === sid ? (ledger.waveCount || 0) : 0
|
|
73
|
+
const next = { sessionId: sid, waveCount: prev + 1, lastWave: waveName || null, at: new Date().toISOString() }
|
|
74
|
+
return { ledger: next, crossWaveAdvisory: next.waveCount >= 2 }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 阶段-会话账本(token 减负④,2026-09-23 R9 实证后):同一 SILLYSPEC_SESSION_ID 连续完成
|
|
79
|
+
* ≥2 个流程阶段 → 升级告警(同 Wave 账本的阶段粒度版)。R9 数据:单会话五阶段 53.5M 输入、
|
|
80
|
+
* 尾段单轮 30 万+、archive 3 分钟 7.2M——肥上下文税随会话内阶段数单调累积。架构约束
|
|
81
|
+
* (conventions 钉):CLI 只产信号与接力载荷,不做会话编排——新会话创建者是用户或平台
|
|
82
|
+
* (session-fork 基建),升级块动作主体必须写明。纯函数(IO 由调用方做)。
|
|
83
|
+
*/
|
|
84
|
+
export function updateStageSessionLedger(ledger, sessionId, stageName) {
|
|
85
|
+
const sid = String(sessionId || 'anon')
|
|
86
|
+
const prev = ledger && ledger.sessionId === sid ? (ledger.stageCount || 0) : 0
|
|
87
|
+
const stages = ledger && ledger.sessionId === sid && Array.isArray(ledger.stages) ? [...ledger.stages] : []
|
|
88
|
+
stages.push(String(stageName || ''))
|
|
89
|
+
const next = { sessionId: sid, stageCount: prev + 1, stages: stages.slice(-8), lastStage: stageName || null, at: new Date().toISOString() }
|
|
90
|
+
return { ledger: next, crossStageAdvisory: next.stageCount >= 2 }
|
|
91
|
+
}
|
|
92
|
+
|
|
64
93
|
function refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName) {
|
|
65
94
|
if (!changeName || /^quick-[0-9a-f]{8}$/.test(changeName)) return
|
|
66
95
|
try {
|
|
@@ -105,22 +134,37 @@ export function resolveWaitingStepWithAnswer(steps, doneAnswer, nowStr) {
|
|
|
105
134
|
* 纯事实零编造(无「通过/成功」等判断词——判断在 gate,不在摘要);语义性说明(方案
|
|
106
135
|
* 取舍/用户反馈)不合成,摘要自带手写指引。长度控制在 MAX_OUTPUT(200) 截断线内。
|
|
107
136
|
*/
|
|
108
|
-
export function synthesizeStepOutput({ stageName, stepName, cwd }) {
|
|
137
|
+
export function synthesizeStepOutput({ stageName, stepName, cwd, platformOpts = {} }) {
|
|
138
|
+
// 窗口归属性判定(坑 stage-burst-timeline-quality-collapse ②,2026-09-24 实证):
|
|
139
|
+
// git 窗口只在能归属到本变更文件根时给出——worktree 内执行(specDriftAnchor 锚定)
|
|
140
|
+
// 时 git 向上发现 worktree 根,窗口本就归属正确;平台模式(specRoot 锚定 daemon spec
|
|
141
|
+
// 根,变更文件不在 cwd 仓——常为主仓根)取不到可归属根,窗口段整体缺省:错误窗口
|
|
142
|
+
// (列出主仓无关文件)比没有更误导。纯本地模式 cwd 即变更仓,维持原行为。
|
|
143
|
+
const windowAttributable = platformOpts.specDriftAnchor ? true : !platformOpts.specRoot
|
|
109
144
|
let windowNote = ''
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
145
|
+
if (windowAttributable) {
|
|
146
|
+
try {
|
|
147
|
+
const porcelain = gitQuiet(cwd, ['status', '--porcelain'])
|
|
148
|
+
if (porcelain !== null) {
|
|
149
|
+
const paths = porcelain.split('\n').filter(Boolean).map((l) => (l.slice(3) || '').trim()).filter(Boolean)
|
|
150
|
+
if (paths.length === 0) {
|
|
151
|
+
windowNote = ';工作区无未提交变更'
|
|
152
|
+
} else {
|
|
153
|
+
// basename 在 pop 之后再过滤(坑 stage-burst ②):未跟踪目录条目 `?? dir/` 尾段
|
|
154
|
+
// 为空串,pop 得 '',join 出「(、)」空名——先取后滤,全空时只报计数不报名单。
|
|
155
|
+
const names = paths.slice(0, 3).map((p) => p.split(/[\\/]/).pop()).filter(Boolean)
|
|
156
|
+
windowNote = names.length === 0
|
|
157
|
+
? `;变更窗口 ${paths.length} 文件`
|
|
158
|
+
: `;变更窗口 ${paths.length} 文件(${names.join('、')}${paths.length > 3 ? ' 等' : ''})`
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch { /* git 不可读 → 窗口段缺省(摘要仍成立) */ }
|
|
162
|
+
}
|
|
119
163
|
return `【CLI 合成】步骤「${stepName}」完成${windowNote};门禁以本次 CLI 校验输出为准(方案取舍等语义说明需 --output 手写)`
|
|
120
164
|
}
|
|
121
165
|
|
|
122
166
|
export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
123
|
-
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false, isNoDocs = false, isSkipApply = false, sessionFlag = null, quickFiles = [] } = options
|
|
167
|
+
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false, isNoDocs = false, isSkipApply = false, sessionFlag = null, quickFiles = [], linkedChanges = [], linkedChangesAuto = [] } = options
|
|
124
168
|
// specRoot(平台) > specDriftAnchor(worktree 漂移锚定主仓) > cwd/.sillyspec(本地)——
|
|
125
169
|
// 与 prompt.js resolvePromptSpecBase 同序:--done 收尾的 user-inputs.md/超长 artifact 落盘
|
|
126
170
|
// 必须与 prompt 渲染同一根,否则写进 worktree 副本目录、随 cleanup 整目录删除。
|
|
@@ -144,8 +188,8 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
144
188
|
}
|
|
145
189
|
|
|
146
190
|
const steps = stageData.steps
|
|
147
|
-
// blocked(deps
|
|
148
|
-
// 2026-08-27 实证):门控阻断时 enforceDepsGate
|
|
191
|
+
// blocked(deps 门控阻断标记)也视为当前步骤(坑 deps-gate-blocked-invisible,
|
|
192
|
+
// 2026-08-27 实证):门控阻断时 enforceDepsGate 把当前步置 blocked 后
|
|
149
193
|
// exit(1),若谓词漏 blocked,被阻断步骤对 findIndex 永久隐身——重试 --done 落到其后第一个
|
|
150
194
|
// pending 步骤上(错步记账,DB 显示与事实脱节),且 blocked 步骤无任何恢复路径(状态机卡死,
|
|
151
195
|
// 只能 --reset 丢进度)。门控本身在下方会重新校验:deps 已修复 → 正常完成;未修复 → 再次
|
|
@@ -210,7 +254,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
210
254
|
// --output 手写——合成摘要自带该指引。quick 阶段除外(末步四字段硬契约
|
|
211
255
|
// validateQuickResult,合成文本过不了四字段校验;quick 的省略提示由其阶段文案承担)。
|
|
212
256
|
if (!outputText && stageName !== 'quick' && currentIdx !== -1 && steps[currentIdx]) {
|
|
213
|
-
outputText = synthesizeStepOutput({ stageName, stepName: steps[currentIdx].name, cwd })
|
|
257
|
+
outputText = synthesizeStepOutput({ stageName, stepName: steps[currentIdx].name, cwd, platformOpts })
|
|
214
258
|
console.log(`🤖 --output 未提供——CLI 已按事实合成步骤摘要(gate 校验照常;方案取舍等语义说明下次带 --output 手写)。`)
|
|
215
259
|
}
|
|
216
260
|
// ── waiting 前置守卫(坑 archive-step3-wait-answer-hint-late)──
|
|
@@ -345,9 +389,8 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
345
389
|
// progress show/doctor 看到的仍是 pending,误导诊断)
|
|
346
390
|
const persistBlocked = () => pm._write(cwd, progress, changeName)
|
|
347
391
|
await enforceDepsGate(stageName, cwd, changeName, steps[currentIdx], steps, currentIdx, specBase, platformOpts, persistBlocked)
|
|
348
|
-
// review.json
|
|
349
|
-
//
|
|
350
|
-
await enforceReviewJsonGate(stageName, cwd, changeName, steps[currentIdx], steps, currentIdx, specBase, platformOpts, persistBlocked)
|
|
392
|
+
// review.json 字段硬门(enforceReviewJsonGate)已随 Task Review 层退役删除
|
|
393
|
+
// (2026-09-26-task-review-retire)——勾选回归 agent 手动(完成=实现+测试绿+wt-commit 即勾)。
|
|
351
394
|
// 符号影响面报告硬门(ql-20260816-005-3d7f):execute「加载上下文」步产出落盘核验——
|
|
352
395
|
// symbol-impact.md 存在 + plan 每 task 有结论行;防前缀步被一句「上下文在会话内」盖章跳过。
|
|
353
396
|
await enforceSymbolImpactGate(stageName, changeName, steps[currentIdx]?.name, specBase)
|
|
@@ -502,7 +545,12 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
502
545
|
}
|
|
503
546
|
|
|
504
547
|
steps[currentIdx].status = 'completed'
|
|
505
|
-
|
|
548
|
+
// burst 单调时钟(坑 stage-burst-timeline-quality-collapse ①,2026-09-24 实证):
|
|
549
|
+
// completeStepBurst 毫秒级循环收口 + 秒级时间串会把剩余步骤全部戳成同一秒——平台
|
|
550
|
+
// 步骤时间线九步同秒不可读。burst 每轮传 completedAtOverride(轮间严格递增);
|
|
551
|
+
// 单步路径不传,落真实时刻不变。
|
|
552
|
+
const _completedStamp = options.completedAtOverride instanceof Date ? options.completedAtOverride : new Date()
|
|
553
|
+
steps[currentIdx].completedAt = _completedStamp.toLocaleString('zh-CN',{hour12:false})
|
|
506
554
|
if (outputText) {
|
|
507
555
|
const MAX_OUTPUT = 200
|
|
508
556
|
if (outputText.length > MAX_OUTPUT) {
|
|
@@ -546,45 +594,47 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
546
594
|
|
|
547
595
|
// execute 批量完成检测:plan 全勾 + 代码客观核验通过 → 剩余 step 一次性标 completed,
|
|
548
596
|
// 使本次 --done 直接进入阶段完成分支(治"3 Wave 做完仍逐次 +1、需重走 7 次 --done")。
|
|
549
|
-
//
|
|
597
|
+
// per-task 自动勾选与 review 草稿兜底已随 Task Review 层退役(2026-09-26-task-review-retire):
|
|
598
|
+
// 勾选回归 agent 手动(完成=实现+测试绿+wt-commit 即勾,同 thin 工作单元语义),假勾防线由
|
|
599
|
+
// detectExecuteBatchFinish 内 checkExecuteCodeEvidence 代码证据核验 + verify 测试对账承担。
|
|
550
600
|
if (stageName === 'execute' && changeName) {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
if (_ac.skippedCount > 0) {
|
|
556
|
-
console.warn(` ⚠️ ${_ac.skippedCount} 个 task 未勾(review.json 缺失/fail)→ 批量完成条件不满足,仍按单步推进`)
|
|
557
|
-
}
|
|
558
|
-
const _bf = await detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, platformOpts, steps })
|
|
559
|
-
if (_bf.batched && _bf.aligned > 0) {
|
|
560
|
-
console.log(`\n🚀 execute 批量完成:plan 全勾 + 代码核验通过,一次性补完 ${_bf.aligned} 个剩余 step → 进入阶段完成分支`)
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// per-task review 草稿兜底(坑 worktree-execute-apply-friction 坑2):主 agent 直接实现模式
|
|
564
|
-
// 无子代理 review 落盘 → Task Review Gate 报缺 review.json 阻断。每次 execute --done 跑(幂等,
|
|
565
|
-
// 已存在跳过),据 git diff base..head 按 allowed_paths 归属生成 cannot_verify 草稿,进 gate 前就位。
|
|
601
|
+
// 自动勾选(full-autopilot-parity,R21 thin 实证迁移):execute --done 时解析 run 内提交的
|
|
602
|
+
// task-NN token,自动勾选 tasks.md 未勾条目——agent 手动勾选是 R19/R20 的行为惯性(tick-loop-nudge
|
|
603
|
+
// 提示词无效),机器代勾与 thin 的 flow done 同逻辑。fail-soft。
|
|
566
604
|
try {
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
605
|
+
const _tasksMdPath = join(specBase, 'changes', changeName, 'tasks.md')
|
|
606
|
+
if (existsSync(_tasksMdPath)) {
|
|
607
|
+
const _runIdFile = join(resolveRuntimeRoot(platformOpts, specBase), `current-execute-run-id-${changeName}`)
|
|
608
|
+
let _tickCommits = ''
|
|
609
|
+
try {
|
|
610
|
+
if (existsSync(_runIdFile)) {
|
|
611
|
+
_tickCommits = gitQuiet(cwd, ['log', '--format=%B%x1e', 'HEAD~20..HEAD']) || ''
|
|
612
|
+
}
|
|
613
|
+
} catch {}
|
|
614
|
+
if (!_tickCommits) _tickCommits = gitQuiet(cwd, ['log', '--format=%B%x1e', '-20']) || ''
|
|
615
|
+
if (_tickCommits) {
|
|
616
|
+
const _texts = String(_tickCommits).split('\x1e').map(x => x.trim()).filter(Boolean).join('\n')
|
|
617
|
+
const _evidenced = new Set()
|
|
618
|
+
for (const m of _texts.matchAll(/task-(\d{1,2})/gi)) _evidenced.add(String(Number(m[1])).padStart(2, '0'))
|
|
619
|
+
if (_evidenced.size > 0) {
|
|
620
|
+
let _md = readFileSync(_tasksMdPath, 'utf8')
|
|
621
|
+
let _ticked = 0
|
|
622
|
+
for (const nn of _evidenced) {
|
|
623
|
+
const re = new RegExp(`^- \\[ \\](.*task-${nn}[:\\s])`, 'm')
|
|
624
|
+
if (re.test(_md)) { _md = _md.replace(re, `- [x]$1`); _ticked++ }
|
|
625
|
+
}
|
|
626
|
+
if (_ticked > 0) {
|
|
627
|
+
const { writeAtomicSync } = await import('../fs-atomic.js')
|
|
628
|
+
writeAtomicSync(_tasksMdPath, _md)
|
|
629
|
+
console.log(`✅ [自动勾选] ${_ticked} 个任务有提交证据但未勾——已机器代勾(full-autopilot-parity)`)
|
|
630
|
+
}
|
|
631
|
+
}
|
|
584
632
|
}
|
|
585
633
|
}
|
|
586
|
-
} catch
|
|
587
|
-
|
|
634
|
+
} catch { /* auto-tick fail-soft */ }
|
|
635
|
+
const _bf = await detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, platformOpts, steps })
|
|
636
|
+
if (_bf.batched && _bf.aligned > 0) {
|
|
637
|
+
console.log(`\n🚀 execute 批量完成:plan 全勾 + 代码核验通过,一次性补完 ${_bf.aligned} 个剩余 step → 进入阶段完成分支`)
|
|
588
638
|
}
|
|
589
639
|
}
|
|
590
640
|
|
|
@@ -594,9 +644,30 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
594
644
|
// (测试对账/验收矩阵/module-impact 死信/PASS 封顶)照常全跑:批量省的是 --done
|
|
595
645
|
// 肥上下文往返,不省任何门。镜像 execute 批量模式(乐观预标戳 + 失败一并回滚)。
|
|
596
646
|
if (stageName === 'verify' && changeName) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
647
|
+
// 批量对齐前补亲测(cli-protocol-trust / R17 臂3 拦截⑥实证):乐观对齐会把 noAI 亲测步
|
|
648
|
+
// (step4 verifyRunQualityScan)一并标 completed → 亲测永不执行 → 收尾 PASS 封顶门因
|
|
649
|
+
// integrationRan≠ran 拒 → gate_rollback → --reopen 四步绕行。对齐面含该步时先亲跑
|
|
650
|
+
// executeVerifyQualityScan(幂等+指纹复用——已有有效记录时秒回零重复实测),失败则放弃
|
|
651
|
+
// 批量保持单步推进(亲测步照 noAI 硬门走)——「批量省的是上下文往返,不省任何门」承诺恢复成立。
|
|
652
|
+
// 判定经 hasPendingVerifyScanStep:DB 步骤行只有 {name,status}(_cliAction 不持久化——评审
|
|
653
|
+
// P1 修复:初版直接读 s._cliAction 恒 false 死代码),按 stageRegistry 定义索引对齐判。
|
|
654
|
+
let _skipBatch = false
|
|
655
|
+
if (hasPendingVerifyScanStep(stageName, steps)) {
|
|
656
|
+
try {
|
|
657
|
+
const { executeVerifyQualityScan } = await import('./verify-quality-scan.js')
|
|
658
|
+
console.log(' 🧪 批量对齐面含 noAI 亲测步——先亲测(幂等,有记录秒回)…')
|
|
659
|
+
await executeVerifyQualityScan({ cwd, specBase, changeName, platformOpts })
|
|
660
|
+
console.log(' 🧪 noAI 亲测完成(质量扫描记录落盘,亲测门不省)')
|
|
661
|
+
} catch (e) {
|
|
662
|
+
console.warn(' ⚠️ 亲测补跑失败——放弃批量对齐,保持单步推进(亲测步按 noAI 硬门走):' + (e && e.message ? e.message : e))
|
|
663
|
+
_skipBatch = true
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (!_skipBatch) {
|
|
667
|
+
const _vb = detectVerifyBatchFinish({ changeName, specBase, steps })
|
|
668
|
+
if (_vb.batched && _vb.aligned > 0) {
|
|
669
|
+
console.log(`\n🚀 verify 批量完成:报告结论已填 + facts 在场,一次性补完 ${_vb.aligned} 个剩余 step → 进入阶段完成分支(收尾门禁照常全跑)`)
|
|
670
|
+
}
|
|
600
671
|
}
|
|
601
672
|
}
|
|
602
673
|
|
|
@@ -619,8 +690,11 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
619
690
|
}
|
|
620
691
|
// quick 收尾(W6 Step6b 抽至 complete-handlers.js handleQuickStageCompletion);
|
|
621
692
|
// isNoDocs(task-02)--no-docs 豁免随链透传进 auditQuickCompletion options;
|
|
622
|
-
// sessionFlag(task-03)--session 随链透传进 quick
|
|
623
|
-
|
|
693
|
+
// sessionFlag(task-03)--session 随链透传进 quick 轻量归档链所有权校验;
|
|
694
|
+
// linkedChanges/linkedChangesAuto(R4-S-Q 缺陷 B):--done 时显式 --linked-changes 随链透传——
|
|
695
|
+
// 完成侧 guard 从落盘文件读,不接 opts 会静默丢弃 --done 时声明的关联(蒸馏尾/quicklog
|
|
696
|
+
// 关联变更行/closeQuickLinkedChanges 三处消费同断)
|
|
697
|
+
await handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, isNoDocs, sessionFlag, platformOpts, pm, quickFiles, linkedChanges, linkedChangesAuto })
|
|
624
698
|
|
|
625
699
|
// ── reopen --done 回填(坑 brainstorm-reopen-step-state-desync)──
|
|
626
700
|
// nextPendingIdx === -1 且无 waiting,说明要进阶段完成分支。此时若存在 stale 步骤
|
|
@@ -709,7 +783,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
709
783
|
|
|
710
784
|
// gate 全过:persist completed(task-01 移后)。此处到 _write 之间若崩,DB 仍 in-progress(内存已 completed 但未落盘),下次进 CLI 读 DB 即 in-progress,不产生"假 completed"。
|
|
711
785
|
pm._write(cwd, progress, changeName)
|
|
712
|
-
triggerSync(cwd, changeName, platformOpts)
|
|
786
|
+
triggerSync(cwd, changeName, platformOpts, { completion: true })
|
|
713
787
|
|
|
714
788
|
const total = steps.length
|
|
715
789
|
console.log(`✅ ${stageName} 阶段已完成(${total}/${total} 步)`)
|
|
@@ -717,8 +791,54 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
717
791
|
// 瘦会话交接提示(P1-5 v1,2026-09-20 对撞实验驱动):主流程阶段完成时点提示
|
|
718
792
|
// 「下一阶段可新会话续跑」——CLI prompt 自足(进度/上下文全由进度库与注入提供),
|
|
719
793
|
// 每阶段一个瘦会话省掉肥上下文 × N 请求的重发税。advisory 一行,不强制。
|
|
794
|
+
// ④(2026-09-23 R9 实证后)升级:同一会话连续完成 ≥2 阶段 → 肥上下文税升级告警
|
|
795
|
+
// (带实测数据 + handoff 机器预览态接力段引用 + 动作主体写明用户/平台——CLI 只产信号
|
|
796
|
+
// 不做编排,conventions 钉死的架构约束)。账本仿 wave-session-ledger(best-effort)。
|
|
720
797
|
if (['brainstorm', 'plan', 'execute', 'verify'].includes(stageName) && changeName) {
|
|
721
|
-
|
|
798
|
+
let stageLedger = null
|
|
799
|
+
let crossStage = false
|
|
800
|
+
try {
|
|
801
|
+
const ledgerPath = join(specBase, '.runtime', `stage-session-ledger-${changeName}.json`)
|
|
802
|
+
try { stageLedger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { /* 首阶段 */ }
|
|
803
|
+
const r = updateStageSessionLedger(stageLedger, process.env.SILLYSPEC_SESSION_ID, stageName)
|
|
804
|
+
stageLedger = r.ledger
|
|
805
|
+
crossStage = r.crossStageAdvisory
|
|
806
|
+
try { mkdirSync(join(specBase, '.runtime'), { recursive: true }); writeFileSync(ledgerPath, JSON.stringify(stageLedger, null, 1) + '\n', 'utf8') } catch { /* 账本 best-effort */ }
|
|
807
|
+
} catch { /* ④ 升级提示 best-effort 零副作用 */ }
|
|
808
|
+
// R16-b 修正(2026-09-25):plan ⛔ 优先于 crossStage 升级告警——plan+brainstorm 同会话
|
|
809
|
+
// 完成时(stageCount=2)此前走告警分支把 ⛔ 压掉了,agent 只见建议不见断崖动作。
|
|
810
|
+
if (stageName === 'plan') {
|
|
811
|
+
// 阶段断崖 B(R16 减负批次 A 相位,2026-09-24):plan→execute 是第二大跳变——
|
|
812
|
+
// execute 是最大单段(R15 45.6M/173r/均轮 263K,占全流程 53%),带着 brainstorm+plan
|
|
813
|
+
// 的 14-16M 历史跑 execute 每轮都在重放。⛔ directive 级(同 execute→verify 模板)。
|
|
814
|
+
console.log(``)
|
|
815
|
+
console.log(`⛔ 阶段断崖:plan 已收口,execute 建议在新的瘦会话执行。`)
|
|
816
|
+
console.log(` 本会话上下文已含 brainstorm+plan 全部历史(R15 实测 execute 段 45.6M/173 轮/均轮 263K——大头是历史重放税,活只有十几 M)。`)
|
|
817
|
+
console.log(` execute 的全部输入在盘上(design.md/plan.md/tasks/task-*.md),新会话零背景可续跑。`)
|
|
818
|
+
console.log(` 动作:`)
|
|
819
|
+
console.log(` 1. sillyspec handoff --change ${changeName} # 生成交接块(即切割凭证,stage.wall=hard 下生成后墙自动放行)`)
|
|
820
|
+
console.log(` 2. 新会话/子代理:粘贴交接块 → run execute --change ${changeName}`)
|
|
821
|
+
console.log(` 同会话硬续会被 stage.wall=hard 在 run 入口拒(--same-session 逃生口)。`)
|
|
822
|
+
} else if (stageName === 'execute') {
|
|
823
|
+
// 阶段断崖(2026-09-24 第一性原理落地):execute→verify 是上下文最贵的跳变
|
|
824
|
+
// (R11 实测 verify 71 轮 × 300K/轮 = 21.3M,其中 ~17M 是 execute 历史重放税)。
|
|
825
|
+
// verify 的每一步输入都在盘上(design/tasks/review/verify-probes),零对话依赖。
|
|
826
|
+
// ⛔ directive 级——agent 看到应当执行 handoff 而非硬续。
|
|
827
|
+
console.log(``)
|
|
828
|
+
console.log(`⛔ 阶段断崖:execute 已收口,verify 必须在新的瘦会话执行。`)
|
|
829
|
+
console.log(` 本会话上下文已含 execute 全部历史(R11 实测 verify 阶段因历史重放多付 ~17M token——占总 24%)。`)
|
|
830
|
+
console.log(` verify 的全部输入在盘上(design.md/tasks/review.json/verify-probes),新会话零背景可续跑。`)
|
|
831
|
+
console.log(` 动作:`)
|
|
832
|
+
console.log(` 1. sillyspec handoff --change ${changeName} # 生成交接块(即切割凭证,stage.wall=hard 下生成后墙自动放行)`)
|
|
833
|
+
console.log(` 2. 新会话/子代理:粘贴交接块 → run verify --change ${changeName}`)
|
|
834
|
+
console.log(` 同会话硬续会被 stage.wall=hard 在 run 入口拒(--same-session 应急逃生口)。`)
|
|
835
|
+
} else if (crossStage && stageLedger) {
|
|
836
|
+
console.log(`\n⚠️ 肥上下文税升级告警:本会话(${stageLedger.sessionId})已连续完成 ${stageLedger.stageCount} 个阶段(${(stageLedger.stages || []).join(' → ')})——单上下文历史重放实测在累积(R9:五阶段 53.5M 输入、尾段单轮 30 万+、归档 3 分钟烧 7.2M)。`)
|
|
837
|
+
console.log(` 建议现在换会话:sillyspec handoff --change ${changeName}(交接块含机器预览态接力段——watcher 投影的进行态参考)→ 由**用户新开会话**(或平台 session-fork)续跑下一阶段;保持 SILLYSPEC_SESSION_ID 不变。`)
|
|
838
|
+
console.log(` 会话不能自建会话——编排权在用户/平台,CLI 只产信号与接力载荷(conventions 架构约束)。同会话硬续不阻断。`)
|
|
839
|
+
} else {
|
|
840
|
+
console.log(`💡 瘦会话模式:下一阶段可在新会话/子代理续跑(sillyspec handoff --change ${changeName} 生成交接块;保持 SILLYSPEC_SESSION_ID 不变)——CLI prompt 自足`)
|
|
841
|
+
}
|
|
722
842
|
}
|
|
723
843
|
|
|
724
844
|
// 平台模式产物落点指针(platform-docs-dual-location,2026-09-15 EHS 实证):完成时点在
|
|
@@ -784,7 +904,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
784
904
|
// 历史教训:曾因此让 agent 在 brainstorm 完成后被误导去跑 scan。
|
|
785
905
|
const _bscale = readDesignScale(specBase, changeName)
|
|
786
906
|
if (_bscale === 'small') {
|
|
787
|
-
console.log(`\n👉 brainstorm 已完成(small)。下一步:sillyspec
|
|
907
|
+
console.log(`\n👉 brainstorm 已完成(small)。下一步:sillyspec flow start --change ${changeName}(轻量变更收编头脑风暴产物,2 调用收口)`)
|
|
788
908
|
} else {
|
|
789
909
|
console.log(`\n👉 brainstorm 已完成。下一步:sillyspec run plan${changeName ? ` --change ${changeName}` : ''}(scale=large 或未标 small 走完整 plan)`)
|
|
790
910
|
}
|
|
@@ -812,7 +932,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
812
932
|
|
|
813
933
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
814
934
|
pm._write(cwd, progress, changeName)
|
|
815
|
-
triggerSync(cwd, changeName, platformOpts)
|
|
935
|
+
triggerSync(cwd, changeName, platformOpts, { completion: true })
|
|
816
936
|
// 单步完成也刷新 change title:design.md 在 brainstorm step6 落盘,此后每次 --done 都该让
|
|
817
937
|
// changes.title 反映中文描述,不等阶段收尾(机制见 refreshChangeTitleFromArtifacts)。
|
|
818
938
|
refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName)
|
|
@@ -844,6 +964,27 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
844
964
|
if (defSteps && defSteps[nextPendingIdx]) {
|
|
845
965
|
console.log(`\n🚀 advanced to step ${nextPendingIdx + 1}/${steps.length}: ${defSteps[nextPendingIdx].name}`)
|
|
846
966
|
}
|
|
967
|
+
// ── Wave 边界 handoff 默认动作(2026-09-21 R5 接线 ql-20260921-007,①)──
|
|
968
|
+
// 刚完成的步是 Wave 步且下一步也是 Wave 步 = Wave 边界:把「换瘦会话」从建议升为默认动作
|
|
969
|
+
// (同会话硬续不阻断——advisory 语义)。账本:同会话跨 ≥2 Wave 时提示语气升级(肥上下文税
|
|
970
|
+
// 累积实证见 updateWaveSessionLedger 注释)。best-effort:账本读写失败零输出不阻断。
|
|
971
|
+
if (stageName === 'execute' && changeName && defSteps && defSteps[currentIdx] && defSteps[nextPendingIdx] &&
|
|
972
|
+
/Wave \d+ 执行/.test(defSteps[currentIdx].name || '') && /Wave \d+ 执行/.test(defSteps[nextPendingIdx].name || '')) {
|
|
973
|
+
try {
|
|
974
|
+
const ledgerPath = join(specBase, '.runtime', `wave-session-ledger-${changeName}.json`)
|
|
975
|
+
let ledger = null
|
|
976
|
+
try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { /* 首 Wave */ }
|
|
977
|
+
const { ledger: nextLedger, crossWaveAdvisory } = updateWaveSessionLedger(ledger, process.env.SILLYSPEC_SESSION_ID, defSteps[currentIdx].name)
|
|
978
|
+
try { mkdirSync(join(specBase, '.runtime'), { recursive: true }); writeFileSync(ledgerPath, JSON.stringify(nextLedger, null, 1) + '\n', 'utf8') } catch { /* 账本 best-effort */ }
|
|
979
|
+
console.log(`\n🔄 Wave 边界——默认动作:下一 Wave 换瘦会话续跑(每请求背全量历史,130K 上下文 ×每步重发≈1/4 墙钟,实测 2026-09-21):`)
|
|
980
|
+
console.log(` 1. sillyspec handoff --change ${changeName} # 生成交接块(含任务面/未决阻断/决策 ID 回源)`)
|
|
981
|
+
console.log(` 2. 新会话(SILLYSPEC_SESSION_ID 不变)粘贴交接块 → sillyspec run execute --change ${changeName}`)
|
|
982
|
+
console.log(` 同会话硬续不阻断;上下文已压缩过(Read 旧工件可回源)则续跑无妨。`)
|
|
983
|
+
if (crossWaveAdvisory) {
|
|
984
|
+
console.log(` ⚠️ 本会话已完成 ${nextLedger.waveCount} 个 Wave——肥上下文税在累积(缓存救账单不救墙钟),强烈建议 handoff。`)
|
|
985
|
+
}
|
|
986
|
+
} catch { /* 边界提示 best-effort */ }
|
|
987
|
+
}
|
|
847
988
|
// quick 末步四字段前置预告(坑 quick-step3-four-fields-late):末步 --done 的 --output 四字段
|
|
848
989
|
// 是硬校验(缺任一项被拒 + 回滚),但模板藏在 step3 长 prompt 中段——task-08 同因(长 prompt
|
|
849
990
|
// 易被 tail 截断),agent 常到 --done 被拦才第一次见到模板。推进到末步的此刻用底部短块预告
|
|
@@ -1126,6 +1267,139 @@ function prefetchDiffFileSet(ctx) {
|
|
|
1126
1267
|
}
|
|
1127
1268
|
}
|
|
1128
1269
|
|
|
1270
|
+
/**
|
|
1271
|
+
* burst 完成侧循环包装(2026-09-22-stage-burst-fold D-003@v2/D-004@v2/D-005/D-009,FR-03):
|
|
1272
|
+
* burst 模式下一次 --done 收口全部剩余步——循环调既有 completeStep(**本体零改动**,全部守卫
|
|
1273
|
+
* ——WAIT 硬校验/waiting 前置/requiresWait 门控/意图断言/门禁/并发防护——逐轮原样生效)。
|
|
1274
|
+
*
|
|
1275
|
+
* 循环语义(每轮):
|
|
1276
|
+
* 1. **轮首:尾随 stale 拉回**(D-003@v2,钉死先于退出判定——「仅剩尾随 stale、无 pending」的轮
|
|
1277
|
+
* 必须先拉回再判退出,否则 --reopen 中途开 burst 时渲染集合与完成集合发散、stale 尾残留阶梯):
|
|
1278
|
+
* 首个非 completed/skipped 步为 stale → 拉回 pending + pm._write(与 runStage 单步路径同语义)。
|
|
1279
|
+
* 2. 重算 pending(谓词 pending|in-progress|blocked,与 completeStep 内部一致);无 → 退出循环。
|
|
1280
|
+
* 3. 调 completeStep(pm, progress, stageName, cwd, **null**, inputText, {...roundOptions, printNext:false})
|
|
1281
|
+
* ——每轮 outputText=null 走 P0-2 事实合成(per-step 摘要 CLI 合成,agent 只给一次整体 --output)。
|
|
1282
|
+
* completeStep 遇守卫失败 process.exit 即 burst 天然断点(agent 修复后重跑 --done 从断点幂等续推);
|
|
1283
|
+
* 续推/停轮以每轮后重读 DB 态为准(正常完成也返回 truthy 进度对象——真值性不代表失败):
|
|
1284
|
+
* 无 pending → 收口透传;pending 未前进(失败步保持/重拦)→ 停断点 + exitCode=1。
|
|
1285
|
+
* 4. 每轮后 pm.read 重读(null=并发归档删除 → 报错 exitCode=1,对齐 command.js auto 路径 BUG-05 语义)。
|
|
1286
|
+
* 5. **--answer 单次消费**(D-004@v2):轮前快照全部步 waitAnswer、轮后比对——任一步 waitAnswer
|
|
1287
|
+
* 新变为 === doneAnswer → 已消费,后续轮剥离(防 brainstorm 双 requiresWait 步〔选方案/确认设计〕
|
|
1288
|
+
* 把步4答案错填步5;快照比对防 waiting 解析重定向漏检)。
|
|
1289
|
+
* 6. **--step 断言仅首轮**(D-005):第二轮起当前步已推进、断言必然 mismatch,首轮后剥离。
|
|
1290
|
+
*
|
|
1291
|
+
* 循环上限 50 轮(单阶段步数不可能超),耗尽仍有 pending → 报错防死循环。
|
|
1292
|
+
* agent 的整体 --output 只打一行横幅(D-009:不落步记录——语义错归属会污染步骤审计面)。
|
|
1293
|
+
*
|
|
1294
|
+
* @param {import('./shared.js').ProgressManagerLike} pm
|
|
1295
|
+
* @param {object} progress
|
|
1296
|
+
* @param {string} stageName
|
|
1297
|
+
* @param {string} cwd
|
|
1298
|
+
* @param {string|null} outputText agent 整体摘要(仅横幅展示,不落步记录)
|
|
1299
|
+
* @param {string|null} [inputText]
|
|
1300
|
+
* @param {object} [options] 透传 completeStep 全量字段(printNext 强制 false、每轮 outputText 强制 null)
|
|
1301
|
+
* @returns {Promise<object|undefined>} 末轮 completeStep 返回值
|
|
1302
|
+
*/
|
|
1303
|
+
export async function completeStepBurst(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
1304
|
+
if (outputText) console.log(`📦 burst 收口摘要:${outputText}`)
|
|
1305
|
+
let roundOptions = { ...options, printNext: false }
|
|
1306
|
+
// burst 单调时钟基准(坑 stage-burst ①):null=首轮(用真实时刻),此后每轮保严格递增
|
|
1307
|
+
let burstClock = null
|
|
1308
|
+
const MAX_ROUNDS = 50
|
|
1309
|
+
let lastResult
|
|
1310
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
1311
|
+
// ① 轮首尾随 stale 拉回(先于退出判定)
|
|
1312
|
+
const stepsNow = progress?.stages?.[stageName]?.steps
|
|
1313
|
+
if (Array.isArray(stepsNow) && stepsNow.length > 0) {
|
|
1314
|
+
const firstOpenIdx = stepsNow.findIndex(s => s && s.status !== 'completed' && s.status !== 'skipped')
|
|
1315
|
+
if (firstOpenIdx !== -1 && stepsNow[firstOpenIdx].status === 'stale') {
|
|
1316
|
+
console.log(`⚠️ Step "${stepsNow[firstOpenIdx].name}" 处于 stale,burst 轮首拉回待执行。`)
|
|
1317
|
+
stepsNow[firstOpenIdx].status = 'pending'
|
|
1318
|
+
// 拉回清残留(坑 stage-burst-timeline-quality-collapse ③,2026-09-24 实证):拉回
|
|
1319
|
+
// 仅改 status 不清 output/completedAt → 平台时间线出现「pending 步挂着已完成合成
|
|
1320
|
+
// 文案」的矛盾态行。与 reopenStage fromStep 步口径一致(pending 必不带完成面)。
|
|
1321
|
+
delete stepsNow[firstOpenIdx].output
|
|
1322
|
+
delete stepsNow[firstOpenIdx].output_truncated
|
|
1323
|
+
delete stepsNow[firstOpenIdx].output_original_length
|
|
1324
|
+
delete stepsNow[firstOpenIdx].completedAt
|
|
1325
|
+
pm._write(cwd, progress, options.changeName)
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
// ② 重算 pending(谓词与 completeStep 内部一致)
|
|
1329
|
+
const stepsForPending = progress?.stages?.[stageName]?.steps
|
|
1330
|
+
const pendingIdx = Array.isArray(stepsForPending)
|
|
1331
|
+
? stepsForPending.findIndex(s => s && (s.status === 'pending' || s.status === 'in-progress' || s.status === 'blocked'))
|
|
1332
|
+
: -1
|
|
1333
|
+
if (pendingIdx === -1) {
|
|
1334
|
+
if (round === 0) {
|
|
1335
|
+
// 退化场景对齐(execute 轮 Grill P2-2):无任何待完成步的 --done 委托 completeStep 走
|
|
1336
|
+
// 既有 no-pending 处理(「没有待完成的步骤」报错 / --confirm stale 回填 / 矛盾态自愈)
|
|
1337
|
+
// ——burst 不静默吞掉该信号(静默 break 会让 auto 路径跳过 nextInFlow 推进)。
|
|
1338
|
+
return await completeStep(pm, progress, stageName, cwd, outputText ?? null, inputText, roundOptions)
|
|
1339
|
+
}
|
|
1340
|
+
break
|
|
1341
|
+
}
|
|
1342
|
+
// ③ 轮前 waitAnswer 快照(D-004@v2 消费检测基准)
|
|
1343
|
+
const answerSnapshot = roundOptions.doneAnswer != null && Array.isArray(stepsForPending)
|
|
1344
|
+
? stepsForPending.map(s => (s && s.waitAnswer) || null)
|
|
1345
|
+
: null
|
|
1346
|
+
// 单调时钟(坑 stage-burst-timeline-quality-collapse ①):completedAt 是秒级字符串——
|
|
1347
|
+
// 比较必须取秒粒度(毫秒级真实推进落在同一秒内仍会产出相同字符串),同秒或追不上
|
|
1348
|
+
// 上一轮戳 → +1s 递增,保轮间 completedAt 字符串严格互异且递增(平台时间线可读)。
|
|
1349
|
+
const _now = new Date()
|
|
1350
|
+
const _stamped = (!burstClock || Math.floor(_now.getTime() / 1000) > Math.floor(burstClock.getTime() / 1000))
|
|
1351
|
+
? _now
|
|
1352
|
+
: new Date(burstClock.getTime() + 1000)
|
|
1353
|
+
burstClock = _stamped
|
|
1354
|
+
const result = await completeStep(pm, progress, stageName, cwd, null, inputText, {
|
|
1355
|
+
...roundOptions,
|
|
1356
|
+
completedAtOverride: _stamped,
|
|
1357
|
+
})
|
|
1358
|
+
lastResult = result
|
|
1359
|
+
// ④ 每轮后重读(completeStep 内部已落库;重读后下一轮谓词基于真实 DB 态)。
|
|
1360
|
+
// 注意 completeStep 正常完成单步也返回 truthy 进度对象({stageCompleted:false, nextPendingIdx})
|
|
1361
|
+
// ——续推/停轮判定以重读 DB 态为准,不以返回值真值性为准(探针实证:步 7 正常完成返回
|
|
1362
|
+
// {stageCompleted:false,currentIdx:6,nextPendingIdx:7})。
|
|
1363
|
+
const fresh = pm.read(cwd, options.changeName)
|
|
1364
|
+
if (!fresh) {
|
|
1365
|
+
console.error(`❌ burst 收口推进后进度数据消失(变更 ${options.changeName} 可能被并发归档/删除),请用 sillyspec progress show 核对当前状态`)
|
|
1366
|
+
process.exitCode = 1
|
|
1367
|
+
return lastResult
|
|
1368
|
+
}
|
|
1369
|
+
progress = fresh
|
|
1370
|
+
// 停轮判定:重读后无 pending → 阶段收口(gate 返回态由 lastResult 透传);pending 未前进
|
|
1371
|
+
//(门禁失败步保持 pending / blocked 重拦 / 前置步被重置)→ 停在断点防同步锤击(重跑幂等续推)。
|
|
1372
|
+
const stepsAfterRound = progress?.stages?.[stageName]?.steps
|
|
1373
|
+
const nextPendingIdx = Array.isArray(stepsAfterRound)
|
|
1374
|
+
? stepsAfterRound.findIndex(s => s && (s.status === 'pending' || s.status === 'in-progress' || s.status === 'blocked'))
|
|
1375
|
+
: -1
|
|
1376
|
+
if (nextPendingIdx === -1) break
|
|
1377
|
+
if (nextPendingIdx <= pendingIdx) {
|
|
1378
|
+
if (!process.exitCode) process.exitCode = 1
|
|
1379
|
+
break
|
|
1380
|
+
}
|
|
1381
|
+
// ⑤ --answer 单次消费:快照比对(任一步 waitAnswer 新变为 === doneAnswer 即已消费)
|
|
1382
|
+
if (roundOptions.doneAnswer != null && answerSnapshot) {
|
|
1383
|
+
const stepsAfter = progress?.stages?.[stageName]?.steps || []
|
|
1384
|
+
const consumed = stepsAfter.some((s, i) => s && s.waitAnswer === roundOptions.doneAnswer
|
|
1385
|
+
&& (answerSnapshot[i] == null || answerSnapshot[i] !== roundOptions.doneAnswer))
|
|
1386
|
+
if (consumed) roundOptions = { ...roundOptions, doneAnswer: undefined }
|
|
1387
|
+
}
|
|
1388
|
+
// ⑥ --step 断言仅首轮
|
|
1389
|
+
if (round === 0 && roundOptions.stepAssert !== undefined) roundOptions = { ...roundOptions, stepAssert: undefined }
|
|
1390
|
+
}
|
|
1391
|
+
// 防死循环:耗尽仍有 pending
|
|
1392
|
+
const stepsFinal = progress?.stages?.[stageName]?.steps
|
|
1393
|
+
const stillOpenIdx = Array.isArray(stepsFinal)
|
|
1394
|
+
? stepsFinal.findIndex(s => s && (s.status === 'pending' || s.status === 'in-progress' || s.status === 'blocked'))
|
|
1395
|
+
: -1
|
|
1396
|
+
if (stillOpenIdx !== -1) {
|
|
1397
|
+
console.error(`❌ burst 收口循环达到上限(${MAX_ROUNDS} 轮)仍有待完成步骤「${stepsFinal[stillOpenIdx].name}」——疑似状态推进异常,请 sillyspec progress show 核对后重跑。`)
|
|
1398
|
+
process.exitCode = 1
|
|
1399
|
+
}
|
|
1400
|
+
return lastResult
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1129
1403
|
/**
|
|
1130
1404
|
* 判定 task 是否该被 autoCheckPlanFromReviews 自动勾选。
|
|
1131
1405
|
* 端到端/deployment-critical task 要求 review spec+quality 双 pass(cannot_verify 不算,防批量完成
|
|
@@ -1292,6 +1566,22 @@ export async function autoCheckPlanFromReviews({ stageName, changeName, cwd, pla
|
|
|
1292
1566
|
* verify-facts.json 在场(探针跑过)+ 锚定步(step 1-2)已完成。批量只标剩余 step
|
|
1293
1567
|
* completed,收尾门禁由阶段完成分支照常执行(批量不绕门)。
|
|
1294
1568
|
*/
|
|
1569
|
+
/**
|
|
1570
|
+
* 批量对齐面是否含未完成的 verify 亲测步(cli-protocol-trust,评审 P1 修复的判定半边):
|
|
1571
|
+
* DB 步骤行只有 {name,status}——_cliAction 在 stageRegistry 的步骤定义上,按索引对齐判定
|
|
1572
|
+
* (进度库步骤按定义种子,同序同名);步对象自带 _cliAction 时兜底直读。导出供 test 直测。
|
|
1573
|
+
*/
|
|
1574
|
+
export function hasPendingVerifyScanStep(stageName, steps) {
|
|
1575
|
+
try {
|
|
1576
|
+
const defSteps = stageRegistry[stageName]?.steps || []
|
|
1577
|
+
return (steps || []).some((s, i) =>
|
|
1578
|
+
(s.status === 'pending' || s.status === 'in-progress') &&
|
|
1579
|
+
(s._cliAction === 'verifyRunQualityScan' || defSteps[i]?._cliAction === 'verifyRunQualityScan'))
|
|
1580
|
+
} catch {
|
|
1581
|
+
return false // fail-open:判定异常不阻断批量(亲测门兜底由收尾对账承担)
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1295
1585
|
export function detectVerifyBatchFinish({ changeName, specBase, steps }) {
|
|
1296
1586
|
if (!changeName) return { batched: false, aligned: 0 }
|
|
1297
1587
|
try {
|
|
@@ -1570,7 +1860,7 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
|
|
|
1570
1860
|
|
|
1571
1861
|
progress.lastActive = now
|
|
1572
1862
|
pm._write(cwd, progress, changeName)
|
|
1573
|
-
triggerSync(cwd, changeName, platformOpts)
|
|
1863
|
+
triggerSync(cwd, changeName, platformOpts, { completion: true })
|
|
1574
1864
|
|
|
1575
1865
|
// 盖章路径的收尾提示独立分叉:wait 已在记录态完成(非等待用户),提示 --done 直接收尾;
|
|
1576
1866
|
// 无 --inherit-from 路径输出与现状逐字节一致(兼容红线)。
|
|
@@ -1700,7 +1990,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
1700
1990
|
|
|
1701
1991
|
progress.lastActive = now
|
|
1702
1992
|
pm._write(cwd, progress, changeName)
|
|
1703
|
-
triggerSync(cwd, changeName, platformOpts)
|
|
1993
|
+
triggerSync(cwd, changeName, platformOpts, { completion: true })
|
|
1704
1994
|
// wait 解除持久化点(含 repeatable 多轮)同样刷新 change title——多轮确认期间 design.md 已存在
|
|
1705
1995
|
refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName)
|
|
1706
1996
|
|
|
@@ -1809,14 +2099,6 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
1809
2099
|
if (stageName === 'execute') {
|
|
1810
2100
|
console.log(` ⚠️ 若 worktree 改动还没 apply 到主工作区,先:sillyspec worktree apply ${changeName}`)
|
|
1811
2101
|
console.log(` (apply 不需要先 commit,支持 working tree 未提交改动)`)
|
|
1812
|
-
// plan.md checkbox auto-check:execute 完成 + review.json pass → 自动勾选(治本,比警告可靠)
|
|
1813
|
-
const _ac = await autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts })
|
|
1814
|
-
if (_ac.autoChecked) {
|
|
1815
|
-
console.log(` ✅ 自动勾选 ${_ac.checkedCount} 个 task checkbox(基于 review.json pass)`)
|
|
1816
|
-
}
|
|
1817
|
-
if (_ac.skippedCount > 0) {
|
|
1818
|
-
console.warn(` ⚠️ ${_ac.skippedCount} 个 task 未勾(review.json 缺失/fail)→ archive 会拦。补 review 后重跑 execute --done 触发自动勾`)
|
|
1819
|
-
}
|
|
1820
2102
|
}
|
|
1821
2103
|
}
|
|
1822
2104
|
return { stageCompleted: true, currentIdx, nextPendingIdx: -1 }
|
|
@@ -1876,7 +2158,7 @@ export async function skipStep(pm, progress, stageName, cwd, changeName, platfor
|
|
|
1876
2158
|
steps[currentIdx].skippedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1877
2159
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1878
2160
|
pm._write(cwd, progress, changeName)
|
|
1879
|
-
triggerSync(cwd, changeName, platformOpts)
|
|
2161
|
+
triggerSync(cwd, changeName, platformOpts, { completion: true })
|
|
1880
2162
|
|
|
1881
2163
|
console.log(`⏭️ Step ${currentIdx + 1}/${steps.length} 已跳过:${steps[currentIdx].name}`)
|
|
1882
2164
|
|