sillyspec 3.28.8 → 3.28.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-execute/SKILL.md +222 -224
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/change-risk-profile.js +20 -0
- package/src/config-schema.js +32 -0
- package/src/db.js +11 -3
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +33 -3
- package/src/docs-gate.js +105 -6
- package/src/doctor-diagnostics.js +224 -2
- package/src/friction-ledger.js +171 -0
- package/src/hooks/worktree-guard.js +13 -1
- package/src/index.js +188 -17
- package/src/knowledge-classify.js +363 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/progress/change-registry.js +183 -8
- package/src/progress/shared.js +3 -2
- package/src/progress.js +100 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +155 -14
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +107 -12
- package/src/run/complete-handlers.js +511 -21
- package/src/run/complete.js +54 -16
- package/src/run/concurrent-detect.js +98 -0
- package/src/run/gates.js +50 -9
- package/src/run/prompt.js +134 -5
- package/src/run/quick-audit.js +111 -13
- package/src/run/shared.js +76 -10
- package/src/run/stage.js +6 -0
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +365 -35
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +23 -7
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/execute.js +85 -3
- package/src/stages/knowledge.js +52 -3
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +114 -6
- package/src/sync.js +135 -7
- package/src/task-review.js +292 -21
- package/src/verify-postcheck.js +20 -0
- package/src/verify-probes.js +340 -4
- package/src/worktree-apply.js +538 -44
- package/src/worktree-deps.js +100 -2
- package/src/worktree.js +117 -5
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/.patch16-tmp.mjs +0 -169
- package/sa-rename-probe/src/new.js +0 -4
package/src/run/complete.js
CHANGED
|
@@ -23,6 +23,7 @@ import { writeAtomicSync } from '../fs-atomic.js'
|
|
|
23
23
|
import { gitQuiet } from '../git-helper.js'
|
|
24
24
|
import { withFileLock } from '../quicklog.js'
|
|
25
25
|
import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir } from './shared.js'
|
|
26
|
+
import { isExplicitReviewWrite } from '../task-review.js'
|
|
26
27
|
|
|
27
28
|
// auto 模式 brainstorm 步骤表感知解析(2026-09-08 E2E 实证 bug:--done/--wait 推进主模式 8 步表,
|
|
28
29
|
// 与 runAutoMode getAutoSteps 的 4 步 auto 表双轨互踩——ensureAutoStage 判非 auto 表重种清零,
|
|
@@ -119,7 +120,7 @@ export function synthesizeStepOutput({ stageName, stepName, cwd }) {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
122
|
-
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false, quickFiles = [] } = options
|
|
123
|
+
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false, isNoDocs = false, isSkipApply = false, sessionFlag = null, quickFiles = [] } = options
|
|
123
124
|
// specRoot(平台) > specDriftAnchor(worktree 漂移锚定主仓) > cwd/.sillyspec(本地)——
|
|
124
125
|
// 与 prompt.js resolvePromptSpecBase 同序:--done 收尾的 user-inputs.md/超长 artifact 落盘
|
|
125
126
|
// 必须与 prompt 渲染同一根,否则写进 worktree 副本目录、随 cleanup 整目录删除。
|
|
@@ -494,7 +495,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
494
495
|
// archive「确认归档」收尾:--confirm 门控 + archiveChangeDirectory 移动 + 推荐文档校验
|
|
495
496
|
// (抽成 handleArchiveConfirmStep;缺 --confirm 时返回 early-return 对象,completeStep 透传)
|
|
496
497
|
{
|
|
497
|
-
const _archiveEarlyReturn = await handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase, platformOpts })
|
|
498
|
+
const _archiveEarlyReturn = await handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase, platformOpts, isSkipApply, sessionFlag })
|
|
498
499
|
if (_archiveEarlyReturn) return _archiveEarlyReturn
|
|
499
500
|
}
|
|
500
501
|
|
|
@@ -568,8 +569,10 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
568
569
|
console.log(` 恢复:sillyspec run ${stageName} --continue --answer "用户回答"${changeName ? ` --change ${changeName}` : ''}`)
|
|
569
570
|
return { stageCompleted: false, currentIdx, nextPendingIdx: -1 }
|
|
570
571
|
}
|
|
571
|
-
// quick 收尾(W6 Step6b 抽至 complete-handlers.js handleQuickStageCompletion
|
|
572
|
-
|
|
572
|
+
// quick 收尾(W6 Step6b 抽至 complete-handlers.js handleQuickStageCompletion);
|
|
573
|
+
// isNoDocs(task-02)--no-docs 豁免随链透传进 auditQuickCompletion options;
|
|
574
|
+
// sessionFlag(task-03)--session 随链透传进 quick 轻量归档链所有权校验
|
|
575
|
+
await handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, isNoDocs, sessionFlag, platformOpts, pm, quickFiles })
|
|
573
576
|
|
|
574
577
|
// ── reopen --done 回填(坑 brainstorm-reopen-step-state-desync)──
|
|
575
578
|
// nextPendingIdx === -1 且无 waiting,说明要进阶段完成分支。此时若存在 stale 步骤
|
|
@@ -675,7 +678,16 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
675
678
|
const { consumeFrictionHint } = await import('../friction-tally.js')
|
|
676
679
|
const r = await consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
677
680
|
if (r && r.hint) console.log(`\n${r.hint}`)
|
|
678
|
-
|
|
681
|
+
// 台账滚动(2026-09-15-tax-governance FR-02 主滚动点):consume 得到的非零 counts 按
|
|
682
|
+
// change 滚动合并进 <runtimeRoot>/friction-ledger.json 幸存台账(merge-by-change——
|
|
683
|
+
// --reopen 重跑二次 consume 累加非双计);空 counts 跳过写(防干净收尾落空文件);
|
|
684
|
+
// archivedAt 此时不落(prune 侧落定);runtimeRoot 与 consume 同源(specBase 同变量
|
|
685
|
+
// + resolveRuntimeRoot 单点)。台账写失败 fail-soft 在既有 try/catch 内,绝不阻断收尾。
|
|
686
|
+
if (changeName && r && r.counts && Object.keys(r.counts).length > 0) {
|
|
687
|
+
const { mergeFrictionEntry } = await import('../friction-ledger.js')
|
|
688
|
+
await mergeFrictionEntry(resolveRuntimeRoot(platformOpts, specBase), { change: changeName, counts: r.counts })
|
|
689
|
+
}
|
|
690
|
+
} catch { /* 摩擦提示/台账失败不影响收尾 */ }
|
|
679
691
|
}
|
|
680
692
|
|
|
681
693
|
if (stageName === 'execute') {
|
|
@@ -834,7 +846,7 @@ async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts,
|
|
|
834
846
|
if (typeof frozenPatch === 'string') {
|
|
835
847
|
const patchText = frozenPatch.endsWith('\n') ? frozenPatch : frozenPatch + '\n'
|
|
836
848
|
writeFileSync(join(changeDir, 'scope-audit.patch'), patchText)
|
|
837
|
-
snap.patchSha256 = createHash('sha256').update(patchText, 'utf8').digest('hex')
|
|
849
|
+
snap.patchSha256 = createHash('sha256').update(patchText.replace(/\r\n/g, '\n'), 'utf8').digest('hex')
|
|
838
850
|
snap.patchStatus = 'ok'
|
|
839
851
|
} else if (frozenPatch === null) {
|
|
840
852
|
snap.patchStatus = 'failed'
|
|
@@ -975,35 +987,49 @@ function prefetchDiffFileSet(ctx) {
|
|
|
975
987
|
* changedFiles 非空且 git diff 实测非空。防 allowed_paths 误归属他人 diff 或陈旧 review.json 导致
|
|
976
988
|
* 空任务被自动勾选/批量放行。ctx 缺省时保持现行判定(向后兼容)。
|
|
977
989
|
*
|
|
978
|
-
*
|
|
990
|
+
* ql-20260915-001 修复②(坑 review-write-draft-marker-stuck,2026-09-14 用户实证:文档面任务
|
|
991
|
+
* 反复「草稿 changedFiles 为空,跳过自动勾选」断链):显式 CLI 写入(writeTaskReview 通道,
|
|
992
|
+
* writtenBy 锚定,带 agent 供给 verdict)一律按非草稿处理——不受 changedFiles 空/实测 diff 空
|
|
993
|
+
* 限制(.sillyspec/ 文档面任务归因被滤、纯验证任务零 diff 是合法形态;agent 已用
|
|
994
|
+
* review write --force --changed-files 显式表态,verdict 即结论)。草稿守卫警告同步改为
|
|
995
|
+
* 可行动:含 task 名与解锁命令示例,不再只报现象。
|
|
996
|
+
*
|
|
997
|
+
* @param {{ok?:boolean, review?:{specVerdict?:string, qualityVerdict?:string, reviewerNotes?:string, changedFiles?:string[], task?:string, writtenBy?:string}}} r readReview 结果
|
|
979
998
|
* @param {boolean} endToEnd 是否端到端/deployment-critical task
|
|
980
999
|
* @param {{gitDir?:string, base?:string, head?:string}|null} [ctx] 实测 diff 上下文;缺省保持现行判定
|
|
1000
|
+
* @param {string} [changeName] 变更名(草稿守卫警告里解锁命令示例用)
|
|
981
1001
|
* @returns {boolean}
|
|
982
1002
|
*/
|
|
983
|
-
export function shouldAutoCheckTask(r, endToEnd, ctx = null) {
|
|
1003
|
+
export function shouldAutoCheckTask(r, endToEnd, ctx = null, changeName = '') {
|
|
984
1004
|
if (!r?.ok) return false
|
|
985
1005
|
const spec = r.review?.specVerdict
|
|
986
1006
|
const quality = r.review?.qualityVerdict
|
|
987
1007
|
if (spec === 'fail' || quality === 'fail') return false
|
|
988
1008
|
if (endToEnd) return spec === 'pass' && quality === 'pass'
|
|
989
1009
|
|
|
1010
|
+
// ql-20260915-001 修复②:显式 CLI 写入(review write 通道)按非草稿处理,直接勾——
|
|
1011
|
+
// verdict 非 fail 已过上面守卫,文档面/纯验证任务的空 changedFiles 不再拦截
|
|
1012
|
+
if (isExplicitReviewWrite(r.review)) return true
|
|
1013
|
+
|
|
990
1014
|
// W2 task-04 草稿零 diff 守卫(FR-03)
|
|
991
1015
|
// ctx 给定且 review 为自动草稿时,要求 changedFiles 非空且实测 diff 非空
|
|
992
1016
|
if (ctx && r.review?.reviewerNotes && r.review.reviewerNotes.includes('auto-generated draft')) {
|
|
1017
|
+
const taskId = r.review.task || 'task-NN'
|
|
1018
|
+
const unlockCmd = `sillyspec review write${changeName ? ` --change ${changeName}` : ' --change <变更名>'} --task ${taskId} --spec pass --quality pass --force --changed-files <文件>`
|
|
993
1019
|
const changedFiles = r.review.changedFiles || []
|
|
994
1020
|
if (changedFiles.length === 0) {
|
|
995
|
-
console.warn(`⚠️ 草稿 review changedFiles
|
|
1021
|
+
console.warn(`⚠️ 草稿 review changedFiles 为空,跳过自动勾选(task=${taskId})——解锁:${unlockCmd}(纯验证任务用 --changed-files "" + --evidence "<验证说明>")`)
|
|
996
1022
|
return false
|
|
997
1023
|
}
|
|
998
1024
|
if (!ctx.gitDir || !ctx.base || !ctx.head) {
|
|
999
|
-
console.warn(`⚠️ ctx 信息不完整(gitDir=${ctx.gitDir}, base=${ctx.base}, head=${ctx.head}),跳过草稿 diff
|
|
1025
|
+
console.warn(`⚠️ ctx 信息不完整(gitDir=${ctx.gitDir}, base=${ctx.base}, head=${ctx.head}),跳过草稿 diff 校验,保守不勾(task=${taskId})——解锁:${unlockCmd}`)
|
|
1000
1026
|
return false
|
|
1001
1027
|
}
|
|
1002
1028
|
// 预取路径:一次全量 diff 文件集内存归属(autoCheckPlanFromReviews/批量路径已 prefetch)
|
|
1003
1029
|
if (ctx.diffFileSet) {
|
|
1004
1030
|
const hit = changedFiles.filter(f => ctx.diffFileSet.has(f))
|
|
1005
1031
|
if (hit.length === 0) {
|
|
1006
|
-
console.warn(`⚠️ 草稿 review 实测 diff 为空(base=${ctx.base.slice(0, 8)}, head=${ctx.head.slice(0, 8)}, files=${changedFiles.length}
|
|
1032
|
+
console.warn(`⚠️ 草稿 review 实测 diff 为空(task=${taskId}, base=${ctx.base.slice(0, 8)}, head=${ctx.head.slice(0, 8)}, files=${changedFiles.length}),跳过自动勾选——解锁:${unlockCmd}`)
|
|
1007
1033
|
return false
|
|
1008
1034
|
}
|
|
1009
1035
|
console.log(` ✓ 草稿 diff 校验通过(实测 ${hit.length}/${changedFiles.length} 个声明文件有改动)`)
|
|
@@ -1014,12 +1040,12 @@ export function shouldAutoCheckTask(r, endToEnd, ctx = null) {
|
|
|
1014
1040
|
// 用数组参数避免 shell 拆词,复用 git-helper 安全模式
|
|
1015
1041
|
const diffResult = gitQuiet(ctx.gitDir, ['diff', '--name-only', `${ctx.base}..${ctx.head}`, '--', ...changedFiles], { trim: true })
|
|
1016
1042
|
if (!diffResult || diffResult.trim() === '') {
|
|
1017
|
-
console.warn(`⚠️ 草稿 review 实测 diff 为空(base=${ctx.base.slice(0,8)}, head=${ctx.head.slice(0,8)}, files=${changedFiles.length}
|
|
1043
|
+
console.warn(`⚠️ 草稿 review 实测 diff 为空(task=${taskId}, base=${ctx.base.slice(0,8)}, head=${ctx.head.slice(0,8)}, files=${changedFiles.length}),跳过自动勾选——解锁:${unlockCmd}`)
|
|
1018
1044
|
return false
|
|
1019
1045
|
}
|
|
1020
1046
|
console.log(` ✓ 草稿 diff 校验通过(${diffResult.trim().split('\n').length} 个文件有改动)`)
|
|
1021
1047
|
} catch (e) {
|
|
1022
|
-
console.warn(`⚠️ 草稿 diff
|
|
1048
|
+
console.warn(`⚠️ 草稿 diff 校验失败(task=${taskId}: ${e && e.message ? e.message : e}),保守不勾——解锁:${unlockCmd}`)
|
|
1023
1049
|
return false
|
|
1024
1050
|
}
|
|
1025
1051
|
}
|
|
@@ -1079,7 +1105,7 @@ export async function autoCheckPlanFromReviews({ stageName, changeName, cwd, pla
|
|
|
1079
1105
|
&& r.review?.qualityVerdict !== 'fail'
|
|
1080
1106
|
const endToEnd = verdictUsable
|
|
1081
1107
|
&& isEndToEndTaskText(match + ' ' + readTaskCardText(changeDir, taskNum))
|
|
1082
|
-
if (shouldAutoCheckTask(r, endToEnd, ctx)) {
|
|
1108
|
+
if (shouldAutoCheckTask(r, endToEnd, ctx, changeName)) {
|
|
1083
1109
|
checkedCount++
|
|
1084
1110
|
return `${p1}x${p2}` // 勾选
|
|
1085
1111
|
}
|
|
@@ -1175,7 +1201,10 @@ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBa
|
|
|
1175
1201
|
}
|
|
1176
1202
|
|
|
1177
1203
|
// review 存在:判定是否为自动草稿且零 diff
|
|
1178
|
-
|
|
1204
|
+
// (ql-20260915-001 修复②:显式 CLI 写入(writeTaskReview 通道)按非草稿处理——不阻断
|
|
1205
|
+
// 批量,与勾选层 shouldAutoCheckTask 同口径;agent 已显式给 verdict 的 review 不该因
|
|
1206
|
+
// notes 字面残留 'auto-generated draft' 被当草稿拦截)
|
|
1207
|
+
if (!isExplicitReviewWrite(review) && review?.reviewerNotes && review.reviewerNotes.includes('auto-generated draft')) {
|
|
1179
1208
|
const changedFiles = review.changedFiles || []
|
|
1180
1209
|
// 草稿且 changedFiles 为空 → 阻断
|
|
1181
1210
|
if (changedFiles.length === 0) {
|
|
@@ -1527,7 +1556,16 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
1527
1556
|
const { consumeFrictionHint } = await import('../friction-tally.js')
|
|
1528
1557
|
const r = await consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
1529
1558
|
if (r && r.hint) console.log(`\n${r.hint}`)
|
|
1530
|
-
|
|
1559
|
+
// 台账滚动(2026-09-15-tax-governance FR-02 主滚动点):consume 得到的非零 counts 按
|
|
1560
|
+
// change 滚动合并进 <runtimeRoot>/friction-ledger.json 幸存台账(merge-by-change——
|
|
1561
|
+
// --reopen 重跑二次 consume 累加非双计);空 counts 跳过写(防干净收尾落空文件);
|
|
1562
|
+
// archivedAt 此时不落(prune 侧落定);runtimeRoot 与 consume 同源(specBase 同变量
|
|
1563
|
+
// + resolveRuntimeRoot 单点)。台账写失败 fail-soft 在既有 try/catch 内,绝不阻断收尾。
|
|
1564
|
+
if (changeName && r && r.counts && Object.keys(r.counts).length > 0) {
|
|
1565
|
+
const { mergeFrictionEntry } = await import('../friction-ledger.js')
|
|
1566
|
+
await mergeFrictionEntry(resolveRuntimeRoot(platformOpts, specBase), { change: changeName, counts: r.counts })
|
|
1567
|
+
}
|
|
1568
|
+
} catch { /* 摩擦提示/台账失败不影响收尾 */ }
|
|
1531
1569
|
}
|
|
1532
1570
|
// 阶段完成后明确下一步(agent 常卡:stageData completed 但不知要 run <下一阶段> 推进 currentStage)
|
|
1533
1571
|
const nextStageHint = { brainstorm: 'plan', plan: 'execute', execute: 'verify', verify: 'archive' }[stageName]
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* 纯函数无副作用:不写盘、不 console(console 留给调用点 task-02/03)。
|
|
13
13
|
*/
|
|
14
14
|
import { safeGit, parsePorcelainPath, isQuickMetadata } from './shared.js'
|
|
15
|
+
import { basename } from 'node:path'
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* 内联解析 .sillyspec/changes/<dir>/ 路径取 <dir>。
|
|
@@ -154,3 +155,100 @@ export function formatConcurrentWarning(detected) {
|
|
|
154
155
|
lines.push('提交请用显式 pathspec 隔离本变更文件,勿 git add . 扫入他者工作(execute task 内建议 sillyspec wt-commit 串行提交)。')
|
|
155
156
|
return lines.join('\n')
|
|
156
157
|
}
|
|
158
|
+
|
|
159
|
+
// ── 坑 mixed-baseline-drift-hint(ql-20260915-004)committed-drift 混合基线检测 ──
|
|
160
|
+
// 背景(用户 2026-09-15 实证):并行会话合并断言不同步——worktree 内基于旧快照写的测试断言
|
|
161
|
+
// (旧 id),主仓已合入 id 统一修复 → verify 测试门红——测试源=worktree 快照、被测源=主仓
|
|
162
|
+
// HEAD 的混合基线漂移此前无任何提示。本函数算「主仓自本变更基点后的已提交推进 ∩ 本变更
|
|
163
|
+
// 相关文件」,交两挂点(worktree-apply 成功尾声 / gates.js verify 测试门前)advisory 提示。
|
|
164
|
+
|
|
165
|
+
/** 测试形态文件名:foo.test.mjs / foo.spec.js(与探针 7 isProbe7TestPath 的 .test. 口径同族) */
|
|
166
|
+
const DRIFT_TEST_FORM_RE = /\.(?:test|spec)\.[A-Za-z0-9]+$/
|
|
167
|
+
|
|
168
|
+
/** git 路径反斜杠归一(detectCommittedDrift 内部用,与 normalizeGitPath 同义独立小函数) */
|
|
169
|
+
function toPosix(p) {
|
|
170
|
+
return String(p || '').replace(/\\/g, '/')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 文件名 stem 键:剥扩展名与 .test./.spec. 中缀(foo.js → foo;foo.test.mjs → foo;
|
|
175
|
+
* foo_test.js → foo_test——下划线形不在变体口径内,仅精确匹配兜底)。小写归一。
|
|
176
|
+
*/
|
|
177
|
+
function driftStemKey(p) {
|
|
178
|
+
const name = basename(toPosix(p))
|
|
179
|
+
const m = name.match(/^(.+?)\.(?:test|spec)\.[A-Za-z0-9]+$/)
|
|
180
|
+
if (m) return m[1].toLowerCase()
|
|
181
|
+
const dot = name.lastIndexOf('.')
|
|
182
|
+
return (dot > 0 ? name.slice(0, dot) : name).toLowerCase()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 检测混合基线漂移(非阻断 advisory 用):主仓 `git diff --name-only <baseHash>..<head>`
|
|
187
|
+
* (基点后已提交推进面)∩ 本变更 touchedFiles(或其同 basename 的 .test./.spec. 变体)非空
|
|
188
|
+
* → drift。变体口径:stem 相同且至少一侧为测试形态(src/foo.js ↔ test/foo.test.mjs)——
|
|
189
|
+
* 换位覆盖两方向(他者推进我的源文件的测试变体 / 他者推进我测试文件对应的源文件)。
|
|
190
|
+
*
|
|
191
|
+
* fail-open:baseHash 缺失 / git 异常 → 返回 null(调用方视为未检测,零输出零阻断);
|
|
192
|
+
* diff 空(基点后无推进)→ { drift:false, files:[] }。
|
|
193
|
+
*
|
|
194
|
+
* @param {{ projectRoot: string, baseHash: string, touchedFiles: string[],
|
|
195
|
+
* excludeFiles?: string[], head?: string }} opts
|
|
196
|
+
* - baseHash:worktree 基点(meta.baselineCommit || baseHash)
|
|
197
|
+
* - touchedFiles:本变更交付文件面(apply-manifest files / changedFiles)
|
|
198
|
+
* - excludeFiles:从推进面剔除的文件(verify 挂点传本变更自身面——apply 已提交时
|
|
199
|
+
* baseHash..HEAD 含自身交付,不剔会把「自己合自己」误报成他者推进;apply 挂点用
|
|
200
|
+
* 精确 preMergeHead,无需剔除)
|
|
201
|
+
* - head:推进面终点 ref(缺省 HEAD;merge 路径传 merge 前 HEAD)
|
|
202
|
+
* @returns {{ drift: boolean, files: string[], hint?: string }|null}
|
|
203
|
+
*/
|
|
204
|
+
export function detectCommittedDrift({ projectRoot, baseHash, touchedFiles, excludeFiles = [], head = 'HEAD' }) {
|
|
205
|
+
if (!projectRoot || !baseHash) return null
|
|
206
|
+
const touched = (Array.isArray(touchedFiles) ? touchedFiles : []).map(toPosix).filter(Boolean)
|
|
207
|
+
if (touched.length === 0) return null
|
|
208
|
+
// FR fail-open(与 detectConcurrentChanges 同族):git 读不到不崩不误报
|
|
209
|
+
const diff = safeGit(projectRoot, ['diff', '--name-only', `${baseHash}..${head}`], { timeout: 30000 })
|
|
210
|
+
if (diff.error) return null
|
|
211
|
+
if (!diff.value) return { drift: false, files: [] }
|
|
212
|
+
|
|
213
|
+
const excl = new Set((Array.isArray(excludeFiles) ? excludeFiles : []).map(toPosix).filter(Boolean))
|
|
214
|
+
const touchedSet = new Set(touched)
|
|
215
|
+
const touchedStems = new Set(touched.map(driftStemKey).filter(Boolean))
|
|
216
|
+
const touchedHasTestForm = touched.some(f => DRIFT_TEST_FORM_RE.test(basename(f)))
|
|
217
|
+
|
|
218
|
+
const files = []
|
|
219
|
+
for (const raw of diff.value.split('\n').map(s => s.trim()).filter(Boolean)) {
|
|
220
|
+
const p = toPosix(raw)
|
|
221
|
+
if (excl.has(p)) continue
|
|
222
|
+
const exact = touchedSet.has(p)
|
|
223
|
+
// 变体:非精确命中时,stem 相同且至少一侧为测试形态(防 docs/foo.md ↔ test/foo.test.mjs
|
|
224
|
+
// 之外的松散同名误报——两侧都非测试形态的不同目录同名不算相关)
|
|
225
|
+
const variant = !exact
|
|
226
|
+
&& touchedStems.has(driftStemKey(p))
|
|
227
|
+
&& (DRIFT_TEST_FORM_RE.test(basename(p)) || touchedHasTestForm)
|
|
228
|
+
if (exact || variant) files.push(p)
|
|
229
|
+
}
|
|
230
|
+
if (files.length === 0) return { drift: false, files: [] }
|
|
231
|
+
return {
|
|
232
|
+
drift: true,
|
|
233
|
+
files,
|
|
234
|
+
hint: `主仓自基点 ${String(baseHash).slice(0, 8)} 后已合入触及 ${files.length} 个本变更相关文件的推进(${files.slice(0, 5).join('、')}${files.length > 5 ? ' 等' : ''})——worktree 快照内验证过的断言可能漂移,建议合并态复跑相关测试`,
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* committed-drift 检测结果 → 多行 ⚠️ 提示串(两挂点共用文案,与 formatConcurrentWarning 同族)。
|
|
240
|
+
* @param {{ drift: boolean, files?: string[], hint?: string }|null} drift detectCommittedDrift 产物
|
|
241
|
+
* @param {{ tailNote?: string }} [opts] tailNote 追加在首行末的挂点语境注(如 verify 挂点的
|
|
242
|
+
* 「实测若红先做合并态归因」);缺省无
|
|
243
|
+
* @returns {string|null} 无漂移/未检测返回 null
|
|
244
|
+
*/
|
|
245
|
+
export function formatCommittedDriftWarning(drift, opts = {}) {
|
|
246
|
+
if (!drift || !drift.drift) return null
|
|
247
|
+
const files = Array.isArray(drift.files) ? drift.files : []
|
|
248
|
+
const note = opts.tailNote ? `(${opts.tailNote})` : ''
|
|
249
|
+
return [
|
|
250
|
+
'⚠️ 混合基线提示:主仓自本变更基点后已合入触及以下本变更相关文件的推进'
|
|
251
|
+
+ `(${files.length} 个)——worktree 快照内验证过的断言可能漂移,建议合并态复跑相关测试${note}:`,
|
|
252
|
+
...files.map(f => ` - ${f}`),
|
|
253
|
+
].join('\n')
|
|
254
|
+
}
|
package/src/run/gates.js
CHANGED
|
@@ -27,7 +27,7 @@ import { writeAtomicSync } from '../fs-atomic.js'
|
|
|
27
27
|
import { triggerSync, resolveChangeDir, resolveRuntimeRoot } from './shared.js'
|
|
28
28
|
import { runValidators } from '../stage-contract.js'
|
|
29
29
|
import { handleScanStageCompleted, handleExecuteWorktreeCleanup } from './complete-handlers.js'
|
|
30
|
-
import { detectConcurrentChanges, formatConcurrentWarning } from './concurrent-detect.js'
|
|
30
|
+
import { detectConcurrentChanges, formatConcurrentWarning, detectCommittedDrift, formatCommittedDriftWarning } from './concurrent-detect.js'
|
|
31
31
|
import { stageRegistry } from '../stages/index.js'
|
|
32
32
|
import { normalizeTaskId } from '../taskcard.js'
|
|
33
33
|
import { recordFrictionEvent } from '../friction-tally.js'
|
|
@@ -643,6 +643,28 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
643
643
|
// 再由 CLI 亲自执行 local.yaml 的测试命令,与 verify-result.md 的自报告对账:
|
|
644
644
|
// 自报告 PASS 但实测失败 → 阻断(防止"文案通过"绕过验证)。
|
|
645
645
|
if (stageName === 'verify') {
|
|
646
|
+
// 坑 mixed-baseline-drift-hint(ql-20260915-004):verify 实测门执行前的混合基线 advisory
|
|
647
|
+
//(提前预警不阻断)——apply-manifest 的基点后主仓已提交推进若触及本变更相关文件(含
|
|
648
|
+
// .test. 变体),worktree 快照内验证过的断言可能漂移,本次实测红先做合并态归因。推进面
|
|
649
|
+
// 剔除本变更自身文件面(apply 已提交时 baseHash..HEAD 含自身交付,不剔会自污染误报);
|
|
650
|
+
// 剔除后精确命中信号由 apply 时点提示兜底,此处保变体方向(他者推进我源文件的测试变体 /
|
|
651
|
+
// 我测试文件对应的源文件)。无 manifest(in-place/quick)→ no-op 零行为变化。
|
|
652
|
+
try {
|
|
653
|
+
const driftManifestPath = join(specBase, 'changes', changeName, 'apply-manifest.json')
|
|
654
|
+
if (existsSync(driftManifestPath)) {
|
|
655
|
+
const driftManifest = JSON.parse(readFileSync(driftManifestPath, 'utf8'))
|
|
656
|
+
const driftFace = (driftManifest && Array.isArray(driftManifest.files)
|
|
657
|
+
? driftManifest.files.map(f => f && f.path).filter(Boolean) : [])
|
|
658
|
+
if (driftManifest && driftManifest.baseHash && driftFace.length > 0) {
|
|
659
|
+
const drift = detectCommittedDrift({
|
|
660
|
+
projectRoot: cwd, baseHash: driftManifest.baseHash,
|
|
661
|
+
touchedFiles: driftFace, excludeFiles: driftFace,
|
|
662
|
+
})
|
|
663
|
+
const driftMsg = formatCommittedDriftWarning(drift, { tailNote: '本次实测若红,先做合并态归因' })
|
|
664
|
+
if (driftMsg) console.warn(driftMsg)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
} catch { /* fail-soft:drift 检测异常不影响 verify 门 */ }
|
|
646
668
|
const { runVerifyTestCheck, printVerifyTestCheck } = await import('../verify-postcheck.js')
|
|
647
669
|
// P0-1 指纹复用(noai-ir-roadmap §3 前置一·防重复跑):noAI 质量扫描步已实测且代码指纹
|
|
648
670
|
// 未变 → 直接复用免重跑(长套件 2~10 分钟不再跑两遍);无记录/失配/git 不可用 → 照旧
|
|
@@ -1151,13 +1173,17 @@ export function readDesignScale(specBase, changeName) {
|
|
|
1151
1173
|
/**
|
|
1152
1174
|
* 校验变更目录下近 10 分钟新增的 .md/.yaml/.yml 文件含 author/created_at 元数据(advisory 打印)。
|
|
1153
1175
|
* 从 complete.js 迁入(completeStageGates 共享收尾管线消费)。
|
|
1176
|
+
* 按变更隔离(2026-09-14 quick,用户反馈③):带 changeName 时本变更目录的缺元数据文件逐个
|
|
1177
|
+
* 列出;其他变更目录的命中折叠为一行汇总并点名目录与计数——多 agent 并发仓里并行会话的他者
|
|
1178
|
+
* 文件不再逐条混入本变更收尾输出(同批后续变更包也走汇总行,agent 按目录名自认领)。
|
|
1179
|
+
* changeName 缺省(历史调用面)保持全量逐列。
|
|
1154
1180
|
*/
|
|
1155
|
-
export function validateMetadata(cwd, stageName, specBase) {
|
|
1181
|
+
export function validateMetadata(cwd, stageName, specBase, changeName) {
|
|
1156
1182
|
const changesDir = join(specBase, 'changes')
|
|
1157
1183
|
if (!existsSync(changesDir)) return
|
|
1158
1184
|
|
|
1159
1185
|
const cutoff = Date.now() - 10 * 60 * 1000
|
|
1160
|
-
const missing =
|
|
1186
|
+
const missing = new Set()
|
|
1161
1187
|
|
|
1162
1188
|
function walk(dir) {
|
|
1163
1189
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -1168,19 +1194,34 @@ export function validateMetadata(cwd, stageName, specBase) {
|
|
|
1168
1194
|
const mtime = statSync(full).mtimeMs
|
|
1169
1195
|
if (mtime < cutoff) continue
|
|
1170
1196
|
const content = readFileSync(full, 'utf-8')
|
|
1171
|
-
if (!content.includes('author:') && !content.includes('author:')) missing.
|
|
1172
|
-
if (!content.includes('created_at:') && !content.includes('created_at:')) missing.
|
|
1197
|
+
if (!content.includes('author:') && !content.includes('author:')) missing.add(full)
|
|
1198
|
+
if (!content.includes('created_at:') && !content.includes('created_at:')) missing.add(full)
|
|
1173
1199
|
} catch (e) { /* skip unreadable files */ }
|
|
1174
1200
|
}
|
|
1175
1201
|
}
|
|
1176
1202
|
|
|
1177
1203
|
walk(changesDir)
|
|
1178
|
-
|
|
1179
|
-
|
|
1204
|
+
if (missing.size === 0) return
|
|
1205
|
+
|
|
1206
|
+
// 归属分组:changes/ 下第一段目录名([\\/] 切分兼容 Windows join 产物的反斜杠)
|
|
1207
|
+
const current = []
|
|
1208
|
+
const otherCounts = new Map()
|
|
1209
|
+
for (const f of missing) {
|
|
1210
|
+
const top = relative(changesDir, f).split(/[\\/]/)[0]
|
|
1211
|
+
if (!changeName || top === changeName) current.push(f)
|
|
1212
|
+
else otherCounts.set(top, (otherCounts.get(top) || 0) + 1)
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (current.length > 0) {
|
|
1180
1216
|
console.log(`\n⚠️ 以下文件缺少 author 或 created_at 元数据:`)
|
|
1181
|
-
|
|
1217
|
+
current.forEach(f => console.log(` - ${relative(cwd, f) || f}`))
|
|
1182
1218
|
console.log('请在文件头部添加 author(git 用户名)和 created_at(精确到秒)')
|
|
1183
1219
|
}
|
|
1220
|
+
if (otherCounts.size > 0) {
|
|
1221
|
+
const parts = [...otherCounts.entries()].map(([dir, n]) => `${dir}×${n}`).join('、')
|
|
1222
|
+
const total = [...otherCounts.values()].reduce((a, b) => a + b, 0)
|
|
1223
|
+
console.log(`\nℹ️ 另有 ${total} 个其他变更目录的文件近 10 分钟新增且缺元数据(${parts})——已按变更隔离折叠:并行会话产物请忽略;若为本步骤创建的同批变更包,请补 author/created_at`)
|
|
1224
|
+
}
|
|
1184
1225
|
}
|
|
1185
1226
|
|
|
1186
1227
|
/**
|
|
@@ -1339,7 +1380,7 @@ export async function completeStageGates({ stageName, cwd, changeName, platformO
|
|
|
1339
1380
|
const settledCount = steps.filter(s => s.status === 'completed' || s.status === 'skipped').length
|
|
1340
1381
|
const total = steps.length
|
|
1341
1382
|
|
|
1342
|
-
validateMetadata(cwd, stageName, specBase)
|
|
1383
|
+
validateMetadata(cwd, stageName, specBase, changeName)
|
|
1343
1384
|
|
|
1344
1385
|
// 验证关键文件位置(仅当所有步骤已结案 completed‖skipped 时才校验)
|
|
1345
1386
|
if (settledCount === total && total > 0) {
|
package/src/run/prompt.js
CHANGED
|
@@ -29,6 +29,11 @@ import { nowWallClock } from '../datetime.js'
|
|
|
29
29
|
import { parseModuleMapSimple } from '../modules.js'
|
|
30
30
|
import { readModuleRecentChanges } from '../module-changelog.js'
|
|
31
31
|
import { REVIEW_SCHEMA_VERSION, isValidExecuteRunId } from '../task-review.js'
|
|
32
|
+
// 机械知识注入(2026-09-14-knowledge-loop-close task-04,FR-04):两者均为 fs/path 叶子模块,
|
|
33
|
+
// 静态 import 不引入环(stages/* 反向引用本文件会撞 index.js 顶层 stageRegistry TDZ——见
|
|
34
|
+
// buildKnowledgeInjection docstring)。
|
|
35
|
+
import { matchKnowledge } from '../knowledge-match.js'
|
|
36
|
+
import { appendKnowledgeHit } from '../knowledge-hits.js'
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
39
|
* 从 _module-map.yaml 读取模块上下文索引
|
|
@@ -220,6 +225,95 @@ export function buildQuickSemanticGuardInjection({ specBase, cwd, changeName } =
|
|
|
220
225
|
}
|
|
221
226
|
}
|
|
222
227
|
|
|
228
|
+
// ── 机械知识注入共享底座(2026-09-14-knowledge-loop-close task-04,FR-04 / D-002@v1 总体方案 D)──
|
|
229
|
+
// 知识消费从 report 级升级为正文级:matchKnowledge 命中 → 按 entries 出现序(INDEX 行序)取
|
|
230
|
+
// 前 3 个不同 file,单文件首 40 行截断 + 截断标记,渲染「📚 命中知识」段(R-02 膨胀控制);
|
|
231
|
+
// 未命中 section=''(调用方零字节零变化)。命中同时经 task-01 的 appendKnowledgeHit 落
|
|
232
|
+
// .runtime/knowledge-hits.jsonl 一条 type:inject 记录;既有 knowledge-hit-report.json 由
|
|
233
|
+
// execute 调用方照旧落盘(X-004 升级既有机制而非并行新建,新旧遥测共存)。
|
|
234
|
+
//
|
|
235
|
+
// ⚠️ 同款格式在 src/stages/execute.js buildWavePrompt 内有本地孪生实现(Wave 粒度注入)——
|
|
236
|
+
// 本文件无法被 execute.js 静态 import(prompt.js → stages/index.js → execute.js 既有依赖方向,
|
|
237
|
+
// 反向边会在 execute.js 作模块图入口时触发 index.js 顶层 stageRegistry 对其 definition const 的
|
|
238
|
+
// TDZ 求值,2026-09-14 ESM 环实证)。格式漂移由 test/knowledge-inject.test.mjs 的格式等价断言
|
|
239
|
+
// 锁定——改此处必同步改孪生。
|
|
240
|
+
export const KNOWLEDGE_INJECT_MAX_FILES = 3
|
|
241
|
+
export const KNOWLEDGE_INJECT_MAX_LINES = 40
|
|
242
|
+
|
|
243
|
+
/** top-N 选取:按 entries 出现序(INDEX 行序)取前 N 个不同 file(matchKnowledge 布尔 filter
|
|
244
|
+
* 无相关度排序,X-009——出现序即唯一稳定序),每 file 取首个命中 entry 作代表。 */
|
|
245
|
+
function pickTopKnowledgeEntries(entries, maxFiles) {
|
|
246
|
+
const seen = new Set()
|
|
247
|
+
const picked = []
|
|
248
|
+
for (const e of entries) {
|
|
249
|
+
if (seen.has(e.file)) continue
|
|
250
|
+
seen.add(e.file)
|
|
251
|
+
picked.push(e)
|
|
252
|
+
if (picked.length >= maxFiles) break
|
|
253
|
+
}
|
|
254
|
+
return picked
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** 命中正文段渲染(段头 + Status/Sources 命中报告语义 + top-3 文件截断正文)。 */
|
|
258
|
+
function renderKnowledgeInjectSection(knowledgeResult, { knowledgeDir, maxFiles, maxLines }) {
|
|
259
|
+
const ref = e => (e.anchor ? `${e.file}#${e.anchor}` : e.file)
|
|
260
|
+
const lines = []
|
|
261
|
+
lines.push(`📚 命中知识(CLI 按任务描述机械匹配,top-${maxFiles})`)
|
|
262
|
+
lines.push(`Status: matched | Entries: ${knowledgeResult.entries.length} | Sources:`)
|
|
263
|
+
for (const e of knowledgeResult.entries) lines.push(` - ${ref(e)}`)
|
|
264
|
+
lines.push(`(命中清单如上;正文按 INDEX 行序注入前 ${maxFiles} 个不同文件,单文件首 ${maxLines} 行截断——未注入条目需要时按 Sources 路径自行读取)`)
|
|
265
|
+
for (const e of pickTopKnowledgeEntries(knowledgeResult.entries, maxFiles)) {
|
|
266
|
+
lines.push('')
|
|
267
|
+
lines.push(`── ${ref(e)}${e.display ? `(${e.display})` : ''} ──`)
|
|
268
|
+
let body = ''
|
|
269
|
+
try {
|
|
270
|
+
// CRLF/CR 归一(Windows 知识文件 + 行数截断按 \n 切分)
|
|
271
|
+
body = readFileSync(join(knowledgeDir, e.file), 'utf8').replace(/\r\n?/g, '\n')
|
|
272
|
+
} catch (err) {
|
|
273
|
+
lines.push(`(文件不可读:${err && err.message ? err.message : err})`)
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
const bodyLines = body.split('\n')
|
|
277
|
+
if (bodyLines.length > maxLines) {
|
|
278
|
+
lines.push(bodyLines.slice(0, maxLines).join('\n'))
|
|
279
|
+
lines.push('…(截断)')
|
|
280
|
+
} else {
|
|
281
|
+
lines.push(body.replace(/\n+$/, ''))
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return lines.join('\n')
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 机械知识注入一站式入口(execute「确认执行范围」step 与 quick step1 两个 prompt.js 内注入点共用)。
|
|
289
|
+
*
|
|
290
|
+
* @param {object} opts
|
|
291
|
+
* @param {string} opts.knowledgeDir - knowledge 目录(INDEX.md 所在)
|
|
292
|
+
* @param {string} opts.runtimeDir - .runtime 目录(hits.jsonl 落点)
|
|
293
|
+
* @param {string} opts.change - 变更名 / quick sessionId(进遥测记录 change 字段)
|
|
294
|
+
* @param {string} opts.query - 匹配查询串(execute: changeName+tasks.md 任务行;quick: guard.taskDescription)
|
|
295
|
+
* @returns {{matched: boolean, section: string, report: string, json: object}}
|
|
296
|
+
* section:命中时的「📚 命中知识」段(含段头与正文);未命中恒 ''(调用方零字节)。
|
|
297
|
+
* report/json:matchKnowledge 原样透传(execute 调用方沿用旧 report.json 落盘与未命中替换口径)。
|
|
298
|
+
* 遥测 append 单独 fail-soft(hits 写失败不影响注入本体);匹配/渲染异常向上抛由调用方降级。
|
|
299
|
+
*/
|
|
300
|
+
export function buildKnowledgeInjection({ knowledgeDir, runtimeDir, change, query, maxFiles = KNOWLEDGE_INJECT_MAX_FILES, maxLines = KNOWLEDGE_INJECT_MAX_LINES } = {}) {
|
|
301
|
+
const knowledgeResult = matchKnowledge(knowledgeDir, String(query || ''))
|
|
302
|
+
if (!knowledgeResult.matched) {
|
|
303
|
+
return { matched: false, section: '', report: knowledgeResult.report, json: knowledgeResult.json }
|
|
304
|
+
}
|
|
305
|
+
const section = renderKnowledgeInjectSection(knowledgeResult, { knowledgeDir, maxFiles, maxLines })
|
|
306
|
+
try {
|
|
307
|
+
appendKnowledgeHit(runtimeDir, {
|
|
308
|
+
type: 'inject',
|
|
309
|
+
change: change || '',
|
|
310
|
+
query: String(query || ''),
|
|
311
|
+
matchedFiles: knowledgeResult.entries.map(e => (e.anchor ? `${e.file}#${e.anchor}` : e.file)),
|
|
312
|
+
})
|
|
313
|
+
} catch { /* 遥测 fail-soft(R-04):hits 落盘失败不阻断注入本体 */ }
|
|
314
|
+
return { matched: true, section, report: knowledgeResult.report, json: knowledgeResult.json }
|
|
315
|
+
}
|
|
316
|
+
|
|
223
317
|
// parseModuleMapSimple 复用 modules.js 的 canonical 实现(合并历史 copy-paste 副本,2026-08-07;
|
|
224
318
|
// 无循环依赖:modules.js 仅 import fs/path/db.js,prompt.js → modules.js 单向)。
|
|
225
319
|
/**
|
|
@@ -754,9 +848,13 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
754
848
|
}
|
|
755
849
|
|
|
756
850
|
// Knowledge hit report: execute 阶段注入匹配结果
|
|
851
|
+
// 2026-09-14-knowledge-loop-close task-04(FR-04,X-004 升级既有机制):命中清单报告 → 命中
|
|
852
|
+
// 正文注入——命中时占位符替换为「📚 命中知识」段(top-3 不同 file + 首 40 行截断,段头保留
|
|
853
|
+
// Status/Entries/Sources 命中报告语义),并逐条 appendKnowledgeHit 落 hits.jsonl 遥测;
|
|
854
|
+
// 未命中替换值与升级前字节一致(knowledgeResult.report == 'Status: no matches',零膨胀)。
|
|
855
|
+
// 既有 knowledge-hit-report.json 照旧落盘(未命中也写 matched:false 快照,行为不变)。
|
|
757
856
|
if (stageName === 'execute' && promptText.includes('{KNOWLEDGE_HIT_REPORT}')) {
|
|
758
857
|
try {
|
|
759
|
-
const { matchKnowledge } = await import('../knowledge-match.js')
|
|
760
858
|
const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
761
859
|
const knowledgeDir = join(effectiveSpecBase, 'knowledge')
|
|
762
860
|
// taskContext: changeName + tasks.md 任务名(注册表唯一真相,2026-08-20-task-truth-unify)
|
|
@@ -774,12 +872,20 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
774
872
|
}
|
|
775
873
|
} catch {}
|
|
776
874
|
}
|
|
777
|
-
const knowledgeResult = matchKnowledge(knowledgeDir, taskContext)
|
|
778
|
-
promptText = promptText.replace(/\{KNOWLEDGE_HIT_REPORT\}/g, knowledgeResult.report)
|
|
779
|
-
// 写入 runtime JSON
|
|
780
875
|
const runtimeDir = join(effectiveSpecBase, '.runtime')
|
|
876
|
+
const knowledgeInjection = buildKnowledgeInjection({
|
|
877
|
+
knowledgeDir,
|
|
878
|
+
runtimeDir,
|
|
879
|
+
change: changeName || '',
|
|
880
|
+
query: taskContext,
|
|
881
|
+
})
|
|
882
|
+
promptText = promptText.replace(
|
|
883
|
+
/\{KNOWLEDGE_HIT_REPORT\}/g,
|
|
884
|
+
knowledgeInjection.matched ? knowledgeInjection.section : knowledgeInjection.report
|
|
885
|
+
)
|
|
886
|
+
// 写入 runtime JSON(既有机制照旧,新旧遥测共存)
|
|
781
887
|
mkdirSync(runtimeDir, { recursive: true })
|
|
782
|
-
writeAtomicSync(join(runtimeDir, 'knowledge-hit-report.json'), JSON.stringify(
|
|
888
|
+
writeAtomicSync(join(runtimeDir, 'knowledge-hit-report.json'), JSON.stringify(knowledgeInjection.json, null, 2) + '\n')
|
|
783
889
|
} catch (e) {
|
|
784
890
|
promptText = promptText.replace(/\{KNOWLEDGE_HIT_REPORT\}/g, 'Status: no matches (error: ' + e.message + ')')
|
|
785
891
|
}
|
|
@@ -1108,6 +1214,29 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1108
1214
|
}
|
|
1109
1215
|
}
|
|
1110
1216
|
|
|
1217
|
+
// ── 机械知识注入(quick step1,2026-09-14-knowledge-loop-close task-04,FR-04)──
|
|
1218
|
+
// 判定口径与上方模块上下文注入同锚(quickFirstStep):查询串用 guard.taskDescription 现成
|
|
1219
|
+
// 读取(X-008——changeName 是 quick-<hash> 无语义);命中追加「📚 命中知识」段到 prompt 末尾
|
|
1220
|
+
// (top-3 同规则),并 appendKnowledgeHit 落 hits.jsonl;未命中零字节(prompt 与现状字节一致,
|
|
1221
|
+
// 零命中静默)。全链 fail-soft:注入失败不阻断 quick 启动。
|
|
1222
|
+
if (quickFirstStep) {
|
|
1223
|
+
try {
|
|
1224
|
+
const kiSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1225
|
+
const kiQuery = String(readQuickGuardField(changeName, kiSpecBase, 'taskDescription') || '')
|
|
1226
|
+
if (kiQuery.trim() !== '') {
|
|
1227
|
+
const ki = buildKnowledgeInjection({
|
|
1228
|
+
knowledgeDir: join(kiSpecBase, 'knowledge'),
|
|
1229
|
+
runtimeDir: join(kiSpecBase, '.runtime'),
|
|
1230
|
+
change: changeName || '',
|
|
1231
|
+
query: kiQuery,
|
|
1232
|
+
})
|
|
1233
|
+
if (ki.section) {
|
|
1234
|
+
promptText = promptText + '\n\n' + ki.section
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
} catch { /* fail-soft:知识注入异常不阻断 quick step1 prompt 输出 */ }
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1111
1240
|
// ── 语义护栏进场注入(task-05,FR-03,D-001@v1 模块四)──
|
|
1112
1241
|
// 判定口径独立:与上方模块上下文注入同锚(quickFirstStep——quick step1「理解任务」),不嵌进
|
|
1113
1242
|
// {QUICK_CONTEXT_DIGEST} 占位符守卫内——模板改版去占位符时护栏静默失效(plan 审查 N2)。
|