@shgroup/dsh-serenity-hooks 1.98.0 → 1.100.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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.98.0",
3
+ "version": "1.100.0",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
package/lib/index.js CHANGED
@@ -11429,43 +11429,18 @@ function createCognitiveTrajectoryTool(ctx) {
11429
11429
  return defineTool({
11430
11430
  name: "cognitive_trajectory",
11431
11431
  description: [
11432
- "A COGNITIVE TRAJECTORY is a durable body (SESSION.md) that outlives every conversation carrying it.",
11433
- "AXIOM 1 — THE TRAJECTORY IS THE SUBJECT; THE CONVERSATION IS ITS CARRIER. Rebuilding the carrier replaces the conversation and leaves SESSION.md byte-identical.",
11434
- "AXIOM 2 — A TRAJECTORY HAS NO LIFECYCLE. It never completes, closes, or expires, and age never counts against it. The only thing that changes is WHERE it is stored: archive moves it out of the active list without touching its directory.",
11435
- "AXIOM 3 — IDENTITY IS THE CODE: the fixed-position segment of the directory name `YYYY-MM-DD--<code>[--<desc>]`. The code (not the summary, not the title) is the unique identifier, and create --id lets you choose it.",
11436
- "DOES NOT: this tool never deletes a trajectory, never edits SESSION.md content, and never schedules recurring work (that is cro-guide).",
11437
- "ENTITIES. TRAJECTORY = a directory under AGENT_SESSIONS/ holding SESSION.md. CARRIER = a dsh session bound to it. BINDING DECLARATION = the file `<trajectory>/current.dsh_session_bind` — ONE FILE PER TRAJECTORY, a single slot, never a table.",
11438
- "IMMUTABLE 1 — ONE SESSION BELONGS TO AT MOST ONE TRAJECTORY, FOR LIFE. use refuses to move a session that already carries a different trajectory. There is NO --force override (retired 2026-10-20; passing it returns a refusal explaining why). To give another trajectory a carrier, open a new session.",
11439
- "IMMUTABLE 2 — ONE TRAJECTORY HAS AT MOST ONE CURRENT CARRIER, and THE BINDING DECLARATION IS THE ONLY TRUTH. If several dsh sessions happen to be open, only the one named in the declaration carries the trajectory; the others are simply unbound. Never infer a binding from \"a session looks active\".",
11440
- "IMMUTABLE 3 — A DECLARATION WHOSE SESSION NO LONGER EXISTS COUNTS AS UNBOUND. No cleanup is ever required, and a stale declaration is deliberately left in place: it records who carried this trajectory last.",
11441
- "ACTIONS, GROUPED BY EFFECT — read the grouping before choosing, because it answers \"does this change what I am bound to?\":",
11442
- "(A) LIFECYCLE — changes which cognitive trajectory THIS conversation is bound to:",
11443
- "list (inventory + stats) / show (read one SESSION.md) / create (make a NEW trajectory) / use (bind THIS conversation) / rebuild (clear-and-rebuild THIS conversation in place) / archive (move a trajectory out of the active list; one-way, never overwrites).",
11444
- "(B) DELIVERY — sends ONE message to ANOTHER trajectory; your own binding is unchanged: send-now (right now) / send-later (one future instant).",
11445
- "(C) REFERENCE — cro-guide: how to give a trajectory its own program that decides when it should be woken.",
11446
- "(D) INSPECTION — interrupted: list dsh sessions whose LAST turn did not finish — reason.kind is \"interrupted\" (process died; the host synthesised the closer on resume) OR \"aborted\" (terminated normally: restart, dispose, user cancel). Both mean \"this turn did not complete\"; picking only one misses an entire class. PURE READ: no periodic scheduling, no auto-continue, no log mutation — deciding IF/WHEN/WHICH to resume belongs to the calling trajectory (this is the \"entry point only\" contract). The criterion is EXACTLY \"the last turn did not finish\", NOT \"was ever interrupted\": one long session may have been interrupted dozens of times historically, and treating those as pending would resurrect sessions that already recovered. Each hit reports the turn number and the tool names that had a call but no result (best-effort — those side effects are UNKNOWN, so re-running them blindly is dangerous).",
11447
- "HOW TO CHOOSE (a rule, not a guess):",
11448
- "not yet bound + starting new work ⇒ create (with no prior binding it activates immediately, so the new trajectory has a carrier at once).",
11449
- "already bound + recording a SEPARATE future line ⇒ create (it does NOT steal your binding; activate the new one later by send-now).",
11450
- "want THIS conversation to move ⇒ use. It refuses if this session already carries another trajectory (IMMUTABLE 1) — that refusal is the design, not an error to work around.",
11451
- "want to make another trajectory act ⇒ send-now. The target needs no prior setup: if it has no live session, one is created and bound automatically, and your message is injected.",
11452
- "RECEIPTS (read no more than is written): send-now returns a synchronous receipt meaning exactly \"injected / turn started\" — NEVER \"the target ran or replied\". To prove the target actually moved, read its SESSION.md.",
11453
- "send-now never queues behind the current turn: a busy target is steered mid-turn (read in the CURRENT turn), an idle target starts a turn at once.",
11454
- "send-later is fire-and-forget by design: no receipt, no recall, no panel.",
11455
- "ASYMMETRY (deliberate): send-now may create a missing carrier; send-later NEVER does. Scheduling for a trajectory that never existed is almost always a mistyped name, and auto-creating would hide that error.",
11456
- "CARRIER GUARD: send-now auto-creates a carrier ONLY for a trajectory that has never had one. If a declaration exists but its session is gone, it refuses and says so — one trajectory keeps one carrier. Two independent gates can refuse: (a) the declared session cannot be resolved at all; (b) it existed and is now gone.",
11457
- "PARAMETERS: create needs --desc <desc> [--id <code>] [--goal], OR --issue <ticket> (exactly one), PLUS --summary (≤20 chars).",
11458
- "use needs --name + --summary (≤20 chars). --force is ACCEPTED BUT REFUSED (retired): it exists only to explain why.",
11459
- "rebuild needs --summary (next-phase, ≤20 chars) + optional --note (≤200 chars, task focus for the rebuilt self).",
11460
- "send-now needs target + message. send-later needs target + at (RFC3339 with timezone, or +30m / +2h) + message.",
11461
- "target/name accept a code (S### or custom), a full AGENT_SESSIONS directory name, or a fuzzy keyword.",
11462
- "AUTHORIZATION (read before creating anything): create and send-now both cost real work and must serve a human intent. Call create only when the user asked for a new line of work (or confirmed it) — never to \"tidy up\" or pre-provision what nobody requested. When in doubt, ask instead of creating. This is a behavioral rule, not a mechanical gate: treat it as etiquette with a cost.",
11463
- "uid: READS a legacy TRAJECTORY-UID only. Setting one is retired — a trajectory's code is its identity now; use create --id <code>.",
11464
- "summary is appended to the dsh session title as <code>-<date>-<summary>; the code and date stay server-derived unless you pass --id.",
11465
- "SELF-DECLARATION MANUAL — how to write your own charter (PROMPT.md), declare yourself resident",
11466
- "(trajectory.json `resident.serves`), or restrict yourself (tool/model whitelists):",
11467
- "container_admin config docs trajectory-self. Read it before changing any file in your own directory."
11468
- ].join(" "),
11432
+ "A cognitive trajectory is a durable body — its SESSION.md — that outlives every conversation carrying it.",
11433
+ "Three axioms hold. The trajectory is the subject and the conversation is only its carrier, so rebuilding the carrier leaves SESSION.md byte for byte identical. A trajectory has no lifecycle: it never completes, closes, or expires, and its age never counts against it — only where it is stored changes, so archiving removes it from the active list without touching its directory. And identity is the code, the fixed `YYYY-MM-DD--<code>[--<desc>]` segment of the directory name, never the summary or the title; create --id chooses it.",
11434
+ "What this tool never does: it never deletes a trajectory, never edits the content of SESSION.md, and never schedules recurring work, because that belongs to cro-guide.",
11435
+ "A trajectory is a directory under AGENT_SESSIONS/ holding a SESSION.md, a carrier is a dsh session bound to one, and the binding declaration is the file current.dsh_session_bind inside the trajectory: one file per trajectory, a single slot, never a table.",
11436
+ "Three rules are immutable. A session belongs to at most one trajectory for its whole life, so use refuses a session already carrying another, and there is no --force override — that flag was retired on 2026-10-20 and now only explains why; to give another trajectory a carrier, open a new session. A trajectory has at most one current carrier, and the binding declaration is the only truth about which, so other open sessions are simply unbound; never infer a binding from a session looking active. A declaration whose session no longer exists counts as unbound, and no cleanup is required, because a stale declaration deliberately records who carried the trajectory last.",
11437
+ "Actions are grouped by effect, and the grouping answers one question: would this change what I am bound to? The lifecycle group does change this conversation's binding — list, show, create, use, rebuild, and archive, where archiving is one-way, never overwrites, and leaves content untouched. The delivery group sends one message to another trajectory and leaves your own binding alone — send-now and send-later. One action is a reference rather than an operation: cro-guide explains how to give a trajectory its own program deciding when it should be woken. The inspection action is read-only: interrupted returns the dsh sessions whose last turn did not finish. A turn is unfinished when its reason.kind is \"interrupted\", meaning the process died, or \"aborted\", meaning it ended normally through a restart, a dispose, or a cancel; both mean the turn did not complete, so taking only one would miss a whole class. It schedules nothing, continues nothing, and mutates no log — deciding whether and when to resume belongs to the caller. The criterion is exactly \"the last turn did not finish\", never \"was ever interrupted\", since one long session may have been interrupted dozens of times and already recovered. Each hit also names the turn number and the tools that made a call without a result, but those side effects are unknown, so do not re-run them blindly.",
11438
+ "Choose by rule, not by guess. Not yet bound and starting new work: create, which activates itself at once when there is no prior binding. Already bound and recording a separate future line: also create, since that leaves your binding intact and you can activate it later with send-now. Want this conversation to move: use, and treat its refusal — when the session already carries another trajectory — as the design rather than an error to work around. Want another trajectory to act: send-now, whose target needs no prior setup, since a live session is created, bound, and injected when none exists.",
11439
+ "Read a receipt for no more than it says. send-now returns a synchronous receipt meaning the message was injected and the turn started; it never means the target ran or replied, and to prove the target moved you must read its SESSION.md. send-now never queues behind the current turn — a busy target is steered mid-turn while an idle one starts at once. send-later is fire-and-forget by design, with no receipt and no recall.",
11440
+ "The asymmetry is deliberate: send-now may create a missing carrier, send-later never does, because scheduling work for a trajectory that never existed is almost always a mistyped name. A carrier guard applies to send-now: it creates a carrier automatically only for a trajectory that has never had one, and otherwise refuses, either because a declaration exists but its session is gone or because the declared session cannot be resolved at all — one trajectory keeps one carrier.",
11441
+ "For parameters: create needs --desc with optional --id and --goal, or --issue instead, exactly one of those two forms, plus --summary of at most 20 characters. use needs --name and --summary. rebuild needs --summary naming the next phase in at most 20 characters, plus an optional --note of at most 200 characters stating the task focus for the rebuilt self. send-now needs a target and a message; send-later needs a target, an instant as RFC 3339 with a timezone or a relative form like +30m or +2h, and a message. A target or name may be a code such as S### or a custom one, a full AGENT_SESSIONS directory name, or a fuzzy keyword. --force is still accepted but always refused, being retired and existing only to explain why. uid reads a legacy TRAJECTORY-UID only, since setting one is retired and the code is now the identity. summary is appended to the dsh session title alongside the code and date, which stay server-derived unless you pass --id.",
11442
+ "Two rules of conduct, not operations. create and send-now both cost real work and must serve a human intent, so call create only when the user asked for a new line of work or confirmed one — never to tidy up and never to pre-provision what nobody requested; when in doubt, ask. That is etiquette with a cost rather than a mechanical gate. And to describe yourself rather than operate: your own charter in PROMPT.md, a resident declaration through resident.serves in trajectory.json, and self-restriction by tool and model whitelists all live in container_admin config docs trajectory-self — read that chapter before changing any file in your own directory."
11443
+ ].join("\n\n"),
11469
11444
  parameters: {
11470
11445
  action: {
11471
11446
  type: "string",
@@ -12660,8 +12635,6 @@ function createContainerAdminTool(ctx) {
12660
12635
  }
12661
12636
  //#endregion
12662
12637
  //#region src/seams/tool-visibility.ts
12663
- /** ACC 自己注册的工具名集合(退化档只发这些 —— 它们一定在册) */
12664
- const REGISTERED_TOOLS_SET = new Set(REGISTERED_TOOLS);
12665
12638
  /** 收集器:把各来源的 deny 合并成一个判决(**纯函数、无副作用**) */
12666
12639
  function collectVisibilityDecision(sources) {
12667
12640
  const deny = /* @__PURE__ */ new Set();
@@ -12693,12 +12666,13 @@ const applied = /* @__PURE__ */ new Map();
12693
12666
  * (旧集合是新集合的子集时叠不叠都看不出来,但**不是子集时**会残留 —— 必须撤。)
12694
12667
  * ③ 🔴 **未注册的名字不许炸掉整次发射**:宿主契约"unknown names … fail"。
12695
12668
  * 合并后集合里只要有一个不在册的名字,整次 restrict 就失败 ⇒ 六个机制**全都不生效**。
12696
- * ⇒ 策略 = **直接发 → 全量被拒才退到「只发 ACC 自己注册的那些」一档**,退档时
12697
- * 把被放弃的名字如实记账(进 `dropped`,由调用方决定要不要告警)。
12669
+ * ⇒ 策略 = **直接发 → 被拒时从宿主的错误文案里解析出它点名的未知名字 → 只剔那几个、其余重发一次**。
12670
+ * 被剔掉的名字进 `dropped`(调用方决定要不要告警)。
12698
12671
  * ⚠️ **不做 `tools.get` 预探活** —— 那个视图查不出 agent-plane 贡献的工具
12699
12672
  * (`bash` / `schedule_*`),会把**在册的名字误判成未注册**(见下方发射段的实测记录)。
12700
- * ⚠️ 这比旧行为**更保守**:旧行为是"某条 restrict 失败 ⇒ 那一格静默不生效",
12701
- * 新行为是"全量失败 ⇒ 至少 ACC 那一格照常生效" —— **等价或更好**,且总有日志。
12673
+ * 🔴 **也不按"谁注册的"降级** —— `REGISTERED_TOOLS` 只含 ACC 自己的 14 个,
12674
+ * 用它当"在册面"会**整类丢掉宿主工具**(2026-10-24 真机缺陷,见发射段)。
12675
+ * ⇒ 唯一合法的在册判据是**宿主自己的回答**:要么发一次成功,要么它说出哪些名字不认识。
12702
12676
  *
12703
12677
  * @returns `'unchanged'` / `'applied'` / `'failed'`(`failed` 时 `deny` 保持原样)
12704
12678
  */
@@ -12741,33 +12715,72 @@ function applyVisibilityDecision(agent, sessionId, decision) {
12741
12715
  dropped
12742
12716
  };
12743
12717
  } catch (firstError) {
12744
- const accOnly = names.filter((n) => REGISTERED_TOOLS_SET.has(n));
12745
- if (accOnly.length === 0 || accOnly.length === names.length) return {
12718
+ const message = errorText(firstError);
12719
+ const unknown = parseUnknownToolNames(message);
12720
+ if (unknown.length === 0) return {
12746
12721
  status: "failed",
12747
12722
  dropped,
12748
- error: firstError.message
12723
+ error: message
12724
+ };
12725
+ const unknownSet = new Set(unknown);
12726
+ const remaining = names.filter((n) => !unknownSet.has(n));
12727
+ dropped.push(...names.filter((n) => unknownSet.has(n)));
12728
+ if (remaining.length === 0) return {
12729
+ status: "failed",
12730
+ dropped,
12731
+ error: message
12749
12732
  };
12750
- const abandoned = names.filter((n) => !REGISTERED_TOOLS_SET.has(n));
12751
12733
  try {
12752
- const dispose = tools.restrict({ deny: accOnly });
12734
+ const dispose = tools.restrict({ deny: remaining });
12753
12735
  applied.set(sessionId, {
12754
- signature,
12755
- deny: accOnly,
12736
+ signature: decisionSignature({
12737
+ ...decision,
12738
+ deny: remaining
12739
+ }),
12740
+ deny: remaining,
12756
12741
  dispose
12757
12742
  });
12758
12743
  return {
12759
12744
  status: "applied",
12760
- dropped: abandoned
12745
+ dropped
12761
12746
  };
12762
12747
  } catch (secondError) {
12763
12748
  return {
12764
12749
  status: "failed",
12765
12750
  dropped,
12766
- error: secondError.message
12751
+ error: errorText(secondError)
12767
12752
  };
12768
12753
  }
12769
12754
  }
12770
12755
  }
12756
+ /** 取异常的可读文案(非 Error 也能取到 message;都没有则 String()) */
12757
+ function errorText(e) {
12758
+ if (e instanceof Error) return e.message;
12759
+ if (typeof e === "object" && e !== null && "message" in e && typeof e.message === "string") return e.message;
12760
+ return String(e);
12761
+ }
12762
+ /**
12763
+ * 从宿主的 `tools.restrict()` 错误文案里解析出**被点名的未知工具名**。
12764
+ *
12765
+ * 宿主逐字形态(`dsh-tools/lib/types/index.js` 的 `restrict()`):
12766
+ * ```
12767
+ * tools.restrict() names unknown global tool(s) "a", "b"; known global tools: x, y, z
12768
+ * ```
12769
+ * 单数时是 `tool`,复数时是 `tool(s)`——两种都匹配。
12770
+ *
12771
+ * 🔴 **只取分号之前那一段**:分号后是 `known global tools:` 清单,**不能**误当成未知名字。
12772
+ * 🔴 解析不出来就返回 `[]`(调用方据此走"如实失败、不记账"),**绝不猜**。
12773
+ */
12774
+ function parseUnknownToolNames(message) {
12775
+ const head = message.split(";")[0] ?? "";
12776
+ if (!/names unknown global tool/.test(head)) return [];
12777
+ const out = [];
12778
+ for (const m of head.matchAll(/"([^"]+)"/g)) {
12779
+ const name = m[1];
12780
+ if (name !== void 0 && name !== "") out.push(name);
12781
+ }
12782
+ return [...new Set(out)];
12783
+ }
12771
12784
  /** 会话销毁清理(防 per-会话 Map 无界增长;lifecycle 调用) */
12772
12785
  function forgetToolVisibility(sessionId) {
12773
12786
  const existing = applied.get(sessionId);
@@ -51,12 +51,13 @@ export declare function getToolVisibilityDiagnostics(): {
51
51
  * (旧集合是新集合的子集时叠不叠都看不出来,但**不是子集时**会残留 —— 必须撤。)
52
52
  * ③ 🔴 **未注册的名字不许炸掉整次发射**:宿主契约"unknown names … fail"。
53
53
  * 合并后集合里只要有一个不在册的名字,整次 restrict 就失败 ⇒ 六个机制**全都不生效**。
54
- * ⇒ 策略 = **直接发 → 全量被拒才退到「只发 ACC 自己注册的那些」一档**,退档时
55
- * 把被放弃的名字如实记账(进 `dropped`,由调用方决定要不要告警)。
54
+ * ⇒ 策略 = **直接发 → 被拒时从宿主的错误文案里解析出它点名的未知名字 → 只剔那几个、其余重发一次**。
55
+ * 被剔掉的名字进 `dropped`(调用方决定要不要告警)。
56
56
  * ⚠️ **不做 `tools.get` 预探活** —— 那个视图查不出 agent-plane 贡献的工具
57
57
  * (`bash` / `schedule_*`),会把**在册的名字误判成未注册**(见下方发射段的实测记录)。
58
- * ⚠️ 这比旧行为**更保守**:旧行为是"某条 restrict 失败 ⇒ 那一格静默不生效",
59
- * 新行为是"全量失败 ⇒ 至少 ACC 那一格照常生效" —— **等价或更好**,且总有日志。
58
+ * 🔴 **也不按"谁注册的"降级** —— `REGISTERED_TOOLS` 只含 ACC 自己的 14 个,
59
+ * 用它当"在册面"会**整类丢掉宿主工具**(2026-10-24 真机缺陷,见发射段)。
60
+ * ⇒ 唯一合法的在册判据是**宿主自己的回答**:要么发一次成功,要么它说出哪些名字不认识。
60
61
  *
61
62
  * @returns `'unchanged'` / `'applied'` / `'failed'`(`failed` 时 `deny` 保持原样)
62
63
  */
@@ -73,5 +74,18 @@ export declare function applyVisibilityDecision(agent: {
73
74
  dropped: string[];
74
75
  error?: string;
75
76
  };
77
+ /**
78
+ * 从宿主的 `tools.restrict()` 错误文案里解析出**被点名的未知工具名**。
79
+ *
80
+ * 宿主逐字形态(`dsh-tools/lib/types/index.js` 的 `restrict()`):
81
+ * ```
82
+ * tools.restrict() names unknown global tool(s) "a", "b"; known global tools: x, y, z
83
+ * ```
84
+ * 单数时是 `tool`,复数时是 `tool(s)`——两种都匹配。
85
+ *
86
+ * 🔴 **只取分号之前那一段**:分号后是 `known global tools:` 清单,**不能**误当成未知名字。
87
+ * 🔴 解析不出来就返回 `[]`(调用方据此走"如实失败、不记账"),**绝不猜**。
88
+ */
89
+ export declare function parseUnknownToolNames(message: string): string[];
76
90
  /** 会话销毁清理(防 per-会话 Map 无界增长;lifecycle 调用) */
77
91
  export declare function forgetToolVisibility(sessionId: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.98.0",
3
+ "version": "1.100.0",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档)。",
5
5
  "license": "MIT",
6
6
  "repository": {