@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,82 +0,0 @@
1
- /**
2
- * ho_init —— 首次安装:在用户给的目录里建「全局文件体系」,可选顺带注册为工作区。
3
- * 与设置页的「首次安装向导」同一套实现(lib/scaffold.mjs),因此两条路径行为一致。
4
- */
5
- import fs from 'node:fs';
6
- import { nameOf } from '../lib/names.mjs';
7
- import path from 'node:path';
8
- import { textOutput } from '../lib/output.mjs';
9
- import { NL } from '../lib/text.mjs';
10
- import { createGlobalWorkspace } from '../lib/scaffold.mjs';
11
-
12
- export function pmInitTool(env) {
13
- return {
14
- name: nameOf('init'),
15
- description:
16
- '首次安装:在指定目录(不存在会创建)下建全局文件体系(根为 <目录>/_Workspace),可顺带注册工作区。',
17
- parameters: {
18
- type: 'object',
19
- additionalProperties: false,
20
- required: ['parent'],
21
- properties: {
22
- parent: { type: 'string', description: '目标父目录绝对路径(不存在会创建);全局根建在它里面' },
23
- name: { type: 'string', description: '全局根目录名,默认 _Workspace' },
24
- register: { type: 'boolean', description: '建好后顺带注册为工作区(默认 false)' },
25
- title: { type: 'string', description: 'register 时的工作区标题,默认「全局」' },
26
- },
27
- },
28
- output: textOutput(),
29
- async execute(args) {
30
- const r = createGlobalWorkspace(args.parent, { name: args.name });
31
- const out = [];
32
- if (!r.ok) {
33
- out.push('未建立:' + r.error);
34
- if (r.root) out.push('目标:' + r.root);
35
- out.push('');
36
- out.push('用法:ho_init(parent="<已存在的目录绝对路径>")。插件只新建、不覆盖。');
37
- return { text: out.join(NL) };
38
- }
39
- out.push('已建立全局文件体系:' + r.root);
40
- out.push(' 目录 ' + r.created.length + ' 个:' + r.created.slice(0, 5).join('、') + (r.created.length > 5 ? ' 等' : ''));
41
- out.push(' 文件 ' + r.written.length + ' 个:' + (r.written.length ? r.written.join('、') : '(都已存在,未覆盖)'));
42
- if (r.existed) out.push(' 目标目录原为空目录,直接使用。');
43
- if (r.createdParent) out.push(' 父目录原本不存在,已创建。');
44
- if (r.skipped.length) out.push(' 跳过(已存在、未覆盖):' + r.skipped.join('、'));
45
-
46
- let registered = null;
47
- if (args.register === true) {
48
- const reg = env.registry();
49
- if (!reg || typeof reg.create !== 'function') {
50
- out.push('');
51
- out.push('注册未完成:workspaceRegistry 不可用。请把下面这个路径粘进 DSH 的新建工作区窗口:');
52
- out.push(' ' + r.root);
53
- return { text: out.join(NL) };
54
- }
55
- const title = String(args.title || '').trim() || '全局';
56
- try {
57
- const w = await reg.create(r.root, title);
58
- registered = w;
59
- out.push('');
60
- out.push('已注册工作区:' + title + '(' + String(w && w.id ? w.id : '?') + ')');
61
- } catch (e) {
62
- out.push('');
63
- out.push('注册失败:' + String(e && e.message ? e.message : e));
64
- out.push('可把下面这个路径粘进 DSH 的新建工作区窗口:');
65
- out.push(' ' + r.root);
66
- return { text: out.join(NL) };
67
- }
68
- }
69
-
70
- out.push('');
71
- out.push('下一步:');
72
- if (!registered) {
73
- out.push(' 1. 注册为工作区:ho_workspace(action="create", path="' + r.root + '", title="全局"),或把该路径粘进 DSH 的新建工作区窗口;');
74
- out.push(' 2. 新建一个标题含「全局」的会话,闸门、交接体检、会话水位即接入。');
75
- } else {
76
- out.push(' 新建一个标题含「全局」的会话,闸门、交接体检、会话水位即接入。');
77
- }
78
- out.push('(也可以在设置页的 dsh-handover 中完成「首次安装」,效果相同。)');
79
- return { text: out.join(NL) };
80
- },
81
- };
82
- }
@@ -1,38 +0,0 @@
1
- /** ho_pack —— 打包族:action=check(前置闸门)、pack(打包)、verify(校包)、restore(默认预演)。 */
2
- import { textOutput } from '../lib/output.mjs';
3
- import { nameOf } from '../lib/names.mjs';
4
- import { packCheck, packNow, packVerify, packRestore } from '../lib/pack-ops.mjs';
5
-
6
- export function pmPackTool(env) {
7
- return {
8
- name: nameOf('pack'),
9
- description: '迁移包四合一:check 前置闸门、pack 打包、verify 校包、restore 恢复(默认预演)。',
10
- parameters: {
11
- type: 'object',
12
- additionalProperties: false,
13
- properties: {
14
- action: { type: 'string', enum: ['check', 'pack', 'verify', 'restore'], description: '缺省 pack' },
15
- kind: { type: 'string', enum: ['project', 'task', 'global'], description: 'pack 的目标类型' },
16
- name: { type: 'string', description: 'pack 的项目名、任务名' },
17
- out: { type: 'string', description: 'pack 的输出目录' },
18
- note: { type: 'string', description: 'pack 写进 MIGRATION.md 的说明' },
19
- items: { type: 'array', items: { type: 'string' }, description: 'check 检查的项目名(global 表示全局)' },
20
- min_age_min: { type: 'number', description: 'check 要求静止分钟(默认 90)' },
21
- pack: { type: 'string', description: 'verify/restore 的包路径' },
22
- target: { type: 'string', description: 'restore 的目标根(默认当前)' },
23
- apply: { type: 'boolean', description: 'restore 时 true 才写入' },
24
- dry_run: { type: 'boolean', description: 'pack 只列清单;restore 只预演' },
25
- format: { type: 'string', enum: ['auto', '7z', 'tar'], description: 'pack 的归档格式:auto=有 7-Zip 用 7z,否则内置 tar.gz(默认)' },
26
- },
27
- },
28
- output: textOutput(),
29
- async execute(args, exec) {
30
- const action = typeof args.action === 'string' && args.action ? args.action : 'pack';
31
- if (action === 'check') return { text: packCheck(env, args, exec) };
32
- if (action === 'verify') return { text: packVerify(env, args, exec) };
33
- if (action === 'restore') return { text: packRestore(env, args, exec) };
34
- if (action !== 'pack') throw new Error('未知 action:' + action + '(check、pack、verify、restore)');
35
- return { text: packNow(env, args, exec) };
36
- },
37
- };
38
- }
@@ -1,37 +0,0 @@
1
- /** ho_phase —— 本会话阶段:水位 / 已压缩次数 / 锁状态 / 建议动作(纯只读,不压缩)。 */
2
- import { textOutput } from '../lib/output.mjs';
3
- import { nameOf } from '../lib/names.mjs';
4
- import { NL } from '../lib/text.mjs';
5
- import { decide } from '../lib/phase.mjs';
6
-
7
- export function pmPhaseTool(env) {
8
- return {
9
- name: nameOf('phase'),
10
- description:
11
- '本会话阶段:水位、已压缩次数、锁状态、建议动作。只读。',
12
- parameters: {
13
- type: 'object',
14
- additionalProperties: false,
15
- properties: {
16
- simulateRatio: { type: 'number', description: '演练用:用这个比例替代真实水位(0-1),不写任何状态' },
17
- },
18
- },
19
- output: textOutput(),
20
- async execute(args, exec) {
21
- const meId = env.selfId(exec);
22
- const real = env.selfPct(meId);
23
- const ratio = typeof args.simulateRatio === 'number' ? args.simulateRatio : (real === null ? null : real / 100);
24
- const count = env.compactionCount(exec);
25
- const lock = env.lockState(exec);
26
- const adv = decide({ ratio, compactionsUsed: count.used, cfg: env.cfg, lockState: lock });
27
- const out = [];
28
- out.push('会话 ' + (meId || '未知'));
29
- out.push('水位 ' + (ratio === null ? '读不到' : (Math.round(ratio * 1000) / 10).toFixed(1) + '%') + (typeof args.simulateRatio === 'number' ? ' (演练模拟值)' : '') + (typeof env.staleNote === 'function' ? env.staleNote(exec) : ''));
30
- const usedText = count.used + '/' + (env.cfg.maxCompactions) + ' 次';
31
- out.push('压缩 ' + usedText + (count.note ? ' ' + count.note : ''));
32
- out.push('锁 ' + lock);
33
- out.push('建议 ' + adv.action + ' —— ' + adv.reason);
34
- return { text: out.join(NL) };
35
- },
36
- };
37
- }
@@ -1,104 +0,0 @@
1
- /** ho_snapshot —— 库快照:逐字节复制 + sha256 清单 + PROVENANCE(Charter §4.4)。 */
2
- import fs from 'node:fs';
3
- import { nameOf } from '../lib/names.mjs';
4
- import path from 'node:path';
5
- import crypto from 'node:crypto';
6
- import { textOutput } from '../lib/output.mjs';
7
- import { NL, isDir } from '../lib/text.mjs';
8
- import { assertProjectName } from '../lib/role.mjs';
9
-
10
- const SKIP_DIRS = ['__pycache__', '.git', 'node_modules', '.obsidian'];
11
- const SKIP_FILES = ['.DS_Store', 'Thumbs.db'];
12
-
13
- function stamp() {
14
- const d = new Date();
15
- const p = (n) => (n < 10 ? '0' + n : '' + n);
16
- return d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + '-' + p(d.getHours()) + p(d.getMinutes());
17
- }
18
-
19
- function walk(root, rel, rows) {
20
- for (const name of fs.readdirSync(path.join(root, rel))) {
21
- const full = path.join(root, rel, name);
22
- const st = fs.statSync(full);
23
- const r = rel ? rel + '/' + name : name;
24
- if (st.isDirectory()) {
25
- if (SKIP_DIRS.indexOf(name) !== -1) continue;
26
- walk(root, r, rows);
27
- } else if (st.isFile()) {
28
- if (SKIP_FILES.indexOf(name) !== -1) continue;
29
- if (name.endsWith('.pyc')) continue;
30
- rows.push({ rel: r, size: st.size });
31
- }
32
- }
33
- return rows;
34
- }
35
-
36
- export function pmSnapshotTool(env) {
37
- return {
38
- name: nameOf('snapshot'),
39
- description:
40
- '库快照:逐字节复制、sha256 清单、来源说明。',
41
- parameters: {
42
- type: 'object',
43
- additionalProperties: false,
44
- required: ['project'],
45
- properties: {
46
- project: { type: 'string', description: '项目名(Projects/<项目>/ 下)' },
47
- tag: { type: 'string', description: '标签,默认 行为检查' },
48
- dry_run: { type: 'boolean', description: '只统计不落盘' },
49
- },
50
- },
51
- output: textOutput(),
52
- async execute(args, exec) {
53
- const ws = env.resolveWs(exec);
54
- const project = assertProjectName(typeof args.project === 'string' ? args.project.trim() : '');
55
- if (!project) throw new Error('project 不能为空(写项目名,如 Demo)');
56
- const projDir = path.join(ws, 'Projects', project);
57
- if (!isDir(projDir)) throw new Error('找不到项目:' + projDir);
58
- let lib = null;
59
- for (const name of fs.readdirSync(projDir)) {
60
- const p = path.join(projDir, name);
61
- if (isDir(p) && fs.existsSync(path.join(p, 'RULES.md'))) { lib = p; break; }
62
- }
63
- if (!lib) throw new Error('该项目下没有带 RULES.md 的库根:' + projDir);
64
- const rows = walk(lib, '', []);
65
- const total = rows.reduce((a, b) => a + b.size, 0);
66
- const out = [];
67
- out.push('源库 ' + lib);
68
- out.push('文件 ' + rows.length + ' 个,共 ' + (total / 1048576).toFixed(2) + ' MB');
69
- if (args.dry_run) { out.push('dry_run:不落盘。'); return { text: out.join(NL) }; }
70
- const tag = args.tag || '行为检查';
71
- const dest = path.join(ws, '_Temp', 'Checks', args.project + '_' + tag + '_' + stamp());
72
- if (fs.existsSync(dest)) throw new Error('目标已存在,不覆盖:' + dest);
73
- const libDest = path.join(dest, 'lib');
74
- fs.mkdirSync(libDest, { recursive: true });
75
- const manifest = ['# 源库: ' + lib, '# 文件: ' + rows.length + ' 字节: ' + total];
76
- let drift = 0;
77
- for (const r of rows) {
78
- const src = path.join(lib, r.rel);
79
- const dst = path.join(libDest, r.rel);
80
- fs.mkdirSync(path.dirname(dst), { recursive: true });
81
- fs.copyFileSync(src, dst);
82
- const h1 = crypto.createHash('sha256').update(fs.readFileSync(src)).digest('hex');
83
- const h2 = crypto.createHash('sha256').update(fs.readFileSync(dst)).digest('hex');
84
- if (h1 !== h2) drift++;
85
- manifest.push(r.rel + String.fromCharCode(9) + r.size + String.fromCharCode(9) + h1);
86
- }
87
- fs.writeFileSync(path.join(dest, 'MANIFEST.tsv'), manifest.join(NL), 'utf8');
88
- const prov = [
89
- '# PROVENANCE',
90
- '- 源库: ' + lib,
91
- '- 生成时间: ' + new Date().toISOString(),
92
- '- 发起会话: ' + (env.selfId(exec) || '?'),
93
- '- 文件数/字节: ' + rows.length + ' / ' + total,
94
- '- 排除: __pycache__ .git node_modules .obsidian *.pyc .DS_Store Thumbs.db',
95
- '- 校验: 逐文件 sha256 比对,drift=' + drift,
96
- '- 含个人数据,不得外发、不得进 _Archives',
97
- ].join(NL);
98
- fs.writeFileSync(path.join(dest, 'PROVENANCE.md'), prov, 'utf8');
99
- out.push('已落盘 ' + dest);
100
- out.push('校验 drift=' + drift + (drift === 0 ? '(逐文件一致)' : '(**不一致,判失败**)'));
101
- return { text: out.join(NL) };
102
- },
103
- };
104
- }
@@ -1,57 +0,0 @@
1
- /** ho_tokens —— token 体检(替代 token_probe/token_survey 的常用判据)。 */
2
- import { textOutput } from '../lib/output.mjs';
3
- import { nameOf } from '../lib/names.mjs';
4
- import { NL, pick } from '../lib/text.mjs';
5
- import { findLog, decodeLog, turnStarts } from '../lib/zstd-log.mjs';
6
-
7
- function analyzeLog(records) {
8
- const turns = turnStarts(records).length;
9
- let steps = 0, outTok = 0, maxResult = 0, toolResults = 0;
10
- const perTurn = new Map();
11
- for (const r of records) {
12
- if (!r || !r.type) continue;
13
- if (r.type === 'step/start') {
14
- steps++;
15
- const t = pick(r, 'data', 'turn');
16
- if (typeof t === 'number') perTurn.set(t, (perTurn.get(t) || 0) + 1);
17
- } else if (r.type === 'assistant/message') {
18
- const u = pick(r, 'data', 'usage');
19
- if (u && typeof u.outputTokens === 'number') outTok += u.outputTokens;
20
- } else if (r.type === 'tool/result') {
21
- toolResults++;
22
- try { const len = JSON.stringify(pick(r, 'data') || '').length; if (len > maxResult) maxResult = len; } catch { /* ignore */ }
23
- }
24
- }
25
- const heavyTurns = Array.from(perTurn.values()).filter((n) => n > 20).length;
26
- return { turns, steps, outTok, maxResult, toolResults, heavyTurns, perStep: steps ? Math.round(outTok / steps) : 0 };
27
- }
28
-
29
- export function pmTokensTool(env) {
30
- return {
31
- name: nameOf('tokens'),
32
- description:
33
- '会话 token 体检:轮数、步数、每步 token、最贵单条返回。只读。',
34
- parameters: {
35
- type: 'object', additionalProperties: false, required: ['sessions'],
36
- properties: { sessions: { type: 'array', items: { type: 'string' }, description: '会话 id 列表' } },
37
- },
38
- output: textOutput(),
39
- async execute(args, exec) {
40
- const out = [];
41
- const list = Array.isArray(args.sessions) ? args.sessions.filter((s) => typeof s === 'string' && s.trim() !== '') : [];
42
- if (!list.length) throw new Error('sessions 不能为空(给会话 id 数组)');
43
- out.push('会话 轮 步 输出tok 每步tok 最贵返回 超20步轮 判定');
44
- for (const sid of list) {
45
- const full = sid.indexOf('session-') === 0 ? sid : 'session-' + sid;
46
- const log = findLog(full);
47
- if (!log) { out.push(sid + ' ERR 日志找不到'); continue; }
48
- const recs = decodeLog(log);
49
- if (!recs) { out.push(sid + ' ERR 日志解不出'); continue; }
50
- const m = analyzeLog(recs);
51
- const verdict = m.perStep > 2000 ? '偏贵(每步 >2k)' : m.heavyTurns > 0 ? '有超 20 步的轮' : '正常';
52
- out.push([sid.slice(-8), m.turns, m.steps, m.outTok, m.perStep, Math.round(m.maxResult / 1024) + 'KB', m.heavyTurns, verdict].join(' '));
53
- }
54
- return { text: out.join(NL) };
55
- },
56
- };
57
- }
@@ -1,37 +0,0 @@
1
- /** peer_list —— 平级会话一览:id / 项目 / 水位 / 运行状态。只读本地投影缓存,不激活任何会话。 */
2
- import { textOutput } from '../lib/output.mjs';
3
- import { peerNameOf } from '../lib/names.mjs';
4
- import { NL } from '../lib/text.mjs';
5
- import { readProjCache, rowLine, sessionRows, stamp } from '../lib/peer.mjs';
6
-
7
- export function pmPeerListTool(env) {
8
- return {
9
- name: peerNameOf('list'),
10
- description:
11
- '会话清点(只读缓存,不激活任何会话)。',
12
- parameters: {
13
- type: 'object',
14
- additionalProperties: false,
15
- properties: {
16
- include_subagents: { type: 'boolean', description: '同时列出 origin=subagent 的子会话' },
17
- limit: { type: 'number', description: '最多列出多少行(默认 40)' },
18
- },
19
- },
20
- output: textOutput(),
21
- async execute(args) {
22
- const controller = env.controller();
23
- if (!controller || typeof controller.list !== 'function') {
24
- throw new Error('peer_list:sessionController 不可用(插件未注入该服务)');
25
- }
26
- const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(200, Math.trunc(args.limit))) : 40;
27
- const rows = await sessionRows(controller, args.include_subagents === true);
28
- if (rows.length === 0) return { text: 'no sessions' };
29
- const lines = rows.slice(0, limit).map((row) => {
30
- const cache = readProjCache(row.sessionId);
31
- const state = row.running ? 'running' : row.agentAvailable ? 'idle' : 'cold';
32
- return rowLine(row.sessionId, cache, state) + ' | ' + stamp(row.updatedAt);
33
- });
34
- return { text: lines.join(NL) };
35
- },
36
- };
37
- }
@@ -1,75 +0,0 @@
1
- /** peer_new —— 新建平级会话(无父血缘,永远不会自动通知任何人)。 */
2
- import { randomUUID } from 'node:crypto';
3
- import { peerNameOf } from '../lib/names.mjs';
4
- import { textOutput } from '../lib/output.mjs';
5
- import { resolveWorkspaceId } from '../lib/peer.mjs';
6
-
7
- export function pmPeerNewTool(env) {
8
- return {
9
- name: peerNameOf('new'),
10
- description:
11
- '新建平级会话,可指定工作区与标题。',
12
- parameters: {
13
- type: 'object',
14
- additionalProperties: false,
15
- properties: {
16
- cwd: { type: 'string', description: '新会话的工作目录' },
17
- workspace: {
18
- type: 'string',
19
- description:
20
- '工作区 id、标题、路径。传入后**直接建立在该工作区内**(cwd 取该工作区的 path),不再归入「未分类」。与 cwd 互斥。',
21
- },
22
- title: { type: 'string', description: '新会话的显示标题' },
23
- message: { type: 'string', description: '可选:投递一条起始消息' },
24
- },
25
- },
26
- output: textOutput(),
27
- async execute(args) {
28
- const controller = env.controller();
29
- if (!controller || typeof controller.create !== 'function') {
30
- throw new Error('peer_new:sessionController.create 不可用(插件未注入该服务)');
31
- }
32
- const wantsWorkspace = typeof args.workspace === 'string' && args.workspace.trim() !== '';
33
- if (wantsWorkspace && args.cwd !== undefined) {
34
- throw new Error(
35
- 'peer_new: 传 workspace 时不要再传 cwd —— session.create 只接受其中之一(cwd 会自动取该工作区的 path)',
36
- );
37
- }
38
- let request = {};
39
- if (wantsWorkspace) {
40
- const workspaceId = resolveWorkspaceId(args.workspace);
41
- if (workspaceId === null || workspaceId === undefined) {
42
- throw new Error(
43
- 'peer_new: 找不到工作区「' + args.workspace + '」(可用 id / 标题 / 路径;数据源 storages/workspace.json)',
44
- );
45
- }
46
- request = { workspaceId };
47
- } else if (args.cwd !== undefined) {
48
- request = { cwd: args.cwd };
49
- }
50
- const created = await controller.create(request);
51
- const id = created.sessionId;
52
- if (typeof args.title === 'string' && args.title.trim() !== '') {
53
- try {
54
- await controller.rename({ sessionId: id, title: args.title.trim() });
55
- } catch (error) {
56
- if (typeof env.log === 'function') env.log('peer_new: 改标题失败:' + String(error));
57
- }
58
- }
59
- let tail = '';
60
- if (typeof args.message === 'string' && args.message.trim() !== '') {
61
- await controller.prompt(
62
- {
63
- requestId: randomUUID(),
64
- sessionId: id,
65
- mode: 'queue',
66
- content: [{ type: 'text', text: args.message }],
67
- },
68
- new AbortController().signal,
69
- );
70
- tail = ' (opening message delivered)';
71
- }
72
- return { text: 'created ' + id + tail };
73
- },
74
- };
75
- }
@@ -1,41 +0,0 @@
1
- /** peer_peek —— 可选监测:读另一个会话日志里最近若干条对话。只读,但内容进本会话上下文。 */
2
- import { textOutput } from '../lib/output.mjs';
3
- import { peerNameOf } from '../lib/names.mjs';
4
- import { NL } from '../lib/text.mjs';
5
- import { findSessionLog, decodeSessionLog, conversation, stamp } from '../lib/peer.mjs';
6
-
7
- export function pmPeerPeekTool(env) {
8
- return {
9
- name: peerNameOf('peek'),
10
- description:
11
- '查看指定会话近期的对话(消耗本会话 token)。',
12
- parameters: {
13
- type: 'object',
14
- additionalProperties: false,
15
- required: ['session_id'],
16
- properties: {
17
- session_id: { type: 'string', description: '目标会话 id' },
18
- limit: { type: 'number', description: '取最近多少条(默认 8,上限 40)' },
19
- },
20
- },
21
- output: textOutput(),
22
- async execute(args) {
23
- const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(40, Math.trunc(args.limit))) : 8;
24
- const file = findSessionLog(args.session_id);
25
- if (file === null) return { text: 'no session log found for ' + args.session_id };
26
- const events = decodeSessionLog(file);
27
- if (events === null) return { text: 'this Node build has no zstd support; cannot read the session log' };
28
- const items = conversation(events, limit);
29
- const head = 'session ' + args.session_id + ' | 最近 ' + String(items.length) + ' 条 | ' + file;
30
- if (items.length === 0) return { text: head + NL + '(没有可读的对话内容)' };
31
- const body = items
32
- .map((it) => {
33
- const one = it.text.replace(/\s+/g, ' ').trim();
34
- const cut = one.length > 400 ? one.slice(0, 400) + ' …' : one;
35
- return '[' + stamp(it.t) + ' ' + it.who + '] ' + cut;
36
- })
37
- .join(NL);
38
- return { text: head + NL + body };
39
- },
40
- };
41
- }
@@ -1,31 +0,0 @@
1
- /** peer_rename —— 改会话显示标题:命名约定 项目会话 <项目名><N>、全局 全局<N>。 */
2
- import { textOutput } from '../lib/output.mjs';
3
- import { peerNameOf } from '../lib/names.mjs';
4
-
5
- export function pmPeerRenameTool(env) {
6
- return {
7
- name: peerNameOf('rename'),
8
- description:
9
- '只改显示标题,不唤醒目标会话。',
10
- parameters: {
11
- type: 'object',
12
- additionalProperties: false,
13
- required: ['session_id', 'title'],
14
- properties: {
15
- session_id: { type: 'string', description: '目标会话 id' },
16
- title: { type: 'string', description: '新的显示标题(如 Demo4 / DemoB4 / 全局11)' },
17
- },
18
- },
19
- output: textOutput(),
20
- async execute(args) {
21
- const controller = env.controller();
22
- if (!controller || typeof controller.rename !== 'function') {
23
- throw new Error('peer_rename:sessionController.rename 不可用(插件未注入该服务)');
24
- }
25
- const title = String(args.title ?? '').trim();
26
- if (title === '') throw new Error('peer_rename: title 不能为空');
27
- await controller.rename({ sessionId: args.session_id, title });
28
- return { text: 'renamed ' + args.session_id + ' -> ' + title };
29
- },
30
- };
31
- }
@@ -1,52 +0,0 @@
1
- /** peer_send —— 给指定会话投一条消息:唯一会花对方 token 的动作。 */
2
- import { randomUUID } from 'node:crypto';
3
- import { peerNameOf } from '../lib/names.mjs';
4
- import { textOutput } from '../lib/output.mjs';
5
- import { NL } from '../lib/text.mjs';
6
- import { peerMarker } from '../lib/peer.mjs';
7
-
8
- export function pmPeerSendTool(env) {
9
- return {
10
- name: peerNameOf('send'),
11
- description:
12
- '向指定会话投递一条消息;唯一会消耗对方 token 的动作,尽量少用。',
13
- parameters: {
14
- type: 'object',
15
- additionalProperties: false,
16
- required: ['session_id', 'message'],
17
- properties: {
18
- session_id: { type: 'string', description: '目标会话 id' },
19
- message: { type: 'string', description: '要投递的正文' },
20
- mode: { type: 'string', enum: ['queue', 'steer'], description: 'queue(默认)或 steer' },
21
- },
22
- },
23
- output: textOutput(),
24
- async execute(args, exec) {
25
- const controller = env.controller();
26
- if (!controller || typeof controller.prompt !== 'function') {
27
- throw new Error('peer_send:sessionController.prompt 不可用(插件未注入该服务)');
28
- }
29
- const message = typeof args.message === 'string' ? args.message : '';
30
- if (message.trim() === '') throw new Error('message 不能为空');
31
- const mode = args.mode === 'steer' ? 'steer' : 'queue';
32
- // 来源标记:dsh 把宿主投递的消息记成 source.kind === "user",不加标记的话,
33
- // 平级会话的消息在日志和界面上与用户消息完全无法区分。
34
- const sender = exec?.agent?.session?.header?.id ?? 'unknown';
35
- const marker = peerMarker(sender);
36
- const body = message.startsWith(marker) ? message : marker + NL + message;
37
- const receipt = await controller.prompt(
38
- {
39
- requestId: randomUUID(),
40
- sessionId: args.session_id,
41
- mode,
42
- content: [{ type: 'text', text: body }],
43
- },
44
- new AbortController().signal,
45
- );
46
- return {
47
- text:
48
- 'accepted=' + String(receipt.accepted) + ' mode=' + mode + ' target=' + args.session_id + ' from=' + sender,
49
- };
50
- },
51
- };
52
- }
@@ -1,46 +0,0 @@
1
- /** peer_status —— 单个会话的详细状态(水位 / 运行状态 / 轮步 / cwd / preset / 平级或子会话)。只读本地。 */
2
- import { textOutput } from '../lib/output.mjs';
3
- import { peerNameOf } from '../lib/names.mjs';
4
- import { NL } from '../lib/text.mjs';
5
- import { readProjCache, pressurePct, isSubagent, isBusy, projectName, pick, sessionRows, stamp } from '../lib/peer.mjs';
6
-
7
- export function pmPeerStatusTool(env) {
8
- return {
9
- name: peerNameOf('status'),
10
- description:
11
- '单会话详情:水位、运行状态、轮数、步数、工作目录。',
12
- parameters: {
13
- type: 'object',
14
- additionalProperties: false,
15
- required: ['session_id'],
16
- properties: {
17
- session_id: { type: 'string', description: '目标会话 id' },
18
- },
19
- },
20
- output: textOutput(),
21
- async execute(args) {
22
- const controller = env.controller();
23
- if (!controller || typeof controller.list !== 'function') {
24
- throw new Error('peer_status:sessionController 不可用(插件未注入该服务)');
25
- }
26
- const cache = readProjCache(args.session_id);
27
- const rows = await sessionRows(controller, true);
28
- const row = rows.find((r) => r.sessionId === args.session_id) ?? null;
29
- if (cache === null && row === null) return { text: 'unknown session: ' + args.session_id };
30
- const stats = pick(cache, 'record', 'rows', 'sessionStats', 'val') ?? {};
31
- const pct = cache === null ? null : pressurePct(cache);
32
- const lines = [
33
- 'session ' + args.session_id,
34
- 'kind ' + (cache === null ? '?' : isSubagent(cache) ? '子会话(subagent)' : '平级会话(peer)'),
35
- 'project ' + (cache === null ? '-' : projectName(cache)),
36
- 'pressure ' + (pct === null ? '?' : pct.toFixed(1) + '%') + (pct !== null && pct >= 32 ? ' 已进入预警区' : ''),
37
- 'state ' + (isBusy(cache) === true ? 'running' : row === null ? 'cold' : row.agentAvailable ? 'idle' : 'cold'),
38
- 'turns ' + String(stats.turns ?? '?') + ' steps ' + String(stats.steps ?? '?'),
39
- 'cwd ' + String(row?.cwd ?? pick(cache, 'record', 'identity', 'cwd') ?? '-'),
40
- 'preset ' + String(pick(cache, 'record', 'rows', 'agentPreset', 'val') ?? '-'),
41
- 'updated ' + stamp(row?.updatedAt ?? pick(cache, 'record', 'identity', 'createdAt')),
42
- ];
43
- return { text: lines.join(NL) };
44
- },
45
- };
46
- }