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
@@ -82,6 +82,7 @@ brainstorm 完成时按 design.md frontmatter 的 `scale` 分叉:
82
82
  brainstorm 在 `tier=independent` 规模下除 design.md 等四件套外,还需产出一个 stage 级 `review.json`,CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
83
83
 
84
84
  - 路径:`.sillyspec/.runtime/stage-reviews/brainstorm-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-brainstorm-<变更名>`。
85
+ - **run-id / marker 由 CLI 自动生成注入**(review step prompt 渲染时 echo 完整目录路径 + 写 marker;撞 gate 报缺 review.json 时 gate 也 echo 完整路径 + 写 marker)。直接用 CLI 给的路径写 review.json,**勿手算 run-id(必须 `review-` 前缀)、勿手写 marker**。卡住时用 `sillyspec register-stage-review --change <名> --stage brainstorm` 一步生成。
85
86
  - 字段(`schemaVersion:1`,`reviewType=design` —— 区别于 execute 的 `acceptance`):
86
87
 
87
88
  ```json
@@ -88,6 +88,7 @@ execute 完成时,每个 task 必须有 `review.json` 且 verdict 通过,否
88
88
  execute 还有**第二道**独立的 stage 级审查:除逐 task review.json 外,整个 execute 阶段完成还需一个 stage 级 `review.json`(在 "完成确认"/acceptance 步骤产出)。CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
89
89
 
90
90
  - 路径:`.sillyspec/.runtime/stage-reviews/execute-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-execute-<变更名>`。
91
+ - **run-id / marker 由 CLI 自动生成注入**(review step prompt 渲染时 echo 完整目录路径 + 写 marker;撞 gate 报缺 review.json 时 gate 也 echo 完整路径 + 写 marker)。直接用 CLI 给的路径写 review.json,**勿手算 run-id(必须 `review-` 前缀)、勿手写 marker**。卡住时用 `sillyspec register-stage-review --change <名> --stage execute` 一步生成。
91
92
  - 字段(`schemaVersion:1`,`reviewType=acceptance` —— 区别于 brainstorm/plan 的 `"design"`):
92
93
 
93
94
  ```json
@@ -51,6 +51,7 @@ plan 的步骤是动态的:`generate_plan`(生成分级计划)→ `review_
51
51
  `review_plan` 步骤在 `tier=independent` 时产出一个 stage 级 `review.json`,CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
52
52
 
53
53
  - 路径:`.sillyspec/.runtime/stage-reviews/plan-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-plan-<变更名>`。
54
+ - **run-id / marker 由 CLI 自动生成注入**(review step prompt 渲染时 echo 完整目录路径 + 写 marker;撞 gate 报缺 review.json 时 gate 也 echo 完整路径 + 写 marker)。直接用 CLI 给的路径写 review.json,**勿手算 run-id(必须 `review-` 前缀)、勿手写 marker**。卡住时用 `sillyspec register-stage-review --change <名> --stage plan` 一步生成。
54
55
  - 字段(`schemaVersion:1`,`reviewType=plan` —— plan 阶段主审查文档是 `plan.md`;execute 阶段才是 `acceptance`):
55
56
 
56
57
  ```json
@@ -61,10 +61,11 @@ sillyspec run quick --done --change quick-<hash> --output "…" # 完成该会
61
61
  |---|---|
62
62
  | `--linked-changes none\|a,b` | **显式关联变更(取代 `--change`,推荐)**。none=不关联,a,b=关联列表 |
63
63
  | `--files a.js,b.js` | 显式声明本次允许修改的文件(边界保护) |
64
- | `--file-notes "p::注 \|\| p::注"` | quick `--done` 用:QUICKLOG「文件:」行落盘为多行带括注 bullet(省事后手改文件行)。格式 `path::括注`,`\|\|` 分隔多条 |
64
+ | `--file-notes "p::注 \|\| p::注"` | quick `--done` 用:QUICKLOG「文件:」行落盘为多行带括注 bullet(省事后手改文件行)。格式 `path::括注`,`\|\|` 分隔多条;**只随 step3 --done 同命令传**(CLI 短进程,step1/step2 传无效,不带到 step3) |
65
65
  | `--allow-new` | 允许新增文件(默认禁止,防意外创建) |
66
+ | `--allow-delete` | 允许删除文件(默认 fail-closed,删除是破坏性操作;确认删除带此 flag 显式解锁) |
66
67
  | `--force-baseline` | 允许覆盖 baseline 受保护文件 / 压制 `.sillyspec/` 危险判定(危险,慎用) |
