sillyspec 3.25.9 → 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-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-quick/SKILL.md +3 -1
- package/.claude/skills/sillyspec-verify/SKILL.md +1 -1
- package/.sillyspec-platform-cleaned +1 -0
- package/.sillyspec-platform.json +7 -0
- 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 +170 -161
- package/src/dispatch/backends/local-agent.js +152 -0
- package/src/dispatch/backends/sillyhub-mcp.js +242 -0
- package/src/dispatch/probe.js +210 -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 +326 -36
- package/src/init.js +7 -14
- package/src/local-detect.js +120 -110
- 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 +85 -70
- package/src/progress/consistency-doctor.js +71 -14
- package/src/progress/shared.js +8 -5
- package/src/progress/stage-machine.js +67 -65
- package/src/progress/step-store.js +72 -67
- package/src/progress.js +448 -236
- package/src/quicklog.js +59 -12
- package/src/run/command.js +78 -39
- package/src/run/complete-handlers.js +1028 -889
- package/src/run/complete.js +132 -212
- package/src/run/concurrent-detect.js +132 -0
- package/src/run/gates.js +302 -19
- package/src/run/prompt.js +22 -68
- package/src/run/quick-audit.js +10 -9
- package/src/run/shared.js +722 -600
- package/src/run/stage.js +43 -24
- package/src/scan-postcheck.js +9 -31
- package/src/sillyhub-mcp/client.js +394 -0
- package/src/sillyhub-mcp/config.js +62 -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 +118 -10
- 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 +10 -7
- package/src/stages/execute.js +64 -22
- package/src/stages/plan-postcheck.js +152 -0
- package/src/stages/plan.js +16 -31
- package/src/stages/quick.js +25 -9
- package/src/stages/scan.js +53 -8
- package/src/stages/status.js +0 -1
- package/src/stages/verify.js +14 -9
- package/src/sync.js +433 -16
- package/src/task-review.js +59 -1
- package/src/verify-postcheck.js +938 -862
- package/src/version.js +19 -0
- package/src/worktree-apply.js +218 -93
- 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/quicklog.js
CHANGED
|
@@ -117,6 +117,36 @@ function sanitizeResult(resultText) {
|
|
|
117
117
|
return String(resultText || '').trim()
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// ── quick step3 --file-notes 旁路通道 ──
|
|
121
|
+
// flag 在 command.js 解析(--file-notes),completeQuicklogEntry 在 quick 收尾时消费。
|
|
122
|
+
// 不经 completeStep → handleQuickStageCompletion → completeQuicklogEntry 三层透传(会扩 3 处签名,
|
|
123
|
+
// 且 quick 收尾路径是多 agent 并发改热点);用 per-process setter,CLI 单进程生命周期内有效,
|
|
124
|
+
// completeQuicklogEntry 读后即清。无 --file-notes 时为 '' → 回退 changedFiles 单行(向后兼容)。
|
|
125
|
+
let _pendingFileNotes = ''
|
|
126
|
+
export function setQuickFileNotes(raw) { _pendingFileNotes = raw == null ? '' : String(raw) }
|
|
127
|
+
export function getQuickFileNotes() { return _pendingFileNotes }
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 解析 --file-notes 原文 → [{path, note}]。
|
|
131
|
+
* 格式:「path1::括注1 || path2::括注2」——`||` 分隔条目,`::` 分隔路径与括注(首个 `::` 为界,
|
|
132
|
+
* 括注内含 `::` 不误切);路径反斜杠归一正斜杠(匹配 git 路径风格)。无 `::` 的段 → note=''。
|
|
133
|
+
* 空 / 全空段 → [](调用方回退 changedFiles 单行)。导出供测试 + 未来直接调用。
|
|
134
|
+
*/
|
|
135
|
+
export function parseFileNotes(raw) {
|
|
136
|
+
if (!raw) return []
|
|
137
|
+
return String(raw)
|
|
138
|
+
.split('||')
|
|
139
|
+
.map((seg) => {
|
|
140
|
+
const s = seg.trim()
|
|
141
|
+
if (!s) return null
|
|
142
|
+
const idx = s.indexOf('::')
|
|
143
|
+
const path = (idx === -1 ? s : s.slice(0, idx)).replace(/\\/g, '/').trim()
|
|
144
|
+
const note = idx === -1 ? '' : s.slice(idx + 2).trim()
|
|
145
|
+
return path ? { path, note } : null
|
|
146
|
+
})
|
|
147
|
+
.filter(Boolean)
|
|
148
|
+
}
|
|
149
|
+
|
|
120
150
|
// 结果块必填字段(quick step3 --done --output 的结构化结果模板,见 src/stages/quick.js)。
|
|
121
151
|
// 4 个必填字段标签从 manifest 同源(stage-contract-spec.js quick.result-labels),prompt 事前契约与本校验单源。
|
|
122
152
|
const RESULT_REQUIRED_LABELS = getRule('quick.result-labels').data.literals
|
|
@@ -284,7 +314,7 @@ function splitSingleLineFields(body) {
|
|
|
284
314
|
return scanFields(body, findBoundaryLabel) ?? scanFields(body, (b, l, f) => b.indexOf(l, f))
|
|
285
315
|
}
|
|
286
316
|
|
|
287
|
-
function flipEntryInContent(content, qlId, result, changedFiles = []) {
|
|
317
|
+
function flipEntryInContent(content, qlId, result, changedFiles = [], fileNotes = []) {
|
|
288
318
|
const lines = content.split('\n')
|
|
289
319
|
const startIdx = lines.findIndex(l => l.startsWith(`## ${qlId} |`))
|
|
290
320
|
if (startIdx === -1) return null
|
|
@@ -303,11 +333,16 @@ function flipEntryInContent(content, qlId, result, changedFiles = []) {
|
|
|
303
333
|
for (let i = startIdx + 1; i < endIdx; i++) {
|
|
304
334
|
if (/^状态:进行中\r?$/.test(lines[i])) { lines[i] = '状态:已完成'; flipped = true }
|
|
305
335
|
if (lines[i].startsWith('结果:')) hasResult = true
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
if (
|
|
310
|
-
|
|
336
|
+
// 文件行落盘:--file-notes 优先(多行 bullet 带括注),否则回填审计到的实际改动文件单行,
|
|
337
|
+
// 都空则不动(保持「(见实际改动)」)。bullet 用「数组元素内嵌 \n」写——lines.join('\n') 展平为
|
|
338
|
+
// 多行,数组长度不变 → 与下方状态/结果 splice 的索引完全解耦(changedFiles 单行同理原地替换)。
|
|
339
|
+
if (lines[i].startsWith('文件:')) {
|
|
340
|
+
if (fileNotes.length > 0) {
|
|
341
|
+
const bullets = fileNotes.map((n) => `- ${n.path}${n.note ? `(${n.note})` : ''}`)
|
|
342
|
+
lines[i] = `文件:\n${bullets.join('\n')}`
|
|
343
|
+
} else if (changedFiles.length > 0) {
|
|
344
|
+
lines[i] = `文件:${changedFiles.join(', ')}`
|
|
345
|
+
}
|
|
311
346
|
}
|
|
312
347
|
}
|
|
313
348
|
if (!flipped && !lines.slice(startIdx, endIdx).some(l => /^状态:已完成\r?$/.test(l))) {
|
|
@@ -330,11 +365,19 @@ function flipEntryInContent(content, qlId, result, changedFiles = []) {
|
|
|
330
365
|
}
|
|
331
366
|
// 多行结果(结构化 需求/根因/方案/结果 字段块):逐行插入为独立字段行。
|
|
332
367
|
// 单行结果:保持「结果:<一句话>」一行,向后兼容简单用例。
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
368
|
+
const resultLines = /\r?\n/.test(body)
|
|
369
|
+
? body.split(/\r?\n/).filter(l => l.trim() !== '')
|
|
370
|
+
: [`结果:${body}`]
|
|
371
|
+
// 结果块属本条目:从 endIdx 往前跳过本条目尾部空行,在最后一个非空行之后插入。
|
|
372
|
+
// 否则结果块会落在尾空行「之后」,与下一条目标题紧贴(缺空行分隔),且被空行从本条目
|
|
373
|
+
// 「文件:」行隔开——视觉上像属于下一条目(用户实证多条目 QUICKLOG 间距 bug)。
|
|
374
|
+
let insertAt = endIdx
|
|
375
|
+
while (insertAt - 1 > startIdx && lines[insertAt - 1].trim() === '') insertAt--
|
|
376
|
+
lines.splice(insertAt, 0, ...resultLines)
|
|
377
|
+
// 兜底:结果块之后、下一个 ## 标题之间若无空行(条目原本无尾空行 / 轮转归档手改等),补一个。
|
|
378
|
+
const after = insertAt + resultLines.length
|
|
379
|
+
if (after < lines.length && lines[after].startsWith('## ')) {
|
|
380
|
+
lines.splice(after, 0, '')
|
|
338
381
|
}
|
|
339
382
|
}
|
|
340
383
|
return lines.join('\n')
|
|
@@ -397,6 +440,10 @@ export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultTex
|
|
|
397
440
|
const linked = Array.isArray(linkedChanges) ? linkedChanges : []
|
|
398
441
|
// 实际改动文件(调用方 complete-handlers 已用 isQuickMetadata 过滤 quick 自身元数据)。空 → 不动文件行。
|
|
399
442
|
const realFiles = Array.isArray(changedFiles) ? changedFiles.filter(Boolean) : []
|
|
443
|
+
// --file-notes(command.js 经 setQuickFileNotes 注入):有则文件行写多行 bullet 带括注。
|
|
444
|
+
// 读后即清(per-process,防残留跨调用)。flipEntryInContent 优先用 fileNotes,空则回退 realFiles。
|
|
445
|
+
const fileNotes = parseFileNotes(_pendingFileNotes)
|
|
446
|
+
_pendingFileNotes = ''
|
|
400
447
|
|
|
401
448
|
await withFileLock(lockPath, async () => {
|
|
402
449
|
// 条目可能在主文件或轮转归档中
|
|
@@ -404,7 +451,7 @@ export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultTex
|
|
|
404
451
|
const filePath = join(quicklogDir, f)
|
|
405
452
|
let content = ''
|
|
406
453
|
try { content = readFileSync(filePath, 'utf8') } catch { continue }
|
|
407
|
-
const updated = flipEntryInContent(content, qlId, result, realFiles)
|
|
454
|
+
const updated = flipEntryInContent(content, qlId, result, realFiles, fileNotes)
|
|
408
455
|
if (updated !== null) {
|
|
409
456
|
await writeAtomic(filePath, updated) // 命中处原子落盘(只改含目标条目的那一个文件)
|
|
410
457
|
break
|
package/src/run/command.js
CHANGED
|
@@ -23,7 +23,7 @@ import { join, resolve, dirname } from 'node:path'
|
|
|
23
23
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
|
|
24
24
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
25
25
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
26
|
-
import { resolveSpecDir, countAncestorSpecDirs, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift } from './shared.js'
|
|
26
|
+
import { resolveSpecDir, countAncestorSpecDirs, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot } from './shared.js'
|
|
27
27
|
import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
28
28
|
import { outputStep } from './prompt.js'
|
|
29
29
|
import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
|
|
@@ -32,6 +32,7 @@ import { ProgressManager } from '../progress.js'
|
|
|
32
32
|
import { validateChangeExists } from '../stage-contract.js'
|
|
33
33
|
import { stageRegistry, auxiliaryStages } from '../stages/index.js'
|
|
34
34
|
import { definition as brainstormAutoDef } from '../stages/brainstorm-auto.js'
|
|
35
|
+
import { setQuickFileNotes } from '../quicklog.js'
|
|
35
36
|
|
|
36
37
|
// F2/F4: 哪些 flag 吃下一个 token 作「值」(其余 --flag 都是布尔,不吞值)。
|
|
37
38
|
// 校验循环只对 VALUE_FLAGS 跳下一个 token(否则 --done 后跟 typo 会被当 --done 的值吞掉)。
|
|
@@ -40,7 +41,7 @@ const VALUE_FLAGS = new Set([
|
|
|
40
41
|
'--reason', '--options', '--answer', '--confirm-mode',
|
|
41
42
|
'--output', '--input', '--change', '--linked-changes',
|
|
42
43
|
'--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
|
|
43
|
-
'--files', '--from-step', '--mode', '--dir', '--confirm-mode',
|
|
44
|
+
'--files', '--file-notes', '--from-step', '--mode', '--dir', '--confirm-mode',
|
|
44
45
|
])
|
|
45
46
|
|
|
46
47
|
/**
|
|
@@ -214,7 +215,6 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
214
215
|
const waitReason = getFlagValue('--reason')
|
|
215
216
|
const waitOptions = getFlagValue('--options')
|
|
216
217
|
const continueAnswer = getFlagValue('--answer')
|
|
217
|
-
const confirmMode = getFlagValue('--confirm-mode')
|
|
218
218
|
const resolvedSpecDir = specDir || getFlagValue('--spec-dir') || getFlagValue('--spec-root')
|
|
219
219
|
const platformOpts = {
|
|
220
220
|
specRoot: resolvedSpecDir ? resolve(resolvedSpecDir) : null,
|
|
@@ -291,7 +291,8 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
291
291
|
|
|
292
292
|
// 统一规范基路径:平台模式用 specRoot,本地模式用 cwd/.sillyspec
|
|
293
293
|
// runCommand 后续所有 .sillyspec/ 操作必须用 specBase
|
|
294
|
-
|
|
294
|
+
// let:下方 worktree 副本漂移守卫命中时锚回 wt.mainSpecBase(task-05/D-03)
|
|
295
|
+
let specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
295
296
|
|
|
296
297
|
// 漂移提醒:cwd 祖先链 ≥2 个 .sillyspec = monorepo 多实例,当前命中的「最近」实例
|
|
297
298
|
// 可能不是用户意图的项目(如 cd 进被独立 scan 的子项目跑测试后忘回根)。
|
|
@@ -407,6 +408,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
407
408
|
// 3. 仍读不到 → 生成新 UUID(兼容旧行为;进度可能命中空行,由后续 pm.read 兜底)
|
|
408
409
|
const QUICK_SID_RE = /^quick-[0-9a-f]{8}$/
|
|
409
410
|
let quickSessionId = null
|
|
411
|
+
let quickFallbackUsed = false // Q7: --done 未带 --change 时 fallback 读 current-quick-run-id 命中(区别于显式 --change quick-<hex>)
|
|
410
412
|
if (stageName === 'quick') {
|
|
411
413
|
// 1373-1376 已把 quick 的 --change 值清进 linkedChanges、changeName 置 null。
|
|
412
414
|
// 此处回看 --change 原始值:若恰好是单个 quick-<8hex> → 识别为本会话 sessionId(精确恢复),
|
|
@@ -423,11 +425,11 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
423
425
|
const isDoneLike = isDone || isStatus || isSkip || isReset || isReopen
|
|
424
426
|
if (isDoneLike) {
|
|
425
427
|
try {
|
|
426
|
-
const runtimeRoot = platformOpts
|
|
428
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specRoot)
|
|
427
429
|
const idFile = join(runtimeRoot, 'current-quick-run-id')
|
|
428
430
|
if (existsSync(idFile)) {
|
|
429
431
|
const v = readFileSync(idFile, 'utf8').trim()
|
|
430
|
-
if (QUICK_SID_RE.test(v)) quickSessionId = v
|
|
432
|
+
if (QUICK_SID_RE.test(v)) { quickSessionId = v; quickFallbackUsed = true }
|
|
431
433
|
}
|
|
432
434
|
} catch {}
|
|
433
435
|
}
|
|
@@ -448,6 +450,16 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
448
450
|
quickFiles = flags[filesIdx + 1].split(',').map(f => f.trim()).filter(Boolean)
|
|
449
451
|
}
|
|
450
452
|
|
|
453
|
+
// 解析 --file-notes "path::括注 || path::括注"(quick 专用:QUICKLOG「文件:」行落盘多行括注)。
|
|
454
|
+
// 旁路注入 quicklog.js(per-process setter),不经 complete 收尾路径透传(避碰多 agent 并发改的收尾热点)。
|
|
455
|
+
// completeQuicklogEntry 读后即清;不传 → '' → 回填审计到的实际文件单行(向后兼容)。
|
|
456
|
+
let quickFileNotes = ''
|
|
457
|
+
const fileNotesIdx = flags.indexOf('--file-notes')
|
|
458
|
+
if (fileNotesIdx !== -1 && flags[fileNotesIdx + 1]) {
|
|
459
|
+
quickFileNotes = flags[fileNotesIdx + 1]
|
|
460
|
+
}
|
|
461
|
+
setQuickFileNotes(quickFileNotes)
|
|
462
|
+
|
|
451
463
|
const isAllowNew = flags.includes('--allow-new')
|
|
452
464
|
const isForceBaseline = flags.includes('--force-baseline')
|
|
453
465
|
const isForceRescan = flags.includes('--force-rescan')
|
|
@@ -459,7 +471,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
459
471
|
'--reason', '--options', '--answer', '--confirm-mode',
|
|
460
472
|
'--output', '--input', '--change', '--linked-changes',
|
|
461
473
|
'--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
|
|
462
|
-
'--files', '--allow-new', '--force-baseline', '--force-rescan',
|
|
474
|
+
'--files', '--file-notes', '--allow-new', '--force-baseline', '--force-rescan',
|
|
463
475
|
'--json', '--dir', '--help',
|
|
464
476
|
'--reopen', '--from-step', '--mode',
|
|
465
477
|
'--deep', '--quick', '--standard', // scan profile 三档显式选择(scan-profile.js 从 argv 读;互斥见下方 PROFILE_FLAGS 检测)
|
|
@@ -484,7 +496,8 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
484
496
|
const isAuxiliary = auxiliaryStages.includes(stageName)
|
|
485
497
|
// scan 元数据追踪(存储在 stageData.scanMeta 中,completeStep 通过 progress 访问)
|
|
486
498
|
|
|
487
|
-
|
|
499
|
+
// let:下方 worktree 副本漂移守卫命中时用主仓 specRoot 重建 pm(task-05/D-03)
|
|
500
|
+
let pm = new ProgressManager({ specDir: specRoot })
|
|
488
501
|
|
|
489
502
|
// quick 阶段:确定关联变更
|
|
490
503
|
// 优先级:--linked-changes / --change 显式 > 已持久化 quick-guard.json(--done 复用)> 交互式 > 非交互 fallback
|
|
@@ -522,18 +535,29 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
522
535
|
process.exit(2) // 用法错(execute 必须显式 --change)→ exit 2
|
|
523
536
|
}
|
|
524
537
|
|
|
525
|
-
// worktree
|
|
526
|
-
// checkout 副本
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
//
|
|
538
|
+
// worktree 副本漂移自动锚定守卫(坑 worktree-execute-spec-drift,D-03@v1):specBase 命中 worktree 内
|
|
539
|
+
// .sillyspec checkout 副本 = agent 误 cd 进 worktree 跑 plan/execute/verify/archive(100% 误操作场景)。
|
|
540
|
+
// 不 exit:自动把 specBase 锚回主仓 wt.mainSpecBase 后继续——进度/产出落主仓,不再写分裂副本(副本随
|
|
541
|
+
// 工作树清理丢失)。必须在 validateChangeExists 之前:副本里 change 目录真实存在,存在性校验会被骗
|
|
542
|
+
// 放行;锚定后用主仓 specBase 复查。覆盖 plan/execute/verify/archive(validateChangeExists 同 Set)。
|
|
543
|
+
// 关键坑:连带重写 specBase/specRoot/specDir + 重建 pm——四者同源派生(pm._customSpecDir 持旧 specRoot),
|
|
544
|
+
// 只改 specBase 会让下游 validateChangeExists(specBase) 与 pm.read(走 pm._customSpecDir) 仍指副本。
|
|
545
|
+
// 平台模式/显式 --spec-dir 跳过(已明确指定,与 line~285 warn / quick 守卫条件对齐)。
|
|
546
|
+
// 其他漂移(下方 changeMissing 守卫 / quick session drift 守卫)非副本场景,仍 exit(2) 不自动纠正。
|
|
530
547
|
if (!platformOpts.specRoot && !specDir
|
|
531
548
|
&& ['plan', 'execute', 'verify', 'archive'].includes(stageName)) {
|
|
532
549
|
const wt = detectWorktreeSpecDrift(specBase)
|
|
533
550
|
if (wt) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
551
|
+
specBase = wt.mainSpecBase
|
|
552
|
+
specRoot = wt.mainSpecBase
|
|
553
|
+
specDir = wt.mainSpecBase
|
|
554
|
+
pm = new ProgressManager({ specDir: specRoot })
|
|
555
|
+
// 【坑 execute-runs-isolation】下游 runtimeRoot 解析(execute-runs/stage-reviews 落点)经
|
|
556
|
+
// platformOpts 透传读此锚点,落主仓 .runtime,不随 worktree cleanup 整目录删消失。
|
|
557
|
+
// D-02:只参与 runtimeRoot 解析(resolveRuntimeRoot),绝不设 specRoot/runtimeRoot——
|
|
558
|
+
// 否则触发平台 sentinel 副作用(triggerSync/checkApproval 跳过 + prompt 误进平台渲染分支)。
|
|
559
|
+
platformOpts.specDriftAnchor = wt.mainSpecBase
|
|
560
|
+
console.warn(`⚠️ 已自动锚定主仓 spec:${wt.mainSpecBase}(原 cwd 命中 worktree 副本 ${wt.changeName},已纠正,流程继续)`)
|
|
537
561
|
}
|
|
538
562
|
}
|
|
539
563
|
|
|
@@ -564,13 +588,13 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
564
588
|
}
|
|
565
589
|
}
|
|
566
590
|
|
|
567
|
-
let progress =
|
|
591
|
+
let progress = pm.read(cwd, changeName)
|
|
568
592
|
|
|
569
593
|
if (!progress) {
|
|
570
594
|
// 如果指定了变更名或有变更目录,自动初始化变更的 progress
|
|
571
595
|
const autoChange = changeName || resolveChangeNameAuto(cwd, specRoot)
|
|
572
596
|
if (autoChange) {
|
|
573
|
-
progress =
|
|
597
|
+
progress = pm.initChange(cwd, autoChange)
|
|
574
598
|
} else if (isAuxiliary) {
|
|
575
599
|
let autoName = changeName || resolveChangeNameAuto(cwd, specRoot) || 'default'
|
|
576
600
|
// archive 特例:归档后变更从活跃列表排除(listChanges WHERE status='active'),
|
|
@@ -585,14 +609,14 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
585
609
|
.pop()
|
|
586
610
|
if (latest) {
|
|
587
611
|
autoName = latest.replace(/^\d{4}-\d{2}-\d{2}-/, '')
|
|
588
|
-
progress =
|
|
612
|
+
progress = pm.read(cwd, autoName)
|
|
589
613
|
}
|
|
590
614
|
}
|
|
591
615
|
} catch {}
|
|
592
616
|
}
|
|
593
617
|
changeName = autoName
|
|
594
618
|
if (!progress) {
|
|
595
|
-
progress =
|
|
619
|
+
progress = pm.initChange(cwd, autoName)
|
|
596
620
|
// initChange 可能因 project 表为空返回 null
|
|
597
621
|
if (!progress) {
|
|
598
622
|
progress = { currentStage: stageName, stages: {}, lastActive: new Date().toLocaleString('zh-CN', { hour12: false }), project: '' }
|
|
@@ -612,7 +636,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
612
636
|
console.log(`🔄 自动创建变更:${autoName}`)
|
|
613
637
|
console.log(` 提示:可以用 --change <名称> 指定自定义变更名`)
|
|
614
638
|
console.log(` 或事后重命名:sillyspec change-rename ${autoName} <新名称>`)
|
|
615
|
-
progress =
|
|
639
|
+
progress = pm.initChange(cwd, autoName)
|
|
616
640
|
changeName = autoName
|
|
617
641
|
} else {
|
|
618
642
|
console.error('❌ 未找到进度数据,请先运行 sillyspec init 或指定 --change <变更名>')
|
|
@@ -624,6 +648,21 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
624
648
|
// 确保 progress 有 currentChange
|
|
625
649
|
const effectiveChange = changeName || progress.currentChange || resolveChangeName(cwd, progress, specRoot)
|
|
626
650
|
|
|
651
|
+
// ── Q7:quick --done 不带 --change 时 fallback 读 current-quick-run-id 可能命中他者会话 ──
|
|
652
|
+
// 并发两 quick 会话:B 后启动覆盖 A 的 id(last-writer-wins),A 的 --done 不带 --change → 读到 B 的 sessionId
|
|
653
|
+
// → 误操作 B 的 progress/QUICKLOG。fallback 命中的会话若已完成或无可推进步骤(id stale / 他者已收尾),
|
|
654
|
+
// 拒绝推进并要求显式 --change。(live A/B race 需 per-session 锚 commit,见 review Q2/D2,本守卫只兜 stale/completed。)
|
|
655
|
+
if (stageName === 'quick' && isDone && quickFallbackUsed) {
|
|
656
|
+
const qs = progress.stages.quick
|
|
657
|
+
const hasActionable = qs?.steps && qs.steps.some(s => s.status === 'pending' || s.status === 'waiting' || s.status === 'in-progress')
|
|
658
|
+
if (!qs || qs.status === 'completed' || !hasActionable) {
|
|
659
|
+
console.error(`\n❌ --done 未带 --change,回退读到的 quick 会话 ${changeName} 已不可推进(已完成或无待办步骤)。`)
|
|
660
|
+
console.error(` 并发多 quick 会话时 .runtime/current-quick-run-id 可能指向他者会话(last-writer-wins)。`)
|
|
661
|
+
console.error(` 请显式带 --change <quick-session-id> 指定要完成的会话(sillyspec run quick --status 查各会话进度)。`)
|
|
662
|
+
process.exit(2) // 用法错(fallback 命中 stale/他者会话)→ exit 2
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
627
666
|
// -- auto 模式:自动推进所有流程阶段
|
|
628
667
|
if (stageName === 'auto') {
|
|
629
668
|
return await runAutoMode(pm, progress, cwd, flags, effectiveChange, platformOpts)
|
|
@@ -632,7 +671,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
632
671
|
// --change 只作为变更名标识,不再拦截流程
|
|
633
672
|
// 注册变更到全局活跃列表(如果尚未注册)
|
|
634
673
|
if (effectiveChange) {
|
|
635
|
-
|
|
674
|
+
pm.registerChange(cwd, effectiveChange)
|
|
636
675
|
}
|
|
637
676
|
|
|
638
677
|
// --reset
|
|
@@ -669,12 +708,12 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
669
708
|
if (freshSteps && freshSteps.length > 0) {
|
|
670
709
|
if (!progress.stages[stageName]) progress.stages[stageName] = { status: 'stale', steps: [] }
|
|
671
710
|
progress.stages[stageName].steps = freshSteps.map(s => ({ name: s.name, status: 'pending' }))
|
|
672
|
-
|
|
673
|
-
progress =
|
|
711
|
+
pm._write(cwd, progress, effectiveChange)
|
|
712
|
+
progress = pm.read(cwd, effectiveChange) || progress
|
|
674
713
|
}
|
|
675
714
|
}
|
|
676
715
|
|
|
677
|
-
const result =
|
|
716
|
+
const result = pm.reopenStage(cwd, stageName, {
|
|
678
717
|
fromStep: fromStepValue,
|
|
679
718
|
changeName: effectiveChange,
|
|
680
719
|
})
|
|
@@ -692,7 +731,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
692
731
|
console.log('')
|
|
693
732
|
|
|
694
733
|
// 重新读取 progress
|
|
695
|
-
progress =
|
|
734
|
+
progress = pm.read(cwd, effectiveChange) || progress
|
|
696
735
|
|
|
697
736
|
// 注入 revision context 到 platformOpts,供 outputStep 使用
|
|
698
737
|
const stageData = progress.stages[stageName]
|
|
@@ -724,7 +763,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
724
763
|
// currentIdx === -1 自动重置(~1944 行);in-progress 输出当前 step;全新由
|
|
725
764
|
// ensureStageSteps 初始化;显式从头用 --reset(已由 !isReset 排除出本分支)。
|
|
726
765
|
try {
|
|
727
|
-
const runtimeRoot = platformOpts
|
|
766
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specRoot)
|
|
728
767
|
mkdirSync(runtimeRoot, { recursive: true })
|
|
729
768
|
writeAtomicSync(join(runtimeRoot, 'current-quick-run-id'), quickSessionId + '\n')
|
|
730
769
|
} catch (e) {
|
|
@@ -740,9 +779,9 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
740
779
|
// 确保步骤已初始化
|
|
741
780
|
const changed = await ensureStageSteps(progress, stageName, cwd, specRoot)
|
|
742
781
|
if (changed && effectiveChange) {
|
|
743
|
-
|
|
782
|
+
pm._write(cwd, progress, effectiveChange)
|
|
744
783
|
triggerSync(cwd, effectiveChange, platformOpts)
|
|
745
|
-
progress =
|
|
784
|
+
progress = pm.read(cwd, effectiveChange) || progress
|
|
746
785
|
}
|
|
747
786
|
|
|
748
787
|
// --status
|
|
@@ -772,7 +811,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
772
811
|
// --done
|
|
773
812
|
if (isDone) {
|
|
774
813
|
const doneAnswer = getFlagValue('--answer')
|
|
775
|
-
return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts,
|
|
814
|
+
return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, doneAnswer, isForceBaseline, isAllowNew })
|
|
776
815
|
}
|
|
777
816
|
|
|
778
817
|
// 默认:输出当前步骤
|
|
@@ -898,7 +937,7 @@ async function resetStage(pm, progress, stageName, cwd, changeName, platformOpts
|
|
|
898
937
|
steps: defSteps ? defSteps.map(s => ({ name: s.name, status: 'pending' })) : []
|
|
899
938
|
}
|
|
900
939
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
901
|
-
|
|
940
|
+
pm._write(cwd, progress, changeName)
|
|
902
941
|
triggerSync(cwd, changeName, platformOpts)
|
|
903
942
|
console.log(`🔄 ${stageName} 阶段已重置`)
|
|
904
943
|
}
|
|
@@ -948,18 +987,18 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
948
987
|
completedAt: null,
|
|
949
988
|
steps: brainstormAutoDef.steps.map(s => ({ name: s.name, status: 'pending' }))
|
|
950
989
|
}
|
|
951
|
-
|
|
990
|
+
pm._write(cwd, progress, changeName)
|
|
952
991
|
triggerSync(cwd, changeName, platformOpts)
|
|
953
|
-
progress =
|
|
992
|
+
progress = pm.read(cwd, changeName)
|
|
954
993
|
return progress
|
|
955
994
|
}
|
|
956
995
|
}
|
|
957
996
|
const changed = await ensureStageSteps(progress, stage, cwd)
|
|
958
997
|
if (stageChanged || changed) {
|
|
959
|
-
|
|
998
|
+
pm._write(cwd, progress, changeName)
|
|
960
999
|
triggerSync(cwd, changeName, platformOpts)
|
|
961
1000
|
}
|
|
962
|
-
progress =
|
|
1001
|
+
progress = pm.read(cwd, changeName)
|
|
963
1002
|
return progress
|
|
964
1003
|
}
|
|
965
1004
|
|
|
@@ -1051,7 +1090,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1051
1090
|
|
|
1052
1091
|
const result = await completeStep(pm, progress, currentStage, cwd, outputText, inputText, { printNext: false, changeName, platformOpts })
|
|
1053
1092
|
if (!result) return
|
|
1054
|
-
progress =
|
|
1093
|
+
progress = pm.read(cwd, changeName)
|
|
1055
1094
|
|
|
1056
1095
|
const nextPendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
|
|
1057
1096
|
if (nextPendingIdx !== -1) {
|
|
@@ -1084,7 +1123,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1084
1123
|
if (currentStage === 'brainstorm' && next === 'plan') {
|
|
1085
1124
|
const changeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot || null)
|
|
1086
1125
|
if (changeDir) {
|
|
1087
|
-
const nextActionFile = join(changeDir, '
|
|
1126
|
+
const nextActionFile = join(changeDir, 'next-action.json')
|
|
1088
1127
|
try {
|
|
1089
1128
|
const nextAction = JSON.parse(readFileSync(nextActionFile, 'utf8'))
|
|
1090
1129
|
if (nextAction.has_blocking_questions === true) {
|
|
@@ -1115,9 +1154,9 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1115
1154
|
}
|
|
1116
1155
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1117
1156
|
await ensureStageSteps(progress, next, cwd)
|
|
1118
|
-
|
|
1157
|
+
pm._write(cwd, progress, changeName)
|
|
1119
1158
|
triggerSync(cwd, changeName, platformOpts)
|
|
1120
|
-
progress =
|
|
1159
|
+
progress = pm.read(cwd, changeName)
|
|
1121
1160
|
|
|
1122
1161
|
console.log(`\n${currentStage} complete. Auto advanced to ${next}.`)
|
|
1123
1162
|
const nextSteps = await getAutoSteps(next)
|