sillyspec 3.18.2 → 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/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 +200 -3
- package/src/scan-postcheck.js +34 -2
- package/src/stage-contract.js +86 -6
- package/src/stages/brainstorm.js +23 -0
- package/src/stages/execute.js +110 -2
- 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) {
|
|
@@ -1358,6 +1476,13 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
1358
1476
|
|
|
1359
1477
|
let currentIdx = steps.findIndex(s => s.status !== 'completed' && s.status !== 'skipped')
|
|
1360
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
|
+
|
|
1361
1486
|
// ── scanProfile: 根据 project 规模动态裁剪步骤 ──
|
|
1362
1487
|
let scanProfile = null
|
|
1363
1488
|
if (stageName === 'scan' && steps.length > 0 && currentIdx === 0) {
|
|
@@ -1453,6 +1578,29 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
1453
1578
|
console.log(` 继续执行将从中断处恢复,用 --reset 可重新开始。\n`)
|
|
1454
1579
|
}
|
|
1455
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
|
+
|
|
1456
1604
|
const defSteps = await getStageSteps(stageName, cwd, progress)
|
|
1457
1605
|
if (defSteps && defSteps[currentIdx]) {
|
|
1458
1606
|
// noAI 步骤自动完成(CLI-only,不需要 Agent 参与)
|
|
@@ -2336,6 +2484,34 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2336
2484
|
console.warn(` - ${w}`)
|
|
2337
2485
|
}
|
|
2338
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
|
+
}
|
|
2339
2515
|
} else if (actualCompleted < actualTotal) {
|
|
2340
2516
|
// 实际步骤未全部完成,跳过 validator(状态可能不同步)
|
|
2341
2517
|
console.log(`\n⚠️ 阶段校验跳过:${actualTotal} 步中仅 ${actualCompleted} 步标记为已完成,可能存在状态不同步。如确认阶段已完成,请运行 --status 确认。`)
|
|
@@ -2509,8 +2685,12 @@ function showStatus(progress, stageName) {
|
|
|
2509
2685
|
const stageDef = stageRegistry[stageName]
|
|
2510
2686
|
|
|
2511
2687
|
if (!stageData || !stageData.steps || stageData.steps.length === 0) {
|
|
2512
|
-
console.log(`阶段:${stageName}(${stageDef
|
|
2688
|
+
console.log(`阶段:${stageName}(${stageDef?.title || stageName})`)
|
|
2513
2689
|
console.log(`进度:未初始化`)
|
|
2690
|
+
if (stageData?.status) {
|
|
2691
|
+
console.log(`状态:${stageData.status}`)
|
|
2692
|
+
if (stageData.staleReason) console.log(`⚠️ 失效原因:${stageData.staleReason}`)
|
|
2693
|
+
}
|
|
2514
2694
|
return
|
|
2515
2695
|
}
|
|
2516
2696
|
|
|
@@ -2518,8 +2698,25 @@ function showStatus(progress, stageName) {
|
|
|
2518
2698
|
const completed = steps.filter(s => s.status === 'completed' || s.status === 'skipped').length
|
|
2519
2699
|
const bar = '█'.repeat(completed) + '░'.repeat(steps.length - completed)
|
|
2520
2700
|
|
|
2521
|
-
console.log(`阶段:${stageName}(${stageDef
|
|
2522
|
-
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('')
|
|
2523
2720
|
|
|
2524
2721
|
const firstPending = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
2525
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,7 +600,7 @@ export function checkTransition(fromStage, toStage) {
|
|
|
520
600
|
return { allowed: true }
|
|
521
601
|
}
|
|
522
602
|
|
|
523
|
-
//
|
|
603
|
+
// 同阶段内重复运行:允许(继续执行当前阶段的下一步、或修订模式继续)
|
|
524
604
|
if (fromStage === toStage) {
|
|
525
605
|
return { allowed: true }
|
|
526
606
|
}
|
|
@@ -537,7 +617,7 @@ export function checkTransition(fromStage, toStage) {
|
|
|
537
617
|
return { allowed: false, reason: 'archive 的前置阶段是 verify,不能从 ' + fromStage + ' 跳转' }
|
|
538
618
|
}
|
|
539
619
|
|
|
540
|
-
//
|
|
620
|
+
// 从辅助阶段进入主流程:允许
|
|
541
621
|
if (auxiliaryStages.includes(fromStage)) {
|
|
542
622
|
return { allowed: true }
|
|
543
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
|
|
package/src/stages/execute.js
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* 校验 plan.md 是否满足 execute 执行契约
|
|
6
|
+
* @param {string} planContent - plan.md 文件内容
|
|
7
|
+
* @returns {{ ok: boolean, errors: string[], warnings: string[], tasks: object[], waves: object[] }}
|
|
8
|
+
*/
|
|
9
|
+
export function validatePlanForExecute(planContent) {
|
|
10
|
+
const errors = []
|
|
11
|
+
const warnings = []
|
|
12
|
+
|
|
13
|
+
if (!planContent || !planContent.trim()) {
|
|
14
|
+
return { ok: false, errors: ['plan.md 内容为空'], warnings, tasks: [], waves: [] }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const waves = parseWavesFromPlan(planContent)
|
|
18
|
+
|
|
19
|
+
// 收集所有 task
|
|
20
|
+
const allTasks = []
|
|
21
|
+
for (const wave of waves) {
|
|
22
|
+
for (const task of wave.tasks) {
|
|
23
|
+
allTasks.push(task)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 检查 1: 至少有一个 checkbox task
|
|
28
|
+
if (allTasks.length === 0) {
|
|
29
|
+
errors.push('plan.md 中没有找到 checkbox task(格式: "- [ ] task-XX: 任务名")')
|
|
30
|
+
return { ok: false, errors, warnings, tasks: allTasks, waves }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 检查 2: task id 唯一性
|
|
34
|
+
const idCounts = {}
|
|
35
|
+
for (const task of allTasks) {
|
|
36
|
+
if (task.index != null) {
|
|
37
|
+
const key = `task-${task.index}`
|
|
38
|
+
idCounts[key] = (idCounts[key] || 0) + 1
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const [id, count] of Object.entries(idCounts)) {
|
|
42
|
+
if (count > 1) {
|
|
43
|
+
errors.push(`task id 重复: ${id} 出现 ${count} 次`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 检查 3: task id 连续性(从 1 开始)
|
|
48
|
+
const ids = allTasks
|
|
49
|
+
.map(t => t.index)
|
|
50
|
+
.filter(i => i != null)
|
|
51
|
+
.sort((a, b) => a - b)
|
|
52
|
+
if (ids.length > 0) {
|
|
53
|
+
const expected = Array.from({ length: ids.length }, (_, i) => ids[0] + i)
|
|
54
|
+
// 只检查以 task-01 起始的情况(常见模式)
|
|
55
|
+
if (ids[0] === 1) {
|
|
56
|
+
for (let i = 0; i < ids.length; i++) {
|
|
57
|
+
if (ids[i] !== i + 1) {
|
|
58
|
+
errors.push(`task id 不连续: 期望 task-${String(i + 1).padStart(2, '0')}, 实际 task-${String(ids[i]).padStart(2, '0')}`)
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 检查 4: task name 非空
|
|
66
|
+
for (const task of allTasks) {
|
|
67
|
+
if (!task.name || !task.name.trim()) {
|
|
68
|
+
errors.push(`task-${String(task.index || '?').padStart(2, '0')}: 任务名为空`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 检查 5: task 无 id 的 warning(不限制只在有 id 时检查)
|
|
73
|
+
for (const wave of waves) {
|
|
74
|
+
for (const task of wave.tasks) {
|
|
75
|
+
if (task.index == null) {
|
|
76
|
+
warnings.push(`Wave ${wave.index}: task "${task.name}" 没有 task id(建议格式 task-XX: 名称)`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { ok: errors.length === 0, errors, warnings, tasks: allTasks, waves }
|
|
82
|
+
}
|
|
83
|
+
|
|
4
84
|
export const definition = {
|
|
5
85
|
name: 'execute',
|
|
6
86
|
title: '波次执行',
|
|
@@ -46,6 +126,27 @@ const fixedPrefix = [
|
|
|
46
126
|
- 用 main_symbols 字段找到核心类/函数的定义位置
|
|
47
127
|
- 子代理优先读模块卡片理解语义,再读 entrypoints/main_symbols 对应的源码
|
|
48
128
|
|
|
129
|
+
### 符号影响面扩展检查
|
|
130
|
+
11. **符号影响面扫描**(Critical — execute 前必做):
|
|
131
|
+
- 读取所有 tasks/task-NN.md,提取每个任务涉及的修改文件
|
|
132
|
+
- 对每个修改文件,检查是否涉及以下变更类型:
|
|
133
|
+
- class 构造函数参数变更(新增/删除/修改参数)
|
|
134
|
+
- 接口(interface)定义变更
|
|
135
|
+
- DTO / 类型定义变更
|
|
136
|
+
- API client 方法签名变更
|
|
137
|
+
- 函数/方法签名变更(参数增删改)
|
|
138
|
+
- 如果涉及上述变更类型,执行调用点搜索:
|
|
139
|
+
\`\`\`bash
|
|
140
|
+
rg "new ClassName\(" src/
|
|
141
|
+
rg "ClassName\(" src/
|
|
142
|
+
rg "methodName\(" src/
|
|
143
|
+
rg "import.*from.*filePath" src/
|
|
144
|
+
\`\`\`
|
|
145
|
+
- 将搜索到的调用点与 plan.md 和 tasks/task-NN.md 的 allowed_paths 对比
|
|
146
|
+
- **发现调用点不在任何 task 的 allowed_paths 中 → 直接阻断 execute**
|
|
147
|
+
- 报告:列出每个受影响符号、调用点位置、是否在任务范围内
|
|
148
|
+
- 如果调用点不在范围内但任务明确写了"不改原因",记录但不阻断
|
|
149
|
+
|
|
49
150
|
### 输出
|
|
50
151
|
已加载的上下文摘要(含模块文档 + 源码锚点)`,
|
|
51
152
|
outputHint: '上下文摘要',
|
|
@@ -91,6 +192,12 @@ sillyspec run execute --done --output "worktree 路径 + 分支名 + 模式"`,
|
|
|
91
192
|
- auto — 全部自动执行
|
|
92
193
|
5. 查询知识库:读取 \`.sillyspec/knowledge/INDEX.md\`,根据 Task 关键词匹配
|
|
93
194
|
|
|
195
|
+
### 知识命中报告
|
|
196
|
+
{KNOWLEDGE_HIT_REPORT}
|
|
197
|
+
|
|
198
|
+
如上所示的知识条目与本次任务相关。请阅读这些条目以获取项目约定和已知模式。
|
|
199
|
+
如无命中条目(Status: no matches),跳过本节。
|
|
200
|
+
|
|
94
201
|
### 铁律
|
|
95
202
|
- **不要询问用户确认频率**,确认模式由 CLI \`--confirm-mode\` 参数决定
|
|
96
203
|
- 如果未检测到 \`--confirm-mode\`,默认使用 wave 模式`,
|
|
@@ -403,12 +510,13 @@ export function buildExecuteSteps(planFilePath = null, options = {}) {
|
|
|
403
510
|
|
|
404
511
|
if (planFilePath && existsSync(planFilePath)) {
|
|
405
512
|
const planContent = readFileSync(planFilePath, 'utf8')
|
|
513
|
+
// Plan → Execute 契约由 plan 阶段完成时的 postcheck 把关(run.js completeStep),
|
|
514
|
+
// 此处只负责解析 waves,避免 buildExecuteSteps 与进程退出耦合。
|
|
406
515
|
waves = parseWavesFromPlan(planContent)
|
|
407
|
-
// 从 planFilePath 推导 changeDir: .sillyspec/changes/<name>/plan.md
|
|
408
516
|
changeDir = path.dirname(planFilePath)
|
|
409
517
|
}
|
|
410
518
|
|
|
411
|
-
//
|
|
519
|
+
// 没解析出 Wave(plan 不存在或不含可识别 task)→ 默认 3 Wave(向后兼容)
|
|
412
520
|
if (!waves || waves.length === 0) {
|
|
413
521
|
waves = []
|
|
414
522
|
for (let i = 1; i <= 3; i++) {
|