sillyspec 3.26.11 → 3.26.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-archive/SKILL.md +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-knowledge/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +5 -5
- package/.claude/skills/sillyspec-quick/SKILL.md +5 -1
- package/README.md +7 -5
- package/SKILL.md +5 -4
- package/package.json +1 -1
- package/packages/dashboard/server/executor.js +10 -1
- package/packages/dashboard/server/index.js +108 -46
- package/src/config-schema.js +3 -0
- package/src/contract-matrix.js +22 -13
- package/src/db.js +24 -4
- package/src/dispatch/probe.js +99 -63
- package/src/docs-check.js +58 -13
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +208 -21
- package/src/fs-atomic.js +3 -1
- package/src/git-helper.js +7 -4
- package/src/hooks/worktree-guard.js +8 -2
- package/src/index.js +200 -27
- package/src/init.js +3 -2
- package/src/local-detect.js +9 -1
- package/src/modules.js +7 -5
- package/src/progress/stage-machine.js +589 -578
- package/src/progress/step-store.js +1 -1
- package/src/progress.js +187 -30
- package/src/quicklog.js +47 -16
- package/src/run/command.js +88 -46
- package/src/run/complete-handlers.js +27 -15
- package/src/run/complete.js +1186 -1121
- package/src/run/concurrent-detect.js +3 -1
- package/src/run/gates.js +172 -25
- package/src/run/prompt.js +17 -9
- package/src/run/scan-profile.js +35 -23
- package/src/run/shared.js +164 -63
- package/src/run/stage.js +41 -1
- package/src/setup.js +8 -10
- package/src/sillyhub-mcp/client.js +74 -6
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +43 -8
- package/src/stage-contract.js +12 -38
- package/src/stage-review.js +40 -1
- package/src/stages/archive.js +4 -3
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +27 -13
- package/src/stages/doctor.js +29 -16
- package/src/stages/execute.js +170 -154
- package/src/stages/knowledge.js +18 -3
- package/src/stages/plan-postcheck.js +72 -37
- package/src/stages/plan.js +70 -48
- package/src/stages/quick.js +3 -1
- package/src/stages/verify.js +1 -1
- package/src/sync.js +339 -225
- package/src/task-review.js +281 -64
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +48 -35
- package/src/worktree-apply.js +58 -34
- package/src/worktree-deps.js +120 -20
- package/src/worktree.js +55 -9
- package/.npmrc-publish +0 -2
|
@@ -104,8 +104,10 @@ export function detectConcurrentChanges(cwd, { changeName, linkedChanges = [], o
|
|
|
104
104
|
if (!file) continue
|
|
105
105
|
|
|
106
106
|
// rule1:落 .sillyspec/changes/<dir>/ 且 <dir> 非本变更非关联 → otherActiveChanges 去重。
|
|
107
|
+
// 'archive' 排除(坑 archive-dir-as-other-change):归档移动产生 .sillyspec/changes/archive/...
|
|
108
|
+
// 脏路径,extractChangeDir 返回 'archive',不排除会把归档动作误报成「他者活跃变更: archive」。
|
|
107
109
|
const dir = extractChangeDir(file)
|
|
108
|
-
if (dir && !ownChangeSet.has(normalizeGitPath(dir))) {
|
|
110
|
+
if (dir && dir !== 'archive' && !ownChangeSet.has(normalizeGitPath(dir))) {
|
|
109
111
|
otherActiveSet.add(dir)
|
|
110
112
|
continue
|
|
111
113
|
}
|
package/src/run/gates.js
CHANGED
|
@@ -26,15 +26,16 @@ import { runValidators } from '../stage-contract.js'
|
|
|
26
26
|
import { handleScanStageCompleted, handleExecuteWorktreeCleanup } from './complete-handlers.js'
|
|
27
27
|
import { detectConcurrentChanges, formatConcurrentWarning } from './concurrent-detect.js'
|
|
28
28
|
import { stageRegistry } from '../stages/index.js'
|
|
29
|
+
import { normalizeTaskId } from '../taskcard.js'
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
-
* 只认 checkbox 行 `- [ ] task-XX:` / `- [x] task-XX:`(与 execute.js
|
|
33
|
-
*
|
|
32
|
+
* 从任务注册表(tasks.md)提取全部 task id(task-XX)——符号影响面覆盖度校验用。
|
|
33
|
+
* 只认 checkbox 行 `- [ ] task-XX:` / `- [x] task-XX:`(与 execute.js parseTaskRegistry 同口径)。
|
|
34
|
+
* 2026-08-20-task-truth-unify:源从 plan.md 迁 tasks.md(任务唯一真相);ql-xxx 行不收。
|
|
34
35
|
*/
|
|
35
|
-
function
|
|
36
|
-
if (!
|
|
37
|
-
return [...String(
|
|
36
|
+
function extractTaskIdsFromRegistry(tasksContent) {
|
|
37
|
+
if (!tasksContent) return []
|
|
38
|
+
return [...String(tasksContent).matchAll(/^[-*]\s*\[[ xX]\]\s*task-(\d+)\b/gim)]
|
|
38
39
|
.map(m => `task-${m[1].padStart(2, '0')}`)
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -56,8 +57,10 @@ export function validateSymbolImpactReport({ reportContent, planContent }) {
|
|
|
56
57
|
if (!reportContent || !String(reportContent).trim()) {
|
|
57
58
|
return { ok: false, errors: ['symbol-impact.md 不存在或内容为空——「加载上下文」步的符号影响面报告必须落盘(逐 task 一行结论,「无签名级变更」也要显式写)'] }
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
// 2026-08-20-task-truth-unify:task 清单源迁 tasks.md(参数名 planContent 保持兼容既有调用,
|
|
61
|
+
// 语义=任务注册表内容;调用方 enforceSymbolImpactGate 已改读 tasks.md)
|
|
62
|
+
const taskIds = extractTaskIdsFromRegistry(planContent)
|
|
63
|
+
if (taskIds.length === 0) return { ok: true, errors } // 注册表无 task 可列(默认兜底形态)→ 无可校验对象
|
|
61
64
|
const missing = taskIds.filter(id => !String(reportContent).includes(id))
|
|
62
65
|
for (const id of missing) {
|
|
63
66
|
errors.push(`${id} 未在 symbol-impact.md 中出现——每个 task 都要有一行结论(含「无签名级变更」的显式声明)`)
|
|
@@ -72,14 +75,14 @@ export function validateSymbolImpactReport({ reportContent, planContent }) {
|
|
|
72
75
|
export async function enforceSymbolImpactGate(stageName, changeName, currentStepName, specBase) {
|
|
73
76
|
if (stageName !== 'execute') return
|
|
74
77
|
if (!currentStepName || !currentStepName.includes('加载上下文')) return
|
|
75
|
-
const
|
|
78
|
+
const tasksPath = join(specBase, 'changes', changeName || '', 'tasks.md')
|
|
76
79
|
const reportPath = join(specBase, 'changes', changeName || '', 'symbol-impact.md')
|
|
77
|
-
if (!existsSync(
|
|
78
|
-
let
|
|
79
|
-
try {
|
|
80
|
+
if (!existsSync(tasksPath)) return // 注册表缺失场景已有 plan 阶段 gate 把关,此处不重复拦
|
|
81
|
+
let tasksContent = ''
|
|
82
|
+
try { tasksContent = readFileSync(tasksPath, 'utf8') } catch { return }
|
|
80
83
|
let reportContent = null
|
|
81
84
|
try { reportContent = readFileSync(reportPath, 'utf8') } catch { /* 缺失由 validate 报 */ }
|
|
82
|
-
const result = validateSymbolImpactReport({ reportContent, planContent })
|
|
85
|
+
const result = validateSymbolImpactReport({ reportContent, planContent: tasksContent })
|
|
83
86
|
if (result.ok) return
|
|
84
87
|
console.error('❌ ── 符号影响面报告校验阻断(本次 --done 未完成,进度未推进)──')
|
|
85
88
|
console.error(` 「加载上下文」步的符号影响面检查报告未落盘或不完整:`)
|
|
@@ -89,6 +92,26 @@ export async function enforceSymbolImpactGate(stageName, changeName, currentStep
|
|
|
89
92
|
process.exit(1)
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
/**
|
|
96
|
+
* UI 原型软提醒(非阻断):brainstorm「生成规范文件」步 --done 时,design.md 命中前端文件
|
|
97
|
+
* 但变更目录无 prototype-*.html → console.warn 一行,用户可否决。程度判断(布局级才必须出)
|
|
98
|
+
* 是语义判断,CLI 不硬卡只提示——治 agent 对明显 UI 改造静默跳过原型的坑。
|
|
99
|
+
*/
|
|
100
|
+
export function warnMissingUiPrototype(stageName, changeName, currentStepName, specBase) {
|
|
101
|
+
if (stageName !== 'brainstorm' || !currentStepName || !currentStepName.includes('生成规范文件')) return
|
|
102
|
+
if (!changeName || !specBase) return
|
|
103
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
104
|
+
const designPath = join(changeDir, 'design.md')
|
|
105
|
+
if (!existsSync(designPath)) return
|
|
106
|
+
try {
|
|
107
|
+
if (readdirSync(changeDir).some(f => /^prototype-.*\.html$/i.test(f))) return
|
|
108
|
+
} catch { return } // 目录不可读(平台模式 specRoot 差异等)→ 静默跳过,不制造噪音
|
|
109
|
+
const design = readFileSync(designPath, 'utf8')
|
|
110
|
+
const feExts = [...new Set((design.match(/\.(vue|svelte|astro|tsx|jsx|css|scss|sass|less|styl|html)\b/gi) || []).map(s => s.replace('.', '').toLowerCase()))]
|
|
111
|
+
if (feExts.length === 0) return
|
|
112
|
+
console.warn(` ⚠️ design.md 命中前端文件类型(${feExts.join('/')})但变更目录无 prototype-*.html——轻微样式改动可忽略;布局/交互流程级变化应补原型(sillyspec run brainstorm --reopen 回到「分段展示设计」,或直接让 agent 补生成)`)
|
|
113
|
+
}
|
|
114
|
+
|
|
92
115
|
/**
|
|
93
116
|
* 判断当前 execute step 所在 wave 是否全部 task 都声明 no_deps_verify: true(D-006@v2)。
|
|
94
117
|
* 仅 wave 执行步骤(名如 "Wave N 执行")可 opt-out;非 wave 步骤恒返回 false(保守过门)。
|
|
@@ -105,10 +128,13 @@ function isCurrentWaveAllNoDepsVerify(stepName, changeDir) {
|
|
|
105
128
|
const waveRe = new RegExp(`^##\\s*Wave\\s+${waveN}\\b[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s|\\n#\\s|$)`, 'm')
|
|
106
129
|
const waveMatch = plan.match(waveRe)
|
|
107
130
|
if (!waveMatch) return false
|
|
108
|
-
|
|
131
|
+
// 2026-08-20-task-truth-unify:Wave 段任务为纯 ID 引用行("- task-XX"),不再有 checkbox
|
|
132
|
+
const taskIds = [...waveMatch[1].matchAll(/^[-*]\s+(task-\d+)\s*$/gim)].map(x => x[1])
|
|
109
133
|
if (taskIds.length === 0) return false
|
|
110
134
|
for (const id of taskIds) {
|
|
111
|
-
|
|
135
|
+
// 体检 BUG-19:plan 常态写补零 task-01,但 AI 写出 task-3 时卡片实际是 task-03.md——
|
|
136
|
+
// 未归一则 existsSync 恒 false,wave 级 no_deps_verify 豁免静默失效(保守方向)
|
|
137
|
+
const cardPath = join(changeDir, 'tasks', `${normalizeTaskId(id)}.md`)
|
|
112
138
|
if (!existsSync(cardPath)) return false // 卡片缺失 → 保守不跳
|
|
113
139
|
const card = readFileSync(cardPath, 'utf8')
|
|
114
140
|
const fm = card.match(/^---\n([\s\S]*?)\n---/)
|
|
@@ -118,6 +144,99 @@ function isCurrentWaveAllNoDepsVerify(stepName, changeDir) {
|
|
|
118
144
|
return true
|
|
119
145
|
}
|
|
120
146
|
|
|
147
|
+
/**
|
|
148
|
+
* doctor --align-execute-progress --confirm 的前置 review 门(坑 doctor-align-bypass-review-gate,
|
|
149
|
+
* 2026-08-20 实证):alignExecuteToPlan 直写 completed「绕过 completeStep 推导」,此前 execute 的
|
|
150
|
+
* Stage Review Gate 与 Task Review Gate 被整体跳过——而 worktree 清理后的恢复场景恰是最需要
|
|
151
|
+
* review 审计的时刻(实证靠 15 份 task review + verify 全程补足覆盖才没出事)。
|
|
152
|
+
*
|
|
153
|
+
* 本函数供 CLI 层(index.js doctor align 分支)在 confirm 落盘前调用,跑与正常完成路径同源的
|
|
154
|
+
* 只读校验(不自动生成 marker/runId——恢复读场景缺失就是缺失,如实报告):
|
|
155
|
+
* 1. Stage Review:tier=independent 时须存在有效 execute stage review.json(verdict 非 fail);
|
|
156
|
+
* tier=self 放行(与正常 gate 分级一致)。
|
|
157
|
+
* 2. Task Review:所有 task 的 review.json 齐备且 verdict 非 fail(runId 解析同 Task Review Gate:
|
|
158
|
+
* marker → 含 tasks/ 目录扫描;不 generate)。
|
|
159
|
+
*
|
|
160
|
+
* @param {{ cwd: string, changeName: string, specBase: string, platformOpts?: object }} opts
|
|
161
|
+
* @returns {Promise<boolean>} true=被门阻断(CLI 应放弃 align 并 exit 1);false=校验通过可继续
|
|
162
|
+
*/
|
|
163
|
+
export async function enforceAlignExecuteReviewGate({ cwd, changeName, specBase, platformOpts = {} }) {
|
|
164
|
+
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
165
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, effectiveSpecBase)
|
|
166
|
+
const reviewChangeDir = resolveChangeDir(cwd, { currentChange: changeName }, platformOpts?.specRoot || null)
|
|
167
|
+
const blocked = (msgs) => {
|
|
168
|
+
console.error('\n❌ doctor --align-execute-progress 前置 review 校验未过——execute 完成审计不能绕过:')
|
|
169
|
+
for (const m of msgs) console.error(' - ' + m)
|
|
170
|
+
console.error(' 解法:补齐上述 review 后重跑 align(stage review:sillyspec register-stage-review --change ' + changeName + ' --stage execute;task review:按报错路径补 review.json)')
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 1. Stage Review(tier 分级同正常 gate)
|
|
175
|
+
try {
|
|
176
|
+
const { classifyReviewTier } = await import('../review-tier.js')
|
|
177
|
+
const { validateStageReview, getLatestStageReviewRunId, printStageReviewResult } = await import('../stage-review.js')
|
|
178
|
+
const designPath = reviewChangeDir ? join(reviewChangeDir, 'design.md') : null
|
|
179
|
+
let planLevel = null
|
|
180
|
+
if (reviewChangeDir) {
|
|
181
|
+
const planPath = join(reviewChangeDir, 'plan.md')
|
|
182
|
+
if (existsSync(planPath)) {
|
|
183
|
+
const fmLine = readFileSync(planPath, 'utf8').split('\n').find(l => l.trim().startsWith('plan_level:'))
|
|
184
|
+
if (fmLine) planLevel = fmLine.split(':')[1].trim()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const tier = classifyReviewTier({ planLevel, designPath })
|
|
188
|
+
if (tier.tier === 'self') {
|
|
189
|
+
console.log('ℹ️ Stage Review: execute tier=self(' + tier.reason + '),align 前置门放行(不强制独立审查)')
|
|
190
|
+
} else {
|
|
191
|
+
// 只读解析(不 generate/写 marker):恢复场景下缺失即缺失
|
|
192
|
+
const reviewRunId = getLatestStageReviewRunId(runtimeRoot, 'execute', changeName)
|
|
193
|
+
if (!reviewRunId) {
|
|
194
|
+
return blocked(['execute stage review runId 无法定位(marker 缺失且 stage-reviews/ 无含 review 的 run 目录)'])
|
|
195
|
+
}
|
|
196
|
+
const searchDirs = [effectiveSpecBase, reviewChangeDir, cwd].filter(Boolean)
|
|
197
|
+
const reviewResult = validateStageReview({ stage: 'execute', reviewType: 'acceptance', runtimeRoot, reviewRunId, searchDirs })
|
|
198
|
+
printStageReviewResult(reviewResult, { stage: 'execute', reviewRunId, runtimeRoot })
|
|
199
|
+
if (!reviewResult.ok) return true // printStageReviewResult 已给明细,不再重复列
|
|
200
|
+
}
|
|
201
|
+
} catch (e) {
|
|
202
|
+
// fail-closed:门自身异常阻断 align(与正常 gate 一致),不静默放行
|
|
203
|
+
console.error('❌ align 前置 Stage Review 校验异常,阻断对齐: ' + e.message)
|
|
204
|
+
return true
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 2. Task Review(runId 只读解析:marker → 含 tasks/ 扫描;不 generate)
|
|
208
|
+
try {
|
|
209
|
+
const { validateTaskReviews, printReviewResult, resolveLatestExecuteRunIdWithTasks, isValidExecuteRunId } = await import('../task-review.js')
|
|
210
|
+
const planPath = reviewChangeDir ? join(reviewChangeDir, 'plan.md') : null
|
|
211
|
+
if (!planPath || !existsSync(planPath)) {
|
|
212
|
+
return blocked(['plan.md 不存在,Task Review 无从校验(align 的 checkbox 判定依赖同目录,疑似路径解析异常)'])
|
|
213
|
+
}
|
|
214
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
215
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
216
|
+
let executeRunId = ''
|
|
217
|
+
try {
|
|
218
|
+
if (existsSync(runIdFile)) {
|
|
219
|
+
const c = readFileSync(runIdFile, 'utf8').trim()
|
|
220
|
+
if (c && isValidExecuteRunId(c)) executeRunId = c
|
|
221
|
+
}
|
|
222
|
+
} catch {}
|
|
223
|
+
if (!executeRunId) {
|
|
224
|
+
executeRunId = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName }) || ''
|
|
225
|
+
}
|
|
226
|
+
if (!executeRunId) {
|
|
227
|
+
return blocked(['execute runId 无法定位(marker 缺失且 execute-runs/ 无含 review 的 run 目录)——15/N 份 task review 所在 run 不可寻,无法对账'])
|
|
228
|
+
}
|
|
229
|
+
const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId, changeDir: reviewChangeDir, gitDir: cwd })
|
|
230
|
+
printReviewResult(reviewResult, { runtimeRoot, executeRunId })
|
|
231
|
+
if (!reviewResult.ok) return true // 报错明细已在 printReviewResult 输出
|
|
232
|
+
console.log('✅ align 前置 review 校验通过(stage review + task review 均齐备),继续对齐')
|
|
233
|
+
} catch (e) {
|
|
234
|
+
console.error('❌ align 前置 Task Review 校验异常,阻断对齐: ' + e.message)
|
|
235
|
+
return true
|
|
236
|
+
}
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
|
|
121
240
|
/**
|
|
122
241
|
* execute deps 验证硬门(change 2026-06-28-worktree-deps-provision / D-001@v1, D-003@v1, D-006@v2)。
|
|
123
242
|
* depsStatus 不达标且非 wave 级 opt-out 时阻断 --done:置 step=blocked + exit(1),与 requiresWait 同范式。
|
|
@@ -178,19 +297,34 @@ export async function enforceReviewJsonGate(stageName, cwd, changeName, step, st
|
|
|
178
297
|
const planPath = join(specBase, 'changes', changeName, 'plan.md')
|
|
179
298
|
if (!existsSync(runIdFile) || !existsSync(planPath)) return true
|
|
180
299
|
const { validateCheckedTaskReviews, resolveLatestExecuteRunIdWithTasks, isValidExecuteRunId } = await import('../task-review.js')
|
|
181
|
-
|
|
300
|
+
// existsSync 与 read 之间 marker 可能被并发 cleanup/归档删除(多 agent 场景):读失败按
|
|
301
|
+
//「marker 缺失」处理,走漂移兜底重定位,不让 ENOENT 冒成顶层 stack(与 :426-435 口径对齐)
|
|
302
|
+
let executeRunId = ''
|
|
303
|
+
try {
|
|
304
|
+
executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
305
|
+
} catch {
|
|
306
|
+
console.warn('⚠️ execute run marker 读取失败(可能被并发清理),改扫真实 run 目录')
|
|
307
|
+
executeRunId = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName }) || ''
|
|
308
|
+
}
|
|
182
309
|
// marker 是 agent 可写内容:格式校验防注入/穿越,非法视为缺失走漂移兜底重定位
|
|
183
310
|
if (executeRunId && !isValidExecuteRunId(executeRunId)) {
|
|
184
311
|
console.warn(`⚠️ execute run marker 内容非法(期望 exec-YYYY-MM-DD-HHMMSS,实得 ${JSON.stringify(executeRunId.slice(0, 60))}),改扫真实 run 目录`)
|
|
185
312
|
executeRunId = ''
|
|
186
313
|
}
|
|
187
|
-
|
|
314
|
+
// 2026-08-20-task-truth-unify:已勾 task 清单源迁 tasks.md(勾选唯一落点);tasks.md 缺失
|
|
315
|
+
// 回退 planPath 内容(旧变更兼容读侧)。planPath 存在性已在上方把关。
|
|
316
|
+
const tasksRegistryPath = join(specBase, 'changes', changeName, 'tasks.md')
|
|
317
|
+
const planContent = existsSync(tasksRegistryPath)
|
|
318
|
+
? readFileSync(tasksRegistryPath, 'utf8')
|
|
319
|
+
: readFileSync(planPath, 'utf8')
|
|
320
|
+
// marker 读失败且重定位无果(无任何含 tasks/ 的 run):无 run 可校验,放行(下游 Task Review Gate 兜底)
|
|
321
|
+
if (!executeRunId) return true
|
|
188
322
|
// marker 漂移兜底(gate-atom-a 正确修法):marker 指向的 run 缺 tasks/(generateExecuteRunId 只写
|
|
189
323
|
// marker 不建目录,漂移后新 run 不继承旧 review)时,无视 marker 改扫 execute-runs/ 取 mtime 最新
|
|
190
324
|
// 且真正含 tasks/ 的 run,用其齐备的 review.json 校验,避免误报「review.json 不存在」。注意不能用
|
|
191
325
|
// resolveLatestExecuteRunId——它见 marker 非空即原样返回(不校验目录),恰是本场景要绕开的值。
|
|
192
326
|
if (executeRunId && !existsSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'))) {
|
|
193
|
-
const relocated = resolveLatestExecuteRunIdWithTasks({ runtimeRoot })
|
|
327
|
+
const relocated = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName })
|
|
194
328
|
if (relocated && relocated !== executeRunId) {
|
|
195
329
|
console.warn(`⚠️ execute run marker 漂移:${executeRunId} 无 tasks/,改用真实含 review 的 run ${relocated}`)
|
|
196
330
|
executeRunId = relocated
|
|
@@ -331,18 +465,21 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
331
465
|
console.log('\n✅ 验证通过,下一步:sillyspec run archive')
|
|
332
466
|
}
|
|
333
467
|
|
|
334
|
-
// ── Plan postcheck contract:plan.md
|
|
468
|
+
// ── Plan postcheck contract:tasks.md(注册表)× plan.md(Wave 引用)须满足 execute 契约 ──
|
|
335
469
|
if (stageName === 'plan') {
|
|
336
470
|
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
337
471
|
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
338
472
|
if (planPath && existsSync(planPath)) {
|
|
339
473
|
const { validatePlanForExecute } = await import('../stages/execute.js')
|
|
340
|
-
|
|
341
|
-
const
|
|
474
|
+
// CRLF 归一:Windows 编辑器/python 写文件可产生 CRLF,归一后交校验(与 plan-postcheck.js 同口径)
|
|
475
|
+
const planContent = readFileSync(planPath, 'utf8').replace(/\r\n/g, '\n')
|
|
476
|
+
const tasksPath = join(planFile, 'tasks.md')
|
|
477
|
+
const tasksContent = existsSync(tasksPath) ? readFileSync(tasksPath, 'utf8').replace(/\r\n/g, '\n') : ''
|
|
478
|
+
const planValidation = validatePlanForExecute(tasksContent, planContent)
|
|
342
479
|
if (!planValidation.ok) {
|
|
343
480
|
console.error(`\n❌ Plan → Execute Contract 校验失败:`)
|
|
344
481
|
for (const err of planValidation.errors) console.error(` - ${err}`)
|
|
345
|
-
console.error(`\n plan.md 不满足 execute 契约,请修复后重新完成此步骤。`)
|
|
482
|
+
console.error(`\n tasks.md/plan.md 不满足 execute 契约,请修复后重新完成此步骤。`)
|
|
346
483
|
// 阻断 completed
|
|
347
484
|
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
348
485
|
}
|
|
@@ -415,7 +552,12 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
415
552
|
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
416
553
|
|
|
417
554
|
if (planPath && existsSync(planPath)) {
|
|
418
|
-
|
|
555
|
+
// 2026-08-20-task-truth-unify:任务清单源迁 tasks.md(validateTaskReviews 内按
|
|
556
|
+
// checkbox 行解析,tasks.md 行格式兼容原正则);缺失回退 plan.md(旧变更兼容读侧)
|
|
557
|
+
const tasksRegistryPath = join(planFile, 'tasks.md')
|
|
558
|
+
const planContent = existsSync(tasksRegistryPath)
|
|
559
|
+
? readFileSync(tasksRegistryPath, 'utf8')
|
|
560
|
+
: readFileSync(planPath, 'utf8')
|
|
419
561
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, effectiveSpecBase)
|
|
420
562
|
|
|
421
563
|
// execute run id:从变更专属标记文件读取(agent 可写内容,格式校验防注入/穿越)
|
|
@@ -436,7 +578,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
436
578
|
// marker 缺失:先扫描 execute-runs/ 既有目录找回真实 runId(与 getLatestStageReviewRunId
|
|
437
579
|
// 目录扫描兜底同语义),避免 marker 丢失而 agent 已用旧 runId 落盘时,直接 generate 新 ID
|
|
438
580
|
// 找不到旧 review、误判缺 review.json。仅当确实无既有 run 才 generate 新 ID 并落盘。
|
|
439
|
-
const { generateExecuteRunId, resolveLatestExecuteRunId } = await import('../task-review.js')
|
|
581
|
+
const { generateExecuteRunId, resolveLatestExecuteRunId, stampExecuteRunChange } = await import('../task-review.js')
|
|
440
582
|
executeRunId = resolveLatestExecuteRunId({ runtimeRoot, changeName }) || ''
|
|
441
583
|
if (!executeRunId) {
|
|
442
584
|
executeRunId = generateExecuteRunId()
|
|
@@ -446,6 +588,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
446
588
|
//(gate 自身写 run 目录失败不能静默放行完成)。
|
|
447
589
|
mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
|
|
448
590
|
writeFileSync(runIdFile, executeRunId + '\n')
|
|
591
|
+
stampExecuteRunChange(runtimeRoot, executeRunId, changeName)
|
|
449
592
|
}
|
|
450
593
|
}
|
|
451
594
|
|
|
@@ -480,7 +623,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
480
623
|
// 检查是否存在 checkbox 已勾但 review 不通过的情况
|
|
481
624
|
const uncheckedTasks = reviewResult.errors.filter(e => e.includes('缺少 review.json'))
|
|
482
625
|
if (uncheckedTasks.length > 0) {
|
|
483
|
-
console.error('\n⚠️ 部分任务已在
|
|
626
|
+
console.error('\n⚠️ 部分任务已在 tasks.md 中勾选,但 review.json 不存在。')
|
|
484
627
|
console.error(` 请取消勾选这些任务的 checkbox,或补充对应的 review.json(execute run ID: ${executeRunId})。`)
|
|
485
628
|
}
|
|
486
629
|
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
@@ -731,6 +874,10 @@ export async function completeStageGates({ stageName, cwd, changeName, platformO
|
|
|
731
874
|
stageData.steps = freshSteps
|
|
732
875
|
stageData.status = 'pending'
|
|
733
876
|
stageData.completedAt = null
|
|
877
|
+
// scan 的 quick 档 profile 随步骤表一并失效(坑 scan-quick-profile-step-mismatch 收尾):
|
|
878
|
+
// getStageSteps 感知 scanProfile 返回 3 步表,重置后 DB 是 11 步注册表,残留 profile 会让
|
|
879
|
+
// 下次 --status 走 ensureStageSteps 时 11 vs 3 误报漂移重播种。下次 run scan 会重算 profile。
|
|
880
|
+
if (stageName === 'scan' && stageData.scanProfile) delete stageData.scanProfile
|
|
734
881
|
if (progress.currentStage === stageName) progress.currentStage = ''
|
|
735
882
|
pm._write(cwd, progress, changeName)
|
|
736
883
|
}
|
package/src/run/prompt.js
CHANGED
|
@@ -183,8 +183,11 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
183
183
|
console.log(`project: ${projectName}`)
|
|
184
184
|
if (changeName) {
|
|
185
185
|
console.log(`change: ${changeName}`)
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
// changeDir 根(坑 change-dir-base-mismatch):平台/漂移锚定用绝对根(specDriftAnchor 防
|
|
187
|
+
// worktree 副本错位),普通本地保持相对 '.sillyspec' 展示契约(test 钉死)。此前
|
|
188
|
+
// isPlatform 用 specRoot||runtimeRoot 判定、取值只拿 specRoot——runtimeRoot-only 平台组合
|
|
189
|
+
// join(null,...) 直接 TypeError。
|
|
190
|
+
const changeDirBase = platformOpts?.specRoot || platformOpts?.specDriftAnchor || '.sillyspec'
|
|
188
191
|
const changeDir = join(changeDirBase, 'changes', changeName)
|
|
189
192
|
console.log(`changeDir: ${changeDir}`)
|
|
190
193
|
}
|
|
@@ -257,7 +260,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
257
260
|
if (existsSync(sessionGuardFile)) {
|
|
258
261
|
qlIdVal = JSON.parse(readFileSync(sessionGuardFile, 'utf8')).quicklogId || ''
|
|
259
262
|
}
|
|
260
|
-
} catch {
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.warn(`⚠️ quicklog-id 占位符读取 guard.json 失败: ${err && err.message ? err.message : err}`);
|
|
265
|
+
}
|
|
261
266
|
promptText = promptText.replace(/<quicklog-id>/g, qlIdVal || '(未分配)')
|
|
262
267
|
}
|
|
263
268
|
// 替换 <linked-changes> 占位符(quick 阶段:从 .runtime/quick-sessions/<sessionId>/guard.json 读关联变更)
|
|
@@ -414,14 +419,16 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
414
419
|
const { matchKnowledge } = await import('../knowledge-match.js')
|
|
415
420
|
const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
416
421
|
const knowledgeDir = join(effectiveSpecBase, 'knowledge')
|
|
417
|
-
// taskContext: changeName +
|
|
422
|
+
// taskContext: changeName + tasks.md 任务名(注册表唯一真相,2026-08-20-task-truth-unify)
|
|
418
423
|
let taskContext = changeName || ''
|
|
419
424
|
if (changeName) {
|
|
425
|
+
const tasksMdPath = join(effectiveSpecBase, 'changes', changeName, 'tasks.md')
|
|
420
426
|
const planPath = join(effectiveSpecBase, 'changes', changeName, 'plan.md')
|
|
427
|
+
const srcPath = existsSync(tasksMdPath) ? tasksMdPath : planPath
|
|
421
428
|
try {
|
|
422
|
-
const
|
|
423
|
-
// match
|
|
424
|
-
const taskLines = [...
|
|
429
|
+
const registryContent = readFileSync(srcPath, 'utf8')
|
|
430
|
+
// match "- [ ] task-01: title" / "## task-01: title"(兼容旧 plan.md 内联两种形态)
|
|
431
|
+
const taskLines = [...registryContent.matchAll(/(?:^\- \[[ x]\] |^## )task-\d+[^:]*:?\s*(.+)/gm)]
|
|
425
432
|
if (taskLines.length > 0) {
|
|
426
433
|
taskContext += ' ' + taskLines.map(t => t[1]).join(' ')
|
|
427
434
|
}
|
|
@@ -711,8 +718,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
711
718
|
console.log('- **平台模式:local.yaml 中的 commands 必须在 package.json scripts 中真实存在,不存在的标记 unavailable。**')
|
|
712
719
|
}
|
|
713
720
|
if (changeName) {
|
|
714
|
-
|
|
715
|
-
|
|
721
|
+
// changeDir 根与上方头部同源(坑 change-dir-base-mismatch):平台/漂移锚定绝对根,
|
|
722
|
+
// 普通本地保持相对 '.sillyspec' 展示;runtimeRoot-only 组合不再 join(null) 崩溃
|
|
723
|
+
const changeDirBase = platformOpts?.specRoot || platformOpts?.specDriftAnchor || '.sillyspec'
|
|
716
724
|
const changeDir = join(changeDirBase, 'changes', changeName)
|
|
717
725
|
console.log(`- **文件路径规则:所有变更文件必须写入 \`${changeDir}/\` 目录下。不要自己拼接路径,直接使用 changeDir 值。示例:\`${changeDir}/proposal.md\`**`)
|
|
718
726
|
}
|
package/src/run/scan-profile.js
CHANGED
|
@@ -105,21 +105,15 @@ export function computeScanProfile(cwd, platformOpts) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
109
|
-
* quick
|
|
110
|
-
*
|
|
108
|
+
* quick 档 3 步表构建(唯一真相):applyScanProfileSteps(首进程裁剪)与 getStageSteps
|
|
109
|
+
* (后续进程重取定义)共用。此前 quick 步骤表只在首进程 applyScanProfileSteps 里内联构建,
|
|
110
|
+
* getStageSteps('scan') 恒返回 11 步注册表 → 同进程 outputStep 用注册表 defSteps[1] 渲染错位
|
|
111
|
+
* prompt;跨进程 ensureStageSteps 见 3 vs 11 触发长度漂移重播种,quick 已完成步丢失、
|
|
112
|
+
* agent 从未做过的注册表步骤被 --done(坑 scan-quick-profile-step-mismatch)。
|
|
111
113
|
*/
|
|
112
|
-
export function
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (mode === 'quick') {
|
|
117
|
-
const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
118
|
-
const projectName = basename(cwd)
|
|
119
|
-
const docsRoot = join(specBase, 'docs', projectName)
|
|
120
|
-
|
|
121
|
-
// Step 1: CLI preflight(不调 AI,自动完成)
|
|
122
|
-
const step1 = {
|
|
114
|
+
export function buildQuickScanSteps() {
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
123
117
|
name: '项目概览(自动探测)',
|
|
124
118
|
status: 'pending',
|
|
125
119
|
noAI: true,
|
|
@@ -127,9 +121,8 @@ export function applyScanProfileSteps(stageData, profile, cwd, platformOpts) {
|
|
|
127
121
|
prompt: '',
|
|
128
122
|
outputHint: 'preflight 结果',
|
|
129
123
|
optional: false
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const step2 = {
|
|
124
|
+
},
|
|
125
|
+
{
|
|
133
126
|
name: '生成核心文档',
|
|
134
127
|
status: 'pending',
|
|
135
128
|
prompt: `## Quick Scan — 核心文档生成
|
|
@@ -156,13 +149,32 @@ export function applyScanProfileSteps(stageData, profile, cwd, platformOpts) {
|
|
|
156
149
|
生成的文件列表`,
|
|
157
150
|
outputHint: '文件列表',
|
|
158
151
|
optional: false
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
// 名字与注册表末步一致(按名匹配/迁移语义);prompt 清空、noAI postcheck 自动执行
|
|
155
|
+
name: '自检和提交',
|
|
156
|
+
status: 'pending',
|
|
157
|
+
prompt: '',
|
|
158
|
+
outputHint: '结果',
|
|
159
|
+
optional: false,
|
|
160
|
+
noAI: true,
|
|
161
|
+
_cliAction: 'scanPostcheck'
|
|
159
162
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 根据 scanProfile 裁剪步骤
|
|
168
|
+
* quick: 3 步 — CLI preflight / AI generate / CLI postcheck
|
|
169
|
+
* standard: 跳过续扫检测(4), 跳过可选步骤(9)
|
|
170
|
+
*/
|
|
171
|
+
export function applyScanProfileSteps(stageData, profile, cwd, platformOpts) {
|
|
172
|
+
const steps = stageData.steps
|
|
173
|
+
const mode = profile.mode
|
|
174
|
+
|
|
175
|
+
if (mode === 'quick') {
|
|
176
|
+
// 3 步表构建抽至 buildQuickScanSteps(getStageSteps 同源复用,防跨进程 3 vs 11 漂移)
|
|
177
|
+
stageData.steps = buildQuickScanSteps()
|
|
166
178
|
return
|
|
167
179
|
}
|
|
168
180
|
|