sillyspec 3.25.9 → 3.26.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.
Files changed (76) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +8 -13
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +8 -6
  3. package/.claude/skills/sillyspec-continue/SKILL.md +3 -3
  4. package/.claude/skills/sillyspec-doctor/SKILL.md +1 -1
  5. package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
  6. package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
  7. package/.claude/skills/sillyspec-quick/SKILL.md +3 -1
  8. package/.claude/skills/sillyspec-verify/SKILL.md +1 -1
  9. package/.sillyspec-platform-cleaned +1 -0
  10. package/.sillyspec-platform.json +7 -0
  11. package/README.md +9 -0
  12. package/package.json +2 -2
  13. package/src/change-list.js +11 -8
  14. package/src/change-risk-profile.js +22 -0
  15. package/src/db.js +170 -161
  16. package/src/dispatch/backends/local-agent.js +152 -0
  17. package/src/dispatch/backends/sillyhub-mcp.js +242 -0
  18. package/src/dispatch/probe.js +210 -0
  19. package/src/dispatch/strategy.js +121 -0
  20. package/src/doctor-diagnostics.js +19 -25
  21. package/src/fs-atomic.js +69 -69
  22. package/src/git-helper.js +66 -0
  23. package/src/hooks/worktree-guard.js +64 -55
  24. package/src/index.js +326 -36
  25. package/src/init.js +7 -14
  26. package/src/local-detect.js +120 -110
  27. package/src/machine-interface.js +54 -19
  28. package/src/migrate.js +14 -4
  29. package/src/modules.js +16 -5
  30. package/src/progress/change-registry.js +85 -70
  31. package/src/progress/consistency-doctor.js +71 -14
  32. package/src/progress/shared.js +8 -5
  33. package/src/progress/stage-machine.js +67 -65
  34. package/src/progress/step-store.js +72 -67
  35. package/src/progress.js +448 -236
  36. package/src/quicklog.js +59 -12
  37. package/src/run/command.js +78 -39
  38. package/src/run/complete-handlers.js +1028 -889
  39. package/src/run/complete.js +132 -212
  40. package/src/run/concurrent-detect.js +132 -0
  41. package/src/run/gates.js +302 -19
  42. package/src/run/prompt.js +22 -68
  43. package/src/run/quick-audit.js +10 -9
  44. package/src/run/shared.js +722 -600
  45. package/src/run/stage.js +43 -24
  46. package/src/scan-postcheck.js +9 -31
  47. package/src/sillyhub-mcp/client.js +394 -0
  48. package/src/sillyhub-mcp/config.js +62 -0
  49. package/src/spec-dir-typo.js +71 -0
  50. package/src/stage-contract-engine.js +22 -1
  51. package/src/stage-contract-spec.js +11 -4
  52. package/src/stage-contract.js +26 -3
  53. package/src/stage-review.js +118 -10
  54. package/src/stages/archive.js +4 -6
  55. package/src/stages/brainstorm-auto.js +26 -20
  56. package/src/stages/brainstorm.js +29 -5
  57. package/src/stages/cmd-existence.js +145 -0
  58. package/src/stages/doctor.js +10 -7
  59. package/src/stages/execute.js +64 -22
  60. package/src/stages/plan-postcheck.js +152 -0
  61. package/src/stages/plan.js +16 -31
  62. package/src/stages/quick.js +25 -9
  63. package/src/stages/scan.js +53 -8
  64. package/src/stages/status.js +0 -1
  65. package/src/stages/verify.js +14 -9
  66. package/src/sync.js +433 -16
  67. package/src/task-review.js +59 -1
  68. package/src/verify-postcheck.js +938 -862
  69. package/src/version.js +19 -0
  70. package/src/worktree-apply.js +218 -93
  71. package/src/worktree-deps.js +89 -5
  72. package/src/worktree.js +213 -111
  73. package/templates/prompts/taskcard-rules.md +27 -0
  74. package/templates/prompts/verify-probes.md +3 -3
  75. package/.claude/skills/sillyspec-propose/SKILL.md +0 -87
  76. package/src/stages/propose.js +0 -181
