@mingmaple/dsh-handover 0.9.22 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +117 -113
  2. package/docs/migrate.md +52 -41
  3. package/docs/releases/0.9.12.md +2 -3
  4. package/docs/releases/0.9.14.md +1 -5
  5. package/docs/releases/0.9.22.md +1 -1
  6. package/docs/releases/1.0.0.md +33 -0
  7. package/docs/troubleshoot.md +221 -61
  8. package/package.json +5 -2
  9. package/scripts/behavior.mjs +398 -0
  10. package/scripts/blank-dsh.mjs +12 -2
  11. package/scripts/blank-smoke.mjs +95 -44
  12. package/scripts/check-docs.mjs +7 -0
  13. package/scripts/portability.mjs +71 -0
  14. package/scripts/style-gate.mjs +571 -0
  15. package/src/client.js +1028 -994
  16. package/src/index.mjs +273 -109
  17. package/src/lib/absrefs.mjs +1 -1
  18. package/src/lib/api.mjs +9 -3
  19. package/src/lib/archive.mjs +1 -1
  20. package/src/lib/brief.mjs +8 -6
  21. package/src/lib/config.mjs +21 -6
  22. package/src/lib/docmap.mjs +5 -8
  23. package/src/lib/drift.mjs +1 -1
  24. package/src/lib/events.mjs +1 -1
  25. package/src/lib/facts.mjs +19 -12
  26. package/src/lib/gates.mjs +12 -12
  27. package/src/lib/global-task.mjs +69 -0
  28. package/src/lib/guide.mjs +80 -75
  29. package/src/lib/handover-io.mjs +69 -6
  30. package/src/lib/identity.mjs +330 -0
  31. package/src/lib/legacy.mjs +89 -0
  32. package/src/lib/lint.mjs +14 -11
  33. package/src/lib/manual.mjs +117 -80
  34. package/src/lib/mover.mjs +18 -14
  35. package/src/lib/office.mjs +42 -2
  36. package/src/lib/pack-ops.mjs +10 -10
  37. package/src/lib/pathkey.mjs +24 -0
  38. package/src/lib/phase.mjs +8 -13
  39. package/src/lib/profile.mjs +1 -1
  40. package/src/lib/reminder.mjs +110 -0
  41. package/src/lib/role.mjs +1 -1
  42. package/src/lib/scaffold.mjs +30 -10
  43. package/src/lib/schedule.mjs +5 -5
  44. package/src/lib/session-create.mjs +2 -2
  45. package/src/lib/session-ops.mjs +76 -47
  46. package/src/lib/sessions.mjs +40 -2
  47. package/src/lib/sevenzip.mjs +10 -1
  48. package/src/lib/status.mjs +337 -336
  49. package/src/lib/succession.mjs +14 -14
  50. package/src/lib/system.mjs +364 -0
  51. package/src/templates/Changelog.skeleton.md +3 -3
  52. package/src/templates/Charter.skeleton.md +13 -9
  53. package/src/templates/Handover.global.skeleton.md +20 -13
  54. package/src/templates/Handover.template.md +89 -0
  55. package/src/templates/Plan.skeleton.md +34 -0
  56. package/src/templates/Prompt_Templates.md +91 -10
  57. package/src/templates/Runtime_Manual.md +254 -139
  58. package/src/templates/System.skeleton.md +31 -0
  59. package/src/templates/Systems.global.skeleton.md +13 -0
  60. package/src/templates/rules/Delegation_Rules.md +45 -0
  61. package/src/templates/rules/Handover_Rules.md +46 -24
  62. package/src/templates/rules/Migration_Rules.md +7 -7
  63. package/src/templates/rules/Misc_Task_Standard.md +17 -10
  64. package/src/templates/rules/Writing_Rules.md +33 -16
  65. package/src/templates/standards/README.md +19 -7
  66. package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203/README.md +120 -0
  67. package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203//344/276/235/346/215/256_/350/256/272/346/226/207.md +389 -0
  68. package/src/tools/ho_backup.mjs +8 -8
  69. package/src/tools/ho_changelog.mjs +4 -4
  70. package/src/tools/ho_charter.mjs +7 -6
  71. package/src/tools/ho_checkpoint.mjs +34 -31
  72. package/src/tools/ho_docmap.mjs +135 -49
  73. package/src/tools/ho_gate.mjs +9 -4
  74. package/src/tools/ho_handover.mjs +61 -35
  75. package/src/tools/ho_office.mjs +45 -4
  76. package/src/tools/ho_profile.mjs +3 -3
  77. package/src/tools/ho_sessions.mjs +43 -22
  78. package/src/tools/ho_status.mjs +14 -9
  79. package/src/tools/ho_succeed.mjs +3 -3
  80. package/src/tools/ho_workspace.mjs +83 -51
  81. package/src/tools/peer.mjs +219 -8
