sillyspec 3.25.11 → 3.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-quick/SKILL.md +3 -1
- package/.sillyspec-platform-cleaned +1 -0
- package/.sillyspec-platform.json +7 -0
- package/package.json +1 -1
- package/src/db.js +10 -2
- package/src/dispatch/probe.js +11 -9
- package/src/index.js +151 -3
- package/src/local-detect.js +120 -110
- package/src/progress/change-registry.js +15 -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 +296 -6
- package/src/quicklog.js +46 -7
- package/src/run/command.js +13 -2
- package/src/run/complete-handlers.js +30 -7
- package/src/run/complete.js +17 -4
- package/src/run/gates.js +13 -2
- package/src/run/shared.js +74 -0
- 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
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { basename, join, resolve, relative, isAbsolute } from 'node:path'
|
|
26
26
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from 'node:fs'
|
|
27
27
|
import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
|
|
28
|
-
import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata } from './shared.js'
|
|
28
|
+
import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, resolveRuntimeRoot } from './shared.js'
|
|
29
29
|
import { detectConcurrentChanges, formatConcurrentWarning } from './concurrent-detect.js'
|
|
30
30
|
import { stageRegistry } from '../stages/index.js'
|
|
31
31
|
import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
|
|
@@ -130,9 +130,32 @@ export function findAlreadyArchivedDir(archiveDir, changeName) {
|
|
|
130
130
|
/**
|
|
131
131
|
* 归档时清理可能残留的 worktree(execute 自动清理未走到 / 有未 apply 变更被遗弃)。
|
|
132
132
|
* 安全策略:有未 apply 变更时保留 worktree 并警告,避免误删用户未应用的代码。
|
|
133
|
+
* 同时清理该变更的 execute / stage-review runId marker(只写不删的累积物,
|
|
134
|
+
* 见 stage.js execute 启动固定 executeRunId / stage-review.js stageReviewMarkerPath)。
|
|
133
135
|
* 从 archiveChangeDirectory 抽出,供正常归档 + 自愈归档复用。
|
|
134
136
|
*/
|
|
135
|
-
async function archiveWorktreeCleanup(cwd, archiveChangeName) {
|
|
137
|
+
async function archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts = {}) {
|
|
138
|
+
// ── 清理 runId marker:execute(current-execute-run-id-<change>)与 stage-review
|
|
139
|
+
// (current-stage-review-run-id-<stage>-<change>)。marker 只服务 execute→verify→archive
|
|
140
|
+
// 期间,归档后无读者;不删则 .runtime 随变更数无限累积。runtimeRoot 解析同写入侧
|
|
141
|
+
// (resolveRuntimeRoot 锚主仓),避免平台模式清理错位置。
|
|
142
|
+
try {
|
|
143
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
144
|
+
if (existsSync(runtimeRoot)) {
|
|
145
|
+
const markers = readdirSync(runtimeRoot).filter((f) =>
|
|
146
|
+
f === `current-execute-run-id-${archiveChangeName}` ||
|
|
147
|
+
(f.startsWith('current-stage-review-run-id-') && f.endsWith(`-${archiveChangeName}`)),
|
|
148
|
+
)
|
|
149
|
+
for (const m of markers) {
|
|
150
|
+
try { unlinkSync(join(runtimeRoot, m)) } catch {}
|
|
151
|
+
}
|
|
152
|
+
if (markers.length > 0) {
|
|
153
|
+
console.log(`🧹 归档清理 ${markers.length} 个 runId marker(execute/stage-review)`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {
|
|
157
|
+
console.warn(`⚠️ 归档 runId marker 清理失败(不阻断归档): ${e.message}`)
|
|
158
|
+
}
|
|
136
159
|
try {
|
|
137
160
|
const { WorktreeManager } = await import('../worktree.js')
|
|
138
161
|
const wm = new WorktreeManager({ cwd })
|
|
@@ -154,7 +177,7 @@ async function archiveWorktreeCleanup(cwd, archiveChangeName) {
|
|
|
154
177
|
}
|
|
155
178
|
}
|
|
156
179
|
|
|
157
|
-
export async function archiveChangeDirectory(pm, cwd, progress, specBase) {
|
|
180
|
+
export async function archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts = {}) {
|
|
158
181
|
const archiveChangeName = progress.currentChange
|
|
159
182
|
if (!archiveChangeName) {
|
|
160
183
|
console.error('❌ 归档失败:未找到当前变更名(currentChange)')
|
|
@@ -176,7 +199,7 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase) {
|
|
|
176
199
|
if (alreadyArchivedDir) {
|
|
177
200
|
console.log(`ℹ️ 源目录不存在但变更已在 archive/(${basename(alreadyArchivedDir)}),判定已归档,自愈进度 DB`)
|
|
178
201
|
pm.unregisterChange(cwd, archiveChangeName)
|
|
179
|
-
await archiveWorktreeCleanup(cwd, archiveChangeName)
|
|
202
|
+
await archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts)
|
|
180
203
|
console.log(`📦 已自愈归档:${archiveChangeName} → archive/${basename(alreadyArchivedDir)}/`)
|
|
181
204
|
return alreadyArchivedDir
|
|
182
205
|
}
|
|
@@ -221,7 +244,7 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase) {
|
|
|
221
244
|
} catch {}
|
|
222
245
|
|
|
223
246
|
// 归档时清理可能残留的 worktree(自愈路径也复用,见上方 srcDir 缺失分支)。
|
|
224
|
-
await archiveWorktreeCleanup(cwd, archiveChangeName)
|
|
247
|
+
await archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts)
|
|
225
248
|
|
|
226
249
|
console.log(`📦 已归档:${archiveChangeName} → archive/${destName}/`)
|
|
227
250
|
return destDir
|
|
@@ -236,7 +259,7 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase) {
|
|
|
236
259
|
*
|
|
237
260
|
* @returns {{stageCompleted:false,currentIdx,nextPendingIdx:number}|null}
|
|
238
261
|
*/
|
|
239
|
-
export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase }) {
|
|
262
|
+
export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase, platformOpts = {} }) {
|
|
240
263
|
if (stageName !== 'archive' || steps[currentIdx]?.name !== '确认归档') return null
|
|
241
264
|
if (!confirm) {
|
|
242
265
|
steps[currentIdx].status = 'pending'
|
|
@@ -246,7 +269,7 @@ export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, c
|
|
|
246
269
|
console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
|
|
247
270
|
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
248
271
|
}
|
|
249
|
-
const archivedDir = await archiveChangeDirectory(pm, cwd, progress, specBase)
|
|
272
|
+
const archivedDir = await archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts)
|
|
250
273
|
if (archivedDir && existsSync(archivedDir)) {
|
|
251
274
|
const recommendedDocs = ['design.md', 'module-impact.md']
|
|
252
275
|
const missingRecommended = recommendedDocs.filter(d => !existsSync(join(archivedDir, d)))
|
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。
|
|
@@ -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
|
}
|