sillyspec 3.31.0 → 3.32.1

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 (74) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +1 -1
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
  3. package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-flow/SKILL.md +10 -6
  5. package/.claude/skills/sillyspec-quick/SKILL.md +105 -7
  6. package/.zcodeignore +66 -0
  7. package/AGENTS.md +16 -16
  8. package/assets/command-cards/flow.md +9 -3
  9. package/assets/command-cards/run-quick.md +4 -1
  10. package/package.json +2 -2
  11. package/src/agent-session-log.js +27 -5
  12. package/src/config-schema.js +21 -0
  13. package/src/datetime.js +51 -0
  14. package/src/db.js +10 -3
  15. package/src/decision-distill.js +21 -12
  16. package/src/design-facts.js +46 -6
  17. package/src/flow-draft.js +468 -95
  18. package/src/flow-parity.js +199 -5
  19. package/src/flow-review.js +136 -6
  20. package/src/flow.js +786 -145
  21. package/src/foreign-declared.js +75 -3
  22. package/src/fr-index.js +148 -162
  23. package/src/git-helper.js +16 -3
  24. package/src/hooks/worktree-guard.js +16 -5
  25. package/src/hunk-attribution.js +28 -5
  26. package/src/index.js +186 -19
  27. package/src/knowledge-classify.js +3 -1
  28. package/src/knowledge-digest.js +55 -26
  29. package/src/knowledge-match.js +135 -7
  30. package/src/knowledge-stats.js +105 -4
  31. package/src/knowledge-vector.js +160 -0
  32. package/src/machine-interface.js +90 -2
  33. package/src/module-impact.js +22 -1
  34. package/src/modules.js +9 -0
  35. package/src/progress/change-registry.js +20 -0
  36. package/src/progress/stage-machine.js +4 -7
  37. package/src/progress.js +8 -1
  38. package/src/quicklog.js +20 -0
  39. package/src/redomain.js +23 -7
  40. package/src/route-hindsight.js +276 -0
  41. package/src/run/archive-distill.js +3 -1
  42. package/src/run/command.js +65 -12
  43. package/src/run/complete-handlers.js +219 -5
  44. package/src/run/complete.js +99 -22
  45. package/src/run/gate-snapshot.js +15 -0
  46. package/src/run/gates.js +60 -4
  47. package/src/run/prompt.js +159 -31
  48. package/src/run/quick-audit.js +66 -7
  49. package/src/run/shared.js +6 -11
  50. package/src/run/stage.js +5 -1
  51. package/src/run/verify-quality-scan.js +84 -0
  52. package/src/scan-facts.js +2 -1
  53. package/src/scope-audit.js +138 -5
  54. package/src/sentinel-assertions.js +67 -16
  55. package/src/stage-contract-engine.js +4 -1
  56. package/src/stage-contract-spec.js +103 -4
  57. package/src/stage-contract.js +55 -7
  58. package/src/stages/brainstorm.js +27 -14
  59. package/src/stages/knowledge.js +4 -3
  60. package/src/stages/plan-postcheck.js +98 -3
  61. package/src/stages/plan.js +22 -2
  62. package/src/stages/quick.js +1 -1
  63. package/src/stages/verify.js +2 -1
  64. package/src/task-review.js +1 -1
  65. package/src/task-tick.js +121 -0
  66. package/src/taskcard-frontmatter.js +31 -0
  67. package/src/ui-visual.js +43 -8
  68. package/src/verify-postcheck.js +518 -38
  69. package/src/verify-probes.js +216 -5
  70. package/src/watcher.js +1067 -1031
  71. package/src/worktree-apply.js +9 -1
  72. package/src/worktree.js +21 -6
  73. package/templates/agents-instruction.md +15 -15
  74. package/templates/prompts/taskcard-rules.md +1 -0
@@ -350,7 +350,9 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
350
350
  const frKnowledgeRoot = specRoot ? join(specRoot, 'knowledge') : join(cwd, '.sillyspec', 'knowledge')
351
351
  const req = parseChangeRequirements(changeDir)
352
352
  if (!req.missing && req.frs.length > 0) {
353
- const domains = resolveTouchedDomains(changeDir, discoverModuleIndex(frKnowledgeRoot))
353
+ // unmapped 过滤对齐 frDupGateFlow(2026-09-29-rot-retire-inject-cap):停车场池不进
354
+ // 跨域标题比对——原全量载入 723 条做 CPU 税且产出跨域误指认
355
+ const domains = resolveTouchedDomains(changeDir, discoverModuleIndex(frKnowledgeRoot)).filter((d) => d !== 'unmapped')
354
356
  const active = readActiveFrDigest(frKnowledgeRoot, domains)
355
357
  if (active.length > 0) {
356
358
  for (const fr of req.frs) {
@@ -411,6 +413,37 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
411
413
  )
412
414
  }
413
415
  }
416
+
417
+ // ── 闭环收口 custom 判定(2026-09-29-brainstorm-closure-gates)──
418
+ // doubt-closure:「自审存疑:」应用形态行须含闭合 token——只查带冒号的应用形态,裸词
419
+ // 判定会撞 design-init 自审 checklist 模板行(含裸词)常驻误报。
420
+ {
421
+ const doubtRule = getRule('brainstorm.design.doubt-closure')
422
+ const markerRe = new RegExp(doubtRule.data.markerPattern.pattern, doubtRule.data.markerPattern.flags)
423
+ const closureRe = new RegExp(doubtRule.data.closureTokens.join('|'), 'i')
424
+ for (const line of content.split(/\r?\n/)) {
425
+ if (!markerRe.test(line) || closureRe.test(line)) continue
426
+ warnings.push(doubtRule.failMessage.replaceAll('${line}', line.trim().slice(0, 60)))
427
+ }
428
+ }
429
+ // risk-mitigation:风险登记表 R-xx 行的应对策略列(行末单元格)非空且非占位——design-init
430
+ // 骨架预填「(待填应对策略)」命中即提醒填语义;「接受」显式接受是合法闭合(归属明确)。
431
+ {
432
+ const riskRule = getRule('brainstorm.design.risk-mitigation')
433
+ const rowRe = new RegExp(riskRule.data.rowPattern.pattern, riskRule.data.rowPattern.flags)
434
+ const cellPlaceholderRe = new RegExp(riskRule.data.placeholderCell, 'i')
435
+ for (const line of content.split(/\r?\n/)) {
436
+ const rowMatch = line.match(rowRe)
437
+ if (!rowMatch) continue
438
+ let cells = line.split('|').slice(1).map(c => c.trim())
439
+ if (cells.length > 0 && cells[cells.length - 1] === '' && line.trimEnd().endsWith('|')) cells.pop()
440
+ if (cells.length < 2) continue // 行形态不完整(仅编号列)——表结构问题交 Grill 审查,不在此硬判
441
+ const lastCell = cells[cells.length - 1].replace(/^[((]/, '').replace(/[))]$/, '').trim()
442
+ if (cellPlaceholderRe.test(lastCell)) {
443
+ warnings.push(riskRule.failMessage.replaceAll('${id}', rowMatch[1]))
444
+ }
445
+ }
446
+ }
414
447
  }
