@mingmaple/dsh-handover 0.9.6 → 0.9.13

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.
Files changed (50) hide show
  1. package/README.md +53 -17
  2. package/docs/migrate.md +1 -1
  3. package/docs/releases/0.9.12.md +28 -0
  4. package/docs/releases/0.9.13.md +30 -0
  5. package/docs/troubleshoot.md +43 -6
  6. package/package.json +70 -57
  7. package/scripts/blank-dsh.mjs +6 -5
  8. package/scripts/blank-smoke.mjs +268 -58
  9. package/scripts/build.mjs +3 -0
  10. package/scripts/check-docs.mjs +23 -2
  11. package/scripts/office.mjs +81 -0
  12. package/src/client.js +92 -23
  13. package/src/index.mjs +264 -145
  14. package/src/lib/api.mjs +67 -9
  15. package/src/lib/config.mjs +4 -3
  16. package/src/lib/consent.mjs +59 -20
  17. package/src/lib/facts.mjs +5 -4
  18. package/src/lib/guide.mjs +14 -7
  19. package/src/lib/handover-io.mjs +20 -0
  20. package/src/lib/manual.mjs +23 -12
  21. package/src/lib/names.mjs +2 -2
  22. package/src/lib/office.mjs +272 -0
  23. package/src/lib/pack-ops.mjs +3 -0
  24. package/src/lib/phase.mjs +6 -4
  25. package/src/lib/prompt.mjs +18 -0
  26. package/src/lib/role.mjs +52 -18
  27. package/src/lib/schedule.mjs +10 -14
  28. package/src/lib/session-create.mjs +42 -0
  29. package/src/lib/session-ops.mjs +183 -79
  30. package/src/lib/sessions.mjs +9 -8
  31. package/src/lib/status.mjs +8 -2
  32. package/src/lib/succession.mjs +244 -0
  33. package/src/lib/text.mjs +27 -0
  34. package/src/lib/webroute.mjs +1 -1
  35. package/src/lib/workspace.mjs +1 -1
  36. package/src/selftest.mjs +15 -0
  37. package/src/templates/Charter.skeleton.md +1 -1
  38. package/src/templates/Prompt_Templates.md +9 -17
  39. package/src/templates/Runtime_Manual.md +17 -14
  40. package/src/templates/rules/Handover_Rules.md +3 -2
  41. package/src/templates/rules/Misc_Task_Standard.md +3 -3
  42. package/src/tools/ho_checkpoint.mjs +11 -4
  43. package/src/tools/ho_diag.mjs +5 -3
  44. package/src/tools/ho_handover.mjs +7 -6
  45. package/src/tools/ho_office.mjs +59 -0
  46. package/src/tools/ho_profile.mjs +10 -2
  47. package/src/tools/ho_sessions.mjs +3 -2
  48. package/src/tools/ho_snapshot.mjs +2 -1
  49. package/src/tools/ho_succeed.mjs +24 -0
  50. package/src/tools/ho_workspace.mjs +7 -3
@@ -5,7 +5,7 @@ import { NL } from '../lib/text.mjs';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import crypto from 'node:crypto';
8
- import { readHandover, writeHandover } from '../lib/handover-io.mjs';
8
+ import { readHandover, writeHandover, archiveStamp, archiveFileName } from '../lib/handover-io.mjs';
9
9
  import { archiveDirFor } from '../lib/role.mjs';
10
10
 
