sillyspec 3.30.0 → 3.32.0
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-auto/SKILL.md +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +10 -6
- package/.claude/skills/sillyspec-quick/SKILL.md +116 -18
- package/.zcodeignore +66 -0
- package/AGENTS.md +15 -15
- package/assets/command-cards/flow.md +9 -3
- package/assets/command-cards/run-quick.md +4 -1
- package/package.json +2 -2
- package/src/agent-session-log.js +27 -5
- package/src/config-schema.js +21 -0
- package/src/datetime.js +51 -0
- package/src/db.js +10 -3
- package/src/decision-distill.js +21 -12
- package/src/design-facts.js +46 -6
- package/src/flow-draft.js +468 -95
- package/src/flow-parity.js +62 -4
- package/src/flow-review.js +136 -6
- package/src/flow.js +729 -125
- package/src/foreign-declared.js +71 -3
- package/src/fr-index.js +148 -162
- package/src/git-helper.js +16 -3
- package/src/hooks/worktree-guard.js +16 -5
- package/src/hunk-attribution.js +28 -5
- package/src/index.js +184 -17
- package/src/knowledge-classify.js +3 -1
- package/src/knowledge-digest.js +55 -26
- package/src/knowledge-match.js +135 -7
- package/src/knowledge-stats.js +66 -4
- package/src/knowledge-vector.js +160 -0
- package/src/machine-interface.js +90 -2
- package/src/module-impact.js +22 -1
- package/src/modules.js +9 -0
- package/src/progress/change-registry.js +20 -0
- package/src/progress/stage-machine.js +4 -7
- package/src/progress.js +8 -1
- package/src/quicklog.js +20 -0
- package/src/redomain.js +23 -7
- package/src/route-hindsight.js +276 -0
- package/src/run/archive-distill.js +3 -1
- package/src/run/command.js +65 -12
- package/src/run/complete-handlers.js +219 -5
- package/src/run/complete.js +77 -11
- package/src/run/gate-snapshot.js +15 -0
- package/src/run/gates.js +60 -4
- package/src/run/prompt.js +159 -31
- package/src/run/quick-audit.js +66 -7
- package/src/run/shared.js +6 -11
- package/src/run/stage.js +5 -1
- package/src/run/verify-quality-scan.js +84 -0
- package/src/scan-facts.js +2 -1
- package/src/scope-audit.js +9 -2
- package/src/sentinel-assertions.js +67 -16
- package/src/stage-contract-engine.js +4 -1
- package/src/stage-contract-spec.js +103 -4
- package/src/stage-contract.js +55 -7
- package/src/stages/brainstorm.js +27 -14
- package/src/stages/knowledge.js +4 -3
- package/src/stages/plan-postcheck.js +98 -3
- package/src/stages/plan.js +22 -2
- package/src/stages/verify.js +2 -1
- package/src/task-review.js +1 -1
- package/src/task-tick.js +121 -0
- package/src/taskcard-frontmatter.js +31 -0
- package/src/ui-visual.js +43 -8
- package/src/verify-postcheck.js +509 -37
- package/src/verify-probes.js +216 -5
- package/src/watcher.js +1067 -1031
- package/src/worktree-apply.js +9 -1
- package/src/worktree.js +21 -6
- package/templates/agents-instruction.md +15 -15
- package/templates/prompts/taskcard-rules.md +1 -0
|
@@ -410,6 +410,60 @@ export function extractPendingDocSyncRows(content) {
|
|
|
410
410
|
return rows
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
+
/**
|
|
414
|
+
* module-impact.md pending 死信一键回填(2026-10-07-flow-friction-batch3 task-04):verify 硬拦
|
|
415
|
+
* 「更新结果」表 pending/待办行后,agent 实测写一次性 node 脚本批量回填——CLI 化。语义对齐
|
|
416
|
+
* 文件内规则行「确定不同步的行改 skipped 并在操作列写明原因」:状态列 pending/待办→skipped,
|
|
417
|
+
* --reason 追加进操作列(`——skipped:<reason>`);done/其余状态行与表外内容逐字不动;幂等
|
|
418
|
+
* (无 pending 行时零写盘)。与 extractPendingDocSyncRows 同一节段解析口径(不二算规则集)。
|
|
419
|
+
* @param {string} mdPath module-impact.md 路径
|
|
420
|
+
* @param {{ reason?: string }} opts reason 追加进操作列的一句话原因(缺省只翻状态)
|
|
421
|
+
* @returns {{ filled: number, ok: boolean, error?: string }} filled=回填行数;ok=false 时 error 带原因
|
|
422
|
+
*/
|
|
423
|
+
export function fillModuleImpactSkipped(mdPath, { reason = '' } = {}) {
|
|
424
|
+
let content
|
|
425
|
+
try { content = readFileSync(mdPath, 'utf8') } catch (e) {
|
|
426
|
+
return { filled: 0, ok: false, error: `module-impact.md 不可读:${mdPath}(${e && e.message ? e.message : e})` }
|
|
427
|
+
}
|
|
428
|
+
const normalized = content.replace(/\r\n/g, '\n')
|
|
429
|
+
const sectionStart = normalized.search(/^#{2,3}\s*更新结果\s*$/m)
|
|
430
|
+
if (sectionStart === -1) return { filled: 0, ok: false, error: 'module-impact.md 无「更新结果」小节——无可回填行(先核对文件是否为骨架未填形态)' }
|
|
431
|
+
const titleMatch = normalized.slice(sectionStart).match(/^#{2,3}\s*更新结果\s*$/m)
|
|
432
|
+
const bodyStart = sectionStart + titleMatch[0].length
|
|
433
|
+
const afterTitle = normalized.slice(bodyStart)
|
|
434
|
+
const nextSection = afterTitle.search(/^#{1,3}\s/m)
|
|
435
|
+
const section = nextSection === -1 ? afterTitle : afterTitle.slice(0, nextSection)
|
|
436
|
+
const sectionEnd = nextSection === -1 ? normalized.length : bodyStart + nextSection
|
|
437
|
+
const PENDING_STATUSES = new Set(['pending', '待办', '未同步', 'not-done', 'todo'])
|
|
438
|
+
let filled = 0
|
|
439
|
+
const newSection = section.split('\n').map((line) => {
|
|
440
|
+
const trimmed = line.trim()
|
|
441
|
+
if (!trimmed.startsWith('|')) return line
|
|
442
|
+
const cells = trimmed.split('|').map(c => c.trim())
|
|
443
|
+
const lastCell = cells[cells.length - 1]
|
|
444
|
+
if (lastCell === '' && cells.length >= 2) cells.pop()
|
|
445
|
+
const statusCell = cells[cells.length - 1]
|
|
446
|
+
if (!statusCell || !PENDING_STATUSES.has(statusCell.toLowerCase())) return line
|
|
447
|
+
filled++
|
|
448
|
+
// 结构 | 目标 | 操作 | 状态 | → cells ['', 目标, 操作, 状态];操作列 = 倒数第二格
|
|
449
|
+
if (cells.length >= 3 && reason) {
|
|
450
|
+
cells[cells.length - 2] = `${cells[cells.length - 2]}——skipped:${reason}`
|
|
451
|
+
}
|
|
452
|
+
cells[cells.length - 1] = 'skipped'
|
|
453
|
+
// 重拼保留「| a | b | c |」空格形态(cells[0] 是行首 | 产生的空串);行首缩进对齐原行
|
|
454
|
+
const indent = line.slice(0, line.indexOf('|'))
|
|
455
|
+
return `${indent}| ${cells.slice(1).join(' | ')} |`
|
|
456
|
+
}).join('\n')
|
|
457
|
+
if (filled === 0) return { filled: 0, ok: true }
|
|
458
|
+
const next = normalized.slice(0, bodyStart) + newSection + normalized.slice(sectionEnd)
|
|
459
|
+
try {
|
|
460
|
+
writeFileSync(mdPath, next, 'utf8')
|
|
461
|
+
return { filled, ok: true }
|
|
462
|
+
} catch (e) {
|
|
463
|
+
return { filled: 0, ok: false, error: `回填写盘失败:${e && e.message ? e.message : e}` }
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
413
467
|
/**
|
|
414
468
|
* 提取 module-impact.md「更新结果」表中的已完成(done)行声明的目标文档路径(债单 D-4 窄口径)。
|
|
415
469
|
*
|
|
@@ -508,6 +562,47 @@ export function resolveArchiveDocAddPaths(cwd, specBase, tokens) {
|
|
|
508
562
|
return [...out]
|
|
509
563
|
}
|
|
510
564
|
|
|
565
|
+
/**
|
|
566
|
+
* 归档源侧移动补暂存(坑 archive-stage-claim,2026-10-06-module-map-list-leak 收口实测发现)。
|
|
567
|
+
*
|
|
568
|
+
* safeGit 契约是返回 {value, error} 不抛错——此前调用方忽略返回值无条件打成功提示,add 失败
|
|
569
|
+
* (index.lock 竞态等)时「已补暂存 N 项」假成功、暂存面实际缺项(实测声称 1 项暂存面为空,
|
|
570
|
+
* 靠核对暂存面纪律兜住后手工补提交)。本助手逐批 add 并校验 error,失败明细返回给调用方
|
|
571
|
+
* 择路打印(fail-soft:不抛错不阻断归档链,幂等 add 重跑同值)。
|
|
572
|
+
*
|
|
573
|
+
* @param {{ cwd: string, paths: string[] }} opts paths 已按本变更目录名过滤的精确 pathspec
|
|
574
|
+
* @returns {{ ok: boolean, count: number, failures: Array<{ path: string, error: string }> }}
|
|
575
|
+
*/
|
|
576
|
+
export function stageArchiveSourceSideMoves({ cwd, paths }) {
|
|
577
|
+
const failures = []
|
|
578
|
+
let count = 0
|
|
579
|
+
for (const batch of chunkPaths(paths)) {
|
|
580
|
+
const r = safeGit(cwd, ['add', '--', ...batch])
|
|
581
|
+
if (r && r.error) {
|
|
582
|
+
for (const p of batch) failures.push({ path: p, error: r.error })
|
|
583
|
+
} else {
|
|
584
|
+
count += batch.length
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return { ok: failures.length === 0, count, failures }
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* 补暂存结果文案(成功=既有提示逐字不变;失败=⚠️ 告警含失败路径与 error 首行 + 手工兜底指引;
|
|
592
|
+
* 空清单零文案)。独立导出供测试断言两半(返回值与文案),打印决策归调用方。
|
|
593
|
+
* @param {{ ok: boolean, count: number, failures: Array<{ path: string, error: string }> }} r
|
|
594
|
+
* @returns {string}
|
|
595
|
+
*/
|
|
596
|
+
export function renderStageSourceSideMovesMessage(r) {
|
|
597
|
+
if (!r || r.count === 0 && (!r.failures || r.failures.length === 0)) return ''
|
|
598
|
+
if (r.ok) return `🧾 已补暂存本变更归档的源侧移动(${r.count} 项,归档成单次原子提交)`
|
|
599
|
+
const lines = [`⚠️ 补暂存本变更归档的源侧移动失败(${r.failures.length} 个路径,成功 ${r.count} 项)——暂存面可能缺项,归档提交前请核对 git diff --cached --name-only:`]
|
|
600
|
+
for (const f of r.failures.slice(0, 10)) lines.push(` - ${f.path}(${f.error})`)
|
|
601
|
+
if (r.failures.length > 10) lines.push(` … 共 ${r.failures.length} 个`)
|
|
602
|
+
lines.push(` 手工兜底:git add -- <上述路径> 后重跑收口命令(归档子步幂等续)`)
|
|
603
|
+
return lines.join('\n')
|
|
604
|
+
}
|
|
605
|
+
|
|
511
606
|
/**
|
|
512
607
|
* 归档收尾的窄化 git add(ql-20260915-001 修复④,坑 archive-git-add-sweeps-parallel-docs)。
|
|
513
608
|
*
|
|
@@ -731,6 +826,60 @@ export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir,
|
|
|
731
826
|
*
|
|
732
827
|
* @returns {Promise<void>} 失败面沿链内既有 process.exit 语义(纯搬运不改)。
|
|
733
828
|
*/
|
|
829
|
+
/**
|
|
830
|
+
* 归档提交 pathspec 构成(2026-10-06-archive-cmd-race-and-brief:共享暂存区竞态加固)。
|
|
831
|
+
*
|
|
832
|
+
* 背景:旧实现照抄 `git diff --cached --name-status` 单元格——多 agent 共享仓下,兄弟会话/
|
|
833
|
+
* 后台同步可在「打印→执行」窗口瞬改暂存区(实测 flow-status-json 收尾:建议命令含转瞬即逝
|
|
834
|
+
* 的源侧 A 条目,照抄执行报 pathspec did not match;单会话镜像复盘命令干净=竞态即现即逝)。
|
|
835
|
+
*
|
|
836
|
+
* 构成按稳定性分级:
|
|
837
|
+
* - src 侧(changes/<名>/ 非归档):仅取 HEAD 树在册路径——HEAD 不可变,不受暂存区竞态
|
|
838
|
+
* 影响;rename src / 删除 D 恒在册,转瞬即逝的源侧 A 条目恒不在册(天然滤幽灵)。
|
|
839
|
+
* - dst 侧:本变更专属归档目录单条 pathspec(archive/<名>/ 与窄化 add 同一专属前提),
|
|
840
|
+
* 目录在场即匹配,条目级抖动无感。
|
|
841
|
+
* - knowledge/docs 共享面:HEAD 在册(M 形态)或工作区在场(蒸馏新产 A 形态)才入列。
|
|
842
|
+
* - 其余丢弃并回报(dropped),由调用方显式提示。
|
|
843
|
+
*
|
|
844
|
+
* 纯函数(git 读数由调用方喂入),单测直接钉构成口径。
|
|
845
|
+
* @param {{changeName:string, stagedNameStatus:string, headTreeFiles:string, existsFn?:(p:string)=>boolean}} p
|
|
846
|
+
* @returns {{pathspecs:string[], dropped:string[]}}
|
|
847
|
+
*/
|
|
848
|
+
export function resolveArchiveCommitPathspecs({ changeName, stagedNameStatus, headTreeFiles, existsFn }) {
|
|
849
|
+
const changesPrefix = '.sillyspec/changes/'
|
|
850
|
+
// 与旧实现同源的单元格收集(--name-status:R 行 src<TAB>dst 两格,A/M 单格)
|
|
851
|
+
const mineCells = new Set()
|
|
852
|
+
for (const line of String(stagedNameStatus || '').split('\n')) {
|
|
853
|
+
if (!line.trim()) continue
|
|
854
|
+
for (const cell of line.split('\t').slice(1)) {
|
|
855
|
+
const p = cell.trim().replace(/^"|"$/g, '')
|
|
856
|
+
if (!p) continue
|
|
857
|
+
if (p.startsWith(changesPrefix + changeName + '/')
|
|
858
|
+
|| p.startsWith(changesPrefix + 'archive/' + changeName + '/')
|
|
859
|
+
|| p.startsWith('.sillyspec/knowledge/')
|
|
860
|
+
|| p.startsWith('.sillyspec/docs/')) mineCells.add(p)
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const headSet = new Set(String(headTreeFiles || '').split('\n').map((s) => s.trim()).filter(Boolean))
|
|
864
|
+
const srcPaths = []
|
|
865
|
+
let archiveHit = false
|
|
866
|
+
const sharedPaths = []
|
|
867
|
+
const dropped = []
|
|
868
|
+
for (const p of mineCells) {
|
|
869
|
+
if (p.startsWith(changesPrefix + 'archive/' + changeName + '/')) {
|
|
870
|
+
archiveHit = true
|
|
871
|
+
} else if (p.startsWith(changesPrefix + changeName + '/')) {
|
|
872
|
+
if (headSet.has(p)) srcPaths.push(p)
|
|
873
|
+
else dropped.push(p)
|
|
874
|
+
} else {
|
|
875
|
+
if (headSet.has(p) || (typeof existsFn === 'function' && existsFn(p))) sharedPaths.push(p)
|
|
876
|
+
else dropped.push(p)
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const pathspecs = [...(archiveHit ? [`${changesPrefix}archive/${changeName}/`] : []), ...srcPaths, ...sharedPaths]
|
|
880
|
+
return { pathspecs, dropped }
|
|
881
|
+
}
|
|
882
|
+
|
|
734
883
|
export async function runArchiveChain({ pm, cwd, specBase, changeName, srcDir, destDir, skipApply = false, skipPlanCheck = false, deliverableFiles = null, docsIncrement = null } = {}) {
|
|
735
884
|
const archiveDir = dirname(destDir) // 抽取补:原闭包变量(changes/archive),从 destDir 推导等价
|
|
736
885
|
// ── 模块文档认领 advisory(资产三小件③,2026-09-23)──
|
|
@@ -858,9 +1007,16 @@ export async function runArchiveChain({ pm, cwd, specBase, changeName, srcDir, d
|
|
|
858
1007
|
// pathspec 把并行会话同目录未提交文件夹带进共享暂存区):改为窄化——changes 侧只 add 本变更
|
|
859
1008
|
// 归档目录 archive/<destName>/,docs 侧按 module-impact「更新结果」done 行精确文件集(提取
|
|
860
1009
|
// 失败回退目录级 + 前置 warning,见 archiveNarrowedGitAdd)。
|
|
1010
|
+
// destName 推导(2026-10-05-flow-tail-polish 根因修复):本链签名无 destName——此前下方
|
|
1011
|
+
// 调用引用未声明标识符抛 ReferenceError 被空 catch 静默吞,archiveNarrowedGitAdd 在 flow
|
|
1012
|
+
// done 路径从未生效(归档目录/knowledge/docs 自动暂存全失效,靠 agent 手工兜底——实测
|
|
1013
|
+
// porcelain 实证)。basename(destDir) 与调用方 destDir 恒一致(flow.js 与 run archive 同构)。
|
|
1014
|
+
const destName = basename(destDir)
|
|
861
1015
|
try {
|
|
862
1016
|
archiveNarrowedGitAdd({ cwd, specBase, destDir, destName })
|
|
863
|
-
} catch {
|
|
1017
|
+
} catch (e) {
|
|
1018
|
+
console.warn(`⚠️ 归档窄化 git add 异常(不阻断归档,交付面靠下方探测块兜底/agent git status 核对): ${(e && e.message) || e}`)
|
|
1019
|
+
}
|
|
864
1020
|
|
|
865
1021
|
// ── 他者半归档残留探测(坑 archive-other-residual-rename,2026-08-21 实证)──
|
|
866
1022
|
// 并行变更的手动归档把 R 残留(源目录 rename 目标行)留在暂存区;本变更归档提交时
|
|
@@ -875,6 +1031,10 @@ export async function runArchiveChain({ pm, cwd, specBase, changeName, srcDir, d
|
|
|
875
1031
|
const changesPrefix = '.sillyspec/changes/'
|
|
876
1032
|
const minePaths = []
|
|
877
1033
|
const othersResidual = []
|
|
1034
|
+
// 兜底信号(2026-10-05-flow-tail-polish):正常路径 archiveNarrowedGitAdd 已暂存归档目录
|
|
1035
|
+
// 与 knowledge——这两个信号只在窄化 add 失败降级时非空/置位
|
|
1036
|
+
let untrackedArchiveHit = false
|
|
1037
|
+
const knowledgePending = []
|
|
878
1038
|
for (const line of raw.split('\n')) {
|
|
879
1039
|
if (!line || line.length < 4) continue
|
|
880
1040
|
const x = line[0], y = line[1]
|
|
@@ -894,18 +1054,72 @@ export async function runArchiveChain({ pm, cwd, specBase, changeName, srcDir, d
|
|
|
894
1054
|
|| p.startsWith(changesPrefix + 'archive/' + changeName + '/'))) {
|
|
895
1055
|
minePaths.push(p)
|
|
896
1056
|
}
|
|
1057
|
+
} else if (x === '?' && y === '?') {
|
|
1058
|
+
// 未跟踪兜底:归档新目录(目录聚合/逐文件两形态都收,archive/<me>/ 专属本变更零共享)
|
|
1059
|
+
if (dst === `${changesPrefix}archive/${changeName}/` || dst.startsWith(`${changesPrefix}archive/${changeName}/`)) {
|
|
1060
|
+
untrackedArchiveHit = true
|
|
1061
|
+
} else if (dst.startsWith('.sillyspec/knowledge/')) {
|
|
1062
|
+
knowledgePending.push(dst)
|
|
1063
|
+
}
|
|
1064
|
+
} else if (x === ' ' && y === 'M' && dst.startsWith('.sillyspec/knowledge/')) {
|
|
1065
|
+
// knowledge 未暂存修改(同兜底语义)
|
|
1066
|
+
knowledgePending.push(dst)
|
|
897
1067
|
}
|
|
898
1068
|
}
|
|
899
1069
|
if (minePaths.length > 0) {
|
|
900
1070
|
// ql-20260915-001 修复④:补暂存源侧移动改精确 pathspec(minePaths 已按本变更目录名
|
|
901
1071
|
// 过滤,chunkPaths 分批防 Windows argv 上限)——原 add -A -- .sillyspec/changes/ 目录级
|
|
902
1072
|
// 会顺带扫入并行会话在 changes/ 下的未提交文件(坑 archive-git-add-sweeps-parallel-docs
|
|
903
|
-
// 同坑不同点;git add -- <已删路径> 即暂存删除,语义等价 -A
|
|
904
|
-
|
|
905
|
-
|
|
1073
|
+
// 同坑不同点;git add -- <已删路径> 即暂存删除,语义等价 -A 限本变更面)。
|
|
1074
|
+
// 坑 archive-stage-claim(2026-10-06-module-map-list-leak 收口实测):safeGit 不抛错返回
|
|
1075
|
+
// {value,error}——此前忽略返回值无条件打成功提示,add 失败(index.lock 竞态等)时假成功。
|
|
1076
|
+
const staged = stageArchiveSourceSideMoves({ cwd, paths: minePaths })
|
|
1077
|
+
const msg = renderStageSourceSideMovesMessage(staged)
|
|
1078
|
+
if (msg) (staged.ok ? console.log : console.warn)(msg)
|
|
1079
|
+
}
|
|
1080
|
+
if (untrackedArchiveHit) {
|
|
1081
|
+
// 文件级枚举补暂存(守 AGENTS.md 规则 11 不用目录级 add;实际仅窄化 add 降级时可达)
|
|
1082
|
+
try {
|
|
1083
|
+
const archFiles = []
|
|
1084
|
+
const walk = (d) => { for (const e of readdirSync(d, { withFileTypes: true })) { const p = join(d, e.name); if (e.isDirectory()) walk(p); else archFiles.push(p) } }
|
|
1085
|
+
walk(destDir)
|
|
1086
|
+
const rel = archFiles.map((p) => relative(cwd, p).replace(/\\/g, '/'))
|
|
1087
|
+
for (const batch of chunkPaths(rel)) safeGit(cwd, ['add', '--', ...batch])
|
|
1088
|
+
console.log(`🧾 已补暂存未跟踪的归档目录文件(${rel.length} 项,archive/${changeName}/ 专属本变更——窄化 add 降级兜底)`)
|
|
1089
|
+
} catch (e) {
|
|
1090
|
+
console.warn(`⚠️ 归档目录兜底补暂存失败(请手工 git add -- .sillyspec/changes/archive/${changeName}/ 下文件): ${(e && e.message) || e}`)
|
|
906
1091
|
}
|
|
907
|
-
console.log(`🧾 已补暂存本变更归档的源侧移动(${minePaths.length} 项,归档成单次原子提交)`)
|
|
908
1092
|
}
|
|
1093
|
+
if (knowledgePending.length > 0) {
|
|
1094
|
+
console.log(`📎 归档提交待办(knowledge 蒸馏产物未暂存——核对归属后随归档提交一并带上):`)
|
|
1095
|
+
console.log(` git add -- ${knowledgePending.join(' ')}`)
|
|
1096
|
+
}
|
|
1097
|
+
// 归档提交一笔到位提示(2026-10-06-litest-p1-fixes 实测 b112d738/2c8fd25c:git commit
|
|
1098
|
+
// -- <pathspec> 是部分提交——rename 条目 pathspec 只匹配 archive/ 侧时提交树里源文件
|
|
1099
|
+
// 不会被删(D 留暂存区,源侧得再来一笔)。补暂存只保证暂存面完整,提交 pathspec 仍由
|
|
1100
|
+
// agent 掌握)。聚合本变更归档面全量 staged 路径打印可直接执行的一笔到位 commit 命令。
|
|
1101
|
+
// 2026-10-06-archive-cmd-race-and-brief 竞态加固:真仓实测(flow-status-json 收尾)照抄执行报
|
|
1102
|
+
// pathspec did not match——共享仓多会话/后台同步可在「打印→执行」窗口瞬改暂存区,单会话镜像
|
|
1103
|
+
// 复盘命令干净(竞态即现即逝)。照抄暂存面不可靠,pathspec 构成按稳定性分级(见
|
|
1104
|
+
// resolveArchiveCommitPathspecs JSDoc);无 staged 面不打印(幂等重入)。
|
|
1105
|
+
try {
|
|
1106
|
+
const stagedRaw = gitQuiet(cwd, ['diff', '--cached', '--name-status'])
|
|
1107
|
+
const headTreeRaw = gitQuiet(cwd, ['ls-tree', '-r', '--name-only', 'HEAD'])
|
|
1108
|
+
const { pathspecs, dropped } = resolveArchiveCommitPathspecs({
|
|
1109
|
+
changeName,
|
|
1110
|
+
stagedNameStatus: String(stagedRaw || ''),
|
|
1111
|
+
headTreeFiles: String(headTreeRaw || ''),
|
|
1112
|
+
existsFn: (p) => { try { return existsSync(join(cwd, p)) } catch { return false } },
|
|
1113
|
+
})
|
|
1114
|
+
if (pathspecs.length > 0) {
|
|
1115
|
+
console.log(`🧾 归档提交一笔到位(src 锚 HEAD 树/归档侧目录 pathspec/共享面按在场——共享暂存区竞态加固):`)
|
|
1116
|
+
console.log(` git commit -m "chore(archive): ${changeName} 归档留档" -- ${pathspecs.join(' ')}`)
|
|
1117
|
+
if (dropped.length > 0) {
|
|
1118
|
+
console.log(` ⚠️ 丢弃 ${dropped.length} 个瞬时暂存条目(HEAD 不在册且工作区不在场——并行会话动了共享暂存区,未入命令):${dropped.join('、')}`)
|
|
1119
|
+
}
|
|
1120
|
+
console.log(` (若执行报 pathspec 不匹配:共享仓暂存区在打印→执行间又被并行会话改动——git diff --cached --name-only 全量重核,以实际面替换 pathspec 后再提交)`)
|
|
1121
|
+
}
|
|
1122
|
+
} catch { /* 提示失败不阻断归档(advisory) */ }
|
|
909
1123
|
if (othersResidual.length > 0) {
|
|
910
1124
|
const owners = [...new Set(othersResidual)].filter(Boolean)
|
|
911
1125
|
console.warn(`⚠️ 检测到「他者半归档」残留(暂存区存在其他变更的 rename 记录):${owners.join('、')}`)
|
package/src/run/complete.js
CHANGED
|
@@ -451,6 +451,70 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
+
// ── 方案步 --done 知识检索命中回显(FR-03/D-004,2026-09-28-unclear-req-to-brainstorm)──
|
|
455
|
+
// 设计时点此前零检索面(自动注入只锚变更入口)——方案步 --done 对 --output 与 decisions.md
|
|
456
|
+
// 当前条目(D-xxx 标题+question 拼串;执行期裁决 D-007:条目无时间戳,「自上次 --done 新增」
|
|
457
|
+
// 无机械锚点,取全部条目为超集)跑既有检索匹配器 matchKnowledge(复用不另写);命中 → warn
|
|
458
|
+
// 回显(decisionHits rejected 优先排序为匹配器既有行为)+ evidence 回应提示;v1 只 warn 不
|
|
459
|
+
// 阻断(D-004 故障面自留退路)。逃生阀 local.yaml commands.knowledge-gate: false/off 关回显
|
|
460
|
+
// (缺省开,config-schema 已登记);无命中输出与现状一致。fail-open:检索异常静默放行。
|
|
461
|
+
if (stageName === 'brainstorm' && steps[currentIdx]?.name?.includes('提出 2-3 种方案') && changeName) {
|
|
462
|
+
try {
|
|
463
|
+
let _kgOn = true
|
|
464
|
+
try {
|
|
465
|
+
const _ly = readFileSync(join(specBase, 'local.yaml'), 'utf8')
|
|
466
|
+
const _kgm = _ly.match(/^\s*knowledge-gate\s*:\s*(true|false|on|off)\s*$/m)
|
|
467
|
+
if (_kgm && (_kgm[1] === 'false' || _kgm[1] === 'off')) _kgOn = false
|
|
468
|
+
} catch { /* 无 local.yaml = 缺省开 */ }
|
|
469
|
+
if (_kgOn) {
|
|
470
|
+
let _kgQuery = String(outputText || '')
|
|
471
|
+
let _dm = ''
|
|
472
|
+
try {
|
|
473
|
+
_dm = readFileSync(join(specBase, 'changes', changeName, 'decisions.md'), 'utf8')
|
|
474
|
+
const _parts = []
|
|
475
|
+
for (const m of _dm.matchAll(/^##\s+(D-\d+@v\d+):\s*(.+)$/gm)) _parts.push(`${m[1]} ${m[2]}`)
|
|
476
|
+
for (const m of _dm.matchAll(/^- question:\s*(.+)$/gm)) _parts.push(m[1])
|
|
477
|
+
if (_parts.length > 0) _kgQuery += '\n' + _parts.join('\n')
|
|
478
|
+
} catch { /* 无 decisions.md = 仅 --output */ }
|
|
479
|
+
_kgQuery = _kgQuery.slice(0, 4000)
|
|
480
|
+
if (_kgQuery.trim()) {
|
|
481
|
+
const { matchKnowledgeHybrid } = await import('../knowledge-vector.js')
|
|
482
|
+
const km = await matchKnowledgeHybrid(join(specBase, 'knowledge'), _kgQuery, { cwd })
|
|
483
|
+
// 零分不弹(2026-09-28-knowledge-gate-denoise):score 零且非死路的 rejected 与查询零主题
|
|
484
|
+
// 重叠——空标题条目靠状态蹭进回显是行为实测三例的噪音源;死路条目不受限(防复潮先验)。
|
|
485
|
+
const _hits = (km.decisionHits || []).filter((h) => h.deathPath || (h.status === 'rejected' && h.score > 0))
|
|
486
|
+
// 已回应不重弹:decisions.md 正文已含「命中 id+域文件名」共现(小白鼠回应形态
|
|
487
|
+
// 「unmapped.md D-001@v1」)→ 视为已甄别过,静默;同轮查询串仍含其词面属预期。
|
|
488
|
+
const _answered = _dm
|
|
489
|
+
? (h) => {
|
|
490
|
+
const _dom = String(h.file || '').replace(/^decisions\//, '').replace(/\.md$/, '')
|
|
491
|
+
return _dm.includes(h.id) && _dm.includes(_dom)
|
|
492
|
+
}
|
|
493
|
+
: () => false
|
|
494
|
+
const _echoHits = _hits.filter((h) => !_answered(h))
|
|
495
|
+
const _entries = km.matched ? (km.entries || []) : []
|
|
496
|
+
if (_echoHits.length > 0 || _entries.length > 0) {
|
|
497
|
+
const { deathPathNote } = await import('../knowledge-match.js')
|
|
498
|
+
console.warn(`\n⚠️ [knowledge-gate] 方案/决策知识命中(设计时点防复潮提示,warn 不阻断):`)
|
|
499
|
+
for (const h of _echoHits.slice(0, 5)) {
|
|
500
|
+
const _note = h.status === 'rejected'
|
|
501
|
+
? ` 否决理由:${h.reason || '(未记录)'}`
|
|
502
|
+
: h.deathPath ? ` ⚰️死路注记:${deathPathNote(h.reason)}` : ''
|
|
503
|
+
console.warn(` - ${h.id} ${h.title}(${h.file})status=${h.status || '?'}${_note}`)
|
|
504
|
+
}
|
|
505
|
+
for (const e of _entries.slice(0, 3)) {
|
|
506
|
+
const _base = e.anchor ? `${e.file}#${e.anchor}` : e.file
|
|
507
|
+
console.warn(` - 知识条目 ${_base}${e.display ? `(${e.display})` : ''}`)
|
|
508
|
+
}
|
|
509
|
+
console.warn(` 须在对应决策的 evidence 回应或说明不复潮(v1 提示级;local.yaml commands.knowledge-gate: false 可关本回显)`)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
} catch (_kgEx) {
|
|
514
|
+
console.warn(` ⚠️ knowledge-gate 检索自身异常,fail-open 放行(不误拦本次完成):${_kgEx && _kgEx.message ? _kgEx.message : _kgEx}`)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
454
518
|
// ── noAI 步骤硬门(坑 noai-done-bypass):noAI 步骤的确定性校验不可被 --done 绕过 ──
|
|
455
519
|
// 正常路径 agent 跑 `run <stage>` 推进到 noAI step 时,runStage 自动执行 _cliAction
|
|
456
520
|
// (stage.js noAI 分支,不写 step output);若 agent 对 noAI step 直接 --done,此前
|
|
@@ -898,15 +962,17 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
898
962
|
// verify 的"验证通过"提示延后到下方 validator 通过后才打印,
|
|
899
963
|
// 避免校验失败(FAIL / 缺 verify-result.md)时仍声称"验证通过可以归档"。
|
|
900
964
|
} else if (stageName === 'brainstorm') {
|
|
901
|
-
// brainstorm 下一步按 design.md 的 scale
|
|
965
|
+
// brainstorm 下一步按 design.md 的 scale 分叉(2026-09-29-brainstorm-exit-thin-default:large→plan;small/未标→flow start 收编),
|
|
902
966
|
// 不走下方 _getNextSuggestion —— 后者按全局状态机「第一个未完成且上游就绪的阶段」推荐,
|
|
903
967
|
// 当 scan 未完成时会误推 scan(回头路),与 brainstorm 已完成、应进入 plan/quick 矛盾。
|
|
904
968
|
// 历史教训:曾因此让 agent 在 brainstorm 完成后被误导去跑 scan。
|
|
905
969
|
const _bscale = readDesignScale(specBase, changeName)
|
|
906
|
-
if (_bscale === '
|
|
907
|
-
console.log(`\n👉 brainstorm 已完成(
|
|
970
|
+
if (_bscale === 'large') {
|
|
971
|
+
console.log(`\n👉 brainstorm 已完成(large)。下一步:sillyspec run plan${changeName ? ` --change ${changeName}` : ''}(完整五阶段)`)
|
|
908
972
|
} else {
|
|
909
|
-
|
|
973
|
+
// 未标/small 默认收编轻量道(2026-09-29-brainstorm-exit-thin-default:此前「未标走完整 plan」
|
|
974
|
+
// 是缺省即厚;翻转与 thin-default-flip 同哲学——升厚留运行时证据+用户决策)
|
|
975
|
+
console.log(`\n👉 brainstorm 已完成(scale=${_bscale || '未标,默认 small'})。下一步:sillyspec flow start --change ${changeName}(轻量变更收编头脑风暴产物,2 调用收口;实测失败自动升厚 / --upgrade-thick 用户决策)`)
|
|
910
976
|
}
|
|
911
977
|
} else if (stageName === 'quick') {
|
|
912
978
|
// quick 是收尾阶段(辅助流程,不走主链 scan→archive),完成后该提交,而非推 scan。
|
|
@@ -2086,16 +2152,16 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
2086
2152
|
// 阶段完成后明确下一步(agent 常卡:stageData completed 但不知要 run <下一阶段> 推进 currentStage)
|
|
2087
2153
|
const nextStageHint = { brainstorm: 'plan', plan: 'execute', execute: 'verify', verify: 'archive' }[stageName]
|
|
2088
2154
|
if (nextStageHint) {
|
|
2089
|
-
// brainstorm 按 design.md frontmatter 的 scale
|
|
2090
|
-
//
|
|
2091
|
-
//
|
|
2155
|
+
// brainstorm 按 design.md frontmatter 的 scale 分叉(2026-09-29-brainstorm-exit-thin-default
|
|
2156
|
+
// 与 completeStep 主路径同口径):large → run plan(完整五阶段);small/未标 → flow start
|
|
2157
|
+
// 收编轻量道(此前 small→quick --linked-changes 是旧收编通道、未标→plan 是缺省即厚,均已退役)。
|
|
2092
2158
|
let hintStage = nextStageHint
|
|
2093
2159
|
let hintChangeFlag = changeName ? ` --change ${changeName}` : ''
|
|
2094
|
-
if (stageName === 'brainstorm' && changeName && readDesignScale(specBase, changeName)
|
|
2095
|
-
|
|
2096
|
-
|
|
2160
|
+
if (stageName === 'brainstorm' && changeName && readDesignScale(specBase, changeName) !== 'large') {
|
|
2161
|
+
console.log(`\n👉 ${stageName} 已完成。下一步:sillyspec flow start --change ${changeName}(轻量变更收编头脑风暴产物,2 调用收口)`)
|
|
2162
|
+
} else {
|
|
2163
|
+
console.log(`\n👉 ${stageName} 已完成。下一步:sillyspec run ${hintStage}${hintChangeFlag}`)
|
|
2097
2164
|
}
|
|
2098
|
-
console.log(`\n👉 ${stageName} 已完成。下一步:sillyspec run ${hintStage}${hintChangeFlag}`)
|
|
2099
2165
|
if (stageName === 'execute') {
|
|
2100
2166
|
console.log(` ⚠️ 若 worktree 改动还没 apply 到主工作区,先:sillyspec worktree apply ${changeName}`)
|
|
2101
2167
|
console.log(` (apply 不需要先 commit,支持 working tree 未提交改动)`)
|
package/src/run/gate-snapshot.js
CHANGED
|
@@ -722,6 +722,21 @@ export function detectSymlinkStoreLayout(cwd) {
|
|
|
722
722
|
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
723
723
|
if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) return 'bun'
|
|
724
724
|
if (existsSync(join(cwd, 'lerna.json'))) return 'lerna'
|
|
725
|
+
// ── 子目录 lockfile(apps 型 monorepo——2026-09-30-snapshot-symlink-store-subdir)──
|
|
726
|
+
// 实证:multi-agent-platform(frontend/ + sillyhub-daemon/ 各自 pnpm-lock.yaml,根目录无
|
|
727
|
+
// lockfile)漏检 → 快照照建,verify 收敛循环每轮 ~160s 纯烧快照构建/junction。packages/*
|
|
728
|
+
// workspace 型不扫(该形态 lockfile 在根,根判据已覆盖)。子目录命中=该 app 的
|
|
729
|
+
// node_modules 符号链接网同样跨根失效——跳快照回主仓是既有保守裁决(宁主仓口径)。
|
|
730
|
+
try {
|
|
731
|
+
for (const e of readdirSync(cwd, { withFileTypes: true })) {
|
|
732
|
+
if (!e.isDirectory()) continue
|
|
733
|
+
if (e.name === 'node_modules' || e.name.startsWith('.')) continue
|
|
734
|
+
const d = join(cwd, e.name)
|
|
735
|
+
if (existsSync(join(d, 'pnpm-lock.yaml'))) return `pnpm(subdir:${e.name})`
|
|
736
|
+
if (existsSync(join(d, 'bun.lockb')) || existsSync(join(d, 'bun.lock'))) return `bun(subdir:${e.name})`
|
|
737
|
+
if (existsSync(join(d, 'lerna.json'))) return `lerna(subdir:${e.name})`
|
|
738
|
+
}
|
|
739
|
+
} catch { /* 根不可读(与非仓目录同语义)→ 落到 packageManager 判据 */ }
|
|
725
740
|
try {
|
|
726
741
|
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'))
|
|
727
742
|
const pm = pkg && pkg.packageManager ? String(pkg.packageManager) : ''
|
package/src/run/gates.js
CHANGED
|
@@ -34,6 +34,7 @@ import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, unlinkSync,
|
|
|
34
34
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
35
35
|
import { triggerSync, resolveChangeDir, resolveRuntimeRoot } from './shared.js'
|
|
36
36
|
import { runValidators } from '../stage-contract.js'
|
|
37
|
+
import { computeGateFingerprint, lookupGreenCache, storeGreenCache, greenCacheNotice } from './green-cache.js'
|
|
37
38
|
import { handleScanStageCompleted, handleExecuteWorktreeCleanup } from './complete-handlers.js'
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -956,6 +957,19 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
956
957
|
if (consult.reuse) ledgerReuse = consult
|
|
957
958
|
} catch { /* 账本咨询异常 → 真跑 */ }
|
|
958
959
|
}
|
|
960
|
+
// ── R8 green-cache 第二层复用(2026-09-30-verify-done-green-reuse):--done 收敛循环防重复
|
|
961
|
+
// 真跑。同指纹(HEAD+代码脏面+local.yaml,文档面剔除——green-cache.js 单点口径)30min 内
|
|
962
|
+
// 真跑绿 → 免重跑。实证:multi-agent-platform 2026-09-30 tool-report-activation 收敛循环
|
|
963
|
+
// 码态恒定(12:59-14:15 零 commit)下 test 实测 10 轮 ×~290s 全真跑——P2 账本 miss 即裸奔,
|
|
964
|
+
// 本层兜住。指纹取主仓 cwd/specBase(非快照根:快照目录每轮不同,入指纹永不命中;快照
|
|
965
|
+
// 内容 = HEAD+变更文件 overlay,主仓指纹等价描述)。优先级:P2 账本(三键精确)>
|
|
966
|
+
// P0-1 质量扫描复用 > 本层 > 真跑;fail-open:指纹/缓存任何异常一律回退真跑。──
|
|
967
|
+
let greenHit = null
|
|
968
|
+
let gateFingerprint = null
|
|
969
|
+
try {
|
|
970
|
+
gateFingerprint = computeGateFingerprint({ cwd, specBase })
|
|
971
|
+
if (gateFingerprint) greenHit = lookupGreenCache({ runtimeRoot: resolveRuntimeRoot(platformOpts, specBase), scope: changeName, kind: 'test', fingerprint: gateFingerprint })
|
|
972
|
+
} catch { /* fail-open:缓存链异常回退真跑 */ }
|
|
959
973
|
if (ledgerReuse) {
|
|
960
974
|
testCheck = {
|
|
961
975
|
status: 'passed',
|
|
@@ -970,6 +984,16 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
970
984
|
testCheck = reusableScan.testResult
|
|
971
985
|
console.log(`\n♻️ Verify 测试对账:复用 noAI 质量扫描步的实测结果(代码指纹匹配,免重跑;实测于 ${reusableScan.ranAt || '本变更 verify 期间'}${testCheck.resultPath ? `,台账 ${testCheck.resultPath}` : ''})`)
|
|
972
986
|
printVerifyTestCheck(testCheck)
|
|
987
|
+
} else if (greenHit) {
|
|
988
|
+
testCheck = {
|
|
989
|
+
status: 'passed',
|
|
990
|
+
reason: greenCacheNotice(greenHit, 'verify-test'),
|
|
991
|
+
command: '(green-cache 同指纹复用)',
|
|
992
|
+
exitCode: 0, durationMs: 0, outputTail: null,
|
|
993
|
+
resultPath: null, strategy: null, cached: true,
|
|
994
|
+
}
|
|
995
|
+
console.log(`\n♻️ Verify 测试对账:green-cache 同指纹复用(HEAD+代码脏面+local.yaml 全等,近期真跑绿——P2 账本未命中时的第二层免重跑;SILLYSPEC_GREEN_CACHE_OFF=1 可关闭)`)
|
|
996
|
+
printVerifyTestCheck(testCheck)
|
|
973
997
|
} else {
|
|
974
998
|
// 测试实测是同步 execSync,长套件可跑 2~10min 且中途无输出——先预告避免 agent 误判卡死
|
|
975
999
|
console.log(`\n⏳ Verify 测试对账:CLI 亲自执行 local.yaml 的 commands.test(同步,耗时可能较长,请等待…)`)
|
|
@@ -1039,6 +1063,12 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1039
1063
|
}
|
|
1040
1064
|
} catch { /* 记账异常不影响门禁(下次仍真跑) */ }
|
|
1041
1065
|
}
|
|
1066
|
+
// green-cache 记绿(R8 接线,与 P2 同口径:只有通过结果入缓存——失败永不缓存)
|
|
1067
|
+
if (testCheck && testCheck.status === 'passed' && gateFingerprint) {
|
|
1068
|
+
try {
|
|
1069
|
+
storeGreenCache({ runtimeRoot: resolveRuntimeRoot(platformOpts, specBase), scope: changeName, kind: 'test', fingerprint: gateFingerprint, result: { status: 'passed', exitCode: 0 } })
|
|
1070
|
+
} catch { /* 写缓存失败不影响门禁(下次真跑) */ }
|
|
1071
|
+
}
|
|
1042
1072
|
}
|
|
1043
1073
|
// ── trace 晋升(2026-09-24-fr-test-bindings task-04,fr-test-binding §3.2/D-003@v1):
|
|
1044
1074
|
// verify 门通过点按矩阵判定列晋升 candidate→active(covered/covered-service→active+
|
|
@@ -1112,8 +1142,16 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1112
1142
|
const { runVerifyLintCheck, printVerifyLintCheck } = await import('../verify-postcheck.js')
|
|
1113
1143
|
// P0-1 指纹复用:noAI 质量扫描记录同指纹时 lint 一并复用(lint tally 只记真实执行次数)
|
|
1114
1144
|
let lintCheck = (reusableScan && reusableScan.lintResult) || null
|
|
1145
|
+
let lintGreenHit = null
|
|
1146
|
+
try {
|
|
1147
|
+
lintGreenHit = gateFingerprint ? lookupGreenCache({ runtimeRoot: resolveRuntimeRoot(platformOpts, specBase), scope: changeName, kind: 'lint', fingerprint: gateFingerprint }) : null
|
|
1148
|
+
} catch { /* fail-open:缓存链异常回退真跑 */ }
|
|
1115
1149
|
if (lintCheck) {
|
|
1116
1150
|
console.log(`\n♻️ Verify lint 对账:复用 noAI 质量扫描步的实测结果(代码指纹匹配,免重跑)`)
|
|
1151
|
+
} else if (lintGreenHit) {
|
|
1152
|
+
// R8 green-cache 第二层(同 test 段口径):近期同指纹真跑绿免重跑;tally 只记真实执行
|
|
1153
|
+
lintCheck = { status: 'passed', exitCode: 0, durationMs: 0, failureFiles: [], cached: true, reason: greenCacheNotice(lintGreenHit, 'verify-lint') }
|
|
1154
|
+
console.log(`\n♻️ Verify lint 对账:green-cache 同指纹复用(HEAD+代码脏面+local.yaml 全等,近期真跑绿免重跑;SILLYSPEC_GREEN_CACHE_OFF=1 可关闭)`)
|
|
1117
1155
|
} else {
|
|
1118
1156
|
lintCheck = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase, timeoutMs: verifyGateSnap ? 5 * 60 * 1000 : undefined })
|
|
1119
1157
|
// 快照 lint 超时回退主仓(2026-09-12 dogfood 两连实证:junction I/O 病态慢,3min/5min 均被
|
|
@@ -1123,6 +1161,12 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1123
1161
|
console.warn('⚠️ 快照 lint 超时(node_modules junction I/O 慢)→ 主仓复跑 lint(并行噪声可能混入——失败先做归属鉴定)')
|
|
1124
1162
|
lintCheck = runVerifyLintCheck({ cwd, specBase })
|
|
1125
1163
|
}
|
|
1164
|
+
// green-cache 记绿(R8 接线:passed 才缓存——失败永不缓存;快照超时回退的主仓复跑同记)
|
|
1165
|
+
if (lintCheck.status === 'passed' && gateFingerprint) {
|
|
1166
|
+
try {
|
|
1167
|
+
storeGreenCache({ runtimeRoot: resolveRuntimeRoot(platformOpts, specBase), scope: changeName, kind: 'lint', fingerprint: gateFingerprint, result: { status: 'passed', exitCode: 0 } })
|
|
1168
|
+
} catch { /* 写缓存失败不影响门禁(下次真跑) */ }
|
|
1169
|
+
}
|
|
1126
1170
|
if (lintCheck.status !== 'skipped') {
|
|
1127
1171
|
console.log(`\n⏳ Verify lint 对账:CLI 亲自执行 local.yaml 的 commands.lint…`)
|
|
1128
1172
|
}
|
|
@@ -1527,9 +1571,20 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1527
1571
|
console.warn(`\n⚠️ module-impact.md「更新结果」表有 ${execPendingRows.length} 个 pending/待办项(文档同步债)——verify 阶段会硬拦:`)
|
|
1528
1572
|
for (const row of execPendingRows) console.warn(` - ${row}`)
|
|
1529
1573
|
console.warn(' 建议 execute 收尾就完成模块文档同步并回填 done/skipped(说明原因),别把债拖到 verify 末尾。')
|
|
1574
|
+
console.warn(' 批量跳过不同步行可一键回填:sillyspec module-impact --change <变更名> --fill-skipped --reason "<真实原因>"')
|
|
1530
1575
|
}
|
|
1531
1576
|
}
|
|
1532
1577
|
} catch { /* 提示失败不阻断 execute */ }
|
|
1578
|
+
// UI 视觉证据前置 advisory(2026-10-07-flow-friction-batch3 task-05):证据应执行期随手落盘、
|
|
1579
|
+
// verify 收口才执法(postmortem 实证发现时机太晚)。warn 级不阻断 execute;fail-soft。
|
|
1580
|
+
try {
|
|
1581
|
+
const uiChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
1582
|
+
if (uiChangeDir) {
|
|
1583
|
+
const { uiEvidenceExecuteAdvisory } = await import('../ui-visual.js')
|
|
1584
|
+
const uiAdv = uiEvidenceExecuteAdvisory({ changeDir: uiChangeDir, specBase })
|
|
1585
|
+
if (uiAdv) console.warn(`\n⚠️ ${uiAdv}`)
|
|
1586
|
+
}
|
|
1587
|
+
} catch { /* advisory 失败不阻断 execute */ }
|
|
1533
1588
|
}
|
|
1534
1589
|
|
|
1535
1590
|
// ── Execute Task Review Gate 已退役(2026-09-26-task-review-retire)──
|
|
@@ -1697,10 +1752,11 @@ export function validateFileLocations(cwd, stageName, progress, changeName, spec
|
|
|
1697
1752
|
if (!existsSync(changeDir)) return
|
|
1698
1753
|
|
|
1699
1754
|
// 每个阶段完成后预期存在的文件
|
|
1700
|
-
// brainstorm:scale=small(
|
|
1701
|
-
//
|
|
1702
|
-
//
|
|
1703
|
-
|
|
1755
|
+
// brainstorm:scale=small/未标(收编轻量道,2026-09-29-brainstorm-exit-thin-default 起未标默认 small)只必产
|
|
1756
|
+
// design.md;显式 large → 四件套全。避免对合法收编变更误报"⬜ proposal/requirements/tasks 未找到"
|
|
1757
|
+
// (本检查仅 advisory 打印不 gate,但误导输出仍要消除)。注:stage-contract 硬门条件(ne:small)在并行
|
|
1758
|
+
// 变更在途面,未标仍轻推补值——忘写 scale 的 agent 被门推向显式落值,v1 可接受。
|
|
1759
|
+
const brainstormExpected = readDesignScale(specBase, effectiveChange) !== 'large'
|
|
1704
1760
|
? ['design.md']
|
|
1705
1761
|
: ['design.md', 'proposal.md', 'requirements.md', 'tasks.md']
|
|
1706
1762
|
const expectedFiles = {
|