sillyspec 3.12.4 → 3.12.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.12.4",
3
+ "version": "3.12.6",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -14,7 +14,7 @@ import path from 'path'
14
14
 
15
15
  // ── 常量 ──
16
16
 
17
- const ALLOWED_STAGES = ['execute', 'quick']
17
+ const WORKTREE_STAGES = ['execute'] // 这些阶段必须在 worktree 里
18
18
 
19
19
  const WORKTREE_SEGMENT = '.sillyspec/.runtime/worktrees/'
20
20
 
@@ -48,11 +48,11 @@ const DANGER_PREFIXES = ['sudo', 'rm -rf', 'rm -r', 'rmdir']
48
48
  const STAGE_HINTS = {
49
49
  '(none)': [
50
50
  '没有检测到活跃的 SillySpec 流程。',
51
- '你需要先启动一个任务流程才能修改源码:',
51
+ '你需要先启动一个任务流程才能修改源码(调用对应的 sillyspec skill):',
52
52
  '',
53
- ' 小改动(≤3 文件):sillyspec run quick "任务描述"',
54
- ' 大改动(>3 文件):sillyspec run brainstorm → plan → execute',
55
- ' 全自动模式: sillyspec run auto "任务描述"',
53
+ ' BUG修复(skill sillyspec-quick):sillyspec run quick "任务描述"',
54
+ ' 逻辑变更(skill sillyspec-brainstorm):sillyspec run brainstorm → plan → execute → verify → archive',
55
+ ' 全自动模式(skill sillyspec-auto):sillyspec run auto "任务描述"',
56
56
  ],
57
57
  'brainstorm': [
58
58
  '当前在 brainstorm(需求分析)阶段,这个阶段只写文档,不写代码。',
@@ -306,12 +306,8 @@ function isSingleCommandReadonly(cmd, extraReadonlyCommands = []) {
306
306
  return false
307
307
  }
308
308
 
309
- // sillyspec worktree 命令
310
- if (cmdName === 'sillyspec') {
311
- const sub = parts[1] || ''
312
- if (sub === 'worktree') return true
313
- return false
314
- }
309
+ // sillyspec 命令全部放行(CLI 工具本身安全)
310
+ if (cmdName === 'sillyspec') return true
315
311
 
316
312
  return false
317
313
  }
@@ -414,14 +410,17 @@ export function shouldBlockWrite(filePath, cwd) {
414
410
  const effectiveCwd = cwd || process.cwd()
415
411
  const stage = readCurrentStage(effectiveCwd) || '(none)'
416
412
 
417
- if (!ALLOWED_STAGES.includes(stage)) {
413
+ if (!['execute', 'quick'].includes(stage)) {
418
414
  return {
419
415
  blocked: true,
420
416
  reason: buildStageHint(stage)
421
417
  }
422
418
  }
423
419
 
424
- // 3. 位置门禁
420
+ // quick 阶段直接放行(不要求 worktree)
421
+ if (stage === 'quick') return { blocked: false }
422
+
423
+ // execute 阶段:位置门禁
425
424
  if (isInsideWorktree(absPath)) return { blocked: false }
426
425
 
427
426
  // noWorktree 模式:无隔离环境,禁止源码写入(降级到更严格)
@@ -468,9 +467,8 @@ export function shouldBlockBash(command, cwd) {
468
467
 
469
468
  // 阶段门禁(使用 fallback 读取)
470
469
  const stage = readCurrentStage(effectiveCwd) || '(none)'
471
- const stageOk = ALLOWED_STAGES.includes(stage)
472
470
 
473
- if (!stageOk) {
471
+ if (!['execute', 'quick'].includes(stage)) {
474
472
  // 非 execute/quick 阶段,只允许只读白名单
475
473
  const localConfig = loadLocalConfig(effectiveCwd)
476
474
  const extraReadonly = localConfig.worktreeHook?.readonlyCommands || localConfig['worktree-hook']?.readonlyCommands || []
@@ -481,7 +479,16 @@ export function shouldBlockBash(command, cwd) {
481
479
  }
482
480
  }
483
481
 
484
- // execute/quick 阶段 + 主工作区
482
+ // quick 阶段直接放行(不要求 worktree)
483
+ if (stage === 'quick') {
484
+ // 危险黑名单仍然拦截
485
+ if (matchDangerBlacklist(command)) {
486
+ return { blocked: true, reason: `dangerous command blocked: ${command.trim()}` }
487
+ }
488
+ return { blocked: false }
489
+ }
490
+
491
+ // execute 阶段 + 主工作区
485
492
  const localConfig = loadLocalConfig(effectiveCwd)
486
493
  const extraReadonly = localConfig.worktreeHook?.readonlyCommands || localConfig['worktree-hook']?.readonlyCommands || []
487
494
 
package/src/index.js CHANGED
@@ -268,7 +268,8 @@ async function main() {
268
268
  case 'worktree': {
269
269
  const { WorktreeManager } = await import('./worktree.js');
270
270
  const wtSubCmd = filteredArgs[1];
271
- const wtName = filteredArgs[2];
271
+ // 提取第一个非 -- 开头的位置参数作为 wtName
272
+ const wtName = filteredArgs.slice(2).find(a => !a.startsWith('-'));
272
273
  const wm = new WorktreeManager({ cwd: dir });
273
274
 
274
275
  if (!wtSubCmd || wtSubCmd === 'help' || wtSubCmd === '--help' || wtSubCmd === '-h') {
@@ -338,6 +339,11 @@ SillySpec worktree — git worktree 隔离管理
338
339
  } else {
339
340
  console.log(`✅ 已应用 ${result.changedFiles.length} 个文件变更`);
340
341
  }
342
+ if (result.warnings && result.warnings.length > 0) {
343
+ for (const w of result.warnings) {
344
+ console.log(`⚠️ ${w}`);
345
+ }
346
+ }
341
347
  break;
342
348
  }
343
349
  case 'list': {
package/src/run.js CHANGED
@@ -195,6 +195,28 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
195
195
  if (projectName && promptText.includes('<project>')) {
196
196
  promptText = promptText.replace(/<project>/g, projectName)
197
197
  }
198
+ // 替换 <git-user> 占位符
199
+ if (promptText.includes('<git-user>')) {
200
+ const { execSync } = await import('child_process')
201
+ try {
202
+ const gitUser = execSync('git config user.name', { cwd, encoding: 'utf8', timeout: 5000 }).trim()
203
+ promptText = promptText.replace(/<git-user>/g, gitUser)
204
+ } catch {
205
+ promptText = promptText.replace(/<git-user>/g, 'unknown')
206
+ }
207
+ }
208
+ // 替换时间戳占位符
209
+ const now = new Date()
210
+ const nowDatetime = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0') + ' ' + String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0') + ':' + String(now.getSeconds()).padStart(2,'0')
211
+ const nowTimestamp = now.getFullYear() + String(now.getMonth()+1).padStart(2,'0') + String(now.getDate()).padStart(2,'0') + '-' + String(now.getHours()).padStart(2,'0') + String(now.getMinutes()).padStart(2,'0') + String(now.getSeconds()).padStart(2,'0')
212
+ const nowDate = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0')
213
+ promptText = promptText.replace(/<now-datetime>/g, nowDatetime)
214
+ promptText = promptText.replace(/<now-timestamp>/g, nowTimestamp)
215
+ promptText = promptText.replace(/<now-date>/g, nowDate)
216
+ // 替换 <change-name> 占位符
217
+ if (changeName && promptText.includes('<change-name>')) {
218
+ promptText = promptText.replace(/<change-name>/g, changeName)
219
+ }
198
220
  console.log(promptText)
199
221
  console.log(`\n### ⚠️ 铁律`)
200
222
  console.log('- **文档是核心资产,代码是文档的产物。** 没有文档就没有代码——文档是 AI 的记忆,是团队协作的基础,是后续维护的唯一依据。任何代码产出必须先有对应的设计/规范文档支撑。')
@@ -46,8 +46,8 @@ export const definition = {
46
46
  \`\`\`markdown
47
47
  # 模块影响分析
48
48
 
49
- author: <git用户名>
50
- created_at: <YYYY-MM-DD HH:mm:ss>
49
+ author: <git-user>
50
+ created_at: <now-datetime>
51
51
 
52
52
  ## 变更:<change-name>
53
53
 
@@ -90,7 +90,7 @@ module-impact.md 路径 + 影响模块数量 + 未匹配文件数量`,
90
90
  - 更新:只改相关章节(当前设计/对外接口/依赖关系等),保持其他章节不变
91
91
  - 正文重写为当前状态,不追加历史
92
92
  - 底部"变更索引"追加一行:\`| <日期> | <变更名> | <一句话摘要> |\`
93
- d. 更新头部元数据:\`> 最后更新:YYYY-MM-DD\`、\`> 最近变更:<change-name>\`
93
+ d. 更新头部元数据:\`> 最后更新:<now-date>\`、\`> 最近变更:<change-name>\`
94
94
  4. 展示所有模块文档的更新内容(diff 摘要),请用户确认
95
95
  5. 用户确认后,写入 \`.sillyspec/docs/<project>/modules/*.md\`
96
96
  6. 用户拒绝时,不写入模块文档,但提示"module-impact.md 已保留,可稍后手动同步"
@@ -100,7 +100,7 @@ module-impact.md 路径 + 影响模块数量 + 未匹配文件数量`,
100
100
  \`\`\`markdown
101
101
  # <module-name>
102
102
 
103
- > 最后更新:YYYY-MM-DD
103
+ > 最后更新:<now-date>
104
104
  > 最近变更:<change-name>
105
105
  > 模块路径:<glob patterns>
106
106
 
@@ -24,10 +24,10 @@ export const definition = {
24
24
 
25
25
  ### 创建任务记录(必须执行)
26
26
  理解完任务后,立即创建记录文件:
27
- 1. \`git config user.name\` 获取用户名
28
- 2. 无 \`--change\`:创建 .sillyspec/quicklog/QUICKLOG-<git用户名>.md\`(已存在则追加),写入:
27
+ 1. 使用预注入的 git 用户名:\`<git-user>\`
28
+ 2. 无 \`--change\`:创建 .sillyspec/quicklog/QUICKLOG-\`<git-user>\`.md\`(已存在则追加),写入:
29
29
  \`\`\`
30
- ## YYYY-MM-DD HH:mm:ss — <一句话任务描述>
30
+ ## <now-datetime> — <一句话任务描述>
31
31
  状态:进行中
32
32
  文件:<预估要改的文件>
33
33
  \`\`\`
@@ -40,33 +40,12 @@ export const definition = {
40
40
  outputHint: '任务理解',
41
41
  optional: false
42
42
  },
43
- {
44
- name: '创建 worktree',
45
- prompt: `为本次 quick 任务创建隔离的 git worktree。
46
-
47
- ### 操作
48
- 1. 确定变更名(change name):
49
- - 如携带 \`--change <变更名>\`,使用该变更名
50
- - 否则,生成临时变更名:\`quick-<当前时间戳 YYYYMMDD-HHmmss>\`
51
- 2. 运行 \`sillyspec worktree create <变更名>\`
52
- 3. 记录输出的 worktree 路径(后续步骤需要使用)
53
- 4. 如果创建失败 → 报错并停止(不要在无隔离状态下继续)
54
-
55
- ### 输出
56
- worktree 路径 + 变更名 + 分支名`,
57
- outputHint: 'worktree 路径',
58
- optional: false
59
- },
60
43
  {
61
44
  name: '实现并验证',
62
- prompt: `实现任务。
63
-
64
- ### 工作目录
65
- 你必须在上一步记录的 worktree 路径中工作。
66
- 不要在主工作区修改源码文件。所有代码变更只在 worktree 中进行。
45
+ prompt: `直接在主工作区实现任务。
67
46
 
68
47
  ### 操作
69
- 1. 先读后写:调用已有方法前 \`cat\` 源文件确认签名,\`grep\` 确认方法存在(在 worktree 中读取)
48
+ 1. 先读后写:调用已有方法前 \`cat\` 源文件确认签名,\`grep\` 确认方法存在
70
49
  2. 写代码完成任务
71
50
  3. 如涉及逻辑变更,建议写单元测试验证(不强制,纯配置/文档/小改动可跳过)
72
51
  4. **不要编译!** 除非用户明确要求或改动量很大
@@ -81,24 +60,6 @@ worktree 路径 + 变更名 + 分支名`,
81
60
  outputHint: '实现摘要',
82
61
  optional: false
83
62
  },
84
- {
85
- name: 'apply 并 cleanup',
86
- prompt: `将 worktree 中的变更应用到主工作区并清理。
87
-
88
- ### 操作
89
- 1. 运行 \`sillyspec worktree apply --check-only <变更名>\`
90
- 2. 展示 diff 摘要(文件列表 + 变更统计)
91
- 3. 展示检查结果(是否通过文件清单校验)
92
- 4. 用户确认后运行 \`sillyspec worktree apply <变更名>\`
93
- 5. apply 成功 → 自动 cleanup,进入下一步
94
- 6. apply 失败 → 展示错误详情,用户选择重试或手动处理
95
- 7. 如果用户不想 apply → 运行 \`sillyspec worktree cleanup <变更名>\` 丢弃变更
96
-
97
- ### 输出
98
- apply 结果 + 下一步建议`,
99
- outputHint: 'apply 结果',
100
- optional: false
101
- },
102
63
  {
103
64
  name: '暂存和更新记录',
104
65
  prompt: `Git 暂存并更新任务记录。
@@ -134,7 +134,7 @@ export const definition = {
134
134
 
135
135
  ### 每个子代理的共同要求
136
136
  - **上下文注入**:主 agent 在启动子代理前,必须将以下信息拼入子代理 prompt:
137
- - 项目名(<project>)
137
+ - 项目名(直接用本次 prompt 中的实际项目名)
138
138
  - 断点续扫步骤列出的缺失文档列表(哪些要生成、哪些跳过)
139
139
  - 环境探测结果摘要(构建工具、语言框架、关键依赖)
140
140
  - _env-detect.md 内容(如存在,直接贴入)
@@ -265,7 +265,7 @@ _module-map.yaml 生成结果(已存在/已生成/模块列表)`,
265
265
  3. 按以下模板写入目标文件:
266
266
 
267
267
  # <module-name>
268
- > 最后更新:YYYY-MM-DD
268
+ > 最后更新:<now-date>
269
269
  > 最近变更:scan(初始生成)
270
270
  > 模块路径:<glob patterns>
271
271
 
@@ -251,8 +251,13 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
251
251
 
252
252
  result.ok = true;
253
253
 
254
- // --- 8. 成功后自动 cleanup ---
255
- wm.cleanup(changeName);
254
+ // --- 8. 成功后自动 cleanup(失败不影响整体结果) ---
255
+ try {
256
+ wm.cleanup(changeName);
257
+ } catch (cleanupErr) {
258
+ result.warnings = result.warnings || [];
259
+ result.warnings.push(`cleanup 失败(不影响应用结果): ${cleanupErr.message}`);
260
+ }
256
261
 
257
262
  } catch (e) {
258
263
  result.errors.push(`patch 生成/应用异常: ${e.message}`);
package/src/worktree.js CHANGED
@@ -106,7 +106,13 @@ export class WorktreeManager {
106
106
 
107
107
  // 1. 检查 worktree 是否已存在
108
108
  if (existsSync(worktreePath)) {
109
- throw new Error(`worktree already exists: ${name}. Run cleanup first.`);
109
+ // 目录在但 meta.json 不存在(幽灵状态),自动清理
110
+ if (!this.getMeta(name)) {
111
+ console.log(`⚠️ 检测到幽灵 worktree 目录(无 meta.json),自动清理...`);
112
+ try { rmSync(worktreePath, { recursive: true, force: true }); } catch {}
113
+ } else {
114
+ throw new Error(`worktree already exists: ${name}. Run cleanup first.`);
115
+ }
110
116
  }
111
117
 
112
118
  // 2. 检查分支是否已存在
@@ -223,26 +229,27 @@ export class WorktreeManager {
223
229
  cleanup(changeName) {
224
230
  const name = validateChangeName(changeName);
225
231
  const meta = this.getMeta(name);
232
+ const worktreePath = this.getWorktreePath(name);
226
233
 
227
- if (!meta) {
228
- throw new Error(`worktree not found: ${name}。meta.json 不存在,可能已被清理或从未创建。`);
234
+ if (!meta && !existsSync(worktreePath)) {
235
+ throw new Error(`worktree not found: ${name}。meta.json 不存在,目录也不存在,可能已被清理或从未创建。`);
229
236
  }
230
237
 
231
- const worktreePath = meta.worktreePath || this.getWorktreePath(name);
232
- const branch = meta.branch || BRANCH_PREFIX + name;
233
-
234
- // 2. 移除 git worktree
238
+ // 1. 尝试 git worktree remove
235
239
  try {
236
240
  git(this.cwd, `worktree remove ${worktreePath} --force`);
237
241
  } catch {
238
242
  // git worktree remove 失败,尝试直接删除目录
239
- try {
240
- if (existsSync(worktreePath)) {
241
- rmSync(worktreePath, { recursive: true, force: true });
242
- }
243
- } catch (e) {
244
- throw new Error(`清理 worktree 目录失败: ${e.message}`);
243
+ }
244
+ const branch = (meta && meta.branch) || BRANCH_PREFIX + name;
245
+
246
+ // 2. 确保目录已删除
247
+ try {
248
+ if (existsSync(worktreePath)) {
249
+ rmSync(worktreePath, { recursive: true, force: true });
245
250
  }
251
+ } catch (e) {
252
+ throw new Error(`清理 worktree 目录失败: ${e.message}`);
246
253
  }
247
254
 
248
255
  // 3. 删除分支(忽略分支不存在的错误)