sillyspec 3.15.2 → 3.16.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.
package/src/run.js CHANGED
@@ -147,7 +147,7 @@ async function ensureStageSteps(progress, stageName, cwd) {
147
147
  /**
148
148
  * 输出当前步骤的 prompt
149
149
  */
150
- async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName) {
150
+ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}) {
151
151
  const step = steps[stepIndex]
152
152
  const total = steps.length
153
153
  const projectName = dbProjectName || basename(cwd)
@@ -218,6 +218,43 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
218
218
  if (changeName && promptText.includes('<change-name>')) {
219
219
  promptText = promptText.replace(/<change-name>/g, changeName)
220
220
  }
221
+ // 平台模式:注入路径覆盖指令(仅 scan 阶段)
222
+ if (stageName === 'scan') {
223
+ const projectName = dbProjectName || basename(cwd)
224
+ const specSillyspec = platformOpts?.specRoot
225
+ ? join(platformOpts.specRoot, '.sillyspec')
226
+ : join(cwd, '.sillyspec')
227
+ const docsRoot = join(specSillyspec, 'docs', projectName)
228
+ const projectsRoot = join(specSillyspec, 'projects')
229
+
230
+ promptText = promptText.replace(/\{DOCS_ROOT\}/g, docsRoot)
231
+ promptText = promptText.replace(/\{PROJECTS_ROOT\}/g, projectsRoot)
232
+
233
+ // 平台模式附加指令
234
+ if (platformOpts?.specRoot || platformOpts?.runtimeRoot) {
235
+ const platformDirectives = []
236
+ if (platformOpts.specRoot) {
237
+ platformDirectives.push(
238
+ `## ⚠️ 平台模式\n` +
239
+ `文档路径已参数化:\n` +
240
+ `- 文档根目录: \`${docsRoot}/\`\n` +
241
+ `- 项目注册表: \`${projectsRoot}/\`\n` +
242
+ `创建目录: \`mkdir -p ${docsRoot}/{scan,modules,flows} ${projectsRoot}\`\n`
243
+ )
244
+ }
245
+ if (platformOpts.runtimeRoot) {
246
+ const scanRunId = platformOpts.scanRunId || 'scan-' + new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')
247
+ platformDirectives.push(
248
+ `运行时产物写入: \`${platformOpts.runtimeRoot}/scan-runs/${scanRunId}/\`\n`
249
+ )
250
+ }
251
+ if (platformOpts.workspaceId) {
252
+ platformDirectives.push(`workspace_id: ${platformOpts.workspaceId}`)
253
+ }
254
+ promptText = platformDirectives.join('\n') + '\n\n' + promptText
255
+ }
256
+ }
257
+
221
258
  console.log(promptText)
222
259
  console.log(`\n### ⚠️ 铁律`)
223
260
  console.log('- **文档是核心资产,代码是文档的产物。** 没有文档就没有代码——文档是 AI 的记忆,是团队协作的基础,是后续维护的唯一依据。任何代码产出必须先有对应的设计/规范文档支撑。')
@@ -265,6 +302,50 @@ export async function runCommand(args, cwd) {
265
302
  const isConfirm = flags.includes('--confirm')
266
303
  const isSkipApproval = flags.includes('--skip-approval')
267
304
 
305
+ // 平台模式参数(供 SillyHub 等平台调用)
306
+ const getFlagValue = (name) => {
307
+ const idx = flags.indexOf(name)
308
+ return idx !== -1 && flags[idx + 1] ? flags[idx + 1] : null
309
+ }
310
+ const platformOpts = {
311
+ specRoot: getFlagValue('--spec-root'),
312
+ runtimeRoot: getFlagValue('--runtime-root'),
313
+ workspaceId: getFlagValue('--workspace-id'),
314
+ scanRunId: getFlagValue('--scan-run-id'),
315
+ }
316
+
317
+ // 跨 --done 生命周期:优先从 metadata 文件恢复 platformOpts
318
+ // 首次 scan 时写入,后续 --done 读取
319
+ const platformOptsFile = join(cwd, '.sillyspec', '.runtime', 'platform-scan.json')
320
+ if (isDone || isSkip) {
321
+ // --done/--skip 阶段:从文件恢复
322
+ try {
323
+ const { readFileSync } = await import('fs')
324
+ const saved = JSON.parse(readFileSync(platformOptsFile, 'utf8'))
325
+ if (saved.specRoot) platformOpts.specRoot = saved.specRoot
326
+ if (saved.runtimeRoot) platformOpts.runtimeRoot = saved.runtimeRoot
327
+ if (saved.workspaceId) platformOpts.workspaceId = saved.workspaceId
328
+ if (saved.scanRunId) platformOpts.scanRunId = saved.scanRunId
329
+ } catch {
330
+ // 文件不存在,说明不是平台模式,跳过
331
+ }
332
+ } else if (platformOpts.specRoot || platformOpts.runtimeRoot) {
333
+ // 首次 scan:持久化 platformOpts
334
+ try {
335
+ const { mkdirSync, writeFileSync } = await import('fs')
336
+ mkdirSync(join(cwd, '.sillyspec', '.runtime'), { recursive: true })
337
+ writeFileSync(platformOptsFile, JSON.stringify({
338
+ specRoot: platformOpts.specRoot,
339
+ runtimeRoot: platformOpts.runtimeRoot,
340
+ workspaceId: platformOpts.workspaceId,
341
+ scanRunId: platformOpts.scanRunId,
342
+ savedAt: new Date().toISOString(),
343
+ }, null, 2) + '\n')
344
+ } catch {
345
+ // 静默失败,不影响主流程
346
+ }
347
+ }
348
+
268
349
  // 解析 --output
269
350
  let outputText = null
270
351
  const outputIdx = flags.indexOf('--output')
@@ -286,6 +367,26 @@ export async function runCommand(args, cwd) {
286
367
  changeName = flags[changeIdx + 1]
287
368
  }
288
369
 
370
+ // 未知参数 fail-fast
371
+ const knownFlags = new Set([
372
+ '--done', '--skip', '--status', '--reset', '--confirm', '--skip-approval',
373
+ '--output', '--input', '--change',
374
+ '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
375
+ '--json', '--dir', '--help',
376
+ ])
377
+ for (let i = 0; i < flags.length; i++) {
378
+ const f = flags[i]
379
+ if (f.startsWith('--')) {
380
+ if (!knownFlags.has(f)) {
381
+ console.error(`❌ 未知参数: ${f}`)
382
+ console.error(`已知参数: ${[...knownFlags].sort().join(', ')}`)
383
+ process.exit(1)
384
+ }
385
+ // 跳过 value 参数
386
+ i++
387
+ }
388
+ }
389
+
289
390
  const isAuxiliary = auxiliaryStages.includes(stageName)
290
391
 
291
392
  const pm = new ProgressManager()
@@ -361,11 +462,11 @@ export async function runCommand(args, cwd) {
361
462
 
362
463
  // --done
363
464
  if (isDone) {
364
- return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange })
465
+ return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, platformOpts })
365
466
  }
