@mingmaple/dsh-handover 0.0.0-stage → 0.9.6
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/LICENSE +21 -0
- package/README.md +118 -2
- package/docs/migrate.md +61 -0
- package/docs/troubleshoot.md +48 -0
- package/package.json +55 -3
- package/scripts/blank-dsh.mjs +203 -0
- package/scripts/blank-smoke.mjs +820 -0
- package/scripts/build.mjs +58 -0
- package/scripts/check-docs.mjs +74 -0
- package/scripts/e2e-migration.mjs +127 -0
- package/scripts/lib/register.mjs +103 -0
- package/src/client.js +887 -0
- package/src/cordis.patch.yml +16 -0
- package/src/index.mjs +631 -0
- package/src/lib/absrefs.mjs +112 -0
- package/src/lib/api.mjs +234 -0
- package/src/lib/archive.mjs +198 -0
- package/src/lib/brief.mjs +74 -0
- package/src/lib/charter.mjs +28 -0
- package/src/lib/config.mjs +36 -0
- package/src/lib/consent.mjs +84 -0
- package/src/lib/drift.mjs +65 -0
- package/src/lib/events.mjs +89 -0
- package/src/lib/extra-prompt.mjs +41 -0
- package/src/lib/facts.mjs +70 -0
- package/src/lib/gates.mjs +148 -0
- package/src/lib/guide.mjs +99 -0
- package/src/lib/handover-io.mjs +49 -0
- package/src/lib/lint.mjs +152 -0
- package/src/lib/manual.mjs +124 -0
- package/src/lib/mover.mjs +663 -0
- package/src/lib/names.mjs +33 -0
- package/src/lib/output.mjs +11 -0
- package/src/lib/pack-ops.mjs +329 -0
- package/src/lib/peer.mjs +260 -0
- package/src/lib/phase.mjs +34 -0
- package/src/lib/profile.mjs +204 -0
- package/src/lib/registry.mjs +53 -0
- package/src/lib/retarget-helper.tmpl +46 -0
- package/src/lib/role.mjs +217 -0
- package/src/lib/rpc-carrier.mjs +65 -0
- package/src/lib/scaffold.mjs +118 -0
- package/src/lib/schedule.mjs +227 -0
- package/src/lib/session-ops.mjs +362 -0
- package/src/lib/sessions.mjs +142 -0
- package/src/lib/sevenzip.mjs +48 -0
- package/src/lib/status.mjs +171 -0
- package/src/lib/text.mjs +74 -0
- package/src/lib/webroute.mjs +130 -0
- package/src/lib/workspace.mjs +23 -0
- package/src/lib/zstd-log.mjs +65 -0
- package/src/selftest.mjs +84 -0
- package/src/templates/Changelog.skeleton.md +10 -0
- package/src/templates/Charter.skeleton.md +53 -0
- package/src/templates/Handover.global.skeleton.md +56 -0
- package/src/templates/Prompt_Templates.md +96 -0
- package/src/templates/Runtime_Manual.md +166 -0
- package/src/templates/rules/Handover_Rules.md +43 -0
- package/src/templates/rules/Migration_Rules.md +32 -0
- package/src/templates/rules/Misc_Task_Standard.md +23 -0
- package/src/templates/rules/Writing_Rules.md +24 -0
- package/src/tools/ho_brief.mjs +57 -0
- package/src/tools/ho_changelog.mjs +59 -0
- package/src/tools/ho_charter.mjs +89 -0
- package/src/tools/ho_checkpoint.mjs +125 -0
- package/src/tools/ho_diag.mjs +85 -0
- package/src/tools/ho_drift.mjs +41 -0
- package/src/tools/ho_gate.mjs +69 -0
- package/src/tools/ho_handover.mjs +83 -0
- package/src/tools/ho_init.mjs +82 -0
- package/src/tools/ho_pack.mjs +38 -0
- package/src/tools/ho_phase.mjs +37 -0
- package/src/tools/ho_profile.mjs +97 -0
- package/src/tools/ho_sessions.mjs +114 -0
- package/src/tools/ho_snapshot.mjs +103 -0
- package/src/tools/ho_tokens.mjs +57 -0
- package/src/tools/ho_workspace.mjs +213 -0
- package/src/tools/peer_list.mjs +37 -0
- package/src/tools/peer_new.mjs +75 -0
- package/src/tools/peer_peek.mjs +41 -0
- package/src/tools/peer_rename.mjs +31 -0
- package/src/tools/peer_send.mjs +52 -0
- package/src/tools/peer_status.mjs +46 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** ho_checkpoint —— 写交接 + 登记压缩;压缩由调度器在轮次结束后执行(交接路径按角色)。 */
|
|
2
|
+
import { textOutput } from '../lib/output.mjs';
|
|
3
|
+
import { nameOf } from '../lib/names.mjs';
|
|
4
|
+
import { NL } from '../lib/text.mjs';
|
|
5
|
+
import { readHandover, writeHandover } from '../lib/handover-io.mjs';
|
|
6
|
+
import { eventsOf, countCompactions } from '../lib/events.mjs';
|
|
7
|
+
import { decide } from '../lib/phase.mjs';
|
|
8
|
+
import { checkConsent, consumedConsent } from '../lib/consent.mjs';
|
|
9
|
+
|
|
10
|
+
export function pmCheckpointTool(env) {
|
|
11
|
+
return {
|
|
12
|
+
name: nameOf('checkpoint'),
|
|
13
|
+
description:
|
|
14
|
+
'写交接、登记压缩;回合结束后自动压缩并续跑。',
|
|
15
|
+
parameters: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
additionalProperties: false,
|
|
18
|
+
properties: {
|
|
19
|
+
handover_text: { type: 'string', description: '要写入的交接正文;不给则要求磁盘上已有合格交接' },
|
|
20
|
+
handover_path: { type: 'string', description: '覆盖交接路径(演练用)' },
|
|
21
|
+
at_risk: { type: 'boolean', description: '当前处在关键步骤:为 true 时,水位在提醒线~交接线之间只提醒、不压缩(过交接线仍会压)' },
|
|
22
|
+
force: { type: 'boolean', description: '忽略关键步骤判定,直接排压缩' },
|
|
23
|
+
confirm: { type: 'boolean', description: '判定为「可压缩」后须经用户同意才传 true;还需用户在设置页点「允许压缩」(5 分钟一次性授权),否则不排压缩' },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
output: textOutput(),
|
|
27
|
+
async execute(args, exec) {
|
|
28
|
+
const ws = env.resolveWs(exec);
|
|
29
|
+
const meId = env.selfId(exec);
|
|
30
|
+
if (!meId) throw new Error("拿不到本会话 id,无法调度压缩");
|
|
31
|
+
const info = env.roleOf(exec);
|
|
32
|
+
const target = args.handover_path
|
|
33
|
+
? { path: args.handover_path, kind: info.role === 'project' ? 'project' : info.role === 'misc' ? 'misc' : 'global' }
|
|
34
|
+
: env.handoverFor(exec);
|
|
35
|
+
if (!target || !target.path) {
|
|
36
|
+
return { text: [
|
|
37
|
+
'没有可用的交接路径:' + (target && target.reason ? target.reason : '未知'),
|
|
38
|
+
'先指定角色(设置页的 dsh-handover 中「会话角色」),或显式传 handover_path="<交接文件绝对路径>"。',
|
|
39
|
+
'压缩会丢历史,没有合格交接就不排压缩。',
|
|
40
|
+
].join(NL) };
|
|
41
|
+
}
|
|
42
|
+
const file = target.path;
|
|
43
|
+
const st = typeof env.compactionState === "function" ? env.compactionState(exec) : null;
|
|
44
|
+
const count = st ? { used: st.used, lock: st.lock } : countCompactions(eventsOf(exec));
|
|
45
|
+
const self = env.selfPct(meId);
|
|
46
|
+
const ratio = self === null ? null : self / 100;
|
|
47
|
+
const out = [];
|
|
48
|
+
const recentSec = st && st.lastSummaryMs ? (Date.now() - st.lastSummaryMs) / 1000 : null;
|
|
49
|
+
if (recentSec !== null && recentSec < 120 && args.force !== true) {
|
|
50
|
+
out.push("拒绝排压缩:" + Math.round(recentSec) + " 秒前刚完成一次压缩(日志里有 compaction/summary),避免重复排程。");
|
|
51
|
+
out.push("等一轮让投影缓存刷新;确实要再压一次请加 force=true。");
|
|
52
|
+
return { text: out.join(NL) };
|
|
53
|
+
}
|
|
54
|
+
if (count.lock === "open") {
|
|
55
|
+
out.push("拒绝排压缩:检测到未闭合的压缩锁(compaction/start 无配对 end)。");
|
|
56
|
+
out.push("处理:等当前压缩结束后用 ho_phase 复查;若长时间不动,说明上次压缩崩在中间。");
|
|
57
|
+
return { text: out.join(NL) };
|
|
58
|
+
}
|
|
59
|
+
const adv = decide({ ratio, compactionsUsed: count.used, cfg: env.cfg, lockState: count.lock });
|
|
60
|
+
if (adv.action === "new_session") {
|
|
61
|
+
out.push("不建议压缩:" + adv.reason);
|
|
62
|
+
out.push('下一步:用 ho_handover(action=write) 写交接,再由全局 ho_sessions(action="new") 建新会话。');
|
|
63
|
+
return { text: out.join(NL) };
|
|
64
|
+
}
|
|
65
|
+
if (adv.action === "pause" && args.at_risk === true && args.force !== true) {
|
|
66
|
+
out.push("已暂停(关键步骤):" + adv.reason);
|
|
67
|
+
out.push("到阶段边界再调一次 ho_checkpoint(不带 at_risk 或带 force)。");
|
|
68
|
+
return { text: out.join(NL) };
|
|
69
|
+
}
|
|
70
|
+
if (adv.action === "continue" && args.force !== true) {
|
|
71
|
+
out.push("水位未到线(" + (ratio === null ? "读不到" : (Math.round(ratio * 1000) / 10).toFixed(1) + "%") + "):不压。");
|
|
72
|
+
out.push("需要强制演练可加 force=true。");
|
|
73
|
+
return { text: out.join(NL) };
|
|
74
|
+
}
|
|
75
|
+
// 判断逻辑不动;只有判定可以压缩之后,才停下来问用户。
|
|
76
|
+
if (args.confirm !== true) {
|
|
77
|
+
out.push('判定:可以压缩(' + adv.reason + ')。');
|
|
78
|
+
out.push('水位 ' + (ratio === null ? '读不到' : (Math.round(ratio * 1000) / 10).toFixed(1) + '%') + ',已压缩 ' + count.used + '/3。');
|
|
79
|
+
out.push('压缩会丢历史并自动续跑 —— 先问用户「要压缩吗?」,回答同意后再调 ho_checkpoint(confirm=true);不同意就保持原样。');
|
|
80
|
+
return { text: out.join(NL) };
|
|
81
|
+
}
|
|
82
|
+
let wrote = null;
|
|
83
|
+
if (typeof args.handover_text === "string" && args.handover_text.trim() !== "") {
|
|
84
|
+
wrote = writeHandover(ws, file, args.handover_text, "write", { kind: target.kind, info });
|
|
85
|
+
out.push("交接已写入 " + file + "(" + wrote.bytes + " B,备份:" + (wrote.backup || "无") + ")· 角色 " + info.role);
|
|
86
|
+
out.push("体检 " + wrote.verdict + (wrote.reasons.length ? ":" : " · 结构齐备"));
|
|
87
|
+
for (const r of wrote.reasons) out.push(" - " + r);
|
|
88
|
+
if (wrote.verdict !== "通过") {
|
|
89
|
+
out.push("体检不通过 ⇒ 不排压缩(先补结构,压缩后就没机会改了)。");
|
|
90
|
+
return { text: out.join(NL) };
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
const cur = readHandover(file);
|
|
94
|
+
if (cur.text === null || cur.text.trim() === "") {
|
|
95
|
+
out.push("没有交接正文,磁盘上也没有:" + file);
|
|
96
|
+
out.push("拒绝压缩 —— 压缩会丢历史,必须先有交接。");
|
|
97
|
+
return { text: out.join(NL) };
|
|
98
|
+
}
|
|
99
|
+
out.push("沿用磁盘上已有交接:" + file);
|
|
100
|
+
}
|
|
101
|
+
// 严格闸门:confirm=true 只表示意图,是否同意以用户点过授权为准。
|
|
102
|
+
// 任何角色(全局、项目、杂项)都靠同一条判据:用户是否在设置页点过授权。
|
|
103
|
+
// 会话自行判断水位与询问用户,用户点授权后自行排压缩,不必上报全局。
|
|
104
|
+
const cs = checkConsent(meId);
|
|
105
|
+
if (!cs.ok) {
|
|
106
|
+
out.push('交接已写好,但**没有排压缩**:' + cs.reason + '。');
|
|
107
|
+
out.push('授权方式(任选其一):① 在聊天框发送 /allow-compact(最快,仅授权本会话);② 在设置页的 dsh-handover 中点击「压缩授权」一次。授权有效期 5 分钟,其后须再调用 ho_checkpoint(confirm=true)。');
|
|
108
|
+
out.push('如此设限的原因:压缩会丢弃历史,而参数由 AI 自行填写——惟有用户在设置页当面点击的那一次,方视为同意。');
|
|
109
|
+
return { text: out.join(NL) };
|
|
110
|
+
}
|
|
111
|
+
// 成本提示:低于交接线强行压缩会白付一次摘要,且本轮前缀缓存失效。
|
|
112
|
+
const handoverRatio = (env.cfg && env.cfg.handoverRatio) || 0.35;
|
|
113
|
+
if (ratio !== null && ratio < handoverRatio) {
|
|
114
|
+
out.push('注意成本:当前水位 ' + (Math.round(ratio * 1000) / 10) + '% 低于交接线 ' + Math.round(handoverRatio * 100) + '% —— 属于提前压缩,会白付一次摘要调用,且本轮前缀缓存失效。');
|
|
115
|
+
}
|
|
116
|
+
consumedConsent(meId);
|
|
117
|
+
const n = count.used + 1;
|
|
118
|
+
env.schedule(meId, n);
|
|
119
|
+
out.push('用户已授权(' + cs.atText + ' 点击,剩 ' + Math.round(cs.leftMs / 1000) + ' 秒)。');
|
|
120
|
+
out.push("已登记压缩(第 " + n + "/" + env.cfg.maxCompactions + " 次)。本回合结束后执行,完成后会自动收到续跑消息。");
|
|
121
|
+
out.push("压缩期间不要结束会话;如果 60 秒内没有续跑消息,用 ho_phase 看状态。");
|
|
122
|
+
return { text: out.join(NL) };
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** ho_diag —— 诊断:服务是否可解析、调度器是否就绪、配置是否如预期。不写任何东西。 */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { nameOf } from '../lib/names.mjs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { textOutput } from '../lib/output.mjs';
|
|
6
|
+
import { NL } from '../lib/text.mjs';
|
|
7
|
+
import { eventsOf, countCompactions } from '../lib/events.mjs';
|
|
8
|
+
|
|
9
|
+
export function pmDiagTool(env, state) {
|
|
10
|
+
return {
|
|
11
|
+
name: nameOf('diag'),
|
|
12
|
+
description:
|
|
13
|
+
'插件自检:服务、调度器、压缩计数、工作区是否接入。只读。',
|
|
14
|
+
parameters: { type: "object", additionalProperties: false, properties: {} },
|
|
15
|
+
output: textOutput(),
|
|
16
|
+
async execute(args, exec) {
|
|
17
|
+
const out = [];
|
|
18
|
+
const meId = env.selfId(exec);
|
|
19
|
+
out.push("会话 " + (meId || "?"));
|
|
20
|
+
out.push("调度器 " + (state.schedulerReady() ? "就绪" : "未就绪" + (state.schedulerError() ? ":" + state.schedulerError() : "(稍后重试或重启)")));
|
|
21
|
+
if (typeof env.injectList === 'function') out.push('inject ' + env.injectList().join(' / '));
|
|
22
|
+
if (typeof env.initErrors === 'function') {
|
|
23
|
+
const errs = env.initErrors();
|
|
24
|
+
out.push('初始化 ' + (errs.length ? '有 ' + errs.length + ' 处失败:' + errs.join(';') : '无错误'));
|
|
25
|
+
}
|
|
26
|
+
if (typeof env.probeServices === "function") {
|
|
27
|
+
const svc = env.probeServices();
|
|
28
|
+
out.push("服务 " + Object.keys(svc).map((k) => k + "=" + (svc[k] ? "可用" : "—")).join(" "));
|
|
29
|
+
}
|
|
30
|
+
if (typeof env.guardLast === "function") {
|
|
31
|
+
const g = env.guardLast();
|
|
32
|
+
out.push("唤醒判定 " + (g
|
|
33
|
+
? (g.allow ? "放行" : "拦截") + " · 依据 " + g.source + (g.title ? " · 标题 " + g.title : "") + " · 会话 " + g.sid
|
|
34
|
+
: "本进程还没有 peer_send、peer_new 调用"));
|
|
35
|
+
}
|
|
36
|
+
if (typeof env.probeAgentScope === "function") {
|
|
37
|
+
const a = env.probeAgentScope(exec);
|
|
38
|
+
const show = ["compaction", "compactNow", "tokenMeter", "sessions", "llm", "agents"]
|
|
39
|
+
.map((k) => k + "=" + (a[k] ? "可用" : "—")).join(" ");
|
|
40
|
+
out.push("会话作用域 " + (a.ctx ? show : "无法获取 exec.agent.ctx"));
|
|
41
|
+
}
|
|
42
|
+
const st = typeof env.compactionState === "function"
|
|
43
|
+
? env.compactionState(exec)
|
|
44
|
+
: (() => { const c = countCompactions(eventsOf(exec)); return { used: c.used, lock: c.lock, source: 'events' }; })();
|
|
45
|
+
out.push("压缩 " + st.used + "/" + env.cfg.maxCompactions + " 次;锁 " + st.lock + ";来源 " + st.source);
|
|
46
|
+
out.push("水位 " + (env.selfPct(meId) === null ? "读不到" : env.selfPct(meId).toFixed(1) + "%") + (typeof env.staleNote === "function" ? env.staleNote(exec) : ""));
|
|
47
|
+
const ws = env.resolveWs(exec);
|
|
48
|
+
let wsOk = false;
|
|
49
|
+
try { wsOk = fs.existsSync(path.join(ws, '_Control_Center')); } catch { wsOk = false; }
|
|
50
|
+
if (typeof env.roleOf === "function") {
|
|
51
|
+
try {
|
|
52
|
+
const info = env.roleOf(exec);
|
|
53
|
+
const ts = typeof env.toolsetOf === 'function' ? env.toolsetOf(info.role) : null;
|
|
54
|
+
out.push("角色 " + info.role + (info.project ? "(项目 " + info.project + ")" : info.task ? "(任务 " + info.task + ")" : "") + " · 依据 " + info.source + (ts ? " · 可见工具 " + ts.allow.length + "/" + (ts.allow.length + ts.deny.length) : ""));
|
|
55
|
+
if (typeof env.roleState === "function") {
|
|
56
|
+
const rs = env.roleState();
|
|
57
|
+
out.push("角色收窄 " + (rs.gating || "未触发") + (rs.last ? " · 最近:" + rs.last.sid + " → " + rs.last.role + "(" + rs.last.at + ")" : ""));
|
|
58
|
+
if (rs.bootstrap) out.push("一次性引导 " + rs.bootstrap);
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {
|
|
61
|
+
out.push("角色 判定失败:" + (e && e.message ? e.message : e));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
out.push("工作区 " + ws + (wsOk
|
|
65
|
+
? " 已接入"
|
|
66
|
+
: " — 未接入(未找到 _Control_Center):让用户走设置页「首次安装」,或本会话直接 ho_init(parent=\"<目录>\", register=true)"));
|
|
67
|
+
try {
|
|
68
|
+
const mv = typeof env.moveInfo === "function" ? env.moveInfo() : null;
|
|
69
|
+
if (mv) out.push("迁移 " + mv.phase + " · " + mv.src + (mv.dst ? " → " + mv.dst : ""));
|
|
70
|
+
} catch { /* ignore */ }
|
|
71
|
+
out.push("配置 warn=" + env.cfg.warnRatio + " handover=" + env.cfg.handoverRatio + " hard=" + env.cfg.hardRatio + " max=" + env.cfg.maxCompactions);
|
|
72
|
+
if (typeof env.handoverFor === "function") {
|
|
73
|
+
try {
|
|
74
|
+
const hp = env.handoverFor(exec);
|
|
75
|
+
out.push("交接路径 " + (hp && hp.path ? hp.path + "(" + hp.kind + ")" : "无 —— " + ((hp && hp.reason) || "角色未定")));
|
|
76
|
+
} catch (e) {
|
|
77
|
+
out.push("交接路径 解析失败:" + (e && e.message ? e.message : e));
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
out.push("交接路径 " + (env.cfg.handoverPath || "(按角色解析)"));
|
|
81
|
+
}
|
|
82
|
+
return { text: out.join(NL) };
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** ho_drift —— 交接漂移体检(替代 handover_check.mjs)。 */
|
|
2
|
+
import { textOutput } from '../lib/output.mjs';
|
|
3
|
+
import { nameOf } from '../lib/names.mjs';
|
|
4
|
+
import { NL } from '../lib/text.mjs';
|
|
5
|
+
import { computeDrift, driftVerdict } from '../lib/drift.mjs';
|
|
6
|
+
|
|
7
|
+
export function pmDriftTool(env) {
|
|
8
|
+
return {
|
|
9
|
+
name: nameOf('drift'),
|
|
10
|
+
description:
|
|
11
|
+
'交接漂移体检:Δ=当前轮−交接轮,超过容忍轮数(默认 3)需刷新交接。只读。',
|
|
12
|
+
parameters: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
properties: {
|
|
16
|
+
slack: { type: 'number', description: '容忍轮数(默认 3)' },
|
|
17
|
+
all: { type: 'boolean', description: '列全部会话,而不只是每个项目的在任会话' },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
output: textOutput(),
|
|
21
|
+
async execute(args, exec) {
|
|
22
|
+
const ws = env.resolveWs(exec);
|
|
23
|
+
const res = computeDrift({
|
|
24
|
+
ws,
|
|
25
|
+
cacheDir: env.cacheDir(),
|
|
26
|
+
slack: typeof args.slack === 'number' ? args.slack : env.cfg.driftSlack,
|
|
27
|
+
all: args.all === true,
|
|
28
|
+
});
|
|
29
|
+
const out = [];
|
|
30
|
+
out.push('交接匹配检查(跟着交接走;冗余 ' + res.slack + ' 轮) 工作区根 ' + ws);
|
|
31
|
+
out.push(' 项目 会话类型 会话 当前轮 交接轮 Δ 判定');
|
|
32
|
+
for (const r of res.rows) {
|
|
33
|
+
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];
|
|
34
|
+
out.push(' ' + cells.join(' '));
|
|
35
|
+
}
|
|
36
|
+
if (!res.rows.length) out.push(' 读不到任何项目会话(Projects/ 下对应库不存在时属正常)');
|
|
37
|
+
out.push(driftVerdict(res));
|
|
38
|
+
return { text: out.join(NL) };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** ho_gate —— 通用约束条闸门 + 交接文档体检。 */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { nameOf } from '../lib/names.mjs';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { textOutput } from '../lib/output.mjs';
|
|
6
|
+
import { NL } from '../lib/text.mjs';
|
|
7
|
+
import { syncCheck, renderSyncCheck } from '../lib/gates.mjs';
|
|
8
|
+
import { lintGlobal, lintProject, renderLintGlobal, renderLintProject } from '../lib/lint.mjs';
|
|
9
|
+
import { SETUP_STEPS } from '../lib/guide.mjs';
|
|
10
|
+
|
|
11
|
+
function lintAllProjects(ws) {
|
|
12
|
+
const meta = path.join(ws, "_Project_Meta");
|
|
13
|
+
const rows = [];
|
|
14
|
+
let dirs = [];
|
|
15
|
+
try { dirs = fs.readdirSync(meta).sort(); } catch { return rows; }
|
|
16
|
+
for (const name of dirs) {
|
|
17
|
+
const dir = path.join(meta, name);
|
|
18
|
+
try { if (!fs.statSync(dir).isDirectory()) continue; } catch { continue; }
|
|
19
|
+
const f = path.join(dir, "Handover.md");
|
|
20
|
+
rows.push({ name, file: f, res: lintProject(f) });
|
|
21
|
+
}
|
|
22
|
+
return rows;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function pmGateTool(env) {
|
|
26
|
+
return {
|
|
27
|
+
name: nameOf('gate'),
|
|
28
|
+
description:
|
|
29
|
+
'通用约束条闸门(按库型判定)+交接文档体检。只读。',
|
|
30
|
+
parameters: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
properties: {
|
|
34
|
+
scope: { type: 'string', enum: ['all', 'global', 'project'], description: '默认 all' },
|
|
35
|
+
project: { type: 'string', description: 'scope=project 时的项目名' },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
output: textOutput(),
|
|
39
|
+
async execute(args, exec) {
|
|
40
|
+
const ws = env.resolveWs(exec);
|
|
41
|
+
const scope = args.scope || "all";
|
|
42
|
+
if (scope !== "all" && scope !== "global" && scope !== "project") {
|
|
43
|
+
throw new Error("scope 只能是 all | global | project(收到 " + String(scope) + ")");
|
|
44
|
+
}
|
|
45
|
+
const out = [];
|
|
46
|
+
if (scope === "all") {
|
|
47
|
+
const g = syncCheck(ws);
|
|
48
|
+
if (g.noWorkspace) {
|
|
49
|
+
out.push('通用约束条闸门:未接入工作区(' + g.path + ')');
|
|
50
|
+
out.push('把工作区接入就好,一共四步:');
|
|
51
|
+
SETUP_STEPS.forEach((s, i) => out.push(' ' + (i + 1) + '. ' + s));
|
|
52
|
+
return { text: out.join(NL) };
|
|
53
|
+
}
|
|
54
|
+
out.push(renderSyncCheck(g).join(NL));
|
|
55
|
+
out.push(renderLintProject(null, lintAllProjects(ws)).join(NL));
|
|
56
|
+
} else if (scope === "project") {
|
|
57
|
+
const name = args.project;
|
|
58
|
+
if (!name) throw new Error("scope=project 时必须给 project 参数");
|
|
59
|
+
const f = path.join(ws, "_Project_Meta", name, "Handover.md");
|
|
60
|
+
out.push(renderLintProject(name, [{ name, file: f, res: lintProject(f) }]).join(NL));
|
|
61
|
+
}
|
|
62
|
+
if (scope === "all" || scope === "global") {
|
|
63
|
+
const p = path.join(ws, "_Control_Center", "records", "Global_Handover.md");
|
|
64
|
+
out.push(renderLintGlobal(p, lintGlobal(p)).join(NL));
|
|
65
|
+
}
|
|
66
|
+
return { text: out.join(NL + NL) };
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** ho_handover —— 读/写/追加/归档**本会话自己的**交接(路径按角色解析;写前备份、写后按角色体检)。 */
|
|
2
|
+
import { textOutput } from '../lib/output.mjs';
|
|
3
|
+
import { nameOf } from '../lib/names.mjs';
|
|
4
|
+
import { NL } from '../lib/text.mjs';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { readHandover, writeHandover } from '../lib/handover-io.mjs';
|
|
9
|
+
import { archiveDirFor } from '../lib/role.mjs';
|
|
10
|
+
|
|
11
|
+
function noPath(input) {
|
|
12
|
+
const reason = input && input.reason ? input.reason : '未知原因';
|
|
13
|
+
return [
|
|
14
|
+
'没有可用的交接路径:' + reason,
|
|
15
|
+
'两条处置:',
|
|
16
|
+
' ① 在设置页的 dsh-handover 中「会话角色」给本会话指定角色(项目角色要填项目名);',
|
|
17
|
+
' ② 临时显式传 path="<交接文件绝对路径>"。',
|
|
18
|
+
'(插件不再默认读全局交接——那是以前会串角色的老行为。)',
|
|
19
|
+
].join(NL);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function pmHandoverTool(env) {
|
|
23
|
+
return {
|
|
24
|
+
name: nameOf('handover'),
|
|
25
|
+
description:
|
|
26
|
+
'读写、追加、归档本会话自己的交接;写前备份、写后按角色体检。',
|
|
27
|
+
parameters: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
required: ['action'],
|
|
31
|
+
properties: {
|
|
32
|
+
action: { type: 'string', enum: ['read', 'write', 'append', 'archive'], description: 'read | write | append | archive' },
|
|
33
|
+
text: { type: 'string', description: 'write/append 时的正文(read/archive 忽略)' },
|
|
34
|
+
path: { type: 'string', description: '覆盖默认路径(演练或强制指定时用)' },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
output: textOutput(),
|
|
38
|
+
async execute(args, exec) {
|
|
39
|
+
const ws = env.resolveWs(exec);
|
|
40
|
+
const info = env.roleOf(exec);
|
|
41
|
+
const target = args.path
|
|
42
|
+
? { path: args.path, kind: info.role === 'project' ? 'project' : info.role === 'misc' ? 'misc' : 'global', role: info.role, project: info.project, task: info.task }
|
|
43
|
+
: env.handoverFor(exec);
|
|
44
|
+
if (!target || !target.path) return { text: noPath(target) };
|
|
45
|
+
|
|
46
|
+
if (args.action === "read") {
|
|
47
|
+
const r = readHandover(target.path);
|
|
48
|
+
if (r.text === null) return { text: "交接文档不存在:" + target.path + NL + "(第一次写用 action=write 或 action=append)" };
|
|
49
|
+
return { text: r.text };
|
|
50
|
+
}
|
|
51
|
+
if (args.action === "archive") {
|
|
52
|
+
const src = readHandover(target.path);
|
|
53
|
+
if (src.text === null) throw new Error("要归档的交接不存在:" + target.path);
|
|
54
|
+
const d = new Date();
|
|
55
|
+
const p = (n) => (n < 10 ? "0" + n : "" + n);
|
|
56
|
+
const stampText = d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + "_" + p(d.getHours()) + p(d.getMinutes());
|
|
57
|
+
const dir = archiveDirFor(ws, info);
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
const base = info.role === 'project' ? (info.project + '_Handover_') : info.role === 'misc' ? ((info.task || 'task') + '_Handover_') : 'Global_Handover_';
|
|
60
|
+
const dest = path.join(dir, base + stampText + ".md");
|
|
61
|
+
if (fs.existsSync(dest)) throw new Error("归档目标已存在,不覆盖:" + dest);
|
|
62
|
+
fs.copyFileSync(target.path, dest);
|
|
63
|
+
const a = crypto.createHash("sha256").update(fs.readFileSync(target.path)).digest("hex");
|
|
64
|
+
const b = crypto.createHash("sha256").update(fs.readFileSync(dest)).digest("hex");
|
|
65
|
+
const out = [];
|
|
66
|
+
out.push("已归档 " + dest + "(" + fs.statSync(dest).size + " B)");
|
|
67
|
+
out.push("sha256 " + (a === b ? "一致 " + a.slice(0, 16) + "…" : "**不一致,归档失败**"));
|
|
68
|
+
return { text: out.join(NL) };
|
|
69
|
+
}
|
|
70
|
+
if (args.action !== "write" && args.action !== "append") {
|
|
71
|
+
throw new Error("未知 action:" + String(args.action) + "(只能是 read | write | append | archive)");
|
|
72
|
+
}
|
|
73
|
+
if (typeof args.text !== "string" || args.text.trim() === "") throw new Error("write/append 需要 text");
|
|
74
|
+
const res = writeHandover(ws, target.path, args.text, args.action === "append" ? "append" : "write", { kind: target.kind, info });
|
|
75
|
+
const out = [];
|
|
76
|
+
out.push("已写入 " + res.path + "(" + res.bytes + " B)· 角色 " + info.role + (target.kind !== 'global' ? "(" + (info.project || info.task) + ")" : ""));
|
|
77
|
+
out.push("备份 " + (res.backup || "(原文件不存在,无需备份)"));
|
|
78
|
+
out.push("体检 " + res.verdict + (res.reasons.length ? "" : " · 结构齐备"));
|
|
79
|
+
for (const r of res.reasons) out.push(" - " + r);
|
|
80
|
+
return { text: out.join(NL) };
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
}
|