sillyspec 3.25.11 → 3.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-quick/SKILL.md +3 -1
- package/.sillyspec-platform-cleaned +1 -0
- package/.sillyspec-platform.json +7 -0
- package/README.md +3 -5
- package/package.json +2 -3
- package/src/db-engine.js +55 -0
- package/src/db.js +53 -35
- package/src/dispatch/probe.js +11 -9
- package/src/doctor-diagnostics.js +6 -6
- package/src/hooks/worktree-guard.js +18 -26
- package/src/index.js +151 -3
- package/src/local-detect.js +120 -110
- package/src/progress/change-registry.js +43 -2
- package/src/progress/shared.js +2 -2
- package/src/progress/stage-machine.js +6 -0
- package/src/progress/step-store.js +10 -0
- package/src/progress.js +302 -10
- package/src/quicklog.js +48 -9
- package/src/run/command.js +13 -2
- package/src/run/complete-handlers.js +37 -8
- package/src/run/complete.js +17 -4
- package/src/run/gates.js +13 -2
- package/src/run/shared.js +74 -0
- package/src/run/stage.js +6 -1
- package/src/sillyhub-mcp/client.js +12 -7
- package/src/sillyhub-mcp/config.js +62 -0
- package/src/stage-review.js +105 -1
- package/src/stages/doctor.js +1 -1
- package/src/stages/execute.js +6 -5
- package/src/stages/quick.js +24 -8
- package/src/stages/scan.js +44 -5
- package/src/stages/verify.js +2 -2
- package/src/sync.js +430 -13
- package/src/task-review.js +29 -0
- package/src/verify-postcheck.js +7 -2
- package/src/worktree-apply.js +135 -27
package/src/run/complete.js
CHANGED
|
@@ -179,7 +179,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
179
179
|
// archive「确认归档」收尾:--confirm 门控 + archiveChangeDirectory 移动 + 推荐文档校验
|
|
180
180
|
// (抽成 handleArchiveConfirmStep;缺 --confirm 时返回 early-return 对象,completeStep 透传)
|
|
181
181
|
{
|
|
182
|
-
const _archiveEarlyReturn = await handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase })
|
|
182
|
+
const _archiveEarlyReturn = await handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase, platformOpts })
|
|
183
183
|
if (_archiveEarlyReturn) return _archiveEarlyReturn
|
|
184
184
|
}
|
|
185
185
|
|
|
@@ -269,7 +269,12 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
269
269
|
if (outputText) {
|
|
270
270
|
const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
|
|
271
271
|
const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
|
|
272
|
-
|
|
272
|
+
try {
|
|
273
|
+
appendFileSync(inputsPath, entry)
|
|
274
|
+
} catch (e) {
|
|
275
|
+
// best-effort:Windows AV/索引占用偶发 EPERM,历史日志写失败不阻断主流程(review-2026-08-09 #7,对齐 shared.js triggerSync)
|
|
276
|
+
console.warn('⚠️ user-inputs.md 追加失败(不阻断):', e.message)
|
|
277
|
+
}
|
|
273
278
|
}
|
|
274
279
|
|
|
275
280
|
// 阶段完成收尾共享管线(handleScan manifest + validateMetadata/FileLocations + auxiliary 重置 +
|
|
@@ -346,7 +351,11 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
346
351
|
if (outputText) {
|
|
347
352
|
const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
|
|
348
353
|
const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
|
|
349
|
-
|
|
354
|
+
try {
|
|
355
|
+
appendFileSync(inputsPath, entry)
|
|
356
|
+
} catch (e) {
|
|
357
|
+
console.warn('⚠️ user-inputs.md 追加失败(不阻断):', e.message)
|
|
358
|
+
}
|
|
350
359
|
}
|
|
351
360
|
|
|
352
361
|
const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
@@ -693,7 +702,11 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
693
702
|
// Append to user-inputs.md
|
|
694
703
|
const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
|
|
695
704
|
const entry = `\n## ${now} | ${changeName || '?'} | ${stageName}: ${currentStep.name} [CONTINUED]\n- 回答:${answer}\n`
|
|
696
|
-
|
|
705
|
+
try {
|
|
706
|
+
appendFileSync(inputsPath, entry)
|
|
707
|
+
} catch (e) {
|
|
708
|
+
console.warn('⚠️ user-inputs.md 追加失败(不阻断):', e.message)
|
|
709
|
+
}
|
|
697
710
|
|
|
698
711
|
// shouldReturnToCurrentStep: 回到当前步骤继续执行(repeatable=多轮探索,requiresWait=确认后执行动作)
|
|
699
712
|
if (shouldReturnToCurrentStep) {
|
package/src/run/gates.js
CHANGED
|
@@ -115,9 +115,20 @@ export async function enforceReviewJsonGate(stageName, cwd, changeName, step, st
|
|
|
115
115
|
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
116
116
|
const planPath = join(specBase, 'changes', changeName, 'plan.md')
|
|
117
117
|
if (!existsSync(runIdFile) || !existsSync(planPath)) return true
|
|
118
|
-
|
|
118
|
+
let executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
119
119
|
const planContent = readFileSync(planPath, 'utf8')
|
|
120
|
-
const { validateCheckedTaskReviews } = await import('../task-review.js')
|
|
120
|
+
const { validateCheckedTaskReviews, resolveLatestExecuteRunIdWithTasks } = await import('../task-review.js')
|
|
121
|
+
// marker 漂移兜底(gate-atom-a 正确修法):marker 指向的 run 缺 tasks/(generateExecuteRunId 只写
|
|
122
|
+
// marker 不建目录,漂移后新 run 不继承旧 review)时,无视 marker 改扫 execute-runs/ 取 mtime 最新
|
|
123
|
+
// 且真正含 tasks/ 的 run,用其齐备的 review.json 校验,避免误报「review.json 不存在」。注意不能用
|
|
124
|
+
// resolveLatestExecuteRunId——它见 marker 非空即原样返回(不校验目录),恰是本场景要绕开的值。
|
|
125
|
+
if (executeRunId && !existsSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'))) {
|
|
126
|
+
const relocated = resolveLatestExecuteRunIdWithTasks({ runtimeRoot })
|
|
127
|
+
if (relocated && relocated !== executeRunId) {
|
|
128
|
+
console.warn(`⚠️ execute run marker 漂移:${executeRunId} 无 tasks/,改用真实含 review 的 run ${relocated}`)
|
|
129
|
+
executeRunId = relocated
|
|
130
|
+
}
|
|
131
|
+
}
|
|
121
132
|
const result = validateCheckedTaskReviews({ planContent, runtimeRoot, executeRunId })
|
|
122
133
|
if (result.ok) return true
|
|
123
134
|
if (steps && steps[currentIdx]) steps[currentIdx].status = 'blocked'
|
package/src/run/shared.js
CHANGED
|
@@ -349,6 +349,80 @@ export async function triggerSync(cwd, changeName, platformOpts = {}) {
|
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
/**
|
|
353
|
+
* 触发 pull(下行同步,task-10 / D-009 / FR-04 / FR-06)。
|
|
354
|
+
* 复用 triggerSync 的 8s 熔断与 Best Effort 语义;未连接平台静默跳过(与现状一致)。
|
|
355
|
+
* 注入时机:CLI 启动(run/--done)+ 关键决策点(approve/archive 前)+ 手动 platform pull。
|
|
356
|
+
* 不在每步 pull(避免高频写入与网络压力),仅低频边界点。
|
|
357
|
+
* @param {string} cwd
|
|
358
|
+
* @param {string} changeName - 当前活跃变更(多变更时传 null 跳过,避免误拉)
|
|
359
|
+
* @param {object} [platformOpts] - 平台模式 opts(specRoot/runtimeRoot 存在则跳过,走平台自有链路)
|
|
360
|
+
*/
|
|
361
|
+
export async function triggerPull(cwd, changeName, platformOpts = {}) {
|
|
362
|
+
// 平台模式(SillyHub)走自己的链路,跳过
|
|
363
|
+
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return
|
|
364
|
+
try {
|
|
365
|
+
const syncMod = await import('../sync.js')
|
|
366
|
+
const sm = new syncMod.SyncManager(cwd)
|
|
367
|
+
// 未连接平台静默跳过(本地独立用户合法状态,不噪音)
|
|
368
|
+
if (!sm._getPlatform()) return
|
|
369
|
+
let timer
|
|
370
|
+
try {
|
|
371
|
+
await Promise.race([
|
|
372
|
+
sm.pull(changeName),
|
|
373
|
+
new Promise((resolve) => { timer = setTimeout(resolve, SYNC_TOTAL_TIMEOUT_MS) }),
|
|
374
|
+
])
|
|
375
|
+
} finally {
|
|
376
|
+
clearTimeout(timer)
|
|
377
|
+
}
|
|
378
|
+
} catch (e) {
|
|
379
|
+
// pull 失败静默跳过(Best Effort,失败不影响正确性)
|
|
380
|
+
console.warn('⚠️ 拉取失败:', e.message)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* triggerPull 的便捷封装:未显式传 changeName 时自动推导单活跃变更(task-10)。
|
|
386
|
+
* 多活跃 / 无活跃变更时跳过(无法确定目标,避免误拉);其余语义同 triggerPull。
|
|
387
|
+
* 供 index.js 在 stage 命令(run/--done/archive)与 approve 决策点注入一行调用。
|
|
388
|
+
*/
|
|
389
|
+
export async function triggerPullActiveChange(cwd, platformOpts = {}) {
|
|
390
|
+
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return
|
|
391
|
+
// 先检查是否连接平台:未连接直接 return,避免 _ensureDB 在无 local.yaml 的 cwd 创建空 DB 污染
|
|
392
|
+
let sm = null
|
|
393
|
+
try {
|
|
394
|
+
const syncMod = await import('../sync.js')
|
|
395
|
+
sm = new syncMod.SyncManager(cwd)
|
|
396
|
+
if (!sm._getPlatform()) return
|
|
397
|
+
} catch {
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
let cn = null
|
|
401
|
+
try {
|
|
402
|
+
const { ProgressManager } = await import('../progress.js')
|
|
403
|
+
const pm = new ProgressManager({})
|
|
404
|
+
const changes = pm.listChanges(cwd)
|
|
405
|
+
if (changes.length === 1) cn = changes[0]
|
|
406
|
+
} catch {
|
|
407
|
+
// progress 不可达则跳过(Best Effort)
|
|
408
|
+
}
|
|
409
|
+
if (!cn) return
|
|
410
|
+
// 已确认连接 + 单活跃变更,调 pull(复用 triggerPull 的 8s 熔断)
|
|
411
|
+
try {
|
|
412
|
+
let timer
|
|
413
|
+
try {
|
|
414
|
+
await Promise.race([
|
|
415
|
+
sm.pull(cn),
|
|
416
|
+
new Promise((resolve) => { timer = setTimeout(resolve, SYNC_TOTAL_TIMEOUT_MS) }),
|
|
417
|
+
])
|
|
418
|
+
} finally {
|
|
419
|
+
clearTimeout(timer)
|
|
420
|
+
}
|
|
421
|
+
} catch (e) {
|
|
422
|
+
console.warn('⚠️ 拉取失败:', e.message)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
352
426
|
/**
|
|
353
427
|
* 审批检查:execute 阶段启动前检查(W6 Step8a 从 run.js 搬入,runStage + runAutoMode 共用)。
|
|
354
428
|
* 平台模式走自己的链路,跳过;否则 await import sync.js。
|
package/src/run/stage.js
CHANGED
|
@@ -22,7 +22,7 @@ import { writeAtomicSync } from '../fs-atomic.js'
|
|
|
22
22
|
import { resolveSpecDir, resolveChangeDir, resolveRuntimeRoot, resolveQuickSessionsDir, triggerSync, safeGit, parsePorcelainPath, formatWaitOptions, checkApproval, getStageSteps } from './shared.js'
|
|
23
23
|
import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck } from './scan-profile.js'
|
|
24
24
|
import { outputStep } from './prompt.js'
|
|
25
|
-
import { allocateQuicklogEntry, deriveTitleFromLinkedChange } from '../quicklog.js'
|
|
25
|
+
import { allocateQuicklogEntry, deriveTitleFromLinkedChange, sanitizeDesc } from '../quicklog.js'
|
|
26
26
|
import { checkTransition } from '../stage-contract.js'
|
|
27
27
|
import { completeStageGates } from './gates.js'
|
|
28
28
|
|
|
@@ -288,6 +288,11 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
288
288
|
if (allowNew) parts.push('允许新增文件')
|
|
289
289
|
console.log(`🛡️ quick 变更边界已记录: ${parts.join(', ')}`)
|
|
290
290
|
console.log(`📝 QUICKLOG 条目已创建: ${qlId}`)
|
|
291
|
+
// 回填 DB changes 行的 title + quicklog_id,让 quick-<hex> 可读、DB↔QUICKLOG 可对账。
|
|
292
|
+
// title 用 quickDesc(任务描述或关联变更标题)经 sanitizeDesc 压一行限长,与 QUICKLOG 条目标题同源。
|
|
293
|
+
try {
|
|
294
|
+
pm.updateChangeMeta(cwd, changeName, { title: sanitizeDesc(quickDesc), quicklogId: qlId });
|
|
295
|
+
} catch { /* 回填失败不阻断 quick 启动 */ }
|
|
291
296
|
pm._write(cwd, progress, changeName)
|
|
292
297
|
} catch (e) {
|
|
293
298
|
console.warn(`⚠️ baseline 记录失败: ${e.message}`)
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* - 网络失败 / 非 2xx / 异常一律 console.warn 不抛错,绝不抛穿到 execute(约束:HTTP 错误保守返回 unavailable/空/false)
|
|
6
6
|
* - 未配置(缺 SILLYHUB_MCP_URL 或 SILLYHUB_MCP_TOKEN)→ 所有方法降级,不发网络
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* 构造函数可传 { url, token, timeoutMs }
|
|
8
|
+
* 配置来源(优先级):显式参数 > local.yaml mcp 段(via readMcpConfig)> 环境变量 SILLYHUB_MCP_URL/TOKEN(fallback);
|
|
9
|
+
* 构造函数可传 { cwd, url, token, timeoutMs },缺省经 readMcpConfig(cwd) 读 local.yaml mcp 段 + env fallback。
|
|
10
10
|
*
|
|
11
11
|
* 端点:POST ${url}/mcp/(尾斜杠必需,MCP streamable HTTP 协议要求),Bearer token 鉴权。
|
|
12
12
|
* 请求体:JSON-RPC 2.0(tools/call 调 tool;tools/list 列 tool schema,task-11 路径A 探测用)。
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* - 仅用 Node 原生 fetch(engine>=18),不引入新依赖。
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { readMcpConfig } from './config.js';
|
|
23
|
+
|
|
22
24
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
23
25
|
// MCP streamable HTTP 协议版本(task 指定 2025-11-25)
|
|
24
26
|
const MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
@@ -26,13 +28,16 @@ const MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
|
26
28
|
export class SillyHubMcpClient {
|
|
27
29
|
/**
|
|
28
30
|
* @param {object} [opts]
|
|
29
|
-
* @param {string} [opts.
|
|
30
|
-
* @param {string} [opts.
|
|
31
|
+
* @param {string} [opts.cwd] - local.yaml 所在主仓库根,默认 process.cwd()(design §7.3)
|
|
32
|
+
* @param {string} [opts.url] - 显式覆盖(优先级最高 > readMcpConfig > env);显式传空串视为未配置
|
|
33
|
+
* @param {string} [opts.token] - 显式覆盖(优先级最高 > readMcpConfig > env)
|
|
31
34
|
* @param {number} [opts.timeoutMs] - 请求超时毫秒,默认 10000
|
|
32
35
|
*/
|
|
33
|
-
constructor({ url, token, timeoutMs } = {}) {
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
+
constructor({ cwd, url, token, timeoutMs } = {}) {
|
|
37
|
+
const workCwd = cwd || process.cwd();
|
|
38
|
+
const cfg = readMcpConfig(workCwd);
|
|
39
|
+
const u = url !== undefined ? url : (cfg?.url ?? '');
|
|
40
|
+
const t = token !== undefined ? token : (cfg?.token ?? '');
|
|
36
41
|
// 去掉 url 尾部斜杠,便于后续稳定拼 /mcp/(跨平台:URL 一律用正斜杠,不用 path.join)
|
|
37
42
|
this._url = typeof u === 'string' ? u.replace(/\/+$/, '') : '';
|
|
38
43
|
this._token = typeof t === 'string' ? t : '';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* readMcpConfig — SillyHub MCP 凭据共享 helper(design §7.2 / §6 mcp 凭据数据流)。
|
|
3
|
+
*
|
|
4
|
+
* 统一读源,供三处消费点:① client.js 构造函数(_url/_token/_configured/_endpoint)
|
|
5
|
+
* ② dispatch/probe.js configFingerprint(缓存 key)+ no-config 快速路径 ③ execute.js
|
|
6
|
+
* getDispatchMode hasConfig(派发三态判定)。
|
|
7
|
+
*
|
|
8
|
+
* 优先级:local.yaml mcp 段(mcp.url + mcp.token 两键齐全)>
|
|
9
|
+
* process.env.SILLYHUB_MCP_URL/TOKEN fallback;
|
|
10
|
+
* mcp 段缺或任一键缺才回退 env。env 也缺 → 返回 null。
|
|
11
|
+
*
|
|
12
|
+
* 读法参考 src/dispatch/probe.js readProbeTtlFromLocalYaml 的 best-effort 风格:
|
|
13
|
+
* join(cwd,'.sillyspec','local.yaml') → existsSync 守卫 → jsYaml.load(readFileSync) → 取 doc.mcp。
|
|
14
|
+
*
|
|
15
|
+
* 铁律(design R-07):
|
|
16
|
+
* - best-effort 绝不抛:文件不存在 / jsYaml.load 抛 / doc 非对象 全 try/catch 回退 env
|
|
17
|
+
* - 纯 fs + env 读,绝不发网络(保 probe no-config 快速路径「不发网络」零回归)
|
|
18
|
+
* - 不引新依赖(js-yaml 复用 dispatch/probe.js:17 已引)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import jsYaml from 'js-yaml';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 读 SillyHub MCP 凭据(local.yaml mcp 段 + env fallback,best-effort 不抛不发网络)。
|
|
27
|
+
* @param {string} cwd - 主仓库根(local.yaml 在 <cwd>/.sillyspec/local.yaml)
|
|
28
|
+
* @returns {{url: string, token: string} | null}
|
|
29
|
+
* 凭据齐全 → { url(尾斜杠归一), token };local.yaml mcp 段缺/缺键 + env 缺 → null
|
|
30
|
+
*/
|
|
31
|
+
export function readMcpConfig(cwd) {
|
|
32
|
+
// 1. local.yaml mcp 段优先
|
|
33
|
+
try {
|
|
34
|
+
const p = join(cwd, '.sillyspec', 'local.yaml');
|
|
35
|
+
if (existsSync(p)) {
|
|
36
|
+
const doc = jsYaml.load(readFileSync(p, 'utf8'));
|
|
37
|
+
if (doc && typeof doc === 'object') {
|
|
38
|
+
const mcp = doc.mcp;
|
|
39
|
+
if (mcp && typeof mcp === 'object') {
|
|
40
|
+
const u = typeof mcp.url === 'string' ? mcp.url : '';
|
|
41
|
+
const t = typeof mcp.token === 'string' ? mcp.token : '';
|
|
42
|
+
// 两键齐全才视为有效源(design §7.2「缺键 → 回退 env」)
|
|
43
|
+
if (u && t) {
|
|
44
|
+
return { url: u.replace(/\/+$/, ''), token: t };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// best-effort:文件读 / js-yaml 解析失败 → 继续回退 env
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. env fallback(兼容旧部署 / 现有测试不设 env 场景,design §9)
|
|
54
|
+
const eu = process.env.SILLYHUB_MCP_URL;
|
|
55
|
+
const et = process.env.SILLYHUB_MCP_TOKEN;
|
|
56
|
+
if (typeof eu === 'string' && eu && typeof et === 'string' && et) {
|
|
57
|
+
return { url: eu.replace(/\/+$/, ''), token: et };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. local.yaml mcp 段缺/缺键 + env 缺 → null(调用方 _configured=false 降级不发网络)
|
|
61
|
+
return null;
|
|
62
|
+
}
|
package/src/stage-review.js
CHANGED
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
* (平台模式落 <runtimeRoot>/stage-reviews/...,与 execute-runs 同构)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { existsSync, readFileSync, mkdirSync, readdirSync } from 'fs'
|
|
16
|
+
import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync } from 'fs'
|
|
17
17
|
import { join } from 'path'
|
|
18
18
|
import { createHash } from 'crypto'
|
|
19
19
|
import { VALID_VERDICTS, REVIEW_SCHEMA_VERSION } from './task-review.js'
|
|
20
20
|
import { detectSpecDirTypo } from './spec-dir-typo.js'
|
|
21
|
+
import { resolveRuntimeRoot } from './run/shared.js'
|
|
21
22
|
|
|
22
23
|
// 文档型 stage review 的合法 reviewType
|
|
23
24
|
export const STAGE_REVIEW_TYPES = ['design', 'plan', 'proposal', 'code', 'acceptance']
|
|
@@ -434,3 +435,106 @@ export function printStageReviewResult(result, context = {}) {
|
|
|
434
435
|
}
|
|
435
436
|
}
|
|
436
437
|
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 注册一个 stage 级 review(确定性 writer)—— 生成 run 目录 + review.json 骨架(或 adopt
|
|
441
|
+
* agent 草稿)+ 写 marker,治 tier=independent 时「调度者手动派独立子代理 → marker 不写 →
|
|
442
|
+
* gate 取错 run」的死锁。与 task 级 generateTaskReviewDrafts(task-review.js)对称。
|
|
443
|
+
*
|
|
444
|
+
* docHash 由 CLI 直接 computeDocHash 算(命令有文件访问权),消除 agent 手算 hash 易错
|
|
445
|
+
* (部分实现 P6.1b defer:仅本 scaffold 路径确定性,不改 agent 自写 review.json 的 hash 链路)。
|
|
446
|
+
*
|
|
447
|
+
* 纯新增,不改 validateStageReview / getLatestStageReviewRunId / renderReviewJsonContract 等
|
|
448
|
+
* 现有函数;self-check 只验 mechanics(schema + docHash),verdict 留给 Stage Review Gate 判。
|
|
449
|
+
*
|
|
450
|
+
* @param {object} opts
|
|
451
|
+
* @param {string} opts.changeName - 变更名
|
|
452
|
+
* @param {string} opts.stage - brainstorm|plan|execute
|
|
453
|
+
* @param {string} [opts.fromFile] - adopt 模式:agent 已写的 review.json(保留 verdict/checklist,重算 docHash + 规范化 reviewedFiles[0])
|
|
454
|
+
* @param {string} opts.cwd - 主仓 cwd(解析 specBase)
|
|
455
|
+
* @param {object} [opts.platformOpts] - 平台选项(specRoot 等)
|
|
456
|
+
* @returns {{ ok: boolean, reviewRunId: string, reviewPath: string, markerPath: string, mode: 'skeleton'|'adopted', mainDoc: string, review: object }}
|
|
457
|
+
* @throws {Error} changeName 空 / stage 非法 / 主文档缺失 / --from 不存在或 schema 不过
|
|
458
|
+
*/
|
|
459
|
+
export function registerStageReview({ changeName, stage, fromFile, cwd, platformOpts = {} }) {
|
|
460
|
+
if (!changeName) throw new Error('register-stage-review: changeName 不能为空')
|
|
461
|
+
if (!stage || !STAGE_MAIN_DOC[stage]) {
|
|
462
|
+
throw new Error(`register-stage-review: stage 无效 "${stage}"(应为 brainstorm|plan|execute)`)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
466
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
467
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
468
|
+
const reviewType = STAGE_REVIEW_TYPE[stage]
|
|
469
|
+
const mainDoc = STAGE_MAIN_DOC[stage]
|
|
470
|
+
const mainDocPath = join(changeDir, mainDoc)
|
|
471
|
+
if (!existsSync(mainDocPath)) {
|
|
472
|
+
throw new Error(`register-stage-review: 主审查文档不存在 ${mainDocPath}(${stage} 审 ${mainDoc}),无法算 docHash`)
|
|
473
|
+
}
|
|
474
|
+
const docHash = computeDocHash(mainDocPath)
|
|
475
|
+
if (!docHash) {
|
|
476
|
+
throw new Error(`register-stage-review: 计算 docHash 失败(computeDocHash 返回 null):${mainDocPath}`)
|
|
477
|
+
}
|
|
478
|
+
const reviewedFiles = [`changes/${changeName}/${mainDoc}`]
|
|
479
|
+
|
|
480
|
+
let review
|
|
481
|
+
let mode
|
|
482
|
+
if (fromFile) {
|
|
483
|
+
// adopt 模式:保留 agent 的 verdict/checklist/reviewerNotes/requiredEvidence,仅修 mechanics
|
|
484
|
+
const fromAbs = existsSync(fromFile) ? fromFile : join(cwd, fromFile)
|
|
485
|
+
if (!existsSync(fromAbs)) {
|
|
486
|
+
throw new Error(`register-stage-review: --from 文件不存在 ${fromFile}(相对 cwd ${fromAbs} 也未命中)`)
|
|
487
|
+
}
|
|
488
|
+
let parsed
|
|
489
|
+
try {
|
|
490
|
+
parsed = JSON.parse(readFileSync(fromAbs, 'utf8'))
|
|
491
|
+
} catch (e) {
|
|
492
|
+
throw new Error(`register-stage-review: --from 文件 JSON 解析失败 ${fromAbs}: ${e.message}`)
|
|
493
|
+
}
|
|
494
|
+
const schemaResult = validateStageReviewSchema(parsed)
|
|
495
|
+
if (!schemaResult.ok) {
|
|
496
|
+
throw new Error(`register-stage-review: --from 文件 schema 校验失败 — ${schemaResult.errors.join('; ')}`)
|
|
497
|
+
}
|
|
498
|
+
review = { ...parsed, docHash, reviewedFiles }
|
|
499
|
+
mode = 'adopted'
|
|
500
|
+
} else {
|
|
501
|
+
// 骨架模式:cannot_verify 待审占位(schema 反逃逸:cannot_verify 必须带非空 requiredEvidence)
|
|
502
|
+
review = {
|
|
503
|
+
schemaVersion: REVIEW_SCHEMA_VERSION,
|
|
504
|
+
reviewType,
|
|
505
|
+
specVerdict: 'cannot_verify',
|
|
506
|
+
qualityVerdict: 'cannot_verify',
|
|
507
|
+
reviewedFiles,
|
|
508
|
+
docHash,
|
|
509
|
+
requiredEvidence: [`待独立审查子代理对照 ${mainDoc} 逐节核验(骨架由 register-stage-review 生成)`],
|
|
510
|
+
reviewerNotes: '骨架由 register-stage-review 生成,verdict 待独立审查子代理填写',
|
|
511
|
+
}
|
|
512
|
+
mode = 'skeleton'
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const reviewRunId = generateStageReviewRunId()
|
|
516
|
+
const reviewDir = join(runtimeRoot, 'stage-reviews', `${stage}-${reviewRunId}`)
|
|
517
|
+
const reviewPath = join(reviewDir, 'review.json')
|
|
518
|
+
mkdirSync(reviewDir, { recursive: true })
|
|
519
|
+
writeFileSync(reviewPath, JSON.stringify(review, null, 2) + '\n')
|
|
520
|
+
|
|
521
|
+
const markerPath = stageReviewMarkerPath(runtimeRoot, stage, changeName)
|
|
522
|
+
if (existsSync(markerPath)) {
|
|
523
|
+
console.warn(`⚠️ register-stage-review: marker 已存在 ${markerPath},将被覆盖为 ${reviewRunId}`)
|
|
524
|
+
}
|
|
525
|
+
mkdirSync(runtimeRoot, { recursive: true })
|
|
526
|
+
writeFileSync(markerPath, reviewRunId + '\n')
|
|
527
|
+
|
|
528
|
+
// self-check(fail-closed):刚写的 review 必过 schema + docHash 真实性。只验 mechanics,
|
|
529
|
+
// 不判 verdict(verdict 是 agent/子代理的审查结论,even fail 也如实落盘,由 Stage Review Gate 裁决)。
|
|
530
|
+
const schemaRecheck = validateStageReviewSchema(review)
|
|
531
|
+
if (!schemaRecheck.ok) {
|
|
532
|
+
throw new Error(`register-stage-review: 写入后 schema 自检失败(不应发生)— ${schemaRecheck.errors.join('; ')}`)
|
|
533
|
+
}
|
|
534
|
+
const hashRecheck = verifyStageReviewDocHash(review, [specBase, changeDir, cwd])
|
|
535
|
+
if (!hashRecheck.ok) {
|
|
536
|
+
throw new Error(`register-stage-review: 写入后 docHash 自检失败(不应发生)— ${hashRecheck.errors.join('; ')}`)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return { ok: true, reviewRunId, reviewPath, markerPath, mode, mainDoc, review }
|
|
540
|
+
}
|
package/src/stages/doctor.js
CHANGED
|
@@ -350,7 +350,7 @@ timeout 5 which docker 2>/dev/null && echo "✅ Docker 可用" || echo "ℹ️ D
|
|
|
350
350
|
|
|
351
351
|
**常见问题及修复:**
|
|
352
352
|
- CLI 未安装 → \`npm install -g sillyspec\`
|
|
353
|
-
- 缺少 local.yaml → \`sillyspec
|
|
353
|
+
- 缺少 local.yaml → \`sillyspec local detect\` 重新生成,或手动创建
|
|
354
354
|
- local.yaml 缺少 test 命令 → 补充对应命令
|
|
355
355
|
- 缺少 scan 文档(ARCHITECTURE.md 等)→ \`sillyspec run scan\` 重新扫描
|
|
356
356
|
- sillyspec.db 状态不一致 → \`sillyspec run <阶段> --reset\` 重置对应阶段
|
package/src/stages/execute.js
CHANGED
|
@@ -4,6 +4,7 @@ import { buildContractMatrix, buildConsumerInjection, buildContractFieldInjectio
|
|
|
4
4
|
import { getRule } from '../stage-contract-spec.js'
|
|
5
5
|
import { renderDispatchInstruction } from '../dispatch/strategy.js'
|
|
6
6
|
import { isPathASupported } from '../dispatch/backends/sillyhub-mcp.js'
|
|
7
|
+
import { readMcpConfig } from '../sillyhub-mcp/config.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* 校验 plan.md 是否满足 execute 执行契约
|
|
@@ -118,7 +119,7 @@ const fixedPrefix = [
|
|
|
118
119
|
1. 读取 tasks.md(执行计划)
|
|
119
120
|
2. 读取 design.md(技术方案)
|
|
120
121
|
3. 读取 CONVENTIONS.md、ARCHITECTURE.md
|
|
121
|
-
4. 读取 local.yaml
|
|
122
|
+
4. 读取 local.yaml(构建命令);若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
|
|
122
123
|
5. 加载项目总览 \`.sillyspec/docs/<project>/scan/PROJECT.md\`(如存在)
|
|
123
124
|
|
|
124
125
|
### 模块文档加载
|
|
@@ -237,7 +238,7 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
|
237
238
|
本步骤由当前 agent 执行,不需要启动独立子代理。
|
|
238
239
|
|
|
239
240
|
### 操作
|
|
240
|
-
1. 读取 local.yaml
|
|
241
|
+
1. 读取 local.yaml 获取构建和测试命令;若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
|
|
241
242
|
2. 运行测试套件(单元测试、集成测试)
|
|
242
243
|
3. 运行 lint 检查 **+ 格式化**:凡变更涉及的源码,既跑 lint check 也跑 formatter(如 \`ruff format\` / \`prettier --write\` / \`black\`),不要只跑 check——只 check 不 format 会把格式问题留到 commit 时被 pre-commit hook 拦截
|
|
243
244
|
4. 如果有测试失败 → 分析原因,标注是代码问题还是测试本身的问题
|
|
@@ -446,7 +447,7 @@ function parseWavesFromPlan(planContent) {
|
|
|
446
447
|
* 同步判定派发后端模式(不发网络,零回归关键)— task-07
|
|
447
448
|
*
|
|
448
449
|
* stub 下 isPathASupported()=false → 有配置也只 local-fallback(加提示),不注入完整 SillyHub 指令。
|
|
449
|
-
*
|
|
450
|
+
* 现有测试套件不设 SILLYHUB_MCP_URL/TOKEN 且无 local.yaml mcp 段 → readMcpConfig 返回 null → 默认 'local' → buildWavePrompt 不注入派发段,
|
|
450
451
|
* 输出与改前字节一致(零回归,D-005)。
|
|
451
452
|
*
|
|
452
453
|
* @returns {'local' | 'local-fallback' | 'sillyhub'}
|
|
@@ -455,7 +456,7 @@ function parseWavesFromPlan(planContent) {
|
|
|
455
456
|
* - sillyhub:有配置且路径A 落地(isPathASupported()=true),注入完整 SillyHub 派发指令
|
|
456
457
|
*/
|
|
457
458
|
export function getDispatchMode() {
|
|
458
|
-
const hasConfig = !!(process.
|
|
459
|
+
const hasConfig = !!readMcpConfig(process.cwd())
|
|
459
460
|
if (!hasConfig) return 'local'
|
|
460
461
|
return isPathASupported() ? 'sillyhub' : 'local-fallback'
|
|
461
462
|
}
|
|
@@ -599,7 +600,7 @@ ${prototypes.map(p => `- \`${path.join(protoRelDir, p)}\``).join('\n')}
|
|
|
599
600
|
dispatchSection = `\n### 派发后端:SillyHub MCP(探测可用,一 Wave 一 mission)\n\n本次 Wave 派发经 SillyHub MCP。按以下派发指令执行(含 create_mission / dispatch_worker / 轮询 list_workers / 超时 kill lease 防双写 / 回收 + Local 兜底):\n\n${instruction}\n`
|
|
600
601
|
} else if (worktreePath && dispatchMode === 'local-fallback') {
|
|
601
602
|
// 有 MCP 配置但路径A 未落地:加短提示,派发仍走 Local(与默认行为一致)
|
|
602
|
-
dispatchSection = `\n### 派发后端提示:SillyHub MCP 已配置但路径A 未落地\n\n检测到
|
|
603
|
+
dispatchSection = `\n### 派发后端提示:SillyHub MCP 已配置但路径A 未落地\n\n检测到 local.yaml mcp 段或 env 配置,但 SillyHub \`dispatch_worker\` 尚不支持 \`worktree_path\`(路径A 跨仓未落地)。本次派发走 Local(本机 Agent tool),与默认行为一致——上方「执行方式」与「工作目录」段适用。\n`
|
|
603
604
|
}
|
|
604
605
|
// dispatchMode === 'local'(无配置)或 worktreePath 为空 → dispatchSection = '' → 输出与改前字节一致(零回归)
|
|
605
606
|
|
package/src/stages/quick.js
CHANGED
|
@@ -33,7 +33,7 @@ export const definition = {
|
|
|
33
33
|
|
|
34
34
|
**你不要创建或修改任何 QUICKLOG 的条目骨架 / tasks.md 记录**——ql-ID 分配、「进行中」条目、状态翻转、task 勾选、轮转全由 CLI 接管,你无需手写。<quicklog-id> 可用于 design.md / plan.md / archive / 模块变更索引引用。
|
|
35
35
|
|
|
36
|
-
>
|
|
36
|
+
> ℹ️ step 3 --done 时,CLI 已落盘结构化 QUICKLOG 条目(标题从「需求:」提取、正文四字段分行、文件行可用 --file-notes 写多行括注)。事后只需按需核对,详见 step 3「QUICKLOG 正文核对」。
|
|
37
37
|
|
|
38
38
|
### 输出
|
|
39
39
|
任务理解 + 上下文摘要`,
|
|
@@ -82,12 +82,26 @@ export const definition = {
|
|
|
82
82
|
|
|
83
83
|
缺任一项,\`--done\` 会被拒并提示缺失字段,补全后重跑即可(不丢进度)。
|
|
84
84
|
|
|
85
|
+
### 📎 文件括注 --file-notes(可选,省事后手改文件行)
|
|
86
|
+
若希望 QUICKLOG「文件:」行直接落盘为多行带括注(而非单行逗号路径),\`--done\` 时加 \`--file-notes\`,CLI 落盘为多行 \`- path(括注)\`,免去事后手改:
|
|
87
|
+
|
|
88
|
+
\`\`\`
|
|
89
|
+
sillyspec run quick --done --change <id> --output "需求:… 根因:… 方案:… 结果:…" --file-notes "src/a.js::登录端点串限流 || src/b.js::新建 INCR 计数"
|
|
90
|
+
\`\`\`
|
|
91
|
+
|
|
92
|
+
格式:\`path::括注\` 一条,\`||\` 分隔多条(括注可省略只留 \`path\`)。不传则「文件:」行回填审计到的实际改动文件单行(仍可事后手改)。
|
|
93
|
+
|
|
94
|
+
### 收尾推荐顺序(模块文档在 --done 前,QUICKLOG 在 --done 后,别记混)
|
|
95
|
+
1. 【--done 前】命中模块 → 改模块文档 → \`git add -- <模块文档>\`(见下方「模块文档同步」)
|
|
96
|
+
2. 【--done】\`sillyspec run quick --done --change <id> --output "四字段" [--file-notes "..."]\`(CLI 自动翻完成 + 勾 task + 落盘 QUICKLOG 标题/文件/正文)
|
|
97
|
+
3. 【--done 后】核对 QUICKLOG 标题(CLI 已从「需求:」自动提取,仅弱标题才手改)→ 若改了再 \`git add\`
|
|
98
|
+
|
|
85
99
|
### 操作
|
|
86
100
|
1. 查看 \`git status --porcelain\`,确认只包含本次 quick 相关文件
|
|
87
101
|
2. 使用 \`git add -- <file...>\` 暂存本次 quick 实际修改的文件(不要 commit,由用户通过统一提交工具处理)
|
|
88
102
|
- 禁止使用 \`git add -A\`
|
|
89
103
|
- 不要暂存 quick 开始前就已存在的无关改动
|
|
90
|
-
3. QUICKLOG / tasks.md 记录由 CLI 在本 step 完成时自动收尾(翻「已完成」+ 勾选 task +
|
|
104
|
+
3. QUICKLOG / tasks.md 记录由 CLI 在本 step 完成时自动收尾(翻「已完成」+ 勾选 task + 轮转),你无需手动建/翻骨架——**QUICKLOG 核对按需**(见下方「QUICKLOG 正文核对」;文件行可用 --file-notes 免手改)
|
|
91
105
|
4. 如果发现项目特有的坑,追加到 \`{SPEC_ROOT}/knowledge/uncategorized.md\`
|
|
92
106
|
5. 任务比预期复杂 → 建议用完整流程
|
|
93
107
|
|
|
@@ -105,17 +119,19 @@ export const definition = {
|
|
|
105
119
|
### 输出
|
|
106
120
|
暂存确认 + 记录路径 + 模块文档同步结果(如有)
|
|
107
121
|
|
|
108
|
-
### 📝 QUICKLOG
|
|
109
|
-
|
|
122
|
+
### 📝 QUICKLOG 正文核对(--done 之后,按需;多数无需手改)
|
|
123
|
+
CLI 落盘的条目已是结构化产物(非简版骨架):标题从「需求:」自动提取、正文四字段自动分行、文件行用 --file-notes 时为多行括注。--done 后只需核对,多数情况无需手改:
|
|
124
|
+
|
|
125
|
+
1. 标题:CLI 已从「需求:」提取摘要刷新标题;仅当需求写得太弱(占位/笼统)才手改为真实需求摘要。
|
|
126
|
+
2. 文件:用了 --file-notes 已是多行括注;没传则回填审计到的实际文件单行,可事后补括注(示例见下)。
|
|
127
|
+
3. 正文:四字段已分行落盘,按需扩写充实(不允许只留一段「结果:」)。
|
|
110
128
|
|
|
111
|
-
|
|
112
|
-
2. 文件:把 CLI 写的单行「文件:a.js, b.js」改写为多行 bullet,每个文件带关键改动括注;禁止「(见实际改动)」或无括注纯路径单行。示例:
|
|
129
|
+
补括注示例(仅当没用 --file-notes、想事后补时):
|
|
113
130
|
文件:
|
|
114
131
|
- backend/app/modules/auth/router.py(登录端点串 check_rate_limit→assert_captcha_if_needed)
|
|
115
132
|
- backend/app/modules/auth/captcha_service.py(新建:限流 INCR + 失败计数 + Pillow 滑块生成/校验)
|
|
116
|
-
3. 正文:核对 需求: 根因: 方案: 结果: 四段逐项充实(--output 已写、CLI 原样插入,按需扩写),不允许只留一段「结果:」。
|
|
117
133
|
|
|
118
|
-
一条 quick = 一条独立 ql
|
|
134
|
+
一条 quick = 一条独立 ql 条目;核对只动本次 <quicklog-id> 条目,不追加到旧条目。QUICKLOG 在 .sillyspec/(gitignore),核对不影响 --done 已通过的边界审计。`,
|
|
119
135
|
outputHint: '暂存和记录确认',
|
|
120
136
|
optional: false
|
|
121
137
|
}
|
package/src/stages/scan.js
CHANGED
|
@@ -191,24 +191,63 @@ export const definition = {
|
|
|
191
191
|
2. 调用 \`sillyspec local detect\` 命令生成 local.yaml(该命令封装了纯 fs 项目类型嗅探:package.json→nodejs / pom.xml→maven / build.gradle→gradle / Makefile→make / 否则 generic,对应默认 commands)。**不要自行嗅探 package.json/pom.xml/build.gradle 等文件**——探测逻辑唯一归属 local-detect.js,本步骤只负责调用命令并报告结果。
|
|
192
192
|
3. 该命令内部已处理:确保目录存在(mkdir -p)、已存在则跳过、原子写入(先写 tmp 再 rename)。
|
|
193
193
|
4. 报告生成的 project.type 与 local.yaml 路径。
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
5. **detect 只生成 \`project.type\` / \`commands.{build,test,lint}\`(存在性核验驱动)/ \`test_strategy\` 骨架**,以下策略字段由你(agent)按项目实际情况补充——能在 package.json / lockfile / 构建文件 / CI 配置 / README 中找到依据就填,不确定留空或注释:
|
|
195
|
+
- \`commands.install\`:依赖安装命令(如 \`npm install\` / \`pnpm install\` / \`yarn\`)
|
|
196
|
+
- \`env\`:构建/测试所需环境变量键值(仅从 README / CI 配置 / .env.example 能确证的)
|
|
197
|
+
- \`test_strategy\`:detect 未注入时按项目实际补(full=全量 / module=只测变更模块 / skip=跳过测试)
|
|
198
|
+
- \`modules\`:模块路径根(module_paths,如 \`src/\` / \`packages/\`)
|
|
199
|
+
- \`known_failures\`:已知失败/不稳定测试清单(仅从 CI 历史 / 测试注释能确证的)
|
|
200
|
+
|
|
201
|
+
### platform / dispatch / mcp 段检查(FR-08,可选补充)
|
|
202
|
+
detect 不写这三段,由你按项目是否接入 sillyhub 平台决定是否补:
|
|
203
|
+
- **platform 段缺失**(要接入 sillyhub 平台)→ 运行 \`sillyspec platform connect <url> <token>\` 自动写入(推荐)
|
|
204
|
+
- **dispatch 段调参缺失**(启用 sillyhub 派发且需调优)→ 手填示例 \`probe_ttl_ms\` / \`poll_interval_ms\` / \`worker_timeout_ms\`(仅在你确知调优值时填,否则留默认)
|
|
205
|
+
- **mcp 段缺失**(dispatch worker 走 MCP 协议连 sillyhub)→ 优先 \`sillyspec platform connect\`(同源假设,platform+mcp 同写);不同源则手填 \`mcp.url\` / \`mcp.token\`,或设环境变量 \`SILLYHUB_MCP_URL\` / \`SILLYHUB_MCP_TOKEN\`(不入盘)
|
|
206
|
+
|
|
207
|
+
### ⛔ 铁律(R-04,防编造)
|
|
208
|
+
只写能从 package.json / lockfile / 构建文件 / CI 配置 / README 中确证的事实。不确定的字段一律留空或写注释占位,**绝不编造命令、路径或凭据**。commands 键的存在性已由 detect 核验,不要重复核验。
|
|
209
|
+
|
|
210
|
+
### 输出格式参考(detect 自动产出骨架 + agent 补充策略字段)
|
|
196
211
|
\`\`\`yaml
|
|
197
212
|
# SillySpec 本地配置(自动生成,可手动修改)
|
|
198
213
|
project:
|
|
199
214
|
type: nodejs # nodejs/maven/gradle/make/generic
|
|
200
215
|
|
|
216
|
+
# commands 键存在性由 detect 核验 package.json scripts 驱动:
|
|
217
|
+
# 有 scripts.build 才写 build,有 scripts.test 才写 test,有 scripts.lint 才写 lint
|
|
218
|
+
# (仅 test/lint 无 build 的项目,build 键不写)
|
|
201
219
|
commands:
|
|
202
|
-
build: "npm run build"
|
|
220
|
+
build: "npm run build" # 仅 scripts.build 存在时由 detect 写入
|
|
203
221
|
test: "npm test"
|
|
204
222
|
lint: "npm run lint"
|
|
223
|
+
# install: "npm install" # ← agent 补:依赖安装命令
|
|
205
224
|
|
|
206
225
|
# 测试策略:full=全量测试, module=只测变更模块, skip=跳过测试
|
|
207
226
|
test_strategy: module
|
|
227
|
+
|
|
228
|
+
# ↓ 以下为 agent 按项目实际补充的策略字段(不确定留空或注释)
|
|
229
|
+
# env: # ← agent 补:构建/测试所需环境变量
|
|
230
|
+
# NODE_ENV: test
|
|
231
|
+
# modules: # ← agent 补:模块路径根(module_paths)
|
|
232
|
+
# - src/
|
|
233
|
+
# known_failures: # ← agent 补:已知失败/不稳定测试
|
|
234
|
+
# - "test/flaky.test.mjs"
|
|
235
|
+
|
|
236
|
+
# ↓ 外部连接段(接入 sillyhub 平台时补,detect 不写)
|
|
237
|
+
# platform: # ← sillyspec platform connect <url> <token> 写入
|
|
238
|
+
# url: ...
|
|
239
|
+
# token: ...
|
|
240
|
+
# mcp: # ← platform connect 同源写入 或 手填 或 env(SILLYHUB_MCP_URL/TOKEN)
|
|
241
|
+
# url: ...
|
|
242
|
+
# token: ...
|
|
243
|
+
# dispatch: # ← agent 按需调参(确知调优值才填)
|
|
244
|
+
# probe_ttl_ms: 30000
|
|
245
|
+
# poll_interval_ms: 2000
|
|
246
|
+
# worker_timeout_ms: 60000
|
|
208
247
|
\`\`\`
|
|
209
248
|
|
|
210
249
|
### 输出
|
|
211
|
-
local.yaml 生成结果(已存在/已生成 + project.type
|
|
250
|
+
local.yaml 生成结果(已存在/已生成 + project.type + agent 补充的策略字段摘要)`,
|
|
212
251
|
outputHint: 'local.yaml 生成状态',
|
|
213
252
|
optional: false
|
|
214
253
|
},
|
|
@@ -538,7 +577,7 @@ INDEX.md 维护索引,格式(每行:关键词1|关键词2 → [条目名](
|
|
|
538
577
|
7. 确认所有文档都写入 \`{DOCS_ROOT}/\`(spec-root 下),**而非源码目录下的 .sillyspec/**
|
|
539
578
|
8. 检查是否出现 tool_use_error 或 API Error 未恢复
|
|
540
579
|
9. 检查 7 份文档 header 是否包含 author 和 created_at
|
|
541
|
-
10.
|
|
580
|
+
10. 复查 detect 核验结果——commands 键已由 detect 核验 scripts 存在性,agent 无需重复核验或标 unavailable,仅确认 local.yaml 与 detect 产出一致
|
|
542
581
|
|
|
543
582
|
### ⛔ API 错误处理
|
|
544
583
|
- 遇到 API Error 529(服务过载)或 rate_limit 时,**停止当前操作并报告**,不要自动重试
|
package/src/stages/verify.js
CHANGED
|
@@ -66,7 +66,7 @@ export const definition = {
|
|
|
66
66
|
- 如果存在 P0/P1 unresolved/blocking 决策,验证结论不能为 PASS
|
|
67
67
|
- 如果发现 superseded 决策被下游引用,标记为 ⚠️ stale decision reference
|
|
68
68
|
3. 加载项目信息:\`cat .sillyspec/projects/*.yaml 2>/dev/null\`
|
|
69
|
-
4. 加载本地配置:\`cat .sillyspec/local.yaml 2>/dev/null\`(构建命令、测试命令、lint
|
|
69
|
+
4. 加载本地配置:\`cat .sillyspec/local.yaml 2>/dev/null\`(构建命令、测试命令、lint 命令等)若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
|
|
70
70
|
5. 加载代码规范:\`cat .sillyspec/docs/<project>/scan/CONVENTIONS.md 2>/dev/null\`
|
|
71
71
|
- 测试现状:\`cat .sillyspec/docs/<project>/scan/TESTING.md 2>/dev/null\`(了解既有测试约定与覆盖范围,验收时对照)
|
|
72
72
|
- 技术债清单:\`cat .sillyspec/docs/<project>/scan/CONCERNS.md 2>/dev/null\`(🔴/🟡 区域;本次变更若触碰须在 verify-result.md 标注)
|
|
@@ -164,7 +164,7 @@ export const definition = {
|
|
|
164
164
|
prompt: `运行代码质量扫描(测试实测统一由 CLI 对账执行,本步不重复手动跑全量)。
|
|
165
165
|
|
|
166
166
|
### 操作
|
|
167
|
-
1. 读取 \`.sillyspec/local.yaml\` 获取构建、测试和 lint
|
|
167
|
+
1. 读取 \`.sillyspec/local.yaml\` 获取构建、测试和 lint 命令若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
|
|
168
168
|
2. **不要手动重复跑 commands.test**——CLI 会在最终 --done 时统一执行一次(按变更命中模块子集),本步再跑 = 与 CLI 对账重复耗时(实测 198s×2)。如为提前发现实现问题,可对变更模块做**针对性快速冒烟**(可选,非必需):
|
|
169
169
|
- Maven:\`mvn test -pl <变更模块> -am\`(仅编译变更模块及其依赖)
|
|
170
170
|
- Gradle:\`./gradlew :<模块>:test\`
|