@mingmaple/dsh-handover 0.9.12 → 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 +15 -10
- package/docs/releases/0.9.12.md +28 -0
- package/docs/releases/0.9.13.md +30 -0
- package/docs/troubleshoot.md +11 -3
- package/package.json +1 -1
- package/scripts/blank-dsh.mjs +6 -5
- package/scripts/blank-smoke.mjs +72 -23
- package/scripts/check-docs.mjs +23 -2
- package/scripts/office.mjs +81 -0
- package/src/client.js +25 -14
- package/src/index.mjs +47 -23
- package/src/lib/api.mjs +5 -4
- package/src/lib/consent.mjs +59 -20
- package/src/lib/facts.mjs +5 -4
- package/src/lib/guide.mjs +4 -1
- package/src/lib/manual.mjs +23 -12
- package/src/lib/names.mjs +2 -2
- package/src/lib/office.mjs +272 -0
- package/src/lib/phase.mjs +1 -1
- package/src/lib/role.mjs +4 -4
- package/src/lib/session-ops.mjs +147 -89
- package/src/lib/succession.mjs +244 -0
- 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 +12 -9
- package/src/templates/rules/Handover_Rules.md +2 -2
- package/src/templates/rules/Misc_Task_Standard.md +3 -3
- package/src/tools/ho_office.mjs +59 -0
- package/src/tools/ho_succeed.mjs +24 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 换任自服务:在任会话在用户授权后,自己校验交接、建接任会话、登记在任并投递启动提示词。
|
|
3
|
+
*
|
|
4
|
+
* 越权防线:建什么角色的会话只认会话自身已登记的角色(env.roleOf),不采信调用参数;
|
|
5
|
+
* 名字只认登记表的历史最大号(nextName),不采信会话自述。
|
|
6
|
+
* 全部校验(角色、授权、交接结构、在任身份、工作区、提示词)都在第一次写盘之前完成:
|
|
7
|
+
* 任何一项不通过,整体不落任何写操作,只返回中文原因。
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import { NL, readUtf8 } from './text.mjs';
|
|
11
|
+
import { lineageKeyOf, loadOffice, nextName, registerHolder, stampNow } from './office.mjs';
|
|
12
|
+
import { checkConsent, consumedConsent } from './consent.mjs';
|
|
13
|
+
import { newSessionIn } from './session-create.mjs';
|
|
14
|
+
import { sendPrompt } from './prompt.mjs';
|
|
15
|
+
import { resolveWorkspaceFor, promptFor } from './session-ops.mjs';
|
|
16
|
+
import { watermark } from './facts.mjs';
|
|
17
|
+
|
|
18
|
+
/** 交接里不得残留的占位符。 */
|
|
19
|
+
export const HANDOVER_PLACEHOLDERS = ['待填', 'TODO', '(建完后填)', '<填'];
|
|
20
|
+
|
|
21
|
+
function msgOf(e) {
|
|
22
|
+
return String(e && e.message ? e.message : e);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 找接任指令节:含「## 下一步」或「## 2.」这类标题;没有标题时接受 §2 的写法。
|
|
27
|
+
* 判据与 ho_gate 的交接体检一致:这一节至少要有一个条目(列表、表格或编号行)。
|
|
28
|
+
*/
|
|
29
|
+
function nextSectionOf(text) {
|
|
30
|
+
const lines = text.split(NL);
|
|
31
|
+
let start = -1;
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
|
+
const l = lines[i];
|
|
34
|
+
if (!l.startsWith('## ')) continue;
|
|
35
|
+
if (l.indexOf('下一步') !== -1 || /^##\s*2(?:[.、\s]|$)/.test(l)) { start = i; break; }
|
|
36
|
+
}
|
|
37
|
+
if (start === -1) {
|
|
38
|
+
if (/§\s*2/.test(text)) return { ok: true };
|
|
39
|
+
return { ok: false, reason: '缺少接任指令节:应有「## 下一步」或「## 2.」一节' };
|
|
40
|
+
}
|
|
41
|
+
const rows = [];
|
|
42
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
43
|
+
if (lines[i].startsWith('## ')) break;
|
|
44
|
+
const t = lines[i].trim();
|
|
45
|
+
if (/^[-*]\s+/.test(t)) rows.push(t);
|
|
46
|
+
else if (/^\|/.test(t) && t.indexOf('---') === -1) rows.push(t);
|
|
47
|
+
else if (/^\d+[.、]\s+/.test(t)) rows.push(t);
|
|
48
|
+
}
|
|
49
|
+
if (!rows.length) return { ok: false, reason: '「下一步」一节没有列出任何条目' };
|
|
50
|
+
return { ok: true };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 交接结构检查(只判结构):返回缺项列表,空数组表示通过。 */
|
|
54
|
+
export function auditHandover(file) {
|
|
55
|
+
const problems = [];
|
|
56
|
+
try {
|
|
57
|
+
const st = fs.statSync(file);
|
|
58
|
+
if (!st.isFile()) return ['交接路径不是文件:' + file];
|
|
59
|
+
} catch {
|
|
60
|
+
return ['交接文件不存在:' + file];
|
|
61
|
+
}
|
|
62
|
+
let text = '';
|
|
63
|
+
try { text = readUtf8(file).replace(/^\uFEFF/, ''); } catch (e) { return ['交接文件读取失败:' + msgOf(e)]; }
|
|
64
|
+
if (!text.trim()) return ['交接文件为空:' + file];
|
|
65
|
+
const next = nextSectionOf(text);
|
|
66
|
+
if (!next.ok) problems.push(next.reason);
|
|
67
|
+
for (const p of HANDOVER_PLACEHOLDERS) {
|
|
68
|
+
if (text.indexOf(p) !== -1) problems.push('仍有占位符:' + p);
|
|
69
|
+
}
|
|
70
|
+
return problems;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 在旧会话的交接末尾追加「换任登记」:先备份到同目录 .bak,再追加。 */
|
|
74
|
+
function appendSuccessionBlock(file, lines) {
|
|
75
|
+
const cur = readUtf8(file);
|
|
76
|
+
const bom = cur.charCodeAt(0) === 0xFEFF ? '\uFEFF' : '';
|
|
77
|
+
const body = bom ? cur.slice(1) : cur;
|
|
78
|
+
fs.copyFileSync(file, file + '.bak');
|
|
79
|
+
const gap = body.endsWith(NL) ? NL : NL + NL;
|
|
80
|
+
const block = ['## 换任登记', ''].concat(lines).join(NL);
|
|
81
|
+
fs.writeFileSync(file, bom + body + gap + block + NL, 'utf8');
|
|
82
|
+
return file + '.bak';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 执行一次换任自服务。
|
|
87
|
+
* @param {object} env 插件环境
|
|
88
|
+
* @param {object} exec 工具执行上下文
|
|
89
|
+
* @param {object} [opts] { note, force }:force 为 true 时越过「水位未到线」判据
|
|
90
|
+
* @returns {Promise<string>} 中文结果或失败原因
|
|
91
|
+
*/
|
|
92
|
+
export async function runSuccession(env, exec, opts) {
|
|
93
|
+
const o = opts || {};
|
|
94
|
+
const note = typeof o.note === 'string' ? o.note.trim() : '';
|
|
95
|
+
const reject = (reason) => '换任未执行,未做任何写入。' + NL + reason;
|
|
96
|
+
|
|
97
|
+
// 1. 自身角色与类别:只认已登记的角色。
|
|
98
|
+
const info = env.roleOf(exec) || {};
|
|
99
|
+
const kind = String(info.role || 'plain');
|
|
100
|
+
if (kind === 'plain') return reject('普通会话不参与换任。');
|
|
101
|
+
if (kind !== 'global' && kind !== 'project' && kind !== 'misc') return reject('本会话角色无法用于换任:' + kind);
|
|
102
|
+
const project = String(info.project || '');
|
|
103
|
+
const task = String(info.task || '');
|
|
104
|
+
const key = lineageKeyOf(kind, project, task);
|
|
105
|
+
if (!key) return reject(kind === 'project' ? '项目名缺失,无法确定类别。' : '任务名缺失,无法确定类别。');
|
|
106
|
+
|
|
107
|
+
// 2. 用户授权:AI 不能自行批准。
|
|
108
|
+
const selfId = env.selfId(exec);
|
|
109
|
+
if (!selfId) return reject('拿不到当前会话 id,无法校验授权。');
|
|
110
|
+
const cs = checkConsent(selfId, 'succeed');
|
|
111
|
+
if (!cs.ok) {
|
|
112
|
+
return reject([
|
|
113
|
+
cs.reason + '。',
|
|
114
|
+
'授权方式(任选其一):',
|
|
115
|
+
' ① 在聊天框发送 /allow-succeed(最快,仅授权本会话);',
|
|
116
|
+
' ② 在设置页的 dsh-handover 中点击「换任授权」的「允许换任」。',
|
|
117
|
+
'授权有效期 5 分钟,用后失效。',
|
|
118
|
+
].join(NL));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 3. 交接结构检查(只判结构,不判质量)。
|
|
122
|
+
const hp = env.handoverFor(exec) || {};
|
|
123
|
+
const handoverFile = hp.path ? String(hp.path) : '';
|
|
124
|
+
if (!handoverFile) return reject('没有交接路径:' + (hp.reason || '本会话角色未确定'));
|
|
125
|
+
const problems = auditHandover(handoverFile);
|
|
126
|
+
if (problems.length) {
|
|
127
|
+
return reject(['交接结构未通过:' + handoverFile]
|
|
128
|
+
.concat(problems.map((p) => ' - ' + p))
|
|
129
|
+
.concat(['补齐后重新调用 ho_succeed。'])
|
|
130
|
+
.join(NL));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 4. 在任身份与重复换任:登记表是唯一记录。
|
|
134
|
+
const ws = String(env.resolveWs(exec) || '');
|
|
135
|
+
if (!ws) return reject('无法确定工作区根,读不到在任登记表。');
|
|
136
|
+
const store = loadOffice(ws);
|
|
137
|
+
const lin = store.lineages[key];
|
|
138
|
+
// 只有本类别的在任者本人可换任:既防旧会话复活,也防同项目的旁支会话抢先。
|
|
139
|
+
if (!lin || !lin.current || !lin.current.sessionId) {
|
|
140
|
+
return reject('本类别(' + key + ')在在任登记表里还没有现任记录:先由全局建会话并登记,再换任。');
|
|
141
|
+
}
|
|
142
|
+
if (String(lin.current.sessionId) !== String(selfId)) {
|
|
143
|
+
return reject('本会话不是在任者:本类别现任为 ' + String(lin.current.name || '(未命名)')
|
|
144
|
+
+ '(' + String(lin.current.sessionId) + '),不得换任。');
|
|
145
|
+
}
|
|
146
|
+
{
|
|
147
|
+
const history = Array.isArray(lin.history) ? lin.history : [];
|
|
148
|
+
const closed = history.find((h) => h && String(h.sessionId || '') === String(selfId) && h.to);
|
|
149
|
+
if (closed) {
|
|
150
|
+
const curName = lin.current && lin.current.name ? lin.current.name : '(空缺)';
|
|
151
|
+
return reject('本类别已为本会话记过一次换任(' + String(closed.to || '') + ',现任 ' + curName + '),旧会话不得再次换任。');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 4.1 水位未到线:本会话就是在任者且水位低于提醒线时,换任通常是多余的。
|
|
156
|
+
// 放在授权与交接校验之后、任何写入之前;取不到水位就跳过。
|
|
157
|
+
if (o.force !== true) {
|
|
158
|
+
let pct = null;
|
|
159
|
+
try { pct = typeof env.selfPct === 'function' ? env.selfPct(selfId) : null; } catch { pct = null; }
|
|
160
|
+
if (pct !== null && pct !== undefined && Number(pct) < watermark(env.cfg).warnValue) {
|
|
161
|
+
return reject('本会话水位 ' + Number(pct).toFixed(1) + '%,尚未到线,通常无需换任;确需换任请传 force=true。');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 5. 分配编号:登记表历史最大号加一,不采用会话自述。
|
|
166
|
+
const name = nextName(store, key);
|
|
167
|
+
|
|
168
|
+
// 6. 确定工作区:与 ho_sessions(action="new") 同一套解析;杂项落到本会话所在工作区。
|
|
169
|
+
let workspaceId = '';
|
|
170
|
+
try {
|
|
171
|
+
workspaceId = resolveWorkspaceFor(env, exec, kind, project, task, kind === 'misc' ? { workspace: ws } : {});
|
|
172
|
+
} catch (e) {
|
|
173
|
+
return reject(msgOf(e));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 7. 拼启动提示词:末尾追加交接路径与「先读交接、读毕报到」。
|
|
177
|
+
let prompt = '';
|
|
178
|
+
try {
|
|
179
|
+
const base = promptFor(env, { ws, kind, project, task, title: name, sessionId: '', globalSessionId: selfId, extra: '' });
|
|
180
|
+
prompt = base + NL + NL + [
|
|
181
|
+
'接任交接:' + handoverFile,
|
|
182
|
+
'先读这份交接再动手;读毕向用户报告当前停在哪一步、交接列有哪些待办,然后等待用户指派任务。',
|
|
183
|
+
].join(NL);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
return reject('启动提示词生成失败:' + msgOf(e));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 8. 建会话:失败即停止,不登记。
|
|
189
|
+
const made = await newSessionIn(env, { workspaceId, title: name, role: kind, project, task });
|
|
190
|
+
const sessionId = made && made.sessionId ? String(made.sessionId) : '';
|
|
191
|
+
if (!sessionId) return reject('建会话失败,未做任何登记:' + ((made && made.error) || '原因未知'));
|
|
192
|
+
// 提示词里预留的是「建完后填」,拿到会话 id 后在这里替换成真实 id。
|
|
193
|
+
const text = prompt.split('(建完后填)').join(sessionId);
|
|
194
|
+
|
|
195
|
+
// 9. 登记在任:原在任者自动进历史,Registry.md 在役列同步。
|
|
196
|
+
let reg = null;
|
|
197
|
+
try {
|
|
198
|
+
reg = registerHolder(ws, { kind, project, task, name, sessionId, workspace: ws, note });
|
|
199
|
+
} catch (e) {
|
|
200
|
+
return [
|
|
201
|
+
'会话已建 ' + sessionId + '(' + name + '),但登记在任失败:' + msgOf(e),
|
|
202
|
+
'启动提示词尚未投递;先修好登记表,再用 peer_send 通知它。',
|
|
203
|
+
].join(NL);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 10. 投递启动提示词:失败要如实报出,会话已经存在。
|
|
207
|
+
let delivered = false;
|
|
208
|
+
let deliveryError = '';
|
|
209
|
+
try {
|
|
210
|
+
const r = await sendPrompt(env.controller(), sessionId, text, 'queue');
|
|
211
|
+
delivered = !!(r && r.accepted);
|
|
212
|
+
if (!delivered) deliveryError = '会话控制器未接受(accepted=false)';
|
|
213
|
+
} catch (e) {
|
|
214
|
+
deliveryError = msgOf(e);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 11. 旧会话的交接末尾追加换任登记;写前备份。
|
|
218
|
+
let appendNote = '';
|
|
219
|
+
try {
|
|
220
|
+
const bak = appendSuccessionBlock(handoverFile, [
|
|
221
|
+
'- 接任者:' + name,
|
|
222
|
+
'- 会话 id:' + sessionId,
|
|
223
|
+
'- 时刻:' + stampNow(),
|
|
224
|
+
...(note ? ['- 备注:' + note] : []),
|
|
225
|
+
'- 旧会话停止作业。',
|
|
226
|
+
]);
|
|
227
|
+
appendNote = '已写入交接末尾(备份 ' + bak + ')';
|
|
228
|
+
} catch (e) {
|
|
229
|
+
appendNote = '换任登记写入失败:' + msgOf(e);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 12. 消费授权:一次授权只够换任一次。
|
|
233
|
+
try { consumedConsent(selfId, 'succeed'); } catch { /* 授权文件损坏不影响本次结果 */ }
|
|
234
|
+
|
|
235
|
+
// 13. 结果。
|
|
236
|
+
const out = [];
|
|
237
|
+
out.push('换任完成:' + name + '(' + sessionId + ')');
|
|
238
|
+
out.push('在任登记:' + key + (reg && reg.registry && reg.registry.changed && reg.registry.changed.length
|
|
239
|
+
? ',Registry.md 已同步在役列' : ''));
|
|
240
|
+
out.push('启动提示词:' + (delivered ? '已投递' : '未送达:' + deliveryError + '。会话已建,可用 peer_send 补投'));
|
|
241
|
+
out.push('交接登记:' + appendNote);
|
|
242
|
+
out.push('本会话到此停止,等待用户在新会话中派任务。');
|
|
243
|
+
return out.join(NL);
|
|
244
|
+
}
|
package/src/lib/workspace.mjs
CHANGED
|
@@ -9,7 +9,7 @@ export function hasCenter(p) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* 没有 exec
|
|
12
|
+
* 没有 exec 时的备用做法:配置 workspaceRoot → 标题含「全局」且带 _Control_Center 的工作区
|
|
13
13
|
* → 任一带 _Control_Center 的工作区 → 进程 cwd。
|
|
14
14
|
*/
|
|
15
15
|
export function routeWorkspace(env) {
|
package/src/selftest.mjs
CHANGED
|
@@ -10,6 +10,8 @@ import { pmGateTool } from './tools/ho_gate.mjs';
|
|
|
10
10
|
import { pmSessionsTool } from './tools/ho_sessions.mjs';
|
|
11
11
|
import { pmPhaseTool } from './tools/ho_phase.mjs';
|
|
12
12
|
import { pmCharterTool } from './tools/ho_charter.mjs';
|
|
13
|
+
import { pmOfficeTool } from './tools/ho_office.mjs';
|
|
14
|
+
import { pmSucceedTool } from './tools/ho_succeed.mjs';
|
|
13
15
|
import path from 'node:path';
|
|
14
16
|
|
|
15
17
|
const argv = process.argv.slice(2);
|
|
@@ -54,6 +56,13 @@ const env = {
|
|
|
54
56
|
lockState: () => 'unknown',
|
|
55
57
|
};
|
|
56
58
|
|
|
59
|
+
/** 换任只读用例:只验授权判据,不带用户授权时应当返回「没有授权」,而不是抛异常。 */
|
|
60
|
+
const envSucceed = Object.assign({}, env, {
|
|
61
|
+
selfId: () => 'session-selftest-succeed',
|
|
62
|
+
roleOf: () => ({ role: 'project', project: 'Demo', task: '', source: 'selftest' }),
|
|
63
|
+
handoverFor: () => ({ path: path.join(ws, '_Project_Meta', 'Demo', 'Handover.md'), reason: '' }),
|
|
64
|
+
});
|
|
65
|
+
|
|
57
66
|
const exec = null;
|
|
58
67
|
const cases = [
|
|
59
68
|
{ t: pmBriefTool(env), a: {}, flag: "brief" },
|
|
@@ -61,6 +70,8 @@ const cases = [
|
|
|
61
70
|
{ t: pmSessionsTool(env), a: {}, flag: "sessions" },
|
|
62
71
|
{ t: pmPhaseTool(env), a: {}, flag: "phase" },
|
|
63
72
|
{ t: pmCharterTool(env), a: { doc: "charter", section: "8.1.1.4", maxLines: 30 }, flag: "charter" },
|
|
73
|
+
{ t: pmOfficeTool(env), a: { query: "全局" }, flag: "office" },
|
|
74
|
+
{ t: pmSucceedTool(envSucceed), a: {}, flag: "succeed", want: /没有授权/ },
|
|
64
75
|
];
|
|
65
76
|
|
|
66
77
|
let failed = 0;
|
|
@@ -71,6 +82,10 @@ for (const c of cases) {
|
|
|
71
82
|
try {
|
|
72
83
|
const out = await c.t.execute(c.a, exec);
|
|
73
84
|
console.log(out.text);
|
|
85
|
+
if (c.want && !c.want.test(String(out && out.text))) {
|
|
86
|
+
failed++;
|
|
87
|
+
console.log("FAILED: 输出不符合预期 " + c.want);
|
|
88
|
+
}
|
|
74
89
|
} catch (e) {
|
|
75
90
|
failed++;
|
|
76
91
|
console.log("FAILED: " + (e && e.message ? e.message : String(e)));
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
```text
|
|
8
8
|
你是<角色>,由全局管理对话创建并交接。
|
|
9
9
|
第一条要读的:<绝对路径>(读毕再开始作业)。| 项目→ _Project_Meta/<项目名>/Handover.md;全局→ records/Global_Handover.md;杂项首任→ rules/Misc_Task_Standard.md
|
|
10
|
-
|
|
10
|
+
不得每轮回执,也不发送「收到」「已完成」一类确认;全局只看文件末态。
|
|
11
11
|
你的 session id:<本会话 id>。确需向全局发消息时,以 `peer_send` 发至 <全局 id>。| 不得据 peer_list 推测 id
|
|
12
12
|
水位:≥32% 应先写毕交接、再通知全局,并提醒用户换会话;>35% 不得新开任务。
|
|
13
13
|
边界:只读写 <范围>。删除或覆盖前须先留快照,**删除须经用户确认**。对用户可见一律中文(含工具 description 字段)。
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
库根:Projects/<项目名>/<项目名>库/ ;元数据:_Project_Meta/<项目名>/ ;快照:_Temp/<项目名>/Snapshots/ ;版本冻结:_Archives/<项目名>/Backups/
|
|
38
38
|
开始作业前应先读:_Project_Meta/<项目名>/Handover.md → 库根 SKILL.md →(修改文档时)_Control_Center/rules/ 下对应规范。
|
|
39
39
|
两件必查:库内 RULES.md 里必须有「运行期省 token」与「上下文水位自查」两条;缺者应照 Workspace_Charter.md 原文补上(唯一来源,不得自行改动数字)。
|
|
40
|
-
|
|
40
|
+
换任自办,规则见插件注入的提示段;**不得自行调用 peer_new**,也不向全局请求换任。
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
### 2.3 杂项(只在首任时由全局发,此后自理)
|
|
@@ -47,25 +47,17 @@
|
|
|
47
47
|
你是杂项任务会话,标题「<任务短名>」,不编号。
|
|
48
48
|
任务目录:_Misc/tasks/<任务短名>/(产物、脚本、日志都放这里)
|
|
49
49
|
三件文件:Project_Overview.md(是什么/边界/验收判据)· Changelog.md(环境事实/变更/回滚,只追加)· Handover.md(在途)
|
|
50
|
-
|
|
50
|
+
换任自办(规则见插件注入的提示段):接任者名字与会话 id 由插件写进登记表并在旧交接末尾追加登记,不通知全局。
|
|
51
51
|
本任务:<任务正文>| 用户原话照抄,不改写、不缩范围、不替用户补验收判据
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
## 3 消息二 ·
|
|
54
|
+
## 3 消息二 · 换任(已停用)
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
【交接请求】<项目名>|<本会话 id>|水位 <xx>%
|
|
58
|
-
已写完 _Project_Meta/<项目名>/Handover.md(<行数> 行,已归档旧版到 _Archives/<项目名>/Handover/)。
|
|
59
|
-
在途:<有/无>;一句话:<现在卡在哪>。
|
|
60
|
-
请建接任会话。
|
|
61
|
-
```
|
|
56
|
+
> 换任由在任会话调用 `ho_succeed` 完成:用户授权、插件体检交接、分配编号、建立接任会话并投递启动提示。不再有「项目 → 全局:交接请求」的消息,也不再需要全局代办。本节保留编号,避免后续节号变动。
|
|
62
57
|
|
|
63
|
-
## 4 消息三 ·
|
|
58
|
+
## 4 消息三 · 全局代建(仅备选)
|
|
64
59
|
|
|
65
|
-
|
|
66
|
-
【已交接】<项目名>|接任已建:<项目名><N>|session id <新 id>
|
|
67
|
-
你的交接已归档,可以停止操作:不得新开任务,不得修改文件。
|
|
68
|
-
```
|
|
60
|
+
> 仅在自动换任失败、且用户要求时使用:全局用 `ho_sessions(action="new")` 建接任会话,标题按 `<项目名><N>`(N=本机下一个序号),启动提示必须带新会话自己的 session id、现任全局 session id 与旧会话 `Handover.md` 的绝对路径。建完请用户在旧会话里确认停止。
|
|
69
61
|
|
|
70
62
|
## 5 消息四 · 全局 → 项目:新任务派发
|
|
71
63
|
|
|
@@ -76,7 +68,7 @@
|
|
|
76
68
|
约束:<本次不许碰的东西;无则删本行>
|
|
77
69
|
```
|
|
78
70
|
|
|
79
|
-
>
|
|
71
|
+
> 派活前先查在任(`ho_office` 空参数,零成本);复用准则固定写在插件注入的提示段,本节不重复。
|
|
80
72
|
|
|
81
73
|
## 6 消息五 · 迁移通知(打包前刷交接)
|
|
82
74
|
|
|
@@ -91,6 +83,6 @@
|
|
|
91
83
|
|
|
92
84
|
## 7 发送纪律
|
|
93
85
|
|
|
94
|
-
>
|
|
86
|
+
> 汇报时机见插件注入的提示段;格式与频率纪律(不寒暄、一次一件事、并发 ≤3、未被要求勿唤醒、不转述)见 `rules/Workspace_Charter.md` §7.2.4 第 9 条,本节不重复。
|
|
95
87
|
>
|
|
96
88
|
> 本节只留一条**模板自身**的要求:发送前删掉所有 `|` 之后的提示与 `<>` 占位,只留实值。
|
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
> 这是**按需取回**的全文:系统提示里只留短段与指针,需要细节时用 `ho_charter(doc="runtime")` 取本文。
|
|
4
4
|
>
|
|
5
|
-
> **内置即默认**:本文、Charter 骨架、交接、台账骨架、提示词模板、四份规则文档都随插件发布(`dist/templates/`)。工作区里的同名文件只是「本地化覆盖」;删掉就自动回到内置,功能不降级。`ho_init`
|
|
5
|
+
> **内置即默认**:本文、Charter 骨架、交接、台账骨架、提示词模板、四份规则文档都随插件发布(`dist/templates/`)。工作区里的同名文件只是「本地化覆盖」;删掉就自动回到内置,功能不降级。`ho_init` 是把默认口径写入磁盘,便于修改,**不是使用前提**。
|
|
6
6
|
> 口径冲突时以工作区的 `_Control_Center/rules/` 为准;本文只讲「插件怎么用、角色怎么分」。
|
|
7
7
|
|
|
8
8
|
## 1. 角色四档
|
|
9
9
|
|
|
10
10
|
| 角色 | 判定顺序 | 交接文件 | 可见工具 |
|
|
11
11
|
|---|---|---|---|
|
|
12
|
-
| 全局 | 角色表 → 标题含「全局」 → 全局工作区 | `_Control_Center/records/Global_Handover.md` |
|
|
13
|
-
| 项目 | 角色表(带项目名)→ cwd 在 `Projects/<名>/` → 首条提示含 `Projects/<名>/` | `_Project_Meta/<项目>/Handover.md` |
|
|
14
|
-
| 杂项 | 角色表(带任务名)→ cwd 在 `_Misc/tasks/<任务>/` | `_Misc/tasks/<任务>/Handover.md` |
|
|
12
|
+
| 全局 | 角色表 → 标题含「全局」 → 全局工作区 | `_Control_Center/records/Global_Handover.md` | 24 个(全部) |
|
|
13
|
+
| 项目 | 角色表(带项目名)→ cwd 在 `Projects/<名>/` → 首条提示含 `Projects/<名>/` | `_Project_Meta/<项目>/Handover.md` | 11 个 |
|
|
14
|
+
| 杂项 | 角色表(带任务名)→ cwd 在 `_Misc/tasks/<任务>/` | `_Misc/tasks/<任务>/Handover.md` | 11 个 |
|
|
15
15
|
| 普通 | 子会话,或都不中 | 无(不参与交接) | 0 个(不注入提示段) |
|
|
16
16
|
|
|
17
17
|
角色表在 `~/.dsh/storages/dsh-handover.roles.json`,是**本机状态**,不进仓库、不进迁移包。子会话默认普通;需要时由全局在角色表里指定。
|
|
18
18
|
设定入口:设置 → dsh-handover →「会话角色」;或对全局说「把 Demo1 设成项目角色」,它用 `ho_sessions(action="role")` 办。
|
|
19
19
|
|
|
20
|
-
## 2. 工具清单(
|
|
20
|
+
## 2. 工具清单(24 个)
|
|
21
21
|
|
|
22
22
|
### 只读
|
|
23
23
|
- `ho_brief` —— 起始与收束的单条命令:水位、闸门、会话清点、阶段建议。
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
- `ho_drift` —— 交接漂移:Δ=当前轮−交接轮,>3 需刷新。
|
|
29
29
|
- `ho_diag` —— 插件自检:服务、调度器、压缩计数、工作区、角色判定。
|
|
30
30
|
- `ho_tokens` —— 会话 token 体检:轮数、步数、每步 token、最贵单条返回。
|
|
31
|
+
- `ho_office` —— 查在任:输入「全局」「全局3」「项目名」等,返回现任会话的名字与会话 id;多个名字空格分隔,留空列出全部。
|
|
31
32
|
- `peer_list` / `peer_status` / `peer_peek` —— 平级会话清点与近期对话(peek 会花本会话 token)。
|
|
32
33
|
|
|
33
34
|
### 写
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
- `ho_workspace` —— 工作区 list、create、delete(删除只删注册)与换盘动作。
|
|
40
41
|
- `ho_init` —— 首次安装:在指定目录下建整套全局文件体系,可顺带注册工作区。
|
|
41
42
|
- `ho_profile` —— 项目或杂项的骨架注册:由所属会话通读自己的库后写入并校验。
|
|
43
|
+
- `ho_succeed` —— 换任:用户授权后校验交接、按记录分配编号、建立接任会话、登记在任并投递启动提示词。
|
|
42
44
|
- `peer_send` / `peer_new` / `peer_rename` —— 投递消息、建平级会话、只改标题。
|
|
43
45
|
|
|
44
46
|
### ho_sessions 的 action
|
|
@@ -57,9 +59,10 @@
|
|
|
57
59
|
|
|
58
60
|
- 32% 提醒、35% 交接线、85% 硬顶;同一会话最多压缩 3 次(共 4 段使用)。数值来自 `config.mjs`,提示段与设置页由 `lib/facts.mjs` 统一渲染。
|
|
59
61
|
- 到线流程:`ho_handover` 写交接(体检不通过就拒绝压缩)→ `ho_checkpoint` 登记(`resume=continue|wait`)→ 轮末自动压缩 → 继续时投续跑消息;收尾时不唤醒,等用户派新任务。
|
|
60
|
-
- 第 4
|
|
62
|
+
- 第 4 段到线或压缩额度用尽:由在任会话自己换任——写交接 → 请用户在聊天框敲 `/allow-succeed` 授权 → 调 `ho_succeed`(校验交接、分配编号、建立接任会话、登记在任、投递提示词)。全局仍可用 `ho_sessions(action="new")` 手动建立会话,作为自动流程失败时的备选方式。
|
|
61
63
|
- 关键步骤不要压:`ho_checkpoint` 传 `at_risk=true`,它只提醒不压;硬顶 85% 除外。
|
|
62
64
|
- 压缩不可逆:历史会被摘要替换,所以「先把交接写合格」是硬前提。
|
|
65
|
+
- 在任登记表由插件维护:`_Control_Center/records/Office.json` 记每一类会话的现任与会话 id;查现任用 `ho_office`,不起会话时用 `node scripts/office.mjs 全局 全局3`。
|
|
63
66
|
|
|
64
67
|
## 4. 交接文件的结构要求
|
|
65
68
|
|
|
@@ -124,14 +127,14 @@
|
|
|
124
127
|
|
|
125
128
|
### 12.1 分层(谁在每轮付费)
|
|
126
129
|
|
|
127
|
-
| 层 | 内容 | 何时进入上下文 |
|
|
130
|
+
| 层 | 内容 | 何时进入上下文 | 实测参考(加权 token) |
|
|
128
131
|
|---|---|---|---|
|
|
129
|
-
| L0 常驻 |
|
|
132
|
+
| L0 常驻 | 角色段(全局五节;项目、杂项各节) | **每一轮** | 全局约 385;项目与杂项约 370;长路径与迁移提示档约 435、419(参考值,不设上限) |
|
|
130
133
|
| L0 用户内容 | 全局前置提示词(用户开关,默认关闭) | 开启且非空时每轮 | 由用户自管,不计入本插件预算 |
|
|
131
134
|
| L1 手册 | 本文件 | 按需取回,可**按节**取 | 不设上限(不进每轮) |
|
|
132
135
|
| L2 规则文档 | 工作区 `_Control_Center/rules/` | 按需 | 由工作区自管 |
|
|
133
136
|
|
|
134
|
-
> 加权 token 的算法:CJK 与全角标点每字符 1 token,其余每 4 字符 1 token
|
|
137
|
+
> 加权 token 的算法:CJK 与全角标点每字符 1 token,其余每 4 字符 1 token。实测常量在 `lib/facts.mjs` 的 `TOKEN_BUDGET`,冒烟只打印实测值,**不设上限**,也不用于拒绝或截断。
|
|
135
138
|
>
|
|
136
139
|
> L0 是成本主体:它按轮次乘算,所以只放「每轮都要用到的判定」。任何「一个会话生命周期里只用几次」的流程,一律以指针落到 L1。
|
|
137
140
|
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
## 2. 什么时候写
|
|
17
17
|
|
|
18
|
-
- 水位 **32
|
|
18
|
+
- 水位 **32%**:开始收尾,不要开新任务。
|
|
19
19
|
- 水位 **35%**:必须写完交接 → `ho_checkpoint` 登记 → 轮末自动压缩 → 续跑。
|
|
20
20
|
- 水位 **85%**:硬顶,不压也得压。
|
|
21
|
-
- 同一会话最多压 3 次;第 4
|
|
21
|
+
- 同一会话最多压 3 次;第 4 段写交接,用户授权后由在任会话调用 `ho_succeed` 换任(**不得自行新建会话**)。
|
|
22
22
|
|
|
23
23
|
## 3. 必须写清(缺一项就会在换任时丢信息)
|
|
24
24
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## 1. 什么算杂项
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
一次性或短期任务:不建项目库、不进 `_Project_Meta/Registry.md`。落点 `_Misc/tasks/<任务短名>/`。
|
|
6
6
|
|
|
7
7
|
## 2. 目录约定
|
|
8
8
|
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
## 3. 边界
|
|
16
16
|
|
|
17
|
-
- 只写上面三处;`_Control_Center`、`Projects
|
|
18
|
-
-
|
|
17
|
+
- 只写上面三处;`_Control_Center`、`Projects`、其他任务目录都不得写入。
|
|
18
|
+
- 接任:杂项须用户授权(`/allow-succeed` 或设置页),再由本会话调用 `ho_succeed`,由插件按登记表建立接任会话;项目会话不得自行新建会话。
|
|
19
19
|
- 交接与水位协议同项目(32% 提醒 / 35% 交接线 / 85% 硬顶)。
|
|
20
20
|
|
|
21
21
|
## 4. 升级为项目
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** ho_office —— 在任登记表:查某条类别现在由哪个会话在任(只读)。 */
|
|
2
|
+
import { textOutput } from '../lib/output.mjs';
|
|
3
|
+
import { nameOf } from '../lib/names.mjs';
|
|
4
|
+
import { NL } from '../lib/text.mjs';
|
|
5
|
+
import { loadOffice, resolveOffice, rowsOf, formatRow, officePath } from '../lib/office.mjs';
|
|
6
|
+
|
|
7
|
+
export function pmOfficeTool(env) {
|
|
8
|
+
return {
|
|
9
|
+
name: nameOf('office'),
|
|
10
|
+
description:
|
|
11
|
+
'查在任:输入「全局」「全局3」「项目名」等,返回现任会话的名字与 id;多个用空格分隔,留空列全部。只读。',
|
|
12
|
+
parameters: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
properties: {
|
|
16
|
+
query: { type: 'string', description: '要查的名字,如「全局」「全局3」「Mingent」「抢名额脚本」;多个用空格或逗号分隔;留空=列出全部在任' },
|
|
17
|
+
json: { type: 'boolean', description: 'true 时输出机器可读 JSON(供脚本解析)' },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
output: textOutput(),
|
|
21
|
+
async execute(args, exec) {
|
|
22
|
+
const ws = env.resolveWs(exec);
|
|
23
|
+
if (!ws) return { text: '无法确定工作区根,读不到在任登记表。' };
|
|
24
|
+
const store = loadOffice(ws);
|
|
25
|
+
const rows = rowsOf(store);
|
|
26
|
+
if (!rows.length) {
|
|
27
|
+
return { text: '在任登记表还是空的(' + officePath(ws) + '):先登记一次在任会话,再查。' };
|
|
28
|
+
}
|
|
29
|
+
const raw = String(args.query || '').trim();
|
|
30
|
+
const tokens = raw ? raw.split(/[\s,,、;;]+/).filter(Boolean) : [];
|
|
31
|
+
if (!tokens.length) {
|
|
32
|
+
if (args.json) return { text: JSON.stringify({ updatedAt: store.updatedAt || '', rows }, null, 2) };
|
|
33
|
+
const out = [];
|
|
34
|
+
let lastKind = '';
|
|
35
|
+
for (const r of rows) {
|
|
36
|
+
if (r.kind !== lastKind) {
|
|
37
|
+
if (out.length) out.push('');
|
|
38
|
+
out.push('【' + (r.kind === 'global' ? '全局' : r.kind === 'project' ? '项目' : r.kind === 'misc' ? '杂项' : r.kind) + '】');
|
|
39
|
+
lastKind = r.kind;
|
|
40
|
+
}
|
|
41
|
+
out.push(formatRow(r));
|
|
42
|
+
}
|
|
43
|
+
return { text: out.join(NL) };
|
|
44
|
+
}
|
|
45
|
+
const out = [];
|
|
46
|
+
const hits = [];
|
|
47
|
+
for (const t of tokens) {
|
|
48
|
+
const r = resolveOffice(store, t);
|
|
49
|
+
if (!r.ok) { out.push(t + ' — ' + r.reason); continue; }
|
|
50
|
+
if (r.vacant) { out.push(t + ' — 类别「' + r.key + '」当前空缺(无人在任)'); continue; }
|
|
51
|
+
const row = { key: r.key, kind: r.lineage.kind, project: r.lineage.project, task: r.lineage.task, current: r.lineage.current };
|
|
52
|
+
out.push(formatRow(row, t));
|
|
53
|
+
hits.push({ askedAs: t, via: r.via, key: r.key, kind: r.lineage.kind, name: r.lineage.current.name, sessionId: r.lineage.current.sessionId });
|
|
54
|
+
}
|
|
55
|
+
if (args.json) return { text: JSON.stringify({ updatedAt: store.updatedAt || '', hits, lines: out }, null, 2) };
|
|
56
|
+
return { text: out.join(NL) };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** ho_succeed —— 换任自服务:在任会话在用户授权后,自行校验交接、建接任会话并登记在任。 */
|
|
2
|
+
import { textOutput } from '../lib/output.mjs';
|
|
3
|
+
import { nameOf } from '../lib/names.mjs';
|
|
4
|
+
import { runSuccession } from '../lib/succession.mjs';
|
|
5
|
+
|
|
6
|
+
export function pmSucceedTool(env) {
|
|
7
|
+
return {
|
|
8
|
+
name: nameOf('succeed'),
|
|
9
|
+
description:
|
|
10
|
+
'换任:校验本会话交接,建接任会话并登记在任;须用户先授权(/allow-succeed)。',
|
|
11
|
+
parameters: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
additionalProperties: false,
|
|
14
|
+
properties: {
|
|
15
|
+
note: { type: 'string', description: '写进在任登记的备注(可选)' },
|
|
16
|
+
force: { type: 'boolean', description: '本会话水位未到线时仍换任(默认不传)' },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
output: textOutput(),
|
|
20
|
+
async execute(args, exec) {
|
|
21
|
+
return { text: await runSuccession(env, exec, { note: args && args.note, force: args && args.force === true }) };
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|