package/src/run/prompt.js CHANGED
@@ -20,8 +20,9 @@
20
20
  import { basename, join } from 'node:path'
21
21
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
22
22
  import { stageRegistry } from '../stages/index.js'
23
- import { resolvePromptIncludes, safeGit, WAIT_MARKER_RE } from './shared.js'
23
+ import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, WAIT_MARKER_RE } from './shared.js'
24
24
  import { renderStageContract } from '../stage-contract-spec.js'
25
+ import { parseModuleMapSimple } from '../modules.js'
25
26
 
26
27
  /**
27
28
  * 从 _module-map.yaml 读取模块上下文索引
@@ -36,6 +37,14 @@ function loadModuleContextIndex(specBase, projectName) {
36
37
  const mapPath = join(specBase, 'docs', projectName, 'modules', '_module-map.yaml')
37
38
  if (!existsSync(mapPath)) return null
38
39
  const content = readFileSync(mapPath, 'utf8')
40
+ // schema_version 校验(advisory):modules.js 写 schema_version: 2;缺失或非 2 → warn,
41
+ // 提示 v1/v2 格式混用风险(旧格式字段解析会静默错位)。正常生成的文件不触发。
42
+ const sv = content.match(/^schema_version:\s*(\d+)/m)
43
+ if (!sv) {
44
+ console.warn(`⚠️ _module-map.yaml 缺少 schema_version 声明(期望 2),模块解析可能错位:${mapPath}`)
45
+ } else if (sv[1] !== '2') {
46
+ console.warn(`⚠️ _module-map.yaml schema_version=${sv[1]}(期望 2),可能为旧格式,模块解析可能错位:${mapPath}`)
47
+ }
39
48
  return parseModuleMapSimple(content)
40
49
  } catch {
41
50
  return null
@@ -104,63 +113,8 @@ function buildModuleContextInjection(taskDescription, moduleIndex, specBase, pro
104
113
  return injection
105
114
  }
106
115
 
107
- // 复用 modules.js 的简单 YAML 解析(避免循环依赖)
108
- function parseModuleMapSimple(content) {
109
- const modules = {}
110
- let currentModule = null
111
- let currentKey = null
112
- let currentArray = null
113
-
114
- for (const line of content.split('\n')) {
115
- const moduleMatch = line.match(/^ ([a-zA-Z0-9_-]+):$/)
116
- if (moduleMatch) {
117
- if (currentArray && currentModule && currentKey) {
118
- modules[currentModule][currentKey] = currentArray
119
- }
120
- currentModule = moduleMatch[1]
121
- modules[currentModule] = {}
122
- currentKey = null
123
- currentArray = null
124
- continue
125
- }
126
- if (!currentModule) continue
127
-
128
- const arrayFieldMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons|core_files|test_files|related_docs|verify_commands):$/)
129
- if (arrayFieldMatch) {
130
- if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray
131
- currentKey = arrayFieldMatch[1]
132
- currentArray = []
133
- continue
134
- }
135
-
136
- const inlineArrayMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons|core_files|test_files|related_docs|verify_commands): \[(.*)\]$/)
137
- if (inlineArrayMatch) {
138
- if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray
139
- const vals = inlineArrayMatch[2].split(',').map(v => v.trim()).filter(Boolean)
140
- modules[currentModule][inlineArrayMatch[1]] = vals
141
- currentKey = null
142
- currentArray = null
143
- continue
144
- }
145
-
146
- const scalarMatch = line.match(/^ (status|doc|needs_review|role|risk_level): (.+)$/)
147
- if (scalarMatch) {
148
- if (currentArray && currentKey) { modules[currentModule][currentKey] = currentArray; currentArray = null; currentKey = null }
149
- modules[currentModule][scalarMatch[1]] = scalarMatch[2]
150
- continue
151
- }
152
-
153
- const itemMatch = line.match(/^ - (.+)$/)
154
- if (itemMatch && currentArray !== null) {
155
- currentArray.push(itemMatch[1].trim())
156
- continue
157
- }
158
- }
159
- if (currentArray && currentModule && currentKey) {
160
- modules[currentModule][currentKey] = currentArray
161
- }
162
- return modules
163
- }
116
+ // parseModuleMapSimple 复用 modules.js 的 canonical 实现(合并历史 copy-paste 副本,2026-08-07;
117
+ // 无循环依赖:modules.js 仅 import fs/path/db.js,prompt.js → modules.js 单向)。
164
118
  /**
165
119
  * 输出当前步骤的 prompt
166
120
  */
