sillyspec 3.17.1 → 3.17.2

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.2",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/run.js CHANGED
@@ -475,19 +475,12 @@ export async function runCommand(args, cwd, specDir = null) {
475
475
  // 首次 scan 时写入,所有后续调用(包括 run、--done、--skip)都读取
476
476
  // 优先在 specDir 下查找,否则回退到 cwd/.sillyspec/.runtime/
477
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]
478
+ // 平台参数恢复策略:
479
+ // 1. 优先检查 cwd/.sillyspec-platform.json(轻量指针文件,不污染 .sillyspec 结构)
480
+ // 2. 然后检查 specRoot/.runtime/platform-scan.json(首次 scan 写入)
481
+ const platformPointer = join(cwd, '.sillyspec-platform.json')
482
+ const platformScanFile = join(specRoot, '.runtime', 'platform-scan.json')
483
+ let platformOptsFile = existsSync(platformPointer) ? platformPointer : platformScanFile
491
484
  let platformFileExists = existsSync(platformOptsFile)
492
485
  // 如果命令行没传 spec-root,尝试从持久化文件恢复
493
486
  if (!platformOpts.specRoot && !platformOpts.runtimeRoot) {
@@ -528,10 +521,9 @@ export async function runCommand(args, cwd, specDir = null) {
528
521
  scanRunId: platformOpts.scanRunId,
529
522
  savedAt: new Date().toISOString(),
530
523
  }, 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({
524
+ // 恢复指针:在 cwd 下写 .sillyspec-platform.json(不在 .sillyspec 内,不污染源码结构)
525
+ // 供后续 --done(不带 --spec-root)找到 specDir
526
+ writeFileSync(join(cwd, '.sillyspec-platform.json'), JSON.stringify({
535
527
  specRoot: platformOpts.specRoot,
536
528
  runtimeRoot: platformOpts.runtimeRoot,
537
529
  workspaceId: platformOpts.workspaceId,
@@ -543,6 +535,10 @@ export async function runCommand(args, cwd, specDir = null) {
543
535
  }
544
536
  }
545
537
 
538
+ // 统一规范基路径:平台模式用 specRoot,本地模式用 cwd/.sillyspec
539
+ // runCommand 后续所有 .sillyspec/ 操作必须用 specBase
540
+ const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
541
+
546
542
  // 解析 --output
547
543
  let outputText = null
548
544
  const outputIdx = flags.indexOf('--output')
@@ -777,7 +773,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
777
773
  startedAt: new Date().toISOString(),
778
774
  }
779
775
  // 写入 quick-guard.json 供 worktree-guard hook 读取
780
- const guardFile = join(cwd, '.sillyspec', '.runtime', 'quick-guard.json')
776
+ const guardFile = join(specBase, '.runtime', 'quick-guard.json')
781
777
  writeFileSync(guardFile, JSON.stringify(progress.quickGuard, null, 2))
782
778
  const parts = [`${baselineFiles.length} 个已有脏文件`]
783
779
  if (allowedFiles.length > 0) parts.push(`${allowedFiles.length} 个 allowedFiles`)
@@ -802,8 +798,8 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
802
798
  }
803
799
  }
804
800
 
805
- function validateMetadata(cwd, stageName) {
806
- const changesDir = join(cwd, '.sillyspec', 'changes')
801
+ function validateMetadata(cwd, stageName, specBase) {
802
+ const changesDir = join(specBase, 'changes')
807
803
  if (!existsSync(changesDir)) return
808
804
 
809
805
  const cutoff = Date.now() - 10 * 60 * 1000
@@ -837,11 +833,11 @@ function validateMetadata(cwd, stageName) {
837
833
  * 验证关键文件是否存在于正确的变更目录下
838
834
  * 防止 AI 将文件写到错误的路径
839
835
  */
840
- function validateFileLocations(cwd, stageName, progress, changeName) {
836
+ function validateFileLocations(cwd, stageName, progress, changeName, specBase) {
841
837
  const effectiveChange = changeName || progress.currentChange
842
838
  if (!effectiveChange) return
843
839
 
844
- const changeDir = join(cwd, '.sillyspec', 'changes', effectiveChange)
840
+ const changeDir = join(specBase, 'changes', effectiveChange)
845
841
  if (!existsSync(changeDir)) return
846
842
 
847
843
  // 每个阶段完成后预期存在的文件
@@ -867,7 +863,7 @@ function validateFileLocations(cwd, stageName, progress, changeName) {
867
863
  console.log(` 变更目录:${changeDir.replace(cwd + '/', '')}/`)
868
864
  for (const f of missing) {
869
865
  // 检查是否写到了错误的位置
870
- const wrongPath = join(cwd, '.sillyspec', 'changes', 'change', effectiveChange, f)
866
+ const wrongPath = join(specBase, 'changes', 'change', effectiveChange, f)
871
867
  if (existsSync(wrongPath)) {
872
868
  console.log(` ❌ ${f} — 不存在,但发现了错误路径:${wrongPath.replace(cwd + '/', '')}`)
873
869
  console.log(` 提示:应该写入 ${changeDir.replace(cwd + '/', '')}/${f}`)
@@ -887,7 +883,7 @@ async function archiveChangeDirectory(pm, cwd, progress) {
887
883
  console.error('❌ 归档失败:未找到当前变更名(currentChange)')
888
884
  process.exit(1)
889
885
  }
890
- const changesDir = join(cwd, '.sillyspec', 'changes')
886
+ const changesDir = join(specBase, 'changes')
891
887
  const archiveDir = join(changesDir, 'archive')
892
888
  const srcDir = join(changesDir, archiveChangeName)
893
889
  const date = new Date().toISOString().slice(0, 10)
@@ -915,6 +911,7 @@ async function archiveChangeDirectory(pm, cwd, progress) {
915
911
 
916
912
  async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
917
913
  const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
914
+ const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
918
915
  const stageData = progress.stages[stageName]
919
916
  if (!stageData || !stageData.steps) {
920
917
  console.error(`❌ 阶段 ${stageName} 未初始化`)
@@ -938,7 +935,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
938
935
  // 平台模式:artifact 写入 runtime-root,否则写 .sillyspec/.runtime/artifacts
939
936
  const artifactBase = platformOpts?.runtimeRoot
940
937
  ? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown')
941
- : join(cwd, '.sillyspec', '.runtime', 'artifacts')
938
+ : join(specBase, '.runtime', 'artifacts')
942
939
  mkdirSync(artifactBase, { recursive: true })
943
940
  const ts = new Date().toISOString().slice(0,19).replace(/[-T:]/g, '')
944
941
  writeFileSync(join(artifactBase, `${changeName || 'unknown'}-${stageName}-step${currentIdx + 1}-${ts}.txt`), outputText)
@@ -1019,7 +1016,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1019
1016
  if (projectNames.length === 0) {
1020
1017
  // 回退:读取所有已注册项目
1021
1018
  console.warn('⚠️ 未能从 step 2 输出解析项目列表,回退扫描所有注册项目')
1022
- const projectsDir = join(cwd, '.sillyspec', 'projects')
1019
+ const projectsDir = join(specBase, 'projects')
1023
1020
  if (existsSync(projectsDir)) {
1024
1021
  projectNames = readdirSync(projectsDir)
1025
1022
  .filter(f => f.endsWith('.yaml'))
@@ -1031,8 +1028,8 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1031
1028
  }
1032
1029
 
1033
1030
  // 保存到 runtime 供后续使用 + 防重复展开
1034
- const scanStatePath = join(cwd, '.sillyspec', '.runtime', 'scan-projects.json')
1035
- mkdirSync(join(cwd, '.sillyspec', '.runtime'), { recursive: true })
1031
+ const scanStatePath = join(specBase, '.runtime', 'scan-projects.json')
1032
+ mkdirSync(join(specBase, '.runtime'), { recursive: true })
1036
1033
  let scanState = { projects: projectNames, expanded: false }
1037
1034
  if (existsSync(scanStatePath)) {
1038
1035
  try { scanState = JSON.parse(readFileSync(scanStatePath, 'utf8')) } catch {}
@@ -1051,14 +1048,14 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1051
1048
  let insertPos = insertBase
1052
1049
  for (const pName of projectNames) {
1053
1050
  // 读取项目配置获取 projectRoot
1054
- const projYaml = join(cwd, '.sillyspec', 'projects', `${pName}.yaml`)
1051
+ const projYaml = join(specBase, 'projects', `${pName}.yaml`)
1055
1052
  let projectRoot = '.'
1056
1053
  if (existsSync(projYaml)) {
1057
1054
  const yamlContent = readFileSync(projYaml, 'utf8')
1058
1055
  const pathMatch = yamlContent.match(/^path:\s*(.+)/m)
1059
1056
  if (pathMatch) projectRoot = pathMatch[1].trim()
1060
1057
  }
1061
- const docOutputDir = `.sillyspec/docs/${pName}`
1058
+ const docOutputDir = platformOpts.specRoot ? `${specBase}/docs/${pName}` : `.sillyspec/docs/${pName}`
1062
1059
  const contextPrefix = `\n---\n## 当前项目\n- **项目名**: ${pName}\n- **项目路径**: ${projectRoot}\n- **文档输出**: ${docOutputDir}\n\n⚠️ 本步骤只处理上面这个项目,不要处理其他项目。\n---\n\n`
1063
1060
 
1064
1061
  for (const ppStep of perProjectSteps) {
@@ -1098,7 +1095,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1098
1095
 
1099
1096
  // Append to user-inputs.md
1100
1097
  if (outputText) {
1101
- const inputsPath = join(cwd, '.sillyspec', '.runtime', 'user-inputs.md')
1098
+ const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
1102
1099
  const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
1103
1100
  appendFileSync(inputsPath, entry)
1104
1101
  }
@@ -1109,7 +1106,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1109
1106
  const { mkdirSync, writeFileSync } = await import('fs')
1110
1107
  const { join } = await import('path')
1111
1108
  const { execSync } = await import('child_process')
1112
- const manifestDir = join(platformOpts.specRoot, '.sillyspec')
1109
+ const manifestDir = platformOpts.specRoot
1113
1110
  mkdirSync(manifestDir, { recursive: true })
1114
1111
  let sourceCommit = null
1115
1112
  try {
@@ -1176,10 +1173,10 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1176
1173
  printScanPostCheckResult(postResult)
1177
1174
  }
1178
1175
 
1179
- validateMetadata(cwd, stageName)
1176
+ validateMetadata(cwd, stageName, specBase)
1180
1177
 
1181
1178
  // 验证关键文件是否在正确的变更目录下
1182
- validateFileLocations(cwd, stageName, progress, changeName)
1179
+ validateFileLocations(cwd, stageName, progress, changeName, specBase)
1183
1180
 
1184
1181
  // 辅助阶段完成后重置步骤
1185
1182
  const stageDef = stageRegistry[stageName]
@@ -1251,7 +1248,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1251
1248
  // 清理 quick-guard.json
1252
1249
  try {
1253
1250
  const { unlinkSync } = await import('fs')
1254
- const guardFile = join(cwd, '.sillyspec', '.runtime', 'quick-guard.json')
1251
+ const guardFile = join(specBase, '.runtime', 'quick-guard.json')
1255
1252
  unlinkSync(guardFile)
1256
1253
  } catch {}
1257
1254
  if (review.status === 'blocked') {
@@ -1276,7 +1273,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1276
1273
 
1277
1274
  // Append to user-inputs.md
1278
1275
  if (outputText) {
1279
- const inputsPath = join(cwd, '.sillyspec', '.runtime', 'user-inputs.md')
1276
+ const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
1280
1277
  const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
1281
1278
  appendFileSync(inputsPath, entry)
1282
1279
  }
@@ -1302,7 +1299,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1302
1299
  projectsToCheck = [currentProjectName]
1303
1300
  } else {
1304
1301
  // 兼容旧模式(未展开):检查所有项目
1305
- const projectsDir = join(cwd, '.sillyspec', 'projects')
1302
+ const projectsDir = join(specBase, 'projects')
1306
1303
  const projectFiles = existsSync(projectsDir)
1307
1304
  ? readdirSync(projectsDir).filter(f => f.endsWith('.yaml'))
1308
1305
  : []
@@ -60,14 +60,16 @@ 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