@mingmaple/dsh-handover 0.9.14 → 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 (102) hide show
  1. package/README.md +123 -92
  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 +34 -0
  6. package/docs/releases/1.0.0.md +33 -0
  7. package/docs/troubleshoot.md +230 -46
  8. package/package.json +74 -71
  9. package/scripts/behavior.mjs +398 -0
  10. package/scripts/blank-dsh.mjs +41 -25
  11. package/scripts/blank-smoke.mjs +278 -109
  12. package/scripts/check-docs.mjs +7 -0
  13. package/scripts/e2e-migration.mjs +2 -2
  14. package/scripts/office.mjs +3 -3
  15. package/scripts/portability.mjs +71 -0
  16. package/scripts/style-gate.mjs +571 -0
  17. package/src/client.js +1028 -955
  18. package/src/index.mjs +313 -142
  19. package/src/lib/absrefs.mjs +1 -1
  20. package/src/lib/api.mjs +18 -10
  21. package/src/lib/archive.mjs +1 -1
  22. package/src/lib/brief.mjs +8 -6
  23. package/src/lib/config.mjs +23 -8
  24. package/src/lib/consent.mjs +54 -1
  25. package/src/lib/docmap.mjs +140 -0
  26. package/src/lib/drift.mjs +1 -1
  27. package/src/lib/events.mjs +1 -1
  28. package/src/lib/facts.mjs +19 -12
  29. package/src/lib/gates.mjs +30 -6
  30. package/src/lib/global-task.mjs +69 -0
  31. package/src/lib/guide.mjs +81 -85
  32. package/src/lib/handover-io.mjs +69 -6
  33. package/src/lib/identity.mjs +330 -0
  34. package/src/lib/legacy.mjs +89 -0
  35. package/src/lib/lint.mjs +17 -13
  36. package/src/lib/manual.mjs +135 -59
  37. package/src/lib/mover.mjs +23 -19
  38. package/src/lib/names.mjs +14 -19
  39. package/src/lib/office.mjs +63 -15
  40. package/src/lib/pack-ops.mjs +11 -11
  41. package/src/lib/pathkey.mjs +24 -0
  42. package/src/lib/peer.mjs +4 -4
  43. package/src/lib/phase.mjs +8 -13
  44. package/src/lib/profile.mjs +1 -1
  45. package/src/lib/reminder.mjs +110 -0
  46. package/src/lib/role.mjs +9 -6
  47. package/src/lib/scaffold.mjs +68 -26
  48. package/src/lib/schedule.mjs +5 -6
  49. package/src/lib/session-create.mjs +3 -3
  50. package/src/lib/session-ops.mjs +80 -51
  51. package/src/lib/sessions.mjs +41 -3
  52. package/src/lib/sevenzip.mjs +10 -1
  53. package/src/lib/status.mjs +337 -176
  54. package/src/lib/succession.mjs +16 -16
  55. package/src/lib/system.mjs +364 -0
  56. package/src/lib/zstd-log.mjs +1 -1
  57. package/src/selftest.mjs +6 -6
  58. package/src/templates/Changelog.skeleton.md +3 -3
  59. package/src/templates/Charter.skeleton.md +13 -9
  60. package/src/templates/Handover.global.skeleton.md +20 -13
  61. package/src/templates/Handover.template.md +89 -0
  62. package/src/templates/Plan.skeleton.md +34 -0
  63. package/src/templates/Prompt_Templates.md +101 -20
  64. package/src/templates/Runtime_Manual.md +263 -122
  65. package/src/templates/System.skeleton.md +31 -0
  66. package/src/templates/Systems.global.skeleton.md +13 -0
  67. package/src/templates/rules/Delegation_Rules.md +45 -0
  68. package/src/templates/rules/Handover_Rules.md +46 -23
  69. package/src/templates/rules/Migration_Rules.md +7 -7
  70. package/src/templates/rules/Misc_Task_Standard.md +17 -10
  71. package/src/templates/rules/Writing_Rules.md +33 -16
  72. package/src/templates/standards/README.md +37 -0
  73. package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203/README.md +120 -0
  74. 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
  75. package/src/tools/ho_backup.mjs +135 -0
  76. package/src/tools/ho_changelog.mjs +4 -4
  77. package/src/tools/ho_charter.mjs +7 -6
  78. package/src/tools/ho_checkpoint.mjs +36 -32
  79. package/src/tools/ho_docmap.mjs +239 -0
  80. package/src/tools/ho_gate.mjs +11 -5
  81. package/src/tools/ho_handover.mjs +61 -35
  82. package/src/tools/ho_office.mjs +46 -5
  83. package/src/tools/ho_profile.mjs +3 -3
  84. package/src/tools/ho_sessions.mjs +46 -26
  85. package/src/tools/ho_status.mjs +251 -0
  86. package/src/tools/ho_succeed.mjs +3 -3
  87. package/src/tools/ho_workspace.mjs +146 -42
  88. package/src/tools/peer.mjs +416 -0
  89. package/src/tools/ho_brief.mjs +0 -57
  90. package/src/tools/ho_diag.mjs +0 -87
  91. package/src/tools/ho_drift.mjs +0 -41
  92. package/src/tools/ho_init.mjs +0 -82
  93. package/src/tools/ho_pack.mjs +0 -38
  94. package/src/tools/ho_phase.mjs +0 -37
  95. package/src/tools/ho_snapshot.mjs +0 -104
  96. package/src/tools/ho_tokens.mjs +0 -57
  97. package/src/tools/peer_list.mjs +0 -37
  98. package/src/tools/peer_new.mjs +0 -75
  99. package/src/tools/peer_peek.mjs +0 -41
  100. package/src/tools/peer_rename.mjs +0 -31
  101. package/src/tools/peer_send.mjs +0 -52
  102. package/src/tools/peer_status.mjs +0 -46
