sillyspec 3.23.0 → 3.23.1

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.23.0",
3
+ "version": "3.23.1",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/index.js CHANGED
@@ -632,12 +632,13 @@ SillySpec worktree — git worktree 隔离管理
632
632
  }
633
633
  case 'apply': {
634
634
  if (!wtName) {
635
- console.error('❌ 用法: sillyspec worktree apply <change-name> [--check-only]');
635
+ console.error('❌ 用法: sillyspec worktree apply <change-name> [--check-only] [--merge]');
636
636
  process.exit(1);
637
637
  }
638
638
  const checkOnly = args.includes('--check-only');
639
+ const merge = args.includes('--merge');
639
640
  const { applyWorktree } = await import('./worktree-apply.js');
640
- const result = applyWorktree(wtName, { cwd: dir, checkOnly });
641
+ const result = applyWorktree(wtName, { cwd: dir, checkOnly, merge });
641
642
 
642
643
  if (result.errors.length > 0) {
643
644
  console.error(`❌ 校验失败:`);
@@ -657,6 +658,8 @@ SillySpec worktree — git worktree 隔离管理
657
658
  for (const f of result.changedFiles) {
658
659
  console.log(` ${f}`);
659
660
  }
661
+ } else if (result.merged) {
662
+ console.log(`✅ 已通过 git merge 应用变更(baseline 漂移降级,引入合并提交)${result.mergeSummary ? ':' + result.mergeSummary : ''}`);
660
663
  } else {
661
664
  console.log(`✅ 已应用 ${result.changedFiles.length} 个文件变更`);
662
665
  }
@@ -710,6 +713,7 @@ SillySpec worktree — git worktree 隔离管理
710
713
  console.log('Action: blocked');
711
714
  console.log(' → 检查变更: sillyspec worktree diff ' + wtName);
712
715
  console.log(' → 丢弃变更: sillyspec worktree cleanup ' + wtName);
716
+ console.log(' → baseline 漂移降级: sillyspec worktree apply ' + wtName + ' --merge');
713
717
  }
714
718
  break;
715
719
  }
package/src/run.js CHANGED
@@ -3321,7 +3321,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
3321
3321
  } catch {}
3322
3322
 
3323
3323
  const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId, changeDir: planFile, gitDir: reviewGitDir })
3324
- printReviewResult(reviewResult)
3324
+ printReviewResult(reviewResult, { runtimeRoot, executeRunId })
3325
3325
 
