sillyspec 3.29.4 → 3.30.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 +57 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +18 -116
- package/AGENTS.md +38 -20
- package/SKILL.md +9 -3
- package/assets/command-cards/flow.md +32 -0
- package/assets/command-cards/run-archive.md +26 -0
- package/assets/command-cards/run-brainstorm.md +33 -0
- package/assets/command-cards/run-execute.md +29 -0
- package/assets/command-cards/run-plan.md +27 -0
- package/assets/command-cards/run-quick.md +17 -0
- package/assets/command-cards/run-verify.md +29 -0
- package/assets/command-cards/status.md +27 -0
- package/package.json +2 -1
- package/packages/dashboard/server/executor.js +2 -1
- package/packages/dashboard/server/parser.js +7 -7
- package/r7-test-env/README-R7-TEST.md +93 -0
- package/r7-test-env/demo-project/lib/calc.js +6 -0
- package/r7-test-env/demo-project/package.json +1 -0
- package/r7-test-env/demo-project/test.js +10 -0
- package/r7-test-env/prompt-R7-SMOKE.md +41 -0
- package/r7-test-env/sillyspec-r7.cmd +3 -0
- package/round4/account.mjs +98 -0
- package/round4/b1-analyze-task08.mjs +40 -0
- package/round4/b1-b1-target-validation.md +32 -0
- package/round4/b3a-prompt-audit-result.md +66 -0
- package/round4/b3a-prompt-audit.mjs +89 -0
- package/round4/brief-autocompact.md +34 -0
- package/round4/brief-session-replay.md +50 -0
- package/round4/deep-read-synthesis.md +38 -0
- package/round4/extract-full.mjs +38 -0
- package/round4/extract-timeline.mjs +43 -0
- package/round4/find-sessions.mjs +13 -0
- package/round4/full-r4-l.md +4707 -0
- package/round4/full-r4-o-l.md +4300 -0
- package/round4/full-r4-os.md +1681 -0
- package/round4/full-r4-sf.md +4255 -0
- package/round4/full-r4-sq.md +1734 -0
- package/round4/gate-audit-relay-receipt.md +30 -0
- package/round4/gate-value-audit.md +52 -0
- package/round4/optimization-plan.md +165 -0
- package/round4/prereg.md +67 -0
- package/round4/prompt-R4-L.md +63 -0
- package/round4/prompt-R4-O-L.md +59 -0
- package/round4/prompt-R4-O-S.md +56 -0
- package/round4/prompt-R4-S-F.md +60 -0
- package/round4/prompt-R4-S-Q.md +66 -0
- package/round4/r4-final-report.html +347 -0
- package/round4/r4-os-timeline.txt +297 -0
- package/round4/r4-sf-timeline.txt +738 -0
- package/round4/r4-sq-timeline.txt +322 -0
- package/round4/r4l-timeline.txt +793 -0
- package/round4/r4ol-timeline.txt +729 -0
- package/round4/r4sf-timeline.txt +852 -0
- package/round4/r5-scenario-matrix.md +46 -0
- package/round4/result-S.md +171 -0
- package/round4/result-final.md +46 -0
- package/round4/result-quality.md +63 -0
- package/src/change-list.js +9 -0
- package/src/classify-change.js +6 -0
- package/src/command-cards.js +74 -0
- package/src/commit-guard.js +2 -0
- package/src/config-schema.js +64 -12
- package/src/contract-matrix.js +26 -5
- package/src/db.js +35 -4
- package/src/decision-distill.js +9 -3
- package/src/diagnostic-codes.js +2 -1
- package/src/docs-check.js +1 -1
- package/src/doctor-diagnostics.js +44 -2
- package/src/flow-draft.js +748 -0
- package/src/flow-parity.js +236 -0
- package/src/flow-review.js +184 -0
- package/src/flow.js +1492 -0
- package/src/foreign-declared.js +17 -1
- package/src/fr-index.js +396 -22
- package/src/fs-atomic.js +5 -1
- package/src/git-helper.js +2 -2
- package/src/greenfield-bootstrap.js +98 -0
- package/src/handoff.js +120 -7
- package/src/hunk-attribution.js +191 -0
- package/src/index.js +602 -21
- package/src/init.js +25 -8
- package/src/knowledge-classify.js +47 -1
- package/src/knowledge-digest.js +162 -0
- package/src/knowledge-quicklog.js +246 -0
- package/src/machine-draft.js +128 -0
- package/src/machine-interface.js +194 -7
- package/src/mcp-server.js +1 -0
- package/src/module-impact.js +51 -22
- package/src/module-resolve.js +3 -1
- package/src/preview-progress.js +160 -0
- package/src/progress/change-registry.js +11 -3
- package/src/progress/shared.js +3 -2
- package/src/progress.js +56 -13
- package/src/quicklog.js +44 -6
- package/src/redomain.js +147 -0
- package/src/review-material-pack.js +204 -1
- package/src/run/archive-distill.js +4 -2
- package/src/run/bg-sync.js +67 -0
- package/src/run/command.js +127 -10
- package/src/run/complete-handlers.js +363 -73
- package/src/run/complete.js +356 -74
- package/src/run/gate-snapshot-ledger.js +213 -0
- package/src/run/gate-snapshot.js +260 -38
- package/src/run/gates.js +482 -268
- package/src/run/green-cache.js +123 -0
- package/src/run/junction-rm.js +70 -0
- package/src/run/next.js +14 -0
- package/src/run/prompt.js +185 -55
- package/src/run/quick-audit.js +213 -14
- package/src/run/shared.js +123 -4
- package/src/run/stage.js +182 -216
- package/src/run/test-ledger.js +264 -0
- package/src/run/verify-quality-scan.js +84 -7
- package/src/scope-audit.js +6 -1
- package/src/sentinel-assertions.js +114 -0
- package/src/setup.js +3 -3
- package/src/spec-sync.js +41 -1
- package/src/stage-contract.js +26 -8
- package/src/stage-review-checklist.js +2 -0
- package/src/stage-review.js +7 -2
- package/src/stage-templates.js +157 -0
- package/src/stages/archive.js +2 -0
- package/src/stages/brainstorm.js +24 -106
- package/src/stages/execute.js +222 -85
- package/src/stages/knowledge.js +74 -1
- package/src/stages/plan-postcheck.js +385 -20
- package/src/stages/plan.js +23 -1
- package/src/stages/quick.js +10 -2
- package/src/stages/verify.js +4 -2
- package/src/sync.js +81 -23
- package/src/task-done.js +181 -0
- package/src/task-review.js +8 -55
- package/src/taskcard-frontmatter.js +57 -0
- package/src/test-bindings.js +549 -0
- package/src/timeline.js +218 -0
- package/src/ui-visual.js +175 -0
- package/src/verify-draft.js +214 -0
- package/src/verify-postcheck.js +919 -159
- package/src/verify-probes.js +103 -25
- package/src/watcher.js +1031 -0
- package/src/worktree-apply.js +44 -4
- package/src/worktree-deps.js +22 -8
- package/src/worktree-policy.js +42 -0
- package/src/worktree.js +102 -15
- package/templates/agents-instruction.md +37 -18
|
@@ -52,6 +52,8 @@ import { resolveSessionIdentity } from '../progress.js'
|
|
|
52
52
|
// checkDbScriptDeclarationGate(task-04 / FR-05)同排静态引入:门函数自身纯文件集 ×
|
|
53
53
|
// 文本声明对账(verify-probes 文法经 worktree-apply 顶层动态绑定),同样无环约束。
|
|
54
54
|
import { chunkPaths, checkDbScriptDeclarationGate } from '../worktree-apply.js'
|
|
55
|
+
import { parseModulePathsSubset } from '../decision-distill.js'
|
|
56
|
+
import { parseUncategorizedEntries } from '../knowledge-classify.js'
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
59
|
* 清洗项目名:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符。
|
|
@@ -527,6 +529,25 @@ export function archiveNarrowedGitAdd({ cwd, specBase, destDir, destName }) {
|
|
|
527
529
|
// changes 侧:本变更归档目录精确收窄(quick 轻量归档 closeSingleQuickLinkedChange 同款)
|
|
528
530
|
safeGit(cwd, ['add', '--', `.sillyspec/changes/archive/${destName}/`])
|
|
529
531
|
|
|
532
|
+
// knowledge 侧(cli-protocol-trust / R17 臂3 实证 distill 产物 untracked 漏提交):本归档链
|
|
533
|
+
// distill 刚写的 fr 域文件/decisions/INDEX 必在 status 面——按 status 窄化逐文件精确 add
|
|
534
|
+
// (非目录级:避免扫入他侧并行会话的同目录 WIP;追加型共享面整文件提交与既有惯例一致)。
|
|
535
|
+
try {
|
|
536
|
+
const _knSt = gitQuiet(cwd, ['status', '--porcelain'])
|
|
537
|
+
if (_knSt) {
|
|
538
|
+
const knFiles = String(_knSt).split('\n').map((l) => {
|
|
539
|
+
const p = l.slice(3).trim().replace(/^"|"$/g, '')
|
|
540
|
+
const arrow = p.indexOf(' -> ')
|
|
541
|
+
return arrow !== -1 ? p.slice(arrow + 4) : p // rename 形态取新路径(评审 P3 清偿)
|
|
542
|
+
}).filter((p) => p.replace(/\\/g, '/').startsWith('.sillyspec/knowledge/'))
|
|
543
|
+
for (const batch of chunkPaths(knFiles)) safeGit(cwd, ['add', '--', ...batch])
|
|
544
|
+
if (knFiles.length > 0) console.log(` 📚 knowledge 蒸馏产物入暂存:${knFiles.length} 个文件(fr 域/decisions/INDEX)`)
|
|
545
|
+
else console.log(' ℹ️ knowledge 面无待入暂存文件(本归档无蒸馏产物变更)')
|
|
546
|
+
}
|
|
547
|
+
} catch (e) {
|
|
548
|
+
console.warn(`⚠️ knowledge 侧 add 失败(fail-soft,不阻断归档链):${e && e.message ? e.message : e}`)
|
|
549
|
+
}
|
|
550
|
+
|
|
530
551
|
// docs 侧:module-impact「更新结果」done 行 → 精确文件集。
|
|
531
552
|
// 三态(对齐修复④要求):
|
|
532
553
|
// - module-impact.md 无「## 更新结果」段 / 文件缺失 / 读取异常 → docPaths=null → 回退目录级
|
|
@@ -570,56 +591,161 @@ export function archiveNarrowedGitAdd({ cwd, specBase, destDir, destName }) {
|
|
|
570
591
|
return { docsAdded: null, fallbackDocs: true }
|
|
571
592
|
}
|
|
572
593
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
594
|
+
/**
|
|
595
|
+
* flow 变更交付面推导(资产三小件③)——flow.js changedFilesSinceBaseline 同口径本仓实现
|
|
596
|
+
* (flow.js 为并行会话在改,不 cross-import;两处语义等价:diff 基线→HEAD + porcelain dirty,
|
|
597
|
+
* 排除 .sillyspec/ 内部产物)。docsIncrement 单列 .sillyspec/docs/**(文档认领增量信号)。
|
|
598
|
+
* @returns {{ deliverableFiles: string[], docsIncrement: string[] }|null}
|
|
599
|
+
* 非 flow 变更(无 flow-state.yaml / 无 baseline_commit / 读取异常)→ null(调用方跳过 advisory)
|
|
600
|
+
*/
|
|
601
|
+
export function deriveFlowDeliverableFace({ cwd, srcDir }) {
|
|
602
|
+
let baseline = null
|
|
603
|
+
try {
|
|
604
|
+
const raw = readFileSync(join(srcDir, 'flow-state.yaml'), 'utf8')
|
|
605
|
+
const m = raw.match(/^baseline_commit:\s*'?([0-9a-f]{7,40})'?\s*$/m)
|
|
606
|
+
baseline = m ? m[1] : null
|
|
607
|
+
} catch { return null }
|
|
608
|
+
if (!baseline) return null
|
|
609
|
+
const all = new Set()
|
|
610
|
+
const addAll = (out) => {
|
|
611
|
+
if (!out) return
|
|
612
|
+
for (const l of String(out).split('\n')) {
|
|
613
|
+
const p = l.trim().replace(/\\/g, '/')
|
|
614
|
+
if (p) all.add(p)
|
|
615
|
+
}
|
|
578
616
|
}
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
// 幂等自愈(issue archive-stage-physical-tracking-desync):源目录已不存在,但变更可能已被
|
|
588
|
-
// 手动 / 部分流程移到 changes/archive/ 并 commit,而 --done --confirm 从未正式跑完 → 进度 DB
|
|
589
|
-
// 卡 archive 阶段、active 列表仍列此 change。检测到已归档则回填进度(unregisterChange)并
|
|
590
|
-
// 成功返回,让收尾流程把 archive 阶段标完成,而非 exit(1) 死路(source 已移走无法重跑 move)。
|
|
591
|
-
const alreadyArchivedDir = findAlreadyArchivedDir(archiveDir, archiveChangeName)
|
|
592
|
-
if (alreadyArchivedDir) {
|
|
593
|
-
console.log(`ℹ️ 源目录不存在但变更已在 archive/(${basename(alreadyArchivedDir)}),判定已归档,自愈进度 DB`)
|
|
594
|
-
// 终态一致化(坑 manual-archive-desync-status-only):手动搬目录绕过标准归档后,本自愈路径
|
|
595
|
-
// 不能只翻 status——同时收尾 archive 阶段(steps/stages/current_stage),否则推送平台的终态
|
|
596
|
-
// 是「已归档 + 归档 0/5 + 停在 execute」的矛盾体,详情页渲染成「进度丢失」
|
|
597
|
-
pm.unregisterChange(cwd, archiveChangeName, { archiveStepNames: typeof pm.archiveStepNamesForArchive === 'function' ? pm.archiveStepNamesForArchive() : null })
|
|
598
|
-
await archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts)
|
|
599
|
-
console.log(`📦 已自愈归档:${archiveChangeName} → archive/${basename(alreadyArchivedDir)}/`)
|
|
600
|
-
return alreadyArchivedDir
|
|
617
|
+
addAll(gitQuiet(cwd, ['diff', '--name-only', `${baseline}..HEAD`]))
|
|
618
|
+
const s = gitQuiet(cwd, ['status', '--porcelain'])
|
|
619
|
+
if (s) {
|
|
620
|
+
for (const line of String(s).split('\n')) {
|
|
621
|
+
if (!line || line.length < 4) continue
|
|
622
|
+
const p = line.slice(3).trim().replace(/^"|"$/g, '')
|
|
623
|
+
const arrow = p.indexOf(' -> ')
|
|
624
|
+
all.add((arrow !== -1 ? p.slice(arrow + 4) : p).replace(/\\/g, '/'))
|
|
601
625
|
}
|
|
602
|
-
console.error(`❌ 归档失败:源目录不存在 ${srcDir}`)
|
|
603
|
-
console.error(` 且 changes/archive/ 下未找到该变更的归档目录。若已手动归档请核对目录名;否则先补全变更产物。`)
|
|
604
|
-
process.exit(1)
|
|
605
626
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
627
|
+
return {
|
|
628
|
+
deliverableFiles: [...all].filter((p) => !p.startsWith('.sillyspec/')),
|
|
629
|
+
docsIncrement: [...all].filter((p) => p.startsWith('.sillyspec/docs/')),
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* 模块文档认领 advisory(资产三小件③,advisory 不阻断归档链)。
|
|
635
|
+
*
|
|
636
|
+
* 背景:flow done/burst 收口不产生模块文档增量(旧流程的文档同步挂在 execute/verify 步骤里)
|
|
637
|
+
* ——R7-L 重放实证零模块文档的资产缺口。判定:deliverableFiles 交付面 × module-map
|
|
638
|
+
* (discoverModuleIndex 同源解析 parseModulePathsSubset,按项目扫描保卡片路径归属)交集
|
|
639
|
+
* ≥1 且本次变更无 .sillyspec/docs/** 增量 → 一行警告列出未认领模块卡路径。
|
|
640
|
+
* --no-docs 豁免仅存在于 quick 出口(run/command.js L2 docClaim),archive 链无该 flag——
|
|
641
|
+
* 出现豁免语义时在 runArchiveChain 接线处尊重。
|
|
642
|
+
* @returns {string|null} 命中 → 警告文案;负例(纯 spec 变更/有文档增量/无交集/无 map)→ null
|
|
643
|
+
*/
|
|
644
|
+
export function buildArchiveModuleDocAdvisory({ specBase, deliverableFiles, docsIncrement }) {
|
|
645
|
+
if (!Array.isArray(deliverableFiles) || deliverableFiles.length === 0) return null
|
|
646
|
+
if (Array.isArray(docsIncrement) && docsIncrement.length > 0) return null
|
|
647
|
+
const posix = deliverableFiles.map((f) => String(f).replace(/\\/g, '/')).filter((f) => f && !f.startsWith('.sillyspec/'))
|
|
648
|
+
if (posix.length === 0) return null
|
|
649
|
+
const docsDir = join(specBase, 'docs')
|
|
650
|
+
if (!existsSync(docsDir)) return null
|
|
651
|
+
const cards = []
|
|
652
|
+
for (const d of readdirSync(docsDir, { withFileTypes: true })) {
|
|
653
|
+
if (!d.isDirectory()) continue
|
|
654
|
+
const mapPath = join(docsDir, d.name, 'modules', '_module-map.yaml')
|
|
655
|
+
if (!existsSync(mapPath)) continue
|
|
656
|
+
let mods
|
|
657
|
+
try { mods = parseModulePathsSubset(readFileSync(mapPath, 'utf8')) } catch { continue }
|
|
658
|
+
for (const [id, m] of Object.entries(mods || {})) {
|
|
659
|
+
const prefixes = [...(m && m.paths ? m.paths : []), ...(m && m.core_files ? m.core_files : [])]
|
|
660
|
+
.map((p) => String(p).replace(/\\/g, '/').replace(/\/+$/, '')).filter(Boolean)
|
|
661
|
+
const hit = prefixes.some((p) => posix.some((f) => f === p || f.startsWith(p + '/')))
|
|
662
|
+
if (hit && m && typeof m.doc === 'string' && m.doc.trim() !== '') {
|
|
663
|
+
// doc 值相对 docs/<项目>/(如 modules/core-engine.md),不再叠 modules/ 段
|
|
664
|
+
const card = `.sillyspec/docs/${d.name}/${m.doc.replace(/^\.\//, '').replace(/\\/g, '/')}`
|
|
665
|
+
if (!cards.some((c) => c.card === card)) cards.push({ id, card })
|
|
666
|
+
}
|
|
620
667
|
}
|
|
621
|
-
if (check.action !== 'self') pm.setChangeOwner(cwd, archiveChangeName, session)
|
|
622
668
|
}
|
|
669
|
+
if (cards.length === 0) return null
|
|
670
|
+
return `⚠️ 模块文档认领 advisory:交付面命中 ${cards.map((c) => c.id).join('、')} 模块但本次变更无 .sillyspec/docs/** 增量——未认领模块卡:${cards.map((c) => c.card).join('、')}(advisory 不阻断归档;确认无需文档增量可忽略,需落盘则先补模块卡再归档)`
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ── 归档时间线烤制(2026-09-28-archive-timeline-bake)──
|
|
674
|
+
// 事件流(.runtime/watcher-events-<change>.jsonl)机本位:gitignore + 平台同步排除区,归档后
|
|
675
|
+
// 本机清理/跨机 clone 均无从合成时间线。本函数在归档链 rename 后调用:用与 `watcher timeline`
|
|
676
|
+
// CLI 同一合成面(readWatcherEvents × parseTaskLines/readBirthTs/flow-state tier ×
|
|
677
|
+
// resolveCommitAnchors(gitLook))把渲染文本写 destDir/timeline.md、原始事件字节副本写
|
|
678
|
+
// destDir/watcher-events.jsonl(尺寸帽防巨型事件流污染 git,超帽只烤渲染面)。整体 fail-open:
|
|
679
|
+
// 异常返回 {ok:false} 由调用方一行警告,不阻断归档。runtimeRoot 解析与 spawnWatcher 写侧同链
|
|
680
|
+
// (resolvePlatformOpts > resolveRuntimeRoot)——平台模式下事件流在平台 runtime 目录也能读对。
|
|
681
|
+
// gitLook/写读函数/尺寸帽/nowIso 为测试注入面(缺省 gitQuiet 包装与 index.js timeline 接线同形)。
|
|
682
|
+
export const BAKE_EVENTS_COPY_MAX_BYTES = 2 * 1024 * 1024
|
|
683
|
+
|
|
684
|
+
export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir, gitLook = null, writeImpl = writeFileSync, readRawImpl = readFileSync, eventsCopyMaxBytes = BAKE_EVENTS_COPY_MAX_BYTES, nowIso = null }) {
|
|
685
|
+
try {
|
|
686
|
+
const { resolvePlatformOpts } = await import('../progress.js')
|
|
687
|
+
const runtimeRoot = resolveRuntimeRoot(resolvePlatformOpts(cwd), specBase)
|
|
688
|
+
const { readWatcherEvents, watcherEventsPath } = await import('../watcher.js')
|
|
689
|
+
const res = readWatcherEvents({ runtimeRoot, change: changeName })
|
|
690
|
+
if (!res.exists) return { ok: true, skipped: true, reason: `无事件流(${watcherEventsPath(runtimeRoot, changeName)})` }
|
|
691
|
+
const { parseTaskLines, renderBakedTimeline, resolveCommitAnchors, readBirthTs } = await import('../timeline.js')
|
|
692
|
+
let tasks = null
|
|
693
|
+
try { tasks = parseTaskLines(readRawImpl(join(destDir, 'tasks.md'), 'utf8')) } catch { /* 任务面缺失照烤(渲染层有降级段) */ }
|
|
694
|
+
let tier = null
|
|
695
|
+
try { const m = /^tier:\s*(\S+)/m.exec(readRawImpl(join(destDir, 'flow-state.yaml'), 'utf8')); if (m) tier = m[1] } catch { /* 头脑风暴预段等无 flow-state 按未知 */ }
|
|
696
|
+
const look = gitLook || ((hash) => {
|
|
697
|
+
const out = gitQuiet(cwd, ['log', '-1', '--format=%h%x1f%aI%x1f%B', hash])
|
|
698
|
+
if (typeof out !== 'string' || !out.trim()) return null
|
|
699
|
+
const sep = out.indexOf('\x1f')
|
|
700
|
+
const rest = out.slice(sep + 1)
|
|
701
|
+
const sep2 = rest.indexOf('\x1f')
|
|
702
|
+
return { hash: out.slice(0, sep), dateISO: rest.slice(0, sep2), message: rest.slice(sep2 + 1) }
|
|
703
|
+
})
|
|
704
|
+
const anchors = resolveCommitAnchors(res.events, look)
|
|
705
|
+
let eventsRaw = null
|
|
706
|
+
try { eventsRaw = readRawImpl(watcherEventsPath(runtimeRoot, changeName), 'utf8') } catch { /* 副本缺源只烤渲染面 */ }
|
|
707
|
+
const oversize = eventsRaw != null && Buffer.byteLength(eventsRaw, 'utf8') > eventsCopyMaxBytes
|
|
708
|
+
// 评审 P3-1 清偿:副本未落盘(读源失败或超帽)时头注记必须显式「未随包」,不虚报在场
|
|
709
|
+
const timelineMd = renderBakedTimeline({ change: changeName, events: res.events, tasks, anchors, birthTs: readBirthTs(destDir), tier, bakedAtIso: nowIso || new Date().toISOString(), eventsCopySkipped: eventsRaw == null || oversize })
|
|
710
|
+
writeImpl(join(destDir, 'timeline.md'), timelineMd)
|
|
711
|
+
const files = ['timeline.md']
|
|
712
|
+
if (eventsRaw != null && !oversize) {
|
|
713
|
+
writeImpl(join(destDir, 'watcher-events.jsonl'), eventsRaw)
|
|
714
|
+
files.push('watcher-events.jsonl')
|
|
715
|
+
}
|
|
716
|
+
return { ok: true, skipped: false, files, eventsCopySkipped: oversize }
|
|
717
|
+
} catch (e) {
|
|
718
|
+
return { ok: false, skipped: false, error: e && e.message ? e.message : String(e) }
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* runArchiveChain —— 归档执行链(R7 切片二 task-02 从 archiveChangeDirectory 抽出,纯搬运)。
|
|
724
|
+
*
|
|
725
|
+
* 链面:未 apply 交付面检查(--skip-apply 留痕旁路)→ plan.md 硬校验(skipPlanCheck 旁路:
|
|
726
|
+
* flow thin 轻量工件面无 plan.md,task-03 flow done 归档子步传 true;既有 archive 流程缺省
|
|
727
|
+
* false 行为零变化)→ module-impact pending 死信校验 → 目标目录检查 → 目录搬移(rename 重试)
|
|
728
|
+
* → 时间线烤制(fail-open,2026-09-28-archive-timeline-bake)→ unregisterChange 终态一致化
|
|
729
|
+
* → archiveNarrowedGitAdd 窄化暂存 → 他者半归档残留探测。
|
|
730
|
+
* 消费方:archiveChangeDirectory(既有 archive 流程)与 flow done 归档子步(R7)。
|
|
731
|
+
*
|
|
732
|
+
* @returns {Promise<void>} 失败面沿链内既有 process.exit 语义(纯搬运不改)。
|
|
733
|
+
*/
|
|
734
|
+
export async function runArchiveChain({ pm, cwd, specBase, changeName, srcDir, destDir, skipApply = false, skipPlanCheck = false, deliverableFiles = null, docsIncrement = null } = {}) {
|
|
735
|
+
const archiveDir = dirname(destDir) // 抽取补:原闭包变量(changes/archive),从 destDir 推导等价
|
|
736
|
+
// ── 模块文档认领 advisory(资产三小件③,2026-09-23)──
|
|
737
|
+
// flow done/burst 收口不产生模块文档增量(旧流程挂在 execute/verify 步骤)——交付面 ×
|
|
738
|
+
// module-map 交集 ≥1 且无 .sillyspec/docs/** 增量 → 一行警告列未认领模块卡。advisory 不
|
|
739
|
+
// 阻断;fail-open(异常零输出)。交付面来源:调用方显式清单优先(burst 等未来调用方),
|
|
740
|
+
// 缺省从 srcDir/flow-state.yaml 的 baseline_commit 自算(deriveFlowDeliverableFace);非 flow
|
|
741
|
+
// 变更(无 flow-state.yaml)跳过——legacy archive 已有 module-impact「更新结果」死信硬门。
|
|
742
|
+
try {
|
|
743
|
+
const face = (deliverableFiles !== null && docsIncrement !== null)
|
|
744
|
+
? { deliverableFiles, docsIncrement }
|
|
745
|
+
: deriveFlowDeliverableFace({ cwd, srcDir })
|
|
746
|
+
const docMsg = face ? buildArchiveModuleDocAdvisory({ specBase, ...face }) : null
|
|
747
|
+
if (docMsg) console.warn(docMsg)
|
|
748
|
+
} catch { /* advisory fail-open:任何异常不碰归档链 */ }
|
|
623
749
|
// ── 归档收口(2026-09-14-change-ownership-guards task-03 / FR-02 / D-002@v1)──
|
|
624
750
|
// worktree 有未 apply 交付面 → 归档阻断:归档会注销 change + 清理 worktree(目录/分支/meta),
|
|
625
751
|
// 交付物失去进主仓通道,形成「归档完成但交付物未进主仓」悬空态(troubleshooting §65 事件①
|
|
@@ -630,28 +756,28 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platfo
|
|
|
630
756
|
try {
|
|
631
757
|
const { WorktreeManager } = await import('../worktree.js')
|
|
632
758
|
const wmGate = new WorktreeManager({ cwd })
|
|
633
|
-
const gateMeta = wmGate.getMeta(
|
|
759
|
+
const gateMeta = wmGate.getMeta(changeName)
|
|
634
760
|
if (gateMeta
|
|
635
761
|
&& gateMeta.mode !== 'in-place-fallback'
|
|
636
762
|
&& gateMeta.mode !== 'native-worktree'
|
|
637
763
|
&& gateMeta.worktreePath
|
|
638
764
|
&& existsSync(gateMeta.worktreePath)) {
|
|
639
765
|
const { applyWorktree } = await import('../worktree-apply.js')
|
|
640
|
-
const checkApply = applyWorktree(
|
|
766
|
+
const checkApply = applyWorktree(changeName, { cwd, checkOnly: true })
|
|
641
767
|
const face = (checkApply && Array.isArray(checkApply.changedFiles)) ? checkApply.changedFiles : []
|
|
642
768
|
if (face.length > 0) {
|
|
643
769
|
const facePreview = `${face.slice(0, 5).join('、')}${face.length > 5 ? ' 等' : ''}`
|
|
644
|
-
if (!
|
|
770
|
+
if (!skipApply) {
|
|
645
771
|
console.error(`❌ 归档失败:worktree 存在未 apply 的交付面(${face.length} 个文件:${facePreview})`)
|
|
646
772
|
console.error(` 归档会清理 worktree(目录+分支+meta),交付物将失去进主仓的通道,形成「归档完成但交付物未进主仓」悬空态。`)
|
|
647
|
-
console.error(` 先 apply 再归档: sillyspec worktree apply ${
|
|
773
|
+
console.error(` 先 apply 再归档: sillyspec worktree apply ${changeName}`)
|
|
648
774
|
console.error(` 确认无需 apply(交付面已由其他会话落地/纯探索性变更等): sillyspec run archive --done --confirm --skip-apply(显式跳过并留痕归档记录)`)
|
|
649
775
|
process.exit(1)
|
|
650
776
|
}
|
|
651
777
|
console.warn(`⚠️ 已按 --skip-apply 跳过未 apply 交付面检查(${face.length} 个文件未确认落地主仓:${facePreview})——留痕归档记录`)
|
|
652
778
|
writeFileSync(join(srcDir, 'skip-apply.record.json'), JSON.stringify({
|
|
653
779
|
schemaVersion: 1,
|
|
654
|
-
change:
|
|
780
|
+
change: changeName,
|
|
655
781
|
flag: '--skip-apply',
|
|
656
782
|
skippedAt: new Date().toISOString(),
|
|
657
783
|
deliverableCount: face.length,
|
|
@@ -667,7 +793,9 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platfo
|
|
|
667
793
|
}
|
|
668
794
|
// 移动前硬校验:变更包必须含 plan.md,否则不该归档。
|
|
669
795
|
// 在移动前阻断(而非移动后),目录尚未动,用户可直接修复后重试。
|
|
670
|
-
|
|
796
|
+
// skipPlanCheck(R7 切片二 / task-03 flow 轻量工件面):flow-state 在场的 thin change 无
|
|
797
|
+
// plan.md(轻量工件面四件套),flow done 归档子步传 true 旁路;既有 archive 流程缺省 false 零变化。
|
|
798
|
+
if (!skipPlanCheck && !existsSync(join(srcDir, 'plan.md'))) {
|
|
671
799
|
console.error(`❌ 归档失败:变更目录缺少 plan.md(${srcDir})`)
|
|
672
800
|
console.error(` plan.md 是归档的必需产物。请先补全 plan 阶段产出再归档。`)
|
|
673
801
|
process.exit(1)
|
|
@@ -705,9 +833,22 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platfo
|
|
|
705
833
|
process.exit(1)
|
|
706
834
|
}
|
|
707
835
|
|
|
836
|
+
// ── 时间线烤制(2026-09-28-archive-timeline-bake)──事件流机本位(.runtime gitignore),
|
|
837
|
+
// 归档时把合成时间线 + 原始事件副本烤进归档目录(跨机进 git);fail-open:跳过/失败一行
|
|
838
|
+
// 注记不阻断归档。位于 rename 之后(目录归本链独占、destName 文件面完整)、窄化 git add
|
|
839
|
+
// 之前(烤制文件随 archiveNarrowedGitAdd 确定性进暂存)。
|
|
840
|
+
try {
|
|
841
|
+
const baked = await bakeArchiveTimeline({ cwd, specBase, changeName, destDir })
|
|
842
|
+
if (baked.skipped) console.log(`⏭️ 时间线烤制跳过:${baked.reason}`)
|
|
843
|
+
else if (baked.ok) console.log(`📦 时间线烤制完成:${baked.files.join('、')}(随归档包进 git)`)
|
|
844
|
+
else console.warn(`⚠️ 时间线烤制失败(不阻断归档): ${baked.error}`)
|
|
845
|
+
} catch (e) {
|
|
846
|
+
console.warn(`⚠️ 时间线烤制异常(不阻断归档): ${e && e.message ? e.message : e}`)
|
|
847
|
+
}
|
|
848
|
+
|
|
708
849
|
// 正常路径同样走终态一致化(坑 manual-archive-desync-status-only):标准 --done --confirm 流程
|
|
709
850
|
// 中 completeStep 随后也会逐项完成,此处先收尾是幂等写同值——保证任何走出本函数的归档终态一致
|
|
710
|
-
pm.unregisterChange(cwd,
|
|
851
|
+
pm.unregisterChange(cwd, changeName, { archiveStepNames: typeof pm.archiveStepNamesForArchive === 'function' ? pm.archiveStepNamesForArchive() : null })
|
|
711
852
|
|
|
712
853
|
// CLI 下沉 git add(坑4,FR-04):确定性暂存归档目录 + 模块文档,不靠 step5 prompt 驱动。
|
|
713
854
|
// step5 prompt 的 git add 保留作幂等兜底;POSIX 路径跨平台(git 接受正斜杠)。
|
|
@@ -745,12 +886,12 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platfo
|
|
|
745
886
|
// 已暂存 rename → 半归档残留;按目标目录名归属他者变更
|
|
746
887
|
const m = /^\.sillyspec\/changes\/archive\/([^/]+)\//.exec(dst)
|
|
747
888
|
const owner = m ? m[1] : '?'
|
|
748
|
-
if (owner !==
|
|
889
|
+
if (owner !== changeName) othersResidual.push(owner)
|
|
749
890
|
} else if (x === ' ' && (y === 'D' || y === 'A' || y === 'M')) {
|
|
750
891
|
// 未暂存的源侧移动:源目录 D / 新位置 A——指向本变更的补暂存
|
|
751
892
|
const p = dst || src
|
|
752
|
-
if (p && (p.startsWith(changesPrefix +
|
|
753
|
-
|| p.startsWith(changesPrefix + 'archive/' +
|
|
893
|
+
if (p && (p.startsWith(changesPrefix + changeName + '/')
|
|
894
|
+
|| p.startsWith(changesPrefix + 'archive/' + changeName + '/'))) {
|
|
754
895
|
minePaths.push(p)
|
|
755
896
|
}
|
|
756
897
|
}
|
|
@@ -773,6 +914,60 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platfo
|
|
|
773
914
|
}
|
|
774
915
|
}
|
|
775
916
|
} catch { /* 探测失败不阻断归档(advisory) */ }
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export async function archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts = {}, gateOpts = {}) {
|
|
920
|
+
const archiveChangeName = progress.currentChange
|
|
921
|
+
if (!archiveChangeName) {
|
|
922
|
+
console.error('❌ 归档失败:未找到当前变更名(currentChange)')
|
|
923
|
+
process.exit(1)
|
|
924
|
+
}
|
|
925
|
+
const changesDir = join(specBase, 'changes')
|
|
926
|
+
const archiveDir = join(changesDir, 'archive')
|
|
927
|
+
const srcDir = join(changesDir, archiveChangeName)
|
|
928
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
929
|
+
const destName = archiveDestDirName(date, archiveChangeName)
|
|
930
|
+
const destDir = join(archiveDir, destName)
|
|
931
|
+
|
|
932
|
+
if (!existsSync(srcDir)) {
|
|
933
|
+
// 幂等自愈(issue archive-stage-physical-tracking-desync):源目录已不存在,但变更可能已被
|
|
934
|
+
// 手动 / 部分流程移到 changes/archive/ 并 commit,而 --done --confirm 从未正式跑完 → 进度 DB
|
|
935
|
+
// 卡 archive 阶段、active 列表仍列此 change。检测到已归档则回填进度(unregisterChange)并
|
|
936
|
+
// 成功返回,让收尾流程把 archive 阶段标完成,而非 exit(1) 死路(source 已移走无法重跑 move)。
|
|
937
|
+
const alreadyArchivedDir = findAlreadyArchivedDir(archiveDir, archiveChangeName)
|
|
938
|
+
if (alreadyArchivedDir) {
|
|
939
|
+
console.log(`ℹ️ 源目录不存在但变更已在 archive/(${basename(alreadyArchivedDir)}),判定已归档,自愈进度 DB`)
|
|
940
|
+
// 终态一致化(坑 manual-archive-desync-status-only):手动搬目录绕过标准归档后,本自愈路径
|
|
941
|
+
// 不能只翻 status——同时收尾 archive 阶段(steps/stages/current_stage),否则推送平台的终态
|
|
942
|
+
// 是「已归档 + 归档 0/5 + 停在 execute」的矛盾体,详情页渲染成「进度丢失」
|
|
943
|
+
pm.unregisterChange(cwd, archiveChangeName, { archiveStepNames: typeof pm.archiveStepNamesForArchive === 'function' ? pm.archiveStepNamesForArchive() : null })
|
|
944
|
+
await archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts)
|
|
945
|
+
console.log(`📦 已自愈归档:${archiveChangeName} → archive/${basename(alreadyArchivedDir)}/`)
|
|
946
|
+
return alreadyArchivedDir
|
|
947
|
+
}
|
|
948
|
+
console.error(`❌ 归档失败:源目录不存在 ${srcDir}`)
|
|
949
|
+
console.error(` 且 changes/archive/ 下未找到该变更的归档目录。若已手动归档请核对目录名;否则先补全变更产物。`)
|
|
950
|
+
process.exit(1)
|
|
951
|
+
}
|
|
952
|
+
// ── 所有权护栏(2026-09-14-change-ownership-guards task-03 / FR-01 / D-001@v1 接线点)──
|
|
953
|
+
// 归档=接管类操作(注销 change + 移目录 + 清 worktree),他人活跃 change 硬拒。判定在
|
|
954
|
+
// withMainRepoLock 锁内(handleArchiveConfirmStep 调用点),判定-执行无 TOCTOU;放行分支
|
|
955
|
+
// no-owner/takeover-stale 重写 owner=本会话(setChangeOwner 同事务刷新 last_active,心跳从
|
|
956
|
+
// 接管时刻起算)。会话标识三级解析:--session > env SILLYSPEC_SESSION_ID > anon@host(降级
|
|
957
|
+
// 时 resolveSessionIdentity 自带教学 warning)。消费 task-02 契约(assertChangeOwnership
|
|
958
|
+
// 纯读不写库),不重复实现判定语义。
|
|
959
|
+
{
|
|
960
|
+
const { session } = resolveSessionIdentity({ flagSession: gateOpts.sessionFlag || null, cwd })
|
|
961
|
+
const check = pm.assertChangeOwnership(cwd, archiveChangeName, { selfSession: session, nowMs: Date.now() })
|
|
962
|
+
if (!check.allowed) {
|
|
963
|
+
console.error(`🚫 归档失败:变更 ${archiveChangeName} 正被其他会话持有(owner: ${check.owner},最后活跃: ${check.lastActive || '未知'}),活跃窗口 ${Math.round(check.heartbeatMs / 60000)} 分钟内拒绝归档`)
|
|
964
|
+
console.error(` 等对方会话收尾后重试(窗口过后自动接管);确认对方已放弃且本会话应接管:先经 sillyspec worktree apply/cleanup ${archiveChangeName} --takeover 显式接管(重写 owner 留痕)再归档`)
|
|
965
|
+
process.exit(1)
|
|
966
|
+
}
|
|
967
|
+
if (check.action !== 'self') pm.setChangeOwner(cwd, archiveChangeName, session)
|
|
968
|
+
}
|
|
969
|
+
// ── 归档执行链(task-02 抽取:链体在 runArchiveChain,行为等价纯搬运)──
|
|
970
|
+
await runArchiveChain({ pm, cwd, specBase, changeName: archiveChangeName, srcDir, destDir, skipApply: Boolean(gateOpts.skipApply), skipPlanCheck: false })
|
|
776
971
|
|
|
777
972
|
// 归档时清理可能残留的 worktree(自愈路径也复用,见上方 srcDir 缺失分支)。
|
|
778
973
|
await archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts)
|
|
@@ -1265,6 +1460,25 @@ export async function handleWorkflowPostCheck({ stageName, steps, currentIdx, cw
|
|
|
1265
1460
|
}
|
|
1266
1461
|
return null
|
|
1267
1462
|
}
|
|
1463
|
+
/**
|
|
1464
|
+
* guard.linkedChanges/linkedChangesAuto 并入 --done 显式声明(R4-S-Q 缺陷 B 纯函数核):
|
|
1465
|
+
* 持久化值 ∪ 显式值(去重保序,persisted 在前);显式 ['none'] 语义为清空 manual 面(auto 不动);
|
|
1466
|
+
* 无变化返回原对象引用(调用点以此判「是否需要回写」)。guard 非对象原样返回。
|
|
1467
|
+
*/
|
|
1468
|
+
export function mergeGuardLinkedChanges(guard, explicitLinked, explicitAuto) {
|
|
1469
|
+
if (!guard || typeof guard !== 'object') return guard
|
|
1470
|
+
const cur = Array.isArray(guard.linkedChanges) ? guard.linkedChanges : []
|
|
1471
|
+
const curAuto = Array.isArray(guard.linkedChangesAuto) ? guard.linkedChangesAuto : []
|
|
1472
|
+
const ex = (Array.isArray(explicitLinked) ? explicitLinked : []).filter(Boolean)
|
|
1473
|
+
const exAuto = (Array.isArray(explicitAuto) ? explicitAuto : []).filter(Boolean)
|
|
1474
|
+
const next = ex.length === 1 && ex[0] === 'none' ? [] : [...new Set([...cur, ...ex])]
|
|
1475
|
+
const nextAuto = exAuto.length === 0 ? curAuto : [...new Set([...curAuto, ...exAuto])]
|
|
1476
|
+
const unchanged = next.length === cur.length && next.every((v, i) => v === cur[i])
|
|
1477
|
+
&& nextAuto.length === curAuto.length && nextAuto.every((v, i) => v === curAuto[i])
|
|
1478
|
+
if (unchanged) return guard
|
|
1479
|
+
return { ...guard, linkedChanges: next, linkedChangesAuto: nextAuto }
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1268
1482
|
/**
|
|
1269
1483
|
* quick 阶段完成收尾(W6 Step6b 从 completeStep 内联块抽出):
|
|
1270
1484
|
* 强校验 QUICKLOG 条目 + 审计(auditQuickCompletion)+ 结果摘要结构校验 + 翻状态/勾 tasks.md
|
|
@@ -1274,7 +1488,7 @@ export async function handleWorkflowPostCheck({ stageName, steps, currentIdx, cw
|
|
|
1274
1488
|
* isForceBaseline/isAllowNew/platformOpts。辅助函数直接 import(safeGit/auditQuickCompletion ← shared,
|
|
1275
1489
|
* printQuickAuditReview ← quick-audit,4 个 quicklog fns ← quicklog,unlinkSync/rmSync ← fs 静态)。
|
|
1276
1490
|
*/
|
|
1277
|
-
export async function handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, isNoDocs, sessionFlag = null, platformOpts, pm, quickFiles = [] }) {
|
|
1491
|
+
export async function handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, isNoDocs, sessionFlag = null, platformOpts, pm, quickFiles = [], linkedChanges = [], linkedChangesAuto = [] }) {
|
|
1278
1492
|
// quick 收尾:强校验 QUICKLOG 条目 + 翻状态 + 勾 tasks.md(CLI 接管)
|
|
1279
1493
|
if (stageName === 'quick') {
|
|
1280
1494
|
// §4.6 从 session guard.json 读 guard(不依赖 progress.quickGuard)。
|
|
@@ -1293,6 +1507,22 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1293
1507
|
: (existsSync(legacyGuardFile) ? JSON.parse(readFileSync(legacyGuardFile, 'utf8')) : null)
|
|
1294
1508
|
} catch {}
|
|
1295
1509
|
|
|
1510
|
+
// R4-S-Q 缺陷 B:--done 时显式 --linked-changes 并入 guard(启动持久化值 ∪ 本次显式值,
|
|
1511
|
+
// 'none' 清空)再回写 sessionGuardFile。下游关账 closeQuickLinkedChanges / 资产尾蒸馏 /
|
|
1512
|
+
// quicklog「关联变更」渲染全读 guard——不并入则 --done 时声明的关联被静默丢弃(R4-S-Q
|
|
1513
|
+
// 实测:--linked-changes 两次都在命令行上,资产尾零触发 + 条目渲染「(无)」)。
|
|
1514
|
+
// guard 缺失(brownfield)不造对象——保持 D-3 无 guard 降级语义不变。
|
|
1515
|
+
try {
|
|
1516
|
+
if (guard && (linkedChanges.length > 0 || linkedChangesAuto.length > 0)) {
|
|
1517
|
+
const merged = mergeGuardLinkedChanges(guard, linkedChanges, linkedChangesAuto)
|
|
1518
|
+
if (merged !== guard) {
|
|
1519
|
+
guard = merged
|
|
1520
|
+
writeAtomicSync(sessionGuardFile, JSON.stringify(guard, null, 2))
|
|
1521
|
+
console.log(`🔗 --done 显式关联变更已并入 guard: ${(guard.linkedChanges ?? []).join(', ') || '(none 清空)'}`)
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
} catch { /* 并入/回写失败不拦完成,guard 内存原值兜底 */ }
|
|
1525
|
+
|
|
1296
1526
|
// 强校验 / 收尾:本会话必须有一条真实 QUICKLOG 条目(治「报 SAFE 但漏写」bug)。
|
|
1297
1527
|
// guard 缺失(brownfield:新代码前启动的会话)不阻断——兜底补写一条记录,保住「完成必有记录」不变量。
|
|
1298
1528
|
const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
|
|
@@ -1401,8 +1631,8 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1401
1631
|
|
|
1402
1632
|
// ── quick 资产尾(2026-09-20-quick-asset-tail,D-001/D-002/D-005):门禁过后三件机械
|
|
1403
1633
|
// 事,agent 零新增命令/零写作义务,fail-open 全链(任何异常 warn 不拦 quick 完成)。
|
|
1404
|
-
// ①
|
|
1405
|
-
// + lite
|
|
1634
|
+
// ① 轻量通道蒸馏尾:linked 真变更(非 quick-<hex>)→ decision-distill + fr-index(幂等)
|
|
1635
|
+
// + lite 归档(治「轻量通道零资产 + 僵尸 brainstorm 态」——autocompact 对撞实证)。
|
|
1406
1636
|
// 挂点在门禁后(D-002 时序:防未实现设计进索引)。
|
|
1407
1637
|
// ② 模块 changelog 机械追加:changedFiles×moduleIndex join → 边车存在才 append 一行
|
|
1408
1638
|
// (纯 quick 的检索面——QUICKLOG 是时间流水账,模块边车是空间索引)。
|
|
@@ -1750,6 +1980,27 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1750
1980
|
softFiles,
|
|
1751
1981
|
})
|
|
1752
1982
|
console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
|
|
1983
|
+
// ── ql 绑定行落盘(2026-09-24-fr-test-bindings task-05,fr-test-binding §3.2/D-004@v1):
|
|
1984
|
+
// 窗口测试文件(命名三信号)机械落 candidate(discovery=machine/confirmed_by=null——
|
|
1985
|
+
// gate 实测证明「跑过且过」,不证明绑定关系正确,诚实恒 candidate;确认走 tests --bind)。
|
|
1986
|
+
// fail-open:落行异常不拦收尾。──
|
|
1987
|
+
try {
|
|
1988
|
+
const stripAnno = (f) => String(f).replace(/([^)]*)$/,'').trim()
|
|
1989
|
+
const isTestFile = (p) => /(^|\/)(test|tests|__tests__)(\/|$)/i.test(p) || /^test_/i.test(p.slice(p.lastIndexOf('/')+1)) || /[._](test|spec)\.[^.]+$/i.test(p)
|
|
1990
|
+
const testFiles = [...new Set((annotatedRealFiles || []).map(stripAnno).filter(isTestFile))]
|
|
1991
|
+
if (testFiles.length > 0) {
|
|
1992
|
+
const { upsertQlBindings } = await import('../test-bindings.js')
|
|
1993
|
+
upsertQlBindings({
|
|
1994
|
+
specBase, qlId,
|
|
1995
|
+
rows: testFiles.map((f, i) => ({
|
|
1996
|
+
anchor: qlId, row_id: `${changeName}:win-${i}:${f}`, tests: [f],
|
|
1997
|
+
reason: 'regression', state: 'candidate', discovery: 'machine', confirmed_by: null,
|
|
1998
|
+
source_change: changeName,
|
|
1999
|
+
})),
|
|
2000
|
+
})
|
|
2001
|
+
console.log(`🔗 ql 绑定候选落盘:${testFiles.length} 行(${qlId},candidate——确认走 sillyspec tests --bind)`)
|
|
2002
|
+
}
|
|
2003
|
+
} catch { /* ql 绑定落行 fail-open */ }
|
|
1753
2004
|
// 范围快照/patch 审计级落盘(quick-359a48f1):quicklog/patches/<qlId>.json+.patch——按
|
|
1754
2005
|
// ql-ID 对齐 QUICKLOG 条目(平台按条目抓取),json 冗余 sessionId 供 guard 清理后记录态
|
|
1755
2006
|
// 反查(computeQuickAudit → findQuickPatchRecord 同链)。patch 为 --done 时点全量冻结
|
|
@@ -2624,19 +2875,56 @@ export function checkKnowledgeBaselineRatchet(specBase) {
|
|
|
2624
2875
|
}
|
|
2625
2876
|
|
|
2626
2877
|
/**
|
|
2627
|
-
*
|
|
2628
|
-
*
|
|
2629
|
-
*
|
|
2878
|
+
* 知识收件箱横幅纯函数(2026-09-23 知识可见性 quick):待审条数 > 0 即渲染(不再基线内静默——
|
|
2879
|
+
* 用户反馈「聊天过程中完全不知道有待审知识」),标题直出聊天面(前 3 条 + 余量指引),超基线
|
|
2880
|
+
* 升级 ⚠️ 前缀。零条目零输出(降噪:清空即静默)。已 export 供 test/knowledge-inbox.test.mjs。
|
|
2881
|
+
*/
|
|
2882
|
+
export function buildKnowledgeInboxLines({ count = 0, baseline = null, over = false, titles = [] } = {}) {
|
|
2883
|
+
if (!Number.isFinite(count) || count <= 0) return []
|
|
2884
|
+
const head = over
|
|
2885
|
+
? `\n⚠️ 知识收件箱:uncategorized 待审 ${count} 条,超 knowledge-baseline 基线 ${baseline}(软警告不阻断)——请转达用户审阅,别只留在文件里`
|
|
2886
|
+
: `\n📚 知识收件箱:uncategorized 待审 ${count} 条——请转达用户审阅,别只留在文件里`
|
|
2887
|
+
const lines = [head]
|
|
2888
|
+
const shown = titles.filter(Boolean).slice(0, 3)
|
|
2889
|
+
shown.forEach((t, i) => lines.push(` ${i + 1}. ${t}`))
|
|
2890
|
+
if (count > shown.length) lines.push(` …(另 ${count - shown.length} 条,sillyspec knowledge inbox 看全文)`)
|
|
2891
|
+
lines.push(` 归类:sillyspec knowledge classify --title "…" --file conventions|patterns|known-issues.md`)
|
|
2892
|
+
return lines
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
/** uncategorized 待审标题清单(前 limit+1 条,供横幅与余量计算)。fail-open:读失败空集。 */
|
|
2896
|
+
function listUncategorizedTitles(specBase, limit = 3) {
|
|
2897
|
+
try {
|
|
2898
|
+
const p = join(specBase, 'knowledge', 'uncategorized.md')
|
|
2899
|
+
if (!existsSync(p)) return []
|
|
2900
|
+
return parseUncategorizedEntries(readFileSync(p, 'utf8').replace(/\r\n/g, '\n'))
|
|
2901
|
+
.slice(0, limit + 1).map((e) => e.title)
|
|
2902
|
+
} catch { return [] }
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
/**
|
|
2906
|
+
* 棘轮对比 + console 渲染(quick --done 与 archive 收尾共用):over → 收件箱横幅 ⚠️ 升级形态;
|
|
2907
|
+
* 基线内但有存量 → 📚 横幅(可见性修复:不再静默);tightened → 📉 单行留痕;零条目零输出。
|
|
2908
|
+
* 自身 try/catch fail-open(R-05:棘轮误伤的兜底就是什么都不做)。
|
|
2630
2909
|
*/
|
|
2631
2910
|
function renderKnowledgeBaselineRatchet(specBase) {
|
|
2632
2911
|
try {
|
|
2633
2912
|
const r = checkKnowledgeBaselineRatchet(specBase)
|
|
2634
2913
|
if (r && r.status === 'over') {
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2914
|
+
for (const l of buildKnowledgeInboxLines({ count: r.count, baseline: r.baseline, over: true, titles: listUncategorizedTitles(specBase) })) {
|
|
2915
|
+
console.warn(l)
|
|
2916
|
+
}
|
|
2917
|
+
return
|
|
2918
|
+
}
|
|
2919
|
+
if (r && r.status === 'tightened') {
|
|
2638
2920
|
console.log(`📉 knowledge-baseline 已自动收紧:${r.baseline} → ${r.count}(uncategorized 待归类条数下降)`)
|
|
2639
2921
|
}
|
|
2922
|
+
// 可见性修复(2026-09-23 用户反馈):基线内存量不再静默——有待审即出横幅,清空才静默
|
|
2923
|
+
if (r && Number.isFinite(r.count) && r.count > 0) {
|
|
2924
|
+
for (const l of buildKnowledgeInboxLines({ count: r.count, baseline: r.baseline, over: false, titles: listUncategorizedTitles(specBase) })) {
|
|
2925
|
+
console.log(l)
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2640
2928
|
} catch { /* 棘轮渲染失败零副作用(fail-open,R-05) */ }
|
|
2641
2929
|
}
|
|
2642
2930
|
|
|
@@ -2835,7 +3123,7 @@ async function runArchiveCeremonyDualRunExit({ cwd, specBase, changeName, platfo
|
|
|
2835
3123
|
// ── quick 资产尾(2026-09-20-quick-asset-tail,D-001/D-002/D-004)─────────────────
|
|
2836
3124
|
|
|
2837
3125
|
/**
|
|
2838
|
-
* lite 归档(D-004
|
|
3126
|
+
* lite 归档(D-004):轻量通道(brainstorm→linked quick)变更的轻量收口——所有权 assert →
|
|
2839
3127
|
* 命名 → rename → unregisterChange。不复用 archiveChangeDirectory 的 9 处重门(未 apply
|
|
2840
3128
|
* 交付面等 worktree 导向,linked quick 变更无 worktree 语义会被误拦);重件
|
|
2841
3129
|
* (module-impact/ROADMAP/delta)明确豁免。自愈:源目录缺失但已在 archive/ 时补
|
|
@@ -2875,19 +3163,19 @@ export async function liteArchiveChange({ pm, cwd, specBase, changeName, platfor
|
|
|
2875
3163
|
mkdirSync(archiveDir, { recursive: true })
|
|
2876
3164
|
renameSync(srcDir, destDir) // 先 rename 后 unregister:失败时 change 留 active 可重试(D-004 顺序保证)
|
|
2877
3165
|
pm.unregisterChange(cwd, changeName, { archiveStepNames: typeof pm.archiveStepNamesForArchive === 'function' ? pm.archiveStepNamesForArchive() : null })
|
|
2878
|
-
console.log(`📦 lite 归档:${changeName} → archive/${destName}
|
|
3166
|
+
console.log(`📦 lite 归档:${changeName} → archive/${destName}/(轻量通道轻收口:蒸馏已先行,module-impact/ROADMAP 豁免)`)
|
|
2879
3167
|
return { archivedTo: destDir }
|
|
2880
3168
|
}
|
|
2881
3169
|
|
|
2882
3170
|
/**
|
|
2883
|
-
*
|
|
3171
|
+
* 轻量通道蒸馏尾(D-001/D-002):quick --done 门禁过后对 linked 真变更(非 quick-<hex>)跑
|
|
2884
3172
|
* decision-distill + fr-index(均幂等)→ lite 归档。挂点在质量闸后(D-002:防未实现设计进
|
|
2885
3173
|
* 索引——quick 中途废弃永不 --done 永不蒸馏);两文件均缺失零打扰跳过;fail-open 全链
|
|
2886
3174
|
* (任何异常 warn 留痕不拦 quick 完成)。
|
|
2887
3175
|
* @returns {Promise<{distilled: boolean, frCount: number, decisionCount: number, archived: boolean, warnings: string[]}>}
|
|
2888
3176
|
*/
|
|
2889
3177
|
export async function distillLinkedChangeAssets({ pm, cwd, specBase, changeName, linkedChanges = [], platformOpts = {} }) {
|
|
2890
|
-
const out = { distilled: false, frCount: 0, decisionCount: 0, archived: false, warnings: [] }
|
|
3178
|
+
const out = { distilled: false, frCount: 0, decisionCount: 0, archived: false, warnings: [], frFiles: [] }
|
|
2891
3179
|
const realChanges = (linkedChanges || []).filter((c) => c && !QUICK_SID_RE.test(c))
|
|
2892
3180
|
if (realChanges.length === 0) return out // 纯 quick:无关联真变更,零打扰
|
|
2893
3181
|
try {
|
|
@@ -2911,14 +3199,15 @@ export async function distillLinkedChangeAssets({ pm, cwd, specBase, changeName,
|
|
|
2911
3199
|
// FR 索引(requirements.md → knowledge/fr/,幂等:同变更名 no-op)
|
|
2912
3200
|
if (hasReqs) {
|
|
2913
3201
|
try {
|
|
2914
|
-
const { indexRequirements } = await import('../fr-index.js')
|
|
2915
|
-
|
|
2916
|
-
|
|
3202
|
+
const { indexRequirements, archiveDeliverableFiles } = await import('../fr-index.js')
|
|
3203
|
+
// greenfield-bootstrap:供 deliverableFiles(同 archive-distill 口径——防域路由退化 unmapped)
|
|
3204
|
+
const r = indexRequirements({ changeDir, knowledgeRoot, headHash: '', deliverableFiles: archiveDeliverableFiles(changeDir) })
|
|
3205
|
+
if (r && Array.isArray(r.written) && r.written.length > 0) { out.frCount += r.written.length; r.written.forEach((w) => { if (w && w.file && !out.frFiles.includes(w.file)) out.frFiles.push(w.file) }); anyDistilled = true }
|
|
2917
3206
|
} catch (e) { out.warnings.push(`fr-index ${linked} 异常跳过:${e && e.message ? e.message : e}`) }
|
|
2918
3207
|
}
|
|
2919
3208
|
}
|
|
2920
3209
|
out.distilled = anyDistilled
|
|
2921
|
-
// lite 归档(蒸馏后;仅四件套容器变更——requirements.md 在场=brainstorm
|
|
3210
|
+
// lite 归档(蒸馏后;仅四件套容器变更——requirements.md 在场=brainstorm 承接的轻量通道形态;
|
|
2922
3211
|
// 纯信号自动关联的 proposal-only 变更不归档〔quick-single-change-auto-link §3 契约钉死〕)
|
|
2923
3212
|
for (const linked of realChanges) {
|
|
2924
3213
|
if (!existsSync(join(specBase, 'changes', linked, 'requirements.md'))) continue
|
|
@@ -2929,7 +3218,8 @@ export async function distillLinkedChangeAssets({ pm, cwd, specBase, changeName,
|
|
|
2929
3218
|
} catch (e) { out.warnings.push(`lite 归档 ${linked} 异常跳过:${e && e.message ? e.message : e}`) }
|
|
2930
3219
|
}
|
|
2931
3220
|
if (anyDistilled || out.archived) {
|
|
2932
|
-
|
|
3221
|
+
const frDetail = out.frFiles.length > 0 ? `(${out.frFiles.join('、')})` : ''
|
|
3222
|
+
console.log(`📚 quick 资产尾:${out.frCount} 条 FR 入索引${frDetail}、${out.decisionCount} 条决策入 knowledge${out.archived ? '、linked 变更 lite 归档' : ''}(门禁后自动,agent 零新增命令)`)
|
|
2933
3223
|
}
|
|
2934
3224
|
} catch (e) {
|
|
2935
3225
|
out.warnings.push(`蒸馏尾异常(fail-open 不拦 quick 完成):${e && e.message ? e.message : e}`)
|