@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
@@ -1,21 +1,19 @@
1
- /** ho_handover —— 读/写/追加/归档**本会话自己的**交接(路径按角色解析;写前备份、写后按角色体检)。 */
1
+ /** ho_handover —— 读/新建/追加/归档**本会话自己的**交接(路径按角色解析;整篇重写先归档旧交接,写后按角色体检)。 */
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 fs from 'node:fs';
6
5
  import path from 'node:path';
7
- import crypto from 'node:crypto';
8
- import { readHandover, writeHandover, archiveStamp, archiveFileName } from '../lib/handover-io.mjs';
9
- import { archiveDirFor } from '../lib/role.mjs';
6
+ import { readHandover, writeHandover, rewriteHandover, archiveHandover } from '../lib/handover-io.mjs';
7
+ import { assertTaskName } from '../lib/role.mjs';
10
8
 
11
9
  function noPath(input) {
12
10
  const reason = input && input.reason ? input.reason : '未知原因';
13
11
  return [
14
12
  '没有可用的交接路径:' + reason,
15
- '两条处置:',
16
- ' ① 在设置页的 dsh-handover 中「会话角色」给本会话指定角色(项目角色要填项目名);',
13
+ '处置:',
14
+ ' ① 在设置页的 dsh-handover 中「会话角色」给本会话指定角色(项目角色须填项目名);',
17
15
  ' ② 临时显式传 path="<交接文件绝对路径>"。',
18
- '(插件不再默认读全局交接——那是以前会串角色的老行为。)',
16
+ '(插件不再默认读全局交接:旧版行为会导致角色串用。)',
19
17
  ].join(NL);
20
18
  }
21
19
 
