@mengyuly/dsh-ponytail 0.1.6 → 0.2.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/CHANGELOG.md CHANGED
@@ -3,6 +3,85 @@
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.1] - 2026-08-25
7
+
8
+ ### Fixed
9
+
10
+ - P1: 主 `ponytail` 技能不再携带旧版 Full 规则集——技能正文改为指向注入段的
11
+ **模式感知指针卡**(`PONYTAIL MODE ACTIVE` 段是唯一规则源),Lite/Ultra
12
+ 会话里模型不会从技能加载到与当前档位冲突的 Full 规则。
13
+ - `/ponytail default` 在「保存值与覆盖源值恰好相同」时(如 env=full 且
14
+ `/ponytail default full`)也会点名覆盖来源,不再漏报。
15
+ - `sync:dist` 的产物变更状态检查补上 `src/`(0.2.0 起同步 src 镜像)。
16
+ - CHANGELOG 章节顺序修正(Unreleased 回到最顶部)。
17
+ - README 兼容矩阵文字更新为 Node 22/24。
18
+
19
+ ### Changed
20
+
21
+ - `/ponytail-help` 补充 Profile 级 `defaultMode` 配置与完整优先级链
22
+ (会话 override > env > Profile > 用户 config > full);Ultra 描述改为
23
+ 「先删后加、质疑投机,但绝不删明确要求」,与 0.2.0 新语义一致。
24
+ - 模块头部注释同步(content.ts / modes.ts)。
25
+
26
+ ### Tests
27
+
28
+ - ponytail 技能指针卡断言(不含旧 Full 规则)。
29
+ - `/ponytail default` 同值覆盖源提示测试。
30
+ - 62 项测试全绿(Ubuntu/Windows × Node 22/24 CI 矩阵)。
31
+
32
+ ## Unreleased
33
+ ## Unreleased
34
+
35
+ ### Security
36
+
37
+ - Documented the development-only `child_process` boundary (`SECURITY.md`):
38
+ `scripts/**` is excluded from the npm tarball, has no install lifecycle
39
+ hook, and is unreachable from the installed runtime entry.
40
+ - Added tarball checks preventing `scripts/` (and `src/`, `tests/`, `test/`,
41
+ `tools/`) from being published, plus a post-install assertion that the
42
+ installed package contains no `scripts/`.
43
+ - Added checks preventing `preinstall` / `install` / `postinstall` /
44
+ `prepare` lifecycle hooks from silently invoking development tooling.
45
+ - Classified repository-only `child_process` findings as accepted
46
+ development-tooling risk.
47
+
48
+ ## [0.2.0] - 2026-08-24
49
+
50
+ ### Added
51
+
52
+ - 真正区分 lite/full/ultra 的 Prompt:从「Markdown 正则过滤同一份正文」改为
53
+ **结构化片段组合**(Common 规则 + 永不可删的 Safety 边界 + 各档独立规则)。
54
+ - `lite`:完整交付明确要求、可一句话提示更简方案、不挑战明确需求;
55
+ - `full`:完整七级阶梯、默认最短正确实现、修根因;
56
+ - `ultra`:先删后加、主动质疑投机性功能/缓存/抽象/配置/新依赖、先给最小
57
+ 正确版并说明完整版条件、不是无脑拒绝;
58
+ - 三档共享 Common + Safety(输入校验/数据丢失防护/安全/无障碍/明确验收项)。
59
+ - 常驻注入从 ~1.3k tokens 降到 **lite≈369 / full≈420 / ultra≈406** tokens。
60
+ - **Cordis Profile 级 `defaultMode` 配置**:`config: { defaultMode }` 按
61
+ profile 生效(`web → full`、`tui → lite` 等),优先级
62
+ `会话 override > env > Profile > 用户 config.json > full`;非法值告警一次
63
+ 并回退;Profile 配置初始化时读取(Cordis 无公开配置变更事件),重启生效;
64
+ `/ponytail default` 的 saved/effective 提示现在会点名覆盖来源
65
+ (`PONYTAIL_DEFAULT_MODE` / `profile configuration`)。
66
+ - 兼容矩阵:CI 扩展为 **Node 22 × Node 24 × ubuntu × windows**(4 组合);
67
+ `dist-provenance.json` 增加 `generatedBy.cordis`;README 记录实测矩阵
68
+ (含 web profile 本机验证、tui/headless 如实标注未验证)。
69
+ - 结构化 Prompt 的行为测试、快照测试、token 统计测试;Profile 优先级测试
70
+ (env>profile、会话 override>env、非法回退、双 profile 不同默认)。
71
+
72
+ ### Changed
73
+
74
+ - 默认模式解析加入 Profile 档(代码/测试/README 三处一致)。
75
+ - README:三档真实差异、Profile 配置示例、兼容矩阵、子代理边界表述。
76
+
77
+ ### Fixed
78
+
79
+ - 删除随旧实现遗留的 `filterSkillBodyForMode` 正则过滤路径及其测试
80
+ (被结构化组合取代)。
81
+
82
+ ### Security
83
+
84
+ - 无变化(0.1.6 的 eval-free 产物与 dev-tooling 边界保持)。
6
85
  ## [0.1.6] - 2026-08-24
7
86
 
8
87
  ### 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,26 +52,56 @@ 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
- - 本机(Linux,Node.js **v24.16.0**,deepseek-harness checkout 构建)与 CI 矩阵(**ubuntu-latest + windows-latest**,Node 24)上验证通过。与之精确匹配的已发布 DSH/Cordis 版本**待确认**——checkout 是预发布工作树,非发布 tag。
104
+ - 本机(Linux,Node.js **v24.16.0**,deepseek-harness checkout 构建)与 CI 矩阵(**ubuntu-latest + windows-latest × Node 22/24**)上验证通过。与之精确匹配的已发布 DSH/Cordis 版本**待确认**——checkout 是预发布工作树,非发布 tag。
68
105
  - 权威源码在 deepseek-harness monorepo 的 `packages/community/ponytail`(`@deepseek-ai/dsh-ponytail`);本仓库(`@mengyuly/dsh-ponytail`)是**发行镜像**:随包附构建产物,不是独立真源。
69
106
 
70
107
  ## 发行维护