67
- | `--confirm` | ⚠️ 仅打印变更概览,**不解锁 blocked**(blocked 仍 exit 1)。真正解锁用 `--force-baseline`/`--allow-new` |
68
+ | `--confirm` | ⚠️ 仅打印变更概览,**不解锁 blocked**(blocked 仍 exit 1)。真正解锁用 `--force-baseline`/`--allow-new`/`--allow-delete` |
68
69
 
69
70
  ## 审计与并发变更(`--done` 边界审计)
70
71
 
@@ -73,7 +74,8 @@ sillyspec run quick --done --change quick-<hash> --output "…" # 完成该会
73
74
  - **并发其他会话的 `.sillyspec/changes/<非关联变更>/` 不再被本 quick 拦截**。quick 自己没有 `changes/` 目录,该路径下非关联内容视为并发会话的工作,整体放行(确定性审计无法区分「并发工作」与「本 quick 偷建变更」,后者这类意图软判定留给 sillyhub)。
74
75
  - **关联变更的文件仍走审计**:reverse-sync 改自己关联变更的 `design.md` 会被拦,需 `--force-baseline` 显式确认。
75
76
  - **baseline 折叠目录前缀匹配**:step1 启动时整片 `changes/` 未跟踪会被 git 折叠成 `?? .sillyspec/changes/`(带尾斜杠 token);审计时若该目录下文件被并发会话跟踪而展开成文件级路径,按尾斜杠 token 前缀放行其下所有文件,不误判。
76
- - **`--confirm` ≠ 解锁**:它只打印概览。blocked 时真正解锁的是 `--force-baseline`(压制 `.sillyspec/` 危险判定 + baseline 覆盖)/`--allow-new`(放行新增),两者在 step1 或 `--done` 传都生效。
77
+ - **同文件并发 warn(advisory,不阻断)**:step1 启动时若你的某个 `allowedFile` 已在他者脏文件列表里(他者也改了这文件),--done 时 CLI 会比对当前内容 hash 与启动时录入的 hash——不一致(你也改了它)即判「同文件并发」并 warn:整文件 pathspec 提交会夹带他者 hunk。warn 给出分离指引(`git add -p <file>` 交互选你自己的 hunk,或 `git diff <file> > mine.patch` 编辑后 `git apply --cached mine.patch` 再 commit)。这**不阻断** --done(你可能有意整文件提交),看 warn 后自行决定是否分离。
78
+ - **`--confirm` ≠ 解锁**:它只打印概览。blocked 时真正解锁的是 `--force-baseline`(压制 `.sillyspec/` 危险判定 + baseline 覆盖)/`--allow-new`(放行新增)/`--allow-delete`(放行删除,删除是破坏性操作默认 fail-closed,须显式 opt-in),三者可在 step1 或 `--done` 传,都生效。
77
79
 
78
80
  ## 典型用法
79
81
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.26.4",
3
+ "version": "3.26.5",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/git-helper.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * 三者统一带 `-c safe.directory=<cwd>` per-command(不污染全局 config)+ `-C cwd` 前缀;
11
11
  * 默认 timeout 5000ms(opts.timeout 可覆盖),默认 trim(opts.trim:false 保留原样输出)。
12
12
  * 数组形式天然规避 shell 拆词,路径含空格 / 元字符安全。
13
+ * safeGit 额外支持 opts.retryOnTimeout:ETIMEDOUT(机器忙瞬时抖动)时用 2× timeout 重试一次。
13
14
  */
14
15
  import { execFileSync } from 'node:child_process'
15
16
 
@@ -17,22 +18,34 @@ import { execFileSync } from 'node:child_process'
17
18
  * 安全执行 git:失败不抛,返回 { value, error } 结构。
18
19
  * @param {string} cwd
19
20
  * @param {string[]} args
20
- * @param {{ trim?: boolean, timeout?: number }} [opts]
21
+ * @param {{ trim?: boolean, timeout?: number, retryOnTimeout?: boolean }} [opts]
21
22
  * - trim:false 保留原样输出(git status --porcelain 首行前导空格
22
23
  * 是状态码的一部分,trim 会削掉致 parsePorcelainPath 丢首字符,坑见 auditQuickCompletion 注释)
23
24
  * - timeout 默认 5000ms,长操作(大 diff / worktree list)可传更大值
