sillyspec 3.26.4 → 3.26.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.
Files changed (60) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -0
  2. package/.claude/skills/sillyspec-execute/SKILL.md +1 -0
  3. package/.claude/skills/sillyspec-plan/SKILL.md +1 -0
  4. package/.claude/skills/sillyspec-quick/SKILL.md +5 -3
  5. package/package.json +1 -1
  6. package/src/git-helper.js +21 -8
  7. package/src/index.js +2 -0
  8. package/src/quicklog.js +2 -0
  9. package/src/run/command.js +50 -7
  10. package/src/run/complete-handlers.js +95 -1
  11. package/src/run/complete.js +23 -5
  12. package/src/run/gates.js +1 -1
  13. package/src/run/prompt.js +34 -20
  14. package/src/run/quick-audit.js +5 -6
  15. package/src/run/shared.js +45 -11
  16. package/src/run/stage.js +10 -0
  17. package/src/run.js +5 -5
  18. package/src/stage-contract-spec.js +11 -0
  19. package/src/stage-contract.js +11 -1
  20. package/src/stage-review.js +9 -1
  21. package/src/stages/archive.js +12 -26
  22. package/src/stages/execute.js +45 -0
  23. package/src/stages/plan.js +17 -1
  24. package/src/stages/quick.js +2 -0
  25. package/src/stages/scan.js +2 -2
  26. package/src/stages/verify.js +1 -0
  27. package/src/worktree-apply.js +3 -3
  28. package/src/worktree.js +83 -8
  29. package/packages/dashboard/index.html +0 -15
  30. package/packages/dashboard/package-lock.json +0 -2384
  31. package/packages/dashboard/public/favicon.jpg +0 -0
  32. package/packages/dashboard/public/logo.jpg +0 -0
  33. package/packages/dashboard/public/prototype-dashboard.html +0 -836
  34. package/packages/dashboard/public/prototype-overview.html +0 -256
  35. package/packages/dashboard/src/App.vue +0 -558
  36. package/packages/dashboard/src/components/ActionBar.vue +0 -93
  37. package/packages/dashboard/src/components/CommandPalette.vue +0 -96
  38. package/packages/dashboard/src/components/DetailPanel.vue +0 -137
  39. package/packages/dashboard/src/components/DocPreview.vue +0 -257
  40. package/packages/dashboard/src/components/DocTree.vue +0 -114
  41. package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
  42. package/packages/dashboard/src/components/LogStream.vue +0 -65
  43. package/packages/dashboard/src/components/PipelineStage.vue +0 -95
  44. package/packages/dashboard/src/components/PipelineView.vue +0 -156
  45. package/packages/dashboard/src/components/ProjectCard.vue +0 -187
  46. package/packages/dashboard/src/components/ProjectList.vue +0 -210
  47. package/packages/dashboard/src/components/ProjectOverview.vue +0 -152
  48. package/packages/dashboard/src/components/StageBadge.vue +0 -67
  49. package/packages/dashboard/src/components/StepCard.vue +0 -94
  50. package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
  51. package/packages/dashboard/src/components/detail/DocsDetail.vue +0 -48
  52. package/packages/dashboard/src/components/detail/GitDetail.vue +0 -61
  53. package/packages/dashboard/src/components/detail/TechDetail.vue +0 -43
  54. package/packages/dashboard/src/composables/useDashboard.js +0 -213
  55. package/packages/dashboard/src/composables/useKeyboard.js +0 -119
  56. package/packages/dashboard/src/composables/useLayout.js +0 -131
  57. package/packages/dashboard/src/composables/useWebSocket.js +0 -129
  58. package/packages/dashboard/src/main.js +0 -8
  59. package/packages/dashboard/src/style.css +0 -132
  60. package/packages/dashboard/vite.config.js +0 -18