11
11
  function noPath(input) {
@@ -51,13 +51,14 @@ export function pmHandoverTool(env) {
51
51
  if (args.action === "archive") {
52
52
  const src = readHandover(target.path);
53
53
  if (src.text === null) throw new Error("要归档的交接不存在:" + target.path);
54
- const d = new Date();
55
- const p = (n) => (n < 10 ? "0" + n : "" + n);
56
- const stampText = d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + "_" + p(d.getHours()) + p(d.getMinutes());
54
+ const stampText = archiveStamp(new Date());
57
55
  const dir = archiveDirFor(ws, info);
58
56
  fs.mkdirSync(dir, { recursive: true });
59
- const base = info.role === 'project' ? (info.project + '_Handover_') : info.role === 'misc' ? ((info.task || 'task') + '_Handover_') : 'Global_Handover_';
60
- const dest = path.join(dir, base + stampText + ".md");
57
+ // 命名按 Handover_Rules §4.2:<原文件名>_<YYYYMMDD_HHMMSS>.md;同一秒重复归档加 -2、-3。
58
+ let dest = path.join(dir, archiveFileName(target.path, stampText));
59
+ for (let n = 2; fs.existsSync(dest) && n < 100; n++) {
60
+ dest = path.join(dir, archiveFileName(target.path, stampText + "-" + n));
61
+ }
61
62
  if (fs.existsSync(dest)) throw new Error("归档目标已存在,不覆盖:" + dest);
62
63
  fs.copyFileSync(target.path, dest);
63
64
  const a = crypto.createHash("sha256").update(fs.readFileSync(target.path)).digest("hex");
@@ -0,0 +1,59 @@
1
+ /** ho_office —— 在任登记表:查某条类别现在由哪个会话在任(只读)。 */
2
+ import { textOutput } from '../lib/output.mjs';
3
+ import { nameOf } from '../lib/names.mjs';
4
+ import { NL } from '../lib/text.mjs';
5
+ import { loadOffice, resolveOffice, rowsOf, formatRow, officePath } from '../lib/office.mjs';
6
+
7
+ export function pmOfficeTool(env) {
8
+ return {
9
+ name: nameOf('office'),
10
+ description:
11
+ '查在任:输入「全局」「全局3」「项目名」等,返回现任会话的名字与 id;多个用空格分隔,留空列全部。只读。',
12
+ parameters: {
13
+ type: 'object',
14
+ additionalProperties: false,
15
+ properties: {
16
+ query: { type: 'string', description: '要查的名字,如「全局」「全局3」「Mingent」「抢名额脚本」;多个用空格或逗号分隔;留空=列出全部在任' },
17
+ json: { type: 'boolean', description: 'true 时输出机器可读 JSON(供脚本解析)' },
18
+ },
19
+ },
20
+ output: textOutput(),
21
+ async execute(args, exec) {
22
+ const ws = env.resolveWs(exec);
23
+ if (!ws) return { text: '无法确定工作区根,读不到在任登记表。' };
24
+ const store = loadOffice(ws);
25
+ const rows = rowsOf(store);
26
+ if (!rows.length) {
27
+ return { text: '在任登记表还是空的(' + officePath(ws) + '):先登记一次在任会话,再查。' };
28
+ }
29
+ const raw = String(args.query || '').trim();
30
+ const tokens = raw ? raw.split(/[\s,,、;;]+/).filter(Boolean) : [];
31
+ if (!tokens.length) {
32
+ if (args.json) return { text: JSON.stringify({ updatedAt: store.updatedAt || '', rows }, null, 2) };
33
+ const out = [];
34
+ let lastKind = '';
35
+ for (const r of rows) {
36
+ if (r.kind !== lastKind) {
37
+ if (out.length) out.push('');
38
+ out.push('【' + (r.kind === 'global' ? '全局' : r.kind === 'project' ? '项目' : r.kind === 'misc' ? '杂项' : r.kind) + '】');
39
+ lastKind = r.kind;
40
+ }
41
+ out.push(formatRow(r));
42
+ }
43
+ return { text: out.join(NL) };
44
+ }
45
+ const out = [];
46
+ const hits = [];
47
+ for (const t of tokens) {
48
+ const r = resolveOffice(store, t);
49
+ if (!r.ok) { out.push(t + ' — ' + r.reason); continue; }
50
+ if (r.vacant) { out.push(t + ' — 类别「' + r.key + '」当前空缺(无人在任)'); continue; }
51
+ const row = { key: r.key, kind: r.lineage.kind, project: r.lineage.project, task: r.lineage.task, current: r.lineage.current };
52
+ out.push(formatRow(row, t));
53
+ hits.push({ askedAs: t, via: r.via, key: r.key, kind: r.lineage.kind, name: r.lineage.current.name, sessionId: r.lineage.current.sessionId });
54
+ }
55
+ if (args.json) return { text: JSON.stringify({ updatedAt: store.updatedAt || '', hits, lines: out }, null, 2) };
56
+ return { text: out.join(NL) };
57
+ },
58
+ };
59
+ }
@@ -3,15 +3,23 @@ import { textOutput } from '../lib/output.mjs';
3
3
  import { nameOf } from '../lib/names.mjs';
4
4
  import { NL } from '../lib/text.mjs';
5
5
  import { presets, readProfile, writeProfile, validateProfile, profilePathFor, PROFILE_KINDS } from '../lib/profile.mjs';
6
+ import { assertProjectName, assertTaskName } from '../lib/role.mjs';
6
7
  import { stampTz } from '../lib/brief.mjs';
7
8
 
8
9
  function targetOf(env, info, args, exec) {
9
10
  if (info && (info.role === 'project' || info.role === 'misc')) {
11
+ const own = info.role === 'project' ? String(info.project || '') : String(info.task || '');
12
+ const given = String(args.project || args.task || '').trim();
13
+ // 项目会话只处理自己的骨架;传了别的名字就明确报错,而不是静默忽略入参(实测踩过)。
14
+ if (given && given !== own) {
15
+ throw new Error('本会话是' + (info.role === 'project' ? '项目「' + own + '」' : '杂项「' + own + '」')
16
+ + ',只能读写自己的骨架;要看「' + given + '」请由全局会话执行 ho_profile。');
17
+ }
10
18
  return { role: info.role, project: info.project || '', task: info.task || '', self: true };
11
19
  }
12
20
  if (info && info.role === 'global') {
13
- if (args.project) return { role: 'project', project: String(args.project), task: '', self: false };
14
- if (args.task) return { role: 'misc', project: '', task: String(args.task), self: false };
21
+ if (args.project) return { role: 'project', project: assertProjectName(String(args.project)), task: '', self: false };
22
+ if (args.task) return { role: 'misc', project: '', task: assertTaskName(String(args.task)), self: false };
15
23
  throw new Error('全局会话使用 ho_profile 时须给 project 或 task');
16
24
  }
17
25
  throw new Error('普通会话不参与骨架注册');
@@ -67,6 +67,7 @@ export function pmSessionsTool(env) {
67
67
  properties: {
68
68
  action: { type: 'string', enum: ['list', 'role', 'new', 'order', 'place', 'prompt', 'archive', 'unarchive'], description: '缺省 list' },
69
69
  order: { type: 'string', enum: ['handover', 'compact'], description: 'order 的动作:handover 让它写交接再换任;compact 给它排压缩(先要有合格交接)' },
70
+ resume: { type: 'string', enum: ['continue', 'wait'], description: 'order=compact 时的去向:缺省 wait(交接后收尾,压缩后不唤醒);continue 表示压缩后按交接继续' },
70
71
  include_subagents: { type: 'boolean', description: 'list 时含子会话' },
71
72
  limit: { type: 'number', description: 'list 行数(默认 40)' },
72
73
  warn: { type: 'number', description: 'list 预警阈值(默认 ' + watermark(env.cfg).warn + ')' },
@@ -76,8 +77,8 @@ export function pmSessionsTool(env) {
76
77
  task: { type: 'string', description: '任务名(role=misc 必填)' },
77
78
  notify: { type: 'boolean', description: 'role 时投整理指令(默认 true,会唤醒它一轮)' },
78
79
  title: { type: 'string', description: 'new 的标题;prompt 用于推断代际' },
79
- workspace: { type: 'string', description: 'new、place 的工作区 id、标题、路径' },
80
- cwd: { type: 'string', description: 'new 的工作目录(与 workspace 二选一)' },
80
+ workspace: { type: 'string', description: 'new 必填:目标工作区的 id、标题或路径;new 时不给也能按 project 自动匹配同名工作区' },
81
+ cwd: { type: 'string', description: 'new 的工作目录,必须是某个已注册工作区的根(与 workspace 二选一)' },
81
82
  message: { type: 'string', description: 'new 的启动提示词;不给则自动生成' },
82
83
  force: { type: 'boolean', description: 'new 越过 <' + watermark(env.cfg).warn + ' 复用判据;order=compact 越过 120 秒去抖;archive 允许停掉在跑的' },
83
84
  kind: { type: 'string', enum: ['global', 'project', 'misc', 'migration'], description: 'prompt 的类型;new 时用作角色候选' },
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import crypto from 'node:crypto';
6
6
  import { textOutput } from '../lib/output.mjs';
7
7
  import { NL, isDir } from '../lib/text.mjs';
8
+ import { assertProjectName } from '../lib/role.mjs';
8
9
 
9
10
  const SKIP_DIRS = ['__pycache__', '.git', 'node_modules', '.obsidian'];
10
11
  const SKIP_FILES = ['.DS_Store', 'Thumbs.db'];
@@ -50,7 +51,7 @@ export function pmSnapshotTool(env) {
50
51
  output: textOutput(),
51
52
  async execute(args, exec) {
52
53
  const ws = env.resolveWs(exec);
53
- const project = typeof args.project === 'string' ? args.project.trim() : '';
54
+ const project = assertProjectName(typeof args.project === 'string' ? args.project.trim() : '');
54
55
  if (!project) throw new Error('project 不能为空(写项目名,如 Demo)');
55
56
  const projDir = path.join(ws, 'Projects', project);
56
57
  if (!isDir(projDir)) throw new Error('找不到项目:' + projDir);
@@ -0,0 +1,24 @@
1
+ /** ho_succeed —— 换任自服务:在任会话在用户授权后,自行校验交接、建接任会话并登记在任。 */
2
+ import { textOutput } from '../lib/output.mjs';
3
+ import { nameOf } from '../lib/names.mjs';
4
+ import { runSuccession } from '../lib/succession.mjs';
5
+
6
+ export function pmSucceedTool(env) {
7
+ return {
8
+ name: nameOf('succeed'),
9
+ description:
10
+ '换任:校验本会话交接,建接任会话并登记在任;须用户先授权(/allow-succeed)。',
11
+ parameters: {
12
+ type: 'object',
13
+ additionalProperties: false,
14
+ properties: {
15
+ note: { type: 'string', description: '写进在任登记的备注(可选)' },
16
+ force: { type: 'boolean', description: '本会话水位未到线时仍换任(默认不传)' },
17
+ },
18
+ },
19
+ output: textOutput(),
20
+ async execute(args, exec) {
21
+ return { text: await runSuccession(env, exec, { note: args && args.note, force: args && args.force === true }) };
22
+ },
23
+ };
24
+ }
@@ -47,8 +47,9 @@ export function pmWorkspaceTool(env) {
47
47
  enum: ['list', 'create', 'delete', 'move-probe', 'move', 'move-switch', 'move-status', 'move-retarget', 'move-repair', 'move-cleanup'],
48
48
  description: 'list / create / delete / move-probe / move / move-switch / move-status / move-retarget / move-repair / move-cleanup',
49
49
  },
50
- path: { type: 'string', description: 'create 用:已存在的绝对目录(不会替你先建)' },
50
+ path: { type: 'string', description: 'create 用:目录绝对路径;默认要求已存在,ensure=true 时自动创建' },
51
51
  title: { type: 'string', description: 'create 用:显示标题' },
52
+ ensure: { type: 'boolean', description: 'create 用:目录不存在时自动创建(默认 false,不替你建目录)' },
52
53
  workspace: { type: 'string', description: 'delete 用:工作区 id、标题、路径' },
53
54
  to: { type: 'string', description: 'move 用:新根绝对路径(父目录可自动建)' },
54
55
  confirm: { type: 'boolean', description: 'move-cleanup 用:true 才真的删旧副本' },
@@ -82,10 +83,13 @@ export function pmWorkspaceTool(env) {
82
83
  if (action === 'create') {
83
84
  if (!reg || typeof reg.create !== 'function') throw new Error('workspaceRegistry.create 不可用');
84
85
  const p = String(args.path || '').trim();
85
- if (!p) throw new Error('create 需要 path(已存在的绝对目录)');
86
+ if (!p) throw new Error('create 需要 path(目录绝对路径)');
86
87
  let st = null;
87
88
  try { st = fs.statSync(p); } catch { st = null; }
88
- if (!st || !st.isDirectory()) throw new Error('目录不存在或不是目录:' + p + '(本工具不替你建目录,先手工建好再注册)');
89
+ if ((!st || !st.isDirectory()) && args.ensure === true) {
90
+ try { fs.mkdirSync(p, { recursive: true }); st = fs.statSync(p); } catch (e) { throw new Error('建目录失败:' + p + '(' + String(e && e.message ? e.message : e) + ')'); }
91
+ }
92
+ if (!st || !st.isDirectory()) throw new Error('目录不存在或不是目录:' + p + '(默认不替你建目录;要建请传 ensure=true,或先手工建好)');
89
93
  const title = String(args.title || '').trim() || p.split(/[\\/]/).filter(Boolean).pop() || p;
90
94
  let w = null;
91
95
  try { w = await reg.create(p, title); } catch (e) { throw new Error('建工作区失败:' + String(e && e.message ? e.message : e)); }