366
467
 
367
468
  // 默认:输出当前步骤
368
- return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval)
469
+ return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts)
369
470
  }
370
471
 
371
472
  /**
@@ -380,7 +481,7 @@ function resolveChangeNameAuto(cwd) {
380
481
  return null
381
482
  }
382
483
 
383
- async function runStage(pm, progress, stageName, cwd, changeName, skipApproval = false) {
484
+ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval = false, platformOpts = {}) {
384
485
  // execute 阶段启动前检查审批
385
486
  if (stageName === 'execute' && !skipApproval) {
386
487
  const approval = await checkApproval(cwd, changeName)
@@ -444,7 +545,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
444
545
 
445
546
  const defSteps = await getStageSteps(stageName, cwd, progress)
446
547
  if (defSteps && defSteps[currentIdx]) {
447
- await outputStep(stageName, currentIdx, defSteps, cwd, changeName, progress.project || null)
548
+ await outputStep(stageName, currentIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
448
549
  }
449
550
  }
450
551
 
@@ -527,7 +628,7 @@ function validateFileLocations(cwd, stageName, progress, changeName) {
527
628
  }
528
629
 
529
630
  async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
530
- const { printNext = true, confirm = false, changeName } = options
631
+ const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
531
632
  const stageData = progress.stages[stageName]
532
633
  if (!stageData || !stageData.steps) {
533
634
  console.error(`❌ 阶段 ${stageName} 未初始化`)
@@ -548,10 +649,13 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
548
649
  const MAX_OUTPUT = 200
549
650
  if (outputText.length > MAX_OUTPUT) {
550
651
  steps[currentIdx].output = outputText.slice(0, MAX_OUTPUT) + '…'
551
- const artifactsDir = join(cwd, '.sillyspec', '.runtime', 'artifacts')
552
- mkdirSync(artifactsDir, { recursive: true })
652
+ // 平台模式:artifact 写入 runtime-root,否则写 .sillyspec/.runtime/artifacts
653
+ const artifactBase = platformOpts?.runtimeRoot
654
+ ? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown')
655
+ : join(cwd, '.sillyspec', '.runtime', 'artifacts')
656
+ mkdirSync(artifactBase, { recursive: true })
553
657
  const ts = new Date().toISOString().slice(0,19).replace(/[-T:]/g, '')
554
- writeFileSync(join(artifactsDir, `${changeName || 'unknown'}-${stageName}-step${currentIdx + 1}-${ts}.txt`), outputText)
658
+ writeFileSync(join(artifactBase, `${changeName || 'unknown'}-${stageName}-step${currentIdx + 1}-${ts}.txt`), outputText)
555
659
  } else {
556
660
  steps[currentIdx].output = outputText
557
661
  }
@@ -683,6 +787,56 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
683
787
  appendFileSync(inputsPath, entry)
684
788
  }
685
789
 
790
+ // 平台模式:scan 完成后生成 manifest.json
791
+ if (stageName === 'scan' && platformOpts.specRoot) {
792
+ try {
793
+ const { mkdirSync, writeFileSync } = await import('fs')
794
+ const { join } = await import('path')
795
+ const { execSync } = await import('child_process')
796
+ const manifestDir = join(platformOpts.specRoot, '.sillyspec')
797
+ mkdirSync(manifestDir, { recursive: true })
798
+ let sourceCommit = null
799
+ try {
800
+ sourceCommit = execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', timeout: 5000 }).trim()
801
+ } catch {}
802
+ const manifest = {
803
+ workspace_id: platformOpts.workspaceId || null,
804
+ scan_run_id: platformOpts.scanRunId || null,
805
+ source_commit: sourceCommit,
806
+ generated_at: new Date().toISOString(),
807
+ schema_version: 1,
808
+ }
809
+ const manifestPath = join(manifestDir, 'manifest.json')
810
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n')
811
+ console.log(`📄 manifest.json 已写入: ${manifestPath}`)
812
+ if (!sourceCommit) {
813
+ console.log(`⚠️ source_commit 无法获取(可能非 git 目录),已设为 null`)
814
+ }
815
+ // 清理平台参数临时文件
816
+ const { unlinkSync } = await import('fs')
817
+ const platformOptsFile = join(cwd, '.sillyspec', '.runtime', 'platform-scan.json')
818
+ try { unlinkSync(platformOptsFile) } catch {}
819
+
820
+ // 平台模式后置校验:检查 source_root 是否被污染
821
+ if (platformOpts.specRoot) {
822
+ const { readdirSync } = await import('fs')
823
+ const localDocsDir = join(cwd, '.sillyspec', 'docs')
824
+ try {
825
+ if (existsSync(localDocsDir)) {
826
+ const entries = readdirSync(localDocsDir, { recursive: true }).filter(e => e.endsWith('.md'))
827
+ if (entries.length > 0) {
828
+ console.warn(`⚠️ 平台模式后置校验:source_root 下存在 ${entries.length} 个文档文件:`)
829
+ console.warn(` 路径:${localDocsDir}/`)
830
+ console.warn(` 可能原因:agent 未遵守路径覆盖,将文档写入到了 cwd 而非 spec-root`)
831
+ }
832
+ }
833
+ } catch {}
834
+ }
835
+ } catch (e) {
836
+ console.warn(`⚠️ manifest.json 写入失败: ${e.message}`)
837
+ }
838
+ }
839
+
686
840
  validateMetadata(cwd, stageName)
687
841
 
688
842
  // 验证关键文件是否在正确的变更目录下
@@ -862,7 +1016,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
862
1016
  }
863
1017
 
864
1018
  if (printNext) {
865
- await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null)
1019
+ await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
866
1020
  }
867
1021
  return { stageCompleted: false, currentIdx, nextPendingIdx }
868
1022
  }
@@ -900,7 +1054,7 @@ async function skipStep(pm, progress, stageName, cwd, changeName) {
900
1054
  const nextPendingIdx = steps.findIndex(s => s.status === 'pending')
901
1055
  if (nextPendingIdx !== -1 && defSteps) {
902
1056
  console.log('')
903
- await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null)
1057
+ await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
904
1058
  }
905
1059
  }
906
1060
 
@@ -1045,7 +1199,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
1045
1199
  }
1046
1200
  }
1047
1201
  }
1048
- await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null)
1202
+ await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
1049
1203
  return
1050
1204
  }
1051
1205
 
@@ -1054,7 +1208,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
1054
1208
  process.exit(1)
1055
1209
  }
1056
1210
 
1057
- const result = await completeStep(pm, progress, currentStage, cwd, outputText, inputText, { printNext: false, changeName })
1211
+ const result = await completeStep(pm, progress, currentStage, cwd, outputText, inputText, { printNext: false, changeName, platformOpts })
1058
1212
  if (!result) return
1059
1213
  progress = await pm.read(cwd, changeName)
1060
1214
 
@@ -1075,7 +1229,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
1075
1229
  }
1076
1230
  }
1077
1231
  }
1078
- await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null)
1232
+ await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
1079
1233
  return
1080
1234
  }
1081
1235
 
@@ -1117,6 +1271,6 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
1117
1271
  }
1118
1272
  }
1119
1273
  }
1120
- await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null)
1274
+ await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null, platformOpts)
1121
1275
  }
1122
1276
  }
@@ -91,6 +91,48 @@ for f in .sillyspec/projects/*.yaml; do
91
91
  done
92
92
  \`\`\`
93
93
 
94
+ ### 6. Worktree 隔离环境检查
95
+ \`\`\`bash
96
+ # 检测当前目录是否在 submodule 中
97
+ SUPERPROJECT=$(git rev-parse --show-superproject-working-tree 2>/dev/null)
98
+ if [ -n "$SUPERPROJECT" ]; then
99
+ echo "⚠️ 当前目录在 git submodule 内,worktree 隔离不可用"
100
+ else
101
+ echo "✅ 不在 submodule 中"
102
+ fi
103
+
104
+ # 检测是否已在 linked worktree 中
105
+ GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
106
+ GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
107
+ if [ "$GIT_DIR" != "$GIT_COMMON" ] && [ -z "$SUPERPROJECT" ]; then
108
+ echo "✅ 已在 linked worktree 中"
109
+ else
110
+ echo "ℹ️ 在主仓库中(非 worktree)"
111
+ fi
112
+
113
+ # 检查 worktree 存储目录是否被 .gitignore 忽略
114
+ WT_DIR='.sillyspec/.runtime/worktrees'
115
+ if git check-ignore -q "$WT_DIR" 2>/dev/null; then
116
+ echo "✅ worktree 目录已被 .gitignore 忽略 ($WT_DIR)"
117
+ else
118
+ echo "❌ worktree 目录未被 .gitignore 忽略 ($WT_DIR) — worktree 创建将被阻断"
119
+ echo " 修复: 在 .gitignore 中添加 $WT_DIR/"
120
+ fi
121
+
122
+ # 检查 DB 中的 isolation 状态
123
+ DB_FILE='.sillyspec/.runtime/sillyspec.db'
124
+ if [ -f "$DB_FILE" ]; then
125
+ echo ""
126
+ echo "isolation 状态(来自 sillyspec.db):"
127
+ sqlite3 -header -column "$DB_FILE" "SELECT name, isolation_status AS status, isolation_mode AS mode, isolation_reason AS reason FROM changes WHERE status='active'" 2>/dev/null || echo "⚠️ 查询 isolation 失败"
128
+ else
129
+ echo ""
130
+ echo "ℹ️ sillyspec.db 不存在(尚未初始化)"
131
+ fi
132
+ else
133
+ echo "ℹ️ gate-status.json 不存在(尚未进入 execute 阶段)"
134
+ fi
135
+ \`\`\`\n
94
136
  ### 输出
95
137
  汇总所有检查结果,按以下格式:
96
138
  \`\`\`
@@ -61,8 +61,14 @@ const fixedPrefix = [
61
61
  3. 后续所有子代理的 cwd 设为该 worktree 路径
62
62
  4. 如果创建失败 → 报错并停止(不要在无隔离状态下继续)
63
63
 
64
+ ### 降级模式
65
+ CLI 可能自动降级(sandbox 限制、已在 linked worktree 中):
66
+ - \`mode: native-worktree\` — 已在 linked worktree,直接复用
67
+ - \`mode: in-place-fallback\` — git worktree add 失败,降级为 in-place + baseline protection
68
+ - 这两种模式都会输出 worktree 路径和分支名,正常继续即可
69
+
64
70
  ### 输出
65
- worktree 路径 + 分支名
71
+ worktree 路径 + 分支名 + 模式(如果有)
66
72
 
67
73
  ### 完成后执行
68
74
  sillyspec run execute --done --output "worktree 路径 + 分支名"`,
@@ -184,19 +190,40 @@ const fixedSuffix = [
184
190
  name: '完成确认',
185
191
  prompt: `所有任务完成后的收尾。
186
192
 
187
- ### 操作(有 worktree)
193
+ 先检查当前 worktree 的隔离模式:
194
+ \`\`\`bash
195
+ node -e "import('./src/worktree.js').then(w => { const wm = new w.WorktreeManager(); const m = wm.getMeta('<change-name>'); console.log(m ? JSON.stringify({mode: m.mode, path: m.worktreePath}) : 'no meta'); })"
196
+ # 或从 DB 读取:
197
+ sqlite3 -json .sillyspec/.runtime/sillyspec.db "SELECT isolation_status, isolation_mode, isolation_reason FROM changes WHERE name='<change-name>'" 2>/dev/null
198
+ \`\`\`
199
+
200
+ ### 操作(mode = worktree,SillySpec 创建的隔离 worktree)
188
201
  1. 运行 \`sillyspec worktree apply --check-only <change-name>\`
189
202
  2. 展示 diff 摘要(文件列表 + 变更统计)
190
203
  3. 检查结果说明(是否通过文件清单校验)
191
204
  4. 用户确认后运行 \`sillyspec worktree apply <change-name>\`
192
- 5. apply 成功 → 自动 cleanup
205
+ 5. apply 成功 → 运行 \`sillyspec worktree cleanup <change-name>\` → 输出 Worktree: cleaned
193
206
  6. apply 失败 → 展示错误详情,用户选择重试或手动处理
194
207
  7. 如果用户不想 apply → 运行 \`sillyspec worktree cleanup <change-name>\` 丢弃
195
208
  8. 建议下一步:\`sillyspec run verify\`
196
209
 
210
+ ### 操作(mode = native-worktree,用户已有的 linked worktree)
211
+ 1. 运行 \`sillyspec worktree apply --check-only <change-name>\`
212
+ 2. 展示 diff 摘要
213
+ 3. 用户确认后运行 \`sillyspec worktree apply <change-name>\`
214
+ 4. **不要运行 cleanup** — 这是用户自己的 worktree,SillySpec 不能删除
215
+ 5. 输出 Worktree: kept(SillySpec 未创建此 worktree,保留不动)
216
+ 6. 建议下一步:\`sillyspec run verify\`
217
+
218
+ ### 操作(mode = in-place-fallback,降级模式无隔离目录)
219
+ 1. 展示本次执行摘要(\`git diff\` 查看变更)
220
+ 2. 跳过 apply 和 cleanup(没有隔离 worktree)
221
+ 3. 输出 Worktree: none(降级为 in-place,无隔离目录需要清理)
222
+ 4. 建议下一步:\`sillyspec run verify\`
223
+
197
224
  ### 操作(无 worktree / --no-worktree 模式)
198
- 1. 跳过 apply 和 cleanup 步骤(因为没有 worktree)
199
- 2. 展示本次执行摘要
225
+ 1. 展示本次执行摘要
226
+ 2. 输出 Worktree: none
200
227
  3. 提示用户直接使用 \`git diff\` 查看变更
201
228
  4. 建议下一步:\`sillyspec run verify\`
202
229
 
@@ -27,12 +27,12 @@ export const definition = {
27
27
  1. 使用预注入的 git 用户名:\`<git-user>\`
28
28
  2. 无 \`--change\`:创建 .sillyspec/quicklog/QUICKLOG-\`<git-user>\`.md\`(已存在则追加),写入:
29
29
  \`\`\`
30
- ## ql-<YYYYMMDD>-<NNN>-<short4> | <now-datetime> | <一句话任务描述>
30
+ ## ql-<YYYYMMDD>-<NNN>-<XXXX> | <now-datetime> | <一句话任务描述>
31
31
  状态:进行中
32
32
  文件:<预估要改的文件>
33
33
  \`\`\`
34
- - ID 格式:\`ql-YYYYMMDD-NNN-XXXX\`(如 ql-20260603-001-a3f2)
35
- - \`XXXX\` 是 4 位随机十六进制(防多文件/并发冲突)
34
+ - ID 格式:\`ql-YYYYMMDD-NNN-XXXX\`
35
+ - \`XXXX\` 是 4 位随机十六进制字符(如 a3f2、b7c1、00ef),**不是描述词缩写**
36
36
  - 追加前扫描文件中已有的 \`ql-<当天日期>-\` 前缀的最大序号,+1 作为新序号
37
37
  - 每天从 001 开始,跨日重新计数
38
38
  - 此 ID 可被 design.md / plan.md / archive / module 变更索引引用
@@ -12,7 +12,7 @@ export const definition = {
12
12
  1. 列出项目顶层目录:\`ls -d */ 2>/dev/null | grep -v node_modules | grep -v '.git' | grep -v '.sillyspec'\`
13
13
  2. 对每个顶层目录,快速判断是否为独立项目(检查 package.json / pom.xml / build.gradle / pyproject.toml / go.mod 等构建文件)
14
14
  3. 对每个疑似独立项目,检测技术栈:\`cat <dir>/package.json 2>/dev/null | head -5\` 或类似
15
- 4. 对比 \`.sillyspec/projects/\` 已有配置,找出未注册的子项目
15
+ 4. 对比 \`{PROJECTS_ROOT}/\` 已有配置,找出未注册的子项目
16
16
 
17
17
  ### 判断标准(满足任一即为子项目)
18
18
  - 有独立的构建文件(package.json, pom.xml, build.gradle, pyproject.toml 等)
@@ -41,8 +41,8 @@ export const definition = {
41
41
  prompt: `确定本次要扫描的项目列表。
