sillyspec 3.23.9 → 3.24.1
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-quick/SKILL.md +1 -0
- package/package.json +1 -1
- package/src/quicklog.js +333 -306
- package/src/run.js +61 -5
- package/src/stage-contract.js +44 -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 +3 -3
- package/src/stages/propose.js +1 -1
- package/src/stages/quick.js +29 -18
- 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,9 +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'
|
|
14
|
-
import { allocateQuicklogEntry, completeQuicklogEntry, findQuicklogEntry } from './quicklog.js'
|
|
33
|
+
import { allocateQuicklogEntry, completeQuicklogEntry, findQuicklogEntry, validateQuickResult } from './quicklog.js'
|
|
15
34
|
import { checkbox } from '@inquirer/prompts'
|
|
16
35
|
|
|
17
36
|
/**
|
|
@@ -159,7 +178,7 @@ function resolveSpecDir(cwd, opts = {}) {
|
|
|
159
178
|
return join(resolve(cwd), '.sillyspec')
|
|
160
179
|
}
|
|
161
180
|
import { stageRegistry, auxiliaryStages } from './stages/index.js'
|
|
162
|
-
import { checkTransition, runValidators } from './stage-contract.js'
|
|
181
|
+
import { checkTransition, runValidators, validateChangeExists } from './stage-contract.js'
|
|
163
182
|
import { buildExecuteSteps } from './stages/execute.js'
|
|
164
183
|
import { buildPlanSteps } from './stages/plan.js'
|
|
165
184
|
import { formatExecuteSummary } from './worktree-apply.js'
|
|
@@ -677,7 +696,8 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
677
696
|
console.log(`changeDir: ${changeDir}`)
|
|
678
697
|
}
|
|
679
698
|
console.log(`---\n`)
|
|
680
|
-
|
|
699
|
+
// persona 只在 stage 首步注入(step0)——角色设定一次即可,后续 step 重复注入纯属 token 浪费
|
|
700
|
+
if (personas[stageName] && stepIndex === 0) {
|
|
681
701
|
console.log(personas[stageName])
|
|
682
702
|
console.log('')
|
|
683
703
|
}
|
|
@@ -690,7 +710,9 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
690
710
|
console.log(guardrails.trim())
|
|
691
711
|
console.log('')
|
|
692
712
|
}
|
|
693
|
-
|
|
713
|
+
// 先解析 {{include: name}}(把外部模板片段拉进 prompt),再做下方占位符替换,
|
|
714
|
+
// 保证模板内容里的 {SPEC_ROOT}/<change-name> 等也能被替换
|
|
715
|
+
let promptText = resolvePromptIncludes(step.prompt)
|
|
694
716
|
// 替换 prompt 中的占位符
|
|
695
717
|
if (projectName && promptText.includes('<project>')) {
|
|
696
718
|
promptText = promptText.replace(/<project>/g, projectName)
|
|
@@ -1567,6 +1589,18 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1567
1589
|
process.exit(1)
|
|
1568
1590
|
}
|
|
1569
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
|
+
|
|
1570
1604
|
let progress = await pm.read(cwd, changeName)
|
|
1571
1605
|
|
|
1572
1606
|
if (!progress) {
|
|
@@ -3091,6 +3125,27 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
3091
3125
|
progress.lastQuickReview = review
|
|
3092
3126
|
}
|
|
3093
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
|
+
|
|
3094
3149
|
if (!qlId) {
|
|
3095
3150
|
// 无 ql-ID(guard 缺失或 brownfield 无 quicklogId):补分配后立即完成,不阻断。
|
|
3096
3151
|
try {
|
|
@@ -3118,9 +3173,10 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
3118
3173
|
process.exit(1)
|
|
3119
3174
|
}
|
|
3120
3175
|
// 翻状态进行中→已完成 + 追加结果 + 勾选关联 tasks.md
|
|
3176
|
+
// resultText 不再截断:结构化结果块(需求/根因/方案/结果)完整落盘,多行写成字段化块。
|
|
3121
3177
|
try {
|
|
3122
3178
|
await completeQuicklogEntry(specBase, gitUser, qlId, {
|
|
3123
|
-
resultText: outputText
|
|
3179
|
+
resultText: outputText || '',
|
|
3124
3180
|
linkedChanges,
|
|
3125
3181
|
})
|
|
3126
3182
|
console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
|
package/src/stage-contract.js
CHANGED
|
@@ -33,6 +33,50 @@ 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
|
+
|
|
60
|
+
// archive 特例:step4 --confirm 已把变更从 changes/<name>/ 移到 changes/archive/<date>-<name>/。
|
|
61
|
+
// step5(更新路线图/提交)--change <name> 不应因 changes/<name>/ 已移走而被前置校验误拦。
|
|
62
|
+
// 精确匹配归档目录名(YYYY-MM-DD-<changeName>),避免后缀子串误匹配(auth ≠ deep-auth)。
|
|
63
|
+
if (stageName === 'archive') {
|
|
64
|
+
const archiveRoot = join(specBase, 'changes', 'archive')
|
|
65
|
+
if (existsSync(archiveRoot)) {
|
|
66
|
+
const archived = readdirSync(archiveRoot).find(d => {
|
|
67
|
+
const m = d.match(/^\d{4}-\d{2}-\d{2}-(.+)$/)
|
|
68
|
+
return m && m[1] === changeName
|
|
69
|
+
})
|
|
70
|
+
if (archived) return null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
changeName,
|
|
75
|
+
specBase,
|
|
76
|
+
message: `变更 "${changeName}" 在当前 spec 下不存在:${join(specBase, 'changes', changeName)}`,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
36
80
|
function collectIdsFromLine(line, re, ids) {
|
|
37
81
|
for (const match of line.matchAll(re)) {
|
|
38
82
|
ids.add(match[0].toUpperCase())
|