25
+ * - retryOnTimeout:true 时,ETIMEDOUT 用 2× timeout 重试一次(默认 false 向后兼容)。
26
+ * 机器忙时 git 子进程启动慢易瞬时超时;审计锚点 git 调用失败即 blocked 中断 quick,
27
+ * 重试一次能消化绝大多数瞬时抖动,免去用户手工重跑。权限 / 损坏等非超时错误不重试(救不回)。
24
28
  * @returns {{ value: string|null, error: string|null }}
25
29
  */
26
30
  export function safeGit(cwd, args, opts = {}) {
27
- const { trim = true, timeout = 5000 } = opts
31
+ const { trim = true, timeout = 5000, retryOnTimeout = false } = opts
28
32
  const fullArgs = ['-c', `safe.directory=${cwd}`, '-C', cwd, ...args]
29
- try {
30
- let value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout })
31
- if (trim) value = value.trim()
32
- return { value, error: null }
33
- } catch (e) {
34
- return { value: null, error: e.message.split('\n')[0] }
33
+ // 单次尝试:返回 { value, errorObj }(errorObj 为原始 Error 或 null),不在内部格式化 message。
34
+ // 格式化(取首行)推迟到最终返回,避免重试分支重复格式化,并保留原始 code 供重试判定。
35
+ const attempt = (t) => {
36
+ try {
37
+ let value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout: t })
38
+ if (trim) value = value.trim()
39
+ return { value, errorObj: null }
40
+ } catch (e) {
41
+ return { value: null, errorObj: e }
42
+ }
43
+ }
44
+ let { value, errorObj } = attempt(timeout)
45
+ if (errorObj && retryOnTimeout && errorObj.code === 'ETIMEDOUT') {
46
+ ({ value, errorObj } = attempt(timeout * 2))
35
47
  }
48
+ return { value, error: errorObj ? errorObj.message.split('\n')[0] : null }
36
49
  }
37
50
 
38
51
  /**
package/src/index.js CHANGED
@@ -1015,6 +1015,8 @@ SillySpec worktree — git worktree 隔离管理
1015
1015
  } else if (result.result === 'skipped') {
1016
1016
  console.log(`⏭️ worktree 跳过清理: ${wtName} (mode: ${result.mode})`);
1017
1017
  console.log(` 原因: in-place 模式没有隔离目录需要清理`);
1018
+ } else if (result.result === 'blocked') {
1019
+ console.error(`🚫 拒绝清理:有未落主仓交付变更,请先 sillyspec worktree apply ${wtName} 或 --force`);
1018
1020
  } else {
1019
1021
  console.log(`ℹ️ worktree 未找到: ${wtName}`);
1020
1022
  }
package/src/quicklog.js CHANGED
@@ -243,6 +243,8 @@ async function rotateIfNeeded(userFile, gitUser) {
243
243
  } else {
244
244
  renameSync(userFile, archiveFile)
245
245
  }
246
+ // echo 轮转:归档文件是 git 跟踪的新文件,提交时带上(勿漏,否则旧 ql 条目只在本地)
247
+ console.log(`🔄 QUICKLOG 已轮转(>500 行):${basename(userFile)} → ${basename(archiveFile)}(提交时带上归档文件,勿漏)`)
246
248
  }
247
249
 
248
250
  function tasksPath(specBase, change) {
@@ -28,6 +28,7 @@ import { resolveQuickLinkedChanges } from './quick-audit.js'
28
28
  import { outputStep } from './prompt.js'
29
29
  import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
30
30
  import { runStage } from './stage.js'
31
+ import { sanitizeDesc } from '../quicklog.js'
31
32
  import { ProgressManager } from '../progress.js'
32
33
  import { validateChangeExists } from '../stage-contract.js'
33
34
  import { stageRegistry, auxiliaryStages } from '../stages/index.js'
@@ -44,6 +45,32 @@ const VALUE_FLAGS = new Set([
44
45
  '--files', '--file-notes', '--from-step', '--mode', '--dir', '--confirm-mode',
45
46
  ])
46
47
 
48
+ /**
49
+ * 检测 `--files` 是否被空格分隔误用(坑 quick-files-space-separated-silently-drops)。
50
+ *
51
+ * --files 是单值 flag(VALUE_FLAGS:校验循环只跳一个 token、--k=v 只展开一个值),空格分隔的
52
+ * 多文件只有首个被当值,其余沦为位置参数被 `:482` 的 `startsWith('--')` 校验静默忽略 →
53
+ * guard.allowedFiles 只剩首个 → --done 边界审计误拦,边界保护形同虚设。
54
+ *
55
+ * 返回 --files 值及其后所有连续非 flag token(疑似被丢的多文件);length > 1 即误用,调用方
56
+ * fail-loud(process.exit 2,同 run --json :109 显式拒绝静默吞风格)。贪婪收集多值会破坏
57
+ * VALUE_FLAGS 单值框架(校验循环/等号展开/getFlagValue 都假设单值),故仅检测不改语义。
58
+ *
59
+ * 纯函数:无副作用、不读 process、不 mutate 入参,供 command.js 解析 + 单测复用。
60
+ * @param {string[]} flags runCommand 规范化后的 flags 数组(--k=v 已展开)
61
+ * @returns {string[]} --files 值 + 其后连续非 flag token;无 --files / 值是 flag 名 / 含逗号 → []
62
+ */
63
+ export function detectSpaceSeparatedFiles(flags) {
64
+ const idx = flags.indexOf('--files')
65
+ if (idx === -1) return []
66
+ const val = flags[idx + 1]
67
+ // 值缺失 / 值本身是 flag 名(--files --done 漏值)/ 逗号分隔(正确用法)→ 不检测
68
+ if (!val || val.startsWith('--') || val.includes(',')) return []
69
+ const suspects = [val]
70
+ for (let j = idx + 2; j < flags.length && !flags[j].startsWith('--'); j++) suspects.push(flags[j])
71
+ return suspects
72
+ }
73
+
47
74
  /**
48
75
  * 从 progress 或变更目录推导变更名
49
76
  */
