sillyspec 3.28.4 → 3.28.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 (49) hide show
  1. package/AGENTS.md +1 -0
  2. package/package.json +1 -1
  3. package/results-review-tier.md +18 -0
  4. package/src/agent-session-log.js +4 -3
  5. package/src/archive-delta.js +2 -2
  6. package/src/change-list.js +11 -1
  7. package/src/commit-suggest.js +52 -1
  8. package/src/config-cat.js +6 -1
  9. package/src/decision-distill.js +45 -5
  10. package/src/decisions-io.js +96 -0
  11. package/src/design-facts.js +3 -0
  12. package/src/dispatch/probe.js +31 -8
  13. package/src/docs-check.js +68 -3
  14. package/src/index.js +426 -17
  15. package/src/mcp-server.js +144 -0
  16. package/src/module-impact.js +8 -1
  17. package/src/modules.js +46 -34
  18. package/src/progress/change-registry.js +18 -0
  19. package/src/progress.js +13 -1
  20. package/src/quicklog.js +60 -6
  21. package/src/review-dispatch.js +377 -0
  22. package/src/run/archive-distill.js +48 -0
  23. package/src/run/command.js +19 -5
  24. package/src/run/complete-handlers.js +63 -3
  25. package/src/run/complete.js +139 -2
  26. package/src/run/gate-snapshot.js +88 -0
  27. package/src/run/gates.js +115 -55
  28. package/src/run/prompt.js +54 -9
  29. package/src/run/quick-audit.js +38 -13
  30. package/src/run/shared.js +109 -6
  31. package/src/run/stage.js +17 -2
  32. package/src/run/verify-quality-scan.js +336 -0
  33. package/src/scope-audit.js +735 -0
  34. package/src/sillyhub-mcp/client.js +58 -3
  35. package/src/stage-review-checklist.js +44 -0
  36. package/src/stage-review.js +122 -4
  37. package/src/stages/archive.js +57 -164
  38. package/src/stages/brainstorm.js +5 -9
  39. package/src/stages/execute.js +13 -10
  40. package/src/stages/plan.js +3 -10
  41. package/src/stages/verify.js +14 -33
  42. package/src/sync.js +51 -10
  43. package/src/task-review.js +108 -16
  44. package/src/taskcard.js +46 -3
  45. package/src/validate-artifacts.js +163 -0
  46. package/src/verify-postcheck.js +90 -10
  47. package/src/verify-probes.js +134 -12
  48. package/src/worktree-apply.js +119 -14
  49. package/templates/agents-instruction.md +1 -0
