@mingmaple/dsh-handover 0.9.22 → 1.0.0
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 +117 -113
- package/docs/migrate.md +52 -41
- package/docs/releases/0.9.12.md +2 -3
- package/docs/releases/0.9.14.md +1 -5
- package/docs/releases/0.9.22.md +1 -1
- package/docs/releases/1.0.0.md +33 -0
- package/docs/troubleshoot.md +221 -61
- package/package.json +5 -2
- package/scripts/behavior.mjs +398 -0
- package/scripts/blank-dsh.mjs +12 -2
- package/scripts/blank-smoke.mjs +95 -44
- package/scripts/check-docs.mjs +7 -0
- package/scripts/portability.mjs +71 -0
- package/scripts/style-gate.mjs +571 -0
- package/src/client.js +1028 -994
- package/src/index.mjs +273 -109
- package/src/lib/absrefs.mjs +1 -1
- package/src/lib/api.mjs +9 -3
- package/src/lib/archive.mjs +1 -1
- package/src/lib/brief.mjs +8 -6
- package/src/lib/config.mjs +21 -6
- package/src/lib/docmap.mjs +5 -8
- package/src/lib/drift.mjs +1 -1
- package/src/lib/events.mjs +1 -1
- package/src/lib/facts.mjs +19 -12
- package/src/lib/gates.mjs +12 -12
- package/src/lib/global-task.mjs +69 -0
- package/src/lib/guide.mjs +80 -75
- package/src/lib/handover-io.mjs +69 -6
- package/src/lib/identity.mjs +330 -0
- package/src/lib/legacy.mjs +89 -0
- package/src/lib/lint.mjs +14 -11
- package/src/lib/manual.mjs +117 -80
- package/src/lib/mover.mjs +18 -14
- package/src/lib/office.mjs +42 -2
- package/src/lib/pack-ops.mjs +10 -10
- package/src/lib/pathkey.mjs +24 -0
- package/src/lib/phase.mjs +8 -13
- package/src/lib/profile.mjs +1 -1
- package/src/lib/reminder.mjs +110 -0
- package/src/lib/role.mjs +1 -1
- package/src/lib/scaffold.mjs +30 -10
- package/src/lib/schedule.mjs +5 -5
- package/src/lib/session-create.mjs +2 -2
- package/src/lib/session-ops.mjs +76 -47
- package/src/lib/sessions.mjs +40 -2
- package/src/lib/sevenzip.mjs +10 -1
- package/src/lib/status.mjs +337 -336
- package/src/lib/succession.mjs +14 -14
- package/src/lib/system.mjs +364 -0
- package/src/templates/Changelog.skeleton.md +3 -3
- package/src/templates/Charter.skeleton.md +13 -9
- package/src/templates/Handover.global.skeleton.md +20 -13
- package/src/templates/Handover.template.md +89 -0
- package/src/templates/Plan.skeleton.md +34 -0
- package/src/templates/Prompt_Templates.md +91 -10
- package/src/templates/Runtime_Manual.md +254 -139
- package/src/templates/System.skeleton.md +31 -0
- package/src/templates/Systems.global.skeleton.md +13 -0
- package/src/templates/rules/Delegation_Rules.md +45 -0
- package/src/templates/rules/Handover_Rules.md +46 -24
- package/src/templates/rules/Migration_Rules.md +7 -7
- package/src/templates/rules/Misc_Task_Standard.md +17 -10
- package/src/templates/rules/Writing_Rules.md +33 -16
- package/src/templates/standards/README.md +19 -7
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203/README.md +120 -0
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203//344/276/235/346/215/256_/350/256/272/346/226/207.md +389 -0
- package/src/tools/ho_backup.mjs +8 -8
- package/src/tools/ho_changelog.mjs +4 -4
- package/src/tools/ho_charter.mjs +7 -6
- package/src/tools/ho_checkpoint.mjs +34 -31
- package/src/tools/ho_docmap.mjs +135 -49
- package/src/tools/ho_gate.mjs +9 -4
- package/src/tools/ho_handover.mjs +61 -35
- package/src/tools/ho_office.mjs +45 -4
- package/src/tools/ho_profile.mjs +3 -3
- package/src/tools/ho_sessions.mjs +43 -22
- package/src/tools/ho_status.mjs +14 -9
- package/src/tools/ho_succeed.mjs +3 -3
- package/src/tools/ho_workspace.mjs +83 -51
- package/src/tools/peer.mjs +219 -8
package/src/lib/pack-ops.mjs
CHANGED
|
@@ -26,7 +26,7 @@ function chooseFormat(args, cfg) {
|
|
|
26
26
|
const want = String((args && args.format) || (cfg && cfg.packFormat) || 'auto').toLowerCase();
|
|
27
27
|
if (want === '7z') {
|
|
28
28
|
const bin = sevenZipBin(cfg && cfg.sevenZipPath);
|
|
29
|
-
if (!bin) throw new Error('
|
|
29
|
+
if (!bin) throw new Error('指定 format=7z,但本机无 7-Zip。可省略 format 改用 auto,或写 format="tar" 使用内置归档器。');
|
|
30
30
|
return { fmt: '7z', bin };
|
|
31
31
|
}
|
|
32
32
|
if (want === 'tar' || want === 'targz' || want === 'tar.gz') return { fmt: 'tar', bin: '' };
|
|
@@ -48,11 +48,11 @@ function unpack(packPath, dir) {
|
|
|
48
48
|
}
|
|
49
49
|
if (fmt === '7z') {
|
|
50
50
|
const bin = sevenZipBin('');
|
|
51
|
-
if (!bin) return { code: -1, out: '
|
|
51
|
+
if (!bin) return { code: -1, out: '该包为 7z 格式,但本机无 7-Zip;请安装 7-Zip,或改用 tar.gz 包。' };
|
|
52
52
|
fs.mkdirSync(dir, { recursive: true });
|
|
53
53
|
return run7zSafe(bin, packPath, dir);
|
|
54
54
|
}
|
|
55
|
-
return { code: -1, out: '
|
|
55
|
+
return { code: -1, out: '无法识别包格式(既非 7z 魔数,也非 gzip):' + packPath };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** 包内路径必须相对且不越界:拒绝绝对路径、盘符、..(防止恢复时写到目标目录之外)。 */
|
|
@@ -121,7 +121,7 @@ export function packCheck(env, args, exec) {
|
|
|
121
121
|
const live = scan.rows.filter((r) => r.project === item);
|
|
122
122
|
if (live.length) {
|
|
123
123
|
const best = live.slice().sort((a, b) => b.mtime - a.mtime)[0];
|
|
124
|
-
if (best.state === 'running') why.push('会话 ' + best.sid + '
|
|
124
|
+
if (best.state === 'running') why.push('会话 ' + best.sid + ' 正在运行');
|
|
125
125
|
if (now - best.mtime < minAge) why.push('最后活动距今 ' + Math.round((now - best.mtime) / 60000) + ' 分钟 < ' + Math.round(minAge / 60000));
|
|
126
126
|
}
|
|
127
127
|
const hand = path.join(ws, '_Project_Meta', item, 'Handover.md');
|
|
@@ -170,7 +170,7 @@ export function packNow(env, args, exec) {
|
|
|
170
170
|
if (!entries.length) throw new Error('没有可打包的文件:' + roots.map((r) => r[0]).join('、'));
|
|
171
171
|
const total = entries.reduce((a, b) => a + b.bytes, 0);
|
|
172
172
|
const out = [];
|
|
173
|
-
out.push('目标 ' + kind + (name ? ':' + name : '') + '
|
|
173
|
+
out.push('目标 ' + kind + (name ? ':' + name : '') + ',' + entries.length + ' 个文件 / ' + (total / 1048576).toFixed(2) + ' MB');
|
|
174
174
|
if (args.dry_run) {
|
|
175
175
|
for (const e of entries.slice(0, 20)) out.push(' ' + e.rel + ' ' + e.bytes + ' B');
|
|
176
176
|
const pre = scanAbsoluteRefs(ws, entries.map((e) => e.rel));
|
|
@@ -205,7 +205,7 @@ export function packNow(env, args, exec) {
|
|
|
205
205
|
fs.writeFileSync(path.join(stagingRoot, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
|
|
206
206
|
fs.writeFileSync(path.join(stagingRoot, 'CHECKSUMS.sha256'), files.map((f) => f.sha256 + ' ' + f.path).join(NL) + NL, 'utf8');
|
|
207
207
|
const absLines = renderAbsoluteRefs(abs, { title: '绝对路径体检(包内)', max: 200 });
|
|
208
|
-
const md = ['# 迁移包(最简版)', '', '- 目标: ' + kind + (name ? ':' + name : ''), '- 生成: ' + manifest.createdAt, '- 文件: ' + files.length + ' 个 / ' + total + ' B', '- 说明: ' + (args.note || '(无)'), '', ...absLines, '', '## 跨平台注意', '', '-
|
|
208
|
+
const md = ['# 迁移包(最简版)', '', '- 目标: ' + kind + (name ? ':' + name : ''), '- 生成: ' + manifest.createdAt, '- 文件: ' + files.length + ' 个 / ' + total + ' B', '- 说明: ' + (args.note || '(无)'), '', ...absLines, '', '## 跨平台注意', '', '- **库内引用一律相对路径**:本包内有 ' + abs.totalHits + ' 处写死的绝对路径,插件只报告、不改写文件内容,迁移后须由人工或项目会话修改。', '- 源平台 ' + manifest.sourcePlatform + ';换行 CRLF ' + abs.crlf + ' 个 / LF ' + abs.lf + ' 个' + (abs.nonUtf8.length ? ';非 UTF-8 ' + abs.nonUtf8.length + ' 个' : '') + '。', '- 目标机若为其他系统(Windows ↔ macOS/Linux):盘符、路径分隔符、大小写敏感性、换行与编码均须项目会话自行核对。', '', '## 目标环境前置(先完成这两项,再执行恢复)', '', '1. 安装 dsh-handover 插件(否则本工具不存在);', '2. 打开本包随附的交接文件(_Project_Meta/<项目名>/Handover.md 或全局 _Control_Center/records/Global_Handover.md),按其中内容恢复状态。', '', '## 恢复', '', 'ho_backup(action="restore", pack="<本包绝对路径>", dry_run=true) # 先预演,再加 apply=true', '', '> 会话本身不迁移:连续性只依托交接文档。'].join(NL);
|
|
209
209
|
fs.writeFileSync(path.join(stagingRoot, 'MIGRATION.md'), md, 'utf8');
|
|
210
210
|
const outDir = args.out || path.join(ws, '_Archives', '_Global', 'Backups');
|
|
211
211
|
fs.mkdirSync(outDir, { recursive: true });
|
|
@@ -263,7 +263,7 @@ export function packVerify(env, args, exec) {
|
|
|
263
263
|
out.push('文件 ' + manifest.files.length + ' 个;一致 ' + same.length + ' / 漂移 ' + drift.length + ' / 缺失 ' + missing.length);
|
|
264
264
|
for (const p of drift.slice(0, 10)) out.push(' 漂移:' + p);
|
|
265
265
|
for (const p of missing.slice(0, 10)) out.push(' 缺失:' + p);
|
|
266
|
-
out.push(drift.length === 0 && missing.length === 0 ? '
|
|
266
|
+
out.push(drift.length === 0 && missing.length === 0 ? '判定:包内即现版,可直接使用' : '判定:包已作废(磁盘内容已变更),须重新打包');
|
|
267
267
|
return out.join(NL);
|
|
268
268
|
}
|
|
269
269
|
|
|
@@ -323,10 +323,10 @@ export function packRestore(env, args, exec) {
|
|
|
323
323
|
out.push('包 ' + packPath + ',目标 ' + target);
|
|
324
324
|
out.push('文件 ' + manifest.files.length + ' 个;新增 ' + adds.length + ' / 覆盖 ' + overwrites.length);
|
|
325
325
|
for (const p of overwrites.slice(0, 10)) out.push(' 覆盖:' + p);
|
|
326
|
-
if (crossPlatform) out.push('注意:源平台 ' + manifest.sourcePlatform + ' ≠ 本机 ' + process.platform + '
|
|
326
|
+
if (crossPlatform) out.push('注意:源平台 ' + manifest.sourcePlatform + ' ≠ 本机 ' + process.platform + ';路径分隔符、盘符、大小写、换行与编码均须项目会话自行核对。');
|
|
327
327
|
out.push(...renderAbsoluteRefs(abs, { title: '绝对路径体检(恢复前)', max: 20 }));
|
|
328
|
-
if (abs.totalHits) out.push('
|
|
329
|
-
if (!apply) out.push('
|
|
328
|
+
if (abs.totalHits) out.push(' 上述位置插件不作改动;迁移后由人工或项目会话按新机器修改,库内引用统一使用相对路径。');
|
|
329
|
+
if (!apply) out.push('预演:未写入任何文件。确认无误后以 apply=true 再次调用。');
|
|
330
330
|
else out.push('已写入;被覆盖的旧文件备份在 ' + backupDir);
|
|
331
331
|
return out.join(NL);
|
|
332
332
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路径等价键:跨平台比较同一路径时用。
|
|
3
|
+
* Windows 与 macOS 的默认文件系统不区分大小写,Linux 区分;因此只在 win32 与 darwin 上折叠大小写。
|
|
4
|
+
* 反斜杠一律折成正斜杠,去掉末尾分隔符,便于比较用户输入与登记表里的路径。
|
|
5
|
+
* `platform` 可显式传入,便于按平台写单元测试。
|
|
6
|
+
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
/** 返回用于比较的路径键。 */
|
|
10
|
+
export function pathKey(p, platform) {
|
|
11
|
+
const plat = platform || process.platform;
|
|
12
|
+
const norm = String(p || '').replace(/\\/g, '/').replace(/\/+$/, '');
|
|
13
|
+
return (plat === 'win32' || plat === 'darwin') ? norm.toLowerCase() : norm;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 两个路径是否指向同一位置(按当前或指定平台判定)。 */
|
|
17
|
+
export function samePath(a, b, platform) {
|
|
18
|
+
return pathKey(a, platform) === pathKey(b, platform);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 把工作区相对路径('/' 分隔)转成本机路径。 */
|
|
22
|
+
export function relToAbs(root, rel) {
|
|
23
|
+
return path.join(root, String(rel).split('/').join(path.sep));
|
|
24
|
+
}
|
package/src/lib/phase.mjs
CHANGED
|
@@ -3,34 +3,29 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @param {{ratio:number|null, compactionsUsed:number, cfg:object, lockState:string}} input
|
|
5
5
|
* @returns {{action:string, reason:string}}
|
|
6
|
-
* action ∈ continue |
|
|
6
|
+
* action ∈ continue | checkpoint | force_compact | new_session | blocked | unknown
|
|
7
7
|
*/
|
|
8
8
|
export function decide(input) {
|
|
9
9
|
const cfg = input.cfg || {};
|
|
10
|
-
const warn = cfg.warnRatio === undefined ? 0.32 : cfg.warnRatio;
|
|
11
10
|
const handover = cfg.handoverRatio === undefined ? 0.35 : cfg.handoverRatio;
|
|
12
11
|
const hard = cfg.hardRatio === undefined ? 0.85 : cfg.hardRatio;
|
|
13
12
|
const maxComp = cfg.maxCompactions === undefined ? 3 : cfg.maxCompactions;
|
|
14
13
|
const used = typeof input.compactionsUsed === 'number' ? input.compactionsUsed : 0;
|
|
15
14
|
if (input.lockState === 'orphaned') {
|
|
16
|
-
return { action: 'blocked', reason: '检测到未闭合的压缩锁(compaction/start 无配对 end
|
|
15
|
+
return { action: 'blocked', reason: '检测到未闭合的压缩锁(compaction/start 无配对 end):先处理压缩锁,再排新压缩' };
|
|
17
16
|
}
|
|
18
17
|
// 额度用尽优先于水位判定:3 次压满后再压不是「水位没到」,而是该换任了。
|
|
19
|
-
// 放在水位判定之前,force 也就绕不过去(force 只在水位分支里放行)。
|
|
20
18
|
if (used >= maxComp) {
|
|
21
|
-
return { action: 'new_session', reason: '已压缩 ' + used + '/' + maxComp + '
|
|
19
|
+
return { action: 'new_session', reason: '已压缩 ' + used + '/' + maxComp + ' 次:压缩额度用尽,应写交接并新建会话' };
|
|
22
20
|
}
|
|
23
21
|
if (input.ratio === null || input.ratio === undefined) {
|
|
24
|
-
return { action: 'unknown', reason: '
|
|
22
|
+
return { action: 'unknown', reason: '水位读不到:需人工判断,不得盲目压缩' };
|
|
25
23
|
}
|
|
26
24
|
if (input.ratio >= hard) {
|
|
27
|
-
return { action: 'force_compact', reason: '已到硬顶(' + Math.round(hard * 100) + '
|
|
25
|
+
return { action: 'force_compact', reason: '已到硬顶(' + Math.round(hard * 100) + '%):应强制压缩,任务中途亦同' };
|
|
28
26
|
}
|
|
29
27
|
if (input.ratio >= handover) {
|
|
30
|
-
return { action: 'checkpoint', reason: '
|
|
28
|
+
return { action: 'checkpoint', reason: '已过可压缩线(' + Math.round(handover * 100) + '%):先刷新交接,再登记压缩' };
|
|
31
29
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
return { action: 'continue', reason: '水位正常' };
|
|
36
|
-
}
|
|
30
|
+
return { action: 'continue', reason: '水位可继续;提前压缩仅在助手建议,或当前用户主动要求并授权时进行' };
|
|
31
|
+
}
|
package/src/lib/profile.mjs
CHANGED
|
@@ -88,7 +88,7 @@ function defaultProfile(info) {
|
|
|
88
88
|
|
|
89
89
|
function cleanList(v, field, errors) {
|
|
90
90
|
if (v === undefined) return [];
|
|
91
|
-
if (!Array.isArray(v)) { errors.push(field + '
|
|
91
|
+
if (!Array.isArray(v)) { errors.push(field + ' 须为数组'); return []; }
|
|
92
92
|
const out = [];
|
|
93
93
|
for (const x of v) {
|
|
94
94
|
const s = String(x == null ? '' : x).trim().replace(/\\/g, '/');
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一次性后台提醒(1.0):35% 与 50% 各投一次。
|
|
3
|
+
*
|
|
4
|
+
* 投递方式(用户定):尾注入(Tail-end Injection)。插件工具返回结果时,把提醒贴在结果的末尾,
|
|
5
|
+
* 模型在下一步立刻读到;不改系统提示词(避免改写前缀缓存)、不拆回合、不另起消息、不打断工具。
|
|
6
|
+
* 不往会话日志写任何事件:宿主对 developer/message 有「必须匹配已打开的轮与步」的强校验,
|
|
7
|
+
* 写事件会把会话写成 SessionFormatError(已实测发生过)。
|
|
8
|
+
*
|
|
9
|
+
* 两处风险:
|
|
10
|
+
* 1) 压缩刚完成时投影缓存还是压缩前的旧水位,跳过恢复后的第一次判定。
|
|
11
|
+
* 2) 模型整轮不调用插件工具时没有注入点;此时它可自行用 ho_status 读水位。
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { readSessionPressurePct } from './sessions.mjs';
|
|
17
|
+
|
|
18
|
+
export function remindersPath() {
|
|
19
|
+
return path.join(os.homedir(), '.dsh', 'storages', 'dsh-handover.reminders.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function readReminderState() {
|
|
23
|
+
try {
|
|
24
|
+
const v = JSON.parse(fs.readFileSync(remindersPath(), 'utf8').replace(/^\uFEFF/, ''));
|
|
25
|
+
const sessions = v && typeof v.sessions === 'object' && v.sessions ? v.sessions : {};
|
|
26
|
+
return { version: 1, sessions };
|
|
27
|
+
} catch {
|
|
28
|
+
return { version: 1, sessions: {} };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeReminderState(state) {
|
|
33
|
+
const file = remindersPath();
|
|
34
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
35
|
+
const tmp = file + '.tmp-' + process.pid;
|
|
36
|
+
fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + String.fromCharCode(10), 'utf8');
|
|
37
|
+
fs.renameSync(tmp, file);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 35% 提醒文本。 */
|
|
41
|
+
export function remindText35() {
|
|
42
|
+
return '当前上下文约占窗口 35%。先把正在做的任务收束到可暂停的位置。'
|
|
43
|
+
+ '随后判断剩余流程总量:较少则继续完成;较多则做完当前小步骤后停止,'
|
|
44
|
+
+ '向当前用户确认是否继续,或写出剩余流程的行动计划、刷新交接,再压缩或换任。'
|
|
45
|
+
+ '判定标准:完成全部剩余流程后,上下文占用不超过 45%。本条为一次性后台提醒。';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 50% 提醒文本。 */
|
|
49
|
+
export function remindText50() {
|
|
50
|
+
return '当前上下文约占窗口 50%。先把正在做的任务收束到可暂停的位置。'
|
|
51
|
+
+ '随后判断剩余流程总量:较少则继续完成;较多则做完当前小步骤后停止,'
|
|
52
|
+
+ '向当前用户确认是否继续,或写出剩余流程的行动计划、刷新交接,再压缩或换任。'
|
|
53
|
+
+ '判定标准:完成全部剩余流程后,上下文占用不超过 55%。本条为一次性后台提醒。';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 判定一次:水位到线且该线未提醒过时写状态并返回提醒正文。
|
|
58
|
+
* 只应由「工具返回结果」这条路径调用:那时模型正好在等下一次装配。
|
|
59
|
+
* @param {{ remindRatio?: number, remind2Ratio?: number }} cfg
|
|
60
|
+
* @param {string} sid 会话 id
|
|
61
|
+
* @returns {string} 提醒正文;不需要投时为空串
|
|
62
|
+
*/
|
|
63
|
+
export function checkReminder(cfg, sid) {
|
|
64
|
+
const id = String(sid || '');
|
|
65
|
+
if (!id) return '';
|
|
66
|
+
const state = readReminderState();
|
|
67
|
+
const rec = state.sessions[id] || {};
|
|
68
|
+
// 压缩刚完成后的那次判定:投影缓存可能还是压缩前的旧水位,跳过。
|
|
69
|
+
if (rec.skipNext) {
|
|
70
|
+
rec.skipNext = false;
|
|
71
|
+
state.sessions[id] = rec;
|
|
72
|
+
try { writeReminderState(state); } catch { /* 写盘失败不阻断工具 */ }
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
const pct = readSessionPressurePct(id);
|
|
76
|
+
if (pct === null || pct === undefined) return '';
|
|
77
|
+
const r1 = (cfg && cfg.remindRatio !== undefined ? cfg.remindRatio : 0.35) * 100;
|
|
78
|
+
const r2 = (cfg && cfg.remind2Ratio !== undefined ? cfg.remind2Ratio : 0.5) * 100;
|
|
79
|
+
const parts = [];
|
|
80
|
+
let changed = false;
|
|
81
|
+
if (pct >= r1 && !rec.r35) { rec.r35 = true; changed = true; parts.push(remindText35()); }
|
|
82
|
+
if (pct >= r2 && !rec.r50) { rec.r50 = true; changed = true; parts.push(remindText50()); }
|
|
83
|
+
if (!changed) return '';
|
|
84
|
+
state.sessions[id] = rec;
|
|
85
|
+
try { writeReminderState(state); } catch { /* 写盘失败不阻断工具 */ }
|
|
86
|
+
return parts.join(String.fromCharCode(10, 10));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 工具结果末尾的提醒尾巴:命中阈值时返回要 append 的文本,否则空串。
|
|
91
|
+
* 形如 `[System Note: …]`,紧贴工具结果,模型下一步立刻读到。
|
|
92
|
+
* @param {{ remindRatio?: number, remind2Ratio?: number }} cfg
|
|
93
|
+
* @param {string} sid 会话 id
|
|
94
|
+
*/
|
|
95
|
+
export function reminderTailNote(cfg, sid) {
|
|
96
|
+
const text = checkReminder(cfg, sid);
|
|
97
|
+
if (!text) return '';
|
|
98
|
+
return '[System Note: ' + text + ']';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 记一次「压缩后跳过下一轮判定」;由 ho_checkpoint 在登记压缩时写入。 */
|
|
102
|
+
export function markSkipNext(sid) {
|
|
103
|
+
const id = String(sid || '');
|
|
104
|
+
if (!id) return;
|
|
105
|
+
const state = readReminderState();
|
|
106
|
+
const rec = state.sessions[id] || {};
|
|
107
|
+
rec.skipNext = true;
|
|
108
|
+
state.sessions[id] = rec;
|
|
109
|
+
try { writeReminderState(state); } catch { /* 写盘失败只影响一次判定 */ }
|
|
110
|
+
}
|
package/src/lib/role.mjs
CHANGED
|
@@ -64,7 +64,7 @@ function writeRolesFile(cfg, data) {
|
|
|
64
64
|
/** 写入/更新一条角色;返回写入后的条目。 */
|
|
65
65
|
export function writeRole(cfg, sessionId, patch) {
|
|
66
66
|
const sid = String(sessionId || '');
|
|
67
|
-
if (!sid) throw new Error('
|
|
67
|
+
if (!sid) throw new Error('缺少会话 id');
|
|
68
68
|
const data = readRoles(cfg);
|
|
69
69
|
const prev = data.sessions[sid] || {};
|
|
70
70
|
const next = Object.assign({}, prev, patch || {});
|
package/src/lib/scaffold.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
+
import { pathKey } from './pathkey.mjs';
|
|
9
10
|
|
|
10
11
|
/** 插件自带的模板目录(dist/templates 或 src/templates)。 */
|
|
11
12
|
const TPL = new URL('../templates/', import.meta.url);
|
|
@@ -23,6 +24,7 @@ const DIRS = [
|
|
|
23
24
|
'_Control_Center/standards',
|
|
24
25
|
'_Control_Center/templates',
|
|
25
26
|
'_Control_Center/tools',
|
|
27
|
+
'_Control_Center/tasks',
|
|
26
28
|
'_Control_Center/user',
|
|
27
29
|
'Projects',
|
|
28
30
|
'_Project_Meta',
|
|
@@ -34,33 +36,41 @@ const DIRS = [
|
|
|
34
36
|
const FILES = [
|
|
35
37
|
['_Control_Center/records/Global_Handover.md', 'Handover.global.skeleton.md'],
|
|
36
38
|
['_Control_Center/records/Global_Changelog.md', 'Changelog.skeleton.md'],
|
|
39
|
+
// 全局总注册:只列各任务体系文件的路径;由 ho_docmap 或 scaffold 维护。
|
|
40
|
+
['_Control_Center/records/systems.md', 'Systems.global.skeleton.md'],
|
|
37
41
|
['_Control_Center/rules/Workspace_Charter.md', 'Charter.skeleton.md'],
|
|
38
42
|
['_Control_Center/templates/Prompt_Templates.md', 'Prompt_Templates.md'],
|
|
39
|
-
|
|
43
|
+
['_Control_Center/templates/Handover.md', 'Handover.template.md'],
|
|
44
|
+
['_Control_Center/templates/Plan.skeleton.md', 'Plan.skeleton.md'],
|
|
45
|
+
['_Control_Center/templates/System.skeleton.md', 'System.skeleton.md'],
|
|
46
|
+
// 通用规范:一条规范一个子目录,各角色会话都可读,只有全局写。
|
|
40
47
|
['_Control_Center/standards/README.md', 'standards/README.md'],
|
|
48
|
+
['_Control_Center/standards/写作规范/README.md', 'standards/写作规范/README.md'],
|
|
49
|
+
['_Control_Center/standards/写作规范/依据_论文.md', 'standards/写作规范/依据_论文.md'],
|
|
41
50
|
// 规则类:工作区没有这份时,ho_charter 会退回同名内置版本;这里先铺一份可改的默认口径。
|
|
42
51
|
['_Control_Center/rules/Handover_Rules.md', 'rules/Handover_Rules.md'],
|
|
43
52
|
['_Control_Center/rules/Migration_Rules.md', 'rules/Migration_Rules.md'],
|
|
44
53
|
['_Control_Center/rules/Misc_Task_Standard.md', 'rules/Misc_Task_Standard.md'],
|
|
54
|
+
['_Control_Center/rules/Delegation_Rules.md', 'rules/Delegation_Rules.md'],
|
|
45
55
|
['_Control_Center/user/Writing_Rules.md', 'rules/Writing_Rules.md'],
|
|
46
56
|
];
|
|
47
57
|
|
|
48
58
|
/** 规整用户粘贴的路径:去掉引号/尾分隔符、展开 ~、解析成绝对路径。 */
|
|
49
59
|
function normalizeParent(input) {
|
|
50
60
|
let s = String(input || '').trim();
|
|
51
|
-
if (!s) return { error: '
|
|
61
|
+
if (!s) return { error: '请先填写目录(父目录,全局根建于其中)' };
|
|
52
62
|
s = s.replace(/^["']+|["']+$/g, '').trim();
|
|
53
|
-
if (!s) return { error: '
|
|
63
|
+
if (!s) return { error: '请先填写目录' };
|
|
54
64
|
if (s === '~') s = os.homedir();
|
|
55
65
|
else if (s.startsWith('~/') || s.startsWith('~\\')) s = path.join(os.homedir(), s.slice(2));
|
|
56
66
|
const abs = path.resolve(s);
|
|
57
67
|
let st = null;
|
|
58
68
|
try { st = fs.statSync(abs); } catch { st = null; }
|
|
59
|
-
if (st && !st.isDirectory()) return { error: '
|
|
69
|
+
if (st && !st.isDirectory()) return { error: '该路径不是目录:' + abs, path: abs };
|
|
60
70
|
// 目标父目录不存在就建:允许直接指定尚不存在的路径。
|
|
61
71
|
if (!st) {
|
|
62
72
|
try { fs.mkdirSync(abs, { recursive: true }); }
|
|
63
|
-
catch (e) { return { error: '
|
|
73
|
+
catch (e) { return { error: '无法创建该目录:' + abs + '(' + String(e && e.message ? e.message : e) + ')', path: abs }; }
|
|
64
74
|
}
|
|
65
75
|
return { parent: abs, createdParent: !st };
|
|
66
76
|
}
|
|
@@ -78,6 +88,7 @@ export function ensureScaffold(root) {
|
|
|
78
88
|
const created = [];
|
|
79
89
|
const written = [];
|
|
80
90
|
const skipped = [];
|
|
91
|
+
const missingTemplates = [];
|
|
81
92
|
for (const d of DIRS) {
|
|
82
93
|
const p = path.join(abs, d.split('/').join(path.sep));
|
|
83
94
|
if (!fs.existsSync(p)) created.push(d);
|
|
@@ -90,14 +101,23 @@ export function ensureScaffold(root) {
|
|
|
90
101
|
for (const [rel, tpl] of FILES) {
|
|
91
102
|
const dst = path.join(abs, rel.split('/').join(path.sep));
|
|
92
103
|
if (fs.existsSync(dst)) { skipped.push(rel); continue; }
|
|
104
|
+
let body = '';
|
|
93
105
|
try {
|
|
94
|
-
|
|
106
|
+
body = template(tpl);
|
|
107
|
+
} catch {
|
|
108
|
+
// 内置模板缺失时不写空文件,登记后由调用方报出,避免静默留下半套体系。
|
|
109
|
+
missingTemplates.push(rel);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
114
|
+
fs.writeFileSync(dst, body, 'utf8');
|
|
95
115
|
written.push(rel);
|
|
96
116
|
} catch (e) {
|
|
97
|
-
return { ok: false, error: '写文件失败(' + rel + '):' + (e && e.message ? e.message : e), created, written, skipped };
|
|
117
|
+
return { ok: false, error: '写文件失败(' + rel + '):' + (e && e.message ? e.message : e), created, written, skipped, missingTemplates };
|
|
98
118
|
}
|
|
99
119
|
}
|
|
100
|
-
return { ok: true, created, written, skipped };
|
|
120
|
+
return { ok: true, created, written, skipped, missingTemplates };
|
|
101
121
|
}
|
|
102
122
|
|
|
103
123
|
/**
|
|
@@ -112,9 +132,9 @@ export function createGlobalWorkspace(parentInput, opts) {
|
|
|
112
132
|
|
|
113
133
|
const name = (opts && typeof opts.name === 'string' && opts.name.trim()) || DEFAULT_ROOT_NAME;
|
|
114
134
|
if (name.indexOf('/') !== -1 || name.indexOf('\\') !== -1 || name === '.' || name === '..') {
|
|
115
|
-
return { ok: false, error: '
|
|
135
|
+
return { ok: false, error: '根目录名不得含路径分隔符:' + name };
|
|
116
136
|
}
|
|
117
|
-
const sameName = path.basename(t.parent)
|
|
137
|
+
const sameName = pathKey(path.basename(t.parent)) === pathKey(name);
|
|
118
138
|
const root = sameName ? t.parent : path.join(t.parent, name);
|
|
119
139
|
|
|
120
140
|
let existed = false;
|
package/src/lib/schedule.mjs
CHANGED
|
@@ -71,9 +71,9 @@ export function createScheduler(deps) {
|
|
|
71
71
|
|
|
72
72
|
async function compactOnce(sessionId) {
|
|
73
73
|
const agent = agentOf(sessionId);
|
|
74
|
-
if (!agent) throw new Error("
|
|
74
|
+
if (!agent) throw new Error("会话不在内存中(可能已休眠)");
|
|
75
75
|
const compaction = compactionOf(agent);
|
|
76
|
-
if (!compaction || typeof compaction.compactNow !== "function") throw new Error("compaction.compactNow 不可用(profile
|
|
76
|
+
if (!compaction || typeof compaction.compactNow !== "function") throw new Error("compaction.compactNow 不可用(profile 级与会话作用域均未取到)");
|
|
77
77
|
const ac = new AbortController();
|
|
78
78
|
const timer = later(() => ac.abort(new Error("timeout")), cfg.compactTimeoutMs || 120000);
|
|
79
79
|
try {
|
|
@@ -128,7 +128,7 @@ export function createScheduler(deps) {
|
|
|
128
128
|
if (!agent) { fail(sessionId, "会话不在内存,无法压缩"); return; }
|
|
129
129
|
if (agent.status === "running") {
|
|
130
130
|
const idle = await waitIdle(agent, cfg.waitIdleMs || 180000);
|
|
131
|
-
if (!idle) { fail(sessionId, "
|
|
131
|
+
if (!idle) { fail(sessionId, "等待 " + Math.round((cfg.waitIdleMs || 180000) / 1000) + " 秒后,会话仍未空闲"); return; }
|
|
132
132
|
}
|
|
133
133
|
try {
|
|
134
134
|
const result = await compactOnce(sessionId);
|
|
@@ -157,7 +157,7 @@ export function createScheduler(deps) {
|
|
|
157
157
|
} catch (e) {
|
|
158
158
|
const code = e && e.code;
|
|
159
159
|
if (code === "busy") {
|
|
160
|
-
if (Date.now() - job.startedAt > (cfg.waitIdleMs || 180000)) { fail(sessionId, "
|
|
160
|
+
if (Date.now() - job.startedAt > (cfg.waitIdleMs || 180000)) { fail(sessionId, "压缩持续处于 busy 状态(存在其他压缩或排队工作)"); return; }
|
|
161
161
|
retryLater(sessionId, 800);
|
|
162
162
|
return;
|
|
163
163
|
}
|
|
@@ -181,7 +181,7 @@ export function createScheduler(deps) {
|
|
|
181
181
|
pending.delete(sessionId);
|
|
182
182
|
note("checkpoint failed " + sessionId + ": " + msg);
|
|
183
183
|
try {
|
|
184
|
-
promptSelf(sessionId, "【dsh-handover】压缩未执行:" + msg + "
|
|
184
|
+
promptSelf(sessionId, "【dsh-handover】压缩未执行:" + msg + "。历史未被压缩,交接已就绪。请将本条消息原样告知当前用户,不得自行重试压缩。").catch(() => {});
|
|
185
185
|
} catch { /* ignore */ }
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -16,7 +16,7 @@ export async function newSessionIn(env, opts) {
|
|
|
16
16
|
}
|
|
17
17
|
const workspaceId = String(o.workspaceId || '');
|
|
18
18
|
const cwd = String(o.cwd || '');
|
|
19
|
-
if (!workspaceId && !cwd) return { error: '
|
|
19
|
+
if (!workspaceId && !cwd) return { error: '建会话须给出 workspaceId 或 cwd' };
|
|
20
20
|
let created = null;
|
|
21
21
|
try {
|
|
22
22
|
created = workspaceId ? await controller.create({ workspaceId }) : await controller.create({ cwd });
|
|
@@ -24,7 +24,7 @@ export async function newSessionIn(env, opts) {
|
|
|
24
24
|
return { error: '建会话失败:' + String(e && e.message ? e.message : e) };
|
|
25
25
|
}
|
|
26
26
|
const sessionId = String((created && created.sessionId) || '');
|
|
27
|
-
if (!sessionId) return { error: 'create
|
|
27
|
+
if (!sessionId) return { error: 'create 未返回 sessionId' };
|
|
28
28
|
const title = String(o.title || '');
|
|
29
29
|
if (title) {
|
|
30
30
|
try { if (typeof controller.rename === 'function') await controller.rename({ sessionId, title }); } catch { /* 改名失败可事后用 peer(action="rename") */ }
|