@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.
- package/README.md +123 -92
- package/docs/migrate.md +52 -41
- package/docs/releases/0.9.12.md +2 -3
- package/docs/releases/0.9.14.md +1 -5
- package/docs/releases/0.9.22.md +34 -0
- package/docs/releases/1.0.0.md +33 -0
- package/docs/troubleshoot.md +230 -46
- package/package.json +74 -71
- package/scripts/behavior.mjs +398 -0
- package/scripts/blank-dsh.mjs +41 -25
- package/scripts/blank-smoke.mjs +278 -109
- package/scripts/check-docs.mjs +7 -0
- package/scripts/e2e-migration.mjs +2 -2
- package/scripts/office.mjs +3 -3
- package/scripts/portability.mjs +71 -0
- package/scripts/style-gate.mjs +571 -0
- package/src/client.js +1028 -955
- package/src/index.mjs +313 -142
- package/src/lib/absrefs.mjs +1 -1
- package/src/lib/api.mjs +18 -10
- package/src/lib/archive.mjs +1 -1
- package/src/lib/brief.mjs +8 -6
- package/src/lib/config.mjs +23 -8
- package/src/lib/consent.mjs +54 -1
- package/src/lib/docmap.mjs +140 -0
- package/src/lib/drift.mjs +1 -1
- package/src/lib/events.mjs +1 -1
- package/src/lib/facts.mjs +19 -12
- package/src/lib/gates.mjs +30 -6
- package/src/lib/global-task.mjs +69 -0
- package/src/lib/guide.mjs +81 -85
- package/src/lib/handover-io.mjs +69 -6
- package/src/lib/identity.mjs +330 -0
- package/src/lib/legacy.mjs +89 -0
- package/src/lib/lint.mjs +17 -13
- package/src/lib/manual.mjs +135 -59
- package/src/lib/mover.mjs +23 -19
- package/src/lib/names.mjs +14 -19
- package/src/lib/office.mjs +63 -15
- package/src/lib/pack-ops.mjs +11 -11
- package/src/lib/pathkey.mjs +24 -0
- package/src/lib/peer.mjs +4 -4
- package/src/lib/phase.mjs +8 -13
- package/src/lib/profile.mjs +1 -1
- package/src/lib/reminder.mjs +110 -0
- package/src/lib/role.mjs +9 -6
- package/src/lib/scaffold.mjs +68 -26
- package/src/lib/schedule.mjs +5 -6
- package/src/lib/session-create.mjs +3 -3
- package/src/lib/session-ops.mjs +80 -51
- package/src/lib/sessions.mjs +41 -3
- package/src/lib/sevenzip.mjs +10 -1
- package/src/lib/status.mjs +337 -176
- package/src/lib/succession.mjs +16 -16
- package/src/lib/system.mjs +364 -0
- package/src/lib/zstd-log.mjs +1 -1
- package/src/selftest.mjs +6 -6
- package/src/templates/Changelog.skeleton.md +3 -3
- package/src/templates/Charter.skeleton.md +13 -9
- package/src/templates/Handover.global.skeleton.md +20 -13
- package/src/templates/Handover.template.md +89 -0
- package/src/templates/Plan.skeleton.md +34 -0
- package/src/templates/Prompt_Templates.md +101 -20
- package/src/templates/Runtime_Manual.md +263 -122
- package/src/templates/System.skeleton.md +31 -0
- package/src/templates/Systems.global.skeleton.md +13 -0
- package/src/templates/rules/Delegation_Rules.md +45 -0
- package/src/templates/rules/Handover_Rules.md +46 -23
- package/src/templates/rules/Migration_Rules.md +7 -7
- package/src/templates/rules/Misc_Task_Standard.md +17 -10
- package/src/templates/rules/Writing_Rules.md +33 -16
- package/src/templates/standards/README.md +37 -0
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203/README.md +120 -0
- 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
- package/src/tools/ho_backup.mjs +135 -0
- package/src/tools/ho_changelog.mjs +4 -4
- package/src/tools/ho_charter.mjs +7 -6
- package/src/tools/ho_checkpoint.mjs +36 -32
- package/src/tools/ho_docmap.mjs +239 -0
- package/src/tools/ho_gate.mjs +11 -5
- package/src/tools/ho_handover.mjs +61 -35
- package/src/tools/ho_office.mjs +46 -5
- package/src/tools/ho_profile.mjs +3 -3
- package/src/tools/ho_sessions.mjs +46 -26
- package/src/tools/ho_status.mjs +251 -0
- package/src/tools/ho_succeed.mjs +3 -3
- package/src/tools/ho_workspace.mjs +146 -42
- package/src/tools/peer.mjs +416 -0
- package/src/tools/ho_brief.mjs +0 -57
- package/src/tools/ho_diag.mjs +0 -87
- package/src/tools/ho_drift.mjs +0 -41
- package/src/tools/ho_init.mjs +0 -82
- package/src/tools/ho_pack.mjs +0 -38
- package/src/tools/ho_phase.mjs +0 -37
- package/src/tools/ho_snapshot.mjs +0 -104
- package/src/tools/ho_tokens.mjs +0 -57
- package/src/tools/peer_list.mjs +0 -37
- package/src/tools/peer_new.mjs +0 -75
- package/src/tools/peer_peek.mjs +0 -41
- package/src/tools/peer_rename.mjs +0 -31
- package/src/tools/peer_send.mjs +0 -52
- package/src/tools/peer_status.mjs +0 -46
package/src/tools/ho_office.mjs
CHANGED
|
@@ -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
|
-
'
|
|
19
|
+
'在任登记:query 查在任(缺省);action=register 由本会话首次注册自身名称,注册后不得自行修改。',
|
|
12
20
|
parameters: {
|
|
13
21
|
type: 'object',
|
|
14
22
|
additionalProperties: false,
|
|
15
23
|
properties: {
|
|
16
|
-
|
|
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: '
|
|
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) : [];
|
package/src/tools/ho_profile.mjs
CHANGED
|
@@ -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
|
-
+ '
|
|
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: '
|
|
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).
|
|
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("
|
|
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("
|
|
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
|
|
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
|
|
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 + "
|
|
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: '
|
|
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
|
|
70
|
-
resume: { type: 'string', enum: ['continue', 'wait'], description: 'order=compact 时的去向:缺省 wait
|
|
71
|
-
include_subagents: { type: 'boolean', description: 'list
|
|
72
|
-
limit: { type: 'number', description: 'list
|
|
73
|
-
warn: { type: 'number', description: 'list
|
|
74
|
-
session_id: { type: 'string', description: 'role
|
|
75
|
-
role: { type: 'string', enum: ['global', 'project', 'misc', 'plain'], description: 'role 要设成的角色;new
|
|
76
|
-
project: { type: 'string', description: '项目名(role=project
|
|
77
|
-
|
|
78
|
-
|
|
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).
|
|
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
|
|
88
|
+
globalSessionId: { type: 'string', description: 'prompt:在任全局 id' },
|
|
87
89
|
extra: { type: 'string', description: 'prompt 附加一句;order 时作为「全局附加」拼进指令' },
|
|
88
|
-
confirm: { type: 'boolean', description: 'new、order
|
|
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({
|
|
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('
|
|
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
|
+
}
|
package/src/tools/ho_succeed.mjs
CHANGED
|
@@ -7,13 +7,13 @@ export function pmSucceedTool(env) {
|
|
|
7
7
|
return {
|
|
8
8
|
name: nameOf('succeed'),
|
|
9
9
|
description:
|
|
10
|
-
'
|
|
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(),
|