@@ -1,3 +1,5 @@
1
+ import { REVIEW_CHECKLISTS } from '../stage-review-checklist.js'
2
+
1
3
  export const definition = {
2
4
  name: 'brainstorm',
3
5
  title: '头脑风暴',
@@ -380,6 +382,7 @@ design.md 文件路径 + 自审结果
380
382
  tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
381
383
  - tier=self:当前 agent 直接执行下方交叉审查(小变更)
382
384
  - tier=independent:必须用 Agent tool 启动一个独立的设计审查子代理(独立上下文,不共享你的分析与倾向),子代理按下方"交叉审查模型"审查 design.md 并输出 review.json。review.json 产物契约(CLI Stage Review Gate 将硬校验,schema + 完整示例 + docHash 算法如下,照抄改值):
385
+ 宿主环境无 Agent tool 可用(调用报 Unknown agent / Available agents: none)→ 不卡死:主代理切换为审查者角色自审替代,reviewerNotes 首行记录「降级:环境无子代理可用」,逐条结论附源码锚点(file:line 或 grep/read 证据)补偿独立性。
383
386
  {REVIEW_JSON_CONTRACT}
384
387
  子代理只产出 review + Unresolved Blockers,**是否调用 sillyspec run brainstorm --wait 仍由你(主 agent)根据其 verdict 决定**(子代理不直接操作 CLI 状态机)。
385
388
 
@@ -403,18 +406,11 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
403
406
 
404
407
  ### 交叉审查模型
405
408
  按三层检查并输出 cross-check matrix:
406
- 1. **定义层**:模糊概念是否有可测试定义。例如"高可用""异常数据""本地缓存""重试"。
407
- 2. **一致性层**:跨章节/跨产物是否打架。例如数据流 vs 容错策略、schema vs 输入格式、非目标 vs tasks。
408
- 3. **可行性层**:关键假设是否有来源。例如 P99 延迟、上游 SLA、缓存 TTL、数据量、权限模型、兼容旧配置。
409
+ ${REVIEW_CHECKLISTS.brainstorm.slice(0, 3).map((item, index) => (index + 1) + '. ' + item).join('\n')}
409
410
 
410
411
  ### 交叉点抽取
411
412
  重点找这些交叉点:
412
- - 模块 A 依赖模块 B 的实体/状态/接口
413
- - requirements.md 的 FR 与 design.md 的数据模型/API/状态机
414
- - design.md 的容错策略与数据流、缓存、重试、回滚
415
- - tasks.md 的执行范围与 design.md 的非目标
416
- - decisions.md 的 D-xxx@vN 与 design.md 当前说法
417
- - scan/module docs 或源码中的真实约束与 design.md 假设
413
+ ${REVIEW_CHECKLISTS.brainstorm.slice(3).map((item) => '- ' + item).join('\n')}
418
414
 
419
415
  ### 问答处理
420
416
  1. 先自动交叉审查,不要一上来问用户。
@@ -6,6 +6,7 @@ import { renderDispatchInstruction } from '../dispatch/strategy.js'
6
6
  import { isPathASupported } from '../dispatch/backends/sillyhub-mcp.js'
7
7
  import { readMcpConfig } from '../sillyhub-mcp/config.js'
8
8
  import { gitQuiet } from '../git-helper.js'
9
+ import { REVIEW_CHECKLISTS } from '../stage-review-checklist.js'
9
10
  import { parseRepo } from './plan-postcheck.js'
10
11
  // 模块卡分级解析(token 成本优化 P0a)。注意:module-resolve.js 反向 import 本文件的
11
12
  // parseTaskRegistry(注册表同口径)——双向引用均为函数声明 + 调用时解引(无模块初始化期
@@ -323,6 +324,7 @@ worktree 路径 + 分支名 + 模式
323
324
 
324
325
  如上为 CLI 用 git 事实算出的本变更触及模块文档欠账(无输出=无欠账或归属数据缺失)。
325
326
  欠账处理:Wave 收尾时顺手同步对应模块卡,不必为此停下。变更索引类条目追加到卡同目录 \`<module>.changelog.md\` sidecar(无则创建;可先跑 \`sillyspec modules split-changelog\` 迁出历史段)——勿把历史条目堆回模块卡正文(卡是子代理的读取税)。
327
+ ⚠️ **铁律:模块卡 / sidecar 一律写主仓绝对路径 \`{SPEC_ROOT}/docs/<project>/modules/\`(CLI 已替换为绝对路径)**——在 worktree 内执行时**绝不**写 worktree checkout 出来的 \`.sillyspec\` 副本(该副本随 worktree cleanup 整目录蒸发,主仓永远读不到;2026-09-10 实证 task-13 子代理写副本、归档阶段被迫 checkout 补救)。
326
328
 
327
329
  ### 铁律
328
330
  - **不要询问用户确认频率**,默认 wave 模式;用户已明确口头指定时遵从其指定`,
@@ -342,13 +344,12 @@ const acceptanceSteps = [
342
344
  tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
343
345
  - tier=self:当前 agent 汇总执行(对照 design.md 逐项检查 + 偏差说明)
344
346
  - tier=independent:必须用 Agent tool 启动一个独立的 QA 子代理(独立上下文,不共享实现者的分析),子代理对照 design.md 逐项检查实现一致性并输出 review.json。review.json 产物契约(CLI Stage Review Gate 将硬校验,schema + 完整示例 + docHash 算法如下,照抄改值;reviewedFiles 除主文档 design.md 外可追加 git diff 涉及的源码文件):
347
+ 宿主环境无 Agent tool 可用(调用报 Unknown agent / Available agents: none)→ 不卡死:主代理切换为审查者角色自审替代,reviewerNotes 首行记录「降级:环境无子代理可用」,逐条结论附源码锚点(file:line 或 grep/read 证据)补偿独立性。
345
348
  {REVIEW_JSON_CONTRACT}
346
349
  该 acceptance review 同时覆盖"代码审查"视角(风格/bug/安全/冗余),后续代码审查步骤仅需轻量复审。
347
350
 
348
351
  **审查范围分级(省重复消耗,task review 已覆盖的不全量重审)**:每个 task 在 Task Review Gate 已产出 review.json({SPEC_ROOT}/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-XX/review.json,specVerdict/qualityVerdict 双 pass)。QA 子代理按它分层:双 pass 的 task 只**抽查**(读 1-2 个核心 diff 文件抽验 reviewerNotes 与实际改动相符,不必逐文件重审);未双 pass(fail/cannot_verify/缺失)的 task 必须全量重审。无论抽查还是全量,以下三项始终必查——task review 铁律是"只看当前 task 的 diff",这三项是它覆盖不到、只有 stage review 能兜住的:
349
- 1. 跨 task 交界(A 产出的接口/数据结构与 B 的消费是否对得上)
350
- 2. design.md 整体对照(最终实现拼起来是否仍符合设计意图,而非仅各 task 局部合规)
351
- 3. 组装行为(全量测试/构建/启动通过——单 task 测试全绿 ≠ 组装正确)
352
+ ${REVIEW_CHECKLISTS.execute.map((item, index) => ' ' + (index + 1) + '. ' + item).join('\n')}
352
353
 
353
354
  **产物唯一化(省重复消耗)**:本步逐项对照结论**只落盘一份**——直接写进 review.json 的 \`checklist\` 数组(item=设计要点/FR/决策,note=实现状态 ✅/⚠️/❌ + 偏差说明 + commit 锚点),reviewerNotes 写汇总。**不要**另写独立的 design-check.md 长文(同一份 design×diff 二次消费;2026-08-22 实测该重复一遍 ≈8 分钟全量重读)。
354
355
  **gate 重试修复**:review.json 落盘后若 design.md 又有改版,gate 会**自动机械重算 docHash 放行**(verdict/checklist 保留,结论是否仍适用于新文档需人工确认)——**不要重做审查**(重做=同一材料第三遍);主文档路径错/缺失不会被自动修复,按 gate 报错修正 reviewedFiles[0],或跑 \`sillyspec register-stage-review --change <变更名> --stage execute --refresh-hash\`。
@@ -1061,7 +1062,7 @@ ${indexLines}
1061
1062
  ${crossLines}${crossWorktreeSection}${crossLegacySection}
1062
1063
 
1063
1064
  **回收跨仓 task(head 锡点,CLI 自动落盘,勿手写):**
1064
- - 子代理完成 commit 后,正常写 review.json(verdict/notes)并勾选 checkbox 即可。execute \`--done\` 时 CLI 自动对跨仓仓(worktree 模式=该仓 worktree,legacy=仓根)\`git rev-parse HEAD\` 写入该 task 卡 \`head_commit:\`(幂等,已存在不覆盖——你若手写了精确锚点则以你的为准)。
1065
+ - 子代理完成 commit 后,正常写 review.json(verdict/notes)即可——checkbox 由 CLI 按 review verdict 自动勾选(review write 落盘即勾)。execute \`--done\` 时 CLI 自动对跨仓仓(worktree 模式=该仓 worktree,legacy=仓根)\`git rev-parse HEAD\` 写入该 task 卡 \`head_commit:\`(幂等,已存在不覆盖——你若手写了精确锚点则以你的为准)。
1065
1066
  - review.json 的 mechanics 字段(\`base\`/\`head\`/\`changedFiles\`/\`diffPaths\`)无需手算:\`base\` 取 task 卡 \`base_commit\`、\`head\` 取 task 卡 \`head_commit\`,写完跑 \`sillyspec backfill-reviews --change <变更名> --adopt\` 一键代填(verdict 保留)。
1066
1067
  `
1067
1068
  }
@@ -1111,7 +1112,9 @@ ${workdirLines}
1111
1112
  \`\`\`
1112
1113
 
1113
1114
  ### 注意
1114
- 蓝图文件(tasks.md / design.md / proposal.md / requirements.md)在主工作区 {SPEC_ROOT}/changes/<change>/ 下(CLI 已替换为主仓绝对路径),它们可能不在 worktree 中。读取蓝图时使用主工作区路径,不要拼接到 worktree 路径下;同理,spec 流程产物(module-impact.md / knowledge 条目 / 模块文档)只写主仓 {SPEC_ROOT},绝不写进 worktree 副本。
1115
+ 蓝图文件(tasks.md / design.md / proposal.md / requirements.md)在主工作区 {SPEC_ROOT}/changes/<change>/ 下(CLI 已替换为主仓绝对路径),它们可能不在 worktree 中。读取蓝图时使用主工作区路径,不要拼接到 worktree 路径下。
1116
+
1117
+ ⚠️ **铁律:spec 流程产物只写主仓 {SPEC_ROOT},绝不写进 worktree 副本**——包括 module-impact.md / knowledge 条目 / 模块卡与 \`<module>.changelog.md\` sidecar。在 worktree 内发现 \`.sillyspec/\` 目录是 checkout 副本,写进去的任何内容都会随 worktree cleanup 整目录蒸发(2026-09-10 实证:模块文档写副本、归档被迫 checkout 补救)。子代理 prompt 中涉及此类产物时,必须原样带上主仓绝对路径。
1115
1118
  `
1116
1119
  : ''
1117
1120
  }
@@ -1158,7 +1161,7 @@ ${workdirLines}
1158
1161
  你的角色是调度者 + 审查者(batch 只合并实现、不合并审查):
1159
1162
  1. 为每个任务启动一个子代理(Agent tool),或按上述三条件把多个任务合并为一个 batch 子代理,同 Wave 内可并行
1160
1163
  2. 子代理完成后审查结果——batch 子代理只做实现与自验,task 审查、review.json 产出与 checkbox 勾选仍归你(主 agent),在子代理返回后逐 task 进行;审查 batch 报告时逐 task 对照 allowed_paths 检查改动文件清单有无越权
