sillyspec 3.28.9 → 3.28.11
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 +223 -222
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
- package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/change-risk-profile.js +51 -3
- package/src/config-schema.js +51 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/design-facts.js +94 -18
- package/src/docs-check.js +14 -10
- package/src/docs-debt.js +7 -2
- package/src/docs-gate.js +42 -15
- package/src/endpoint-extractor.js +50 -2
- package/src/foreign-declared.js +25 -16
- package/src/hooks/worktree-guard.js +75 -2
- package/src/index.js +147 -2
- package/src/knowledge-classify.js +20 -1
- package/src/machine-interface.js +26 -0
- package/src/probe7-anchor-check.js +87 -0
- package/src/progress/stage-machine.js +34 -5
- package/src/progress.js +1 -1
- package/src/quicklog.js +240 -5
- package/src/run/command.js +25 -2
- package/src/run/complete-handlers.js +172 -1
- package/src/run/complete.js +143 -11
- package/src/run/gate-snapshot.js +372 -2
- package/src/run/gates.js +130 -12
- package/src/run/prompt.js +166 -7
- package/src/run/shared.js +81 -1
- package/src/scope-audit.js +101 -31
- package/src/stage-contract-spec.js +14 -0
- package/src/stage-contract.js +314 -8
- package/src/stage-review-checklist.js +3 -1
- package/src/stage-review.js +137 -2
- package/src/stages/archive.js +9 -5
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/execute.js +62 -8
- package/src/stages/knowledge.js +24 -3
- package/src/stages/plan-postcheck.js +74 -10
- package/src/stages/plan.js +8 -1
- package/src/stages/quick.js +5 -5
- package/src/stages/verify.js +3 -2
- package/src/sync-noise.js +57 -1
- package/src/sync.js +77 -4
- package/src/task-review.js +84 -29
- package/src/verify-facts-schema.js +78 -9
- package/src/verify-postcheck.js +439 -68
- package/src/verify-probes.js +1156 -45
- package/src/worktree-apply.js +323 -8
- package/src/worktree-deps.js +24 -1
- package/src/worktree.js +274 -10
- package/src/write-audit.js +55 -0
package/src/run/gates.js
CHANGED
|
@@ -322,12 +322,14 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
|
|
|
322
322
|
// 下游已有物理目录存在性判定兜底(G2/R3),这里 warn 留下根因线索,避免静默误诊。
|
|
323
323
|
console.warn(`⚠️ worktree meta 读取失败(deps gate 将走物理目录判定): ${e.message}`)
|
|
324
324
|
}
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
325
|
+
const depsOk = (s) => ['linked', 'installed', 'n/a'].includes(s)
|
|
326
|
+
const changeDir = changeName ? join(specBase, 'changes', changeName) : null
|
|
327
|
+
|
|
328
|
+
// main 侧就绪后的跨仓 worktree deps 校验(坑 cross-repo-no-worktree-isolation):
|
|
329
|
+
// 跨仓仓供给失败同样无构建/测试能力(前端仓无 node_modules 建不了 build)。只校验已建
|
|
330
|
+
// worktree 的跨仓仓(legacy 直写模式无 meta,维持原语义,下游 Task Review 兜底)。
|
|
331
|
+
// 放行返回 true;跨仓未就绪置 blocked + exit(1)。
|
|
332
|
+
const crossCheckOrExit = async () => {
|
|
331
333
|
let crossFailed = []
|
|
332
334
|
try {
|
|
333
335
|
const crossSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
@@ -351,8 +353,39 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
|
|
|
351
353
|
if (persist) { try { await persist() } catch { /* 落盘失败不吞阻断语义 */ } }
|
|
352
354
|
process.exit(1)
|
|
353
355
|
}
|
|
354
|
-
|
|
356
|
+
|
|
357
|
+
if (depsOk(meta?.depsStatus)) return await crossCheckOrExit()
|
|
358
|
+
|
|
359
|
+
// wave 级 opt-out(先于重供给:无需 deps 能力的 wave 不花 install 成本)
|
|
355
360
|
if (isCurrentWaveAllNoDepsVerify(step?.name, changeDir)) return true
|
|
361
|
+
|
|
362
|
+
// ── --done 重供给重试(坑 deps-gate-manual-retry-ineffective,2026-09-15 wp 会话实证)──
|
|
363
|
+
// 阻断提示承诺「在 worktree 内手动安装依赖后重试」,但 manual install 不改 meta.depsStatus,
|
|
364
|
+
// failed 态直读 meta 永不翻绿 → 重试 --done 必撞同一堵墙,唯一出路 doctor --fix(wp 会话
|
|
365
|
+
// 正是手动编译成功后重试仍被拦,被迫多跑一轮 doctor)。对齐 run execute 入口
|
|
366
|
+
// ensureDepsFreshness 的同款供给:阻断前重供给一次并写回 meta——PATH 已修好的 shell 里
|
|
367
|
+
// 重跑 --done 即等效 doctor --fix;marker 型生态(nodejs)手动安装补齐 node_modules 后
|
|
368
|
+
// 重供给的 install 也能自然通过。仍失败则维持阻断,不无限重试。
|
|
369
|
+
let depsStatus = meta?.depsStatus
|
|
370
|
+
if (wm && changeName) {
|
|
371
|
+
let wtPath = null
|
|
372
|
+
try { wtPath = wm.getWorktreePath(changeName) } catch { /* meta 缺失走物理目录判定 */ }
|
|
373
|
+
if (wtPath && existsSync(wtPath)) {
|
|
374
|
+
try {
|
|
375
|
+
const { provisionDeps } = await import('../worktree-deps.js')
|
|
376
|
+
console.log(`🔄 deps 门控:depsStatus=${depsStatus || 'unknown'},阻断前按 --done 重试路径重供给一次(与 doctor --fix 同款)...`)
|
|
377
|
+
const deps = provisionDeps(wtPath, cwd, { specBase }) || {}
|
|
378
|
+
meta = { ...meta, ...deps }
|
|
379
|
+
writeAtomicSync(join(wtPath, 'meta.json'), JSON.stringify(meta, null, 2) + '\n')
|
|
380
|
+
depsStatus = meta.depsStatus
|
|
381
|
+
console.log(` 重供给完成:depsStatus=${depsStatus || 'unknown'}${meta.depsError ? `(${meta.depsError})` : ''}`)
|
|
382
|
+
if (depsOk(depsStatus)) return await crossCheckOrExit()
|
|
383
|
+
} catch (e) {
|
|
384
|
+
console.warn(`⚠️ deps 门控重供给失败(维持阻断判定):${e.message}`)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
356
389
|
if (steps && steps[currentIdx]) {
|
|
357
390
|
steps[currentIdx].status = 'blocked'
|
|
358
391
|
// 阻断原因持久化(坑 deps-gate-blocked-invisible):--status/诊断时可见,不依赖阻断当刻的
|
|
@@ -845,6 +878,11 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
845
878
|
})
|
|
846
879
|
const reconcileEnvelope = buildReconcileTargetFilesEnvelope(reconcileCheck)
|
|
847
880
|
const reconcileBlocked = printReconcileTargetFilesCheck(reconcileCheck, reconcileEnvelope)
|
|
881
|
+
// 跨仓 per-repo 对账明细渲染(坑 cross-repo-reconcile-blindness,2026-09-15 复盘兑现 D-004
|
|
882
|
+
// 分期):reconcileTargetFiles 的 crossRepo 字段(advisory 不阻断)——各仓 对上/②缺/③多
|
|
883
|
+
// 逐仓一行 + ②缺明细(跨仓声明落空是该去对应仓干活的信号,但不翻主仓状态——锚点窗口
|
|
884
|
+
// 脆弱期会假信号)。无跨仓卡时 crossRepo 为空数组零输出。
|
|
885
|
+
printCrossRepoReconcile(reconcileCheck.crossRepo)
|
|
848
886
|
// 结果落盘(design Wave 2 承诺:对齐 .runtime/verify-runs/<ts>/ 先例)——五状态全落(含
|
|
849
887
|
// 放行态),fail-soft 不影响 gate 判定;②类阻断回执同样留档供平台/审计消费
|
|
850
888
|
writeReconcileRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: reconcileEnvelope, result: reconcileCheck })
|
|
@@ -872,6 +910,23 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
872
910
|
})
|
|
873
911
|
const probeEnvelope = buildProbeConsistencyEnvelope(probeCheck)
|
|
874
912
|
const probeBlocked = printProbeConsistencyCheck(probeCheck, probeEnvelope)
|
|
913
|
+
// 探针7 covered 证据锚点 advisory(坑 probe7-covered-anchor-missing,2026-09-15 复盘:7 行
|
|
914
|
+
// covered 证据缺 file:line 被审查打回——预填说明已载明口径,机器在 --done 时点再拦一道,
|
|
915
|
+
// 省一轮人工往返)。advisory 不阻断;独立模块零环(verify-probes 是多会话冲突面,不碰)。
|
|
916
|
+
try {
|
|
917
|
+
const { checkProbe7AnchorCoverage } = await import('../probe7-anchor-check.js')
|
|
918
|
+
const p7ReportPath = join(specBase, 'changes', changeName, 'verify-result.md')
|
|
919
|
+
if (existsSync(p7ReportPath)) {
|
|
920
|
+
const p7Anchor = checkProbe7AnchorCoverage(readFileSync(p7ReportPath, 'utf8'))
|
|
921
|
+
if (p7Anchor.applicable && p7Anchor.missingAnchors.length > 0) {
|
|
922
|
+
console.warn(`\n⚠️ 探针7 锚点校验:${p7Anchor.missingAnchors.length}/${p7Anchor.coveredRows} 行判定为 covered 但证据列缺 file:line / \`.test.\` 文件名 / 反引号路径锚点(advisory 不阻断,审查会要求回补):`)
|
|
923
|
+
for (const m of p7Anchor.missingAnchors.slice(0, 10)) {
|
|
924
|
+
console.warn(` - ${m.task}:${m.acceptance || '(空 acceptance)'}——证据当前为「${m.evidence || '(空)'}」,应给 \`测试文件路径:行号\` 首命中锚点、\`.test.\` 测试文件名或反引号包裹的路径/测试名(行号可省)`)
|
|
925
|
+
}
|
|
926
|
+
if (p7Anchor.missingAnchors.length > 10) console.warn(` …还有 ${p7Anchor.missingAnchors.length - 10} 行`)
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
} catch { /* 锚点校验异常 fail-soft 不影响 gate */ }
|
|
875
930
|
// 结果落盘(对齐 writeReconcileRunResult 先例):独立文件 probe-consistency-result.json——
|
|
876
931
|
// 与 reconcile-result.json 同 gate 运行同目录组织(verify-runs/<ts>/),但不并入后者:
|
|
877
932
|
// reconcile-result.json 的 schema 已被 P3a 消费方锁定,塞探针字段(读改写/扩参)只会引入
|
|
@@ -996,8 +1051,14 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
996
1051
|
// 不在自动刷新范围,照常 fail-closed。
|
|
997
1052
|
const gated = validateStageReviewWithAutoRefresh({ stage: stageName, reviewType, runtimeRoot, reviewRunId, searchDirs, autoRefresh: true })
|
|
998
1053
|
if (gated.autoRefreshed) {
|
|
999
|
-
console.warn(`\n🔄 stage review docHash
|
|
1054
|
+
console.warn(`\n🔄 stage review docHash 失配已自动刷新(第 ${gated.refreshOrdinal || 1} 次;hash ${gated.hashDrift || 'n/a'};主文档改版后机械重算,verdict/checklist 保留——结论是否仍适用于新文档需人工确认)`)
|
|
1000
1055
|
console.warn(` 已就地更新: ${gated.refreshedPath}`)
|
|
1056
|
+
// C3 升级提醒(2026-09-15 wp EHS 实证:NEW: 批注/需求调整/Reverse Sync 多次走此通道):
|
|
1057
|
+
// 同一 review 被反复刷新 = 主文档在审查通过后反复变更,「审查后的变更不再见审查」的
|
|
1058
|
+
// 面积在累积——序数 ≥2 从一句注记升级为显式人工确认提醒(仍放行,不伪造重审)。
|
|
1059
|
+
if ((gated.refreshOrdinal || 1) >= 2) {
|
|
1060
|
+
console.warn(` ⚠️ 该 review 已第 ${gated.refreshOrdinal} 次刷新——主文档在审查后反复变更,建议人工复核 verdict 结论续用性(涉及设计要点/接口变更时应重审,勿把自动放行当「新文档已审过」)`)
|
|
1061
|
+
}
|
|
1001
1062
|
}
|
|
1002
1063
|
const reviewResult = gated.result
|
|
1003
1064
|
printStageReviewResult(reviewResult, { stage: stageName, reviewRunId, runtimeRoot, changeName })
|
|
@@ -1010,7 +1071,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1010
1071
|
// channel 识别(2026-09-10 通道优先序批次):reviewer.channel 结构化落款优先,兼容
|
|
1011
1072
|
// reviewerNotes 首行「降级:」约定;platform 通道(P2 review-dispatch 落地后)ℹ️ 留痕
|
|
1012
1073
|
// missionId 供追溯,其余通道正常放行。
|
|
1013
|
-
const { classifyReviewerChannel } = await import('../stage-review.js')
|
|
1074
|
+
const { classifyReviewerChannel, hasDelegatedWriteDisclosure } = await import('../stage-review.js')
|
|
1014
1075
|
const reviewChannel = classifyReviewerChannel(reviewResult.review)
|
|
1015
1076
|
if (reviewChannel === 'self') {
|
|
1016
1077
|
console.warn(`\n⚠️ Stage Review 降级自审(tier=independent 但宿主无 Agent tool):${stageName} review 按 prompt 降级条款由当前 agent 自审产出——独立性折损已留痕(reviewerNotes 首行「降级:」),结论应附源码锚点补偿,建议人工抽查关键结论。`)
|
|
@@ -1018,6 +1079,12 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1018
1079
|
const mid = reviewResult.review && reviewResult.review.reviewer && reviewResult.review.reviewer.missionId
|
|
1019
1080
|
console.log(`\nℹ️ Stage Review 平台派发(channel=platform${mid ? `,missionId=${mid}` : ',missionId 未落款'}):${stageName} review 由平台独立 worker 产出(独立进程/会话)。`)
|
|
1020
1081
|
}
|
|
1082
|
+
// C4 代落盘审计行(2026-09-15 wp EHS 实证:平台写通道故障下主流程代子代理落盘 review,
|
|
1083
|
+
// 独立性折损当时只在 notes 里、gate 不可见):reviewerNotes 首行「代落盘:」披露 → ⚠️ 留痕
|
|
1084
|
+
// (与降级自审同哲学:可见的折损优于静默伪装;首行约定识别,不阻断)。
|
|
1085
|
+
if (hasDelegatedWriteDisclosure(reviewResult.review)) {
|
|
1086
|
+
console.warn(`\n⚠️ Stage Review 代落盘披露:${stageName} review 的 reviewerNotes 首行标记「代落盘」——review.json 由主代理代(写通道故障的)审查者落盘,独立性依赖审查者结论回传的忠实性,建议人工抽查关键结论与回传文本一致性。`)
|
|
1087
|
+
}
|
|
1021
1088
|
}
|
|
1022
1089
|
} catch (e) {
|
|
1023
1090
|
// fail-closed:Gate 自身异常阻断完成,不静默放行(与 Task Review Gate 一致)
|
|
@@ -1554,6 +1621,35 @@ export function writeReconcileRunResult({ runtimeRoot, changeName, envelope, res
|
|
|
1554
1621
|
* @param {object} [envelope] 预构造信封(缺省现算;显式传入便于调用方先断言再打印)
|
|
1555
1622
|
* @returns {boolean} true = 阻断(status='missing_declared')
|
|
1556
1623
|
*/
|
|
1624
|
+
/**
|
|
1625
|
+
* 打印跨仓 per-repo 对账结果(cross-repo-reconcile.js 产出的 advisory 明细,verify 块接线)。
|
|
1626
|
+
* 恒不阻断(返回 void):跨仓 diff 锚点=各仓最近提交窗口,锚点脆弱期(已 commit 多笔/全在
|
|
1627
|
+
* working-tree)会产②类假信号——明细供人工裁决,不翻主仓对账状态。
|
|
1628
|
+
* @param {Array<{repo:string,repoPath:string|null,declaredCount:number,actualCount:number,
|
|
1629
|
+
* matched:string[],missing:Array<{task:string,path:string,isNew?:boolean}>,
|
|
1630
|
+
* undeclared:string[],scaffoldCount:number,degradedReason:string|null}>|undefined} crossRepo
|
|
1631
|
+
*/
|
|
1632
|
+
function printCrossRepoReconcile(crossRepo) {
|
|
1633
|
+
if (!Array.isArray(crossRepo) || crossRepo.length === 0) return
|
|
1634
|
+
console.log(`\n🔀 跨仓 per-repo 对账(advisory 不阻断,锚点=各仓最近提交窗口):`)
|
|
1635
|
+
for (const r of crossRepo) {
|
|
1636
|
+
if (r.degradedReason) {
|
|
1637
|
+
console.warn(` ⚠️ ${r.repo}:${r.degradedReason}`)
|
|
1638
|
+
continue
|
|
1639
|
+
}
|
|
1640
|
+
const flag = (r.missing.length > 0 || r.undeclared.length > 0) ? '⚠️ ' : '✅ '
|
|
1641
|
+
console.log(` ${flag}${r.repo}(${r.repoPath}):声明 ${r.declaredCount} / 实测 ${r.actualCount} / 对上 ${r.matched.length} / ②缺 ${r.missing.length} / ③多 ${r.undeclared.length}${r.scaffoldCount > 0 ? ` / 脚手架聚合 ${r.scaffoldCount}` : ''}`)
|
|
1642
|
+
for (const m of r.missing.slice(0, 10)) {
|
|
1643
|
+
console.warn(` ②缺 ${m.task}: ${m.path}${m.isNew ? '(NEW: 声明新建,该仓未见)' : ''}——锚点窗口外已提交或多笔时可能假信号,到该仓 git log 核实`)
|
|
1644
|
+
}
|
|
1645
|
+
if (r.missing.length > 10) console.warn(` …还有 ${r.missing.length - 10} 条②缺`)
|
|
1646
|
+
for (const u of r.undeclared.slice(0, 10)) {
|
|
1647
|
+
console.warn(` ③多 ${u}(该仓最近窗口实际改动,未在任何跨仓卡声明)`)
|
|
1648
|
+
}
|
|
1649
|
+
if (r.undeclared.length > 10) console.warn(` …还有 ${r.undeclared.length - 10} 条③多`)
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1557
1653
|
export function printReconcileTargetFilesCheck(r, envelope = buildReconcileTargetFilesEnvelope(r)) {
|
|
1558
1654
|
// ②类(声明未做=计划落空):ERROR 阻断
|
|
1559
1655
|
if (r.status === 'missing_declared') {
|
|
@@ -1569,11 +1665,33 @@ export function printReconcileTargetFilesCheck(r, envelope = buildReconcileTarge
|
|
|
1569
1665
|
// ③类(做了没声明=scope creep):WARNING 放行,suspectTask 尽力归因随行列出
|
|
1570
1666
|
if (r.status === 'undeclared') {
|
|
1571
1667
|
console.warn(`\n⚠️ target_files 对账发现 ${r.undeclared.length} 个实际改动未在任何 task 卡声明(③类 scope creep,advisory 不阻断):`)
|
|
1572
|
-
|
|
1668
|
+
if (r.undeclared.length <= 20) {
|
|
1669
|
+
for (const u of r.undeclared) {
|
|
1573
1670
|
console.warn(` - ${u.path}${u.suspectTask ? `(疑似 ${u.suspectTask} 的改动——该 task 的 review.json changedFiles 提及)` : ''}`)
|
|
1574
1671
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1672
|
+
} else {
|
|
1673
|
+
// >20 条逐行刷屏改按顶层目录聚合(坑 reconcile-undeclared-noise,2026-09-15 wp EHS 会话
|
|
1674
|
+
// 实证:58 条③类多为 worktree baseline 拷入的环境文件——agent 需要的是形状不是清单):
|
|
1675
|
+
// 目录计数降序 + 每目录至多 2 个样例;完整清单随后由 writeReconcileRunResult 落盘
|
|
1676
|
+
// reconcile-result.json(gate 输出 📄 行含具体路径),suspectTask 归因以落盘为准。
|
|
1677
|
+
const byDir = new Map()
|
|
1678
|
+
for (const u of r.undeclared) {
|
|
1679
|
+
const p = String(u.path || '')
|
|
1680
|
+
const slash = p.indexOf('/')
|
|
1681
|
+
const dir = slash > 0 ? p.slice(0, slash + 1) : '(仓根)'
|
|
1682
|
+
const rec = byDir.get(dir) || { count: 0, samples: [] }
|
|
1683
|
+
rec.count++
|
|
1684
|
+
if (rec.samples.length < 2) rec.samples.push(p)
|
|
1685
|
+
byDir.set(dir, rec)
|
|
1686
|
+
}
|
|
1687
|
+
const dirs = [...byDir.entries()].sort((a, b) => b[1].count - a[1].count)
|
|
1688
|
+
for (const [dir, rec] of dirs.slice(0, 8)) {
|
|
1689
|
+
console.warn(` - ${dir} ×${rec.count}(样例:${rec.samples.join('、')})`)
|
|
1690
|
+
}
|
|
1691
|
+
if (dirs.length > 8) console.warn(` …另有 ${dirs.length - 8} 个目录`)
|
|
1692
|
+
console.warn(` 共 ${r.undeclared.length} 条,>20 触发按目录聚合;完整逐条清单(含疑似 task 归因)见对账落盘 reconcile-result.json(下方 📄 行)。`)
|
|
1693
|
+
}
|
|
1694
|
+
console.warn(` evidence: matched=${envelope.evidence.matched_count} missing=${envelope.evidence.missing_declared_count} undeclared=${envelope.evidence.undeclared_count}`)
|
|
1577
1695
|
for (const fix of envelope.supportedFixes) console.warn(` 提示:${fix}`)
|
|
1578
1696
|
console.warn(` 对账口径 actual=${(r.sources || []).join(' + ') || 'n/a'}(③类误报先核对 sources 与并行会话剔除提示)。`)
|
|
1579
1697
|
return false
|
package/src/run/prompt.js
CHANGED
|
@@ -316,6 +316,68 @@ export function buildKnowledgeInjection({ knowledgeDir, runtimeDir, change, quer
|
|
|
316
316
|
|
|
317
317
|
// parseModuleMapSimple 复用 modules.js 的 canonical 实现(合并历史 copy-paste 副本,2026-08-07;
|
|
318
318
|
// 无循环依赖:modules.js 仅 import fs/path/db.js,prompt.js → modules.js 单向)。
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 等效验证先例库(坑 verify-precedent-only-in-prose,2026-09-15 EHS 生产实证:mvn test 被
|
|
322
|
+
* 公司框架 parent pom pluginManagement 硬编码 surefire skip=true、-D 覆盖无效——等效口径
|
|
323
|
+
* dependency:build-classpath+javac+JUnitCore 第二次复用靠 agent 翻旧 verify-result.md 正文
|
|
324
|
+
* 续命;先例只活在散文里 = 换个 agent/换个变更就断档)。结构化登记进 local.yaml
|
|
325
|
+
* verify_precedents 段,verify prompt 时点注入提示(与 test_strategy 预检提示同挂点)。
|
|
326
|
+
*
|
|
327
|
+
* 对象数组形态(行扫描,同 extractKnownFailures 免 YAML 依赖口径——注释/空行/块界容错):
|
|
328
|
+
* verify_precedents:
|
|
329
|
+
* - id: mvn-test-surefire-skip
|
|
330
|
+
* standard_command: mvn test
|
|
331
|
+
* reason: <标准命令为何不可用>
|
|
332
|
+
* equivalent: <已验证的等效口径>
|
|
333
|
+
* established_by: <确立该口径的变更名>
|
|
334
|
+
* notes: <可选补充>
|
|
335
|
+
* 残项(standard_command 与 equivalent 双缺)过滤——登记不完整的先例没有可执行口径。
|
|
336
|
+
* @param {string|null} yamlText local.yaml 全文
|
|
337
|
+
* @returns {Array<object>} 先例对象数组
|
|
338
|
+
*/
|
|
339
|
+
export function parseVerifyPrecedents(yamlText) {
|
|
340
|
+
const lines = String(yamlText || '').split(/\r?\n/)
|
|
341
|
+
const headIdx = lines.findIndex(l => /^verify_precedents:\s*(?:#.*)?$/.test(l))
|
|
342
|
+
if (headIdx < 0) return []
|
|
343
|
+
const items = []
|
|
344
|
+
let cur = null
|
|
345
|
+
const clean = (v) => String(v || '').replace(/\s+#.*$/, '').trim()
|
|
346
|
+
for (let i = headIdx + 1; i < lines.length; i++) {
|
|
347
|
+
const l = lines[i]
|
|
348
|
+
if (/^\S/.test(l)) break // 下一个顶层键 → 块结束
|
|
349
|
+
if (!l.trim() || /^\s*#/.test(l)) continue
|
|
350
|
+
const item = l.match(/^\s*-\s+([A-Za-z_][\w-]*):\s*(.*)$/)
|
|
351
|
+
if (item) {
|
|
352
|
+
cur = {}
|
|
353
|
+
items.push(cur)
|
|
354
|
+
cur[item[1]] = clean(item[2])
|
|
355
|
+
continue
|
|
356
|
+
}
|
|
357
|
+
const kv = l.match(/^\s*([A-Za-z_][\w-]*):\s*(.*)$/)
|
|
358
|
+
if (kv && cur) cur[kv[1]] = clean(kv[2])
|
|
359
|
+
}
|
|
360
|
+
return items.filter(x => x && (x.standard_command || x.equivalent))
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* 先例提示渲染(verify prompt 注入用;空输入 → '',调用方零输出)。
|
|
365
|
+
* 行动指引刻意指向「把等效命令配进 commands.test 照常全量实测对账」而非 test_strategy: skip
|
|
366
|
+
* 放行——EHS 实证里被环境阻断 deferred 的集成测试正是 P1 缺陷藏身处,能实测就实测。
|
|
367
|
+
* @param {Array<object>|null} precedents
|
|
368
|
+
* @returns {string}
|
|
369
|
+
*/
|
|
370
|
+
export function renderVerifyPrecedentHint(precedents) {
|
|
371
|
+
if (!Array.isArray(precedents) || precedents.length === 0) return ''
|
|
372
|
+
const L = [`【验证先例提示】本仓已登记 ${precedents.length} 条等效验证先例(标准命令不可用时的既有替代口径):`]
|
|
373
|
+
for (const p of precedents.slice(0, 5)) {
|
|
374
|
+
L.push(`- ${p.id || '(无 id)'}:\`${p.standard_command || '?'}\` 不可用(${p.reason || '原因未注'})→ 等效口径 \`${p.equivalent || '?'}\`(确立于 ${p.established_by || '?'}${p.notes ? `;${p.notes}` : ''})`)
|
|
375
|
+
}
|
|
376
|
+
if (precedents.length > 5) L.push(`- …另有 ${precedents.length - 5} 条未展开(读 local.yaml verify_precedents 段看全量)`)
|
|
377
|
+
L.push('适用时把等效命令配进 local.yaml commands.test(或 modules.<m>.test)后照常全量实测对账,而不是只靠 test_strategy: skip 放行;先例不适用(环境已变/口径失效)可忽略并在 verify-result 注记。')
|
|
378
|
+
return L.join('\n')
|
|
379
|
+
}
|
|
380
|
+
|
|
319
381
|
/**
|
|
320
382
|
* 提示词路径根的单一解析:specRoot(平台) > specDriftAnchor(worktree 漂移锚定主仓) > cwd/.sillyspec(本地)。
|
|
321
383
|
* 治 execute review.json 提示路径写 worktree 副本、--done 校验读主仓的分裂(坑 execute-prompt-spec-drift):
|
|
@@ -980,7 +1042,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
980
1042
|
if (['brainstorm', 'plan', 'execute'].includes(stageName) && promptText.includes('{REVIEW_TIER}')) {
|
|
981
1043
|
try {
|
|
982
1044
|
const { classifyReviewTier } = await import('../review-tier.js')
|
|
983
|
-
const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath, readReviewChannelPriority } = await import('../stage-review.js')
|
|
1045
|
+
const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath, readReviewChannelPriority, getLatestStageReviewRunId, collectSameStagePriorReview, renderPriorRoundFindingsMd } = await import('../stage-review.js')
|
|
984
1046
|
const tierSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
985
1047
|
const tierChangeDir = changeName ? join(tierSpecBase, 'changes', changeName) : null
|
|
986
1048
|
const designPath = tierChangeDir ? join(tierChangeDir, 'design.md') : null
|
|
@@ -1020,11 +1082,52 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1020
1082
|
// 头部渲染「审查执行通道」段(按用户配置序,见 local.yaml review_dispatch.channel_priority)。
|
|
1021
1083
|
const reviewChannelPriority = readReviewChannelPriority(cwd)
|
|
1022
1084
|
const reviewContractMd = renderReviewJsonContract({ stage: stageName, changeDir: tierChangeDir, reviewRunId, tier: tier.tier, channelPriority: reviewChannelPriority })
|
|
1085
|
+
// C2 前阶段实证清单机械附带(坑 review-subagent-redundant-verify,2026-09-15 wp EHS 实证:
|
|
1086
|
+
// plan 审查把 brainstorm 已两轮实证的三仓源码全量重验——94 分钟不收敛被用户三催。此前只有
|
|
1087
|
+
// prompt 散文说「前序实证勿重验」,子代理并不知道前序具体审过了什么;现把前序 stage review
|
|
1088
|
+
// 的 checklist pass 项机械注入派发 prompt(封顶 15 条防灌爆),审查子代理据此跳过已实证面。
|
|
1089
|
+
let priorFactsMd = ''
|
|
1090
|
+
if (tier.tier === 'independent') {
|
|
1091
|
+
try {
|
|
1092
|
+
const priorStages = stageName === 'plan' ? ['brainstorm'] : stageName === 'execute' ? ['brainstorm', 'plan'] : []
|
|
1093
|
+
const factLines = []
|
|
1094
|
+
for (const ps of priorStages) {
|
|
1095
|
+
let priorRunId = null
|
|
1096
|
+
try { priorRunId = getLatestStageReviewRunId(tierRuntimeRoot, ps, changeName) } catch {}
|
|
1097
|
+
if (!priorRunId) continue
|
|
1098
|
+
const priorReviewPath = join(tierRuntimeRoot, 'stage-reviews', `${ps}-${priorRunId}`, 'review.json')
|
|
1099
|
+
if (!existsSync(priorReviewPath)) continue
|
|
1100
|
+
try {
|
|
1101
|
+
const priorReview = JSON.parse(readFileSync(priorReviewPath, 'utf8'))
|
|
1102
|
+
for (const c of (Array.isArray(priorReview.checklist) ? priorReview.checklist : [])) {
|
|
1103
|
+
if (c && c.result === 'pass' && c.item) factLines.push(`- [${ps}] ${String(c.item).replace(/\s+/g, ' ').slice(0, 100)}`)
|
|
1104
|
+
}
|
|
1105
|
+
} catch {}
|
|
1106
|
+
}
|
|
1107
|
+
if (factLines.length > 0) {
|
|
1108
|
+
const shown = factLines.slice(0, 15)
|
|
1109
|
+
priorFactsMd = `\n**前序阶段独立审查已实证(pass)结论——直接引用勿重验**(展示 ${shown.length}/${factLines.length} 条;EHS 复盘实证:重验已实证面 = 94 分钟不收敛的同款浪费;本阶段只审自己特有面,结论存疑时才定向补证):\n${shown.join('\n')}\n`
|
|
1110
|
+
}
|
|
1111
|
+
} catch {}
|
|
1112
|
+
}
|
|
1113
|
+
// 同阶段上一轮复审基线(ql-20260916-021,Superpowers scoped re-review 采纳①):上一轮
|
|
1114
|
+
// FAIL 的 findings 与已实证 pass 面机械回灌给复审子代理——复审以增量为主(核验修复 +
|
|
1115
|
+
// 本次改版新增面),防全量重读与重复报告已修问题(独立复审子代理无对话历史,不知上一轮
|
|
1116
|
+
// 审过什么/报过什么)。骨架轮/他变更轮由采集端过滤;tier=self 不注入(当前 agent 自带
|
|
1117
|
+
// 上一轮上下文)。
|
|
1118
|
+
try {
|
|
1119
|
+
const priorRound = collectSameStagePriorReview(tierRuntimeRoot, stageName, changeName)
|
|
1120
|
+
if (priorRound) {
|
|
1121
|
+
const blockMd = renderPriorRoundFindingsMd(priorRound)
|
|
1122
|
+
priorFactsMd = priorFactsMd ? priorFactsMd + blockMd : blockMd
|
|
1123
|
+
}
|
|
1124
|
+
} catch {}
|
|
1023
1125
|
promptText = promptText
|
|
1024
1126
|
.split('{REVIEW_TIER}').join(tier.tier)
|
|
1025
1127
|
.split('{REVIEW_TIER_REASON}').join(tier.reason)
|
|
1026
1128
|
.split('{STAGE_REVIEW_RUN_ID}').join(reviewRunId)
|
|
1027
1129
|
.split('{REVIEW_JSON_CONTRACT}').join(reviewContractMd)
|
|
1130
|
+
.split('{PRIOR_REVIEW_FACTS}').join(priorFactsMd)
|
|
1028
1131
|
} catch (e) {
|
|
1029
1132
|
// 降级 self,避免 prompt 残留占位符
|
|
1030
1133
|
promptText = promptText
|
|
@@ -1032,6 +1135,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1032
1135
|
.split('{REVIEW_TIER_REASON}').join('分级异常降级 self: ' + e.message)
|
|
1033
1136
|
.split('{STAGE_REVIEW_RUN_ID}').join('review-unknown')
|
|
1034
1137
|
.split('{REVIEW_JSON_CONTRACT}').join('(review 契约注入失败,按 schemaVersion=1 + reviewType + verdicts∈pass/fail/cannot_verify + reviewedFiles + docHash=主文档 sha256 产出)')
|
|
1138
|
+
.split('{PRIOR_REVIEW_FACTS}').join('')
|
|
1035
1139
|
}
|
|
1036
1140
|
}
|
|
1037
1141
|
|
|
@@ -1129,13 +1233,14 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1129
1233
|
// 推荐组合、无从否决——故在 prompt 时点经 {EVIDENCE_AUTO_RECOMMENDATION} 把 task-11
|
|
1130
1234
|
// resolveTestStrategy 的 evidence_auto_recommendation.summary 渲染进 step prompt(含推荐
|
|
1131
1235
|
// 理由、降级注记与「可在 verify-result.md 否决并改跑全量」路径),供用户否决(FR-11)。
|
|
1132
|
-
// 仅 test_strategy=evidence-auto 且 recommendation
|
|
1133
|
-
//
|
|
1236
|
+
// 仅 test_strategy=evidence-auto 且 recommendation 非空时渲染推荐;full/module/skip/已配置
|
|
1237
|
+
// 替换为空串零输出;**未配置**且 commands.test 已配时渲染 test_strategy 预检提示(2026-09-15
|
|
1238
|
+
// 复盘:环境硬伤的全量实测打回返工,提前在 prompt 时点给配置指引)。fail-soft:读取/解析异常降级单行说明不抛,
|
|
1134
1239
|
// 绝不阻断 verify prompt 输出(对齐上方 {WORKTREE_BASELINE_INFO} 注入先例)。
|
|
1135
1240
|
if (stageName === 'verify' && promptText.includes('{EVIDENCE_AUTO_RECOMMENDATION}')) {
|
|
1136
1241
|
let eaInjected = ''
|
|
1137
1242
|
try {
|
|
1138
|
-
const { resolveTestStrategy } = await import('../verify-postcheck.js')
|
|
1243
|
+
const { resolveTestStrategy, extractTestCommand, extractTestStrategy } = await import('../verify-postcheck.js')
|
|
1139
1244
|
const eaSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1140
1245
|
// 信任边界:local.yaml 只读 specBase(平台 specRoot / 漂移锚定主仓 / cwd 本地),与
|
|
1141
1246
|
// runVerifyTestCheck 的 specBase 来源同口径,不读 agent 可写的 worktree 副本。
|
|
@@ -1144,7 +1249,20 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1144
1249
|
const eaChangeDir = changeName ? join(eaSpecBase, 'changes', changeName) : null
|
|
1145
1250
|
const resolution = resolveTestStrategy({ yamlText: eaYamlText, changeDir: eaChangeDir })
|
|
1146
1251
|
const rec = resolution.evidence_auto_recommendation
|
|
1147
|
-
if (rec && rec.summary)
|
|
1252
|
+
if (rec && rec.summary) {
|
|
1253
|
+
eaInjected = rec.summary
|
|
1254
|
+
} else if (eaYamlText && extractTestCommand(eaYamlText) && !extractTestStrategy(eaYamlText)) {
|
|
1255
|
+
// test_strategy 预检提示(2026-09-15 复盘教训「test_strategy 该在进 verify 前配好,
|
|
1256
|
+
// 而不是被 verify-test gate 打回后才补」):已配 commands.test 但顶层 test_strategy
|
|
1257
|
+
// 未设(缺省=全量实测对账),prompt 时点先给配置指引——环境注定跑不了全量时省一轮
|
|
1258
|
+
// 「--done 实测打回 → 补配置 → 重跑」返工
|
|
1259
|
+
eaInjected = '【test_strategy 预检提示】local.yaml 已配置 commands.test 但未设顶层 test_strategy(缺省=全量实测对账)。若该命令在当前环境无法真实执行(框架/环境硬伤,如测试框架硬编码 skip、依赖外部服务),先在 local.yaml 显式配 `test_strategy: skip`(或 `module` + modules 映射收窄)并注释理由再继续——避免 verify --done 实测对账打回后返工。'
|
|
1260
|
+
}
|
|
1261
|
+
// 等效验证先例注入(verify-precedent-only-in-prose,2026-09-15 EHS 实证):先例与
|
|
1262
|
+
// 预检提示同挂点追加——标准命令不可用但已有验证过的等效口径时,指引优先配进
|
|
1263
|
+
// commands.test 实测对账而非 skip 放行。
|
|
1264
|
+
const _precHint = renderVerifyPrecedentHint(parseVerifyPrecedents(eaYamlText))
|
|
1265
|
+
if (_precHint) eaInjected = eaInjected ? `${eaInjected}\n\n${_precHint}` : _precHint
|
|
1148
1266
|
} catch (e) {
|
|
1149
1267
|
eaInjected = `(evidence-auto 推荐注入失败:${e.message}——请读 .sillyspec/local.yaml 确认 test_strategy;--done 时 CLI 仍会按 resolveTestStrategy 实测对账,推荐不可用时可显式设 test_strategy: full/module)`
|
|
1150
1268
|
}
|
|
@@ -1194,6 +1312,42 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1194
1312
|
promptText = promptText.split('{SCOPE_AUDIT_TABLE}').join(scopeTable)
|
|
1195
1313
|
}
|
|
1196
1314
|
|
|
1315
|
+
// {HANDOVER_SUMMARY}(2026-09-17-pass-cap-semantics task-04,FR-06 注入态):archive「确认
|
|
1316
|
+
// 归档」步的移交项清单注入——读 changes/<name>/verify-facts.json 的 facts.handover(task-01
|
|
1317
|
+
// 产出 { count, items[] },items 行含 type/item/condition/severity),blocking 置顶 + severity
|
|
1318
|
+
// 标注 + 封顶渲染(maxRows 8,对齐 {SCOPE_AUDIT_TABLE} 60/量级收窄——移交项是裁决面不是
|
|
1319
|
+
// 对账面),表尾指路 verify-facts.json 看全量。fail-soft:handover 段缺失/零条目/facts.json
|
|
1320
|
+
// 缺失 → 空串(正常态零输出);读取异常降级单行指引——注入永不阻断归档 prompt 输出
|
|
1321
|
+
//(与门阻断语义分离,对齐 {SCOPE_AUDIT_TABLE} 注入先例)。
|
|
1322
|
+
if (stageName === 'archive' && promptText.includes('{HANDOVER_SUMMARY}')) {
|
|
1323
|
+
let handoverBlock = ''
|
|
1324
|
+
try {
|
|
1325
|
+
const hsSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1326
|
+
const hsFactsPath = changeName ? join(hsSpecBase, 'changes', changeName, 'verify-facts.json') : null
|
|
1327
|
+
if (hsFactsPath && existsSync(hsFactsPath)) {
|
|
1328
|
+
const hsFacts = JSON.parse(readFileSync(hsFactsPath, 'utf8'))
|
|
1329
|
+
const hsItems = (hsFacts && hsFacts.handover && Array.isArray(hsFacts.handover.items)) ? hsFacts.handover.items : []
|
|
1330
|
+
if (hsItems.length > 0) {
|
|
1331
|
+
const HS_MAX_ROWS = 8
|
|
1332
|
+
const sevOf = (it) => (it && it.severity === 'blocking') ? 'blocking' : 'advisory'
|
|
1333
|
+
const sorted = [...hsItems].sort((a, b) => (sevOf(a) === 'blocking' ? 0 : 1) - (sevOf(b) === 'blocking' ? 0 : 1))
|
|
1334
|
+
const rows = sorted.slice(0, HS_MAX_ROWS).map((it) => {
|
|
1335
|
+
const cond = it && it.condition ? ` | 条件: ${it.condition}` : ''
|
|
1336
|
+
return `- [${sevOf(it)}] ${(it && it.type) || 'unknown'} | ${(it && it.item) || ''}${cond}`
|
|
1337
|
+
})
|
|
1338
|
+
handoverBlock = [
|
|
1339
|
+
`【移交项清单(CLI 机械注入,共 ${hsItems.length} 条,blocking 置顶)】`,
|
|
1340
|
+
...rows,
|
|
1341
|
+
...(sorted.length > HS_MAX_ROWS ? [`……(其余 ${sorted.length - HS_MAX_ROWS} 条略——全量见 verify-facts.json)`] : []),
|
|
1342
|
+
].join(String.fromCharCode(10))
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
} catch (e) {
|
|
1346
|
+
handoverBlock = '(移交项清单注入失败:' + (e && e.message ? e.message : e) + '——回退读 changes/<变更名>/verify-facts.json 的 handover 段核对后再确认归档,注入失败不阻断归档)'
|
|
1347
|
+
}
|
|
1348
|
+
promptText = promptText.split('{HANDOVER_SUMMARY}').join(handoverBlock)
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1197
1351
|
// 注入模块上下文(brainstorm/plan/execute 阶段全步 + quick 首步「理解任务」——刀①:quick
|
|
1198
1352
|
// step1 原让 agent cat module-map 再挑模块卡读,改为按任务描述匹配后注入,基于 Module Context Index)
|
|
1199
1353
|
const quickFirstStep = stageName === 'quick' && step && step.name === '理解任务'
|
|
@@ -1352,10 +1506,15 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1352
1506
|
// archive-batch-31-tool-notes ②:通用 --done 模板不带该 flag,agent 照抄执行撞
|
|
1353
1507
|
// 「请添加 --confirm」确认门,误以为参数没带。
|
|
1354
1508
|
const confirmFlag = step.requiresConfirm === true ? ' --confirm' : ''
|
|
1355
|
-
// auto driver(2026-09-08-auto-driver D-002/P1
|
|
1509
|
+
// auto driver(2026-09-08-auto-driver D-002/P1-Ⅲ):auto 模式下命令一律 run auto 形态——
|
|
1356
1510
|
// 与 SS-META 的 doneCommand 同源(同一 cmdStage 变量),消灭「正文 run <stage> / 元数据 run auto」双命令
|
|
1357
1511
|
const cmdStage = autoMeta ? 'auto' : stageName
|
|
1358
|
-
|
|
1512
|
+
// 意图断言(坑 execute-concurrent-done-skips-next-wave 终解):execute 的 Wave 步骤是动态
|
|
1513
|
+
// 步骤表,并发 --done 会落到错误 Wave 且用旧摘要静默完成——完成命令带 --step "<本步名>"
|
|
1514
|
+
//(CLI 校验一致才放行),照抄命令即得并发防护。仅 execute 注入(quick/静态阶段测试断言
|
|
1515
|
+
// 提示格式,且静态步骤名稳定无漂移风险)。
|
|
1516
|
+
const stepAssertFlag = (stageName === 'execute' && step?.name) ? ` --step "${step.name}"` : ''
|
|
1517
|
+
const doneCommand = `sillyspec run ${cmdStage} --done${confirmFlag}${stepAssertFlag}${changeFlag} --output "你的摘要"`
|
|
1359
1518
|
if (requiresWait) {
|
|
1360
1519
|
console.log(`本步骤必须等待用户输入,不能直接 --done:`)
|
|
1361
1520
|
console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
|
package/src/run/shared.js
CHANGED
|
@@ -124,6 +124,12 @@ export function resolvePromptIncludes(text) {
|
|
|
124
124
|
* home 拒绝守卫:home 下的 .sillyspec 恒不命中——历史污染源(smoke 测试在 home 下临时目录
|
|
125
125
|
* 跑 CLI,_ensureDB 读路径即建库,~/.sillyspec 长出一整套平行进度库后,任何 home 子目录
|
|
126
126
|
* 跑命令都会向上撞它,污染自我延续)。撞 home 即停,回退 cwd/.sillyspec(子目录自建)。
|
|
127
|
+
* tmpdir 硬边界(坑 temp-root-stray-sillyspec,2026-09-16-friction5-hardening verify 移交项①
|
|
128
|
+
* 实证):某次 cwd=%TEMP% 直系的调用在 Temp 根遗留游离 .sillyspec 后,所有 temp 夹具的祖先
|
|
129
|
+
* 链都被它劫持(agent-automation-batch4『add frontend 建档』/feedback-batch2 两测试假败的真
|
|
130
|
+
* 根因——specBase 解析到 Temp\.sillyspec,夹具内断言读自身路径 ENOENT)。temp 下合法的
|
|
131
|
+
* .sillyspec 只会在夹具自身内部:起点在 tmpdir 子树内时,tmpdir 层及其上永不检查、走到即停
|
|
132
|
+
* (比 home 守卫更硬:home 是「跳过一层继续走」,tmp 是「整段封顶」——tmpdir 之上无合法项目根)。
|
|
127
133
|
* @param {string} cwd
|
|
128
134
|
* @param {object} [opts]
|
|
129
135
|
* @param {string} [opts.specDir] - 用户指定的 specDir(通过 --spec-dir 或 --spec-root)
|
|
@@ -132,11 +138,15 @@ export function resolvePromptIncludes(text) {
|
|
|
132
138
|
export function resolveSpecDir(cwd, opts = {}) {
|
|
133
139
|
if (opts.specDir) return resolve(opts.specDir)
|
|
134
140
|
const home = os.homedir()
|
|
141
|
+
const tmp = os.tmpdir()
|
|
135
142
|
const origin = resolve(cwd)
|
|
136
143
|
const originBelowHome = origin.startsWith(home + sep) // 严格子目录,不含 home 自身
|
|
144
|
+
const originInTmp = origin === tmp || origin.startsWith(tmp + sep) // 含 tmpdir 自身
|
|
137
145
|
let dir = origin
|
|
138
146
|
let passedHome = false
|
|
139
147
|
while (true) {
|
|
148
|
+
// tmpdir 硬边界:起点在 tmpdir 子树内时,走到 tmpdir 层即封顶(该层及其上是游离库温床,永不命中)
|
|
149
|
+
if (originInTmp && dir === tmp) break
|
|
140
150
|
// home 拒绝守卫:起点在 home 子树内时,home 层不检查 .sillyspec,
|
|
141
151
|
// 防止 home 子目录遍历经过 home 误命中 ~/.sillyspec;
|
|
142
152
|
// passedHome:经过 home 后不再检查(覆盖遍历到真实 home 的场景)。
|
|
@@ -635,6 +645,71 @@ export function checkPlatformManaged(cwd) {
|
|
|
635
645
|
}
|
|
636
646
|
}
|
|
637
647
|
|
|
648
|
+
/**
|
|
649
|
+
* 平台模式产物落点指针(坑 platform-docs-dual-location,2026-09-15 EHS 生产实证:
|
|
650
|
+
* 变更 9 文档落 daemon specs 目录,工作树 .sillyspec/ 只剩旧变更残留、worktree 无
|
|
651
|
+
* .sillyspec——人类在工作树里找变更文档扑空,只有平台 changes files API 能读,双位置
|
|
652
|
+
* 造成「文件去哪了」困惑)。阶段 --done 完成时在工作树 .sillyspec/ 落一份人类可读指针
|
|
653
|
+
* PLATFORM-DOCS-POINTER.md:specRoot/runtimeRoot 物理路径 + workspaceId + 本变更文档
|
|
654
|
+
* 清单(.md 文件名+mtime + tasks/ 计数)+ 获取方式提示。
|
|
655
|
+
*
|
|
656
|
+
* 口径与 writePlatformPointer 三写同款豁免:本地模式(无 specRoot/runtimeRoot)零行为;
|
|
657
|
+
* 自指回环(repo-native junction 指回 cwd/.sillyspec)不写;temp 残留形态(specRoot 在
|
|
658
|
+
* 系统 temp 而项目不在)不写。fail-soft:任何写失败只 warn 不抛(对齐 user-inputs.md
|
|
659
|
+
* 追加先例)——指针是便利件,绝不阻断阶段完成。
|
|
660
|
+
*
|
|
661
|
+
* @param {string} cwd 项目根(指针落 cwd/.sillyspec/)
|
|
662
|
+
* @param {string|null} changeName 变更名(可空——无变更上下文时只写根路径头部)
|
|
663
|
+
* @param {{specRoot?: string|null, runtimeRoot?: string|null, workspaceId?: string|null}} [platformOpts]
|
|
664
|
+
* @param {string|null} [stageName] 触发阶段(展示用)
|
|
665
|
+
* @returns {{written: boolean, reason?: string, path?: string, error?: string}}
|
|
666
|
+
*/
|
|
667
|
+
export function writePlatformDocsPointer(cwd, changeName, platformOpts = {}, stageName = null) {
|
|
668
|
+
try {
|
|
669
|
+
const specRoot = platformOpts?.specRoot || null
|
|
670
|
+
const runtimeRoot = platformOpts?.runtimeRoot || null
|
|
671
|
+
if (!specRoot && !runtimeRoot) return { written: false, reason: 'local-mode' }
|
|
672
|
+
if (specRoot && isSelfReferentialSpecRoot(cwd, specRoot)) return { written: false, reason: 'self-referential' }
|
|
673
|
+
if (isTempResidueSpecRoot(cwd, specRoot)) return { written: false, reason: 'temp-residue' }
|
|
674
|
+
const localSpecDir = join(cwd, '.sillyspec')
|
|
675
|
+
mkdirSync(localSpecDir, { recursive: true })
|
|
676
|
+
const ts = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
677
|
+
const lines = [
|
|
678
|
+
'<!-- sillyspec 平台模式产物落点指针(CLI 在阶段 --done 完成时自动维护;本地模式不生成本文件) -->',
|
|
679
|
+
'# 变更文档实际落点(工作树 .sillyspec/ 不是本仓权威数据面)',
|
|
680
|
+
'',
|
|
681
|
+
`- 平台 spec 根(proposal/design/verify-result 等变更文档): ${specRoot || '(未指定)'}`,
|
|
682
|
+
`- 运行时根(worktrees/contract-artifacts/verify-logs): ${runtimeRoot || '(未指定)'}`,
|
|
683
|
+
platformOpts?.workspaceId ? `- 工作区 ID: ${platformOpts.workspaceId}` : null,
|
|
684
|
+
`- 最近更新: ${stageName || '?'} 阶段完成 @ ${ts}${changeName ? `(change: ${changeName})` : ''}`,
|
|
685
|
+
].filter(Boolean)
|
|
686
|
+
const changeDir = (specRoot && changeName) ? join(specRoot, 'changes', changeName) : null
|
|
687
|
+
if (changeDir && existsSync(changeDir)) {
|
|
688
|
+
const docs = []
|
|
689
|
+
for (const f of readdirSync(changeDir)) {
|
|
690
|
+
if (!f.endsWith('.md')) continue
|
|
691
|
+
try {
|
|
692
|
+
const mtime = statSync(join(changeDir, f)).mtime.toLocaleString('zh-CN', { hour12: false })
|
|
693
|
+
docs.push(`- ${f}(mtime ${mtime})`)
|
|
694
|
+
} catch { docs.push(`- ${f}`) }
|
|
695
|
+
}
|
|
696
|
+
const tasksDir = join(changeDir, 'tasks')
|
|
697
|
+
if (existsSync(tasksDir)) {
|
|
698
|
+
const cards = readdirSync(tasksDir).filter(f => f.endsWith('.md'))
|
|
699
|
+
if (cards.length > 0) docs.push(`- tasks/(${cards.length} 张任务卡)`)
|
|
700
|
+
}
|
|
701
|
+
if (docs.length > 0) lines.push('', `## 本变更文档(${changeName})`, ...docs)
|
|
702
|
+
}
|
|
703
|
+
lines.push('', '获取方式:直接读上行物理路径,或经平台 changes files API(/changes/{id}/files/raw?path=...)逐文件下载。')
|
|
704
|
+
const pointerPath = join(localSpecDir, 'PLATFORM-DOCS-POINTER.md')
|
|
705
|
+
writeFileSync(pointerPath, lines.join('\n') + '\n')
|
|
706
|
+
return { written: true, path: pointerPath }
|
|
707
|
+
} catch (e) {
|
|
708
|
+
console.warn('⚠️ 平台产物落点指针写入失败(不阻断):', e && e.message ? e.message : e)
|
|
709
|
+
return { written: false, reason: 'error', error: e && e.message }
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
638
713
|
/**
|
|
639
714
|
* 自指 specRoot 判定(repo-native junction 回环检测,变更 2026-08-23-repo-native-spec-backfill)。
|
|
640
715
|
*
|
|
@@ -1035,7 +1110,12 @@ export function isQuickMetadata(p, linkedChanges = []) {
|
|
|
1035
1110
|
const file = normalizeGitPath(p)
|
|
1036
1111
|
if (file.startsWith('.sillyspec/quicklog/')
|
|
1037
1112
|
|| file.startsWith('.sillyspec/.runtime/')
|
|
1038
|
-
|
|
1113
|
+
// 知识库整体白名单(2026-09-15 用户实测反馈:登记知识条目是 quick 收尾常规动作,此前
|
|
1114
|
+
// INDEX/known-issues/patterns/conventions/decisions 非元数据 → 危险门 blocked 只能 --force-baseline,
|
|
1115
|
+
// 且 perFileNotes 强制 --file-notes 覆盖):knowledge/ 是 CLI 自管面(classify append-only +
|
|
1116
|
+
// validate 守门),与 quicklog 同级。可追溯性靠 quicklog 结构化条目 + git diff,不靠文件行。
|
|
1117
|
+
// 前缀带尾斜杠:knowledge-base/ 等相近目录不命中。
|
|
1118
|
+
|| file.startsWith('.sillyspec/knowledge/')
|
|
1039
1119
|
|| (/^\.sillyspec\/docs\/[^/]+\/modules\/[^/]+\.md$/.test(file))
|
|
1040
1120
|
|| (/^\.sillyspec\/docs\/[^/]+\/modules\/_module-map\.yaml$/.test(file))) return true
|
|
1041
1121
|
if (file.startsWith('.sillyspec/changes/')) {
|