sillyspec 3.26.8 → 3.26.10

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.
Files changed (55) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +1 -2
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -2
  3. package/.claude/skills/sillyspec-execute/SKILL.md +11 -3
  4. package/.claude/skills/sillyspec-explore/SKILL.md +0 -1
  5. package/.claude/skills/sillyspec-plan/SKILL.md +1 -2
  6. package/.claude/skills/sillyspec-quick/SKILL.md +4 -5
  7. package/.claude/skills/sillyspec-scan/SKILL.md +2 -2
  8. package/.claude/skills/sillyspec-status/SKILL.md +0 -1
  9. package/.claude/skills/sillyspec-verify/SKILL.md +4 -3
  10. package/README.md +5 -9
  11. package/package.json +2 -2
  12. package/packages/dashboard/server/index.js +14 -8
  13. package/src/change-risk-profile.js +5 -207
  14. package/src/config-schema.js +6 -1
  15. package/src/constants.js +97 -106
  16. package/src/db-engine.js +1 -1
  17. package/src/docs-check.js +34 -5
  18. package/src/git-helper.js +2 -2
  19. package/src/hooks/worktree-guard.js +1 -1
  20. package/src/index.js +96 -13
  21. package/src/init.js +14 -3
  22. package/src/machine-interface.js +7 -4
  23. package/src/progress/stage-machine.js +3 -0
  24. package/src/quicklog.js +203 -4
  25. package/src/run/command.js +101 -3
  26. package/src/run/complete-handlers.js +1399 -1254
  27. package/src/run/complete.js +9 -1
  28. package/src/run/concurrent-detect.js +22 -0
  29. package/src/run/gates.js +67 -1
  30. package/src/run/prompt.js +761 -713
  31. package/src/run/quick-audit.js +146 -137
  32. package/src/run/shared.js +1215 -1109
  33. package/src/run/stage.js +17 -1
  34. package/src/scan-diff.js +388 -0
  35. package/src/scan-staleness.js +18 -13
  36. package/src/spec-sync.js +319 -0
  37. package/src/stage-contract-spec.js +1 -1
  38. package/src/stage-review.js +1 -1
  39. package/src/stages/archive.js +2 -1
  40. package/src/stages/brainstorm.js +2 -2
  41. package/src/stages/execute.js +1003 -985
  42. package/src/stages/plan-postcheck.js +22 -1
  43. package/src/stages/plan.js +74 -40
  44. package/src/stages/quick.js +13 -5
  45. package/src/stages/scan.js +4 -3
  46. package/src/stages/verify.js +14 -12
  47. package/src/sync.js +22 -0
  48. package/src/task-review.js +12 -4
  49. package/src/workflow.js +8 -185
  50. package/src/worktree-apply.js +122 -0
  51. package/src/worktree.js +50 -6
  52. package/templates/claude-instruction.md +1 -1
  53. package/templates/prompts/taskcard-rules.md +1 -0
  54. package/templates/prompts/verify-probes.md +2 -2
  55. package/templates/skills/sillyspec-onboard/SKILL.md +0 -92