@@ -414,7 +414,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
414
414
  if (changedFiles.length === 0) {
415
415
  // 没有变更
416
416
  if (!checkOnly) {
417
- wm.cleanup(changeName);
417
+ wm.cleanup(changeName, { force: true });
418
418
  }
419
419
  result.ok = true;
420
420
  return result;
@@ -646,7 +646,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
646
646
 
647
647
  // --- 8. 成功后自动 cleanup(失败不影响整体结果) ---
648
648
  try {
649
- wm.cleanup(changeName);
649
+ wm.cleanup(changeName, { force: true });
650
650
  } catch (cleanupErr) {
651
651
  result.warnings = result.warnings || [];
652
652
  result.warnings.push(`cleanup 失败(不影响应用结果): ${cleanupErr.message}`);
@@ -756,7 +756,7 @@ function applyByMerge(result, changeName, projectRoot, wm) {
756
756
  result.ok = true;
757
757
  try { result.mergeSummary = git(projectRoot, ['log', '--oneline', '-1']); } catch {}
758
758
  try {
759
- wm.cleanup(changeName);
759
+ wm.cleanup(changeName, { force: true });
760
760
  } catch (cleanupErr) {
761
761
  result.warnings = result.warnings || [];
762
762
  result.warnings.push(`cleanup 失败(不影响 merge 结果): ${cleanupErr.message}`);
package/src/worktree.js CHANGED
@@ -236,6 +236,53 @@ export function copyUntrackedEntry(src, dst) {
236
236
  }
237
237
  }
238
238
 
239
+ /**
240
+ * execute 阶段级核验(防空跑谎报,D-002@v1 / FR-04/05/06):聚合 review.changedFiles 的交付文件,
241
+ * 逐个核验存在于 worktree 分支 tree 或 worktree 工作区。两处皆无 = 声称实现但从未落盘。
242
+ *
243
+ * - checked:false:worktreePath 不存在或 branch 不存在(git rev-parse --verify <branch> 失败)
244
+ * → 无法核验(worktree 已丢 / 分支已删),调用方保守提示人工确认。
245
+ * - 逐文件:`git rev-parse --verify --quiet <branch>:<file>` 成功(在分支 tree)或
246
+ * existsSync(worktreePath/<file>)(在工作区)→ verified;两处皆无 → missing。
247
+ * tree 探测用 rev-parse --verify --quiet 而非 `git cat-file -e`:语义等价(按 branch tree 解析
248
+ * blob),但 cat-file 对不存在路径会往 stderr 打 `fatal: path ... does not exist` 噪音
249
+ * (git-helper gitQuiet 只吞退出码不吞 stderr),--quiet 完全静默。
250
+ *
251
+ * git 调用走项目统一入口 gitQuiet(git-helper.js,数组形式 execFileSync 不经 shell,Windows 安全)。
252
+ * 宽松非阻断:本函数只返回结果不 warn/exit,由调用方决定(design 为 warn 不 exit)。
253
+ *
254
+ * @param {object} p
255
+ * @param {string} p.worktreePath - worktree 根目录(绝对路径,meta.worktreePath)
256
+ * @param {string} p.branch - sillyspec/<change> 分支名(核验 tree)
257
+ * @param {string[]} p.changedFiles - review 声称的交付文件(调用方已过滤主仓 repo、非 .sillyspec)
258
+ * @returns {{ missing: string[], verified: string[], checked: boolean }}
259
+ * checked=false 表示无法核验(worktree/分支不存在),调用方保守提示人工确认。
260
+ */
261
+ export function findMissingDeliverables({ worktreePath, branch, changedFiles }) {
262
+ const files = Array.isArray(changedFiles)
263
+ ? changedFiles.filter(f => typeof f === 'string' && f.trim() !== '')
264
+ : []
265
+ // worktree 目录不存在 → 无法核验(目录已丢,如 apply 后 cleanup / 并行 session 清过)
266
+ if (!worktreePath || !existsSync(worktreePath)) {
267
+ return { missing: [], verified: [], checked: false }
268
+ }
269
+ // branch 不存在(rev-parse --verify 失败)→ 无法核验分支 tree
270
+ if (!branch || !gitQuiet(worktreePath, ['rev-parse', '--verify', '--quiet', branch])) {
271
+ return { missing: [], verified: [], checked: false }
272
+ }
273
+ const missing = []
274
+ const verified = []
275
+ for (const raw of files) {
276
+ // 统一正斜杠:review.json 可能落 Windows 反斜杠路径,rev-parse tree 路径一律 /
277
+ const f = raw.replace(/\\/g, '/')
278
+ const inBranch = gitQuiet(worktreePath, ['rev-parse', '--verify', '--quiet', `${branch}:${f}`]) !== null
279
+ const inWorktree = existsSync(join(worktreePath, f))
280
+ if (inBranch || inWorktree) verified.push(raw)
281
+ else missing.push(raw)
282
+ }
283
+ return { missing, verified, checked: true }
284
+ }
285
+
239
286
  export class WorktreeManager {
240
287
  constructor({ cwd, worktreeDir } = {}) {
241
288
  this.cwd = cwd || process.cwd();
@@ -704,8 +751,9 @@ export class WorktreeManager {
704
751
  * 三重清理:git worktree 注册 + worktree 目录 + meta 目录。
705
752
  * @param {string} changeName
706
753
  * @param {{ force?: boolean, maxRetries?: number }} opts
707
- * @returns {{ result: 'cleaned'|'force-cleaned'|'skipped'|'kept'|'partial', mode: string|null, details: string[], residual: string[] }}
708
- * result 取值:cleaned=git remove 成功;force-cleaned=git remove 失败但 fallback 清理完成;
754
+ * @returns {{ result: 'blocked'|'cleaned'|'force-cleaned'|'skipped'|'kept'|'partial', mode: string|null, details: string[], residual: string[] }}
755
+ * result 取值:blocked=有未落主仓交付变更,fail-closed 拒绝清理(需 force 绕过,D-001@v1);
756
+ * cleaned=git remove 成功;force-cleaned=git remove 失败但 fallback 清理完成;
709
757
  * partial=有残留(目录/meta/git 注册未清);skipped=无需清理;kept=native-worktree 保留。
710
758
  * residual:未清干净的路径/引用列表(空数组表示干净)。
711
759
  */
@@ -731,6 +779,21 @@ export class WorktreeManager {
731
779
  return { result: 'kept', mode, details: ['native-worktree: 外部隔离环境,跳过清理'], residual: [] };
732
780
  }
733
781
 
782
+ // fail-closed 保护(D-001@v1):junction 解链与 git worktree remove --force 之前,检查未落主仓交付变更。
783
+ // 清理即蒸发:worktree 有未 commit / 未 apply 到主仓 HEAD 的交付文件,拒绝清理,需显式 --force 绕过。
784
+ // hasUnappliedChanges 判定 main HEAD(byte-identical),git apply --3way 不 commit → apply 后仍判 true,
785
+ // 故 apply 后自动 cleanup 与 execute reset 显式传 force:true 绕过(D-006)。in-place / native-worktree
786
+ // 由 hasUnappliedChanges 内部返回 hasChanges:false,自然跳过保护,零回归。
787
+ if (!force) {
788
+ const check = this.hasUnappliedChanges(name);
789
+ if (check.hasChanges) {
790
+ console.error(`🚫 worktree cleanup 拒绝:${check.changedFiles.length} 个交付变更未落地主工作区 HEAD,清理会丢失代码。`);
791
+ for (const f of check.changedFiles) console.error(` ${f}`);
792
+ console.error(' 请先落地(sillyspec worktree apply <name>)或 commit 到分支,或显式 --force 强制清理。');
793
+ return { result: 'blocked', mode, details: [...details, 'blocked: uncommitted deliverable changes'], residual: [] };
794
+ }
795
+ }
796
+
734
797
  const branch = (meta && meta.branch) || BRANCH_PREFIX + name;
735
798
 
736
799
  // Windows 保护:先解链接 worktree/node_modules(junction 指向主 checkout),
@@ -1045,6 +1108,9 @@ export class WorktreeManager {
1045
1108
  const result = this.cleanup(name);
1046
1109
  if (result.result === 'cleaned' || result.result === 'force-cleaned') {
1047
1110
  fixed.push(`cleaned stale: ${name}`);
1111
+ } else if (result.result === 'blocked') {
1112
+ console.error(`🚫 拒绝清理过期 worktree ${name}: 有未落主仓交付变更,请先 sillyspec worktree apply ${name} 或 commit 到分支,再重试 doctor --fix`);
1113
+ unfixable.push(`cleanup blocked: ${name}(有未落主仓交付变更)`);
1048
1114
  } else {
1049
1115
  unfixable.push(`cleanup skipped: ${name}`);
1050
1116
  }
@@ -1093,7 +1159,10 @@ export class WorktreeManager {
1093
1159
 
1094
1160
  const worktreePath = meta.worktreePath;
1095
1161
  if (!worktreePath || !existsSync(worktreePath)) {
1096
- return { hasChanges: false, changedFiles: [], reason: 'worktree dir not found' };
1162
+ // 目录不存在:无法判定是否有未落代码——分支可能有已 commit 但未 apply 的交付(execute 批量完成
1163
+ // cleanup 删分支 ref 盲区,memory execute-batch-cleanup-deletes-branch-recovery)。保守保留
1164
+ // (hasChanges:true),防 cleanup 误删分支让 commit 变 dangling。目录真丢时用户 --force 强清。
1165
+ return { hasChanges: true, changedFiles: [], reason: 'worktree dir not found (conservative keep)' };
1097
1166
  }
1098
1167
 
1099
1168
  // in-place 模式没有隔离目录,不算有未 apply 的变更
@@ -1107,16 +1176,22 @@ export class WorktreeManager {
1107
1176
 
1108
1177
  const diffBase = meta.baselineCommit || meta.baseHash;
1109
1178
  if (!diffBase) {
1110
- return { hasChanges: false, changedFiles: [], reason: 'no diff base' };
1179
+ // 有 meta 但缺 diff 起点:无法判定是否有未落代码,保守保留(防 cleanup 删分支)
1180
+ return { hasChanges: true, changedFiles: [], reason: 'no diff base (conservative keep)' };
1111
1181
  }
1112
1182
 
1113
1183
  const isDeliverable = f => f && !f.startsWith('.sillyspec/') && f !== 'meta.json';
1114
1184
  try {
1115
1185
  // 1) 候选交付变更(worktree 工作区相对 diffBase)。--no-renames:rename 退化成 D+A,两侧文件都进集
1116
- const tracked = (gitQuiet(worktreePath, ['diff', '--no-renames', '--name-only', diffBase], { timeout: 30000 }) || '')
1117
- .split('\n').filter(Boolean).filter(isDeliverable);
1118
- const untracked = (gitQuiet(worktreePath, ['ls-files', '--others', '--exclude-standard'], { timeout: 30000 }) || '')
1119
- .split('\n').filter(Boolean).filter(isDeliverable);
1186
+ const trackedRaw = gitQuiet(worktreePath, ['diff', '--no-renames', '--name-only', diffBase], { timeout: 30000 });
1187
+ const untrackedRaw = gitQuiet(worktreePath, ['ls-files', '--others', '--exclude-standard'], { timeout: 30000 });
1188
+ // git 检测失败(返回 null,如 worktree .git 半坏/目录部分删除)→ 不能当"无改动",保守保留。
1189
+ // 只有 git 成功返回空才判"真无改动"(可安全清理)。
1190
+ if (trackedRaw === null || untrackedRaw === null) {
1191
+ return { hasChanges: true, changedFiles: [], reason: `git detection failed (conservative keep): tracked=${trackedRaw === null ? 'null' : 'ok'} untracked=${untrackedRaw === null ? 'null' : 'ok'}` };
1192
+ }
1193
+ const tracked = trackedRaw.split('\n').filter(Boolean).filter(isDeliverable);
1194
+ const untracked = untrackedRaw.split('\n').filter(Boolean).filter(isDeliverable);
1120
1195
 
1121
1196
  if (tracked.length === 0 && untracked.length === 0) {
1122
1197
  return { hasChanges: false, changedFiles: [], reason: 'no changes in worktree' };
@@ -1,15 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>SillySpec Dashboard</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
10
- </head>
11
- <body style="background-color: #0A0A0B; margin: 0;">
12
- <div id="app"></div>
13
- <script type="module" src="/src/main.js"></script>
14
- </body>
15
- </html>