1161
- 3. 勾选 tasks.md 中对应任务的 checkbox
1164
+ 3. checkbox 由 CLI 自动勾选(review write 落盘即按 verdict 勾选 tasks.md;勿手动勾选)
1162
1165
  4. 记录改动文件和测试结果
1163
1166
 
1164
1167
  ${worktreeSection}${crossRepoCommitSection}${dispatchSection}
@@ -1211,21 +1214,21 @@ ${taskList}
1211
1214
  - 用户明确要求编译时
1212
1215
  4. 每个任务完成后:
1213
1216
  - **在 worktree 内 git add -A && git commit -m "<task-NN 摘要>"**(坑 subagent-uncommitted-newfile-apply3way,2026-08-22 实证:纯新增文件不 commit 时 apply 的 git apply --3way 报 "does not exist in index" 直接炸——未 commit 的新文件不在 base commit 也不在 index,patch 生成取不到。commit 后 base..HEAD diff 完整、apply 顺畅,review.json 的 head 也有真实锚点)
1214
- - **先写 review.json 再勾选 checkbox**(见下方 Task Review Gate)
1215
- - **任务边界上报(每任务一次,主仓根目录)**:勾选 checkbox 后跑一次 \`sillyspec platform sync --change <change-name>\`——以任务粒度把「最后信号」(last_pushed_at)与 tasks.md 勾选状态推上平台(变更中心「进行中」可见性);未连接平台时该命令静默跳过,无需先检查连接状态
1217
+ - **写 review.json 即可**(checkbox 由 CLI 自动勾选,见下方 Task Review Gate)
1218
+ - **任务边界上报(每任务一次,主仓根目录)**:review write 落盘(CLI 已自动勾选)后跑一次 \`sillyspec platform sync --change <change-name>\`——以任务粒度把「最后信号」(last_pushed_at)与 tasks.md 勾选状态推上平台(变更中心「进行中」可见性);未连接平台时该命令静默跳过,无需先检查连接状态
1216
1219
  - **既跑 lint check 也跑 formatter**:凡变更涉及的源码跑项目的 lint 检查 **和** 格式化(如 \`ruff format\` / \`prettier --write\`),不要只跑 check——只 check 不 format 会把格式问题留到 commit 时被 pre-commit hook 拦截(worktree 内二进制可能缺失,先 \`which <bin>\` 确认,缺则 \`uv tool install\` / \`uv sync\`)
1217
1220
  - 记录改动文件和测试结果
1218
1221
  5. 遇到 BLOCKED → 记录原因,选择:重试/跳过/停止
1219
1222
 
1220
1223
  ### Task Review Gate
1221
1224
 
1222
- 每个子代理完成后、勾选 checkbox **之前**,你必须创建 task review。
1225
+ 每个子代理完成后,你必须创建 task review(review write 落盘后 CLI 自动勾选 checkbox——P2-f 起勾选唯一写入者是 CLI)。
1223
1226
 
1224
1227
  **操作步骤:**
1225
1228
  1. 读取当前 task 的 git diff(从 task 开始到完成的变更)
1226
1229
  2. 对照 plan.md 中该 task 的描述和 tasks/task-XX.md(如果存在)检查实现是否符合要求
1227
1230
  3. 写入 review.json 文件
1228
- 4. **只有 review.json 写入成功后,才允许勾选 tasks.md 中对应任务的 checkbox**(勾选唯一落点;CLI 的 autoCheckPlanFromReviews 机器勾选器同样写 tasks.md,文件锁 .tasks.md.lock 串行化双路勾选)
1231
+ 4. **禁止手动勾选 tasks.md 的 checkbox**(P2-f task 真源归一:review.json verdict 是唯一真源,tasks.md 勾选是它的显示态,唯一写入者是 CLI——review write 落盘即勾、execute --done 时 autoCheckPlanFromReviews 兜底;agent 手勾与机器勾的漂移面就此退役)
1229
1232
 
1230
1233
  **review.json 路径:**
1231
1234
 
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'fs'
2
2
  import path from 'path'
3
3
  import { getRule } from '../stage-contract-spec.js'
4
+ import { REVIEW_CHECKLISTS } from '../stage-review-checklist.js'
4
5
 
5
6
  // 从 plan-postcheck.js 重导出(保持向后兼容)
6
7
  export {
@@ -331,18 +332,10 @@ plan.md 审查通过后、进入 execute 前,若 plan_level=full(跨模块/
331
332
  tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
332
333
  - tier=self:当前 agent 直接执行下方审查清单(小变更,独立审查仪式成本 > 收益)
333
334
  - tier=independent:必须用 Agent tool 启动一个独立的计划审查子代理(独立上下文,不共享你生成 plan 时的分析与倾向),由子代理执行下方审查清单并输出 review.json
335
+ 宿主环境无 Agent tool 可用(调用报 Unknown agent / Available agents: none)→ 不卡死:主代理切换为审查者角色自审替代,reviewerNotes 首行记录「降级:环境无子代理可用」,逐条结论附源码锚点(file:line 或 grep/read 证据)补偿独立性。
334
336
 
335
337
  ### 审查清单(读取 plan.md 的 plan_level,逐条核对)
336
- - [ ] task 编号与 Wave checkbox 格式正确,execute 依赖此格式解析任务
337
- - [ ] plan_level 档位与实际复杂度匹配(none/light/full 没选错)
338
- - [ ] 跨任务契约:task-A 的产出(接口/DTO/响应)被 task-B 消费时,consumer 是否在 TaskCard expects_from 声明所需字段、provider 是否在 provides 承诺、两边字段一致?(plan-postcheck 会硬校验,此处是独立视角复查)
339
- - [ ] 文件覆盖:design.md 文件变更清单中的每个源码文件,是否都被至少一个 task 的 allowed_paths 覆盖?(漏覆盖 = execute 必然漏改。跨仓变更对账口径:design 清单按「## <repo-key> 仓变更」分段、路径相对各仓根,task 卡 repo: + 同口径相对路径匹配——allowed_paths 带仓库名前缀或绝对路径 = 永不命中、对账不上)
340
- - [ ] 不存在 P0/P1 unresolved blocker 残留
341
- - [ ] 没有实现细节泄漏到 plan.md(接口签名/代码示例应在 tasks/task-NN.md)
342
- - [ ] 关键路径与 Wave 依赖合理(无循环依赖、无遗漏前置)
343
- - [ ] Wave 分组说明:依赖方向违规(depends_on 同 Wave / 后置 Wave)在 --done 时会按拓扑**自动修复**(提案经一致性+文件面验证后落盘);共享文件的手工分 Wave 串行是合法安全模式,不会被自动改写
344
- - [ ] 连带测试归属:本批改动是否会导致既有测试断言失效(改共享/被多 task 依赖源文件、改被测试精确匹配的值如 UI 文案/按钮文本/错误信息/常量/枚举字面量、改函数签名或返回结构等单文件场景)?此类 task 是否在 related_tests 声明了失效测试、且路径在 allowed_paths 内(或由独立测试 task 覆盖)?(漏声明 = execute 阶段测试债、主代理事后兜底)
345
- - [ ] acceptance 字段对照实际 schema/类型源文件核验存在性与形态,不凭 design.md 文字臆断(plan-postcheck best-effort grep 会给 allowed_paths 源文件未命中的 snake_case/camelCase 标识符提 warning,此处是语义层复查;臆断 = execute 阶段返工)
338
+ ${REVIEW_CHECKLISTS.plan.map((item) => '- [ ] ' + item).join('\n')}
346
339
 
347
340
  ### tier=independent 时:启动 plan-review 子代理
348
341
  用 Agent tool 启动子代理(subagent_type: general),prompt 要点:
@@ -98,7 +98,7 @@ export const definition = {
98
98
  },
99
99
  {
100
100
  name: '逐项检查任务',
101
- prompt: `对照 tasks.md(任务注册表唯一真相)检查每个任务完成状态。勾选由 execute 双路写入(agent 按 review gate 手动勾 + CLI autoCheckPlanFromReviews 机器勾选器按 review.json 自动勾),本阶段只读对照、不改勾。
101
+ prompt: `对照 tasks.md(任务注册表唯一真相)检查每个任务完成状态。勾选唯一写入者是 CLI(review write 落盘即按 review.json verdict 勾选,execute --done 时 autoCheckPlanFromReviews 兜底;P2-f 起 agent 不再手勾),本阶段只读对照、不改勾。
102
102
 
103
103
  ### 勾选状态(CLI 注入,勿手数)
104
104
  {TASKS_CHECKBOX}
@@ -169,36 +169,17 @@ export const definition = {
169
169
  },
170
170
  {
171
171
  name: '运行测试和质量扫描',
172
- prompt: `运行代码质量扫描(测试实测统一由 CLI 对账执行,本步不重复手动跑全量)。
173
-
174
- ### 操作
175
- 1. 构建命令(CLI 自 local.yaml 注入,勿再读文件;未配置时按注入说明跑 local detect):
176
- {LOCAL_COMMANDS}
177
- 2. **不要手动重复跑 commands.test**——CLI 会在最终 --done 时统一执行一次(按变更命中模块子集),本步再跑 = 与 CLI 对账重复耗时(实测 198s×2)。如为提前发现实现问题,可对变更模块做**针对性快速冒烟**(可选,非必需):
178
- - Maven:\`mvn test -pl <变更模块> -am\`(仅编译变更模块及其依赖)
179
- - Gradle:\`./gradlew :<模块>:test\`
180
- - npm/pnpm:\`pnpm test --filter=<包名>\` 或 \`npm test -- --testPathPattern=<相关文件>\`
181
- - Python:\`pytest <变更模块路径>/\`
182
- 3. 如果 local.yaml 有 lint 命令,运行 lint 检查并修复报出的问题(--done 时 CLI 会亲自实测 commands.lint 对账,实测失败会明示)
183
- 4. 搜索技术债务:grep TODO/FIXME/HACK/XXX(仅限变更文件)
184
-
185
- ### 注意
186
- - **CLI 对账机制**:verify 阶段最终 --done 时,CLI 会亲自执行 local.yaml 的 commands.test(同步,耗时可能较长);实测失败会直接阻断 verify 完成,谎报测试结果没有意义。commands.lint 同样会被 CLI 实测对账(advisory)
187
- - 冒烟测试非必需:全量/模块实测结果以 CLI 对账为准,本步跑的结果仅供你提前发现问题并写入验证报告
188
-
189
- ### 检查选择指引(按变更影响面收窄,本地聚焦;FR-12)
190
- 按本次变更的实际影响面选择检查组合,避免无差别全量:
191
- - **行为类改动**(源码逻辑/数据结构/接口/调用关系/配置)→ 聚焦测试:只跑变更命中模块的子集(local.yaml 配 \`test_strategy: module\` + modules 映射),CLI --done 对账同口径收窄
192
- - **文档/prompt 类改动**(*.md、docs/** 等)→ 文档检查(\`sillyspec docs check\` 口径),不跑代码测试
193
- - **门禁/契约类改动**(gate/contract 相关文件、对外接口契约)→ 契约对账(\`sillyspec gate\` 口径)优先于跑测试
194
- - **全量**仅在用户明确要求、或仓库级不可分变更(横切基础设施/全仓重命名等无法按模块拆分)时执行——本地聚焦,全量留给 CI 或明确要求
195
-
196
- ### 测试策略推荐(CLI 注入;test_strategy: evidence-auto 时非空,其余策略为空)
197
- {EVIDENCE_AUTO_RECOMMENDATION}
198
-
199
- ### 输出
200
- 质量扫描结果 + 技术债务标记`,
201
- outputHint: '质量扫描结果 + 技术债务',
172
+ // noAI 化(P0-1,docs/sillyspec/noai-ir-roadmap.md §3):test/lint 实测提前到本步由 CLI
173
+ // 亲自执行(run/verify-quality-scan.js executeVerifyQualityScan)——本步此前的 agent 角色
174
+ // 是「可选冒烟 + lint 转述」纯中继(旧 prompt 自述「CLI 会在 --done 统一跑,实测 198s×2」)。
175
+ // 结果按代码指纹落盘,--done 对账指纹匹配即复用免重跑(前置一);失败打印输出 + 并行 WIP
176
+ // 归因提示后不盖 completed,agent 修复后 run verify 复入本步(前置二)。步骤名不变
177
+ //(存量进度库按名匹配迁移);test_strategy 收窄(module/evidence-auto/skip)由实测函数
178
+ // 自行解析,检查选择指引随之退役。
179
+ noAI: true,
180
+ _cliAction: 'verifyRunQualityScan',
181
+ prompt: '',
182
+ outputHint: '质量扫描结果(CLI 实测输出 + 指纹复用记录)',
202
183
  optional: false