@@ -444,6 +471,16 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
444
471
  }
445
472
 
446
473
  // 解析 --files a.js,b.js(quick 专用:显式声明 allowedFiles)
474
+ // --files 是单值 flag,空格分隔的多文件只取首个致其余静默丢失(坑 quick-files-space-separated-silently-drops)。
475
+ // detectSpaceSeparatedFiles 检测误用 → fail-loud 报错指明逗号用法(同 run --json :109 风格)。
476
+ const spaceSuspects = detectSpaceSeparatedFiles(flags)
477
+ if (spaceSuspects.length > 1) {
478
+ console.error('❌ --files 检测到空格分隔的多文件:CLI 只识别首个,其余会被静默丢弃(边界保护失效)。')
479
+ console.error(` 疑似多文件:${spaceSuspects.join(' ')}`)
480
+ console.error(' --files 是单值参数,多文件请用逗号分隔(无空格):')
481
+ console.error(` --files ${spaceSuspects.join(',')}`)
482
+ process.exit(2) // 用法错 → exit 2
483
+ }
447
484
  let quickFiles = []
448
485
  const filesIdx = flags.indexOf('--files')
449
486
  if (filesIdx !== -1 && flags[filesIdx + 1]) {
@@ -461,6 +498,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
461
498
  setQuickFileNotes(quickFileNotes)
462
499
 
463
500
  const isAllowNew = flags.includes('--allow-new')
501
+ const isAllowDelete = flags.includes('--allow-delete')
464
502
  const isForceBaseline = flags.includes('--force-baseline')
465
503
  const isForceRescan = flags.includes('--force-rescan')
466
504
 
@@ -471,7 +509,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
471
509
  '--reason', '--options', '--answer', '--confirm-mode',
472
510
  '--output', '--input', '--change', '--linked-changes',
473
511
  '--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
474
- '--files', '--file-notes', '--allow-new', '--force-baseline', '--force-rescan',
512
+ '--files', '--file-notes', '--allow-new', '--allow-delete', '--force-baseline', '--force-rescan',
475
513
  '--json', '--dir', '--help',
476
514
  '--reopen', '--from-step', '--mode',
477
515
  '--deep', '--quick', '--standard', // scan profile 三档显式选择(scan-profile.js 从 argv 读;互斥见下方 PROFILE_FLAGS 检测)
@@ -594,7 +632,10 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
594
632
  // 如果指定了变更名或有变更目录,自动初始化变更的 progress
595
633
  const autoChange = changeName || resolveChangeNameAuto(cwd, specRoot)
596
634
  if (autoChange) {
597
- progress = pm.initChange(cwd, autoChange)
635
+ // 创建时即写 title(与 name/KEY 同时落 db):--input 需求描述 sanitizeDesc 优先,无则用 name 兜底。
636
+ // proposal 还没写、无权威标题来源,这是临时值;brainstorm/plan 完成 proposal/design 落盘后由
637
+ // complete.js 通用完成路径 deriveTitleFromLinkedChange 刷新为真实 # 标题(与 quick 启动 title 同源)。
638
+ progress = pm.initChange(cwd, autoChange, { title: inputText ? sanitizeDesc(inputText) : autoChange })
598
639
  } else if (isAuxiliary) {
599
640
  let autoName = changeName || resolveChangeNameAuto(cwd, specRoot) || 'default'
600
641
  // archive 特例:归档后变更从活跃列表排除(listChanges WHERE status='active'),
@@ -616,7 +657,8 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
616
657
  }
617
658
  changeName = autoName
618
659
  if (!progress) {
619
- progress = pm.initChange(cwd, autoName)
660
+ // auxiliary 创建也写 title(--input 优先 / name 兜底),同上完整流程语义。
661
+ progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
620
662
  // initChange 可能因 project 表为空返回 null
621
663
  if (!progress) {
622
664
  progress = { currentStage: stageName, stages: {}, lastActive: new Date().toLocaleString('zh-CN', { hour12: false }), project: '' }
@@ -636,7 +678,8 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
636
678
  console.log(`🔄 自动创建变更:${autoName}`)
637
679
  console.log(` 提示:可以用 --change <名称> 指定自定义变更名`)
638
680
  console.log(` 或事后重命名:sillyspec change-rename ${autoName} <新名称>`)
639
- progress = pm.initChange(cwd, autoName)
681
+ // brainstorm 自动创建变更也写 title(--input 需求描述优先 / 自动名兜底),proposal 落盘后刷新。
682
+ progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
640
683
  changeName = autoName
641
684
  } else {
642
685
  console.error('❌ 未找到进度数据,请先运行 sillyspec init 或指定 --change <变更名>')
@@ -811,11 +854,11 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
811
854
  // --done
812
855
  if (isDone) {
813
856
  const doneAnswer = getFlagValue('--answer')
814
- return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, doneAnswer, isForceBaseline, isAllowNew })
857
+ return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, doneAnswer, isForceBaseline, isAllowNew, isAllowDelete })
815
858
  }
