sillyspec 3.20.2 → 3.20.3

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.
Files changed (208) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +1 -5
  2. package/.claude/skills/sillyspec-auto/SKILL.md +2 -8
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -28
  4. package/.claude/skills/sillyspec-commit/SKILL.md +3 -4
  5. package/.claude/skills/sillyspec-continue/SKILL.md +4 -5
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +3 -12
  7. package/.claude/skills/sillyspec-execute/SKILL.md +2 -15
  8. package/.claude/skills/sillyspec-explore/SKILL.md +1 -14
  9. package/.claude/skills/sillyspec-plan/SKILL.md +36 -5
  10. package/.claude/skills/sillyspec-propose/SKILL.md +0 -4
  11. package/.claude/skills/sillyspec-quick/SKILL.md +1 -5
  12. package/.claude/skills/sillyspec-resume/SKILL.md +66 -23
  13. package/.claude/skills/sillyspec-scan/SKILL.md +1 -5
  14. package/.claude/skills/sillyspec-state/SKILL.md +8 -8
  15. package/.claude/skills/sillyspec-status/SKILL.md +1 -5
  16. package/.claude/skills/sillyspec-verify/SKILL.md +1 -5
  17. package/.claude/skills/sillyspec-workspace/SKILL.md +3 -11
  18. package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +97 -0
  19. package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +51 -0
  20. package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +29 -0
  21. package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +34 -0
  22. package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +13 -0
  23. package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +43 -0
  24. package/.sillyspec/changes/auto-mode/design.md +50 -0
  25. package/.sillyspec/changes/auto-mode/proposal.md +19 -0
  26. package/.sillyspec/changes/auto-mode/requirements.md +21 -0
  27. package/.sillyspec/changes/auto-mode/tasks.md +7 -0
  28. package/.sillyspec/changes/brainstorm-archive/2026-04-05-dashboard-design.md +206 -0
  29. package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +199 -0
  30. package/.sillyspec/changes/dashboard/design.md +219 -0
  31. package/.sillyspec/changes/dashboard/design.md.braindraft +206 -0
  32. package/.sillyspec/changes/run-command-design/design.md +1230 -0
  33. package/.sillyspec/changes/unified-docs-design/design.md +199 -0
  34. package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
  35. package/.sillyspec/knowledge/INDEX.md +8 -0
  36. package/.sillyspec/knowledge/uncategorized.md +3 -0
  37. package/.sillyspec/plans/2026-04-05-dashboard.md +737 -0
  38. package/.sillyspec/projects/sillyspec.yaml +3 -0
  39. package/README.md +11 -13
  40. package/SKILL.md +40 -44
  41. package/dist/steps/brainstorm/01-load-context.md +30 -0
  42. package/dist/steps/brainstorm/02-reuse-check.md +6 -0
  43. package/dist/steps/brainstorm/03-prototype-analysis.md +11 -0
  44. package/dist/steps/brainstorm/04-module-split.md +23 -0
  45. package/dist/steps/brainstorm/05-dialog-explore.md +8 -0
  46. package/dist/steps/brainstorm/06-propose-approaches.md +3 -0
  47. package/dist/steps/brainstorm/07-present-design.md +3 -0
  48. package/dist/steps/brainstorm/08-write-design.md +21 -0
  49. package/dist/steps/brainstorm/09-self-review.md +15 -0
  50. package/dist/steps/brainstorm/10-user-confirm.md +3 -0
  51. package/dist/steps/brainstorm/11-output-spec.md +7 -0
  52. package/dist/steps/brainstorm/manifest.yaml +26 -0
  53. package/dist/steps/execute/01-load-context.md +41 -0
  54. package/dist/steps/execute/02-scan-conventions.md +47 -0
  55. package/dist/steps/execute/03-skill-mcp.md +19 -0
  56. package/dist/steps/execute/04-assign-task.md +22 -0
  57. package/dist/steps/execute/04b-prompt-template.md +54 -0
  58. package/dist/steps/execute/05-write-test.md +7 -0
  59. package/dist/steps/execute/06-write-code.md +8 -0
  60. package/dist/steps/execute/07-run-test.md +26 -0
  61. package/dist/steps/execute/08-fix-issues.md +28 -0
  62. package/dist/steps/execute/09-next-task.md +33 -0
  63. package/dist/steps/execute/manifest.yaml +28 -0
  64. package/dist/steps/plan/01-load-context.md +22 -0
  65. package/dist/steps/plan/02-anchor-confirm.md +1 -0
  66. package/dist/steps/plan/03-expand-tasks.md +33 -0
  67. package/dist/steps/plan/04-mark-order.md +15 -0
  68. package/dist/steps/plan/05-e2e-planning.md +17 -0
  69. package/dist/steps/plan/06-self-check.md +16 -0
  70. package/dist/steps/plan/07-save.md +1 -0
  71. package/dist/steps/plan/manifest.yaml +18 -0
  72. package/dist/steps/scan/01-env-detect.md +51 -0
  73. package/dist/steps/scan/02-tech-stack.md +16 -0
  74. package/dist/steps/scan/03-conventions.md +16 -0
  75. package/dist/steps/scan/04-structure.md +19 -0
  76. package/dist/steps/scan/05-quality.md +18 -0
  77. package/dist/steps/scan/06-complete.md +49 -0
  78. package/dist/steps/scan/manifest.yaml +16 -0
  79. package/dist/steps/verify/01-load-specs.md +28 -0
  80. package/dist/steps/verify/02-check-tasks.md +1 -0
  81. package/dist/steps/verify/03-check-design.md +6 -0
  82. package/dist/steps/verify/04-run-tests.md +7 -0
  83. package/dist/steps/verify/05-e2e-tests.md +27 -0
  84. package/dist/steps/verify/05b-e2e-fix.md +33 -0
  85. package/dist/steps/verify/06-code-quality.md +25 -0
  86. package/dist/steps/verify/07-lint-check.md +27 -0
  87. package/dist/steps/verify/08-output-report.md +14 -0
  88. package/dist/steps/verify/manifest.yaml +22 -0
  89. package/package.json +1 -7
  90. package/packages/dashboard/dist/assets/{index-Bq_Z2hne.js → index-D1EVTLmc.js} +1264 -1264
  91. package/packages/dashboard/dist/assets/index-DGe8CqeP.css +1 -0
  92. package/packages/dashboard/dist/index.html +2 -2
  93. package/packages/dashboard/package-lock.json +6 -6
  94. package/packages/dashboard/server/executor.js +1 -1
  95. package/packages/dashboard/server/index.js +18 -98
  96. package/packages/dashboard/server/parser.js +71 -139
  97. package/packages/dashboard/server/watcher.js +6 -14
  98. package/packages/dashboard/src/App.vue +185 -418
  99. package/packages/dashboard/src/components/ActionBar.vue +1 -10
  100. package/packages/dashboard/src/components/CommandPalette.vue +1 -5
  101. package/packages/dashboard/src/components/DocPreview.vue +8 -105
  102. package/packages/dashboard/src/components/DocTree.vue +19 -75
  103. package/packages/dashboard/src/components/PipelineStage.vue +2 -22
  104. package/packages/dashboard/src/components/PipelineView.vue +5 -32
  105. package/packages/dashboard/src/components/ProjectOverview.vue +139 -113
  106. package/packages/dashboard/src/components/StageBadge.vue +3 -17
  107. package/packages/dashboard/src/components/StepCard.vue +2 -7
  108. package/packages/dashboard/src/composables/useDashboard.js +0 -28
  109. package/src/derive.js +147 -0
  110. package/src/index.js +40 -688
  111. package/src/init.js +63 -119
  112. package/src/migrate.js +7 -7
  113. package/src/progress.js +248 -1474
  114. package/src/run.js +302 -3008
  115. package/src/setup.js +64 -2
  116. package/src/stages/archive.js +17 -123
  117. package/src/stages/brainstorm.js +48 -454
  118. package/src/stages/doctor.js +46 -99
  119. package/src/stages/execute.js +59 -420
  120. package/src/stages/index.js +18 -12
  121. package/src/stages/plan.js +189 -492
  122. package/src/stages/propose.js +11 -70
  123. package/src/stages/quick.js +13 -52
  124. package/src/stages/scan.js +68 -485
  125. package/src/stages/status.js +1 -1
  126. package/src/stages/verify.js +16 -203
  127. package/src/step.js +543 -0
  128. package/.claude/skills/sillyspec-knowledge/SKILL.md +0 -270
  129. package/.husky/pre-push +0 -13
  130. package/CLAUDE.md +0 -18
  131. package/docs/brainstorm-plan-contract.md +0 -64
  132. package/docs/plan-execute-contract.md +0 -123
  133. package/docs/platform-scan-protocol.md +0 -298
  134. package/docs/revision-mode.md +0 -115
  135. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +0 -99
  136. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +0 -218
  137. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +0 -167
  138. package/docs/sillyspec/file-lifecycle/storage-and-state.md +0 -148
  139. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +0 -193
  140. package/docs/sillyspec/file-lifecycle.md +0 -125
  141. package/docs/workflow-contract-regression.md +0 -106
  142. package/docs/worktree-isolation.md +0 -252
  143. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +0 -1
  144. package/packages/dashboard/dist/prototype-dashboard.html +0 -836
  145. package/packages/dashboard/dist/prototype-overview.html +0 -256
  146. package/packages/dashboard/public/prototype-dashboard.html +0 -836
  147. package/packages/dashboard/public/prototype-overview.html +0 -256
  148. package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
  149. package/packages/dashboard/src/components/ProjectCard.vue +0 -187
  150. package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
  151. package/packages/dashboard/src/composables/useLayout.js +0 -131
  152. package/src/brainstorm-postcheck.js +0 -158
  153. package/src/change-list.js +0 -52
  154. package/src/change-risk-profile.js +0 -352
  155. package/src/classify-change.js +0 -73
  156. package/src/constants.js +0 -70
  157. package/src/contract-matrix.js +0 -278
  158. package/src/db.js +0 -201
  159. package/src/endpoint-extractor.js +0 -315
  160. package/src/hooks/claude-pre-tool-use.cjs +0 -125
  161. package/src/hooks/worktree-guard.js +0 -761
  162. package/src/knowledge-match.js +0 -130
  163. package/src/modules.js +0 -482
  164. package/src/scan-postcheck.js +0 -383
  165. package/src/stage-contract.js +0 -700
  166. package/src/stages/brainstorm-auto.js +0 -229
  167. package/src/stages/explore.js +0 -34
  168. package/src/stages/knowledge.js +0 -498
  169. package/src/stages/plan-postcheck.js +0 -513
  170. package/src/sync.js +0 -497
  171. package/src/task-review.js +0 -346
  172. package/src/workflow.js +0 -785
  173. package/src/worktree-apply.js +0 -549
  174. package/src/worktree.js +0 -932
  175. package/templates/workflows/archive-impact.yaml +0 -79
  176. package/templates/workflows/scan-docs.yaml +0 -132
  177. package/test/brainstorm-plan-contract.test.mjs +0 -273
  178. package/test/check-syntax.mjs +0 -26
  179. package/test/cli-top-level-aliases.test.mjs +0 -174
  180. package/test/contract-artifacts.test.mjs +0 -323
  181. package/test/decision-supersede.test.mjs +0 -277
  182. package/test/knowledge-match.test.mjs +0 -231
  183. package/test/plan-execute-contract.test.mjs +0 -330
  184. package/test/plan-optimization.test.mjs +0 -572
  185. package/test/platform-artifacts.test.mjs +0 -190
  186. package/test/platform-failure-samples.test.mjs +0 -199
  187. package/test/platform-recovery-chain.test.mjs +0 -179
  188. package/test/platform-recovery.test.mjs +0 -167
  189. package/test/platform-scan-p0.test.mjs +0 -175
  190. package/test/revision-v1.test.mjs +0 -1145
  191. package/test/run-sanitize-project-name.test.mjs +0 -51
  192. package/test/run-scan-postcheck-fail.test.mjs +0 -64
  193. package/test/run-scan-project-parse.test.mjs +0 -200
  194. package/test/run-tests.mjs +0 -48
  195. package/test/scan-docs-yaml-placeholders.test.mjs +0 -84
  196. package/test/scan-knowledge.test.mjs +0 -175
  197. package/test/scan-paths.test.mjs +0 -68
  198. package/test/scan-postcheck-project-priority.test.mjs +0 -85
  199. package/test/scan-postcheck.test.mjs +0 -197
  200. package/test/scan-workflow-anyfailed-block.test.mjs +0 -52
  201. package/test/spec-dir.test.mjs +0 -206
  202. package/test/stage-contract-failed-post-check.test.mjs +0 -102
  203. package/test/stage-contract.test.mjs +0 -299
  204. package/test/stage-definitions.test.mjs +0 -39
  205. package/test/wait-gates.test.mjs +0 -501
  206. package/test/workflow-spec-base.test.mjs +0 -142
  207. package/test/worktree-guard.test.mjs +0 -136
  208. package/test/worktree-native-overlay.test.mjs +0 -188
