sillyspec 3.28.4 → 3.28.6
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/AGENTS.md +1 -0
- package/package.json +1 -1
- package/results-review-tier.md +18 -0
- package/sa-rename-probe/src/new.js +4 -0
- package/src/agent-session-log.js +4 -3
- package/src/archive-delta.js +2 -2
- package/src/change-list.js +11 -1
- package/src/commit-suggest.js +52 -1
- package/src/config-cat.js +6 -1
- package/src/config-schema.js +13 -0
- package/src/decision-distill.js +53 -7
- 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 +77 -8
- package/src/friction-tally.js +221 -0
- package/src/index.js +430 -18
- package/src/mcp-server.js +144 -0
- package/src/module-impact.js +8 -1
- package/src/modules.js +46 -34
- package/src/progress/change-registry.js +20 -0
- package/src/progress.js +16 -1
- package/src/quicklog.js +60 -6
- package/src/review-dispatch.js +377 -0
- package/src/run/archive-distill.js +48 -0
- package/src/run/command.js +35 -5
- package/src/run/complete-handlers.js +136 -3
- package/src/run/complete.js +170 -2
- package/src/run/gate-snapshot.js +159 -0
- package/src/run/gates.js +166 -69
- package/src/run/prompt.js +54 -9
- package/src/run/quick-audit.js +78 -16
- package/src/run/shared.js +138 -7
- package/src/run/stage.js +31 -2
- package/src/run/verify-quality-scan.js +383 -0
- package/src/scope-audit.js +941 -0
- package/src/sillyhub-mcp/client.js +58 -3
- package/src/stage-review-checklist.js +44 -0
- package/src/stage-review.js +122 -4
- package/src/stages/archive.js +57 -164
- package/src/stages/brainstorm.js +8 -12
- package/src/stages/execute.js +13 -10
- package/src/stages/plan.js +3 -10
- package/src/stages/verify.js +14 -33
- package/src/sync.js +51 -10
- package/src/task-review.js +108 -16
- package/src/taskcard.js +46 -3
- package/src/validate-artifacts.js +163 -0
- package/src/verify-postcheck.js +90 -10
- package/src/verify-probes.js +134 -12
- package/src/worktree-apply.js +119 -14
- package/templates/agents-instruction.md +1 -0
|
@@ -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
|
+
}
|
package/src/run/command.js
CHANGED
|
@@ -23,10 +23,10 @@ import { basename, join, resolve, dirname } from 'node:path'
|
|
|
23
23
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync, unlinkSync } from 'node:fs'
|
|
24
24
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
25
25
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
26
|
-
import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME } from './shared.js'
|
|
26
|
+
import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, assertDatedChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME, warnSelfRefPointerOnce } from './shared.js'
|
|
27
27
|
import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
28
28
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
29
|
-
import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
|
|
29
|
+
import { completeStep, skipStep, waitStep, continueStep, synthesizeStepOutput } from './complete.js'
|
|
30
30
|
import { runStage } from './stage.js'
|
|
31
31
|
import { sanitizeDesc } from '../quicklog.js'
|
|
32
32
|
import { ProgressManager } from '../progress.js'
|
|
@@ -267,6 +267,13 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
267
267
|
process.exit(2) // 用法错 → exit 2
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
// P1-3(noai-ir-roadmap §4):--meta → 本次调用渲染 SS-META 机器块(prompt.js outputStep
|
|
271
|
+
// 读 SILLYSPEC_RUN_META)。CLI 是短进程:env 只在本进程生命周期内有效,不跨调用泄漏;
|
|
272
|
+
// auto 模式本就恒渲染(autoMeta 路径不变),--meta 对常规模式补齐同款出口。
|
|
273
|
+
if (flags.includes('--meta')) {
|
|
274
|
+
process.env.SILLYSPEC_RUN_META = '1'
|
|
275
|
+
}
|
|
276
|
+
|
|
270
277
|
if (!stageRegistry[stageName] && stageName !== 'auto') {
|
|
271
278
|
console.error(`❌ 未知阶段: ${stageName}`)
|
|
272
279
|
console.error(`可选: ${Object.keys(stageRegistry).join(', ')}, auto`)
|
|
@@ -395,7 +402,9 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
395
402
|
// 忽略恢复(specRoot/runtimeRoot/workspaceId/scanRunId 全不回填,platformOpts 保持
|
|
396
403
|
// 无平台参数状态),按本地模式运行——内置 sync 走本地链路,specRoot 保持本地取值链。
|
|
397
404
|
if (isSelfReferentialSpecRoot(cwd, saved.specRoot)) {
|
|
398
|
-
|
|
405
|
+
// 跨进程窗口降频(2026-09-10 驾驭小结第五批①):daemon junction 形态下每命令命中的重复噪音
|
|
406
|
+
warnSelfRefPointerOnce(cwd, saved.specRoot,
|
|
407
|
+
`⚠️ 检测到自指平台指针(repo-native junction 回环,specRoot 指回本地 .sillyspec),已忽略并按本地模式运行: ${platformOptsFile}(10 分钟内不重复提示)`)
|
|
399
408
|
} else {
|
|
400
409
|
if (saved.specRoot) platformOpts.specRoot = saved.specRoot
|
|
401
410
|
if (saved.runtimeRoot) platformOpts.runtimeRoot = saved.runtimeRoot
|
|
@@ -820,6 +829,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
820
829
|
'--deep', '--quick', '--standard', // scan profile 三档显式选择(scan-profile.js 从 argv 读;互斥见下方 PROFILE_FLAGS 检测)
|
|
821
830
|
'--adopt-branch', // execute 显式收编既有 sillyspec/<change> 分支为 worktree 工作分支(坑 worktree-user-branch-conflict)
|
|
822
831
|
'--diff', '--base', '--full', '--report', // scan diff(D-001:command.js 只补 flag,裸 token 解析归 index.js 子命令拦截)
|
|
832
|
+
'--meta', // P1-3:渲染 SS-META 机器块(布尔 flag,不吃值)
|
|
823
833
|
'-h',
|
|
824
834
|
])
|
|
825
835
|
for (let i = 0; i < flags.length; i++) {
|
|
@@ -1036,6 +1046,22 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1036
1046
|
process.exit(2) // 用法/环境错(目标变更不在当前库)→ exit 2
|
|
1037
1047
|
}
|
|
1038
1048
|
}
|
|
1049
|
+
// ── 变更名日期前缀门禁(brainstorm step6 规则 CLI 化,2026-09-11 实证 friction-signal-hint)──
|
|
1050
|
+
// 格式 YYYY-MM-DD-<简短描述> 此前只是 prompt 约束,agent 自拟名/重命名丢前缀时 CLI 照单
|
|
1051
|
+
// 全收静默物化。只拦「净新建」:DB 无行(!progress 已证)+ changes/(含 archive/)无目录;
|
|
1052
|
+
// 已物化存量(含归档无前缀旧名 auto-flow-optimization 等)不追诉,照常自愈初始化。
|
|
1053
|
+
// quick 阶段豁免:其 changeName 是会话 key(quick-<8hex>),非变更名。
|
|
1054
|
+
if (changeName && stageName !== 'quick' && !/^quick-[0-9a-f]{8}$/.test(changeName)
|
|
1055
|
+
&& !existsSync(join(specBase, 'changes', changeName))
|
|
1056
|
+
&& !existsSync(join(specBase, 'changes', 'archive', changeName))) {
|
|
1057
|
+
try {
|
|
1058
|
+
assertDatedChangeName(changeName)
|
|
1059
|
+
} catch (e) {
|
|
1060
|
+
console.error(`❌ ${e.message}`)
|
|
1061
|
+
console.error(` 重试:sillyspec run ${stageName} --change <YYYY-MM-DD-简短描述>`)
|
|
1062
|
+
process.exit(2) // 用法错(新变更名格式非法)→ exit 2
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1039
1065
|
// 如果指定了变更名或有变更目录,自动初始化变更的 progress
|
|
1040
1066
|
const autoChange = changeName || resolveChangeNameAuto(cwd, specRoot)
|
|
1041
1067
|
if (autoChange) {
|
|
@@ -1738,8 +1764,12 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1738
1764
|
}
|
|
1739
1765
|
|
|
1740
1766
|
if (!outputText) {
|
|
1741
|
-
|
|
1742
|
-
|
|
1767
|
+
// P0-2(noai-ir-roadmap §3):auto --done 的 --output 同样可省略——与 completeStep 同源
|
|
1768
|
+
// 的事实性摘要合成(此前硬拒 exit 2 是用法守卫;SS-META 的 doneCommand 仍带 --output,
|
|
1769
|
+
// 照抄路径零变化,省略路径不再罚一轮)。
|
|
1770
|
+
const _autoStep = progress?.stages?.[currentStage]?.steps?.find((s) => s && ['pending', 'in-progress', 'blocked'].includes(s.status))
|
|
1771
|
+
outputText = synthesizeStepOutput({ stageName: currentStage, stepName: _autoStep?.name || currentStage, cwd })
|
|
1772
|
+
console.log(`🤖 auto --done 未带 --output——CLI 已按事实合成步骤摘要(语义说明下次带 --output 手写)。`)
|
|
1743
1773
|
}
|
|
1744
1774
|
|
|
1745
1775
|
const result = await completeStep(pm, progress, currentStage, cwd, outputText, inputText, { printNext: false, changeName, platformOpts })
|