@mingmaple/dsh-handover 0.9.13 → 0.9.14
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 +1 -1
- package/docs/releases/0.9.14.md +18 -0
- package/package.json +1 -1
- package/scripts/blank-smoke.mjs +65 -0
- package/src/lib/session-ops.mjs +33 -4
- package/src/lib/succession.mjs +21 -5
package/README.md
CHANGED
|
@@ -146,7 +146,7 @@ npm run test:docs # 文档校验:工具名、旧数、链
|
|
|
146
146
|
|
|
147
147
|
## 版本
|
|
148
148
|
|
|
149
|
-
当前版本 0.9.
|
|
149
|
+
当前版本 0.9.14,首次公开版本为 0.9.6。内部版本史不随仓库发布。
|
|
150
150
|
|
|
151
151
|
支持的内核为 0.2.1-rc.2 至 0.3.0(不含 0.3.0 及其候选版),见 `package.json` 的 `engines.dsh`;0.2.2-rc.1 上的工具装配、角色裁剪与压缩调度已实测通过。
|
|
152
152
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# 0.9.14
|
|
2
|
+
|
|
3
|
+
修三处与新建会话、换任相关的路径与登记问题,均在真机演练中复现后修复。
|
|
4
|
+
|
|
5
|
+
## 修复
|
|
6
|
+
|
|
7
|
+
- 新建杂项会话:启动提示词里的交接路径此前按调用者角色推算,杂项被当成全局,指向全局交接文件。现按新会话自身的角色取路径:杂项读 `_Misc/tasks/<任务>/Handover.md`,项目读 `_Project_Meta/<项目>/Handover.md`;杂项没有任务交接时退回杂项任务标准。强制交接路径的配置项只对全局角色生效。
|
|
8
|
+
- 在任登记表的工作区一栏改记会话实际所在的工作区,不再记调用者所在的工作区根。
|
|
9
|
+
- 杂项换任:接任会话此前落在工作区根,现落回本会话所在的注册工作区。做法是按会话工作目录反查工作区注册表,反查不到时才退回工作区根。
|
|
10
|
+
- 换任启动提示词里的「全局 id」改为现任全局会话。此前填入的是发起换任的前任会话,项目与杂项的接任者因此被指向已经退休的会话。
|
|
11
|
+
|
|
12
|
+
## 测试
|
|
13
|
+
|
|
14
|
+
冒烟新增三条回归断言:新建杂项会话的启动提示读本任务交接、杂项接任者落回本会话所在的工作区、接任提示的全局 id 为现任全局。
|
|
15
|
+
|
|
16
|
+
## 升级
|
|
17
|
+
|
|
18
|
+
先移除旧版本再安装;或替换文件后在插件列表停用再启用。已存在的在任登记表无需改动,工作区一栏在下次登记时自然更新。
|
package/package.json
CHANGED
package/scripts/blank-smoke.mjs
CHANGED
|
@@ -1019,6 +1019,71 @@ try {
|
|
|
1019
1019
|
} catch (e) { newErr = String(e && e.message ? e.message : e); }
|
|
1020
1020
|
ok(/工作区/.test(newErr) && /已注册的工作区/.test(newErr), '新建会话缺工作区时应报错并列出可选工作区', newErr.slice(0, 160));
|
|
1021
1021
|
|
|
1022
|
+
// 回归锁(0.9.14):① 新建杂项会话的启动提示读本任务交接,不退回全局交接;
|
|
1023
|
+
// ② 杂项换任的接任者落回本会话所在的注册工作区;③ 接任提示里的「全局 id」是现任全局,不是退休前任。
|
|
1024
|
+
{
|
|
1025
|
+
const { registerHolder } = await import(DIST('lib/office.mjs'));
|
|
1026
|
+
const storages = path.join(BLANK, '.dsh', 'storages');
|
|
1027
|
+
fs.mkdirSync(storages, { recursive: true });
|
|
1028
|
+
const miscWs = path.join(WS, '_Misc');
|
|
1029
|
+
fs.writeFileSync(path.join(storages, 'workspace.json'), JSON.stringify({
|
|
1030
|
+
tables: { workspaces: {
|
|
1031
|
+
'ws-glob': { id: 'ws-glob', title: '全局', path: WS },
|
|
1032
|
+
'ws-misc': { id: 'ws-misc', title: '_Misc', path: miscWs },
|
|
1033
|
+
} },
|
|
1034
|
+
}), 'utf8');
|
|
1035
|
+
const seen = { creates: [], prompts: [] };
|
|
1036
|
+
ctx.sessionController.create = async (a) => { seen.creates.push(a || {}); return { sessionId: 'session-made-' + seen.creates.length }; };
|
|
1037
|
+
ctx.sessionController.rename = async () => { };
|
|
1038
|
+
ctx.sessionController.prompt = async (a) => {
|
|
1039
|
+
const part = a && Array.isArray(a.content) ? a.content[0] : null;
|
|
1040
|
+
seen.prompts.push(String(part && part.text ? part.text : ''));
|
|
1041
|
+
return { accepted: true };
|
|
1042
|
+
};
|
|
1043
|
+
const makerExec2 = { name: 'ho_sessions', arguments: {}, agent: { session: { header: { id: 'session-maker2', cwd: WS } } } };
|
|
1044
|
+
const sessT = reg.tools.find((t) => t.name === 'ho_sessions');
|
|
1045
|
+
{
|
|
1046
|
+
let createdOut = '';
|
|
1047
|
+
try {
|
|
1048
|
+
const r = await sessT.execute({ action: 'new', confirm: true, title: '杂项2', kind: 'misc', role: 'misc', task: '抢名额', workspace: 'ws-misc' }, makerExec2);
|
|
1049
|
+
createdOut = String((r && r.text) || '');
|
|
1050
|
+
} catch (e) { createdOut = 'ERR ' + String(e && e.message ? e.message : e); }
|
|
1051
|
+
const p = seen.prompts[seen.prompts.length - 1] || '';
|
|
1052
|
+
ok(/已建会话/.test(createdOut), '新建杂项会话应成功', createdOut.slice(0, 200));
|
|
1053
|
+
ok(/_Misc[\\/]tasks[\\/]抢名额[\\/]Handover\.md/.test(p), '新建杂项会话的启动提示应读本任务交接', p.slice(0, 200));
|
|
1054
|
+
ok(p.indexOf('Global_Handover') === -1, '新建杂项会话的启动提示不得指向全局交接', p.slice(0, 200));
|
|
1055
|
+
}
|
|
1056
|
+
{
|
|
1057
|
+
const { runSuccession } = await import(DIST('lib/succession.mjs'));
|
|
1058
|
+
const { grantConsent } = await import(DIST('lib/consent.mjs'));
|
|
1059
|
+
const miscHandover = path.join(miscWs, 'tasks', '抢名额', 'Handover.md');
|
|
1060
|
+
fs.mkdirSync(path.dirname(miscHandover), { recursive: true });
|
|
1061
|
+
fs.writeFileSync(miscHandover, '# 抢名额交接' + '\n\n## 下一步\n- 等用户指派下一步\n', 'utf8');
|
|
1062
|
+
registerHolder(WS, { kind: 'global', name: '全局1', sessionId: 'session-global-cur', workspace: WS });
|
|
1063
|
+
registerHolder(WS, { kind: 'misc', task: '抢名额', name: '杂项1', sessionId: 'session-misc-old', workspace: miscWs });
|
|
1064
|
+
grantConsent('session-misc-old', 'blank-smoke', 'succeed');
|
|
1065
|
+
const succOut = await runSuccession({
|
|
1066
|
+
cfg: resolveConfig({ workspaceRoot: WS }),
|
|
1067
|
+
roleOf: () => ({ role: 'misc', task: '抢名额', project: '' }),
|
|
1068
|
+
selfId: () => 'session-misc-old',
|
|
1069
|
+
selfCwd: () => miscWs,
|
|
1070
|
+
handoverFor: () => ({ path: miscHandover, reason: '' }),
|
|
1071
|
+
resolveWs: () => WS,
|
|
1072
|
+
selfPct: () => 40,
|
|
1073
|
+
registry: () => ({ list: () => [] }),
|
|
1074
|
+
controller: () => ctx.sessionController,
|
|
1075
|
+
writeRole: () => { },
|
|
1076
|
+
}, null, {});
|
|
1077
|
+
ok(/换任完成/.test(succOut), '杂项换任应能走通(授权、体检、建会话、登记、投递)', succOut.split('\n').join(' ').slice(0, 240));
|
|
1078
|
+
const madeArgs = seen.creates[seen.creates.length - 1] || {};
|
|
1079
|
+
ok(madeArgs.workspaceId === 'ws-misc', '杂项接任者应落回本会话所在的工作区', JSON.stringify(madeArgs));
|
|
1080
|
+
const p = seen.prompts[seen.prompts.length - 1] || '';
|
|
1081
|
+
ok(new RegExp('.{0,1}抢名额[\\\\/]Handover\\.md').test(p), '接任提示末尾应带本任务交接路径', p.slice(-320));
|
|
1082
|
+
ok(p.indexOf('session-global-cur') !== -1, '接任提示的「全局 id」应是现任全局', p.slice(-320));
|
|
1083
|
+
ok(p.indexOf('session-misc-old') === -1, '接任提示不得把退休前任当全局', p.slice(-320));
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1022
1087
|
fs.rmSync(BLANK, { recursive: true, force: true });
|
|
1023
1088
|
console.log('');
|
|
1024
1089
|
if (fails.length) {
|
package/src/lib/session-ops.mjs
CHANGED
|
@@ -94,6 +94,28 @@ export function resolveWorkspaceFor(env, exec, kind, project, task, opts) {
|
|
|
94
94
|
+ '全局会话落在标题含「全局」的工作区;项目会话同时传 project 与该项目的工作区;杂项会话传该项目外的独立工作区。');
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* 某个目录是不是已注册工作区的根:是则返回 workspaceId,否则返回空串(不抛错)。
|
|
99
|
+
* 换任时用来让接任者落回本会话所在的那个工作区,而不是工作区根。
|
|
100
|
+
*/
|
|
101
|
+
export function workspaceIdOfPath(env, p) {
|
|
102
|
+
const want = normPath(p);
|
|
103
|
+
if (!want || want === '-') return '';
|
|
104
|
+
const hit = listWorkspaces(env).find((r) => {
|
|
105
|
+
const rp = normPath(String(r.path || ''));
|
|
106
|
+
return rp && rp !== '-' && rp === want;
|
|
107
|
+
});
|
|
108
|
+
return hit ? String(hit.id || '') : '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** workspaceId 对应的根路径;查不到返回空串(登记表的工作区一栏用它写成实际落点)。 */
|
|
112
|
+
export function workspacePathOf(env, id) {
|
|
113
|
+
const key = String(id || '');
|
|
114
|
+
if (!key) return '';
|
|
115
|
+
const hit = listWorkspaces(env).find((r) => String(r.id || '') === key);
|
|
116
|
+
return hit && hit.path ? String(hit.path) : '';
|
|
117
|
+
}
|
|
118
|
+
|
|
97
119
|
/** 已注册工作区清单(插件服务优先,退化读 workspace.json):新建会话时用来列出可选项。 */
|
|
98
120
|
function listWorkspaces(env) {
|
|
99
121
|
try {
|
|
@@ -197,16 +219,23 @@ export async function sessionNew(env, args, exec) {
|
|
|
197
219
|
// 建会话即登记在任:全局手工建/换会话与自服务换任共用一张登记表,不让注册表漏记或与实况分叉。
|
|
198
220
|
try {
|
|
199
221
|
if (officeKind) {
|
|
200
|
-
|
|
222
|
+
// 工作区一栏写**实际落点**(用户指定的那个工作区),不是调用者自己所在的工作区根。
|
|
223
|
+
const reg = registerHolder(ws, {
|
|
224
|
+
kind: officeKind, project, task, name: title, sessionId,
|
|
225
|
+
workspace: workspacePathOf(env, workspaceId) || ws,
|
|
226
|
+
});
|
|
201
227
|
out.push('在任登记:' + reg.key + ' → ' + title + (reg.previous ? '(原 ' + reg.previous.name + ' 进历史)' : ''));
|
|
202
228
|
}
|
|
203
229
|
} catch (e) {
|
|
204
230
|
out.push('在任登记失败:' + String(e && e.message ? e.message : e) + '(不影响会话本身)');
|
|
205
231
|
}
|
|
206
232
|
const meId = env.selfId(exec) || '(未知)';
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
const
|
|
233
|
+
// 交接路径按**新会话自身的角色**取用:项目读项目交接、杂项读任务交接,都不退回全局交接。
|
|
234
|
+
// cfg.handoverPath 是「强制全局交接路径」的演练开关,只对全局角色生效。
|
|
235
|
+
const hp = handoverPathFor(ws, finalRole === 'project' && project ? { role: 'project', project }
|
|
236
|
+
: finalRole === 'misc' && task ? { role: 'misc', task } : { role: 'global' });
|
|
237
|
+
const handover = (finalRole === 'global' ? env.cfg.handoverPath : '') || hp.path
|
|
238
|
+
|| (finalRole === 'misc' ? path.join(ws, '_Control_Center', 'rules', 'Misc_Task_Standard.md') : '');
|
|
210
239
|
const text = args.message || [
|
|
211
240
|
'先读交接:' + handover + '。',
|
|
212
241
|
'读毕先向用户报告,不得自行开展作业:一、当前停在哪一步;二、交接列有哪些待办;三、建议先做哪一条。待用户指派后再动手。交接无待办时如实说明,不得编造任务。',
|
package/src/lib/succession.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { lineageKeyOf, loadOffice, nextName, registerHolder, stampNow } from './
|
|
|
12
12
|
import { checkConsent, consumedConsent } from './consent.mjs';
|
|
13
13
|
import { newSessionIn } from './session-create.mjs';
|
|
14
14
|
import { sendPrompt } from './prompt.mjs';
|
|
15
|
-
import { resolveWorkspaceFor, promptFor } from './session-ops.mjs';
|
|
15
|
+
import { resolveWorkspaceFor, promptFor, workspaceIdOfPath, workspacePathOf } from './session-ops.mjs';
|
|
16
16
|
import { watermark } from './facts.mjs';
|
|
17
17
|
|
|
18
18
|
/** 交接里不得残留的占位符。 */
|
|
@@ -22,6 +22,14 @@ function msgOf(e) {
|
|
|
22
22
|
return String(e && e.message ? e.message : e);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** 现任全局会话 id(登记表是唯一事实源);没有记录或读不到时返回空串。 */
|
|
26
|
+
function currentGlobalId(ws) {
|
|
27
|
+
try {
|
|
28
|
+
const lin = loadOffice(ws).lineages['全局'];
|
|
29
|
+
return lin && lin.current && lin.current.sessionId ? String(lin.current.sessionId) : '';
|
|
30
|
+
} catch { return ''; }
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
/**
|
|
26
34
|
* 找接任指令节:含「## 下一步」或「## 2.」这类标题;没有标题时接受 §2 的写法。
|
|
27
35
|
* 判据与 ho_gate 的交接体检一致:这一节至少要有一个条目(列表、表格或编号行)。
|
|
@@ -165,18 +173,26 @@ export async function runSuccession(env, exec, opts) {
|
|
|
165
173
|
// 5. 分配编号:登记表历史最大号加一,不采用会话自述。
|
|
166
174
|
const name = nextName(store, key);
|
|
167
175
|
|
|
168
|
-
// 6. 确定工作区:与 ho_sessions(action="new")
|
|
176
|
+
// 6. 确定工作区:与 ho_sessions(action="new") 同一套解析。
|
|
177
|
+
// 杂项接任者落回**本会话所在的注册工作区**(按会话 cwd 反查);反查不到才退回工作区根。
|
|
169
178
|
let workspaceId = '';
|
|
170
179
|
try {
|
|
171
|
-
|
|
180
|
+
let wsOpt = {};
|
|
181
|
+
if (kind === 'misc') {
|
|
182
|
+
const selfCwd = typeof env.selfCwd === 'function' ? String(env.selfCwd(exec) || '') : '';
|
|
183
|
+
wsOpt = selfCwd && workspaceIdOfPath(env, selfCwd) ? { cwd: selfCwd } : { workspace: ws };
|
|
184
|
+
}
|
|
185
|
+
workspaceId = resolveWorkspaceFor(env, exec, kind, project, task, wsOpt);
|
|
172
186
|
} catch (e) {
|
|
173
187
|
return reject(msgOf(e));
|
|
174
188
|
}
|
|
175
189
|
|
|
176
190
|
// 7. 拼启动提示词:末尾追加交接路径与「先读交接、读毕报到」。
|
|
191
|
+
// 「全局 id」填**现任全局**:项目与杂项换任时,前任不是全局,不能顶替这个位置。
|
|
177
192
|
let prompt = '';
|
|
178
193
|
try {
|
|
179
|
-
const
|
|
194
|
+
const globalId = kind === 'global' ? selfId : (currentGlobalId(ws) || selfId);
|
|
195
|
+
const base = promptFor(env, { ws, kind, project, task, title: name, sessionId: '', globalSessionId: globalId, extra: '' });
|
|
180
196
|
prompt = base + NL + NL + [
|
|
181
197
|
'接任交接:' + handoverFile,
|
|
182
198
|
'先读这份交接再动手;读毕向用户报告当前停在哪一步、交接列有哪些待办,然后等待用户指派任务。',
|
|
@@ -195,7 +211,7 @@ export async function runSuccession(env, exec, opts) {
|
|
|
195
211
|
// 9. 登记在任:原在任者自动进历史,Registry.md 在役列同步。
|
|
196
212
|
let reg = null;
|
|
197
213
|
try {
|
|
198
|
-
reg = registerHolder(ws, { kind, project, task, name, sessionId, workspace: ws, note });
|
|
214
|
+
reg = registerHolder(ws, { kind, project, task, name, sessionId, workspace: workspacePathOf(env, workspaceId) || ws, note });
|
|
199
215
|
} catch (e) {
|
|
200
216
|
return [
|
|
201
217
|
'会话已建 ' + sessionId + '(' + name + '),但登记在任失败:' + msgOf(e),
|