sillyspec 3.16.2 → 3.17.1
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/package.json +1 -1
- package/src/change-list.js +1 -1
- package/src/index.js +12 -5
- package/src/init.js +47 -36
- package/src/progress.js +21 -4
- package/src/run.js +169 -77
- package/src/scan-postcheck.js +179 -0
- package/src/stage-contract.js +14 -5
- package/src/stages/execute.js +11 -5
- package/src/stages/scan.js +17 -1
- package/src/worktree-apply.js +5 -3
- package/src/worktree.js +5 -3
- package/test/platform-recovery.test.mjs +139 -0
- package/test/scan-postcheck.test.mjs +179 -0
- package/test/spec-dir.test.mjs +200 -0
package/package.json
CHANGED
package/src/change-list.js
CHANGED
|
@@ -40,7 +40,7 @@ export function parseFileChangeList(designMdPath) {
|
|
|
40
40
|
continue
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const filePath = cells[1].trim()
|
|
43
|
+
const filePath = cells[1].trim().replace(/^`|`$/g, '')
|
|
44
44
|
|
|
45
45
|
// 忽略空路径、注释、.sillyspec/ 内的路径
|
|
46
46
|
if (!filePath || filePath === '—' || filePath === '-' || filePath.startsWith('.sillyspec/')) continue
|
package/src/index.js
CHANGED
|
@@ -22,6 +22,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
22
22
|
[--tool <name>] 只安装指定工具
|
|
23
23
|
[--interactive] 交互式引导
|
|
24
24
|
[--dir <path>] 指定目录
|
|
25
|
+
[--spec-dir <path>] 指定规范目录(默认 <项目>/.sillyspec)
|
|
25
26
|
|
|
26
27
|
sillyspec setup [--list] 安装推荐 MCP 工具
|
|
27
28
|
[--list] 查看已安装状态
|
|
@@ -32,7 +33,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
32
33
|
--status 查看阶段进度
|
|
33
34
|
--reset 重置阶段
|
|
34
35
|
--change <name> 设置当前变更名
|
|
35
|
-
--spec-
|
|
36
|
+
--spec-dir <path> 指定规范目录(默认 <项目>/.sillyspec)
|
|
36
37
|
--runtime-root <path> 平台模式:运行时产物根路径
|
|
37
38
|
--workspace-id <id> 平台模式:workspace ID
|
|
38
39
|
--scan-run-id <id> 平台模式:scan run ID
|
|
@@ -70,9 +71,11 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
70
71
|
选项:
|
|
71
72
|
--json 输出 JSON(给 AI 程序化读取)
|
|
72
73
|
--dir <path> 指定项目目录(默认当前目录)
|
|
74
|
+
--spec-dir <path> 指定规范目录(默认 <项目目录>/.sillyspec)
|
|
73
75
|
|
|
74
76
|
示例:
|
|
75
77
|
sillyspec init
|
|
78
|
+
sillyspec init --spec-dir /data/specs/my-project
|
|
76
79
|
sillyspec run scan
|
|
77
80
|
sillyspec run brainstorm
|
|
78
81
|
sillyspec run quick
|
|
@@ -100,6 +103,7 @@ async function main() {
|
|
|
100
103
|
let json = false;
|
|
101
104
|
let saveWorkflowRunFlag = false;
|
|
102
105
|
let targetDir = process.cwd();
|
|
106
|
+
let specDir = null;
|
|
103
107
|
let tool = null;
|
|
104
108
|
let interactive = false;
|
|
105
109
|
const filteredArgs = [];
|
|
@@ -112,6 +116,9 @@ async function main() {
|
|
|
112
116
|
} else if (args[i] === '--dir' && args[i + 1]) {
|
|
113
117
|
targetDir = resolve(args[i + 1]);
|
|
114
118
|
i++;
|
|
119
|
+
} else if (args[i] === '--spec-dir' && args[i + 1]) {
|
|
120
|
+
specDir = resolve(args[i + 1]);
|
|
121
|
+
i++;
|
|
115
122
|
} else if (args[i] === '--tool' && args[i + 1]) {
|
|
116
123
|
tool = args[i + 1];
|
|
117
124
|
i++;
|
|
@@ -144,7 +151,7 @@ async function main() {
|
|
|
144
151
|
|
|
145
152
|
switch (command) {
|
|
146
153
|
case 'init':
|
|
147
|
-
await cmdInit(dir, { tool, interactive });
|
|
154
|
+
await cmdInit(dir, { tool, interactive, specDir });
|
|
148
155
|
break;
|
|
149
156
|
case 'setup':
|
|
150
157
|
const setupList = filteredArgs.includes('--list') || filteredArgs.includes('-l');
|
|
@@ -247,7 +254,7 @@ async function main() {
|
|
|
247
254
|
}
|
|
248
255
|
case 'run': {
|
|
249
256
|
const { runCommand } = await import('./run.js')
|
|
250
|
-
await runCommand(filteredArgs.slice(1), dir)
|
|
257
|
+
await runCommand(filteredArgs.slice(1), dir, specDir)
|
|
251
258
|
break
|
|
252
259
|
}
|
|
253
260
|
case 'dashboard': {
|
|
@@ -278,7 +285,7 @@ async function main() {
|
|
|
278
285
|
const wtSubCmd = filteredArgs[1];
|
|
279
286
|
const wtName = filteredArgs.slice(2).find(a => !a.startsWith('-'));
|
|
280
287
|
const wm = new WorktreeManager({ cwd: dir });
|
|
281
|
-
const pm = new ProgressManager();
|
|
288
|
+
const pm = new ProgressManager({ specDir });
|
|
282
289
|
|
|
283
290
|
// isolation 写入 DB 的辅助函数
|
|
284
291
|
async function _writeIsolationToDB(cwd, changeName, info) {
|
|
@@ -525,7 +532,7 @@ SillySpec platform — SillyHub 平台同步
|
|
|
525
532
|
console.error('❌ 用法: sillyspec change-rename <旧变更名> <新变更名>');
|
|
526
533
|
process.exit(1);
|
|
527
534
|
}
|
|
528
|
-
const pm = new ProgressManager();
|
|
535
|
+
const pm = new ProgressManager({ specDir });
|
|
529
536
|
await pm.renameChange(dir, oldName, newName);
|
|
530
537
|
break;
|
|
531
538
|
}
|
package/src/init.js
CHANGED
|
@@ -103,30 +103,35 @@ function isTTY() {
|
|
|
103
103
|
|
|
104
104
|
// ── 核心安装逻辑 ──
|
|
105
105
|
|
|
106
|
-
async function doInstall(projectDir, tools, subprojects = []) {
|
|
106
|
+
async function doInstall(projectDir, tools, subprojects = [], specDir = null) {
|
|
107
|
+
// specDir: 规范目录(默认 projectDir/.sillyspec)
|
|
108
|
+
// projectDir: 源码项目根目录(用于工具检测、指令注入、.gitignore)
|
|
109
|
+
const spec = specDir || join(projectDir, '.sillyspec');
|
|
110
|
+
|
|
107
111
|
// 创建基础目录
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
+
// spec/projects/ → 项目注册表
|
|
113
|
+
// spec/docs/<name>/ → 统一文档中心
|
|
114
|
+
// spec/knowledge/ → 跨项目共享知识库
|
|
115
|
+
// spec/.runtime/ → progress (gitignored)
|
|
112
116
|
|
|
113
117
|
// 注册当前项目到 projects/
|
|
114
118
|
const projectName = basename(projectDir) || 'project';
|
|
115
|
-
const projectsDir = join(
|
|
119
|
+
const projectsDir = join(spec, 'projects');
|
|
116
120
|
mkdirSync(projectsDir, { recursive: true });
|
|
117
121
|
const projectYamlPath = join(projectsDir, `${projectName}.yaml`);
|
|
118
122
|
if (!existsSync(projectYamlPath)) {
|
|
119
|
-
|
|
123
|
+
// path 相对于 specDir,跨平台可寻址
|
|
124
|
+
writeFileSync(projectYamlPath, `name: ${projectName}\npath: ${projectDir}\nstatus: active\n`);
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
// 创建
|
|
123
|
-
const scanDir = join(
|
|
127
|
+
// 创建 docs/<projectName>/scan/ 子目录(代码扫描结果)
|
|
128
|
+
const scanDir = join(spec, 'docs', projectName, 'scan');
|
|
124
129
|
mkdirSync(scanDir, { recursive: true });
|
|
125
130
|
const gitkeepPath = join(scanDir, '.gitkeep');
|
|
126
131
|
if (!existsSync(gitkeepPath)) writeFileSync(gitkeepPath, '');
|
|
127
132
|
|
|
128
|
-
// 复制 workflow 模板到
|
|
129
|
-
const workflowsDir = join(
|
|
133
|
+
// 复制 workflow 模板到 workflows/
|
|
134
|
+
const workflowsDir = join(spec, 'workflows');
|
|
130
135
|
const templatesDir = join(__dirname, '..', 'templates', 'workflows');
|
|
131
136
|
if (existsSync(templatesDir)) {
|
|
132
137
|
mkdirSync(workflowsDir, { recursive: true });
|
|
@@ -142,11 +147,11 @@ async function doInstall(projectDir, tools, subprojects = []) {
|
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
// 创建 shared/workspace 目录
|
|
145
|
-
mkdirSync(join(
|
|
146
|
-
mkdirSync(join(
|
|
150
|
+
mkdirSync(join(spec, 'shared'), { recursive: true });
|
|
151
|
+
mkdirSync(join(spec, 'workspace'), { recursive: true });
|
|
147
152
|
|
|
148
153
|
// 创建知识库骨架
|
|
149
|
-
const knowledgeDir = join(
|
|
154
|
+
const knowledgeDir = join(spec, 'knowledge');
|
|
150
155
|
mkdirSync(knowledgeDir, { recursive: true });
|
|
151
156
|
const indexPath = join(knowledgeDir, 'INDEX.md');
|
|
152
157
|
if (!existsSync(indexPath)) {
|
|
@@ -157,29 +162,33 @@ async function doInstall(projectDir, tools, subprojects = []) {
|
|
|
157
162
|
writeFileSync(uncatPath, `# 未分类知识\n\n> execute/quick 执行中发现的坑暂存于此,用户审阅后归类到对应文件并更新 INDEX.md。\n`);
|
|
158
163
|
}
|
|
159
164
|
|
|
160
|
-
// 创建 .
|
|
161
|
-
const runtimeDir = join(
|
|
165
|
+
// 创建 .runtime/ 目录结构(全局状态)
|
|
166
|
+
const runtimeDir = join(spec, '.runtime');
|
|
162
167
|
for (const sub of ['artifacts', 'history', 'logs', 'templates']) {
|
|
163
168
|
mkdirSync(join(runtimeDir, sub), { recursive: true });
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
// 初始化 SQLite
|
|
167
|
-
const pm = new ProgressManager();
|
|
171
|
+
// 初始化 SQLite 数据库
|
|
172
|
+
const pm = new ProgressManager({ specDir: spec });
|
|
168
173
|
await pm.init(projectDir);
|
|
169
174
|
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
// .gitignore 只在 specDir 在项目内时才修改
|
|
176
|
+
const isExternalSpec = specDir && resolve(spec) !== resolve(projectDir, '.sillyspec');
|
|
177
|
+
if (!isExternalSpec) {
|
|
178
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
179
|
+
const ignoreRules = ['.sillyspec/codebase/SCAN-RAW.md', '.sillyspec/local.yaml', '.sillyspec/.runtime/'];
|
|
180
|
+
if (existsSync(gitignorePath)) {
|
|
181
|
+
const content = readFileSync(gitignorePath, 'utf8');
|
|
182
|
+
let updated = content.trimEnd();
|
|
183
|
+
for (const rule of ignoreRules) {
|
|
184
|
+
if (!updated.includes(rule)) {
|
|
185
|
+
updated += '\n' + rule;
|
|
186
|
+
}
|
|
178
187
|
}
|
|
188
|
+
writeFileSync(gitignorePath, updated + '\n');
|
|
189
|
+
} else {
|
|
190
|
+
writeFileSync(gitignorePath, ignoreRules.join('\n') + '\n');
|
|
179
191
|
}
|
|
180
|
-
writeFileSync(gitignorePath, updated + '\n');
|
|
181
|
-
} else {
|
|
182
|
-
writeFileSync(gitignorePath, ignoreRules.join('\n') + '\n');
|
|
183
192
|
}
|
|
184
193
|
|
|
185
194
|
// 注入指令文件(codex/gemini/opencode)
|
|
@@ -218,7 +227,7 @@ async function doInstall(projectDir, tools, subprojects = []) {
|
|
|
218
227
|
|
|
219
228
|
// ── 安装完成总结 ──
|
|
220
229
|
|
|
221
|
-
function showSummary(version, tools) {
|
|
230
|
+
function showSummary(version, tools, specDir) {
|
|
222
231
|
const toolLabels = tools.map(t => TOOL_LABELS[t] || t);
|
|
223
232
|
|
|
224
233
|
console.log('');
|
|
@@ -227,7 +236,7 @@ function showSummary(version, tools) {
|
|
|
227
236
|
console.log(chalk.green(' ═══════════════════════════════════════'));
|
|
228
237
|
console.log('');
|
|
229
238
|
console.log(` 已安装工具: ${chalk.cyan(toolLabels.join(', '))}`);
|
|
230
|
-
console.log(
|
|
239
|
+
console.log(` 📁 规范目录: ${chalk.cyan(specDir || '.sillyspec')}`);
|
|
231
240
|
console.log('');
|
|
232
241
|
console.log(' 下一步:使用 AI 技能开始工作');
|
|
233
242
|
console.log(' OpenClaw: ' + chalk.bold('/sillyspec:brainstorm'));
|
|
@@ -251,8 +260,9 @@ export function getVersion() {
|
|
|
251
260
|
// ── 主命令 ──
|
|
252
261
|
|
|
253
262
|
export async function cmdInit(projectDir, options = {}) {
|
|
254
|
-
const { tool, interactive } = options;
|
|
263
|
+
const { tool, interactive, specDir } = options;
|
|
255
264
|
const version = getVersion();
|
|
265
|
+
const resolvedSpecDir = specDir ? resolve(specDir) : null;
|
|
256
266
|
|
|
257
267
|
// ── 交互式模式(--interactive 或 -i)──
|
|
258
268
|
if (interactive && isTTY()) {
|
|
@@ -350,8 +360,8 @@ export async function cmdInit(projectDir, options = {}) {
|
|
|
350
360
|
}
|
|
351
361
|
|
|
352
362
|
console.log('');
|
|
353
|
-
await doInstall(projectDir, selectedTools, subprojects);
|
|
354
|
-
showSummary(version, selectedTools);
|
|
363
|
+
await doInstall(projectDir, selectedTools, subprojects, resolvedSpecDir);
|
|
364
|
+
showSummary(version, selectedTools, resolvedSpecDir);
|
|
355
365
|
return;
|
|
356
366
|
}
|
|
357
367
|
|
|
@@ -369,12 +379,13 @@ export async function cmdInit(projectDir, options = {}) {
|
|
|
369
379
|
tools = detectTools(projectDir);
|
|
370
380
|
}
|
|
371
381
|
|
|
372
|
-
await doInstall(projectDir, tools);
|
|
382
|
+
await doInstall(projectDir, tools, [], resolvedSpecDir);
|
|
373
383
|
|
|
374
384
|
console.log('');
|
|
375
385
|
console.log(chalk.green(` ✅ SillySpec v${version} 安装完成!`));
|
|
376
386
|
console.log('');
|
|
377
|
-
|
|
387
|
+
const specDisplay = resolvedSpecDir || '.sillyspec';
|
|
388
|
+
console.log(` 📁 规范目录: ${chalk.cyan(specDisplay)}`);
|
|
378
389
|
console.log('');
|
|
379
390
|
console.log(' 下一步:使用 AI 技能开始工作');
|
|
380
391
|
console.log(` OpenClaw: ${chalk.bold('/sillyspec:brainstorm')}`);
|
package/src/progress.js
CHANGED
|
@@ -15,8 +15,10 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkS
|
|
|
15
15
|
import { join, basename } from 'path';
|
|
16
16
|
import { DB } from './db.js';
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
const
|
|
18
|
+
// 默认规范目录名(相对于 cwd)
|
|
19
|
+
const SPEC_DIR_NAME = '.sillyspec';
|
|
20
|
+
const RUNTIME_SUBDIR = '.runtime';
|
|
21
|
+
const CHANGES_SUBDIR = 'changes';
|
|
20
22
|
const GLOBAL_FILE = 'global.json';
|
|
21
23
|
const CURRENT_VERSION = 3;
|
|
22
24
|
const VALID_STAGES = ['scan', 'brainstorm', 'propose', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
|
|
@@ -50,14 +52,27 @@ function makeInitialGlobal(project) {
|
|
|
50
52
|
// ── ProgressManager ──
|
|
51
53
|
|
|
52
54
|
export class ProgressManager {
|
|
55
|
+
/**
|
|
56
|
+
* @param {object} [opts]
|
|
57
|
+
* @param {string} [opts.specDir] - 规范目录绝对路径(默认 cwd/.sillyspec)
|
|
58
|
+
*/
|
|
59
|
+
constructor(opts = {}) {
|
|
60
|
+
this._customSpecDir = opts.specDir || null;
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
// ── 路径工具 ──
|
|
54
64
|
|
|
65
|
+
/** 获取 specDir(优先自定义,否则 cwd/.sillyspec) */
|
|
66
|
+
_getSpecDir(cwd) {
|
|
67
|
+
return this._customSpecDir || join(cwd, SPEC_DIR_NAME);
|
|
68
|
+
}
|
|
69
|
+
|
|
55
70
|
_runtimePath(cwd, ...parts) {
|
|
56
|
-
return join(cwd,
|
|
71
|
+
return join(this._getSpecDir(cwd), RUNTIME_SUBDIR, ...parts);
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
_changePath(cwd, changeName, ...parts) {
|
|
60
|
-
return join(cwd,
|
|
75
|
+
return join(this._getSpecDir(cwd), CHANGES_SUBDIR, changeName, ...parts);
|
|
61
76
|
}
|
|
62
77
|
|
|
63
78
|
_ensureRuntimeDir(cwd) {
|
|
@@ -1207,6 +1222,8 @@ export class ProgressManager {
|
|
|
1207
1222
|
}
|
|
1208
1223
|
|
|
1209
1224
|
_ensureGitignore(cwd) {
|
|
1225
|
+
// 外部 specDir 不需要修改项目 .gitignore
|
|
1226
|
+
if (this._customSpecDir) return;
|
|
1210
1227
|
const gitignorePath = join(cwd, '.gitignore');
|
|
1211
1228
|
const rule = '.sillyspec/.runtime/';
|
|
1212
1229
|
if (existsSync(gitignorePath)) {
|