sillyspec 3.28.0 → 3.28.2
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/archive-delta.js +419 -0
- package/src/design-facts.js +354 -0
- package/src/docs-check.js +79 -8
- package/src/doctor-diagnostics.js +11 -2
- package/src/endpoint-baseline.js +109 -0
- package/src/git-helper.js +31 -0
- package/src/index.js +213 -4
- package/src/modules.js +41 -12
- package/src/progress/change-registry.js +20 -0
- package/src/progress/consistency-doctor.js +124 -5
- package/src/progress/stage-machine.js +13 -2
- package/src/progress.js +3 -0
- package/src/quicklog.js +51 -0
- package/src/run/command.js +70 -1
- package/src/run/complete-handlers.js +53 -4
- package/src/run/complete.js +46 -1
- package/src/run/gates.js +333 -2
- package/src/run/prompt.js +70 -0
- package/src/run/scan-profile.js +98 -6
- package/src/run/shared.js +12 -1
- package/src/run/stage.js +7 -1
- package/src/scan-diff.js +54 -1
- package/src/scan-facts.js +173 -0
- package/src/scan-postcheck.js +110 -3
- package/src/spec-sync.js +19 -2
- package/src/stage-review.js +62 -3
- package/src/stages/archive.js +5 -5
- package/src/stages/brainstorm.js +10 -10
- package/src/stages/execute.js +15 -13
- package/src/stages/plan-postcheck.js +194 -1
- package/src/stages/plan.js +6 -2
- package/src/stages/scan.js +44 -102
- package/src/stages/verify.js +9 -7
- package/src/taskcard.js +8 -1
- package/src/verify-postcheck.js +610 -2
- package/src/verify-probes.js +89 -10
- package/src/workflow.js +30 -0
- package/templates/agents-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +5 -0
- package/templates/workflows/scan-docs.yaml +6 -0
package/src/quicklog.js
CHANGED
|
@@ -713,6 +713,57 @@ export async function allocateQuicklogEntry(specBase, gitUser, { description, li
|
|
|
713
713
|
})
|
|
714
714
|
}
|
|
715
715
|
|
|
716
|
+
/**
|
|
717
|
+
* 以**给定 ql-ID** 追加一条「进行中」骨架条目(不分配新 ID)。
|
|
718
|
+
*
|
|
719
|
+
* 坑 platform-takeover-phantom-progress-db 同日变体:quick --done 兜底复用启动分配的
|
|
720
|
+
* ql-ID(进度库 changes.quicklog_id 可查)时,该 ID 的条目可能不在当前 specBase 的
|
|
721
|
+
* QUICKLOG 文件里(启动与 --done 落到不同库/文件的分裂形态)——用原 ID 补建骨架再走
|
|
722
|
+
* 完成翻态,保证「同一次会话只有一个 ql-ID」。条目已存在则幂等跳过(不重复追加)。
|
|
723
|
+
* 与 allocateQuicklogEntry 同锁同格式同平台推送,仅跳过 ID 分配段。
|
|
724
|
+
*/
|
|
725
|
+
export async function appendQuicklogEntryWithId(specBase, gitUser, qlId, { description = '(guard 缺失补建)', linkedChanges = [], allowedFiles = [] } = {}) {
|
|
726
|
+
if (!qlId || typeof qlId !== 'string') throw new Error('appendQuicklogEntryWithId: qlId 必填')
|
|
727
|
+
if (findQuicklogEntry(specBase, gitUser, qlId)) return { qlId, existed: true }
|
|
728
|
+
const quicklogDir = join(specBase, 'quicklog')
|
|
729
|
+
mkdirSync(quicklogDir, { recursive: true })
|
|
730
|
+
const user = sanitizeQuicklogUser(gitUser) || 'unknown'
|
|
731
|
+
const userFile = join(quicklogDir, `QUICKLOG-${user}.md`)
|
|
732
|
+
const lockPath = join(quicklogDir, `.QUICKLOG-${user}.md.lock`)
|
|
733
|
+
const desc = sanitizeDesc(description)
|
|
734
|
+
const linked = Array.isArray(linkedChanges) ? linkedChanges : []
|
|
735
|
+
const files = Array.isArray(allowedFiles) ? allowedFiles : []
|
|
736
|
+
|
|
737
|
+
return await withFileLock(lockPath, async () => {
|
|
738
|
+
// 双检(锁内):并发兜底同 ID 只落一条
|
|
739
|
+
if (findQuicklogEntry(specBase, gitUser, qlId)) return { qlId, existed: true }
|
|
740
|
+
await rotateIfNeeded(userFile, user)
|
|
741
|
+
const entry = [
|
|
742
|
+
'',
|
|
743
|
+
`## ${qlId} | ${nowDatetime()} | ${desc}`,
|
|
744
|
+
'状态:进行中',
|
|
745
|
+
`关联变更:${linked.length > 0 ? linked.join(', ') : '(无)'}`,
|
|
746
|
+
`文件:${files.length > 0 ? files.join(', ') : '(见实际改动)'}`,
|
|
747
|
+
'',
|
|
748
|
+
].join('\n')
|
|
749
|
+
appendFileSync(userFile, entry)
|
|
750
|
+
for (const c of linked) await appendTaskCheckbox(specBase, c, qlId, desc)
|
|
751
|
+
await pushQuicklogEntryToPlatform(specBase, {
|
|
752
|
+
ql_id: qlId,
|
|
753
|
+
timestamp: nowDatetime(),
|
|
754
|
+
title: desc,
|
|
755
|
+
status: 'in_progress',
|
|
756
|
+
status_note: null,
|
|
757
|
+
author_raw: user,
|
|
758
|
+
linked_changes: linked.filter(c => /^\d{4}-\d{2}-\d{2}-/.test(c)),
|
|
759
|
+
files: files.map(f => ({ path: f, note: null })),
|
|
760
|
+
body_sections: {},
|
|
761
|
+
raw_block: entry,
|
|
762
|
+
}).catch(() => {})
|
|
763
|
+
return { qlId, existed: false }
|
|
764
|
+
})
|
|
765
|
+
}
|
|
766
|
+
|
|
716
767
|
/**
|
|
717
768
|
* 翻某 qlId 条目为「已完成」+ 追加结果 + 勾选关联 tasks.md。持锁。
|
|
718
769
|
*/
|
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, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, PLATFORM_MANAGED_FILENAME } from './shared.js'
|
|
26
|
+
import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, PLATFORM_MANAGED_FILENAME } from './shared.js'
|
|
27
27
|
import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
28
28
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
29
29
|
import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
|
|
@@ -107,6 +107,38 @@ function warnMsysMangledFlag(flag, value) {
|
|
|
107
107
|
console.error(' 非本意 → 去掉前导 / 或改写表述后重发本命令;确需原样 → 命令前加 MSYS_NO_PATHCONV=1。')
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* done-like 步骤动作(--done/--skip/--wait/--continue/--reset/--reopen)的目标 change 在当前
|
|
112
|
+
* 库是否已「物化」(坑 done-phantom-change-silent-create,2026-09-04 事故元凶①)。
|
|
113
|
+
*
|
|
114
|
+
* 背景:接管指针切换/重建进度库后,--change 常指向旧库的变更名;新库里既无 DB 行也无
|
|
115
|
+
* changes/<name>/ 目录,runCommand 的 !progress 分支却无条件 initChange 静默新建——幻影
|
|
116
|
+
* 变更零进度起步,--done 被记到幻影上,原变更进度停滞(本次事故直接元凶)。
|
|
117
|
+
*
|
|
118
|
+
* 物化口径(progress 已知为 null,只查磁盘标记):
|
|
119
|
+
* - 普通变更:changes/<name>/ 存在(archive 已归档目录也算——归档后 DB 行仍可读,
|
|
120
|
+
* 但行被清理的恢复场景认 archive 目录物化,走 initChange 自愈是合法路径);
|
|
121
|
+
* - quick-<8hex> 会话:无 changes/ 目录,物化标记 = 会话 guard(per-session 目录或
|
|
122
|
+
* legacy 单文件——legacy 无法归属会话,保守认物化,宁可漏拦不误杀真会话)。
|
|
123
|
+
*
|
|
124
|
+
* 纯函数:只读磁盘,供 command.js 守卫 + 单测复用。
|
|
125
|
+
* @param {object} opts
|
|
126
|
+
* @param {string|null} opts.target - 目标 change 名(--change 值或 changes/ 目录唯一推导)
|
|
127
|
+
* @param {string} opts.specBase - 当前进度库根(平台 specRoot 或 cwd/.sillyspec)
|
|
128
|
+
* @param {string|null} [opts.quickSessionsDir] - quick 会话 guard 目录(缺省按 specBase/.runtime 推导)
|
|
129
|
+
* @returns {boolean} true = 目标已物化(可走既有 initChange 自愈),false = 幻影(应拒绝新建)
|
|
130
|
+
*/
|
|
131
|
+
export function doneLikeTargetMaterialized({ target, specBase, quickSessionsDir = null }) {
|
|
132
|
+
if (!target || !specBase) return false
|
|
133
|
+
if (/^quick-[0-9a-f]{8}$/.test(target)) {
|
|
134
|
+
const rt = quickSessionsDir || join(specBase, '.runtime', 'quick-sessions')
|
|
135
|
+
return existsSync(join(rt, target, 'guard.json'))
|
|
136
|
+
|| existsSync(join(specBase, '.runtime', 'quick-guard.json'))
|
|
137
|
+
}
|
|
138
|
+
return existsSync(join(specBase, 'changes', target))
|
|
139
|
+
|| existsSync(join(specBase, 'changes', 'archive', target))
|
|
140
|
+
}
|
|
141
|
+
|
|
110
142
|
/**
|
|
111
143
|
* 从 progress 或变更目录推导变更名
|
|
112
144
|
*/
|
|
@@ -951,6 +983,43 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
951
983
|
}
|
|
952
984
|
|
|
953
985
|
if (!progress) {
|
|
986
|
+
// ── done-like 存在性守卫(坑 done-phantom-change-silent-create,2026-09-04 事故元凶①)──
|
|
987
|
+
// --done/--skip/--wait/--continue/--reset/--reopen 都预设「目标 change 已存在于当前进度库」。
|
|
988
|
+
// 此前 !progress 时无条件 initChange 静默新建——接管指针切库/换库后 --change 指向旧库的
|
|
989
|
+
// 变更名,新库里凭空建幻影变更再把动作记上去。目标已物化(changes/ 目录或 quick 会话
|
|
990
|
+
// guard 存在)仍放行走下方 initChange 自愈(DB 重建/迁移后从目录物化是合法恢复路径)。
|
|
991
|
+
if (isDone || isSkip || isWait || isContinue || isReset || isReopen) {
|
|
992
|
+
const actionFlag = isDone ? '--done' : isSkip ? '--skip' : isWait ? '--wait'
|
|
993
|
+
: isContinue ? '--continue' : isReset ? '--reset' : '--reopen'
|
|
994
|
+
const guardTarget = changeName || resolveChangeNameAuto(cwd, specRoot)
|
|
995
|
+
if (!doneLikeTargetMaterialized({
|
|
996
|
+
target: guardTarget,
|
|
997
|
+
specBase,
|
|
998
|
+
quickSessionsDir: resolveQuickSessionsDir(platformOpts, specBase),
|
|
999
|
+
})) {
|
|
1000
|
+
const pointerPath = join(cwd, '.sillyspec-platform.json')
|
|
1001
|
+
const pointerMode = existsSync(pointerPath)
|
|
1002
|
+
let activeList = []
|
|
1003
|
+
try { activeList = pm.listChanges(cwd) } catch { /* 列举失败不掩盖主报错 */ }
|
|
1004
|
+
console.error(`❌ ${actionFlag} 找不到目标变更的进度,拒绝静默新建(防幻影变更)。`)
|
|
1005
|
+
console.error(` 目标变更: ${guardTarget || '(未指定 --change,且当前库无唯一活跃变更)'}`)
|
|
1006
|
+
console.error(` 当前进度库: ${specBase}${pointerMode ? `(平台接管指针 ${pointerPath} 指向此库)` : '(本地模式)'}`)
|
|
1007
|
+
console.error(` 本库活跃变更: ${activeList.length > 0 ? activeList.join('、') : '(无)'}`)
|
|
1008
|
+
console.error(' 排查:')
|
|
1009
|
+
console.error(` ① 变更名是否打错——从上面活跃列表选,或 sillyspec progress show 查看各变更进度`)
|
|
1010
|
+
if (pointerMode) {
|
|
1011
|
+
console.error(` ② 最近是否切换/重建过平台接管指针(重跑平台 scan 会换库)——原变更可能留在旧库:检查 ${pointerPath} 的 specRoot,或用 --spec-dir 显式指回旧库`)
|
|
1012
|
+
} else {
|
|
1013
|
+
console.error(` ② cwd 是否漂移/切库——用 --spec-dir <路径>/.sillyspec 显式指定正确的进度库`)
|
|
1014
|
+
}
|
|
1015
|
+
if (stageName === 'quick' || /^quick-[0-9a-f]{8}$/.test(guardTarget || '')) {
|
|
1016
|
+
console.error(` ③ quick 会话跨进程恢复须带 --change <quick-session-id>;本库无该会话 guard——会话可能属于旧库`)
|
|
1017
|
+
} else {
|
|
1018
|
+
console.error(` ③ 确要新建变更:先 sillyspec run brainstorm --change <名>(不带 ${actionFlag})`)
|
|
1019
|
+
}
|
|
1020
|
+
process.exit(2) // 用法/环境错(目标变更不在当前库)→ exit 2
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
954
1023
|
// 如果指定了变更名或有变更目录,自动初始化变更的 progress
|
|
955
1024
|
const autoChange = changeName || resolveChangeNameAuto(cwd, specRoot)
|
|
956
1025
|
if (autoChange) {
|
|
@@ -31,7 +31,7 @@ import { detectConcurrentChanges, formatConcurrentWarning, resolveConcurrentAnch
|
|
|
31
31
|
import { stageRegistry } from '../stages/index.js'
|
|
32
32
|
import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
|
|
33
33
|
import { printQuickAuditReview, runQuickTestLintGate, printQuickTestLintGate } from './quick-audit.js'
|
|
34
|
-
import { validateQuickResult, allocateQuicklogEntry, findQuicklogEntry, completeQuicklogEntry, extractTitleFromResult } from '../quicklog.js'
|
|
34
|
+
import { validateQuickResult, allocateQuicklogEntry, appendQuicklogEntryWithId, findQuicklogEntry, completeQuicklogEntry, extractTitleFromResult } from '../quicklog.js'
|
|
35
35
|
import { getRule } from '../stage-contract-spec.js'
|
|
36
36
|
import { archiveDestDirName } from '../stage-contract.js'
|
|
37
37
|
|
|
@@ -432,6 +432,31 @@ export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, c
|
|
|
432
432
|
console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
|
|
433
433
|
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
434
434
|
}
|
|
435
|
+
// ── 归档前 delta.md 自动生成(P3d task-02,fail-soft 零阻断)──
|
|
436
|
+
// 目录移走前在 changes/<name>/ 落一份 Before/Delta/After 三段式快照(四源采集容缺,
|
|
437
|
+
// buildDeltaReport 见 ../archive-delta.js),随归档包留存。失败只 console.error 留痕、
|
|
438
|
+
// 提示 sillyspec delta --change 手动补,绝不抛出——归档主流程既有失败面(rename/git/清理)
|
|
439
|
+
// 一个不叠加。specRoot 同 complete.js :87 口径(specBase 已含 platform specRoot);
|
|
440
|
+
// runtimeRoot 同本文件 archiveWorktreeCleanup 既有用法 resolveRuntimeRoot(platformOpts, specBase)。
|
|
441
|
+
const deltaChangeName = progress.currentChange || changeName
|
|
442
|
+
if (deltaChangeName) {
|
|
443
|
+
try {
|
|
444
|
+
const { buildDeltaReport } = await import('../archive-delta.js')
|
|
445
|
+
const deltaChangeDir = join(specBase, 'changes', deltaChangeName)
|
|
446
|
+
const deltaMd = buildDeltaReport({
|
|
447
|
+
changeDir: deltaChangeDir,
|
|
448
|
+
specRoot: specBase,
|
|
449
|
+
// project 缺参可 null(loadModuleMap null 降级路径已支持,报告内逐段降级注记)
|
|
450
|
+
project: progress?.project || null,
|
|
451
|
+
runtimeRoot: resolveRuntimeRoot(platformOpts, specBase),
|
|
452
|
+
})
|
|
453
|
+
writeFileSync(join(deltaChangeDir, 'delta.md'), deltaMd)
|
|
454
|
+
console.log(`📊 归档前已生成 delta.md: ${join(deltaChangeDir, 'delta.md')}`)
|
|
455
|
+
} catch (e) {
|
|
456
|
+
console.error(`⚠️ 归档前 delta.md 自动生成失败(不阻断归档): ${e.message}`)
|
|
457
|
+
console.error(` 可手动补: sillyspec delta --change ${deltaChangeName} --spec-dir ${specBase}`)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
435
460
|
// 主仓互斥锁(坑 main-repo-no-mutex 二批):archiveChangeDirectory 改主仓共享状态(目录
|
|
436
461
|
// rename + 共享 index 的 git add + marker 删除 + worktree 清理),与并行会话的 apply/cleanup
|
|
437
462
|
// 互踩。exit 钩子兜底:其内部 guard 失败 exit(1) 时锁也会被清(见 withMainRepoLock)。
|
|
@@ -988,7 +1013,27 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
988
1013
|
}
|
|
989
1014
|
|
|
990
1015
|
if (!qlId) {
|
|
991
|
-
//
|
|
1016
|
+
// 坑 platform-takeover-phantom-progress-db 同日变体:guard 缺失时兜底**优先复用启动
|
|
1017
|
+
// 分配的 ql-ID**(进度库 changes.quicklog_id,quick 启动时 stage.js 回填)——落码
|
|
1018
|
+
// 注释/模块文档的 ql-ID 是启动时就给出的,补分配新号必然制造引用劈叉(同一次会话
|
|
1019
|
+
// 两个 ID、两份 QUICKLOG 各半)。ID 的条目不在当前 specBase(启动/完成分裂库形态)
|
|
1020
|
+
// → 用原 ID 补建骨架,完成态落在同一 ID 上。库中也无(真 brownfield/极老会话)才
|
|
1021
|
+
// 补分配。
|
|
1022
|
+
try {
|
|
1023
|
+
const dbQlId = pm.getQuicklogId(cwd, changeName)
|
|
1024
|
+
if (dbQlId) {
|
|
1025
|
+
const re = await appendQuicklogEntryWithId(specBase, gitUser, dbQlId, {
|
|
1026
|
+
description: guard?.taskDescription || '(guard 缺失补建)',
|
|
1027
|
+
linkedChanges,
|
|
1028
|
+
allowedFiles: Array.isArray(guard?.allowedFiles) ? guard.allowedFiles : [],
|
|
1029
|
+
})
|
|
1030
|
+
qlId = dbQlId
|
|
1031
|
+
console.log(`📝 QUICKLOG 兜底补写复用启动 ql-ID: ${qlId}(guard 缺失,ID 取自进度库 quicklog_id${re.existed ? '' : ',条目已补建'})`)
|
|
1032
|
+
}
|
|
1033
|
+
} catch { /* 读取/补建失败退回补分配,不阻断 */ }
|
|
1034
|
+
}
|
|
1035
|
+
if (!qlId) {
|
|
1036
|
+
// 无 ql-ID(库中也未回填,真 brownfield):补分配后立即完成,不阻断。
|
|
992
1037
|
try {
|
|
993
1038
|
const alloc = await allocateQuicklogEntry(specBase, gitUser, {
|
|
994
1039
|
description: guard?.taskDescription || '(补分配)',
|
|
@@ -1530,7 +1575,9 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
|
|
|
1530
1575
|
try { unlinkSync(platformOptsFile) } catch {}
|
|
1531
1576
|
|
|
1532
1577
|
// CLI 层 post-check(替代旧的简单检查)
|
|
1533
|
-
const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult } = await import('../scan-postcheck.js')
|
|
1578
|
+
const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult, stampScanDocHeaders } = await import('../scan-postcheck.js')
|
|
1579
|
+
// ② frontmatter CLI 盖章(2026-09-05,幂等兜底——正常已在 scanFinalize / quick postcheck 盖过)
|
|
1580
|
+
stampScanDocHeaders({ cwd, specDir: platformOpts.specRoot, mode: stageData.scanProfile?.mode })
|
|
1534
1581
|
const postResult = runScanPostCheck({
|
|
1535
1582
|
cwd,
|
|
1536
1583
|
specDir: platformOpts.specRoot,
|
|
@@ -1613,7 +1660,9 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
|
|
|
1613
1660
|
|
|
1614
1661
|
// 非 platform 模式 scan 也做轻量 post-check + 结构化输出
|
|
1615
1662
|
if (stageName === 'scan' && !platformOpts.specRoot && !platformOpts.runtimeRoot) {
|
|
1616
|
-
const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult } = await import('../scan-postcheck.js')
|
|
1663
|
+
const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult, stampScanDocHeaders } = await import('../scan-postcheck.js')
|
|
1664
|
+
// ② frontmatter CLI 盖章(2026-09-05,幂等兜底)
|
|
1665
|
+
stampScanDocHeaders({ cwd, specDir: null, mode: stageData.scanProfile?.mode })
|
|
1617
1666
|
const postResult = runScanPostCheck({ cwd, specDir: null, outputText, scanProfile: stageData.scanProfile || null })
|
|
1618
1667
|
printScanPostCheckResult(postResult)
|
|
1619
1668
|
// 结构化结果写入 .sillyspec/.runtime/
|
package/src/run/complete.js
CHANGED
|
@@ -21,12 +21,13 @@ import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } fr
|
|
|
21
21
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
22
22
|
import { withFileLock } from '../quicklog.js'
|
|
23
23
|
import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir } from './shared.js'
|
|
24
|
-
import { executeScanPreflight, executeScanPostcheck, computeScanProfile } from './scan-profile.js'
|
|
24
|
+
import { executeScanPreflight, executeScanPostcheck, computeScanProfile, executeScanDetectProjects, executeScanResumeCheck, executeScanFinalize } from './scan-profile.js'
|
|
25
25
|
import { executePlanPostcheck as runPlanPostcheckLib } from '../stages/plan-postcheck.js'
|
|
26
26
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
27
27
|
import { enforceDepsGate, enforceReviewJsonGate, enforceSymbolImpactGate, warnMissingUiPrototype, completeStageGates, readDesignScale } from './gates.js'
|
|
28
28
|
import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact } from './complete-handlers.js'
|
|
29
29
|
import { formatExecuteSummary } from '../worktree-apply.js'
|
|
30
|
+
import { validateDecisionModuleRefs } from '../design-facts.js'
|
|
30
31
|
import { isEndToEndTaskText } from '../change-risk-profile.js'
|
|
31
32
|
import { deriveTitleFromLinkedChange } from '../quicklog.js'
|
|
32
33
|
import { gitQuiet } from '../git-helper.js'
|
|
@@ -279,6 +280,37 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
279
280
|
await enforceSymbolImpactGate(stageName, changeName, steps[currentIdx]?.name, specBase)
|
|
280
281
|
// UI 原型软提醒(非阻断):brainstorm 收尾步 design.md 命中前端文件但无 prototype → 提示用户可否决
|
|
281
282
|
warnMissingUiPrototype(stageName, changeName, steps[currentIdx]?.name, specBase)
|
|
283
|
+
// 决策模块域核验硬门(task-02 接线 design-facts,FR-02/D-002):brainstorm「生成规范文件」步
|
|
284
|
+
// --done 时核验 decisions.md 模块域引用。ERROR(decision_module_ref_invalid,消息已内嵌前缀)
|
|
285
|
+
// = 模块幻觉/书写错误 → fail-closed exit 1(进度不推进,步骤保持 pending);WARNING
|
|
286
|
+
// (decision_module_domain_gap 声明×实改差异)→ warn 放行留人工裁量;skipped(无决策文件/
|
|
287
|
+
// 无 _module-map.yaml 索引)→ 单行 info 放行(无索引不误拦)。fail-soft:核验自身异常 →
|
|
288
|
+
// fail-open(warn 留痕后放行——门禁代码缺陷不应误拦真实完成,与「无索引跳过」同向)。
|
|
289
|
+
// project 解析与 handleWorkflowPostCheck 同链:progress.project(平台模式真实项目名)>
|
|
290
|
+
// steps[idx].project > null(brainstorm 非 perProject 展开,后两位常规为空)。
|
|
291
|
+
if (stageName === 'brainstorm' && steps[currentIdx]?.name?.includes('生成规范文件') && changeName) {
|
|
292
|
+
try {
|
|
293
|
+
const _dmProject = progress?.project || steps[currentIdx]?.project || null
|
|
294
|
+
const _dmResult = validateDecisionModuleRefs({
|
|
295
|
+
changeDir: join(specBase, 'changes', changeName),
|
|
296
|
+
specRoot: specBase,
|
|
297
|
+
project: _dmProject,
|
|
298
|
+
})
|
|
299
|
+
if ((_dmResult.errors || []).length > 0) {
|
|
300
|
+
console.error(`❌ ── 决策模块域核验阻断(本次 --done 未完成,进度未推进)──`)
|
|
301
|
+
for (const _dmErr of _dmResult.errors) console.error(` • ${_dmErr}`)
|
|
302
|
+
console.error(` 修复:更正 decisions.md 对应条目的「模块域」字段(模块 ID 取自 docs/${_dmProject || '<项目>'}/modules/_module-map.yaml),或以 NEW:<名> 前缀声明新模块后重跑:`)
|
|
303
|
+
console.error(` sillyspec run brainstorm --done${changeName ? ` --change ${changeName}` : ''} --output "修复说明"`)
|
|
304
|
+
process.exit(1)
|
|
305
|
+
}
|
|
306
|
+
for (const _dmWarn of (_dmResult.warnings || [])) console.warn(` ⚠️ ${_dmWarn}`)
|
|
307
|
+
if (_dmResult.skipped) {
|
|
308
|
+
console.log(`ℹ️ [decision_module_check_skipped] ${_dmResult.skipped}`)
|
|
309
|
+
}
|
|
310
|
+
} catch (_dmEx) {
|
|
311
|
+
console.warn(` ⚠️ 决策模块域核验自身异常,fail-open 放行(不误拦本次完成):${_dmEx && _dmEx.message ? _dmEx.message : _dmEx}`)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
282
314
|
|
|
283
315
|
// ── noAI 步骤硬门(坑 noai-done-bypass):noAI 步骤的确定性校验不可被 --done 绕过 ──
|
|
284
316
|
// 正常路径 agent 跑 `run <stage>` 推进到 noAI step 时,runStage 自动执行 _cliAction
|
|
@@ -296,6 +328,12 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
296
328
|
await executeScanPreflight(cwd, platformOpts, stageData.scanProfile || computeScanProfile(cwd, platformOpts))
|
|
297
329
|
} else if (_cliAction === 'scanPostcheck') {
|
|
298
330
|
await executeScanPostcheck(cwd, platformOpts, stageData.scanProfile || computeScanProfile(cwd, platformOpts))
|
|
331
|
+
} else if (_cliAction === 'scanDetectProjects') {
|
|
332
|
+
await executeScanDetectProjects(cwd, platformOpts)
|
|
333
|
+
} else if (_cliAction === 'scanResumeCheck') {
|
|
334
|
+
await executeScanResumeCheck(cwd, platformOpts)
|
|
335
|
+
} else if (_cliAction === 'scanFinalize') {
|
|
336
|
+
await executeScanFinalize(cwd, platformOpts)
|
|
299
337
|
} else if (_cliAction === 'planPostcheck') {
|
|
300
338
|
await runPlanPostcheckLib({ cwd, specRoot: platformOpts?.specRoot, resolveChangeDir, progress })
|
|
301
339
|
} else {
|
|
@@ -507,6 +545,13 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
507
545
|
console.log(`\n👉 下一步:sillyspec run verify${changeName ? ` --change ${changeName}` : ''}(验证通过后才能归档)`)
|
|
508
546
|
}
|
|
509
547
|
} else if (stageName === 'archive') {
|
|
548
|
+
// 机械暂存 CLI 化(2026-09-05 全流程审计):原步骤 6 的 3 条固定路径 git add agent 指令
|
|
549
|
+
// 改为完成时 CLI 直做(未变更路径 git add 是无害 no-op)。平台模式跳过(specRoot 在源码仓外)。
|
|
550
|
+
if (!platformOpts?.specRoot && !platformOpts?.runtimeRoot) {
|
|
551
|
+
const { stageArchiveArtifacts } = await import('../git-helper.js')
|
|
552
|
+
const r = stageArchiveArtifacts(cwd)
|
|
553
|
+
if (r.staged.length > 0) console.log(` 📦 已暂存归档产物(${r.staged.length} 个路径,未提交,由统一提交工具处理)`)
|
|
554
|
+
}
|
|
510
555
|
console.log('\n👉 归档完成!现在可以提交了:git commit -m "..."')
|
|
511
556
|
} else if (stageName === 'verify') {
|
|
512
557
|
// verify 的"验证通过"提示延后到下方 validator 通过后才打印,
|