sillyspec 3.20.2 → 3.20.4

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 (133) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +21 -21
  2. package/.claude/skills/sillyspec-auto/SKILL.md +83 -83
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -44
  4. package/.claude/skills/sillyspec-commit/SKILL.md +106 -106
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +31 -31
  7. package/.claude/skills/sillyspec-execute/SKILL.md +30 -30
  8. package/.claude/skills/sillyspec-explore/SKILL.md +109 -109
  9. package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
  10. package/.claude/skills/sillyspec-plan/SKILL.md +21 -21
  11. package/.claude/skills/sillyspec-propose/SKILL.md +21 -21
  12. package/.claude/skills/sillyspec-quick/SKILL.md +21 -21
  13. package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
  14. package/.claude/skills/sillyspec-scan/SKILL.md +21 -21
  15. package/.claude/skills/sillyspec-state/SKILL.md +54 -54
  16. package/.claude/skills/sillyspec-status/SKILL.md +21 -21
  17. package/.claude/skills/sillyspec-verify/SKILL.md +21 -21
  18. package/.claude/skills/sillyspec-workspace/SKILL.md +157 -157
  19. package/.husky/pre-push +13 -13
  20. package/CLAUDE.md +18 -18
  21. package/README.md +198 -188
  22. package/SKILL.md +90 -91
  23. package/bin/sillyspec.js +2 -2
  24. package/docs/brainstorm-plan-contract.md +64 -64
  25. package/docs/plan-execute-contract.md +123 -123
  26. package/docs/platform-scan-protocol.md +298 -298
  27. package/docs/revision-mode.md +115 -115
  28. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -99
  29. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -218
  30. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -167
  31. package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -148
  32. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -193
  33. package/docs/sillyspec/file-lifecycle.md +125 -125
  34. package/docs/workflow-contract-regression.md +106 -106
  35. package/docs/worktree-isolation.md +252 -252
  36. package/package.json +40 -40
  37. package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
  38. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
  39. package/packages/dashboard/dist/index.html +16 -16
  40. package/packages/dashboard/index.html +15 -15
  41. package/packages/dashboard/package-lock.json +2384 -2384
  42. package/packages/dashboard/package.json +25 -25
  43. package/packages/dashboard/server/executor.js +86 -86
  44. package/packages/dashboard/server/index.js +588 -588
  45. package/packages/dashboard/server/parser.js +526 -526
  46. package/packages/dashboard/server/watcher.js +344 -344
  47. package/packages/dashboard/src/App.vue +558 -558
  48. package/packages/dashboard/src/components/ActionBar.vue +93 -93
  49. package/packages/dashboard/src/components/CommandPalette.vue +96 -96
  50. package/packages/dashboard/src/components/DetailPanel.vue +137 -137
  51. package/packages/dashboard/src/components/LogStream.vue +65 -65
  52. package/packages/dashboard/src/components/PipelineStage.vue +95 -95
  53. package/packages/dashboard/src/components/PipelineView.vue +156 -156
  54. package/packages/dashboard/src/components/ProjectList.vue +210 -210
  55. package/packages/dashboard/src/components/StageBadge.vue +67 -67
  56. package/packages/dashboard/src/components/StepCard.vue +94 -94
  57. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
  58. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
  59. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
  60. package/packages/dashboard/src/composables/useDashboard.js +170 -170
  61. package/packages/dashboard/src/composables/useKeyboard.js +119 -119
  62. package/packages/dashboard/src/composables/useWebSocket.js +129 -129
  63. package/packages/dashboard/src/main.js +8 -8
  64. package/packages/dashboard/src/style.css +132 -132
  65. package/packages/dashboard/vite.config.js +18 -18
  66. package/src/brainstorm-postcheck.js +158 -158
  67. package/src/change-list.js +52 -52
  68. package/src/change-risk-profile.js +352 -352
  69. package/src/classify-change.js +73 -73
  70. package/src/constants.js +70 -70
  71. package/src/contract-matrix.js +278 -278
  72. package/src/db.js +201 -201
  73. package/src/endpoint-extractor.js +315 -315
  74. package/src/hooks/claude-pre-tool-use.cjs +125 -125
  75. package/src/hooks/worktree-guard.js +653 -653
  76. package/src/index.js +922 -900
  77. package/src/init.js +431 -431
  78. package/src/knowledge-match.js +130 -130
  79. package/src/migrate.js +117 -117
  80. package/src/modules.js +482 -482
  81. package/src/progress.js +1734 -1734
  82. package/src/run.js +3465 -3358
  83. package/src/scan-postcheck.js +387 -383
  84. package/src/setup.js +398 -398
  85. package/src/stage-contract.js +700 -700
  86. package/src/stages/archive.js +160 -160
  87. package/src/stages/brainstorm-auto.js +229 -229
  88. package/src/stages/brainstorm.js +645 -645
  89. package/src/stages/doctor.js +365 -365
  90. package/src/stages/execute.js +625 -625
  91. package/src/stages/explore.js +34 -34
  92. package/src/stages/index.js +29 -29
  93. package/src/stages/knowledge.js +498 -498
  94. package/src/stages/plan-postcheck.js +511 -513
  95. package/src/stages/plan.js +582 -582
  96. package/src/stages/propose.js +174 -174
  97. package/src/stages/quick.js +82 -82
  98. package/src/stages/scan.js +558 -558
  99. package/src/stages/status.js +65 -65
  100. package/src/stages/verify.js +322 -322
  101. package/src/sync.js +497 -497
  102. package/src/task-review.js +346 -346
  103. package/src/workflow.js +785 -785
  104. package/src/worktree-apply.js +549 -549
  105. package/src/worktree-deps.js +185 -0
  106. package/src/worktree.js +982 -932
  107. package/templates/workflows/archive-impact.yaml +79 -79
  108. package/templates/workflows/scan-docs.yaml +132 -132
  109. package/test/brainstorm-plan-contract.test.mjs +273 -273
  110. package/test/check-syntax.mjs +26 -26
  111. package/test/contract-artifacts.test.mjs +323 -323
  112. package/test/decision-supersede.test.mjs +277 -277
  113. package/test/knowledge-match.test.mjs +231 -231
  114. package/test/plan-execute-contract.test.mjs +330 -330
  115. package/test/plan-optimization.test.mjs +572 -572
  116. package/test/platform-artifacts.test.mjs +166 -166
  117. package/test/platform-failure-samples.test.mjs +199 -199
  118. package/test/platform-recovery-chain.test.mjs +167 -167
  119. package/test/platform-recovery.test.mjs +136 -136
  120. package/test/platform-scan-p0.test.mjs +168 -168
  121. package/test/revision-v1.test.mjs +1145 -1145
  122. package/test/run-scan-project-parse.test.mjs +200 -200
  123. package/test/run-tests.mjs +48 -48
  124. package/test/scan-knowledge.test.mjs +175 -175
  125. package/test/scan-paths.test.mjs +68 -68
  126. package/test/scan-postcheck.test.mjs +197 -197
  127. package/test/spec-dir.test.mjs +206 -206
  128. package/test/stage-contract.test.mjs +299 -299
  129. package/test/stage-definitions.test.mjs +39 -39
  130. package/test/wait-gates.test.mjs +496 -496
  131. package/test/worktree-deps-provision.test.mjs +148 -0
  132. package/test/worktree-guard.test.mjs +71 -71
  133. package/test/worktree-native-overlay.test.mjs +188 -188