415
448
 
416
449
  const decisionsFile = join(changeDir, 'decisions.md')
@@ -425,10 +458,22 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
425
458
  warnings.push('decisions.md 存在但没有当前版本 D-xxx@vN 决策 ID')
426
459
  } else {
427
460
  const design = readIfExists(join(changeDir, 'design.md'))
428
- const requirements = readIfExists(join(changeDir, 'requirements.md'))
429
- const tasks = readIfExists(join(changeDir, 'tasks.md'))
430
- // decision 的天然引用落点是 design.md;requirements(需求按 FR 组织)与
431
- // tasks(骨架,待 plan 展开)不强求逐条引用每个架构决策,否则批量误报。
461
+ // requirements 的 D 覆盖点名(2026-09-29-brainstorm-closure-gates):prompt 一直要求
462
+ // 「requirements.md 必须引用全部当前版本 D-xxx@vN,未覆盖的标注为剩余风险」,机器面原只盯
463
+ // design.md——闭环收口补齐:裸号词边界匹配(同 warnMissingIds 语义),裸号出现在决策覆盖
464
+ // 矩阵或「剩余风险」行都算归属(覆盖或显式登记=开口有主),不出现才点名。tasks 仍不强求
465
+ // (骨架,plan 阶段展开——plan --done 的 id-traceability 已覆盖 plan.md 侧)。
466
+ const requirementsForCoverage = readIfExists(join(changeDir, 'requirements.md'))
467
+ if (requirementsForCoverage) {
468
+ const reqUpper = requirementsForCoverage.toUpperCase()
469
+ const coverageFail = getRule('brainstorm.requirements.decision-coverage').failMessage
470
+ for (const id of decisionIds) {
471
+ const base = id.replace(/@V\d+$/, '')
472
+ if (!new RegExp(`\\b${base}\\b`).test(reqUpper)) {
473
+ warnings.push(coverageFail.replaceAll('${id}', id))
474
+ }
475
+ }
476
+ }
432
477
  warnMissingIds(warnings, decisionIds, design, 'design.md', 'decisions.md')
433
478
  }
434
479
  // 故障面/退役判据软警告(FR-01,2026-09-15-tax-governance):architecture+accepted 缺字段
@@ -1204,12 +1249,15 @@ export function judgeApiCoverageMatrix(args) {
1204
1249
  if (!apiEvidenceHasAnchorForm(e)) {
1205
1250
  // 分诊(2026-09-23 执行会话实证:design接口表# 字样在场但 # 后非 METHOD /path 形态时,
1206
1251
  // 泛化「缺五形态之一」让人无法区分「形态没写对」与「写了没算数」——三轮试错后 agent 被逼
1207
- // 换更弱的 DDL@ 形态过门,fail-closed 反而促成了更泛的锚点。定向报出该形态的命中条件。)
1252
+ // 换更弱的 DDL@ 形态过门,fail-closed 反而促成了更泛的锚点。定向报出该形态的命中条件。
1253
+ // 2026-09-30 二次实证(multi-agent-platform tool-report-activation 会话):泛化分支文案
1254
+ // 「用例依据锚点」里的「用例依据」是第 3 列列名(caseId),而校验只查第 5 列证据列——
1255
+ // 文案把 agent 引向错误的列,再坑三轮。锚点文案一律点名「证据列」,禁用列名歧义词。)
1208
1256
  API_ANCHOR_DESIGN_API_RE.lastIndex = 0
1209
1257
  const bareDesignAnchor = /design接口表#/.test(e) && !API_ANCHOR_DESIGN_API_RE.test(e)
1210
1258
  anchorViolations.push(bareDesignAnchor
1211
1259
  ? `${rowLabel(r)}:证据含 design接口表# 但 # 后未提取到 METHOD /path——该形态仅 \`design接口表#POST /api/xx\` 形态计命中(仅写表名/行号/散文描述不计)`
1212
- : `${rowLabel(r)}:证据缺用例依据锚点(五形态之一)`)
1260
+ : `${rowLabel(r)}:证据列(矩阵第 5 列)缺锚点(五形态之一)——锚点只认证据列,写在第 3 列「用例依据」不计`)
1213
1261
  }
1214
1262
  }