816
859
 
817
860
  // 默认:输出当前步骤
818
- return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts, { quickFiles, isAllowNew, isForceBaseline, isForceRescan, linkedChanges, taskDescription: inputText })
861
+ return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts, { quickFiles, isAllowNew, isAllowDelete, isForceBaseline, isForceRescan, linkedChanges, taskDescription: inputText })
819
862
  }
820
863
 
821
864
  /**
@@ -915,7 +958,7 @@ async function resetStage(pm, progress, stageName, cwd, changeName, platformOpts
915
958
  const wm = new WorktreeManager({ cwd })
916
959
  const meta = wm.getMeta(changeName)
917
960
  if (meta) {
918
- const cleanResult = wm.cleanup(changeName)
961
+ const cleanResult = wm.cleanup(changeName, { force: true })
919
962
  if (cleanResult.residual?.length > 0) {
920
963
  console.warn(`⚠️ reset 清理 worktree 残留: ${cleanResult.residual.join('; ')}`)
921
964
  console.warn(` 手动处理: sillyspec worktree cleanup ${changeName} --force`)
@@ -625,7 +625,7 @@ export async function handleWorkflowPostCheck({ stageName, steps, currentIdx, cw
625
625
  * isForceBaseline/isAllowNew/platformOpts。辅助函数直接 import(safeGit/auditQuickCompletion ← shared,
626
626
  * printQuickAuditReview ← quick-audit,4 个 quicklog fns ← quicklog,unlinkSync/rmSync ← fs 静态)。
627
627
  */
