sillyspec 3.25.8 → 3.25.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-auto/SKILL.md +8 -13
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +8 -6
- package/.claude/skills/sillyspec-continue/SKILL.md +3 -3
- package/.claude/skills/sillyspec-doctor/SKILL.md +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
- package/.claude/skills/sillyspec-verify/SKILL.md +1 -1
- package/README.md +9 -0
- package/package.json +2 -2
- package/src/change-list.js +11 -8
- package/src/change-risk-profile.js +22 -0
- package/src/db.js +160 -159
- package/src/dispatch/backends/local-agent.js +152 -0
- package/src/dispatch/backends/sillyhub-mcp.js +242 -0
- package/src/dispatch/probe.js +208 -0
- package/src/dispatch/strategy.js +121 -0
- package/src/doctor-diagnostics.js +19 -25
- package/src/fs-atomic.js +69 -69
- package/src/git-helper.js +66 -0
- package/src/hooks/worktree-guard.js +64 -55
- package/src/index.js +215 -34
- package/src/init.js +7 -14
- package/src/machine-interface.js +54 -19
- package/src/migrate.js +14 -4
- package/src/modules.js +16 -5
- package/src/progress/change-registry.js +72 -70
- package/src/progress/consistency-doctor.js +71 -14
- package/src/progress/shared.js +6 -3
- package/src/progress/stage-machine.js +61 -65
- package/src/progress/step-store.js +62 -67
- package/src/progress.js +155 -233
- package/src/quicklog.js +13 -5
- package/src/run/command.js +65 -37
- package/src/run/complete-handlers.js +1005 -889
- package/src/run/complete.js +115 -208
- package/src/run/concurrent-detect.js +132 -0
- package/src/run/gates.js +289 -17
- package/src/run/prompt.js +22 -68
- package/src/run/quick-audit.js +10 -9
- package/src/run/shared.js +648 -600
- package/src/run/stage.js +43 -24
- package/src/scan-postcheck.js +9 -31
- package/src/sillyhub-mcp/client.js +389 -0
- package/src/spec-dir-typo.js +71 -0
- package/src/stage-contract-engine.js +22 -1
- package/src/stage-contract-spec.js +11 -4
- package/src/stage-contract.js +26 -3
- package/src/stage-review.js +13 -9
- package/src/stages/archive.js +4 -6
- package/src/stages/brainstorm-auto.js +26 -20
- package/src/stages/brainstorm.js +29 -5
- package/src/stages/cmd-existence.js +145 -0
- package/src/stages/doctor.js +9 -6
- package/src/stages/execute.js +61 -20
- package/src/stages/plan-postcheck.js +152 -0
- package/src/stages/plan.js +16 -31
- package/src/stages/quick.js +1 -1
- package/src/stages/scan.js +9 -3
- package/src/stages/status.js +0 -1
- package/src/stages/verify.js +12 -7
- package/src/sync.js +4 -4
- package/src/task-review.js +30 -1
- package/src/verify-postcheck.js +933 -862
- package/src/version.js +19 -0
- package/src/worktree-apply.js +85 -68
- package/src/worktree-deps.js +89 -5
- package/src/worktree.js +213 -111
- package/templates/prompts/taskcard-rules.md +27 -0
- package/templates/prompts/verify-probes.md +3 -3
- package/.claude/skills/sillyspec-propose/SKILL.md +0 -87
- package/src/stages/propose.js +0 -181
package/src/run/gates.js
CHANGED
|
@@ -20,9 +20,12 @@
|
|
|
20
20
|
* 不搬:ensureDepsFreshness(execute 入口 deps 自检,调用方 runStage 非 completeStep,归属未来 execute-handler)
|
|
21
21
|
*/
|
|
22
22
|
import { basename, join } from 'node:path'
|
|
23
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
24
|
-
import { triggerSync, resolveChangeDir } from './shared.js'
|
|
23
|
+
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
24
|
+
import { triggerSync, resolveChangeDir, resolveRuntimeRoot } from './shared.js'
|
|
25
25
|
import { runValidators } from '../stage-contract.js'
|
|
26
|
+
import { handleScanStageCompleted, handleExecuteWorktreeCleanup } from './complete-handlers.js'
|
|
27
|
+
import { detectConcurrentChanges, formatConcurrentWarning } from './concurrent-detect.js'
|
|
28
|
+
import { stageRegistry } from '../stages/index.js'
|
|
26
29
|
|
|
27
30
|
/**
|
|
28
31
|
* 判断当前 execute step 所在 wave 是否全部 task 都声明 no_deps_verify: true(D-006@v2)。
|
|
@@ -108,7 +111,7 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
|
|
|
108
111
|
*/
|
|
109
112
|
export async function enforceReviewJsonGate(stageName, cwd, changeName, step, steps, currentIdx, specBase, platformOpts) {
|
|
110
113
|
if (stageName !== 'execute' || !changeName) return true
|
|
111
|
-
const runtimeRoot = platformOpts
|
|
114
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
112
115
|
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
113
116
|
const planPath = join(specBase, 'changes', changeName, 'plan.md')
|
|
114
117
|
if (!existsSync(runIdFile) || !existsSync(planPath)) return true
|
|
@@ -158,10 +161,10 @@ function rollbackStageCompletion(stageData, steps, currentIdx) {
|
|
|
158
161
|
* 漏写 triggerSync / 写错 return 结构导致行为分裂。返回 nextPendingIdx=currentIdx,
|
|
159
162
|
* 让上层走「完成但不推进」分支,--done 被拒、agent 修复产物后重跑。
|
|
160
163
|
*/
|
|
161
|
-
|
|
164
|
+
function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts) {
|
|
162
165
|
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
163
166
|
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
164
|
-
|
|
167
|
+
pm._write(cwd, progress, changeName)
|
|
165
168
|
triggerSync(cwd, changeName, platformOpts)
|
|
166
169
|
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
167
170
|
}
|
|
@@ -216,12 +219,19 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
216
219
|
// 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
|
|
217
220
|
// 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
|
|
218
221
|
const { runVerifyParityCheck, printVerifyParityCheck } = await import('../verify-postcheck.js')
|
|
219
|
-
const
|
|
222
|
+
const parityRuntimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
223
|
+
const parityCheck = runVerifyParityCheck({ cwd, specBase, changeName, runtimeRoot: parityRuntimeRoot })
|
|
220
224
|
printVerifyParityCheck(parityCheck)
|
|
221
225
|
// ── 删除探针(advisory,不阻断):切斯特顿栅栏护栏,用 git 事实对账静默删除代码 ──
|
|
222
226
|
const { runVerifyDeletionCheck, printVerifyDeletionCheck } = await import('../verify-postcheck.js')
|
|
223
227
|
const deletionCheck = runVerifyDeletionCheck({ cwd, specBase, changeName })
|
|
224
228
|
printVerifyDeletionCheck(deletionCheck)
|
|
229
|
+
// ── required-evidence 对账(advisory,不阻断):闭合 execute→verify evidence 死链 ──
|
|
230
|
+
// execute Task Review Gate 把 cannot_verify 任务的 evidence 落盘 verify-required-evidence.json,
|
|
231
|
+
// 本探针查每个 cannot_verify 任务是否在 verify-result.md 体现(CLI 只查提及,满足度 agent 自报告)。
|
|
232
|
+
const { runVerifyRequiredEvidenceCheck, printVerifyRequiredEvidenceCheck } = await import('../verify-postcheck.js')
|
|
233
|
+
const evidenceCheck = runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName })
|
|
234
|
+
printVerifyRequiredEvidenceCheck(evidenceCheck)
|
|
225
235
|
console.log('\n✅ 验证通过,下一步:sillyspec run archive')
|
|
226
236
|
}
|
|
227
237
|
|
|
@@ -250,12 +260,12 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
250
260
|
}
|
|
251
261
|
}
|
|
252
262
|
|
|
253
|
-
// ── Stage Review Gate:brainstorm/plan/
|
|
263
|
+
// ── Stage Review Gate:brainstorm/plan/execute 独立审查(按 tier 分级)──
|
|
254
264
|
// tier=self 放行+审计打印;tier=independent 必须 review.json 且 verdict 非 fail,fail-closed
|
|
255
|
-
if (['brainstorm', 'plan', '
|
|
265
|
+
if (['brainstorm', 'plan', 'execute'].includes(stageName)) {
|
|
256
266
|
try {
|
|
257
267
|
const { classifyReviewTier } = await import('../review-tier.js')
|
|
258
|
-
const { validateStageReview, getLatestStageReviewRunId, printStageReviewResult } = await import('../stage-review.js')
|
|
268
|
+
const { validateStageReview, getLatestStageReviewRunId, printStageReviewResult, generateStageReviewRunId, stageReviewMarkerPath } = await import('../stage-review.js')
|
|
259
269
|
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
260
270
|
const reviewChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
261
271
|
const designPath = reviewChangeDir ? join(reviewChangeDir, 'design.md') : null
|
|
@@ -268,15 +278,23 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
268
278
|
}
|
|
269
279
|
}
|
|
270
280
|
const tier = classifyReviewTier({ planLevel, designPath })
|
|
271
|
-
const runtimeRoot = platformOpts
|
|
281
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, effectiveSpecBase)
|
|
272
282
|
|
|
273
283
|
if (tier.tier === 'self') {
|
|
274
284
|
console.log('\nℹ️ Stage Review: ' + stageName + ' tier=self(' + tier.reason + '),已降级为当前 agent 自审,不强制独立子代理。')
|
|
275
285
|
} else {
|
|
276
|
-
|
|
286
|
+
let reviewRunId = getLatestStageReviewRunId(runtimeRoot, stageName, changeName)
|
|
287
|
+
if (!reviewRunId) {
|
|
288
|
+
// marker 缺失(execute 批量完成跳过 prompt 渲染等场景)→ 自生 + 写盘
|
|
289
|
+
// 让 gate 读到确定 ID,错误从 execute-null 变 execute-review-<id>(可执行)
|
|
290
|
+
reviewRunId = generateStageReviewRunId()
|
|
291
|
+
try {
|
|
292
|
+
mkdirSync(runtimeRoot, { recursive: true })
|
|
293
|
+
writeFileSync(stageReviewMarkerPath(runtimeRoot, stageName, changeName), reviewRunId + '\n')
|
|
294
|
+
} catch {}
|
|
295
|
+
}
|
|
277
296
|
const reviewType = stageName === 'brainstorm' ? 'design'
|
|
278
297
|
: stageName === 'plan' ? 'plan'
|
|
279
|
-
: stageName === 'propose' ? 'proposal'
|
|
280
298
|
: 'acceptance'
|
|
281
299
|
const searchDirs = [effectiveSpecBase, reviewChangeDir, cwd].filter(Boolean)
|
|
282
300
|
const reviewResult = validateStageReview({ stage: stageName, reviewType, runtimeRoot, reviewRunId, searchDirs })
|
|
@@ -302,7 +320,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
302
320
|
|
|
303
321
|
if (planPath && existsSync(planPath)) {
|
|
304
322
|
const planContent = readFileSync(planPath, 'utf8')
|
|
305
|
-
const runtimeRoot = platformOpts
|
|
323
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, effectiveSpecBase)
|
|
306
324
|
|
|
307
325
|
// execute run id:从变更专属标记文件读取
|
|
308
326
|
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
@@ -313,10 +331,16 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
313
331
|
}
|
|
314
332
|
} catch {}
|
|
315
333
|
if (!executeRunId) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
//
|
|
319
|
-
|
|
334
|
+
// marker 缺失:先扫描 execute-runs/ 既有目录找回真实 runId(与 getLatestStageReviewRunId
|
|
335
|
+
// 目录扫描兜底同语义),避免 marker 丢失而 agent 已用旧 runId 落盘时,直接 generate 新 ID
|
|
336
|
+
// 找不到旧 review、误判缺 review.json。仅当确实无既有 run 才 generate 新 ID 并落盘。
|
|
337
|
+
const { generateExecuteRunId, resolveLatestExecuteRunId } = await import('../task-review.js')
|
|
338
|
+
executeRunId = resolveLatestExecuteRunId({ runtimeRoot, changeName }) || ''
|
|
339
|
+
if (!executeRunId) {
|
|
340
|
+
executeRunId = generateExecuteRunId()
|
|
341
|
+
// 落盘(marker 缺失时 fallback 生成后写盘,保证后续 checkbox/gate 读到同一 ID)
|
|
342
|
+
try { mkdirSync(runtimeRoot, { recursive: true }); writeFileSync(runIdFile, executeRunId + '\n') } catch {}
|
|
343
|
+
}
|
|
320
344
|
}
|
|
321
345
|
|
|
322
346
|
// git 真实性校验目录:worktree 存在则用 worktree(base/head commit 在其中),否则主仓库
|
|
@@ -363,3 +387,251 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
363
387
|
return null
|
|
364
388
|
}
|
|
365
389
|
|
|
390
|
+
/**
|
|
391
|
+
* 读取 design.md frontmatter 的 scale 字段(brainstorm 末步写入 'small'|'large')。
|
|
392
|
+
* completeStep 的下一步提示据此分叉:small→quick,large/读不到→plan(fail-safe 走重流程)。
|
|
393
|
+
* 只解析首个 YAML frontmatter 块,避免误读正文里的 "scale:"。
|
|
394
|
+
* 从 complete.js 迁入(completeStageGates 共享收尾管线 + completeStep brainstorm 提示消费)。
|
|
395
|
+
*/
|
|
396
|
+
export function readDesignScale(specBase, changeName) {
|
|
397
|
+
if (!changeName) return null
|
|
398
|
+
const designPath = join(specBase, 'changes', changeName, 'design.md')
|
|
399
|
+
if (!existsSync(designPath)) return null
|
|
400
|
+
const text = readFileSync(designPath, 'utf8')
|
|
401
|
+
const fm = text.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
|
|
402
|
+
if (!fm) return null
|
|
403
|
+
const m = fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
|
|
404
|
+
return m ? m[1] : null
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* 校验变更目录下近 10 分钟新增的 .md/.yaml/.yml 文件含 author/created_at 元数据(advisory 打印)。
|
|
409
|
+
* 从 complete.js 迁入(completeStageGates 共享收尾管线消费)。
|
|
410
|
+
*/
|
|
411
|
+
export function validateMetadata(cwd, stageName, specBase) {
|
|
412
|
+
const changesDir = join(specBase, 'changes')
|
|
413
|
+
if (!existsSync(changesDir)) return
|
|
414
|
+
|
|
415
|
+
const cutoff = Date.now() - 10 * 60 * 1000
|
|
416
|
+
const missing = []
|
|
417
|
+
|
|
418
|
+
function walk(dir) {
|
|
419
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
420
|
+
const full = join(dir, entry.name)
|
|
421
|
+
try {
|
|
422
|
+
if (entry.isDirectory()) { walk(full); continue }
|
|
423
|
+
if (!/\.(md|yaml|yml)$/.test(entry.name)) continue
|
|
424
|
+
const mtime = statSync(full).mtimeMs
|
|
425
|
+
if (mtime < cutoff) continue
|
|
426
|
+
const content = readFileSync(full, 'utf-8')
|
|
427
|
+
if (!content.includes('author:') && !content.includes('author:')) missing.push(full)
|
|
428
|
+
if (!content.includes('created_at:') && !content.includes('created_at:')) missing.push(full)
|
|
429
|
+
} catch (e) { /* skip unreadable files */ }
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
walk(changesDir)
|
|
434
|
+
const unique = [...new Set(missing)]
|
|
435
|
+
if (unique.length > 0) {
|
|
436
|
+
console.log(`\n⚠️ 以下文件缺少 author 或 created_at 元数据:`)
|
|
437
|
+
unique.forEach(f => console.log(` - ${f.replace(cwd + '/', '')}`))
|
|
438
|
+
console.log('请在文件头部添加 author(git 用户名)和 created_at(精确到秒)')
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* 验证关键文件是否存在于正确的变更目录下(advisory 打印,防 AI 写错路径)。
|
|
444
|
+
* 从 complete.js 迁入(completeStageGates 共享收尾管线消费)。
|
|
445
|
+
*/
|
|
446
|
+
export function validateFileLocations(cwd, stageName, progress, changeName, specBase) {
|
|
447
|
+
const effectiveChange = changeName || progress.currentChange
|
|
448
|
+
if (!effectiveChange) return
|
|
449
|
+
|
|
450
|
+
const changeDir = join(specBase, 'changes', effectiveChange)
|
|
451
|
+
if (!existsSync(changeDir)) return
|
|
452
|
+
|
|
453
|
+
// 每个阶段完成后预期存在的文件
|
|
454
|
+
// brainstorm:scale=small(小变更)只必产 design.md;large/未标 scale → 四件套全。
|
|
455
|
+
// 与 validateBrainstormOutputs 的 BRAINSTORM_RULES condition(scale≠small)同源,避免对合法 small 变更
|
|
456
|
+
// 误报"⬜ proposal/requirements/tasks 未找到"(本检查仅 advisory 打印不 gate,但误导输出仍要消除)。
|
|
457
|
+
const brainstormExpected = readDesignScale(specBase, effectiveChange) === 'small'
|
|
458
|
+
? ['design.md']
|
|
459
|
+
: ['design.md', 'proposal.md', 'requirements.md', 'tasks.md']
|
|
460
|
+
const expectedFiles = {
|
|
461
|
+
brainstorm: brainstormExpected,
|
|
462
|
+
plan: ['plan.md'],
|
|
463
|
+
archive: ['module-impact.md'],
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const expected = expectedFiles[stageName]
|
|
467
|
+
if (!expected) return
|
|
468
|
+
|
|
469
|
+
const missing = []
|
|
470
|
+
for (const file of expected) {
|
|
471
|
+
if (!existsSync(join(changeDir, file))) {
|
|
472
|
+
missing.push(file)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (missing.length > 0) {
|
|
477
|
+
console.log(`\n⚠️ 文件位置验证:以下文件未在变更目录中找到`)
|
|
478
|
+
console.log(` 变更目录:${changeDir.replace(cwd + '/', '')}/`)
|
|
479
|
+
for (const f of missing) {
|
|
480
|
+
// 检查是否写到了错误的位置
|
|
481
|
+
const wrongPath = join(specBase, 'changes', 'change', effectiveChange, f)
|
|
482
|
+
if (existsSync(wrongPath)) {
|
|
483
|
+
console.log(` ❌ ${f} — 不存在,但发现了错误路径:${wrongPath.replace(cwd + '/', '')}`)
|
|
484
|
+
console.log(` 提示:应该写入 ${changeDir.replace(cwd + '/', '')}/${f}`)
|
|
485
|
+
} else {
|
|
486
|
+
console.log(` ⬜ ${f} — 未找到(该阶段可能未产出此文件)`)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
console.log(`\n✅ 文件位置验证:所有 ${expected.length} 个预期文件均在变更目录中`)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* 读本变更 design.md §6「文件变更清单」表格,提取声明交付的文件路径(concurrent 预检 ownFiles 源,D-002)。
|
|
496
|
+
*
|
|
497
|
+
* 用途:execute --done 并发预检在 in-place-fallback 模式下,需把本变更交付文件从 foreignFiles 排除
|
|
498
|
+
* (否则自己产出的脏文件会被当他者)。worktree 模式下交付文件不在主仓 git status,ownFiles 用空数组即可。
|
|
499
|
+
*
|
|
500
|
+
* 解析:定位 `## 6.` 段(到下一个 `## N.` 前),逐行扫表格行第 2 列的反引号文件路径,跳过表头/分隔行。
|
|
501
|
+
* 设计容错(design §9 / R-02):design.md 缺失 / 无 §6 / 解析空 → 返回 [],foreignFiles 退化为保守噪音,
|
|
502
|
+
* otherActiveChanges 仍可靠。
|
|
503
|
+
*
|
|
504
|
+
* @param {string} specBase 规范根(.sillyspec/ 所在)
|
|
505
|
+
* @param {string} changeName 当前变更名
|
|
506
|
+
* @returns {string[]} 本变更声明交付的文件路径(git 路径,正斜杠)
|
|
507
|
+
*/
|
|
508
|
+
function readDesignOwnFiles(specBase, changeName) {
|
|
509
|
+
if (!specBase || !changeName) return []
|
|
510
|
+
let content
|
|
511
|
+
try {
|
|
512
|
+
content = readFileSync(join(specBase, 'changes', changeName, 'design.md'), 'utf8')
|
|
513
|
+
} catch {
|
|
514
|
+
return []
|
|
515
|
+
}
|
|
516
|
+
const files = []
|
|
517
|
+
let inSection6 = false
|
|
518
|
+
for (const line of content.split('\n')) {
|
|
519
|
+
const heading = line.match(/^##\s+(\d+)\./)
|
|
520
|
+
if (heading) {
|
|
521
|
+
inSection6 = heading[1] === '6'
|
|
522
|
+
continue
|
|
523
|
+
}
|
|
524
|
+
if (!inSection6 || !line.startsWith('|')) continue
|
|
525
|
+
// 跳过表头分隔行(|---|---|)
|
|
526
|
+
if (/^\|[\s:|-]+\|?$/.test(line.trim())) continue
|
|
527
|
+
// 表格第 2 列反引号文件路径:`| 新增 | \`src/.../x.js\` | 说明 |`
|
|
528
|
+
const m = line.match(/^\|[^|]+\|\s*`([^`]+)`/)
|
|
529
|
+
if (m) files.push(m[1].trim())
|
|
530
|
+
}
|
|
531
|
+
return files
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* 阶段完成收尾共享管线(从 completeStep 抽出,消除 noAI 末步 / continueStep 完成分支绕过 gate 的
|
|
536
|
+
* S1/S2/S3 三处不对称)。调用方已自行标记 stageData.status='completed' 并 pm._write 落盘后调用本函数。
|
|
537
|
+
*
|
|
538
|
+
* 序列:handleScanStageCompleted → validateMetadata → validateFileLocations[completed‖skipped 守卫] →
|
|
539
|
+
* auxiliary 重置 → runStageCompletionGates[同守卫] → handleExecuteWorktreeCleanup。
|
|
540
|
+
*
|
|
541
|
+
* 关键陷阱(design §5.4):auxiliary 重置把 stageData.steps 换成 freshSteps(全 pending);下方
|
|
542
|
+
* runStageCompletionGates 守卫与 rollbackStageCompletion 必须用入参 steps(pre-reset 原数组),
|
|
543
|
+
* 不得重读 stageData.steps(否则计数恒 0 → gate 永跳过)。本函数全程用入参 steps,未重读。
|
|
544
|
+
*
|
|
545
|
+
* @returns {Promise<{stageCompleted:false,currentIdx,nextPendingIdx:number}|null>}
|
|
546
|
+
* null = 全部通过,调用方继续自管收尾(如下一步提示);
|
|
547
|
+
* 非 null = gate/handler 失败已 rollback,调用方直接 return。
|
|
548
|
+
*/
|
|
549
|
+
export async function completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText }) {
|
|
550
|
+
// 整体 try/catch(task-04 / review-2026-08-09 #2):收尾段(execute 预检 + scan handler + validate* +
|
|
551
|
+
// auxiliary 重置 + runStageCompletionGates)任一段抛非结构化异常 → rollbackCompletionAndReturn
|
|
552
|
+
// (回滚 in-progress + 落盘 + 返回未完成对象),不冒顶 exit 1。正常的 _scanResult/_gateEarlyReturn
|
|
553
|
+
// early-return 是 return 非 throw,不被 catch 拦;下方 handleExecuteWorktreeCleanup 在 try 外(副作用独立,失败不 rollback stage 状态)。
|
|
554
|
+
try {
|
|
555
|
+
// ── execute --done 并发他者改动预检(FR-06/FR-07,非阻断 advisory)──
|
|
556
|
+
// 仅 execute 触发,不影响 scan/plan/verify/archive 等 stage 的 completeStageGates。
|
|
557
|
+
// 纯副作用:console.warn 后照常推进,不改 stageData / 不阻断级联(FR-07)。
|
|
558
|
+
// 整个钩子 try/catch 兜底——任何异常吞掉,绝不影响 gate 级联(FR-07 不阻断铁律)。
|
|
559
|
+
if (stageName === 'execute') {
|
|
560
|
+
try {
|
|
561
|
+
// ownFiles 源(D-002 + B-002,钉死):动态 import WorktreeManager 取 meta.mode(复用上方
|
|
562
|
+
// Task Review Gate 的 ../worktree.js 动态 import 先例)。WorktreeManager 无 appliedFiles
|
|
563
|
+
// 字段(B-002 已证),不用模糊的「worktree applied 文件」。
|
|
564
|
+
// - worktree 模式(meta.mode 非 in-place-fallback):主仓 git status 看不见本变更交付文件
|
|
565
|
+
// → ownFiles=[](无害,dogfood 当前实际模式)
|
|
566
|
+
// - in-place-fallback 模式:交付文件就在主仓 dirty → ownFiles 读 design.md §6 文件清单排除
|
|
567
|
+
let ownFiles = []
|
|
568
|
+
// linkedChanges:execute 通常无(quick 管线概念,存 quick guard 文件);stageData 取不到则 []。
|
|
569
|
+
const linkedChanges = Array.isArray(stageData?.linkedChanges) ? stageData.linkedChanges : []
|
|
570
|
+
try {
|
|
571
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
572
|
+
const wm = new WorktreeManager({ cwd })
|
|
573
|
+
const meta = wm.getMeta(changeName)
|
|
574
|
+
if (meta?.mode === 'in-place-fallback') {
|
|
575
|
+
ownFiles = readDesignOwnFiles(specBase, changeName)
|
|
576
|
+
}
|
|
577
|
+
} catch {
|
|
578
|
+
// getMeta 抛(meta.json 缺 / worktree 损坏)→ ownFiles=[] 兜底,不崩(FR-07)。
|
|
579
|
+
ownFiles = []
|
|
580
|
+
}
|
|
581
|
+
const detected = detectConcurrentChanges(cwd, { changeName, linkedChanges, ownFiles })
|
|
582
|
+
const warn = formatConcurrentWarning(detected)
|
|
583
|
+
if (warn) console.warn(warn)
|
|
584
|
+
} catch {
|
|
585
|
+
// FR-07 不阻断:钩子任何意外异常静默吞掉,gate 级联照常推进。
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// scan 平台 manifest + post-check(S1 平台受害者:noAI scanPostcheck 末步 / continueStep 现也走这里)
|
|
590
|
+
const _scanResult = await handleScanStageCompleted({ stageName, currentIdx, cwd, progress, pm, stageData, changeName, outputText, platformOpts })
|
|
591
|
+
if (_scanResult) return _scanResult
|
|
592
|
+
|
|
593
|
+
// 守卫计数:completed‖skipped === total(修 S3);用入参 steps(pre-reset 原数组,design §5.4)
|
|
594
|
+
const settledCount = steps.filter(s => s.status === 'completed' || s.status === 'skipped').length
|
|
595
|
+
const total = steps.length
|
|
596
|
+
|
|
597
|
+
validateMetadata(cwd, stageName, specBase)
|
|
598
|
+
|
|
599
|
+
// 验证关键文件位置(仅当所有步骤已结案 completed‖skipped 时才校验)
|
|
600
|
+
if (settledCount === total && total > 0) {
|
|
601
|
+
validateFileLocations(cwd, stageName, progress, changeName, specBase)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 辅助阶段完成后重置步骤(scan 等 auxiliary 阶段重置回 pending 可重跑)
|
|
605
|
+
const stageDef = stageRegistry[stageName]
|
|
606
|
+
if (stageDef?.auxiliary) {
|
|
607
|
+
const freshSteps = (stageDef.steps || []).map(s => ({
|
|
608
|
+
name: s.name,
|
|
609
|
+
status: 'pending',
|
|
610
|
+
output: null,
|
|
611
|
+
completedAt: null
|
|
612
|
+
}))
|
|
613
|
+
stageData.steps = freshSteps
|
|
614
|
+
stageData.status = 'pending'
|
|
615
|
+
stageData.completedAt = null
|
|
616
|
+
if (progress.currentStage === stageName) progress.currentStage = ''
|
|
617
|
+
pm._write(cwd, progress, changeName)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// 阶段完成校验 gate 级联(runValidators → verify-test → Plan→Execute → Stage Review → Task Review)
|
|
621
|
+
if (settledCount === total && total > 0) {
|
|
622
|
+
const _gateEarlyReturn = await runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx })
|
|
623
|
+
if (_gateEarlyReturn) return _gateEarlyReturn
|
|
624
|
+
} else if (settledCount < total) {
|
|
625
|
+
console.log(`\n⚠️ 阶段校验跳过:${total} 步中仅 ${settledCount} 步标记为已结案(completed‖skipped),可能存在状态不同步。如确认阶段已完成,请运行 --status 确认。`)
|
|
626
|
+
}
|
|
627
|
+
} catch (e) {
|
|
628
|
+
console.error(`\n❌ ${stageName} 阶段完成收尾异常(已 rollback 为 in-progress,请修复后重新 --done):${e?.message || e}`)
|
|
629
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// execute worktree cleanup(completeStep / continueStep 完成分支统一调用,避免双清理)
|
|
633
|
+
await handleExecuteWorktreeCleanup({ stageName, changeName, cwd })
|
|
634
|
+
|
|
635
|
+
return null
|
|
636
|
+
}
|
|
637
|
+
|
package/src/run/prompt.js
CHANGED
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
import { basename, join } from 'node:path'
|
|
21
21
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
22
22
|
import { stageRegistry } from '../stages/index.js'
|
|
23
|
-
import { resolvePromptIncludes, safeGit, WAIT_MARKER_RE } from './shared.js'
|
|
23
|
+
import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, WAIT_MARKER_RE } from './shared.js'
|
|
24
24
|
import { renderStageContract } from '../stage-contract-spec.js'
|
|
25
|
+
import { parseModuleMapSimple } from '../modules.js'
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* 从 _module-map.yaml 读取模块上下文索引
|
|
@@ -36,6 +37,14 @@ function loadModuleContextIndex(specBase, projectName) {
|
|
|
36
37
|
const mapPath = join(specBase, 'docs', projectName, 'modules', '_module-map.yaml')
|
|
37
38
|
if (!existsSync(mapPath)) return null
|
|
38
39
|
const content = readFileSync(mapPath, 'utf8')
|
|
40
|
+
// schema_version 校验(advisory):modules.js 写 schema_version: 2;缺失或非 2 → warn,
|
|
41
|
+
// 提示 v1/v2 格式混用风险(旧格式字段解析会静默错位)。正常生成的文件不触发。
|
|
42
|
+
const sv = content.match(/^schema_version:\s*(\d+)/m)
|
|
43
|
+
if (!sv) {
|
|
44
|
+
console.warn(`⚠️ _module-map.yaml 缺少 schema_version 声明(期望 2),模块解析可能错位:${mapPath}`)
|
|
45
|
+
} else if (sv[1] !== '2') {
|
|
46
|
+
console.warn(`⚠️ _module-map.yaml schema_version=${sv[1]}(期望 2),可能为旧格式,模块解析可能错位:${mapPath}`)
|
|
47
|
+
}
|
|
39
48
|
return parseModuleMapSimple(content)
|
|
40
49
|
} catch {
|
|
41
50
|
return null
|
|
@@ -104,63 +113,8 @@ function buildModuleContextInjection(taskDescription, moduleIndex, specBase, pro
|
|
|
104
113
|
return injection
|
|
105
114
|
}
|
|
106
115
|
|
|
107
|
-
// 复用 modules.js
|
|
108
|
-
|
|
109
|
-
const modules = {}
|
|
110
|
-
let currentModule = null
|
|
111
|
-
let currentKey = null
|
|
112
|
-
let currentArray = null
|
|
113
|
-
|
|
114
|
-
for (const line of content.split('\n')) {
|
|
115
|
-
const moduleMatch = line.match(/^ ([a-zA-Z0-9_-]+):$/)
|
|
116
|
-
if (moduleMatch) {
|
|
117
|
-
if (currentArray && currentModule && currentKey) {
|
|
118
|
-
modules[currentModule][currentKey] = currentArray
|
|
119
|
-
}
|
|
120
|
-
currentModule = moduleMatch[1]
|
|
121
|
-
modules[currentModule] = {}
|
|
122
|
-
currentKey = null
|
|
123
|
-
currentArray = null
|
|
124
|
-
continue
|
|
125
|
-
}
|
|
126
|
-
if (!currentModule) continue
|
|
127
|
-
|
|
128
|
-
const arrayFieldMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons|core_files|test_files|related_docs|verify_commands):$/)
|
|
129
|
-
if (arrayFieldMatch) {
|
|
130
|
-
if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray
|
|
131
|
-
currentKey = arrayFieldMatch[1]
|
|
132
|
-
currentArray = []
|
|
133
|
-
continue
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const inlineArrayMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons|core_files|test_files|related_docs|verify_commands): \[(.*)\]$/)
|
|
137
|
-
if (inlineArrayMatch) {
|
|
138
|
-
if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray
|
|
139
|
-
const vals = inlineArrayMatch[2].split(',').map(v => v.trim()).filter(Boolean)
|
|
140
|
-
modules[currentModule][inlineArrayMatch[1]] = vals
|
|
141
|
-
currentKey = null
|
|
142
|
-
currentArray = null
|
|
143
|
-
continue
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const scalarMatch = line.match(/^ (status|doc|needs_review|role|risk_level): (.+)$/)
|
|
147
|
-
if (scalarMatch) {
|
|
148
|
-
if (currentArray && currentKey) { modules[currentModule][currentKey] = currentArray; currentArray = null; currentKey = null }
|
|
149
|
-
modules[currentModule][scalarMatch[1]] = scalarMatch[2]
|
|
150
|
-
continue
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const itemMatch = line.match(/^ - (.+)$/)
|
|
154
|
-
if (itemMatch && currentArray !== null) {
|
|
155
|
-
currentArray.push(itemMatch[1].trim())
|
|
156
|
-
continue
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (currentArray && currentModule && currentKey) {
|
|
160
|
-
modules[currentModule][currentKey] = currentArray
|
|
161
|
-
}
|
|
162
|
-
return modules
|
|
163
|
-
}
|
|
116
|
+
// parseModuleMapSimple 复用 modules.js 的 canonical 实现(合并历史 copy-paste 副本,2026-08-07;
|
|
117
|
+
// 无循环依赖:modules.js 仅 import fs/path/db.js,prompt.js → modules.js 单向)。
|
|
164
118
|
/**
|
|
165
119
|
* 输出当前步骤的 prompt
|
|
166
120
|
*/
|
|
@@ -450,7 +404,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
450
404
|
if (stageName === 'execute' && promptText.includes('{EXECUTE_RUN_ID}')) {
|
|
451
405
|
let runId = ''
|
|
452
406
|
const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
453
|
-
const runtimeRoot = platformOpts
|
|
407
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, execSpecBase)
|
|
454
408
|
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
455
409
|
try {
|
|
456
410
|
if (existsSync(runIdFile)) {
|
|
@@ -466,10 +420,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
466
420
|
promptText = promptText.replace(/\{EXECUTE_RUN_ID\}/g, runId)
|
|
467
421
|
}
|
|
468
422
|
|
|
469
|
-
// Stage Review Tier:brainstorm/plan/
|
|
423
|
+
// Stage Review Tier:brainstorm/plan/execute 阶段注入审查分级占位符
|
|
470
424
|
// (scanProfile 只在 scan 生效、change-risk-profile 只管 apply/verify,都不约束这些阶段的审查方式,
|
|
471
425
|
// 故按 plan_level / 变更文件数分级:self 当前 agent 自审,independent 强制独立子代理 + review.json)
|
|
472
|
-
if (['brainstorm', 'plan', '
|
|
426
|
+
if (['brainstorm', 'plan', 'execute'].includes(stageName) && promptText.includes('{REVIEW_TIER}')) {
|
|
473
427
|
try {
|
|
474
428
|
const { classifyReviewTier } = await import('../review-tier.js')
|
|
475
429
|
const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath } = await import('../stage-review.js')
|
|
@@ -488,7 +442,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
488
442
|
// reviewRunId:优先读 marker 复用(保证多次渲染 prompt 注入同一 ID == gate 读取的 ID,
|
|
489
443
|
// 修复「prompt 多次渲染 / 多次 review 时 gate 取错 ID 读错 review.json」),marker 缺失才
|
|
490
444
|
// generate + 落盘。对齐 execute {EXECUTE_RUN_ID} 段(prompt.js:449-467)。
|
|
491
|
-
const tierRuntimeRoot = platformOpts
|
|
445
|
+
const tierRuntimeRoot = resolveRuntimeRoot(platformOpts, tierSpecBase)
|
|
492
446
|
const reviewRunIdMarker = stageReviewMarkerPath(tierRuntimeRoot, stageName, changeName)
|
|
493
447
|
let reviewRunId = ''
|
|
494
448
|
try {
|
|
@@ -526,7 +480,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
526
480
|
try {
|
|
527
481
|
const { summarizeTaskCompletion } = await import('../task-review.js')
|
|
528
482
|
const tcrSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
529
|
-
const tcrRuntimeRoot = platformOpts
|
|
483
|
+
const tcrRuntimeRoot = resolveRuntimeRoot(platformOpts, tcrSpecBase)
|
|
530
484
|
const tcrChangeDir = changeName ? join(tcrSpecBase, 'changes', changeName) : null
|
|
531
485
|
const summary = tcrChangeDir
|
|
532
486
|
? summarizeTaskCompletion({ changeDir: tcrChangeDir, runtimeRoot: tcrRuntimeRoot, changeName })
|
|
@@ -585,11 +539,11 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
585
539
|
if (stepIndex === 0) {
|
|
586
540
|
console.log(`\n### ⚠️ 铁律`)
|
|
587
541
|
console.log('- 文档优先:代码产出必须先有对应的设计/规范文档支撑。')
|
|
588
|
-
console.log('-
|
|
589
|
-
console.log('-
|
|
590
|
-
console.log('-
|
|
591
|
-
console.log('-
|
|
592
|
-
console.log('-
|
|
542
|
+
console.log('- 聚焦本步骤:只执行本步骤描述的操作并完整做完;自行扩展或跳步会破坏状态机推进,后续步骤再做后续事。')
|
|
543
|
+
console.log('- 已完成步骤视为只读:需修订用 `sillyspec run <stage> --reopen --from-step N` 回退重做,直接回头改会让进度记录与产物脱节。')
|
|
544
|
+
console.log('- CLI 子命令以本 prompt 或上一条 --done 输出的字面为准;不确定时停下问用户,猜测命令会让状态机推进到错误位置。')
|
|
545
|
+
console.log('- 本步骤产物落盘后立即执行 prompt 末尾的 --done:CLI 据此校验产出并推进状态机;不跑则进度永远停在本步。')
|
|
546
|
+
console.log('- 变更目录改名用 `sillyspec change-rename <旧名> <新名>`:mv/rename 会漏改进度库引用,导致变更失联。')
|
|
593
547
|
console.log('- 文档类型文件(.md/.yaml/.json 等)头部必须包含 author(git 用户名)和 created_at(精确到秒)')
|
|
594
548
|
console.log('- 执行构建/测试前必须先读 local.yaml,优先使用其中配置的命令、路径和环境变量;未配置时才使用默认值')
|
|
595
549
|
}
|
package/src/run/quick-audit.js
CHANGED
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 路径修正(相对 src/run/):
|
|
8
8
|
* - resolveQuickLinkedChanges 的 quick-recommend.js 动态 import './quick-recommend.js' → '../quick-recommend.js'
|
|
9
|
-
* -
|
|
9
|
+
* - 脏文件信号改用 safeGit(带 safe.directory,Q3);不再 import execSync
|
|
10
10
|
* - parsePorcelainPath 从 ./shared.js 重 import;checkbox 顶部 import
|
|
11
11
|
*/
|
|
12
|
-
import { execSync } from 'node:child_process'
|
|
13
12
|
import { checkbox } from '@inquirer/prompts'
|
|
14
|
-
import { parsePorcelainPath } from './shared.js'
|
|
13
|
+
import { parsePorcelainPath, safeGit } from './shared.js'
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
16
|
* 打印 quick 完成审计结论(按 review.status 输出 SAFE/WARNING/BLOCKED)。
|
|
@@ -55,7 +54,7 @@ export function printQuickAuditReview(review) {
|
|
|
55
54
|
export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles, taskDescription, nonInteractive }) {
|
|
56
55
|
let activeChanges = []
|
|
57
56
|
try {
|
|
58
|
-
activeChanges =
|
|
57
|
+
activeChanges = pm.listChanges(cwd)
|
|
59
58
|
} catch {
|
|
60
59
|
activeChanges = []
|
|
61
60
|
}
|
|
@@ -67,15 +66,17 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
|
|
|
67
66
|
return []
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
//
|
|
69
|
+
// 脏文件(推荐信号之一)。safeGit 带 safe.directory,避免 linked worktree/容器/挂载点下裸
|
|
70
|
+
// `git status` 抛错致推荐信号静默丢失(multi-agent-review Q3 同类)。推荐非关键路径,失败回退 []。
|
|
71
|
+
// trim:false:porcelain 首行前导空格是状态码,trim 会削掉致 parsePorcelainPath 丢首字符。
|
|
71
72
|
let baselineFiles = []
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
baselineFiles =
|
|
73
|
+
const statusResult = safeGit(cwd, ['status', '--porcelain'], { trim: false })
|
|
74
|
+
if (!statusResult.error) {
|
|
75
|
+
baselineFiles = (statusResult.value || '').split('\n').filter(Boolean)
|
|
75
76
|
.map(l => parsePorcelainPath(l))
|
|
76
77
|
.filter(Boolean)
|
|
77
78
|
.filter(f => !f.startsWith('.sillyspec/'))
|
|
78
|
-
}
|
|
79
|
+
}
|
|
79
80
|
|
|
80
81
|
// 推荐打分(脏文件 + 任务描述双信号)
|
|
81
82
|
let recommendations = []
|