package/src/workflow.js CHANGED
@@ -48,7 +48,7 @@ export function loadWorkflow(cwd, name, validate = true) {
48
48
  * @param {object} wf - workflow 定义
49
49
  * @returns {string[]} 错误列表,空数组表示通过
50
50
  */
51
- export function validateWorkflow(wf) {
51
+ function validateWorkflow(wf) {
52
52
  const errors = []
53
53
  const roles = wf.roles || []
54
54
  const roleIds = new Set(roles.map(r => r.id))
@@ -256,8 +256,13 @@ export function runPostCheck(wf, cwd, projectName, placeholders = {}, specBase)
256
256
  let json = JSON.stringify(resolved)
257
257
  for (const [key, value] of Object.entries(allPlaceholders)) {
258
258
  // 支持 {key} 和 <key> 两种占位符语法
259
- json = json.replace(new RegExp(`\{${key}\}`, 'g'), value)
260
- json = json.replace(new RegExp(`<${key}>`, 'g'), value)
259
+ // 字符串值先做 JSON 转义再嵌入:Windows 路径等含反斜杠/引号的值裸替换进 JSON 字面量会
260
+ // 破坏 parse(实测报「Bad escaped character」→ scan 深度扫描质量门静默 fail-open)。
261
+ // JSON.stringify(v).slice(1,-1) 产出合法的 JSON 字符串内容(反斜杠→\\、引号→\");
262
+ // 非字符串值保持 String(v) 原样(数字占位符不被引号包裹,语义不变)。
263
+ const embedValue = (v) => (typeof v === 'string' ? JSON.stringify(v).slice(1, -1) : String(v))
264
+ json = json.replace(new RegExp(`\{${key}\}`, 'g'), embedValue(value))
265
+ json = json.replace(new RegExp(`<${key}>`, 'g'), embedValue(value))
261
266
  }
262
267
  resolved = JSON.parse(json)
263
268
 
@@ -537,188 +542,6 @@ export function formatCheckReport(result) {
537
542
  return lines.join('\n')
538
543
  }
539
544
 
540
- // ─── 兼容适配层 ───
541
-
542
- /**
543
- * 兼容旧接口:generateRetryPrompt
544
- * @deprecated 直接用 runPostCheck 返回的 retry_prompts
545
- */
546
- export function generateRetryPrompt(wf, checkResult, projectName) {
547
- const resolved = replaceProjectPlaceholder(wf, projectName)
548
- const lines = []
549
- lines.push('上一次 workflow 执行存在失败项,请重试。\n')
550
-
551
- const roles = resolved.roles || []
552
- const roleResults = checkResult.roles || []
553
- for (const r of roleResults) {
554
- if (r.status === 'pass') continue
555
- const role = roles.find(rl => rl.id === r.id)
556
- if (!role) continue
557
-
558
- lines.push(`### 失败角色:${r.name} (${r.id})`)
559
- lines.push(`失败原因:`)
560
- const roleFailures = (checkResult.failures || []).filter(f => f.role_id === r.id)
561
- for (const f of roleFailures) {
562
- lines.push(`- ${f.message}`)
563
- }
564
- lines.push('')
565
-
566
- for (const output of (role.outputs || [])) {
567
- lines.push(`目标文件:\`${output.path}\``)
568
- }
569
-
570
- if (role.constraints && role.constraints.length > 0) {
571
- lines.push('约束:')
572
- for (const c of role.constraints) {
573
- lines.push(`- ${c}`)
574
- }
575
- }
576
-
577
- lines.push('')
578
- lines.push('⚠️ 你必须确保文件写入指定路径。不要只报告完成,请用 write 工具实际写入。')
579
- lines.push('')
580
- }
581
-
582
- return lines.join('\n')
583
- }
584
-
585
- // ─── Role Prompt 生成(Level 2)───
586
-
587
- /**
588
- * 根据 workflow role 定义生成子代理 prompt
589
- * @param {object} wf - workflow 定义
590
- * @param {string} roleId - 角色ID
591
- * @param {string} projectName - 项目名
592
- * @param {object} context - 额外上下文(envSummary, missingDocs 等)
593
- * @returns {string|null} 生成的 prompt,或 null(角色不存在)
594
- */
595
- export function generateRolePrompt(wf, roleId, projectName, context = {}) {
596
- const resolved = replaceProjectPlaceholder(wf, projectName)
597
- const role = (resolved.roles || []).find(r => r.id === roleId)
598
- if (!role) return null
599
-
600
- const lines = []
601
- lines.push(`## 子代理任务:${role.name} (${roleId})`)
602
- lines.push('')
603
- lines.push(`项目:${projectName}`)
604
-
605
- // 任务描述
606
- if (role.task) {
607
- lines.push(`任务:${role.task}`)
608
- }
609
-
610
- // 依赖角色的输出(depends_on + from_role)
611
- const deps = role.depends_on || []
612
- if (deps.length > 0) {
613
- lines.push('')
614
- lines.push('前置依赖(已完成角色的输出):')
615
- const inputs = role.inputs || {}
616
- for (const depId of deps) {
617
- const depRole = (resolved.roles || []).find(r => r.id === depId)
618
- if (!depRole) continue
619
- if (inputs.from_role === depId) {
620
- lines.push(`- ${depRole.name}(${depId}):${inputs.output_description || ''}`)
621
- if (inputs.output) {
622
- const depOutput = (depRole.outputs || []).find(o => {
623
- const outputName = o.name || o.path?.split('/').pop()?.replace(/\.md$/, '') || ''
624
- return outputName === inputs.output
625
- })
626
- if (depOutput) {
627
- lines.push(` 输出文件:${depOutput.path}`)
628
- }
629
- }
630
- } else {
631
- lines.push(`- ${depRole.name}(${depId})`)
632
- for (const o of (depRole.outputs || [])) {
633
- lines.push(` 输出:${o.path}`)
634
- }
635
- }
636
- }
637
- }
638
-
639
- // 输入提示
640
- const inputs = role.inputs || {}
641
- const inputPaths = inputs.paths || []
642
- const inputHints = inputs.hints || {}
643
- if (inputPaths.length > 0) {
644
- lines.push('')
645
- lines.push('搜索范围:')
646
- for (const p of inputPaths) {
647
- lines.push(`- ${p}`)
648
- }
649
- }
650
- if (inputHints.grep_patterns && inputHints.grep_patterns.length > 0) {
651
- lines.push('')
652
- lines.push('搜索关键词:')
653
- lines.push(`- ${inputHints.grep_patterns.join(', ')}`)
654
- }
655
-
656
- // 额外上下文
657
- if (context.envSummary) {
658
- lines.push('')
659
- lines.push('环境探测结果:')
660
- lines.push(context.envSummary)
661
- }
662
- if (context.missingDocs) {
663
- lines.push('')
664
- lines.push('缺失文档列表:')
665
- lines.push(context.missingDocs)
666
- }
667
-
668
- // 输出目标
669
- const outputs = role.outputs || []
670
- lines.push('')
671
- lines.push('目标文件:')
672
- for (const o of outputs) {
673
- lines.push(`- \`${o.path}\``)
674
- }
675
-
676
- // 约束
677
- if (role.constraints && role.constraints.length > 0) {
678
- lines.push('')
679
- lines.push('约束:')
680
- for (const c of role.constraints) {
681
- lines.push(`- ${c}`)
682
- }
683
- }
684
-
685
- // 检查要求(告诉子代理需要满足什么)
686
- for (const o of outputs) {
687
- const checks = o.checks || []
688
- for (const check of checks) {
689
- if (check.type === 'contains_sections' && check.sections) {
690
- lines.push('')
691
- lines.push(`必须包含章节:${check.sections.map(s => `"## ${s}"`).join(', ')}`)
692
- }
693
- if (check.type === 'min_lines') {
694
- lines.push(`文件长度要求:至少 ${check.min} 行`)
695
- }
696
- }
697
- }
698
-
699
- lines.push('')
700
- lines.push('⚠️ 必须用 write 工具将文件写入磁盘!写完后用 read 工具确认文件存在!')
701
-
702
- return lines.join('\n')
703
- }
704
-
705
- /**
706
- * 为 workflow 的所有角色生成 role prompts
707
- * @param {object} wf - workflow 定义
708
- * @param {string} projectName - 项目名
709
- * @param {object} context - 额外上下文
710
- * @returns {Array<{ roleId: string, roleName: string, prompt: string }>}
711
- */
712
- export function generateAllRolePrompts(wf, projectName, context = {}) {
713
- const resolved = replaceProjectPlaceholder(wf, projectName)
714
- const roles = resolved.roles || []
715
- return roles.map(role => ({
716
- roleId: role.id,
717
- roleName: role.name || role.id,
718
- prompt: generateRolePrompt(resolved, role.id, projectName, context)
719
- }))
720
- }
721
-
722
545
  // ─── Workflow Run 归档 ───
723
546
 
724
547
 
@@ -697,6 +697,124 @@ function rollbackApply(projectRoot, trackedFiles, newFiles) {
697
697
  return { conflicts, error };
698
698
  }
699
699
 
700
+ /**
701
+ * merge 前预对齐 baseline 并行文件到 main 版(D-002@v1 / task-02)。
702
+ *
703
+ * 背景:baseline checkpoint 把 execute 启动时主仓 dirty 的并行会话文件快照进了 worktree 分支
704
+ * (worktree.js _createBaselineCheckpoint)。apply --merge 时主仓常已把这批文件推进(并行会话
705
+ * 落了 commit),分支侧旧快照与 main 新版在 merge-base 两侧同改同一文件 → merge 冲突。预对齐在
706
+ * merge 前把这批文件 checkout 成 main 版并提交到分支,使三方合并对该文件 ours == theirs,
707
+ * 消除冲突主因。
708
+ *
709
+ * 过滤集(四条件全满足才对齐;任一不满足跳过该文件):
710
+ * a. baseline checkpoint 文件集(已提交口径):git diff --name-only <baseHash>..<baselineCommit>
711
+ * ——不用 result.changedFiles(工作区口径且已过滤基础设施文件)
712
+ * b. ∩ main 已推进集:git diff --name-only <merge-base>..<main HEAD>
713
+ * (merge-base = 分支与 main HEAD 的最近公共祖先)
714
+ * c. ∖ 分支已变更集:git diff --name-only <baselineCommit>..<branch> 含该文件(本变更交付,不动)
715
+ * d. ∖ worktree 工作区 dirty 集:git status --porcelain -- <file> 非空(未提交改动会被 checkout 覆盖)
716
+ * 另排除「main HEAD 与分支 tip 内容已一致」的文件(对齐无意义,且防 nothing-to-commit 空提交)。
717
+ *
718
+ * 执行 cwd:checkout/commit 在 worktreePath(改的是 worktree 分支的 index+工作区);mainHead /
719
+ * merge-base 锚点在 projectRoot 读取。merge 本身仍在 projectRoot(applyByMerge 原逻辑不动)。
720
+ *
721
+ * 降级(不阻断):不适用场景(无 baseline checkpoint / 非 worktree 模式(in-place 的 worktreePath
722
+ * 就是主工作区,在其上 commit 等于绕过验收污染 main;native-worktree 是外部隔离环境)/ worktree
723
+ * 目录丢失 / worktree HEAD ≠ 分支 tip(checkout+commit 会落错分支,merge 的却是 meta.branch))
724
+ * 静默跳过;任一 git 步骤失败 → best-effort 回滚已 checkout 文件 + console.warn + result.warnings
725
+ * → 调用方走原 merge 路径(降级不阻断,D-002@v1 约束③)。
726
+ *
727
+ * @param {object} p
728
+ * @param {object} p.meta - worktree meta(baseHash / baselineCommit / worktreePath / mode)
729
+ * @param {string} p.branch - 将被 merge 的分支名(meta.branch)
730
+ * @param {string} p.projectRoot - 主仓库根(merge 的执行侧)
731
+ * @param {object} p.result - applyWorktree 的 result(预对齐信息写入 result.warnings,可追溯,约束④)
732
+ * @returns {{ aligned: string[], commit: string|null }} aligned=本次对齐的文件(空数组=未对齐)
733
+ */
734
+ function preAlignBaselineToMain({ meta, branch, projectRoot, result }) {
735
+ const pushWarning = (msg) => {
736
+ result.warnings = result.warnings || [];
737
+ result.warnings.push(msg);
738
+ };
739
+
740
+ // 不适用场景(非失败,静默跳过)
741
+ if (!meta || !meta.baseHash || !meta.baselineCommit) return { aligned: [], commit: null };
742
+ if (meta.mode && meta.mode !== 'worktree') return { aligned: [], commit: null };
743
+ const worktreePath = meta.worktreePath;
744
+ if (!worktreePath || !existsSync(worktreePath)) return { aligned: [], commit: null };
745
+ const wtHead = gitQuiet(worktreePath, ['rev-parse', 'HEAD']);
746
+ const branchTip = gitQuiet(projectRoot, ['rev-parse', branch]);
747
+ if (!wtHead || !branchTip || wtHead !== branchTip) return { aligned: [], commit: null };
748
+
749
+ const { baseHash, baselineCommit } = meta;
750
+ const done = []; // 已 checkout 的文件(失败回滚用)
751
+ try {
752
+ // (a) baseline checkpoint 文件集(已提交口径;--no-renames:rename 退化为 D+A,两侧路径都进集)
753
+ const baselineSet = new Set(
754
+ (git(worktreePath, ['diff', '--no-renames', '--name-only', baseHash, baselineCommit]) || '')
755
+ .split('\n').filter(Boolean)
756
+ );
757
+ if (baselineSet.size === 0) return { aligned: [], commit: null };
758
+
759
+ // (b) main 已推进集(merge-base 后 main HEAD 的树差异;锚点拿不到 → 交原 merge,不算失败)
760
+ const mainHead = git(projectRoot, ['rev-parse', 'HEAD']);
761
+ const mergeBase = git(projectRoot, ['merge-base', branch, 'HEAD']);
762
+ if (!mainHead || !mergeBase) return { aligned: [], commit: null };
763
+ const mainAdvanced = new Set(
764
+ (git(projectRoot, ['diff', '--no-renames', '--name-only', mergeBase, mainHead]) || '')
765
+ .split('\n').filter(Boolean)
766
+ );
767
+
768
+ // (c) 分支已变更集(baselineCommit → 分支 tip 的已提交交付,不动)
769
+ const branchChanged = new Set(
770
+ (git(worktreePath, ['diff', '--no-renames', '--name-only', baselineCommit, branchTip]) || '')
771
+ .split('\n').filter(Boolean)
772
+ );
773
+
774
+ // (d) worktree 工作区 dirty + 两侧 HEAD 同内容排除 → 最终对齐集
775
+ const candidates = [];
776
+ for (const f of baselineSet) {
777
+ if (!mainAdvanced.has(f) || branchChanged.has(f)) continue;
778
+ // dirty(checkout 会覆盖 worktree 未提交改动)→ 跳过该文件
779
+ if ((gitQuiet(worktreePath, ['status', '--porcelain', '--', f]) || '') !== '') continue;
780
+ // main HEAD 与分支 tip 内容已一致 → 对齐无意义 + 防空提交
781
+ if ((gitQuiet(worktreePath, ['diff', '--name-only', mainHead, branchTip, '--', f]) || '') === '') continue;
782
+ candidates.push(f);
783
+ }
784
+ if (candidates.length === 0) return { aligned: [], commit: null };
785
+
786
+ // 对齐:逐文件 checkout main 版(worktree 内执行,同时更新 worktree 分支的 index+工作区)
787
+ for (const f of candidates) {
788
+ git(worktreePath, ['checkout', mainHead, '--', f]);
789
+ done.push(f);
790
+ }
791
+ // 显式 pathspec 提交:只提交对齐文件,不扫入 worktree 内其他 staged 改动(坑 git-commit-sweeps-prestaged)。
792
+ // --no-verify:对齐 commit 与 baseline checkpoint 同性质(锚点非交付物,worktree.js _createBaselineCheckpoint
793
+ // 同款处理)——项目 pre-commit hook(如 husky lint+全量测试)会把预对齐拖成分钟级甚至失败降级。
794
+ git(worktreePath, [
795
+ 'commit', '--no-verify', '-m',
796
+ `sillyspec: align baseline files to main (pre-merge, ${done.length} files)`,
797
+ '--', ...done,
798
+ ]);
799
+ const shortHash = gitQuiet(worktreePath, ['rev-parse', '--short', 'HEAD']);
800
+ pushWarning(
801
+ `merge 前预对齐 ${done.length} 个 baseline 并行文件到 main 版` +
802
+ `${shortHash ? `(align commit ${shortHash})` : ''}:${done.join(', ')}`
803
+ );
804
+ return { aligned: done, commit: shortHash };
805
+ } catch (e) {
806
+ // 降级:任一步失败 → best-effort 回滚已 checkout 文件(checkout <branchTip> -- f 还原 index+工作区)
807
+ for (const f of done) {
808
+ gitQuiet(worktreePath, ['checkout', branchTip, '--', f]);
809
+ }
810
+ const msg = `baseline 预对齐失败,已跳过(降级走原 merge 路径):${(e.message || '').split('\n')[0]}` +
811
+ (done.length > 0 ? `;已回滚 ${done.length} 个已 checkout 文件` : '');
812
+ console.warn(`⚠️ ${msg}`);
813
+ pushWarning(msg);
814
+ return { aligned: [], commit: null };
815
+ }
816
+ }
817
+
700
818
  /**
701
819
  * baseline 漂移时的 merge 降级路径(D-001)。
702
820
  *
@@ -705,6 +823,7 @@ function rollbackApply(projectRoot, trackedFiles, newFiles) {
705
823
  * git merge 比 patch 鲁棒,能处理 baseline 漂移 + 潜在冲突。会引入合并提交
706
824
  * (D-002:与 worktree.md:84「patch 而非 merge 保持线性历史」架构决策的张力——
707
825
  * 仅作 --merge 显式 opt-in,不改变默认 patch 行为)。
826
+ * merge 前先经 preAlignBaselineToMain 预对齐 baseline 并行文件到 main 版(D-002@v1 / task-02,失败降级走原 merge)。
708
827
  *
709
828
  * merge 冲突时不自动解决:git merge --abort 回滚到合并前状态 + 报冲突文件列表。
710
829
  *
@@ -720,6 +839,9 @@ function applyByMerge(result, changeName, projectRoot, wm) {
720
839
  // 用 meta.branch(native-worktree 模式分支名可能不是 sillyspec/<change>),不硬编码。
721
840
  const branch = meta.branch || `sillyspec/${changeName}`;
722
841
 
842
+ // --- merge 前预对齐(D-002@v1 / task-02):baseline 并行旧文件对齐 main 版,消除 merge 冲突主因 ---
843
+ preAlignBaselineToMain({ meta, branch, projectRoot, result });
844
+
723
845
  try {
724
846
  git(projectRoot, ['merge', '--no-ff', branch], { timeout: 30000 });
725
847
  } catch (e) {
package/src/worktree.js CHANGED
@@ -58,7 +58,7 @@ export function detectIsolation(cwd = process.cwd()) {
58
58
  * @param {string} cwd - 项目根目录
59
59
  * @returns {{ ignored: boolean, path: string }}
60
60
  */
61
- export function checkWorktreeDirIgnored(cwd = process.cwd()) {
61
+ function checkWorktreeDirIgnored(cwd = process.cwd()) {
62
62
  const relPath = WORKTREES_REL;
63
63
  try {
64
64
  git(cwd, ['check-ignore', '-q', relPath]);
@@ -195,7 +195,7 @@ function validateChangeName(changeName) {
195
195
  * @param {string} cwd
196
196
  * @returns {{ supported: boolean, version: string|null, reason?: string }}
197
197
  */
198
- export function isGitWorktreeSupported(cwd = process.cwd()) {
198
+ function isGitWorktreeSupported(cwd = process.cwd()) {
199
199
  try {
200
200
  const raw = git(cwd, ['--version']);
201
201
  const match = raw.match(/git version (\d+)\.(\d+)/);
@@ -757,6 +757,43 @@ export class WorktreeManager {
757
757
  * partial=有残留(目录/meta/git 注册未清);skipped=无需清理;kept=native-worktree 保留。
758
758
  * residual:未清干净的路径/引用列表(空数组表示干净)。
759
759
  */
760
+ /**
761
+ * fail-closed:清理删分支前的审计引用校验(2026-08-18,两例实录:execute --done 批量完成 /
762
+ * worktree apply 后自动 cleanup 均无条件删分支 ref,task commit 变 dangling)。
763
+ * apply 只复制文件内容不携带 commit(主仓重 commit 后 hash 不同),分支 ref 一删,
764
+ * task review.json 的 base/head 引用即悬空(gc 后真丢,git diff base..head 无法复核)。
765
+ * 有引用 → cleanup 保留分支并提示手动删;校验自身异常也按"有引用"处理(宁保留勿误删)。
766
+ * @private
767
+ * @param {string} branch - 待删分支名
768
+ * @returns {string[]} 引用该分支 commit 的 review.json 路径列表(空 = 无引用,删分支安全)
769
+ */
770
+ _branchReviewReferences(branch) {
771
+ const runsDir = join(this.cwd, '.sillyspec', '.runtime', 'execute-runs');
772
+ if (!existsSync(runsDir)) return [];
773
+ const branchCommits = new Set(
774
+ (gitQuiet(this.cwd, ['rev-list', branch]) || '').split('\n').map(s => s.trim()).filter(Boolean)
775
+ );
776
+ if (branchCommits.size === 0) return [];
777
+ const refs = [];
778
+ const visit = (dir) => {
779
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
780
+ const p = join(dir, entry.name);
781
+ if (entry.isDirectory()) visit(p);
782
+ else if (entry.name === 'review.json') {
783
+ try {
784
+ const r = JSON.parse(readFileSync(p, 'utf8'));
785
+ for (const key of ['base', 'head']) {
786
+ const h = typeof r[key] === 'string' ? r[key].trim() : '';
787
+ if (h && branchCommits.has(h)) { refs.push(p); break; }
788
+ }
789
+ } catch { /* review.json 损坏不参与判定 */ }
790
+ }
791
+ }
792
+ };
793
+ visit(runsDir);
794
+ return refs;
795
+ }
796
+
760
797
  cleanup(changeName, { force = false, maxRetries = 3 } = {}) {
761
798
  const name = validateChangeName(changeName);
762
799
  const meta = this.getMeta(name);
@@ -862,12 +899,19 @@ export class WorktreeManager {
862
899
  // prune 失败不阻断
863
900
  }
864
901
 
865
- // 4. 删除分支(忽略分支不存在的错误)
902
+ // 4. 删除分支(fail-closed:task review 引用可达性校验。force 也不绕过——force 语义 =
903
+ // 丢弃内容,不含丢弃审计链;确要删用 git branch -D 手动执行)
866
904
  try {
867
- gitQuiet(this.cwd, ['branch', '-D', branch]);
868
- details.push('branch deleted');
905
+ const reviewRefs = this._branchReviewReferences(branch);
906
+ if (reviewRefs.length > 0) {
907
+ details.push(`branch kept: ${reviewRefs.length} 个 task review.json 引用分支上的 commit(base/head 审计保护)`);
908
+ console.log(`🔗 分支保留:${branch} 上有 ${reviewRefs.length} 个 task review.json 引用其 commit(apply 只复制文件内容,ref 删除后 base/head 悬空无法复核)。确要删除:git branch -D ${branch}`);
909
+ } else {
910
+ gitQuiet(this.cwd, ['branch', '-D', branch]);
911
+ details.push('branch deleted');
912
+ }
869
913
  } catch {
870
- // 分支可能已被删除,幂等跳过
914
+ // 分支可能已被删除(幂等跳过);引用校验自身异常也归入此分支——不因校验失败而误删
871
915
  }
872
916
 
873
917
  // 5. 清除 meta 目录(in-place 模式也执行——这是 in-place meta 残留的修复点)
@@ -12,7 +12,7 @@
12
12
  6. **判规模选档**:≤3 文件、范围明确走 `quick`;多阶段 / 架构级走完整流程。
13
13
  7. **代码先行不补流程(倒推 B 模式)**:代码若已先写好,**不回头补 brainstorm/plan 装样子**——用 `quick --done` 收尾 + 补 quicklog 条目,把已落盘改动如实登记进进度库。
14
14
  8. **实证核验再 `--done`**:`quick --done` 前,触及 `src`/`test` 的改动先 `npm test` + `npm run lint`;纯 doc/配置改动可跳过(lint 不扫 docs/,空转无信息),以落盘文件与测试结果为准,不信口头"已完成"。
15
- 9. **中途停下用 `sillyspec status` / `sillyspec resume` 存进度**,不直接 commit 半成品。
15
+ 9. **中途停下不靠额外命令存进度**——进度已由上一次 `--done` 自动落盘;恢复时用 `sillyspec progress show` 查看进度,再用 `sillyspec run <stage>` 续跑,不直接 commit 半成品。
16
16
  10. **实现完成后对照文档验收**(design.md / 模块文档),并检查是否影响已有测试。
17
17
  11. **非测试逻辑本身有误时,禁止改测试来"通过"**——修逻辑,不修测试。
18
18
  12. **hook 拦截提交时禁止跳过**(`.husky/pre-push`),修复问题后再提交。
@@ -1,6 +1,7 @@
1
1
  TaskCard 格式规则(必须严格遵守):
2
2
  - 总长度 20~40 行,不要写成长文档
3
3
  - frontmatter 只含必要字段,不加 estimated_hours
4
+ - title 用英文短语(kebab 风格动词短语,如 "Add readonly auxiliary constant"),title_zh 用对应中文标题——两字段是同一任务的双语表示,非不同信息;归档产物与 sillyhub 平台解析各取所需(title_zh 为中文展示首选)
4
5
  - goal: 一句话,用 > 多行字符串
5
6
  - implementation: 列表,每条一个具体步骤
6
7
  - acceptance: 列表,每条可独立验证(不是表格)
@@ -46,11 +46,11 @@ grep -rl "<关键词>" <源码目录>/ --include="*.java" --include="*.js" --inc
46
46
  3. Diff 对账:
47
47
  - 前端调用路径在 backend 端点清单中找不到 → **❌ Missing backend endpoint**(advisory:CLI 复核后仅 warn,不硬阻断归档;但 contract gap 是真实集成缺陷,应诚实标 FAIL 并回 execute 补端点)
48
48
  - backend 端点在前端无调用 → ⚠️ Unused backend endpoint(warning,不阻断)
49
- 4. 输出对账结果表格:
49
+ 4. 输出对账结果表格(示例用通用占位路径,实际填本仓真实文件):
50
50
 
51
51
  | 状态 | 前端调用 | 后端端点 | 文件 |
52
52
  |---|---|---|---|
53
- | ❌ missing | GET /api/ppm/project-plan/{param}/plan-nodes | — | frontend/src/lib/ppm/plan.ts |
53
+ | ❌ missing | GET /api/<域>/<资源>/<param> | — | <前端调用所在文件,如 src/lib/<模块>.ts> |
54
54
 
55
55
  如果发现 Missing backend endpoint,在验证报告中标记为 ❌ contract gap(CLI 仅 advisory 提示、不硬阻断归档;但缺口真实存在——诚实判 FAIL 并回 execute 补齐端点,勿因 CLI 不拦而放行)。
56
56
 
@@ -1,92 +0,0 @@
1
- ---
2
- name: sillyspec-onboard
3
- description: 为未安装 SillySpec 的全新项目接入规范驱动开发工作流——引导安装 CLI、初始化接入、建立项目库,并把「用 SillySpec 走流程」确立为项目默认工作方式。适合用户说"给这个项目接上 sillyspec"或新项目开工前确认流程。
4
- ---
5
-
6
- ## 交互规范
7
-
8
- **当需要用户从多个选项中做出选择时,必须使用 Claude Code 内置的 AskUserQuestion 工具,将选项以参数传入。** 涉及是否全局安装 CLI / 是否建立项目库等决策,让用户亲手拍板,不替用户做决定。
9
-
10
- ## 何时使用
11
-
12
- - 目标项目**没有安装/未接入** SillySpec(判断:项目根无 `.sillyspec/` 目录;或 `sillyspec --version` 命令不可用)
13
- - 用户想让 SillySpec 成为该项目的开发流程控制器
14
-
15
- ## 铁律
16
-
17
- - **必须用 exec 工具(shell)执行 CLI,不编造命令**;命中的命令固定为下方公开 CLI,不自行发明子命令
18
- - 本 skill 只做**接入动作**(安装/初始化/验证),不写业务代码、不改项目源码
19
- - 全程在主工作区根执行 CLI,不进入 worktree
20
- - 每个决策点用 AskUserQuestion 让用户拍板,不静默代选
21
-
22
- ## 核心流程
23
-
24
- ### Step 0: 判定接入状态
25
-
26
- ```bash
27
- # 项目根是否已有 .sillyspec/(已接入)
28
- ls -d .sillyspec 2>/dev/null
29
- # CLI 是否可用
30
- sillyspec --version 2>/dev/null
31
- ```
32
-
33
- - 已接入(`.sillyspec/` 存在)→ 不重复接入,提示用户项目已用 SillySpec 管理,退出。
34
- - 未接入但 CLI 已装 → 跳到 Step 2。
35
- - 都无 → 继续 Step 1。
36
-
37
- ### Step 1: 引导安装 CLI(全局)
38
-
39
- ```bash
40
- npm install -g sillyspec
41
- ```
42
-
43
- - 若当前环境沙箱/权限导致 `npm -g` 失败,提示用户用 `! npm install -g sillyspec` 手动执行(输出直达会话)。
44
- - 装完验证:`sillyspec --version` 可输出版本号。
45
-
46
- ### Step 2: 初始化接入项目
47
-
48
- ```bash
49
- sillyspec init
50
- ```
51
-
52
- `init` 会自动:创建 `.sillyspec/`(项目注册、文档中心、知识库、运行时进度库)、检测项目中的 AI 工具(Claude Code 等)、给 `CLAUDE.md` 注入 SillySpec 受管段(若项目已有文件)或生成完整指引模板(若没有)、安装对应命令模板/MCP 工具。
53
-
54
- - 新项目为空目录时建议走 `sillyspec run scan`(见 Step 3)补齐项目库;已有代码但未接入的存量项目,先确认项目状态再决定是否 scan。
55
- - 验证产物:
56
- ```bash
57
- ls .sillyspec/projects/ # 项目注册表
58
- grep -c "SillySpec" CLAUDE.md # 受管段/指引已注入(>0)
59
- ```
60
-
61
- ### Step 3: 建立项目库(知识基石)
62
-
63
- ```bash
64
- sillyspec run scan
65
- ```
66
-
67
- 把项目的代码风格(CONVENTIONS.md)、架构(ARCHITECTURE.md)沉淀为后续 agent 的检索知识。这是「让后续 agent 不瞎猜」的关键一步,新项目建议立即做。
68
-
69
- ### Step 4: 价值认知锚 —— 让后续 agent 愿意用
70
-
71
- 接入完成后,**让"用 SillySpec 走流程"成为项目默认工作方式**靠的是已落盘的载体,不是本 skill(skill 是一次性的):
72
-
73
- 1. 确认价值锚已就位:
74
- - `CLAUDE.md` 含 SillySpec 段(Step 2 已验证)——agent 每次会话读取项目说明书时都会看到「禁止绕过流程 / 新功能走 brainstorm→plan→execute→verify→archive / 小修复走 quick」。
75
- - `.claude/skills/` 含 SillySpec 持续 skill(init 已复制 plan/quick/brainstorm 等)——agent 在开发任务中 `/plan`、`/quick` 即走流程。
76
- - `.sillyspec/` 进度库已建——进度自动落盘,多 agent 并发不互相覆盖。
77
-
78
- 2. 向用户/后续 agent 说明价值(一句话口径,复用 init 注入的陈述,不另造文案):
79
- > SillySpec 是给 Agent 调用的 CLI 流程控制器:你通过 CLI 告诉它"我在哪",它告诉你"下一步做什么";你执行步骤,它校验产出、推进状态,人类只在关键决策点介入审批。多 agent 同时操作代码时进度统一落盘、不丢不回滚。
80
-
81
- 3. 给后续 agent 一个「开工首查」示例:
82
- ```bash
83
- sillyspec progress show # 看项目当前阶段/进行中变更
84
- sillyspec run <stage> # 续跑当前阶段(brainstorm/plan/execute/verify/archive)
85
- ```
86
-
87
- ## 分发与安装(本 skill 是引导型)
88
-
89
- 本 skill 是「接入引导」,只在新项目接入时用一次,**不应随项目复制进 `.claude/skills/` 长期留存**。使用方式二选一:
90
-
91
- - **项目级一次性**:复制本 `SKILL.md` 到目标项目 `.claude/skills/sillyspec-onboard/SKILL.md`,接入完成后删除。
92
- - **全局可用**:复制到 `~/.claude/skills/sillyspec-onboard/SKILL.md`(或 `~/.config/claude/skills/`,随 Claude Code 版本),任何新项目都能呼起 `/sillyspec-onboard`。