sillyspec 3.28.7 → 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-quick/SKILL.md +2 -0
- package/.husky/pre-push +3 -1
- 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/agent-session-log.js +446 -35
- package/src/change-risk-profile.js +20 -0
- package/src/config-cat.js +6 -3
- package/src/config-schema.js +32 -0
- package/src/constants.js +3 -0
- package/src/db.js +23 -6
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +33 -3
- package/src/docs-check.js +137 -12
- package/src/docs-gate.js +108 -6
- package/src/doctor-diagnostics.js +243 -9
- package/src/friction-ledger.js +171 -0
- package/src/friction-tally.js +7 -2
- package/src/git-helper.js +6 -4
- package/src/hooks/worktree-guard.js +72 -30
- package/src/index.js +356 -81
- package/src/knowledge-classify.js +363 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/local-detect.js +30 -15
- package/src/local-register.js +13 -4
- package/src/mcp-server.js +38 -8
- package/src/progress/change-registry.js +224 -43
- package/src/progress/consistency-doctor.js +11 -4
- package/src/progress/shared.js +3 -2
- package/src/progress/stage-machine.js +3 -1
- package/src/progress/step-store.js +30 -17
- package/src/progress.js +123 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +199 -32
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +124 -15
- package/src/run/complete-handlers.js +514 -24
- package/src/run/complete.js +82 -25
- package/src/run/concurrent-detect.js +99 -1
- package/src/run/gate-snapshot.js +115 -5
- package/src/run/gates.js +85 -14
- package/src/run/prompt.js +143 -6
- package/src/run/quick-audit.js +136 -15
- package/src/run/shared.js +174 -20
- package/src/run/stage.js +6 -0
- package/src/run/verify-quality-scan.js +14 -3
- 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/spec-sync.js +19 -14
- package/src/stage-contract-spec.js +4 -3
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +29 -12
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/execute.js +91 -4
- package/src/stages/knowledge.js +52 -3
- package/src/stages/plan-postcheck.js +57 -12
- 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 +201 -11
- package/src/task-review.js +333 -27
- package/src/verify-facts-schema.js +10 -3
- package/src/verify-postcheck.js +41 -6
- package/src/verify-probes.js +348 -7
- package/src/worktree-apply.js +594 -62
- package/src/worktree-cross.js +12 -18
- package/src/worktree-deps.js +100 -2
- package/src/worktree.js +255 -30
- package/src/wt-commit.js +128 -0
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/sa-rename-probe/src/new.js +0 -4
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'
|
|
@@ -485,8 +485,14 @@ export async function enforceReviewJsonGate(stageName, cwd, changeName, step, st
|
|
|
485
485
|
* 若校验失败不回滚,DB 会与真实产物不一致(hook/doctor/下游阶段全部误判),
|
|
486
486
|
* 且所有步骤都是 completed 时 agent 无法重新 --done("没有待完成的步骤")。
|
|
487
487
|
* 此处将 stage 回滚为 in-progress,最后一步重置为 pending,供修复产物后重做。
|
|
488
|
+
*
|
|
489
|
+
* 批量乐观标记一并回滚(坑 execute-batch-rollback-half-state,2026-08-29 用户实证):execute
|
|
490
|
+
* 批量完成(detectExecuteBatchFinish)把剩余步骤批量标 completed 后任一收尾 gate 失败,原先只
|
|
491
|
+
* 回滚 currentIdx → 落库「步骤 11 pending + 12-16 completed」半套状态,步骤序倒挂、要再补一次
|
|
492
|
+
* --done 才对齐。批量标记打 _batchAligned 戳(内存字段不落库),gate 失败 = 批量完成整体不成立,
|
|
493
|
+
* 戳上的步骤随当前步一起回滚 pending,下次 --done 满足条件再重新批量标记。
|
|
488
494
|
*/
|
|
489
|
-
function rollbackStageCompletion(stageData, steps, currentIdx) {
|
|
495
|
+
export function rollbackStageCompletion(stageData, steps, currentIdx) {
|
|
490
496
|
// 辅助阶段在 validator 前已被重置为 pending(steps 也换成了新数组),不要覆盖
|
|
491
497
|
if (stageData.status === 'completed') {
|
|
492
498
|
stageData.status = 'in-progress'
|
|
@@ -496,6 +502,14 @@ function rollbackStageCompletion(stageData, steps, currentIdx) {
|
|
|
496
502
|
steps[currentIdx].status = 'pending'
|
|
497
503
|
steps[currentIdx].completedAt = null
|
|
498
504
|
}
|
|
505
|
+
for (const s of steps) {
|
|
506
|
+
if (!s) continue
|
|
507
|
+
if (s._batchAligned && s.status === 'completed') {
|
|
508
|
+
s.status = 'pending'
|
|
509
|
+
s.completedAt = null
|
|
510
|
+
}
|
|
511
|
+
s._batchAligned = false
|
|
512
|
+
}
|
|
499
513
|
}
|
|
500
514
|
|
|
501
515
|
/**
|
|
@@ -511,7 +525,7 @@ function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx,
|
|
|
511
525
|
// friction 尾参(friction-signal-hint task-02):gate 失败摩擦埋点标签 {type, detail}——各调用点
|
|
512
526
|
// 按所属 gate 段传来源标签(审查两段走 review_rejected),未传兜底通用 gate-cascade。
|
|
513
527
|
// recordFrictionEvent 自带静默降级(非法类型/写失败返回 null),不影响回滚/返回语义。
|
|
514
|
-
recordFrictionEvent({ cwd, changeName, platformOpts, ...(friction || { type: 'gate_rollback', detail: 'gate-cascade' }) })
|
|
528
|
+
void recordFrictionEvent({ cwd, changeName, platformOpts, ...(friction || { type: 'gate_rollback', detail: 'gate-cascade' }) })
|
|
515
529
|
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
516
530
|
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
517
531
|
pm._write(cwd, progress, changeName)
|
|
@@ -629,6 +643,28 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
629
643
|
// 再由 CLI 亲自执行 local.yaml 的测试命令,与 verify-result.md 的自报告对账:
|
|
630
644
|
// 自报告 PASS 但实测失败 → 阻断(防止"文案通过"绕过验证)。
|
|
631
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 门 */ }
|
|
632
668
|
const { runVerifyTestCheck, printVerifyTestCheck } = await import('../verify-postcheck.js')
|
|
633
669
|
// P0-1 指纹复用(noai-ir-roadmap §3 前置一·防重复跑):noAI 质量扫描步已实测且代码指纹
|
|
634
670
|
// 未变 → 直接复用免重跑(长套件 2~10 分钟不再跑两遍);无记录/失配/git 不可用 → 照旧
|
|
@@ -646,7 +682,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
646
682
|
const { createVerifyGateSnapshot } = await import('./gate-snapshot.js')
|
|
647
683
|
verifyGateSnap = await createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts })
|
|
648
684
|
if (verifyGateSnap) {
|
|
649
|
-
console.log(`🧪 Verify
|
|
685
|
+
console.log(`🧪 Verify 实测隔离快照(根:${verifyGateSnap.snapshotRoot}):HEAD + 本变更 ${verifyGateSnap.changeFileCount} 个文件${verifyGateSnap.sourceRoot ? '(overlay 自 worktree——本变更分支内容定向跑)' : ''}(并行会话脏文件不参与门判定)`)
|
|
650
686
|
}
|
|
651
687
|
} catch { /* 快照链路异常 → 主仓现行为 */ }
|
|
652
688
|
}
|
|
@@ -679,7 +715,11 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
679
715
|
}
|
|
680
716
|
} catch { /* fail-soft:回填失败不影响门禁 */ }
|
|
681
717
|
if (testCheck.status === 'failed') {
|
|
682
|
-
if (verifyGateSnap) {
|
|
718
|
+
if (verifyGateSnap) {
|
|
719
|
+
try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
|
|
720
|
+
printSnapshotFailureHint(verifyGateSnap) } catch { /* 提示失败不影响阻断 */ }
|
|
721
|
+
try { verifyGateSnap.cleanup() } catch {} ; verifyGateSnap = null
|
|
722
|
+
}
|
|
683
723
|
console.error('\n❌ verify 阶段被阻断:verify-result.md 自报告通过,但 CLI 实测测试失败。')
|
|
684
724
|
// 并行 WIP 归因鉴别(坑 verify-reconcile-foreign-wip):单一实现迁至 verify-quality-scan.js
|
|
685
725
|
// renderVerifyTestAttribution(P0-1 前置二:noAI 质量扫描失败路径与本 --done 路径共用,
|
|
@@ -700,13 +740,21 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
700
740
|
if (lintCheck) {
|
|
701
741
|
console.log(`\n♻️ Verify lint 对账:复用 noAI 质量扫描步的实测结果(代码指纹匹配,免重跑)`)
|
|
702
742
|
} else {
|
|
703
|
-
lintCheck = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase })
|
|
743
|
+
lintCheck = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase, timeoutMs: verifyGateSnap ? 5 * 60 * 1000 : undefined })
|
|
744
|
+
// 快照 lint 超时回退主仓(2026-09-12 dogfood 两连实证:junction I/O 病态慢,3min/5min 均被
|
|
745
|
+
// 超时杀——主仓 60~110s 正常。按用户建议「自动回退」而非假败/advisory:主仓复跑保硬门,
|
|
746
|
+
// 并行噪声混入时失败输出带归属鉴定提示)
|
|
747
|
+
if (verifyGateSnap && lintCheck.status === 'failed' && /超时/.test(String(lintCheck.reason || ''))) {
|
|
748
|
+
console.warn('⚠️ 快照 lint 超时(node_modules junction I/O 慢)→ 主仓复跑 lint(并行噪声可能混入——失败先做归属鉴定)')
|
|
749
|
+
lintCheck = runVerifyLintCheck({ cwd, specBase })
|
|
750
|
+
}
|
|
704
751
|
if (lintCheck.status !== 'skipped') {
|
|
705
752
|
console.log(`\n⏳ Verify lint 对账:CLI 亲自执行 local.yaml 的 commands.lint…`)
|
|
706
753
|
}
|
|
707
754
|
}
|
|
708
755
|
// 快照收尾(test+lint 两检查都已完成;parity/删除探针走主仓 git 事实不依赖快照)
|
|
709
756
|
const usedSnapshot = Boolean(verifyGateSnap)
|
|
757
|
+
const usedSnapInfo = verifyGateSnap ? { snapshotRoot: verifyGateSnap.snapshotRoot, changeFileCount: verifyGateSnap.changeFileCount, sourceRoot: verifyGateSnap.sourceRoot } : null
|
|
710
758
|
if (verifyGateSnap) { try { verifyGateSnap.cleanup() } catch {} ; verifyGateSnap = null }
|
|
711
759
|
printVerifyLintCheck(lintCheck)
|
|
712
760
|
// lint 硬门(2026-09-09 升硬:观察期 14 次 5 败全真阳性,pre-push 本就硬拦——fail-fast 前移;
|
|
@@ -714,6 +762,10 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
714
762
|
const { shouldBlockVerifyLint } = await import('../verify-postcheck.js')
|
|
715
763
|
if (shouldBlockVerifyLint(lintCheck)) {
|
|
716
764
|
console.error('\n❌ verify 阶段被阻断:CLI 亲自实测 lint 失败(agent 的 lint 自报告与实测不符时以实测为准)。')
|
|
765
|
+
if (usedSnapInfo) {
|
|
766
|
+
try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
|
|
767
|
+
printSnapshotFailureHint(usedSnapInfo) } catch { /* 提示失败不影响阻断 */ }
|
|
768
|
+
}
|
|
717
769
|
if (lintCheck.outputTail) {
|
|
718
770
|
const tail = lintCheck.outputTail.split('\n').slice(-8).join('\n')
|
|
719
771
|
console.error(' 输出(末尾):')
|
|
@@ -1121,13 +1173,17 @@ export function readDesignScale(specBase, changeName) {
|
|
|
1121
1173
|
/**
|
|
1122
1174
|
* 校验变更目录下近 10 分钟新增的 .md/.yaml/.yml 文件含 author/created_at 元数据(advisory 打印)。
|
|
1123
1175
|
* 从 complete.js 迁入(completeStageGates 共享收尾管线消费)。
|
|
1176
|
+
* 按变更隔离(2026-09-14 quick,用户反馈③):带 changeName 时本变更目录的缺元数据文件逐个
|
|
1177
|
+
* 列出;其他变更目录的命中折叠为一行汇总并点名目录与计数——多 agent 并发仓里并行会话的他者
|
|
1178
|
+
* 文件不再逐条混入本变更收尾输出(同批后续变更包也走汇总行,agent 按目录名自认领)。
|
|
1179
|
+
* changeName 缺省(历史调用面)保持全量逐列。
|
|
1124
1180
|
*/
|
|
1125
|
-
export function validateMetadata(cwd, stageName, specBase) {
|
|
1181
|
+
export function validateMetadata(cwd, stageName, specBase, changeName) {
|
|
1126
1182
|
const changesDir = join(specBase, 'changes')
|
|
1127
1183
|
if (!existsSync(changesDir)) return
|
|
1128
1184
|
|
|
1129
1185
|
const cutoff = Date.now() - 10 * 60 * 1000
|
|
1130
|
-
const missing =
|
|
1186
|
+
const missing = new Set()
|
|
1131
1187
|
|
|
1132
1188
|
function walk(dir) {
|
|
1133
1189
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -1138,19 +1194,34 @@ export function validateMetadata(cwd, stageName, specBase) {
|
|
|
1138
1194
|
const mtime = statSync(full).mtimeMs
|
|
1139
1195
|
if (mtime < cutoff) continue
|
|
1140
1196
|
const content = readFileSync(full, 'utf-8')
|
|
1141
|
-
if (!content.includes('author:') && !content.includes('author:')) missing.
|
|
1142
|
-
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)
|
|
1143
1199
|
} catch (e) { /* skip unreadable files */ }
|
|
1144
1200
|
}
|
|
1145
1201
|
}
|
|
1146
1202
|
|
|
1147
1203
|
walk(changesDir)
|
|
1148
|
-
|
|
1149
|
-
|
|
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) {
|
|
1150
1216
|
console.log(`\n⚠️ 以下文件缺少 author 或 created_at 元数据:`)
|
|
1151
|
-
|
|
1217
|
+
current.forEach(f => console.log(` - ${relative(cwd, f) || f}`))
|
|
1152
1218
|
console.log('请在文件头部添加 author(git 用户名)和 created_at(精确到秒)')
|
|
1153
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
|
+
}
|
|
1154
1225
|
}
|
|
1155
1226
|
|
|
1156
1227
|
/**
|
|
@@ -1309,7 +1380,7 @@ export async function completeStageGates({ stageName, cwd, changeName, platformO
|
|
|
1309
1380
|
const settledCount = steps.filter(s => s.status === 'completed' || s.status === 'skipped').length
|
|
1310
1381
|
const total = steps.length
|
|
1311
1382
|
|
|
1312
|
-
validateMetadata(cwd, stageName, specBase)
|
|
1383
|
+
validateMetadata(cwd, stageName, specBase, changeName)
|
|
1313
1384
|
|
|
1314
1385
|
// 验证关键文件位置(仅当所有步骤已结案 completed‖skipped 时才校验)
|
|
1315
1386
|
if (settledCount === total && total > 0) {
|
package/src/run/prompt.js
CHANGED
|
@@ -22,13 +22,18 @@ import { basename, join } from 'node:path'
|
|
|
22
22
|
import { existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs'
|
|
23
23
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
24
24
|
import { stageRegistry } from '../stages/index.js'
|
|
25
|
-
import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, parsePorcelainPath, WAIT_MARKER_RE, QUICK_SID_RE } from './shared.js'
|
|
25
|
+
import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, parsePorcelainPath, WAIT_MARKER_RE, QUICK_SID_RE, triggerStepStartSync } from './shared.js'
|
|
26
26
|
import { renderSemanticGuardBlock, readSemanticGuardEnabled } from '../semantic-guard.js'
|
|
27
27
|
import { renderStageContract } from '../stage-contract-spec.js'
|
|
28
28
|
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
|
/**
|
|
@@ -309,6 +403,14 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
309
403
|
return false
|
|
310
404
|
}
|
|
311
405
|
|
|
406
|
+
// X3 渲染侧接线(2026-08-29-change-delete-closure-and-spec-pull 遗留活跃坑收口;曾因并行
|
|
407
|
+
// 会话提交丢弃一次,2026-09-12 复检重接):步骤 prompt 渲染即步骤起点——补推一次 progress
|
|
408
|
+
// 刷新平台 last_pushed_at(停滞判定不再把长步骤误报为「停滞」)。不 await:prompt 渲染不被
|
|
409
|
+
// 网络阻塞;未连接平台静默 / quick 会话降级跳过 / 8s 熔断 / 失败只 warn,契约全在
|
|
410
|
+
// triggerStepStartSync 内(shared.js docstring 即本接线点的说明),Node 事件循环保证在飞
|
|
411
|
+
// POST 落地后进程才退出。
|
|
412
|
+
triggerStepStartSync(cwd, changeName, platformOpts)
|
|
413
|
+
|
|
312
414
|
// ── 首个 agent 可见步索引(「首步一次建立」类注入的锚点)──
|
|
313
415
|
// step0 为 noAI(brainstorm/execute/verify 的 step1「进度确认」,2026-09-07)时不渲染 prompt,
|
|
314
416
|
// persona/护栏/完成契约/铁律/平台路径约束等 step0 专属注入须顺延到首个渲染步,否则整段丢失
|
|
@@ -746,9 +848,13 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
746
848
|
}
|
|
747
849
|
|
|
748
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 快照,行为不变)。
|
|
749
856
|
if (stageName === 'execute' && promptText.includes('{KNOWLEDGE_HIT_REPORT}')) {
|
|
750
857
|
try {
|
|
751
|
-
const { matchKnowledge } = await import('../knowledge-match.js')
|
|
752
858
|
const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
753
859
|
const knowledgeDir = join(effectiveSpecBase, 'knowledge')
|
|
754
860
|
// taskContext: changeName + tasks.md 任务名(注册表唯一真相,2026-08-20-task-truth-unify)
|
|
@@ -766,12 +872,20 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
766
872
|
}
|
|
767
873
|
} catch {}
|
|
768
874
|
}
|
|
769
|
-
const knowledgeResult = matchKnowledge(knowledgeDir, taskContext)
|
|
770
|
-
promptText = promptText.replace(/\{KNOWLEDGE_HIT_REPORT\}/g, knowledgeResult.report)
|
|
771
|
-
// 写入 runtime JSON
|
|
772
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(既有机制照旧,新旧遥测共存)
|
|
773
887
|
mkdirSync(runtimeDir, { recursive: true })
|
|
774
|
-
writeAtomicSync(join(runtimeDir, 'knowledge-hit-report.json'), JSON.stringify(
|
|
888
|
+
writeAtomicSync(join(runtimeDir, 'knowledge-hit-report.json'), JSON.stringify(knowledgeInjection.json, null, 2) + '\n')
|
|
775
889
|
} catch (e) {
|
|
776
890
|
promptText = promptText.replace(/\{KNOWLEDGE_HIT_REPORT\}/g, 'Status: no matches (error: ' + e.message + ')')
|
|
777
891
|
}
|
|
@@ -1100,6 +1214,29 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1100
1214
|
}
|
|
1101
1215
|
}
|
|
1102
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
|
+
|
|
1103
1240
|
// ── 语义护栏进场注入(task-05,FR-03,D-001@v1 模块四)──
|
|
1104
1241
|
// 判定口径独立:与上方模块上下文注入同锚(quickFirstStep——quick step1「理解任务」),不嵌进
|
|
1105
1242
|
// {QUICK_CONTEXT_DIGEST} 占位符守卫内——模板改版去占位符时护栏静默失效(plan 审查 N2)。
|
package/src/run/quick-audit.js
CHANGED
|
@@ -137,6 +137,17 @@ export function printQuickAuditReview(review) {
|
|
|
137
137
|
}
|
|
138
138
|
console.warn(` quick 不强制文档同步,欠账已随本条 QUICKLOG「审计:」行落盘(可事后追溯)。若改动触及接口/契约,建议顺手同步模块文档。`)
|
|
139
139
|
}
|
|
140
|
+
// spec 共享面他者噪声(坑 foreign-spec-churn-fail-closed):并行会话归档移动/文档收尾的
|
|
141
|
+
// 删除与 EOL-only 假 M——软警告点名归因,不再逼 --allow-delete/--allow-new 解锁。
|
|
142
|
+
if (review.foreignSpecChurn && review.foreignSpecChurn.length > 0) {
|
|
143
|
+
console.warn(`⚠️ spec 共享面 ${review.foreignSpecChurn.length} 个非本会话声明的删除(.sillyspec/ docs/,疑似并行会话归档移动/收尾——归其会话收口,本会话不阻断):`)
|
|
144
|
+
for (const f2 of review.foreignSpecChurn.slice(0, 6)) console.warn(` - ${f2}`)
|
|
145
|
+
if (review.foreignSpecChurn.length > 6) console.warn(` … 共 ${review.foreignSpecChurn.length} 个`)
|
|
146
|
+
}
|
|
147
|
+
if (review.eolOnlyFiles && review.eolOnlyFiles.length > 0) {
|
|
148
|
+
console.warn(`⚠️ ${review.eolOnlyFiles.length} 个文件仅行尾重写(内容零变化,gen:types/编辑器 CRLF 假 M——已剔出本会话审计):${review.eolOnlyFiles.slice(0, 5).join('、')}${review.eolOnlyFiles.length > 5 ? ' 等' : ''}`)
|
|
149
|
+
console.warn(` 根治:.gitattributes 声明 text=auto eol=lf(或按文件类型钉死),生成器重写不再制造假 M`)
|
|
150
|
+
}
|
|
140
151
|
if (review.docsCheckHint && review.docsCheckHint.invalid > 0) {
|
|
141
152
|
console.warn(`\n📎 文档引用失效(docs check):本次改动的文档含 ${review.docsCheckHint.invalid}/${review.docsCheckHint.total} 处失效 file:line 引用。`)
|
|
142
153
|
// 逐条指名(2026-09-11 用户实证:只报计数不指名哪处,四轮复现才定位——门禁输出
|
|
@@ -167,14 +178,97 @@ export function printQuickAuditReview(review) {
|
|
|
167
178
|
else console.warn(` 行号漂移 → 更新到当前源码;跑 sillyspec docs check 可看建议行号。`)
|
|
168
179
|
}
|
|
169
180
|
}
|
|
181
|
+
|
|
182
|
+
// ── [gate] 分级门禁 advisory 块(FR-03,2026-09-14-quick-exit-tiered-gates task-02):L1/L2 画像
|
|
183
|
+
// 打印级别/跨度/模块清单/风险命中/检查项 + --no-docs 指引,L0 零输出。全部 advisory:不改 status
|
|
184
|
+
// 三态与 exit code(D-003);窗口内未声明脏文件维持既有归属分流(上方 ⚖️/🔍 块),不并入文档
|
|
185
|
+
// 认领判定(D-005);数据源 = review.gateProfile(auditQuickCompletion 挂载,scope-audit 重放同源)。
|
|
186
|
+
const gate = review.gateProfile
|
|
187
|
+
if (gate && (gate.level === 'L1' || gate.level === 'L2')) {
|
|
188
|
+
const isL2 = gate.level === 'L2'
|
|
189
|
+
const spanTxt = gate.degraded
|
|
190
|
+
? 'module-map 缺失(降级档:按文件数判级)'
|
|
191
|
+
: `跨 ${gate.moduleSpan} 模块${Array.isArray(gate.modules) && gate.modules.length > 0 ? `(${gate.modules.map(m => m.id).join(' · ')})` : ''}`
|
|
192
|
+
console.warn(`\n${isL2 ? '🛑' : '🚧'} [gate] ${gate.level} 规模门(${spanTxt} · ${gate.fileCount} 文件:${gate.codeFileCount} 代码 / ${gate.testFileCount} 测试)——advisory 提示不阻断:`)
|
|
193
|
+
if (Array.isArray(gate.unmappedFiles) && gate.unmappedFiles.length > 0) {
|
|
194
|
+
console.warn(` ⚠️ ${gate.unmappedFiles.length} 个文件未命中 module-map(${gate.unmappedFiles.slice(0, 5).join(', ')}${gate.unmappedFiles.length > 5 ? ' 等' : ''})`)
|
|
195
|
+
}
|
|
196
|
+
if (!isL2) {
|
|
197
|
+
// L1 检查项:每文件注记(--file-notes 覆盖率)+ 测试增量(机械规则)
|
|
198
|
+
console.warn(` - 每文件注记检查:${gate.checks?.perFileNotes ? '✅ --file-notes 已覆盖全部变更文件' : `❌ 缺失——补 --file-notes "path1::注 || path2::注"(覆盖变更文件全集)`}`)
|
|
199
|
+
const td = gate.checks?.testDelta
|
|
200
|
+
console.warn(` - 测试增量检查:${td === 'missing' ? `❌ missing——${gate.codeFileCount} 个代码文件改动无测试文件(补 test 改动进本会话)` : td === 'ok' ? '✅ 已含测试改动' : '— 不适用(≤1 代码文件)'}`)
|
|
201
|
+
} else {
|
|
202
|
+
// L2 检查项:模块文档认领(claimed/missing/exempt-no-docs)+ 风险命中的运行时证据要求
|
|
203
|
+
const dc = gate.checks?.docClaim
|
|
204
|
+
const noClaimable = gate.degraded || !Array.isArray(gate.modules) || gate.modules.length === 0
|
|
205
|
+
if (dc === 'exempt-no-docs') {
|
|
206
|
+
console.warn(` - 模块文档认领:🛡️ 已 --no-docs 显式豁免(豁免已留痕 QUICKLOG 审计行)`)
|
|
207
|
+
} else if (noClaimable) {
|
|
208
|
+
// degraded / 无触及模块:computeGateProfile 空真 claimed——如实显示不适用,不出「已认领」误导
|
|
209
|
+
console.warn(` - 模块文档认领:— 不适用(${gate.degraded ? '无 module-map' : '触及文件均未命中模块'},无可认领卡片)`)
|
|
210
|
+
} else if (dc === 'claimed') {
|
|
211
|
+
console.warn(` - 模块文档认领:✅ 触及模块的卡片文件已在改动集`)
|
|
212
|
+
} else {
|
|
213
|
+
console.warn(` - 模块文档认领:❌ missing——触及模块(${Array.isArray(gate.modules) && gate.modules.length > 0 ? gate.modules.map(m => m.id).join(' · ') : '—'})的卡片文件不在改动集`)
|
|
214
|
+
console.warn(` ➜ 同步模块卡进本次改动,或确认无需文档落盘时 --done 带 --no-docs 显式豁免(豁免留痕,不阻断完成)`)
|
|
215
|
+
}
|
|
216
|
+
if (Array.isArray(gate.riskHits) && gate.riskHits.length > 0) {
|
|
217
|
+
console.warn(` - 风险命中 ${gate.riskHits.length} 处(运行时证据要求——风险路径改动需在 --result「结果:」附运行验证说明,advisory 不等待):`)
|
|
218
|
+
for (const h of gate.riskHits.slice(0, 8)) {
|
|
219
|
+
console.warn(` ⚠️ ${h.pattern} ← ${h.file}`)
|
|
220
|
+
}
|
|
221
|
+
if (gate.riskHits.length > 8) console.warn(` … 共 ${gate.riskHits.length} 处`)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
170
225
|
}
|
|
171
226
|
|
|
172
227
|
/**
|
|
173
|
-
*
|
|
174
|
-
* -
|
|
175
|
-
*
|
|
228
|
+
* [gate] 分级门禁 auditNote 组装(FR-03 task-02,gate-audit-note):L1/L2 → 单行 `[gate]` 注记
|
|
229
|
+
* (随 complete-handlers 既有 auditNotes 通道落 QUICKLOG,--no-docs 豁免同通道留痕);
|
|
230
|
+
* L0 / 无画像 → null(零落账,验收「L0 无 gate 落账行」)。纯函数、与上方 [gate] 打印块同一
|
|
231
|
+
* review.gateProfile 数据源;单独导出为可测(buildSemanticGuardHits 同款考量——避开
|
|
232
|
+
* handleQuickStageCompletion 的 quicklog 重机件直测组装层)。
|
|
233
|
+
* @param {object|null} gate review.gateProfile(auditQuickCompletion 产物;null/异常形状 → null)
|
|
234
|
+
* @returns {string|null}
|
|
235
|
+
*/
|
|
236
|
+
export function buildGateAuditNote(gate) {
|
|
237
|
+
if (!gate || typeof gate !== 'object' || (gate.level !== 'L1' && gate.level !== 'L2')) return null
|
|
238
|
+
const span = gate.degraded
|
|
239
|
+
? 'module-map 缺失(降级档按文件数判级)'
|
|
240
|
+
: `跨 ${gate.moduleSpan} 模块`
|
|
241
|
+
const seg = [`[gate] ${gate.level}(${span} · ${gate.fileCount} 文件:${gate.codeFileCount} 代码/${gate.testFileCount} 测试)advisory`]
|
|
242
|
+
if (gate.level === 'L1') {
|
|
243
|
+
seg.push(`每文件注记${gate.checks?.perFileNotes ? '已全覆盖' : '缺失(--file-notes 覆盖变更文件全集)'}`)
|
|
244
|
+
const td = gate.checks?.testDelta
|
|
245
|
+
seg.push(`测试增量${td === 'ok' ? '已含' : td === 'missing' ? `缺失(${gate.codeFileCount} 个代码文件无测试改动)` : '不适用(≤1 代码文件)'}`)
|
|
246
|
+
} else {
|
|
247
|
+
const dc = gate.checks?.docClaim
|
|
248
|
+
const noClaimable = gate.degraded || !Array.isArray(gate.modules) || gate.modules.length === 0
|
|
249
|
+
seg.push(dc === 'exempt-no-docs' ? '模块文档认领已 --no-docs 显式豁免'
|
|
250
|
+
: noClaimable ? '模块文档认领不适用(无可认领模块)'
|
|
251
|
+
: dc === 'claimed' ? '模块文档认领已覆盖(模块卡在改动集)'
|
|
252
|
+
: '模块文档认领缺失(同步模块卡进改动集,或 --no-docs 显式豁免)')
|
|
253
|
+
if (Array.isArray(gate.riskHits) && gate.riskHits.length > 0) {
|
|
254
|
+
seg.push(`风险命中 ${gate.riskHits.length} 处(${gate.riskHits.slice(0, 5).map(h => `${h.pattern}←${h.file}`).join('、')}${gate.riskHits.length > 5 ? ' 等' : ''})需运行时证据`)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return seg.join(';')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* quick 阶段关联变更解析(含单候选信号门控)。
|
|
262
|
+
* - 0 活跃变更 → 不关联
|
|
263
|
+
* - 1 活跃变更 → 跑 recommendChanges 双信号打分:命中(score>0)→ 自动关联 + 大声提示
|
|
264
|
+
* + autoLinked 溯源;未命中 → 不关联 + 提示。坑 quick-single-change-auto-link(2026-09-14
|
|
265
|
+
* 实证):原实现无条件 `return [activeChanges[0]]`——多 agent 仓库里唯一活跃变更常是他者
|
|
266
|
+
* 会话遗留,挂载污染 tasks.md 且 --done 僵尸清理通道可误归档他者变更;单候选还绕过
|
|
267
|
+
* recommend 的 quick-<hex8> 会话过滤(新 quick 互挂另一活跃 quick)。
|
|
176
268
|
* - ≥2 活跃变更 + 交互 → checkbox 多选(推荐项默认勾,空选 = 不关联)
|
|
177
|
-
* - ≥2 活跃变更 + 非交互 →
|
|
269
|
+
* - ≥2 活跃变更 + 非交互 → 不关联 + 提示用 --change a,b
|
|
270
|
+
* @returns {Promise<{changes: string[], autoLinked: string[]}>} changes=最终关联清单;
|
|
271
|
+
* autoLinked=其中机器自动关联的子集(guard 落 linkedChangesAuto 溯源,--done 归档闸消费)
|
|
178
272
|
*/
|
|
179
273
|
export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles, taskDescription, nonInteractive }) {
|
|
180
274
|
let activeChanges = []
|
|
@@ -183,13 +277,7 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
|
|
|
183
277
|
} catch {
|
|
184
278
|
activeChanges = []
|
|
185
279
|
}
|
|
186
|
-
if (activeChanges.length === 0) return []
|
|
187
|
-
if (activeChanges.length === 1) return [activeChanges[0]]
|
|
188
|
-
|
|
189
|
-
if (nonInteractive || !process.stdin.isTTY) {
|
|
190
|
-
console.log('💡 非交互环境,已默认不关联变更;如需关联请用 --change a,b')
|
|
191
|
-
return []
|
|
192
|
-
}
|
|
280
|
+
if (activeChanges.length === 0) return { changes: [], autoLinked: [] }
|
|
193
281
|
|
|
194
282
|
// 脏文件(推荐信号之一)。safeGit 带 safe.directory,避免 linked worktree/容器/挂载点下裸
|
|
195
283
|
// `git status` 抛错致推荐信号静默丢失(multi-agent-review Q3 同类)。推荐非关键路径,失败回退 []。
|
|
@@ -203,7 +291,7 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
|
|
|
203
291
|
.filter(f => !f.startsWith('.sillyspec/'))
|
|
204
292
|
}
|
|
205
293
|
|
|
206
|
-
// 推荐打分(脏文件 +
|
|
294
|
+
// 推荐打分(脏文件 + 任务描述双信号)——单候选也跑(信号门控),非仅 ≥2 交互推荐用
|
|
207
295
|
let recommendations = []
|
|
208
296
|
try {
|
|
209
297
|
// quick-recommend.js 在 src/,本模块在 src/run/ → 退一层(真环依赖,保留动态 import)
|
|
@@ -212,6 +300,26 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
|
|
|
212
300
|
} catch {
|
|
213
301
|
recommendations = activeChanges.map(name => ({ name, score: 0, reasons: [] }))
|
|
214
302
|
}
|
|
303
|
+
|
|
304
|
+
// 单候选信号门控(坑 quick-single-change-auto-link):有真实信号(脏文件命中 design 清单 /
|
|
305
|
+
// 任务描述命中 proposal)才自动关联,且大声提示可反悔;无信号一律不关联。quick-<hex8> 会话
|
|
306
|
+
// 行已被 recommendChanges 过滤 → recommendations 无该候选 → 不关联(防会话互挂)。
|
|
307
|
+
if (activeChanges.length === 1) {
|
|
308
|
+
const name = activeChanges[0]
|
|
309
|
+
const rec = recommendations.find(r => r.name === name)
|
|
310
|
+
if (rec && rec.score > 0) {
|
|
311
|
+
console.log(`🔗 唯一活跃变更 ${name} 命中关联信号(${rec.reasons.slice(0, 2).join(';')})——已自动关联;无关请带 --linked-changes none 重启`)
|
|
312
|
+
return { changes: [name], autoLinked: [name] }
|
|
313
|
+
}
|
|
314
|
+
console.log(`💡 唯一活跃变更 ${name} 未命中关联信号(任务描述/脏文件无重叠)——默认不关联;如需关联请带 --linked-changes ${name} 重启`)
|
|
315
|
+
return { changes: [], autoLinked: [] }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (nonInteractive || !process.stdin.isTTY) {
|
|
319
|
+
console.log('💡 非交互环境,已默认不关联变更;如需关联请用 --change a,b')
|
|
320
|
+
return { changes: [], autoLinked: [] }
|
|
321
|
+
}
|
|
322
|
+
|
|
215
323
|
const scoreMap = new Map(recommendations.map(r => [r.name, r.score]))
|
|
216
324
|
const reasonMap = new Map(recommendations.map(r => [r.name, r.reasons]))
|
|
217
325
|
const recommendedSet = new Set(recommendations.filter(r => r.score > 0).map(r => r.name))
|
|
@@ -238,7 +346,8 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
|
|
|
238
346
|
// 动态 import:@inquirer/prompts 只在本交互分支加载,不进 run 命令通用启动路径
|
|
239
347
|
const { checkbox } = await import('@inquirer/prompts')
|
|
240
348
|
const selected = await checkbox({ message: '关联变更(空格切换,回车确认)', choices })
|
|
241
|
-
|
|
349
|
+
// 用户在 TTY 上亲手勾选确认 = 显式协作声明,不进 autoLinked
|
|
350
|
+
return { changes: selected, autoLinked: [] }
|
|
242
351
|
}
|
|
243
352
|
|
|
244
353
|
// ============ quick --done test+lint 硬门禁(2026-09-02 跨 agent 工单 P0-2)============
|
|
@@ -338,16 +447,28 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
338
447
|
if (snapshot) {
|
|
339
448
|
gateCwd = snapshot.snapshotRoot
|
|
340
449
|
gateSpecBase = join(snapshot.snapshotRoot, '.sillyspec')
|
|
341
|
-
console.log(`🧪
|
|
450
|
+
console.log(`🧪 门禁隔离快照(根:${snapshot.snapshotRoot}):HEAD + 本会话 ${snapshot.overlaid} 个文件(并行会话脏文件不参与判定)`)
|
|
342
451
|
}
|
|
343
452
|
} catch { /* 快照链路异常 → 主仓现行为 */ }
|
|
344
453
|
}
|
|
345
454
|
try {
|
|
346
455
|
const test = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName })
|
|
347
|
-
|
|
456
|
+
let lint = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase, timeoutMs: snapshot ? 5 * 60 * 1000 : undefined })
|
|
457
|
+
// 快照 lint 超时回退主仓(2026-09-12 dogfood 两连实证:junction I/O 病态慢,3min/5min 均被
|
|
458
|
+
// 超时杀——主仓 60~110s 正常。按用户建议「自动回退」而非假败/advisory:主仓复跑保硬门,
|
|
459
|
+
// 并行噪声混入时失败输出带归属鉴定提示)
|
|
460
|
+
if (snapshot && lint.status === 'failed' && /超时/.test(String(lint.reason || ''))) {
|
|
461
|
+
console.warn('⚠️ 快照 lint 超时(node_modules junction I/O 慢)→ 主仓复跑 lint(并行噪声可能混入——失败先做归属鉴定)')
|
|
462
|
+
lint = runVerifyLintCheck({ cwd, specBase })
|
|
463
|
+
}
|
|
348
464
|
const failed = []
|
|
349
465
|
if (test.status === 'failed') failed.push('test')
|
|
350
466
|
if (lint.status === 'failed') failed.push('lint')
|
|
467
|
+
if (failed.length > 0 && snapshot) {
|
|
468
|
+
try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
|
|
469
|
+
printSnapshotFailureHint({ snapshotRoot: snapshot.snapshotRoot, changeFileCount: snapshot.overlaid, sourceRoot: snapshot.sourceRoot || null }, { offEnv: 'SILLYSPEC_QUICK_GATE_SNAPSHOT_OFF' })
|
|
470
|
+
} catch { /* 提示失败不影响门禁语义 */ }
|
|
471
|
+
}
|
|
351
472
|
return {
|
|
352
473
|
action: failed.length > 0 ? 'fail' : 'pass',
|
|
353
474
|
failed,
|