sillyspec 3.25.4 → 3.25.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.
@@ -439,9 +439,16 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
439
439
  designContent: readIfExists(join(changeDir, 'design.md')),
440
440
  planContent: readIfExists(join(changeDir, 'plan.md')),
441
441
  })
442
+ const conclusion = extractVerifyConclusion(verify)
442
443
  if (['integration-critical', 'deployment-critical'].includes(changeRiskProfile.level)) {
443
- const conclusion = extractVerifyConclusion(verify)
444
- if (conclusion === 'PASS WITH NOTES' || conclusion === 'PASS') {
444
+ // 显式 risk_level 声明(design frontmatter,非关键词误判)下,PASS WITH NOTES 视为对残留项的
445
+ // 诚实声明,不强求全量集成证据——豁免本就来自 design 的明确判断。自动关键词判级维持严格:
446
+ // PASS WITH NOTES 仍要求证据齐全,防 agent 用 PASS WITH NOTES 绕证据门控。
447
+ const requiresEvidence = conclusion === 'PASS' || (conclusion === 'PASS WITH NOTES' && !changeRiskProfile.explicit)
448
+ if (conclusion === 'PASS WITH NOTES' && changeRiskProfile.explicit) {
449
+ warnings.push(`[${changeRiskProfile.level}] 结论 PASS WITH NOTES:design frontmatter 显式声明 risk_level=${changeRiskProfile.level},残留项须为真实集成证据缺口,并在 verify-result.md 如实说明`)
450
+ }
451
+ if (requiresEvidence) {
445
452
  const evidenceCheck = checkIntegrationEvidence(verify, changeRiskProfile.requiredVerification)
446
453
  if (!evidenceCheck.ok) {
447
454
  // A: 报错说人话 —— 把「缺哪一项、要写/做什么、判级原因」逐条列出,
@@ -455,7 +462,9 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
455
462
  return `\n 〔${k}〕${vn.desc}${lit}`
456
463
  })
457
464
  .join('')
458
- const cause = RISK_LEVEL_CAUSES[changeRiskProfile.level] || ''
465
+ const cause = changeRiskProfile.explicit
466
+ ? RISK_LEVEL_CAUSES.explicit
467
+ : (RISK_LEVEL_CAUSES[changeRiskProfile.level] || '')
459
468
  errors.push(
460
469
  `[${changeRiskProfile.level}] 验证结论为 ${conclusion},但缺少真实集成证据。\n` +
461
470
  ` 缺失项(需在 verify-result.md 如实提供并满足):${evidenceCheck.errors.join('; ')}\n` +
@@ -463,8 +472,9 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
463
472
  ` 风险判级原因:${cause}\n` +
464
473
  ` 命中触发词:${changeRiskProfile.triggers.join(', ')}\n` +
465
474
  ` 出路:① 补全上述缺失的真实集成证据(真实启动 daemon/backend、集成测试、运行日志)后保持 PASS;` +
466
- `或 ② 如实改结论 FAIL(承认端到端未验,留待部署后补)。` +
467
- `仅改结论文案/措辞蹭字面关键词会被对账,PASS WITH NOTES 在此等级下不被允许。`
475
+ `或 ② 如实改结论 FAIL(承认端到端未验,留待部署后补);` +
476
+ `或 ③ 若判级是关键词误伤(实际并未触碰 daemon/session/启动入口),在 design.md frontmatter 加 risk_level: <真实等级>(如 unit-sufficient)显式覆盖后重跑。` +
477
+ `仅改结论文案/措辞蹭字面关键词会被对账。`
468
478
  )
469
479
  }
470
480
  warnings.push(...evidenceCheck.warnings)
@@ -475,13 +485,29 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
475
485
  return { ok: errors.length === 0, errors, warnings }
476
486
  }
477
487
 