3326
3326
  if (!reviewResult.ok) {
3327
3327
  // Task review 校验失败,阻断 execute 完成
@@ -3329,7 +3329,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
3329
3329
  const uncheckedTasks = reviewResult.errors.filter(e => e.includes('缺少 review.json'))
3330
3330
  if (uncheckedTasks.length > 0) {
3331
3331
  console.error('\n⚠️ 部分任务已在 plan.md 中勾选,但 review.json 不存在。')
3332
- console.error(' 请取消勾选这些任务的 checkbox,或补充对应的 review.json。')
3332
+ console.error(` 请取消勾选这些任务的 checkbox,或补充对应的 review.json(execute run ID: ${executeRunId})。`)
3333
3333
  }
3334
3334
  rollbackStageCompletion(stageData, steps, currentIdx)
3335
3335
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
@@ -620,7 +620,7 @@ ${taskList}
620
620
 
621
621
  **review.json 路径:**
622
622
 
623
- task-XX 对应:.sillyspec/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-XX/review.json
623
+ task-XX 对应:{SPEC_ROOT}/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-XX/review.json
624
624
 
625
625
  本 execute run 的固定 ID 是:{EXECUTE_RUN_ID}
626
626
  **所有 task 的 review.json 必须使用这个 ID,不要自行创建新目录。**
@@ -641,7 +641,7 @@ task-XX 对应:.sillyspec/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-XX
641
641
  ### 完成后
642
642
  1. 为每个后端 router task,扫描变更文件提取 API 端点 artifact:
643
643
  - 在变更文件中搜索所有 router 注册路径(@router.get/post/put/delete)
644
- - 将端点清单写入 .sillyspec/.runtime/contract-artifacts/<task-name>/endpoints.json
644
+ - 将端点清单写入 {SPEC_ROOT}/.runtime/contract-artifacts/<task-name>/endpoints.json
645
645
  - 格式: { "task": "task-XX", "type": "backend_endpoints", "endpoints": [{ "method": "GET", "path": "/api/ppm/xxx" }] }
646
646
  2. 运行 sillyspec run execute --done --input "用户原始反馈" --output "Wave ${waveIndex} 结果摘要"`
647
647
  }
@@ -179,7 +179,7 @@ export function validateTaskReviews(opts) {
179
179
  if (isTaskLowRisk(changeDir, taskId)) {
180
180
  warnings.push(`${taskId}: 缺少 review.json — task 声明 low_risk: true,已豁免评审(type-only/机械迁移)`)
181
181
  } else {
182
- errors.push(`${taskId}: 缺少 review.json — task 未经过评审`)
182
+ errors.push(`${taskId}: 缺少 review.json — task 未经过评审(期望路径:${reviewPath},execute run ID: ${executeRunId})`)
183
183
  }
184
184
  }
185
185
  continue
@@ -447,7 +447,7 @@ export function ensureTaskReviewDir(runtimeRoot, executeRunId, taskId) {
447
447
  * 打印校验结果
448
448
  * @param {{ ok: boolean, errors: string[], warnings: string[], requiredEvidence: Array }} result
449
449
  */
450
- export function printReviewResult(result) {
450
+ export function printReviewResult(result, context = {}) {
451
451
  if (result.ok && result.warnings.length === 0) {
452
452
  console.log('\n✅ Task Review Gate — 所有任务评审通过')
453
453
  return
@@ -458,7 +458,10 @@ export function printReviewResult(result) {
458
458
  for (const err of result.errors) {
459
459
  console.error(` - ${err}`)
460
460
  }
461
- console.error('\n 提示:为缺失/失败的任务补充 review.json,然后重新 --done')
461
+ const hint = context.runtimeRoot && context.executeRunId
462
+ ? `期望路径:${context.runtimeRoot}/execute-runs/${context.executeRunId}/tasks/<task-XX>/review.json`
463
+ : '为缺失/失败的任务补充 review.json'
464
+ console.error(`\n 提示:${hint},然后重新 --done`)
462
465
  }
463
466
 
464
467
  if (result.warnings.length > 0) {
@@ -73,7 +73,7 @@ function getFileBlobHash(cwd, treeish, filePath) {
73
73
  * errors: string[]
74
74
  * }}
75
75
  */
76
- export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
76
+ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = false } = {}) {
77
77
  const projectRoot = cwd || process.cwd();
78
78
  const wm = new WorktreeManager({ cwd: projectRoot });
79
79
  const meta = wm.getMeta(changeName);
@@ -84,6 +84,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
84
84
  hashMismatchFiles: [],
85
85
  patchPath: null,
86
86
  errors: [],
87
+ merged: false,
87
88
  };
88
89
 
89
90
  // --- 1. 校验 worktree 存在 + meta.json 有效 ---
@@ -172,9 +173,13 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
172
173
  const raw = `staged:${staged}\nunstaged:${unstaged}\nuntracked:${untracked}`;
173
174
  const currentHash = createHash('sha256').update(raw).digest('hex').slice(0, 16);
174
175
  if (currentHash !== meta.baselineHash) {
176
+ if (merge && !checkOnly) {
177
+ // baseline 漂移 + --merge 降级(D-001):跳过步骤 5-7(patch 路径),走 git merge
178
+ return applyByMerge(result, changeName, projectRoot, wm);
179
+ }
175
180
  result.errors.push(
176
181
  `主工作区 baseline 已变化(execute 前后不一致),不能直接 apply task.patch。\n` +
177
- `建议:重新创建 worktree 或手动检查冲突。\n` +
182
+ `建议:重新创建 worktree,或用 --merge 降级(git merge sillyspec/${changeName} 替代 patch,会引入合并提交)。\n` +
178
183
  `execute 前 baseline: ${meta.baselineHash}\n` +
179
184
  `当前 baseline: ${currentHash}`
180
185
  );
@@ -320,6 +325,53 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
320
325
  return result;
321
326
  }
322
327
 
328
+ /**
329
+ * baseline 漂移时的 merge 降级路径(D-001)。
330
+ *
331
+ * 当主工作区 baseline 在 execute 期间漂移、patch 无法干净应用时,用
332
+ * `git merge sillyspec/<change>` 替代 patch apply(BRANCH_PREFIX='sillyspec/',worktree.js:18)。
333
+ * git merge 比 patch 鲁棒,能处理 baseline 漂移 + 潜在冲突。会引入合并提交
334
+ * (D-002:与 worktree.md:84「patch 而非 merge 保持线性历史」架构决策的张力——
335
+ * 仅作 --merge 显式 opt-in,不改变默认 patch 行为)。
336
+ *
337
+ * merge 冲突时不自动解决:git merge --abort 回滚到合并前状态 + 报冲突文件列表。
338
+ *
339
+ * @param {object} result - applyWorktree 的 result 对象(mutate 后返回)
340
+ * @param {string} changeName
341
+ * @param {string} projectRoot - 主仓库根
342
+ * @param {object} wm - WorktreeManager 实例
343
+ * @returns {object} result(merged=true 表示走了 merge 降级)
344
+ */
345
+ function applyByMerge(result, changeName, projectRoot, wm) {
346
+ try {
347
+ git(projectRoot, `merge --no-ff sillyspec/${changeName}`);
348
+ result.merged = true;
349
+ result.ok = true;
350
+ try { result.mergeSummary = git(projectRoot, `log --oneline -1`); } catch {}
351
+ // 成功后自动 cleanup(与 patch 路径步骤 8 一致;分支已合并,删除安全)
352
+ try {
353
+ wm.cleanup(changeName);
354
+ } catch (cleanupErr) {
355
+ result.warnings = result.warnings || [];
356
+ result.warnings.push(`cleanup 失败(不影响 merge 结果): ${cleanupErr.message}`);
357
+ }
358
+ } catch (e) {
359
+ // merge 冲突:先取冲突文件列表,再 abort 回滚(避免半成品合并)
360
+ let conflictFiles = [];
361
+ try {
362
+ const cf = gitQuiet(projectRoot, `diff --name-only --diff-filter=U`);
363
+ conflictFiles = cf ? cf.split('\n').filter(Boolean) : [];
364
+ } catch {}
365
+ try { gitQuiet(projectRoot, `merge --abort`); } catch {}
366
+ result.errors.push(
367
+ `git merge sillyspec/${changeName} 冲突,请手动解决。冲突文件:\n` +
368
+ (conflictFiles.length ? ` ${conflictFiles.join('\n ')}\n` : ` (未能获取冲突文件列表)\n`) +
369
+ `已执行 git merge --abort 回滚到合并前状态。`
370
+ );
371
+ }
372
+ return result;
373
+ }
374
+
323
375
  /**
324
376
  * 风险审计:评估 worktree 变更是否可以安全自动 apply
325
377
  *