dsh-immune-system 1.0.0 → 1.0.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.
package/README.md CHANGED
@@ -55,8 +55,8 @@ dsh plugin --profile <profile> add dsh-immune-system
55
55
  **tarball / git**(等价,无需 npm 账号):
56
56
 
57
57
  ```bash
58
- cd dsh-immune-system && npm pack # → dsh-immune-system-0.9.8.tgz
59
- dsh plugin --profile <profile> add ./dsh-immune-system-0.9.8.tgz
58
+ cd dsh-immune-system && npm pack # → dsh-immune-system-<version>.tgz
59
+ dsh plugin --profile <profile> add ./dsh-immune-system-<version>.tgz
60
60
  dsh plugin --profile <profile> add github:meat-daemon-404/dsh-plugin-immune
61
61
  ```
62
62
 
@@ -149,10 +149,13 @@ KPI(含预算)、搜索与清除、页签计数、排序、状态圆点、
149
149
 
150
150
  > 诚实说明:客户端与路由的**契约**有测试(模拟 `__ModuleLoader__` 装载、假 ctx 验证路由、
151
151
  > 跨源拒绝、无 web 服务时不注册、预算透传),布局则用真实 React 18 做过静态渲染自检
152
- > (含深色模式、空态、窄屏)。**0.9.8 之前**它还从未被宿主真正当成浏览器半——根因是
153
- > manifest 漏了 `dsh.client` 声明(实测:`/plugins/dsh-immune-system/api` 返回 200,
154
- > 说明宿主半在跑;而 `/plugins/dsh-immune-system/client.js` 返回 404)。0.9.8 起已补齐,
155
- > 但需要**重启 DSH** 后到 `设置 → 插件` 确认 tab 出现。
152
+ > (含深色模式、空态、窄屏)。
153
+ >
154
+ > **已在真实宿主里验证过**:0.9.8 补齐 `dsh.client` 声明后,`设置 → 插件` 里的 tab
155
+ > 确实出现了(此前 `/plugins/dsh-immune-system/client.js` 返回 404,只有宿主半在跑)。
156
+ > 0.9.12 进一步把它放进 **shadow root**——真实的设置弹窗会用自己的样式表重排裸
157
+ > `table`/`td`/`div`,导致行被压成几像素、下拉框空白;shadow root 双向隔离,
158
+ > 宿主 CSS 进不来、我们的样式也出不去。
156
159
 
157
160
  ### 同步与体检
158
161
 
@@ -346,9 +349,12 @@ node "<plugin>/dist/zcode/src/cli.js" doctor # 安装自检
346
349
 
347
350
  - **模型端点默认拒绝环回**:本地 Ollama 类服务需显式设 `IMMUNE_ALLOW_PRIVATE_ENDPOINT=1`。
348
351
  这是防 SSRF 的代价,不是疏漏。
349
- - **外部 LLM 蒸馏未实测**:规则蒸馏器全流程有测试;接真实模型端点的路径只有 mock 覆盖。
352
+ - **外部 LLM 蒸馏未实测**:规则蒸馏器全流程有测试;接真实模型端点的路径只有 mock 覆盖
353
+ (含批量路径的降级链)。
350
354
  - **真实 agent 端到端未验**:验证期间模型额度返回 402,turn 到不了工具阶段。同一条链路改为
351
355
  向安装副本投喂真实事件载荷验证(含真 Cordis 宿主的 20 条断言)。
356
+ - **`IMMUNE_GUARD=warn` 仅 ZCode 有效**:DSH 的 `PreToolDecision` 没有携带上下文的字段,
357
+ 那边只能"拦或不拦"。
352
358
 
353
359
  ---
354
360
 
@@ -376,7 +382,7 @@ dsh plugin --profile <profile> add dsh-immune-system
376
382
 
377
383
  ```bash
378
384
  cd dsh-immune-system && npm pack
379
- dsh plugin --profile <profile> add ./dsh-immune-system-0.9.8.tgz
385
+ dsh plugin --profile <profile> add ./dsh-immune-system-<version>.tgz
380
386
  dsh plugin --profile <profile> add github:meat-daemon-404/dsh-plugin-immune
381
387
  ```
