dsh-plugin-guide 0.3.29 → 0.3.31

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.
@@ -9,14 +9,31 @@ const LONG_ALIASES = {
9
9
  help: "help",
10
10
  version: "version"
11
11
  };
12
+ /** Raised for a malformed or unrecognised argument. */
13
+ var UsageError = class extends Error {};
12
14
  /**
13
15
  * Parse an argv slice (without the node/script prefix) into positionals and flags.
16
+ *
17
+ * `allowed` is the set of long flag names the caller accepts (aliases included).
18
+ * Passing it turns an unrecognised flag into a `UsageError` instead of a silently
19
+ * ignored one: `dsh-plugin-dev check --stirct` used to parse the typo, check
20
+ * nothing extra, and exit 0 — a gate that can be disabled by a spelling mistake
21
+ * is worse than no gate, because CI stays green and everyone believes it ran.
22
+ *
14
23
  * @param argv - process.argv.slice(2) style input.
24
+ * @param allowed - optional allowlist of accepted long flag names.
15
25
  * @returns the parsed structure.
16
26
  */
17
- function parseArgs(argv) {
27
+ function parseArgs(argv, allowed) {
18
28
  const positionals = [];
19
29
  const flags = {};
30
+ const permitted = allowed ? new Set(allowed) : void 0;
31
+ const assertAllowed = (key) => {
32
+ if (permitted && !permitted.has(key)) {
33
+ const known = [...permitted].sort().join(", ");
34
+ throw new UsageError(`unknown option "--${key}" (accepted: ${known})`);
35
+ }
36
+ };
20
37
  for (let i = 0; i < argv.length; i++) {
21
38
  const arg = argv[i];
22
39
  if (arg === "--") {
@@ -27,10 +44,13 @@ function parseArgs(argv) {
27
44
  const body = arg.slice(2);
28
45
  const eq = body.indexOf("=");
29
46
  if (eq >= 0) {
30
- setFlag(flags, body.slice(0, eq), body.slice(eq + 1));
47
+ const key = body.slice(0, eq);
48
+ assertAllowed(LONG_ALIASES[key] ?? key);
49
+ setFlag(flags, key, body.slice(eq + 1));
31
50
  continue;
32
51
  }
33
52
  const key = body;
53
+ assertAllowed(LONG_ALIASES[key] ?? key);
34
54
  const next = argv[i + 1];
35
55
  if (next !== void 0 && !next.startsWith("-")) {
36
56
  setFlag(flags, key, next);
@@ -39,7 +59,10 @@ function parseArgs(argv) {
39
59
  continue;
40
60
  }
41
61
  if (arg.startsWith("-") && arg.length > 1) {
42
- for (const ch of arg.slice(1)) flags[ch] = true;
62
+ for (const ch of arg.slice(1)) {
63
+ assertAllowed(ch);
64
+ flags[ch] = true;
65
+ }
43
66
  continue;
44
67
  }
45
68
  positionals.push(arg);
@@ -254,82 +277,82 @@ const SKILL_SECTIONS = {
254
277
  "patch-valid": {
255
278
  file: "guide/plugin-dev-guide.md",
256
279
  section: "§2.2",
257
- heading: "两个概念、两种清单(bundle manifest)"
280
+ heading: "三个概念、三种清单(package.json 的 `dsh` 键)"
258
281
  },
259
282
  "patch-ids-unique": {
260
283
  file: "guide/plugin-dev-guide.md",
261
284
  section: "§2.3",
262
- heading: "配置分层顺序(按 id 整行覆盖)"
285
+ heading: "配置分层顺序(后层覆盖前层,逐行按 `id` 整段替换 config)"
263
286
  },
264
287
  "manifest-bundle-patch": {
265
288
  file: "guide/plugin-dev-guide.md",
266
289
  section: "§2.2",
267
- heading: "bundle 最小结构(dsh.bundle.patch)"
290
+ heading: "三个概念、三种清单(package.json 的 `dsh` 键)"
268
291
  },
269
292
  "manifest-main": {
270
293
  file: "guide/plugin-dev-guide.md",
271
294
  section: "§7.3",
272
- heading: "main/types 指向 lib 产物"
295
+ heading: "常见坑清单"
273
296
  },
274
297
  "manifest-peers": {
275
298
  file: "guide/plugin-dev-guide.md",
276
299
  section: "§7.3",
277
- heading: "cordis 双副本与 peer 对齐宿主"
300
+ heading: "常见坑清单"
278
301
  },
279
302
  "manifest-engines": {
280
303
  file: "guide/plugin-dev-guide.md",
281
304
  section: "§8",
282
- heading: "规范与质量门禁(Node 版本)"
305
+ heading: "规范与质量门禁(对插件作者的实际影响)"
283
306
  },
284
307
  "manifest-files": {
285
308
  file: "guide/plugin-dev-guide.md",
286
309
  section: "§7.1",
287
- heading: "files 白名单与构建产物"
310
+ heading: "从 GitHub 安装的 build-script 坑"
288
311
  },
289
312
  "manifest-package-manager": {
290
313
  file: "guide/plugin-dev-guide.md",
291
314
  section: "§8",
292
- heading: "packageManager 固定 pnpm"
315
+ heading: "规范与质量门禁(对插件作者的实际影响)"
293
316
  },
294
317
  "display-meta": {
295
318
  file: "guide/plugin-dev-guide.md",
296
319
  section: "§7.0.1",
297
- heading: "展示元数据与图标(locale meta + 导出 icon)"
320
+ heading: "展示元数据与图标(0.2 世代新增的硬性交付项)"
298
321
  },
299
322
  "readme-five-langs": {
300
323
  file: "guide/plugin-dev-guide.md",
301
324
  section: "§8",
302
- heading: "文档双语/多语成对"
325
+ heading: "规范与质量门禁(对插件作者的实际影响)"
303
326
  },
304
327
  "readme-consistency": {
305
328
  file: "guide/plugin-dev-guide.md",
306
329
  section: "§8",
307
- heading: "五语 README 同步"
330
+ heading: "规范与质量门禁(对插件作者的实际影响)"
308
331
  },
309
332
  "redline-persona-role": {
310
- file: "SKILL.md",
311
- section: "§边界",
312
- heading: "注入提示词段落以角色句开头、保持短小"
333
+ file: "guide/plugin-dev-guide.md",
334
+ section: "§3.7",
335
+ heading: "仓库级红线(写进 AGENTS.md,违反会挂门禁)"
313
336
  },
314
337
  "redline-waterfall-next": {
315
338
  file: "guide/plugin-dev-guide.md",
316
339
  section: "§3.5",
317
- heading: "waterfall 铁律:必须调用 next()"
340
+ heading: "事件(Events)"
318
341
  },
319
342
  "redline-no-hardcoded-tunables": {
320
343
  file: "guide/plugin-dev-guide.md",
321
344
  section: "§3.6",
322
- heading: "配置 Schema 化、不硬编码可调参数"
345
+ heading: "配置(Config)"
323
346
  },
324
347
  "redline-effect-registration": {
325
348
  file: "guide/plugin-dev-guide.md",
326
349
  section: "§3.3",
327
- heading: "注册即 effect(disposer 可逆)"
350
+ heading: "注册即 effect"
328
351
  },
329
352
  "redline-async-apply-registration": {
330
353
  file: "guide/plugin-dev-guide.md",
331
- section: "§3.3",
332
- heading: "async apply 首个 await 之后不得注册(卸载窗口竞态)"
354
+ section: "§3.3.1",
355
+ heading: "async `apply()` 的注册顺序(最容易写成静默缺陷的一条)"
333
356
  }
334
357
  };
335
358
  /** Fallback citation for any check id not in the table. */
@@ -909,7 +932,8 @@ function checkManifestFiles(_root, pkg) {
909
932
  const problems = [];
910
933
  for (const patchFile of patchFiles) if (!files.includes(patchFile)) problems.push(`files whitelist is missing the patch file "${patchFile}"`);
911
934
  if (main && !files.includes(main) && !files.some((f) => main.startsWith(`${f.replace(/\/$/, "")}/`))) problems.push(`files whitelist is missing the main entry "${main}"`);
912
- if (!files.some((f) => f === "lib" || f === "dist")) problems.push("files whitelist has no built-artifact directory (`lib` or `dist`)");
935
+ const entryDir = main?.includes("/") ? main.slice(0, main.indexOf("/")) : void 0;
936
+ if (entryDir && (entryDir === "lib" || entryDir === "dist") && !files.includes(entryDir)) problems.push(`files whitelist has no built-artifact directory ("${entryDir}"), but main points into it`);
913
937
  if (problems.length > 0) return {
914
938
  id: "manifest-files",
915
939
  severity: "error",
@@ -1104,14 +1128,26 @@ function checkReadmeConsistency(root) {
1104
1128
  skillRef: ref
1105
1129
  };
1106
1130
  }
1107
- /** Body of SKILL.md after its YAML frontmatter, or undefined. */
1131
+ /**
1132
+ * Body of SKILL.md after its YAML frontmatter, or undefined when the file is
1133
+ * absent. Falls back to the whole text when there is no well-formed block.
1134
+ *
1135
+ * CRLF tolerance is load-bearing: without `.gitattributes`, a Windows checkout
1136
+ * (`core.autocrlf=true`) delivers CRLF while CI and the published tarball
1137
+ * deliver LF. An exact `startsWith('---\n')` guard therefore failed locally,
1138
+ * leaving the frontmatter in the "body" — so `redline-persona-role` judged the
1139
+ * `name:` line as the opening paragraph and emitted a false warning on every
1140
+ * local run while CI stayed green. Keep this in step with `index.js`.
1141
+ */
1108
1142
  function skillBody(root) {
1109
1143
  const text = readText(join(root, "SKILL.md"));
1110
1144
  if (!text) return void 0;
1111
- if (!text.startsWith("---\n")) return text;
1112
- const end = text.indexOf("\n---", 4);
1113
- if (end < 0) return text;
1114
- return text.slice(end + 4).replace(/^\n+/, "");
1145
+ const open = /^---\r?\n/.exec(text);
1146
+ if (!open) return text;
1147
+ const rest = text.slice(open[0].length);
1148
+ const close = /\r?\n---[ \t]*(?:\r?\n|$)/.exec(rest);
1149
+ if (!close) return text;
1150
+ return rest.slice(close.index + close[0].length).replace(/^(?:[ \t]*\r?\n)+/, "");
1115
1151
  }
1116
1152
  function checkRedlinePersonaRole(root) {
1117
1153
  const ref = skillRefFor("redline-persona-role");
@@ -1921,7 +1957,37 @@ function timeoutMs(flags, key, envKey, fallback) {
1921
1957
  * @param cwd - working directory (injectable for tests).
1922
1958
  */
1923
1959
  async function main(argv, cwd = process.cwd()) {
1924
- const { positionals, flags } = parseArgs(argv);
1960
+ const KNOWN_FLAGS = [
1961
+ "help",
1962
+ "h",
1963
+ "version",
1964
+ "V",
1965
+ "lang",
1966
+ "dir",
1967
+ "force",
1968
+ "git",
1969
+ "cwd",
1970
+ "json",
1971
+ "strict",
1972
+ "dsh",
1973
+ "pnpm",
1974
+ "profile",
1975
+ "base",
1976
+ "headless",
1977
+ "timeout",
1978
+ "smoke-timeout"
1979
+ ];
1980
+ let parsed;
1981
+ try {
1982
+ parsed = parseArgs(argv, KNOWN_FLAGS);
1983
+ } catch (err) {
1984
+ if (err instanceof UsageError) {
1985
+ process.stderr.write(`dsh-plugin-dev: ${err.message}\n`);
1986
+ return 2;
1987
+ }
1988
+ throw err;
1989
+ }
1990
+ const { positionals, flags } = parsed;
1925
1991
  if (flagBool(flags, "version") || flags.V === true) {
1926
1992
  process.stdout.write(`${readCliVersion()}\n`);
1927
1993
  return 0;
@@ -148,6 +148,37 @@ ctx.effect(() => { const t = setInterval(...); return () => clearInterval(t) })
148
148
 
149
149
  框架代管:`ctx.on` / 服务 `register()` / `ctx.effect()`。`HMR`(`@deepseek-ai/cordis-plugin-hmr`)正因注册全部可逆而成立。
150
150
 
151
+ #### 3.3.1 ⚠️ async `apply()` 的注册顺序(最容易写成静默缺陷的一条)
152
+
153
+ `apply` **可以是 async**,但**第一个 `await` 之后再做注册是错的**:从 `await` 返回到注册完成之间存在一个
154
+ 卸载窗口——插件已开始 `UNLOADING`、effect 却还没建立,于是那次注册**既不会被记入 fiber 的 disposer 集合,
155
+ 也不会报错**。表现为"插件装了、日志全绿、能力却时有时无"(高负载下才复现)。
156
+
157
+ **规则:注册必须在第一个 `await` 之前完成。** 需要异步数据才能决定注册内容时,先取数据再注册:
158
+
159
+ ```ts
160
+ // ✅ 正确:await 只用于取数据,注册全部落在 await 之前的同步段之后、但不再 await
161
+ export async function apply(ctx) {
162
+ const cfg = await loadRemoteConfig() // 允许 await
163
+ if (!cfg.enabled) return // 允许提前返回
164
+ ctx.effect(() => ctx.tools.register(buildTool(cfg))) // 注册紧随其后,不再 await
165
+ }
166
+ ```
167
+
168
+ ```ts
169
+ // ❌ 错误:await 之后才注册,卸载窗口内注册会丢
170
+ export async function apply(ctx) {
171
+ ctx.on('a', handlerA)
172
+ await something()
173
+ ctx.tools.register(toolB) // ← 这次注册可能永远不会被撤销
174
+ }
175
+ ```
176
+
177
+ **自查**:`dsh-plugin-dev check` 的 `redline-async-apply-registration` 就是本条的机械检测
178
+ (家族内唯一检测器,见 [`family-toolchain.md`](family-toolchain.md) §3.2)。
179
+ **官方契约**:`apply` 返回 disposer 函数 / `null` / `undefined` / `Promise<disposer>` / (异步)可迭代——
180
+ async `apply` 合法,但**必须返回 `Promise<disposer>`**,不能靠"await 之后再注册"来隐式注册。
181
+
151
182
  ### 3.4 服务(Service)
152
183
 
153
184
  - **消费**:`export const inject = ['tools']` → `apply` 里 `ctx.tools` 已就绪。可选依赖:不写 inject,用 `ctx.get('metrics')?.method()`。
@@ -200,9 +231,10 @@ export function apply(ctx: Context, config: Config) { /* config 已校验+补默
200
231
 
201
232
  1. **注册即 effect**:所有贡献走 `ctx.effect()` / `ctx.on()`;registry 的 `register()` 返回 disposer。
202
233
  2. **waterfall 监听器必须调 `next()`**。
203
- 3. **模型可见 ⟺ 已记录**(Model-visible ⟺ logged):任何进入模型请求的内容必须能从会话日志重建;新增模型可见输入必须新增会话事件。运行时不变式会断言这一点。
234
+ 3. **模型可见 ⟺ 已记录**(Model-visible ⟺ logged):任何进入模型请求的内容必须能从会话日志重建;新增模型可见输入必须新增会话事件。~~运行时不变式会断言这一点~~ —— ⚠️ **该强制手段已于 `0.2.1-alpha.1` 随运行时 invariants 整体移除**(`upgrade-guide/v0.2.0-rc.2/remove-runtime-invariants`),所以这条**现在完全靠评审与自觉**,没有任何机械兜底。见 §3.6 与 [`migration-0.2.md`](migration-0.2.md) §4。
204
235
  4. **跨边界 opaque id 用 branded**:`Branded<B>`(`dsh-brand`,纯类型、零运行时依赖),从不裸 `string`;构造走 per-type factory(`SessionId` / `CallId` / `JobId` / `GoalId` 等),防止不同 id 在类型层互换。
205
236
  5. **会话事件版本规则**:`SessionEventMap` 成员默认 required-on-read——不认识该事件类型的 build 一律拒绝整份日志,除非该事件带信封的 `ignorable: true`。**插件不得新增事件类型**(见 §3.5 红线):`Session.append()` 写不了信封标记。只有结构格式变更才 bump `SESSION_FORMAT_VERSION`(当前 **4**;V3→V4 把 results 提升为 tool-role 消息、把消息 source 改为生产者自有的 `source.kind`,V3 读方拒绝 V4 日志)。新增**模型可见输入**由宿主侧事件承担(红线 3),插件只消费。
237
+ 6. **注入的提示词段落要短、且以角色句开头**:插件写进系统提示词的段落(`systemPrompt.section()` 的正文)**第一句应当是说明"你是谁/做什么"的短句**,整段保持精简。原因有二:它是每轮请求都付费的固定前缀;而且段落开头若不是完整句子,模型会把后续内容当作它的延续来读。**自查**:`dsh-plugin-dev check` 的 `redline-persona-role`(段落首句需以 `。.?!` 收尾、首句 ≤200 字符、整段 ≤600 字符)。**这条是"工程卫生"而非官方硬门禁**——官方文档没有对应条款,别在 README 里把它说成官方要求。
206
238
 
207
239
  ---
208
240
 
@@ -306,6 +338,70 @@ declare module '@deepseek-ai/dsh-llm' {
306
338
 
307
339
  **工具的 `parameters` 是 DSH 自己的方言,不是 JSON Schema**;Remote 服务调用是**位置参数**而不是对象参数(`get()` 能用,最有欺骗性);往 `ctx` 上写**未声明**的属性会抛错并可能静默杀掉整个插件的激活——可选服务一律 `ctx.get(name)`,**`ctx.<service>` 只对自己 `inject` 里声明过的服务合法**。
308
340
 
341
+ ### 4.9 插件内如何发布一个 skill(本仓库就是参考实现)
342
+
343
+ 一个插件可以**自己携带一份 agent 技能**——这是把"知识/流程"交付给宿主的标准做法,也是本仓库
344
+ (`dsh-plugin-guide`)唯一在做的事。机制曾经在社区资料里被写错成"systemPrompt section + 工具注册",
345
+ **那不是这条路**。
346
+
347
+ **契约**(官方 `docs/subsystems/skills.md`):
348
+
349
+ - `ctx.skills` 是 **provider 注册表**,注册是**同步**的,必须在 `apply()` 内完成。
350
+ - 插件走的是**运行时贡献**入口 `ctx.skills.register(definition)`,其类型为
351
+ `SkillRegistration = Omit<SkillDefinition, 'invocation' | 'provider'> & { invocation?; provider? }`
352
+ ——`invocation` 省略即模型与用户两侧都可见,`provider` 省略即落到注册表自有的 runtime provider。
353
+ - **返回的是 Cordis effect disposer**:撤销该贡献并使发现缓存失效。所以照例写成
354
+ `ctx.effect(() => ctx.skills.register({...}))`。
355
+ - 要携带随包资料,给 **`resourceBase: { kind: 'directory', path: <包目录> }`**;技能正文里的
356
+ `./guide/...`、`./references/...` 相对路径就按该目录解析。**这是"渐进披露"得以成立的关键**:
357
+ 技能正文只放路由,重资料留在包里按需读,不必一次塞进模型上下文。
358
+
359
+ **最小可用实现**(取自本仓库 `index.js`,可直接抄):
360
+
361
+ ```js
362
+ import { readFileSync } from 'node:fs'
363
+ import { dirname, join } from 'node:path'
364
+ import { fileURLToPath } from 'node:url'
365
+
366
+ export const name = 'my-skill-plugin'
367
+ export const inject = ['skills']
368
+
369
+ const packageRoot = dirname(fileURLToPath(import.meta.url))
370
+
371
+ export function apply(ctx) {
372
+ const { description, body } = splitFrontmatter(readFileSync(join(packageRoot, 'SKILL.md'), 'utf8'))
373
+ ctx.effect(() =>
374
+ ctx.skills.register({
375
+ name: 'my-skill-plugin',
376
+ source: 'bundled',
377
+ description, // 决定"何时被加载"
378
+ content: body, // 技能正文
379
+ resourceBase: { kind: 'directory', path: packageRoot },
380
+ }),
381
+ )
382
+ }
383
+ ```
384
+
385
+ **四个实测要点**(每一条都踩过):
386
+
387
+ 1. **`description` 就是触发器,要写"何时使用"而不是"这是什么"。** 宿主只把 name + description 放进目录给模型挑,
388
+ 所以写 `Use when developing, reviewing, packaging, debugging, or answering questions about …`
389
+ 这类带场景动词的句子,而不是名词短语。
390
+ 2. **解析 frontmatter 要容忍 CRLF。** 用精确的 `startsWith('---\n')` 会在 Windows 检出(`core.autocrlf=true`)
391
+ 上失配,后果是**静默退化**:描述掉回兜底字符串(技能变得不易被选中),且**整份文件连同 frontmatter 被当成技能正文**注入。
392
+ 本仓库为此加了 `.gitattributes`(`* text=auto eol=lf`),并把解析改成行尾无关。
393
+ 3. **截断 frontmatter 与裁掉正文前导空行是两件事。** 只做前者,正文会以换行开头;宿主的 filesystem provider
394
+ 自己会裁,但**不是所有 provider 都裁**,自己裁更稳。
395
+ 4. **单测要真的加载入口模块。** `index.js` 长期零测试,缺陷因此活到发布之后;测试只需一个
396
+ `{ effect, skills: { register } }` 桩,就能断言"注册的 name、用的是 frontmatter 描述还是兜底、
397
+ `resourceBase` 是否指向包目录"。本仓库 `tests/entry.test.ts` 就是这 8 条断言。
398
+
399
+ **与"工具"的区别**:注册工具(`ctx.tools.register`)会让**每一次请求**都付该工具定义的 token;
400
+ 注册技能只在模型**按描述选中**它时才加载正文。给 agent 交付"资料/流程"用技能,交付"可执行动作"才用工具。
401
+
402
+ **相关**:`references/official-plugin-dev-skill.md`(官方自带开发技能的要点)、
403
+ `references/official-docs/docs/subsystems/skills.md`(完整契约与 ranked local discovery 表)。
404
+
309
405
  ---
310
406
 
311
407
  ## 5. 能力分层:Service Definition / Provider / Consumer
@@ -356,7 +452,7 @@ LLM 适配器同理:继承 `LlmAdapter` 实现 `stream(options)`,`ctx.llm.re
356
452
  | 子代理委派 | `ctx.subagents` provider 注册表 + `dsh-tool-subagent`(现有多家 provider:in-process spawn / fork / `dsh-subagent-acp` / `-codex` / `-claude-code` / `-dsh-sdk`) |
357
453
  | 多代理编排 | `ctx.workflowEngine` 缝(Definition/Provider/Consumer)+ PTC workflow engine + `workflow`/`ralph` 工具 Consumer |
358
454
  | MCP | 每服务器一个插件:发现工具 → `ctx.tools.register()` |
359
- | Skills | section + 工具注册;调用时 `inject()` 技能内容 |
455
+ | Skills | ⚠️ **不是** section + 工具注册:插件用 **`ctx.skills.register()`** 注册一个运行时技能(`SkillRegistration` = `SkillDefinition` 去掉 `invocation`/`provider`,两者可省略走默认),**在同一 `apply()` 内同步注册**,返回的 disposer 撤销贡献并使发现缓存失效。随包携带资料时给 **`resourceBase: { kind: 'directory', path: <包目录> }`**,技能正文里的 `./...` 相对路径即按该目录解析。官方参考实现是 `dsh-skill-badge`(注册一个不可变的 `bundled` 候选 + 暴露打包资源目录)。**本仓库 `index.js` 是第二个可跑的例子**——见下方 §"插件内如何发布 skill" |
360
456
  | 定时任务 | 注册模型可调的调度工具;定时器 → 空闲 `followup(source:{kind:'cron'})` / 忙碌 `inject()` |
361
457
  | 遥测/回放 | `session/event` → JSONL;回放 = `sessions.create(id, {seed})` |
362
458
  | 上报通道(0.2 新增) | `ctx.otel`:`createEventReporter(options)` / `createSessionLogReporter(options)`——自带传输与批处理,未用时**不分配** provider/transport;**消费者必须在自己 fiber 释放时 drain 通道** |
@@ -181,3 +181,5 @@ Documentación comunitaria de desarrollo — plantillas/tutoriales/errores, list
181
181
  - Investigación del repo: `references/harness-repo.md`
182
182
  - Comunidad/ecosistema: `references/community-ecosystem.md` + `references/community-repo-deep-dive.md`
183
183
  - Todas las URLs de fuentes: `references/sources.md`
184
+ - [Matriz de versiones](version-matrix.md) — los cuatro valores «latest», etiquetas `latest` congeladas, fallas de compatibilidad y selección de rango de peers.
185
+ - [Lista de migración](migration-0.2.md) — §0–§12 cubren los cambios que rompen compatibilidad, **§13 los 19 seams de `0.2.1-alpha.2`**.
@@ -181,3 +181,5 @@ Consumer (`dsh-tool-my-cap`): `inject = ['tools','myCap']`, `ctx.tools.register(
181
181
  - रिपो शोध: `references/harness-repo.md`
182
182
  - समुदाय/पारिस्थितिकी: `references/community-ecosystem.md` + `references/community-repo-deep-dive.md`
183
183
  - सभी स्रोत URL: `references/sources.md`
184
+ - [संस्करण मैट्रिक्स](version-matrix.md) — चार "latest" मान, जमे हुए `latest` टैग, संगतता-भंग बिंदु और peer रेंज का चयन।
185
+ - [माइग्रेशन सूची](migration-0.2.md) — §0–§12 संगतता-भंग बदलाव, **§13 में `0.2.1-alpha.2` के 19 seams**।
@@ -181,3 +181,5 @@ Documentação comunitária de desenvolvimento — modelos/tutoriais/armadilhas,
181
181
  - Pesquisa do repo: `references/harness-repo.md`
182
182
  - Comunidade/ecossistema: `references/community-ecosystem.md` + `references/community-repo-deep-dive.md`
183
183
  - Todas as URLs de fontes: `references/sources.md`
184
+ - [Matriz de versões](version-matrix.md) — os quatro valores "latest", tags `latest` congeladas, falhas de compatibilidade e seleção de intervalo de peers.
185
+ - [Lista de migração](migration-0.2.md) — §0–§12 cobrem as mudanças que quebram compatibilidade, **§13 os 19 seams de `0.2.1-alpha.2`**.
package/index.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ // Type declarations for the bundle entry point (`index.js`).
2
+ //
3
+ // `index.js` is plain ESM JavaScript and is intentionally not part of the
4
+ // TypeScript program (`tsconfig.json` includes only `src`, `tests` and the build
5
+ // configs, with `allowJs` off). `tests/entry.test.ts` imports it, so without this
6
+ // file `noImplicitAny` rejects the import as an untyped module.
7
+ //
8
+ // The declaration is deliberately loose: this module exists to be loaded by the
9
+ // host's Cordis loader, not consumed as a typed library, and the shapes it
10
+ // touches (`ctx.effect`, `ctx.skills.register`) belong to the harness rather
11
+ // than to this package. Pinning them here would create a second, drifting copy
12
+ // of the host contract — the exact failure mode `guide/family-toolchain.md`
13
+ // warns about. The test asserts the runtime shape instead.
14
+
15
+ /** Cordis plugin name contributed by this bundle. */
16
+ export declare const name: string
17
+
18
+ /** Services this plugin requires before `apply` runs. */
19
+ export declare const inject: string[]
20
+
21
+ /**
22
+ * Split a SKILL.md document into its frontmatter description and instruction
23
+ * body. Falls back to `{ description: undefined, body: text }` when there is no
24
+ * well-formed frontmatter block. Tolerates both LF and CRLF line endings.
25
+ */
26
+ export declare function splitFrontmatter(text: string): {
27
+ description: string | undefined
28
+ body: string
29
+ }
30
+
31
+ /** Register the knowledge-base skill. Registration is an effect. */
32
+ export declare function apply(ctx: any): void
package/index.js CHANGED
@@ -21,18 +21,33 @@ const packageRoot = dirname(fileURLToPath(import.meta.url))
21
21
 
22
22
  /**
23
23
  * Strip the YAML frontmatter block from SKILL.md and return the description
24
- * plus the instruction body. A malformed or missing block fails loud through
25
- * the read itself or falls back to the full text as the body.
24
+ * plus the instruction body. A malformed or missing block falls back to the
25
+ * full text as the body.
26
+ *
27
+ * CRLF tolerance is deliberate and load-bearing, not cosmetic. Without
28
+ * `.gitattributes`, git's `core.autocrlf=true` checks this repository out with
29
+ * CRLF while the published tarball and every CI checkout use LF. An exact
30
+ * `startsWith('---\n')` guard therefore took a different branch on a Windows
31
+ * working tree and the skill silently registered its generic fallback
32
+ * description instead of the trigger-phrase description from the frontmatter —
33
+ * the text that decides when the host loads the skill at all. `.gitattributes`
34
+ * now pins LF; this function no longer depends on that being true.
35
+ *
26
36
  * @param text - raw SKILL.md content.
27
37
  * @returns parsed description (when the frontmatter carries one) and body.
28
38
  */
29
- function splitFrontmatter(text) {
30
- if (!text.startsWith('---\n')) return { description: undefined, body: text }
31
- const end = text.indexOf('\n---', 4)
32
- if (end < 0) return { description: undefined, body: text }
33
- const meta = text.slice(4, end)
34
- const body = text.slice(end + 4).replace(/^\n+/, '')
35
- const match = /^description:\s*(.+)$/m.exec(meta)
39
+ export function splitFrontmatter(text) {
40
+ // Match the opening fence with either ending, then find the closing fence.
41
+ const open = /^---\r?\n/.exec(text)
42
+ if (!open) return { description: undefined, body: text }
43
+ const rest = text.slice(open[0].length)
44
+ const close = /\r?\n---[ \t]*(?:\r?\n|$)/.exec(rest)
45
+ if (!close) return { description: undefined, body: text }
46
+ const meta = rest.slice(0, close.index)
47
+ // Trim the separating blank lines so the body starts at the first heading,
48
+ // matching the host provider's own body trimming.
49
+ const body = rest.slice(close.index + close[0].length).replace(/^(?:[ \t]*\r?\n)+/, '')
50
+ const match = /^description:[ \t]*(.+?)[ \t]*$/m.exec(meta)
36
51
  return { description: match?.[1]?.trim(), body }
37
52
  }
38
53
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-plugin-guide",
3
- "version": "0.3.29",
4
- "description": "The dsh-plugin-guide knowledge base plus the dsh-plugin-dev CLI toolchain: official docs, Cordis primer, community deep-dives, and battle-tested pitfalls registered as an on-demand agent skill, with a scaffolder, static checker, and pack verifier for building DSH plugins.",
3
+ "version": "0.3.31",
4
+ "description": "DSH plugin development as an on-demand agent skill: official docs mirror, version-compatibility matrix, migration seams, measured pitfalls, plus a scaffolder, checker and pack verifier CLI.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "files": [
17
17
  "index.js",
18
+ "index.d.ts",
18
19
  "bin",
19
20
  "dist",
20
21
  "templates",
@@ -35,15 +36,21 @@
35
36
  ],
36
37
  "keywords": [
37
38
  "dsh",
38
- "deepseek-harness",
39
39
  "dsh-plugin",
40
+ "dsh-plugin-development",
41
+ "dsh-skill",
42
+ "deepseek",
43
+ "deepseek-harness",
40
44
  "cordis",
41
- "agent-skill",
42
45
  "plugin-development",
46
+ "agent-skill",
47
+ "agent-skills",
43
48
  "knowledge-base",
49
+ "documentation",
44
50
  "cli",
45
51
  "scaffold",
46
- "checker"
52
+ "checker",
53
+ "typescript"
47
54
  ],
48
55
  "license": "Apache-2.0",
49
56
  "repository": {
@@ -57,6 +64,8 @@
57
64
  "typecheck:checkout": "tsc -p tsconfig.checkout.json --noEmit",
58
65
  "test": "vitest run",
59
66
  "check:readmes": "node scripts/check-readme-sync.mjs",
67
+ "check:parity": "node scripts/check-readme-parity.mjs",
68
+ "check:ps1-bom": "node scripts/check-ps1-bom.mjs",
60
69
  "build:llms": "node scripts/build-llms-full.mjs",
61
70
  "check:kit": "pwsh -File scripts/verify-kit.ps1",
62
71
  "verify:artifacts": "node scripts/verify-artifacts.mjs",
@@ -64,15 +73,14 @@
64
73
  "prepare": "tsdown",
65
74
  "prepack": "tsdown"
66
75
  },
67
- "dependencies": {
68
- "tsdown": "^0.22.14",
69
- "typescript": "^5.9.3"
70
- },
76
+ "dependencies": {},
71
77
  "devDependencies": {
72
78
  "@deepseek-ai/dsh-attachment": "0.2.1-alpha.2",
73
79
  "@types/node": "^22.20.1",
74
80
  "@types/semver": "^7.8.0",
75
81
  "semver": "^7.8.5",
82
+ "tsdown": "^0.22.14",
83
+ "typescript": "^5.9.3",
76
84
  "vitest": "^3.2.7"
77
85
  },
78
86
  "peerDependencies": {