203
184
  },
204
185
  {
@@ -230,9 +211,9 @@ export const definition = {
230
211
 
231
212
  ### 🧹 服务进程登记与自动回收(真实启动验证必读,坑 verify-service-process-leak)
232
213
  「真实启动一次」起的长驻服务(uvicorn/node server 等)**必须登记 PID,CLI 在 verify 收尾自动回收**——不登记的进程会挂死机器(实测 uvicorn 漏挂一天多):
233
- 1. 后台启动服务并取 PID(PowerShell:\`Start-Process -PassThru\` 取 \`.Id\`;bash:\`cmd & echo $!\`)
214
+ 1. 后台启动服务并取 PID(PowerShell:\`Start-Process -PassThru\` 取 \`.Id\`;bash:\`cmd & echo $!\`)。**优先登记服务本体(叶子)PID**——经 shell 包装启动(\`sh -c\` / \`cmd /c\` / uvicorn --reload 的 reloader)时 CLI 虽已按进程树击杀(2026-09-10 起连带子进程),但叶子 PID 最稳:包装 shell 退出后子进程 PID 不漂移、跨平台一致
234
215
  2. 每行一个 PID 追加到 \`{SPEC_ROOT}/.runtime/verify-services-<change-name>.pids\`(\`Add-Content\` / \`echo >>\`;**按变更名分片**——多会话并发 verify 时各自的 --done 只回收自己的服务,互不误杀)
235
- 3. verify \`--done\` 时 CLI 读该文件逐个 kill 并清空——你在 verify-result.md 的「生命周期终态断言」里写「PID 已登记,CLI 收尾回收」即可
216
+ 3. verify \`--done\` 时 CLI 读该文件**逐进程树击杀**(包装进程连同子进程)并清空——你在 verify-result.md 的「生命周期终态断言」里写「PID 已登记,CLI 收尾回收」即可
236
217
  登记是硬要求:没有 PID 登记的「真实启动」证据在 deployment-critical 门控下视为不完整(进程无法证明被回收)。
237
218
 
238
219
  ### 输出
package/src/sync.js CHANGED
@@ -471,6 +471,28 @@ export class SyncManager {
471
471
  );
472
472
  }
473
473
 
474
+ // mcp 段成对签发(2026-09-10 平台侧 P0-2 修复的消费侧落地,交付遗留②):mcp-tokens
475
+ // 签发 API 的 201 响应带 gateway_url,与 token 成对——「url 指哪、token 就在哪生效」,
476
+ // 消 spike 实证的凭据三头分裂(mcp.url 指远端而 token 属本地部署 / 旧 token 缺 read
477
+ // scope 致 get_daemon_status 等全挂)。scope 定 read+dispatch(P2 审查通道:get_daemon_
478
+ // status/get_worker_result 需 read,dispatch_worker 需 dispatch;converge 按 D-004 不申请)。
479
+ // 失败(旧 backend 无端点 / 403 / 断网)→ 降级旧行为(下方 §7.4 分支),不阻断 connect。
480
+ let mcpIssued = null;
481
+ if (resolved && resolved.workspace_id) {
482
+ const issueUrl = `${normalizedUrl}/api/workspaces/${resolved.workspace_id}/mcp-tokens`;
483
+ const issued = await fetchJson(issueUrl, {
484
+ method: 'POST',
485
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
486
+ body: JSON.stringify({ name: 'sillyspec-cli', scope: ['read', 'dispatch'] }),
487
+ });
488
+ if (issued && typeof issued.token === 'string' && issued.token.startsWith('shmcp_')) {
489
+ mcpIssued = issued;
490
+ console.log(`[sync] MCP token 成对签发成功 (gateway: ${issued.gateway_url || normalizedUrl})`);
491
+ } else {
492
+ console.warn('[sync] MCP token 签发失败(旧 backend 无端点/权限不足),mcp 段按旧口径处理');
493
+ }
494
+ }
495
+
474
496
  // 解析推送者身份(D-004):显式 > git user.name > env;全失败留空不写 user 字段
475
497
  const resolvedUser = resolvePlatformUser(this.cwd, user);
476
498
 
@@ -489,10 +511,16 @@ export class SyncManager {
489
511
  }
490
512
  let text = readLocalYamlRaw(this.cwd);
491
513
  text = replaceTopLevelSection(text, 'platform', platformEntries.join('\n'));
492
- // mcp 段同源假设(design §7.4):url 复用 platform 的 url;token 用原始 user 级 token
493
- // (非换发的 effectiveToken——MCP 派发需 user 级权限,platform 段才是最小权限的 shpsync_)。
494
- // 用户已手填 mcp 段则保留不覆盖(R-09,文本级检测)
495
- if (findTopLevelSectionRange(text, 'mcp') === null) {
514
+ // mcp 段写入优先级(2026-09-10 成对签发批次):①签发成功 → 覆盖写(含手填旧段——
515
+ // 成对值是确定性有效凭据,覆盖陈旧/错部署段是修复不是破坏,spike 实证的「旧 token
516
+ // 永不愈合」正源于旧口径的不覆盖);②签发失败但段缺 → §7.4 同源假设补 url + user
517
+ // token(旧行为);③签发失败且段在 → 手填段保留(R-09 不覆盖)。
518
+ if (mcpIssued) {
519
+ const gwUrl = typeof mcpIssued.gateway_url === 'string' && mcpIssued.gateway_url
520
+ ? mcpIssued.gateway_url.replace(/\/+$/, '')
521
+ : `${normalizedUrl}/mcp`;
522
+ text = replaceTopLevelSection(text, 'mcp', ` url: ${yamlStr(gwUrl)}\n token: ${yamlStr(mcpIssued.token)}`);
523
+ } else if (findTopLevelSectionRange(text, 'mcp') === null) {
496
524
  text = replaceTopLevelSection(text, 'mcp', ` url: ${yamlStr(normalizedUrl)}\n token: ${yamlStr(token)}`);
497
525
  }
498
526
  writeLocalYamlRaw(this.cwd, text);
@@ -671,10 +699,21 @@ export class SyncManager {
671
699
  });
672
700
 
673
701
  // X1(task-04 契约):409 + code='change_deleted' = 平台已删 key 拒收——不是 base_ts 冲突。
674
- // 单行提示即可,不落冲突文件、不打全幅横幅:墓碑已收敛后的重复上行(归档收尾多步各推一次)、
702
+ // 不落冲突文件、不打全幅横幅:墓碑已收敛后的重复上行(归档收尾多步各推一次)、
675
703
  // 多用户下他端删除后本地仍在推进,都是可预期态,按冲突卡死人工 resolve 反而是误报。
676
704
  if (res.status === 409 && res.body && res.body.code === 'change_deleted') {
677
- console.warn(`⚠️ [sync] 平台已删除变更「${changeName}」,本次进度上行被拒收(change_deleted);本地如仍需推进请在平台侧确认`);
705
+ // 预期回执分流(2026-09-10 驾驭小结第四批③,用户实锤「提示语义滞后一个动作」):本地 DB
706
+ // status 已是 archived/deleted(unregisterChange 链——CLI 归档/change-delete 收尾自己置的)
707
+ // 时,本次上行本就是 CLI 主动发的墓碑,409 是平台「已删」幂等回执——本地注销早已完成,
708
+ // 「请在平台侧确认」口吻滞后一个动作;降为 ℹ️ 单行。仅本地仍 active(他端删除)才 warn。
709
+ const localTombstone = tombstoneDue
710
+ || progressData?.changes?.[0]?.status === 'archived'
711
+ || progressData?.changes?.[0]?.status === 'deleted'
712
+ if (localTombstone) {
713
+ console.log(`ℹ️ [sync] 平台已删除变更「${changeName}」——本地注销已由 CLI 完成,墓碑上行 409 属预期回执,无需动作`);
714
+ } else {
715
+ console.warn(`⚠️ [sync] 平台已删除变更「${changeName}」,本次进度上行被拒收(change_deleted);本地如仍需推进请在平台侧确认`);
716
+ }
678
717
  return { synced: 0, errors: [], platformDeleted: true, reason: '平台已删除(change_deleted 拒收)' };
679
718
  }
680
719
 
@@ -1075,10 +1114,12 @@ export class SyncManager {
1075
1114
  /**
1076
1115
  * 获取当前平台配置,未连接返回 null。
1077
1116
  * 优先级:env SILLYHUB_PLATFORM_URL + SILLYHUB_PLATFORM_TOKEN(两键齐全才生效——
1078
- * daemon 注入通道,平台模式 specRoot/local.yaml 常无 platform 段,与链路 D
1079
- * readPlatformPushConfig 同款先例)> local.yaml platform 段。
1080
- * env 由 daemon 注入、CLI 不管其生命周期(disconnect 仍只清 local.yaml——daemon
1081
- * 停止注入即失效);triggerSync 平台模式放行(2026-08-26)后这是回传走通的关键通道。
1117
+ * 预留的外部注入通道,与链路 D readPlatformPushConfig 同款先例。⚠️ daemon 并未实现
1118
+ * 该注入(2026-09-09 核对全仓历史,见 multi-agent-platform 仓
1119
+ * docs/sillyspec/init-lease-silent-no-local-yaml.md)——env 需用户/脚本在 shell 显式
1120
+ * export,别按「daemon 会注」排障)> local.yaml platform 段。
1121
+ * env 生命周期 CLI 不管(disconnect 仍只清 local.yaml);triggerSync 平台模式放行
1122
+ * (2026-08-26)后平台模式回传的主凭据实为 local.yaml platform 段(env 缺席时)。
1082
1123
  */
1083
1124
  _getPlatform() {
1084
1125
  if (process.env.SILLYHUB_PLATFORM_URL && process.env.SILLYHUB_PLATFORM_TOKEN) {
@@ -781,25 +781,83 @@ export function writeVerifyRequiredEvidence(changeDir, requiredEvidence) {
781
781
  }
782
782
 
783
783
  /**
784
- * 生成 execute run id
785
- * @returns {string} 如 'exec-2026-06-23-131400'
784
+ * 生成 execute run id。
785
+ * changeName 给定时附 change 名 FNV-1a 哈希后缀(坑 exec-run-id-same-second-collision
786
+ * 结构化隔离层,2026-09-10 驾驭小结第三批①,用户建议「run 目录取 change 名哈希隔离」):
787
+ * 两变更同秒启动 execute 必得不同 runId(run 目录天然分家),与 claimExecuteRunId 的
788
+ * 排他认领互补——认领治「同 ID 两主」,哈希治「不同变更天生同 ID」。同变更重试(marker
789
+ * 断裂 regenerate)哈希恒同,幂等;无参调用保持裸秒级形态(向后兼容/测试注入)。
790
+ * @param {string|null} [changeName] 变更名(缺省=裸秒级形态,无哈希后缀)
791
+ * @returns {string} 如 'exec-2026-06-23-131400' / 'exec-2026-06-23-131400-3fa2c1'
786
792
  */
787
- export function generateExecuteRunId() {
793
+ export function generateExecuteRunId(changeName = null) {
788
794
  const now = new Date()
789
795
  const pad = (n) => String(n).padStart(2, '0')
790
- return `exec-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
796
+ const base = `exec-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
797
+ if (!changeName) return base
798
+ return `${base}-${changeNameHash(changeName)}`
799
+ }
800
+
801
+ /** change 名 → 6 位小写十六进制哈希(FNV-1a 32bit 截断;确定性、字符集落在 runId 后缀合法域 [a-z0-9]) */
802
+ function changeNameHash(name) {
803
+ let h = 0x811c9dc5
804
+ for (let i = 0; i < name.length; i++) {
805
+ h ^= name.charCodeAt(i)
806
+ h = Math.imul(h, 0x01000193) >>> 0
807
+ }
808
+ return h.toString(16).padStart(6, '0').slice(0, 6)
809
+ }
810
+
811
+ /**
812
+ * 排他认领 execute run 目录(坑 exec-run-id-same-second-collision,2026-09-10 用户两次实锤):
813
+ * generateExecuteRunId 是秒级时间戳,并行会话(不同 change)同秒启动 execute 会生成同一
814
+ * runId——两变更的 tasks/task-NN/review.json 落进同一 execute-runs/<runId>/ 互相覆盖。
815
+ * 认领靠非递归 mkdir 的原子性:目录已存在(EEXIST)= 他者已认领同秒 ID → 附随机短后缀
816
+ * 重试;真实 fs 障碍(execute-runs 被普通文件占用等)原样上抛,由四处写入点既有分层 fail
817
+ * 语义接管(stage throw / gates 直穿 fail-closed / prompt+task-review catch 降级,
818
+ * execute-run-dir-fail-loud.test.mjs 锁定)。认领成功即含 tasks/ 子目录(不变量:认领则在,
819
+ * 后续 marker 写入点 mkdir 为幂等 no-op)。
820
+ * @param {string} runtimeRoot .sillyspec/.runtime 绝对路径
821
+ * @param {string|null} [baseId] 认领基准 ID(须为裸秒级形态;缺省现取 generateExecuteRunId()。
822
+ * 测试注入定值以确定性模拟「同秒两会话」,消除跨秒抖动)
823
+ * @returns {string} 已认领的 runId(无碰撞=原样秒级形态;碰撞=带 -<rand4> 后缀)
824
+ */
825
+ export function claimExecuteRunId(runtimeRoot, baseId = null) {
826
+ const base = baseId || generateExecuteRunId()
827
+ mkdirSync(join(runtimeRoot, 'execute-runs'), { recursive: true })
828
+ for (let attempt = 0; attempt < 8; attempt++) {
829
+ const candidate = attempt === 0 ? base : `${base}-${randomRunIdSuffix()}`
830
+ try {
831
+ mkdirSync(join(runtimeRoot, 'execute-runs', candidate)) // 非递归=排他:EEXIST 即同秒碰撞
832
+ mkdirSync(join(runtimeRoot, 'execute-runs', candidate, 'tasks'), { recursive: true })
833
+ return candidate
834
+ } catch (e) {
835
+ if (e.code !== 'EEXIST') throw e
836
+ }
837
+ }
838
+ // 极端拥挤兜底:放弃排他返回带后缀 ID(调用方 marker 写入点的 recursive mkdir 兜住;
839
+ // 同秒 8 次随机后缀连续碰撞的概率可忽略,行为不劣于修复前)
840
+ return `${base}-${randomRunIdSuffix()}`
841
+ }
842
+
843
+ function randomRunIdSuffix() {
844
+ return Math.random().toString(36).slice(2, 6).padEnd(4, '0')
791
845
  }
792
846
 
793
847
  /**
794
- * execute run id 格式校验:exec-YYYY-MM-DD-HHMMSS。
848
+ * execute run id 格式校验:exec-YYYY-MM-DD-HHMMSS(可选 1-2 段小写字母数字短后缀)。
795
849
  * marker 文件是 agent 可写内容,读回后既注入 prompt({EXECUTE_RUN_ID})又拼进
796
850
  * join(runtimeRoot, 'execute-runs', runId) 路径——格式校验同时防提示词注入与路径穿越
797
851
  * (对齐 stage-review marker 的 review- 前缀校验范式)。
852
+ * 后缀两段语义:①change 哈希段(generateExecuteRunId(changeName),坑
853
+ * exec-run-id-same-second-collision 结构化隔离);②claim 碰撞随机段(claimExecuteRunId
854
+ * 同秒认领冲突换道)。存量无后缀 ID(历史 run)继续通过,读侧全形态兼容。
798
855
  * @param {string} runId
799
856
  * @returns {boolean}
800
857
  */
801
858
  export function isValidExecuteRunId(runId) {
802
- return typeof runId === 'string' && /^exec-\d{4}-\d{2}-\d{2}-\d{6}$/.test(runId)
859
+ return typeof runId === 'string'
860
+ && /^exec-\d{4}-\d{2}-\d{2}-\d{6}(?:-[a-z0-9]{1,8}){0,2}$/.test(runId)
803
861
  }
804
862
 
805
863
  /**
@@ -838,8 +896,10 @@ export function resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName = n
838
896
  .sort((a, b) => b.mtime - a.mtime)
839
897
  if (entries.length === 0) return null
840
898
  // changeName 给定时优先 change 戳等值的 run(坑 worktree-cleanup-marker-chain:mtime 最新
841
- // 会错拿其他变更的 run);无戳命中退回 mtime 最新(向后兼容,调用方多为 marker 漂移兜底,
842
- // 宁可拿最新 run 也不空手而归——覆盖度由调用方 validateTaskReviews 复校验)
899
+ // 会错拿其他变更的 run);无戳命中退 mtime 最新——但排除「戳属他变更」的有主 run(坑
900
+ // exec-run-id-same-second-collision 收尾,2026-09-10 驾驭小结第二批②:与
901
+ // resolveExecuteRunForChange / resolveLatestExecuteRunId 同语义,戳存在且不等值 = 有明确
902
+ // 主人,误拿即串台)。无 changeName(旧行为,调用方 marker 漂移兜底)纯 mtime 零回归。
843
903
  if (changeName) {
844
904
  for (const x of entries) {
845
905
  try {
@@ -847,6 +907,11 @@ export function resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName = n
847
907
  if (c === changeName) return x.e
848
908
  } catch {}
849
909
  }
910
+ const ownerless = entries.find(x => {
911
+ try { return !existsSync(join(runsDir, x.e, 'change')) } catch { return true }
912
+ })
913
+ if (ownerless) return ownerless.e
914
+ return null
850
915
  }
851
916
  return entries[0].e
852
917
  } catch {
@@ -865,11 +930,16 @@ export function resolveLatestExecuteRunId({ runtimeRoot, changeName }) {
865
930
  } catch {}
866
931
  // marker 缺失(worktree cleanup / 归档清理 / 并行会话误删——坑 worktree-cleanup-marker-chain)
867
932
  // 时不再盲目取 mtime 最新(会拿到其他变更的 run):先按 change 归属戳过滤,命中才返回;
868
- // 无戳(旧 run)退回 mtime 最新保持向后兼容。
933
+ // 无戳(旧 run)退 mtime 最新——但排除「戳属他变更」的有主 run(坑
934
+ // exec-run-id-same-second-collision 收尾,2026-09-10 驾驭小结第二批②:writeTaskReview 在
935
+ // marker 缺失场景靠本函数定位 run,误拿有主 run 即把本变更 review.json 写进他变更 run 的
936
+ // tasks/——串台残余入口。与 resolveExecuteRunForChange 的覆盖度启发排除同语义);全部 run
937
+ // 有主 → null(宁缺毋错,调用方走「无 run」分支而非错写)。
869
938
  try {
870
939
  const candidates = listExecuteRunCandidates(runtimeRoot, changeName)
871
940
  if (candidates.stamped.length > 0) return candidates.stamped[0]
872
- if (candidates.all.length > 0) return candidates.all[0]
941
+ const ownerless = candidates.all.find(runId => readExecuteRunChangeStamp(runtimeRoot, runId) === null)
942
+ if (ownerless) return ownerless
873
943
  } catch {}
874
944
  return null
875
945
  }
@@ -1054,11 +1124,13 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
1054
1124
  }
1055
1125
  } catch {}
1056
1126
  if (!executeRunId) {
1057
- executeRunId = generateExecuteRunId()
1127
+ executeRunId = generateExecuteRunId(changeName)
1058
1128
  // D-001#1 fallback 写入点:mkdir execute-runs/<runId>/tasks 先于 marker(不变量:marker 在则目录在)。
1059
1129
  // 保留 fail-open 契约(调用方 catch 降级)——只去静默:失败 console.error 留痕,不 throw
1060
1130
  //(草稿兜底缺了也只是退回 gate 报缺 review.json,不比修复前差)。
1061
1131
  try {
1132
+ // 排他认领(坑 exec-run-id-same-second-collision):并行会话同秒 runId 碰撞在认领处消解
1133
+ executeRunId = claimExecuteRunId(runtimeRoot, executeRunId)
1062
1134
  mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
1063
1135
  writeFileSync(runIdFile, executeRunId + '\n')
1064
1136
  stampExecuteRunChange(runtimeRoot, executeRunId, changeName)
@@ -1086,9 +1158,27 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
1086
1158
  const wtFiles = parsePorcelainFiles(wtStatus)
1087
1159
  .map(p => String(p).replace(/\\/g, '/'))
1088
1160
  .filter(p => p !== '.sillyspec' && !p.startsWith('.sillyspec/'))
1089
- if (wtFiles.length > 0) {
1090
- diffFiles = [...new Set([...(Array.isArray(diffFiles) ? diffFiles : []), ...wtFiles])]
1091
- console.log(`[sillyspec] 草稿归属并入 ${wtFiles.length} 个 worktree 未提交文件(子代理未 commit 的改动按 allowed_paths 归属)`)
1161
+ // 已提交口径补齐(2026-09-10 用户反馈②「先提交则 diff 空」:worktree 内已 commit 的改动
1162
+ // 对主仓 base..head 与 status 双不可见,草稿 changedFiles 恒空)——merge-base(主仓 HEAD,
1163
+ // worktree HEAD)(= 创建锚点)..worktree HEAD 的 commit diff 并入,与
1164
+ // resolveVerifyChangedFiles 的同款补齐同源(verify-postcheck.js,两处口径须同步改)。
1165
+ // in-place 退化(wtGitDir===cwd)时已提交改动在主仓 diff 内,跳过防双并;git 失败 fail-open。
1166
+ let committedFiles = []
1167
+ if (wtGitDir !== cwd) {
1168
+ try {
1169
+ const mainHead = runGit(cwd, ['rev-parse', 'HEAD'])
1170
+ const wtHead = runGit(wtGitDir, ['rev-parse', 'HEAD'])
1171
+ const mb = (mainHead && wtHead) ? runGit(wtGitDir, ['merge-base', wtHead, mainHead]) : null
1172
+ if (mb) {
1173
+ committedFiles = String(runGit(wtGitDir, ['diff', '--name-only', mb, wtHead]) || '')
1174
+ .split('\n').map(p => p.replace(/^"|"$/g, '').replace(/\\/g, '/').trim())
1175
+ .filter(p => p && p !== '.sillyspec' && !p.startsWith('.sillyspec/'))
1176
+ }
1177
+ } catch { /* 已提交补齐失败退回 status-only(fail-open,不拖垮未提交并入) */ }
1178
+ }
1179
+ if (wtFiles.length > 0 || committedFiles.length > 0) {
1180
+ diffFiles = [...new Set([...(Array.isArray(diffFiles) ? diffFiles : []), ...wtFiles, ...committedFiles])]
1181
+ console.log(`[sillyspec] 草稿归属并入 worktree 改动 ${wtFiles.length + committedFiles.length} 个(未提交 ${wtFiles.length} + 已提交 ${committedFiles.length},按 allowed_paths 归属)`)
1092
1182
  }
1093
1183
  }
1094
1184
  } catch { /* working-tree 并入失败退回 commit diff 口径(fail-open,不阻断草稿) */ }
@@ -1568,12 +1658,14 @@ export async function writeTaskReview({
1568
1658
  executeRunId = resolveLatestExecuteRunId({ runtimeRoot, changeName }) || ''
1569
1659
  }
1570
1660
  if (!executeRunId) {
1571
- executeRunId = generateExecuteRunId()
1572
- warnings.push(`无 execute run marker——已生成新 runId ${executeRunId} 并建目录(正常应先跑过 execute Wave 步)`)
1661
+ executeRunId = generateExecuteRunId(changeName)
1573
1662
  try {
1663
+ // 排他认领(坑 exec-run-id-same-second-collision)+ 幂等 mkdir,认领碰撞换后缀
1664
+ executeRunId = claimExecuteRunId(runtimeRoot, executeRunId)
1574
1665
  mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
1575
1666
  writeFileSync(markerFile, executeRunId + '\n')
1576
1667
  stampExecuteRunChange(runtimeRoot, executeRunId, changeName)
1668
+ warnings.push(`无 execute run marker——已生成新 runId ${executeRunId} 并建目录(正常应先跑过 execute Wave 步)`)
1577
1669
  } catch (e) {
1578
1670
  return { ok: false, errors: [`execute run 目录/marker 写入失败:${e.message}`], warnings }
1579
1671
  }
package/src/taskcard.js CHANGED
@@ -74,6 +74,7 @@ export function buildTaskcardSkeleton({ taskId, title, titleZh, author, now, dep
74
74
  title: yamlScalar(title),
75
75
  title_zh: yamlScalar(titleZh),
76
76
  author: yamlScalar(author),
77
+ generated_by: 'sillyspec-taskcard', // P2-e provenance 戳(CLI 骨架出品标记)
77
78
  created_at: now,
78
79
  priority: 'P0',
79
80
  depends_on: (Array.isArray(dependsOn) && dependsOn.length > 0)
@@ -93,6 +94,7 @@ id: ${fm.id}
93
94
  title: ${fm.title}
94
95
  title_zh: ${fm.title_zh}
95
96
  author: ${fm.author}
97
+ generated_by: ${fm.generated_by}
96
98
  created_at: ${fm.created_at}
97
99
  priority: ${fm.priority}
98
100
  depends_on: ${fm.depends_on}
@@ -232,6 +234,18 @@ export function cmdTaskcard(changeName, opts = {}) {
232
234
  // 本地墙钟(坑 taskcard-created-at-utc:toISOString 落 UTC,本地 09:39 写 01:39,子代理手工改两次)
233
235
  const now = nowWallClock()
234
236
 
237
+ // ── depends_on 的 plan.md Wave 反填(坑 taskcard-depends-wave-backfill,2026-09-10 驾驭
238
+ // 小结第五批③,三批子代理均发现骨架 depends_on 为空):注册表行内注解
239
+ // `(depends_on: task-01,02)` 是第一来源,但 agent 写 tasks.md 常漏注解 → 骨架全空 →
240
+ // plan-adopt-waves 拓扑退化全进 Wave 1。plan.md Wave 段(`## Wave N` + `- task-NN` 纯 ID
241
+ // 引用行)是 plan 阶段的分组真相——Wave N 任务 depends_on = Wave N-1 全部任务(波间串行
242
+ // 波内并行的既定语义,传递闭包等价)。仅注解缺失时兜底,注解在场优先(更细粒度)。
243
+ const waveDepsByTask = parsePlanWaveDeps(existsSync(planPath) ? planPath : null)
244
+ function depsFor(id, reg) {
245
+ if (reg && Array.isArray(reg.dependsOn) && reg.dependsOn.length > 0) return reg.dependsOn
246
+ return waveDepsByTask.get(id) || []
247
+ }
248
+
235
249
  const tasksDir = join(changeDir, 'tasks')
236
250
  mkdirSync(tasksDir, { recursive: true })
237
251
 
@@ -248,12 +262,41 @@ export function cmdTaskcard(changeName, opts = {}) {
248
262
  }
249
263
  writeFileSync(filePath, buildTaskcardSkeleton({
250
264
  taskId: id, title: finalTitle, titleZh: finalTitleZh, author, now,
251
- // depends_on 从 tasks.md 行内注解反填(坑 taskcard-design-field-conflicts):注册表
252
- // 注解与 plan/design 的依赖声明同源,骨架直接带上省主代理逐卡转录
253
- dependsOn: reg ? reg.dependsOn : [],
265
+ // depends_on 双来源:① tasks.md 行内注解反填(坑 taskcard-design-field-conflicts)
266
+ // ②注解缺失时按 plan.md Wave 分组兜底(坑 taskcard-depends-wave-backfill,见上)
267
+ dependsOn: depsFor(id, reg),
254
268
  sets,
255
269
  }), 'utf8')
256
270
  created.push(filePath)
257
271
  }
258
272
  return { created, skipped, tasksDir }
259
273
  }
274
+
275
+ /**
276
+ * plan.md Wave 段解析 → 每任务的跨波依赖(Wave N 任务 → Wave N-1 全部任务 ID)。
277
+ * Wave 段形态(plan 铁律):`## Wave N` 标题 + `- task-NN` 纯 ID 引用行。Wave 1 / 无 Wave /
278
+ * 解析异常 → 空 Map(调用方回退注解来源,fail-soft)。
279
+ * @param {string|null} planPath plan.md 绝对路径(null → 空 Map)
280
+ * @returns {Map<string, string[]>} taskId → 前一波任务 ID 列表
281
+ */
282
+ export function parsePlanWaveDeps(planPath) {
283
+ const out = new Map()
284
+ if (!planPath) return out
285
+ let content
286
+ try { content = readFileSync(planPath, 'utf8').replace(/\r\n/g, '\n') } catch { return out }
287
+ const waves = [] // 每元素:task id 数组(按 Wave 标题出现顺序)
288
+ let cur = null
289
+ for (const line of content.split('\n')) {
290
+ const w = line.match(/^##\s+Wave\s+(\d+)\s*$/i)
291
+ if (w) { cur = []; waves.push(cur); continue }
292
+ if (cur === null) continue
293
+ const t = line.match(/^[-*]\s+(task-\d{1,3})\s*$/i)
294
+ if (t) cur.push(`task-${t[1].replace(/^task-/i, '').padStart(2, '0')}`)
295
+ }
296
+ for (let w = 1; w < waves.length; w++) {
297
+ const prev = waves[w - 1]
298
+ if (!prev || prev.length === 0) continue
299
+ for (const id of waves[w]) out.set(id, [...prev])
300
+ }
301
+ return out
302
+ }