sillyspec 3.13.0 → 3.14.1

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.13.0",
3
+ "version": "3.14.1",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -27,6 +27,7 @@
27
27
  "@inquirer/prompts": "^7.10.1",
28
28
  "chalk": "^5.6.2",
29
29
  "chokidar": "^4.0",
30
+ "js-yaml": "^4.2.0",
30
31
  "open": "^10.1",
31
32
  "ora": "^9.3.0",
32
33
  "sql.js": "^1.14.1",
package/src/index.js CHANGED
@@ -94,6 +94,7 @@ async function main() {
94
94
 
95
95
  // 解析全局选项
96
96
  let json = false;
97
+ let saveWorkflowRunFlag = false;
97
98
  let targetDir = process.cwd();
98
99
  let tool = null;
99
100
  let interactive = false;
@@ -102,6 +103,8 @@ async function main() {
102
103
  for (let i = 0; i < args.length; i++) {
103
104
  if (args[i] === '--json') {
104
105
  json = true;
106
+ } else if (args[i] === '--save') {
107
+ saveWorkflowRunFlag = true;
105
108
  } else if (args[i] === '--dir' && args[i + 1]) {
106
109
  targetDir = resolve(args[i + 1]);
107
110
  i++;
@@ -485,6 +488,137 @@ SillySpec platform — SillyHub 平台同步
485
488
  await pm.renameChange(dir, oldName, newName);
486
489
  break;
487
490
  }
491
+ case 'workflow': {
492
+ const wfSub = filteredArgs[1];
493
+ if (!wfSub || wfSub === 'help' || wfSub === '--help') {
494
+ console.log(`
495
+ SillySpec workflow — 工作流管理
496
+
497
+ 用法:
498
+ sillyspec workflow check <name> [--project <project>] [--json]
499
+ sillyspec workflow list
500
+ `);
501
+ break;
502
+ }
503
+ if (wfSub === 'list') {
504
+ const { listWorkflows } = await import('./workflow.js');
505
+ const names = listWorkflows(dir);
506
+ if (names.length === 0) {
507
+ console.log('未找到 workflow 定义(.sillyspec/workflows/*.yaml)');
508
+ } else {
509
+ console.log(`可用 workflow:`);
510
+ for (const name of names) {
511
+ const { loadWorkflow } = await import('./workflow.js');
512
+ const wf = loadWorkflow(dir, name);
513
+ const specVer = wf?.spec_version || wf?.version || '?';
514
+ const mode = wf?.orchestration?.mode || '?';
515
+ const roles = wf?.roles?.length || 0;
516
+ console.log(` ${name} (spec v${specVer}, ${mode}, ${roles} roles)`);
517
+ }
518
+ }
519
+ break;
520
+ }
521
+ if (wfSub === 'check') {
522
+ const { loadWorkflow, runPostCheck, listWorkflows, saveWorkflowRun } = await import('./workflow.js');
523
+ const wfName = filteredArgs[2];
524
+ if (!wfName) {
525
+ console.error('❌ 请指定 workflow 名称,例如:sillyspec workflow check scan-docs --project sillyspec');
526
+ process.exit(2);
527
+ }
528
+ const wf = loadWorkflow(dir, wfName);
529
+ if (!wf) {
530
+ console.error(`❌ 未找到 workflow: ${wfName}`);
531
+ console.error(`可用 workflow:${listWorkflows(dir).join(', ') || '无'}`);
532
+ process.exit(2);
533
+ }
534
+ // depends_on 校验
535
+ if (wf._validationErrors && wf._validationErrors.length > 0) {
536
+ console.error('❌ workflow YAML 校验失败:');
537
+ for (const err of wf._validationErrors) {
538
+ console.error(` ${err}`);
539
+ }
540
+ process.exit(2);
541
+ }
542
+ // spec_version 校验
543
+ const specVer = wf.spec_version || wf.version;
544
+ if (!specVer) {
545
+ console.error('❌ workflow YAML 缺少 spec_version 字段');
546
+ process.exit(2);
547
+ }
548
+ const SUPPORTED_SPECS = [1];
549
+ if (!SUPPORTED_SPECS.includes(specVer)) {
550
+ console.error(`❌ 不支持的 spec_version: ${specVer}(支持: ${SUPPORTED_SPECS.join(', ')})`);
551
+ process.exit(2);
552
+ }
553
+ // 解析 --project
554
+ const projectIdx = filteredArgs.indexOf('--project');
555
+ const project = projectIdx !== -1 && filteredArgs[projectIdx + 1] ? filteredArgs[projectIdx + 1] : null;
556
+ // 解析 --json(已在顶层解析)
557
+ const isJson = json;
558
+ // 解析 --change
559
+ const changeIdx = filteredArgs.indexOf('--change');
560
+ const changeName = changeIdx !== -1 && filteredArgs[changeIdx + 1] ? filteredArgs[changeIdx + 1] : null;
561
+
562
+ if (!project && wfName !== 'archive-impact') {
563
+ console.error('❌ 请指定 --project,例如:--project sillyspec');
564
+ process.exit(2);
565
+ }
566
+
567
+ // 执行检查
568
+ let resolvedWf = wf;
569
+ const placeholders = {};
570
+ if (changeName) placeholders['change-name'] = changeName;
571
+ // 替换占位符
572
+ let jsonStr = JSON.stringify(resolvedWf);
573
+ if (changeName) jsonStr = jsonStr.replace(/<change-name>/g, changeName);
574
+ resolvedWf = JSON.parse(jsonStr);
575
+
576
+ const projectName = project || 'sillyspec';
577
+ const result = runPostCheck(resolvedWf, dir, projectName, placeholders);
578
+
579
+ if (isJson) {
580
+ console.log(JSON.stringify(result, null, 2));
581
+ } else {
582
+ // 带项目维度前缀的输出(从统一结果对象格式化)
583
+ const lines = [`\n📋 Workflow Post-Check: ${result.workflow} (project: ${result.project})\n`];
584
+ for (const r of (result.roles || [])) {
585
+ const icon = r.status === 'pass' ? '✅' : '❌';
586
+ lines.push(`${icon} [${result.project}] ${r.name} (${r.id})`);
587
+ const roleFailures = (result.failures || []).filter(f => f.role_id === r.id);
588
+ for (const f of roleFailures) {
589
+ lines.push(` └─ ${f.message}`);
590
+ }
591
+ }
592
+ const wfFailures = (result.workflow_checks || []).filter(c => c.status === 'fail');
593
+ if (wfFailures.length > 0) {
594
+ lines.push('');
595
+ for (const f of wfFailures) {
596
+ lines.push(`❌ [${result.project}] 全局: ${f.detail}`);
597
+ }
598
+ }
599
+ lines.push('');
600
+ if (result.status === 'pass') {
601
+ lines.push('✅ 全部检查通过');
602
+ } else {
603
+ lines.push('❌ 存在失败项');
604
+ }
605
+ console.log(lines.join('\n'));
606
+ }
607
+
608
+ // exit code: 0=通过, 1=检查失败, 2=参数/YAML错误
609
+ if (saveWorkflowRunFlag) {
610
+ const saved = saveWorkflowRun(result, { cwd: dir, source: 'cli' });
611
+ if (saved) {
612
+ if (!isJson) console.log(`\n📁 结果已归档:${saved}`);
613
+ }
614
+ }
615
+ process.exit(result.status === 'pass' ? 0 : 1);
616
+ } else {
617
+ console.error(`❌ 未知子命令: workflow ${wfSub}`);
618
+ process.exit(1);
619
+ }
620
+ break;
621
+ }
488
622
  case 'modules': {
489
623
  const modulesSub = filteredArgs[1];
490
624
  if (!modulesSub || modulesSub === 'help' || modulesSub === '--help') {
package/src/init.js CHANGED
@@ -125,6 +125,22 @@ async function doInstall(projectDir, tools, subprojects = []) {
125
125
  const gitkeepPath = join(scanDir, '.gitkeep');
126
126
  if (!existsSync(gitkeepPath)) writeFileSync(gitkeepPath, '');
127
127
 
128
+ // 复制 workflow 模板到 .sillyspec/workflows/
129
+ const workflowsDir = join(projectDir, '.sillyspec', 'workflows');
130
+ const templatesDir = join(__dirname, '..', 'templates', 'workflows');
131
+ if (existsSync(templatesDir)) {
132
+ mkdirSync(workflowsDir, { recursive: true });
133
+ for (const file of readdirSync(templatesDir)) {
134
+ if (file.endsWith('.yaml')) {
135
+ const srcPath = join(templatesDir, file);
136
+ const dstPath = join(workflowsDir, file);
137
+ if (!existsSync(dstPath)) {
138
+ writeFileSync(dstPath, readFileSync(srcPath));
139
+ }
140
+ }
141
+ }
142
+ }
143
+
128
144
  // 创建 shared/workspace 目录
129
145
  mkdirSync(join(projectDir, '.sillyspec', 'shared'), { recursive: true });
130
146
  mkdirSync(join(projectDir, '.sillyspec', 'workspace'), { recursive: true });
package/src/run.js CHANGED
@@ -584,6 +584,88 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
584
584
  }
585
585
  }
586
586
 
587
+ // scan 阶段 step 2 "构建扫描项目列表" 完成后,按项目展开 perProject 步骤
588
+ if (stageName === 'scan' && steps[currentIdx]?.name === '构建扫描项目列表') {
589
+ // 解析项目列表:从 step 2 输出提取,或回退读取 projects/*.yaml
590
+ let projectNames = []
591
+ if (outputText) {
592
+ // 匹配 "1. project-name" 格式
593
+ const matches = outputText.match(/^\s*\d+\.\s+(\S+)/gm)
594
+ if (matches) {
595
+ projectNames = matches.map(m => m.replace(/^\s*\d+\.\s+/, '').replace(/[—\-:].*$/, '').trim())
596
+ }
597
+ }
598
+ if (projectNames.length === 0) {
599
+ // 回退:读取所有已注册项目
600
+ console.warn('⚠️ 未能从 step 2 输出解析项目列表,回退扫描所有注册项目')
601
+ const projectsDir = join(cwd, '.sillyspec', 'projects')
602
+ if (existsSync(projectsDir)) {
603
+ projectNames = readdirSync(projectsDir)
604
+ .filter(f => f.endsWith('.yaml'))
605
+ .map(f => f.replace(/\.yaml$/, ''))
606
+ }
607
+ }
608
+ if (projectNames.length === 0) {
609
+ projectNames = ['sillyspec'] // 最终兜底
610
+ }
611
+
612
+ // 保存到 runtime 供后续使用 + 防重复展开
613
+ const scanStatePath = join(cwd, '.sillyspec', '.runtime', 'scan-projects.json')
614
+ mkdirSync(join(cwd, '.sillyspec', '.runtime'), { recursive: true })
615
+ let scanState = { projects: projectNames, expanded: false }
616
+ if (existsSync(scanStatePath)) {
617
+ try { scanState = JSON.parse(readFileSync(scanStatePath, 'utf8')) } catch {}
618
+ }
619
+
620
+ // 收集当前步骤之后所有 perProject 步骤
621
+ const stageDef = stageRegistry[stageName]
622
+ const allSteps = stageDef?.steps || []
623
+ const perProjectSteps = allSteps.filter(s => s.perProject)
624
+
625
+ // 防重复展开:runtime 标记 或 steps 已含项目标识
626
+ const alreadyExpanded = scanState.expanded || steps.some(s => s.name?.match(/\[.+\]\s*$/))
627
+ if (!alreadyExpanded && perProjectSteps.length > 0) {
628
+ // 找到当前步骤(step 2)在动态 steps 中的位置
629
+ const insertBase = currentIdx + 1
630
+ let insertPos = insertBase
631
+ for (const pName of projectNames) {
632
+ // 读取项目配置获取 projectRoot
633
+ const projYaml = join(cwd, '.sillyspec', 'projects', `${pName}.yaml`)
634
+ let projectRoot = '.'
635
+ if (existsSync(projYaml)) {
636
+ const yamlContent = readFileSync(projYaml, 'utf8')
637
+ const pathMatch = yamlContent.match(/^path:\s*(.+)/m)
638
+ if (pathMatch) projectRoot = pathMatch[1].trim()
639
+ }
640
+ const docOutputDir = `.sillyspec/docs/${pName}`
641
+ const contextPrefix = `\n---\n## 当前项目\n- **项目名**: ${pName}\n- **项目路径**: ${projectRoot}\n- **文档输出**: ${docOutputDir}\n\n⚠️ 本步骤只处理上面这个项目,不要处理其他项目。\n---\n\n`
642
+
643
+ for (const ppStep of perProjectSteps) {
644
+ steps.splice(insertPos, 0, {
645
+ name: `${ppStep.name} [${pName}]`,
646
+ project: pName,
647
+ status: 'pending',
648
+ prompt: contextPrefix + ppStep.prompt,
649
+ outputHint: ppStep.outputHint,
650
+ optional: ppStep.optional
651
+ })
652
+ insertPos++
653
+ }
654
+ }
655
+ // 移除原始的 perProject 步骤(未展开的版本)
656
+ for (let i = steps.length - 1; i >= 0; i--) {
657
+ if (steps[i].perProject && !steps[i].name?.includes('[')) {
658
+ steps.splice(i, 1)
659
+ }
660
+ }
661
+ console.log(` 📝 已按项目展开 ${perProjectSteps.length} 个步骤 × ${projectNames.length} 个项目 = ${perProjectSteps.length * projectNames.length} 个项目步骤`)
662
+ console.log(` 📁 扫描项目:${projectNames.join(', ')}`)
663
+ // 标记已展开,防止 resume 重复插入
664
+ scanState.expanded = true
665
+ writeFileSync(scanStatePath, JSON.stringify(scanState))
666
+ }
667
+ }
668
+
587
669
  const nextPendingIdx = steps.findIndex(s => s.status === 'pending')
588
670
 
589
671
  if (nextPendingIdx === -1) {
@@ -687,6 +769,83 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
687
769
 
688
770
  const defSteps = await getStageSteps(stageName, cwd, progress)
689
771
  console.log(`✅ Step ${currentIdx + 1}/${steps.length} 完成:${steps[currentIdx].name}\n`)
772
+
773
+ // Workflow post_check:scan 深度扫描完成后自动检查产物
774
+ if (stageName === 'scan' && steps[currentIdx]?.name?.includes('深度扫描')) {
775
+ try {
776
+ const { loadWorkflow, runPostCheck, formatCheckReport, saveWorkflowRun } = await import('./workflow.js')
777
+ const wf = loadWorkflow(cwd, 'scan-docs')
778
+ if (wf) {
779
+ // 确定当前项目:优先从 step metadata 读取,回退从 display name 提取
780
+ const currentProjectName = steps[currentIdx].project
781
+ || (steps[currentIdx].name.match(/\[([^\]]+)\]\s*$/) || [])[1]
782
+ || null
783
+
784
+ // 确定要检查的项目列表
785
+ let projectsToCheck = []
786
+ if (currentProjectName) {
787
+ // 按项目展开模式:只检查当前项目
788
+ projectsToCheck = [currentProjectName]
789
+ } else {
790
+ // 兼容旧模式(未展开):检查所有项目
791
+ const projectsDir = join(cwd, '.sillyspec', 'projects')
792
+ const projectFiles = existsSync(projectsDir)
793
+ ? readdirSync(projectsDir).filter(f => f.endsWith('.yaml'))
794
+ : []
795
+ projectsToCheck = projectFiles.map(f => f.replace(/\.yaml$/, ''))
796
+ }
797
+
798
+ let anyFailed = false
799
+ for (const pName of projectsToCheck) {
800
+ const result = runPostCheck(wf, cwd, pName)
801
+ const report = formatCheckReport(result)
802
+ console.log(report)
803
+ if (result.status === 'fail') {
804
+ anyFailed = true
805
+ // retry_prompts 由 _checkWorkflow 自动生成
806
+ for (const rp of (result.retry_prompts || [])) {
807
+ console.log(`\n🔄 重试提示(项目 ${pName}):\n`)
808
+ console.log(rp.prompt)
809
+ }
810
+ }
811
+ const saved = saveWorkflowRun(result, { cwd, source: 'run.js', stage: 'verify', step: steps[currentIdx]?.name })
812
+ if (saved) console.log(`📁 结果已归档:${saved}`)
813
+ }
814
+ if (anyFailed) {
815
+ console.log(`\n⚠️ 存在检查失败项,请按上面的重试提示修复后再继续。`)
816
+ }
817
+ }
818
+ } catch (e) {
819
+ console.warn(`⚠️ workflow 检查跳过:${e.message}`)
820
+ }
821
+ }
822
+
823
+ // Workflow post_check:archive extract-module-impact 完成后检查产物
824
+ if (stageName === 'archive' && steps[currentIdx]?.name?.includes('extract-module-impact')) {
825
+ try {
826
+ const { loadWorkflow, runPostCheck, formatCheckReport, saveWorkflowRun } = await import('./workflow.js')
827
+ const wf = loadWorkflow(cwd, 'archive-impact')
828
+ if (wf && changeName) {
829
+ const raw = JSON.stringify(wf)
830
+ const resolved = JSON.parse(raw.replace(/<change-name>/g, changeName))
831
+ const result = runPostCheck(resolved, cwd, 'sillyspec')
832
+ // 只报告 impact-analyzer 的结果(doc-syncer 是后续步骤)
833
+ const impactResult = (result.roles || []).find(r => r.id === 'impact-analyzer')
834
+ if (impactResult) {
835
+ const icon = impactResult.status === 'pass' ? '✅' : '❌'
836
+ console.log(`${icon} module-impact.md 检查${impactResult.status === 'pass' ? '通过' : '失败'}`)
837
+ for (const f of (result.failures || []).filter(f => f.role_id === 'impact-analyzer')) {
838
+ console.log(` └─ ${f}`)
839
+ }
840
+ }
841
+ const saved = saveWorkflowRun(result, { cwd, source: 'run.js', stage: 'archive', step: steps[currentIdx]?.name })
842
+ if (saved) console.log(`📁 结果已归档:${saved}`)
843
+ }
844
+ } catch (e) {
845
+ console.warn(`⚠️ workflow 检查跳过:${e.message}`)
846
+ }
847
+ }
848
+
690
849
  if (printNext) {
691
850
  await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null)
692
851
  }
@@ -20,20 +20,21 @@ export const definition = {
20
20
  },
21
21
  {
22
22
  name: 'extract-module-impact',
23
- prompt: `分析本次变更影响的模块,生成模块影响记录。
23
+ prompt: `按照 \`.sillyspec/workflows/archive-impact.yaml\` 中定义的 \`impact-analyzer\` 角色规则,分析本次变更影响的模块。
24
24
 
25
25
  ### 操作
26
- 1. 读取变更目录下的 proposal.md、design.md、tasks.md
27
- 2. 运行 \`git diff --name-only HEAD~1\`(或 \`git diff --name-only --cached\`)获取真实修改文件列表
28
- 3. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`
26
+ 1. 读取 \`.sillyspec/workflows/archive-impact.yaml\`,了解角色定义和检查规则
27
+ 2. 读取变更目录下的 proposal.md、design.md、tasks.md
28
+ 3. 运行 \`git diff --name-only HEAD~1\`(或 \`git diff --name-only --cached\`)获取真实修改文件列表
29
+ 4. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`
29
30
  - **如果不存在**:提示"建议运行 scan 生成模块映射",但继续执行。跳到步骤 7 生成只有 unmapped 部分的 module-impact.md
30
- 4. 三重交叉验证:
31
+ 5. 三重交叉验证:
31
32
  - 声明范围:proposal.md / design.md 中的"变更范围"/"文件变更清单"
32
33
  - 任务范围:tasks.md / plan.md 中的任务文件路径
33
34
  - 真实变更:git diff 文件列表
34
35
  - **以 git diff 为准**(真实 > 声明)
35
- 5. 将 git diff 文件按 \`_module-map.yaml\` 的 paths glob 匹配到模块
36
- 6. 生成模块影响矩阵:
36
+ 6. 将 git diff 文件按 \`_module-map.yaml\` 的 paths glob 匹配到模块
37
+ 7. 生成模块影响矩阵:
37
38
 
38
39
  | 模块 | 影响类型 | 相关文件 | 更新内容摘要 | needs_review |
39
40
  |------|----------|----------|-------------|-------------|
@@ -41,33 +42,10 @@ export const definition = {
41
42
  影响类型:逻辑变更 / 数据结构变更 / 接口变更 / 调用关系变更 / 配置变更 / 新增
42
43
  needs_review:如果影响无法完全确定,标记为 true
43
44
 
44
- 7. 未匹配到任何模块的文件归入"未匹配文件"表格
45
- 8. 生成 \`.sillyspec/changes/<change-name>/module-impact.md\`,格式:
46
-
47
- \`\`\`markdown
48
- # 模块影响分析
49
-
50
- author: <git-user>
51
- created_at: <now-datetime>
52
-
53
- ## 变更:<change-name>
54
-
55
- ## 模块影响矩阵
56
- | 模块 | 影响类型 | 相关文件 | 更新内容摘要 | needs_review |
57
- |------|----------|----------|-------------|-------------|
58
-
59
- ## 未匹配文件
60
- | 文件路径 | 说明 |
61
- |----------|------|
62
-
63
- ## 更新结果
64
- (sync-module-docs 步骤完成后回填)
65
- | 目标 | 操作 | 状态 |
66
- |------|------|------|
67
- \`\`\`
68
-
69
- ### 输出
70
- module-impact.md 路径 + 影响模块数量 + 未匹配文件数量`,
45
+ 8. 未匹配到任何模块的文件归入"未匹配文件"表格
46
+ 9. 生成 \`.sillyspec/changes/<change-name>/module-impact.md\`
47
+ 10. 完成后运行 workflow 检查:
48
+ \`node -e "import('./src/workflow.js').then(w => { /* 用 loadWorkflow 加载 archive-impact,用 runPostCheck 检查 */ })\``,
71
49
  outputHint: 'module-impact.md 路径 + 影响摘要',
72
50
  optional: false
73
51
  },