sillyspec 3.33.3 → 3.33.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-config/SKILL.md +48 -0
- package/.claude/skills/sillyspec-flow/SKILL.md +4 -0
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/src/config-known-failures.js +258 -0
- package/src/config-schema.js +28 -0
- package/src/config-validate.js +180 -0
- package/src/decision-distill.js +26 -5
- package/src/decision-redomain.js +5 -4
- package/src/docs-check.js +8 -4
- package/src/doctor-diagnostics.js +89 -6
- package/src/flow-parity.js +22 -2
- package/src/flow-preflight.js +67 -0
- package/src/flow.js +252 -55
- package/src/fr-index.js +7 -3
- package/src/git-helper.js +82 -0
- package/src/handoff.js +4 -0
- package/src/hooks/worktree-guard.js +12 -3
- package/src/hot-cache.js +60 -0
- package/src/index.js +111 -7
- package/src/knowledge-graph.js +82 -2
- package/src/knowledge-match.js +18 -2
- package/src/knowledge-quicklog.js +17 -2
- package/src/knowledge-vector.js +5 -3
- package/src/machine-interface.js +34 -13
- package/src/progress/change-registry.js +15 -0
- package/src/progress/shared.js +4 -0
- package/src/progress/stage-machine.js +15 -2
- package/src/progress.js +5 -0
- package/src/redomain-unmapped.js +204 -0
- package/src/redomain.js +5 -3
- package/src/review-dispatch.js +208 -47
- package/src/run/bg-sync.js +4 -0
- package/src/run/code-face-key.js +4 -1
- package/src/run/command.js +20 -5
- package/src/run/complete-handlers.js +38 -4
- package/src/run/complete.js +185 -14
- package/src/run/gates.js +30 -1
- package/src/run/prompt.js +9 -1
- package/src/run/quick-audit.js +4 -2
- package/src/run/shared.js +18 -1
- package/src/run/stage.js +22 -19
- package/src/run/verify-quality-scan.js +51 -0
- package/src/runtime-paths.js +3 -0
- package/src/sillyhub-mcp/client.js +11 -6
- package/src/spawn-settle.js +47 -0
- package/src/spec-sync.js +89 -3
- package/src/stages/knowledge.js +25 -3
- package/src/timing.js +55 -0
- package/src/verify-postcheck.js +153 -11
- package/src/watcher.js +4 -0
- package/src/worktree-apply.js +67 -4
- package/src/worktree.js +80 -11
- package/src/yaml-scalar-unquote.js +43 -0
- package/templates/agents-instruction.md +1 -1
package/src/hot-cache.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hot-cache.js — 跨进程热路径缓存小工具(2026-10-21-knowledge-tier-denoise FR-03)
|
|
3
|
+
*
|
|
4
|
+
* 服务对象:纯读派生面(quicklog flow 归档伪条目扫描、knowledge-graph 构建)——输入是
|
|
5
|
+
* 冻结/低频变更面(归档目录、知识库、模块图),派生结果跨进程可复用。落点
|
|
6
|
+
* .sillyspec/.runtime/cache/(gitignored 且被平台 spec 上传排除——与 vector-capability
|
|
7
|
+
* 同款纪律;writeAtomicSync 自建父目录)。
|
|
8
|
+
*
|
|
9
|
+
* 契约:
|
|
10
|
+
* - 读:SILLYSPEC_HOT_CACHE=off 逃生阀直 null;version/fingerprint 任一不匹配 → null;
|
|
11
|
+
* ttlMs>0 且 savedAt 超窗 → null;坏 JSON/缺文件 → null(fail-open,缓存非真相源)
|
|
12
|
+
* - 写:原子;写失败 fail-soft 返回 false(加速面失败不阻断本体)
|
|
13
|
+
* - fingerprint 须可 JSON 稳定序列化且键序/元素序确定(数组按序构造,dirStamp 已排序)
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, readdirSync, statSync } from 'fs'
|
|
16
|
+
import { join } from 'path'
|
|
17
|
+
import { writeAtomicSync } from './fs-atomic.js'
|
|
18
|
+
|
|
19
|
+
/** 读缓存:命中返回 payload,任何不匹配/损坏/关闭 → null。 */
|
|
20
|
+
export function readHotCache(cachePath, { version, fingerprint, ttlMs = 0, now = Date.now() }) {
|
|
21
|
+
if (process.env.SILLYSPEC_HOT_CACHE === 'off') return null
|
|
22
|
+
try {
|
|
23
|
+
const obj = JSON.parse(readFileSync(cachePath, 'utf-8'))
|
|
24
|
+
if (!obj || obj.version !== version) return null
|
|
25
|
+
if (JSON.stringify(obj.fingerprint) !== JSON.stringify(fingerprint)) return null
|
|
26
|
+
if (ttlMs > 0 && !(typeof obj.savedAt === 'number' && now - obj.savedAt <= ttlMs)) return null
|
|
27
|
+
return obj.payload
|
|
28
|
+
} catch { return null }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 写缓存:原子落盘 {version, fingerprint, savedAt, payload};失败 fail-soft。 */
|
|
32
|
+
export function writeHotCache(cachePath, { version, fingerprint, payload, now = Date.now() }) {
|
|
33
|
+
try {
|
|
34
|
+
writeAtomicSync(cachePath, JSON.stringify({ version, fingerprint, savedAt: now, payload }))
|
|
35
|
+
return true
|
|
36
|
+
} catch { return false }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 目录级指纹:子项名全集 + 目录 mtimeMs——子项增删/改名必变;不感知深层内容变化
|
|
40
|
+
* (配冻结输入面用:归档目录、或配 TTL 硬上限)。目录缺席 → null。 */
|
|
41
|
+
export function dirStamp(dir) {
|
|
42
|
+
try {
|
|
43
|
+
return { names: readdirSync(dir).sort(), mtimeMs: statSync(dir).mtimeMs }
|
|
44
|
+
} catch { return null }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 文件级指纹:目录内全部文件(含 _module-map.yaml 等非 md)名 → mtimeMs:size,按名排序。
|
|
48
|
+
* 感知本目录文件的新增/删除/就地重写(模块卡归档重写、知识文件蒸馏都在此列);
|
|
49
|
+
* 不递归子目录(子目录增删由上层逐目录 fileDirStamp 覆盖)。目录缺席 → null。 */
|
|
50
|
+
export function fileDirStamp(dir) {
|
|
51
|
+
try {
|
|
52
|
+
const out = []
|
|
53
|
+
for (const f of readdirSync(dir).sort()) {
|
|
54
|
+
const st = statSync(join(dir, f))
|
|
55
|
+
if (st.isDirectory()) continue
|
|
56
|
+
out.push([f, `${st.mtimeMs}:${st.size}`])
|
|
57
|
+
}
|
|
58
|
+
return out
|
|
59
|
+
} catch { return null }
|
|
60
|
+
}
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 只负责两件事:init(安装命令模板)和 setup(安装 MCP 工具)。
|
|
7
7
|
* 状态管理通过 sillyspec.db(SQLite)完成,使用 `sillyspec progress` 命令。
|
|
8
8
|
*/
|
|
9
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, unlinkSync } from 'fs';
|
|
9
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, writeSync, unlinkSync } from 'fs';
|
|
10
10
|
import { runtimeStatePath } from './runtime-paths.js'
|
|
11
11
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
12
12
|
import { basename, dirname, extname, join, resolve, isAbsolute, sep, relative } from 'path';
|
|
@@ -237,6 +237,47 @@ function refuseRetiredQuickFreshStart(stageArgs) {
|
|
|
237
237
|
process.exit(1)
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* 全局输出收集器(2026-10-21-protocol-tax FR-03)。
|
|
242
|
+
*
|
|
243
|
+
* 收编会话考古(z1)发现的 agent 自发模式:44/48 次把 CLI 输出重定向进日志再 tail 尾部——
|
|
244
|
+
* 工具精心生成的规则大半没进上下文,且 shell 包装会掩蔽真实退出码。内建后:全文落
|
|
245
|
+
* --out-file 文件、stdout 只回提示行+尾 N 行(--tail,默认 40),退出码零拦截(失败信息
|
|
246
|
+
* 进尾行或全文件,不丢)。
|
|
247
|
+
*
|
|
248
|
+
* 实现要点:
|
|
249
|
+
* - 拦截点选 process.stdout.write/process.stderr.write(console.log/error 底层同路,
|
|
250
|
+
* JSON envelope 直写 process.stdout 的命令也被覆盖;stdio:'inherit' 的子进程直写 fd
|
|
251
|
+
* 不经此层——已知豁免面,退出码不受影响)。
|
|
252
|
+
* - exit 钩子同步落盘+尾行输出用 fs.writeSync 直写 fd(exit 期 pipe 异步写会丢)。
|
|
253
|
+
* - 观测面优先、绝不改变命令成败:写文件失败退化为全量打印;收集器任何异常不影响退出码。
|
|
254
|
+
*/
|
|
255
|
+
function installOutFileRedirect(outFile, tailN) {
|
|
256
|
+
const entries = [];
|
|
257
|
+
const push = (chunk) => { entries.push(typeof chunk === 'string' ? chunk : chunk.toString()); return true; };
|
|
258
|
+
process.stdout.write = push;
|
|
259
|
+
process.stderr.write = push;
|
|
260
|
+
process.on('exit', () => {
|
|
261
|
+
const full = entries.join('');
|
|
262
|
+
let wrote = true;
|
|
263
|
+
try { writeFileSync(outFile, full); } catch { wrote = false; }
|
|
264
|
+
if (!wrote) {
|
|
265
|
+
try {
|
|
266
|
+
writeSync(2, `⚠️ --out-file 写入失败(${outFile})——改为全量打印:\n`);
|
|
267
|
+
writeSync(1, full);
|
|
268
|
+
} catch { /* exit 期 best-effort */ }
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const lines = full.split('\n');
|
|
272
|
+
if (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
273
|
+
const tail = lines.slice(-tailN);
|
|
274
|
+
try {
|
|
275
|
+
writeSync(1, `📄 全文已写 ${outFile}(${lines.length} 行,尾 ${tail.length} 行):\n`);
|
|
276
|
+
if (tail.length) writeSync(1, tail.join('\n') + '\n');
|
|
277
|
+
} catch { /* exit 期 best-effort */ }
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
240
281
|
async function main() {
|
|
241
282
|
const args = process.argv.slice(2);
|
|
242
283
|
|
|
@@ -269,6 +310,9 @@ async function main() {
|
|
|
269
310
|
// 平台模式 flag(init 落平台指针用;非 init 命令时忽略——runCommand 自行解析 filteredArgs)
|
|
270
311
|
let platformWorkspaceId = null;
|
|
271
312
|
let platformRuntimeRoot = null;
|
|
313
|
+
// 全局输出收集器(2026-10-21-protocol-tax FR-03):--out-file <路径> / --tail N
|
|
314
|
+
let outFileRedirect = null;
|
|
315
|
+
let outFileTailLines = null;
|
|
272
316
|
const filteredArgs = [];
|
|
273
317
|
|
|
274
318
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -305,6 +349,13 @@ async function main() {
|
|
|
305
349
|
platformRuntimeRoot = resolve(args[i + 1]);
|
|
306
350
|
filteredArgs.push(args[i], args[i + 1]);
|
|
307
351
|
i++;
|
|
352
|
+
} else if (args[i] === '--out-file' && args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
353
|
+
// 全局输出收集器(2026-10-21-protocol-tax FR-03):全文落文件、stdout 只回尾 N 行
|
|
354
|
+
outFileRedirect = resolve(args[i + 1]);
|
|
355
|
+
i++;
|
|
356
|
+
} else if (args[i] === '--tail' && args[i + 1] && /^\d+$/.test(args[i + 1]) && parseInt(args[i + 1], 10) > 0) {
|
|
357
|
+
outFileTailLines = parseInt(args[i + 1], 10);
|
|
358
|
+
i++;
|
|
308
359
|
} else if (args[i] === '--interactive' || args[i] === '-i') {
|
|
309
360
|
interactive = true;
|
|
310
361
|
} else if (args[i] === '--allow-worktree-cwd') {
|
|
@@ -324,6 +375,13 @@ async function main() {
|
|
|
324
375
|
process.exit(2);
|
|
325
376
|
}
|
|
326
377
|
|
|
378
|
+
// --out-file/--tail 校验与安装(2026-10-21-protocol-tax FR-03):必须在任何命令输出之前装好
|
|
379
|
+
if (outFileTailLines !== null && outFileRedirect === null) {
|
|
380
|
+
console.error('❌ --tail 需与 --out-file 同用(控制 --out-file 模式下 stdout 尾部行数,默认 40)。');
|
|
381
|
+
process.exit(2);
|
|
382
|
+
}
|
|
383
|
+
if (outFileRedirect) installOutFileRedirect(outFileRedirect, outFileTailLines ?? 40);
|
|
384
|
+
|
|
327
385
|
// ── 旧布局迁移(2026-10-18 runtime-relayout):per-change 散文件入 state/、可删产物入 temp/。
|
|
328
386
|
// 每次入口跑一遍(幂等:已收敛后 readdir 一轮零写入);fail-open 绝不阻断命令。
|
|
329
387
|
// init 除外——目标仓尚无 .sillyspec 可迁。resolveEffectiveDir 是函数声明(提升后可调)。
|
|
@@ -1683,12 +1741,18 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
1683
1741
|
const rdStage = rdStageIdx >= 0 && args[rdStageIdx + 1] && !String(args[rdStageIdx + 1]).startsWith("--") ? args[rdStageIdx + 1] : null;
|
|
1684
1742
|
const rdStatus = args.includes('--status');
|
|
1685
1743
|
const rdKill = args.includes('--kill');
|
|
1744
|
+
const rdSalvage = args.includes('--salvage');
|
|
1686
1745
|
const rdMode = rdKill ? 'kill' : rdStatus ? 'status' : 'create';
|
|
1687
|
-
const rdUsage = '用法: sillyspec review-dispatch --change <名> --stage <brainstorm|plan|execute> [--status] [--kill]\n' +
|
|
1746
|
+
const rdUsage = '用法: sillyspec review-dispatch --change <名> --stage <brainstorm|plan|execute> [--status] [--kill] [--status --salvage]\n' +
|
|
1688
1747
|
' 缺省=派发(probe 前置→create_mission(external)→dispatch_worker(read_only),异步返回 missionId)\n' +
|
|
1689
1748
|
' --status=轮询状态+停滞提示;worker completed 自动回收落盘 review.json(reviewer.channel=platform)\n' +
|
|
1749
|
+
' --status --salvage=终态原文兜底:worker-answer.md 落盘 + register-stage-review --from 回收指引(提取失败时用)\n' +
|
|
1690
1750
|
' --kill=清本地在途记录+平台处置指引(不自动 kill,处置权在人)';
|
|
1691
1751
|
if (!rdChange) { console.error(rdUsage); process.exit(2); }
|
|
1752
|
+
if (rdSalvage && !rdStatus) {
|
|
1753
|
+
console.error(`❌ --salvage 仅与 --status 组合使用(completed 提取失败后的终态原文兜底回收)\n${rdUsage}`);
|
|
1754
|
+
process.exit(2);
|
|
1755
|
+
}
|
|
1692
1756
|
assertSafeChangeName(rdChange, '--change 变更名');
|
|
1693
1757
|
if (rdMode === 'create' && !['brainstorm', 'plan', 'execute'].includes(rdStage)) {
|
|
1694
1758
|
console.error(`❌ create 形态必须 --stage ∈ brainstorm|plan|execute(实际:${rdStage || '缺'})\n${rdUsage}`);
|
|
@@ -1706,7 +1770,7 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
1706
1770
|
const rdSpecBase = resolvePlatformSpecDir(dir, specDir);
|
|
1707
1771
|
const r = await runReviewDispatch({
|
|
1708
1772
|
mode: rdMode, cwd: dir, specBase: rdSpecBase, changeName: rdChange, stage: rdStage,
|
|
1709
|
-
client: rdClient, budgetUsd: rdCfg.budget_usd, stallMs: rdCfg.stall_ms,
|
|
1773
|
+
client: rdClient, budgetUsd: rdCfg.budget_usd, stallMs: rdCfg.stall_ms, salvage: rdSalvage,
|
|
1710
1774
|
});
|
|
1711
1775
|
if (r.ok) {
|
|
1712
1776
|
if (r.statusLine) console.log(`🛰️ ${r.statusLine}`);
|
|
@@ -1721,6 +1785,7 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
1721
1785
|
} else {
|
|
1722
1786
|
console.error(`❌ review-dispatch(${rdMode}): ${r.reason}`);
|
|
1723
1787
|
if (r.statusLine) console.error(` ${r.statusLine}`);
|
|
1788
|
+
if (r.salvagePath) console.error(` 📄 worker 终态原文:${r.salvagePath}`);
|
|
1724
1789
|
if (r.errors) for (const e of r.errors) console.error(` - ${e}`);
|
|
1725
1790
|
if (r.guidance) console.error(` 下一步:\n${r.guidance}`);
|
|
1726
1791
|
process.exitCode = 1;
|
|
@@ -3264,8 +3329,13 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
3264
3329
|
const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
|
|
3265
3330
|
// 下行 pull:stage 命令(run/--done,含 archive)启动时拉一次(task-10 / D-009 / FR-04)
|
|
3266
3331
|
// 不在每步 pull,仅低频边界点;未连接平台静默跳过
|
|
3267
|
-
|
|
3268
|
-
|
|
3332
|
+
// 例外(2026-10-21-protocol-tax task-06 / FR-06):status 只读查看不 pull——消费仓实测
|
|
3333
|
+
// pull 占 status 6.3s 的大头(profile 71% idle 等网络);本地 DB 是查看态事实源
|
|
3334
|
+
//(progress show 同款语义——它从不 pull),新鲜度交给写命令与 daemon/bg-sync。
|
|
3335
|
+
if (command !== 'status') {
|
|
3336
|
+
const { triggerPullActiveChange } = await import('./run/shared.js')
|
|
3337
|
+
await triggerPullActiveChange(effectiveDir)
|
|
3338
|
+
}
|
|
3269
3339
|
await runCommand(stageArgs, effectiveDir, specDir, { json })
|
|
3270
3340
|
break
|
|
3271
3341
|
}
|
|
@@ -5322,6 +5392,10 @@ SillySpec config — local.yaml 配置键速查
|
|
|
5322
5392
|
git common-dir 主仓根;worktree 内 .sillyspec 是 checkout
|
|
5323
5393
|
副本无此文件,cat 自动解析到主仓)
|
|
5324
5394
|
sillyspec config cat --json 机读 JSON(dir/path/source/content;未找到退出 1)
|
|
5395
|
+
sillyspec config known-failures list 列 known_failures 豁免键
|
|
5396
|
+
sillyspec config known-failures add <key…> 增豁免键(行级手术保注释、原子写;坏 YAML 拒改)
|
|
5397
|
+
sillyspec config known-failures remove <key…> 删豁免键(未命中静默忽略)
|
|
5398
|
+
sillyspec config validate [--json] 逐键校验 local.yaml(错误 exit 1;缺文件=零配置可用 exit 0)
|
|
5325
5399
|
|
|
5326
5400
|
说明:
|
|
5327
5401
|
- 数据源:src/config-schema.js(唯一真相;reader 见各键「读取点」)
|
|
@@ -5363,11 +5437,41 @@ SillySpec config — local.yaml 配置键速查
|
|
|
5363
5437
|
if (!content.endsWith('\n')) process.stdout.write('\n')
|
|
5364
5438
|
break;
|
|
5365
5439
|
}
|
|
5440
|
+
// config validate(2026-10-23-config-validate-skill):逐键校验(schema 单一源;YAML/
|
|
5441
|
+
// 类型/枚举/段内必填凭据闭合,未知键警告)——agent 写完配置的硬验证门(技能末步=本命令
|
|
5442
|
+
// exit 0 才算完成);缺文件=零配置可用 info exit 0(69 键仅凭据族真必填)。
|
|
5443
|
+
if (configSub === 'validate') {
|
|
5444
|
+
let specBase = null
|
|
5445
|
+
try { specBase = resolvePlatformSpecDir(dir, specDir) } catch { /* 同 cat:pointer 异常走祖先链 */ }
|
|
5446
|
+
const { cmdConfigValidate } = await import('./config-validate.js')
|
|
5447
|
+
const exitCode = cmdConfigValidate({ dir, specBase, json })
|
|
5448
|
+
process.exitCode = exitCode
|
|
5449
|
+
break;
|
|
5450
|
+
}
|
|
5451
|
+
// config known-failures(2026-10-21-p0-p1-hardening FR-07 / dsh z2 实证:known_failures
|
|
5452
|
+
// 无 CLI 管理面 → agent 手改 local.yaml,PS5.1 编码把 50KB 文件写成 mojibake 等用户
|
|
5453
|
+
// 2h13m)。行级增删查:保注释、原子写、坏 YAML 拒改——消灭手改场景。
|
|
5454
|
+
if (configSub === 'known-failures') {
|
|
5455
|
+
let specBase = null
|
|
5456
|
+
try { specBase = resolvePlatformSpecDir(dir, specDir) } catch { /* 同 cat:pointer 异常走祖先链 */ }
|
|
5457
|
+
const { resolveLocalYaml } = await import('./config-cat.js')
|
|
5458
|
+
const located = resolveLocalYaml(dir, { specBase })
|
|
5459
|
+
const { runKnownFailuresCommand } = await import('./config-known-failures.js')
|
|
5460
|
+
const verb = filteredArgs[2];
|
|
5461
|
+
const keys = filteredArgs.slice(3).filter((a) => a && !a.startsWith('--'));
|
|
5462
|
+
const exitCode = runKnownFailuresCommand({
|
|
5463
|
+
yamlPath: located.exists ? located.path : join(dir, '.sillyspec', 'local.yaml'),
|
|
5464
|
+
exists: located.exists,
|
|
5465
|
+
verb, keys, json,
|
|
5466
|
+
});
|
|
5467
|
+
process.exitCode = exitCode;
|
|
5468
|
+
break;
|
|
5469
|
+
}
|
|
5366
5470
|
if (!isSchema) {
|
|
5367
|
-
const sug = didYouMean(configSub, ['schema', 'cat']);
|
|
5471
|
+
const sug = didYouMean(configSub, ['schema', 'cat', 'validate', 'known-failures']);
|
|
5368
5472
|
console.error(`❌ 未知子命令: config ${configSub}`);
|
|
5369
5473
|
if (sug) console.error(` 你是想输入「config ${sug}」吗?`);
|
|
5370
|
-
console.error('用法: sillyspec config [schema|cat] [--json]
|
|
5474
|
+
console.error('用法: sillyspec config [schema|cat|validate|known-failures] [--json] 打印配置键 / 读内容 / 校验 / 管理 known_failures 键');
|
|
5371
5475
|
process.exit(2);
|
|
5372
5476
|
}
|
|
5373
5477
|
const { renderSchemaHuman, renderSchemaJson } = await import('./config-schema.js');
|
package/src/knowledge-graph.js
CHANGED
|
@@ -18,10 +18,11 @@
|
|
|
18
18
|
* 模块域内解析(沿 fr-index renderFrLines / module-changelog 行形态的机械契约)。
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
21
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
|
|
22
22
|
import { join } from 'path'
|
|
23
23
|
import { parseKnowledgeIndex, parseDecisionEntries, anchorFilePaths } from './knowledge-match.js'
|
|
24
24
|
import { modulePathMatches, parseModuleMapSimple } from './modules.js'
|
|
25
|
+
import { readHotCache, writeHotCache, dirStamp, fileDirStamp } from './hot-cache.js'
|
|
25
26
|
|
|
26
27
|
// ── 本体常量(design.md 总体方案 §1 的代码化) ──
|
|
27
28
|
|
|
@@ -60,7 +61,7 @@ function extractFilePaths(text, cap = 15) {
|
|
|
60
61
|
|
|
61
62
|
// ── 构建器 ──
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
function buildKnowledgeGraphFresh(specRoot) {
|
|
64
65
|
const nodes = new Map()
|
|
65
66
|
const edges = []
|
|
66
67
|
const byEdgeType = new Map()
|
|
@@ -340,6 +341,85 @@ export function buildKnowledgeGraph(specRoot) {
|
|
|
340
341
|
return { root: specRoot, nodes, edges, byEdgeType, stats }
|
|
341
342
|
}
|
|
342
343
|
|
|
344
|
+
// ── 跨进程缓存(FR-03,2026-10-21-knowledge-tier-denoise)──
|
|
345
|
+
// 图召回是四层检索的第三层 advisory 保底(fail-soft);构建成本随归档线性(逐变更读
|
|
346
|
+
// design.md+change-patch.json),flow start/知识门/doctor 热路径上无缓存重付。D-003 墓碑
|
|
347
|
+
// 切割原则不变:md/yaml 仍是唯一真相源——缓存只存派生图、键在输入指纹上,不构成第二
|
|
348
|
+
// 真相源;任何失配全量重建。
|
|
349
|
+
// 陈旧边界(有意接受):指纹感知本目录文件级变化与目录增删,但 docs 树深层就地重写若
|
|
350
|
+
// 未触及被 stat 的文件面则漏——TTL 10min 硬上限兜底,advisory 面有界陈旧可接受。
|
|
351
|
+
|
|
352
|
+
const GRAPH_CACHE_VERSION = 1
|
|
353
|
+
const GRAPH_CACHE_TTL_MS = 10 * 60 * 1000
|
|
354
|
+
|
|
355
|
+
function statSignature(p) {
|
|
356
|
+
try { const s = statSync(p); return `${s.mtimeMs}:${s.size}` } catch { return null }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** 输入面指纹:knowledge(INDEX+decisions+fr)+ docs 各项目(modules+scan 文件级)+
|
|
360
|
+
* 归档根(dirStamp)+ quicklog test-bindings。键序固定、数组排序——JSON 稳定比较。
|
|
361
|
+
* 各面缺席记 null(合法形态:无 docs/ 的小仓、无 fr 库)——缺席本身是指纹的一部分。 */
|
|
362
|
+
function knowledgeGraphFingerprint(specRoot) {
|
|
363
|
+
const knowledgeDir = join(specRoot, 'knowledge')
|
|
364
|
+
const docsDir = join(specRoot, 'docs')
|
|
365
|
+
let projects = []
|
|
366
|
+
try {
|
|
367
|
+
projects = readdirSync(docsDir, { withFileTypes: true })
|
|
368
|
+
.filter((d) => d.isDirectory())
|
|
369
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
370
|
+
.map((d) => [
|
|
371
|
+
d.name,
|
|
372
|
+
fileDirStamp(join(docsDir, d.name, 'modules')),
|
|
373
|
+
fileDirStamp(join(docsDir, d.name, 'scan')),
|
|
374
|
+
])
|
|
375
|
+
} catch { /* docs/ 缺席 = 无项目面(合法形态)*/ }
|
|
376
|
+
return {
|
|
377
|
+
index: statSignature(join(knowledgeDir, 'INDEX.md')),
|
|
378
|
+
decisions: fileDirStamp(join(knowledgeDir, 'decisions')),
|
|
379
|
+
fr: fileDirStamp(join(knowledgeDir, 'fr')),
|
|
380
|
+
projects,
|
|
381
|
+
archive: dirStamp(join(specRoot, 'changes', 'archive')),
|
|
382
|
+
testBindings: statSignature(join(specRoot, 'quicklog', 'test-bindings.json')),
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** 缓存载荷 → 图对象(nodes Map 按 payload 序重建保迭代序一致;byEdgeType/stats 复算)。 */
|
|
387
|
+
function reviveGraph(payload, specRoot) {
|
|
388
|
+
const nodes = new Map(payload.nodes.map((n) => [String(n.id), n]))
|
|
389
|
+
const edges = payload.edges.map((e) => ({ s: String(e.s), t: String(e.t), type: e.type, note: String(e.note || '') }))
|
|
390
|
+
const byEdgeType = new Map()
|
|
391
|
+
for (const e of edges) {
|
|
392
|
+
if (!byEdgeType.has(e.type)) byEdgeType.set(e.type, [])
|
|
393
|
+
byEdgeType.get(e.type).push(e)
|
|
394
|
+
}
|
|
395
|
+
const stats = {
|
|
396
|
+
nodeCount: nodes.size,
|
|
397
|
+
edgeCount: edges.length,
|
|
398
|
+
byNodeType: [...nodes.values()].reduce((a, n) => (a[n.type] = (a[n.type] || 0) + 1, a), {}),
|
|
399
|
+
byEdgeType: edges.reduce((a, e) => (a[e.type] = (a[e.type] || 0) + 1, a), {}),
|
|
400
|
+
}
|
|
401
|
+
return { root: payload.root || specRoot, nodes, edges, byEdgeType, stats }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** 构建入口(缓存包装,签名与返回结构不变):指纹+TTL+版本键三闸,命中零全量重扫。 */
|
|
405
|
+
export function buildKnowledgeGraph(specRoot) {
|
|
406
|
+
const fingerprint = knowledgeGraphFingerprint(specRoot)
|
|
407
|
+
const cachePath = join(specRoot, '.runtime', 'cache', 'knowledge-graph.json')
|
|
408
|
+
if (fingerprint) {
|
|
409
|
+
const cached = readHotCache(cachePath, { version: GRAPH_CACHE_VERSION, fingerprint, ttlMs: GRAPH_CACHE_TTL_MS })
|
|
410
|
+
if (cached && Array.isArray(cached.nodes) && Array.isArray(cached.edges)) return reviveGraph(cached, specRoot)
|
|
411
|
+
}
|
|
412
|
+
const graph = buildKnowledgeGraphFresh(specRoot)
|
|
413
|
+
if (fingerprint) {
|
|
414
|
+
writeHotCache(cachePath, {
|
|
415
|
+
version: GRAPH_CACHE_VERSION,
|
|
416
|
+
fingerprint,
|
|
417
|
+
payload: { root: graph.root, nodes: [...graph.nodes.values()], edges: graph.edges },
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
return graph
|
|
421
|
+
}
|
|
422
|
+
|
|
343
423
|
/** changelog 三态行解析(表格行 ∪ 列表行 ∪ 标题态——backend 侧主形态;坏行跳过 fail-soft)。 */
|
|
344
424
|
export function parseChangelogEntries(content) {
|
|
345
425
|
const out = []
|
package/src/knowledge-match.js
CHANGED
|
@@ -292,6 +292,16 @@ function asciiWordHit(sh, text) {
|
|
|
292
292
|
return new RegExp(`(^|[^a-z0-9-])${sh.replace(/[.*+?^${}()|[\]\\-]/g, '\\$&')}([^a-z0-9-]|$)`, 'i').test(text)
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
/** 停车场文件(未归类 unmapped 域——decisions 与 fr 两面):检索核心零返回,文件本身
|
|
296
|
+
* 保留在盘上供人工直查(search 面以 parkingFiltered 披露指路)。2026-10-21-knowledge-tier-denoise
|
|
297
|
+
* FR-01,对齐 FR 注入面 2026-09-29-rot-retire-inject-cap 的 unmapped 过滤口径(停车场非行为域)。 */
|
|
298
|
+
const PARKING_FILES = new Set(['decisions/unmapped.md', 'fr/unmapped.md'])
|
|
299
|
+
|
|
300
|
+
/** file 是否停车场文件(decisions/fr unmapped;Windows 反斜杠归一后判等)。 */
|
|
301
|
+
export function isParkingFile(file) {
|
|
302
|
+
return PARKING_FILES.has(String(file || '').replace(/\\/g, '/'))
|
|
303
|
+
}
|
|
304
|
+
|
|
295
305
|
/**
|
|
296
306
|
* 零命中查询侧词片回退(2026-09-29-decision-route-vocab):路由行零命中时,只测「查询自己的
|
|
297
307
|
* 词片」——逐 decisions 域文件数词片在条目文本(标题∪理由)的出现次数,落在「跨条目复现但
|
|
@@ -309,6 +319,7 @@ export function fallbackByQueryShingles(indexDir, taskContext) {
|
|
|
309
319
|
if (all.length === 0) return null
|
|
310
320
|
const byFile = new Map()
|
|
311
321
|
for (const h of all) {
|
|
322
|
+
if (isParkingFile(h.file)) continue // 停车场文件不参与词片回退(FR-01)
|
|
312
323
|
let st = byFile.get(h.file)
|
|
313
324
|
if (!st) { st = { entries: [], texts: [] }; byFile.set(h.file, st) }
|
|
314
325
|
st.entries.push(h)
|
|
@@ -395,16 +406,21 @@ export function matchByRouting(indexDir, taskContext) {
|
|
|
395
406
|
}
|
|
396
407
|
|
|
397
408
|
const contextLower = taskContext.toLowerCase()
|
|
398
|
-
const
|
|
409
|
+
const matchedRaw = allEntries.filter(entry => {
|
|
399
410
|
return entry.keywords.some(kw => keywordMatchesContext(kw, contextLower))
|
|
400
411
|
})
|
|
412
|
+
// 停车场过滤(FR-01,2026-10-21-knowledge-tier-denoise):unmapped 路由行不进命中集;
|
|
413
|
+
// 只命中停车场行 → 按零命中处理(hybrid 链继续走向量/图/词片层),json.parkingFiltered
|
|
414
|
+
// 披露被滤文件供 search 面提示人工直查(可见性不因去噪丢失)。
|
|
415
|
+
const parkingFiltered = [...new Set(matchedRaw.filter((e) => isParkingFile(e.file)).map((e) => e.file))]
|
|
416
|
+
const matched = matchedRaw.filter((e) => !isParkingFile(e.file))
|
|
401
417
|
|
|
402
418
|
if (matched.length === 0) {
|
|
403
419
|
return {
|
|
404
420
|
matched: false,
|
|
405
421
|
entries: [],
|
|
406
422
|
report: 'Status: no matches',
|
|
407
|
-
json: { matched: false, entry_count: 0, entries: [] },
|
|
423
|
+
json: { matched: false, entry_count: 0, entries: [], ...(parkingFiltered.length > 0 ? { parkingFiltered } : {}) },
|
|
408
424
|
decisionHits: []
|
|
409
425
|
}
|
|
410
426
|
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
|
|
21
21
|
import { join } from 'path'
|
|
22
|
+
import { readHotCache, writeHotCache, dirStamp } from './hot-cache.js'
|
|
22
23
|
|
|
23
24
|
const QUICKLOG_ENTRY_RE = /^##\s+(ql-[\w-]+)\s*\|\s*([^|]+)\s*\|\s*(.+)$/
|
|
24
25
|
// 文件注记行:`- path(中文括注)` / `- path(ascii)` / 裸 `- path`(allocateQuicklogEntry
|
|
@@ -95,10 +96,23 @@ function parseQuicklogFileEntries(specBase) {
|
|
|
95
96
|
return entries
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
/** 伪条目扫描缓存版本(解析契约变化时 bump 即全量失效)。 */
|
|
100
|
+
const FLOW_ARCHIVE_CACHE_VERSION = 1
|
|
101
|
+
|
|
98
102
|
/** 源②:flow 归档变更伪条目——目录内 flow-state.yaml 在场即认(不校验 yaml 内容:伪条目
|
|
99
|
-
* 只需「这曾是一个 flow 变更」的存在信号,字段全来自目录名与 proposal.md,缺件逐项降级)。
|
|
100
|
-
|
|
103
|
+
* 只需「这曾是一个 flow 变更」的存在信号,字段全来自目录名与 proposal.md,缺件逐项降级)。
|
|
104
|
+
* 跨进程缓存(FR-03,2026-10-21-knowledge-tier-denoise):指纹=归档根子项名全集+根 mtimeMs
|
|
105
|
+
* (一次 readdir+一次 stat,廉价每次重算)——命中即免逐目录列举与逐变更 proposal.md 读。
|
|
106
|
+
* 归档件是冻结审计面(无就地编辑形态),指纹对增删/改名完备;坏缓存/失配 fail-open 全量重建。
|
|
107
|
+
* 导出供测试断言缓存三态(命中/失效/坏缓存)。 */
|
|
108
|
+
export function parseFlowArchiveEntries(specBase) {
|
|
101
109
|
const archiveDir = join(specBase, 'changes', 'archive')
|
|
110
|
+
const fingerprint = dirStamp(archiveDir)
|
|
111
|
+
if (fingerprint === null) return []
|
|
112
|
+
const cachePath = join(specBase, '.runtime', 'cache', 'quicklog-archive-entries.json')
|
|
113
|
+
const cached = readHotCache(cachePath, { version: FLOW_ARCHIVE_CACHE_VERSION, fingerprint })
|
|
114
|
+
if (Array.isArray(cached)) return cached
|
|
115
|
+
|
|
102
116
|
let dirs
|
|
103
117
|
try {
|
|
104
118
|
dirs = readdirSync(archiveDir, { withFileTypes: true })
|
|
@@ -125,6 +139,7 @@ function parseFlowArchiveEntries(specBase) {
|
|
|
125
139
|
out.push({ qlId: d.name, date, title: title || d.name, solution: '', files: [], source: 'flow' })
|
|
126
140
|
} catch { /* 单目录读取异常 → 跳过该目录(fail-open,不影响其余源) */ }
|
|
127
141
|
}
|
|
142
|
+
writeHotCache(cachePath, { version: FLOW_ARCHIVE_CACHE_VERSION, fingerprint, payload: out })
|
|
128
143
|
return out
|
|
129
144
|
}
|
|
130
145
|
|
package/src/knowledge-vector.js
CHANGED
|
@@ -38,7 +38,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
|
38
38
|
import { runtimeStatePath } from './runtime-paths.js'
|
|
39
39
|
import { join, dirname } from 'path'
|
|
40
40
|
import { readPlatformConfig } from './sync.js'
|
|
41
|
-
import { matchByRouting, fallbackByQueryShingles, parseDecisionEntries } from './knowledge-match.js'
|
|
41
|
+
import { matchByRouting, fallbackByQueryShingles, parseDecisionEntries, isParkingFile } from './knowledge-match.js'
|
|
42
42
|
|
|
43
43
|
const VECTOR_TIMEOUT_MS = 3000
|
|
44
44
|
const ENDPOINT_PATH = '/api/spec/knowledge/vector-search'
|
|
@@ -181,12 +181,14 @@ function specPathToFile(specPath) {
|
|
|
181
181
|
return String(specPath).replace(/^knowledge\//, '').replace(/\\/g, '/')
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/** 平台候选 → matchKnowledge
|
|
185
|
-
|
|
184
|
+
/** 平台候选 → matchKnowledge 同构结果(策略面全本地解析)。导出供测试(FR-01 停车场
|
|
185
|
+
* 候选丢弃断言;策略面全在本地解析的契约不变)。 */
|
|
186
|
+
export function buildResultFromPlatform(indexDir, pvResults) {
|
|
186
187
|
const byFile = new Map()
|
|
187
188
|
for (const r of pvResults) {
|
|
188
189
|
const file = specPathToFile(r.spec_path)
|
|
189
190
|
if (!file.startsWith('decisions/')) continue
|
|
191
|
+
if (isParkingFile(file)) continue // 停车场候选丢弃(FR-01)——平台向量语料含 unmapped,策略面本地滤
|
|
190
192
|
if (!byFile.has(file)) byFile.set(file, [])
|
|
191
193
|
byFile.get(file).push(r)
|
|
192
194
|
}
|
package/src/machine-interface.js
CHANGED
|
@@ -352,9 +352,24 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
352
352
|
// 绿缓存根:与 runtimeRoot 解析同源(drift 锚主仓口径);解析失败 null → 不缓存不命中
|
|
353
353
|
let greenRoot = null;
|
|
354
354
|
try { greenRoot = resolveRuntimeRoot({ runtimeRoot, specDriftAnchor }, specRoot); } catch { greenRoot = null; }
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
355
|
+
// 失败签名复用(2026-10-21-verify-reuse-large-repo FR-03):真跑前咨询上次质量扫描失败——
|
|
356
|
+
// 同签名(代码指纹×豁免面)+ 快照口径一致(CLI 门主仓口径跑,plannedSnapshot=false)+
|
|
357
|
+
// 30min 窗口 → 不重跑,合成 failed(♻️ 披露 + 四出路)照常进 check not-ok 语义。
|
|
358
|
+
// gate verify 预检每轮整跑同签名失败 10~13.5 分钟的浪费由此消除(dsh 0ff6f1c8 取证)。
|
|
359
|
+
let vt = null;
|
|
360
|
+
try {
|
|
361
|
+
const { consultFailedScanReuse, synthesizeReusedFailedCheck } = await import('./run/verify-quality-scan.js');
|
|
362
|
+
const consult = consultFailedScanReuse({
|
|
363
|
+
specBase: specRoot, cwd, changeName, plannedSnapshot: false,
|
|
364
|
+
forceRerun: Boolean(process.env.SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN),
|
|
365
|
+
});
|
|
366
|
+
if (consult.reuse) vt = synthesizeReusedFailedCheck(consult.record);
|
|
367
|
+
} catch { /* 咨询异常 → 照旧 green-cache/真跑 */ }
|
|
368
|
+
if (vt == null) {
|
|
369
|
+
vt = greenCachedCheck({ kind: 'test', greenRoot, cwd, specBase: specRoot, changeName },
|
|
370
|
+
() => runVerifyTestCheck({ cwd, specBase: specRoot, changeName, ctx }),
|
|
371
|
+
(hit) => ({ status: 'passed', exitCode: 0, durationMs: 0, resultPath: null, mode: null, fallbackReason: null, exemptedCount: 0, cached: true, reason: greenCacheNotice(hit, 'verify-test') }));
|
|
372
|
+
}
|
|
358
373
|
const vtWarnings = [];
|
|
359
374
|
if (vt.cached) vtWarnings.push(vt.reason);
|
|
360
375
|
if (vt.status === 'skipped') {
|
|
@@ -455,7 +470,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
455
470
|
// 回合):①target_files reconcile 只读跑(reconcileTargetFiles 纯计算不落盘,--done 的写结果
|
|
456
471
|
// 文件在其调用方);②stage review 在场探测(marker+review.json——缺则报预期路径,同 --done
|
|
457
472
|
// Stage Review Gate 口径,只报缺不生成)。全只读零副作用;未传 full 零行为变化。
|
|
458
|
-
if (full && (stage === 'verify' || stage === 'execute')) {
|
|
473
|
+
if (full && (stage === 'verify' || stage === 'execute' || stage === 'brainstorm' || stage === 'plan')) {
|
|
459
474
|
// e1. reconcile(verify --done 的 target_files 对账门)
|
|
460
475
|
if (stage === 'verify') {
|
|
461
476
|
try {
|
|
@@ -484,10 +499,13 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
484
499
|
});
|
|
485
500
|
}
|
|
486
501
|
}
|
|
487
|
-
// e2. stage review 在场探测(
|
|
488
|
-
//
|
|
489
|
-
// register 枚举亦无 verify,verify 阶段不产
|
|
490
|
-
|
|
502
|
+
// e2. stage review 在场探测(Stage Review Gate 拦截面的 --full 预检——brainstorm/plan/execute
|
|
503
|
+
// 三阶段与 gates.js Stage Review Gate 实拦范围逐字对齐;verify 不在其列,坑
|
|
504
|
+
// stage-review-verify-enum-gap 2026-10-11:register 枚举亦无 verify,verify 阶段不产
|
|
505
|
+
// stage review 期望;verify 的真实门面是 e1 对账。dsh z1 实证(2026-10-21-p0-p1-hardening
|
|
506
|
+
// FR-04):此前 execute 单值使 brainstorm Step8 --done 被 Stage Review Gate「伏击」——
|
|
507
|
+
// gate 预检全绿而 --done 被拦,46 分钟+3 轮子代理评审才过)
|
|
508
|
+
if (stage === 'execute' || stage === 'brainstorm' || stage === 'plan') try {
|
|
491
509
|
const { getLatestStageReviewRunId } = await import('./stage-review.js');
|
|
492
510
|
const rrRoot = runtimeRoot || join(specRoot, '.runtime');
|
|
493
511
|
const runId = getLatestStageReviewRunId(rrRoot, stage, changeName);
|
|
@@ -496,7 +514,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
496
514
|
code: checkCode('full-stage-review') || 'full_stage_review_missing',
|
|
497
515
|
ok: !!runId,
|
|
498
516
|
errors: !runId
|
|
499
|
-
? [`缺 ${stage} 阶段的 stage review.json(tier=independent
|
|
517
|
+
? [`缺 ${stage} 阶段的 stage review.json(tier=independent 要求独立审查产出)——${stage} --done 将被 Stage Review Gate 拦;可先 sillyspec register-stage-review --change ${changeName} --stage ${stage} 生成骨架再填(降级条款照 review.json reviewerNotes 首行留痕)`]
|
|
500
518
|
: [],
|
|
501
519
|
data: { runId: runId || null },
|
|
502
520
|
});
|
|
@@ -518,9 +536,12 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
518
536
|
// register 指引;在场 → 静默 ok。独立 check id 不与 --full 的 full-stage-review 碰撞,
|
|
519
537
|
// error 语义(--done 拦)仍归 --full 档与 --done 本体。
|
|
520
538
|
// stage 期望面对齐(坑 stage-review-verify-enum-gap,2026-10-11 实证):Stage Review Gate
|
|
521
|
-
// 实际只拦 brainstorm/plan/execute(gates.js),register-stage-review 枚举亦无 verify——
|
|
522
|
-
// verify 阶段不产 stage review 提示(提示命令不可执行 + 指称的门禁不存在,双重误导)。
|
|
523
|
-
|
|
539
|
+
// 实际只拦 brainstorm/plan/execute(gates.js),register-stage-review 枚举亦无 verify——
|
|
540
|
+
// verify 阶段不产 stage review 提示(提示命令不可执行 + 指称的门禁不存在,双重误导)。
|
|
541
|
+
// 覆盖面扩全(2026-10-21-p0-p1-hardening FR-04 / dsh z1 实证):此前 execute 单值——brainstorm
|
|
542
|
+
// Step8 --done 被 Stage Review Gate 伏击(预检绿灯 vs 收口拦截口径不一致,损失 46 分钟)。
|
|
543
|
+
// 现与 gates.js 拦截面三阶段逐字对齐;文案按 stage 参数化。
|
|
544
|
+
if (!full && (stage === 'execute' || stage === 'brainstorm' || stage === 'plan')) {
|
|
524
545
|
try {
|
|
525
546
|
const { getLatestStageReviewRunId } = await import('./stage-review.js');
|
|
526
547
|
const rrRoot = runtimeRoot || join(specRoot, '.runtime');
|
|
@@ -531,7 +552,7 @@ if (!full && stage === 'execute') {
|
|
|
531
552
|
ok: true,
|
|
532
553
|
informational: true,
|
|
533
554
|
warnings: runId ? [] : [
|
|
534
|
-
`缺 ${stage} 阶段的 stage review.json
|
|
555
|
+
`缺 ${stage} 阶段的 stage review.json——${stage} --done 的 Stage Review Gate 会拦(本提示不改变 gate 结论)。先产出评审再注册:sillyspec register-stage-review --change ${changeName} --stage ${stage}(骨架生成后填,或 --from review.json 过继独立评审产出)`,
|
|
535
556
|
],
|
|
536
557
|
data: { runId: runId || null },
|
|
537
558
|
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// pm._changePath(persistence-core 留 facade 本体)。无共享常量依赖。
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, renameSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
|
+
import { hostname } from 'os';
|
|
6
7
|
import { appendDeletedLedger } from '../deleted-ledger.js';
|
|
7
8
|
|
|
8
9
|
// 所有权活跃心跳窗缺省(2026-09-14-change-ownership-guards task-02 / FR-01):owner 非本会话
|
|
@@ -290,6 +291,7 @@ export class ChangeRegistry {
|
|
|
290
291
|
* - opts.forced(--takeover) → takeover-forced 放行(无条件接管,调用方重写 owner 留痕)
|
|
291
292
|
* - owner 为空(无行/NULL) → no-owner 放行(存量无主行,任何会话可接管)
|
|
292
293
|
* - owner === selfSession → self 放行(本会话自有 change 零路径变化)
|
|
294
|
+
* - owner 本机匿名占位 + selfSession 显式 → takeover-anon-upgrade 放行(同主机显式身份自动升级,FR-05)
|
|
293
295
|
* - owner 他人 且 now-last_active ≥ 窗口 → takeover-stale 放行(窗口外自动接管重写)
|
|
294
296
|
* - owner 他人 且活跃窗内 → blocked-active-owner 拒绝(allowed:false,调用方打结构化错误)
|
|
295
297
|
* last_active 缺失/不可解析按陈旧处理:无法证明新鲜即不拦——护栏拦「可证活跃」,不因时间戳
|
|
@@ -313,6 +315,19 @@ export class ChangeRegistry {
|
|
|
313
315
|
if (opts.forced) return { allowed: true, action: 'takeover-forced', owner, lastActive, heartbeatMs };
|
|
314
316
|
if (owner === null) return { allowed: true, action: 'no-owner', owner: null, lastActive, heartbeatMs };
|
|
315
317
|
if (owner === opts.selfSession) return { allowed: true, action: 'self', owner, lastActive, heartbeatMs };
|
|
318
|
+
// 匿名占位所有权自动升级(2026-10-21-p0-p1-hardening FR-05 / dsh z1 实证:首调未设
|
|
319
|
+
// SESSION_ID → owner 落 anon@<host>,同机 agent 数分钟后补设显式身份,被
|
|
320
|
+
// blocked-active-owner 拦 15 分钟活跃窗,被迫 --takeover——自己锁自己)。anon@ 是机器级
|
|
321
|
+
// 降级标识(缺省只拦他机),同主机的显式身份(--session/env 三级解析,非 anon 降级)严格
|
|
322
|
+
// 更具体:直接放行升级(调用方 setChangeOwner 重写留痕),不必 --takeover。跨主机匿名
|
|
323
|
+
// (owner 的 @ 后缀 ≠ 本机 hostname)与任何显式 owner 持有不在此列,照旧新鲜度判定。
|
|
324
|
+
if (
|
|
325
|
+
typeof owner === 'string' && owner.startsWith('anon@')
|
|
326
|
+
&& typeof opts.selfSession === 'string' && opts.selfSession && !opts.selfSession.startsWith('anon@')
|
|
327
|
+
&& owner === `anon@${hostname()}`
|
|
328
|
+
) {
|
|
329
|
+
return { allowed: true, action: 'takeover-anon-upgrade', owner, lastActive, heartbeatMs };
|
|
330
|
+
}
|
|
316
331
|
// owner 他人:新鲜度决定拒绝 / 自动接管
|
|
317
332
|
const lastMs = lastActive ? new Date(lastActive).getTime() : NaN;
|
|
318
333
|
const nowMs = Number.isFinite(opts.nowMs) ? opts.nowMs : Date.now();
|
package/src/progress/shared.js
CHANGED
|
@@ -16,6 +16,10 @@ export const STAGE_LABELS = {
|
|
|
16
16
|
quick: '⚡ 快速任务',
|
|
17
17
|
explore: '🧭 自由探索',
|
|
18
18
|
archive: '📦 归档变更',
|
|
19
|
+
// thin 展示键(2026-10-21-overview-thin-stage):轻量变更出身投影专用(overview
|
|
20
|
+
// stage_label 查找),不进 VALID_STAGES/STAGE_ORDER——thin 不走阶段机,加进阶段
|
|
21
|
+
// 清单会建空阶段行扰动级联。◈ 对齐 SillyHub 前端旁路徽标字形(◈ thin)。
|
|
22
|
+
thin: '◈ 轻量变更',
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
// 完整阶段顺序(含 scan),仅用于展示/迭代顺序
|