@@ -1,19 +1,29 @@
1
- /** ho_office —— 在任登记表:查某条类别现在由哪个会话在任(只读)。 */
1
+ /** ho_office —— 在任登记表:查在任;也用于会话首次注册自己的名字。 */
2
2
  import { textOutput } from '../lib/output.mjs';
3
3
  import { nameOf } from '../lib/names.mjs';
4
4
  import { NL } from '../lib/text.mjs';
5
- import { loadOffice, resolveOffice, rowsOf, formatRow, officePath } from '../lib/office.mjs';
5
+ import { loadOffice, resolveOffice, rowsOf, formatRow, officePath, registerHolder, lineageKeyOf } from '../lib/office.mjs';
6
+
7
+ function validDisplayName(name) {
8
+ const s = String(name || '').trim();
9
+ if (!s) return '名字不能为空';
10
+ if (s.length > 40) return '名字不超过 40 个字符';
11
+ if (/[\\/:*?"<>|\r\n]/.test(s)) return '名字不能含路径分隔符或换行';
12
+ return '';
13
+ }
6
14
 
7
15
  export function pmOfficeTool(env) {
8
16
  return {
9
17
  name: nameOf('office'),
10
18
  description:
11
- '查在任:输入「全局」「全局3」「项目名」等,返回现任会话的名字与 id;多个用空格分隔,留空列全部。只读。',
19
+ '在任登记:query 查在任(缺省);action=register 由本会话首次注册自身名称,注册后不得自行修改。',
12
20
  parameters: {
13
21
  type: 'object',
14
22
  additionalProperties: false,
15
23
  properties: {
16
- query: { type: 'string', description: '要查的名字,如「全局」「全局3」「Mingent」「抢名额脚本」;多个用空格或逗号分隔;留空=列出全部在任' },
24
+ action: { type: 'string', enum: ['read', 'register'], description: '缺省 read;register 首次注册名字' },
25
+ name: { type: 'string', description: 'action=register 时的注册名(≤40 字,不含路径分隔符)' },
26
+ query: { type: 'string', description: '要查的名字,如「全局」「全局3」「Demo1」「示例任务」;多个用空格或逗号分隔;留空=列出全部在任' },
17
27
  json: { type: 'boolean', description: 'true 时输出机器可读 JSON(供脚本解析)' },
18
28
  },
19
29
  },
@@ -21,10 +31,41 @@ export function pmOfficeTool(env) {
21
31
  async execute(args, exec) {
22
32
  const ws = env.resolveWs(exec);
23
33
  if (!ws) return { text: '无法确定工作区根,读不到在任登记表。' };
34
+ const action = typeof args.action === 'string' && args.action ? args.action : 'read';
35
+ if (action === 'register') {
36
+ const meId = String(env.selfId(exec) || '');
37
+ if (!meId) throw new Error('无法取得本会话 id,不能注册');
38
+ let info = null;
39
+ try { info = env.roleOf(exec); } catch { info = null; }
40
+ const role = info && info.role ? info.role : 'plain';
41
+ if (role !== 'project' && role !== 'misc' && role !== 'global') {
42
+ throw new Error('普通会话没有注册名;先在设置页设定项目或杂项角色');
43
+ }
44
+ const name = String(args.name || '').trim();
45
+ const bad = validDisplayName(name);
46
+ if (bad) throw new Error(bad);
47
+ const kind = role;
48
+ const project = role === 'project' ? String(info.project || '') : '';
49
+ const task = role === 'misc' ? String(info.task || '') : '';
50
+ const key = lineageKeyOf(kind, project, task);
51
+ const store = loadOffice(ws);
52
+ const lin = store.lineages[key];
53
+ const cur = lin && lin.current;
54
+ if (cur && cur.sessionId && String(cur.sessionId) !== meId) {
55
+ throw new Error('类别「' + key + '」已有在任会话:' + (cur.name || '') + '(' + cur.sessionId + ')。不得抢注;先由全局处理在任会话。');
56
+ }
57
+ if (cur && String(cur.sessionId) === meId) {
58
+ if (String(cur.name || '') === name) return { text: '已注册:' + name + '(' + key + ')。注册后不得自行修改。' };
59
+ throw new Error('本会话已注册为「' + cur.name + '」,注册后不得自行修改;改名须找全局。');
60
+ }
61
+ const reg = registerHolder(ws, { kind, project, task, name, sessionId: meId });
62
+ try { if (typeof env.refreshPromptFor === 'function') env.refreshPromptFor(meId); } catch { /* 重装失败不影响注册 */ }
63
+ return { text: '已注册:' + name + '(' + reg.key + ',会话 ' + meId + ')。注册后不得自行修改;改名须找全局。' };
64
+ }
24
65
  const store = loadOffice(ws);
25
66
  const rows = rowsOf(store);
26
67
  if (!rows.length) {
27
- return { text: '在任登记表还是空的(' + officePath(ws) + '):先登记一次在任会话,再查。' };
68
+ return { text: '在任登记表为空(' + officePath(ws) + '):先登记一次在任会话,再查询。' };
28
69
  }
29
70
  const raw = String(args.query || '').trim();
30
71
  const tokens = raw ? raw.split(/[\s,,、;;]+/).filter(Boolean) : [];
@@ -13,7 +13,7 @@ function targetOf(env, info, args, exec) {
13
13
  // 项目会话只处理自己的骨架;传了别的名字就明确报错,而不是静默忽略入参(实测踩过)。
14
14
  if (given && given !== own) {
15
15
  throw new Error('本会话是' + (info.role === 'project' ? '项目「' + own + '」' : '杂项「' + own + '」')
16
- + ',只能读写自己的骨架;要看「' + given + '」请由全局会话执行 ho_profile。');
16
+ + ',只能读写自己的骨架;如需查看「' + given + '」,由全局会话执行 ho_profile。');
17
17
  }
18
18
  return { role: info.role, project: info.project || '', task: info.task || '', self: true };
19
19
  }
@@ -32,7 +32,7 @@ function scopeOf(t) {
32
32
  export function pmProfileTool(env) {
33
33
  return {
34
34
  name: nameOf('profile'),
35
- description: '项目或杂项骨架:读取、注册、校验、取预设。只写本角色的 _Project_Meta 或 _Misc 目录。',
35
+ description: '项目或杂项骨架:读取、注册、校验、取预设。仅写本角色的 _Project_Meta 或 _Misc 目录。',
36
36
  parameters: {
37
37
  type: 'object',
38
38
  additionalProperties: false,
@@ -81,7 +81,7 @@ export function pmProfileTool(env) {
81
81
  return { text: out.join(NL) };
82
82
  }
83
83
  if (action === 'register') {
84
- if (!t.self) throw new Error('注册须由该项目或杂项会话本人执行');
84
+ if (!t.self) throw new Error('注册须由该项目或杂项会话自身执行');
85
85
  if (!r.exists) throw new Error('未找到骨架文件:' + p + '。先通读本库并写入该文件,再执行注册。');
86
86
  if (r.errors.length) throw new Error('骨架结构不合法:' + r.errors.join(';'));
87
87
  const saved = writeProfile(ws, scope, r.profile);
@@ -2,16 +2,16 @@
2
2
  import { textOutput } from '../lib/output.mjs';
3
3
  import { nameOf } from '../lib/names.mjs';
4
4
  import { NL } from '../lib/text.mjs';
5
- import path from 'node:path';
6
5
  import { scanSessions } from '../lib/sessions.mjs';
7
6
  import { syncCheck } from '../lib/gates.mjs';
8
7
  import { stamp } from '../lib/brief.mjs';
9
8
  import { sessionNew, sessionPrompt, sessionArchive, sessionUnarchive, sessionOrder, sessionPlace } from '../lib/session-ops.mjs';
9
+ import { createGlobalTask } from '../lib/global-task.mjs';
10
10
  import { readRoles, resolveRole, ROLE_LABEL, sessionMeta } from '../lib/role.mjs';
11
11
  import { watermark } from '../lib/facts.mjs';
12
12
 
13
13
  function list(env, args, exec) {
14
- const warn = typeof args.warn === "number" ? args.warn : watermark(env.cfg).warnValue;
14
+ const warn = typeof args.warn === "number" ? args.warn : watermark(env.cfg).remindValue;
15
15
  const limit = typeof args.limit === "number" ? args.limit : 40;
16
16
  const scan = scanSessions({
17
17
  dir: env.cacheDir(),
@@ -27,7 +27,7 @@ function list(env, args, exec) {
27
27
  return resolveRole({ cfg: env.cfg, roles: store, sessionId: r.sid, cwd: meta.cwd, firstPrompt: meta.firstPrompt, subagent: meta.subagent, title: r.project });
28
28
  };
29
29
  const out = [];
30
- out.push("活跃 / 沉默线:最后活动 " + stamp(scan.cutoffMs) + " 之后算活跃(" + scan.silentHours + " 小时;Charter 7.2.3)");
30
+ out.push("活跃与沉默线:最后活动晚于 " + stamp(scan.cutoffMs) + " 记为活跃(" + scan.silentHours + " 小时;Charter 7.2.3)");
31
31
  for (const r of scan.rows.slice(0, limit)) {
32
32
  const pct = r.pct === null ? "水位 ?" : "水位 " + r.pct.toFixed(1) + "%";
33
33
  const lab = r.mtime >= scan.cutoffMs ? "活跃" : "沉默";
@@ -37,19 +37,19 @@ function list(env, args, exec) {
37
37
  }
38
38
  const notifProjects = new Set(scan.notify.map((r) => r.project));
39
39
  const skipProjects = new Set(scan.skip.map((r) => r.project));
40
- out.push("通知判定:活跃项目 " + notifProjects.size + " 个,需要通知 " + scan.notify.length + " 个会话(仅各项目在任)/ 沉默项目 " + skipProjects.size + " 个,跳过 " + scan.skip.length + " 个会话(沉默项目全部+前任)");
40
+ out.push("通知判定:活跃项目 " + notifProjects.size + " 个,需要通知 " + scan.notify.length + " 个会话(仅各项目在任)/ 沉默项目 " + skipProjects.size + " 个,跳过 " + scan.skip.length + " 个会话(沉默项目全部+已交出的旧会话)");
41
41
  for (const r of scan.notify) out.push(" 要通知【在任】" + r.project + "(" + r.sid + ") 最后活动 " + stamp(r.mtime));
42
- if (scan.skip.length) out.push(" 跳过(沉默项目,含其前任):" + scan.skip.map((x) => x.project + "(" + x.sid.slice(-8) + ")").join("、"));
42
+ if (scan.skip.length) out.push(" 跳过(沉默项目,含其已交出的旧会话):" + scan.skip.map((x) => x.project + "(" + x.sid.slice(-8) + ")").join("、"));
43
43
  const gate = syncCheck(env.resolveWs(exec));
44
- if (gate.error) out.push("通用约束条闸门(Charter §7.2.3):读不到 §8.1.1.4 正文 —— 请人工核 " + gate.path);
44
+ if (gate.error) out.push("通用约束条闸门(Charter §7.2.3):读不到 §8.1.1.4 正文,请人工核对 " + gate.path);
45
45
  else if (!gate.ok) {
46
46
  const bad = gate.libraries.filter((l) => !l.ok).map((l) => l.name + "(缺 " + l.missing.length + " 行)");
47
- out.push("通用约束条闸门(Charter §7.2.3「闸门」):**未过** —— " + bad.join("、"));
47
+ out.push("通用约束条闸门(Charter §7.2.3「闸门」):未通过 —— " + bad.join("、"));
48
48
  out.push("判定:通用约束条有库缺,交接前必须补齐(Charter §7.2.3 闸门)");
49
49
  return out.join(NL);
50
50
  } else out.push("通用约束条闸门(Charter §7.2.3「闸门」):" + gate.total + "/" + gate.total + " 已落位");
51
51
  const verdict = scan.hot.length
52
- ? "判定:" + scan.hot.length + " 个会话已进预警区(>=" + warn + "%),按 Charter 7.2.3 先收齐交接再换会话"
52
+ ? "判定:" + scan.hot.length + " 个会话已进入预警区(>=" + warn + "%),按 Charter 7.2.3 先收齐交接,再换会话"
53
53
  : scan.unknown.length
54
54
  ? "判定:" + scan.unknown.length + " 个会话水位无法读取,需人工判断"
55
55
  : "判定:没有会话进入预警区(>=" + warn + "%),可以继续";
@@ -60,38 +60,53 @@ function list(env, args, exec) {
60
60
  export function pmSessionsTool(env) {
61
61
  return {
62
62
  name: nameOf('sessions'),
63
- description: '会话清点与调度;action=role 设置角色、new 建会话、order 下指令(交接、压缩)、place 移入工作区、prompt 出提示词、archive/unarchive 归档。',
63
+ description: '会话清点与调度:action=list 清点、role 设定角色、new 新建会话(from_task 可把全局任务独立为项目或杂项)、task-init 建全局任务库、order 下达指令、place 移入工作区、prompt 生成启动提示词、archive 与 unarchive 归档。',
64
64
  parameters: {
65
65
  type: 'object',
66
66
  additionalProperties: false,
67
67
  properties: {
68
- action: { type: 'string', enum: ['list', 'role', 'new', 'order', 'place', 'prompt', 'archive', 'unarchive'], description: '缺省 list' },
69
- order: { type: 'string', enum: ['handover', 'compact'], description: 'order 的动作:handover 让它写交接再换任;compact 给它排压缩(先要有合格交接)' },
70
- resume: { type: 'string', enum: ['continue', 'wait'], description: 'order=compact 时的去向:缺省 wait(交接后收尾,压缩后不唤醒);continue 表示压缩后按交接继续' },
71
- include_subagents: { type: 'boolean', description: 'list 时含子会话' },
72
- limit: { type: 'number', description: 'list 行数(默认 40)' },
73
- warn: { type: 'number', description: 'list 预警阈值(默认 ' + watermark(env.cfg).warn + ')' },
74
- session_id: { type: 'string', description: 'role/order/place/archive/unarchive 的目标会话' },
75
- role: { type: 'string', enum: ['global', 'project', 'misc', 'plain'], description: 'role 要设成的角色;new 时也据此登记接任者角色' },
76
- project: { type: 'string', description: '项目名(role=project 必填;new 时为换任项目)' },
77
- task: { type: 'string', description: '任务名(role=misc 必填)' },
78
- notify: { type: 'boolean', description: 'role 时投整理指令(默认 true,会唤醒它一轮)' },
68
+ action: { type: 'string', enum: ['list', 'role', 'new', 'task-init', 'order', 'place', 'prompt', 'archive', 'unarchive'], description: '缺省 list' },
69
+ order: { type: 'string', enum: ['handover', 'compact'], description: 'order 的动作:handover 令目标会话写交接后换任;compact 为其排压缩(须先有合格交接)' },
70
+ resume: { type: 'string', enum: ['continue', 'wait'], description: 'order=compact 时的去向:缺省 wait(任务已收尾,压缩后不唤醒);continue 为压缩后按交接继续' },
71
+ include_subagents: { type: 'boolean', description: 'list:是否列出子会话' },
72
+ limit: { type: 'number', description: 'list 的行数(默认 40)' },
73
+ warn: { type: 'number', description: 'list 的预警阈值(默认 ' + watermark(env.cfg).remind + ')' },
74
+ session_id: { type: 'string', description: 'role、order、place、archive、unarchive 的目标会话' },
75
+ role: { type: 'string', enum: ['global', 'project', 'misc', 'plain'], description: 'role 要设成的角色;new 时据此登记接任者角色' },
76
+ project: { type: 'string', description: '项目名(role=project 时必填;new 时为接任会话所属项目)' },
77
+ migrate: { type: 'string', enum: ['auto', 'direct'], description: 'role 时项目与杂项互转的方式:auto(缺省)先快照再移动;direct 直接建新体系' },
78
+ from_task: { type: 'string', description: 'new 时把某个全局任务整体独立为项目或杂项会话(沿用现有体系,不重建、不重新注册)' },
79
+ task: { type: 'string', description: '任务名(task-init 必填;role=misc 必填;new 时为接任会话所属任务)' },
80
+ notify: { type: 'boolean', description: 'role 时投递首轮指令(默认 true,将唤醒该会话一轮)' },
79
81
  title: { type: 'string', description: 'new 的标题;prompt 用于推断代际' },
80
82
  workspace: { type: 'string', description: 'new 必填:目标工作区的 id、标题或路径;new 时不给也能按 project 自动匹配同名工作区' },
81
83
  cwd: { type: 'string', description: 'new 的工作目录,必须是某个已注册工作区的根(与 workspace 二选一)' },
82
84
  message: { type: 'string', description: 'new 的启动提示词;不给则自动生成' },
83
- force: { type: 'boolean', description: 'new 越过 <' + watermark(env.cfg).warn + ' 复用判据;order=compact 越过 120 秒去抖;archive 允许停掉在跑的' },
85
+ force: { type: 'boolean', description: 'new 越过 <' + watermark(env.cfg).remind + ' 复用判据;order=compact 越过 120 秒去抖;archive 允许停掉在跑的' },
84
86
  kind: { type: 'string', enum: ['global', 'project', 'misc', 'migration'], description: 'prompt 的类型;new 时用作角色候选' },
85
87
  sessionId: { type: 'string', description: 'prompt:新会话自己的 id' },
86
- globalSessionId: { type: 'string', description: 'prompt:现任全局 id' },
88
+ globalSessionId: { type: 'string', description: 'prompt:在任全局 id' },
87
89
  extra: { type: 'string', description: 'prompt 附加一句;order 时作为「全局附加」拼进指令' },
88
- confirm: { type: 'boolean', description: 'new、order 需要用户明确同意后传 true;否则只报错、不做任何事' },
90
+ confirm: { type: 'boolean', description: 'new、order 需要当前用户明确同意后传 true;否则只报错、不做任何事' },
89
91
  },
90
92
  },
91
93
  output: textOutput(),
92
94
  async execute(args, exec) {
93
95
  const action = typeof args.action === 'string' && args.action ? args.action : 'list';
94
96
  if (action === 'list') return { text: list(env, args, exec) };
97
+ if (action === 'task-init') {
98
+ const info = env.roleOf(exec);
99
+ if (!info || info.role !== 'global') throw new Error('task-init 只对全局会话开放');
100
+ const ws = env.resolveWs(exec);
101
+ const r = createGlobalTask(ws, args.task);
102
+ const out = [];
103
+ out.push('已建立全局任务库:' + r.dir);
104
+ out.push(' 新建 ' + r.created.length + ' 个文件' + (r.created.length ? ':' + r.created.join('、') : ''));
105
+ out.push('交接:_Control_Center/tasks/' + String(args.task) + '/Handover.md;用 ho_handover(action=…, task="' + String(args.task) + '") 读写。');
106
+ out.push('体系注册已登记进 _Control_Center/records/systems.md;行动计划用 ho_docmap(action="plan", task="' + String(args.task) + '", mode="write")。');
107
+ out.push('独立为项目或杂项会话:ho_sessions(action="new", role="project"|"misc", from_task="' + String(args.task) + '", …)(须当前用户确认)。');
108
+ return { text: out.join(NL) };
109
+ }
95
110
  if (action === 'new') return { text: await sessionNew(env, args, exec) };
96
111
  if (action === 'order') return { text: await sessionOrder(env, args) };
97
112
  if (action === 'place') return { text: await sessionPlace(env, args) };
@@ -100,12 +115,17 @@ export function pmSessionsTool(env) {
100
115
  if (action === 'unarchive') return { text: await sessionUnarchive(env, args) };
101
116
  if (action === 'role') {
102
117
  if (typeof env.applyRole !== 'function') throw new Error('宿主侧没有装配 applyRole');
103
- const r = await env.applyRole({ sessionId: args.session_id, role: args.role, project: args.project, task: args.task, wake: args.notify !== false });
118
+ const r = await env.applyRole({
119
+ sessionId: args.session_id, role: args.role, project: args.project, task: args.task,
120
+ wake: args.notify !== false, migrate: args.migrate,
121
+ });
104
122
  const out = [];
105
123
  out.push(r.ok ? '已设定角色:' + (ROLE_LABEL[r.role] || r.role) : '设定失败:' + r.error);
106
124
  if (r.ok) {
107
- out.push(' 存活会话当场生效:' + (r.live ? '是(工具集与提示段已换)' : '否(下次开轮时生效)'));
108
- out.push(' 整理指令:' + (r.delivered ? '已投递(会唤醒它一轮)' : (r.error ? '投递失败:' + r.error : '未投递')));
125
+ out.push(' 存活会话当场生效:' + (r.live ? '是(工具集与提示段已更换)' : '否(下次开轮时生效)'));
126
+ if (r.migrated) out.push(' 身份互转:' + r.migrated + ' —— ' + r.migrationSummary);
127
+ out.push(' 首轮指令:' + (r.delivered ? '已投递(将唤醒该会话一轮)' : (r.error ? '投递失败:' + r.error : '未投递')));
128
+ out.push(' 注册名:' + (r.registeredName || '尚未注册(由该会话首轮与当前用户确认后,用 ho_office action="register" 注册)'));
109
129
  }
110
130
  return { text: out.join(NL) };
111
131
  }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * ho_status —— 会话状态:一个入口,按 view 分流。
3
+ * phase(默认):水位、压缩次数、锁、建议动作
4
+ * brief:水位、闸门、会话清点、交接漂移、阶段建议
5
+ * diag:服务可解析性、调度器、初始化错误、角色与工具集
6
+ * drift:交接漂移体检
7
+ * tokens:按会话 id 统计轮数、步数、每步 token
8
+ * 全部只读;原 ho_phase、ho_brief、ho_diag、ho_drift、ho_tokens 合并于此。
9
+ */
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { textOutput } from '../lib/output.mjs';
13
+ import { nameOf } from '../lib/names.mjs';
14
+ import { NL, pick } from '../lib/text.mjs';
15
+ import { syncCheck } from '../lib/gates.mjs';
16
+ import { scanSessions, readSelfPressure } from '../lib/sessions.mjs';
17
+ import { composeBrief } from '../lib/brief.mjs';
18
+ import { decide } from '../lib/phase.mjs';
19
+ import { computeDrift, driftVerdict } from '../lib/drift.mjs';
20
+ import { watermark } from '../lib/facts.mjs';
21
+ import { eventsOf, countCompactions } from '../lib/events.mjs';
22
+ import { findLog, decodeLog, turnStarts } from '../lib/zstd-log.mjs';
23
+ import { activeConsentsFor, formatConsentLine } from '../lib/consent.mjs';
24
+ import { readReminderState } from '../lib/reminder.mjs';
25
+
26
+ export const VIEWS = ['phase', 'brief', 'diag', 'drift', 'tokens'];
27
+
28
+ function analyzeLog(records) {
29
+ const turns = turnStarts(records).length;
30
+ let steps = 0, outTok = 0, maxResult = 0, toolResults = 0;
31
+ const perTurn = new Map();
32
+ for (const r of records) {
33
+ if (!r || !r.type) continue;
34
+ if (r.type === 'step/start') {
35
+ steps++;
36
+ const t = pick(r, 'data', 'turn');
37
+ if (typeof t === 'number') perTurn.set(t, (perTurn.get(t) || 0) + 1);
38
+ } else if (r.type === 'assistant/message') {
39
+ const u = pick(r, 'data', 'usage');
40
+ if (u && typeof u.outputTokens === 'number') outTok += u.outputTokens;
41
+ } else if (r.type === 'tool/result') {
42
+ toolResults++;
43
+ try { const len = JSON.stringify(pick(r, 'data') || '').length; if (len > maxResult) maxResult = len; } catch { /* ignore */ }
44
+ }
45
+ }
46
+ const heavyTurns = Array.from(perTurn.values()).filter((n) => n > 20).length;
47
+ return { turns, steps, outTok, maxResult, toolResults, heavyTurns, perStep: steps ? Math.round(outTok / steps) : 0 };
48
+ }
49
+
50
+ /** 授权状态行:压缩与换任各一句,未授权写「无授权」。 */
51
+ function consentLine(meId) {
52
+ try { return formatConsentLine(activeConsentsFor(meId)); } catch { return ''; }
53
+ }
54
+
55
+ function phaseText(env, args, exec) {
56
+ const meId = env.selfId(exec);
57
+ const real = env.selfPct(meId);
58
+ const ratio = typeof args.simulateRatio === 'number' ? args.simulateRatio : (real === null ? null : real / 100);
59
+ const count = env.compactionCount(exec);
60
+ const lock = env.lockState(exec);
61
+ const adv = decide({ ratio, compactionsUsed: count.used, cfg: env.cfg, lockState: lock });
62
+ const out = [];
63
+ out.push('会话 ' + (meId || '未知'));
64
+ out.push('水位 ' + (ratio === null ? '读不到' : (Math.round(ratio * 1000) / 10).toFixed(1) + '%')
65
+ + (typeof args.simulateRatio === 'number' ? ' (演练模拟值)' : '')
66
+ + (typeof env.staleNote === 'function' ? env.staleNote(exec) : ''));
67
+ out.push('压缩 ' + count.used + '/' + env.cfg.maxCompactions + ' 次' + (count.note ? ' ' + count.note : ''));
68
+ out.push('锁 ' + lock);
69
+ out.push('建议 ' + adv.action + ' —— ' + adv.reason);
70
+ const c = consentLine(meId);
71
+ if (c) out.push('授权 ' + c);
72
+ return out;
73
+ }
74
+
75
+ function briefText(env, args, exec) {
76
+ const ws = env.resolveWs(exec);
77
+ const w = watermark(env.cfg);
78
+ const warn = typeof args.threshold === 'number' ? args.threshold : w.remindValue;
79
+ const gate = syncCheck(ws);
80
+ const meId = env.selfId(exec);
81
+ const self = readSelfPressure(env.cacheDir(), meId);
82
+ const scan = scanSessions({
83
+ dir: env.cacheDir(),
84
+ meId,
85
+ meCwd: env.selfCwd(exec),
86
+ warn,
87
+ silentHours: env.cfg.sessionScanHours,
88
+ });
89
+ const drift = computeDrift({ ws, cacheDir: env.cacheDir(), slack: env.cfg.driftSlack, all: false });
90
+ const ratio = self.used === null || self.win === null ? null : self.used / self.win;
91
+ const adv = decide({ ratio, compactionsUsed: 0, cfg: env.cfg, lockState: 'unknown' });
92
+ const selfPct = self.used === null || self.win === null ? null : (self.used / self.win) * 100;
93
+ const ok = gate.ok && (selfPct === null || selfPct < warn);
94
+ const lines = composeBrief({
95
+ cfg: env.cfg,
96
+ selfUsed: self.used,
97
+ selfWin: self.win,
98
+ gate,
99
+ scan,
100
+ drift,
101
+ advice: { text: adv.action + ' —— ' + adv.reason },
102
+ ok,
103
+ });
104
+ const c = consentLine(meId);
105
+ if (c) lines.push('授权 ' + c);
106
+ return lines;
107
+ }
108
+
109
+ function diagText(env, state, exec) {
110
+ const out = [];
111
+ const meId = env.selfId(exec);
112
+ out.push('会话 ' + (meId || '?'));
113
+ if (typeof env.version === 'function') out.push('版本 ' + env.version());
114
+ out.push('调度器 ' + (state.schedulerReady() ? '就绪' : '未就绪' + (state.schedulerError() ? ':' + state.schedulerError() : '(稍后重试或重启)')));
115
+ if (typeof env.injectList === 'function') out.push('inject ' + env.injectList().join(' / '));
116
+ if (typeof env.initErrors === 'function') {
117
+ const errs = env.initErrors();
118
+ out.push('初始化 ' + (errs.length ? '有 ' + errs.length + ' 处失败:' + errs.join(';') : '无错误'));
119
+ }
120
+ if (typeof env.probeServices === 'function') {
121
+ const svc = env.probeServices();
122
+ out.push('服务 ' + Object.keys(svc).map((k) => k + '=' + (svc[k] ? '可用' : '—')).join(' '));
123
+ }
124
+ if (typeof env.guardLast === 'function') {
125
+ const g = env.guardLast();
126
+ out.push('通信判定 ' + (g
127
+ ? (g.allow ? '放行' : '拦截') + ' · 依据 ' + g.source + (g.title ? ' · 标题 ' + g.title : '') + ' · 会话 ' + g.sid
128
+ : '本进程还没有 peer(action="send"、"new")调用'));
129
+ }
130
+ if (typeof env.probeAgentScope === 'function') {
131
+ const a = env.probeAgentScope(exec);
132
+ const show = ['compaction', 'compactNow', 'tokenMeter', 'sessions', 'llm', 'agents']
133
+ .map((k) => k + '=' + (a[k] ? '可用' : '—')).join(' ');
134
+ out.push('会话作用域 ' + (a.ctx ? show : '无法获取 exec.agent.ctx'));
135
+ }
136
+ const st = typeof env.compactionState === 'function'
137
+ ? env.compactionState(exec)
138
+ : (() => { const c = countCompactions(eventsOf(exec)); return { used: c.used, lock: c.lock, source: 'events' }; })();
139
+ out.push('压缩 ' + st.used + '/' + env.cfg.maxCompactions + ' 次;锁 ' + st.lock + ';来源 ' + st.source);
140
+ const pct = env.selfPct(meId);
141
+ out.push('水位 ' + (pct === null ? '读不到' : pct.toFixed(1) + '%') + (typeof env.staleNote === 'function' ? env.staleNote(exec) : ''));
142
+ try {
143
+ const rec = readReminderState().sessions[String(meId || '')] || {};
144
+ out.push('提醒 35%=' + (rec.r35 ? '已提醒' : '未提醒') + ' 50%=' + (rec.r50 ? '已提醒' : '未提醒'));
145
+ } catch { /* 提醒状态读不到不影响自检 */ }
146
+ const ws = env.resolveWs(exec);
147
+ let wsOk = false;
148
+ try { wsOk = fs.existsSync(path.join(ws, '_Control_Center')); } catch { wsOk = false; }
149
+ if (typeof env.roleOf === 'function') {
150
+ try {
151
+ const info = env.roleOf(exec);
152
+ const ts = typeof env.toolsetOf === 'function' ? env.toolsetOf(info.role) : null;
153
+ out.push('角色 ' + info.role + (info.project ? '(项目 ' + info.project + ')' : info.task ? '(任务 ' + info.task + ')' : '') + ' · 依据 ' + info.source + (ts ? ' · 可见工具 ' + ts.allow.length + '/' + (ts.allow.length + ts.deny.length) : ''));
154
+ if (typeof env.roleState === 'function') {
155
+ const rs = env.roleState();
156
+ const cnt = typeof rs.count === 'number' ? ' · 已装配 ' + rs.count + ' 个会话' : '';
157
+ out.push('角色装配 ' + (rs.mode || '未知') + cnt + (rs.last ? ' · 最近:' + rs.last.sid + ' → ' + rs.last.role + '(' + rs.last.at + ')' : ''));
158
+ }
159
+ } catch (e) {
160
+ out.push('角色 判定失败:' + (e && e.message ? e.message : e));
161
+ }
162
+ }
163
+ out.push('工作区 ' + ws + (wsOk
164
+ ? ' 已接入'
165
+ : ' 未接入(未找到 _Control_Center):请当前用户在设置页执行「首次安装」,或由本会话直接 ho_workspace(action="init", parent="<目录>", register=true)'));
166
+ try {
167
+ const mv = typeof env.moveInfo === 'function' ? env.moveInfo() : null;
168
+ if (mv) out.push('迁移 ' + mv.phase + ' · ' + mv.src + (mv.dst ? ' → ' + mv.dst : ''));
169
+ } catch { /* ignore */ }
170
+ out.push('配置 remind=' + env.cfg.remindRatio + '·' + env.cfg.remind2Ratio + ' handover=' + env.cfg.handoverRatio + ' pureWindow=' + env.cfg.pureWindowRatio + ' hard=' + env.cfg.hardRatio + ' max=' + env.cfg.maxCompactions);
171
+ if (typeof env.handoverFor === 'function') {
172
+ try {
173
+ const hp = env.handoverFor(exec);
174
+ out.push('交接路径 ' + (hp && hp.path ? hp.path + '(' + hp.kind + ')' : '无,' + ((hp && hp.reason) || '角色未定')));
175
+ } catch (e) {
176
+ out.push('交接路径 解析失败:' + (e && e.message ? e.message : e));
177
+ }
178
+ } else {
179
+ out.push('交接路径 ' + (env.cfg.handoverPath || '(按角色解析)'));
180
+ }
181
+ return out;
182
+ }
183
+
184
+ function driftText(env, args, exec) {
185
+ const ws = env.resolveWs(exec);
186
+ const res = computeDrift({
187
+ ws,
188
+ cacheDir: env.cacheDir(),
189
+ slack: typeof args.slack === 'number' ? args.slack : env.cfg.driftSlack,
190
+ all: args.all === true,
191
+ });
192
+ const out = [];
193
+ out.push('交接匹配检查(以交接为准;容忍 ' + res.slack + ' 轮) 工作区根 ' + ws);
194
+ out.push(' 项目 会话类型 会话 当前轮 交接轮 Δ 判定');
195
+ for (const r of res.rows) {
196
+ const cells = [r.project, r.kind, r.sid.slice(-8), String(r.turns), r.handoverTurn === null ? '?' : String(r.handoverTurn), r.delta === null ? '?' : String(r.delta), r.verdict];
197
+ out.push(' ' + cells.join(' '));
198
+ }
199
+ if (!res.rows.length) out.push(' 读不到任何项目会话(Projects/ 下对应库不存在时属正常)');
200
+ out.push(driftVerdict(res));
201
+ return out;
202
+ }
203
+
204
+ function tokensText(args) {
205
+ const out = [];
206
+ const list = Array.isArray(args.sessions) ? args.sessions.filter((s) => typeof s === 'string' && s.trim() !== '') : [];
207
+ if (!list.length) throw new Error('view=tokens 时 sessions 不能为空(给会话 id 数组)');
208
+ out.push('会话 轮 步 输出tok 每步tok 最贵返回 超20步轮 判定');
209
+ for (const sid of list) {
210
+ const full = sid.indexOf('session-') === 0 ? sid : 'session-' + sid;
211
+ const log = findLog(full);
212
+ if (!log) { out.push(sid + ' ERR 日志不存在'); continue; }
213
+ const recs = decodeLog(log);
214
+ if (!recs) { out.push(sid + ' ERR 日志无法解码'); continue; }
215
+ const m = analyzeLog(recs);
216
+ const verdict = m.perStep > 2000 ? '偏贵(每步 >2k)' : m.heavyTurns > 0 ? '有超 20 步的轮' : '正常';
217
+ out.push([sid.slice(-8), m.turns, m.steps, m.outTok, m.perStep, Math.round(m.maxResult / 1024) + 'KB', m.heavyTurns, verdict].join(' '));
218
+ }
219
+ return out;
220
+ }
221
+
222
+ export function pmStatusTool(env, state) {
223
+ const w = watermark(env.cfg);
224
+ return {
225
+ name: nameOf('status'),
226
+ description:
227
+ '会话状态(只读):view=phase 水位与建议(缺省)、brief 起始简报、diag 插件自检、drift 交接漂移、tokens token 体检。',
228
+ parameters: {
229
+ type: 'object',
230
+ additionalProperties: false,
231
+ properties: {
232
+ view: { type: 'string', enum: VIEWS, description: '缺省 phase' },
233
+ threshold: { type: 'number', description: 'view=brief:预警阈值百分比(默认 ' + w.remindValue + ')' },
234
+ simulateRatio: { type: 'number', description: 'view=phase:演练用比例替代真实水位(0-1),不写任何状态' },
235
+ sessions: { type: 'array', items: { type: 'string' }, description: 'view=tokens:会话 id 列表' },
236
+ slack: { type: 'number', description: 'view=drift:容忍轮数(默认 3)' },
237
+ all: { type: 'boolean', description: 'view=drift:列全部会话,而不只是每个项目的在任会话' },
238
+ },
239
+ },
240
+ output: textOutput(),
241
+ async execute(args, exec) {
242
+ const view = typeof args.view === 'string' && args.view ? args.view : 'phase';
243
+ if (VIEWS.indexOf(view) === -1) throw new Error('view 只能是 ' + VIEWS.join(' | ') + '(收到 ' + String(view) + ')');
244
+ if (view === 'phase') return { text: phaseText(env, args, exec).join(NL) };
245
+ if (view === 'brief') return { text: briefText(env, args, exec).join(NL) };
246
+ if (view === 'diag') return { text: diagText(env, state, exec).join(NL) };
247
+ if (view === 'drift') return { text: driftText(env, args, exec).join(NL) };
248
+ return { text: tokensText(args).join(NL) };
249
+ },
250
+ };
251
+ }
@@ -7,13 +7,13 @@ export function pmSucceedTool(env) {
7
7
  return {
8
8
  name: nameOf('succeed'),
9
9
  description:
10
- '换任:校验本会话交接,建接任会话并登记在任;须用户先授权(/allow-succeed)。',
10
+ '换任:校验本会话交接,建立接任会话并登记在任;须由当前用户先行授权(/allow-succeed)。',
11
11
  parameters: {
12
12
  type: 'object',
13
13
  additionalProperties: false,
14
14
  properties: {
15
- note: { type: 'string', description: '写进在任登记的备注(可选)' },
16
- force: { type: 'boolean', description: '本会话水位未到线时仍换任(默认不传)' },
15
+ note: { type: 'string', description: '写入在任登记的备注(可选)' },
16
+ force: { type: 'boolean', description: '本会话水位未到线时仍执行换任(缺省不传)' },
17
17
  },
18
18
  },
19
19
  output: textOutput(),