sillyspec 3.24.8 → 3.25.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/package.json +1 -1
- package/src/constants.js +13 -0
- package/src/contract-matrix.js +48 -20
- package/src/endpoint-extractor.js +92 -6
- package/src/hooks/worktree-guard.js +16 -4
- package/src/index.js +1 -1
- package/src/progress.js +26 -46
- package/src/run.js +550 -449
- package/src/stage-contract.js +7 -9
- package/src/stages/index.js +1 -1
- package/src/verify-postcheck.js +41 -0
- package/src/worktree-apply.js +45 -18
- package/src/worktree.js +13 -5
package/src/run.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* CLI 成为流程引擎,AI 变成步骤执行器。
|
|
5
5
|
* 支持多变更并行:每个变更状态存储在 sillyspec.db 中。
|
|
6
6
|
*/
|
|
7
|
-
import { basename, join, resolve, dirname, relative, isAbsolute } from 'path'
|
|
7
|
+
import { basename, join, resolve, dirname, relative, isAbsolute, extname } from 'path'
|
|
8
8
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, rmSync, statSync } from 'fs'
|
|
9
9
|
import { writeAtomicSync, renameSyncRetry } from './fs-atomic.js'
|
|
10
10
|
import { randomBytes, randomUUID } from 'crypto'
|
|
@@ -895,10 +895,14 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
895
895
|
if (platformOpts.workspaceId) {
|
|
896
896
|
platformDirectives.push(`workspace_id: ${platformOpts.workspaceId}`)
|
|
897
897
|
}
|
|
898
|
-
// 平台 directives
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
|
|
898
|
+
// 平台 directives 前置条件:
|
|
899
|
+
// - step0(对齐 persona 仅 step0 策略):路径列表/Write 规则/占位符映射一次建立即可;
|
|
900
|
+
// - scan 阶段所有 step:scan 的 profileDirectives 本就每步注入(见下方 L917),平台路径
|
|
901
|
+
// 约束也需每步可见——否则 quick profile 的 step0 是 noAI preflight(自动执行),run scan
|
|
902
|
+
// 输出的首个可见 step 是 step1,会漏掉 specDir 路径,导致平台模式写入约束丢失。
|
|
903
|
+
// 后续 step 靠 changeDir header + footer 平台规则(L1077+)维持,platformDirectives
|
|
904
|
+
// 每步重复约 500 tokens 仅在 scan(步数有限)可接受。
|
|
905
|
+
if (stepIndex === 0 || stageName === 'scan') {
|
|
902
906
|
promptText = platformDirectives.join('\n') + '\n\n' + promptText
|
|
903
907
|
}
|
|
904
908
|
} else {
|
|
@@ -1116,6 +1120,36 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
1116
1120
|
console.log(`sillyspec run ${stageName} --done${changeFlag} --input "用户原始需求/反馈" --output "你的摘要"`)
|
|
1117
1121
|
}
|
|
1118
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* 估算源码规模(文件数 + 字节数)—— 跨平台 Node 遍历,替代 Unix `find`(原生 Windows 无 find,
|
|
1125
|
+
* 原 find 失效会让所有 Windows 项目永远 fallback 到 standard profile)。
|
|
1126
|
+
* 扩展名与排除目录与原 find 命令逐字一致;maxFiles 封顶避免超大仓库遍历过久
|
|
1127
|
+
* (超限时 fileCount 已远超 quick/standard 阈值,不影响 profile 判定)。
|
|
1128
|
+
*/
|
|
1129
|
+
function estimateSourceSize(cwd, maxFiles = 5000) {
|
|
1130
|
+
const sourceExts = new Set(['.js','.ts','.tsx','.py','.java','.go','.rs','.rb','.php','.c','.cpp','.h','.jsx','.vue','.svelte'])
|
|
1131
|
+
const skipDirs = new Set(['node_modules','.git','dist','build','__pycache__','.sillyspec','.claude'])
|
|
1132
|
+
let fileCount = 0
|
|
1133
|
+
let sourceBytes = 0
|
|
1134
|
+
const stack = [cwd]
|
|
1135
|
+
while (stack.length) {
|
|
1136
|
+
const dir = stack.pop()
|
|
1137
|
+
let entries
|
|
1138
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) }
|
|
1139
|
+
catch { continue }
|
|
1140
|
+
for (const e of entries) {
|
|
1141
|
+
if (e.isDirectory()) {
|
|
1142
|
+
if (!skipDirs.has(e.name)) stack.push(join(dir, e.name))
|
|
1143
|
+
} else if (e.isFile() && sourceExts.has(extname(e.name))) {
|
|
1144
|
+
fileCount++
|
|
1145
|
+
try { sourceBytes += statSync(join(dir, e.name)).size } catch {}
|
|
1146
|
+
if (fileCount >= maxFiles) return { fileCount, sourceBytes }
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return { fileCount, sourceBytes }
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1119
1153
|
/**
|
|
1120
1154
|
* 根据 project 规模计算 scan profile
|
|
1121
1155
|
* quick: fileCount≤30 && sourceBytes≤80KB && projectCount≤3 → 3 步,0 子代理,5 份文档
|
|
@@ -1138,19 +1172,15 @@ function computeScanProfile(cwd, platformOpts) {
|
|
|
1138
1172
|
}
|
|
1139
1173
|
} catch {}
|
|
1140
1174
|
|
|
1141
|
-
//
|
|
1175
|
+
// 快速估算源码规模(Node 遍历,跨平台;原 Unix find 在原生 Windows 上失效会永远 fallback standard)
|
|
1142
1176
|
let fileCount = 0
|
|
1143
1177
|
let sourceBytes = 0
|
|
1144
1178
|
try {
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
fileCount = files.length
|
|
1149
|
-
for (const f of files) {
|
|
1150
|
-
try { sourceBytes += statSync(f).size } catch {}
|
|
1151
|
-
}
|
|
1179
|
+
const est = estimateSourceSize(cwd)
|
|
1180
|
+
fileCount = est.fileCount
|
|
1181
|
+
sourceBytes = est.sourceBytes
|
|
1152
1182
|
} catch {
|
|
1153
|
-
//
|
|
1183
|
+
// 遍历异常时假设中等规模
|
|
1154
1184
|
return { mode: 'standard', reason: '无法估算项目规模', maxAgentCalls: 1, maxDocs: 8 }
|
|
1155
1185
|
}
|
|
1156
1186
|
|
|
@@ -1333,6 +1363,24 @@ async function executePlanPostcheck(cwd, platformOpts, progress) {
|
|
|
1333
1363
|
})
|
|
1334
1364
|
}
|
|
1335
1365
|
|
|
1366
|
+
/**
|
|
1367
|
+
* 替换 prompt 文本中的路径根占位符 {SPEC_ROOT}/{DOCS_ROOT}/{PROJECTS_ROOT}/
|
|
1368
|
+
* {WORKFLOWS_ROOT}/{KNOWLEDGE_ROOT}。平台模式与常规模式共用:仅传入的 roots 值不同。
|
|
1369
|
+
* 占位符值都是绝对路径、互不包含其它占位符,故替换顺序不影响结果。
|
|
1370
|
+
*
|
|
1371
|
+
* 注:本函数由并行会话引入(outputStep 两处逐字重复逻辑的抽取),重构期间 run.js 一次
|
|
1372
|
+
* git checkout 误覆盖其未提交版本,此处按 test/prompt-placeholders.test.mjs 的契约重建,
|
|
1373
|
+
* 行为等价。
|
|
1374
|
+
*/
|
|
1375
|
+
export function applyRootPlaceholders(text, roots) {
|
|
1376
|
+
return text
|
|
1377
|
+
.replaceAll('{SPEC_ROOT}', roots.specRoot)
|
|
1378
|
+
.replaceAll('{DOCS_ROOT}', roots.docsRoot)
|
|
1379
|
+
.replaceAll('{PROJECTS_ROOT}', roots.projectsRoot)
|
|
1380
|
+
.replaceAll('{WORKFLOWS_ROOT}', roots.workflowsRoot)
|
|
1381
|
+
.replaceAll('{KNOWLEDGE_ROOT}', roots.knowledgeRoot)
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1336
1384
|
/**
|
|
1337
1385
|
* sillyspec run <stage> 主命令
|
|
1338
1386
|
*/
|
|
@@ -2844,12 +2892,469 @@ function rollbackStageCompletion(stageData, steps, currentIdx) {
|
|
|
2844
2892
|
}
|
|
2845
2893
|
}
|
|
2846
2894
|
|
|
2895
|
+
/**
|
|
2896
|
+
* 阶段完成校验失败后的统一收尾:回滚 stage/step 状态 + 落盘 + sync + 返回「未完成」。
|
|
2897
|
+
*
|
|
2898
|
+
* completeStep 的各 gate(runValidators / verify-test 对账 / plan→execute contract /
|
|
2899
|
+
* Stage Review / Execute Task Review)失败时都走这套动作;原先每个失败分支手写重复
|
|
2900
|
+
* ~7 次(含 lastActive 落盘 + triggerSync + return 结构),统一进来消坑,避免某分支
|
|
2901
|
+
* 漏写 triggerSync / 写错 return 结构导致行为分裂。返回 nextPendingIdx=currentIdx,
|
|
2902
|
+
* 让上层走「完成但不推进」分支,--done 被拒、agent 修复产物后重跑。
|
|
2903
|
+
*/
|
|
2904
|
+
async function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts) {
|
|
2905
|
+
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
2906
|
+
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
2907
|
+
await pm._write(cwd, progress, changeName)
|
|
2908
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
2909
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
/**
|
|
2913
|
+
* archive 阶段「确认归档」步骤的收尾处理器(从 completeStep 抽出,行为保持)。
|
|
2914
|
+
*
|
|
2915
|
+
* 两件事:
|
|
2916
|
+
* 1. --confirm 门控:缺 --confirm → 回退 step 状态、提示、返回 early-return 对象(completeStep 透传)
|
|
2917
|
+
* 2. --confirm 通过 → archiveChangeDirectory 移动变更目录 + 推荐文档(design.md / module-impact.md)校验
|
|
2918
|
+
* (contracts.archive.validators 为空,两个 validator 生效窗口互斥;plan.md 已在移动前硬校验阻断)
|
|
2919
|
+
*
|
|
2920
|
+
* @returns {{stageCompleted:false,currentIdx,nextPendingIdx:number}|null}
|
|
2921
|
+
*/
|
|
2922
|
+
async function handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase }) {
|
|
2923
|
+
if (stageName !== 'archive' || steps[currentIdx]?.name !== '确认归档') return null
|
|
2924
|
+
if (!confirm) {
|
|
2925
|
+
steps[currentIdx].status = 'pending'
|
|
2926
|
+
steps[currentIdx].completedAt = null
|
|
2927
|
+
if (outputText) steps[currentIdx].output = null
|
|
2928
|
+
await pm._write(cwd, progress, changeName)
|
|
2929
|
+
console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
|
|
2930
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
2931
|
+
}
|
|
2932
|
+
const archivedDir = await archiveChangeDirectory(pm, cwd, progress, specBase)
|
|
2933
|
+
if (archivedDir && existsSync(archivedDir)) {
|
|
2934
|
+
const recommendedDocs = ['design.md', 'module-impact.md']
|
|
2935
|
+
const missingRecommended = recommendedDocs.filter(d => !existsSync(join(archivedDir, d)))
|
|
2936
|
+
if (missingRecommended.length > 0) {
|
|
2937
|
+
console.warn(`\n⚠️ 归档校验警告:归档目录缺少推荐文档`)
|
|
2938
|
+
for (const d of missingRecommended) console.warn(` - ${d}(${archivedDir})`)
|
|
2939
|
+
} else {
|
|
2940
|
+
console.log(`\n✅ 归档校验通过:核心文档齐全`)
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
return null
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
/**
|
|
2947
|
+
* plan 阶段「generate_plan」步骤完成后,动态插入任务蓝图(coordinator)+ postcheck 步骤
|
|
2948
|
+
* (从 completeStep 抽出,行为保持)。使用稳定 id 匹配,不依赖中文标题。
|
|
2949
|
+
*
|
|
2950
|
+
* plan.md 已含任务时,buildPlanSteps 返回 [fixedPrefix(classify/generate_plan/review_plan),
|
|
2951
|
+
* coordinator, postcheck];本函数把 generate_plan 之后的 coordinator+postcheck 插到当前步后。
|
|
2952
|
+
*/
|
|
2953
|
+
async function handlePlanGeneratePlanStep({ stageName, steps, currentIdx, defStepsForCurrent, cwd, progress }) {
|
|
2954
|
+
if (stageName !== 'plan') return
|
|
2955
|
+
const currentStepDef = defStepsForCurrent?.[currentIdx]
|
|
2956
|
+
const currentStepEntry = steps[currentIdx]
|
|
2957
|
+
const stepId = currentStepDef?.id || currentStepEntry?.id || currentStepEntry?._stepId
|
|
2958
|
+
if (stepId !== 'generate_plan') return
|
|
2959
|
+
const changeDir = resolveChangeDir(cwd, progress)
|
|
2960
|
+
if (!changeDir) return
|
|
2961
|
+
const planFile = join(changeDir, 'plan.md')
|
|
2962
|
+
if (!existsSync(planFile)) return
|
|
2963
|
+
const planContent = readFileSync(planFile, 'utf8')
|
|
2964
|
+
const { buildPlanSteps, fixedPrefix, fixedSuffix } = await import('./stages/plan.js')
|
|
2965
|
+
const fullSteps = buildPlanSteps(changeDir, planContent)
|
|
2966
|
+
const prefixLen = fixedPrefix.length
|
|
2967
|
+
const suffixLen = fixedSuffix.length
|
|
2968
|
+
// 新结构:[...fixedPrefix, coordinatorStep?, postcheckStep?];fixedSuffix 为空,coordinator+postcheck 都在 prefix 之后
|
|
2969
|
+
const coordinatorSteps = fullSteps.slice(prefixLen, suffixLen > 0 ? -suffixLen : undefined)
|
|
2970
|
+
if (coordinatorSteps.length === 0) return
|
|
2971
|
+
for (let i = 0; i < coordinatorSteps.length; i++) {
|
|
2972
|
+
const stepDef = coordinatorSteps[i]
|
|
2973
|
+
const stepEntry = {
|
|
2974
|
+
id: stepDef.id,
|
|
2975
|
+
name: stepDef.name,
|
|
2976
|
+
status: 'pending',
|
|
2977
|
+
prompt: stepDef.prompt || '',
|
|
2978
|
+
outputHint: stepDef.outputHint,
|
|
2979
|
+
optional: stepDef.optional
|
|
2980
|
+
}
|
|
2981
|
+
// 传递 noAI / _cliAction 属性
|
|
2982
|
+
if (stepDef.noAI) stepEntry.noAI = true
|
|
2983
|
+
if (stepDef._cliAction) stepEntry._cliAction = stepDef._cliAction
|
|
2984
|
+
steps.splice(currentIdx + 1 + i, 0, stepEntry)
|
|
2985
|
+
}
|
|
2986
|
+
console.log(` 📝 已动态插入 ${coordinatorSteps.length} 个步骤(${coordinatorSteps.map(s => s.name).join(', ')})`)
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
/**
|
|
2990
|
+
* scan 阶段 step 2「构建扫描项目列表」完成后,按项目展开 perProject 步骤(从 completeStep 抽出,行为保持)。
|
|
2991
|
+
*
|
|
2992
|
+
* 只接受结构化输出(scan_projects YAML block 或 BEGIN_PROJECT_LIST 标记块),校验通过后
|
|
2993
|
+
* 自动注册 projects/<id>.yaml + 写 scan-projects.json + 把 perProject 步骤按项目展开。
|
|
2994
|
+
* 不展开 completeStep 提前 return(失败只记 validationError,由 completeStep 继续推进下一步)。
|
|
2995
|
+
*/
|
|
2996
|
+
async function handleScanProjectListStep({ stageName, steps, currentIdx, outputText, stageData, specBase, cwd, platformOpts }) {
|
|
2997
|
+
if (stageName !== 'scan' || steps[currentIdx]?.name !== '构建扫描项目列表') return
|
|
2998
|
+
// 解析项目列表:只接受结构化输出(YAML block 或 BEGIN_PROJECT_LIST 标记)
|
|
2999
|
+
// 不再从自由文本猜测项目名——自由文本列表的误解析会导致垃圾项目落盘
|
|
3000
|
+
let parsedProjects = [] // Array<{id, path?}>
|
|
3001
|
+
let parsedFromStructuredOutput = false
|
|
3002
|
+
if (outputText) {
|
|
3003
|
+
// 格式 A: YAML block — 匹配 scan_projects: 下所有 - id: xxx 条目(含多行属性)
|
|
3004
|
+
const yamlBlock = outputText.match(/scan_projects:\s*\n([\s\S]+?)(?=$|\n[^\s])/)
|
|
3005
|
+
if (yamlBlock) {
|
|
3006
|
+
const entries = [...yamlBlock[1].matchAll(/-\s+id:\s*(\S+)(?:[\s\S]*?)(?=\n\s+-\s+id:|$)/g)]
|
|
3007
|
+
for (const m of entries) {
|
|
3008
|
+
const id = sanitizeProjectName(m[1])
|
|
3009
|
+
if (!id) continue
|
|
3010
|
+
// 提取可选 path 字段
|
|
3011
|
+
const pathMatch = m[0].match(/path:\s*(\S+)/)
|
|
3012
|
+
const entry = pathMatch ? { id, path: pathMatch[1].trim() } : { id }
|
|
3013
|
+
parsedProjects.push(entry)
|
|
3014
|
+
}
|
|
3015
|
+
parsedFromStructuredOutput = parsedProjects.length > 0
|
|
3016
|
+
}
|
|
3017
|
+
// 格式 B: BEGIN_PROJECT_LIST ... END_PROJECT_LIST 标记块
|
|
3018
|
+
if (!parsedFromStructuredOutput) {
|
|
3019
|
+
const blockMatch = outputText.match(/BEGIN_PROJECT_LIST\s*\n([\s\S]*?)\n*END_PROJECT_LIST/)
|
|
3020
|
+
if (blockMatch) {
|
|
3021
|
+
const raw = [...blockMatch[1].matchAll(/^-\s+(\S+)/gm)].map(m => m[1])
|
|
3022
|
+
parsedProjects = raw.map(s => sanitizeProjectName(s)).filter(Boolean).map(id => ({ id }))
|
|
3023
|
+
parsedFromStructuredOutput = parsedProjects.length > 0
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
const projectNames = parsedProjects.map(p => p.id)
|
|
3029
|
+
|
|
3030
|
+
if (parsedFromStructuredOutput) {
|
|
3031
|
+
stageData.scanMeta = stageData.scanMeta || {}
|
|
3032
|
+
stageData.scanMeta.projectListParsed = true
|
|
3033
|
+
} else {
|
|
3034
|
+
// 结构化输出未解析到 → 回退读取已有 projects/*.yaml
|
|
3035
|
+
// 读取时也校验:path 不存在的 yaml 视为垃圾,直接跳过
|
|
3036
|
+
console.warn('⚠️ step 2 未输出结构化项目列表,回退扫描已注册项目')
|
|
3037
|
+
stageData.scanMeta = stageData.scanMeta || {}
|
|
3038
|
+
stageData.scanMeta.projectListParsed = false
|
|
3039
|
+
const projectsDir = join(specBase, 'projects')
|
|
3040
|
+
if (existsSync(projectsDir)) {
|
|
3041
|
+
const yamlFiles = readdirSync(projectsDir).filter(f => f.endsWith('.yaml'))
|
|
3042
|
+
const fallbackProjects = []
|
|
3043
|
+
const fallbackSkipped = []
|
|
3044
|
+
for (const yf of yamlFiles) {
|
|
3045
|
+
const pName = yf.replace(/\.yaml$/, '')
|
|
3046
|
+
const yamlContent = readFileSync(join(projectsDir, yf), 'utf8')
|
|
3047
|
+
const pathMatch = yamlContent.match(/^path:\s*(.+)/m)
|
|
3048
|
+
const pPath = pathMatch ? pathMatch[1].trim() : pName
|
|
3049
|
+
// 校验 path 是否存在且在 source_root 内
|
|
3050
|
+
const absPath = resolve(cwd, pPath)
|
|
3051
|
+
if (pPath.includes('..') || (!absPath.startsWith(resolve(cwd)) && absPath !== resolve(cwd))) {
|
|
3052
|
+
fallbackSkipped.push(`${pName} (path 越界: ${pPath})`)
|
|
3053
|
+
continue
|
|
3054
|
+
}
|
|
3055
|
+
if (!existsSync(absPath)) {
|
|
3056
|
+
fallbackSkipped.push(`${pName} (path 不存在: ${pPath})`)
|
|
3057
|
+
continue
|
|
3058
|
+
}
|
|
3059
|
+
fallbackProjects.push({ id: pName, path: pPath })
|
|
3060
|
+
}
|
|
3061
|
+
if (fallbackSkipped.length > 0) {
|
|
3062
|
+
console.warn(`⚠️ 跳过 ${fallbackSkipped.length} 个垃圾/过期项目配置:${fallbackSkipped.join(', ')}`)
|
|
3063
|
+
console.warn(' 建议清理 projects/ 下的无效 yaml 文件')
|
|
3064
|
+
}
|
|
3065
|
+
parsedProjects = fallbackProjects
|
|
3066
|
+
projectNames.length = 0
|
|
3067
|
+
projectNames.push(...fallbackProjects.map(p => p.id))
|
|
3068
|
+
}
|
|
3069
|
+
if (parsedProjects.length === 0) {
|
|
3070
|
+
// 无结构化输出 + 无合法已有项目 → step 2 失败
|
|
3071
|
+
console.error('❌ step 2 未输出结构化项目列表,且 projects/ 下无合法项目配置')
|
|
3072
|
+
console.error(' 请在 --output 中输出 scan_projects YAML block 或 BEGIN_PROJECT_LIST 标记块')
|
|
3073
|
+
steps[currentIdx].validationError = '未输出结构化项目列表且无合法 fallback'
|
|
3074
|
+
// 不展开 perProject 步骤,直接跳到下一步
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
// 校验解析出的项目列表(原子守卫:不通过就不落盘)
|
|
3079
|
+
const validation = validateParsedProjects(parsedProjects, cwd)
|
|
3080
|
+
if (!validation.ok) {
|
|
3081
|
+
console.error(`❌ 项目列表校验失败: ${validation.errors.join('; ')}`)
|
|
3082
|
+
console.error(' step 2 完成,但不展开 perProject 步骤。请检查 --output 中的项目列表。')
|
|
3083
|
+
steps[currentIdx].validationError = validation.errors.join('; ')
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
// 自动注册 + 保存 runtime + 展开 perProject 步骤(仅在校验通过时)
|
|
3087
|
+
const projectsDir = join(specBase, 'projects')
|
|
3088
|
+
if (validation.ok) {
|
|
3089
|
+
for (const proj of parsedProjects) {
|
|
3090
|
+
const pName = proj.id
|
|
3091
|
+
const projYaml = join(projectsDir, `${pName}.yaml`)
|
|
3092
|
+
if (!existsSync(projYaml)) {
|
|
3093
|
+
mkdirSync(projectsDir, { recursive: true })
|
|
3094
|
+
const candidates = [
|
|
3095
|
+
join(cwd, pName),
|
|
3096
|
+
join(cwd, 'backend', pName),
|
|
3097
|
+
join(cwd, 'packages', pName),
|
|
3098
|
+
join(cwd, 'apps', pName),
|
|
3099
|
+
join(cwd, 'services', pName),
|
|
3100
|
+
]
|
|
3101
|
+
const detected = candidates.find(c => existsSync(c))
|
|
3102
|
+
const regPath = detected || join(cwd, pName)
|
|
3103
|
+
writeFileSync(projYaml, `name: ${pName}\npath: ${regPath}\nstatus: active\n`)
|
|
3104
|
+
console.log(` 📝 自动注册子项目: ${pName} → ${regPath}`)
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
// 保存 runtime 状态
|
|
3109
|
+
const scanStatePath = join(specBase, '.runtime', 'scan-projects.json')
|
|
3110
|
+
mkdirSync(join(specBase, '.runtime'), { recursive: true })
|
|
3111
|
+
let scanState = { projects: projectNames, expanded: false }
|
|
3112
|
+
if (existsSync(scanStatePath)) {
|
|
3113
|
+
try { scanState = JSON.parse(readFileSync(scanStatePath, 'utf8')) } catch {}
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
// 收集当前步骤之后所有 perProject 步骤
|
|
3117
|
+
const stageDef = stageRegistry[stageName]
|
|
3118
|
+
const allSteps = stageDef?.steps || []
|
|
3119
|
+
const perProjectSteps = allSteps.filter(s => s.perProject)
|
|
3120
|
+
|
|
3121
|
+
// 防重复展开
|
|
3122
|
+
const alreadyExpanded = scanState.expanded || steps.some(s => s.name?.match(/\[.+\]\s*$/))
|
|
3123
|
+
if (!alreadyExpanded && perProjectSteps.length > 0) {
|
|
3124
|
+
// 找到当前步骤(step 2)在动态 steps 中的位置
|
|
3125
|
+
const insertBase = currentIdx + 1
|
|
3126
|
+
let insertPos = insertBase
|
|
3127
|
+
for (const pName of projectNames) {
|
|
3128
|
+
// 读取项目配置获取 projectRoot
|
|
3129
|
+
const projYaml = join(specBase, 'projects', `${pName}.yaml`)
|
|
3130
|
+
let projectRoot = '.'
|
|
3131
|
+
if (existsSync(projYaml)) {
|
|
3132
|
+
const yamlContent = readFileSync(projYaml, 'utf8')
|
|
3133
|
+
const pathMatch = yamlContent.match(/^path:\s*(.+)/m)
|
|
3134
|
+
if (pathMatch) projectRoot = pathMatch[1].trim()
|
|
3135
|
+
}
|
|
3136
|
+
const docOutputDir = platformOpts.specRoot ? `${specBase}/docs/${pName}` : `.sillyspec/docs/${pName}`
|
|
3137
|
+
const contextPrefix = `\n---\n## 当前项目\n- **项目名**: ${pName}\n- **项目路径**: ${projectRoot}\n- **文档输出**: ${docOutputDir}\n\n⚠️ 本步骤只处理上面这个项目,不要处理其他项目。\n---\n\n`
|
|
3138
|
+
|
|
3139
|
+
for (const ppStep of perProjectSteps) {
|
|
3140
|
+
steps.splice(insertPos, 0, {
|
|
3141
|
+
name: `${ppStep.name} [${pName}]`,
|
|
3142
|
+
project: pName,
|
|
3143
|
+
status: 'pending',
|
|
3144
|
+
prompt: contextPrefix + ppStep.prompt,
|
|
3145
|
+
outputHint: ppStep.outputHint,
|
|
3146
|
+
optional: ppStep.optional
|
|
3147
|
+
})
|
|
3148
|
+
insertPos++
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
// 移除原始的 perProject 步骤(未展开的版本)
|
|
3152
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
3153
|
+
if (steps[i].perProject && !steps[i].name?.includes('[')) {
|
|
3154
|
+
steps.splice(i, 1)
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
console.log(` 📝 已按项目展开 ${perProjectSteps.length} 个步骤 × ${projectNames.length} 个项目 = ${perProjectSteps.length * projectNames.length} 个项目步骤`)
|
|
3158
|
+
console.log(` 📁 扫描项目:${projectNames.join(', ')}`)
|
|
3159
|
+
// 标记已展开,防止 resume 重复插入
|
|
3160
|
+
scanState.expanded = true
|
|
3161
|
+
writeFileSync(scanStatePath, JSON.stringify(scanState))
|
|
3162
|
+
} // end !alreadyExpanded
|
|
3163
|
+
} // end validation.ok
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
/**
|
|
3167
|
+
* 阶段完成校验 gate 级联(从 completeStep 抽出,行为保持)。仅当所有步骤确实标记为 completed 时
|
|
3168
|
+
* 由 completeStep 调用。顺序:runValidators → verify-test 对账 → Plan→Execute contract →
|
|
3169
|
+
* Stage Review Gate → Execute Task Review Gate。任一 gate 失败 → rollbackCompletionAndReturn
|
|
3170
|
+
* (统一回滚 + 返回 early-return 对象);全部通过 → 返回 null(completeStep 继续收尾)。
|
|
3171
|
+
*
|
|
3172
|
+
* @returns {{stageCompleted:false,currentIdx,nextPendingIdx:number}|null}
|
|
3173
|
+
*/
|
|
3174
|
+
async function runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx }) {
|
|
3175
|
+
const projectName = progress.project || basename(cwd)
|
|
3176
|
+
const contractResult = runValidators(stageName, cwd, changeName, { projectName, specRoot: platformOpts?.specRoot })
|
|
3177
|
+
if (contractResult.errors.length > 0) {
|
|
3178
|
+
console.error(`\n❌ 阶段 ${stageName} 校验失败:`)
|
|
3179
|
+
for (const err of contractResult.errors) {
|
|
3180
|
+
console.error(` - ${err}`)
|
|
3181
|
+
}
|
|
3182
|
+
console.error(`\n 提示:修复缺失产物后重新完成此步骤(--skip-approval 只跳过阶段转换/审批检查,不能跳过产物校验)`)
|
|
3183
|
+
// 产物校验失败必须阻断完成 —— 否则 validator 形同虚设,
|
|
3184
|
+
// verify 会带着 FAIL/缺 verify-result.md 被 ✅ 标记完成(历史教训)。
|
|
3185
|
+
// plan/execute 的专项契约校验(下方)在产物齐全后才需要继续跑,故此处先 return。
|
|
3186
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
3187
|
+
}
|
|
3188
|
+
if (contractResult.warnings.length > 0) {
|
|
3189
|
+
console.warn(`\n⚠️ 阶段 ${stageName} 校验警告:`)
|
|
3190
|
+
for (const w of contractResult.warnings) {
|
|
3191
|
+
console.warn(` - ${w}`)
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
// verify 产物校验通过 + 结论非 FAIL(否则上面已阻断)。
|
|
3196
|
+
// 再由 CLI 亲自执行 local.yaml 的测试命令,与 verify-result.md 的自报告对账:
|
|
3197
|
+
// 自报告 PASS 但实测失败 → 阻断(防止"文案通过"绕过验证)。
|
|
3198
|
+
if (stageName === 'verify') {
|
|
3199
|
+
const { runVerifyTestCheck, printVerifyTestCheck } = await import('./verify-postcheck.js')
|
|
3200
|
+
const testCheck = runVerifyTestCheck({ cwd, specBase, changeName })
|
|
3201
|
+
printVerifyTestCheck(testCheck)
|
|
3202
|
+
if (testCheck.status === 'failed') {
|
|
3203
|
+
console.error('\n❌ verify 阶段被阻断:verify-result.md 自报告通过,但 CLI 实测测试失败。')
|
|
3204
|
+
console.error(' 请修复失败的测试并更新 verify-result.md 后重新完成此步骤。')
|
|
3205
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
3206
|
+
}
|
|
3207
|
+
// 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
|
|
3208
|
+
// 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
|
|
3209
|
+
const { runVerifyParityCheck, printVerifyParityCheck } = await import('./verify-postcheck.js')
|
|
3210
|
+
const parityCheck = runVerifyParityCheck({ cwd, specBase, changeName, runtimeRoot: platformOpts?.runtimeRoot })
|
|
3211
|
+
printVerifyParityCheck(parityCheck)
|
|
3212
|
+
console.log('\n✅ 验证通过,下一步:sillyspec run archive')
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
// ── Plan postcheck contract:plan.md 必须满足 execute 契约 ──
|
|
3216
|
+
if (stageName === 'plan') {
|
|
3217
|
+
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
3218
|
+
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
3219
|
+
if (planPath && existsSync(planPath)) {
|
|
3220
|
+
const { validatePlanForExecute } = await import('./stages/execute.js')
|
|
3221
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
3222
|
+
const planValidation = validatePlanForExecute(planContent)
|
|
3223
|
+
if (!planValidation.ok) {
|
|
3224
|
+
console.error(`\n❌ Plan → Execute Contract 校验失败:`)
|
|
3225
|
+
for (const err of planValidation.errors) console.error(` - ${err}`)
|
|
3226
|
+
console.error(`\n plan.md 不满足 execute 契约,请修复后重新完成此步骤。`)
|
|
3227
|
+
// 阻断 completed
|
|
3228
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
3229
|
+
}
|
|
3230
|
+
if (planValidation.warnings.length > 0) {
|
|
3231
|
+
console.warn(`\n⚠️ Plan contract 警告(不阻断完成):`)
|
|
3232
|
+
for (const w of planValidation.warnings) console.warn(` - ${w}`)
|
|
3233
|
+
}
|
|
3234
|
+
if (planValidation.ok) {
|
|
3235
|
+
console.log(`\n✅ Plan → Execute Contract 校验通过(${planValidation.tasks.length} tasks, ${planValidation.waves.length} waves)`)
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
// ── Stage Review Gate:brainstorm/plan/propose/execute 独立审查(按 tier 分级)──
|
|
3241
|
+
// tier=self 放行+审计打印;tier=independent 必须 review.json 且 verdict 非 fail,fail-closed
|
|
3242
|
+
if (['brainstorm', 'plan', 'propose', 'execute'].includes(stageName)) {
|
|
3243
|
+
try {
|
|
3244
|
+
const { classifyReviewTier } = await import('./review-tier.js')
|
|
3245
|
+
const { validateStageReview, getLatestStageReviewRunId, printStageReviewResult } = await import('./stage-review.js')
|
|
3246
|
+
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
3247
|
+
const reviewChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
3248
|
+
const designPath = reviewChangeDir ? join(reviewChangeDir, 'design.md') : null
|
|
3249
|
+
let planLevel = null
|
|
3250
|
+
if (reviewChangeDir) {
|
|
3251
|
+
const planPath = join(reviewChangeDir, 'plan.md')
|
|
3252
|
+
if (existsSync(planPath)) {
|
|
3253
|
+
const fmLine = readFileSync(planPath, 'utf8').split('\n').find(l => l.trim().startsWith('plan_level:'))
|
|
3254
|
+
if (fmLine) planLevel = fmLine.split(':')[1].trim()
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
const tier = classifyReviewTier({ planLevel, designPath })
|
|
3258
|
+
const runtimeRoot = platformOpts?.runtimeRoot || join(effectiveSpecBase, '.runtime')
|
|
3259
|
+
|
|
3260
|
+
if (tier.tier === 'self') {
|
|
3261
|
+
console.log('\nℹ️ Stage Review: ' + stageName + ' tier=self(' + tier.reason + '),已降级为当前 agent 自审,不强制独立子代理。')
|
|
3262
|
+
} else {
|
|
3263
|
+
const reviewRunId = getLatestStageReviewRunId(runtimeRoot, stageName)
|
|
3264
|
+
const reviewType = stageName === 'brainstorm' ? 'design'
|
|
3265
|
+
: stageName === 'plan' ? 'plan'
|
|
3266
|
+
: stageName === 'propose' ? 'proposal'
|
|
3267
|
+
: 'acceptance'
|
|
3268
|
+
const searchDirs = [effectiveSpecBase, reviewChangeDir, cwd].filter(Boolean)
|
|
3269
|
+
const reviewResult = validateStageReview({ stage: stageName, reviewType, runtimeRoot, reviewRunId, searchDirs })
|
|
3270
|
+
printStageReviewResult(reviewResult, { stage: stageName })
|
|
3271
|
+
if (!reviewResult.ok) {
|
|
3272
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
} catch (e) {
|
|
3276
|
+
// fail-closed:Gate 自身异常阻断完成,不静默放行(与 Task Review Gate 一致)
|
|
3277
|
+
console.error('❌ Stage Review Gate 异常,阻断 ' + stageName + ' 完成: ' + e.message)
|
|
3278
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
// ── Execute Task Review Gate:所有 task 必须有 review.json 且 verdict 通过 ──
|
|
3283
|
+
if (stageName === 'execute') {
|
|
3284
|
+
try {
|
|
3285
|
+
const { validateTaskReviews, printReviewResult, writeVerifyRequiredEvidence } = await import('./task-review.js')
|
|
3286
|
+
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
3287
|
+
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
3288
|
+
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
3289
|
+
|
|
3290
|
+
if (planPath && existsSync(planPath)) {
|
|
3291
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
3292
|
+
const runtimeRoot = platformOpts?.runtimeRoot || join(effectiveSpecBase, '.runtime')
|
|
3293
|
+
|
|
3294
|
+
// execute run id:从变更专属标记文件读取
|
|
3295
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
3296
|
+
let executeRunId = ''
|
|
3297
|
+
try {
|
|
3298
|
+
if (existsSync(runIdFile)) {
|
|
3299
|
+
executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
3300
|
+
}
|
|
3301
|
+
} catch {}
|
|
3302
|
+
if (!executeRunId) {
|
|
3303
|
+
const { generateExecuteRunId } = await import('./task-review.js')
|
|
3304
|
+
executeRunId = generateExecuteRunId()
|
|
3305
|
+
// 落盘(marker 缺失时 fallback 生成后写盘,保证后续 checkbox/gate 读到同一 ID)
|
|
3306
|
+
try { mkdirSync(runtimeRoot, { recursive: true }); writeFileSync(runIdFile, executeRunId + '\n') } catch {}
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
// git 真实性校验目录:worktree 存在则用 worktree(base/head commit 在其中),否则主仓库
|
|
3310
|
+
let reviewGitDir = cwd
|
|
3311
|
+
try {
|
|
3312
|
+
const { WorktreeManager } = await import('./worktree.js')
|
|
3313
|
+
const wm = new WorktreeManager({ cwd })
|
|
3314
|
+
const meta = wm.getMeta(changeName)
|
|
3315
|
+
if (meta?.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
3316
|
+
reviewGitDir = meta.worktreePath
|
|
3317
|
+
}
|
|
3318
|
+
} catch {}
|
|
3319
|
+
|
|
3320
|
+
const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId, changeDir: planFile, gitDir: reviewGitDir })
|
|
3321
|
+
printReviewResult(reviewResult, { runtimeRoot, executeRunId })
|
|
3322
|
+
|
|
3323
|
+
if (!reviewResult.ok) {
|
|
3324
|
+
// Task review 校验失败,阻断 execute 完成
|
|
3325
|
+
// 检查是否存在 checkbox 已勾但 review 不通过的情况
|
|
3326
|
+
const uncheckedTasks = reviewResult.errors.filter(e => e.includes('缺少 review.json'))
|
|
3327
|
+
if (uncheckedTasks.length > 0) {
|
|
3328
|
+
console.error('\n⚠️ 部分任务已在 plan.md 中勾选,但 review.json 不存在。')
|
|
3329
|
+
console.error(` 请取消勾选这些任务的 checkbox,或补充对应的 review.json(execute run ID: ${executeRunId})。`)
|
|
3330
|
+
}
|
|
3331
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
// cannot_verify 的 requiredEvidence 写入 change 目录,供 verify 阶段消费
|
|
3335
|
+
if (reviewResult.requiredEvidence.length > 0) {
|
|
3336
|
+
const evidencePath = writeVerifyRequiredEvidence(join(effectiveSpecBase, 'changes', changeName), reviewResult.requiredEvidence)
|
|
3337
|
+
if (evidencePath) {
|
|
3338
|
+
console.log(`📄 verify-required-evidence.json 已写入: ${evidencePath}`)
|
|
3339
|
+
console.log(' verify 阶段必须满足这些证据要求。')
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
} catch (e) {
|
|
3344
|
+
// fail-closed:Gate 自身异常时不能默认放行,否则异常成了绕过评审的通道
|
|
3345
|
+
console.error(`❌ Task Review Gate 异常,阻断 execute 完成: ${e.message}`)
|
|
3346
|
+
console.error(' 请检查 review.json / plan.md 是否可读,修复后重新完成此步骤。')
|
|
3347
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
return null
|
|
3351
|
+
}
|
|
3352
|
+
|
|
2847
3353
|
async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
2848
3354
|
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, confirmMode = null, isForceBaseline = false, isAllowNew = false } = options
|
|
2849
3355
|
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
2850
3356
|
const stageData = progress.stages[stageName]
|
|
2851
3357
|
const scanProfile = stageData?.scanProfile || null
|
|
2852
|
-
let archivedDir = null // archive step「确认归档」实际归档目录(archiveChangeDirectory 返回)
|
|
2853
3358
|
|
|
2854
3359
|
// ── WAIT MARKER 硬校验 ──
|
|
2855
3360
|
// 如果 output 包含等待标记,拒绝 --done 推进
|
|
@@ -2940,246 +3445,34 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2940
3445
|
}
|
|
2941
3446
|
}
|
|
2942
3447
|
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
await pm._write(cwd, progress, changeName)
|
|
2949
|
-
console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
|
|
2950
|
-
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
2951
|
-
}
|
|
2952
|
-
archivedDir = await archiveChangeDirectory(pm, cwd, progress, specBase)
|
|
2953
|
-
}
|
|
2954
|
-
|
|
2955
|
-
// archive "确认归档" 步骤完成后,校验归档完整性
|
|
2956
|
-
if (stageName === 'archive' && steps[currentIdx]?.name === '确认归档' && confirm) {
|
|
2957
|
-
// contracts.archive.validators 为空数组(两个 validator 生效窗口互斥,注册为阶段级会误报),
|
|
2958
|
-
// 故 runValidators('archive') 是空跑。这里直接对实际归档目录做内容完整性校验。
|
|
2959
|
-
// plan.md 已在 archiveChangeDirectory 移动前硬校验阻断,此处只查推荐文档。
|
|
2960
|
-
if (archivedDir && existsSync(archivedDir)) {
|
|
2961
|
-
const recommendedDocs = ['design.md', 'module-impact.md']
|
|
2962
|
-
const missingRecommended = recommendedDocs.filter(d => !existsSync(join(archivedDir, d)))
|
|
2963
|
-
if (missingRecommended.length > 0) {
|
|
2964
|
-
console.warn(`\n⚠️ 归档校验警告:归档目录缺少推荐文档`)
|
|
2965
|
-
for (const d of missingRecommended) console.warn(` - ${d}(${archivedDir})`)
|
|
2966
|
-
} else {
|
|
2967
|
-
console.log(`\n✅ 归档校验通过:核心文档齐全`)
|
|
2968
|
-
}
|
|
2969
|
-
}
|
|
2970
|
-
}
|
|
2971
|
-
|
|
2972
|
-
// plan 阶段 "generate_plan" 完成后,动态插入任务蓝图 + postcheck 步骤
|
|
2973
|
-
// 使用稳定 id 匹配,不依赖中文标题
|
|
2974
|
-
if (stageName === 'plan') {
|
|
2975
|
-
const currentStepDef = defStepsForCurrent?.[currentIdx]
|
|
2976
|
-
const currentStepEntry = steps[currentIdx]
|
|
2977
|
-
const stepId = currentStepDef?.id || currentStepEntry?.id || currentStepEntry?._stepId
|
|
2978
|
-
if (stepId === 'generate_plan') {
|
|
2979
|
-
const changeDir = resolveChangeDir(cwd, progress)
|
|
2980
|
-
if (changeDir) {
|
|
2981
|
-
const planFile = join(changeDir, 'plan.md')
|
|
2982
|
-
if (existsSync(planFile)) {
|
|
2983
|
-
const planContent = readFileSync(planFile, 'utf8')
|
|
2984
|
-
const { buildPlanSteps, fixedPrefix, fixedSuffix } = await import('./stages/plan.js')
|
|
2985
|
-
const fullSteps = buildPlanSteps(changeDir, planContent)
|
|
2986
|
-
const prefixLen = fixedPrefix.length
|
|
2987
|
-
const suffixLen = fixedSuffix.length
|
|
2988
|
-
// 新结构:[...fixedPrefix, coordinatorStep?, postcheckStep?]
|
|
2989
|
-
// fixedSuffix 为空,所以 coordinator + postcheck 都在 prefix 之后
|
|
2990
|
-
const coordinatorSteps = fullSteps.slice(prefixLen, suffixLen > 0 ? -suffixLen : undefined)
|
|
2991
|
-
if (coordinatorSteps.length > 0) {
|
|
2992
|
-
for (let i = 0; i < coordinatorSteps.length; i++) {
|
|
2993
|
-
const stepDef = coordinatorSteps[i]
|
|
2994
|
-
const stepEntry = {
|
|
2995
|
-
id: stepDef.id,
|
|
2996
|
-
name: stepDef.name,
|
|
2997
|
-
status: 'pending',
|
|
2998
|
-
prompt: stepDef.prompt || '',
|
|
2999
|
-
outputHint: stepDef.outputHint,
|
|
3000
|
-
optional: stepDef.optional
|
|
3001
|
-
}
|
|
3002
|
-
// 传递 noAI / _cliAction 属性
|
|
3003
|
-
if (stepDef.noAI) stepEntry.noAI = true
|
|
3004
|
-
if (stepDef._cliAction) stepEntry._cliAction = stepDef._cliAction
|
|
3005
|
-
steps.splice(currentIdx + 1 + i, 0, stepEntry)
|
|
3006
|
-
}
|
|
3007
|
-
console.log(` 📝 已动态插入 ${coordinatorSteps.length} 个步骤(${coordinatorSteps.map(s => s.name).join(', ')})`)
|
|
3008
|
-
}
|
|
3009
|
-
}
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3448
|
+
// archive「确认归档」收尾:--confirm 门控 + archiveChangeDirectory 移动 + 推荐文档校验
|
|
3449
|
+
// (抽成 handleArchiveConfirmStep;缺 --confirm 时返回 early-return 对象,completeStep 透传)
|
|
3450
|
+
{
|
|
3451
|
+
const _archiveEarlyReturn = await handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase })
|
|
3452
|
+
if (_archiveEarlyReturn) return _archiveEarlyReturn
|
|
3012
3453
|
}
|
|
3013
3454
|
|
|
3014
|
-
//
|
|
3015
|
-
|
|
3016
|
-
// 解析项目列表:只接受结构化输出(YAML block 或 BEGIN_PROJECT_LIST 标记)
|
|
3017
|
-
// 不再从自由文本猜测项目名——自由文本列表的误解析会导致垃圾项目落盘
|
|
3018
|
-
let parsedProjects = [] // Array<{id, path?}>
|
|
3019
|
-
let parsedFromStructuredOutput = false
|
|
3020
|
-
if (outputText) {
|
|
3021
|
-
// 格式 A: YAML block — 匹配 scan_projects: 下所有 - id: xxx 条目(含多行属性)
|
|
3022
|
-
const yamlBlock = outputText.match(/scan_projects:\s*\n([\s\S]+?)(?=$|\n[^\s])/)
|
|
3023
|
-
if (yamlBlock) {
|
|
3024
|
-
const entries = [...yamlBlock[1].matchAll(/-\s+id:\s*(\S+)(?:[\s\S]*?)(?=\n\s+-\s+id:|$)/g)]
|
|
3025
|
-
for (const m of entries) {
|
|
3026
|
-
const id = sanitizeProjectName(m[1])
|
|
3027
|
-
if (!id) continue
|
|
3028
|
-
// 提取可选 path 字段
|
|
3029
|
-
const pathMatch = m[0].match(/path:\s*(\S+)/)
|
|
3030
|
-
const entry = pathMatch ? { id, path: pathMatch[1].trim() } : { id }
|
|
3031
|
-
parsedProjects.push(entry)
|
|
3032
|
-
}
|
|
3033
|
-
parsedFromStructuredOutput = parsedProjects.length > 0
|
|
3034
|
-
}
|
|
3035
|
-
// 格式 B: BEGIN_PROJECT_LIST ... END_PROJECT_LIST 标记块
|
|
3036
|
-
if (!parsedFromStructuredOutput) {
|
|
3037
|
-
const blockMatch = outputText.match(/BEGIN_PROJECT_LIST\s*\n([\s\S]*?)\n*END_PROJECT_LIST/)
|
|
3038
|
-
if (blockMatch) {
|
|
3039
|
-
const raw = [...blockMatch[1].matchAll(/^-\s+(\S+)/gm)].map(m => m[1])
|
|
3040
|
-
parsedProjects = raw.map(s => sanitizeProjectName(s)).filter(Boolean).map(id => ({ id }))
|
|
3041
|
-
parsedFromStructuredOutput = parsedProjects.length > 0
|
|
3042
|
-
}
|
|
3043
|
-
}
|
|
3044
|
-
}
|
|
3455
|
+
// plan 阶段 "generate_plan" 完成后,动态插入任务蓝图 + postcheck 步骤(抽成 handlePlanGeneratePlanStep)
|
|
3456
|
+
await handlePlanGeneratePlanStep({ stageName, steps, currentIdx, defStepsForCurrent, cwd, progress })
|
|
3045
3457
|
|
|
3046
|
-
|
|
3458
|
+
// scan 阶段 step 2「构建扫描项目列表」完成后,按项目展开 perProject 步骤(抽成 handleScanProjectListStep)
|
|
3459
|
+
await handleScanProjectListStep({ stageName, steps, currentIdx, outputText, stageData, specBase, cwd, platformOpts })
|
|
3047
3460
|
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
const pName = yf.replace(/\.yaml$/, '')
|
|
3064
|
-
const yamlContent = readFileSync(join(projectsDir, yf), 'utf8')
|
|
3065
|
-
const pathMatch = yamlContent.match(/^path:\s*(.+)/m)
|
|
3066
|
-
const pPath = pathMatch ? pathMatch[1].trim() : pName
|
|
3067
|
-
// 校验 path 是否存在且在 source_root 内
|
|
3068
|
-
const absPath = resolve(cwd, pPath)
|
|
3069
|
-
if (pPath.includes('..') || (!absPath.startsWith(resolve(cwd)) && absPath !== resolve(cwd))) {
|
|
3070
|
-
fallbackSkipped.push(`${pName} (path 越界: ${pPath})`)
|
|
3071
|
-
continue
|
|
3072
|
-
}
|
|
3073
|
-
if (!existsSync(absPath)) {
|
|
3074
|
-
fallbackSkipped.push(`${pName} (path 不存在: ${pPath})`)
|
|
3075
|
-
continue
|
|
3076
|
-
}
|
|
3077
|
-
fallbackProjects.push({ id: pName, path: pPath })
|
|
3078
|
-
}
|
|
3079
|
-
if (fallbackSkipped.length > 0) {
|
|
3080
|
-
console.warn(`⚠️ 跳过 ${fallbackSkipped.length} 个垃圾/过期项目配置:${fallbackSkipped.join(', ')}`)
|
|
3081
|
-
console.warn(' 建议清理 projects/ 下的无效 yaml 文件')
|
|
3082
|
-
}
|
|
3083
|
-
parsedProjects = fallbackProjects
|
|
3084
|
-
projectNames.length = 0
|
|
3085
|
-
projectNames.push(...fallbackProjects.map(p => p.id))
|
|
3086
|
-
}
|
|
3087
|
-
if (parsedProjects.length === 0) {
|
|
3088
|
-
// 无结构化输出 + 无合法已有项目 → step 2 失败
|
|
3089
|
-
console.error('❌ step 2 未输出结构化项目列表,且 projects/ 下无合法项目配置')
|
|
3090
|
-
console.error(' 请在 --output 中输出 scan_projects YAML block 或 BEGIN_PROJECT_LIST 标记块')
|
|
3091
|
-
steps[currentIdx].validationError = '未输出结构化项目列表且无合法 fallback'
|
|
3092
|
-
// 不展开 perProject 步骤,直接跳到下一步
|
|
3093
|
-
}
|
|
3094
|
-
}
|
|
3095
|
-
|
|
3096
|
-
// 校验解析出的项目列表(原子守卫:不通过就不落盘)
|
|
3097
|
-
const validation = validateParsedProjects(parsedProjects, cwd)
|
|
3098
|
-
if (!validation.ok) {
|
|
3099
|
-
console.error(`❌ 项目列表校验失败: ${validation.errors.join('; ')}`)
|
|
3100
|
-
console.error(' step 2 完成,但不展开 perProject 步骤。请检查 --output 中的项目列表。')
|
|
3101
|
-
steps[currentIdx].validationError = validation.errors.join('; ')
|
|
3102
|
-
}
|
|
3103
|
-
|
|
3104
|
-
// 自动注册 + 保存 runtime + 展开 perProject 步骤(仅在校验通过时)
|
|
3105
|
-
const projectsDir = join(specBase, 'projects')
|
|
3106
|
-
if (validation.ok) {
|
|
3107
|
-
for (const proj of parsedProjects) {
|
|
3108
|
-
const pName = proj.id
|
|
3109
|
-
const projYaml = join(projectsDir, `${pName}.yaml`)
|
|
3110
|
-
if (!existsSync(projYaml)) {
|
|
3111
|
-
mkdirSync(projectsDir, { recursive: true })
|
|
3112
|
-
const candidates = [
|
|
3113
|
-
join(cwd, pName),
|
|
3114
|
-
join(cwd, 'backend', pName),
|
|
3115
|
-
join(cwd, 'packages', pName),
|
|
3116
|
-
join(cwd, 'apps', pName),
|
|
3117
|
-
join(cwd, 'services', pName),
|
|
3118
|
-
]
|
|
3119
|
-
const detected = candidates.find(c => existsSync(c))
|
|
3120
|
-
const regPath = detected || join(cwd, pName)
|
|
3121
|
-
writeFileSync(projYaml, `name: ${pName}\npath: ${regPath}\nstatus: active\n`)
|
|
3122
|
-
console.log(` 📝 自动注册子项目: ${pName} → ${regPath}`)
|
|
3123
|
-
}
|
|
3124
|
-
}
|
|
3125
|
-
|
|
3126
|
-
// 保存 runtime 状态
|
|
3127
|
-
const scanStatePath = join(specBase, '.runtime', 'scan-projects.json')
|
|
3128
|
-
mkdirSync(join(specBase, '.runtime'), { recursive: true })
|
|
3129
|
-
let scanState = { projects: projectNames, expanded: false }
|
|
3130
|
-
if (existsSync(scanStatePath)) {
|
|
3131
|
-
try { scanState = JSON.parse(readFileSync(scanStatePath, 'utf8')) } catch {}
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
// 收集当前步骤之后所有 perProject 步骤
|
|
3135
|
-
const stageDef = stageRegistry[stageName]
|
|
3136
|
-
const allSteps = stageDef?.steps || []
|
|
3137
|
-
const perProjectSteps = allSteps.filter(s => s.perProject)
|
|
3138
|
-
|
|
3139
|
-
// 防重复展开
|
|
3140
|
-
const alreadyExpanded = scanState.expanded || steps.some(s => s.name?.match(/\[.+\]\s*$/))
|
|
3141
|
-
if (!alreadyExpanded && perProjectSteps.length > 0) {
|
|
3142
|
-
// 找到当前步骤(step 2)在动态 steps 中的位置
|
|
3143
|
-
const insertBase = currentIdx + 1
|
|
3144
|
-
let insertPos = insertBase
|
|
3145
|
-
for (const pName of projectNames) {
|
|
3146
|
-
// 读取项目配置获取 projectRoot
|
|
3147
|
-
const projYaml = join(specBase, 'projects', `${pName}.yaml`)
|
|
3148
|
-
let projectRoot = '.'
|
|
3149
|
-
if (existsSync(projYaml)) {
|
|
3150
|
-
const yamlContent = readFileSync(projYaml, 'utf8')
|
|
3151
|
-
const pathMatch = yamlContent.match(/^path:\s*(.+)/m)
|
|
3152
|
-
if (pathMatch) projectRoot = pathMatch[1].trim()
|
|
3153
|
-
}
|
|
3154
|
-
const docOutputDir = platformOpts.specRoot ? `${specBase}/docs/${pName}` : `.sillyspec/docs/${pName}`
|
|
3155
|
-
const contextPrefix = `\n---\n## 当前项目\n- **项目名**: ${pName}\n- **项目路径**: ${projectRoot}\n- **文档输出**: ${docOutputDir}\n\n⚠️ 本步骤只处理上面这个项目,不要处理其他项目。\n---\n\n`
|
|
3156
|
-
|
|
3157
|
-
for (const ppStep of perProjectSteps) {
|
|
3158
|
-
steps.splice(insertPos, 0, {
|
|
3159
|
-
name: `${ppStep.name} [${pName}]`,
|
|
3160
|
-
project: pName,
|
|
3161
|
-
status: 'pending',
|
|
3162
|
-
prompt: contextPrefix + ppStep.prompt,
|
|
3163
|
-
outputHint: ppStep.outputHint,
|
|
3164
|
-
optional: ppStep.optional
|
|
3165
|
-
})
|
|
3166
|
-
insertPos++
|
|
3167
|
-
}
|
|
3168
|
-
}
|
|
3169
|
-
// 移除原始的 perProject 步骤(未展开的版本)
|
|
3170
|
-
for (let i = steps.length - 1; i >= 0; i--) {
|
|
3171
|
-
if (steps[i].perProject && !steps[i].name?.includes('[')) {
|
|
3172
|
-
steps.splice(i, 1)
|
|
3173
|
-
}
|
|
3174
|
-
}
|
|
3175
|
-
console.log(` 📝 已按项目展开 ${perProjectSteps.length} 个步骤 × ${projectNames.length} 个项目 = ${perProjectSteps.length * projectNames.length} 个项目步骤`)
|
|
3176
|
-
console.log(` 📁 扫描项目:${projectNames.join(', ')}`)
|
|
3177
|
-
// 标记已展开,防止 resume 重复插入
|
|
3178
|
-
scanState.expanded = true
|
|
3179
|
-
writeFileSync(scanStatePath, JSON.stringify(scanState))
|
|
3180
|
-
} // end !alreadyExpanded
|
|
3181
|
-
} // end validation.ok
|
|
3182
|
-
} // end step 2
|
|
3461
|
+
// execute「Wave N 执行」步骤完成 → 扫 worktree 提取 provider endpoint artifact
|
|
3462
|
+
// (供 verify 阶段 parity 对账 + consumer task 上游契约注入)。接线自 contract-matrix pipeline。
|
|
3463
|
+
if (stageName === 'execute' && /^Wave \d+ 执行$/.test(steps[currentIdx]?.name || '')) {
|
|
3464
|
+
try {
|
|
3465
|
+
const { extractArtifactsForChange } = await import('./contract-matrix.js')
|
|
3466
|
+
let worktreePath = null
|
|
3467
|
+
try {
|
|
3468
|
+
const { WorktreeManager } = await import('./worktree.js')
|
|
3469
|
+
const meta = new WorktreeManager({ cwd }).getMeta(changeName)
|
|
3470
|
+
if (meta?.worktreePath && existsSync(meta.worktreePath)) worktreePath = meta.worktreePath
|
|
3471
|
+
} catch {}
|
|
3472
|
+
const msg = extractArtifactsForChange({ changeDir: join(specBase, 'changes', changeName), specBase, changeName, worktreePath })
|
|
3473
|
+
if (msg) console.log(msg)
|
|
3474
|
+
} catch (e) { console.warn(`⚠️ 契约 artifact 提取跳过: ${e?.message || e}`) }
|
|
3475
|
+
}
|
|
3183
3476
|
|
|
3184
3477
|
const nextPendingIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
3185
3478
|
|
|
@@ -3521,204 +3814,8 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
3521
3814
|
|
|
3522
3815
|
// 阶段完成校验 — 防御性守卫:仅当所有步骤确实标记为 completed 时才跑 validator
|
|
3523
3816
|
if (actualCompleted === actualTotal && actualTotal > 0) {
|
|
3524
|
-
const
|
|
3525
|
-
|
|
3526
|
-
if (contractResult.errors.length > 0) {
|
|
3527
|
-
console.error(`\n❌ 阶段 ${stageName} 校验失败:`)
|
|
3528
|
-
for (const err of contractResult.errors) {
|
|
3529
|
-
console.error(` - ${err}`)
|
|
3530
|
-
}
|
|
3531
|
-
console.error(`\n 提示:修复缺失产物后重新完成此步骤(--skip-approval 只跳过阶段转换/审批检查,不能跳过产物校验)`)
|
|
3532
|
-
// 产物校验失败必须阻断完成 —— 否则 validator 形同虚设,
|
|
3533
|
-
// verify 会带着 FAIL/缺 verify-result.md 被 ✅ 标记完成(历史教训)。
|
|
3534
|
-
// plan/execute 的专项契约校验(下方)在产物齐全后才需要继续跑,故此处先 return。
|
|
3535
|
-
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
3536
|
-
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
3537
|
-
await pm._write(cwd, progress, changeName)
|
|
3538
|
-
triggerSync(cwd, changeName, platformOpts)
|
|
3539
|
-
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
3540
|
-
}
|
|
3541
|
-
if (contractResult.warnings.length > 0) {
|
|
3542
|
-
console.warn(`\n⚠️ 阶段 ${stageName} 校验警告:`)
|
|
3543
|
-
for (const w of contractResult.warnings) {
|
|
3544
|
-
console.warn(` - ${w}`)
|
|
3545
|
-
}
|
|
3546
|
-
}
|
|
3547
|
-
|
|
3548
|
-
// verify 产物校验通过 + 结论非 FAIL(否则上面已阻断)。
|
|
3549
|
-
// 再由 CLI 亲自执行 local.yaml 的测试命令,与 verify-result.md 的自报告对账:
|
|
3550
|
-
// 自报告 PASS 但实测失败 → 阻断(防止"文案通过"绕过验证)。
|
|
3551
|
-
if (stageName === 'verify') {
|
|
3552
|
-
const { runVerifyTestCheck, printVerifyTestCheck } = await import('./verify-postcheck.js')
|
|
3553
|
-
const testCheck = runVerifyTestCheck({ cwd, specBase, changeName })
|
|
3554
|
-
printVerifyTestCheck(testCheck)
|
|
3555
|
-
if (testCheck.status === 'failed') {
|
|
3556
|
-
console.error('\n❌ verify 阶段被阻断:verify-result.md 自报告通过,但 CLI 实测测试失败。')
|
|
3557
|
-
console.error(' 请修复失败的测试并更新 verify-result.md 后重新完成此步骤。')
|
|
3558
|
-
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
3559
|
-
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
3560
|
-
await pm._write(cwd, progress, changeName)
|
|
3561
|
-
triggerSync(cwd, changeName, platformOpts)
|
|
3562
|
-
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
3563
|
-
}
|
|
3564
|
-
console.log('\n✅ 验证通过,下一步:sillyspec run archive')
|
|
3565
|
-
}
|
|
3566
|
-
|
|
3567
|
-
// ── Plan postcheck contract:plan.md 必须满足 execute 契约 ──
|
|
3568
|
-
if (stageName === 'plan') {
|
|
3569
|
-
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
3570
|
-
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
3571
|
-
if (planPath && existsSync(planPath)) {
|
|
3572
|
-
const { validatePlanForExecute } = await import('./stages/execute.js')
|
|
3573
|
-
const planContent = readFileSync(planPath, 'utf8')
|
|
3574
|
-
const planValidation = validatePlanForExecute(planContent)
|
|
3575
|
-
if (!planValidation.ok) {
|
|
3576
|
-
console.error(`\n❌ Plan → Execute Contract 校验失败:`)
|
|
3577
|
-
for (const err of planValidation.errors) console.error(` - ${err}`)
|
|
3578
|
-
console.error(`\n plan.md 不满足 execute 契约,请修复后重新完成此步骤。`)
|
|
3579
|
-
// 阻断 completed
|
|
3580
|
-
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
3581
|
-
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
3582
|
-
await pm._write(cwd, progress, changeName)
|
|
3583
|
-
triggerSync(cwd, changeName, platformOpts)
|
|
3584
|
-
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
3585
|
-
}
|
|
3586
|
-
if (planValidation.warnings.length > 0) {
|
|
3587
|
-
console.warn(`\n⚠️ Plan contract 警告(不阻断完成):`)
|
|
3588
|
-
for (const w of planValidation.warnings) console.warn(` - ${w}`)
|
|
3589
|
-
}
|
|
3590
|
-
if (planValidation.ok) {
|
|
3591
|
-
console.log(`\n✅ Plan → Execute Contract 校验通过(${planValidation.tasks.length} tasks, ${planValidation.waves.length} waves)`)
|
|
3592
|
-
}
|
|
3593
|
-
}
|
|
3594
|
-
}
|
|
3595
|
-
|
|
3596
|
-
// ── Stage Review Gate:brainstorm/plan/propose/execute 独立审查(按 tier 分级)──
|
|
3597
|
-
// tier=self 放行+审计打印;tier=independent 必须 review.json 且 verdict 非 fail,fail-closed
|
|
3598
|
-
if (['brainstorm', 'plan', 'propose', 'execute'].includes(stageName)) {
|
|
3599
|
-
try {
|
|
3600
|
-
const { classifyReviewTier } = await import('./review-tier.js')
|
|
3601
|
-
const { validateStageReview, getLatestStageReviewRunId, printStageReviewResult } = await import('./stage-review.js')
|
|
3602
|
-
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
3603
|
-
const reviewChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
3604
|
-
const designPath = reviewChangeDir ? join(reviewChangeDir, 'design.md') : null
|
|
3605
|
-
let planLevel = null
|
|
3606
|
-
if (reviewChangeDir) {
|
|
3607
|
-
const planPath = join(reviewChangeDir, 'plan.md')
|
|
3608
|
-
if (existsSync(planPath)) {
|
|
3609
|
-
const fmLine = readFileSync(planPath, 'utf8').split('\n').find(l => l.trim().startsWith('plan_level:'))
|
|
3610
|
-
if (fmLine) planLevel = fmLine.split(':')[1].trim()
|
|
3611
|
-
}
|
|
3612
|
-
}
|
|
3613
|
-
const tier = classifyReviewTier({ planLevel, designPath })
|
|
3614
|
-
const runtimeRoot = platformOpts?.runtimeRoot || join(effectiveSpecBase, '.runtime')
|
|
3615
|
-
|
|
3616
|
-
if (tier.tier === 'self') {
|
|
3617
|
-
console.log('\nℹ️ Stage Review: ' + stageName + ' tier=self(' + tier.reason + '),已降级为当前 agent 自审,不强制独立子代理。')
|
|
3618
|
-
} else {
|
|
3619
|
-
const reviewRunId = getLatestStageReviewRunId(runtimeRoot, stageName)
|
|
3620
|
-
const reviewType = stageName === 'brainstorm' ? 'design'
|
|
3621
|
-
: stageName === 'plan' ? 'plan'
|
|
3622
|
-
: stageName === 'propose' ? 'proposal'
|
|
3623
|
-
: 'acceptance'
|
|
3624
|
-
const searchDirs = [effectiveSpecBase, reviewChangeDir, cwd].filter(Boolean)
|
|
3625
|
-
const reviewResult = validateStageReview({ stage: stageName, reviewType, runtimeRoot, reviewRunId, searchDirs })
|
|
3626
|
-
printStageReviewResult(reviewResult, { stage: stageName })
|
|
3627
|
-
if (!reviewResult.ok) {
|
|
3628
|
-
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
3629
|
-
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
3630
|
-
await pm._write(cwd, progress, changeName)
|
|
3631
|
-
triggerSync(cwd, changeName, platformOpts)
|
|
3632
|
-
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
3633
|
-
}
|
|
3634
|
-
}
|
|
3635
|
-
} catch (e) {
|
|
3636
|
-
// fail-closed:Gate 自身异常阻断完成,不静默放行(与 Task Review Gate 一致)
|
|
3637
|
-
console.error('❌ Stage Review Gate 异常,阻断 ' + stageName + ' 完成: ' + e.message)
|
|
3638
|
-
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
3639
|
-
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
3640
|
-
await pm._write(cwd, progress, changeName)
|
|
3641
|
-
triggerSync(cwd, changeName, platformOpts)
|
|
3642
|
-
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
3643
|
-
}
|
|
3644
|
-
}
|
|
3645
|
-
|
|
3646
|
-
// ── Execute Task Review Gate:所有 task 必须有 review.json 且 verdict 通过 ──
|
|
3647
|
-
if (stageName === 'execute') {
|
|
3648
|
-
try {
|
|
3649
|
-
const { validateTaskReviews, printReviewResult, writeVerifyRequiredEvidence } = await import('./task-review.js')
|
|
3650
|
-
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
3651
|
-
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
3652
|
-
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
3653
|
-
|
|
3654
|
-
if (planPath && existsSync(planPath)) {
|
|
3655
|
-
const planContent = readFileSync(planPath, 'utf8')
|
|
3656
|
-
const runtimeRoot = platformOpts?.runtimeRoot || join(effectiveSpecBase, '.runtime')
|
|
3657
|
-
|
|
3658
|
-
// execute run id:从变更专属标记文件读取
|
|
3659
|
-
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
3660
|
-
let executeRunId = ''
|
|
3661
|
-
try {
|
|
3662
|
-
if (existsSync(runIdFile)) {
|
|
3663
|
-
executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
3664
|
-
}
|
|
3665
|
-
} catch {}
|
|
3666
|
-
if (!executeRunId) {
|
|
3667
|
-
const { generateExecuteRunId } = await import('./task-review.js')
|
|
3668
|
-
executeRunId = generateExecuteRunId()
|
|
3669
|
-
// 落盘(marker 缺失时 fallback 生成后写盘,保证后续 checkbox/gate 读到同一 ID)
|
|
3670
|
-
try { mkdirSync(runtimeRoot, { recursive: true }); writeFileSync(runIdFile, executeRunId + '\n') } catch {}
|
|
3671
|
-
}
|
|
3672
|
-
|
|
3673
|
-
// git 真实性校验目录:worktree 存在则用 worktree(base/head commit 在其中),否则主仓库
|
|
3674
|
-
let reviewGitDir = cwd
|
|
3675
|
-
try {
|
|
3676
|
-
const { WorktreeManager } = await import('./worktree.js')
|
|
3677
|
-
const wm = new WorktreeManager({ cwd })
|
|
3678
|
-
const meta = wm.getMeta(changeName)
|
|
3679
|
-
if (meta?.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
3680
|
-
reviewGitDir = meta.worktreePath
|
|
3681
|
-
}
|
|
3682
|
-
} catch {}
|
|
3683
|
-
|
|
3684
|
-
const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId, changeDir: planFile, gitDir: reviewGitDir })
|
|
3685
|
-
printReviewResult(reviewResult, { runtimeRoot, executeRunId })
|
|
3686
|
-
|
|
3687
|
-
if (!reviewResult.ok) {
|
|
3688
|
-
// Task review 校验失败,阻断 execute 完成
|
|
3689
|
-
// 检查是否存在 checkbox 已勾但 review 不通过的情况
|
|
3690
|
-
const uncheckedTasks = reviewResult.errors.filter(e => e.includes('缺少 review.json'))
|
|
3691
|
-
if (uncheckedTasks.length > 0) {
|
|
3692
|
-
console.error('\n⚠️ 部分任务已在 plan.md 中勾选,但 review.json 不存在。')
|
|
3693
|
-
console.error(` 请取消勾选这些任务的 checkbox,或补充对应的 review.json(execute run ID: ${executeRunId})。`)
|
|
3694
|
-
}
|
|
3695
|
-
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
3696
|
-
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
3697
|
-
await pm._write(cwd, progress, changeName)
|
|
3698
|
-
triggerSync(cwd, changeName, platformOpts)
|
|
3699
|
-
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
3700
|
-
}
|
|
3701
|
-
|
|
3702
|
-
// cannot_verify 的 requiredEvidence 写入 change 目录,供 verify 阶段消费
|
|
3703
|
-
if (reviewResult.requiredEvidence.length > 0) {
|
|
3704
|
-
const evidencePath = writeVerifyRequiredEvidence(join(effectiveSpecBase, 'changes', changeName), reviewResult.requiredEvidence)
|
|
3705
|
-
if (evidencePath) {
|
|
3706
|
-
console.log(`📄 verify-required-evidence.json 已写入: ${evidencePath}`)
|
|
3707
|
-
console.log(' verify 阶段必须满足这些证据要求。')
|
|
3708
|
-
}
|
|
3709
|
-
}
|
|
3710
|
-
}
|
|
3711
|
-
} catch (e) {
|
|
3712
|
-
// fail-closed:Gate 自身异常时不能默认放行,否则异常成了绕过评审的通道
|
|
3713
|
-
console.error(`❌ Task Review Gate 异常,阻断 execute 完成: ${e.message}`)
|
|
3714
|
-
console.error(' 请检查 review.json / plan.md 是否可读,修复后重新完成此步骤。')
|
|
3715
|
-
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
3716
|
-
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
3717
|
-
await pm._write(cwd, progress, changeName)
|
|
3718
|
-
triggerSync(cwd, changeName, platformOpts)
|
|
3719
|
-
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
3720
|
-
}
|
|
3721
|
-
}
|
|
3817
|
+
const _gateEarlyReturn = await runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx })
|
|
3818
|
+
if (_gateEarlyReturn) return _gateEarlyReturn
|
|
3722
3819
|
} else if (actualCompleted < actualTotal) {
|
|
3723
3820
|
// 实际步骤未全部完成,跳过 validator(状态可能不同步)
|
|
3724
3821
|
console.log(`\n⚠️ 阶段校验跳过:${actualTotal} 步中仅 ${actualCompleted} 步标记为已完成,可能存在状态不同步。如确认阶段已完成,请运行 --status 确认。`)
|
|
@@ -3884,6 +3981,10 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
3884
3981
|
return { stageCompleted: false, currentIdx, nextPendingIdx }
|
|
3885
3982
|
}
|
|
3886
3983
|
|
|
3984
|
+
// 测试专用导出:completeStep 是 step 完成处理核心(产物校验/gate 链/sync/auto 推进),
|
|
3985
|
+
// 行为保持重构需要 characterization 测试直接驱动它。有先例:worktree-guard.js 的 _queryDbFirstCellForTest。
|
|
3986
|
+
export { completeStep as _completeStepForTest }
|
|
3987
|
+
|
|
3887
3988
|
async function skipStep(pm, progress, stageName, cwd, changeName, platformOpts = {}) {
|
|
3888
3989
|
const stageData = progress.stages[stageName]
|
|
3889
3990
|
if (!stageData || !stageData.steps) {
|