sillyspec 3.20.6 → 3.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +23 -0
- package/.claude/skills/sillyspec-archive/SKILL.md +86 -21
- package/.claude/skills/sillyspec-auto/SKILL.md +98 -83
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +78 -44
- package/.claude/skills/sillyspec-doctor/SKILL.md +61 -31
- package/.claude/skills/sillyspec-execute/SKILL.md +90 -30
- package/.claude/skills/sillyspec-explore/SKILL.md +96 -109
- package/.claude/skills/sillyspec-export/SKILL.md +4 -0
- package/.claude/skills/sillyspec-init/SKILL.md +7 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +74 -21
- package/.claude/skills/sillyspec-propose/SKILL.md +61 -21
- package/.claude/skills/sillyspec-quick/SKILL.md +84 -21
- package/.claude/skills/sillyspec-scan/SKILL.md +74 -21
- package/.claude/skills/sillyspec-state/SKILL.md +11 -1
- package/.claude/skills/sillyspec-status/SKILL.md +55 -21
- package/.claude/skills/sillyspec-verify/SKILL.md +82 -21
- package/.claude/skills/sillyspec-workspace/SKILL.md +12 -0
- package/CLAUDE.md +4 -0
- package/package.json +2 -2
- package/src/change-list.js +51 -16
- package/src/contract-matrix.js +67 -0
- package/src/doctor-diagnostics.js +575 -0
- package/src/hooks/worktree-guard.js +111 -111
- package/src/index.js +154 -82
- package/src/progress.js +59 -9
- package/src/run.js +139 -38
- package/src/stage-contract.js +14 -2
- package/src/stages/execute.js +37 -18
- package/src/stages/plan-postcheck.js +234 -0
- package/src/stages/plan.js +13 -0
- package/src/sync.js +13 -3
- package/src/worktree.js +69 -28
- package/docs/brainstorm-plan-contract.md +0 -64
- package/docs/plan-execute-contract.md +0 -123
- package/docs/platform-scan-protocol.md +0 -298
- package/docs/revision-mode.md +0 -115
- package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +0 -99
- package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +0 -223
- package/docs/sillyspec/file-lifecycle/stage-artifacts.md +0 -167
- package/docs/sillyspec/file-lifecycle/storage-and-state.md +0 -148
- package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +0 -211
- package/docs/sillyspec/file-lifecycle.md +0 -132
- package/docs/workflow-contract-regression.md +0 -106
- package/docs/worktree-isolation.md +0 -252
- package/test/brainstorm-plan-contract.test.mjs +0 -273
- package/test/check-syntax.mjs +0 -26
- package/test/cli-top-level-aliases.test.mjs +0 -174
- package/test/contract-artifacts.test.mjs +0 -323
- package/test/decision-ref-version.mjs +0 -85
- package/test/decision-supersede.test.mjs +0 -277
- package/test/knowledge-match.test.mjs +0 -231
- package/test/plan-execute-contract.test.mjs +0 -357
- package/test/plan-optimization.test.mjs +0 -572
- package/test/platform-artifacts.test.mjs +0 -190
- package/test/platform-failure-samples.test.mjs +0 -199
- package/test/platform-recovery-chain.test.mjs +0 -179
- package/test/platform-recovery.test.mjs +0 -167
- package/test/platform-scan-p0.test.mjs +0 -186
- package/test/quick-recommend.test.mjs +0 -146
- package/test/revision-v1.test.mjs +0 -1145
- package/test/run-sanitize-project-name.test.mjs +0 -51
- package/test/run-scan-postcheck-fail.test.mjs +0 -64
- package/test/run-scan-project-parse.test.mjs +0 -200
- package/test/run-tests.mjs +0 -48
- package/test/runtime-cleanup-keeps-worktree.test.mjs +0 -107
- package/test/scan-docs-yaml-placeholders.test.mjs +0 -84
- package/test/scan-knowledge.test.mjs +0 -175
- package/test/scan-paths.test.mjs +0 -68
- package/test/scan-postcheck-project-priority.test.mjs +0 -85
- package/test/scan-postcheck.test.mjs +0 -197
- package/test/scan-workflow-anyfailed-block.test.mjs +0 -52
- package/test/spec-dir.test.mjs +0 -206
- package/test/stage-contract-failed-post-check.test.mjs +0 -102
- package/test/stage-contract.test.mjs +0 -301
- package/test/stage-definitions.test.mjs +0 -39
- package/test/wait-gates.test.mjs +0 -501
- package/test/workflow-spec-base.test.mjs +0 -142
- package/test/worktree-deps-provision.test.mjs +0 -148
- package/test/worktree-guard.test.mjs +0 -136
- package/test/worktree-native-overlay.test.mjs +0 -188
package/src/run.js
CHANGED
|
@@ -1341,7 +1341,18 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1341
1341
|
inputText = flags[inputIdx + 1]
|
|
1342
1342
|
}
|
|
1343
1343
|
|
|
1344
|
-
// 解析 --
|
|
1344
|
+
// 解析 --linked-changes <a,b|none>(quick 专用:显式声明关联变更,CI/脚本友好)
|
|
1345
|
+
// 与 --change 解耦:--linked-changes 语义清晰(关联变更),不与「指定变更名」混淆。
|
|
1346
|
+
// null = 未指定(走持久化/交互/兼容回退);[] = 显式 none(不关联);[...] = 显式列表
|
|
1347
|
+
let explicitLinked = null
|
|
1348
|
+
const linkedIdx = flags.indexOf('--linked-changes')
|
|
1349
|
+
if (linkedIdx !== -1 && flags[linkedIdx + 1]) {
|
|
1350
|
+
const v = flags[linkedIdx + 1].trim()
|
|
1351
|
+
explicitLinked = v.toLowerCase() === 'none' ? [] : v.split(',').map(s => s.trim()).filter(Boolean)
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// 解析 --change <name>(quick 阶段向后兼容:逗号分隔作为「关联变更」;
|
|
1355
|
+
// 历史写法,语义与「指定变更名」冲突,新用法建议改用 --linked-changes)
|
|
1345
1356
|
let changeName = null
|
|
1346
1357
|
let linkedChanges = []
|
|
1347
1358
|
const changeIdx = flags.indexOf('--change')
|
|
@@ -1352,6 +1363,10 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1352
1363
|
changeName = null
|
|
1353
1364
|
}
|
|
1354
1365
|
}
|
|
1366
|
+
// --linked-changes 优先于 --change(显式 > 隐式兼容)
|
|
1367
|
+
if (explicitLinked !== null) {
|
|
1368
|
+
linkedChanges = explicitLinked
|
|
1369
|
+
}
|
|
1355
1370
|
// quick 的 progress 键固定走 'default':归属语义已由 linkedChanges 承担(→ quick-guard.json
|
|
1356
1371
|
// → <linked-changes> 占位符注入 prompt)。progress 不能走 pm.read(cwd, null) 的自动检测——
|
|
1357
1372
|
// 多活跃 change 项目里 quick 步骤进度会随活跃 change 增减在各 change/default 间漂移,
|
|
@@ -1376,7 +1391,7 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1376
1391
|
'--done', '--skip', '--status', '--reset', '--confirm', '--skip-approval',
|
|
1377
1392
|
'--wait', '--continue', '--non-interactive', '--interactive',
|
|
1378
1393
|
'--reason', '--options', '--answer', '--confirm-mode',
|
|
1379
|
-
'--output', '--input', '--change',
|
|
1394
|
+
'--output', '--input', '--change', '--linked-changes',
|
|
1380
1395
|
'--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
|
|
1381
1396
|
'--files', '--allow-new', '--force-baseline', '--force-rescan',
|
|
1382
1397
|
'--json', '--dir', '--help',
|
|
@@ -1400,13 +1415,28 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1400
1415
|
|
|
1401
1416
|
const pm = new ProgressManager({ specDir: specRoot })
|
|
1402
1417
|
|
|
1403
|
-
// quick
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1418
|
+
// quick 阶段:确定关联变更
|
|
1419
|
+
// 优先级:--linked-changes / --change 显式 > 已持久化 quick-guard.json(--done 复用)> 交互式 > 非交互 fallback
|
|
1420
|
+
// 关键:--done 收尾时复用首次 run 持久化的 linkedChanges,不在管道/CI 下重复弹交互 prompt。
|
|
1421
|
+
// explicitLinked === null 且未传 --change 时才进入此分支(显式 --linked-changes none 不应触发交互)。
|
|
1422
|
+
if (stageName === 'quick' && explicitLinked === null && linkedChanges.length === 0) {
|
|
1423
|
+
const guardFile = join(specRoot, '.runtime', 'quick-guard.json')
|
|
1424
|
+
let persistedLinked = null
|
|
1425
|
+
try {
|
|
1426
|
+
if (existsSync(guardFile)) {
|
|
1427
|
+
const g = JSON.parse(readFileSync(guardFile, 'utf8'))
|
|
1428
|
+
if (Array.isArray(g.linkedChanges)) persistedLinked = g.linkedChanges
|
|
1429
|
+
}
|
|
1430
|
+
} catch {}
|
|
1431
|
+
if (persistedLinked) {
|
|
1432
|
+
linkedChanges = persistedLinked
|
|
1433
|
+
} else {
|
|
1434
|
+
linkedChanges = await resolveQuickLinkedChanges({
|
|
1435
|
+
pm, cwd, specDir: specRoot, quickFiles,
|
|
1436
|
+
taskDescription: inputText || '',
|
|
1437
|
+
nonInteractive: isNonInteractive,
|
|
1438
|
+
})
|
|
1439
|
+
}
|
|
1410
1440
|
}
|
|
1411
1441
|
|
|
1412
1442
|
let progress = await pm.read(cwd, changeName)
|
|
@@ -1607,7 +1637,7 @@ async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles, taskDes
|
|
|
1607
1637
|
if (activeChanges.length === 0) return []
|
|
1608
1638
|
if (activeChanges.length === 1) return [activeChanges[0]]
|
|
1609
1639
|
|
|
1610
|
-
if (nonInteractive || process.stdin.isTTY
|
|
1640
|
+
if (nonInteractive || !process.stdin.isTTY) {
|
|
1611
1641
|
console.log('💡 非交互环境,已默认不关联变更;如需关联请用 --change a,b')
|
|
1612
1642
|
return []
|
|
1613
1643
|
}
|
|
@@ -1702,7 +1732,10 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
1702
1732
|
console.log(`🔗 worktree 已创建: ${result.worktreePath} (分支: ${result.branch}, 模式: ${result.mode})`)
|
|
1703
1733
|
} catch (e) {
|
|
1704
1734
|
console.error(`❌ worktree 创建失败: ${e.message}`)
|
|
1705
|
-
console.error(`
|
|
1735
|
+
console.error(` 修复建议:`)
|
|
1736
|
+
console.error(` 1. 运行 sillyspec worktree doctor --fix 检查并修复 worktree 状态`)
|
|
1737
|
+
console.error(` 2. 或手动清理残留:git worktree prune && git branch -D sillyspec/${effectiveChange}`)
|
|
1738
|
+
console.error(` 3. 必要时删除残留目录 .sillyspec/.runtime/worktrees/${effectiveChange}/ 后重试`)
|
|
1706
1739
|
process.exit(1)
|
|
1707
1740
|
}
|
|
1708
1741
|
}
|
|
@@ -2024,6 +2057,13 @@ async function archiveChangeDirectory(pm, cwd, progress, specBase) {
|
|
|
2024
2057
|
console.error(`❌ 归档失败:源目录不存在 ${srcDir}`)
|
|
2025
2058
|
process.exit(1)
|
|
2026
2059
|
}
|
|
2060
|
+
// 移动前硬校验:变更包必须含 plan.md,否则不该归档。
|
|
2061
|
+
// 在移动前阻断(而非移动后),目录尚未动,用户可直接修复后重试。
|
|
2062
|
+
if (!existsSync(join(srcDir, 'plan.md'))) {
|
|
2063
|
+
console.error(`❌ 归档失败:变更目录缺少 plan.md(${srcDir})`)
|
|
2064
|
+
console.error(` plan.md 是归档的必需产物。请先补全 plan 阶段产出再归档。`)
|
|
2065
|
+
process.exit(1)
|
|
2066
|
+
}
|
|
2027
2067
|
if (existsSync(destDir)) {
|
|
2028
2068
|
console.error(`❌ 归档失败:目标目录已存在 ${destDir}`)
|
|
2029
2069
|
process.exit(1)
|
|
@@ -2037,7 +2077,32 @@ async function archiveChangeDirectory(pm, cwd, progress, specBase) {
|
|
|
2037
2077
|
}
|
|
2038
2078
|
|
|
2039
2079
|
await pm.unregisterChange(cwd, archiveChangeName)
|
|
2080
|
+
|
|
2081
|
+
// 归档时清理可能残留的 worktree(execute 自动清理未走到 / 有未 apply 变更被遗弃)。
|
|
2082
|
+
// 安全策略:有未 apply 变更时保留 worktree 并警告,避免误删用户未应用的代码。
|
|
2083
|
+
try {
|
|
2084
|
+
const { WorktreeManager } = await import('./worktree.js')
|
|
2085
|
+
const wm = new WorktreeManager({ cwd })
|
|
2086
|
+
const meta = wm.getMeta(archiveChangeName)
|
|
2087
|
+
if (meta) {
|
|
2088
|
+
const check = meta.mode !== 'in-place-fallback' ? wm.hasUnappliedChanges(archiveChangeName) : { hasChanges: false }
|
|
2089
|
+
if (check.hasChanges) {
|
|
2090
|
+
console.warn(`⚠️ 归档时 worktree 仍有 ${check.changedFiles.length} 个未 apply 变更,保留 worktree`)
|
|
2091
|
+
console.warn(` 确认不需要后手动清理: sillyspec worktree cleanup ${archiveChangeName} --force`)
|
|
2092
|
+
} else {
|
|
2093
|
+
const cleanResult = wm.cleanup(archiveChangeName)
|
|
2094
|
+
if (cleanResult.residual?.length > 0) {
|
|
2095
|
+
console.warn(`⚠️ 归档 worktree 清理残留: ${cleanResult.residual.join('; ')}`)
|
|
2096
|
+
console.warn(` 手动处理: sillyspec worktree cleanup ${archiveChangeName} --force`)
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
} catch (e) {
|
|
2101
|
+
console.warn(`⚠️ 归档 worktree 清理失败(不阻断归档): ${e.message}`)
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2040
2104
|
console.log(`📦 已归档:${archiveChangeName} → archive/${date}-${archiveChangeName}/`)
|
|
2105
|
+
return destDir
|
|
2041
2106
|
}
|
|
2042
2107
|
|
|
2043
2108
|
// ── Wait Step ──
|
|
@@ -2249,9 +2314,8 @@ async function continueStep(pm, progress, stageName, cwd, answer, options = {})
|
|
|
2249
2314
|
console.log('🔗 Worktree: n/a (no meta)');
|
|
2250
2315
|
} else if (meta.mode === 'native-worktree') {
|
|
2251
2316
|
console.log('🔗 Worktree: kept (外部隔离环境)');
|
|
2252
|
-
} else if (meta.mode === 'in-place-fallback') {
|
|
2253
|
-
console.log('🔗 Worktree: n/a (in-place 模式)');
|
|
2254
2317
|
} else {
|
|
2318
|
+
// in-place 模式不再短路:cleanup 现在能安全处理 in-place(只清 meta,不碰主工作区)
|
|
2255
2319
|
const check = wm.hasUnappliedChanges(changeName);
|
|
2256
2320
|
if (check.hasChanges) {
|
|
2257
2321
|
console.log(`🔗 Worktree: pending apply (${check.changedFiles.length} 个未应用变更)`);
|
|
@@ -2259,6 +2323,10 @@ async function continueStep(pm, progress, stageName, cwd, answer, options = {})
|
|
|
2259
2323
|
} else {
|
|
2260
2324
|
const cleanResult = wm.cleanup(changeName);
|
|
2261
2325
|
console.log(`🔗 Worktree: ${cleanResult.result}`);
|
|
2326
|
+
if (cleanResult.residual?.length > 0) {
|
|
2327
|
+
console.warn(` ⚠️ 清理残留: ${cleanResult.residual.join('; ')}`);
|
|
2328
|
+
console.warn(` 手动处理: sillyspec worktree cleanup ${changeName} --force`);
|
|
2329
|
+
}
|
|
2262
2330
|
}
|
|
2263
2331
|
}
|
|
2264
2332
|
} catch (e) {
|
|
@@ -2375,6 +2443,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2375
2443
|
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
2376
2444
|
const stageData = progress.stages[stageName]
|
|
2377
2445
|
const scanProfile = stageData?.scanProfile || null
|
|
2446
|
+
let archivedDir = null // archive step「确认归档」实际归档目录(archiveChangeDirectory 返回)
|
|
2378
2447
|
|
|
2379
2448
|
// ── WAIT MARKER 硬校验 ──
|
|
2380
2449
|
// 如果 output 包含等待标记,拒绝 --done 推进
|
|
@@ -2462,23 +2531,22 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2462
2531
|
console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
|
|
2463
2532
|
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
2464
2533
|
}
|
|
2465
|
-
await archiveChangeDirectory(pm, cwd, progress, specBase)
|
|
2534
|
+
archivedDir = await archiveChangeDirectory(pm, cwd, progress, specBase)
|
|
2466
2535
|
}
|
|
2467
2536
|
|
|
2468
2537
|
// archive "确认归档" 步骤完成后,校验归档完整性
|
|
2469
2538
|
if (stageName === 'archive' && steps[currentIdx]?.name === '确认归档' && confirm) {
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
console.warn(` - ${w}`)
|
|
2539
|
+
// contracts.archive.validators 为空数组(两个 validator 生效窗口互斥,注册为阶段级会误报),
|
|
2540
|
+
// 故 runValidators('archive') 是空跑。这里直接对实际归档目录做内容完整性校验。
|
|
2541
|
+
// plan.md 已在 archiveChangeDirectory 移动前硬校验阻断,此处只查推荐文档。
|
|
2542
|
+
if (archivedDir && existsSync(archivedDir)) {
|
|
2543
|
+
const recommendedDocs = ['design.md', 'module-impact.md']
|
|
2544
|
+
const missingRecommended = recommendedDocs.filter(d => !existsSync(join(archivedDir, d)))
|
|
2545
|
+
if (missingRecommended.length > 0) {
|
|
2546
|
+
console.warn(`\n⚠️ 归档校验警告:归档目录缺少推荐文档`)
|
|
2547
|
+
for (const d of missingRecommended) console.warn(` - ${d}(${archivedDir})`)
|
|
2548
|
+
} else {
|
|
2549
|
+
console.log(`\n✅ 归档校验通过:核心文档齐全`)
|
|
2482
2550
|
}
|
|
2483
2551
|
}
|
|
2484
2552
|
}
|
|
@@ -2936,10 +3004,11 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2936
3004
|
// summary 失败不影响主流程
|
|
2937
3005
|
console.log('\n👉 下一步:sillyspec run verify(验证通过后才能归档)')
|
|
2938
3006
|
}
|
|
2939
|
-
} else if (stageName === 'verify') {
|
|
2940
|
-
console.log('\n👉 下一步:sillyspec run archive(验证通过,可以归档了)')
|
|
2941
3007
|
} else if (stageName === 'archive') {
|
|
2942
3008
|
console.log('\n👉 归档完成!现在可以提交了:git commit -m "..."')
|
|
3009
|
+
} else if (stageName === 'verify') {
|
|
3010
|
+
// verify 的"验证通过"提示延后到下方 validator 通过后才打印,
|
|
3011
|
+
// 避免校验失败(FAIL / 缺 verify-result.md)时仍声称"验证通过可以归档"。
|
|
2943
3012
|
} else {
|
|
2944
3013
|
console.log(`\n下一步由你决定:sillyspec run <stage>(brainstorm/plan/execute/verify/archive 等)`)
|
|
2945
3014
|
}
|
|
@@ -2954,6 +3023,13 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2954
3023
|
console.error(` - ${err}`)
|
|
2955
3024
|
}
|
|
2956
3025
|
console.error(`\n 提示:修复缺失产物后重新运行此步骤,或使用 --skip-approval 跳过校验`)
|
|
3026
|
+
// 产物校验失败必须阻断完成 —— 否则 validator 形同虚设,
|
|
3027
|
+
// verify 会带着 FAIL/缺 verify-result.md 被 ✅ 标记完成(历史教训)。
|
|
3028
|
+
// plan/execute 的专项契约校验(下方)在产物齐全后才需要继续跑,故此处先 return。
|
|
3029
|
+
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
3030
|
+
await pm._write(cwd, progress, changeName)
|
|
3031
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
3032
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
2957
3033
|
}
|
|
2958
3034
|
if (contractResult.warnings.length > 0) {
|
|
2959
3035
|
console.warn(`\n⚠️ 阶段 ${stageName} 校验警告:`)
|
|
@@ -2962,6 +3038,11 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2962
3038
|
}
|
|
2963
3039
|
}
|
|
2964
3040
|
|
|
3041
|
+
// verify 产物校验通过 + 结论非 FAIL(否则上面已阻断),此时才能声称"验证通过"
|
|
3042
|
+
if (stageName === 'verify') {
|
|
3043
|
+
console.log('\n✅ 验证通过,下一步:sillyspec run archive')
|
|
3044
|
+
}
|
|
3045
|
+
|
|
2965
3046
|
// ── Plan postcheck contract:plan.md 必须满足 execute 契约 ──
|
|
2966
3047
|
if (stageName === 'plan') {
|
|
2967
3048
|
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
@@ -3061,23 +3142,21 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
3061
3142
|
console.log('🔗 Worktree: n/a (no meta)');
|
|
3062
3143
|
} else if (meta.mode === 'native-worktree') {
|
|
3063
3144
|
console.log('🔗 Worktree: kept (外部隔离环境)');
|
|
3064
|
-
} else if (meta.mode === 'in-place-fallback') {
|
|
3065
|
-
console.log('🔗 Worktree: n/a (in-place 模式)');
|
|
3066
3145
|
} else {
|
|
3146
|
+
// in-place 模式不再短路:cleanup 现在能安全处理 in-place(只清 meta,不碰主工作区)
|
|
3067
3147
|
const check = wm.hasUnappliedChanges(changeName);
|
|
3068
3148
|
if (check.hasChanges) {
|
|
3069
3149
|
console.log(`🔗 Worktree: pending apply (${check.changedFiles.length} 个未应用变更)`);
|
|
3070
3150
|
console.log(` 下一步: sillyspec worktree apply ${changeName}`);
|
|
3071
3151
|
} else {
|
|
3072
3152
|
const cleanResult = wm.cleanup(changeName);
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
console.
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
}
|
|
3153
|
+
console.log(`🔗 Worktree: ${cleanResult.result}`);
|
|
3154
|
+
if (cleanResult.residual?.length > 0) {
|
|
3155
|
+
console.warn(` ⚠️ 清理残留: ${cleanResult.residual.join('; ')}`);
|
|
3156
|
+
console.warn(` 手动处理: sillyspec worktree cleanup ${changeName} --force`);
|
|
3157
|
+
} else if (cleanResult.details?.length > 0) {
|
|
3158
|
+
for (const d of cleanResult.details) {
|
|
3159
|
+
if (d.startsWith('⚠️')) console.log(` ${d}`);
|
|
3081
3160
|
}
|
|
3082
3161
|
}
|
|
3083
3162
|
}
|
|
@@ -3333,6 +3412,28 @@ function showStatus(progress, stageName) {
|
|
|
3333
3412
|
}
|
|
3334
3413
|
|
|
3335
3414
|
async function resetStage(pm, progress, stageName, cwd, changeName, platformOpts = {}) {
|
|
3415
|
+
// execute 阶段 reset 时清理自建 worktree,否则下次 run execute 会因 existingMeta 存在
|
|
3416
|
+
// 直接复用带脏状态的旧 worktree(启动逻辑:meta 存在即复用,不查健康状态)
|
|
3417
|
+
if (stageName === 'execute' && changeName) {
|
|
3418
|
+
try {
|
|
3419
|
+
const { WorktreeManager } = await import('./worktree.js')
|
|
3420
|
+
const wm = new WorktreeManager({ cwd })
|
|
3421
|
+
const meta = wm.getMeta(changeName)
|
|
3422
|
+
if (meta) {
|
|
3423
|
+
const cleanResult = wm.cleanup(changeName)
|
|
3424
|
+
if (cleanResult.residual?.length > 0) {
|
|
3425
|
+
console.warn(`⚠️ reset 清理 worktree 残留: ${cleanResult.residual.join('; ')}`)
|
|
3426
|
+
console.warn(` 手动处理: sillyspec worktree cleanup ${changeName} --force`)
|
|
3427
|
+
} else if (cleanResult.result === 'kept') {
|
|
3428
|
+
console.log(`🔗 旧 worktree 保留 (${cleanResult.mode}: 外部隔离环境)`)
|
|
3429
|
+
} else if (cleanResult.result !== 'skipped') {
|
|
3430
|
+
console.log(`🧹 已清理旧 worktree (${cleanResult.result}, mode: ${cleanResult.mode}),下次 execute 将重建干净环境`)
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
} catch (e) {
|
|
3434
|
+
console.warn(`⚠️ reset 清理 worktree 失败(不阻断 reset): ${e.message}`)
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3336
3437
|
const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
3337
3438
|
progress.stages[stageName] = {
|
|
3338
3439
|
status: 'in-progress',
|
package/src/stage-contract.js
CHANGED
|
@@ -391,10 +391,11 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
391
391
|
return { ok: false, errors, warnings }
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
-
// verify
|
|
394
|
+
// verify 阶段必须产出 verify-result.md —— 不存在则不能完成。
|
|
395
|
+
// 历史教训:AI 可能跳过报告直接 --done,导致"假完成"。此处提级为 error 强制阻断。
|
|
395
396
|
const verifyResult = join(changeDir, 'verify-result.md')
|
|
396
397
|
if (!existsSync(verifyResult)) {
|
|
397
|
-
|
|
398
|
+
errors.push(`verify-result.md 不存在 — verify 阶段必须产出验证报告才能完成(${verifyResult})`)
|
|
398
399
|
}
|
|
399
400
|
|
|
400
401
|
// 确保核心规范文件仍然存在
|
|
@@ -415,6 +416,17 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
415
416
|
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
416
417
|
warnMissingIds(warnings, decisionIds, verify, 'verify-result.md', 'decisions.md')
|
|
417
418
|
|
|
419
|
+
// ── FAIL 结论门控(适用于所有变更,不限风险等级)──
|
|
420
|
+
// verify-result.md 结论为 FAIL 时,verify 阶段不能 completed。
|
|
421
|
+
// 历史教训:CLI 曾不校验结论,AI 写 FAIL 后 verify 仍被标记完成并提示"验证通过可以归档"。
|
|
422
|
+
const conclusionLine = verify.match(/^##\s*结论\s*\n\s*(PASS(?:\s+WITH\s+NOTES)?|FAIL)/im)
|
|
423
|
+
const conclusionStr = conclusionLine ? conclusionLine[1].toUpperCase().replace(/\s+/g, ' ') : ''
|
|
424
|
+
if (conclusionStr === 'FAIL') {
|
|
425
|
+
errors.push('verify-result.md 结论为 FAIL — 验证未通过,不能标记 verify 完成;请修复后重新运行验证')
|
|
426
|
+
} else if (!conclusionStr) {
|
|
427
|
+
warnings.push('verify-result.md 未识别到「## 结论」章节(应为 PASS / PASS WITH NOTES / FAIL)')
|
|
428
|
+
}
|
|
429
|
+
|
|
418
430
|
// ── P0: Change Risk Gate — 核心功能缺少真实集成验证时 FAIL ──
|
|
419
431
|
const changeRiskProfile = detectChangeRisk({
|
|
420
432
|
designContent: readIfExists(join(changeDir, 'design.md')),
|
package/src/stages/execute.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs'
|
|
2
2
|
import path from 'path'
|
|
3
|
+
import { buildContractMatrix, buildConsumerInjection, buildContractFieldInjection } from '../contract-matrix.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* 校验 plan.md 是否满足 execute 执行契约
|
|
@@ -430,42 +431,60 @@ function buildWavePrompt(wave, waveIndex, changeDir, worktreePath) {
|
|
|
430
431
|
let contractInjection = ''
|
|
431
432
|
if (changeDir) {
|
|
432
433
|
try {
|
|
433
|
-
const { buildContractMatrix, buildConsumerInjection } = require('../contract-matrix.js')
|
|
434
434
|
const planFile = path.join(changeDir, 'plan.md')
|
|
435
435
|
if (existsSync(planFile)) {
|
|
436
436
|
const planContent = readFileSync(planFile, 'utf8')
|
|
437
|
+
// 收集本 wave 所有 task(端点注入与字段注入共用)
|
|
438
|
+
const waveTasks = wave.tasks.map((t, ti) => {
|
|
439
|
+
const num = String(t.index || (ti + 1)).padStart(2, '0')
|
|
440
|
+
return `task-${num}`
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
// 1) 端点级契约(provider/consumer via buildContractMatrix)
|
|
437
444
|
const contracts = buildContractMatrix(planContent, changeDir)
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const num = String(t.index || (ti + 1)).padStart(2, '0')
|
|
442
|
-
return `task-${num}`
|
|
443
|
-
})
|
|
444
|
-
const relevantContracts = contracts.filter(c => waveTasks.includes(c.consumer))
|
|
445
|
-
if (relevantContracts.length > 0) {
|
|
446
|
-
contractInjection = `
|
|
445
|
+
const relevantContracts = contracts.filter(c => waveTasks.includes(c.consumer))
|
|
446
|
+
if (relevantContracts.length > 0) {
|
|
447
|
+
contractInjection = `
|
|
447
448
|
### API Contract Matrix
|
|
448
449
|
本 Wave 存在前端/后端跨 task 契约:
|
|
449
450
|
${relevantContracts.map(c => `- **${c.consumer}** 消费 **${c.provider}** 产出的 API`).join('\n')}
|
|
450
451
|
`
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
### 子代理 ${taskName} 的契约注入
|
|
452
|
+
for (const taskName of waveTasks) {
|
|
453
|
+
const injection = buildConsumerInjection(changeDir, path.join(changeDir, '..', '..'), taskName, contracts)
|
|
454
|
+
if (injection) {
|
|
455
|
+
contractInjection += `
|
|
456
|
+
### 子代理 ${taskName} 的端点契约注入
|
|
457
457
|
为 ${taskName} 启动子代理时,在子代理 prompt 末尾追加以下内容:
|
|
458
458
|
|
|
459
459
|
<contract-injection>
|
|
460
460
|
${injection}
|
|
461
461
|
</contract-injection>
|
|
462
462
|
`
|
|
463
|
-
}
|
|
464
463
|
}
|
|
465
464
|
}
|
|
466
465
|
}
|
|
466
|
+
|
|
467
|
+
// 2) 字段级契约(expects_from ↔ provides)
|
|
468
|
+
// 命中「provider 漏字段、consumer fallback 编造 → 运行时 403/500」这类 bug:
|
|
469
|
+
// 把 consumer 期望字段 vs provider 承诺字段显式注入子代理 prompt
|
|
470
|
+
for (const taskName of waveTasks) {
|
|
471
|
+
const fi = buildContractFieldInjection(changeDir, taskName)
|
|
472
|
+
if (fi) {
|
|
473
|
+
contractInjection += `
|
|
474
|
+
### 子代理 ${taskName} 的字段契约注入
|
|
475
|
+
为 ${taskName} 启动子代理时,在子代理 prompt 末尾追加以下内容:
|
|
476
|
+
|
|
477
|
+
<contract-field-injection>
|
|
478
|
+
${fi}
|
|
479
|
+
</contract-field-injection>
|
|
480
|
+
`
|
|
481
|
+
}
|
|
482
|
+
}
|
|
467
483
|
}
|
|
468
|
-
} catch {
|
|
484
|
+
} catch (e) {
|
|
485
|
+
// 契约注入是 best-effort:失败不阻断 execute,只记录
|
|
486
|
+
console.warn(` ⚠️ 契约注入跳过: ${e?.message || e}`)
|
|
487
|
+
}
|
|
469
488
|
}
|
|
470
489
|
|
|
471
490
|
// 构建任务摘要(不再内联完整蓝图,减少上下文污染)
|