sillyspec 3.8.6 → 3.9.0
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.
- package/dist/steps/brainstorm/01-load-context.md +30 -0
- package/dist/steps/brainstorm/02-reuse-check.md +6 -0
- package/dist/steps/brainstorm/03-prototype-analysis.md +11 -0
- package/dist/steps/brainstorm/04-module-split.md +23 -0
- package/dist/steps/brainstorm/05-dialog-explore.md +8 -0
- package/dist/steps/brainstorm/06-propose-approaches.md +3 -0
- package/dist/steps/brainstorm/07-present-design.md +3 -0
- package/dist/steps/brainstorm/08-write-design.md +21 -0
- package/dist/steps/brainstorm/09-self-review.md +15 -0
- package/dist/steps/brainstorm/10-user-confirm.md +3 -0
- package/dist/steps/brainstorm/11-output-spec.md +7 -0
- package/dist/steps/brainstorm/manifest.yaml +26 -0
- package/dist/steps/execute/01-load-context.md +41 -0
- package/dist/steps/execute/02-scan-conventions.md +47 -0
- package/dist/steps/execute/03-skill-mcp.md +19 -0
- package/dist/steps/execute/04-assign-task.md +22 -0
- package/dist/steps/execute/04b-prompt-template.md +54 -0
- package/dist/steps/execute/05-write-test.md +7 -0
- package/dist/steps/execute/06-write-code.md +8 -0
- package/dist/steps/execute/07-run-test.md +26 -0
- package/dist/steps/execute/08-fix-issues.md +28 -0
- package/dist/steps/execute/09-next-task.md +33 -0
- package/dist/steps/execute/manifest.yaml +28 -0
- package/dist/steps/plan/01-load-context.md +22 -0
- package/dist/steps/plan/02-anchor-confirm.md +1 -0
- package/dist/steps/plan/03-expand-tasks.md +33 -0
- package/dist/steps/plan/04-mark-order.md +15 -0
- package/dist/steps/plan/05-e2e-planning.md +17 -0
- package/dist/steps/plan/06-self-check.md +16 -0
- package/dist/steps/plan/07-save.md +1 -0
- package/dist/steps/plan/manifest.yaml +18 -0
- package/dist/steps/scan/01-env-detect.md +51 -0
- package/dist/steps/scan/02-tech-stack.md +16 -0
- package/dist/steps/scan/03-conventions.md +16 -0
- package/dist/steps/scan/04-structure.md +19 -0
- package/dist/steps/scan/05-quality.md +18 -0
- package/dist/steps/scan/06-complete.md +49 -0
- package/dist/steps/scan/manifest.yaml +16 -0
- package/dist/steps/verify/01-load-specs.md +28 -0
- package/dist/steps/verify/02-check-tasks.md +1 -0
- package/dist/steps/verify/03-check-design.md +6 -0
- package/dist/steps/verify/04-run-tests.md +7 -0
- package/dist/steps/verify/05-e2e-tests.md +27 -0
- package/dist/steps/verify/05b-e2e-fix.md +33 -0
- package/dist/steps/verify/06-code-quality.md +25 -0
- package/dist/steps/verify/07-lint-check.md +27 -0
- package/dist/steps/verify/08-output-report.md +14 -0
- package/dist/steps/verify/manifest.yaml +22 -0
- package/package.json +3 -3
- package/src/index.js +10 -0
- package/src/step.js +543 -0
- package/templates/archive.md +0 -1
- package/templates/brainstorm.md +11 -81
- package/templates/commit.md +0 -2
- package/templates/execute.md +0 -10
- package/templates/explore.md +0 -1
- package/templates/plan.md +0 -11
- package/templates/scan.md +2 -18
- package/templates/verify.md +1 -18
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
|
+
}
|
package/templates/archive.md
CHANGED
|
@@ -109,7 +109,6 @@ git log --oneline --no-merges --since="<创建时间>" -- "*.ts" "*.js" "*.vue"
|
|
|
109
109
|
|
|
110
110
|
- **tasks.md:** 确保所有 checkbox 都已勾选 `[x]`
|
|
111
111
|
- **ROADMAP.md**(如存在):标记对应 Phase 已完成
|
|
112
|
-
- **STATE.md:** 清除当前变更信息,历史记录追加归档完成(含精确到秒的时间戳)
|
|
113
112
|
- **Git 暂存:** `git add .sillyspec/`
|
|
114
113
|
|
|
115
114
|
**工作区模式下:** 如果变更属于某个子项目,cd 到子项目目录执行 git add。工作区根目录无 git 则跳过。
|
package/templates/brainstorm.md
CHANGED
|
@@ -12,32 +12,6 @@
|
|
|
12
12
|
- ❌ 一次性抛出多个问题(必须逐个等待回答)
|
|
13
13
|
- ❌ 用户确认前自行推进到 plan 或任何后续阶段
|
|
14
14
|
|
|
15
|
-
## 状态检查(必须先执行)
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
cat .sillyspec/STATE.md 2>/dev/null
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
- phase 为 `brainstorm` 或无 STATE.md → ✅ 继续
|
|
22
|
-
- 其他 phase → 提示用户当前阶段,建议先完成
|
|
23
|
-
|
|
24
|
-
## 进度恢复检查(必须先执行)
|
|
25
|
-
|
|
26
|
-
1. 读取 `.sillyspec/.runtime/progress.json`(使用 read 工具)
|
|
27
|
-
2. 如果文件存在且 `currentStage` 为 `brainstorm`:
|
|
28
|
-
a. 按 resume-dialog.md 模板向用户展示恢复信息(见下方)
|
|
29
|
-
b. 用户确认后,将 `resumeCount` +1,更新 `lastActiveAt`,写入 progress.json
|
|
30
|
-
c. 将已完成步骤的结论作为「已确认的决策,无需重新讨论」注入上下文
|
|
31
|
-
d. 从 `inProgressStep.id` 继续,**禁止回头重新讨论已完成步骤**
|
|
32
|
-
3. 如果文件不存在或 `currentStage` 不是 `brainstorm`:正常启动
|
|
33
|
-
|
|
34
|
-
### 恢复对话规则
|
|
35
|
-
- 首次恢复(resumeCount < 3):友好欢迎,展示进度条和关键结论
|
|
36
|
-
- 频繁中断(resumeCount >= 3):建议重新开始,但尊重用户选择
|
|
37
|
-
- 长时间中断(距 lastActiveAt > 24h):先回顾上次聊了什么,再问是否继续
|
|
38
|
-
- 恢复时说「欢迎回来」而非「检测到中断」
|
|
39
|
-
- 参考模板:`.sillyspec/.runtime/templates/resume-dialog.md`
|
|
40
|
-
|
|
41
15
|
## 用户想法
|
|
42
16
|
$ARGUMENTS
|
|
43
17
|
|
|
@@ -45,19 +19,17 @@ $ARGUMENTS
|
|
|
45
19
|
|
|
46
20
|
## Checklist(必须按顺序完成,不允许跳步或并行)
|
|
47
21
|
|
|
48
|
-
- [ ] **Step 1** — 加载项目上下文
|
|
49
|
-
- [ ] **Step 2** — 协作与复用检查(同名变更 +
|
|
50
|
-
- [ ] **Step 3** —
|
|
51
|
-
- [ ] **Step 4** — 评估需求范围,复杂需求拆分子项目/阶段,生成 MASTER.md
|
|
52
|
-
- [ ] **Step 5** — 对话式探索(一次一个问题,2-3
|
|
53
|
-
- [ ] **Step 6** — 提出 2-3 个方案并推荐
|
|
54
|
-
- [ ] **Step 7** — 分段展示设计,逐段确认
|
|
55
|
-
- [ ] **Step 8** — 写设计文档并保存
|
|
56
|
-
- [ ] **Step 9** — AI
|
|
57
|
-
- [ ] **Step 10** — 用户确认设计方案
|
|
58
|
-
- [ ] **Step 11** — 输出 design.md
|
|
59
|
-
- [ ] **Step 12** — 更新 STATE.md → 保存进度
|
|
60
|
-
- [ ] **Step 13** — 保存最终进度
|
|
22
|
+
- [ ] **Step 1** — 加载项目上下文
|
|
23
|
+
- [ ] **Step 2** — 协作与复用检查(同名变更 + 全局模板)
|
|
24
|
+
- [ ] **Step 3** — 原型/设计图分析(如有)
|
|
25
|
+
- [ ] **Step 4** — 评估需求范围,复杂需求拆分子项目/阶段,生成 MASTER.md
|
|
26
|
+
- [ ] **Step 5** — 对话式探索(一次一个问题,2-3 轮内完成)
|
|
27
|
+
- [ ] **Step 6** — 提出 2-3 个方案并推荐
|
|
28
|
+
- [ ] **Step 7** — 分段展示设计,逐段确认
|
|
29
|
+
- [ ] **Step 8** — 写设计文档并保存
|
|
30
|
+
- [ ] **Step 9** — AI 自审(对照约束检查)
|
|
31
|
+
- [ ] **Step 10** — 用户确认设计方案
|
|
32
|
+
- [ ] **Step 11** — 输出 design.md
|
|
61
33
|
|
|
62
34
|
**终态:** brainstorm 完成后唯一出口是 `/sillyspec:plan`。不允许直接进入 execute 或任何代码操作。
|
|
63
35
|
|
|
@@ -191,50 +163,8 @@ git add .sillyspec/changes/<变更名>/MASTER.md
|
|
|
191
163
|
|
|
192
164
|
用户确认后,确认 design.md 已包含完整内容(动机、需求、方案、文件变更、代码风格参照)。如 Step 8 已保存则无需重复。
|
|
193
165
|
|
|
194
|
-
### Step 12: 更新 STATE.md
|
|
195
|
-
|
|
196
|
-
自动更新 `.sillyspec/STATE.md`(不存在则创建):当前变更、阶段、下一步 `/sillyspec:plan`、关键决策、历史记录。不需要 Git 提交。
|
|
197
|
-
|
|
198
|
-
### Step 13: 保存最终进度
|
|
199
|
-
|
|
200
|
-
1. 更新 `.sillyspec/.runtime/progress.json`:
|
|
201
|
-
- `stages.brainstorm.status` 设为 `completed`
|
|
202
|
-
- 写入 `stageSummary`(2-3 句总结核心设计决策)
|
|
203
|
-
- `currentStage` 更新为 `plan`
|
|
204
|
-
- `checkpoint` 设为「brainstorm 完成,等待 plan」
|
|
205
|
-
2. Append `user-inputs.md` 最终记录
|
|
206
|
-
3. 告知用户:「brainstorm 完成 ✅ 进度已保存,下一步 /sillyspec:plan」
|
|
207
|
-
|
|
208
166
|
## 关键原则
|
|
209
167
|
- YAGNI — 无情砍掉不需要的功能
|
|
210
168
|
- 总是探索替代方案
|
|
211
169
|
- 设计可以很短,但必须存在
|
|
212
170
|
- "简单"的项目更需要设计——未检视的假设造成最大浪费
|
|
213
|
-
|
|
214
|
-
## 进度保存规则(⚠️ HARD-GATE)
|
|
215
|
-
|
|
216
|
-
**每步完成后必须执行,不允许跳过:**
|
|
217
|
-
|
|
218
|
-
1. 使用 write 工具更新 `.sillyspec/.runtime/progress.json`
|
|
219
|
-
2. 将当前步骤 ID 加入 `completedSteps`
|
|
220
|
-
3. 在 `summaries` 中写入本步结构化摘要:
|
|
221
|
-
- `conclusion`(1-2句核心结论,必填)
|
|
222
|
-
- `decisions`(用户确认的决策列表)
|
|
223
|
-
- `rejectedAlternatives`(被否方案 + 简要理由)
|
|
224
|
-
- `userMessages`(用户影响决策的原话)
|
|
225
|
-
- `openQuestions`(遗留问题)
|
|
226
|
-
- `keyEntities`(涉及的关键实体/概念)
|
|
227
|
-
4. 更新 `checkpoint`(一句话描述)
|
|
228
|
-
5. 刷新 `lastActiveAt`(ISO 格式)
|
|
229
|
-
6. `_version` +1
|
|
230
|
-
7. Append `.sillyspec/.runtime/user-inputs.md`(格式见 progress-format.md)
|
|
231
|
-
8. 对用户说:「✅ 第X步完成,进度已保存。」
|
|
232
|
-
|
|
233
|
-
**下步启动检查:** 进入下一步前,先确认 progress.json 的 currentStep 已更新。如果发现未保存,立即补保存。
|
|
234
|
-
|
|
235
|
-
## 多任务并行提醒
|
|
236
|
-
|
|
237
|
-
如果用户在 brainstorm 过程中临时要求执行 quick 任务:
|
|
238
|
-
- quick 任务不写入 progress.json,不影响当前 brainstorm 状态
|
|
239
|
-
- quick 完成后,brainstorm 从断点继续
|
|
240
|
-
- 告知用户:「quick 任务会独立执行,brainstorm 进度不受影响」
|
package/templates/commit.md
CHANGED
|
@@ -54,10 +54,8 @@ cat "$LATEST/tasks.md" 2>/dev/null
|
|
|
54
54
|
```
|
|
55
55
|
筛选时间戳 > LAST_COMMIT_TIME 的已勾选 task。
|
|
56
56
|
|
|
57
|
-
**来源 C — 阶段产出(scan/brainstorm/propose/plan/archive 等):**
|
|
58
57
|
检查 `.sillyspec/` 下新增或修改的文件,根据路径识别来源阶段:
|
|
59
58
|
- `docs/*/scan/*.md` → scan 产出
|
|
60
|
-
- `changes/<name>/proposal.md` → propose 产出
|
|
61
59
|
- `changes/<name>/design.md` 或 `tasks.md` → plan 产出
|
|
62
60
|
- `changes/archive/` → archive 产出
|
|
63
61
|
- `specs/*.md` → brainstorm 产出
|