package/src/workflow.js DELETED
@@ -1,785 +0,0 @@
1
- /**
2
- * SillySpec Workflow Engine
3
- *
4
- * 定义、检查和执行结构化工作流。
5
- * 职责:
6
- * - 加载 .sillyspec/workflows/*.yaml
7
- * - 运行 post_check 验证产物
8
- * - 按角色定位失败 + 生成重试 prompt
9
- * - 根据 role 定义生成 role prompts(Level 2)
10
- */
11
-
12
- import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from 'fs'
13
- import { join, resolve, basename } from 'path'
14
- import jsYaml from 'js-yaml'
15
- import { WORKFLOW_STATUS } from './constants.js'
16
-
17
- // ─── Workflow 加载 ───
18
-
19
- /**
20
- * 查找并加载指定名称的 workflow YAML
21
- * @param {string} cwd - 项目根目录
22
- * @param {string} name - workflow 名称(如 'scan-docs')
23
- * @returns {object|null} workflow 定义,或 null
24
- */
25
- export function loadWorkflow(cwd, name, validate = true) {
26
- const wfDir = join(cwd, '.sillyspec', 'workflows')
27
- if (!existsSync(wfDir)) return null
28
-
29
- // 优先找 <name>.yaml,其次找 <name>.yml
30
- for (const ext of ['.yaml', '.yml']) {
31
- const f = join(wfDir, `${name}${ext}`)
32
- if (existsSync(f)) {
33
- const raw = readFileSync(f, 'utf8')
34
- const wf = jsYaml.load(raw)
35
- if (validate) {
36
- const errors = validateWorkflow(wf)
37
- if (errors.length > 0) return { _validationErrors: errors, ...wf }
38
- }
39
- return wf
40
- }
41
- }
42
- return null
43
- }
44
-
45
- /**
46
- * 校验 workflow YAML 结构
47
- * @param {object} wf - workflow 定义
48
- * @returns {string[]} 错误列表,空数组表示通过
49
- */
50
- export function validateWorkflow(wf) {
51
- const errors = []
52
- const roles = wf.roles || []
53
- const roleIds = new Set(roles.map(r => r.id))
54
-
55
- for (const role of roles) {
56
- if (!role.id) {
57
- errors.push(`role 缺少 id 字段`)
58
- continue
59
- }
60
- // depends_on 校验
61
- const deps = role.depends_on || []
62
- for (const depId of deps) {
63
- if (!roleIds.has(depId)) {
64
- errors.push(`role "${role.id}" 的 depends_on 引用了不存在的 role "${depId}"`)
65
- }
66
- }
67
- // depends_on 循环检测(简单两层:A→B→A)
68
- for (const depId of deps) {
69
- const depRole = roles.find(r => r.id === depId)
70
- if (depRole && (depRole.depends_on || []).includes(role.id)) {
71
- errors.push(`循环依赖:"${role.id}" ↔ "${depId}"`)
72
- }
73
- }
74
- // from_role 校验
75
- const inputs = role.inputs || {}
76
- if (!Array.isArray(inputs)) {
77
- // inputs is a mapping
78
- if (inputs.from_role) {
79
- if (!roleIds.has(inputs.from_role)) {
80
- errors.push(`role "${role.id}" 的 inputs.from_role 引用了不存在的 role "${inputs.from_role}"`)
81
- }
82
- if (inputs.output) {
83
- const sourceRole = roles.find(r => r.id === inputs.from_role)
84
- if (sourceRole) {
85
- const outputExists = (sourceRole.outputs || []).some(o => {
86
- const outputName = o.name || o.path?.split('/').pop()?.replace(/\.md$/, '') || ''
87
- return outputName === inputs.output
88
- })
89
- if (!outputExists) {
90
- errors.push(`role "${role.id}" 的 inputs.from_role "${inputs.from_role}" 没有名为 "${inputs.output}" 的 output`)
91
- }
92
- }
93
- }
94
- if (!deps.includes(inputs.from_role)) {
95
- errors.push(`role "${role.id}" 的 inputs.from_role "${inputs.from_role}" 未在 depends_on 中声明`)
96
- }
97
- }
98
- } else {
99
- // inputs is an array (legacy format)
100
- for (const input of inputs) {
101
- if (input.from_role) {
102
- if (!roleIds.has(input.from_role)) {
103
- errors.push(`role "${role.id}" 的 inputs.from_role 引用了不存在的 role "${input.from_role}"`)
104
- }
105
- if (!deps.includes(input.from_role)) {
106
- errors.push(`role "${role.id}" 的 inputs.from_role "${input.from_role}" 未在 depends_on 中声明`)
107
- }
108
- }
109
- }
110
- }
111
- }
112
- return errors
113
- }
114
-
115
- /**
116
- * 列出所有可用 workflow
117
- */
118
- export function listWorkflows(cwd) {
119
- const wfDir = join(cwd, '.sillyspec', 'workflows')
120
- if (!existsSync(wfDir)) return []
121
- const files = readdirSync(wfDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
122
- return files.map(f => f.replace(/\.(yaml|yml)$/, ''))
123
- }
124
-
125
- // ─── 占位符替换 ───
126
-
127
- /**
128
- * 替换 workflow YAML 中的 <project> 占位符
129
- * @param {object} wf - workflow 定义(会被修改)
130
- * @param {string} projectName - 项目名
131
- */
132
- function replaceProjectPlaceholder(wf, projectName) {
133
- const json = JSON.stringify(wf)
134
- const replaced = json.replace(/<project>/g, projectName)
135
- return JSON.parse(replaced)
136
- }
137
-
138
- // ─── Post Check ───
139
-
140
- /**
141
- * 检查结果项
142
- * @typedef {{ role: string, output: string, path: string, check: string, passed: boolean, detail?: string }} CheckResult
143
- */
144
-
145
- /**
146
- * 对单个 output 运行检查
147
- * @param {object} outputDef - output 定义
148
- * @param {string} basePath - 被检查的文件所在目录
149
- * @param {string} cwd - 项目根目录
150
- * @returns {CheckResult}
151
- */
152
- function checkOutput(outputDef, projectName, cwd, specBase) {
153
- // specBase 优先(平台模式 platformOpts.specRoot,已含或不含 .sillyspec 语义);
154
- // 未传时回退 join(cwd, '.sillyspec'),等价于旧行为 resolve(cwd, '.sillyspec/...')
155
- const effectiveBase = specBase || join(cwd, '.sillyspec')
156
- // 将 <project> 替换为实际项目名
157
- let rawPath = (outputDef.path || '').replace(/<project>/g, projectName)
158
- // 旧版兼容:如果 path 以 .sillyspec/ 开头(相对路径),strip 前缀避免双拼接
159
- // 新版 yaml 用 {SPEC_ROOT} 已在 runPostCheck 中替换为绝对路径,不会走这里
160
- if (rawPath.startsWith('.sillyspec/')) {
161
- rawPath = rawPath.slice('.sillyspec/'.length)
162
- }
163
- const fullPath = resolve(effectiveBase, rawPath)
164
- const checks = outputDef.checks || []
165
- const results = []
166
-
167
- for (const check of checks) {
168
- switch (check.type) {
169
- case 'file_exists': {
170
- const exists = existsSync(fullPath)
171
- results.push({ passed: exists, check: 'file_exists', detail: exists ? '' : `文件不存在: ${rawPath}` })
172
- break
173
- }
174
- case 'no_empty_files': {
175
- if (existsSync(fullPath)) {
176
- const content = readFileSync(fullPath, 'utf8')
177
- const empty = content.trim().length === 0
178
- results.push({ passed: !empty, check: 'no_empty_files', detail: empty ? `文件为空: ${rawPath}` : '' })
179
- } else {
180
- results.push({ passed: false, check: 'no_empty_files', detail: `文件不存在: ${rawPath}` })
181
- }
182
- break
183
- }
184
- case 'min_lines': {
185
- if (existsSync(fullPath)) {
186
- const content = readFileSync(fullPath, 'utf8')
187
- const lines = content.split('\n').length
188
- const min = check.min || 1
189
- results.push({ passed: lines >= min, check: `min_lines(${min})`, detail: lines >= min ? '' : `文件只有 ${lines} 行,要求至少 ${min} 行: ${rawPath}` })
190
- } else {
191
- results.push({ passed: false, check: `min_lines(${check.min || 1})`, detail: `文件不存在: ${rawPath}` })
192
- }
193
- break
194
- }
195
- case 'contains_sections': {
196
- if (existsSync(fullPath)) {
197
- const content = readFileSync(fullPath, 'utf8')
198
- const sections = check.sections || []
199
- const missing = sections.filter(s => !content.includes(`## ${s}`))
200
- results.push({ passed: missing.length === 0, check: 'contains_sections', detail: missing.length > 0 ? `缺少章节: ${missing.join(', ')} — ${rawPath}` : '' })
201
- } else {
202
- results.push({ passed: false, check: 'contains_sections', detail: `文件不存在: ${rawPath}` })
203
- }
204
- break
205
- }
206
- case 'no_placeholder': {
207
- if (existsSync(fullPath)) {
208
- const content = readFileSync(fullPath, 'utf8')
209
- const patterns = check.patterns || ['待补充', 'TODO', 'TBD', '未分析', '根据项目情况', '根据实际情况', '按需填写']
210
- // 只匹配独立成行的占位文本,不匹配行内引用
211
- const lineMatches = patterns.filter(p => {
212
- const regex = new RegExp(`^\\s*[-*]?\\s*${p}\\s*$`, 'm')
213
- return regex.test(content)
214
- })
215
- results.push({ passed: lineMatches.length === 0, check: 'no_placeholder', detail: lineMatches.length > 0 ? `包含占位文本: ${lineMatches.map(m => `"${m}"`).join(', ')} — ${rawPath}` : '' })
216
- } else {
217
- results.push({ passed: false, check: 'no_placeholder', detail: `文件不存在: ${rawPath}` })
218
- }
219
- break
220
- }
221
- default:
222
- results.push({ passed: true, check: check.type, detail: `未知检查类型,跳过: ${check.type}` })
223
- }
224
- }
225
-
226
- return results
227
- }
228
-
229
- /**
230
- * 运行 workflow 的 post_check
231
- * @param {object} wf - workflow 定义
232
- * @param {string} cwd - 项目根目录
233
- * @param {string} projectName - 项目名
234
- * @returns {{ passed: boolean, roleResults: Array<{ roleId: string, roleName: string, passed: boolean, failures: string[] }>, workflowFailures: string[] }}
235
- */
236
- /**
237
- * 统一的 Workflow Check 结果协议
238
- * CLI 和 run.js 共用同一份结构化结果
239
- *
240
- * 返回结构:
241
- * {
242
- * workflow: string, // workflow 名称
243
- * project: string, // 项目名
244
- * status: 'pass'|'fail', // 总体状态
245
- * spec_version: number, // spec 版本
246
- * roles: [{ id, name, status, outputs: [{ path, status, checks: [{ type, status, detail }] }] }],
247
- * workflow_checks: [{ type, status, detail }],
248
- * failures: [{ level: 'role'|'workflow', role_id?, output?, check, message }],
249
- * retry_prompts: [{ role_id, role_name, prompt }]
250
- * }
251
- */
252
- export function runPostCheck(wf, cwd, projectName, placeholders = {}, specBase) {
253
- let resolved = replaceProjectPlaceholder(wf, projectName)
254
-
255
- // 自动注入 {SPEC_ROOT} 占位符(yaml 模板用 {SPEC_ROOT} 表示规范根目录)
256
- // effectiveBase 与 _checkWorkflow 内部一致:specBase || join(cwd, '.sillyspec')
257
- const effectiveBase = specBase || join(cwd, '.sillyspec')
258
- const allPlaceholders = { SPEC_ROOT: effectiveBase, ...placeholders }
259
-
260
- let json = JSON.stringify(resolved)
261
- for (const [key, value] of Object.entries(allPlaceholders)) {
262
- // 支持 {key} 和 <key> 两种占位符语法
263
- json = json.replace(new RegExp(`\{${key}\}`, 'g'), value)
264
- json = json.replace(new RegExp(`<${key}>`, 'g'), value)
265
- }
266
- resolved = JSON.parse(json)
267
-
268
- return _checkWorkflow(resolved, cwd, projectName, specBase)
269
- }
270
-
271
- function _checkWorkflow(wf, cwd, projectName, specBase) {
272
- const effectiveBase = specBase || join(cwd, '.sillyspec')
273
- const workflowName = wf.name || 'unknown'
274
- const specVersion = wf.spec_version || wf.version || 0
275
- const workflowChecks = wf.checks?.workflow_level || []
276
- const roleLevelChecks = wf.checks?.role_level || []
277
- const roles = []
278
- const failures = []
279
- const workflowCheckResults = []
280
-
281
- // 1. 角色级别检查
282
- for (const role of wf.roles || []) {
283
- const roleId = role.id
284
- const roleName = role.name || roleId
285
- const outputDefs = role.outputs || []
286
- const outputs = []
287
-
288
- for (const outputDef of outputDefs) {
289
- const rawPath = (outputDef.path || '').replace(/<project>/g, projectName)
290
- // role_level checks: 全局施加到每个 output(与 output 自身的 checks 合并)
291
- const mergedOutputDef = roleLevelChecks.length > 0
292
- ? { ...outputDef, checks: [...(outputDef.checks || []), ...roleLevelChecks] }
293
- : outputDef
294
- const checkResults = checkOutput(mergedOutputDef, projectName, cwd, effectiveBase)
295
- const outputPassed = checkResults.every(c => c.passed)
296
-
297
- outputs.push({
298
- path: rawPath,
299
- status: outputPassed ? 'pass' : 'fail',
300
- checks: checkResults.map(c => ({
301
- type: c.check,
302
- status: c.passed ? 'pass' : 'fail',
303
- detail: c.detail
304
- }))
305
- })
306
-
307
- for (const cr of checkResults) {
308
- if (!cr.passed) {
309
- failures.push({
310
- level: 'role',
311
- role_id: roleId,
312
- output: rawPath,
313
- check: cr.check,
314
- message: cr.detail
315
- })
316
- }
317
- }
318
- }
319
-
320
- const rolePassed = outputs.every(o => o.status === 'pass')
321
- roles.push({
322
- id: roleId,
323
- name: roleName,
324
- status: rolePassed ? 'pass' : 'fail',
325
- outputs
326
- })
327
- }
328
-
329
- // 2. 工作流级别检查
330
- for (const check of workflowChecks) {
331
- switch (check.type) {
332
- case 'file_count': {
333
- const scanDir = join(effectiveBase, 'docs', projectName, check.path || 'scan/')
334
- if (existsSync(scanDir)) {
335
- const files = readdirSync(scanDir).filter(f => f.endsWith('.md'))
336
- const min = check.min || 0
337
- if (files.length < min) {
338
- const detail = `文件数不足: ${scanDir} 有 ${files.length} 个 .md 文件,要求至少 ${min} 个`
339
- workflowCheckResults.push({ type: 'file_count', status: 'fail', detail })
340
- failures.push({ level: 'workflow', check: 'file_count', message: detail })
341
- } else {
342
- workflowCheckResults.push({ type: 'file_count', status: 'pass', detail: '' })
343
- }
344
- } else {
345
- const detail = `目录不存在: ${scanDir}`
346
- workflowCheckResults.push({ type: 'file_count', status: 'fail', detail })
347
- failures.push({ level: 'workflow', check: 'file_count', message: detail })
348
- }
349
- break
350
- }
351
- case 'no_empty_files': {
352
- const scanDir = join(effectiveBase, 'docs', projectName, check.path || 'scan/')
353
- if (existsSync(scanDir)) {
354
- const files = readdirSync(scanDir).filter(f => f.endsWith('.md'))
355
- let anyEmpty = false
356
- for (const f of files) {
357
- const content = readFileSync(join(scanDir, f), 'utf8')
358
- if (content.trim().length === 0) {
359
- const detail = `空文件: ${join(scanDir, f)}`
360
- workflowCheckResults.push({ type: 'no_empty_files', status: 'fail', detail })
361
- failures.push({ level: 'workflow', check: 'no_empty_files', message: detail })
362
- anyEmpty = true
363
- }
364
- }
365
- if (!anyEmpty) {
366
- workflowCheckResults.push({ type: 'no_empty_files', status: 'pass', detail: '' })
367
- }
368
- } else {
369
- const detail = `目录不存在: ${scanDir}`
370
- workflowCheckResults.push({ type: 'no_empty_files', status: 'fail', detail })
371
- failures.push({ level: 'workflow', check: 'no_empty_files', message: detail })
372
- }
373
- break
374
- }
375
- case 'file_exists': {
376
- // workflow_level file_exists: check.path 可以是目录或文件,相对 effectiveBase
377
- let checkPath = check.path || ''
378
- // 兼容 .sillyspec/ 前缀
379
- if (checkPath.startsWith('.sillyspec/')) checkPath = checkPath.slice('.sillyspec/'.length)
380
- // 替换 <change-name> 占位符(archive-impact 用)
381
- checkPath = checkPath.replace(/<change-name>/g, wf._changeName || '')
382
- const fullPath = join(effectiveBase, checkPath)
383
- if (!existsSync(fullPath)) {
384
- const detail = `文件不存在: ${checkPath}`
385
- workflowCheckResults.push({ type: 'file_exists', status: 'fail', detail })
386
- failures.push({ level: 'workflow', check: 'file_exists', message: detail })
387
- } else {
388
- workflowCheckResults.push({ type: 'file_exists', status: 'pass', detail: '' })
389
- }
390
- break
391
- }
392
- case 'min_lines': {
393
- let checkPath = check.path || ''
394
- if (checkPath.startsWith('.sillyspec/')) checkPath = checkPath.slice('.sillyspec/'.length)
395
- checkPath = checkPath.replace(/<change-name>/g, wf._changeName || '')
396
- const fullPath = join(effectiveBase, checkPath)
397
- if (existsSync(fullPath)) {
398
- const content = readFileSync(fullPath, 'utf8')
399
- const lines = content.split('\n').length
400
- const min = check.min || 1
401
- if (lines < min) {
402
- const detail = `文件只有 ${lines} 行,要求至少 ${min} 行: ${checkPath}`
403
- workflowCheckResults.push({ type: 'min_lines', status: 'fail', detail })
404
- failures.push({ level: 'workflow', check: 'min_lines', message: detail })
405
- } else {
406
- workflowCheckResults.push({ type: 'min_lines', status: 'pass', detail: '' })
407
- }
408
- } else {
409
- const detail = `文件不存在: ${checkPath}`
410
- workflowCheckResults.push({ type: 'min_lines', status: 'fail', detail })
411
- failures.push({ level: 'workflow', check: 'min_lines', message: detail })
412
- }
413
- break
414
- }
415
- case 'contains_sections': {
416
- let checkPath = check.path || ''
417
- if (checkPath.startsWith('.sillyspec/')) checkPath = checkPath.slice('.sillyspec/'.length)
418
- checkPath = checkPath.replace(/<change-name>/g, wf._changeName || '')
419
- const fullPath = join(effectiveBase, checkPath)
420
- if (existsSync(fullPath)) {
421
- const content = readFileSync(fullPath, 'utf8')
422
- const sections = check.sections || []
423
- const missing = sections.filter(s => !content.includes(`## ${s}`))
424
- if (missing.length > 0) {
425
- const detail = `缺少章节: ${missing.join(', ')} — ${checkPath}`
426
- workflowCheckResults.push({ type: 'contains_sections', status: 'fail', detail })
427
- failures.push({ level: 'workflow', check: 'contains_sections', message: detail })
428
- } else {
429
- workflowCheckResults.push({ type: 'contains_sections', status: 'pass', detail: '' })
430
- }
431
- } else {
432
- const detail = `文件不存在: ${checkPath}`
433
- workflowCheckResults.push({ type: 'contains_sections', status: 'fail', detail })
434
- failures.push({ level: 'workflow', check: 'contains_sections', message: detail })
435
- }
436
- break
437
- }
438
- case 'no_placeholder': {
439
- let checkPath = check.path || ''
440
- if (checkPath.startsWith('.sillyspec/')) checkPath = checkPath.slice('.sillyspec/'.length)
441
- checkPath = checkPath.replace(/<change-name>/g, wf._changeName || '')
442
- const fullPath = join(effectiveBase, checkPath)
443
- if (existsSync(fullPath)) {
444
- const content = readFileSync(fullPath, 'utf8')
445
- const patterns = check.patterns || ['待补充', 'TODO', 'TBD', '未分析', '根据项目情况', '根据实际情况', '按需填写']
446
- const lineMatches = patterns.filter(p => {
447
- const regex = new RegExp(`^\\s*[-*]?\\s*${p}\\s*$`, 'm')
448
- return regex.test(content)
449
- })
450
- if (lineMatches.length > 0) {
451
- const detail = `包含占位文本: ${lineMatches.map(m => `"${m}"`).join(', ')} — ${checkPath}`
452
- workflowCheckResults.push({ type: 'no_placeholder', status: 'fail', detail })
453
- failures.push({ level: 'workflow', check: 'no_placeholder', message: detail })
454
- } else {
455
- workflowCheckResults.push({ type: 'no_placeholder', status: 'pass', detail: '' })
456
- }
457
- } else {
458
- const detail = `文件不存在: ${checkPath}`
459
- workflowCheckResults.push({ type: 'no_placeholder', status: 'fail', detail })
460
- failures.push({ level: 'workflow', check: 'no_placeholder', message: detail })
461
- }
462
- break
463
- }
464
- default:
465
- workflowCheckResults.push({ type: check.type, status: 'pass', detail: `未知检查类型,跳过: ${check.type}` })
466
- }
467
- }
468
-
469
- const allPassed = roles.every(r => r.status === 'pass') && workflowCheckResults.every(c => c.status === 'pass')
470
-
471
- // 生成 retry prompts
472
- const retryPrompts = []
473
- if (!allPassed) {
474
- for (const role of roles.filter(r => r.status === 'fail')) {
475
- const roleFailures = failures.filter(f => f.role_id === role.id)
476
- const targetFiles = [...new Set(roleFailures.map(f => f.output).filter(Boolean))]
477
- const roleDef = (wf.roles || []).find(r => r.id === role.id)
478
- const constraints = roleDef?.constraints || []
479
- let prompt = `上一次 workflow 执行存在失败项,请重试。\n\n`
480
- prompt += `### 失败角色:${role.name} (${role.id})\n失败原因:\n`
481
- for (const f of roleFailures) {
482
- prompt += `- ${f.message}\n`
483
- }
484
- prompt += `\n`
485
- for (const fp of targetFiles) {
486
- prompt += `目标文件:\`${fp}\`\n`
487
- }
488
- if (constraints.length > 0) {
489
- prompt += `约束:\n`
490
- for (const c of constraints) {
491
- prompt += `- ${c}\n`
492
- }
493
- }
494
- prompt += `\n⚠️ 你必须确保文件写入指定路径。不要只报告完成,请用 write 工具实际写入。`
495
- retryPrompts.push({ role_id: role.id, role_name: role.name, prompt })
496
- }
497
- }
498
-
499
- return {
500
- workflow: workflowName,
501
- project: projectName,
502
- status: allPassed ? 'pass' : 'fail',
503
- spec_version: specVersion,
504
- roles,
505
- workflow_checks: workflowCheckResults,
506
- failures,
507
- retry_prompts: retryPrompts
508
- }
509
- }
510
-
511
- /**
512
- * 格式化检查结果为人类可读报告(兼容旧接口)
513
- */
514
- export function formatCheckReport(result) {
515
- const lines = []
516
- lines.push('\n📋 Workflow Post-Check 报告\n')
517
-
518
- for (const r of (result.roles || [])) {
519
- const icon = r.status === 'pass' ? '✅' : '❌'
520
- lines.push(`${icon} ${r.name} (${r.id})`)
521
- // 兼容新旧格式
522
- const outputFailures = (r.outputs || []).flatMap(o =>
523
- (o.checks || []).filter(c => c.status === 'fail').map(c => c.detail)
524
- )
525
- for (const f of outputFailures) {
526
- lines.push(` └─ ${f}`)
527
- }
528
- }
529
-
530
- if ((result.workflow_checks || []).some(c => c.status === 'fail')) {
531
- lines.push('')
532
- for (const c of result.workflow_checks) {
533
- if (c.status === 'fail') {
534
- lines.push(`❌ 全局检查失败: ${c.detail}`)
535
- }
536
- }
537
- }
538
-
539
- if (result.status === 'pass') {
540
- lines.push('\n✅ 全部检查通过')
541
- } else {
542
- lines.push('\n❌ 存在失败项,请根据以下重试提示修复:')
543
- }
544
-
545
- return lines.join('\n')
546
- }
547
-
548
- // ─── 兼容适配层 ───
549
-
550
- /**
551
- * 兼容旧接口:generateRetryPrompt
552
- * @deprecated 直接用 runPostCheck 返回的 retry_prompts
553
- */
554
- export function generateRetryPrompt(wf, checkResult, projectName) {
555
- const resolved = replaceProjectPlaceholder(wf, projectName)
556
- const lines = []
557
- lines.push('上一次 workflow 执行存在失败项,请重试。\n')
558
-
559
- const roles = resolved.roles || []
560
- const roleResults = checkResult.roles || []
561
- for (const r of roleResults) {
562
- if (r.status === 'pass') continue
563
- const role = roles.find(rl => rl.id === r.id)
564
- if (!role) continue
565
-
566
- lines.push(`### 失败角色:${r.name} (${r.id})`)
567
- lines.push(`失败原因:`)
568
- const roleFailures = (checkResult.failures || []).filter(f => f.role_id === r.id)
569
- for (const f of roleFailures) {
570
- lines.push(`- ${f.message}`)
571
- }
572
- lines.push('')
573
-
574
- for (const output of (role.outputs || [])) {
575
- lines.push(`目标文件:\`${output.path}\``)
576
- }
577
-
578
- if (role.constraints && role.constraints.length > 0) {
579
- lines.push('约束:')
580
- for (const c of role.constraints) {
581
- lines.push(`- ${c}`)
582
- }
583
- }
584
-
585
- lines.push('')
586
- lines.push('⚠️ 你必须确保文件写入指定路径。不要只报告完成,请用 write 工具实际写入。')
587
- lines.push('')
588
- }
589
-
590
- return lines.join('\n')
591
- }
592
-
593
- // ─── Role Prompt 生成(Level 2)───
594
-
595
- /**
596
- * 根据 workflow role 定义生成子代理 prompt
597
- * @param {object} wf - workflow 定义
598
- * @param {string} roleId - 角色ID
599
- * @param {string} projectName - 项目名
600
- * @param {object} context - 额外上下文(envSummary, missingDocs 等)
601
- * @returns {string|null} 生成的 prompt,或 null(角色不存在)
602
- */
603
- export function generateRolePrompt(wf, roleId, projectName, context = {}) {
604
- const resolved = replaceProjectPlaceholder(wf, projectName)
605
- const role = (resolved.roles || []).find(r => r.id === roleId)
606
- if (!role) return null
607
-
608
- const lines = []
609
- lines.push(`## 子代理任务:${role.name} (${roleId})`)
610
- lines.push('')
611
- lines.push(`项目:${projectName}`)
612
-
613
- // 任务描述
614
- if (role.task) {
615
- lines.push(`任务:${role.task}`)
616
- }
617
-
618
- // 依赖角色的输出(depends_on + from_role)
619
- const deps = role.depends_on || []
620
- if (deps.length > 0) {
621
- lines.push('')
622
- lines.push('前置依赖(已完成角色的输出):')
623
- const inputs = role.inputs || {}
624
- for (const depId of deps) {
625
- const depRole = (resolved.roles || []).find(r => r.id === depId)
626
- if (!depRole) continue
627
- if (inputs.from_role === depId) {
628
- lines.push(`- ${depRole.name}(${depId}):${inputs.output_description || ''}`)
629
- if (inputs.output) {
630
- const depOutput = (depRole.outputs || []).find(o => {
631
- const outputName = o.name || o.path?.split('/').pop()?.replace(/\.md$/, '') || ''
632
- return outputName === inputs.output
633
- })
634
- if (depOutput) {
635
- lines.push(` 输出文件:${depOutput.path}`)
636
- }
637
- }
638
- } else {
639
- lines.push(`- ${depRole.name}(${depId})`)
640
- for (const o of (depRole.outputs || [])) {
641
- lines.push(` 输出:${o.path}`)
642
- }
643
- }
644
- }
645
- }
646
-
647
- // 输入提示
648
- const inputs = role.inputs || {}
649
- const inputPaths = inputs.paths || []
650
- const inputHints = inputs.hints || {}
651
- if (inputPaths.length > 0) {
652
- lines.push('')
653
- lines.push('搜索范围:')
654
- for (const p of inputPaths) {
655
- lines.push(`- ${p}`)
656
- }
657
- }
658
- if (inputHints.grep_patterns && inputHints.grep_patterns.length > 0) {
659
- lines.push('')
660
- lines.push('搜索关键词:')
661
- lines.push(`- ${inputHints.grep_patterns.join(', ')}`)
662
- }
663
-
664
- // 额外上下文
665
- if (context.envSummary) {
666
- lines.push('')
667
- lines.push('环境探测结果:')
668
- lines.push(context.envSummary)
669
- }
670
- if (context.missingDocs) {
671
- lines.push('')
672
- lines.push('缺失文档列表:')
673
- lines.push(context.missingDocs)
674
- }
675
-
676
- // 输出目标
677
- const outputs = role.outputs || []
678
- lines.push('')
679
- lines.push('目标文件:')
680
- for (const o of outputs) {
681
- lines.push(`- \`${o.path}\``)
682
- }
683
-
684
- // 约束
685
- if (role.constraints && role.constraints.length > 0) {
686
- lines.push('')
687
- lines.push('约束:')
688
- for (const c of role.constraints) {
689
- lines.push(`- ${c}`)
690
- }
691
- }
692
-
693
- // 检查要求(告诉子代理需要满足什么)
694
- for (const o of outputs) {
695
- const checks = o.checks || []
696
- for (const check of checks) {
697
- if (check.type === 'contains_sections' && check.sections) {
698
- lines.push('')
699
- lines.push(`必须包含章节:${check.sections.map(s => `"## ${s}"`).join(', ')}`)
700
- }
701
- if (check.type === 'min_lines') {
702
- lines.push(`文件长度要求:至少 ${check.min} 行`)
703
- }
704
- }
705
- }
706
-
707
- lines.push('')
708
- lines.push('⚠️ 必须用 write 工具将文件写入磁盘!写完后用 read 工具确认文件存在!')
709
-
710
- return lines.join('\n')
711
- }
712
-
713
- /**
714
- * 为 workflow 的所有角色生成 role prompts
715
- * @param {object} wf - workflow 定义
716
- * @param {string} projectName - 项目名
717
- * @param {object} context - 额外上下文
718
- * @returns {Array<{ roleId: string, roleName: string, prompt: string }>}
719
- */
720
- export function generateAllRolePrompts(wf, projectName, context = {}) {
721
- const resolved = replaceProjectPlaceholder(wf, projectName)
722
- const roles = resolved.roles || []
723
- return roles.map(role => ({
724
- roleId: role.id,
725
- roleName: role.name || role.id,
726
- prompt: generateRolePrompt(resolved, role.id, projectName, context)
727
- }))
728
- }
729
-
730
- // ─── Workflow Run 归档 ───
731
-
732
-
733
- /**
734
- * 将 workflow check 结果归档到 .sillyspec/.runtime/workflow-runs/
735
- * @param {object} result - runPostCheck 返回的结构化结果
736
- * @param {object} options
737
- * @param {string} options.cwd - 项目根目录
738
- * @param {string} [options.source] - 调用来源('run.js' / 'cli')
739
- * @param {string} [options.stage] - 阶段名(scan/archive)
740
- * @param {string} [options.step] - 步骤名
741
- * @returns {string|null} 保存路径,失败返回 null
742
- */
743
- export function saveWorkflowRun(result, options = {}) {
744
- const { cwd = '.', source = 'unknown', stage, step, runtimeRoot, scanRunId } = options
745
- // 平台模式:写入 runtime-root/scan-runs/<scan-run-id>/workflow-runs/
746
- // 本地模式:写入 cwd/.sillyspec/.runtime/workflow-runs/
747
- const runDir = runtimeRoot
748
- ? join(runtimeRoot, 'scan-runs', scanRunId || 'unknown', 'workflow-runs')
749
- : join(cwd, '.sillyspec', '.runtime', 'workflow-runs')
750
- try {
751
- mkdirSync(runDir, { recursive: true })
752
- } catch (e) {
753
- console.warn('⚠️ 无法创建 workflow-runs 目录:', e.message)
754
- return null
755
- }
756
-
757
- const now = new Date()
758
- const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14)
759
- const filename = `${ts}-${result.workflow || 'unknown'}-${result.project || 'default'}-${result.status}.json`
760
- const filepath = join(runDir, filename)
761
-
762
- const record = {
763
- run_id: filename.replace('.json', ''),
764
- created_at: now.toISOString(),
765
- source,
766
- ...(stage ? { stage } : {}),
767
- ...(step ? { step } : {}),
768
- workflow: result.workflow,
769
- project: result.project,
770
- status: result.status,
771
- spec_version: result.spec_version,
772
- roles: result.roles,
773
- workflow_checks: result.workflow_checks,
774
- failures: result.failures,
775
- retry_prompts: result.retry_prompts
776
- }
777
-
778
- try {
779
- writeFileSync(filepath, JSON.stringify(record, null, 2), 'utf8')
780
- return filepath
781
- } catch (e) {
782
- console.warn('⚠️ 保存 workflow run 失败:', e.message)
783
- return null
784
- }
785
- }