sillyspec 3.28.3 → 3.28.5
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 +114 -114
- package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
- package/.claude/skills/sillyspec-state/SKILL.md +25 -25
- package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
- package/AGENTS.md +25 -0
- package/INSTRUCTIONS.md +12 -0
- package/package.json +1 -1
- package/results-review-tier.md +18 -0
- package/src/agent-session-log.js +4 -3
- package/src/archive-delta.js +82 -0
- package/src/change-delete.js +2 -1
- package/src/change-list.js +18 -3
- package/src/change-risk-profile.js +77 -6
- package/src/commit-suggest.js +52 -1
- package/src/config-cat.js +17 -1
- package/src/constants.js +3 -1
- package/src/decision-distill.js +45 -5
- package/src/decisions-io.js +96 -0
- package/src/design-facts.js +3 -0
- package/src/dispatch/probe.js +31 -8
- package/src/docs-check.js +102 -3
- package/src/doctor-diagnostics.js +441 -5
- package/src/foreign-declared.js +53 -2
- package/src/index.js +606 -22
- package/src/mcp-server.js +144 -0
- package/src/module-impact.js +84 -12
- package/src/modules.js +46 -34
- package/src/plan-adopt-waves.js +13 -5
- package/src/progress/change-registry.js +34 -0
- package/src/progress.js +75 -2
- package/src/quick-recommend.js +6 -1
- package/src/quicklog.js +70 -8
- package/src/review-dispatch.js +377 -0
- package/src/run/archive-distill.js +48 -0
- package/src/run/command.js +48 -13
- package/src/run/complete-handlers.js +215 -9
- package/src/run/complete.js +184 -2
- package/src/run/gate-snapshot.js +88 -0
- package/src/run/gates.js +206 -58
- package/src/run/prompt.js +150 -14
- package/src/run/quick-audit.js +38 -13
- package/src/run/shared.js +183 -12
- package/src/run/stage.js +52 -2
- package/src/run/verify-quality-scan.js +336 -0
- package/src/runtime-hygiene.js +84 -0
- package/src/scope-audit.js +735 -0
- package/src/sillyhub-mcp/client.js +58 -3
- package/src/spec-sync.js +63 -8
- package/src/stage-contract.js +83 -3
- package/src/stage-review-checklist.js +44 -0
- package/src/stage-review.js +125 -4
- package/src/stages/archive.js +60 -167
- package/src/stages/brainstorm.js +7 -17
- package/src/stages/doctor.js +41 -515
- package/src/stages/execute.js +13 -10
- package/src/stages/plan-postcheck.js +191 -7
- package/src/stages/plan.js +7 -35
- package/src/stages/quick.js +11 -11
- package/src/stages/verify.js +24 -39
- package/src/sync-noise.js +158 -0
- package/src/sync.js +93 -27
- package/src/task-review.js +133 -17
- package/src/taskcard.js +46 -3
- package/src/validate-artifacts.js +163 -0
- package/src/verify-facts-schema.js +141 -0
- package/src/verify-postcheck.js +347 -29
- package/src/verify-probes.js +275 -24
- package/src/workflow.js +6 -0
- package/src/worktree-apply.js +167 -24
- package/src/worktree.js +5 -1
- package/templates/agents-instruction.md +1 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-server.js(P2-g = MCP Phase 2,noai-ir-roadmap §5):SillySpec 最小 stdio MCP server。
|
|
3
|
+
*
|
|
4
|
+
* 动机:agent 此前靠 shell 调 CLI + 解析人类可读文本,flag 幻觉与文本解析漂移是真实痛点
|
|
5
|
+
* (铁律里专门有一条「不确定命令时停下问用户」)。MCP tools 的 schema 化参数把 flag 面锁死;
|
|
6
|
+
* machine-interface 的 JSON envelope(gate/derive/progress)与 next 的 JSON 输出是现成地基。
|
|
7
|
+
*
|
|
8
|
+
* 形态:`sillyspec mcp` 启动(hosts 配 command=sillyspec args=[mcp])。stdio 上跑 JSON-RPC 2.0
|
|
9
|
+
* 行协议(每行一个请求/响应)。**只读 tools**(Phase 2 边界:--done 等状态推进不暴露——
|
|
10
|
+
* Phase 3 另计;工具全部经子进程 `--json` 执行,stdout 纪律天然隔离,不 import 重模块):
|
|
11
|
+
* - sillyspec_next:状态探测与下一步建议(detectNextStep)
|
|
12
|
+
* - sillyspec_gate:阶段完成门控聚合(machine-interface runGate envelope)
|
|
13
|
+
* - sillyspec_derive:单项事实核验(runDerive envelope)
|
|
14
|
+
* - sillyspec_progress:全局进度总览(progress show --json envelope)
|
|
15
|
+
*/
|
|
16
|
+
import { createInterface } from 'node:readline'
|
|
17
|
+
import { spawn } from 'node:child_process'
|
|
18
|
+
import { join, dirname } from 'node:path'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
22
|
+
const CLI_ENTRY = join(__dirname, 'index.js')
|
|
23
|
+
const PROTOCOL_VERSION = '2025-06-18'
|
|
24
|
+
const SERVER_INFO = { name: 'sillyspec', version: '3.28.4' }
|
|
25
|
+
|
|
26
|
+
const TOOLS = [
|
|
27
|
+
{
|
|
28
|
+
name: 'sillyspec_next',
|
|
29
|
+
description: '项目状态探测:状态 + 下一步命令 + 依据 + 活跃变更列表(只读;无活跃进度时从产物反推)',
|
|
30
|
+
inputSchema: { type: 'object', properties: { spec_dir: { type: 'string', description: '规范目录(缺省 <cwd>/.sillyspec)' } } },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'sillyspec_gate',
|
|
34
|
+
description: '阶段完成门控聚合:变更的 <stage> 阶段此刻能否标记完成(envelope:ok/errors/warnings/checks;exit 0/1/2)',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
stage: { type: 'string', enum: ['brainstorm', 'plan', 'execute', 'verify', 'archive'], description: '目标阶段' },
|
|
39
|
+
change: { type: 'string', description: '变更名(必填)' },
|
|
40
|
+
spec_dir: { type: 'string' },
|
|
41
|
+
},
|
|
42
|
+
required: ['stage', 'change'],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'sillyspec_derive',
|
|
47
|
+
description: '单项事实核验(facet:execute-evidence / verify-test / task-reviews / artifacts)',
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: {
|
|
51
|
+
facet: { type: 'string', enum: ['execute-evidence', 'verify-test', 'task-reviews', 'artifacts'] },
|
|
52
|
+
change: { type: 'string' },
|
|
53
|
+
spec_dir: { type: 'string' },
|
|
54
|
+
},
|
|
55
|
+
required: ['facet', 'change'],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'sillyspec_progress',
|
|
60
|
+
description: '全局进度总览(活跃变更 × 阶段 × 步骤态,envelope JSON;只读)',
|
|
61
|
+
inputSchema: { type: 'object', properties: { spec_dir: { type: 'string' } } },
|
|
62
|
+
},
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
/** tools/call 路由:子进程 --json(stdout 纪律隔离;CLI 的 stderr 透传进 text 供诊断) */
|
|
66
|
+
async function callTool(name, args) {
|
|
67
|
+
const argv = ['--meta'] // 占位防误配:--meta 是 run 的 flag,工具不走 run
|
|
68
|
+
argv.length = 0
|
|
69
|
+
const specDir = args && typeof args.spec_dir === 'string' && args.spec_dir ? ['--spec-dir', args.spec_dir] : []
|
|
70
|
+
switch (name) {
|
|
71
|
+
case 'sillyspec_next':
|
|
72
|
+
return await runCli(['next', '--json', ...specDir])
|
|
73
|
+
case 'sillyspec_gate':
|
|
74
|
+
return await runCli(['gate', String(args.stage), '--change', String(args.change), '--json', ...specDir])
|
|
75
|
+
case 'sillyspec_derive':
|
|
76
|
+
return await runCli(['derive', String(args.facet), '--change', String(args.change), '--json', ...specDir])
|
|
77
|
+
case 'sillyspec_progress':
|
|
78
|
+
return await runCli(['progress', 'show', '--json', ...specDir])
|
|
79
|
+
default:
|
|
80
|
+
return { text: `未知工具: ${name}`, isError: true }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function runCli(cliArgs) {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const p = spawn(process.execPath, [CLI_ENTRY, ...cliArgs], {
|
|
87
|
+
cwd: process.cwd(),
|
|
88
|
+
env: process.env,
|
|
89
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
90
|
+
})
|
|
91
|
+
let out = ''
|
|
92
|
+
let err = ''
|
|
93
|
+
p.stdout.on('data', (d) => { out += String(d) })
|
|
94
|
+
p.stderr.on('data', (d) => { err += String(d) })
|
|
95
|
+
p.on('close', (code) => {
|
|
96
|
+
// envelope 原样透传(text = stdout JSON 全文;machine 侧自解);stderr 仅失败时附诊断
|
|
97
|
+
const text = (out.trim() || err.trim().slice(-2000) || `(exit ${code},无输出)`)
|
|
98
|
+
resolve({ text, isError: code !== null && code !== 0 && code !== 1, exitCode: code })
|
|
99
|
+
})
|
|
100
|
+
p.on('error', (e) => resolve({ text: `CLI 子进程失败: ${e.message}`, isError: true }))
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 单请求分发。返回 result 对象(或抛 {code, message} 形态错误由上层包 error)。 */
|
|
105
|
+
async function handleMessage(msg) {
|
|
106
|
+
const { id, method, params } = msg
|
|
107
|
+
switch (method) {
|
|
108
|
+
case 'initialize':
|
|
109
|
+
return { protocolVersion: (params && params.protocolVersion) || PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: SERVER_INFO }
|
|
110
|
+
case 'ping':
|
|
111
|
+
return {}
|
|
112
|
+
case 'tools/list':
|
|
113
|
+
return { tools: TOOLS }
|
|
114
|
+
case 'tools/call': {
|
|
115
|
+
const name = params && params.name
|
|
116
|
+
const args = (params && params.arguments) || {}
|
|
117
|
+
const r = await callTool(name, args)
|
|
118
|
+
return { content: [{ type: 'text', text: r.text }], ...(r.isError ? { isError: true } : {}) }
|
|
119
|
+
}
|
|
120
|
+
default:
|
|
121
|
+
throw { code: -32601, message: `Method not found: ${method}` }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** stdio 行协议主循环(readline 逐行 JSON;通知无 id 不回包)。导出供测试与 `sillyspec mcp` 接线。 */
|
|
126
|
+
export function startMcpServer({ input = process.stdin, output = process.stdout } = {}) {
|
|
127
|
+
const rl = createInterface({ input })
|
|
128
|
+
rl.on('line', async (line) => {
|
|
129
|
+
const trimmed = line.trim()
|
|
130
|
+
if (!trimmed) return
|
|
131
|
+
let msg
|
|
132
|
+
try { msg = JSON.parse(trimmed) } catch { return /* 非 JSON 行静默忽略(协议健壮性) */ }
|
|
133
|
+
if (msg === null || typeof msg !== 'object' || !msg.method) return
|
|
134
|
+
if (msg.id === undefined || msg.id === null) return // notification(如 notifications/initialized)
|
|
135
|
+
try {
|
|
136
|
+
const result = await handleMessage(msg)
|
|
137
|
+
output.write(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result }) + '\n')
|
|
138
|
+
} catch (e) {
|
|
139
|
+
const err = e && typeof e === 'object' && e.code !== undefined ? e : { code: -32603, message: (e && e.message) || String(e) }
|
|
140
|
+
output.write(JSON.stringify({ jsonrpc: '2.0', id: msg.id, error: err }) + '\n')
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
return new Promise((resolve) => rl.on('close', () => resolve()))
|
|
144
|
+
}
|
package/src/module-impact.js
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
* diff 源:resolveVerifyChangedFiles(worktree-aware,与 task-review 同源);
|
|
11
11
|
* module-map:.sillyspec/docs/<p>/modules/_module-map.yaml 的 modules.<id>.paths
|
|
12
12
|
* (目录条目以 / 结尾按前缀匹配,与模块上下文索引读侧口径一致)。
|
|
13
|
+
* 声明源(plan --done 首版,2026-09-08 刀②):design 文件变更清单——代码未写、无 diff,
|
|
14
|
+
* 归属分类同一套 module-map 前缀匹配,仅输入来源不同。
|
|
13
15
|
*/
|
|
14
|
-
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
16
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
15
17
|
import { join } from 'path'
|
|
16
18
|
import { resolveVerifyChangedFiles } from './verify-postcheck.js'
|
|
17
19
|
import { resolveSpecDir } from './run/shared.js'
|
|
@@ -68,11 +70,67 @@ function classifyFile(posixPath, modulePaths) {
|
|
|
68
70
|
|
|
69
71
|
/**
|
|
70
72
|
* 生成 module-impact.md 骨架。
|
|
71
|
-
* @param {{ cwd: string, changeName: string, specDir?: string|null }} opts
|
|
73
|
+
* @param {{ cwd: string, changeName: string, specDir?: string|null, sourceFiles?: string[]|null, origin?: string|null }} opts
|
|
74
|
+
* sourceFiles:声明来源(plan --done 首版用 design 文件变更清单——此时代码未写、无 diff 可取);
|
|
75
|
+
* 缺省走 diff 来源(resolveVerifyChangedFiles,archive/execute 后的真实改动)。
|
|
76
|
+
* origin:骨架首行的生成方说明(区分 plan 首版与 CLI 命令生成,追溯来源用)。
|
|
72
77
|
* @returns {{ markdown: string, matchedCount: number, unmatchedCount: number }|null}
|
|
73
|
-
* 无 module-map /
|
|
78
|
+
* 无 module-map / 无可归类文件 → null(无可归类输入,agent 全手写)
|
|
74
79
|
*/
|
|
75
|
-
|
|
80
|
+
/**
|
|
81
|
+
* 模块文档 sidecar 同步命令化(2026-09-09 刀批,轮次经济学 §3.1 模块文档同步项):
|
|
82
|
+
* quick/archive 两处 prompt 让 agent 手写 sidecar 追加行——机械部分(变更归属模块、
|
|
83
|
+
* sidecar 追加、卡 updated_at 戳)CLI 代算,agent 只留卡片正文语义更新。
|
|
84
|
+
* 幂等:sidecar 已含 `- <change> |` 行则跳过该模块;卡不存在只建 sidecar 不建卡(卡内容是语义)。
|
|
85
|
+
* @returns {{ synced: string[], unmatched: string[], skipped: string[] }}
|
|
86
|
+
*/
|
|
87
|
+
export function syncModuleDocSidecars({ cwd, changeName, note, specDir = null }) {
|
|
88
|
+
const specBase = resolveSpecDir(cwd, { specDir })
|
|
89
|
+
// 复用 generateModuleImpactSkeleton 的 map 发现 + 归类(diff 来源)
|
|
90
|
+
const skel = generateModuleImpactSkeleton({ cwd, changeName, specDir })
|
|
91
|
+
if (!skel) return { synced: [], unmatched: [], skipped: [], reason: '无 module-map 或无 diff 可归类' }
|
|
92
|
+
const modulesDir = findModulesDir(specBase)
|
|
93
|
+
if (!modulesDir) return { synced: [], unmatched: [], skipped: [], reason: 'modules 目录不存在' }
|
|
94
|
+
// 从骨架 markdown 反解归类结果(命中模块集合 + 未匹配清单)
|
|
95
|
+
const hitModules = [...skel.markdown.matchAll(/^\| (\S+) \| `([^`]+)`/gm)].map(m => m[1])
|
|
96
|
+
const unmatched = [...skel.markdown.matchAll(/^- `([^`]+)` /gm)].map(m => m[1])
|
|
97
|
+
const synced = []
|
|
98
|
+
const skipped = []
|
|
99
|
+
for (const mod of [...new Set(hitModules)]) {
|
|
100
|
+
const sidecarPath = join(modulesDir, `${mod}.changelog.md`)
|
|
101
|
+
let sidecar = ''
|
|
102
|
+
try { sidecar = readFileSync(sidecarPath, 'utf8') } catch { sidecar = '' }
|
|
103
|
+
if (sidecar.includes(`- ${changeName} |`)) { skipped.push(mod); continue }
|
|
104
|
+
const NL = String.fromCharCode(10)
|
|
105
|
+
const line = `- ${changeName} | ${note || '(见该变更 quicklog/归档)'}`
|
|
106
|
+
const base = sidecar
|
|
107
|
+
? sidecar.replace(/\n*$/, NL)
|
|
108
|
+
: `# ${mod} 变更索引` + NL + NL
|
|
109
|
+
writeFileSync(sidecarPath, base + line + NL)
|
|
110
|
+
// 卡 updated_at 戳(存在才戳)
|
|
111
|
+
const cardPath = join(modulesDir, `${mod}.md`)
|
|
112
|
+
try {
|
|
113
|
+
const card = readFileSync(cardPath, 'utf8')
|
|
114
|
+
const stamped = card.replace(/^updated_at:.*$/m, `updated_at: ${new Date().toISOString().slice(0, 19) + '+08:00'}`)
|
|
115
|
+
if (stamped !== card) writeFileSync(cardPath, stamped)
|
|
116
|
+
} catch { /* 卡不存在——sidecar 照写,卡内容留给 agent */ }
|
|
117
|
+
synced.push(mod)
|
|
118
|
+
}
|
|
119
|
+
return { synced, unmatched, skipped }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function findModulesDir(specBase) {
|
|
123
|
+
try {
|
|
124
|
+
for (const p of readdirSync(join(specBase, 'docs'), { withFileTypes: true })) {
|
|
125
|
+
if (!p.isDirectory()) continue
|
|
126
|
+
const cand = join(specBase, 'docs', p.name, 'modules')
|
|
127
|
+
if (existsSync(join(cand, '_module-map.yaml'))) return cand
|
|
128
|
+
}
|
|
129
|
+
} catch { /* 无 docs/ */ }
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null, sourceFiles = null, origin = null }) {
|
|
76
134
|
const specBase = resolveSpecDir(cwd, { specDir })
|
|
77
135
|
|
|
78
136
|
// 找 _module-map.yaml(docs/<p>/modules/;多项目取首个含 modules 索引的)
|
|
@@ -92,15 +150,27 @@ export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null }
|
|
|
92
150
|
const modulePaths = parseModuleMapPaths(readFileSync(moduleMapPath, 'utf8'))
|
|
93
151
|
if (modulePaths.size === 0) return null
|
|
94
152
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
153
|
+
let sourceFileList
|
|
154
|
+
if (sourceFiles) {
|
|
155
|
+
// NEW: 前缀剥离(坑 module-impact-new-prefix-mismatch,2026-09-10 驾驭小结③,用户实锤):
|
|
156
|
+
// design 清单待建文件带 NEW: 标记,pathMatches 比对侧早已剥(坑
|
|
157
|
+
// platform-sync-progress-rollback-and-db-corruption 坑3「仅在比对处剥」),本归类是同
|
|
158
|
+
// 语义比对(目标文件 × module-map paths)却漏剥——前缀不剥则 classifyFile 前缀匹配必
|
|
159
|
+
// 失配,NEW 文件全部落「未匹配文件」逼手动回填。剥后与裸路径去重(NEW: a.js 与 a.js 同文件)。
|
|
160
|
+
sourceFileList = [...new Set(sourceFiles
|
|
161
|
+
.map(f => String(f).replace(/^NEW:\s*/, '').split('\\').join('/').trim())
|
|
162
|
+
.filter(f => f && !f.startsWith('.sillyspec/')))]
|
|
163
|
+
} else {
|
|
164
|
+
// includeWorkingTree 同 verify module 子集(坑 module-subset-zero-hit-uncommitted):
|
|
165
|
+
// worktree 未提交改动也计入模块影响面(module-impact 按实际 diff 归属模块)
|
|
166
|
+
const diffFiles = resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase }) || []
|
|
167
|
+
sourceFileList = diffFiles.map(f => f.split('\\').join('/')).filter(f => !f.startsWith('.sillyspec/'))
|
|
168
|
+
}
|
|
169
|
+
if (sourceFileList.length === 0) return null
|
|
100
170
|
|
|
101
171
|
const byModule = new Map()
|
|
102
172
|
const unmatched = []
|
|
103
|
-
for (const f of
|
|
173
|
+
for (const f of sourceFileList) {
|
|
104
174
|
const mod = classifyFile(f, modulePaths)
|
|
105
175
|
if (mod) {
|
|
106
176
|
if (!byModule.has(mod)) byModule.set(mod, [])
|
|
@@ -111,7 +181,9 @@ export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null }
|
|
|
111
181
|
}
|
|
112
182
|
|
|
113
183
|
const L = [
|
|
114
|
-
|
|
184
|
+
origin
|
|
185
|
+
? `# 模块影响分析(骨架由 ${origin} 生成)`
|
|
186
|
+
: '# 模块影响分析(骨架由 `sillyspec module-impact --change <变更名>` 生成)',
|
|
115
187
|
'',
|
|
116
188
|
'> 文件×模块归属由 CLI 按 _module-map.yaml paths 前缀匹配预填;',
|
|
117
189
|
'> **影响类型**(逻辑变更/数据结构变更/接口变更/调用关系变更/配置变更/新增)与 review 标记是语义判断,',
|
|
@@ -162,5 +234,5 @@ export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null }
|
|
|
162
234
|
L.push('规则:execute/verify 完成文档同步后把对应行回填 done;确定不同步的行改 skipped 并在操作列写明原因。')
|
|
163
235
|
L.push('')
|
|
164
236
|
|
|
165
|
-
return { markdown: L.join('\n'), matchedCount:
|
|
237
|
+
return { markdown: L.join('\n'), matchedCount: sourceFileList.length - unmatched.length, unmatchedCount: unmatched.length }
|
|
166
238
|
}
|
package/src/modules.js
CHANGED
|
@@ -87,18 +87,16 @@ export async function rebuildModuleMap(cwd, { force = false } = {}) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// 解析现有 _module-map.yaml
|
|
91
|
-
|
|
92
|
-
//
|
|
90
|
+
// 解析现有 _module-map.yaml(复用 parseModuleMapSimple canonical 解析)。P2-c
|
|
91
|
+
//(noai-ir-roadmap §5 字段分离):existing 条目**全字段**整体保留——merge 语义下
|
|
92
|
+
// rebuild 只做「补缺 + 增新卡」,--force 不再可能清空 tags/main_symbols/depends_on/
|
|
93
|
+
// used_by/status/needs_review 等人工维护字段(旧版只保 3 个列表字段,实例 map 文件头
|
|
94
|
+
// 常年挂着「勿跑 rebuild --force 会清空手动维护」的警告——该警告随本变更退役)。
|
|
93
95
|
let existingModules = {};
|
|
94
96
|
if (existingMap) {
|
|
95
97
|
const parsed = parseModuleMapSimple(existingMap);
|
|
96
98
|
for (const [id, fields] of Object.entries(parsed || {})) {
|
|
97
|
-
existingModules[id] = {
|
|
98
|
-
core_files: Array.isArray(fields.core_files) ? fields.core_files : [],
|
|
99
|
-
test_files: Array.isArray(fields.test_files) ? fields.test_files : [],
|
|
100
|
-
entrypoints: Array.isArray(fields.entrypoints) ? fields.entrypoints : [],
|
|
101
|
-
};
|
|
99
|
+
existingModules[id] = { ...(fields || {}) };
|
|
102
100
|
}
|
|
103
101
|
}
|
|
104
102
|
|
|
@@ -132,53 +130,67 @@ export async function rebuildModuleMap(cwd, { force = false } = {}) {
|
|
|
132
130
|
...cards.map(c => c.moduleId)
|
|
133
131
|
]);
|
|
134
132
|
|
|
133
|
+
// ── P2-c merge 语义发射:existing 全字段优先(人工维护不丢)→ 卡片补缺 → 骨架默认垫底 ──
|
|
134
|
+
const stripQuotes = (v) => String(v).replace(/^"|"$/g, '')
|
|
135
135
|
for (const moduleId of allModuleIds) {
|
|
136
136
|
const card = cards.find(c => c.moduleId === moduleId);
|
|
137
137
|
const cardContent = card?.content || '';
|
|
138
138
|
|
|
139
139
|
// 从模块卡片提取 context index 字段
|
|
140
140
|
const role = extractSection(cardContent, '定位') || '';
|
|
141
|
-
const contract = extractSection(cardContent, '契约摘要') || '';
|
|
142
|
-
const logic = extractSection(cardContent, '关键逻辑') || '';
|
|
143
|
-
const notes = extractSection(cardContent, '注意事项') || '';
|
|
144
141
|
|
|
145
|
-
yaml += ` ${moduleId}:\n`;
|
|
146
|
-
yaml += ` status: active\n`;
|
|
147
|
-
if (card) yaml += ` doc: modules/${card.filename}\n`;
|
|
148
|
-
else yaml += ` doc: modules/${moduleId}.md\n`;
|
|
149
|
-
yaml += ` needs_review: false\n`;
|
|
150
|
-
yaml += ` review_reasons: []\n`;
|
|
151
|
-
// context index 字段(v2)
|
|
152
|
-
if (role) yaml += ` role: "${escapeYamlString(role)}"\n`;
|
|
153
|
-
// core_files / test_files / entrypoints 从已有 _module-map.yaml 全量保留(列表项级保真)
|
|
154
142
|
const existing = existingModules[moduleId] || {};
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
143
|
+
const merged = {
|
|
144
|
+
// 骨架默认(垫底)
|
|
145
|
+
status: 'active',
|
|
146
|
+
doc: `modules/${(card && card.filename) || moduleId + '.md'}`,
|
|
147
|
+
needs_review: false,
|
|
148
|
+
review_reasons: [],
|
|
149
|
+
risk_level: 'low',
|
|
150
|
+
verify_commands: [],
|
|
151
|
+
related_docs: [],
|
|
152
|
+
// existing 全字段覆盖(人工维护优先)
|
|
153
|
+
...existing,
|
|
154
|
+
}
|
|
155
|
+
// 卡片补缺:role 仅当 existing 没有时从卡片取;doc 在卡片在场时以卡片文件名为准
|
|
156
|
+
//(existing.doc 可能指向已改名的旧文件);卡片不在场则保留 existing.doc
|
|
157
|
+
if (!merged.role && role) merged.role = role
|
|
158
|
+
if (card) merged.doc = `modules/${card.filename}`
|
|
159
|
+
|
|
160
|
+
yaml += ` ${moduleId}:\n`;
|
|
161
|
+
for (const k of ['status', 'doc', 'needs_review']) {
|
|
162
|
+
if (merged[k] !== undefined) yaml += ` ${k}: ${typeof merged[k] === 'boolean' ? merged[k] : merged[k]}\n`;
|
|
163
|
+
}
|
|
164
|
+
if (merged.role) yaml += ` role: "${escapeYamlString(stripQuotes(merged.role))}"\n`;
|
|
165
|
+
// 列表字段全量保真(existing 用什么字段名就保什么——paths/core_files 双形态兼容读者)
|
|
166
|
+
for (const k of ['paths', 'core_files', 'test_files', 'entrypoints', 'main_symbols', 'tags', 'aliases', 'depends_on', 'used_by', 'review_reasons', 'related_docs', 'verify_commands']) {
|
|
167
|
+
const items = merged[k];
|
|
168
|
+
if (items === undefined) continue;
|
|
169
|
+
if (Array.isArray(items) && items.length === 0) { yaml += ` ${k}: []\n`; continue }
|
|
170
|
+
if (!Array.isArray(items) || items.length === 0) continue;
|
|
171
|
+
yaml += ` ${k}:\n`;
|
|
172
|
+
for (const p of items) yaml += ` - ${p}\n`;
|
|
160
173
|
}
|
|
161
|
-
if (
|
|
162
|
-
yaml += ` risk_level: low\n`;
|
|
163
|
-
if (contract || logic) yaml += ` related_docs: []\n`;
|
|
174
|
+
if (merged.risk_level !== undefined) yaml += ` risk_level: ${merged.risk_level}\n`;
|
|
164
175
|
yaml += `\n`;
|
|
165
176
|
}
|
|
166
177
|
|
|
167
|
-
//
|
|
178
|
+
// 写入保护(multi-agent-platform 坑 modules-rebuild-destructive):默认 dry-run 只预览不写。
|
|
179
|
+
// P2-c 后 --force 为 merge 语义(手动字段全量保留),但重写仍会刷新 generated_at/source_commit
|
|
180
|
+
// 与排版——dry-run 预览默认保留,确认写入用 --force。
|
|
168
181
|
if (!force && existingMap) {
|
|
169
|
-
console.log('⚠️ rebuild
|
|
170
|
-
console.log('
|
|
182
|
+
console.log('⚠️ rebuild 默认不写入(预览保护)。--force 为 merge 语义:existing 全字段保留(tags/main_symbols/depends_on/used_by/status/needs_review 等手动维护字段不丢),只补缺 + 增新卡');
|
|
183
|
+
console.log(' 本次为预览,未写入磁盘。确认写入请运行:sillyspec modules rebuild --force');
|
|
171
184
|
return { dryRun: true, path: mapPath }
|
|
172
185
|
}
|
|
173
186
|
|
|
174
187
|
writeFileSync(mapPath, yaml, 'utf8');
|
|
175
|
-
console.log(`✅ _module-map.yaml
|
|
188
|
+
console.log(`✅ _module-map.yaml 已重建(merge 语义):${mapPath}`);
|
|
176
189
|
console.log(` 模块数量:${allModuleIds.size}`);
|
|
177
190
|
for (const id of allModuleIds) {
|
|
178
191
|
console.log(` - ${id}`);
|
|
179
192
|
}
|
|
180
|
-
console.log(`\n
|
|
181
|
-
console.log(`ℹ️ schema 已升级到 v2,支持 role/core_files/test_files/entrypoints/depends_on/risk_level/verify_commands 等字段。`);
|
|
193
|
+
console.log(`\nℹ️ 手动维护字段(tags/entrypoints/main_symbols/depends_on/used_by/status/needs_review 等)已全量保留;rebuild 只补缺与增新卡。`);
|
|
182
194
|
return { dryRun: false, path: mapPath, modules: allModuleIds.size }
|
|
183
195
|
}
|
|
184
196
|
|
package/src/plan-adopt-waves.js
CHANGED
|
@@ -60,12 +60,17 @@ function updateSummaryWaveColumn(lines, waveOf) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* @param {{ changeDir: string, dryRun?: boolean }} opts
|
|
63
|
+
* @param {{ changeDir: string, dryRun?: boolean, mode?: 'write'|'proposal' }} opts
|
|
64
|
+
* mode='proposal'(2026-09-09-plan-derived):只读产拓扑布局提案——渲染 Wave 段草稿与
|
|
65
|
+
* W 列同步结果但不落盘,供 postcheck「提案-验证-落盘」自动修复(FR-01)验证后决断写否。
|
|
64
66
|
* @returns {{ ok: boolean, error?: string, waves?: string[][], planPath: string, dryRun: boolean,
|
|
65
67
|
* waveBlock?: string[], tableRowsUpdated?: number, tableRowsSkipped?: number,
|
|
68
|
+
* planMdDraft?: string, rewritten?: boolean,
|
|
66
69
|
* postcheck?: { errors: string[], warnings: string[] } }}
|
|
70
|
+
* planMdDraft:proposal 档下的完整 plan.md 草稿文本;rewritten:与原文比较是否有实际变更
|
|
71
|
+
* (幂等重排零变更 → false,调用方静默);write 档沿用既有 postcheck 字段。
|
|
67
72
|
*/
|
|
68
|
-
export function adoptPlanWaves({ changeDir, dryRun = false }) {
|
|
73
|
+
export function adoptPlanWaves({ changeDir, dryRun = false, mode = 'write' }) {
|
|
69
74
|
const planPath = join(changeDir, 'plan.md')
|
|
70
75
|
if (!existsSync(planPath)) return { ok: false, error: `plan.md 不存在: ${planPath}`, planPath, dryRun }
|
|
71
76
|
const tasksDir = join(changeDir, 'tasks')
|
|
@@ -108,9 +113,11 @@ export function adoptPlanWaves({ changeDir, dryRun = false }) {
|
|
|
108
113
|
|
|
109
114
|
const { updated, skipped } = updateSummaryWaveColumn(lines, waveOf)
|
|
110
115
|
const content = lines.join('\n').replace(/\n*$/, '\n')
|
|
116
|
+
const rewritten = content !== original.replace(/\r\n/g, '\n').replace(/\n*$/, '\n')
|
|
111
117
|
|
|
112
|
-
if (dryRun) {
|
|
113
|
-
return { ok: true, waves, planPath, dryRun: true, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped
|
|
118
|
+
if (dryRun || mode === 'proposal') {
|
|
119
|
+
return { ok: true, waves, planPath, dryRun: true, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped,
|
|
120
|
+
planMdDraft: content, rewritten }
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
writeFileSync(planPath, content, 'utf8')
|
|
@@ -122,5 +129,6 @@ export function adoptPlanWaves({ changeDir, dryRun = false }) {
|
|
|
122
129
|
} catch (e) {
|
|
123
130
|
postcheck = { errors: [`复跑校验异常: ${e?.message || e}`], warnings: [] }
|
|
124
131
|
}
|
|
125
|
-
return { ok: true, waves, planPath, dryRun: false, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped,
|
|
132
|
+
return { ok: true, waves, planPath, dryRun: false, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped,
|
|
133
|
+
rewritten, postcheck }
|
|
126
134
|
}
|
|
@@ -46,6 +46,40 @@ export class ChangeRegistry {
|
|
|
46
46
|
* @param {string} changeName - 变更名
|
|
47
47
|
* @returns {{ current_stage: string, status: string }|null} 无该行返回 null(未注册目录桩)
|
|
48
48
|
*/
|
|
49
|
+
/**
|
|
50
|
+
* 读某变更某阶段的 completed_at(2026-09-08-ir-verify-facts FR-02:verifyStartAt 基准——
|
|
51
|
+
* evidence 分类核验的 mtime 窗口下界,取 execute 行 completed_at)。只读不抛:无行/读失败
|
|
52
|
+
* 返回 null(调用方走 R-05 fallback,与 getChangeCreatedAt 同容错风格)。
|
|
53
|
+
*/
|
|
54
|
+
getStageCompletedAt(cwd, changeName, stage) {
|
|
55
|
+
try {
|
|
56
|
+
const db = this.pm._ensureDB(cwd);
|
|
57
|
+
const row = db.getDb().prepare(
|
|
58
|
+
`SELECT s.completed_at FROM stages s JOIN changes c ON s.change_id = c.id
|
|
59
|
+
WHERE c.name = ? AND s.stage = ? ORDER BY s.completed_at DESC LIMIT 1`
|
|
60
|
+
).get(changeName, stage);
|
|
61
|
+
return row && row.completed_at ? row.completed_at : null;
|
|
62
|
+
} catch { return null; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 读某变更某阶段的 started_at(2026-09-10 用户反馈②:evidence mtime 窗口锚点从
|
|
67
|
+
* execute completed_at 放宽到 started_at——证据合法产自 execute 或 verify 两窗口,
|
|
68
|
+
* 锚「完成时刻」会把 execute 期间产的证据判旧,逼出「先提交则 diff 空、不提交则
|
|
69
|
+
* mtime 旧」的时序两难)。只读不抛:无行/列空/读失败返回 null(调用方退
|
|
70
|
+
* getStageCompletedAt → R-05 fallback,与 getStageCompletedAt 同族容错)。
|
|
71
|
+
*/
|
|
72
|
+
getStageStartedAt(cwd, changeName, stage) {
|
|
73
|
+
try {
|
|
74
|
+
const db = this.pm._ensureDB(cwd);
|
|
75
|
+
const row = db.getDb().prepare(
|
|
76
|
+
`SELECT s.started_at FROM stages s JOIN changes c ON s.change_id = c.id
|
|
77
|
+
WHERE c.name = ? AND s.stage = ? ORDER BY s.started_at DESC LIMIT 1`
|
|
78
|
+
).get(changeName, stage);
|
|
79
|
+
return row && row.started_at ? row.started_at : null;
|
|
80
|
+
} catch { return null; }
|
|
81
|
+
}
|
|
82
|
+
|
|
49
83
|
getChangeStage(cwd, changeName) {
|
|
50
84
|
const db = this.pm._ensureDB(cwd);
|
|
51
85
|
const sqlDb = db.getDb();
|
package/src/progress.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* worktree-guard hook 直读 sillyspec.db,不再有 gate-status.json 缓存双源(task-10 废除)。
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync, copyFileSync, readdirSync, statSync } from 'fs';
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync, copyFileSync, readdirSync, statSync, realpathSync } from 'fs';
|
|
14
14
|
import { join, basename, dirname, resolve, sep } from 'path';
|
|
15
15
|
import { tmpdir } from 'os';
|
|
16
16
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
@@ -25,13 +25,19 @@ import { STAGE_ORDER, MAIN_FLOW_ORDER, VALID_STAGES, STAGE_LABELS, SPEC_DIR_NAME
|
|
|
25
25
|
// resolveSpecDir 单一真相源在 src/run/shared.js(含 home 拒绝守卫,坑 cwd-correction-home-collision
|
|
26
26
|
// 根治:home 下 .sillyspec 恒不命中,防 smoke/临时目录污染自我延续)。此处 re-export 保持
|
|
27
27
|
// 既有 import 路径兼容;run/shared.js 对 progress.js 只有动态 import,无静态循环。
|
|
28
|
-
import { resolveSpecDir, checkPlatformManaged, PLATFORM_MANAGED_FILENAME, isSelfReferentialSpecRoot } from './run/shared.js';
|
|
28
|
+
import { resolveSpecDir, checkPlatformManaged, PLATFORM_MANAGED_FILENAME, isSelfReferentialSpecRoot, isTempResidueSpecRoot } from './run/shared.js';
|
|
29
29
|
export { resolveSpecDir };
|
|
30
30
|
|
|
31
31
|
// 默认规范目录名(相对于 cwd)
|
|
32
32
|
// SPEC_DIR_NAME → ./progress/shared.js(W6 Step9d)
|
|
33
33
|
const RUNTIME_SUBDIR = '.runtime';
|
|
34
34
|
|
|
35
|
+
// import() 的 pre-import 快照保留份数。这类快照只服务「刚 pull 完发现平台盖错、要退回
|
|
36
|
+
// pull 前」这一个场景,最新一份即可覆盖;越旧的快照 schema 可能已 bump,恢复价值随时间衰减。
|
|
37
|
+
// 可用 SILLYSPEC_PREIMPORT_BAK_KEEP 覆盖(灾难排查期想多留几份时)。
|
|
38
|
+
const PRE_IMPORT_BAK_KEEP_DEFAULT = 1;
|
|
39
|
+
const PRE_IMPORT_BAK_RE = /^sillyspec\.db\.pre-import-.*\.bak$/;
|
|
40
|
+
|
|
35
41
|
/**
|
|
36
42
|
* 向上查找含 .sillyspec 目录的祖先目录——实现已收敛至 src/run/shared.js(单一真相源,
|
|
37
43
|
* 含 home 拒绝守卫),此处 re-export。见顶部 import 说明。
|
|
@@ -95,6 +101,18 @@ export function resolvePlatformSpecDir(cwd, explicitSpecDir = null) {
|
|
|
95
101
|
// 指针丢失不该静默当纯本地项目处理(会建本地进度库 → 状态分裂)。
|
|
96
102
|
const decl = checkPlatformManaged(cwd);
|
|
97
103
|
if (decl) {
|
|
104
|
+
// temp 残留降级(2026-09-08 temp 投毒治理):声明 specRoot 在系统 temp 且 cwd 不在
|
|
105
|
+
// temp → 可证伪的测试/联调残留(真实平台 spec 根永不在 temp),与指针路径的 temp
|
|
106
|
+
// 警告(下方 resolvedTmp 分支)及自指声明降级(run/command.js 恢复链)同类。warn +
|
|
107
|
+
// 自动清理声明后按本地模式继续——不清理则每次命令重复 warn,且 disconnect 会连带
|
|
108
|
+
// 清 local.yaml platform 段误伤真实平台连接,不适合作为残留清理手段。
|
|
109
|
+
// 删除失败仍按本地继续(best effort;下次命令重复 warn 提示手动清理)。
|
|
110
|
+
if (isTempResidueSpecRoot(cwd, decl.specRoot)) {
|
|
111
|
+
const declarationPath = join(resolve(cwd), PLATFORM_MANAGED_FILENAME);
|
|
112
|
+
console.warn(`⚠️ 检测到 temp 残留平台接管声明(specRoot 指向系统 temp 目录:${decl.specRoot},测试/联调投毒特征),已自动清理并按本地模式运行: ${declarationPath}`);
|
|
113
|
+
try { unlinkSync(declarationPath) } catch { /* best effort */ }
|
|
114
|
+
return resolveSpecDir(cwd);
|
|
115
|
+
}
|
|
98
116
|
throw new PlatformManagedError({
|
|
99
117
|
declarationPath: join(resolve(cwd), PLATFORM_MANAGED_FILENAME),
|
|
100
118
|
specRoot: decl.specRoot,
|
|
@@ -141,6 +159,14 @@ export function resolvePlatformSpecDir(cwd, explicitSpecDir = null) {
|
|
|
141
159
|
` 修复:sillyspec platform pointer --cleanup(删除 ${pointerPath}),或删除该 pointer 文件后回到项目目录重跑。`
|
|
142
160
|
);
|
|
143
161
|
}
|
|
162
|
+
// junction 规范化(2026-09-10 驾驭小结第五批①,用户实锤「design-init/endpoints 落盘路径
|
|
163
|
+
// 偶发锚到 daemon junction 副本」):daemon 写入的 pointer specRoot 可能是通向真实 spec 根的
|
|
164
|
+
// junction/symlink 路径——经 junction 读写与真实路径同文件(无害)但路径呈现与比对漂移、
|
|
165
|
+
// junction 随 daemon 生命周期可能蒸发。realpath 坍缩为真实路径(失败回退原值,保守不干预)。
|
|
166
|
+
try {
|
|
167
|
+
const canonical = realpathSync(ptr.specRoot)
|
|
168
|
+
if (canonical && canonical !== ptr.specRoot) return canonical
|
|
169
|
+
} catch { /* realpath 失败(权限/竞态删除)→ 原值(上方已过存在性检查) */ }
|
|
144
170
|
return ptr.specRoot;
|
|
145
171
|
}
|
|
146
172
|
|
|
@@ -613,6 +639,14 @@ export class ProgressManager {
|
|
|
613
639
|
if (existsSync(walSidecar)) {
|
|
614
640
|
try { copyFileSync(walSidecar, `${bakPath}-wal`); } catch { /* 侧车消失=已 checkpoint,无碍 */ }
|
|
615
641
|
}
|
|
642
|
+
// 写完即回收旧快照:每次 pull / resolve --take-platform 都新增一份全库快照,无上限时
|
|
643
|
+
// 按同步次数无限累积(实证单仓 292 份 / 317MB)。裁剪放在写入侧而非独立 GC 命令——
|
|
644
|
+
// 靠人工记得跑 GC 等于没有回收。fail-open:卫生动作不能阻断 import 主流程。
|
|
645
|
+
try {
|
|
646
|
+
this._pruneImportBaks(cwd, bakPath);
|
|
647
|
+
} catch (e) {
|
|
648
|
+
console.warn(`⚠️ pre-import 快照裁剪失败(不阻断 import): ${e.message}`);
|
|
649
|
+
}
|
|
616
650
|
|
|
617
651
|
const pushedAt = progressObj.pushed_at ?? null;
|
|
618
652
|
const now = new Date().toISOString();
|
|
@@ -707,6 +741,38 @@ export class ProgressManager {
|
|
|
707
741
|
return { ok: true, imported: cn, reason: undefined, bakPath };
|
|
708
742
|
}
|
|
709
743
|
|
|
744
|
+
/**
|
|
745
|
+
* 滚动裁剪 .runtime/ 下的 pre-import 快照,只留最新 N 份(含各自 -wal 侧车)。
|
|
746
|
+
*
|
|
747
|
+
* 排序依据:文件名内嵌 ISO 时间戳(冒号/点已替换为 '-',见 import() 的 bakTs),
|
|
748
|
+
* 定宽且无进位歧义,字典序 == 时间序,无需 statSync(mtime 会被 copy/同步工具改写)。
|
|
749
|
+
*
|
|
750
|
+
* @param {string} cwd
|
|
751
|
+
* @param {string|null} keepPath - 本次刚写的快照路径,绝不裁(并发 import 互删护栏)
|
|
752
|
+
* @returns {number} 实际删除的快照份数
|
|
753
|
+
*/
|
|
754
|
+
_pruneImportBaks(cwd, keepPath) {
|
|
755
|
+
let keep = Number(process.env.SILLYSPEC_PREIMPORT_BAK_KEEP);
|
|
756
|
+
if (!Number.isInteger(keep) || keep < 1) keep = PRE_IMPORT_BAK_KEEP_DEFAULT;
|
|
757
|
+
|
|
758
|
+
const runtimeDir = this._runtimePath(cwd);
|
|
759
|
+
const baks = readdirSync(runtimeDir).filter(f => PRE_IMPORT_BAK_RE.test(f)).sort();
|
|
760
|
+
const keepName = keepPath ? basename(keepPath) : null;
|
|
761
|
+
const doomed = baks.slice(0, Math.max(0, baks.length - keep)).filter(f => f !== keepName);
|
|
762
|
+
|
|
763
|
+
let removed = 0;
|
|
764
|
+
for (const f of doomed) {
|
|
765
|
+
// 单份删除失败(他进程持句柄 / 权限)不连坐其余份:跳过即可,下次 import 再收。
|
|
766
|
+
try { unlinkSync(join(runtimeDir, f)); } catch { continue; }
|
|
767
|
+
removed++;
|
|
768
|
+
const wal = join(runtimeDir, `${f}-wal`);
|
|
769
|
+
if (existsSync(wal)) {
|
|
770
|
+
try { unlinkSync(wal); } catch { /* 侧车残留无害:不参与恢复链,下次裁剪再收 */ }
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return removed;
|
|
774
|
+
}
|
|
775
|
+
|
|
710
776
|
/**
|
|
711
777
|
* 本地脏度标记(D-013 / task-04):写入路径末尾更新 changes.last_local_modified_ts。
|
|
712
778
|
* 在写方法的事务内调用则属于该事务(原子,失败整体回滚);import 是例外不调本方法
|
|
@@ -839,6 +905,13 @@ export class ProgressManager {
|
|
|
839
905
|
// 单变更阶段查询(quick 轻量归档阶段闸):无行 null,读失败抛(调用方 fail-closed)
|
|
840
906
|
getChangeStage(cwd, changeName) { return this._changeRegistry.getChangeStage(cwd, changeName); }
|
|
841
907
|
|
|
908
|
+
// verifyStartAt 基准只读(2026-09-08-ir-verify-facts):execute 行 completed_at;无行 null 走 R-05 fallback
|
|
909
|
+
getStageCompletedAt(cwd, changeName, stage) { return this._changeRegistry.getStageCompletedAt(cwd, changeName, stage); }
|
|
910
|
+
|
|
911
|
+
// evidence mtime 窗口锚点(2026-09-10 用户反馈②):execute 行 started_at——证据合法产自 execute
|
|
912
|
+
// 或 verify 两窗口,锚「完成时刻」会把 execute 期间产的证据判旧(时序两难);无行 null 走 completed_at → R-05
|
|
913
|
+
getStageStartedAt(cwd, changeName, stage) { return this._changeRegistry.getStageStartedAt(cwd, changeName, stage); }
|
|
914
|
+
|
|
842
915
|
registerChange(cwd, changeName) { return this._changeRegistry.registerChange(cwd, changeName); }
|
|
843
916
|
|
|
844
917
|
updateChangeIsolation(cwd, changeName, isolation) { return this._changeRegistry.updateChangeIsolation(cwd, changeName, isolation); }
|
package/src/quick-recommend.js
CHANGED
|
@@ -84,12 +84,17 @@ export function recommendChanges({
|
|
|
84
84
|
taskDescription = '',
|
|
85
85
|
}) {
|
|
86
86
|
if (!activeChanges || activeChanges.length === 0) return []
|
|
87
|
+
// 活跃 quick 会话过滤(2026-09-09 §7-3):quick-<hex8> 是会话不是完整流程变更——无
|
|
88
|
+
// design/proposal 语义可关联(冒烟实证:新 quick 被自动挂上另一活跃 quick 当关联变更)。
|
|
89
|
+
// 形态单源 QUICK_SID_RE(run/shared.js);此处不 import 防把 fs 纯函数拖进 shared 依赖链。
|
|
90
|
+
const QUICK_SESSION_ID_RE = /^quick-[0-9a-f]{8}$/
|
|
91
|
+
const candidateChanges = activeChanges.filter((name) => !QUICK_SESSION_ID_RE.test(name))
|
|
87
92
|
|
|
88
93
|
const dirtyFiles = [...new Set([...baselineFiles, ...quickFiles])]
|
|
89
94
|
.filter(f => f && !f.startsWith('.sillyspec/'))
|
|
90
95
|
const descTokens = [...new Set(tokenizeDescription(taskDescription))]
|
|
91
96
|
|
|
92
|
-
const results =
|
|
97
|
+
const results = candidateChanges.map(name => {
|
|
93
98
|
const reasons = []
|
|
94
99
|
|
|
95
100
|
// 信号 1:脏文件命中 design.md 文件变更清单
|