sillyspec 3.33.5 → 3.33.7

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/src/git-helper.js CHANGED
@@ -96,23 +96,31 @@ function gitChildEnv(env) {
96
96
  * 安全执行 git:失败不抛,返回 { value, error } 结构。
97
97
  * @param {string} cwd
98
98
  * @param {string[]} args
99
- * @param {{ trim?: boolean, timeout?: number, retryOnTimeout?: boolean }} [opts]
99
+ * @param {{ trim?: boolean, timeout?: number, retryOnTimeout?: boolean, input?: string|Buffer, stdio?: string[] }} [opts]
100
100
  * - trim:false 保留原样输出(git status --porcelain 首行前导空格
101
101
  * 是状态码的一部分,trim 会削掉致 parsePorcelainPath 丢首字符,坑见 auditQuickCompletion 注释)
102
102
  * - timeout 默认 5000ms,长操作(大 diff / worktree list)可传更大值
103
103
  * - retryOnTimeout:true 时,ETIMEDOUT 用 2× timeout 重试一次(默认 false 向后兼容)。
104
104
  * 机器忙时 git 子进程启动慢易瞬时超时;审计锚点 git 调用失败即 blocked 中断 quick,
105
105
  * 重试一次能消化绝大多数瞬时抖动,免去用户手工重跑。权限 / 损坏等非超时错误不重试(救不回)。
106
+ * - input: 写入子进程 stdin;传入时自动把 stdio[0] 设为 pipe(否则 Node 静默丢弃 input)
107
+ * - stdio: 显式覆盖子进程 stdio
106
108
  * @returns {{ value: string|null, error: string|null }}
107
109
  */
108
110
  export function safeGit(cwd, args, opts = {}) {
109
- const { trim = true, timeout = 5000, retryOnTimeout = false } = opts
111
+ const { trim = true, timeout = 5000, retryOnTimeout = false, input, stdio = null } = opts
110
112
  const fullArgs = buildFullArgs(cwd, args)
113
+ // stdin 形态:input 传入时必须把 stdin 设为 pipe——Node 的 execFileSync 在
114
+ // stdio[0]='ignore' 时会静默丢弃 input(commit-tree -F - 会收到空消息)。
115
+ const childStdio = stdio || (input === undefined ? ['ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'])
111
116
  // 单次尝试:返回 { value, errorObj }(errorObj 为原始 Error 或 null),不在内部格式化 message。
112
117
  // 格式化(取首行)推迟到最终返回,避免重试分支重复格式化,并保留原始 code 供重试判定。
113
118
  const attempt = (t) => {
114
119
  try {
115
- let value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout: t, maxBuffer: GIT_MAX_BUFFER, stdio: ['ignore', 'pipe', 'pipe'], env: gitChildEnv(), windowsHide: true })
120
+ let value = execFileSync('git', fullArgs, {
121
+ encoding: 'utf8', timeout: t, maxBuffer: GIT_MAX_BUFFER, stdio: childStdio,
122
+ ...(input === undefined ? {} : { input }), env: gitChildEnv(), windowsHide: true,
123
+ })
116
124
  if (trim) value = value.trim()
117
125
  return { value, errorObj: null }
118
126
  } catch (e) {
@@ -131,18 +139,26 @@ export function safeGit(cwd, args, opts = {}) {
131
139
  * 调用方自行 catch;不经 shell,参数以数组元素传递。
132
140
  * @param {string} cwd
133
141
  * @param {string[]} args
134
- * @param {{ trim?: boolean, timeout?: number, encoding?: string }} [opts]
142
+ * @param {{ trim?: boolean, timeout?: number, encoding?: string, env?: NodeJS.ProcessEnv, input?: string|Buffer, stdio?: string[] }} [opts]
135
143
  * - encoding:'buffer' 返回原始 Buffer(git diff --binary 等**二进制输出**专用——
136
144
  * utf8 解码会破坏 NUL 字节使补丁不可应用;Buffer 输出跳过 trim)
145
+ * - input: 写入子进程 stdin(如 commit-tree -F - 的多行消息);传入时自动把 stdio[0] 设为 pipe
146
+ * - stdio: 显式覆盖子进程 stdio(push 需要实时透传 hook 输出时用 inherit)
137
147
  * @returns {string|Buffer}
138
148
  */
139
149
  export function git(cwd, args, opts = {}) {
140
- const { trim = true, timeout = 5000, encoding = 'utf8', env } = opts
150
+ const { trim = true, timeout = 5000, encoding = 'utf8', env, input, stdio = null } = opts
141
151
  const fullArgs = buildFullArgs(cwd, args)
142
152
  // env(终批-③):可选注入(如 baseline checkpoint 的临时 GIT identity)——调用方必须
143
153
  // 用 { ...process.env, GIT_xxx } 展开形式,裸替换会丢 SystemRoot/USERPROFILE/TEMP(Windows);
144
154
  // 最终经 gitChildEnv 展开合并并权威注入 GIT_OPTIONAL_LOCKS=0(调用方 GIT 键照常透传)
145
- const value = execFileSync('git', fullArgs, { encoding, timeout, maxBuffer: GIT_MAX_BUFFER, stdio: ['ignore', 'pipe', 'pipe'], env: gitChildEnv(env), windowsHide: true })
155
+ // stdio:input 传入时 stdin 必须 pipe——Node 在 stdio[0]='ignore' 下会静默丢弃 input
156
+ // (commit-tree -F - 收到空消息);push 用 inherit 透传 pre-push hook 输出。
157
+ const childStdio = stdio || (input === undefined ? ['ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'])
158
+ const value = execFileSync('git', fullArgs, {
159
+ encoding, timeout, maxBuffer: GIT_MAX_BUFFER, stdio: childStdio,
160
+ ...(input === undefined ? {} : { input }), env: gitChildEnv(env), windowsHide: true,
161
+ })
146
162
  if (Buffer.isBuffer(value)) return value
147
163
  return trim ? value.trim() : value
148
164
  }
package/src/index.js CHANGED
@@ -119,7 +119,7 @@ SillySpec CLI — 规范驱动开发工具包
119
119
  sillyspec fourpiece-init --change <name> 生成 proposal/requirements/decisions 骨架(frontmatter+章节+模板占位;已存在不覆盖)
120
120
  sillyspec delta --change <name> [--json] 生成变更 delta.md(Before/Delta/After 三段式聚合;幂等覆盖重跑即刷新)
121
121
  sillyspec next 项目状态探测:输出当前状态 + 下一步命令 + 依据(吸收 continue/resume 手工探测表)
122
- sillyspec commit [--json] 智能提交建议:收集 QUICKLOG/已勾 task/阶段产出语义,生成建议 message(只建议不执行)
122
+ sillyspec commit [--json] 智能提交建议:收集 QUICKLOG/已勾 task/阶段产出语义,生成建议 message(只建议不执行);sillyspec push [--dry-run] [--mode by-change|one|none] [--change <名>] [--no-push] [--undo] [--json] 推送前压扁本地未推送提交并快进推送(默认按变更分段;不改写已推送历史、不 force)
123
123
  sillyspec verify-probes --change <name> [--init [--force]] verify 机械探针(TODO 标记/测试覆盖/API 对账/删除对账);--init 生成 verify-result.md 骨架(--force 覆盖重生成,手填内容会重置)
124
124
  sillyspec module-impact --change <name> 生成 module-impact.md 骨架(文件×模块归属按 module-map 预填 + 未匹配清单)
125
125
  sillyspec scope-audit --change <name> [--json] 变更范围对账:计划×实际三态全表 + 行数(✓ 计划内/⚠️ 计划外/⚠️ 计划未动;quick 会话传 quick-<id> 出归属表;跨仓条目按 local.yaml repos 分仓真实对账——--json 出 repos[] 仓库维度汇总;已归档变更可查——快照记录态;advisory 只读不设门禁)
@@ -458,7 +458,7 @@ async function main() {
458
458
  // 1-2 个交互来回)。守卫建议的事(cd 回主仓根重跑)自动做掉:⚠️警告 + 重锚定主仓根执行——
459
459
  // 写入恒落主仓(单源不变量延续,worktree 副本零 CLI 写入);刻意以本 worktree 作独立实例跑
460
460
  // 的显式路径(--allow-worktree-cwd --spec-dir)在警告中原样保留;wt-commit 豁免不变。
461
- if (!args.includes('--allow-worktree-cwd') && command !== 'wt-commit') {
461
+ if (!args.includes('--allow-worktree-cwd') && command !== 'wt-commit' && command !== 'push') {
462
462
  const guardDir = args.includes('--dir') ? dir : process.cwd();
463
463
  const wtInfo = detectCwdInsideWorktree(guardDir);
464
464
  if (wtInfo) {
@@ -5774,8 +5774,39 @@ SillySpec pull — 拉取服务器 spec 快照到本地(X2 / FR-07)
5774
5774
  printed = wAll ? wRes.events.length : wRes.warnings.length;
5775
5775
  }
5776
5776
  }
5777
+ case 'push': {
5778
+ // 推送前压扁(2026-10-23-push-squash):显式命令,不接 pre-push hook 自动改写。
5779
+ // 默认 by-change,只处理 base..HEAD 本地未推送段;commit-tree + update-ref 保 index/工作区
5780
+ // 零变化,pre/post 备份 ref 可 undo;绝不 force / 不 --no-verify。--json 已由全局层剥离。
5781
+ // 放在 switch 末尾:避免插行造成 docs 里 index.js:N 的行号引用整体漂移。
5782
+ const { parsePushArgs, runPush, PUSH_USAGE } = await import('./push.js');
5783
+ const parsed = parsePushArgs(filteredArgs.slice(1));
5784
+ if (!parsed.ok) {
5785
+ for (const e of parsed.errors) console.error(`❌ ${e}`);
5786
+ console.error(PUSH_USAGE);
5787
+ process.exit(2);
5788
+ }
5789
+ if (parsed.options.help) {
5790
+ console.log(PUSH_USAGE);
5791
+ break;
5792
+ }
5793
+ const r = await runPush({ cwd: dir, specDir, ...parsed.options, json });
5794
+ if (json) {
5795
+ console.log(JSON.stringify(r.envelope, null, 2));
5796
+ for (const w of r.warnings || []) console.error(`⚠️ ${w}`);
5797
+ process.exitCode = r.exitCode;
5798
+ break;
5799
+ }
5800
+ for (const line of r.humanLines || []) console.log(line);
5801
+ for (const w of r.warnings || []) console.warn(`⚠️ ${w}`);
5802
+ if (!r.ok) {
5803
+ for (const e of r.errors || []) console.error(`❌ ${e}`);
5804
+ process.exitCode = r.exitCode || 1;
5805
+ }
5806
+ break;
5807
+ }
5777
5808
  default: {
5778
- const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'agent-log', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'change-delete', 'knowledge', 'platform', 'pull', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime', 'watcher'];
5809
+ const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'agent-log', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'change-delete', 'knowledge', 'platform', 'pull', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime', 'watcher', 'commit', 'push'];
5779
5810
  const suggestion = didYouMean(command, topCommands);
5780
5811
  console.error(`❌ 未知命令: ${command}`);
5781
5812
  if (command === '--status') {
@@ -14,16 +14,92 @@
14
14
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'fs'
15
15
  import { runtimeStatePath } from './runtime-paths.js'
16
16
  import { join } from 'path'
17
+ import { readPlatformPushConfig } from './agent-session-log.js'
17
18
 
18
19
  const HITS_FILENAME = 'knowledge-hits.jsonl'
19
20
 
21
+ // 直报超时上限(2026-10-28-knowledge-hits-cli-push):对齐 agent-log PUSH_TIMEOUT_MS
22
+ // 量级——最坏情况下进程退出被活跃 socket 拖住的时长有界。
23
+ const HITS_PUSH_TIMEOUT_MS = 10_000
24
+
20
25
  function hitsFilePath(runtimeDir) {
21
26
  return runtimeStatePath(runtimeDir, HITS_FILENAME)
22
27
  }
23
28
 
29
+ /**
30
+ * runtimeDir → specBase 反推(剥尾 `.runtime` 段;跨平台:正反斜杠与尾分隔符容忍)。
31
+ *
32
+ * 全部 appendKnowledgeHit 调用形态的 runtimeDir 最终都是 `<specBase>/.runtime`
33
+ * (join(specBase,'.runtime') 或 resolveRuntimeRoot 产物)——统一剥最后一段反推
34
+ * specBase 供平台配置读取(local.yaml 在 specBase 根)。结果保留输入的分隔符
35
+ * 形态(不做平台归一——Node fs 对 / 与 \ 双向兼容,归一反而扭曲 POSIX 形态输入)。
36
+ *
37
+ * @param {string} runtimeDir
38
+ * @returns {string} specBase(runtimeDir 末段不是 .runtime 时原样返回——容错面)
39
+ */
40
+ export function specBaseFromRuntimeDir(runtimeDir) {
41
+ const raw = String(runtimeDir)
42
+ const norm = raw.replace(/\\/g, '/').replace(/\/+$/, '')
43
+ const idx = norm.lastIndexOf('/')
44
+ const last = idx === -1 ? norm : norm.slice(idx + 1)
45
+ if (last !== '.runtime') return runtimeDir
46
+ // \→/ 等长替换、去尾 / 只动末尾——norm 前 idx 字符与原串位置一一对应,
47
+ // 直接按剥点切片即保留原分隔符形态的 specBase。
48
+ return raw.slice(0, idx)
49
+ }
50
+
51
+ /**
52
+ * 单行遥测直报平台(2026-10-28-knowledge-hits-cli-push FR-01)。
53
+ *
54
+ * POST {url}/api/changes/-/hits-sync(Bearer shpsync_ 令牌——workspace 由令牌派生),
55
+ * body {lines:[line], daemon_local_id:'cli'};接收端行级幂等(行 sha256 去重,
56
+ * 平台仓 2026-10-28-knowledge-hits-cli-direct)。best-effort 语义照
57
+ * pushAgentLogToPlatform 先例:无配置/被禁用静默跳过(返回 pushed:null);
58
+ * 网络/非 2xx/超时 warn 一行不抛(返回 pushed:false, reason)——本地行已落盘、
59
+ * daemon 周期上行与平台按钮 flush 为兜底通道。
60
+ *
61
+ * @param {object} p
62
+ * @param {string} p.specBase - local.yaml 所在 spec 基路径
63
+ * @param {object} [p.env] - env(默认 process.env;测试注入用)
64
+ * @param {string} p.line - 已落盘的遥测行(JSON 字符串)
65
+ * @returns {Promise<{pushed:boolean|null, reason?:string}>}
66
+ */
67
+ export async function pushKnowledgeHitToPlatform({ specBase, env = process.env, line }) {
68
+ if (env.SILLYSPEC_HITS_PUSH === '0') {
69
+ return { pushed: null, reason: 'disabled' }
70
+ }
71
+ const cfg = readPlatformPushConfig(specBase, env)
72
+ if (!cfg) return { pushed: null, reason: 'no-config' }
73
+ const controller = new AbortController()
74
+ const timer = setTimeout(() => controller.abort(), HITS_PUSH_TIMEOUT_MS)
75
+ try {
76
+ const res = await fetch(`${cfg.url.replace(/\/+$/, '')}/api/changes/-/hits-sync`, {
77
+ method: 'POST',
78
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${cfg.token}` },
79
+ body: JSON.stringify({ lines: [line], daemon_local_id: 'cli' }),
80
+ signal: controller.signal,
81
+ })
82
+ if (!res.ok) {
83
+ console.warn(`[knowledge-hits-push] → HTTP ${res.status}(本地行已落盘,daemon 兜底)`)
84
+ return { pushed: false, reason: `http-${res.status}` }
85
+ }
86
+ return { pushed: true }
87
+ } catch (err) {
88
+ const msg = err && err.name === 'AbortError' ? '超时' : (err && err.message ? err.message : err)
89
+ console.warn(`[knowledge-hits-push] 直报失败: ${msg}(本地行已落盘,daemon 兜底)`)
90
+ return { pushed: false, reason: 'network' }
91
+ } finally {
92
+ clearTimeout(timer)
93
+ }
94
+ }
95
+
24
96
  /**
25
97
  * 追加一条遥测记录(目录/文件缺失自动创建)。
26
98
  *
99
+ * 2026-10-28-knowledge-hits-cli-push:落盘成功后 void fire 平台直报(FR-01/02)——
100
+ * 不 await 不抛(失败 warn 一行由 pushKnowledgeHitToPlatform 内部收口),主流程
101
+ * 同步语义与返回值不变;无配置/被禁用时零行为差异。
102
+ *
27
103
  * @param {string} runtimeDir - .runtime 目录
28
104
  * @param {object} hit - 记录对象,常用字段 { type:'inject'|'classify', change, query, matchedFiles, at }
29
105
  * (at 缺省时补当前 ISO 时间;其余字段原样序列化,classify 的 qlId/targetFile 等照写)
@@ -35,7 +111,10 @@ export function appendKnowledgeHit(runtimeDir, hit) {
35
111
  record.at = new Date().toISOString()
36
112
  }
37
113
  mkdirSync(runtimeDir, { recursive: true })
38
- appendFileSync(hitsFilePath(runtimeDir), JSON.stringify(record) + '\n', 'utf8')
114
+ const line = JSON.stringify(record)
115
+ appendFileSync(hitsFilePath(runtimeDir), line + '\n', 'utf8')
116
+ // 写后直报(void fire):本地已落盘,直报成败不影响本函数语义(FR-02)。
117
+ void pushKnowledgeHitToPlatform({ specBase: specBaseFromRuntimeDir(runtimeDir), line }).catch(() => {})
39
118
  return record
40
119
  }
41
120
 
@@ -0,0 +1,82 @@
1
+ /**
2
+ * knowledge-scope.js — 知识注入作用域解析(2026-10-23-knowledge-scope-inject)
3
+ *
4
+ * 背景:知识注入此前只有「触达域 FR + 关键词命中」两类——AGENTS 规约 00(不枚举定义
5
+ * 开放世界/不锁死开发语言框架)一直在指令文件里,但决策时点没有「每次都注入」通道,
6
+ * agent 在配置/机制设计上犯语言锁定即实证(py_runner_flags 一案)。本模块让知识条目
7
+ * 声明作用域,注入逻辑按类分流:
8
+ * - always:每次变更都注入(跨仓通用规约,如工具中立性)
9
+ * - repo:本仓库每次变更都注入(仓级约束)
10
+ * - module:<域>[,<域>]:仅当触达域(resolveTouchedDomains 同源)命中时注入
11
+ * - 无标记:关键词命中现状(不恒注入)
12
+ *
13
+ * 标记形态(HTML 注释,主观内容用 HTML 注释不污染渲染;置于 section heading 之后的
14
+ * 本节内任意位置有效):
15
+ * <!-- scope: always -->
16
+ * <!-- scope: repo -->
17
+ * <!-- scope: module:cli-entry,core-engine -->
18
+ *
19
+ * 条目粒度 = section(heading)——knowledge 的 INDEX 路由与命中面都以 file#heading 为
20
+ * 单位,标记跟随原文、旧知识零迁移。纯读 fail-soft(坏文件/不可读跳过,异常返回空态)。
21
+ */
22
+ import { readdirSync, readFileSync } from 'fs'
23
+ import { join } from 'path'
24
+
25
+ const SCOPE_RE = /<!--\s*scope\s*:\s*(always|repo|module\s*:\s*([^>]+?))\s*-->/i
26
+
27
+ /**
28
+ * 扫描 knowledgeRoot 顶层 *.md(INDEX.md 除外)收集作用域条目。
29
+ * @returns {{ always: Array, repo: Array, modules: Record<string, Array> }}
30
+ * 条目形如 { id: 'file.md#heading', file, heading, summary, scope, domains? }
31
+ */
32
+ export function collectScopedKnowledge(knowledgeRoot) {
33
+ const out = { always: [], repo: [], modules: {} }
34
+ let entries = []
35
+ try { entries = readdirSync(knowledgeRoot) } catch { return out }
36
+ for (const f of entries) {
37
+ if (!String(f).endsWith('.md') || f === 'INDEX.md') continue
38
+ let text
39
+ try { text = readFileSync(join(knowledgeRoot, f), 'utf8') } catch { continue }
40
+ let cur = null // { heading, scope, domains, summary }
41
+ const flush = () => {
42
+ if (!cur || !cur.scope) return
43
+ const entry = {
44
+ id: `${f}#${cur.heading}`,
45
+ file: f,
46
+ heading: cur.heading,
47
+ summary: cur.summary || '',
48
+ scope: cur.scope,
49
+ ...(cur.scope === 'module' ? { domains: cur.domains } : {}),
50
+ }
51
+ if (cur.scope === 'always') out.always.push(entry)
52
+ else if (cur.scope === 'repo') out.repo.push(entry)
53
+ else for (const d of cur.domains) (out.modules[d] ||= []).push(entry)
54
+ }
55
+ for (const line of String(text).split(/\r?\n/)) {
56
+ const h = /^(#{1,6})\s+(.+?)\s*$/.exec(line)
57
+ if (h) { flush(); cur = { heading: h[2].trim(), scope: null, domains: [], summary: '' }; continue }
58
+ if (!cur) continue
59
+ const m = SCOPE_RE.exec(line)
60
+ if (m) {
61
+ const kind = m[1].toLowerCase()
62
+ if (kind === 'always' || kind === 'repo') { cur.scope = kind; cur.domains = [] }
63
+ else {
64
+ cur.scope = 'module'
65
+ cur.domains = String(m[2] || '').split(',').map((s) => s.trim()).filter(Boolean)
66
+ }
67
+ continue
68
+ }
69
+ // 首行有效正文作摘要(跳空行/注释/代码围栏),≤80 字
70
+ if (!cur.summary) {
71
+ const t = line.trim()
72
+ if (t && !t.startsWith('<!--') && !t.startsWith('```') && !t.startsWith('|')) {
73
+ cur.summary = t.slice(0, 80)
74
+ }
75
+ }
76
+ }
77
+ flush()
78
+ }
79
+ return out
80
+ }
81
+
82
+ export default { collectScopedKnowledge }
@@ -513,6 +513,9 @@ export class ChangeRegistry {
513
513
  * @param {string} cwd - 项目根目录
514
514
  * @param {string} oldName - 旧变更名
515
515
  * @param {string} newName - 新变更名
516
+ *
517
+ * 成功后旧名落删除台账 status='deleted'(2026-10-23-rename-platform-tombstone):
518
+ * 平台按 change_key 建行,旧平台名行靠 sync 台账墓碑分支收敛,防幽灵永挂。
516
519
  */
517
520
  renameChange(cwd, oldName, newName) {
518
521
  if (!oldName || !newName) {
@@ -588,7 +591,18 @@ export class ChangeRegistry {
588
591
  } else {
589
592
  mkdirSync(newDir, { recursive: true });
590
593
  }
591
- if (renamed) console.log(`✅ 变更已重命名:${oldName} → ${newName}`);
594
+ if (renamed) {
595
+ console.log(`✅ 变更已重命名:${oldName} → ${newName}`);
596
+ // 平台旧名墓碑凭证(2026-10-23-rename-platform-tombstone,坑9 幽灵家族第三变种):
597
+ // 平台按 change_key 建行而 rename 只改本地,旧平台名行无终态凭证会永挂 active
598
+ // (2026-10-23-new-change-43a74c51 实证:自动建名 16 秒后被改名,平台幽灵)。
599
+ // 旧名对平台即终态弃名,语义用既有 'deleted' 承载(平台 _apply_cli_tombstone 只认
600
+ // deleted/archived,不扩表面);daemon reconcile / 显式 platform sync --change 旧名
601
+ // 时由 sync 台账分支合成墓碑收敛。仅 renamed(DB+目录双成)落账:missing/conflict/
602
+ // stolen/FS 回滚分支到此为止(行还叫旧名时不得有其终态台账)。fail-open 同删除/归档。
603
+ appendDeletedLedger(this.pm._getSpecDir(cwd), oldName, 'deleted');
604
+ console.log('ℹ️ 旧名已记删除台账(status=deleted),平台旧名行将由墓碑同步收敛');
605
+ }
592
606
  }
593
607
 
594
608
  /**