sillyspec 3.27.1 → 3.27.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 (224) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +8 -0
  2. package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
  3. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  4. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
  5. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  6. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  7. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
  8. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  9. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
  10. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
  11. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
  12. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
  13. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  14. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  15. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  16. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
  17. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  18. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
  19. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
  20. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  21. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
  22. package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
  23. package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
  24. package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
  25. package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
  26. package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
  27. package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
  28. package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
  29. package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
  30. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
  31. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  32. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
  33. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
  34. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
  35. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
  36. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
  37. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
  38. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
  39. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
  40. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
  41. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
  42. package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
  43. package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
  44. package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
  45. package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
  46. package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
  47. package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
  48. package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
  49. package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
  50. package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
  51. package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
  52. package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
  53. package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
  54. package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
  55. package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
  56. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
  57. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
  58. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
  59. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
  60. package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
  61. package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
  62. package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
  63. package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
  64. package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
  65. package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
  66. package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
  67. package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
  68. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
  69. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
  70. package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
  71. package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
  72. package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
  73. package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
  74. package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
  75. package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
  76. package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
  77. package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
  78. package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
  79. package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
  80. package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
  81. package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
  82. package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
  83. package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
  84. package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
  85. package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
  86. package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
  87. package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
  88. package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
  89. package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
  90. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
  91. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
  92. package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
  93. package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
  94. package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
  95. package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
  96. package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
  97. package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
  98. package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
  99. package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
  100. package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
  101. package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
  102. package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
  103. package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
  104. package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
  105. package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
  106. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
  107. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
  108. package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
  109. package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
  110. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
  111. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
  112. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
  113. package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
  114. package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
  115. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
  116. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
  117. package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
  118. package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
  119. package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
  120. package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
  121. package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
  122. package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
  123. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
  124. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
  125. package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
  126. package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
  127. package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
  128. package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
  129. package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
  130. package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
  131. package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
  132. package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
  133. package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
  134. package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
  135. package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
  136. package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
  137. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
  138. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
  139. package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
  140. package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
  141. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
  142. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
  143. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
  144. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
  145. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
  146. package/README.md +1 -1
  147. package/eval/README.md +86 -0
  148. package/eval/eval.config.json +28 -0
  149. package/eval/lib/adapters/cli.mjs +63 -0
  150. package/eval/lib/adapters/dryrun.mjs +27 -0
  151. package/eval/lib/config.mjs +82 -0
  152. package/eval/lib/db.mjs +76 -0
  153. package/eval/lib/report.mjs +120 -0
  154. package/eval/lib/runner.mjs +157 -0
  155. package/eval/lib/tasks.mjs +73 -0
  156. package/eval/lib/util.mjs +107 -0
  157. package/eval/lib/workdir.mjs +30 -0
  158. package/eval/run.mjs +83 -0
  159. package/eval/tasks/001-slugify/files/package.json +6 -0
  160. package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  161. package/eval/tasks/001-slugify/task.json +6 -0
  162. package/eval/tasks/001-slugify/verify.mjs +28 -0
  163. package/eval/tasks/002-each-day/files/package.json +6 -0
  164. package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  165. package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  166. package/eval/tasks/002-each-day/task.json +6 -0
  167. package/eval/tasks/002-each-day/verify.mjs +31 -0
  168. package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  169. package/eval/tasks/003-word-report/files/package.json +6 -0
  170. package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  171. package/eval/tasks/003-word-report/task.json +6 -0
  172. package/eval/tasks/003-word-report/verify.mjs +38 -0
  173. package/package.json +4 -2
  174. package/src/agent-session-log.js +803 -0
  175. package/src/change-list.js +22 -2
  176. package/src/change-risk-profile.js +83 -14
  177. package/src/config-schema.js +18 -3
  178. package/src/contract-matrix.js +153 -8
  179. package/src/datetime.js +20 -0
  180. package/src/decision-distill.js +517 -0
  181. package/src/dispatch/backends/local-agent.js +4 -4
  182. package/src/docs-check.js +227 -0
  183. package/src/docs-debt.js +187 -24
  184. package/src/doctor-diagnostics.js +102 -2
  185. package/src/foreign-declared.js +78 -0
  186. package/src/index.js +99 -10
  187. package/src/init.js +9 -7
  188. package/src/knowledge-match.js +107 -6
  189. package/src/local-register.js +7 -1
  190. package/src/module-resolve.js +12 -1
  191. package/src/modules.js +2 -1
  192. package/src/plan-adopt-waves.js +126 -0
  193. package/src/quicklog.js +61 -7
  194. package/src/run/command.js +139 -20
  195. package/src/run/complete-handlers.js +48 -19
  196. package/src/run/complete.js +43 -3
  197. package/src/run/gates.js +87 -22
  198. package/src/run/prompt.js +84 -42
  199. package/src/run/quick-audit.js +28 -0
  200. package/src/run/shared.js +176 -11
  201. package/src/run/stage.js +23 -0
  202. package/src/run.js +2 -2
  203. package/src/scan-postcheck.js +30 -17
  204. package/src/stage-contract-spec.js +2 -1
  205. package/src/stage-contract.js +68 -4
  206. package/src/stages/archive.js +53 -12
  207. package/src/stages/brainstorm-auto.js +7 -0
  208. package/src/stages/brainstorm.js +11 -0
  209. package/src/stages/doctor.js +157 -1
  210. package/src/stages/execute.js +33 -6
  211. package/src/stages/plan-postcheck.js +79 -18
  212. package/src/stages/plan.js +4 -0
  213. package/src/stages/quick.js +6 -1
  214. package/src/stages/verify.js +31 -14
  215. package/src/sync.js +94 -2
  216. package/src/task-review.js +2 -2
  217. package/src/taskcard-placeholders.js +21 -0
  218. package/src/taskcard.js +10 -1
  219. package/src/verify-postcheck.js +315 -13
  220. package/src/verify-probes.js +46 -4
  221. package/src/worktree-apply.js +184 -7
  222. package/src/worktree-deps.js +69 -0
  223. package/src/worktree.js +201 -58
  224. package/templates/prompts/taskcard-rules.md +1 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * plan-adopt-waves.js — `sillyspec plan-adopt-waves` 把 plan.md 的 Wave 段一键重排为
