@mingmaple/dsh-handover 0.0.0-stage → 0.9.12

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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -2
  3. package/docs/migrate.md +61 -0
  4. package/docs/troubleshoot.md +77 -0
  5. package/package.json +70 -5
  6. package/scripts/blank-dsh.mjs +203 -0
  7. package/scripts/blank-smoke.mjs +981 -0
  8. package/scripts/build.mjs +61 -0
  9. package/scripts/check-docs.mjs +74 -0
  10. package/scripts/e2e-migration.mjs +127 -0
  11. package/scripts/lib/register.mjs +103 -0
  12. package/src/client.js +945 -0
  13. package/src/cordis.patch.yml +16 -0
  14. package/src/index.mjs +726 -0
  15. package/src/lib/absrefs.mjs +112 -0
  16. package/src/lib/api.mjs +291 -0
  17. package/src/lib/archive.mjs +198 -0
  18. package/src/lib/brief.mjs +74 -0
  19. package/src/lib/charter.mjs +28 -0
  20. package/src/lib/config.mjs +37 -0
  21. package/src/lib/consent.mjs +84 -0
  22. package/src/lib/drift.mjs +65 -0
  23. package/src/lib/events.mjs +89 -0
  24. package/src/lib/extra-prompt.mjs +41 -0
  25. package/src/lib/facts.mjs +70 -0
  26. package/src/lib/gates.mjs +148 -0
  27. package/src/lib/guide.mjs +103 -0
  28. package/src/lib/handover-io.mjs +69 -0
  29. package/src/lib/lint.mjs +152 -0
  30. package/src/lib/manual.mjs +124 -0
  31. package/src/lib/mover.mjs +663 -0
  32. package/src/lib/names.mjs +33 -0
  33. package/src/lib/output.mjs +11 -0
  34. package/src/lib/pack-ops.mjs +332 -0
  35. package/src/lib/peer.mjs +260 -0
  36. package/src/lib/phase.mjs +36 -0
  37. package/src/lib/profile.mjs +204 -0
  38. package/src/lib/prompt.mjs +18 -0
  39. package/src/lib/registry.mjs +53 -0
  40. package/src/lib/retarget-helper.tmpl +46 -0
  41. package/src/lib/role.mjs +251 -0
  42. package/src/lib/rpc-carrier.mjs +65 -0
  43. package/src/lib/scaffold.mjs +118 -0
  44. package/src/lib/schedule.mjs +223 -0
  45. package/src/lib/session-create.mjs +42 -0
  46. package/src/lib/session-ops.mjs +408 -0
  47. package/src/lib/sessions.mjs +143 -0
  48. package/src/lib/sevenzip.mjs +48 -0
  49. package/src/lib/status.mjs +177 -0
  50. package/src/lib/text.mjs +101 -0
  51. package/src/lib/webroute.mjs +130 -0
  52. package/src/lib/workspace.mjs +23 -0
  53. package/src/lib/zstd-log.mjs +65 -0
  54. package/src/selftest.mjs +84 -0
  55. package/src/templates/Changelog.skeleton.md +10 -0
  56. package/src/templates/Charter.skeleton.md +53 -0
  57. package/src/templates/Handover.global.skeleton.md +56 -0
  58. package/src/templates/Prompt_Templates.md +96 -0
  59. package/src/templates/Runtime_Manual.md +166 -0
  60. package/src/templates/rules/Handover_Rules.md +44 -0
  61. package/src/templates/rules/Migration_Rules.md +32 -0
  62. package/src/templates/rules/Misc_Task_Standard.md +23 -0
  63. package/src/templates/rules/Writing_Rules.md +24 -0
  64. package/src/tools/ho_brief.mjs +57 -0
  65. package/src/tools/ho_changelog.mjs +59 -0
  66. package/src/tools/ho_charter.mjs +89 -0
  67. package/src/tools/ho_checkpoint.mjs +132 -0
  68. package/src/tools/ho_diag.mjs +87 -0
  69. package/src/tools/ho_drift.mjs +41 -0
  70. package/src/tools/ho_gate.mjs +69 -0
  71. package/src/tools/ho_handover.mjs +84 -0
  72. package/src/tools/ho_init.mjs +82 -0
  73. package/src/tools/ho_pack.mjs +38 -0
  74. package/src/tools/ho_phase.mjs +37 -0
  75. package/src/tools/ho_profile.mjs +105 -0
  76. package/src/tools/ho_sessions.mjs +115 -0
  77. package/src/tools/ho_snapshot.mjs +104 -0
  78. package/src/tools/ho_tokens.mjs +57 -0
  79. package/src/tools/ho_workspace.mjs +217 -0
  80. package/src/tools/peer_list.mjs +37 -0
  81. package/src/tools/peer_new.mjs +75 -0
  82. package/src/tools/peer_peek.mjs +41 -0
  83. package/src/tools/peer_rename.mjs +31 -0
  84. package/src/tools/peer_send.mjs +52 -0
  85. package/src/tools/peer_status.mjs +46 -0
@@ -0,0 +1,37 @@
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
+ }
@@ -0,0 +1,75 @@
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
+ }
@@ -0,0 +1,41 @@
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
+ }
@@ -0,0 +1,31 @@
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
+ }
@@ -0,0 +1,52 @@
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
+ }
@@ -0,0 +1,46 @@
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
+ }