42
42
 
43
43
  ### 操作
44
- 1. \`ls .sillyspec/projects/*.yaml 2>/dev/null\` — 列出所有已注册项目
45
- 2. 对每个项目,检查已有的 scan 文档状态:\`ls .sillyspec/docs/<project>/scan/*.md 2>/dev/null\`
44
+ 1. \`ls {PROJECTS_ROOT}/*.yaml 2>/dev/null\` — 列出所有已注册项目
45
+ 2. 对每个项目,检查已有的 scan 文档状态:\`ls {DOCS_ROOT}/scan/*.md 2>/dev/null\`
46
46
  3. 按以下格式展示:
47
47
 
48
48
  \`\`\`
@@ -75,7 +75,7 @@ export const definition = {
75
75
  1. 进入项目目录(子项目用其 path,如 \`packages/dashboard/\`)
76
76
  2. \`cat package.json pom.xml build.gradle go.mod Cargo.toml requirements.txt pyproject.toml Gemfile composer.json 2>/dev/null\`
77
77
  3. \`find <project-dir> -maxdepth 2 -name "*.config.*" -not -path "*/node_modules/*" -not -path "*/.git/*" | head -20 | xargs cat 2>/dev/null\`
78
- 4. 结果保存到 \`.sillyspec/docs/<project>/scan/_env-detect.md\`(临时文件,扫描完删除)
78
+ 4. 结果保存到 \`{DOCS_ROOT}/scan/_env-detect.md\`(临时文件,扫描完删除)
79
79
 
80
80
  ### 输出
81
81
  每个项目的环境探测结果摘要`,