@@ -450,7 +404,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
450
404
  if (stageName === 'execute' && promptText.includes('{EXECUTE_RUN_ID}')) {
451
405
  let runId = ''
452
406
  const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
453
- const runtimeRoot = platformOpts?.runtimeRoot || join(execSpecBase, '.runtime')
407
+ const runtimeRoot = resolveRuntimeRoot(platformOpts, execSpecBase)
454
408
  const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
455
409
  try {
456
410
  if (existsSync(runIdFile)) {
@@ -466,10 +420,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
466
420
  promptText = promptText.replace(/\{EXECUTE_RUN_ID\}/g, runId)
467
421
  }
468
422
 
469
- // Stage Review Tier:brainstorm/plan/propose/execute 阶段注入审查分级占位符
423
+ // Stage Review Tier:brainstorm/plan/execute 阶段注入审查分级占位符
470
424
  // (scanProfile 只在 scan 生效、change-risk-profile 只管 apply/verify,都不约束这些阶段的审查方式,
471
425
  // 故按 plan_level / 变更文件数分级:self 当前 agent 自审,independent 强制独立子代理 + review.json)
472
- if (['brainstorm', 'plan', 'propose', 'execute'].includes(stageName) && promptText.includes('{REVIEW_TIER}')) {
426
+ if (['brainstorm', 'plan', 'execute'].includes(stageName) && promptText.includes('{REVIEW_TIER}')) {
473
427
  try {
474
428
  const { classifyReviewTier } = await import('../review-tier.js')
475
429
  const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath } = await import('../stage-review.js')
@@ -488,7 +442,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
488
442
  // reviewRunId:优先读 marker 复用(保证多次渲染 prompt 注入同一 ID == gate 读取的 ID,
489
443
  // 修复「prompt 多次渲染 / 多次 review 时 gate 取错 ID 读错 review.json」),marker 缺失才
490
444
  // generate + 落盘。对齐 execute {EXECUTE_RUN_ID} 段(prompt.js:449-467)。
491
- const tierRuntimeRoot = platformOpts?.runtimeRoot || join(tierSpecBase, '.runtime')
445
+ const tierRuntimeRoot = resolveRuntimeRoot(platformOpts, tierSpecBase)
492
446
  const reviewRunIdMarker = stageReviewMarkerPath(tierRuntimeRoot, stageName, changeName)
493
447
  let reviewRunId = ''
494
448
  try {
@@ -526,7 +480,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
526
480
  try {
527
481
  const { summarizeTaskCompletion } = await import('../task-review.js')
528
482
  const tcrSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
529
- const tcrRuntimeRoot = platformOpts?.runtimeRoot || join(tcrSpecBase, '.runtime')
483
+ const tcrRuntimeRoot = resolveRuntimeRoot(platformOpts, tcrSpecBase)
530
484
  const tcrChangeDir = changeName ? join(tcrSpecBase, 'changes', changeName) : null
531
485
  const summary = tcrChangeDir
532
486
  ? summarizeTaskCompletion({ changeDir: tcrChangeDir, runtimeRoot: tcrRuntimeRoot, changeName })
@@ -585,11 +539,11 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
585
539
  if (stepIndex === 0) {
586
540
  console.log(`\n### ⚠️ 铁律`)
587
541
  console.log('- 文档优先:代码产出必须先有对应的设计/规范文档支撑。')
588
- console.log('- 只做本步骤描述的操作,不得自行扩展或跳过')
589
- console.log('- 不要回头修改已完成的步骤')
590
- console.log('- 不要编造不存在的 CLI 子命令')
591
- console.log('- 完成后立即执行 --done 命令,不得跳过')
592
- console.log('- 不要用 mv/rename 重命名变更目录,必须用 `sillyspec change-rename <旧名> <新名>`')
542
+ console.log('- 聚焦本步骤:只执行本步骤描述的操作并完整做完;自行扩展或跳步会破坏状态机推进,后续步骤再做后续事。')
543
+ console.log('- 已完成步骤视为只读:需修订用 `sillyspec run <stage> --reopen --from-step N` 回退重做,直接回头改会让进度记录与产物脱节。')
544
+ console.log('- CLI 子命令以本 prompt 或上一条 --done 输出的字面为准;不确定时停下问用户,猜测命令会让状态机推进到错误位置。')
545
+ console.log('- 本步骤产物落盘后立即执行 prompt 末尾的 --done:CLI 据此校验产出并推进状态机;不跑则进度永远停在本步。')
546
+ console.log('- 变更目录改名用 `sillyspec change-rename <旧名> <新名>`:mv/rename 会漏改进度库引用,导致变更失联。')
593
547
  console.log('- 文档类型文件(.md/.yaml/.json 等)头部必须包含 author(git 用户名)和 created_at(精确到秒)')
594
548
  console.log('- 执行构建/测试前必须先读 local.yaml,优先使用其中配置的命令、路径和环境变量;未配置时才使用默认值')
595
549
  }
@@ -6,12 +6,11 @@
6
6
  *
7
7
  * 路径修正(相对 src/run/):
8
8
  * - resolveQuickLinkedChanges 的 quick-recommend.js 动态 import './quick-recommend.js' → '../quick-recommend.js'
9
- * - 原动态 import('child_process') 删除,改顶部静态 execSync
9
+ * - 脏文件信号改用 safeGit(带 safe.directory,Q3);不再 import execSync
10
10
  * - parsePorcelainPath 从 ./shared.js 重 import;checkbox 顶部 import
11
11
  */
12
- import { execSync } from 'node:child_process'
13
12
  import { checkbox } from '@inquirer/prompts'
14
- import { parsePorcelainPath } from './shared.js'
13
+ import { parsePorcelainPath, safeGit } from './shared.js'
15
14
 
16
15
  /**
17
16
  * 打印 quick 完成审计结论(按 review.status 输出 SAFE/WARNING/BLOCKED)。
@@ -55,7 +54,7 @@ export function printQuickAuditReview(review) {
55
54
  export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles, taskDescription, nonInteractive }) {
56
55
  let activeChanges = []
57
56
  try {
58
- activeChanges = await pm.listChanges(cwd)
57
+ activeChanges = pm.listChanges(cwd)
59
58
  } catch {
60
59
  activeChanges = []
61
60
  }
@@ -67,15 +66,17 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
67
66
  return []
68
67
  }
69
68
 
70
- // 脏文件(推荐信号之一)
69
+ // 脏文件(推荐信号之一)。safeGit 带 safe.directory,避免 linked worktree/容器/挂载点下裸
70
+ // `git status` 抛错致推荐信号静默丢失(multi-agent-review Q3 同类)。推荐非关键路径,失败回退 []。
71
+ // trim:false:porcelain 首行前导空格是状态码,trim 会削掉致 parsePorcelainPath 丢首字符。
71
72
  let baselineFiles = []
72
- try {
73
- const gs = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 10000 })
74
- baselineFiles = gs.split('\n').filter(Boolean)
73
+ const statusResult = safeGit(cwd, ['status', '--porcelain'], { trim: false })
74
+ if (!statusResult.error) {
75
+ baselineFiles = (statusResult.value || '').split('\n').filter(Boolean)
75
76
  .map(l => parsePorcelainPath(l))
76
77
  .filter(Boolean)
77
78
  .filter(f => !f.startsWith('.sillyspec/'))
78
- } catch {}
79
+ }
79
80
 
80
81
  // 推荐打分(脏文件 + 任务描述双信号)
81
82
  let recommendations = []