sillyspec 3.27.2 → 3.27.5

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 (221) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +8 -0
  2. package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
  3. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  4. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
  5. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  6. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  7. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
  8. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  9. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
  10. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
  11. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
  12. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
  13. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  14. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  15. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  16. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
  17. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  18. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
  19. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
  20. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  21. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
  22. package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
  23. package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
  24. package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
  25. package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
  26. package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
  27. package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
  28. package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
  29. package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
  30. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
  31. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  32. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
  33. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
  34. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
  35. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
  36. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
  37. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
  38. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
  39. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
  40. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
  41. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
  42. package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
  43. package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
  44. package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
  45. package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
  46. package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
  47. package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
  48. package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
  49. package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
  50. package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
  51. package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
  52. package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
  53. package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
  54. package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
  55. package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
  56. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
  57. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
  58. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
  59. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
  60. package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
  61. package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
  62. package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
  63. package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
  64. package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
  65. package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
  66. package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
  67. package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
  68. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
  69. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
  70. package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
  71. package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
  72. package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
  73. package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
  74. package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
  75. package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
  76. package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
  77. package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
  78. package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
  79. package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
  80. package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
  81. package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
  82. package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
  83. package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
  84. package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
  85. package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
  86. package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
  87. package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
  88. package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
  89. package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
  90. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
  91. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
  92. package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
  93. package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
  94. package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
  95. package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
  96. package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
  97. package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
  98. package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
  99. package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
  100. package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
  101. package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
  102. package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
  103. package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
  104. package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
  105. package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
  106. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
  107. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
  108. package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
  109. package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
  110. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
  111. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
  112. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
  113. package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
  114. package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
  115. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
  116. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
  117. package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
  118. package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
  119. package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
  120. package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
  121. package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
  122. package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
  123. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
  124. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
  125. package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
  126. package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
  127. package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
  128. package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
  129. package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
  130. package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
  131. package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
  132. package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
  133. package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
  134. package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
  135. package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
  136. package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
  137. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
  138. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
  139. package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
  140. package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
  141. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
  142. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
  143. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
  144. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
  145. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
  146. package/eval/README.md +86 -0
  147. package/eval/eval.config.json +28 -0
  148. package/eval/lib/adapters/cli.mjs +63 -0
  149. package/eval/lib/adapters/dryrun.mjs +27 -0
  150. package/eval/lib/config.mjs +82 -0
  151. package/eval/lib/db.mjs +76 -0
  152. package/eval/lib/report.mjs +120 -0
  153. package/eval/lib/runner.mjs +157 -0
  154. package/eval/lib/tasks.mjs +73 -0
  155. package/eval/lib/util.mjs +107 -0
  156. package/eval/lib/workdir.mjs +30 -0
  157. package/eval/run.mjs +83 -0
  158. package/eval/tasks/001-slugify/files/package.json +6 -0
  159. package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  160. package/eval/tasks/001-slugify/task.json +6 -0
  161. package/eval/tasks/001-slugify/verify.mjs +28 -0
  162. package/eval/tasks/002-each-day/files/package.json +6 -0
  163. package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  164. package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  165. package/eval/tasks/002-each-day/task.json +6 -0
  166. package/eval/tasks/002-each-day/verify.mjs +31 -0
  167. package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  168. package/eval/tasks/003-word-report/files/package.json +6 -0
  169. package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  170. package/eval/tasks/003-word-report/task.json +6 -0
  171. package/eval/tasks/003-word-report/verify.mjs +38 -0
  172. package/package.json +4 -2
  173. package/src/agent-session-log.js +45 -5
  174. package/src/change-risk-profile.js +83 -14
  175. package/src/config-schema.js +18 -3
  176. package/src/contract-matrix.js +90 -29
  177. package/src/datetime.js +20 -0
  178. package/src/decision-distill.js +517 -0
  179. package/src/dispatch/backends/local-agent.js +4 -4
  180. package/src/docs-check.js +227 -0
  181. package/src/docs-debt.js +187 -24
  182. package/src/doctor-diagnostics.js +102 -2
  183. package/src/foreign-declared.js +78 -0
  184. package/src/index.js +89 -9
  185. package/src/init.js +9 -7
  186. package/src/knowledge-match.js +107 -6
  187. package/src/local-register.js +7 -1
  188. package/src/module-resolve.js +12 -1
  189. package/src/modules.js +2 -1
  190. package/src/plan-adopt-waves.js +126 -0
  191. package/src/quicklog.js +50 -4
  192. package/src/run/command.js +122 -43
  193. package/src/run/complete-handlers.js +48 -19
  194. package/src/run/complete.js +5 -2
  195. package/src/run/gates.js +87 -22
  196. package/src/run/prompt.js +84 -42
  197. package/src/run/quick-audit.js +11 -0
  198. package/src/run/shared.js +152 -10
  199. package/src/run.js +2 -2
  200. package/src/scan-postcheck.js +30 -17
  201. package/src/stage-contract-spec.js +2 -1
  202. package/src/stage-contract.js +68 -4
  203. package/src/stages/archive.js +53 -12
  204. package/src/stages/brainstorm-auto.js +7 -0
  205. package/src/stages/brainstorm.js +11 -0
  206. package/src/stages/doctor.js +157 -1
  207. package/src/stages/execute.js +32 -6
  208. package/src/stages/plan-postcheck.js +76 -17
  209. package/src/stages/plan.js +1 -0
  210. package/src/stages/quick.js +6 -1
  211. package/src/stages/verify.js +31 -14
  212. package/src/sync.js +41 -3
  213. package/src/task-review.js +2 -2
  214. package/src/taskcard-placeholders.js +21 -0
  215. package/src/taskcard.js +10 -1
  216. package/src/verify-postcheck.js +315 -13
  217. package/src/verify-probes.js +46 -4
  218. package/src/worktree-apply.js +184 -7
  219. package/src/worktree-deps.js +57 -0
  220. package/src/worktree.js +201 -58
  221. package/templates/prompts/taskcard-rules.md +1 -0
