sillyspec 3.28.0 → 3.28.3

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-auto/SKILL.md +54 -93
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -47
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -214
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -270
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -87
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -70
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -64
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -175
  11. package/package.json +1 -1
  12. package/src/archive-delta.js +462 -0
  13. package/src/constants.js +6 -0
  14. package/src/design-facts.js +418 -0
  15. package/src/dispatch/backends/sillyhub-mcp.js +17 -5
  16. package/src/docs-check.js +197 -34
  17. package/src/docs-migrate.js +124 -0
  18. package/src/doctor-diagnostics.js +11 -2
  19. package/src/endpoint-baseline.js +109 -0
  20. package/src/git-helper.js +31 -0
  21. package/src/index.js +381 -22
  22. package/src/modules.js +41 -12
  23. package/src/progress/change-registry.js +39 -0
  24. package/src/progress/consistency-doctor.js +124 -5
  25. package/src/progress/stage-machine.js +13 -2
  26. package/src/progress.js +5 -0
  27. package/src/quicklog.js +51 -0
  28. package/src/run/command.js +199 -5
  29. package/src/run/complete-handlers.js +60 -4
  30. package/src/run/complete.js +105 -6
  31. package/src/run/gates.js +353 -2
  32. package/src/run/progress-confirm.js +66 -0
  33. package/src/run/prompt.js +193 -15
  34. package/src/run/quick-audit.js +12 -5
  35. package/src/run/scan-profile.js +117 -7
  36. package/src/run/shared.js +22 -6
  37. package/src/run/stage.js +14 -1
  38. package/src/scan-diff.js +54 -1
  39. package/src/scan-facts.js +173 -0
  40. package/src/scan-postcheck.js +114 -3
  41. package/src/spec-sync.js +19 -2
  42. package/src/stage-review.js +62 -3
  43. package/src/stages/archive.js +5 -5
  44. package/src/stages/brainstorm-auto.js +2 -1
  45. package/src/stages/brainstorm.js +12 -20
  46. package/src/stages/execute.js +19 -21
  47. package/src/stages/plan-postcheck.js +194 -1
  48. package/src/stages/plan.js +8 -3
  49. package/src/stages/quick.js +4 -2
  50. package/src/stages/scan.js +44 -102
  51. package/src/stages/verify.js +18 -18
  52. package/src/task-review.js +200 -0
  53. package/src/taskcard.js +8 -1
  54. package/src/verify-postcheck.js +771 -29
  55. package/src/verify-probes.js +89 -10
  56. package/src/workflow.js +30 -0
  57. package/templates/agents-instruction.md +1 -1
  58. package/templates/prompts/taskcard-rules.md +5 -0
  59. package/templates/workflows/scan-docs.yaml +6 -0
  60. package/.claude/skills/verify-per-user/SKILL.md +0 -6
@@ -7,23 +7,13 @@ export const definition = {
7
7
  name: '进度确认',
8
8
  // 重命名自「状态检查」(③ status 消歧):老进度里的「状态检查」completed 由迁移逻辑承接,防 currentIdx 回跳
9
9
  migratedFrom: ['状态检查'],
10
- prompt: `检查当前变更的进度状态(sillyspec.db)。用 \`sillyspec progress show\` 查流程进度,不要用 \`sillyspec status\`(项目级快照,不推进流程)。
11
-
12
- ### 操作
13
- 1. 运行 \`sillyspec progress show\`
14
- 2. 确认 currentStage 为 "brainstorm"
15
- 3. 如果有进行中的 brainstorm,提示选择继续或重新开始
16
- 4. 如果未初始化,提示先运行 sillyspec init
17
- 5. **检查变更名称是否有意义**:如果当前变更名是自动生成的(如 \`2026-06-02-new-change-a3f2b7c1\`),询问用户确认实际变更名,然后运行 \`sillyspec change-rename <旧名> <新名>\` 重命名
18
-
19
- ### 输出
20
- 当前状态摘要(1-2 句话)
21
-
22
- ### 注意
23
- - 以 CLI 返回为准,不要自行推断阶段
24
- - 如果阶段不对,输出正确提示并停止
25
- - **不要用 mv 命令重命名变更目录**,必须使用 \`sillyspec change-rename\`,否则 DB 和目录会脱节`,
26
- outputHint: '状态摘要',
10
+ // noAI 化(2026-09-07):快照本就 CLI 注入、阶段路由由 run 落定、变更名自动生成检测为机械
11
+ // 正则(命中只打印 rename 建议,agent 在工具结果可见、可在后续步骤交互改名)——省一轮
12
+ // 复述往返。步骤名保持不变(存量进度库按名匹配迁移)。
13
+ noAI: true,
14
+ _cliAction: 'progressConfirm',
15
+ prompt: '',
16
+ outputHint: '状态摘要(CLI 快照输出)',
27
17
  optional: false
28
18
  },
