sillyspec 3.18.5 → 3.19.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.18.5",
3
+ "version": "3.19.0",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/index.js CHANGED
@@ -289,6 +289,21 @@ async function main() {
289
289
  await runCommand(filteredArgs.slice(1), resolveEffectiveDir(dir), specDir)
290
290
  break
291
291
  }
292
+ // task-10: 顶层命令别名,转发 runCommand,与 case 'run': 路径行为一致
293
+ // help 文本(:44-46)已宣称这些 stage 可直接使用,这里补齐路由避免落 default 分支。
294
+ // 注意:filteredArgs[0] === command,直接透传 filteredArgs 即可让 runCommand
295
+ // 从 args[0] 取到 stage 名(run.js:1036)。与 case 'run': 的 filteredArgs.slice(1)
296
+ // 区别只在于 slice(1) 去掉的是 'run' 字面量,这里 command 本身就是 stage 名不能丢。
297
+ case 'doctor':
298
+ case 'scan':
299
+ case 'status':
300
+ case 'quick':
301
+ case 'explore': {
302
+ const { runCommand } = await import('./run.js')
303
+ const stageArgs = [command, ...filteredArgs.slice(1)]
304
+ await runCommand(stageArgs, resolveEffectiveDir(dir), specDir)
305
+ break
306
+ }
292
307
  case 'dashboard': {
293
308
  // Parse dashboard options
294
309
  let port = 3456;
package/src/run.js CHANGED
@@ -11,6 +11,22 @@ const require = createRequire(import.meta.url)
11
11
  import { ProgressManager } from './progress.js'
12
12
  import { SCAN_STATUS, POINTER_STATUS, isPointerCorrupted } from './constants.js'
13
13
 
14
+ /**
15
+ * 清洗项目名:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符。
16
+ * - 必须含至少一个字母(拒绝纯数字 "0"/"7"/"07",避免 scan-projects.json 脏数据)
17
+ * - 长度必须 ≥ 2(拒绝单字符 "a"/"0")
18
+ * @param {string} name - 原始项目名候选
19
+ * @returns {string | null} 合法项目名或 null(拒绝)
20
+ */
21
+ export function sanitizeProjectName(name) {
22
+ if (!name) return null
23
+ const clean = String(name).replace(/[^a-zA-Z0-9_\-.]/g, '').trim()
24
+ if (!clean) return null
25
+ if (!/[a-zA-Z]/.test(clean)) return null // 纯数字/符号拒绝("0"/"7"/"07")
26
+ if (clean.length < 2) return null // 单字符拒绝("a"/"0")
27
+ return clean
28
+ }
29
+
14
30
  /**
15
31
  * 在容器/Docker 环境下,git 可能因目录所有权不匹配报 dubious ownership。
16
32
  * 使用 -c safe.directory= 临时参数,不污染全局 git config。
@@ -666,6 +682,12 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
666
682
  `2. **不允许**用 cat >、tee、heredoc 等 Bash 方式绕过 Write 工具。\n` +
667
683
  `3. 如果 Write 和 Read 均失败,记录失败并停止当前 step。\n` +
668
684
  `\n` +
685
+ `### 📍 Workflow YAML 占位符映射(task-05)\n` +
686
+ `读取 \`{WORKFLOWS_ROOT}/scan-docs.yaml\` 时,yaml 内的占位符按以下映射替换为绝对路径:\n` +
687
+ `- \`{SPEC_ROOT}\` → \`${specSillyspec}\`(规范目录根)\n` +
688
+ `- \`<project>\` → 当前项目名(见下方 step 提示,等于 \`${projectName}\`)\n` +
689
+ `- 例:\`{SPEC_ROOT}/docs/<project>/scan/ARCHITECTURE.md\` → \`${docsRoot}/scan/ARCHITECTURE.md\`\n` +
690
+ `\n` +
669
691
  `创建目录: \`mkdir -p ${docsRoot}/{scan,modules,flows} ${projectsRoot} ${changesRoot}\`\n`
670
692
  )
671
693
  if (platformOpts.runtimeRoot) {
@@ -750,6 +772,23 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
750
772
  }
751
773
  }
752
774
 
775
+ // Execute: 注入 currentExecuteRunId(从 runtime 标记文件读取)
776
+ 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
+ let runId = ''
780
+ try {
781
+ if (existsSync(runIdFile)) {
782
+ runId = readFileSync(runIdFile, 'utf8').trim()
783
+ }
784
+ } catch {}
785
+ if (!runId) {
786
+ const { generateExecuteRunId, getLatestExecuteRunId } = await import('./task-review.js')
787
+ runId = getLatestExecuteRunId(join(execSpecBase, '.runtime')) || generateExecuteRunId()
788
+ }
789
+ promptText = promptText.replace(/\{EXECUTE_RUN_ID\}/g, runId)
790
+ }
791
+
753
792
  // 注入模块上下文(brainstorm/plan/execute 阶段,基于 Module Context Index)
754
793
  if (['brainstorm', 'plan', 'execute'].includes(stageName) && projectName) {
755
794
  const effectiveSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
@@ -1416,7 +1455,9 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
1416
1455
  const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
1417
1456
  // 状态转换校验
1418
1457
  const prevStage = progress.currentStage || ''
1419
- const transition = checkTransition(prevStage, stageName)
1458
+ // task-07: 提取 prevStage 的 stageData,传给 checkTransition 检测 failed_post_check 门控
1459
+ const fromStageData = (progress.stages && prevStage && progress.stages[prevStage]) || undefined
1460
+ const transition = checkTransition(prevStage, stageName, fromStageData ? { fromStageData } : {})
1420
1461
  if (!transition.allowed) {
1421
1462
  console.error(`❌ 阶段转换不允许: ${prevStage || '(起始)'} → ${stageName}`)
1422
1463
  console.error(` 原因: ${transition.reason}`)
@@ -1461,6 +1502,18 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
1461
1502
  }
1462
1503
  }
1463
1504
 
1505
+ // ── execute 阶段启动时固定 executeRunId ──
1506
+ let currentExecuteRunId = null
1507
+ if (stageName === 'execute') {
1508
+ const { generateExecuteRunId, getLatestExecuteRunId } = await import('./task-review.js')
1509
+ const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
1510
+ const runtimeRoot = join(execSpecBase, '.runtime')
1511
+ currentExecuteRunId = getLatestExecuteRunId(runtimeRoot) || generateExecuteRunId()
1512
+ // 写入 runtime 标记文件,确保整个 execute 生命周期内 runId 不变
1513
+ mkdirSync(runtimeRoot, { recursive: true })
1514
+ writeFileSync(join(runtimeRoot, 'current-execute-run-id'), currentExecuteRunId + '\n')
1515
+ }
1516
+
1464
1517
  // 自动探测 currentChange
1465
1518
  if (autoDetectChange(progress, cwd)) {
1466
1519
  progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
@@ -2150,16 +2203,15 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2150
2203
  if (stageName === 'scan' && steps[currentIdx]?.name === '构建扫描项目列表') {
2151
2204
  // 解析项目列表:从 step 2 输出提取,或回退读取 projects/*.yaml
2152
2205
  let projectNames = []
2153
- // 项目名清洗:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符
2154
- const sanitizeProjectName = (name) => {
2155
- const clean = name.replace(/[^a-zA-Z0-9_\-.]/g, '').trim()
2156
- return clean || null
2157
- }
2206
+ // sanitizeProjectName 已提取到模块顶层(含字母校验 + 长度≥2)
2158
2207
  if (outputText) {
2159
2208
  // 匹配方式 1: "1. project-name" 编号列表
2160
- const numbered = outputText.match(/^\s*\d+\.\s+(\S+)/gm)
2209
+ // 正则收紧:token 必须以字母开头,避免误捕获纯数字 "0"/"7" 和步骤说明中英文行
2210
+ const numbered = outputText.match(/^\s*\d+\.\s+([a-zA-Z][\w\-.]*)/gm)
2161
2211
  if (numbered) {
2162
- const raw = numbered.map(m => m.replace(/^\s*\d+\.\s+/, '').replace(/[—\-:].*$/, '').trim())
2212
+ // task-05 B2 延伸修正:原 /[—\-:].*$/ 会把 ASCII 连字符当后缀分隔符,
2213
+ // 把 order-service 切成 order。现只针对中文长破折号 `—`(LLM 输出列表时常作分隔符)。
2214
+ const raw = numbered.map(m => m.replace(/^\s*\d+\.\s+/, '').replace(/—.*$/, '').trim())
2163
2215
  projectNames = raw.map(sanitizeProjectName).filter(Boolean)
2164
2216
  if (projectNames.length > 0) { stageData.scanMeta = stageData.scanMeta || {}; stageData.scanMeta.projectListParsed = true; }
2165
2217
  }
@@ -2434,8 +2486,18 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2434
2486
  stageData.status = SCAN_STATUS.FAILED_POST_CHECK
2435
2487
  stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
2436
2488
  await pm._write(cwd, progress, changeName)
2489
+ triggerSync(cwd, changeName, platformOpts)
2437
2490
  console.error(`\n❌ scan post-check 失败,状态设为 failed_post_check。不允许 clean success。`)
2438
2491
  console.error(` 请检查上方错误信息并修复后重新 scan。`)
2492
+ // 平台模式:exit(1) 让 daemon/SillyHub 感知非 0 退出码(manifest.json 已落盘,不会被撤销)
2493
+ if (platformOpts.specRoot || platformOpts.runtimeRoot) {
2494
+ console.error(' 平台模式:CLI 将以 exit code 1 退出,通知 SillyHub scan 失败。')
2495
+ process.exit(1)
2496
+ }
2497
+ // 接口与 plan contract (run.js:2551 附近 plan 失败分支) 对齐:
2498
+ // 返回 { stageCompleted:false, currentIdx, nextPendingIdx: currentIdx }
2499
+ // 让上层 runStage 走"完成但不推进"分支,--done 被拒
2500
+ return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
2439
2501
  } else if (postResult.status === 'completed_with_warnings') {
2440
2502
  // 警告不阻止完成,但记录
2441
2503
  stageData.status = 'completed'
@@ -2559,6 +2621,56 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2559
2621
  }
2560
2622
  }
2561
2623
  }
2624
+
2625
+ // ── Execute Task Review Gate:所有 task 必须有 review.json 且 verdict 通过 ──
2626
+ if (stageName === 'execute') {
2627
+ try {
2628
+ const { validateTaskReviews, printReviewResult, writeVerifyRequiredEvidence, getLatestExecuteRunId, generateExecuteRunId } = await import('./task-review.js')
2629
+ const effectiveSpecBase = platformOpts?.specRoot || specBase
2630
+ const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
2631
+ const planPath = planFile ? join(planFile, 'plan.md') : null
2632
+
2633
+ if (planPath && existsSync(planPath)) {
2634
+ const planContent = readFileSync(planPath, 'utf8')
2635
+ const runtimeRoot = join(effectiveSpecBase, '.runtime')
2636
+
2637
+ // 找到 execute run id:优先从 runtime 目录找最新,否则生成新的
2638
+ let executeRunId = getLatestExecuteRunId(runtimeRoot)
2639
+ if (!executeRunId) {
2640
+ executeRunId = generateExecuteRunId()
2641
+ }
2642
+
2643
+ const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId })
2644
+ printReviewResult(reviewResult)
2645
+
2646
+ if (!reviewResult.ok) {
2647
+ // Task review 校验失败,阻断 execute 完成
2648
+ // 检查是否存在 checkbox 已勾但 review 不通过的情况
2649
+ const uncheckedTasks = reviewResult.errors.filter(e => e.includes('缺少 review.json'))
2650
+ if (uncheckedTasks.length > 0) {
2651
+ console.error('\n⚠️ 部分任务已在 plan.md 中勾选,但 review.json 不存在。')
2652
+ console.error(' 请取消勾选这些任务的 checkbox,或补充对应的 review.json。')
2653
+ }
2654
+ progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
2655
+ await pm._write(cwd, progress, changeName)
2656
+ triggerSync(cwd, changeName, platformOpts)
2657
+ return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
2658
+ }
2659
+
2660
+ // cannot_verify 的 requiredEvidence 写入 change 目录,供 verify 阶段消费
2661
+ if (reviewResult.requiredEvidence.length > 0) {
2662
+ const evidencePath = writeVerifyRequiredEvidence(join(effectiveSpecBase, 'changes', changeName), reviewResult.requiredEvidence)
2663
+ if (evidencePath) {
2664
+ console.log(`📄 verify-required-evidence.json 已写入: ${evidencePath}`)
2665
+ console.log(' verify 阶段必须满足这些证据要求。')
2666
+ }
2667
+ }
2668
+ }
2669
+ } catch (e) {
2670
+ console.warn(`⚠️ Task Review Gate 异常: ${e.message}`)
2671
+ // 不阻断,但记录异常
2672
+ }
2673
+ }
2562
2674
  } else if (actualCompleted < actualTotal) {
2563
2675
  // 实际步骤未全部完成,跳过 validator(状态可能不同步)
2564
2676
  console.log(`\n⚠️ 阶段校验跳过:${actualTotal} 步中仅 ${actualCompleted} 步标记为已完成,可能存在状态不同步。如确认阶段已完成,请运行 --status 确认。`)
@@ -2623,8 +2735,17 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2623
2735
  const { loadWorkflow, runPostCheck, formatCheckReport, saveWorkflowRun } = await import('./workflow.js')
2624
2736
  const wf = loadWorkflow(cwd, 'scan-docs')
2625
2737
  if (wf) {
2626
- // 确定当前项目:优先从 step metadata 读取,回退从 display name 提取
2627
- const currentProjectName = steps[currentIdx].project
2738
+ // 确定当前项目(优先级链):
2739
+ // progress.project (dbProjectName,平台模式真实项目名,与 outputStep 占位符渲染对齐)
2740
+ // > change?.project (变更对象的项目字段,平台模式 change 创建时传入)
2741
+ // > steps[idx].project (perProject 展开标记,兼容旧模式)
2742
+ // > steps[idx].name 正则提取 [xxx] 后缀
2743
+ // > null(回退检查所有项目)
2744
+ // task-05 修复:日志显示项目名变 frontend 是 perProject 误展开 bug,
2745
+ // 用 progress.project(与 outputStep 占位符渲染路径一致)修正 myaaa/frontend 分裂。
2746
+ const currentProjectName = progress.project
2747
+ || (typeof change !== 'undefined' && change ? change.project : null)
2748
+ || steps[currentIdx].project
2628
2749
  || (steps[currentIdx].name.match(/\[([^\]]+)\]\s*$/) || [])[1]
2629
2750
  || null
2630
2751
 
@@ -2644,7 +2765,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2644
2765
 
2645
2766
  let anyFailed = false
2646
2767
  for (const pName of projectsToCheck) {
2647
- const result = runPostCheck(wf, cwd, pName)
2768
+ const result = runPostCheck(wf, cwd, pName, {}, specBase)
2648
2769
  const report = formatCheckReport(result)
2649
2770
  console.log(report)
2650
2771
  if (result.status === 'fail') {
@@ -2667,6 +2788,10 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2667
2788
  }
2668
2789
  if (anyFailed) {
2669
2790
  console.log(`\n⚠️ 存在检查失败项,请按上面的重试提示修复后再继续。`)
2791
+ // task-07: 阻断推进(与 task-06 平台模式 scan-postcheck 失败分支 return 结构对齐)
2792
+ // scan 深度扫描产物校验未通过时,不允许 clean success / 进入下一 step,
2793
+ // 让上层走"完成但不推进"分支,--done 被拒。
2794
+ return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
2670
2795
  }
2671
2796
  }
2672
2797
  } catch (e) {
@@ -2682,7 +2807,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2682
2807
  if (wf && changeName) {
2683
2808
  const raw = JSON.stringify(wf)
2684
2809
  const resolved = JSON.parse(raw.replace(/<change-name>/g, changeName))
2685
- const result = runPostCheck(resolved, cwd, 'sillyspec')
2810
+ const result = runPostCheck(resolved, cwd, 'sillyspec', {}, specBase)
2686
2811
  // 只报告 impact-analyzer 的结果(doc-syncer 是后续步骤)
2687
2812
  const impactResult = (result.roles || []).find(r => r.id === 'impact-analyzer')
2688
2813
  if (impactResult) {
@@ -587,9 +587,11 @@ export function getContract(stageName) {
587
587
  * 校验状态转换是否允许
588
588
  * @param {string} fromStage - 当前阶段(空字符串表示变更起始)
589
589
  * @param {string} toStage - 目标阶段
590
+ * @param {{ fromStageData?: { status?: string } | undefined }} [options] - 可选,从 progress.stages[prevStage] 提取
590
591
  * @returns {{ allowed: boolean, reason?: string }}
591
592
  */
592
- export function checkTransition(fromStage, toStage) {
593
+ export function checkTransition(fromStage, toStage, options = {}) {
594
+ const { fromStageData } = options // { status?: string } | undefined
593
595
  const contract = contracts[toStage]
594
596
  if (!contract) {
595
597
  return { allowed: false, reason: `未知阶段: ${toStage}` }
@@ -605,6 +607,17 @@ export function checkTransition(fromStage, toStage) {
605
607
  return { allowed: true }
606
608
  }
607
609
 
610
+ // task-07: failed_post_check 门控
611
+ // scan post-check 未通过时,禁止进入主流程的下游阶段(brainstorm/plan/execute/verify/archive)
612
+ // 必须先重跑 scan 修复。toStage === 'scan' 的重跑路径已被上方 fromStage === toStage 放行。
613
+ // fromStageData.status 缺失(旧数据)时门控不触发(向后兼容)。
614
+ if (fromStage === 'scan' && fromStageData?.status === 'failed_post_check' && toStage !== 'scan') {
615
+ return {
616
+ allowed: false,
617
+ reason: 'scan post-check 未通过(failed_post_check),需修复后重跑 scan 再进入 ' + toStage,
618
+ }
619
+ }
620
+
608
621
  // archive 特殊处理:从 verify 来的允许,从其他主流程阶段来的需要校验
609
622
  if (toStage === 'archive') {
610
623
  if (fromStage === 'verify') {
@@ -531,10 +531,40 @@ ${taskList}
531
531
  - 最后一个 Wave 完成后做一次全量编译验证
532
532
  - 用户明确要求编译时
533
533
  4. 每个任务完成后:
534
- - 勾选 plan.md / tasks.md 中对应任务的 checkbox
534
+ - **先写 review.json 再勾选 checkbox**(见下方 Task Review Gate)
535
535
  - 记录改动文件和测试结果
536
536
  5. 遇到 BLOCKED → 记录原因,选择:重试/跳过/停止
537
537
 
538
+ ### Task Review Gate(必须执行,不可跳过)
539
+
540
+ 每个子代理完成后、勾选 checkbox **之前**,你必须创建 task review。
541
+
542
+ **操作步骤:**
543
+ 1. 读取当前 task 的 git diff(从 task 开始到完成的变更)
544
+ 2. 对照 plan.md 中该 task 的描述和 tasks/task-XX.md(如果存在)检查实现是否符合要求
545
+ 3. 写入 review.json 文件
546
+ 4. **只有 review.json 写入成功后,才允许勾选 plan.md 中的 checkbox**
547
+
548
+ **review.json 路径:**
549
+
550
+ task-XX 对应:.sillyspec/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-XX/review.json
551
+
552
+ 本 execute run 的固定 ID 是:{EXECUTE_RUN_ID}
553
+ **所有 task 的 review.json 必须使用这个 ID,不要自行创建新目录。**
554
+
555
+ **review.json 必填字段:**
556
+
557
+ { "schemaVersion": 1, "task": "task-XX", "base": "<git-base-commit>", "head": "<git-head-commit>",
558
+ "changedFiles": ["src/foo.js"], "specVerdict": "pass|fail|cannot_verify",
559
+ "qualityVerdict": "pass|fail|cannot_verify", "reviewerNotes": "评审说明",
560
+ "requiredEvidence": [] }
561
+
562
+ **评审铁律:**
563
+ - 不信任 implementer 自报结果,对照 diff 和 task brief 验证
564
+ - 只看当前 task 的 diff,不做全仓库漫游审查
565
+ - \`cannot_verify\` 只在确实无法验证且有待补充证据时使用,且 requiredEvidence 必须非空
566
+ - \`sillyspec run execute --done\` 会校验所有 task 的 review.json,缺失或 fail 会阻断完成
567
+
538
568
  ### 完成后
539
569
  1. 为每个后端 router task,扫描变更文件提取 API 端点 artifact:
540
570
  - 在变更文件中搜索所有 router 注册路径(@router.get/post/put/delete)
@@ -543,7 +573,6 @@ ${taskList}
543
573
  2. 运行 sillyspec run execute --done --input "用户原始反馈" --output "Wave ${waveIndex} 结果摘要"`
544
574
  }
545
575
 
546
-
547
576
  /**
548
577
  * 动态构建 execute 步骤列表
549
578
  * @param {string|null} planFilePath - plan 文件路径,null 则用默认 3 Wave
@@ -62,6 +62,14 @@ export const definition = {
62
62
  5. 加载代码规范:\`cat .sillyspec/docs/<project>/scan/CONVENTIONS.md 2>/dev/null\`
63
63
  6. 标注每个文件的存在/不存在状态
64
64
 
65
+ ### Execute Evidence 传递检查
66
+ 7. 检查 verify-required-evidence.json 是否存在(由 execute 阶段 Task Review Gate 写入)
67
+ - 路径:变更目录下的 verify-required-evidence.json
68
+ - 如果存在 → 读取其中的 requiredEvidence 列表,逐条验证是否已满足
69
+ - 每条 evidence 必须在 verify-result.md 中给出明确结论(satisfied / missing / partial)
70
+ - 如果有任何 evidence 为 missing → verify 结论不能为 PASS
71
+ - 如果文件不存在 → 表示 execute 阶段无 cannot_verify 任务,正常继续
72
+
65
73
  ### 模块文档加载
66
74
  7. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
67
75
  8. 根据 design.md 的文件变更清单匹配 _module-map.yaml 中的模块