sillyspec 3.20.1 → 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 (209) 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 +2 -8
  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 -685
  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 -2920
  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 -468
  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/.npmrc-tmp +0 -1
  131. package/CLAUDE.md +0 -18
  132. package/docs/brainstorm-plan-contract.md +0 -64
  133. package/docs/plan-execute-contract.md +0 -123
  134. package/docs/platform-scan-protocol.md +0 -298
  135. package/docs/revision-mode.md +0 -115
  136. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +0 -99
  137. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +0 -218
  138. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +0 -167
  139. package/docs/sillyspec/file-lifecycle/storage-and-state.md +0 -148
  140. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +0 -193
  141. package/docs/sillyspec/file-lifecycle.md +0 -125
  142. package/docs/workflow-contract-regression.md +0 -106
  143. package/docs/worktree-isolation.md +0 -252
  144. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +0 -1
  145. package/packages/dashboard/dist/prototype-dashboard.html +0 -836
  146. package/packages/dashboard/dist/prototype-overview.html +0 -256
  147. package/packages/dashboard/public/prototype-dashboard.html +0 -836
  148. package/packages/dashboard/public/prototype-overview.html +0 -256
  149. package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
  150. package/packages/dashboard/src/components/ProjectCard.vue +0 -187
  151. package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
  152. package/packages/dashboard/src/composables/useLayout.js +0 -131
  153. package/src/brainstorm-postcheck.js +0 -158
  154. package/src/change-list.js +0 -52
  155. package/src/change-risk-profile.js +0 -352
  156. package/src/classify-change.js +0 -73
  157. package/src/constants.js +0 -70
  158. package/src/contract-matrix.js +0 -278
  159. package/src/db.js +0 -201
  160. package/src/endpoint-extractor.js +0 -315
  161. package/src/hooks/claude-pre-tool-use.cjs +0 -125
  162. package/src/hooks/worktree-guard.js +0 -761
  163. package/src/knowledge-match.js +0 -130
  164. package/src/modules.js +0 -482
  165. package/src/scan-postcheck.js +0 -380
  166. package/src/stage-contract.js +0 -700
  167. package/src/stages/brainstorm-auto.js +0 -229
  168. package/src/stages/explore.js +0 -34
  169. package/src/stages/knowledge.js +0 -498
  170. package/src/stages/plan-postcheck.js +0 -513
  171. package/src/sync.js +0 -497
  172. package/src/task-review.js +0 -346
  173. package/src/workflow.js +0 -785
  174. package/src/worktree-apply.js +0 -549
  175. package/src/worktree.js +0 -932
  176. package/templates/workflows/archive-impact.yaml +0 -79
  177. package/templates/workflows/scan-docs.yaml +0 -132
  178. package/test/brainstorm-plan-contract.test.mjs +0 -273
  179. package/test/check-syntax.mjs +0 -26
  180. package/test/cli-top-level-aliases.test.mjs +0 -174
  181. package/test/contract-artifacts.test.mjs +0 -323
  182. package/test/decision-supersede.test.mjs +0 -277
  183. package/test/knowledge-match.test.mjs +0 -231
  184. package/test/plan-execute-contract.test.mjs +0 -330
  185. package/test/plan-optimization.test.mjs +0 -572
  186. package/test/platform-artifacts.test.mjs +0 -190
  187. package/test/platform-failure-samples.test.mjs +0 -199
  188. package/test/platform-recovery-chain.test.mjs +0 -179
  189. package/test/platform-recovery.test.mjs +0 -167
  190. package/test/platform-scan-p0.test.mjs +0 -175
  191. package/test/revision-v1.test.mjs +0 -1145
  192. package/test/run-sanitize-project-name.test.mjs +0 -51
  193. package/test/run-scan-postcheck-fail.test.mjs +0 -64
  194. package/test/run-scan-project-parse.test.mjs +0 -77
  195. package/test/run-tests.mjs +0 -48
  196. package/test/scan-docs-yaml-placeholders.test.mjs +0 -84
  197. package/test/scan-knowledge.test.mjs +0 -175
  198. package/test/scan-paths.test.mjs +0 -68
  199. package/test/scan-postcheck-project-priority.test.mjs +0 -85
  200. package/test/scan-postcheck.test.mjs +0 -183
  201. package/test/scan-workflow-anyfailed-block.test.mjs +0 -52
  202. package/test/spec-dir.test.mjs +0 -206
  203. package/test/stage-contract-failed-post-check.test.mjs +0 -102
  204. package/test/stage-contract.test.mjs +0 -299
  205. package/test/stage-definitions.test.mjs +0 -39
  206. package/test/wait-gates.test.mjs +0 -501
  207. package/test/workflow-spec-base.test.mjs +0 -142
  208. package/test/worktree-guard.test.mjs +0 -136
  209. package/test/worktree-native-overlay.test.mjs +0 -188