488
+ /**
489
+ * 计算归档目标目录名:保证 archive/ 下目录有且仅有一个日期前缀。
490
+ *
491
+ * 变更名创建时常已带 YYYY-MM-DD- 前缀(brainstorm 强制约定 + CLI 自动占位名),
492
+ * 若归档时再无条件拼一次 date- 前缀,会产生 2026-07-28-2026-07-28-<desc> 双日期。
493
+ * 这里先剥离源名前导日期,再统一拼归档当天日期 → 归档目录名恒为 <归档日期>-<纯描述>。
494
+ * 不带日期的源名(如 quick-<hash>)strip 无效,正常补日期,行为不变。
495
+ *
496
+ * 写(archiveChangeDirectory)与校验(validateArchiveOutputs)共用此函数,
497
+ * 避免「写和校验各自硬编码同公式」的漂移(曾因此导致双日期 bug 写校验都自洽通过)。
498
+ */
499
+ export function archiveDestDirName(date, changeName) {
500
+ const stripped = String(changeName).replace(/^\d{4}-\d{2}-\d{2}-/, '')
501
+ return `${date}-${stripped}`
502
+ }
503
+
478
504
  /**
479
505
  * archive 完成校验:检查归档目录完整性
480
506
  */
481
507
  function validateArchiveOutputs(cwd, changeName) {
482
508
  const archiveDir = join(cwd, '.sillyspec', 'changes', 'archive')
483
509
  const date = new Date().toISOString().slice(0, 10)
484
- const destDir = join(archiveDir, `${date}-${changeName}`)
510
+ const destDir = join(archiveDir, archiveDestDirName(date, changeName))
485
511
 
486
512
  // 归档目录不存在 early return(引擎在存在时才跑)
487
513
  if (!existsSync(destDir)) {
@@ -702,7 +728,7 @@ const contracts = {
702
728
  },
703
729
  status: {
704
730
  stage: 'status',
705
- description: '状态查看',
731
+ description: '项目快照',
706
732
  allowedFrom: [],
707
733
  allowedTo: [],
708
734
  validators: [],
@@ -199,7 +199,12 @@ export function verifyStageReviewDocHash(review, searchDirs) {
199
199
  if (actual === null) continue
200
200
  // 找到主文档,比对 hash
201
201
  if (review.docHash.toLowerCase() !== actual.toLowerCase()) {
202
- errors.push(`docHash 与主审查文档 ${primaryRel} 的实际内容不匹配 — review.json 疑似伪造(未真正读取文档)`)
202
+ errors.push(
203
+ `docHash 与主审查文档 ${primaryRel} 的实际内容不匹配。` +
204
+ `若刚改过该文档(如 design 修订后忘重算),重算并更新 review.json 的 docHash 字段:` +
205
+ `在基准目录 ${base} 下运行 sha256sum "${primaryRel}" 取首列 hex 填入;` +
206
+ `若未改文档,则 review.json 疑似伪造(未真正读取文档)。`
207
+ )
203
208
  return { ok: false, errors, warnings }
204
209
  }
205
210
  return { ok: true, errors, warnings }
@@ -5,17 +5,21 @@ export const definition = {
5
5
  steps: [
6
6
  {
7
7
  name: '任务完成度检查',
8
- prompt: `检查 plan.md 中所有任务 checkbox 是否已勾选(plan.md 是任务完成的唯一真相源)。
8
+ prompt: `检查任务完成度,决定是否可归档。
9
9
 
10
- ### 操作
11
- 1. 读取 \`.sillyspec/changes/<change-name>/plan.md\`
12
- 2. 检查所有 \`- [x]\` checkbox 是否已勾选
13
- 3. 如果 plan.md 不存在,回退读取 tasks.md 作为备选
14
- 4. 如有遗漏 → **必须暂停等待用户决定**,不要自行判断"可以归档"
15
- - 调用:\`sillyspec run archive --wait --reason "存在未完成任务,是否继续归档" --options "继续归档,回到execute完成剩余任务" --output "未完成任务列表"\`
10
+ ### 客观完成度报告(CLI 已注入,勿自行数 checkbox)
11
+ {TASK_COMPLETION_REPORT}
12
+
13
+ ### 判定规则
14
+ - **真相源 = review.json verdict**:上方报告由 CLI 从 .runtime/execute-runs/<runId>/tasks/task-NN/review.json 的 specVerdict + qualityVerdict 算出。
15
+ - plan.md 的 - [x] checkbox **仅作显示态参考**:它依赖自动回填,runId marker / review 缺失时会停在未勾态,与客观 verdict 不一致时**以本报告为准**,不要被未勾 checkbox 误导。
16
+ - 报告「未通过 / 缺失」= 0 且 source 为 review.json → 完成度合格,进入下一步。
17
+ - 报告 source 为 plan-checkbox-fallback(客观源不可用)→ 完成度无法客观确认,**必须暂停**让用户交叉核对,不要直接放行。
18
+ - 存在未完成任务 → **必须暂停等待用户决定**,不要自行判断"可以归档"。
19
+ - 调用:sillyspec run archive --wait --reason "存在未完成任务,是否继续归档" --options "继续归档,回到execute完成剩余任务" --output "未完成任务列表"
16
20
 
17
21
  ### 输出
18
- 完成度报告(已勾选/总数 + 未完成任务列表)`,
22
+ 完成度报告(已通过/总数 + 数据源 + 未完成任务列表)`,
19
23
  outputHint: '完成度报告',
20
24
  optional: false
21
25
  },
@@ -137,7 +141,7 @@ module_id: <module-id>
137
141
  2. 确保所有 checkbox 都已勾选
138
142
  3. 让用户确认后,用 \`--confirm\` 完成本步骤:
139
143
  \`sillyspec run archive --done --confirm --output "确认归档"\`
140
- 4. CLI 会创建 \`.sillyspec/changes/archive/\`,并将变更目录移动到 \`.sillyspec/changes/archive/YYYY-MM-DD-<change-name>/\`
144
+ 4. CLI 会创建 \`.sillyspec/changes/archive/\`,并将变更目录移动到 \`.sillyspec/changes/archive/<归档日期>-<纯描述>(源名前导日期去重,避免双日期)/\`
141
145
 
142
146
  ### 输出
143
147
  归档完成 + archive 目录路径`,
@@ -16,8 +16,9 @@ export const definition = {
16
16
 
17
17
  steps: [
18
18
  {
19
- name: '状态检查与上下文加载',
20
- prompt: `检查状态、加载项目上下文、匹配模块。
19
+ name: '进度确认与上下文加载',
20
+ migratedFrom: ['状态检查与上下文加载'],
21
+ prompt: `检查进度、加载项目上下文、匹配模块。用 \`sillyspec progress show\` 查流程进度,不要用 \`sillyspec status\`(项目级快照,不推进流程)。
21
22
 
22
23
  ### 操作
23
24
  1. 运行 \`sillyspec progress show\`,确认 currentStage 为 "brainstorm"
@@ -4,8 +4,10 @@ export const definition = {
4
4
  description: '探索需求、分析技术方案、识别风险',
5
5
  steps: [
6
6
  {
7
- name: '状态检查',
8
- prompt: `检查当前变更的进度状态(sillyspec.db)。
7
+ name: '进度确认',
8
+ // 重命名自「状态检查」(③ status 消歧):老进度里的「状态检查」completed 由迁移逻辑承接,防 currentIdx 回跳
9
+ migratedFrom: ['状态检查'],
10
+ prompt: `检查当前变更的进度状态(sillyspec.db)。用 \`sillyspec progress show\` 查流程进度,不要用 \`sillyspec status\`(项目级快照,不推进流程)。
9
11
 
10
12
  ### 操作
11
13
  1. 运行 \`sillyspec progress show\`
@@ -93,8 +93,9 @@ export const definition = {
93
93
  // 固定前缀步骤定义
94
94
  const fixedPrefix = [
95
95
  {
96
- name: '状态检查',
97
- prompt: `检查当前状态,确认可以执行 execute。
96
+ name: '进度确认',
97
+ migratedFrom: ['状态检查'],
98
+ prompt: `检查当前进度,确认可以执行 execute。用 \`sillyspec progress show\` 查流程进度,不要用 \`sillyspec status\`(项目级快照,不推进流程)。
98
99
 
99
100
  ### 操作
100
101
  1. 运行 \`sillyspec progress show\`
@@ -311,7 +312,7 @@ node -e "import('./src/worktree.js').then(w => { const wm = new w.WorktreeManage
311
312
  2. 系统自动检查:
312
313
  - patch --check 是否通过
313
314
  - 变更是否在 allowed_paths 内
314
- - 主工作区 baseline 是否变化
315
+ - 主工作区是否有未提交 dirty(拦截;已提交推进交 --3way 自动三路合并)
315
316
  - 是否有高风险文件(lockfile/migration/配置/入口)
316
317
  - diff 规模是否异常
317
318
  3. 输出 Apply Decision:
@@ -412,6 +412,13 @@ TaskCard 格式规则(必须严格遵守):
412
412
  - verify: 列表,实际可执行的命令
413
413
  - constraints: 列表,明确边界(含 brownfield 兼容、异常处理)
414
414
  - 不需要:修改文件章节、覆盖来源章节、接口定义章节、TDD 步骤章节、参考章节
415
+ - frontmatter 必须以三减号结尾闭合(开头和结尾各一行 ---)。缺结尾会让 postcheck 提取不到 frontmatter,误报「字段缺失 / allowed_paths 为空」
416
+ - allowed_paths 用块式写法(键名换行后每项一行「 - 路径」),不要用流式方括号 [path]——块式与所有校验器一致最稳
417
+ - implementation / acceptance / constraints / verify 的每条列表项是 YAML plain scalar,避免以下特殊字符触发 ScannerError:
418
+ - 冒号+空格(如代码签名 provider_id:str、queryUsage(id:string))会被当映射键报 mapping values are not allowed here——去掉冒号后空格或改成中文描述
419
+ - 花括号且内部含冒号或引号(如 JS 对象、JS 模板字面量)会报 expected block end——改用不含这些字符的中文描述
420
+ - 引号不成对会报扫描错误
421
+ - 注:goal 用 > 折叠标量可容忍花括号,goal 里写占位符不炸;只有上述 plain scalar 列表项需注意
415
422
  - provides / expects_from 是可选字段:仅当跨 task 契约(一个 task 的接口/DTO/响应被另一个 task 消费)时才填,单 task 或无对外接口场景留空即可
416
423
  - 填写后 plan-postcheck 会做硬对账:consumer 的每个 expects_from[provider].needs 字段必须在对应 provider 的 provides.fields 里,否则 plan 阶段阻断(不进入 execute)
417
424
  - 不要把内部实现字段塞进 provides;只暴露给其他 task 的对外契约形状
@@ -9,8 +9,9 @@ export const definition = {
9
9
  description: '生成结构化规范 — proposal + design + tasks',
10
10
  steps: [
11
11
  {
12
- name: '状态检查',
13
- prompt: `检查当前状态,确认可以执行 propose。
12
+ name: '进度确认',
13
+ migratedFrom: ['状态检查'],
14
+ prompt: `检查当前进度,确认可以执行 propose。用 \`sillyspec progress show\` 查流程进度,不要用 \`sillyspec status\`(项目级快照,不推进流程)。
14
15
 
15
16
  ### 操作
16
17
  1. 运行 \`sillyspec progress show\`
@@ -1,7 +1,7 @@
1
1
  export const definition = {
2
2
  name: 'status',
3
- title: '状态查看',
4
- description: '查看项目进度和状态',
3
+ title: '项目快照',
4
+ description: '项目级只读快照(非流程推进)。查「下一步该做什么/当前阶段进度」请用 sillyspec progress show,勿混淆。',
5
5
  auxiliary: true,
6
6
  steps: [
7
7
  {
@@ -36,16 +36,21 @@ export const definition = {
36
36
 
37
37
  steps: [
38
38
  {
39
- name: '状态检查',
40
- prompt: `检查当前状态,确认可以执行 verify。
39
+ name: '进度确认',
40
+ migratedFrom: ['状态检查'],
41
+ prompt: `> 💡 先说清楚:\`sillyspec run verify\`(不带 --done)**只下发执行指令,不会替你跑测试/构建**——真正的测试由你(Agent)在「运行测试和质量扫描」那步自己执行;CLI 只在最后 --done 时亲自跑 local.yaml 的 test 对账。别以为敲了 run verify 就自动验证了。
42
+
43
+ 检查当前进度,确认可以执行 verify。
44
+
45
+ > ⚠️ 本步用 \`sillyspec progress show\` 查**流程进度**(推进工作流用),不要用 \`sillyspec status\`(那是**项目级快照**,只读、不推进流程,是另一条命令)。
41
46
 
42
47
  ### 操作
43
48
  1. 运行 \`sillyspec progress show\`
44
49
  2. 确认 currentStage 为 "verify"
45
50
 
46
51
  ### 输出
47
- 当前状态摘要`,
48
- outputHint: '状态摘要',
52
+ 当前进度摘要`,
53
+ outputHint: '进度摘要',
49
54
  optional: false
50
55
  },
51
56
  {
@@ -182,6 +187,9 @@ export const definition = {
182
187
  ### 操作
183
188
  1. 汇总以上所有检查结果
184
189
  2. **变更风险等级(change_risk_profile)由 CLI 自动判定与门控**:你无需自己扫描关键词。本步骤 --done 时,CLI 会用 detectChangeRisk 扫描 design.md / plan.md 自动判定等级(doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical)并强制门控:integration-critical / deployment-critical 变更若结论为 PASS / PASS WITH NOTES 但缺少真实集成证据,CLI 直接阻断 verify 完成——谎报结论无效。
190
+ - **判级是机械字面匹配、不认否定语境**:design 里写「本次不改动 daemon/session」仍命中关键词被误判高危级。
191
+ - **误判时的诚实出路(豁免级)**:在 design.md 顶部 frontmatter 加一行 \`risk_level: <真实等级>\`(doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical),CLI 会以声明为准覆盖关键词判级。声明后若是 unit-sufficient 等豁免级,PASS WITH NOTES 不再被强制拦;但结论为 PASS 仍需对应证据。
192
+ - **留痕要求(防逃逸)**:用了显式声明,必须在本报告「变更风险等级」section 写明「risk_level 由 design frontmatter 显式声明 = <等级>(覆盖关键词判级)」+ 一句话理由,让豁免可审计。
185
193
  你只需:在 verify-result.md 的「变更风险等级」section 如实记录变更性质;若变更涉及 daemon/backend 跨进程、session/lease/lifecycle 状态机、或部署启动路径,在「Runtime Evidence」section 提供真实集成证据(启动命令、daemon↔backend 调用与日志关键片段、终态断言)。
186
194
 
187
195
  3. 生成 verify-result.md 文件,保存到 \`.sillyspec/changes/<change-name>/verify-result.md\`
@@ -218,7 +226,7 @@ PASS / PASS WITH NOTES / FAIL
218
226
  (TODO/FIXME/HACK 统计)
219
227
 
220
228
  ## 变更风险等级
221
- (自动检测的 change_risk_profile: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical)
229
+ (自动检测的 change_risk_profile: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical;若 design.md frontmatter 有 risk_level 显式声明,以声明为准并在此注明「显式声明 = <等级>」+ 理由)
222
230
 
223
231
  ## Runtime Evidence(integration-critical / deployment-critical 必填)
224
232
  - daemon 启动命令:
@@ -8,7 +8,7 @@
8
8
  * .sillyspec/.runtime/execute-runs/<runId>/tasks/<taskId>/review.json
9
9
  */
10
10
 
11
- import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'fs'
11
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'fs'
12
12
  import { join, resolve } from 'path'
13
13
  import { execFileSync } from 'child_process'
14
14
 
@@ -33,6 +33,122 @@ export function parseTaskIdsFromPlan(planContent) {
33
33
  return [...ids].sort()
34
34
  }
35
35
 
36
+ /**
37
+ * 统计 plan.md/tasks.md 中 task-NN checkbox 的勾选情况(降级口径)。
38
+ * 与 progress.js readPlanCheckboxStatus 同语义,但纯函数、无类依赖,供 summarizeTaskCompletion 降级复用。
39
+ */
40
+ function countPlanCheckboxes(planContent) {
41
+ const re = /^\s*[-*]\s+\[([ xX])\]\s+task-\d+/gm
42
+ let total = 0
43
+ let checked = 0
44
+ let m
45
+ while ((m = re.exec(planContent)) !== null) {
46
+ total++
47
+ if (m[1] === 'x' || m[1] === 'X') checked++
48
+ }
49
+ return { total, checked }
50
+ }
51
+
52
+ /**
53
+ * 汇总变更的任务完成度 —— archive Step1「任务完成度检查」的客观真相源。
54
+ *
55
+ * 以 execute 阶段产出的 review.json verdict 为准(task-NN 的 specVerdict + qualityVerdict 均 ≠ fail
56
+ * 视为完成),而非 plan.md checkbox。原因:plan.md checkbox 依赖 autoCheckPlanFromReviews 回填,
57
+ * 而 runId marker / review.json 缺失时回填静默 no-op,checkbox 会停在未勾态,导致完成度失真
58
+ * (archive 误判「全未完成」)。review.json 是 task 级客观 verdict,不受回填断裂影响。
59
+ *
60
+ * runId 解析:优先 current-execute-run-id-<changeName> marker(最新一次 execute run);
61
+ * marker 缺失则扫描 execute-runs/ 下子目录、取 mtime 最新的,尽力定位。
62
+ *
63
+ * fail-safe:无 plan / 无 runId / review 全缺等任何异常,降级到 plan.md checkbox 统计 + 标注 source,
64
+ * 绝不抛错、绝不阻断 archive(最坏退回「数 checkbox」现状,不比修复前差)。
65
+ *
66
+ * @param {{ changeDir: string, runtimeRoot: string, changeName: string }} opts
67
+ * @returns {{ source: string, total: number, completed: number, pending: Array<{id:string,reason:string}>, report: string }}
68
+ * source: 'review.json'(客观源可用)/ 'plan-checkbox-fallback'(降级)/ 'no-plan' / 'no-tasks'
69
+ * report: 人类可读报告,供 prompt {TASK_COMPLETION_REPORT} 注入
70
+ */
71
+ export function summarizeTaskCompletion({ changeDir, runtimeRoot, changeName }) {
72
+ const noPlan = {
73
+ source: 'no-plan', total: 0, completed: 0, pending: [],
74
+ report: '⚠️ 未找到 plan.md/tasks.md,无法计算任务完成度。请确认变更目录完整。'
75
+ }
76
+ if (!changeDir) return noPlan
77
+
78
+ const planPath = join(changeDir, 'plan.md')
79
+ const tasksPath = join(changeDir, 'tasks.md')
80
+ let planContent = null
81
+ if (existsSync(planPath)) planContent = readFileSync(planPath, 'utf8')
82
+ else if (existsSync(tasksPath)) planContent = readFileSync(tasksPath, 'utf8')
83
+ else return noPlan
84
+
85
+ const taskIds = parseTaskIdsFromPlan(planContent)
86
+ if (taskIds.length === 0) {
87
+ return { source: 'no-tasks', total: 0, completed: 0, pending: [],
88
+ report: '⚠️ plan.md 未解析出任何 task-NN 条目(plan 可能未按规范写 checkbox 列表)。' }
89
+ }
90
+
91
+ // runId 解析:marker → 扫描最新目录
92
+ let runId = null
93
+ const marker = join(runtimeRoot, `current-execute-run-id-${changeName}`)
94
+ try { if (existsSync(marker)) runId = readFileSync(marker, 'utf8').trim() } catch {}
95
+ if (!runId) {
96
+ try {
97
+ const runsDir = join(runtimeRoot, 'execute-runs')
98
+ if (existsSync(runsDir)) {
99
+ const entries = readdirSync(runsDir)
100
+ .map(e => ({ e, p: join(runsDir, e) }))
101
+ .filter(x => { try { return statSync(x.p).isDirectory() } catch { return false } })
102
+ .map(x => ({ e: x.e, mtime: statSync(x.p).mtimeMs }))
103
+ .sort((a, b) => b.mtime - a.mtime)
104
+ if (entries.length > 0) runId = entries[0].e
105
+ }
106
+ } catch {}
107
+ }
108
+
109
+ // 无 runId → 降级 plan.md checkbox 统计
110
+ if (!runId) {
111
+ const { total, checked } = countPlanCheckboxes(planContent)
112
+ return {
113
+ source: 'plan-checkbox-fallback', total, completed: checked,
114
+ pending: taskIds.slice(0, Math.max(0, total - checked)).map(id => ({ id, reason: 'checkbox 未勾(降级口径)' })),
115
+ report:
116
+ '⚠️ 客观源不可用(无 execute runId marker,review.json 无法定位)→ 降级用 plan.md checkbox。\n' +
117
+ `- 总任务:${total}\n- plan.md 已勾选:${checked}\n` +
118
+ '⚠️ checkbox 可能因 autoCheckPlanFromReviews 回填断裂而失真,务必交叉核对 .runtime/execute-runs/*/tasks/task-NN/review.json 的 verdict 再下结论。'
119
+ }
120
+ }
121
+
122
+ // 有 runId → 读 review.json verdict
123
+ const completed = []
124
+ const pending = []
125
+ for (const id of taskIds) {
126
+ const reviewPath = join(runtimeRoot, 'execute-runs', runId, 'tasks', id, 'review.json')
127
+ const r = readReview(reviewPath)
128
+ if (r.ok && r.review && r.review.specVerdict !== 'fail' && r.review.qualityVerdict !== 'fail') {
129
+ completed.push(id)
130
+ } else if (r.ok && r.review) {
131
+ pending.push({ id, reason: `verdict 未通过(spec=${r.review.specVerdict}, quality=${r.review.qualityVerdict})` })
132
+ } else {
133
+ pending.push({ id, reason: 'review.json 缺失(task 未走完 execute 评审)' })
134
+ }
135
+ }
136
+
137
+ const pendingLines = pending.length > 0
138
+ ? pending.map(p => ` - ${p.id}: ${p.reason}`).join('\n')
139
+ : ' (无)'
140
+ return {
141
+ source: 'review.json', total: taskIds.length, completed: completed.length, pending,
142
+ report:
143
+ `客观任务完成度(真相源 = review.json verdict,runId=${runId}):\n` +
144
+ `- 总任务:${taskIds.length}\n` +
145
+ `- 已通过(spec + quality verdict 均非 fail):${completed.length}\n` +
146
+ `- 未通过 / 缺失:${pending.length}\n` +
147
+ `- 未完成列表:\n${pendingLines}\n` +
148
+ '注:以 review.json verdict 为准;plan.md checkbox 仅作显示态(回填断裂时会与客观 verdict 不一致,以下方客观点为准)。'
149
+ }
150
+ }
151
+
36
152
  /**
37
153
  * 校验单个 review.json 文件
38
154
  * @param {object} review - 解析后的 JSON 对象
@@ -79,6 +195,36 @@ export function validateReviewSchema(review) {
79
195
  return { ok: errors.length === 0, errors }
80
196
  }
81
197
 
198
+ /**
199
+ * 增强 JSON.parse 失败的定位:从 V8 的 "at position N" 算出 line:col + 出错行上下文,
200
+ * 并对常见非法转义(正则的反斜杠序列误入 JSON 字符串)给针对性修复指引。
201
+ * @param {string} raw 原始文件内容
202
+ * @param {string} message V8 抛出的错误消息
203
+ * @returns {string} 增强后的可读消息
204
+ */
205
+ export function enrichJsonParseError(raw, message) {
206
+ const m = /at position (\d+)/.exec(message)
207
+ if (!m) return message
208
+ const pos = Math.min(Number(m[1]), raw.length)
209
+ let line = 1, col = 1
210
+ for (let i = 0; i < pos; i++) {
211
+ if (raw[i] === '\n') { line++; col = 1 } else { col++ }
212
+ }
213
+ const lineStart = raw.lastIndexOf('\n', pos - 1) + 1
214
+ const nlAfter = raw.indexOf('\n', pos)
215
+ const lineEnd = nlAfter === -1 ? raw.length : nlAfter
216
+ const snippet = raw.slice(lineStart, lineEnd)
217
+ const caret = ' '.repeat(Math.max(0, col - 1)) + '^'
218
+ // 单行注释内的反斜杠序列是纯文本,不触发块注释终止;正则字面量在函数体内正常解析。
219
+ let out = message + '\n → 第 ' + line + ' 行第 ' + col + ' 列:\n ' + snippet + '\n ' + caret
220
+ // 先移除合法的成对反斜杠(JSON 中 \\ 表字面 \),否则 "\\z" 会被逐字符正则误判为 "\z" 非法转义
221
+ const hasBadEscape = message.includes('Bad escaped character') || /\\[^"\\/bfnrtu]/.test(snippet.replace(/\\\\/g, ''))
222
+ if (hasBadEscape) {
223
+ out += '\n → 疑似正则转义(如 \\s \\d \\w)混入 JSON 字符串。修复:双写反斜杠(\\\\s),或用 JSON.stringify(obj) 重写 review.json。'
224
+ }
225
+ return out
226
+ }
227
+
82
228
  /**
83
229
  * 读取单个 task 的 review.json
84
230
  * @param {string} reviewPath - review.json 文件路径
@@ -100,8 +246,8 @@ export function readReview(reviewPath) {
100
246
  try {
101
247
  parsed = JSON.parse(raw)
102
248
  } catch (e) {
103
- // 文件存在但 JSON 非法:review 设为 null 但标记 parseError=true
104
- return { ok: false, review: null, parseError: true, errors: [`review.json 解析失败: ${e.message}`] }
249
+ // 文件存在但 JSON 非法:review 设为 null 但标记 parseError=true,错误消息带行列定位 + 转义修复指引
250
+ return { ok: false, review: null, parseError: true, errors: [`review.json 解析失败: ${enrichJsonParseError(raw, e.message)}`] }
105
251
  }
106
252
 
107
253
  const schemaResult = validateReviewSchema(parsed)