@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.
- package/README.md +117 -113
- 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 +1 -1
- package/docs/releases/1.0.0.md +33 -0
- package/docs/troubleshoot.md +221 -61
- package/package.json +5 -2
- package/scripts/behavior.mjs +398 -0
- package/scripts/blank-dsh.mjs +12 -2
- package/scripts/blank-smoke.mjs +95 -44
- package/scripts/check-docs.mjs +7 -0
- package/scripts/portability.mjs +71 -0
- package/scripts/style-gate.mjs +571 -0
- package/src/client.js +1028 -994
- package/src/index.mjs +273 -109
- package/src/lib/absrefs.mjs +1 -1
- package/src/lib/api.mjs +9 -3
- package/src/lib/archive.mjs +1 -1
- package/src/lib/brief.mjs +8 -6
- package/src/lib/config.mjs +21 -6
- package/src/lib/docmap.mjs +5 -8
- 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 +12 -12
- package/src/lib/global-task.mjs +69 -0
- package/src/lib/guide.mjs +80 -75
- 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 +14 -11
- package/src/lib/manual.mjs +117 -80
- package/src/lib/mover.mjs +18 -14
- package/src/lib/office.mjs +42 -2
- package/src/lib/pack-ops.mjs +10 -10
- package/src/lib/pathkey.mjs +24 -0
- 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 +1 -1
- package/src/lib/scaffold.mjs +30 -10
- package/src/lib/schedule.mjs +5 -5
- package/src/lib/session-create.mjs +2 -2
- package/src/lib/session-ops.mjs +76 -47
- package/src/lib/sessions.mjs +40 -2
- package/src/lib/sevenzip.mjs +10 -1
- package/src/lib/status.mjs +337 -336
- package/src/lib/succession.mjs +14 -14
- package/src/lib/system.mjs +364 -0
- 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 +91 -10
- package/src/templates/Runtime_Manual.md +254 -139
- 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 -24
- 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 +19 -7
- 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 +8 -8
- package/src/tools/ho_changelog.mjs +4 -4
- package/src/tools/ho_charter.mjs +7 -6
- package/src/tools/ho_checkpoint.mjs +34 -31
- package/src/tools/ho_docmap.mjs +135 -49
- package/src/tools/ho_gate.mjs +9 -4
- package/src/tools/ho_handover.mjs +61 -35
- package/src/tools/ho_office.mjs +45 -4
- package/src/tools/ho_profile.mjs +3 -3
- package/src/tools/ho_sessions.mjs +43 -22
- package/src/tools/ho_status.mjs +14 -9
- package/src/tools/ho_succeed.mjs +3 -3
- package/src/tools/ho_workspace.mjs +83 -51
- 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
|
|
8
|
-
import {
|
|
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
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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 + "
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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(
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/src/tools/ho_office.mjs
CHANGED
|
@@ -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
|
-
'
|
|
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: '
|
|
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);
|
|
@@ -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).
|
|
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("
|
|
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
|
|
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
|
|
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 + "
|
|
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: '
|
|
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
|
|
69
|
-
resume: { type: 'string', enum: ['continue', 'wait'], description: 'order=compact 时的去向:缺省 wait
|
|
70
|
-
include_subagents: { type: 'boolean', description: 'list
|
|
71
|
-
limit: { type: 'number', description: 'list
|
|
72
|
-
warn: { type: 'number', description: 'list
|
|
73
|
-
session_id: { type: 'string', description: 'role
|
|
74
|
-
role: { type: 'string', enum: ['global', 'project', 'misc', 'plain'], description: 'role 要设成的角色;new
|
|
75
|
-
project: { type: 'string', description: '项目名(role=project
|
|
76
|
-
|
|
77
|
-
|
|
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).
|
|
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
|
|
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({
|
|
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('
|
|
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
|
}
|
package/src/tools/ho_status.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
: '
|
|
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('配置
|
|
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 + ')' : '
|
|
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('
|
|
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
|
|
212
|
+
if (!log) { out.push(sid + ' ERR 日志不存在'); continue; }
|
|
208
213
|
const recs = decodeLog(log);
|
|
209
|
-
if (!recs) { out.push(sid + ' ERR
|
|
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
|
|
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.
|
|
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)' },
|
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(),
|