sillyspec 3.18.2 → 3.18.4
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/contract-matrix.js +278 -0
- package/src/db.js +6 -0
- package/src/endpoint-extractor.js +315 -0
- package/src/index.js +53 -6
- package/src/init.js +31 -4
- package/src/knowledge-match.js +130 -0
- package/src/progress.js +464 -11
- package/src/run.js +287 -7
- 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 +158 -4
- package/src/stages/plan.js +82 -0
- package/src/stages/scan.js +40 -0
- package/src/stages/verify.js +63 -2
- package/src/worktree.js +264 -35
- package/test/brainstorm-plan-contract.test.mjs +273 -0
- package/test/contract-artifacts.test.mjs +323 -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')
|
|
@@ -1104,11 +1148,32 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1104
1148
|
// runCommand 后续所有 .sillyspec/ 操作必须用 specBase
|
|
1105
1149
|
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
1106
1150
|
|
|
1107
|
-
// 平台模式:清理旧版本残留的 cwd/.sillyspec
|
|
1151
|
+
// 平台模式:清理旧版本残留的 cwd/.sillyspec/(防止源码污染)。
|
|
1152
|
+
// ⚠️ 同 init.js:必须保护真实资产(changes/、projects/、sillyspec.db)。
|
|
1108
1153
|
if (platformOpts.specRoot) {
|
|
1109
1154
|
const legacyDir = join(cwd, '.sillyspec')
|
|
1110
1155
|
if (existsSync(legacyDir)) {
|
|
1111
|
-
|
|
1156
|
+
let hasChanges = false;
|
|
1157
|
+
try {
|
|
1158
|
+
const cd = join(legacyDir, 'changes');
|
|
1159
|
+
if (existsSync(cd)) hasChanges = readdirSync(cd).length > 0;
|
|
1160
|
+
} catch {}
|
|
1161
|
+
let hasProjects = false;
|
|
1162
|
+
try {
|
|
1163
|
+
const pd = join(legacyDir, 'projects');
|
|
1164
|
+
if (existsSync(pd)) hasProjects = readdirSync(pd).length > 0;
|
|
1165
|
+
} catch {}
|
|
1166
|
+
const hasDb = existsSync(join(legacyDir, 'sillyspec.db'));
|
|
1167
|
+
|
|
1168
|
+
if (hasChanges || hasProjects || hasDb) {
|
|
1169
|
+
console.error('❌ [sillyspec] 拒绝删除源码目录的 .sillyspec/:检测到真实资产。仅清理运行时残留。');
|
|
1170
|
+
for (const residue of ['.runtime', 'local.yaml', 'codebase']) {
|
|
1171
|
+
const p = join(legacyDir, residue);
|
|
1172
|
+
if (existsSync(p)) { try { rmSync(p, { recursive: true, force: true }) } catch {} }
|
|
1173
|
+
}
|
|
1174
|
+
} else {
|
|
1175
|
+
try { rmSync(legacyDir, { recursive: true, force: true }) } catch {}
|
|
1176
|
+
}
|
|
1112
1177
|
}
|
|
1113
1178
|
}
|
|
1114
1179
|
|
|
@@ -1153,6 +1218,7 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1153
1218
|
'--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
|
|
1154
1219
|
'--files', '--allow-new', '--force-baseline', '--force-rescan',
|
|
1155
1220
|
'--json', '--dir', '--help',
|
|
1221
|
+
'--reopen', '--from-step',
|
|
1156
1222
|
])
|
|
1157
1223
|
for (let i = 0; i < flags.length; i++) {
|
|
1158
1224
|
const f = flags[i]
|
|
@@ -1223,6 +1289,79 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1223
1289
|
return await resetStage(pm, progress, stageName, cwd, effectiveChange, platformOpts)
|
|
1224
1290
|
}
|
|
1225
1291
|
|
|
1292
|
+
// ── 规则 1:completed 阶段直接 run,拒绝(但 --status 放行)──
|
|
1293
|
+
const stageStatus = progress.stages[stageName]?.status
|
|
1294
|
+
if (stageStatus === 'completed' && !isReopen && !isStatus) {
|
|
1295
|
+
console.error(`\n❌ ${stageName} 阶段已完成。`)
|
|
1296
|
+
console.error(` 使用 --reopen 进行修订,或 --reset 从头开始。`)
|
|
1297
|
+
console.error(` 修订示例: sillyspec run ${stageName} --reopen --from-step <步骤序号或名称>`)
|
|
1298
|
+
process.exit(1)
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// ── 规则 5:stale 阶段直接 run,拒绝(但 --status / --reset 放行)──
|
|
1302
|
+
if (stageStatus === 'stale' && !isReopen && !isStatus && !isReset) {
|
|
1303
|
+
const staleReason = progress.stages[stageName]?.staleReason || '上游阶段已修订'
|
|
1304
|
+
console.error(`\n⚠️ ${stageName} 阶段已失效(stale)。`)
|
|
1305
|
+
console.error(` 原因:${staleReason}`)
|
|
1306
|
+
console.error(` 使用 --reopen --from-step <步骤> 进行修订,或 --reset 从头开始。`)
|
|
1307
|
+
process.exit(1)
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// ── --reopen 处理 ──
|
|
1311
|
+
if (isReopen) {
|
|
1312
|
+
// stale/revising 阶段可能 steps 为空,或者 execute 阶段的 steps 需要从最新 plan.md 刷新
|
|
1313
|
+
const stageDataPre = progress.stages[stageName]
|
|
1314
|
+
const needsInit = !stageDataPre || !stageDataPre.steps || stageDataPre.steps.length === 0
|
|
1315
|
+
// execute 阶段在 reopen 时需要从最新 plan.md 重新解析 steps(plan 可能已变更)
|
|
1316
|
+
if (needsInit || stageName === 'execute') {
|
|
1317
|
+
const freshSteps = await getStageSteps(stageName, cwd, progress, specRoot)
|
|
1318
|
+
if (freshSteps && freshSteps.length > 0) {
|
|
1319
|
+
if (!progress.stages[stageName]) progress.stages[stageName] = { status: 'stale', steps: [] }
|
|
1320
|
+
progress.stages[stageName].steps = freshSteps.map(s => ({ name: s.name, status: 'pending' }))
|
|
1321
|
+
await pm._write(cwd, progress, effectiveChange)
|
|
1322
|
+
progress = await pm.read(cwd, effectiveChange) || progress
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
const result = await pm.reopenStage(cwd, stageName, {
|
|
1327
|
+
fromStep: fromStepValue,
|
|
1328
|
+
changeName: effectiveChange,
|
|
1329
|
+
})
|
|
1330
|
+
if (!result.ok) {
|
|
1331
|
+
console.error(`\n❌ ${result.error}`)
|
|
1332
|
+
if (stageStatus === 'completed') {
|
|
1333
|
+
console.error(`\n 提示:sillyspec run ${stageName} --reopen --from-step <步骤序号或名称>`)
|
|
1334
|
+
}
|
|
1335
|
+
process.exit(1)
|
|
1336
|
+
}
|
|
1337
|
+
console.log(`\n🔧 ${stageName} 阶段已重新打开(revision ${result.revision})`)
|
|
1338
|
+
console.log(` 从步骤「${result.fromStep}」开始修订`)
|
|
1339
|
+
console.log(` 该步骤及之后的产出需要重新生成。`)
|
|
1340
|
+
if (stageName === 'execute') console.log(` ⚡ execute 步骤已从最新 plan.md 重新解析。`)
|
|
1341
|
+
console.log('')
|
|
1342
|
+
|
|
1343
|
+
// 重新读取 progress
|
|
1344
|
+
progress = await pm.read(cwd, effectiveChange) || progress
|
|
1345
|
+
|
|
1346
|
+
// 注入 revision context 到 platformOpts,供 outputStep 使用
|
|
1347
|
+
const stageData = progress.stages[stageName]
|
|
1348
|
+
if (stageData && stageData.revision > 0) {
|
|
1349
|
+
platformOpts._revision = {
|
|
1350
|
+
revision: stageData.revision,
|
|
1351
|
+
fromStep: stageData.reopenedFromStep,
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
} else {
|
|
1355
|
+
// 非 reopen 的正常执行:如果阶段处于 revising 状态,也注入 revision context
|
|
1356
|
+
const revStageData = progress.stages[stageName]
|
|
1357
|
+
if (revStageData && revStageData.status === 'revising' && revStageData.revision > 0 && !platformOpts._revision) {
|
|
1358
|
+
platformOpts._revision = {
|
|
1359
|
+
revision: revStageData.revision,
|
|
1360
|
+
fromStep: revStageData.reopenedFromStep,
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1226
1365
|
// 确保步骤已初始化
|
|
1227
1366
|
const changed = await ensureStageSteps(progress, stageName, cwd, specRoot)
|
|
1228
1367
|
if (changed && effectiveChange) {
|
|
@@ -1358,6 +1497,13 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
1358
1497
|
|
|
1359
1498
|
let currentIdx = steps.findIndex(s => s.status !== 'completed' && s.status !== 'skipped')
|
|
1360
1499
|
|
|
1500
|
+
// stale 步骤视为可执行(等同于 pending)
|
|
1501
|
+
if (currentIdx !== -1 && steps[currentIdx].status === 'stale') {
|
|
1502
|
+
steps[currentIdx].status = 'pending'
|
|
1503
|
+
await pm._write(cwd, progress, changeName)
|
|
1504
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1361
1507
|
// ── scanProfile: 根据 project 规模动态裁剪步骤 ──
|
|
1362
1508
|
let scanProfile = null
|
|
1363
1509
|
if (stageName === 'scan' && steps.length > 0 && currentIdx === 0) {
|
|
@@ -1453,6 +1599,29 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
1453
1599
|
console.log(` 继续执行将从中断处恢复,用 --reset 可重新开始。\n`)
|
|
1454
1600
|
}
|
|
1455
1601
|
|
|
1602
|
+
// ── Brainstorm → Plan Contract:plan 启动前校验 design.md ──
|
|
1603
|
+
if (stageName === 'plan' && currentIdx === 0) {
|
|
1604
|
+
const changeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot || null)
|
|
1605
|
+
const designPath = changeDir ? join(changeDir, 'design.md') : null
|
|
1606
|
+
if (designPath && existsSync(designPath)) {
|
|
1607
|
+
const { validateDesignForPlan } = await import('./stages/plan.js')
|
|
1608
|
+
const designContent = readFileSync(designPath, 'utf8')
|
|
1609
|
+
const designValidation = validateDesignForPlan(designContent)
|
|
1610
|
+
if (!designValidation.ok) {
|
|
1611
|
+
console.error(`\n❌ Brainstorm → Plan Contract 校验失败:`)
|
|
1612
|
+
for (const err of designValidation.errors) console.error(` - ${err}`)
|
|
1613
|
+
console.error(`\n design.md 不满足 plan 契约,请先修复后重试。`)
|
|
1614
|
+
console.error(` 提示:sillyspec run brainstorm --reopen --from-step <步骤> 修订设计文档`)
|
|
1615
|
+
process.exit(1)
|
|
1616
|
+
}
|
|
1617
|
+
if (designValidation.warnings.length > 0) {
|
|
1618
|
+
console.log(`⚠️ Design contract 警告(不阻断):`)
|
|
1619
|
+
for (const w of designValidation.warnings) console.log(` - ${w}`)
|
|
1620
|
+
console.log()
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1456
1625
|
const defSteps = await getStageSteps(stageName, cwd, progress)
|
|
1457
1626
|
if (defSteps && defSteps[currentIdx]) {
|
|
1458
1627
|
// noAI 步骤自动完成(CLI-only,不需要 Agent 参与)
|
|
@@ -1795,12 +1964,38 @@ async function continueStep(pm, progress, stageName, cwd, answer, options = {})
|
|
|
1795
1964
|
stageData.completedAt = now
|
|
1796
1965
|
await pm._write(cwd, progress, changeName)
|
|
1797
1966
|
console.log(`\n✅ ${stageName} 阶段已完成(${stageData.steps.length}/${stageData.steps.length} 步)`)
|
|
1967
|
+
// ── execute 阶段完成时条件性清理 worktree ──
|
|
1968
|
+
if (stageName === 'execute' && changeName) {
|
|
1969
|
+
try {
|
|
1970
|
+
const { WorktreeManager } = await import('./worktree.js');
|
|
1971
|
+
const wm = new WorktreeManager({ cwd });
|
|
1972
|
+
const meta = wm.getMeta(changeName);
|
|
1973
|
+
if (!meta) {
|
|
1974
|
+
console.log('🔗 Worktree: n/a (no meta)');
|
|
1975
|
+
} else if (meta.mode === 'native-worktree') {
|
|
1976
|
+
console.log('🔗 Worktree: kept (外部隔离环境)');
|
|
1977
|
+
} else if (meta.mode === 'in-place-fallback') {
|
|
1978
|
+
console.log('🔗 Worktree: n/a (in-place 模式)');
|
|
1979
|
+
} else {
|
|
1980
|
+
const check = wm.hasUnappliedChanges(changeName);
|
|
1981
|
+
if (check.hasChanges) {
|
|
1982
|
+
console.log(`🔗 Worktree: pending apply (${check.changedFiles.length} 个未应用变更)`);
|
|
1983
|
+
console.log(` 下一步: sillyspec worktree apply ${changeName}`);
|
|
1984
|
+
} else {
|
|
1985
|
+
const cleanResult = wm.cleanup(changeName);
|
|
1986
|
+
console.log(`🔗 Worktree: ${cleanResult.result}`);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
} catch (e) {
|
|
1990
|
+
console.warn(`🔗 Worktree: check failed — ${e.message}`);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1798
1993
|
return { stageCompleted: true, currentIdx, nextPendingIdx: -1 }
|
|
1799
1994
|
}
|
|
1800
1995
|
|
|
1801
1996
|
// 输出下一步
|
|
1802
|
-
|
|
1803
|
-
|
|
1997
|
+
if (nextPendingIdx !== -1 && defSteps) {
|
|
1998
|
+
console.log('')
|
|
1804
1999
|
await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, answer)
|
|
1805
2000
|
} else if (nextWaitingIdx !== -1 && defSteps) {
|
|
1806
2001
|
// 下一个步骤也在等待状态
|
|
@@ -2336,11 +2531,75 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2336
2531
|
console.warn(` - ${w}`)
|
|
2337
2532
|
}
|
|
2338
2533
|
}
|
|
2534
|
+
|
|
2535
|
+
// ── Plan postcheck contract:plan.md 必须满足 execute 契约 ──
|
|
2536
|
+
if (stageName === 'plan') {
|
|
2537
|
+
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
2538
|
+
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
2539
|
+
if (planPath && existsSync(planPath)) {
|
|
2540
|
+
const { validatePlanForExecute } = await import('./stages/execute.js')
|
|
2541
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
2542
|
+
const planValidation = validatePlanForExecute(planContent)
|
|
2543
|
+
if (!planValidation.ok) {
|
|
2544
|
+
console.error(`\n❌ Plan → Execute Contract 校验失败:`)
|
|
2545
|
+
for (const err of planValidation.errors) console.error(` - ${err}`)
|
|
2546
|
+
console.error(`\n plan.md 不满足 execute 契约,请修复后重新完成此步骤。`)
|
|
2547
|
+
// 阻断 completed
|
|
2548
|
+
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
2549
|
+
await pm._write(cwd, progress, changeName)
|
|
2550
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
2551
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
2552
|
+
}
|
|
2553
|
+
if (planValidation.warnings.length > 0) {
|
|
2554
|
+
console.warn(`\n⚠️ Plan contract 警告(不阻断完成):`)
|
|
2555
|
+
for (const w of planValidation.warnings) console.warn(` - ${w}`)
|
|
2556
|
+
}
|
|
2557
|
+
if (planValidation.ok) {
|
|
2558
|
+
console.log(`\n✅ Plan → Execute Contract 校验通过(${planValidation.tasks.length} tasks, ${planValidation.waves.length} waves)`)
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2339
2562
|
} else if (actualCompleted < actualTotal) {
|
|
2340
2563
|
// 实际步骤未全部完成,跳过 validator(状态可能不同步)
|
|
2341
2564
|
console.log(`\n⚠️ 阶段校验跳过:${actualTotal} 步中仅 ${actualCompleted} 步标记为已完成,可能存在状态不同步。如确认阶段已完成,请运行 --status 确认。`)
|
|
2342
2565
|
}
|
|
2343
2566
|
|
|
2567
|
+
// ── execute 阶段完成时条件性清理 worktree(不依赖 AI agent 的完成确认步骤)──
|
|
2568
|
+
if (stageName === 'execute' && changeName) {
|
|
2569
|
+
try {
|
|
2570
|
+
const { WorktreeManager } = await import('./worktree.js');
|
|
2571
|
+
const wm = new WorktreeManager({ cwd });
|
|
2572
|
+
const meta = wm.getMeta(changeName);
|
|
2573
|
+
if (!meta) {
|
|
2574
|
+
console.log('🔗 Worktree: n/a (no meta)');
|
|
2575
|
+
} else if (meta.mode === 'native-worktree') {
|
|
2576
|
+
console.log('🔗 Worktree: kept (外部隔离环境)');
|
|
2577
|
+
} else if (meta.mode === 'in-place-fallback') {
|
|
2578
|
+
console.log('🔗 Worktree: n/a (in-place 模式)');
|
|
2579
|
+
} else {
|
|
2580
|
+
const check = wm.hasUnappliedChanges(changeName);
|
|
2581
|
+
if (check.hasChanges) {
|
|
2582
|
+
console.log(`🔗 Worktree: pending apply (${check.changedFiles.length} 个未应用变更)`);
|
|
2583
|
+
console.log(` 下一步: sillyspec worktree apply ${changeName}`);
|
|
2584
|
+
} else {
|
|
2585
|
+
const cleanResult = wm.cleanup(changeName);
|
|
2586
|
+
if (cleanResult.result === 'skipped' || cleanResult.result === 'kept') {
|
|
2587
|
+
console.log(`🔗 Worktree: ${cleanResult.result}`);
|
|
2588
|
+
} else {
|
|
2589
|
+
console.log(`🔗 Worktree: ${cleanResult.result}`);
|
|
2590
|
+
if (cleanResult.details?.length > 0) {
|
|
2591
|
+
for (const d of cleanResult.details) {
|
|
2592
|
+
if (d.startsWith('⚠️')) console.log(` ${d}`);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
} catch (e) {
|
|
2599
|
+
console.warn(`🔗 Worktree: check failed — ${e.message}`);
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2344
2603
|
return { stageCompleted: true, currentIdx, nextPendingIdx: -1 }
|
|
2345
2604
|
}
|
|
2346
2605
|
|
|
@@ -2509,8 +2768,12 @@ function showStatus(progress, stageName) {
|
|
|
2509
2768
|
const stageDef = stageRegistry[stageName]
|
|
2510
2769
|
|
|
2511
2770
|
if (!stageData || !stageData.steps || stageData.steps.length === 0) {
|
|
2512
|
-
console.log(`阶段:${stageName}(${stageDef
|
|
2771
|
+
console.log(`阶段:${stageName}(${stageDef?.title || stageName})`)
|
|
2513
2772
|
console.log(`进度:未初始化`)
|
|
2773
|
+
if (stageData?.status) {
|
|
2774
|
+
console.log(`状态:${stageData.status}`)
|
|
2775
|
+
if (stageData.staleReason) console.log(`⚠️ 失效原因:${stageData.staleReason}`)
|
|
2776
|
+
}
|
|
2514
2777
|
return
|
|
2515
2778
|
}
|
|
2516
2779
|
|
|
@@ -2518,8 +2781,25 @@ function showStatus(progress, stageName) {
|
|
|
2518
2781
|
const completed = steps.filter(s => s.status === 'completed' || s.status === 'skipped').length
|
|
2519
2782
|
const bar = '█'.repeat(completed) + '░'.repeat(steps.length - completed)
|
|
2520
2783
|
|
|
2521
|
-
console.log(`阶段:${stageName}(${stageDef
|
|
2522
|
-
console.log(`进度:[${bar}] ${completed}/${steps.length}
|
|
2784
|
+
console.log(`阶段:${stageName}(${stageDef?.title || stageName})`)
|
|
2785
|
+
console.log(`进度:[${bar}] ${completed}/${steps.length}`)
|
|
2786
|
+
|
|
2787
|
+
// ── Revision v1 信息 ──
|
|
2788
|
+
if (stageData.status === 'revising') {
|
|
2789
|
+
console.log(`\n🔧 修订中 (revision ${stageData.revision || 1})`)
|
|
2790
|
+
if (stageData.reopenedFromStep) console.log(` 从步骤:${stageData.reopenedFromStep}`)
|
|
2791
|
+
if (stageData.reopenedAt) console.log(` 重开时间:${stageData.reopenedAt}`)
|
|
2792
|
+
}
|
|
2793
|
+
if (stageData.status === 'stale') {
|
|
2794
|
+
console.log(`\n⚠️ 已失效`)
|
|
2795
|
+
if (stageData.staleReason) console.log(` 原因:${stageData.staleReason}`)
|
|
2796
|
+
console.log(` 建议:sillyspec run ${stageName} --reopen --from-step 1`)
|
|
2797
|
+
}
|
|
2798
|
+
if (stageData.status === 'completed') {
|
|
2799
|
+
console.log(`\n✅ 已完成`)
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
console.log('')
|
|
2523
2803
|
|
|
2524
2804
|
const firstPending = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
2525
2805
|
|
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
|
|