sillyspec 3.23.8 → 3.24.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-brainstorm/SKILL.md +1 -1
- package/.claude/skills/sillyspec-commit/SKILL.md +3 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +3 -2
- package/package.json +1 -1
- package/src/quicklog.js +333 -0
- package/src/run.js +215 -88
- package/src/stage-contract.js +30 -0
- package/src/stages/brainstorm.js +569 -657
- package/src/stages/execute.js +3 -4
- package/src/stages/plan-postcheck.js +5 -1
- package/src/stages/plan.js +1 -1
- package/src/stages/propose.js +1 -1
- package/src/stages/quick.js +39 -54
- package/src/stages/verify.js +4 -82
- package/src/worktree.js +16 -0
- package/templates/prompts/verify-probes.md +53 -0
package/src/run.js
CHANGED
|
@@ -9,8 +9,28 @@ import { existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, read
|
|
|
9
9
|
import { randomBytes, randomUUID } from 'crypto'
|
|
10
10
|
import { createRequire } from 'module'
|
|
11
11
|
const require = createRequire(import.meta.url)
|
|
12
|
+
import { fileURLToPath } from 'url'
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 解析 prompt 中的 {{include: <name>}} 占位符:读包内 templates/prompts/<name>.md 注入。
|
|
17
|
+
* 把 stage step prompt 里 self-contained 的大块(如 verify 探针)抽到外部模板,
|
|
18
|
+
* CLI 端组装时注入——agent 收到的仍是自包含 prompt 字符串,无需自己 Read。
|
|
19
|
+
* 单次替换(不递归);模板缺失则保留占位符并 warn,便于发现配置错误。
|
|
20
|
+
*/
|
|
21
|
+
function resolvePromptIncludes(text) {
|
|
22
|
+
return text.replace(/\{\{include:\s*([\w.-]+)\s*\}\}/g, (match, name) => {
|
|
23
|
+
const tplPath = join(__dirname, '..', 'templates', 'prompts', `${name}.md`)
|
|
24
|
+
if (!existsSync(tplPath)) {
|
|
25
|
+
console.warn(`⚠️ prompt include 模板缺失: ${name} (期望: ${tplPath})`)
|
|
26
|
+
return match
|
|
27
|
+
}
|
|
28
|
+
return readFileSync(tplPath, 'utf8')
|
|
29
|
+
})
|
|
30
|
+
}
|
|
12
31
|
import { ProgressManager } from './progress.js'
|
|
13
32
|
import { SCAN_STATUS, POINTER_STATUS, isPointerCorrupted } from './constants.js'
|
|
33
|
+
import { allocateQuicklogEntry, completeQuicklogEntry, findQuicklogEntry, validateQuickResult } from './quicklog.js'
|
|
14
34
|
import { checkbox } from '@inquirer/prompts'
|
|
15
35
|
|
|
16
36
|
/**
|
|
@@ -158,7 +178,7 @@ function resolveSpecDir(cwd, opts = {}) {
|
|
|
158
178
|
return join(resolve(cwd), '.sillyspec')
|
|
159
179
|
}
|
|
160
180
|
import { stageRegistry, auxiliaryStages } from './stages/index.js'
|
|
161
|
-
import { checkTransition, runValidators } from './stage-contract.js'
|
|
181
|
+
import { checkTransition, runValidators, validateChangeExists } from './stage-contract.js'
|
|
162
182
|
import { buildExecuteSteps } from './stages/execute.js'
|
|
163
183
|
import { buildPlanSteps } from './stages/plan.js'
|
|
164
184
|
import { formatExecuteSummary } from './worktree-apply.js'
|
|
@@ -676,7 +696,8 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
676
696
|
console.log(`changeDir: ${changeDir}`)
|
|
677
697
|
}
|
|
678
698
|
console.log(`---\n`)
|
|
679
|
-
|
|
699
|
+
// persona 只在 stage 首步注入(step0)——角色设定一次即可,后续 step 重复注入纯属 token 浪费
|
|
700
|
+
if (personas[stageName] && stepIndex === 0) {
|
|
680
701
|
console.log(personas[stageName])
|
|
681
702
|
console.log('')
|
|
682
703
|
}
|
|
@@ -689,7 +710,9 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
689
710
|
console.log(guardrails.trim())
|
|
690
711
|
console.log('')
|
|
691
712
|
}
|
|
692
|
-
|
|
713
|
+
// 先解析 {{include: name}}(把外部模板片段拉进 prompt),再做下方占位符替换,
|
|
714
|
+
// 保证模板内容里的 {SPEC_ROOT}/<change-name> 等也能被替换
|
|
715
|
+
let promptText = resolvePromptIncludes(step.prompt)
|
|
693
716
|
// 替换 prompt 中的占位符
|
|
694
717
|
if (projectName && promptText.includes('<project>')) {
|
|
695
718
|
promptText = promptText.replace(/<project>/g, projectName)
|
|
@@ -721,6 +744,19 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
721
744
|
if (changeName && promptText.includes('<quick-session-id>')) {
|
|
722
745
|
promptText = promptText.replace(/<quick-session-id>/g, changeName)
|
|
723
746
|
}
|
|
747
|
+
// 替换 <quicklog-id> 占位符(quick 阶段:从 session guard.json 读 CLI 分配的 ql-ID,
|
|
748
|
+
// 供 agent 在模块文档变更索引等处引用)
|
|
749
|
+
if (promptText.includes('<quicklog-id>')) {
|
|
750
|
+
const specBaseQl = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
751
|
+
let qlIdVal = ''
|
|
752
|
+
try {
|
|
753
|
+
const sessionGuardFile = join(specBaseQl, '.runtime', 'quick-sessions', changeName, 'guard.json')
|
|
754
|
+
if (existsSync(sessionGuardFile)) {
|
|
755
|
+
qlIdVal = JSON.parse(readFileSync(sessionGuardFile, 'utf8')).quicklogId || ''
|
|
756
|
+
}
|
|
757
|
+
} catch {}
|
|
758
|
+
promptText = promptText.replace(/<quicklog-id>/g, qlIdVal || '(未分配)')
|
|
759
|
+
}
|
|
724
760
|
// 替换 <linked-changes> 占位符(quick 阶段:从 .runtime/quick-sessions/<sessionId>/guard.json 读关联变更)
|
|
725
761
|
if (promptText.includes('<linked-changes>')) {
|
|
726
762
|
const specBaseLc = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
@@ -1553,6 +1589,18 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1553
1589
|
process.exit(1)
|
|
1554
1590
|
}
|
|
1555
1591
|
|
|
1592
|
+
// --change 变更名存在性校验(治 cwd 漂移误匹配,缺陷 execute-in-place-windows-pitfalls 坑5):
|
|
1593
|
+
// 必须在 pm.read/initChange 之前——initChange 会为 --change 指定的新名字创建 changes/ 目录,
|
|
1594
|
+
// 校验若在其后则目录已存在、永远通过。用原始 changeName(--change 参数),覆盖 plan/execute/
|
|
1595
|
+
// verify/archive;quick-<8hex> sessionId 与 brainstorm 新建豁免(见 validateChangeExists)。
|
|
1596
|
+
const changeMissing = validateChangeExists(specBase, stageName, changeName)
|
|
1597
|
+
if (changeMissing) {
|
|
1598
|
+
console.error(`❌ ${changeMissing.message}`)
|
|
1599
|
+
console.error(` 可能 cwd 漂移——当前 cwd=${cwd},命中的 spec=${specBase}。`)
|
|
1600
|
+
console.error(` 若意图操作别的项目,请 cd 到对应项目根,或用 --spec-dir 指定正确的 spec 目录。`)
|
|
1601
|
+
process.exit(1)
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1556
1604
|
let progress = await pm.read(cwd, changeName)
|
|
1557
1605
|
|
|
1558
1606
|
if (!progress) {
|
|
@@ -1740,7 +1788,7 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1740
1788
|
}
|
|
1741
1789
|
|
|
1742
1790
|
// 默认:输出当前步骤
|
|
1743
|
-
return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts, { quickFiles, isAllowNew, isForceBaseline, isForceRescan, linkedChanges })
|
|
1791
|
+
return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts, { quickFiles, isAllowNew, isForceBaseline, isForceRescan, linkedChanges, taskDescription: inputText })
|
|
1744
1792
|
}
|
|
1745
1793
|
|
|
1746
1794
|
/**
|
|
@@ -2000,45 +2048,67 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
2000
2048
|
console.log(`🔄 ${stageName} 阶段已自动重置,重新开始。\n`)
|
|
2001
2049
|
}
|
|
2002
2050
|
|
|
2003
|
-
// quick 阶段:记录 baselineFiles
|
|
2004
|
-
|
|
2051
|
+
// quick 阶段:记录 baselineFiles + 分配 ql-ID(CLI 接管 QUICKLOG 写入)
|
|
2052
|
+
// 幂等判据是 session guard.json 文件(跨进程可靠),不是 progress.quickGuard
|
|
2053
|
+
// (D-003@v1:顶层 quickGuard 不跨进程持久化;agent 在 step 间用 `run quick` 取下一步
|
|
2054
|
+
// prompt 时每个新进程都进此块,按文件判幂等才不会重复分配 ql-ID / 重复写条目)。
|
|
2055
|
+
if (stageName === 'quick') {
|
|
2056
|
+
const sessionGuardDir = join(specBase, '.runtime', 'quick-sessions', changeName)
|
|
2057
|
+
const guardFile = join(sessionGuardDir, 'guard.json')
|
|
2058
|
+
let existingGuard = null
|
|
2005
2059
|
try {
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
//
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2060
|
+
if (existsSync(guardFile)) existingGuard = JSON.parse(readFileSync(guardFile, 'utf8'))
|
|
2061
|
+
} catch {}
|
|
2062
|
+
if (existingGuard) {
|
|
2063
|
+
// 跨进程重入:复用已分配的 ql-ID,跳过 baseline 重捕与分配(幂等)
|
|
2064
|
+
progress.quickGuard = existingGuard
|
|
2065
|
+
} else {
|
|
2066
|
+
try {
|
|
2067
|
+
const { execSync } = await import('child_process')
|
|
2068
|
+
const gitStatus = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 10000 })
|
|
2069
|
+
// 记录全部预存脏文件(含 untracked + .sillyspec/ 路径)。quick 会话期间自身写入的元数据
|
|
2070
|
+
// (quicklog/.runtime/modules/_module-map 等)由 auditQuickCompletion 的 isQuickMetadata 精确豁免,
|
|
2071
|
+
// 不需要这里粗放过滤 .sillyspec/——旧过滤致预存 untracked .sillyspec/changes/ 不进 baseline,
|
|
2072
|
+
// 却在 audit 被当「危险(.sillyspec/)+新增」误判永久 blocked(ql-20260713-002-7628 修复)。
|
|
2073
|
+
const baselineFiles = gitStatus
|
|
2074
|
+
.trim().split('\n').filter(Boolean)
|
|
2075
|
+
.map(line => line.slice(3).trim())
|
|
2076
|
+
const allowedFiles = quickOpts?.quickFiles || []
|
|
2077
|
+
const allowNew = quickOpts?.isAllowNew || false
|
|
2078
|
+
const forceBaseline = quickOpts?.isForceBaseline || false
|
|
2079
|
+
const linkedChanges = Array.isArray(quickOpts?.linkedChanges) ? quickOpts.linkedChanges : []
|
|
2080
|
+
// CLI 接管:分配 ql-ID + 写 QUICKLOG「进行中」条目 + 关联 tasks.md(持锁、当天唯一)
|
|
2081
|
+
const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
|
|
2082
|
+
const { qlId } = await allocateQuicklogEntry(specBase, gitUser, {
|
|
2083
|
+
description: quickOpts?.taskDescription || '',
|
|
2084
|
+
linkedChanges,
|
|
2085
|
+
allowedFiles,
|
|
2086
|
+
})
|
|
2087
|
+
progress.quickGuard = {
|
|
2088
|
+
sessionId: changeName,
|
|
2089
|
+
name_zh: '快速任务守卫',
|
|
2090
|
+
baselineCommit: safeGit(cwd, ['rev-parse', 'HEAD']).value,
|
|
2091
|
+
baselineFiles,
|
|
2092
|
+
allowedFiles,
|
|
2093
|
+
allowNew,
|
|
2094
|
+
forceBaseline,
|
|
2095
|
+
linkedChanges,
|
|
2096
|
+
quicklogId: qlId,
|
|
2097
|
+
startedAt: new Date().toISOString(),
|
|
2098
|
+
}
|
|
2099
|
+
// 写入 .runtime/quick-sessions/<sessionId>/guard.json 供 worktree-guard hook 读取
|
|
2100
|
+
// (D-002:按 session 存,多会话各自 guard 不互覆盖。runStage 作用域内 sessionId == changeName == quick-<uuid8>,见 §4.4/4.5)
|
|
2101
|
+
mkdirSync(sessionGuardDir, { recursive: true })
|
|
2102
|
+
writeFileSync(guardFile, JSON.stringify(progress.quickGuard, null, 2))
|
|
2103
|
+
const parts = [`${baselineFiles.length} 个已有脏文件`]
|
|
2104
|
+
if (allowedFiles.length > 0) parts.push(`${allowedFiles.length} 个 allowedFiles`)
|
|
2105
|
+
if (allowNew) parts.push('允许新增文件')
|
|
2106
|
+
console.log(`🛡️ quick 变更边界已记录: ${parts.join(', ')}`)
|
|
2107
|
+
console.log(`📝 QUICKLOG 条目已创建: ${qlId}`)
|
|
2108
|
+
await pm._write(cwd, progress, changeName)
|
|
2109
|
+
} catch (e) {
|
|
2110
|
+
console.warn(`⚠️ baseline 记录失败: ${e.message}`)
|
|
2028
2111
|
}
|
|
2029
|
-
// 写入 .runtime/quick-sessions/<sessionId>/guard.json 供 worktree-guard hook 读取
|
|
2030
|
-
// (D-002:按 session 存,多会话各自 guard 不互覆盖。runStage 作用域内 sessionId == changeName == quick-<uuid8>,见 §4.4/4.5)
|
|
2031
|
-
const sessionGuardDir = join(specBase, '.runtime', 'quick-sessions', changeName)
|
|
2032
|
-
mkdirSync(sessionGuardDir, { recursive: true })
|
|
2033
|
-
const guardFile = join(sessionGuardDir, 'guard.json')
|
|
2034
|
-
writeFileSync(guardFile, JSON.stringify(progress.quickGuard, null, 2))
|
|
2035
|
-
const parts = [`${baselineFiles.length} 个已有脏文件`]
|
|
2036
|
-
if (allowedFiles.length > 0) parts.push(`${allowedFiles.length} 个 allowedFiles`)
|
|
2037
|
-
if (allowNew) parts.push('允许新增文件')
|
|
2038
|
-
console.log(`🛡️ quick 变更边界已记录: ${parts.join(', ')}`)
|
|
2039
|
-
await pm._write(cwd, progress, changeName)
|
|
2040
|
-
} catch (e) {
|
|
2041
|
-
console.warn(`⚠️ baseline 记录失败: ${e.message}`)
|
|
2042
2112
|
}
|
|
2043
2113
|
}
|
|
2044
2114
|
|
|
@@ -3011,61 +3081,118 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
3011
3081
|
if (steps[wsIdx].waitReason) console.log(` 原因:${steps[wsIdx].waitReason}`)
|
|
3012
3082
|
return { stageCompleted: false, currentIdx, nextPendingIdx: -1 }
|
|
3013
3083
|
}
|
|
3014
|
-
// quick
|
|
3084
|
+
// quick 收尾:强校验 QUICKLOG 条目 + 翻状态 + 勾 tasks.md(CLI 接管)
|
|
3015
3085
|
if (stageName === 'quick') {
|
|
3016
|
-
|
|
3017
|
-
const hasQuicklog = existsSync(quicklogDir) && readdirSync(quicklogDir).some(f => f.endsWith('.md') && f.startsWith('QUICKLOG'))
|
|
3018
|
-
if (!hasQuicklog) {
|
|
3019
|
-
console.error(`\n❌ quick 阶段完成校验失败:未检测到 QUICKLOG 记录文件。`)
|
|
3020
|
-
console.error(` step 2 要求创建 quicklog 记录,但文件不存在。`)
|
|
3021
|
-
console.error(` 请先创建 quicklog 记录再 --done。`)
|
|
3022
|
-
return { stageCompleted: false, currentIdx, nextPendingIdx: -1 }
|
|
3023
|
-
}
|
|
3024
|
-
// §4.6 quick 收尾:从 session guard.json 读 guard(不依赖 progress.quickGuard)。
|
|
3086
|
+
// §4.6 从 session guard.json 读 guard(不依赖 progress.quickGuard)。
|
|
3025
3087
|
// D-003@v1:progress._write 不持久化顶层 quickGuard,跨进程 --done 时读出的 progress 无 quickGuard,
|
|
3026
3088
|
// 若仍用 if (progress.quickGuard) 驱动收尾会整体跳过,导致 .runtime/quick-sessions/<sessionId>/ 残留僵尸。
|
|
3027
3089
|
// 改为从文件读 guard:优先 session 目录 guard.json,回退旧单文件 quick-guard.json(task-03 前兼容)。
|
|
3028
3090
|
// sessionId == changeName == quick-<uuid8>(completeStep 作用域内 changeName 已解构自 options)。
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3091
|
+
const runtimeBase = platformOpts.runtimeRoot || join(specBase, '.runtime')
|
|
3092
|
+
const sessionGuardFile = join(runtimeBase, 'quick-sessions', changeName, 'guard.json')
|
|
3093
|
+
const legacyGuardFile = join(specBase, '.runtime', 'quick-guard.json')
|
|
3094
|
+
let guard = null
|
|
3095
|
+
try {
|
|
3096
|
+
guard = existsSync(sessionGuardFile)
|
|
3097
|
+
? JSON.parse(readFileSync(sessionGuardFile, 'utf8'))
|
|
3098
|
+
: (existsSync(legacyGuardFile) ? JSON.parse(readFileSync(legacyGuardFile, 'utf8')) : null)
|
|
3099
|
+
} catch {}
|
|
3100
|
+
|
|
3101
|
+
// 强校验 / 收尾:本会话必须有一条真实 QUICKLOG 条目(治「报 SAFE 但漏写」bug)。
|
|
3102
|
+
// guard 缺失(brownfield:新代码前启动的会话)不阻断——兜底补写一条记录,保住「完成必有记录」不变量。
|
|
3103
|
+
const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
|
|
3104
|
+
let qlId = guard?.quicklogId || null
|
|
3105
|
+
const linkedChanges = Array.isArray(guard?.linkedChanges) ? guard.linkedChanges : []
|
|
3106
|
+
|
|
3107
|
+
// 审计:仅在有 guard 时跑(brownfield 无 guard 跳过,兼容 D-003 brownfield 行为)。
|
|
3108
|
+
if (guard) {
|
|
3109
|
+
// --done 的 --force-baseline/--allow-new 并入 guard(与 step1 持久化值取或)。
|
|
3110
|
+
// 修复 ql-20260713-002-7628:旧代码解析了这两个 flag 但只传 {isConfirm} 给审计,
|
|
3111
|
+
// 致 --done --force-baseline 静默无效、用户被误导「重跑 --confirm」也无法解锁。
|
|
3112
|
+
const mergedGuard = {
|
|
3113
|
+
...guard,
|
|
3114
|
+
forceBaseline: guard.forceBaseline || isForceBaseline,
|
|
3115
|
+
allowNew: guard.allowNew || isAllowNew,
|
|
3116
|
+
}
|
|
3117
|
+
const review = await auditQuickCompletion(cwd, mergedGuard, { isConfirm: confirm })
|
|
3118
|
+
printQuickAuditReview(review)
|
|
3119
|
+
if (review.status === 'blocked') {
|
|
3120
|
+
steps[currentIdx].status = 'pending'
|
|
3121
|
+
steps[currentIdx].completedAt = null
|
|
3122
|
+
if (outputText) steps[currentIdx].output = null
|
|
3123
|
+
process.exit(1)
|
|
3057
3124
|
}
|
|
3058
|
-
|
|
3059
|
-
|
|
3125
|
+
progress.lastQuickReview = review
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
// 结果摘要结构校验(最后一步、isDone 且带了 --output 时):--output 是 QUICKLOG「结果:」
|
|
3129
|
+
// 归档的唯一来源,要求按 需求/根因/方案/结果 模板给全(见 stages/quick.js step3 prompt)。
|
|
3130
|
+
// 确定性校验:只查必填字段是否齐全,不判内容质量。缺字段 → 本次不完成(回滚 step 状态 +
|
|
3131
|
+
// exit 1),保留「进行中」条目,agent 补全 --output 后重跑 --done 即可,不丢进度。
|
|
3132
|
+
// 仅 completeQuicklogEntry 会实际持久化时才校验;前两个 step 的 --done output 不入 QUICKLOG,不校验。
|
|
3133
|
+
if (outputText) {
|
|
3134
|
+
const resultCheck = validateQuickResult(outputText)
|
|
3135
|
+
if (!resultCheck.ok) {
|
|
3136
|
+
console.error(`\n❌ quick 结果摘要结构不完整:缺少字段 ${resultCheck.missing.join('、')}`)
|
|
3137
|
+
console.error(` --output 会写入 QUICKLOG「结果:」,请按模板补全再重跑 --done:`)
|
|
3138
|
+
console.error(` 需求:用户/任务要什么`)
|
|
3139
|
+
console.error(` 根因:为什么这样改(纯新增/样式则写「无,纯新增/纯样式」)`)
|
|
3140
|
+
console.error(` 方案:怎么改的`)
|
|
3141
|
+
console.error(` 结果:验证情况(测试数 / lint / typecheck / 部署状态)`)
|
|
3142
|
+
steps[currentIdx].status = 'pending'
|
|
3143
|
+
steps[currentIdx].completedAt = null
|
|
3144
|
+
if (outputText) steps[currentIdx].output = null
|
|
3145
|
+
process.exit(1)
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
if (!qlId) {
|
|
3150
|
+
// 无 ql-ID(guard 缺失或 brownfield 无 quicklogId):补分配后立即完成,不阻断。
|
|
3060
3151
|
try {
|
|
3061
|
-
const
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3152
|
+
const alloc = await allocateQuicklogEntry(specBase, gitUser, {
|
|
3153
|
+
description: guard?.taskDescription || '(补分配)',
|
|
3154
|
+
linkedChanges,
|
|
3155
|
+
allowedFiles: Array.isArray(guard?.allowedFiles) ? guard.allowedFiles : [],
|
|
3156
|
+
})
|
|
3157
|
+
qlId = alloc.qlId
|
|
3158
|
+
console.log(`📝 QUICKLOG 兜底补写: ${qlId}(guard 缺失/brownfield 会话)`)
|
|
3159
|
+
} catch (e) {
|
|
3160
|
+
console.error(`\n❌ QUICKLOG 补分配失败: ${e.message}`)
|
|
3161
|
+
steps[currentIdx].status = 'pending'
|
|
3162
|
+
steps[currentIdx].completedAt = null
|
|
3163
|
+
if (outputText) steps[currentIdx].output = null
|
|
3164
|
+
process.exit(1)
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
if (!findQuicklogEntry(specBase, gitUser, qlId)) {
|
|
3168
|
+
console.error(`\n❌ quick 阶段完成校验失败:QUICKLOG 条目 ${qlId} 不存在。`)
|
|
3169
|
+
console.error(` 会话期间记录被删除或从未写入。请检查 .sillyspec/quicklog/ 后重跑 --done。`)
|
|
3170
|
+
steps[currentIdx].status = 'pending'
|
|
3171
|
+
steps[currentIdx].completedAt = null
|
|
3172
|
+
if (outputText) steps[currentIdx].output = null
|
|
3173
|
+
process.exit(1)
|
|
3174
|
+
}
|
|
3175
|
+
// 翻状态进行中→已完成 + 追加结果 + 勾选关联 tasks.md
|
|
3176
|
+
// resultText 不再截断:结构化结果块(需求/根因/方案/结果)完整落盘,多行写成字段化块。
|
|
3177
|
+
try {
|
|
3178
|
+
await completeQuicklogEntry(specBase, gitUser, qlId, {
|
|
3179
|
+
resultText: outputText || '',
|
|
3180
|
+
linkedChanges,
|
|
3181
|
+
})
|
|
3182
|
+
console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
|
|
3183
|
+
} catch (e) {
|
|
3184
|
+
console.warn(`⚠️ QUICKLOG 完成态写入失败: ${e.message}`)
|
|
3068
3185
|
}
|
|
3186
|
+
|
|
3187
|
+
// 清理 session 目录(rmSync/unlinkSync 容忍不存在)。
|
|
3188
|
+
try {
|
|
3189
|
+
const { unlinkSync } = await import('fs')
|
|
3190
|
+
if (changeName) {
|
|
3191
|
+
const sessionDir = join(runtimeBase, 'quick-sessions', changeName)
|
|
3192
|
+
rmSync(sessionDir, { recursive: true, force: true })
|
|
3193
|
+
}
|
|
3194
|
+
if (existsSync(legacyGuardFile)) unlinkSync(legacyGuardFile)
|
|
3195
|
+
} catch {}
|
|
3069
3196
|
}
|
|
3070
3197
|
|
|
3071
3198
|
stageData.status = 'completed'
|
package/src/stage-contract.js
CHANGED
|
@@ -33,6 +33,36 @@ function resolveChangeDir(cwd, changeName, specRoot = null) {
|
|
|
33
33
|
return join(changesRoot, changeName)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* --change 变更名存在性校验(治 cwd 漂移误匹配,缺陷 execute-in-place-windows-pitfalls 坑5)。
|
|
38
|
+
*
|
|
39
|
+
* 多项目 monorepo 下,cwd 在子项目目录会让 resolveSpecDir 命中子项目 spec,
|
|
40
|
+
* 此时 --change 传根项目变更名,旧逻辑(resolveChangeDir 纯拼路径)不报错而静默
|
|
41
|
+
* fallback 误启动子项目流程。本校验对「操作已有变更」的阶段强制 changes/<changeName>
|
|
42
|
+
* 存在,不存在则 fail-fast,把 cwd 漂移暴露给用户。
|
|
43
|
+
*
|
|
44
|
+
* 豁免:
|
|
45
|
+
* - 非 plan/execute/verify/archive 阶段(scan/brainstorm/quick/explore 不校验:
|
|
46
|
+
* brainstorm 可新建变更、quick 用 sessionId、scan/explore 无 change 语义)
|
|
47
|
+
* - quick-<8hex> sessionId(quick 会话 changeName 是 sessionId,不在 changes/)
|
|
48
|
+
*
|
|
49
|
+
* @param {string} specBase - 规范根目录(.sillyspec 或平台 specRoot)
|
|
50
|
+
* @param {string} stageName - 阶段名
|
|
51
|
+
* @param {string} changeName - 变更名(--change 或 auto)
|
|
52
|
+
* @returns {{ changeName: string, specBase: string, message: string } | null} null=通过,对象=失败
|
|
53
|
+
*/
|
|
54
|
+
export function validateChangeExists(specBase, stageName, changeName) {
|
|
55
|
+
const STAGES_REQUIRE_EXISTING_CHANGE = new Set(['plan', 'execute', 'verify', 'archive'])
|
|
56
|
+
if (!changeName || !STAGES_REQUIRE_EXISTING_CHANGE.has(stageName)) return null
|
|
57
|
+
if (/^quick-[0-9a-f]{8}$/.test(changeName)) return null
|
|
58
|
+
if (existsSync(join(specBase, 'changes', changeName))) return null
|
|
59
|
+
return {
|
|
60
|
+
changeName,
|
|
61
|
+
specBase,
|
|
62
|
+
message: `变更 "${changeName}" 在当前 spec 下不存在:${join(specBase, 'changes', changeName)}`,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
36
66
|
function collectIdsFromLine(line, re, ids) {
|
|
37
67
|
for (const match of line.matchAll(re)) {
|
|
38
68
|
ids.add(match[0].toUpperCase())
|