382
388
 
@@ -13,7 +13,6 @@
13
13
  * - paths are normalised to a project-relative form so a failure in
14
14
  * `./src/a.ts` and a success in `<cwd>/src/a.ts` still match.
15
15
  */
16
- import { basename, isAbsolute, relative, sep } from 'node:path';
17
16
  /** Programs that never change state; their success proves nothing. */
18
17
  const PROBE_PROGRAMS = new Set([
19
18
  'ls',
@@ -58,22 +57,32 @@ function pickString(obj, keys) {
58
57
  /**
59
58
  * Normalise a path to a project-relative, forward-slash form.
60
59
  *
61
- * Absolute paths outside the project are collapsed to their basename so two
62
- * checkouts of the same file still match.
60
+ * Works on both platforms: drive-letter paths are absolute everywhere (the
61
+ * check is explicit, because POSIX `isAbsolute` does not recognise `C:/…`), and
62
+ * a path inside `cwd` is made relative to it so the same file matches across
63
+ * machines and checkouts.
63
64
  */
64
65
  export function normaliseTargetPath(rawPath, cwd) {
65
- const normalised = rawPath.replace(/\\/g, '/').trim();
66
- if (!normalised)
66
+ const p = rawPath.replace(/\\/g, '/').trim();
67
+ if (!p)
67
68
  return '';
68
- if (cwd && isAbsolute(normalised)) {
69
- const rel = relative(cwd, normalised).replace(/\\/g, '/');
70
- if (rel && !rel.startsWith('..'))
71
- return rel;
72
- return basename(normalised);
69
+ if (cwd) {
70
+ const base = cwd.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
71
+ if (base) {
72
+ const lower = p.toLowerCase();
73
+ if (lower === base)
74
+ return '';
75
+ if (lower.startsWith(`${base}/`))
76
+ return p.slice(base.length + 1);
77
+ }
73
78
  }
74
- if (isAbsolute(normalised) || /^[A-Za-z]:\//.test(normalised))
75
- return basename(normalised);
76
- return normalised.replace(/^\.\//, '');
79
+ if (/^[A-Za-z]:[\/]/.test(p) || p.startsWith('/')) {
80
+ // Outside the project: collapse to the basename (matches the original
81
+ // Windows behaviour and keeps signatures comparable across checkouts).
82
+ const parts = p.split('/').filter(Boolean);
83
+ return parts[parts.length - 1] ?? p;
84
+ }
85
+ return p.replace(/^\.\//, '');
77
86
  }
78
87
  /** Split a shell command into `program` and its first non-flag subcommand. */
79
88
  export function parseCommand(command) {
@@ -83,11 +92,13 @@ export function parseCommand(command) {
83
92
  .trim()
84
93
  .split(/\s+/)
85
94
  .filter(Boolean);
86
- // Skip leading env assignments (FOO=bar cmd).
95
+ // Skip leading env assignments (FOO=bar cmd), then strip a leading path from
96
+ // the program token ("C:\tools\npm.cmd test" -> "npm.cmd") on any platform.
87
97
  let index = 0;
88
98
  while (index < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index] ?? ''))
89
99
  index += 1;
90
- const program = basename((tokens[index] ?? '').replace(/^["']|["']$/g, ''));
100
+ const programToken = (tokens[index] ?? '').replace(/^["']|["']$/g, '');
101
+ const program = (programToken.split(/[\\/]/).pop() ?? programToken).toLowerCase();
91
102
  let sub = '';
92
103
  for (let i = index + 1; i < tokens.length; i += 1) {
93
104
  const token = tokens[i] ?? '';
@@ -175,4 +186,3 @@ export function canCloseEpisode(failedSignature, incoming) {
175
186
  }
176
187
  return !failedTarget || failedTarget === '?' || !incomingTarget || incomingTarget === '?';
177
188
  }
178
- export { sep as pathSeparator };
@@ -16,7 +16,7 @@
16
16
  * identifies another clone of the plugin wherever it sits on disk.
17
17
  */
18
18
  import { existsSync, readFileSync, realpathSync } from 'node:fs';
19
- import { dirname, resolve } from 'node:path';
19
+ import { resolve } from 'node:path';
20
20
  import { fileURLToPath } from 'node:url';
21
21
  /** Package names that mean "this is the immune system itself". */
22
22
  const SELF_PACKAGE_NAMES = new Set(['dsh-immune-system', 'dsh-plugin-immune']);
@@ -43,14 +43,37 @@ function safeRealpath(path) {
43
43
  function normalizePath(value) {
44
44
  return value.replace(/\\\\/g, '\\').replace(/\\/g, '/').toLowerCase();
45
45
  }
46
+ /**
47
+ * Parent directory that treats BOTH separators as separators, on every platform.
48
+ *
49
+ * `path.dirname` uses the platform separator: on Linux a Windows path like
50
+ * `D:\work\app\src\main.ts` has no `/`, so dirname returns '.' and an upward
51
+ * walk would scan the current workspace instead of the named path — that exact
52
+ * false positive failed CI (the workspace root has a package.json whose name is
53
+ * this plugin's).
54
+ *
55
+ * A drive root (`D:`, `D:/`) has no parent and returns null: resolving against
56
+ * it would use the process's drive-relative cwd, which is how the CI false
57
+ * positive happened.
58
+ */
59
+ function parentDir(path) {
60
+ const normalised = path.replace(/\\/g, '/');
61
+ const cut = normalised.lastIndexOf('/');
62
+ if (cut <= 0)
63
+ return null;
64
+ const parent = normalised.slice(0, cut);
65
+ if (/^[A-Za-z]:$/.test(parent))
66
+ return null; // drive root
67
+ return parent.length >= 2 ? parent : null;
68
+ }
46
69
  /** Nearest directory at or above `start` that holds a package.json. */
47
70
  function findPackageRoot(start) {
48
71
  let dir = start;
49
72
  for (let i = 0; i < MAX_UPWARD; i += 1) {
50
73
  if (existsSync(resolve(dir, 'package.json')))
51
74
  return dir;
52
- const parent = dirname(dir);
53
- if (parent === dir)
75
+ const parent = parentDir(dir);
76
+ if (parent === null || parent === dir)
54
77
  return null;
55
78
  dir = parent;
56
79
  }
@@ -77,7 +100,7 @@ export function moduleRoot() {
77
100
  try {
78
101
  const here = fileURLToPath(import.meta.url);
79
102
  const real = safeRealpath(here) ?? here;
80
- const root = findPackageRoot(dirname(real));
103
+ const root = findPackageRoot(parentDir(real) ?? real);
81
104
  cachedModuleRoot = root ? normalizePath(root) : null;
82
105
  }
83
106
  catch {
@@ -124,7 +147,9 @@ export function isSelfEpisode(trace, markers = []) {
124
147
  // Fingerprint: another clone of the plugin, wherever it lives.
125
148
  for (const candidate of collectPaths(trace)) {
126
149
  const real = safeRealpath(candidate) ?? candidate;
127
- const pkgRoot = findPackageRoot(dirname(real)) ?? findPackageRoot(real);
150
+ // Starting from the file path itself (not its dir) is deliberate: on POSIX
151
+ // a Windows-style path has no dirname, so parentDir would lose the walk.
152
+ const pkgRoot = findPackageRoot(real);
128
153
  if (!pkgRoot)
129
154
  continue;
130
155
  const name = packageNameAt(pkgRoot);
@@ -20,6 +20,7 @@
20
20
  import { createInterface } from 'node:readline';
21
21
  import { pathToFileURL } from 'node:url';
22
22
  import { ImmuneStore } from '../../core/store/index.js';
23
+ import { renderSkillMarkdown } from '../../core/export.js';
23
24
  const SERVER_INFO = { name: 'dsh-immune-system', version: '0.8.0' };
24
25
  const TOOLS = [
25
26
  {
@@ -39,6 +40,17 @@ const TOOLS = [
39
40
  description: 'Counts: active/retired rules, total injections, queued distillation jobs, database path.',
40
41
  inputSchema: { type: 'object', properties: {} },
41
42
  },
43
+ {
44
+ name: 'immune_export',
45
+ description: 'Render the proven rules (hits >= threshold) as a SKILL.md document and return its text. Save the text to share the library with teammates or commit it.',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: {
49
+ minHits: { type: 'number', description: 'Minimum hit count. Default 3.' },
50
+ title: { type: 'string', description: 'Skill title. Default "project-pitfalls".' },
51
+ },
52
+ },
53
+ },
42
54
  {
43
55
  name: 'immune_learn',
44
56
  description: 'Teach a rule directly, without waiting for a failed-then-fixed episode. Use for conventions the agent keeps getting wrong.',
@@ -116,6 +128,16 @@ export function callTool(store, name, args) {
116
128
  `queued distill jobs: ${store.pendingJobCount()}\n` +
117
129
  `database: ${store.dbPath}`);
118
130
  }
131
+ case 'immune_export': {
132
+ const minHits = typeof args.minHits === 'number' && args.minHits > 0 ? args.minHits : 3;
133
+ const title = typeof args.title === 'string' && args.title.trim() ? args.title.trim() : 'project-pitfalls';
134
+ const rows = store.exportable(minHits);
135
+ if (rows.length === 0) {
136
+ return `No rule has reached ${minHits} hits yet; nothing worth sharing.`;
137
+ }
138
+ const md = renderSkillMarkdown(rows, { minHits, title });
139
+ return `Exported ${rows.length} rules as SKILL.md:\n\n${md}`;
140
+ }
119
141
  case 'immune_learn': {
120
142
  const trigger = typeof args.trigger === 'string' ? args.trigger : '';
121
143
  const symptom = typeof args.symptom === 'string' ? args.symptom : '';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-immune-system",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Self-evolving pitfall immunity for coding agents: capture every failed-then-fixed tool episode, distil it into a reusable antibody, and inject the relevant ones before the agent repeats the mistake.",
5
5
  "type": "module",
6
6
  "main": "./dist/dsh/src/index.js",
@@ -24,7 +24,7 @@
24
24
  "dsh": {
25
25
  "id": "dsh-immune-system",
26
26
  "name": "DSH Immune System",
27
- "version": "1.0.0",
27
+ "version": "1.0.1",
28
28
  "repo": "meat-daemon-404/dsh-plugin-immune",
29
29
  "permissions": "读取工具调用结果与斜杠命令输入;在本机写 SQLite 抗体库(默认落在 $DSH_HOME/storages/dsh-immune-system/ 下按项目隔离);仅在显式配置 IMMUNE_LLM_BASE_URL 与 IMMUNE_LLM_MODEL 时才向该端点发起请求;不读取其它插件的配置,不执行任意命令",
30
30
  "bundle": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-immune-system",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Self-evolving pitfall immunity for DeepSeek Harness: capture every failed-then-fixed tool episode, distil it into a reusable antibody stored in SQLite+FTS5, and inject the relevant ones before the agent repeats the mistake.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,7 +24,7 @@
24
24
  "dsh": {
25
25
  "id": "dsh-immune-system",
26
26
  "name": "DSH Immune System",
27
- "version": "1.0.0",
27
+ "version": "1.0.1",
28
28
  "repo": "meat-daemon-404/dsh-plugin-immune",
29
29
  "permissions": "读取工具调用结果与斜杠命令输入;在本机写 SQLite 抗体库(默认落在 $DSH_HOME/storages/dsh-immune-system/ 下按项目隔离);仅在显式配置 IMMUNE_LLM_BASE_URL 与 IMMUNE_LLM_MODEL 时才向该端点发起请求;不读取其它插件的配置,不执行任意命令",
30
30
  "bundle": {