sillyspec 3.29.4 → 3.31.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 +1 -1
- 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 +7 -105
- 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
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* greenfield-bootstrap.js — 绿地仓模块图草案起草(2026-09-25-greenfield-bootstrap)
|
|
3
|
+
*
|
|
4
|
+
* R17 实证:无模块图的新仓,FR 域路由全落伪域/unmapped(臂2 auto-claude、臂3 unmapped),
|
|
5
|
+
* 知识复利从第一条断流。本模块在 flow start 检测到模块图缺席且 --input 有路径语料时,
|
|
6
|
+
* 机器起草初始 _module-map.yaml:
|
|
7
|
+
* - 模块 id = 路径目录段里首个非泛化段(cli/server/api…),paths = 该文件所在目录(/ 结尾)
|
|
8
|
+
* - generator: flow-bootstrap-draft + status: draft 自明身份——与 scan 产的正式图区分;
|
|
9
|
+
* 模块图契约本就是机器(scan)产物,「手动维护」指 paths 后续增量,缺席首建不冲突
|
|
10
|
+
* (方案评审裁定三条件:草案头带警告 + draft 标识 + 不含 blast 段——判级对缺席安全降级 S1)
|
|
11
|
+
* - 不覆盖已有文件;全库任一 _module-map.yaml 在场(多项目仓)即不写(幂等)
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { join, dirname } from 'node:path'
|
|
15
|
+
import { writeAtomicSync } from './fs-atomic.js'
|
|
16
|
+
|
|
17
|
+
const GENERIC_SEGMENTS = new Set(['src', 'lib', 'test', 'tests', 'spec', 'docs', 'doc', 'app', 'packages', 'scripts', 'config', 'public', 'internal', 'node_modules'])
|
|
18
|
+
|
|
19
|
+
/** 路径组 → { moduleId → Set<dirPath> }(目录以 / 结尾;根文件无模块归属丢弃)。 */
|
|
20
|
+
function groupByModule(paths) {
|
|
21
|
+
const mods = new Map()
|
|
22
|
+
for (const raw of paths) {
|
|
23
|
+
const p = String(raw || '').replace(/\\/g, '/')
|
|
24
|
+
const segs = p.split('/').filter(Boolean)
|
|
25
|
+
if (segs.length < 2) continue // 根文件:无目录段
|
|
26
|
+
const dirSegs = segs.slice(0, -1)
|
|
27
|
+
const rawMod = dirSegs.find((s) => !GENERIC_SEGMENTS.has(s.toLowerCase()))
|
|
28
|
+
if (!rawMod) continue // 全泛化段(src/x.js):无可命名模块——留给 unmapped 兜底
|
|
29
|
+
// id 消毒(评审 P2 清偿):FR 域正则 [a-z0-9-]——大写/点/下划线段会铸出不可回读域;
|
|
30
|
+
// 消毒后空(纯符号段如 .github → github,纯点段丢弃)则该路径不建模块
|
|
31
|
+
const modId = rawMod.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^[-.]+|[-.]+$/g, '')
|
|
32
|
+
if (!modId || /^-+$/.test(modId)) continue
|
|
33
|
+
const dir = dirSegs.join('/') + '/'
|
|
34
|
+
if (!mods.has(modId)) mods.set(modId, new Set())
|
|
35
|
+
mods.get(modId).add(dir)
|
|
36
|
+
}
|
|
37
|
+
return mods
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 项目目录名:package.json name 优先(sanitize 成文件名安全段),fallback 'app'。 */
|
|
41
|
+
function projectName(cwd) {
|
|
42
|
+
try {
|
|
43
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'))
|
|
44
|
+
const n = String(pkg && pkg.name || '').split('/').pop().replace(/[^\w.-]/g, '-').replace(/^[-.]+|[-.]+$/g, '')
|
|
45
|
+
if (n) return n
|
|
46
|
+
} catch { /* 非 node 项目或缺件 */ }
|
|
47
|
+
return 'app'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 起草初始 _module-map.yaml。
|
|
52
|
+
* @param {{ cwd: string, specBase: string, paths: string[] }} opts
|
|
53
|
+
* @returns {{ written: boolean, path?: string, modules: number, moduleIds: string[], reason?: string }}
|
|
54
|
+
*/
|
|
55
|
+
export function draftModuleMap({ cwd, specBase, paths }) {
|
|
56
|
+
const docsDir = join(specBase, 'docs')
|
|
57
|
+
// 幂等闸门:任一项目已有模块图(含草案)即不写——多项目仓防重复建
|
|
58
|
+
try {
|
|
59
|
+
if (existsSync(docsDir)) {
|
|
60
|
+
for (const e of readdirSync(docsDir, { withFileTypes: true })) {
|
|
61
|
+
if (e.isDirectory() && existsSync(join(docsDir, e.name, 'modules', '_module-map.yaml'))) {
|
|
62
|
+
return { written: false, modules: 0, moduleIds: [], reason: `已有模块图(${e.name})` }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch { /* 扫描异常按缺席处理 */ }
|
|
67
|
+
const mods = groupByModule(paths)
|
|
68
|
+
if (mods.size === 0) return { written: false, modules: 0, moduleIds: [], reason: '路径语料无可命名模块(全泛化段/根文件)' }
|
|
69
|
+
const proj = projectName(cwd)
|
|
70
|
+
const mapDir = join(docsDir, proj, 'modules')
|
|
71
|
+
mkdirSync(mapDir, { recursive: true })
|
|
72
|
+
const mapPath = join(mapDir, '_module-map.yaml')
|
|
73
|
+
if (existsSync(mapPath)) return { written: false, modules: 0, moduleIds: [], reason: '目标文件已存在(不覆盖)' }
|
|
74
|
+
const lines = [
|
|
75
|
+
'schema_version: 2',
|
|
76
|
+
'generated_at: ' + new Date().toISOString(),
|
|
77
|
+
'generator: flow-bootstrap-draft',
|
|
78
|
+
'status: draft',
|
|
79
|
+
'',
|
|
80
|
+
'# ⚠️ 绿地草案——flow start 按 --input 路径语料机器聚合,未经 scan 校准。',
|
|
81
|
+
'# 校准路径:sillyspec run scan(或 modules 重建)后转正;转正前域路由按本草案分流(不再落 unmapped)。',
|
|
82
|
+
'# ⚠️ 勿跑 modules rebuild --force——会清空手动维护的 paths(与正式图同警告)。',
|
|
83
|
+
'',
|
|
84
|
+
'modules:',
|
|
85
|
+
]
|
|
86
|
+
const ids = [...mods.keys()].sort()
|
|
87
|
+
for (const id of ids) {
|
|
88
|
+
lines.push(` ${id}:`)
|
|
89
|
+
lines.push(' status: draft')
|
|
90
|
+
lines.push(' paths:')
|
|
91
|
+
for (const p of [...mods.get(id)].sort()) lines.push(` - ${p}`)
|
|
92
|
+
lines.push('')
|
|
93
|
+
}
|
|
94
|
+
writeAtomicSync(mapPath, lines.join('\n'))
|
|
95
|
+
return { written: true, path: mapPath.replace(/\\/g, '/'), modules: ids.length, moduleIds: ids }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default { draftModuleMap }
|
package/src/handoff.js
CHANGED
|
@@ -6,13 +6,66 @@
|
|
|
6
6
|
* 本就自足(进度快照/模块命中/知识注入全由进度库与 prompt 渲染提供,新会话零背景可
|
|
7
7
|
* 续跑)——「每阶段一个瘦会话」是零机制改动的省法,缺的只是把交接信息打成一块可粘贴。
|
|
8
8
|
*
|
|
9
|
-
* 定位:纯只读(ProgressManager 读路径 +
|
|
9
|
+
* 定位:纯只读(ProgressManager 读路径 + 变更目录文本 + 环境探测),不写盘不动状态。输出:
|
|
10
10
|
* ① 新会话启动块(保持 SILLYSPEC_SESSION_ID 不变——变更所有权按此判定);
|
|
11
11
|
* ② 下一阶段建议(当前阶段完成 → MAIN_FLOW_ORDER 后继;进行中 → 同阶段续跑);
|
|
12
|
-
* ③ 当前状态摘要(阶段进度 +
|
|
12
|
+
* ③ 当前状态摘要(阶段进度 + 停留步骤);
|
|
13
|
+
* ④ 上下文三段(B4a 加厚,2026-09-21-r5 优化 C-2):未决阻断(waiting/blocked/failed 步骤)/
|
|
14
|
+
* 任务面(tasks.md checkbox 计数+待办清单)/ 决策 ID 清单(decisions.md D-xxx@vN,翻案先回源)——
|
|
15
|
+
* 整块 80 行帽(HANDOFF_LINE_CAP),超限从上下文段尾截,粘贴块与警示行永不动。
|
|
13
16
|
*/
|
|
14
17
|
import { ProgressManager, resolveSpecDir } from './progress.js';
|
|
15
18
|
import { MAIN_FLOW_ORDER, STAGE_LABELS } from './progress/shared.js';
|
|
19
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
|
|
22
|
+
/** 交接块总行数帽(优化方案 C-2 B4a:把「任务太大爆窗」从硬墙变操作规程——块本身不许变大到失控) */
|
|
23
|
+
const HANDOFF_LINE_CAP = 80;
|
|
24
|
+
|
|
25
|
+
function readChangeFile(specRoot, change, name) {
|
|
26
|
+
try { return readFileSync(join(specRoot, 'changes', change, name), 'utf8'); } catch { return null; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 任务面摘要:tasks.md checkbox 行计数(done/total + 待办 ID 清单)。
|
|
31
|
+
* @param {string|null} tasksMdText
|
|
32
|
+
* @returns {string|null} 无 checkbox 行(非 execute 期/未落盘)返回 null
|
|
33
|
+
*/
|
|
34
|
+
export function summarizeTaskFace(tasksMdText) {
|
|
35
|
+
if (!tasksMdText) return null;
|
|
36
|
+
const boxes = [...tasksMdText.matchAll(/^-\s?\[([ xX])\]\s*(task-\d+)/gm)];
|
|
37
|
+
if (!boxes.length) return null;
|
|
38
|
+
const pending = boxes.filter((m) => m[1].toLowerCase() !== 'x').map((m) => m[2]);
|
|
39
|
+
let line = `📦 任务面:${boxes.length - pending.length}/${boxes.length} 完成`;
|
|
40
|
+
if (pending.length) {
|
|
41
|
+
line += `(待办:${pending.slice(0, 8).join('、')}${pending.length > 8 ? ` 等 ${pending.length} 项` : ''})`;
|
|
42
|
+
}
|
|
43
|
+
return line;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 决策清单摘要:decisions.md 的 `## D-xxx@vN` 标题行 ID(跨会话恢复时按 ID 回源翻否决理由)。
|
|
48
|
+
* @param {string|null} decisionsMdText
|
|
49
|
+
* @returns {string|null}
|
|
50
|
+
*/
|
|
51
|
+
export function summarizeDecisions(decisionsMdText) {
|
|
52
|
+
if (!decisionsMdText) return null;
|
|
53
|
+
const ids = [...decisionsMdText.matchAll(/^##\s+(D-[\w-]+@v\d+)/gm)].map((m) => m[1]);
|
|
54
|
+
if (!ids.length) return null;
|
|
55
|
+
return `🧠 决策:${ids.slice(0, 10).join('、')}${ids.length > 10 ? ` 等 ${ids.length} 条` : ''}(翻案/复潮先读 decisions.md 对应条目)`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 未决阻断摘要:进度库步骤态 waiting/blocked/failed(--wait 决策点与门禁拦截点)。
|
|
60
|
+
* @param {object|null} stageData
|
|
61
|
+
* @returns {string|null}
|
|
62
|
+
*/
|
|
63
|
+
export function summarizeBlockers(stageData) {
|
|
64
|
+
const steps = Array.isArray(stageData && stageData.steps) ? stageData.steps : [];
|
|
65
|
+
const stuck = steps.filter((s) => s && (s.status === 'waiting' || s.status === 'blocked' || s.status === 'failed'));
|
|
66
|
+
if (!stuck.length) return null;
|
|
67
|
+
return `⛔ 未决:${stuck.slice(0, 3).map((s) => `「${String(s.name || s.stepName || s.title || `Step ${s.index ?? '?'}`).slice(0, 20)}」${s.status}`).join(';')}${stuck.length > 3 ? ` 等 ${stuck.length} 步` : ''}`;
|
|
68
|
+
}
|
|
16
69
|
|
|
17
70
|
/**
|
|
18
71
|
* 解析下一阶段建议。
|
|
@@ -52,12 +105,14 @@ export async function buildHandoff({ cwd, specBase, changeName } = {}) {
|
|
|
52
105
|
let target = changeName;
|
|
53
106
|
let picked = false;
|
|
54
107
|
if (!target) {
|
|
55
|
-
// 唯一活跃主流程变更自动选中(listChanges=目录+DB 联合口径,read 取 currentStage
|
|
108
|
+
// 唯一活跃主流程变更自动选中(listChanges=目录+DB 联合口径,read 取 currentStage);
|
|
109
|
+
// 轻量变更(flow-state.yaml 在场)同样入候选——thin 变更也需要跨会话交接(评审 P3 清偿)
|
|
110
|
+
const isThinChange = (cn) => existsSync(join(specRoot, 'changes', cn, 'flow-state.yaml'));
|
|
56
111
|
const names = pm.listChanges(cwd) || [];
|
|
57
112
|
const mains = [];
|
|
58
113
|
for (const cn of names) {
|
|
59
114
|
const p = pm.read(cwd, cn);
|
|
60
|
-
if (p && MAIN_FLOW_ORDER.includes(p.currentStage || '')) mains.push({ name: cn, stage: p.currentStage });
|
|
115
|
+
if (p && (MAIN_FLOW_ORDER.includes(p.currentStage || '') || isThinChange(cn))) mains.push({ name: cn, stage: p.currentStage });
|
|
61
116
|
}
|
|
62
117
|
if (mains.length === 1) {
|
|
63
118
|
target = mains[0].name;
|
|
@@ -65,7 +120,7 @@ export async function buildHandoff({ cwd, specBase, changeName } = {}) {
|
|
|
65
120
|
} else if (mains.length > 1) {
|
|
66
121
|
return { ok: false, error: `多活跃主流程变更(${mains.length} 个:${mains.map((m) => m.name).join('、')})——请带 --change <名> 指定` };
|
|
67
122
|
} else {
|
|
68
|
-
return { ok: false, error: '
|
|
123
|
+
return { ok: false, error: '无活跃主流程/轻量变更(quick/scan 会话或全部归档)——handoff 面向主流程与轻量变更交接' };
|
|
69
124
|
}
|
|
70
125
|
}
|
|
71
126
|
|
|
@@ -75,12 +130,25 @@ export async function buildHandoff({ cwd, specBase, changeName } = {}) {
|
|
|
75
130
|
const currentStage = progress.currentStage || '';
|
|
76
131
|
const stageData = (progress.stages && progress.stages[currentStage]) || null;
|
|
77
132
|
const stageCompleted = !!(stageData && stageData.status === 'completed');
|
|
78
|
-
|
|
133
|
+
let suggestion = nextStageSuggestion(currentStage, stageCompleted);
|
|
134
|
+
// 轻量变更(thin/flow)感知(2026-09-25-quick-channel-retire 评审 P1):flow-state.yaml 在场 =
|
|
135
|
+
// 轻量变更,续跑/收口走 flow 协议——run <stage> 对 thin 变更会被混跑守卫拒(升厚须用户同意
|
|
136
|
+
// 带 --upgrade-thick),交接命令改写为 flow 入口
|
|
137
|
+
const isThin = existsSync(join(specRoot, 'changes', target, 'flow-state.yaml'));
|
|
138
|
+
if (isThin) {
|
|
139
|
+
suggestion = { next: 'flow', mode: 'resume', reason: '轻量变更(thin)——新会话重跑 flow start 看恢复简报,干完跑 flow done 收口;run <stage> 会被混跑守卫拒(升厚须用户同意带 --upgrade-thick)' };
|
|
140
|
+
}
|
|
79
141
|
const sessionId = process.env.SILLYSPEC_SESSION_ID || null;
|
|
80
142
|
|
|
81
143
|
const steps = Array.isArray(stageData && stageData.steps) ? stageData.steps : [];
|
|
82
144
|
const doneSteps = steps.filter((s) => s && s.status === 'completed').length;
|
|
83
145
|
|
|
146
|
+
// ── 上下文三段(B4a 加厚:任务面 / 未决 / 决策——进度库+变更目录机械抽取,仍纯只读)──
|
|
147
|
+
const faceLine = summarizeTaskFace(readChangeFile(specRoot, target, 'tasks.md'));
|
|
148
|
+
const decisionLine = summarizeDecisions(readChangeFile(specRoot, target, 'decisions.md'));
|
|
149
|
+
const blockerLine = summarizeBlockers(stageData);
|
|
150
|
+
const contextLines = [blockerLine, faceLine, decisionLine].filter(Boolean);
|
|
151
|
+
|
|
84
152
|
const lines = [];
|
|
85
153
|
lines.push('🔗 交接块(瘦会话模式——下一阶段/续跑在新会话执行,主会话收尾省上下文)');
|
|
86
154
|
lines.push('');
|
|
@@ -88,7 +156,9 @@ export async function buildHandoff({ cwd, specBase, changeName } = {}) {
|
|
|
88
156
|
if (sessionId) lines.push(`export SILLYSPEC_SESSION_ID=${sessionId}`);
|
|
89
157
|
else lines.push('export SILLYSPEC_SESSION_ID=<本变更的会话标识——旧会话 echo $SILLYSPEC_SESSION_ID 取>');
|
|
90
158
|
lines.push(`cd ${cwd}`);
|
|
91
|
-
if (suggestion.next) {
|
|
159
|
+
if (suggestion.next === 'flow') {
|
|
160
|
+
lines.push(`sillyspec flow start --change ${target}`);
|
|
161
|
+
} else if (suggestion.next) {
|
|
92
162
|
lines.push(`sillyspec run ${suggestion.next} --change ${target}`);
|
|
93
163
|
} else {
|
|
94
164
|
lines.push('# (流程终态,无续跑命令)');
|
|
@@ -96,10 +166,52 @@ export async function buildHandoff({ cwd, specBase, changeName } = {}) {
|
|
|
96
166
|
lines.push('# ── 交接块结束 ──');
|
|
97
167
|
lines.push('');
|
|
98
168
|
lines.push(`📋 ${target}:当前 ${STAGE_LABELS[currentStage] || currentStage}${steps.length > 0 ? `(步骤 ${doneSteps}/${steps.length})` : ''}${stageCompleted ? ' ✅ 已完成' : ''}`);
|
|
169
|
+
const contextStart = lines.length;
|
|
170
|
+
for (const c of contextLines) lines.push(c);
|
|
171
|
+
// 80 行帽:粘贴块(头)与警示行(尾)不动,超限从上下文段尾截(决策→任务面→未决,重要性递增反向丢)
|
|
172
|
+
let truncated = false;
|
|
173
|
+
while (lines.length + 3 > HANDOFF_LINE_CAP && lines.length > contextStart) {
|
|
174
|
+
lines.splice(lines.length - 1, 1);
|
|
175
|
+
truncated = true;
|
|
176
|
+
}
|
|
177
|
+
if (truncated) lines.push('…(上下文超 80 行帽截尾——任务面/决策明细回源 tasks.md 与 decisions.md)');
|
|
178
|
+
// 机器预览态段(2026-09-23-watcher-preview-progress task-03 / FR-05):watcher 投影的
|
|
179
|
+
// 进行态参考——新会话据此快速恢复现场(证据引用可回源事件流)。无预览行时零输出(不添行)。
|
|
180
|
+
try {
|
|
181
|
+
const { readPreviewProgress } = await import('./progress.js');
|
|
182
|
+
const pv = readPreviewProgress(specRoot, target);
|
|
183
|
+
if (pv.exists && pv.stages.length > 0) {
|
|
184
|
+
lines.push('');
|
|
185
|
+
lines.push(`👀 机器预览态(watcher 推断 ${pv.stages.length} 行——非流程真相,权威以 progress show 为准):`);
|
|
186
|
+
for (const r of pv.stages) {
|
|
187
|
+
const files = r.evidence && Array.isArray(r.evidence.files) ? r.evidence.files.join('/') : '';
|
|
188
|
+
lines.push(` [预览] ${r.stage} → ${r.status}${files ? `(依据:${files})` : ''} 最后活动 ${r.startedAt || '?'}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch { /* 预览段 best-effort,不影响交接块本体 */ }
|
|
192
|
+
|
|
99
193
|
lines.push(`➡️ ${suggestion.reason}`);
|
|
100
194
|
lines.push('💡 为什么可行:CLI prompt 自足——进度快照/模块命中/知识注入由进度库与渲染提供,新会话零背景可续跑。');
|
|
101
195
|
lines.push('⚠️ SILLYSPEC_SESSION_ID 必须保持不变(变更所有权/接管判定按此标识);缺省降级机器级标识只拦他机。');
|
|
102
196
|
|
|
197
|
+
// handoffAt 切割凭证(R16-b 实证修正 2026-09-25):stage.wall=hard 的墙按「前驱收口后
|
|
198
|
+
// 是否执行过 handoff」判定硬续(SESSION_ID 是所有权标识非会话标识,handoff 要求新会话
|
|
199
|
+
// 保持不变——SESSION_ID 相同不能当硬续证据)。本命令执行即切割:best-effort 给
|
|
200
|
+
// stage-session ledger 盖 handoffAt 章,墙据此放行新会话的 execute/verify 首入。
|
|
201
|
+
try {
|
|
202
|
+
const ledgerPath = join(specRoot, '.runtime', `stage-session-ledger-${target}.json`);
|
|
203
|
+
let ledger = null;
|
|
204
|
+
try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')); } catch { /* 无账本(阶段未收口)→ 无墙可放行,零动作 */ }
|
|
205
|
+
if (ledger && typeof ledger === 'object') {
|
|
206
|
+
ledger.handoffAt = new Date().toISOString();
|
|
207
|
+
try {
|
|
208
|
+
const { writeFileSync, mkdirSync } = await import('node:fs');
|
|
209
|
+
mkdirSync(join(specRoot, '.runtime'), { recursive: true });
|
|
210
|
+
writeFileSync(ledgerPath, JSON.stringify(ledger, null, 1) + '\n', 'utf8');
|
|
211
|
+
} catch { /* 盖章失败不影响交接块本体 */ }
|
|
212
|
+
}
|
|
213
|
+
} catch { /* handoffAt 盖章 best-effort */ }
|
|
214
|
+
|
|
103
215
|
return {
|
|
104
216
|
ok: true,
|
|
105
217
|
change: target,
|
|
@@ -108,6 +220,7 @@ export async function buildHandoff({ cwd, specBase, changeName } = {}) {
|
|
|
108
220
|
stageCompleted,
|
|
109
221
|
suggestion,
|
|
110
222
|
sessionId,
|
|
223
|
+
truncated,
|
|
111
224
|
lines,
|
|
112
225
|
};
|
|
113
226
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hunk-attribution —— 提交面行级归属门(2026-09-27-hunk-attribution-gate)。
|
|
3
|
+
*
|
|
4
|
+
* 背景事故(本仓 4f859053 实证):两个会话并发编辑同一文件,文件级 pathspec 提交把并行
|
|
5
|
+
* 会话 25 行在途代码夹带进冻结面——规则 11 的文件级对账防「别的文件」,防不住「同一文件
|
|
6
|
+
* 里别人的行」。用户裁决:不默认挂工作树(合并税过重,wt-parallel-commit-race 等坑史为
|
|
7
|
+
* 证),改为把对账升到 hunk 级归属门。
|
|
8
|
+
*
|
|
9
|
+
* 三类信号(诚实口径:机器无法判定「哪行是谁的」,门的目标是把静默混合变成显式中断):
|
|
10
|
+
* ① 未归因文件:提交面文件不在本变更声明面(design 清单 ∪ requirements 测试绑定,
|
|
11
|
+
* 复用 parseFileChangeList / extractRequirementBindings / testAnchorFile 同源解析);
|
|
12
|
+
* ② 跨变更竞争:其他活跃变更的声明面与提交面相交——同文件无法按行归属,显式列出
|
|
13
|
+
* 竞争文件、对方变更名与 hunk 数,收口前人工核对;
|
|
14
|
+
* ③ 在途残留:提交面文件当前工作树仍有未提交 diff——活跃并发 WIP 信号。
|
|
15
|
+
*
|
|
16
|
+
* 分级:local.yaml hunk_gate(warn 默认 / error / off)。error 档由接线方(flow.js probes
|
|
17
|
+
* 子步)阻断收口;本模块只产结果不执法。fail-soft:非 git 环境与异常降级跳过。
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
20
|
+
import { join } from 'node:path'
|
|
21
|
+
|
|
22
|
+
/** local.yaml 读 hunk_gate(warn|error|off,默认 warn;CRLF 容错)。 */
|
|
23
|
+
export function readHunkGate(specBase) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = readFileSync(join(specBase, 'local.yaml'), 'utf8')
|
|
26
|
+
const m = raw.match(/^\s*hunk_gate\s*:\s*['\"]?(warn|error|off)['\"]?\s*$/m)
|
|
27
|
+
return m ? m[1] : 'warn'
|
|
28
|
+
} catch {
|
|
29
|
+
return 'warn'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizePath(p) {
|
|
34
|
+
return String(p).replace(/^NEW:\s*/, '').replace(/\\/g, '/').trim()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 本变更声明面(design 清单 ∪ requirements 测试绑定路径)。解析器与 flow done 既有
|
|
39
|
+
* 「提交面夹带嫌疑 advisory」同源(不二算口径一致)。
|
|
40
|
+
* @returns {{ declared: Set<string>, parseFailed: boolean }}
|
|
41
|
+
*/
|
|
42
|
+
export async function collectDeclaredFace(changeDir, change) {
|
|
43
|
+
const declared = new Set()
|
|
44
|
+
let parseFailed = false
|
|
45
|
+
try {
|
|
46
|
+
const { parseFileChangeList } = await import('./change-list.js')
|
|
47
|
+
for (const p of parseFileChangeList(join(changeDir, 'design.md'))) declared.add(normalizePath(p))
|
|
48
|
+
} catch { parseFailed = true }
|
|
49
|
+
try {
|
|
50
|
+
const { extractRequirementBindings } = await import('./flow-draft.js')
|
|
51
|
+
const { testAnchorFile } = await import('./test-bindings.js')
|
|
52
|
+
for (const row of extractRequirementBindings({ changeDir, change })) {
|
|
53
|
+
for (const t of row.tests || []) declared.add(normalizePath(testAnchorFile(t)))
|
|
54
|
+
}
|
|
55
|
+
} catch { parseFailed = true }
|
|
56
|
+
return { declared, parseFailed }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 其他活跃变更的声明面(排除 archive 与本变更;design 缺失的变更跳过)。 */
|
|
60
|
+
async function collectForeignDeclarations(specBase, changeName) {
|
|
61
|
+
const map = new Map() // file -> Set<changeName>
|
|
62
|
+
const changesRoot = join(specBase, 'changes')
|
|
63
|
+
let dirs
|
|
64
|
+
try { dirs = readdirSync(changesRoot, { withFileTypes: true }) } catch { return map }
|
|
65
|
+
const { parseFileChangeList } = await import('./change-list.js')
|
|
66
|
+
for (const d of dirs) {
|
|
67
|
+
if (!d.isDirectory()) continue
|
|
68
|
+
if (d.name === 'archive' || d.name === changeName) continue
|
|
69
|
+
const designPath = join(changesRoot, d.name, 'design.md')
|
|
70
|
+
if (!existsSync(designPath)) continue
|
|
71
|
+
try {
|
|
72
|
+
for (const p of parseFileChangeList(designPath)) {
|
|
73
|
+
const f = normalizePath(p)
|
|
74
|
+
if (!map.has(f)) map.set(f, new Set())
|
|
75
|
+
map.get(f).add(d.name)
|
|
76
|
+
}
|
|
77
|
+
} catch { /* 单变更解析失败跳过(fail-soft) */ }
|
|
78
|
+
}
|
|
79
|
+
return map
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 数某文件 baseline..HEAD 的 hunk 数(二进制/重命名无 @@ 头计 0——按文件级处理)。 */
|
|
83
|
+
function countHunks(gitFn, cwd, baselineCommit, file) {
|
|
84
|
+
try {
|
|
85
|
+
const out = gitFn(cwd, ['diff', `${baselineCommit}..HEAD`, '--', file])
|
|
86
|
+
const text = String(out || '')
|
|
87
|
+
let n = 0
|
|
88
|
+
for (const line of text.split('\n')) if (line.startsWith('@@')) n++
|
|
89
|
+
return n
|
|
90
|
+
} catch {
|
|
91
|
+
return -1
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 当前工作树未提交修改文件集(M 状态;新增未跟踪不算——residue 只关心已提交文件的并发改动)。 */
|
|
96
|
+
function collectWorkingTreeModified(gitFn, cwd) {
|
|
97
|
+
try {
|
|
98
|
+
const out = gitFn(cwd, ['status', '--porcelain'])
|
|
99
|
+
const set = new Set()
|
|
100
|
+
for (const line of String(out || '').split('\n')) {
|
|
101
|
+
const status = line.slice(0, 2)
|
|
102
|
+
const path = normalizePath(line.slice(3).split(' -> ').pop())
|
|
103
|
+
// porcelain XY 双列:X=暂存位 Y=工作树位(" M a.js" 是未暂存修改)——任一列 M/A 都算在途
|
|
104
|
+
if ((status[0] === 'M' || status[0] === 'A' || status[1] === 'M' || status[1] === 'A') && path) set.add(path)
|
|
105
|
+
}
|
|
106
|
+
return set
|
|
107
|
+
} catch {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 行级归属门主体。committedFiles 为 baseline..HEAD 提交面文件清单(相对路径正斜杠)。
|
|
114
|
+
* @returns {{ ok: boolean, degraded: boolean, gate: string, unattributed: Array,
|
|
115
|
+
* contended: Array, residue: Array, notes: string[] }}
|
|
116
|
+
*/
|
|
117
|
+
export async function runHunkAttributionGate({ cwd, specBase, changeName, baselineCommit, committedFiles, gitFn, gate = 'warn' }) {
|
|
118
|
+
const result = { ok: true, degraded: false, gate, faceCount: 0, unattributed: [], contended: [], residue: [], notes: [] }
|
|
119
|
+
if (gate === 'off') {
|
|
120
|
+
result.notes.push('hunk_gate=off——归属门关闭')
|
|
121
|
+
return result
|
|
122
|
+
}
|
|
123
|
+
if (!baselineCommit || !Array.isArray(committedFiles) || committedFiles.length === 0) {
|
|
124
|
+
result.notes.push('提交面为空或基线缺失——归属门跳过')
|
|
125
|
+
return result
|
|
126
|
+
}
|
|
127
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
128
|
+
const { declared } = await collectDeclaredFace(changeDir, changeName)
|
|
129
|
+
|
|
130
|
+
const deliverables = committedFiles
|
|
131
|
+
.map(normalizePath)
|
|
132
|
+
.filter((f) => !f.startsWith('.sillyspec/'))
|
|
133
|
+
result.faceCount = deliverables.length
|
|
134
|
+
|
|
135
|
+
// ① 未归因文件(声明面全空时降为提示,不指认夹带——advisory 同口径)
|
|
136
|
+
if (declared.size > 0) {
|
|
137
|
+
for (const f of deliverables) {
|
|
138
|
+
if (!declared.has(f)) {
|
|
139
|
+
result.unattributed.push({ file: f, hunks: countHunks(gitFn, cwd, baselineCommit, f) })
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
result.notes.push('本变更无文件声明面(design 无清单、requirements 无绑定)——未归因判定降为提示')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ② 跨变更竞争(同文件无法按行归属——显式暴露)
|
|
147
|
+
const foreign = await collectForeignDeclarations(specBase, changeName)
|
|
148
|
+
for (const f of deliverables) {
|
|
149
|
+
const by = foreign.get(f)
|
|
150
|
+
if (by && by.size > 0) {
|
|
151
|
+
result.contended.push({ file: f, by: [...by], hunks: countHunks(gitFn, cwd, baselineCommit, f) })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ③ 在途残留(提交面文件当前仍有未提交修改——活跃并发 WIP)
|
|
156
|
+
const modified = collectWorkingTreeModified(gitFn, cwd)
|
|
157
|
+
if (modified) {
|
|
158
|
+
for (const f of deliverables) {
|
|
159
|
+
if (modified.has(f)) result.residue.push({ file: f })
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
result.ok = result.unattributed.length === 0 && result.contended.length === 0
|
|
164
|
+
return result
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 渲染(warn/error 由接线方选择输出流;本函数只产行)。 */
|
|
168
|
+
export function renderHunkAttributionLines(r) {
|
|
169
|
+
const L = []
|
|
170
|
+
if (!r || r.gate === 'off' || r.degraded) {
|
|
171
|
+
L.push(`- 不适用(${r && r.gate === 'off' ? 'hunk_gate=off' : '降级跳过(非 git 环境或异常)'})`)
|
|
172
|
+
for (const n of (r && r.notes) || []) L.push(`- ℹ️ ${n}`)
|
|
173
|
+
return L
|
|
174
|
+
}
|
|
175
|
+
const attributed = r.faceCount - r.unattributed.length
|
|
176
|
+
L.push(`- ℹ️ 提交面 ${r.faceCount} 个交付文件对账:声明面归因 ${attributed}、未归因 ${r.unattributed.length}、跨变更竞争 ${r.contended.length}、在途残留 ${r.residue.length}`)
|
|
177
|
+
for (const u of r.unattributed) {
|
|
178
|
+
L.push(`- ⚠️ 未归因:\`${u.file}\`(${u.hunks >= 0 ? `${u.hunks} hunk` : 'hunk 数不可得'})不在 design 清单与 requirements 测试绑定——属并行会话在途交付则须显式 pathspec 隔离并协调归属;属本变更则补 design 自声明`)
|
|
179
|
+
}
|
|
180
|
+
for (const c of r.contended) {
|
|
181
|
+
L.push(`- ⚠️ 跨变更竞争:\`${c.file}\` 同时被活跃变更 ${c.by.join('、')} 声明(${c.hunks >= 0 ? `${c.hunks} hunk` : '?'})——同文件无法按行归属,收口前逐 hunk 核对全部改动确属本变更,否则协调串行或拆分提交`)
|
|
182
|
+
}
|
|
183
|
+
for (const s of r.residue) {
|
|
184
|
+
L.push(`- ⚠️ 在途残留:\`${s.file}\` 已进提交面但当前工作树仍有未提交修改——活跃并发 WIP 信号,确认无 interleaved 提交风险`)
|
|
185
|
+
}
|
|
186
|
+
if (r.unattributed.length === 0 && r.contended.length === 0 && r.residue.length === 0) {
|
|
187
|
+
L.push('- ✅ 提交面全部文件归属清晰(声明面覆盖、无跨变更竞争、无在途残留)')
|
|
188
|
+
}
|
|
189
|
+
for (const n of r.notes || []) L.push(`- ℹ️ ${n}`)
|
|
190
|
+
return L
|
|
191
|
+
}
|