sillyspec 3.25.2 → 3.25.3
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/package.json +1 -1
- package/src/quicklog.js +11 -3
- package/src/run/complete-handlers.js +11 -2
- package/src/run/gates.js +1 -1
- package/src/run/prompt.js +16 -2
- package/src/run/shared.js +35 -23
- package/src/run.js +2 -2
- package/src/stage-review.js +34 -2
- package/src/stages/plan-postcheck.js +61 -3
- package/src/worktree-apply.js +8 -3
- package/src/worktree.js +8 -3
- package/.playwright-mcp/console-2026-07-27T15-45-50-621Z.log +0 -1
- package/.playwright-mcp/page-2026-07-27T15-45-51-459Z.yml +0 -144
- package/.playwright-mcp/prompt-final.png +0 -0
- package/.playwright-mcp/prompt-view.png +0 -0
package/package.json
CHANGED
package/src/quicklog.js
CHANGED
|
@@ -215,7 +215,7 @@ async function checkTaskCheckbox(specBase, change, qlId) {
|
|
|
215
215
|
// 可能是 CRLF,split('\n') 后每行带 \r。原代码 `lines[i] === '状态:进行中'` 精确匹配
|
|
216
216
|
// 恒失败 → 走 splice「兜底插入」→ 条目内同时出现「状态:已完成」+「状态:进行中」。
|
|
217
217
|
// 状态行匹配与「已完成」判断统一用行首前缀匹配,容忍行尾 \r;写入保持 CRLF 不扩大改动。
|
|
218
|
-
function flipEntryInContent(content, qlId, result) {
|
|
218
|
+
function flipEntryInContent(content, qlId, result, changedFiles = []) {
|
|
219
219
|
const lines = content.split('\n')
|
|
220
220
|
const startIdx = lines.findIndex(l => l.startsWith(`## ${qlId} |`))
|
|
221
221
|
if (startIdx === -1) return null
|
|
@@ -228,6 +228,12 @@ function flipEntryInContent(content, qlId, result) {
|
|
|
228
228
|
for (let i = startIdx + 1; i < endIdx; i++) {
|
|
229
229
|
if (/^状态:进行中\r?$/.test(lines[i])) { lines[i] = '状态:已完成'; flipped = true }
|
|
230
230
|
if (lines[i].startsWith('结果:')) hasResult = true
|
|
231
|
+
// 回填实际改动文件(调用方已用 isQuickMetadata 过滤 quick 自身元数据,只留业务文件)。
|
|
232
|
+
// 原地替换 lines[i],不改数组长度 → 与下方状态/结果 splice 的索引完全解耦。
|
|
233
|
+
// changedFiles 空(brownfield 无 guard / 调用方未传)→ 不动文件行,保持「(见实际改动)」。
|
|
234
|
+
if (changedFiles.length > 0 && lines[i].startsWith('文件:')) {
|
|
235
|
+
lines[i] = `文件:${changedFiles.join(', ')}`
|
|
236
|
+
}
|
|
231
237
|
}
|
|
232
238
|
if (!flipped && !lines.slice(startIdx, endIdx).some(l => /^状态:已完成\r?$/.test(l))) {
|
|
233
239
|
// 无状态行(异常),插一条已完成
|
|
@@ -297,11 +303,13 @@ export async function allocateQuicklogEntry(specBase, gitUser, { description, li
|
|
|
297
303
|
/**
|
|
298
304
|
* 翻某 qlId 条目为「已完成」+ 追加结果 + 勾选关联 tasks.md。持锁。
|
|
299
305
|
*/
|
|
300
|
-
export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultText = '', linkedChanges = [] } = {}) {
|
|
306
|
+
export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultText = '', linkedChanges = [], changedFiles = [] } = {}) {
|
|
301
307
|
const quicklogDir = join(specBase, 'quicklog')
|
|
302
308
|
const lockPath = join(quicklogDir, `.QUICKLOG-${(gitUser || 'unknown')}.md.lock`)
|
|
303
309
|
const result = sanitizeResult(resultText)
|
|
304
310
|
const linked = Array.isArray(linkedChanges) ? linkedChanges : []
|
|
311
|
+
// 实际改动文件(调用方 complete-handlers 已用 isQuickMetadata 过滤 quick 自身元数据)。空 → 不动文件行。
|
|
312
|
+
const realFiles = Array.isArray(changedFiles) ? changedFiles.filter(Boolean) : []
|
|
305
313
|
|
|
306
314
|
await withFileLock(lockPath, async () => {
|
|
307
315
|
// 条目可能在主文件或轮转归档中
|
|
@@ -309,7 +317,7 @@ export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultTex
|
|
|
309
317
|
const filePath = join(quicklogDir, f)
|
|
310
318
|
let content = ''
|
|
311
319
|
try { content = readFileSync(filePath, 'utf8') } catch { continue }
|
|
312
|
-
const updated = flipEntryInContent(content, qlId, result)
|
|
320
|
+
const updated = flipEntryInContent(content, qlId, result, realFiles)
|
|
313
321
|
if (updated !== null) {
|
|
314
322
|
await writeAtomic(filePath, updated) // 命中处原子落盘(只改含目标条目的那一个文件)
|
|
315
323
|
break
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { basename, join, resolve, relative, isAbsolute } from 'node:path'
|
|
24
24
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from 'node:fs'
|
|
25
25
|
import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
|
|
26
|
-
import { resolveChangeDir, safeGit, auditQuickCompletion, triggerSync } from './shared.js'
|
|
26
|
+
import { resolveChangeDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata } from './shared.js'
|
|
27
27
|
import { stageRegistry } from '../stages/index.js'
|
|
28
28
|
import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
|
|
29
29
|
import { printQuickAuditReview } from './quick-audit.js'
|
|
@@ -559,6 +559,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
559
559
|
const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
|
|
560
560
|
let qlId = guard?.quicklogId || null
|
|
561
561
|
const linkedChanges = Array.isArray(guard?.linkedChanges) ? guard.linkedChanges : []
|
|
562
|
+
// 审计结果(仅 guard 存在时填充)。提到 if(guard) 外声明,供下方回填 QUICKLOG 文件行复用
|
|
563
|
+
// review.changedFiles;brownfield 无 guard 时保持 null → 文件行不回填(降级,不报错)。
|
|
564
|
+
let review = null
|
|
562
565
|
|
|
563
566
|
// 审计:仅在有 guard 时跑(brownfield 无 guard 跳过,兼容 D-003 brownfield 行为)。
|
|
564
567
|
if (guard) {
|
|
@@ -570,7 +573,7 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
570
573
|
forceBaseline: guard.forceBaseline || isForceBaseline,
|
|
571
574
|
allowNew: guard.allowNew || isAllowNew,
|
|
572
575
|
}
|
|
573
|
-
|
|
576
|
+
review = await auditQuickCompletion(cwd, mergedGuard, { isConfirm: confirm })
|
|
574
577
|
printQuickAuditReview(review)
|
|
575
578
|
if (review.status === 'blocked') {
|
|
576
579
|
steps[currentIdx].status = 'pending'
|
|
@@ -632,9 +635,15 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
632
635
|
// 翻状态进行中→已完成 + 追加结果 + 勾选关联 tasks.md
|
|
633
636
|
// resultText 不再截断:结构化结果块(需求/根因/方案/结果)完整落盘,多行写成字段化块。
|
|
634
637
|
try {
|
|
638
|
+
// 回填实际改动文件:review.changedFiles 含 quick 自身元数据(quicklog/.runtime/modules 等),
|
|
639
|
+
// 用 isQuickMetadata 过滤掉,只留真实业务文件。brownfield 无 review → 空数组,文件行不动。
|
|
640
|
+
const realFiles = Array.isArray(review?.changedFiles)
|
|
641
|
+
? review.changedFiles.filter(f => !isQuickMetadata(f, linkedChanges))
|
|
642
|
+
: []
|
|
635
643
|
await completeQuicklogEntry(specBase, gitUser, qlId, {
|
|
636
644
|
resultText: outputText || '',
|
|
637
645
|
linkedChanges,
|
|
646
|
+
changedFiles: realFiles,
|
|
638
647
|
})
|
|
639
648
|
console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
|
|
640
649
|
} catch (e) {
|
package/src/run/gates.js
CHANGED
|
@@ -234,7 +234,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
234
234
|
if (tier.tier === 'self') {
|
|
235
235
|
console.log('\nℹ️ Stage Review: ' + stageName + ' tier=self(' + tier.reason + '),已降级为当前 agent 自审,不强制独立子代理。')
|
|
236
236
|
} else {
|
|
237
|
-
const reviewRunId = getLatestStageReviewRunId(runtimeRoot, stageName)
|
|
237
|
+
const reviewRunId = getLatestStageReviewRunId(runtimeRoot, stageName, changeName)
|
|
238
238
|
const reviewType = stageName === 'brainstorm' ? 'design'
|
|
239
239
|
: stageName === 'plan' ? 'plan'
|
|
240
240
|
: stageName === 'propose' ? 'proposal'
|
package/src/run/prompt.js
CHANGED
|
@@ -472,7 +472,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
472
472
|
if (['brainstorm', 'plan', 'propose', 'execute'].includes(stageName) && promptText.includes('{REVIEW_TIER}')) {
|
|
473
473
|
try {
|
|
474
474
|
const { classifyReviewTier } = await import('../review-tier.js')
|
|
475
|
-
const { generateStageReviewRunId, renderReviewJsonContract } = await import('../stage-review.js')
|
|
475
|
+
const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath } = await import('../stage-review.js')
|
|
476
476
|
const tierSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
477
477
|
const tierChangeDir = changeName ? join(tierSpecBase, 'changes', changeName) : null
|
|
478
478
|
const designPath = tierChangeDir ? join(tierChangeDir, 'design.md') : null
|
|
@@ -485,7 +485,21 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
487
|
const tier = classifyReviewTier({ planLevel, designPath })
|
|
488
|
-
|
|
488
|
+
// reviewRunId:优先读 marker 复用(保证多次渲染 prompt 注入同一 ID == gate 读取的 ID,
|
|
489
|
+
// 修复「prompt 多次渲染 / 多次 review 时 gate 取错 ID 读错 review.json」),marker 缺失才
|
|
490
|
+
// generate + 落盘。对齐 execute {EXECUTE_RUN_ID} 段(prompt.js:449-467)。
|
|
491
|
+
const tierRuntimeRoot = platformOpts?.runtimeRoot || join(tierSpecBase, '.runtime')
|
|
492
|
+
const reviewRunIdMarker = stageReviewMarkerPath(tierRuntimeRoot, stageName, changeName)
|
|
493
|
+
let reviewRunId = ''
|
|
494
|
+
try {
|
|
495
|
+
if (existsSync(reviewRunIdMarker)) {
|
|
496
|
+
reviewRunId = readFileSync(reviewRunIdMarker, 'utf8').trim()
|
|
497
|
+
}
|
|
498
|
+
} catch {}
|
|
499
|
+
if (!reviewRunId) {
|
|
500
|
+
reviewRunId = generateStageReviewRunId()
|
|
501
|
+
try { mkdirSync(tierRuntimeRoot, { recursive: true }); writeFileSync(reviewRunIdMarker, reviewRunId + '\n') } catch {}
|
|
502
|
+
}
|
|
489
503
|
// review.json 产物契约(schema + 示例 + docHash 算法 + 重算提示)事前注入,与 validateStageReviewSchema 同源
|
|
490
504
|
const reviewContractMd = renderReviewJsonContract({ stage: stageName, changeDir: tierChangeDir, reviewRunId, tier: tier.tier })
|
|
491
505
|
promptText = promptText
|
package/src/run/shared.js
CHANGED
|
@@ -188,6 +188,38 @@ export function parsePorcelainPath(line) {
|
|
|
188
188
|
return path.replace(/\\/g, '/')
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* quick 自身写入的 .sillyspec/ 元数据判定(auditQuickCompletion 审计 与 QUICKLOG「文件:」行回填
|
|
193
|
+
* 单源)。回填文件行时复用:review.changedFiles 含这些元数据时过滤掉,只留真实业务文件。
|
|
194
|
+
*
|
|
195
|
+
* 归类口径:
|
|
196
|
+
* - quicklog/.runtime/modules/_module-map/knowledge-uncategorized 等 quick 自身产物 → 元数据
|
|
197
|
+
* - .sillyspec/changes/:quick 自己没有该目录,其下文件要么是关联变更(reverse-sync),
|
|
198
|
+
* 要么是并发他者会话的工作。非关联变更目录整体视为元数据放行;关联变更文件不算(属本 quick 真实改动)
|
|
199
|
+
*
|
|
200
|
+
* 「并发工作 vs 偷建变更」的意图软判定留给 sillyhub,确定性校验只做路径归类。
|
|
201
|
+
*
|
|
202
|
+
* @param {string} p 文件路径(容错 \\ / 混用)
|
|
203
|
+
* @param {string[]} [linkedChanges] 关联变更名列表(影响 changes/ 归类)
|
|
204
|
+
* @returns {boolean} true=quick 元数据,应从业务文件列表过滤掉
|
|
205
|
+
*/
|
|
206
|
+
export function isQuickMetadata(p, linkedChanges = []) {
|
|
207
|
+
const normalizeGitPath = (x) => String(x).replace(/\\/g, '/')
|
|
208
|
+
const file = normalizeGitPath(p)
|
|
209
|
+
if (file.startsWith('.sillyspec/quicklog/')
|
|
210
|
+
|| file.startsWith('.sillyspec/.runtime/')
|
|
211
|
+
|| file === '.sillyspec/knowledge/uncategorized.md'
|
|
212
|
+
|| (/^\.sillyspec\/docs\/[^/]+\/modules\/[^/]+\.md$/.test(file))
|
|
213
|
+
|| (/^\.sillyspec\/docs\/[^/]+\/modules\/_module-map\.yaml$/.test(file))) return true
|
|
214
|
+
if (file.startsWith('.sillyspec/changes/')) {
|
|
215
|
+
const linkedChangeNames = new Set((Array.isArray(linkedChanges) ? linkedChanges : []).map(c => normalizeGitPath(c)))
|
|
216
|
+
const m = file.match(/^\.sillyspec\/changes\/([^/]+)(\/|$)/)
|
|
217
|
+
// bare 折叠 token(git 把全新未跟踪 changes/ 折叠成 `changes/`)或具体但非关联的变更 → 放行
|
|
218
|
+
if (!m || !linkedChangeNames.has(m[1])) return true
|
|
219
|
+
}
|
|
220
|
+
return false
|
|
221
|
+
}
|
|
222
|
+
|
|
191
223
|
/**
|
|
192
224
|
* quick 完成审计:对比 baseline 与实际变更。
|
|
193
225
|
* @returns {{ status: 'safe'|'warning'|'blocked', reasons: string[], changedFiles: string[], newFiles: string[], deletedFiles: string[], baselineHit: string[] }}
|
|
@@ -215,26 +247,6 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
215
247
|
const f = normalizeGitPath(p)
|
|
216
248
|
return baselineExact.has(f) || baselineDirs.some(d => f.startsWith(d))
|
|
217
249
|
}
|
|
218
|
-
// quick 自己没有 .sillyspec/changes/ 目录——该路径下任何文件要么是关联变更(reverse-sync),
|
|
219
|
-
// 要么是并发其他会话的变更工作。多会话并行时别人的 changes/ 不应被本 quick 审计拦截
|
|
220
|
-
// (确定性校验无法区分「并发工作」与「本 quick 偷建变更」,后者这类意图软判定留给 sillyhub)。
|
|
221
|
-
// 故:非关联变更目录整体放行;关联变更的文件仍走正常审计(reverse-sync 可见,需 --force-baseline)。
|
|
222
|
-
const linkedChangeNames = new Set((Array.isArray(guard.linkedChanges) ? guard.linkedChanges : []).map(c => normalizeGitPath(c)))
|
|
223
|
-
const isQuickMetadata = (p) => {
|
|
224
|
-
const file = normalizeGitPath(p)
|
|
225
|
-
if (file.startsWith('.sillyspec/quicklog/')
|
|
226
|
-
|| file.startsWith('.sillyspec/.runtime/')
|
|
227
|
-
|| file === '.sillyspec/knowledge/uncategorized.md'
|
|
228
|
-
|| (/^\.sillyspec\/docs\/[^/]+\/modules\/[^/]+\.md$/.test(file))
|
|
229
|
-
|| (/^\.sillyspec\/docs\/[^/]+\/modules\/_module-map\.yaml$/.test(file))) return true
|
|
230
|
-
// .sillyspec/changes/ 下:bare 折叠 token(git 把全新未跟踪 changes/ 折叠成 `changes/`)
|
|
231
|
-
// 或具体但【非关联】的变更 → 都属并发其他会话工作,放行;关联变更文件仍走审计。
|
|
232
|
-
if (file.startsWith('.sillyspec/changes/')) {
|
|
233
|
-
const m = file.match(/^\.sillyspec\/changes\/([^/]+)(\/|$)/)
|
|
234
|
-
if (!m || !linkedChangeNames.has(m[1])) return true
|
|
235
|
-
}
|
|
236
|
-
return false
|
|
237
|
-
}
|
|
238
250
|
const DANGEROUS_PATTERNS = [
|
|
239
251
|
'package.json',
|
|
240
252
|
'package-lock.json',
|
|
@@ -269,7 +281,7 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
269
281
|
}
|
|
270
282
|
|
|
271
283
|
// 检查危险文件(除非 force-baseline)
|
|
272
|
-
if (file.startsWith('.sillyspec/') && !isQuickMetadata(file) && !forceBaseline) {
|
|
284
|
+
if (file.startsWith('.sillyspec/') && !isQuickMetadata(file, guard.linkedChanges) && !forceBaseline) {
|
|
273
285
|
result.reasons.push(`危险文件变更: ${file}`)
|
|
274
286
|
}
|
|
275
287
|
|
|
@@ -293,7 +305,7 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
293
305
|
// 检查 new files(除非 allow-new)
|
|
294
306
|
if (!allowNew) {
|
|
295
307
|
for (const f of result.newFiles) {
|
|
296
|
-
if (!isQuickMetadata(f)) {
|
|
308
|
+
if (!isQuickMetadata(f, guard.linkedChanges)) {
|
|
297
309
|
result.reasons.push(`新增文件(需 --allow-new): ${f}`)
|
|
298
310
|
}
|
|
299
311
|
}
|
|
@@ -302,7 +314,7 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
302
314
|
// 检查 allowedFiles 范围
|
|
303
315
|
if (allowedFiles.length > 0) {
|
|
304
316
|
for (const f of result.changedFiles) {
|
|
305
|
-
if (!allowedFiles.includes(f) && !isQuickMetadata(f)) {
|
|
317
|
+
if (!allowedFiles.includes(f) && !isQuickMetadata(f, guard.linkedChanges)) {
|
|
306
318
|
result.reasons.push(`超出 allowedFiles: ${f}`)
|
|
307
319
|
}
|
|
308
320
|
}
|
package/src/run.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
// W6 Step8c: 主命令分发(runCommand)+ auto 模式 + ensureStageSteps 搬至 ./run/command.js
|
|
14
14
|
export { runCommand, ensureStageSteps } from './run/command.js'
|
|
15
|
-
// barrel re-export: parsePorcelainPath + auditQuickCompletion 被 test 直接 import(契约保留)
|
|
16
|
-
export { parsePorcelainPath, auditQuickCompletion } from './run/shared.js'
|
|
15
|
+
// barrel re-export: parsePorcelainPath + auditQuickCompletion + isQuickMetadata 被 test 直接 import(契约保留)
|
|
16
|
+
export { parsePorcelainPath, auditQuickCompletion, isQuickMetadata } from './run/shared.js'
|
|
17
17
|
// barrel re-export: applyRootPlaceholders 被 test/prompt-placeholders.test.mjs 直接 import(契约保留)
|
|
18
18
|
export { applyRootPlaceholders } from './run/prompt.js'
|
|
19
19
|
// barrel re-export: sanitizeProjectName + validateParsedProjects 被 test 直接 import(随 handleScan 搬走,契约保留)
|
package/src/stage-review.js
CHANGED
|
@@ -220,13 +220,45 @@ export function generateStageReviewRunId() {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
/**
|
|
223
|
-
*
|
|
223
|
+
* stage review marker 文件路径(prompt 渲染时写 / getLatestStageReviewRunId 读,同源)。
|
|
224
|
+
*
|
|
225
|
+
* 对齐 execute 的 current-execute-run-id-<change> 语义:含 change 防多 change 串台
|
|
226
|
+
* (同一 stage 不同变更的 review 各自独立 marker);changeName 缺失时退化为按 stage 分。
|
|
227
|
+
*
|
|
228
|
+
* @param {string} runtimeRoot - .runtime 绝对路径
|
|
229
|
+
* @param {string} stage - brainstorm|plan|propose|execute
|
|
230
|
+
* @param {string} [changeName] - 变更名(防多 change 串台)
|
|
231
|
+
* @returns {string} marker 文件绝对路径
|
|
232
|
+
*/
|
|
233
|
+
export function stageReviewMarkerPath(runtimeRoot, stage, changeName) {
|
|
234
|
+
const suffix = changeName ? `${stage}-${changeName}` : stage
|
|
235
|
+
return join(runtimeRoot, `current-stage-review-run-id-${suffix}`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 获取最新的 stage review run id
|
|
240
|
+
*
|
|
241
|
+
* 优先读 marker 文件(prompt 渲染 {REVIEW_TIER} 时写入,保证 gate 取的 ID == 注入给 agent
|
|
242
|
+
* 的 ID —— 修复「prompt 多次渲染 / 多次 review 时 gate 取错 ID 读错 review.json」),
|
|
243
|
+
* 对齐 getLatestExecuteRunId(task-review.js:420-428)。marker 缺失时 fallback 扫描
|
|
244
|
+
* stage-reviews/<stage>-review-* 目录取字典序最新(向后兼容无 marker 旧数据)。
|
|
224
245
|
*
|
|
225
246
|
* @param {string} runtimeRoot - .runtime 绝对路径
|
|
226
247
|
* @param {string} stage - brainstorm|plan|propose|execute
|
|
248
|
+
* @param {string} [changeName] - 变更名(读对应 marker,防多 change 串台)
|
|
227
249
|
* @returns {string|null} run id(如 'review-2026-07-16-143000'),无则 null
|
|
228
250
|
*/
|
|
229
|
-
export function getLatestStageReviewRunId(runtimeRoot, stage) {
|
|
251
|
+
export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
|
|
252
|
+
// 优先读 marker(prompt 写 / gate 读同源,对齐 execute current-execute-run-id 机制)
|
|
253
|
+
try {
|
|
254
|
+
const markerPath = stageReviewMarkerPath(runtimeRoot, stage, changeName)
|
|
255
|
+
if (existsSync(markerPath)) {
|
|
256
|
+
const content = readFileSync(markerPath, 'utf8').trim()
|
|
257
|
+
if (content) return content
|
|
258
|
+
}
|
|
259
|
+
} catch {}
|
|
260
|
+
|
|
261
|
+
// fallback:扫描 stage-reviews/<stage>-review-* 目录取字典序最新(向后兼容无 marker 旧数据)
|
|
230
262
|
const dir = join(runtimeRoot, 'stage-reviews')
|
|
231
263
|
if (!existsSync(dir)) return null
|
|
232
264
|
try {
|
|
@@ -202,6 +202,34 @@ export function topoSortWaves(depMap) {
|
|
|
202
202
|
return { waves, error: null }
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* 解析 plan.md 显式 Wave 分组 → Map<taskId, waveIndex>。
|
|
207
|
+
*
|
|
208
|
+
* Wave 口径与 execute.js parseWavesFromPlan 一致(execute 的真实并行单元,非 topoSort 建议值):
|
|
209
|
+
* - `## Wave N` 标题下的 task-XX checkbox 归入 Wave N;
|
|
210
|
+
* - 非 Wave 标题行(## 自检 等)退出当前 Wave 段,避免其 checkbox 混入。
|
|
211
|
+
* 返回 null = plan.md 不存在或无显式 Wave 标题 → execute 会把所有 task 放进单个隐式 Wave
|
|
212
|
+
* 全并行(execute.js:402-408),调用方据此判「同文件即冲突」。
|
|
213
|
+
*
|
|
214
|
+
* @param {string} planPath - plan.md 绝对路径
|
|
215
|
+
* @returns {Map<string, number>|null} taskId → waveIndex;null = 无显式 Wave
|
|
216
|
+
*/
|
|
217
|
+
function parseTaskWavesFromPlan(planPath) {
|
|
218
|
+
if (!existsSync(planPath)) return null
|
|
219
|
+
const content = readFileSync(planPath, 'utf8')
|
|
220
|
+
if (!/^#+\s*Wave\s+\d+/im.test(content)) return null
|
|
221
|
+
const map = new Map()
|
|
222
|
+
let currentWave = null
|
|
223
|
+
for (const line of content.split('\n')) {
|
|
224
|
+
const wm = line.match(/^#+\s*Wave\s+(\d+)/i)
|
|
225
|
+
if (wm) { currentWave = parseInt(wm[1], 10); continue }
|
|
226
|
+
if (/^#+\s/.test(line)) { currentWave = null; continue } // 非 Wave 标题退出当前 Wave 段
|
|
227
|
+
const tm = line.match(/^[-*]\s*\[[ x]\]\s*(task-\d+)/i)
|
|
228
|
+
if (tm && currentWave !== null) map.set(tm[1], currentWave)
|
|
229
|
+
}
|
|
230
|
+
return map
|
|
231
|
+
}
|
|
232
|
+
|
|
205
233
|
/**
|
|
206
234
|
* 本地一致性校验器
|
|
207
235
|
* @param {string} changeDir - 变更目录
|
|
@@ -258,10 +286,40 @@ export function validateBlueprintConsistency(changeDir) {
|
|
|
258
286
|
}
|
|
259
287
|
}
|
|
260
288
|
|
|
261
|
-
//
|
|
289
|
+
// 路径冲突(Wave 感知):同 Wave 内 >1 task 共享 allowed_path → execute 强制并行(execute.js:603)
|
|
290
|
+
// 子代理会互相覆盖该文件 → error。跨 Wave 同文件 → 串行执行安全 → warning。
|
|
291
|
+
// Wave 口径 = plan.md 显式 `## Wave N`(parseTaskWavesFromPlan,与 execute 同源,非 topoSort 建议值);
|
|
292
|
+
// plan.md 无显式 Wave → execute 全并行(隐式单 Wave,execute.js:402-408)→ 同文件即冲突。
|
|
293
|
+
const waveOfTask = parseTaskWavesFromPlan(pJoin(changeDir, 'plan.md'))
|
|
262
294
|
for (const [p, owners] of pathOwners) {
|
|
263
|
-
if (owners.length
|
|
264
|
-
|
|
295
|
+
if (owners.length < 2) continue
|
|
296
|
+
if (waveOfTask === null) {
|
|
297
|
+
errors.push(
|
|
298
|
+
`路径 ${p} 被 ${owners.length} 个 task 修改: ${owners.join(', ')} — plan.md 无显式 Wave 划分,` +
|
|
299
|
+
`execute 会把它们放进同一隐式 Wave 全并行(execute.js:402-408),子代理互相覆盖。` +
|
|
300
|
+
`解法:拆到不同 Wave(串行),或合并为单个 task。`
|
|
301
|
+
)
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
// 显式 Wave:按 wave 分组 owners,找同 Wave 冲突
|
|
305
|
+
const byWave = new Map()
|
|
306
|
+
for (const t of owners) {
|
|
307
|
+
const w = waveOfTask.get(t)
|
|
308
|
+
if (w === undefined) continue // 未列入任何 Wave → execute 不执行,不参与冲突判定
|
|
309
|
+
if (!byWave.has(w)) byWave.set(w, [])
|
|
310
|
+
byWave.get(w).push(t)
|
|
311
|
+
}
|
|
312
|
+
for (const [w, ts] of byWave) {
|
|
313
|
+
if (ts.length > 1) {
|
|
314
|
+
errors.push(
|
|
315
|
+
`路径 ${p} 被 Wave ${w} 内 ${ts.length} 个 task 修改: ${ts.join(', ')} — 同 Wave 任务 execute 强制并行(execute.js:603),` +
|
|
316
|
+
`子代理会互相覆盖该文件。解法:把它们拆到不同 Wave(串行)。`
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// 跨 Wave 共享:串行安全,仅提示
|
|
321
|
+
if (byWave.size > 1) {
|
|
322
|
+
warnings.push(`路径 ${p} 跨 Wave 被修改: ${owners.join(', ')}(不同 Wave 串行执行,安全;确认是否有意共享)`)
|
|
265
323
|
}
|
|
266
324
|
}
|
|
267
325
|
|
package/src/worktree-apply.js
CHANGED
|
@@ -185,11 +185,16 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
185
185
|
|
|
186
186
|
// --- 4.5 校验:主工作区 baseline 是否变化(防 execute 期间主工作区被修改)---
|
|
187
187
|
// 注意:必须和 computeBaselineHash (worktree.js) 使用相同的排除规则。
|
|
188
|
-
//
|
|
188
|
+
// 排除非交付物的元数据/文档 churn(execute 自身改的 + 多操作者常改的 agent 指引/文档),
|
|
189
|
+
// 否则别人改 CLAUDE.md/docs/.claude → 整树 hash 变 → apply 误阻断(多操作者仓库高频踩坑)。
|
|
190
|
+
// 安全:放过它们不丢真冲突——下方 step5a(未提交∩changedFiles)+step5b(HEAD blob) 对交付文件
|
|
191
|
+
// 做精确 per-file 校验,不读这里的 exclude。
|
|
189
192
|
// - .sillyspec/:plan/design 蓝图 + runtime 产物
|
|
190
|
-
// -
|
|
193
|
+
// - .claude/:agent 配置/skills/CLAUDE.md
|
|
194
|
+
// - docs/:文档(deliverable 文档冲突由 step5a/5b 兜底)
|
|
195
|
+
// - CLAUDE.md:根 agent 指引
|
|
191
196
|
if (meta.baselineHash) {
|
|
192
|
-
const exclude = '-- . ":(exclude).sillyspec/" ":(exclude)docs/
|
|
197
|
+
const exclude = '-- . ":(exclude).sillyspec/" ":(exclude).claude/" ":(exclude)docs/" ":(exclude)CLAUDE.md"';
|
|
193
198
|
const staged = gitQuiet(projectRoot, `diff --cached ${exclude}`) || '';
|
|
194
199
|
const unstaged = gitQuiet(projectRoot, `diff ${exclude}`) || '';
|
|
195
200
|
const untracked = gitQuiet(projectRoot, `ls-files --others --exclude-standard ${exclude}`) || '';
|
package/src/worktree.js
CHANGED
|
@@ -168,11 +168,16 @@ function sleepMs(ms) {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
export function computeBaselineHash(cwd) {
|
|
171
|
-
//
|
|
171
|
+
// 排除非交付物的元数据/文档 churn,避免多操作者仓库里别人改这些 → 整树 hash 变 → apply 误阻断
|
|
172
|
+
// (execute 自身也会改 .sillyspec/docs.sillyspec,否则每个 execute 收尾都因自身改动 BLOCKED)。
|
|
173
|
+
// 安全:coarse hash 放过这些不丢真冲突——applyWorktree step5a(未提交∩changedFiles)+step5b(HEAD blob)
|
|
174
|
+
// 对交付文件做精确 per-file 校验,不读这里的 exclude。
|
|
172
175
|
// - .sillyspec/:brainstorm/plan 蓝图 + runtime 产物
|
|
173
|
-
// -
|
|
176
|
+
// - .claude/:agent 配置/skills/CLAUDE.md(多操作者 agent 指引 churn)
|
|
177
|
+
// - docs/:文档(非代码交付物;deliverable 文档冲突由 step5a/5b 兜底)
|
|
178
|
+
// - CLAUDE.md:根 agent 指引(多操作者常改)
|
|
174
179
|
// 必须和 applyWorktree step 4.5 (worktree-apply.js) 使用相同的排除规则。
|
|
175
|
-
const exclude = '-- . ":(exclude).sillyspec/" ":(exclude)docs/
|
|
180
|
+
const exclude = '-- . ":(exclude).sillyspec/" ":(exclude).claude/" ":(exclude)docs/" ":(exclude)CLAUDE.md"';
|
|
176
181
|
const staged = gitQuiet(cwd, `diff --cached ${exclude}`) || '';
|
|
177
182
|
const unstaged = gitQuiet(cwd, `diff ${exclude}`) || '';
|
|
178
183
|
const untracked = gitQuiet(cwd, `ls-files --others --exclude-standard ${exclude}`) || '';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[ 809ms] [ERROR] Failed to load resource: the server responded with a status of 404 (File not found) @ http://localhost:8848/favicon.ico:0
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
- generic [active] [ref=e1]:
|
|
2
|
-
- banner [ref=e2]:
|
|
3
|
-
- generic [ref=e3]:
|
|
4
|
-
- generic [ref=e4]:
|
|
5
|
-
- text: Silly
|
|
6
|
-
- emphasis [ref=e5]: Spec
|
|
7
|
-
- generic [ref=e6]: Prompt Dossier
|
|
8
|
-
- generic [ref=e7]:
|
|
9
|
-
- text: ⌕
|
|
10
|
-
- textbox "搜索步骤名 / prompt 内容 / 占位符…" [ref=e8]
|
|
11
|
-
- generic [ref=e9]:
|
|
12
|
-
- generic [ref=e10]: 12 stages · 71 steps · 87k
|
|
13
|
-
- button "◐" [ref=e11] [cursor=pointer]
|
|
14
|
-
- generic [ref=e87]:
|
|
15
|
-
- navigation [ref=e88]:
|
|
16
|
-
- generic [ref=e89]:
|
|
17
|
-
- generic [ref=e90]:
|
|
18
|
-
- text: 主流程
|
|
19
|
-
- generic [ref=e91]: "4"
|
|
20
|
-
- generic [ref=e92] [cursor=pointer]:
|
|
21
|
-
- generic [ref=e93]: "01"
|
|
22
|
-
- generic [ref=e94]: 头脑风暴
|
|
23
|
-
- generic [ref=e95]: "8"
|
|
24
|
-
- generic [ref=e96] [cursor=pointer]:
|
|
25
|
-
- generic [ref=e97]: "02"
|
|
26
|
-
- generic [ref=e98]: 实现计划
|
|
27
|
-
- generic [ref=e99]: "5"
|
|
28
|
-
- generic [ref=e100] [cursor=pointer]:
|
|
29
|
-
- generic [ref=e101]: "03"
|
|
30
|
-
- generic [ref=e102]: 波次执行
|
|
31
|
-
- generic [ref=e103]: "12"
|
|
32
|
-
- generic [ref=e104] [cursor=pointer]:
|
|
33
|
-
- generic [ref=e105]: "04"
|
|
34
|
-
- generic [ref=e106]: 验证
|
|
35
|
-
- generic [ref=e107]: "7"
|
|
36
|
-
- generic [ref=e108]:
|
|
37
|
-
- generic [ref=e109]:
|
|
38
|
-
- text: 辅助
|
|
39
|
-
- generic [ref=e110]: "6"
|
|
40
|
-
- generic [ref=e111] [cursor=pointer]:
|
|
41
|
-
- generic [ref=e112]: "05"
|
|
42
|
-
- generic [ref=e113]: 项目扫描
|
|
43
|
-
- generic [ref=e114]: "11"
|
|
44
|
-
- generic [ref=e115] [cursor=pointer]:
|
|
45
|
-
- generic [ref=e116]: "06"
|
|
46
|
-
- generic [ref=e117]: 快速任务
|
|
47
|
-
- generic [ref=e118]: "3"
|
|
48
|
-
- generic [ref=e119] [cursor=pointer]:
|
|
49
|
-
- generic [ref=e120]: "07"
|
|
50
|
-
- generic [ref=e121]: 自由探索
|
|
51
|
-
- generic [ref=e122]: "1"
|
|
52
|
-
- generic [ref=e123] [cursor=pointer]:
|
|
53
|
-
- generic [ref=e124]: "08"
|
|
54
|
-
- generic [ref=e125]: 归档
|
|
55
|
-
- generic [ref=e126]: "5"
|
|
56
|
-
- generic [ref=e127] [cursor=pointer]:
|
|
57
|
-
- generic [ref=e128]: "09"
|
|
58
|
-
- generic [ref=e129]: 状态
|
|
59
|
-
- generic [ref=e130]: "3"
|
|
60
|
-
- generic [ref=e131] [cursor=pointer]:
|
|
61
|
-
- generic [ref=e132]: "10"
|
|
62
|
-
- generic [ref=e133]: 自检
|
|
63
|
-
- generic [ref=e134]: "5"
|
|
64
|
-
- generic [ref=e135]:
|
|
65
|
-
- generic [ref=e136]:
|
|
66
|
-
- text: 变体
|
|
67
|
-
- generic [ref=e137]: "1"
|
|
68
|
-
- generic [ref=e138] [cursor=pointer]:
|
|
69
|
-
- generic [ref=e139]: "11"
|
|
70
|
-
- generic [ref=e140]: 自动模式头脑风暴
|
|
71
|
-
- generic [ref=e141]: "4"
|
|
72
|
-
- generic [ref=e142]:
|
|
73
|
-
- generic [ref=e143]:
|
|
74
|
-
- text: 已废弃
|
|
75
|
-
- generic [ref=e144]: "1"
|
|
76
|
-
- generic [ref=e145] [cursor=pointer]:
|
|
77
|
-
- generic [ref=e146]: "12"
|
|
78
|
-
- generic [ref=e147]: propose · 已废弃
|
|
79
|
-
- generic [ref=e148]: "7"
|
|
80
|
-
- main [ref=e149]:
|
|
81
|
-
- generic [ref=e150]: 主流程 · STAGE 01
|
|
82
|
-
- heading "头脑风暴" [level=1] [ref=e151]
|
|
83
|
-
- paragraph [ref=e152]: 探索需求、分析技术方案、识别风险
|
|
84
|
-
- generic [ref=e153]:
|
|
85
|
-
- generic [ref=e154]: sourcesrc/stages/brainstorm.js
|
|
86
|
-
- generic [ref=e155]: persona资深架构师
|
|
87
|
-
- generic [ref=e156]: steps8
|
|
88
|
-
- generic [ref=e157]:
|
|
89
|
-
- button "全部展开" [ref=e158] [cursor=pointer]
|
|
90
|
-
- button "全部折叠" [ref=e159] [cursor=pointer]
|
|
91
|
-
- generic [ref=e160]: 8 STEPS
|
|
92
|
-
- generic [ref=e161]:
|
|
93
|
-
- article [ref=e162]:
|
|
94
|
-
- generic [ref=e163] [cursor=pointer]:
|
|
95
|
-
- generic [ref=e164]: 01 / 08
|
|
96
|
-
- generic [ref=e165]: 状态检查
|
|
97
|
-
- generic [ref=e166]: ▸
|
|
98
|
-
- article [ref=e167]:
|
|
99
|
-
- generic [ref=e168] [cursor=pointer]:
|
|
100
|
-
- generic [ref=e169]: 02 / 08
|
|
101
|
-
- generic [ref=e170]: 加载项目上下文
|
|
102
|
-
- generic [ref=e171]: ▸
|
|
103
|
-
- article [ref=e172]:
|
|
104
|
-
- generic [ref=e173] [cursor=pointer]:
|
|
105
|
-
- generic [ref=e174]: 03 / 08
|
|
106
|
-
- generic [ref=e175]: 对话式探索与需求澄清
|
|
107
|
-
- generic [ref=e176]:
|
|
108
|
-
- generic [ref=e177]: conditionalWait
|
|
109
|
-
- generic [ref=e178]: repeatable×8
|
|
110
|
-
- generic [ref=e179]: ▸
|
|
111
|
-
- article [ref=e180]:
|
|
112
|
-
- generic [ref=e181] [cursor=pointer]:
|
|
113
|
-
- generic [ref=e182]: 04 / 08
|
|
114
|
-
- generic [ref=e183]: 提出 2-3 种方案
|
|
115
|
-
- generic [ref=e184]: requiresWait
|
|
116
|
-
- generic [ref=e186]: ▸
|
|
117
|
-
- article [ref=e187]:
|
|
118
|
-
- generic [ref=e188] [cursor=pointer]:
|
|
119
|
-
- generic [ref=e189]: 05 / 08
|
|
120
|
-
- generic [ref=e190]: 分段展示设计
|
|
121
|
-
- generic [ref=e191]: requiresWait
|
|
122
|
-
- generic [ref=e193]: ▸
|
|
123
|
-
- article [ref=e194]:
|
|
124
|
-
- generic [ref=e195] [cursor=pointer]:
|
|
125
|
-
- generic [ref=e196]: 06 / 08
|
|
126
|
-
- generic [ref=e197]: 写设计文档并自审
|
|
127
|
-
- generic [ref=e198]: ▸
|
|
128
|
-
- article [ref=e199]:
|
|
129
|
-
- generic [ref=e200] [cursor=pointer]:
|
|
130
|
-
- generic [ref=e201]: 07 / 08
|
|
131
|
-
- generic [ref=e202]: Design Grill 交叉审查
|
|
132
|
-
- generic [ref=e203]: conditionalWait
|
|
133
|
-
- generic [ref=e205]: ▸
|
|
134
|
-
- article [ref=e206]:
|
|
135
|
-
- generic [ref=e207] [cursor=pointer]:
|
|
136
|
-
- generic [ref=e208]: 08 / 08
|
|
137
|
-
- generic [ref=e209]: 用户确认并生成规范文件
|
|
138
|
-
- generic [ref=e210]: requiresWait
|
|
139
|
-
- generic [ref=e212]: ▸
|
|
140
|
-
- contentinfo [ref=e213]:
|
|
141
|
-
- text: 数据机械提取自
|
|
142
|
-
- code [ref=e214]: src/stages/*.js
|
|
143
|
-
- text: · 单文件自包含 · file:// 可直接打开 · 生成命令:
|
|
144
|
-
- code [ref=e215]: node docs/prompt/_build-site.mjs
|
|
Binary file
|
|
Binary file
|