sillyspec 3.28.12 → 3.29.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-execute/SKILL.md +222 -223
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -8
- package/.claude/skills/sillyspec-verify/SKILL.md +87 -91
- package/package.json +1 -1
- package/src/blast-surface.js +168 -0
- package/src/ceremony-tier.js +359 -0
- package/src/change-risk-profile.js +75 -202
- package/src/config-schema.js +21 -0
- package/src/decision-distill.js +32 -25
- package/src/decisions-io.js +32 -0
- package/src/design-facts.js +1 -0
- package/src/doctor-diagnostics.js +148 -1
- package/src/fr-index.js +359 -0
- package/src/index.js +92 -5
- package/src/knowledge-stats.js +124 -2
- package/src/modules.js +28 -0
- package/src/prefill.js +491 -0
- package/src/probe7-anchor-check.js +8 -6
- package/src/quick-gate-profile.js +8 -4
- package/src/quick-session-owner.js +63 -0
- package/src/review-dispatch.js +473 -1
- package/src/review-material-pack.js +265 -0
- package/src/review-tier.js +185 -41
- package/src/run/archive-distill.js +35 -0
- package/src/run/command.js +158 -87
- package/src/run/complete-handlers.js +82 -1
- package/src/run/complete.js +37 -1
- package/src/run/gates.js +301 -19
- package/src/run/prompt.js +454 -21
- package/src/run/shared.js +36 -0
- package/src/run/verify-quality-scan.js +14 -6
- package/src/scope-audit.js +8 -1
- package/src/span-risk-surface.js +127 -0
- package/src/stage-contract-spec.js +2 -2
- package/src/stage-contract.js +172 -98
- package/src/stage-review.js +26 -2
- package/src/stages/brainstorm.js +34 -16
- package/src/stages/execute.js +10 -2
- package/src/stages/plan.js +29 -12
- package/src/stages/verify.js +7 -7
- package/src/taskcard.js +3 -0
- package/src/verify-postcheck.js +375 -20
- package/src/verify-probes.js +964 -30
- package/src/worktree.js +1 -1
- package/templates/prompts/taskcard-rules.md +1 -0
- package/templates/prompts/verify-probes.md +1 -1
package/src/run/prompt.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { basename, join } from 'node:path'
|
|
22
22
|
import { existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs'
|
|
23
|
+
import { createHash } from 'node:crypto'
|
|
24
|
+
import jsYaml from 'js-yaml'
|
|
23
25
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
24
26
|
import { stageRegistry } from '../stages/index.js'
|
|
25
27
|
import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, parsePorcelainPath, WAIT_MARKER_RE, QUICK_SID_RE, triggerStepStartSync } from './shared.js'
|
|
@@ -34,6 +36,97 @@ import { REVIEW_SCHEMA_VERSION, isValidExecuteRunId } from '../task-review.js'
|
|
|
34
36
|
// buildKnowledgeInjection docstring)。
|
|
35
37
|
import { matchKnowledge } from '../knowledge-match.js'
|
|
36
38
|
import { appendKnowledgeHit } from '../knowledge-hits.js'
|
|
39
|
+
import { readActiveFrDigest } from '../fr-index.js'
|
|
40
|
+
// 前置失败清单 validator 面(2026-09-18-preflight-slimming task-01):design-facts /
|
|
41
|
+
// stage-contract-engine 均为 src/ 叶子方向(不反向 import 本模块),静态 import 不引入环。
|
|
42
|
+
import { validateDesignFileList } from '../design-facts.js'
|
|
43
|
+
import { evaluateRules } from '../stage-contract-engine.js'
|
|
44
|
+
// 注入账本锁(2026-09-18-preflight-slimming task-02,R-06):withFileLock(quicklog.js:40 先例,
|
|
45
|
+
// 防多会话 read-modify-write 丢更新)——quicklog.js 仅 import path/fs/crypto/git-helper/
|
|
46
|
+
// stage-contract-spec,不反向引用本模块,静态 import 不引入环。
|
|
47
|
+
import { withFileLock } from '../quicklog.js'
|
|
48
|
+
|
|
49
|
+
// ═══════════════════════════════════════════════════════════════
|
|
50
|
+
// ceremony 档位仪式菜单(2026-09-18-ceremony-risk-pricing task-05,D-002/D-005)
|
|
51
|
+
// ═══════════════════════════════════════════════════════════════
|
|
52
|
+
|
|
53
|
+
/** ceremony 档位序(与 ceremony-tier.js CEREMONY_TIERS 同序;此处仅作消费侧排序/枚举校验,不 reimport) */
|
|
54
|
+
const CEREMONY_TIER_ORDER = ['S0', 'S1', 'S2', 'S3']
|
|
55
|
+
|
|
56
|
+
/** 档位→仪式菜单(消费侧映射,design「档位→仪式菜单」契约:引擎只定价不映射) */
|
|
57
|
+
const CEREMONY_TIER_MENUS = [
|
|
58
|
+
['S0', 'CLI 清单核验(stage-review-checklist 机械项逐条核对,零 token 发散)'],
|
|
59
|
+
['S1', 'CLI 清单核验 + 定向探针抽查(机械项全查 + 高风险交叉点定向源码探针,无需独立子代理)'],
|
|
60
|
+
['S2', '独立评审×1(独立审查子代理单轮 + review.json)'],
|
|
61
|
+
['S3', '两轮独立评审 + Grill 深查(两轮独立子代理交叉 + review.json)'],
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 读 local.yaml `ceremony:` 段(best-effort 绝不抛,对齐 readReviewChannelPriority 范式)。
|
|
66
|
+
* force_tier:S0|S1|S2|S3 枚举逃生阀(非法值 warn 后忽略);shadow:boolean 缺省 true。
|
|
67
|
+
* config-schema.js ceremony 节的 prompt 面 reader(影子派发侧消费随 task-06 接线)。
|
|
68
|
+
*/
|
|
69
|
+
export function readCeremonyLocalConfig(cwd) {
|
|
70
|
+
let raw = null
|
|
71
|
+
try {
|
|
72
|
+
const p = join(cwd || process.cwd(), '.sillyspec', 'local.yaml')
|
|
73
|
+
if (existsSync(p)) raw = jsYaml.load(readFileSync(p, 'utf8'))
|
|
74
|
+
} catch { raw = null }
|
|
75
|
+
const c = raw && typeof raw === 'object' && raw.ceremony && typeof raw.ceremony === 'object'
|
|
76
|
+
? raw.ceremony
|
|
77
|
+
: null
|
|
78
|
+
const forceRaw = c && typeof c.force_tier === 'string' ? c.force_tier.trim() : null
|
|
79
|
+
const forceTier = CEREMONY_TIER_ORDER.includes(forceRaw) ? forceRaw : null
|
|
80
|
+
if (forceRaw && !forceTier) {
|
|
81
|
+
console.warn(`[sillyspec] ceremony.force_tier 未知档位「${forceRaw}」已忽略(可选:${CEREMONY_TIER_ORDER.join(' | ')})`)
|
|
82
|
+
}
|
|
83
|
+
const shadow = c && typeof c.shadow === 'boolean' ? c.shadow : true
|
|
84
|
+
return { forceTier, shadow }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* ceremony 档位菜单渲染({REVIEW_TIER} 注入值的追加块——追加不替换:tier: self/independent
|
|
89
|
+
* 语义与 {REVIEW_TIER_REASON} 注入机制原样保留,菜单块按 ceremonyTier 标注当前档)。
|
|
90
|
+
*
|
|
91
|
+
* 逃生阀(ceremony.force_tier)只升不降:强制档高于客观定价档 → 菜单当前档随调、注入
|
|
92
|
+
* tier 值升为 independent(S2/S3);低于客观档 → 只留注记不降档——防 prompt 面与 gate 侧
|
|
93
|
+
* classifyReviewTier(未读逃生阀)判定分裂(prompt 说 self 而 gate 等 review.json 的错位阻断)。
|
|
94
|
+
*
|
|
95
|
+
* 强制轻仪审计痕(FR-02):agent 报 plan_level=full 而档位 S0/S1 → 菜单块追加固定审计行
|
|
96
|
+
* (照「分级异常降级 self」既有降级戳先例:注入文案即留痕),明示勿因「计划写得完整」自行升仪。
|
|
97
|
+
*
|
|
98
|
+
* @param {{ ceremonyTier?: string, tierValue?: string, planLevel?: string|null, cwd?: string }} args
|
|
99
|
+
* @returns {{ tierValue: string, menuMd: string }} menuMd 以 \n 开头(拼在 tier 值后);无有效档位时为 ''
|
|
100
|
+
*/
|
|
101
|
+
export function renderCeremonyTierInjection({ ceremonyTier, tierValue, planLevel, cwd } = {}) {
|
|
102
|
+
const baseTierValue = tierValue || 'self'
|
|
103
|
+
if (!CEREMONY_TIER_ORDER.includes(ceremonyTier)) {
|
|
104
|
+
return { tierValue: baseTierValue, menuMd: '' }
|
|
105
|
+
}
|
|
106
|
+
const { forceTier, shadow } = readCeremonyLocalConfig(cwd)
|
|
107
|
+
let active = ceremonyTier
|
|
108
|
+
let forceNote = ''
|
|
109
|
+
let effectiveTierValue = baseTierValue
|
|
110
|
+
if (forceTier && forceTier !== ceremonyTier) {
|
|
111
|
+
if (CEREMONY_TIER_ORDER.indexOf(forceTier) > CEREMONY_TIER_ORDER.indexOf(ceremonyTier)) {
|
|
112
|
+
active = forceTier
|
|
113
|
+
forceNote = `——local.yaml ceremony.force_tier=${forceTier} 强制升档(逃生阀,绕过客观定价)`
|
|
114
|
+
if (active === 'S2' || active === 'S3') effectiveTierValue = 'independent'
|
|
115
|
+
} else {
|
|
116
|
+
forceNote = `——local.yaml ceremony.force_tier=${forceTier} 低于客观定价档,不降档(只升不降)`
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const menuLines = CEREMONY_TIER_MENUS.map(([t, menu]) => `- ${t}:${menu}${t === active ? ' ◀ 当前档' : ''}`)
|
|
120
|
+
const lightTier = active === 'S0' || active === 'S1'
|
|
121
|
+
const shadowNote = lightTier && shadow
|
|
122
|
+
? '\n(影子期 on:轻档明面轻仪,后台静默派发重仪式对照只记账不阻断——local.yaml ceremony.shadow 开关,对照报告见 doctor)'
|
|
123
|
+
: ''
|
|
124
|
+
const auditTrail = planLevel === 'full' && lightTier
|
|
125
|
+
? '\n⚠️ 强制轻仪审计痕:agent 报 plan_level=full 但 ceremony 档为轻档——仪式按 risk 计价、plan_level 仅编排,按当前轻档菜单执行,勿因「计划写得完整」自行升回 independent×2'
|
|
126
|
+
: ''
|
|
127
|
+
const menuMd = `\nceremony_tier: ${active}${forceNote}(仪式按 risk 计价,plan_level 仅编排——四档菜单:)\n${menuLines.join('\n')}${shadowNote}${auditTrail}`
|
|
128
|
+
return { tierValue: effectiveTierValue, menuMd }
|
|
129
|
+
}
|
|
37
130
|
|
|
38
131
|
/**
|
|
39
132
|
* 从 _module-map.yaml 读取模块上下文索引
|
|
@@ -70,10 +163,9 @@ export function loadModuleContextIndex(specBase, projectName) {
|
|
|
70
163
|
* @param {object} moduleIndex - loadModuleContextIndex 返回值
|
|
71
164
|
* @param {string} specBase - 规范目录
|
|
72
165
|
* @param {string} projectName - 项目名
|
|
73
|
-
* @returns {string}
|
|
166
|
+
* @returns {{text: string, frModules: Array<{id: string, count: number}>}} 上下文注入文本(空=无匹配模块)+ 有活需求的模块清单(供遥测)
|
|
74
167
|
*/
|
|
75
|
-
function buildModuleContextInjection(taskDescription, moduleIndex, specBase, projectName) {
|
|
76
|
-
if (!moduleIndex || !taskDescription) return ''
|
|
168
|
+
export function buildModuleContextInjection(taskDescription, moduleIndex, specBase, projectName) { if (!moduleIndex || !taskDescription) return { text: '', frModules: [] }
|
|
77
169
|
|
|
78
170
|
const taskLower = taskDescription.toLowerCase()
|
|
79
171
|
const matched = []
|
|
@@ -93,15 +185,22 @@ function buildModuleContextInjection(taskDescription, moduleIndex, specBase, pro
|
|
|
93
185
|
if (score > 0) matched.push({ moduleId, data, score, matchReasons })
|
|
94
186
|
}
|
|
95
187
|
|
|
96
|
-
if (matched.length === 0) return ''
|
|
188
|
+
if (matched.length === 0) return { text: '', frModules: [] }
|
|
97
189
|
|
|
98
190
|
matched.sort((a, b) => b.score - a.score)
|
|
99
191
|
|
|
100
192
|
let injection = '\n### 📦 模块上下文(按相关性排序,来自 Module Context Index)\n\n'
|
|
101
193
|
injection += `> 以下模块上下文由 scan 阶段生成的 _module-map.yaml 自动匹配。\n`
|
|
194
|
+
injection += `> 活需求来自 knowledge/fr 索引(active 以索引为准,现场 join)——承接/取代指引见 step8 digest\n`
|
|
102
195
|
injection += `> Matched modules: ${matched.map(m => m.moduleId).join(', ')}\n`
|
|
103
196
|
injection += `> Reasons: ${matched.map(m => m.matchReasons.join(', ')).join('; ')}\n\n`
|
|
104
197
|
|
|
198
|
+
// FR 活需求注入期 join(ql-20260919-002,L2 替代形态):join 键 = 域≡模块 id
|
|
199
|
+
// (fr-index.js resolveTouchedDomains 同键),现场查 active 条目——id+标题紧凑行(场景名
|
|
200
|
+
// 留给 brainstorm step8 digest:此处设计期感知,digest 写作期指引——**有意冗余勿当重复
|
|
201
|
+
// 优化掉**)。截前 5 条+溢出指针行;空段消隐(与 digest 空态同款)。fail-open:读失败该
|
|
202
|
+
// 模块无活需求行,绝不阻塞注入。
|
|
203
|
+
const frModules = []
|
|
105
204
|
for (const { moduleId, data } of matched) {
|
|
106
205
|
injection += `#### ${moduleId}\n`
|
|
107
206
|
if (data.role) injection += `- **职责**: ${String(data.role).slice(0, 100)}\n`
|
|
@@ -124,10 +223,21 @@ function buildModuleContextInjection(taskDescription, moduleIndex, specBase, pro
|
|
|
124
223
|
if (recentChanges.length > 0) {
|
|
125
224
|
injection += `- **最近变更**: ${recentChanges.slice(0, 3).map(e => e.name).join('、')}\n`
|
|
126
225
|
}
|
|
226
|
+
// 活需求行(注入期 join 本体)
|
|
227
|
+
try {
|
|
228
|
+
const frs = readActiveFrDigest(join(specBase, 'knowledge'), [moduleId])
|
|
229
|
+
if (frs.length > 0) {
|
|
230
|
+
const top = frs.slice(0, 5).map(f => `${f.id} ${f.title}`.trim()).join(';')
|
|
231
|
+
injection += `- **活需求**: ${top}`
|
|
232
|
+
if (frs.length > 5) injection += `;+${frs.length - 5} 条见 knowledge/fr/${moduleId}.md`
|
|
233
|
+
injection += '\n'
|
|
234
|
+
frModules.push({ id: moduleId, count: frs.length })
|
|
235
|
+
}
|
|
236
|
+
} catch { /* fail-open:FR 读失败该模块无活需求行 */ }
|
|
127
237
|
injection += '\n'
|
|
128
238
|
}
|
|
129
239
|
|
|
130
|
-
return injection
|
|
240
|
+
return { text: injection, frModules }
|
|
131
241
|
}
|
|
132
242
|
|
|
133
243
|
/**
|
|
@@ -450,6 +560,198 @@ export function requiresUser(step, promptText) {
|
|
|
450
560
|
return WAIT_MARKER_RE.test(String(promptText || ''))
|
|
451
561
|
}
|
|
452
562
|
|
|
563
|
+
// ── 前置失败清单 + 注入账本分叉(2026-09-18-preflight-slimming task-01,D-001@v1 / D-002@v1)──
|
|
564
|
+
// 纯函数面三导出之二/之三(hasDecisionId 在 src/decisions-io.js)。本节只打底零接线:
|
|
565
|
+
// {PREFLIGHT_FAILURES} 占位符渲染与模块/scan 注入分叉归 task-02,stages 步骤定义上的
|
|
566
|
+
// preflightValidators 声明归 task-04——声明落地前 renderPreflightFailures 恒返 '',旧 prompt
|
|
567
|
+
// 逐字节不变(design 兼容策略第一条)。
|
|
568
|
+
|
|
569
|
+
/** 前置清单聚合条数帽(R-01 防打架三约束:条数帽 5——截断防清单全文顶回上下文) */
|
|
570
|
+
const PREFLIGHT_MAX_ITEMS = 5
|
|
571
|
+
|
|
572
|
+
/** 单 validator 超时帽 ms(R-02:只读快跑不拖慢 prompt 渲染;超时该 validator 计为跳过) */
|
|
573
|
+
const PREFLIGHT_VALIDATOR_TIMEOUT_MS = 3000
|
|
574
|
+
|
|
575
|
+
/** 超时哨兵(Promise.race 输出侧标记;Symbol 防与 validator 返回值撞车) */
|
|
576
|
+
const PREFLIGHT_TIMEOUT = Symbol('preflight-validator-timeout')
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* 已声明 validator 名 → 只读校验函数映射(分发骨架的映射表)。
|
|
580
|
+
*
|
|
581
|
+
* 步骤定义在 stages/* 上声明 `preflightValidators: ['<键名>', ...]`(task-04 落地),渲染时按
|
|
582
|
+
* 声明序查本表调用。本表收录键:
|
|
583
|
+
* - `design-file-list`:design-facts.validateDesignFileList 只读快查(design.md 文件清单
|
|
584
|
+
* 幻觉路径 gate 前置)——errors 项为 {path, message},取 message。
|
|
585
|
+
* - `four-piece-rules`:stage-contract 引擎轻查 brainstorm 四件套规则(evaluateRules 纯 kind
|
|
586
|
+
* dispatch,existsSync/readFileSync 量级)。scale 读取与 stage-contract.js
|
|
587
|
+
* validateBrainstormOutputs 同源(design.md frontmatter,fail-safe 无 scale → 四件套全
|
|
588
|
+
* 要求),保证「事前给的 == 事后查的」。v2 扩展点:若内容类规则实测拖慢渲染,可按
|
|
589
|
+
* rule.id 白名单只跑四件套 file-exists 子集。
|
|
590
|
+
* - `postcheck-lite`(plan postcheck 轻子集)/ `allowed-paths-scan`(execute allowed_paths
|
|
591
|
+
* 越界速查):v2 扩展点——task-04 先声明亦安全跳过(未收录键零贡献,不报错)。
|
|
592
|
+
*
|
|
593
|
+
* 每个 validator 返回 { errors: string[] }(error 级失败消息;warning 不入清单——帽 5 内
|
|
594
|
+
* 只放会拦 --done 的项,防噪声挤占)。
|
|
595
|
+
*/
|
|
596
|
+
const PREFLIGHT_VALIDATORS = {
|
|
597
|
+
'design-file-list': ({ changeDir, cwd }) => {
|
|
598
|
+
const r = validateDesignFileList({ changeDir, cwd })
|
|
599
|
+
const errors = (r && Array.isArray(r.errors)) ? r.errors : []
|
|
600
|
+
return { errors: errors.map((e) => (e && e.message) ? e.message : String(e)) }
|
|
601
|
+
},
|
|
602
|
+
'four-piece-rules': ({ changeDir }) => {
|
|
603
|
+
let scale = null
|
|
604
|
+
const designPath = join(changeDir, 'design.md')
|
|
605
|
+
if (existsSync(designPath)) {
|
|
606
|
+
const fm = readFileSync(designPath, 'utf8').match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
|
|
607
|
+
const sm = fm && fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
|
|
608
|
+
if (sm) scale = sm[1]
|
|
609
|
+
}
|
|
610
|
+
const r = evaluateRules('brainstorm', { changeDir, scale })
|
|
611
|
+
return { errors: (r && Array.isArray(r.errors)) ? r.errors : [] }
|
|
612
|
+
},
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* 单 validator 只读快跑(带 3s 超时帽):Promise.race 模式——超时计为跳过(零贡献);
|
|
617
|
+
* validator 抛错原样上抛(由 renderPreflightFailures 外层 catch 整体 fail-open 返 ''——
|
|
618
|
+
* 验收口径「validator 抛错或无失败返回空串」)。finally clearTimeout 防 3s 定时器
|
|
619
|
+
* 拖住 CLI 短进程退出(prompt 渲染完即退,悬挂 handle 会多挂 3s)。
|
|
620
|
+
*
|
|
621
|
+
* @param {string} name PREFLIGHT_VALIDATORS 键名
|
|
622
|
+
* @param {{ changeDir: string|null, cwd: string, specBase: string, stageName: string, stepName: string, changeName: string|null }} ctx
|
|
623
|
+
* @returns {Promise<string[]>} 该 validator 的 error 级失败消息(超时/未收录键 → [])
|
|
624
|
+
*/
|
|
625
|
+
async function runPreflightValidator(name, ctx) {
|
|
626
|
+
const fn = PREFLIGHT_VALIDATORS[name]
|
|
627
|
+
if (typeof fn !== 'function') return [] // 未收录键安全跳过(v2 扩展点见 PREFLIGHT_VALIDATORS 注释)
|
|
628
|
+
let timerId = null
|
|
629
|
+
try {
|
|
630
|
+
const timeout = new Promise((resolve) => { timerId = setTimeout(() => resolve(PREFLIGHT_TIMEOUT), PREFLIGHT_VALIDATOR_TIMEOUT_MS) })
|
|
631
|
+
const result = await Promise.race([Promise.resolve().then(() => fn(ctx)), timeout])
|
|
632
|
+
if (result === PREFLIGHT_TIMEOUT) return []
|
|
633
|
+
return (result && Array.isArray(result.errors)) ? result.errors : []
|
|
634
|
+
} finally {
|
|
635
|
+
if (timerId != null) clearTimeout(timerId)
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* renderPreflightFailures —— 产出型步骤 prompt 的前置失败清单渲染(D-001@v1 Phase 1 纯函数面)。
|
|
641
|
+
*
|
|
642
|
+
* 只读快跑本步骤 --done 将消费的 validator 子集,聚合 error 级失败为清单文本:
|
|
643
|
+
* - 分发骨架:从 stages 注册表步骤定义读 `preflightValidators` 键(task-04 落声明;无声明/
|
|
644
|
+
* 无该 stage/无该步骤名 → 返 ''——渐进兼容,声明落地前零注入)。
|
|
645
|
+
* - 聚合帽:条数帽 5 截断,截断时加「…另 N 项见 gate」行(完整判定归 gate,清单是预检
|
|
646
|
+
* 不是替身);单 validator 超时帽 3s 计跳过;任何异常整体返 ''(fail-open 注入面——
|
|
647
|
+
* 绝不阻塞 prompt 渲染与门判定,R-02)。
|
|
648
|
+
* - 防应试头行(R-07/FR-01):清单头固定「已知失败项(非全部要求)」明示,防清单被当
|
|
649
|
+
* 待办清单应试打磨致清单外质量失查;尾行指路 gate 完整清单。
|
|
650
|
+
*
|
|
651
|
+
* 零副作用:只读不写(validator 全部只读函数),不碰 stages 定义与 outputStep 渲染路径。
|
|
652
|
+
*
|
|
653
|
+
* @param {{ stageName?: string, stepName?: string, cwd?: string, specBase?: string, changeName?: string }} args
|
|
654
|
+
* @returns {Promise<string>} 清单文本;无失败 / 无声明 / 异常 → ''
|
|
655
|
+
*/
|
|
656
|
+
export async function renderPreflightFailures({ stageName, stepName, cwd, specBase, changeName } = {}) {
|
|
657
|
+
try {
|
|
658
|
+
const stageDef = stageRegistry[stageName]
|
|
659
|
+
const steps = (stageDef && Array.isArray(stageDef.steps)) ? stageDef.steps : []
|
|
660
|
+
const step = steps.find((s) => s && s.name === stepName)
|
|
661
|
+
const declared = (step && Array.isArray(step.preflightValidators)) ? step.preflightValidators : []
|
|
662
|
+
if (declared.length === 0) return ''
|
|
663
|
+
const changeDir = (specBase && changeName) ? join(specBase, 'changes', changeName) : null
|
|
664
|
+
const ctx = { changeDir, cwd, specBase, stageName, stepName, changeName }
|
|
665
|
+
// 并行只读快跑(R-02):validator 抛错 → Promise.all 整体 reject → 外层 catch 返 ''
|
|
666
|
+
const outcomes = await Promise.all(declared.map(async (name) => ({ name, errors: await runPreflightValidator(name, ctx) })))
|
|
667
|
+
// 聚合(声明序)+ 折行压平(gate failMessage 可能含换行,清单一项一行)
|
|
668
|
+
const items = []
|
|
669
|
+
for (const o of outcomes) {
|
|
670
|
+
for (const msg of o.errors) items.push(`- ${o.name}: ${String(msg).replace(/\s*\n\s*/g, ' ')}`)
|
|
671
|
+
}
|
|
672
|
+
if (items.length === 0) return ''
|
|
673
|
+
const shown = items.slice(0, PREFLIGHT_MAX_ITEMS)
|
|
674
|
+
const hidden = items.length - shown.length
|
|
675
|
+
return [
|
|
676
|
+
'⚠️ 已知失败项(非全部要求),清单外仍需按步骤说明自检:',
|
|
677
|
+
...shown,
|
|
678
|
+
...(hidden > 0 ? [`…另 ${hidden} 项见 gate`] : []),
|
|
679
|
+
`完整清单:sillyspec gate ${stageName} --json`,
|
|
680
|
+
].join('\n')
|
|
681
|
+
} catch {
|
|
682
|
+
return '' // fail-open:任何异常静默不注(不阻 prompt 渲染)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* shouldInjectFullContext —— 阶段注入账本分叉判定(D-002@v1 Phase 2 纯函数面)。
|
|
688
|
+
*
|
|
689
|
+
* 读 `<runtimeRoot>/prompt-inject-<changeName>.json` 账本({stages:{[stage]:{firstStep,digest,at}}}):
|
|
690
|
+
* - 文件不存在 / JSON 坏 / 无该 stage 记录 → { full: true }(首步语义:本阶段尚未注过全量)
|
|
691
|
+
* - 有记录 → { full: false, digest, firstStep }(同阶段后续步骤注摘要行的判定输入——
|
|
692
|
+
* 「本阶段上下文已于步骤 N 注入(digest 前 8 位)」)
|
|
693
|
+
* - 账本读/解析异常 → { full: true }(design 兼容策略:分叉失败回退每步全量现状)
|
|
694
|
+
*
|
|
695
|
+
* 纯读不写:账本写入(首步全量注入后 withFileLock 幂等落盘)归 task-02;archive 回收
|
|
696
|
+
* (pruneArchivedChangeRuntime 枚举登记)归 task-03。
|
|
697
|
+
*
|
|
698
|
+
* @param {{ changeName?: string, stageName?: string, runtimeRoot?: string }} args
|
|
699
|
+
* @returns {{ full: boolean, digest?: string, firstStep?: number }}
|
|
700
|
+
*/
|
|
701
|
+
export function shouldInjectFullContext({ changeName, stageName, runtimeRoot } = {}) {
|
|
702
|
+
try {
|
|
703
|
+
if (!runtimeRoot || !changeName || !stageName) return { full: true }
|
|
704
|
+
const ledgerPath = join(runtimeRoot, `prompt-inject-${changeName}.json`)
|
|
705
|
+
if (!existsSync(ledgerPath)) return { full: true }
|
|
706
|
+
const ledger = JSON.parse(readFileSync(ledgerPath, 'utf8'))
|
|
707
|
+
const rec = (ledger && ledger.stages && typeof ledger.stages === 'object') ? ledger.stages[stageName] : null
|
|
708
|
+
if (!rec || typeof rec !== 'object') return { full: true }
|
|
709
|
+
return { full: false, digest: rec.digest, firstStep: rec.firstStep }
|
|
710
|
+
} catch {
|
|
711
|
+
return { full: true }
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* 阶段注入摘要行渲染(task-02 接线,D-002@v1 Phase 2):shouldInjectFullContext 判 full=false
|
|
717
|
+
* 时的一行替代注入——同阶段后续步骤不再重注全量模块/scan 上下文,改指路(首步步骤号 + digest
|
|
718
|
+
* 前 8 位 + 可 Read 路径,R-03 摘要丢上下文风险的对冲:该读可读回)。firstStep/digest 缺失
|
|
719
|
+
* (账本记录不完整)给占位符不抛(渲染层永不因账本数据炸)。
|
|
720
|
+
*
|
|
721
|
+
* @param {{ firstStep?: number, digest?: string, specBase: string, projectName: string }} args
|
|
722
|
+
* @returns {string} 单行摘要(「> 」引语形态)
|
|
723
|
+
*/
|
|
724
|
+
function renderPromptInjectSummaryLine({ firstStep, digest, specBase, projectName }) {
|
|
725
|
+
const moduleMapPath = join(specBase, 'docs', projectName, 'modules', '_module-map.yaml')
|
|
726
|
+
const scanDocsDir = join(specBase, 'docs', projectName, 'scan')
|
|
727
|
+
return `> 本阶段模块/scan 上下文已于步骤 ${firstStep ?? '?'} 注入(digest ${digest || 'unknown'});需要时 Read ${moduleMapPath}(模块索引)或 ${scanDocsDir}/ 下 scan 文档(ARCHITECTURE.md / _facts.md)`
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* 阶段注入账本落账(task-02 接线,D-002@v1 Phase 2 / R-06):首步全量注入后写
|
|
732
|
+
* `<runtimeRoot>/prompt-inject-<changeName>.json`({stages:{[stage]:{firstStep,digest,at}}},
|
|
733
|
+
* design 数据模型节)。withFileLock 串行化 read-modify-write(quicklog.js:40 先例——多会话
|
|
734
|
+
* 并行写同账本防丢更新);幂等:已有该 stage 记录不覆盖(并发首步/同步重渲染不改写首步锚点)。
|
|
735
|
+
* digest=全量注入内容 sha256 前 8 位(摘要行可核对的指纹)。异常上抛由调用方 catch 静默
|
|
736
|
+
* (fail-open:账本写失败 → 后续步仍判 full 全量注入,绝不阻塞渲染);archive 回收登记
|
|
737
|
+
* (pruneArchivedChangeRuntime 枚举补本文件名)归 task-03。
|
|
738
|
+
*
|
|
739
|
+
* @param {{ runtimeRoot: string, changeName: string, stageName: string, firstStep: number, digest: string }} args
|
|
740
|
+
*/
|
|
741
|
+
async function recordPromptInjectLedger({ runtimeRoot, changeName, stageName, firstStep, digest }) {
|
|
742
|
+
const ledgerPath = join(runtimeRoot, `prompt-inject-${changeName}.json`)
|
|
743
|
+
await withFileLock(ledgerPath + '.lock', () => {
|
|
744
|
+
let ledger = null
|
|
745
|
+
try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { ledger = null }
|
|
746
|
+
if (!ledger || typeof ledger !== 'object' || Array.isArray(ledger)) ledger = {}
|
|
747
|
+
if (!ledger.stages || typeof ledger.stages !== 'object' || Array.isArray(ledger.stages)) ledger.stages = {}
|
|
748
|
+
if (ledger.stages[stageName] && typeof ledger.stages[stageName] === 'object') return // 幂等:已有该 stage 记录不覆盖
|
|
749
|
+
ledger.stages[stageName] = { firstStep, digest, at: new Date().toISOString() }
|
|
750
|
+
mkdirSync(runtimeRoot, { recursive: true })
|
|
751
|
+
writeAtomicSync(ledgerPath, JSON.stringify(ledger, null, 2) + '\n')
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
|
|
453
755
|
export async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}, prevStepAnswer = null, waitHistory = null, autoMeta = null) {
|
|
454
756
|
const step = steps[stepIndex]
|
|
455
757
|
const total = steps.length
|
|
@@ -831,7 +1133,38 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
831
1133
|
// fail-soft 同三件套;无 quick 占位符自然零输出。
|
|
832
1134
|
if (stageName === 'quick' && promptText.includes('{QUICK_CONTEXT_DIGEST}')) {
|
|
833
1135
|
const digestSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
834
|
-
promptText = promptText.replace(/\{QUICK_CONTEXT_DIGEST
|
|
1136
|
+
promptText = promptText.replace(/\{QUICK_CONTEXT_DIGEST}/g, buildQuickContextDigest(digestSpecBase, projectName))
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// ①c {FR_INDEX_DIGEST}(2026-09-18-fr-index-l1 L1,D-004):brainstorm step8 的触达域现行 FR 注入
|
|
1140
|
+
// ——写作期防重复 FR 的确定性清单(superseded 默认藏);无触达域索引/索引空 → 段消隐。
|
|
1141
|
+
// fr-inject 遥测(L3 证据发生器指标①)fail-soft;token 本体在 stages/brainstorm.js step8 模板。
|
|
1142
|
+
if (stageName === 'brainstorm' && promptText.includes('{FR_INDEX_DIGEST}')) {
|
|
1143
|
+
try {
|
|
1144
|
+
const frSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1145
|
+
const { discoverModuleIndex } = await import('../decision-distill.js')
|
|
1146
|
+
const { resolveTouchedDomains, readActiveFrDigest } = await import('../fr-index.js')
|
|
1147
|
+
const frChangeDir = join(frSpecBase, 'changes', String(changeName || ''))
|
|
1148
|
+
const knowledgeRoot = join(frSpecBase, 'knowledge')
|
|
1149
|
+
const domains = resolveTouchedDomains(frChangeDir, discoverModuleIndex(knowledgeRoot))
|
|
1150
|
+
const entries = readActiveFrDigest(knowledgeRoot, domains)
|
|
1151
|
+
if (entries.length === 0) {
|
|
1152
|
+
promptText = promptText.replace(/\{FR_INDEX_DIGEST\}/g, '(触达域暂无 active FR 索引条目——本变更大概率是这些域的首批需求,照常写作)')
|
|
1153
|
+
} else {
|
|
1154
|
+
const lines = entries.map((e) => `- ${e.id} ${e.title}(来源 ${e.change}${e.scenarios.length ? ';场景:' + e.scenarios.slice(0, 3).join(',') : ''})`)
|
|
1155
|
+
lines.push('')
|
|
1156
|
+
lines.push('> 域解析自本变更 design.md 文件清单(漏域先核对清单)。改写/取代已有行为 → 对应 FR 块加承接行;新行为 → 新 FR 块。superseded 条目默认不列(历史回溯自行读 knowledge/fr/)。')
|
|
1157
|
+
promptText = promptText.replace(/\{FR_INDEX_DIGEST\}/g, lines.join('\n'))
|
|
1158
|
+
}
|
|
1159
|
+
try {
|
|
1160
|
+
const { appendKnowledgeHit } = await import('../knowledge-hits.js')
|
|
1161
|
+
appendKnowledgeHit(join(frSpecBase, '.runtime'), {
|
|
1162
|
+
type: 'fr-inject', change: changeName, domains, count: entries.length, source: 'digest',
|
|
1163
|
+
})
|
|
1164
|
+
} catch { /* 遥测 fail-soft */ }
|
|
1165
|
+
} catch (e) {
|
|
1166
|
+
promptText = promptText.replace(/\{FR_INDEX_DIGEST\}/g, `(FR 索引注入失败:${e && e.message ? e.message : e}——可自行读 {SPEC_ROOT}/knowledge/fr/)`)
|
|
1167
|
+
}
|
|
835
1168
|
}
|
|
836
1169
|
|
|
837
1170
|
// ② {GIT_DIRTY}:工作区脏文件清单(git status --porcelain,quick 收尾步消费)
|
|
@@ -1038,7 +1371,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1038
1371
|
|
|
1039
1372
|
// Stage Review Tier:brainstorm/plan/execute 阶段注入审查分级占位符
|
|
1040
1373
|
// (scanProfile 只在 scan 生效、change-risk-profile 只管 apply/verify,都不约束这些阶段的审查方式,
|
|
1041
|
-
// 故按
|
|
1374
|
+
// 故按 ceremony_tier 风险定价分级——2026-09-18-ceremony-risk-pricing task-05 起 plan_level 降级为
|
|
1375
|
+
// 编排标签不再驱动仪式档:S0/S1 → self 轻仪,S2 → independent×1,S3 → independent 多轮;
|
|
1376
|
+
// {REVIEW_TIER} 注入值 = tier 值 + 追加 ceremony 档位菜单块(renderCeremonyTierInjection),
|
|
1377
|
+
// {REVIEW_TIER_REASON} 注入机制不动(reason 已含 ceremony 档与 blast 来源,task-02 契约))
|
|
1042
1378
|
if (['brainstorm', 'plan', 'execute'].includes(stageName) && promptText.includes('{REVIEW_TIER}')) {
|
|
1043
1379
|
try {
|
|
1044
1380
|
const { classifyReviewTier } = await import('../review-tier.js')
|
|
@@ -1055,6 +1391,12 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1055
1391
|
}
|
|
1056
1392
|
}
|
|
1057
1393
|
const tier = classifyReviewTier({ planLevel, designPath })
|
|
1394
|
+
// ceremony 档位菜单(第三消费面,task-05):classifyReviewTier 双字段的 ceremonyTier 驱动
|
|
1395
|
+
// 仪式档渲染;force_tier 逃生阀只升不降(升向强制 S2/S3 时注入 tier 值随调 independent,
|
|
1396
|
+
// 与下方契约渲染/前序事实注入同用 effective 值,防 prompt 与注入块自相矛盾)
|
|
1397
|
+
const ceremonyInjection = renderCeremonyTierInjection({
|
|
1398
|
+
ceremonyTier: tier.ceremonyTier, tierValue: tier.tier, planLevel, cwd,
|
|
1399
|
+
})
|
|
1058
1400
|
// reviewRunId:优先读 marker 复用(保证多次渲染 prompt 注入同一 ID == gate 读取的 ID,
|
|
1059
1401
|
// 修复「prompt 多次渲染 / 多次 review 时 gate 取错 ID 读错 review.json」),marker 缺失才
|
|
1060
1402
|
// generate + 落盘。对齐 execute {EXECUTE_RUN_ID} 段(prompt.js:449-467)。
|
|
@@ -1081,13 +1423,13 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1081
1423
|
// process.cwd 兜底会读错 local.yaml,2026-09-10 通道配置批次);tier=independent 时契约
|
|
1082
1424
|
// 头部渲染「审查执行通道」段(按用户配置序,见 local.yaml review_dispatch.channel_priority)。
|
|
1083
1425
|
const reviewChannelPriority = readReviewChannelPriority(cwd)
|
|
1084
|
-
const reviewContractMd = renderReviewJsonContract({ stage: stageName, changeDir: tierChangeDir, reviewRunId, tier:
|
|
1426
|
+
const reviewContractMd = renderReviewJsonContract({ stage: stageName, changeDir: tierChangeDir, reviewRunId, tier: ceremonyInjection.tierValue, channelPriority: reviewChannelPriority })
|
|
1085
1427
|
// C2 前阶段实证清单机械附带(坑 review-subagent-redundant-verify,2026-09-15 wp EHS 实证:
|
|
1086
1428
|
// plan 审查把 brainstorm 已两轮实证的三仓源码全量重验——94 分钟不收敛被用户三催。此前只有
|
|
1087
1429
|
// prompt 散文说「前序实证勿重验」,子代理并不知道前序具体审过了什么;现把前序 stage review
|
|
1088
1430
|
// 的 checklist pass 项机械注入派发 prompt(封顶 15 条防灌爆),审查子代理据此跳过已实证面。
|
|
1089
1431
|
let priorFactsMd = ''
|
|
1090
|
-
if (
|
|
1432
|
+
if (ceremonyInjection.tierValue === 'independent') {
|
|
1091
1433
|
try {
|
|
1092
1434
|
const priorStages = stageName === 'plan' ? ['brainstorm'] : stageName === 'execute' ? ['brainstorm', 'plan'] : []
|
|
1093
1435
|
const factLines = []
|
|
@@ -1116,18 +1458,32 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1116
1458
|
// 审过什么/报过什么)。骨架轮/他变更轮由采集端过滤;tier=self 不注入(当前 agent 自带
|
|
1117
1459
|
// 上一轮上下文)。
|
|
1118
1460
|
try {
|
|
1119
|
-
const priorRound = collectSameStagePriorReview(tierRuntimeRoot, stageName, changeName)
|
|
1461
|
+
const priorRound = await collectSameStagePriorReview(tierRuntimeRoot, stageName, changeName)
|
|
1120
1462
|
if (priorRound) {
|
|
1121
1463
|
const blockMd = renderPriorRoundFindingsMd(priorRound)
|
|
1122
1464
|
priorFactsMd = priorFactsMd ? priorFactsMd + blockMd : blockMd
|
|
1123
1465
|
}
|
|
1124
1466
|
} catch {}
|
|
1467
|
+
// {REVIEW_MATERIALS} 机械组装(2026-09-19-review-material-cli-wiring,Gap 1 收口):CLI 半边
|
|
1468
|
+
// 素材(designDigest/fileList/硬约束/diff 摘要/热区/checklist)经 assembleStageReviewMaterials
|
|
1469
|
+
// 组包注入(specBase 用上方 tierSpecBase——块内唯一解析源,平台模式不漂);主代理点名半边
|
|
1470
|
+
// (五交叉点/plan 差量)留位,模板补位指引接手。占位符在场才组装(省无槽步骤的 git IO);
|
|
1471
|
+
// best-effort:失败空串(与占位符缺失同态,模板散文兜底)。
|
|
1472
|
+
let reviewMaterialsMd = ''
|
|
1473
|
+
if (promptText.includes('{REVIEW_MATERIALS}')) {
|
|
1474
|
+
try {
|
|
1475
|
+
const { assembleStageReviewMaterials } = await import('../review-material-pack.js')
|
|
1476
|
+
const matStage = stageName === 'brainstorm' ? 'grill-first' : stageName === 'plan' ? 'plan-review' : 'execute-qa'
|
|
1477
|
+
reviewMaterialsMd = await assembleStageReviewMaterials({ stage: matStage, cwd, changeName, specBase: tierSpecBase })
|
|
1478
|
+
} catch { /* 组装 best-effort:失败零注入 */ }
|
|
1479
|
+
}
|
|
1125
1480
|
promptText = promptText
|
|
1126
|
-
.split('{REVIEW_TIER}').join(
|
|
1481
|
+
.split('{REVIEW_TIER}').join(ceremonyInjection.tierValue + ceremonyInjection.menuMd)
|
|
1127
1482
|
.split('{REVIEW_TIER_REASON}').join(tier.reason)
|
|
1128
1483
|
.split('{STAGE_REVIEW_RUN_ID}').join(reviewRunId)
|
|
1129
1484
|
.split('{REVIEW_JSON_CONTRACT}').join(reviewContractMd)
|
|
1130
1485
|
.split('{PRIOR_REVIEW_FACTS}').join(priorFactsMd)
|
|
1486
|
+
.split('{REVIEW_MATERIALS}').join(reviewMaterialsMd) // 三阶段材料包槽(CLI 组装注入,Gap 1 收口——此前恒空串靠主代理手组,机械保证断一半;主代理点名半边由模板补位指引接手)
|
|
1131
1487
|
} catch (e) {
|
|
1132
1488
|
// 降级 self,避免 prompt 残留占位符
|
|
1133
1489
|
promptText = promptText
|
|
@@ -1136,6 +1492,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1136
1492
|
.split('{STAGE_REVIEW_RUN_ID}').join('review-unknown')
|
|
1137
1493
|
.split('{REVIEW_JSON_CONTRACT}').join('(review 契约注入失败,按 schemaVersion=1 + reviewType + verdicts∈pass/fail/cannot_verify + reviewedFiles + docHash=主文档 sha256 产出)')
|
|
1138
1494
|
.split('{PRIOR_REVIEW_FACTS}').join('')
|
|
1495
|
+
.split('{REVIEW_MATERIALS}').join('') // 降级分支同步 join(plan 审查提示:防占位符字面量残留)
|
|
1139
1496
|
}
|
|
1140
1497
|
}
|
|
1141
1498
|
|
|
@@ -1348,22 +1705,76 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1348
1705
|
promptText = promptText.split('{HANDOVER_SUMMARY}').join(handoverBlock)
|
|
1349
1706
|
}
|
|
1350
1707
|
|
|
1351
|
-
// 注入模块上下文(brainstorm/plan/execute
|
|
1708
|
+
// 注入模块上下文(brainstorm/plan/execute 阶段 + quick 首步「理解任务」——刀①:quick
|
|
1352
1709
|
// step1 原让 agent cat module-map 再挑模块卡读,改为按任务描述匹配后注入,基于 Module Context Index)
|
|
1710
|
+
// ── 阶段感知注入分叉(2026-09-18-preflight-slimming task-02,D-002@v1 Phase 2)──
|
|
1711
|
+
// 三主阶段由「每步全注」改「每阶段首步全量、后续摘要行」:shouldInjectFullContext(task-01)
|
|
1712
|
+
// 读 .runtime/prompt-inject-<change>.json 账本判定——full=真注全量(现状)并在注入后
|
|
1713
|
+
// recordPromptInjectLedger 落账(withFileLock 幂等,R-06);full=假改注一行摘要(首步步骤号 +
|
|
1714
|
+
// digest 前 8 位 + 可 Read 路径,R-03)。quick 不分叉:仅首步注入,本无重注面(现状逐字保留)。
|
|
1715
|
+
// fail-open 红线(design 兼容策略):分叉判定/账本读写任何异常 → 回退全量注入(现状),
|
|
1716
|
+
// 绝不阻塞渲染。
|
|
1353
1717
|
const quickFirstStep = stageName === 'quick' && step && step.name === '理解任务'
|
|
1354
|
-
|
|
1718
|
+
const mainStageModuleInject = ['brainstorm', 'plan', 'execute'].includes(stageName)
|
|
1719
|
+
if ((mainStageModuleInject || quickFirstStep) && projectName) {
|
|
1355
1720
|
const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1356
1721
|
const moduleIndex = loadModuleContextIndex(effectiveSpecBase, projectName)
|
|
1357
1722
|
if (moduleIndex && Object.keys(moduleIndex).length > 0) {
|
|
1358
|
-
//
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1723
|
+
// runtimeRoot 与 {EXECUTE_RUN_ID}/{REVIEW_TIER} 段同源解析(resolveRuntimeRoot(platformOpts, specBase))
|
|
1724
|
+
let injectFork = { full: true } // 缺省=现状全量(quick / 无 changeName / 分叉判定异常)
|
|
1725
|
+
if (mainStageModuleInject && changeName) {
|
|
1726
|
+
try {
|
|
1727
|
+
injectFork = shouldInjectFullContext({
|
|
1728
|
+
changeName, stageName,
|
|
1729
|
+
runtimeRoot: resolveRuntimeRoot(platformOpts, effectiveSpecBase),
|
|
1730
|
+
})
|
|
1731
|
+
} catch { injectFork = { full: true } } // fail-open:判定异常回退全量(现状)
|
|
1363
1732
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
promptText =
|
|
1733
|
+
if (!injectFork.full) {
|
|
1734
|
+
// 同阶段后续步骤:不重注全量,改摘要行(digest 前 8 位 + 可 Read 路径)
|
|
1735
|
+
promptText = renderPromptInjectSummaryLine({
|
|
1736
|
+
firstStep: injectFork.firstStep, digest: injectFork.digest,
|
|
1737
|
+
specBase: effectiveSpecBase, projectName,
|
|
1738
|
+
}) + '\n' + promptText
|
|
1739
|
+
} else {
|
|
1740
|
+
// 尝试从 step prompt / changeName 匹配模块;quick 用 guard.taskDescription(启动 --input)——
|
|
1741
|
+
// changeName 是 quick-<hash> 无语义、step.prompt 全文噪音大(刀①)
|
|
1742
|
+
let taskDesc = step.prompt || changeName || ''
|
|
1743
|
+
if (stageName === 'quick') {
|
|
1744
|
+
taskDesc = readQuickGuardField(changeName, effectiveSpecBase, 'taskDescription') || taskDesc
|
|
1745
|
+
}
|
|
1746
|
+
const injRes = buildModuleContextInjection(taskDesc, moduleIndex, effectiveSpecBase, projectName) || { text: '', frModules: [] }
|
|
1747
|
+
const injection = injRes.text
|
|
1748
|
+
if (injection) {
|
|
1749
|
+
promptText = injection + '\n' + promptText
|
|
1750
|
+
// fr-inject 遥测(module-inject 源,ql-20260919-002):仅 count>0 模块发事件(防 jsonl
|
|
1751
|
+
// 堆 count-0 噪音);type 不变+source 归因(新 type 会静默漏过 knowledge-stats 的 if 链
|
|
1752
|
+
// 缩水 D-006 总数——source 字段保口径)。与 step8 digest 侧可能对同一变更各发一次,
|
|
1753
|
+
// frInjectChanges 按 change 去重不受双发影响。fail-soft。
|
|
1754
|
+
if (injRes.frModules && injRes.frModules.length > 0 && changeName) {
|
|
1755
|
+
try {
|
|
1756
|
+
for (const fm of injRes.frModules) {
|
|
1757
|
+
appendKnowledgeHit(join(effectiveSpecBase, '.runtime'), {
|
|
1758
|
+
type: 'fr-inject', change: changeName, domains: [fm.id], count: fm.count, source: 'module-inject',
|
|
1759
|
+
})
|
|
1760
|
+
}
|
|
1761
|
+
} catch { /* 遥测 fail-soft */ }
|
|
1762
|
+
}
|
|
1763
|
+
// 首步全量注入后落账:本阶段首个非空注入即「首步」锚点(记 firstStep/digest/at——后续
|
|
1764
|
+
// 步骤摘要行的判定输入)。只在真注入非空内容时落账——空注入不占首步位(防摘要行
|
|
1765
|
+
// 「已于步骤 N 注入」指向一个实际没注入任何模块内容的步骤,丢真上下文)。fail-open:
|
|
1766
|
+
// 落账异常静默(语义=后续步仍全量注入),绝不阻塞渲染。
|
|
1767
|
+
if (mainStageModuleInject && changeName) {
|
|
1768
|
+
try {
|
|
1769
|
+
await recordPromptInjectLedger({
|
|
1770
|
+
runtimeRoot: resolveRuntimeRoot(platformOpts, effectiveSpecBase),
|
|
1771
|
+
changeName, stageName,
|
|
1772
|
+
firstStep: stepIndex + 1,
|
|
1773
|
+
digest: createHash('sha256').update(injection, 'utf8').digest('hex').slice(0, 8),
|
|
1774
|
+
})
|
|
1775
|
+
} catch { /* fail-open:账本写失败 → 后续步仍全量注入(现状),不阻塞渲染 */ }
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1367
1778
|
}
|
|
1368
1779
|
}
|
|
1369
1780
|
}
|
|
@@ -1447,6 +1858,28 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1447
1858
|
}
|
|
1448
1859
|
}
|
|
1449
1860
|
|
|
1861
|
+
// ── {PREFLIGHT_FAILURES} 前置失败清单接线(2026-09-18-preflight-slimming task-02,D-001@v1 Phase 1)──
|
|
1862
|
+
// renderPreflightFailures(task-01 纯函数)从 stageRegistry 步骤定义读 preflightValidators
|
|
1863
|
+
// 声明(三 stages 声明统一归 task-04)只读快跑本步骤 --done 将消费的 validator 子集:
|
|
1864
|
+
// 未声明/无失败/超时/异常 → ''。注入位(计划锚点):步骤说明之后、门禁提示(铁律/完成后
|
|
1865
|
+
// 执行/gate 预检)之前——非空且模板无字面占位符时追加到 promptText 末尾;模板含字面
|
|
1866
|
+
// {PREFLIGHT_FAILURES} 时按既有占位符替换机制原位替换(空串替换零残留——「空串时占位符
|
|
1867
|
+
// 零出现」)。空串零注入:声明落地(task-04)前恒 ''——旧 prompt 逐字节不变(design 兼容
|
|
1868
|
+
// 策略第一条,knowledge-inject A/B 双渲染字节一致金丝雀守护)。接线层再包一层 try:
|
|
1869
|
+
// 任何异常静默不注不阻(fail-open 注入面,plan 全局硬约束 2)。
|
|
1870
|
+
let preflightFailuresMd = ''
|
|
1871
|
+
try {
|
|
1872
|
+
const pfSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1873
|
+
preflightFailuresMd = await renderPreflightFailures({
|
|
1874
|
+
stageName, stepName: step.name, cwd, specBase: pfSpecBase, changeName,
|
|
1875
|
+
})
|
|
1876
|
+
} catch { preflightFailuresMd = '' }
|
|
1877
|
+
if (promptText.includes('{PREFLIGHT_FAILURES}')) {
|
|
1878
|
+
promptText = promptText.split('{PREFLIGHT_FAILURES}').join(preflightFailuresMd)
|
|
1879
|
+
} else if (preflightFailuresMd) {
|
|
1880
|
+
promptText = `${promptText}\n\n${preflightFailuresMd}`
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1450
1883
|
console.log(promptText)
|
|
1451
1884
|
// 铁律拆分(W3 token 效率):通用流程纪律(文档优先/不跳步/不编造命令)只在首个 agent 可见步注入——
|
|
1452
1885
|
// 每步重复 ~800B 纯耗 context。但【平台写入规则 + 路径规则】是安全关键(防写错目录/绕过 Write),
|