@mingmaple/dsh-handover 0.9.6 → 0.9.13
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 +53 -17
- package/docs/migrate.md +1 -1
- package/docs/releases/0.9.12.md +28 -0
- package/docs/releases/0.9.13.md +30 -0
- package/docs/troubleshoot.md +43 -6
- package/package.json +70 -57
- package/scripts/blank-dsh.mjs +6 -5
- package/scripts/blank-smoke.mjs +268 -58
- package/scripts/build.mjs +3 -0
- package/scripts/check-docs.mjs +23 -2
- package/scripts/office.mjs +81 -0
- package/src/client.js +92 -23
- package/src/index.mjs +264 -145
- package/src/lib/api.mjs +67 -9
- package/src/lib/config.mjs +4 -3
- package/src/lib/consent.mjs +59 -20
- package/src/lib/facts.mjs +5 -4
- package/src/lib/guide.mjs +14 -7
- package/src/lib/handover-io.mjs +20 -0
- package/src/lib/manual.mjs +23 -12
- package/src/lib/names.mjs +2 -2
- package/src/lib/office.mjs +272 -0
- package/src/lib/pack-ops.mjs +3 -0
- package/src/lib/phase.mjs +6 -4
- package/src/lib/prompt.mjs +18 -0
- package/src/lib/role.mjs +52 -18
- package/src/lib/schedule.mjs +10 -14
- package/src/lib/session-create.mjs +42 -0
- package/src/lib/session-ops.mjs +183 -79
- package/src/lib/sessions.mjs +9 -8
- package/src/lib/status.mjs +8 -2
- package/src/lib/succession.mjs +244 -0
- package/src/lib/text.mjs +27 -0
- package/src/lib/webroute.mjs +1 -1
- package/src/lib/workspace.mjs +1 -1
- package/src/selftest.mjs +15 -0
- package/src/templates/Charter.skeleton.md +1 -1
- package/src/templates/Prompt_Templates.md +9 -17
- package/src/templates/Runtime_Manual.md +17 -14
- package/src/templates/rules/Handover_Rules.md +3 -2
- package/src/templates/rules/Misc_Task_Standard.md +3 -3
- package/src/tools/ho_checkpoint.mjs +11 -4
- package/src/tools/ho_diag.mjs +5 -3
- package/src/tools/ho_handover.mjs +7 -6
- package/src/tools/ho_office.mjs +59 -0
- package/src/tools/ho_profile.mjs +10 -2
- package/src/tools/ho_sessions.mjs +3 -2
- package/src/tools/ho_snapshot.mjs +2 -1
- package/src/tools/ho_succeed.mjs +24 -0
- package/src/tools/ho_workspace.mjs +7 -3
package/src/lib/api.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { createGlobalWorkspace } from './scaffold.mjs';
|
|
|
11
11
|
import { probeMove, startMove, moveStatus, cleanupOld, stageSwitch, repairLink, stageRetarget, applyRetarget } from './mover.mjs';
|
|
12
12
|
import { routeWorkspace } from './workspace.mjs';
|
|
13
13
|
import { grantConsent, listConsent, consumedConsent } from './consent.mjs';
|
|
14
|
+
import { newSessionIn } from './session-create.mjs';
|
|
14
15
|
|
|
15
16
|
function msgOf(e) { return String(e && e.message ? e.message : e); }
|
|
16
17
|
|
|
@@ -53,16 +54,15 @@ export function statusSnapshot(env, info, why) {
|
|
|
53
54
|
return json;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
/**
|
|
57
|
-
function
|
|
57
|
+
/** 允许在设置页与聊天命令里填短号:session-xxxx… 前 8 位即可,这里补全成完整 id。 */
|
|
58
|
+
export function resolveShortSid(env, input) {
|
|
58
59
|
const s = String(input || '').trim();
|
|
59
60
|
if (!s) return '';
|
|
60
61
|
if (s.indexOf('session-') === 0) return s;
|
|
61
62
|
try {
|
|
62
63
|
const dir = typeof env.cacheDir === 'function' ? env.cacheDir() : '';
|
|
63
64
|
if (dir) {
|
|
64
|
-
const
|
|
65
|
-
for (const name of fsMod.readdirSync(dir)) {
|
|
65
|
+
for (const name of fs.readdirSync(dir)) {
|
|
66
66
|
if (!name.endsWith('.json')) continue;
|
|
67
67
|
const stem = name.replace('.json', '');
|
|
68
68
|
if (stem.indexOf('session-' + s) === 0) return stem;
|
|
@@ -73,6 +73,25 @@ function resolveConsentSid(env, input) {
|
|
|
73
73
|
return s;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** 在已注册的工作区里建一个「全局」会话:先按工作区 id,抓不到再用 cwd。 */
|
|
77
|
+
async function createGlobalSession(env, rootPath, title) {
|
|
78
|
+
let workspaceId = '';
|
|
79
|
+
try {
|
|
80
|
+
const reg = env.registry && env.registry();
|
|
81
|
+
const list = reg && typeof reg.list === 'function' ? (reg.list() || []) : [];
|
|
82
|
+
const norm = (s) => String(s || '').replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
83
|
+
const hit = list.find((w) => w && norm(w.path) === norm(rootPath));
|
|
84
|
+
if (hit) workspaceId = String(hit.id || '');
|
|
85
|
+
} catch { workspaceId = ''; }
|
|
86
|
+
const made = await newSessionIn(env, {
|
|
87
|
+
workspaceId,
|
|
88
|
+
cwd: workspaceId ? '' : rootPath,
|
|
89
|
+
title: title || '全局',
|
|
90
|
+
role: 'global',
|
|
91
|
+
});
|
|
92
|
+
return { sessionId: made.sessionId || '', error: made.error || '', workspaceId };
|
|
93
|
+
}
|
|
94
|
+
|
|
76
95
|
export function createApi(env, info) {
|
|
77
96
|
const ok = (value) => ({ ok: true, value });
|
|
78
97
|
const bad = (code, message, http) => ({ ok: false, error: { code, message, details: {} }, http: http || 400 });
|
|
@@ -92,6 +111,44 @@ export function createApi(env, info) {
|
|
|
92
111
|
return r && r.ok ? ok(r) : { ok: false, error: { code: 'setup-failed', message: String((r && r.error) || '建立失败'), details: {} }, http: 400 };
|
|
93
112
|
}
|
|
94
113
|
|
|
114
|
+
if (method === 'deploy') {
|
|
115
|
+
// 一键部署:建文件体系 → 注册「全局」工作区 → 建「全局」会话(三件事一次做完)。
|
|
116
|
+
const scaffold = createGlobalWorkspace(p.parent, { name: p.name });
|
|
117
|
+
if (!scaffold || scaffold.ok !== true) {
|
|
118
|
+
return { ok: false, error: { code: 'setup-failed', message: String((scaffold && scaffold.error) || '建立失败'), details: {} }, http: 400 };
|
|
119
|
+
}
|
|
120
|
+
let ws = null;
|
|
121
|
+
let workspaceError = '';
|
|
122
|
+
try {
|
|
123
|
+
const reg = env.registry && env.registry();
|
|
124
|
+
if (reg && typeof reg.create === 'function') ws = await reg.create(scaffold.root, '全局');
|
|
125
|
+
else workspaceError = 'workspaceRegistry 不可用';
|
|
126
|
+
} catch (e) { workspaceError = '注册工作区失败:' + msgOf(e); }
|
|
127
|
+
const made = await createGlobalSession(env, scaffold.root, '全局');
|
|
128
|
+
invalidateStatus(env);
|
|
129
|
+
env.log('一键部署:体系 ' + scaffold.root + ';工作区 ' + (ws ? String(ws.id) : '失败') + ';会话 ' + (made.sessionId || '失败'));
|
|
130
|
+
return ok({
|
|
131
|
+
root: scaffold.root,
|
|
132
|
+
created: scaffold.created || [],
|
|
133
|
+
written: scaffold.written || [],
|
|
134
|
+
workspace: ws ? { id: String(ws.id || ''), title: String(ws.title || '全局'), path: String(ws.path || scaffold.root) } : null,
|
|
135
|
+
workspaceError,
|
|
136
|
+
sessionId: made.sessionId || '',
|
|
137
|
+
sessionError: made.error || '',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (method === 'global-session') {
|
|
142
|
+
// 已接入工作区但没有全局会话时,补建一个(不建体系、不动既有文件)。
|
|
143
|
+
const root = currentRoot(env);
|
|
144
|
+
if (!root) return bad('no-workspace', '未找到全局根(工作区未接入)');
|
|
145
|
+
const made = await createGlobalSession(env, root, p.title || '全局');
|
|
146
|
+
invalidateStatus(env);
|
|
147
|
+
if (!made.sessionId) return bad('session-failed', made.error || '建会话失败');
|
|
148
|
+
env.log('补建全局会话:' + made.sessionId);
|
|
149
|
+
return ok({ root, sessionId: made.sessionId });
|
|
150
|
+
}
|
|
151
|
+
|
|
95
152
|
if (method === 'register') {
|
|
96
153
|
const target = String(p.path || '').trim();
|
|
97
154
|
if (!target) return bad('bad-request', '缺少 path(已存在的绝对目录)');
|
|
@@ -181,10 +238,11 @@ export function createApi(env, info) {
|
|
|
181
238
|
}
|
|
182
239
|
|
|
183
240
|
if (method === 'consent.grant') {
|
|
184
|
-
const sid =
|
|
241
|
+
const sid = resolveShortSid(env, p.session_id);
|
|
185
242
|
if (!sid) return bad('bad-request', '缺少 session_id');
|
|
186
|
-
const
|
|
187
|
-
|
|
243
|
+
const kind = p.kind === 'succeed' ? 'succeed' : 'compact';
|
|
244
|
+
const g = grantConsent(sid, kind === 'succeed' ? '设置页点击换任' : '设置页点击', kind);
|
|
245
|
+
env.log((kind === 'succeed' ? '换任' : '压缩') + '授权:' + sid + '(5 分钟)');
|
|
188
246
|
invalidateStatus(env);
|
|
189
247
|
return ok(g);
|
|
190
248
|
}
|
|
@@ -193,7 +251,7 @@ export function createApi(env, info) {
|
|
|
193
251
|
|
|
194
252
|
if (method === 'consent.revoke') {
|
|
195
253
|
const sid = String(p.session_id || '').trim();
|
|
196
|
-
if (sid) consumedConsent(sid);
|
|
254
|
+
if (sid) consumedConsent(sid, p.kind === 'succeed' ? 'succeed' : 'compact');
|
|
197
255
|
invalidateStatus(env);
|
|
198
256
|
return ok({ revoked: sid });
|
|
199
257
|
}
|
|
@@ -230,5 +288,5 @@ export function createApi(env, info) {
|
|
|
230
288
|
}
|
|
231
289
|
}
|
|
232
290
|
|
|
233
|
-
return { call, methods: ['status', 'setup', 'register', 'extra-prompt', 'role', 'move.probe', 'move.start', 'move.switch', 'move.status', 'move.retarget', 'move.retarget-stage', 'move.repair', 'move.cleanup', 'consent.grant', 'consent.list', 'consent.revoke'] };
|
|
291
|
+
return { call, methods: ['status', 'setup', 'deploy', 'global-session', 'register', 'extra-prompt', 'role', 'move.probe', 'move.start', 'move.switch', 'move.status', 'move.retarget', 'move.retarget-stage', 'move.repair', 'move.cleanup', 'consent.grant', 'consent.list', 'consent.revoke'] };
|
|
234
292
|
}
|
package/src/lib/config.mjs
CHANGED
|
@@ -7,17 +7,18 @@ export const DEFAULTS = {
|
|
|
7
7
|
maxCompactions: 3,
|
|
8
8
|
autoResume: true,
|
|
9
9
|
sessionScanHours: 12,
|
|
10
|
-
libraries: [],
|
|
11
10
|
handoverPath: '', // 留空 = 按会话角色解析(全局 records/Global_Handover.md;项目 _Project_Meta/<项目>/Handover.md;杂项 _Misc/tasks/<任务>/Handover.md)。非空则强制该路径(演练用)
|
|
12
11
|
rolesPath: '', // 留空 = ~/.dsh/storages/dsh-handover.roles.json
|
|
13
12
|
// 续跑提示:{n} {max} {handover} {role} 会被替换成真值;{handover} 是本会话自己的交接绝对路径。
|
|
14
|
-
|
|
13
|
+
// 只在助手登记压缩时选 resume=continue(默认)才投递;resume=wait 表示已收尾,不唤醒。
|
|
14
|
+
resumePrompt: '【dsh-handover 自动续跑·第 {n}/{max} 次压缩】压缩已完成,历史已被摘要替换。第一步:ho_handover(action=read) 取回本会话的交接({handover});然后按 §2「下一步」继续(若 §2 已无待办,向用户一句话收尾并停止)。不要复述交接内容,不要重做已完成的事。完整流程:ho_charter(doc="runtime")。',
|
|
15
15
|
resumePromptUnknown: '【dsh-handover 自动续跑·第 {n}/{max} 次压缩】压缩已完成,历史已被摘要替换。本会话是普通会话,不参与交接;如需继续,请由全局会话为其指定角色。',
|
|
16
16
|
compactTimeoutMs: 120000,
|
|
17
17
|
compactAttempts: 3,
|
|
18
18
|
waitIdleMs: 180000, // 等会话空闲的上限(超过就放弃并回告)
|
|
19
19
|
driftSlack: 3, // Δ交接容忍轮数(超过即需刷交接)
|
|
20
|
-
wakeAuthority: 'on', //
|
|
20
|
+
wakeAuthority: 'on', // 会话守卫硬拦:on / off(off=连 peer_new 也不拦)
|
|
21
|
+
directPeer: 'on', // 会话之间直接 peer_send:on 允许(不经全局转述);off 退回「只能向全局报告」的旧口径
|
|
21
22
|
roleGating: 'on', // 按角色裁剪工具与提示段:on / off(off=所有会话都看得见全部工具)
|
|
22
23
|
reuseCheck: true, // 建新会话前查在任会话水位(低于 warnRatio 拒绝)
|
|
23
24
|
prepackMinAgeMin: 90, // 打包前要求会话静止分钟数
|
package/src/lib/consent.mjs
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 一次性授权:压缩会丢历史,换任会换掉在任会话,所以「同意」必须由用户自己产生,AI 不能自己给自己批。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 授权一次有效 5
|
|
4
|
+
* 做法:授权入口只有用户亲手产生的那一次(设置页点击或聊天页斜杠命令),
|
|
5
|
+
* 授权一次有效 5 分钟、只对指定会话与指定种类生效;压缩与换任各有各的授权,互不顶替。
|
|
6
|
+
* 种类取 compact(压缩)或 succeed(换任),缺省为 compact;旧文件里没有 kind 的条目按 compact 处理。
|
|
6
7
|
*/
|
|
7
8
|
import fs from 'node:fs';
|
|
8
9
|
import os from 'node:os';
|
|
9
10
|
import path from 'node:path';
|
|
10
11
|
|
|
11
12
|
export const CONSENT_TTL_MS = 5 * 60 * 1000;
|
|
13
|
+
export const CONSENT_KINDS = ['compact', 'succeed'];
|
|
14
|
+
const DEFAULT_KIND = 'compact';
|
|
12
15
|
|
|
13
16
|
function consentPath() {
|
|
14
17
|
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
@@ -37,48 +40,84 @@ function stampNow() {
|
|
|
37
40
|
return d.getUTCFullYear() + '-' + p(d.getUTCMonth() + 1) + '-' + p(d.getUTCDate()) + ' ' + p(d.getUTCHours()) + ':' + p(d.getUTCMinutes()) + ' +0800';
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
function kindOf(kind) {
|
|
44
|
+
const k = String(kind === undefined || kind === null ? '' : kind).trim();
|
|
45
|
+
if (!k) return DEFAULT_KIND;
|
|
46
|
+
if (CONSENT_KINDS.indexOf(k) === -1) throw new Error('未知授权种类:' + k + '(只能是 compact 或 succeed)');
|
|
47
|
+
return k;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 存储键:压缩沿用会话 id 本身(兼容旧文件),换任另加后缀,两种授权可以同时存在。
|
|
52
|
+
*/
|
|
53
|
+
function keyFor(sid, kind) {
|
|
54
|
+
return kind === DEFAULT_KIND ? sid : sid + '|' + kind;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sidOfKey(key) {
|
|
58
|
+
return key.indexOf('|') === -1 ? key : key.slice(0, key.indexOf('|'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 条目自身的种类优先;旧条目没有 kind,按 compact 处理。 */
|
|
62
|
+
function kindOfEntry(key, entry) {
|
|
63
|
+
const k = entry && entry.kind ? String(entry.kind) : '';
|
|
64
|
+
if (CONSENT_KINDS.indexOf(k) !== -1) return k;
|
|
65
|
+
return key.indexOf('|succeed') === -1 ? DEFAULT_KIND : 'succeed';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 用户授权一次:kind 缺省为压缩。 */
|
|
69
|
+
export function grantConsent(sessionId, note, kind) {
|
|
42
70
|
const sid = String(sessionId || '');
|
|
43
71
|
if (!sid) throw new Error('缺少会话 id');
|
|
72
|
+
const k = kindOf(kind);
|
|
44
73
|
const all = readAll();
|
|
45
74
|
const now = Date.now();
|
|
46
|
-
|
|
75
|
+
const key = keyFor(sid, k);
|
|
76
|
+
all[key] = { at: now, until: now + CONSENT_TTL_MS, atText: stampNow(), note: String(note || ''), kind: k };
|
|
47
77
|
writeAll(all);
|
|
48
|
-
return { sessionId: sid, until: new Date(all[
|
|
78
|
+
return { sessionId: sid, kind: k, until: new Date(all[key].until).toISOString(), minutes: CONSENT_TTL_MS / 60000, atText: all[key].atText };
|
|
49
79
|
}
|
|
50
80
|
|
|
51
81
|
/** 授权是否有效(每次读取时一并清除已过期者)。 */
|
|
52
|
-
export function checkConsent(sessionId) {
|
|
82
|
+
export function checkConsent(sessionId, kind) {
|
|
53
83
|
const sid = String(sessionId || '');
|
|
84
|
+
const k = kindOf(kind);
|
|
54
85
|
if (!sid) return { ok: false, reason: '没有会话 id' };
|
|
55
86
|
const all = readAll();
|
|
56
|
-
const
|
|
87
|
+
const key = keyFor(sid, k);
|
|
88
|
+
const hit = all[key];
|
|
57
89
|
const now = Date.now();
|
|
58
90
|
let dirty = false;
|
|
59
|
-
for (const
|
|
60
|
-
if (!all[
|
|
91
|
+
for (const keyName of Object.keys(all)) {
|
|
92
|
+
if (!all[keyName] || !all[keyName].until || all[keyName].until <= now) { delete all[keyName]; dirty = true; }
|
|
61
93
|
}
|
|
62
94
|
if (dirty) { try { writeAll(all); } catch { /* ignore */ } }
|
|
63
95
|
if (!hit || !hit.until || hit.until <= now) {
|
|
64
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
kind: k,
|
|
99
|
+
reason: k === 'succeed' ? '用户还没有授权换任(聊天页命令 /allow-succeed)' : '用户还没有在设置页点「允许压缩」',
|
|
100
|
+
};
|
|
65
101
|
}
|
|
66
|
-
return { ok: true, until: hit.until, leftMs: hit.until - now, atText: hit.atText || '' };
|
|
102
|
+
return { ok: true, kind: k, until: hit.until, leftMs: hit.until - now, atText: hit.atText || '' };
|
|
67
103
|
}
|
|
68
104
|
|
|
69
|
-
export function consumedConsent(sessionId) {
|
|
105
|
+
export function consumedConsent(sessionId, kind) {
|
|
70
106
|
const all = readAll();
|
|
71
107
|
const sid = String(sessionId || '');
|
|
72
|
-
|
|
108
|
+
const key = keyFor(sid, kindOf(kind));
|
|
109
|
+
if (all[key]) { delete all[key]; try { writeAll(all); } catch { /* ignore */ } }
|
|
73
110
|
}
|
|
74
111
|
|
|
75
112
|
export function listConsent() {
|
|
76
113
|
const all = readAll();
|
|
77
114
|
const now = Date.now();
|
|
78
|
-
|
|
79
|
-
sessionId:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
115
|
+
const rows = Object.keys(all).map((key) => ({
|
|
116
|
+
sessionId: sidOfKey(key),
|
|
117
|
+
kind: kindOfEntry(key, all[key]),
|
|
118
|
+
leftMs: Math.max(0, (all[key].until || 0) - now),
|
|
119
|
+
atText: all[key].atText || '',
|
|
120
|
+
note: all[key].note || '',
|
|
83
121
|
})).filter((r) => r.leftMs > 0);
|
|
122
|
+
return rows.sort((a, b) => a.sessionId.localeCompare(b.sessionId, 'zh') || a.kind.localeCompare(b.kind));
|
|
84
123
|
}
|
package/src/lib/facts.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 口径单一来源:水位阈值、压缩次数与提示段 token
|
|
3
|
-
* 运行期文本一律由本模块渲染;改数值只改 config.mjs
|
|
2
|
+
* 口径单一来源:水位阈值、压缩次数与提示段 token 参考值。
|
|
3
|
+
* 运行期文本一律由本模块渲染;改数值只改 config.mjs 的默认值或本文件的参考表。
|
|
4
4
|
* 文档与测试引用同一组常量,避免同一事实多处各写一遍。
|
|
5
5
|
*/
|
|
6
6
|
import { DEFAULTS } from './config.mjs';
|
|
@@ -57,9 +57,10 @@ function isWide(cp) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
60
|
+
* 上下文成本参考值(加权 token)。
|
|
61
61
|
* 全局段按短工作区路径计;worst 档按真实长路径加迁移提示行计。
|
|
62
|
-
*
|
|
62
|
+
* 仅供参考:用于设置页展示与冒烟实测,不作为通过条件,也不参与任何拒绝或截断。
|
|
63
|
+
* 提示词效果优先于字数,实测值超出参考值时如实展示即可。
|
|
63
64
|
*/
|
|
64
65
|
export const TOKEN_BUDGET = {
|
|
65
66
|
globalTokens: 320,
|
package/src/lib/guide.mjs
CHANGED
|
@@ -9,10 +9,11 @@ import { ALL_TOOL_NAMES } from './names.mjs';
|
|
|
9
9
|
|
|
10
10
|
/** 首次安装、尚未接入工作区时的下一步(设置页与工具输出共用同一份)。 */
|
|
11
11
|
export const SETUP_STEPS = [
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
12
|
+
'准备一个长期存在的目录作为全局根的上级目录,不要选临时目录或下载目录。',
|
|
13
|
+
'在本页下方的输入框中粘贴该目录的绝对路径,点「一键部署」:插件会在其中建立 _Workspace 与整套结构,注册为「全局」工作区,并新建一个「全局」会话。',
|
|
14
|
+
'在左侧「全局」工作区中打开新会话即可开始。若只想先铺文件,可点「仅建立文件体系」,之后再注册工作区。',
|
|
15
|
+
'工作区已接入但还没有全局会话时,本页会单独出现「全局会话」卡片,可随时补建。',
|
|
16
|
+
'新建项目或杂项会话时,应当指明目标工作区;找不到工作区时,插件会把已注册的工作区列出来,不会静默落到别处。',
|
|
16
17
|
];
|
|
17
18
|
|
|
18
19
|
export function buildGuide(cfg) {
|
|
@@ -26,13 +27,14 @@ export function buildGuide(cfg) {
|
|
|
26
27
|
'本页除首次安装、全局前置提示词与会话角色外均为只读;需要最新读数时,点击右上角的「刷新」。',
|
|
27
28
|
],
|
|
28
29
|
situations: [
|
|
30
|
+
{ when: '首次安装', say: '部署全局文件体系', what: '本页「一键部署」可一次完成三件事:建文件体系、注册「全局」工作区、建「全局」会话' },
|
|
29
31
|
{ when: '作业开始与结束', say: '先出一份起始简报', what: '汇总上下文水位、通用约束条闸门、会话清点与阶段建议(ho_brief)' },
|
|
30
32
|
{ when: '查阅规则', say: '把 Charter 第 7.2.3 节取出来', what: '按节返回规则正文,避免整篇读取(ho_charter;完整流程用 doc="runtime")' },
|
|
31
|
-
{ when: '交接与压缩续跑', say: '先把交接文档写好,再压缩并继续', what: '
|
|
33
|
+
{ when: '交接与压缩续跑', say: '先把交接文档写好,再压缩并继续', what: '写自己角色的交接、体检、登记压缩,并由助手选择压缩后去向:继续工作时用 resume=continue,任务已收尾时用 resume=wait(压缩后不唤醒)(ho_handover、ho_checkpoint)' },
|
|
32
34
|
{ when: '接近上下文上限', say: '查一下本会话的上下文水位', what: compact + '(ho_phase)' },
|
|
33
|
-
{ when: '换任', say: '
|
|
35
|
+
{ when: '换任', say: '让在任会话自己完成换任', what: '用户先发送 /allow-succeed 授权,在任会话随后校验交接、建接任会话并登记在任(ho_succeed);全局仍可用 ho_sessions action=new 手动新建会话' },
|
|
34
36
|
{ when: '会话角色', say: '把 Demo1 设成项目角色,并让它整理自己的文件', what: '写角色表、当场收窄该会话的工具与提示、投递一次性整理指令(ho_sessions action=role / 本页会话角色)' },
|
|
35
|
-
{ when: '通知项目会话', say: '通知 Demo 更新交接文档', what: '
|
|
37
|
+
{ when: '通知项目会话', say: '通知 Demo 更新交接文档', what: '投递一条消息;会话之间可直接对话,不必经全局转述;会消耗目标会话一整轮 token,应尽量少用(peer_send)' },
|
|
36
38
|
{ when: '全局前置提示词', say: '把这段设为全局前置提示词', what: '保存到本机;仅在你开启且内容非空时注入全部会话,关闭或留空时不注册提示段' },
|
|
37
39
|
{ when: '项目骨架', say: '注册 Demo 的骨架', what: '读取、注册、校验项目或杂项的骨架档案;由该项目会话自己写文件并注册(ho_profile)' },
|
|
38
40
|
{ when: '查看会话', say: '列出当前活跃的会话', what: '水位、运行状态、活跃或沉默、在任或前任(ho_sessions)' },
|
|
@@ -42,6 +44,7 @@ export function buildGuide(cfg) {
|
|
|
42
44
|
{ when: '建立工作区', say: '把这个目录注册为工作区', what: '注册已存在的目录;删除只删注册,目录与会话日志均保留(ho_workspace)' },
|
|
43
45
|
{ when: '首次安装', say: '把某个目录建成全局根目录', what: '在该目录下建立整套全局文件体系,并注册为工作区(ho_init)' },
|
|
44
46
|
{ when: '自检', say: '检查插件当前状态', what: '服务可解析性、压缩调度器、压缩计数、本会话角色与工具集(ho_diag)' },
|
|
47
|
+
{ when: '查在任', say: '现在 Mingent 是谁在任', what: '按全局、项目、杂项查现任会话的名字与会话 id(ho_office);不起会话也可用命令行 node scripts/office.mjs 同名查询' },
|
|
45
48
|
],
|
|
46
49
|
groups: [
|
|
47
50
|
{
|
|
@@ -55,6 +58,7 @@ export function buildGuide(cfg) {
|
|
|
55
58
|
['ho_drift', '交接漂移体检:Δ 为当前轮与交接轮之差,大于 3 需刷新'],
|
|
56
59
|
['ho_diag', '插件自检:服务、调度器、压缩计数、角色判定与工具集'],
|
|
57
60
|
['ho_tokens', '会话 token 体检:轮数、步数、每步 token、最贵的单条返回'],
|
|
61
|
+
['ho_office', '在任登记表:查全局、项目、杂项现在由哪个会话在任(名字与会话 id)'],
|
|
58
62
|
],
|
|
59
63
|
},
|
|
60
64
|
{
|
|
@@ -68,6 +72,7 @@ export function buildGuide(cfg) {
|
|
|
68
72
|
['ho_workspace', '工作区列表、创建、删除;删除只删注册'],
|
|
69
73
|
['ho_init', '首次安装:在指定目录下建立整套全局文件体系,可顺带注册工作区'],
|
|
70
74
|
['ho_profile', '项目或杂项骨架:读取、注册、校验、取预设'],
|
|
75
|
+
['ho_succeed', '换任自服务:在任会话经用户授权后自行校验交接、建接任会话并登记在任'],
|
|
71
76
|
],
|
|
72
77
|
},
|
|
73
78
|
{
|
|
@@ -85,6 +90,8 @@ export function buildGuide(cfg) {
|
|
|
85
90
|
rules: [
|
|
86
91
|
'交接线:上下文水位达到 ' + w.warn + ' 时提醒,达到 ' + w.handover + ' 时写交接并压缩,' + w.hard + ' 为硬上限;同一会话最多压缩 ' + w.max + ' 次,第四段改用新建会话。',
|
|
87
92
|
'压缩不可逆:历史会被摘要替换,因此必须先写出合格的交接文档;体检未通过时,插件拒绝安排压缩。',
|
|
93
|
+
'会话之间可直接 peer_send 对话,不必经全局转述;默认沉默仍适用:只在到交接线且已写交接、需用户决定、发现全局口径有误时发一条,任务完成不向全局报告。',
|
|
94
|
+
'压缩后的去向由助手在登记时选择:任务未完成用 continue 续跑,任务已收尾用 wait 静默收尾,等用户派新任务。',
|
|
88
95
|
'交接文件按角色分离:全局写 _Control_Center/records/Global_Handover.md,项目写 _Project_Meta/<项目>/Handover.md,杂项写 _Misc/tasks/<任务>/Handover.md,互不串用。',
|
|
89
96
|
'会话本身不迁移:跨机器、跨平台的连续性依托交接文档。',
|
|
90
97
|
'全局前置提示词属于用户内容,不是插件约束;关闭或留空时不占用注入点位,可与其它提示词插件并存。',
|
package/src/lib/handover-io.mjs
CHANGED
|
@@ -17,6 +17,26 @@ export function readHandover(file) {
|
|
|
17
17
|
catch { return { path: file, text: null }; }
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* 归档时间戳:`YYYYMMDD_HHMMSS`(到秒)。
|
|
22
|
+
* 规范要求时间戳取归档那一刻;只到分钟会在同一分钟内归档两份时撞名,故到秒。
|
|
23
|
+
*/
|
|
24
|
+
export function archiveStamp(d) {
|
|
25
|
+
const x = d || new Date();
|
|
26
|
+
const p = (n) => (n < 10 ? "0" + n : "" + n);
|
|
27
|
+
return x.getFullYear() + p(x.getMonth() + 1) + p(x.getDate()) + "_" + p(x.getHours()) + p(x.getMinutes()) + p(x.getSeconds());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 归档文件名:`<原文件名>_<YYYYMMDD_HHMMSS>.md`。
|
|
32
|
+
* 「原文件名」取源文件主名(不含扩展名):全局 `Global_Handover_…`、项目与杂项 `Handover_…`;
|
|
33
|
+
* 不加链标识——归档目录本身已按链隔离(规范 §4.1)。
|
|
34
|
+
*/
|
|
35
|
+
export function archiveFileName(file, stampText) {
|
|
36
|
+
const base = String(file || "").split(/[\\/]/).pop().replace(/\.md$/i, "");
|
|
37
|
+
return base + "_" + String(stampText) + ".md";
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
/**
|
|
21
41
|
* 写交接:先把旧文件复制到 <作用域>/Snapshots/pm-handover-<stamp>/,再原子替换,最后按角色体检。
|
|
22
42
|
* @param {object} [opts] { info?: 角色信息(决定体检判据与备份目录), kind?: 'global'|'project'|'misc' }
|
package/src/lib/manual.mjs
CHANGED
|
@@ -10,7 +10,16 @@ import { handoverRuleText, watermark } from './facts.mjs';
|
|
|
10
10
|
const NL = String.fromCharCode(10);
|
|
11
11
|
|
|
12
12
|
/** 完整流程的指针句(三档角色共用)。 */
|
|
13
|
-
const RUNTIME_POINTER = '
|
|
13
|
+
const RUNTIME_POINTER = '细节按节取:ho_charter(doc="runtime", section=…)。';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 复用准则:全局、项目、杂项三处必须同一句。
|
|
17
|
+
* 派活与新建会话前先查在任,低水位在任直接复用;确需另建才新建。
|
|
18
|
+
*/
|
|
19
|
+
function reuseRule(cfg) {
|
|
20
|
+
return '指派任务或新建会话前,先用 ho_office(空参数)查一次在任;同一类别已有在任且其水位低于 '
|
|
21
|
+
+ watermark(cfg).warn + ' 时,直接用 peer_send 复用,不新建;确需另建(原会话不可用、需并行、用户明确要求)时才新建。';
|
|
22
|
+
}
|
|
14
23
|
|
|
15
24
|
/** 全局会话的常驻段:五节骨架(身份、权限、节奏、交接、按需取用)。 */
|
|
16
25
|
export function globalManual(opts) {
|
|
@@ -19,18 +28,19 @@ export function globalManual(opts) {
|
|
|
19
28
|
'dsh-handover 运行期(全局)',
|
|
20
29
|
'',
|
|
21
30
|
'一、身份',
|
|
22
|
-
'
|
|
31
|
+
'本会话为全局管理会话,协调会话、维护规则与交接。',
|
|
23
32
|
'',
|
|
24
33
|
'二、权限',
|
|
25
|
-
'可写 _Control_Center、_Project_Meta
|
|
26
|
-
'
|
|
34
|
+
'可写 _Control_Center、_Project_Meta 与项目结构;Projects 及各项目库内文件只读,不得代写;项目内作业不代办。',
|
|
35
|
+
'新建会话与设定角色仅属全局;会话间可直接 peer_send;换任须用户授权(/allow-succeed 或设置页)后由在任会话调用 ho_succeed,不再请求全局。',
|
|
36
|
+
reuseRule(o.cfg),
|
|
27
37
|
'',
|
|
28
38
|
'三、节奏',
|
|
29
39
|
handoverRuleText(o.cfg),
|
|
30
|
-
'
|
|
40
|
+
'压缩须用户点「压缩授权」,再写交接并 ho_checkpoint(confirm=true);任务未完成传 resume=continue,已收尾传 resume=wait,不再唤醒。confirm 仅表意图,授权以点击为准。关键步骤传 at_risk=true 暂缓。',
|
|
31
41
|
'',
|
|
32
42
|
'四、交接',
|
|
33
|
-
'
|
|
43
|
+
'交接:_Control_Center/records/Global_Handover.md。',
|
|
34
44
|
'',
|
|
35
45
|
'五、按需取用',
|
|
36
46
|
RUNTIME_POINTER,
|
|
@@ -62,8 +72,9 @@ export function scopedManual(info, opts) {
|
|
|
62
72
|
hp.path
|
|
63
73
|
? '交接:' + hp.path + '(ho_handover 默认)。'
|
|
64
74
|
: '交接路径未解析(缺角色或项目名),请先在设置页设定角色。',
|
|
65
|
-
handoverRuleText(o.cfg) + '
|
|
66
|
-
'
|
|
75
|
+
handoverRuleText(o.cfg) + '压缩须用户点「压缩授权」,再由本会话执行 ho_checkpoint(confirm=true);收尾时传 resume=wait,不再唤醒。',
|
|
76
|
+
'会话间可直接 peer_send;默认沉默:仅在水位 ' + watermark(o.cfg).warn + ' 且已写交接、需用户决定、全局口径有误时向全局报告;任务完成不报全局。',
|
|
77
|
+
reuseRule(o.cfg),
|
|
67
78
|
RUNTIME_POINTER,
|
|
68
79
|
];
|
|
69
80
|
const mv = moveLine(o);
|
|
@@ -90,12 +101,12 @@ export function organizeMessageFor(info, opts) {
|
|
|
90
101
|
const o = opts || {};
|
|
91
102
|
const role = info && info.role ? info.role : 'plain';
|
|
92
103
|
if (role === 'plain') return '';
|
|
93
|
-
const head = '【dsh-handover
|
|
104
|
+
const head = '【dsh-handover 角色设定】本会话已设定为「' + scopeLabel(info) + '」。请按下列结构核对自身文件:缺失的目录应建立,位置不当的文件应归位;既有文件就地保留,不得删除或修改内容。';
|
|
94
105
|
if (role === 'project') {
|
|
95
106
|
const p = info.project;
|
|
96
107
|
return [
|
|
97
108
|
head,
|
|
98
|
-
'
|
|
109
|
+
'先通读本库,确定本项目的骨架:代码与文档主体、必需文件、临时与归档位置。构建型、动画型等项目不必套用插件类项目的文档集。',
|
|
99
110
|
'把结论写入 _Project_Meta/' + p + '/project.profile.json;字段格式见 ho_profile(action=presets)。',
|
|
100
111
|
'写入后调用 ho_profile(action=register) 校验并登记;返回缺失项时按清单补齐。',
|
|
101
112
|
'约束:仅可改动本库与 _Project_Meta/' + p + '/;不得改动 _Control_Center 及其他项目;移动较大文件前先执行 ho_snapshot。可用工具:ho_snapshot、ho_handover、ho_phase、ho_checkpoint、ho_profile、peer_send。',
|
|
@@ -106,10 +117,10 @@ export function organizeMessageFor(info, opts) {
|
|
|
106
117
|
const t = info.task;
|
|
107
118
|
return [
|
|
108
119
|
head,
|
|
109
|
-
'
|
|
120
|
+
'先通读本任务目录,确定本任务的骨架:工作文件、规则文件、交接、快照与归档位置。',
|
|
110
121
|
'把结论写入 _Misc/tasks/' + t + '/task.profile.json;字段格式见 ho_profile(action=presets)。',
|
|
111
122
|
'写入后调用 ho_profile(action=register) 校验并登记。',
|
|
112
|
-
'约束:仅可改动 _Misc/tasks/' + t + '/、_Temp/_Misc/' + t + '/、_Archives/_Misc/' + t + '/;_Control_Center、Projects
|
|
123
|
+
'约束:仅可改动 _Misc/tasks/' + t + '/、_Temp/_Misc/' + t + '/、_Archives/_Misc/' + t + '/;_Control_Center、Projects 与其他任务不得写入。工具:ho_snapshot、ho_handover、ho_phase、ho_checkpoint、ho_profile、peer_send。',
|
|
113
124
|
'完成后以一句话说明改动即可。',
|
|
114
125
|
].join(NL);
|
|
115
126
|
}
|
package/src/lib/names.mjs
CHANGED
|
@@ -7,7 +7,7 @@ const PREFIX = 'ho';
|
|
|
7
7
|
export const nameOf = (key) => PREFIX + '_' + key;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* ho_*
|
|
10
|
+
* ho_* 18 个。合并过的家族(刻意保持旧名,参数里用 action 分流):
|
|
11
11
|
* ho_pack —— 打包 / 校验 / 预演 / 恢复(原 ho_pack_verify、ho_pack_restore、ho_pack_precheck)
|
|
12
12
|
* ho_sessions —— 清点 / 设定角色 / 建会话 / 出提示词 / 归档 / 解归档(原 ho_session_new、ho_prompt、ho_session_archive、ho_session_unarchive)
|
|
13
13
|
* ho_profile —— 骨架读取 / 注册 / 校验 / 预设
|
|
@@ -16,7 +16,7 @@ const KEYS = [
|
|
|
16
16
|
'brief', 'gate', 'sessions', 'phase', 'charter',
|
|
17
17
|
'handover', 'checkpoint', 'snapshot', 'diag',
|
|
18
18
|
'drift', 'changelog', 'tokens',
|
|
19
|
-
'pack', 'workspace', 'init', 'profile',
|
|
19
|
+
'pack', 'workspace', 'init', 'profile', 'office', 'succeed',
|
|
20
20
|
];
|
|
21
21
|
|
|
22
22
|
export const NAMES = KEYS.reduce((acc, k) => { acc[k] = nameOf(k); return acc; }, {});
|