sillyspec 3.17.1 → 3.17.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.17.1",
3
+ "version": "3.17.3",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/init.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, statSync } from 'fs';
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
2
2
  import { join, resolve, dirname, basename } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { checkbox, confirm, input } from '@inquirer/prompts';
@@ -108,6 +108,14 @@ async function doInstall(projectDir, tools, subprojects = [], specDir = null) {
108
108
  // projectDir: 源码项目根目录(用于工具检测、指令注入、.gitignore)
109
109
  const spec = specDir || join(projectDir, '.sillyspec');
110
110
 
111
+ // 外部 specDir 时清理旧版本残留的 cwd/.sillyspec/(防止源码污染)
112
+ const legacyDir = join(projectDir, '.sillyspec');
113
+ if (specDir && existsSync(legacyDir)) {
114
+ try { rmSync(legacyDir, { recursive: true, force: true }) } catch {}
115
+ if (!existsSync(legacyDir)) console.log('🧹 已清理旧版本残留的源码 .sillyspec/ 目录');
116
+ else console.error('⚠️ 清理残留 .sillyspec/ 失败');
117
+ }
118
+
111
119
  // 创建基础目录
112
120
  // spec/projects/ → 项目注册表
113
121
  // spec/docs/<name>/ → 统一文档中心
package/src/run.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * 支持多变更并行:每个变更状态存储在 sillyspec.db 中。
6
6
  */
7
7
  import { basename, join, resolve } from 'path'
8
- import { existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, statSync } from 'fs'
8
+ import { existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, rmSync, statSync } from 'fs'
9
9
  import { ProgressManager } from './progress.js'
10
10
 
11
11
  /**
@@ -139,7 +139,9 @@ async function auditQuickCompletion(cwd, guard, options = {}) {
139
139
  return result
140
140
  }
141
141
 
142
- async function triggerSync(cwd, changeName) {
142
+ async function triggerSync(cwd, changeName, platformOpts = {}) {
143
+ // 平台模式(SillyHub)走自己的回传链路,不走 CLI 内置 sync
144
+ if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return
143
145
  try {
144
146
  if (changeName && !existsSync(join(cwd, '.sillyspec', 'changes', changeName))) return
145
147
  const syncMod = await import('./sync.js')
@@ -154,12 +156,13 @@ async function triggerSync(cwd, changeName) {
154
156
  * 审批检查辅助函数:execute 阶段启动前检查
155
157
  * @returns {{ status: string, reason?: string } | null}
156
158
  */
157
- async function checkApproval(cwd, changeName) {
159
+ async function checkApproval(cwd, changeName, platformOpts = {}) {
160
+ // 平台模式不需要 CLI 内置审批检查
161
+ if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return null
158
162
  try {
159
163
  const syncMod = await import('./sync.js')
160
164
  return await syncMod.checkApproval(changeName, cwd)
161
165
  } catch (e) {
162
- // sync.js 不存在或检查失败,静默跳过
163
166
  return null
164
167
  }
165
168
  }
@@ -474,20 +477,13 @@ export async function runCommand(args, cwd, specDir = null) {
474
477
  // 跨 --done 生命周期:从 metadata 文件恢复 platformOpts
475
478
  // 首次 scan 时写入,所有后续调用(包括 run、--done、--skip)都读取
476
479
  // 优先在 specDir 下查找,否则回退到 cwd/.sillyspec/.runtime/
477
- const specRoot = platformOpts.specRoot || resolveSpecDir(cwd)
478
- // platform-scan.json 搜索策略(多路径兼容):
479
- // 平台模式首次 scan 写入 specRoot/.runtime/,但后续 --done 可能不带 --spec-root
480
- // 需要在多个候选位置搜索
481
- const candidatePaths = []
482
- if (platformOpts.specRoot) {
483
- candidatePaths.push(join(platformOpts.specRoot, '.runtime', 'platform-scan.json'))
484
- }
485
- if (resolvedSpecDir) {
486
- candidatePaths.push(join(resolve(resolvedSpecDir), '.runtime', 'platform-scan.json'))
487
- }
488
- candidatePaths.push(join(specRoot, '.runtime', 'platform-scan.json')) // cwd/.sillyspec/.runtime/
489
-
490
- let platformOptsFile = candidatePaths.find(p => existsSync(p)) || candidatePaths[0]
480
+ let specRoot = platformOpts.specRoot || resolveSpecDir(cwd)
481
+ // 平台参数恢复策略:
482
+ // 1. 优先检查 cwd/.sillyspec-platform.json(轻量指针文件,不污染 .sillyspec 结构)
483
+ // 2. 然后检查 specRoot/.runtime/platform-scan.json(首次 scan 写入)
484
+ const platformPointer = join(cwd, '.sillyspec-platform.json')
485
+ const platformScanFile = join(specRoot, '.runtime', 'platform-scan.json')
486
+ let platformOptsFile = existsSync(platformPointer) ? platformPointer : platformScanFile
491
487
  let platformFileExists = existsSync(platformOptsFile)
492
488
  // 如果命令行没传 spec-root,尝试从持久化文件恢复
493
489
  if (!platformOpts.specRoot && !platformOpts.runtimeRoot) {
@@ -506,6 +502,8 @@ export async function runCommand(args, cwd, specDir = null) {
506
502
  console.error(' 解决:重新运行首次 scan 并传入 --spec-root')
507
503
  process.exit(1)
508
504
  }
505
+ // 恢复成功:更新 specRoot(初始值可能是 cwd/.sillyspec,恢复后应为真实 specDir)
506
+ specRoot = platformOpts.specRoot || specRoot
509
507
  } catch (e) {
510
508
  console.error(`❌ 平台模式参数文件读取失败: ${platformOptsFile}`)
511
509
  console.error(` 错误: ${e.message}`)
@@ -528,10 +526,9 @@ export async function runCommand(args, cwd, specDir = null) {
528
526
  scanRunId: platformOpts.scanRunId,
529
527
  savedAt: new Date().toISOString(),
530
528
  }, null, 2) + '\n')
531
- // 恢复指针:在 cwd/.sillyspec/.runtime/ 也写一份,供后续 --done(不带 --spec-root)找到
532
- const cwdRuntimeDir = join(cwd, '.sillyspec', '.runtime')
533
- mkdirSync(cwdRuntimeDir, { recursive: true })
534
- writeFileSync(join(cwdRuntimeDir, 'platform-scan.json'), JSON.stringify({
529
+ // 恢复指针:在 cwd 下写 .sillyspec-platform.json(不在 .sillyspec 内,不污染源码结构)
530
+ // 供后续 --done(不带 --spec-root)找到 specDir
531
+ writeFileSync(join(cwd, '.sillyspec-platform.json'), JSON.stringify({
535
532
  specRoot: platformOpts.specRoot,
536
533
  runtimeRoot: platformOpts.runtimeRoot,
537
534
  workspaceId: platformOpts.workspaceId,
@@ -543,6 +540,18 @@ export async function runCommand(args, cwd, specDir = null) {
543
540
  }
544
541
  }
545
542
 
543
+ // 统一规范基路径:平台模式用 specRoot,本地模式用 cwd/.sillyspec
544
+ // runCommand 后续所有 .sillyspec/ 操作必须用 specBase
545
+ const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
546
+
547
+ // 平台模式:清理旧版本残留的 cwd/.sillyspec/(防止源码污染)
548
+ if (platformOpts.specRoot) {
549
+ const legacyDir = join(cwd, '.sillyspec')
550
+ if (existsSync(legacyDir)) {
551
+ try { rmSync(legacyDir, { recursive: true, force: true }) } catch {}
552
+ }
553
+ }
554
+
546
555
  // 解析 --output
547
556
  let outputText = null
548
557
  const outputIdx = flags.indexOf('--output')
@@ -654,7 +663,7 @@ export async function runCommand(args, cwd, specDir = null) {
654
663
  const changed = await ensureStageSteps(progress, stageName, cwd, specRoot)
655
664
  if (changed && effectiveChange) {
656
665
  await pm._write(cwd, progress, effectiveChange)
657
- triggerSync(cwd, effectiveChange)
666
+ triggerSync(cwd, effectiveChange, platformOpts)
658
667
  progress = await pm.read(cwd, effectiveChange) || progress
659
668
  }
660
669
 
@@ -704,7 +713,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
704
713
 
705
714
  // execute 阶段启动前检查审批
706
715
  if (stageName === 'execute' && !skipApproval) {
707
- const approval = await checkApproval(cwd, changeName)
716
+ const approval = await checkApproval(cwd, changeName, platformOpts)
708
717
  if (approval) {
709
718
  if (approval.status === 'rejected') {
710
719
  console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
@@ -721,7 +730,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
721
730
  if (autoDetectChange(progress, cwd)) {
722
731
  progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
723
732
  await pm._write(cwd, progress, changeName)
724
- triggerSync(cwd, changeName)
733
+ triggerSync(cwd, changeName, platformOpts)
725
734
  }
726
735
 
727
736
  const stageData = progress.stages[stageName]
@@ -735,7 +744,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
735
744
  progress.currentStage = stageName
736
745
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
737
746
  await pm._write(cwd, progress, changeName)
738
- triggerSync(cwd, changeName)
747
+ triggerSync(cwd, changeName, platformOpts)
739
748
  }
740
749
 
741
750
  const steps = stageData.steps
@@ -751,7 +760,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
751
760
  stageData.startedAt = new Date().toLocaleString('zh-CN', { hour12: false })
752
761
  stageData.completedAt = null
753
762
  await pm._write(cwd, progress, changeName)
754
- triggerSync(cwd, changeName)
763
+ triggerSync(cwd, changeName, platformOpts)
755
764
  currentIdx = 0
756
765
  console.log(`🔄 ${stageName} 阶段已自动重置,重新开始。\n`)
757
766
  }
@@ -777,7 +786,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
777
786
  startedAt: new Date().toISOString(),
778
787
  }
779
788
  // 写入 quick-guard.json 供 worktree-guard hook 读取
780
- const guardFile = join(cwd, '.sillyspec', '.runtime', 'quick-guard.json')
789
+ const guardFile = join(specBase, '.runtime', 'quick-guard.json')
781
790
  writeFileSync(guardFile, JSON.stringify(progress.quickGuard, null, 2))
782
791
  const parts = [`${baselineFiles.length} 个已有脏文件`]
783
792
  if (allowedFiles.length > 0) parts.push(`${allowedFiles.length} 个 allowedFiles`)
@@ -802,8 +811,8 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
802
811
  }
803
812
  }
804
813
 
805
- function validateMetadata(cwd, stageName) {
806
- const changesDir = join(cwd, '.sillyspec', 'changes')
814
+ function validateMetadata(cwd, stageName, specBase) {
815
+ const changesDir = join(specBase, 'changes')
807
816
  if (!existsSync(changesDir)) return
808
817
 
809
818
  const cutoff = Date.now() - 10 * 60 * 1000
@@ -837,11 +846,11 @@ function validateMetadata(cwd, stageName) {
837
846
  * 验证关键文件是否存在于正确的变更目录下
838
847
  * 防止 AI 将文件写到错误的路径
839
848
  */
840
- function validateFileLocations(cwd, stageName, progress, changeName) {
849
+ function validateFileLocations(cwd, stageName, progress, changeName, specBase) {
841
850
  const effectiveChange = changeName || progress.currentChange
842
851
  if (!effectiveChange) return
843
852
 
844
- const changeDir = join(cwd, '.sillyspec', 'changes', effectiveChange)
853
+ const changeDir = join(specBase, 'changes', effectiveChange)
845
854
  if (!existsSync(changeDir)) return
846
855
 
847
856
  // 每个阶段完成后预期存在的文件
@@ -867,7 +876,7 @@ function validateFileLocations(cwd, stageName, progress, changeName) {
867
876
  console.log(` 变更目录:${changeDir.replace(cwd + '/', '')}/`)
868
877
  for (const f of missing) {
869
878
  // 检查是否写到了错误的位置
870
- const wrongPath = join(cwd, '.sillyspec', 'changes', 'change', effectiveChange, f)
879
+ const wrongPath = join(specBase, 'changes', 'change', effectiveChange, f)
871
880
  if (existsSync(wrongPath)) {
872
881
  console.log(` ❌ ${f} — 不存在,但发现了错误路径:${wrongPath.replace(cwd + '/', '')}`)
873
882
  console.log(` 提示:应该写入 ${changeDir.replace(cwd + '/', '')}/${f}`)
@@ -887,7 +896,7 @@ async function archiveChangeDirectory(pm, cwd, progress) {
887
896
  console.error('❌ 归档失败:未找到当前变更名(currentChange)')
888
897
  process.exit(1)
889
898
  }
890
- const changesDir = join(cwd, '.sillyspec', 'changes')
899
+ const changesDir = join(specBase, 'changes')
891
900
  const archiveDir = join(changesDir, 'archive')
892
901
  const srcDir = join(changesDir, archiveChangeName)
893
902
  const date = new Date().toISOString().slice(0, 10)
@@ -915,6 +924,7 @@ async function archiveChangeDirectory(pm, cwd, progress) {
915
924
 
916
925
  async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
917
926
  const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
927
+ const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
918
928
  const stageData = progress.stages[stageName]
919
929
  if (!stageData || !stageData.steps) {
920
930
  console.error(`❌ 阶段 ${stageName} 未初始化`)
@@ -938,7 +948,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
938
948
  // 平台模式:artifact 写入 runtime-root,否则写 .sillyspec/.runtime/artifacts
939
949
  const artifactBase = platformOpts?.runtimeRoot
940
950
  ? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown')
941
- : join(cwd, '.sillyspec', '.runtime', 'artifacts')
951
+ : join(specBase, '.runtime', 'artifacts')
942
952
  mkdirSync(artifactBase, { recursive: true })
943
953
  const ts = new Date().toISOString().slice(0,19).replace(/[-T:]/g, '')
944
954
  writeFileSync(join(artifactBase, `${changeName || 'unknown'}-${stageName}-step${currentIdx + 1}-${ts}.txt`), outputText)
@@ -1010,16 +1020,33 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1010
1020
  // 解析项目列表:从 step 2 输出提取,或回退读取 projects/*.yaml
1011
1021
  let projectNames = []
1012
1022
  if (outputText) {
1013
- // 匹配 "1. project-name" 格式
1014
- const matches = outputText.match(/^\s*\d+\.\s+(\S+)/gm)
1015
- if (matches) {
1016
- projectNames = matches.map(m => m.replace(/^\s*\d+\.\s+/, '').replace(/[—\-:].*$/, '').trim())
1023
+ // 匹配方式 1: "1. project-name" 编号列表
1024
+ const numbered = outputText.match(/^\s*\d+\.\s+(\S+)/gm)
1025
+ if (numbered) {
1026
+ projectNames = numbered.map(m => m.replace(/^\s*\d+\.\s+/, '').replace(/[—\-:].*$/, '').trim())
1027
+ }
1028
+ // 匹配方式 2: 括号枚举 "子项目frontend/order-service/user-service" 或 "项目: a, b, c"
1029
+ if (projectNames.length === 0) {
1030
+ const parenMatch = outputText.match(/(?:子项目|项目)[\s::]*(\S+(?:[\/、,,]+\S+)*)/)
1031
+ if (parenMatch) {
1032
+ projectNames = parenMatch[1]
1033
+ .split(/[\/、,,]+/)
1034
+ .map(s => s.trim())
1035
+ .filter(Boolean)
1036
+ }
1037
+ }
1038
+ // 匹配方式 3: 结构化 YAML block "scan_projects:\n - id: name"
1039
+ if (projectNames.length === 0) {
1040
+ const yamlMatch = outputText.match(/scan_projects:\s*\n((?:\s+-\s+id:\s+\S+\s*\n?)+)/)
1041
+ if (yamlMatch) {
1042
+ projectNames = [...yamlMatch[1].matchAll(/-\s+id:\s*(\S+)/g)].map(m => m[1])
1043
+ }
1017
1044
  }
1018
1045
  }
1019
1046
  if (projectNames.length === 0) {
1020
1047
  // 回退:读取所有已注册项目
1021
1048
  console.warn('⚠️ 未能从 step 2 输出解析项目列表,回退扫描所有注册项目')
1022
- const projectsDir = join(cwd, '.sillyspec', 'projects')
1049
+ const projectsDir = join(specBase, 'projects')
1023
1050
  if (existsSync(projectsDir)) {
1024
1051
  projectNames = readdirSync(projectsDir)
1025
1052
  .filter(f => f.endsWith('.yaml'))
@@ -1031,8 +1058,8 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1031
1058
  }
1032
1059
 
1033
1060
  // 保存到 runtime 供后续使用 + 防重复展开
1034
- const scanStatePath = join(cwd, '.sillyspec', '.runtime', 'scan-projects.json')
1035
- mkdirSync(join(cwd, '.sillyspec', '.runtime'), { recursive: true })
1061
+ const scanStatePath = join(specBase, '.runtime', 'scan-projects.json')
1062
+ mkdirSync(join(specBase, '.runtime'), { recursive: true })
1036
1063
  let scanState = { projects: projectNames, expanded: false }
1037
1064
  if (existsSync(scanStatePath)) {
1038
1065
  try { scanState = JSON.parse(readFileSync(scanStatePath, 'utf8')) } catch {}
@@ -1051,14 +1078,14 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1051
1078
  let insertPos = insertBase
1052
1079
  for (const pName of projectNames) {
1053
1080
  // 读取项目配置获取 projectRoot
1054
- const projYaml = join(cwd, '.sillyspec', 'projects', `${pName}.yaml`)
1081
+ const projYaml = join(specBase, 'projects', `${pName}.yaml`)
1055
1082
  let projectRoot = '.'
1056
1083
  if (existsSync(projYaml)) {
1057
1084
  const yamlContent = readFileSync(projYaml, 'utf8')
1058
1085
  const pathMatch = yamlContent.match(/^path:\s*(.+)/m)
1059
1086
  if (pathMatch) projectRoot = pathMatch[1].trim()
1060
1087
  }
1061
- const docOutputDir = `.sillyspec/docs/${pName}`
1088
+ const docOutputDir = platformOpts.specRoot ? `${specBase}/docs/${pName}` : `.sillyspec/docs/${pName}`
1062
1089
  const contextPrefix = `\n---\n## 当前项目\n- **项目名**: ${pName}\n- **项目路径**: ${projectRoot}\n- **文档输出**: ${docOutputDir}\n\n⚠️ 本步骤只处理上面这个项目,不要处理其他项目。\n---\n\n`
1063
1090
 
1064
1091
  for (const ppStep of perProjectSteps) {
@@ -1094,11 +1121,11 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1094
1121
  stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
1095
1122
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
1096
1123
  await pm._write(cwd, progress, changeName)
1097
- triggerSync(cwd, changeName)
1124
+ triggerSync(cwd, changeName, platformOpts)
1098
1125
 
1099
1126
  // Append to user-inputs.md
1100
1127
  if (outputText) {
1101
- const inputsPath = join(cwd, '.sillyspec', '.runtime', 'user-inputs.md')
1128
+ const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
1102
1129
  const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
1103
1130
  appendFileSync(inputsPath, entry)
1104
1131
  }
@@ -1109,7 +1136,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1109
1136
  const { mkdirSync, writeFileSync } = await import('fs')
1110
1137
  const { join } = await import('path')
1111
1138
  const { execSync } = await import('child_process')
1112
- const manifestDir = join(platformOpts.specRoot, '.sillyspec')
1139
+ const manifestDir = platformOpts.specRoot
1113
1140
  mkdirSync(manifestDir, { recursive: true })
1114
1141
  let sourceCommit = null
1115
1142
  try {
@@ -1130,7 +1157,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1130
1157
  }
1131
1158
  // 清理平台参数临时文件
1132
1159
  const { unlinkSync } = await import('fs')
1133
- const platformOptsFile = join(specRoot, '.runtime', 'platform-scan.json')
1160
+ const platformOptsFile = join(manifestDir, '.runtime', 'platform-scan.json')
1134
1161
  try { unlinkSync(platformOptsFile) } catch {}
1135
1162
 
1136
1163
  // CLI 层 post-check(替代旧的简单检查)
@@ -1176,10 +1203,10 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1176
1203
  printScanPostCheckResult(postResult)
1177
1204
  }
1178
1205
 
1179
- validateMetadata(cwd, stageName)
1206
+ validateMetadata(cwd, stageName, specBase)
1180
1207
 
1181
1208
  // 验证关键文件是否在正确的变更目录下
1182
- validateFileLocations(cwd, stageName, progress, changeName)
1209
+ validateFileLocations(cwd, stageName, progress, changeName, specBase)
1183
1210
 
1184
1211
  // 辅助阶段完成后重置步骤
1185
1212
  const stageDef = stageRegistry[stageName]
@@ -1251,7 +1278,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1251
1278
  // 清理 quick-guard.json
1252
1279
  try {
1253
1280
  const { unlinkSync } = await import('fs')
1254
- const guardFile = join(cwd, '.sillyspec', '.runtime', 'quick-guard.json')
1281
+ const guardFile = join(specBase, '.runtime', 'quick-guard.json')
1255
1282
  unlinkSync(guardFile)
1256
1283
  } catch {}
1257
1284
  if (review.status === 'blocked') {
@@ -1272,11 +1299,11 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1272
1299
 
1273
1300
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
1274
1301
  await pm._write(cwd, progress, changeName)
1275
- triggerSync(cwd, changeName)
1302
+ triggerSync(cwd, changeName, platformOpts)
1276
1303
 
1277
1304
  // Append to user-inputs.md
1278
1305
  if (outputText) {
1279
- const inputsPath = join(cwd, '.sillyspec', '.runtime', 'user-inputs.md')
1306
+ const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
1280
1307
  const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
1281
1308
  appendFileSync(inputsPath, entry)
1282
1309
  }
@@ -1302,7 +1329,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1302
1329
  projectsToCheck = [currentProjectName]
1303
1330
  } else {
1304
1331
  // 兼容旧模式(未展开):检查所有项目
1305
- const projectsDir = join(cwd, '.sillyspec', 'projects')
1332
+ const projectsDir = join(specBase, 'projects')
1306
1333
  const projectFiles = existsSync(projectsDir)
1307
1334
  ? readdirSync(projectsDir).filter(f => f.endsWith('.yaml'))
1308
1335
  : []
@@ -1392,7 +1419,7 @@ async function skipStep(pm, progress, stageName, cwd, changeName) {
1392
1419
  steps[currentIdx].skippedAt = new Date().toLocaleString('zh-CN',{hour12:false})
1393
1420
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
1394
1421
  await pm._write(cwd, progress, changeName)
1395
- triggerSync(cwd, changeName)
1422
+ triggerSync(cwd, changeName, platformOpts)
1396
1423
 
1397
1424
  console.log(`⏭️ Step ${currentIdx + 1}/${steps.length} 已跳过:${steps[currentIdx].name}`)
1398
1425
 
@@ -1455,7 +1482,7 @@ async function resetStage(pm, progress, stageName, cwd, changeName) {
1455
1482
  }
1456
1483
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
1457
1484
  await pm._write(cwd, progress, changeName)
1458
- triggerSync(cwd, changeName)
1485
+ triggerSync(cwd, changeName, platformOpts)
1459
1486
  console.log(`🔄 ${stageName} 阶段已重置`)
1460
1487
  }
1461
1488
 
@@ -1481,7 +1508,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
1481
1508
  const changed = await ensureStageSteps(progress, stage, cwd)
1482
1509
  if (stageChanged || changed) {
1483
1510
  await pm._write(cwd, progress, changeName)
1484
- triggerSync(cwd, changeName)
1511
+ triggerSync(cwd, changeName, platformOpts)
1485
1512
  }
1486
1513
  progress = await pm.read(cwd, changeName)
1487
1514
  return progress
@@ -1532,7 +1559,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
1532
1559
  }
1533
1560
  // execute 阶段启动前检查审批
1534
1561
  if (currentStage === 'execute' && !skipApproval) {
1535
- const approval = await checkApproval(cwd, changeName)
1562
+ const approval = await checkApproval(cwd, changeName, platformOpts)
1536
1563
  if (approval) {
1537
1564
  if (approval.status === 'rejected') {
1538
1565
  console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
@@ -1562,7 +1589,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
1562
1589
  const defSteps = await getStageSteps(currentStage, cwd, progress)
1563
1590
  // execute 阶段启动前检查审批
1564
1591
  if (currentStage === 'execute' && !skipApproval) {
1565
- const approval = await checkApproval(cwd, changeName)
1592
+ const approval = await checkApproval(cwd, changeName, platformOpts)
1566
1593
  if (approval) {
1567
1594
  if (approval.status === 'rejected') {
1568
1595
  console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
@@ -1595,7 +1622,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
1595
1622
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
1596
1623
  await ensureStageSteps(progress, next, cwd)
1597
1624
  await pm._write(cwd, progress, changeName)
1598
- triggerSync(cwd, changeName)
1625
+ triggerSync(cwd, changeName, platformOpts)
1599
1626
  progress = await pm.read(cwd, changeName)
1600
1627
 
1601
1628
  console.log(`\n${currentStage} complete. Auto advanced to ${next}.`)
@@ -1604,7 +1631,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
1604
1631
  if (firstPending !== -1) {
1605
1632
  // execute 阶段启动前检查审批
1606
1633
  if (next === 'execute' && !skipApproval) {
1607
- const approval = await checkApproval(cwd, changeName)
1634
+ const approval = await checkApproval(cwd, changeName, platformOpts)
1608
1635
  if (approval) {
1609
1636
  if (approval.status === 'rejected') {
1610
1637
  console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
@@ -60,19 +60,39 @@ console.log('\n=== Test 1: platform-scan.json 写入位置 ===')
60
60
  run(`node "${binCLI}" --dir "${cwd}" --spec-dir "${sd}" run scan --spec-root "${sd}" --runtime-root "${sd}/runtime" --workspace-id ws1 --scan-run-id sr1`)
61
61
 
62
62
  const inSpecDir = join(sd, '.runtime', 'platform-scan.json')
63
- const inCwd = join(cwd, '.sillyspec', '.runtime', 'platform-scan.json')
63
+ const pointerFile = join(cwd, '.sillyspec-platform.json')
64
64
  assert(existsSync(inSpecDir), `platform-scan.json 在 specDir/.runtime/`)
65
- assert(existsSync(inCwd), `恢复指针也写入 cwd/.sillyspec/.runtime/`)
65
+ assert(existsSync(pointerFile), `恢复指针在 cwd/.sillyspec-platform.json(不在 .sillyspec 内)`)
66
66
 
67
67
  const content = JSON.parse(readFileSync(inSpecDir, 'utf8'))
68
68
  assert(content.specRoot === sd, `specRoot 指向 specDir`)
69
69
  assert(content.workspaceId === 'ws1', `workspaceId 保存正确`)
70
70
  assert(content.scanRunId === 'sr1', `scanRunId 保存正确`)
71
+ // 关键:cwd/.sillyspec/ 不应被创建
72
+ assert(!existsSync(join(cwd, '.sillyspec')), `cwd/.sillyspec/ 未被创建(源码零污染)`)
71
73
  clean(cwd, sd)
72
74
  }
73
75
 
74
- // ── Test 2: --done 不带 --spec-root 时恢复 ──
75
- console.log('\n=== Test 2: --done 恢复平台参数 ===')
76
+ // ── Test 2: 残留清理:旧版本创建的 cwd/.sillyspec 会被自动删除 ──
77
+ console.log('\n=== Test 2: 旧版本残留清理 ===')
78
+ {
79
+ const cwd = setup('t2'), sd = spec('t2')
80
+ // 模拟旧版本创建的残留
81
+ mkdirSync(join(cwd, '.sillyspec', '.runtime'), { recursive: true })
82
+ writeFileSync(join(cwd, '.sillyspec', '.runtime', 'old.db'), 'x')
83
+ mkdirSync(join(cwd, '.sillyspec', 'changes'), { recursive: true })
84
+ assert(existsSync(join(cwd, '.sillyspec')), `残留存在`)
85
+ // init 时应清理
86
+ run(`node "${binCLI}" init "${cwd}" --spec-dir "${sd}"`)
87
+ assert(!existsSync(join(cwd, '.sillyspec')), `init 清理了 cwd/.sillyspec/`)
88
+ // run 时也不应再创建
89
+ run(`node "${binCLI}" --dir "${cwd}" --spec-dir "${sd}" run scan --spec-root "${sd}" --runtime-root "${sd}/runtime" --workspace-id ws --scan-run-id sr 2>&1`)
90
+ assert(!existsSync(join(cwd, '.sillyspec')), `run 后 cwd/.sillyspec/ 仍不存在`)
91
+ clean(cwd, sd)
92
+ }
93
+
94
+ // ── Test 3: --done 不带 --spec-root 时恢复 ──
95
+ console.log('\n=== Test 3: --done 恢复平台参数 ===')
76
96
  {
77
97
  const cwd = setup('t2'), sd = spec('t2')
78
98
  run(`node "${binCLI}" init "${cwd}" --spec-dir "${sd}"`)
@@ -87,7 +107,7 @@ console.log('\n=== Test 2: --done 恢复平台参数 ===')
87
107
  // ── Test 3-6: stage-contract 路径(通过 runValidators) ──
88
108
  const { runValidators } = await import(pathToFileURL(join(root, 'src', 'stage-contract.js')).href)
89
109
 
90
- console.log('\n=== Test 3: specDir 有文档 → 校验通过 ===')
110
+ console.log('\n=== Test 5: specDir 有文档 → 校验通过 ===')
91
111
  {
92
112
  const cwd = setup('t3'), sd = spec('t3')
93
113
  const proj = basename(cwd)
@@ -98,7 +118,7 @@ console.log('\n=== Test 3: specDir 有文档 → 校验通过 ===')
98
118
  clean(cwd, sd)
99
119
  }
100
120
 
101
- console.log('\n=== Test 4: specDir 缺文档 → 校验失败,路径不含 .sillyspec ===')
121
+ console.log('\n=== Test 5: specDir 缺文档 → 校验失败,路径不含 .sillyspec ===')
102
122
  {
103
123
  const cwd = setup('t4'), sd = spec('t4')
104
124
  const proj = basename(cwd)
@@ -112,7 +132,7 @@ console.log('\n=== Test 4: specDir 缺文档 → 校验失败,路径不含 .si
112
132
  clean(cwd, sd)
113
133
  }
114
134
 
115
- console.log('\n=== Test 5: 非平台模式有文档 → 校验通过 ===')
135
+ console.log('\n=== Test 7: 非平台模式有文档 → 校验通过 ===')
116
136
  {
117
137
  const cwd = setup('t5')
118
138
  const proj = basename(cwd)
@@ -122,7 +142,7 @@ console.log('\n=== Test 5: 非平台模式有文档 → 校验通过 ===')
122
142
  clean(cwd)
123
143
  }
124
144
 
125
- console.log('\n=== Test 6: 非平台模式缺文档 → 路径含 .sillyspec ===')
145
+ console.log('\n=== Test 7: 非平台模式缺文档 → 路径含 .sillyspec ===')
126
146
  {
127
147
  const cwd = setup('t6')
128
148
  const proj = basename(cwd)