sillyspec 3.26.11 → 3.26.14
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-archive/SKILL.md +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-knowledge/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +5 -5
- package/.claude/skills/sillyspec-quick/SKILL.md +5 -1
- package/README.md +7 -5
- package/SKILL.md +5 -4
- package/package.json +1 -1
- package/packages/dashboard/server/executor.js +10 -1
- package/packages/dashboard/server/index.js +108 -46
- package/src/config-schema.js +3 -0
- package/src/contract-matrix.js +22 -13
- package/src/db.js +24 -4
- package/src/dispatch/probe.js +99 -63
- package/src/docs-check.js +58 -13
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +208 -21
- package/src/fs-atomic.js +3 -1
- package/src/git-helper.js +7 -4
- package/src/hooks/worktree-guard.js +8 -2
- package/src/index.js +200 -27
- package/src/init.js +3 -2
- package/src/local-detect.js +9 -1
- package/src/modules.js +7 -5
- package/src/progress/stage-machine.js +589 -578
- package/src/progress/step-store.js +1 -1
- package/src/progress.js +187 -30
- package/src/quicklog.js +47 -16
- package/src/run/command.js +88 -46
- package/src/run/complete-handlers.js +27 -15
- package/src/run/complete.js +1186 -1121
- package/src/run/concurrent-detect.js +3 -1
- package/src/run/gates.js +172 -25
- package/src/run/prompt.js +17 -9
- package/src/run/scan-profile.js +35 -23
- package/src/run/shared.js +164 -63
- package/src/run/stage.js +41 -1
- package/src/setup.js +8 -10
- package/src/sillyhub-mcp/client.js +74 -6
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +43 -8
- package/src/stage-contract.js +12 -38
- package/src/stage-review.js +40 -1
- package/src/stages/archive.js +4 -3
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +27 -13
- package/src/stages/doctor.js +29 -16
- package/src/stages/execute.js +170 -154
- package/src/stages/knowledge.js +18 -3
- package/src/stages/plan-postcheck.js +72 -37
- package/src/stages/plan.js +70 -48
- package/src/stages/quick.js +3 -1
- package/src/stages/verify.js +1 -1
- package/src/sync.js +339 -225
- package/src/task-review.js +281 -64
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +48 -35
- package/src/worktree-apply.js +58 -34
- package/src/worktree-deps.js +120 -20
- package/src/worktree.js +55 -9
- package/.npmrc-publish +0 -2
package/src/run/command.js
CHANGED
|
@@ -23,7 +23,7 @@ import { basename, 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, resolveRuntimeRoot, writePlatformPointer, checkPlatformManaged, PLATFORM_MANAGED_FILENAME } from './shared.js'
|
|
26
|
+
import { resolveSpecDir, countAncestorSpecDirs, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, writePlatformPointer, checkPlatformManaged, PLATFORM_MANAGED_FILENAME } 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'
|
|
@@ -108,6 +108,11 @@ export async function ensureStageSteps(progress, stageName, cwd, specDir = null)
|
|
|
108
108
|
// 检查步骤数量是否匹配(execute 动态步骤可能变化)
|
|
109
109
|
if (progress.stages[stageName].steps.length !== steps.length) {
|
|
110
110
|
const oldSteps = progress.stages[stageName].steps
|
|
111
|
+
// 漂移显式化(坑 execute-step-table-drift,2026-08-20 实证):plan.md Wave 数在 execute
|
|
112
|
+
// 中途被改后 DB 步骤表与重算定义错位(17/12 交替),重播种本身按名保留完成态是对的,
|
|
113
|
+
// 但此前完全静默——agent 只见各处计数打架的报错,不知道发生了什么、该不该继续
|
|
114
|
+
console.warn(`⚠️ ${stageName} 步骤表与当前定义漂移(DB ${oldSteps.length} 步 → 定义 ${steps.length} 步,常见原因:execute 中途修改了 plan.md 的 Wave 结构)`)
|
|
115
|
+
console.warn(` 已按步骤名保留完成态重播种(新增步骤置 pending;Wave 重编号时同名步骤继承旧状态,勾选真相以 tasks.md/review.json 为准)`)
|
|
111
116
|
progress.stages[stageName].steps = steps.map(s => {
|
|
112
117
|
const old = oldSteps.find(step => step.name === s.name)
|
|
113
118
|
if (old) return old // 精确名命中 → 保留旧状态
|
|
@@ -195,10 +200,13 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
195
200
|
// ── cwd 纠正:向上查找真实项目根 ──
|
|
196
201
|
// 防止多 project 工作区中 cwd 停在子目录(如 backend/)时
|
|
197
202
|
// 状态写入子目录下误建的 .sillyspec,导致状态分裂
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
// HUB-12c:守卫须同时查平台接管声明——指针被 cleanup、声明还在、祖先有别的 .sillyspec
|
|
204
|
+
//(monorepo 嵌套)时,cwd 被上移后 checkPlatformManaged 在新 cwd 扑空 → 静默落本地库,
|
|
205
|
+
// 双入口 fail-closed 被绕过(状态分裂正是它要防的)
|
|
206
|
+
if (!specDir && !existsSync(join(cwd, '.sillyspec-platform.json')) && !checkPlatformManaged(cwd)) {
|
|
207
|
+
// 平台模式(cwd 有 .sillyspec-platform.json 指针或接管声明)不做 cwd 纠正:指针已明确
|
|
208
|
+
// specDir,向上找 .sillyspec 反而会撞到无关项目(如用户 home 的 .sillyspec),导致
|
|
209
|
+
// --done 恢复读写 ~/.sillyspec-platform.json 出错的 specRoot。
|
|
202
210
|
const resolvedRoot = resolveSpecDir(cwd)
|
|
203
211
|
if (resolvedRoot && resolvedRoot !== join(cwd, '.sillyspec')) {
|
|
204
212
|
const realRoot = dirname(resolvedRoot)
|
|
@@ -360,8 +368,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
360
368
|
// 纯只读比较:零会话/QUICKLOG 副作用,仅解析 --base/--full/--report 后转发 scan-diff 退出码。
|
|
361
369
|
if (stageName === 'scan' && flags.includes('--diff')) {
|
|
362
370
|
const { runScanDiff } = await import('../scan-diff.js')
|
|
363
|
-
const
|
|
364
|
-
const diffBase = diffBaseIdx >= 0 && flags[diffBaseIdx + 1] ? flags[diffBaseIdx + 1] : null
|
|
371
|
+
const diffBase = getFlagValue('--base')
|
|
365
372
|
process.exitCode = runScanDiff({
|
|
366
373
|
projectRoot: cwd,
|
|
367
374
|
specBase,
|
|
@@ -410,26 +417,26 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
410
417
|
}
|
|
411
418
|
|
|
412
419
|
// 解析 --output
|
|
420
|
+
// F4 守卫统一走 getFlagValue(坑 raw-flag-value-bypass):裸 flags[idx+1] 会把漏值后紧跟的
|
|
421
|
+
// flag 名当值(--output --input x → output="--input"),--change --done 甚至会建出名为
|
|
422
|
+
// --done 的变更目录(assertSafeChangeName 的 [\w.-] 恰好放行)。getFlagValue 的 F4 注释
|
|
423
|
+
// 点名过本坑,此前只有 from-step 等少数 flag 用了它。
|
|
413
424
|
let outputText = null
|
|
414
|
-
const
|
|
415
|
-
if (
|
|
416
|
-
outputText = flags[outputIdx + 1]
|
|
417
|
-
}
|
|
425
|
+
const outputValue = getFlagValue('--output')
|
|
426
|
+
if (outputValue !== null) outputText = outputValue
|
|
418
427
|
|
|
419
428
|
// 解析 --input
|
|
420
429
|
let inputText = null
|
|
421
|
-
const
|
|
422
|
-
if (
|
|
423
|
-
inputText = flags[inputIdx + 1]
|
|
424
|
-
}
|
|
430
|
+
const inputValue = getFlagValue('--input')
|
|
431
|
+
if (inputValue !== null) inputText = inputValue
|
|
425
432
|
|
|
426
433
|
// 解析 --linked-changes <a,b|none>(quick 专用:显式声明关联变更,CI/脚本友好)
|
|
427
434
|
// 与 --change 解耦:--linked-changes 语义清晰(关联变更),不与「指定变更名」混淆。
|
|
428
435
|
// null = 未指定(走持久化/交互/兼容回退);[] = 显式 none(不关联);[...] = 显式列表
|
|
429
436
|
let explicitLinked = null
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
432
|
-
const v =
|
|
437
|
+
const linkedValue = getFlagValue('--linked-changes')
|
|
438
|
+
if (linkedValue !== null) {
|
|
439
|
+
const v = linkedValue.trim()
|
|
433
440
|
explicitLinked = v.toLowerCase() === 'none' ? [] : v.split(',').map(s => s.trim()).filter(Boolean)
|
|
434
441
|
}
|
|
435
442
|
|
|
@@ -437,9 +444,9 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
437
444
|
// 历史写法,语义与「指定变更名」冲突,新用法建议改用 --linked-changes)
|
|
438
445
|
let changeName = null
|
|
439
446
|
let linkedChanges = []
|
|
440
|
-
const
|
|
441
|
-
if (
|
|
442
|
-
changeName =
|
|
447
|
+
const changeValue = getFlagValue('--change')
|
|
448
|
+
if (changeValue !== null) {
|
|
449
|
+
changeName = changeValue
|
|
443
450
|
if (stageName === 'quick') {
|
|
444
451
|
linkedChanges = changeName.split(',').map(s => s.trim()).filter(Boolean)
|
|
445
452
|
changeName = null
|
|
@@ -481,7 +488,9 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
481
488
|
// 1373-1376 已把 quick 的 --change 值清进 linkedChanges、changeName 置 null。
|
|
482
489
|
// 此处回看 --change 原始值:若恰好是单个 quick-<8hex> → 识别为本会话 sessionId(精确恢复),
|
|
483
490
|
// 并撤销把它当 linkedChanges 的误判。多值或不匹配 → 维持 linkedChanges 语义(旧兼容)。
|
|
484
|
-
|
|
491
|
+
// F4 守卫同款(changeIdx 变量已随裸读清理移除,此处改 getFlagValue 复读原始值)
|
|
492
|
+
const rawChangeValue = getFlagValue('--change')
|
|
493
|
+
const rawChange = rawChangeValue !== null ? rawChangeValue.trim() : null
|
|
485
494
|
const rawIsSingleSid = rawChange && rawChange.indexOf(',') === -1 && QUICK_SID_RE.test(rawChange)
|
|
486
495
|
if (rawIsSingleSid) {
|
|
487
496
|
// --done --change quick-<uuid8>:精确恢复(撤销 1373-1376 把它误当 linkedChanges)
|
|
@@ -541,18 +550,18 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
541
550
|
process.exit(2) // 用法错 → exit 2
|
|
542
551
|
}
|
|
543
552
|
let quickFiles = []
|
|
544
|
-
const
|
|
545
|
-
if (
|
|
546
|
-
quickFiles =
|
|
553
|
+
const filesValue = getFlagValue('--files')
|
|
554
|
+
if (filesValue !== null) {
|
|
555
|
+
quickFiles = filesValue.split(',').map(f => f.trim()).filter(Boolean)
|
|
547
556
|
}
|
|
548
557
|
|
|
549
558
|
// 解析 --file-notes "path::括注 || path::括注"(quick 专用:QUICKLOG「文件:」行落盘多行括注)。
|
|
550
559
|
// 旁路注入 quicklog.js(per-process setter),不经 complete 收尾路径透传(避碰多 agent 并发改的收尾热点)。
|
|
551
560
|
// completeQuicklogEntry 读后即清;不传 → '' → 回填审计到的实际文件单行(向后兼容)。
|
|
552
561
|
let quickFileNotes = ''
|
|
553
|
-
const
|
|
554
|
-
if (
|
|
555
|
-
quickFileNotes =
|
|
562
|
+
const fileNotesValue = getFlagValue('--file-notes')
|
|
563
|
+
if (fileNotesValue !== null) {
|
|
564
|
+
quickFileNotes = fileNotesValue
|
|
556
565
|
}
|
|
557
566
|
setQuickFileNotes(quickFileNotes)
|
|
558
567
|
|
|
@@ -763,16 +772,20 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
763
772
|
let autoName = changeName || resolveChangeNameAuto(cwd, specRoot) || 'default'
|
|
764
773
|
// archive 特例:归档后变更从活跃列表排除(listChanges WHERE status='active'),
|
|
765
774
|
// 不带 --change 回退 default 会读错变更。无活跃变更时取最新归档变更,读其现有 progress。
|
|
775
|
+
// 归档目录名 = 原变更名恒等迁移(archiveDestDirName 不改名),直接原名对读——此前按
|
|
776
|
+
// 「日期前缀过滤 + 剥日期」找,剥出的短名与 DB 行名(含日期)恒不符 → pm.read 返回
|
|
777
|
+
// null → initChange 凭空建幻影变更再跑 archive(坑 archive-fallback-phantom-change)。
|
|
766
778
|
if (stageName === 'archive' && autoName === 'default') {
|
|
767
779
|
try {
|
|
768
780
|
const archiveDir = join(specBase, 'changes', 'archive')
|
|
769
781
|
if (existsSync(archiveDir)) {
|
|
770
|
-
const latest = readdirSync(archiveDir)
|
|
771
|
-
.filter(d =>
|
|
782
|
+
const latest = readdirSync(archiveDir, { withFileTypes: true })
|
|
783
|
+
.filter(d => d.isDirectory())
|
|
784
|
+
.map(d => d.name)
|
|
772
785
|
.sort()
|
|
773
786
|
.pop()
|
|
774
787
|
if (latest) {
|
|
775
|
-
autoName = latest
|
|
788
|
+
autoName = latest
|
|
776
789
|
progress = pm.read(cwd, autoName)
|
|
777
790
|
}
|
|
778
791
|
}
|
|
@@ -879,12 +892,20 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
879
892
|
// stale/revising 阶段可能 steps 为空,或者 execute 阶段的 steps 需要从最新 plan.md 刷新
|
|
880
893
|
const stageDataPre = progress.stages[stageName]
|
|
881
894
|
const needsInit = !stageDataPre || !stageDataPre.steps || stageDataPre.steps.length === 0
|
|
882
|
-
// execute 阶段在 reopen 时需要从最新 plan.md 重新解析 steps(plan
|
|
895
|
+
// execute 阶段在 reopen 时需要从最新 plan.md 重新解析 steps(plan 可能已变更)。
|
|
896
|
+
// 按名保留旧状态(坑 execute-reopen-wipes-prior-completion):reopenStage 的语义是
|
|
897
|
+
// 「i < fromStep 保持原状(completed)」,此前预置全 pending 会把 fromStep 之前已完成的
|
|
898
|
+
// 步骤一并清空——CLI 打印「从步骤 N 开始修订」、prompt 注入「之前已完成不需要重做」,
|
|
899
|
+
// 实际 currentIdx 却回到 0,agent 被迫从头重做,修订语义完全失效。
|
|
883
900
|
if (needsInit || stageName === 'execute') {
|
|
884
901
|
const freshSteps = await getStageSteps(stageName, cwd, progress, specRoot)
|
|
885
902
|
if (freshSteps && freshSteps.length > 0) {
|
|
886
903
|
if (!progress.stages[stageName]) progress.stages[stageName] = { status: 'stale', steps: [] }
|
|
887
|
-
progress.stages[stageName].steps
|
|
904
|
+
const oldSteps = progress.stages[stageName].steps || []
|
|
905
|
+
progress.stages[stageName].steps = freshSteps.map(s => {
|
|
906
|
+
const old = oldSteps.find(step => step.name === s.name)
|
|
907
|
+
return old ? { ...old } : { name: s.name, status: 'pending' }
|
|
908
|
+
})
|
|
888
909
|
pm._write(cwd, progress, effectiveChange)
|
|
889
910
|
progress = pm.read(cwd, effectiveChange) || progress
|
|
890
911
|
}
|
|
@@ -1140,15 +1161,17 @@ async function resetStage(pm, progress, stageName, cwd, changeName, platformOpts
|
|
|
1140
1161
|
async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts = {}) {
|
|
1141
1162
|
const flowStages = ['brainstorm', 'plan', 'execute', 'verify', 'archive']
|
|
1142
1163
|
const isDone = flags.includes('--done')
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1164
|
+
// F4 守卫(坑 raw-flag-value-bypass,与 runCommand getFlagValue 同款):裸 flags[idx+1] 会把
|
|
1165
|
+
// 漏值后紧跟的 flag 名当值(--output --done → outputText="--done" 污染落盘 output)。
|
|
1166
|
+
const autoFlagValue = (name) => {
|
|
1167
|
+
const i = flags.indexOf(name)
|
|
1168
|
+
const next = i !== -1 ? flags[i + 1] : undefined
|
|
1169
|
+
return next && !String(next).startsWith('--') ? next : null
|
|
1170
|
+
}
|
|
1171
|
+
const outputText = autoFlagValue('--output')
|
|
1172
|
+
const inputText = autoFlagValue('--input')
|
|
1147
1173
|
const skipApproval = flags.includes('--skip-approval')
|
|
1148
|
-
const explicitMode = (
|
|
1149
|
-
const m = flags.indexOf('--mode')
|
|
1150
|
-
return m !== -1 && flags[m + 1] ? flags[m + 1] : null
|
|
1151
|
-
})()
|
|
1174
|
+
const explicitMode = autoFlagValue('--mode')
|
|
1152
1175
|
const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
1153
1176
|
|
|
1154
1177
|
// Helper: 在 auto 模式下获取步骤定义
|
|
@@ -1185,7 +1208,10 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1185
1208
|
return progress
|
|
1186
1209
|
}
|
|
1187
1210
|
}
|
|
1188
|
-
|
|
1211
|
+
// specRoot 必传(坑 auto-mode-specroot-miss):平台模式 specRoot 是外部目录,漏传时
|
|
1212
|
+
// getStageSteps 从 cwd 推导 miss 掉 plan/execute 动态步骤(buildPlanSteps(null) 只剩
|
|
1213
|
+
// 2 步 fixedPrefix),种出的步骤表两次 --done 即判阶段完成,与 getAutoSteps 渲染双轨失配
|
|
1214
|
+
const changed = await ensureStageSteps(progress, stage, cwd, platformOpts?.specRoot || null)
|
|
1189
1215
|
if (stageChanged || changed) {
|
|
1190
1216
|
pm._write(cwd, progress, changeName)
|
|
1191
1217
|
triggerSync(cwd, changeName, platformOpts)
|
|
@@ -1271,8 +1297,9 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1271
1297
|
console.log(`⏳ 变更 ${changeName} 的执行审批待处理中...`)
|
|
1272
1298
|
console.log(' 提示:使用 --skip-approval 跳过审批检查')
|
|
1273
1299
|
}
|
|
1300
|
+
// HUB-07:unknown 醒目警告 + 留痕(原单行 warn 不显眼且无记录)
|
|
1274
1301
|
if (approval.status === 'unknown') {
|
|
1275
|
-
|
|
1302
|
+
warnApprovalUnknown(cwd, changeName, approval.reason)
|
|
1276
1303
|
}
|
|
1277
1304
|
}
|
|
1278
1305
|
}
|
|
@@ -1288,6 +1315,12 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1288
1315
|
const result = await completeStep(pm, progress, currentStage, cwd, outputText, inputText, { printNext: false, changeName, platformOpts })
|
|
1289
1316
|
if (!result) return
|
|
1290
1317
|
progress = pm.read(cwd, changeName)
|
|
1318
|
+
// change 行被并发归档/删除时 read 返回 null(体检 BUG-05):引导排查而非 TypeError 崩溃
|
|
1319
|
+
if (!progress) {
|
|
1320
|
+
console.error(`❌ 完成步骤后进度数据消失(变更 ${changeName} 可能被并发归档/删除),请用 sillyspec progress show 核对当前状态`)
|
|
1321
|
+
process.exitCode = 1
|
|
1322
|
+
return
|
|
1323
|
+
}
|
|
1291
1324
|
|
|
1292
1325
|
const nextPendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
|
|
1293
1326
|
if (nextPendingIdx !== -1) {
|
|
@@ -1304,8 +1337,9 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1304
1337
|
console.log(`⏳ 变更 ${changeName} 的执行审批待处理中...`)
|
|
1305
1338
|
console.log(' 提示:使用 --skip-approval 跳过审批检查')
|
|
1306
1339
|
}
|
|
1340
|
+
// HUB-07:unknown 醒目警告 + 留痕(原单行 warn 不显眼且无记录)
|
|
1307
1341
|
if (approval.status === 'unknown') {
|
|
1308
|
-
|
|
1342
|
+
warnApprovalUnknown(cwd, changeName, approval.reason)
|
|
1309
1343
|
}
|
|
1310
1344
|
}
|
|
1311
1345
|
}
|
|
@@ -1353,10 +1387,17 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1353
1387
|
progress.stages[next].startedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1354
1388
|
}
|
|
1355
1389
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1356
|
-
|
|
1390
|
+
// specRoot 同上(坑 auto-mode-specroot-miss):平台模式动态步骤表必须从外部 specRoot 构建
|
|
1391
|
+
await ensureStageSteps(progress, next, cwd, platformOpts?.specRoot || null)
|
|
1357
1392
|
pm._write(cwd, progress, changeName)
|
|
1358
1393
|
triggerSync(cwd, changeName, platformOpts)
|
|
1359
1394
|
progress = pm.read(cwd, changeName)
|
|
1395
|
+
// 同 BUG-05:写入后被并发删除的窗口,null 直接解引用会 TypeError
|
|
1396
|
+
if (!progress) {
|
|
1397
|
+
console.error(`❌ 阶段推进后进度数据消失(变更 ${changeName} 可能被并发归档/删除),请用 sillyspec progress show 核对当前状态`)
|
|
1398
|
+
process.exitCode = 1
|
|
1399
|
+
return
|
|
1400
|
+
}
|
|
1360
1401
|
|
|
1361
1402
|
console.log(`\n${currentStage} complete. Auto advanced to ${next}.`)
|
|
1362
1403
|
const nextSteps = await getAutoSteps(next)
|
|
@@ -1374,8 +1415,9 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1374
1415
|
console.log(`⏳ 变更 ${changeName} 的执行审批待处理中...`)
|
|
1375
1416
|
console.log(' 提示:使用 --skip-approval 跳过审批检查')
|
|
1376
1417
|
}
|
|
1418
|
+
// HUB-07:unknown 醒目警告 + 留痕(原单行 warn 不显眼且无记录)
|
|
1377
1419
|
if (approval.status === 'unknown') {
|
|
1378
|
-
|
|
1420
|
+
warnApprovalUnknown(cwd, changeName, approval.reason)
|
|
1379
1421
|
}
|
|
1380
1422
|
}
|
|
1381
1423
|
}
|
|
@@ -98,10 +98,9 @@ export function validateParsedProjects(projects, sourceRoot) {
|
|
|
98
98
|
/**
|
|
99
99
|
* 在 changes/archive/ 下查找变更 <changeName> 是否已被归档(archive 脱钩自愈用)。
|
|
100
100
|
*
|
|
101
|
-
* 归档目录名 =
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* 防止同名巧合误判)。issue: archive-stage-physical-tracking-desync。
|
|
101
|
+
* 归档目录名 = 原变更名(直接移入 archive/,不重命名)。
|
|
102
|
+
* 精确匹配 + 要求目录含 plan.md(归档必备产物,防止同名巧合误判)。
|
|
103
|
+
* issue: archive-stage-physical-tracking-desync。
|
|
105
104
|
*
|
|
106
105
|
* @param {string} archiveDir - changes/archive 绝对路径
|
|
107
106
|
* @param {string} changeName - 变更名(currentChange)
|
|
@@ -115,15 +114,9 @@ export function findAlreadyArchivedDir(archiveDir, changeName) {
|
|
|
115
114
|
} catch {
|
|
116
115
|
return null
|
|
117
116
|
}
|
|
118
|
-
|
|
119
|
-
const targetDesc = descOf(changeName)
|
|
120
|
-
if (!targetDesc) return null
|
|
121
|
-
// 1. 精确原名命中(手动 mv 保留原 changeName)
|
|
117
|
+
// 精确原名命中
|
|
122
118
|
const exact = entries.find((e) => e === changeName && existsSync(join(archiveDir, e, 'plan.md')))
|
|
123
119
|
if (exact) return join(archiveDir, exact)
|
|
124
|
-
// 2. 描述部分命中(archiveDestDirName 产出的 <date>-<desc>,或手动剥前导日期的目录)
|
|
125
|
-
const descMatch = entries.find((e) => descOf(e) === targetDesc && existsSync(join(archiveDir, e, 'plan.md')))
|
|
126
|
-
if (descMatch) return join(archiveDir, descMatch)
|
|
127
120
|
return null
|
|
128
121
|
}
|
|
129
122
|
|
|
@@ -142,9 +135,14 @@ async function archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platform
|
|
|
142
135
|
try {
|
|
143
136
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
144
137
|
if (existsSync(runtimeRoot)) {
|
|
138
|
+
// stage-review marker 精确匹配(坑 marker-suffix-overmatch):marker 名是
|
|
139
|
+
// current-stage-review-run-id-<stage>-<change>,按 endsWith('-'+change) 后缀匹配会在归档
|
|
140
|
+
// 手工短名变更(如 login)时误删自动日期前缀变更(2026-08-01-login 天然以 '-login' 结尾)
|
|
141
|
+
// 的 marker。stage 集合与写入侧 stageReviewMarkerPath 的调用方一致。
|
|
142
|
+
const REVIEW_STAGES = ['brainstorm', 'plan', 'execute', 'verify', 'archive']
|
|
145
143
|
const markers = readdirSync(runtimeRoot).filter((f) =>
|
|
146
144
|
f === `current-execute-run-id-${archiveChangeName}` ||
|
|
147
|
-
(f
|
|
145
|
+
REVIEW_STAGES.some(st => f === `current-stage-review-run-id-${st}-${archiveChangeName}`),
|
|
148
146
|
)
|
|
149
147
|
for (const m of markers) {
|
|
150
148
|
try { unlinkSync(join(runtimeRoot, m)) } catch {}
|
|
@@ -506,9 +504,12 @@ export async function handleScanProjectListStep({ stageName, steps, currentIdx,
|
|
|
506
504
|
const yamlContent = readFileSync(join(projectsDir, yf), 'utf8')
|
|
507
505
|
const pathMatch = yamlContent.match(/^path:\s*(.+)/m)
|
|
508
506
|
const pPath = pathMatch ? pathMatch[1].trim() : pName
|
|
509
|
-
// 校验 path 是否存在且在 source_root
|
|
507
|
+
// 校验 path 是否存在且在 source_root 内。
|
|
508
|
+
// 用 relative 判越界(与 validateParsedProjects 同口径):startsWith 前缀比较
|
|
509
|
+
// 无分隔符,C:\repo 会放行兄弟目录 C:\repository2(体检 BUG-13)
|
|
510
510
|
const absPath = resolve(cwd, pPath)
|
|
511
|
-
|
|
511
|
+
const rel = relative(resolve(cwd), absPath)
|
|
512
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
512
513
|
fallbackSkipped.push(`${pName} (path 越界: ${pPath})`)
|
|
513
514
|
continue
|
|
514
515
|
}
|
|
@@ -1162,7 +1163,9 @@ export async function handleExecuteWaveArtifact({ stageName, steps, currentIdx,
|
|
|
1162
1163
|
export async function collectExecuteChangedFiles({ runtimeRoot, changeName }) {
|
|
1163
1164
|
if (!runtimeRoot || !changeName) return []
|
|
1164
1165
|
const { resolveLatestExecuteRunIdWithTasks, readReview, normalizeRepoKey } = await import('../task-review.js')
|
|
1165
|
-
|
|
1166
|
+
// changeName 透传:run 解析按 change 戳优先归属(坑 worktree-cleanup-marker-chain),
|
|
1167
|
+
// 避免 marker 断裂后聚合到其他变更 run 的 changedFiles 误报 missing
|
|
1168
|
+
const runId = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName })
|
|
1166
1169
|
if (!runId) return []
|
|
1167
1170
|
const tasksDir = join(runtimeRoot, 'execute-runs', runId, 'tasks')
|
|
1168
1171
|
if (!existsSync(tasksDir)) return []
|
|
@@ -1300,6 +1303,15 @@ export async function handleExecuteWorktreeCleanup({ stageName, changeName, cwd
|
|
|
1300
1303
|
export async function handleScanStageCompleted({ stageName, currentIdx, cwd, progress, pm, stageData, changeName, outputText, platformOpts }) {
|
|
1301
1304
|
// 平台模式:scan 完成后生成 manifest.json + post-check
|
|
1302
1305
|
if (stageName === 'scan' && (platformOpts.specRoot || platformOpts.runtimeRoot)) {
|
|
1306
|
+
if (!platformOpts.specRoot) {
|
|
1307
|
+
// 只传 runtimeRoot 无 specRoot(command.js 允许的组合):manifest 无处落盘。
|
|
1308
|
+
// 显式 fail-closed 报错——此前 mkdirSync(null) 抛 TypeError 被外层 catch 吞掉,
|
|
1309
|
+
// postcheck/指针升级/阻断全部静默跳过,scan 失败也"干净成功"(体检 BUG-12)
|
|
1310
|
+
console.error(`❌ 平台模式缺少 --spec-root,无法写 manifest.json(scan postcheck 中止)`)
|
|
1311
|
+
stageData.scanMeta = stageData.scanMeta || {}; stageData.scanMeta.manifestWritten = false
|
|
1312
|
+
stageData.scanMeta.manifestError = 'missing specRoot'
|
|
1313
|
+
return
|
|
1314
|
+
}
|
|
1303
1315
|
try {
|
|
1304
1316
|
stageData.scanMeta = stageData.scanMeta || {}; stageData.scanMeta.manifestWritten = false; // 默认失败
|
|
1305
1317
|
const manifestDir = platformOpts.specRoot
|