sillyspec 3.18.1 → 3.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +24 -23
- package/.claude/skills/sillyspec-execute/SKILL.md +8 -1
- package/docs/brainstorm-plan-contract.md +64 -0
- package/docs/plan-execute-contract.md +123 -0
- package/docs/revision-mode.md +115 -0
- package/docs/sillyspec/file-lifecycle.md +13 -4
- package/docs/workflow-contract-regression.md +106 -0
- package/package.json +1 -1
- package/packages/dashboard/dist/assets/{index-DpLHK4jv.js → index-Bq_Z2hne.js} +568 -568
- package/packages/dashboard/dist/assets/{index-BcM2J-hv.css → index-O2W5RV4z.css} +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/src/components/PipelineStage.vue +22 -2
- package/packages/dashboard/src/components/PipelineView.vue +10 -2
- package/packages/dashboard/src/components/StageBadge.vue +17 -3
- package/packages/dashboard/src/components/StepCard.vue +7 -2
- package/src/change-risk-profile.js +167 -0
- package/src/db.js +6 -0
- package/src/index.js +17 -1
- package/src/knowledge-match.js +130 -0
- package/src/progress.js +464 -11
- package/src/run.js +269 -29
- package/src/scan-postcheck.js +34 -2
- package/src/stage-contract.js +90 -5
- package/src/stages/brainstorm.js +23 -0
- package/src/stages/execute.js +122 -16
- package/src/stages/plan.js +82 -0
- package/src/stages/scan.js +40 -0
- package/src/stages/verify.js +38 -2
- package/test/brainstorm-plan-contract.test.mjs +273 -0
- package/test/knowledge-match.test.mjs +231 -0
- package/test/plan-execute-contract.test.mjs +330 -0
- package/test/platform-failure-samples.test.mjs +4 -0
- package/test/revision-v1.test.mjs +1145 -0
- package/test/scan-knowledge.test.mjs +175 -0
- package/test/scan-postcheck.test.mjs +3 -0
- package/test/spec-dir.test.mjs +8 -3
- package/test/stage-definitions.test.mjs +1 -1
package/src/run.js
CHANGED
|
@@ -547,6 +547,18 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
547
547
|
const total = steps.length
|
|
548
548
|
const projectName = dbProjectName || basename(cwd)
|
|
549
549
|
|
|
550
|
+
// ── Revision context injection ──
|
|
551
|
+
const revisionCtx = platformOpts?._revision
|
|
552
|
+
if (revisionCtx) {
|
|
553
|
+
console.log(`### 🔄 Revision Context`)
|
|
554
|
+
console.log(`本阶段处于修订模式(revision ${revisionCtx.revision}),不是首次执行。`)
|
|
555
|
+
console.log(`- 修订起始步骤:${revisionCtx.fromStep}`)
|
|
556
|
+
console.log(`- 当前步骤之前已完成的步骤仍然有效,不需要重做。`)
|
|
557
|
+
console.log(`- 当前步骤及之后的步骤需要重新生成或调整已有产物。`)
|
|
558
|
+
console.log(`- 已有产物文件(design.md、plan.md 等)被保留,审视并更新它们,而不是从零创建。`)
|
|
559
|
+
console.log(`- 不要绕过 CLI 进度追踪。\n`)
|
|
560
|
+
}
|
|
561
|
+
|
|
550
562
|
const personas = {
|
|
551
563
|
brainstorm: `### 🎯 你的角色:资深架构师
|
|
552
564
|
你是一位有 15 年经验的系统架构师。先理解业务本质,再设计技术方案。决策附理由,方案列 trade-off。不确定就说不确定,不猜。`,
|
|
@@ -708,6 +720,36 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
708
720
|
promptText = promptText.replace(/\{SPEC_ROOT\}/g, specSillyspec)
|
|
709
721
|
}
|
|
710
722
|
|
|
723
|
+
// Knowledge hit report: execute 阶段注入匹配结果
|
|
724
|
+
if (stageName === 'execute' && promptText.includes('{KNOWLEDGE_HIT_REPORT}')) {
|
|
725
|
+
try {
|
|
726
|
+
const { matchKnowledge } = await import('./knowledge-match.js')
|
|
727
|
+
const effectiveSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
728
|
+
const knowledgeDir = join(effectiveSpecBase, 'knowledge')
|
|
729
|
+
// taskContext: changeName + plan.md task names for better matching
|
|
730
|
+
let taskContext = changeName || ''
|
|
731
|
+
if (changeName) {
|
|
732
|
+
const planPath = join(effectiveSpecBase, 'changes', changeName, 'plan.md')
|
|
733
|
+
try {
|
|
734
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
735
|
+
// match both "- [ ] task-01: title" and "## task-01: title" formats
|
|
736
|
+
const taskLines = [...planContent.matchAll(/(?:^\- \[[ x]\] |^## )task-\d+[^:]*:?\s*(.+)/gm)]
|
|
737
|
+
if (taskLines.length > 0) {
|
|
738
|
+
taskContext += ' ' + taskLines.map(t => t[1]).join(' ')
|
|
739
|
+
}
|
|
740
|
+
} catch {}
|
|
741
|
+
}
|
|
742
|
+
const knowledgeResult = matchKnowledge(knowledgeDir, taskContext)
|
|
743
|
+
promptText = promptText.replace(/\{KNOWLEDGE_HIT_REPORT\}/g, knowledgeResult.report)
|
|
744
|
+
// 写入 runtime JSON
|
|
745
|
+
const runtimeDir = join(effectiveSpecBase, '.runtime')
|
|
746
|
+
mkdirSync(runtimeDir, { recursive: true })
|
|
747
|
+
writeFileSync(join(runtimeDir, 'knowledge-hit-report.json'), JSON.stringify(knowledgeResult.json, null, 2) + '\n')
|
|
748
|
+
} catch (e) {
|
|
749
|
+
promptText = promptText.replace(/\{KNOWLEDGE_HIT_REPORT\}/g, 'Status: no matches (error: ' + e.message + ')')
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
711
753
|
// 注入模块上下文(brainstorm/plan/execute 阶段,基于 Module Context Index)
|
|
712
754
|
if (['brainstorm', 'plan', 'execute'].includes(stageName) && projectName) {
|
|
713
755
|
const effectiveSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
@@ -1016,6 +1058,8 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1016
1058
|
const isSkip = flags.includes('--skip')
|
|
1017
1059
|
const isStatus = flags.includes('--status')
|
|
1018
1060
|
const isReset = flags.includes('--reset')
|
|
1061
|
+
const isReopen = flags.includes('--reopen')
|
|
1062
|
+
const fromStepValue = getFlagValue('--from-step')
|
|
1019
1063
|
const isConfirm = flags.includes('--confirm')
|
|
1020
1064
|
const isSkipApproval = flags.includes('--skip-approval')
|
|
1021
1065
|
const isWait = flags.includes('--wait')
|
|
@@ -1153,6 +1197,7 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1153
1197
|
'--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
|
|
1154
1198
|
'--files', '--allow-new', '--force-baseline', '--force-rescan',
|
|
1155
1199
|
'--json', '--dir', '--help',
|
|
1200
|
+
'--reopen', '--from-step',
|
|
1156
1201
|
])
|
|
1157
1202
|
for (let i = 0; i < flags.length; i++) {
|
|
1158
1203
|
const f = flags[i]
|
|
@@ -1223,6 +1268,79 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1223
1268
|
return await resetStage(pm, progress, stageName, cwd, effectiveChange, platformOpts)
|
|
1224
1269
|
}
|
|
1225
1270
|
|
|
1271
|
+
// ── 规则 1:completed 阶段直接 run,拒绝(但 --status 放行)──
|
|
1272
|
+
const stageStatus = progress.stages[stageName]?.status
|
|
1273
|
+
if (stageStatus === 'completed' && !isReopen && !isStatus) {
|
|
1274
|
+
console.error(`\n❌ ${stageName} 阶段已完成。`)
|
|
1275
|
+
console.error(` 使用 --reopen 进行修订,或 --reset 从头开始。`)
|
|
1276
|
+
console.error(` 修订示例: sillyspec run ${stageName} --reopen --from-step <步骤序号或名称>`)
|
|
1277
|
+
process.exit(1)
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// ── 规则 5:stale 阶段直接 run,拒绝(但 --status / --reset 放行)──
|
|
1281
|
+
if (stageStatus === 'stale' && !isReopen && !isStatus && !isReset) {
|
|
1282
|
+
const staleReason = progress.stages[stageName]?.staleReason || '上游阶段已修订'
|
|
1283
|
+
console.error(`\n⚠️ ${stageName} 阶段已失效(stale)。`)
|
|
1284
|
+
console.error(` 原因:${staleReason}`)
|
|
1285
|
+
console.error(` 使用 --reopen --from-step <步骤> 进行修订,或 --reset 从头开始。`)
|
|
1286
|
+
process.exit(1)
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// ── --reopen 处理 ──
|
|
1290
|
+
if (isReopen) {
|
|
1291
|
+
// stale/revising 阶段可能 steps 为空,或者 execute 阶段的 steps 需要从最新 plan.md 刷新
|
|
1292
|
+
const stageDataPre = progress.stages[stageName]
|
|
1293
|
+
const needsInit = !stageDataPre || !stageDataPre.steps || stageDataPre.steps.length === 0
|
|
1294
|
+
// execute 阶段在 reopen 时需要从最新 plan.md 重新解析 steps(plan 可能已变更)
|
|
1295
|
+
if (needsInit || stageName === 'execute') {
|
|
1296
|
+
const freshSteps = await getStageSteps(stageName, cwd, progress, specRoot)
|
|
1297
|
+
if (freshSteps && freshSteps.length > 0) {
|
|
1298
|
+
if (!progress.stages[stageName]) progress.stages[stageName] = { status: 'stale', steps: [] }
|
|
1299
|
+
progress.stages[stageName].steps = freshSteps.map(s => ({ name: s.name, status: 'pending' }))
|
|
1300
|
+
await pm._write(cwd, progress, effectiveChange)
|
|
1301
|
+
progress = await pm.read(cwd, effectiveChange) || progress
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const result = await pm.reopenStage(cwd, stageName, {
|
|
1306
|
+
fromStep: fromStepValue,
|
|
1307
|
+
changeName: effectiveChange,
|
|
1308
|
+
})
|
|
1309
|
+
if (!result.ok) {
|
|
1310
|
+
console.error(`\n❌ ${result.error}`)
|
|
1311
|
+
if (stageStatus === 'completed') {
|
|
1312
|
+
console.error(`\n 提示:sillyspec run ${stageName} --reopen --from-step <步骤序号或名称>`)
|
|
1313
|
+
}
|
|
1314
|
+
process.exit(1)
|
|
1315
|
+
}
|
|
1316
|
+
console.log(`\n🔧 ${stageName} 阶段已重新打开(revision ${result.revision})`)
|
|
1317
|
+
console.log(` 从步骤「${result.fromStep}」开始修订`)
|
|
1318
|
+
console.log(` 该步骤及之后的产出需要重新生成。`)
|
|
1319
|
+
if (stageName === 'execute') console.log(` ⚡ execute 步骤已从最新 plan.md 重新解析。`)
|
|
1320
|
+
console.log('')
|
|
1321
|
+
|
|
1322
|
+
// 重新读取 progress
|
|
1323
|
+
progress = await pm.read(cwd, effectiveChange) || progress
|
|
1324
|
+
|
|
1325
|
+
// 注入 revision context 到 platformOpts,供 outputStep 使用
|
|
1326
|
+
const stageData = progress.stages[stageName]
|
|
1327
|
+
if (stageData && stageData.revision > 0) {
|
|
1328
|
+
platformOpts._revision = {
|
|
1329
|
+
revision: stageData.revision,
|
|
1330
|
+
fromStep: stageData.reopenedFromStep,
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
} else {
|
|
1334
|
+
// 非 reopen 的正常执行:如果阶段处于 revising 状态,也注入 revision context
|
|
1335
|
+
const revStageData = progress.stages[stageName]
|
|
1336
|
+
if (revStageData && revStageData.status === 'revising' && revStageData.revision > 0 && !platformOpts._revision) {
|
|
1337
|
+
platformOpts._revision = {
|
|
1338
|
+
revision: revStageData.revision,
|
|
1339
|
+
fromStep: revStageData.reopenedFromStep,
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1226
1344
|
// 确保步骤已初始化
|
|
1227
1345
|
const changed = await ensureStageSteps(progress, stageName, cwd, specRoot)
|
|
1228
1346
|
if (changed && effectiveChange) {
|
|
@@ -1253,7 +1371,8 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1253
1371
|
|
|
1254
1372
|
// --done
|
|
1255
1373
|
if (isDone) {
|
|
1256
|
-
|
|
1374
|
+
const doneAnswer = getFlagValue('--answer')
|
|
1375
|
+
return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, confirmMode, doneAnswer })
|
|
1257
1376
|
}
|
|
1258
1377
|
|
|
1259
1378
|
// 默认:输出当前步骤
|
|
@@ -1301,6 +1420,26 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
1301
1420
|
}
|
|
1302
1421
|
}
|
|
1303
1422
|
|
|
1423
|
+
// execute 阶段:CLI 自动创建 worktree(不等 AI agent)
|
|
1424
|
+
if (stageName === 'execute' && changeName) {
|
|
1425
|
+
const effectiveChange = changeName
|
|
1426
|
+
const { WorktreeManager } = await import('./worktree.js')
|
|
1427
|
+
const wm = new WorktreeManager({ cwd })
|
|
1428
|
+
const existingMeta = wm.getMeta(effectiveChange)
|
|
1429
|
+
if (existingMeta) {
|
|
1430
|
+
console.log(`🔗 worktree 已存在: ${existingMeta.worktreePath} (${existingMeta.mode})`)
|
|
1431
|
+
} else {
|
|
1432
|
+
try {
|
|
1433
|
+
const result = wm.create(effectiveChange)
|
|
1434
|
+
console.log(`🔗 worktree 已创建: ${result.worktreePath} (分支: ${result.branch}, 模式: ${result.mode})`)
|
|
1435
|
+
} catch (e) {
|
|
1436
|
+
console.error(`❌ worktree 创建失败: ${e.message}`)
|
|
1437
|
+
console.error(` 继续执行前请解决上述问题,或使用 --no-worktree 跳过。`)
|
|
1438
|
+
process.exit(1)
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1304
1443
|
// 自动探测 currentChange
|
|
1305
1444
|
if (autoDetectChange(progress, cwd)) {
|
|
1306
1445
|
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
@@ -1337,6 +1476,13 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
1337
1476
|
|
|
1338
1477
|
let currentIdx = steps.findIndex(s => s.status !== 'completed' && s.status !== 'skipped')
|
|
1339
1478
|
|
|
1479
|
+
// stale 步骤视为可执行(等同于 pending)
|
|
1480
|
+
if (currentIdx !== -1 && steps[currentIdx].status === 'stale') {
|
|
1481
|
+
steps[currentIdx].status = 'pending'
|
|
1482
|
+
await pm._write(cwd, progress, changeName)
|
|
1483
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1340
1486
|
// ── scanProfile: 根据 project 规模动态裁剪步骤 ──
|
|
1341
1487
|
let scanProfile = null
|
|
1342
1488
|
if (stageName === 'scan' && steps.length > 0 && currentIdx === 0) {
|
|
@@ -1432,6 +1578,29 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
1432
1578
|
console.log(` 继续执行将从中断处恢复,用 --reset 可重新开始。\n`)
|
|
1433
1579
|
}
|
|
1434
1580
|
|
|
1581
|
+
// ── Brainstorm → Plan Contract:plan 启动前校验 design.md ──
|
|
1582
|
+
if (stageName === 'plan' && currentIdx === 0) {
|
|
1583
|
+
const changeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot || null)
|
|
1584
|
+
const designPath = changeDir ? join(changeDir, 'design.md') : null
|
|
1585
|
+
if (designPath && existsSync(designPath)) {
|
|
1586
|
+
const { validateDesignForPlan } = await import('./stages/plan.js')
|
|
1587
|
+
const designContent = readFileSync(designPath, 'utf8')
|
|
1588
|
+
const designValidation = validateDesignForPlan(designContent)
|
|
1589
|
+
if (!designValidation.ok) {
|
|
1590
|
+
console.error(`\n❌ Brainstorm → Plan Contract 校验失败:`)
|
|
1591
|
+
for (const err of designValidation.errors) console.error(` - ${err}`)
|
|
1592
|
+
console.error(`\n design.md 不满足 plan 契约,请先修复后重试。`)
|
|
1593
|
+
console.error(` 提示:sillyspec run brainstorm --reopen --from-step <步骤> 修订设计文档`)
|
|
1594
|
+
process.exit(1)
|
|
1595
|
+
}
|
|
1596
|
+
if (designValidation.warnings.length > 0) {
|
|
1597
|
+
console.log(`⚠️ Design contract 警告(不阻断):`)
|
|
1598
|
+
for (const w of designValidation.warnings) console.log(` - ${w}`)
|
|
1599
|
+
console.log()
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1435
1604
|
const defSteps = await getStageSteps(stageName, cwd, progress)
|
|
1436
1605
|
if (defSteps && defSteps[currentIdx]) {
|
|
1437
1606
|
// noAI 步骤自动完成(CLI-only,不需要 Agent 参与)
|
|
@@ -1832,14 +2001,24 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1832
2001
|
const currentStepDef = defStepsForCurrent?.[currentIdx] || {}
|
|
1833
2002
|
const currentStep = steps[currentIdx]
|
|
1834
2003
|
if (currentStepDef.requiresWait === true && !currentStep.waitAnswer) {
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
if (
|
|
1838
|
-
|
|
2004
|
+
// 检查 --done 是否带了 --answer:如果是,自动补全 waitAnswer 状态,一步完成
|
|
2005
|
+
const doneAnswer = typeof options !== 'undefined' && options.doneAnswer ? options.doneAnswer : null
|
|
2006
|
+
if (doneAnswer) {
|
|
2007
|
+
currentStep.status = 'waiting'
|
|
2008
|
+
currentStep.waitAnswer = doneAnswer
|
|
2009
|
+
currentStep.waitReason = currentStepDef.waitReason || '等待用户输入'
|
|
2010
|
+
console.log(`⚠️ Step "${currentStep.name}" 需要 wait,但 --done 带了 --answer,自动补全 wait 状态。`)
|
|
2011
|
+
} else {
|
|
2012
|
+
console.error(`❌ Step "${currentStep.name}" 必须先等待用户输入,不能直接 --done。`)
|
|
2013
|
+
console.error(` 原因:${currentStepDef.waitReason || '该步骤需要人工确认/回答'}`)
|
|
2014
|
+
if (currentStepDef.waitOptions) {
|
|
2015
|
+
console.error(` 选项:${currentStepDef.waitOptions.join(', ')}`)
|
|
2016
|
+
}
|
|
2017
|
+
console.error(` 请先执行:`)
|
|
2018
|
+
console.error(` sillyspec run ${stageName} --wait --reason "${currentStepDef.waitReason || '等待用户输入'}" --options "${(currentStepDef.waitOptions || ['确认']).join(',')}"${changeName ? ` --change ${changeName}` : ''} --output "你的问题/方案摘要"`)
|
|
2019
|
+
console.error(` 或使用 --done --answer "用户回答" 一步完成 wait + done`)
|
|
2020
|
+
process.exit(1)
|
|
1839
2021
|
}
|
|
1840
|
-
console.error(` 请先执行:`)
|
|
1841
|
-
console.error(` sillyspec run ${stageName} --wait --reason "${currentStepDef.waitReason || '等待用户输入'}" --options "${(currentStepDef.waitOptions || ['确认']).join(',')}"${changeName ? ` --change ${changeName}` : ''} --output "你的问题/方案摘要"`)
|
|
1842
|
-
process.exit(1)
|
|
1843
2022
|
}
|
|
1844
2023
|
|
|
1845
2024
|
steps[currentIdx].status = 'completed'
|
|
@@ -2234,10 +2413,16 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2234
2413
|
}
|
|
2235
2414
|
}
|
|
2236
2415
|
|
|
2416
|
+
// 防御性守卫变量:确认所有步骤确实标记为 completed
|
|
2417
|
+
const actualCompleted = steps.filter(s => s.status === 'completed').length
|
|
2418
|
+
const actualTotal = steps.length
|
|
2419
|
+
|
|
2237
2420
|
validateMetadata(cwd, stageName, specBase)
|
|
2238
2421
|
|
|
2239
|
-
//
|
|
2240
|
-
|
|
2422
|
+
// 验证关键文件是否在正确的变更目录下(仅当所有步骤确实完成时才校验)
|
|
2423
|
+
if (actualCompleted === actualTotal && actualTotal > 0) {
|
|
2424
|
+
validateFileLocations(cwd, stageName, progress, changeName, specBase)
|
|
2425
|
+
}
|
|
2241
2426
|
|
|
2242
2427
|
// 辅助阶段完成后重置步骤
|
|
2243
2428
|
const stageDef = stageRegistry[stageName]
|
|
@@ -2281,24 +2466,58 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2281
2466
|
} else {
|
|
2282
2467
|
console.log(`\n下一步由你决定:sillyspec run <stage>(brainstorm/plan/execute/verify/archive 等)`)
|
|
2283
2468
|
}
|
|
2284
|
-
return { stageCompleted: true, currentIdx, nextPendingIdx: -1 }
|
|
2285
|
-
}
|
|
2286
2469
|
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2470
|
+
// 阶段完成校验 — 防御性守卫:仅当所有步骤确实标记为 completed 时才跑 validator
|
|
2471
|
+
if (actualCompleted === actualTotal && actualTotal > 0) {
|
|
2472
|
+
const projectName = progress.project || basename(cwd)
|
|
2473
|
+
const contractResult = runValidators(stageName, cwd, changeName, { projectName, specRoot: platformOpts?.specRoot })
|
|
2474
|
+
if (contractResult.errors.length > 0) {
|
|
2475
|
+
console.error(`\n❌ 阶段 ${stageName} 校验失败:`)
|
|
2476
|
+
for (const err of contractResult.errors) {
|
|
2477
|
+
console.error(` - ${err}`)
|
|
2478
|
+
}
|
|
2479
|
+
console.error(`\n 提示:修复缺失产物后重新运行此步骤,或使用 --skip-approval 跳过校验`)
|
|
2480
|
+
}
|
|
2481
|
+
if (contractResult.warnings.length > 0) {
|
|
2482
|
+
console.warn(`\n⚠️ 阶段 ${stageName} 校验警告:`)
|
|
2483
|
+
for (const w of contractResult.warnings) {
|
|
2484
|
+
console.warn(` - ${w}`)
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
// ── Plan postcheck contract:plan.md 必须满足 execute 契约 ──
|
|
2489
|
+
if (stageName === 'plan') {
|
|
2490
|
+
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
2491
|
+
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
2492
|
+
if (planPath && existsSync(planPath)) {
|
|
2493
|
+
const { validatePlanForExecute } = await import('./stages/execute.js')
|
|
2494
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
2495
|
+
const planValidation = validatePlanForExecute(planContent)
|
|
2496
|
+
if (!planValidation.ok) {
|
|
2497
|
+
console.error(`\n❌ Plan → Execute Contract 校验失败:`)
|
|
2498
|
+
for (const err of planValidation.errors) console.error(` - ${err}`)
|
|
2499
|
+
console.error(`\n plan.md 不满足 execute 契约,请修复后重新完成此步骤。`)
|
|
2500
|
+
// 阻断 completed
|
|
2501
|
+
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
2502
|
+
await pm._write(cwd, progress, changeName)
|
|
2503
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
2504
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
2505
|
+
}
|
|
2506
|
+
if (planValidation.warnings.length > 0) {
|
|
2507
|
+
console.warn(`\n⚠️ Plan contract 警告(不阻断完成):`)
|
|
2508
|
+
for (const w of planValidation.warnings) console.warn(` - ${w}`)
|
|
2509
|
+
}
|
|
2510
|
+
if (planValidation.ok) {
|
|
2511
|
+
console.log(`\n✅ Plan → Execute Contract 校验通过(${planValidation.tasks.length} tasks, ${planValidation.waves.length} waves)`)
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
} else if (actualCompleted < actualTotal) {
|
|
2516
|
+
// 实际步骤未全部完成,跳过 validator(状态可能不同步)
|
|
2517
|
+
console.log(`\n⚠️ 阶段校验跳过:${actualTotal} 步中仅 ${actualCompleted} 步标记为已完成,可能存在状态不同步。如确认阶段已完成,请运行 --status 确认。`)
|
|
2301
2518
|
}
|
|
2519
|
+
|
|
2520
|
+
return { stageCompleted: true, currentIdx, nextPendingIdx: -1 }
|
|
2302
2521
|
}
|
|
2303
2522
|
|
|
2304
2523
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
@@ -2466,8 +2685,12 @@ function showStatus(progress, stageName) {
|
|
|
2466
2685
|
const stageDef = stageRegistry[stageName]
|
|
2467
2686
|
|
|
2468
2687
|
if (!stageData || !stageData.steps || stageData.steps.length === 0) {
|
|
2469
|
-
console.log(`阶段:${stageName}(${stageDef
|
|
2688
|
+
console.log(`阶段:${stageName}(${stageDef?.title || stageName})`)
|
|
2470
2689
|
console.log(`进度:未初始化`)
|
|
2690
|
+
if (stageData?.status) {
|
|
2691
|
+
console.log(`状态:${stageData.status}`)
|
|
2692
|
+
if (stageData.staleReason) console.log(`⚠️ 失效原因:${stageData.staleReason}`)
|
|
2693
|
+
}
|
|
2471
2694
|
return
|
|
2472
2695
|
}
|
|
2473
2696
|
|
|
@@ -2475,8 +2698,25 @@ function showStatus(progress, stageName) {
|
|
|
2475
2698
|
const completed = steps.filter(s => s.status === 'completed' || s.status === 'skipped').length
|
|
2476
2699
|
const bar = '█'.repeat(completed) + '░'.repeat(steps.length - completed)
|
|
2477
2700
|
|
|
2478
|
-
console.log(`阶段:${stageName}(${stageDef
|
|
2479
|
-
console.log(`进度:[${bar}] ${completed}/${steps.length}
|
|
2701
|
+
console.log(`阶段:${stageName}(${stageDef?.title || stageName})`)
|
|
2702
|
+
console.log(`进度:[${bar}] ${completed}/${steps.length}`)
|
|
2703
|
+
|
|
2704
|
+
// ── Revision v1 信息 ──
|
|
2705
|
+
if (stageData.status === 'revising') {
|
|
2706
|
+
console.log(`\n🔧 修订中 (revision ${stageData.revision || 1})`)
|
|
2707
|
+
if (stageData.reopenedFromStep) console.log(` 从步骤:${stageData.reopenedFromStep}`)
|
|
2708
|
+
if (stageData.reopenedAt) console.log(` 重开时间:${stageData.reopenedAt}`)
|
|
2709
|
+
}
|
|
2710
|
+
if (stageData.status === 'stale') {
|
|
2711
|
+
console.log(`\n⚠️ 已失效`)
|
|
2712
|
+
if (stageData.staleReason) console.log(` 原因:${stageData.staleReason}`)
|
|
2713
|
+
console.log(` 建议:sillyspec run ${stageName} --reopen --from-step 1`)
|
|
2714
|
+
}
|
|
2715
|
+
if (stageData.status === 'completed') {
|
|
2716
|
+
console.log(`\n✅ 已完成`)
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
console.log('')
|
|
2480
2720
|
|
|
2481
2721
|
const firstPending = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
2482
2722
|
|
package/src/scan-postcheck.js
CHANGED
|
@@ -95,12 +95,13 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
95
95
|
})
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
// 3. 检查文档 header(author / created_at
|
|
98
|
+
// 3. 检查文档 header(author / created_at)— 只看文件头部,避免正文出现同名词被误判
|
|
99
99
|
const existingDocs = REQUIRED_SCAN_DOCS.filter(f => existsSync(join(specScanDir, f)))
|
|
100
100
|
const docsMissingHeader = []
|
|
101
101
|
for (const doc of existingDocs) {
|
|
102
102
|
const content = readFileSync(join(specScanDir, doc), 'utf8')
|
|
103
|
-
|
|
103
|
+
const headerSlice = content.slice(0, 512)
|
|
104
|
+
if (!/author\s*:/.test(headerSlice) || !/created_at\s*:/.test(headerSlice)) {
|
|
104
105
|
docsMissingHeader.push(doc)
|
|
105
106
|
}
|
|
106
107
|
}
|
|
@@ -183,6 +184,37 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
183
184
|
})
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
// 7.5 knowledge 产物校验
|
|
188
|
+
const knowledgeDir = join(specDir, 'knowledge')
|
|
189
|
+
if (existsSync(knowledgeDir)) {
|
|
190
|
+
const indexPath = join(knowledgeDir, 'INDEX.md')
|
|
191
|
+
if (!existsSync(indexPath)) {
|
|
192
|
+
checks.push({
|
|
193
|
+
name: 'knowledge_index_missing',
|
|
194
|
+
severity: CHECK_SEVERITY.WARNING,
|
|
195
|
+
detail: `knowledge/INDEX.md 不存在`
|
|
196
|
+
})
|
|
197
|
+
} else {
|
|
198
|
+
// 检查 INDEX.md 引用的文件是否真实存在
|
|
199
|
+
const indexContent = readFileSync(indexPath, 'utf8')
|
|
200
|
+
const referencedFiles = [...indexContent.matchAll(/\(([^)]+\.md)/g)].map(m => m[1])
|
|
201
|
+
const missingRefs = referencedFiles.filter(f => !existsSync(join(knowledgeDir, f)))
|
|
202
|
+
if (missingRefs.length > 0) {
|
|
203
|
+
checks.push({
|
|
204
|
+
name: 'knowledge_broken_refs',
|
|
205
|
+
severity: CHECK_SEVERITY.WARNING,
|
|
206
|
+
detail: `INDEX.md 引用了不存在的文件: ${missingRefs.join(', ')}`
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
checks.push({
|
|
212
|
+
name: 'knowledge_dir_missing',
|
|
213
|
+
severity: CHECK_SEVERITY.WARNING,
|
|
214
|
+
detail: `knowledge/ 目录不存在`
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
186
218
|
// 8. 计算 finalStatus
|
|
187
219
|
const hasFailed = checks.some(c => c.severity === CHECK_SEVERITY.FAILED)
|
|
188
220
|
const hasWarning = checks.some(c => c.severity === CHECK_SEVERITY.WARNING)
|
package/src/stage-contract.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
9
9
|
import { join, basename } from 'path'
|
|
10
|
+
import { detectChangeRisk, checkIntegrationEvidence } from './change-risk-profile.js'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* 校验结果
|
|
@@ -233,6 +234,17 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
|
|
|
233
234
|
if (!content.includes('自审') && !content.includes('Self-Review') && !content.includes('Self-review')) {
|
|
234
235
|
warnings.push('design.md 缺少「自审」章节')
|
|
235
236
|
}
|
|
237
|
+
|
|
238
|
+
// P1: 涉及生命周期关键词时,design.md 必须包含生命周期契约表
|
|
239
|
+
const hasLifecycleKeyword = /\b(session|lease|agent[._-]?run|daemon|lifecycle|state[._-]?transition|claim|heartbeat)\b/i.test(content)
|
|
240
|
+
if (hasLifecycleKeyword) {
|
|
241
|
+
const hasLifecycleTable =
|
|
242
|
+
/生命周期契约表|lifecycle[._-]?contract|lifecycle[._-]?matrix|Lifecycle Contract/i.test(content) ||
|
|
243
|
+
/事件.*发起方.*接收方.*必需字段.*状态变化/.test(content)
|
|
244
|
+
if (!hasLifecycleTable) {
|
|
245
|
+
errors.push('design.md 涉及生命周期关键词(session/lease/agent_run/daemon/lifecycle)但缺少「生命周期契约表」— 必须列出完整的事件×状态转换矩阵')
|
|
246
|
+
}
|
|
247
|
+
}
|
|
236
248
|
}
|
|
237
249
|
|
|
238
250
|
if (existsSync(join(changeDir, 'tasks.md'))) {
|
|
@@ -294,12 +306,62 @@ function validatePlanOutputs(cwd, changeName, context = {}) {
|
|
|
294
306
|
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
295
307
|
warnMissingIds(warnings, decisionIds, plan, 'plan.md', 'decisions.md')
|
|
296
308
|
}
|
|
309
|
+
// ── P0: 生产接线路径检查:design 提到入口但 task 的 allowed_paths 不含入口文件 ──
|
|
310
|
+
const designContent = readIfExists(join(changeDir, 'design.md'))
|
|
311
|
+
if (designContent) {
|
|
312
|
+
const entryPointPatterns = [
|
|
313
|
+
/\b(cli\.ts|main\.ts|server\.(?:js|ts)|index\.(?:js|ts))\b.*\b(?:实例化|instantiate|构造|new\s)/gi,
|
|
314
|
+
/\bnew\s+(Daemon|SessionManager|App|Server|Application)\b/gi,
|
|
315
|
+
/\b(?:在|from)\s+['"]?(cli\.ts|main\.ts|server\.(?:js|ts)|index\.(?:js|ts))['"]?/gi,
|
|
316
|
+
/\b(?:注入|inject)\b.*\b(?:构造|constructor|初始化|init|实例化|instantiate)\b/gi,
|
|
317
|
+
/\b(?:启动路径|startup|entrypoint|bootstrap|daemon[._-]?start|main.*entry)\b/gi,
|
|
318
|
+
]
|
|
319
|
+
const mentionedFiles = new Set()
|
|
320
|
+
for (const pattern of entryPointPatterns) {
|
|
321
|
+
pattern.lastIndex = 0
|
|
322
|
+
for (const match of designContent.matchAll(pattern)) {
|
|
323
|
+
const fileMatch = match[0].match(/\b(cli\.ts|main\.ts|server\.(?:js|ts)|index\.(?:js|ts))\b/i)
|
|
324
|
+
if (fileMatch) mentionedFiles.add(fileMatch[1].toLowerCase())
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (mentionedFiles.size > 0) {
|
|
328
|
+
const tasksDir = join(changeDir, 'tasks')
|
|
329
|
+
const allAllowedPaths = new Set()
|
|
330
|
+
if (existsSync(tasksDir)) {
|
|
331
|
+
const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/i.test(f))
|
|
332
|
+
for (const taskFile of taskFiles) {
|
|
333
|
+
const taskContent = readFileSync(join(tasksDir, taskFile), 'utf8')
|
|
334
|
+
const allowedSection = taskContent.match(/allowed_paths:\s*\n((?:\s+-\s+.+\n?)+)/)
|
|
335
|
+
if (allowedSection) {
|
|
336
|
+
const paths = allowedSection[1].match(/-\s+(.+)/g) || []
|
|
337
|
+
for (const p of paths) allAllowedPaths.add(p.replace(/^-\s+/, '').trim().toLowerCase())
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// 也从 plan.md 文件变更清单中收集
|
|
342
|
+
if (existsSync(planFile)) {
|
|
343
|
+
const planContent = readFileSync(planFile, 'utf8')
|
|
344
|
+
const planFileChanges = planContent.match(/\|\s*(?:新增|修改|new|modify|update)\s*\|\s*`?([^`|]+)`?\s*\|/gi) || []
|
|
345
|
+
for (const line of planFileChanges) {
|
|
346
|
+
const file = line.match(/\|\s*(?:新增|修改|new|modify|update)\s*\|\s*`?([^`|]+)`?\s*\|/i)
|
|
347
|
+
if (file) allAllowedPaths.add(file[1].trim().toLowerCase())
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
for (const mentionedFile of mentionedFiles) {
|
|
351
|
+
const found = [...allAllowedPaths].some(p => p.includes(mentionedFile))
|
|
352
|
+
if (!found) {
|
|
353
|
+
const noChangePattern = new RegExp(`不需要改.*${mentionedFile}|${mentionedFile}.*不需要|不修改.*${mentionedFile}|${mentionedFile}.*不变|${mentionedFile}.*no.?change`, 'i')
|
|
354
|
+
if (!noChangePattern.test(designContent)) {
|
|
355
|
+
errors.push(`生产接线路径矛盾: design.md 提到了入口文件 "${mentionedFile}" 但所有 task 的 allowed_paths 中均不含该文件`)
|
|
356
|
+
warnings.push(`提示: 如果确实不需要修改 ${mentionedFile},请在 design.md 中明确写明理由`)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
297
363
|
return { ok: errors.length === 0, errors, warnings }
|
|
298
364
|
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* verify 完成校验:检查变更目录和 verify 产物
|
|
302
|
-
*/
|
|
303
365
|
function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
304
366
|
const { specRoot } = context
|
|
305
367
|
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
@@ -334,6 +396,24 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
334
396
|
}
|
|
335
397
|
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
336
398
|
warnMissingIds(warnings, decisionIds, verify, 'verify-result.md', 'decisions.md')
|
|
399
|
+
|
|
400
|
+
// ── P0: Change Risk Gate — 核心功能缺少真实集成验证时 FAIL ──
|
|
401
|
+
const changeRiskProfile = detectChangeRisk({
|
|
402
|
+
designContent: readIfExists(join(changeDir, 'design.md')),
|
|
403
|
+
planContent: readIfExists(join(changeDir, 'plan.md')),
|
|
404
|
+
})
|
|
405
|
+
if (['integration-critical', 'deployment-critical'].includes(changeRiskProfile.level)) {
|
|
406
|
+
const conclusionMatch = verify.match(/^## 结论\s*\n\s*(PASS|PASS WITH NOTES|FAIL)/im)
|
|
407
|
+
const conclusion = conclusionMatch ? conclusionMatch[1] : ''
|
|
408
|
+
if (conclusion === 'PASS WITH NOTES' || conclusion === 'PASS') {
|
|
409
|
+
const evidenceCheck = checkIntegrationEvidence(verify, changeRiskProfile.requiredVerification)
|
|
410
|
+
if (!evidenceCheck.ok) {
|
|
411
|
+
errors.push(`[${changeRiskProfile.level}] 验证结论为 ${conclusion},但缺少真实集成证据:${evidenceCheck.errors.join('; ')}`)
|
|
412
|
+
errors.push(`触发词: ${changeRiskProfile.triggers.join(', ')} — PASS WITH NOTES 不被允许,必须 FAIL 或提供集成证据`)
|
|
413
|
+
}
|
|
414
|
+
warnings.push(...evidenceCheck.warnings)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
337
417
|
}
|
|
338
418
|
|
|
339
419
|
return { ok: errors.length === 0, errors, warnings }
|
|
@@ -520,6 +600,11 @@ export function checkTransition(fromStage, toStage) {
|
|
|
520
600
|
return { allowed: true }
|
|
521
601
|
}
|
|
522
602
|
|
|
603
|
+
// 同阶段内重复运行:允许(继续执行当前阶段的下一步、或修订模式继续)
|
|
604
|
+
if (fromStage === toStage) {
|
|
605
|
+
return { allowed: true }
|
|
606
|
+
}
|
|
607
|
+
|
|
523
608
|
// archive 特殊处理:从 verify 来的允许,从其他主流程阶段来的需要校验
|
|
524
609
|
if (toStage === 'archive') {
|
|
525
610
|
if (fromStage === 'verify') {
|
|
@@ -532,7 +617,7 @@ export function checkTransition(fromStage, toStage) {
|
|
|
532
617
|
return { allowed: false, reason: 'archive 的前置阶段是 verify,不能从 ' + fromStage + ' 跳转' }
|
|
533
618
|
}
|
|
534
619
|
|
|
535
|
-
//
|
|
620
|
+
// 从辅助阶段进入主流程:允许
|
|
536
621
|
if (auxiliaryStages.includes(fromStage)) {
|
|
537
622
|
return { allowed: true }
|
|
538
623
|
}
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -344,6 +344,28 @@ HTML 原型文件路径(或"跳过"如果不适合)`,
|
|
|
344
344
|
| 删除 | src/xxx/OldFile.java | 已被 xx 替代 |
|
|
345
345
|
|
|
346
346
|
7. **接口定义**:方法签名、数据结构(代码类任务必填)
|
|
347
|
+
7.5. **生命周期契约表**(涉及以下关键词时必填,否则可省略):
|
|
348
|
+
|
|
349
|
+
如果本次变更涉及以下任何关键词:
|
|
350
|
+
session / lease / agent_run / daemon / lifecycle / state transition / complete / end / claim / heartbeat
|
|
351
|
+
|
|
352
|
+
则必须在 design.md 中包含「生命周期契约表」章节,格式如下:
|
|
353
|
+
|
|
354
|
+
| 事件 | 发起方 | 接收方 | 必需字段 | 状态变化 |
|
|
355
|
+
|---|---|---|---|---|
|
|
356
|
+
| claim lease | daemon | backend | leaseId, claimToken, agentRunId | pending → running |
|
|
357
|
+
| create session | backend | daemon | sessionId, leaseId, claimToken | session active |
|
|
358
|
+
| submit message | daemon | backend | leaseId, claimToken, agentRunId | append messages |
|
|
359
|
+
| turn result | daemon | backend | runId, status, output | running → completed/failed |
|
|
360
|
+
| session end | daemon | backend | sessionId, reason | active → ended |
|
|
361
|
+
|
|
362
|
+
判断规则:
|
|
363
|
+
- design.md 或需求中出现上述关键词 → 必须生成此表
|
|
364
|
+
- 表中的每个事件 → 必须有对应代码任务、接口任务、测试任务
|
|
365
|
+
- 表中的必需字段 → 必须出现在相关 DTO/interface 定义中
|
|
366
|
+
- 缺少任一事件 → 在 design.md 风险登记中明确记录
|
|
367
|
+
|
|
368
|
+
**判定方法**:在自审阶段,如果检测到上述关键词但 design.md 中没有此表 → 自审不通过
|
|
347
369
|
8. **数据模型**(如涉及):表结构/字段变更
|
|
348
370
|
9. **兼容策略**(brownfield 必填):
|
|
349
371
|
- 未配置新功能时行为不变
|
|
@@ -382,6 +404,7 @@ HTML 原型文件路径(或"跳过"如果不适合)`,
|
|
|
382
404
|
- 非目标清晰:是否明确界定了不做的事
|
|
383
405
|
- 兼容策略(brownfield):是否说明了回退路径
|
|
384
406
|
- 风险识别:是否识别了关键技术风险和对策
|
|
407
|
+
- **生命周期契约表**(如涉及 session/lease/agent_run/daemon/lifecycle/claim/heartbeat 等关键词):design.md 是否包含完整的生命周期契约表?每个事件是否有必需字段定义?字段是否出现在 DTO/interface 中?
|
|
385
408
|
5. 自审发现问题 → 修改后重新检查
|
|
386
409
|
6. 全部通过 → 进入下一步
|
|
387
410
|
|