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
@@ -1,501 +0,0 @@
1
- /**
2
- * repeatableWait / requiresWait / 普通 wait 门控测试
3
- *
4
- * 测试点:
5
- * 1. repeatableWait --continue 后仍 pending(不 completed)
6
- * 2. requiresWait --continue 后仍 pending(不 completed)
7
- * 3. 普通 wait --continue 后 completed
8
- * 4. requiresWait 直接 --done 被拒绝
9
- * 5. repeatableWait 多轮后 --done 可推进
10
- * 6. brainstorm validator 检测四件套缺失
11
- */
12
-
13
- import { join, resolve, basename, dirname } from 'path'
14
- import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'fs'
15
- import { fileURLToPath, pathToFileURL } from 'url'
16
- import { execSync } from 'child_process'
17
- import { tmpdir } from 'os'
18
-
19
- const __filename = fileURLToPath(import.meta.url)
20
- const __dirname = dirname(__filename)
21
- const root = resolve(__dirname, '..')
22
- const binCLI = join(root, 'bin', 'sillyspec.js')
23
-
24
- function imp(path) {
25
- return import(pathToFileURL(path).href)
26
- }
27
-
28
- let passed = 0
29
- let failed = 0
30
-
31
- function assert(condition, msg) {
32
- if (condition) {
33
- console.log(` ✅ PASS: ${msg}`)
34
- passed++
35
- } else {
36
- console.log(` ❌ FAIL: ${msg}`)
37
- failed++
38
- }
39
- }
40
-
41
- function run(cmd, opts = {}) {
42
- try {
43
- return execSync(cmd, { encoding: 'utf8', timeout: 10000, ...opts })
44
- } catch (e) {
45
- return (e.stdout || '') + (e.stderr || '')
46
- }
47
- }
48
-
49
- function tmpDir(label) {
50
- const dir = join(tmpdir(), `sillyspec-wait-test-${label}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`)
51
- mkdirSync(dir, { recursive: true })
52
- return dir
53
- }
54
-
55
- function cleanup(dir) {
56
- try { rmSync(dir, { recursive: true, force: true }) } catch {}
57
- }
58
-
59
- // ── Setup helper: create initialized project with a change ──
60
- function setupProject(label) {
61
- const projectDir = tmpDir(label)
62
- run(`node "${binCLI}" init "${projectDir}"`)
63
- // Create a change directory with four-piece files
64
- const changeName = '2026-06-14-test-change'
65
- const changeDir = join(projectDir, '.sillyspec', 'changes', changeName)
66
- mkdirSync(changeDir, { recursive: true })
67
- // Register the change in DB by running a harmless command
68
- const specDir = join(projectDir, '.sillyspec')
69
- return { projectDir, changeName, changeDir, specDir }
70
- }
71
-
72
- function writeFourFiles(changeDir) {
73
- writeFileSync(join(changeDir, 'design.md'), `# Design\n\n## 背景\nTest\n## 文件变更清单\n| 操作 | 文件 | 说明 |\n## 风险登记\n| 编号 | 风险 |\n## 自审\nOK\n`)
74
- writeFileSync(join(changeDir, 'proposal.md'), `# Proposal\n## 动机\nTest\n## 不在范围内\n- Nothing\n## 成功标准\nWorks\n`)
75
- writeFileSync(join(changeDir, 'requirements.md'), `# Requirements\n## 角色\n| 角色 |\n## 功能需求\n### FR-01: Test\nGiven X\nWhen Y\nThen Z\n`)
76
- writeFileSync(join(changeDir, 'tasks.md'), `# Tasks\n- [ ] Task 1: do something\n- [ ] Task 2: do more\n`)
77
- }
78
-
79
- // ── Read progress from DB ──
80
- async function readProgress(projectDir, changeName) {
81
- const { ProgressManager } = await imp(join(root, 'src', 'progress.js'))
82
- const pm = new ProgressManager()
83
- const progress = await pm.read(projectDir, changeName)
84
- return progress
85
- }
86
-
87
- async function writeProgress(projectDir, changeName, progress) {
88
- const { ProgressManager } = await imp(join(root, 'src', 'progress.js'))
89
- const pm = new ProgressManager()
90
- await pm._write(projectDir, progress, changeName)
91
- }
92
-
93
- // ================================================================
94
- // Test 1: repeatableWait --continue 后仍 pending
95
- // ================================================================
96
- console.log('\n=== Test 1: repeatableWait --continue 后仍 pending ===')
97
- {
98
- const { projectDir, changeName } = setupProject('repeatable')
99
-
100
- // Init brainstorm stage for the change
101
- run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --change ${changeName}`)
102
-
103
- // Read progress, find the repeatable step (对话式探索 = step 6, index 5)
104
- const progress = await readProgress(projectDir, changeName)
105
- const brainstormData = progress.stages.brainstorm
106
- assert(brainstormData && brainstormData.steps, 'brainstorm steps initialized')
107
-
108
- // Find 对话式探索 step index
109
- const exploreIdx = brainstormData.steps.findIndex(s => s.name === '对话式探索')
110
- assert(exploreIdx !== -1, '找到"对话式探索"步骤')
111
-
112
- // Set steps before explore to completed, set explore to waiting
113
- for (let i = 0; i < exploreIdx; i++) {
114
- brainstormData.steps[i].status = 'completed'
115
- brainstormData.steps[i].completedAt = new Date().toISOString()
116
- }
117
- brainstormData.steps[exploreIdx].status = 'waiting'
118
- brainstormData.steps[exploreIdx].waitReason = '等待用户回答需求问题'
119
- brainstormData.steps[exploreIdx].waitOptions = ['继续补充', '信息够了']
120
- brainstormData.steps[exploreIdx].waitedAt = new Date().toISOString()
121
- brainstormData.steps[exploreIdx].output = '你到底想要什么?'
122
- await writeProgress(projectDir, changeName, progress)
123
-
124
- // Now --continue with an answer
125
- const output = run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --continue --answer "我想要一个用户管理系统" --change ${changeName}`)
126
- assert(output.includes('回到当前步骤'), 'continue 输出包含"回到当前步骤"')
127
- assert(output.includes('🔁'), 'continue 输出包含 🔁 标记')
128
-
129
- // Read progress again, check step is still pending (not completed)
130
- const progress2 = await readProgress(projectDir, changeName)
131
- const step = progress2.stages.brainstorm.steps[exploreIdx]
132
- assert(step.status === 'pending', `"对话式探索" --continue 后状态是 pending (实际: ${step.status})`)
133
- assert(Array.isArray(step.waitAnswers) && step.waitAnswers.length === 1, `waitAnswers 有 1 条记录 (实际: ${JSON.stringify(step.waitAnswers)})`)
134
- assert(step.waitRound === 1, `waitRound === 1 (实际: ${step.waitRound})`)
135
-
136
- cleanup(projectDir)
137
- }
138
-
139
- // ================================================================
140
- // Test 2: requiresWait --continue 后仍 pending
141
- // ================================================================
142
- console.log('\n=== Test 2: requiresWait --continue 后仍 pending ===')
143
- {
144
- const { projectDir, changeName } = setupProject('requires')
145
-
146
- run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --change ${changeName}`)
147
-
148
- const progress = await readProgress(projectDir, changeName)
149
- const brainstormData = progress.stages.brainstorm
150
-
151
- // Find 提出 2-3 种方案 step
152
- const proposeIdx = brainstormData.steps.findIndex(s => s.name === '提出 2-3 种方案')
153
- assert(proposeIdx !== -1, '找到"提出 2-3 种方案"步骤')
154
-
155
- // Set all steps before it to completed, set it to waiting
156
- for (let i = 0; i < proposeIdx; i++) {
157
- brainstormData.steps[i].status = 'completed'
158
- brainstormData.steps[i].completedAt = new Date().toISOString()
159
- }
160
- brainstormData.steps[proposeIdx].status = 'waiting'
161
- brainstormData.steps[proposeIdx].waitReason = '等待用户选择方案'
162
- brainstormData.steps[proposeIdx].waitOptions = ['方案A', '方案B', '方案C']
163
- brainstormData.steps[proposeIdx].waitedAt = new Date().toISOString()
164
- brainstormData.steps[proposeIdx].output = '方案A vs 方案B'
165
- await writeProgress(projectDir, changeName, progress)
166
-
167
- // --continue with selection
168
- const output = run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --continue --answer "方案A" --change ${changeName}`)
169
- assert(output.includes('回到当前步骤'), 'continue 输出包含"回到当前步骤"')
170
-
171
- // Check step is pending, not completed
172
- const progress2 = await readProgress(projectDir, changeName)
173
- const step = progress2.stages.brainstorm.steps[proposeIdx]
174
- assert(step.status === 'pending', `"提出方案" --continue 后状态是 pending (实际: ${step.status})`)
175
- assert(step.waitAnswer === '方案A', 'waitAnswer === 方案A')
176
-
177
- cleanup(projectDir)
178
- }
179
-
180
- // ================================================================
181
- // Test 3: 普通 wait --continue 后 completed
182
- // ================================================================
183
- console.log('\n=== Test 3: 普通 wait --continue 后 completed ===')
184
- {
185
- const { projectDir, changeName } = setupProject('normal-wait')
186
-
187
- run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --change ${changeName}`)
188
-
189
- const progress = await readProgress(projectDir, changeName)
190
- const brainstormData = progress.stages.brainstorm
191
-
192
- // Find 状态检查 (step 0) — it's a normal step, not requiresWait
193
- // Simulate it being in waiting state (as if someone manually --wait'd it)
194
- brainstormData.steps[0].status = 'waiting'
195
- brainstormData.steps[0].waitReason = '等待原因'
196
- brainstormData.steps[0].waitedAt = new Date().toISOString()
197
- await writeProgress(projectDir, changeName, progress)
198
-
199
- // --continue
200
- const output = run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --continue --answer "继续" --change ${changeName}`)
201
-
202
- const progress2 = await readProgress(projectDir, changeName)
203
- const step = progress2.stages.brainstorm.steps[0]
204
- assert(step.status === 'completed', `普通 wait --continue 后 completed (实际: ${step.status})`)
205
-
206
- cleanup(projectDir)
207
- }
208
-
209
- // ================================================================
210
- // Test 4: requiresWait 直接 --done 被拒绝
211
- // ================================================================
212
- console.log('\n=== Test 4: requiresWait 直接 --done 被拒绝 ===')
213
- {
214
- const { projectDir, changeName } = setupProject('refuse-done')
215
-
216
- run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --change ${changeName}`)
217
-
218
- const progress = await readProgress(projectDir, changeName)
219
- const brainstormData = progress.stages.brainstorm
220
-
221
- // Find 提出 2-3 种方案 (requiresWait=true)
222
- const proposeIdx = brainstormData.steps.findIndex(s => s.name === '提出 2-3 种方案')
223
- for (let i = 0; i < proposeIdx; i++) {
224
- brainstormData.steps[i].status = 'completed'
225
- brainstormData.steps[i].completedAt = new Date().toISOString()
226
- }
227
- // Step is pending, no waitAnswer
228
- brainstormData.steps[proposeIdx].status = 'pending'
229
- await writeProgress(projectDir, changeName, progress)
230
-
231
- // Try --done directly (should fail because requiresWait && !waitAnswer)
232
- const output = run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --done --output "推荐方案A" --change ${changeName}`)
233
- assert(output.includes('必须先等待用户输入') || output.includes('不能直接'), 'requiresWait 步骤直接 --done 被拒绝')
234
-
235
- // Verify step is still pending
236
- const progress2 = await readProgress(projectDir, changeName)
237
- const step = progress2.stages.brainstorm.steps[proposeIdx]
238
- assert(step.status === 'pending', `拒绝后步骤仍是 pending (实际: ${step.status})`)
239
-
240
- cleanup(projectDir)
241
- }
242
-
243
- // ================================================================
244
- // Test 5: repeatableWait 多轮收集后 --done 可推进
245
- // ================================================================
246
- console.log('\n=== Test 5: repeatableWait 多轮后 --done 可推进 ===')
247
- {
248
- const { projectDir, changeName } = setupProject('multi-round')
249
-
250
- run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --change ${changeName}`)
251
-
252
- const progress = await readProgress(projectDir, changeName)
253
- const brainstormData = progress.stages.brainstorm
254
-
255
- const exploreIdx = brainstormData.steps.findIndex(s => s.name === '对话式探索')
256
- for (let i = 0; i < exploreIdx; i++) {
257
- brainstormData.steps[i].status = 'completed'
258
- brainstormData.steps[i].completedAt = new Date().toISOString()
259
- }
260
- brainstormData.steps[exploreIdx].status = 'waiting'
261
- brainstormData.steps[exploreIdx].waitReason = '等待用户回答'
262
- brainstormData.steps[exploreIdx].waitedAt = new Date().toISOString()
263
- brainstormData.steps[exploreIdx].output = '问题1'
264
- await writeProgress(projectDir, changeName, progress)
265
-
266
- // Round 1
267
- run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --continue --answer "回答1" --change ${changeName}`)
268
- let p2 = await readProgress(projectDir, changeName)
269
- assert(p2.stages.brainstorm.steps[exploreIdx].status === 'pending', '第1轮后仍 pending')
270
- assert(p2.stages.brainstorm.steps[exploreIdx].waitAnswers.length === 1, '1 条历史回答')
271
-
272
- // Simulate another --wait cycle
273
- p2.stages.brainstorm.steps[exploreIdx].status = 'waiting'
274
- p2.stages.brainstorm.steps[exploreIdx].waitReason = '等待用户回答'
275
- p2.stages.brainstorm.steps[exploreIdx].waitedAt = new Date().toISOString()
276
- p2.stages.brainstorm.steps[exploreIdx].output = '问题2'
277
- await writeProgress(projectDir, changeName, p2)
278
-
279
- // Round 2
280
- run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --continue --answer "回答2" --change ${changeName}`)
281
- let p3 = await readProgress(projectDir, changeName)
282
- assert(p3.stages.brainstorm.steps[exploreIdx].status === 'pending', '第2轮后仍 pending')
283
- assert(p3.stages.brainstorm.steps[exploreIdx].waitAnswers.length === 2, '2 条历史回答')
284
-
285
- // Now agent does --done (has waitAnswer so should pass requiresWait gate)
286
- run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --done --output "需求已明确" --change ${changeName}`)
287
- let p4 = await readProgress(projectDir, changeName)
288
- assert(p4.stages.brainstorm.steps[exploreIdx].status === 'completed', '--done 后步骤 completed')
289
-
290
- cleanup(projectDir)
291
- }
292
-
293
- // ================================================================
294
- // Test 6: brainstorm validator 检测四件套缺失
295
- // ================================================================
296
- console.log('\n=== Test 6: brainstorm validator 检测四件套缺失 ===')
297
- {
298
- const { projectDir, changeName, changeDir } = setupProject('validator')
299
- // Don't write the four files — validator should catch this
300
-
301
- const { runValidators } = await imp(join(root, 'src', 'stage-contract.js'))
302
- const result = runValidators('brainstorm', projectDir, changeName, {})
303
- assert(result.ok === false, '四件套缺失时 ok=false')
304
- assert(result.errors.length >= 4, `至少 4 个 error (实际: ${result.errors.length})`)
305
- assert(result.errors.some(e => e.includes('design.md')), 'error 包含 design.md')
306
- assert(result.errors.some(e => e.includes('proposal.md')), 'error 包含 proposal.md')
307
- assert(result.errors.some(e => e.includes('requirements.md')), 'error 包含 requirements.md')
308
- assert(result.errors.some(e => e.includes('tasks.md')), 'error 包含 tasks.md')
309
-
310
- // Now write all four files
311
- writeFourFiles(changeDir)
312
- const result2 = runValidators('brainstorm', projectDir, changeName, {})
313
- assert(result2.ok === true, '四件套存在时 ok=true')
314
- assert(result2.errors.length === 0, '四件套存在时无 error')
315
-
316
- cleanup(projectDir)
317
- }
318
-
319
- // ================================================================
320
- // Test 7: tasks.md 空列表只 warning 不 error
321
- // ================================================================
322
- console.log('\n=== Test 7: tasks.md 空列表只 warning 不 error ===')
323
- {
324
- const { projectDir, changeName, changeDir } = setupProject('empty-tasks')
325
-
326
- writeFileSync(join(changeDir, 'design.md'), `# Design\n## 背景\n## 文件变更清单\n## 风险登记\n## 自审\n`)
327
- writeFileSync(join(changeDir, 'proposal.md'), `# Proposal\n## 不在范围内\n- x\n`)
328
- writeFileSync(join(changeDir, 'requirements.md'), `# Requirements\n### FR-01: x\nGiven\nWhen\nThen\n`)
329
- writeFileSync(join(changeDir, 'tasks.md'), `# Tasks\n(no items)\n`) // no list items
330
-
331
- const { runValidators } = await imp(join(root, 'src', 'stage-contract.js'))
332
- const result = runValidators('brainstorm', projectDir, changeName, {})
333
- assert(result.ok === true, '空 tasks.md → ok=true (文件存在)')
334
- assert(result.warnings.some(w => w.includes('任务列表')), '空 tasks.md → warning about 任务列表')
335
-
336
- cleanup(projectDir)
337
- }
338
-
339
-
340
- // ================================================================
341
- // Test 8: 多个 waiting 步骤 → --continue 必须报错
342
- // ================================================================
343
- console.log('\n=== Test 8: 多个 waiting 步骤 → --continue 报错 ===')
344
- {
345
- const { projectDir, changeName } = setupProject('multi-waiting')
346
-
347
- run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --change ${changeName}`)
348
-
349
- const progress = await readProgress(projectDir, changeName)
350
- const brainstormData = progress.stages.brainstorm
351
- // Set two steps to waiting
352
- brainstormData.steps[0].status = 'waiting'
353
- brainstormData.steps[0].waitReason = '等待1'
354
- brainstormData.steps[0].waitedAt = new Date().toISOString()
355
- brainstormData.steps[1].status = 'waiting'
356
- brainstormData.steps[1].waitReason = '等待2'
357
- brainstormData.steps[1].waitedAt = new Date().toISOString()
358
- await writeProgress(projectDir, changeName, progress)
359
-
360
- const output = run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --continue --answer "test" --change ${changeName}`)
361
- assert(output.includes('检测到') && output.includes('等待中的步骤'), '多个 waiting 步骤时报错')
362
-
363
- cleanup(projectDir)
364
- }
365
-
366
- // ================================================================
367
- // Test 9: --skip-approval 不能绕过 requiresWait
368
- // ================================================================
369
- console.log('\n=== Test 9: --skip-approval 不能绕过 requiresWait ===')
370
- {
371
- const { projectDir, changeName } = setupProject('skip-approval')
372
-
373
- run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --change ${changeName}`)
374
-
375
- const progress = await readProgress(projectDir, changeName)
376
- const brainstormData = progress.stages.brainstorm
377
- const proposeIdx = brainstormData.steps.findIndex(s => s.name === '提出 2-3 种方案')
378
- for (let i = 0; i < proposeIdx; i++) {
379
- brainstormData.steps[i].status = 'completed'
380
- brainstormData.steps[i].completedAt = new Date().toISOString()
381
- }
382
- brainstormData.steps[proposeIdx].status = 'pending'
383
- await writeProgress(projectDir, changeName, progress)
384
-
385
- const output = run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --done --skip-approval --output "推荐方案A" --change ${changeName}`)
386
- assert(output.includes('必须先等待用户输入') || output.includes('不能直接'), '--skip-approval 无法绕过 requiresWait')
387
-
388
- cleanup(projectDir)
389
- }
390
-
391
- // ================================================================
392
- // Test 10: repeatableWait 达到 maxWaitRounds 后再次 --wait 被拒绝
393
- // ================================================================
394
- console.log('\n=== Test 10: maxWaitRounds 达到上限后 --wait 被拒绝 ===')
395
- {
396
- const { projectDir, changeName } = setupProject('max-rounds')
397
-
398
- run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --change ${changeName}`)
399
-
400
- const progress = await readProgress(projectDir, changeName)
401
- const brainstormData = progress.stages.brainstorm
402
- const exploreIdx = brainstormData.steps.findIndex(s => s.name === '对话式探索')
403
- for (let i = 0; i < exploreIdx; i++) {
404
- brainstormData.steps[i].status = 'completed'
405
- brainstormData.steps[i].completedAt = new Date().toISOString()
406
- }
407
- // Simulate already having maxRounds (3) rounds
408
- brainstormData.steps[exploreIdx].status = 'pending'
409
- brainstormData.steps[exploreIdx].waitRound = 3
410
- brainstormData.steps[exploreIdx].maxWaitRounds = 3
411
- brainstormData.steps[exploreIdx].waitAnswer = 'previous answer'
412
- await writeProgress(projectDir, changeName, progress)
413
-
414
- // Try to --wait again (should be refused)
415
- const output = run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --wait --reason "还想再问" --output "再问一个问题" --change ${changeName}`)
416
- assert(output.includes('已达到最大等待轮次') || output.includes('maxWaitRounds'), '达到 maxWaitRounds 后 --wait 被拒绝')
417
-
418
- // But --done should still work (has waitAnswer)
419
- const output2 = run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --done --output "需求已明确" --change ${changeName}`)
420
- const progress2 = await readProgress(projectDir, changeName)
421
- const step = progress2.stages.brainstorm.steps[exploreIdx]
422
- assert(step.status === 'completed', '达到上限后 --done 仍可推进')
423
-
424
- cleanup(projectDir)
425
- }
426
-
427
- // ================================================================
428
- // Test 11: specRoot 模式下 changes 缺失 → validator error
429
- // ================================================================
430
- console.log('\n=== Test 11: specRoot 模式下 changes 缺失 → fail-fast ===')
431
- {
432
- const specRoot = tmpDir('specroot-nofail')
433
- const { runValidators } = await imp(join(root, 'src', 'stage-contract.js'))
434
- // specRoot exists but has no changes/ directory
435
- const result = runValidators('brainstorm', '/some/project', 'test-change', { specRoot })
436
- assert(result.ok === false, 'specRoot 缺 changes 目录时 ok=false')
437
- assert(result.errors.some(e => e.includes('缺少 changes 目录')), 'error 包含「缺少 changes 目录」')
438
-
439
- cleanup(specRoot)
440
- }
441
-
442
- // ================================================================
443
- // Test 12: waitRound=0 正确写入读取(不被 || 吞掉)
444
- // ================================================================
445
- console.log('\n=== Test 12: waitRound=0 正确持久化 ===')
446
- {
447
- const { projectDir, changeName } = setupProject('waitround-zero')
448
-
449
- run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --change ${changeName}`)
450
-
451
- const progress = await readProgress(projectDir, changeName)
452
- const brainstormData = progress.stages.brainstorm
453
- // Manually set waitRound=0 (simulates edge case)
454
- brainstormData.steps[0].status = 'pending'
455
- brainstormData.steps[0].waitRound = 0
456
- brainstormData.steps[0].maxWaitRounds = 3
457
- await writeProgress(projectDir, changeName, progress)
458
-
459
- const p2 = await readProgress(projectDir, changeName)
460
- // waitRound=0 should be read back as 0 (not null/undefined)
461
- assert(p2.stages.brainstorm.steps[0].waitRound === 0, `waitRound=0 正确读取 (实际: ${p2.stages.brainstorm.steps[0].waitRound})`)
462
- assert(p2.stages.brainstorm.steps[0].maxWaitRounds === 3, `maxWaitRounds=3 正确读取 (实际: ${p2.stages.brainstorm.steps[0].maxWaitRounds})`)
463
-
464
- cleanup(projectDir)
465
- }
466
-
467
- // ================================================================
468
- // Test 13: 已有 waiting 步骤时 --wait 被拒绝
469
- // ================================================================
470
- console.log('\n=== Test 13: 已有 waiting 步骤时 --wait 被拒绝 ===')
471
- {
472
- const { projectDir, changeName } = setupProject('existing-wait')
473
-
474
- run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --change ${changeName}`)
475
-
476
- const progress = await readProgress(projectDir, changeName)
477
- const brainstormData = progress.stages.brainstorm
478
- // Set step 0 to waiting
479
- brainstormData.steps[0].status = 'waiting'
480
- brainstormData.steps[0].waitReason = '等待中'
481
- brainstormData.steps[0].waitedAt = new Date().toISOString()
482
- await writeProgress(projectDir, changeName, progress)
483
-
484
- // Try --wait on another step (should be refused)
485
- const output = run(`node "${binCLI}" --dir "${projectDir}" run brainstorm --wait --reason "新问题" --output "新等待" --change ${changeName}`)
486
- assert(output.includes('已有步骤处于等待状态') || output.includes('等待状态'), '已有 waiting 步骤时 --wait 被拒绝')
487
-
488
- cleanup(projectDir)
489
- }
490
-
491
- // ── Summary ──
492
- console.log(`\n${'='.repeat(50)}`)
493
- if (failed === 0) {
494
- console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
495
- console.log(`${'='.repeat(50)}`)
496
- console.log('\n🎉 wait gates 测试全部通过!')
497
- } else {
498
- console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
499
- console.log(`${'='.repeat(50)}`)
500
- }
501
- process.exit(failed > 0 ? 1 : 0)
@@ -1,142 +0,0 @@
1
- /**
2
- * task-04: workflow.js checkOutput / _checkWorkflow / runPostCheck 支持 specBase
3
- *
4
- * 覆盖:
5
- * - AC-01/AC-03: 平台模式 specBase 透传,scanDir = join(specBase, 'docs', project, 'scan/')
6
- * - AC-02/AC-04: 非平台模式 specBase 缺省,回退 join(cwd, '.sillyspec')(旧行为)
7
- * - AC-05: runPostCheck(wf, cwd, name, {}, specBase) 显式传 specBase 透传到 _checkWorkflow/checkOutput
8
- * - AC-06: archive 同样支持
9
- */
10
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
11
- import { tmpdir } from 'os'
12
- import { join, resolve } from 'node:path'
13
- import { runPostCheck } from '../src/workflow.js'
14
-
15
- let passed = 0
16
- let failed = 0
17
- function assert (cond, msg) {
18
- if (cond) { console.log(`✅ PASS: ${msg}`); passed++ }
19
- else { console.error(`❌ FAIL: ${msg}`); failed++ }
20
- }
21
- function assertEqual (actual, expected, msg) {
22
- const ok = actual === expected
23
- if (ok) { console.log(`✅ PASS: ${msg}`); passed++ }
24
- else { console.error(`❌ FAIL: ${msg}\n expected: ${JSON.stringify(expected)}\n actual: ${JSON.stringify(actual)}`); failed++ }
25
- }
26
-
27
- // ── 用例 1: 平台模式 specBase — workflow_level file_count 检查 join(specBase, 'docs', project, 'scan/') ──
28
- {
29
- const specBase = mkdtempSync(join(tmpdir(), 'spec-base-platform-'))
30
- const projectName = 'frontend'
31
- const scanDir = join(specBase, 'docs', projectName, 'scan')
32
- mkdirSync(scanDir, { recursive: true })
33
- writeFileSync(join(scanDir, 'a.md'), '# A\ncontent\n')
34
-
35
- const wf = {
36
- name: 'scan-docs',
37
- checks: { workflow_level: [ { type: 'file_count', path: 'scan/', min: 1 } ] },
38
- roles: [],
39
- }
40
- const result = runPostCheck(wf, '/fake/cwd', projectName, {}, specBase)
41
- const fc = (result.workflow_checks || []).find(c => c.type === 'file_count')
42
- assert(!!fc, '平台模式 file_count 检查被执行')
43
- assertEqual(fc && fc.status, 'pass', '平台模式 specBase 下 file_count 通过(找到 1 个 md)')
44
- assert(!(fc && fc.detail && fc.detail.includes('/fake/cwd')),
45
- '平台模式 detail 不含 cwd(说明用 specBase 而非裸 cwd)')
46
- rmSync(specBase, { recursive: true, force: true })
47
- }
48
-
49
- // ── 用例 2: 平台模式 specBase — 目录不存在时失败且 detail 含 specBase 路径 ──
50
- {
51
- const specBase = mkdtempSync(join(tmpdir(), 'spec-base-empty-'))
52
- const projectName = 'backend'
53
- const wf = {
54
- name: 'scan-docs',
55
- checks: { workflow_level: [ { type: 'file_count', path: 'scan/', min: 1 } ] },
56
- roles: [],
57
- }
58
- const result = runPostCheck(wf, '/fake/cwd', projectName, {}, specBase)
59
- const fc = (result.workflow_checks || []).find(c => c.type === 'file_count')
60
- assertEqual(fc && fc.status, 'fail', '平台模式 specBase 下目录不存在时 file_count 失败')
61
- assert(!!(fc && fc.detail && fc.detail.includes(specBase.replace(/\\/g, '/'))) ||
62
- !!(fc && fc.detail && fc.detail.includes(specBase)),
63
- '平台模式 detail 含 specBase 路径(说明用 specBase)')
64
- assert(!(fc && fc.detail && fc.detail.includes('/fake/cwd/.sillyspec')),
65
- 'detail 不含 fake/cwd/.sillyspec(未回退裸 cwd)')
66
- rmSync(specBase, { recursive: true, force: true })
67
- }
68
-
69
- // ── 用例 3: 非平台模式 specBase 缺省 — 回退 join(cwd, '.sillyspec')(旧行为) ──
70
- {
71
- const cwd = mkdtempSync(join(tmpdir(), 'legacy-cwd-'))
72
- const sillyspecDir = join(cwd, '.sillyspec')
73
- const projectName = 'sillyspec'
74
- const scanDir = join(sillyspecDir, 'docs', projectName, 'scan')
75
- mkdirSync(scanDir, { recursive: true })
76
- writeFileSync(join(scanDir, 'a.md'), '# A\n')
77
-
78
- const wf = {
79
- name: 'scan-docs',
80
- checks: { workflow_level: [ { type: 'file_count', path: 'scan/', min: 1 } ] },
81
- roles: [],
82
- }
83
- // 不传 specBase(第 5 位留空)
84
- const result = runPostCheck(wf, cwd, projectName)
85
- const fc = (result.workflow_checks || []).find(c => c.type === 'file_count')
86
- assertEqual(fc && fc.status, 'pass', '非平台模式 specBase 缺省时回退 cwd/.sillyspec 通过')
87
- rmSync(cwd, { recursive: true, force: true })
88
- }
89
-
90
- // ── 用例 4: 平台模式 role-level checkOutput — specBase 下产出文件存在 ──
91
- {
92
- const specBase = mkdtempSync(join(tmpdir(), 'spec-base-role-'))
93
- const projectName = 'myproj'
94
- const docAbsPath = join(specBase, 'docs', projectName, 'scan', 'ARCHITECTURE.md')
95
- mkdirSync(join(specBase, 'docs', projectName, 'scan'), { recursive: true })
96
- writeFileSync(docAbsPath, '# Arch\nline1\nline2\nline3\nline4\nline5\n')
97
-
98
- // outputDef.path 模拟 run.js:645 把 {SPEC_ROOT} 替换为 specBase 后的绝对路径
99
- const wf = {
100
- name: 'scan-docs',
101
- roles: [
102
- {
103
- id: 'doc-writer',
104
- name: 'Doc Writer',
105
- outputs: [
106
- { path: docAbsPath.replace(/\\/g, '/'), checks: [ { type: 'file_exists' }, { type: 'min_lines', min: 3 } ] },
107
- ],
108
- },
109
- ],
110
- }
111
- const result = runPostCheck(wf, '/fake/cwd', projectName, {}, specBase)
112
- const role = (result.roles || [])[0]
113
- assertEqual(role && role.status, 'pass', '平台模式 role-level checkOutput 用 specBase 解析绝对路径通过')
114
- rmSync(specBase, { recursive: true, force: true })
115
- }
116
-
117
- // ── 用例 5: 接口向后兼容 — 不传 specBase 时 placeholders 仍工作 ──
118
- {
119
- const cwd = mkdtempSync(join(tmpdir(), 'ph-cwd-'))
120
- const projectName = 'demo'
121
- const scanDir = join(cwd, '.sillyspec', 'docs', projectName, 'scan')
122
- mkdirSync(scanDir, { recursive: true })
123
- writeFileSync(join(scanDir, 'a.md'), '# A\n')
124
-
125
- const wf = {
126
- name: 'scan-docs',
127
- checks: { workflow_level: [ { type: 'file_count', path: 'scan/', min: 1 } ] },
128
- roles: [],
129
- }
130
- // 旧调用方式:runPostCheck(wf, cwd, name, placeholders)
131
- const result = runPostCheck(wf, cwd, projectName, { SOME_KEY: 'value' })
132
- const fc = (result.workflow_checks || []).find(c => c.type === 'file_count')
133
- assertEqual(fc && fc.status, 'pass', '向后兼容:旧 4 参调用(带 placeholders)仍工作')
134
- rmSync(cwd, { recursive: true, force: true })
135
- }
136
-
137
- console.log(`\n${'='.repeat(50)}`)
138
- console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
139
- console.log(`${'='.repeat(50)}`)
140
- if (failed > 0) {
141
- process.exit(1)
142
- }