1215
1263
  if (anchorViolations.length > 0) {
@@ -54,10 +54,20 @@ export const definition = {
54
54
  - 多项目且用户未指定:列出项目列表,需要用户确认本次需求属于哪个子项目
55
55
 
56
56
  ### 早期规模筛查(判断是否该走轻量变更)
57
- 加载上下文后,用需求描述 + 模块上下文**粗判**本次变更规模:
58
- - **明显 small**(满足:预计改动 ≤ 2 个文件、单模块、无 schema/API/状态机/权限变更;或属于改文案/修 bug/样式调整/配置微调等纯执行类)→ 输出:「此变更规模较小,建议直接走轻量变更」+ 一句依据,给出建议命令 \`sillyspec flow start --change <名> --input "<需求>"\`(2 调用协议收口)。用户同意则本阶段可在此收尾(\`--done\` 并提示转轻量变更),不必继续走完整设计流程。
59
- - **拿不准或明显 large**(涉及多模块、schema、状态流转、新架构等)→ 不要提示轻量变更,继续进入下一步「对话式探索与需求澄清」。
60
- - 这是**粗判**,只为让明显的小变更免走完整设计流程;不确定就继续,后续「生成规范文件」步骤会基于 design.md 文件清单做精判兜底。
57
+ 加载上下文后,用需求描述 + 模块上下文**粗判**本次变更规模(判据同 Step 8 复杂度/上下文轴——2026-09-29-brainstorm-exit-thin-default):
58
+ - **明显 small**(满足:单上下文可吞吐的纯执行类——改文案/修 bug/样式调整/配置微调,预计无 Wave 编排/上下文分片需求)→ 输出:「此变更规模较小,建议直接走轻量变更」+ 一句依据,给出可照抄建议命令(--input 引号内换行合法):
59
+
60
+ \`\`\`
61
+ sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>
62
+
63
+ 成功标准:
64
+ - <可验证标准>"
65
+ \`\`\`
66
+
67
+ (2 调用协议收口)。用户同意则本阶段可在此收尾(\`--done\` 并提示转轻量变更),不必继续走完整设计流程。
68
+ - **明显 large**(需要 Wave 并行编排/上下文分片/多阶段治理,或用户显式要求完整流程)→ 不要提示轻量变更,继续进入下一步「对话式探索与需求澄清」。
69
+ - **拿不准 → 继续探索**(这正是头脑风暴的职责;澄清完成后 Step 8 按同轴精判——注意:经过头脑风暴澄清的需求通常已可单上下文吞吐,出口默认收编轻量道)。
70
+ - 这是**粗判**,只为让明显的纯执行小变更免走设计流程;后续「生成规范文件」步骤做精判兜底。
61
71
 
62
72
  ### 输出
63
73
  项目现状理解摘要(3-5 句话,关键约定和架构决策)+ 可能涉及的模块列表 + 本次需求所属子项目 + (如命中)quick 建议
@@ -203,12 +213,13 @@ export const definition = {
203
213
  waitReason: '等待用户选择方案',
204
214
  waitOptions: ['方案A', '方案B', '方案C'],
205
215
  prompt: `基于需求理解和澄清结果,提出 2-3 种实现方案。
206
-
216
+ {UI_VISUAL_GUIDANCE}
207
217
  ### 操作
208
218
  1. 每种方案列出:核心思路、优势、劣势
209
219
  2. 如果上一步产生了 D-xxx@vN 决策记录,方案必须说明覆盖/违反哪些当前版本决策
210
- 3. 给出推荐方案和理由
211
- 4. 用户选定方案后,把选择结果**当场追加**到 \`{SPEC_ROOT}/changes/<change-name>/decisions.md\`(增量落盘规则同「对话式探索」步:目录/文件缺失先创建、按 D-xxx@vN 判重):type 用 architecture、source 用 user、question 记方案取舍问题、answer 记用户所选方案与理由要点、evidence 记方案选择轮次——方案选择是本变更最关键的用户决策,必须随答随落盘,不能只留在对话里
220
+ 3. 定稿前机制词检索(固定动作):方案引入新机制词(如 分类/词表/枚举/门/协议/状态机——例词为举例非机制,不构成封闭清单)时,先跑 \`sillyspec knowledge search --query "<机制词>"\`,命中条目必读(尤其 status=rejected 的防复潮条目)再定稿方案
221
+ 4. 给出推荐方案和理由
222
+ 5. 用户选定方案后,把选择结果**当场追加**到 \`{SPEC_ROOT}/changes/<change-name>/decisions.md\`(增量落盘规则同「对话式探索」步:目录/文件缺失先创建、按 D-xxx@vN 判重):type 用 architecture、source 用 user、question 记方案取舍问题、answer 记用户所选方案与理由要点、evidence 记方案选择轮次——方案选择是本变更最关键的用户决策,必须随答随落盘,不能只留在对话里
212
223
 
213
224
  ### 铁律 — 必须等待用户选择方案
214
225
  - 列出方案对比表和推荐后必须暂停等用户选择,**不要自己说"推荐方案 A"然后当用户选了**(命令由 CLI 在下方注入)
@@ -234,9 +245,10 @@ export const definition = {
234
245
  1. 简单项目:几句话整体描述
235
246
  2. 复杂项目:按模块/Phase 分段展示,每段 200-300 字
236
247
  3. 展示完整设计方案(不要逐段停顿,一次性展示)
237
- 4. 确认变更名(格式:\`YYYY-MM-DD-<简短描述>\`,例如 \`2026-05-13-user-auth\`——CLI 已强制:新建/改名不带日期前缀直接 exit 2)
238
- 5. 按「HTML 原型分级」生成原型或写出跳过原因(二选一必填,见下)
239
- 6. 暂停等待用户确认或修改意见
248
+ 4. 分段设计含新机制时同上检索(固定动作):设计的分段内容引入新机制词(例词为举例非机制,不构成封闭清单)时,先跑 \`sillyspec knowledge search --query "<机制词>"\`,命中条目必读(尤其 status=rejected 的防复潮条目)再定稿设计
249
+ 5. 确认变更名(格式:\`YYYY-MM-DD-<简短描述>\`,例如 \`2026-05-13-user-auth\`——CLI 已强制:新建/改名不带日期前缀直接 exit 2)
250
+ 6. 按「HTML 原型分级」生成原型或写出跳过原因(二选一必填,见下)
251
+ 7. 暂停等待用户确认或修改意见
240
252
 
241
253
  ### HTML 原型分级(按改动程度,不是"是否碰前端文件")
242
254
  判定对象是**改造后的界面相对现状的变化程度**:
@@ -515,11 +527,12 @@ status: passed | needs-user-input | blocked | skipped
515
527
  prompt: `按变更规模生成规范文件并给出实现路径建议(设计已在前置步骤确认过,本步不再暂停等确认,生成后展示摘要即可)。
516
528
 
517
529
  ### 规模评估(展示前先做)
518
- 读取 design.md 的「文件变更清单」,判断本次变更规模:
519
- - **small(小变更)**:改动 ≤ 2 个文件、单模块、无跨模块依赖、无状态机/schema/API 变更
520
- - **large(大变更)**:不满足上述任意一条
530
+ 按**复杂度/上下文轴**判断本次变更规模(2026-09-29-brainstorm-exit-thin-default:此前文件数轴「≤2 文件才 small」与选道表第 3 行的复杂度措辞不一致,且模糊需求探索完很少只碰 2 文件——出口几乎必然五阶段,实测坐实):
531
+ - **small(收编轻量道)**:单上下文可吞吐——预计无需 Wave 并行编排、无需上下文分片(单会话吃得下全部任务)、无多阶段治理/设计期人机对抗需求
532
+ - **large(完整五阶段)**:需要 Wave 并行编排、或单任务规模大需上下文分片、或多阶段治理、或用户显式要求完整流程
533
+ - **拿不准 → small**(与 thin-default-flip 同哲学:预判反诱发误升厚——R16 实证;升厚留运行时证据:实测失败自动升厚+--upgrade-thick 用户决策)
521
534
  在 design.md frontmatter 写入 \`scale: small\` 或 \`scale: large\`(frontmatter 不存在则补 \`author\`/\`created_at\`/\`scale\`)。规模决定下面的产物范围和实现路径。
522
- (注:早期「加载项目上下文」步骤已做过一次粗判并可能建议过走 quick;此处基于 design.md 文件清单做精判兜底。)
535
+ (注:早期「加载项目上下文」步骤已做过一次粗判并可能建议过走 quick;此处基于 design 方案实质做精判兜底——判据同轴。)
523
536
 
524
537
  ### 操作
525
538
  1. **按规模生成规范文件**:
@@ -19,7 +19,8 @@
19
19
  import { existsSync, readFileSync, readdirSync, mkdirSync, statSync } from 'fs'
20
20
  import { join, basename } from 'path'
21
21
  import { writeAtomicSync } from '../fs-atomic.js'
22
- import { parseKnowledgeIndex, matchKnowledge } from '../knowledge-match.js'
22
+ import { parseKnowledgeIndex } from '../knowledge-match.js'
23
+ import { matchKnowledgeHybrid } from '../knowledge-vector.js'
23
24
 
24
25
  // ── 工具函数 ──
25
26
 
@@ -106,8 +107,8 @@ export async function cmdSearch(dir, args, opts = {}) {
106
107
  return
107
108
  }
108
109
 
109
- // 复用 knowledge-match 引擎
110
- const result = matchKnowledge(knowledgeDir, query)
110
+ // 复用 knowledge-match 引擎(三层 hybrid:路由→平台向量→本地词片,2026-09-29-knowledge-vector-recall)
111
+ const result = await matchKnowledgeHybrid(knowledgeDir, query, { cwd: dir })
111
112
 
112
113
  if (!result.matched) {
113
114
  output(true, { query, matches: [] })
@@ -21,7 +21,7 @@ import { parseFileChangeList, pathMatches } from '../change-list.js'
21
21
  import { getRule } from '../stage-contract-spec.js'
22
22
  import { TASKCARD_PLACEHOLDERS } from '../taskcard-placeholders.js'
23
23
  import { validateScriptCommands } from './cmd-existence.js'
24
- import { parseTaskFrontmatter } from '../taskcard-frontmatter.js'
24
+ import { parseTaskFrontmatter, diagnoseTaskYamlError } from '../taskcard-frontmatter.js'
25
25
 
26
26
  // ═══════════════════════════════════════════════════════════════
27
27
  // 解析工具(从 plan.js 迁移)
@@ -614,7 +614,8 @@ export function validateBlueprintConsistency(changeDir, opts = {}) {
614
614
  if (ts.length > 1) {
615
615
  errors.push(
616
616
  `路径 ${p} 被 Wave ${w} 内 ${ts.length} 个 task 修改: ${ts.join(', ')} — 同 Wave 任务 execute 强制并行(execute.js:603),` +
617
- `子代理会互相覆盖该文件。解法:把它们拆到不同 Wave(串行)。` +
617
+ `子代理会互相覆盖该文件。解法:把它们拆到不同 Wave(串行)——手工拆易踩编号/依赖坑(非合法 Wave 号→伪并行串行链),` +
618
+ `推荐 sillyspec plan-adopt-waves --change <变更名> 按 depends_on 拓扑一键重排(重排后同波文件面有交集会被拒绝,再手工拆分那一个)。` +
618
619
  `(注:quick 阶段对同文件并发只提示不阻断——那是轻量流程无并行子代理、最坏后果 git 可分离;` +
619
620
  `此处硬拦是因并行覆盖不可自动恢复,两阶段宽严不同是设计使然)`
620
621
  )
@@ -1309,8 +1310,12 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
1309
1310
  if (!fmParse.ok) {
1310
1311
  const isDupReported = dupKeys.length > 0 && /duplicated mapping key/i.test(fmParse.error.message)
1311
1312
  if (!isDupReported) {
1313
+ // 分诊(2026-10-06-verify-friction-fix task-03):js-yaml 消息 + 出错行双信号给中文修复
1314
+ // 动作——知识源是 taskcard-rules.md,此前只透传英文原文,厚流程实证连撞 6 轮门禁
1315
+ const errLineText = (content.split('\n')[fmParse.error.line - 1] || '').replace(/\r$/, '')
1316
+ const fmDiag = diagnoseTaskYamlError(fmParse.error.message, errLineText)
1312
1317
  errors.push(
1313
- `${dupTaskId || file}: frontmatter 非法 YAML(${file}:${fmParse.error.line}:${fmParse.error.column} ${fmParse.error.message})——契约/验收/标量字段全链路不可读,plan 门禁拒绝放行;修复卡片 frontmatter(值含方括号/全角括号时加引号或改块式列表)`
1318
+ `${dupTaskId || file}: frontmatter 非法 YAML(${file}:${fmParse.error.line}:${fmParse.error.column} ${fmParse.error.message})——契约/验收/标量字段全链路不可读,plan 门禁拒绝放行;分诊:${fmDiag}`
1314
1319
  )
1315
1320
  }
1316
1321
  }
@@ -1472,6 +1477,45 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
1472
1477
  return { ok: errors.length === 0, errors, warnings }
1473
1478
  }
1474
1479
 
1480
+ /**
1481
+ * taskcard --validate 的 CLI 入口(2026-10-06-verify-friction-fix task-03):plan 门禁同源规则
1482
+ * 的零成本自检面——填卡后、--done 门禁前先跑,把「门禁时一次撞一批」前移成「提交前自检」。
1483
+ * 在 validatePlanFeasibility 之上补 target_files 严格形态检查(对账门规则,plan feasibility 不含)。
1484
+ * 注意防环:本函数留在 plan-postcheck(其依赖链已有 taskcard-frontmatter/placeholders),
1485
+ * taskcard.js 不得反向 import 本模块(taskcard-placeholders.js jsdoc 的 ESM 循环告诫同源)。
1486
+ * @param {{ changeDir: string, projectRoot?: string|null }} opts
1487
+ * @returns {{ ok: boolean, errors: string[], warnings: string[], files: number }}
1488
+ */
1489
+ export function validateTaskcardsCli({ changeDir, projectRoot = null }) {
1490
+ const res = validatePlanFeasibility(changeDir, projectRoot)
1491
+ const errors = [...res.errors]
1492
+ const warnings = [...res.warnings]
1493
+ let files = 0
1494
+ try {
1495
+ const tasksDir = pJoin(changeDir, 'tasks')
1496
+ if (existsSync(tasksDir)) {
1497
+ const list = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f)).sort()
1498
+ files = list.length
1499
+ for (const f of list) {
1500
+ const content = _readFileSync(pJoin(tasksDir, f), 'utf8')
1501
+ const fmParse = parseTaskFrontmatter(content)
1502
+ if (!fmParse.ok) continue // 0b 已报,不双报
1503
+ const tf = parseTargetFiles(content)
1504
+ if (!tf.missing) {
1505
+ const bad = tf.entries.filter(e => e.invalid)
1506
+ if (bad.length > 0) {
1507
+ const id = (content.match(/^id:\s*(.+)/m)?.[1] || '').trim() || f
1508
+ errors.push(`${id}: target_files 形态非法——${bad.map(b => `${b.raw}(${b.invalid})`).join(';')}`)
1509
+ }
1510
+ }
1511
+ }
1512
+ }
1513
+ } catch {
1514
+ // 目录不可读 → 可行性结果原样返回(其 errors/warnings 已如实反映)
1515
+ }
1516
+ return { ok: errors.length === 0, errors, warnings, files }
1517
+ }
1518
+
1475
1519
  /**
1476
1520
  * Plan 产物校验:检查 plan.md 和 tasks/ 是否齐全
1477
1521
  * @param {string} changeDir - 变更目录
@@ -2010,7 +2054,46 @@ function deriveModuleIdFor(filePath, mapYaml) {
2010
2054
  * @param {{ cwd: string, specRoot?: string, resolveChangeDir: Function, progress?: object }} context
2011
2055
  * @throws {Error} 校验失败时抛出
2012
2056
  */
2057
+ /**
2058
+ * Wave 形态错误自动重排判定(2026-10-07-wave-auto-adopt-review-dedup task-01):postmortem
2059
+ * (provider-model-list)实测 Wave 返工链三轮——同 Wave 冲突/伪并行串行链都是 adopt-waves
2060
+ * 机械可解的形态,却要 agent 撞 postcheck 后按文案找命令。此处错误全为 Wave 形态类时自动
2061
+ * 跑 plan-adopt-waves 重排并整体复跑 postcheck(预算 1 次,防环);逃生键 local.yaml
2062
+ * plan.auto_adopt_waves: false(缺省 true)。混有非 Wave 类错误不自动重排(行为=现状)——
2063
+ * 重排会掩盖真实问题的上下文。
2064
+ */
2065
+ const WAVE_SHAPE_ERROR_RE = /被 Wave \d+ 内 \d+ 个 task 修改|伪并行串行链|Wave 依赖方向违规/
2066
+ function allWaveShapeErrors(errors) {
2067
+ const list = Array.isArray(errors) ? errors : []
2068
+ return list.length > 0 && list.every(e => WAVE_SHAPE_ERROR_RE.test(String(e)))
2069
+ }
2070
+ function autoAdoptWavesEnabled(specDir) {
2071
+ try {
2072
+ const p = pJoin(specDir, 'local.yaml')
2073
+ if (!existsSync(p)) return true
2074
+ const doc = jsYaml.load(_readFileSync(p, 'utf8'))
2075
+ return !(doc && doc.plan && doc.plan.auto_adopt_waves === false)
2076
+ } catch { return true }
2077
+ }
2078
+ async function tryAutoAdoptWaves(errors, changeDir, specDir, budget) {
2079
+ if (budget <= 0) return false
2080
+ if (!allWaveShapeErrors(errors)) return false
2081
+ if (!autoAdoptWavesEnabled(specDir)) return false
2082
+ const { adoptPlanWaves } = await import('../plan-adopt-waves.js')
2083
+ const res = adoptPlanWaves({ changeDir })
2084
+ if (!res || !res.ok) {
2085
+ console.warn(` ⚠️ Wave 自动拓扑重排未成(${res && res.error ? res.error : '未知原因'})——按原报错处理(手工出口:sillyspec plan-adopt-waves --change <变更名>)`)
2086
+ return false
2087
+ }
2088
+ console.log(`\n🔀 检测到 Wave 形态问题——已自动按 depends_on 拓扑重排 plan.md(plan-adopt-waves,任务总表 W 列同步)并整体复跑校验:`)
2089
+ return true
2090
+ }
2091
+
2013
2092
  export async function executePlanPostcheck(context) {
2093
+ return executePlanPostcheckRun(context, { autoAdoptBudget: 1 })
2094
+ }
2095
+
2096
+ async function executePlanPostcheckRun(context, { autoAdoptBudget = 0 } = {}) {
2014
2097
  const { cwd, specRoot, resolveChangeDir, progress } = context
2015
2098
 
2016
2099
  const specDir = specRoot || pJoin(cwd, '.sillyspec')
@@ -2207,6 +2290,14 @@ export async function executePlanPostcheck(context) {
2207
2290
 
2208
2291
  // ── 聚合输出:一轮 --done 暴露全部失败项(坑6③)──
2209
2292
  if (failures.length > 0) {
2293
+ // Wave 形态自动重排判定(task-01):唯一失败组是蓝图一致性且错误全为 Wave 形态 → 自动
2294
+ // adopt-waves 重排并整体复跑一次(预算 1)。挂统一收口点而非 consistency 单点:混有
2295
+ // feasibility/coverage 等其他类错误时不重排(重排会掩盖真实问题上下文,实测 WA4 形态)。
2296
+ if (failures.length === 1 && failures[0].name === '蓝图一致性校验') {
2297
+ if (await tryAutoAdoptWaves(failures[0].errors, changeDir, specDir, autoAdoptBudget)) {
2298
+ return executePlanPostcheckRun(context, { autoAdoptBudget: 0 })
2299
+ }
2300
+ }
2210
2301
  console.error(`\n❌ plan postcheck 失败(${failures.length} 类问题,已全部列出——一次修复后重跑,无需逐个迭代):`)
2211
2302
  for (const f of failures) {
2212
2303
  console.error(`\n 【${f.name}】`)
@@ -2347,6 +2438,10 @@ export async function executePlanPostcheck(context) {
2347
2438
  const structure = assessWaveStructure({ existingWaves, topoWaves: waves, depMap, wavePathSets })
2348
2439
  if (structure.advisory) console.warn(` ⚠️ ${structure.advisory}`)
2349
2440
  if (structure.error) {
2441
+ // 伪并行碎片自动重排(task-01):mergeable 对 ≥2 是 adopt-waves 的标准解
2442
+ if (await tryAutoAdoptWaves([structure.error], changeDir, specDir, autoAdoptBudget)) {
2443
+ return executePlanPostcheckRun(context, { autoAdoptBudget: 0 })
2444
+ }
2350
2445
  console.error(`\n❌ ${structure.error}`)
2351
2446
  throw new Error(structure.error)
2352
2447
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'fs'
2
2
  import path from 'path'
3
+ import jsYaml from 'js-yaml'
3
4
  import { getRule } from '../stage-contract-spec.js'
4
5
  import { REVIEW_CHECKLISTS } from '../stage-review-checklist.js'
5
6
 
@@ -414,6 +415,20 @@ execute/verify 阶段会按实际代码变更更新此文档;archive 阶段会
414
415
  */
415
416
  export function buildCoordinatorStep(changeDir, taskNames) {
416
417
  const changeName = path.basename(changeDir)
418
+ // 填卡 batch 阈值项目化(2026-10-06-verify-docs-prefill task-03):「task 总数 ≤N 主 agent 直填」
419
+ // 原硬编码 8——430 万 token 填卡实证后按项目体系可调(local.yaml plan.fill_batch_min_tasks,
420
+ // integer ≥1,非法/缺省回退 8)。specBase 自 changeDir 推导(changes/<名> 的父目录)。
421
+ const fillBatchMin = (() => {
422
+ try {
423
+ const specBase = String(changeDir || '').replace(/[\\/]+changes[\\/]+[^\\/]+$/, '')
424
+ if (!specBase || specBase === changeDir) return 8
425
+ const p = path.join(specBase, 'local.yaml')
426
+ if (!existsSync(p)) return 8
427
+ const doc = jsYaml.load(readFileSync(p, 'utf8'))
428
+ const v = doc && doc.plan ? Number(doc.plan.fill_batch_min_tasks) : NaN
429
+ return Number.isInteger(v) && v >= 1 ? v : 8
430
+ } catch { return 8 }
431
+ })()
417
432
  const taskList = taskNames.map(t => {
418
433
  const num = t.num
419
434
  return `- task-${num}: ${t.name}`
@@ -496,8 +511,8 @@ ${taskList}
496
511
  幂等,已存在的卡跳过不覆盖;骨架带 LF 行尾 + 闭合 frontmatter + 硬校验 9 字段 + depends_on 反填。**只有主 agent 跑这一次,子代理一律不再运行 taskcard CLI。**
497
512
  1. 确认 \`${changeDir}/tasks/\` 目录存在(上一步预生成会自动创建)
498
513
  2. **默认主 agent 自己填卡(P0-2,2026-09-20 对撞实验驱动——6 个填卡子代理 6.7M token 做的是主 agent 可自完成的誊写,独立性价值为零):**
499
- - task 总数 ≤8 或变更单仓单模块 → 主 agent 逐卡 Edit 填充(骨架已预生成,誊写近乎免费),**不派子代理**
500
- - 仅当 task 总数 >8 **且**跨多模块/跨仓、或主会话上下文已明显吃紧时,才按 batch 分派子代理(此时并行省墙钟有真实收益)
514
+ - task 总数 ≤${fillBatchMin} 或变更单仓单模块 → 主 agent 逐卡 Edit 填充(骨架已预生成,誊写近乎免费),**不派子代理**
515
+ - 仅当 task 总数 >${fillBatchMin} **且**跨多模块/跨仓、或主会话上下文已明显吃紧时,才按 batch 分派子代理(此时并行省墙钟有真实收益)
501
516
  3. **(分派形态时)按 batch 分派子代理(减少总子代理数量,而不是一个 task 一个子代理):**
502
517
  - 把任务按「同一 Wave + 同一模块/相近能力 + 无跨 batch 强依赖」原则分组
503
518
  - **每个 batch 包含 2~4 个 task**;Wave 内任务数 ≤4 时整个 Wave 可作为一个 batch
@@ -508,6 +523,11 @@ ${taskList}
508
523
  5. 每个子代理使用下方「批量 TaskCard 子代理 prompt」,一次生成该 batch 的全部 task-N.md
509
524
  6. 等待所有 batch 子代理完成
510
525
  7. 验证每个 task-N.md 文件已生成且非空
526
+ 8. **全部卡片填充完成后,主 agent 跑一次零成本自检(plan 门禁同源规则前置暴露,免 Step 4 门禁批量报错返工)**:
527
+ \`\`\`
528
+ sillyspec taskcard ${changeName} --validate
529
+ \`\`\`
530
+ 报错即门禁报错(frontmatter YAML/占位符/必要字段/target_files 形态,YAML 错误带中文分诊动作);修复后重跑至通过再进下一步。
511
531
 
512
532
  > 设计意图:plan.md 里 task 数可以较多(能力拆分完整),但 TaskCard 生成阶段的**默认形态是主 agent 直填**——卡是 design/plan 的誊写不是创作,主 agent 刚写完 plan 上下文最热;子代理只在规模/上下文压力真实存在时才有净收益(每个子代理都要重读 design+plan+源码,6 个子代理 6.7M token 的实证教训)。分派时合并 batch,避免子代理数量随 task 数线性爆炸。
513
533
 
@@ -1,7 +1,7 @@
1
1
  export const definition = {
2
2
  name: 'quick',
3
3
  title: '快速任务(已退役,仅存量在途会话收尾)',
4
- description: 'quick 通道已退役(v3.31.0 起拒绝新会话);新工作走轻量变更 flow start',
4
+ description: 'quick 通道已退役(v3.30.0 起拒绝新会话);新工作走轻量变更 flow start',
5
5
  auxiliary: true,
6
6
  steps: [
7
7
  {
@@ -164,7 +164,8 @@ export const definition = {
164
164
  2. 如果存在:
165
165
  - 逐个读取 tasks/task-NN.md,对照 frontmatter 的 \`acceptance:\` 列表逐条核验(TaskCard 协议的验收标准在 frontmatter YAML,正文无 checkbox)
166
166
  - 每条 acceptance 对照实际实现/测试结果判定满足与否,未满足的项列为不通过
167
- - 覆盖对账走探针 7 矩阵:verify-result.md「探针 7:验收×测试覆盖矩阵」段已由 CLI 预填归属测试文件与关键词提示(骨架未生成时先跑 \`sillyspec verify-probes --change <change-name> --init\`,幂等不覆盖已有正文);逐行填判定(covered/covered-service/partial/uncovered/non-testable 五选一——covered-service 适用:端点行为由 service 层等非端点层测试锁定,证据附测试锚点)与证据——covered/covered-service/partial 附测试锚点(file:line / \`.test.\` 测试文件名 / 反引号包裹的路径或测试名,行号可省),non-testable 写一句理由;判定/证据未填会被 verify \`--done\` 门禁阻断(fail-closed)
167
+ - 覆盖对账走探针 7 矩阵:verify-result.md「探针 7:验收×测试覆盖矩阵」段已由 CLI 预填归属测试文件与关键词提示(骨架未生成时先跑 \`sillyspec verify-probes --change <change-name> --init\`,幂等不覆盖已有正文);逐行填判定(covered/covered-service/partial/uncovered/non-testable 五选一——covered-service 适用:端点行为由 service 层等非端点层测试锁定,证据附测试锚点)与证据——covered/covered-service/partial 附测试锚点(file:line / \`.test.\` 测试文件名 / 反引号包裹的路径或测试名,行号可省),non-testable 写一句理由;判定/证据未填会被 verify \`--done\` 门禁阻断(fail-closed);矩阵里带「既有用例候选(FR 关联回归面)」注记的测试是本变更未改动的既有回归用例——复核其覆盖性后如实填判定,命中≠结论
168
+ - **收口前预检(免逐轮撞门)**:verify-result.md 各节填完后、跑 \`--done\` 前,先跑 \`sillyspec gate verify --change <change-name> --docs-only\`——秒级只读预检纯文档契约缺项(visual-evidence/结论槽/移交项结构化等一次列清,不执行测试);缺项清零后再走 \`--done\` 的完整实测门
168
169
  3. 如果不存在:跳过此步骤
169
170
 
170
171
  ### 输出
@@ -1015,7 +1015,7 @@ export function stampExecuteRunChange(runtimeRoot, runId, changeName) {
1015
1015
  * 读 run 目录的 change 归属戳。
1016
1016
  * @returns {string|null} 戳内容(trim 后非空),无戳/读失败返回 null
1017
1017
  */
1018
- function readExecuteRunChangeStamp(runtimeRoot, runId) {
1018
+ export function readExecuteRunChangeStamp(runtimeRoot, runId) {
1019
1019
  try {
1020
1020
  const c = readFileSync(join(runtimeRoot, 'execute-runs', runId, 'change'), 'utf8').trim()
1021
1021
  return c || null
@@ -0,0 +1,121 @@
1
+ /**
2
+ * task-tick.js — `sillyspec task tick` 轻量勾选动词(2026-10-03-voluntary-task-tick)。
3
+ *
4
+ * 定位:让「边干边勾」成为最顺手路径——做完一个任务单元,一条命令翻格并立即拿到机器
5
+ * 确认(进度 N/M + 下一任务指针)。翻格产生的 tasks.md 变更由 watcher 既有 task-done
6
+ * 事件链实时上平台——闭环确认是 OpenSpec apply 循环「勾后重询」的本地对等物(2026-10-03
7
+ * local-usage-caliber-fix 0/12 事故的自愿路径修复:正反馈替代验证强制)。
8
+ *
9
+ * 事件直写(2026-10-07-thin-tasks-v3):翻格成功后向本变更 watcher 事件流追加分级精确
10
+ * task-done 事件(source:'task-tick',checked N→M 逐格口径)——watcher 3s 轮询对快速连续
11
+ * tick 只能采样出合并跳,收口节奏门以 CLI 精确事件为权威去重(detectBatchCheckCadence)。
12
+ * best-effort:事件写失败不阻断翻格(节奏门退回采样判,fail 方向安全)。
13
+ *
14
+ * 边界:
15
+ * - 纯簿记动词:无 review/verdict/ownership 仪式(与 task done 四合一的分界——那是厚档
16
+ * 任务卡的收尾件;本动词对 thin 工作分解面与覆写面同样适用,自愿使用不违 D-007)。
17
+ * - 幂等:已勾再勾走 already 分支(exit 0,零事件直写)——并发/重放零副作用;写盘走 writeAtomicSync。
18
+ * - 行内替换只动 checkbox 态字符,其余字节(含 CRLF)逐字保留。
19
+ * - 归档件只读:变更已归档时拒收(exit 2)。
20
+ */
21
+ import { existsSync, readFileSync, appendFileSync } from 'node:fs'
22
+ import { join } from 'node:path'
23
+
24
+ /** 任务行(宽容缩进/星号有序标记,与哨兵判集行锚同口径 + 缩进容差)。 */
25
+ const TASK_ROW_RE = /^(\s*[-*] \[)( |x|X)(\] task-(\d+):)(.*)$/
26
+
27
+ /** 归一 task id:task-1 → task-01(与 governance-autopilot 勾选 token 归一同口径)。 */
28
+ export function normalizeTaskId(id) {
29
+ const m = /^task-(\d+)$/.exec(String(id || '').trim())
30
+ return m ? `task-${String(Number(m[1])).padStart(2, '0')}` : null
31
+ }
32
+
33
+ /** 任务行清单(行序即文档序):[{id, checked, desc}]。 */
34
+ function collectRows(md) {
35
+ const rows = []
36
+ for (const line of String(md || '').split(/\r?\n/)) {
37
+ const m = line.match(TASK_ROW_RE)
38
+ if (m) rows.push({ id: `task-${String(Number(m[4])).padStart(2, '0')}`, checked: m[2].toLowerCase() === 'x', desc: (m[5] || '').trim() })
39
+ }
40
+ return rows
41
+ }
42
+
43
+ /**
44
+ * 纯函数:翻格 + 进度回显材料。
45
+ * @param {{tasksMd:string, taskId:string}} opts
46
+ * @returns {{kind:'ticked'|'already', tasksMd:string, checked:number, total:number, next:{id:string,desc:string}|null}
47
+ * |{kind:'unknown', available:string[]}}
48
+ */
49
+ export function tickTasksMd({ tasksMd, taskId }) {
50
+ const id = normalizeTaskId(taskId)
51
+ if (!id) return { kind: 'unknown', available: [] }
52
+ const rows = collectRows(tasksMd)
53
+ const target = rows.find((r) => r.id === id)
54
+ if (!target) return { kind: 'unknown', available: rows.map((r) => r.id) }
55
+ let out = String(tasksMd || '')
56
+ if (!target.checked) {
57
+ const re = new RegExp(`^(\\s*[-*] \\[) (\\] ${id}:)`, 'm')
58
+ out = out.replace(re, '$1x$2')
59
+ }
60
+ const after = collectRows(out)
61
+ const checked = after.filter((r) => r.checked).length
62
+ const pending = after.find((r) => !r.checked)
63
+ return {
64
+ kind: target.checked ? 'already' : 'ticked',
65
+ tasksMd: out,
66
+ checked,
67
+ total: after.length,
68
+ next: pending ? { id: pending.id, desc: pending.desc } : null,
69
+ }
70
+ }
71
+
72
+ /**
73
+ * IO 编排(index.js 分派入口):定位变更目录 → tickTasksMd → 原子写盘 → 回显。
74
+ * exit 0=翻格/幂等;exit 2=未知 id / 缺 tasks.md / 已归档。
75
+ */
76
+ export async function runTaskTick({ changeName, cwd, taskId, specBase }) {
77
+ const changesRoot = join(specBase, 'changes')
78
+ const tasksPath = join(changesRoot, changeName, 'tasks.md')
79
+ if (!existsSync(tasksPath)) {
80
+ if (existsSync(join(changesRoot, 'archive', changeName))) {
81
+ console.error(`❌ 变更 ${changeName} 已归档——归档件只读,不勾选`)
82
+ } else {
83
+ console.error(`❌ 未找到 ${tasksPath}——task tick 面向在途变更的任务注册表(确认 --change)`)
84
+ }
85
+ process.exit(2)
86
+ }
87
+ const r = tickTasksMd({ tasksMd: readFileSync(tasksPath, 'utf8'), taskId })
88
+ if (r.kind === 'unknown') {
89
+ const ids = (r.available || []).join('、') || '(tasks.md 无任务行)'
90
+ console.error(`❌ tasks.md 中无 ${taskId}——可选任务:${ids}`)
91
+ process.exit(2)
92
+ }
93
+ if (r.kind === 'ticked') {
94
+ const { writeAtomicSync } = await import('./fs-atomic.js')
95
+ writeAtomicSync(tasksPath, r.tasksMd)
96
+ // 事件直写(2026-10-07-thin-tasks-v3):精确单格跳进事件流,节奏门按 source 去重采样合并跳。
97
+ try {
98
+ const { resolveRuntimeRoot } = await import('./run/shared.js')
99
+ const runtimeRoot = resolveRuntimeRoot({}, specBase)
100
+ const eventsPath = join(runtimeRoot, `watcher-events-${changeName}.jsonl`)
101
+ const from = Math.max(0, r.checked - 1)
102
+ appendFileSync(eventsPath, JSON.stringify({
103
+ ts: Date.now(), kind: 'task-done', stage: 'tasks',
104
+ detail: `checked ${from}→${r.checked}`, provisional: true, source: 'task-tick',
105
+ }) + '\n', 'utf8')
106
+ } catch { /* 事件 best-effort:写失败翻格不受影响(节奏门退回采样判) */ }
107
+ // 工件即时重推(2026-10-07-allticked-gate-docs-resync):翻格即任务面变更——顺带触发
108
+ // spec-sync,平台不再停留 start 时点快照(真实会话实证:改写后平台恒显初稿)。best-effort。
109
+ try {
110
+ const { triggerSync } = await import('./run/shared.js')
111
+ await triggerSync(cwd, changeName, { specRoot: specBase })
112
+ } catch { /* 重推 best-effort:失败不影响翻格主流程 */ }
113
+ }
114
+ const nextTip = r.next
115
+ ? `|下一任务:${r.next.id} ${r.next.desc.slice(0, 40)}${r.next.desc.length > 40 ? '…' : ''}`
116
+ : `|🎉 ${r.total}/${r.total} 全勾——可收口:sillyspec flow done --change ${changeName}`
117
+ console.log(`${r.kind === 'ticked' ? '✅ 已勾' : 'ℹ️ 已是勾选态(幂等跳过写盘)'}(${r.checked}/${r.total})${nextTip}`)
118
+ return r
119
+ }
120
+
121
+ export default { tickTasksMd, runTaskTick, normalizeTaskId }
@@ -55,3 +55,34 @@ export function parseTaskFrontmatter(content) {
55
55
  }
56
56
  return { ok: true, hasFrontmatter: true, fm, error: null }
57
57
  }
58
+
59
+ /**
60
+ * js-yaml 报错分诊(2026-10-06-verify-friction-fix task-03):消息 + 出错行内容双信号映射中文
61
+ * 修复动作。背景:分诊知识长期躺在 templates/prompts/taskcard-rules.md L19-23 未接进门禁报错,
62
+ * 厚流程实证 agent 连撞 6 轮 YAML 门禁;且 js-yaml v4 真实消息与文档措辞有漂移(实证
63
+ * `title: A: B` 报 bad indentation 而非 mapping values),只匹配消息文本不可靠——先看出错行
64
+ * 的首字符是否保留指示符,再看行内是否「冒号+空格」,消息文本作家族归类兜底。
65
+ * @param {string} message js-yaml 异常 message(单行化后)
66
+ * @param {string} [errorLineText] 出错行原文(文件 1 基行内容;缺省只按消息分诊)
67
+ * @returns {string} 中文修复动作(非空)
68
+ */
69
+ export function diagnoseTaskYamlError(message, errorLineText = '') {
70
+ const msg = String(message || '')
71
+ const line = String(errorLineText || '').replace(/\r$/, '')
72
+ // 剥列表项/缩进前缀后看首个 token 字符:反引号/@/% 是 YAML 保留指示符,js-yaml 对其报的
73
+ // 消息家族不稳定(实测反引号列表项报 bad indentation of a sequence entry),行内容才是可靠信号
74
+ const itemBody = line.replace(/^\s*(?:-\s*)?/, '')
75
+ if (/^[`@%]/.test(itemBody)) {
76
+ return '列表项/值以 YAML 保留指示符开头(反引号 `、@、%)——行首加普通文字前缀(如「执行 」)或给整行值加单引号包裹'
77
+ }
78
+ if (/quoted scalar/i.test(msg)) return '引号不成对——补齐成对引号,或改用中文引号/去掉引号'
79
+ if (/duplicated mapping key/i.test(msg)) return '同一字段写了两次——保留正确一处删除其余(骨架已反填的键勿重复手填)'
80
+ if (/expected ',' or ']'/i.test(msg)) return '方括号流序列内有裸特殊字符——改块式列表(键名换行后每项一行「 - 路径」);空 [] 占位不受影响'
81
+ if (/expected ',' or '}'/i.test(msg)) return '花括号且内部含冒号或引号(JS 对象/模板字面量)——改用不含这些字符的中文描述'
82
+ if (/mapping values are not allowed|bad indentation/i.test(msg)) {
83
+ if (/: /.test(line)) return '值含「冒号+空格」(如 A: B、url: http://x)被当成嵌套映射键——去掉冒号后的空格(写 A:B)或改写为不含「: 」的中文描述'
84
+ return '缩进或映射结构错——块式列表项保持两空格缩进、与上级键名对齐;值含「: 」时改写描述'
85
+ }
86
+ if (/cannot start any token/i.test(msg)) return '行内含 YAML 无法起始 token 的字符(如反引号/@ 开头)——加文字前缀或用引号包裹该值'
87
+ return '值含特殊字符——给值加单引号、去掉「: 」冒号空格,或改块式列表(规则全表见 templates/prompts/taskcard-rules.md)'
88
+ }