@@ -100,7 +100,7 @@ function appendSuccessionBlock(file, lines) {
100
100
  export async function runSuccession(env, exec, opts) {
101
101
  const o = opts || {};
102
102
  const note = typeof o.note === 'string' ? o.note.trim() : '';
103
- const reject = (reason) => '换任未执行,未做任何写入。' + NL + reason;
103
+ const reject = (reason) => '换任未执行,未写入任何内容。' + NL + reason;
104
104
 
105
105
  // 1. 自身角色与类别:只认已登记的角色。
106
106
  const info = env.roleOf(exec) || {};
@@ -120,8 +120,8 @@ export async function runSuccession(env, exec, opts) {
120
120
  return reject([
121
121
  cs.reason + '。',
122
122
  '授权方式(任选其一):',
123
- ' ① 在聊天框发送 /allow-succeed(最快,仅授权本会话);',
124
- ' ② 在设置页的 dsh-handover 中,点击「授权」里的「授权换任」。',
123
+ ' ① 在聊天框发送 /allow-succeed(仅授权本会话,最为快捷);',
124
+ ' ② 在设置页的 dsh-handover 中,点击「授权」内的「授权换任」。',
125
125
  '授权有效期 5 分钟,用后失效。',
126
126
  ].join(NL));
127
127
  }
@@ -129,7 +129,7 @@ export async function runSuccession(env, exec, opts) {
129
129
  // 3. 交接结构检查(只判结构,不判质量)。
130
130
  const hp = env.handoverFor(exec) || {};
131
131
  const handoverFile = hp.path ? String(hp.path) : '';
132
- if (!handoverFile) return reject('没有交接路径:' + (hp.reason || '本会话角色未确定'));
132
+ if (!handoverFile) return reject('没有可用的交接路径:' + (hp.reason || '本会话角色未确定'));
133
133
  const problems = auditHandover(handoverFile);
134
134
  if (problems.length) {
135
135
  return reject(['交接结构未通过:' + handoverFile]
@@ -145,10 +145,10 @@ export async function runSuccession(env, exec, opts) {
145
145
  const lin = store.lineages[key];
146
146
  // 只有本类别的在任者本人可换任:既防旧会话复活,也防同项目的旁支会话抢先。
147
147
  if (!lin || !lin.current || !lin.current.sessionId) {
148
- return reject('本类别(' + key + ')在在任登记表里还没有现任记录:先由全局建会话并登记,再换任。');
148
+ return reject('本类别(' + key + ')在在任登记表中尚无在任记录:先由全局建立会话并登记,再换任。');
149
149
  }
150
150
  if (String(lin.current.sessionId) !== String(selfId)) {
151
- return reject('本会话不是在任者:本类别现任为 ' + String(lin.current.name || '(未命名)')
151
+ return reject('本会话不是在任者:本类别在任为 ' + String(lin.current.name || '(未命名)')
152
152
  + '(' + String(lin.current.sessionId) + '),不得换任。');
153
153
  }
154
154
  {
@@ -156,17 +156,17 @@ export async function runSuccession(env, exec, opts) {
156
156
  const closed = history.find((h) => h && String(h.sessionId || '') === String(selfId) && h.to);
157
157
  if (closed) {
158
158
  const curName = lin.current && lin.current.name ? lin.current.name : '(空缺)';
159
- return reject('本类别已为本会话记过一次换任(' + String(closed.to || '') + ',现任 ' + curName + '),旧会话不得再次换任。');
159
+ return reject('本类别已为本会话记过一次换任(' + String(closed.to || '') + ',在任 ' + curName + '),旧会话不得再次换任。');
160
160
  }
161
161
  }
162
162
 
163
- // 4.1 水位未到线:本会话就是在任者且水位低于提醒线时,换任通常是多余的。
163
+ // 4.1 水位未到线:本会话就是在任者且水位低于提醒线时,换任通常多余。
164
164
  // 放在授权与交接校验之后、任何写入之前;取不到水位就跳过。
165
165
  if (o.force !== true) {
166
166
  let pct = null;
167
167
  try { pct = typeof env.selfPct === 'function' ? env.selfPct(selfId) : null; } catch { pct = null; }
168
- if (pct !== null && pct !== undefined && Number(pct) < watermark(env.cfg).warnValue) {
169
- return reject('本会话水位 ' + Number(pct).toFixed(1) + '%,尚未到线,通常无需换任;确需换任请传 force=true。');
168
+ if (pct !== null && pct !== undefined && Number(pct) < watermark(env.cfg).remindValue) {
169
+ return reject('本会话水位 ' + Number(pct).toFixed(1) + '%,未到提醒线,通常无需换任;确需换任请传 force=true。');
170
170
  }
171
171
  }
172
172
 
@@ -195,7 +195,7 @@ export async function runSuccession(env, exec, opts) {
195
195
  const base = promptFor(env, { ws, kind, project, task, title: name, sessionId: '', globalSessionId: globalId, extra: '' });
196
196
  prompt = base + NL + NL + [
197
197
  '接任交接:' + handoverFile,
198
- '先读这份交接再动手;读毕向当前用户报告当前停在哪一步、交接列有哪些待办,然后等待当前用户指派任务。',
198
+ '先读这份交接,再开始作业。读毕向当前用户报告:当前停在哪一步;交接列有哪些待办。随后等待当前用户指派任务。',
199
199
  ].join(NL);
200
200
  } catch (e) {
201
201
  return reject('启动提示词生成失败:' + msgOf(e));
@@ -215,7 +215,7 @@ export async function runSuccession(env, exec, opts) {
215
215
  } catch (e) {
216
216
  return [
217
217
  '会话已建 ' + sessionId + '(' + name + '),但登记在任失败:' + msgOf(e),
218
- '启动提示词尚未投递;先修好登记表,再用 peer(action="send")通知它。',
218
+ '启动提示词尚未投递。先修好登记表,再用 peer(action="send")通知该会话。',
219
219
  ].join(NL);
220
220
  }
221
221
 
@@ -253,8 +253,8 @@ export async function runSuccession(env, exec, opts) {
253
253
  out.push('换任完成:' + name + '(' + sessionId + ')');
254
254
  out.push('在任登记:' + key + (reg && reg.registry && reg.registry.changed && reg.registry.changed.length
255
255
  ? ',Registry.md 已同步在役列' : ''));
256
- out.push('启动提示词:' + (delivered ? '已投递' : '未送达:' + deliveryError + '。会话已建,可用 peer(action="send")补投'));
256
+ out.push('启动提示词:' + (delivered ? '已投递' : '未送达:' + deliveryError + '。会话已建,可用 peer(action="send")补投。'));
257
257
  out.push('交接登记:' + appendNote);
258
- out.push('本会话到此停止,等待当前用户在新会话中派任务。');
258
+ out.push('本会话到此停止,等待当前用户在新会话中指派任务。');
259
259
  return out.join(NL);
260
260
  }
@@ -0,0 +1,364 @@
1
+ /**
2
+ * 体系注册与文档定位(1.0):每个任务一份注册文件,写目录结构、规范文件与计划;
3
+ * 全局总注册只列各任务体系文件的路径,不承载正文。
4
+ *
5
+ * 路径约定:
6
+ * - 全局总注册:_Control_Center/records/systems.md
7
+ * - 项目:_Project_Meta/<项目>/System.md
8
+ * - 杂项:_Misc/tasks/<任务>/System.md
9
+ * - 全局任务:_Control_Center/tasks/<任务>/System.md
10
+ *
11
+ * 定位:注册只给路径与用途,是否读正文由会话判断;空注册不注入正文,只注入一句提示。
12
+ */
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { NL, readUtf8, isFile, isDir } from './text.mjs';
16
+ import { projectNameOk, taskNameOk } from './role.mjs';
17
+ import { readProfile } from './profile.mjs';
18
+
19
+ export const GLOBAL_REGISTRY_REL = path.join('_Control_Center', 'records', 'systems.md');
20
+ export const REGISTRY_NAME = 'System.md';
21
+
22
+ const TICK = String.fromCharCode(96);
23
+
24
+ export function lineageOf(info) {
25
+ const role = info && info.role ? info.role : '';
26
+ if (role === 'global') return { kind: 'global', project: '', task: '', key: '全局' };
27
+ if (role === 'project' && info.project) return { kind: 'project', project: String(info.project), task: '', key: String(info.project) };
28
+ if (role === 'misc' && info.task) return { kind: 'misc', project: '', task: String(info.task), key: String(info.task) };
29
+ if (role === 'global-task' && info.task) return { kind: 'global-task', project: '', task: String(info.task), key: String(info.task) };
30
+ return null;
31
+ }
32
+
33
+ export function lineageLabel(info) {
34
+ const lin = lineageOf(info);
35
+ if (!lin) return '';
36
+ if (lin.kind === 'project') return '项目「' + lin.project + '」';
37
+ if (lin.kind === 'misc') return '杂项任务「' + lin.task + '」';
38
+ if (lin.kind === 'global-task') return '全局任务「' + lin.task + '」';
39
+ return '全局';
40
+ }
41
+
42
+ /** 任务库根(相对工作区)。项目优先取骨架档案登记的根,其次 `<项目>库`,最后项目目录。 */
43
+ export function taskRoot(ws, info) {
44
+ const lin = lineageOf(info);
45
+ if (!lin) return '';
46
+ if (lin.kind === 'project') {
47
+ const p = lin.project;
48
+ try {
49
+ const pf = readProfile(ws, { role: 'project', project: p });
50
+ if (pf && pf.exists && pf.profile && pf.profile.root) return String(pf.profile.root);
51
+ } catch { /* 骨架读不到时按目录规则推断 */ }
52
+ const lib = path.join('Projects', p, p + '库');
53
+ return isDir(path.join(String(ws || ''), lib)) ? lib : path.join('Projects', p);
54
+ }
55
+ if (lin.kind === 'misc') return path.join('_Misc', 'tasks', lin.task);
56
+ if (lin.kind === 'global-task') return path.join('_Control_Center', 'tasks', lin.task);
57
+ return '_Control_Center';
58
+ }
59
+
60
+ /** 注册文件相对路径。 */
61
+ export function registryRelPath(info) {
62
+ const lin = lineageOf(info);
63
+ if (!lin) return null;
64
+ if (lin.kind === 'global') return GLOBAL_REGISTRY_REL;
65
+ if (lin.kind === 'project') return path.join('_Project_Meta', lin.project, REGISTRY_NAME);
66
+ if (lin.kind === 'misc') return path.join('_Misc', 'tasks', lin.task, REGISTRY_NAME);
67
+ if (lin.kind === 'global-task') return path.join('_Control_Center', 'tasks', lin.task, REGISTRY_NAME);
68
+ return null;
69
+ }
70
+
71
+ export function registryAbsPath(ws, info) {
72
+ const rel = registryRelPath(info);
73
+ return rel ? path.join(String(ws || ''), rel) : null;
74
+ }
75
+
76
+ export function planRelPath(ws, info) {
77
+ const root = taskRoot(ws, info);
78
+ return root ? path.join(root, 'PLAN.md') : '';
79
+ }
80
+
81
+ function normRel(p) {
82
+ return String(p || '').replace(/\\/g, '/').replace(/^\.\//, '');
83
+ }
84
+
85
+ function cellsOf(line) {
86
+ return line.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.replace(new RegExp(TICK, 'g'), '').trim());
87
+ }
88
+
89
+ /** 解析任务级注册:返回 { title, structure: [{path, about}], files: [{path, about, when}], plan }。 */
90
+ export function parseRegistry(text) {
91
+ const out = { title: '', structure: [], files: [], plan: '' };
92
+ let section = '';
93
+ for (const raw of String(text || '').split(/\r?\n/)) {
94
+ const line = raw.trim();
95
+ if (line.startsWith('# ')) { out.title = line.slice(2).trim(); continue; }
96
+ if (line.startsWith('## ')) { section = line.slice(3).trim(); continue; }
97
+ if (section.indexOf('计划') !== -1) {
98
+ const m = /^[-*]\s*`?([^`::]+\.md)`?/.exec(line);
99
+ if (m) out.plan = m[1].trim();
100
+ continue;
101
+ }
102
+ if (!line.startsWith('|')) continue;
103
+ const cells = cellsOf(line);
104
+ if (cells.length < 2) continue;
105
+ if (cells[0] === '路径') continue;
106
+ if (cells.every((c) => /^[-:\s]*$/.test(c))) continue;
107
+ if (section.indexOf('目录') !== -1) out.structure.push({ path: cells[0], about: cells[1] || '' });
108
+ else if (section.indexOf('规范') !== -1 || section.indexOf('文件') !== -1) {
109
+ out.files.push({ path: cells[0], about: cells[1] || '', when: cells[2] || '' });
110
+ }
111
+ }
112
+ return out;
113
+ }
114
+
115
+ /** 渲染任务级注册文件。 */
116
+ export function renderRegistry(info, data) {
117
+ const label = lineageLabel(info);
118
+ const lines = ['# 体系注册(' + label + ')', '', '## 目录结构', '', '| 路径 | 用途 |', '|---|---|'];
119
+ for (const e of data.structure || []) lines.push('| ' + wrap(e.path) + ' | ' + e.about + ' |');
120
+ lines.push('', '## 规范文件', '', '| 路径 | 用途 | 何时读 |', '|---|---|---|');
121
+ for (const e of data.files || []) lines.push('| ' + wrap(e.path) + ' | ' + e.about + ' | ' + (e.when || '') + ' |');
122
+ lines.push('', '## 计划', '');
123
+ if (data.plan) lines.push('- ' + wrap(data.plan) + ':当前行动计划');
124
+ lines.push('');
125
+ lines.push('只给路径与用途:是否读正文由会话判断,不得无目的通读。');
126
+ return lines.join(NL) + NL;
127
+ }
128
+
129
+ function wrap(p) {
130
+ return TICK + normRel(p) + TICK;
131
+ }
132
+
133
+ /** 读取任务级注册;不存在返回 exists=false。 */
134
+ export function readRegistry(ws, info) {
135
+ const file = registryAbsPath(ws, info);
136
+ if (!file) return { file: null, exists: false, data: { structure: [], files: [], plan: '' } };
137
+ if (!isFile(file)) return { file, exists: false, data: { structure: [], files: [], plan: '' } };
138
+ let text = '';
139
+ try { text = readUtf8(file); } catch { return { file, exists: true, error: '读取失败', data: { structure: [], files: [], plan: '' } }; }
140
+ return { file, exists: true, data: parseRegistry(text), text };
141
+ }
142
+
143
+ /** 路径合法且存在:只收工作区相对路径。 */
144
+ export function pathProblem(ws, p) {
145
+ const raw = normRel(p);
146
+ if (!raw) return '路径为空';
147
+ if (/^[a-zA-Z]:/.test(raw) || raw.startsWith('/') || raw.startsWith('\\')) return '须写工作区相对路径,不得写绝对路径';
148
+ const norm = path.normalize(raw).replace(/\\/g, '/');
149
+ if (norm === '..' || norm.startsWith('../')) return '不得指向工作区之外';
150
+ const abs = path.join(String(ws || ''), norm);
151
+ if (!isFile(abs) && !isDir(abs)) return '不存在:' + norm;
152
+ return '';
153
+ }
154
+
155
+ export function validateEntries(ws, entries) {
156
+ const bad = [];
157
+ for (const e of entries) {
158
+ const problem = pathProblem(ws, e.path);
159
+ if (problem) bad.push({ path: e.path, problem });
160
+ }
161
+ return bad;
162
+ }
163
+
164
+ /** 解析全局总注册:返回 [{ task, kind, path }]。 */
165
+ export function parseGlobalRegistry(text) {
166
+ const rows = [];
167
+ for (const raw of String(text || '').split(/\r?\n/)) {
168
+ const line = raw.trim();
169
+ if (!line.startsWith('|')) continue;
170
+ const cells = cellsOf(line);
171
+ if (cells.length < 3) continue;
172
+ if (cells[0] === '任务') continue;
173
+ if (cells.every((c) => /^[-:\s]*$/.test(c))) continue;
174
+ rows.push({ task: cells[0], kind: cells[1] || '', path: cells[2] });
175
+ }
176
+ return rows;
177
+ }
178
+
179
+ const KIND_LABEL = { project: '项目', misc: '杂项', 'global-task': '全局任务' };
180
+
181
+ /** 扫描磁盘上的任务注册,重建全局总注册。返回 { rows, file, changed }。 */
182
+ export function syncGlobalRefs(ws) {
183
+ const root = String(ws || '');
184
+ const rows = [];
185
+ const pushFrom = (base, kind, nameOf) => {
186
+ let names = [];
187
+ try { names = fs.readdirSync(base); } catch { return; }
188
+ for (const name of names) {
189
+ const dir = path.join(base, name);
190
+ if (fs.existsSync(path.join(dir, '.deregistered.json'))) continue;
191
+ const file = path.join(dir, REGISTRY_NAME);
192
+ if (!isFile(file)) continue;
193
+ rows.push({ task: nameOf(name), kind: KIND_LABEL[kind] || kind, path: path.join(base, name, REGISTRY_NAME), mtime: 0 });
194
+ }
195
+ };
196
+ pushFrom(path.join(root, '_Project_Meta'), 'project', (n) => n);
197
+ pushFrom(path.join(root, '_Misc', 'tasks'), 'misc', (n) => n);
198
+ pushFrom(path.join(root, '_Control_Center', 'tasks'), 'global-task', (n) => n);
199
+ rows.sort((a, b) => (a.kind === b.kind ? a.task.localeCompare(b.task, 'zh') : a.kind.localeCompare(b.kind)));
200
+ const file = path.join(root, GLOBAL_REGISTRY_REL);
201
+ const lines = ['# 体系注册(全局)', '', '| 任务 | 类别 | 体系文件 |', '|---|---|---|'];
202
+ for (const r of rows) lines.push('| ' + r.task + ' | ' + r.kind + ' | ' + wrap(r.path) + ' |');
203
+ lines.push('');
204
+ lines.push('本文件只列各任务体系文件的路径;各任务体系正文不注入全局提示段。');
205
+ const body = lines.join(NL) + NL;
206
+ let changed = true;
207
+ try { changed = readUtf8(file) !== body; } catch { changed = true; }
208
+ if (changed) {
209
+ fs.mkdirSync(path.dirname(file), { recursive: true });
210
+ const tmp = file + '.tmp-' + process.pid;
211
+ fs.writeFileSync(tmp, body, 'utf8');
212
+ fs.renameSync(tmp, file);
213
+ }
214
+ return { rows, file, changed, body };
215
+ }
216
+
217
+ /** 全局总注册正文;不存在时按磁盘重建。 */
218
+ export function readGlobalRegistry(ws) {
219
+ const synced = syncGlobalRefs(ws);
220
+ return { file: synced.file, exists: true, text: synced.body, rows: synced.rows };
221
+ }
222
+
223
+ /** 注册内容注入块:全局只注入引用清单;项目与杂项注入本任务注册全文(压缩为一行一条)。 */
224
+ export function systemBlock(ws, info) {
225
+ const lin = lineageOf(info);
226
+ if (!lin) return '';
227
+ if (lin.kind === 'global') {
228
+ const g = readGlobalRegistry(ws);
229
+ if (!g.rows.length) return '体系注册(全局):暂无任务体系;新建任务后由插件登记路径。';
230
+ const lines = ['体系注册(全局;只列路径,不注入各任务正文)'];
231
+ for (const r of g.rows) lines.push('- ' + normRel(r.path) + ' —— ' + r.kind + '「' + r.task + '」');
232
+ return lines.join(NL);
233
+ }
234
+ const reg = readRegistry(ws, info);
235
+ if (!reg.exists) return '体系注册:未设置(首轮讨论体系后用 ho_docmap(action="write") 注册)';
236
+ const d = reg.data;
237
+ const lines = ['体系注册(' + lineageLabel(info) + ')'];
238
+ for (const e of d.structure) lines.push('- 目录 ' + normRel(e.path) + ':' + (e.about || ''));
239
+ for (const e of d.files) lines.push('- 规范 ' + normRel(e.path) + ':' + (e.about || '') + (e.when ? '(' + e.when + ')' : ''));
240
+ if (d.plan) lines.push('- 计划 ' + normRel(d.plan));
241
+ return lines.join(NL);
242
+ }
243
+
244
+ /** 注册文件签名:文件不存在为 'none';用于热更新判断。 */
245
+ export function registrySignature(ws, info) {
246
+ const lin = lineageOf(info);
247
+ if (!lin) return '';
248
+ if (lin.kind === 'global') {
249
+ try {
250
+ const st = fs.statSync(path.join(String(ws || ''), GLOBAL_REGISTRY_REL));
251
+ return 'g:' + st.size + ':' + Math.round(st.mtimeMs);
252
+ } catch { return 'g:none'; }
253
+ }
254
+ const file = registryAbsPath(ws, info);
255
+ if (!file) return '';
256
+ try {
257
+ const st = fs.statSync(file);
258
+ return 'r:' + st.size + ':' + Math.round(st.mtimeMs);
259
+ } catch { return 'r:none'; }
260
+ }
261
+
262
+ /** 体检:校验某类别的注册路径;不传 info 时检查全部。返回 { rows, missing }。 */
263
+ export function checkRegistries(ws, info) {
264
+ const targets = [];
265
+ if (info) targets.push(info);
266
+ else {
267
+ try {
268
+ for (const name of fs.readdirSync(path.join(String(ws || ''), '_Project_Meta'))) {
269
+ if (isFile(path.join(ws, '_Project_Meta', name, REGISTRY_NAME))) targets.push({ role: 'project', project: name });
270
+ }
271
+ } catch { /* 无项目元数据 */ }
272
+ try {
273
+ for (const name of fs.readdirSync(path.join(String(ws || ''), '_Misc', 'tasks'))) {
274
+ if (isFile(path.join(ws, '_Misc', 'tasks', name, REGISTRY_NAME))) targets.push({ role: 'misc', task: name });
275
+ }
276
+ } catch { /* 无杂项任务 */ }
277
+ try {
278
+ for (const name of fs.readdirSync(path.join(String(ws || ''), '_Control_Center', 'tasks'))) {
279
+ if (isFile(path.join(ws, '_Control_Center', 'tasks', name, REGISTRY_NAME))) targets.push({ role: 'global-task', task: name });
280
+ }
281
+ } catch { /* 无全局任务 */ }
282
+ }
283
+ const rows = [];
284
+ let missing = 0;
285
+ for (const t of targets) {
286
+ const reg = readRegistry(ws, t);
287
+ const entries = reg.data.structure.concat(reg.data.files);
288
+ const bad = validateEntries(ws, entries);
289
+ const planBad = reg.data.plan && pathProblem(ws, reg.data.plan) ? [{ path: reg.data.plan, problem: pathProblem(ws, reg.data.plan) }] : [];
290
+ missing += bad.length + planBad.length;
291
+ rows.push({ label: lineageLabel(t), rel: normRel(registryRelPath(t) || ''), exists: reg.exists, entries: entries.length, bad: bad.concat(planBad) });
292
+ }
293
+ return { rows, missing };
294
+ }
295
+
296
+ /** 写任务级注册:原子写,写前备份。 */
297
+ export function writeRegistry(ws, info, data) {
298
+ const file = registryAbsPath(ws, info);
299
+ if (!file) throw new Error('没有可写的注册路径:角色未定');
300
+ fs.mkdirSync(path.dirname(file), { recursive: true });
301
+ let backup = '';
302
+ if (isFile(file)) {
303
+ backup = file + '.bak';
304
+ try { fs.copyFileSync(file, backup); } catch { backup = ''; }
305
+ }
306
+ const body = renderRegistry(info, data);
307
+ const tmp = file + '.tmp-' + process.pid;
308
+ fs.writeFileSync(tmp, body, 'utf8');
309
+ fs.renameSync(tmp, file);
310
+ return { file, backup, bytes: Buffer.byteLength(body, 'utf8') };
311
+ }
312
+
313
+ // ---------------------------------------------------------------- 计划
314
+
315
+ function stamp() {
316
+ const d = new Date();
317
+ const p = (n) => String(n).padStart(2, '0');
318
+ return d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + '_' + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds());
319
+ }
320
+
321
+ /** 归档旧计划到任务根 _archive/,返回归档路径;没有旧计划返回空串。 */
322
+ export function archivePlan(ws, info) {
323
+ const rel = planRelPath(ws, info);
324
+ if (!rel) return '';
325
+ const abs = path.join(String(ws || ''), rel);
326
+ if (!isFile(abs)) return '';
327
+ const dir = path.join(path.dirname(abs), '_archive');
328
+ fs.mkdirSync(dir, { recursive: true });
329
+ const dst = path.join(dir, 'PLAN_' + stamp() + '.md');
330
+ fs.renameSync(abs, dst);
331
+ return dst;
332
+ }
333
+
334
+ export function readPlan(ws, info) {
335
+ const rel = planRelPath(ws, info);
336
+ const abs = rel ? path.join(String(ws || ''), rel) : '';
337
+ if (!abs || !isFile(abs)) return { rel, exists: false, text: '' };
338
+ try { return { rel, exists: true, text: readUtf8(abs) }; }
339
+ catch { return { rel, exists: true, text: '', error: '读取失败' }; }
340
+ }
341
+
342
+ /** 写计划:先归档旧计划,再原子写入。返回 { rel, archived, bytes }。 */
343
+ export function writePlan(ws, info, text) {
344
+ const rel = planRelPath(ws, info);
345
+ if (!rel) throw new Error('没有可写的计划路径:角色未定');
346
+ const archived = archivePlan(ws, info);
347
+ const abs = path.join(String(ws || ''), rel);
348
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
349
+ const body = String(text || '');
350
+ const tmp = abs + '.tmp-' + process.pid;
351
+ fs.writeFileSync(tmp, body, 'utf8');
352
+ fs.renameSync(tmp, abs);
353
+ return { rel, archived, bytes: Buffer.byteLength(body, 'utf8') };
354
+ }
355
+
356
+ /** 把计划写入本任务注册的「计划」一节;注册不存在时返回 false。 */
357
+ export function pointRegistryToPlan(ws, info) {
358
+ const reg = readRegistry(ws, info);
359
+ if (!reg.exists) return false;
360
+ const rel = planRelPath(ws, info);
361
+ const data = { structure: reg.data.structure, files: reg.data.files, plan: rel };
362
+ writeRegistry(ws, info, data);
363
+ return true;
364
+ }
@@ -1,7 +1,7 @@
1
- # Global Handover Changelog(全局交接变更日志)
1
+ # 全局交接台账
2
2
 
3
- > 追加式历史日志。每次改动 _Control_Center 的全局规则、结构或 Global_Handover.md 本身时,追加一行。
4
- > 由插件 ho_changelog 按格式追加(新行插在最新一行之前)。
3
+ > 追加式历史记录。每次修改 `_Control_Center` 的全局规则、结构或 `Global_Handover.md` 本身时,追加一行。
4
+ > 由插件 `ho_changelog` 按格式追加(新行插在最新一行之前)。
5
5
 
6
6
  ## 变更记录
7
7
 
@@ -1,13 +1,13 @@
1
1
  # Workspace Charter(骨架)
2
2
 
3
- > 本文件由 dsh-handover 首次安装向导生成,只含可运行所需的最小内容;按需继续写。
4
- > 铁律:§8.1.1.4 那 24 行是唯一正文来源,任何项目库的 RULES.md 都要逐字复制它(外加一行「上下文水位自查」)。改这里就必须同步改所有库。
3
+ > 本文件由 dsh-handover 首次安装向导生成,只含可运行所需的最小内容;其余内容按需补充。
4
+ > 铁律:§8.1.1.4 的 24 行是唯一正文来源,任何项目库的 RULES.md 都须逐字复制该节(外加一行「上下文水位自查」)。修改本节须同步修改全部库。
5
5
 
6
6
  ## 8. 工程约束
7
7
 
8
- ### 8.1.1.4 项目侧通用约束条(可直接粘贴;改这里就要重新同步所有项目)
8
+ ### 8.1.1.4 项目侧通用约束条(可直接粘贴;修改本节须重新同步全部项目)
9
9
 
10
- > 同步方式=把下面代码块的内容逐行复制进 `Projects/<项目名>/<库名>/RULES.md`,再加一行「上下文水位自查」。
10
+ > 同步方式:将下列代码块内容逐行复制进 `Projects/<项目名>/<库名>/RULES.md`,并追加一行「上下文水位自查」。
11
11
 
12
12
  ```markdown
13
13
  **运行期省 token(通用约束;项目无关,全部在役项目共用这一份正文;数值按 27 个退役会话跨项目实测定)**
@@ -41,13 +41,17 @@
41
41
 
42
42
  ## 9. 上下文水位自查
43
43
 
44
- 每个项目库的 `RULES.md` 必须含一行「上下文水位自查」(说明该库会话在多少水位时写交接、多少时换任)。
45
- 闸门(ho_gate)会逐库检查:标题行在、24 行正文一行不缺、水位自查在。缺任何一项,该库判红。
44
+ 每个项目库的 `RULES.md` 应含一行「上下文水位自查」。水位 35% 写毕交接,水位 50% 不再新开任务,水位 85% 为硬顶;压缩至多 3 次,第 4 次换任。
45
+ `ho_gate` 会逐库检查:标题行存在、24 行正文一行不缺、水位自查存在。缺任何一项,该库判定为未通过。
46
46
 
47
47
  ## 10. 工作区约定
48
48
 
49
- - 全局交接:`_Control_Center/records/Global_Handover.md`(ho_handover 读写,写前自动备份)
50
- - 台账:`_Control_Center/records/Global_Changelog.md`(ho_changelog 按格式追加)
51
- - 会话提示词模板:`_Control_Center/templates/Prompt_Templates.md`(ho_sessions 的 action=prompt 渲染)
49
+ - 全局交接:`_Control_Center/records/Global_Handover.md`(由 `ho_handover` 读写,整篇重写前先归档)
50
+ - 台账:`_Control_Center/records/Global_Changelog.md`(由 `ho_changelog` 按格式追加)
51
+ - 会话提示词模板:`_Control_Center/templates/Prompt_Templates.md`(由 `ho_sessions` 的 `action=prompt` 渲染)
52
+ - 体系注册:全局总注册 `_Control_Center/records/systems.md`(只列各任务体系文件路径),各任务一份 `System.md`(全局任务 `_Control_Center/tasks/<任务名>/System.md`、项目 `_Project_Meta/<项目名>/System.md`、杂项 `_Misc/tasks/<任务名>/System.md`)
53
+ - 行动计划:任务根 `PLAN.md`;新建计划前归档旧计划到任务根 `_archive/`
54
+ - 暂任提示词:任务根 `Delegation_Prompt.md`;规则见 `_Control_Center/rules/Delegation_Rules.md`
55
+ - 写作规范:`_Control_Center/standards/写作规范/README.md`(W-01 至 W-42)
52
56
  - 项目库:`Projects/<项目名>/<库名>/`,每库一份 `RULES.md`;项目交接在 `_Project_Meta/<项目名>/Handover.md`
53
57
  - 杂项:`_Misc/tasks/<任务短名>/`;归档 `_Archives/`;临时与快照 `_Temp/`
@@ -1,6 +1,6 @@
1
- # 全局 交接文档
1
+ # 全局交接文档
2
2
 
3
- > 本文件由 **dsh-handover 首次安装向导**生成。§7 里那行 session id 是**占位**——第一个全局会话接手时把它换成本机真实的 id。
3
+ > 本文件由 dsh-handover 首次安装向导生成。§7 中带 session id 的行是占位,第一个全局会话接手时将其换成本机真实的 id。
4
4
 
5
5
  ## 0. 身份与寻址
6
6
 
@@ -12,21 +12,23 @@
12
12
  | 接任 | 无(首任) |
13
13
  | 上级、报告对象 | 用户 |
14
14
  | 本文件 | _Control_Center/records/Global_Handover.md |
15
- | 冲突以谁为准 | rules/Workspace_Charter.md |
15
+ | 冲突以谁为准 | _Control_Center/rules/Workspace_Charter.md |
16
16
  | 修订 | 首次安装向导生成 |
17
17
 
18
18
  ## 1. 正在做的事
19
19
 
20
20
  | 项 | 现状 |
21
21
  |---|---|
22
- | 初始化 | 工作区骨架已生成;等第一个标题含「全局」的会话接手 |
22
+ | 初始化 | 工作区骨架已生成;等待第一个标题含「全局」的会话接手 |
23
+ | 体系注册 | 全局总注册已建立路径清单,尚无任务入口;任务建立后由全局增行 |
23
24
 
24
25
  ## 2. 下一步
25
26
 
26
27
  | # | 动作 | 落点 | 完成判据 |
27
28
  |---|---|---|---|
28
- | 1 | 全局会话接手:核闸门、把 §0 身份信息填实 | 本文件 §0 | ho_gate 三项全绿 |
29
- | 2 | 按需新建项目库 | Projects/ | 各库 RULES.md 24/24 + 上下文水位自查 |
29
+ | 1 | 全局会话接手:核对闸门、补全 §0 身份信息 | 本文件 §0 | `ho_gate` 3 项全部通过 |
30
+ | 2 | 按需新建项目库 | Projects/ | 各库 RULES.md 24/24 且含上下文水位自查 |
31
+ | 3 | 建立任务体系注册并登记全局总注册 | `_Control_Center/records/systems.md` | `ho_docmap` 检查路径全部有效 |
30
32
 
31
33
  ## 3. 待用户决定
32
34
 
@@ -36,18 +38,23 @@
36
38
 
37
39
  | # | 用户原话要点 | 状态 | 落在哪 |
38
40
  |---|---|---|---|
39
- | 1 | 安装插件并在指定目录建好全局根 | 已落地 | 本文件 |
41
+ | 1 | 安装插件并在指定目录建立全局根 | 已落地 | 本文件 |
40
42
 
41
- ## 5. 坑点与禁区
43
+ ## 5. 风险与禁区
42
44
 
43
- - 禁区:改 §8.1.1.4 那 24 行正文会让所有项目库对不上——要改就同步改全部库。
45
+ - 禁区:修改 §8.1.1.4 的 24 行正文后,各项目库的 `RULES.md` 会与本章程不一致;确需修改须同步全部库。
46
+ - 风险:项目库内文件由该项目会话自行修改;全局代写会使交接与库内事实分离。
44
47
 
45
48
  ## 6. 关键路径
46
49
 
47
- - 交接:_Control_Center/records/Global_Handover.md
48
- - 规则:_Control_Center/rules/Workspace_Charter.md
49
- - 台账:_Control_Center/records/Global_Changelog.md
50
- - 提示词模板:_Control_Center/templates/Prompt_Templates.md
50
+ - 交接:`_Control_Center/records/Global_Handover.md`
51
+ - 规则:`_Control_Center/rules/Workspace_Charter.md`
52
+ - 体系注册:`_Control_Center/records/systems.md`(全局总注册)、`_Control_Center/tasks/` 下各全局任务的 `System.md`
53
+ - 计划:各全局任务根 `PLAN.md`
54
+ - 暂任规则:`_Control_Center/rules/Delegation_Rules.md`
55
+ - 写作规范:`_Control_Center/standards/写作规范/README.md`
56
+ - 台账:`_Control_Center/records/Global_Changelog.md`
57
+ - 提示词模板:`_Control_Center/templates/Prompt_Templates.md`
51
58
 
52
59
  ## 7. 会话 id 对照
53
60