package/src/step.js ADDED
@@ -0,0 +1,543 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync, statSync, rmSync } from 'fs';
2
+ import { join, resolve } from 'path';
3
+ import { randomBytes } from 'crypto';
4
+
5
+ // ── 简易 YAML 工具 ──
6
+
7
+ function parseYaml(text) {
8
+ const result = {};
9
+ for (const line of text.split('\n')) {
10
+ const trimmed = line.trimStart();
11
+ if (!trimmed || trimmed.startsWith('#')) continue;
12
+ const m = trimmed.match(/^(\w[\w-]*):\s*(.*)/);
13
+ if (m) {
14
+ const key = m[1];
15
+ const val = m[2].trim();
16
+ if (val === '' || val === '[]') result[key] = val === '[]' ? [] : '';
17
+ else if (val.startsWith('[')) result[key] = val.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
18
+ else if (/^\d+$/.test(val)) result[key] = parseInt(val, 10);
19
+ else result[key] = val;
20
+ }
21
+ }
22
+ return result;
23
+ }
24
+
25
+ function parseManifest(text) {
26
+ const lines = text.split('\n');
27
+ const result = { phase: '', description: '', requires: [], steps: [] };
28
+ let inSteps = false;
29
+ let currentStep = null;
30
+
31
+ for (const line of lines) {
32
+ const trimmed = line.trimStart();
33
+ if (!trimmed || trimmed.startsWith('#')) continue;
34
+
35
+ if (inSteps && trimmed.startsWith('- file:')) {
36
+ if (currentStep) result.steps.push(currentStep);
37
+ currentStep = { file: '', name: '' };
38
+ currentStep.file = trimmed.match(/^- file:\s*(.+)/)?.[1]?.trim() || '';
39
+ continue;
40
+ }
41
+ if (inSteps && currentStep && trimmed.startsWith('name:')) {
42
+ currentStep.name = trimmed.match(/^name:\s*(.+)/)?.[1]?.trim() || '';
43
+ continue;
44
+ }
45
+ if (inSteps && !trimmed.startsWith('-') && !trimmed.startsWith(' ')) {
46
+ if (currentStep) result.steps.push(currentStep);
47
+ currentStep = null;
48
+ inSteps = false;
49
+ }
50
+
51
+ if (trimmed.startsWith('phase:')) result.phase = trimmed.match(/^phase:\s*(.+)/)?.[1]?.trim() || '';
52
+ else if (trimmed.startsWith('description:')) result.description = trimmed.match(/^description:\s*(.+)/)?.[1]?.trim() || '';
53
+ else if (trimmed.startsWith('requires:')) {
54
+ const m = trimmed.match(/^requires:\s*\[?(.*?)\]?$/);
55
+ result.requires = m ? m[1].split(',').map(s => s.trim()).filter(Boolean) : [];
56
+ }
57
+ else if (trimmed.startsWith('steps:')) inSteps = true;
58
+ }
59
+ if (currentStep) result.steps.push(currentStep);
60
+ return result;
61
+ }
62
+
63
+ function toYaml(obj) {
64
+ return Object.entries(obj).map(([k, v]) => {
65
+ if (Array.isArray(v)) return `${k}: []`;
66
+ return `${k}: ${v}`;
67
+ }).join('\n') + '\n';
68
+ }
69
+
70
+ // ── 文件操作 ──
71
+
72
+ function atomicWrite(filePath, content) {
73
+ const tmpPath = filePath + '.tmp.' + process.pid;
74
+ writeFileSync(tmpPath, content, 'utf8');
75
+ writeFileSync(filePath, content, 'utf8');
76
+ try { unlinkSync(tmpPath); } catch {}
77
+ }
78
+
79
+ function findSillyspecDir(dir) {
80
+ const p = join(dir, '.sillyspec');
81
+ if (existsSync(p)) return p;
82
+ return null;
83
+ }
84
+
85
+ function getAvailablePhases(sillyDir) {
86
+ const stepsDir = join(sillyDir, 'steps');
87
+ if (!existsSync(stepsDir)) return [];
88
+ return readdirSync(stepsDir, { withFileTypes: true })
89
+ .filter(d => d.isDirectory() && existsSync(join(stepsDir, d.name, 'manifest.yaml')))
90
+ .map(d => d.name);
91
+ }
92
+
93
+ // ── 会话管理 ──
94
+
95
+ function getSessionId(dir, explicitId) {
96
+ if (explicitId) return explicitId;
97
+ if (process.env.SILLYSPEC_SESSION) return process.env.SILLYSPEC_SESSION;
98
+ const markerFile = join(dir, '.sillyspec', '.session');
99
+ if (existsSync(markerFile)) return readFileSync(markerFile, 'utf8').trim();
100
+ // 生成新 ID 并原子写入
101
+ const id = randomBytes(4).toString('hex');
102
+ const sillyDir = join(dir, '.sillyspec');
103
+ try {
104
+ writeFileSync(markerFile, id + '\n', { flag: 'wx' });
105
+ } catch {
106
+ // 已存在,读取
107
+ return readFileSync(markerFile, 'utf8').trim();
108
+ }
109
+ return id;
110
+ }
111
+
112
+ function getProgress(sillyDir, sessionId) {
113
+ const pFile = join(sillyDir, 'sessions', sessionId, 'progress.yaml');
114
+ if (!existsSync(pFile)) return {};
115
+ return parseYaml(readFileSync(pFile, 'utf8'));
116
+ }
117
+
118
+ function saveProgress(sillyDir, sessionId, progress) {
119
+ const pDir = join(sillyDir, 'sessions', sessionId);
120
+ mkdirSync(pDir, { recursive: true });
121
+ atomicWrite(join(pDir, 'progress.yaml'), toYaml(progress));
122
+ }
123
+
124
+ function updateLastActive(sillyDir, sessionId) {
125
+ const pDir = join(sillyDir, 'sessions', sessionId);
126
+ mkdirSync(pDir, { recursive: true });
127
+ atomicWrite(join(pDir, 'last-active'), Date.now().toString());
128
+ }
129
+
130
+ function getLastActive(sillyDir, sessionId) {
131
+ const f = join(sillyDir, 'sessions', sessionId, 'last-active');
132
+ if (!existsSync(f)) return null;
133
+ try { return parseInt(readFileSync(f, 'utf8').trim(), 10); } catch { return null; }
134
+ }
135
+
136
+ // ── 时间格式化 ──
137
+
138
+ function timeAgo(ts) {
139
+ if (!ts) return '未知';
140
+ const diff = Date.now() - ts;
141
+ const mins = Math.floor(diff / 60000);
142
+ if (mins < 60) return `${mins}m ago`;
143
+ const hours = Math.floor(mins / 60);
144
+ if (hours < 24) return `${hours}h ago`;
145
+ const days = Math.floor(hours / 24);
146
+ return `${days}d ago`;
147
+ }
148
+
149
+ // ── 核心逻辑 ──
150
+
151
+ function getManifest(sillyDir, phase) {
152
+ const mFile = join(sillyDir, 'steps', phase, 'manifest.yaml');
153
+ if (!existsSync(mFile)) return null;
154
+ return parseManifest(readFileSync(mFile, 'utf8'));
155
+ }
156
+
157
+ function checkPhaseComplete(sillyDir, phase, progress) {
158
+ const m = getManifest(sillyDir, phase);
159
+ if (!m) return false;
160
+ const p = parseInt(progress[phase], 10) || 0;
161
+ return p >= m.steps.length;
162
+ }
163
+
164
+ function formatNextOutput(manifest, stepNum, stepFile) {
165
+ return `---\nphase: ${manifest.phase}\nstep: ${stepNum}/${manifest.steps.length}\nname: ${manifest.steps[stepNum - 1].name}\n---\n\n${stepFile}\n\n---\n完成后请执行: sillyspec step ${manifest.phase} --next\n`;
166
+ }
167
+
168
+ function handleNext(dir, sillyDir, phase, sessionId, json) {
169
+ const manifest = getManifest(sillyDir, phase);
170
+ if (!manifest) {
171
+ return { ok: false, error: `❌ 阶段 "${phase}" 不存在`, available: getAvailablePhases(sillyDir) };
172
+ }
173
+
174
+ // 前置依赖提醒
175
+ for (const req of manifest.requires) {
176
+ const progress = getProgress(sillyDir, sessionId);
177
+ if (!checkPhaseComplete(sillyDir, req, progress)) {
178
+ console.log(`⚠️ 建议先完成 ${req} 阶段`);
179
+ }
180
+ }
181
+
182
+ const progress = getProgress(sillyDir, sessionId);
183
+ let p = parseInt(progress[phase], 10);
184
+ if (isNaN(p) || p < 0) {
185
+ console.log(`⚠️ progress 值非法,已重置为 0`);
186
+ p = 0;
187
+ progress[phase] = 0;
188
+ saveProgress(sillyDir, sessionId, progress);
189
+ }
190
+
191
+ const total = manifest.steps.length;
192
+
193
+ if (p >= total) {
194
+ if (json) return { ok: true, done: true, phase, total };
195
+ console.log(`DONE ✅`);
196
+ return { ok: true, done: true, phase, total };
197
+ }
198
+
199
+ // 确定要分发的步骤
200
+ let nextStep;
201
+ if (p === 0) {
202
+ nextStep = 1;
203
+ } else {
204
+ nextStep = p + 1;
205
+ }
206
+
207
+ if (nextStep > total) {
208
+ if (json) return { ok: true, done: true, phase, total };
209
+ console.log(`DONE ✅`);
210
+ return { ok: true, done: true, phase, total };
211
+ }
212
+
213
+ // 标记进度
214
+ progress[phase] = nextStep;
215
+ saveProgress(sillyDir, sessionId, progress);
216
+ updateLastActive(sillyDir, sessionId);
217
+
218
+ // 读取步骤文件
219
+ const stepFile = join(sillyDir, 'steps', phase, manifest.steps[nextStep - 1].file);
220
+ if (!existsSync(stepFile)) {
221
+ return { ok: false, error: `❌ 步骤文件不存在: ${manifest.steps[nextStep - 1].file}` };
222
+ }
223
+ const content = readFileSync(stepFile, 'utf8');
224
+
225
+ if (json) {
226
+ return { ok: true, done: false, phase, step: nextStep, total, name: manifest.steps[nextStep - 1].name, content };
227
+ }
228
+
229
+ console.log(formatNextOutput(manifest, nextStep, content));
230
+ return { ok: true, done: false, phase, step: nextStep, total, name: manifest.steps[nextStep - 1].name };
231
+ }
232
+
233
+ function handleJump(dir, sillyDir, phase, target, sessionId, json) {
234
+ const manifest = getManifest(sillyDir, phase);
235
+ if (!manifest) {
236
+ return { ok: false, error: `❌ 阶段 "${phase}" 不存在`, available: getAvailablePhases(sillyDir) };
237
+ }
238
+
239
+ const total = manifest.steps.length;
240
+ if (target < 1 || target > total) {
241
+ return { ok: false, error: `❌ 步骤 ${target} 越界(有效范围: 1-${total})` };
242
+ }
243
+
244
+ const progress = getProgress(sillyDir, sessionId);
245
+ progress[phase] = target;
246
+ saveProgress(sillyDir, sessionId, progress);
247
+ updateLastActive(sillyDir, sessionId);
248
+
249
+ const stepFile = join(sillyDir, 'steps', phase, manifest.steps[target - 1].file);
250
+ if (!existsSync(stepFile)) {
251
+ return { ok: false, error: `❌ 步骤文件不存在: ${manifest.steps[target - 1].file}` };
252
+ }
253
+ const content = readFileSync(stepFile, 'utf8');
254
+
255
+ if (json) {
256
+ return { ok: true, phase, step: target, total, name: manifest.steps[target - 1].name, content };
257
+ }
258
+
259
+ console.log(formatNextOutput(manifest, target, content));
260
+ return { ok: true, phase, step: target, total, name: manifest.steps[target - 1].name };
261
+ }
262
+
263
+ function handleStatus(sillyDir, sessionId, json) {
264
+ const phases = getAvailablePhases(sillyDir);
265
+ const progress = getProgress(sillyDir, sessionId);
266
+ const results = [];
267
+
268
+ for (const phase of phases) {
269
+ const manifest = getManifest(sillyDir, phase);
270
+ const p = parseInt(progress[phase], 10) || 0;
271
+ const total = manifest ? manifest.steps.length : 0;
272
+ const name = manifest ? (manifest.steps[Math.max(0, p - 1)]?.name || '') : '';
273
+ results.push({ phase, progress: p, total, name });
274
+ }
275
+
276
+ if (json) return { ok: true, phases: results };
277
+
278
+ for (const r of results) {
279
+ if (r.total === 0) {
280
+ console.log(`${r.phase}: 未开始`);
281
+ } else if (r.progress === 0) {
282
+ console.log(`${r.phase}: 未开始`);
283
+ } else if (r.progress >= r.total) {
284
+ console.log(`${r.phase}: 已完成 ✅`);
285
+ } else {
286
+ console.log(`${r.phase}: 步骤 ${r.progress}/${r.total}(${r.name})`);
287
+ }
288
+ }
289
+ return { ok: true, phases: results };
290
+ }
291
+
292
+ function handleList(sillyDir, phase, sessionId, json) {
293
+ const manifest = getManifest(sillyDir, phase);
294
+ if (!manifest) {
295
+ return { ok: false, error: `❌ 阶段 "${phase}" 不存在`, available: getAvailablePhases(sillyDir) };
296
+ }
297
+
298
+ const progress = getProgress(sillyDir, sessionId);
299
+ const current = parseInt(progress[phase], 10) || 0;
300
+ const results = [];
301
+
302
+ if (!json) console.log(`# ${phase} — ${manifest.description}`);
303
+
304
+ for (let i = 0; i < manifest.steps.length; i++) {
305
+ const num = i + 1;
306
+ const s = manifest.steps[i];
307
+ let status = '';
308
+ if (num < current) status = '✅';
309
+ else if (num === current) status = '← 当前';
310
+ results.push({ step: num, name: s.name, status: num < current ? 'done' : num === current ? 'current' : 'pending' });
311
+
312
+ if (!json) {
313
+ console.log(` ${num}. ${s.name.padEnd(16)}${status}`);
314
+ }
315
+ }
316
+ return { ok: true, phase, description: manifest.description, steps: results };
317
+ }
318
+
319
+ function handleSessions(sillyDir, json) {
320
+ const sessionsDir = join(sillyDir, 'sessions');
321
+ if (!existsSync(sessionsDir)) {
322
+ if (json) return { ok: true, sessions: [] };
323
+ console.log('暂无会话');
324
+ return { ok: true, sessions: [] };
325
+ }
326
+
327
+ const dirs = readdirSync(sessionsDir, { withFileTypes: true })
328
+ .filter(d => d.isDirectory())
329
+ .map(d => d.name);
330
+
331
+ if (dirs.length === 0) {
332
+ if (json) return { ok: true, sessions: [] };
333
+ console.log('暂无会话');
334
+ return { ok: true, sessions: [] };
335
+ }
336
+
337
+ const phases = getAvailablePhases(sillyDir);
338
+ const results = [];
339
+
340
+ for (const sid of dirs) {
341
+ const progress = getProgress(sillyDir, sid);
342
+ const last = getLastActive(sillyDir, sid);
343
+ const phaseSummaries = phases.map(ph => {
344
+ const manifest = getManifest(sillyDir, ph);
345
+ const p = parseInt(progress[ph], 10) || 0;
346
+ const total = manifest ? manifest.steps.length : 0;
347
+ return { phase: ph, progress: p, total };
348
+ });
349
+ const allDone = phaseSummaries.every(ps => ps.total > 0 && ps.progress >= ps.total);
350
+ results.push({ sessionId: sid, phases: phaseSummaries, lastActive: last, allDone });
351
+ }
352
+
353
+ if (json) return { ok: true, sessions: results };
354
+
355
+ for (const r of results) {
356
+ const phaseStr = r.phases.map(ps => `${ps.phase} ${ps.progress}/${ps.total}`).join(' ');
357
+ const done = r.allDone ? ' ✅' : '';
358
+ console.log(`${r.sessionId} ${phaseStr} (last: ${timeAgo(r.lastActive)})${done}`);
359
+ }
360
+ return { ok: true, sessions: results };
361
+ }
362
+
363
+ function handleClean(sillyDir, options, json) {
364
+ const { allDone, olderThan, sessionId: targetSession } = options;
365
+ const sessionsDir = join(sillyDir, 'sessions');
366
+ if (!existsSync(sessionsDir)) {
367
+ if (json) return { ok: true, cleaned: [], dryRun: !targetSession && !allDone && !olderThan };
368
+ console.log('暂无会话');
369
+ return { ok: true, cleaned: [], dryRun: true };
370
+ }
371
+
372
+ const dirs = readdirSync(sessionsDir, { withFileTypes: true })
373
+ .filter(d => d.isDirectory())
374
+ .map(d => d.name);
375
+
376
+ const phases = getAvailablePhases(sillyDir);
377
+ const toClean = [];
378
+
379
+ for (const sid of dirs) {
380
+ const progress = getProgress(sillyDir, sid);
381
+ const last = getLastActive(sillyDir, sid);
382
+ const isAllDone = phases.every(ph => {
383
+ const manifest = getManifest(sillyDir, ph);
384
+ const p = parseInt(progress[ph], 10) || 0;
385
+ return manifest && p >= manifest.steps.length;
386
+ });
387
+ const ageMs = last ? Date.now() - last : Infinity;
388
+ const ageDays = ageMs / (1000 * 60 * 60 * 24);
389
+
390
+ let match = false;
391
+ if (targetSession) match = sid === targetSession;
392
+ else if (allDone) match = true;
393
+ else if (olderThan !== null) match = ageDays >= olderThan;
394
+
395
+ toClean.push({ sessionId: sid, allDone: isAllDone, lastActive: last, ageDays, match });
396
+ }
397
+
398
+ // 无参数 = dry-run
399
+ const isDryRun = !targetSession && !allDone && olderThan === null;
400
+
401
+ if (isDryRun) {
402
+ if (json) return { ok: true, dryRun: true, sessions: toClean.map(s => ({ sessionId: s.sessionId, allDone: s.allDone, lastActive: s.lastActive, ageDays: Math.floor(s.ageDays) })) };
403
+ console.log('📋 会话列表(dry-run,未执行清理):');
404
+ console.log('');
405
+ for (const s of toClean) {
406
+ const status = s.allDone ? '✅ 全部完成' : `进行中(${Math.floor(s.ageDays)}天前活跃)`;
407
+ console.log(` ${s.sessionId} ${status} (last: ${timeAgo(s.lastActive)})`);
408
+ }
409
+ return { ok: true, dryRun: true, sessions: toClean };
410
+ }
411
+
412
+ // 执行清理
413
+ const cleaned = toClean.filter(s => s.match);
414
+ if (cleaned.length === 0) {
415
+ if (json) return { ok: true, cleaned: [] };
416
+ console.log('没有匹配的会话需要清理');
417
+ return { ok: true, cleaned: [] };
418
+ }
419
+
420
+ for (const s of cleaned) {
421
+ rmSync(join(sessionsDir, s.sessionId), { recursive: true });
422
+ }
423
+
424
+ if (json) return { ok: true, cleaned: cleaned.map(s => s.sessionId) };
425
+ console.log(`🗑️ 已清理 ${cleaned.length} 个会话:`);
426
+ for (const s of cleaned) console.log(` - ${s.sessionId}`);
427
+ return { ok: true, cleaned: cleaned.map(s => s.sessionId) };
428
+ }
429
+
430
+ // ── 主入口 ──
431
+
432
+ export async function cmdStep(dir, args = []) {
433
+ const sillyDir = findSillyspecDir(dir);
434
+ if (!sillyDir) {
435
+ console.log('❌ 未找到 .sillyspec/ 目录,请先执行 sillyspec init');
436
+ process.exit(1);
437
+ }
438
+
439
+ // 解析参数
440
+ let json = false;
441
+ let sessionId = null;
442
+ let action = null;
443
+ let phase = null;
444
+ let jumpTarget = null;
445
+ let cleanAllDone = false;
446
+ let cleanOlderThan = null;
447
+ let cleanSession = null;
448
+
449
+ for (let i = 0; i < args.length; i++) {
450
+ const a = args[i];
451
+ if (a === '--json') json = true;
452
+ else if (a === '--session' && args[i + 1]) { sessionId = args[++i]; }
453
+ else if (a === '--next') action = 'next';
454
+ else if (a === '--status') action = 'status';
455
+ else if (a === '--list') action = 'list';
456
+ else if (a === '--jump' && args[i + 1]) { action = 'jump'; jumpTarget = parseInt(args[++i], 10); }
457
+ else if (a === '--sessions') action = 'sessions';
458
+ else if (a === '--clean') {
459
+ action = 'clean';
460
+ // 检查后续选项
461
+ if (args[i + 1] === '--all-done') { cleanAllDone = true; i++; }
462
+ else if (args[i + 1] === '--session' && args[i + 2]) { cleanSession = args[i + 2]; i += 2; }
463
+ else if (args[i + 1] === '--older-than' && args[i + 2]) { cleanOlderThan = parseFloat(args[i + 2]); i += 2; }
464
+ }
465
+ else if (!a.startsWith('-')) {
466
+ if (!phase) phase = a;
467
+ }
468
+ }
469
+
470
+ const sid = getSessionId(dir, sessionId);
471
+
472
+ // 如果 phase 提供但步骤目录不存在
473
+ if (phase && action && action !== 'sessions' && action !== 'clean') {
474
+ const stepsDir = join(sillyDir, 'steps');
475
+ if (!existsSync(stepsDir) || !existsSync(join(stepsDir, phase, 'manifest.yaml'))) {
476
+ const available = getAvailablePhases(sillyDir);
477
+ if (json) {
478
+ console.log(JSON.stringify({ ok: false, error: `阶段 "${phase}" 不存在`, available }));
479
+ process.exit(1);
480
+ }
481
+ console.log(`❌ 阶段 "${phase}" 不存在`);
482
+ if (available.length > 0) {
483
+ console.log(`可用阶段: ${available.join(', ')}`);
484
+ } else {
485
+ console.log('暂无可用阶段(.sillyspec/steps/ 下无 manifest.yaml)');
486
+ }
487
+ process.exit(1);
488
+ }
489
+ }
490
+
491
+ let result;
492
+ switch (action) {
493
+ case 'next':
494
+ result = handleNext(dir, sillyDir, phase, sid, json);
495
+ break;
496
+ case 'jump':
497
+ result = handleJump(dir, sillyDir, phase, jumpTarget, sid, json);
498
+ break;
499
+ case 'status':
500
+ result = handleStatus(sillyDir, sid, json);
501
+ break;
502
+ case 'list':
503
+ result = handleList(sillyDir, phase, sid, json);
504
+ break;
505
+ case 'sessions':
506
+ result = handleSessions(sillyDir, json);
507
+ break;
508
+ case 'clean':
509
+ result = handleClean(sillyDir, { allDone: cleanAllDone, olderThan: cleanOlderThan, sessionId: cleanSession }, json);
510
+ break;
511
+ default:
512
+ console.log('❌ 请指定操作: --next, --status, --list, --jump <n>, --sessions, --clean');
513
+ console.log('');
514
+ console.log('用法:');
515
+ console.log(' sillyspec step <phase> --next 获取步骤 + 自动推进');
516
+ console.log(' sillyspec step <phase> --status 查看进度');
517
+ console.log(' sillyspec step <phase> --list 查看步骤列表');
518
+ console.log(' sillyspec step <phase> --jump <n> 跳到指定步骤');
519
+ console.log(' sillyspec step --sessions 查看所有会话');
520
+ console.log(' sillyspec step --clean dry-run 列出可清理会话');
521
+ console.log(' sillyspec step --clean --all-done 清理所有阶段完成的会话');
522
+ console.log(' sillyspec step --clean --session <id> 清理指定会话');
523
+ console.log(' sillyspec step --clean --older-than <days> 清理 N 天前的会话');
524
+ console.log('');
525
+ console.log('选项:');
526
+ console.log(' --session <id> 指定会话 ID');
527
+ console.log(' --json 输出 JSON');
528
+ process.exit(1);
529
+ }
530
+
531
+ if (result && !result.ok) {
532
+ if (json) {
533
+ console.log(JSON.stringify(result));
534
+ } else if (result.error) {
535
+ console.log(result.error);
536
+ }
537
+ process.exit(1);
538
+ }
539
+
540
+ if (json && result) {
541
+ console.log(JSON.stringify(result));
542
+ }
543
+ }