@@ -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
@@ -919,112 +919,27 @@ function errorMessage(error) {
919
919
  /**
920
920
  * Ponytail skill bodies, ported from github.com/DietrichGebert/ponytail and
921
921
  * lightly adapted to the DeepSeek Harness surface (slash commands and the
922
- * `skill` tool). `ponytail` is the source the system-prompt ruleset is
923
- * filtered from; the other five ship verbatim as runtime skills.
922
+ * `skill` tool). The `ponytail` skill is a mode-aware pointer card: the actual
923
+ * ruleset is injected per session as the mode-filtered `PONYTAIL MODE ACTIVE`
924
+ * section (see `instructions.ts`) and must not be duplicated here. The other
925
+ * five skills ship verbatim as runtime skills.
924
926
  *
925
927
  * @module @deepseek-ai/dsh-ponytail
926
928
  */
927
929
  /** The always-on lazy-senior-dev ruleset: also registered as a loadable skill. */
928
930
  const PONYTAIL_SKILL_BODY = `
929
- You are a lazy senior developer. Lazy means efficient, not careless. You have
930
- seen every over-engineered codebase and been paged at 3am for one. The best
931
- code is the code never written.
932
-
933
- ## Persistence
934
-
935
- ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if
936
- unsure. Off only: "stop ponytail" / "normal mode" / \`/ponytail off\`. Default:
937
- **full**. Switch: \`/ponytail lite|full|ultra\`.
938
-
939
- ## The ladder
940
-
941
- Stop at the first rung that holds:
942
-
943
- 1. **Does this need to exist at all?** Speculative need = skip it, say so in one line. (YAGNI)
944
- 2. **Already in this codebase?** A helper, util, type, or pattern that already lives here → reuse it. Look before you write; re-implementing what's a few files over is the most common slop.
945
- 3. **Stdlib does it?** Use it.
946
- 4. **Native platform feature covers it?** \`<input type="date">\` over a picker lib, CSS over JS, DB constraint over app code.
947
- 5. **Already-installed dependency solves it?** Use it. Never add a new one for what a few lines can do.
948
- 6. **Can it be one line?** One line.
949
- 7. **Only then:** the minimum code that works.
950
-
951
- The ladder is a reflex, not a research project — but it runs *after* you
952
- understand the problem, not instead of it. Read the task and the code it
953
- touches first, trace the real flow end to end, then climb. Two rungs work →
954
- take the higher one and move on. The first lazy solution that works is the
955
- right one — once you actually know what the change has to touch.
956
-
957
- **Bug fix = root cause, not symptom.** A report names a symptom. Before you
958
- edit, grep every caller of the function you're about to touch. The lazy fix IS
959
- the root-cause fix: one guard in the shared function is a smaller diff than a
960
- guard in every caller — and patching only the path the ticket names leaves
961
- every sibling caller still broken. Fix it once, where all callers route through.
962
-
963
- ## Rules
964
-
965
- - No unrequested abstractions: no interface with one implementation, no factory for one product, no config for a value that never changes.
966
- - No boilerplate, no scaffolding "for later", later can scaffold for itself.
967
- - Deletion over addition. Boring over clever, clever is what someone decodes at 3am.
968
- - Fewest files possible. Shortest working diff wins — but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug.
969
- - Complex request? Ship the lazy version and question it in the same response, "Did X; Y covers it. Need full X? Say so." Never stall on an answer you can default.
970
- - Two stdlib options, same size? Take the one that's correct on edge cases. Lazy means writing less code, not picking the flimsier algorithm.
971
- - Mark deliberate simplifications that cut a real corner with a known ceiling (global lock, O(n²) scan, naive heuristic) with a \`ponytail:\` comment naming the ceiling and upgrade path (\`# ponytail: global lock, per-account locks if throughput matters\`).
972
-
973
- ## Output
974
-
975
- Code first. Then at most three short lines: what was skipped, when to add it.
976
- No essays, no feature tours, no design notes. If the explanation is longer
977
- than the code, delete the explanation, every paragraph defending a
978
- simplification is complexity smuggled back in as prose. Explanation the user
979
- explicitly asked for (a report, a walkthrough, per-phase notes) is not debt,
980
- give it in full, the rule is only against unrequested prose.
981
-
982
- Pattern: \`[code] → skipped: [X], add when [Y].\`
983
-
984
- ## Intensity
985
-
986
- | Level | What change |
987
- |-------|------------|
988
- | **lite** | Build what's asked, but name the lazier alternative in one line. User picks. |
989
- | **full** | The ladder enforced. Stdlib and native first. Shortest diff, shortest explanation. Default. |
990
- | **ultra** | YAGNI extremist. Deletion before addition. Ship the one-liner and challenge the rest of the requirement in the same breath. |
991
-
992
- Example: "Add a cache for these API responses."
993
- - lite: "Done, cache added. FYI: \`functools.lru_cache\` covers this in one line if you'd rather not own a cache class."
994
- - full: "\`@lru_cache(maxsize=1000)\` on the fetch function. Skipped custom cache class, add when lru_cache measurably falls short."
995
- - ultra: "No cache until a profiler says so. When it does: \`@lru_cache\`. A hand-rolled TTL cache class is a bug farm with a hit rate."
996
-
997
- ## When NOT to be lazy
998
-
999
- Never simplify away: input validation at trust boundaries, error handling
1000
- that prevents data loss, security measures, accessibility basics, anything
1001
- explicitly requested. User insists on the full version → build it, no
1002
- re-arguing.
1003
-
1004
- Never lazy about understanding the problem. The ladder shortens the
1005
- solution, never the reading. Trace the whole thing first — every file the
1006
- change touches, the actual flow — before picking a rung. Laziness that skips
1007
- comprehension to ship a small diff is the dangerous kind: it dresses up as
1008
- efficiency and ships a confident wrong fix. Read fully, then be lazy.
1009
-
1010
- Hardware is never the ideal on paper: a real clock drifts, a real sensor
1011
- reads off, a PCA9685 runs a few percent fast. Leave the calibration knob, not
1012
- just less code, the physical world needs tuning a minimal model can't see.
1013
-
1014
- Lazy code without its check is unfinished. Non-trivial logic (a branch, a
1015
- loop, a parser, a money/security path) leaves ONE runnable check behind, the
1016
- smallest thing that fails if the logic breaks: an \`assert\`-based
1017
- \`demo()\`/\`__main__\` self-check or one small test file. No frameworks, no
1018
- fixtures, no per-function suites unless asked. Trivial one-liners need no
1019
- test, YAGNI applies to tests too.
1020
-
1021
- ## Boundaries
1022
-
1023
- Ponytail governs what you build, not how you talk. "stop ponytail" / "normal
1024
- mode" / \`/ponytail off\`: revert. Level is session-scoped until changed; the
1025
- configured default (env or config file) applies to new sessions.
1026
-
1027
- The shortest path to done is the right path.
931
+ You are the ponytail persona — the lazy senior developer. Your active ruleset
932
+ is ALREADY injected every turn as the "PONYTAIL MODE ACTIVE level: <mode>"
933
+ system-prompt section, filtered to this session's intensity. Follow exactly
934
+ that section; do NOT reload, replace, or re-derive the ruleset from anywhere
935
+ else — the section is the single source of truth and it is mode-aware.
936
+
937
+ - Switch level: \`/ponytail lite|full|ultra|off\` (session-scoped)
938
+ - Query: \`/ponytail status\`
939
+ - Deactivate: "stop ponytail" / "normal mode"
940
+ - One-shot skills: \`/ponytail-review\`, \`/ponytail-audit\`, \`/ponytail-debt\`,
941
+ \`/ponytail-gain\`, \`/ponytail-help\`
942
+ - Reference: https://github.com/DietrichGebert/ponytail
1028
943
  `;
1029
944
  const PONYTAIL_DESCRIPTION = "Force the laziest solution that actually works — simplest, shortest, most minimal. Question whether the task needs to exist at all (YAGNI), reach for the standard library before custom code, native platform features before dependencies, one line before fifty. Supports intensity levels lite, full (default), and ultra. Use on ANY coding task: writing, adding, refactoring, fixing, reviewing, or designing code, and choosing libraries or dependencies. Also use when the user says \"ponytail\", \"be lazy\", \"lazy mode\", \"simplest solution\", \"minimal solution\", \"yagni\", \"do less\", or \"shortest path\", or complains about over-engineering, bloat, boilerplate, or unnecessary dependencies. Do NOT use for non-coding requests (general knowledge, prose, translation, summaries, recipes).";
1030
945
  const REVIEW_SKILL_BODY = `
@@ -1196,7 +1111,7 @@ write flag files, or persist anything.
1196
1111
  |-------|---------|-------------|
1197
1112
  | **Lite** | \`/ponytail lite\` | Build what's asked, name the lazier alternative in one line. |
1198
1113
  | **Full** | \`/ponytail\` | The ladder enforced: YAGNI → stdlib → native → one line → minimum. Default. |
1199
- | **Ultra** | \`/ponytail ultra\` | YAGNI extremist. Deletion before addition. Challenges requirements before building. |
1114
+ | **Ultra** | \`/ponytail ultra\` | YAGNI extremist: deletion first, questions speculation never cuts explicit requirements. |
1200
1115
  | **Off** | \`/ponytail off\` | Ponytail stops injecting its ruleset for this session. |
1201
1116
 
1202
1117
  Level is session-scoped until changed.
@@ -1236,12 +1151,23 @@ export PONYTAIL_DEFAULT_MODE=ultra
1236
1151
  { "defaultMode": "lite" }
1237
1152
  \`\`\`
1238
1153
 
1154
+ **Profile config** (per DSH profile, via the bundle row's \`config\` — e.g.
1155
+ \`tui\` → lite):
1156
+
1157
+ \`\`\`yaml
1158
+ - insert:
1159
+ - id: ponytail
1160
+ name: '@mengyuly/dsh-ponytail'
1161
+ config:
1162
+ defaultMode: lite
1163
+ \`\`\`
1164
+
1239
1165
  Set \`"off"\` to disable auto-activation on session start, activate manually
1240
1166
  with \`/ponytail\` when wanted. \`/ponytail default <mode>\` persists a new
1241
- default to the config file; an exported \`PONYTAIL_DEFAULT_MODE\` still
1242
- outranks the saved value for new sessions.
1167
+ default to the user config file; an exported \`PONYTAIL_DEFAULT_MODE\` or a
1168
+ profile \`defaultMode\` still outranks the saved value for new sessions.
1243
1169
 
1244
- Resolution: env var > config file > \`full\`.
1170
+ Resolution: session override > env var > profile config > config file > \`full\`.
1245
1171
 
1246
1172
  ## More
1247
1173
 
@@ -1317,8 +1243,9 @@ function ponytailSkills() {
1317
1243
  //#endregion
1318
1244
  //#region lib/types/modes.js
1319
1245
  /**
1320
- * Ponytail mode resolution: the default level comes from the
1321
- * `PONYTAIL_DEFAULT_MODE` environment variable, then the optional config file
1246
+ * Ponytail mode resolution: the effective default comes from, in order, the
1247
+ * `PONYTAIL_DEFAULT_MODE` environment variable, the Cordis profile
1248
+ * `defaultMode`, the optional user config file
1322
1249
  * `~/.config/ponytail/config.json` (`defaultMode`), then `full`. Setting a
1323
1250
  * level via the `/ponytail` command is session-scoped and lives in an
1324
1251
  * in-memory, per-agent {@link ModeStore}.
@@ -1366,21 +1293,25 @@ function configPath(env = process.env) {
1366
1293
  return join(configDir(env), "config.json");
1367
1294
  }
1368
1295
  /**
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.
1296
+ * Read the configured default with diagnostics. Priority:
1297
+ * `PONYTAIL_DEFAULT_MODE` Cordis profile `defaultMode` user config file
1298
+ * `full`. A missing config file is normal and yields no issue; a broken one
1299
+ * yields the fallback mode plus one issue for the caller to warn about once.
1300
+ * @param env - the process environment to read.
1301
+ * @param profileMode - the validated Cordis profile `defaultMode`, or `null`
1302
+ * when the profile config is absent or invalid (invalid values are reported
1303
+ * by the caller; this function only consumes valid ones).
1373
1304
  */
1374
- function readDefaultModeInfo(env = process.env) {
1305
+ function readDefaultModeInfo(env = process.env, profileMode = null) {
1375
1306
  const path = configPath(env);
1376
1307
  const envMode = normalizeRuntimeMode(env.PONYTAIL_DEFAULT_MODE);
1377
1308
  let configText;
1378
1309
  try {
1379
1310
  configText = readFileSync(path, "utf8");
1380
1311
  } catch (error) {
1381
- if (error.code === "ENOENT") return { mode: envMode ?? "full" };
1312
+ if (error.code === "ENOENT") return { mode: envMode ?? profileMode ?? "full" };
1382
1313
  return {
1383
- mode: envMode ?? "full",
1314
+ mode: envMode ?? profileMode ?? "full",
1384
1315
  issue: {
1385
1316
  kind: "read",
1386
1317
  detail: `${path}: ${error.message}`
@@ -1412,6 +1343,10 @@ function readDefaultModeInfo(env = process.env) {
1412
1343
  mode: envMode,
1413
1344
  ...configIssue ? { issue: configIssue } : {}
1414
1345
  };
1346
+ if (profileMode) return {
1347
+ mode: profileMode,
1348
+ ...configIssue ? { issue: configIssue } : {}
1349
+ };
1415
1350
  if (configIssue) return {
1416
1351
  mode: DEFAULT_MODE,
1417
1352
  issue: configIssue
@@ -1420,10 +1355,19 @@ function readDefaultModeInfo(env = process.env) {
1420
1355
  }
1421
1356
  /**
1422
1357
  * Read the configured default for this host: environment variable first, then
1423
- * the config file, then `full`.
1358
+ * the Cordis profile `defaultMode`, then the user config file, then `full`.
1424
1359
  */
1425
- function readDefaultMode(env = process.env) {
1426
- return readDefaultModeInfo(env).mode;
1360
+ function readDefaultMode(env = process.env, profileMode = null) {
1361
+ return readDefaultModeInfo(env, profileMode).mode;
1362
+ }
1363
+ /**
1364
+ * Why a `saved` default is not the effective one — for the `/ponytail default`
1365
+ * result message. `null` means the saved value is effective.
1366
+ */
1367
+ function defaultOverrideReason(env, profileMode) {
1368
+ if (normalizeRuntimeMode(env.PONYTAIL_DEFAULT_MODE)) return "PONYTAIL_DEFAULT_MODE";
1369
+ if (profileMode) return "profile configuration";
1370
+ return null;
1427
1371
  }
1428
1372
  /**
1429
1373
  * Persist a new default level to the config file, preserving other fields.
@@ -1514,55 +1458,102 @@ function isSubagentSession(header) {
1514
1458
  //#endregion
1515
1459
  //#region lib/types/instructions.js
1516
1460
  /**
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.
1461
+ * Structured ponytail ruleset composition. Each intensity is built from
1462
+ * explicit fragments common rules, a never-cut safety boundary list, and
1463
+ * the mode's own rules instead of filtering one Markdown body with regexes.
1464
+ * The three intensities therefore differ in their actual instructions, not
1465
+ * just in a table row.
1520
1466
  *
1521
1467
  * @module @deepseek-ai/dsh-ponytail
1522
1468
  */
1469
+ /** Shared identity line, carried by every non-`off` mode. */
1470
+ const INTRO = "You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.";
1523
1471
  /**
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.
1472
+ * Understanding-and-reuse baseline, identical in every non-`off` mode.
1528
1473
  */
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.";
1474
+ const COMMON_RULES = [
1475
+ "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.",
1476
+ "Reuse what already exists in this codebase before writing anything new.",
1477
+ "Reach for the standard library, platform-native features, and already-installed dependencies before custom code.",
1478
+ "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.",
1479
+ "Explain briefly, but never omit the key decisions."
1480
+ ].join("\n");
1481
+ /**
1482
+ * The never-cut list. Every non-`off` mode keeps these; intensities tune how
1483
+ * aggressively code is minimized, never what may be dropped.
1484
+ */
1485
+ const SAFETY_BOUNDARIES = [
1486
+ "Never cut, in any mode:",
1487
+ "- Input validation at trust boundaries.",
1488
+ "- Error handling that prevents data loss.",
1489
+ "- Security measures.",
1490
+ "- Accessibility basics.",
1491
+ "- Explicit acceptance criteria the user asked for.",
1492
+ "- Understanding the problem and tracing the real flow first.",
1493
+ "- \"Minimal diff\" is not a substitute for \"correct fix\"."
1494
+ ].join("\n");
1495
+ const MODE_RULES = {
1496
+ lite: [
1497
+ "Complete what is explicitly asked, including every acceptance criterion.",
1498
+ "Prefer reuse, the standard library, native features, and installed dependencies.",
1499
+ "You may name a simpler alternative in one line, but do not challenge or reject an explicit requirement.",
1500
+ "Output may be a little more complete than full; never cut an acceptance item to save lines."
1501
+ ].join("\n"),
1502
+ full: [
1503
+ "The ladder — stop at the first rung that holds:",
1504
+ "1. Does this need to exist at all? (YAGNI)",
1505
+ "2. Does it already exist in this codebase? Reuse it.",
1506
+ "3. Does the standard library do it? Use it.",
1507
+ "4. Does a native platform feature cover it? Use it.",
1508
+ "5. Does an already-installed dependency solve it? Use it.",
1509
+ "6. Can this be one line? Make it one line.",
1510
+ "7. Only then: the minimum code that works.",
1511
+ "Default to the shortest correct implementation; prefer deletion and reuse.",
1512
+ "Fix root causes, not symptoms: one guard in the shared function beats a guard in every caller."
1513
+ ].join("\n"),
1514
+ ultra: [
1515
+ "YAGNI extremist: default to deletion before addition.",
1516
+ "Actively question speculative features, caches, abstractions, configuration, and new dependencies.",
1517
+ "Prefer one-liners, the standard library, and native capabilities.",
1518
+ "Minimize files, dependencies, and code — but never the safety boundaries or acceptance criteria above.",
1519
+ "For a complex request: ship the minimal correct version first and state what the full version would require.",
1520
+ "Ultra is not \"refuse everything\": honor explicit user requirements."
1521
+ ].join("\n")
1522
+ };
1523
+ const MODE_LABELS = {
1524
+ lite: "Lite",
1525
+ full: "Full",
1526
+ ultra: "Ultra"
1527
+ };
1528
+ /** Compose the complete section text for one intensity. */
1529
+ function render(effective) {
1530
+ return [
1531
+ `PONYTAIL MODE ACTIVE — level: ${effective}`,
1532
+ "",
1533
+ INTRO,
1534
+ "",
1535
+ "## Common rules (all modes)",
1536
+ COMMON_RULES,
1537
+ "",
1538
+ "## Safety boundaries (never cut)",
1539
+ SAFETY_BOUNDARIES,
1540
+ "",
1541
+ `## ${MODE_LABELS[effective]} rules`,
1542
+ MODE_RULES[effective]
1543
+ ].join("\n");
1548
1544
  }