@@ -0,0 +1,600 @@
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
+ import { contentNonEmpty, lineCount, missingSections, placeholderLineMatches } from './check-primitives.js'
17
+
18
+ // ─── Workflow 加载 ───
19
+
20
+ /**
21
+ * 查找并加载指定名称的 workflow YAML
22
+ * @param {string} cwd - 项目根目录
23
+ * @param {string} name - workflow 名称(如 'scan-docs')
24
+ * @returns {object|null} workflow 定义,或 null
25
+ */
26
+ export function loadWorkflow(cwd, name, validate = true) {
27
+ const wfDir = join(cwd, '.sillyspec', 'workflows')
28
+ if (!existsSync(wfDir)) return null
29
+
30
+ // 优先找 <name>.yaml,其次找 <name>.yml
31
+ for (const ext of ['.yaml', '.yml']) {
32
+ const f = join(wfDir, `${name}${ext}`)
33
+ if (existsSync(f)) {
34
+ const raw = readFileSync(f, 'utf8')
35
+ const wf = jsYaml.load(raw)
36
+ if (validate) {
37
+ const errors = validateWorkflow(wf)
38
+ if (errors.length > 0) return { _validationErrors: errors, ...wf }
39
+ }
40
+ return wf
41
+ }
42
+ }
43
+ return null
44
+ }
45
+
46
+ /**
47
+ * 校验 workflow YAML 结构
48
+ * @param {object} wf - workflow 定义
49
+ * @returns {string[]} 错误列表,空数组表示通过
50
+ */
51
+ function validateWorkflow(wf) {
52
+ const errors = []
53
+ const roles = wf.roles || []
54
+ const roleIds = new Set(roles.map(r => r.id))
55
+
56
+ for (const role of roles) {
57
+ if (!role.id) {
58
+ errors.push(`role 缺少 id 字段`)
59
+ continue
60
+ }
61
+ // depends_on 校验
62
+ const deps = role.depends_on || []
63
+ for (const depId of deps) {
64
+ if (!roleIds.has(depId)) {
65
+ errors.push(`role "${role.id}" 的 depends_on 引用了不存在的 role "${depId}"`)
66
+ }
67
+ }
68
+ // depends_on 循环检测(简单两层:A→B→A)
69
+ for (const depId of deps) {
70
+ const depRole = roles.find(r => r.id === depId)
71
+ if (depRole && (depRole.depends_on || []).includes(role.id)) {
72
+ errors.push(`循环依赖:"${role.id}" ↔ "${depId}"`)
73
+ }
74
+ }
75
+ // from_role 校验
76
+ const inputs = role.inputs || {}
77
+ if (!Array.isArray(inputs)) {
78
+ // inputs is a mapping
79
+ if (inputs.from_role) {
80
+ if (!roleIds.has(inputs.from_role)) {
81
+ errors.push(`role "${role.id}" 的 inputs.from_role 引用了不存在的 role "${inputs.from_role}"`)
82
+ }
83
+ if (inputs.output) {
84
+ const sourceRole = roles.find(r => r.id === inputs.from_role)
85
+ if (sourceRole) {
86
+ const outputExists = (sourceRole.outputs || []).some(o => {
87
+ const outputName = o.name || o.path?.split('/').pop()?.replace(/\.md$/, '') || ''
88
+ return outputName === inputs.output
89
+ })
90
+ if (!outputExists) {
91
+ errors.push(`role "${role.id}" 的 inputs.from_role "${inputs.from_role}" 没有名为 "${inputs.output}" 的 output`)
92
+ }
93
+ }
94
+ }
95
+ if (!deps.includes(inputs.from_role)) {
96
+ errors.push(`role "${role.id}" 的 inputs.from_role "${inputs.from_role}" 未在 depends_on 中声明`)
97
+ }
98
+ }
99
+ } else {
100
+ // inputs is an array (legacy format)
101
+ for (const input of inputs) {
102
+ if (input.from_role) {
103
+ if (!roleIds.has(input.from_role)) {
104
+ errors.push(`role "${role.id}" 的 inputs.from_role 引用了不存在的 role "${input.from_role}"`)
105
+ }
106
+ if (!deps.includes(input.from_role)) {
107
+ errors.push(`role "${role.id}" 的 inputs.from_role "${input.from_role}" 未在 depends_on 中声明`)
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ return errors
114
+ }
115
+
116
+ /**
117
+ * 列出所有可用 workflow
118
+ */
119
+ export function listWorkflows(cwd) {
120
+ const wfDir = join(cwd, '.sillyspec', 'workflows')
121
+ if (!existsSync(wfDir)) return []
122
+ const files = readdirSync(wfDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
123
+ return files.map(f => f.replace(/\.(yaml|yml)$/, ''))
124
+ }
125
+
126
+ // ─── 占位符替换 ───
127
+
128
+ /**
129
+ * 替换 workflow YAML 中的 <project> 占位符
130
+ * @param {object} wf - workflow 定义(会被修改)
131
+ * @param {string} projectName - 项目名
132
+ */
133
+ function replaceProjectPlaceholder(wf, projectName) {
134
+ const json = JSON.stringify(wf)
135
+ const replaced = json.replace(/<project>/g, projectName)
136
+ return JSON.parse(replaced)
137
+ }
138
+
139
+ // ─── Post Check ───
140
+
141
+ /**
142
+ * 检查结果项
143
+ * @typedef {{ role: string, output: string, path: string, check: string, passed: boolean, detail?: string }} CheckResult
144
+ */
145
+
146
+ /**
147
+ * 对单个 output 运行检查
148
+ * @param {object} outputDef - output 定义
149
+ * @param {string} basePath - 被检查的文件所在目录
150
+ * @param {string} cwd - 项目根目录
151
+ * @returns {CheckResult}
152
+ */
153
+ function checkOutput(outputDef, projectName, cwd, specBase) {
154
+ // specBase 优先(平台模式 platformOpts.specRoot,已含或不含 .sillyspec 语义);
155
+ // 未传时回退 join(cwd, '.sillyspec'),等价于旧行为 resolve(cwd, '.sillyspec/...')
156
+ const effectiveBase = specBase || join(cwd, '.sillyspec')
157
+ // 将 <project> 替换为实际项目名
158
+ let rawPath = (outputDef.path || '').replace(/<project>/g, projectName)
159
+ // 旧版兼容:如果 path 以 .sillyspec/ 开头(相对路径),strip 前缀避免双拼接
160
+ // 新版 yaml 用 {SPEC_ROOT} 已在 runPostCheck 中替换为绝对路径,不会走这里
161
+ if (rawPath.startsWith('.sillyspec/')) {
162
+ rawPath = rawPath.slice('.sillyspec/'.length)
163
+ }
164
+ const fullPath = resolve(effectiveBase, rawPath)
165
+ const checks = outputDef.checks || []
166
+ const results = []
167
+
168
+ for (const check of checks) {
169
+ switch (check.type) {
170
+ case 'file_exists': {
171
+ const exists = existsSync(fullPath)
172
+ results.push({ passed: exists, check: 'file_exists', detail: exists ? '' : `文件不存在: ${rawPath}` })
173
+ break
174
+ }
175
+ case 'no_empty_files': {
176
+ if (existsSync(fullPath)) {
177
+ const content = readFileSync(fullPath, 'utf8')
178
+ const empty = !contentNonEmpty(content)
179
+ results.push({ passed: !empty, check: 'no_empty_files', detail: empty ? `文件为空: ${rawPath}` : '' })
180
+ } else {
181
+ results.push({ passed: false, check: 'no_empty_files', detail: `文件不存在: ${rawPath}` })
182
+ }
183
+ break
184
+ }
185
+ case 'min_lines': {
186
+ if (existsSync(fullPath)) {
187
+ const content = readFileSync(fullPath, 'utf8')
188
+ const lines = lineCount(content)
189
+ const min = check.min || 1
190
+ results.push({ passed: lines >= min, check: `min_lines(${min})`, detail: lines >= min ? '' : `文件只有 ${lines} 行,要求至少 ${min} 行: ${rawPath}` })
191
+ } else {
192
+ results.push({ passed: false, check: `min_lines(${check.min || 1})`, detail: `文件不存在: ${rawPath}` })
193
+ }
194
+ break
195
+ }
196
+ case 'contains_sections': {
197
+ if (existsSync(fullPath)) {
198
+ const content = readFileSync(fullPath, 'utf8')
199
+ const sections = check.sections || []
200
+ const missing = missingSections(content, sections)
201
+ results.push({ passed: missing.length === 0, check: 'contains_sections', detail: missing.length > 0 ? `缺少章节: ${missing.join(', ')} — ${rawPath}` : '' })
202
+ } else {
203
+ results.push({ passed: false, check: 'contains_sections', detail: `文件不存在: ${rawPath}` })
204
+ }
205
+ break
206
+ }
207
+ case 'no_placeholder': {
208
+ if (existsSync(fullPath)) {
209
+ const content = readFileSync(fullPath, 'utf8')
210
+ const lineMatches = placeholderLineMatches(content, check.patterns)
211
+ results.push({ passed: lineMatches.length === 0, check: 'no_placeholder', detail: lineMatches.length > 0 ? `包含占位文本: ${lineMatches.map(m => `"${m}"`).join(', ')} — ${rawPath}` : '' })
212
+ } else {
213
+ results.push({ passed: false, check: 'no_placeholder', detail: `文件不存在: ${rawPath}` })
214
+ }
215
+ break
216
+ }
217
+ default:
218
+ results.push({ passed: true, check: check.type, detail: `未知检查类型,跳过: ${check.type}` })
219
+ }
220
+ }
221
+
222
+ return results
223
+ }
224
+
225
+ /**
226
+ * 运行 workflow 的 post_check
227
+ * @param {object} wf - workflow 定义
228
+ * @param {string} cwd - 项目根目录
229
+ * @param {string} projectName - 项目名
230
+ * @returns {{ passed: boolean, roleResults: Array<{ roleId: string, roleName: string, passed: boolean, failures: string[] }>, workflowFailures: string[] }}
231
+ */
232
+ /**
233
+ * 统一的 Workflow Check 结果协议
234
+ * CLI 和 run.js 共用同一份结构化结果
235
+ *
236
+ * 返回结构:
237
+ * {
238
+ * workflow: string, // workflow 名称
239
+ * project: string, // 项目名
240
+ * status: 'pass'|'fail', // 总体状态
241
+ * spec_version: number, // spec 版本
242
+ * roles: [{ id, name, status, outputs: [{ path, status, checks: [{ type, status, detail }] }] }],
243
+ * workflow_checks: [{ type, status, detail }],
244
+ * failures: [{ level: 'role'|'workflow', role_id?, output?, check, message }],
245
+ * retry_prompts: [{ role_id, role_name, prompt }]
246
+ * }
247
+ */
248
+ export function runPostCheck(wf, cwd, projectName, placeholders = {}, specBase) {
249
+ let resolved = replaceProjectPlaceholder(wf, projectName)
250
+
251
+ // 自动注入 {SPEC_ROOT} 占位符(yaml 模板用 {SPEC_ROOT} 表示规范根目录)
252
+ // effectiveBase 与 _checkWorkflow 内部一致:specBase || join(cwd, '.sillyspec')
253
+ const effectiveBase = specBase || join(cwd, '.sillyspec')
254
+ const allPlaceholders = { SPEC_ROOT: effectiveBase, ...placeholders }
255
+
256
+ let json = JSON.stringify(resolved)
257
+ for (const [key, value] of Object.entries(allPlaceholders)) {
258
+ // 支持 {key} 和 <key> 两种占位符语法
259
+ // 字符串值先做 JSON 转义再嵌入:Windows 路径等含反斜杠/引号的值裸替换进 JSON 字面量会
260
+ // 破坏 parse(实测报「Bad escaped character」→ scan 深度扫描质量门静默 fail-open)。
261
+ // JSON.stringify(v).slice(1,-1) 产出合法的 JSON 字符串内容(反斜杠→\\、引号→\");
262
+ // 非字符串值保持 String(v) 原样(数字占位符不被引号包裹,语义不变)。
263
+ const embedValue = (v) => (typeof v === 'string' ? JSON.stringify(v).slice(1, -1) : String(v))
264
+ json = json.replace(new RegExp(`\{${key}\}`, 'g'), embedValue(value))
265
+ json = json.replace(new RegExp(`<${key}>`, 'g'), embedValue(value))
266
+ }
267
+ resolved = JSON.parse(json)
268
+
269
+ return _checkWorkflow(resolved, cwd, projectName, specBase)
270
+ }
271
+
272
+ function _checkWorkflow(wf, cwd, projectName, specBase) {
273
+ const effectiveBase = specBase || join(cwd, '.sillyspec')
274
+ const workflowName = wf.name || 'unknown'
275
+ const specVersion = wf.spec_version || wf.version || 0
276
+ const workflowChecks = wf.checks?.workflow_level || []
277
+ const roleLevelChecks = wf.checks?.role_level || []
278
+ const roles = []
279
+ const failures = []
280
+ const workflowCheckResults = []
281
+
282
+ // 1. 角色级别检查
283
+ for (const role of wf.roles || []) {
284
+ const roleId = role.id
285
+ const roleName = role.name || roleId
286
+ const outputDefs = role.outputs || []
287
+ const outputs = []
288
+
289
+ for (const outputDef of outputDefs) {
290
+ const rawPath = (outputDef.path || '').replace(/<project>/g, projectName)
291
+ // role_level checks: 全局施加到每个 output(与 output 自身的 checks 合并)
292
+ const mergedOutputDef = roleLevelChecks.length > 0
293
+ ? { ...outputDef, checks: [...(outputDef.checks || []), ...roleLevelChecks] }
294
+ : outputDef
295
+ const checkResults = checkOutput(mergedOutputDef, projectName, cwd, effectiveBase)
296
+ const outputPassed = checkResults.every(c => c.passed)
297
+
298
+ outputs.push({
299
+ path: rawPath,
300
+ status: outputPassed ? 'pass' : 'fail',
301
+ checks: checkResults.map(c => ({
302
+ type: c.check,
303
+ status: c.passed ? 'pass' : 'fail',
304
+ detail: c.detail
305
+ }))
306
+ })
307
+
308
+ for (const cr of checkResults) {
309
+ if (!cr.passed) {
310
+ failures.push({
311
+ level: 'role',
312
+ role_id: roleId,
313
+ output: rawPath,
314
+ check: cr.check,
315
+ message: cr.detail
316
+ })
317
+ }
318
+ }
319
+ }
320
+
321
+ const rolePassed = outputs.every(o => o.status === 'pass')
322
+ roles.push({
323
+ id: roleId,
324
+ name: roleName,
325
+ status: rolePassed ? 'pass' : 'fail',
326
+ outputs
327
+ })
328
+ }
329
+
330
+ // 2. 工作流级别检查
331
+ for (const check of workflowChecks) {
332
+ switch (check.type) {
333
+ case 'file_count': {
334
+ const scanDir = join(effectiveBase, 'docs', projectName, check.path || 'scan/')
335
+ if (existsSync(scanDir)) {
336
+ const files = readdirSync(scanDir).filter(f => f.endsWith('.md'))
337
+ const min = check.min || 0
338
+ if (files.length < min) {
339
+ const detail = `文件数不足: ${scanDir} 有 ${files.length} 个 .md 文件,要求至少 ${min} 个`
340
+ workflowCheckResults.push({ type: 'file_count', status: 'fail', detail })
341
+ failures.push({ level: 'workflow', check: 'file_count', message: detail })
342
+ } else {
343
+ workflowCheckResults.push({ type: 'file_count', status: 'pass', detail: '' })
344
+ }
345
+ } else {
346
+ const detail = `目录不存在: ${scanDir}`
347
+ workflowCheckResults.push({ type: 'file_count', status: 'fail', detail })
348
+ failures.push({ level: 'workflow', check: 'file_count', message: detail })
349
+ }
350
+ break
351
+ }
352
+ case 'no_empty_files': {
353
+ const scanDir = join(effectiveBase, 'docs', projectName, check.path || 'scan/')
354
+ if (existsSync(scanDir)) {
355
+ const files = readdirSync(scanDir).filter(f => f.endsWith('.md'))
356
+ let anyEmpty = false
357
+ for (const f of files) {
358
+ const content = readFileSync(join(scanDir, f), 'utf8')
359
+ if (!contentNonEmpty(content)) {
360
+ const detail = `空文件: ${join(scanDir, f)}`
361
+ workflowCheckResults.push({ type: 'no_empty_files', status: 'fail', detail })
362
+ failures.push({ level: 'workflow', check: 'no_empty_files', message: detail })
363
+ anyEmpty = true
364
+ }
365
+ }
366
+ if (!anyEmpty) {
367
+ workflowCheckResults.push({ type: 'no_empty_files', status: 'pass', detail: '' })
368
+ }
369
+ } else {
370
+ const detail = `目录不存在: ${scanDir}`
371
+ workflowCheckResults.push({ type: 'no_empty_files', status: 'fail', detail })
372
+ failures.push({ level: 'workflow', check: 'no_empty_files', message: detail })
373
+ }
374
+ break
375
+ }
376
+ case 'file_exists': {
377
+ // workflow_level file_exists: check.path 可以是目录或文件,相对 effectiveBase
378
+ let checkPath = check.path || ''
379
+ // 兼容 .sillyspec/ 前缀
380
+ if (checkPath.startsWith('.sillyspec/')) checkPath = checkPath.slice('.sillyspec/'.length)
381
+ // 替换 <change-name> 占位符(archive-impact 用)
382
+ checkPath = checkPath.replace(/<change-name>/g, wf._changeName || '')
383
+ const fullPath = join(effectiveBase, checkPath)
384
+ if (!existsSync(fullPath)) {
385
+ const detail = `文件不存在: ${checkPath}`
386
+ workflowCheckResults.push({ type: 'file_exists', status: 'fail', detail })
387
+ failures.push({ level: 'workflow', check: 'file_exists', message: detail })
388
+ } else {
389
+ workflowCheckResults.push({ type: 'file_exists', status: 'pass', detail: '' })
390
+ }
391
+ break
392
+ }
393
+ case 'min_lines': {
394
+ let checkPath = check.path || ''
395
+ if (checkPath.startsWith('.sillyspec/')) checkPath = checkPath.slice('.sillyspec/'.length)
396
+ checkPath = checkPath.replace(/<change-name>/g, wf._changeName || '')
397
+ const fullPath = join(effectiveBase, checkPath)
398
+ if (existsSync(fullPath)) {
399
+ const content = readFileSync(fullPath, 'utf8')
400
+ const lines = lineCount(content)
401
+ const min = check.min || 1
402
+ if (lines < min) {
403
+ const detail = `文件只有 ${lines} 行,要求至少 ${min} 行: ${checkPath}`
404
+ workflowCheckResults.push({ type: 'min_lines', status: 'fail', detail })
405
+ failures.push({ level: 'workflow', check: 'min_lines', message: detail })
406
+ } else {
407
+ workflowCheckResults.push({ type: 'min_lines', status: 'pass', detail: '' })
408
+ }
409
+ } else {
410
+ const detail = `文件不存在: ${checkPath}`
411
+ workflowCheckResults.push({ type: 'min_lines', status: 'fail', detail })
412
+ failures.push({ level: 'workflow', check: 'min_lines', message: detail })
413
+ }
414
+ break
415
+ }
416
+ case 'contains_sections': {
417
+ let checkPath = check.path || ''
418
+ if (checkPath.startsWith('.sillyspec/')) checkPath = checkPath.slice('.sillyspec/'.length)
419
+ checkPath = checkPath.replace(/<change-name>/g, wf._changeName || '')
420
+ const fullPath = join(effectiveBase, checkPath)
421
+ if (existsSync(fullPath)) {
422
+ const content = readFileSync(fullPath, 'utf8')
423
+ const sections = check.sections || []
424
+ const missing = missingSections(content, sections)
425
+ if (missing.length > 0) {
426
+ const detail = `缺少章节: ${missing.join(', ')} — ${checkPath}`
427
+ workflowCheckResults.push({ type: 'contains_sections', status: 'fail', detail })
428
+ failures.push({ level: 'workflow', check: 'contains_sections', message: detail })
429
+ } else {
430
+ workflowCheckResults.push({ type: 'contains_sections', status: 'pass', detail: '' })
431
+ }
432
+ } else {
433
+ const detail = `文件不存在: ${checkPath}`
434
+ workflowCheckResults.push({ type: 'contains_sections', status: 'fail', detail })
435
+ failures.push({ level: 'workflow', check: 'contains_sections', message: detail })
436
+ }
437
+ break
438
+ }
439
+ case 'no_placeholder': {
440
+ let checkPath = check.path || ''
441
+ if (checkPath.startsWith('.sillyspec/')) checkPath = checkPath.slice('.sillyspec/'.length)
442
+ checkPath = checkPath.replace(/<change-name>/g, wf._changeName || '')
443
+ const fullPath = join(effectiveBase, checkPath)
444
+ if (existsSync(fullPath)) {
445
+ const content = readFileSync(fullPath, 'utf8')
446
+ const lineMatches = placeholderLineMatches(content, check.patterns)
447
+ if (lineMatches.length > 0) {
448
+ const detail = `包含占位文本: ${lineMatches.map(m => `"${m}"`).join(', ')} — ${checkPath}`
449
+ workflowCheckResults.push({ type: 'no_placeholder', status: 'fail', detail })
450
+ failures.push({ level: 'workflow', check: 'no_placeholder', message: detail })
451
+ } else {
452
+ workflowCheckResults.push({ type: 'no_placeholder', status: 'pass', detail: '' })
453
+ }
454
+ } else {
455
+ const detail = `文件不存在: ${checkPath}`
456
+ workflowCheckResults.push({ type: 'no_placeholder', status: 'fail', detail })
457
+ failures.push({ level: 'workflow', check: 'no_placeholder', message: detail })
458
+ }
459
+ break
460
+ }
461
+ default:
462
+ workflowCheckResults.push({ type: check.type, status: 'pass', detail: `未知检查类型,跳过: ${check.type}` })
463
+ }
464
+ }
465
+
466
+ const allPassed = roles.every(r => r.status === 'pass') && workflowCheckResults.every(c => c.status === 'pass')
467
+
468
+ // 生成 retry prompts
469
+ const retryPrompts = []
470
+ if (!allPassed) {
471
+ for (const role of roles.filter(r => r.status === 'fail')) {
472
+ const roleFailures = failures.filter(f => f.role_id === role.id)
473
+ const targetFiles = [...new Set(roleFailures.map(f => f.output).filter(Boolean))]
474
+ const roleDef = (wf.roles || []).find(r => r.id === role.id)
475
+ const constraints = roleDef?.constraints || []
476
+ let prompt = `上一次 workflow 执行存在失败项,请重试。\n\n`
477
+ prompt += `### 失败角色:${role.name} (${role.id})\n失败原因:\n`
478
+ for (const f of roleFailures) {
479
+ prompt += `- ${f.message}\n`
480
+ }
481
+ prompt += `\n`
482
+ for (const fp of targetFiles) {
483
+ prompt += `目标文件:\`${fp}\`\n`
484
+ }
485
+ if (constraints.length > 0) {
486
+ prompt += `约束:\n`
487
+ for (const c of constraints) {
488
+ prompt += `- ${c}\n`
489
+ }
490
+ }
491
+ prompt += `\n⚠️ 你必须确保文件写入指定路径。不要只报告完成,请用 write 工具实际写入。`
492
+ retryPrompts.push({ role_id: role.id, role_name: role.name, prompt })
493
+ }
494
+ }
495
+
496
+ return {
497
+ workflow: workflowName,
498
+ project: projectName,
499
+ status: allPassed ? 'pass' : 'fail',
500
+ spec_version: specVersion,
501
+ roles,
502
+ workflow_checks: workflowCheckResults,
503
+ failures,
504
+ retry_prompts: retryPrompts
505
+ }
506
+ }
507
+
508
+ /**
509
+ * 格式化检查结果为人类可读报告(兼容旧接口)
510
+ */
511
+ export function formatCheckReport(result) {
512
+ const lines = []
513
+ lines.push('\n📋 Workflow Post-Check 报告\n')
514
+
515
+ for (const r of (result.roles || [])) {
516
+ const icon = r.status === 'pass' ? '✅' : '❌'
517
+ lines.push(`${icon} ${r.name} (${r.id})`)
518
+ // 兼容新旧格式
519
+ const outputFailures = (r.outputs || []).flatMap(o =>
520
+ (o.checks || []).filter(c => c.status === 'fail').map(c => c.detail)
521
+ )
522
+ for (const f of outputFailures) {
523
+ lines.push(` └─ ${f}`)
524
+ }
525
+ }
526
+
527
+ if ((result.workflow_checks || []).some(c => c.status === 'fail')) {
528
+ lines.push('')
529
+ for (const c of result.workflow_checks) {
530
+ if (c.status === 'fail') {
531
+ lines.push(`❌ 全局检查失败: ${c.detail}`)
532
+ }
533
+ }
534
+ }
535
+
536
+ if (result.status === 'pass') {
537
+ lines.push('\n✅ 全部检查通过')
538
+ } else {
539
+ lines.push('\n❌ 存在失败项,请根据以下重试提示修复:')
540
+ }
541
+
542
+ return lines.join('\n')
543
+ }
544
+
545
+ // ─── Workflow Run 归档 ───
546
+
547
+
548
+ /**
549
+ * 将 workflow check 结果归档到 .sillyspec/.runtime/workflow-runs/
550
+ * @param {object} result - runPostCheck 返回的结构化结果
551
+ * @param {object} options
552
+ * @param {string} options.cwd - 项目根目录
553
+ * @param {string} [options.source] - 调用来源('run.js' / 'cli')
554
+ * @param {string} [options.stage] - 阶段名(scan/archive)
555
+ * @param {string} [options.step] - 步骤名
556
+ * @returns {string|null} 保存路径,失败返回 null
557
+ */
558
+ export function saveWorkflowRun(result, options = {}) {
559
+ const { cwd = '.', source = 'unknown', stage, step, runtimeRoot, scanRunId } = options
560
+ // 平台模式:写入 runtime-root/scan-runs/<scan-run-id>/workflow-runs/
561
+ // 本地模式:写入 cwd/.sillyspec/.runtime/workflow-runs/
562
+ const runDir = runtimeRoot
563
+ ? join(runtimeRoot, 'scan-runs', scanRunId || 'unknown', 'workflow-runs')
564
+ : join(cwd, '.sillyspec', '.runtime', 'workflow-runs')
565
+ try {
566
+ mkdirSync(runDir, { recursive: true })
567
+ } catch (e) {
568
+ console.warn('⚠️ 无法创建 workflow-runs 目录:', e.message)
569
+ return null
570
+ }
571
+
572
+ const now = new Date()
573
+ const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14)
574
+ const filename = `${ts}-${result.workflow || 'unknown'}-${result.project || 'default'}-${result.status}.json`
575
+ const filepath = join(runDir, filename)
576
+
577
+ const record = {
578
+ run_id: filename.replace('.json', ''),
579
+ created_at: now.toISOString(),
580
+ source,
581
+ ...(stage ? { stage } : {}),
582
+ ...(step ? { step } : {}),
583
+ workflow: result.workflow,
584
+ project: result.project,
585
+ status: result.status,
586
+ spec_version: result.spec_version,
587
+ roles: result.roles,
588
+ workflow_checks: result.workflow_checks,
589
+ failures: result.failures,
590
+ retry_prompts: result.retry_prompts
591
+ }
592
+
593
+ try {
594
+ writeFileSync(filepath, JSON.stringify(record, null, 2), 'utf8')
595
+ return filepath
596
+ } catch (e) {
597
+ console.warn('⚠️ 保存 workflow run 失败:', e.message)
598
+ return null
599
+ }
600
+ }