sillyspec 3.25.6 → 3.25.7
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/init.js +69 -0
- package/src/quicklog.js +41 -2
- package/src/run/complete.js +83 -2
- package/src/run/shared.js +14 -1
- package/src/run/stage.js +8 -2
- package/src/stage-review.js +1 -1
- package/templates/claude-instruction.md +23 -0
- package/CLAUDE.md +0 -40
package/package.json
CHANGED
package/src/init.js
CHANGED
|
@@ -123,6 +123,70 @@ function injectInstructions(tool, projectDir) {
|
|
|
123
123
|
writeFileSync(filePath, content.trimEnd() + '\n\n' + INJECTION_CONTENT);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// ── 注入 CLAUDE.md(claude,版本感知幂等)──
|
|
127
|
+
// 与 codex/gemini/opencode 不同:claude 需要完整 CLAUDE.md 模板 + 版本感知三态。
|
|
128
|
+
// 故 claude 不进 INSTRUCTION_TOOLS,单独走此函数(design: 2026-08-02-init-claude-md)。
|
|
129
|
+
const CLAUDE_TEMPLATE_PATH = join(__dirname, '..', 'templates', 'claude-instruction.md');
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 为 Claude Code 注入 CLAUDE.md(版本感知幂等)。
|
|
133
|
+
* - 不存在:写完整模板(templates/claude-instruction.md)+ 顶部版本注释
|
|
134
|
+
* - 存在无标记:追加受管段(INJECTION_CONTENT)+ 版本块标记
|
|
135
|
+
* - 存在同版本标记:跳过
|
|
136
|
+
* - 存在异版本标记:追加态刷新块 / 完整态仅 stderr 提示
|
|
137
|
+
* @param {string} projectDir - 源码项目根
|
|
138
|
+
*/
|
|
139
|
+
export function injectClaudeInstructions(projectDir) {
|
|
140
|
+
const filePath = join(projectDir, 'CLAUDE.md');
|
|
141
|
+
const version = getVersion();
|
|
142
|
+
// 完整态顶部注释(新文件首行,轻量、不限制后续编辑)
|
|
143
|
+
const fullHeader = `<!-- SillySpec v${version} — 由 sillyspec init 生成,可自由编辑;重跑 init 同版本不更新 -->`;
|
|
144
|
+
// 追加态受管段(包版本块标记,明确"勿手动编辑此段")
|
|
145
|
+
const appendBlock =
|
|
146
|
+
`<!-- SillySpec v${version} START — 由 sillyspec init 注入,勿手动编辑此段 -->\n` +
|
|
147
|
+
INJECTION_CONTENT.trimEnd() +
|
|
148
|
+
`\n<!-- SillySpec END -->`;
|
|
149
|
+
|
|
150
|
+
// 状态 1:文件不存在 → 写完整模板 + 顶部版本注释
|
|
151
|
+
if (!existsSync(filePath)) {
|
|
152
|
+
let template = '';
|
|
153
|
+
try {
|
|
154
|
+
template = readFileSync(CLAUDE_TEMPLATE_PATH, 'utf8');
|
|
155
|
+
} catch {
|
|
156
|
+
console.error(`❌ [sillyspec] 未找到 Claude 模板:${CLAUDE_TEMPLATE_PATH}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
writeFileSync(filePath, fullHeader + '\n' + template);
|
|
160
|
+
console.log(chalk.green(` ✓ Claude Code CLAUDE.md 已生成(SillySpec v${version})`));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const content = readFileSync(filePath, 'utf8');
|
|
165
|
+
|
|
166
|
+
// 无任何 `<!-- SillySpec v` 标记 → 状态 2:追加受管段(原文字节保留)
|
|
167
|
+
const markMatch = content.match(/<!-- SillySpec v(\S+)/);
|
|
168
|
+
if (!markMatch) {
|
|
169
|
+
writeFileSync(filePath, content.trimEnd() + '\n\n' + appendBlock + '\n');
|
|
170
|
+
console.log(chalk.green(` ✓ Claude Code CLAUDE.md 已追加 SillySpec 受管段(v${version})`));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const existingVersion = markMatch[1];
|
|
175
|
+
// 状态 3:同版本 → 跳过(幂等,不写)
|
|
176
|
+
if (existingVersion === version) return;
|
|
177
|
+
|
|
178
|
+
// 状态 4:异版本(升级)。区分追加态(有 START...END 块)/ 完整态(仅顶部注释)。
|
|
179
|
+
const startBlockRe = /<!-- SillySpec v\S+\s+START[\s\S]*?<!-- SillySpec END -->/;
|
|
180
|
+
if (startBlockRe.test(content)) {
|
|
181
|
+
// 4a:追加态 → 用当前版本受管段替换该块(块外用户内容字节保留)
|
|
182
|
+
writeFileSync(filePath, content.replace(startBlockRe, appendBlock));
|
|
183
|
+
console.log(chalk.green(` ✓ Claude Code CLAUDE.md 受管段已升级(v${existingVersion} → v${version})`));
|
|
184
|
+
} else {
|
|
185
|
+
// 4b:完整态 → 不覆盖(保留用户改动),仅 stderr 打印升级提示
|
|
186
|
+
console.error(`⚠️ [sillyspec] SillySpec 升级 v${existingVersion}→v${version},CLAUDE.md 未自动更新(保留你的改动)。如需采用新模板:备份后删除 CLAUDE.md 再跑 sillyspec init。`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
126
190
|
// ── 检测工具 ──
|
|
127
191
|
|
|
128
192
|
function detectTools(projectDir) {
|
|
@@ -273,6 +337,11 @@ async function doInstall(projectDir, tools, subprojects = [], specDir = null) {
|
|
|
273
337
|
}
|
|
274
338
|
}
|
|
275
339
|
|
|
340
|
+
// 注入 CLAUDE.md(claude,版本感知幂等三态四分支;claude 不进 INSTRUCTION_TOOLS)
|
|
341
|
+
if (tools.includes('claude')) {
|
|
342
|
+
injectClaudeInstructions(projectDir);
|
|
343
|
+
}
|
|
344
|
+
|
|
276
345
|
// 复制 skills 到各工具目录
|
|
277
346
|
const skillToolDirs = {
|
|
278
347
|
claude: '.claude/skills',
|
package/src/quicklog.js
CHANGED
|
@@ -81,6 +81,36 @@ function sanitizeDesc(description) {
|
|
|
81
81
|
return s.length > 120 ? s.slice(0, 120) + '…' : s
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// 从关联变更的 proposal/design 提取首个 # 标题(去「提案书(Proposal)— / 设计文档(Design)—」固定前缀)。
|
|
85
|
+
// 让 quick --linked-changes 启动时(用户常不带 --input)也能拿到语义标题,而非 (quick 任务) 占位。
|
|
86
|
+
// 读不到任何标题返回 ''(调用方再回退占位符,保持向后兼容)。
|
|
87
|
+
export function deriveTitleFromLinkedChange(specBase, change) {
|
|
88
|
+
if (!change) return ''
|
|
89
|
+
for (const f of ['proposal.md', 'design.md']) {
|
|
90
|
+
let content
|
|
91
|
+
try { content = readFileSync(join(specBase, 'changes', change, f), 'utf8') } catch { continue }
|
|
92
|
+
const m = content.match(/^#\s+(.+?)\s*$/m)
|
|
93
|
+
if (!m) continue
|
|
94
|
+
const raw = m[1].trim()
|
|
95
|
+
// 标题惯例「# 提案书(Proposal)— <desc>」「# 设计文档(Design)— <desc>」→ 取破折号后的 desc
|
|
96
|
+
const dash = raw.match(/[—-]\s*(.+)$/)
|
|
97
|
+
return dash ? dash[1].trim() : raw
|
|
98
|
+
}
|
|
99
|
+
return ''
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 从 quick step3 --output 四字段提取「需求:」摘要,用于翻完成时刷新标题行
|
|
103
|
+
// (覆盖启动时的占位/弱标题)。提取失败返回 ''(不刷新)。
|
|
104
|
+
function extractTitleFromResult(result) {
|
|
105
|
+
if (!result) return ''
|
|
106
|
+
const m = String(result).match(/需求:([^\n\r]*?)(?:\s+根因:|$)/)
|
|
107
|
+
if (!m) return ''
|
|
108
|
+
let t = m[1].replace(/[,。;,;].*$/, '').trim() // 截到首个标点,取核心句
|
|
109
|
+
if (!t) return ''
|
|
110
|
+
if (t.length > 80) t = t.slice(0, 80) + '…'
|
|
111
|
+
return t
|
|
112
|
+
}
|
|
113
|
+
|
|
84
114
|
function sanitizeResult(resultText) {
|
|
85
115
|
// 保留换行:结果块支持字段化结构(需求:/根因:/方案:/结果:...)。多行时 flipEntryInContent
|
|
86
116
|
// 写成字段块(追加在「状态:」行下方),不是单条「结果:<长行>」。仅去首尾空白。
|
|
@@ -189,10 +219,13 @@ function tasksPath(specBase, change) {
|
|
|
189
219
|
return join(specBase, 'changes', change, 'tasks.md')
|
|
190
220
|
}
|
|
191
221
|
|
|
192
|
-
// 追加未勾选 task;幂等(已含同 qlId
|
|
222
|
+
// 追加未勾选 task;幂等(已含同 qlId 则跳过)。
|
|
223
|
+
// 关联变更目录不存在(笔误 / 未建 / 仅作标签关联)→ 不 fabricate stub 目录(历史坑 quick-change-phantom:
|
|
224
|
+
// mkdirSync 硬造 changes/<名>/tasks.md,致 quick --done 边界审计自造自拦 BLOCK)。关联仍记入
|
|
225
|
+
// QUICKLOG「关联变更」行(allocateQuicklogEntry 独立写入,不依赖本函数)。
|
|
193
226
|
function appendTaskCheckbox(specBase, change, qlId, desc) {
|
|
194
227
|
const dir = join(specBase, 'changes', change)
|
|
195
|
-
|
|
228
|
+
if (!existsSync(dir)) return
|
|
196
229
|
const p = tasksPath(specBase, change)
|
|
197
230
|
let content = existsSync(p) ? readFileSync(p, 'utf8') : ''
|
|
198
231
|
if (content.includes(qlId)) return
|
|
@@ -219,6 +252,12 @@ function flipEntryInContent(content, qlId, result, changedFiles = []) {
|
|
|
219
252
|
const lines = content.split('\n')
|
|
220
253
|
const startIdx = lines.findIndex(l => l.startsWith(`## ${qlId} |`))
|
|
221
254
|
if (startIdx === -1) return null
|
|
255
|
+
// 翻完成时若能从结果提取「需求:」摘要,刷新标题行(覆盖启动时的占位/弱标题)
|
|
256
|
+
const newTitle = extractTitleFromResult(result)
|
|
257
|
+
if (newTitle) {
|
|
258
|
+
const headerCore = lines[startIdx].match(/^(## \S+ \| [^|]+ \| )/)
|
|
259
|
+
if (headerCore) lines[startIdx] = headerCore[1] + newTitle
|
|
260
|
+
}
|
|
222
261
|
let endIdx = lines.length
|
|
223
262
|
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
224
263
|
if (lines[i].startsWith('## ')) { endIdx = i; break }
|
package/src/run/complete.js
CHANGED
|
@@ -54,6 +54,22 @@ function validateMetadata(cwd, stageName, specBase) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* 读取 design.md frontmatter 的 scale 字段(brainstorm 末步写入 'small'|'large')。
|
|
59
|
+
* completeStep 的下一步提示据此分叉:small→quick,large/读不到→plan(fail-safe 走重流程)。
|
|
60
|
+
* 只解析首个 YAML frontmatter 块,避免误读正文里的 "scale:"。
|
|
61
|
+
*/
|
|
62
|
+
function readDesignScale(specBase, changeName) {
|
|
63
|
+
if (!changeName) return null
|
|
64
|
+
const designPath = join(specBase, 'changes', changeName, 'design.md')
|
|
65
|
+
if (!existsSync(designPath)) return null
|
|
66
|
+
const text = readFileSync(designPath, 'utf8')
|
|
67
|
+
const fm = text.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
|
|
68
|
+
if (!fm) return null
|
|
69
|
+
const m = fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
|
|
70
|
+
return m ? m[1] : null
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
/**
|
|
58
74
|
* 验证关键文件是否存在于正确的变更目录下
|
|
59
75
|
* 防止 AI 将文件写到错误的路径
|
|
@@ -100,6 +116,36 @@ function validateFileLocations(cwd, stageName, progress, changeName, specBase) {
|
|
|
100
116
|
}
|
|
101
117
|
}
|
|
102
118
|
|
|
119
|
+
/**
|
|
120
|
+
* 坑1:用 --done --answer 解掉「已 waiting 的步骤」。
|
|
121
|
+
*
|
|
122
|
+
* completeStep 的 currentIdx 选择(findIndex pending||in-progress)排除 waiting,导致
|
|
123
|
+
* --done --answer 落到已 --wait 暂停的 requiresWait 步骤时跳过它、--answer 静默丢失、步骤永久
|
|
124
|
+
* 卡 WAITING、末步报「Step N 等待用户输入」无法 finish。本函数把首个 waiting 步骤拉回 pending
|
|
125
|
+
* + 补 waitAnswer(对齐 continueStep 对 requiresWait 的 shouldReturnToCurrentStep=回 pending 语义),
|
|
126
|
+
* 让主流程 requiresWait 门控见 waitAnswer 已置→不阻断→正常 completed。
|
|
127
|
+
*
|
|
128
|
+
* @param {object[]} steps - stageData.steps(原地修改)
|
|
129
|
+
* @param {string|null|undefined} doneAnswer - --answer 值;空则不触发(返回 -1)
|
|
130
|
+
* @param {string} nowStr - 时间戳字符串(用于 waitAnswers[].answeredAt)
|
|
131
|
+
* @returns {number} 被解的 waiting 步骤 idx;无 waiting 或无 answer 返回 -1
|
|
132
|
+
*/
|
|
133
|
+
export function resolveWaitingStepWithAnswer(steps, doneAnswer, nowStr) {
|
|
134
|
+
if (!doneAnswer) return -1
|
|
135
|
+
const waitIdx = steps.findIndex(s => s && s.status === 'waiting')
|
|
136
|
+
if (waitIdx === -1) return -1
|
|
137
|
+
const ws = steps[waitIdx]
|
|
138
|
+
ws.waitAnswer = doneAnswer
|
|
139
|
+
ws.waitAnswers = Array.isArray(ws.waitAnswers) ? ws.waitAnswers : []
|
|
140
|
+
ws.waitAnswers.push({ round: (ws.waitRound || 0) + 1, answer: doneAnswer, answeredAt: nowStr })
|
|
141
|
+
delete ws.waitReason
|
|
142
|
+
delete ws.waitOptions
|
|
143
|
+
delete ws.waitedAt
|
|
144
|
+
ws.status = 'pending'
|
|
145
|
+
ws.completedAt = null
|
|
146
|
+
return waitIdx
|
|
147
|
+
}
|
|
148
|
+
|
|
103
149
|
export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
104
150
|
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, confirmMode = null, isForceBaseline = false, isAllowNew = false } = options
|
|
105
151
|
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
@@ -129,7 +175,17 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
129
175
|
}
|
|
130
176
|
|
|
131
177
|
const steps = stageData.steps
|
|
132
|
-
|
|
178
|
+
let currentIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
179
|
+
// ── 坑1:--done --answer 解「已 waiting 的步骤」──
|
|
180
|
+
// findIndex 仅查 pending/in-progress,排除 waiting;故 --done --answer 落到已 --wait 暂停的
|
|
181
|
+
// requiresWait 步骤时会跳过它、把 --answer 静默丢弃,步骤永久卡 WAITING、末步报「等待用户输入」。
|
|
182
|
+
// 修复:带 doneAnswer 且存在 waiting 步骤时,把首个 waiting 拉回 pending + 补 waitAnswer,主流程
|
|
183
|
+
// requiresWait 门控见 waitAnswer 已置→不阻断→正常 completed。仅 --answer 触发,普通 --done 零变化。
|
|
184
|
+
const _resolvedWaitIdx = resolveWaitingStepWithAnswer(steps, options && options.doneAnswer, new Date().toLocaleString('zh-CN', { hour12: false }))
|
|
185
|
+
if (_resolvedWaitIdx !== -1) {
|
|
186
|
+
currentIdx = _resolvedWaitIdx
|
|
187
|
+
console.log(`⚠️ Step "${steps[_resolvedWaitIdx].name}" 此前处于 waiting,--done --answer 已补回答并拉回待完成。`)
|
|
188
|
+
}
|
|
133
189
|
if (currentIdx === -1) {
|
|
134
190
|
console.error('没有待完成的步骤')
|
|
135
191
|
process.exit(1)
|
|
@@ -332,6 +388,22 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
332
388
|
} else if (stageName === 'verify') {
|
|
333
389
|
// verify 的"验证通过"提示延后到下方 validator 通过后才打印,
|
|
334
390
|
// 避免校验失败(FAIL / 缺 verify-result.md)时仍声称"验证通过可以归档"。
|
|
391
|
+
} else if (stageName === 'brainstorm') {
|
|
392
|
+
// brainstorm 下一步按 design.md 的 scale 分叉(与末步 prompt 的 large→plan / small→quick 对齐),
|
|
393
|
+
// 不走下方 _getNextSuggestion —— 后者按全局状态机「第一个未完成且上游就绪的阶段」推荐,
|
|
394
|
+
// 当 scan 未完成时会误推 scan(回头路),与 brainstorm 已完成、应进入 plan/quick 矛盾。
|
|
395
|
+
// 历史教训:曾因此让 agent 在 brainstorm 完成后被误导去跑 scan。
|
|
396
|
+
const _bscale = readDesignScale(specBase, changeName)
|
|
397
|
+
if (_bscale === 'small') {
|
|
398
|
+
console.log(`\n👉 brainstorm 已完成(small)。下一步:sillyspec run quick --linked-changes ${changeName}`)
|
|
399
|
+
} else {
|
|
400
|
+
console.log(`\n👉 brainstorm 已完成。下一步:sillyspec run plan${changeName ? ` --change ${changeName}` : ''}(scale=large 或未标 small 走完整 plan)`)
|
|
401
|
+
}
|
|
402
|
+
} else if (stageName === 'quick') {
|
|
403
|
+
// quick 是收尾阶段(辅助流程,不走主链 scan→archive),完成后该提交,而非推 scan。
|
|
404
|
+
// 走下方 _getNextSuggestion 会因 scan 是 STAGE_ORDER 首位、永未完成而误推 scan(回头路),
|
|
405
|
+
// 与 brainstorm 同类问题(见上注释),故给 quick 专属分支。
|
|
406
|
+
console.log('\n👉 quick 已完成。下一步:提交本次改动(git commit / sillyspec-commit),或 sillyspec run <stage> 继续其他阶段。')
|
|
335
407
|
} else {
|
|
336
408
|
// D1 暗衔接修:阶段刚完成,CLI 本就知道状态机下一步是哪个阶段(_getNextSuggestion),
|
|
337
409
|
// 却只丢一句「下一步由你决定」让 agent 自己猜命令。改为按 progress 实际状态给出精确命令。
|
|
@@ -738,7 +810,16 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
738
810
|
// 阶段完成后明确下一步(agent 常卡:stageData completed 但不知要 run <下一阶段> 推进 currentStage)
|
|
739
811
|
const nextStageHint = { brainstorm: 'plan', plan: 'execute', execute: 'verify', verify: 'archive' }[stageName]
|
|
740
812
|
if (nextStageHint) {
|
|
741
|
-
|
|
813
|
+
// brainstorm 按 design.md frontmatter 的 scale 分叉(与末步 prompt 对齐):
|
|
814
|
+
// small → quick --linked-changes(小变更免走完整 plan);large / 读不到 scale → plan(保守默认)。
|
|
815
|
+
// 修历史 bug:此处曾硬编码 brainstorm→plan 不读 scale,small 档与末步 prompt(quick)矛盾、误导小变更进 plan。
|
|
816
|
+
let hintStage = nextStageHint
|
|
817
|
+
let hintChangeFlag = changeName ? ` --change ${changeName}` : ''
|
|
818
|
+
if (stageName === 'brainstorm' && changeName && readDesignScale(specBase, changeName) === 'small') {
|
|
819
|
+
hintStage = 'quick'
|
|
820
|
+
hintChangeFlag = ` --linked-changes ${changeName}`
|
|
821
|
+
}
|
|
822
|
+
console.log(`\n👉 ${stageName} 已完成。下一步:sillyspec run ${hintStage}${hintChangeFlag}`)
|
|
742
823
|
if (stageName === 'execute') {
|
|
743
824
|
console.log(` ⚠️ 若 worktree 改动还没 apply 到主工作区,先:sillyspec worktree apply ${changeName}`)
|
|
744
825
|
console.log(` (apply 不需要先 commit,支持 working tree 未提交改动)`)
|
package/src/run/shared.js
CHANGED
|
@@ -278,6 +278,11 @@ export function resolveChangeDir(cwd, progress, specDir = null) {
|
|
|
278
278
|
/**
|
|
279
279
|
* 触发 sync(平台模式走自己的链路,跳过;否则 await import sync.js)。
|
|
280
280
|
*/
|
|
281
|
+
// sync 总超时熔断:sync.js 是 best-effort 后台回传(每请求已有 10s 超时),但 sync() 可能串行多次
|
|
282
|
+
// fetchJson 累积等待、阻塞 --done。给 8s 总超时,超时放弃(best-effort,失败不影响正确性,下次 --done 重试)。
|
|
283
|
+
// 历史痛点:--done 在 sync 慢时体感 hang,用户被迫用外部 timeout 兜底。
|
|
284
|
+
const SYNC_TOTAL_TIMEOUT_MS = 8_000
|
|
285
|
+
|
|
281
286
|
export async function triggerSync(cwd, changeName, platformOpts = {}) {
|
|
282
287
|
// 平台模式(SillyHub)走自己的回传链路,不走 CLI 内置 sync
|
|
283
288
|
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return
|
|
@@ -285,7 +290,15 @@ export async function triggerSync(cwd, changeName, platformOpts = {}) {
|
|
|
285
290
|
if (changeName && !existsSync(join(cwd, '.sillyspec', 'changes', changeName))) return
|
|
286
291
|
// shared.js 在 src/run/,sync.js 在 src/ → 退一层
|
|
287
292
|
const syncMod = await import('../sync.js')
|
|
288
|
-
|
|
293
|
+
let timer
|
|
294
|
+
try {
|
|
295
|
+
await Promise.race([
|
|
296
|
+
syncMod.sync(changeName, cwd),
|
|
297
|
+
new Promise((resolve) => { timer = setTimeout(resolve, SYNC_TOTAL_TIMEOUT_MS) }),
|
|
298
|
+
])
|
|
299
|
+
} finally {
|
|
300
|
+
clearTimeout(timer) // sync 先完成则清掉未触发的 timer,避免泄漏
|
|
301
|
+
}
|
|
289
302
|
} catch (e) {
|
|
290
303
|
// sync.js 不存在或同步失败,静默跳过
|
|
291
304
|
console.warn('⚠️ 同步失败:', e.message)
|
package/src/run/stage.js
CHANGED
|
@@ -22,7 +22,7 @@ import { writeAtomicSync } from '../fs-atomic.js'
|
|
|
22
22
|
import { resolveSpecDir, resolveChangeDir, triggerSync, safeGit, parsePorcelainPath, formatWaitOptions, checkApproval, getStageSteps } from './shared.js'
|
|
23
23
|
import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck } from './scan-profile.js'
|
|
24
24
|
import { outputStep } from './prompt.js'
|
|
25
|
-
import { allocateQuicklogEntry } from '../quicklog.js'
|
|
25
|
+
import { allocateQuicklogEntry, deriveTitleFromLinkedChange } from '../quicklog.js'
|
|
26
26
|
import { checkTransition } from '../stage-contract.js'
|
|
27
27
|
|
|
28
28
|
export async function runStage(pm, progress, stageName, cwd, changeName, skipApproval = false, platformOpts = {}, quickOpts = {}) {
|
|
@@ -239,8 +239,14 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
239
239
|
const linkedChanges = Array.isArray(quickOpts?.linkedChanges) ? quickOpts.linkedChanges : []
|
|
240
240
|
// CLI 接管:分配 ql-ID + 写 QUICKLOG「进行中」条目 + 关联 tasks.md(持锁、当天唯一)
|
|
241
241
|
const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
|
|
242
|
+
// 标题回退:启动 quick 不带 --input 时,从关联变更的 proposal/design 提取语义标题,
|
|
243
|
+
// 避免 QUICKLOG/tasks.md 落 (quick 任务) 占位(deriveTitleFromLinkedChange 读不到则空→sanitizeDesc 回退占位)。
|
|
244
|
+
let quickDesc = quickOpts?.taskDescription || ''
|
|
245
|
+
if (!quickDesc && linkedChanges.length > 0) {
|
|
246
|
+
quickDesc = deriveTitleFromLinkedChange(specBase, linkedChanges[0])
|
|
247
|
+
}
|
|
242
248
|
const { qlId } = await allocateQuicklogEntry(specBase, gitUser, {
|
|
243
|
-
description:
|
|
249
|
+
description: quickDesc,
|
|
244
250
|
linkedChanges,
|
|
245
251
|
allowedFiles,
|
|
246
252
|
})
|
package/src/stage-review.js
CHANGED
|
@@ -41,7 +41,7 @@ const STAGE_REVIEW_TYPE = { brainstorm: 'design', plan: 'plan', propose: 'propos
|
|
|
41
41
|
*/
|
|
42
42
|
export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier } = {}) {
|
|
43
43
|
if (tier === 'self') {
|
|
44
|
-
return '> review.json 契约:tier=self(
|
|
44
|
+
return '> review.json 契约:tier=self(此刻 design.md 变更文件数 ≤3,当前 agent 自审即可)。⚠️ 注意:本判定基于此刻 design.md 快照——Stage Review Gate 会以 --done 时刻的 design.md 重新判定,若 design 后续扩大到 >3 文件,tier 将升级为 independent 并硬要求独立审查子代理产出 review.json。以 gate 实际校验结果为准(FAILED 即 independent 需补 review.json),勿据本提示提前断言无需 review.json。'
|
|
45
45
|
}
|
|
46
46
|
const reviewType = STAGE_REVIEW_TYPE[stage] || 'design'
|
|
47
47
|
const mainDoc = STAGE_MAIN_DOC[stage] || 'design.md'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Claude Code 指引
|
|
2
|
+
|
|
3
|
+
## 项目说明
|
|
4
|
+
本项目使用 **SillySpec** 管理变更,采用文档驱动开发。SillySpec 是给 Agent 调用的 CLI 流程控制器,不是给人类直接使用的产品,也不是处理业务逻辑的工具——你(Agent)通过 CLI 告诉它"我在哪",它告诉你"下一步做什么";你执行步骤,它校验产出、推进状态,人类只在关键决策点介入审批。要考虑多 agent 同时操作代码,代码随时可能变化。所有变更以稳定、可用、可维护为目标,按生产级标准处理。
|
|
5
|
+
|
|
6
|
+
## 核心规则
|
|
7
|
+
1. **禁止绕过本文件规则和 SillySpec 流程**。所有变更走 sillyspec 流程,不裸改裸提交。
|
|
8
|
+
2. **改代码前必须先说明依据**——依据的文档路径(design.md / 模块文档 / file-lifecycle.md)或现有代码依据,无依据不改。
|
|
9
|
+
3. **新功能 / 大改动走完整流程**:`brainstorm → plan → execute → verify → archive`。
|
|
10
|
+
4. **小修复 / 小调整走 quick**:`sillyspec run quick`。
|
|
11
|
+
5. **执行顺序**:文档 → 读代码 → 写测试 → 写实现 → 跑测试 → 验收 → 更新文档。
|
|
12
|
+
6. **判规模选档**:≤3 文件、范围明确走 `quick`;多阶段 / 架构级走完整流程。
|
|
13
|
+
7. **代码先行不补流程(倒推 B 模式)**:代码若已先写好,**不回头补 brainstorm/plan 装样子**——用 `quick --done` 收尾 + 补 quicklog 条目,把已落盘改动如实登记进进度库。
|
|
14
|
+
8. **实证核验再 `--done`**:`quick --done` 前先 `npm test` + `npm run lint`,以落盘文件与测试结果为准,不信口头"已完成"。
|
|
15
|
+
9. **中途停下用 `sillyspec status` / `sillyspec resume` 存进度**,不直接 commit 半成品。
|
|
16
|
+
10. **实现完成后对照文档验收**(design.md / 模块文档),并检查是否影响已有测试。
|
|
17
|
+
11. **非测试逻辑本身有误时,禁止改测试来"通过"**——修逻辑,不修测试。
|
|
18
|
+
12. **hook 拦截提交时禁止跳过**(`.husky/pre-push`),修复问题后再提交。
|
|
19
|
+
13. **代码必须兼容 Windows / Linux / macOS**(路径 / 换行 / 并发都要顾)。
|
|
20
|
+
14. **任务记录隔离**:永不重置 / reset / 清零已存在的 change;多个活跃 change 各自 `--change <名>` 隔离不重叠;quick 同一 QUICKLOG 按 ql-ID 条目追加,不冲突。
|
|
21
|
+
15. **quicklog 手动精修**:CLI 只写骨架,`--done` 后手动补语义化标题 / 文件多行带括注 / 结果四段。
|
|
22
|
+
16. **代码可能随时在修改**(多 agent 并行),Edit 前重跑 + 查最新态;破坏性 git op 前先备份。
|
|
23
|
+
17. **不奉承用户**,禁止"你说得对"类话术,直接给结论、依据、方案。
|
package/CLAUDE.md
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# Claude Code 指引
|
|
2
|
-
|
|
3
|
-
# SillySpec工具定位
|
|
4
|
-
1. 是给Agent调用的CLI工具,非人类直接使用
|
|
5
|
-
2. 管理Agent工作流的CLI工具,非业务工具
|
|
6
|
-
|
|
7
|
-
## 文件生命周期文档同步
|
|
8
|
-
每次修改 `src/stages/` 下的阶段定义(prompt、步骤、输出文件名等)或 `src/run.js`、`src/progress.js` 等影响文件生命周期的代码后,**必须同步更新** `docs/sillyspec/file-lifecycle.md`,确保文档与代码一致。
|
|
9
|
-
|
|
10
|
-
### 触发更新的典型改动
|
|
11
|
-
- 新增/删除/重命名阶段步骤
|
|
12
|
-
- 修改步骤 prompt 中的输出文件名(如 verify-result.md)
|
|
13
|
-
- 修改阶段间的流转逻辑(如 archive 归档方式)
|
|
14
|
-
- 新增/删除运行时文件类型(如 gate-status.json)
|
|
15
|
-
- 修改 ProgressManager 的数据存储方式(如 SQLite 表结构变更)
|
|
16
|
-
|
|
17
|
-
### 更新检查清单
|
|
18
|
-
- [ ] 文件名引用一致(prompt 输出的文件名 == validateFileLocations 期望的文件名)
|
|
19
|
-
- [ ] 阶段步骤描述与 `src/stages/*.js` 一致
|
|
20
|
-
- [ ] 归档/清理流程描述与实际代码逻辑一致
|
|
21
|
-
- [ ] 数据库 Schema 描述与 `src/db.js` 一致
|
|
22
|
-
- [ ] 更新文档头部 `updated_at` 时间戳
|
|
23
|
-
|
|
24
|
-
## 提示词文档同步(docs/prompt/)
|
|
25
|
-
`docs/prompt/` 收录各阶段 CLI→Agent 的**逐步提示词原文**(人类可读 md 参考)。详见 `docs/prompt/README.md`。
|
|
26
|
-
|
|
27
|
-
### 数据源关系(重要 — 避免双写漂移)
|
|
28
|
-
- **唯一数据源 = `src/stages/*.js`**(`definition.steps[].prompt`,CLI 运行时读这里)+ `src/run/prompt.js`(注入框架:persona / 铁律 / 占位符替换 / 完成后执行命令模板)。
|
|
29
|
-
- `docs/prompt/*.md` 是源码的**机械提取人类可读镜像**,由 `node docs/prompt/_extract.mjs` 一键再生(源码 → `_extracted.json` → md)。提取脚本对静态阶段直接读 `definition.steps`,对动态阶段(plan/execute)用示例输入跑 `buildPlanSteps`/`buildExecuteSteps` 得到真实 prompt。
|
|
30
|
-
- **禁止手改 md 里的 prompt 原文**(下次提取会被覆盖,且与源码不一致)。要改提示词,改源码后重跑脚本。
|
|
31
|
-
|
|
32
|
-
### 触发同步的改动
|
|
33
|
-
- 修改 `src/stages/*.js`:`definition.steps[].prompt`、步骤名、步骤增删/重排、wait 配置、`_globalGuardrails`、title/description
|
|
34
|
-
- 修改 `src/run/prompt.js`:persona / 铁律 / 占位符替换 / 完成后执行命令模板
|
|
35
|
-
- 修改 `stage-review.js` / `review-tier.js` / `knowledge-match.js`:影响 `{REVIEW_JSON_CONTRACT}` / `{REVIEW_TIER}` / `{KNOWLEDGE_HIT_REPORT}` 等动态块
|
|
36
|
-
|
|
37
|
-
### 同步步骤
|
|
38
|
-
1. 改源码
|
|
39
|
-
2. 运行 `node docs/prompt/_extract.mjs`(刷新 `docs/prompt/_extracted.json`)
|
|
40
|
-
3. 按变动同步对应 `docs/prompt/<stage>.md`(prompt 正文以 `_extracted.json` 为准逐字替换)+ 必要时更新 `docs/prompt/README.md`(占位符总表 / persona 表 / CLI 注入框架)
|