sillyspec 3.28.3 → 3.28.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-auto/SKILL.md +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
- package/.claude/skills/sillyspec-state/SKILL.md +25 -25
- package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
- package/AGENTS.md +25 -0
- package/INSTRUCTIONS.md +12 -0
- package/package.json +1 -1
- package/results-review-tier.md +18 -0
- package/src/agent-session-log.js +4 -3
- package/src/archive-delta.js +82 -0
- package/src/change-delete.js +2 -1
- package/src/change-list.js +18 -3
- package/src/change-risk-profile.js +77 -6
- package/src/commit-suggest.js +52 -1
- package/src/config-cat.js +17 -1
- package/src/constants.js +3 -1
- package/src/decision-distill.js +45 -5
- package/src/decisions-io.js +96 -0
- package/src/design-facts.js +3 -0
- package/src/dispatch/probe.js +31 -8
- package/src/docs-check.js +102 -3
- package/src/doctor-diagnostics.js +441 -5
- package/src/foreign-declared.js +53 -2
- package/src/index.js +606 -22
- package/src/mcp-server.js +144 -0
- package/src/module-impact.js +84 -12
- package/src/modules.js +46 -34
- package/src/plan-adopt-waves.js +13 -5
- package/src/progress/change-registry.js +34 -0
- package/src/progress.js +75 -2
- package/src/quick-recommend.js +6 -1
- package/src/quicklog.js +70 -8
- package/src/review-dispatch.js +377 -0
- package/src/run/archive-distill.js +48 -0
- package/src/run/command.js +48 -13
- package/src/run/complete-handlers.js +215 -9
- package/src/run/complete.js +184 -2
- package/src/run/gate-snapshot.js +88 -0
- package/src/run/gates.js +206 -58
- package/src/run/prompt.js +150 -14
- package/src/run/quick-audit.js +38 -13
- package/src/run/shared.js +183 -12
- package/src/run/stage.js +52 -2
- package/src/run/verify-quality-scan.js +336 -0
- package/src/runtime-hygiene.js +84 -0
- package/src/scope-audit.js +735 -0
- package/src/sillyhub-mcp/client.js +58 -3
- package/src/spec-sync.js +63 -8
- package/src/stage-contract.js +83 -3
- package/src/stage-review-checklist.js +44 -0
- package/src/stage-review.js +125 -4
- package/src/stages/archive.js +60 -167
- package/src/stages/brainstorm.js +7 -17
- package/src/stages/doctor.js +41 -515
- package/src/stages/execute.js +13 -10
- package/src/stages/plan-postcheck.js +191 -7
- package/src/stages/plan.js +7 -35
- package/src/stages/quick.js +11 -11
- package/src/stages/verify.js +24 -39
- package/src/sync-noise.js +158 -0
- package/src/sync.js +93 -27
- package/src/task-review.js +133 -17
- package/src/taskcard.js +46 -3
- package/src/validate-artifacts.js +163 -0
- package/src/verify-facts-schema.js +141 -0
- package/src/verify-postcheck.js +347 -29
- package/src/verify-probes.js +275 -24
- package/src/workflow.js +6 -0
- package/src/worktree-apply.js +167 -24
- package/src/worktree.js +5 -1
- package/templates/agents-instruction.md +1 -0
package/src/quicklog.js
CHANGED
|
@@ -155,6 +155,39 @@ function extractRawBlock(content, qlId) {
|
|
|
155
155
|
|
|
156
156
|
// 落盘条目块 → 推送 payload(design §5.3:以落盘终态为准,不从入参拼)。
|
|
157
157
|
// 宽松标签解析对齐平台:全半角冒号、多条状态行取最后、文件行单行/多行 bullet。
|
|
158
|
+
/**
|
|
159
|
+
* QUICKLOG 结构化 sidecar(P1-4,noai-ir-roadmap §4):completeQuicklogEntry 组装 payload 的
|
|
160
|
+
* 同时把终态结构落 .runtime/quicklog-sidecar/<qlId>.json——一次解析持久化,daemon/查询/重推
|
|
161
|
+
* 直读结构化数据不必再解析 md(md 只给人看)。fail-soft:sidecar 是加速层不是正确性依赖,
|
|
162
|
+
* 落盘失败零阻断(读取侧自动回退 md 解析路径)。
|
|
163
|
+
*/
|
|
164
|
+
const QUICKLOG_SIDECAR_SCHEMA_VERSION = 1
|
|
165
|
+
|
|
166
|
+
export function quicklogSidecarPath(specBase, qlId) {
|
|
167
|
+
return join(specBase, '.runtime', 'quicklog-sidecar', `${qlId}.json`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function writeQuicklogSidecar(specBase, payload) {
|
|
171
|
+
if (!payload || !payload.ql_id) return false
|
|
172
|
+
try {
|
|
173
|
+
const p = quicklogSidecarPath(specBase, payload.ql_id)
|
|
174
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
175
|
+
writeAtomic(p, JSON.stringify({ schemaVersion: QUICKLOG_SIDECAR_SCHEMA_VERSION, ...payload }, null, 2) + String.fromCharCode(10))
|
|
176
|
+
return true
|
|
177
|
+
} catch {
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function readQuicklogSidecar(specBase, qlId) {
|
|
183
|
+
try {
|
|
184
|
+
const rec = JSON.parse(readFileSync(quicklogSidecarPath(specBase, qlId), 'utf8'))
|
|
185
|
+
return rec && rec.schemaVersion === QUICKLOG_SIDECAR_SCHEMA_VERSION ? rec : null
|
|
186
|
+
} catch {
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
158
191
|
function buildPushPayloadFromRaw(rawBlock, { ql_id, author_raw, status, linked_changes, fallback_title }) {
|
|
159
192
|
const payload = {
|
|
160
193
|
ql_id,
|
|
@@ -294,9 +327,17 @@ export function extractTitleFromResult(result) {
|
|
|
294
327
|
if (!result) return ''
|
|
295
328
|
const m = String(result).match(/需求:([^\n\r]*?)(?:\s+根因:|$)/)
|
|
296
329
|
if (!m) return ''
|
|
297
|
-
|
|
330
|
+
// 不再截到首个标点(2026-09-08 用户反馈②):旧口径迫使 agent 刻意避开标点写标题,
|
|
331
|
+
// 「A,B」被截成「A」反而降低可读性。--req 的模板指引本就要求一句话短标题,
|
|
332
|
+
// 标点由写者自主掌控;CLI 只兜底超长截断。
|
|
333
|
+
let t = m[1].trim()
|
|
298
334
|
if (!t) return ''
|
|
299
|
-
if (t.length > 80)
|
|
335
|
+
if (t.length > 80) {
|
|
336
|
+
// 超长截断优先在标点/空格处断(标题残句可读),找不到就近边界才硬截。
|
|
337
|
+
const cut = t.slice(0, 80)
|
|
338
|
+
const m2 = cut.match(/^[\s\S]*[,。;:,;:]\s*/)
|
|
339
|
+
t = (m2 && m2[0].length >= 40 ? m2[0] : cut).replace(/[,。;:,;:\s]+$/, '') + '…'
|
|
340
|
+
}
|
|
300
341
|
return t
|
|
301
342
|
}
|
|
302
343
|
|
|
@@ -560,7 +601,7 @@ function splitSingleLineFields(body) {
|
|
|
560
601
|
return scanFields(body, findBoundaryLabel) ?? scanFields(body, (b, l, f) => b.indexOf(l, f))
|
|
561
602
|
}
|
|
562
603
|
|
|
563
|
-
function flipEntryInContent(content, qlId, result, changedFiles = [], fileNotes = [], auditNotes = []) {
|
|
604
|
+
function flipEntryInContent(content, qlId, result, changedFiles = [], fileNotes = [], auditNotes = [], softFiles = []) {
|
|
564
605
|
const lines = content.split('\n')
|
|
565
606
|
const startIdx = lines.findIndex(l => l.startsWith(`## ${qlId} |`))
|
|
566
607
|
if (startIdx === -1) return null
|
|
@@ -583,9 +624,19 @@ function flipEntryInContent(content, qlId, result, changedFiles = [], fileNotes
|
|
|
583
624
|
// 都空则不动(保持「(见实际改动)」)。bullet 用「数组元素内嵌 \n」写——lines.join('\n') 展平为
|
|
584
625
|
// 多行,数组长度不变 → 与下方状态/结果 splice 的索引完全解耦(changedFiles 单行同理原地替换)。
|
|
585
626
|
if (lines[i].startsWith('文件:')) {
|
|
627
|
+
// 软归属(2026-09-10 用户反馈):softFiles = 审计判定的「窗口内未声明同模块测试文件」——
|
|
628
|
+
// 补入文件行 bullet 带括注标记,消每回手工核对;fileNotes 已显式括注的不重复加。
|
|
629
|
+
// fileNotes 空但有 softFiles 时同样升级 bullet(单行格式放不下括注标记)。
|
|
630
|
+
const notedPaths = new Set(fileNotes.map((n) => n.path))
|
|
631
|
+
const softBullets = []
|
|
632
|
+
for (const f of softFiles) {
|
|
633
|
+
if (!notedPaths.has(f)) softBullets.push(`- ${f}(软归属·同模块测试,未声明)`)
|
|
634
|
+
}
|
|
586
635
|
if (fileNotes.length > 0) {
|
|
587
|
-
const bullets = fileNotes.map((n) => `- ${n.path}${n.note ? `(${n.note})` : ''}`)
|
|
636
|
+
const bullets = [...fileNotes.map((n) => `- ${n.path}${n.note ? `(${n.note})` : ''}`), ...softBullets]
|
|
588
637
|
lines[i] = `文件:\n${bullets.join('\n')}`
|
|
638
|
+
} else if (softBullets.length > 0) {
|
|
639
|
+
lines[i] = `文件:\n${[...changedFiles.map((f) => `- ${f}`), ...softBullets].join('\n')}`
|
|
589
640
|
} else if (changedFiles.length > 0) {
|
|
590
641
|
lines[i] = `文件:${changedFiles.join(', ')}`
|
|
591
642
|
}
|
|
@@ -767,7 +818,7 @@ export async function appendQuicklogEntryWithId(specBase, gitUser, qlId, { descr
|
|
|
767
818
|
/**
|
|
768
819
|
* 翻某 qlId 条目为「已完成」+ 追加结果 + 勾选关联 tasks.md。持锁。
|
|
769
820
|
*/
|
|
770
|
-
export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultText = '', linkedChanges = [], changedFiles = [], auditNotes = [] } = {}) {
|
|
821
|
+
export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultText = '', linkedChanges = [], changedFiles = [], auditNotes = [], softFiles = [] } = {}) {
|
|
771
822
|
const quicklogDir = join(specBase, 'quicklog')
|
|
772
823
|
// 与 allocateQuicklogEntry 同源消毒:锁文件路径含 user(防穿越写,两入口必须一致否则锁不上同一文件)
|
|
773
824
|
const user = sanitizeQuicklogUser(gitUser) || 'unknown'
|
|
@@ -780,6 +831,11 @@ export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultTex
|
|
|
780
831
|
// 读后即清(per-process,防残留跨调用)。flipEntryInContent 优先用 fileNotes,空则回退 realFiles。
|
|
781
832
|
const fileNotes = parseFileNotes(_pendingFileNotes)
|
|
782
833
|
_pendingFileNotes = ''
|
|
834
|
+
// 软归属文件(complete-handlers 传入,已过 isQuickMetadata;反斜杠归一与 fileNotes 同口径):
|
|
835
|
+
// 窗口内未声明同模块测试文件,flipEntryInContent 补入文件行 bullet 带「软归属」括注。
|
|
836
|
+
const soft = Array.isArray(softFiles)
|
|
837
|
+
? softFiles.filter(Boolean).map((f) => String(f).replace(/\\/g, '/'))
|
|
838
|
+
: []
|
|
783
839
|
|
|
784
840
|
await withFileLock(lockPath, async () => {
|
|
785
841
|
// 条目可能在主文件或轮转归档中
|
|
@@ -790,7 +846,7 @@ export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultTex
|
|
|
790
846
|
let content = ''
|
|
791
847
|
try { content = readFileSync(filePath, 'utf8') } catch { continue }
|
|
792
848
|
const updated = flipEntryInContent(content, qlId, result, realFiles, fileNotes,
|
|
793
|
-
Array.isArray(auditNotes) ? auditNotes.filter((n) => typeof n === 'string' && n.trim() !== '') : [])
|
|
849
|
+
Array.isArray(auditNotes) ? auditNotes.filter((n) => typeof n === 'string' && n.trim() !== '') : [], soft)
|
|
794
850
|
if (updated !== null) {
|
|
795
851
|
await writeAtomic(filePath, updated) // 命中处原子落盘(只改含目标条目的那一个文件)
|
|
796
852
|
updatedContent = updated
|
|
@@ -804,12 +860,15 @@ export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultTex
|
|
|
804
860
|
// (design §5.3:翻完成时标题行被 extractTitleFromResult 刷新,推送须与落盘一致,
|
|
805
861
|
// 不能用入参拼——标题/结果块/文件行都会在 flipEntryInContent 中重写)。
|
|
806
862
|
const rawBlock = extractRawBlock(updatedContent, qlId)
|
|
807
|
-
|
|
863
|
+
const finalPayload = buildPushPayloadFromRaw(rawBlock, {
|
|
808
864
|
ql_id: qlId,
|
|
809
865
|
author_raw: user,
|
|
810
866
|
status: 'completed',
|
|
811
867
|
linked_changes: linked,
|
|
812
|
-
})
|
|
868
|
+
})
|
|
869
|
+
// P1-4:终态结构 sidecar 持久化(同一解析器产出,md/sidecar 恒一致;fail-soft)
|
|
870
|
+
writeQuicklogSidecar(specBase, finalPayload)
|
|
871
|
+
await pushQuicklogEntryToPlatform(specBase, finalPayload).catch(() => {})
|
|
813
872
|
})
|
|
814
873
|
}
|
|
815
874
|
|
|
@@ -887,6 +946,9 @@ export async function cancelQuickSession({ specBase, gitUser, qlId, sessionId =
|
|
|
887
946
|
// 会产出 \r\r\n 字节级污染(git diff 全文件红);原子写防 agent/dashboard 轮询读半截
|
|
888
947
|
const eol = content.includes('\r\n') ? '\r\n' : '\n'
|
|
889
948
|
await writeAtomic(p, lines.join(eol))
|
|
949
|
+
// P1-4:sidecar 同步翻「已取消」(无 sidecar 的存量条目跳过——md 仍是真相源)
|
|
950
|
+
const _sc = readQuicklogSidecar(specBase, qlId)
|
|
951
|
+
if (_sc) writeQuicklogSidecar(specBase, { ..._sc, status: '已取消' })
|
|
890
952
|
}
|
|
891
953
|
break
|
|
892
954
|
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-dispatch — tier=independent 独立审查的平台派发核心(2026-09-10-review-dispatch task-03 / FR-01~04 / D-001~003@1)
|
|
3
|
+
*
|
|
4
|
+
* 定位:纯逻辑核心 + 注入式编排。MCP 调用全部经注入的 client(SillyHubMcpClient 实例,
|
|
5
|
+
* probe 同款依赖注入风格——本模块不 import client.js,测试零网络);probe 经参数注入
|
|
6
|
+
* (默认真 probeSillyHub,测试传 stub)。
|
|
7
|
+
*
|
|
8
|
+
* 三链(runReviewDispatch 总入口,task-04 薄壳消费):
|
|
9
|
+
* create —— probe 三层前置 → createMission(external) → dispatchWorker(read_only)
|
|
10
|
+
* → 在途记录(O_EXCL 防双开)→ 立即返回(异步,D-002)
|
|
11
|
+
* status —— listWorkers 状态迁移 → detectStall(queued 不计时,D-003)
|
|
12
|
+
* → 终态回收(getWorkerResult → artifacts 提取 → persistStageReview 落盘
|
|
13
|
+
* 既有 stage-reviews 路径 + reviewer.channel=platform 落款)
|
|
14
|
+
* kill —— 清本地在途记录 + 平台处置指引(不自动 kill 任何东西,D-003)
|
|
15
|
+
*
|
|
16
|
+
* 失败全链路有出口(design 风险登记):probe 不可用/网络异常 → {ok:false, reason,
|
|
17
|
+
* guidance}(guidance 按 channel_priority 去 platform 剩余序渲染),绝不抛穿。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
|
|
21
|
+
import { join } from 'node:path'
|
|
22
|
+
import jsYaml from 'js-yaml'
|
|
23
|
+
import { writeAtomicSync } from './fs-atomic.js'
|
|
24
|
+
import { REVIEW_CHECKLISTS } from './stage-review-checklist.js'
|
|
25
|
+
import {
|
|
26
|
+
renderReviewJsonContract,
|
|
27
|
+
validateStageReviewSchema,
|
|
28
|
+
computeDocHash,
|
|
29
|
+
readReviewChannelPriority,
|
|
30
|
+
} from './stage-review.js'
|
|
31
|
+
|
|
32
|
+
// stage → 主审查文档(与 stage-review.js STAGE_MAIN_DOC 同映射;该常量未导出——本地声明并
|
|
33
|
+
// 与 design.md 模块设计对齐,persistStageReview 消费方算 docHash 用)
|
|
34
|
+
const STAGE_MAIN_DOC = { brainstorm: 'design.md', plan: 'plan.md', execute: 'design.md' }
|
|
35
|
+
|
|
36
|
+
/** 在途记录状态机(design 生命周期契约表):五态,终态即清记录(无 terminalAt 字段)。 */
|
|
37
|
+
const DISPATCH_STATES = ['dispatching', 'in-flight', 'completed', 'failed', 'abandoned']
|
|
38
|
+
|
|
39
|
+
/** 在途记录路径:{SPEC_ROOT}/.runtime/review-dispatch-<change>.json */
|
|
40
|
+
function dispatchRecordPath(specBase, changeName) {
|
|
41
|
+
return join(specBase, '.runtime', `review-dispatch-${changeName}.json`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 读在途记录(无/坏 → null,fail-open 调用方按无在途处理)。 */
|
|
45
|
+
export function readDispatchRecord(specBase, changeName) {
|
|
46
|
+
try {
|
|
47
|
+
const p = dispatchRecordPath(specBase, changeName)
|
|
48
|
+
if (!existsSync(p)) return null
|
|
49
|
+
const r = JSON.parse(readFileSync(p, 'utf8'))
|
|
50
|
+
return r && typeof r === 'object' && typeof r.missionId === 'string' ? r : null
|
|
51
|
+
} catch { return null }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 创建在途记录(O_EXCL 语义:已存在即拒绝——防双 CLI 并发双开,D-002 附带契约)。
|
|
56
|
+
* @returns {{created: boolean, existing?: object}} created=false 时带 existing 供调用方给指引。
|
|
57
|
+
*/
|
|
58
|
+
export function createDispatchRecord(specBase, changeName, record) {
|
|
59
|
+
const p = dispatchRecordPath(specBase, changeName)
|
|
60
|
+
mkdirSync(join(specBase, '.runtime'), { recursive: true })
|
|
61
|
+
try {
|
|
62
|
+
writeFileSync(p, JSON.stringify(record, null, 2) + '\n', { flag: 'wx' })
|
|
63
|
+
return { created: true }
|
|
64
|
+
} catch (e) {
|
|
65
|
+
if (e && (e.code === 'EEXIST' || e.code === 'EPERM')) {
|
|
66
|
+
// Windows 上 wx 对已存在文件可能抛 EPERM——两者都按「已存在」处理
|
|
67
|
+
return { created: false, existing: readDispatchRecord(specBase, changeName) }
|
|
68
|
+
}
|
|
69
|
+
throw e
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 更新在途记录(原子替换;无记录时 no-op 返回 false)。 */
|
|
74
|
+
function updateDispatchRecord(specBase, changeName, patch) {
|
|
75
|
+
const cur = readDispatchRecord(specBase, changeName)
|
|
76
|
+
if (!cur) return false
|
|
77
|
+
writeAtomicSync(dispatchRecordPath(specBase, changeName), JSON.stringify({ ...cur, ...patch }, null, 2) + '\n')
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 清在途记录(终态回收 / kill / 失败后调用;设计口径「终态即清」——删除文件不留 tombstone,
|
|
82
|
+
* review.json(completed)与命令输出(missionId)是持久真相)。不存在 no-op。 */
|
|
83
|
+
export function clearDispatchRecord(specBase, changeName) {
|
|
84
|
+
const p = dispatchRecordPath(specBase, changeName)
|
|
85
|
+
try { if (existsSync(p)) { rmSync(p); return true } return false } catch { return false }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 停滞判定(D-003):queued 不计时(排队慢是正常形态);running 后 lastStateAt 超窗 → stalled。
|
|
90
|
+
* @returns {{stalled: boolean, queuedWait: boolean, hint: string}}
|
|
91
|
+
*/
|
|
92
|
+
export function detectStall(record, nowMs, stallMs) {
|
|
93
|
+
const lastState = record && typeof record.lastState === 'string' ? record.lastState : ''
|
|
94
|
+
const lastAt = record && typeof record.lastStateAt === 'number' ? record.lastStateAt : null
|
|
95
|
+
if (lastState === 'queued' || lastState === 'pending' || lastAt === null) {
|
|
96
|
+
return { stalled: false, queuedWait: lastState === 'queued' || lastState === 'pending', hint: '' }
|
|
97
|
+
}
|
|
98
|
+
const stalled = nowMs - lastAt > stallMs
|
|
99
|
+
return {
|
|
100
|
+
stalled,
|
|
101
|
+
queuedWait: false,
|
|
102
|
+
hint: stalled
|
|
103
|
+
? `疑似停滞(${lastState} 超 ${Math.round(stallMs / 60000)} 分钟无状态迁移)。三选项:①继续等(慢模型/长任务属正常)②sillyspec review-dispatch --kill 显式处置 ③按通道优先序降级(宿主子代理/降级自审)`
|
|
104
|
+
: '',
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 从 artifacts 提取 review JSON(双通道容错,design 风险登记「平台 artifacts 形态演进」):
|
|
110
|
+
* ① kind 含 review/review_json 的 artifact,content/content_ref/text 字段 JSON.parse 优先;
|
|
111
|
+
* ② 兜底任一 artifact 文本字段提取首个平衡 {} 对象。全失败 → null(调用方给人工 get_worker_result 指引)。
|
|
112
|
+
*/
|
|
113
|
+
export function extractReviewFromArtifacts(artifacts) {
|
|
114
|
+
if (!Array.isArray(artifacts)) return null
|
|
115
|
+
const texts = []
|
|
116
|
+
for (const a of artifacts) {
|
|
117
|
+
if (!a || typeof a !== 'object') continue
|
|
118
|
+
const kind = String(a.kind || '')
|
|
119
|
+
const text = typeof a.content === 'string' ? a.content
|
|
120
|
+
: typeof a.content_ref === 'string' ? a.content_ref
|
|
121
|
+
: typeof a.text === 'string' ? a.text : null
|
|
122
|
+
if (text === null) continue
|
|
123
|
+
if (/review/i.test(kind)) {
|
|
124
|
+
try {
|
|
125
|
+
const obj = JSON.parse(text)
|
|
126
|
+
if (obj && typeof obj === 'object') return obj
|
|
127
|
+
} catch { /* 该条不是合法 JSON,落兜底通道 */ }
|
|
128
|
+
}
|
|
129
|
+
texts.push(text)
|
|
130
|
+
}
|
|
131
|
+
for (const t of texts) {
|
|
132
|
+
const start = t.indexOf('{')
|
|
133
|
+
if (start === -1) continue
|
|
134
|
+
// 首个平衡对象:括号配对扫描(字符串字面量内的 } 不计数)
|
|
135
|
+
let depth = 0, inStr = false, esc = false
|
|
136
|
+
for (let i = start; i < t.length; i++) {
|
|
137
|
+
const ch = t[i]
|
|
138
|
+
if (esc) { esc = false; continue }
|
|
139
|
+
if (ch === '\\') { esc = true; continue }
|
|
140
|
+
if (ch === '"') { inStr = !inStr; continue }
|
|
141
|
+
if (inStr) continue
|
|
142
|
+
if (ch === '{') depth++
|
|
143
|
+
else if (ch === '}') {
|
|
144
|
+
depth--
|
|
145
|
+
if (depth === 0) {
|
|
146
|
+
try {
|
|
147
|
+
const obj = JSON.parse(t.slice(start, i + 1))
|
|
148
|
+
if (obj && typeof obj === 'object') return obj
|
|
149
|
+
} catch { break }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 组装 reviewer 任务书(FR-01 派发质量的核心):四要素——①单源清单(前缀按 checklist
|
|
159
|
+
* docblock 规则)②review.json 契约(与 gate 同源)③期望产出方式(read_only worker:
|
|
160
|
+
* 结论经 artifacts 返回,**不写任何文件**——落盘归 CLI)④铁律段(禁改文件/禁 git commit)。
|
|
161
|
+
*/
|
|
162
|
+
export function buildReviewerTaskBook({ stage, changeDir, reviewRunId, channelPriority }) {
|
|
163
|
+
const mainDoc = STAGE_MAIN_DOC[stage] || 'design.md'
|
|
164
|
+
const items = (REVIEW_CHECKLISTS[stage] || []).map((t, i) => `${i + 1}. ${t}`).join('\n')
|
|
165
|
+
const contract = renderReviewJsonContract({ stage, changeDir, reviewRunId, tier: 'independent', channelPriority })
|
|
166
|
+
const workerPrompt = [
|
|
167
|
+
`你是独立审查者(platform 通道派发)。审查对象:${changeDir ? join(changeDir, mainDoc) : mainDoc}`,
|
|
168
|
+
'',
|
|
169
|
+
'## 审查清单(逐条给 pass/gap/fail + 证据锚点)',
|
|
170
|
+
items,
|
|
171
|
+
'',
|
|
172
|
+
contract,
|
|
173
|
+
'',
|
|
174
|
+
'## 产出方式(read_only 约束)',
|
|
175
|
+
'- **禁止修改任何文件、禁止 git commit、禁止写盘**——你只读文档与必要的源码佐证。',
|
|
176
|
+
'- 把**完整 review.json 文本**(按上方契约 schema,含 reviewer: {"channel":"platform"})作为产出返回(artifacts 里的 content 字段放纯 JSON 文本)。',
|
|
177
|
+
'- docHash 可占位(调度方 CLI 会按主文档机械重算),其余字段照实填写。',
|
|
178
|
+
].join('\n')
|
|
179
|
+
return {
|
|
180
|
+
objective: `独立审查 ${mainDoc}(stage=${stage},清单 ${items.split('\n').length} 条)并产出 review.json 结论`,
|
|
181
|
+
workerPrompt,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 落盘 stage review(FR-04 终态回收):reviewer 落款补 channel=platform + missionId;
|
|
187
|
+
* docHash 机械重算(worker 只读无盘,占位 hash 由真值替换——与 gate auto-refresh 同哲学);
|
|
188
|
+
* schema 校验不过 → 返回错误(不落盘坏 review)。
|
|
189
|
+
* @returns {{ok: boolean, reviewPath?: string, errors?: string[]}}
|
|
190
|
+
*/
|
|
191
|
+
export function persistStageReview({ runtimeRoot, stage, changeName, reviewRunId, reviewObj, missionId, mainDocPath }) {
|
|
192
|
+
const review = {
|
|
193
|
+
...reviewObj,
|
|
194
|
+
reviewer: { channel: 'platform', missionId: missionId || null, model: (reviewObj && reviewObj.reviewer && reviewObj.reviewer.model) || null },
|
|
195
|
+
}
|
|
196
|
+
if (mainDocPath && existsSync(mainDocPath)) {
|
|
197
|
+
const h = computeDocHash(mainDocPath)
|
|
198
|
+
if (h) review.docHash = h
|
|
199
|
+
}
|
|
200
|
+
const schema = validateStageReviewSchema(review)
|
|
201
|
+
if (!schema.ok) return { ok: false, errors: schema.errors }
|
|
202
|
+
review.reviewedFiles = Array.isArray(review.reviewedFiles) && review.reviewedFiles.length > 0
|
|
203
|
+
? review.reviewedFiles
|
|
204
|
+
: [`changes/${changeName}/${STAGE_MAIN_DOC[stage] || 'design.md'}`]
|
|
205
|
+
const dir = join(runtimeRoot, 'stage-reviews', `${stage}-${reviewRunId}`)
|
|
206
|
+
mkdirSync(dir, { recursive: true })
|
|
207
|
+
const reviewPath = join(dir, 'review.json')
|
|
208
|
+
writeAtomicSync(reviewPath, JSON.stringify(review, null, 2) + '\n')
|
|
209
|
+
return { ok: true, reviewPath }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 降级指引(channel_priority 去 platform 的剩余序渲染;probe 失败 / worker 失败共用)。 */
|
|
213
|
+
export function renderFallbackGuidance(cwd) {
|
|
214
|
+
const rest = readReviewChannelPriority(cwd).filter((c) => c !== 'platform')
|
|
215
|
+
const DESC = {
|
|
216
|
+
'agent-tool': '宿主 Agent tool 派独立子代理(subagent_type: general)',
|
|
217
|
+
'host-mcp': '宿主自有 MCP 派独立执行者',
|
|
218
|
+
'self': '降级自审:reviewerNotes 首行记「降级:环境无子代理可用」+ reviewer.channel="self"(gate ⚠️ 留痕)',
|
|
219
|
+
}
|
|
220
|
+
return rest.map((c, i) => `${i + 1}. ${c} —— ${DESC[c] || c}`).join('\n')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 总入口(task-04 薄壳消费契约):三链编排。
|
|
225
|
+
* @param {object} opts
|
|
226
|
+
* mode: 'create'|'status'|'kill';cwd/specBase/changeName 必填;stage(create 必填);
|
|
227
|
+
* client: SillyHubMcpClient 实例(create/status 用);probe: 可注入(默认真 probeSillyHub);
|
|
228
|
+
* budgetUsd(默认 1.0);stallMs(默认 900000);reviewRunId(status 回收时缺省读 marker)。
|
|
229
|
+
* @returns {Promise<{ok: boolean, ...}>} 永不抛——所有异常转 {ok:false, reason}
|
|
230
|
+
*/
|
|
231
|
+
export async function runReviewDispatch(opts = {}) {
|
|
232
|
+
const {
|
|
233
|
+
mode = 'create', cwd = process.cwd(), specBase, changeName,
|
|
234
|
+
stage, client, probe, budgetUsd = 1.0, stallMs = 900_000, reviewRunId,
|
|
235
|
+
} = opts
|
|
236
|
+
if (!specBase || !changeName) return { ok: false, reason: '缺少 specBase/changeName' }
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
if (mode === 'kill') {
|
|
240
|
+
const rec = readDispatchRecord(specBase, changeName)
|
|
241
|
+
clearDispatchRecord(specBase, changeName)
|
|
242
|
+
return {
|
|
243
|
+
ok: true,
|
|
244
|
+
killedLocally: true,
|
|
245
|
+
platformHint: rec
|
|
246
|
+
? `本地在途记录已清。平台侧 mission ${rec.missionId} 的 lease 请在平台 UI 处置(或等 daemon 自然回收)——本命令不自动 kill(D-003 处置权在人)。后续走通道降级:\n${renderFallbackGuidance(cwd)}`
|
|
247
|
+
: '无在途记录(已清或从未派发)。',
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (mode === 'create') {
|
|
252
|
+
if (!stage || !STAGE_MAIN_DOC[stage]) return { ok: false, reason: `stage 必填 ∈ ${Object.keys(STAGE_MAIN_DOC).join('|')}` }
|
|
253
|
+
// 幂等闸:已有 in-flight/dispatching 记录拒双开
|
|
254
|
+
const cur = readDispatchRecord(specBase, changeName)
|
|
255
|
+
if (cur && (cur.state === 'in-flight' || cur.state === 'dispatching')) {
|
|
256
|
+
return { ok: false, reason: `已有在途派发(mission ${cur.missionId},state ${cur.state})`, existing: cur, guidance: '先 sillyspec review-dispatch --status 轮询;或 --kill 显式放弃后重派。' }
|
|
257
|
+
}
|
|
258
|
+
if (!client || !client.probeDaemon) {
|
|
259
|
+
return { ok: false, reason: 'client 未注入(SillyHubMcpClient 实例)' }
|
|
260
|
+
}
|
|
261
|
+
// probe 三层前置(no-config / daemon-unreachable / daemon-offline 任一 → 非零退 + 降级指引)
|
|
262
|
+
const probeFn = probe || (await import('./dispatch/probe.js')).probeSillyHub
|
|
263
|
+
const pr = await probeFn({ client, worktreePath: null, cwd })
|
|
264
|
+
if (!pr || pr.available !== true) {
|
|
265
|
+
return { ok: false, reason: `平台通道不可用(${pr && pr.reason ? pr.reason : 'probe 异常'})`, guidance: renderFallbackGuidance(cwd) }
|
|
266
|
+
}
|
|
267
|
+
// reviewRunId:优先复用 marker(与 prompt/gate 同链,勿手算)
|
|
268
|
+
let runId = reviewRunId
|
|
269
|
+
if (!runId) {
|
|
270
|
+
const { getLatestStageReviewRunId, generateStageReviewRunId, stageReviewMarkerPath } = await import('./stage-review.js')
|
|
271
|
+
const runtimeRoot = join(specBase, '.runtime')
|
|
272
|
+
runId = getLatestStageReviewRunId(runtimeRoot, stage, changeName) || generateStageReviewRunId()
|
|
273
|
+
try {
|
|
274
|
+
mkdirSync(runtimeRoot, { recursive: true })
|
|
275
|
+
writeAtomicSync(stageReviewMarkerPath(runtimeRoot, stage, changeName), runId + '\n')
|
|
276
|
+
} catch { /* marker 写失败不阻断派发(status 回收时再补) */ }
|
|
277
|
+
}
|
|
278
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
279
|
+
const book = buildReviewerTaskBook({ stage, changeDir, reviewRunId: runId, channelPriority: readReviewChannelPriority(cwd) })
|
|
280
|
+
const mission = await client.createMission({ objective: book.objective, orchestrationMode: 'external', budgetUsd })
|
|
281
|
+
const missionId = mission && mission.missionId
|
|
282
|
+
if (!missionId) return { ok: false, reason: 'create_mission 未返回 missionId', guidance: renderFallbackGuidance(cwd) }
|
|
283
|
+
const cr = createDispatchRecord(specBase, changeName, {
|
|
284
|
+
change: changeName, stage, missionId, workerId: null, state: 'dispatching',
|
|
285
|
+
createdAt: new Date().toISOString(), lastState: 'dispatching', lastStateAt: Date.now(), reviewRunId: runId,
|
|
286
|
+
})
|
|
287
|
+
if (!cr.created) {
|
|
288
|
+
return { ok: false, reason: '并发双开被拒(O_EXCL)', existing: cr.existing, guidance: '先 --status 或 --kill。' }
|
|
289
|
+
}
|
|
290
|
+
let worker = null
|
|
291
|
+
try {
|
|
292
|
+
worker = await client.dispatchWorker({
|
|
293
|
+
missionId, objective: book.objective, readOnly: true,
|
|
294
|
+
worktreePath: cwd, workerPrompt: book.workerPrompt,
|
|
295
|
+
})
|
|
296
|
+
} catch (e) {
|
|
297
|
+
updateDispatchRecord(specBase, changeName, { state: 'abandoned', errorCode: 'dispatch_worker_threw' })
|
|
298
|
+
return { ok: false, reason: `dispatch_worker 异常: ${e && e.message}`, guidance: '记录已转 abandoned——可重派;平台侧 mission 请在 UI 核对。' }
|
|
299
|
+
}
|
|
300
|
+
const workerId = worker && worker.workerId
|
|
301
|
+
if (!workerId) {
|
|
302
|
+
updateDispatchRecord(specBase, changeName, { state: 'abandoned', errorCode: 'no_worker_id' })
|
|
303
|
+
return { ok: false, reason: 'dispatch_worker 未返回 workerId', guidance: '记录已转 abandoned;可重派或降级:\n' + renderFallbackGuidance(cwd) }
|
|
304
|
+
}
|
|
305
|
+
updateDispatchRecord(specBase, changeName, { workerId, state: 'in-flight', lastState: 'queued', lastStateAt: Date.now() })
|
|
306
|
+
return {
|
|
307
|
+
ok: true, missionId, workerId, reviewRunId: runId,
|
|
308
|
+
nextStep: `已异步派发(D-002,不阻塞等待)。轮询:sillyspec review-dispatch --status --change ${changeName}`,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (mode === 'status') {
|
|
313
|
+
const rec = readDispatchRecord(specBase, changeName)
|
|
314
|
+
if (!rec) return { ok: false, reason: '无在途记录——先 create(缺省形态)派发。' }
|
|
315
|
+
if (!client || !client.listWorkers) return { ok: false, reason: 'client 未注入' }
|
|
316
|
+
const workers = await client.listWorkers(rec.missionId)
|
|
317
|
+
const w = Array.isArray(workers) ? workers.find((x) => x && (x.id === rec.workerId || x.worker_id === rec.workerId)) : null
|
|
318
|
+
const st = w ? String(w.status || '') : 'unknown'
|
|
319
|
+
const statusLine = `mission ${rec.missionId} worker ${rec.workerId}: ${st}(最近迁移 ${new Date(rec.lastStateAt).toLocaleString('zh-CN')})`
|
|
320
|
+
// 状态迁移更新(同态不刷时间戳——停滞计时基于真实迁移)
|
|
321
|
+
if (w && st && st !== rec.lastState) {
|
|
322
|
+
updateDispatchRecord(specBase, changeName, { lastState: st, lastStateAt: Date.now() })
|
|
323
|
+
rec.lastState = st; rec.lastStateAt = Date.now()
|
|
324
|
+
}
|
|
325
|
+
if (st === 'completed') {
|
|
326
|
+
const gr = await client.getWorkerResult({ missionId: rec.missionId, workerId: rec.workerId })
|
|
327
|
+
const review = extractReviewFromArtifacts(gr && gr.artifacts)
|
|
328
|
+
if (!review) {
|
|
329
|
+
return { ok: false, state: 'completed-no-artifact', statusLine, reason: 'worker completed 但 artifacts 提取不到 review JSON', guidance: `人工核对:平台 get_worker_result(mission ${rec.missionId} / worker ${rec.workerId});确认无法回收再 --kill 降级。` }
|
|
330
|
+
}
|
|
331
|
+
const runtimeRoot = join(specBase, '.runtime')
|
|
332
|
+
const mainDocPath = join(specBase, 'changes', changeName, STAGE_MAIN_DOC[rec.stage] || 'design.md')
|
|
333
|
+
const p = persistStageReview({
|
|
334
|
+
runtimeRoot, stage: rec.stage, changeName, reviewRunId: rec.reviewRunId,
|
|
335
|
+
reviewObj: review, missionId: rec.missionId, mainDocPath,
|
|
336
|
+
})
|
|
337
|
+
if (!p.ok) {
|
|
338
|
+
return { ok: false, state: 'bad-review', statusLine, errors: p.errors, guidance: 'artifacts 里的 review 不符 schema——--kill 后降级,或人工按契约修 review.json(mission 证据保留)。' }
|
|
339
|
+
}
|
|
340
|
+
clearDispatchRecord(specBase, changeName)
|
|
341
|
+
return { ok: true, state: 'completed', statusLine, reviewPath: p.reviewPath, verdict: `${review.specVerdict}/${review.qualityVerdict}` }
|
|
342
|
+
}
|
|
343
|
+
if (st === 'failed' || st === 'killed') {
|
|
344
|
+
clearDispatchRecord(specBase, changeName)
|
|
345
|
+
return { ok: false, state: st, statusLine, reason: `worker 终态 ${st}`, guidance: '降级:\n' + renderFallbackGuidance(cwd) }
|
|
346
|
+
}
|
|
347
|
+
// 在途:停滞检测(queued 不计时)
|
|
348
|
+
const stall = detectStall(rec, Date.now(), stallMs)
|
|
349
|
+
return {
|
|
350
|
+
ok: true, state: st || 'in-flight', statusLine,
|
|
351
|
+
stalled: stall.stalled, stallHint: stall.hint,
|
|
352
|
+
nextStep: stall.stalled ? stall.hint : '继续轮询 --status(或稍后重查)。',
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return { ok: false, reason: `未知 mode: ${mode}` }
|
|
357
|
+
} catch (e) {
|
|
358
|
+
return { ok: false, reason: `review-dispatch 异常: ${e && e.message ? e.message : String(e)}` }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// task-06 消费契约预留(配置读取在该 task 落地;此处常量为缺省值单一来源)
|
|
363
|
+
const DEFAULT_REVIEW_DISPATCH_CONFIG = { budget_usd: 1.0, stall_ms: 900_000, timeout_ms: 0 }
|
|
364
|
+
export function readReviewDispatchConfig(cwd) {
|
|
365
|
+
try {
|
|
366
|
+
const p = join(cwd || process.cwd(), '.sillyspec', 'local.yaml')
|
|
367
|
+
if (!existsSync(p)) return { ...DEFAULT_REVIEW_DISPATCH_CONFIG }
|
|
368
|
+
const doc = jsYaml.load(readFileSync(p, 'utf8'))
|
|
369
|
+
const rd = doc && typeof doc === 'object' && doc.review_dispatch && typeof doc.review_dispatch === 'object' ? doc.review_dispatch : {}
|
|
370
|
+
const num = (v, dflt) => (typeof v === 'number' && v >= 0 ? v : dflt)
|
|
371
|
+
return {
|
|
372
|
+
budget_usd: num(rd.budget_usd, DEFAULT_REVIEW_DISPATCH_CONFIG.budget_usd),
|
|
373
|
+
stall_ms: num(rd.stall_ms, DEFAULT_REVIEW_DISPATCH_CONFIG.stall_ms),
|
|
374
|
+
timeout_ms: num(rd.timeout_ms, DEFAULT_REVIEW_DISPATCH_CONFIG.timeout_ms),
|
|
375
|
+
}
|
|
376
|
+
} catch { return { ...DEFAULT_REVIEW_DISPATCH_CONFIG } }
|
|
377
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run/archive-distill.js(P0-4 安全变体,docs/sillyspec/noai-ir-roadmap.md §3)。
|
|
3
|
+
*
|
|
4
|
+
* archive「decision-distill 决策提炼」步 noAI 化:该步旧 prompt 本身就是「调用 CLI 纯函数
|
|
5
|
+
* distillIntoKnowledge 并转述返回」——全流程最纯的中继步(提炼/幂等/INDEX 路由本体全在
|
|
6
|
+
* src/decision-distill.js),LLM 在此零判断增量,仅 rejected 缺字段时的人工裁决留价值。
|
|
7
|
+
*
|
|
8
|
+
* 动作语义(与旧 prompt 三点分流逐字对齐):
|
|
9
|
+
* - written 非空 → 已写盘,逐条打印(旧「常规」分支);
|
|
10
|
+
* - needsWait 真 → 该批 rejected 条目未写盘,打印裁决指引:补录 decisions.md 后重跑提炼
|
|
11
|
+
* (幂等,命令行保留在指引里),或经用户裁决跳过并在「确认归档」--output 注记
|
|
12
|
+
* (旧 conditionalWait 三段式收敛到确认归档的用户确认点——归档的用户裁决本就归那里);
|
|
13
|
+
* - skipped(无 decisions.md / 0 条入选)→ 零输出注记(同 docs-debt 无债零输出原则);
|
|
14
|
+
* - 执行异常 → warn 降级跳过不阻断归档(旧步第 4 点 best-effort 语义)。
|
|
15
|
+
*
|
|
16
|
+
* 全路径不抛——本步永远可完成(裁决是确认归档步的输入,不是本步的阻断条件)。
|
|
17
|
+
*/
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { gitQuiet } from '../git-helper.js'
|
|
20
|
+
|
|
21
|
+
export async function executeArchiveDistill({ cwd, specBase, changeName }) {
|
|
22
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
23
|
+
const knowledgeRoot = join(specBase, 'knowledge')
|
|
24
|
+
let headHash = ''
|
|
25
|
+
try {
|
|
26
|
+
headHash = gitQuiet(cwd, ['rev-parse', '--short', 'HEAD']) || ''
|
|
27
|
+
} catch { /* git 不可用 → 空串(「最近确认」字段留空,不阻断) */ }
|
|
28
|
+
try {
|
|
29
|
+
const { distillIntoKnowledge } = await import('../decision-distill.js')
|
|
30
|
+
const r = distillIntoKnowledge(changeDir, knowledgeRoot, headHash)
|
|
31
|
+
const written = Array.isArray(r && r.written) ? r.written : []
|
|
32
|
+
if (written.length > 0) {
|
|
33
|
+
console.log(`\n📝 决策提炼(CLI 机械执行,幂等):${written.length} 条已写入决策知识库——`)
|
|
34
|
+
for (const w of written) console.log(` - ${typeof w === 'string' ? w : JSON.stringify(w)}`)
|
|
35
|
+
} else if (r && r.skipped) {
|
|
36
|
+
console.log(`\nℹ️ 决策提炼零输出:${r.skipped}`)
|
|
37
|
+
}
|
|
38
|
+
if (r && r.needsWait) {
|
|
39
|
+
console.warn(`\n⚠️ rejected 决策缺否决理由/复潮条件(needsWait)——这些条目未写盘,其余条目照常提炼。`)
|
|
40
|
+
console.warn(` 裁决指引(确认归档 --confirm 前处理):`)
|
|
41
|
+
console.warn(` ① 补录:把缺失字段补进 ${join(changeDir, 'decisions.md')} 后重跑提炼(幂等)——`)
|
|
42
|
+
console.warn(` node --input-type=module -e "import { distillIntoKnowledge } from '<sillyspec 源码路径>/src/decision-distill.js'; console.log(JSON.stringify(distillIntoKnowledge(${JSON.stringify(changeDir)}, ${JSON.stringify(knowledgeRoot)}, ${JSON.stringify(headHash)}), null, 2))"`)
|
|
43
|
+
console.warn(` ② 跳过:经用户裁决不入库该条,并在「确认归档」--output 注记跳过原因。`)
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.warn(`\n⚠️ 决策提炼执行异常,降级跳过(best-effort,不阻断归档——同旧步降级语义): ${e && e.message ? e.message : e}`)
|
|
47
|
+
}
|
|
48
|
+
}
|