sillyspec 3.20.0 → 3.20.2
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/brainstorm-postcheck.js +158 -0
- package/src/change-risk-profile.js +202 -17
- package/src/classify-change.js +73 -0
- package/src/index.js +7 -4
- package/src/run.js +270 -95
- package/src/scan-postcheck.js +15 -12
- package/src/stages/brainstorm-auto.js +229 -0
- package/src/stages/scan.js +20 -3
- package/test/run-scan-project-parse.test.mjs +166 -43
- package/test/scan-postcheck.test.mjs +18 -4
- package/.npmrc-tmp +0 -1
package/src/run.js
CHANGED
|
@@ -27,6 +27,52 @@ export function sanitizeProjectName(name) {
|
|
|
27
27
|
return clean
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* 校验从 step 2 解析出的项目列表。
|
|
32
|
+
* 不通过则不落盘 projects/*.yaml、不展开 perProject 步骤。
|
|
33
|
+
*
|
|
34
|
+
* @param {Array<{id: string, path?: string}>} projects - 项目列表(含可选 path)
|
|
35
|
+
* @param {string} sourceRoot - 源码根目录,用于 path 安全校验
|
|
36
|
+
*/
|
|
37
|
+
export function validateParsedProjects(projects, sourceRoot) {
|
|
38
|
+
const errors = []
|
|
39
|
+
if (!projects || projects.length === 0) {
|
|
40
|
+
return { ok: false, errors: ['项目列表为空'] }
|
|
41
|
+
}
|
|
42
|
+
if (projects.length > 10) {
|
|
43
|
+
return { ok: false, errors: [`项目数量 ${projects.length} 超过上限 10,疑似误解析`] }
|
|
44
|
+
}
|
|
45
|
+
const safeRoot = resolve(sourceRoot)
|
|
46
|
+
const seen = new Set()
|
|
47
|
+
for (const proj of projects) {
|
|
48
|
+
const id = proj.id || proj
|
|
49
|
+
if (seen.has(id)) {
|
|
50
|
+
errors.push(`重复项目名: ${id}`)
|
|
51
|
+
}
|
|
52
|
+
seen.add(id)
|
|
53
|
+
// slug 合法性:只允许 a-z 0-9 _ - .,长度 2-64
|
|
54
|
+
if (!/^[a-zA-Z][\w\-.]{1,63}$/.test(id)) {
|
|
55
|
+
errors.push(`项目名 "${id}" 不合法(需 slug 格式:字母开头,只含 a-zA-Z0-9_-., 长度 2-64)`)
|
|
56
|
+
}
|
|
57
|
+
// path 安全校验(如果提供了 path)
|
|
58
|
+
if (proj.path) {
|
|
59
|
+
if (proj.path.includes('..')) {
|
|
60
|
+
errors.push(`项目 "${id}" 的 path 包含 .. ,拒绝越界`)
|
|
61
|
+
} else {
|
|
62
|
+
const absPath = resolve(safeRoot, proj.path)
|
|
63
|
+
if (!absPath.startsWith(safeRoot + '/') && absPath !== safeRoot) {
|
|
64
|
+
errors.push(`项目 "${id}" 的 path "${proj.path}" 解析后超出 source_root`)
|
|
65
|
+
}
|
|
66
|
+
if (!existsSync(absPath)) {
|
|
67
|
+
errors.push(`项目 "${id}" 的 path "${proj.path}" 不存在`)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (errors.length > 0) return { ok: false, errors }
|
|
73
|
+
return { ok: true, errors: [] }
|
|
74
|
+
}
|
|
75
|
+
|
|
30
76
|
/**
|
|
31
77
|
* 在容器/Docker 环境下,git 可能因目录所有权不匹配报 dubious ownership。
|
|
32
78
|
* 使用 -c safe.directory= 临时参数,不污染全局 git config。
|
|
@@ -113,6 +159,9 @@ import { checkTransition, runValidators } from './stage-contract.js'
|
|
|
113
159
|
import { buildExecuteSteps } from './stages/execute.js'
|
|
114
160
|
import { buildPlanSteps } from './stages/plan.js'
|
|
115
161
|
import { formatExecuteSummary } from './worktree-apply.js'
|
|
162
|
+
import { classifyChange } from './classify-change.js'
|
|
163
|
+
import { detectRiskProfile } from './change-risk-profile.js'
|
|
164
|
+
import { definition as brainstormAutoDef } from './stages/brainstorm-auto.js'
|
|
116
165
|
|
|
117
166
|
/**
|
|
118
167
|
* 从 _module-map.yaml 读取模块上下文索引
|
|
@@ -772,19 +821,19 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
772
821
|
}
|
|
773
822
|
}
|
|
774
823
|
|
|
775
|
-
// Execute: 注入 currentExecuteRunId
|
|
824
|
+
// Execute: 注入 currentExecuteRunId(从变更专属标记文件读取)
|
|
776
825
|
if (stageName === 'execute' && promptText.includes('{EXECUTE_RUN_ID}')) {
|
|
777
|
-
const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
778
|
-
const runIdFile = join(execSpecBase, '.runtime', 'current-execute-run-id')
|
|
779
826
|
let runId = ''
|
|
827
|
+
const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
828
|
+
const runIdFile = join(execSpecBase, '.runtime', `current-execute-run-id-${effectiveChange}`)
|
|
780
829
|
try {
|
|
781
830
|
if (existsSync(runIdFile)) {
|
|
782
831
|
runId = readFileSync(runIdFile, 'utf8').trim()
|
|
783
832
|
}
|
|
784
833
|
} catch {}
|
|
785
834
|
if (!runId) {
|
|
786
|
-
const { generateExecuteRunId
|
|
787
|
-
|
|
835
|
+
const { generateExecuteRunId } = await import('./task-review.js')
|
|
836
|
+
runId = generateExecuteRunId()
|
|
788
837
|
}
|
|
789
838
|
promptText = promptText.replace(/\{EXECUTE_RUN_ID\}/g, runId)
|
|
790
839
|
}
|
|
@@ -1283,7 +1332,7 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1283
1332
|
'--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
|
|
1284
1333
|
'--files', '--allow-new', '--force-baseline', '--force-rescan',
|
|
1285
1334
|
'--json', '--dir', '--help',
|
|
1286
|
-
'--reopen', '--from-step',
|
|
1335
|
+
'--reopen', '--from-step', '--mode',
|
|
1287
1336
|
])
|
|
1288
1337
|
for (let i = 0; i < flags.length; i++) {
|
|
1289
1338
|
const f = flags[i]
|
|
@@ -1340,7 +1389,7 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1340
1389
|
|
|
1341
1390
|
// -- auto 模式:自动推进所有流程阶段
|
|
1342
1391
|
if (stageName === 'auto') {
|
|
1343
|
-
return await runAutoMode(pm, progress, cwd, flags, effectiveChange)
|
|
1392
|
+
return await runAutoMode(pm, progress, cwd, flags, effectiveChange, platformOpts)
|
|
1344
1393
|
}
|
|
1345
1394
|
|
|
1346
1395
|
// --change 只作为变更名标识,不再拦截流程
|
|
@@ -1528,16 +1577,24 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
1528
1577
|
}
|
|
1529
1578
|
}
|
|
1530
1579
|
|
|
1531
|
-
// ── execute 阶段启动时固定 executeRunId ──
|
|
1580
|
+
// ── execute 阶段启动时固定 executeRunId(绑定变更名,避免跨变更复用) ──
|
|
1532
1581
|
let currentExecuteRunId = null
|
|
1533
1582
|
if (stageName === 'execute') {
|
|
1534
|
-
const { generateExecuteRunId
|
|
1583
|
+
const { generateExecuteRunId } = await import('./task-review.js')
|
|
1535
1584
|
const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
1536
1585
|
const runtimeRoot = join(execSpecBase, '.runtime')
|
|
1537
|
-
|
|
1538
|
-
// 写入 runtime 标记文件,确保整个 execute 生命周期内 runId 不变
|
|
1586
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
1539
1587
|
mkdirSync(runtimeRoot, { recursive: true })
|
|
1540
|
-
|
|
1588
|
+
// 优先读取已有的变更专属标记文件
|
|
1589
|
+
try {
|
|
1590
|
+
if (existsSync(runIdFile)) {
|
|
1591
|
+
currentExecuteRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
1592
|
+
}
|
|
1593
|
+
} catch {}
|
|
1594
|
+
if (!currentExecuteRunId) {
|
|
1595
|
+
currentExecuteRunId = generateExecuteRunId()
|
|
1596
|
+
writeFileSync(runIdFile, currentExecuteRunId + '\n')
|
|
1597
|
+
}
|
|
1541
1598
|
}
|
|
1542
1599
|
|
|
1543
1600
|
// 自动探测 currentChange
|
|
@@ -2202,7 +2259,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2202
2259
|
// plan 阶段 "generate_plan" 完成后,动态插入任务蓝图 + postcheck 步骤
|
|
2203
2260
|
// 使用稳定 id 匹配,不依赖中文标题
|
|
2204
2261
|
if (stageName === 'plan') {
|
|
2205
|
-
const currentStepDef =
|
|
2262
|
+
const currentStepDef = defStepsForCurrent?.[currentIdx]
|
|
2206
2263
|
const currentStepEntry = steps[currentIdx]
|
|
2207
2264
|
const stepId = currentStepDef?.id || currentStepEntry?.id || currentStepEntry?._stepId
|
|
2208
2265
|
if (stepId === 'generate_plan') {
|
|
@@ -2243,91 +2300,132 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2243
2300
|
|
|
2244
2301
|
// scan 阶段 step 2 "构建扫描项目列表" 完成后,按项目展开 perProject 步骤
|
|
2245
2302
|
if (stageName === 'scan' && steps[currentIdx]?.name === '构建扫描项目列表') {
|
|
2246
|
-
//
|
|
2247
|
-
|
|
2248
|
-
|
|
2303
|
+
// 解析项目列表:只接受结构化输出(YAML block 或 BEGIN_PROJECT_LIST 标记)
|
|
2304
|
+
// 不再从自由文本猜测项目名——自由文本列表的误解析会导致垃圾项目落盘
|
|
2305
|
+
let parsedProjects = [] // Array<{id, path?}>
|
|
2306
|
+
let parsedFromStructuredOutput = false
|
|
2249
2307
|
if (outputText) {
|
|
2250
|
-
//
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
if (projectNames.length === 0) {
|
|
2262
|
-
const parenMatch = outputText.match(/(?:子项目|项目)[\s::]*(\S+(?:[\/、,,]+\S+)*)/)
|
|
2263
|
-
if (parenMatch) {
|
|
2264
|
-
const raw = parenMatch[1].split(/[\/、,,]+/).map(s => s.trim()).filter(Boolean)
|
|
2265
|
-
projectNames = raw.map(sanitizeProjectName).filter(Boolean)
|
|
2266
|
-
if (projectNames.length > 0) { stageData.scanMeta = stageData.scanMeta || {}; stageData.scanMeta.projectListParsed = true; }
|
|
2308
|
+
// 格式 A: YAML block — 匹配 scan_projects: 下所有 - id: xxx 条目(含多行属性)
|
|
2309
|
+
const yamlBlock = outputText.match(/scan_projects:\s*\n([\s\S]+?)(?=$|\n[^\s])/)
|
|
2310
|
+
if (yamlBlock) {
|
|
2311
|
+
const entries = [...yamlBlock[1].matchAll(/-\s+id:\s*(\S+)(?:[\s\S]*?)(?=\n\s+-\s+id:|$)/g)]
|
|
2312
|
+
for (const m of entries) {
|
|
2313
|
+
const id = sanitizeProjectName(m[1])
|
|
2314
|
+
if (!id) continue
|
|
2315
|
+
// 提取可选 path 字段
|
|
2316
|
+
const pathMatch = m[0].match(/path:\s*(\S+)/)
|
|
2317
|
+
const entry = pathMatch ? { id, path: pathMatch[1].trim() } : { id }
|
|
2318
|
+
parsedProjects.push(entry)
|
|
2267
2319
|
}
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2320
|
+
parsedFromStructuredOutput = parsedProjects.length > 0
|
|
2321
|
+
}
|
|
2322
|
+
// 格式 B: BEGIN_PROJECT_LIST ... END_PROJECT_LIST 标记块
|
|
2323
|
+
if (!parsedFromStructuredOutput) {
|
|
2324
|
+
const blockMatch = outputText.match(/BEGIN_PROJECT_LIST\s*\n([\s\S]*?)\n*END_PROJECT_LIST/)
|
|
2325
|
+
if (blockMatch) {
|
|
2326
|
+
const raw = [...blockMatch[1].matchAll(/^-\s+(\S+)/gm)].map(m => m[1])
|
|
2327
|
+
parsedProjects = raw.map(s => sanitizeProjectName(s)).filter(Boolean).map(id => ({ id }))
|
|
2328
|
+
parsedFromStructuredOutput = parsedProjects.length > 0
|
|
2276
2329
|
}
|
|
2277
2330
|
}
|
|
2278
2331
|
}
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2332
|
+
|
|
2333
|
+
const projectNames = parsedProjects.map(p => p.id)
|
|
2334
|
+
|
|
2335
|
+
if (parsedFromStructuredOutput) {
|
|
2336
|
+
stageData.scanMeta = stageData.scanMeta || {}
|
|
2337
|
+
stageData.scanMeta.projectListParsed = true
|
|
2338
|
+
} else {
|
|
2339
|
+
// 结构化输出未解析到 → 回退读取已有 projects/*.yaml
|
|
2340
|
+
// 读取时也校验:path 不存在的 yaml 视为垃圾,直接跳过
|
|
2341
|
+
console.warn('⚠️ step 2 未输出结构化项目列表,回退扫描已注册项目')
|
|
2342
|
+
stageData.scanMeta = stageData.scanMeta || {}
|
|
2343
|
+
stageData.scanMeta.projectListParsed = false
|
|
2283
2344
|
const projectsDir = join(specBase, 'projects')
|
|
2284
2345
|
if (existsSync(projectsDir)) {
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2346
|
+
const yamlFiles = readdirSync(projectsDir).filter(f => f.endsWith('.yaml'))
|
|
2347
|
+
const fallbackProjects = []
|
|
2348
|
+
const fallbackSkipped = []
|
|
2349
|
+
for (const yf of yamlFiles) {
|
|
2350
|
+
const pName = yf.replace(/\.yaml$/, '')
|
|
2351
|
+
const yamlContent = readFileSync(join(projectsDir, yf), 'utf8')
|
|
2352
|
+
const pathMatch = yamlContent.match(/^path:\s*(.+)/m)
|
|
2353
|
+
const pPath = pathMatch ? pathMatch[1].trim() : pName
|
|
2354
|
+
// 校验 path 是否存在且在 source_root 内
|
|
2355
|
+
const absPath = resolve(cwd, pPath)
|
|
2356
|
+
if (pPath.includes('..') || (!absPath.startsWith(resolve(cwd)) && absPath !== resolve(cwd))) {
|
|
2357
|
+
fallbackSkipped.push(`${pName} (path 越界: ${pPath})`)
|
|
2358
|
+
continue
|
|
2359
|
+
}
|
|
2360
|
+
if (!existsSync(absPath)) {
|
|
2361
|
+
fallbackSkipped.push(`${pName} (path 不存在: ${pPath})`)
|
|
2362
|
+
continue
|
|
2363
|
+
}
|
|
2364
|
+
fallbackProjects.push({ id: pName, path: pPath })
|
|
2365
|
+
}
|
|
2366
|
+
if (fallbackSkipped.length > 0) {
|
|
2367
|
+
console.warn(`⚠️ 跳过 ${fallbackSkipped.length} 个垃圾/过期项目配置:${fallbackSkipped.join(', ')}`)
|
|
2368
|
+
console.warn(' 建议清理 projects/ 下的无效 yaml 文件')
|
|
2369
|
+
}
|
|
2370
|
+
parsedProjects = fallbackProjects
|
|
2371
|
+
projectNames.length = 0
|
|
2372
|
+
projectNames.push(...fallbackProjects.map(p => p.id))
|
|
2373
|
+
}
|
|
2374
|
+
if (parsedProjects.length === 0) {
|
|
2375
|
+
// 无结构化输出 + 无合法已有项目 → step 2 失败
|
|
2376
|
+
console.error('❌ step 2 未输出结构化项目列表,且 projects/ 下无合法项目配置')
|
|
2377
|
+
console.error(' 请在 --output 中输出 scan_projects YAML block 或 BEGIN_PROJECT_LIST 标记块')
|
|
2378
|
+
steps[currentIdx].validationError = '未输出结构化项目列表且无合法 fallback'
|
|
2379
|
+
// 不展开 perProject 步骤,直接跳到下一步
|
|
2288
2380
|
}
|
|
2289
2381
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2382
|
+
|
|
2383
|
+
// 校验解析出的项目列表(原子守卫:不通过就不落盘)
|
|
2384
|
+
const validation = validateParsedProjects(parsedProjects, cwd)
|
|
2385
|
+
if (!validation.ok) {
|
|
2386
|
+
console.error(`❌ 项目列表校验失败: ${validation.errors.join('; ')}`)
|
|
2387
|
+
console.error(' step 2 完成,但不展开 perProject 步骤。请检查 --output 中的项目列表。')
|
|
2388
|
+
steps[currentIdx].validationError = validation.errors.join('; ')
|
|
2292
2389
|
}
|
|
2293
2390
|
|
|
2294
|
-
//
|
|
2391
|
+
// 自动注册 + 保存 runtime + 展开 perProject 步骤(仅在校验通过时)
|
|
2295
2392
|
const projectsDir = join(specBase, 'projects')
|
|
2296
|
-
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2393
|
+
if (validation.ok) {
|
|
2394
|
+
for (const proj of parsedProjects) {
|
|
2395
|
+
const pName = proj.id
|
|
2396
|
+
const projYaml = join(projectsDir, `${pName}.yaml`)
|
|
2397
|
+
if (!existsSync(projYaml)) {
|
|
2398
|
+
mkdirSync(projectsDir, { recursive: true })
|
|
2399
|
+
const candidates = [
|
|
2400
|
+
join(cwd, pName),
|
|
2401
|
+
join(cwd, 'backend', pName),
|
|
2402
|
+
join(cwd, 'packages', pName),
|
|
2403
|
+
join(cwd, 'apps', pName),
|
|
2404
|
+
join(cwd, 'services', pName),
|
|
2405
|
+
]
|
|
2406
|
+
const detected = candidates.find(c => existsSync(c))
|
|
2407
|
+
const regPath = detected || join(cwd, pName)
|
|
2408
|
+
writeFileSync(projYaml, `name: ${pName}\npath: ${regPath}\nstatus: active\n`)
|
|
2409
|
+
console.log(` 📝 自动注册子项目: ${pName} → ${regPath}`)
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// 保存 runtime 状态
|
|
2414
|
+
const scanStatePath = join(specBase, '.runtime', 'scan-projects.json')
|
|
2415
|
+
mkdirSync(join(specBase, '.runtime'), { recursive: true })
|
|
2416
|
+
let scanState = { projects: projectNames, expanded: false }
|
|
2417
|
+
if (existsSync(scanStatePath)) {
|
|
2418
|
+
try { scanState = JSON.parse(readFileSync(scanStatePath, 'utf8')) } catch {}
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
// 收集当前步骤之后所有 perProject 步骤
|
|
2422
|
+
const stageDef = stageRegistry[stageName]
|
|
2423
|
+
const allSteps = stageDef?.steps || []
|
|
2424
|
+
const perProjectSteps = allSteps.filter(s => s.perProject)
|
|
2327
2425
|
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2426
|
+
// 防重复展开
|
|
2427
|
+
const alreadyExpanded = scanState.expanded || steps.some(s => s.name?.match(/\[.+\]\s*$/))
|
|
2428
|
+
if (!alreadyExpanded && perProjectSteps.length > 0) {
|
|
2331
2429
|
// 找到当前步骤(step 2)在动态 steps 中的位置
|
|
2332
2430
|
const insertBase = currentIdx + 1
|
|
2333
2431
|
let insertPos = insertBase
|
|
@@ -2366,8 +2464,9 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2366
2464
|
// 标记已展开,防止 resume 重复插入
|
|
2367
2465
|
scanState.expanded = true
|
|
2368
2466
|
writeFileSync(scanStatePath, JSON.stringify(scanState))
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2467
|
+
} // end !alreadyExpanded
|
|
2468
|
+
} // end validation.ok
|
|
2469
|
+
} // end step 2
|
|
2371
2470
|
|
|
2372
2471
|
const nextPendingIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
2373
2472
|
|
|
@@ -2667,7 +2766,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2667
2766
|
// ── Execute Task Review Gate:所有 task 必须有 review.json 且 verdict 通过 ──
|
|
2668
2767
|
if (stageName === 'execute') {
|
|
2669
2768
|
try {
|
|
2670
|
-
const { validateTaskReviews, printReviewResult, writeVerifyRequiredEvidence
|
|
2769
|
+
const { validateTaskReviews, printReviewResult, writeVerifyRequiredEvidence } = await import('./task-review.js')
|
|
2671
2770
|
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
2672
2771
|
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
2673
2772
|
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
@@ -2676,9 +2775,16 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2676
2775
|
const planContent = readFileSync(planPath, 'utf8')
|
|
2677
2776
|
const runtimeRoot = join(effectiveSpecBase, '.runtime')
|
|
2678
2777
|
|
|
2679
|
-
//
|
|
2680
|
-
|
|
2778
|
+
// execute run id:从变更专属标记文件读取
|
|
2779
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
2780
|
+
let executeRunId = ''
|
|
2781
|
+
try {
|
|
2782
|
+
if (existsSync(runIdFile)) {
|
|
2783
|
+
executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
2784
|
+
}
|
|
2785
|
+
} catch {}
|
|
2681
2786
|
if (!executeRunId) {
|
|
2787
|
+
const { generateExecuteRunId } = await import('./task-review.js')
|
|
2682
2788
|
executeRunId = generateExecuteRunId()
|
|
2683
2789
|
}
|
|
2684
2790
|
|
|
@@ -3016,14 +3122,28 @@ async function resetStage(pm, progress, stageName, cwd, changeName, platformOpts
|
|
|
3016
3122
|
/**
|
|
3017
3123
|
* auto 模式:自动推进 brainstorm → plan → execute → verify
|
|
3018
3124
|
*/
|
|
3019
|
-
async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
3020
|
-
const flowStages = ['brainstorm', 'plan', 'execute', 'verify']
|
|
3125
|
+
async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts = {}) {
|
|
3126
|
+
const flowStages = ['brainstorm', 'plan', 'execute', 'verify', 'archive']
|
|
3021
3127
|
const isDone = flags.includes('--done')
|
|
3022
3128
|
const outputIdx = flags.indexOf('--output')
|
|
3023
3129
|
const outputText = outputIdx !== -1 && flags[outputIdx + 1] ? flags[outputIdx + 1] : null
|
|
3024
3130
|
const inputIdx = flags.indexOf('--input')
|
|
3025
3131
|
const inputText = inputIdx !== -1 && flags[inputIdx + 1] ? flags[inputIdx + 1] : null
|
|
3026
3132
|
const skipApproval = flags.includes('--skip-approval')
|
|
3133
|
+
const explicitMode = (() => {
|
|
3134
|
+
const m = flags.indexOf('--mode')
|
|
3135
|
+
return m !== -1 && flags[m + 1] ? flags[m + 1] : null
|
|
3136
|
+
})()
|
|
3137
|
+
const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
3138
|
+
|
|
3139
|
+
// Helper: 在 auto 模式下获取步骤定义
|
|
3140
|
+
const getAutoSteps = async (stage) => {
|
|
3141
|
+
if (stage === 'brainstorm') {
|
|
3142
|
+
return brainstormAutoDef.steps
|
|
3143
|
+
}
|
|
3144
|
+
return getStageSteps(stage, cwd, progress, platformOpts?.specRoot || null)
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3027
3147
|
const nextInFlow = (stage) => {
|
|
3028
3148
|
const i = flowStages.indexOf(stage)
|
|
3029
3149
|
return i >= 0 && i < flowStages.length - 1 ? flowStages[i + 1] : null
|
|
@@ -3032,6 +3152,24 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
3032
3152
|
const ensureAutoStage = async (stage) => {
|
|
3033
3153
|
const stageChanged = progress.currentStage !== stage
|
|
3034
3154
|
progress.currentStage = stage
|
|
3155
|
+
// Auto 模式下 brainstorm 使用 artifact-first 步骤
|
|
3156
|
+
if (stage === 'brainstorm') {
|
|
3157
|
+
const existingSteps = progress.stages?.brainstorm?.steps
|
|
3158
|
+
const isAutoModeSteps = existingSteps?.length === 4 && existingSteps?.[0]?.name === '状态检查与上下文加载'
|
|
3159
|
+
if (!isAutoModeSteps) {
|
|
3160
|
+
if (!progress.stages) progress.stages = {}
|
|
3161
|
+
progress.stages.brainstorm = {
|
|
3162
|
+
status: 'in-progress',
|
|
3163
|
+
startedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
|
|
3164
|
+
completedAt: null,
|
|
3165
|
+
steps: brainstormAutoDef.steps.map(s => ({ name: s.name, status: 'pending' }))
|
|
3166
|
+
}
|
|
3167
|
+
await pm._write(cwd, progress, changeName)
|
|
3168
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
3169
|
+
progress = await pm.read(cwd, changeName)
|
|
3170
|
+
return progress
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3035
3173
|
const changed = await ensureStageSteps(progress, stage, cwd)
|
|
3036
3174
|
if (stageChanged || changed) {
|
|
3037
3175
|
await pm._write(cwd, progress, changeName)
|
|
@@ -3041,6 +3179,18 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
3041
3179
|
return progress
|
|
3042
3180
|
}
|
|
3043
3181
|
|
|
3182
|
+
// ── Classify change on first entry ──
|
|
3183
|
+
if (!progress.stages?.brainstorm?.status && !progress.stages?.plan?.status) {
|
|
3184
|
+
const { classifyChange } = await import('./classify-change.js')
|
|
3185
|
+
const classification = classifyChange({ description: inputText || '', explicitMode })
|
|
3186
|
+
if (classification.mode === 'quick') {
|
|
3187
|
+
console.log(`📊 auto 模式分类:${classification.mode}(${classification.reason})`)
|
|
3188
|
+
console.log(` 此变更建议使用 quick 模式,运行:sillyspec run quick "${inputText || '需求'}"`)
|
|
3189
|
+
return
|
|
3190
|
+
}
|
|
3191
|
+
console.log(`📊 auto 模式分类:${classification.mode}(${classification.reason})`)
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3044
3194
|
let currentStage = progress.currentStage
|
|
3045
3195
|
if (!currentStage || progress.stages?.[currentStage]?.status === 'completed') {
|
|
3046
3196
|
currentStage = firstOpenStage()
|
|
@@ -3076,7 +3226,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
3076
3226
|
}
|
|
3077
3227
|
console.log('')
|
|
3078
3228
|
|
|
3079
|
-
const defSteps = await
|
|
3229
|
+
const defSteps = await getAutoSteps(currentStage)
|
|
3080
3230
|
const pendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
|
|
3081
3231
|
if (pendingIdx === -1) {
|
|
3082
3232
|
const wsIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'waiting') ?? -1
|
|
@@ -3121,7 +3271,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
3121
3271
|
|
|
3122
3272
|
const nextPendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
|
|
3123
3273
|
if (nextPendingIdx !== -1) {
|
|
3124
|
-
const defSteps = await
|
|
3274
|
+
const defSteps = await getAutoSteps(currentStage)
|
|
3125
3275
|
// execute 阶段启动前检查审批
|
|
3126
3276
|
if (currentStage === 'execute' && !skipApproval) {
|
|
3127
3277
|
const approval = await checkApproval(cwd, changeName, platformOpts)
|
|
@@ -3146,6 +3296,31 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
3146
3296
|
return
|
|
3147
3297
|
}
|
|
3148
3298
|
|
|
3299
|
+
// ── next-action.json 驱动:brainstorm → plan 推进判断 ──
|
|
3300
|
+
if (currentStage === 'brainstorm' && next === 'plan') {
|
|
3301
|
+
const changeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot || null)
|
|
3302
|
+
if (changeDir) {
|
|
3303
|
+
const nextActionFile = join(changeDir, 'brainstorm', 'next-action.json')
|
|
3304
|
+
try {
|
|
3305
|
+
const nextAction = JSON.parse(readFileSync(nextActionFile, 'utf8'))
|
|
3306
|
+
if (nextAction.has_blocking_questions === true) {
|
|
3307
|
+
console.log(`\n⏸️ brainstorm 有阻塞问题,无法自动进入 plan:`)
|
|
3308
|
+
for (const q of (nextAction.questions || [])) {
|
|
3309
|
+
console.log(` Q-${q.id}: ${q.question}`)
|
|
3310
|
+
if (q.options) console.log(` 选项:${q.options.join(' / ')}`)
|
|
3311
|
+
if (q.recommended) console.log(` 推荐:${q.recommended}`)
|
|
3312
|
+
}
|
|
3313
|
+
console.log(`\n 请回答阻塞问题后继续:sillyspec run auto --done --output "已回答"`)
|
|
3314
|
+
return
|
|
3315
|
+
}
|
|
3316
|
+
console.log(`\n✅ next-action.json: ${nextAction.status},自动进入 plan`)
|
|
3317
|
+
} catch (e) {
|
|
3318
|
+
// next-action.json 不存在或格式错误,继续推进(向后兼容)
|
|
3319
|
+
console.log(`\n⚠️ next-action.json 未找到,继续进入 plan`)
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3149
3324
|
progress.currentStage = next
|
|
3150
3325
|
if (!progress.stages[next]) {
|
|
3151
3326
|
progress.stages[next] = { status: 'pending', steps: [], startedAt: null, completedAt: null }
|
|
@@ -3161,7 +3336,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
3161
3336
|
progress = await pm.read(cwd, changeName)
|
|
3162
3337
|
|
|
3163
3338
|
console.log(`\n${currentStage} complete. Auto advanced to ${next}.`)
|
|
3164
|
-
const nextSteps = await
|
|
3339
|
+
const nextSteps = await getAutoSteps(next)
|
|
3165
3340
|
const firstPending = progress.stages[next]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
|
|
3166
3341
|
if (firstPending !== -1) {
|
|
3167
3342
|
// execute 阶段启动前检查审批
|
package/src/scan-postcheck.js
CHANGED
|
@@ -151,19 +151,22 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
// 5. 检查 AI
|
|
154
|
+
// 5. 检查 AI 输出中的真实错误标记
|
|
155
|
+
// 注意:不对 agent 描述性文本做全文正则匹配,防止误报。
|
|
156
|
+
// 只检测明显是 agent 运行时失败的信号(未被捕获的错误块),
|
|
157
|
+
// 而非 agent 正常描述中提到这些词。
|
|
155
158
|
if (outputText) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
{
|
|
160
|
-
|
|
161
|
-
]
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
checks.push({ name: ep.name, severity: CHECK_SEVERITY.WARNING, detail: ep.detail })
|
|
165
|
-
}
|
|
159
|
+
// 5a: 检测未被恢复的 API 错误(连续多次、非描述性提及)
|
|
160
|
+
const apiErrorCount = (outputText.match(/API Error\b.*?\b529\b/gi) || []).length
|
|
161
|
+
if (apiErrorCount >= 2) {
|
|
162
|
+
checks.push({ name: 'api_error_529', severity: CHECK_SEVERITY.WARNING, detail: 'AI 输出中包含多次 API Error 529' })
|
|
163
|
+
}
|
|
164
|
+
const rateLimitCount = (outputText.match(/rate.?limit.*?exhausted/gi) || []).length
|
|
165
|
+
if (rateLimitCount >= 2) {
|
|
166
|
+
checks.push({ name: 'rate_limit_exhausted', severity: CHECK_SEVERITY.WARNING, detail: 'AI 输出中包含多次 rate_limit exhausted' })
|
|
166
167
|
}
|
|
168
|
+
// tool_use_error 和 fallback 已移除:agent 描述性文本中正常提及这些词
|
|
169
|
+
// 不应触发 warning。真正的问题会通过文档缺失、manifest 失败等其他检查捕获。
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
// 6. manifest 写入状态检查
|
|
@@ -300,7 +303,7 @@ export function formatStructuredResult(result, meta = {}) {
|
|
|
300
303
|
structured.failure_categories.bad_references.push(entry)
|
|
301
304
|
}
|
|
302
305
|
// AI 输出质量类
|
|
303
|
-
else if (['
|
|
306
|
+
else if (['api_error_529', 'rate_limit_exhausted'].includes(check.name)) {
|
|
304
307
|
structured.failure_categories.quality_warnings.push(entry)
|
|
305
308
|
}
|
|
306
309
|
// manifest/project 列表问题
|