sillyspec 3.25.5 → 3.25.7
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-brainstorm/SKILL.md +1 -1
- package/.claude/skills/sillyspec-scan/SKILL.md +12 -7
- package/.claude/skills/sillyspec-verify/SKILL.md +2 -2
- package/package.json +1 -1
- package/src/change-list.js +100 -12
- package/src/constants.js +11 -0
- package/src/index.js +1 -1
- package/src/init.js +69 -0
- package/src/quicklog.js +41 -2
- package/src/run/command.js +57 -2
- package/src/run/complete-handlers.js +5 -1
- package/src/run/complete.js +103 -3
- package/src/run/gates.js +37 -0
- package/src/run/quick-audit.js +8 -1
- package/src/run/scan-profile.js +29 -10
- package/src/run/shared.js +175 -5
- package/src/run/stage.js +12 -2
- package/src/scan-postcheck.js +25 -11
- package/src/stage-contract.js +6 -1
- package/src/stage-review.js +11 -5
- package/src/stages/brainstorm.js +9 -14
- package/src/stages/plan-postcheck.js +1 -1
- package/src/stages/plan.js +6 -0
- package/src/stages/quick.js +31 -31
- package/src/stages/scan.js +2 -1
- package/src/stages/verify.js +6 -3
- package/src/task-review.js +159 -0
- package/src/verify-postcheck.js +148 -3
- package/src/worktree-apply.js +34 -29
- package/templates/claude-instruction.md +23 -0
- package/templates/prompts/verify-probes.md +13 -2
- package/CLAUDE.md +0 -40
package/src/scan-postcheck.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'fs'
|
|
9
9
|
import { join, basename } from 'path'
|
|
10
|
-
import { SCAN_STATUS, CHECK_SEVERITY, SCAN_REQUIRED_DOCS } from './constants.js'
|
|
10
|
+
import { SCAN_STATUS, CHECK_SEVERITY, SCAN_REQUIRED_DOCS, SCAN_REQUIRED_DOCS_QUICK } from './constants.js'
|
|
11
11
|
|
|
12
12
|
const REQUIRED_SCAN_DOCS = SCAN_REQUIRED_DOCS
|
|
13
13
|
|
|
@@ -22,8 +22,12 @@ const REQUIRED_SCAN_DOCS = SCAN_REQUIRED_DOCS
|
|
|
22
22
|
* @param {number} [opts.scanMeta.projectCount] - 实际展开的项目数量
|
|
23
23
|
* @returns {{ status: 'success'|'completed_with_warnings'|'failed_post_check', checks: Array<{name, severity, detail}> }}
|
|
24
24
|
*/
|
|
25
|
-
export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {} } ) {
|
|
25
|
+
export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}, scanProfile = null } ) {
|
|
26
26
|
const isPlatform = !!specDir
|
|
27
|
+
// profile 感知:quick 档只要求 4 份核心文档,否则要求完整 7 份。
|
|
28
|
+
// 否则 quick 扫描(--quick 显式 或 小项目自动判定)在平台模式会因缺 INTEGRATIONS/TESTING/CONCERNS 直接判 failed。
|
|
29
|
+
const mode = scanProfile?.mode || null
|
|
30
|
+
const requiredDocs = mode === 'quick' ? SCAN_REQUIRED_DOCS_QUICK : SCAN_REQUIRED_DOCS
|
|
27
31
|
const checks = []
|
|
28
32
|
|
|
29
33
|
if (!isPlatform) {
|
|
@@ -31,10 +35,13 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
31
35
|
const localSpec = join(cwd, '.sillyspec')
|
|
32
36
|
const scanDir = join(localSpec, 'docs', basename(cwd), 'scan')
|
|
33
37
|
|
|
34
|
-
//
|
|
35
|
-
const missing =
|
|
38
|
+
// 检查必需文档是否存在(quick profile 按 4 份清单,否则 7 份)
|
|
39
|
+
const missing = requiredDocs.filter(f => !existsSync(join(scanDir, f)))
|
|
36
40
|
if (missing.length > 0) {
|
|
37
|
-
checks.push({ name: 'missing_docs', severity: CHECK_SEVERITY.WARNING, detail: `缺少 ${missing.length} 份 scan
|
|
41
|
+
checks.push({ name: 'missing_docs', severity: CHECK_SEVERITY.WARNING, detail: `缺少 ${missing.length} 份 scan 文档(${mode || 'full'} profile): ${missing.join(', ')}` })
|
|
42
|
+
}
|
|
43
|
+
if (mode === 'quick') {
|
|
44
|
+
checks.push({ name: 'quick_profile_notice', severity: CHECK_SEVERITY.WARNING, detail: 'quick scan:仅生成 4 份核心文档(PROJECT/ARCHITECTURE/CONVENTIONS/STRUCTURE),深度扫描待补齐' })
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
const hasWarning = checks.some(c => c.severity === 'warning')
|
|
@@ -74,21 +81,24 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
// 2. spec_root
|
|
84
|
+
// 2. spec_root 检查必需文档(quick profile 按 4 份清单,否则 7 份)
|
|
78
85
|
const specScanDir = join(specDir, 'docs', projectName, 'scan')
|
|
79
|
-
const missingDocs =
|
|
86
|
+
const missingDocs = requiredDocs.filter(f => !existsSync(join(specScanDir, f)))
|
|
80
87
|
if (missingDocs.length > 0) {
|
|
81
88
|
checks.push({
|
|
82
|
-
name: missingDocs.length ===
|
|
89
|
+
name: missingDocs.length === requiredDocs.length ? 'all_docs_missing' : 'partial_docs_missing',
|
|
83
90
|
severity: CHECK_SEVERITY.FAILED,
|
|
84
|
-
detail: missingDocs.length ===
|
|
91
|
+
detail: missingDocs.length === requiredDocs.length
|
|
85
92
|
? `spec_root 下无任何 scan 文档(${specScanDir}/),扫描可能未执行`
|
|
86
|
-
: `spec_root 缺少必需文档: ${missingDocs.join(', ')}
|
|
93
|
+
: `spec_root 缺少必需文档: ${missingDocs.join(', ')}(${mode === 'quick' ? 'quick profile 要求 4 份核心文档' : '7 份 scan 文档均为 required'})`
|
|
87
94
|
})
|
|
88
95
|
}
|
|
96
|
+
if (mode === 'quick') {
|
|
97
|
+
checks.push({ name: 'quick_profile_notice', severity: CHECK_SEVERITY.WARNING, detail: 'quick scan:仅生成 4 份核心文档(PROJECT/ARCHITECTURE/CONVENTIONS/STRUCTURE),深度扫描待补齐' })
|
|
98
|
+
}
|
|
89
99
|
|
|
90
100
|
// 3. 检查文档 header(author / created_at)— 只看文件头部,避免正文出现同名词被误判
|
|
91
|
-
const existingDocs =
|
|
101
|
+
const existingDocs = requiredDocs.filter(f => existsSync(join(specScanDir, f)))
|
|
92
102
|
const docsMissingHeader = []
|
|
93
103
|
for (const doc of existingDocs) {
|
|
94
104
|
const content = readFileSync(join(specScanDir, doc), 'utf8')
|
|
@@ -310,6 +320,10 @@ export function formatStructuredResult(result, meta = {}) {
|
|
|
310
320
|
else if (check.name === 'docs_missing_header') {
|
|
311
321
|
structured.failure_categories.quality_warnings.push(entry)
|
|
312
322
|
}
|
|
323
|
+
// quick profile 说明(informational warning,非缺陷)
|
|
324
|
+
else if (check.name === 'quick_profile_notice') {
|
|
325
|
+
structured.failure_categories.quality_warnings.push(entry)
|
|
326
|
+
}
|
|
313
327
|
// 兜底:归入 violations
|
|
314
328
|
else {
|
|
315
329
|
structured.failure_categories.violations.push(entry)
|
package/src/stage-contract.js
CHANGED
|
@@ -66,9 +66,14 @@ export function validateChangeExists(specBase, stageName, changeName) {
|
|
|
66
66
|
if (stageName === 'archive') {
|
|
67
67
|
const archiveRoot = join(specBase, 'changes', 'archive')
|
|
68
68
|
if (existsSync(archiveRoot)) {
|
|
69
|
+
// 归档目录名恒为 <归档日>-<纯描述>(archiveDestDirName 写入时已 strip 源名前导日期)。
|
|
70
|
+
// 校验侧同样 strip changeName 前导日期再比 —— 否则「写 strip、读拿完整源名」语义不一致,
|
|
71
|
+
// 会让带日期前缀的源名(brainstorm 约定,如 2026-07-29-sidebar-menu-restructure)在 step5
|
|
72
|
+
// 被前置校验误拦(坑 archive-step5-leading-date-change-name)。
|
|
73
|
+
const stripped = String(changeName).replace(/^\d{4}-\d{2}-\d{2}-/, '')
|
|
69
74
|
const archived = readdirSync(archiveRoot).find(d => {
|
|
70
75
|
const m = d.match(/^\d{4}-\d{2}-\d{2}-(.+)$/)
|
|
71
|
-
return m && m[1] ===
|
|
76
|
+
return m && m[1] === stripped
|
|
72
77
|
})
|
|
73
78
|
if (archived) return null
|
|
74
79
|
}
|
package/src/stage-review.js
CHANGED
|
@@ -41,7 +41,7 @@ const STAGE_REVIEW_TYPE = { brainstorm: 'design', plan: 'plan', propose: 'propos
|
|
|
41
41
|
*/
|
|
42
42
|
export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier } = {}) {
|
|
43
43
|
if (tier === 'self') {
|
|
44
|
-
return '> review.json 契约:tier=self(
|
|
44
|
+
return '> review.json 契约:tier=self(此刻 design.md 变更文件数 ≤3,当前 agent 自审即可)。⚠️ 注意:本判定基于此刻 design.md 快照——Stage Review Gate 会以 --done 时刻的 design.md 重新判定,若 design 后续扩大到 >3 文件,tier 将升级为 independent 并硬要求独立审查子代理产出 review.json。以 gate 实际校验结果为准(FAILED 即 independent 需补 review.json),勿据本提示提前断言无需 review.json。'
|
|
45
45
|
}
|
|
46
46
|
const reviewType = STAGE_REVIEW_TYPE[stage] || 'design'
|
|
47
47
|
const mainDoc = STAGE_MAIN_DOC[stage] || 'design.md'
|
|
@@ -195,10 +195,16 @@ export function verifyStageReviewDocHash(review, searchDirs) {
|
|
|
195
195
|
|
|
196
196
|
for (const base of searchDirs || []) {
|
|
197
197
|
const abs = join(base, primaryRel)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
|
|
198
|
+
if (!existsSync(abs)) continue
|
|
199
|
+
// 找到主文档,比对 hash。容忍「原始字节」与「LF 规范化」两种口径:Windows 下 CRLF/LF 在
|
|
200
|
+
// git add / eol 规范化前后字节会漂移,同一文件 sha256 偶发不一致(坑 worktree-execute-apply-friction
|
|
201
|
+
// 坑3)——agent 写的 docHash(原始 sha256sum)与 gate 重算只要任一口径匹配即认可,消除 CRLF
|
|
202
|
+
// 漂移导致的反复「疑似伪造」误报。两口径都对不上仍判不匹配(防伪造能力不降级)。
|
|
203
|
+
const content = readFileSync(abs, 'utf8')
|
|
204
|
+
const actualRaw = createHash('sha256').update(content).digest('hex')
|
|
205
|
+
const actualLf = createHash('sha256').update(content.replace(/\r\n/g, '\n')).digest('hex')
|
|
206
|
+
const claimed = String(review.docHash).toLowerCase()
|
|
207
|
+
if (claimed !== actualRaw.toLowerCase() && claimed !== actualLf.toLowerCase()) {
|
|
202
208
|
errors.push(
|
|
203
209
|
`docHash 与主审查文档 ${primaryRel} 的实际内容不匹配。` +
|
|
204
210
|
`若刚改过该文档(如 design 修订后忘重算),重算并更新 review.json 的 docHash 字段:` +
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -60,7 +60,7 @@ export const definition = {
|
|
|
60
60
|
加载上下文后,用需求描述 + 模块上下文**粗判**本次变更规模:
|
|
61
61
|
- **明显 small**(满足:预计改动 ≤ 2 个文件、单模块、无 schema/API/状态机/权限变更;或属于改文案/修 bug/样式调整/配置微调等纯执行类)→ 输出:「此变更规模较小,建议直接走 quick 流程」+ 一句依据,给出建议命令 \`sillyspec run quick "<需求>"\`。用户同意则本阶段可在此收尾(\`--done\` 并提示转 quick),不必继续走完整设计流程。
|
|
62
62
|
- **拿不准或明显 large**(涉及多模块、schema、状态流转、新架构等)→ 不要提示 quick,继续进入下一步「对话式探索与需求澄清」。
|
|
63
|
-
-
|
|
63
|
+
- 这是**粗判**,只为让明显的小变更免走完整设计流程;不确定就继续,后续「生成规范文件」步骤会基于 design.md 文件清单做精判兜底。
|
|
64
64
|
|
|
65
65
|
### 输出
|
|
66
66
|
项目现状理解摘要(3-5 句话,关键约定和架构决策)+ 可能涉及的模块列表 + 本次需求所属子项目 + (如命中)quick 建议
|
|
@@ -442,11 +442,10 @@ status: passed | needs-user-input | blocked | skipped
|
|
|
442
442
|
optional: false
|
|
443
443
|
},
|
|
444
444
|
{
|
|
445
|
-
name: '
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
prompt: `用户确认设计方案,按变更规模生成规范文件并给出实现路径建议。
|
|
445
|
+
name: '生成规范文件',
|
|
446
|
+
// 重命名自「用户确认并生成规范文件」(去确认门控:设计已在「分段展示设计」步确认过,末步再问纯冗余),老进度 completed 由迁移逻辑承接
|
|
447
|
+
migratedFrom: ['用户确认并生成规范文件'],
|
|
448
|
+
prompt: `按变更规模生成规范文件并给出实现路径建议(设计已在前置步骤确认过,本步不再暂停等确认,生成后展示摘要即可)。
|
|
450
449
|
|
|
451
450
|
### 规模评估(展示前先做)
|
|
452
451
|
读取 design.md 的「文件变更清单」,判断本次变更规模:
|
|
@@ -456,13 +455,13 @@ status: passed | needs-user-input | blocked | skipped
|
|
|
456
455
|
(注:早期「加载项目上下文」步骤已做过一次粗判并可能建议过走 quick;此处基于 design.md 文件清单做精判兜底。)
|
|
457
456
|
|
|
458
457
|
### 操作
|
|
459
|
-
1.
|
|
460
|
-
2. 暂停等待用户选择:✅ 确认 / ✏️ 修改 / ❌ 推翻重来
|
|
461
|
-
3. 确认后,**按规模生成规范文件**:
|
|
458
|
+
1. **按规模生成规范文件**:
|
|
462
459
|
- **scale=large**:在 \`{SPEC_ROOT}/changes/<change-name>/\` 下生成完整四件套(design.md / decisions.md 可选 / proposal.md / requirements.md / tasks.md),实现路径 → \`sillyspec run plan --change <变更名>\`
|
|
463
460
|
- **scale=small**:只生成/补全 design.md(proposal/requirements/tasks 对 quick 无用,不生成),实现路径 → \`sillyspec run quick --linked-changes <变更名>\`
|
|
464
461
|
- 两种规模都执行 \`git add .sillyspec/\` — 暂存规范文件(不要 commit,由用户通过统一提交工具处理)。**平台模式跳过 git add**(specRoot 不在 sourceRoot 的 git repo 内)
|
|
465
462
|
|
|
463
|
+
2. 生成完成后展示 design.md 摘要 + **规模评估结果(small/large + 一句依据)** + 实现路径建议,告知用户“如有异议直接说,可修改文件、改 scale 或 --reopen 回退”(不暂停,展示完直接 --done)
|
|
464
|
+
|
|
466
465
|
所有规范文件头部必须包含 YAML frontmatter:
|
|
467
466
|
\`\`\`yaml
|
|
468
467
|
---
|
|
@@ -553,15 +552,11 @@ Then 期望结果
|
|
|
553
552
|
4. \`git add .sillyspec/\` — 暂存所有新增文件(不要 commit,由用户通过统一提交工具处理)。**平台模式跳过 git add**(specRoot 不在 sourceRoot 的 git repo 内)
|
|
554
553
|
5. 后续变更包的骨架文件同样必须包含 \`author: <git-user>\` 和 \`created_at: <now-datetime>\`
|
|
555
554
|
|
|
556
|
-
### 铁律 — 必须等待用户最终确认
|
|
557
|
-
- 展示 design.md 摘要后暂停等用户确认,**不要自己说"用户已确认"然后生成文件**(命令由 CLI 在下方注入)
|
|
558
|
-
- 只有用户明确确认(--answer "确认")后才生成规范文件**
|
|
559
|
-
|
|
560
555
|
### 输出
|
|
561
556
|
所有规范文件路径(含后续变更包目录列表)
|
|
562
557
|
|
|
563
558
|
### 注意
|
|
564
|
-
-
|
|
559
|
+
- 本步不再等待用户确认,生成完成后直接 --done;用户事后提出修改/推翻时,修改对应文件或用 --reopen 回退重做
|
|
565
560
|
- 禁止自动 commit
|
|
566
561
|
- 推翻重来回到「对话式探索与需求澄清」步骤
|
|
567
562
|
- 表名/字段名/类名必须来自真实代码或标注"新增"
|
|
@@ -64,7 +64,7 @@ function parseTaskId(content, filename) {
|
|
|
64
64
|
* @param {string} content
|
|
65
65
|
* @returns {string[]}
|
|
66
66
|
*/
|
|
67
|
-
function parseAllowedPaths(content) {
|
|
67
|
+
export function parseAllowedPaths(content) {
|
|
68
68
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
69
69
|
if (!fmMatch) return []
|
|
70
70
|
const fm = fmMatch[1]
|
package/src/stages/plan.js
CHANGED
|
@@ -322,6 +322,7 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
|
322
322
|
- [ ] 不存在 P0/P1 unresolved blocker 残留
|
|
323
323
|
- [ ] 没有实现细节泄漏到 plan.md(接口签名/代码示例应在 tasks/task-NN.md)
|
|
324
324
|
- [ ] 关键路径与 Wave 依赖合理(无循环依赖、无遗漏前置)
|
|
325
|
+
- [ ] 连带测试归属:改共享/被多 task 依赖源文件的 task 是否在 related_tests 声明了会失效的既有测试、且路径在 allowed_paths 内(或由独立测试 task 覆盖)?(漏声明 = execute 阶段测试债、主代理事后兜底)
|
|
325
326
|
|
|
326
327
|
### tier=independent 时:启动 plan-review 子代理
|
|
327
328
|
用 Agent tool 启动子代理(subagent_type: general),prompt 要点:
|
|
@@ -401,6 +402,9 @@ verify:
|
|
|
401
402
|
constraints:
|
|
402
403
|
- 边界约束 1(如:不加测试)
|
|
403
404
|
- 边界约束 2(如:不修改传入参数)
|
|
405
|
+
related_tests: # 可选。仅当本 task 改共享/被多 task 依赖的源文件时填
|
|
406
|
+
- path: frontend/src/lib/errors.test.ts # 因本次改动断言失效的既有测试文件
|
|
407
|
+
reason: 旧断言假设单例,改归属键后需同步
|
|
404
408
|
---
|
|
405
409
|
|
|
406
410
|
TaskCard 格式规则(必须严格遵守):
|
|
@@ -422,6 +426,7 @@ TaskCard 格式规则(必须严格遵守):
|
|
|
422
426
|
- provides / expects_from 是可选字段:仅当跨 task 契约(一个 task 的接口/DTO/响应被另一个 task 消费)时才填,单 task 或无对外接口场景留空即可
|
|
423
427
|
- 填写后 plan-postcheck 会做硬对账:consumer 的每个 expects_from[provider].needs 字段必须在对应 provider 的 provides.fields 里,否则 plan 阶段阻断(不进入 execute)
|
|
424
428
|
- 不要把内部实现字段塞进 provides;只暴露给其他 task 的对外契约形状
|
|
429
|
+
- related_tests 是可选字段:仅当本 task 改共享源文件(被既有测试引用、或被多 task 依赖)时填,列出会因本次改动断言失效的既有测试文件 + 为何失效。关键:这些测试路径必须同时写进本 task 的 allowed_paths(否则子代理被 allowed_paths 铁律锁死、改不了测试 → 退化成主代理事后兜底);若测试由独立的测试 task 负责,则在该测试 task 的 allowed_paths 覆盖、本字段留空
|
|
425
430
|
- 如果存在 decisions.md,无法覆盖的 D-xxx@vN 在 constraints 中标注
|
|
426
431
|
- **保存前格式自检**(plan-postcheck 会硬校验,不通过到 Step 4 会批量报错,先在这里逐条自查):
|
|
427
432
|
- frontmatter 字段齐全:id、title、title_zh、author、created_at、priority、depends_on、blocks、allowed_paths、goal、implementation、acceptance、verify、constraints
|
|
@@ -464,6 +469,7 @@ ${subagentPrompts}
|
|
|
464
469
|
- allowed_paths 有无冲突
|
|
465
470
|
- depends_on 与 plan.md Wave 分组是否一致
|
|
466
471
|
- provides/expects_from 契约自洽:每个 expects_from[provider].needs 字段都在该 provider task 的 provides.fields 里(plan-postcheck 会硬校验,这里提前自查)
|
|
472
|
+
- related_tests(若填)的测试路径是否都在本 task 或某 task 的 allowed_paths 内(否则子代理无权改 → execute 测试债、主代理事后兜底)
|
|
467
473
|
- 如发现矛盾,列出问题清单,不要自动修复`
|
|
468
474
|
|
|
469
475
|
return {
|
package/src/stages/quick.js
CHANGED
|
@@ -8,8 +8,8 @@ export const definition = {
|
|
|
8
8
|
name: '理解任务',
|
|
9
9
|
prompt: `解析任务参数,加载项目上下文。
|
|
10
10
|
|
|
11
|
-
### 📌 本 quick 会话 sessionId: <quick-session-id>
|
|
12
|
-
- CLI 是短进程,run 与 done 是独立进程,sessionId 靠 --change 跨进程传递
|
|
11
|
+
### 📌 本 quick 会话 sessionId: <quick-session-id>
|
|
12
|
+
- CLI 是短进程,run 与 done 是独立进程,sessionId 靠 --change 跨进程传递
|
|
13
13
|
- **完成每个 step 必须带 --change <quick-session-id>**(命令由 CLI 在下方注入);多会话并发时不带会命中他者会话状态(不带时 fallback 读 current-quick-run-id,单会话兼容;多会话不可靠)
|
|
14
14
|
|
|
15
15
|
### 操作
|
|
@@ -26,15 +26,15 @@ export const definition = {
|
|
|
26
26
|
9. 根据任务描述初步判断可能涉及的模块
|
|
27
27
|
10. 读取匹配到的 \`{SPEC_ROOT}/docs/<project>/modules/<module>.md\`
|
|
28
28
|
|
|
29
|
-
### QUICKLOG 记录(CLI 已接管,无需你手动创建)
|
|
30
|
-
本 quick 会话的 ql-ID 由 CLI 在启动时分配,已注入为 \`<quicklog-id>\`。CLI 同时已自动:
|
|
31
|
-
- 在 \`{SPEC_ROOT}/quicklog/QUICKLOG-<git-user>.md\` 写入「进行中」条目(含关联变更与预估文件)
|
|
32
|
-
- 对每个关联变更 \`<c>\`:在 \`{SPEC_ROOT}/changes/<c>/tasks.md\` 追加未勾选 task \`- [ ] <quicklog-id> <任务描述>\`
|
|
33
|
-
|
|
34
|
-
**你不要创建或修改任何 QUICKLOG 的条目骨架 / tasks.md 记录**——ql-ID 分配、「进行中」条目、状态翻转、task 勾选、轮转全由 CLI 接管,你无需手写。<quicklog-id> 可用于 design.md / plan.md / archive / 模块变更索引引用。
|
|
35
|
-
|
|
36
|
-
> ⚠️ 例外:step 3 --done 之后,CLI 落盘的 QUICKLOG 只是简版骨架,**你必须手动精修正文成丰富格式**(标题语义化 + 文件多行带括注 + 四段充实),详见 step 3「QUICKLOG 正文精修」。这是 quick 交付质量的一部分,不可跳过。
|
|
37
|
-
|
|
29
|
+
### QUICKLOG 记录(CLI 已接管,无需你手动创建)
|
|
30
|
+
本 quick 会话的 ql-ID 由 CLI 在启动时分配,已注入为 \`<quicklog-id>\`。CLI 同时已自动:
|
|
31
|
+
- 在 \`{SPEC_ROOT}/quicklog/QUICKLOG-<git-user>.md\` 写入「进行中」条目(含关联变更与预估文件)
|
|
32
|
+
- 对每个关联变更 \`<c>\`:在 \`{SPEC_ROOT}/changes/<c>/tasks.md\` 追加未勾选 task \`- [ ] <quicklog-id> <任务描述>\`
|
|
33
|
+
|
|
34
|
+
**你不要创建或修改任何 QUICKLOG 的条目骨架 / tasks.md 记录**——ql-ID 分配、「进行中」条目、状态翻转、task 勾选、轮转全由 CLI 接管,你无需手写。<quicklog-id> 可用于 design.md / plan.md / archive / 模块变更索引引用。
|
|
35
|
+
|
|
36
|
+
> ⚠️ 例外:step 3 --done 之后,CLI 落盘的 QUICKLOG 只是简版骨架,**你必须手动精修正文成丰富格式**(标题语义化 + 文件多行带括注 + 四段充实),详见 step 3「QUICKLOG 正文精修」。这是 quick 交付质量的一部分,不可跳过。
|
|
37
|
+
|
|
38
38
|
### 输出
|
|
39
39
|
任务理解 + 上下文摘要`,
|
|
40
40
|
outputHint: '任务理解',
|
|
@@ -42,16 +42,16 @@ export const definition = {
|
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
name: '实现并验证',
|
|
45
|
-
prompt: `直接在主工作区实现任务。
|
|
46
|
-
|
|
47
|
-
### 边界声明(quick 不校验 design.md)
|
|
48
|
-
关联变更的 design.md 仅供理解意图,**不作为验收基准**——quick 不 enforce design 一致性。需要 design 一致性保证 → 走完整流程(plan + verify)。发现 design 本身有错时,可 Reverse Sync 回写(见下方铁律)。
|
|
49
|
-
|
|
45
|
+
prompt: `直接在主工作区实现任务。
|
|
46
|
+
|
|
47
|
+
### 边界声明(quick 不校验 design.md)
|
|
48
|
+
关联变更的 design.md 仅供理解意图,**不作为验收基准**——quick 不 enforce design 一致性。需要 design 一致性保证 → 走完整流程(plan + verify)。发现 design 本身有错时,可 Reverse Sync 回写(见下方铁律)。
|
|
49
|
+
|
|
50
50
|
### 操作
|
|
51
51
|
1. 先读后写:调用已有方法前 \`cat\` 源文件确认签名,\`grep\` 确认方法存在
|
|
52
52
|
2. 写代码完成任务
|
|
53
53
|
3. 如涉及逻辑变更,建议写单元测试验证(不强制,纯配置/文档/小改动可跳过)
|
|
54
|
-
4.
|
|
54
|
+
4. **不要做无谓的全量编译/构建。** 但——跑针对性的单元测试始终允许且鼓励(见 step3);若本任务就是修测试/构建/基础设施本身,跑测试/构建即验收,应当跑,不算"编译"
|
|
55
55
|
|
|
56
56
|
### 输出
|
|
57
57
|
实现摘要 + 修改文件列表
|
|
@@ -67,7 +67,7 @@ export const definition = {
|
|
|
67
67
|
name: '暂存和更新记录',
|
|
68
68
|
prompt: `Git 暂存并更新任务记录。
|
|
69
69
|
|
|
70
|
-
### 📌 收尾确认
|
|
70
|
+
### 📌 收尾确认
|
|
71
71
|
- 本步骤是最后一步,完成后 quick 会话即结束(--change 传递见首步 sessionId 说明)
|
|
72
72
|
|
|
73
73
|
### ⚠️ 结果摘要模板(必填,CLI 会校验结构)
|
|
@@ -102,19 +102,19 @@ export const definition = {
|
|
|
102
102
|
- 使用 \`git add -- <module-doc>\` 暂存更新的模块文件
|
|
103
103
|
9. 未命中任何模块 → 跳过,不做额外操作
|
|
104
104
|
|
|
105
|
-
### 输出
|
|
106
|
-
暂存确认 + 记录路径 + 模块文档同步结果(如有)
|
|
107
|
-
|
|
108
|
-
### 📝 QUICKLOG 正文精修(--done 之后必做,不可跳过)
|
|
109
|
-
sillyspec run quick --done 成功后,CLI 已在 {SPEC_ROOT}/quicklog/QUICKLOG-<git-user>.md 落盘本次 <quicklog-id> 条目的简版骨架:状态翻「已完成」、「文件:」是单行逗号路径、正文是 --output 的四字段。骨架是机械产物不达标,你必须立即编辑该条目手动精修成丰富格式,三项必改:
|
|
110
|
-
|
|
111
|
-
1. 标题:把 ## <quicklog-id> | <时间> | <标题> 中的 <标题> 改为真实需求摘要(如「登录加 IP 限流(5 次/分)+ 失败 3 次滑块验证」)。禁止保留 CLI 兜底的 (quick 任务) 或一句话笼统描述。
|
|
112
|
-
2. 文件:把 CLI 写的单行「文件:a.js, b.js」改写为多行 bullet,每个文件带关键改动括注;禁止「(见实际改动)」或无括注纯路径单行。示例:
|
|
113
|
-
文件:
|
|
114
|
-
- backend/app/modules/auth/router.py(登录端点串 check_rate_limit→assert_captcha_if_needed)
|
|
115
|
-
- backend/app/modules/auth/captcha_service.py(新建:限流 INCR + 失败计数 + Pillow 滑块生成/校验)
|
|
116
|
-
3. 正文:核对 需求: 根因: 方案: 结果: 四段逐项充实(--output 已写、CLI 原样插入,按需扩写),不允许只留一段「结果:」。
|
|
117
|
-
|
|
105
|
+
### 输出
|
|
106
|
+
暂存确认 + 记录路径 + 模块文档同步结果(如有)
|
|
107
|
+
|
|
108
|
+
### 📝 QUICKLOG 正文精修(--done 之后必做,不可跳过)
|
|
109
|
+
sillyspec run quick --done 成功后,CLI 已在 {SPEC_ROOT}/quicklog/QUICKLOG-<git-user>.md 落盘本次 <quicklog-id> 条目的简版骨架:状态翻「已完成」、「文件:」是单行逗号路径、正文是 --output 的四字段。骨架是机械产物不达标,你必须立即编辑该条目手动精修成丰富格式,三项必改:
|
|
110
|
+
|
|
111
|
+
1. 标题:把 ## <quicklog-id> | <时间> | <标题> 中的 <标题> 改为真实需求摘要(如「登录加 IP 限流(5 次/分)+ 失败 3 次滑块验证」)。禁止保留 CLI 兜底的 (quick 任务) 或一句话笼统描述。
|
|
112
|
+
2. 文件:把 CLI 写的单行「文件:a.js, b.js」改写为多行 bullet,每个文件带关键改动括注;禁止「(见实际改动)」或无括注纯路径单行。示例:
|
|
113
|
+
文件:
|
|
114
|
+
- backend/app/modules/auth/router.py(登录端点串 check_rate_limit→assert_captcha_if_needed)
|
|
115
|
+
- backend/app/modules/auth/captcha_service.py(新建:限流 INCR + 失败计数 + Pillow 滑块生成/校验)
|
|
116
|
+
3. 正文:核对 需求: 根因: 方案: 结果: 四段逐项充实(--output 已写、CLI 原样插入,按需扩写),不允许只留一段「结果:」。
|
|
117
|
+
|
|
118
118
|
一条 quick = 一条独立 ql 条目;精修只动本次 <quicklog-id> 条目,不追加到旧条目。QUICKLOG 在 .sillyspec/(gitignore),精修不影响 --done 已通过的边界审计。`,
|
|
119
119
|
outputHint: '暂存和记录确认',
|
|
120
120
|
optional: false
|
package/src/stages/scan.js
CHANGED
|
@@ -139,7 +139,8 @@ export const definition = {
|
|
|
139
139
|
对每个项目分别执行(将 \`<project>\` 替换为实际项目名)
|
|
140
140
|
4. 如果检查报告有失败项,按报告中的角色和文件重试失败的部分
|
|
141
141
|
|
|
142
|
-
### 覆盖保护
|
|
142
|
+
### 覆盖保护
|
|
143
|
+
- **scan_depth: quick 的浅层文档允许覆盖升级**:读取旧 frontmatter 时,若旧文档含 scan_depth: quick(由 --quick 快速接入生成的浅层版本),即使其 source_commit 与当前 HEAD 一致、updated_at 未变,也允许覆盖重写——深度扫描的目的就是把它升级为完整文档。
|
|
143
144
|
- 生成每份 scan 文档时,frontmatter 必须包含:
|
|
144
145
|
\`\`\`yaml
|
|
145
146
|
---
|
package/src/stages/verify.js
CHANGED
|
@@ -118,8 +118,8 @@ export const definition = {
|
|
|
118
118
|
{{include: verify-probes}}
|
|
119
119
|
|
|
120
120
|
### 探针结果处理
|
|
121
|
-
-
|
|
122
|
-
-
|
|
121
|
+
- 将六个探针的结果汇总为「探针报告」
|
|
122
|
+
- 如果探针发现问题(未实现标记、关键词缺失、测试缺失、决策未闭环、API 契约缺口、代码删除对账),在最终验证报告中明确标注
|
|
123
123
|
- 探针发现的问题不等同于验证失败,但必须在报告中列出
|
|
124
124
|
|
|
125
125
|
### 设计一致性检查
|
|
@@ -191,6 +191,7 @@ export const definition = {
|
|
|
191
191
|
- **误判时的诚实出路(豁免级)**:在 design.md 顶部 frontmatter 加一行 \`risk_level: <真实等级>\`(doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical),CLI 会以声明为准覆盖关键词判级。声明后若是 unit-sufficient 等豁免级,PASS WITH NOTES 不再被强制拦;但结论为 PASS 仍需对应证据。
|
|
192
192
|
- **留痕要求(防逃逸)**:用了显式声明,必须在本报告「变更风险等级」section 写明「risk_level 由 design frontmatter 显式声明 = <等级>(覆盖关键词判级)」+ 一句话理由,让豁免可审计。
|
|
193
193
|
你只需:在 verify-result.md 的「变更风险等级」section 如实记录变更性质;若变更涉及 daemon/backend 跨进程、session/lease/lifecycle 状态机、或部署启动路径,在「Runtime Evidence」section 提供真实集成证据(启动命令、daemon↔backend 调用与日志关键片段、终态断言)。
|
|
194
|
+
- **集成证据是自报告、CLI 不独立运行时核验**:「Runtime Evidence」由你如实填写,CLI 只校验其**字面存在**(是否含关键词),**不会替你启动 daemon、打真实请求或跑迁移**——它是否名副其实取决于你是否实跑过。务必实跑后据实填写,不得凭堆关键词通过门控。(测试套件对账另算:commands.test 由 CLI 真实执行,那块谎报无效。)
|
|
194
195
|
|
|
195
196
|
3. 生成 verify-result.md 文件,保存到 \`.sillyspec/changes/<change-name>/verify-result.md\`
|
|
196
197
|
4. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)
|
|
@@ -213,6 +214,8 @@ PASS / PASS WITH NOTES / FAIL
|
|
|
213
214
|
- 关键词覆盖:...
|
|
214
215
|
- 测试覆盖:...
|
|
215
216
|
- 决策追踪覆盖:...
|
|
217
|
+
- API 契约对账:...
|
|
218
|
+
- 代码删除对账:声明修改/新增却被整文件删除 = 高风险;清单未列出的删除 = 未声明(CLI 会用 git diff --name-status HEAD 独立复核)
|
|
216
219
|
|
|
217
220
|
## 决策追踪矩阵(如存在 decisions.md)
|
|
218
221
|
| 决策 ID | FR | Task | Evidence | 状态 |
|
|
@@ -228,7 +231,7 @@ PASS / PASS WITH NOTES / FAIL
|
|
|
228
231
|
## 变更风险等级
|
|
229
232
|
(自动检测的 change_risk_profile: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical;若 design.md frontmatter 有 risk_level 显式声明,以声明为准并在此注明「显式声明 = <等级>」+ 理由)
|
|
230
233
|
|
|
231
|
-
## Runtime Evidence(integration-critical / deployment-critical
|
|
234
|
+
## Runtime Evidence(integration-critical / deployment-critical 必填;自报告,CLI 仅校验字面存在、不独立核验,须真实执行过)
|
|
232
235
|
- daemon 启动命令:
|
|
233
236
|
- backend 地址:
|
|
234
237
|
- 创建 session / 调用核心 API 的请求:
|
package/src/task-review.js
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
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
|
+
import { pathMatches } from './change-list.js'
|
|
15
|
+
import { parseAllowedPaths } from './stages/plan-postcheck.js'
|
|
16
|
+
import { resolveVerifyChangedFiles } from './verify-postcheck.js'
|
|
17
|
+
import { WorktreeManager } from './worktree.js'
|
|
14
18
|
|
|
15
19
|
// ── review.json schema version ──
|
|
16
20
|
export const REVIEW_SCHEMA_VERSION = 1
|
|
@@ -258,6 +262,34 @@ export function readReview(reviewPath) {
|
|
|
258
262
|
return { ok: true, review: parsed, errors: [] }
|
|
259
263
|
}
|
|
260
264
|
|
|
265
|
+
/**
|
|
266
|
+
* 校验 plan.md 里所有「已勾选 [x]」task 的 review.json 是否 schema 完整(坑 review-json-field-gap)。
|
|
267
|
+
*
|
|
268
|
+
* Task Review Gate(validateTaskReviews)只在 execute 整阶段完成时跑,单 task --done 不校验 →
|
|
269
|
+
* 子代理勾了 checkbox 却漏写/漏字段 review.json,要到收尾才暴露,用户被迫事后批量补。本函数供
|
|
270
|
+
* enforceReviewJsonGate 在每次 execute --done 调用:已勾 [x] = 声称完成 = 必有完整 review.json;
|
|
271
|
+
* 未勾 task 不校验(还没做)。readReview 已区分 missing/parseError/schemaError,这里只聚合。
|
|
272
|
+
*
|
|
273
|
+
* @param {{ planContent: string, runtimeRoot: string, executeRunId: string }} opts
|
|
274
|
+
* @returns {{ ok: boolean, failures: Array<{ taskId:string, reviewPath:string, kind:string, errors:string[] }> }}
|
|
275
|
+
* kind ∈ missing(review.json 不存在)/ parseError(JSON 坏)/ schemaError(字段缺)
|
|
276
|
+
*/
|
|
277
|
+
export function validateCheckedTaskReviews({ planContent, runtimeRoot, executeRunId }) {
|
|
278
|
+
const failures = []
|
|
279
|
+
const re = /^\s*[-*]\s*\[x\]\s*task-(\d+)/gim
|
|
280
|
+
let m
|
|
281
|
+
while ((m = re.exec(planContent)) !== null) {
|
|
282
|
+
const taskId = `task-${m[1].padStart(2, '0')}`
|
|
283
|
+
const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId, 'review.json')
|
|
284
|
+
const r = readReview(reviewPath)
|
|
285
|
+
if (!r.ok) {
|
|
286
|
+
const kind = r.parseError ? 'parseError' : (r.schemaError ? 'schemaError' : 'missing')
|
|
287
|
+
failures.push({ taskId, reviewPath, kind, errors: r.errors })
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return { ok: failures.length === 0, failures }
|
|
291
|
+
}
|
|
292
|
+
|
|
261
293
|
/**
|
|
262
294
|
* execute --done 时的 task review 总校验
|
|
263
295
|
*
|
|
@@ -548,6 +580,133 @@ export function generateExecuteRunId() {
|
|
|
548
580
|
return `exec-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
|
549
581
|
}
|
|
550
582
|
|
|
583
|
+
/**
|
|
584
|
+
* worktree execute「主 agent 直接实现」模式收尾兜底:per-task review.json 缺失时,
|
|
585
|
+
* 据 git diff base..head 按 task allowed_paths 归属,自动落盘 cannot_verify 草稿。
|
|
586
|
+
*
|
|
587
|
+
* 根因(坑 worktree-execute-apply-friction 坑2):per-task review.json 无程序化 writer,
|
|
588
|
+
* 全靠子代理手写;主 agent 统一实现强依赖链/机械 task 时不走子代理 review 流程 →
|
|
589
|
+
* review.json 全缺,execute --done 的 Task Review Gate 报「task-XX 缺少 review.json」阻断。
|
|
590
|
+
*
|
|
591
|
+
* 复用(与 Task Review Gate / verify 同源,单一真相):
|
|
592
|
+
* - resolveVerifyChangedFiles(worktree-aware,meta.json 为权威)→ base..head diff 文件集
|
|
593
|
+
* - WorktreeManager.getMeta → baseHash(base)+ worktreePath(决定 head/gitDir,同 gates.js reviewGitDir)
|
|
594
|
+
* - parseAllowedPaths + pathMatches(同 worktree-apply Gate2 口径)→ task 文件归属
|
|
595
|
+
* - exec-id 解析同 gates.js:269 / autoCheckPlanFromReviews:current-execute-run-id-<change> marker,
|
|
596
|
+
* 缺失则 generateExecuteRunId + 落盘(保证草稿与后续 gate 读同一 run 目录)
|
|
597
|
+
*
|
|
598
|
+
* 行为:
|
|
599
|
+
* - 仅 review.json 不存在时写(幂等,已有人工/子代理 verdict 一律跳过,绝不覆盖)
|
|
600
|
+
* - changedFiles 为空的 task 不生成(verifyReviewGitEvidence 判空 diff 伪造,留给 agent)
|
|
601
|
+
* - 草稿 verdict=cannot_verify + 非空 requiredEvidence(过 schema,流转 verify 阶段兑现)
|
|
602
|
+
* - 不属任何 task 的文件累计 unattributed(顺带修复/非源码),不强塞某 task
|
|
603
|
+
*
|
|
604
|
+
* fail-open:任何异常 → 返回统计,仅 console.warn,不阻断 execute 完成(草稿是兜底,
|
|
605
|
+
* 缺了也只是退回原状——gate 报缺 review.json,agent 手补,不比修复前差)。
|
|
606
|
+
*
|
|
607
|
+
* @param {{ changeName: string, cwd: string, platformOpts?: object }} opts
|
|
608
|
+
* @returns {Promise<{ generated: number, skipped: number, unattributed: string[], reason?: string, executeRunId?: string }>}
|
|
609
|
+
*/
|
|
610
|
+
export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts = {} }) {
|
|
611
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
612
|
+
const runtimeRoot = platformOpts.runtimeRoot || join(specBase, '.runtime')
|
|
613
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
614
|
+
const tasksDir = join(changeDir, 'tasks')
|
|
615
|
+
|
|
616
|
+
if (!changeName) {
|
|
617
|
+
return { generated: 0, skipped: 0, unattributed: [], reason: '无 changeName' }
|
|
618
|
+
}
|
|
619
|
+
// 无 task 卡片目录 → 非多 task 变更,无需补草稿
|
|
620
|
+
if (!existsSync(tasksDir)) {
|
|
621
|
+
return { generated: 0, skipped: 0, unattributed: [], reason: '无 tasks/ 目录' }
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// exec-id:与 Task Review Gate(gates.js:269)/ autoCheckPlanFromReviews 同源
|
|
625
|
+
const runIdFile = join(runtimeRoot, 'current-execute-run-id-' + changeName)
|
|
626
|
+
let executeRunId = ''
|
|
627
|
+
try { if (existsSync(runIdFile)) executeRunId = readFileSync(runIdFile, 'utf8').trim() } catch {}
|
|
628
|
+
if (!executeRunId) {
|
|
629
|
+
executeRunId = generateExecuteRunId()
|
|
630
|
+
try { mkdirSync(runtimeRoot, { recursive: true }); writeFileSync(runIdFile, executeRunId + '\n') } catch {}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// base..head diff 文件集(worktree-aware;null=git 不可用,[]=无 commit diff)
|
|
634
|
+
const diffFiles = resolveVerifyChangedFiles(cwd, changeName)
|
|
635
|
+
if (!diffFiles || diffFiles.length === 0) {
|
|
636
|
+
return { generated: 0, skipped: 0, unattributed: [], executeRunId, reason: 'base..head 无代码 diff(改动未 commit?)' }
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// base/head + gitDir:与 gates.js reviewGitDir 同源(worktree 优先,in-place 回退 cwd)
|
|
640
|
+
let base = null
|
|
641
|
+
let head = null
|
|
642
|
+
let reviewGitDir = cwd
|
|
643
|
+
try {
|
|
644
|
+
const wm = new WorktreeManager({ cwd })
|
|
645
|
+
const meta = wm.getMeta(changeName)
|
|
646
|
+
base = (meta && (meta.baselineCommit || meta.baseHash)) || null
|
|
647
|
+
if (meta && meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
648
|
+
reviewGitDir = meta.worktreePath
|
|
649
|
+
}
|
|
650
|
+
} catch {}
|
|
651
|
+
if (!base) {
|
|
652
|
+
return { generated: 0, skipped: 0, unattributed: diffFiles, executeRunId, reason: '无 worktree meta.baseHash(无法定 base/head)' }
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
head = runGit(reviewGitDir, ['rev-parse', 'HEAD'])
|
|
656
|
+
} catch {
|
|
657
|
+
return { generated: 0, skipped: 0, unattributed: diffFiles, executeRunId, reason: 'git rev-parse HEAD 失败(' + reviewGitDir + ')' }
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f)).sort()
|
|
661
|
+
let generated = 0
|
|
662
|
+
let skipped = 0
|
|
663
|
+
const attributed = new Set()
|
|
664
|
+
|
|
665
|
+
for (const tf of taskFiles) {
|
|
666
|
+
const taskId = tf.replace(/\.md$/, '')
|
|
667
|
+
const reviewDir = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId)
|
|
668
|
+
const reviewPath = join(reviewDir, 'review.json')
|
|
669
|
+
|
|
670
|
+
// 幂等:review.json 已存在(无论合法/解析错/schema 错)一律跳过,绝不覆盖人工/子代理 verdict
|
|
671
|
+
const existing = readReview(reviewPath)
|
|
672
|
+
if (existing.ok || existing.parseError || existing.schemaError) {
|
|
673
|
+
skipped++
|
|
674
|
+
continue
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const content = readFileSync(join(tasksDir, tf), 'utf8')
|
|
678
|
+
const allowedPaths = parseAllowedPaths(content)
|
|
679
|
+
const changedFiles = allowedPaths.length > 0
|
|
680
|
+
? diffFiles.filter(f => allowedPaths.some(p => pathMatches(f, p)))
|
|
681
|
+
: []
|
|
682
|
+
|
|
683
|
+
// 空 changedFiles 的 task 不生成(verifyReviewGitEvidence 判空 diff 伪造,留给 agent 手补)
|
|
684
|
+
if (changedFiles.length === 0) {
|
|
685
|
+
skipped++
|
|
686
|
+
continue
|
|
687
|
+
}
|
|
688
|
+
changedFiles.forEach(f => attributed.add(f))
|
|
689
|
+
|
|
690
|
+
const draft = {
|
|
691
|
+
schemaVersion: REVIEW_SCHEMA_VERSION,
|
|
692
|
+
task: taskId,
|
|
693
|
+
base,
|
|
694
|
+
head,
|
|
695
|
+
changedFiles,
|
|
696
|
+
specVerdict: 'cannot_verify',
|
|
697
|
+
qualityVerdict: 'cannot_verify',
|
|
698
|
+
requiredEvidence: ['auto-generated draft: 待 agent 对照 ' + taskId + ' brief + diff 复核后升级为 pass/fail'],
|
|
699
|
+
reviewerNotes: 'auto-generated draft from git diff ' + base.slice(0, 8) + '..' + head.slice(0, 8) + ';verdict=未评审(worktree execute 主 agent 实现模式兜底,坑2)',
|
|
700
|
+
}
|
|
701
|
+
mkdirSync(reviewDir, { recursive: true })
|
|
702
|
+
writeFileSync(reviewPath, JSON.stringify(draft, null, 2) + '\n')
|
|
703
|
+
generated++
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const unattributed = diffFiles.filter(f => !attributed.has(f))
|
|
707
|
+
return { generated, skipped, unattributed, executeRunId }
|
|
708
|
+
}
|
|
709
|
+
|
|
551
710
|
/**
|
|
552
711
|
* 获取当前(或最新)execute run id
|
|
553
712
|
* 从 runtime 目录下查找 execute-runs/ 子目录
|