sillyspec 3.29.4 → 3.31.0
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-auto/SKILL.md +1 -1
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +7 -105
- package/AGENTS.md +38 -20
- package/SKILL.md +9 -3
- package/assets/command-cards/flow.md +32 -0
- package/assets/command-cards/run-archive.md +26 -0
- package/assets/command-cards/run-brainstorm.md +33 -0
- package/assets/command-cards/run-execute.md +29 -0
- package/assets/command-cards/run-plan.md +27 -0
- package/assets/command-cards/run-quick.md +17 -0
- package/assets/command-cards/run-verify.md +29 -0
- package/assets/command-cards/status.md +27 -0
- package/package.json +2 -1
- package/packages/dashboard/server/executor.js +2 -1
- package/packages/dashboard/server/parser.js +7 -7
- package/r7-test-env/README-R7-TEST.md +93 -0
- package/r7-test-env/demo-project/lib/calc.js +6 -0
- package/r7-test-env/demo-project/package.json +1 -0
- package/r7-test-env/demo-project/test.js +10 -0
- package/r7-test-env/prompt-R7-SMOKE.md +41 -0
- package/r7-test-env/sillyspec-r7.cmd +3 -0
- package/round4/account.mjs +98 -0
- package/round4/b1-analyze-task08.mjs +40 -0
- package/round4/b1-b1-target-validation.md +32 -0
- package/round4/b3a-prompt-audit-result.md +66 -0
- package/round4/b3a-prompt-audit.mjs +89 -0
- package/round4/brief-autocompact.md +34 -0
- package/round4/brief-session-replay.md +50 -0
- package/round4/deep-read-synthesis.md +38 -0
- package/round4/extract-full.mjs +38 -0
- package/round4/extract-timeline.mjs +43 -0
- package/round4/find-sessions.mjs +13 -0
- package/round4/full-r4-l.md +4707 -0
- package/round4/full-r4-o-l.md +4300 -0
- package/round4/full-r4-os.md +1681 -0
- package/round4/full-r4-sf.md +4255 -0
- package/round4/full-r4-sq.md +1734 -0
- package/round4/gate-audit-relay-receipt.md +30 -0
- package/round4/gate-value-audit.md +52 -0
- package/round4/optimization-plan.md +165 -0
- package/round4/prereg.md +67 -0
- package/round4/prompt-R4-L.md +63 -0
- package/round4/prompt-R4-O-L.md +59 -0
- package/round4/prompt-R4-O-S.md +56 -0
- package/round4/prompt-R4-S-F.md +60 -0
- package/round4/prompt-R4-S-Q.md +66 -0
- package/round4/r4-final-report.html +347 -0
- package/round4/r4-os-timeline.txt +297 -0
- package/round4/r4-sf-timeline.txt +738 -0
- package/round4/r4-sq-timeline.txt +322 -0
- package/round4/r4l-timeline.txt +793 -0
- package/round4/r4ol-timeline.txt +729 -0
- package/round4/r4sf-timeline.txt +852 -0
- package/round4/r5-scenario-matrix.md +46 -0
- package/round4/result-S.md +171 -0
- package/round4/result-final.md +46 -0
- package/round4/result-quality.md +63 -0
- package/src/change-list.js +9 -0
- package/src/classify-change.js +6 -0
- package/src/command-cards.js +74 -0
- package/src/commit-guard.js +2 -0
- package/src/config-schema.js +64 -12
- package/src/contract-matrix.js +26 -5
- package/src/db.js +35 -4
- package/src/decision-distill.js +9 -3
- package/src/diagnostic-codes.js +2 -1
- package/src/docs-check.js +1 -1
- package/src/doctor-diagnostics.js +44 -2
- package/src/flow-draft.js +748 -0
- package/src/flow-parity.js +236 -0
- package/src/flow-review.js +184 -0
- package/src/flow.js +1492 -0
- package/src/foreign-declared.js +17 -1
- package/src/fr-index.js +396 -22
- package/src/fs-atomic.js +5 -1
- package/src/git-helper.js +2 -2
- package/src/greenfield-bootstrap.js +98 -0
- package/src/handoff.js +120 -7
- package/src/hunk-attribution.js +191 -0
- package/src/index.js +602 -21
- package/src/init.js +25 -8
- package/src/knowledge-classify.js +47 -1
- package/src/knowledge-digest.js +162 -0
- package/src/knowledge-quicklog.js +246 -0
- package/src/machine-draft.js +128 -0
- package/src/machine-interface.js +194 -7
- package/src/mcp-server.js +1 -0
- package/src/module-impact.js +51 -22
- package/src/module-resolve.js +3 -1
- package/src/preview-progress.js +160 -0
- package/src/progress/change-registry.js +11 -3
- package/src/progress/shared.js +3 -2
- package/src/progress.js +56 -13
- package/src/quicklog.js +44 -6
- package/src/redomain.js +147 -0
- package/src/review-material-pack.js +204 -1
- package/src/run/archive-distill.js +4 -2
- package/src/run/bg-sync.js +67 -0
- package/src/run/command.js +127 -10
- package/src/run/complete-handlers.js +363 -73
- package/src/run/complete.js +356 -74
- package/src/run/gate-snapshot-ledger.js +213 -0
- package/src/run/gate-snapshot.js +260 -38
- package/src/run/gates.js +482 -268
- package/src/run/green-cache.js +123 -0
- package/src/run/junction-rm.js +70 -0
- package/src/run/next.js +14 -0
- package/src/run/prompt.js +185 -55
- package/src/run/quick-audit.js +213 -14
- package/src/run/shared.js +123 -4
- package/src/run/stage.js +182 -216
- package/src/run/test-ledger.js +264 -0
- package/src/run/verify-quality-scan.js +84 -7
- package/src/scope-audit.js +6 -1
- package/src/sentinel-assertions.js +114 -0
- package/src/setup.js +3 -3
- package/src/spec-sync.js +41 -1
- package/src/stage-contract.js +26 -8
- package/src/stage-review-checklist.js +2 -0
- package/src/stage-review.js +7 -2
- package/src/stage-templates.js +157 -0
- package/src/stages/archive.js +2 -0
- package/src/stages/brainstorm.js +24 -106
- package/src/stages/execute.js +222 -85
- package/src/stages/knowledge.js +74 -1
- package/src/stages/plan-postcheck.js +385 -20
- package/src/stages/plan.js +23 -1
- package/src/stages/quick.js +10 -2
- package/src/stages/verify.js +4 -2
- package/src/sync.js +81 -23
- package/src/task-done.js +181 -0
- package/src/task-review.js +8 -55
- package/src/taskcard-frontmatter.js +57 -0
- package/src/test-bindings.js +549 -0
- package/src/timeline.js +218 -0
- package/src/ui-visual.js +175 -0
- package/src/verify-draft.js +214 -0
- package/src/verify-postcheck.js +919 -159
- package/src/verify-probes.js +103 -25
- package/src/watcher.js +1031 -0
- package/src/worktree-apply.js +44 -4
- package/src/worktree-deps.js +22 -8
- package/src/worktree-policy.js +42 -0
- package/src/worktree.js +102 -15
- package/templates/agents-instruction.md +37 -18
package/src/flow.js
ADDED
|
@@ -0,0 +1,1492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flow.js — 2-调用协议(R7 切片二 / D-002 D-003 D-007 / FR-03~06)。
|
|
3
|
+
*
|
|
4
|
+
* 协议形状属性(机械 harness 可验,非 agent 配额):轻量跑道 CLI 必需交互 = 2——
|
|
5
|
+
* ① `flow start`(一次下发:建 change + 基线锚定 + 轻量变更说明 + 全部材料**路径**清单
|
|
6
|
+
* ——稳定前缀=缓存最优;已存在 change → 恢复简报(盘面状态:checkbox/提交/账本/
|
|
7
|
+
* dirty files → 做到哪、剩什么));
|
|
8
|
+
* ② `flow done`(唯一裁决点:六子步幂等——工件校验/P2 账本对账+亲测/探针/distill/
|
|
9
|
+
* 归档/事件收口)。
|
|
10
|
+
* 中间零协议必需交互;agent 自愿 status/verify 合法不计入(D-007:协议记账单位=change 级
|
|
11
|
+
* ≈ 一次 GSD Phase;task 卡是干活单位非协议检查点)。
|
|
12
|
+
*
|
|
13
|
+
* fail-closed 三句(用户裁定钉死):①亲自实测失败=整单 FAIL exit≠0(不继续 distill/归档);
|
|
14
|
+
* ②实测超时=失败(同 quick 门,无部分成功当绿);③半态可重入不可假绿——归档子步未完成前
|
|
15
|
+
* change 仍 active,重入从断点续不新开 change。
|
|
16
|
+
*
|
|
17
|
+
* 配置(local.yaml,config-schema 注册;design 措辞 `flow: thin|legacy` 落地为单键
|
|
18
|
+
* `flow.mode: thin|legacy`——YAML 单键形态语义一致):缺省 thin(2026-09-25-thin-default-flip
|
|
19
|
+
* 入口归一——D-010@v2 实验通道定位由轻量变更加固件【设计记录/测试绑定/patch 留档/预段收编】推翻,
|
|
20
|
+
* quick 退役第 1 步);legacy=显式回旧道,既有 run <stage> 全族逐字不动,flow start 拒跑并指路
|
|
21
|
+
* (切回一行 yaml)。thin change 上跑 run <stage> =
|
|
22
|
+
* 混跑回退(flow-state 落 legacy_fallback,flow done 按厚档裁决——两套记账不叠加)。
|
|
23
|
+
*
|
|
24
|
+
* 状态落文件不落 DB(红线):.sillyspec/changes/<名>/flow-state.yaml(tier/baseline_commit/
|
|
25
|
+
* 六子步完成标记/legacy_fallback/route_hint)——fs-atomic 原子写,缺文件=未参与 thin。
|
|
26
|
+
* 幂等循 task-done 先例:子步各查自身完成标记,中断半态重入断点续,中段失败精确报告。
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync } from 'node:fs'
|
|
29
|
+
import { createHash } from 'node:crypto'
|
|
30
|
+
import { join, relative } from 'node:path'
|
|
31
|
+
import yaml from 'js-yaml'
|
|
32
|
+
import { git, gitQuiet } from './git-helper.js'
|
|
33
|
+
import { writeAtomicSync } from './fs-atomic.js'
|
|
34
|
+
import { resolveRuntimeRoot, triggerSync } from './run/shared.js'
|
|
35
|
+
import { detectUiTouch, buildUiGuidanceLines, runUiVisualProbe, readUiVisualGate, UI_EVIDENCE_FILENAME } from './ui-visual.js'
|
|
36
|
+
import { runHunkAttributionGate, readHunkGate, renderHunkAttributionLines } from './hunk-attribution.js'
|
|
37
|
+
|
|
38
|
+
const FLOW_STATE_FILE = 'flow-state.yaml'
|
|
39
|
+
const SUBSTEPS = ['artifacts', 'ledger', 'patch', 'review', 'probes', 'distill', 'archive', 'events']
|
|
40
|
+
|
|
41
|
+
/** 读 flow-state(缺文件/损坏 → null = 未参与 thin)。 */
|
|
42
|
+
export function readFlowState(changeDir) {
|
|
43
|
+
try {
|
|
44
|
+
const raw = readFileSync(join(changeDir, FLOW_STATE_FILE), 'utf8')
|
|
45
|
+
const obj = yaml.load(raw)
|
|
46
|
+
return obj && typeof obj === 'object' ? obj : null
|
|
47
|
+
} catch {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 原子写 flow-state(patch 合并,幂等)。 */
|
|
53
|
+
export function writeFlowState(changeDir, patch) {
|
|
54
|
+
const cur = readFlowState(changeDir) || {}
|
|
55
|
+
writeAtomicSync(join(changeDir, FLOW_STATE_FILE), yaml.dump({
|
|
56
|
+
tier: 'thin',
|
|
57
|
+
legacy_fallback: false,
|
|
58
|
+
substeps: {},
|
|
59
|
+
...cur,
|
|
60
|
+
...patch,
|
|
61
|
+
substeps: { ...(cur.substeps || {}), ...(patch.substeps || {}) },
|
|
62
|
+
updatedAt: new Date().toISOString(),
|
|
63
|
+
}, { lineWidth: 120 }) + '\n')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 读 local.yaml 的 flow 配置(缺省 thin——2026-09-25-thin-default-flip 翻转:入口归一,
|
|
67
|
+
* D-010@v2 实验通道定位由轻量变更加固件(设计记录/测试绑定/patch 留档/预段收编)的数据推翻;
|
|
68
|
+
* 显式 mode: legacy 的仓维持旧道。文本级读同既有习惯)。 */
|
|
69
|
+
export function readFlowConfig(specBase) {
|
|
70
|
+
try {
|
|
71
|
+
const raw = readFileSync(join(specBase, 'local.yaml'), 'utf8')
|
|
72
|
+
const m = raw.match(/^\s*mode\s*:\s*(thin|legacy)\s*$/m)
|
|
73
|
+
let mode = m ? m[1] : 'thin'
|
|
74
|
+
if (/^flow\s*:\s*(thin|legacy)\s*$/m.test(raw)) mode = raw.match(/^flow\s*:\s*(thin|legacy)\s*$/m)[1]
|
|
75
|
+
const th = raw.match(/^\s*edit_ratio_threshold\s*:\s*([0-9.]+)\s*$/m)
|
|
76
|
+
const en = raw.match(/^\s*edit_ratio_enforcement\s*:\s*(advisory|block)\s*$/m)
|
|
77
|
+
return {
|
|
78
|
+
mode,
|
|
79
|
+
editRatioThreshold: th ? Number(th[1]) : 0.5,
|
|
80
|
+
editRatioEnforcement: en ? en[1] : 'advisory',
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
return { mode: 'thin', editRatioThreshold: 0.5, editRatioEnforcement: 'advisory' }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 基线以来变更文件(git diff 基线→HEAD + 工作区 dirty——flow done 测试门清单来源,D-003)。
|
|
88
|
+
* 排除 .sillyspec/ 内部产物(观测对象≠交付物;spec 文件不触发测试门也不算 dirty 恢复信号)。 */
|
|
89
|
+
function changedFilesSinceBaseline(cwd, baselineCommit) {
|
|
90
|
+
const files = new Set()
|
|
91
|
+
const addIfDeliverable = (p) => {
|
|
92
|
+
if (p && !p.replace(/\\/g, '/').startsWith('.sillyspec/')) files.add(p)
|
|
93
|
+
}
|
|
94
|
+
if (baselineCommit) {
|
|
95
|
+
const d = gitQuiet(cwd, ['diff', '--name-only', `${baselineCommit}..HEAD`])
|
|
96
|
+
if (d) for (const l of String(d).split('\n')) if (l.trim()) addIfDeliverable(l.trim())
|
|
97
|
+
}
|
|
98
|
+
const s = gitQuiet(cwd, ['status', '--porcelain'])
|
|
99
|
+
if (s) {
|
|
100
|
+
for (const line of String(s).split('\n')) {
|
|
101
|
+
if (!line || line.length < 4) continue
|
|
102
|
+
const p = line.slice(3).trim().replace(/^"|"$/g, '')
|
|
103
|
+
const arrow = p.indexOf(' -> ')
|
|
104
|
+
const path = arrow !== -1 ? p.slice(arrow + 4) : p
|
|
105
|
+
addIfDeliverable(path)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return [...files]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 材料路径清单(稳定前缀——缓存最优;按在场性列出,不读内容)。 */
|
|
112
|
+
function materialPaths(specBase, changeName, changeDir) {
|
|
113
|
+
const paths = [
|
|
114
|
+
join(changeDir, FLOW_STATE_FILE),
|
|
115
|
+
join(specBase, 'docs', 'sillyspec', 'scan', 'PROJECT.md'),
|
|
116
|
+
join(specBase, 'docs', 'sillyspec', 'scan', 'CONVENTIONS.md'),
|
|
117
|
+
join(specBase, 'docs', 'sillyspec', 'modules', '_module-map.yaml'),
|
|
118
|
+
join(specBase, 'knowledge', 'INDEX.md'),
|
|
119
|
+
]
|
|
120
|
+
return paths.filter((p) => { try { return existsSync(p) } catch { return false } })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** --input 语料中的路径样 token 提取(fresh 起点的域路由依据——best-effort:无 design/diff 时
|
|
124
|
+
* 唯一可判材料;提取失败=空数组,走空域诚实提示)。 */
|
|
125
|
+
function extractInputPaths(input) {
|
|
126
|
+
const out = new Set()
|
|
127
|
+
for (const m of String(input || '').matchAll(/[\w.@+-]+(?:\/[\w.@+-]+)+/g)) {
|
|
128
|
+
const p = m[0].replace(/\/+$/, '')
|
|
129
|
+
if (p.length > 1) out.add(p.replace(/\\/g, '/'))
|
|
130
|
+
}
|
|
131
|
+
return [...out]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 轻量道知识注入段(2026-09-25-thin-fr-inject-parity):默认快道读取面对齐——
|
|
136
|
+
* {FR_INDEX_DIGEST}/{DECISION_HITS}/execute 知识命中报告此前只在 run 族装配,轻量道 fresh
|
|
137
|
+
* 不经 brainstorm,knowledge/fr 现行 FR、否决决策、已知坑对 agent 全不可见(实证:FR-runtime-020
|
|
138
|
+
* 与实现相反长期无人撞见)。域路由与 distill 同口径(resolveTouchedDomains 的 filesOverride
|
|
139
|
+
* 旁路):fresh=--input 提取路径、resume=基线 diff、adopt=design 工件交付清单。
|
|
140
|
+
* 纯读 fail-open;注入段独立成块(材料清单稳定前缀不被动态内容污染);空态留一行可见。
|
|
141
|
+
* @returns {{ lines: string[], summary: { domains: string[], frCount: number, rejectedDecisions: number, knowledgeEntries: number } }}
|
|
142
|
+
*/
|
|
143
|
+
export async function flowKnowledgeDigest({ specBase, change, changeDir, input, filesOverride }) {
|
|
144
|
+
const summary = { domains: [], frCount: 0, rejectedDecisions: 0, knowledgeEntries: 0 }
|
|
145
|
+
const lines = []
|
|
146
|
+
try {
|
|
147
|
+
const knowledgeRoot = join(specBase, 'knowledge')
|
|
148
|
+
const { discoverModuleIndex } = await import('./decision-distill.js')
|
|
149
|
+
const { resolveTouchedDomains, readActiveFrDigest } = await import('./fr-index.js')
|
|
150
|
+
const moduleIndex = discoverModuleIndex(knowledgeRoot)
|
|
151
|
+
let domains = []
|
|
152
|
+
let basis = ''
|
|
153
|
+
if (Array.isArray(filesOverride) && filesOverride.length > 0) {
|
|
154
|
+
domains = resolveTouchedDomains(changeDir, moduleIndex, filesOverride).filter((d) => d !== 'unmapped')
|
|
155
|
+
basis = filesOverride.length > 0 ? 'input/diff 路径' : ''
|
|
156
|
+
} else if (existsSync(join(changeDir, 'design.md'))) {
|
|
157
|
+
domains = resolveTouchedDomains(changeDir, moduleIndex).filter((d) => d !== 'unmapped')
|
|
158
|
+
basis = 'design.md 交付清单'
|
|
159
|
+
}
|
|
160
|
+
const frs = domains.length > 0 ? readActiveFrDigest(knowledgeRoot, domains) : []
|
|
161
|
+
summary.domains = domains
|
|
162
|
+
summary.frCount = frs.length
|
|
163
|
+
lines.push(`🧠 知识注入(轻量道读取面——现行 FR/否决决策/已知坑,动手前扫一眼):`)
|
|
164
|
+
if (domains.length === 0) {
|
|
165
|
+
lines.push(` 触达域:起点无依据(--input 无路径语料、无 design 清单)——干活涉及 src 后 resume/收口会补查;`)
|
|
166
|
+
lines.push(` 相关域现行 FR 可自行查 knowledge/fr/<域>.md(INDEX.md 有路由)。`)
|
|
167
|
+
} else {
|
|
168
|
+
lines.push(` 触达域(${basis}):${domains.join('、')}`)
|
|
169
|
+
if (frs.length === 0) {
|
|
170
|
+
lines.push(` 现行 FR:(该域暂无 active FR 索引条目——本变更大概率是首批需求)`)
|
|
171
|
+
} else {
|
|
172
|
+
const flagged = frs.filter((f) => f.needsReview)
|
|
173
|
+
const ordered = [...flagged, ...frs.filter((f) => !f.needsReview)]
|
|
174
|
+
for (const f of ordered.slice(0, 8)) {
|
|
175
|
+
lines.push(` - ${f.id} ${f.title}${f.needsReview ? ` ⚠️待复核(${f.needsReview})` : ''}${f.unconfirmed > 0 ? ` ⚪${f.unconfirmed}未确认绑定` : ''}`)
|
|
176
|
+
}
|
|
177
|
+
if (ordered.length > 8) lines.push(` (+${ordered.length - 8} 条见 knowledge/fr/ 对应域文件)`)
|
|
178
|
+
// 抽查确认(2026-09-27-confirm-on-use 三层治理①层):干活中本来就在消费这些条目——
|
|
179
|
+
// 相符则收口前翻牌(机械防橡皮图章:--evidence 必须是可解析的真实测试路径),
|
|
180
|
+
// 不符留给 knowledge digest 信号。至多点名 2 个(抽查式,防全勾仪式化)。
|
|
181
|
+
const unconfirmed = ordered.filter((f) => f.unconfirmed > 0).slice(0, 2)
|
|
182
|
+
if (unconfirmed.length > 0) {
|
|
183
|
+
lines.push(` 🔍 抽查确认(至多 ${unconfirmed.length} 条,干活中顺带核):${unconfirmed.map((f) => f.id).join('、')} —— 绑定与实态相符则收口前 \`sillyspec tests confirm --anchor <id> --evidence <真实测试路径>\`(翻 active);不符则不动,留给 knowledge digest 信号`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const { matchKnowledge } = await import('./knowledge-match.js')
|
|
188
|
+
const km = matchKnowledge(knowledgeRoot, `${change}\n${input || ''}`)
|
|
189
|
+
const rejected = (km.decisionHits || []).filter((h) => h.status === 'rejected')
|
|
190
|
+
summary.rejectedDecisions = rejected.length
|
|
191
|
+
summary.knowledgeEntries = km.matched ? (km.entries || []).length : 0
|
|
192
|
+
if (rejected.length > 0) {
|
|
193
|
+
lines.push(` ⚠️ 否决决策(历史已否决,防复潮——除非复潮条件满足勿重提,复潮须在本变更 decisions.md 记新版本):`)
|
|
194
|
+
for (const h of rejected.slice(0, 5)) {
|
|
195
|
+
lines.push(` - ${h.id} ${h.title}(${h.file})否决理由:${h.reason || '(未记录)'}`)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (km.matched && (km.entries || []).length > 0) {
|
|
199
|
+
const src = km.entries.slice(0, 3).map((e) => (e.anchor ? `${e.file}#${e.anchor}` : e.file)).join(';')
|
|
200
|
+
lines.push(` 📚 知识命中(INDEX 关键词,按需 Read):${src}`)
|
|
201
|
+
}
|
|
202
|
+
if (domains.length === 0 && rejected.length === 0 && !km.matched) {
|
|
203
|
+
lines.length = 0
|
|
204
|
+
lines.push(`🧠 知识注入(轻量道读取面):语料未命中知识库(无触达域依据、无否决命中、INDEX 无对齐条目)——可自行查 knowledge/INDEX.md。`)
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
lines.length = 0
|
|
208
|
+
lines.push(`🧠 知识注入(轻量道读取面):注入失败(${(e && e.message) || e})——可自行读 knowledge/INDEX.md 与 knowledge/fr/`)
|
|
209
|
+
}
|
|
210
|
+
return { lines, summary }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* flow done 收口的 FR 腐烂 suspect(2026-09-25-thin-fr-inject-parity 起,fr-rot-precision 收紧):
|
|
215
|
+
* quick-done 钩子迁轻量道 + 按文件面交集判相关度(域级全标→三分判据,评审实测基线 131 条 →
|
|
216
|
+
* strong 83 / unknown 8 / skip 40)。对触达域每条 active FR:coverage = frCoverageFiles(来源变更)
|
|
217
|
+
* ∪ bindings,与本次归属文件面单向匹配(changed 恒文件级:相等 || changed.startsWith(cov 补/):
|
|
218
|
+
* 交集非空 → strong(打待复核标记,下次知识注入带 ⚠️);coverage 空 → unknown(不打标,遥测
|
|
219
|
+
* 单列——宁漏勿滥:漏标只损失注入排序优先级);非空无交集 → skip。
|
|
220
|
+
* 遥测 count 语义=strong(防污染 knowledge-stats 的 rotSuspectByDomain 消费读数,评审 P1-1)。
|
|
221
|
+
* advisory 零阻断,fail-open。
|
|
222
|
+
* @returns {{ warn: string|null, warnInfo?: string|null, domains: string[], strong: number, unknown: number, skip: number, marked: number }}
|
|
223
|
+
*/
|
|
224
|
+
export async function rotSuspectFlow({ specBase, change, changeDir, files }) {
|
|
225
|
+
// 查询核迁 fr-index.activeFrCoverageHits 单源(2026-09-26-dynamic-test-inference)——
|
|
226
|
+
// 同一「覆盖面∩触碰文件」查询双消费:本函数打待复核标记(信号面);verify 门
|
|
227
|
+
// collectFrLinkedTests 反用为需求关联回归测试面。判据语义逐字不变。
|
|
228
|
+
const { activeFrCoverageHits, markFrNeedsReview } = await import('./fr-index.js')
|
|
229
|
+
const q = activeFrCoverageHits({ specBase, change, changeDir, files })
|
|
230
|
+
const knowledgeRoot = join(specBase, 'knowledge')
|
|
231
|
+
const { domains, hits: strong, unknownSources, unknownFrCount, skip } = q
|
|
232
|
+
if (domains.length === 0) {
|
|
233
|
+
return { warn: null, warnInfo: null, domains, strong: 0, unknown: 0, skip: 0, marked: 0 }
|
|
234
|
+
}
|
|
235
|
+
const { appendKnowledgeHit } = await import('./knowledge-hits.js')
|
|
236
|
+
appendKnowledgeHit(join(specBase, '.runtime'), {
|
|
237
|
+
type: 'fr-rot-suspect', change, domains,
|
|
238
|
+
// unknown=覆盖面不可判定的 FR 条数(评审 P2-1 清偿:与旧版 frs.length-strong-skip 口径逐字等价,
|
|
239
|
+
// 非来源变更数);unknownSources 披露来源名单(FR 多条可共享一个来源变更)
|
|
240
|
+
strong: strong.length, unknown: unknownFrCount, skip, count: strong.length, // count=strong:knowledge-stats 消费口径(评审 P1-1)
|
|
241
|
+
unknownSources: [...unknownSources], source: 'flow-done',
|
|
242
|
+
})
|
|
243
|
+
let marked = 0
|
|
244
|
+
if (strong.length > 0) {
|
|
245
|
+
const mr = markFrNeedsReview(knowledgeRoot, strong.map((f) => f.id), change)
|
|
246
|
+
marked = mr.marked
|
|
247
|
+
}
|
|
248
|
+
const warn = strong.length > 0
|
|
249
|
+
? `⚠️ [FR 腐烂 suspect·advisory] 触达 ${domains.join('、')} 域的 ${strong.length} 条 active FR 与本次交付文件面有覆盖交集——若改动影响这些行为,请在 requirements 承接/supersede 对账(已打待复核标记 ${marked} 条;下次知识注入带 ⚠️)`
|
|
250
|
+
: null
|
|
251
|
+
const warnInfo = unknownFrCount > 0
|
|
252
|
+
? `ℹ️ 另有 ${unknownFrCount} 条 active FR 无法判定覆盖面(来源变更无归档件且无测试绑定,不计入 suspect,不打标):${unknownSources.slice(0, 5).join('、')}${unknownSources.length > 5 ? ' 等' : ''}`
|
|
253
|
+
: null
|
|
254
|
+
return { warn, warnInfo, domains, strong: strong.length, unknown: unknownFrCount, skip, marked }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* flow done distill 前的 FR 重复嫌疑软门(2026-09-25-thin-fr-inject-parity):brainstorm --done
|
|
259
|
+
* 判据迁轻量道——新 FR(无承接)× 同域 active 条目标题 bigram 重叠 ≥0.6 → advisory warning +
|
|
260
|
+
* fr-duplicate-warning 遥测。双出路:requirements 加承接行或改标题区分;不阻断。
|
|
261
|
+
* @returns {{ warn: string|null, hits: Array<{ local: string, active: string, title: string }> }}
|
|
262
|
+
*/
|
|
263
|
+
export async function frDupGateFlow({ specBase, change, changeDir, files }) {
|
|
264
|
+
const { parseChangeRequirements, resolveTouchedDomains, readActiveFrDigest, frTitleOverlap, FR_TITLE_OVERLAP_THRESHOLD } = await import('./fr-index.js')
|
|
265
|
+
const req = parseChangeRequirements(changeDir)
|
|
266
|
+
if (req.missing || req.frs.length === 0) return { warn: null, hits: [] }
|
|
267
|
+
const knowledgeRoot = join(specBase, 'knowledge')
|
|
268
|
+
const { discoverModuleIndex } = await import('./decision-distill.js')
|
|
269
|
+
const moduleIndex = discoverModuleIndex(knowledgeRoot)
|
|
270
|
+
const domains = resolveTouchedDomains(changeDir, moduleIndex, Array.isArray(files) ? files : []).filter((d) => d !== 'unmapped')
|
|
271
|
+
if (domains.length === 0) return { warn: null, hits: [] }
|
|
272
|
+
const active = readActiveFrDigest(knowledgeRoot, domains)
|
|
273
|
+
const hits = []
|
|
274
|
+
for (const fr of req.frs) {
|
|
275
|
+
if ((fr.supersedes || []).length > 0 || !fr.title) continue
|
|
276
|
+
// 取最高重叠对(fr-rot-precision:对齐 brainstorm 软门语义——多命中时指认最相近的,非首个过阈者)
|
|
277
|
+
let hit = null
|
|
278
|
+
for (const a of active) {
|
|
279
|
+
const o = frTitleOverlap(fr.title, a.title)
|
|
280
|
+
if (!hit || o > hit.o) hit = { a, o }
|
|
281
|
+
}
|
|
282
|
+
if (hit && hit.o >= FR_TITLE_OVERLAP_THRESHOLD) {
|
|
283
|
+
const scen = (hit.a.scenarios || []).filter((s) => s && s !== '(无场景名)').join(';')
|
|
284
|
+
hits.push({ local: fr.local, active: hit.a.id, title: hit.a.title, overlap: hit.o, scenarios: scen })
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (hits.length === 0) return { warn: null, hits }
|
|
288
|
+
const { appendKnowledgeHit } = await import('./knowledge-hits.js')
|
|
289
|
+
appendKnowledgeHit(join(specBase, '.runtime'), { type: 'fr-duplicate-warning', change, hits: hits.length, source: 'flow-done' })
|
|
290
|
+
const pairs = hits.map((h) => {
|
|
291
|
+
const scen = h.scenarios ? `(场景:${h.scenarios})` : ''
|
|
292
|
+
return `${h.local}↔${h.active}「${h.title}」${scen}`
|
|
293
|
+
}).join('、')
|
|
294
|
+
const warn = `⚠️ [FR 重复嫌疑·advisory] ${hits.length} 条新 FR 与同域 active 条目标题高度重叠:${pairs}——双出路:requirements 加承接行(承接: FR-xxx,改写时对照上述场景)或改标题区分;本次放行不阻断`
|
|
295
|
+
return { warn, hits }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** 变更目录实际产物枚举(2026-09-25-flow-tick-prototype,adopt 路径必读面):adopt 时头脑风暴
|
|
299
|
+
* 产出的原型 HTML/决策清单等不在 materialPaths(那是稳定前缀 scan 面)——动态扫变更目录把实际
|
|
300
|
+
* 在场产物列成必读清单,原型显式点名(agent 看不到就不会用)。 */
|
|
301
|
+
function changeArtifactPaths(changeDir) {
|
|
302
|
+
const out = []
|
|
303
|
+
try {
|
|
304
|
+
for (const e of readdirSync(changeDir, { withFileTypes: true })) {
|
|
305
|
+
if (e.isDirectory()) {
|
|
306
|
+
if (e.name === 'prototypes') {
|
|
307
|
+
for (const f of readdirSync(join(changeDir, 'prototypes'))) out.push(join(changeDir, 'prototypes', f))
|
|
308
|
+
}
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
if (/\.(md|html)$/i.test(e.name) && e.name !== FLOW_STATE_FILE) out.push(join(changeDir, e.name))
|
|
312
|
+
}
|
|
313
|
+
} catch { /* 目录异常 = 空清单 */ }
|
|
314
|
+
return out.filter((p) => { try { return existsSync(p) } catch { return false } })
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* flow start —— 第 1 次协议调用(建卡+下发)。已存在 change → 恢复简报(不新建不重置)。
|
|
319
|
+
* @param {{change:string, input?:string, thick?:boolean, withTasks?:boolean, cwd:string, specBase:string, json?:boolean}} p
|
|
320
|
+
*/
|
|
321
|
+
export async function cmdFlowStart({ change, input, thick = false, withTasks = false, reviewForce = null, cwd, specBase, runtimeRootOpt = null, json = false }) {
|
|
322
|
+
// local.yaml 缺席 fail-fast(R22 实证:缺 local.yaml 时 flow done 测试门静默兜底裸
|
|
323
|
+
// python -m pytest → aiobotocore 假红/全量 860s 撞 600s 帽 → 11 轮重试 135min)。
|
|
324
|
+
// local.yaml 是 init 的职责面——flow start 在此拒绝,不让 agent 在错误的测试配置上走完全程
|
|
325
|
+
// 才在收口处发现。恢复/adopt 路径的变更目录已存在时跳过(在途变更不因配置后补而拦)。
|
|
326
|
+
const _localYaml = join(specBase, 'local.yaml')
|
|
327
|
+
if (!existsSync(_localYaml) && !existsSync(join(specBase, 'changes', change))) {
|
|
328
|
+
console.error('❌ local.yaml 不存在——先跑 sillyspec init(mode/lint/known_failures 等 init 级配置的落点)。')
|
|
329
|
+
console.error(' 测试面无需在此配置(2026-09-26 起:测试门按变更动态推断——本变更测试 ∪ FR 关联回归 ∪ import 依赖,')
|
|
330
|
+
console.error(' runner 自项目结构 pyproject/package.json 推断;modules.*.test/commands.test 已退役,后者仅显式 test_strategy: full 生效)。')
|
|
331
|
+
process.exit(2)
|
|
332
|
+
}
|
|
333
|
+
const cfg = readFlowConfig(specBase)
|
|
334
|
+
if (cfg.mode === 'legacy') {
|
|
335
|
+
console.error('❌ 本仓显式配置 flow.mode=legacy——走既有流程:sillyspec run <stage> --change <名>')
|
|
336
|
+
console.error(' 切回轻量跑道(2026-09-25 起缺省即 thin):local.yaml 删掉 mode: legacy 或改为 mode: thin')
|
|
337
|
+
process.exit(2)
|
|
338
|
+
}
|
|
339
|
+
// PM 锚定 specBase(平台参数面修复:此前裸构锚 resolveSpecDir(cwd)——DB 行/change 目录落本地,
|
|
340
|
+
// specBase 侧无目录 → writeFlowState ENOENT 崩溃、进度与工件分裂两处根)
|
|
341
|
+
const { ProgressManager } = await import('./progress.js')
|
|
342
|
+
const pm = new ProgressManager({ specDir: specBase })
|
|
343
|
+
const changesDir = join(specBase, 'changes')
|
|
344
|
+
const changeDir = join(changesDir, change)
|
|
345
|
+
const runtimeRoot = resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase)
|
|
346
|
+
|
|
347
|
+
let proceedFreshEmptyDir = false
|
|
348
|
+
if (existsSync(changeDir)) {
|
|
349
|
+
const st = readFlowState(changeDir)
|
|
350
|
+
if (!st) {
|
|
351
|
+
// adopt 收编(2026-09-25-thin-brainstorm-prestage):头脑风暴预段产物(proposal/design 在场、
|
|
352
|
+
// 无 flow-state)收编进轻量变更——brainstorm 是 run 族预段,先跑后到不触发混跑守卫;产物原样
|
|
353
|
+
// 保留,机器只补缺件与绑定面。无产物 = 真 legacy 既有变更,维持原拒收。
|
|
354
|
+
const hasBsArtifacts = existsSync(join(changeDir, 'proposal.md')) || existsSync(join(changeDir, 'design.md'))
|
|
355
|
+
if (hasBsArtifacts) {
|
|
356
|
+
const head = gitQuiet(cwd, ['rev-parse', 'HEAD'])
|
|
357
|
+
const baseline = typeof head === 'string' && head.trim() ? head.trim() : null
|
|
358
|
+
writeFlowState(changeDir, { tier: 'thin', born_face: 'thin', adopted_from: 'brainstorm', review_force: reviewForce, baseline_commit: baseline, substeps: {} })
|
|
359
|
+
try {
|
|
360
|
+
const { redraftMissingArtifacts, ensureBindingSlots } = await import('./flow-draft.js')
|
|
361
|
+
const r = redraftMissingArtifacts({ changeDir, change, input: null, runtimeRoot })
|
|
362
|
+
if (r.drafted.length > 0) console.log(`📌 收编补生成缺失机器稿:${r.drafted.join('、')}(brainstorm 未产的工件机器补齐,criteria 从 proposal 成功标准回提;已存在文件未动)`)
|
|
363
|
+
const b = ensureBindingSlots({ changeDir })
|
|
364
|
+
if (b.appended) console.log(`📌 收编追加测试绑定槽 ${b.slots} 枚(brainstorm requirements 无绑定面——干活时作答,flow done 校验)`)
|
|
365
|
+
} catch (e) { console.warn(`⚠️ 收编补件失败(best-effort): ${(e && e.message) || e}`) }
|
|
366
|
+
try {
|
|
367
|
+
const { spawnWatcher } = await import('./watcher.js')
|
|
368
|
+
const w = await spawnWatcher(cwd, change, { specBase })
|
|
369
|
+
if (w.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/watcher-events-${change}.jsonl(恒带 provisional:true)`)
|
|
370
|
+
} catch { /* 观测旁路 best-effort */ }
|
|
371
|
+
const materials = materialPaths(specBase, change, changeDir)
|
|
372
|
+
const artifacts = changeArtifactPaths(changeDir)
|
|
373
|
+
const prototypePaths = artifacts.filter((p) => /\.html$/i.test(p))
|
|
374
|
+
// 知识注入(2026-09-25-thin-fr-inject-parity):adopt 路径域路由走 design.md 交付清单
|
|
375
|
+
let digestLines = []
|
|
376
|
+
try { digestLines = (await flowKnowledgeDigest({ specBase, change, changeDir, input: null })).lines } catch { /* 注入 best-effort */ }
|
|
377
|
+
console.log([
|
|
378
|
+
`🧲 头脑风暴产物已收编进轻量跑道: ${change}(adopted_from=brainstorm,baseline=${baseline ? baseline.slice(0, 10) : '(无 git 历史)'})`,
|
|
379
|
+
`══════════════════════════════════════`,
|
|
380
|
+
`📖 必读(头脑风暴产出——方案的承诺锚,先读完再动手):`,
|
|
381
|
+
...artifacts.map((p) => ` - ${p}`),
|
|
382
|
+
prototypePaths.length > 0
|
|
383
|
+
? `🖼️ 原型在场(${prototypePaths.length} 个 HTML):实现前必看,界面/交互/流程按原型对齐;有出入以 design.md 承诺为准并在回复中说明。`
|
|
384
|
+
: null,
|
|
385
|
+
`【你要做的】直接干活:改代码、写测试。design/decisions 是本变更的承诺锚(flow done 豁免 design 四节槽,以其为准)。`,
|
|
386
|
+
`requirements 测试绑定槽(收编追加)每条 FR 至少一行作答;写码前后顺手填。`,
|
|
387
|
+
`✅ 任务面归你(thin-agent-tasks):tasks.md 是机器预填的标准逐条草稿——按实际实现路径覆写它(增删改组随意,保持 \`- [ ] task-NN:\` 行形态),完成一个你自己的任务单元即勾 \`- [x]\`——`,
|
|
388
|
+
` 勾选是收口哨兵的证据面:全勾但零提交 token/review.json 会被拒收;flow status 随时看勾选进度。`,
|
|
389
|
+
`⚠️ 交付纪律:收口前交付代码显式 pathspec 提交——冻结件范围=baseline..HEAD,未提交不进审计件。`,
|
|
390
|
+
``,
|
|
391
|
+
`【协议调用 2/2(干完后)】sillyspec flow done --change ${change}`,
|
|
392
|
+
``,
|
|
393
|
+
`材料路径清单(按需 Read):`,
|
|
394
|
+
...materials.map((p) => ` - ${p}`),
|
|
395
|
+
``,
|
|
396
|
+
...digestLines,
|
|
397
|
+
].filter(Boolean).join('\n'))
|
|
398
|
+
try { await triggerSync(cwd, change) } catch { /* 同步绝不阻断协议面 */ }
|
|
399
|
+
return { adopted: true, change, baseline }
|
|
400
|
+
}
|
|
401
|
+
// 平台 writer 预建空目录放行(平台参数面):平台派发先建目录后 spawn——空目录=全新 thin
|
|
402
|
+
// 起点而非 legacy 记账证据;非空且无头脑风暴产物才是真 legacy 拒收面
|
|
403
|
+
let dirEmpty = false
|
|
404
|
+
try { dirEmpty = readdirSync(changeDir).length === 0 } catch { dirEmpty = false }
|
|
405
|
+
if (dirEmpty) {
|
|
406
|
+
console.log('ℹ️ 预建空变更目录放行(平台 writer 形态):按全新轻量跑道 start 处理')
|
|
407
|
+
proceedFreshEmptyDir = true
|
|
408
|
+
} else {
|
|
409
|
+
console.error(`❌ change 目录已存在但无 ${FLOW_STATE_FILE}(legacy 记账的既有变更)——混跑回退:走 run <stage> 续跑,勿用 flow`)
|
|
410
|
+
process.exit(2)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (!proceedFreshEmptyDir) {
|
|
414
|
+
// 幂等补起草(2026-09-25-thin-dogfood-fixes 修复①):draft 谱系是 start 时点快照,工具
|
|
415
|
+
// 升级新增工件后重入补缺(已存在不碰、ledger 合并)——恢复简报前执行,简报读到补齐后的盘面。
|
|
416
|
+
try {
|
|
417
|
+
const { redraftMissingArtifacts } = await import('./flow-draft.js')
|
|
418
|
+
const r = redraftMissingArtifacts({ changeDir, change, input, runtimeRoot })
|
|
419
|
+
if (r.drafted.length > 0) {
|
|
420
|
+
console.log(`📌 重入补生成缺失机器稿 ${r.drafted.length} 件:${r.drafted.join('、')}(工具升级晚于 start 的在途变更补件;已存在文件未动)`)
|
|
421
|
+
}
|
|
422
|
+
} catch (e) { console.warn(`⚠️ 补起草失败(不阻断恢复简报): ${(e && e.message) || e}`) }
|
|
423
|
+
// 声明通道 resume 生效(fr-governance-sweep 评审 P3 清偿):--review/--no-review 对在途
|
|
424
|
+
// 变更重入 start 时落盘 review_force——与新变更/adopt 两路口径一致(此前 resume 只打简报
|
|
425
|
+
// 即 return,flag 静默失效)。幂等:未带 flag(null)不覆盖既有声明。
|
|
426
|
+
if (reviewForce !== null) {
|
|
427
|
+
writeFlowState(changeDir, { review_force: reviewForce })
|
|
428
|
+
console.log(`⚖️ 已更新评审声明通道:review_force=${reviewForce}(resume 落盘)`)
|
|
429
|
+
}
|
|
430
|
+
// 知识注入(2026-09-25-thin-fr-inject-parity):resume 路径域路由走基线以来文件面——
|
|
431
|
+
// changedFilesSinceBaseline(fr-rot-precision 评审 P2:含未提交工作树/untracked、剔 .sillyspec,
|
|
432
|
+
// 与收口口径同源;裸 git diff 双提交区间会漏干活期未提交文件);best-effort 不阻断恢复简报。
|
|
433
|
+
let resumeDigest = { lines: [], summary: null }
|
|
434
|
+
try {
|
|
435
|
+
resumeDigest = await flowKnowledgeDigest({ specBase, change, changeDir, input: null, filesOverride: changedFilesSinceBaseline(cwd, st.baseline_commit) })
|
|
436
|
+
} catch { /* 注入 best-effort */ }
|
|
437
|
+
printRecoveryBriefing({ cwd, specBase, change, changeDir, runtimeRoot, st, digestLines: resumeDigest.lines })
|
|
438
|
+
return { recovery: true }
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// 需求清晰度门(2026-09-25-thin-brainstorm-prestage):轻量跑道假定输入已含决策——--input 缺失
|
|
443
|
+
// 或成功标准提取 0 条时不建变更、exit 2 给两选一(头脑风暴预段 / 补成功标准重跑)。
|
|
444
|
+
// 重入与 adopt 路径在上方分支早退,不受此门影响。CLI 只产信号,选择权归用户/agent。
|
|
445
|
+
{
|
|
446
|
+
const { extractSuccessCriteria } = await import('./flow-draft.js')
|
|
447
|
+
if (!input || extractSuccessCriteria(input).length === 0) {
|
|
448
|
+
console.error(`❓ 需求不够清晰(--input ${input ? '在场但「成功标准」条目提取 0 条' : '缺失'})——轻量跑道假定输入已含决策,两选一:`)
|
|
449
|
+
console.error(` ① 头脑风暴预段(需求不明时推荐):sillyspec run brainstorm --change ${change}`)
|
|
450
|
+
console.error(` 人机交互探索需求、出 design/决策/原型;完成后回来 sillyspec flow start --change ${change},产物自动收编续跑轻量变更`)
|
|
451
|
+
console.error(` ② 确认输入已含决策:sillyspec flow start --change ${change} --input "<完整需求>",input 过门格式:`)
|
|
452
|
+
console.error(` 先写动机/背景;随后独立一行只写「成功标准:」;再每行一条「- <可验证标准>」`)
|
|
453
|
+
process.exit(2)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// 升厚预判已删除(2026-09-25-thin-precheck-removal):技术面关键词(数据库/迁移等)测错轴——
|
|
458
|
+
// R16 实证碰迁移的变更轻量变更带评审最优收口,预判反诱发误升厚(臂 A 129M)与误报打断。选道只留
|
|
459
|
+
// 形态信号:清晰度门管「需求说不清楚」(预段收编),升厚只留用户决策(--upgrade-thick 同意门)
|
|
460
|
+
// 与运行时证据(实测失败升档/edit_ratio/评审——风险面在收口时点按承诺词/diff 原语/盲维判定)。
|
|
461
|
+
|
|
462
|
+
pm.initChange(cwd, change, {})
|
|
463
|
+
try {
|
|
464
|
+
const { resolveSessionIdentity } = await import('./progress.js')
|
|
465
|
+
const { session } = resolveSessionIdentity({ flagSession: null, cwd })
|
|
466
|
+
pm.claimChangeOwner(cwd, change, session)
|
|
467
|
+
} catch { /* claim 失败不阻断启动(fail-open,同 runCommand 接线) */ }
|
|
468
|
+
|
|
469
|
+
// watcher 拉起(D-001:change 启动即观测——flow start 走 index.js 分发不经 runCommand,
|
|
470
|
+
// 需在此独立接线;语义同 command.js 侧:无条件 spawn+单飞锁合并+best-effort 不阻断,
|
|
471
|
+
// 未连接平台也 spawn(本地 jsonl 是事件唯一真相源);SILLYSPEC_WATCHER=0 逃生阀)。
|
|
472
|
+
try {
|
|
473
|
+
const { spawnWatcher } = await import('./watcher.js')
|
|
474
|
+
const r = await spawnWatcher(cwd, change, { specBase })
|
|
475
|
+
if (r.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/watcher-events-${change}.jsonl(恒带 provisional:true)`)
|
|
476
|
+
} catch { /* 观测旁路 best-effort,绝不阻断协议面 */ }
|
|
477
|
+
|
|
478
|
+
const head = gitQuiet(cwd, ['rev-parse', 'HEAD'])
|
|
479
|
+
const baseline = typeof head === 'string' && head.trim() ? head.trim() : null
|
|
480
|
+
writeFlowState(changeDir, {
|
|
481
|
+
tier: thick ? 'thick' : 'thin',
|
|
482
|
+
// born_face=工件面出身(R7 切片四修正:失败升厚改 tier 只升仪式不回溯出身——轻量面出身归档
|
|
483
|
+
// 恒走 skipPlanCheck,防「升厚后无 plan.md 归档死锁」;厚面出身(--thick 起步)才要 plan.md)
|
|
484
|
+
born_face: thick ? 'thick' : 'thin',
|
|
485
|
+
with_tasks: Boolean(withTasks),
|
|
486
|
+
review_force: reviewForce,
|
|
487
|
+
baseline_commit: baseline,
|
|
488
|
+
substeps: {},
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
// 全件机器起草(R7 切片三 / FR-07:工件回填轮=0——治理工件 CLI 写,agent 只裁例外)。
|
|
492
|
+
// 任务卡分岔(用户裁定#3):默认 thin+直写零任务卡;--thick/--with-tasks 才生成任务卡。
|
|
493
|
+
let drafted = []
|
|
494
|
+
try {
|
|
495
|
+
const { draftAll } = await import('./flow-draft.js')
|
|
496
|
+
const r = draftAll({ changeDir, change, input, withTasks: thick || withTasks, runtimeRoot })
|
|
497
|
+
drafted = r.written
|
|
498
|
+
} catch (e) {
|
|
499
|
+
console.warn(`⚠️ 机器起草失败(轻量工件面降级为 flow done 默认验收;best-effort 不阻断): ${(e && e.message) || e}`)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const materials = materialPaths(specBase, change, changeDir)
|
|
503
|
+
// 绿地 bootstrap(greenfield-bootstrap,R17 实证:无模块图仓 FR 全落伪域/unmapped,知识复利
|
|
504
|
+
// 从第一条断流):模块图缺席且 --input 有路径语料 → 机器起草初始 _module-map.yaml 草案(按
|
|
505
|
+
// 目录段聚合 modules.<id>.paths;generator=flow-bootstrap-draft + status=draft 标识;不含
|
|
506
|
+
// blast 段——判级对缺席有安全降级;不覆盖已有文件)。草案身份醒目提示,指路 scan 校准。
|
|
507
|
+
try {
|
|
508
|
+
const { discoverModuleIndex } = await import('./decision-distill.js')
|
|
509
|
+
const knowledgeRoot = join(specBase, 'knowledge')
|
|
510
|
+
if (discoverModuleIndex(knowledgeRoot) === null) {
|
|
511
|
+
const bsPaths = extractInputPaths(input)
|
|
512
|
+
if (bsPaths.length > 0) {
|
|
513
|
+
const { draftModuleMap } = await import('./greenfield-bootstrap.js')
|
|
514
|
+
const r = draftModuleMap({ cwd, specBase, paths: bsPaths })
|
|
515
|
+
if (r.written) {
|
|
516
|
+
console.log(`🗺️ [绿地草案] 模块图缺席——已起草初始 _module-map.yaml(${r.modules} 个模块:${r.moduleIds.join('、')})→ ${r.path}`)
|
|
517
|
+
console.log(` 草案身份(status: draft):建议跑 sillyspec run scan 或 modules 校准后转正——不校准也可用,域路由按草案分流(不再全落 unmapped)`)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} catch (e) { console.warn(`⚠️ 绿地模块图草案起草失败(best-effort 不阻断 start):${(e && e.message) || e}`) }
|
|
522
|
+
// 知识注入(2026-09-25-thin-fr-inject-parity):fresh 起点域路由用 --input 提取的路径样
|
|
523
|
+
// token(best-effort)——brainstorm 的 {FR_INDEX_DIGEST}/{DECISION_HITS} 注入面对齐到轻量道。
|
|
524
|
+
let digest = { lines: [], summary: { domains: [], frCount: 0, rejectedDecisions: 0, knowledgeEntries: 0 } }
|
|
525
|
+
try {
|
|
526
|
+
digest = await flowKnowledgeDigest({ specBase, change, changeDir, input, filesOverride: extractInputPaths(input) })
|
|
527
|
+
} catch { /* 注入 best-effort 不阻断 start */ }
|
|
528
|
+
const lines = [
|
|
529
|
+
`🏃 flow start(${thick ? 'thick 厚档(--thick 显式声明,人声明不做启发式)' : 'thin 轻量跑道'}): ${change}`,
|
|
530
|
+
`══════════════════════════════════════`,
|
|
531
|
+
`【协议调用 1/2(本次)】change 已建 + 基线锚定(baseline_commit=${baseline ? baseline.slice(0, 10) : '(无 git 历史)'})${withTasks ? ' + 任务卡模式(--with-tasks:中间自愿用 task done,收尾仍 flow done)' : ''}`,
|
|
532
|
+
``,
|
|
533
|
+
`【你要做的】直接干活:改代码、写测试。治理工件不用你写——flow done 机器做(协议记账单位=change 级)。`,
|
|
534
|
+
`例外裁决面(唯一合法 .sillyspec 书写):AGENT 槽填充 / flow amend-draft(确要改机器稿时)。`,
|
|
535
|
+
`⚠️ design.md 四节 AGENT 槽(做法/接口契约/边界并发四问/风险)动码前后顺手作答——每节至少一行,`,
|
|
536
|
+
` 写「不适用:<理由>」也算答;flow done 空槽拒收(承诺锚点,评审与 FR 对账都对着它)。`,
|
|
537
|
+
`📜 requirements 的 FR 是机器摘录候选——语义要改写时直接编辑机器段后跑 flow amend-draft 留痕`,
|
|
538
|
+
` (槽里写的不进 FR 索引);输入含编号行为条目时机器已优先摘编号条目。`,
|
|
539
|
+
`📦 冻结面在 flow done 时点采集(baseline..HEAD 提交面):未提交交付文件——会话专属 worktree 自动并入;`,
|
|
540
|
+
` 共享主仓可带 --freeze-dirty 显式声明并入;git 中间提交归档后可 reset --soft 压扁为单提交`,
|
|
541
|
+
` (审计真相在 change.patch 冻结件 sha256 锚定,不依赖 git 历史形态)。`,
|
|
542
|
+
`⚖️ 独立评审定档(flow done 按危险证据判,不看文件数):高危承诺词/盲维实质作答/diff 危险`,
|
|
543
|
+
` 原语/决策密度任一命中即需评审(届时会收到评审任务书,起子代理产出 review.json);豁免`,
|
|
544
|
+
` 也有 1/4 抽查采样。要强制/豁免可重启时带 --review / --no-review${reviewForce === true ? '(本变更已声明 --review)' : reviewForce === false ? '(本变更已声明 --no-review)' : ''}。`,
|
|
545
|
+
`⚠️ 交付纪律:收口前先把交付代码用显式 pathspec 提交(git add -- <文件> && git commit)——`,
|
|
546
|
+
` patch 冻结件范围=baseline..HEAD 提交面,未提交的代码不进审计件(R16 评审 P2 实证);`,
|
|
547
|
+
` tasks.md 一并显式 pathspec 提交(勾选证据进 git 历史,勿 untracked 直至归档——R19 实证)。`,
|
|
548
|
+
`✅ 任务面归你(thin-agent-tasks):tasks.md 是机器预填的标准逐条草稿——按实际实现路径覆写它(增删改组随意,保持 \`- [ ] task-NN:\` 行形态),完成一个你自己的任务单元即勾 \`- [x]\`——`,
|
|
549
|
+
` 勾选是收口哨兵的证据面:全勾但区间提交的标题或正文均无 task-NN 且无 review.json 会被拒收;`,
|
|
550
|
+
` flow status 随时看勾选进度(②执行阶段勾选滞后时 status 会带提醒行——边干边勾)。`,
|
|
551
|
+
``,
|
|
552
|
+
`🛑 三断点纪律(可控性要求——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
|
|
553
|
+
` ① spec 断点:填完 FR 区和 design 槽后,把摘要给用户看(FR 条目+盲维作答+方案概述),`,
|
|
554
|
+
` 等用户确认方案再动手写代码——方案错了返工最贵。`,
|
|
555
|
+
` ② 执行断点:写完代码跑完测试后,把测试结果(过了几个/挂了什么)给用户看,`,
|
|
556
|
+
` 等用户确认再跑收口。`,
|
|
557
|
+
` ③ 归档断点:flow done 跑完后(无论过/拒),把结果(归档成功/被什么拦了)给用户看。`,
|
|
558
|
+
` 用户明确说「直接跑完/不用问我」→ 三断点全跳过(agent 自主干到底)。`,
|
|
559
|
+
` 随时可查进度:sillyspec flow status --change ${change}`,
|
|
560
|
+
...(detectUiTouch(input)
|
|
561
|
+
? [...buildUiGuidanceLines(), '']
|
|
562
|
+
: []),
|
|
563
|
+
``,
|
|
564
|
+
`【协议调用 2/2(干完后)】sillyspec flow done --change ${change}`,
|
|
565
|
+
` 测试对账:P2 账本优先,无记录 CLI 亲测(fail-closed:实测失败/超时=整单 FAIL exit≠0;中断重入断点续)。`,
|
|
566
|
+
``,
|
|
567
|
+
`材料路径清单(稳定前缀,按需 Read):`,
|
|
568
|
+
...materials.map((p) => ` - ${p}`),
|
|
569
|
+
``,
|
|
570
|
+
...digest.lines,
|
|
571
|
+
]
|
|
572
|
+
if (json) {
|
|
573
|
+
console.log(JSON.stringify({ change, tier: thick ? 'thick' : 'thin', baseline, materials, knowledgeDigest: digest.summary }))
|
|
574
|
+
} else {
|
|
575
|
+
console.log(lines.join('\n'))
|
|
576
|
+
}
|
|
577
|
+
// 平台同步(2026-09-22-thin-fr-distill-sync:flow 走 index.js 分发不经 runCommand,此前后台
|
|
578
|
+
// spec-sync 从不触发——轻量变更 docs/knowledge 不推平台。对齐 run 族语义:尾部 best-effort 后台推)
|
|
579
|
+
try { await triggerSync(cwd, change) } catch { /* 同步绝不阻断协议面 */ }
|
|
580
|
+
return { change, baseline, materials }
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** 恢复简报:盘面状态(checkbox/提交/账本/dirty files)→ 做到哪、剩什么、下一步。 */
|
|
584
|
+
function printRecoveryBriefing({ cwd, specBase, change, changeDir, runtimeRoot, st, digestLines = [] }) {
|
|
585
|
+
const done = []
|
|
586
|
+
const left = []
|
|
587
|
+
for (const k of SUBSTEPS) (st.substeps?.[k] === 'done' ? done : left).push(k)
|
|
588
|
+
let checked = 0, total = 0
|
|
589
|
+
try {
|
|
590
|
+
const t = readFileSync(join(changeDir, 'tasks.md'), 'utf8')
|
|
591
|
+
checked = (t.match(/^- \[x\]/gm) || []).length
|
|
592
|
+
total = (t.match(/^- \[( |x)\]/gm) || []).length
|
|
593
|
+
} catch { /* 无 tasks.md = 零任务卡(thin 默认) */ }
|
|
594
|
+
const commits = st.baseline_commit
|
|
595
|
+
? (gitQuiet(cwd, ['rev-list', '--count', `${st.baseline_commit}..HEAD`]) || '0').toString().trim()
|
|
596
|
+
: '?'
|
|
597
|
+
const dirty = changedFilesSinceBaseline(cwd, st.baseline_commit).length
|
|
598
|
+
const hasLedger = existsSync(join(runtimeRoot, `verify-quality-scan-${change}.json`))
|
|
599
|
+
console.log([
|
|
600
|
+
`🔁 flow start 恢复简报(重入): ${change}(tier=${st.tier}${st.legacy_fallback ? ',legacy_fallback=true 混跑回退中' : ''})`,
|
|
601
|
+
`══════════════════════════════════════`,
|
|
602
|
+
`- 做到哪:任务勾选 ${checked}/${total};基线以来提交 ${commits} 个;变更/dirty 文件 ${dirty} 个;P2 质量扫描记录 ${hasLedger ? '在场' : '无'}。`,
|
|
603
|
+
`- 六子步标记:${done.length ? `已完成 ${done.join('/')}` : '(无)'}${left.length ? `;待办 ${left.join('/')}` : ';全部完成'}`,
|
|
604
|
+
`- 剩什么:${left.length === 0 && dirty === 0 ? '活已干完' : dirty > 0 ? '活未干完(继续改代码)' : '收尾待裁决'}`,
|
|
605
|
+
`- 下一步:${left.length === 0 ? `sillyspec flow done --change ${change}` : `继续干活;干完跑 flow done(断点续)`}`,
|
|
606
|
+
...(digestLines.length > 0 ? ['', ...digestLines] : []),
|
|
607
|
+
].join('\n'))
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* flow done —— 第 2 次协议调用(唯一裁决点,六子步幂等)。
|
|
612
|
+
* 子步:artifacts(工件校验)→ ledger(账本对账+亲测)→ probes(探针)→ distill(决策提炼)
|
|
613
|
+
* → archive(归档经 runArchiveChain,thin 轻量工件面跳过 plan.md 硬校验)→ events(事件收口)。
|
|
614
|
+
*/
|
|
615
|
+
export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false }) {
|
|
616
|
+
const changeDir = join(specBase, 'changes', change)
|
|
617
|
+
const st = readFlowState(changeDir)
|
|
618
|
+
if (!st) {
|
|
619
|
+
console.error(`❌ ${change} 无 ${FLOW_STATE_FILE}(未参与 thin 协议)——走 run <stage> 既有流程`)
|
|
620
|
+
process.exit(2)
|
|
621
|
+
}
|
|
622
|
+
if (st.legacy_fallback) {
|
|
623
|
+
console.error('❌ 本 change 已混跑回退 legacy(跑过 run <stage>)——剩余流程按厚档走 run <stage>,flow done 不再裁决')
|
|
624
|
+
process.exit(2)
|
|
625
|
+
}
|
|
626
|
+
const runtimeRoot = resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase)
|
|
627
|
+
// 归属收窄清单(2026-09-25-thin-dogfood-fixes 修复②):baseline..HEAD 不分作者——并行会话在
|
|
628
|
+
// start..done 之间的提交会混入本变更实测面与 FR 域路由。复用 verify 对账同款切分器:他侧显式
|
|
629
|
+
// 声明(quick --files / design 清单)的文件剔除归他者;未声明文件保留(fail-closed 不因并行漏跑)。
|
|
630
|
+
// ledger 门与 distill 的 deliverableFiles 单源走这里(此前 distill 直取 git diff 未切分)。
|
|
631
|
+
const attributedChangedFiles = async () => {
|
|
632
|
+
let files = changedFilesSinceBaseline(cwd, st.baseline_commit)
|
|
633
|
+
try {
|
|
634
|
+
const { splitOwnVsForeignDiffFiles } = await import('./foreign-declared.js')
|
|
635
|
+
const { own, foreign } = splitOwnVsForeignDiffFiles(cwd, change, files, { specBase })
|
|
636
|
+
if (foreign.length > 0) {
|
|
637
|
+
console.log(`🔗 归属收窄:剔除 ${foreign.length} 个他侧声明文件(${foreign.slice(0, 5).map((x) => x.file).join(', ')}${foreign.length > 5 ? ' 等' : ''})——不进本变更实测面与 FR 域路由`)
|
|
638
|
+
return own
|
|
639
|
+
}
|
|
640
|
+
} catch { /* 切分失败 fail-closed 保留全量 */ }
|
|
641
|
+
return files
|
|
642
|
+
}
|
|
643
|
+
let markDir = changeDir
|
|
644
|
+
const mark = (k) => { writeFlowState(markDir, { substeps: { [k]: 'done' } }); doneList.push(k) }
|
|
645
|
+
const doneList = []
|
|
646
|
+
const skip = (k) => { doneList.push(`${k}(skip)`) }
|
|
647
|
+
let reviewOutcome = null
|
|
648
|
+
let gateSummaryText = null
|
|
649
|
+
// 遥测单点(2026-09-25-thin-parity-assets 修评审 P2①:失败路径 exit 前也要落账——校准信号
|
|
650
|
+
// 不许在失败面丢失;成功收口走函数末尾同款记录,失败面只多不少)
|
|
651
|
+
const appendTelemetry = (review) => {
|
|
652
|
+
try {
|
|
653
|
+
appendFileSync(join(runtimeRoot, 'flow-telemetry.jsonl'), JSON.stringify({
|
|
654
|
+
ts: new Date().toISOString(), change, protocolCalls: 2,
|
|
655
|
+
draftAmendments: st.edit_ratio != null ? 1 : 0, editRatio: st.edit_ratio ?? null,
|
|
656
|
+
routeHint: st.route_hint ?? null, tier: st.tier, upgraded: st.upgrade_reason ?? null,
|
|
657
|
+
review,
|
|
658
|
+
}) + '\n', 'utf8')
|
|
659
|
+
} catch { /* 遥测 best-effort */ }
|
|
660
|
+
}
|
|
661
|
+
const reportMidFail = (failed) => {
|
|
662
|
+
console.error(`❌ flow done 中断于子步「${failed}」。已完成:${doneList.length ? doneList.join('、') : '(无)'};待办:${SUBSTEPS.filter((k) => st.substeps?.[k] !== 'done' && k !== failed).join('、') || '(无)'}`)
|
|
663
|
+
console.error(' 重入:修复后重跑同一条命令——已完成子步幂等跳过,从断点续(半态可重入不可假绿:归档子步未完成前 change 仍 active)')
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ① artifacts:机器稿指纹校验(draft-ledger 在场时三态拒收——标记缺失/哈希失配/手工重锚
|
|
667
|
+
// 未审计;AGENT 槽是合法书写面不参与指纹;缺 ledger=not-applicable 放行)
|
|
668
|
+
// + 设计记录空槽拒收(2026-09-24 v3 设计记录全档化第一片:盲维四问每跑必答——空槽=未答,
|
|
669
|
+
// 「不适用:<理由>」也是答;纯文档检查前置于实测门,秒级失败秒级返工)
|
|
670
|
+
if (st.substeps?.artifacts === 'done') { skip('artifacts') } else {
|
|
671
|
+
const { verifyFlowDrafts, verifyDesignRecordFilled } = await import('./flow-draft.js')
|
|
672
|
+
const r = verifyFlowDrafts({ changeDir, change, runtimeRoot })
|
|
673
|
+
if (r.applicable && r.violations.length > 0) {
|
|
674
|
+
console.error(`❌ 工件校验拒收(机器稿指纹三态):`)
|
|
675
|
+
for (const v of r.violations) console.error(` - ${v}`)
|
|
676
|
+
reportMidFail('artifacts')
|
|
677
|
+
process.exit(1)
|
|
678
|
+
}
|
|
679
|
+
// 头脑风暴预段设计豁免(2026-09-25-thin-brainstorm-prestage):adopted 变更的 design 是
|
|
680
|
+
// brainstorm 人机交互产物(比四节骨架丰富)——承诺以其为准,槽位门不适用
|
|
681
|
+
if (st.adopted_from === 'brainstorm' && existsSync(join(changeDir, 'design.md'))) {
|
|
682
|
+
console.log('ℹ️ 头脑风暴预段设计在场——豁免 design 四节槽门(adopted_from=brainstorm,设计承诺以 brainstorm design 为准)')
|
|
683
|
+
} else {
|
|
684
|
+
const dr = verifyDesignRecordFilled({ changeDir })
|
|
685
|
+
if (dr.applicable && dr.emptySlots.length > 0) {
|
|
686
|
+
console.error(`❌ 设计记录未作答:design.md 有 ${dr.emptySlots.length} 个空 AGENT 槽(${dr.emptySlots.join('、')})`)
|
|
687
|
+
console.error(` 每节至少写一行(小改动可写「不适用:<理由>」)——设计承诺是评审与 FR 对账的锚点,空槽=承诺未落盘`)
|
|
688
|
+
console.error(` 恢复指引:槽标记(<!--AGENT:槽N)被删时,从 .runtime/step-guides/ 的指纹缓存可找骨架原文;或删 design.md 后重入 flow start 补生成(criteria 从 proposal 回提)`)
|
|
689
|
+
reportMidFail('artifacts')
|
|
690
|
+
process.exit(1)
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
mark('artifacts')
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// ② ledger:P2 账本对账+亲测(runQuickTestLintGate 同源:账本优先→真跑→recordTestLedger 落账)
|
|
697
|
+
if (st.substeps?.ledger === 'done') {
|
|
698
|
+
skip('ledger')
|
|
699
|
+
// 实测面回填(2026-09-25-thin-r16-patches 修复③:断点续跑收口的回执不失忆——review 回填同族)
|
|
700
|
+
if (!gateSummaryText) {
|
|
701
|
+
try {
|
|
702
|
+
const { backfillGateSummary } = await import('./flow-parity.js')
|
|
703
|
+
gateSummaryText = backfillGateSummary(runtimeRoot, change)
|
|
704
|
+
} catch { /* 回填 best-effort */ }
|
|
705
|
+
}
|
|
706
|
+
} else {
|
|
707
|
+
const { runQuickTestLintGate } = await import('./run/quick-audit.js')
|
|
708
|
+
const changedFiles = await attributedChangedFiles()
|
|
709
|
+
// 哨兵断言(2026-09-25-sentinel-wiring):tasks.md 全勾但零完成证据(区间提交消息标题与正文
|
|
710
|
+
// 均无 task-NN token 且无对应 review.json)→ 拒收——L0 硬门接线,两道收口同一哨兵(quick 侧同判)。
|
|
711
|
+
// 证据面=整条提交消息(2026-09-25-thin-done-gate-calibration 坑2:%s 只取标题行,正文里的
|
|
712
|
+
// token 被判零证据,与文案「提交带 task-NN」口径漂移)——%B%x1e 按提交切记录,advisory 的
|
|
713
|
+
// 提交计数不因多行正文失真。
|
|
714
|
+
try {
|
|
715
|
+
// 自动勾选(governance-autopilot):哨兵检查前,解析区间提交的 task-NN token,
|
|
716
|
+
// 自动勾选 tasks.md 中尚未勾选但有证据的条目——agent 零手工编辑 tasks.md。
|
|
717
|
+
const _tasksMdPath = join(changeDir, 'tasks.md')
|
|
718
|
+
if (existsSync(_tasksMdPath)) {
|
|
719
|
+
const _tickLog = gitQuiet(cwd, ['log', '--format=%B%x1e', `${st.baseline_commit}..HEAD`])
|
|
720
|
+
if (_tickLog) {
|
|
721
|
+
const _commitTexts = String(_tickLog).split('\x1e').map(x => x.trim()).filter(Boolean).join('\n')
|
|
722
|
+
const _evidencedTasks = new Set()
|
|
723
|
+
for (const m of _commitTexts.matchAll(/task-(\d{1,2})/gi)) _evidencedTasks.add(String(Number(m[1])).padStart(2, '0'))
|
|
724
|
+
if (_evidencedTasks.size > 0) {
|
|
725
|
+
let _tasksMd = readFileSync(_tasksMdPath, 'utf8')
|
|
726
|
+
let _autoTicked = 0
|
|
727
|
+
for (const nn of _evidencedTasks) {
|
|
728
|
+
const re = new RegExp(`^- \\[ \\](.*task-${nn}:)`, 'm')
|
|
729
|
+
if (re.test(_tasksMd)) {
|
|
730
|
+
_tasksMd = _tasksMd.replace(re, `- [x]$1`)
|
|
731
|
+
_autoTicked++
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (_autoTicked > 0) {
|
|
735
|
+
const { writeAtomicSync } = await import('./fs-atomic.js')
|
|
736
|
+
writeAtomicSync(_tasksMdPath, _tasksMd)
|
|
737
|
+
console.log(`✅ [自动勾选] ${_autoTicked} 个任务有提交证据但未勾——已机器代勾(governance-autopilot)`)
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const { detectFakeCheckCompletion } = await import('./sentinel-assertions.js')
|
|
743
|
+
const tasksPath = join(changeDir, 'tasks.md')
|
|
744
|
+
if (existsSync(tasksPath)) {
|
|
745
|
+
const _logRaw = gitQuiet(cwd, ['log', '--format=%B%x1e', `${st.baseline_commit}..HEAD`])
|
|
746
|
+
if (!_logRaw) {
|
|
747
|
+
console.log('ℹ️ 哨兵:git log 不可用,跳过(fail-open——空提交组照判会假拦)')
|
|
748
|
+
} else {
|
|
749
|
+
const commitMessages = String(_logRaw).split('\x1e').map((x) => x.trim()).filter(Boolean)
|
|
750
|
+
const sent = detectFakeCheckCompletion({ changeDir, tasksMd: readFileSync(tasksPath, 'utf8'), commits: commitMessages })
|
|
751
|
+
if (sent.status === 'fake') {
|
|
752
|
+
console.error(`🚫 哨兵断言拒收:tasks.md 全勾(${sent.checked}/${sent.claimTotal})但 ${sent.missing.length} 个任务零完成证据(区间提交标题与正文均无 token、无 review.json):${sent.missing.join('、')}`)
|
|
753
|
+
console.error(' 补证据(提交标题或正文带 task-NN,或产 review.json)或取消勾选后重跑——假完成主张不许过门')
|
|
754
|
+
appendTelemetry({ sentinel: 'fake', missing: sent.missing.length })
|
|
755
|
+
reportMidFail('ledger')
|
|
756
|
+
process.exit(1)
|
|
757
|
+
}
|
|
758
|
+
if (sent.status === 'complete') {
|
|
759
|
+
console.log(`🛡️ 哨兵:全勾 ${sent.checked}/${sent.claimTotal} 证据齐(提交 token/review.json)`)
|
|
760
|
+
// 勾选时点判定(2026-09-26-tick-loop-nudge,R19 行为发现):tasks.md 的 git 首次提交
|
|
761
|
+
// == 收口窗口最后提交(一把勾模式)→ 放行但打行为提醒(不阻断——token 证据已验,
|
|
762
|
+
// 这是提醒边干边勾纪律,非假勾)。tasks.md 全程 untracked(未随交付提交)时同款提醒。
|
|
763
|
+
try {
|
|
764
|
+
const tasksIn = gitQuiet(cwd, ['log', '--format=%h', '-n', '1', '--', tasksPath])
|
|
765
|
+
const lastIn = gitQuiet(cwd, ['log', '--format=%h', '-n', '1'])
|
|
766
|
+
if (!tasksIn) {
|
|
767
|
+
console.warn(`⚠️ 勾选时点:tasks.md 未随交付提交(untracked 直至归档)——勾选证据链靠归档兜底,git 历史不可回溯;下次收口前 tasks.md 一并显式 pathspec 提交`)
|
|
768
|
+
} else if (String(tasksIn).trim() === String(lastIn).trim()) {
|
|
769
|
+
console.warn(`⚠️ 勾选时点:tasks.md 的首次 git 提交 == 收口最后提交(一把勾模式)——token 证据齐放行,但边干边勾是进度锚(OS Guardrails 同款纪律),下次完成单元即勾`)
|
|
770
|
+
}
|
|
771
|
+
} catch { /* 时点判定 fail-soft */ }
|
|
772
|
+
}
|
|
773
|
+
// 勾选缺失 advisory(2026-09-25-flow-tick-prototype):有任务行但全未勾(status='none' 且
|
|
774
|
+
// claimTotal>0 且 checked===0)而区间有提交 → 记账缺失提醒(不阻断——不勾选不是假勾,是漏账)
|
|
775
|
+
if (sent.status === 'none' && sent.claimTotal > 0 && sent.checked === 0 && commitMessages.length > 0) {
|
|
776
|
+
console.warn(`⚠️ 任务勾选缺失:tasks.md 有 ${sent.claimTotal} 条任务但一条未勾(区间已有 ${commitMessages.length} 个提交)——`)
|
|
777
|
+
console.warn(` 规范动作是把 tasks.md 覆写为自己的工作分解并完成一个单元即勾选(- [ ] → - [x]);请补勾完成项后再收口(本次放行不阻断)`)
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
} catch (e) { console.warn(`⚠️ 哨兵断言失败(fail-open 放行,best-effort): ${(e && e.message) || e}`) }
|
|
782
|
+
// 勾选节奏 advisory(2026-09-26-thin-check-cadence):watcher 事件流 task-done 单拍跳 ≥2 格 =
|
|
783
|
+
// 一把全勾——per-task 时间戳/进度信号面失真(39 条流零例外实证)。warn 不阻断(对齐上方
|
|
784
|
+
// 「任务勾选缺失」advisory 档位:节奏是习惯问题非造假主张,L0 硬门另有其人);观测旁路
|
|
785
|
+
// 缺席(无流/无事件/读失败)静默跳过——fail-open,watcher 非真相源。
|
|
786
|
+
try {
|
|
787
|
+
const { readWatcherEvents } = await import('./watcher.js')
|
|
788
|
+
const { detectBatchCheckCadence } = await import('./sentinel-assertions.js')
|
|
789
|
+
const stream = readWatcherEvents({ runtimeRoot, change })
|
|
790
|
+
const batch = stream.exists ? detectBatchCheckCadence(stream.events) : null
|
|
791
|
+
if (batch) {
|
|
792
|
+
const at = Number.isFinite(batch.ts) ? new Date(batch.ts).toLocaleTimeString() : '未知时刻'
|
|
793
|
+
console.warn(`⚠️ 勾选节奏:tasks.md 单拍多格勾选(${batch.detail},${at})——未按工作单元逐个勾选`)
|
|
794
|
+
console.warn(' 规范动作是完成一个工作单元即勾一格(- [ ] → - [x]);一把全勾使进度信号与 per-task 时间戳失真(本次放行不阻断)')
|
|
795
|
+
}
|
|
796
|
+
} catch { /* 节奏 advisory best-effort:读流失败静默 */ }
|
|
797
|
+
const gate = await runQuickTestLintGate({ cwd, specBase, changedFiles, changeName: change, skipSentinel: true /* flow 侧已有带 baseline 的哨兵,quick 侧区间不可靠——单判不双判 */ })
|
|
798
|
+
if (gate && gate.action === 'fail') {
|
|
799
|
+
console.error(`❌ 测试门 FAIL(整单 FAIL——实测失败/超时=失败,不继续 distill/归档):`)
|
|
800
|
+
console.error(` ${gate.reason || gate.message || JSON.stringify(gate)}`)
|
|
801
|
+
// 失败触发升级(R7 切片四 / FR-10 / 护栏#4:不依赖 agent 主动)——剩余流程按厚档走
|
|
802
|
+
writeFlowState(changeDir, { tier: 'thick', upgrade_reason: `verify 实测失败(${gate.reason || 'test fail'})——失败自动升厚` })
|
|
803
|
+
console.error(' ⬆️ 已自动升厚档(tier=thick):重入修复后剩余流程按厚档语义(归档不跳过 plan.md 校验)')
|
|
804
|
+
reportMidFail('ledger')
|
|
805
|
+
process.exit(1)
|
|
806
|
+
}
|
|
807
|
+
// 实测面对账(2026-09-25 修复③):agent 可核对自己的测试有没有被扫到——命令/时长/结果文件
|
|
808
|
+
// 一行打全(skip 路径 test/lint 为 null 显示 —;结果文件 test-result.json 含完整文件清单可回溯)。
|
|
809
|
+
// skipped 标因(2026-09-25-platform-feedback-batch2 C):跳过必须说为什么——环境缺件/无测试面/
|
|
810
|
+
// 模块未命中/策略 skip 各有 reason,实测面透传首句让下游能区分。
|
|
811
|
+
const fmt = (r) => r
|
|
812
|
+
? `${r.status}${r.command ? ` ← ${r.command}` : ''}${typeof r.durationMs === 'number' ? `(${(r.durationMs / 1000).toFixed(1)}s)` : ''}${r.resultPath ? ` 结果:${r.resultPath}` : ''}${r.status === 'skipped' && r.reason ? ` — 跳过原因:${String(r.reason).split('。')[0]}。` : ''}`
|
|
813
|
+
: '—'
|
|
814
|
+
gateSummaryText = `test: ${fmt(gate && gate.test)}|lint: ${fmt(gate && gate.lint)}|门文件 ${Array.isArray(changedFiles) ? changedFiles.length : '?'} 个`
|
|
815
|
+
console.log(`🧾 实测面对账 — ${gateSummaryText}`)
|
|
816
|
+
try { writeFlowState(changeDir, { gate_summary: gateSummaryText }) } catch { /* 存档 best-effort */ }
|
|
817
|
+
// FR 腐烂 suspect(2026-09-25-thin-fr-inject-parity):quick-done 钩子迁轻量道(quick 退役后
|
|
818
|
+
// 原侧悬空)——归属文件面触达域的 active FR 打待复核标记,下次知识注入带 ⚠️。advisory 不阻断。
|
|
819
|
+
try {
|
|
820
|
+
const rot = await rotSuspectFlow({ specBase, change, changeDir, files: changedFiles })
|
|
821
|
+
if (rot.warn) console.warn(rot.warn)
|
|
822
|
+
if (rot.warnInfo) console.warn(rot.warnInfo)
|
|
823
|
+
} catch { /* rot fail-open */ }
|
|
824
|
+
mark('ledger')
|
|
825
|
+
|
|
826
|
+
// ── 绑定校验+自动补全(governance-autopilot,R21 实证 P1 修正:从 artifacts 移到 ledger
|
|
827
|
+
// 之后——测试已跑、verify-runs/test-result.json 已生成,auto-bind 才有数据可读)──
|
|
828
|
+
const { verifyRequirementBindings: _vrb } = await import('./flow-draft.js')
|
|
829
|
+
let _rb = _vrb({ changeDir })
|
|
830
|
+
if (_rb.applicable && _rb.emptySlots.length > 0) {
|
|
831
|
+
let _autoFilled = 0
|
|
832
|
+
try {
|
|
833
|
+
const _runtimeRoot = resolveRuntimeRoot(platformOpts || {}, specBase)
|
|
834
|
+
const _runsDir = join(_runtimeRoot, 'verify-runs')
|
|
835
|
+
let _testFiles = new Set()
|
|
836
|
+
if (existsSync(_runsDir)) {
|
|
837
|
+
const _runs = readdirSync(_runsDir).sort().reverse()
|
|
838
|
+
for (const _rd of _runs.slice(0, 3)) {
|
|
839
|
+
const _tr = join(_runsDir, _rd, 'test-result.json')
|
|
840
|
+
if (!existsSync(_tr)) continue
|
|
841
|
+
try {
|
|
842
|
+
const _j = JSON.parse(readFileSync(_tr, 'utf8'))
|
|
843
|
+
// P1 修复:从 command 解析测试文件路径(匹配 test_*.py / *.test.ts / *.spec.tsx 等)
|
|
844
|
+
const _cmd = String(_j.command || '')
|
|
845
|
+
for (const _fm of _cmd.matchAll(/([\w\/\\.-]+\.\w{1,5})/g)) {
|
|
846
|
+
if (/test|spec/i.test(_fm[1])) _testFiles.add(_fm[1].replace(/\\/g, '/'))
|
|
847
|
+
}
|
|
848
|
+
if (_testFiles.size > 0) break
|
|
849
|
+
} catch {}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (_testFiles.size > 0) {
|
|
853
|
+
const _reqPath = join(changeDir, 'requirements.md')
|
|
854
|
+
const _lines = readFileSync(_reqPath, 'utf8').split('\n')
|
|
855
|
+
const _bindingLine = [..._testFiles].slice(0, 3).join('、')
|
|
856
|
+
for (let _li = 0; _li < _lines.length - 1; _li++) {
|
|
857
|
+
if (/^<!--\s*AGENT:测试绑定/.test(_lines[_li]) && !_lines[_li + 1].trim()) {
|
|
858
|
+
_lines[_li + 1] = _bindingLine
|
|
859
|
+
_autoFilled++
|
|
860
|
+
_li++
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (_autoFilled > 0) {
|
|
864
|
+
const { writeAtomicSync } = await import('./fs-atomic.js')
|
|
865
|
+
writeAtomicSync(_reqPath, _lines.join('\n'))
|
|
866
|
+
console.log(`✅ [自动绑定] ${_autoFilled} 个空绑定槽已从测试结果补全(${_bindingLine.slice(0, 60)})——agent 可覆盖`)
|
|
867
|
+
_rb = _vrb({ changeDir })
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
} catch { /* 自动补全 fail-soft */ }
|
|
871
|
+
if (_rb.applicable && _rb.emptySlots.length > 0 && _autoFilled === 0) {
|
|
872
|
+
console.error(`❌ 需求测试绑定未作答:requirements.md ${_rb.emptySlots.length} 处(${_rb.emptySlots.slice(0, 4).join('、')}${_rb.emptySlots.length > 4 ? ' 等' : ''})`)
|
|
873
|
+
console.error(` 每条 FR 至少一行:测试文件项目相对全路径+用例名;无测试面写「不适用:<理由>」`)
|
|
874
|
+
reportMidFail('ledger')
|
|
875
|
+
process.exit(1)
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// ②b patch:变更级 patch 留档(2026-09-25-thin-patch-bindings,noAI):quicklog/patches 生态
|
|
881
|
+
// 退役后审计件挂变更自身。范围=归属收窄后的本变更文件面(他侧声明剔除、含工作树未提交与
|
|
882
|
+
// untracked 自拼 hunk),基=baseline,时点=done 冻结。fail-soft:失败只告警不阻断归档、
|
|
883
|
+
// 不标 done(重入 done 重试)。
|
|
884
|
+
if (st.substeps?.patch === 'done') { skip('patch') } else {
|
|
885
|
+
let patchOk = false
|
|
886
|
+
try {
|
|
887
|
+
// patch 面 = 本变更可归属变化(2026-09-25-thin-patch-scope-fix 收窄:dirty 全扫面会把并行
|
|
888
|
+
// 会话未声明 WIP 冻结进来——上变更实测 49 文件泄漏):①baseline..HEAD 提交面(.sillyspec/
|
|
889
|
+
// 治理面只留本变更目录,他侧 quicklog/knowledge WIP 不入)②本变更目录全部工作树件(未提交
|
|
890
|
+
// 治理面/槽位作答)——排除 patch 自引用。提交面再过他侧声明切分(他侧同窗口提交的防线)。
|
|
891
|
+
if (!st.baseline_commit) {
|
|
892
|
+
console.log('📦 patch 留档跳过:无 git 基线(无历史仓)')
|
|
893
|
+
patchOk = true
|
|
894
|
+
} else {
|
|
895
|
+
const ownPrefix = `.sillyspec/changes/${change}/`
|
|
896
|
+
const diffOut = gitQuiet(cwd, ['diff', '--name-only', `${st.baseline_commit}..HEAD`])
|
|
897
|
+
const committedRaw = String(diffOut || '').split('\n').map((s) => s.trim().replace(/\\/g, '/')).filter(Boolean)
|
|
898
|
+
// 提交面过滤抽为 flow-parity.filterCommittedFace(2026-09-27-tool-debt-cleanup:
|
|
899
|
+
// +.sillyspec/docs/ 交付文档保留——模块卡漏出审计 patch 的评审 P2 修复)
|
|
900
|
+
const { filterCommittedFace } = await import('./flow-parity.js')
|
|
901
|
+
let committed = filterCommittedFace(committedRaw, ownPrefix)
|
|
902
|
+
// 夹带嫌疑 advisory(坑 parallel-session-stale-snapshot-carried-in-commit,2026-09-26
|
|
903
|
+
// 实证:daemon 遥测主题提交整体夹带并行会话旧分叉 page.tsx,静默回滚 main 已落地三处
|
|
904
|
+
// 功能——git 无冲突、聚焦测试不覆盖挂载面,绿灯直过)。提交面里未被本变更任何声明面
|
|
905
|
+
// (design 文件变更清单 ∪ requirements 测试绑定文件)提及的交付文件,收口前显式归因:
|
|
906
|
+
// 属并行会话在途 → pathspec 隔离(AGENTS.md 规则 11);属本变更 → 补 design 自声明。
|
|
907
|
+
// advisory 不阻断(多会话并行是常态,提示归因而非拒绝收口);声明面全空(adopt 等)时
|
|
908
|
+
// 降为「无声明面」提示,不指认夹带。
|
|
909
|
+
try {
|
|
910
|
+
const declared = new Set()
|
|
911
|
+
const { parseFileChangeList } = await import('./change-list.js')
|
|
912
|
+
for (const p of parseFileChangeList(join(changeDir, 'design.md'))) declared.add(p)
|
|
913
|
+
const { extractRequirementBindings } = await import('./flow-draft.js')
|
|
914
|
+
const { testAnchorFile } = await import('./test-bindings.js')
|
|
915
|
+
for (const row of extractRequirementBindings({ changeDir, change })) {
|
|
916
|
+
for (const t of row.tests || []) declared.add(testAnchorFile(t))
|
|
917
|
+
}
|
|
918
|
+
const deliverables = committed.filter((f) => !f.startsWith('.sillyspec/'))
|
|
919
|
+
const undeclared = deliverables.filter((f) => !declared.has(f))
|
|
920
|
+
if (declared.size > 0 && undeclared.length > 0) {
|
|
921
|
+
console.warn(`⚠️ 提交面夹带嫌疑:${undeclared.length} 个 baseline..HEAD 提交文件未被本变更声明面提及(design 文件变更清单/requirements 测试绑定): ${undeclared.slice(0, 5).join('、')}${undeclared.length > 5 ? ' 等' : ''}`)
|
|
922
|
+
console.warn(` 属并行会话在途交付 → 整文件覆盖即静默回滚风险,收口提交用显式 pathspec 隔离(AGENTS.md 规则 11);确属本变更 → 补 design「文件变更清单」自声明后重跑 flow done`)
|
|
923
|
+
} else if (declared.size === 0 && deliverables.length > 0) {
|
|
924
|
+
console.warn(`⚠️ 本变更无任何文件声明面(design 无清单、requirements 无绑定),提交面 ${deliverables.length} 个交付文件无法归因——建议补 design「文件变更清单」自声明(冻结面归属与夹带识别的锚点)`)
|
|
925
|
+
}
|
|
926
|
+
} catch { /* 声明面解析失败不阻断冻结 */ }
|
|
927
|
+
// 提交面不过 foreign 声明切分(2026-09-25-sentinel-evidence-freeze ⑤:已提交的文件就是
|
|
928
|
+
// 本变更的——陈旧声明的旧变更不该抢走 baseline..HEAD 里我实际提交的文件,静默少文件+门禁
|
|
929
|
+
// 静默 skipped 是平台狗粮实证。foreign 声明切分保留给 dirty 面与 attributedChangedFiles
|
|
930
|
+
// (实测面/FR 域路由)——那里归属确实模糊。)
|
|
931
|
+
// dirty 面切分 + 警告不再静默:
|
|
932
|
+
let dirtyForeign = []
|
|
933
|
+
try {
|
|
934
|
+
const { splitOwnVsForeignDiffFiles } = await import('./foreign-declared.js')
|
|
935
|
+
const statusOut = gitQuiet(cwd, ['status', '--porcelain'])
|
|
936
|
+
const dirtyAll = String(statusOut || '').split('\n').map((l) => {
|
|
937
|
+
if (!l || l.length < 4) return null
|
|
938
|
+
const p = line => line.slice(3).trim().replace(/^"|"$/g, '')
|
|
939
|
+
const raw = l.slice(3).trim().replace(/^"|"$/g, '')
|
|
940
|
+
const arrow = raw.indexOf(' -> ')
|
|
941
|
+
return (arrow !== -1 ? raw.slice(arrow + 4) : raw).replace(/\\/g, '/')
|
|
942
|
+
}).filter(Boolean).filter((f) => !f.startsWith('.sillyspec/'))
|
|
943
|
+
if (dirtyAll.length > 0) {
|
|
944
|
+
const dirtySplit = splitOwnVsForeignDiffFiles(cwd, change, dirtyAll, { specBase })
|
|
945
|
+
dirtyForeign = dirtySplit.foreign
|
|
946
|
+
}
|
|
947
|
+
} catch { /* dirty 切分失败不阻断 */ }
|
|
948
|
+
if (dirtyForeign.length > 0) {
|
|
949
|
+
console.warn(`⚠️ patch 留档:${dirtyForeign.length} 个 dirty 文件被其他活跃变更声明排除(${dirtyForeign.slice(0, 3).map((x) => `${x.file}←${x.owners[0]}`).join(', ')}${dirtyForeign.length > 3 ? ' 等' : ''})——如有误(陈旧声明),清理该旧变更或用 --refreeze 重冻结`)
|
|
950
|
+
}
|
|
951
|
+
const changeDirFiles = []
|
|
952
|
+
const walk = (dir) => {
|
|
953
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
954
|
+
// flow-state.yaml 是运行态(机器写、冻结后仍随子步推进变化)——不入审计 patch
|
|
955
|
+
//(2026-09-27-tool-debt-cleanup:旧口径把它当 new file 冻进 patch,评审 P2)
|
|
956
|
+
if (e.name === 'flow-state.yaml') continue
|
|
957
|
+
const p = join(dir, e.name)
|
|
958
|
+
if (e.isDirectory()) walk(p)
|
|
959
|
+
else changeDirFiles.push(relative(cwd, p).replace(/\\/g, '/'))
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
try { walk(changeDir) } catch { /* 目录异常=空面 */ }
|
|
963
|
+
// 冻结面收集(2026-09-25-thin-r16-patches 修复①):committed 为主;会话专属 worktree
|
|
964
|
+
// (gate-snapshot 同款路径判定)下未提交 dirty 交付面一并入冻结——独占树内全归属本变更;
|
|
965
|
+
// 共享主仓 dirty 无法归属只警告(他侧声明免警告),审计缺口显式化不再静默。
|
|
966
|
+
let exclusiveFrom = null // 'flag' | 'worktree' | null——入冻路径来源(输出标签区分)
|
|
967
|
+
if (freezeDirty === true) {
|
|
968
|
+
exclusiveFrom = 'flag' // --freeze-dirty:显式声明非他侧声明 dirty 全归属本变更(独占树自动路径的手动版)
|
|
969
|
+
} else {
|
|
970
|
+
try {
|
|
971
|
+
const { shouldSkipGateSnapshotForWorktree } = await import('./run/gate-snapshot.js')
|
|
972
|
+
if (shouldSkipGateSnapshotForWorktree(cwd)) exclusiveFrom = 'worktree'
|
|
973
|
+
} catch { /* 判定异常按共享主仓 */ }
|
|
974
|
+
}
|
|
975
|
+
const exclusive = exclusiveFrom != null
|
|
976
|
+
let freeze = null
|
|
977
|
+
try {
|
|
978
|
+
const { collectFreezeFiles } = await import('./flow-parity.js')
|
|
979
|
+
freeze = collectFreezeFiles({ cwd, specBase, change, committed, exclusive })
|
|
980
|
+
} catch { freeze = { files: [...committed], dirtyAdded: [], dirtyWarned: [] } }
|
|
981
|
+
if (freeze.dirtyAdded.length > 0) {
|
|
982
|
+
console.log(`🔒 ${exclusiveFrom === 'flag' ? '--freeze-dirty 显式声明' : '会话专属 worktree'}:${freeze.dirtyAdded.length} 个未提交交付文件一并入冻结面`)
|
|
983
|
+
}
|
|
984
|
+
if (freeze.dirtyWarned.length > 0) {
|
|
985
|
+
console.warn(`⚠️ ${freeze.dirtyWarned.length} 个未提交交付文件未入冻结面(共享主仓无法归属)——三选一:`)
|
|
986
|
+
console.warn(` ① 本次接受缺口(审计面少这些文件)② 确认全部归属本变更:重跑 flow done --freeze-dirty 并入冻结 ③ 下次用会话专属 worktree。Git 中间提交可归档后压扁(见收口指引)`)
|
|
987
|
+
console.warn(` ${freeze.dirtyWarned.slice(0, 3).join('、')}${freeze.dirtyWarned.length > 3 ? ' 等' : ''}`)
|
|
988
|
+
}
|
|
989
|
+
const ownFiles = [...new Set([...freeze.files, ...changeDirFiles])]
|
|
990
|
+
.filter((f) => f && !f.endsWith('change.patch') && !f.endsWith('change-patch.json'))
|
|
991
|
+
if (ownFiles.length === 0) {
|
|
992
|
+
console.log('📦 patch 留档跳过:本变更可归属文件面为空')
|
|
993
|
+
patchOk = true
|
|
994
|
+
} else {
|
|
995
|
+
const { buildFrozenPatch, collectNumstatByPath } = await import('./scope-audit.js')
|
|
996
|
+
const frozen = buildFrozenPatch(cwd, ownFiles, { baseRef: st.baseline_commit })
|
|
997
|
+
const stats = collectNumstatByPath(cwd, ownFiles, { baseRef: st.baseline_commit })
|
|
998
|
+
let additions = 0
|
|
999
|
+
let deletions = 0
|
|
1000
|
+
for (const f of ownFiles) {
|
|
1001
|
+
const s = stats.get(f)
|
|
1002
|
+
if (s && Number.isFinite(s.additions)) additions += s.additions
|
|
1003
|
+
if (s && Number.isFinite(s.deletions)) deletions += s.deletions
|
|
1004
|
+
}
|
|
1005
|
+
const head = gitQuiet(cwd, ['rev-parse', 'HEAD'])
|
|
1006
|
+
// 模块文档对账(2026-09-25-thin-parity-assets:厚道 module-impact 死信门的轻量变更 advisory
|
|
1007
|
+
// 等价物——模块文档是后续变更门禁收窄/知识注入的原料,失供是复利折旧)。
|
|
1008
|
+
// 2026-09-27-thin-module-scope-persist:调用前移至 meta 写盘前——结构化结果
|
|
1009
|
+
// (modules/uncoveredDirs/moduleMaps)随 change-patch.json 落盘供平台展示影响模块
|
|
1010
|
+
// 范围,单一计算喂 console 与落盘两处。ownFiles 传冻结面(与 files[] 同口径——
|
|
1011
|
+
// worktree/--freeze-dirty 场景 dirty 交付也计入模块命中;.sillyspec 治理面对账内部
|
|
1012
|
+
// 滤除);三键恒在场(对账异常兜底空数组,评审 P3 清偿);best-effort 不阻断。
|
|
1013
|
+
let moduleScope = { modules: [], uncoveredDirs: [], moduleMaps: [] }
|
|
1014
|
+
try {
|
|
1015
|
+
const { reconcileModuleDocs } = await import('./flow-parity.js')
|
|
1016
|
+
const rec = reconcileModuleDocs({ cwd, specBase, ownFiles, committedRaw })
|
|
1017
|
+
if (rec.lines.length > 0) for (const l of rec.lines) console.log(l)
|
|
1018
|
+
moduleScope = { modules: rec.modules, uncoveredDirs: rec.uncoveredDirs, moduleMaps: rec.moduleMaps }
|
|
1019
|
+
} catch { /* 对账 best-effort(三键空数组兜底) */ }
|
|
1020
|
+
const meta = {
|
|
1021
|
+
change,
|
|
1022
|
+
baseline: st.baseline_commit,
|
|
1023
|
+
head: typeof head === 'string' ? head.trim() : null,
|
|
1024
|
+
files: ownFiles,
|
|
1025
|
+
totals: { files: ownFiles.length, additions, deletions },
|
|
1026
|
+
savedAt: new Date().toISOString(),
|
|
1027
|
+
note: 'flow done 时点冻结(本变更可归属面:baseline..HEAD 提交面过滤 .sillyspec/ 非本变更目录但保留 .sillyspec/docs/ 交付文档 + 本变更目录工作树件;含未提交与 untracked,排除 flow-state.yaml 运行态)',
|
|
1028
|
+
// 模块对账结构化面(2026-09-27-thin-module-scope-persist):done 时点口径(与 files[]
|
|
1029
|
+
// 同时点语义,不随模块图后续变更回写);modules[] 空≠无影响(可能未登记——看 uncoveredDirs)
|
|
1030
|
+
...moduleScope,
|
|
1031
|
+
}
|
|
1032
|
+
if (typeof frozen === 'string' && frozen) {
|
|
1033
|
+
const patchText = frozen.endsWith('\n') ? frozen : frozen + '\n'
|
|
1034
|
+
writeFileSync(join(changeDir, 'change.patch'), patchText)
|
|
1035
|
+
meta.patchSha256 = createHash('sha256').update(patchText.replace(/\r\n/g, '\n'), 'utf8').digest('hex')
|
|
1036
|
+
meta.patchStatus = 'ok'
|
|
1037
|
+
} else {
|
|
1038
|
+
meta.patchStatus = 'failed'
|
|
1039
|
+
}
|
|
1040
|
+
writeFileSync(join(changeDir, 'change-patch.json'), JSON.stringify(meta, null, 2) + '\n')
|
|
1041
|
+
console.log(`📦 变更 patch 留档:change.patch + change-patch.json(${ownFiles.length} 文件,+${additions}/-${deletions}${meta.patchStatus === 'ok' ? ',sha256 已锚' : '——patch 采集失败已留痕'})`)
|
|
1042
|
+
// design 声明面自证(2026-09-25-platform-feedback-batch2 E):design.md 文件变更清单
|
|
1043
|
+
// 声明的交付文件是否都在冻结面——不在=承诺改了但没交付(承诺未兑现面,advisory 不
|
|
1044
|
+
// 阻断——可能是范围裁剪了但 design 没同步更新)
|
|
1045
|
+
try {
|
|
1046
|
+
const designPath = join(changeDir, 'design.md')
|
|
1047
|
+
if (existsSync(designPath)) {
|
|
1048
|
+
const { parseFileChangeListDetailed } = await import('./change-list.js')
|
|
1049
|
+
const declared = parseFileChangeListDetailed(designPath).map((e) => e.path)
|
|
1050
|
+
const frozenSet = new Set(ownFiles.map((f) => String(f).replace(/\\/g, '/')))
|
|
1051
|
+
const missing = declared.filter((p) => !frozenSet.has(p) && !p.startsWith('.sillyspec/'))
|
|
1052
|
+
if (missing.length > 0) {
|
|
1053
|
+
console.warn(`⚠️ design 声明面自证:${missing.length} 个声明文件不在冻结面(${missing.slice(0, 5).join(', ')}${missing.length > 5 ? ' 等' : ''})——design 说了要改但交付里没有,确认是范围裁剪(请同步更新 design 清单)还是遗漏`)
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
} catch { /* 自证 best-effort */ }
|
|
1057
|
+
patchOk = true
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
} catch (e) {
|
|
1061
|
+
console.warn(`⚠️ patch 留档失败(fail-soft 不阻断归档;重入 flow done 从断点重试):${(e && e.message) || e}`)
|
|
1062
|
+
}
|
|
1063
|
+
if (patchOk) mark('patch')
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// ②c review:危险证据定档的独立评审(2026-09-25-thin-review-slice)。定档=证据累积制
|
|
1067
|
+
// (承诺词一票/盲维实质作答/diff 原语/决策密度/声明一票),文件数出局;缺省要评审、豁免要
|
|
1068
|
+
// 多证并举、豁免者 1/4 定额抽查采样。评审任务书由 CLI 渲染(agent 起干净上下文子代理执行,
|
|
1069
|
+
// 协议调用数不变);FAIL 或 P1 发现=拦截(修复后删件重评再重跑 done,断点续)。
|
|
1070
|
+
if (st.substeps?.review === 'done') {
|
|
1071
|
+
skip('review')
|
|
1072
|
+
// 回填(修评审 P2②:续跑成功轮遥测不失忆——review 已发生但子步已标,从留档件回读结论)
|
|
1073
|
+
try {
|
|
1074
|
+
const { validateReviewJson } = await import('./flow-review.js')
|
|
1075
|
+
const rp = join(changeDir, 'review.json')
|
|
1076
|
+
if (existsSync(rp)) {
|
|
1077
|
+
const v = validateReviewJson(rp)
|
|
1078
|
+
reviewOutcome = v.ok
|
|
1079
|
+
? { required: true, sampled: false, verdict: v.review.verdict, findingsP1: (v.review.findings || []).filter((f) => /^P1$/i.test(String(f.severity || ''))).length, backfilled: true }
|
|
1080
|
+
: { required: true, sampled: false, verdict: 'invalid', backfilled: true }
|
|
1081
|
+
} else {
|
|
1082
|
+
reviewOutcome = { required: false, sampled: false, verdict: 'exempt', backfilled: true }
|
|
1083
|
+
}
|
|
1084
|
+
} catch { reviewOutcome = { required: null, verdict: 'done', backfilled: true } }
|
|
1085
|
+
} else {
|
|
1086
|
+
const { classifyReviewNeed, renderReviewerTaskbook, validateReviewJson } = await import('./flow-review.js')
|
|
1087
|
+
let patchText = null
|
|
1088
|
+
try { patchText = readFileSync(join(changeDir, 'change.patch'), 'utf8') } catch { /* patch 子步 skip/失败面 */ }
|
|
1089
|
+
const tier = classifyReviewNeed({ changeDir, input: null, patchText, editRatio: st.edit_ratio ?? null, reviewForce: st.review_force ?? null, change })
|
|
1090
|
+
if (!tier.required) {
|
|
1091
|
+
console.log(`⚖️ 评审豁免(低风险证据齐全):${tier.exemptEvidence.join(';')}`)
|
|
1092
|
+
reviewOutcome = { required: false, sampled: false, verdict: 'exempt' }
|
|
1093
|
+
mark('review')
|
|
1094
|
+
} else if (!existsSync(join(changeDir, 'review.json'))) {
|
|
1095
|
+
console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,起一个干净上下文的子代理执行后重跑本命令:\n`)
|
|
1096
|
+
console.log(renderReviewerTaskbook({ change, changeDir }))
|
|
1097
|
+
reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'missing' }
|
|
1098
|
+
appendTelemetry(reviewOutcome) // 修评审 P2①:失败面先落账再 exit(校准信号不丢)
|
|
1099
|
+
reportMidFail('review')
|
|
1100
|
+
process.exit(1)
|
|
1101
|
+
} else {
|
|
1102
|
+
const v = validateReviewJson(join(changeDir, 'review.json'))
|
|
1103
|
+
if (!v.ok) {
|
|
1104
|
+
console.error(`❌ review.json 校验失败(${v.errors.join(';')})——按任务书 schema 修正后重跑`)
|
|
1105
|
+
reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'invalid' }
|
|
1106
|
+
appendTelemetry(reviewOutcome) // 修评审 P2①
|
|
1107
|
+
reportMidFail('review')
|
|
1108
|
+
process.exit(1)
|
|
1109
|
+
}
|
|
1110
|
+
const p1s = (v.review.findings || []).filter((f) => /^P1$/i.test(String(f.severity || '')))
|
|
1111
|
+
const others = (v.review.findings || []).filter((f) => !/^P1$/i.test(String(f.severity || '')))
|
|
1112
|
+
if (v.review.verdict === 'FAIL' || p1s.length > 0) {
|
|
1113
|
+
console.error(`❌ 独立评审未过(verdict=${v.review.verdict},P1 发现 ${p1s.length} 项)——承诺与实现不一致,不许归档:`)
|
|
1114
|
+
for (const f of [...p1s, ...others]) console.error(` [${f.severity}] ${f.title} — ${f.evidence || ''}${f.location ? `(${f.location})` : ''}`)
|
|
1115
|
+
console.error(` 修复后删除 review.json 并重新执行评审任务书(起子代理重评),再重跑 flow done(断点续)`)
|
|
1116
|
+
reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'FAIL', findingsP1: p1s.length }
|
|
1117
|
+
appendTelemetry(reviewOutcome) // 修评审 P2①:sampled+FAIL 恰是误豁免率校准的最关键信号
|
|
1118
|
+
reportMidFail('review')
|
|
1119
|
+
process.exit(1)
|
|
1120
|
+
}
|
|
1121
|
+
if (others.length > 0) {
|
|
1122
|
+
console.warn(`⚠️ 评审非阻断发现 ${others.length} 项(P2/P3,随归档留档):`)
|
|
1123
|
+
for (const f of others) console.warn(` [${f.severity}] ${f.title} — ${f.evidence || ''}`)
|
|
1124
|
+
}
|
|
1125
|
+
console.log(`✅ 独立评审通过(reviewer=${v.review.reviewer}${tier.sampled ? ',豁免抽查采样命中' : ''})`)
|
|
1126
|
+
reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'PASS', findingsP1: 0 }
|
|
1127
|
+
mark('review')
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// ③ probes:thin 轻量跑无 verify-result 骨架——探针产物面(probe1-8 事实核验)由 flow done
|
|
1132
|
+
// 裁决自含(测试门+工件指纹);升厚(tier=thick)时探针链由 run verify 的既有 --init --draft
|
|
1133
|
+
// 产物面承接(第 3 批资产复用,不重做)。本子步记账占位:thin=not-applicable 直过。
|
|
1134
|
+
if (st.substeps?.probes === 'done') { skip('probes') } else {
|
|
1135
|
+
if (st.tier === 'thick') console.log('ℹ️ 厚档探针链:run verify --done 时经 verify-probes --init --draft 承接(第 3 批既有面)')
|
|
1136
|
+
// 探针 12 error 档收口拦截(2026-09-27-ui-visual-guidance:UI 视觉证据分级门——
|
|
1137
|
+
// 视觉降级无用户裁决留痕恒拦;ui_visual_gate=error 时缺 visual-evidence.md 亦拦;
|
|
1138
|
+
// 默认 warn 档只警告不拦。与 run/gates.js verify 收尾同一检测单点,不二算)。
|
|
1139
|
+
try {
|
|
1140
|
+
const uiProbe = runUiVisualProbe({ changeDir, gate: readUiVisualGate(specBase) })
|
|
1141
|
+
if (uiProbe.level === 'error') {
|
|
1142
|
+
console.error('\n❌ UI 视觉证据门(探针 12 error 档)阻断收口:')
|
|
1143
|
+
for (const n of uiProbe.notes) console.error(` - ${n}`)
|
|
1144
|
+
console.error(` 修复:补渲染对照证据到 ${join(changeDir, UI_EVIDENCE_FILENAME)};降级项补「用户裁决」留痕段后重跑(断点续,已完成子步幂等跳过)。`)
|
|
1145
|
+
reportMidFail('probes')
|
|
1146
|
+
process.exit(1)
|
|
1147
|
+
} else if (uiProbe.level === 'warn') {
|
|
1148
|
+
console.warn(`⚠️ UI 视觉证据(探针 12 warn 档):${uiProbe.notes.join(';')}`)
|
|
1149
|
+
}
|
|
1150
|
+
} catch (e) {
|
|
1151
|
+
console.warn(`⚠️ UI 视觉证据探针异常(fail-soft 降级放行):${(e && e.message) || e}`)
|
|
1152
|
+
}
|
|
1153
|
+
// hunk 归属门(2026-09-27-hunk-attribution-gate:提交面行级对账——规则 11 文件级 pathspec
|
|
1154
|
+
// 防不住「同文件异行」的并行会话夹带,本会话 4f859053 实证。三类信号:未归因文件、跨变更
|
|
1155
|
+
// 竞争、在途残留;hunk_gate warn 默认(警告)/error(阻断)/off。与「提交面夹带嫌疑
|
|
1156
|
+
// advisory」互补:advisory 在冻结面采集处看文件名,本门在 probes 执法看 hunk 与竞争面。
|
|
1157
|
+
try {
|
|
1158
|
+
const gate = readHunkGate(specBase)
|
|
1159
|
+
let committedForGate = []
|
|
1160
|
+
try {
|
|
1161
|
+
committedForGate = String(gitQuiet(cwd, ['diff', '--name-only', `${st.baseline_commit}..HEAD`]) || '')
|
|
1162
|
+
.split('\n').map((x) => x.trim().replace(/\\/g, '/')).filter(Boolean)
|
|
1163
|
+
} catch { /* 无 git/无基线 → 空面,模块内降级跳过 */ }
|
|
1164
|
+
const hunkResult = await runHunkAttributionGate({
|
|
1165
|
+
cwd, specBase, changeName: change, baselineCommit: st.baseline_commit,
|
|
1166
|
+
committedFiles: committedForGate, gitFn: gitQuiet, gate,
|
|
1167
|
+
})
|
|
1168
|
+
for (const line of renderHunkAttributionLines(hunkResult)) {
|
|
1169
|
+
if (line.startsWith('- ✅') || line.startsWith('- ℹ️')) console.log(`🔍 ${line.slice(2)}`)
|
|
1170
|
+
else if (line.startsWith('- ⚠️')) {
|
|
1171
|
+
if (gate === 'error') console.error(`❌ ${line.slice(2)}`)
|
|
1172
|
+
else console.warn(`⚠️ ${line.slice(2)}`)
|
|
1173
|
+
} else if (line.startsWith('- ❌')) console.error(`❌ ${line.slice(2)}`)
|
|
1174
|
+
}
|
|
1175
|
+
if (gate === 'error' && !hunkResult.ok) {
|
|
1176
|
+
console.error(' 修复:未归因文件补 design 自声明或协调归属;竞争文件逐 hunk 核对后协调串行或拆分提交。清零后重跑(断点续)。')
|
|
1177
|
+
reportMidFail('probes')
|
|
1178
|
+
process.exit(1)
|
|
1179
|
+
}
|
|
1180
|
+
} catch (e) {
|
|
1181
|
+
console.warn(`⚠️ hunk 归属门异常(fail-soft 降级放行):${(e && e.message) || e}`)
|
|
1182
|
+
}
|
|
1183
|
+
mark('probes')
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// ④ distill:决策提炼(rejected/needsWait 异态 → 升厚留人工裁决,不静默吞)+ FR 索引提炼
|
|
1187
|
+
// (2026-09-22-thin-fr-distill-sync:轻量变更此前只蒸馏 decisions 不调 indexRequirements——
|
|
1188
|
+
// requirements 永不进 knowledge/fr,知识复利在新默认道断流;轻量变更无 design.md,域路由
|
|
1189
|
+
// 以基线以来交付 diff 供 deliverableFiles,伪域回退同口径)
|
|
1190
|
+
if (st.substeps?.distill === 'done') { skip('distill') } else {
|
|
1191
|
+
// 槽4收割(2026-09-25-thin-parity-assets:design「风险与死路」实质作答合成 decisions.md——
|
|
1192
|
+
// 轻量变更决策产出从零到一;已有 decisions 不覆盖)
|
|
1193
|
+
try {
|
|
1194
|
+
const { harvestSlot4Decision } = await import('./flow-parity.js')
|
|
1195
|
+
const h = harvestSlot4Decision({ changeDir, change })
|
|
1196
|
+
if (h.harvested) console.log(`🌱 槽4(风险与死路)收割 → decisions.md(随蒸馏链进 knowledge)`)
|
|
1197
|
+
} catch { /* 收割 best-effort */ }
|
|
1198
|
+
try {
|
|
1199
|
+
const { distillIntoKnowledge } = await import('./decision-distill.js')
|
|
1200
|
+
const knowledgeRoot = join(specBase, 'knowledge')
|
|
1201
|
+
const head = gitQuiet(cwd, ['rev-parse', 'HEAD'])
|
|
1202
|
+
const res = distillIntoKnowledge(changeDir, knowledgeRoot, typeof head === 'string' ? head.trim() : '', null)
|
|
1203
|
+
const abnormal = res && (res.rejected > 0 || res.needsWait > 0)
|
|
1204
|
+
if (abnormal) {
|
|
1205
|
+
writeFlowState(changeDir, { tier: 'thick', upgrade_reason: `distill 异态(rejected=${res.rejected} needsWait=${res.needsWait})——升厚留人工裁决` })
|
|
1206
|
+
console.warn(`⚠️ distill 异态 → 已升厚档(tier=thick)。剩余流程按厚档走 run <stage> 完整仪式;归档子步仍将执行(厚档语义不跳过 plan.md 校验)。`)
|
|
1207
|
+
}
|
|
1208
|
+
} catch (e) {
|
|
1209
|
+
console.warn(`⚠️ distill best-effort 失败(不阻断归档链): ${(e && e.message) || e}`)
|
|
1210
|
+
}
|
|
1211
|
+
// 测试绑定行落盘(2026-09-25-thin-patch-bindings):requirements 绑定槽 → test-trace.json
|
|
1212
|
+
// (FR 局部锚/candidate/machine),紧接的 indexRequirements 发号后随既有归档提升铸全局。
|
|
1213
|
+
try {
|
|
1214
|
+
const { extractRequirementBindings } = await import('./flow-draft.js')
|
|
1215
|
+
const rows = extractRequirementBindings({ changeDir, change })
|
|
1216
|
+
if (rows.length > 0) {
|
|
1217
|
+
const { writeChangeTrace } = await import('./test-bindings.js')
|
|
1218
|
+
const w = writeChangeTrace(changeDir, change, rows)
|
|
1219
|
+
if (w.changed) console.log(`🔗 测试绑定行落盘:${rows.length} 行 → test-trace.json(随 indexRequirements 发号归档提升)`)
|
|
1220
|
+
}
|
|
1221
|
+
} catch (e) { console.warn(`⚠️ 测试绑定行落盘失败(fail-open):${(e && e.message) || e}`) }
|
|
1222
|
+
try {
|
|
1223
|
+
const { indexRequirements } = await import('./fr-index.js')
|
|
1224
|
+
const knowledgeRoot = join(specBase, 'knowledge')
|
|
1225
|
+
const head = gitQuiet(cwd, ['rev-parse', 'HEAD'])
|
|
1226
|
+
const deliverableFiles = await attributedChangedFiles()
|
|
1227
|
+
// FR 重复嫌疑软门(2026-09-25-thin-fr-inject-parity):brainstorm --done 判据迁轻量道——
|
|
1228
|
+
// 新 FR × 同域 active 标题重叠 ≥0.6 → advisory(承接或改标题双出路),不阻断 distill。
|
|
1229
|
+
try {
|
|
1230
|
+
const dup = await frDupGateFlow({ specBase, change, changeDir, files: deliverableFiles })
|
|
1231
|
+
if (dup.warn) console.warn(dup.warn)
|
|
1232
|
+
} catch { /* 软门 fail-open */ }
|
|
1233
|
+
const r = indexRequirements({ changeDir, knowledgeRoot, headHash: typeof head === 'string' ? head.trim() : '', deliverableFiles })
|
|
1234
|
+
if (r && Array.isArray(r.written) && r.written.length > 0) {
|
|
1235
|
+
console.log(`📚 FR 索引提炼:${r.written.map((w) => w.id).join('、')} → knowledge/fr/(域=${r.written[0].file})`)
|
|
1236
|
+
}
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
console.warn(`⚠️ FR 索引提炼 best-effort 失败(不阻断归档链): ${(e && e.message) || e}`)
|
|
1239
|
+
}
|
|
1240
|
+
mark('distill')
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// ⑤ archive:归档经 runArchiveChain(thin 轻量工件面跳过 plan.md 硬校验;thick 不跳)
|
|
1244
|
+
if (st.substeps?.archive === 'done') { skip('archive') } else {
|
|
1245
|
+
// verify-result 机器回执(2026-09-25-thin-parity-assets:人类可读收口结论,随归档留档)
|
|
1246
|
+
try {
|
|
1247
|
+
const { renderVerifyReceipt } = await import('./flow-parity.js')
|
|
1248
|
+
let patchMeta = null
|
|
1249
|
+
try { patchMeta = JSON.parse(readFileSync(join(changeDir, 'change-patch.json'), 'utf8')) } catch { /* 无冻结件 */ }
|
|
1250
|
+
let traceCount = 0
|
|
1251
|
+
try { traceCount = (JSON.parse(readFileSync(join(changeDir, 'test-trace.json'), 'utf8')).rows || []).length } catch { /* 无锚行 */ }
|
|
1252
|
+
// 实测面回填(断点续跑 ledger 已 skip 时本轮 var 为 null——从 flow-state.gate_summary 回读。
|
|
1253
|
+
// 不回读 verify-runs/test-result.json:隔离快照模式的结果落在临时快照目录且收尾即清,不可靠)
|
|
1254
|
+
if (!gateSummaryText) {
|
|
1255
|
+
try {
|
|
1256
|
+
const stNow = readFlowState(changeDir)
|
|
1257
|
+
if (stNow && stNow.gate_summary) gateSummaryText = String(stNow.gate_summary) + '(断点续跑回读)'
|
|
1258
|
+
} catch { /* 回读失败留 — */ }
|
|
1259
|
+
}
|
|
1260
|
+
const headNow = gitQuiet(cwd, ['rev-parse', 'HEAD'])
|
|
1261
|
+
writeFileSync(join(changeDir, 'verify-result.md'), renderVerifyReceipt({
|
|
1262
|
+
change, baseline: st.baseline_commit, head: typeof headNow === 'string' ? headNow.trim() : null,
|
|
1263
|
+
gateSummary: gateSummaryText, review: reviewOutcome, traceCount, patchMeta,
|
|
1264
|
+
generatedAt: new Date().toISOString(),
|
|
1265
|
+
}))
|
|
1266
|
+
console.log('🧾 验证回执已合成:verify-result.md(随归档留档)')
|
|
1267
|
+
} catch (e) { console.warn(`⚠️ 回执合成失败(不阻断归档):${(e && e.message) || e}`) }
|
|
1268
|
+
const { ProgressManager } = await import('./progress.js')
|
|
1269
|
+
const { runArchiveChain } = await import('./run/complete-handlers.js')
|
|
1270
|
+
// 锚定同 start(平台参数面)
|
|
1271
|
+
const { archiveDestDirName } = await import('./stage-contract.js')
|
|
1272
|
+
const pm = new ProgressManager()
|
|
1273
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
1274
|
+
const destName = archiveDestDirName(date, change)
|
|
1275
|
+
const destDir = join(specBase, 'changes', 'archive', destName)
|
|
1276
|
+
await runArchiveChain({
|
|
1277
|
+
pm, cwd, specBase, changeName: change,
|
|
1278
|
+
srcDir: changeDir,
|
|
1279
|
+
destDir,
|
|
1280
|
+
skipPlanCheck: (st.born_face ?? st.tier) === 'thin',
|
|
1281
|
+
})
|
|
1282
|
+
// 归档后 changeDir 已搬走——后续子步标记随归档目录走(flow-state 随变更包留存审计)
|
|
1283
|
+
markDir = destDir
|
|
1284
|
+
mark('archive')
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// ⑥ events:事件收口(watcher 观测旁路 best-effort 读回,非真相源)
|
|
1288
|
+
if (st.substeps?.events === 'done') { skip('events') } else {
|
|
1289
|
+
try {
|
|
1290
|
+
const eventsPath = join(runtimeRoot, `watcher-events-${change}.jsonl`)
|
|
1291
|
+
if (existsSync(eventsPath)) {
|
|
1292
|
+
const n = readFileSync(eventsPath, 'utf8').split('\n').filter(Boolean).length
|
|
1293
|
+
console.log(`📊 watcher 事件收口:${n} 条 provisional 事件在案(.runtime 留档,平台展示面)`)
|
|
1294
|
+
} else {
|
|
1295
|
+
console.log('📊 watcher 事件收口:无事件文件(观测旁路未在跑——best-effort,不影响裁决)')
|
|
1296
|
+
}
|
|
1297
|
+
} catch { /* 读回失败不阻断 */ }
|
|
1298
|
+
mark('events')
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// ── 切片四收口面:路由信号醒目打印 + 遥测四列记一笔(advisory 定案——只记录不判罚)──
|
|
1302
|
+
const cfg = readFlowConfig(specBase)
|
|
1303
|
+
if (st.route_hint === 'thick') {
|
|
1304
|
+
console.warn(`🧭 route_hint: thick(机器稿改写比例 ${st.edit_ratio ?? '?'} > 阈值 ${cfg.editRatioThreshold}——决策覆盖度低,该走厚档;advisory 不强制,测绿已轻量档过)`)
|
|
1305
|
+
if (cfg.editRatioEnforcement === 'block') {
|
|
1306
|
+
console.error('❌ flow.edit_ratio_enforcement=block:超阈阻断 done(降阈/改走 thick/回退 advisory 三选一)')
|
|
1307
|
+
process.exit(1)
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
appendTelemetry(reviewOutcome) // 成功收口(失败面已在各失败路径提前落账,appendTelemetry 单点)
|
|
1311
|
+
|
|
1312
|
+
console.log(`📦 归档断点:全部子步完成,即将归档——agent 把收口结果(子步清单+评审结论+验证回执)给用户看。`)
|
|
1313
|
+
console.log(`✅ flow done 完成(2/2 协议调用收口):${change}——${SUBSTEPS.length} 子步 ${doneList.join('、')};change 已归档注销。`)
|
|
1314
|
+
// Git 历史整理指引(2026-09-25-thin-freeze-git-hygiene):多轮中间提交可压扁为单提交——审计
|
|
1315
|
+
// 真相在 change.patch(sha256 锚定)与归档产物,不依赖历史形态;baseline 在 change-patch.json。
|
|
1316
|
+
try {
|
|
1317
|
+
const finalHead = gitQuiet(cwd, ['rev-parse', 'HEAD'])
|
|
1318
|
+
if (st.baseline_commit && typeof finalHead === 'string' && finalHead.trim() && finalHead.trim() !== st.baseline_commit) {
|
|
1319
|
+
console.log(`🧹 Git 历史可自由整理:多轮中间提交可压扁为单提交——git reset --soft ${st.baseline_commit.slice(0, 10)} && git commit -m "<最终提交信息>"`)
|
|
1320
|
+
console.log(` 审计真相已冻结在归档件 change.patch(sha256 锚定)+ verify-result + review.json,不依赖 git 历史;`)
|
|
1321
|
+
console.log(` 注:knowledge「最近确认」hash 为溯源快照,压扁后成孤儿引用属预期(非活引用)。`)
|
|
1322
|
+
}
|
|
1323
|
+
} catch { /* 指引 best-effort */ }
|
|
1324
|
+
// 平台同步(同 flow start 尾部接线——归档后的 docs/knowledge/FR 面随本轮回推平台)
|
|
1325
|
+
try { await triggerSync(cwd, change) } catch { /* 同步绝不阻断协议面 */ }
|
|
1326
|
+
return { change, substeps: doneList }
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/** flow 命令族入口(index.js case 'flow' 接线):flow start|done|amend-draft。 */
|
|
1330
|
+
export async function cmdFlow(args, cwd, specDir = null) {
|
|
1331
|
+
const sub = args[0] || ''
|
|
1332
|
+
const rest = args.slice(1)
|
|
1333
|
+
// 平台参数面(2026-09-25-thin-platform-args,平台侧三子代理核对驱动):specBase 统一走
|
|
1334
|
+
// resolvePlatformSpecDir——显式 --spec-dir/--spec-root > .sillyspec-platform.json 指针(fail-closed,
|
|
1335
|
+
// 指针失效报错不静默回退本地防状态分裂)> 本地。此前 flow 族零平台支持:--spec-dir 是 ENOENT
|
|
1336
|
+
// 崩溃路径、指针不读、--spec-root 静默忽略——平台 thin 派发被完全挡住。
|
|
1337
|
+
const getFlag = (name) => {
|
|
1338
|
+
const i = rest.indexOf(name)
|
|
1339
|
+
return i !== -1 && i + 1 < rest.length ? rest[i + 1] : null
|
|
1340
|
+
}
|
|
1341
|
+
const hasFlag = (name) => rest.includes(name)
|
|
1342
|
+
let specBase
|
|
1343
|
+
try {
|
|
1344
|
+
const { resolvePlatformSpecDir } = await import('./progress.js')
|
|
1345
|
+
specBase = resolvePlatformSpecDir(cwd, specDir || getFlag('--spec-root'))
|
|
1346
|
+
} catch (e) {
|
|
1347
|
+
console.error(`❌ 平台 spec 根解析失败(fail-closed,不回退本地防状态分裂):${(e && e.message) || e}`)
|
|
1348
|
+
console.error(' 修复:重跑平台 scan 重建 .sillyspec-platform.json 指针,或显式传 --spec-dir <specRoot>')
|
|
1349
|
+
process.exit(2)
|
|
1350
|
+
}
|
|
1351
|
+
// --runtime-root 透传(start/done 内部 resolveRuntimeRoot 优先取 platformOpts.runtimeRoot)
|
|
1352
|
+
const runtimeRootOpt = getFlag('--runtime-root') || null
|
|
1353
|
+
// 变更名白名单(平台核对实证:穿越名 ../evil 实测逃逸、default/quick-<hex> 形态崩溃)——
|
|
1354
|
+
// 词字符/点/横线/连字符/中文,拒路径分隔符与点穿越;default 与 quick-<8hex> 是进度库辅助键
|
|
1355
|
+
// 非实体变更,flow 层直接拒收
|
|
1356
|
+
const validateChangeName = (name, exitCode = 2) => {
|
|
1357
|
+
const bad = !name || name === 'default' || /^quick-[0-9a-f]{8}$/.test(name)
|
|
1358
|
+
|| !/^[\w.\-一-鿿]{1,120}$/.test(name) || name.includes('..') || /[\/]/.test(name)
|
|
1359
|
+
if (bad) {
|
|
1360
|
+
console.error(`❌ 非法变更名「${name}」——flow 族要求:词字符/点/横线/中文,无路径分隔符与 ..,非 default/quick-<hex> 辅助键(平台键 <日期>-<slug>-<hex6> 天然合法)`)
|
|
1361
|
+
process.exit(exitCode)
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if (sub === 'start') {
|
|
1365
|
+
const change = getFlag('--change') || `flow-${new Date().toISOString().slice(0, 10)}-${Math.random().toString(16).slice(2, 6)}`
|
|
1366
|
+
validateChangeName(change)
|
|
1367
|
+
return cmdFlowStart({
|
|
1368
|
+
change,
|
|
1369
|
+
input: getFlag('--input') || undefined,
|
|
1370
|
+
thick: hasFlag('--thick'),
|
|
1371
|
+
withTasks: hasFlag('--with-tasks'),
|
|
1372
|
+
reviewForce: hasFlag('--review') ? true : hasFlag('--no-review') ? false : null,
|
|
1373
|
+
cwd, specBase,
|
|
1374
|
+
json: hasFlag('--json'),
|
|
1375
|
+
})
|
|
1376
|
+
}
|
|
1377
|
+
if (sub === 'status') {
|
|
1378
|
+
// 三断点配套(2026-09-25-flow-checkpoints):随时可查当前变更阶段/槽位/子步进度
|
|
1379
|
+
const change = getFlag('--change')
|
|
1380
|
+
if (!change) { console.error('❌ flow status 需 --change <名>'); process.exit(2) }
|
|
1381
|
+
validateChangeName(change)
|
|
1382
|
+
const changeDir = join(specBase, 'changes', change)
|
|
1383
|
+
const st = readFlowState(changeDir)
|
|
1384
|
+
if (!st) {
|
|
1385
|
+
// 归档检测:归档后目录搬至 changes/archive/<日期>-<名>(原目录不存在)——查 archive 目录
|
|
1386
|
+
const archiveDir = join(specBase, 'changes', 'archive')
|
|
1387
|
+
let isArchived = false
|
|
1388
|
+
try {
|
|
1389
|
+
if (existsSync(archiveDir)) isArchived = readdirSync(archiveDir).some((e) => e === change) // 精确匹配(fr-governance-sweep:归档目录名恒等 change 名,includes 子串会把 flow-check 误命中 flow-checkpoints)
|
|
1390
|
+
} catch { /* best-effort */ }
|
|
1391
|
+
if (isArchived) {
|
|
1392
|
+
console.log(`📦 ${change}:已归档`)
|
|
1393
|
+
} else if (existsSync(changeDir)) {
|
|
1394
|
+
console.log(`📁 ${change}:变更目录在场但无 flow-state(可能头脑风暴预段产物,尚未进入轻量变更)`)
|
|
1395
|
+
} else {
|
|
1396
|
+
console.log(`❓ ${change}:变更不存在`)
|
|
1397
|
+
}
|
|
1398
|
+
return
|
|
1399
|
+
}
|
|
1400
|
+
const subDone = SUBSTEPS.filter((k) => st.substeps?.[k] === 'done')
|
|
1401
|
+
const subLeft = SUBSTEPS.filter((k) => st.substeps?.[k] !== 'done')
|
|
1402
|
+
// 槽位快检
|
|
1403
|
+
let designFilled = false, frFilled = false, bindingsFilled = 0, bindingsTotal = 0
|
|
1404
|
+
try {
|
|
1405
|
+
const dText = readFileSync(join(changeDir, 'design.md'), 'utf8')
|
|
1406
|
+
designFilled = Array.from(dText.matchAll(/<!--AGENT:槽\d+[^\n]*-->\n(\S)/g)).length >= 3
|
|
1407
|
+
} catch { /* 无 design */ }
|
|
1408
|
+
try {
|
|
1409
|
+
const rText = readFileSync(join(changeDir, 'requirements.md'), 'utf8')
|
|
1410
|
+
frFilled = /### FR-\d+:/.test(rText.replace(/<!--[\s\S]*?-->/g, ''))
|
|
1411
|
+
bindingsTotal = (rText.match(/<!--AGENT:测试绑定FR-\d+/g) || []).length
|
|
1412
|
+
bindingsFilled = (rText.match(/<!--AGENT:测试绑定FR-\d+[^\n]*-->\n\S/g) || []).length
|
|
1413
|
+
} catch { /* 无 requirements */ }
|
|
1414
|
+
// 阶段推断
|
|
1415
|
+
let phase = '① spec(填 FR + design 槽)'
|
|
1416
|
+
if (designFilled && frFilled && bindingsFilled >= bindingsTotal && bindingsTotal > 0) phase = '② 执行(写代码跑测试)→ ①卡点:spec 摘要给用户确认'
|
|
1417
|
+
if (subDone.includes('ledger')) phase = '③ 归档(flow done 收口)'
|
|
1418
|
+
// 勾选提醒(2026-09-26-tick-loop-nudge,R19 实证:起点简报一次性指令几小时后失效,OS 的
|
|
1419
|
+
// 边干边勾靠 Guardrails 常驻干活循环——本行把提醒带进 agent 中途必经的 status 面):
|
|
1420
|
+
// ②执行阶段且有完成迹象(区间已有提交)但勾选滞后 → 一行轻推(不重复刷——只在滞后时出现)。
|
|
1421
|
+
let tickNudge = null
|
|
1422
|
+
try {
|
|
1423
|
+
if (phase.startsWith('②')) {
|
|
1424
|
+
const t = readFileSync(join(changeDir, 'tasks.md'), 'utf8')
|
|
1425
|
+
const c = (t.match(/^- \[x\]/gm) || []).length
|
|
1426
|
+
const tot = (t.match(/^- \[( |x)\]/gm) || []).length
|
|
1427
|
+
const commits = st.baseline_commit ? (gitQuiet(cwd, ['rev-list', '--count', `${st.baseline_commit}..HEAD`]) || '0').toString().trim() : '0'
|
|
1428
|
+
if (tot > 0 && c < tot && parseInt(commits, 10) > 0) {
|
|
1429
|
+
tickNudge = ` ✅ 勾选提醒:已完成单元请顺手勾(${c}/${tot},区间已有 ${commits} 提交——边干边勾是哨兵证据面与进度锚,勿攒到收口一把勾)`
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
} catch { /* 提醒 fail-soft */ }
|
|
1433
|
+
console.log([
|
|
1434
|
+
`📋 ${change}`,
|
|
1435
|
+
` 阶段:${phase}`,
|
|
1436
|
+
` design 槽:${designFilled ? '✅ 已填' : '⬜ 未填'}|FR 区:${frFilled ? '✅ 已填' : '⬜ 未填'}|绑定槽:${bindingsFilled}/${bindingsTotal}`,
|
|
1437
|
+
(() => { try { const t = readFileSync(join(changeDir, 'tasks.md'), 'utf8'); const c = (t.match(/^- \[x\]/gm) || []).length; const tot = (t.match(/^- \[( |x)\]/gm) || []).length; return ` 任务勾选:${c}/${tot}` } catch { return null } })(),
|
|
1438
|
+
tickNudge,
|
|
1439
|
+
` 子步:${subDone.length}/${SUBSTEPS.length}${subDone.length > 0 ? `(${subDone.join('、')})` : ''}`,
|
|
1440
|
+
subLeft.length > 0 ? ` 待办:${subLeft.join('、')}` : '',
|
|
1441
|
+
st.legacy_fallback ? ` ⚠️ 已升厚(legacy_fallback)——剩余流程走 run <stage>` : '',
|
|
1442
|
+
].filter(Boolean).join('\n'))
|
|
1443
|
+
return
|
|
1444
|
+
}
|
|
1445
|
+
if (sub === 'done') {
|
|
1446
|
+
const change = getFlag('--change')
|
|
1447
|
+
if (!change) { console.error('❌ flow done 需 --change <名>'); process.exit(2) }
|
|
1448
|
+
validateChangeName(change)
|
|
1449
|
+
// --refreeze(2026-09-25-sentinel-evidence-freeze ⑤):重置 patch 子步标记强制下次重冻结
|
|
1450
|
+
// (冻结面归属有误时的人工逃生口——提交面已不过 foreign 切分,dirty 面排除有警告指引到此)
|
|
1451
|
+
if (hasFlag('--refreeze')) {
|
|
1452
|
+
const cd = join(specBase, 'changes', change)
|
|
1453
|
+
const stRf = readFlowState(cd)
|
|
1454
|
+
if (stRf) {
|
|
1455
|
+
writeFlowState(cd, { substeps: { patch: null } })
|
|
1456
|
+
console.log('🔄 --refreeze:patch 子步标记已重置,本次 done 将重新冻结(change.patch 按 baseline..HEAD 最新面重建)')
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
return cmdFlowDone({ change, cwd, specBase, runtimeRootOpt, freezeDirty: hasFlag('--freeze-dirty') })
|
|
1460
|
+
}
|
|
1461
|
+
if (sub === 'amend-draft') {
|
|
1462
|
+
// 机器稿唯一留痕修改通道(R7 切片三 / FR-08):重锚哈希 + ledger amendment 审计;
|
|
1463
|
+
// 首版原文 body 永存(切片四 editRatio 基准)。
|
|
1464
|
+
const change = getFlag('--change')
|
|
1465
|
+
if (!change) { console.error('❌ flow amend-draft 需 --change <名>'); process.exit(2) }
|
|
1466
|
+
const changeDir = join(specBase, 'changes', change)
|
|
1467
|
+
const runtimeRoot = resolveRuntimeRoot({}, specBase)
|
|
1468
|
+
const { amendFlowDraft } = await import('./flow-draft.js')
|
|
1469
|
+
const r = amendFlowDraft({ changeDir, change, runtimeRoot })
|
|
1470
|
+
if (r.reanchored.length === 0) {
|
|
1471
|
+
console.error('(无可重锚机器稿——draft ledger 不在案或稿件无标记段)')
|
|
1472
|
+
process.exit(1)
|
|
1473
|
+
}
|
|
1474
|
+
// 切片四:editRatio 路由信号(D-005 advisory 定案——测绿可轻量档过;超阈提示+route_hint 落档)
|
|
1475
|
+
const cfg = readFlowConfig(specBase)
|
|
1476
|
+
// 路由信号取段级最大改写比(任一段过半=该段决策未被输入覆盖——聚合比会被未改段稀释);
|
|
1477
|
+
// 段级明细 sectionRatios 已随 amendment 入 ledger,遥测可见。
|
|
1478
|
+
const maxRatio = Math.max(0, ...Object.values(r.sectionRatios || {}))
|
|
1479
|
+
if (maxRatio > cfg.editRatioThreshold) {
|
|
1480
|
+
writeFlowState(changeDir, { route_hint: 'thick', edit_ratio: maxRatio })
|
|
1481
|
+
console.warn(`⚠️ route_hint: thick——机器稿段级最大改写比例 ${maxRatio}(阈值 ${cfg.editRatioThreshold}):决策覆盖度低,该走厚档(advisory:测绿可轻量档过,不强制;flow done 将醒目提示+遥测记账)`)
|
|
1482
|
+
} else {
|
|
1483
|
+
writeFlowState(changeDir, { edit_ratio: maxRatio })
|
|
1484
|
+
}
|
|
1485
|
+
console.log(`✅ flow amend-draft 留痕重锚:${r.reanchored.join('、')}——ledger amendment 审计在案;editRatio=${maxRatio}(段级最大;基准=首版原文,AGENT 槽不计入)`)
|
|
1486
|
+
return r
|
|
1487
|
+
}
|
|
1488
|
+
console.error('用法: sillyspec flow start --change <名> --input "<任务>" [--thick|--with-tasks] | sillyspec flow done --change <名>')
|
|
1489
|
+
process.exit(2)
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
export default { cmdFlow, cmdFlowStart, cmdFlowDone, readFlowState, writeFlowState, readFlowConfig }
|