3
+ * depends_on 拓扑分组(坑 wave-manual-mismatch-noise,2026-08-24 用户反馈二期:主控手排
4
+ * Wave 与 CLI 拓扑比对不一致只能手动改;依赖方向违规修复后,本命令给一条零手写出路)。
5
+ *
6
+ * 行为:
7
+ * - depMap(tasks/*.md 的 depends_on,collectTaskDepMap 与 postcheck 同源)→ topoSortWaves
8
+ * - 重写 plan.md 的 `## Wave N` 段块(段内按编号排序;首段「并行,无依赖」、其余「依赖前序 Wave」)
9
+ * - 无显式 Wave 标题 → 插在「## 任务总表」前(无总表则文末追加)
10
+ * - best-effort 同步任务总表的 W 列(仅认 7 列模板行;不匹配的行 warn 不强改)
11
+ * - Wave 段内若含非引用内容(正文/表格)→ 拒绝重写(防误删),提示先手动整理
12
+ * - 写后复跑 validateBlueprintConsistency 打印结果(防 adopt 把手工安全串行收敛出
13
+ * 同 Wave 共享路径冲突——topo 只看依赖不看文件重叠)
14
+ * - --dry-run 只打印将写入的 Wave 段不落盘;幂等(重复跑产出相同结构);行尾统一 LF
15
+ */
16
+ import { existsSync, readFileSync, writeFileSync } from 'fs'
17
+ import { join } from 'path'
18
+ import { topoSortWaves, collectTaskDepMap, validateBlueprintConsistency } from './stages/plan-postcheck.js'
19
+
20
+ const WAVE_HEADING_RE = /^#+\s*Wave\s+\d+/i
21
+ const TASK_REF_RE = /^[-*]\s+(?:\[[ x]\]\s*)?task-\d+\s*[::\s]*$/i
22
+ const LEGACY_TASK_REF_RE = /^[-*]\s*\[[ x]\]\s*task-\d+/i
23
+ const SUMMARY_HEADING_RE = /^#+\s*任务总表/
24
+
25
+ /** 渲染标准 Wave 段块(段落间空行分隔,结尾不带多余空行——由调用方控制拼接) */
26
+ function renderWaveBlock(waves) {
27
+ const L = []
28
+ waves.forEach((wave, i) => {
29
+ const sorted = [...wave].sort((a, b) =>
30
+ parseInt(String(a).replace(/\D/g, ''), 10) - parseInt(String(b).replace(/\D/g, ''), 10))
31
+ L.push(`## Wave ${i + 1}(${i === 0 ? '并行,无依赖' : '依赖前序 Wave'})`)
32
+ for (const t of sorted) L.push(`- ${t}`)
33
+ if (i < waves.length - 1) L.push('')
34
+ })
35
+ return L
36
+ }
37
+
38
+ /** best-effort 更新任务总表 W 列:仅认 `| task-XX | … | W1 | … |` 形态的 7+ 列模板行 */
39
+ function updateSummaryWaveColumn(lines, waveOf) {
40
+ let inSummary = false
41
+ let updated = 0
42
+ let skipped = 0
43
+ for (let i = 0; i < lines.length; i++) {
44
+ const line = lines[i]
45
+ if (/^#+\s/.test(line)) { inSummary = SUMMARY_HEADING_RE.test(line); continue }
46
+ if (!inSummary) continue
47
+ const m = line.match(/^\|\s*task-(\d+)\s*\|/)
48
+ if (!m) continue
49
+ const cells = line.split('|')
50
+ // 模板 7 列:| 编号 | 任务 | Wave | 优先级 | 依赖 | 覆盖 FR/D | 说明 | → split 后首尾空串
51
+ if (cells.length < 8 || !/^\s*W\d+\s*$/.test(cells[3] || '')) { skipped++; continue }
52
+ const taskId = `task-${String(m[1]).padStart(2, '0')}`
53
+ const waveIdx = waveOf.get(taskId)
54
+ if (waveIdx === undefined) { skipped++; continue }
55
+ cells[3] = ` W${waveIdx + 1} `
56
+ lines[i] = cells.join('|')
57
+ updated++
58
+ }
59
+ return { updated, skipped }
60
+ }
61
+
62
+ /**
63
+ * @param {{ changeDir: string, dryRun?: boolean }} opts
64
+ * @returns {{ ok: boolean, error?: string, waves?: string[][], planPath: string, dryRun: boolean,
65
+ * waveBlock?: string[], tableRowsUpdated?: number, tableRowsSkipped?: number,
66
+ * postcheck?: { errors: string[], warnings: string[] } }}
67
+ */
68
+ export function adoptPlanWaves({ changeDir, dryRun = false }) {
69
+ const planPath = join(changeDir, 'plan.md')
70
+ if (!existsSync(planPath)) return { ok: false, error: `plan.md 不存在: ${planPath}`, planPath, dryRun }
71
+ const tasksDir = join(changeDir, 'tasks')
72
+ if (!existsSync(tasksDir)) return { ok: false, error: `tasks/ 目录不存在: ${tasksDir}`, planPath, dryRun }
73
+ const depMap = collectTaskDepMap(tasksDir)
74
+ if (depMap.size === 0) return { ok: false, error: 'tasks/ 下没有 task-NN.md 卡片,无从推导 Wave 分组', planPath, dryRun }
75
+
76
+ const { waves, error: topoError } = topoSortWaves(depMap)
77
+ if (topoError) return { ok: false, error: `拓扑排序失败: ${topoError}`, planPath, dryRun }
78
+
79
+ const original = readFileSync(planPath, 'utf8')
80
+ const lines = original.split(/\r?\n/)
81
+ const waveBlock = renderWaveBlock(waves)
82
+ const waveOf = new Map()
83
+ waves.forEach((ws, i) => { for (const t of ws) waveOf.set(t, i) })
84
+
85
+ const firstWaveIdx = lines.findIndex(l => WAVE_HEADING_RE.test(l))
86
+ if (firstWaveIdx >= 0) {
87
+ // Wave 段块末尾 = 最后一个 Wave 标题之后连续的任务引用行(空行容忍但不断段;
88
+ // 遇非 Wave 标题即止)。段内混入非引用内容 → 拒绝重写(防误删正文/表格)。
89
+ let endIdx = firstWaveIdx
90
+ for (let i = firstWaveIdx; i < lines.length; i++) {
91
+ if (WAVE_HEADING_RE.test(lines[i])) { endIdx = i; continue }
92
+ if (TASK_REF_RE.test(lines[i]) || LEGACY_TASK_REF_RE.test(lines[i])) { endIdx = i; continue }
93
+ if (lines[i].trim() === '') continue
94
+ if (/^#+\s/.test(lines[i])) break
95
+ return { ok: false, error: `Wave 段内含非引用内容(第 ${i + 1} 行「${lines[i].slice(0, 40)}」)——为防误删拒绝重写,请先把 Wave 段整理为纯 "- task-XX" 引用行`, planPath, dryRun }
96
+ }
97
+ lines.splice(firstWaveIdx, endIdx - firstWaveIdx + 1, ...waveBlock)
98
+ } else {
99
+ // 无显式 Wave 标题:插在「## 任务总表」前;无总表则文末追加
100
+ const summaryIdx = lines.findIndex(l => SUMMARY_HEADING_RE.test(l))
101
+ if (summaryIdx >= 0) {
102
+ lines.splice(summaryIdx, 0, ...waveBlock, '')
103
+ } else {
104
+ while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop()
105
+ lines.push('', ...waveBlock)
106
+ }
107
+ }
108
+
109
+ const { updated, skipped } = updateSummaryWaveColumn(lines, waveOf)
110
+ const content = lines.join('\n').replace(/\n*$/, '\n')
111
+
112
+ if (dryRun) {
113
+ return { ok: true, waves, planPath, dryRun: true, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped }
114
+ }
115
+
116
+ writeFileSync(planPath, content, 'utf8')
117
+ // 复跑 blueprint 一致性(advisory 打印):topo 只看依赖不看文件重叠——adopt 可能把手工
118
+ // 安全串行收敛出同 Wave 共享路径冲突,立即暴露而非留到 execute 撞
119
+ let postcheck = null
120
+ try {
121
+ postcheck = validateBlueprintConsistency(changeDir)
122
+ } catch (e) {
123
+ postcheck = { errors: [`复跑校验异常: ${e?.message || e}`], warnings: [] }
124
+ }
125
+ return { ok: true, waves, planPath, dryRun: false, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped, postcheck }
126
+ }
package/src/quicklog.js CHANGED
@@ -32,13 +32,15 @@ const sleep = (ms) => new Promise(r => setTimeout(r, ms))
32
32
  * 与 rename 之间),相比旧实现的必现双进已可忽略。
33
33
  * @param {string} lockPath 锁文件路径(建议与被保护文件同目录)
34
34
  * @param {() => (any|Promise<any>)} fn 临界区
35
- * @param {{staleMs?:number, timeoutMs?:number, retryMs?:number}} opts
35
+ * @param {{staleMs?:number, timeoutMs?:number, retryMs?:number, content?:string}} opts
36
+ * content:锁文件内容(默认 pid-randomId)。供超时报错时展示持有者语义(如 apply 锁写
37
+ * {pid, changeName})——仅展示用途,抢占/偷锁逻辑只比对内容一致性不解析语义。
36
38
  */
37
39
  export async function withFileLock(lockPath, fn, opts = {}) {
38
- const { staleMs = 30000, timeoutMs = 10000, retryMs = 50 } = opts
40
+ const { staleMs = 30000, timeoutMs = 10000, retryMs = 50, content = null } = opts
39
41
  mkdirSync(dirname(lockPath), { recursive: true })
40
42
  const start = Date.now()
41
- const myId = `${process.pid}-${randomBytes(6).toString('hex')}`
43
+ const myId = content || `${process.pid}-${randomBytes(6).toString('hex')}`
42
44
  let fd = null
43
45
  // 抢锁循环
44
46
  while (true) {
@@ -174,6 +176,9 @@ function buildPushPayloadFromRaw(rawBlock, { ql_id, author_raw, status, linked_c
174
176
  payload.timestamp = parts[1] || null
175
177
  payload.title = parts.slice(2).join('|').trim() || payload.title
176
178
  }
179
+ // 顶层标签白名单(^ 行首锚定):D-004@v1 根因块嵌套四子字段(- 现象:/- 根因:/- 护栏:/- 证据: 列表行)
180
+ // 因「- 」前缀不匹配本正则——它们是根因块正文内的列表行续行,不是新顶层标签,经 lastLabel 挂载
181
+ // 进 body_sections[根因](见下方 inFiles 复位注释)。顶层四字段边界解析不受影响(R-03)。
177
182
  const labelRe = /^(状态|关联变更|文件|审计|需求|根因|方案|结果)\s*[::]\s*(.*)$/
178
183
  const bulletRe = /^-\s+(.*)$/
179
184
  let inFiles = false
@@ -190,7 +195,9 @@ function buildPushPayloadFromRaw(rawBlock, { ql_id, author_raw, status, linked_c
190
195
  else if (label === '关联变更') { payload.linked_changes = value.split(/[,,、+;;]/).map(s => s.trim()).filter(Boolean); inLinked = true; inFiles = false }
191
196
  else if (label === '文件') { if (value) payload.files.push(...value.split(/[,,、+;;]/).map(s => s.trim()).filter(Boolean).map(p => ({ path: p, note: null }))); inFiles = true; inLinked = false }
192
197
  else if (label === '审计') { /* D-8 advisory 行:只进 raw_block 不进结构化段(body_sections 不扩 schema),并阻断续行误挂 */ lastLabel = null; inFiles = false; inLinked = false }
193
- else { payload.body_sections[label] = value; lastLabel = label }
198
+ // 进入 需求/根因/方案/结果 字段块须关闭 inFiles/inLinked 续行模式(task-07 / D-004@v1):否则根因块内
199
+ // 嵌套子字段列表行(- 现象:… 等)会命中下方「文件 bullet」分支被劫进 payload.files,从根因正文截断丢失。
200
+ else { payload.body_sections[label] = value; lastLabel = label; inFiles = false; inLinked = false }
194
201
  continue
195
202
  }
196
203
  if (bulletRe.test(stripped) && inFiles) {
@@ -265,10 +272,18 @@ export function deriveTitleFromLinkedChange(specBase, change) {
265
272
  try { content = readFileSync(join(specBase, 'changes', change, f), 'utf8') } catch { continue }
266
273
  const m = content.match(/^#\s+(.+?)\s*$/m)
267
274
  if (!m) continue
268
- const raw = m[1].trim()
269
- // 标题惯例「# 提案书(Proposal)— <desc>」「# 设计文档(Design)— <desc>」→ 取破折号后的 desc
275
+ let raw = m[1].trim()
276
+ // 标题惯例「# 提案书(Proposal)— <desc>」「# 设计文档(Design)— <desc>」→ 取破折号后的 desc。
277
+ // 固定前缀显式剥(坑 linked-task-placeholder-title,2026-08-22 实证:标题「# 提案书(Proposal):
278
+ // xxx」或「# 提案书(Proposal)」无破折号时 dash 匹配失败,整串前缀落进 tasks.md 追加行)——
279
+ // 先剥固定模板词(含全/半角冒号分隔形态),再剥破折号;剥完为空(纯模板标题)继续找下一文档
280
+ for (const prefix of [/^提案书(Proposal)\s*[::—\-]?\s*/, /^设计文档(Design)\s*[::—\-]?\s*/, /^Proposal\s*[::—\-]?\s*/i, /^Design\s*[::—\-]?\s*/i]) {
281
+ raw = raw.replace(prefix, '').trim()
282
+ }
283
+ if (!raw) continue
270
284
  const dash = raw.match(/[—-]\s*(.+)$/)
271
- return dash ? dash[1].trim() : raw
285
+ const title = dash ? dash[1].trim() : raw
286
+ if (title) return title
272
287
  }
273
288
  return ''
274
289
  }
@@ -324,6 +339,37 @@ export function parseFileNotes(raw) {
324
339
  .filter(Boolean)
325
340
  }
326
341
 
342
+ /**
343
+ * 校验 --file-notes 原文格式(坑 quick-file-notes-format-silent,2026-08-24 用户反馈二期②:
344
+ * 多文件分隔符是 `||` 双竖线,写错成 `|`/`,` 时整段被当成第一个文件的括注静默落盘,只能手工修
345
+ * QUICKLOG)。命令入口 fail-fast 用:每个 `||` 段必须是 `path::括注`(`::` 必填,括注可空);
346
+ * 括注内再出现 `::` 视为疑似漏写 `||` 分隔符。返回问题清单供调用方拼报错。
347
+ * @param {string} raw
348
+ * @returns {{ ok: boolean, problems: Array<{ seg: string, reason: string, hint: string }> }}
349
+ */
350
+ export function validateFileNotesFormat(raw) {
351
+ const problems = []
352
+ if (!raw) return { ok: true, problems }
353
+ for (const seg of String(raw).split('||')) {
354
+ const s = seg.trim()
355
+ if (!s) continue
356
+ const idx = s.indexOf('::')
357
+ if (idx === -1) {
358
+ problems.push({ seg: s, reason: '缺 `::` 路径/括注分隔符', hint: '格式为 path::括注;不要括注也要写 path::(括注可空)' })
359
+ continue
360
+ }
361
+ const path = s.slice(0, idx).trim()
362
+ if (!path) {
363
+ problems.push({ seg: s, reason: '`::` 前路径为空', hint: '格式为 path::括注' })
364
+ continue
365
+ }
366
+ if (s.slice(idx + 2).includes('::')) {
367
+ problems.push({ seg: s, reason: '括注内出现 `::`', hint: '疑似多文件漏写 `||` 分隔符(双竖线)——多文件写法:path1::括注1 || path2::括注2' })
368
+ }
369
+ }
370
+ return { ok: problems.length === 0, problems }
371
+ }
372
+
327
373
  // 结果块必填字段(quick step3 --done --output 的结构化结果模板,见 src/stages/quick.js)。
328
374
  // 4 个必填字段标签从 manifest 同源(stage-contract-spec.js quick.result-labels),prompt 事前契约与本校验单源。
329
375
  const RESULT_REQUIRED_LABELS = getRule('quick.result-labels').data.literals
@@ -336,6 +382,8 @@ const RESULT_REQUIRED_LABELS = getRule('quick.result-labels').data.literals
336
382
  * @returns {{ ok: boolean, missing: string[] }} ok=true 通过;missing=缺失的字段名列表
337
383
  */
338
384
  export function validateQuickResult(text) {
385
+ // 子串包含判定:根因块含嵌套四子字段列表行(- 现象:… 等,task-07 / D-004@v1 合法形态)时,
386
+ // 顶层四标签仍须齐备(模板契约),嵌套行不影响判定(「- 」前缀不构成顶层标签,亦不缺字段)。
339
387
  const missing = RESULT_REQUIRED_LABELS.filter(label => !String(text || '').includes(label))
340
388
  return { ok: missing.length === 0, missing }
341
389
  }
@@ -478,6 +526,9 @@ async function checkTaskCheckbox(specBase, change, qlId) {
478
526
  // 前缀」或正则 split(/(?=需求:|根因:|方案:|结果:)/))因前导是「/|( 等非边界字符而跳过。严格失败
479
527
  // (如真实标签前导是「,」这类弱标点)退回宽松顺序扫描(上一标签之后首次出现)。缺标签返回 null → 落单行
480
528
  // 兜底(--done 契约校验仍会拦缺字段)。残余边界:正文引用标签且前导恰好是空白/句末标点时仍可能错位。
529
+ // task-07 / D-004@v1 声明:本切分只作用于「单行四字段压缩归一」路径,嵌套四子字段(- 现象:/- 根因:/
530
+ // - 护栏:/- 证据: 列表行)必须以多行列表行形态写入根因块正文——按序扫描先命中顶层「根因:」标签,
531
+ // 其后的嵌套「- 根因:」等字样落在根因字段正文内不构成新边界,无需改动三个边界函数即天然兼容(Grill C-15)。
481
532
  function isFieldBoundary(body, idx) {
482
533
  if (idx <= 0) return true
483
534
  const prev = body[idx - 1]
@@ -559,6 +610,9 @@ function flipEntryInContent(content, qlId, result, changedFiles = [], fileNotes
559
610
  if (segs) body = segs.join('\n')
560
611
  }
561
612
  // 多行结果(结构化 需求/根因/方案/结果 字段块):逐行插入为独立字段行。
613
+ // 根因块嵌套四子字段(- 现象:/- 根因:/- 护栏:/- 证据: 列表行,D-004@v1 postmortem 形态)属
614
+ // 根因块正文:在此随多行结果逐行原样落盘(带「- 」前缀、保序含换行),不被字段边界扫描误切——
615
+ // 单行归一切分只认「- 」前缀之外的需求:/根因:/方案:/结果: 标签,嵌套行天然不参与边界判定。
562
616
  // 单行结果:保持「结果:<一句话>」一行,向后兼容简单用例。
563
617
  const resultLines = /\r?\n/.test(body)
564
618
  ? body.split(/\r?\n/).filter(l => l.trim() !== '')
@@ -23,7 +23,7 @@ import { basename, join, resolve, dirname } from 'node:path'
23
23
  import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
24
24
  import { randomBytes, randomUUID } from 'node:crypto'
25
25
  import { writeAtomicSync } from '../fs-atomic.js'
26
- import { resolveSpecDir, countAncestorSpecDirs, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, writePlatformPointer, checkPlatformManaged, PLATFORM_MANAGED_FILENAME } from './shared.js'
26
+ import { resolveSpecDir, countAncestorSpecDirs, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, PLATFORM_MANAGED_FILENAME } from './shared.js'
27
27
  import { resolveQuickLinkedChanges } from './quick-audit.js'
28
28
  import { outputStep } from './prompt.js'
29
29
  import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
@@ -163,6 +163,9 @@ sillyspec run ${stageName}${stage && stage.title ? ` — ${stage.title}` : ''}${
163
163
  * sillyspec run <stage> 主命令
164
164
  */
165
165
  export async function runCommand(args, cwd, specDir = null, opts = {}) {
166
+ // 原始 cwd 快照(下方 cwd 纠正前):agent 在子目录启动时,其会话 transcript 挂在子目录
167
+ // 对应的 harness 项目目录下,agent-log 探测须用原始 cwd(纠正后的是项目根,探测会扑空)
168
+ const invokedCwd = cwd
166
169
  // W1-B: run <stage> 暂不支持 --json。之前 --json 进 knownFlags 白名单却从不读取(静默吞),
167
170
  // agent 按 gate/derive 契约期望 envelope 实则拿到混合人类文本。显式 fail-fast 杜绝歧义;
168
171
  // 完整 run envelope 待 outputStep 重构(W6)后支持(prompt + nextAction 结构化)。
@@ -316,19 +319,27 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
316
319
  try {
317
320
  const { readFileSync } = await import('fs')
318
321
  const saved = JSON.parse(readFileSync(platformOptsFile, 'utf8'))
319
- if (saved.specRoot) platformOpts.specRoot = saved.specRoot
320
- if (saved.runtimeRoot) platformOpts.runtimeRoot = saved.runtimeRoot
321
- if (saved.workspaceId) platformOpts.workspaceId = saved.workspaceId
322
- if (saved.scanRunId) platformOpts.scanRunId = saved.scanRunId
323
- // 平台模式 fail-fast:文件存在但缺少 specRoot
324
- if (!platformOpts.specRoot && !platformOpts.runtimeRoot) {
325
- console.error(`❌ 平台模式参数文件存在但缺少 specRoot/runtimeRoot: ${platformOptsFile}`)
326
- console.error(' 可能原因:platform-scan.json 损坏或写入不完整')
327
- console.error(' 解决:重新运行首次 scan 并传入 --spec-root')
328
- process.exit(2) // 环境错(平台文件损坏)→ exit 2
322
+ // 自指指针免疫(FR-4,变更 2026-08-23-repo-native-spec-backfill):saved.specRoot 经
323
+ // realpath 解析回本地 .sillyspec(repo-native junction 回环,旧模板投毒残留)→ 整体
324
+ // 忽略恢复(specRoot/runtimeRoot/workspaceId/scanRunId 全不回填,platformOpts 保持
325
+ // 无平台参数状态),按本地模式运行——内置 sync 走本地链路,specRoot 保持本地取值链。
326
+ if (isSelfReferentialSpecRoot(cwd, saved.specRoot)) {
327
+ console.warn(`⚠️ 检测到自指平台指针(repo-native junction 回环,specRoot 指回本地 .sillyspec),已忽略并按本地模式运行: ${platformOptsFile}`)
328
+ } else {
329
+ if (saved.specRoot) platformOpts.specRoot = saved.specRoot
330
+ if (saved.runtimeRoot) platformOpts.runtimeRoot = saved.runtimeRoot
331
+ if (saved.workspaceId) platformOpts.workspaceId = saved.workspaceId
332
+ if (saved.scanRunId) platformOpts.scanRunId = saved.scanRunId
333
+ // 平台模式 fail-fast:文件存在但缺少 specRoot
334
+ if (!platformOpts.specRoot && !platformOpts.runtimeRoot) {
335
+ console.error(`❌ 平台模式参数文件存在但缺少 specRoot/runtimeRoot: ${platformOptsFile}`)
336
+ console.error(' 可能原因:platform-scan.json 损坏或写入不完整')
337
+ console.error(' 解决:重新运行首次 scan 并传入 --spec-root')
338
+ process.exit(2) // 环境错(平台文件损坏)→ exit 2
339
+ }
340
+ // 恢复成功:更新 specRoot(初始值可能是 cwd/.sillyspec,恢复后应为真实 specDir)
341
+ specRoot = platformOpts.specRoot || specRoot
329
342
  }
330
- // 恢复成功:更新 specRoot(初始值可能是 cwd/.sillyspec,恢复后应为真实 specDir)
331
- specRoot = platformOpts.specRoot || specRoot
332
343
  } catch (e) {
333
344
  console.error(`❌ 平台模式参数文件读取失败: ${platformOptsFile}`)
334
345
  console.error(` 错误: ${e.message}`)
@@ -346,12 +357,20 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
346
357
  if (!platformOpts.specRoot && !platformOpts.runtimeRoot && !platformFileExists) {
347
358
  const decl = checkPlatformManaged(cwd)
348
359
  if (decl) {
349
- console.error(`❌ 平台接管声明生效:本项目已由平台托管(原 specRoot: ${decl.specRoot || '(未记录)'}),但恢复指针缺失,拒绝静默回退本地模式。`)
350
- console.error(` 声明文件: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
351
- console.error(' 恢复:① 重跑平台 scan/init(带 --spec-root)重建指针;② 确认不再使用平台:sillyspec platform disconnect(删除接管声明);③ 显式 --spec-dir <路径> 临时指定目录。')
352
- // exit(1) 而非邻近环境错的 exit(2):这是"状态保护阻断"(对齐 PointerUnreachableError
353
- // 顶层 catch 语义),非"用法/环境错"——见 design.md §5.3 v2 复审观察 (a)
354
- process.exit(1)
360
+ // 陈旧自指声明降级(FR-4,变更 2026-08-23-repo-native-spec-backfill):decl.specRoot 经
361
+ // realpath 解析回本地 .sillyspec(repo-native junction 投毒残留、指针已被清理)→ 视为
362
+ // 陈旧声明,warn 后按本地模式继续(不 exit 1)——声明描述的"平台接管"物理上指向本地
363
+ // .sillyspec,无跨库状态分裂风险;非自指声明维持 fail-closed 原样(状态保护阻断)。
364
+ if (isSelfReferentialSpecRoot(cwd, decl.specRoot)) {
365
+ console.warn(`⚠️ 检测到陈旧的自指平台接管声明(repo-native junction 回环,原 specRoot 指回本地 .sillyspec),已降级并按本地模式运行;可 sillyspec platform disconnect 清理残留声明: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
366
+ } else {
367
+ console.error(`❌ 平台接管声明生效:本项目已由平台托管(原 specRoot: ${decl.specRoot || '(未记录)'}),但恢复指针缺失,拒绝静默回退本地模式。`)
368
+ console.error(` 声明文件: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
369
+ console.error(' 恢复:① 重跑平台 scan/init(带 --spec-root)重建指针;② 确认不再使用平台:sillyspec platform disconnect(删除接管声明);③ 显式 --spec-dir <路径> 临时指定目录。')
370
+ // exit(1) 而非邻近环境错的 exit(2):这是"状态保护阻断"(对齐 PointerUnreachableError
371
+ // 顶层 catch 语义),非"用法/环境错"——见 design.md §5.3 v2 复审观察 (a)
372
+ process.exit(1)
373
+ }
355
374
  }
356
375
  }
357
376
  // 持久化 platformOpts
@@ -398,7 +417,11 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
398
417
  // ⚠️ 同 init.js:必须保护真实资产(changes/、projects/、sillyspec.db)。
399
418
  // 只在「首次」执行一次——用 cwd 下的 .sillyspec-platform-cleaned 标记文件记录已处理,
400
419
  // 后续每次 run 直接跳过,避免重复检查 + 红叉噪声(此清理不阻塞流程、不动真实资产)。
401
- if (platformOpts.specRoot) {
420
+ // 自指守卫(FR-4,变更 2026-08-23-repo-native-spec-backfill):显式自指 --spec-root
421
+ // (repo-native junction 回环)下 cwd/.sillyspec 与 specRoot 同物理目录——它就是平台
422
+ // 规范目录本体,绝非"旧版本残留";跳过整个清理决策(含 marker 写入,防后续真平台
423
+ // 接入被误标记已清理)。否则 rmSync 会删掉 repo-native 唯一真理源并留下悬空链接。
424
+ if (platformOpts.specRoot && !isSelfReferentialSpecRoot(cwd, platformOpts.specRoot)) {
402
425
  const legacyDir = join(cwd, '.sillyspec')
403
426
  const cleanedMarker = join(cwd, '.sillyspec-platform-cleaned')
404
427
  if (!existsSync(cleanedMarker)) {
@@ -466,6 +489,20 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
466
489
  const inputValue = getFlagValue('--input')
467
490
  if (inputValue !== null) inputText = inputValue
468
491
 
492
+ // quick 位置参数 → 任务描述(与 auto 模式建议的 `sillyspec run quick "需求"` 用法一致)。
493
+ // 旧行为静默忽略裸 token(QUICKLOG 标题回退占位);现显式消费——跳过值类 flag 的值
494
+ // (--files src/a.js 的 src/a.js 不是位置参数),取第一个真位置 token。配合下方
495
+ // 「新会话必须带描述」启动门(坑 quick-no-input-placeholder-title)。
496
+ if (stageName === 'quick' && inputText === null) {
497
+ for (let i = 0; i < flags.length; i++) {
498
+ const t = flags[i]
499
+ if (typeof t !== 'string') continue
500
+ if (t.startsWith('--')) { if (VALUE_FLAGS.has(t)) i++; continue }
501
+ inputText = t
502
+ break
503
+ }
504
+ }
505
+
469
506
  // 解析 --linked-changes <a,b|none>(quick 专用:显式声明关联变更,CI/脚本友好)
470
507
  // 与 --change 解耦:--linked-changes 语义清晰(关联变更),不与「指定变更名」混淆。
471
508
  // null = 未指定(走持久化/交互/兼容回退);[] = 显式 none(不关联);[...] = 显式列表
@@ -520,6 +557,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
520
557
  const QUICK_SID_RE = /^quick-[0-9a-f]{8}$/
521
558
  let quickSessionId = null
522
559
  let quickFallbackUsed = false // Q7: --done 未带 --change 时 fallback 读 current-quick-run-id 命中(区别于显式 --change quick-<hex>)
560
+ let quickSidFresh = false // 本轮刚生成全新 sessionId(非 --change 精确恢复 / 非 fallback)——供下方「新会话必须带 --input」门判定
523
561
  if (stageName === 'quick') {
524
562
  // 1373-1376 已把 quick 的 --change 值清进 linkedChanges、changeName 置 null。
525
563
  // 此处回看 --change 原始值:若恰好是单个 quick-<8hex> → 识别为本会话 sessionId(精确恢复),
@@ -548,6 +586,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
548
586
  }
549
587
  if (!quickSessionId) {
550
588
  quickSessionId = 'quick-' + randomUUID().slice(0, 8)
589
+ quickSidFresh = true
551
590
  }
552
591
  changeName = quickSessionId
553
592
  } else {
@@ -556,6 +595,43 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
556
595
  }
557
596
  }
558
597
 
598
+ // ── 本地 agent 会话日志登记 + 上报(平台会话展示用,best-effort)──
599
+ // 平台/daemon 模式下 SillyHub 只能看到 CLI 阶段信息,看不到本地 agent 的实际执行日志。
600
+ // 在此探测当前 agent 环境(Claude Code / Codex / ZCode transcript / SILLYSPEC_AGENT_LOG
601
+ // 覆盖),把日志本地路径登记进 <runtimeRoot>/agent-session-log.json(留底)并立即
602
+ // REST 上报平台(POST /api/agent-logs,与进度上报同风格;协议见
603
+ // docs/platform-agent-log-protocol.md)。探测不到不发不写;任何失败只 warn 一行,
604
+ // 绝不阻断 run 主流程。
605
+ // 位置(2026-08-23-agent-activity-sessions task-01):必须在上方 changeName/quickSessionId
606
+ // 解析完成之后——上报携带会话化上下文 context:entry 级 changeKey/quickId(平台按
607
+ // (workspace, harness, entry.ctx) 聚合建会话;quick 会话 quickId 优先且与 changeKey 互斥)
608
+ // + body 级 hubSessionId(daemon 派发平台会话时注入 env SILLYHUB_SESSION_ID,非空才带)。
609
+ // 两段解析均在顶层作用域,此处统一算一次 context 两值皆可达。
610
+ try {
611
+ const isQuickSession = stageName === 'quick' && Boolean(quickSessionId)
612
+ const hubSessionIdEnv = typeof process.env.SILLYHUB_SESSION_ID === 'string' ? process.env.SILLYHUB_SESSION_ID.trim() : ''
613
+ const agentLogContext = {
614
+ hubSessionId: hubSessionIdEnv || null,
615
+ // quick 会话 → quickId(quick-<8hex> 完整原样,不剥前缀);普通场景 → changeKey(--change 值,无则 null)
616
+ changeKey: isQuickSession ? null : (changeName || null),
617
+ quickId: isQuickSession ? quickSessionId : null,
618
+ }
619
+ const { recordAgentLogInvocation } = await import('../agent-session-log.js')
620
+ const r = await recordAgentLogInvocation({
621
+ cwd,
622
+ invokedCwd,
623
+ platformOpts,
624
+ specBase,
625
+ context: agentLogContext,
626
+ // 只记 flag 名不记值(--output 等 flag 值是 agent 工作文本,不进产物)
627
+ command: [stageName, ...flags.filter(t => typeof t === 'string' && t.startsWith('--'))].join(' '),
628
+ })
629
+ if (r && r.isNew && r.latestLogPath) {
630
+ const pushNote = r.pushed === true ? '已上报平台' : r.pushed === false ? '上报失败(本地已留底)' : '本地留底'
631
+ console.log(`📄 本地 agent 日志已登记(${pushNote}): ${r.latestLogPath}`)
632
+ }
633
+ } catch { /* best-effort:登记失败不影响 run 主流程 */ }
634
+
559
635
  // quick 关联变更存在性守卫(坑 quick-change-phantom-linked):quick 的 --change/--linked-changes
560
636
  // 被解析为关联变更后静默接受不存在的名字(quick 不建变更,链接幻影名必是笔误/语义误用——想给
561
637
  // 会话起名却传了 --change),后果是 QUICKLOG 挂悬空关联污染 change 关联图谱 + --done 时 sessionId
@@ -594,10 +670,23 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
594
670
  // 解析 --file-notes "path::括注 || path::括注"(quick 专用:QUICKLOG「文件:」行落盘多行括注)。
595
671
  // 旁路注入 quicklog.js(per-process setter),不经 complete 收尾路径透传(避碰多 agent 并发改的收尾热点)。
596
672
  // completeQuicklogEntry 读后即清;不传 → '' → 回填审计到的实际文件单行(向后兼容)。
673
+ // 格式 fail-fast(坑 quick-file-notes-format-silent,2026-08-24):分隔符写错(`|`/`,` 等)时
674
+ // 整段会被静默挤进第一个文件的括注、QUICKLOG 落盘后只能手工修——解析处直接拦。
597
675
  let quickFileNotes = ''
598
676
  const fileNotesValue = getFlagValue('--file-notes')
599
677
  if (fileNotesValue !== null) {
600
678
  quickFileNotes = fileNotesValue
679
+ const { validateFileNotesFormat } = await import('../quicklog.js')
680
+ const fv = validateFileNotesFormat(quickFileNotes)
681
+ if (!fv.ok) {
682
+ console.error(`❌ --file-notes 格式不对(${fv.problems.length} 处),已拒绝执行(旧行为会静默挤错进 QUICKLOG,只能手工修):`)
683
+ for (const p of fv.problems.slice(0, 5)) {
684
+ console.error(` 「${p.seg.slice(0, 80)}」${p.reason}——${p.hint}`)
685
+ }
686
+ if (fv.problems.length > 5) console.error(` …等共 ${fv.problems.length} 处`)
687
+ console.error(' 正确格式: --file-notes "path1::括注1 || path2::括注2"(`||` 双竖线分隔多文件,`::` 分隔路径与括注)')
688
+ process.exit(2) // 用法错(--file-notes 格式非法)→ exit 2
689
+ }
601
690
  }
602
691
  setQuickFileNotes(quickFileNotes)
603
692
 
@@ -667,6 +756,22 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
667
756
  process.exit(0)
668
757
  }
669
758
 
759
+ // 坑 quick-no-input-placeholder-title(2026-08-24 用户反馈二期①):全新 quick 会话(上方
760
+ // 刚生成新 sessionId)不带 --input 且无 --linked-changes 可取标题 → 拒绝启动(exit 2)。
761
+ // 旧行为只 warn 提示「建议放弃重启」,占位标题「(quick 任务)」已落盘、只能 reset 重来——
762
+ // 语义标题要拖到最终 --done 才回填,平台「快速修复」列表全程隐藏。此刻零沉没成本(progress
763
+ // 行/QUICKLOG/guard 均未创建),直接拒掉最便宜。放在 --help 短路之后(help 查询不该被拦)、
764
+ // 任何会话副作用之前。精确恢复(--change quick-<hex>)与 done-like(--done/--reset/--cancel/
765
+ // --status/--skip/--reopen)不受限;--linked-changes 启动可从关联变更 proposal/design 提取标题。
766
+ if (stageName === 'quick' && quickSidFresh
767
+ && !(isDone || isStatus || isSkip || isReset || isReopen || isCancel)
768
+ && !inputText && !(Array.isArray(linkedChanges) && linkedChanges.length > 0)) {
769
+ console.error('❌ 新 quick 会话必须带 --input "<一句话任务描述>"(或 --linked-changes <a,b> 从关联变更取标题)。')
770
+ console.error(' 不带描述启动会落「(quick 任务)」占位标题:平台「快速修复」列表隐藏占位条目、语义标题要拖到最终 --done 才回填,长任务全程不可见。')
771
+ console.error(' 用法: sillyspec run quick --input "<一句话任务描述>" [--linked-changes <a,b>] [--files <...>]')
772
+ process.exit(2) // 用法错(新会话无任务描述)→ exit 2
773
+ }
774
+
670
775
  const isAuxiliary = auxiliaryStages.includes(stageName)
671
776
  // scan 元数据追踪(存储在 stageData.scanMeta 中,completeStep 通过 progress 访问)
672
777
 
@@ -1076,6 +1181,20 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1076
1181
  progress = pm.read(cwd, effectiveChange) || progress
1077
1182
  }
1078
1183
 
1184
+ // --file-notes 非末步静默忽略前置 warn(坑 quick-file-notes-nonfinal-ignored,2026-08-22 实证:
1185
+ // step2 --done 传 --file-notes 被静默丢——CLI 短进程,注入随进程结束即丢,白传一轮)。
1186
+ // 放此处:ensureStageSteps 之后 progress 已就绪可判步骤状态;仅 quick + 带了 --file-notes +
1187
+ // 非「唯一 pending 的末步 --done」时提示。末步 --done 是消费点,不提示。
1188
+ if (quickFileNotes !== '' && stageName === 'quick' && !isStatus) {
1189
+ try {
1190
+ const qSteps = progress?.stages?.quick?.steps
1191
+ const pendingCount = Array.isArray(qSteps) ? qSteps.filter(st => ['pending', 'in-progress'].includes(st?.status)).length : 0
1192
+ if (!isDone || pendingCount > 1) {
1193
+ console.warn(`⚠️ --file-notes 本次不会生效:它只在 quick 末步(暂存和更新记录)--done 时随收尾消费——CLI 是短进程,本次注入随进程结束即丢。请在末步 --done 时连同 --output 一起传。`)
1194
+ }
1195
+ } catch { /* 判定失败不提示(fail-open) */ }
1196
+ }
1197
+
1079
1198
  // --status
1080
1199
  if (isStatus) {
1081
1200
  // D9:状态展示末尾附「下一步该跑什么」。pm 持有状态机,按 progress 实际进度算出精确命令;
@@ -25,7 +25,7 @@
25
25
  import { basename, join, resolve, relative, isAbsolute } from 'node:path'
26
26
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from 'node:fs'
27
27
  import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
28
- import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, resolveRuntimeRoot } from './shared.js'
28
+ import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, resolveRuntimeRoot, collectOtherQuickSessionDeclarations } from './shared.js'
29
29
  import { detectConcurrentChanges, formatConcurrentWarning, resolveConcurrentAnchor } from './concurrent-detect.js'
30
30
  import { stageRegistry } from '../stages/index.js'
31
31
  import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
@@ -431,7 +431,11 @@ export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, c
431
431
  console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
432
432
  return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
433
433
  }
434
- const archivedDir = await archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts)
434
+ // 主仓互斥锁(坑 main-repo-no-mutex 二批):archiveChangeDirectory 改主仓共享状态(目录
435
+ // rename + 共享 index 的 git add + marker 删除 + worktree 清理),与并行会话的 apply/cleanup
436
+ // 互踩。exit 钩子兜底:其内部 guard 失败 exit(1) 时锁也会被清(见 withMainRepoLock)。
437
+ const { withMainRepoLock } = await import('../worktree-apply.js')
438
+ const archivedDir = await withMainRepoLock(cwd, changeName, 'archive-finalize', () => archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts))
435
439
  if (archivedDir && existsSync(archivedDir)) {
436
440
  // 内存快照同步(坑 archive-progress-show-stale,2026-08-21 实证):archiveChangeDirectory 内
437
441
  // unregisterChange 已在 DB 写 current_stage='archive'(终态一致化),但本进程 progress 是命令
@@ -863,6 +867,11 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
863
867
  // (平台模式 resolveSpecDir(cwd) 会 miss specRoot 静默丢信号,plan 审查 gap-5)
864
868
  specBase,
865
869
  projectName: progress?.project || null,
870
+ // 他者 active 会话声明索引(坑 foreign-session-declared-false-block):多 agent 并发时
871
+ // 并行会话 --files 声明的文件退栈归该会话审计,不再误拦本会话。与上方 guard 读取同源
872
+ // (resolveQuickSessionsDir 同参,平台模式 runtimeRoot 对齐);fail-open:采集失败 → 空
873
+ // → 回到无豁免现状。
874
+ otherSessionsDeclared: collectOtherQuickSessionDeclarations(platformOpts, specBase, changeName),
866
875
  }
867
876
  review = await auditQuickCompletion(cwd, mergedGuard, { isConfirm: confirm })
868
877
  printQuickAuditReview(review)
@@ -913,8 +922,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
913
922
  const isLastStep = currentIdx === steps.length - 1
914
923
  if (isLastStep && !outputText) {
915
924
  console.error('\n❌ quick 最后一步 --done 必须带 --output(四字段结果模板)。')
916
- console.error(' --output 是 QUICKLOG「结果:」归档的唯一来源。补全后重跑 --done(不丢进度),照抄模板:')
917
- console.error(' sillyspec run quick --done --change <changeName> --output "需求:… 根因:… 方案:… 结果:…"')
925
+ console.error(' --output 是 QUICKLOG「结果:」归档的唯一来源。补全后重跑 --done(不丢进度),推荐四参数形式(CLI 自动合成,无嵌套冒号事故面):')
926
+ console.error(' sillyspec run quick --done --change <changeName> --req "一句话语义化短标题" --cause "为什么这样改" --solution "怎么改的" --result "验证情况(测试数 / lint / typecheck / 部署状态)"')
927
+ console.error(' 兼容旧形式:--output "需求:… 根因:… 方案:… 结果:…"')
918
928
  steps[currentIdx].status = 'pending'
919
929
  steps[currentIdx].completedAt = null
920
930
  process.exit(1)
@@ -924,7 +934,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
924
934
  if (!resultCheck.ok) {
925
935
  console.error('\n' + getRule('quick.result-labels').failMessage.replaceAll('${missing}', resultCheck.missing.join('、')))
926
936
  console.error(` --output 是 QUICKLOG「结果:」归档的唯一来源,四个标签必须放在 --output 里(不是 --input)。`)
927
- console.error(` 补全后重跑 --done(不丢进度),直接照抄此模板:`)
937
+ console.error(` 补全后重跑 --done(不丢进度)。推荐四参数形式(CLI 自动合成结构化模板,避免旧形式嵌套全角冒号被拆分判定缺字段):`)
938
+ console.error(` sillyspec run quick --done --change <changeName> --req "一句话语义化短标题(即 QUICKLOG 条目标题)" --cause "为什么这样改(纯新增/样式则写「无,纯新增/纯样式」)" --solution "怎么改的" --result "验证情况(测试数 / lint / typecheck / 部署状态)"`)
939
+ console.error(` 或照抄旧形式模板:`)
928
940
  console.error(` sillyspec run quick --done --change <changeName> --output "需求:用户/任务要什么`)
929
941
  console.error(` 根因:为什么这样改(纯新增/样式则写「无,纯新增/纯样式」)`)
930
942
  console.error(` 方案:怎么改的`)
@@ -1331,22 +1343,39 @@ export async function handleExecuteWorktreeCleanup({ stageName, changeName, cwd
1331
1343
  } else if (meta.mode === 'native-worktree') {
1332
1344
  console.log('🔗 Worktree: kept (外部隔离环境)');
1333
1345
  } else {
1334
- // in-place 模式不再短路:cleanup 现在能安全处理 in-place(只清 meta,不碰主工作区)
1335
- const check = wm.hasUnappliedChanges(changeName);
1336
- if (check.hasChanges) {
1337
- console.log(`🔗 Worktree: pending apply (${check.changedFiles.length} 个未应用变更)`);
1338
- console.log(` 下一步: sillyspec worktree apply ${changeName}`);
1339
- } else {
1340
- const cleanResult = wm.cleanup(changeName);
1341
- console.log(`🔗 Worktree: ${cleanResult.result}`);
1342
- if (cleanResult.residual?.length > 0) {
1343
- console.warn(` ⚠️ 清理残留: ${cleanResult.residual.join('; ')}`);
1344
- console.warn(` 手动处理: sillyspec worktree cleanup ${changeName} --force`);
1345
- } else if (cleanResult.details?.length > 0) {
1346
- for (const d of cleanResult.details) {
1347
- if (d.startsWith('⚠️')) console.log(` ${d}`);
1346
+ // in-place 模式不再短路:cleanup 现在能安全处理 in-place(只清 meta,不碰主工作区)。
1347
+ // 主仓互斥锁(坑 main-repo-no-mutex 二批):检查+清理与并行会话的 apply/cleanup 互斥
1348
+ // (防 TOCTOU:检查时他者正在 apply → 判定漂移)。best-effort:锁超时不阻断 execute
1349
+ // 完成,降级为保留 worktree + 手动清理指引。
1350
+ try {
1351
+ const { withMainRepoLock } = await import('../worktree-apply.js')
1352
+ const cleanResult = await withMainRepoLock(cwd, changeName, 'execute-cleanup', () => {
1353
+ const check = wm.hasUnappliedChanges(changeName);
1354
+ if (check.hasChanges) {
1355
+ return { kept: true, check }
1356
+ }
1357
+ return { kept: false, result: wm.cleanup(changeName) }
1358
+ })
1359
+ if (cleanResult.kept) {
1360
+ console.log(`🔗 Worktree: pending apply (${cleanResult.check.changedFiles.length} 个未应用变更)`);
1361
+ console.log(` 下一步: sillyspec worktree apply ${changeName}`);
1362
+ } else {
1363
+ console.log(`🔗 Worktree: ${cleanResult.result.result}`);
1364
+ if (cleanResult.result.residual?.length > 0) {
1365
+ console.warn(` ⚠️ 清理残留: ${cleanResult.result.residual.join('; ')}`);
1366
+ console.warn(` 手动处理: sillyspec worktree cleanup ${changeName} --force`);
1367
+ } else if (cleanResult.result.details?.length > 0) {
1368
+ for (const d of cleanResult.result.details) {
1369
+ if (d.startsWith('⚠️')) console.log(` ${d}`);
1370
+ }
1348
1371
  }
1349
1372
  }
1373
+ } catch (lockErr) {
1374
+ if (/互斥锁被占用/.test(String(lockErr.message))) {
1375
+ console.log(`🔗 Worktree 自动清理跳过(主仓互斥锁被他者会话持有)——稍后手动: sillyspec worktree cleanup ${changeName}`);
1376
+ } else {
1377
+ console.warn(`⚠️ worktree 清理异常(不阻断): ${lockErr.message}`);
1378
+ }
1350
1379
  }
1351
1380
  }
1352
1381
  } catch (e) {