sillyspec 3.26.14 → 3.26.15
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.
- package/.claude/skills/sillyspec-archive/SKILL.md +2 -0
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
- package/.claude/skills/sillyspec-commit/SKILL.md +18 -60
- package/.claude/skills/sillyspec-continue/SKILL.md +2 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
- package/.claude/skills/sillyspec-plan/SKILL.md +10 -2
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -2
- package/.claude/skills/sillyspec-resume/SKILL.md +2 -0
- package/.claude/skills/sillyspec-verify/SKILL.md +2 -0
- package/.claude/skills/sillyspec-workspace/SKILL.md +7 -1
- package/package.json +1 -1
- package/src/commit-suggest.js +202 -0
- package/src/config-cat.js +88 -0
- package/src/docs-check.js +48 -12
- package/src/doctor-diagnostics.js +49 -0
- package/src/hooks/worktree-guard.js +20 -1
- package/src/index.js +508 -6
- package/src/init.js +1 -1
- package/src/local-register.js +98 -0
- package/src/module-impact.js +146 -0
- package/src/progress/change-registry.js +43 -4
- package/src/progress/stage-machine.js +10 -5
- package/src/progress.js +13 -1
- package/src/quicklog.js +106 -1
- package/src/run/command.js +95 -1
- package/src/run/complete-handlers.js +56 -3
- package/src/run/complete.js +4 -7
- package/src/run/gates.js +62 -4
- package/src/run/multi-repo-context.js +3 -1
- package/src/run/next.js +128 -0
- package/src/run/prompt.js +20 -2
- package/src/run/shared.js +2 -2
- package/src/scan-postcheck.js +142 -2
- package/src/stage-contract-spec.js +1 -1
- package/src/stage-review.js +15 -6
- package/src/stages/archive.js +1 -1
- package/src/stages/brainstorm.js +1 -0
- package/src/stages/execute.js +105 -26
- package/src/stages/knowledge.js +3 -2
- package/src/stages/quick.js +14 -6
- package/src/stages/scan.js +1 -0
- package/src/stages/verify.js +7 -49
- package/src/task-review.js +218 -4
- package/src/verify-probes.js +264 -0
- package/src/workspace.js +125 -0
- package/src/worktree-apply.js +15 -1
- package/src/worktree.js +58 -8
- package/templates/claude-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +1 -1
- package/templates/prompts/verify-probes.md +13 -47
package/src/stages/archive.js
CHANGED
|
@@ -36,7 +36,7 @@ export const definition = {
|
|
|
36
36
|
5. module-impact.md 第一行标题必须用中文:# 模块影响分析(Module Impact)— <变更简述>
|
|
37
37
|
|
|
38
38
|
### 降级(module-impact.md 不存在)
|
|
39
|
-
若缺失(如旧变更未经新 plan 流程,或 small
|
|
39
|
+
若缺失(如旧变更未经新 plan 流程,或 small 变更从未生成):先跑 \`sillyspec module-impact --change <change-name>\`——CLI 按 _module-map.yaml paths 前缀匹配预填「模块影响矩阵」骨架(文件×模块归属 + 未匹配文件清单,机械部分全代算),你只需逐行填「影响类型」(逻辑变更/数据结构变更/接口变更/调用关系变更/配置变更/新增)与 needs_review 标记,补首行中文标题 \`# 模块影响分析(Module Impact)— <变更简述>\`。骨架无从生成(无 module-map / 无 diff)时才按 archive-impact.yaml 的 impact-analyzer 角色规则全手写。
|
|
40
40
|
|
|
41
41
|
确认完成后,下一步 sync-module-docs 会读 module-impact.md 更新模块卡片文档。`,
|
|
42
42
|
outputHint: 'module-impact.md 终审结果 + 修正摘要',
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -45,6 +45,7 @@ export const definition = {
|
|
|
45
45
|
7. 检查全局模板:\`ls ~/.sillyspec/templates/\`
|
|
46
46
|
- 有匹配模板 → 询问是否基于模板
|
|
47
47
|
- 无相关内容 → 跳过,不输出
|
|
48
|
+
8. 查询知识库:\`cat {SPEC_ROOT}/knowledge/INDEX.md 2>/dev/null\`(无索引则跳过)——按需求关键词命中条目时读取对应知识文件;known-issues/已知坑要在方案期就规避,不要等 execute 的命中报告才发现
|
|
48
49
|
|
|
49
50
|
### 模块匹配方法
|
|
50
51
|
读取 _module-map.yaml 后,根据用户描述的需求关键词,匹配相关模块:
|
package/src/stages/execute.js
CHANGED
|
@@ -214,6 +214,7 @@ const fixedPrefix = [
|
|
|
214
214
|
|
|
215
215
|
### 符号影响面扩展检查
|
|
216
216
|
11. **符号影响面扫描**(Critical — execute 前必做):
|
|
217
|
+
- **报告骨架勿手写**:先跑 \`sillyspec symbol-impact --change <change-name>\`——CLI 从 tasks.md 生成逐 task \`<!--TODO-->\` 骨架(gate 拦截时也会自动落一份);把每行占位替换为真实结论(**未替换的 TODO 占位会被 gate 拒绝**,骨架不能直接过门)
|
|
217
218
|
- 读取所有 tasks/task-NN.md,提取每个任务涉及的修改文件
|
|
218
219
|
- 对每个修改文件,检查是否涉及以下变更类型:
|
|
219
220
|
- class 构造函数参数变更(新增/删除/修改参数)
|
|
@@ -221,7 +222,7 @@ const fixedPrefix = [
|
|
|
221
222
|
- DTO / 类型定义变更
|
|
222
223
|
- API client 方法签名变更
|
|
223
224
|
- 函数/方法签名变更(参数增删改)
|
|
224
|
-
-
|
|
225
|
+
- 如果涉及上述变更类型,执行调用点搜索(用于补骨架外的调用点证据,不是生成报告的方式):
|
|
225
226
|
\`\`\`bash
|
|
226
227
|
rg "new ClassName\(" src/
|
|
227
228
|
rg "ClassName\(" src/
|
|
@@ -230,7 +231,7 @@ const fixedPrefix = [
|
|
|
230
231
|
\`\`\`
|
|
231
232
|
- 将搜索到的调用点与 plan.md 和 tasks/task-NN.md 的 allowed_paths 对比
|
|
232
233
|
- **发现调用点不在任何 task 的 allowed_paths 中 → 直接阻断 execute**
|
|
233
|
-
- **报告落盘(CLI
|
|
234
|
+
- **报告落盘(CLI 硬校验)**:结论写入 \`{SPEC_ROOT}/changes/<change-name>/symbol-impact.md\`,每个 task 一行结论(task-XX: 变更类型 + 受影响调用点 + 是否在范围内;无签名级变更也要显式写「无签名级变更」)。\`--done\` 时 CLI 校验该文件存在且覆盖 plan.md 全部 task,缺失或不覆盖会阻断完成
|
|
234
235
|
- 如果调用点不在范围内但任务明确写了"不改原因",记录但不阻断
|
|
235
236
|
|
|
236
237
|
### 输出
|
|
@@ -558,19 +559,20 @@ function resolveTaskRepo(task, changeDir) {
|
|
|
558
559
|
}
|
|
559
560
|
|
|
560
561
|
/**
|
|
561
|
-
* 把 base_commit
|
|
562
|
-
*
|
|
563
|
-
* 防止同 Wave 多 task 改同一跨仓仓时 HEAD 推进致 diff 范围漂移(design §5.3 约束① / R-01)。
|
|
562
|
+
* 把 commit 锡点(base_commit/head_commit)写入 task 卡 frontmatter(W3 task-08,D-010;
|
|
563
|
+
* head 侧 2026-08-21 审计项③ 补对称)。
|
|
564
564
|
*
|
|
565
|
-
* 策略:幂等写——frontmatter
|
|
566
|
-
*
|
|
565
|
+
* 策略:幂等写——frontmatter 有该字段行则就地替换,无则按 D-010 协议顺序插入:
|
|
566
|
+
* head_commit 跟在 base_commit 行后;base_commit 跟在 repo 行后;均无则插 frontmatter
|
|
567
|
+
* 首部。仅改 frontmatter 不动正文。已存在相同值不重复写。
|
|
567
568
|
*
|
|
568
569
|
* @param {string} taskFilePath - task-NN.md 绝对路径
|
|
569
|
-
* @param {
|
|
570
|
+
* @param {'base_commit'|'head_commit'} field - 锚点字段名
|
|
571
|
+
* @param {string} commit - commit sha
|
|
570
572
|
* @returns {boolean} 是否实际写入(false = 文件不存在 / 写失败 / 值未变)
|
|
571
573
|
*/
|
|
572
|
-
function
|
|
573
|
-
if (!taskFilePath || !
|
|
574
|
+
export function writeCommitAnchorToTaskCard(taskFilePath, field, commit) {
|
|
575
|
+
if (!taskFilePath || !commit || (field !== 'base_commit' && field !== 'head_commit')) return false
|
|
574
576
|
if (!existsSync(taskFilePath)) return false
|
|
575
577
|
let content
|
|
576
578
|
try {
|
|
@@ -579,27 +581,28 @@ function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
|
|
|
579
581
|
return false
|
|
580
582
|
}
|
|
581
583
|
// CRLF 归一(坑 base-commit-crlf-frontmatter,plan-postcheck.js 同款):跨仓 task 卡被
|
|
582
|
-
// 编辑器/子代理写成 CRLF 后 `^---\n` 匹配失败 →
|
|
584
|
+
// 编辑器/子代理写成 CRLF 后 `^---\n` 匹配失败 → 锚点静默不落盘,而 Wave
|
|
583
585
|
// prompt 仍声称「CLI 已落盘」。归一后按 LF 写回(taskcard 生成侧本就强制 LF)。
|
|
584
586
|
content = content.replace(/\r\n?/g, '\n')
|
|
585
587
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
586
588
|
if (!fmMatch) return false
|
|
587
589
|
const fm = fmMatch[1]
|
|
588
|
-
const
|
|
590
|
+
const lineRe = new RegExp(`^${field}:\\s.*$`, 'm')
|
|
589
591
|
let newFm
|
|
590
|
-
if (
|
|
591
|
-
const existing = (fm.match(
|
|
592
|
-
if (existing ===
|
|
593
|
-
newFm = fm.replace(
|
|
592
|
+
if (lineRe.test(fm)) {
|
|
593
|
+
const existing = (fm.match(lineRe) || [''])[0]
|
|
594
|
+
if (existing === `${field}: ${commit}`) return false // 值未变,幂等跳过
|
|
595
|
+
newFm = fm.replace(lineRe, `${field}: ${commit}`)
|
|
594
596
|
} else {
|
|
595
|
-
//
|
|
596
|
-
|
|
597
|
-
const anchor =
|
|
598
|
-
|
|
599
|
-
|
|
597
|
+
// 插入锚点(D-010 协议顺序 repo→base_commit→head_commit):head 跟 base 后,
|
|
598
|
+
// base 跟 repo 后,都无则 frontmatter 首字段
|
|
599
|
+
const anchor = `${field}: ${commit}`
|
|
600
|
+
const prevField = field === 'head_commit' ? 'base_commit' : 'repo'
|
|
601
|
+
const prevRe = new RegExp(`^${prevField}:\\s.*$`, 'm')
|
|
602
|
+
if (prevRe.test(fm)) {
|
|
603
|
+
newFm = fm.replace(prevRe, m => m + '\n' + anchor)
|
|
600
604
|
} else {
|
|
601
|
-
|
|
602
|
-
newFm = anchor.trimEnd() + '\n' + fm
|
|
605
|
+
newFm = anchor + '\n' + fm
|
|
603
606
|
}
|
|
604
607
|
}
|
|
605
608
|
const newContent = content.replace(/^---\n[\s\S]*?\n---/, `---\n${newFm}\n---`)
|
|
@@ -612,6 +615,75 @@ function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
|
|
|
612
615
|
}
|
|
613
616
|
}
|
|
614
617
|
|
|
618
|
+
function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
|
|
619
|
+
return writeCommitAnchorToTaskCard(taskFilePath, 'base_commit', baseCommit)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* 跨仓 task head_commit 锡点自动落盘(2026-08-21 agent-手工产出审计项③,D-010 补对称)。
|
|
624
|
+
*
|
|
625
|
+
* base_commit 已在派发时 CLI 落盘,head_commit 此前靠主 agent 按 Wave prompt 指引手跑
|
|
626
|
+
* `rev-parse` 手写(漏抄/抄错直接炸 Task Review Gate 真实性校验)。本函数在 execute
|
|
627
|
+
* --done 时机补齐另一半:扫描 task 卡,跨仓 task(repo≠main)缺 head_commit 的,实时
|
|
628
|
+
* `git -C <跨仓仓根> rev-parse HEAD` 幂等写入(**已存在不覆盖**——agent 手写的精确锚点优先)。
|
|
629
|
+
*
|
|
630
|
+
* best-effort:MultiRepoContext 构造失败 / git 不可达 → 跳过该 task 记 reason,绝不抛。
|
|
631
|
+
*
|
|
632
|
+
* @param {{ changeName: string, cwd: string, specBase?: string, platformOpts?: object }} opts
|
|
633
|
+
* @returns {Promise<{ stamped: number, stampedTasks: string[], skipped: number, reasons: string[] }>}
|
|
634
|
+
*/
|
|
635
|
+
export async function stampCrossRepoHeadCommits({ changeName, cwd, specBase, platformOpts = {} }) {
|
|
636
|
+
const base = specBase || platformOpts.specRoot || path.join(cwd, '.sillyspec')
|
|
637
|
+
const reasons = []
|
|
638
|
+
const stampedTasks = []
|
|
639
|
+
if (!changeName) {
|
|
640
|
+
return { stamped: 0, stampedTasks, skipped: 0, reasons: ['无 changeName'] }
|
|
641
|
+
}
|
|
642
|
+
const tasksDir = path.join(base, 'changes', changeName, 'tasks')
|
|
643
|
+
if (!existsSync(tasksDir)) {
|
|
644
|
+
return { stamped: 0, stampedTasks, skipped: 0, reasons: ['无 tasks/ 目录(单 task 变更,无跨仓锡点)'] }
|
|
645
|
+
}
|
|
646
|
+
let ctx = null
|
|
647
|
+
try {
|
|
648
|
+
const { getOrCreateMultiRepoContext } = await import('../run/shared.js')
|
|
649
|
+
ctx = await getOrCreateMultiRepoContext({ cwd, changeName, platformOpts })
|
|
650
|
+
} catch (e) {
|
|
651
|
+
return { stamped: 0, stampedTasks, skipped: 0, reasons: [`MultiRepoContext 构造失败: ${e.message}`] }
|
|
652
|
+
}
|
|
653
|
+
let stamped = 0
|
|
654
|
+
let skipped = 0
|
|
655
|
+
for (const f of readdirSync(tasksDir).filter(n => /^task-\d+\.md$/.test(n)).sort()) {
|
|
656
|
+
const taskFile = path.join(tasksDir, f)
|
|
657
|
+
let content
|
|
658
|
+
try {
|
|
659
|
+
content = readFileSync(taskFile, 'utf8')
|
|
660
|
+
} catch {
|
|
661
|
+
skipped++
|
|
662
|
+
continue
|
|
663
|
+
}
|
|
664
|
+
if (/^head_commit:/m.test(content)) continue // 已有(手写或前次落盘)不覆盖
|
|
665
|
+
const repoKey = parseRepo(content)
|
|
666
|
+
if (!repoKey || repoKey === 'main') continue // 主仓 task 锚 meta,无 head_commit 字段
|
|
667
|
+
const entry = ctx && ctx.resolve ? ctx.resolve(repoKey) : null
|
|
668
|
+
if (!entry || !entry.gitDir) {
|
|
669
|
+
skipped++
|
|
670
|
+
reasons.push(`${f}: repo "${repoKey}" 无法解析 gitDir(local.yaml repos 未注册?)`)
|
|
671
|
+
continue
|
|
672
|
+
}
|
|
673
|
+
const head = gitQuiet(entry.gitDir, ['rev-parse', 'HEAD'])
|
|
674
|
+
if (!head) {
|
|
675
|
+
skipped++
|
|
676
|
+
reasons.push(`${f}: git rev-parse HEAD 失败(${entry.gitDir})`)
|
|
677
|
+
continue
|
|
678
|
+
}
|
|
679
|
+
if (writeCommitAnchorToTaskCard(taskFile, 'head_commit', head)) {
|
|
680
|
+
stamped++
|
|
681
|
+
stampedTasks.push(f.replace(/\.md$/, ''))
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return { stamped, stampedTasks, skipped, reasons }
|
|
685
|
+
}
|
|
686
|
+
|
|
615
687
|
/**
|
|
616
688
|
* 为 Wave 生成 prompt(强制子代理执行)
|
|
617
689
|
*
|
|
@@ -768,9 +840,9 @@ ${crossLines}
|
|
|
768
840
|
**派发跨仓 task 前(base 锡点,CLI 已在 prompt 构造时落盘):**
|
|
769
841
|
- 跨仓 task 卡 frontmatter 的 \`base_commit\` 已由 CLI 实时 \`git -C <跨仓仓根> rev-parse HEAD\` 锁定(base 锡点,约束①)。子代理在此 HEAD 上改+commit,不受同 Wave 其他 task 推进 HEAD 影响。
|
|
770
842
|
|
|
771
|
-
**回收跨仓 task
|
|
772
|
-
-
|
|
773
|
-
- review.json 的
|
|
843
|
+
**回收跨仓 task(head 锡点,CLI 自动落盘,勿手写):**
|
|
844
|
+
- 子代理完成 commit 后,正常写 review.json(verdict/notes)并勾选 checkbox 即可。execute \`--done\` 时 CLI 自动 \`git -C <跨仓仓根> rev-parse HEAD\` 写入该 task 卡 \`head_commit:\`(幂等,已存在不覆盖——你若手写了精确锚点则以你的为准)。
|
|
845
|
+
- review.json 的 mechanics 字段(\`base\`/\`head\`/\`changedFiles\`)无需手算:\`base\` 取 task 卡 \`base_commit\`、\`head\` 取 task 卡 \`head_commit\`,写完跑 \`sillyspec backfill-reviews --change <变更名> --adopt\` 一键代填(verdict 保留)。
|
|
774
846
|
|
|
775
847
|
**跨仓 task 子代理 prompt 必须注入:**
|
|
776
848
|
> 该 task 改的是 \`<repo>\` 仓,workdir=\`<跨仓仓根>\`。**直接在该仓主干工作区改+commit(git add + git commit 到该仓主干),不经主仓 worktree、不建分支。** commit 到该仓主干即落盘,apply 阶段对跨仓 task 为 no-op(design §5.4 G1)。
|
|
@@ -963,6 +1035,13 @@ task-XX 对应:{SPEC_ROOT}/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-X
|
|
|
963
1035
|
- 无 _module-map.yaml 时跳过模块匹配,仅按文件清单更新 unmapped 部分
|
|
964
1036
|
这是可选更新(不阻断 execute 完成),但保持 module-impact 与实际变更一致利于 verify 核对与 archive 终审。
|
|
965
1037
|
|
|
1038
|
+
### 知识沉淀(主代理在本 Wave 收尾,与 module-impact.md 同批)
|
|
1039
|
+
本 Wave 各 task 子代理报告里若提到**真正的项目特有坑**(跨变更可复用、未来 agent 可能再次踩到、且不是本任务专属细节),由你在 Wave 收尾统一追加到 \`{SPEC_ROOT}/knowledge/uncategorized.md\`:
|
|
1040
|
+
- 条目格式:\`## <一句话标题>\` + 一段说明(坑的来龙去脉 + 规避/解法),末尾标注来源(task-XX)
|
|
1041
|
+
- 不由各 task 子代理分别写(并行改同一文件会互相覆盖),只由主代理在 Wave 收尾统一追加——与 module-impact.md 同一收尾节奏
|
|
1042
|
+
- 不要为了完成任务而硬凑条目;纯新增/纯样式/单点 bug 修复/临时绕过不写;拿不准时不写,宁缺毋滥
|
|
1043
|
+
- execute 末步「知识库审阅」会检查待确认条目并提示用户归类
|
|
1044
|
+
|
|
966
1045
|
### 完成后
|
|
967
1046
|
1. 为每个后端 router task,扫描变更文件提取 API 端点 artifact:
|
|
968
1047
|
- 在变更文件中搜索所有 router 注册路径(@router.get/post/put/delete)
|
package/src/stages/knowledge.js
CHANGED
|
@@ -232,11 +232,12 @@ export async function cmdValidate(dir, args, opts = {}) {
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
// 4. uncategorized.md
|
|
235
|
+
// 4. uncategorized.md 条目数(多了提示需清理)。条目行契约是 `## <qlId> | <标题>`
|
|
236
|
+
//(quick 自动候选与手工条目同格式),h3 形态一并兼容;原 `^###` 只数 h3 恒为 0
|
|
236
237
|
const uncategorizedPath = join(knowledgeDir, 'uncategorized.md')
|
|
237
238
|
if (existsSync(uncategorizedPath)) {
|
|
238
239
|
const content = readFileSync(uncategorizedPath, 'utf8')
|
|
239
|
-
const entryCount = (content.match(
|
|
240
|
+
const entryCount = (content.match(/^#{2,3}\s+\S/gm) || []).length
|
|
240
241
|
if (entryCount >= 10) {
|
|
241
242
|
warnings.push({
|
|
242
243
|
code: 'too_many_uncategorized',
|
package/src/stages/quick.js
CHANGED
|
@@ -81,7 +81,14 @@ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等
|
|
|
81
81
|
- 本步骤是最后一步,完成后 quick 会话即结束(--change 传递见首步 sessionId 说明)
|
|
82
82
|
|
|
83
83
|
### ⚠️ 结果摘要模板(必填,CLI 会校验结构)
|
|
84
|
-
|
|
84
|
+
|
|
85
|
+
🔧 **推荐四参数形式**(CLI 自动合成结构化模板,杜绝嵌套冒号/缺字段/截断规则踩坑):
|
|
86
|
+
|
|
87
|
+
\`\`\`
|
|
88
|
+
sillyspec run quick --done --change <id> --req "一句话语义化短标题(写「改了什么」)" --cause "为什么这样改(纯新增/配置/样式写「无,纯新增/纯样式」)" --solution "怎么改的" --result "验证情况(测试数 / lint / typecheck / 部署状态)"
|
|
89
|
+
\`\`\`
|
|
90
|
+
|
|
91
|
+
兼容旧形式 \`--output "需求:… 根因:… 方案:… 结果:…"\`(**必须按此结构给全四项**,逐项一句话,不可用「见前述」替代;缺任一项 \`--done\` 会被拒并提示缺失字段,补全后重跑即可不丢进度):
|
|
85
92
|
|
|
86
93
|
\`\`\`
|
|
87
94
|
需求:一句话语义化短标题(写「改了什么」)
|
|
@@ -90,15 +97,13 @@ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等
|
|
|
90
97
|
结果:验证情况(测试数 / lint / typecheck / 部署状态)
|
|
91
98
|
\`\`\`
|
|
92
99
|
|
|
93
|
-
⚠️
|
|
94
|
-
|
|
95
|
-
缺任一项,\`--done\` 会被拒并提示缺失字段,补全后重跑即可(不丢进度)。
|
|
100
|
+
⚠️ **「需求:」(= --req)就是 QUICKLOG 条目标题**——CLI 提取该字段、截到首个标点(,。;,;)、超 80 字再截断。写成完整需求长句会被截成语义不完整的状语前半段(如「在 X 时,…」标题只剩「在 X 时」),事后还得手改。一句话说清改了什么(如「登录限流修复——INCR 计数误清」),需求背景放「根因:/方案:」字段。旧形式正文内避免嵌套全角冒号(标签后紧接嵌套冒号会被拆分判定缺字段)——这正是四参数形式消灭的事故面。
|
|
96
101
|
|
|
97
102
|
### 📎 文件括注 --file-notes(可选,省事后手改文件行)
|
|
98
103
|
若希望 QUICKLOG「文件:」行直接落盘为多行带括注(而非单行逗号路径),\`--done\` 时加 \`--file-notes\`,CLI 落盘为多行 \`- path(括注)\`,免去事后手改:
|
|
99
104
|
|
|
100
105
|
\`\`\`
|
|
101
|
-
sillyspec run quick --done --change <id> --
|
|
106
|
+
sillyspec run quick --done --change <id> --req "…" --cause "…" --solution "…" --result "…" --file-notes "src/a.js::登录端点串限流 || src/b.js::新建 INCR 计数"
|
|
102
107
|
\`\`\`
|
|
103
108
|
|
|
104
109
|
格式:\`path::括注\` 一条,\`||\` 分隔多条(括注可省略只留 \`path\`)。不传则「文件:」行回填审计到的实际改动文件单行(仍可事后手改)。
|
|
@@ -116,7 +121,10 @@ sillyspec run quick --done --change <id> --output "需求:… 根因:… 方
|
|
|
116
121
|
- 禁止使用 \`git add -A\`
|
|
117
122
|
- 不要暂存 quick 开始前就已存在的无关改动
|
|
118
123
|
3. QUICKLOG / tasks.md 记录由 CLI 在本 step 完成时自动收尾(翻「已完成」+ 勾选 task + 轮转),你无需手动建/翻骨架——**QUICKLOG 核对按需**(见下方「QUICKLOG 正文核对」;文件行可用 --file-notes 免手改)
|
|
119
|
-
4.
|
|
124
|
+
4. 只有遇到**真正的项目特有坑**(跨变更可复用、未来 agent 可能再次踩到、且不是本任务专属细节)时,才追加到 \`{SPEC_ROOT}/knowledge/uncategorized.md\`:
|
|
125
|
+
- 格式:\`## <一句话标题>\` + 一段说明(坑的来龙去脉 + 规避/解法),末尾标注来源 \`(<quicklog-id>)\`
|
|
126
|
+
- 不要为了完成任务而硬凑条目;纯新增/纯样式/单点 bug 修复/临时绕过不写
|
|
127
|
+
- 拿不准时不写,宁缺毋滥
|
|
120
128
|
5. 任务比预期复杂 → 建议用完整流程
|
|
121
129
|
|
|
122
130
|
### 模块文档同步
|
package/src/stages/scan.js
CHANGED
|
@@ -154,6 +154,7 @@ export const definition = {
|
|
|
154
154
|
generator: sillyspec-scan
|
|
155
155
|
---
|
|
156
156
|
\`\`\`
|
|
157
|
+
(\`<git-head-short>\` / \`<now-iso-datetime>\` 两占位符由 CLI 注入 prompt 时自动替换为当前 HEAD 短哈希与时间——照 prompt 里已替换的真实值抄写,勿自跑 git rev-parse;漏 header 事后 \`sillyspec scan-fix-headers\` 一键补 author/created_at)
|
|
157
158
|
- 覆盖已有 scan 文档前先读取旧 frontmatter;如果旧文档的 \`source_commit\` 与当前 HEAD 不一致,或旧文档 \`updated_at\` 晚于本次 scan 开始时间,不要覆盖。
|
|
158
159
|
- 如果用户明确传入 \`--force-rescan\`,允许覆盖,但仍需写入新的 \`source_commit\` 和 \`updated_at\`。
|
|
159
160
|
|
package/src/stages/verify.js
CHANGED
|
@@ -197,57 +197,15 @@ export const definition = {
|
|
|
197
197
|
你只需:在 verify-result.md 的「变更风险等级」section 如实记录变更性质;若变更涉及 daemon/backend 跨进程、session/lease/lifecycle 状态机、或部署启动路径,在「Runtime Evidence」section 提供真实集成证据(启动命令、daemon↔backend 调用与日志关键片段、终态断言)。
|
|
198
198
|
- **集成证据是自报告、CLI 不独立运行时核验**:「Runtime Evidence」由你如实填写,CLI 只校验其**字面存在**(是否含关键词),**不会替你启动 daemon、打真实请求或跑迁移**——它是否名副其实取决于你是否实跑过。务必实跑后据实填写,不得凭堆关键词通过门控。(测试套件对账另算:commands.test 由 CLI 真实执行,那块谎报无效。)
|
|
199
199
|
|
|
200
|
-
3.
|
|
201
|
-
4. 给出结论:PASS / PASS WITH NOTES / FAIL
|
|
200
|
+
3. **生成 verify-result.md 骨架(勿从零手写)**:先跑 \`sillyspec verify-probes --change <change-name> --init\`——一条命令生成九章节骨架(已存在不覆盖),其中**探针结果章节已机械预填**(探针 1 的 TODO/FIXME 命中清单、探针 3 的测试覆盖、探针 5 的 API 契约对账表、探针 6 的删除对账三态判定),文件落 \`.sillyspec/changes/<change-name>/verify-result.md\`。你只需把各 \`<!--TODO-->\` 占位替换为语义结论;半语义探针(2 关键词覆盖 / 4 决策追踪)与断言抽查、集成盲区标注由你补在对应 TODO 处
|
|
201
|
+
4. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)——**结论必须写明 PASS/FAIL 字样,留「待填」会被 gate 判不过**
|
|
202
202
|
5. **核对 module-impact.md**(若 changes/<change>/module-impact.md 存在):对照本次实际代码变更(git diff)与 module-impact.md 的模块影响矩阵,发现不一致(漏标受影响模块 / 影响类型错误 / 实际未触碰的模块被误标)则在 verify-result.md 标注。module-impact 由 plan 首版生成、execute 各 Wave 更新,verify 是最后一次核对机会(archive 仅终审不再生成)。这是 advisory 核对(不阻断 verify 完成),但 module-impact 与实际严重背离应记为风险。
|
|
203
203
|
|
|
204
|
-
### verify-result.md
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
PASS / PASS WITH NOTES / FAIL
|
|
210
|
-
|
|
211
|
-
## 任务完成度
|
|
212
|
-
(逐项检查任务的结果)
|
|
213
|
-
|
|
214
|
-
## 设计一致性
|
|
215
|
-
(对照 design.md 的检查结果)
|
|
216
|
-
|
|
217
|
-
## 探针结果
|
|
218
|
-
- 未实现标记扫描:...
|
|
219
|
-
- 关键词覆盖:...
|
|
220
|
-
- 测试覆盖(含断言有效性抽查):...
|
|
221
|
-
- 决策追踪覆盖:...
|
|
222
|
-
- API 契约对账:...
|
|
223
|
-
- 代码删除对账:声明修改/新增却被整文件删除 = 高风险;清单未列出的删除 = 未声明(CLI 会用 git diff --name-status HEAD advisory 复核,仅 warn 不阻断——是否 FAIL blocker 由你诚实判定)
|
|
224
|
-
|
|
225
|
-
## 决策追踪矩阵(如存在 decisions.md)
|
|
226
|
-
| 决策 ID | FR | Task | Evidence | 状态 |
|
|
227
|
-
|---|---|---|---|---|
|
|
228
|
-
| D-001@v1 | FR-01 | task-01 | test/file/path | PASS/PARTIAL/MISSING |
|
|
229
|
-
|
|
230
|
-
## 测试结果
|
|
231
|
-
(测试套件执行结果)
|
|
232
|
-
|
|
233
|
-
## 技术债务
|
|
234
|
-
(TODO/FIXME/HACK 统计)
|
|
235
|
-
|
|
236
|
-
## 变更风险等级
|
|
237
|
-
(自动检测的 change_risk_profile: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical;若 design.md frontmatter 有 risk_level 显式声明,以声明为准并在此注明「显式声明 = <等级>」+ 理由)
|
|
238
|
-
|
|
239
|
-
## Runtime Evidence(integration-critical / deployment-critical 必填;自报告,CLI 仅校验字面存在、不独立核验,须真实执行过)
|
|
240
|
-
按本次变更实际触碰的运行时组件填写(模板是通用形态——长驻进程 / 服务端点 / 生命周期状态机的证据结构;未涉及的行写「不涉及」并注明原因,勿堆砌无关关键词):
|
|
241
|
-
- 长驻进程/服务 启动命令:
|
|
242
|
-
- 服务地址(本变更触碰的端点):
|
|
243
|
-
- 触发核心路径的请求/命令(附关键响应):
|
|
244
|
-
- 进程日志关键片段(证明走了新路径、无降级/回退分支):
|
|
245
|
-
- 生命周期终态断言(本变更涉及的状态机:初始态 → 运行态 → 终态,逐个确认到达):
|
|
246
|
-
- 失败模式排除(针对本变更声明的风险,逐条说明为何未触发):
|
|
247
|
-
|
|
248
|
-
## 代码审查
|
|
249
|
-
(问题列表 + 总体评价)
|
|
250
|
-
\`\`\`
|
|
204
|
+
### verify-result.md 章节结构(骨架已含,占位替换即可)
|
|
205
|
+
|
|
206
|
+
结论(PASS / PASS WITH NOTES / FAIL)→ 任务完成度 → 设计一致性 → 探针结果(已预填)→ 测试结果 → 决策追踪矩阵(\`| 决策 ID | FR | Task | Evidence | 状态 |\`,存在 decisions.md 才留)→ 技术债务 → 变更风险等级 → Runtime Evidence → 代码审查。
|
|
207
|
+
|
|
208
|
+
**Runtime Evidence 行结构**(integration/deployment-critical 必填;按实际触碰的运行时组件写,未涉及的行写「不涉及」勿堆关键词):长驻进程启动命令 / 触碰的服务端点 / 触发核心路径的请求(附关键响应)/ 进程日志关键片段(证明走了新路径)/ 生命周期终态断言(初始态→运行态→终态)/ 失败模式排除(逐条说明为何未触发)。
|
|
251
209
|
|
|
252
210
|
### 输出
|
|
253
211
|
verify-result.md 路径 + 验证报告摘要 + 下一步命令
|
package/src/task-review.js
CHANGED
|
@@ -525,12 +525,15 @@ export function validateTaskReviews(opts) {
|
|
|
525
525
|
|
|
526
526
|
// QUAL-01 收口:原为本地 execFileSync('git')(无 safe.directory、与 git-helper 口径分裂),
|
|
527
527
|
// 现走统一入口;抛错/trim 语义不变,timeout 15000 保留(rev-parse/diff 大仓余量)
|
|
528
|
-
function runGit(gitDir, args) {
|
|
529
|
-
return git(gitDir, args, { timeout: 15000 })
|
|
528
|
+
function runGit(gitDir, args, opts = {}) {
|
|
529
|
+
return git(gitDir, args, { timeout: 15000, ...opts })
|
|
530
530
|
}
|
|
531
531
|
|
|
532
532
|
// porcelain 状态行 → 变更文件路径(对齐 shared.js:343 parsePorcelainPath:`XY path`、引号包裹、
|
|
533
533
|
// rename `R old -> new` 取箭头后)。未 commit 的 working-tree 改动对账用。
|
|
534
|
+
// ⚠️ 入参必须是【未 trim】的原始输出(git(..., {trim:false}))——porcelain 行首空格是状态码
|
|
535
|
+
// 一部分,git() 默认 trim 会把 ' M feature.js' 削成 'M feature.js',slice(3) 再咬掉路径首字符
|
|
536
|
+
// (实测 'eature.js')→ 未提交文件对账全错位(verifyReviewGitEvidence 相交比对误判「完全不相交」)。
|
|
534
537
|
function parsePorcelainFiles(statusOut) {
|
|
535
538
|
const files = []
|
|
536
539
|
for (const line of String(statusOut).split('\n')) {
|
|
@@ -639,7 +642,7 @@ export function verifyReviewGitEvidence(review, gitDir, cache = null) {
|
|
|
639
642
|
try {
|
|
640
643
|
let wtStatus = c.wtStatus.get(gitDir)
|
|
641
644
|
if (wtStatus === undefined) {
|
|
642
|
-
wtStatus = runGit(gitDir, ['status', '--porcelain'])
|
|
645
|
+
wtStatus = runGit(gitDir, ['status', '--porcelain'], { trim: false })
|
|
643
646
|
c.wtStatus.set(gitDir, wtStatus)
|
|
644
647
|
}
|
|
645
648
|
if (wtStatus && wtStatus.trim().length > 0) {
|
|
@@ -983,7 +986,30 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
983
986
|
}
|
|
984
987
|
|
|
985
988
|
// base..head diff 文件集(worktree-aware;null=git 不可用,[]=无 commit diff)
|
|
986
|
-
|
|
989
|
+
let diffFiles = resolveVerifyChangedFiles(cwd, changeName)
|
|
990
|
+
// ⚠️ 并入 worktree 未提交改动(坑 draft-attribution-uncommitted-worktree,2026-08-21 实证):
|
|
991
|
+
// 子代理默认不 commit(execute 复盘 a 同源事实),真实改动全在 worktree working-tree——只看
|
|
992
|
+
// base..HEAD commit diff 时归属恒空,9/9 草稿全成「无归属」靠主代理手写升级。与
|
|
993
|
+
// verifyReviewGitEvidence 的 working-tree 并入口径同源:status --porcelain 文件(排除
|
|
994
|
+
// .sillyspec/ 运行时产物)并入归属集,按 allowed_paths 正常路径归属;review.head 仍为 HEAD
|
|
995
|
+
// commit——evidence 校验对「commit diff 空 + working-tree 有改动」明示不判伪造,语义一致。
|
|
996
|
+
try {
|
|
997
|
+
const wm = new WorktreeManager({ cwd })
|
|
998
|
+
const meta = wm.getMeta(changeName)
|
|
999
|
+
if (meta) {
|
|
1000
|
+
const wtGitDir = (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath))
|
|
1001
|
+
? meta.worktreePath
|
|
1002
|
+
: cwd
|
|
1003
|
+
const wtStatus = runGit(wtGitDir, ['status', '--porcelain'], { trim: false })
|
|
1004
|
+
const wtFiles = parsePorcelainFiles(wtStatus)
|
|
1005
|
+
.map(p => String(p).replace(/\\/g, '/'))
|
|
1006
|
+
.filter(p => p !== '.sillyspec' && !p.startsWith('.sillyspec/'))
|
|
1007
|
+
if (wtFiles.length > 0) {
|
|
1008
|
+
diffFiles = [...new Set([...(Array.isArray(diffFiles) ? diffFiles : []), ...wtFiles])]
|
|
1009
|
+
console.log(`[sillyspec] 草稿归属并入 ${wtFiles.length} 个 worktree 未提交文件(子代理未 commit 的改动按 allowed_paths 归属)`)
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
} catch { /* working-tree 并入失败退回 commit diff 口径(fail-open,不阻断草稿) */ }
|
|
987
1013
|
// 单仓模式(无 ctx):主仓无 diff 即无任何 task 可生成 → 提前返回(原逻辑零回归)。
|
|
988
1014
|
// 有 ctx:主仓无 diff 不阻断——跨仓 task 的 diff 在跨仓仓根独立取(per-task),主仓 task 自然跳过(空 changedFiles)。
|
|
989
1015
|
if (!ctx && (!diffFiles || diffFiles.length === 0)) {
|
|
@@ -1150,6 +1176,194 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
1150
1176
|
return { generated, skipped, noAttribution: noAttributionCount, unattributed, executeRunId }
|
|
1151
1177
|
}
|
|
1152
1178
|
|
|
1179
|
+
/**
|
|
1180
|
+
* adoptTaskReviewMechanics —— 已存在 review.json 的 mechanics 字段一键重算代填(保 verdict)。
|
|
1181
|
+
*
|
|
1182
|
+
* 背景(2026-08-21 agent-手工产出审计项①):review.json 里 schemaVersion/task/base/head/
|
|
1183
|
+
* changedFiles(/repo) 全部可由 CLI 从 git + task 卡机械推导(generateTaskReviewDrafts 已证明),
|
|
1184
|
+
* agent/子代理手算 hash、手抄 diff 清单是纯错误面——而 gate(verifyReviewGitEvidence)校验的
|
|
1185
|
+
* 恰是这些字段。与草稿生成互补:草稿只补「缺失」,adopt 修「已存在但 mechanics 错/缺」——
|
|
1186
|
+
* 保留人工语义字段(specVerdict/qualityVerdict/requiredEvidence/reviewerNotes),只重写 mechanics。
|
|
1187
|
+
*
|
|
1188
|
+
* verdict 保护策略(不猜 agent 本意):
|
|
1189
|
+
* - 合法 verdict(VALID_VERDICTS)原样保留;
|
|
1190
|
+
* - 非法/缺失 verdict 降级 cannot_verify,reviewerNotes 追加标记,requiredEvidence 缺失补占位
|
|
1191
|
+
* (schema 要求 cannot_verify 必带非空 requiredEvidence);
|
|
1192
|
+
* - JSON 解析失败的 review 不救(无法可信保留任何字段),报出来留给 agent 修 JSON 后重跑。
|
|
1193
|
+
*
|
|
1194
|
+
* fail-open:与草稿生成同契约——缺数据返回统计与 reasons,不抛。
|
|
1195
|
+
*
|
|
1196
|
+
* @param {{ changeName: string, cwd: string, platformOpts?: object, ctx?: object|null }} opts
|
|
1197
|
+
* @returns {Promise<{ adopted: number, unchanged: number, skipped: number, missing: number,
|
|
1198
|
+
* downgraded: string[], reasons: string[], executeRunId?: string }>}
|
|
1199
|
+
*/
|
|
1200
|
+
export async function adoptTaskReviewMechanics({ changeName, cwd, platformOpts = {}, ctx = null }) {
|
|
1201
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
1202
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
1203
|
+
const tasksDir = join(specBase, 'changes', changeName, 'tasks')
|
|
1204
|
+
const reasons = []
|
|
1205
|
+
|
|
1206
|
+
if (!changeName) {
|
|
1207
|
+
return { adopted: 0, unchanged: 0, skipped: 0, missing: 0, downgraded: [], reasons: ['无 changeName'] }
|
|
1208
|
+
}
|
|
1209
|
+
if (!existsSync(tasksDir)) {
|
|
1210
|
+
return { adopted: 0, unchanged: 0, skipped: 0, missing: 0, downgraded: [], reasons: ['无 tasks/ 目录'] }
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// executeRunId:与草稿/gate 同源 marker。缺 marker 不新建 run——adopt 修的是当前 run 里
|
|
1214
|
+
// 已存在的 review,marker 丢失说明无「当前 run」语义,无对象可修(gate 对 marker 缺失放行,
|
|
1215
|
+
// 下游 Task Review Gate 兜底,这里同口径不造 run)。
|
|
1216
|
+
const runIdFile = join(runtimeRoot, 'current-execute-run-id-' + changeName)
|
|
1217
|
+
let executeRunId = ''
|
|
1218
|
+
try {
|
|
1219
|
+
if (existsSync(runIdFile)) {
|
|
1220
|
+
const c = readFileSync(runIdFile, 'utf8').trim()
|
|
1221
|
+
if (isValidExecuteRunId(c)) executeRunId = c
|
|
1222
|
+
}
|
|
1223
|
+
} catch {}
|
|
1224
|
+
if (!executeRunId) {
|
|
1225
|
+
return { adopted: 0, unchanged: 0, skipped: 0, missing: 0, downgraded: [], reasons: ['无 execute run marker——无当前 run,adopt 无对象'] }
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// base/head + diff:与 generateTaskReviewDrafts 同源口径(meta 锡点 + worktree rev-parse +
|
|
1229
|
+
// resolveVerifyChangedFiles),保证 adopt 产物与 gate 校验输入一致。
|
|
1230
|
+
const diffFiles = resolveVerifyChangedFiles(cwd, changeName)
|
|
1231
|
+
let base = null
|
|
1232
|
+
let head = null
|
|
1233
|
+
let reviewGitDir = cwd
|
|
1234
|
+
try {
|
|
1235
|
+
const wm = new WorktreeManager({ cwd })
|
|
1236
|
+
const meta = wm.getMeta(changeName)
|
|
1237
|
+
base = (meta && (meta.baselineCommit || meta.baseHash)) || null
|
|
1238
|
+
if (meta && meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
1239
|
+
reviewGitDir = meta.worktreePath
|
|
1240
|
+
}
|
|
1241
|
+
} catch {}
|
|
1242
|
+
if (base) {
|
|
1243
|
+
try {
|
|
1244
|
+
head = runGit(reviewGitDir, ['rev-parse', 'HEAD'])
|
|
1245
|
+
} catch {
|
|
1246
|
+
head = null
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f)).sort()
|
|
1251
|
+
let adopted = 0
|
|
1252
|
+
let unchanged = 0
|
|
1253
|
+
let skipped = 0
|
|
1254
|
+
let missing = 0
|
|
1255
|
+
const downgraded = []
|
|
1256
|
+
|
|
1257
|
+
for (const tf of taskFiles) {
|
|
1258
|
+
const taskId = tf.replace(/\.md$/, '')
|
|
1259
|
+
const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId, 'review.json')
|
|
1260
|
+
if (!existsSync(reviewPath)) {
|
|
1261
|
+
missing++
|
|
1262
|
+
continue
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const existing = readReview(reviewPath)
|
|
1266
|
+
if (existing.parseError) {
|
|
1267
|
+
skipped++
|
|
1268
|
+
reasons.push(`${taskId}: JSON 解析失败,adopt 不猜字段——先修 JSON 再重跑`)
|
|
1269
|
+
continue
|
|
1270
|
+
}
|
|
1271
|
+
const prev = existing.review || {}
|
|
1272
|
+
|
|
1273
|
+
const content = readFileSync(join(tasksDir, tf), 'utf8')
|
|
1274
|
+
const allowedPaths = parseAllowedPaths(content)
|
|
1275
|
+
const taskRepo = normalizeRepoKey(parseRepo(content))
|
|
1276
|
+
const crossEntry = (ctx && taskRepo !== DEFAULT_REPO_KEY) ? ctx.resolve(taskRepo) : null
|
|
1277
|
+
let taskBase = base
|
|
1278
|
+
let taskHead = head
|
|
1279
|
+
let taskChangedFiles
|
|
1280
|
+
let draftRepo
|
|
1281
|
+
|
|
1282
|
+
if (crossEntry) {
|
|
1283
|
+
const baseCommit = parseBaseCommit(content)
|
|
1284
|
+
const headCommit = parseHeadCommit(content)
|
|
1285
|
+
if (!baseCommit || !headCommit) {
|
|
1286
|
+
skipped++
|
|
1287
|
+
reasons.push(`${taskId}: 跨仓 task 卡缺 base/head 锡点,无法 adopt`)
|
|
1288
|
+
continue
|
|
1289
|
+
}
|
|
1290
|
+
taskBase = baseCommit
|
|
1291
|
+
taskHead = headCommit
|
|
1292
|
+
draftRepo = taskRepo
|
|
1293
|
+
let crossDiffFiles = []
|
|
1294
|
+
try {
|
|
1295
|
+
const out = runGit(crossEntry.gitDir, ['diff', '--name-only', `${baseCommit}..${headCommit}`])
|
|
1296
|
+
crossDiffFiles = out ? out.split('\n').filter(Boolean).map(p => p.replace(/\\/g, '/')) : []
|
|
1297
|
+
} catch {
|
|
1298
|
+
skipped++
|
|
1299
|
+
reasons.push(`${taskId}: 跨仓 git diff 失败(${crossEntry.gitDir})`)
|
|
1300
|
+
continue
|
|
1301
|
+
}
|
|
1302
|
+
taskChangedFiles = allowedPaths.length > 0
|
|
1303
|
+
? crossDiffFiles.filter(f => allowedPaths.some(p => pathMatches(f, p)))
|
|
1304
|
+
: crossDiffFiles
|
|
1305
|
+
} else {
|
|
1306
|
+
const mainDiff = Array.isArray(diffFiles) ? diffFiles : []
|
|
1307
|
+
taskChangedFiles = allowedPaths.length > 0
|
|
1308
|
+
? mainDiff.filter(f => allowedPaths.some(p => pathMatches(f, p)))
|
|
1309
|
+
: []
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (!crossEntry && (!taskBase || !taskHead)) {
|
|
1313
|
+
skipped++
|
|
1314
|
+
reasons.push(`${taskId}: 主仓缺 worktree meta base/head,无法 adopt`)
|
|
1315
|
+
continue
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const merged = { ...prev }
|
|
1319
|
+
merged.schemaVersion = REVIEW_SCHEMA_VERSION
|
|
1320
|
+
merged.task = taskId
|
|
1321
|
+
merged.base = taskBase
|
|
1322
|
+
merged.head = taskHead
|
|
1323
|
+
merged.changedFiles = taskChangedFiles
|
|
1324
|
+
if (draftRepo) merged.repo = draftRepo
|
|
1325
|
+
|
|
1326
|
+
let verdictTouched = false
|
|
1327
|
+
for (const v of ['specVerdict', 'qualityVerdict']) {
|
|
1328
|
+
if (!VALID_VERDICTS.includes(merged[v])) {
|
|
1329
|
+
merged[v] = 'cannot_verify'
|
|
1330
|
+
verdictTouched = true
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (verdictTouched) downgraded.push(taskId)
|
|
1334
|
+
let evidenceFilled = false
|
|
1335
|
+
if ((merged.specVerdict === 'cannot_verify' || merged.qualityVerdict === 'cannot_verify')
|
|
1336
|
+
&& (!Array.isArray(merged.requiredEvidence) || merged.requiredEvidence.length === 0)) {
|
|
1337
|
+
merged.requiredEvidence = [`adopt 代填:原 review 缺 requiredEvidence(verdict=${merged.specVerdict}/${merged.qualityVerdict}),待人工复核补证据`]
|
|
1338
|
+
evidenceFilled = true
|
|
1339
|
+
}
|
|
1340
|
+
if (verdictTouched) {
|
|
1341
|
+
merged.reviewerNotes = String(merged.reviewerNotes || '') + (merged.reviewerNotes ? '; ' : '')
|
|
1342
|
+
+ 'adopt: 原 verdict 非法/缺失已降级 cannot_verify(不猜本意)'
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const mechanicsChanged = prev.schemaVersion !== merged.schemaVersion
|
|
1346
|
+
|| prev.base !== merged.base
|
|
1347
|
+
|| prev.head !== merged.head
|
|
1348
|
+
|| JSON.stringify(prev.changedFiles ?? null) !== JSON.stringify(merged.changedFiles)
|
|
1349
|
+
|| prev.repo !== merged.repo
|
|
1350
|
+
|
|
1351
|
+
try {
|
|
1352
|
+
if (mechanicsChanged || verdictTouched || evidenceFilled) {
|
|
1353
|
+
writeFileSync(reviewPath, JSON.stringify(merged, null, 2) + '\n')
|
|
1354
|
+
adopted++
|
|
1355
|
+
} else {
|
|
1356
|
+
unchanged++
|
|
1357
|
+
}
|
|
1358
|
+
} catch (e) {
|
|
1359
|
+
skipped++
|
|
1360
|
+
reasons.push(`${taskId}: 写回失败 ${e.message}`)
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
return { adopted, unchanged, skipped, missing, downgraded, reasons, executeRunId }
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1153
1367
|
/**
|
|
1154
1368
|
* 获取当前(或最新)execute run id
|
|
1155
1369
|
* 从 runtime 目录下查找 execute-runs/ 子目录
|