sillyspec 3.28.4 → 3.28.6
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/AGENTS.md +1 -0
- package/package.json +1 -1
- package/results-review-tier.md +18 -0
- package/sa-rename-probe/src/new.js +4 -0
- package/src/agent-session-log.js +4 -3
- package/src/archive-delta.js +2 -2
- package/src/change-list.js +11 -1
- package/src/commit-suggest.js +52 -1
- package/src/config-cat.js +6 -1
- package/src/config-schema.js +13 -0
- package/src/decision-distill.js +53 -7
- package/src/decisions-io.js +96 -0
- package/src/design-facts.js +3 -0
- package/src/dispatch/probe.js +31 -8
- package/src/docs-check.js +77 -8
- package/src/friction-tally.js +221 -0
- package/src/index.js +430 -18
- package/src/mcp-server.js +144 -0
- package/src/module-impact.js +8 -1
- package/src/modules.js +46 -34
- package/src/progress/change-registry.js +20 -0
- package/src/progress.js +16 -1
- package/src/quicklog.js +60 -6
- package/src/review-dispatch.js +377 -0
- package/src/run/archive-distill.js +48 -0
- package/src/run/command.js +35 -5
- package/src/run/complete-handlers.js +136 -3
- package/src/run/complete.js +170 -2
- package/src/run/gate-snapshot.js +159 -0
- package/src/run/gates.js +166 -69
- package/src/run/prompt.js +54 -9
- package/src/run/quick-audit.js +78 -16
- package/src/run/shared.js +138 -7
- package/src/run/stage.js +31 -2
- package/src/run/verify-quality-scan.js +383 -0
- package/src/scope-audit.js +941 -0
- package/src/sillyhub-mcp/client.js +58 -3
- package/src/stage-review-checklist.js +44 -0
- package/src/stage-review.js +122 -4
- package/src/stages/archive.js +57 -164
- package/src/stages/brainstorm.js +8 -12
- package/src/stages/execute.js +13 -10
- package/src/stages/plan.js +3 -10
- package/src/stages/verify.js +14 -33
- package/src/sync.js +51 -10
- package/src/task-review.js +108 -16
- package/src/taskcard.js +46 -3
- package/src/validate-artifacts.js +163 -0
- package/src/verify-postcheck.js +90 -10
- package/src/verify-probes.js +134 -12
- package/src/worktree-apply.js +119 -14
- package/templates/agents-instruction.md +1 -0
package/src/run/gates.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { basename, join, relative, dirname } from 'node:path'
|
|
23
23
|
import { createHash } from 'node:crypto'
|
|
24
|
+
import { execFileSync } from 'node:child_process'
|
|
24
25
|
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
25
26
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
26
27
|
import { triggerSync, resolveChangeDir, resolveRuntimeRoot } from './shared.js'
|
|
@@ -29,6 +30,7 @@ import { handleScanStageCompleted, handleExecuteWorktreeCleanup } from './comple
|
|
|
29
30
|
import { detectConcurrentChanges, formatConcurrentWarning } from './concurrent-detect.js'
|
|
30
31
|
import { stageRegistry } from '../stages/index.js'
|
|
31
32
|
import { normalizeTaskId } from '../taskcard.js'
|
|
33
|
+
import { recordFrictionEvent } from '../friction-tally.js'
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* 从任务注册表(tasks.md)提取全部 task id(task-XX)——符号影响面覆盖度校验用。
|
|
@@ -505,7 +507,11 @@ function rollbackStageCompletion(stageData, steps, currentIdx) {
|
|
|
505
507
|
* 漏写 triggerSync / 写错 return 结构导致行为分裂。返回 nextPendingIdx=currentIdx,
|
|
506
508
|
* 让上层走「完成但不推进」分支,--done 被拒、agent 修复产物后重跑。
|
|
507
509
|
*/
|
|
508
|
-
function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts) {
|
|
510
|
+
function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, friction) {
|
|
511
|
+
// friction 尾参(friction-signal-hint task-02):gate 失败摩擦埋点标签 {type, detail}——各调用点
|
|
512
|
+
// 按所属 gate 段传来源标签(审查两段走 review_rejected),未传兜底通用 gate-cascade。
|
|
513
|
+
// recordFrictionEvent 自带静默降级(非法类型/写失败返回 null),不影响回滚/返回语义。
|
|
514
|
+
recordFrictionEvent({ cwd, changeName, platformOpts, ...(friction || { type: 'gate_rollback', detail: 'gate-cascade' }) })
|
|
509
515
|
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
510
516
|
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
511
517
|
pm._write(cwd, progress, changeName)
|
|
@@ -556,12 +562,14 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
556
562
|
// ── verify 收尾前置:facts slot-backfill + verifyStartAt(2026-09-08-ir-verify-facts FR-03)──
|
|
557
563
|
// 执行次序(design Phase 3):backfill(结论/证据/回执槽固化)先行 → runValidators(结论门/
|
|
558
564
|
// 集成证据门读得到固化值与 context.verifyStartAt)→ verify 专属块(test 实测 → tests 段二次
|
|
559
|
-
// 回填 → cannot_verify 硬门)。verifyStartAt
|
|
560
|
-
//
|
|
565
|
+
// 回填 → cannot_verify 硬门)。verifyStartAt 锚点(2026-09-10 用户反馈②放宽):execute 行
|
|
566
|
+
// **started_at**(证据合法产自 execute 或 verify 两窗口)→ 缺列/旧行退 completed_at →
|
|
567
|
+
// 再缺走 R-05 design created_at fallback。
|
|
561
568
|
let verifyStartAtIso = null
|
|
562
569
|
if (stageName === 'verify') {
|
|
563
570
|
try {
|
|
564
|
-
verifyStartAtIso = pm.
|
|
571
|
+
verifyStartAtIso = (pm.getStageStartedAt && pm.getStageStartedAt(cwd, changeName, 'execute'))
|
|
572
|
+
|| pm.getStageCompletedAt(cwd, changeName, 'execute')
|
|
565
573
|
} catch { verifyStartAtIso = null }
|
|
566
574
|
try {
|
|
567
575
|
const verifyMdPath = join(specBase, 'changes', changeName, 'verify-result.md')
|
|
@@ -593,7 +601,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
593
601
|
// 产物校验失败必须阻断完成 —— 否则 validator 形同虚设,
|
|
594
602
|
// verify 会带着 FAIL/缺 verify-result.md 被 ✅ 标记完成(历史教训)。
|
|
595
603
|
// plan/execute 的专项契约校验(下方)在产物齐全后才需要继续跑,故此处先 return。
|
|
596
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
604
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'validators' })
|
|
597
605
|
}
|
|
598
606
|
if (contractResult.warnings.length > 0) {
|
|
599
607
|
console.warn(`\n⚠️ 阶段 ${stageName} 校验警告:`)
|
|
@@ -622,10 +630,39 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
622
630
|
// 自报告 PASS 但实测失败 → 阻断(防止"文案通过"绕过验证)。
|
|
623
631
|
if (stageName === 'verify') {
|
|
624
632
|
const { runVerifyTestCheck, printVerifyTestCheck } = await import('../verify-postcheck.js')
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
633
|
+
// P0-1 指纹复用(noai-ir-roadmap §3 前置一·防重复跑):noAI 质量扫描步已实测且代码指纹
|
|
634
|
+
// 未变 → 直接复用免重跑(长套件 2~10 分钟不再跑两遍);无记录/失配/git 不可用 → 照旧
|
|
635
|
+
// 亲测(本路径语义与产物逐字不变)。
|
|
636
|
+
const { loadReusableQualityScan } = await import('./verify-quality-scan.js')
|
|
637
|
+
const reusableScan = loadReusableQualityScan({ specBase, cwd, changeName })
|
|
638
|
+
// ── 隔离快照定向跑(2026-09-10 驾驭小结第八批,用户第二次真实阻塞:verify 实测门跑
|
|
639
|
+
// main 工作区,多会话并发任何人的 WIP 都能弄红别人的门):HEAD 干净基线 + 本变更文件集
|
|
640
|
+
// overlay(worktree meta 在则源=worktree 根——等价「--worktree 定向跑本变更分支内容」)+
|
|
641
|
+
// 变更文档随快照(test_strategy 消费)。并行会话在途文件物理不进快照、不参与本门判定。
|
|
642
|
+
// 快照基建失败 → 回退主仓现行为(失败路径沿用 renderVerifyTestAttribution 归因提示)。
|
|
643
|
+
let verifyGateSnap = null
|
|
644
|
+
if (!process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF) {
|
|
645
|
+
try {
|
|
646
|
+
const { createVerifyGateSnapshot } = await import('./gate-snapshot.js')
|
|
647
|
+
verifyGateSnap = await createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts })
|
|
648
|
+
if (verifyGateSnap) {
|
|
649
|
+
console.log(`🧪 Verify 实测隔离快照:HEAD + 本变更 ${verifyGateSnap.changeFileCount} 个文件${verifyGateSnap.sourceRoot ? '(overlay 自 worktree——本变更分支内容定向跑)' : ''}(并行会话脏文件不参与门判定)`)
|
|
650
|
+
}
|
|
651
|
+
} catch { /* 快照链路异常 → 主仓现行为 */ }
|
|
652
|
+
}
|
|
653
|
+
const gateCwd = verifyGateSnap ? verifyGateSnap.snapshotRoot : cwd
|
|
654
|
+
const gateSpecBase = verifyGateSnap ? join(verifyGateSnap.snapshotRoot, '.sillyspec') : specBase
|
|
655
|
+
let testCheck
|
|
656
|
+
if (reusableScan && reusableScan.testResult) {
|
|
657
|
+
testCheck = reusableScan.testResult
|
|
658
|
+
console.log(`\n♻️ Verify 测试对账:复用 noAI 质量扫描步的实测结果(代码指纹匹配,免重跑;实测于 ${reusableScan.ranAt || '本变更 verify 期间'}${testCheck.resultPath ? `,台账 ${testCheck.resultPath}` : ''})`)
|
|
659
|
+
printVerifyTestCheck(testCheck)
|
|
660
|
+
} else {
|
|
661
|
+
// 测试实测是同步 execSync,长套件可跑 2~10min 且中途无输出——先预告避免 agent 误判卡死
|
|
662
|
+
console.log(`\n⏳ Verify 测试对账:CLI 亲自执行 local.yaml 的 commands.test(同步,耗时可能较长,请等待…)`)
|
|
663
|
+
testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName, ctx })
|
|
664
|
+
printVerifyTestCheck(testCheck)
|
|
665
|
+
}
|
|
629
666
|
// tests 段二次回填(2026-09-08-ir-verify-facts FR-03 次序:实测在 runValidators 之后,
|
|
630
667
|
// tests 快照此刻才可得;不参与门禁——供 P3d 追溯)
|
|
631
668
|
try {
|
|
@@ -642,56 +679,35 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
642
679
|
}
|
|
643
680
|
} catch { /* fail-soft:回填失败不影响门禁 */ }
|
|
644
681
|
if (testCheck.status === 'failed') {
|
|
682
|
+
if (verifyGateSnap) { try { verifyGateSnap.cleanup() } catch {} ; verifyGateSnap = null }
|
|
645
683
|
console.error('\n❌ verify 阶段被阻断:verify-result.md 自报告通过,但 CLI 实测测试失败。')
|
|
646
|
-
// 并行 WIP 归因鉴别(坑 verify-reconcile-foreign-wip
|
|
647
|
-
//
|
|
684
|
+
// 并行 WIP 归因鉴别(坑 verify-reconcile-foreign-wip):单一实现迁至 verify-quality-scan.js
|
|
685
|
+
// renderVerifyTestAttribution(P0-1 前置二:noAI 质量扫描失败路径与本 --done 路径共用,
|
|
686
|
+
// 输出逐字保持)——实测跑在主仓共享工作区,并行会话在途 WIP 物理在场,他者文件命中
|
|
687
|
+
// 声明集时提示复验归因(含判据②生成产物旧基线 / verify-attr2 双过滤),不静默背锅。
|
|
648
688
|
try {
|
|
649
|
-
const {
|
|
650
|
-
|
|
651
|
-
const foreignMap = collectForeignDeclaredFiles(cwd, changeName, { specBase, runtimeRoot: resolveRuntimeRoot(platformOpts, specBase) })
|
|
652
|
-
// dirty 集提前算好:归因提示①(他者在途)与②(生成产物旧基线)共用
|
|
653
|
-
const dirty = (gitQuiet(cwd, ['diff', '--name-only', 'HEAD']) || '')
|
|
654
|
-
.split('\n').filter(Boolean).map(f => f.replace(/\\/g, '/'))
|
|
655
|
-
const dirtySet = new Set(dirty)
|
|
656
|
-
if (foreignMap.size > 0) {
|
|
657
|
-
const hitForeign = dirty.filter(f => foreignMap.has(f))
|
|
658
|
-
if (hitForeign.length > 0) {
|
|
659
|
-
console.error(` ℹ️ 归因提示:主仓检出 ${hitForeign.length} 个并行会话声明的在途文件(${hitForeign.slice(0, 5).join(', ')}${hitForeign.length > 5 ? ' 等' : ''})——实测失败可能混入他者 WIP 而非本变更问题。待其提交/收尾后复验,仍失败才是本变更的。`)
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
// 第二判据(坑 derived-artifact-stale-baseline):本变更 apply 的文件(apply-pathspec 落盘)
|
|
663
|
-
// 与主仓近期提交重叠——本变更旧基线的生成产物可能覆盖了已合入内容,先在新基线重跑生成命令再复验。
|
|
664
|
-
// 坑 verify-attr2-foreign-wip-misdirection(2026-08-28 实证):并行会话在途/近期收尾文件
|
|
665
|
-
// 让重叠集虚高(51 文件),每轮误导 agent 重跑 gen:types,实际失败源是他者 WIP。两道过滤:
|
|
666
|
-
// - 必须 dirty:apply 落盘的旧内容此刻物理在场(verify gate 时本变更未提交,旧基线覆盖
|
|
667
|
-
// 必然表现为对 HEAD 的修改)——内容与 HEAD 一致时重跑生成命令改变不了本轮实测;
|
|
668
|
-
// - 剔除他者声明集:那是归因提示①的领地(待其收尾后复验),不是生成产物过期。
|
|
669
|
-
try {
|
|
670
|
-
const pathspecFile = join(resolveRuntimeRoot(platformOpts, specBase), `apply-pathspec-${changeName}.txt`)
|
|
671
|
-
if (changeName && existsSync(pathspecFile)) {
|
|
672
|
-
const ownFiles = new Set(readFileSync(pathspecFile, 'utf8').split('\n').map(l => l.trim()).filter(Boolean).map(f => f.replace(/\\/g, '/')))
|
|
673
|
-
if (ownFiles.size > 0) {
|
|
674
|
-
const recent = (gitQuiet(cwd, ['log', '-n', '10', '--name-only', '--pretty=format:']) || '')
|
|
675
|
-
.split('\n').map(l => l.trim().replace(/\\/g, '/')).filter(Boolean)
|
|
676
|
-
const overlap = [...new Set(recent)].filter(f => ownFiles.has(f))
|
|
677
|
-
const actionable = filterStaleBaselineOverlap(overlap, dirtySet, foreignMap)
|
|
678
|
-
if (actionable.length > 0) {
|
|
679
|
-
console.error(` ℹ️ 归因提示②:本变更有 ${actionable.length} 个文件与主仓最近 10 条提交重叠且旧内容仍在工作区(${actionable.slice(0, 5).join(', ')}${actionable.length > 5 ? ' 等' : ''})——若为生成产物(api-types 等),本变更可能在旧基线生成并覆盖了已合入内容;先在新基线重跑生成命令(如 gen:types)再复验。(重叠共 ${overlap.length} 个,其余未在工作区改动或属他者会话在途文件,已归提示①/无需处理)`)
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
} catch { /* 判据②失败不影响主流程 */ }
|
|
689
|
+
const { renderVerifyTestAttribution } = await import('./verify-quality-scan.js')
|
|
690
|
+
await renderVerifyTestAttribution({ cwd, changeName, specBase, runtimeRoot: resolveRuntimeRoot(platformOpts, specBase) })
|
|
684
691
|
} catch { /* 归因提示失败不影响阻断语义 */ }
|
|
685
692
|
console.error(' 请修复失败的测试并更新 verify-result.md 后重新完成此步骤。')
|
|
686
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
693
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-test' })
|
|
687
694
|
}
|
|
688
695
|
// lint 对账(2026-08-21 审查 CLI-1):test 侧"自报告 PASS 但实测失败→阻断"已闭环,
|
|
689
696
|
// lint 侧此前纯口头——CLI 亲自执行 commands.lint,advisory 起步(失败打印不阻断,观察期后升级)
|
|
690
697
|
const { runVerifyLintCheck, printVerifyLintCheck } = await import('../verify-postcheck.js')
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
698
|
+
// P0-1 指纹复用:noAI 质量扫描记录同指纹时 lint 一并复用(lint tally 只记真实执行次数)
|
|
699
|
+
let lintCheck = (reusableScan && reusableScan.lintResult) || null
|
|
700
|
+
if (lintCheck) {
|
|
701
|
+
console.log(`\n♻️ Verify lint 对账:复用 noAI 质量扫描步的实测结果(代码指纹匹配,免重跑)`)
|
|
702
|
+
} else {
|
|
703
|
+
lintCheck = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase })
|
|
704
|
+
if (lintCheck.status !== 'skipped') {
|
|
705
|
+
console.log(`\n⏳ Verify lint 对账:CLI 亲自执行 local.yaml 的 commands.lint…`)
|
|
706
|
+
}
|
|
694
707
|
}
|
|
708
|
+
// 快照收尾(test+lint 两检查都已完成;parity/删除探针走主仓 git 事实不依赖快照)
|
|
709
|
+
const usedSnapshot = Boolean(verifyGateSnap)
|
|
710
|
+
if (verifyGateSnap) { try { verifyGateSnap.cleanup() } catch {} ; verifyGateSnap = null }
|
|
695
711
|
printVerifyLintCheck(lintCheck)
|
|
696
712
|
// lint 硬门(2026-09-09 升硬:观察期 14 次 5 败全真阳性,pre-push 本就硬拦——fail-fast 前移;
|
|
697
713
|
// 逃生 SILLYSPEC_VERIFY_LINT_GATE=advisory)
|
|
@@ -703,8 +719,19 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
703
719
|
console.error(' 输出(末尾):')
|
|
704
720
|
for (const line of tail.split('\n')) console.error(` | ${line}`)
|
|
705
721
|
}
|
|
722
|
+
// 污染归属提示(2026-09-10 驾驭小结第八批建议②,对齐 test 侧 renderVerifyTestAttribution
|
|
723
|
+
// 先例):实测跑在主仓共享工作区(快照不可用/被 ENV 关闭的 fallback 路径)且存在非本变更
|
|
724
|
+
// 的未提交文件时,失败可能是并行会话污染——先做归属鉴定,不静默背锅。
|
|
725
|
+
if (!usedSnapshot) {
|
|
726
|
+
try {
|
|
727
|
+
const { renderVerifyTestAttribution } = await import('./verify-quality-scan.js')
|
|
728
|
+
console.error(' ℹ️ 本次实测跑在主仓共享工作区(未走隔离快照):')
|
|
729
|
+
await renderVerifyTestAttribution({ cwd, changeName, specBase, runtimeRoot: resolveRuntimeRoot(platformOpts, specBase) })
|
|
730
|
+
console.error(' 亦可跑 sillyspec scope-audit --change ' + changeName + ' 做污染归属鉴定(计划×实际三态全表)。')
|
|
731
|
+
} catch { /* 归因提示失败不影响阻断语义 */ }
|
|
732
|
+
}
|
|
706
733
|
console.error(' 修复后重跑 --done(进度不丢);确认要跳过实测请设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。')
|
|
707
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
734
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-lint' })
|
|
708
735
|
}
|
|
709
736
|
// 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
|
|
710
737
|
// 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
|
|
@@ -720,9 +747,19 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
720
747
|
// execute Task Review Gate 把 cannot_verify 任务的 evidence 落盘 verify-required-evidence.json。
|
|
721
748
|
// v2 槽优先分类核验(FR-02):blocked = missing 无豁免或 satisfied 核验不过 → 阻断 verify 完成
|
|
722
749
|
// (此前 advisory 只查「提及」——死链不闭环);无槽存量 md 降级 legacy 子串对账(warning 不阻断)。
|
|
723
|
-
const { runVerifyRequiredEvidenceCheck, printVerifyRequiredEvidenceCheck } = await import('../verify-postcheck.js')
|
|
750
|
+
const { runVerifyRequiredEvidenceCheck, printVerifyRequiredEvidenceCheck, trackVerifyResultRegression } = await import('../verify-postcheck.js')
|
|
724
751
|
const evidenceCheck = runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName, verifyStartAt: verifyStartAtIso })
|
|
725
752
|
printVerifyRequiredEvidenceCheck(evidenceCheck)
|
|
753
|
+
// verify-result.md 内容回退检测(2026-09-10 用户反馈③:报告被平台同步覆盖回旧版一次,
|
|
754
|
+
// 只能靠人眼发现重写):高水位指纹(hash+mtime)检出「内容变且 mtime 回退」→ ⚠️ 告警。
|
|
755
|
+
// 纯 advisory——真回退也由 agent 决定重写,CLI 只保证可见。
|
|
756
|
+
try {
|
|
757
|
+
const vr = trackVerifyResultRegression(specBase, changeName, join(specBase, 'changes', changeName, 'verify-result.md'))
|
|
758
|
+
if (vr.regressed) {
|
|
759
|
+
console.warn(`\n⚠️ verify-result.md 疑被覆盖回旧版(内容变化 + mtime 回退至 ${new Date(vr.prevMtime).toLocaleString('zh-CN')} 之前)——平台/daemon 按服务端旧版本回写的指纹。`)
|
|
760
|
+
console.warn(` 若你刚重写过请忽略本行(下次读取刷新高水位);否则当前盘上可能是旧内容,需对照记忆/git 重新补写。`)
|
|
761
|
+
}
|
|
762
|
+
} catch { /* 检测失败静默(advisory) */ }
|
|
726
763
|
if (evidenceCheck.status === 'blocked') {
|
|
727
764
|
console.error(`\n❌ verify 阶段被阻断:cannot_verify 证据账未闭环(${evidenceCheck.summary})。`)
|
|
728
765
|
for (const d of (evidenceCheck.detailed || [])) {
|
|
@@ -734,7 +771,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
734
771
|
console.error(` 修复:在 verify-result.md「## 证据账(cannot_verify 任务)」槽段逐 task 一行——`)
|
|
735
772
|
console.error(` - task-NN: satisfied | verifiedFiles: <精确路径>(CLI 核验:代码类 存在×mtime×diff 交集;日志/文档类豁免 diff)`)
|
|
736
773
|
console.error(` - 确实无法验证 → 填 missing 并加(豁免:<一句话理由>)后缀;部分满足 → partial + 已核验路径`)
|
|
737
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
774
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
738
775
|
}
|
|
739
776
|
// ── target_files 声明 ↔ 实际改动 对账(task-05 / ir-stage-p3a:②ERROR 阻断 / ③WARNING 放行)──
|
|
740
777
|
// 「计划落空」(task 卡声明没做)此前全盲区:review.json changedFiles 是 agent 手写不能当
|
|
@@ -760,7 +797,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
760
797
|
// 放行态),fail-soft 不影响 gate 判定;②类阻断回执同样留档供平台/审计消费
|
|
761
798
|
writeReconcileRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: reconcileEnvelope, result: reconcileCheck })
|
|
762
799
|
if (reconcileBlocked) {
|
|
763
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
800
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
764
801
|
}
|
|
765
802
|
// ── verify-result.md 探针预填段一致性抽查(task-03 / ir-stage-p3b D-002@v1 方案A)──
|
|
766
803
|
// 「#### 探针 N」机械预填段(verify-probes --init 生成)长在 agent 可编辑的正文里,其防篡改
|
|
@@ -789,7 +826,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
789
826
|
// 两检查间耦合;各写各的回执、按文件名订阅更干净。fail-soft 不影响 gate 判定
|
|
790
827
|
writeProbeConsistencyRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: probeEnvelope, result: probeCheck })
|
|
791
828
|
if (probeBlocked) {
|
|
792
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
829
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
793
830
|
}
|
|
794
831
|
// ── 行号漂移自动重锚(2026-09-09 §7-6:quick --done 已接,verify/archive 收尾补接)──
|
|
795
832
|
// 本变更 diff 中的 .md 文档经 autoReanchorDocRefs 定点重锚(fixable 唯一/优选命中才改 +
|
|
@@ -823,7 +860,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
823
860
|
console.error(`\n❌ verify 阶段被阻断:module-impact.md「更新结果」表存在 ${pendingRows.length} 个未清 pending/待办项(死信)`)
|
|
824
861
|
for (const row of pendingRows) console.error(` - ${row}`)
|
|
825
862
|
console.error(' 文档同步是 verify 的收尾义务:请完成模块文档同步并回填状态为 done/skipped(说明原因),再重新完成 verify。')
|
|
826
|
-
return rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
863
|
+
return rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
827
864
|
}
|
|
828
865
|
}
|
|
829
866
|
}
|
|
@@ -852,7 +889,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
852
889
|
for (const err of planValidation.errors) console.error(` - ${err}`)
|
|
853
890
|
console.error(`\n tasks.md/plan.md 不满足 execute 契约,请修复后重新完成此步骤。`)
|
|
854
891
|
// 阻断 completed
|
|
855
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
892
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'plan-execute-contract' })
|
|
856
893
|
}
|
|
857
894
|
if (planValidation.warnings.length > 0) {
|
|
858
895
|
console.warn(`\n⚠️ Plan contract 警告(不阻断完成):`)
|
|
@@ -913,13 +950,27 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
913
950
|
const reviewResult = gated.result
|
|
914
951
|
printStageReviewResult(reviewResult, { stage: stageName, reviewRunId, runtimeRoot, changeName })
|
|
915
952
|
if (!reviewResult.ok) {
|
|
916
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
953
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'review_rejected', detail: 'stage-review' })
|
|
954
|
+
}
|
|
955
|
+
// 降级自审留痕(2026-09-10 用户反馈①:PI agent 等宿主无 Agent tool,prompt 降级条款允许
|
|
956
|
+
// 当前 agent 自审产出 review.json)。放行但独立性折损必须可见:⚠️ 审计行区分降级 review
|
|
957
|
+
// 与真子代理 review(事后可审计),与 ⚖️/🔍 软归属同哲学——可见的折损优于静默的伪装。
|
|
958
|
+
// channel 识别(2026-09-10 通道优先序批次):reviewer.channel 结构化落款优先,兼容
|
|
959
|
+
// reviewerNotes 首行「降级:」约定;platform 通道(P2 review-dispatch 落地后)ℹ️ 留痕
|
|
960
|
+
// missionId 供追溯,其余通道正常放行。
|
|
961
|
+
const { classifyReviewerChannel } = await import('../stage-review.js')
|
|
962
|
+
const reviewChannel = classifyReviewerChannel(reviewResult.review)
|
|
963
|
+
if (reviewChannel === 'self') {
|
|
964
|
+
console.warn(`\n⚠️ Stage Review 降级自审(tier=independent 但宿主无 Agent tool):${stageName} review 按 prompt 降级条款由当前 agent 自审产出——独立性折损已留痕(reviewerNotes 首行「降级:」),结论应附源码锚点补偿,建议人工抽查关键结论。`)
|
|
965
|
+
} else if (reviewChannel === 'platform') {
|
|
966
|
+
const mid = reviewResult.review && reviewResult.review.reviewer && reviewResult.review.reviewer.missionId
|
|
967
|
+
console.log(`\nℹ️ Stage Review 平台派发(channel=platform${mid ? `,missionId=${mid}` : ',missionId 未落款'}):${stageName} review 由平台独立 worker 产出(独立进程/会话)。`)
|
|
917
968
|
}
|
|
918
969
|
}
|
|
919
970
|
} catch (e) {
|
|
920
971
|
// fail-closed:Gate 自身异常阻断完成,不静默放行(与 Task Review Gate 一致)
|
|
921
972
|
console.error('❌ Stage Review Gate 异常,阻断 ' + stageName + ' 完成: ' + e.message)
|
|
922
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
973
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'review_rejected', detail: 'stage-review' })
|
|
923
974
|
}
|
|
924
975
|
}
|
|
925
976
|
|
|
@@ -978,14 +1029,16 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
978
1029
|
// marker 缺失:先扫描 execute-runs/ 既有目录找回真实 runId(与 getLatestStageReviewRunId
|
|
979
1030
|
// 目录扫描兜底同语义),避免 marker 丢失而 agent 已用旧 runId 落盘时,直接 generate 新 ID
|
|
980
1031
|
// 找不到旧 review、误判缺 review.json。仅当确实无既有 run 才 generate 新 ID 并落盘。
|
|
981
|
-
const { generateExecuteRunId, resolveLatestExecuteRunId, stampExecuteRunChange } = await import('../task-review.js')
|
|
1032
|
+
const { generateExecuteRunId, resolveLatestExecuteRunId, stampExecuteRunChange, claimExecuteRunId } = await import('../task-review.js')
|
|
982
1033
|
executeRunId = resolveLatestExecuteRunId({ runtimeRoot, changeName }) || ''
|
|
983
1034
|
if (!executeRunId) {
|
|
984
|
-
executeRunId = generateExecuteRunId()
|
|
1035
|
+
executeRunId = generateExecuteRunId(changeName)
|
|
985
1036
|
// 落盘(marker 缺失时 fallback 生成后写盘,保证后续 checkbox/gate 读到同一 ID)
|
|
986
1037
|
// D-001#1 fallback 写入点:mkdir execute-runs/<runId>/tasks 先于 marker(不变量:
|
|
987
1038
|
// marker 在则目录在)。不 try/catch——异常直穿外层 catch 走 fail-closed 阻断
|
|
988
1039
|
//(gate 自身写 run 目录失败不能静默放行完成)。
|
|
1040
|
+
// 排他认领(坑 exec-run-id-same-second-collision):并行会话同秒碰撞在认领处消解
|
|
1041
|
+
executeRunId = claimExecuteRunId(runtimeRoot, executeRunId)
|
|
989
1042
|
mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
|
|
990
1043
|
writeAtomicSync(runIdFile, executeRunId + '\n')
|
|
991
1044
|
stampExecuteRunChange(runtimeRoot, executeRunId, changeName)
|
|
@@ -1026,7 +1079,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1026
1079
|
console.error('\n⚠️ 部分任务已在 tasks.md 中勾选,但 review.json 不存在。')
|
|
1027
1080
|
console.error(` 请取消勾选这些任务的 checkbox,或补充对应的 review.json(execute run ID: ${executeRunId})。`)
|
|
1028
1081
|
}
|
|
1029
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
1082
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'review_rejected', detail: 'task-review' })
|
|
1030
1083
|
}
|
|
1031
1084
|
|
|
1032
1085
|
// cannot_verify 的 requiredEvidence 写入 change 目录,供 verify 阶段消费
|
|
@@ -1042,7 +1095,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1042
1095
|
// fail-closed:Gate 自身异常时不能默认放行,否则异常成了绕过评审的通道
|
|
1043
1096
|
console.error(`❌ Task Review Gate 异常,阻断 execute 完成: ${e.message}`)
|
|
1044
1097
|
console.error(' 请检查 review.json / plan.md 是否可读,修复后重新完成此步骤。')
|
|
1045
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
1098
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'review_rejected', detail: 'task-review' })
|
|
1046
1099
|
}
|
|
1047
1100
|
}
|
|
1048
1101
|
return null
|
|
@@ -1263,9 +1316,13 @@ export async function completeStageGates({ stageName, cwd, changeName, platformO
|
|
|
1263
1316
|
validateFileLocations(cwd, stageName, progress, changeName, specBase)
|
|
1264
1317
|
}
|
|
1265
1318
|
|
|
1266
|
-
// 辅助阶段完成后重置步骤(scan 等 auxiliary 阶段重置回 pending
|
|
1319
|
+
// 辅助阶段完成后重置步骤(scan 等 auxiliary 阶段重置回 pending 可重跑)。
|
|
1320
|
+
// archive 例外(P0-4 并步补丁):确认归档成为末步后本重置会与 unregisterChange 的终态
|
|
1321
|
+
// (steps 全 completed + current_stage=archive——平台按步骤数渲染完成度的数据源)互相
|
|
1322
|
+
// 覆盖,把已注销变更抹回 0/N。archive 是终态阶段(变更已注销,不存在「重跑归档」流),
|
|
1323
|
+
// 不参与可重跑重置。
|
|
1267
1324
|
const stageDef = stageRegistry[stageName]
|
|
1268
|
-
if (stageDef?.auxiliary) {
|
|
1325
|
+
if (stageDef?.auxiliary && stageName !== 'archive') {
|
|
1269
1326
|
const freshSteps = (stageDef.steps || []).map(s => ({
|
|
1270
1327
|
name: s.name,
|
|
1271
1328
|
status: 'pending',
|
|
@@ -1292,7 +1349,7 @@ export async function completeStageGates({ stageName, cwd, changeName, platformO
|
|
|
1292
1349
|
}
|
|
1293
1350
|
} catch (e) {
|
|
1294
1351
|
console.error(`\n❌ ${stageName} 阶段完成收尾异常(已 rollback 为 in-progress,请修复后重新 --done):${e?.message || e}`)
|
|
1295
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
1352
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'internal-error' })
|
|
1296
1353
|
}
|
|
1297
1354
|
|
|
1298
1355
|
// execute worktree cleanup(completeStep / continueStep 完成分支统一调用,避免双清理)
|
|
@@ -1615,11 +1672,50 @@ export function printProbeConsistencyCheck(r, envelope = buildProbeConsistencyEn
|
|
|
1615
1672
|
* 一天多——verify prompt 要求 PID 按变更分片登记 .runtime/verify-services-<change>.pids。
|
|
1616
1673
|
* 坑 verify-pids-cross-session-kill(2026-08-23):原单文件无归属,A 会话 --done 会把 B 正在
|
|
1617
1674
|
* 收集 Runtime Evidence 的服务一并杀掉——分片后各回收各的;旧单文件兼容回收(升级过渡期)。
|
|
1675
|
+
* 坑 verify-service-process-leak 子进程泄漏(2026-09-10 驾驭小结第三批③,用户实锤 python
|
|
1676
|
+
* 子进程泄漏):原 process.kill(pid) 只杀登记的 shell 包装进程(`cmd /c` / `sh -c` 起的
|
|
1677
|
+
* uvicorn 等),子进程成了孤儿继续挂机——改 killProcessTree 进程树击杀(win taskkill /T /F
|
|
1678
|
+
* + POSIX 递归枚举子进程);prompt 指引同步补「优先登记服务本体(叶子)PID」。
|
|
1618
1679
|
* 回执同样按变更分片(原单份被并行会话后写覆盖),checkIntegrationEvidence(stage-contract)
|
|
1619
1680
|
* 读分片名注入证据匹配。best-effort:ESRCH(已退出)静默,kill 失败 warn 不阻断 verify。
|
|
1620
1681
|
*
|
|
1621
1682
|
* @returns {{ reaped: number, receiptPath: string|null }}
|
|
1622
1683
|
*/
|
|
1684
|
+
export function killProcessTree(pid, sig = 'SIGTERM') {
|
|
1685
|
+
const pidNum = parseInt(pid, 10)
|
|
1686
|
+
if (!Number.isInteger(pidNum) || pidNum <= 0) return false
|
|
1687
|
+
if (process.platform === 'win32') {
|
|
1688
|
+
// /T = 连同子进程树(shell 包装下的 python/node 服务本体);/F = 强制(长驻服务常吞 SIGTERM 等价物)
|
|
1689
|
+
try {
|
|
1690
|
+
execFileSync('taskkill', ['/PID', String(pidNum), '/T', '/F'], { stdio: 'ignore', timeout: 15000 })
|
|
1691
|
+
return true
|
|
1692
|
+
} catch (e) {
|
|
1693
|
+
// taskkill 对不存在进程退出码 128(等价 ESRCH,已退出按回收静默);其余(拒绝访问等)上抛
|
|
1694
|
+
if (e && (e.status === 128 || /could not be found|找不到/i.test(String(e.stderr || '') + String(e.message || '')))) return false
|
|
1695
|
+
throw e
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
// POSIX:先递归杀子进程(shell 包装的子进程是服务本体),再杀登记进程本身
|
|
1699
|
+
for (const child of listChildPids(pidNum)) {
|
|
1700
|
+
try { killProcessTree(child, sig) } catch { /* 子进程已退出/权限不足不连坐父进程回收 */ }
|
|
1701
|
+
}
|
|
1702
|
+
try { process.kill(pidNum, sig); return true }
|
|
1703
|
+
catch (e) { if (e.code === 'ESRCH') return false; throw e }
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/** ps 枚举直接子进程 PID(POSIX;失败返回空——杀不到子进程不阻断父进程回收) */
|
|
1707
|
+
function listChildPids(parentPid) {
|
|
1708
|
+
try {
|
|
1709
|
+
const out = execFileSync('ps', ['-axo', 'pid=,ppid='], { encoding: 'utf8', timeout: 5000 })
|
|
1710
|
+
const children = []
|
|
1711
|
+
for (const line of String(out).split('\n')) {
|
|
1712
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)$/)
|
|
1713
|
+
if (m && Number(m[2]) === parentPid) children.push(Number(m[1]))
|
|
1714
|
+
}
|
|
1715
|
+
return children
|
|
1716
|
+
} catch { return [] }
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1623
1719
|
export function reapVerifyServices(platformOpts, specBase, changeName) {
|
|
1624
1720
|
const runtimeDir = resolveRuntimeRoot(platformOpts, specBase)
|
|
1625
1721
|
const reapFile = (path, label) => {
|
|
@@ -1627,11 +1723,12 @@ export function reapVerifyServices(platformOpts, specBase, changeName) {
|
|
|
1627
1723
|
const pids = readFileSync(path, 'utf8').split('\n').map(l => l.trim()).filter(l => /^\d+$/.test(l))
|
|
1628
1724
|
const reaped = []; const failed = []
|
|
1629
1725
|
for (const pid of pids) {
|
|
1630
|
-
|
|
1631
|
-
|
|
1726
|
+
// 进程树击杀(坑 verify-service-process-leak 子进程泄漏):连带 shell 包装下的服务本体
|
|
1727
|
+
try { if (killProcessTree(pid)) reaped.push(pid) }
|
|
1728
|
+
catch (e) { if (e.code !== 'ESRCH') failed.push(`${pid}(${e.code || e.status || e.message})`) }
|
|
1632
1729
|
}
|
|
1633
1730
|
try { unlinkSync(path) } catch {}
|
|
1634
|
-
if (reaped.length > 0) console.log(`🧹 verify 服务进程已回收 ${reaped.length} 个${label}(PID: ${reaped.join(', ')}
|
|
1731
|
+
if (reaped.length > 0) console.log(`🧹 verify 服务进程已回收 ${reaped.length} 个${label}(PID: ${reaped.join(', ')},进程树击杀)`)
|
|
1635
1732
|
return { reaped: reaped.length, failed }
|
|
1636
1733
|
}
|
|
1637
1734
|
const own = reapFile(join(runtimeDir, `verify-services-${changeName}.pids`), '')
|
package/src/run/prompt.js
CHANGED
|
@@ -803,13 +803,15 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
803
803
|
}
|
|
804
804
|
} catch {}
|
|
805
805
|
if (!runId) {
|
|
806
|
-
const { generateExecuteRunId } = await import('../task-review.js')
|
|
807
|
-
runId = generateExecuteRunId()
|
|
806
|
+
const { generateExecuteRunId, claimExecuteRunId } = await import('../task-review.js')
|
|
807
|
+
runId = generateExecuteRunId(changeName)
|
|
808
808
|
// 落盘(与启动站点一致),保证 agent 收到的 ID == gate/checkbox 读取的 ID
|
|
809
809
|
// D-001#1 fallback 写入点:mkdir execute-runs/<runId>/tasks 先于 marker(不变量:marker 在则目录在)。
|
|
810
810
|
// 渲染路径异常不能炸 prompt 输出 → catch 内 console.error 留痕 + 保留降级(ID 注入继续,
|
|
811
811
|
// 目录由下游 review 写入 / 其他写入点补齐)。
|
|
812
812
|
try {
|
|
813
|
+
// 排他认领(坑 exec-run-id-same-second-collision):并行会话同秒碰撞在认领处消解
|
|
814
|
+
runId = claimExecuteRunId(runtimeRoot, runId)
|
|
813
815
|
mkdirSync(join(runtimeRoot, 'execute-runs', runId, 'tasks'), { recursive: true })
|
|
814
816
|
writeAtomicSync(runIdFile, runId + '\n')
|
|
815
817
|
} catch (e) {
|
|
@@ -825,7 +827,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
825
827
|
if (['brainstorm', 'plan', 'execute'].includes(stageName) && promptText.includes('{REVIEW_TIER}')) {
|
|
826
828
|
try {
|
|
827
829
|
const { classifyReviewTier } = await import('../review-tier.js')
|
|
828
|
-
const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath } = await import('../stage-review.js')
|
|
830
|
+
const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath, readReviewChannelPriority } = await import('../stage-review.js')
|
|
829
831
|
const tierSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
830
832
|
const tierChangeDir = changeName ? join(tierSpecBase, 'changes', changeName) : null
|
|
831
833
|
const designPath = tierChangeDir ? join(tierChangeDir, 'design.md') : null
|
|
@@ -859,8 +861,12 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
859
861
|
console.log(` 📁 Stage Review 写入目录(直接复制给 review 子代理,勿手拼 runId):${reviewDir}/`)
|
|
860
862
|
} catch {}
|
|
861
863
|
}
|
|
862
|
-
// review.json 产物契约(schema + 示例 + docHash 算法 + 重算提示)事前注入,与 validateStageReviewSchema
|
|
863
|
-
|
|
864
|
+
// review.json 产物契约(schema + 示例 + docHash 算法 + 重算提示)事前注入,与 validateStageReviewSchema 同源。
|
|
865
|
+
// 通道优先序显式传入(readReviewChannelPriority 吃精确 cwd——平台模式 specRoot 异位时
|
|
866
|
+
// process.cwd 兜底会读错 local.yaml,2026-09-10 通道配置批次);tier=independent 时契约
|
|
867
|
+
// 头部渲染「审查执行通道」段(按用户配置序,见 local.yaml review_dispatch.channel_priority)。
|
|
868
|
+
const reviewChannelPriority = readReviewChannelPriority(cwd)
|
|
869
|
+
const reviewContractMd = renderReviewJsonContract({ stage: stageName, changeDir: tierChangeDir, reviewRunId, tier: tier.tier, channelPriority: reviewChannelPriority })
|
|
864
870
|
promptText = promptText
|
|
865
871
|
.split('{REVIEW_TIER}').join(tier.tier)
|
|
866
872
|
.split('{REVIEW_TIER_REASON}').join(tier.reason)
|
|
@@ -1012,6 +1018,29 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1012
1018
|
promptText = promptText.split('{ARCHIVE_IMPACT_AUDIT}').join(auditBlock)
|
|
1013
1019
|
}
|
|
1014
1020
|
|
|
1021
|
+
// {SCOPE_AUDIT_TABLE}(2026-09-10-change-scope-audit task-05,FR-03):archive「确认归档」步的
|
|
1022
|
+
// 变更范围对账全表注入——computeChangeScopeAudit + renderScopeAuditTable 代算(计划×实际
|
|
1023
|
+
// 三态 + 行数,D-003 同源:只 import scope-audit.js 导出,不自研采集)。maxRows 60 截断防
|
|
1024
|
+
// prompt 膨胀(R-03,表尾自带「完整表跑 scope-audit」指引)。fail-soft:注入异常降级单行
|
|
1025
|
+
// 指引(回退手跑 scope-audit 命令),不阻断归档 prompt 输出(对齐 {ARCHIVE_IMPACT_AUDIT}
|
|
1026
|
+
// 注入先例);ok=false(quick 会话不存在 / 实际侧整体失败等)同样附手跑指引——归档零新增
|
|
1027
|
+
// 阻断面(D-006)。
|
|
1028
|
+
if (stageName === 'archive' && promptText.includes('{SCOPE_AUDIT_TABLE}')) {
|
|
1029
|
+
let scopeTable = ''
|
|
1030
|
+
try {
|
|
1031
|
+
const { computeChangeScopeAudit, renderScopeAuditTable } = await import('../scope-audit.js')
|
|
1032
|
+
const saSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1033
|
+
const saResult = await computeChangeScopeAudit({ cwd, specBase: saSpecBase, changeName, platformOpts })
|
|
1034
|
+
scopeTable = renderScopeAuditTable(saResult, { maxRows: 60 })
|
|
1035
|
+
if (saResult && saResult.ok === false) {
|
|
1036
|
+
scopeTable += '\n(对账降级——回退手跑 sillyspec scope-audit --change <name> 查看原因后再确认归档,注入失败不阻断归档)'
|
|
1037
|
+
}
|
|
1038
|
+
} catch (e) {
|
|
1039
|
+
scopeTable = '(变更范围对账表注入失败:' + (e && e.message ? e.message : e) + '——回退手跑 sillyspec scope-audit --change <name> 查看后再确认归档,注入失败不阻断归档)'
|
|
1040
|
+
}
|
|
1041
|
+
promptText = promptText.split('{SCOPE_AUDIT_TABLE}').join(scopeTable)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1015
1044
|
// 注入模块上下文(brainstorm/plan/execute 阶段全步 + quick 首步「理解任务」——刀①:quick
|
|
1016
1045
|
// step1 原让 agent cat module-map 再挑模块卡读,改为按任务描述匹配后注入,基于 Module Context Index)
|
|
1017
1046
|
const quickFirstStep = stageName === 'quick' && step && step.name === '理解任务'
|
|
@@ -1087,7 +1116,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1087
1116
|
console.log('- CLI 子命令以本 prompt 或上一条 --done 输出的字面为准;不确定时停下问用户,猜测命令会让状态机推进到错误位置。')
|
|
1088
1117
|
console.log('- 本步骤产物落盘后立即执行 prompt 末尾的 --done:CLI 据此校验产出并推进状态机;不跑则进度永远停在本步。')
|
|
1089
1118
|
console.log('- 变更目录改名用 `sillyspec change-rename <旧名> <新名>`:mv/rename 会漏改进度库引用,导致变更失联。')
|
|
1090
|
-
console.log('-
|
|
1119
|
+
console.log('- 变更产物文档一律用 CLI 骨架命令生成(fourpiece-init / design-init / taskcard / module-impact / verify-probes --init)——骨架已预填 author/created_at/generated_by 元数据,勿删勿手拼 frontmatter;仅从零手写的补充文档才需手填 author(git 用户名)与 created_at(精确到秒)')
|
|
1091
1120
|
console.log('- 执行构建/测试前必须先读 local.yaml,优先使用其中配置的命令、路径和环境变量;未配置时才使用默认值')
|
|
1092
1121
|
}
|
|
1093
1122
|
// 平台模式 + 路径规则(安全关键,每步注入;step1+ 起带精简标题,不复述通用铁律)
|
|
@@ -1123,6 +1152,13 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1123
1152
|
const mayNeedWait = WAIT_MARKER_RE.test(stepPrompt) || requiresWait || conditionalWait
|
|
1124
1153
|
|
|
1125
1154
|
console.log(`\n### 完成后执行`)
|
|
1155
|
+
// P0-3(noai-ir-roadmap §3):gate 预检前置——gate 失败 → rollback → 重做一轮是最贵的循环,
|
|
1156
|
+
// 多数失败是机械的(占位符未替换/文件未建/行号失效)。gate 是只读聚合预检,--done 前跑一次
|
|
1157
|
+
// 把「重做一轮」变「本地预检」。主阶段且有 change 才提示(quick 边界审计在 --done 内联、
|
|
1158
|
+
// 辅助阶段无 gate 语义)。
|
|
1159
|
+
if (['brainstorm', 'plan', 'execute', 'verify', 'archive'].includes(stageName) && changeName) {
|
|
1160
|
+
console.log(`💡 预检(只读,省一轮 gate 失败重跑):sillyspec gate ${stageName} --change ${changeName} [--json]`)
|
|
1161
|
+
}
|
|
1126
1162
|
// requiresConfirm 步骤(如 archive「确认归档」)的完成命令必须带 --confirm——坑
|
|
1127
1163
|
// archive-batch-31-tool-notes ②:通用 --done 模板不带该 flag,agent 照抄执行撞
|
|
1128
1164
|
// 「请添加 --confirm」确认门,误以为参数没带。
|
|
@@ -1146,15 +1182,24 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1146
1182
|
console.log(`如果不需要用户决策,正常完成:`)
|
|
1147
1183
|
}
|
|
1148
1184
|
console.log(doneCommand + (autoMeta ? '' : ' --input "用户原始需求/反馈"'))
|
|
1185
|
+
// P0-2(noai-ir-roadmap §3):--output 可省略提示(auto 模式除外——auto driver 的
|
|
1186
|
+
// SS-META doneCommand 仍带 --output,照抄路径零变化;省略路径由 command.js 同源合成)。
|
|
1187
|
+
if (!autoMeta) {
|
|
1188
|
+
console.log(`(--output 可省略:省略时 CLI 按门禁与 diff 事实合成事实性摘要;方案取舍/用户反馈等语义说明才需要手写)`)
|
|
1189
|
+
}
|
|
1149
1190
|
|
|
1150
1191
|
// ── SS-META 机器可读元数据块(change: 2026-09-08-auto-driver,D-002@v1,FR-01)──
|
|
1151
|
-
//
|
|
1152
|
-
//
|
|
1153
|
-
|
|
1192
|
+
// 单行 HTML 注释内 JSON,agent 低噪可读、脚本一行正则可提取(<!--SS-META:(.*)-->)。
|
|
1193
|
+
// requiresUser 四源见上方纯函数;doneCommand 与正文同源。
|
|
1194
|
+
// P1-3(noai-ir-roadmap §4):SS-META 从 auto 专属毕业——常规模式 `run <stage> --meta`
|
|
1195
|
+
// 同样渲染(command.js 置 SILLYSPEC_RUN_META=1,CLI 短进程生命周期内有效,不跨调用泄漏);
|
|
1196
|
+
// meta 增补 change 字段(宿主脚本免再从正文 header 行解析变更名,向后兼容追加)。
|
|
1197
|
+
if (autoMeta || process.env.SILLYSPEC_RUN_META === '1') {
|
|
1154
1198
|
const meta = {
|
|
1155
1199
|
stage: stageName,
|
|
1156
1200
|
stepIndex: stepIndex + 1,
|
|
1157
1201
|
stepName: step.name,
|
|
1202
|
+
change: changeName || null,
|
|
1158
1203
|
requiresUser: requiresWait || mayNeedWait || step.requiresConfirm === true,
|
|
1159
1204
|
doneCommand,
|
|
1160
1205
|
waitHint: (requiresWait || mayNeedWait)
|