@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 +53 -0
- package/README.md +48 -10
- package/dist-provenance.json +2 -1
- package/lib/index.js +120 -52
- package/lib/types/index.d.ts +14 -1
- package/lib/types/instructions.d.ts +9 -15
- package/lib/types/modes.d.ts +16 -7
- package/package.json +1 -1
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
|

|
|
4
4
|

|
|
5
5
|
[](https://www.npmjs.com/package/@mengyuly/dsh-ponytail)
|
|
6
|
+
[](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` —
|
|
36
|
-
-
|
|
37
|
-
-
|
|
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
|
-
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
-
|
|
75
|
+
- 常驻注入:**lite ≈ 369 / 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
|
-
-
|
|
81
|
+
- 档位差异在**规则语义**上(见上),三者体积相近(≤ 满档 ×1.25)。
|
|
61
82
|
- 上游 Claude 专属的 statusline 徽标无 DSH 对应物,MCP 服务器因 DSH 有一等 system-prompt 注入点而弃用。
|
|
62
|
-
-
|
|
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
|
|
package/dist-provenance.json
CHANGED
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:
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
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
|
|
1426
|
-
|
|
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
|
-
*
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
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
|
-
|
|
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} (
|
|
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
|
|
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);
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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;
|
package/lib/types/modes.d.ts
CHANGED
|
@@ -47,17 +47,26 @@ export interface DefaultModeResolution {
|
|
|
47
47
|
readonly issue?: DefaultModeIssue;
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
* Read the configured default with diagnostics:
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|