@@ -1,206 +1,206 @@
1
- /**
2
- * --spec-dir 功能测试
3
- *
4
- * 测试点:
5
- * 1. ProgressManager 外部 specDir 路径正确
6
- * 2. init 外部 specDir 不污染源码
7
- * 3. 默认模式不受影响
8
- * 4. 平台模式 prompt 注入(scan/brainstorm/plan/execute/verify/quick)
9
- * 5. 非 platform 模式占位符替换(无 undefined/null)
10
- * 6. --spec-dir 与 --spec-root 兼容
11
- * 7. progress 使用外部 specDir
12
- */
13
-
14
- import { join, resolve, basename, dirname } from 'path'
15
- import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs'
16
- import { fileURLToPath, pathToFileURL } from 'url'
17
- import { execSync } from 'child_process'
18
- import { tmpdir } from 'os'
19
-
20
- const __filename = fileURLToPath(import.meta.url)
21
- const __dirname = dirname(__filename)
22
- const root = resolve(__dirname, '..')
23
- const binCLI = join(root, 'bin', 'sillyspec.js')
24
-
25
- function imp(path) {
26
- return import(pathToFileURL(path).href)
27
- }
28
-
29
- let passed = 0
30
- let failed = 0
31
-
32
- function assert(condition, msg) {
33
- if (condition) {
34
- console.log(` ✅ PASS: ${msg}`)
35
- passed++
36
- } else {
37
- console.log(` ❌ FAIL: ${msg}`)
38
- failed++
39
- }
40
- }
41
-
42
- function tmpDir(name) {
43
- const dir = join(tmpdir(), `spec-dir-test-${name}-${Date.now()}`)
44
- mkdirSync(dir, { recursive: true })
45
- return dir
46
- }
47
-
48
- function cleanup(dir) {
49
- try { rmSync(dir, { recursive: true, force: true }) } catch {}
50
- }
51
-
52
- function run(cmd) {
53
- return execSync(cmd, { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] })
54
- }
55
-
56
- // ── Test 1: ProgressManager 外部 specDir ──
57
- console.log('\n=== Test 1: ProgressManager 外部 specDir ===')
58
- {
59
- const { ProgressManager } = await imp(join(root, 'src', 'progress.js'))
60
- const tmp = tmpDir('pm')
61
- const specDir = join(tmp, 'external-spec')
62
-
63
- const pm = new ProgressManager({ specDir })
64
- assert(pm._getSpecDir(tmp) === specDir, `_getSpecDir 返回自定义路径`)
65
-
66
- // 无自定义 specDir 时,resolveSpecDir 会向上查找 .sillyspec 目录
67
- // 在测试环境中可能命中上层已有的 .sillyspec,所以只检查返回值是否有效路径
68
- const pm2 = new ProgressManager()
69
- const resolved = pm2._getSpecDir(tmp)
70
- assert(typeof resolved === 'string' && resolved.length > 0 && resolved.endsWith('.sillyspec'),
71
- `_getSpecDir 无自定义时返回有效 .sillyspec 路径 (got: ${resolved})`)
72
-
73
- assert(pm._runtimePath(tmp) === join(specDir, '.runtime'), `_runtimePath 基于 specDir`)
74
- assert(pm._changePath(tmp, 'c') === join(specDir, 'changes', 'c'), `_changePath 基于 specDir`)
75
-
76
- // 外部 specDir 时 _ensureGitignore 应跳过
77
- const gitignoreResult = pm._ensureGitignore(tmp)
78
- assert(gitignoreResult === undefined, `外部 specDir 时 _ensureGitignore 跳过`)
79
-
80
- cleanup(tmp)
81
- }
82
-
83
- // ── Test 2: init 外部 specDir 不污染源码 ──
84
- console.log('\n=== Test 2: init 外部 specDir 不污染源码 ===')
85
- {
86
- const { cmdInit } = await imp(join(root, 'src', 'init.js'))
87
- const projectDir = tmpDir('project')
88
- const specDir = tmpDir('spec')
89
-
90
- await cmdInit(projectDir, { specDir })
91
-
92
- assert(!existsSync(join(projectDir, '.sillyspec')), '源码目录不含 .sillyspec')
93
- assert(!existsSync(join(projectDir, '.gitignore')), '外部 specDir 时不创建 .gitignore')
94
- assert(existsSync(join(specDir, 'projects')), `specDir/projects 存在`)
95
- assert(existsSync(join(specDir, 'docs')), `specDir/docs 存在`)
96
- assert(existsSync(join(specDir, '.runtime', 'sillyspec.db')), `specDir/.runtime/sillyspec.db 存在`)
97
- assert(existsSync(join(specDir, 'workflows')), `specDir/workflows 存在`)
98
- assert(existsSync(join(projectDir, '.claude')), `源码目录 .claude 存在(工具指令)`)
99
-
100
- cleanup(projectDir)
101
- cleanup(specDir)
102
- }
103
-
104
- // ── Test 3: 默认模式不受影响 ──
105
- console.log('\n=== Test 3: 默认模式不受影响 ===')
106
- {
107
- const { cmdInit } = await imp(join(root, 'src', 'init.js'))
108
- const projectDir = tmpDir('default')
109
-
110
- await cmdInit(projectDir, {})
111
-
112
- assert(existsSync(join(projectDir, '.sillyspec')), '默认模式创建 .sillyspec 在项目内')
113
- assert(existsSync(join(projectDir, '.sillyspec', '.runtime', 'sillyspec.db')), '默认模式 DB 在项目内')
114
- assert(existsSync(join(projectDir, '.gitignore')), '默认模式创建 .gitignore')
115
-
116
- cleanup(projectDir)
117
- }
118
-
119
- // ── Test 4: 平台模式 prompt 注入(多 stage) ──
120
- console.log('\n=== Test 4: 平台模式 prompt 注入 ===')
121
- {
122
- const projectDir = tmpDir('prompt-p')
123
- const specDir = tmpDir('prompt-s')
124
-
125
- run(`node "${binCLI}" init "${projectDir}" --spec-dir "${specDir}"`)
126
-
127
- // execute 阶段会自动创建 worktree,在非 git 环境下会失败,跳过
128
- const stages = ['scan', 'brainstorm', 'plan', 'verify', 'quick']
129
- for (const stage of stages) {
130
- const output = run(`node "${binCLI}" --dir "${projectDir}" --spec-dir "${specDir}" run ${stage} --skip-approval`)
131
- assert(output.includes('平台模式'), `${stage}: 包含平台模式指令`)
132
- assert(output.includes(`规范目录(specDir): \`${specDir}\``), `${stage}: 包含正确的 specDir 路径`)
133
- }
134
-
135
- // scan 额外检查
136
- const scanOutput = run(`node "${binCLI}" --dir "${projectDir}" --spec-dir "${specDir}" run scan`)
137
- assert(scanOutput.includes('严禁写入源码目录'), 'scan: 包含严禁写入源码目录')
138
- assert(scanOutput.includes('Write 工具失败时,不允许'), 'scan: 包含 Write 工具规则')
139
- assert(scanOutput.includes('变更目录'), 'scan: 包含变更目录')
140
-
141
- cleanup(projectDir)
142
- cleanup(specDir)
143
- }
144
-
145
- // ── Test 5: 非 platform 模式占位符替换 ──
146
- console.log('\n=== Test 5: 非 platform 模式占位符替换 ===')
147
- {
148
- const projectDir = tmpDir('noplatform')
149
-
150
- run(`node "${binCLI}" init "${projectDir}"`)
151
-
152
- const output = run(`node "${binCLI}" --dir "${projectDir}" run scan`)
153
-
154
- assert(!output.includes('平台模式 — 写入路径约束'), '非 platform 模式不含平台指令')
155
- assert(!output.includes('{DOCS_ROOT}'), '{DOCS_ROOT} 被正确替换')
156
- assert(!output.includes('undefined'), '输出不含 undefined 路径')
157
- assert(!output.includes('null/.sillyspec'), '输出不含 null 路径')
158
-
159
- cleanup(projectDir)
160
- }
161
-
162
- // ── Test 6: --spec-root 兼容 ──
163
- console.log('\n=== Test 6: --spec-root 兼容 ===')
164
- {
165
- const projectDir = tmpDir('compat-p')
166
- const specDir = tmpDir('compat-s')
167
-
168
- run(`node "${binCLI}" init "${projectDir}" --spec-dir "${specDir}"`)
169
-
170
- const output = run(`node "${binCLI}" --dir "${projectDir}" run scan --spec-root "${specDir}"`)
171
- assert(output.includes('平台模式'), '--spec-root 兼容:仍触发平台模式指令')
172
-
173
- cleanup(projectDir)
174
- cleanup(specDir)
175
- }
176
-
177
- // ── Test 7: progress 使用外部 specDir ──
178
- console.log('\n=== Test 7: progress 使用外部 specDir ===')
179
- {
180
- const { ProgressManager } = await imp(join(root, 'src', 'progress.js'))
181
- const projectDir = tmpDir('progress-p')
182
- const specDir = tmpDir('progress-s')
183
-
184
- const pm = new ProgressManager({ specDir })
185
- await pm.init(projectDir)
186
-
187
- assert(existsSync(join(specDir, '.runtime', 'sillyspec.db')), 'DB 创建在外部 specDir')
188
- assert(!existsSync(join(projectDir, '.sillyspec')), '源码目录不含 .sillyspec')
189
-
190
- await pm.initChange(projectDir, 'test-change')
191
- assert(existsSync(join(specDir, 'changes', 'test-change')), 'changes 创建在外部 specDir')
192
-
193
- const progress = await pm.read(projectDir, 'test-change')
194
- assert(progress !== null, '能从外部 specDir 读取 progress')
195
- assert(progress.currentChange === 'test-change', `currentChange 正确`)
196
-
197
- cleanup(projectDir)
198
- cleanup(specDir)
199
- }
200
-
201
- // ── 汇总 ──
202
- console.log(`\n${'='.repeat(50)}`)
203
- console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
204
- console.log(`${'='.repeat(50)}`)
205
-
206
- if (failed > 0) throw new Error(`${failed} test(s) failed`)
1
+ /**
2
+ * --spec-dir 功能测试
3
+ *
4
+ * 测试点:
5
+ * 1. ProgressManager 外部 specDir 路径正确
6
+ * 2. init 外部 specDir 不污染源码
7
+ * 3. 默认模式不受影响
8
+ * 4. 平台模式 prompt 注入(scan/brainstorm/plan/execute/verify/quick)
9
+ * 5. 非 platform 模式占位符替换(无 undefined/null)
10
+ * 6. --spec-dir 与 --spec-root 兼容
11
+ * 7. progress 使用外部 specDir
12
+ */
13
+
14
+ import { join, resolve, basename, dirname } from 'path'
15
+ import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs'
16
+ import { fileURLToPath, pathToFileURL } from 'url'
17
+ import { execSync } from 'child_process'
18
+ import { tmpdir } from 'os'
19
+
20
+ const __filename = fileURLToPath(import.meta.url)
21
+ const __dirname = dirname(__filename)
22
+ const root = resolve(__dirname, '..')
23
+ const binCLI = join(root, 'bin', 'sillyspec.js')
24
+
25
+ function imp(path) {
26
+ return import(pathToFileURL(path).href)
27
+ }
28
+
29
+ let passed = 0
30
+ let failed = 0
31
+
32
+ function assert(condition, msg) {
33
+ if (condition) {
34
+ console.log(` ✅ PASS: ${msg}`)
35
+ passed++
36
+ } else {
37
+ console.log(` ❌ FAIL: ${msg}`)
38
+ failed++
39
+ }
40
+ }
41
+
42
+ function tmpDir(name) {
43
+ const dir = join(tmpdir(), `spec-dir-test-${name}-${Date.now()}`)
44
+ mkdirSync(dir, { recursive: true })
45
+ return dir
46
+ }
47
+
48
+ function cleanup(dir) {
49
+ try { rmSync(dir, { recursive: true, force: true }) } catch {}
50
+ }
51
+
52
+ function run(cmd) {
53
+ return execSync(cmd, { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] })
54
+ }
55
+
56
+ // ── Test 1: ProgressManager 外部 specDir ──
57
+ console.log('\n=== Test 1: ProgressManager 外部 specDir ===')
58
+ {
59
+ const { ProgressManager } = await imp(join(root, 'src', 'progress.js'))
60
+ const tmp = tmpDir('pm')
61
+ const specDir = join(tmp, 'external-spec')
62
+
63
+ const pm = new ProgressManager({ specDir })
64
+ assert(pm._getSpecDir(tmp) === specDir, `_getSpecDir 返回自定义路径`)
65
+
66
+ // 无自定义 specDir 时,resolveSpecDir 会向上查找 .sillyspec 目录
67
+ // 在测试环境中可能命中上层已有的 .sillyspec,所以只检查返回值是否有效路径
68
+ const pm2 = new ProgressManager()
69
+ const resolved = pm2._getSpecDir(tmp)
70
+ assert(typeof resolved === 'string' && resolved.length > 0 && resolved.endsWith('.sillyspec'),
71
+ `_getSpecDir 无自定义时返回有效 .sillyspec 路径 (got: ${resolved})`)
72
+
73
+ assert(pm._runtimePath(tmp) === join(specDir, '.runtime'), `_runtimePath 基于 specDir`)
74
+ assert(pm._changePath(tmp, 'c') === join(specDir, 'changes', 'c'), `_changePath 基于 specDir`)
75
+
76
+ // 外部 specDir 时 _ensureGitignore 应跳过
77
+ const gitignoreResult = pm._ensureGitignore(tmp)
78
+ assert(gitignoreResult === undefined, `外部 specDir 时 _ensureGitignore 跳过`)
79
+
80
+ cleanup(tmp)
81
+ }
82
+
83
+ // ── Test 2: init 外部 specDir 不污染源码 ──
84
+ console.log('\n=== Test 2: init 外部 specDir 不污染源码 ===')
85
+ {
86
+ const { cmdInit } = await imp(join(root, 'src', 'init.js'))
87
+ const projectDir = tmpDir('project')
88
+ const specDir = tmpDir('spec')
89
+
90
+ await cmdInit(projectDir, { specDir })
91
+
92
+ assert(!existsSync(join(projectDir, '.sillyspec')), '源码目录不含 .sillyspec')
93
+ assert(!existsSync(join(projectDir, '.gitignore')), '外部 specDir 时不创建 .gitignore')
94
+ assert(existsSync(join(specDir, 'projects')), `specDir/projects 存在`)
95
+ assert(existsSync(join(specDir, 'docs')), `specDir/docs 存在`)
96
+ assert(existsSync(join(specDir, '.runtime', 'sillyspec.db')), `specDir/.runtime/sillyspec.db 存在`)
97
+ assert(existsSync(join(specDir, 'workflows')), `specDir/workflows 存在`)
98
+ assert(existsSync(join(projectDir, '.claude')), `源码目录 .claude 存在(工具指令)`)
99
+
100
+ cleanup(projectDir)
101
+ cleanup(specDir)
102
+ }
103
+
104
+ // ── Test 3: 默认模式不受影响 ──
105
+ console.log('\n=== Test 3: 默认模式不受影响 ===')
106
+ {
107
+ const { cmdInit } = await imp(join(root, 'src', 'init.js'))
108
+ const projectDir = tmpDir('default')
109
+
110
+ await cmdInit(projectDir, {})
111
+
112
+ assert(existsSync(join(projectDir, '.sillyspec')), '默认模式创建 .sillyspec 在项目内')
113
+ assert(existsSync(join(projectDir, '.sillyspec', '.runtime', 'sillyspec.db')), '默认模式 DB 在项目内')
114
+ assert(existsSync(join(projectDir, '.gitignore')), '默认模式创建 .gitignore')
115
+
116
+ cleanup(projectDir)
117
+ }
118
+
119
+ // ── Test 4: 平台模式 prompt 注入(多 stage) ──
120
+ console.log('\n=== Test 4: 平台模式 prompt 注入 ===')
121
+ {
122
+ const projectDir = tmpDir('prompt-p')
123
+ const specDir = tmpDir('prompt-s')
124
+
125
+ run(`node "${binCLI}" init "${projectDir}" --spec-dir "${specDir}"`)
126
+
127
+ // execute 阶段会自动创建 worktree,在非 git 环境下会失败,跳过
128
+ const stages = ['scan', 'brainstorm', 'plan', 'verify', 'quick']
129
+ for (const stage of stages) {
130
+ const output = run(`node "${binCLI}" --dir "${projectDir}" --spec-dir "${specDir}" run ${stage} --skip-approval`)
131
+ assert(output.includes('平台模式'), `${stage}: 包含平台模式指令`)
132
+ assert(output.includes(`规范目录(specDir): \`${specDir}\``), `${stage}: 包含正确的 specDir 路径`)
133
+ }
134
+
135
+ // scan 额外检查
136
+ const scanOutput = run(`node "${binCLI}" --dir "${projectDir}" --spec-dir "${specDir}" run scan`)
137
+ assert(scanOutput.includes('严禁写入源码目录'), 'scan: 包含严禁写入源码目录')
138
+ assert(scanOutput.includes('Write 工具失败时,不允许'), 'scan: 包含 Write 工具规则')
139
+ assert(scanOutput.includes('变更目录'), 'scan: 包含变更目录')
140
+
141
+ cleanup(projectDir)
142
+ cleanup(specDir)
143
+ }
144
+
145
+ // ── Test 5: 非 platform 模式占位符替换 ──
146
+ console.log('\n=== Test 5: 非 platform 模式占位符替换 ===')
147
+ {
148
+ const projectDir = tmpDir('noplatform')
149
+
150
+ run(`node "${binCLI}" init "${projectDir}"`)
151
+
152
+ const output = run(`node "${binCLI}" --dir "${projectDir}" run scan`)
153
+
154
+ assert(!output.includes('平台模式 — 写入路径约束'), '非 platform 模式不含平台指令')
155
+ assert(!output.includes('{DOCS_ROOT}'), '{DOCS_ROOT} 被正确替换')
156
+ assert(!output.includes('undefined'), '输出不含 undefined 路径')
157
+ assert(!output.includes('null/.sillyspec'), '输出不含 null 路径')
158
+
159
+ cleanup(projectDir)
160
+ }
161
+
162
+ // ── Test 6: --spec-root 兼容 ──
163
+ console.log('\n=== Test 6: --spec-root 兼容 ===')
164
+ {
165
+ const projectDir = tmpDir('compat-p')
166
+ const specDir = tmpDir('compat-s')
167
+
168
+ run(`node "${binCLI}" init "${projectDir}" --spec-dir "${specDir}"`)
169
+
170
+ const output = run(`node "${binCLI}" --dir "${projectDir}" run scan --spec-root "${specDir}"`)
171
+ assert(output.includes('平台模式'), '--spec-root 兼容:仍触发平台模式指令')
172
+
173
+ cleanup(projectDir)
174
+ cleanup(specDir)
175
+ }
176
+
177
+ // ── Test 7: progress 使用外部 specDir ──
178
+ console.log('\n=== Test 7: progress 使用外部 specDir ===')
179
+ {
180
+ const { ProgressManager } = await imp(join(root, 'src', 'progress.js'))
181
+ const projectDir = tmpDir('progress-p')
182
+ const specDir = tmpDir('progress-s')
183
+
184
+ const pm = new ProgressManager({ specDir })
185
+ await pm.init(projectDir)
186
+
187
+ assert(existsSync(join(specDir, '.runtime', 'sillyspec.db')), 'DB 创建在外部 specDir')
188
+ assert(!existsSync(join(projectDir, '.sillyspec')), '源码目录不含 .sillyspec')
189
+
190
+ await pm.initChange(projectDir, 'test-change')
191
+ assert(existsSync(join(specDir, 'changes', 'test-change')), 'changes 创建在外部 specDir')
192
+
193
+ const progress = await pm.read(projectDir, 'test-change')
194
+ assert(progress !== null, '能从外部 specDir 读取 progress')
195
+ assert(progress.currentChange === 'test-change', `currentChange 正确`)
196
+
197
+ cleanup(projectDir)
198
+ cleanup(specDir)
199
+ }
200
+
201
+ // ── 汇总 ──
202
+ console.log(`\n${'='.repeat(50)}`)
203
+ console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
204
+ console.log(`${'='.repeat(50)}`)
205
+
206
+ if (failed > 0) throw new Error(`${failed} test(s) failed`)