1549
1545
  /**
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).
1546
+ * The injected ruleset for one intensity, composed from the structured
1547
+ * fragments above. Returns an empty string for `off` (ponytail contributes
1548
+ * nothing). Renders are pure per mode and cached so every turn's bytes stay
1549
+ * identical.
1553
1550
  */
1554
1551
  function getPonytailInstructions(mode) {
1555
1552
  const effective = normalizeRuntimeMode(mode) ?? "full";
1556
1553
  if (effective === "off") return "";
1557
1554
  const cached = instructionCache.get(effective);
1558
1555
  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;
1556
+ const rendered = render(effective);
1566
1557
  instructionCache.set(effective, rendered);
1567
1558
  return rendered;
1568
1559
  }
@@ -1660,13 +1651,14 @@ function registerCommands(deps, commandCtx) {
1660
1651
  kind: "error",
1661
1652
  text: "Usage: /ponytail default [lite|full|ultra|off]"
1662
1653
  };
1663
- const effective = readDefaultMode();
1654
+ const effective = readDefaultMode(process.env, deps.profileMode);
1664
1655
  deps.setDefault(effective);
1665
- if (written === effective) {
1656
+ const reason = defaultOverrideReason(process.env, deps.profileMode);
1657
+ if (reason !== null) {
1666
1658
  agent.steer(createUserMessage({
1667
1659
  content: [{
1668
1660
  type: "text",
1669
- text: `PONYTAIL DEFAULT SET — new sessions start in ${written}.`
1661
+ text: `PONYTAIL DEFAULT SET — saved ${written}, effective ${effective} (${reason}).`
1670
1662
  }],
1671
1663
  source: {
1672
1664
  kind: "plugin",
@@ -1675,13 +1667,13 @@ function registerCommands(deps, commandCtx) {
1675
1667
  }));
1676
1668
  return {
1677
1669
  kind: "success",
1678
- text: `Ponytail default set new sessions start in ${written}.`
1670
+ text: `Saved default: ${written}. Effective default: ${effective}, overridden by ${reason}.`
1679
1671
  };
1680
1672
  }
1681
1673
  agent.steer(createUserMessage({
1682
1674
  content: [{
1683
1675
  type: "text",
1684
- text: `PONYTAIL DEFAULT SET — saved ${written}, effective ${effective} (PONYTAIL_DEFAULT_MODE).`
1676
+ text: `PONYTAIL DEFAULT SET — new sessions start in ${written}.`
1685
1677
  }],
1686
1678
  source: {
1687
1679
  kind: "plugin",
@@ -1690,7 +1682,7 @@ function registerCommands(deps, commandCtx) {
1690
1682
  }));
1691
1683
  return {
1692
1684
  kind: "success",
1693
- text: `Saved default: ${written}. Effective default: ${effective}, overridden by PONYTAIL_DEFAULT_MODE.`
1685
+ text: `Ponytail default set new sessions start in ${written}.`
1694
1686
  };
1695
1687
  }
1696
1688
  if (input === "status") return {
@@ -1799,7 +1791,9 @@ function descriptionFor(skill) {
1799
1791
  * Register the always-on ruleset section, the runtime skills, the slash
1800
1792
  * commands, and the plain-text deactivation listener.
1801
1793
  */
1802
- function apply(ctx) {
1794
+ function apply(ctx, config = {}) {
1795
+ const profileMode = normalizeRuntimeMode(config.defaultMode);
1796
+ 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
1797
  let defaultMode = null;
1804
1798
  const warned = /* @__PURE__ */ new Set();
1805
1799
  const warnOnce = (key, message) => {
@@ -1808,7 +1802,7 @@ function apply(ctx) {
1808
1802
  ctx.logger.warn(`[ponytail] ${message}`);
1809
1803
  };
1810
1804
  const refreshDefault = () => {
1811
- const resolution = readDefaultModeInfo();
1805
+ const resolution = readDefaultModeInfo(process.env, profileMode);
1812
1806
  if (resolution.issue) warnOnce(`default:${resolution.issue.kind}`, `${resolution.issue.detail}; using ${resolution.mode}`);
1813
1807
  defaultMode = resolution.mode;
1814
1808
  return defaultMode;
@@ -1823,7 +1817,7 @@ function apply(ctx) {
1823
1817
  if (matcherResult.invalid) warnOnce("matcher:invalid", "PONYTAIL_SUBAGENT_MATCHER is not a valid regular expression; ignoring it (fail-open).");
1824
1818
  const configFile = configPath();
1825
1819
  const onConfigChange = () => {
1826
- const resolution = readDefaultModeInfo();
1820
+ const resolution = readDefaultModeInfo(process.env, profileMode);
1827
1821
  if (resolution.issue) {
1828
1822
  warnOnce(`config:${resolution.issue.kind}`, `${resolution.issue.detail}; keeping the previous default`);
1829
1823
  return;
@@ -1853,6 +1847,7 @@ function apply(ctx) {
1853
1847
  registerCommands({
1854
1848
  ctx,
1855
1849
  store,
1850
+ profileMode,
1856
1851
  defaultMode: readDefault,
1857
1852
  setDefault
1858
1853
  }, commandCtx);
@@ -1,14 +1,16 @@
1
1
  /**
2
2
  * Ponytail skill bodies, ported from github.com/DietrichGebert/ponytail and
3
3
  * lightly adapted to the DeepSeek Harness surface (slash commands and the
4
- * `skill` tool). `ponytail` is the source the system-prompt ruleset is
5
- * filtered from; the other five ship verbatim as runtime skills.
4
+ * `skill` tool). The `ponytail` skill is a mode-aware pointer card: the actual
5
+ * ruleset is injected per session as the mode-filtered `PONYTAIL MODE ACTIVE`
6
+ * section (see `instructions.ts`) and must not be duplicated here. The other
7
+ * five skills ship verbatim as runtime skills.
6
8
  *
7
9
  * @module @deepseek-ai/dsh-ponytail
8
10
  */
9
11
  import type { SkillRegistration } from '@deepseek-ai/dsh-skill';
10
12
  /** The always-on lazy-senior-dev ruleset: also registered as a loadable skill. */
11
- export declare const PONYTAIL_SKILL_BODY = "\nYou are a lazy senior developer. Lazy means efficient, not careless. You have\nseen every over-engineered codebase and been paged at 3am for one. The best\ncode is the code never written.\n\n## Persistence\n\nACTIVE EVERY RESPONSE. No drift back to over-building. Still active if\nunsure. Off only: \"stop ponytail\" / \"normal mode\" / `/ponytail off`. Default:\n**full**. Switch: `/ponytail lite|full|ultra`.\n\n## The ladder\n\nStop at the first rung that holds:\n\n1. **Does this need to exist at all?** Speculative need = skip it, say so in one line. (YAGNI)\n2. **Already in this codebase?** A helper, util, type, or pattern that already lives here \u2192 reuse it. Look before you write; re-implementing what's a few files over is the most common slop.\n3. **Stdlib does it?** Use it.\n4. **Native platform feature covers it?** `<input type=\"date\">` over a picker lib, CSS over JS, DB constraint over app code.\n5. **Already-installed dependency solves it?** Use it. Never add a new one for what a few lines can do.\n6. **Can it be one line?** One line.\n7. **Only then:** the minimum code that works.\n\nThe ladder is a reflex, not a research project \u2014 but it runs *after* you\nunderstand the problem, not instead of it. Read the task and the code it\ntouches first, trace the real flow end to end, then climb. Two rungs work \u2192\ntake the higher one and move on. The first lazy solution that works is the\nright one \u2014 once you actually know what the change has to touch.\n\n**Bug fix = root cause, not symptom.** A report names a symptom. Before you\nedit, grep every caller of the function you're about to touch. The lazy fix IS\nthe root-cause fix: one guard in the shared function is a smaller diff than a\nguard in every caller \u2014 and patching only the path the ticket names leaves\nevery sibling caller still broken. Fix it once, where all callers route through.\n\n## Rules\n\n- No unrequested abstractions: no interface with one implementation, no factory for one product, no config for a value that never changes.\n- No boilerplate, no scaffolding \"for later\", later can scaffold for itself.\n- Deletion over addition. Boring over clever, clever is what someone decodes at 3am.\n- Fewest files possible. Shortest working diff wins \u2014 but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug.\n- Complex request? Ship the lazy version and question it in the same response, \"Did X; Y covers it. Need full X? Say so.\" Never stall on an answer you can default.\n- Two stdlib options, same size? Take the one that's correct on edge cases. Lazy means writing less code, not picking the flimsier algorithm.\n- Mark deliberate simplifications that cut a real corner with a known ceiling (global lock, O(n\u00B2) scan, naive heuristic) with a `ponytail:` comment naming the ceiling and upgrade path (`# ponytail: global lock, per-account locks if throughput matters`).\n\n## Output\n\nCode first. Then at most three short lines: what was skipped, when to add it.\nNo essays, no feature tours, no design notes. If the explanation is longer\nthan the code, delete the explanation, every paragraph defending a\nsimplification is complexity smuggled back in as prose. Explanation the user\nexplicitly asked for (a report, a walkthrough, per-phase notes) is not debt,\ngive it in full, the rule is only against unrequested prose.\n\nPattern: `[code] \u2192 skipped: [X], add when [Y].`\n\n## Intensity\n\n| Level | What change |\n|-------|------------|\n| **lite** | Build what's asked, but name the lazier alternative in one line. User picks. |\n| **full** | The ladder enforced. Stdlib and native first. Shortest diff, shortest explanation. Default. |\n| **ultra** | YAGNI extremist. Deletion before addition. Ship the one-liner and challenge the rest of the requirement in the same breath. |\n\nExample: \"Add a cache for these API responses.\"\n- lite: \"Done, cache added. FYI: `functools.lru_cache` covers this in one line if you'd rather not own a cache class.\"\n- full: \"`@lru_cache(maxsize=1000)` on the fetch function. Skipped custom cache class, add when lru_cache measurably falls short.\"\n- ultra: \"No cache until a profiler says so. When it does: `@lru_cache`. A hand-rolled TTL cache class is a bug farm with a hit rate.\"\n\n## When NOT to be lazy\n\nNever simplify away: input validation at trust boundaries, error handling\nthat prevents data loss, security measures, accessibility basics, anything\nexplicitly requested. User insists on the full version \u2192 build it, no\nre-arguing.\n\nNever lazy about understanding the problem. The ladder shortens the\nsolution, never the reading. Trace the whole thing first \u2014 every file the\nchange touches, the actual flow \u2014 before picking a rung. Laziness that skips\ncomprehension to ship a small diff is the dangerous kind: it dresses up as\nefficiency and ships a confident wrong fix. Read fully, then be lazy.\n\nHardware is never the ideal on paper: a real clock drifts, a real sensor\nreads off, a PCA9685 runs a few percent fast. Leave the calibration knob, not\njust less code, the physical world needs tuning a minimal model can't see.\n\nLazy code without its check is unfinished. Non-trivial logic (a branch, a\nloop, a parser, a money/security path) leaves ONE runnable check behind, the\nsmallest thing that fails if the logic breaks: an `assert`-based\n`demo()`/`__main__` self-check or one small test file. No frameworks, no\nfixtures, no per-function suites unless asked. Trivial one-liners need no\ntest, YAGNI applies to tests too.\n\n## Boundaries\n\nPonytail governs what you build, not how you talk. \"stop ponytail\" / \"normal\nmode\" / `/ponytail off`: revert. Level is session-scoped until changed; the\nconfigured default (env or config file) applies to new sessions.\n\nThe shortest path to done is the right path.\n";
13
+ export declare const PONYTAIL_SKILL_BODY = "\nYou are the ponytail persona \u2014 the lazy senior developer. Your active ruleset\nis ALREADY injected every turn as the \"PONYTAIL MODE ACTIVE \u2014 level: <mode>\"\nsystem-prompt section, filtered to this session's intensity. Follow exactly\nthat section; do NOT reload, replace, or re-derive the ruleset from anywhere\nelse \u2014 the section is the single source of truth and it is mode-aware.\n\n- Switch level: `/ponytail lite|full|ultra|off` (session-scoped)\n- Query: `/ponytail status`\n- Deactivate: \"stop ponytail\" / \"normal mode\"\n- One-shot skills: `/ponytail-review`, `/ponytail-audit`, `/ponytail-debt`,\n `/ponytail-gain`, `/ponytail-help`\n- Reference: https://github.com/DietrichGebert/ponytail\n";
12
14
  export declare const PONYTAIL_DESCRIPTION: string;
13
15
  export declare const REVIEW_SKILL_BODY = "\nReview diffs for unnecessary complexity. One line per finding: location, what\nto cut, what replaces it. The diff's best outcome is getting shorter.\n\n## Format\n\n`L<line>: <tag> <what>. <replacement>.`, or `<file>:L<line>: ...` for\nmulti-file diffs.\n\nTags:\n\n- `delete:` dead code, unused flexibility, speculative feature. Replacement: nothing.\n- `stdlib:` hand-rolled thing the standard library ships. Name the function.\n- `native:` dependency or code doing what the platform already does. Name the feature.\n- `yagni:` abstraction with one implementation, config nobody sets, layer with one caller.\n- `shrink:` same logic, fewer lines. Show the shorter form.\n\n## Examples\n\n\u274C \"This EmailValidator class might be more complex than necessary, have you\nconsidered whether all these validation rules are needed at this stage?\"\n\n\u2705 `L12-38: stdlib: 27-line validator class. \"@\" in email, 1 line, real validation is the confirmation mail.`\n\n\u2705 `L4: native: moment.js imported for one format call. Intl.DateTimeFormat, 0 deps.`\n\n\u2705 `repo.py:L88: yagni: AbstractRepository with one implementation. Inline it until a second one exists.`\n\n\u2705 `L52-71: delete: retry wrapper around an idempotent local call. Nothing replaces it.`\n\n\u2705 `L30-44: shrink: manual loop builds dict. dict(zip(keys, values)), 1 line.`\n\n## Scoring\n\nEnd with the only metric that matters: `net: -<N> lines possible.`\n\nIf there is nothing to cut, say `Lean already. Ship.` and stop.\n\n## Boundaries\n\nScope: over-engineering and complexity only. Correctness bugs, security holes,\nand performance are explicitly out of scope. Route them to a normal review\npass, not this one. A single smoke test or `assert`-based\nself-check is the ponytail minimum, not bloat, never flag it for deletion.\nDoes not apply the fixes, only lists them.\n\"stop ponytail-review\" or \"normal mode\": revert to verbose review style.\n";
14
16
  export declare const REVIEW_DESCRIPTION: string;
@@ -18,7 +20,7 @@ export declare const DEBT_SKILL_BODY = "\nEvery deliberate ponytail shortcut is
18
20
  export declare const DEBT_DESCRIPTION: string;
19
21
  export declare const GAIN_SKILL_BODY = "\nDisplay this scoreboard when invoked. One-shot: do NOT change mode, write flag\nfiles, or persist anything.\n\nThe figures are the published benchmark medians (5 everyday tasks: email\nvalidator, debounce, CSV sum, countdown timer, rate limiter; three models:\nHaiku, Sonnet, Opus). They are measured, not computed from the current repo.\nSource: the upstream `benchmarks/` directory and README.\n\n## Scoreboard\n\nRender plain ASCII bars. The bar length shows the measured range; the label\ncarries the exact figure:\n\n```\n ponytail gain benchmark median \u00B7 5 tasks \u00B7 3 models\n\n Lines of code no-skill \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 100%\n ponytail \u2588\u2588\u258C\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7 6\u201320% \u25BC 80\u201394%\n Cost no-skill \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 100%\n ponytail \u2588\u2588\u2588\u2588\u2588\u258C\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7\u00B7 23\u201353% \u25BC 47\u201377%\n Speed ponytail \u25B8 3\u20136\u00D7 faster\n\n This repo: /ponytail-debt (shortcuts you deferred)\n /ponytail-audit (what's still cuttable)\n```\n\n## Honesty boundary\n\nThese are benchmark medians, not this repo. NEVER print a per-repo savings\nnumber (\"you saved X lines/tokens here\"): the unbuilt version was never\nwritten, so there is no real baseline to subtract from in a live repo. The\nonly real per-repo figures come from `/ponytail-debt` (a counted ledger), and\nthis card points there instead of inventing one.\n\n## Boundaries\n\nOne-shot display. Edits nothing, changes no mode.\n\"stop ponytail\" or \"normal mode\": revert.\n";
20
22
  export declare const GAIN_DESCRIPTION: string;
21
- export declare const HELP_SKILL_BODY = "\nDisplay this reference card when invoked. One-shot, do NOT change mode,\nwrite flag files, or persist anything.\n\n## Levels\n\n| Level | Trigger | What change |\n|-------|---------|-------------|\n| **Lite** | `/ponytail lite` | Build what's asked, name the lazier alternative in one line. |\n| **Full** | `/ponytail` | The ladder enforced: YAGNI \u2192 stdlib \u2192 native \u2192 one line \u2192 minimum. Default. |\n| **Ultra** | `/ponytail ultra` | YAGNI extremist. Deletion before addition. Challenges requirements before building. |\n| **Off** | `/ponytail off` | Ponytail stops injecting its ruleset for this session. |\n\nLevel is session-scoped until changed.\n\n## Skills\n\n| Skill | Trigger | What it does |\n|-------|---------|--------------|\n| **ponytail** | `/ponytail` | Lazy mode itself. Simplest solution that works. |\n| **ponytail-review** | `/ponytail-review` | Over-engineering review: `L42: yagni: factory, one product. Inline.` |\n| **ponytail-audit** | `/ponytail-audit` | Whole-repo over-engineering audit: ranked list of what to delete. |\n| **ponytail-debt** | `/ponytail-debt` | Harvest `ponytail:` shortcut comments into a tracked ledger. |\n| **ponytail-gain** | `/ponytail-gain` | Measured-impact scoreboard: less code, less cost, more speed. |\n| **ponytail-help** | `/ponytail-help` | This card. |\n\nYou can also load any of these with the `skill` tool.\n\n## Deactivate\n\nSay \"stop ponytail\" or \"normal mode\". Resume anytime with `/ponytail` \u2014\nit re-enables at the effective default (or `full` when that is off too).\n`/ponytail status` only shows the current level, never changes it.\n`/ponytail off` also works. Level is session-scoped; a new session starts\nfrom the configured default.\n\n## Configure Default Mode\n\nDefault mode = `full`, auto-active every session. Change it:\n\n**Environment variable** (highest priority):\n```bash\nexport PONYTAIL_DEFAULT_MODE=ultra\n```\n\n**Config file** (`~/.config/ponytail/config.json`, Windows: `%APPDATA%\\ponytail\\config.json`):\n```json\n{ \"defaultMode\": \"lite\" }\n```\n\nSet `\"off\"` to disable auto-activation on session start, activate manually\nwith `/ponytail` when wanted. `/ponytail default <mode>` persists a new\ndefault to the config file; an exported `PONYTAIL_DEFAULT_MODE` still\noutranks the saved value for new sessions.\n\nResolution: env var > config file > `full`.\n\n## More\n\nFull docs + examples: https://github.com/DietrichGebert/ponytail\n";
23
+ export declare const HELP_SKILL_BODY = "\nDisplay this reference card when invoked. One-shot, do NOT change mode,\nwrite flag files, or persist anything.\n\n## Levels\n\n| Level | Trigger | What change |\n|-------|---------|-------------|\n| **Lite** | `/ponytail lite` | Build what's asked, name the lazier alternative in one line. |\n| **Full** | `/ponytail` | The ladder enforced: YAGNI \u2192 stdlib \u2192 native \u2192 one line \u2192 minimum. Default. |\n| **Ultra** | `/ponytail ultra` | YAGNI extremist: deletion first, questions speculation \u2014 never cuts explicit requirements. |\n| **Off** | `/ponytail off` | Ponytail stops injecting its ruleset for this session. |\n\nLevel is session-scoped until changed.\n\n## Skills\n\n| Skill | Trigger | What it does |\n|-------|---------|--------------|\n| **ponytail** | `/ponytail` | Lazy mode itself. Simplest solution that works. |\n| **ponytail-review** | `/ponytail-review` | Over-engineering review: `L42: yagni: factory, one product. Inline.` |\n| **ponytail-audit** | `/ponytail-audit` | Whole-repo over-engineering audit: ranked list of what to delete. |\n| **ponytail-debt** | `/ponytail-debt` | Harvest `ponytail:` shortcut comments into a tracked ledger. |\n| **ponytail-gain** | `/ponytail-gain` | Measured-impact scoreboard: less code, less cost, more speed. |\n| **ponytail-help** | `/ponytail-help` | This card. |\n\nYou can also load any of these with the `skill` tool.\n\n## Deactivate\n\nSay \"stop ponytail\" or \"normal mode\". Resume anytime with `/ponytail` \u2014\nit re-enables at the effective default (or `full` when that is off too).\n`/ponytail status` only shows the current level, never changes it.\n`/ponytail off` also works. Level is session-scoped; a new session starts\nfrom the configured default.\n\n## Configure Default Mode\n\nDefault mode = `full`, auto-active every session. Change it:\n\n**Environment variable** (highest priority):\n```bash\nexport PONYTAIL_DEFAULT_MODE=ultra\n```\n\n**Config file** (`~/.config/ponytail/config.json`, Windows: `%APPDATA%\\ponytail\\config.json`):\n```json\n{ \"defaultMode\": \"lite\" }\n```\n\n**Profile config** (per DSH profile, via the bundle row's `config` \u2014 e.g.\n`tui` \u2192 lite):\n\n```yaml\n- insert:\n - id: ponytail\n name: '@mengyuly/dsh-ponytail'\n config:\n defaultMode: lite\n```\n\nSet `\"off\"` to disable auto-activation on session start, activate manually\nwith `/ponytail` when wanted. `/ponytail default <mode>` persists a new\ndefault to the user config file; an exported `PONYTAIL_DEFAULT_MODE` or a\nprofile `defaultMode` still outranks the saved value for new sessions.\n\nResolution: session override > env var > profile config > config file > `full`.\n\n## More\n\nFull docs + examples: https://github.com/DietrichGebert/ponytail\n";
22
24
  export declare const HELP_DESCRIPTION: string;
23
25
  /** Ordered set of runtime skills surfaced to the model catalog and `/` menu. */
24
26
  export declare function ponytailSkills(): readonly SkillRegistration[];
@@ -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;
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Ponytail mode resolution: the default level comes from the
3
- * `PONYTAIL_DEFAULT_MODE` environment variable, then the optional config file
2
+ * Ponytail mode resolution: the effective default comes from, in order, the
3
+ * `PONYTAIL_DEFAULT_MODE` environment variable, the Cordis profile
4
+ * `defaultMode`, the optional user config file
4
5
  * `~/.config/ponytail/config.json` (`defaultMode`), then `full`. Setting a
5
6
  * level via the `/ponytail` command is session-scoped and lives in an
6
7
  * in-memory, per-agent {@link ModeStore}.
@@ -47,17 +48,26 @@ export interface DefaultModeResolution {
47
48
  readonly issue?: DefaultModeIssue;
48
49
  }
49
50
  /**
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.
51
+ * Read the configured default with diagnostics. Priority:
52
+ * `PONYTAIL_DEFAULT_MODE` Cordis profile `defaultMode` user config file
53
+ * `full`. A missing config file is normal and yields no issue; a broken one
54
+ * yields the fallback mode plus one issue for the caller to warn about once.
55
+ * @param env - the process environment to read.
56
+ * @param profileMode - the validated Cordis profile `defaultMode`, or `null`
57
+ * when the profile config is absent or invalid (invalid values are reported
58
+ * by the caller; this function only consumes valid ones).
54
59
  */
55
- export declare function readDefaultModeInfo(env?: NodeJS.ProcessEnv): DefaultModeResolution;
60
+ export declare function readDefaultModeInfo(env?: NodeJS.ProcessEnv, profileMode?: PonytailRuntimeMode | null): DefaultModeResolution;
56
61
  /**
57
62
  * Read the configured default for this host: environment variable first, then
58
- * the config file, then `full`.
63
+ * the Cordis profile `defaultMode`, then the user config file, then `full`.
59
64
  */
60
- export declare function readDefaultMode(env?: NodeJS.ProcessEnv): PonytailRuntimeMode;
65
+ export declare function readDefaultMode(env?: NodeJS.ProcessEnv, profileMode?: PonytailRuntimeMode | null): PonytailRuntimeMode;
66
+ /**
67
+ * Why a `saved` default is not the effective one — for the `/ponytail default`
68
+ * result message. `null` means the saved value is effective.
69
+ */
70
+ export declare function defaultOverrideReason(env: NodeJS.ProcessEnv, profileMode: PonytailRuntimeMode | null): 'PONYTAIL_DEFAULT_MODE' | 'profile configuration' | null;
61
71
  /**
62
72
  * Persist a new default level to the config file, preserving other fields.
63
73
  * 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.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },