sillyspec 3.26.11 → 3.26.13
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-archive/SKILL.md +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-knowledge/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +5 -5
- package/.claude/skills/sillyspec-quick/SKILL.md +5 -1
- package/README.md +7 -5
- package/SKILL.md +5 -4
- package/package.json +1 -1
- package/packages/dashboard/server/executor.js +10 -1
- package/packages/dashboard/server/index.js +108 -46
- package/src/config-schema.js +3 -0
- package/src/contract-matrix.js +22 -13
- package/src/db.js +24 -4
- package/src/dispatch/probe.js +99 -63
- package/src/docs-check.js +58 -13
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +208 -21
- package/src/fs-atomic.js +3 -1
- package/src/git-helper.js +7 -4
- package/src/hooks/worktree-guard.js +8 -2
- package/src/index.js +161 -12
- package/src/init.js +3 -2
- package/src/local-detect.js +9 -1
- package/src/modules.js +7 -5
- package/src/progress/stage-machine.js +589 -578
- package/src/progress/step-store.js +1 -1
- package/src/progress.js +184 -30
- package/src/quicklog.js +47 -16
- package/src/run/command.js +31 -8
- package/src/run/complete-handlers.js +21 -14
- package/src/run/complete.js +1181 -1121
- package/src/run/gates.js +148 -25
- package/src/run/prompt.js +9 -5
- package/src/run/shared.js +138 -58
- package/src/run/stage.js +41 -1
- package/src/setup.js +8 -10
- package/src/sillyhub-mcp/client.js +74 -6
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +43 -8
- package/src/stage-contract.js +12 -38
- package/src/stages/archive.js +4 -3
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +2 -1
- package/src/stages/doctor.js +28 -13
- package/src/stages/execute.js +164 -153
- package/src/stages/plan-postcheck.js +49 -28
- package/src/stages/plan.js +57 -40
- package/src/stages/quick.js +3 -1
- package/src/stages/verify.js +1 -1
- package/src/sync.js +323 -221
- package/src/task-review.js +246 -61
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +48 -35
- package/src/worktree-apply.js +58 -34
- package/src/worktree-deps.js +120 -20
- package/src/worktree.js +55 -9
- package/.npmrc-publish +0 -2
package/src/run/gates.js
CHANGED
|
@@ -26,15 +26,16 @@ import { runValidators } from '../stage-contract.js'
|
|
|
26
26
|
import { handleScanStageCompleted, handleExecuteWorktreeCleanup } from './complete-handlers.js'
|
|
27
27
|
import { detectConcurrentChanges, formatConcurrentWarning } from './concurrent-detect.js'
|
|
28
28
|
import { stageRegistry } from '../stages/index.js'
|
|
29
|
+
import { normalizeTaskId } from '../taskcard.js'
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
-
* 只认 checkbox 行 `- [ ] task-XX:` / `- [x] task-XX:`(与 execute.js
|
|
33
|
-
*
|
|
32
|
+
* 从任务注册表(tasks.md)提取全部 task id(task-XX)——符号影响面覆盖度校验用。
|
|
33
|
+
* 只认 checkbox 行 `- [ ] task-XX:` / `- [x] task-XX:`(与 execute.js parseTaskRegistry 同口径)。
|
|
34
|
+
* 2026-08-20-task-truth-unify:源从 plan.md 迁 tasks.md(任务唯一真相);ql-xxx 行不收。
|
|
34
35
|
*/
|
|
35
|
-
function
|
|
36
|
-
if (!
|
|
37
|
-
return [...String(
|
|
36
|
+
function extractTaskIdsFromRegistry(tasksContent) {
|
|
37
|
+
if (!tasksContent) return []
|
|
38
|
+
return [...String(tasksContent).matchAll(/^[-*]\s*\[[ xX]\]\s*task-(\d+)\b/gim)]
|
|
38
39
|
.map(m => `task-${m[1].padStart(2, '0')}`)
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -56,8 +57,10 @@ export function validateSymbolImpactReport({ reportContent, planContent }) {
|
|
|
56
57
|
if (!reportContent || !String(reportContent).trim()) {
|
|
57
58
|
return { ok: false, errors: ['symbol-impact.md 不存在或内容为空——「加载上下文」步的符号影响面报告必须落盘(逐 task 一行结论,「无签名级变更」也要显式写)'] }
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
// 2026-08-20-task-truth-unify:task 清单源迁 tasks.md(参数名 planContent 保持兼容既有调用,
|
|
61
|
+
// 语义=任务注册表内容;调用方 enforceSymbolImpactGate 已改读 tasks.md)
|
|
62
|
+
const taskIds = extractTaskIdsFromRegistry(planContent)
|
|
63
|
+
if (taskIds.length === 0) return { ok: true, errors } // 注册表无 task 可列(默认兜底形态)→ 无可校验对象
|
|
61
64
|
const missing = taskIds.filter(id => !String(reportContent).includes(id))
|
|
62
65
|
for (const id of missing) {
|
|
63
66
|
errors.push(`${id} 未在 symbol-impact.md 中出现——每个 task 都要有一行结论(含「无签名级变更」的显式声明)`)
|
|
@@ -72,14 +75,14 @@ export function validateSymbolImpactReport({ reportContent, planContent }) {
|
|
|
72
75
|
export async function enforceSymbolImpactGate(stageName, changeName, currentStepName, specBase) {
|
|
73
76
|
if (stageName !== 'execute') return
|
|
74
77
|
if (!currentStepName || !currentStepName.includes('加载上下文')) return
|
|
75
|
-
const
|
|
78
|
+
const tasksPath = join(specBase, 'changes', changeName || '', 'tasks.md')
|
|
76
79
|
const reportPath = join(specBase, 'changes', changeName || '', 'symbol-impact.md')
|
|
77
|
-
if (!existsSync(
|
|
78
|
-
let
|
|
79
|
-
try {
|
|
80
|
+
if (!existsSync(tasksPath)) return // 注册表缺失场景已有 plan 阶段 gate 把关,此处不重复拦
|
|
81
|
+
let tasksContent = ''
|
|
82
|
+
try { tasksContent = readFileSync(tasksPath, 'utf8') } catch { return }
|
|
80
83
|
let reportContent = null
|
|
81
84
|
try { reportContent = readFileSync(reportPath, 'utf8') } catch { /* 缺失由 validate 报 */ }
|
|
82
|
-
const result = validateSymbolImpactReport({ reportContent, planContent })
|
|
85
|
+
const result = validateSymbolImpactReport({ reportContent, planContent: tasksContent })
|
|
83
86
|
if (result.ok) return
|
|
84
87
|
console.error('❌ ── 符号影响面报告校验阻断(本次 --done 未完成,进度未推进)──')
|
|
85
88
|
console.error(` 「加载上下文」步的符号影响面检查报告未落盘或不完整:`)
|
|
@@ -105,10 +108,13 @@ function isCurrentWaveAllNoDepsVerify(stepName, changeDir) {
|
|
|
105
108
|
const waveRe = new RegExp(`^##\\s*Wave\\s+${waveN}\\b[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s|\\n#\\s|$)`, 'm')
|
|
106
109
|
const waveMatch = plan.match(waveRe)
|
|
107
110
|
if (!waveMatch) return false
|
|
108
|
-
|
|
111
|
+
// 2026-08-20-task-truth-unify:Wave 段任务为纯 ID 引用行("- task-XX"),不再有 checkbox
|
|
112
|
+
const taskIds = [...waveMatch[1].matchAll(/^[-*]\s+(task-\d+)\s*$/gim)].map(x => x[1])
|
|
109
113
|
if (taskIds.length === 0) return false
|
|
110
114
|
for (const id of taskIds) {
|
|
111
|
-
|
|
115
|
+
// 体检 BUG-19:plan 常态写补零 task-01,但 AI 写出 task-3 时卡片实际是 task-03.md——
|
|
116
|
+
// 未归一则 existsSync 恒 false,wave 级 no_deps_verify 豁免静默失效(保守方向)
|
|
117
|
+
const cardPath = join(changeDir, 'tasks', `${normalizeTaskId(id)}.md`)
|
|
112
118
|
if (!existsSync(cardPath)) return false // 卡片缺失 → 保守不跳
|
|
113
119
|
const card = readFileSync(cardPath, 'utf8')
|
|
114
120
|
const fm = card.match(/^---\n([\s\S]*?)\n---/)
|
|
@@ -118,6 +124,99 @@ function isCurrentWaveAllNoDepsVerify(stepName, changeDir) {
|
|
|
118
124
|
return true
|
|
119
125
|
}
|
|
120
126
|
|
|
127
|
+
/**
|
|
128
|
+
* doctor --align-execute-progress --confirm 的前置 review 门(坑 doctor-align-bypass-review-gate,
|
|
129
|
+
* 2026-08-20 实证):alignExecuteToPlan 直写 completed「绕过 completeStep 推导」,此前 execute 的
|
|
130
|
+
* Stage Review Gate 与 Task Review Gate 被整体跳过——而 worktree 清理后的恢复场景恰是最需要
|
|
131
|
+
* review 审计的时刻(实证靠 15 份 task review + verify 全程补足覆盖才没出事)。
|
|
132
|
+
*
|
|
133
|
+
* 本函数供 CLI 层(index.js doctor align 分支)在 confirm 落盘前调用,跑与正常完成路径同源的
|
|
134
|
+
* 只读校验(不自动生成 marker/runId——恢复读场景缺失就是缺失,如实报告):
|
|
135
|
+
* 1. Stage Review:tier=independent 时须存在有效 execute stage review.json(verdict 非 fail);
|
|
136
|
+
* tier=self 放行(与正常 gate 分级一致)。
|
|
137
|
+
* 2. Task Review:所有 task 的 review.json 齐备且 verdict 非 fail(runId 解析同 Task Review Gate:
|
|
138
|
+
* marker → 含 tasks/ 目录扫描;不 generate)。
|
|
139
|
+
*
|
|
140
|
+
* @param {{ cwd: string, changeName: string, specBase: string, platformOpts?: object }} opts
|
|
141
|
+
* @returns {Promise<boolean>} true=被门阻断(CLI 应放弃 align 并 exit 1);false=校验通过可继续
|
|
142
|
+
*/
|
|
143
|
+
export async function enforceAlignExecuteReviewGate({ cwd, changeName, specBase, platformOpts = {} }) {
|
|
144
|
+
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
145
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, effectiveSpecBase)
|
|
146
|
+
const reviewChangeDir = resolveChangeDir(cwd, { currentChange: changeName }, platformOpts?.specRoot || null)
|
|
147
|
+
const blocked = (msgs) => {
|
|
148
|
+
console.error('\n❌ doctor --align-execute-progress 前置 review 校验未过——execute 完成审计不能绕过:')
|
|
149
|
+
for (const m of msgs) console.error(' - ' + m)
|
|
150
|
+
console.error(' 解法:补齐上述 review 后重跑 align(stage review:sillyspec register-stage-review --change ' + changeName + ' --stage execute;task review:按报错路径补 review.json)')
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 1. Stage Review(tier 分级同正常 gate)
|
|
155
|
+
try {
|
|
156
|
+
const { classifyReviewTier } = await import('../review-tier.js')
|
|
157
|
+
const { validateStageReview, getLatestStageReviewRunId, printStageReviewResult } = await import('../stage-review.js')
|
|
158
|
+
const designPath = reviewChangeDir ? join(reviewChangeDir, 'design.md') : null
|
|
159
|
+
let planLevel = null
|
|
160
|
+
if (reviewChangeDir) {
|
|
161
|
+
const planPath = join(reviewChangeDir, 'plan.md')
|
|
162
|
+
if (existsSync(planPath)) {
|
|
163
|
+
const fmLine = readFileSync(planPath, 'utf8').split('\n').find(l => l.trim().startsWith('plan_level:'))
|
|
164
|
+
if (fmLine) planLevel = fmLine.split(':')[1].trim()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const tier = classifyReviewTier({ planLevel, designPath })
|
|
168
|
+
if (tier.tier === 'self') {
|
|
169
|
+
console.log('ℹ️ Stage Review: execute tier=self(' + tier.reason + '),align 前置门放行(不强制独立审查)')
|
|
170
|
+
} else {
|
|
171
|
+
// 只读解析(不 generate/写 marker):恢复场景下缺失即缺失
|
|
172
|
+
const reviewRunId = getLatestStageReviewRunId(runtimeRoot, 'execute', changeName)
|
|
173
|
+
if (!reviewRunId) {
|
|
174
|
+
return blocked(['execute stage review runId 无法定位(marker 缺失且 stage-reviews/ 无含 review 的 run 目录)'])
|
|
175
|
+
}
|
|
176
|
+
const searchDirs = [effectiveSpecBase, reviewChangeDir, cwd].filter(Boolean)
|
|
177
|
+
const reviewResult = validateStageReview({ stage: 'execute', reviewType: 'acceptance', runtimeRoot, reviewRunId, searchDirs })
|
|
178
|
+
printStageReviewResult(reviewResult, { stage: 'execute', reviewRunId, runtimeRoot })
|
|
179
|
+
if (!reviewResult.ok) return true // printStageReviewResult 已给明细,不再重复列
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// fail-closed:门自身异常阻断 align(与正常 gate 一致),不静默放行
|
|
183
|
+
console.error('❌ align 前置 Stage Review 校验异常,阻断对齐: ' + e.message)
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 2. Task Review(runId 只读解析:marker → 含 tasks/ 扫描;不 generate)
|
|
188
|
+
try {
|
|
189
|
+
const { validateTaskReviews, printReviewResult, resolveLatestExecuteRunIdWithTasks, isValidExecuteRunId } = await import('../task-review.js')
|
|
190
|
+
const planPath = reviewChangeDir ? join(reviewChangeDir, 'plan.md') : null
|
|
191
|
+
if (!planPath || !existsSync(planPath)) {
|
|
192
|
+
return blocked(['plan.md 不存在,Task Review 无从校验(align 的 checkbox 判定依赖同目录,疑似路径解析异常)'])
|
|
193
|
+
}
|
|
194
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
195
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
196
|
+
let executeRunId = ''
|
|
197
|
+
try {
|
|
198
|
+
if (existsSync(runIdFile)) {
|
|
199
|
+
const c = readFileSync(runIdFile, 'utf8').trim()
|
|
200
|
+
if (c && isValidExecuteRunId(c)) executeRunId = c
|
|
201
|
+
}
|
|
202
|
+
} catch {}
|
|
203
|
+
if (!executeRunId) {
|
|
204
|
+
executeRunId = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName }) || ''
|
|
205
|
+
}
|
|
206
|
+
if (!executeRunId) {
|
|
207
|
+
return blocked(['execute runId 无法定位(marker 缺失且 execute-runs/ 无含 review 的 run 目录)——15/N 份 task review 所在 run 不可寻,无法对账'])
|
|
208
|
+
}
|
|
209
|
+
const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId, changeDir: reviewChangeDir, gitDir: cwd })
|
|
210
|
+
printReviewResult(reviewResult, { runtimeRoot, executeRunId })
|
|
211
|
+
if (!reviewResult.ok) return true // 报错明细已在 printReviewResult 输出
|
|
212
|
+
console.log('✅ align 前置 review 校验通过(stage review + task review 均齐备),继续对齐')
|
|
213
|
+
} catch (e) {
|
|
214
|
+
console.error('❌ align 前置 Task Review 校验异常,阻断对齐: ' + e.message)
|
|
215
|
+
return true
|
|
216
|
+
}
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
|
|
121
220
|
/**
|
|
122
221
|
* execute deps 验证硬门(change 2026-06-28-worktree-deps-provision / D-001@v1, D-003@v1, D-006@v2)。
|
|
123
222
|
* depsStatus 不达标且非 wave 级 opt-out 时阻断 --done:置 step=blocked + exit(1),与 requiresWait 同范式。
|
|
@@ -178,19 +277,34 @@ export async function enforceReviewJsonGate(stageName, cwd, changeName, step, st
|
|
|
178
277
|
const planPath = join(specBase, 'changes', changeName, 'plan.md')
|
|
179
278
|
if (!existsSync(runIdFile) || !existsSync(planPath)) return true
|
|
180
279
|
const { validateCheckedTaskReviews, resolveLatestExecuteRunIdWithTasks, isValidExecuteRunId } = await import('../task-review.js')
|
|
181
|
-
|
|
280
|
+
// existsSync 与 read 之间 marker 可能被并发 cleanup/归档删除(多 agent 场景):读失败按
|
|
281
|
+
//「marker 缺失」处理,走漂移兜底重定位,不让 ENOENT 冒成顶层 stack(与 :426-435 口径对齐)
|
|
282
|
+
let executeRunId = ''
|
|
283
|
+
try {
|
|
284
|
+
executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
285
|
+
} catch {
|
|
286
|
+
console.warn('⚠️ execute run marker 读取失败(可能被并发清理),改扫真实 run 目录')
|
|
287
|
+
executeRunId = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName }) || ''
|
|
288
|
+
}
|
|
182
289
|
// marker 是 agent 可写内容:格式校验防注入/穿越,非法视为缺失走漂移兜底重定位
|
|
183
290
|
if (executeRunId && !isValidExecuteRunId(executeRunId)) {
|
|
184
291
|
console.warn(`⚠️ execute run marker 内容非法(期望 exec-YYYY-MM-DD-HHMMSS,实得 ${JSON.stringify(executeRunId.slice(0, 60))}),改扫真实 run 目录`)
|
|
185
292
|
executeRunId = ''
|
|
186
293
|
}
|
|
187
|
-
|
|
294
|
+
// 2026-08-20-task-truth-unify:已勾 task 清单源迁 tasks.md(勾选唯一落点);tasks.md 缺失
|
|
295
|
+
// 回退 planPath 内容(旧变更兼容读侧)。planPath 存在性已在上方把关。
|
|
296
|
+
const tasksRegistryPath = join(specBase, 'changes', changeName, 'tasks.md')
|
|
297
|
+
const planContent = existsSync(tasksRegistryPath)
|
|
298
|
+
? readFileSync(tasksRegistryPath, 'utf8')
|
|
299
|
+
: readFileSync(planPath, 'utf8')
|
|
300
|
+
// marker 读失败且重定位无果(无任何含 tasks/ 的 run):无 run 可校验,放行(下游 Task Review Gate 兜底)
|
|
301
|
+
if (!executeRunId) return true
|
|
188
302
|
// marker 漂移兜底(gate-atom-a 正确修法):marker 指向的 run 缺 tasks/(generateExecuteRunId 只写
|
|
189
303
|
// marker 不建目录,漂移后新 run 不继承旧 review)时,无视 marker 改扫 execute-runs/ 取 mtime 最新
|
|
190
304
|
// 且真正含 tasks/ 的 run,用其齐备的 review.json 校验,避免误报「review.json 不存在」。注意不能用
|
|
191
305
|
// resolveLatestExecuteRunId——它见 marker 非空即原样返回(不校验目录),恰是本场景要绕开的值。
|
|
192
306
|
if (executeRunId && !existsSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'))) {
|
|
193
|
-
const relocated = resolveLatestExecuteRunIdWithTasks({ runtimeRoot })
|
|
307
|
+
const relocated = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName })
|
|
194
308
|
if (relocated && relocated !== executeRunId) {
|
|
195
309
|
console.warn(`⚠️ execute run marker 漂移:${executeRunId} 无 tasks/,改用真实含 review 的 run ${relocated}`)
|
|
196
310
|
executeRunId = relocated
|
|
@@ -331,18 +445,21 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
331
445
|
console.log('\n✅ 验证通过,下一步:sillyspec run archive')
|
|
332
446
|
}
|
|
333
447
|
|
|
334
|
-
// ── Plan postcheck contract:plan.md
|
|
448
|
+
// ── Plan postcheck contract:tasks.md(注册表)× plan.md(Wave 引用)须满足 execute 契约 ──
|
|
335
449
|
if (stageName === 'plan') {
|
|
336
450
|
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
337
451
|
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
338
452
|
if (planPath && existsSync(planPath)) {
|
|
339
453
|
const { validatePlanForExecute } = await import('../stages/execute.js')
|
|
340
|
-
|
|
341
|
-
const
|
|
454
|
+
// CRLF 归一:Windows 编辑器/python 写文件可产生 CRLF,归一后交校验(与 plan-postcheck.js 同口径)
|
|
455
|
+
const planContent = readFileSync(planPath, 'utf8').replace(/\r\n/g, '\n')
|
|
456
|
+
const tasksPath = join(planFile, 'tasks.md')
|
|
457
|
+
const tasksContent = existsSync(tasksPath) ? readFileSync(tasksPath, 'utf8').replace(/\r\n/g, '\n') : ''
|
|
458
|
+
const planValidation = validatePlanForExecute(tasksContent, planContent)
|
|
342
459
|
if (!planValidation.ok) {
|
|
343
460
|
console.error(`\n❌ Plan → Execute Contract 校验失败:`)
|
|
344
461
|
for (const err of planValidation.errors) console.error(` - ${err}`)
|
|
345
|
-
console.error(`\n plan.md 不满足 execute 契约,请修复后重新完成此步骤。`)
|
|
462
|
+
console.error(`\n tasks.md/plan.md 不满足 execute 契约,请修复后重新完成此步骤。`)
|
|
346
463
|
// 阻断 completed
|
|
347
464
|
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
348
465
|
}
|
|
@@ -415,7 +532,12 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
415
532
|
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
416
533
|
|
|
417
534
|
if (planPath && existsSync(planPath)) {
|
|
418
|
-
|
|
535
|
+
// 2026-08-20-task-truth-unify:任务清单源迁 tasks.md(validateTaskReviews 内按
|
|
536
|
+
// checkbox 行解析,tasks.md 行格式兼容原正则);缺失回退 plan.md(旧变更兼容读侧)
|
|
537
|
+
const tasksRegistryPath = join(planFile, 'tasks.md')
|
|
538
|
+
const planContent = existsSync(tasksRegistryPath)
|
|
539
|
+
? readFileSync(tasksRegistryPath, 'utf8')
|
|
540
|
+
: readFileSync(planPath, 'utf8')
|
|
419
541
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, effectiveSpecBase)
|
|
420
542
|
|
|
421
543
|
// execute run id:从变更专属标记文件读取(agent 可写内容,格式校验防注入/穿越)
|
|
@@ -436,7 +558,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
436
558
|
// marker 缺失:先扫描 execute-runs/ 既有目录找回真实 runId(与 getLatestStageReviewRunId
|
|
437
559
|
// 目录扫描兜底同语义),避免 marker 丢失而 agent 已用旧 runId 落盘时,直接 generate 新 ID
|
|
438
560
|
// 找不到旧 review、误判缺 review.json。仅当确实无既有 run 才 generate 新 ID 并落盘。
|
|
439
|
-
const { generateExecuteRunId, resolveLatestExecuteRunId } = await import('../task-review.js')
|
|
561
|
+
const { generateExecuteRunId, resolveLatestExecuteRunId, stampExecuteRunChange } = await import('../task-review.js')
|
|
440
562
|
executeRunId = resolveLatestExecuteRunId({ runtimeRoot, changeName }) || ''
|
|
441
563
|
if (!executeRunId) {
|
|
442
564
|
executeRunId = generateExecuteRunId()
|
|
@@ -446,6 +568,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
446
568
|
//(gate 自身写 run 目录失败不能静默放行完成)。
|
|
447
569
|
mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
|
|
448
570
|
writeFileSync(runIdFile, executeRunId + '\n')
|
|
571
|
+
stampExecuteRunChange(runtimeRoot, executeRunId, changeName)
|
|
449
572
|
}
|
|
450
573
|
}
|
|
451
574
|
|
|
@@ -480,7 +603,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
480
603
|
// 检查是否存在 checkbox 已勾但 review 不通过的情况
|
|
481
604
|
const uncheckedTasks = reviewResult.errors.filter(e => e.includes('缺少 review.json'))
|
|
482
605
|
if (uncheckedTasks.length > 0) {
|
|
483
|
-
console.error('\n⚠️ 部分任务已在
|
|
606
|
+
console.error('\n⚠️ 部分任务已在 tasks.md 中勾选,但 review.json 不存在。')
|
|
484
607
|
console.error(` 请取消勾选这些任务的 checkbox,或补充对应的 review.json(execute run ID: ${executeRunId})。`)
|
|
485
608
|
}
|
|
486
609
|
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
package/src/run/prompt.js
CHANGED
|
@@ -257,7 +257,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
257
257
|
if (existsSync(sessionGuardFile)) {
|
|
258
258
|
qlIdVal = JSON.parse(readFileSync(sessionGuardFile, 'utf8')).quicklogId || ''
|
|
259
259
|
}
|
|
260
|
-
} catch {
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.warn(`⚠️ quicklog-id 占位符读取 guard.json 失败: ${err && err.message ? err.message : err}`);
|
|
262
|
+
}
|
|
261
263
|
promptText = promptText.replace(/<quicklog-id>/g, qlIdVal || '(未分配)')
|
|
262
264
|
}
|
|
263
265
|
// 替换 <linked-changes> 占位符(quick 阶段:从 .runtime/quick-sessions/<sessionId>/guard.json 读关联变更)
|
|
@@ -414,14 +416,16 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
414
416
|
const { matchKnowledge } = await import('../knowledge-match.js')
|
|
415
417
|
const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
416
418
|
const knowledgeDir = join(effectiveSpecBase, 'knowledge')
|
|
417
|
-
// taskContext: changeName +
|
|
419
|
+
// taskContext: changeName + tasks.md 任务名(注册表唯一真相,2026-08-20-task-truth-unify)
|
|
418
420
|
let taskContext = changeName || ''
|
|
419
421
|
if (changeName) {
|
|
422
|
+
const tasksMdPath = join(effectiveSpecBase, 'changes', changeName, 'tasks.md')
|
|
420
423
|
const planPath = join(effectiveSpecBase, 'changes', changeName, 'plan.md')
|
|
424
|
+
const srcPath = existsSync(tasksMdPath) ? tasksMdPath : planPath
|
|
421
425
|
try {
|
|
422
|
-
const
|
|
423
|
-
// match
|
|
424
|
-
const taskLines = [...
|
|
426
|
+
const registryContent = readFileSync(srcPath, 'utf8')
|
|
427
|
+
// match "- [ ] task-01: title" / "## task-01: title"(兼容旧 plan.md 内联两种形态)
|
|
428
|
+
const taskLines = [...registryContent.matchAll(/(?:^\- \[[ x]\] |^## )task-\d+[^:]*:?\s*(.+)/gm)]
|
|
425
429
|
if (taskLines.length > 0) {
|
|
426
430
|
taskContext += ' ' + taskLines.map(t => t[1]).join(' ')
|
|
427
431
|
}
|
package/src/run/shared.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 本模块仅 re-export,run/ 层现有调用方路径与行为不变。
|
|
12
12
|
*/
|
|
13
13
|
import { basename, join, resolve, dirname, sep } from 'node:path'
|
|
14
|
-
import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
|
|
15
15
|
import { fileURLToPath } from 'node:url'
|
|
16
16
|
import os from 'node:os'
|
|
17
17
|
|
|
@@ -336,12 +336,26 @@ export const PLATFORM_MANAGED_FILENAME = '.sillyspec-platform-managed'
|
|
|
336
336
|
|
|
337
337
|
export function writePlatformPointer(cwd, platformOpts, extra = {}) {
|
|
338
338
|
if (!platformOpts || (!platformOpts.specRoot && !platformOpts.runtimeRoot)) return false
|
|
339
|
+
// HUB-05:合并保留既有生命周期字段。scan 完成时指针被写入 status/completedAt/scanStatus
|
|
340
|
+
//(complete-handlers),但下一次任何平台模式 run(含只读 --status)都会重写指针——
|
|
341
|
+
// 恢复链只回填 specRoot 等四字段,不保留 status → 指针永远回 active、isPointerStale
|
|
342
|
+
// 恒 false、`platform pointer --cleanup` 的 STALE 分支不可达。extra 显式传值仍可覆盖。
|
|
343
|
+
let preserved = {}
|
|
344
|
+
try {
|
|
345
|
+
const existing = JSON.parse(readFileSync(join(cwd, '.sillyspec-platform.json'), 'utf8'))
|
|
346
|
+
if (existing && typeof existing === 'object') {
|
|
347
|
+
for (const k of ['status', 'completedAt', 'scanStatus']) {
|
|
348
|
+
if (existing[k] !== undefined && extra[k] === undefined) preserved[k] = existing[k]
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} catch { /* 无既有指针/损坏 → 全新写入 */ }
|
|
339
352
|
const payload = {
|
|
340
353
|
specRoot: platformOpts.specRoot || null,
|
|
341
354
|
runtimeRoot: platformOpts.runtimeRoot || null,
|
|
342
355
|
workspaceId: platformOpts.workspaceId || null,
|
|
343
356
|
scanRunId: platformOpts.scanRunId || null,
|
|
344
357
|
savedAt: new Date().toISOString(),
|
|
358
|
+
...preserved,
|
|
345
359
|
...extra,
|
|
346
360
|
}
|
|
347
361
|
// 声明文件字段集独立于指针 payload(D-E@v2 四字段:无 status/savedAt/scanRunId——
|
|
@@ -415,54 +429,90 @@ export function resolveChangeDir(cwd, progress, specDir = null) {
|
|
|
415
429
|
// sync 总超时熔断:sync.js 是 best-effort 后台回传(每请求已有 10s 超时),但 sync() 可能串行多次
|
|
416
430
|
// fetchJson 累积等待、阻塞 --done。给 8s 总超时,超时放弃(best-effort,失败不影响正确性,下次 --done 重试)。
|
|
417
431
|
// 历史痛点:--done 在 sync 慢时体感 hang,用户被迫用外部 timeout 兜底。
|
|
432
|
+
// HUB-09:熔断超时时必须 abort 底层在飞请求(opts.signal 一路传到 fetch)——否则熔断后请求
|
|
433
|
+
// 仍在跑,平台可能已接受推送而客户端当作超时放弃(spec 树推送无 base_ts 自愈兜底)。
|
|
418
434
|
const SYNC_TOTAL_TIMEOUT_MS = 8_000
|
|
419
435
|
|
|
436
|
+
/**
|
|
437
|
+
* 熔断 race + 确定性取消:超时触发 controller.abort() 并 resolve,在飞 fetch 收到
|
|
438
|
+
* AbortError 后自然失败(各调用方 catch 返回降级值,进程不挂起)。
|
|
439
|
+
* @param {() => Promise} op 接收 { signal } 的异步操作
|
|
440
|
+
* @param {number} timeoutMs 熔断时长(测试可缩短)
|
|
441
|
+
*/
|
|
442
|
+
async function raceWithAbort(op, timeoutMs = SYNC_TOTAL_TIMEOUT_MS) {
|
|
443
|
+
const controller = new AbortController()
|
|
444
|
+
let timer
|
|
445
|
+
try {
|
|
446
|
+
await Promise.race([
|
|
447
|
+
op({ signal: controller.signal }),
|
|
448
|
+
new Promise((resolve) => { timer = setTimeout(() => { controller.abort(); resolve(null) }, timeoutMs) }),
|
|
449
|
+
])
|
|
450
|
+
} finally {
|
|
451
|
+
clearTimeout(timer)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
420
455
|
// quick 会话 ID 形态(quick-<hex8>)。与 run/command.js QUICK_SID_RE、progress.js
|
|
421
456
|
// initChange 的跳过实体目录判断同源;shared.js 不反向 import command.js(重模块),
|
|
422
457
|
// 局部复制正则保持轻载——改形态时三处同改。
|
|
423
458
|
const QUICK_SID_RE = /^quick-[0-9a-f]{8}$/
|
|
424
459
|
|
|
425
|
-
export async function triggerSync(cwd, changeName, platformOpts = {}) {
|
|
460
|
+
export async function triggerSync(cwd, changeName, platformOpts = {}, opts = {}) {
|
|
426
461
|
// 平台模式(SillyHub)走自己的回传链路,不走 CLI 内置 sync
|
|
427
462
|
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return
|
|
428
463
|
try {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
//
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const syncMod = await import('../sync.js')
|
|
437
|
-
let timer
|
|
438
|
-
try {
|
|
439
|
-
await Promise.race([
|
|
440
|
-
syncMod.syncSpecTreeOnly(changeName, cwd),
|
|
441
|
-
new Promise((resolve) => { timer = setTimeout(resolve, SYNC_TOTAL_TIMEOUT_MS) }),
|
|
442
|
-
])
|
|
443
|
-
} finally {
|
|
444
|
-
clearTimeout(timer)
|
|
445
|
-
}
|
|
446
|
-
}
|
|
464
|
+
// ql-20260818-011:quick 会话按设计无实体变更目录(progress.js initChange 同款
|
|
465
|
+
// 跳过建目录),progress/四件套上行对它是孤儿数据;但 spec 树增量(QUICKLOG/
|
|
466
|
+
// 模块文档的上行通道)以服务器清单为锚,与变更目录无关——降级只推 spec 树。
|
|
467
|
+
if (changeName && QUICK_SID_RE.test(changeName) && !existsSync(join(cwd, '.sillyspec', 'changes', changeName))) {
|
|
468
|
+
// shared.js 在 src/run/,sync.js 在 src/ → 退一层
|
|
469
|
+
const syncMod = await import('../sync.js')
|
|
470
|
+
await raceWithAbort((sig) => syncMod.syncSpecTreeOnly(changeName, cwd, sig), opts.timeoutMs)
|
|
447
471
|
return
|
|
448
472
|
}
|
|
473
|
+
// 目录缺失的非 quick 变更不再静默 return(archive-final-state-sync):归档把
|
|
474
|
+
// changes/<name>/ 移到 changes/archive/<name>/ 并注销 DB 行后,第 4/5 步收尾的
|
|
475
|
+
// triggerSync 仍会触发——DB 里的终态(steps 完成态 + status='archived')与文件树
|
|
476
|
+
// 移动必须推平台,否则平台进度永远停在移动前的最后一步。sync() 内部对目录缺失
|
|
477
|
+
// 本就是 warn-and-continue(serializeForSync 从 DB 读不依赖目录);真实拼错名时
|
|
478
|
+
// 无 DB 行,sync() 在发出任何 HTTP 前即 return,仅多一行 warn(未连接平台则完全静默)。
|
|
449
479
|
// shared.js 在 src/run/,sync.js 在 src/ → 退一层
|
|
450
480
|
const syncMod = await import('../sync.js')
|
|
451
|
-
|
|
452
|
-
try {
|
|
453
|
-
await Promise.race([
|
|
454
|
-
syncMod.sync(changeName, cwd),
|
|
455
|
-
new Promise((resolve) => { timer = setTimeout(resolve, SYNC_TOTAL_TIMEOUT_MS) }),
|
|
456
|
-
])
|
|
457
|
-
} finally {
|
|
458
|
-
clearTimeout(timer) // sync 先完成则清掉未触发的 timer,避免泄漏
|
|
459
|
-
}
|
|
481
|
+
await raceWithAbort((sig) => syncMod.sync(changeName, cwd, sig), opts.timeoutMs)
|
|
460
482
|
} catch (e) {
|
|
461
483
|
// sync.js 不存在或同步失败,静默跳过
|
|
462
484
|
console.warn('⚠️ 同步失败:', e.message)
|
|
463
485
|
}
|
|
464
486
|
}
|
|
465
487
|
|
|
488
|
+
/**
|
|
489
|
+
* 自动 pull 节流(体检 PERF-02):triggerPull/triggerPullActiveChange 在每条 stage 命令
|
|
490
|
+
* 启动时注入,agent 命令往往是秒级连发——每次都发 HTTP GET(daemon 挂/慢时单命令最多
|
|
491
|
+
* 阻塞 8s)。跨进程 marker 节流:10s 内已有自动 pull 则跳过(手动 `platform pull` 不走
|
|
492
|
+
* 此门,语义不变)。错过窗口的平台更新由下一条 >10s 的命令或 push 409 自愈兜底。
|
|
493
|
+
*/
|
|
494
|
+
const AUTO_PULL_THROTTLE_MS = 10_000
|
|
495
|
+
|
|
496
|
+
function _autoPullThrottlePath(cwd) {
|
|
497
|
+
return join(cwd, '.sillyspec', '.runtime', 'auto-pull-throttle.json')
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function _autoPullRecently(cwd) {
|
|
501
|
+
try {
|
|
502
|
+
const t = JSON.parse(readFileSync(_autoPullThrottlePath(cwd), 'utf8'))
|
|
503
|
+
return typeof t.at === 'number' && Date.now() - t.at < AUTO_PULL_THROTTLE_MS
|
|
504
|
+
} catch {
|
|
505
|
+
return false
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function _stampAutoPull(cwd) {
|
|
510
|
+
try {
|
|
511
|
+
mkdirSync(dirname(_autoPullThrottlePath(cwd)), { recursive: true })
|
|
512
|
+
writeFileSync(_autoPullThrottlePath(cwd), JSON.stringify({ at: Date.now() }) + '\n', 'utf8')
|
|
513
|
+
} catch { /* best-effort:节流失效仅退化为逐次 pull */ }
|
|
514
|
+
}
|
|
515
|
+
|
|
466
516
|
/**
|
|
467
517
|
* 触发 pull(下行同步,task-10 / D-009 / FR-04 / FR-06)。
|
|
468
518
|
* 复用 triggerSync 的 8s 熔断与 Best Effort 语义;未连接平台静默跳过(与现状一致)。
|
|
@@ -474,23 +524,18 @@ export async function triggerSync(cwd, changeName, platformOpts = {}) {
|
|
|
474
524
|
* @param {string} changeName - 当前活跃变更(多变更时传 null 跳过,避免误拉)
|
|
475
525
|
* @param {object} [platformOpts] - 平台模式 opts(specRoot/runtimeRoot 存在则跳过,走平台自有链路)
|
|
476
526
|
*/
|
|
477
|
-
export async function triggerPull(cwd, changeName, platformOpts = {}) {
|
|
527
|
+
export async function triggerPull(cwd, changeName, platformOpts = {}, opts = {}) {
|
|
478
528
|
// 平台模式(SillyHub)走自己的链路,跳过
|
|
479
529
|
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return
|
|
480
530
|
try {
|
|
481
531
|
const syncMod = await import('../sync.js')
|
|
482
532
|
const sm = new syncMod.SyncManager(cwd)
|
|
483
|
-
//
|
|
533
|
+
// 未连接平台静默跳过(本地独立用户合法状态,不噪音)——节流 stamp 必须在此之后:
|
|
534
|
+
// 否则未连接的项目每次 run 都写 .sillyspec/.runtime/ 标记(凭空造目录)
|
|
484
535
|
if (!sm._getPlatform()) return
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
sm.pull(changeName, { skipIfLocalDirty: true }),
|
|
489
|
-
new Promise((resolve) => { timer = setTimeout(resolve, SYNC_TOTAL_TIMEOUT_MS) }),
|
|
490
|
-
])
|
|
491
|
-
} finally {
|
|
492
|
-
clearTimeout(timer)
|
|
493
|
-
}
|
|
536
|
+
if (_autoPullRecently(cwd)) return
|
|
537
|
+
_stampAutoPull(cwd)
|
|
538
|
+
await raceWithAbort((sig) => sm.pull(changeName, { skipIfLocalDirty: true, signal: sig.signal }), opts.timeoutMs)
|
|
494
539
|
} catch (e) {
|
|
495
540
|
// pull 失败静默跳过(Best Effort,失败不影响正确性)
|
|
496
541
|
console.warn('⚠️ 拉取失败:', e.message)
|
|
@@ -513,6 +558,10 @@ export async function triggerPullActiveChange(cwd, platformOpts = {}) {
|
|
|
513
558
|
} catch {
|
|
514
559
|
return
|
|
515
560
|
}
|
|
561
|
+
// PERF-02 节流(连接确认后才 stamp——未连接项目不得凭空写 .sillyspec/.runtime/):
|
|
562
|
+
// 与 triggerPull 共用同一 marker(本函数与它在同一命令启动链上互斥出现)
|
|
563
|
+
if (_autoPullRecently(cwd)) return
|
|
564
|
+
_stampAutoPull(cwd)
|
|
516
565
|
let cn = null
|
|
517
566
|
try {
|
|
518
567
|
const { ProgressManager } = await import('../progress.js')
|
|
@@ -523,29 +572,48 @@ export async function triggerPullActiveChange(cwd, platformOpts = {}) {
|
|
|
523
572
|
// progress 不可达则跳过(Best Effort)
|
|
524
573
|
}
|
|
525
574
|
if (!cn) return
|
|
526
|
-
// 已确认连接 + 单活跃变更,调 pull(复用
|
|
575
|
+
// 已确认连接 + 单活跃变更,调 pull(复用 8s 熔断 + abort(HUB-09);skipIfLocalDirty 保守守卫
|
|
527
576
|
// 同 triggerPull——本地脏时跳过 import,防平台旧快照覆盖本地领先进度,ql-20260818-008)
|
|
528
577
|
try {
|
|
529
|
-
|
|
530
|
-
try {
|
|
531
|
-
await Promise.race([
|
|
532
|
-
sm.pull(cn, { skipIfLocalDirty: true }),
|
|
533
|
-
new Promise((resolve) => { timer = setTimeout(resolve, SYNC_TOTAL_TIMEOUT_MS) }),
|
|
534
|
-
])
|
|
535
|
-
} finally {
|
|
536
|
-
clearTimeout(timer)
|
|
537
|
-
}
|
|
578
|
+
await raceWithAbort((sig) => sm.pull(cn, { skipIfLocalDirty: true, signal: sig.signal }))
|
|
538
579
|
} catch (e) {
|
|
539
580
|
console.warn('⚠️ 拉取失败:', e.message)
|
|
540
581
|
}
|
|
541
582
|
}
|
|
542
583
|
|
|
584
|
+
/**
|
|
585
|
+
* 审批状态 unknown(体检 HUB-07)的统一处置:醒目多行警告 + 落 .runtime/approval-unknown.log 留痕。
|
|
586
|
+
*
|
|
587
|
+
* 语义边界:unknown = 已连接平台但 404/断网/超时/非 JSON——**不是审批通过**。放行是
|
|
588
|
+
* fail-open 的既定取舍(网络故障不应硬阻断每一条 execute),但静默放行会让团队审批
|
|
589
|
+
* 形同虚设且无迹可查;至少要(a)让操作者当场看见(b)留下可审计记录。
|
|
590
|
+
* 留痕选 .runtime 日志而非 QUICKLOG:QUICKLOG 是 quick 会话的任务记录,run 流程的
|
|
591
|
+
* 审批事件写进去会污染 quick 语义与边界审计。
|
|
592
|
+
* @param {string} cwd
|
|
593
|
+
* @param {string} changeName
|
|
594
|
+
* @param {string} reason checkApproval 返回的 reason
|
|
595
|
+
*/
|
|
596
|
+
export function warnApprovalUnknown(cwd, changeName, reason) {
|
|
597
|
+
console.warn('')
|
|
598
|
+
console.warn('⚠️⚠️⚠️══════════════════════════════════════════════')
|
|
599
|
+
console.warn(`⚠️ 审批状态未知:变更「${changeName}」无法核实审批(${reason || '请求失败'})`)
|
|
600
|
+
console.warn('⚠️ 本次 execute 按 fail-open 放行——这不是审批通过;平台恢复后请到平台复核审批记录')
|
|
601
|
+
console.warn('⚠️ 排查:平台服务是否可达(sillyspec platform status)/ 网络代理;确认无需审批可 --skip-approval 显式跳过')
|
|
602
|
+
console.warn('⚠️ 本次放行已记录:.sillyspec/.runtime/approval-unknown.log')
|
|
603
|
+
console.warn('⚠️⚠️══════════════════════════════════════════════')
|
|
604
|
+
try {
|
|
605
|
+
const logDir = join(cwd, '.sillyspec', '.runtime')
|
|
606
|
+
mkdirSync(logDir, { recursive: true })
|
|
607
|
+
appendFileSync(join(logDir, 'approval-unknown.log'),
|
|
608
|
+
`${new Date().toISOString()} | change=${changeName} | reason=${reason || 'unknown'}\n`, 'utf8')
|
|
609
|
+
} catch { /* 留痕失败不阻断流程(警告横幅已输出) */ }
|
|
610
|
+
}
|
|
611
|
+
|
|
543
612
|
/**
|
|
544
613
|
* 审批检查:execute 阶段启动前检查(W6 Step8a 从 run.js 搬入,runStage + runAutoMode 共用)。
|
|
545
614
|
* 平台模式走自己的链路,跳过;否则 await import sync.js。
|
|
546
615
|
* @returns {{ status: string, reason?: string } | null}
|
|
547
|
-
*/
|
|
548
|
-
export async function checkApproval(cwd, changeName, platformOpts = {}) {
|
|
616
|
+
*/export async function checkApproval(cwd, changeName, platformOpts = {}) {
|
|
549
617
|
// 平台模式不需要 CLI 内置审批检查
|
|
550
618
|
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return null
|
|
551
619
|
try {
|
|
@@ -774,7 +842,11 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
774
842
|
]
|
|
775
843
|
|
|
776
844
|
for (const entry of currentEntries) {
|
|
777
|
-
|
|
845
|
+
// porcelain 两字符状态码(X=暂存区 Y=工作区)不能先 trim:' D'.trim()==='D' 使第二分支恒假、
|
|
846
|
+
// 'DD'/'AD' 等 trim 后不等于 'D' 而漏检——--allow-delete 门对此类删除静默放行(体检 BUG-04)。
|
|
847
|
+
// 用原始两字符判定任一侧为 D 即删除。
|
|
848
|
+
const rawStatus = entry.slice(0, 2)
|
|
849
|
+
const status = rawStatus.trim()
|
|
778
850
|
const file = parsePorcelainPath(entry) // 已去引号/处理 rename/归一化,修正首行丢首字符
|
|
779
851
|
if (!file) continue
|
|
780
852
|
|
|
@@ -786,7 +858,7 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
786
858
|
if (isBaselineFile(file)) continue
|
|
787
859
|
|
|
788
860
|
result.changedFiles.push(file)
|
|
789
|
-
if (
|
|
861
|
+
if (rawStatus[0] === 'D' || rawStatus[1] === 'D') result.deletedFiles.push(file)
|
|
790
862
|
if (status === '??') result.newFiles.push(file)
|
|
791
863
|
|
|
792
864
|
// 检查是否命中 baseline protected files
|
|
@@ -885,8 +957,12 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
885
957
|
const mdChanged = result.changedFiles.filter((f) => f.endsWith('.md') && !deletedSet.has(f) && !isQuickMetadata(f, guard.linkedChanges))
|
|
886
958
|
if (mdChanged.length > 0) {
|
|
887
959
|
try {
|
|
888
|
-
const { runDocsCheck } = await import('../docs-check.js')
|
|
889
|
-
|
|
960
|
+
const { runDocsCheck, readDocsCheckConfig } = await import('../docs-check.js')
|
|
961
|
+
// HUB-06:补传 crossRepoRoots(与 docs gate / index.js docs check 同口径)——
|
|
962
|
+
// 否则 repo:// 跨仓引用在这条链路恒跳过;无 local.yaml 配置时 undefined = 不启用
|
|
963
|
+
let quickCfg = {}
|
|
964
|
+
try { quickCfg = readDocsCheckConfig(cwd) || {} } catch { quickCfg = {} }
|
|
965
|
+
const dc = runDocsCheck({ projectRoot: cwd, docs: mdChanged, crossRepoRoots: quickCfg.crossRepoRoots })
|
|
890
966
|
if (!dc.ok) {
|
|
891
967
|
result.reasons.push(`本次改动文档含 ${dc.invalid.length} 处失效 file:line 引用(sillyspec docs check 可复现)`)
|
|
892
968
|
result.docsCheckHint = { invalid: dc.invalid.length, total: dc.total }
|
|
@@ -906,7 +982,11 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
906
982
|
const srcChangedFiles = result.changedFiles.filter((f) => f.startsWith('src/') && !deletedSet.has(f))
|
|
907
983
|
if (srcChangedFiles.length > 0) {
|
|
908
984
|
try {
|
|
909
|
-
const { collectDocRefs, runDocsCheck } = await import('../docs-check.js')
|
|
985
|
+
const { collectDocRefs, runDocsCheck, readDocsCheckConfig } = await import('../docs-check.js')
|
|
986
|
+
// HUB-06:活文档(platform-interface-map.md 等最可能写 repo:// 引用的文档)同样补传
|
|
987
|
+
// crossRepoRoots;配置读一次提到循环外(per-cwd 恒同)
|
|
988
|
+
let livingCfg = {}
|
|
989
|
+
try { livingCfg = readDocsCheckConfig(cwd) || {} } catch { livingCfg = {} }
|
|
910
990
|
const livingDocs = await resolveLivingDocs(cwd)
|
|
911
991
|
const files = []
|
|
912
992
|
const docs = []
|
|
@@ -918,7 +998,7 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
918
998
|
if (hit.length === 0) continue
|
|
919
999
|
files.push(...hit)
|
|
920
1000
|
docs.push(docRel)
|
|
921
|
-
const r = runDocsCheck({ projectRoot: cwd, docs: [docRel], keywordAssert: true })
|
|
1001
|
+
const r = runDocsCheck({ projectRoot: cwd, docs: [docRel], keywordAssert: true, crossRepoRoots: livingCfg.crossRepoRoots })
|
|
922
1002
|
for (const x of r.invalid) invalidAll.push({ ...x, doc: docRel })
|
|
923
1003
|
}
|
|
924
1004
|
const invalid = matchInvalidRefsToChanged(invalidAll, srcChangedFiles)
|