@mengyuly/dsh-ponytail 0.1.6 → 0.2.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/CHANGELOG.md CHANGED
@@ -3,6 +3,59 @@
3
3
  All notable changes to `@mengyuly/dsh-ponytail` are documented here.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.2.0] - 2026-08-24
7
+
8
+ ### Added
9
+
10
+ - 真正区分 lite/full/ultra 的 Prompt:从「Markdown 正则过滤同一份正文」改为
11
+ **结构化片段组合**(Common 规则 + 永不可删的 Safety 边界 + 各档独立规则)。
12
+ - `lite`:完整交付明确要求、可一句话提示更简方案、不挑战明确需求;
13
+ - `full`:完整七级阶梯、默认最短正确实现、修根因;
14
+ - `ultra`:先删后加、主动质疑投机性功能/缓存/抽象/配置/新依赖、先给最小
15
+ 正确版并说明完整版条件、不是无脑拒绝;
16
+ - 三档共享 Common + Safety(输入校验/数据丢失防护/安全/无障碍/明确验收项)。
17
+ - 常驻注入从 ~1.3k tokens 降到 **lite≈369 / full≈420 / ultra≈406** tokens。
18
+ - **Cordis Profile 级 `defaultMode` 配置**:`config: { defaultMode }` 按
19
+ profile 生效(`web → full`、`tui → lite` 等),优先级
20
+ `会话 override > env > Profile > 用户 config.json > full`;非法值告警一次
21
+ 并回退;Profile 配置初始化时读取(Cordis 无公开配置变更事件),重启生效;
22
+ `/ponytail default` 的 saved/effective 提示现在会点名覆盖来源
23
+ (`PONYTAIL_DEFAULT_MODE` / `profile configuration`)。
24
+ - 兼容矩阵:CI 扩展为 **Node 22 × Node 24 × ubuntu × windows**(4 组合);
25
+ `dist-provenance.json` 增加 `generatedBy.cordis`;README 记录实测矩阵
26
+ (含 web profile 本机验证、tui/headless 如实标注未验证)。
27
+ - 结构化 Prompt 的行为测试、快照测试、token 统计测试;Profile 优先级测试
28
+ (env>profile、会话 override>env、非法回退、双 profile 不同默认)。
29
+
30
+ ### Changed
31
+
32
+ - 默认模式解析加入 Profile 档(代码/测试/README 三处一致)。
33
+ - README:三档真实差异、Profile 配置示例、兼容矩阵、子代理边界表述。
34
+
35
+ ### Fixed
36
+
37
+ - 删除随旧实现遗留的 `filterSkillBodyForMode` 正则过滤路径及其测试
38
+ (被结构化组合取代)。
39
+
40
+ ### Security
41
+
42
+ - 无变化(0.1.6 的 eval-free 产物与 dev-tooling 边界保持)。
43
+
44
+ ## Unreleased
45
+
46
+ ### Security
47
+
48
+ - Documented the development-only `child_process` boundary (`SECURITY.md`):
49
+ `scripts/**` is excluded from the npm tarball, has no install lifecycle
50
+ hook, and is unreachable from the installed runtime entry.
51
+ - Added tarball checks preventing `scripts/` (and `src/`, `tests/`, `test/`,
52
+ `tools/`) from being published, plus a post-install assertion that the
53
+ installed package contains no `scripts/`.
54
+ - Added checks preventing `preinstall` / `install` / `postinstall` /
55
+ `prepare` lifecycle hooks from silently invoking development tooling.
56
+ - Classified repository-only `child_process` findings as accepted
57
+ development-tooling risk.
58
+
6
59
  ## [0.1.6] - 2026-08-24
7
60
 
