sillyspec 3.24.3 → 3.24.5

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.24.3",
3
+ "version": "3.24.5",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -217,16 +217,27 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot }
217
217
  // ── d. verify 阶段追加 verify-test(CLI 实测 local.yaml commands.test)──
218
218
  if (stage === 'verify') {
219
219
  const vt = runVerifyTestCheck({ cwd, specBase: specRoot, changeName });
220
+ const vtWarnings = [];
221
+ if (vt.status === 'skipped') {
222
+ vtWarnings.push('⚠️ verify-test SKIPPED — gate 未核验测试(local.yaml 未配置 commands.test 或显式无测试)。本次 gate 结论不含测试客观核验,driver 不应据 exit 0 判定测试通过;integration-critical 变更应在 verify 阶段降级 FAIL');
223
+ }
224
+ // 全量 fallback 明示:跑了全量 commands.test 但非变更范围子集,失败可能含未变更
225
+ // 模块的预存错误——driver 不应据 exit 0 判定本次变更范围已客观测试(见 3.24 verify 坑1)。
226
+ if (vt.status !== 'skipped' && vt.mode === 'full' && vt.fallbackReason) {
227
+ vtWarnings.push(`⚠️ verify-test 跑的是全量 commands.test(${vt.fallbackReason});失败可能含未变更模块的预存错误,driver 不应据 exit 0 判定本次变更范围已测`);
228
+ }
220
229
  checks.push({
221
230
  id: 'verify-test',
222
231
  ok: vt.status !== 'failed',
223
232
  errors: vt.status === 'failed' ? [`测试失败: ${vt.reason || ''}`] : [],
224
- warnings: vt.status === 'skipped' ? ['⚠️ verify-test SKIPPED — gate 未核验测试(local.yaml 未配置 commands.test 或显式无测试)。本次 gate 结论不含测试客观核验,driver 不应据 exit 0 判定测试通过;integration-critical 变更应在 verify 阶段降级 FAIL'] : [],
233
+ warnings: vtWarnings,
225
234
  data: {
226
235
  status: vt.status,
227
236
  exitCode: vt.exitCode,
228
237
  durationMs: vt.durationMs,
229
238
  resultPath: vt.resultPath,
239
+ mode: vt.mode ?? null,
240
+ fallbackReason: vt.fallbackReason ?? null,
230
241
  },
231
242
  });
232
243
  }
@@ -351,10 +362,16 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot
351
362
  exitCode: vt.exitCode,
352
363
  durationMs: vt.durationMs,
353
364
  resultPath: vt.resultPath,
365
+ mode: vt.mode ?? null,
366
+ fallbackReason: vt.fallbackReason ?? null,
354
367
  };
355
368
  ok = vt.status !== 'failed';
356
369
  errors = vt.status === 'failed' ? [`测试失败: ${vt.reason || ''}`] : [];
357
- warnings = vt.status === 'skipped' ? ['测试被跳过'] : [];
370
+ warnings = vt.status === 'skipped'
371
+ ? ['测试被跳过']
372
+ : (vt.mode === 'full' && vt.fallbackReason)
373
+ ? [`⚠️ verify-test 跑的是全量 commands.test(${vt.fallbackReason});失败可能含未变更模块的预存错误`]
374
+ : [];
358
375
  exitCode = ok ? EXIT_OK : EXIT_BLOCKED;
359
376
  break;
360
377
  }
package/src/run.js CHANGED
@@ -632,11 +632,26 @@ async function getStageSteps(stageName, cwd, progress, specDir = null) {
632
632
  if (stageName === 'execute') {
633
633
  const changeDir = resolveChangeDir(cwd, progress, specDir)
634
634
  let planFile = null
635
+ let worktreePath = null
635
636
  if (changeDir) {
636
637
  const p = join(changeDir, 'plan.md')
637
638
  if (existsSync(p)) planFile = p
639
+ // 自动检测 worktree 路径,注入 Wave prompt 的 workdir 指令
640
+ // 修复:之前未传 worktreePath 给 buildExecuteSteps,导致 Wave prompt 缺失工作目录指令,
641
+ // 子代理可能把文件写到主工作区而非 worktree 内,破坏隔离。
642
+ try {
643
+ const changeName = basename(changeDir)
644
+ const { WorktreeManager } = await import('./worktree.js')
645
+ const wm = new WorktreeManager({ cwd })
646
+ const meta = wm.getMeta(changeName)
647
+ if (meta?.worktreePath && existsSync(meta.worktreePath)) {
648
+ worktreePath = meta.worktreePath
649
+ }
650
+ } catch {
651
+ // 无 worktree meta 不阻断——可能是首次启动或 in-place 模式
652
+ }
638
653
  }
639
- return buildExecuteSteps(planFile)
654
+ return buildExecuteSteps(planFile, { worktreePath })
640
655
  }
641
656
  if (stageName === 'plan') {
642
657
  const changeDir = resolveChangeDir(cwd, progress, specDir)
@@ -3207,11 +3222,12 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
3207
3222
  const resultCheck = validateQuickResult(outputText)
3208
3223
  if (!resultCheck.ok) {
3209
3224
  console.error(`\n❌ quick 结果摘要结构不完整:缺少字段 ${resultCheck.missing.join('、')}`)
3210
- console.error(` --output 会写入 QUICKLOG「结果:」,请按模板补全再重跑 --done:`)
3211
- console.error(` 需求:用户/任务要什么`)
3225
+ console.error(` --output 是 QUICKLOG「结果:」归档的唯一来源,四个标签必须放在 --output 里(不是 --input)。`)
3226
+ console.error(` 补全后重跑 --done(不丢进度),直接照抄此模板:`)
3227
+ console.error(` sillyspec run quick --done --change <changeName> --output "需求:用户/任务要什么`)
3212
3228
  console.error(` 根因:为什么这样改(纯新增/样式则写「无,纯新增/纯样式」)`)
3213
3229
  console.error(` 方案:怎么改的`)
3214
- console.error(` 结果:验证情况(测试数 / lint / typecheck / 部署状态)`)
3230
+ console.error(` 结果:验证情况(测试数 / lint / typecheck / 部署状态)"`)
3215
3231
  steps[currentIdx].status = 'pending'
3216
3232
  steps[currentIdx].completedAt = null
3217
3233
  if (outputText) steps[currentIdx].output = null
@@ -61,6 +61,47 @@ export function extractTestStrategy(yamlText) {
61
61
  return null
62
62
  }
63
63
 
64
+ /**
65
+ * 计算全量 fallback 的原因文本(供 printVerifyTestCheck / facet 给 agent/daemon 明示)。
66
+ *
67
+ * 返回 null 表示不需要 hint:显式 test_strategy:full(用户有意跑全量),
68
+ * 或 test_strategy:module 已成功命中模块子集(该情况不会走到全量路径)。
69
+ * 其余走全量路径的情况都返回原因字符串,让结果可正确解读——
70
+ * 否则 agent/daemon 会把"全量 commands.test"当成"按变更范围测的",误把
71
+ * 未变更模块的预存错误归因到本次变更(见 3.24 verify 坑1)。
72
+ *
73
+ * 语义对齐 runVerifyTestCheck 的分支:
74
+ * - strategy==='full' → null
75
+ * - strategy==='module' 但无有效 modules 块 → hint
76
+ * - strategy==='module' 有块但 git 不可用(hitCount=-1)→ hint
77
+ * - strategy==='module' 有块但 0 命中(hitCount=0)→ hint
78
+ * - strategy==='module' 有块且命中(hitCount>0)→ null(走子集)
79
+ * - strategy===null(缺省)→ hint(默认全量)
80
+ *
81
+ * @param {object} ctx
82
+ * @param {'full'|'module'|null} ctx.strategy
83
+ * @param {boolean} ctx.modulesPresent - extractModules 是否返回有效映射
84
+ * @param {number} ctx.hitCount - git diff 命中的模块数;-1 表示 git 不可用/非仓库
85
+ * @returns {string|null}
86
+ */
87
+ export function computeFullFallbackReason({ strategy, modulesPresent, hitCount }) {
88
+ if (strategy === 'full') return null
89
+ if (strategy === 'module') {
90
+ if (!modulesPresent) {
91
+ return 'test_strategy: module 但 local.yaml 未配置有效的 modules: 块(需 inline flow: name: { path, test }),回退全量'
92
+ }
93
+ if (hitCount < 0) {
94
+ return 'test_strategy: module 但 git 不可用/非 git 仓库,无法判定命中模块,回退全量'
95
+ }
96
+ if (hitCount === 0) {
97
+ return 'test_strategy: module 但本次 git diff 未命中任何已配置 modules,回退全量'
98
+ }
99
+ return null
100
+ }
101
+ // strategy === null(缺省 → 默认全量)
102
+ return 'local.yaml 未配置 test_strategy(默认全量 commands.test,未按变更范围收窄)'
103
+ }
104
+
64
105
  /**
65
106
  * 从 local.yaml 文本解析 modules 映射块。
66
107
  * 设计约定(见 change 2026-07-10-tooling-followups design.md #2 方案 A):
@@ -258,29 +299,42 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null }) {
258
299
  const localYamlPath = join(specBase, 'local.yaml')
259
300
  const yamlText = existsSync(localYamlPath) ? readFileSync(localYamlPath, 'utf8') : null
260
301
 
261
- // —— 模块子集路径(test_strategy: module)——
262
302
  const strategy = extractTestStrategy(yamlText)
303
+
304
+ // —— 模块子集路径(test_strategy: module)——
305
+ // 算出 modulesPresent / hitCount 供 fallback hint 判定(computeFullFallbackReason);
306
+ // 命中即走子集。gitChangedFiles 返回 null 表示 git 不可用 → hitCount=-1(与 0 命中区分)。
307
+ let modulesPresent = false
308
+ let hitCount = 0
263
309
  if (strategy === 'module') {
264
310
  const modules = extractModules(yamlText)
265
311
  if (modules) {
312
+ modulesPresent = true
266
313
  const changedFiles = gitChangedFiles(cwd)
267
- const hits = changedFiles ? pickHitModules(changedFiles, modules) : []
268
- if (hits.length > 0) {
269
- return runModuleSubset({ cwd, specBase, changeName, hits })
314
+ if (changedFiles === null) {
315
+ hitCount = -1 // git 不可用 / 非仓库
316
+ } else {
317
+ const hits = pickHitModules(changedFiles, modules)
318
+ hitCount = hits.length
319
+ if (hitCount > 0) {
320
+ return runModuleSubset({ cwd, specBase, changeName, hits })
321
+ }
322
+ // 有 modules 配置但无命中 → fallback commands.test(D-002@v1 向后兼容)
270
323
  }
271
- // 有 modules 配置但无命中 / git 不可用 → fallback commands.test(D-002@v1 向后兼容)
272
324
  }
273
325
  // test_strategy:module 但无 modules 配置 → fallback commands.test(brownfield 友好)
274
326
  }
275
327
 
276
328
  // —— 全量路径(默认 full / fallback)——
277
- return runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName })
329
+ // fallbackReason 非 null 表示本次全量是"非显式"的(缺省/配置不全/未命中),需明示。
330
+ const fallbackReason = computeFullFallbackReason({ strategy, modulesPresent, hitCount })
331
+ return runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName, fallbackReason })
278
332
  }
279
333
 
280
334
  /**
281
335
  * 全量跑 commands.test(现有逻辑,brownfield 行为不变)。
282
336
  */
283
- function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName }) {
337
+ function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName, fallbackReason = null }) {
284
338
  const command = extractTestCommand(yamlText)
285
339
 
286
340
  if (!command) {
@@ -294,6 +348,8 @@ function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName })
294
348
  ? 'local.yaml 未配置 commands.test(或标记 unavailable)'
295
349
  : `local.yaml 不存在(${localYamlPath})`,
296
350
  resultPath: null,
351
+ mode: 'full',
352
+ fallbackReason,
297
353
  }
298
354
  }
299
355
 
@@ -327,9 +383,11 @@ function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName })
327
383
  outputTail,
328
384
  reason,
329
385
  resultPath: null,
386
+ mode: 'full',
387
+ fallbackReason,
330
388
  }
331
389
 
332
- writeRunResult({ specBase, changeName, result })
390
+ writeRunResult({ specBase, changeName, result, extra: fallbackReason ? { fallback_reason: fallbackReason } : {} })
333
391
  return result
334
392
  }
335
393
 
@@ -362,6 +420,8 @@ function runModuleSubset({ cwd, specBase, changeName, hits }) {
362
420
  outputTail: outputTail.length > OUTPUT_TAIL_CHARS ? '…' + outputTail.slice(-OUTPUT_TAIL_CHARS) : outputTail,
363
421
  reason,
364
422
  resultPath: null,
423
+ mode: 'module-subset',
424
+ fallbackReason: null,
365
425
  }
366
426
 
367
427
  writeRunResult({
@@ -427,6 +487,19 @@ export function printVerifyTestCheck(result) {
427
487
  for (const line of tail.split('\n')) console.error(` | ${line}`)
428
488
  }
429
489
  }
490
+ // —— 全量 fallback 明示(skipped 已提前 return,此处仅 passed/failed)——
491
+ // 让 agent 知道本次跑的是全量 commands.test、非变更范围子集;失败可能含未变更模块
492
+ // 的预存错误,需先核对用例归属再归因到本次变更(见 3.24 verify 坑1)。
493
+ if (result.mode === 'full' && result.fallbackReason) {
494
+ if (result.status === 'failed') {
495
+ console.warn(` ⚠️ 本次跑的是 commands.test 全量(${result.fallbackReason})。`)
496
+ console.warn(' 失败可能含与本变更无关的预存错误——先核对失败用例是否属于你的变更范围;')
497
+ console.warn(' 或在 local.yaml 配置 test_strategy: module + modules: 块以收窄到变更模块。')
498
+ } else {
499
+ console.warn(` ⚠️ 本次跑的是 commands.test 全量(${result.fallbackReason})。`)
500
+ console.warn(' 如耗时过长,可在 local.yaml 配置 test_strategy: module + modules: 块按模块收窄。')
501
+ }
502
+ }
430
503
  if (result.resultPath) {
431
504
  console.log(`📄 实测结果已写入: ${result.resultPath}`)
432
505
  }