sillyspec 3.15.0 → 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
@@ -10,6 +10,7 @@ import { ProgressManager } from './progress.js'
10
10
  import { stageRegistry, auxiliaryStages } from './stages/index.js'
11
11
  import { buildExecuteSteps } from './stages/execute.js'
12
12
  import { buildPlanSteps } from './stages/plan.js'
13
+ import { formatExecuteSummary } from './worktree-apply.js'
13
14
 
14
15
  /**
15
16
  * 同步触发辅助函数:_write 后 best-effort 同步到平台
@@ -146,7 +147,7 @@ async function ensureStageSteps(progress, stageName, cwd) {
146
147
  /**
147
148
  * 输出当前步骤的 prompt
148
149
  */
149
- async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName) {
150
+ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}) {
150
151
  const step = steps[stepIndex]
151
152
  const total = steps.length
152
153
  const projectName = dbProjectName || basename(cwd)
@@ -217,6 +218,43 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
217
218
  if (changeName && promptText.includes('<change-name>')) {
218
219
  promptText = promptText.replace(/<change-name>/g, changeName)
219
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
+
220
258
  console.log(promptText)
221
259
  console.log(`\n### ⚠️ 铁律`)
222
260
  console.log('- **文档是核心资产,代码是文档的产物。** 没有文档就没有代码——文档是 AI 的记忆,是团队协作的基础,是后续维护的唯一依据。任何代码产出必须先有对应的设计/规范文档支撑。')
@@ -264,6 +302,50 @@ export async function runCommand(args, cwd) {
264
302
  const isConfirm = flags.includes('--confirm')
265
303
  const isSkipApproval = flags.includes('--skip-approval')
266
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
+
267
349
  // 解析 --output
268
350
  let outputText = null
269
351
  const outputIdx = flags.indexOf('--output')
@@ -285,6 +367,26 @@ export async function runCommand(args, cwd) {
285
367
  changeName = flags[changeIdx + 1]
286
368
  }
287
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
+
288
390
  const isAuxiliary = auxiliaryStages.includes(stageName)
289
391
 
290
392
  const pm = new ProgressManager()
@@ -360,11 +462,11 @@ export async function runCommand(args, cwd) {
360
462
 
361
463
  // --done
362
464
  if (isDone) {
363
- 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 })
364
466
  }
365
467
 
366
468
  // 默认:输出当前步骤
367
- return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval)
469
+ return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts)
368
470
  }
369
471
 
370
472
  /**
@@ -379,7 +481,7 @@ function resolveChangeNameAuto(cwd) {
379
481
  return null
380
482
  }
381
483
 
382
- async function runStage(pm, progress, stageName, cwd, changeName, skipApproval = false) {
484
+ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval = false, platformOpts = {}) {
383
485
  // execute 阶段启动前检查审批
384
486
  if (stageName === 'execute' && !skipApproval) {
385
487
  const approval = await checkApproval(cwd, changeName)
@@ -443,7 +545,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
443
545
 
444
546
  const defSteps = await getStageSteps(stageName, cwd, progress)
445
547
  if (defSteps && defSteps[currentIdx]) {
446
- await outputStep(stageName, currentIdx, defSteps, cwd, changeName, progress.project || null)
548
+ await outputStep(stageName, currentIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
447
549
  }
448
550
  }
449
551
 
@@ -526,7 +628,7 @@ function validateFileLocations(cwd, stageName, progress, changeName) {
526
628
  }
527
629
 
528
630
  async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
529
- const { printNext = true, confirm = false, changeName } = options
631
+ const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
530
632
  const stageData = progress.stages[stageName]
531
633
  if (!stageData || !stageData.steps) {
532
634
  console.error(`❌ 阶段 ${stageName} 未初始化`)
@@ -547,10 +649,13 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
547
649
  const MAX_OUTPUT = 200
548
650
  if (outputText.length > MAX_OUTPUT) {
549
651
  steps[currentIdx].output = outputText.slice(0, MAX_OUTPUT) + '…'
550
- const artifactsDir = join(cwd, '.sillyspec', '.runtime', 'artifacts')
551
- 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 })
552
657
  const ts = new Date().toISOString().slice(0,19).replace(/[-T:]/g, '')
553
- 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)
554
659
  } else {
555
660
  steps[currentIdx].output = outputText
556
661
  }
@@ -682,6 +787,56 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
682
787
  appendFileSync(inputsPath, entry)
683
788
  }
684
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
+
685
840
  validateMetadata(cwd, stageName)
686
841
 
687
842
  // 验证关键文件是否在正确的变更目录下
@@ -745,7 +900,21 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
745
900
  console.log(`✅ ${stageName} 阶段已完成(${total}/${total} 步)`)
746
901
 
747
902
  if (stageName === 'execute') {
748
- console.log('\n👉 下一步:sillyspec run verify(验证通过后才能归档)')
903
+ // execute run summary:展示真实可得的结构化信息
904
+ try {
905
+ const lastOutput = steps[steps.length - 1]?.output || ''
906
+ const summary = formatExecuteSummary({
907
+ changeName,
908
+ stepsCompleted: total,
909
+ stepsTotal: total,
910
+ agentSummary: lastOutput,
911
+ cwd,
912
+ })
913
+ console.log(`\n${summary}`)
914
+ } catch (e) {
915
+ // summary 失败不影响主流程
916
+ console.log('\n👉 下一步:sillyspec run verify(验证通过后才能归档)')
917
+ }
749
918
  } else if (stageName === 'verify') {
750
919
  console.log('\n👉 下一步:sillyspec run archive(验证通过,可以归档了)')
751
920
  } else if (stageName === 'archive') {
@@ -847,7 +1016,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
847
1016
  }
848
1017
 
849
1018
  if (printNext) {
850
- await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null)
1019
+ await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
851
1020
  }
852
1021
  return { stageCompleted: false, currentIdx, nextPendingIdx }
853
1022
  }
@@ -885,7 +1054,7 @@ async function skipStep(pm, progress, stageName, cwd, changeName) {
885
1054
  const nextPendingIdx = steps.findIndex(s => s.status === 'pending')
886
1055
  if (nextPendingIdx !== -1 && defSteps) {
887
1056
  console.log('')
888
- await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null)
1057
+ await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
889
1058
  }
890
1059
  }
891
1060
 
@@ -1030,7 +1199,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
1030
1199
  }
1031
1200
  }
1032
1201
  }
1033
- await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null)
1202
+ await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
1034
1203
  return
1035
1204
  }
1036
1205
 
@@ -1039,7 +1208,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
1039
1208
  process.exit(1)
1040
1209
  }
1041
1210
 
1042
- 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 })
1043
1212
  if (!result) return
1044
1213
  progress = await pm.read(cwd, changeName)
1045
1214
 
@@ -1060,7 +1229,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
1060
1229
  }
1061
1230
  }
1062
1231
  }
1063
- await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null)
1232
+ await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
1064
1233
  return
1065
1234
  }
1066
1235
 
@@ -1102,6 +1271,6 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
1102
1271
  }
1103
1272
  }
1104
1273
  }
1105
- await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null)
1274
+ await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null, platformOpts)
1106
1275
  }
1107
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) {