8
61
  ### Fixed
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  ![CI](https://github.com/MengYuil/dsh-ponytail/actions/workflows/ci.yml/badge.svg)
4
4
  ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
5
5
  [![npm](https://img.shields.io/npm/v/@mengyuly/dsh-ponytail)](https://www.npmjs.com/package/@mengyuly/dsh-ponytail)
6
+ [![dsh.so security](https://www.dsh.so/badge/dsh-ponytail-4.svg)](https://www.dsh.so/artifact/dsh-ponytail-4/)
6
7
 
7
8
  把 [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail)(「懒惰资深开发者」最少代码心智)移植成 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 原生插件。功能与效率与上游一致:7 级阶梯规则集每轮注入、强度切换、`/ponytail-*` 斜杠命令。
8
9
 
@@ -32,12 +33,18 @@ dsh plugin --profile web add @mengyuly/dsh-ponytail
32
33
 
33
34
  ## 功能
34
35
 
35
- - **核心模式** `/ponytail` — 每轮注入「懒惰阶梯」:能不做就不做(YAGNI)→ 代码库已有 → 标准库 → 平台原生 → 已装依赖 → 一行能解决 → 才是最少代码。
36
- - `full`(默认)/ `lite` / `ultra` / `off` 四档,**会话级**(会话 A 的档位不影响会话 B,会话结束自动释放)。
37
- - `/ponytail`:已启用时只报告;会话为 `off` 时恢复到有效默认档(有效默认也是 `off` 则回 `full`)。
36
+ - **核心模式** `/ponytail` — 每轮注入结构化的懒惰开发者规则集,**三个档位是真实不同的 Prompt 片段**(不只是换一行):
37
+ - **Common(所有非 off 档共享)**:先理解问题、追踪真实调用流;优先复用/标准库/原生能力/已有依赖;非平凡改动留一个最小可运行检查;解释简短但不省略关键决策。
38
+ - **Safety(任何档位都不可删)**:输入校验、防数据丢失的错误处理、安全措施、无障碍、明确验收项、先理解问题、「最小 diff 正确修复」。
39
+ - **`lite`**:完整交付明确要求;可以一句话指出更简方案,但**不挑战明确需求**;输出可略完整。
40
+ - **`full`(默认)**:完整七级阶梯(YAGNI → 复用 → 标准库 → 原生 → 已装依赖 → 一行 → 最小实现),默认选最短正确实现,修根因而非症状。
41
+ - **`ultra`**:YAGNI 极端(先删后加);主动质疑投机性功能/缓存/抽象/配置/新依赖;复杂需求先给最小正确版并说明完整版条件;**不是无脑拒绝**。
42
+ - `off`:完全不注入。
43
+ - 档位**会话级**(会话 A 不影响会话 B,会话结束自动释放)。
44
+ - 裸 `/ponytail`:已启用时只报告;`off` 时恢复到有效默认档(默认也是 `off` 则回 `full`)。
38
45
  - `/ponytail status`:只查询、永不修改。
39
46
  - `/ponytail lite|full|ultra|off`:显式切换。
40
- - `/ponytail default <mode>`:持久化默认值到配置文件(环境变量仍优先)。
47
+ - `/ponytail default <mode>`:持久化默认值到**用户级配置文件**(env/Profile 仍优先,命令分别提示 saved 与 effective)。
41
48
  - **一次性技能**(用哪个载哪个,不进常驻 prompt):
42
49
  - `/ponytail-review` — 针对最近改动找过度工程,一行一条:位置 + 删什么 + 替代。
43
50
  - `/ponytail-audit` — 全仓库过度工程审计,排序清单。
@@ -45,23 +52,53 @@ dsh plugin --profile web add @mengyuly/dsh-ponytail
45
52
  - `/ponytail-gain` — 收益计分板(更少代码/更省成本/更快)。
46
53
  - `/ponytail-help` — 参考卡。
47
54
  - **停用**:说 `stop ponytail` 或 `normal mode`(兼容中英文句末标点);随时 `/ponytail` 恢复。
48
- - **默认值**:环境变量 `PONYTAIL_DEFAULT_MODE` > `~/.config/ponytail/config.json`(Windows:`%APPDATA%\ponytail\config.json`)的 `{"defaultMode": "lite"}` > `full`。`/ponytail default` 写入的是配置文件,**环境变量设置且合法时仍压过保存值**(命令会分别提示 saved 与 effective)。
49
- - **子代理**:常驻段作用于当前会话自身;DSH 内置 `subagent` 工具跑的是隔离的全新子代理、不继承本 persona。`PONYTAIL_SUBAGENT_MATCHER`(匹配子代理 `agentPreset` 的正则)用于在 harness 会下发给子代理的场景里排除指定子代理;缺省全部注入。
50
- - **配置错误**:非法 JSON / 非法 `defaultMode` / 读取失败 / 非法正则只告警一次(不刷屏);配置文件不存在属正常、不告警;热更新遇到临时非法内容保留上一个合法默认值。
55
+ - **默认值优先级**(代码/测试/文档一致):
56
+ ```
57
+ 会话 override > PONYTAIL_DEFAULT_MODE > Profile config.defaultMode > 用户 config.json > full
58
+ ```
59
+ - **Profile 级配置**(Cordis 官方插件配置 API,各 profile 可不同):
60
+ ```yaml
61
+ # ~/.dsh/profiles/tui/cordis.patch.yml 中给 ponytail 行补 config
62
+ - insert:
63
+ - id: ponytail
64
+ name: '@mengyuly/dsh-ponytail'
65
+ config:
66
+ defaultMode: lite
67
+ ```
68
+ 例:`web → full`、`tui → lite`、`automation → off`。Profile 配置在插件初始化时读取(Cordis 无公开配置变更事件),**改后需重启该 profile**;非法值只告警一次并回退,不影响启动。用户 `config.json` 仍保持热更新。
69
+ - **用户 config.json**(`~/.config/ponytail/config.json`,Windows `%APPDATA%\ponytail\config.json`):`{"defaultMode": "lite"}`,热更新(~1s 轮询),非法内容保留上次合法值。
70
+ - **子代理(如实边界)**:DSH 内置 `subagent` 工具是**隔离派生**,默认**不继承**本插件的 system-prompt;`PONYTAIL_SUBAGENT_MATCHER`(匹配子代理 `agentPreset` 的正则)**只用于筛选能进入本 Prompt 管线的子代理**,不是继承开关;DSH 当前没有公开的子代理派生/可继承 Prompt API,因此**未实现、也不宣称父子 Prompt 继承**(有官方 API 后再考虑只读快照传播)。非法正则告警一次并 fail-open。
71
+ - **配置错误**:非法 JSON / 非法 `defaultMode` / 读取失败 / 非法正则只告警一次(不刷屏);配置文件不存在属正常、不告警。
51
72
 
52
73
  ## 效率
53
74
 
54
- - 常驻注入 ≈ 1.3k tokens/请求,`off` 归零;同模式字节级稳定,KV-cache 前缀命中,切模式后才重算一次。
75
+ - 常驻注入:**lite369 / full ≈ 420 / ultra ≈ 406 tokens**(结构化片段,不再是 ~1.3k);`off` 归零;同模式字节级稳定,KV-cache 前缀命中。
55
76
  - 一次性技能 300–540 tokens 一个,零常驻开销。
56
77
  - 实测同任务 A/B:ponytail 臂 34 行 vs 完整实现臂 272 行,均标准库、均自测通过。
57
78
 
58
79
  ## 已知限制
59
80
 
60
- - 强度档位只切换阶梯表格/示例,阶梯正文恒定;lite/full/ultra 体积差异很小(行为倾向,非大小差异)。
81
+ - 档位差异在**规则语义**上(见上),三者体积相近(≤ 满档 ×1.25)。
61
82
  - 上游 Claude 专属的 statusline 徽标无 DSH 对应物,MCP 服务器因 DSH 有一等 system-prompt 注入点而弃用。
62
- - 配置文件的默认档位热更新(fs 轮询 ~1s,作用于无覆盖的会话);环境变量改动仍需重启。
83
+ - 用户 `config.json` 热更新;`PONYTAIL_DEFAULT_MODE` 与 Profile config 需重启生效。
63
84
  - 发行 `lib/` 是预编译产物;改源码请回主仓重建后同步。
64
85
 
86
+ ## 兼容矩阵(实测,不虚构)
87
+
88
+ | 组件 | 已验证环境 | 备注 |
89
+ |---|---|---|
90
+ | Node.js | 22.x / 24.x | CI 矩阵 4 组合全绿 |
91
+ | OS | ubuntu-latest / windows-latest | CI 矩阵 |
92
+ | DSH | commit `b150a551`(构建所用 checkout) | 与正式发布版本的精确对应关系**待确认** |
93
+ | Cordis | 4.0.1(构建所用 vendor) | 同上 |
94
+ | web profile | 已验证 | 本机真实 profile 长期运行 + 三路径隔离安装实测(npm / GitHub / tgz) |
95
+ | tui profile | 未验证 | 未在 tui profile 中启动测试 |
96
+ | headless profile | 未验证 | 未完整启动;插件单元测试运行于无 UI 环境 |
97
+ | npm tarball | 已验证 | 内容/版本/安装后 smoke/NodeNext consumer |
98
+
99
+ - `dist-provenance.json` 记录实际构建来源(checkout commit + node/typescript/tsdown/cordis 版本)。
100
+ - 不要用 `continue-on-error` 掩盖失败——矩阵全绿才是绿。
101
+
65
102
  ## 测试环境与权威关系
66
103
 
67
104
  - 本机(Linux,Node.js **v24.16.0**,deepseek-harness checkout 构建)与 CI 矩阵(**ubuntu-latest + windows-latest**,Node 24)上验证通过。与之精确匹配的已发布 DSH/Cordis 版本**待确认**——checkout 是预发布工作树,非发布 tag。
@@ -85,6 +122,7 @@ dsh plugin --profile web add @mengyuly/dsh-ponytail
85
122
  - **CI 能力边界(如实)**:CI(ubuntu + windows 矩阵)执行上述静态验证与打包/消费测试,但**不重新构建权威 monorepo**;`verify:dist` 是导出表面/签名/运行时导出的一致性检查,**不是**与权威构建的字节级等价证明——后者由 `sync:dist` 在发布流程中保证。
86
123
  - `dist-provenance.json` 随 npm 包发布,便于审计构建来源。
87
124
  - 本机验证时若 `npm_execpath` 指向其他包管理器(如 pnpm/yarn shim),脚本会自动回退到 PATH 上的 `npm`;临时目录失败时保留需设 `PONYTAIL_VERIFY_KEEP_TEMP=1`。
125
+ - **安全**:`scripts/**` 仅用于开发/构建/发行验证,**不进入 npm tarball**、无安装生命周期钩子、运行时入口不引用;`child_process` 告警属于可接受的开发工具风险。详见 [SECURITY.md](SECURITY.md)。
88
126
 
89
127
  ## 许可
90
128
 
@@ -5,6 +5,7 @@
5
5
  "generatedBy": {
6
6
  "node": "v24.16.0",
7
7
  "typescript": "6.0.3",
8
- "tsdown": "0.22.2"
8
+ "tsdown": "0.22.2",
9
+ "cordis": "4.0.1"
9
10
  }
10
11
  }
package/lib/index.js CHANGED
@@ -1366,21 +1366,25 @@ function configPath(env = process.env) {
1366
1366
  return join(configDir(env), "config.json");
1367
1367
  }
1368
1368
  /**
1369
- * Read the configured default with diagnostics: environment variable first,
1370
- * then the config file, then `full`. A missing config file is normal and
1371
- * yields no issue; a broken one yields the fallback mode plus one issue for
1372
- * the caller to warn about once.
1369
+ * Read the configured default with diagnostics. Priority:
1370
+ * `PONYTAIL_DEFAULT_MODE` Cordis profile `defaultMode` user config file
1371
+ * `full`. A missing config file is normal and yields no issue; a broken one
1372
+ * yields the fallback mode plus one issue for the caller to warn about once.
1373
+ * @param env - the process environment to read.
1374
+ * @param profileMode - the validated Cordis profile `defaultMode`, or `null`
1375
+ * when the profile config is absent or invalid (invalid values are reported
1376
+ * by the caller; this function only consumes valid ones).
1373
1377
  */
1374
- function readDefaultModeInfo(env = process.env) {
1378
+ function readDefaultModeInfo(env = process.env, profileMode = null) {
1375
1379
  const path = configPath(env);
1376
1380
  const envMode = normalizeRuntimeMode(env.PONYTAIL_DEFAULT_MODE);
1377
1381
  let configText;
1378
1382
  try {
1379
1383
  configText = readFileSync(path, "utf8");
1380
1384
  } catch (error) {
1381
- if (error.code === "ENOENT") return { mode: envMode ?? "full" };
1385
+ if (error.code === "ENOENT") return { mode: envMode ?? profileMode ?? "full" };
1382
1386
  return {
1383
- mode: envMode ?? "full",
1387
+ mode: envMode ?? profileMode ?? "full",
1384
1388
  issue: {
1385
1389
  kind: "read",
1386
1390
  detail: `${path}: ${error.message}`
@@ -1412,6 +1416,10 @@ function readDefaultModeInfo(env = process.env) {
1412
1416
  mode: envMode,
1413
1417
  ...configIssue ? { issue: configIssue } : {}
1414
1418
  };
1419
+ if (profileMode) return {
1420
+ mode: profileMode,
1421
+ ...configIssue ? { issue: configIssue } : {}
1422
+ };
1415
1423
  if (configIssue) return {
1416
1424
  mode: DEFAULT_MODE,
1417
1425
  issue: configIssue
@@ -1420,10 +1428,19 @@ function readDefaultModeInfo(env = process.env) {
1420
1428
  }
1421
1429
  /**
1422
1430
  * Read the configured default for this host: environment variable first, then
1423
- * the config file, then `full`.
1431
+ * the Cordis profile `defaultMode`, then the user config file, then `full`.
1432
+ */
1433
+ function readDefaultMode(env = process.env, profileMode = null) {
1434
+ return readDefaultModeInfo(env, profileMode).mode;
1435
+ }
1436
+ /**
1437
+ * Why a `saved` default is not the effective one — for the `/ponytail default`
1438
+ * result message. `null` means the saved value is effective.
1424
1439
  */
1425
- function readDefaultMode(env = process.env) {
1426
- return readDefaultModeInfo(env).mode;
1440
+ function defaultOverrideReason(env, profileMode) {
1441
+ if (normalizeRuntimeMode(env.PONYTAIL_DEFAULT_MODE)) return "PONYTAIL_DEFAULT_MODE";
1442
+ if (profileMode) return "profile configuration";
1443
+ return null;
1427
1444
  }
1428
1445
  /**
1429
1446
  * Persist a new default level to the config file, preserving other fields.
@@ -1514,55 +1531,102 @@ function isSubagentSession(header) {
1514
1531
  //#endregion
1515
1532
  //#region lib/types/instructions.js
1516
1533
  /**
1517
- * Build the mode-filtered ponytail ruleset. Ported from the upstream
1518
- * `hooks/ponytail-instructions.js`, so the injected text is byte-for-byte the
1519
- * same ruleset every other host emits, filtered to the active intensity.
1534
+ * Structured ponytail ruleset composition. Each intensity is built from
1535
+ * explicit fragments common rules, a never-cut safety boundary list, and
1536
+ * the mode's own rules instead of filtering one Markdown body with regexes.
1537
+ * The three intensities therefore differ in their actual instructions, not
1538
+ * just in a table row.
1520
1539
  *
1521
1540
  * @module @deepseek-ai/dsh-ponytail
1522
1541
  */
1542
+ /** Shared identity line, carried by every non-`off` mode. */
1543
+ const INTRO = "You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.";
1523
1544
  /**
1524
- * Keep a line of the skill body only when it belongs to every mode or to the
1525
- * active one. Both shape-sensitive spots (the intensity table rows and the
1526
- * quoted worked examples) are keyed by a mode name; ordinary rules survive
1527
- * verbatim, even ones whose prose starts with a mode-looking word.
1545
+ * Understanding-and-reuse baseline, identical in every non-`off` mode.
1528
1546
  */
1529
- function filterSkillBodyForMode(body, mode) {
1530
- const effective = normalizeRuntimeMode(mode) ?? "full";
1531
- return body.split(/\r?\n/).filter((line) => {
1532
- const tableLabel = line.match(/^\|\s*\*\*(.+?)\*\*\s*\|/);
1533
- if (tableLabel) {
1534
- const labelMode = normalizeRuntimeMode(tableLabel[1]);
1535
- if (labelMode) return labelMode === effective;
1536
- }
1537
- const exampleLabel = line.match(/^-\s*([^:]+):\s*"/);
1538
- if (exampleLabel) {
1539
- const labelMode = normalizeRuntimeMode(exampleLabel[1]);
1540
- if (labelMode) return labelMode === effective;
1541
- }
1542
- return true;
1543
- }).join("\n");
1544
- }
1545
- /** Minimal instruction set if the skill body can't be read (parity fallback). */
1546
- function fallbackInstructions(mode) {
1547
- return "PONYTAIL MODE ACTIVE — level: " + mode + "\n\nYou are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.\n\n## Persistence\n\nACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure. Off only: \"stop ponytail\" / \"normal mode\".\n\nCurrent level: **" + mode + "**. Switch: `/ponytail lite|full|ultra`.\n\n## The ladder\n\nBefore any code, stop at the first rung that holds (the ladder runs after you understand the problem, not instead of it — read the code it touches and trace the real flow first):\n1. Does this need to be built at all? (YAGNI)\n2. Does it already exist in this codebase? Reuse what is already here, do not re-write it.\n3. Does the standard library do this? Use it.\n4. Does a native platform feature cover it? Use it.\n5. Does an already-installed dependency solve it? Use it.\n6. Can this be one line? Make it one line.\n7. Only then: write the minimum code that works.\n\nBug fix = root cause, not symptom: grep every caller of the function you touch and fix the shared function once (a smaller diff than one guard per caller); patching only the path the ticket names leaves a sibling caller broken.\n\n## Rules\n\nNo abstractions that were not requested. No avoidable dependencies. No boilerplate nobody asked for. Deletion over addition. Boring over clever. Fewest files possible. Ship the lazy version and question the complex request in the same response — never stall. Between two same-size stdlib options, pick the one correct on edge cases. Mark deliberate simplifications that cut a real corner with a known ceiling, using a `ponytail:` comment that names the ceiling and upgrade path.\n\n## Output\n\nCode first. Then at most three short lines: what was skipped, when to add it. If the explanation is longer than the code, delete the explanation. Explanation the user explicitly asked for is not debt, give it in full.\n\n## When NOT to be lazy\n\nNever simplify away: understanding the problem (read it fully and trace the real flow before picking a rung — a small diff you do not understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, security measures, accessibility basics, the calibration real hardware needs (the platform is never the spec ideal), anything the user explicitly asked to keep. Lazy code without its check is unfinished: non-trivial logic leaves ONE runnable check behind (assert-based demo/self-check or one small test file; no frameworks). Trivial one-liners need no test.\n\n## Boundaries\n\nPonytail governs what you build, not how you talk. \"stop ponytail\" or \"normal mode\": revert. Level persists until changed.";
1547
+ const COMMON_RULES = [
1548
+ "Understand the problem before choosing a solution: read the code the change touches and trace the real flow end to end. Laziness that skips comprehension ships a confident wrong fix.",
1549
+ "Reuse what already exists in this codebase before writing anything new.",
1550
+ "Reach for the standard library, platform-native features, and already-installed dependencies before custom code.",
1551
+ "A non-trivial change leaves ONE minimal runnable check behind (an assert-based self-check or one small test file; no frameworks). Trivial one-liners need no test.",
1552
+ "Explain briefly, but never omit the key decisions."
1553
+ ].join("\n");
1554
+ /**
1555
+ * The never-cut list. Every non-`off` mode keeps these; intensities tune how
1556
+ * aggressively code is minimized, never what may be dropped.
1557
+ */
1558
+ const SAFETY_BOUNDARIES = [
1559
+ "Never cut, in any mode:",
1560
+ "- Input validation at trust boundaries.",
1561
+ "- Error handling that prevents data loss.",
1562
+ "- Security measures.",
1563
+ "- Accessibility basics.",
1564
+ "- Explicit acceptance criteria the user asked for.",
1565
+ "- Understanding the problem and tracing the real flow first.",
1566
+ "- \"Minimal diff\" is not a substitute for \"correct fix\"."
1567
+ ].join("\n");
1568
+ const MODE_RULES = {
1569
+ lite: [
1570
+ "Complete what is explicitly asked, including every acceptance criterion.",
1571
+ "Prefer reuse, the standard library, native features, and installed dependencies.",
1572
+ "You may name a simpler alternative in one line, but do not challenge or reject an explicit requirement.",
1573
+ "Output may be a little more complete than full; never cut an acceptance item to save lines."
1574
+ ].join("\n"),
1575
+ full: [
1576
+ "The ladder — stop at the first rung that holds:",
1577
+ "1. Does this need to exist at all? (YAGNI)",
1578
+ "2. Does it already exist in this codebase? Reuse it.",
1579
+ "3. Does the standard library do it? Use it.",
1580
+ "4. Does a native platform feature cover it? Use it.",
1581
+ "5. Does an already-installed dependency solve it? Use it.",
1582
+ "6. Can this be one line? Make it one line.",
1583
+ "7. Only then: the minimum code that works.",
1584
+ "Default to the shortest correct implementation; prefer deletion and reuse.",
1585
+ "Fix root causes, not symptoms: one guard in the shared function beats a guard in every caller."
1586
+ ].join("\n"),
1587
+ ultra: [
1588
+ "YAGNI extremist: default to deletion before addition.",
1589
+ "Actively question speculative features, caches, abstractions, configuration, and new dependencies.",
1590
+ "Prefer one-liners, the standard library, and native capabilities.",
1591
+ "Minimize files, dependencies, and code — but never the safety boundaries or acceptance criteria above.",
1592
+ "For a complex request: ship the minimal correct version first and state what the full version would require.",
1593
+ "Ultra is not \"refuse everything\": honor explicit user requirements."
1594
+ ].join("\n")
1595
+ };
1596
+ const MODE_LABELS = {
1597
+ lite: "Lite",
1598
+ full: "Full",
1599
+ ultra: "Ultra"
1600
+ };
1601
+ /** Compose the complete section text for one intensity. */
1602
+ function render(effective) {
1603
+ return [
1604
+ `PONYTAIL MODE ACTIVE — level: ${effective}`,
1605
+ "",
1606
+ INTRO,
1607
+ "",
1608
+ "## Common rules (all modes)",
1609
+ COMMON_RULES,
1610
+ "",
1611
+ "## Safety boundaries (never cut)",
1612
+ SAFETY_BOUNDARIES,
1613
+ "",
1614
+ `## ${MODE_LABELS[effective]} rules`,
1615
+ MODE_RULES[effective]
1616
+ ].join("\n");
1548
1617
  }
1549
1618
  /**
1550
- * The full injected ruleset for one intensity: the "PONYTAIL MODE ACTIVE"
1551
- * header plus the body filtered down to that mode's rows and examples.
1552
- * Returns an empty string for `off` (ponytail contributes nothing).
1619
+ * The injected ruleset for one intensity, composed from the structured
1620
+ * fragments above. Returns an empty string for `off` (ponytail contributes
1621
+ * nothing). Renders are pure per mode and cached so every turn's bytes stay
1622
+ * identical.
1553
1623
  */
1554
1624
  function getPonytailInstructions(mode) {
1555
1625
  const effective = normalizeRuntimeMode(mode) ?? "full";
1556
1626
  if (effective === "off") return "";
1557
1627
  const cached = instructionCache.get(effective);
1558
1628
  if (cached !== void 0) return cached;
1559
- let body;
1560
- try {
1561
- body = filterSkillBodyForMode(PONYTAIL_SKILL_BODY, effective);
1562
- } catch {
1563
- return fallbackInstructions(effective);
1564
- }
1565
- const rendered = "PONYTAIL MODE ACTIVE — level: " + effective + "\n\n" + body;
1629
+ const rendered = render(effective);
1566
1630
  instructionCache.set(effective, rendered);
1567
1631
  return rendered;
1568
1632
  }
@@ -1660,7 +1724,7 @@ function registerCommands(deps, commandCtx) {
1660
1724
  kind: "error",
1661
1725
  text: "Usage: /ponytail default [lite|full|ultra|off]"
1662
1726
  };
1663
- const effective = readDefaultMode();
1727
+ const effective = readDefaultMode(process.env, deps.profileMode);
1664
1728
  deps.setDefault(effective);
1665
1729
  if (written === effective) {
1666
1730
  agent.steer(createUserMessage({
@@ -1678,10 +1742,11 @@ function registerCommands(deps, commandCtx) {
1678
1742
  text: `Ponytail default set — new sessions start in ${written}.`
1679
1743
  };
1680
1744
  }
1745
+ const reason = defaultOverrideReason(process.env, deps.profileMode) ?? "PONYTAIL_DEFAULT_MODE";
1681
1746
  agent.steer(createUserMessage({
1682
1747
  content: [{
1683
1748
  type: "text",
1684
- text: `PONYTAIL DEFAULT SET — saved ${written}, effective ${effective} (PONYTAIL_DEFAULT_MODE).`
1749
+ text: `PONYTAIL DEFAULT SET — saved ${written}, effective ${effective} (${reason}).`
1685
1750
  }],
1686
1751
  source: {
1687
1752
  kind: "plugin",
@@ -1690,7 +1755,7 @@ function registerCommands(deps, commandCtx) {
1690
1755
  }));
1691
1756
  return {
1692
1757
  kind: "success",
1693
- text: `Saved default: ${written}. Effective default: ${effective}, overridden by PONYTAIL_DEFAULT_MODE.`
1758
+ text: `Saved default: ${written}. Effective default: ${effective}, overridden by ${reason}.`
1694
1759
  };
1695
1760
  }
1696
1761
  if (input === "status") return {
@@ -1799,7 +1864,9 @@ function descriptionFor(skill) {
1799
1864
  * Register the always-on ruleset section, the runtime skills, the slash
1800
1865
  * commands, and the plain-text deactivation listener.
1801
1866
  */
1802
- function apply(ctx) {
1867
+ function apply(ctx, config = {}) {
1868
+ const profileMode = normalizeRuntimeMode(config.defaultMode);
1869
+ if (config.defaultMode !== void 0 && profileMode === null) ctx.logger.warn(`[ponytail] profile config defaultMode is not lite|full|ultra|off: ${JSON.stringify(config.defaultMode)}; falling back`);
1803
1870
  let defaultMode = null;
1804
1871
  const warned = /* @__PURE__ */ new Set();
1805
1872
  const warnOnce = (key, message) => {
@@ -1808,7 +1875,7 @@ function apply(ctx) {
1808
1875
  ctx.logger.warn(`[ponytail] ${message}`);
1809
1876
  };
1810
1877
  const refreshDefault = () => {
1811
- const resolution = readDefaultModeInfo();
1878
+ const resolution = readDefaultModeInfo(process.env, profileMode);
1812
1879
  if (resolution.issue) warnOnce(`default:${resolution.issue.kind}`, `${resolution.issue.detail}; using ${resolution.mode}`);
1813
1880
  defaultMode = resolution.mode;
1814
1881
  return defaultMode;
@@ -1823,7 +1890,7 @@ function apply(ctx) {
1823
1890
  if (matcherResult.invalid) warnOnce("matcher:invalid", "PONYTAIL_SUBAGENT_MATCHER is not a valid regular expression; ignoring it (fail-open).");
1824
1891
  const configFile = configPath();
1825
1892
  const onConfigChange = () => {
1826
- const resolution = readDefaultModeInfo();
1893
+ const resolution = readDefaultModeInfo(process.env, profileMode);
1827
1894
  if (resolution.issue) {
1828
1895
  warnOnce(`config:${resolution.issue.kind}`, `${resolution.issue.detail}; keeping the previous default`);
1829
1896
  return;
@@ -1853,6 +1920,7 @@ function apply(ctx) {
1853
1920
  registerCommands({
1854
1921
  ctx,
1855
1922
  store,
1923
+ profileMode,
1856
1924
  defaultMode: readDefault,
1857
1925
  setDefault
1858
1926
  }, commandCtx);
@@ -15,6 +15,19 @@
15
15
  import type { Context } from '@deepseek-ai/cordis';
16
16
  export declare const name = "ponytail";
17
17
  export declare const inject: string[];
18
+ /**
19
+ * Cordis profile-level configuration (set per profile via the bundle row's
20
+ * `config` in a profile patch). `defaultMode` sits between the environment
21
+ * variable and the user config file in the default-resolution chain.
22
+ *
23
+ * No `Config` schema is exported on purpose: cordis passes the raw config
24
+ * through unvalidated then, so an invalid `defaultMode` falls back with a
25
+ * one-time warning instead of failing the plugin mount.
26
+ */
27
+ export interface PonytailConfig {
28
+ /** Profile-scoped default intensity, e.g. `web → full`, `tui → lite`. */
29
+ defaultMode?: unknown;
30
+ }
18
31
  /** Extract the plain text of one user message (only its text blocks). */
19
32
  export declare function messageText(message: {
20
33
  content: readonly {
@@ -33,4 +46,4 @@ export declare function containsDeactivation(messages: readonly {
33
46
  * Register the always-on ruleset section, the runtime skills, the slash
34
47
  * commands, and the plain-text deactivation listener.
35
48
  */
36
- export declare function apply(ctx: Context): void;
49
+ export declare function apply(ctx: Context, config?: PonytailConfig): void;
@@ -1,23 +1,17 @@
1
1
  /**
2
- * Build the mode-filtered ponytail ruleset. Ported from the upstream
3
- * `hooks/ponytail-instructions.js`, so the injected text is byte-for-byte the
4
- * same ruleset every other host emits, filtered to the active intensity.
2
+ * Structured ponytail ruleset composition. Each intensity is built from
3
+ * explicit fragments common rules, a never-cut safety boundary list, and
4
+ * the mode's own rules instead of filtering one Markdown body with regexes.
5
+ * The three intensities therefore differ in their actual instructions, not
6
+ * just in a table row.
5
7
  *
6
8
  * @module @deepseek-ai/dsh-ponytail
7
9
  */
8
10
  import { type PonytailRuntimeMode } from './modes.ts';
9
11
  /**
10
- * Keep a line of the skill body only when it belongs to every mode or to the
11
- * active one. Both shape-sensitive spots (the intensity table rows and the
12
- * quoted worked examples) are keyed by a mode name; ordinary rules survive
13
- * verbatim, even ones whose prose starts with a mode-looking word.
14
- */
15
- export declare function filterSkillBodyForMode(body: string, mode: PonytailRuntimeMode | null | undefined): string;
16
- /** Minimal instruction set if the skill body can't be read (parity fallback). */
17
- export declare function fallbackInstructions(mode: PonytailRuntimeMode): string;
18
- /**
19
- * The full injected ruleset for one intensity: the "PONYTAIL MODE ACTIVE"
20
- * header plus the body filtered down to that mode's rows and examples.
21
- * Returns an empty string for `off` (ponytail contributes nothing).
12
+ * The injected ruleset for one intensity, composed from the structured
13
+ * fragments above. Returns an empty string for `off` (ponytail contributes
14
+ * nothing). Renders are pure per mode and cached so every turn's bytes stay
15
+ * identical.
22
16
  */
23
17
  export declare function getPonytailInstructions(mode: PonytailRuntimeMode | null | undefined): string;
@@ -47,17 +47,26 @@ export interface DefaultModeResolution {
47
47
  readonly issue?: DefaultModeIssue;
48
48
  }
49
49
  /**
50
- * Read the configured default with diagnostics: environment variable first,
51
- * then the config file, then `full`. A missing config file is normal and
52
- * yields no issue; a broken one yields the fallback mode plus one issue for
53
- * the caller to warn about once.
50
+ * Read the configured default with diagnostics. Priority:
51
+ * `PONYTAIL_DEFAULT_MODE` Cordis profile `defaultMode` user config file
52
+ * `full`. A missing config file is normal and yields no issue; a broken one
53
+ * yields the fallback mode plus one issue for the caller to warn about once.
54
+ * @param env - the process environment to read.
55
+ * @param profileMode - the validated Cordis profile `defaultMode`, or `null`
56
+ * when the profile config is absent or invalid (invalid values are reported
57
+ * by the caller; this function only consumes valid ones).
54
58
  */
55
- export declare function readDefaultModeInfo(env?: NodeJS.ProcessEnv): DefaultModeResolution;
59
+ export declare function readDefaultModeInfo(env?: NodeJS.ProcessEnv, profileMode?: PonytailRuntimeMode | null): DefaultModeResolution;
56
60
  /**
57
61
  * Read the configured default for this host: environment variable first, then
58
- * the config file, then `full`.
62
+ * the Cordis profile `defaultMode`, then the user config file, then `full`.
59
63
  */
60
- export declare function readDefaultMode(env?: NodeJS.ProcessEnv): PonytailRuntimeMode;
64
+ export declare function readDefaultMode(env?: NodeJS.ProcessEnv, profileMode?: PonytailRuntimeMode | null): PonytailRuntimeMode;
65
+ /**
66
+ * Why a `saved` default is not the effective one — for the `/ponytail default`
67
+ * result message. `null` means the saved value is effective.
68
+ */
69
+ export declare function defaultOverrideReason(env: NodeJS.ProcessEnv, profileMode: PonytailRuntimeMode | null): 'PONYTAIL_DEFAULT_MODE' | 'profile configuration' | null;
61
70
  /**
62
71
  * Persist a new default level to the config file, preserving other fields.
63
72
  * Returns the normalized mode, or `null` when the value is not a runtime mode.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mengyuly/dsh-ponytail",
3
3
  "description": "Lazy senior dev mode for DeepSeek Harness: always-on minimal-code ruleset, intensity switching, and short review/audit/debt/gain/help skills",
4
- "version": "0.1.6",
4
+ "version": "0.2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },