sillyspec 3.23.0 → 3.23.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.23.0",
3
+ "version": "3.23.2",
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
  }
@@ -80,6 +80,10 @@ function parseAllowedPaths(content) {
80
80
  * @returns {boolean}
81
81
  */
82
82
  function hasAcceptanceCriteria(content) {
83
+ // 新模板把 goal/implementation/acceptance/verify/constraints 放在 frontmatter
84
+ // (acceptance: 作为 fm 字段),老格式或人工写的卡片可能用 body 章节。
85
+ // 两种都认,避免「模板格式」与「postcheck 判定」不一致导致卡片过不了校验。
86
+ if (/^acceptance:/m.test(content)) return true
83
87
  return /##\s*验收标准/.test(content) || /##\s*Acceptance/.test(content)
84
88
  }
85
89
 
@@ -89,6 +93,8 @@ function hasAcceptanceCriteria(content) {
89
93
  * @returns {boolean}
90
94
  */
91
95
  function hasTddOrVerify(content) {
96
+ // 同 hasAcceptanceCriteria:新模板用 frontmatter 的 verify: 字段,老格式用 body 章节。
97
+ if (/^verify:/m.test(content)) return true
92
98
  return /##\s*TDD/.test(content) || /##\s*验证/.test(content) || /##\s*Verify/.test(content)
93
99
  }
94
100
 
@@ -230,13 +236,13 @@ export function validateBlueprintConsistency(changeDir) {
230
236
  taskInfo.set(taskId, { dependsOn, allowedPaths, hasAcceptance, hasTdd, file })
231
237
 
232
238
  if (allowedPaths.length === 0) {
233
- errors.push(`${taskId} (${file}): 缺少 allowed_paths`)
239
+ errors.push(`${taskId} (${file}): frontmatter 缺少 allowed_paths(需非空数组,列出本 task 真实改动的源文件;回归类 task 无源码改动时填被验证的关键入口文件)`)
234
240
  }
235
241
  if (!hasAcceptance) {
236
- errors.push(`${taskId} (${file}): 缺少「验收标准」章节`)
242
+ errors.push(`${taskId} (${file}): 缺少验收标准——frontmatter 需有 acceptance: 列表字段,或 body 需有「## 验收标准」/「## Acceptance」章节`)
237
243
  }
238
244
  if (!hasTdd) {
239
- warnings.push(`${taskId} (${file}): 缺少 TDD/验证步骤章节`)
245
+ warnings.push(`${taskId} (${file}): 缺少验证步骤——frontmatter 需有 verify: 字段,或 body 需有「## TDD」/「## 验证」/「## Verify」章节`)
240
246
  }
241
247
 
242
248
  for (const p of allowedPaths) {
@@ -447,6 +447,10 @@ TaskCard 格式规则(必须严格遵守):
447
447
  - 填写后 plan-postcheck 会做硬对账:consumer 的每个 expects_from[provider].needs 字段必须在对应 provider 的 provides.fields 里,否则 plan 阶段阻断(不进入 execute)
448
448
  - 不要把内部实现字段塞进 provides;只暴露给其他 task 的对外契约形状
449
449
  - 如果存在 decisions.md,无法覆盖的 D-xxx@vN 在 constraints 中标注
450
+ - **保存前格式自检**(plan-postcheck 会硬校验,不通过到 Step 4 会批量报错,先在这里逐条自查):
451
+ - frontmatter 字段齐全:id、title、title_zh、author、created_at、priority、depends_on、blocks、allowed_paths、goal、implementation、acceptance、verify、constraints
452
+ - allowed_paths 非空(至少一个真实源文件路径;回归类 task 无源码改动时填被验证的关键入口文件)
453
+ - acceptance 与 verify 都在 frontmatter 内(漏写会被 plan-postcheck 报「缺少验收标准」/「缺少验证步骤」)
450
454
  - 写完后用 Write tool 保存到文件
451
455
  \`\`\``
452
456
  }).join('\n\n')
@@ -478,8 +482,7 @@ ${subagentPrompts}
478
482
 
479
483
  ## 验收(生成后自查,不另开步骤)
480
484
  - 每个 task-N.md 文件存在且非空
481
- - frontmatter 包含:id、title、author、created_at、priority、depends_on、blocks、allowed_paths
482
- - body 包含:goal、implementation、acceptance、verify、constraints
485
+ - frontmatter 包含:id、title、author、created_at、priority、depends_on、blocks、allowed_paths、goal、implementation、acceptance、verify、constraints
483
486
  - 每个 task 总长度 20~40 行
484
487
  - **一致性自查**:
485
488
  - allowed_paths 有无冲突
@@ -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
  *
package/src/worktree.js CHANGED
@@ -1078,8 +1078,12 @@ export class WorktreeManager {
1078
1078
  if (!status) {
1079
1079
  return gitQuiet(worktreePath, 'rev-parse HEAD');
1080
1080
  }
1081
+ // --no-verify:baseline 是锚点不是交付物,只是把主仓库 dirty 文件快照到 worktree
1082
+ // 分支上以便区分「前置 baseline」与「子代理新增改动」。它不该触发项目 pre-commit
1083
+ // hook(如 ruff format),否则主仓库 dirty 文件中任一不达标的会被 hook reformat
1084
+ // 致 commit 失败 → worktree 创建失败 → execute 无法启动。
1081
1085
  execSync(
1082
- `git commit -m "sillyspec: baseline checkpoint for ${changeName}"`,
1086
+ `git commit --no-verify -m "sillyspec: baseline checkpoint for ${changeName}"`,
1083
1087
  { cwd: worktreePath, encoding: 'utf8', stdio: ['pipe','pipe','pipe'], env }
1084
1088
  );
1085
1089
  const hash = git(worktreePath, 'rev-parse HEAD');