@mingmaple/dsh-handover 0.9.14 → 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 +123 -92
- 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 +34 -0
- package/docs/releases/1.0.0.md +33 -0
- package/docs/troubleshoot.md +230 -46
- package/package.json +74 -71
- package/scripts/behavior.mjs +398 -0
- package/scripts/blank-dsh.mjs +41 -25
- package/scripts/blank-smoke.mjs +278 -109
- package/scripts/check-docs.mjs +7 -0
- package/scripts/e2e-migration.mjs +2 -2
- package/scripts/office.mjs +3 -3
- package/scripts/portability.mjs +71 -0
- package/scripts/style-gate.mjs +571 -0
- package/src/client.js +1028 -955
- package/src/index.mjs +313 -142
- package/src/lib/absrefs.mjs +1 -1
- package/src/lib/api.mjs +18 -10
- package/src/lib/archive.mjs +1 -1
- package/src/lib/brief.mjs +8 -6
- package/src/lib/config.mjs +23 -8
- package/src/lib/consent.mjs +54 -1
- package/src/lib/docmap.mjs +140 -0
- 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 +30 -6
- package/src/lib/global-task.mjs +69 -0
- package/src/lib/guide.mjs +81 -85
- 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 +17 -13
- package/src/lib/manual.mjs +135 -59
- package/src/lib/mover.mjs +23 -19
- package/src/lib/names.mjs +14 -19
- package/src/lib/office.mjs +63 -15
- package/src/lib/pack-ops.mjs +11 -11
- package/src/lib/pathkey.mjs +24 -0
- package/src/lib/peer.mjs +4 -4
- 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 +9 -6
- package/src/lib/scaffold.mjs +68 -26
- package/src/lib/schedule.mjs +5 -6
- package/src/lib/session-create.mjs +3 -3
- package/src/lib/session-ops.mjs +80 -51
- package/src/lib/sessions.mjs +41 -3
- package/src/lib/sevenzip.mjs +10 -1
- package/src/lib/status.mjs +337 -176
- package/src/lib/succession.mjs +16 -16
- package/src/lib/system.mjs +364 -0
- package/src/lib/zstd-log.mjs +1 -1
- package/src/selftest.mjs +6 -6
- 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 +101 -20
- package/src/templates/Runtime_Manual.md +263 -122
- 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 -23
- 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 +37 -0
- 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 +135 -0
- package/src/tools/ho_changelog.mjs +4 -4
- package/src/tools/ho_charter.mjs +7 -6
- package/src/tools/ho_checkpoint.mjs +36 -32
- package/src/tools/ho_docmap.mjs +239 -0
- package/src/tools/ho_gate.mjs +11 -5
- package/src/tools/ho_handover.mjs +61 -35
- package/src/tools/ho_office.mjs +46 -5
- package/src/tools/ho_profile.mjs +3 -3
- package/src/tools/ho_sessions.mjs +46 -26
- package/src/tools/ho_status.mjs +251 -0
- package/src/tools/ho_succeed.mjs +3 -3
- package/src/tools/ho_workspace.mjs +146 -42
- package/src/tools/peer.mjs +416 -0
- package/src/tools/ho_brief.mjs +0 -57
- package/src/tools/ho_diag.mjs +0 -87
- package/src/tools/ho_drift.mjs +0 -41
- package/src/tools/ho_init.mjs +0 -82
- package/src/tools/ho_pack.mjs +0 -38
- package/src/tools/ho_phase.mjs +0 -37
- package/src/tools/ho_snapshot.mjs +0 -104
- package/src/tools/ho_tokens.mjs +0 -57
- package/src/tools/peer_list.mjs +0 -37
- package/src/tools/peer_new.mjs +0 -75
- package/src/tools/peer_peek.mjs +0 -41
- package/src/tools/peer_rename.mjs +0 -31
- package/src/tools/peer_send.mjs +0 -52
- package/src/tools/peer_status.mjs +0 -46
|
@@ -2,33 +2,34 @@
|
|
|
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(),
|
|
28
29
|
async execute(args, exec) {
|
|
29
30
|
const ws = env.resolveWs(exec);
|
|
30
31
|
const meId = env.selfId(exec);
|
|
31
|
-
if (!meId) throw new Error("
|
|
32
|
+
if (!meId) throw new Error("无法获取本会话 id,无法调度压缩");
|
|
32
33
|
const info = env.roleOf(exec);
|
|
33
34
|
const target = args.handover_path
|
|
34
35
|
? { path: args.handover_path, kind: info.role === 'project' ? 'project' : info.role === 'misc' ? 'misc' : 'global' }
|
|
@@ -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;
|
|
@@ -46,55 +47,56 @@ export function pmCheckpointTool(env) {
|
|
|
46
47
|
const self = env.selfPct(meId);
|
|
47
48
|
const ratio = self === null ? null : self / 100;
|
|
48
49
|
const out = [];
|
|
50
|
+
const ratioText = ratio === null ? '读不到' : (Math.round(ratio * 1000) / 10).toFixed(1) + '%';
|
|
49
51
|
const recentSec = st && st.lastSummaryMs ? (Date.now() - st.lastSummaryMs) / 1000 : null;
|
|
50
52
|
if (recentSec !== null && recentSec < 120 && args.force !== true) {
|
|
51
|
-
out.push("拒绝排压缩:" + Math.round(recentSec) + "
|
|
52
|
-
out.push("
|
|
53
|
+
out.push("拒绝排压缩:" + Math.round(recentSec) + " 秒前刚完成一次压缩(日志中有 compaction/summary),以免重复排程。");
|
|
54
|
+
out.push("等待一轮使投影缓存刷新;确需再次压缩,请加 force=true。");
|
|
53
55
|
return { text: out.join(NL) };
|
|
54
56
|
}
|
|
55
57
|
if (count.lock === "open") {
|
|
56
58
|
out.push("拒绝排压缩:检测到未闭合的压缩锁(compaction/start 无配对 end)。");
|
|
57
|
-
out.push("
|
|
59
|
+
out.push("处置:等当前压缩结束后用 ho_status 复查;若长时间无变化,说明上次压缩中途失败。");
|
|
58
60
|
return { text: out.join(NL) };
|
|
59
61
|
}
|
|
60
62
|
const adv = decide({ ratio, compactionsUsed: count.used, cfg: env.cfg, lockState: count.lock });
|
|
61
63
|
if (adv.action === "new_session") {
|
|
62
64
|
out.push("不建议压缩:" + adv.reason);
|
|
63
|
-
out.push('
|
|
65
|
+
out.push('下一步:按固定流程写交接。① ho_handover(action="read") 读旧交接;② action="archive" 归档;③ 读模板 _Control_Center/templates/Handover.md;④ action="new" 新建空白写入。之后由全局 ho_sessions(action="new") 建立新会话。');
|
|
64
66
|
return { text: out.join(NL) };
|
|
65
67
|
}
|
|
66
|
-
if (
|
|
67
|
-
out.push("
|
|
68
|
-
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)。");
|
|
69
70
|
return { text: out.join(NL) };
|
|
70
71
|
}
|
|
71
72
|
if (adv.action === "continue" && args.force !== true) {
|
|
72
|
-
out.push("
|
|
73
|
-
out.push("
|
|
73
|
+
out.push("水位未到可压缩线(" + ratioText + "):不压缩。");
|
|
74
|
+
out.push("需要提前压缩时,先向当前用户说明理由并取得授权,再加 force=true;当前用户主动要求时同样处理。");
|
|
74
75
|
return { text: out.join(NL) };
|
|
75
76
|
}
|
|
76
77
|
// 判断逻辑不动;只有判定可以压缩之后,才停下来问用户。
|
|
77
78
|
if (args.confirm !== true) {
|
|
78
79
|
out.push('判定:可以压缩(' + adv.reason + ')。');
|
|
79
|
-
out.push('水位 ' +
|
|
80
|
-
out.push('
|
|
80
|
+
out.push('水位 ' + ratioText + ',已压缩 ' + count.used + '/' + env.cfg.maxCompactions + '。');
|
|
81
|
+
out.push('压缩会丢失历史并自动续跑。先向当前用户确认是否压缩,同意后再调用 ho_checkpoint(confirm=true);不同意则保持原状。');
|
|
81
82
|
return { text: out.join(NL) };
|
|
82
83
|
}
|
|
83
84
|
let wrote = null;
|
|
84
85
|
if (typeof args.handover_text === "string" && args.handover_text.trim() !== "") {
|
|
85
|
-
wrote =
|
|
86
|
-
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 一致)");
|
|
87
89
|
out.push("体检 " + wrote.verdict + (wrote.reasons.length ? ":" : " · 结构齐备"));
|
|
88
90
|
for (const r of wrote.reasons) out.push(" - " + r);
|
|
89
91
|
if (wrote.verdict !== "通过") {
|
|
90
|
-
out.push("
|
|
92
|
+
out.push("体检未通过,不排压缩(先补齐结构;压缩后无法再改)。");
|
|
91
93
|
return { text: out.join(NL) };
|
|
92
94
|
}
|
|
93
95
|
} else {
|
|
94
96
|
const cur = readHandover(file);
|
|
95
97
|
if (cur.text === null || cur.text.trim() === "") {
|
|
96
98
|
out.push("没有交接正文,磁盘上也没有:" + file);
|
|
97
|
-
out.push("
|
|
99
|
+
out.push("拒绝压缩:压缩会丢失历史,必须先有交接。");
|
|
98
100
|
return { text: out.join(NL) };
|
|
99
101
|
}
|
|
100
102
|
out.push("沿用磁盘上已有交接:" + file);
|
|
@@ -104,28 +106,30 @@ export function pmCheckpointTool(env) {
|
|
|
104
106
|
// 会话自行判断水位与询问用户,用户点授权后自行排压缩,不必上报全局。
|
|
105
107
|
const cs = checkConsent(meId);
|
|
106
108
|
if (!cs.ok) {
|
|
107
|
-
out.push('
|
|
108
|
-
out.push('授权方式(任选其一):① 在聊天框发送 /allow-compact
|
|
109
|
-
out.push('
|
|
109
|
+
out.push('交接已写好,但未排压缩:' + cs.reason + '。');
|
|
110
|
+
out.push('授权方式(任选其一):① 在聊天框发送 /allow-compact(仅授权本会话,最为快捷);② 在设置页的 dsh-handover 中点击「授权压缩」一次。授权有效期 5 分钟,逾期须重新授权后再调用 ho_checkpoint(confirm=true)。');
|
|
111
|
+
out.push('限制原因:压缩会丢弃历史,而调用参数由 AI 自行填写;只有当前用户亲自点击的授权,才视为同意。');
|
|
110
112
|
return { text: out.join(NL) };
|
|
111
113
|
}
|
|
112
|
-
//
|
|
114
|
+
// 成本提示:低于可压缩线强行压缩会多付一次摘要,且本轮前缀缓存失效。
|
|
113
115
|
const handoverRatio = (env.cfg && env.cfg.handoverRatio) || 0.35;
|
|
114
116
|
if (ratio !== null && ratio < handoverRatio) {
|
|
115
|
-
out.push('
|
|
117
|
+
out.push('成本提示:当前水位 ' + (Math.round(ratio * 1000) / 10) + '% 低于可压缩线 ' + Math.round(handoverRatio * 100) + '%,属提前压缩,会多付一次摘要调用,且本轮前缀缓存失效。');
|
|
116
118
|
}
|
|
117
119
|
consumedConsent(meId);
|
|
120
|
+
// 压缩后第一轮的水位读数是压缩前的旧投影,跳过那一轮提醒判定,免得用虚高读数用掉一次性提醒。
|
|
121
|
+
markSkipNext(meId);
|
|
118
122
|
const n = count.used + 1;
|
|
119
123
|
const resume = args.resume === 'wait' ? 'wait' : 'continue';
|
|
120
124
|
env.schedule(meId, n, { resume });
|
|
121
|
-
out.push('
|
|
122
|
-
out.push("已登记压缩(第 " + n + "/" + env.cfg.maxCompactions + "
|
|
125
|
+
out.push('当前用户已授权(' + cs.atText + ' 点击,剩余 ' + Math.round(cs.leftMs / 1000) + ' 秒)。');
|
|
126
|
+
out.push("已登记压缩(第 " + n + "/" + env.cfg.maxCompactions + " 次),本回合结束后执行。");
|
|
123
127
|
if (resume === 'wait') {
|
|
124
|
-
out.push("去向:wait ——
|
|
128
|
+
out.push("去向:wait —— 压缩完成后不唤醒本会话,等待当前用户派新任务。");
|
|
125
129
|
} else {
|
|
126
|
-
out.push("去向:continue —— 压缩完成后自动收到续跑消息;若 60
|
|
130
|
+
out.push("去向:continue —— 压缩完成后自动收到续跑消息;若 60 秒内未收到续跑消息,用 ho_status 查看状态。");
|
|
127
131
|
}
|
|
128
|
-
out.push("
|
|
132
|
+
out.push("压缩期间不得结束会话。");
|
|
129
133
|
return { text: out.join(NL) };
|
|
130
134
|
},
|
|
131
135
|
};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ho_docmap —— 体系注册与文档定位(1.0)。
|
|
3
|
+
*
|
|
4
|
+
* 动作:read 读、write 写、check 校验路径、plan 读写与归档行动计划。
|
|
5
|
+
* 写入范围由会话已登记的角色决定:全局只写总注册,项目只写本项目注册,杂项只写本任务注册。
|
|
6
|
+
* 写入者必须是在任会话(以 Office.json 为准);已交出的旧会话只读。
|
|
7
|
+
* 写入后重装本会话提示段,实现热更新。
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { textOutput } from '../lib/output.mjs';
|
|
12
|
+
import { nameOf } from '../lib/names.mjs';
|
|
13
|
+
import { NL } from '../lib/text.mjs';
|
|
14
|
+
import { loadOffice, lineageKeyOf } from '../lib/office.mjs';
|
|
15
|
+
import {
|
|
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';
|
|
22
|
+
|
|
23
|
+
export const ACTIONS = ['read', 'write', 'check', 'plan'];
|
|
24
|
+
|
|
25
|
+
function ownLineage(env, exec) {
|
|
26
|
+
let info = null;
|
|
27
|
+
try { info = env.roleOf(exec); } catch { info = null; }
|
|
28
|
+
const lin = lineageOf(info);
|
|
29
|
+
return { info, lin };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function holderOf(ws, lin) {
|
|
33
|
+
try {
|
|
34
|
+
const store = loadOffice(ws);
|
|
35
|
+
const key = lineageKeyOf(lin.kind, lin.project, lin.task);
|
|
36
|
+
const row = store.lineages ? store.lineages[key] : null;
|
|
37
|
+
return { key, current: row && row.current ? row.current : null };
|
|
38
|
+
} catch {
|
|
39
|
+
return { key: '', current: null };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function entriesText(entries) {
|
|
44
|
+
if (!entries.length) return '(空:不注入正文)';
|
|
45
|
+
return entries.map((e) => '- ' + e.path + (e.about ? ' —— ' + e.about : '') + (e.when ? '(' + e.when + ')' : '')).join(NL);
|
|
46
|
+
}
|
|
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
|
+
|
|
62
|
+
export function pmDocmapTool(env) {
|
|
63
|
+
return {
|
|
64
|
+
name: nameOf('docmap'),
|
|
65
|
+
description:
|
|
66
|
+
'体系注册与文档定位:read 读、write 写(逐条校验路径存在)、check 校验、plan 读写并归档行动计划;全局总注册只列路径,全局任务用 task 读写自身注册与计划。',
|
|
67
|
+
parameters: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
properties: {
|
|
71
|
+
action: { type: 'string', enum: ACTIONS, description: '缺省 read' },
|
|
72
|
+
entries: {
|
|
73
|
+
type: 'array',
|
|
74
|
+
description: 'write:整份替换。每项 { path: 工作区相对路径, about: 用途, when: 读取时机, section: structure|files };section 缺省 files',
|
|
75
|
+
items: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
required: ['path', 'about'],
|
|
79
|
+
properties: {
|
|
80
|
+
path: { type: 'string', description: '工作区相对路径,必须存在' },
|
|
81
|
+
about: { type: 'string', description: '用途,一句话' },
|
|
82
|
+
when: { type: 'string', description: '读取时机(可选)' },
|
|
83
|
+
section: { type: 'string', enum: ['structure', 'files'], description: '目录结构或规范文件(缺省 files)' },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
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: '全局会话操作某个任务时给任务名(项目与杂项会话省略)' },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
output: textOutput(),
|
|
93
|
+
async execute(args, exec) {
|
|
94
|
+
const ws = env.resolveWs(exec);
|
|
95
|
+
const { info, lin } = ownLineage(env, exec);
|
|
96
|
+
if (!lin) throw new Error('本会话没有角色:体系注册只对全局、项目、杂项会话开放');
|
|
97
|
+
const action = typeof args.action === 'string' && args.action ? args.action : 'read';
|
|
98
|
+
if (ACTIONS.indexOf(action) === -1) throw new Error('action 只能是 ' + ACTIONS.join(' | ') + '(收到 ' + String(action) + ')');
|
|
99
|
+
const label = lineageLabel(info);
|
|
100
|
+
const meId = String(env.selfId(exec) || '');
|
|
101
|
+
|
|
102
|
+
if (action === 'read') {
|
|
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));
|
|
128
|
+
return { text: out.join(NL) };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (action === 'check') {
|
|
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 ? '提示段已重装。' : '提示段未能当场重装:下一轮或重启后生效。');
|
|
170
|
+
return { text: out.join(NL) };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// action=write
|
|
174
|
+
const holder = holderOf(ws, lin);
|
|
175
|
+
if (!holder.current) {
|
|
176
|
+
throw new Error('名册里没有「' + (holder.key || label) + '」的在任记录:以在任记录为写入依据,先完成名称注册(ho_office action="register")');
|
|
177
|
+
}
|
|
178
|
+
if (!meId || String(holder.current.sessionId || '') !== meId) {
|
|
179
|
+
throw new Error('只有在任会话可以写:本条登记的在任会话为 ' + (holder.current.name || '?') + '(' + holder.current.sessionId + '),本会话为只读');
|
|
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(任务名)');
|
|
197
|
+
const list = Array.isArray(args.entries) ? args.entries : null;
|
|
198
|
+
if (!list) throw new Error('write 需要 entries 数组(每项 path、about、when、section)');
|
|
199
|
+
const structure = [];
|
|
200
|
+
const files = [];
|
|
201
|
+
for (const raw of list) {
|
|
202
|
+
const e = raw || {};
|
|
203
|
+
const p = String(e.path === undefined || e.path === null ? '' : e.path).trim();
|
|
204
|
+
const about = String(e.about === undefined || e.about === null ? '' : e.about).trim();
|
|
205
|
+
const when = String(e.when === undefined || e.when === null ? '' : e.when).trim();
|
|
206
|
+
const section = e.section === 'structure' ? 'structure' : 'files';
|
|
207
|
+
if (!p) throw new Error('entries 里有空路径');
|
|
208
|
+
for (const [field, val] of [['path', p], ['about', about], ['when', when]]) {
|
|
209
|
+
if (val.indexOf('|') !== -1) throw new Error(field + ' 里不能出现竖线「|」(表格分隔符):' + val);
|
|
210
|
+
if (val.indexOf(String.fromCharCode(10)) !== -1) throw new Error(field + ' 里不能换行');
|
|
211
|
+
}
|
|
212
|
+
(section === 'structure' ? structure : files).push({ path: p, about, when });
|
|
213
|
+
}
|
|
214
|
+
const bad = validateEntries(ws, structure.concat(files));
|
|
215
|
+
if (bad.length) {
|
|
216
|
+
const lines = ['未写入:下列路径不存在或不合法,逐条修正后重试。'];
|
|
217
|
+
for (const b of bad) lines.push(' - ' + b.path + ':' + b.problem);
|
|
218
|
+
lines.push('路径一律写工作区相对路径(例:Projects/<项目>/README.md)。');
|
|
219
|
+
throw new Error(lines.join(NL));
|
|
220
|
+
}
|
|
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")或删除该行。');
|
|
226
|
+
}
|
|
227
|
+
const r = writeRegistry(ws, writeInfo, { structure, files, plan });
|
|
228
|
+
if (lin.kind === 'global') syncGlobalRefs(ws);
|
|
229
|
+
let refreshed = false;
|
|
230
|
+
try { refreshed = env.refreshPromptFor(meId) === true; } catch { refreshed = false; }
|
|
231
|
+
const out = [];
|
|
232
|
+
out.push('已写入 ' + registryRelPath(writeInfo) + '(目录 ' + structure.length + ' 条、规范 ' + files.length + ' 条)'
|
|
233
|
+
+ (r.backup ? ';原文件备份为 ' + path.basename(r.backup) : ''));
|
|
234
|
+
out.push(refreshed ? '提示段已重装:下一轮即按新注册注入。' : '提示段未能当场重装:重启 DSH 或让会话重建后生效。');
|
|
235
|
+
out.push('只给路径与用途;不设字数上限,但过长的用途会挤占提示段。');
|
|
236
|
+
return { text: out.join(NL) };
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
package/src/tools/ho_gate.mjs
CHANGED
|
@@ -4,8 +4,9 @@ import { nameOf } from '../lib/names.mjs';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import { textOutput } from '../lib/output.mjs';
|
|
6
6
|
import { NL } from '../lib/text.mjs';
|
|
7
|
-
import { syncCheck, renderSyncCheck } from '../lib/gates.mjs';
|
|
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,15 +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));
|
|
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 { /* 旧目录检查失败不影响闸门 */ }
|
|
56
62
|
} else if (scope === "project") {
|
|
57
63
|
const name = args.project;
|
|
58
|
-
if (!name) throw new Error("scope=project
|
|
64
|
+
if (!name) throw new Error("scope=project 时须给 project 参数");
|
|
59
65
|
const f = path.join(ws, "_Project_Meta", name, "Handover.md");
|
|
60
66
|
out.push(renderLintProject(name, [{ name, file: f, res: lintProject(f) }]).join(NL));
|
|
61
67
|
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
/** ho_handover ——
|
|
1
|
+
/** ho_handover —— 读/新建/追加/归档**本会话自己的**交接(路径按角色解析;整篇重写先归档旧交接,写后按角色体检)。 */
|
|
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 fs from 'node:fs';
|
|
6
5
|
import path from 'node:path';
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import { archiveDirFor } from '../lib/role.mjs';
|
|
6
|
+
import { readHandover, writeHandover, rewriteHandover, archiveHandover } from '../lib/handover-io.mjs';
|
|
7
|
+
import { assertTaskName } from '../lib/role.mjs';
|
|
10
8
|
|
|
11
9
|
function noPath(input) {
|
|
12
10
|
const reason = input && input.reason ? input.reason : '未知原因';
|
|
13
11
|
return [
|
|
14
12
|
'没有可用的交接路径:' + reason,
|
|
15
|
-
'
|
|
16
|
-
' ① 在设置页的 dsh-handover
|
|
13
|
+
'处置:',
|
|
14
|
+
' ① 在设置页的 dsh-handover 中「会话角色」给本会话指定角色(项目角色须填项目名);',
|
|
17
15
|
' ② 临时显式传 path="<交接文件绝对路径>"。',
|
|
18
|
-
'
|
|
16
|
+
'(插件不再默认读全局交接:旧版行为会导致角色串用。)',
|
|
19
17
|
].join(NL);
|
|
20
18
|
}
|
|
21
19
|
|
|
@@ -23,58 +21,86 @@ export function pmHandoverTool(env) {
|
|
|
23
21
|
return {
|
|
24
22
|
name: nameOf('handover'),
|
|
25
23
|
description:
|
|
26
|
-
'
|
|
24
|
+
'读写、新建、追加、归档交接;整篇重写用 new(先归档旧交接,再新建空白文件写入);项目与杂项会话读写自己的交接,全局会话可用 task 指向某个全局任务。写后按角色体检。',
|
|
27
25
|
parameters: {
|
|
28
26
|
type: 'object',
|
|
29
27
|
additionalProperties: false,
|
|
30
28
|
required: ['action'],
|
|
31
29
|
properties: {
|
|
32
|
-
action: { type: 'string', enum: ['read', 'write', 'append', 'archive'], description: 'read | write
|
|
33
|
-
text: { type: 'string', description: 'write/append 时的正文(read/archive 忽略)' },
|
|
30
|
+
action: { type: 'string', enum: ['read', 'new', 'write', 'append', 'archive'], description: 'read | new(整篇重写:归档旧交接后新建空白写入)| write(仅首次创建)| append | archive' },
|
|
31
|
+
text: { type: 'string', description: 'new/write/append 时的正文(read/archive 忽略)' },
|
|
34
32
|
path: { type: 'string', description: '覆盖默认路径(演练或强制指定时用)' },
|
|
33
|
+
task: { type: 'string', description: '全局会话读写某个全局任务的交接:_Control_Center/tasks/<任务>/Handover.md' },
|
|
35
34
|
},
|
|
36
35
|
},
|
|
37
36
|
output: textOutput(),
|
|
38
37
|
async execute(args, exec) {
|
|
39
38
|
const ws = env.resolveWs(exec);
|
|
40
39
|
const info = env.roleOf(exec);
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
const taskName = String(args.task || '').trim();
|
|
41
|
+
let target = null;
|
|
42
|
+
if (taskName) {
|
|
43
|
+
if (info.role !== 'global') throw new Error('task 参数只对全局会话开放');
|
|
44
|
+
assertTaskName(taskName);
|
|
45
|
+
target = {
|
|
46
|
+
path: path.join(ws, '_Control_Center', 'tasks', taskName, 'Handover.md'),
|
|
47
|
+
kind: 'project', role: 'global-task', task: taskName,
|
|
48
|
+
};
|
|
49
|
+
} else {
|
|
50
|
+
target = args.path
|
|
51
|
+
? { path: args.path, kind: info.role === 'project' ? 'project' : info.role === 'misc' ? 'misc' : 'global', role: info.role, project: info.project, task: info.task }
|
|
52
|
+
: env.handoverFor(exec);
|
|
53
|
+
}
|
|
44
54
|
if (!target || !target.path) return { text: noPath(target) };
|
|
55
|
+
const archiveOpts = taskName
|
|
56
|
+
? {
|
|
57
|
+
info: { role: 'misc', task: taskName },
|
|
58
|
+
archiveDir: path.join(ws, '_Archives', '_Global', 'tasks', taskName, 'Handover'),
|
|
59
|
+
}
|
|
60
|
+
: { kind: target.kind, info };
|
|
61
|
+
const snapOpts = taskName
|
|
62
|
+
? { kind: target.kind, info: { role: 'misc', task: taskName }, snapshotDir: path.join(ws, '_Temp', '_Global', 'tasks', taskName, 'Snapshots') }
|
|
63
|
+
: { kind: target.kind, info };
|
|
45
64
|
|
|
46
65
|
if (args.action === "read") {
|
|
47
66
|
const r = readHandover(target.path);
|
|
48
|
-
if (r.text === null) return { text: "交接文档不存在:" + target.path + NL + "
|
|
67
|
+
if (r.text === null) return { text: "交接文档不存在:" + target.path + NL + "(首次创建用 action=write;整篇重写用 action=new)" };
|
|
49
68
|
return { text: r.text };
|
|
50
69
|
}
|
|
51
70
|
if (args.action === "archive") {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
for (let n = 2; fs.existsSync(dest) && n < 100; n++) {
|
|
60
|
-
dest = path.join(dir, archiveFileName(target.path, stampText + "-" + n));
|
|
61
|
-
}
|
|
62
|
-
if (fs.existsSync(dest)) throw new Error("归档目标已存在,不覆盖:" + dest);
|
|
63
|
-
fs.copyFileSync(target.path, dest);
|
|
64
|
-
const a = crypto.createHash("sha256").update(fs.readFileSync(target.path)).digest("hex");
|
|
65
|
-
const b = crypto.createHash("sha256").update(fs.readFileSync(dest)).digest("hex");
|
|
71
|
+
if (!readHandover(target.path).text) throw new Error("要归档的交接不存在:" + target.path);
|
|
72
|
+
const a = archiveHandover(ws, target.path, archiveOpts);
|
|
73
|
+
return { text: "已归档 " + a.dest + "(" + a.bytes + " B)" + NL + "sha256 一致 " + a.sha256.slice(0, 16) + "…" };
|
|
74
|
+
}
|
|
75
|
+
if (args.action === "new") {
|
|
76
|
+
if (typeof args.text !== "string" || args.text.trim() === "") throw new Error("new 需要 text");
|
|
77
|
+
const res = rewriteHandover(ws, target.path, args.text, Object.assign({}, snapOpts, archiveOpts));
|
|
66
78
|
const out = [];
|
|
67
|
-
out.push(
|
|
68
|
-
|
|
79
|
+
out.push(res.archived
|
|
80
|
+
? (res.archived.reused
|
|
81
|
+
? "已归档(复用内容相同的副本)" + res.archived.dest + "(" + res.archived.bytes + " B)"
|
|
82
|
+
: "已归档 " + res.archived.dest + "(" + res.archived.bytes + " B,sha256 一致)")
|
|
83
|
+
: "原交接不存在,直接新建(无需归档)");
|
|
84
|
+
out.push("已新建 " + res.path + "(" + res.bytes + " B)· 角色 " + (taskName ? 'global-task' : info.role) + (taskName ? " · 任务「" + taskName + "」" : ""));
|
|
85
|
+
out.push("体检 " + res.verdict + (res.reasons.length ? "" : " · 结构齐备"));
|
|
86
|
+
for (const r of res.reasons) out.push(" - " + r);
|
|
87
|
+
if (res.verdict !== "通过") out.push("提示:修正后重跑 action=new(将再次归档当前文件)。");
|
|
69
88
|
return { text: out.join(NL) };
|
|
70
89
|
}
|
|
71
90
|
if (args.action !== "write" && args.action !== "append") {
|
|
72
|
-
throw new Error("未知 action:" + String(args.action) + "(只能是 read | write | append | archive)");
|
|
91
|
+
throw new Error("未知 action:" + String(args.action) + "(只能是 read | new | write | append | archive)");
|
|
92
|
+
}
|
|
93
|
+
if (typeof args.text !== "string" || args.text.trim() === "") throw new Error(args.action + " 需要 text");
|
|
94
|
+
if (args.action === "write" && readHandover(target.path).text) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
"交接已存在,不能用 write 改写:" + target.path + NL
|
|
97
|
+
+ "整篇重写按固定流程:① ho_handover(action=\"read\") 读旧交接;② ho_handover(action=\"archive\") 归档;③ 读模板 _Control_Center/templates/Handover.md;④ ho_handover(action=\"new\", text=…) 新建空白写入。",
|
|
98
|
+
);
|
|
73
99
|
}
|
|
74
|
-
|
|
75
|
-
const res = writeHandover(ws, target.path, args.text, args.action === "append" ? "append" : "write", { kind: target.kind, info });
|
|
100
|
+
const res = writeHandover(ws, target.path, args.text, args.action, snapOpts);
|
|
76
101
|
const out = [];
|
|
77
|
-
|
|
102
|
+
const scope = taskName ? '全局任务「' + taskName + '」' : (target.kind !== 'global' ? '(' + (info.project || info.task) + ')' : '');
|
|
103
|
+
out.push("已写入 " + res.path + "(" + res.bytes + " B)· 角色 " + (taskName ? 'global-task' : info.role) + scope);
|
|
78
104
|
out.push("备份 " + (res.backup || "(原文件不存在,无需备份)"));
|
|
79
105
|
out.push("体检 " + res.verdict + (res.reasons.length ? "" : " · 结构齐备"));
|
|
80
106
|
for (const r of res.reasons) out.push(" - " + r);
|