628
- export async function handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, platformOpts, pm }) {
628
+ export async function handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, platformOpts, pm }) {
629
629
  // quick 收尾:强校验 QUICKLOG 条目 + 翻状态 + 勾 tasks.md(CLI 接管)
630
630
  if (stageName === 'quick') {
631
631
  // §4.6 从 session guard.json 读 guard(不依赖 progress.quickGuard)。
@@ -666,6 +666,7 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
666
666
  ...guard,
667
667
  forceBaseline: guard.forceBaseline || isForceBaseline,
668
668
  allowNew: guard.allowNew || isAllowNew,
669
+ allowDelete: guard.allowDelete || isAllowDelete,
669
670
  }
670
671
  review = await auditQuickCompletion(cwd, mergedGuard, { isConfirm: confirm })
671
672
  printQuickAuditReview(review)
@@ -833,6 +834,96 @@ export async function handleExecuteWaveArtifact({ stageName, steps, currentIdx,
833
834
  return null
834
835
  }
835
836
 
837
+ /**
838
+ * 聚合最新 execute run 各 task review.json 的 changedFiles(主仓 repo 过滤)。
839
+ *
840
+ * 复用 resolveLatestExecuteRunIdWithTasks(task-review.js:684,规避 marker 漂移)+ readReview。
841
+ * 仅主仓 repo(review.repo 缺省或 'main')的 changedFiles 参与主仓核验;跨仓 repo 的 task 文件
842
+ * 由跨仓仓独立落地,不在主仓 worktree/分支,混入会误报 missing(Grill M11)。
843
+ *
844
+ * @param {{ runtimeRoot: string, changeName: string }} opts
845
+ * @returns {Promise<string[]>} 主仓 changedFiles 聚合(去重保序);无 run / 无 tasks / 读取失败 → []
846
+ */
847
+ export async function collectExecuteChangedFiles({ runtimeRoot, changeName }) {
848
+ if (!runtimeRoot || !changeName) return []
849
+ const { resolveLatestExecuteRunIdWithTasks, readReview, normalizeRepoKey } = await import('../task-review.js')
850
+ const runId = resolveLatestExecuteRunIdWithTasks({ runtimeRoot })
851
+ if (!runId) return []
852
+ const tasksDir = join(runtimeRoot, 'execute-runs', runId, 'tasks')
853
+ if (!existsSync(tasksDir)) return []
854
+ let taskIds
855
+ try {
856
+ taskIds = readdirSync(tasksDir)
857
+ } catch {
858
+ return []
859
+ }
860
+ const files = []
861
+ for (const taskId of taskIds) {
862
+ const r = readReview(join(tasksDir, taskId, 'review.json'))
863
+ if (!r.ok || !r.review) continue
864
+ // 跨仓 repo 过滤:仅主仓(repo 缺省视 'main')参与主仓核验,避免误报
865
+ if (normalizeRepoKey(r.review.repo) !== 'main') continue
866
+ if (Array.isArray(r.review.changedFiles)) {
867
+ for (const f of r.review.changedFiles) {
868
+ // 交付物过滤(与 worktree.js hasUnappliedChanges isDeliverable 同口径):.sillyspec/ 蓝图/
869
+ // runtime 产物与 meta.json 不参与落盘核验——它们随主仓/并行 session 维护,不在 worktree 分支,
870
+ // 混入会误报 missing
871
+ if (typeof f !== 'string' || f.trim() === '') continue
872
+ if (f.startsWith('.sillyspec/') || f === 'meta.json') continue
873
+ files.push(f)
874
+ }
875
+ }
876
+ }
877
+ return [...new Set(files)]
878
+ }
879
+
880
+ /**
881
+ * execute 阶段级核验(防空跑谎报,D-002@v1 / FR-04/05/06):聚合最新 execute run 各 task review 声称的
882
+ * 交付文件,用 findMissingDeliverables 核验其存在于 worktree 分支 tree 或 worktree 工作区。
883
+ *
884
+ * - missing 文件 → console.warn 列清单,提示"apply 将无源可复制"(宽松非阻断,不 exit 不 throw)。
885
+ * - checked:false(worktree 目录 / 分支不存在)→ 保守提示"无法核验,请人工确认"。
886
+ * - 与 Task Review Gate 既有校验(零改动伪造 / 不相交伪造,task-review.js:590-623)互补不重复拦截:
887
+ * 本核验的真实增量窗口是「review 通过后文件被删且未 commit」+「无法核验时给人工确认提示」。
888
+ *
889
+ * 由 handleExecuteWorktreeCleanup 开头调用:核验发生在 worktree cleanup 之前,目录被清前先判定
890
+ * 交付文件是否落盘。整个钩子 try/catch 兜底——任何异常只 warn,绝不影响 execute 完成(FR-07)。
891
+ *
892
+ * ctx:stageName/changeName/cwd。WorktreeManager + findMissingDeliverables ← 动态 ../worktree.js;
893
+ * task-review 读取 ← 动态 ../task-review.js。runtimeRoot 用本地 specBase 解析(无 platformOpts 传入
894
+ * 时平台 runtimeRoot 不在本函数作用域,降级为本仓 .runtime,读不到 review 则跳过,零误报)。
895
+ */
896
+ export async function handleExecuteDeliverableCheck({ stageName, changeName, cwd }) {
897
+ if (stageName !== 'execute' || !changeName) return null
898
+ try {
899
+ const { WorktreeManager, findMissingDeliverables } = await import('../worktree.js')
900
+ const wm = new WorktreeManager({ cwd })
901
+ const meta = wm.getMeta(changeName)
902
+ if (!meta) return null // 无 worktree meta(非 worktree execute / 已清),无需核验
903
+ const specBase = join(cwd, '.sillyspec')
904
+ const runtimeRoot = resolveRuntimeRoot({}, specBase)
905
+ const changedFiles = await collectExecuteChangedFiles({ runtimeRoot, changeName })
906
+ if (changedFiles.length === 0) return null
907
+ const { missing, checked } = findMissingDeliverables({
908
+ worktreePath: meta.worktreePath,
909
+ branch: meta.branch,
910
+ changedFiles,
911
+ })
912
+ if (!checked) {
913
+ console.warn('⚠️ execute 阶段级核验:无法核验(worktree 目录或分支不存在),请人工确认交付文件已落盘。')
914
+ return null
915
+ }
916
+ if (missing.length > 0) {
917
+ console.warn(`⚠️ execute 阶段级核验:以下 ${missing.length} 个声称实现的交付文件既不在分支也不在工作区,疑似空跑/从未落盘:`)
918
+ for (const f of missing) console.warn(` ${f}`)
919
+ console.warn(' 请检查子代理是否真实实现,或先 commit 到分支;apply 将无源可复制。')
920
+ }
921
+ } catch (e) {
922
+ console.warn(`⚠️ execute 阶段级核验跳过: ${e?.message || e}`)
923
+ }
924
+ return null
925
+ }
926
+
836
927
  /**
837
928
  * execute 阶段完成时条件性清理 worktree(W6 Step6c 从 completeStep 完成路径内联块抽出)。
838
929
  * 不依赖 AI agent 的完成确认步骤:有未 apply 变更 → 保留 worktree;否则 cleanup(含 in-place 安全清理)。
@@ -841,6 +932,9 @@ export async function handleExecuteWaveArtifact({ stageName, steps, currentIdx,
841
932
  * ctx:stageName/changeName/cwd。WorktreeManager ← 动态 ../worktree.js。
842
933
  */
843
934
  export async function handleExecuteWorktreeCleanup({ stageName, changeName, cwd }) {
935
+ // 阶段级核验(D-002@v1,防空跑谎报):cleanup 之前判定 review 声称实现的交付文件是否落盘。
936
+ // 宽松非阻断:缺失 warn / 无法核验保守提示 / 异常只 warn,均不影响下方 cleanup 与 execute 完成。
937
+ await handleExecuteDeliverableCheck({ stageName, changeName, cwd })
844
938
  if (stageName === 'execute' && changeName) {
845
939
  try {
846
940
  const { WorktreeManager } = await import('../worktree.js');
@@ -5,9 +5,9 @@
5
5
  * completion path 收尾 + single-step path)+ skipStep/waitStep/continueStep(W6 Step7b-2 续搬)。
6
6
  * 本 commit(7b-1)先搬 completeStep + 2 独占 helper(validateMetadata/validateFileLocations)。
7
7
  *
8
- * 安全锚:run.js 始终 barrel。completeStep 由 run.js import 回来;_completeStepForTest 被 9 个
9
- * run-complete-step-* characterization 测试直接 import,run.js barrel re-export(export { completeStep as
10
- * _completeStepForTest } 沿用 imported-binding re-export)契约保留。
8
+ * 安全锚:run.js 始终 barrel。completeStep 由 run/command.js 在 --done 路径调用,无测试专用导出
9
+ * (曾以 `_completeStepForTest` 别名 re-export 供 characterization 测试直驱动,2026-08-13 这些测试
10
+ * 全部迁到 CLI 子进程 `sillyspec run <stage> --done`,导出已移除——见 run.js barrel 注释)。
11
11
  *
12
12
  * 依赖(completeStep 仅用已抽叶子 + 随搬 helper,零 run.js 闭包,零动态 import):
13
13
  * - shared.js: triggerSync/WAIT_MARKER_RE/getStageSteps;prompt.js: outputStep;gates.js: enforceDepsGate/runStageCompletionGates
@@ -24,6 +24,7 @@ import { enforceDepsGate, enforceReviewJsonGate, completeStageGates, readDesignS
24
24
  import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact } from './complete-handlers.js'
25
25
  import { formatExecuteSummary } from '../worktree-apply.js'
26
26
  import { isEndToEndTaskText } from '../change-risk-profile.js'
27
+ import { deriveTitleFromLinkedChange } from '../quicklog.js'
27
28
 
28
29
  // validateMetadata / readDesignScale / validateFileLocations 已迁至 ./gates.js(completeStageGates
29
30
  // 共享收尾管线,消除 noAI 末步 / continueStep 完成分支绕过 gate 的 S1/S2/S3 不对称)。
@@ -60,7 +61,7 @@ export function resolveWaitingStepWithAnswer(steps, doneAnswer, nowStr) {
60
61
  }
61
62
 
62
63
  export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
63
- const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false } = options
64
+ const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false } = options
64
65
  const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
65
66
  const stageData = progress.stages[stageName]
66
67
 
@@ -248,7 +249,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
248
249
  return { stageCompleted: false, currentIdx, nextPendingIdx: -1 }
249
250
  }
250
251
  // quick 收尾(W6 Step6b 抽至 complete-handlers.js handleQuickStageCompletion)
251
- await handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, platformOpts, pm })
252
+ await handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, platformOpts, pm })
252
253
 
