@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
|
@@ -2,26 +2,27 @@
|
|
|
2
2
|
import { textOutput } from '../lib/output.mjs';
|
|
3
3
|
import { nameOf } from '../lib/names.mjs';
|
|
4
4
|
import { NL } from '../lib/text.mjs';
|
|
5
|
-
import { readHandover,
|
|
5
|
+
import { readHandover, rewriteHandover } from '../lib/handover-io.mjs';
|
|
6
6
|
import { eventsOf, countCompactions } from '../lib/events.mjs';
|
|
7
7
|
import { decide } from '../lib/phase.mjs';
|
|
8
8
|
import { checkConsent, consumedConsent } from '../lib/consent.mjs';
|
|
9
|
+
import { markSkipNext } from '../lib/reminder.mjs';
|
|
9
10
|
|
|
10
11
|
export function pmCheckpointTool(env) {
|
|
11
12
|
return {
|
|
12
13
|
name: nameOf('checkpoint'),
|
|
13
14
|
description:
|
|
14
|
-
'写交接、登记压缩;回合结束后自动压缩。resume=continue
|
|
15
|
+
'写交接、登记压缩;回合结束后自动压缩。resume=continue(缺省)表示压缩后续跑;resume=wait 表示任务已收尾,压缩后不唤醒。',
|
|
15
16
|
parameters: {
|
|
16
17
|
type: 'object',
|
|
17
18
|
additionalProperties: false,
|
|
18
19
|
properties: {
|
|
19
20
|
handover_text: { type: 'string', description: '要写入的交接正文;不给则要求磁盘上已有合格交接' },
|
|
20
21
|
handover_path: { type: 'string', description: '覆盖交接路径(演练用)' },
|
|
21
|
-
resume: { type: 'string', enum: ['continue', 'wait'], description: '压缩后的去向:continue
|
|
22
|
-
at_risk: { type: 'boolean', description: '当前处在关键步骤:为 true
|
|
22
|
+
resume: { type: 'string', enum: ['continue', 'wait'], description: '压缩后的去向:continue(缺省)按交接 §2 继续;wait 表示本轮任务已收尾,压缩后不唤醒,等待当前用户派新任务' },
|
|
23
|
+
at_risk: { type: 'boolean', description: '当前处在关键步骤:为 true 时,水位在提醒线与交接线之间只提醒、不压缩(过交接线仍压缩)' },
|
|
23
24
|
force: { type: 'boolean', description: '忽略关键步骤判定,直接排压缩' },
|
|
24
|
-
confirm: { type: 'boolean', description: '
|
|
25
|
+
confirm: { type: 'boolean', description: '判定为「可压缩」后,须经当前用户同意方可传 true;同时须当前用户在设置页点击「授权压缩」(5 分钟一次性授权),否则不排压缩' },
|
|
25
26
|
},
|
|
26
27
|
},
|
|
27
28
|
output: textOutput(),
|
|
@@ -37,7 +38,7 @@ export function pmCheckpointTool(env) {
|
|
|
37
38
|
return { text: [
|
|
38
39
|
'没有可用的交接路径:' + (target && target.reason ? target.reason : '未知'),
|
|
39
40
|
'先指定角色(设置页的 dsh-handover 中「会话角色」),或显式传 handover_path="<交接文件绝对路径>"。',
|
|
40
|
-
'
|
|
41
|
+
'压缩会丢失历史;没有合格交接,不排压缩。',
|
|
41
42
|
].join(NL) };
|
|
42
43
|
}
|
|
43
44
|
const file = target.path;
|
|
@@ -49,53 +50,53 @@ export function pmCheckpointTool(env) {
|
|
|
49
50
|
const ratioText = ratio === null ? '读不到' : (Math.round(ratio * 1000) / 10).toFixed(1) + '%';
|
|
50
51
|
const recentSec = st && st.lastSummaryMs ? (Date.now() - st.lastSummaryMs) / 1000 : null;
|
|
51
52
|
if (recentSec !== null && recentSec < 120 && args.force !== true) {
|
|
52
|
-
out.push("拒绝排压缩:" + Math.round(recentSec) + "
|
|
53
|
-
out.push("
|
|
53
|
+
out.push("拒绝排压缩:" + Math.round(recentSec) + " 秒前刚完成一次压缩(日志中有 compaction/summary),以免重复排程。");
|
|
54
|
+
out.push("等待一轮使投影缓存刷新;确需再次压缩,请加 force=true。");
|
|
54
55
|
return { text: out.join(NL) };
|
|
55
56
|
}
|
|
56
57
|
if (count.lock === "open") {
|
|
57
58
|
out.push("拒绝排压缩:检测到未闭合的压缩锁(compaction/start 无配对 end)。");
|
|
58
|
-
out.push("
|
|
59
|
+
out.push("处置:等当前压缩结束后用 ho_status 复查;若长时间无变化,说明上次压缩中途失败。");
|
|
59
60
|
return { text: out.join(NL) };
|
|
60
61
|
}
|
|
61
62
|
const adv = decide({ ratio, compactionsUsed: count.used, cfg: env.cfg, lockState: count.lock });
|
|
62
63
|
if (adv.action === "new_session") {
|
|
63
64
|
out.push("不建议压缩:" + adv.reason);
|
|
64
|
-
out.push('
|
|
65
|
+
out.push('下一步:按固定流程写交接。① ho_handover(action="read") 读旧交接;② action="archive" 归档;③ 读模板 _Control_Center/templates/Handover.md;④ action="new" 新建空白写入。之后由全局 ho_sessions(action="new") 建立新会话。');
|
|
65
66
|
return { text: out.join(NL) };
|
|
66
67
|
}
|
|
67
|
-
if (
|
|
68
|
-
out.push("
|
|
69
|
-
out.push("到阶段边界再调一次 ho_checkpoint(不带 at_risk 或带 force)。");
|
|
68
|
+
if (args.at_risk === true && args.force !== true && ratio !== null && ratio < (env.cfg.hardRatio || 0.85)) {
|
|
69
|
+
out.push("已暂缓(关键步骤):水位 " + ratioText + ",先完成当前小步骤再压缩;到阶段边界再调用一次 ho_checkpoint(不带 at_risk,或带 force)。");
|
|
70
70
|
return { text: out.join(NL) };
|
|
71
71
|
}
|
|
72
72
|
if (adv.action === "continue" && args.force !== true) {
|
|
73
|
-
out.push("
|
|
74
|
-
out.push("
|
|
73
|
+
out.push("水位未到可压缩线(" + ratioText + "):不压缩。");
|
|
74
|
+
out.push("需要提前压缩时,先向当前用户说明理由并取得授权,再加 force=true;当前用户主动要求时同样处理。");
|
|
75
75
|
return { text: out.join(NL) };
|
|
76
76
|
}
|
|
77
77
|
// 判断逻辑不动;只有判定可以压缩之后,才停下来问用户。
|
|
78
78
|
if (args.confirm !== true) {
|
|
79
79
|
out.push('判定:可以压缩(' + adv.reason + ')。');
|
|
80
|
-
out.push('水位 ' + ratioText + ',已压缩 ' + count.used + '/
|
|
81
|
-
out.push('
|
|
80
|
+
out.push('水位 ' + ratioText + ',已压缩 ' + count.used + '/' + env.cfg.maxCompactions + '。');
|
|
81
|
+
out.push('压缩会丢失历史并自动续跑。先向当前用户确认是否压缩,同意后再调用 ho_checkpoint(confirm=true);不同意则保持原状。');
|
|
82
82
|
return { text: out.join(NL) };
|
|
83
83
|
}
|
|
84
84
|
let wrote = null;
|
|
85
85
|
if (typeof args.handover_text === "string" && args.handover_text.trim() !== "") {
|
|
86
|
-
wrote =
|
|
87
|
-
out.push("交接已写入 " + file + "(" + wrote.bytes + " B
|
|
86
|
+
wrote = rewriteHandover(ws, file, args.handover_text, { kind: target.kind, info });
|
|
87
|
+
out.push("交接已写入 " + file + "(" + wrote.bytes + " B)· 角色 " + info.role);
|
|
88
|
+
if (wrote.archived) out.push("旧交接已归档 " + wrote.archived.dest + "(sha256 一致)");
|
|
88
89
|
out.push("体检 " + wrote.verdict + (wrote.reasons.length ? ":" : " · 结构齐备"));
|
|
89
90
|
for (const r of wrote.reasons) out.push(" - " + r);
|
|
90
91
|
if (wrote.verdict !== "通过") {
|
|
91
|
-
out.push("
|
|
92
|
+
out.push("体检未通过,不排压缩(先补齐结构;压缩后无法再改)。");
|
|
92
93
|
return { text: out.join(NL) };
|
|
93
94
|
}
|
|
94
95
|
} else {
|
|
95
96
|
const cur = readHandover(file);
|
|
96
97
|
if (cur.text === null || cur.text.trim() === "") {
|
|
97
98
|
out.push("没有交接正文,磁盘上也没有:" + file);
|
|
98
|
-
out.push("
|
|
99
|
+
out.push("拒绝压缩:压缩会丢失历史,必须先有交接。");
|
|
99
100
|
return { text: out.join(NL) };
|
|
100
101
|
}
|
|
101
102
|
out.push("沿用磁盘上已有交接:" + file);
|
|
@@ -105,28 +106,30 @@ export function pmCheckpointTool(env) {
|
|
|
105
106
|
// 会话自行判断水位与询问用户,用户点授权后自行排压缩,不必上报全局。
|
|
106
107
|
const cs = checkConsent(meId);
|
|
107
108
|
if (!cs.ok) {
|
|
108
|
-
out.push('
|
|
109
|
-
out.push('授权方式(任选其一):① 在聊天框发送 /allow-compact
|
|
110
|
-
out.push('
|
|
109
|
+
out.push('交接已写好,但未排压缩:' + cs.reason + '。');
|
|
110
|
+
out.push('授权方式(任选其一):① 在聊天框发送 /allow-compact(仅授权本会话,最为快捷);② 在设置页的 dsh-handover 中点击「授权压缩」一次。授权有效期 5 分钟,逾期须重新授权后再调用 ho_checkpoint(confirm=true)。');
|
|
111
|
+
out.push('限制原因:压缩会丢弃历史,而调用参数由 AI 自行填写;只有当前用户亲自点击的授权,才视为同意。');
|
|
111
112
|
return { text: out.join(NL) };
|
|
112
113
|
}
|
|
113
|
-
//
|
|
114
|
+
// 成本提示:低于可压缩线强行压缩会多付一次摘要,且本轮前缀缓存失效。
|
|
114
115
|
const handoverRatio = (env.cfg && env.cfg.handoverRatio) || 0.35;
|
|
115
116
|
if (ratio !== null && ratio < handoverRatio) {
|
|
116
|
-
out.push('
|
|
117
|
+
out.push('成本提示:当前水位 ' + (Math.round(ratio * 1000) / 10) + '% 低于可压缩线 ' + Math.round(handoverRatio * 100) + '%,属提前压缩,会多付一次摘要调用,且本轮前缀缓存失效。');
|
|
117
118
|
}
|
|
118
119
|
consumedConsent(meId);
|
|
120
|
+
// 压缩后第一轮的水位读数是压缩前的旧投影,跳过那一轮提醒判定,免得用虚高读数用掉一次性提醒。
|
|
121
|
+
markSkipNext(meId);
|
|
119
122
|
const n = count.used + 1;
|
|
120
123
|
const resume = args.resume === 'wait' ? 'wait' : 'continue';
|
|
121
124
|
env.schedule(meId, n, { resume });
|
|
122
|
-
out.push('当前用户已授权(' + cs.atText + '
|
|
123
|
-
out.push("已登记压缩(第 " + n + "/" + env.cfg.maxCompactions + "
|
|
125
|
+
out.push('当前用户已授权(' + cs.atText + ' 点击,剩余 ' + Math.round(cs.leftMs / 1000) + ' 秒)。');
|
|
126
|
+
out.push("已登记压缩(第 " + n + "/" + env.cfg.maxCompactions + " 次),本回合结束后执行。");
|
|
124
127
|
if (resume === 'wait') {
|
|
125
|
-
out.push("去向:wait ——
|
|
128
|
+
out.push("去向:wait —— 压缩完成后不唤醒本会话,等待当前用户派新任务。");
|
|
126
129
|
} else {
|
|
127
|
-
out.push("去向:continue —— 压缩完成后自动收到续跑消息;若 60
|
|
130
|
+
out.push("去向:continue —— 压缩完成后自动收到续跑消息;若 60 秒内未收到续跑消息,用 ho_status 查看状态。");
|
|
128
131
|
}
|
|
129
|
-
out.push("
|
|
132
|
+
out.push("压缩期间不得结束会话。");
|
|
130
133
|
return { text: out.join(NL) };
|
|
131
134
|
},
|
|
132
135
|
};
|
package/src/tools/ho_docmap.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ho_docmap ——
|
|
2
|
+
* ho_docmap —— 体系注册与文档定位(1.0)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 动作:read 读、write 写、check 校验路径、plan 读写与归档行动计划。
|
|
5
|
+
* 写入范围由会话已登记的角色决定:全局只写总注册,项目只写本项目注册,杂项只写本任务注册。
|
|
6
6
|
* 写入者必须是在任会话(以 Office.json 为准);已交出的旧会话只读。
|
|
7
|
-
*
|
|
7
|
+
* 写入后重装本会话提示段,实现热更新。
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
@@ -13,11 +13,14 @@ import { nameOf } from '../lib/names.mjs';
|
|
|
13
13
|
import { NL } from '../lib/text.mjs';
|
|
14
14
|
import { loadOffice, lineageKeyOf } from '../lib/office.mjs';
|
|
15
15
|
import {
|
|
16
|
-
lineageOf, lineageLabel,
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
lineageOf, lineageLabel, registryRelPath, registryAbsPath, readRegistry, readGlobalRegistry,
|
|
17
|
+
validateEntries, checkRegistries, writeRegistry, syncGlobalRefs,
|
|
18
|
+
planRelPath, readPlan, writePlan, archivePlan, pointRegistryToPlan,
|
|
19
|
+
GLOBAL_REGISTRY_REL, REGISTRY_NAME,
|
|
20
|
+
} from '../lib/system.mjs';
|
|
21
|
+
import { taskNameOk } from '../lib/role.mjs';
|
|
19
22
|
|
|
20
|
-
export const ACTIONS = ['read', 'write', 'check'];
|
|
23
|
+
export const ACTIONS = ['read', 'write', 'check', 'plan'];
|
|
21
24
|
|
|
22
25
|
function ownLineage(env, exec) {
|
|
23
26
|
let info = null;
|
|
@@ -38,15 +41,29 @@ function holderOf(ws, lin) {
|
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
function entriesText(entries) {
|
|
41
|
-
if (!entries.length) return '
|
|
44
|
+
if (!entries.length) return '(空:不注入正文)';
|
|
42
45
|
return entries.map((e) => '- ' + e.path + (e.about ? ' —— ' + e.about : '') + (e.when ? '(' + e.when + ')' : '')).join(NL);
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
function targetInfoFor(info, task) {
|
|
49
|
+
if (info && info.role === 'global') {
|
|
50
|
+
const t = String(task || '').trim();
|
|
51
|
+
if (!t) return null;
|
|
52
|
+
if (!taskNameOk(t)) throw new Error('任务名不合法(当目录名用):' + t);
|
|
53
|
+
return { role: 'global-task', task: t };
|
|
54
|
+
}
|
|
55
|
+
return info;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function nodePath(ws, ...parts) {
|
|
59
|
+
return path.join(String(ws || ''), ...parts);
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
export function pmDocmapTool(env) {
|
|
46
63
|
return {
|
|
47
64
|
name: nameOf('docmap'),
|
|
48
65
|
description:
|
|
49
|
-
'
|
|
66
|
+
'体系注册与文档定位:read 读、write 写(逐条校验路径存在)、check 校验、plan 读写并归档行动计划;全局总注册只列路径,全局任务用 task 读写自身注册与计划。',
|
|
50
67
|
parameters: {
|
|
51
68
|
type: 'object',
|
|
52
69
|
additionalProperties: false,
|
|
@@ -54,99 +71,168 @@ export function pmDocmapTool(env) {
|
|
|
54
71
|
action: { type: 'string', enum: ACTIONS, description: '缺省 read' },
|
|
55
72
|
entries: {
|
|
56
73
|
type: 'array',
|
|
57
|
-
description: 'write:整份替换。每项 { path: 工作区相对路径, about:
|
|
74
|
+
description: 'write:整份替换。每项 { path: 工作区相对路径, about: 用途, when: 读取时机, section: structure|files };section 缺省 files',
|
|
58
75
|
items: {
|
|
59
76
|
type: 'object',
|
|
60
77
|
additionalProperties: false,
|
|
61
78
|
required: ['path', 'about'],
|
|
62
79
|
properties: {
|
|
63
80
|
path: { type: 'string', description: '工作区相对路径,必须存在' },
|
|
64
|
-
about: { type: 'string', description: '
|
|
65
|
-
when: { type: 'string', description: '
|
|
81
|
+
about: { type: 'string', description: '用途,一句话' },
|
|
82
|
+
when: { type: 'string', description: '读取时机(可选)' },
|
|
83
|
+
section: { type: 'string', enum: ['structure', 'files'], description: '目录结构或规范文件(缺省 files)' },
|
|
66
84
|
},
|
|
67
85
|
},
|
|
68
86
|
},
|
|
87
|
+
mode: { type: 'string', enum: ['read', 'write', 'archive'], description: 'action=plan 时的动作,缺省 read' },
|
|
88
|
+
text: { type: 'string', description: 'action=plan、mode=write 时的行动计划正文' },
|
|
89
|
+
task: { type: 'string', description: '全局会话操作某个任务时给任务名(项目与杂项会话省略)' },
|
|
69
90
|
},
|
|
70
91
|
},
|
|
71
92
|
output: textOutput(),
|
|
72
93
|
async execute(args, exec) {
|
|
73
94
|
const ws = env.resolveWs(exec);
|
|
74
95
|
const { info, lin } = ownLineage(env, exec);
|
|
75
|
-
if (!lin) throw new Error('
|
|
96
|
+
if (!lin) throw new Error('本会话没有角色:体系注册只对全局、项目、杂项会话开放');
|
|
76
97
|
const action = typeof args.action === 'string' && args.action ? args.action : 'read';
|
|
77
98
|
if (ACTIONS.indexOf(action) === -1) throw new Error('action 只能是 ' + ACTIONS.join(' | ') + '(收到 ' + String(action) + ')');
|
|
78
|
-
const rel = docmapRelPath(info);
|
|
79
|
-
const abs = docmapAbsPath(ws, info);
|
|
80
99
|
const label = lineageLabel(info);
|
|
100
|
+
const meId = String(env.selfId(exec) || '');
|
|
81
101
|
|
|
82
102
|
if (action === 'read') {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
103
|
+
const taskName = String(args.task || '').trim();
|
|
104
|
+
if (lin.kind === 'global' && taskName) {
|
|
105
|
+
const target = targetInfoFor(info, taskName);
|
|
106
|
+
const reg = readRegistry(ws, target);
|
|
107
|
+
const rel = registryRelPath(target);
|
|
108
|
+
const out = ['体系注册(' + lineageLabel(target) + ') ' + rel];
|
|
109
|
+
out.push(reg.exists ? '状态 已设置,目录 ' + reg.data.structure.length + ' 条、规范 ' + reg.data.files.length + ' 条' : '状态 未设置');
|
|
110
|
+
if (reg.data.plan) out.push('计划 ' + reg.data.plan);
|
|
111
|
+
if (reg.data.structure.length) out.push('', '目录结构:', entriesText(reg.data.structure));
|
|
112
|
+
if (reg.data.files.length) out.push('', '规范文件:', entriesText(reg.data.files));
|
|
113
|
+
return { text: out.join(NL) };
|
|
114
|
+
}
|
|
115
|
+
if (lin.kind === 'global') {
|
|
116
|
+
const g = readGlobalRegistry(ws);
|
|
117
|
+
const out = ['体系注册(全局) ' + GLOBAL_REGISTRY_REL, g.rows.length ? '任务 ' + g.rows.length + ' 个' : '暂无任务注册'];
|
|
118
|
+
for (const r of g.rows) out.push(' ' + r.kind + '「' + r.task + '」 ' + r.path);
|
|
119
|
+
return { text: out.join(NL) };
|
|
120
|
+
}
|
|
121
|
+
const reg = readRegistry(ws, info);
|
|
122
|
+
const rel = registryRelPath(info);
|
|
123
|
+
const out = ['体系注册(' + label + ') ' + rel];
|
|
124
|
+
out.push(reg.exists ? '状态 已设置,目录 ' + reg.data.structure.length + ' 条、规范 ' + reg.data.files.length + ' 条' : '状态 未设置(空注册只注入一句提示)');
|
|
125
|
+
if (reg.data.plan) out.push('计划 ' + reg.data.plan);
|
|
126
|
+
if (reg.data.structure.length) out.push('', '目录结构:', entriesText(reg.data.structure));
|
|
127
|
+
if (reg.data.files.length) out.push('', '规范文件:', entriesText(reg.data.files));
|
|
88
128
|
return { text: out.join(NL) };
|
|
89
129
|
}
|
|
90
130
|
|
|
91
131
|
if (action === 'check') {
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
132
|
+
const res = lin.kind === 'global' ? checkRegistries(ws) : checkRegistries(ws, info);
|
|
133
|
+
const out = ['体系注册体检 ' + (lin.kind === 'global' ? '全部任务' : label)];
|
|
134
|
+
if (!res.rows.length) out.push(' 尚无任何任务注册(空注册不注入正文,属正常)');
|
|
135
|
+
for (const r of res.rows) {
|
|
136
|
+
out.push(' ' + r.label + ' ' + r.rel + ' ' + (r.exists ? r.entries + ' 条' : '未设置') + ' ' + (r.bad.length ? '失效 ' + r.bad.length + ' 条' : '路径全部存在'));
|
|
137
|
+
for (const b of r.bad.slice(0, 5)) out.push(' 失效:' + b.path + ' —— ' + b.problem);
|
|
138
|
+
}
|
|
139
|
+
out.push(res.missing ? '判定:' + res.missing + ' 条路径失效,由该类别在任会话用 ho_docmap(action="write") 更新。' : '判定:路径全部存在。');
|
|
140
|
+
return { text: out.join(NL) };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (action === 'plan') {
|
|
144
|
+
const mode = typeof args.mode === 'string' && args.mode ? args.mode : 'read';
|
|
145
|
+
let target = null;
|
|
146
|
+
try { target = targetInfoFor(info, args.task); } catch (e) { throw e; }
|
|
147
|
+
if (!target) throw new Error('全局会话读写计划时须给 task(任务名)');
|
|
148
|
+
const rel = planRelPath(ws, target);
|
|
149
|
+
if (!rel) throw new Error('没有可用的计划路径:角色未定');
|
|
150
|
+
if (mode === 'read') {
|
|
151
|
+
const p = readPlan(ws, target);
|
|
152
|
+
if (!p.exists) return { text: '计划未建立:' + rel + NL + '层二任务应先写行动计划,模板见 _Control_Center/templates/Plan.skeleton.md。' };
|
|
153
|
+
if (p.error) throw new Error(p.error);
|
|
154
|
+
return { text: p.text };
|
|
155
|
+
}
|
|
156
|
+
if (mode === 'archive') {
|
|
157
|
+
const archived = archivePlan(ws, target);
|
|
158
|
+
return { text: archived ? '旧计划已归档:' + archived : '没有可归档的旧计划:' + rel };
|
|
159
|
+
}
|
|
160
|
+
const text = String(args.text || '');
|
|
161
|
+
if (!text.trim()) throw new Error('mode=write 需要 text(行动计划正文)');
|
|
162
|
+
const r = writePlan(ws, target, text);
|
|
163
|
+
let pointed = false;
|
|
164
|
+
try { pointed = pointRegistryToPlan(ws, target); } catch { pointed = false; }
|
|
165
|
+
let refreshed = false;
|
|
166
|
+
try { refreshed = env.refreshPromptFor(meId) === true; } catch { refreshed = false; }
|
|
167
|
+
const out = ['已写入计划 ' + r.rel + '(' + r.bytes + ' B)' + (r.archived ? ';旧计划已归档 ' + r.archived : ';无旧计划')];
|
|
168
|
+
out.push(pointed ? '注册已指向该计划。' : '本任务尚无体系注册,写入注册后再指向计划。');
|
|
169
|
+
out.push(refreshed ? '提示段已重装。' : '提示段未能当场重装:下一轮或重启后生效。');
|
|
99
170
|
return { text: out.join(NL) };
|
|
100
171
|
}
|
|
101
172
|
|
|
102
173
|
// action=write
|
|
103
174
|
const holder = holderOf(ws, lin);
|
|
104
|
-
const meId = String(env.selfId(exec) || '');
|
|
105
175
|
if (!holder.current) {
|
|
106
|
-
throw new Error('
|
|
176
|
+
throw new Error('名册里没有「' + (holder.key || label) + '」的在任记录:以在任记录为写入依据,先完成名称注册(ho_office action="register")');
|
|
107
177
|
}
|
|
108
178
|
if (!meId || String(holder.current.sessionId || '') !== meId) {
|
|
109
179
|
throw new Error('只有在任会话可以写:本条登记的在任会话为 ' + (holder.current.name || '?') + '(' + holder.current.sessionId + '),本会话为只读');
|
|
110
180
|
}
|
|
181
|
+
|
|
182
|
+
if (lin.kind === 'global' && !String(args.task || '').trim()) {
|
|
183
|
+
const g = syncGlobalRefs(ws);
|
|
184
|
+
let refreshed = false;
|
|
185
|
+
try { refreshed = env.refreshPromptFor(meId) === true; } catch { refreshed = false; }
|
|
186
|
+
const out = ['已按磁盘任务重建全局总注册 ' + GLOBAL_REGISTRY_REL + '(' + g.rows.length + ' 个任务)'];
|
|
187
|
+
for (const r of g.rows) out.push(' ' + r.kind + '「' + r.task + '」 ' + r.path);
|
|
188
|
+
out.push(refreshed ? '提示段已重装:下一轮即注入引用清单。' : '提示段未能当场重装:重启 DSH 或让会话重建后生效。');
|
|
189
|
+
return { text: out.join(NL) };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 全局任务:由全局会话直接维护该任务的体系注册。
|
|
193
|
+
const writeInfo = lin.kind === 'global'
|
|
194
|
+
? targetInfoFor(info, String(args.task || '').trim())
|
|
195
|
+
: info;
|
|
196
|
+
if (!writeInfo) throw new Error('全局会话写任务注册时需要 task(任务名)');
|
|
111
197
|
const list = Array.isArray(args.entries) ? args.entries : null;
|
|
112
|
-
if (!list) throw new Error('write 需要 entries 数组(每项 path、about、when)');
|
|
113
|
-
const
|
|
198
|
+
if (!list) throw new Error('write 需要 entries 数组(每项 path、about、when、section)');
|
|
199
|
+
const structure = [];
|
|
200
|
+
const files = [];
|
|
114
201
|
for (const raw of list) {
|
|
115
202
|
const e = raw || {};
|
|
116
203
|
const p = String(e.path === undefined || e.path === null ? '' : e.path).trim();
|
|
117
204
|
const about = String(e.about === undefined || e.about === null ? '' : e.about).trim();
|
|
118
205
|
const when = String(e.when === undefined || e.when === null ? '' : e.when).trim();
|
|
206
|
+
const section = e.section === 'structure' ? 'structure' : 'files';
|
|
119
207
|
if (!p) throw new Error('entries 里有空路径');
|
|
120
208
|
for (const [field, val] of [['path', p], ['about', about], ['when', when]]) {
|
|
121
209
|
if (val.indexOf('|') !== -1) throw new Error(field + ' 里不能出现竖线「|」(表格分隔符):' + val);
|
|
122
210
|
if (val.indexOf(String.fromCharCode(10)) !== -1) throw new Error(field + ' 里不能换行');
|
|
123
211
|
}
|
|
124
|
-
|
|
212
|
+
(section === 'structure' ? structure : files).push({ path: p, about, when });
|
|
125
213
|
}
|
|
126
|
-
const bad = validateEntries(ws,
|
|
214
|
+
const bad = validateEntries(ws, structure.concat(files));
|
|
127
215
|
if (bad.length) {
|
|
128
216
|
const lines = ['未写入:下列路径不存在或不合法,逐条修正后重试。'];
|
|
129
217
|
for (const b of bad) lines.push(' - ' + b.path + ':' + b.problem);
|
|
130
|
-
lines.push('路径一律写工作区相对路径(例:Projects/<项目>/
|
|
218
|
+
lines.push('路径一律写工作区相对路径(例:Projects/<项目>/README.md)。');
|
|
131
219
|
throw new Error(lines.join(NL));
|
|
132
220
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
221
|
+
const cur = readRegistry(ws, writeInfo);
|
|
222
|
+
const plan = cur.data.plan || '';
|
|
223
|
+
if (plan) {
|
|
224
|
+
const problem = validateEntries(ws, [{ path: plan }]);
|
|
225
|
+
if (problem.length) throw new Error('注册里的计划路径已失效:' + plan + NL + '先修正计划(ho_docmap action="plan")或删除该行。');
|
|
138
226
|
}
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
fs.writeFileSync(tmp, body, 'utf8');
|
|
142
|
-
fs.renameSync(tmp, abs);
|
|
227
|
+
const r = writeRegistry(ws, writeInfo, { structure, files, plan });
|
|
228
|
+
if (lin.kind === 'global') syncGlobalRefs(ws);
|
|
143
229
|
let refreshed = false;
|
|
144
|
-
try {
|
|
230
|
+
try { refreshed = env.refreshPromptFor(meId) === true; } catch { refreshed = false; }
|
|
145
231
|
const out = [];
|
|
146
|
-
out.push('已写入 ' +
|
|
147
|
-
|
|
148
|
-
out.push('
|
|
149
|
-
|
|
232
|
+
out.push('已写入 ' + registryRelPath(writeInfo) + '(目录 ' + structure.length + ' 条、规范 ' + files.length + ' 条)'
|
|
233
|
+
+ (r.backup ? ';原文件备份为 ' + path.basename(r.backup) : ''));
|
|
234
|
+
out.push(refreshed ? '提示段已重装:下一轮即按新注册注入。' : '提示段未能当场重装:重启 DSH 或让会话重建后生效。');
|
|
235
|
+
out.push('只给路径与用途;不设字数上限,但过长的用途会挤占提示段。');
|
|
150
236
|
return { text: out.join(NL) };
|
|
151
237
|
},
|
|
152
238
|
};
|
package/src/tools/ho_gate.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { textOutput } from '../lib/output.mjs';
|
|
|
6
6
|
import { NL } from '../lib/text.mjs';
|
|
7
7
|
import { syncCheck, renderSyncCheck, docmapReport, renderDocmapCheck } from '../lib/gates.mjs';
|
|
8
8
|
import { lintGlobal, lintProject, renderLintGlobal, renderLintProject } from '../lib/lint.mjs';
|
|
9
|
+
import { legacyInfo } from '../lib/legacy.mjs';
|
|
9
10
|
import { SETUP_STEPS } from '../lib/guide.mjs';
|
|
10
11
|
|
|
11
12
|
function lintAllProjects(ws) {
|
|
@@ -26,12 +27,12 @@ export function pmGateTool(env) {
|
|
|
26
27
|
return {
|
|
27
28
|
name: nameOf('gate'),
|
|
28
29
|
description:
|
|
29
|
-
'
|
|
30
|
+
'通用约束条闸门(按库型判定)、交接文档体检与体系注册路径检查。只读。',
|
|
30
31
|
parameters: {
|
|
31
32
|
type: 'object',
|
|
32
33
|
additionalProperties: false,
|
|
33
34
|
properties: {
|
|
34
|
-
scope: { type: 'string', enum: ['all', 'global', 'project'], description: '
|
|
35
|
+
scope: { type: 'string', enum: ['all', 'global', 'project'], description: '缺省 all' },
|
|
35
36
|
project: { type: 'string', description: 'scope=project 时的项目名' },
|
|
36
37
|
},
|
|
37
38
|
},
|
|
@@ -47,16 +48,20 @@ export function pmGateTool(env) {
|
|
|
47
48
|
const g = syncCheck(ws);
|
|
48
49
|
if (g.noWorkspace) {
|
|
49
50
|
out.push('通用约束条闸门:未接入工作区(' + g.path + ')');
|
|
50
|
-
out.push('
|
|
51
|
+
out.push('接入工作区共 4 步:');
|
|
51
52
|
SETUP_STEPS.forEach((s, i) => out.push(' ' + (i + 1) + '. ' + s));
|
|
52
53
|
return { text: out.join(NL) };
|
|
53
54
|
}
|
|
54
55
|
out.push(renderSyncCheck(g).join(NL));
|
|
55
56
|
out.push(renderLintProject(null, lintAllProjects(ws)).join(NL));
|
|
56
57
|
out.push(renderDocmapCheck(docmapReport(ws)).join(NL));
|
|
58
|
+
try {
|
|
59
|
+
const legacy = legacyInfo(ws);
|
|
60
|
+
if (legacy.exists) out.push('旧文档定位待迁移:' + legacy.dir + '(ho_workspace action="migrate")');
|
|
61
|
+
} catch { /* 旧目录检查失败不影响闸门 */ }
|
|
57
62
|
} else if (scope === "project") {
|
|
58
63
|
const name = args.project;
|
|
59
|
-
if (!name) throw new Error("scope=project
|
|
64
|
+
if (!name) throw new Error("scope=project 时须给 project 参数");
|
|
60
65
|
const f = path.join(ws, "_Project_Meta", name, "Handover.md");
|
|
61
66
|
out.push(renderLintProject(name, [{ name, file: f, res: lintProject(f) }]).join(NL));
|
|
62
67
|
}
|