@mingmaple/dsh-handover 0.9.12 → 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 +15 -10
- package/docs/releases/0.9.12.md +28 -0
- package/docs/releases/0.9.13.md +30 -0
- package/docs/releases/0.9.14.md +18 -0
- package/docs/troubleshoot.md +11 -3
- package/package.json +1 -1
- package/scripts/blank-dsh.mjs +6 -5
- package/scripts/blank-smoke.mjs +137 -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 +179 -92
- package/src/lib/succession.mjs +260 -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,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 在任登记表(office):回答「这条类别现在谁在任」。
|
|
3
|
+
*
|
|
4
|
+
* 记录保存在 <工作区根>/_Control_Center/records/Office.json:每一类会话(全局/项目名/杂项任务名)
|
|
5
|
+
* 记一个在任会话与历史编号。Registry.md 的「在役会话」列由本文件定点改写——只换那一格,
|
|
6
|
+
* 不动人写的状态文字与整表结构。编号由这里分配(历史最大号 + 1),不采信会话自述。
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { NL, readUtf8 } from './text.mjs';
|
|
11
|
+
|
|
12
|
+
export const OFFICE_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
/** 带时区偏移的时刻文本(与交接、登记表同一写法)。 */
|
|
15
|
+
export function stampNow() {
|
|
16
|
+
const d = new Date(Date.now() + 8 * 3600 * 1000);
|
|
17
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
18
|
+
return d.getUTCFullYear() + '-' + p(d.getUTCMonth() + 1) + '-' + p(d.getUTCDate())
|
|
19
|
+
+ ' ' + p(d.getUTCHours()) + ':' + p(d.getUTCMinutes()) + ' +0800';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const KIND_LABEL = { global: '全局', project: '项目', misc: '杂项' };
|
|
23
|
+
|
|
24
|
+
export function officePath(ws) {
|
|
25
|
+
return path.join(String(ws || ''), '_Control_Center', 'records', 'Office.json');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loadOffice(ws) {
|
|
29
|
+
let v = null;
|
|
30
|
+
try { v = JSON.parse(readUtf8(officePath(ws)).replace(/^\uFEFF/, '')); } catch { v = null; }
|
|
31
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) v = {};
|
|
32
|
+
if (!v.lineages || typeof v.lineages !== 'object' || Array.isArray(v.lineages)) v.lineages = {};
|
|
33
|
+
v.version = OFFICE_VERSION;
|
|
34
|
+
return v;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 原子写:临时文件 + rename,半截文件不会被下一次读当成事实。 */
|
|
38
|
+
export function saveOffice(ws, store) {
|
|
39
|
+
const p = officePath(ws);
|
|
40
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
41
|
+
store.version = OFFICE_VERSION;
|
|
42
|
+
store.updatedAt = stampNow();
|
|
43
|
+
const tmp = p + '.tmp';
|
|
44
|
+
fs.writeFileSync(tmp, JSON.stringify(store, null, 2) + NL, 'utf8');
|
|
45
|
+
fs.renameSync(tmp, p);
|
|
46
|
+
return p;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 类别名:全局固定「全局」,项目用项目名,杂项用任务名。 */
|
|
50
|
+
export function lineageKeyOf(kind, project, task) {
|
|
51
|
+
if (kind === 'global') return '全局';
|
|
52
|
+
if (kind === 'project') return String(project || '').trim();
|
|
53
|
+
if (kind === 'misc') return String(task || '').trim();
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const GEN_RE = /^(.*\D)(\d+)$/;
|
|
58
|
+
|
|
59
|
+
/** 名字去掉尾部代际:全局11 → 全局;没有数字返回空串。 */
|
|
60
|
+
export function baseOf(name) {
|
|
61
|
+
const m = GEN_RE.exec(String(name || '').trim());
|
|
62
|
+
return m ? m[1] : '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 名字里的代际号:全局11 → 11;没有数字返回 null。 */
|
|
66
|
+
export function generationOf(name) {
|
|
67
|
+
const m = GEN_RE.exec(String(name || '').trim());
|
|
68
|
+
return m ? Number(m[2]) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 下一编号:该类别历史上出现过的最大号 + 1;没有历史就从 1 起算。 */
|
|
72
|
+
export function nextName(store, key) {
|
|
73
|
+
const lin = store && store.lineages ? store.lineages[key] : null;
|
|
74
|
+
const names = [];
|
|
75
|
+
if (lin && lin.current && lin.current.name) names.push(lin.current.name);
|
|
76
|
+
for (const h of (lin && Array.isArray(lin.history) ? lin.history : [])) {
|
|
77
|
+
if (h && h.name) names.push(h.name);
|
|
78
|
+
}
|
|
79
|
+
let max = 0;
|
|
80
|
+
for (const n of names) {
|
|
81
|
+
const g = generationOf(n);
|
|
82
|
+
if (g !== null && g > max) max = g;
|
|
83
|
+
}
|
|
84
|
+
return String(key) + String(max + 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 名字 → 类别。用户给的多半是不确定的名字(全局3、全局、Mingent、赢了两次的旧叫法),
|
|
89
|
+
* 按「类别名精确 → 去尾部编号 → 现任名 → 唯一子串」四步收敛;收敛不了就报候选,不瞎猜。
|
|
90
|
+
*/
|
|
91
|
+
export function resolveOffice(store, query) {
|
|
92
|
+
const q = String(query || '').trim();
|
|
93
|
+
if (!q) return { ok: false, reason: '空查询' };
|
|
94
|
+
const keys = Object.keys((store && store.lineages) || {});
|
|
95
|
+
const lower = q.toLowerCase();
|
|
96
|
+
let key = '';
|
|
97
|
+
let via = '';
|
|
98
|
+
const exact = keys.filter((k) => k.toLowerCase() === lower);
|
|
99
|
+
if (exact.length === 1) { key = exact[0]; via = 'exact'; }
|
|
100
|
+
if (!key) {
|
|
101
|
+
const base = baseOf(q);
|
|
102
|
+
if (base) {
|
|
103
|
+
const hit = keys.filter((k) => k.toLowerCase() === base.toLowerCase());
|
|
104
|
+
if (hit.length === 1) { key = hit[0]; via = 'generation'; }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!key) {
|
|
108
|
+
const hit = keys.filter((k) => {
|
|
109
|
+
const cur = store.lineages[k] && store.lineages[k].current;
|
|
110
|
+
return cur && cur.name && String(cur.name).toLowerCase() === lower;
|
|
111
|
+
});
|
|
112
|
+
if (hit.length === 1) { key = hit[0]; via = 'current'; }
|
|
113
|
+
}
|
|
114
|
+
if (!key) {
|
|
115
|
+
const hit = keys.filter((k) => {
|
|
116
|
+
const a = k.toLowerCase();
|
|
117
|
+
return a.indexOf(lower) !== -1 || lower.indexOf(a) !== -1;
|
|
118
|
+
});
|
|
119
|
+
if (hit.length === 1) { key = hit[0]; via = 'fuzzy'; }
|
|
120
|
+
else if (hit.length > 1) return { ok: false, reason: '与多个类别相近:' + hit.join('、') + '(请写全名)', candidates: hit };
|
|
121
|
+
}
|
|
122
|
+
if (!key) return { ok: false, reason: '没有名为「' + q + '」的全局、项目或杂项任务' };
|
|
123
|
+
const lin = store.lineages[key];
|
|
124
|
+
const cur = lin && lin.current;
|
|
125
|
+
if (!cur || !cur.name) return { ok: true, key, lineage: lin, via, vacant: true };
|
|
126
|
+
return { ok: true, key, lineage: lin, via, vacant: false };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function rowsOf(store) {
|
|
130
|
+
const lines = (store && store.lineages) || {};
|
|
131
|
+
return Object.keys(lines).map((key) => {
|
|
132
|
+
const lin = lines[key] || {};
|
|
133
|
+
return {
|
|
134
|
+
key,
|
|
135
|
+
kind: String(lin.kind || ''),
|
|
136
|
+
project: String(lin.project || ''),
|
|
137
|
+
task: String(lin.task || ''),
|
|
138
|
+
workspace: String(lin.workspace || ''),
|
|
139
|
+
current: lin.current || null,
|
|
140
|
+
};
|
|
141
|
+
}).sort((a, b) => {
|
|
142
|
+
const order = { global: 0, project: 1, misc: 2 };
|
|
143
|
+
const d = (order[a.kind] === undefined ? 9 : order[a.kind]) - (order[b.kind] === undefined ? 9 : order[b.kind]);
|
|
144
|
+
return d !== 0 ? d : a.key.localeCompare(b.key, 'zh');
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 一行人类可读:名字、会话 id、类别、(提到旧编号时的来处)。 */
|
|
149
|
+
export function formatRow(row, askedAs) {
|
|
150
|
+
const cur = row && row.current;
|
|
151
|
+
if (!cur || !cur.name) return (row && row.key ? row.key : '?') + ' — 当前空缺';
|
|
152
|
+
const label = KIND_LABEL[row.kind] || row.kind || '-';
|
|
153
|
+
// 只有「拿旧编号问现任」时才加来处标注;直接问类别名或现任名就不必重复。
|
|
154
|
+
const tail = askedAs && askedAs !== cur.name && askedAs !== row.key ? '(' + askedAs + ' → ' + row.key + ')' : '';
|
|
155
|
+
return cur.name + ' ' + cur.sessionId + ' ' + label + (cur.since ? ' 自 ' + cur.since : '') + tail;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 登记一位在任者:原在任者进历史,写盘后同步 Registry.md。 */
|
|
159
|
+
export function registerHolder(ws, opts) {
|
|
160
|
+
const o = opts || {};
|
|
161
|
+
const key = lineageKeyOf(o.kind, o.project, o.task);
|
|
162
|
+
if (!key) throw new Error('类别名缺失:全局用 kind=global,项目给 project,杂项给 task');
|
|
163
|
+
const store = loadOffice(ws);
|
|
164
|
+
const lin = store.lineages[key] || (store.lineages[key] = {
|
|
165
|
+
kind: o.kind, project: String(o.project || ''), task: String(o.task || ''),
|
|
166
|
+
workspace: String(o.workspace || ''), current: null, history: [],
|
|
167
|
+
});
|
|
168
|
+
if (!Array.isArray(lin.history)) lin.history = [];
|
|
169
|
+
const previous = lin.current && lin.current.name ? { ...lin.current } : null;
|
|
170
|
+
if (previous) {
|
|
171
|
+
lin.history.push({
|
|
172
|
+
name: previous.name, sessionId: previous.sessionId,
|
|
173
|
+
from: previous.since || '', to: stampNow(), note: previous.note || '',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
lin.kind = o.kind;
|
|
177
|
+
if (o.project) lin.project = String(o.project);
|
|
178
|
+
if (o.task) lin.task = String(o.task);
|
|
179
|
+
if (o.workspace) lin.workspace = String(o.workspace);
|
|
180
|
+
lin.current = { name: String(o.name || ''), sessionId: String(o.sessionId || ''), since: stampNow(), note: String(o.note || '') };
|
|
181
|
+
if (!lin.current.name || !lin.current.sessionId) throw new Error('在任登记需要 name 与 sessionId');
|
|
182
|
+
saveOffice(ws, store);
|
|
183
|
+
let registry = { changed: [], error: '' };
|
|
184
|
+
try { registry = syncRegistry(ws, store); } catch (e) { registry = { changed: [], error: String(e && e.message ? e.message : e) }; }
|
|
185
|
+
return { key, previous, current: lin.current, registry };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 类别当前空缺(会话退役、项目暂停)。 */
|
|
189
|
+
export function retireHolder(ws, key, note) {
|
|
190
|
+
const store = loadOffice(ws);
|
|
191
|
+
const lin = store.lineages[String(key || '')];
|
|
192
|
+
if (!lin) throw new Error('没有这条类别:' + key);
|
|
193
|
+
const previous = lin.current && lin.current.name ? { ...lin.current } : null;
|
|
194
|
+
if (previous) {
|
|
195
|
+
lin.history.push({ name: previous.name, sessionId: previous.sessionId, from: previous.since || '', to: stampNow(), note: note || previous.note || '' });
|
|
196
|
+
}
|
|
197
|
+
lin.current = null;
|
|
198
|
+
saveOffice(ws, store);
|
|
199
|
+
let registry = { changed: [], error: '' };
|
|
200
|
+
try { registry = syncRegistry(ws, store); } catch (e) { registry = { changed: [], error: String(e && e.message ? e.message : e) }; }
|
|
201
|
+
return { key, previous, registry };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------- Registry.md 定点同步
|
|
205
|
+
|
|
206
|
+
function splitCells(line) {
|
|
207
|
+
// "| a | b |" → ['', ' a ', ' b ', ''];保持两端空格原样,便于回写。
|
|
208
|
+
return line.split('|');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function tailOf(cell) {
|
|
212
|
+
const m = /([^()]*)\s*$/.exec(cell) || /\([^()]*\)\s*$/.exec(cell);
|
|
213
|
+
return m ? ' ' + m[0].trim() : '';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function currentCellFor(row, withName) {
|
|
217
|
+
const cur = row.current;
|
|
218
|
+
if (!cur || !cur.name) return null;
|
|
219
|
+
return withName ? cur.name + ' `' + cur.sessionId + '`' : '`' + cur.sessionId + '`';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 把 Registry.md 里的「在役会话」格改成登记表里的现任。
|
|
224
|
+
* 只认两张表(项目清单、未登记但存在的任务),只换那一格;其它文字一律不动。
|
|
225
|
+
*/
|
|
226
|
+
export function syncRegistry(ws, store) {
|
|
227
|
+
const file = path.join(String(ws || ''), '_Project_Meta', 'Registry.md');
|
|
228
|
+
let text = '';
|
|
229
|
+
try { text = readUtf8(file); } catch { return { changed: [], error: '读不到 Registry.md' }; }
|
|
230
|
+
const bom = text.charCodeAt(0) === 0xFEFF ? '\uFEFF' : '';
|
|
231
|
+
if (bom) text = text.slice(1);
|
|
232
|
+
const lines = text.split(NL);
|
|
233
|
+
const changed = [];
|
|
234
|
+
let mode = '';
|
|
235
|
+
for (let i = 0; i < lines.length; i++) {
|
|
236
|
+
const line = lines[i];
|
|
237
|
+
if (line.startsWith('## ')) {
|
|
238
|
+
if (line.indexOf('项目清单') !== -1) mode = 'project';
|
|
239
|
+
else if (line.indexOf('未登记但存在的任务') !== -1) mode = 'misc';
|
|
240
|
+
else mode = '';
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (!mode || !line.startsWith('|')) continue;
|
|
244
|
+
const cells = splitCells(line);
|
|
245
|
+
// 表头与分隔行跳过
|
|
246
|
+
if (cells.length < 4) continue;
|
|
247
|
+
const name = String(cells[1] || '').trim();
|
|
248
|
+
if (!name || name === '项目名' || name === '任务短名' || /^-+$/.test(name)) continue;
|
|
249
|
+
const row = rowsOf(store).find((r) => r.key === name);
|
|
250
|
+
if (!row) continue;
|
|
251
|
+
if (mode === 'project') {
|
|
252
|
+
if (cells.length < 9) continue; // 8 列 + 首尾空
|
|
253
|
+
const old = String(cells[6] || '');
|
|
254
|
+
const next = currentCellFor(row, true);
|
|
255
|
+
if (!next) continue;
|
|
256
|
+
const want = ' ' + next + tailOf(old) + ' ';
|
|
257
|
+
if (old === want) continue;
|
|
258
|
+
cells[6] = want;
|
|
259
|
+
} else {
|
|
260
|
+
if (cells.length < 5) continue; // 4 列 + 首尾空
|
|
261
|
+
const next = currentCellFor(row, false);
|
|
262
|
+
if (!next) continue;
|
|
263
|
+
const want = ' ' + next + tailOf(String(cells[3] || '')) + ' ';
|
|
264
|
+
if (String(cells[3] || '') === want) continue;
|
|
265
|
+
cells[3] = want;
|
|
266
|
+
}
|
|
267
|
+
lines[i] = cells.join('|');
|
|
268
|
+
changed.push(name);
|
|
269
|
+
}
|
|
270
|
+
if (changed.length) fs.writeFileSync(file, bom + lines.join(NL), 'utf8');
|
|
271
|
+
return { changed, error: '' };
|
|
272
|
+
}
|
package/src/lib/phase.mjs
CHANGED
|
@@ -24,7 +24,7 @@ export function decide(input) {
|
|
|
24
24
|
return { action: 'unknown', reason: '水位读不到:人工判断,不要盲目压缩' };
|
|
25
25
|
}
|
|
26
26
|
if (input.ratio >= hard) {
|
|
27
|
-
return { action: 'force_compact', reason: '已到硬顶(' + Math.round(hard * 100) + '
|
|
27
|
+
return { action: 'force_compact', reason: '已到硬顶(' + Math.round(hard * 100) + '%):强制压缩,哪怕任务中途' };
|
|
28
28
|
}
|
|
29
29
|
if (input.ratio >= handover) {
|
|
30
30
|
return { action: 'checkpoint', reason: '过交接线(' + Math.round(handover * 100) + '%):写交接,随后压缩,再读交接续跑' };
|
package/src/lib/role.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* 会话角色:判定、逐角色交接路径、工具集与文案。角色表是本机状态(不进仓库、不进迁移包)。
|
|
3
3
|
* 判定顺序:角色表 → 全局标题 → 全局工作区 → 子会话(默认普通)→ cwd(Projects/<名> 或 _Misc/tasks/<任务>)→ 首条提示 → 普通。
|
|
4
4
|
*/
|
|
@@ -19,8 +19,8 @@ export const ALL_TOOL_NAMES = TOOL_NAMES.concat(PEER_NAMES);
|
|
|
19
19
|
/** 角色 → 可见工具(其余一律 deny)。改这张表就是改成本结构。 */
|
|
20
20
|
const ROLE_TOOLS = {
|
|
21
21
|
global: ALL_TOOL_NAMES.slice(),
|
|
22
|
-
project: [nameOf('phase'), nameOf('handover'), nameOf('checkpoint'), nameOf('charter'), nameOf('gate'), nameOf('snapshot'), nameOf('profile'), peerNameOf('send'), peerNameOf('status')],
|
|
23
|
-
misc: [nameOf('phase'), nameOf('handover'), nameOf('checkpoint'), nameOf('charter'), nameOf('snapshot'), nameOf('profile'), peerNameOf('send'), peerNameOf('status'), peerNameOf('new')],
|
|
22
|
+
project: [nameOf('phase'), nameOf('handover'), nameOf('checkpoint'), nameOf('charter'), nameOf('gate'), nameOf('snapshot'), nameOf('profile'), nameOf('office'), nameOf('succeed'), peerNameOf('send'), peerNameOf('status')],
|
|
23
|
+
misc: [nameOf('phase'), nameOf('handover'), nameOf('checkpoint'), nameOf('charter'), nameOf('snapshot'), nameOf('profile'), nameOf('office'), nameOf('succeed'), peerNameOf('send'), peerNameOf('status'), peerNameOf('new')],
|
|
24
24
|
plain: [],
|
|
25
25
|
};
|
|
26
26
|
|
|
@@ -179,7 +179,7 @@ export function resolveRole(opts) {
|
|
|
179
179
|
return { role, project: String(hit.project || ''), task: String(hit.task || ''), source: 'store', sessionId: sid };
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
-
// 旧角色表的 unknown
|
|
182
|
+
// 旧角色表的 unknown 是备用状态,按新口径并入普通。
|
|
183
183
|
if (role === 'unknown') {
|
|
184
184
|
return { role: 'plain', project: String(hit.project || ''), task: String(hit.task || ''), source: 'store', sessionId: sid };
|
|
185
185
|
}
|