253
254
  // ── reopen --done 回填(坑 brainstorm-reopen-step-state-desync)──
254
255
  // nextPendingIdx === -1 且无 waiting,说明要进阶段完成分支。此时若存在 stale 步骤
@@ -299,6 +300,16 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
299
300
  const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText, ctx: _completeCtx })
300
301
  if (_stageGatesResult) return _stageGatesResult
301
302
 
303
+ // 完整流程 change title 刷新:proposal/design 落盘后(brainstorm/plan 完成),从 proposal 首个 #
304
+ // 标题刷新 changes.title(人类可读中文标题,用户可随时刷新)。quick-<hex> 无 proposal 目录 →
305
+ // deriveTitleFromLinkedChange 返回 '' 不刷新(quick 走 handleQuickStageCompletion 的 extractTitleFromResult)。
306
+ if (changeName && !/^quick-[0-9a-f]{8}$/.test(changeName)) {
307
+ try {
308
+ const refinedTitle = deriveTitleFromLinkedChange(specBase, changeName)
309
+ if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle })
310
+ } catch { /* title 刷新失败不阻断完成 */ }
311
+ }
312
+
302
313
  // gate 全过:persist completed(task-01 移后)。此处到 _write 之间若崩,DB 仍 in-progress(内存已 completed 但未落盘),下次进 CLI 读 DB 即 in-progress,不产生"假 completed"。
