sillyspec 3.33.3 → 3.33.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-config/SKILL.md +48 -0
- package/.claude/skills/sillyspec-flow/SKILL.md +4 -0
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/src/config-known-failures.js +258 -0
- package/src/config-schema.js +28 -0
- package/src/config-validate.js +180 -0
- package/src/decision-distill.js +26 -5
- package/src/decision-redomain.js +5 -4
- package/src/docs-check.js +8 -4
- package/src/doctor-diagnostics.js +89 -6
- package/src/flow-parity.js +22 -2
- package/src/flow-preflight.js +67 -0
- package/src/flow.js +252 -55
- package/src/fr-index.js +7 -3
- package/src/git-helper.js +82 -0
- package/src/handoff.js +4 -0
- package/src/hooks/worktree-guard.js +12 -3
- package/src/hot-cache.js +60 -0
- package/src/index.js +111 -7
- package/src/knowledge-graph.js +82 -2
- package/src/knowledge-match.js +18 -2
- package/src/knowledge-quicklog.js +17 -2
- package/src/knowledge-vector.js +5 -3
- package/src/machine-interface.js +34 -13
- package/src/progress/change-registry.js +15 -0
- package/src/progress/shared.js +4 -0
- package/src/progress/stage-machine.js +15 -2
- package/src/progress.js +5 -0
- package/src/redomain-unmapped.js +204 -0
- package/src/redomain.js +5 -3
- package/src/review-dispatch.js +208 -47
- package/src/run/bg-sync.js +4 -0
- package/src/run/code-face-key.js +4 -1
- package/src/run/command.js +20 -5
- package/src/run/complete-handlers.js +38 -4
- package/src/run/complete.js +185 -14
- package/src/run/gates.js +30 -1
- package/src/run/prompt.js +9 -1
- package/src/run/quick-audit.js +4 -2
- package/src/run/shared.js +18 -1
- package/src/run/stage.js +22 -19
- package/src/run/verify-quality-scan.js +51 -0
- package/src/runtime-paths.js +3 -0
- package/src/sillyhub-mcp/client.js +11 -6
- package/src/spawn-settle.js +47 -0
- package/src/spec-sync.js +89 -3
- package/src/stages/knowledge.js +25 -3
- package/src/timing.js +55 -0
- package/src/verify-postcheck.js +153 -11
- package/src/watcher.js +4 -0
- package/src/worktree-apply.js +67 -4
- package/src/worktree.js +80 -11
- package/src/yaml-scalar-unquote.js +43 -0
- package/templates/agents-instruction.md +1 -1
|
@@ -9,6 +9,9 @@ import { writeAtomicSync } from '../fs-atomic.js';
|
|
|
9
9
|
import { timeAgo } from '../datetime.js';
|
|
10
10
|
import { runValidators } from '../stage-contract.js';
|
|
11
11
|
import { QUICK_SID_RE } from '../run/shared.js';
|
|
12
|
+
// thin 出身投影读 flow-state(2026-10-21-overview-thin-stage):叶子模块,与 progress.js
|
|
13
|
+
// 2026-10-13-thin-sync-stage 同款 import 先例,零新依赖链。
|
|
14
|
+
import { readFlowState } from '../flow-state.js';
|
|
12
15
|
import { VALID_STAGES, STAGE_LABELS, STAGE_ORDER, MAIN_FLOW_ORDER, SPEC_DIR_NAME, CURRENT_VERSION, STALL_WARN_DAYS, emptyStage } from './shared.js';
|
|
13
16
|
|
|
14
17
|
// quick 会话行(quick-<8hex>)按设计无实体 changes/ 目录(progress.js initChange 跳过建目录),
|
|
@@ -314,12 +317,22 @@ export class StageMachine {
|
|
|
314
317
|
for (const cn of changes) {
|
|
315
318
|
const data = this.pm.read(cwd, cn);
|
|
316
319
|
const dirMissing = _isGhostChange(changesRoot, cn);
|
|
320
|
+
// thin 出身投影(2026-10-21-overview-thin-stage,对齐 2026-10-13-thin-sync-stage
|
|
321
|
+
// 平台上行同款覆写):本地行出生恒 'brainstorm'(thin 不走阶段机),总览按
|
|
322
|
+
// flow-state tier 覆写展示值——否则面板把轻量变更渲染成 brainstorm 主管线。仅覆写
|
|
323
|
+
// envelope 载荷,DB 行不动;listChanges 恒回 active 行(归档不进循环,sync 侧的
|
|
324
|
+
// status 守卫在此天然满足);flow-state 缺失/损坏 readFlowState→null、tier=thick
|
|
325
|
+
// 一律按行值(fail-open 同 sync 口径)。
|
|
326
|
+
let currentStage = data?.currentStage || null;
|
|
327
|
+
if (currentStage && readFlowState(this.pm._changePath(cwd, cn))?.tier === 'thin') {
|
|
328
|
+
currentStage = 'thin';
|
|
329
|
+
}
|
|
317
330
|
const entry = {
|
|
318
331
|
name: cn,
|
|
319
332
|
readable: !!data,
|
|
320
333
|
ghost: dirMissing,
|
|
321
|
-
current_stage:
|
|
322
|
-
stage_label:
|
|
334
|
+
current_stage: currentStage,
|
|
335
|
+
stage_label: currentStage ? (STAGE_LABELS[currentStage] || currentStage) : null,
|
|
323
336
|
last_active: this.pm._dumpIso(data?.lastActive) || null,
|
|
324
337
|
};
|
|
325
338
|
if (data) {
|
package/src/progress.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync, copyFileSync, readdirSync, statSync, realpathSync } from 'fs';
|
|
14
14
|
import { RUNTIME_TEMP_DIRNAME, runtimeTempPath } from './runtime-paths.js'
|
|
15
|
+
import { timedSync } from './timing.js';
|
|
15
16
|
import { join, basename, dirname, resolve, sep } from 'path';
|
|
16
17
|
import { tmpdir, hostname } from 'os';
|
|
17
18
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
@@ -959,6 +960,10 @@ export class ProgressManager {
|
|
|
959
960
|
}
|
|
960
961
|
|
|
961
962
|
_write(cwd, data, changeName = null) {
|
|
963
|
+
return timedSync('state-write', () => this._writeImpl(cwd, data, changeName))
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
_writeImpl(cwd, data, changeName = null) {
|
|
962
967
|
const cn = changeName || data.currentChange;
|
|
963
968
|
if (!cn) {
|
|
964
969
|
console.warn('⚠️ _write: 无变更名,跳过写入');
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* redomain-unmapped.js — unmapped 停车场存量批量清偿驱动(FR-04,2026-10-21-knowledge-tier-denoise)
|
|
3
|
+
*
|
|
4
|
+
* 决策/FR 条目落 unmapped 是域路由失败的兜底——源头匹配器已修(2026-10-15-module-path-glob-match),
|
|
5
|
+
* 存量需机器通道清偿(decision-redomain.js 头注:迁移原语已在,缺批量推断与编排)。本模块即编排:
|
|
6
|
+
* - decisions 条目:文件锚(「文件:」字段行)逐文件 matchedModuleIds 投票 → 目标真模块域
|
|
7
|
+
* - fr 条目:来源变更覆盖面(frCoverageFiles 三源:归档 design 交付表 ∪ change-patch ∪ git)→ 同款投票
|
|
8
|
+
* - 投票零命中/无锚 → 留 unmapped 并在报告列明残量原因(宁漏勿错,不裸删)
|
|
9
|
+
* 迁移原语复用 redomainDecisionEntries / redomainFrEntries:段字节整体搬运、迁域不换号、
|
|
10
|
+
* 目标域同 id 同变更冲突跳过、幂等;INDEX 路由行随原语内 sync 维护(FR-02 泛词已退役)。
|
|
11
|
+
* 只动 knowledge 活库,不回写归档件(D-unmapped-152 死路:归档是冻结审计面)。
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from 'fs'
|
|
14
|
+
import { join } from 'path'
|
|
15
|
+
import { splitKnowledgeSections, discoverModuleIndex } from './decision-distill.js'
|
|
16
|
+
import { matchedModuleIds, frCoverageFiles, FR_SECTION_RE } from './fr-index.js'
|
|
17
|
+
import { anchorFilePaths } from './knowledge-match.js'
|
|
18
|
+
import { redomainDecisionEntries, sectionMeta } from './decision-redomain.js'
|
|
19
|
+
import { redomainFrEntries } from './redomain.js'
|
|
20
|
+
|
|
21
|
+
const DECISION_FILE_LINE_RE = /^文件[::]\s*(.+)$/
|
|
22
|
+
const DECISION_ANCHOR_LINE_RE = /^锚点[::]\s*(.*)$/
|
|
23
|
+
const FR_HEADER_RE = /^## (FR-[\w.-]+-\d+|FR-\d+)\s*(.*)$/
|
|
24
|
+
|
|
25
|
+
/** 决策段 → 路径数组:双源——「文件:」行集(显式字段)∪「锚点:」行 anchorFilePaths 路径
|
|
26
|
+
* 提取(本仓实证 443 条全走锚点源:distill 的 文件: 字段极少发射,锚点是标准兜底
|
|
27
|
+
* 命中源——与 knowledge-match/knowledge-graph 的 anchors 边同判法)。 */
|
|
28
|
+
function filesFromDecisionSection(lines) {
|
|
29
|
+
const out = []
|
|
30
|
+
for (const l of lines) {
|
|
31
|
+
const line = String(l || '')
|
|
32
|
+
const m = line.match(DECISION_FILE_LINE_RE)
|
|
33
|
+
if (m) out.push(...String(m[1]).split(/[,,、\s]+/).map((s) => s.replace(/\\/g, '/')).filter(Boolean))
|
|
34
|
+
const a = line.match(DECISION_ANCHOR_LINE_RE)
|
|
35
|
+
if (a) out.push(...anchorFilePaths(a[1]))
|
|
36
|
+
}
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** FR 段「变更:」行(source change 迁移过滤键同款判法)。 */
|
|
41
|
+
function changeFromSection(lines) {
|
|
42
|
+
for (const l of lines) {
|
|
43
|
+
const m = String(l || '').match(/^变更[::]\s*(\S+)/)
|
|
44
|
+
if (m) return m[1]
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 文件集 → 目标域投票:逐文件 matchedModuleIds(修复后的 glob 匹配器,多命中并计),
|
|
50
|
+
* 最高票为域;平票字典序(确定性)。零命中 → null(留停车场)。 */
|
|
51
|
+
function voteDomain(files, moduleIndex) {
|
|
52
|
+
const tally = new Map()
|
|
53
|
+
for (const f of files) {
|
|
54
|
+
for (const mid of matchedModuleIds(moduleIndex, f)) tally.set(mid, (tally.get(mid) || 0) + 1)
|
|
55
|
+
}
|
|
56
|
+
if (tally.size === 0) return null
|
|
57
|
+
return [...tally.entries()].sort((a, b) => (b[1] - a[1]) || (a[0] < b[0] ? -1 : 1))[0][0]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function planDecisionsFace(knowledgeRoot, archiveRoot, moduleIndex) {
|
|
61
|
+
const fromPath = join(knowledgeRoot, 'decisions', 'unmapped.md')
|
|
62
|
+
if (!existsSync(fromPath)) return { absent: true, groups: [], leftover: [] }
|
|
63
|
+
const parsed = splitKnowledgeSections(readFileSync(fromPath, 'utf8'))
|
|
64
|
+
const groups = new Map()
|
|
65
|
+
const leftover = []
|
|
66
|
+
for (const s of parsed.sections) {
|
|
67
|
+
const meta = sectionMeta(s)
|
|
68
|
+
if (!meta) continue
|
|
69
|
+
// 三源:文件: 行 ∪ 锚点路径(显式锚优先)→ 都不中再走来源变更覆盖面(fr 面同款——
|
|
70
|
+
// 本仓实证 417/443 锚点「未记录」,436/443 有「变更:」行,覆盖面是主力推断源)
|
|
71
|
+
const files = filesFromDecisionSection(s.lines)
|
|
72
|
+
const change = changeFromSection(s.lines)
|
|
73
|
+
let domain = files.length > 0 ? voteDomain(files, moduleIndex) : null
|
|
74
|
+
if (!domain && change) {
|
|
75
|
+
const cov = [...frCoverageFiles({ archiveRoot, changeName: change })]
|
|
76
|
+
if (cov.length > 0) domain = voteDomain(cov, moduleIndex)
|
|
77
|
+
}
|
|
78
|
+
if (!domain) {
|
|
79
|
+
const reason = files.length === 0
|
|
80
|
+
? (change ? '锚点无路径且来源覆盖面不命中任何模块' : '无文件锚/锚点路径且无来源变更')
|
|
81
|
+
: '锚不命中任何模块'
|
|
82
|
+
leftover.push({ id: meta.id, title: meta.title, reason })
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
if (!groups.has(domain)) groups.set(domain, [])
|
|
86
|
+
groups.get(domain).push(meta.id)
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
groups: [...groups.entries()].map(([domain, ids]) => ({ domain, ids })),
|
|
90
|
+
leftover,
|
|
91
|
+
total: parsed.sections.filter((s) => sectionMeta(s)).length,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function planFrFace(knowledgeRoot, archiveRoot, moduleIndex) {
|
|
96
|
+
const fromPath = join(knowledgeRoot, 'fr', 'unmapped.md')
|
|
97
|
+
if (!existsSync(fromPath)) return { absent: true, groups: [], leftover: [] }
|
|
98
|
+
const parsed = splitKnowledgeSections(readFileSync(fromPath, 'utf8'), { sectionRegex: FR_SECTION_RE, buildId: (n) => n })
|
|
99
|
+
const groups = new Map()
|
|
100
|
+
const leftover = []
|
|
101
|
+
for (const s of parsed.sections) {
|
|
102
|
+
const h = FR_HEADER_RE.exec(s.lines[0] || '')
|
|
103
|
+
if (!h) continue
|
|
104
|
+
const id = h[1]
|
|
105
|
+
const title = (h[2] || '').trim()
|
|
106
|
+
const change = changeFromSection(s.lines)
|
|
107
|
+
const files = change ? [...frCoverageFiles({ archiveRoot, changeName: change })] : []
|
|
108
|
+
const domain = files.length > 0 ? voteDomain(files, moduleIndex) : null
|
|
109
|
+
if (!domain) {
|
|
110
|
+
leftover.push({ id, title, reason: !change ? '无来源变更' : '来源变更覆盖面不命中任何模块' })
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
if (!groups.has(domain)) groups.set(domain, [])
|
|
114
|
+
groups.get(domain).push(id)
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
groups: [...groups.entries()].map(([domain, ids]) => ({ domain, ids })),
|
|
118
|
+
leftover,
|
|
119
|
+
total: parsed.sections.filter((s) => FR_HEADER_RE.exec(s.lines[0] || '')).length,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 干跑计划:各面分组(目标域 → 条目 id 集)与残量(id/标题/不可推断原因)。
|
|
125
|
+
* @param {{knowledgeRoot: string, archiveRoot: string}} opts
|
|
126
|
+
* @returns {{decisions: object, fr: object}} 各面 {absent?, groups, leftover, total?}
|
|
127
|
+
*/
|
|
128
|
+
export function planUnmappedRedomain({ knowledgeRoot, archiveRoot }) {
|
|
129
|
+
const moduleIndex = discoverModuleIndex(knowledgeRoot) || { modules: {} }
|
|
130
|
+
return {
|
|
131
|
+
decisions: planDecisionsFace(knowledgeRoot, archiveRoot, moduleIndex),
|
|
132
|
+
fr: planFrFace(knowledgeRoot, archiveRoot, moduleIndex),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 执行清偿(先内部重算计划——apply 自带干跑等价性;幂等:重跑同批 0 迁移)。
|
|
138
|
+
* 单组失败不拦其余组(逐组收集结果与错误如实上报)。
|
|
139
|
+
* @param {{knowledgeRoot: string, archiveRoot: string, faces?: string[]}} opts faces 缺省双面
|
|
140
|
+
* @returns {{results: object, plan: object}} results 各面 {moved, movedByDomain, skippedConflicts, errors}
|
|
141
|
+
*/
|
|
142
|
+
export function applyUnmappedRedomain({ knowledgeRoot, archiveRoot, faces = ['decisions', 'fr'] }) {
|
|
143
|
+
const plan = planUnmappedRedomain({ knowledgeRoot, archiveRoot })
|
|
144
|
+
const empty = { moved: 0, movedByDomain: {}, skippedConflicts: [], errors: [] }
|
|
145
|
+
const results = {}
|
|
146
|
+
const run = (groups, migrate) => {
|
|
147
|
+
const acc = { moved: 0, movedByDomain: {}, skippedConflicts: [], errors: [] }
|
|
148
|
+
for (const g of groups) {
|
|
149
|
+
try {
|
|
150
|
+
const res = migrate(g)
|
|
151
|
+
acc.moved += res.moved.length
|
|
152
|
+
acc.movedByDomain[g.domain] = res.moved.length
|
|
153
|
+
if (Array.isArray(res.skippedConflicts)) acc.skippedConflicts.push(...res.skippedConflicts.map((id) => `${g.domain}:${id}`))
|
|
154
|
+
} catch (e) {
|
|
155
|
+
acc.errors.push({ domain: g.domain, error: e && e.message ? e.message : String(e) })
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return acc
|
|
159
|
+
}
|
|
160
|
+
results.decisions = faces.includes('decisions')
|
|
161
|
+
? run(plan.decisions.groups,
|
|
162
|
+
(g) => redomainDecisionEntries({ knowledgeRoot, from: 'unmapped', to: g.domain, anchors: g.ids }))
|
|
163
|
+
: { ...empty }
|
|
164
|
+
results.fr = faces.includes('fr')
|
|
165
|
+
? run(plan.fr.groups,
|
|
166
|
+
(g) => redomainFrEntries({ knowledgeRoot, from: 'unmapped', to: g.domain, anchors: g.ids }))
|
|
167
|
+
: { ...empty }
|
|
168
|
+
return { results, plan }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* CLI 子命令(stages/knowledge.js 经 tryImportSubcommandImpl 装载):
|
|
173
|
+
* sillyspec knowledge redomain-unmapped [--face decisions|fr] [--apply]
|
|
174
|
+
* 缺省 dry-run 只出报告(JSON 出口与 search/graph 同族);--apply 幂等落盘。
|
|
175
|
+
*/
|
|
176
|
+
export async function cmdRedomainUnmapped(dir, args, opts = {}) {
|
|
177
|
+
const specBase = opts.specDir || join(dir, '.sillyspec')
|
|
178
|
+
const knowledgeRoot = join(specBase, 'knowledge')
|
|
179
|
+
const archiveRoot = join(specBase, 'changes', 'archive')
|
|
180
|
+
const face = args.includes('--face')
|
|
181
|
+
? (args[args.indexOf('--face') + 1] === 'decisions' ? 'decisions' : args[args.indexOf('--face') + 1] === 'fr' ? 'fr' : 'all')
|
|
182
|
+
: 'all'
|
|
183
|
+
const apply = args.includes('--apply')
|
|
184
|
+
const emit = (ok, data, error) => console.log(JSON.stringify({ ok, ...data, ...(error ? { error } : {}) }, null, 2))
|
|
185
|
+
|
|
186
|
+
if (!existsSync(join(knowledgeRoot, 'INDEX.md'))) {
|
|
187
|
+
emit(false, {}, { code: 'knowledge_dir_missing', path: knowledgeRoot })
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
if (apply) {
|
|
191
|
+
const faces = face === 'all' ? ['decisions', 'fr'] : [face]
|
|
192
|
+
const { results, plan } = applyUnmappedRedomain({ knowledgeRoot, archiveRoot, faces })
|
|
193
|
+
const leftoverFace = (name) => (faces.includes(name) ? plan[name].leftover : [])
|
|
194
|
+
emit(true, {
|
|
195
|
+
mode: 'apply', face,
|
|
196
|
+
decisions: results.decisions,
|
|
197
|
+
fr: results.fr,
|
|
198
|
+
leftover: { decisions: leftoverFace('decisions'), fr: leftoverFace('fr') },
|
|
199
|
+
})
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
const plan = planUnmappedRedomain({ knowledgeRoot, archiveRoot })
|
|
203
|
+
emit(true, { mode: 'dry-run', face, decisions: plan.decisions, fr: plan.fr })
|
|
204
|
+
}
|
package/src/redomain.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs'
|
|
16
16
|
import { join } from 'path'
|
|
17
|
-
import { splitKnowledgeSections, joinKnowledgeFile, syncIndexRoutingLines } from './decision-distill.js'
|
|
17
|
+
import { splitKnowledgeSections, joinKnowledgeFile, syncIndexRoutingLines, FR_ROUTE_STRIP_TOKENS } from './decision-distill.js'
|
|
18
18
|
import { FR_SECTION_RE } from './fr-index.js'
|
|
19
19
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
20
20
|
|
|
@@ -141,9 +141,11 @@ export function redomainFrEntries({ knowledgeRoot, from, to, anchors = null, byC
|
|
|
141
141
|
syncIndexRoutingLines(knowledgeRoot, {
|
|
142
142
|
section: 'FR 需求索引',
|
|
143
143
|
subdir: 'fr',
|
|
144
|
+
// 泛词退役(FR-02,2026-10-21-knowledge-tier-denoise):与 fr-index.js 蒸馏侧同款模板
|
|
145
|
+
stripTokens: FR_ROUTE_STRIP_TOKENS,
|
|
144
146
|
makeLine: (d) => d.startsWith('auto-')
|
|
145
|
-
? `- ${d}|${d.slice(5)}
|
|
146
|
-
: `- ${d}
|
|
147
|
+
? `- ${d}|${d.slice(5)} → [fr/${d}.md](fr/${d}.md)`
|
|
148
|
+
: `- ${d} → [fr/${d}.md](fr/${d}.md)`,
|
|
147
149
|
})
|
|
148
150
|
} catch { /* INDEX 缺席/同步失败不拦迁移(主手术已成) */ }
|
|
149
151
|
const after = existsSync(indexPath) ? readFileSync(indexPath, 'utf8') : ''
|
package/src/review-dispatch.js
CHANGED
|
@@ -110,50 +110,65 @@ export function detectStall(record, nowMs, stallMs) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
*
|
|
113
|
+
* 首个平衡 JSON 对象扫描(通道②/③共用):从文本首个 { 起括号配对扫描(字符串字面量内的
|
|
114
|
+
* } 不计数),命中即 JSON.parse 返回对象;无 {} / 首个平衡段解析失败 → null。
|
|
115
|
+
*/
|
|
116
|
+
function scanFirstBalancedJson(text) {
|
|
117
|
+
const t = String(text)
|
|
118
|
+
const start = t.indexOf('{')
|
|
119
|
+
if (start === -1) return null
|
|
120
|
+
let depth = 0, inStr = false, esc = false
|
|
121
|
+
for (let i = start; i < t.length; i++) {
|
|
122
|
+
const ch = t[i]
|
|
123
|
+
if (esc) { esc = false; continue }
|
|
124
|
+
if (ch === '\\') { esc = true; continue }
|
|
125
|
+
if (ch === '"') { inStr = !inStr; continue }
|
|
126
|
+
if (inStr) continue
|
|
127
|
+
if (ch === '{') depth++
|
|
128
|
+
else if (ch === '}') {
|
|
129
|
+
depth--
|
|
130
|
+
if (depth === 0) {
|
|
131
|
+
try {
|
|
132
|
+
const obj = JSON.parse(t.slice(start, i + 1))
|
|
133
|
+
if (obj && typeof obj === 'object') return obj
|
|
134
|
+
} catch { return null }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 从 artifacts / answer 提取 review JSON(三通道容错,design 风险登记「平台 artifacts 形态演进」):
|
|
114
143
|
* ① kind 含 review/review_json 的 artifact,content/content_ref/text 字段 JSON.parse 优先;
|
|
115
|
-
* ② 兜底任一 artifact 文本字段提取首个平衡 {}
|
|
144
|
+
* ② 兜底任一 artifact 文本字段提取首个平衡 {} 对象;
|
|
145
|
+
* ③ answer 正文扫描(2026-10-21-review-dispatch-answer-salvage / dsh 会话实证:worker 把完整
|
|
146
|
+
* review JSON 放回答正文而非 artifacts——任务书已明示该兜底形态)。
|
|
147
|
+
* 第二参可缺省——只传 artifacts 时行为与改前逐字一致(向后兼容)。全失败 → null(调用方给 --salvage 兜底指引)。
|
|
116
148
|
*/
|
|
117
|
-
export function extractReviewFromArtifacts(artifacts) {
|
|
118
|
-
if (!Array.isArray(artifacts)) return null
|
|
149
|
+
export function extractReviewFromArtifacts(artifacts, answer) {
|
|
119
150
|
const texts = []
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
151
|
+
if (Array.isArray(artifacts)) {
|
|
152
|
+
for (const a of artifacts) {
|
|
153
|
+
if (!a || typeof a !== 'object') continue
|
|
154
|
+
const kind = String(a.kind || '')
|
|
155
|
+
const text = typeof a.content === 'string' ? a.content
|
|
156
|
+
: typeof a.content_ref === 'string' ? a.content_ref
|
|
157
|
+
: typeof a.text === 'string' ? a.text : null
|
|
158
|
+
if (text === null) continue
|
|
159
|
+
if (/review/i.test(kind)) {
|
|
160
|
+
try {
|
|
161
|
+
const obj = JSON.parse(text)
|
|
162
|
+
if (obj && typeof obj === 'object') return obj
|
|
163
|
+
} catch { /* 该条不是合法 JSON,落兜底通道 */ }
|
|
164
|
+
}
|
|
165
|
+
texts.push(text)
|
|
132
166
|
}
|
|
133
|
-
texts.push(text)
|
|
134
167
|
}
|
|
168
|
+
if (typeof answer === 'string' && answer) texts.push(answer)
|
|
135
169
|
for (const t of texts) {
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
138
|
-
// 首个平衡对象:括号配对扫描(字符串字面量内的 } 不计数)
|
|
139
|
-
let depth = 0, inStr = false, esc = false
|
|
140
|
-
for (let i = start; i < t.length; i++) {
|
|
141
|
-
const ch = t[i]
|
|
142
|
-
if (esc) { esc = false; continue }
|
|
143
|
-
if (ch === '\\') { esc = true; continue }
|
|
144
|
-
if (ch === '"') { inStr = !inStr; continue }
|
|
145
|
-
if (inStr) continue
|
|
146
|
-
if (ch === '{') depth++
|
|
147
|
-
else if (ch === '}') {
|
|
148
|
-
depth--
|
|
149
|
-
if (depth === 0) {
|
|
150
|
-
try {
|
|
151
|
-
const obj = JSON.parse(t.slice(start, i + 1))
|
|
152
|
-
if (obj && typeof obj === 'object') return obj
|
|
153
|
-
} catch { break }
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
170
|
+
const obj = scanFirstBalancedJson(t)
|
|
171
|
+
if (obj) return obj
|
|
157
172
|
}
|
|
158
173
|
return null
|
|
159
174
|
}
|
|
@@ -161,7 +176,8 @@ export function extractReviewFromArtifacts(artifacts) {
|
|
|
161
176
|
/**
|
|
162
177
|
* 组装 reviewer 任务书(FR-01 派发质量的核心):四要素——①单源清单(前缀按 checklist
|
|
163
178
|
* docblock 规则)②review.json 契约(与 gate 同源)③期望产出方式(read_only worker:
|
|
164
|
-
* 结论经 artifacts 返回,**不写任何文件**——落盘归 CLI
|
|
179
|
+
* 结论经 artifacts 返回,**不写任何文件**——落盘归 CLI;宿主不支持 artifacts 时放回答正文
|
|
180
|
+
* 首个 ```json 块,回收链第三通道兜底)④铁律段(禁改文件/禁 git commit)。
|
|
165
181
|
*/
|
|
166
182
|
export function buildReviewerTaskBook({ stage, changeDir, reviewRunId, channelPriority, probeLines = null }) {
|
|
167
183
|
const mainDoc = STAGE_MAIN_DOC[stage] || 'design.md'
|
|
@@ -183,6 +199,7 @@ export function buildReviewerTaskBook({ stage, changeDir, reviewRunId, channelPr
|
|
|
183
199
|
'## 产出方式(read_only 约束)',
|
|
184
200
|
'- **禁止修改任何文件、禁止 git commit、禁止写盘**——你只读文档与必要的源码佐证。',
|
|
185
201
|
'- 把**完整 review.json 文本**(按上方契约 schema,含 reviewer: {"channel":"platform"})作为产出返回(artifacts 里的 content 字段放纯 JSON 文本)。',
|
|
202
|
+
'- 宿主/平台不支持 artifacts 回传时,把**完整 review.json 文本**放进回答正文的**首个 ```json 代码块**(CLI 回收链会从回答正文扫描兜底)。',
|
|
186
203
|
'- docHash 可占位(调度方 CLI 会按主文档机械重算),其余字段照实填写。',
|
|
187
204
|
].join('\n')
|
|
188
205
|
return {
|
|
@@ -219,6 +236,32 @@ export function persistStageReview({ runtimeRoot, stage, changeName, reviewRunId
|
|
|
219
236
|
return { ok: true, reviewPath }
|
|
220
237
|
}
|
|
221
238
|
|
|
239
|
+
/**
|
|
240
|
+
* worker 终态原文落盘(2026-10-21-review-dispatch-answer-salvage FR-04 证据链):completed
|
|
241
|
+
* 回收路径统一调用——answer 原文优先,缺省序列化 artifacts 原文;两者皆空返回 null(无原文
|
|
242
|
+
* 可落,调用方走「确无结果」口径)。与 review.json 同 run 目录(结构化结论 + 原文证据并存,
|
|
243
|
+
* dsh 事故里人工只能靠平台 UI 贴回原文——原文必须本地在场)。best-effort:写失败 warn 不抛。
|
|
244
|
+
* @returns {string|null} 落盘绝对路径;无内容/写失败 null
|
|
245
|
+
*/
|
|
246
|
+
function writeWorkerAnswer({ runtimeRoot, stage, reviewRunId, gr }) {
|
|
247
|
+
try {
|
|
248
|
+
const answer = gr && typeof gr.answer === 'string' && gr.answer
|
|
249
|
+
? gr.answer
|
|
250
|
+
: gr && Array.isArray(gr.artifacts) && gr.artifacts.length > 0
|
|
251
|
+
? JSON.stringify(gr.artifacts, null, 2) + '\n'
|
|
252
|
+
: null
|
|
253
|
+
if (!answer) return null
|
|
254
|
+
const dir = join(runtimeRoot, 'stage-reviews', `${stage}-${reviewRunId}`)
|
|
255
|
+
mkdirSync(dir, { recursive: true })
|
|
256
|
+
const p = join(dir, 'worker-answer.md')
|
|
257
|
+
writeAtomicSync(p, answer.endsWith('\n') ? answer : answer + '\n')
|
|
258
|
+
return p
|
|
259
|
+
} catch (e) {
|
|
260
|
+
console.warn(`[sillyspec] worker-answer.md 落盘失败(不阻断回收): ${e && e.message ? e.message : String(e)}`)
|
|
261
|
+
return null
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
222
265
|
/** 降级指引(channel_priority 去 platform 的剩余序渲染;probe 失败 / worker 失败共用)。 */
|
|
223
266
|
export function renderFallbackGuidance(cwd) {
|
|
224
267
|
const rest = readReviewChannelPriority(cwd).filter((c) => c !== 'platform')
|
|
@@ -235,13 +278,15 @@ export function renderFallbackGuidance(cwd) {
|
|
|
235
278
|
* @param {object} opts
|
|
236
279
|
* mode: 'create'|'status'|'kill';cwd/specBase/changeName 必填;stage(create 必填);
|
|
237
280
|
* client: SillyHubMcpClient 实例(create/status 用);probe: 可注入(默认真 probeSillyHub);
|
|
238
|
-
* budgetUsd(默认 1.0);stallMs(默认 900000);reviewRunId(status 回收时缺省读 marker
|
|
281
|
+
* budgetUsd(默认 1.0);stallMs(默认 900000);reviewRunId(status 回收时缺省读 marker);
|
|
282
|
+
* salvage(status 专用,默认 false——completed 提取失败时返回 salvagePath + --from 回收指引,
|
|
283
|
+
* 在途记录不清可重试;CLI 形态 `--status --salvage`)。
|
|
239
284
|
* @returns {Promise<{ok: boolean, ...}>} 永不抛——所有异常转 {ok:false, reason}
|
|
240
285
|
*/
|
|
241
286
|
export async function runReviewDispatch(opts = {}) {
|
|
242
287
|
const {
|
|
243
288
|
mode = 'create', cwd = process.cwd(), specBase, changeName,
|
|
244
|
-
stage, client, probe, budgetUsd = 1.0, stallMs = 900_000, reviewRunId,
|
|
289
|
+
stage, client, probe, budgetUsd = 1.0, stallMs = 900_000, reviewRunId, salvage = false,
|
|
245
290
|
} = opts
|
|
246
291
|
if (!specBase || !changeName) return { ok: false, reason: '缺少 specBase/changeName' }
|
|
247
292
|
|
|
@@ -340,21 +385,37 @@ export async function runReviewDispatch(opts = {}) {
|
|
|
340
385
|
}
|
|
341
386
|
if (st === 'completed') {
|
|
342
387
|
const gr = await client.getWorkerResult({ missionId: rec.missionId, workerId: rec.workerId })
|
|
343
|
-
const
|
|
388
|
+
const runtimeRoot = join(specBase, '.runtime')
|
|
389
|
+
// 终态原文先落盘再提取(FR-04:无论提取成败,原文证据与 review.json 同 run 目录在场)
|
|
390
|
+
const answerPath = writeWorkerAnswer({ runtimeRoot, stage: rec.stage, reviewRunId: rec.reviewRunId, gr })
|
|
391
|
+
const review = extractReviewFromArtifacts(gr && gr.artifacts, gr && gr.answer)
|
|
344
392
|
if (!review) {
|
|
345
|
-
|
|
393
|
+
// 两步出口(dsh 事故复盘:旧文案「--kill 降级」混写处置与降级,误导 agent 跳过回收直接自审):
|
|
394
|
+
// ① --salvage 兜底回收(落盘原文 + --from 指引)② 确认平台确无结果才降级
|
|
395
|
+
const reason = 'worker completed 但三通道(artifacts kind / artifacts 文本 / answer 正文)均提取不到 review JSON'
|
|
396
|
+
if (salvage) {
|
|
397
|
+
return {
|
|
398
|
+
ok: false, state: 'completed-no-artifact', statusLine, salvagePath: answerPath, reason,
|
|
399
|
+
guidance: answerPath
|
|
400
|
+
? `① worker 终态原文已落盘 ${answerPath}——人工核对/修整出 review.json 后运行 sillyspec register-stage-review --change ${changeName} --stage ${rec.stage} --from <修整后的review.json> 回收\n② 确认平台确无结果(UI 核对 mission ${rec.missionId})才按通道优先序降级:\n${renderFallbackGuidance(cwd)}`
|
|
401
|
+
: `① 平台未回传任何终态原文(answer 与 artifacts 均空)——在平台 UI 核对 mission ${rec.missionId} / worker ${rec.workerId}\n② 确认确无结果才按通道优先序降级:\n${renderFallbackGuidance(cwd)}`,
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
ok: false, state: 'completed-no-artifact', statusLine, reason,
|
|
406
|
+
guidance: `① 回收兜底:sillyspec review-dispatch --status --salvage --change ${changeName}(自动落盘 worker-answer.md 并提示 register-stage-review --from 回收;平台 UI 可先核对 mission ${rec.missionId})\n② 确认平台确无结果才按通道优先序降级:\n${renderFallbackGuidance(cwd)}`,
|
|
407
|
+
}
|
|
346
408
|
}
|
|
347
|
-
const runtimeRoot = join(specBase, '.runtime')
|
|
348
409
|
const mainDocPath = join(specBase, 'changes', changeName, STAGE_MAIN_DOC[rec.stage] || 'design.md')
|
|
349
410
|
const p = persistStageReview({
|
|
350
411
|
runtimeRoot, stage: rec.stage, changeName, reviewRunId: rec.reviewRunId,
|
|
351
412
|
reviewObj: review, missionId: rec.missionId, mainDocPath,
|
|
352
413
|
})
|
|
353
414
|
if (!p.ok) {
|
|
354
|
-
return { ok: false, state: 'bad-review', statusLine, errors: p.errors, guidance:
|
|
415
|
+
return { ok: false, state: 'bad-review', statusLine, errors: p.errors, guidance: `① 提取到的 review 不符 schema——按契约修整(终态原文参考:${answerPath || '同目录 worker-answer.md'})后运行 sillyspec register-stage-review --change ${changeName} --stage ${rec.stage} --from <修整后的review.json> 回收\n② 确认平台确无结果才按通道优先序降级:\n${renderFallbackGuidance(cwd)}` }
|
|
355
416
|
}
|
|
356
417
|
clearDispatchRecord(specBase, changeName)
|
|
357
|
-
return { ok: true, state: 'completed', statusLine, reviewPath: p.reviewPath, verdict: `${review.specVerdict}/${review.qualityVerdict}` }
|
|
418
|
+
return { ok: true, state: 'completed', statusLine, reviewPath: p.reviewPath, verdict: `${review.specVerdict}/${review.qualityVerdict}`, nextStep: answerPath ? `终态原文已留存 ${answerPath}` : undefined }
|
|
358
419
|
}
|
|
359
420
|
if (st === 'failed' || st === 'killed') {
|
|
360
421
|
clearDispatchRecord(specBase, changeName)
|
|
@@ -375,6 +436,105 @@ export async function runReviewDispatch(opts = {}) {
|
|
|
375
436
|
}
|
|
376
437
|
}
|
|
377
438
|
|
|
439
|
+
// ════════ 薄道评审平台单调用派发(2026-10-23-review-inline-dispatch)════════
|
|
440
|
+
/**
|
|
441
|
+
* 薄道(thin flow)review 子步的平台单调用通道:flow done 评审缺席分支经 local.yaml
|
|
442
|
+
* review_inline_wait 键门控调用——薄道任务书(renderReviewerTaskbook 产物+平台产出方式
|
|
443
|
+
* 尾注,任务书生产权仍在 CLI)派发平台 readOnly worker,有界轮询至完成/超时,三通道
|
|
444
|
+
* 提取复用厚道 extractReviewFromArtifacts(JSON 对象提取与 schema 无关)+ 薄道
|
|
445
|
+
* validateReviewJson 同一源校验,通过才落 changeDir/review.json(reviewer=platform-inline
|
|
446
|
+
* 如实标注)——成功后调用方同调用继续登记子步,missing→PASS 往返消灭。
|
|
447
|
+
* 任何失败返回 {ok:false, reason}——调用方回退现状任务书面(零治理损失)。
|
|
448
|
+
* 不写 dispatch record(那是厚道跨命令异步形态;单调用在途态只随返回值与输出留痕)。
|
|
449
|
+
*/
|
|
450
|
+
export async function runThinReviewInline(opts = {}) {
|
|
451
|
+
const {
|
|
452
|
+
cwd = process.cwd(), specBase, change, changeDir, taskbookText,
|
|
453
|
+
waitMs = 60_000, pollIntervalMs = 5_000, budgetUsd = 1.0,
|
|
454
|
+
client = null, probe = null, log = null,
|
|
455
|
+
} = opts
|
|
456
|
+
const say = typeof log === 'function' ? log : () => {}
|
|
457
|
+
if (!specBase || !change || !changeDir || typeof taskbookText !== 'string' || !taskbookText.trim()) {
|
|
458
|
+
return { ok: false, reason: '缺少 specBase/change/changeDir/taskbookText' }
|
|
459
|
+
}
|
|
460
|
+
let cl = client
|
|
461
|
+
if (!cl) {
|
|
462
|
+
try {
|
|
463
|
+
const { SillyHubMcpClient } = await import('./sillyhub-mcp/client.js')
|
|
464
|
+
cl = new SillyHubMcpClient({ cwd })
|
|
465
|
+
} catch (e) {
|
|
466
|
+
return { ok: false, reason: `client 构建失败(${(e && e.message) || e})` }
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (!cl || typeof cl.createMission !== 'function' || typeof cl.dispatchWorker !== 'function'
|
|
470
|
+
|| typeof cl.listWorkers !== 'function' || typeof cl.getWorkerResult !== 'function') {
|
|
471
|
+
return { ok: false, reason: 'client 不具备平台派发接口(create/dispatch/list/result)' }
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const probeFn = probe || (await import('./dispatch/probe.js')).probeSillyHub
|
|
475
|
+
const pr = await probeFn({ client: cl, worktreePath: null, cwd })
|
|
476
|
+
if (!pr || pr.available !== true) {
|
|
477
|
+
return { ok: false, reason: `平台通道不可用(${pr && pr.reason ? pr.reason : 'probe 异常'})` }
|
|
478
|
+
}
|
|
479
|
+
const objective = `轻量变更独立评审(${change})——按任务书产出 review.json 结论`
|
|
480
|
+
const workerPrompt = taskbookText + '\n' + [
|
|
481
|
+
'',
|
|
482
|
+
'【平台通道产出方式(read_only 约束——覆盖上方「写到文件」指引)】',
|
|
483
|
+
'- 平台 worker 禁止写任何文件、禁止 git 操作:把完整 review.json 文本(上方 schema)作为产出返回。',
|
|
484
|
+
'- 优先放 artifacts(kind 含 review,content 字段放纯 JSON 文本);宿主不支持 artifacts 时放回答正文首个 ```json 代码块(CLI 回收链三通道兜底)。',
|
|
485
|
+
].join('\n')
|
|
486
|
+
const mission = await cl.createMission({ objective, orchestrationMode: 'external', budgetUsd })
|
|
487
|
+
const missionId = mission && mission.missionId
|
|
488
|
+
if (!missionId) return { ok: false, reason: 'create_mission 未返回 missionId' }
|
|
489
|
+
say(`🛰️ 平台评审已派发(mission ${String(missionId).slice(0, 8)})——有界等待 ${Math.round(waitMs / 1000)}s(review_inline_wait)`)
|
|
490
|
+
let worker = null
|
|
491
|
+
try {
|
|
492
|
+
worker = await cl.dispatchWorker({ missionId, objective, readOnly: true, worktreePath: cwd, workerPrompt })
|
|
493
|
+
} catch (e) {
|
|
494
|
+
return { ok: false, reason: `dispatch_worker 异常(${(e && e.message) || e})`, missionId }
|
|
495
|
+
}
|
|
496
|
+
const workerId = worker && worker.workerId
|
|
497
|
+
if (!workerId) return { ok: false, reason: 'dispatch_worker 未返回 workerId', missionId }
|
|
498
|
+
const deadline = Date.now() + Math.max(1000, waitMs)
|
|
499
|
+
let lastState = ''
|
|
500
|
+
while (Date.now() < deadline) {
|
|
501
|
+
await new Promise((r) => setTimeout(r, Math.max(200, Math.min(pollIntervalMs, deadline - Date.now()))))
|
|
502
|
+
let w = null
|
|
503
|
+
try {
|
|
504
|
+
const workers = await cl.listWorkers(missionId)
|
|
505
|
+
w = Array.isArray(workers) ? workers.find((x) => x && (x.id === workerId || x.worker_id === workerId)) : null
|
|
506
|
+
} catch { /* 单次轮询抖动按在途继续(下轮重试) */ }
|
|
507
|
+
const st = w ? String(w.status || '') : 'unknown'
|
|
508
|
+
if (st !== lastState) {
|
|
509
|
+
say(`🛰️ 平台评审状态:${st}(剩 ${Math.max(0, Math.ceil((deadline - Date.now()) / 1000))}s)`)
|
|
510
|
+
lastState = st
|
|
511
|
+
}
|
|
512
|
+
if (st === 'failed' || st === 'killed') return { ok: false, reason: `worker 终态 ${st}`, missionId, workerId }
|
|
513
|
+
if (st === 'completed') {
|
|
514
|
+
const gr = await cl.getWorkerResult({ missionId, workerId })
|
|
515
|
+
const extracted = extractReviewFromArtifacts(gr && gr.artifacts, gr && gr.answer)
|
|
516
|
+
if (!extracted || typeof extracted !== 'object') {
|
|
517
|
+
return { ok: false, reason: 'worker completed 但三通道提取不到 review JSON(artifacts kind/文本、answer 正文)——平台 UI 可回看 mission', missionId, workerId }
|
|
518
|
+
}
|
|
519
|
+
// reviewer 如实标注(覆盖 worker 任意填法);change 对齐本变更;其余字段产物原样不改写
|
|
520
|
+
const candidate = { ...extracted, change, reviewer: 'platform-inline' }
|
|
521
|
+
const reviewPath = join(changeDir, 'review.json')
|
|
522
|
+
writeAtomicSync(reviewPath, JSON.stringify(candidate, null, 2) + '\n')
|
|
523
|
+
const { validateReviewJson } = await import('./flow-review.js')
|
|
524
|
+
const v = validateReviewJson(reviewPath)
|
|
525
|
+
if (!v.ok) {
|
|
526
|
+
try { rmSync(reviewPath, { force: true }) } catch { /* 删件失败由下轮覆盖兜底 */ }
|
|
527
|
+
return { ok: false, reason: `提取的 review 不符薄道 schema(${v.errors.join(';')})——原文可参考平台 mission UI`, missionId, workerId }
|
|
528
|
+
}
|
|
529
|
+
return { ok: true, missionId, workerId, reviewPath }
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return { ok: false, reason: `等待 ${Math.round(waitMs / 1000)}s 未完成(在途 mission ${missionId} 可在平台 UI 回看;本轮回退任务书面)`, missionId, workerId }
|
|
533
|
+
} catch (e) {
|
|
534
|
+
return { ok: false, reason: `inline 派发异常(${(e && e.message) || String(e)})` }
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
378
538
|
// ════════ 影子期派发(2026-09-18-ceremony-risk-pricing task-06 / FR-04 / D-007@v1 / R-04 / R-06)════════
|
|
379
539
|
// 依据:design.md「总体方案 Phase 3」+「风险登记 R-04/R-06」+ plan.md「Wave 4」+ 全局硬约束 2。
|
|
380
540
|
// 定位:S0/S1 轻档变更在明面轻仪主线外,后台静默派发重仪式独立评审**只记账不阻断**——
|
|
@@ -774,7 +934,8 @@ export async function dispatchShadowReview(opts = {}) {
|
|
|
774
934
|
}
|
|
775
935
|
if (st === 'completed') {
|
|
776
936
|
const gr = await client.getWorkerResult({ missionId: rec.missionId, workerId: rec.workerId })
|
|
777
|
-
|
|
937
|
+
// 通道③同步受益(answer 正文——主线同款;影子 accounting-only 不加 salvage 交互面)
|
|
938
|
+
const review = extractReviewFromArtifacts(gr && gr.artifacts, gr && gr.answer)
|
|
778
939
|
if (!review) {
|
|
779
940
|
await mutateShadowLedger(runtimeRoot, (ledger) => {
|
|
780
941
|
const e = ledger.entries.find((x) => x && x.shadowRunId === rec.shadowRunId)
|
package/src/run/bg-sync.js
CHANGED
|
@@ -182,6 +182,10 @@ export async function spawnBackgroundSync(cwd, changeName, platformOpts = {}, op
|
|
|
182
182
|
env,
|
|
183
183
|
});
|
|
184
184
|
child.unref?.();
|
|
185
|
+
// 孵化稳定化(FR-08 / dsh 实证 libuv 退出断言):等 spawn 事件落定(≤150ms 兜底)再放行,
|
|
186
|
+
// 压掉「父进程在子进程句柄建立中途退出」的崩溃竞态窗口。
|
|
187
|
+
const { awaitSpawnSettled } = await import('../spawn-settle.js');
|
|
188
|
+
await awaitSpawnSettled(child);
|
|
185
189
|
} finally {
|
|
186
190
|
if (logFd !== null) {
|
|
187
191
|
try { closeSync(logFd); } catch { /* 父侧 fd 副本关闭;子进程持有继承副本 */ }
|
package/src/run/code-face-key.js
CHANGED
|
@@ -24,7 +24,10 @@ import { createHash } from 'node:crypto';
|
|
|
24
24
|
|
|
25
25
|
function safeGit(cwd, args) {
|
|
26
26
|
try {
|
|
27
|
-
|
|
27
|
+
// maxBuffer:execFileSync 默认 1MB,ls-tree -r -z 大仓输出必超(multi-agent-platform 1.95MB /
|
|
28
|
+
// 本仓 1.76MB,dsh 0ff6f1c8 取证)——ENOBUFS 被 catch 吞成 null 曾让指纹/dedupKey/passedKey
|
|
29
|
+
// 全链失效、verify 同码态全量实测 6×10~13.5min。256MB 对齐 git-helper GIT_MAX_BUFFER 口径。
|
|
30
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60000, maxBuffer: 256 * 1024 * 1024, windowsHide: true });
|
|
28
31
|
} catch {
|
|
29
32
|
return null;
|
|
30
33
|
}
|