sillyspec 3.23.8 → 3.23.9
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-commit/SKILL.md +3 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -2
- package/package.json +1 -1
- package/src/quicklog.js +306 -0
- package/src/run.js +156 -85
- package/src/stages/quick.js +15 -41
|
@@ -42,6 +42,9 @@ USER=$(git config user.name 2>/dev/null || echo "default")
|
|
|
42
42
|
cat .sillyspec/quicklog/QUICKLOG-${USER}.md 2>/dev/null
|
|
43
43
|
# 同时扫描活跃变更目录下的归属 quicklog
|
|
44
44
|
for dir in .sillyspec/changes/*/quicklog/*.md; do [ -f "$dir" ] && cat "$dir"; done 2>/dev/null
|
|
45
|
+
# 注:QUICKLOG 现由 CLI 接管写入(src/quicklog.js),描述取 quick 启动的任务参数、
|
|
46
|
+
# 结果取 step3 --output 一句话,粒度可能较粗;条目仍含 标题|时间|描述 + 状态 + 结果 结构,
|
|
47
|
+
# 可直接读取。描述过泛时结合 diff stat 与 tasks.md 补充语义。
|
|
45
48
|
|
|
46
49
|
来源 B — tasks.md(execute 产生的修改):
|
|
47
50
|
LATEST=$(ls -d .sillyspec/changes/*/ 2>/dev/null | grep -v archive | tail -1)
|
|
@@ -14,7 +14,7 @@ description: 用于明确、低风险、范围很小的直接任务。适合用
|
|
|
14
14
|
|
|
15
15
|
quick 阶段的 `--change` 语义是「关联变更」**且会触发步骤重置**,**不要用 `--change` 来指定关联变更**。多活跃变更时改用:
|
|
16
16
|
|
|
17
|
-
- `--linked-changes none`:不关联,仅记 QUICKLOG
|
|
17
|
+
- `--linked-changes none`:不关联,仅记 QUICKLOG(CLI 启动时写入)
|
|
18
18
|
- `--linked-changes a,b`:显式关联到变更 a、b
|
|
19
19
|
- `--non-interactive`:CI/脚本环境,默认不关联(避免交互 prompt 崩溃)
|
|
20
20
|
|
|
@@ -78,7 +78,7 @@ sillyspec run quick --files src/phone.ts,src/phone.test.ts
|
|
|
78
78
|
- **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
|
|
79
79
|
- quick 直接在主工作区改代码(不创建 worktree),范围必须小且明确
|
|
80
80
|
- 完成后立即 `--done`,不跳过
|
|
81
|
-
-
|
|
81
|
+
- QUICKLOG 记录由 **CLI 接管**:启动时 CLI 自动分配 ql-ID 并在 `.sillyspec/quicklog/QUICKLOG-<user>.md` 写「进行中」条目(含关联变更 tasks.md),完成时 CLI 自动翻「已完成」并勾选 task。**你无需手写 QUICKLOG / tasks.md**,只需用注入的 `<quicklog-id>` 在模块文档变更索引引用
|
|
82
82
|
- **禁止**在没有运行 CLI 的情况下自行决定流程
|
|
83
83
|
|
|
84
84
|
## 用户指令
|
package/package.json
CHANGED
package/src/quicklog.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quicklog.js — QUICKLOG 记录的 CLI 接管层
|
|
3
|
+
*
|
|
4
|
+
* 历史问题:QUICKLOG 条目(ql-ID 分配 + 追加)原本由 Agent(LLM)手写
|
|
5
|
+
* (见 src/stages/quick.js 旧 prompt)。两个后果:
|
|
6
|
+
* 1. 漏写静默通过:CLI 只查目录级存在性,agent 漏写仍报「3/3 SAFE」。
|
|
7
|
+
* 2. 并发丢更新:多 quick 会话并发写同一 per-user QUICKLOG 文件,读-改-写非原子,
|
|
8
|
+
* 已有实证(QUICKLOG-qinyi.md 同一 ql-20260604-001-7a4c 出现两次)。
|
|
9
|
+
*
|
|
10
|
+
* 本模块把分配与写入下沉到 CLI 进程内,O_EXCL lockfile 串行化,彻底消除上述问题。
|
|
11
|
+
* 无新 npm 依赖(仅 fs/path/crypto)——匹配项目零 FS 工具依赖的风格。
|
|
12
|
+
*/
|
|
13
|
+
import { join, dirname, basename } from 'path'
|
|
14
|
+
import {
|
|
15
|
+
openSync, closeSync, unlinkSync, statSync, mkdirSync, existsSync,
|
|
16
|
+
readFileSync, writeFileSync, readdirSync, renameSync, appendFileSync,
|
|
17
|
+
} from 'fs'
|
|
18
|
+
import { randomBytes } from 'crypto'
|
|
19
|
+
|
|
20
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* O_EXCL 文件锁。`openSync(lockPath,'wx')` 独占创建;已存在则按 mtime 判 stale 偷锁,
|
|
24
|
+
* 否则退避重试至超时。CLI 短进程、低频写入,stale 阈值 30s 足够。
|
|
25
|
+
* @param {string} lockPath 锁文件路径(建议与被保护文件同目录)
|
|
26
|
+
* @param {() => (any|Promise<any>)} fn 临界区
|
|
27
|
+
* @param {{staleMs?:number, timeoutMs?:number, retryMs?:number}} opts
|
|
28
|
+
*/
|
|
29
|
+
export async function withFileLock(lockPath, fn, opts = {}) {
|
|
30
|
+
const { staleMs = 30000, timeoutMs = 10000, retryMs = 50 } = opts
|
|
31
|
+
mkdirSync(dirname(lockPath), { recursive: true })
|
|
32
|
+
const start = Date.now()
|
|
33
|
+
let fd = null
|
|
34
|
+
// 抢锁循环
|
|
35
|
+
while (true) {
|
|
36
|
+
try {
|
|
37
|
+
fd = openSync(lockPath, 'wx') // O_EXCL:已存在抛 EEXIST
|
|
38
|
+
break
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (e.code !== 'EEXIST') throw e
|
|
41
|
+
// 锁已存在:判 stale(持有进程崩溃残留)
|
|
42
|
+
try {
|
|
43
|
+
const mtime = statSync(lockPath).mtimeMs
|
|
44
|
+
if (Date.now() - mtime > staleMs) {
|
|
45
|
+
try { unlinkSync(lockPath) } catch {} // 偷 stale 锁后重试
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
if (Date.now() - start > timeoutMs) {
|
|
50
|
+
throw new Error(`文件锁超时(${timeoutMs}ms): ${lockPath}`)
|
|
51
|
+
}
|
|
52
|
+
await sleep(retryMs)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return await fn()
|
|
57
|
+
} finally {
|
|
58
|
+
try { closeSync(fd) } catch {}
|
|
59
|
+
try { unlinkSync(lockPath) } catch {} // 释放(容忍已不存在)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── 内部工具 ──
|
|
64
|
+
|
|
65
|
+
function todayStamp(d = new Date()) {
|
|
66
|
+
return d.getFullYear()
|
|
67
|
+
+ String(d.getMonth() + 1).padStart(2, '0')
|
|
68
|
+
+ String(d.getDate()).padStart(2, '0')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function nowDatetime(d = new Date()) {
|
|
72
|
+
return todayStamp(d).slice(0, 4) + '-' + todayStamp(d).slice(4, 6) + '-' + todayStamp(d).slice(6, 8)
|
|
73
|
+
+ ' ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0') + ':' + String(d.getSeconds()).padStart(2, '0')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 描述压成一行、限长,避免破坏 QUICKLOG 条目结构
|
|
77
|
+
function sanitizeDesc(description) {
|
|
78
|
+
const s = String(description || '').replace(/[\r\n]+/g, ' ').trim()
|
|
79
|
+
if (!s) return '(quick 任务)'
|
|
80
|
+
return s.length > 120 ? s.slice(0, 120) + '…' : s
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sanitizeResult(resultText) {
|
|
84
|
+
const s = String(resultText || '').replace(/\n\s*\n/g, ' ').replace(/\r?\n/g, ' ').trim()
|
|
85
|
+
return s
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function escapeRe(s) {
|
|
89
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function listQuicklogFiles(quicklogDir) {
|
|
93
|
+
try {
|
|
94
|
+
return readdirSync(quicklogDir).filter(f => f.endsWith('.md') && f.startsWith('QUICKLOG'))
|
|
95
|
+
} catch {
|
|
96
|
+
return []
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 原子写:同目录临时文件 + rename 覆盖。reader 不持锁(agent cat / dashboard 轮询),
|
|
101
|
+
// 若直接 writeFileSync 覆盖整文件,reader 会在 truncate→write 间隙读到空/半截(reader-writer 竞态)。
|
|
102
|
+
// rename 同文件系统内原子(POSIX rename / Win MoveFileEx REPLACE_EXISTING),
|
|
103
|
+
// reader 永远看到完整旧版或完整新版,绝不读半截。
|
|
104
|
+
// Windows 差异:rename 覆盖「正被另一进程读取的目标」会抛 EPERM/EBUSY(POSIX 无此问题)。
|
|
105
|
+
// reader 句柄是瞬时的,故对占用类错误做异步重试+退避;非占用错误立即抛。
|
|
106
|
+
async function writeAtomic(filePath, content, opts = {}) {
|
|
107
|
+
const { retries = 100, baseDelayMs = 5, maxDelayMs = 100 } = opts
|
|
108
|
+
const tmp = join(dirname(filePath), basename(filePath) + '.tmp-' + process.pid)
|
|
109
|
+
writeFileSync(tmp, content) // 同目录 = 同文件系统,rename 才原子(跨 fs 会退化 copy+delete)
|
|
110
|
+
for (let attempt = 0; ; attempt++) {
|
|
111
|
+
try {
|
|
112
|
+
renameSync(tmp, filePath)
|
|
113
|
+
return
|
|
114
|
+
} catch (e) {
|
|
115
|
+
const retryable = e.code === 'EBUSY' || e.code === 'EPERM' || e.code === 'EACCES'
|
|
116
|
+
if (!retryable || attempt >= retries) {
|
|
117
|
+
try { unlinkSync(tmp) } catch {}
|
|
118
|
+
throw e
|
|
119
|
+
}
|
|
120
|
+
await sleep(Math.min(baseDelayMs * (attempt + 1), maxDelayMs)) // 线性退避,让出 loop
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 扫描所有 QUICKLOG-*.md(含轮转归档)当天最大 NNN + 已用 XXXX 后缀集
|
|
126
|
+
function scanExisting(quicklogDir, today) {
|
|
127
|
+
let maxSeq = 0
|
|
128
|
+
const usedSuffix = new Set()
|
|
129
|
+
const re = /^## ql-(\d{8})-(\d{3})-([0-9a-fA-F]{4})\b/gm
|
|
130
|
+
for (const f of listQuicklogFiles(quicklogDir)) {
|
|
131
|
+
let content = ''
|
|
132
|
+
try { content = readFileSync(join(quicklogDir, f), 'utf8') } catch { continue }
|
|
133
|
+
let m
|
|
134
|
+
re.lastIndex = 0
|
|
135
|
+
while ((m = re.exec(content)) !== null) {
|
|
136
|
+
if (m[1] === today) {
|
|
137
|
+
maxSeq = Math.max(maxSeq, parseInt(m[2], 10))
|
|
138
|
+
usedSuffix.add(m[3].toLowerCase())
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { maxSeq, usedSuffix }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// >500 行则轮转:rename QUICKLOG-<user>.md → QUICKLOG-<user>-<最后记录日期>.md
|
|
146
|
+
async function rotateIfNeeded(userFile, gitUser) {
|
|
147
|
+
if (!existsSync(userFile)) return
|
|
148
|
+
const content = readFileSync(userFile, 'utf8')
|
|
149
|
+
const lines = content.split('\n')
|
|
150
|
+
if (lines.length <= 500) return
|
|
151
|
+
let lastDate = null
|
|
152
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
153
|
+
const m = lines[i].match(/^## ql-(\d{8})-/)
|
|
154
|
+
if (m) { lastDate = m[1]; break }
|
|
155
|
+
}
|
|
156
|
+
const dateStr = lastDate
|
|
157
|
+
? `${lastDate.slice(0, 4)}-${lastDate.slice(4, 6)}-${lastDate.slice(6, 8)}`
|
|
158
|
+
: nowDatetime().slice(0, 10)
|
|
159
|
+
const archiveFile = join(dirname(userFile), `QUICKLOG-${gitUser}-${dateStr}.md`)
|
|
160
|
+
// 同日已轮转过:两步各自原子化(append+clear 无法单原子,reader 容忍中间态——见 writeAtomic 注释)
|
|
161
|
+
if (existsSync(archiveFile)) {
|
|
162
|
+
const existing = readFileSync(archiveFile, 'utf8')
|
|
163
|
+
await writeAtomic(archiveFile, existing + content)
|
|
164
|
+
await writeAtomic(userFile, '')
|
|
165
|
+
} else {
|
|
166
|
+
renameSync(userFile, archiveFile)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function tasksPath(specBase, change) {
|
|
171
|
+
return join(specBase, 'changes', change, 'tasks.md')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 追加未勾选 task;幂等(已含同 qlId 则跳过)
|
|
175
|
+
function appendTaskCheckbox(specBase, change, qlId, desc) {
|
|
176
|
+
const dir = join(specBase, 'changes', change)
|
|
177
|
+
mkdirSync(dir, { recursive: true })
|
|
178
|
+
const p = tasksPath(specBase, change)
|
|
179
|
+
let content = existsSync(p) ? readFileSync(p, 'utf8') : ''
|
|
180
|
+
if (content.includes(qlId)) return
|
|
181
|
+
const prefix = content && !content.endsWith('\n') ? '\n' : ''
|
|
182
|
+
appendFileSync(p, `${prefix}- [ ] ${qlId} ${desc}\n`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 勾选该 qlId 对应 task:- [ ] → - [x]
|
|
186
|
+
async function checkTaskCheckbox(specBase, change, qlId) {
|
|
187
|
+
const p = tasksPath(specBase, change)
|
|
188
|
+
if (!existsSync(p)) return
|
|
189
|
+
let content = readFileSync(p, 'utf8')
|
|
190
|
+
const re = new RegExp(`- \\[ \\] (${escapeRe(qlId)})`)
|
|
191
|
+
content = content.replace(re, '- [x] $1')
|
|
192
|
+
await writeAtomic(p, content)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 就地翻某 qlId 条目:状态进行中→已完成,追加结果行
|
|
196
|
+
function flipEntryInContent(content, qlId, result) {
|
|
197
|
+
const lines = content.split('\n')
|
|
198
|
+
const startIdx = lines.findIndex(l => l.startsWith(`## ${qlId} |`))
|
|
199
|
+
if (startIdx === -1) return null
|
|
200
|
+
let endIdx = lines.length
|
|
201
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
202
|
+
if (lines[i].startsWith('## ')) { endIdx = i; break }
|
|
203
|
+
}
|
|
204
|
+
let hasResult = false
|
|
205
|
+
let flipped = false
|
|
206
|
+
for (let i = startIdx + 1; i < endIdx; i++) {
|
|
207
|
+
if (lines[i] === '状态:进行中') { lines[i] = '状态:已完成'; flipped = true }
|
|
208
|
+
if (lines[i].startsWith('结果:')) hasResult = true
|
|
209
|
+
}
|
|
210
|
+
if (!flipped && !lines.slice(startIdx, endIdx).includes('状态:已完成')) {
|
|
211
|
+
// 无状态行(异常),插一条已完成
|
|
212
|
+
lines.splice(startIdx + 1, 0, '状态:已完成')
|
|
213
|
+
endIdx += 1
|
|
214
|
+
}
|
|
215
|
+
if (result && !hasResult) {
|
|
216
|
+
lines.splice(endIdx, 0, `结果:${result}`)
|
|
217
|
+
}
|
|
218
|
+
return lines.join('\n')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── 对外 API ──
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 分配 ql-ID 并写「进行中」条目 + 关联 tasks.md。持锁、当天唯一。
|
|
225
|
+
* @returns {Promise<{qlId: string}>}
|
|
226
|
+
*/
|
|
227
|
+
export async function allocateQuicklogEntry(specBase, gitUser, { description, linkedChanges = [], allowedFiles = [] } = {}) {
|
|
228
|
+
const quicklogDir = join(specBase, 'quicklog')
|
|
229
|
+
mkdirSync(quicklogDir, { recursive: true })
|
|
230
|
+
const user = gitUser || 'unknown'
|
|
231
|
+
const userFile = join(quicklogDir, `QUICKLOG-${user}.md`)
|
|
232
|
+
const lockPath = join(quicklogDir, `.QUICKLOG-${user}.md.lock`)
|
|
233
|
+
const desc = sanitizeDesc(description)
|
|
234
|
+
const linked = Array.isArray(linkedChanges) ? linkedChanges : []
|
|
235
|
+
const files = Array.isArray(allowedFiles) ? allowedFiles : []
|
|
236
|
+
const today = todayStamp()
|
|
237
|
+
|
|
238
|
+
return await withFileLock(lockPath, async () => {
|
|
239
|
+
const { maxSeq, usedSuffix } = scanExisting(quicklogDir, today)
|
|
240
|
+
const nextSeq = maxSeq + 1
|
|
241
|
+
// XXXX 4 位 hex 随机后缀(消歧;NNN 已在锁内顺序分配保证唯一,此处仅 belt-and-suspenders)
|
|
242
|
+
let suffix
|
|
243
|
+
let guard = 0
|
|
244
|
+
do {
|
|
245
|
+
suffix = randomBytes(2).toString('hex')
|
|
246
|
+
guard++
|
|
247
|
+
} while (usedSuffix.has(suffix) && guard < 100)
|
|
248
|
+
|
|
249
|
+
await rotateIfNeeded(userFile, user)
|
|
250
|
+
|
|
251
|
+
const qlId = `ql-${today}-${String(nextSeq).padStart(3, '0')}-${suffix}`
|
|
252
|
+
const entry = [
|
|
253
|
+
'',
|
|
254
|
+
`## ${qlId} | ${nowDatetime()} | ${desc}`,
|
|
255
|
+
'状态:进行中',
|
|
256
|
+
`关联变更:${linked.length > 0 ? linked.join(', ') : '(无)'}`,
|
|
257
|
+
`文件:${files.length > 0 ? files.join(', ') : '(见实际改动)'}`,
|
|
258
|
+
'',
|
|
259
|
+
].join('\n')
|
|
260
|
+
appendFileSync(userFile, entry)
|
|
261
|
+
|
|
262
|
+
for (const c of linked) appendTaskCheckbox(specBase, c, qlId, desc)
|
|
263
|
+
|
|
264
|
+
return { qlId }
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 翻某 qlId 条目为「已完成」+ 追加结果 + 勾选关联 tasks.md。持锁。
|
|
270
|
+
*/
|
|
271
|
+
export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultText = '', linkedChanges = [] } = {}) {
|
|
272
|
+
const quicklogDir = join(specBase, 'quicklog')
|
|
273
|
+
const lockPath = join(quicklogDir, `.QUICKLOG-${(gitUser || 'unknown')}.md.lock`)
|
|
274
|
+
const result = sanitizeResult(resultText)
|
|
275
|
+
const linked = Array.isArray(linkedChanges) ? linkedChanges : []
|
|
276
|
+
|
|
277
|
+
await withFileLock(lockPath, async () => {
|
|
278
|
+
// 条目可能在主文件或轮转归档中
|
|
279
|
+
for (const f of listQuicklogFiles(quicklogDir)) {
|
|
280
|
+
const filePath = join(quicklogDir, f)
|
|
281
|
+
let content = ''
|
|
282
|
+
try { content = readFileSync(filePath, 'utf8') } catch { continue }
|
|
283
|
+
const updated = flipEntryInContent(content, qlId, result)
|
|
284
|
+
if (updated !== null) {
|
|
285
|
+
await writeAtomic(filePath, updated) // 命中处原子落盘(只改含目标条目的那一个文件)
|
|
286
|
+
break
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const c of linked) await checkTaskCheckbox(specBase, c, qlId)
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 查某 qlId 条目是否存在(只读,跨所有 QUICKLOG-*.md)。
|
|
295
|
+
*/
|
|
296
|
+
export function findQuicklogEntry(specBase, gitUser, qlId) {
|
|
297
|
+
const quicklogDir = join(specBase, 'quicklog')
|
|
298
|
+
if (!existsSync(quicklogDir)) return false
|
|
299
|
+
const header = `## ${qlId} |`
|
|
300
|
+
for (const f of listQuicklogFiles(quicklogDir)) {
|
|
301
|
+
let content = ''
|
|
302
|
+
try { content = readFileSync(join(quicklogDir, f), 'utf8') } catch { continue }
|
|
303
|
+
if (content.includes(header)) return true
|
|
304
|
+
}
|
|
305
|
+
return false
|
|
306
|
+
}
|
package/src/run.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createRequire } from 'module'
|
|
|
11
11
|
const require = createRequire(import.meta.url)
|
|
12
12
|
import { ProgressManager } from './progress.js'
|
|
13
13
|
import { SCAN_STATUS, POINTER_STATUS, isPointerCorrupted } from './constants.js'
|
|
14
|
+
import { allocateQuicklogEntry, completeQuicklogEntry, findQuicklogEntry } from './quicklog.js'
|
|
14
15
|
import { checkbox } from '@inquirer/prompts'
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -721,6 +722,19 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
721
722
|
if (changeName && promptText.includes('<quick-session-id>')) {
|
|
722
723
|
promptText = promptText.replace(/<quick-session-id>/g, changeName)
|
|
723
724
|
}
|
|
725
|
+
// 替换 <quicklog-id> 占位符(quick 阶段:从 session guard.json 读 CLI 分配的 ql-ID,
|
|
726
|
+
// 供 agent 在模块文档变更索引等处引用)
|
|
727
|
+
if (promptText.includes('<quicklog-id>')) {
|
|
728
|
+
const specBaseQl = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
729
|
+
let qlIdVal = ''
|
|
730
|
+
try {
|
|
731
|
+
const sessionGuardFile = join(specBaseQl, '.runtime', 'quick-sessions', changeName, 'guard.json')
|
|
732
|
+
if (existsSync(sessionGuardFile)) {
|
|
733
|
+
qlIdVal = JSON.parse(readFileSync(sessionGuardFile, 'utf8')).quicklogId || ''
|
|
734
|
+
}
|
|
735
|
+
} catch {}
|
|
736
|
+
promptText = promptText.replace(/<quicklog-id>/g, qlIdVal || '(未分配)')
|
|
737
|
+
}
|
|
724
738
|
// 替换 <linked-changes> 占位符(quick 阶段:从 .runtime/quick-sessions/<sessionId>/guard.json 读关联变更)
|
|
725
739
|
if (promptText.includes('<linked-changes>')) {
|
|
726
740
|
const specBaseLc = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
@@ -1740,7 +1754,7 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1740
1754
|
}
|
|
1741
1755
|
|
|
1742
1756
|
// 默认:输出当前步骤
|
|
1743
|
-
return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts, { quickFiles, isAllowNew, isForceBaseline, isForceRescan, linkedChanges })
|
|
1757
|
+
return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts, { quickFiles, isAllowNew, isForceBaseline, isForceRescan, linkedChanges, taskDescription: inputText })
|
|
1744
1758
|
}
|
|
1745
1759
|
|
|
1746
1760
|
/**
|
|
@@ -2000,45 +2014,67 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
2000
2014
|
console.log(`🔄 ${stageName} 阶段已自动重置,重新开始。\n`)
|
|
2001
2015
|
}
|
|
2002
2016
|
|
|
2003
|
-
// quick 阶段:记录 baselineFiles
|
|
2004
|
-
|
|
2017
|
+
// quick 阶段:记录 baselineFiles + 分配 ql-ID(CLI 接管 QUICKLOG 写入)
|
|
2018
|
+
// 幂等判据是 session guard.json 文件(跨进程可靠),不是 progress.quickGuard
|
|
2019
|
+
// (D-003@v1:顶层 quickGuard 不跨进程持久化;agent 在 step 间用 `run quick` 取下一步
|
|
2020
|
+
// prompt 时每个新进程都进此块,按文件判幂等才不会重复分配 ql-ID / 重复写条目)。
|
|
2021
|
+
if (stageName === 'quick') {
|
|
2022
|
+
const sessionGuardDir = join(specBase, '.runtime', 'quick-sessions', changeName)
|
|
2023
|
+
const guardFile = join(sessionGuardDir, 'guard.json')
|
|
2024
|
+
let existingGuard = null
|
|
2005
2025
|
try {
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
//
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2026
|
+
if (existsSync(guardFile)) existingGuard = JSON.parse(readFileSync(guardFile, 'utf8'))
|
|
2027
|
+
} catch {}
|
|
2028
|
+
if (existingGuard) {
|
|
2029
|
+
// 跨进程重入:复用已分配的 ql-ID,跳过 baseline 重捕与分配(幂等)
|
|
2030
|
+
progress.quickGuard = existingGuard
|
|
2031
|
+
} else {
|
|
2032
|
+
try {
|
|
2033
|
+
const { execSync } = await import('child_process')
|
|
2034
|
+
const gitStatus = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 10000 })
|
|
2035
|
+
// 记录全部预存脏文件(含 untracked + .sillyspec/ 路径)。quick 会话期间自身写入的元数据
|
|
2036
|
+
// (quicklog/.runtime/modules/_module-map 等)由 auditQuickCompletion 的 isQuickMetadata 精确豁免,
|
|
2037
|
+
// 不需要这里粗放过滤 .sillyspec/——旧过滤致预存 untracked .sillyspec/changes/ 不进 baseline,
|
|
2038
|
+
// 却在 audit 被当「危险(.sillyspec/)+新增」误判永久 blocked(ql-20260713-002-7628 修复)。
|
|
2039
|
+
const baselineFiles = gitStatus
|
|
2040
|
+
.trim().split('\n').filter(Boolean)
|
|
2041
|
+
.map(line => line.slice(3).trim())
|
|
2042
|
+
const allowedFiles = quickOpts?.quickFiles || []
|
|
2043
|
+
const allowNew = quickOpts?.isAllowNew || false
|
|
2044
|
+
const forceBaseline = quickOpts?.isForceBaseline || false
|
|
2045
|
+
const linkedChanges = Array.isArray(quickOpts?.linkedChanges) ? quickOpts.linkedChanges : []
|
|
2046
|
+
// CLI 接管:分配 ql-ID + 写 QUICKLOG「进行中」条目 + 关联 tasks.md(持锁、当天唯一)
|
|
2047
|
+
const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
|
|
2048
|
+
const { qlId } = await allocateQuicklogEntry(specBase, gitUser, {
|
|
2049
|
+
description: quickOpts?.taskDescription || '',
|
|
2050
|
+
linkedChanges,
|
|
2051
|
+
allowedFiles,
|
|
2052
|
+
})
|
|
2053
|
+
progress.quickGuard = {
|
|
2054
|
+
sessionId: changeName,
|
|
2055
|
+
name_zh: '快速任务守卫',
|
|
2056
|
+
baselineCommit: safeGit(cwd, ['rev-parse', 'HEAD']).value,
|
|
2057
|
+
baselineFiles,
|
|
2058
|
+
allowedFiles,
|
|
2059
|
+
allowNew,
|
|
2060
|
+
forceBaseline,
|
|
2061
|
+
linkedChanges,
|
|
2062
|
+
quicklogId: qlId,
|
|
2063
|
+
startedAt: new Date().toISOString(),
|
|
2064
|
+
}
|
|
2065
|
+
// 写入 .runtime/quick-sessions/<sessionId>/guard.json 供 worktree-guard hook 读取
|
|
2066
|
+
// (D-002:按 session 存,多会话各自 guard 不互覆盖。runStage 作用域内 sessionId == changeName == quick-<uuid8>,见 §4.4/4.5)
|
|
2067
|
+
mkdirSync(sessionGuardDir, { recursive: true })
|
|
2068
|
+
writeFileSync(guardFile, JSON.stringify(progress.quickGuard, null, 2))
|
|
2069
|
+
const parts = [`${baselineFiles.length} 个已有脏文件`]
|
|
2070
|
+
if (allowedFiles.length > 0) parts.push(`${allowedFiles.length} 个 allowedFiles`)
|
|
2071
|
+
if (allowNew) parts.push('允许新增文件')
|
|
2072
|
+
console.log(`🛡️ quick 变更边界已记录: ${parts.join(', ')}`)
|
|
2073
|
+
console.log(`📝 QUICKLOG 条目已创建: ${qlId}`)
|
|
2074
|
+
await pm._write(cwd, progress, changeName)
|
|
2075
|
+
} catch (e) {
|
|
2076
|
+
console.warn(`⚠️ baseline 记录失败: ${e.message}`)
|
|
2028
2077
|
}
|
|
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
2078
|
}
|
|
2043
2079
|
}
|
|
2044
2080
|
|
|
@@ -3011,61 +3047,96 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
3011
3047
|
if (steps[wsIdx].waitReason) console.log(` 原因:${steps[wsIdx].waitReason}`)
|
|
3012
3048
|
return { stageCompleted: false, currentIdx, nextPendingIdx: -1 }
|
|
3013
3049
|
}
|
|
3014
|
-
// quick
|
|
3050
|
+
// quick 收尾:强校验 QUICKLOG 条目 + 翻状态 + 勾 tasks.md(CLI 接管)
|
|
3015
3051
|
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)。
|
|
3052
|
+
// §4.6 从 session guard.json 读 guard(不依赖 progress.quickGuard)。
|
|
3025
3053
|
// D-003@v1:progress._write 不持久化顶层 quickGuard,跨进程 --done 时读出的 progress 无 quickGuard,
|
|
3026
3054
|
// 若仍用 if (progress.quickGuard) 驱动收尾会整体跳过,导致 .runtime/quick-sessions/<sessionId>/ 残留僵尸。
|
|
3027
3055
|
// 改为从文件读 guard:优先 session 目录 guard.json,回退旧单文件 quick-guard.json(task-03 前兼容)。
|
|
3028
3056
|
// 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
|
-
|
|
3057
|
+
const runtimeBase = platformOpts.runtimeRoot || join(specBase, '.runtime')
|
|
3058
|
+
const sessionGuardFile = join(runtimeBase, 'quick-sessions', changeName, 'guard.json')
|
|
3059
|
+
const legacyGuardFile = join(specBase, '.runtime', 'quick-guard.json')
|
|
3060
|
+
let guard = null
|
|
3061
|
+
try {
|
|
3062
|
+
guard = existsSync(sessionGuardFile)
|
|
3063
|
+
? JSON.parse(readFileSync(sessionGuardFile, 'utf8'))
|
|
3064
|
+
: (existsSync(legacyGuardFile) ? JSON.parse(readFileSync(legacyGuardFile, 'utf8')) : null)
|
|
3065
|
+
} catch {}
|
|
3066
|
+
|
|
3067
|
+
// 强校验 / 收尾:本会话必须有一条真实 QUICKLOG 条目(治「报 SAFE 但漏写」bug)。
|
|
3068
|
+
// guard 缺失(brownfield:新代码前启动的会话)不阻断——兜底补写一条记录,保住「完成必有记录」不变量。
|
|
3069
|
+
const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
|
|
3070
|
+
let qlId = guard?.quicklogId || null
|
|
3071
|
+
const linkedChanges = Array.isArray(guard?.linkedChanges) ? guard.linkedChanges : []
|
|
3072
|
+
|
|
3073
|
+
// 审计:仅在有 guard 时跑(brownfield 无 guard 跳过,兼容 D-003 brownfield 行为)。
|
|
3074
|
+
if (guard) {
|
|
3075
|
+
// --done 的 --force-baseline/--allow-new 并入 guard(与 step1 持久化值取或)。
|
|
3076
|
+
// 修复 ql-20260713-002-7628:旧代码解析了这两个 flag 但只传 {isConfirm} 给审计,
|
|
3077
|
+
// 致 --done --force-baseline 静默无效、用户被误导「重跑 --confirm」也无法解锁。
|
|
3078
|
+
const mergedGuard = {
|
|
3079
|
+
...guard,
|
|
3080
|
+
forceBaseline: guard.forceBaseline || isForceBaseline,
|
|
3081
|
+
allowNew: guard.allowNew || isAllowNew,
|
|
3082
|
+
}
|
|
3083
|
+
const review = await auditQuickCompletion(cwd, mergedGuard, { isConfirm: confirm })
|
|
3084
|
+
printQuickAuditReview(review)
|
|
3085
|
+
if (review.status === 'blocked') {
|
|
3086
|
+
steps[currentIdx].status = 'pending'
|
|
3087
|
+
steps[currentIdx].completedAt = null
|
|
3088
|
+
if (outputText) steps[currentIdx].output = null
|
|
3089
|
+
process.exit(1)
|
|
3057
3090
|
}
|
|
3058
|
-
|
|
3059
|
-
|
|
3091
|
+
progress.lastQuickReview = review
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
if (!qlId) {
|
|
3095
|
+
// 无 ql-ID(guard 缺失或 brownfield 无 quicklogId):补分配后立即完成,不阻断。
|
|
3060
3096
|
try {
|
|
3061
|
-
const
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3097
|
+
const alloc = await allocateQuicklogEntry(specBase, gitUser, {
|
|
3098
|
+
description: guard?.taskDescription || '(补分配)',
|
|
3099
|
+
linkedChanges,
|
|
3100
|
+
allowedFiles: Array.isArray(guard?.allowedFiles) ? guard.allowedFiles : [],
|
|
3101
|
+
})
|
|
3102
|
+
qlId = alloc.qlId
|
|
3103
|
+
console.log(`📝 QUICKLOG 兜底补写: ${qlId}(guard 缺失/brownfield 会话)`)
|
|
3104
|
+
} catch (e) {
|
|
3105
|
+
console.error(`\n❌ QUICKLOG 补分配失败: ${e.message}`)
|
|
3106
|
+
steps[currentIdx].status = 'pending'
|
|
3107
|
+
steps[currentIdx].completedAt = null
|
|
3108
|
+
if (outputText) steps[currentIdx].output = null
|
|
3109
|
+
process.exit(1)
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
if (!findQuicklogEntry(specBase, gitUser, qlId)) {
|
|
3113
|
+
console.error(`\n❌ quick 阶段完成校验失败:QUICKLOG 条目 ${qlId} 不存在。`)
|
|
3114
|
+
console.error(` 会话期间记录被删除或从未写入。请检查 .sillyspec/quicklog/ 后重跑 --done。`)
|
|
3115
|
+
steps[currentIdx].status = 'pending'
|
|
3116
|
+
steps[currentIdx].completedAt = null
|
|
3117
|
+
if (outputText) steps[currentIdx].output = null
|
|
3118
|
+
process.exit(1)
|
|
3068
3119
|
}
|
|
3120
|
+
// 翻状态进行中→已完成 + 追加结果 + 勾选关联 tasks.md
|
|
3121
|
+
try {
|
|
3122
|
+
await completeQuicklogEntry(specBase, gitUser, qlId, {
|
|
3123
|
+
resultText: outputText ? outputText.slice(0, 500) : '',
|
|
3124
|
+
linkedChanges,
|
|
3125
|
+
})
|
|
3126
|
+
console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
|
|
3127
|
+
} catch (e) {
|
|
3128
|
+
console.warn(`⚠️ QUICKLOG 完成态写入失败: ${e.message}`)
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
// 清理 session 目录(rmSync/unlinkSync 容忍不存在)。
|
|
3132
|
+
try {
|
|
3133
|
+
const { unlinkSync } = await import('fs')
|
|
3134
|
+
if (changeName) {
|
|
3135
|
+
const sessionDir = join(runtimeBase, 'quick-sessions', changeName)
|
|
3136
|
+
rmSync(sessionDir, { recursive: true, force: true })
|
|
3137
|
+
}
|
|
3138
|
+
if (existsSync(legacyGuardFile)) unlinkSync(legacyGuardFile)
|
|
3139
|
+
} catch {}
|
|
3069
3140
|
}
|
|
3070
3141
|
|
|
3071
3142
|
stageData.status = 'completed'
|
package/src/stages/quick.js
CHANGED
|
@@ -28,38 +28,15 @@ export const definition = {
|
|
|
28
28
|
9. 根据任务描述初步判断可能涉及的模块
|
|
29
29
|
10. 读取匹配到的 \`{SPEC_ROOT}/docs/<project>/modules/<module>.md\`
|
|
30
30
|
|
|
31
|
-
###
|
|
32
|
-
|
|
31
|
+
### QUICKLOG 记录(CLI 已接管,无需你手动创建)
|
|
32
|
+
本 quick 会话的 ql-ID 由 CLI 在启动时分配,已注入为 \`<quicklog-id>\`。CLI 同时已自动:
|
|
33
|
+
- 在 \`{SPEC_ROOT}/quicklog/QUICKLOG-<git-user>.md\` 写入「进行中」条目(含关联变更与预估文件)
|
|
34
|
+
- 对每个关联变更 \`<c>\`:在 \`{SPEC_ROOT}/changes/<c>/tasks.md\` 追加未勾选 task \`- [ ] <quicklog-id> <任务描述>\`
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
1. 使用预注入的 git 用户名:\`<git-user>\`
|
|
36
|
-
2. 创建/追加 \`{SPEC_ROOT}/quicklog/QUICKLOG-\`<git-user>\`.md\`,写入:
|
|
37
|
-
\`\`\`
|
|
38
|
-
## ql-<YYYYMMDD>-<NNN>-<XXXX> | <now-datetime> | <一句话任务描述>
|
|
39
|
-
状态:进行中
|
|
40
|
-
关联变更:<linked-changes>
|
|
41
|
-
文件:<预估要改的文件>
|
|
42
|
-
\`\`\`
|
|
43
|
-
- ID 格式:\`ql-YYYYMMDD-NNN-XXXX\`
|
|
44
|
-
- \`XXXX\` 是 4 位随机十六进制字符(如 a3f2、b7c1、00ef),**不是描述词缩写**
|
|
45
|
-
- 追加前扫描文件中已有的 \`ql-<当天日期>-\` 前缀的最大序号,+1 作为新序号
|
|
46
|
-
- 每天从 001 开始,跨日重新计数
|
|
47
|
-
- 此 ID 可被 design.md / plan.md / archive / module 变更索引引用
|
|
48
|
-
|
|
49
|
-
**B. 若关联变更(\`<linked-changes>\` 不为「(无)」)**
|
|
50
|
-
3. 解析 \`<linked-changes>\`(逗号分隔的变更名列表,可能有多个 = 一次 quick 同时归属多个变更)
|
|
51
|
-
4. 对**每个**变更 \`<c>\`:在 \`{SPEC_ROOT}/changes/<c>/tasks.md\` 追加一条未勾选 task:
|
|
52
|
-
\`- [ ] <ql-ID> <一句话任务描述>\`(复用步骤 A 生成的同一个 ql-ID)
|
|
53
|
-
- 若 \`changes/<c>/\` 目录或 tasks.md 不存在,先 \`mkdir -p {SPEC_ROOT}/changes/<c>\` 再创建 tasks.md
|
|
54
|
-
- 同一个 ql-ID 出现在 QUICKLOG 和所有关联变更的 tasks.md 中,便于交叉引用
|
|
55
|
-
- \`<linked-changes>\` 为「(无)」时跳过本步,仅保留 QUICKLOG
|
|
56
|
-
|
|
57
|
-
这样 Gate 检测到 \`{SPEC_ROOT}/\` 下有变更,就不会拦截后续的代码修改。
|
|
36
|
+
**你不要创建或修改任何 QUICKLOG / tasks.md 记录**——完成本任务时 CLI 会自动将条目翻为「已完成」并勾选 task(含轮转)。\`<quicklog-id>\` 可用于 design.md / plan.md / archive / 模块变更索引引用。
|
|
58
37
|
|
|
59
38
|
### 输出
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
⚠️ **先创建 quicklog,再输出任务理解。** 如果 quicklog 未创建,CLI post-check 会报 warning。`,
|
|
39
|
+
任务理解 + 上下文摘要`,
|
|
63
40
|
outputHint: '任务理解',
|
|
64
41
|
optional: false
|
|
65
42
|
},
|
|
@@ -97,23 +74,20 @@ quicklog 已创建(必须放在输出的第一行确认)+ 任务理解 + 上
|
|
|
97
74
|
2. 使用 \`git add -- <file...>\` 暂存本次 quick 实际修改的文件(不要 commit,由用户通过统一提交工具处理)
|
|
98
75
|
- 禁止使用 \`git add -A\`
|
|
99
76
|
- 不要暂存 quick 开始前就已存在的无关改动
|
|
100
|
-
3.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
4. QUICKLOG 轮转:超过 500 行则重命名为 \`QUICKLOG-<USER>-YYYY-MM-DD.md\`(日期取最后一条记录的日期)。新文件从空开始,ql-ID 需扫描同目录所有 QUICKLOG 文件中当天最大序号 +1
|
|
104
|
-
5. 如果发现项目特有的坑,追加到 \`{SPEC_ROOT}/knowledge/uncategorized.md\`
|
|
105
|
-
6. 任务比预期复杂 → 建议用完整流程
|
|
77
|
+
3. QUICKLOG / tasks.md 记录由 CLI 在本 step 完成时自动收尾(翻「已完成」+ 勾选 task + 轮转),你无需手动更新
|
|
78
|
+
4. 如果发现项目特有的坑,追加到 \`{SPEC_ROOT}/knowledge/uncategorized.md\`
|
|
79
|
+
5. 任务比预期复杂 → 建议用完整流程
|
|
106
80
|
|
|
107
81
|
### 模块文档同步
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
82
|
+
6. 读取 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
|
|
83
|
+
7. 对比本次修改的文件(\`git diff --name-only HEAD\`)与模块映射
|
|
84
|
+
8. 如果命中模块 → 直接同步模块文档:
|
|
111
85
|
- 读取对应的 \`{SPEC_ROOT}/docs/<project>/modules/<module>.md\`(如不存在则新建)
|
|
112
|
-
-
|
|
113
|
-
- 变更索引格式:\`-
|
|
86
|
+
- 根据本次改动内容更新模块文档(正文描述当前状态,底部变更索引追加本步骤预注入的 ql-ID)
|
|
87
|
+
- 变更索引格式:\`- <quicklog-id> | <一句话描述>\`
|
|
114
88
|
- 写入模块文档
|
|
115
89
|
- 使用 \`git add -- <module-doc>\` 暂存更新的模块文件
|
|
116
|
-
|
|
90
|
+
9. 未命中任何模块 → 跳过,不做额外操作
|
|
117
91
|
|
|
118
92
|
### 输出
|
|
119
93
|
暂存确认 + 记录路径 + 模块文档同步结果(如有)`,
|