303
314
  pm._write(cwd, progress, changeName)
304
315
  triggerSync(cwd, changeName, platformOpts)
@@ -764,6 +775,13 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
764
775
  }
765
776
  const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps: stageData.steps, currentIdx, outputText: null, ctx: _contCtx })
766
777
  if (_stageGatesResult) return _stageGatesResult
778
+ // 完整流程 change title 刷新(同 completeStep 完成分支,wait 解除后阶段完成也刷新)。
779
+ if (changeName && !/^quick-[0-9a-f]{8}$/.test(changeName)) {
780
+ try {
781
+ const refinedTitle = deriveTitleFromLinkedChange(specBase, changeName)
782
+ if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle })
783
+ } catch { /* title 刷新失败不阻断完成 */ }
784
+ }
767
785
  // gate 全过:persist completed(task-03 移后;此处无 triggerSync)。
768
786
  pm._write(cwd, progress, changeName)
769
787
  console.log(`\n✅ ${stageName} 阶段已完成(${stageData.steps.length}/${stageData.steps.length} 步)`)
package/src/run/gates.js CHANGED
@@ -309,7 +309,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
309
309
  : 'acceptance'
310
310
  const searchDirs = [effectiveSpecBase, reviewChangeDir, cwd].filter(Boolean)
311
311
  const reviewResult = validateStageReview({ stage: stageName, reviewType, runtimeRoot, reviewRunId, searchDirs })
312
- printStageReviewResult(reviewResult, { stage: stageName })
312
+ printStageReviewResult(reviewResult, { stage: stageName, reviewRunId, runtimeRoot })
313
313
  if (!reviewResult.ok) {
314
314
  return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
315
315
  }