@@ -90,7 +90,7 @@ export const definition = {
90
90
  ### 操作
91
91
  对扫描列表中的每个项目分别执行:
92
92
  1. 检查 7 份文档是否存在:ARCHITECTURE、STRUCTURE、CONVENTIONS、INTEGRATIONS、TESTING、CONCERNS、PROJECT
93
- 路径:\`.sillyspec/docs/<project>/scan/<DOC>.md\`
93
+ 路径:\`{DOCS_ROOT}/scan/<DOC>.md\`
94
94
  2. 列出已有 ✅ 和缺失 ⬜
95
95
 
96
96
  ### 输出
@@ -187,7 +187,7 @@ local.yaml 生成结果(已存在/已生成)`,
187
187
 
188
188
  ### 操作
189
189
  对扫描列表中的每个项目分别执行:
190
- 1. 检查 \`.sillyspec/docs/<project>/modules/_module-map.yaml\` 是否已存在,已存在则跳过
190
+ 1. 检查 \`{DOCS_ROOT}/modules/_module-map.yaml\` 是否已存在,已存在则跳过
191
191
  2. 分析项目源码目录结构,识别模块划分:
192
192
  - 用 \`find . -maxdepth 3 -type d -not -path "*/node_modules/*" -not -path "*/.git/*"\` 查看目录结构
193
193
  - 每个有明确职责的独立目录识别为一个模块
@@ -200,7 +200,7 @@ local.yaml 生成结果(已存在/已生成)`,
200
200
  4. 分析跨模块依赖关系:
201
201
  - 用 grep import/require 分析模块间的引用链
202
202
  - 填充 depends_on(本模块依赖谁)和 used_by(谁依赖本模块)
203
- 5. 生成 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`
203
+ 5. 生成 \`{DOCS_ROOT}/modules/_module-map.yaml\`
204
204
  6. 如果 modules/ 目录不存在,先创建
205
205
  7. 原子写入(先写 tmp 文件再 rename)
206
206
 
@@ -305,8 +305,8 @@ _module-map.yaml 生成结果(已存在/已生成/模块列表)`,
305
305
 
306
306
  ### 操作
307
307
  对扫描列表中的每个项目分别执行:
308
- 1. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`,获取模块列表和路径
309
- 2. 检查 \`.sillyspec/docs/<project>/modules/\` 下已有的模块文档(<module>.md)
308
+ 1. 读取 \`{DOCS_ROOT}/modules/_module-map.yaml\`,获取模块列表和路径
309
+ 2. 检查 \`{DOCS_ROOT}/modules/\` 下已有的模块文档(<module>.md)
310
310
  3. 列出每个模块的状态:已有文档 / 缺失
311
311
  4. **必须停下来问用户**:
312
312
  - 展示模块列表及现有文档状态
@@ -323,7 +323,7 @@ _module-map.yaml 生成结果(已存在/已生成/模块列表)`,
323
323
  \`\`\`
324
324
  模块名:<module-id>
325
325
  模块路径:<glob patterns>
326
- 目标文件:.sillyspec/docs/<project>/modules/<module-id>.md
326
+ 目标文件:{DOCS_ROOT}/modules/<module-id>.md
327
327
 
328
328
  操作:
329
329
  1. 用 grep/rg 搜索模块路径范围内的源码(禁止读源码全文)
@@ -380,7 +380,7 @@ module_id: <module-id>
380
380
  ⚠️ 这一步是可选的。如果项目模块简单、流程不明显,可以跳过。
381
381
 
382
382
  ### flows/ 目录
383
- 目标目录:\`.sillyspec/docs/<project>/flows/\`
383
+ 目标目录:\`{DOCS_ROOT}/flows/\`
384
384
 
385
385
  根据 _module-map.yaml 中的模块依赖关系,识别跨模块业务流程:
386
386
  1. 读取 \`_module-map.yaml\`,分析 used_by 链条
@@ -410,7 +410,7 @@ step1 → step2 → step3
410
410
  \`\`\`
411
411
 
412
412
  ### glossary.md
413
- 目标文件:\`.sillyspec/docs/<project>/glossary.md\`
413
+ 目标文件:\`{DOCS_ROOT}/glossary.md\`
414
414
 
415
415
  提取项目专有术语:
416
416
  1. 用 grep 搜索 TODO/FIXME 注释中的术语定义
@@ -446,11 +446,11 @@ step1 → step2 → step3
446
446
 
447
447
  ### 操作
448
448
  对扫描列表中的每个项目分别执行:
449
- 1. 检查 7 份 scan 文档是否全部生成(\`.sillyspec/docs/<project>/scan/\`)
450
- 2. 检查模块文档状态(\`.sillyspec/docs/<project>/modules/\`)
449
+ 1. 检查 7 份 scan 文档是否全部生成(\`{DOCS_ROOT}/scan/\`)
450
+ 2. 检查模块文档状态(\`{DOCS_ROOT}/modules/\`)
451
451
  3. 自检门控:ARCHITECTURE(技术栈+Schema摘要)、CONVENTIONS(隐形规则+代码风格)、STRUCTURE(目录结构)、INTEGRATIONS(外部依赖)、TESTING(测试现状)、CONCERNS(技术债务)、PROJECT(项目概览)
452
452
  4. 检查 flows/ 和 glossary.md 是否已生成(如有)
453
- 5. 清理:\`rm -f .sillyspec/docs/<project>/scan/_env-detect.md\`
453
+ 5. 清理:\`rm -f {DOCS_ROOT}/scan/_env-detect.md\`
454
454
  6. \`git add .sillyspec/\` — 暂存扫描结果(不要 commit,由用户通过统一提交工具处理)
455
455
 
456
456
  ### 输出
package/src/workflow.js CHANGED
@@ -630,8 +630,12 @@ export function generateAllRolePrompts(wf, projectName, context = {}) {
630
630
  * @returns {string|null} 保存路径,失败返回 null
631
631
  */
632
632
  export function saveWorkflowRun(result, options = {}) {
633
- const { cwd = '.', source = 'unknown', stage, step } = options
634
- const runDir = join(cwd, '.sillyspec', '.runtime', 'workflow-runs')
633
+ const { cwd = '.', source = 'unknown', stage, step, runtimeRoot, scanRunId } = options
634
+ // 平台模式:写入 runtime-root/scan-runs/<scan-run-id>/workflow-runs/
635
+ // 本地模式:写入 cwd/.sillyspec/.runtime/workflow-runs/
636
+ const runDir = runtimeRoot
637
+ ? join(runtimeRoot, 'scan-runs', scanRunId || 'unknown', 'workflow-runs')
638
+ : join(cwd, '.sillyspec', '.runtime', 'workflow-runs')
635
639
  try {
636
640
  mkdirSync(runDir, { recursive: true })
637
641
  } catch (e) {
@@ -341,10 +341,24 @@ export function formatExecuteSummary({ changeName, stepsCompleted, stepsTotal, a
341
341
  ? `dirty (${baselineCount} baseline file${baselineCount === 1 ? '' : 's'} protected)`
342
342
  : 'clean';
343
343
 
344
+ // Worktree 最终状态
345
+ const mode = meta.mode || 'worktree';
346
+ let worktreeStatus;
347
+ if (mode === 'native-worktree') {
348
+ worktreeStatus = 'kept (external worktree)';
349
+ } else if (mode === 'in-place-fallback') {
350
+ worktreeStatus = 'none (in-place)';
351
+ } else if (!wtExists) {
352
+ worktreeStatus = 'cleaned';
353
+ } else {
354
+ worktreeStatus = 'exists';
355
+ }
356
+
344
357
  lines.push(`Status: COMPLETED`);
345
358
  lines.push(`Steps: ${stepsCompleted} / ${stepsTotal}`);
346
359
  lines.push(`Baseline: ${baselineStatus}`);
347
360
  lines.push(`Apply: ${applyStatus}`);
361
+ lines.push(`Worktree: ${worktreeStatus}`);
348
362
  }
349
363
 
350
364
  // --- Changed files ---