@shgroup/dsh-serenity-hooks 1.32.0 → 1.34.1

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 (100) hide show
  1. package/README.md +12 -11
  2. package/dsh.plugin.json +5 -5
  3. package/lib/acp-core.d.ts +2 -22
  4. package/lib/acp-http.d.ts +8 -0
  5. package/lib/api.d.ts +12 -4
  6. package/lib/autopilot-chain.d.ts +76 -0
  7. package/lib/autopilot-core-DCRkiWEe.js +229 -0
  8. package/lib/autopilot-core.d.ts +132 -0
  9. package/lib/autopilot-ops.d.ts +48 -0
  10. package/lib/autopilot-trajectory-D9Znxo75.js +782 -0
  11. package/lib/autopilot-trajectory.d.ts +79 -83
  12. package/lib/{ccc-DAsSHsub.js → ccc-NlLr_sxy.js} +18 -2
  13. package/lib/ccc-roots-Bd_SjEs7.js +152 -0
  14. package/lib/ccc-roots.d.ts +105 -0
  15. package/lib/ccc.d.ts +23 -6
  16. package/lib/client.js +30 -10
  17. package/lib/clock-runtime.d.ts +197 -0
  18. package/lib/config-ops.d.ts +9 -14
  19. package/lib/constants.d.ts +0 -2
  20. package/lib/container-status.d.ts +183 -0
  21. package/lib/diag-ops.d.ts +76 -0
  22. package/lib/face-host.d.ts +110 -0
  23. package/lib/fs-ops.d.ts +3 -2
  24. package/lib/gateway-auth.d.ts +2 -5
  25. package/lib/gateway.d.ts +0 -34
  26. package/lib/git-ops.d.ts +3 -2
  27. package/lib/handyman-ops.d.ts +3 -2
  28. package/lib/handyman-preset-inherit.d.ts +2 -1
  29. package/lib/host/access.d.ts +8 -7
  30. package/lib/host/contract.d.ts +17 -6
  31. package/lib/host/type-contract.d.ts +1 -2
  32. package/lib/im-bridge.d.ts +6 -5
  33. package/lib/index.d.ts +1 -1
  34. package/lib/index.js +11053 -8305
  35. package/lib/invariant.d.ts +5 -16
  36. package/lib/invariant.js +8 -4
  37. package/lib/json.d.ts +1 -1
  38. package/lib/kit-ops.d.ts +11 -31
  39. package/lib/localstore-ops.d.ts +3 -17
  40. package/lib/msm-ops.d.ts +34 -52
  41. package/lib/opencode-provider.d.ts +4 -5
  42. package/lib/output-guard-seam.d.ts +1 -4
  43. package/lib/output-guard.d.ts +12 -3
  44. package/lib/ports.d.ts +59 -0
  45. package/lib/rebuild.d.ts +5 -4
  46. package/lib/seams/bootstrap.d.ts +4 -3
  47. package/lib/seams/compact.d.ts +2 -1
  48. package/lib/seams/context.d.ts +2 -4
  49. package/lib/seams/env.d.ts +2 -1
  50. package/lib/seams/guards.d.ts +22 -34
  51. package/lib/seams/keeper.d.ts +4 -4
  52. package/lib/seams/system-prompt.d.ts +1 -5
  53. package/lib/session-cleanup.d.ts +3 -2
  54. package/lib/{settings-section-Bm-xG-NR.js → settings-section-DTYZjwUs.js} +70 -15
  55. package/lib/settings-section.d.ts +14 -35
  56. package/lib/skiff-core.d.ts +5 -10
  57. package/lib/skiff-debug.d.ts +15 -21
  58. package/lib/skiff-registry-FTjoWQTQ.js +25 -0
  59. package/lib/skiff-registry.d.ts +2 -1
  60. package/lib/{skiff-role-BYvNnwv1.js → skiff-role-CEHL5cek.js} +1 -1
  61. package/lib/skiff-role.d.ts +2 -2
  62. package/lib/skills/opencode-scan.d.ts +3 -2
  63. package/lib/skills-discovery.d.ts +15 -2
  64. package/lib/status.d.ts +2 -1
  65. package/lib/tools/acc-diag.d.ts +22 -0
  66. package/lib/tools/cce.d.ts +1 -2
  67. package/lib/tools/container-admin.d.ts +7 -3
  68. package/lib/tools/eap.d.ts +2 -3
  69. package/lib/tools/handyman.d.ts +0 -29
  70. package/lib/tools/msm.d.ts +0 -4
  71. package/lib/tools/neat.d.ts +1 -2
  72. package/lib/tools/praxis.d.ts +0 -1
  73. package/lib/tools/skiff-admin.d.ts +2 -2
  74. package/lib/tools/{session.d.ts → trajectory.d.ts} +25 -17
  75. package/lib/totp.d.ts +0 -4
  76. package/lib/trajectory-assistant.d.ts +2 -1
  77. package/lib/trajectory-bound-Cax1p9ut.js +190 -0
  78. package/lib/{session-bound.d.ts → trajectory-bound.d.ts} +5 -6
  79. package/lib/trajectory-ops-mrXK0H0I.js +410 -0
  80. package/lib/{session-ops.d.ts → trajectory-ops.d.ts} +27 -36
  81. package/lib/trajectory-skills.d.ts +71 -0
  82. package/lib/wake-registry-CYxIb484.js +257 -0
  83. package/lib/wake-registry.d.ts +3 -11
  84. package/lib/wake-scheduler.d.ts +23 -20
  85. package/lib/web-fetch-provider.d.ts +0 -21
  86. package/lib/{weixin-api-PjEhmjlZ.js → weixin-api-CtFYm53S.js} +3 -9
  87. package/lib/weixin-api.d.ts +6 -37
  88. package/lib/weixin-bridge.d.ts +5 -24
  89. package/lib/weixin-hook.d.ts +6 -12
  90. package/lib/weixin-output-guard.d.ts +2 -1
  91. package/lib/{weixin-route-wWk4AIwV.js → weixin-route-B2ajFypI.js} +4 -5
  92. package/lib/weixin-route.d.ts +0 -2
  93. package/lib/weixin-send-api.d.ts +15 -6
  94. package/package.json +3 -5
  95. package/experiments/autopilot-trajectory/SKILL.md +0 -115
  96. package/experiments/autopilot-trajectory/scripts/autopilot-trajectory.ts +0 -522
  97. package/lib/session-bound-NHozeLFc.js +0 -822
  98. package/lib/skiff-debug-K2k1JnGj.js +0 -2867
  99. package/lib/tools/autopilot-trajectory.d.ts +0 -25
  100. package/lib/{session-cleanup-CMT4mHWt.js → session-cleanup-B6IHZgww.js} +1 -1