@@ -23,58 +21,86 @@ export function pmHandoverTool(env) {
23
21
  return {
24
22
  name: nameOf('handover'),
25
23
  description:
26
- '读写、追加、归档本会话自己的交接;写前备份、写后按角色体检。',
24
+ '读写、新建、追加、归档交接;整篇重写用 new(先归档旧交接,再新建空白文件写入);项目与杂项会话读写自己的交接,全局会话可用 task 指向某个全局任务。写后按角色体检。',
27
25
  parameters: {
28
26
  type: 'object',
29
27
  additionalProperties: false,
30
28
  required: ['action'],
31
29
  properties: {
32
- action: { type: 'string', enum: ['read', 'write', 'append', 'archive'], description: 'read | write | append | archive' },
33
- text: { type: 'string', description: 'write/append 时的正文(read/archive 忽略)' },
30
+ action: { type: 'string', enum: ['read', 'new', 'write', 'append', 'archive'], description: 'read | new(整篇重写:归档旧交接后新建空白写入)| write(仅首次创建)| append | archive' },
31
+ text: { type: 'string', description: 'new/write/append 时的正文(read/archive 忽略)' },
34
32
  path: { type: 'string', description: '覆盖默认路径(演练或强制指定时用)' },
33
+ task: { type: 'string', description: '全局会话读写某个全局任务的交接:_Control_Center/tasks/<任务>/Handover.md' },
35
34
  },
36
35
  },
37
36
  output: textOutput(),
38
37
  async execute(args, exec) {
39
38
  const ws = env.resolveWs(exec);
40
39
  const info = env.roleOf(exec);
41
- const target = args.path
42
- ? { path: args.path, kind: info.role === 'project' ? 'project' : info.role === 'misc' ? 'misc' : 'global', role: info.role, project: info.project, task: info.task }
43
- : env.handoverFor(exec);
40
+ const taskName = String(args.task || '').trim();
41
+ let target = null;
42
+ if (taskName) {
43
+ if (info.role !== 'global') throw new Error('task 参数只对全局会话开放');
44
+ assertTaskName(taskName);
45
+ target = {
46
+ path: path.join(ws, '_Control_Center', 'tasks', taskName, 'Handover.md'),
47
+ kind: 'project', role: 'global-task', task: taskName,
48
+ };
49
+ } else {
50
+ target = args.path
51
+ ? { path: args.path, kind: info.role === 'project' ? 'project' : info.role === 'misc' ? 'misc' : 'global', role: info.role, project: info.project, task: info.task }
52
+ : env.handoverFor(exec);
53
+ }
44
54
  if (!target || !target.path) return { text: noPath(target) };
55
+ const archiveOpts = taskName
56
+ ? {
57
+ info: { role: 'misc', task: taskName },
58
+ archiveDir: path.join(ws, '_Archives', '_Global', 'tasks', taskName, 'Handover'),
59
+ }
60
+ : { kind: target.kind, info };
61
+ const snapOpts = taskName
62
+ ? { kind: target.kind, info: { role: 'misc', task: taskName }, snapshotDir: path.join(ws, '_Temp', '_Global', 'tasks', taskName, 'Snapshots') }
63
+ : { kind: target.kind, info };
45
64
 
46
65
  if (args.action === "read") {
47
66
  const r = readHandover(target.path);
48
- if (r.text === null) return { text: "交接文档不存在:" + target.path + NL + "(第一次写用 action=write 或 action=append)" };
67
+ if (r.text === null) return { text: "交接文档不存在:" + target.path + NL + "(首次创建用 action=write;整篇重写用 action=new)" };
49
68
  return { text: r.text };
50
69
  }
51
70
  if (args.action === "archive") {
52
- const src = readHandover(target.path);
53
- if (src.text === null) throw new Error("要归档的交接不存在:" + target.path);
54
- const stampText = archiveStamp(new Date());
55
- const dir = archiveDirFor(ws, info);
56
- fs.mkdirSync(dir, { recursive: true });
57
- // 命名按 Handover_Rules §4.2:<原文件名>_<YYYYMMDD_HHMMSS>.md;同一秒重复归档加 -2、-3。
58
- let dest = path.join(dir, archiveFileName(target.path, stampText));
59
- for (let n = 2; fs.existsSync(dest) && n < 100; n++) {
60
- dest = path.join(dir, archiveFileName(target.path, stampText + "-" + n));
61
- }
62
- if (fs.existsSync(dest)) throw new Error("归档目标已存在,不覆盖:" + dest);
63
- fs.copyFileSync(target.path, dest);
64
- const a = crypto.createHash("sha256").update(fs.readFileSync(target.path)).digest("hex");
65
- const b = crypto.createHash("sha256").update(fs.readFileSync(dest)).digest("hex");
71
+ if (!readHandover(target.path).text) throw new Error("要归档的交接不存在:" + target.path);
72
+ const a = archiveHandover(ws, target.path, archiveOpts);
73
+ return { text: "已归档 " + a.dest + "(" + a.bytes + " B)" + NL + "sha256 一致 " + a.sha256.slice(0, 16) + "…" };
74
+ }
75
+ if (args.action === "new") {
76
+ if (typeof args.text !== "string" || args.text.trim() === "") throw new Error("new 需要 text");
77
+ const res = rewriteHandover(ws, target.path, args.text, Object.assign({}, snapOpts, archiveOpts));
66
78
  const out = [];
67
- out.push("已归档 " + dest + "(" + fs.statSync(dest).size + " B)");
68
- out.push("sha256 " + (a === b ? "一致 " + a.slice(0, 16) + "…" : "**不一致,归档失败**"));
79
+ out.push(res.archived
80
+ ? (res.archived.reused
81
+ ? "已归档(复用内容相同的副本)" + res.archived.dest + "(" + res.archived.bytes + " B)"
82
+ : "已归档 " + res.archived.dest + "(" + res.archived.bytes + " B,sha256 一致)")
83
+ : "原交接不存在,直接新建(无需归档)");
84
+ out.push("已新建 " + res.path + "(" + res.bytes + " B)· 角色 " + (taskName ? 'global-task' : info.role) + (taskName ? " · 任务「" + taskName + "」" : ""));
85
+ out.push("体检 " + res.verdict + (res.reasons.length ? "" : " · 结构齐备"));
86
+ for (const r of res.reasons) out.push(" - " + r);
87
+ if (res.verdict !== "通过") out.push("提示:修正后重跑 action=new(将再次归档当前文件)。");
69
88
  return { text: out.join(NL) };
70
89
  }
71
90
  if (args.action !== "write" && args.action !== "append") {
72
- throw new Error("未知 action:" + String(args.action) + "(只能是 read | write | append | archive)");
91
+ throw new Error("未知 action:" + String(args.action) + "(只能是 read | new | write | append | archive)");
92
+ }
93
+ if (typeof args.text !== "string" || args.text.trim() === "") throw new Error(args.action + " 需要 text");
94
+ if (args.action === "write" && readHandover(target.path).text) {
95
+ throw new Error(
96
+ "交接已存在,不能用 write 改写:" + target.path + NL
97
+ + "整篇重写按固定流程:① ho_handover(action=\"read\") 读旧交接;② ho_handover(action=\"archive\") 归档;③ 读模板 _Control_Center/templates/Handover.md;④ ho_handover(action=\"new\", text=…) 新建空白写入。",
98
+ );
73
99
  }
74
- if (typeof args.text !== "string" || args.text.trim() === "") throw new Error("write/append 需要 text");
75
- const res = writeHandover(ws, target.path, args.text, args.action === "append" ? "append" : "write", { kind: target.kind, info });
100
+ const res = writeHandover(ws, target.path, args.text, args.action, snapOpts);
76
101
  const out = [];
77
- out.push("已写入 " + res.path + "(" + res.bytes + " B)· 角色 " + info.role + (target.kind !== 'global' ? "(" + (info.project || info.task) + ")" : ""));
102
+ const scope = taskName ? '全局任务「' + taskName + '」' : (target.kind !== 'global' ? '(' + (info.project || info.task) + ')' : '');
103
+ out.push("已写入 " + res.path + "(" + res.bytes + " B)· 角色 " + (taskName ? 'global-task' : info.role) + scope);
78
104
  out.push("备份 " + (res.backup || "(原文件不存在,无需备份)"));
79
105
  out.push("体检 " + res.verdict + (res.reasons.length ? "" : " · 结构齐备"));
80
106
  for (const r of res.reasons) out.push(" - " + r);
@@ -1,18 +1,28 @@
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: {
24
+ action: { type: 'string', enum: ['read', 'register'], description: '缺省 read;register 首次注册名字' },
25
+ name: { type: 'string', description: 'action=register 时的注册名(≤40 字,不含路径分隔符)' },
16
26
  query: { type: 'string', description: '要查的名字,如「全局」「全局3」「Demo1」「示例任务」;多个用空格或逗号分隔;留空=列出全部在任' },
17
27
  json: { type: 'boolean', description: 'true 时输出机器可读 JSON(供脚本解析)' },
18
28
  },
@@ -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);
@@ -6,11 +6,12 @@ import { scanSessions } from '../lib/sessions.mjs';
6
6
  import { syncCheck } from '../lib/gates.mjs';
7
7
  import { stamp } from '../lib/brief.mjs';
8
8
  import { sessionNew, sessionPrompt, sessionArchive, sessionUnarchive, sessionOrder, sessionPlace } from '../lib/session-ops.mjs';
9
+ import { createGlobalTask } from '../lib/global-task.mjs';
9
10
  import { readRoles, resolveRole, ROLE_LABEL, sessionMeta } from '../lib/role.mjs';
10
11
  import { watermark } from '../lib/facts.mjs';
11
12
 
12
13
  function list(env, args, exec) {
13
- 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;
14
15
  const limit = typeof args.limit === "number" ? args.limit : 40;
15
16
  const scan = scanSessions({
16
17
  dir: env.cacheDir(),
@@ -26,7 +27,7 @@ function list(env, args, exec) {
26
27
  return resolveRole({ cfg: env.cfg, roles: store, sessionId: r.sid, cwd: meta.cwd, firstPrompt: meta.firstPrompt, subagent: meta.subagent, title: r.project });
27
28
  };
28
29
  const out = [];
29
- out.push("活跃 / 沉默线:最后活动 " + stamp(scan.cutoffMs) + " 之后算活跃(" + scan.silentHours + " 小时;Charter 7.2.3)");
30
+ out.push("活跃与沉默线:最后活动晚于 " + stamp(scan.cutoffMs) + " 记为活跃(" + scan.silentHours + " 小时;Charter 7.2.3)");
30
31
  for (const r of scan.rows.slice(0, limit)) {
31
32
  const pct = r.pct === null ? "水位 ?" : "水位 " + r.pct.toFixed(1) + "%";
32
33
  const lab = r.mtime >= scan.cutoffMs ? "活跃" : "沉默";
@@ -40,15 +41,15 @@ function list(env, args, exec) {
40
41
  for (const r of scan.notify) out.push(" 要通知【在任】" + r.project + "(" + r.sid + ") 最后活动 " + stamp(r.mtime));
41
42
  if (scan.skip.length) out.push(" 跳过(沉默项目,含其已交出的旧会话):" + scan.skip.map((x) => x.project + "(" + x.sid.slice(-8) + ")").join("、"));
42
43
  const gate = syncCheck(env.resolveWs(exec));
43
- 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);
44
45
  else if (!gate.ok) {
45
46
  const bad = gate.libraries.filter((l) => !l.ok).map((l) => l.name + "(缺 " + l.missing.length + " 行)");
46
- out.push("通用约束条闸门(Charter §7.2.3「闸门」):**未过** —— " + bad.join("、"));
47
+ out.push("通用约束条闸门(Charter §7.2.3「闸门」):未通过 —— " + bad.join("、"));
47
48
  out.push("判定:通用约束条有库缺,交接前必须补齐(Charter §7.2.3 闸门)");
48
49
  return out.join(NL);
49
50
  } else out.push("通用约束条闸门(Charter §7.2.3「闸门」):" + gate.total + "/" + gate.total + " 已落位");
50
51
  const verdict = scan.hot.length
51
- ? "判定:" + scan.hot.length + " 个会话已进预警区(>=" + warn + "%),按 Charter 7.2.3 先收齐交接再换会话"
52
+ ? "判定:" + scan.hot.length + " 个会话已进入预警区(>=" + warn + "%),按 Charter 7.2.3 先收齐交接,再换会话"
52
53
  : scan.unknown.length
53
54
  ? "判定:" + scan.unknown.length + " 个会话水位无法读取,需人工判断"
54
55
  : "判定:没有会话进入预警区(>=" + warn + "%),可以继续";
@@ -59,30 +60,32 @@ function list(env, args, exec) {
59
60
  export function pmSessionsTool(env) {
60
61
  return {
61
62
  name: nameOf('sessions'),
62
- description: '会话清点与调度;action=role 设置角色、new 建会话、order 下指令(交接、压缩)、place 移入工作区、prompt 出提示词、archive/unarchive 归档。',
63
+ description: '会话清点与调度:action=list 清点、role 设定角色、new 新建会话(from_task 可把全局任务独立为项目或杂项)、task-init 建全局任务库、order 下达指令、place 移入工作区、prompt 生成启动提示词、archive 与 unarchive 归档。',
63
64
  parameters: {
64
65
  type: 'object',
65
66
  additionalProperties: false,
66
67
  properties: {
67
- action: { type: 'string', enum: ['list', 'role', 'new', 'order', 'place', 'prompt', 'archive', 'unarchive'], description: '缺省 list' },
68
- order: { type: 'string', enum: ['handover', 'compact'], description: 'order 的动作:handover 让它写交接再换任;compact 给它排压缩(先要有合格交接)' },
69
- resume: { type: 'string', enum: ['continue', 'wait'], description: 'order=compact 时的去向:缺省 wait(交接后收尾,压缩后不唤醒);continue 表示压缩后按交接继续' },
70
- include_subagents: { type: 'boolean', description: 'list 时含子会话' },
71
- limit: { type: 'number', description: 'list 行数(默认 40)' },
72
- warn: { type: 'number', description: 'list 预警阈值(默认 ' + watermark(env.cfg).warn + ')' },
73
- session_id: { type: 'string', description: 'role/order/place/archive/unarchive 的目标会话' },
74
- role: { type: 'string', enum: ['global', 'project', 'misc', 'plain'], description: 'role 要设成的角色;new 时也据此登记接任者角色' },
75
- project: { type: 'string', description: '项目名(role=project 必填;new 时为换任项目)' },
76
- task: { type: 'string', description: '任务名(role=misc 必填)' },
77
- 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,将唤醒该会话一轮)' },
78
81
  title: { type: 'string', description: 'new 的标题;prompt 用于推断代际' },
79
82
  workspace: { type: 'string', description: 'new 必填:目标工作区的 id、标题或路径;new 时不给也能按 project 自动匹配同名工作区' },
80
83
  cwd: { type: 'string', description: 'new 的工作目录,必须是某个已注册工作区的根(与 workspace 二选一)' },
81
84
  message: { type: 'string', description: 'new 的启动提示词;不给则自动生成' },
82
- 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 允许停掉在跑的' },
83
86
  kind: { type: 'string', enum: ['global', 'project', 'misc', 'migration'], description: 'prompt 的类型;new 时用作角色候选' },
84
87
  sessionId: { type: 'string', description: 'prompt:新会话自己的 id' },
85
- globalSessionId: { type: 'string', description: 'prompt:现任全局 id' },
88
+ globalSessionId: { type: 'string', description: 'prompt:在任全局 id' },
86
89
  extra: { type: 'string', description: 'prompt 附加一句;order 时作为「全局附加」拼进指令' },
87
90
  confirm: { type: 'boolean', description: 'new、order 需要当前用户明确同意后传 true;否则只报错、不做任何事' },
88
91
  },
@@ -91,6 +94,19 @@ export function pmSessionsTool(env) {
91
94
  async execute(args, exec) {
92
95
  const action = typeof args.action === 'string' && args.action ? args.action : 'list';
93
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
+ }
94
110
  if (action === 'new') return { text: await sessionNew(env, args, exec) };
95
111
  if (action === 'order') return { text: await sessionOrder(env, args) };
96
112
  if (action === 'place') return { text: await sessionPlace(env, args) };
@@ -99,12 +115,17 @@ export function pmSessionsTool(env) {
99
115
  if (action === 'unarchive') return { text: await sessionUnarchive(env, args) };
100
116
  if (action === 'role') {
101
117
  if (typeof env.applyRole !== 'function') throw new Error('宿主侧没有装配 applyRole');
102
- 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
+ });
103
122
  const out = [];
104
123
  out.push(r.ok ? '已设定角色:' + (ROLE_LABEL[r.role] || r.role) : '设定失败:' + r.error);
105
124
  if (r.ok) {
106
- out.push(' 存活会话当场生效:' + (r.live ? '是(工具集与提示段已换)' : '否(下次开轮时生效)'));
107
- 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" 注册)'));
108
129
  }
109
130
  return { text: out.join(NL) };
110
131
  }
@@ -21,6 +21,7 @@ import { watermark } from '../lib/facts.mjs';
21
21
  import { eventsOf, countCompactions } from '../lib/events.mjs';
22
22
  import { findLog, decodeLog, turnStarts } from '../lib/zstd-log.mjs';
23
23
  import { activeConsentsFor, formatConsentLine } from '../lib/consent.mjs';
24
+ import { readReminderState } from '../lib/reminder.mjs';
24
25
 
25
26
  export const VIEWS = ['phase', 'brief', 'diag', 'drift', 'tokens'];
26
27
 
@@ -74,7 +75,7 @@ function phaseText(env, args, exec) {
74
75
  function briefText(env, args, exec) {
75
76
  const ws = env.resolveWs(exec);
76
77
  const w = watermark(env.cfg);
77
- const warn = typeof args.threshold === 'number' ? args.threshold : w.warnValue;
78
+ const warn = typeof args.threshold === 'number' ? args.threshold : w.remindValue;
78
79
  const gate = syncCheck(ws);
79
80
  const meId = env.selfId(exec);
80
81
  const self = readSelfPressure(env.cacheDir(), meId);
@@ -138,6 +139,10 @@ function diagText(env, state, exec) {
138
139
  out.push('压缩 ' + st.used + '/' + env.cfg.maxCompactions + ' 次;锁 ' + st.lock + ';来源 ' + st.source);
139
140
  const pct = env.selfPct(meId);
140
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 { /* 提醒状态读不到不影响自检 */ }
141
146
  const ws = env.resolveWs(exec);
142
147
  let wsOk = false;
143
148
  try { wsOk = fs.existsSync(path.join(ws, '_Control_Center')); } catch { wsOk = false; }
@@ -157,16 +162,16 @@ function diagText(env, state, exec) {
157
162
  }
158
163
  out.push('工作区 ' + ws + (wsOk
159
164
  ? ' 已接入'
160
- : ' — 未接入(未找到 _Control_Center):让当前用户走设置页「首次安装」,或本会话直接 ho_workspace(action="init", parent="<目录>", register=true)'));
165
+ : ' 未接入(未找到 _Control_Center):请当前用户在设置页执行「首次安装」,或由本会话直接 ho_workspace(action="init", parent="<目录>", register=true)'));
161
166
  try {
162
167
  const mv = typeof env.moveInfo === 'function' ? env.moveInfo() : null;
163
168
  if (mv) out.push('迁移 ' + mv.phase + ' · ' + mv.src + (mv.dst ? ' → ' + mv.dst : ''));
164
169
  } catch { /* ignore */ }
165
- out.push('配置 warn=' + env.cfg.warnRatio + ' handover=' + env.cfg.handoverRatio + ' hard=' + env.cfg.hardRatio + ' max=' + env.cfg.maxCompactions);
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);
166
171
  if (typeof env.handoverFor === 'function') {
167
172
  try {
168
173
  const hp = env.handoverFor(exec);
169
- out.push('交接路径 ' + (hp && hp.path ? hp.path + '(' + hp.kind + ')' : '无 —— ' + ((hp && hp.reason) || '角色未定')));
174
+ out.push('交接路径 ' + (hp && hp.path ? hp.path + '(' + hp.kind + ')' : '无,' + ((hp && hp.reason) || '角色未定')));
170
175
  } catch (e) {
171
176
  out.push('交接路径 解析失败:' + (e && e.message ? e.message : e));
172
177
  }
@@ -185,7 +190,7 @@ function driftText(env, args, exec) {
185
190
  all: args.all === true,
186
191
  });
187
192
  const out = [];
188
- out.push('交接匹配检查(跟着交接走;冗余 ' + res.slack + ' 轮) 工作区根 ' + ws);
193
+ out.push('交接匹配检查(以交接为准;容忍 ' + res.slack + ' 轮) 工作区根 ' + ws);
189
194
  out.push(' 项目 会话类型 会话 当前轮 交接轮 Δ 判定');
190
195
  for (const r of res.rows) {
191
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];
@@ -204,9 +209,9 @@ function tokensText(args) {
204
209
  for (const sid of list) {
205
210
  const full = sid.indexOf('session-') === 0 ? sid : 'session-' + sid;
206
211
  const log = findLog(full);
207
- if (!log) { out.push(sid + ' ERR 日志找不到'); continue; }
212
+ if (!log) { out.push(sid + ' ERR 日志不存在'); continue; }
208
213
  const recs = decodeLog(log);
209
- if (!recs) { out.push(sid + ' ERR 日志解不出'); continue; }
214
+ if (!recs) { out.push(sid + ' ERR 日志无法解码'); continue; }
210
215
  const m = analyzeLog(recs);
211
216
  const verdict = m.perStep > 2000 ? '偏贵(每步 >2k)' : m.heavyTurns > 0 ? '有超 20 步的轮' : '正常';
212
217
  out.push([sid.slice(-8), m.turns, m.steps, m.outTok, m.perStep, Math.round(m.maxResult / 1024) + 'KB', m.heavyTurns, verdict].join(' '));
@@ -219,13 +224,13 @@ export function pmStatusTool(env, state) {
219
224
  return {
220
225
  name: nameOf('status'),
221
226
  description:
222
- '会话状态(只读):view=phase 水位与建议(默认)、brief 起始简报、diag 插件自检、drift 交接漂移、tokens token 体检。',
227
+ '会话状态(只读):view=phase 水位与建议(缺省)、brief 起始简报、diag 插件自检、drift 交接漂移、tokens token 体检。',
223
228
  parameters: {
224
229
  type: 'object',
225
230
  additionalProperties: false,
226
231
  properties: {
227
232
  view: { type: 'string', enum: VIEWS, description: '缺省 phase' },
228
- threshold: { type: 'number', description: 'view=brief:预警阈值百分比(默认 ' + w.warnValue + ')' },
233
+ threshold: { type: 'number', description: 'view=brief:预警阈值百分比(默认 ' + w.remindValue + ')' },
229
234
  simulateRatio: { type: 'number', description: 'view=phase:演练用比例替代真实水位(0-1),不写任何状态' },
230
235
  sessions: { type: 'array', items: { type: 'string' }, description: 'view=tokens:会话 id 列表' },
231
236
  slack: { type: 'number', description: 'view=drift:容忍轮数(默认 3)' },
@@ -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(),