29
19
  {
@@ -33,8 +23,10 @@ export const definition = {
33
23
  ### 操作
34
24
  1. 读取项目总览 \`{SPEC_ROOT}/docs/<project>/scan/PROJECT.md\` + 共享规范 + 子项目上下文
35
25
  - 漂移事实(CLI 算):{SCAN_STALENESS}
26
+ - 机械事实底稿(CLI 注入,无 scan 底稿则为空):{SCAN_FACTS}
36
27
  2. 加载项目信息:\`cat {SPEC_ROOT}/projects/*.yaml 2>/dev/null\`
37
- 3. 加载本地配置:\`cat {SPEC_ROOT}/local.yaml 2>/dev/null\`
28
+ 3. 构建命令(CLI 自 local.yaml 注入,勿再读文件):
29
+ {LOCAL_COMMANDS}
38
30
  4. 棕地项目:读取 {SPEC_ROOT}/docs/<project>/scan/ 下的 STRUCTURE.md、CONVENTIONS.md、ARCHITECTURE.md
39
31
  5. **加载模块索引**:读取 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`(如存在)
40
32
  - 这一步是高频操作,_module-map.yaml 回答"哪个文件属于哪个模块、模块之间怎么依赖"
@@ -348,7 +340,7 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
348
340
  ### 操作
349
341
  1. 确认变更目录存在:\`mkdir -p {SPEC_ROOT}/changes/<change-name>\`(Windows 用 \`mkdir {SPEC_ROOT}/changes\\<变更名>\` 或 PowerShell \`New-Item -ItemType Directory -Force -Path {SPEC_ROOT}/changes/<change-name>\`)
350
342
  - 变更名格式必须为 \`YYYY-MM-DD-<简短描述>\`(如 \`2026-05-13-user-auth\`)
351
- 2. 将确认的设计写入 \`{SPEC_ROOT}/changes/<change-name>/design.md\`
343
+ 2. 将确认的设计写入 \`{SPEC_ROOT}/changes/<change-name>/design.md\`——优先跑 \`sillyspec design-init --change <change-name>\` 生成骨架再填散文(决策追踪表已按 decisions.md 当前版本预填,逐行补覆盖点后把「待确认」改「已覆盖」;文件清单表骨架就位);存量手写路径仍合法(design-init 对已存在的 design.md 不覆盖)
352
344
  3. 对账整理 \`{SPEC_ROOT}/changes/<change-name>/decisions.md\`(「对话式探索」「提出方案」步已按增量规则随答落盘,本步做对账而非从零写入):
353
345
  - decisions.md 是本次变更的决策台账,不是长期术语表
354
346
  - 只记录有实现/验收影响的决策,闲聊和低风险偏好不记录
@@ -358,7 +350,7 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
358
350
  - 每条记录必须包含:type、status、source、question、answer、normalized_requirement、impacts、evidence、priority
359
351
  - 九字段之外可另加四个可选字段,按需填写、不强制全填(旧格式决策记录缺这些字段不受影响):
360
352
  - 锚点:决策落点的主文件,格式 <src 路径>:<行号或符号>(status=confirmed 时必填)
361
- - 模块域:决策涉及的模块 ID,取自 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`,可多个、用逗号分隔
353
+ - 模块域:决策涉及的模块 ID,取自 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`,可多个、用逗号分隔;规划中的新模块用 NEW:模块名 前缀声明(冒号后不加空格——\`NEW:foo\` 合法、\`NEW: foo\` 属书写错误),「生成规范文件」步 --done 的模块域核验会豁免 NEW 前缀并在模块落地后补录
362
354
  - 否决理由:一句话说明为何否决(status=rejected 时必填)
363
355
  - 复潮条件:什么前提下可重新考虑该决策(status=rejected 时必填)
364
356
  - 长期术语只在 archive/scan 时再提升到 \`{SPEC_ROOT}/docs/<project>/glossary.md\`
@@ -220,17 +220,12 @@ const fixedPrefix = [
220
220
  {
221
221
  name: '进度确认',
222
222
  migratedFrom: ['状态检查'],
223
- prompt: `检查当前进度,确认可以执行 execute。用 \`sillyspec progress show\` 查流程进度,不要用 \`sillyspec status\`(项目级快照,不推进流程)。
224
-
225
- ### 操作
226
- 1. 运行 \`sillyspec progress show\`
227
- 2. 确认 currentStage 为 execute
228
- 3. 如果不是 → 检查是否有未完成的 tasks.md
229
- 4. 确认执行范围($ARGUMENTS 指定 wave/task 或全部)
230
-
231
- ### 输出
232
- 当前状态 + 执行范围确认`,
233
- outputHint: '当前状态 + 执行范围',
223
+ // noAI 化(2026-09-07):快照复述/阶段检查机械化(progressConfirm 补 tasks 勾选计数);
224
+ // 执行范围是会话层信息($ARGUMENTS 不经 CLI),以用户会话指定为准。步骤名不变。
225
+ noAI: true,
226
+ _cliAction: 'progressConfirm',
227
+ prompt: '',
228
+ outputHint: '当前状态 + 执行范围(CLI 快照输出)',
234
229
  optional: false
235
230
  },
236
231
  {
@@ -241,7 +236,8 @@ const fixedPrefix = [
241
236
  1. 读取 tasks.md(任务注册表与勾选唯一真相;plan.md 只提供 Wave 分组/依赖结构——Wave 段下为纯 ID 引用行)
242
237
  2. 读取 design.md(技术方案)
243
238
  3. 读取 CONVENTIONS.md、ARCHITECTURE.md
244
- 4. 读取 local.yaml(构建命令);若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
239
+ 4. 构建命令(CLI 自 local.yaml 注入,勿再读文件;未配置时按注入说明跑 local detect):
240
+ {LOCAL_COMMANDS}
245
241
  5. 加载项目总览 \`.sillyspec/docs/<project>/scan/PROJECT.md\`(如存在)
246
242
 
247
243
  ### 模块文档加载(细粒度卡优先——CLI 已按 tasks 卡 allowed_paths 级联匹配)
@@ -284,18 +280,19 @@ const fixedPrefix = [
284
280
  },
285
281
  {
286
282
  name: '确认 worktree 路径',
287
- prompt: `确认当前 worktree 状态,提取隔离路径。
283
+ prompt: `确认当前 worktree 状态(meta 已由 CLI 注入,勿再跑 worktree meta)。
284
+
285
+ ### worktree 快照(CLI 注入)
286
+ {WORKTREE_META}
288
287
 
289
288
  ### 操作
290
- 1. 运行 \`sillyspec worktree meta <change-name>\` 读取 meta.json
291
- 2. 从输出中提取 worktreePath、branch、mode 字段
292
- 3. 确认 worktree 目录存在(如果是 worktree/native-worktree 模式)
293
- 4. **确认工具链可用**:worktree 内项目工具链(lint/format/test 二进制,如 ruff / prettier / uv)可能不全——对本次会用到的工具先跑一次 \`--version\` 确认;缺失则按项目方式安装(Python 项目 \`uv tool install ruff\` / \`uv sync\`,Node 项目 node_modules 已由 CLI 链接主仓)。不要等到 commit 才发现二进制不在 PATH 被 hook 拦。**Python 项目注意**:worktree 自建 .venv 只含 pyproject 声明依赖,pytest 等 dev 工具可能缺失——优先在 worktree 内补装(\`uv sync --group dev\` / \`uv pip install pytest\`),**不要回退用主仓 venv 跑测试**(主仓 venv 加载的可能是主仓代码而非 worktree 代码,环境不一致会掩真 bug)
289
+ 1. **确认工具链可用**:worktree 内项目工具链(lint/format/test 二进制,如 ruff / prettier / uv)可能不全——对本次会用到的工具先跑一次 \`--version\` 确认;缺失则按项目方式安装(Python 项目 \`uv tool install ruff\` / \`uv sync\`,Node 项目 node_modules 已由 CLI 链接主仓)。不要等到 commit 才发现二进制不在 PATH 被 hook 拦。**Python 项目注意**:worktree 自建 .venv 只含 pyproject 声明依赖,pytest 等 dev 工具可能缺失——优先在 worktree 内补装(\`uv sync --group dev\` / \`uv pip install pytest\`),**不要回退用主仓 venv 跑测试**(主仓 venv 加载的可能是主仓代码而非 worktree 代码,环境不一致会掩真 bug)
290
+ 2. **涉及后端端点的变更拍基线**:跑 \`sillyspec endpoints baseline --change <change-name>\` 拍变更前基线(幂等,已拍过会跳过;在 worktree 内跑即可——CLI 自动把扫描根与落点锚定主仓)——归档时用于计算端点增删
294
291
 
295
292
  ### 铁律
296
293
  - **worktree 已由 CLI 在 execute 阶段启动时自动创建,不要自行创建或跳过**
297
- - **后续所有子代理的 cwd 必须设为该 worktree 路径**
298
- - 如果 meta.json 不存在(说明创建失败),停止并报错
294
+ - **后续所有子代理的 cwd 必须设为快照中的 worktreePath**
295
+ - 快照显示 meta 不可读(说明创建失败)→ 停止并报错
299
296
  - **不要自行检查 git dirty/uncommitted 状态来判断是否可以进入 worktree,CLI 已自动处理**
300
297
 
301
298
  ### 输出
@@ -354,7 +351,7 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
354
351
  3. 组装行为(全量测试/构建/启动通过——单 task 测试全绿 ≠ 组装正确)
355
352
 
356
353
  **产物唯一化(省重复消耗)**:本步逐项对照结论**只落盘一份**——直接写进 review.json 的 \`checklist\` 数组(item=设计要点/FR/决策,note=实现状态 ✅/⚠️/❌ + 偏差说明 + commit 锚点),reviewerNotes 写汇总。**不要**另写独立的 design-check.md 长文(同一份 design×diff 二次消费;2026-08-22 实测该重复一遍 ≈8 分钟全量重读)。
357
- **gate 重试修复**:review.json 落盘后若后续 gate 报 docHash 失配 / 路径错,跑 \`sillyspec register-stage-review --change <变更名> --stage execute --refresh-hash\` 一键重算修复——**不要重做审查**(重做=同一材料第三遍)。
354
+ **gate 重试修复**:review.json 落盘后若 design.md 又有改版,gate 会**自动机械重算 docHash 放行**(verdict/checklist 保留,结论是否仍适用于新文档需人工确认)——**不要重做审查**(重做=同一材料第三遍);主文档路径错/缺失不会被自动修复,按 gate 报错修正 reviewedFiles[0],或跑 \`sillyspec register-stage-review --change <变更名> --stage execute --refresh-hash\`。
358
355
 
359
356
  ### 操作
360
357
  1. 读取 design.md(技术方案)——按章节精准读(design-check 对照表可借 tasks 卡 §锚点定位),避免反复整读全文
@@ -376,7 +373,8 @@ review.json 已落盘(checklist=逐项核验表)+ checklist 摘要与偏差
376
373
  本步骤由当前 agent 执行,不需要启动独立子代理。
377
374
 
378
375
  ### 操作
379
- 1. 读取 local.yaml 获取构建和测试命令;若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
376
+ 1. 构建命令(CLI 自 local.yaml 注入,勿再读文件;未配置时按注入说明跑 local detect):
377
+ {LOCAL_COMMANDS}
380
378
  2. 运行测试套件(单元测试、集成测试)
381
379
  3. 运行 lint 检查 **+ 格式化**:凡变更涉及的源码,既跑 lint check 也跑 formatter(如 \`ruff format\` / \`prettier --write\` / \`black\`),不要只跑 check——只 check 不 format 会把格式问题留到 commit 时被 pre-commit hook 拦截
382
380
  4. 如果有测试失败 → 分析原因,标注是代码问题还是测试本身的问题
@@ -94,6 +94,68 @@ export function parseAllowedPaths(content) {
94
94
  return []
95
95
  }
96
96
 
97
+ /**
98
+ * 解析 task-NN.md frontmatter 的 target_files 字段(对账意图声明,design 总体方案 Wave 1 / X-10)。
99
+ *
100
+ * 与 parseAllowedPaths 同族的列表解析(inline [] + 顶格/缩进块列表双形态、CRLF 入口归一),
101
+ * 但语义相反——**严格模式**:allowed_paths 是 worktree 写入守卫的白名单(目录前缀 / glob /
102
+ * 引号皆合法、pathMatches 容差匹配);target_files 是 verify 对账(task-04 reconcileTargetFiles)
103
+ * 用的逐文件精确意图声明——白名单容差若用于对账,目录前缀会把整目录算成「预期」使 scope
104
+ * creep 检出恒空(D-002)。故此处不剥引号、不放行通配,非法形态记入 entry.invalid 交
105
+ * validateTargetFiles 报 ERROR。解析器纯函数无 IO、不 throw——依赖链已核实无环
106
+ * (verify-postcheck → plan-postcheck 单向),供对账侧跨文件 import 复用。
107
+ *
108
+ * 条目格式:`[NEW:]<仓根相对精确文件路径>`,NEW: 前缀 = 当前不存在的待建文件。
109
+ * 非列表形态(标量 / 未闭合)与 parseAllowedPaths 家族同口径视同未声明(missing),
110
+ * 走调用方的汇总 WARNING 信号,不在解析器内造第三种状态。
111
+ *
112
+ * @param {string} content - task 文件内容(同 parseAllowedPaths:frontmatter 自提取)
113
+ * @returns {{ entries: Array<{raw: string, isNew: boolean, path: string, invalid: string|null}>, missing: boolean }}
114
+ * raw = 条目原文(含 NEW: 前缀);path = 剥 NEW: 与 ./ 前缀、反斜杠归一正斜杠的路径;
115
+ * invalid = 非法形态原因(null = 合法);missing = 字段缺失或空列表(存量卡兼容口径)
116
+ */
117
+ export function parseTargetFiles(content) {
118
+ content = content.replace(/\r\n/g, '\n') // 同 parseAllowedPaths:入口归一,保护喂原始 CRLF 的外部调用方(task-04)
119
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
120
+ if (!fmMatch) return { entries: [], missing: true }
121
+ const fm = fmMatch[1]
122
+ let raws = null
123
+ const inlineMatch = fm.match(/target_files:[ \t]*\[([^\]]*)\]/)
124
+ if (inlineMatch) {
125
+ raws = inlineMatch[1].split(',').map(s => s.trim()).filter(Boolean)
126
+ } else {
127
+ // 块列表 [ \t]*\n 不吃换行(坑6① 家族教训:\s* 贪婪吞顶格列表导致静默判空)
128
+ const blockMatch = fm.match(/target_files:[ \t]*\n((?:[ \t]*-[ \t]+.+\n?)+)/)
129
+ if (blockMatch) {
130
+ raws = blockMatch[1].match(/[ \t]*-[ \t]+(.+)/g)
131
+ ?.map(s => s.replace(/^[ \t]*-[ \t]+/, '').trim()).filter(Boolean) || []
132
+ }
133
+ }
134
+ if (raws === null || raws.length === 0) return { entries: [], missing: true }
135
+
136
+ const entries = raws.map(raw => {
137
+ let path = raw
138
+ let isNew = false
139
+ if (path.startsWith('NEW:')) { isNew = true; path = path.slice('NEW:'.length).trim() }
140
+ // 路径归一正斜杠(design 数据流标注)——UNC `\\srv\x` 归一后 `//srv/x` 恰落入下方绝对路径判定
141
+ path = path.replace(/\\/g, '/')
142
+ // 剥 ./ 前缀(P1 修复,2026-09-07):声明 `./src/a.js` vs actual `src/a.js` 曾字面失配落②类假红
143
+ // (normalizeReconcilePath 只归一 actual 侧)——此处的单源归一同时覆盖 plan 校验与 verify 对账。
144
+ // (\./)+ 贪婪多层:`././src/a.js` 双前缀也归一到层底(对抗复审 Q-1,actual 侧 git 路径永远干净)。
145
+ path = path.replace(/^(\.\/)+/, '')
146
+ // 严格形态判定(X-10:每条 = 精确文件路径,禁 glob/目录前缀/引号/绝对路径)。
147
+ // 只记原因不 throw:解析器供对账复用,非法条目的处置(ERROR)归 plan-postcheck 校验层
148
+ let invalid = null
149
+ if (path === '') invalid = '剥 NEW: / ./ 前缀后为空'
150
+ else if (/[*?]/.test(path)) invalid = '含通配符(* 或 ?)——target_files 须逐文件精确声明,禁 glob(目录级覆盖会令 verify 对账的 scope creep 检出失效)'
151
+ else if (path.endsWith('/')) invalid = '以 / 结尾(目录前缀)——须精确到文件'
152
+ else if (/["'`]/.test(path)) invalid = '含引号/反引号——须裸路径书写(严格口径拒容差,防对账字面不匹配)'
153
+ else if (path.startsWith('/') || /^[A-Za-z]:/.test(path)) invalid = '是绝对路径——须写仓根相对路径'
154
+ return { raw, isNew, path, invalid }
155
+ })
156
+ return { entries, missing: false }
157
+ }
158
+
97
159
  /**
98
160
  * 从 task-NN.md frontmatter 解析标量字段(repo / base_commit / head_commit)。
99
161
  * 复用 parseTaskContracts 的 frontmatter 提取 + js-yaml load 模式(标量可选字段,正则对引号/空值脆弱)。
@@ -985,6 +1047,122 @@ export function validateDesignFileCoverage(changeDir) {
985
1047
  return { ok: errors.length === 0, errors, warnings, designFiles, uncovered }
986
1048
  }
987
1049
 
1050
+ /**
1051
+ * 流程产物前缀(target_files 不该声明的路径域)。
1052
+ * 与 worktree-apply filterDeliverableFiles 同口径硬编码——不 import 复用是因为
1053
+ * worktree-apply → plan-postcheck 已有依赖边(line 21 parseAllowedPaths),反向 import 成环。
1054
+ * 注意 .sillyspec/docs/ 不在此列:dogfood 模块文档 = 交付物(D-2 keepSillyspecDocs),
1055
+ * 声明它是合法的(verify 对账也保留 docs 路径)。
1056
+ */
1057
+ const TARGET_FILES_PROCESS_PREFIXES = ['.sillyspec/changes/', '.sillyspec/.runtime/', '.sillyspec/quicklog/']
1058
+
1059
+ /**
1060
+ * task 卡 target_files 声明核验器(plan-postcheck 第 7 项检查,P3a Wave 1 / X-10 / X-13)。
1061
+ *
1062
+ * 修复场景(design 背景):plan 侧「计划动了不存在的文件」类幻觉此前零核验,agent 写出
1063
+ * 幻觉路径要到 execute 子代理落盘失败才暴露。此处逐条机器核验意图声明,把幻觉/格式问题
1064
+ * 拦在 plan --done;同时为 task-04 verify 对账提供可信的声明侧(对账复用 parseTargetFiles)。
1065
+ *
1066
+ * 分级口径:
1067
+ * ERROR:格式非法(glob/目录前缀/引号/绝对路径——parseTargetFiles.invalid);幻觉路径
1068
+ * (repoRoot 下不存在且未加 NEW: 前缀)。
1069
+ * WARNING(不阻断,存量兼容):字段缺失**每变更汇总一条**(X-13:每卡一条会被存量卡
1070
+ * 噪音淹没);已存在文件带 NEW: 前缀(应去前缀);流程产物路径;越出该卡
1071
+ * allowed_paths 白名单(提前暴露到 apply Gate1 才发现的越权,X-5);design 清单外
1072
+ * 路径(声明与 design 漂移提示);跨仓卡剔除提示(路径相对其声明仓根,主仓核验
1073
+ * 恒假误报,与对账侧 D-004 同口径剔除)。
1074
+ *
1075
+ * fail-open 边界:tasks/ 目录缺失或无卡(同既有检查行为);design.md 缺失/无清单章节
1076
+ * (缺清单 error 归 validateDesignFileCoverage 把关,不重复);卡无 allowed_paths
1077
+ * (feasibility 已报 error,跳过交叉防双重噪音);repoRoot 缺省(纯函数测试场景)跳过存在性。
1078
+ *
1079
+ * @param {string} changeDir - 变更目录(读 tasks/ 卡与 design.md 清单)
1080
+ * @param {string|null} [repoRoot=null] - 主仓根目录(target_files 存在性基准)
1081
+ * @returns {{ ok: boolean, errors: string[], warnings: string[] }}
1082
+ */
1083
+ export function validateTargetFiles(changeDir, repoRoot = null) {
1084
+ const errors = []
1085
+ const warnings = []
1086
+
1087
+ const tasksDir = pJoin(changeDir, 'tasks')
1088
+ if (!existsSync(tasksDir)) {
1089
+ return { ok: true, errors, warnings }
1090
+ }
1091
+ const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f))
1092
+ if (taskFiles.length === 0) {
1093
+ return { ok: true, errors, warnings }
1094
+ }
1095
+
1096
+ // design 清单交叉用清单(双交叉之一):与 validateDesignFileCoverage 同源解析(含按仓分段、
1097
+ // .sillyspec/docs 交付物口径),design 缺失/无清单 → 空列表 → 交叉自动跳过。检查间不共享
1098
+ // 解析结果(各检查独立只读 changeDir 产物),重复解析是聚合架构的既定代价。
1099
+ const designFiles = parseDesignCoverageByRepo(pJoin(changeDir, 'design.md')).allFiles
1100
+
1101
+ let missingCount = 0
1102
+ for (const file of taskFiles) {
1103
+ const content = readFileSync(pJoin(tasksDir, file), 'utf8')
1104
+ if (!/^---\n([\s\S]*?)\n---/.test(content)) continue // feasibility 已报 frontmatter 缺失,不重复
1105
+ const taskId = parseTaskId(content, file) || file
1106
+ const label = `${taskId} (${file})`
1107
+
1108
+ // 跨仓卡剔除:其 target_files 相对声明仓根,主仓 repoRoot 下 existsSync 恒假 → 全员
1109
+ // 误报幻觉路径。verify 对账(task-04)同口径剔除(D-004),此处只提示不核验
1110
+ const repo = parseRepo(content)
1111
+ if (repo) {
1112
+ warnings.push(`${label}: 跨仓卡(repo: ${repo})不参与主仓 target_files 核验——跨仓对账不在本变更范围,声明留待后续分期`)
1113
+ continue
1114
+ }
1115
+
1116
+ const { entries, missing } = parseTargetFiles(content)
1117
+ if (missing) { missingCount++; continue }
1118
+
1119
+ const allowedPaths = parseAllowedPaths(content)
1120
+ for (const e of entries) {
1121
+ if (e.invalid) {
1122
+ errors.push(`${label}: target_files「${e.raw}」格式非法——${e.invalid}。修复:改写为仓根相对的精确文件路径;当前不存在的文件加 NEW: 前缀(如 NEW:src/new.js)`)
1123
+ continue
1124
+ }
1125
+ // 流程产物:声明了也对不上账(verify 对账 filterDeliverableFiles 同口径会滤掉),
1126
+ // 且 changes/ 下路径属流程自产文件、不该是对账意图。提示后跳过后续核验(存在性/交叉
1127
+ // 对流程产物无意义,只会叠加噪音)
1128
+ if (TARGET_FILES_PROCESS_PREFIXES.some(p => e.path.startsWith(p)) || e.path === 'meta.json') {
1129
+ warnings.push(`${label}: target_files「${e.raw}」是流程产物路径——流程产物不进 target_files 声明(verify 对账同口径会将其滤掉,声明了也对不上账)`)
1130
+ continue
1131
+ }
1132
+ // 存在性:非 NEW 且不存在 = 幻觉路径(ERROR,本检查核心价值);NEW 且已存在 = 前缀误用(WARNING)
1133
+ if (repoRoot) {
1134
+ const fileExists = existsSync(pJoin(repoRoot, e.path))
1135
+ if (!e.isNew && !fileExists) {
1136
+ errors.push(`${label}: target_files「${e.raw}」在仓库中不存在且未加 NEW: 前缀——幻觉路径。修复:核对真实路径;确属新建文件则写 NEW:${e.path}`)
1137
+ continue // 路径本身不成立,allowed_paths/design 交叉对它无意义
1138
+ }
1139
+ if (e.isNew && fileExists) {
1140
+ warnings.push(`${label}: target_files「${e.raw}」已存在于仓库——NEW: 前缀仅用于当前不存在的文件,应去前缀`)
1141
+ }
1142
+ }
1143
+ // allowed_paths 交叉(双交叉之二,X-5):声明文件不在该卡白名单 → execute worktree
1144
+ // 写入守卫(apply Gate1)才暴露越权,此处提前到 plan 提示补白名单(容差匹配:目录
1145
+ // 前缀/glob 的 allowed_paths 可覆盖精确 target_file,与 coverage 对账同 pathMatches 语义)
1146
+ if (allowedPaths.length > 0 && !allowedPaths.some(ap => pathMatches(e.path, ap))) {
1147
+ warnings.push(`${label}: target_files「${e.path}」不在本卡 allowed_paths 白名单内——execute 写入守卫会拦(apply Gate1 才暴露越权)。修复:补进 allowed_paths(目录前缀/glob 写法均可)或修正 target_files`)
1148
+ }
1149
+ // design 清单交叉:声明与 design 漂移提示(design 清单是覆盖对账的基准,target_files
1150
+ // 声明清单外文件 = 两份意图清单分叉的早期信号)
1151
+ if (designFiles.length > 0 && !designFiles.some(dp => pathMatches(e.path, dp))) {
1152
+ warnings.push(`${label}: target_files「${e.path}」不在 design.md 文件变更清单内——请对齐(补 design 清单条目或修正声明),防声明与 design 漂移`)
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ // 字段缺失汇总(X-13):每变更一条而非每卡一条——存量卡大量无此字段,每卡一条 WARNING
1158
+ // 会淹没真正有价值的交叉警告;汇总粒度保信号可见又不阻断存量变更
1159
+ if (missingCount > 0) {
1160
+ warnings.push(`${missingCount} 张 task 卡未声明 target_files(存量卡可忽略——verify 对账将跳过;新卡请补:每条为仓根相对精确文件路径,不存在的文件加 NEW: 前缀)`)
1161
+ }
1162
+
1163
+ return { ok: errors.length === 0, errors, warnings }
1164
+ }
1165
+
988
1166
  /**
989
1167
  * Plan 可行性校验器(本地代码证明 execute 前置条件)
990
1168
  * 检查 TaskCard 的完整性和可行性
@@ -1236,7 +1414,7 @@ export async function executePlanPostcheck(context) {
1236
1414
 
1237
1415
  console.log(` 📂 变更目录: ${changeDir}`)
1238
1416
 
1239
- // ── 报错聚合(坑6③):六个检查全部跑完后统一输出全部失败项,一轮 --done 暴露全部问题,
1417
+ // ── 报错聚合(坑6③):七个检查全部跑完后统一输出全部失败项,一轮 --done 暴露全部问题,
1240
1418
  // 不再「失败一个抛一个、修一个冒一个」的迭代盲盒。各检查相互独立(都只读 changeDir 产物),
1241
1419
  // 前置检查失败不使后续检查失真——唯一例外是 tasks/ 目录缺失时后续检查都无意义,仍提前短路。
1242
1420
  const failures = [] // { name, errors, hint }
@@ -1333,6 +1511,21 @@ export async function executePlanPostcheck(context) {
1333
1511
  }
1334
1512
  }
1335
1513
 
1514
+ // ── 1f. task 卡 target_files 声明核验(P3a Wave 1)──
1515
+ // 对账意图声明的 plan 期机器核验:幻觉路径 / 非法格式 → error 硬拦(消灭「计划动不存在
1516
+ // 文件」的幻觉出口);存量卡缺字段仅每变更一条汇总 WARNING(X-13,不阻断零红门禁)。
1517
+ // 核验通过 = task-04 verify 对账的声明侧可信(对账 import parseTargetFiles 同源解析)。
1518
+ // 追加在既有 6 项之后,不触碰其语义与顺序;本检查同样独立、只读 changeDir 产物。
1519
+ const targetFiles = validateTargetFiles(changeDir, context.cwd)
1520
+ if (targetFiles.errors.length > 0) {
1521
+ failures.push({
1522
+ name: 'task 卡 target_files 声明核验(幻觉路径/非法格式)',
1523
+ errors: targetFiles.errors,
1524
+ hint: '修复方式:核对仓库真实路径——已存在文件裸写、新建文件加 NEW: 前缀;每条须为仓根相对的精确文件路径(禁 glob/目录前缀/引号/绝对路径)。',
1525
+ })
1526
+ }
1527
+ printSectionWarnings('target_files 声明', targetFiles.warnings)
1528
+
1336
1529
  // ── 聚合输出:一轮 --done 暴露全部失败项(坑6③)──
1337
1530
  if (failures.length > 0) {
1338
1531
  console.error(`\n❌ plan postcheck 失败(${failures.length} 类问题,已全部列出——一次修复后重跑,无需逐个迭代):`)
@@ -63,7 +63,8 @@ const stepClassify = {
63
63
  - 如果发现 priority=P0/P1 且 status=unresolved/blocking 的决策,停止生成计划,要求先回到 brainstorm 的 Design Grill 修正
64
64
  - 如果发现 superseded 决策,只引用最新版本,不引用旧版本
65
65
  5. 读取 CONVENTIONS.md、ARCHITECTURE.md(技术栈含在 ARCHITECTURE.md)
66
- 6. 读取 local.yaml 获取构建/测试命令
66
+ 6. 构建命令(CLI 自 local.yaml 注入,勿再读文件):
67
+ {LOCAL_COMMANDS}
67
68
  7. 读取 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`(不存在则跳过)
68
69
  - 根据 design.md 的文件变更清单匹配模块
69
70
  - 读取匹配到的模块文档
@@ -134,7 +135,7 @@ const stepGeneratePlan = {
134
135
  1. 读取 plan.md frontmatter 的 \`plan_level:\` 字段(上一步已落盘为持久锚点;文件不存在或无该字段时回退读上一步输出的分类结果)
135
136
  2. 读取 tasks.md 和 design.md 了解需求范围
136
137
  3. 按 plan_level 选择对应模板输出
137
- 4. **写回任务清单**:把展开后的任务清单写回 tasks.md(checkbox 行 \`- [ ] task-XX: 一句话任务名\`,可附 \`[model:xxx]\`/\`(depends_on: task-01,02)\` 行内标注)。**写回规则(D-002@v1)**:保留 frontmatter/中文标题/所有非 task-XX 行(quick 挂载的 ql-xxx 勾选行、注记等逐行保留),仅重写 task-XX checkbox 行集合——防摧毁 quick 挂载条目
138
+ 4. **写回任务清单**:把展开后的任务清单写回 tasks.md(checkbox 行 \`- [ ] task-XX: 一句话任务名\`,可附 \`[model:xxx]\`/\`(depends_on: task-01,02)\` 行内标注)。**写回规则(D-002@v1)**:保留 frontmatter/中文标题/所有非 task-XX 行(quick 挂载的 ql-xxx 勾选行、注记等逐行保留),仅重写 task-XX checkbox 行集合——防摧毁 quick 挂载条目。**展开任务时为每个 task 确定 target_files**(该 task 计划要改动的文件清单,随下一步「生成 TaskCard」按统一格式落进卡 frontmatter 的 target_files 字段):每项为精确仓根相对路径(✅ \`src/foo.js\`),计划新建的文件加 \`NEW:\` 前缀(✅ \`NEW:src/bar.js\`);❌ 禁 glob(\`src/**\`)/目录前缀(\`src/dir/\`)/绝对路径。target_files 与 allowed_paths 语义不同:前者=计划要动的具体文件,后者=权限范围
138
139
  5. 保存 plan.md(审查在下一步"审查计划"独立进行,不在本步自审——避免生成与自审同一次输出);frontmatter 保留 plan_level 字段不删。**plan.md Wave 段下任务一律纯 ID 引用行**(\`- task-XX\`,不重抄任务名——任务名唯一真相在 tasks.md,重抄即双写漂移)
139
140
 
140
141
  > ⚠️ **Wave 段格式铁律(踩坑:旧先例 \`- [ ] task-XX: 描述\` 格式会导致 postcheck 告警):**
@@ -416,6 +417,8 @@ decision_ids: [D-XXX@vN]
416
417
  repo: <repo-key> # 可选。仅跨仓 task 填:local.yaml repos: 注册的仓 key(缺省=main,主仓 task 省略此行)
417
418
  allowed_paths: # ⚠️ 路径相对 repo 声明的仓根(跨仓=跨仓仓根,主仓=主仓根),禁止带仓库名前缀/绝对路径
418
419
  - frontend/src/lib/errors.ts
420
+ target_files: # 可选。本 task 计划要改动的文件清单(文件级意图,供对账;与 allowed_paths 语义不同——那是权限范围)。骨架已预置 [] 占位行,无明确意图保留 [] 不动
421
+ - src/foo.js # ✅ 精确仓根相对路径(口径同 allowed_paths);计划新建文件加 NEW: 前缀(NEW:src/bar.js)。❌ 禁 glob(src/**)/目录前缀(src/dir/)/绝对路径
419
422
  provides: # 可选。仅当本 task 给其他 task 提供接口/DTO/响应时填
420
423
  - contract: <DTO或响应类型名> # 如 DaemonRuntimeRead
421
424
  fields: [field_a, field_b]
@@ -454,6 +457,8 @@ related_tests: # 可选。当本 task 改动会导致
454
457
 
455
458
  ⚠️ **TaskCard 必备字段(缺一 postcheck 直接阻断):** id、title(英文)、**title_zh(中文标题,必填)**、allowed_paths、goal、implementation、acceptance、verify、constraints。骨架已由 taskcard CLI 预生成(含 title_zh 占位),子代理只需 Edit 填值,不要删除任何必备字段。
456
459
 
460
+ 可选字段 **target_files**(本 task 计划要改动的文件清单;骨架已预置 \`target_files: []\` 占位行,按任务计划 Edit 填值,无明确文件级意图保留 []):✅ 精确仓根相对路径 \`src/foo.js\`、计划新建文件 \`NEW:src/bar.js\`;❌ \`src/**\`(glob)、\`src/dir/\`(目录前缀)、绝对路径。语义区分:target_files=计划要动的具体文件,allowed_paths=权限范围。
461
+
457
462
  ## 任务清单
458
463
  ${taskList}
459
464
 
@@ -501,7 +506,7 @@ TaskCard 的契约字段全部在 **frontmatter**(首对 --- 包裹的 YAML
501
506
  1. 读取 ${changeDir}/design.md 和 ${changeDir}/plan.md 了解整体上下文
502
507
  2. 读取本 batch 涉及的相关源文件
503
508
  3. **骨架已由主 agent 预生成**(\`${changeDir}/tasks/task-NN.md\` 已存在,LF 行尾 + 闭合 frontmatter + 硬校验 9 字段齐全)。**禁止再运行 \`sillyspec taskcard\` CLI**——并行子代理各起 CLI 进程会撞进度库 SQLite 锁(2026-08-25 实证);若发现本 batch 某卡骨架缺失,报告主 agent 补跑,不要自己跑
504
- 4. 用 Edit tool 逐卡填充骨架占位符(allowed_paths/goal/implementation/acceptance/verify/constraints 等)。**禁止用 Write 整文件重写**——手写整卡是 CRLF 行尾/漏闭合 --- /漏硬校验字段三类 postcheck 拒绝的根源,骨架 + Edit 从源头消灭
509
+ 4. 用 Edit tool 逐卡填充骨架占位符(allowed_paths/target_files/goal/implementation/acceptance/verify/constraints 等)。**禁止用 Write 整文件重写**——手写整卡是 CRLF 行尾/漏闭合 --- /漏硬校验字段三类 postcheck 拒绝的根源,骨架 + Edit 从源头消灭
505
510
  5. 骨架字段含义与可选字段(provides/expects_from/related_tests,按需插进 frontmatter)参考下述模板:
506
511
 
507
512
  ${taskcardTemplate}
@@ -23,7 +23,8 @@ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等
23
23
  2. 理解任务:模糊则问一个问题确认
24
24
  3. 加载项目信息:\`cat {SPEC_ROOT}/projects/*.yaml 2>/dev/null\`(了解项目结构和技术栈)
25
25
  4. 加载上下文:\`cat {SPEC_ROOT}/docs/<project>/scan/CONVENTIONS.md 2>/dev/null\`
26
- 5. 加载本地配置:\`cat {SPEC_ROOT}/local.yaml 2>/dev/null\`(构建命令、测试命令、环境变量等)
26
+ 5. 构建命令(CLI 自 local.yaml 注入,勿再读文件):
27
+ {LOCAL_COMMANDS}
27
28
  6. 若有关联变更,加载每个变更的设计文档:\`cat {SPEC_ROOT}/changes/<c>/design.md 2>/dev/null\`(理解设计意图)
28
29
  7. 如有需要,查询知识库:\`cat {SPEC_ROOT}/knowledge/INDEX.md 2>/dev/null\`
29
30
 
@@ -121,7 +122,8 @@ sillyspec run quick --done --change <id> --req "…" --cause "…" --solution "
121
122
  3. 【--done 后】核对 QUICKLOG 标题(CLI 已从「需求:」自动提取,仅弱标题才手改)→ 若改了再 \`git add\`
122
123
 
123
124
  ### 操作
124
- 1. 查看 \`git status --porcelain\`,确认只包含本次 quick 相关文件
125
+ 1. 工作区脏文件(CLI 注入,勿重跑 \`git status\`)——确认只包含本次 quick 相关文件,无关改动不暂存:
126
+ {GIT_DIRTY}
125
127
  2. 使用 \`git add -- <file...>\` 暂存本次 quick 实际修改的文件(不要 commit,由用户通过统一提交工具处理)
126
128
  - 禁止使用 \`git add -A\`
127
129
  - 不要暂存 quick 开始前就已存在的无关改动