@@ -0,0 +1,410 @@
1
+ import { basename, dirname, join } from "node:path";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
+ //#region src/trajectory-ops.ts
4
+ /**
5
+ * trajectory-ops.ts — 轨迹工具纯操作层(零 DSH 依赖,可独立单测)
6
+ *
7
+ * 行为对齐 osp(opencode-serenity-plugin/src/session/lib.ts)——osp 是 ACC 工具 spec。
8
+ *
9
+ * v1.33 收敛(S142 §32):本文件只服务 `container_trajectory` 的 6 个动作
10
+ * (list / show / create / use / rebuild / wake-later)。旧 `logbook` 面里
11
+ * **close / archive / health / qa 四个动作已删**(用户裁决「功能上废弃,实际上没用」),
12
+ * 其函数(closeSession / archiveSessions / healthCheck / qaCheck)**同批删除**——
13
+ * 保留下来的只有:create(--desc / --issue 二选一、dry-run 预览)、
14
+ * list(含 summarize 仪表盘)、show、use(激活 + 绑定)、以及供 list 用的 summarize。
15
+ * 「完成」不再由 close 写入,而是从 SESSION.md 的 `[x]` 推导;归档走 `container_fs mv`。
16
+ */
17
+ const TRAJECTORY_ACTIONS = [
18
+ "list",
19
+ "show",
20
+ "create",
21
+ "use",
22
+ "rebuild",
23
+ "wake-later"
24
+ ];
25
+ /**
26
+ * 读取 Session 事件序列(v1.28.1 适配 0.1.2-rc.1 补齐):rc.1 起官方 Session 类
27
+ * 移除 `.events` 属性 → `snapshotEvents()` 方法(dsh-session/src/session.ts:
28
+ * `snapshotEvents(fromSeq, toSeqExclusive)`)。插件早期代码多处裸读 `.events`
29
+ * (经 `as unknown as { events? }` 断言绕过 typecheck),运行时静默 undefined——
30
+ * 造成 first-anchor 每轮重插 / SESSION 激活恢复失效 / rebuild 定位错乱。
31
+ * 统一收敛到本 helper:snapshotEvents() 优先(rc.1 真实形态),`.events` 兜底
32
+ * (测试替身/旧运行时)。所有消费方一律经此读取,禁止再裸读 `.events`。
33
+ * 泛型 T:调用方按需声明事件形状(如 `SessionEvent`),unknown 默认。
34
+ */
35
+ function sessionEvents(session) {
36
+ const s = session;
37
+ if (!s) return [];
38
+ if (typeof s.snapshotEvents === "function") try {
39
+ return s.snapshotEvents() ?? [];
40
+ } catch {}
41
+ return s.events ?? [];
42
+ }
43
+ const SESSION_MD = "SESSION.md";
44
+ const HEALTH_STALE_DAYS = 7;
45
+ const DAY = 864e5;
46
+ function sessionsRoot(root) {
47
+ return join(root, "AGENT_SESSIONS");
48
+ }
49
+ /** 解析 SESSION.md 状态元数据(对齐 osp parseSessionMd) */
50
+ function parseSessionMd(filePath) {
51
+ try {
52
+ const content = readFileSync(filePath, "utf-8");
53
+ return {
54
+ hasSessionMd: true,
55
+ completed: /\[\s*x\s*\]/i.test(content)
56
+ };
57
+ } catch {
58
+ return {
59
+ hasSessionMd: false,
60
+ completed: false
61
+ };
62
+ }
63
+ }
64
+ function readSessionEntry(dirPath) {
65
+ try {
66
+ const st = statSync(dirPath);
67
+ if (!st.isDirectory()) return null;
68
+ const dirName = basename(dirPath);
69
+ const mdPath = join(dirPath, SESSION_MD);
70
+ const status = existsSync(mdPath) ? parseSessionMd(mdPath) : {
71
+ hasSessionMd: false,
72
+ completed: false
73
+ };
74
+ return {
75
+ dirName,
76
+ path: dirPath,
77
+ mtime: st.mtime,
78
+ status
79
+ };
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+ /** 读取 AGENT_SESSIONS 中所有会话,活跃(未完成)排前(对齐 osp readAllSessions) */
85
+ function readAllSessions(sessionsDir) {
86
+ try {
87
+ return readdirSync(sessionsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => readSessionEntry(join(sessionsDir, e.name))).filter((s) => s !== null).sort((a, b) => {
88
+ if (!a.status.completed && b.status.completed) return -1;
89
+ if (a.status.completed && !b.status.completed) return 1;
90
+ return b.mtime.getTime() - a.mtime.getTime();
91
+ });
92
+ } catch {
93
+ return [];
94
+ }
95
+ }
96
+ /** 提取目录名中的会话 ID(S### 或 issue 名);无匹配返回 '' */
97
+ function extractSessionId(dirName) {
98
+ const m = dirName.match(/--S(\d{3,})--/);
99
+ return m ? `S${m[1]}` : "";
100
+ }
101
+ /**
102
+ * 根据 key 查找会话(对齐 osp findSession):
103
+ * 精确目录名 → S### ID(允许 S31→031)→ 唯一模糊子串匹配(多个则报错)
104
+ */
105
+ function findSession(sessionsDir, key) {
106
+ const all = readAllSessions(sessionsDir);
107
+ const byName = all.find((s) => s.dirName === key);
108
+ if (byName) return byName;
109
+ const searchId = key.replace(/^S/, "").padStart(3, "0");
110
+ const byId = all.find((s) => {
111
+ const m = s.dirName.match(/--S(\d{3,})--/);
112
+ return m && m[1] === searchId;
113
+ });
114
+ if (byId) return byId;
115
+ const lower = key.toLowerCase();
116
+ const fuzzy = all.filter((s) => s.dirName.toLowerCase().includes(lower));
117
+ if (fuzzy.length === 1) return fuzzy[0] ?? null;
118
+ if (fuzzy.length > 1) throw new Error(`Found ${fuzzy.length} sessions matching "${key}": ` + fuzzy.map((s) => s.dirName).join(", ") + ". Use a more specific query.");
119
+ return null;
120
+ }
121
+ /** list 子命令(对齐 osp listSessions 文本格式 + active 标记) */
122
+ function listSessions(root, activeId) {
123
+ const sessions = readAllSessions(sessionsRoot(root));
124
+ if (sessions.length === 0) return "(no sessions in AGENT_SESSIONS/)";
125
+ const lines = sessions.map((s) => {
126
+ const age = Math.floor((Date.now() - s.mtime.getTime()) / DAY);
127
+ const sessionId = extractSessionId(s.dirName);
128
+ return `${activeId !== void 0 && sessionId !== "" && sessionId === activeId ? "●" : s.status.completed ? "✓" : "○"} ${s.dirName} (${age}d ago)`;
129
+ });
130
+ return `AGENT_SESSIONS/ (${sessions.length} sessions)\n` + lines.join("\n");
131
+ }
132
+ /** show 子命令(对齐 osp showSession:`# dirName\n\n` + SESSION.md 内容) */
133
+ function showSession(root, key) {
134
+ const session = findSession(sessionsRoot(root), key);
135
+ if (!session) throw new Error(`Session not found: "${key}". Use "list" to see available sessions.`);
136
+ const mdPath = join(session.path, SESSION_MD);
137
+ if (!existsSync(mdPath)) return `Session ${session.dirName} (no SESSION.md — directory exists but is empty)`;
138
+ const content = readFileSync(mdPath, "utf-8");
139
+ return `# ${session.dirName}\n\n${content}`;
140
+ }
141
+ /** 生成 SESSION.md 模板(对齐 osp:goal 写入目标段,时间戳 YYYY-MM-DD HH:mm) */
142
+ function sessionMdTemplate(title, id, goal, now) {
143
+ const ts = now.toISOString().slice(0, 16).replace("T", " ");
144
+ return `# SESSION: ${title}\n- ID: ${id}\n\n## 目标\n${goal ?? "(待补充)"}\n\n## 状态\n- [ ] 进行中\n\n## 关键决策\n| # | 决策 | 理由 |\n|---|------|------|\n| 1 | | |\n\n## 进度记录\n- ${ts} — 创建\n\n## 产出物\n- \n\n## 未解决的问题\n- \n`;
145
+ }
146
+ /** create 子命令(对齐 osp createSession:--desc/--issue 二选一 + dry-run + 长度限制) */
147
+ /** 目录名脱敏(Windows 审计问题 10):非法字符 → '-', 去尾点/空格, 保留名(CON/NUL 等)加前缀 */
148
+ function sanitizeDirName(s) {
149
+ const cleaned = s.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "");
150
+ if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(cleaned)) return `_${cleaned}`;
151
+ return cleaned;
152
+ }
153
+ function createSession(opts) {
154
+ const { root, desc, issue, goal, dryRun } = opts;
155
+ const sessionsDir = sessionsRoot(root);
156
+ const now = /* @__PURE__ */ new Date();
157
+ const datePrefix = now.toISOString().slice(0, 10);
158
+ if (!desc && !issue) throw new Error("create requires either --desc or --issue");
159
+ if (desc && issue) throw new Error("--desc and --issue are mutually exclusive");
160
+ if (issue) {
161
+ if (issue.length > 100) throw new Error(`issue too long: ${issue.length} chars (max 100)`);
162
+ const dirName = `${datePrefix}--${sanitizeDirName(issue)}`;
163
+ const sessionPath = join(sessionsDir, dirName);
164
+ if (!dryRun && existsSync(sessionPath)) throw new Error(`Session directory already exists: "${dirName}"`);
165
+ if (dryRun) return {
166
+ message: `[dry-run] Would create: ${dirName}/`,
167
+ dirName,
168
+ sessionPath,
169
+ sessionId: issue
170
+ };
171
+ mkdirSync(sessionPath, { recursive: true });
172
+ writeFileSync(join(sessionPath, SESSION_MD), sessionMdTemplate(issue, issue, goal, now), "utf-8");
173
+ return {
174
+ message: `Created: ${dirName}/`,
175
+ dirName,
176
+ sessionPath,
177
+ sessionId: issue
178
+ };
179
+ }
180
+ if (!desc || desc.length === 0) throw new Error("description cannot be empty");
181
+ if (desc.length > 200) throw new Error(`description too long: ${desc.length} chars (max 200)`);
182
+ const sessions = readAllSessions(sessionsDir);
183
+ let maxId = 0;
184
+ for (const s of sessions) {
185
+ const m = s.dirName.match(/--S(\d{3,})--/);
186
+ if (m) {
187
+ const num = parseInt(m[1], 10);
188
+ if (num > maxId) maxId = num;
189
+ }
190
+ }
191
+ const nextId = String(maxId + 1).padStart(3, "0");
192
+ const dirName = `${datePrefix}--S${nextId}--${sanitizeDirName(desc)}`;
193
+ const sessionPath = join(sessionsDir, dirName);
194
+ if (!dryRun && existsSync(sessionPath)) throw new Error(`Session directory already exists: "${dirName}"`);
195
+ if (dryRun) return {
196
+ message: `[dry-run] Would create: ${dirName}/\n goal=${goal ?? "(none)"}`,
197
+ dirName,
198
+ sessionPath,
199
+ sessionId: `S${nextId}`
200
+ };
201
+ mkdirSync(sessionPath, { recursive: true });
202
+ writeFileSync(join(sessionPath, SESSION_MD), sessionMdTemplate(desc, `S${nextId}`, goal, now), "utf-8");
203
+ return {
204
+ message: `Created: ${dirName}/ (S${nextId})`,
205
+ dirName,
206
+ sessionPath,
207
+ sessionId: `S${nextId}`
208
+ };
209
+ }
210
+ /**
211
+ * 活动会话跟踪(S134 v1.16.14 内存化,对齐 osp active-state):
212
+ * **不落盘**——活跃会话状态在内存 Map(key = scope = dsh 会话 id),避免落盘标记
213
+ * 文件累积与跨会话串台(落盘版 `.dsh/active-sessions/<scope>` 已被此方案取代)。
214
+ * 进程重启恢复:从**当前会话历史(events)**解析 `[SESSION CONTEXT]` 标记(use 时注入),
215
+ * 只扫自己会话——无全局扫描、无跨会话污染。
216
+ */
217
+ const DEFAULT_SESSION_SCOPE = "default";
218
+ /** [SESSION CONTEXT] 恢复标记(use 时注入 events 历史;进程重启后从 events 解析) */
219
+ const SESSION_CONTEXT_MARKER = "[SESSION CONTEXT] Activated:";
220
+ /** 内存活跃会话:scope(dsh 会话 id)→ 会话信息(不落盘;并行多会话各自 key 隔离) */
221
+ const activeStore = /* @__PURE__ */ new Map();
222
+ /** 全局最近活跃(对齐 osp lastActive;供无 scope 上下文使用) */
223
+ let lastActive = null;
224
+ function getActiveSessionInfo(scope) {
225
+ return activeStore.get(scope) ?? null;
226
+ }
227
+ function getLastActiveSessionInfo() {
228
+ return lastActive;
229
+ }
230
+ function setActiveSessionInfo(scope, info) {
231
+ activeStore.set(scope, info);
232
+ lastActive = info;
233
+ }
234
+ function clearActiveSessionInfo(scope) {
235
+ activeStore.delete(scope);
236
+ if (lastActive && ![...activeStore.values()].some((v) => v === lastActive)) lastActive = null;
237
+ }
238
+ /** 当前 scope 的活跃会话 SESSION.md 绝对路径;无激活返回 null(读内存,不落盘) */
239
+ function readActiveSessionMd(_root, scope = DEFAULT_SESSION_SCOPE) {
240
+ return getActiveSessionInfo(scope)?.mdPath ?? null;
241
+ }
242
+ /**
243
+ * use 子命令:激活会话(写内存 Map)+ 返回对齐 osp 的输出文本
244
+ * (含 [SESSION CONTEXT] 标记 + todowrite 指令;标记随工具结果进 events 历史,
245
+ * 进程重启后从当前会话 events 解析恢复)。
246
+ */
247
+ function useSession(root, key, scope = DEFAULT_SESSION_SCOPE) {
248
+ const session = findSession(sessionsRoot(root), key);
249
+ if (!session) throw new Error(`Session not found: "${key}". Use "list" to see available sessions.`);
250
+ const mdPath = join(session.path, SESSION_MD);
251
+ if (!existsSync(mdPath)) throw new Error(`Session "${session.dirName}" has no SESSION.md — nothing to load.`);
252
+ const sessionId = extractSessionId(session.dirName) || basename(session.dirName);
253
+ const dirName = session.dirName;
254
+ const shortName = dirName.replace(/^\d{4}-\d{2}-\d{2}--/, "");
255
+ setActiveSessionInfo(scope, {
256
+ sessionId,
257
+ dirName,
258
+ mdPath
259
+ });
260
+ return {
261
+ dir: dirName,
262
+ mdPath,
263
+ context: [
264
+ `───────────────────────────────────────────────────────────────`,
265
+ `${SESSION_CONTEXT_MARKER} ${dirName}`,
266
+ `───────────────────────────────────────────────────────────────`,
267
+ `Use "session show ${sessionId}" to view session details.`,
268
+ `SESSION.md path: ${mdPath}`,
269
+ ``,
270
+ `→ All subsequent work should refer back to this session.`,
271
+ ` Use "session show ${sessionId}" to check current progress.`,
272
+ ` After advancing work, update the "进度记录" (progress) section in SESSION.md.`,
273
+ ``,
274
+ `→ BEFORE responding to the user, you MUST call todowrite immediately`,
275
+ ` with the session todo list. The first item MUST be:`,
276
+ ` content: "SESSION: ${sessionId} — ${shortName}"`,
277
+ ` status: "completed", priority: "low"`,
278
+ ` Follow with any tasks parsed from SESSION.md.`,
279
+ `───────────────────────────────────────────────────────────────`
280
+ ].join("\n")
281
+ };
282
+ }
283
+ /**
284
+ * 从会话历史(events)解析会话身份(进程重启恢复;只扫**当前会话**自己的历史——无跨会话串台)。
285
+ *
286
+ * v1.24.11 稳固化(S142 用户需求:重建后新会话必须准确知道从哪个 SESSION 恢复):
287
+ * **路径规范行即可,不再要求 [SESSION CONTEXT] 标记**——`session use` 上下文与重建锚点
288
+ * (buildRebuildAnchor 的 `- Persistent trajectory — SESSION.md path: <rel>` 行)同格式,
289
+ * 因此**仅靠重建锚点的会话(从未显式 use)同样可恢复**。从**尾到头**扫描,最后一条
290
+ * 合法路径胜出(时间序最新);会话目录名/ID 从路径本身派生(单一真相源,不猜)。
291
+ * 路径可为相对(重建锚点存 rel)——绝对化与存在性校验在调用方(知道 root)执行。
292
+ */
293
+ function parseSessionContextFromEvents(events) {
294
+ for (let i = events.length - 1; i >= 0; i--) {
295
+ const strs = [];
296
+ collectStrings(events[i], strs);
297
+ for (const s of strs) {
298
+ const md = extractSessionMdPathFromText(s);
299
+ if (!md) continue;
300
+ let dirName;
301
+ let filePath;
302
+ if (basename(md) === SESSION_MD) {
303
+ dirName = basename(dirname(md));
304
+ filePath = md;
305
+ } else if (isSessionDirName(basename(md))) {
306
+ dirName = basename(md);
307
+ filePath = join(md, SESSION_MD);
308
+ } else continue;
309
+ if (!isSessionDirName(dirName)) continue;
310
+ const idMatch = dirName.match(/--S(\d{3,})--/);
311
+ return {
312
+ sessionId: idMatch ? `S${idMatch[1]}` : dirName,
313
+ dirName,
314
+ mdPath: filePath
315
+ };
316
+ }
317
+ }
318
+ return null;
319
+ }
320
+ /** 递归收集对象/数组/字符串中的全部字符串(保留原文,无 JSON 转义) */
321
+ function collectStrings(v, out) {
322
+ if (typeof v === "string") {
323
+ out.push(v);
324
+ return;
325
+ }
326
+ if (v && typeof v === "object") for (const val of Object.values(v)) collectStrings(val, out);
327
+ }
328
+ /** 规范行正则:`SESSION.md path: <路径>`(路径可含空格 → [^\r\n]+ 整行匹配) */
329
+ const SESSION_MD_PATH_RE = /SESSION\.md path:\s*([^\r\n]+)/;
330
+ /** 从文本提取 SESSION.md 路径(use 上下文 / 重建锚点规范行通用;无匹配返回 null)。
331
+ * 同行已知尾注(如系统提示词 Session 块的 persistent-body 注释)剥除——规范行只取路径本体。 */
332
+ function extractSessionMdPathFromText(text) {
333
+ const m = text.match(SESSION_MD_PATH_RE);
334
+ if (!m) return null;
335
+ let p = m[1].trim();
336
+ const suffix = p.search(/ \(the trajectory's persistent body/);
337
+ if (suffix > 0) p = p.slice(0, suffix).trim();
338
+ return p;
339
+ }
340
+ /** 会话目录名形态校验(createSession 恒带日期前缀 `YYYY-MM-DD--`) */
341
+ function isSessionDirName(dirName) {
342
+ return /^\d{4}-\d{2}-\d{2}--/.test(dirName) && dirName.length > 11;
343
+ }
344
+ /**
345
+ * 从 dsh 会话标题解析 SESSION 目录(编码无关 best-match,U3/U4)——
346
+ * 标题不假设 S### 前缀(CCC 可自定义编码:apaas-xxx / P### / 完整目录名等)。
347
+ * 匹配优先级(全部对 AGENT_SESSIONS 现有目录 best-match,不猜):
348
+ * ① 标题即完整目录名(含日期前缀 `YYYY-MM-DD--`)→ 精确命中
349
+ * ② 标题含 `--<code>--` 段(如完整目录名被截断为 `<code>-日期-概括` 前段)→
350
+ * 按 code 段匹配:取标题首 token(`-` 前),与各目录 `--<code>--`/`--<code>` 尾段比对
351
+ * ③ 唯一模糊子串匹配(多个则返回 null 防误猜)
352
+ * @param title dsh 会话标题(如 `S142-2026-08-24-概括` / `apaas-26116-…` / 完整目录名)
353
+ * @param sessionsDir AGENT_SESSIONS 绝对路径
354
+ * @returns 命中目录的绝对路径(SESSION.md);无/歧义返回 null
355
+ */
356
+ function resolveSessionByTitle(title, sessionsDir) {
357
+ const t = (title ?? "").trim();
358
+ if (!t) return null;
359
+ const all = readAllSessions(sessionsDir);
360
+ if (all.length === 0) return null;
361
+ if (isSessionDirName(t)) {
362
+ const exact = all.find((s) => s.dirName === t);
363
+ if (exact) return join(exact.path, SESSION_MD);
364
+ }
365
+ const codeToken = t.split("-")[0]?.trim() ?? "";
366
+ if (codeToken) {
367
+ const byCode = all.filter((s) => {
368
+ const m = s.dirName.match(/--([^--]+)--/);
369
+ const code = m ? m[1] : null;
370
+ const tailMatch = s.dirName.match(/--([^--]+)$/);
371
+ const tailCode = tailMatch && !s.dirName.includes("--", s.dirName.lastIndexOf("--") + 3) ? tailMatch[1] : null;
372
+ return code === codeToken || tailCode === codeToken || s.dirName === codeToken || s.dirName.includes(`--${codeToken}`);
373
+ });
374
+ if (byCode.length === 1) return join(byCode[0].path, SESSION_MD);
375
+ if (byCode.length > 1) return null;
376
+ }
377
+ const fuzzy = all.filter((s) => s.dirName.toLowerCase().includes(t.toLowerCase()));
378
+ if (fuzzy.length === 1) return join(fuzzy[0].path, SESSION_MD);
379
+ return null;
380
+ }
381
+ /** summary 仪表盘(供 `list` 动作拼接;v1.33 起独立 `summary` 动作已撤,并入 list) */
382
+ function summarize(root) {
383
+ const sessions = readAllSessions(sessionsRoot(root));
384
+ if (sessions.length === 0) return "AGENT_SESSIONS/ is empty.";
385
+ const now = Date.now();
386
+ const completed = sessions.filter((s) => s.status.completed).length;
387
+ const active = sessions.length - completed;
388
+ const stale = sessions.filter((s) => !s.status.completed && (now - s.mtime.getTime()) / DAY > HEALTH_STALE_DAYS).length;
389
+ const ghost = sessions.filter((s) => !s.status.hasSessionMd).length;
390
+ const recent = sessions.slice(0, 5);
391
+ const lines = [
392
+ `AGENT_SESSIONS Summary`,
393
+ `────────────────────────`,
394
+ `Total: ${sessions.length}`,
395
+ `Active: ${active}`,
396
+ `Completed: ${completed}`,
397
+ `Stale: ${stale}`,
398
+ `Ghost: ${ghost}`,
399
+ ``,
400
+ `Recent activity (top 5):`,
401
+ ...recent.map((s) => {
402
+ const age = Math.floor((now - s.mtime.getTime()) / DAY);
403
+ return ` ${s.status.completed ? "✓" : "○"} ${s.dirName} (${age}d ago)`;
404
+ })
405
+ ];
406
+ if (stale > 0) lines.push("", "⚠ Warning: stale trajectories present (no activity > 7d) — see the list above for detail.");
407
+ return lines.join("\n");
408
+ }
409
+ //#endregion
410
+ export { summarize as _, extractSessionMdPathFromText as a, getLastActiveSessionInfo as c, readActiveSessionMd as d, resolveSessionByTitle as f, showSession as g, setActiveSessionInfo as h, createSession as i, listSessions as l, sessionsRoot as m, TRAJECTORY_ACTIONS as n, findSession as o, sessionEvents as p, clearActiveSessionInfo as r, getActiveSessionInfo as s, DEFAULT_SESSION_SCOPE as t, parseSessionContextFromEvents as u, useSession as v };
@@ -1,17 +1,28 @@
1
1
  /**
2
- * session-ops.ts — session 工具纯操作层(零 DSH 依赖,可独立单测)
2
+ * trajectory-ops.ts — 轨迹工具纯操作层(零 DSH 依赖,可独立单测)
3
3
  *
4
- * 行为对齐 osp(opencode-serenity-plugin/src/session/lib.ts)——osp 是 ACC 工具 spec:
5
- * - create:--desc / --issue 二选一(互斥、缺省报错);issue 模式目录 YYYY-MM-DD--<issue>
6
- * (无 S###,sessionId=issue);desc 模式 YYYY-MM-DD--S###--<desc>;goal 写入目标段;dry-run 预览
7
- * - close:需 name + confirm=true;标记 [x] 已完成+已关闭 + 进度记录"关闭"
8
- * - archive:name 缺省 → 批量归档(completed + ≥7 天 → 移动 _archived/);单会话需 completed + grace
9
- * - list/show/health/qa/summary:文本输出格式与 osp 一致
10
- * 保留 dsp S134 活跃会话机制(内存 Map + events 恢复,不落盘)——osp 同为内存 active-state。
4
+ * 行为对齐 osp(opencode-serenity-plugin/src/session/lib.ts)——osp 是 ACC 工具 spec。
5
+ *
6
+ * v1.33 收敛(S142 §32):本文件只服务 `container_trajectory` 的 6 个动作
7
+ * (list / show / create / use / rebuild / wake-later)。旧 `logbook` 面里
8
+ * **close / archive / health / qa 四个动作已删**(用户裁决「功能上废弃,实际上没用」),
9
+ * 其函数(closeSession / archiveSessions / healthCheck / qaCheck)**同批删除**——
10
+ * 保留下来的只有:create(--desc / --issue 二选一、dry-run 预览)、
11
+ * list(含 summarize 仪表盘)、show、use(激活 + 绑定)、以及供 list 用的 summarize。
12
+ * 「完成」不再由 close 写入,而是从 SESSION.md 的 `[x]` 推导;归档走 `container_fs mv`。
13
+ */
14
+ /**
15
+ * `container_trajectory` 工具的动作集合(v1.33,S142 §32 用户裁决后的收敛结果)。
16
+ *
17
+ * 为什么只有 6 个(R↓):旧面是 `logbook`(11)+ `trajectory`(12)两个工具,用户裁决
18
+ * 「都合并成 trajectory,废除 logbook 这个词」并逐条删并 ——
19
+ * summary 并进 list|health/qa 淘汰(判据=旧模板,EAP 已取代;仅"空壳/长期无活动"并入 use)|
20
+ * close/archive 删(completed 由 SESSION.md 的 `[x]` 推导;归档走 container_fs mv)|
21
+ * hook-develop-guide 并进 container_admin msm guide|autopilot 面归 container_admin 的 autopilot 域|
22
+ * wake-add/list/rm 收敛为 wake-later。
11
23
  */
12
- import type { JsonValue } from './json.js';
13
- export type SessionAction = 'list' | 'show' | 'create' | 'use' | 'close' | 'health' | 'qa' | 'archive' | 'summary' | 'hook-develop-guide' | 'rebuild';
14
- export declare const SESSION_ACTIONS: readonly SessionAction[];
24
+ type TrajectoryAction = 'list' | 'show' | 'create' | 'use' | 'rebuild' | 'wake-later';
25
+ export declare const TRAJECTORY_ACTIONS: readonly TrajectoryAction[];
15
26
  /**
16
27
  * 读取 Session 事件序列(v1.28.1 适配 0.1.2-rc.1 补齐):rc.1 起官方 Session 类
17
28
  * 移除 `.events` 属性 → `snapshotEvents()` 方法(dsh-session/src/session.ts:
@@ -26,12 +37,10 @@ export declare function sessionEvents<T = unknown>(session: unknown): readonly T
26
37
  export declare function sessionsRoot(root: string): string;
27
38
  interface SessionStatus {
28
39
  hasSessionMd: boolean;
40
+ /** 「完成」的判据 = SESSION.md 里出现 `[x]`(v1.33 起不再由 close 动作写入) */
29
41
  completed: boolean;
30
- completedCount: number;
31
- pendingCount: number;
32
- unresolvedCount: number;
33
42
  }
34
- export interface SessionEntry {
43
+ interface SessionEntry {
35
44
  dirName: string;
36
45
  path: string;
37
46
  mtime: Date;
@@ -46,7 +55,7 @@ export declare function findSession(sessionsDir: string, key: string): SessionEn
46
55
  export declare function listSessions(root: string, activeId?: string): string;
47
56
  /** show 子命令(对齐 osp showSession:`# dirName\n\n` + SESSION.md 内容) */
48
57
  export declare function showSession(root: string, key: string): string;
49
- export interface CreateSessionOptions {
58
+ interface CreateSessionOptions {
50
59
  root: string;
51
60
  /** --desc 模式的描述(与 issue 互斥) */
52
61
  desc?: string;
@@ -61,9 +70,6 @@ export interface CreateSessionResult {
61
70
  sessionPath: string;
62
71
  sessionId: string;
63
72
  }
64
- /** create 子命令(对齐 osp createSession:--desc/--issue 二选一 + dry-run + 长度限制) */
65
- /** 目录名脱敏(Windows 审计问题 10):非法字符 → '-', 去尾点/空格, 保留名(CON/NUL 等)加前缀 */
66
- export declare function sanitizeDirName(s: string): string;
67
73
  export declare function createSession(opts: CreateSessionOptions): CreateSessionResult;
68
74
  /**
69
75
  * 活动会话跟踪(S134 v1.16.14 内存化,对齐 osp active-state):
@@ -98,11 +104,6 @@ export declare function useSession(root: string, key: string, scope?: string): {
98
104
  mdPath: string;
99
105
  context: string;
100
106
  };
101
- /**
102
- * close 子命令(对齐 osp closeSession):需 name + confirm=true;
103
- * 标记 SESSION.md 为 [x] 已完成 + [x] 已关闭 + 进度记录"关闭";清除该会话的活跃状态。
104
- */
105
- export declare function closeSession(root: string, key: string, confirm: boolean, scope?: string): string;
106
107
  /**
107
108
  * 从会话历史(events)解析会话身份(进程重启恢复;只扫**当前会话**自己的历史——无跨会话串台)。
108
109
  *
@@ -136,16 +137,6 @@ export declare function resolveSessionByTitle(title: string, sessionsDir: string
136
137
  * 只作最后手段(内存/events/锚点全缺时),保证重建锚点至少指向一个真实存在的轨迹。
137
138
  */
138
139
  export declare function findLatestActiveSessionMd(root: string): string | null;
139
- /** health 子命令(对齐 osp healthCheck:stale/stalled/ghost/drift 四类检查,文本输出) */
140
- export declare function healthCheck(root: string): string;
141
- /** archive 子命令(对齐 osp archiveSessions:移动 _archived/;name 缺省批量) */
142
- export declare function archiveSessions(root: string, opts: {
143
- name?: string;
144
- dryRun: boolean;
145
- }): string;
146
- /** summary 子命令(对齐 osp sessionSummary 文本仪表盘) */
140
+ /** summary 仪表盘(供 `list` 动作拼接;v1.33 起独立 `summary` 动作已撤,并入 list) */
147
141
  export declare function summarize(root: string): string;
148
- /** 事实核对:SESSION.md 声明 vs 实际情况(结构/一致性/新鲜度/决策质量/产出物) */
149
- export declare function qaCheck(root: string, key: string): string;
150
- export type SessionInfo = SessionEntry;
151
- export type { JsonValue };
142
+ export {};
@@ -0,0 +1,71 @@
1
+ /**
2
+ * trajectory-skills.ts — 轨迹 skill 声明解析 + 注入内容装配(纯逻辑,零 DSH 依赖)
3
+ *
4
+ * 接口真相源:`docs/trajectory-skill-injection.md` v1.0(§2 数据契约 / §3 注入契约)。
5
+ * 一句话:**轨迹自己声明它要挂哪些 skill**(`SESSION.md` 顶部 YAML frontmatter 的 `skills:`),
6
+ * ACC 在**绑定期间**把这些 skill 的 `SKILL.md` 全文注入系统提示词。
7
+ *
8
+ * 分工(R↓:为什么解析与注册分开):
9
+ * · 本模块 = **纯文本层**(解析 / 装配 / 安全判定)——可脱离 DSH 单测;
10
+ * · `seams/system-prompt.ts` 的 `registerTrajectorySkillSection` = **宿主接入层**
11
+ * (per-agent `ctx.systemPrompt.section`),只做门与注册,不再解析文本。
12
+ *
13
+ * 边界(规格 §2.2):**不解析** `skills` 以外任何键的语义(CCC 可自由使用,ACC 视而不见);
14
+ * **不改写** frontmatter(ACC 只读)。
15
+ */
16
+ /** 全文注入总长上限(字符数;规格 §3「缺省 32 KB」——防上下文爆炸) */
17
+ export declare const TRAJECTORY_SKILLS_MAX_CHARS: number;
18
+ /** 缺失标记(规格 §3/§6-3 的验收锚点:任何"拿不到全文"的情况都必须带它,绝不静默) */
19
+ export declare const SKILL_MISSING_MARK = "[\u7F3A\u5931]";
20
+ /**
21
+ * 抽取文件顶部 YAML frontmatter 区(规格 §2.1)。
22
+ *
23
+ * 规则:**第一行必须是 `---`**;到下一个 `---` 行为止是 frontmatter 区。
24
+ * 首行不是 `---`,或没有闭合 `---` ⇒ 视为**无 frontmatter**(返回 null)。
25
+ * 边界判断(R↓):未闭合块按"无 frontmatter"处理而非吞掉整个文件——半个声明块是格式错误,
26
+ * 猜它的意图等于把坏数据当好消息(宁可零注入,也不注入来路不明的半截列表)。
27
+ *
28
+ * @param content SESSION.md 全文
29
+ * @returns frontmatter 区各行(不含两条 `---` 定界行);无 → null
30
+ */
31
+ export declare function extractFrontmatter(content: string): string[] | null;
32
+ /**
33
+ * 解析声明(规格 §2.1,**最小实现,不引 YAML 依赖**)。
34
+ *
35
+ * 只认 `skills:` 键;未知键一律忽略(向前兼容,不报错)。两种值形态都支持:
36
+ * ① 行内数组 `skills: [home-rhetoric, acc-eap]`
37
+ * ② 缩进列表(`skills:` 后各起一行 ` - a`)
38
+ * 名字:去首尾空白、空项丢弃、**保持书写顺序**(注入顺序 = 书写顺序)、同名去重(保留首次位置)。
39
+ *
40
+ * 未定义形态(不在规格的两种值形态内,如 `skills: a` 裸标量)⇒ **视为无声明**:
41
+ * 本函数只认契约里写明的两种形态,猜第三种等于自造契约(规格 §2.1 只列了 ①②)。
42
+ *
43
+ * @param sessionMd SESSION.md 全文
44
+ * @returns 声明的 skill 名(按书写顺序去重);无 frontmatter / 无 `skills` 键 / 值为空 → `[]`
45
+ */
46
+ export declare function parseDeclaredSkills(sessionMd: string): string[];
47
+ /**
48
+ * 注册门(规格 §3「无绑定 / 无声明 ⇒ 完全不注册该 section」)。
49
+ *
50
+ * 返回 true 的两种情况:
51
+ * · 文件在且**有非空声明** → 注册(有内容可注入)
52
+ * · 文件**不在 / 读不到** → 同样注册——此时 section 文本是响亮的 `[缺失]` 提示
53
+ * (规格 §6-6 要求"不静默";不注册就等于静默)
54
+ * 其余(文件在但无 frontmatter / 无 `skills` 键 / 值为空)⇒ false,不注册、不产生噪声。
55
+ */
56
+ export declare function hasTrajectorySkillsDeclaration(root: string, mdPath: string): boolean;
57
+ /**
58
+ * 装配注入正文(规格 §3):按声明顺序拼接各 skill 的 `SKILL.md` 全文,每段带来源抬头
59
+ * `=== skill: <name> ===`;拿不到全文的一律以 `[缺失]` 响亮提示占位(**不静默**)。
60
+ *
61
+ * 安全(规格 §3 🔒):名字来自 CCC 数据(frontmatter)⇒ **先过 `isSafeSkillName`**;
62
+ * 不安全 ⇒ 按缺失处理,且不调用 `findSkillMd` ⇒ **不发生任何以该名拼出的 fs 访问**。
63
+ *
64
+ * 长度守卫:复用已导出的 `truncateContent`,超限截断并在末尾写明 `truncated`。
65
+ *
66
+ * @param root CCC 根
67
+ * @param mdPath 轨迹的 SESSION.md 路径(**来自绑定的 `mdPath`**,不用 label 拼)
68
+ * @param maxChars 总长上限(字符)
69
+ * @returns 注入正文;无声明 → `''`(空串 = 宿主不产出该 section 块);SESSION.md 缺失 → 响亮提示
70
+ */
71
+ export declare function buildTrajectorySkillsSection(root: string, mdPath: string, maxChars?: number): string;