sillyspec 3.27.2 → 3.27.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.
- package/.claude/skills/sillyspec-execute/SKILL.md +8 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
- package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
- package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
- package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
- package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
- package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
- package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
- package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
- package/eval/README.md +86 -0
- package/eval/eval.config.json +28 -0
- package/eval/lib/adapters/cli.mjs +63 -0
- package/eval/lib/adapters/dryrun.mjs +27 -0
- package/eval/lib/config.mjs +82 -0
- package/eval/lib/db.mjs +76 -0
- package/eval/lib/report.mjs +120 -0
- package/eval/lib/runner.mjs +157 -0
- package/eval/lib/tasks.mjs +73 -0
- package/eval/lib/util.mjs +107 -0
- package/eval/lib/workdir.mjs +30 -0
- package/eval/run.mjs +83 -0
- package/eval/tasks/001-slugify/files/package.json +6 -0
- package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
- package/eval/tasks/001-slugify/task.json +6 -0
- package/eval/tasks/001-slugify/verify.mjs +28 -0
- package/eval/tasks/002-each-day/files/package.json +6 -0
- package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
- package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
- package/eval/tasks/002-each-day/task.json +6 -0
- package/eval/tasks/002-each-day/verify.mjs +31 -0
- package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
- package/eval/tasks/003-word-report/files/package.json +6 -0
- package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
- package/eval/tasks/003-word-report/task.json +6 -0
- package/eval/tasks/003-word-report/verify.mjs +38 -0
- package/package.json +4 -2
- package/src/agent-session-log.js +45 -5
- package/src/change-risk-profile.js +83 -14
- package/src/config-schema.js +18 -3
- package/src/contract-matrix.js +90 -29
- package/src/datetime.js +20 -0
- package/src/decision-distill.js +517 -0
- package/src/dispatch/backends/local-agent.js +4 -4
- package/src/docs-check.js +227 -0
- package/src/docs-debt.js +187 -24
- package/src/doctor-diagnostics.js +102 -2
- package/src/foreign-declared.js +78 -0
- package/src/index.js +89 -9
- package/src/init.js +9 -7
- package/src/knowledge-match.js +107 -6
- package/src/local-register.js +7 -1
- package/src/module-resolve.js +12 -1
- package/src/modules.js +2 -1
- package/src/plan-adopt-waves.js +126 -0
- package/src/quicklog.js +50 -4
- package/src/run/command.js +122 -43
- package/src/run/complete-handlers.js +48 -19
- package/src/run/complete.js +5 -2
- package/src/run/gates.js +87 -22
- package/src/run/prompt.js +84 -42
- package/src/run/quick-audit.js +11 -0
- package/src/run/shared.js +152 -10
- package/src/run.js +2 -2
- package/src/scan-postcheck.js +30 -17
- package/src/stage-contract-spec.js +2 -1
- package/src/stage-contract.js +68 -4
- package/src/stages/archive.js +53 -12
- package/src/stages/brainstorm-auto.js +7 -0
- package/src/stages/brainstorm.js +11 -0
- package/src/stages/doctor.js +157 -1
- package/src/stages/execute.js +32 -6
- package/src/stages/plan-postcheck.js +76 -17
- package/src/stages/plan.js +1 -0
- package/src/stages/quick.js +6 -1
- package/src/stages/verify.js +31 -14
- package/src/sync.js +41 -3
- package/src/task-review.js +2 -2
- package/src/taskcard-placeholders.js +21 -0
- package/src/taskcard.js +10 -1
- package/src/verify-postcheck.js +315 -13
- package/src/verify-probes.js +46 -4
- package/src/worktree-apply.js +184 -7
- package/src/worktree-deps.js +57 -0
- package/src/worktree.js +201 -58
- package/templates/prompts/taskcard-rules.md +1 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SillySpec Stage Review Gate — 阶段级审查校验(文档型)
|
|
3
|
+
*
|
|
4
|
+
* 与 task-review.js 的关系:
|
|
5
|
+
* task-review 校验 execute 每个 task 的 review.json(git 代码 diff 证据:base/head)。
|
|
6
|
+
* stage-review 校验 brainstorm/plan/execute-acceptance 的阶段级 review.json
|
|
7
|
+
* (文档证据:reviewedFiles + docHash)。两者 verdict 语义一致(pass/fail/cannot_verify
|
|
8
|
+
* 三态 + cannot_verify 必须带 requiredEvidence 的反逃逸规则),复用 task-review 导出的
|
|
9
|
+
* VALID_VERDICTS / REVIEW_SCHEMA_VERSION 常量;task-review.js 本身不改、execute 路径零风险。
|
|
10
|
+
*
|
|
11
|
+
* 目录结构:
|
|
12
|
+
* .sillyspec/.runtime/stage-reviews/<stage>-<reviewRunId>/review.json
|
|
13
|
+
* (平台模式落 <runtimeRoot>/stage-reviews/...,与 execute-runs 同构)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync } from 'fs'
|
|
17
|
+
import { join } from 'path'
|
|
18
|
+
import { createHash } from 'crypto'
|
|
19
|
+
import { VALID_VERDICTS, REVIEW_SCHEMA_VERSION } from './task-review.js'
|
|
20
|
+
import { detectSpecDirTypo } from './spec-dir-typo.js'
|
|
21
|
+
import { resolveRuntimeRoot } from './run/shared.js'
|
|
22
|
+
|
|
23
|
+
// 文档型 stage review 的合法 reviewType
|
|
24
|
+
const STAGE_REVIEW_TYPES = ['design', 'plan', 'proposal', 'code', 'acceptance']
|
|
25
|
+
|
|
26
|
+
// checklist 每项 result 的合法值
|
|
27
|
+
const CHECKLIST_RESULTS = ['pass', 'gap', 'fail']
|
|
28
|
+
|
|
29
|
+
// stage → 主审查文档(brainstorm/execute 审 design.md;plan 审 plan.md)
|
|
30
|
+
const STAGE_MAIN_DOC = { brainstorm: 'design.md', plan: 'plan.md', execute: 'design.md' }
|
|
31
|
+
// stage → reviewType(对齐 gates.js Stage Review Gate 的映射)
|
|
32
|
+
const STAGE_REVIEW_TYPE = { brainstorm: 'design', plan: 'plan', execute: 'acceptance' }
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 渲染 review.json 产物契约(markdown)给 review 子代理事前看 —— schema 表 + 完整 JSON 示例 +
|
|
36
|
+
* docHash 算法 + 重算提示 + 位置。复用 validateStageReviewSchema 的同源常量,事前给的 == 事后查的。
|
|
37
|
+
*
|
|
38
|
+
* 历史教训:review 子代理靠读 stage-review.js 源码 + 翻模板才搞清 schema,常错(漏 schemaVersion /
|
|
39
|
+
* checklist 按层嵌套对象 / docHash 用主文档改版前的旧 sha256)。本函数把契约前置进 prompt。
|
|
40
|
+
*
|
|
41
|
+
* @param {{ stage?: string, changeDir?: string, reviewRunId?: string, tier?: string }} opts
|
|
42
|
+
* @returns {string} markdown 段;tier=self 返回简短提示(无需 review.json)
|
|
43
|
+
*/
|
|
44
|
+
export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier } = {}) {
|
|
45
|
+
if (tier === 'self') {
|
|
46
|
+
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。'
|
|
47
|
+
}
|
|
48
|
+
const reviewType = STAGE_REVIEW_TYPE[stage] || 'design'
|
|
49
|
+
const mainDoc = STAGE_MAIN_DOC[stage] || 'design.md'
|
|
50
|
+
const mainDocRel = changeDir ? 'changes/<change>/' + mainDoc : mainDoc
|
|
51
|
+
const stageLbl = stage || '<stage>'
|
|
52
|
+
const runIdLbl = reviewRunId || '<reviewRunId>'
|
|
53
|
+
const L = [
|
|
54
|
+
'## review.json 产物契约(CLI Stage Review Gate 将硬校验,以下为精确 schema —— 事前给的 == 事后查的)',
|
|
55
|
+
'',
|
|
56
|
+
'> 路径:`{SPEC_ROOT}/.runtime/stage-reviews/' + stageLbl + '-' + runIdLbl + '/review.json`(本次 reviewType=' + reviewType + ',主审查文档=' + mainDocRel + ')',
|
|
57
|
+
'',
|
|
58
|
+
'### 必填字段(validateStageReviewSchema 硬校验,缺任一即 schema 失败阻断)',
|
|
59
|
+
'- `schemaVersion`: ' + REVIEW_SCHEMA_VERSION,
|
|
60
|
+
'- `reviewType`: "' + reviewType + '"(必须与本 stage 一致;合法值 ' + STAGE_REVIEW_TYPES.join(' / ') + ')',
|
|
61
|
+
'- `specVerdict` / `qualityVerdict`: ∈ { ' + VALID_VERDICTS.join(' / ') + ' }',
|
|
62
|
+
'- `reviewedFiles`: 非空数组,[0] = 主审查文档 = `' + mainDocRel + '`',
|
|
63
|
+
'- `docHash`: reviewedFiles[0] 文件内容的 sha256(hex,见下方算法)',
|
|
64
|
+
'- `requiredEvidence`: 非空数组(specVerdict 或 qualityVerdict = cannot_verify 时必填,反逃逸)',
|
|
65
|
+
'- `checklist`(可选): 扁平数组 —— 每项 { item: string, result: ∈ { ' + CHECKLIST_RESULTS.join(' / ') + ' }, note?: string }。注意是**扁平数组**,不是按层(定义/一致/可行性)嵌套对象',
|
|
66
|
+
'- `reviewerNotes`: 说明(verdict=fail 时写明阻断项)',
|
|
67
|
+
'',
|
|
68
|
+
'### docHash(一键代填,勿手算)',
|
|
69
|
+
'- **推荐**:docHash 先占位(如 `"TODO"`),review.json 写完后跑',
|
|
70
|
+
' `sillyspec register-stage-review --change <变更名> --stage ' + stageLbl + ' --refresh-hash`',
|
|
71
|
+
' —— CLI 重算真实 sha256 代填,verdict/checklist 原样保留;主文档改版后同样跑这条',
|
|
72
|
+
' (多阶段联动用 `--all` 一次处理)。gate 拦 docHash 失配时也是同一条命令修复。',
|
|
73
|
+
'- 兜底手算口径(极端无 CLI 场景):node `crypto.createHash(\'sha256\').update(readFileSync(...))` (原始字节)/ `sha256sum <file>` 首列 hex。',
|
|
74
|
+
'- ⚠️ **review.json 写入后若 ' + mainDoc + ' 再被改,必须重算 docHash** —— gate 会重算 reviewedFiles[0] 的 sha256 比对,不符判伪造(历史翻车根因:design 改了 rev 后 docHash 仍是旧值)',
|
|
75
|
+
'',
|
|
76
|
+
'### 完整 JSON 示例(照抄改值)',
|
|
77
|
+
'```json',
|
|
78
|
+
'{',
|
|
79
|
+
' "schemaVersion": ' + REVIEW_SCHEMA_VERSION + ',',
|
|
80
|
+
' "reviewType": "' + reviewType + '",',
|
|
81
|
+
' "specVerdict": "pass",',
|
|
82
|
+
' "qualityVerdict": "pass",',
|
|
83
|
+
' "reviewedFiles": ["' + mainDocRel + '"],',
|
|
84
|
+
' "docHash": "<上面算出的 sha256 hex>",',
|
|
85
|
+
' "checklist": [',
|
|
86
|
+
' { "item": "<审查项>", "result": "pass", "note": "<证据/说明>" }',
|
|
87
|
+
' ],',
|
|
88
|
+
' "reviewerNotes": "<总结>"',
|
|
89
|
+
'}',
|
|
90
|
+
'```',
|
|
91
|
+
]
|
|
92
|
+
return L.join('\n')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 计算文件内容的 sha256(用于 docHash 真实性校验,防 agent 凭空伪造 review.json)
|
|
97
|
+
* @param {string} filePath - 绝对路径
|
|
98
|
+
* @returns {string|null} sha256 hex;文件不存在返回 null
|
|
99
|
+
*/
|
|
100
|
+
export function computeDocHash(filePath) {
|
|
101
|
+
if (!filePath || !existsSync(filePath)) return null
|
|
102
|
+
const content = readFileSync(filePath)
|
|
103
|
+
return createHash('sha256').update(content).digest('hex')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 校验文档型 stage review.json 的 schema
|
|
108
|
+
*
|
|
109
|
+
* 与 task-review.validateReviewSchema 的区别:
|
|
110
|
+
* - 不要求 git base/head(证据形态不同)
|
|
111
|
+
* - 改要求 reviewedFiles(被审查文档路径数组)+ docHash(主文档内容 sha256)
|
|
112
|
+
* - 新增 reviewType(design/plan/proposal/code/acceptance)+ checklist
|
|
113
|
+
* - 复用 VALID_VERDICTS 三态 + cannot_verify 联动规则(task-review.js:65-69)
|
|
114
|
+
*
|
|
115
|
+
* @param {object} review - 解析后的 JSON 对象
|
|
116
|
+
* @returns {{ ok: boolean, errors: string[] }}
|
|
117
|
+
*/
|
|
118
|
+
export function validateStageReviewSchema(review) {
|
|
119
|
+
const errors = []
|
|
120
|
+
if (!review || typeof review !== 'object') {
|
|
121
|
+
return { ok: false, errors: ['stage review.json 不是有效 JSON 对象'] }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (review.schemaVersion !== REVIEW_SCHEMA_VERSION) {
|
|
125
|
+
errors.push(`schemaVersion 应为 ${REVIEW_SCHEMA_VERSION},实际为 ${review.schemaVersion}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!STAGE_REVIEW_TYPES.includes(review.reviewType)) {
|
|
129
|
+
errors.push(`reviewType 无效:${review.reviewType}(应为 ${STAGE_REVIEW_TYPES.join('/')})`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!VALID_VERDICTS.includes(review.specVerdict)) {
|
|
133
|
+
errors.push(`specVerdict 无效:${review.specVerdict}(应为 ${VALID_VERDICTS.join('/')})`)
|
|
134
|
+
}
|
|
135
|
+
if (!VALID_VERDICTS.includes(review.qualityVerdict)) {
|
|
136
|
+
errors.push(`qualityVerdict 无效:${review.qualityVerdict}(应为 ${VALID_VERDICTS.join('/')})`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// cannot_verify 必须提供非空 requiredEvidence(与 task-review:65-69 一致,反逃逸)
|
|
140
|
+
if (review.specVerdict === 'cannot_verify' || review.qualityVerdict === 'cannot_verify') {
|
|
141
|
+
if (!Array.isArray(review.requiredEvidence) || review.requiredEvidence.length === 0) {
|
|
142
|
+
errors.push('cannot_verify 的 verdict 必须提供非空的 requiredEvidence 数组')
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// reviewedFiles 必须是非空数组
|
|
147
|
+
if (!Array.isArray(review.reviewedFiles) || review.reviewedFiles.length === 0) {
|
|
148
|
+
errors.push('缺少 reviewedFiles 字段(应为被审查文档路径数组,[0] 为主审查对象,docHash 对应它)')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// docHash 非空
|
|
152
|
+
if (!review.docHash || typeof review.docHash !== 'string') {
|
|
153
|
+
errors.push('缺少 docHash 字段(被审查主文档内容的 sha256)')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// checklist 若提供,校验每项结构
|
|
157
|
+
if (review.checklist !== undefined) {
|
|
158
|
+
if (!Array.isArray(review.checklist)) {
|
|
159
|
+
errors.push('checklist 若提供必须是数组')
|
|
160
|
+
} else {
|
|
161
|
+
for (let i = 0; i < review.checklist.length; i++) {
|
|
162
|
+
const item = review.checklist[i]
|
|
163
|
+
if (!item || typeof item !== 'object' || !item.item) {
|
|
164
|
+
errors.push(`checklist[${i}] 缺少 item 描述`)
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
if (item.result !== undefined && !CHECKLIST_RESULTS.includes(item.result)) {
|
|
168
|
+
errors.push(`checklist[${i}].result 无效:${item.result}(应为 ${CHECKLIST_RESULTS.join('/')})`)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { ok: errors.length === 0, errors }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* docHash 真实性交叉校验:review 声称审查了某文档,重算该文档 sha256 对比。
|
|
179
|
+
*
|
|
180
|
+
* 对应 task-review.verifyReviewGitEvidence 的"防伪造"职责——agent 不能凭空编一份
|
|
181
|
+
* review.json 贴个假 hash。约定 docHash 对应 reviewedFiles[0](主审查对象)。
|
|
182
|
+
*
|
|
183
|
+
* searchDirs 依次尝试解析 reviewedFiles[0] 的相对路径(effectiveSpecBase / changeDir / cwd)。
|
|
184
|
+
* 主文档在所有基准下都找不到时 fail-closed 阻断——连主审查文档都定位不到,要么 reviewedFiles[0]
|
|
185
|
+
* 是伪造路径(防 agent 填假路径跳过校验),要么路径基准严重错位,两者都不该静默放行。
|
|
186
|
+
*
|
|
187
|
+
* @param {object} review - 已通过 schema 校验
|
|
188
|
+
* @param {string[]} searchDirs - 解析相对路径的候选基准目录(绝对路径)
|
|
189
|
+
* @returns {{ ok: boolean, errors: string[], warnings: string[] }}
|
|
190
|
+
*/
|
|
191
|
+
export function verifyStageReviewDocHash(review, searchDirs) {
|
|
192
|
+
const errors = []
|
|
193
|
+
const warnings = []
|
|
194
|
+
const files = Array.isArray(review.reviewedFiles) ? review.reviewedFiles : []
|
|
195
|
+
if (files.length === 0) {
|
|
196
|
+
return { ok: false, errors: ['reviewedFiles 为空,无法做 docHash 校验'], warnings }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const primaryRel = files[0]
|
|
200
|
+
|
|
201
|
+
for (const base of searchDirs || []) {
|
|
202
|
+
const abs = join(base, primaryRel)
|
|
203
|
+
if (!existsSync(abs)) continue
|
|
204
|
+
// 找到主文档,比对 hash。容忍「原始字节」与「LF 规范化」两种口径:Windows 下 CRLF/LF 在
|
|
205
|
+
// git add / eol 规范化前后字节会漂移,同一文件 sha256 偶发不一致(坑 worktree-execute-apply-friction
|
|
206
|
+
// 坑3)——agent 写的 docHash(原始 sha256sum)与 gate 重算只要任一口径匹配即认可,消除 CRLF
|
|
207
|
+
// 漂移导致的反复「疑似伪造」误报。两口径都对不上仍判不匹配(防伪造能力不降级)。
|
|
208
|
+
const content = readFileSync(abs, 'utf8')
|
|
209
|
+
const actualRaw = createHash('sha256').update(content).digest('hex')
|
|
210
|
+
const actualLf = createHash('sha256').update(content.replace(/\r\n/g, '\n')).digest('hex')
|
|
211
|
+
const claimed = String(review.docHash).toLowerCase()
|
|
212
|
+
if (claimed !== actualRaw.toLowerCase() && claimed !== actualLf.toLowerCase()) {
|
|
213
|
+
errors.push(
|
|
214
|
+
`docHash 与主审查文档 ${primaryRel} 的实际内容不匹配。` +
|
|
215
|
+
`若刚改过该文档(如 design 修订后忘重算),一键修复:` +
|
|
216
|
+
`sillyspec register-stage-review --change <变更名> --stage <阶段> --refresh-hash` +
|
|
217
|
+
`(CLI 重算 docHash 代填,verdict 保留;多阶段联动加 --all);` +
|
|
218
|
+
`若未改文档,则 review.json 疑似伪造(未真正读取文档)。`
|
|
219
|
+
)
|
|
220
|
+
return { ok: false, errors, warnings }
|
|
221
|
+
}
|
|
222
|
+
return { ok: true, errors, warnings }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 所有基准都找不到主文档 → fail-closed。连主审查文档都定位不到,说明 reviewedFiles[0] 要么
|
|
226
|
+
// 是伪造路径(恶意 agent 填假路径跳过 docHash 校验),要么路径基准严重错位——两者都该阻断
|
|
227
|
+
// 而非静默放行(历史降级 warning 是可被利用的伪造通道)。searchDirs =
|
|
228
|
+
// [effectiveSpecBase(.sillyspec), reviewChangeDir(changes/<change>), cwd],契约规定的
|
|
229
|
+
// reviewedFiles[0](changes/<change>/<mainDoc> 或 <mainDoc>)在这三个基准下必命中其一。
|
|
230
|
+
errors.push('主审查文档 ' + primaryRel + ' 在 ' + (searchDirs || []).length + ' 个候选基准目录下均不存在 — 无法做 docHash 校验(reviewedFiles[0] 路径伪造或基准错位)')
|
|
231
|
+
return { ok: false, errors, warnings }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 生成 stage review run id
|
|
236
|
+
* @returns {string} 如 'review-2026-07-16-143000'
|
|
237
|
+
*/
|
|
238
|
+
export function generateStageReviewRunId() {
|
|
239
|
+
const now = new Date()
|
|
240
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
241
|
+
return `review-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* stage review marker 文件路径(prompt 渲染时写 / getLatestStageReviewRunId 读,同源)。
|
|
246
|
+
*
|
|
247
|
+
* 对齐 execute 的 current-execute-run-id-<change> 语义:含 change 防多 change 串台
|
|
248
|
+
* (同一 stage 不同变更的 review 各自独立 marker);changeName 缺失时退化为按 stage 分。
|
|
249
|
+
*
|
|
250
|
+
* @param {string} runtimeRoot - .runtime 绝对路径
|
|
251
|
+
* @param {string} stage - brainstorm|plan|execute
|
|
252
|
+
* @param {string} [changeName] - 变更名(防多 change 串台)
|
|
253
|
+
* @returns {string} marker 文件绝对路径
|
|
254
|
+
*/
|
|
255
|
+
export function stageReviewMarkerPath(runtimeRoot, stage, changeName) {
|
|
256
|
+
const suffix = changeName ? `${stage}-${changeName}` : stage
|
|
257
|
+
return join(runtimeRoot, `current-stage-review-run-id-${suffix}`)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 获取最新的 stage review run id
|
|
262
|
+
*
|
|
263
|
+
* 优先读 marker 文件(prompt 渲染 {REVIEW_TIER} 时写入,保证 gate 取的 ID == 注入给 agent
|
|
264
|
+
* 的 ID —— 修复「prompt 多次渲染 / 多次 review 时 gate 取错 ID 读错 review.json」),
|
|
265
|
+
* 对齐 getLatestExecuteRunId(task-review.js:420-428)。marker 缺失时 fallback 扫描
|
|
266
|
+
* stage-reviews/<stage>-review-* 目录取字典序最新(向后兼容无 marker 旧数据)。
|
|
267
|
+
*
|
|
268
|
+
* @param {string} runtimeRoot - .runtime 绝对路径
|
|
269
|
+
* @param {string} stage - brainstorm|plan|execute
|
|
270
|
+
* @param {string} [changeName] - 变更名(读对应 marker,防多 change 串台)
|
|
271
|
+
* @returns {string|null} run id(如 'review-2026-07-16-143000'),无则 null
|
|
272
|
+
*/
|
|
273
|
+
export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
|
|
274
|
+
// 优先读 marker(prompt 写 / gate 读同源,对齐 execute current-execute-run-id 机制)
|
|
275
|
+
try {
|
|
276
|
+
const markerPath = stageReviewMarkerPath(runtimeRoot, stage, changeName)
|
|
277
|
+
if (existsSync(markerPath)) {
|
|
278
|
+
const content = readFileSync(markerPath, 'utf8').trim()
|
|
279
|
+
// marker 内容格式校验(execute 复盘 b2):必须 review- 前缀(generateStageReviewRunId 格式)。
|
|
280
|
+
// agent 手写 marker 可能误填 execute 的 exec- 前缀 → 按 stage-reviews/<stage>-<runId> 拼目录必找不到,
|
|
281
|
+
// 与其读坏 ID 报误导错误,不如显式 warn 并退回目录扫描。
|
|
282
|
+
if (content) {
|
|
283
|
+
if (/^review-/.test(content)) return content
|
|
284
|
+
console.warn(`⚠️ stage-review marker ${markerPath} 内容 "${content.slice(0, 40)}" 不是 review- 前缀(应为 generateStageReviewRunId 格式,勿填 execute 的 exec- ID),忽略并退回目录扫描`)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch {}
|
|
288
|
+
|
|
289
|
+
// fallback:扫描 stage-reviews/<stage>-review-* 目录取字典序最新(向后兼容无 marker 旧数据)
|
|
290
|
+
const dir = join(runtimeRoot, 'stage-reviews')
|
|
291
|
+
if (!existsSync(dir)) return null
|
|
292
|
+
try {
|
|
293
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
|
294
|
+
.filter(e => e.isDirectory() && e.name.startsWith(`${stage}-review-`))
|
|
295
|
+
.map(e => e.name)
|
|
296
|
+
.sort()
|
|
297
|
+
.reverse()
|
|
298
|
+
if (!entries[0]) return null
|
|
299
|
+
// cross-change 防护(execute 复盘 b1):changeName 提供时按 review.json 的 reviewedFiles[0] 归属过滤。
|
|
300
|
+
// 契约 reviewedFiles[0] = `changes/<change>/<mainDoc>`(renderReviewJsonContract),据此排除他变更的 review,
|
|
301
|
+
// 否则批量完成 marker 缺失时可能读到 proxy 等其他变更的 acceptance review 报错误导。
|
|
302
|
+
if (changeName) {
|
|
303
|
+
const matching = entries.find(e => {
|
|
304
|
+
try {
|
|
305
|
+
const rj = JSON.parse(readFileSync(join(dir, e, 'review.json'), 'utf8'))
|
|
306
|
+
const r0 = Array.isArray(rj.reviewedFiles) ? String(rj.reviewedFiles[0]) : ''
|
|
307
|
+
return r0.includes(`changes/${changeName}/`)
|
|
308
|
+
} catch { return false }
|
|
309
|
+
})
|
|
310
|
+
if (matching) return matching.slice(stage.length + 1)
|
|
311
|
+
console.warn(`⚠️ 无 ${stage} 的 review marker,且 stage-reviews/ 下无归属变更 ${changeName} 的 review(reviewedFiles 应含 changes/${changeName}/);为避免跨变更串台不再取最新,返回 null(fail-closed,由调用方报缺 review.json)`)
|
|
312
|
+
return null
|
|
313
|
+
}
|
|
314
|
+
// entries[0] = "plan-review-2026-...",剥掉 `${stage}-` 前缀得到 runId "review-2026-..."
|
|
315
|
+
return entries[0].slice(stage.length + 1)
|
|
316
|
+
} catch {
|
|
317
|
+
return null
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* stage-level review 总校验(brainstorm/plan/execute-acceptance 的 done gate)
|
|
323
|
+
*
|
|
324
|
+
* 规则(与 task-review.validateTaskReviews 对称):
|
|
325
|
+
* - review.json 缺失 → error 阻断
|
|
326
|
+
* - 解析失败 / schema 失败 → error 阻断
|
|
327
|
+
* - reviewType 与期望不符 → error(agent 复制模板未改)
|
|
328
|
+
* - specVerdict 或 qualityVerdict 为 fail → error 阻断
|
|
329
|
+
* - cannot_verify + 非空 requiredEvidence → warning
|
|
330
|
+
* - cannot_verify + 空 requiredEvidence → error(schema 已拦,双保险)
|
|
331
|
+
* - docHash 与主文档实际内容不符 → error(伪造)
|
|
332
|
+
*
|
|
333
|
+
* 调用方约定:先用 classifyReviewTier 判定 tier——tier=self 不调用本函数(直接放行),
|
|
334
|
+
* tier=independent 才调用。
|
|
335
|
+
*
|
|
336
|
+
* @param {object} opts
|
|
337
|
+
* @param {string} opts.stage - brainstorm|plan|execute
|
|
338
|
+
* @param {string} opts.reviewType - design|plan|proposal|code|acceptance
|
|
339
|
+
* @param {string} opts.runtimeRoot - .runtime 绝对路径(平台模式为 runtimeRoot)
|
|
340
|
+
* @param {string} opts.reviewRunId - stage review run id(review-<ts>)
|
|
341
|
+
* @param {string[]} [opts.searchDirs] - docHash 校验的候选基准目录
|
|
342
|
+
* @param {boolean} [opts.verifyDocHash=true] - 是否做 docHash 真实性校验
|
|
343
|
+
* @returns {{ ok: boolean, errors: string[], warnings: string[], review: object|null }}
|
|
344
|
+
*/
|
|
345
|
+
export function validateStageReview(opts) {
|
|
346
|
+
const { stage, reviewType, runtimeRoot, reviewRunId, searchDirs = [], verifyDocHash = true } = opts
|
|
347
|
+
const errors = []
|
|
348
|
+
const warnings = []
|
|
349
|
+
|
|
350
|
+
const reviewDir = join(runtimeRoot, 'stage-reviews', `${stage}-${reviewRunId}`)
|
|
351
|
+
const reviewPath = join(reviewDir, 'review.json')
|
|
352
|
+
|
|
353
|
+
if (!reviewRunId || !existsSync(reviewPath)) {
|
|
354
|
+
const errs = [`缺少 ${stage} 阶段的 stage review.json — tier=independent 要求独立审查子代理产出(期望路径:${reviewPath})`]
|
|
355
|
+
const typo = detectSpecDirTypo(runtimeRoot)
|
|
356
|
+
if (typo) errs.push(`💡 路径疑似拼错:发现 ${typo.typoDir} 目录(应为 ${typo.canonical}),review.json 可能误存于此——检查是否把 .sillyspec 拼成了变体`)
|
|
357
|
+
return {
|
|
358
|
+
ok: false,
|
|
359
|
+
errors: errs,
|
|
360
|
+
warnings,
|
|
361
|
+
review: null,
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let raw
|
|
366
|
+
try {
|
|
367
|
+
raw = readFileSync(reviewPath, 'utf8')
|
|
368
|
+
} catch (e) {
|
|
369
|
+
return { ok: false, errors: [`${stage} review.json 读取失败: ${e.message}`], warnings, review: null }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let parsed
|
|
373
|
+
try {
|
|
374
|
+
parsed = JSON.parse(raw)
|
|
375
|
+
} catch (e) {
|
|
376
|
+
return { ok: false, errors: [`${stage} review.json 解析失败: ${e.message}`], warnings, review: null }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const schemaResult = validateStageReviewSchema(parsed)
|
|
380
|
+
if (!schemaResult.ok) {
|
|
381
|
+
return { ok: false, errors: [`${stage} review.json schema 校验失败 — ${schemaResult.errors.join('; ')}`], warnings, review: parsed }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// reviewType 与期望一致性
|
|
385
|
+
if (parsed.reviewType && parsed.reviewType !== reviewType) {
|
|
386
|
+
errors.push(`${stage} review.json 的 reviewType 为 "${parsed.reviewType}",与期望 "${reviewType}" 不符 — agent 可能复制模板未修改`)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// fail verdict
|
|
390
|
+
if (parsed.specVerdict === 'fail' || parsed.qualityVerdict === 'fail') {
|
|
391
|
+
errors.push(`${stage} review 未通过 — spec: ${parsed.specVerdict}, quality: ${parsed.qualityVerdict}`)
|
|
392
|
+
if (parsed.reviewerNotes) errors.push(`${stage}: ${parsed.reviewerNotes}`)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// docHash 真实性(fail verdict 已错时跳过,避免噪声)
|
|
396
|
+
if (errors.length === 0 && verifyDocHash && searchDirs.length > 0) {
|
|
397
|
+
const dh = verifyStageReviewDocHash(parsed, searchDirs)
|
|
398
|
+
for (const w of dh.warnings) warnings.push(`${stage}: ${w}`)
|
|
399
|
+
if (!dh.ok) {
|
|
400
|
+
for (const err of dh.errors) errors.push(`${stage}: ${err}`)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// cannot_verify → warning(requiredEvidence 非空已由 schema 保证)
|
|
405
|
+
if (errors.length === 0 && (parsed.specVerdict === 'cannot_verify' || parsed.qualityVerdict === 'cannot_verify')) {
|
|
406
|
+
const verdicts = []
|
|
407
|
+
if (parsed.specVerdict === 'cannot_verify') verdicts.push('spec')
|
|
408
|
+
if (parsed.qualityVerdict === 'cannot_verify') verdicts.push('quality')
|
|
409
|
+
warnings.push(`${stage}: ${verdicts.join('+')}=cannot_verify,requiredEvidence 需在后续阶段满足`)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { ok: errors.length === 0, errors, warnings, review: parsed }
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* 打印 stage review 校验结果
|
|
417
|
+
* @param {{ ok: boolean, errors: string[], warnings: string[] }} result
|
|
418
|
+
* @param {object} context - { stage, reviewRunId } 用于错误提示路径
|
|
419
|
+
*/
|
|
420
|
+
export function printStageReviewResult(result, context = {}) {
|
|
421
|
+
const { stage, reviewRunId, runtimeRoot, changeName } = context
|
|
422
|
+
if (result.ok && result.warnings.length === 0) {
|
|
423
|
+
console.log(`\n✅ Stage Review Gate — ${stage} 阶段独立审查通过`)
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (result.errors.length > 0) {
|
|
428
|
+
console.error(`\n🚫 Stage Review Gate — ${stage} FAILED`)
|
|
429
|
+
for (const err of result.errors) {
|
|
430
|
+
console.error(` - ${err}`)
|
|
431
|
+
}
|
|
432
|
+
// echo 完整 review.json 路径:runId + marker 已由 CLI 自动生成写入(prompt.js review step
|
|
433
|
+
// 渲染 / gate 触发),agent 直接派独立子代理把 review.json 写到此路径,勿手算 runId、勿手写
|
|
434
|
+
// marker(历史坑:agent 撞 gate 后自己算 runId 猜错 review- 前缀,填 exec-/裸 ts 被 CLI 忽略)。
|
|
435
|
+
if (reviewRunId) {
|
|
436
|
+
const reviewDir = runtimeRoot ? `${runtimeRoot}/stage-reviews/${stage}-${reviewRunId}` : `.sillyspec/.runtime/stage-reviews/${stage}-${reviewRunId}`
|
|
437
|
+
console.error(`\n 📁 review.json 直接写此路径(runId/marker CLI 已生成写入,勿手算 runId、勿手写 marker):`)
|
|
438
|
+
console.error(` ${reviewDir}/review.json`)
|
|
439
|
+
}
|
|
440
|
+
console.error(`\n 提示:tier=independent 要求独立审查子代理产出 review.json,补全后重新 --done`)
|
|
441
|
+
console.error(` 可用 sillyspec register-stage-review --change <名> --stage ${stage} [--from <已有review.json>] 一步生成 run 目录 + review.json 骨架(docHash 自动算)+ 写 marker + 自检,省掉手动建目录/写 marker`)
|
|
442
|
+
// docHash 失配精确指路(2026-08-21 agent-手工产出审计项②):改版后忘重算是最高频失败,
|
|
443
|
+
// 一键重算命令带上真实 change 名,agent 可直接照抄执行(--refresh-hash 保 verdict)。
|
|
444
|
+
if (result.errors.some(e => String(e).includes('docHash')) && changeName) {
|
|
445
|
+
console.error(` docHash 失配修复:sillyspec register-stage-review --change ${changeName} --stage ${stage} --refresh-hash(主文档改版多阶段联动用 --all)`)
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (result.warnings.length > 0) {
|
|
450
|
+
console.warn(`\n⚠️ Stage Review Gate — ${stage} WARNING`)
|
|
451
|
+
for (const w of result.warnings) {
|
|
452
|
+
console.warn(` - ${w}`)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 注册一个 stage 级 review(确定性 writer)—— 生成 run 目录 + review.json 骨架(或 adopt
|
|
459
|
+
* agent 草稿)+ 写 marker,治 tier=independent 时「调度者手动派独立子代理 → marker 不写 →
|
|
460
|
+
* gate 取错 run」的死锁。与 task 级 generateTaskReviewDrafts(task-review.js)对称。
|
|
461
|
+
*
|
|
462
|
+
* docHash 由 CLI 直接 computeDocHash 算(命令有文件访问权),消除 agent 手算 hash 易错
|
|
463
|
+
* (部分实现 P6.1b defer:仅本 scaffold 路径确定性,不改 agent 自写 review.json 的 hash 链路)。
|
|
464
|
+
*
|
|
465
|
+
* 纯新增,不改 validateStageReview / getLatestStageReviewRunId / renderReviewJsonContract 等
|
|
466
|
+
* 现有函数;self-check 只验 mechanics(schema + docHash),verdict 留给 Stage Review Gate 判。
|
|
467
|
+
*
|
|
468
|
+
* @param {object} opts
|
|
469
|
+
* @param {string} opts.changeName - 变更名
|
|
470
|
+
* @param {string} opts.stage - brainstorm|plan|execute
|
|
471
|
+
* @param {string} [opts.fromFile] - adopt 模式:agent 已写的 review.json(保留 verdict/checklist,重算 docHash + 规范化 reviewedFiles[0])
|
|
472
|
+
* @param {string} opts.cwd - 主仓 cwd(解析 specBase)
|
|
473
|
+
* @param {object} [opts.platformOpts] - 平台选项(specRoot 等)
|
|
474
|
+
* @returns {{ ok: boolean, reviewRunId: string, reviewPath: string, markerPath: string, mode: 'skeleton'|'adopted', mainDoc: string, review: object }}
|
|
475
|
+
* @throws {Error} changeName 空 / stage 非法 / 主文档缺失 / --from 不存在或 schema 不过
|
|
476
|
+
*/
|
|
477
|
+
export function registerStageReview({ changeName, stage, fromFile, cwd, platformOpts = {}, refreshHash = false }) {
|
|
478
|
+
if (!changeName) throw new Error('register-stage-review: changeName 不能为空')
|
|
479
|
+
if (!stage || !STAGE_MAIN_DOC[stage]) {
|
|
480
|
+
throw new Error(`register-stage-review: stage 无效 "${stage}"(应为 brainstorm|plan|execute)`)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
484
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
485
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
486
|
+
const reviewType = STAGE_REVIEW_TYPE[stage]
|
|
487
|
+
const mainDoc = STAGE_MAIN_DOC[stage]
|
|
488
|
+
const mainDocPath = join(changeDir, mainDoc)
|
|
489
|
+
if (!existsSync(mainDocPath)) {
|
|
490
|
+
throw new Error(`register-stage-review: 主审查文档不存在 ${mainDocPath}(${stage} 审 ${mainDoc}),无法算 docHash`)
|
|
491
|
+
}
|
|
492
|
+
const docHash = computeDocHash(mainDocPath)
|
|
493
|
+
if (!docHash) {
|
|
494
|
+
throw new Error(`register-stage-review: 计算 docHash 失败(computeDocHash 返回 null):${mainDocPath}`)
|
|
495
|
+
}
|
|
496
|
+
const reviewedFiles = [`changes/${changeName}/${mainDoc}`]
|
|
497
|
+
|
|
498
|
+
// ── refresh-hash 模式(坑 stage-review-dochash-manual-resync,2026-08-21 实证):改一版 design
|
|
499
|
+
// 要重算 2-3 个 stage review 的 docHash(brainstorm/execute 都锚 design.md),手工易漏、gate
|
|
500
|
+
// 报错才补。就地更新 marker 指向的已有 review.json——保留 verdict/checklist/requiredEvidence
|
|
501
|
+
// (结论是否仍适用由人确认),仅重算 docHash + reviewerNotes 追加刷新记录;不换 run 目录
|
|
502
|
+
// (换新 run 会把 marker 重定向到骨架,丢已审结论)。无既有 review → 报错(该用骨架模式生成)。
|
|
503
|
+
if (refreshHash) {
|
|
504
|
+
if (fromFile) throw new Error('register-stage-review: --refresh-hash 与 --from 互斥(refresh 就地更新既有 review,adopt 引入新文件)')
|
|
505
|
+
const existingRunId = getLatestStageReviewRunId(runtimeRoot, stage, changeName)
|
|
506
|
+
const existingPath = existingRunId
|
|
507
|
+
? join(runtimeRoot, 'stage-reviews', `${stage}-${existingRunId}`, 'review.json')
|
|
508
|
+
: null
|
|
509
|
+
if (!existingPath || !existsSync(existingPath)) {
|
|
510
|
+
throw new Error(`register-stage-review: --refresh-hash 找不到 ${stage} 的既有 review.json(marker ${existingRunId || '无'} 无对应 run)——先去掉 --refresh-hash 生成骨架,或派审查子代理产出 review`)
|
|
511
|
+
}
|
|
512
|
+
let existing
|
|
513
|
+
try {
|
|
514
|
+
existing = JSON.parse(readFileSync(existingPath, 'utf8'))
|
|
515
|
+
} catch (e) {
|
|
516
|
+
throw new Error(`register-stage-review: 既有 review.json 解析失败 ${existingPath}: ${e.message}`)
|
|
517
|
+
}
|
|
518
|
+
const refreshedAt = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
|
519
|
+
const review = {
|
|
520
|
+
...existing,
|
|
521
|
+
docHash,
|
|
522
|
+
reviewedFiles,
|
|
523
|
+
reviewerNotes: `${existing.reviewerNotes || ''}\ndocHash refreshed at ${refreshedAt}(主文档改版后机械重算;verdict 结论是否仍适用需人工确认)`.trim(),
|
|
524
|
+
}
|
|
525
|
+
writeFileSync(existingPath, JSON.stringify(review, null, 2) + '\n')
|
|
526
|
+
const schemaRecheck = validateStageReviewSchema(review)
|
|
527
|
+
if (!schemaRecheck.ok) {
|
|
528
|
+
throw new Error(`register-stage-review: refresh 后 schema 自检失败 — ${schemaRecheck.errors.join('; ')}(原 review 字段异常,先修原文件)`)
|
|
529
|
+
}
|
|
530
|
+
const hashRecheck = verifyStageReviewDocHash(review, [specBase, changeDir, cwd])
|
|
531
|
+
if (!hashRecheck.ok) {
|
|
532
|
+
throw new Error(`register-stage-review: refresh 后 docHash 自检失败(不应发生)— ${hashRecheck.errors.join('; ')}`)
|
|
533
|
+
}
|
|
534
|
+
return { ok: true, reviewRunId: existingRunId, reviewPath: existingPath, markerPath: stageReviewMarkerPath(runtimeRoot, stage, changeName), mode: 'refreshed', mainDoc, review }
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
let review
|
|
538
|
+
let mode
|
|
539
|
+
if (fromFile) {
|
|
540
|
+
// adopt 模式:保留 agent 的 verdict/checklist/reviewerNotes/requiredEvidence,仅修 mechanics
|
|
541
|
+
const fromAbs = existsSync(fromFile) ? fromFile : join(cwd, fromFile)
|
|
542
|
+
if (!existsSync(fromAbs)) {
|
|
543
|
+
throw new Error(`register-stage-review: --from 文件不存在 ${fromFile}(相对 cwd ${fromAbs} 也未命中)`)
|
|
544
|
+
}
|
|
545
|
+
let parsed
|
|
546
|
+
try {
|
|
547
|
+
parsed = JSON.parse(readFileSync(fromAbs, 'utf8'))
|
|
548
|
+
} catch (e) {
|
|
549
|
+
throw new Error(`register-stage-review: --from 文件 JSON 解析失败 ${fromAbs}: ${e.message}`)
|
|
550
|
+
}
|
|
551
|
+
const schemaResult = validateStageReviewSchema(parsed)
|
|
552
|
+
if (!schemaResult.ok) {
|
|
553
|
+
throw new Error(`register-stage-review: --from 文件 schema 校验失败 — ${schemaResult.errors.join('; ')}`)
|
|
554
|
+
}
|
|
555
|
+
review = { ...parsed, docHash, reviewedFiles }
|
|
556
|
+
mode = 'adopted'
|
|
557
|
+
} else {
|
|
558
|
+
// 骨架模式:cannot_verify 待审占位(schema 反逃逸:cannot_verify 必须带非空 requiredEvidence)
|
|
559
|
+
review = {
|
|
560
|
+
schemaVersion: REVIEW_SCHEMA_VERSION,
|
|
561
|
+
reviewType,
|
|
562
|
+
specVerdict: 'cannot_verify',
|
|
563
|
+
qualityVerdict: 'cannot_verify',
|
|
564
|
+
reviewedFiles,
|
|
565
|
+
docHash,
|
|
566
|
+
requiredEvidence: [`待独立审查子代理对照 ${mainDoc} 逐节核验(骨架由 register-stage-review 生成)`],
|
|
567
|
+
reviewerNotes: '骨架由 register-stage-review 生成,verdict 待独立审查子代理填写',
|
|
568
|
+
}
|
|
569
|
+
mode = 'skeleton'
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const reviewRunId = generateStageReviewRunId()
|
|
573
|
+
const reviewDir = join(runtimeRoot, 'stage-reviews', `${stage}-${reviewRunId}`)
|
|
574
|
+
const reviewPath = join(reviewDir, 'review.json')
|
|
575
|
+
mkdirSync(reviewDir, { recursive: true })
|
|
576
|
+
writeFileSync(reviewPath, JSON.stringify(review, null, 2) + '\n')
|
|
577
|
+
|
|
578
|
+
const markerPath = stageReviewMarkerPath(runtimeRoot, stage, changeName)
|
|
579
|
+
if (existsSync(markerPath)) {
|
|
580
|
+
console.warn(`⚠️ register-stage-review: marker 已存在 ${markerPath},将被覆盖为 ${reviewRunId}`)
|
|
581
|
+
}
|
|
582
|
+
mkdirSync(runtimeRoot, { recursive: true })
|
|
583
|
+
writeFileSync(markerPath, reviewRunId + '\n')
|
|
584
|
+
|
|
585
|
+
// self-check(fail-closed):刚写的 review 必过 schema + docHash 真实性。只验 mechanics,
|
|
586
|
+
// 不判 verdict(verdict 是 agent/子代理的审查结论,even fail 也如实落盘,由 Stage Review Gate 裁决)。
|
|
587
|
+
const schemaRecheck = validateStageReviewSchema(review)
|
|
588
|
+
if (!schemaRecheck.ok) {
|
|
589
|
+
throw new Error(`register-stage-review: 写入后 schema 自检失败(不应发生)— ${schemaRecheck.errors.join('; ')}`)
|
|
590
|
+
}
|
|
591
|
+
const hashRecheck = verifyStageReviewDocHash(review, [specBase, changeDir, cwd])
|
|
592
|
+
if (!hashRecheck.ok) {
|
|
593
|
+
throw new Error(`register-stage-review: 写入后 docHash 自检失败(不应发生)— ${hashRecheck.errors.join('; ')}`)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return { ok: true, reviewRunId, reviewPath, markerPath, mode, mainDoc, review }
|
|
597
|
+
}
|