@selesai/code 0.13.35 → 0.13.37
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 +13 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/core/remote-catalog-provider.js +2 -2
- package/dist/core/remote-catalog-provider.test.js +20 -1
- package/dist/extensions/ponytail/index.js +7 -4
- package/dist/extensions/ponytail/ponytail-config.cjs +52 -26
- package/dist/extensions/ponytail/ponytail-instructions.cjs +3 -17
- package/dist/extensions/ponytail/test/extension.test.js +37 -16
- package/dist/extensions/ponytail/test/helpers.test.js +54 -51
- package/dist/skills/ponytail/SKILL.md +5 -7
- package/dist/skills/ponytail-help/SKILL.md +10 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.37] - 2026-09-30
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **Ponytail starts in `lite` by default.** With no `PONYTAIL_DEFAULT_MODE` and no `ponytailMode` in the agent `settings.json`, new sessions and handoffs now start in `lite` instead of `off`. A saved choice still wins: `/ponytail off` (or `/ponytail default off`) keeps it off.
|
|
9
|
+
|
|
10
|
+
## [0.13.36] - 2026-09-29
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Remote model catalogs load again.** The pi.dev catalog request identified itself as `pi/<selesai version>`, and pi.dev answers an unknown Pi version with 404 ("No compatible model catalog is available for this Pi version"). Selesai cached that as an empty catalog, so models newer than the bundled data — such as `openai-codex/gpt-6-sol`, `openai-codex/gpt-6-luna`, and `github-copilot/gpt-6-luna` — showed as `[unavailable]` in the model list even when logged in. The request now carries the upstream Pi platform version (`PI_PLATFORM_VERSION`, the pinned `@earendil-works/pi-ai` release). An existing empty entry in `models-store.json` is refetched after its 4-hour window, or immediately if you delete it.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **Ponytail is off by default and remembers your mode.** Opt in with `/ponytail`. `/ponytail <mode>` saves the choice as `ponytailMode` in the agent `settings.json`, so new sessions and handoffs start in it; other keys are preserved and an unparseable `settings.json` is never overwritten. Modes are `off`, `lite`, and `full` (`ultra` was removed), and every mode now gets upstream's full ponytail prompt.
|
|
17
|
+
|
|
5
18
|
## [0.13.35] - 2026-09-29
|
|
6
19
|
|
|
7
20
|
### Added
|
package/dist/config.d.ts
CHANGED
|
@@ -92,6 +92,8 @@ export declare const APP_NAME: string;
|
|
|
92
92
|
export declare const APP_TITLE: string;
|
|
93
93
|
export declare const CONFIG_DIR_NAME: string;
|
|
94
94
|
export declare const VERSION: string;
|
|
95
|
+
/** Upstream Pi platform version (the pinned pi-ai release). pi.dev gates model catalogs on this, not on VERSION. */
|
|
96
|
+
export declare const PI_PLATFORM_VERSION: string;
|
|
95
97
|
export declare const ENV_AGENT_DIR: string;
|
|
96
98
|
export declare const ENV_SESSION_DIR: string;
|
|
97
99
|
export declare function expandTildePath(path: string): string;
|
package/dist/config.js
CHANGED
|
@@ -463,6 +463,8 @@ export const APP_NAME = piConfigName || "pi";
|
|
|
463
463
|
export const APP_TITLE = piConfigName ? APP_NAME : "π";
|
|
464
464
|
export const CONFIG_DIR_NAME = pkg.piConfig?.configDir || ".pi";
|
|
465
465
|
export const VERSION = pkg.version || "0.0.0";
|
|
466
|
+
/** Upstream Pi platform version (the pinned pi-ai release). pi.dev gates model catalogs on this, not on VERSION. */
|
|
467
|
+
export const PI_PLATFORM_VERSION = pkg.dependencies?.["@earendil-works/pi-ai"]?.replace(/^[^\d]*/, "") || VERSION;
|
|
466
468
|
// e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR
|
|
467
469
|
export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
|
|
468
470
|
export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_SESSION_DIR`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PI_PLATFORM_VERSION } from "../config.js";
|
|
2
2
|
import { getPiUserAgent } from "../utils/pi-user-agent.js";
|
|
3
3
|
const DEFAULT_CATALOG_BASE_URL = "https://pi.dev";
|
|
4
4
|
export const REMOTE_CATALOG_REFRESH_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
@@ -70,7 +70,7 @@ export function withRemoteCatalog(provider, catalogBaseUrl = DEFAULT_CATALOG_BAS
|
|
|
70
70
|
const response = await fetch(url, {
|
|
71
71
|
headers: {
|
|
72
72
|
accept: "application/json",
|
|
73
|
-
"User-Agent": getPiUserAgent(
|
|
73
|
+
"User-Agent": getPiUserAgent(PI_PLATFORM_VERSION),
|
|
74
74
|
...(validator ? { "if-none-match": validator } : {}),
|
|
75
75
|
},
|
|
76
76
|
signal: context.signal,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { PI_PLATFORM_VERSION, VERSION } from "../config.js";
|
|
2
3
|
import { withRemoteCatalog } from "./remote-catalog-provider.js";
|
|
3
4
|
const cachedModel = {
|
|
4
5
|
id: "gpt-6-sol",
|
|
@@ -26,4 +27,22 @@ describe("remote model catalog cache", () => {
|
|
|
26
27
|
await wrapped.refreshModels?.(refreshContext({ models: [cachedModel], checkedAt: localGeneratedAt + 1, lastModified: 0 }));
|
|
27
28
|
expect(wrapped.getModels().map((model) => model.id)).toEqual(["gpt-6-sol"]);
|
|
28
29
|
});
|
|
30
|
+
// pi.dev 404s catalogs for unknown Pi versions; selesai's own VERSION is not a Pi version.
|
|
31
|
+
it("identifies as the upstream Pi platform version, not the selesai version", async () => {
|
|
32
|
+
expect(PI_PLATFORM_VERSION).not.toBe(VERSION);
|
|
33
|
+
const fetchMock = vi.fn(async () => new Response("{}", { status: 200 }));
|
|
34
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
35
|
+
try {
|
|
36
|
+
const provider = { id: "openai-codex", getModels: () => [] };
|
|
37
|
+
await withRemoteCatalog(provider, "https://example.test").refreshModels?.({
|
|
38
|
+
...refreshContext(undefined),
|
|
39
|
+
allowNetwork: true,
|
|
40
|
+
});
|
|
41
|
+
const headers = fetchMock.mock.calls[0][1].headers;
|
|
42
|
+
expect(headers["User-Agent"]).toMatch(new RegExp(`^pi/${PI_PLATFORM_VERSION.replaceAll(".", "\\.")} `));
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
vi.unstubAllGlobals();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
29
48
|
});
|
|
@@ -68,6 +68,12 @@ export default function ponytailExtension(pi) {
|
|
|
68
68
|
|
|
69
69
|
currentMode = normalized;
|
|
70
70
|
pi.appendEntry("ponytail-mode", { mode: normalized });
|
|
71
|
+
// Remember the choice for future sessions (new chats, handoffs) too.
|
|
72
|
+
try {
|
|
73
|
+
if (writeDefaultMode(normalized)) configuredDefaultMode = getDefaultMode();
|
|
74
|
+
} catch (_) {
|
|
75
|
+
// ponytail: unwritable config just means this session-only; not worth failing the command.
|
|
76
|
+
}
|
|
71
77
|
ctx?.ui?.notify?.(`Ponytail mode set to ${normalized}.`, "info");
|
|
72
78
|
};
|
|
73
79
|
|
|
@@ -168,9 +174,6 @@ export default function ponytailExtension(pi) {
|
|
|
168
174
|
// Guard a null/undefined event or a missing systemPrompt: don't crash, and
|
|
169
175
|
// don't prepend the literal string "undefined" to the prompt (#439, #440).
|
|
170
176
|
const base = event?.systemPrompt ? `${event.systemPrompt}\n\n` : "";
|
|
171
|
-
|
|
172
|
-
// ruleset to avoid repeating the same system prompt on every request.
|
|
173
|
-
const compact = currentMode !== "full";
|
|
174
|
-
return { systemPrompt: `${base}${getPonytailInstructions(currentMode, { compact })}` };
|
|
177
|
+
return { systemPrompt: `${base}${getPonytailInstructions(currentMode)}` };
|
|
175
178
|
});
|
|
176
179
|
}
|
|
@@ -3,19 +3,20 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Resolution order for default mode:
|
|
5
5
|
// 1. PONYTAIL_DEFAULT_MODE environment variable
|
|
6
|
-
// 2.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
// 2. "ponytailMode" in the agent settings.json
|
|
7
|
+
// ($SELESAI_CODING_AGENT_DIR/settings.json, else ~/.selesai/agent/settings.json)
|
|
8
|
+
// 3. 'lite' (built-in default)
|
|
9
|
+
//
|
|
10
|
+
// quietStartup still lives in the standalone ponytail config.json (getConfigPath).
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const os = require('os');
|
|
15
15
|
|
|
16
|
-
const DEFAULT_MODE = '
|
|
17
|
-
const VALID_MODES = ['off', 'lite', 'full', '
|
|
18
|
-
const RUNTIME_MODES = ['off', 'lite', 'full'
|
|
16
|
+
const DEFAULT_MODE = 'lite';
|
|
17
|
+
const VALID_MODES = ['off', 'lite', 'full', 'review'];
|
|
18
|
+
const RUNTIME_MODES = ['off', 'lite', 'full'];
|
|
19
|
+
const SETTINGS_KEY = 'ponytailMode';
|
|
19
20
|
|
|
20
21
|
function normalizeMode(mode) {
|
|
21
22
|
if (typeof mode !== 'string') return null;
|
|
@@ -73,26 +74,49 @@ function getClaudeDir() {
|
|
|
73
74
|
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// ponytail: mirrors getAgentDir() in src/config.ts with the "selesai" app name
|
|
78
|
+
// hardcoded (this CJS file can't import the TS config). Revisit if the fork is renamed.
|
|
79
|
+
function getSettingsPath() {
|
|
80
|
+
const envDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
81
|
+
const agentDir = envDir
|
|
82
|
+
? envDir.replace(/^~(?=$|[\\/])/, os.homedir())
|
|
83
|
+
: path.join(os.homedir(), '.selesai', 'agent');
|
|
84
|
+
return path.join(agentDir, 'settings.json');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Returns {} when the file is missing; throws on unreadable/invalid JSON so a
|
|
88
|
+
// write never replaces a settings file we couldn't parse.
|
|
89
|
+
function readSettings() {
|
|
90
|
+
let raw;
|
|
91
|
+
try {
|
|
92
|
+
raw = fs.readFileSync(getSettingsPath(), 'utf8');
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (e.code === 'ENOENT') return {};
|
|
95
|
+
throw e;
|
|
96
|
+
}
|
|
97
|
+
const parsed = JSON.parse(raw.replace(/^\uFEFF/, ''));
|
|
98
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
99
|
+
throw new Error('settings.json is not a JSON object');
|
|
100
|
+
}
|
|
101
|
+
return parsed;
|
|
102
|
+
}
|
|
103
|
+
|
|
76
104
|
function getDefaultMode() {
|
|
77
105
|
// 1. Environment variable (highest priority)
|
|
78
106
|
const envMode = process.env.PONYTAIL_DEFAULT_MODE;
|
|
79
|
-
// ponytail: a default must be a runtime level (off/lite/full
|
|
107
|
+
// ponytail: a default must be a runtime level (off/lite/full); review is
|
|
80
108
|
// a session-only mode, never a valid default (#377). Validate against
|
|
81
109
|
// RUNTIME_MODES so a stray env var or config can't make review the default.
|
|
82
110
|
if (envMode && RUNTIME_MODES.includes(envMode.toLowerCase())) {
|
|
83
111
|
return envMode.toLowerCase();
|
|
84
112
|
}
|
|
85
113
|
|
|
86
|
-
// 2.
|
|
114
|
+
// 2. settings.json
|
|
87
115
|
try {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8').replace(/^\uFEFF/, ''));
|
|
91
|
-
if (config.defaultMode && RUNTIME_MODES.includes(config.defaultMode.toLowerCase())) {
|
|
92
|
-
return config.defaultMode.toLowerCase();
|
|
93
|
-
}
|
|
116
|
+
const mode = normalizeMode(readSettings()[SETTINGS_KEY]);
|
|
117
|
+
if (mode) return mode;
|
|
94
118
|
} catch (e) {
|
|
95
|
-
//
|
|
119
|
+
// Unreadable or invalid settings — fall through
|
|
96
120
|
}
|
|
97
121
|
|
|
98
122
|
// 3. Default
|
|
@@ -121,15 +145,16 @@ function writeDefaultMode(mode) {
|
|
|
121
145
|
const normalized = normalizeMode(mode);
|
|
122
146
|
if (!normalized) return null;
|
|
123
147
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
fs.
|
|
148
|
+
// Keep every other key; readSettings throws rather than letting us clobber a
|
|
149
|
+
// file we couldn't parse.
|
|
150
|
+
// ponytail: no lock shared with SettingsManager. Its saves re-read the file and
|
|
151
|
+
// merge, so our key survives; only a same-millisecond concurrent write could race.
|
|
152
|
+
const settingsPath = getSettingsPath();
|
|
153
|
+
const settings = readSettings();
|
|
154
|
+
if (settings[SETTINGS_KEY] === normalized) return normalized;
|
|
155
|
+
settings[SETTINGS_KEY] = normalized;
|
|
156
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
157
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
133
158
|
return normalized;
|
|
134
159
|
}
|
|
135
160
|
|
|
@@ -140,6 +165,7 @@ module.exports = {
|
|
|
140
165
|
getDefaultMode,
|
|
141
166
|
getConfigDir,
|
|
142
167
|
getConfigPath,
|
|
168
|
+
getSettingsPath,
|
|
143
169
|
getClaudeDir,
|
|
144
170
|
getQuietStartup,
|
|
145
171
|
isShellSafe,
|
|
@@ -6,11 +6,6 @@ const path = require('path');
|
|
|
6
6
|
const { DEFAULT_MODE, normalizeMode, normalizePersistedMode } = require('./ponytail-config.cjs');
|
|
7
7
|
|
|
8
8
|
const INDEPENDENT_MODES = new Set(['review']);
|
|
9
|
-
const COMPACT_MODE_RULES = {
|
|
10
|
-
lite: 'Build the ask; name a lazier alternative.',
|
|
11
|
-
full: 'Enforce ladder; shortest diff.',
|
|
12
|
-
ultra: 'YAGNI first; delete; challenge the rest.',
|
|
13
|
-
};
|
|
14
9
|
const SKILL_PATH = path.join(__dirname, '..', '..', 'skills', 'ponytail', 'SKILL.md');
|
|
15
10
|
|
|
16
11
|
function filterSkillBodyForMode(body, mode) {
|
|
@@ -18,7 +13,7 @@ function filterSkillBodyForMode(body, mode) {
|
|
|
18
13
|
const withoutFrontmatter = String(body || '').replace(/^---[\s\S]*?---\s*/, '');
|
|
19
14
|
|
|
20
15
|
// Only the intensity table rows and worked examples are mode-specific, and
|
|
21
|
-
// both are keyed by a mode name (lite/full
|
|
16
|
+
// both are keyed by a mode name (lite/full). A bullet whose label is
|
|
22
17
|
// not a mode — e.g. "No unrequested abstractions: ..." — is a normal rule
|
|
23
18
|
// and must be kept verbatim.
|
|
24
19
|
return withoutFrontmatter
|
|
@@ -50,7 +45,7 @@ function getFallbackInstructions(mode) {
|
|
|
50
45
|
'You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.\n\n' +
|
|
51
46
|
'## Persistence\n\n' +
|
|
52
47
|
'ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure. Off only: "stop ponytail" / "normal mode".\n\n' +
|
|
53
|
-
'Current level: **' + mode + '**. Switch: `/ponytail lite|full
|
|
48
|
+
'Current level: **' + mode + '**. Switch: `/ponytail off|lite|full`.\n\n' +
|
|
54
49
|
'## The ladder\n\n' +
|
|
55
50
|
'Before 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):\n' +
|
|
56
51
|
'1. Does this need to be built at all? (YAGNI)\n' +
|
|
@@ -79,15 +74,7 @@ function getFallbackInstructions(mode) {
|
|
|
79
74
|
'Ponytail governs what you build, not how you talk. "stop ponytail" or "normal mode": revert. Level persists until changed or session end.';
|
|
80
75
|
}
|
|
81
76
|
|
|
82
|
-
function
|
|
83
|
-
return 'PONYTAIL ' + mode.toUpperCase() + ': persistent every response; off only "stop ponytail"/"normal mode". ' +
|
|
84
|
-
'Trace first. Ladder: YAGNI -> reuse -> stdlib/native -> installed deps -> minimum diff. ' +
|
|
85
|
-
'Fix root cause, not caller symptom. No unrequested abstractions/deps/boilerplate; delete first. ' +
|
|
86
|
-
'Preserve validation, data-loss handling, security, accessibility, explicit asks. ' +
|
|
87
|
-
'Non-trivial logic: one runnable check. Code first; <=3 lines. ' + COMPACT_MODE_RULES[mode];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function getPonytailInstructions(mode, options) {
|
|
77
|
+
function getPonytailInstructions(mode) {
|
|
91
78
|
const configuredMode = normalizePersistedMode(mode) || DEFAULT_MODE;
|
|
92
79
|
|
|
93
80
|
if (INDEPENDENT_MODES.has(configuredMode)) {
|
|
@@ -95,7 +82,6 @@ function getPonytailInstructions(mode, options) {
|
|
|
95
82
|
}
|
|
96
83
|
|
|
97
84
|
const effectiveMode = normalizeMode(configuredMode) || DEFAULT_MODE;
|
|
98
|
-
if (options?.compact === true) return getCompactInstructions(effectiveMode);
|
|
99
85
|
|
|
100
86
|
try {
|
|
101
87
|
return 'PONYTAIL MODE ACTIVE — level: ' + effectiveMode + '\n\n' +
|
|
@@ -42,13 +42,17 @@ function createCommandContext(overrides = {}) {
|
|
|
42
42
|
function withTempConfig(fn) {
|
|
43
43
|
const tempConfigHome = mkdtempSync(join(tmpdir(), "ponytail-test-"));
|
|
44
44
|
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
45
|
+
const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
45
46
|
process.env.XDG_CONFIG_HOME = tempConfigHome;
|
|
47
|
+
process.env.SELESAI_CODING_AGENT_DIR = tempConfigHome;
|
|
46
48
|
|
|
47
49
|
return Promise.resolve()
|
|
48
50
|
.then(fn)
|
|
49
51
|
.finally(() => {
|
|
50
52
|
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
51
53
|
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
54
|
+
if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
55
|
+
else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
|
|
52
56
|
rmSync(tempConfigHome, { recursive: true, force: true });
|
|
53
57
|
});
|
|
54
58
|
}
|
|
@@ -65,15 +69,32 @@ describe("ponytail extension", () => {
|
|
|
65
69
|
const ctx = createCommandContext();
|
|
66
70
|
|
|
67
71
|
await events.get("session_start")({ reason: "startup" }, ctx);
|
|
68
|
-
await commands.get("ponytail").handler("
|
|
72
|
+
await commands.get("ponytail").handler("lite", ctx);
|
|
69
73
|
|
|
70
74
|
expect(appendedEntries.at(-1)).toEqual({
|
|
71
75
|
customType: "ponytail-mode",
|
|
72
|
-
data: { mode: "
|
|
76
|
+
data: { mode: "lite" },
|
|
73
77
|
});
|
|
74
78
|
|
|
75
79
|
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
76
|
-
expect(result.systemPrompt).toMatch(/^BASE\n\nPONYTAIL
|
|
80
|
+
expect(result.systemPrompt).toMatch(/^BASE\n\nPONYTAIL MODE ACTIVE — level: lite\n/);
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
it("/ponytail <mode> carries into brand-new sessions", async () => withTempConfig(async () => {
|
|
84
|
+
const first = createPiHarness();
|
|
85
|
+
await first.events.get("session_start")({ reason: "startup" }, createCommandContext());
|
|
86
|
+
await first.commands.get("ponytail").handler("lite", createCommandContext());
|
|
87
|
+
|
|
88
|
+
// Fresh extension instance + empty session, like a new chat or handoff.
|
|
89
|
+
const next = createPiHarness();
|
|
90
|
+
await next.events.get("session_start")({ reason: "new" }, createCommandContext());
|
|
91
|
+
const result = await next.events.get("before_agent_start")({ systemPrompt: "BASE" }, createCommandContext());
|
|
92
|
+
expect(result.systemPrompt).toMatch(/PONYTAIL MODE ACTIVE — level: lite\n/);
|
|
93
|
+
|
|
94
|
+
await next.commands.get("ponytail").handler("off", createCommandContext());
|
|
95
|
+
const after = createPiHarness();
|
|
96
|
+
await after.events.get("session_start")({ reason: "new" }, createCommandContext());
|
|
97
|
+
expect(await after.events.get("before_agent_start")({ systemPrompt: "BASE" }, createCommandContext())).toBeUndefined();
|
|
77
98
|
}));
|
|
78
99
|
|
|
79
100
|
it("session_start restores latest persisted mode", async () => withTempConfig(async () => {
|
|
@@ -89,7 +110,7 @@ describe("ponytail extension", () => {
|
|
|
89
110
|
await events.get("session_start")({ reason: "resume" }, ctx);
|
|
90
111
|
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
91
112
|
|
|
92
|
-
expect(result.systemPrompt).toMatch(/PONYTAIL
|
|
113
|
+
expect(result.systemPrompt).toMatch(/PONYTAIL MODE ACTIVE — level: lite\n/);
|
|
93
114
|
}));
|
|
94
115
|
|
|
95
116
|
it("review mode retains its independent skill prompt", async () => withTempConfig(async () => {
|
|
@@ -140,7 +161,7 @@ describe("ponytail extension", () => {
|
|
|
140
161
|
const ctx = createCommandContext();
|
|
141
162
|
|
|
142
163
|
await events.get("session_start")({ reason: "startup" }, ctx);
|
|
143
|
-
await commands.get("ponytail").handler("
|
|
164
|
+
await commands.get("ponytail").handler("lite", ctx);
|
|
144
165
|
await events.get("input")({ text: "normal mode", source: "interactive" }, ctx);
|
|
145
166
|
|
|
146
167
|
const disabled = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
@@ -152,11 +173,11 @@ describe("ponytail extension", () => {
|
|
|
152
173
|
const ctx = createCommandContext();
|
|
153
174
|
|
|
154
175
|
await events.get("session_start")({ reason: "startup" }, ctx);
|
|
155
|
-
await commands.get("ponytail").handler("
|
|
176
|
+
await commands.get("ponytail").handler("lite", ctx);
|
|
156
177
|
await events.get("input")({ text: "add a normal mode toggle next to dark mode", source: "interactive" }, ctx);
|
|
157
178
|
|
|
158
179
|
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
159
|
-
expect(result.systemPrompt).toMatch(/PONYTAIL
|
|
180
|
+
expect(result.systemPrompt).toMatch(/PONYTAIL MODE ACTIVE — level: lite\n/);
|
|
160
181
|
}));
|
|
161
182
|
|
|
162
183
|
it("never writes a Ponytail statusline", async () => withTempConfig(async () => {
|
|
@@ -179,7 +200,7 @@ describe("ponytail extension", () => {
|
|
|
179
200
|
|
|
180
201
|
await events.get("session_start")({ reason: "startup" }, ctx);
|
|
181
202
|
await commands.get("ponytail").handler("status", ctx);
|
|
182
|
-
expect(notifications.at(-1).msg).toContain("Ponytail: current
|
|
203
|
+
expect(notifications.at(-1).msg).toContain("Ponytail: current lite • default lite");
|
|
183
204
|
}));
|
|
184
205
|
|
|
185
206
|
it("/ponytail set-default persists the default mode", async () => withTempConfig(async () => {
|
|
@@ -194,7 +215,7 @@ describe("ponytail extension", () => {
|
|
|
194
215
|
|
|
195
216
|
it("/ponytail set-default with env override reports the override", async () => withTempConfig(async () => {
|
|
196
217
|
const previous = process.env.PONYTAIL_DEFAULT_MODE;
|
|
197
|
-
process.env.PONYTAIL_DEFAULT_MODE = "
|
|
218
|
+
process.env.PONYTAIL_DEFAULT_MODE = "full";
|
|
198
219
|
try {
|
|
199
220
|
const { commands, events } = createPiHarness();
|
|
200
221
|
const notifications = [];
|
|
@@ -202,7 +223,7 @@ describe("ponytail extension", () => {
|
|
|
202
223
|
|
|
203
224
|
await events.get("session_start")({ reason: "startup" }, ctx);
|
|
204
225
|
await commands.get("ponytail").handler("default lite", ctx);
|
|
205
|
-
expect(notifications.at(-1).msg).toContain("env override keeps default at
|
|
226
|
+
expect(notifications.at(-1).msg).toContain("env override keeps default at full");
|
|
206
227
|
} finally {
|
|
207
228
|
if (previous === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
208
229
|
else process.env.PONYTAIL_DEFAULT_MODE = previous;
|
|
@@ -224,11 +245,11 @@ describe("ponytail extension", () => {
|
|
|
224
245
|
const ctx = createCommandContext();
|
|
225
246
|
|
|
226
247
|
await events.get("session_start")({ reason: "startup" }, ctx);
|
|
227
|
-
await commands.get("ponytail").handler("
|
|
248
|
+
await commands.get("ponytail").handler("lite", ctx);
|
|
228
249
|
await events.get("input")({ text: "normal mode", source: "extension" }, ctx);
|
|
229
250
|
|
|
230
251
|
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
231
|
-
expect(result.systemPrompt).toMatch(/PONYTAIL
|
|
252
|
+
expect(result.systemPrompt).toMatch(/PONYTAIL MODE ACTIVE — level: lite\n/);
|
|
232
253
|
}));
|
|
233
254
|
|
|
234
255
|
it("setMode ignores invalid modes", async () => withTempConfig(async () => {
|
|
@@ -252,7 +273,7 @@ describe("ponytail extension", () => {
|
|
|
252
273
|
|
|
253
274
|
await events.get("session_start")({ reason: "resume" }, ctx);
|
|
254
275
|
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
255
|
-
expect(result.systemPrompt).toMatch(/PONYTAIL
|
|
276
|
+
expect(result.systemPrompt).toMatch(/PONYTAIL MODE ACTIVE — level: lite\n/);
|
|
256
277
|
}));
|
|
257
278
|
|
|
258
279
|
it("session_start tolerates a missing sessionManager", async () => withTempConfig(async () => {
|
|
@@ -261,7 +282,7 @@ describe("ponytail extension", () => {
|
|
|
261
282
|
|
|
262
283
|
await events.get("session_start")({ reason: "resume" }, ctx);
|
|
263
284
|
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
264
|
-
expect(result.systemPrompt).toMatch(/PONYTAIL MODE ACTIVE — level:
|
|
285
|
+
expect(result.systemPrompt).toMatch(/PONYTAIL MODE ACTIVE — level: lite\n/); // default is lite
|
|
265
286
|
}));
|
|
266
287
|
|
|
267
288
|
it("input handler tolerates missing text", async () => withTempConfig(async () => {
|
|
@@ -269,11 +290,11 @@ describe("ponytail extension", () => {
|
|
|
269
290
|
const ctx = createCommandContext();
|
|
270
291
|
|
|
271
292
|
await events.get("session_start")({ reason: "startup" }, ctx);
|
|
272
|
-
await commands.get("ponytail").handler("
|
|
293
|
+
await commands.get("ponytail").handler("lite", ctx);
|
|
273
294
|
await events.get("input")({ source: "interactive" }, ctx);
|
|
274
295
|
|
|
275
296
|
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
276
|
-
expect(result.systemPrompt).toMatch(/PONYTAIL
|
|
297
|
+
expect(result.systemPrompt).toMatch(/PONYTAIL MODE ACTIVE — level: lite\n/);
|
|
277
298
|
}));
|
|
278
299
|
|
|
279
300
|
it("setMode notifies when ctx has a UI", async () => withTempConfig(async () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
@@ -21,11 +21,11 @@ describe("ponytail helpers", () => {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it("parsePonytailCommand falls back to DEFAULT_MODE for invalid defaults", () => {
|
|
24
|
-
expect(parsePonytailCommand("", "bogus")).toEqual({ type: "set-mode", mode: "
|
|
24
|
+
expect(parsePonytailCommand("", "bogus")).toEqual({ type: "set-mode", mode: "lite" });
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
it("parsePonytailCommand parses modes, status, and default subcommand", () => {
|
|
28
|
-
expect(parsePonytailCommand("
|
|
28
|
+
expect(parsePonytailCommand("lite", "full")).toEqual({ type: "set-mode", mode: "lite" });
|
|
29
29
|
expect(parsePonytailCommand("status", "full")).toEqual({ type: "status" });
|
|
30
30
|
expect(parsePonytailCommand("default lite", "full")).toEqual({ type: "set-default", mode: "lite" });
|
|
31
31
|
});
|
|
@@ -42,44 +42,59 @@ describe("ponytail helpers", () => {
|
|
|
42
42
|
it("resolveSessionMode prefers latest persisted session mode", () => {
|
|
43
43
|
const entries = [
|
|
44
44
|
{ type: "custom", customType: "ponytail-mode", data: { mode: "lite" } },
|
|
45
|
-
{ type: "custom", customType: "ponytail-mode", data: { mode: "
|
|
45
|
+
{ type: "custom", customType: "ponytail-mode", data: { mode: "lite" } },
|
|
46
46
|
];
|
|
47
47
|
|
|
48
|
-
expect(resolveSessionMode(entries, "full")).toBe("
|
|
48
|
+
expect(resolveSessionMode(entries, "full")).toBe("lite");
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
it("resolveSessionMode returns fallback when entries is not an array", () => {
|
|
52
|
-
expect(resolveSessionMode(null, "
|
|
52
|
+
expect(resolveSessionMode(null, "lite")).toBe("lite");
|
|
53
53
|
expect(resolveSessionMode(undefined, "lite")).toBe("lite");
|
|
54
54
|
expect(resolveSessionMode({}, "full")).toBe("full");
|
|
55
|
-
expect(resolveSessionMode("not an array")).toBe("
|
|
55
|
+
expect(resolveSessionMode("not an array")).toBe("lite"); // DEFAULT_MODE fallback
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
it("resolveSessionMode ignores non-ponytail entries and invalid modes", () => {
|
|
59
59
|
const entries = [
|
|
60
|
-
{ type: "custom", customType: "other", data: { mode: "
|
|
60
|
+
{ type: "custom", customType: "other", data: { mode: "lite" } },
|
|
61
61
|
{ type: "custom", customType: "ponytail-mode", data: { mode: "bogus" } },
|
|
62
62
|
];
|
|
63
63
|
expect(resolveSessionMode(entries, "lite")).toBe("lite");
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
it("readDefaultMode and writeDefaultMode use
|
|
66
|
+
it("readDefaultMode and writeDefaultMode use ponytailMode in agent settings.json", () => {
|
|
67
67
|
const tempDir = mkdtempSync(join(tmpdir(), "ponytail-config-"));
|
|
68
|
-
const
|
|
68
|
+
const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
69
69
|
const previousDefault = process.env.PONYTAIL_DEFAULT_MODE;
|
|
70
|
-
const
|
|
71
|
-
process.env.
|
|
70
|
+
const settingsPath = join(tempDir, "settings.json");
|
|
71
|
+
process.env.SELESAI_CODING_AGENT_DIR = tempDir;
|
|
72
72
|
delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
73
73
|
|
|
74
74
|
try {
|
|
75
|
-
expect(readDefaultMode()).toBe("
|
|
76
|
-
expect(writeDefaultMode("
|
|
77
|
-
expect(readDefaultMode()).toBe("
|
|
78
|
-
expect(
|
|
79
|
-
|
|
75
|
+
expect(readDefaultMode()).toBe("lite");
|
|
76
|
+
expect(writeDefaultMode("lite")).toBe("lite");
|
|
77
|
+
expect(readDefaultMode()).toBe("lite");
|
|
78
|
+
expect(JSON.parse(readFileSync(settingsPath, "utf8"))).toEqual({ ponytailMode: "lite" });
|
|
79
|
+
|
|
80
|
+
// Other settings are preserved.
|
|
81
|
+
writeFileSync(settingsPath, JSON.stringify({ theme: "dark", ponytailMode: "lite" }));
|
|
82
|
+
writeDefaultMode("full");
|
|
83
|
+
expect(JSON.parse(readFileSync(settingsPath, "utf8"))).toEqual({ theme: "dark", ponytailMode: "full" });
|
|
84
|
+
|
|
85
|
+
// Removed/unknown modes are ignored, not honored.
|
|
86
|
+
writeFileSync(settingsPath, JSON.stringify({ ponytailMode: "ultra" }));
|
|
87
|
+
expect(readDefaultMode()).toBe("lite");
|
|
88
|
+
expect(writeDefaultMode("ultra")).toBeNull();
|
|
89
|
+
|
|
90
|
+
// An unparseable settings.json is never overwritten.
|
|
91
|
+
writeFileSync(settingsPath, "{ not json");
|
|
92
|
+
expect(() => writeDefaultMode("lite")).toThrow();
|
|
93
|
+
expect(readFileSync(settingsPath, "utf8")).toBe("{ not json");
|
|
94
|
+
expect(readDefaultMode()).toBe("lite");
|
|
80
95
|
} finally {
|
|
81
|
-
if (
|
|
82
|
-
else process.env.
|
|
96
|
+
if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
97
|
+
else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
|
|
83
98
|
if (previousDefault === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
84
99
|
else process.env.PONYTAIL_DEFAULT_MODE = previousDefault;
|
|
85
100
|
rmSync(tempDir, { recursive: true, force: true });
|
|
@@ -97,11 +112,11 @@ describe("ponytail helpers", () => {
|
|
|
97
112
|
}
|
|
98
113
|
});
|
|
99
114
|
|
|
100
|
-
it("readDefaultMode falls back to
|
|
115
|
+
it("readDefaultMode falls back to lite when env is invalid", () => {
|
|
101
116
|
const previous = process.env.PONYTAIL_DEFAULT_MODE;
|
|
102
117
|
process.env.PONYTAIL_DEFAULT_MODE = "bogus";
|
|
103
118
|
try {
|
|
104
|
-
expect(readDefaultMode()).toBe("
|
|
119
|
+
expect(readDefaultMode()).toBe("lite");
|
|
105
120
|
} finally {
|
|
106
121
|
if (previous === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
107
122
|
else process.env.PONYTAIL_DEFAULT_MODE = previous;
|
|
@@ -113,45 +128,33 @@ describe("ponytail helpers", () => {
|
|
|
113
128
|
});
|
|
114
129
|
|
|
115
130
|
it("filterSkillBodyForMode keeps only requested intensity examples and rows", () => {
|
|
116
|
-
const body = `---\nname: ponytail\n---\n| **lite** | keep lite |\n| **full** | keep full |\n
|
|
131
|
+
const body = `---\nname: ponytail\n---\n| **lite** | keep lite |\n| **full** | keep full |\n- lite: \"Lite example\"\n- full: \"Full example\"\nOther line`;
|
|
117
132
|
|
|
118
|
-
const filtered = filterSkillBodyForMode(body, "
|
|
133
|
+
const filtered = filterSkillBodyForMode(body, "lite");
|
|
119
134
|
|
|
120
|
-
expect(filtered).
|
|
135
|
+
expect(filtered).toContain("keep lite");
|
|
121
136
|
expect(filtered).not.toContain("keep full");
|
|
122
|
-
expect(filtered).toContain("
|
|
123
|
-
expect(filtered).not.toContain("
|
|
124
|
-
expect(filtered).toContain("Ultra example");
|
|
137
|
+
expect(filtered).toContain("Lite example");
|
|
138
|
+
expect(filtered).not.toContain("Full example");
|
|
125
139
|
expect(filtered).toContain("Other line");
|
|
126
140
|
});
|
|
127
141
|
|
|
128
|
-
it("
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
expect(compact.lite).toMatch(/Build the ask; name a lazier alternative/);
|
|
143
|
-
expect(compact.full).toMatch(/Enforce ladder; shortest diff/);
|
|
144
|
-
expect(compact.ultra).toMatch(/YAGNI first; delete; challenge the rest/);
|
|
145
|
-
|
|
146
|
-
const fullSkill = getPonytailInstructions("full");
|
|
147
|
-
expect(fullSkill).toMatch(/## The ladder/);
|
|
148
|
-
expect(fullSkill.length).toBeGreaterThan(compact.full.length);
|
|
142
|
+
it("every mode gets the full skill prompt, filtered to its own intensity row", () => {
|
|
143
|
+
const lite = getPonytailInstructions("lite");
|
|
144
|
+
const full = getPonytailInstructions("full");
|
|
145
|
+
|
|
146
|
+
expect(lite).toMatch(/^PONYTAIL MODE ACTIVE — level: lite\n\n/);
|
|
147
|
+
expect(full).toMatch(/^PONYTAIL MODE ACTIVE — level: full\n\n/);
|
|
148
|
+
for (const text of [lite, full]) expect(text).toMatch(/## The ladder/);
|
|
149
|
+
expect(lite).toMatch(/\| \*\*lite\*\* \|/);
|
|
150
|
+
expect(lite).not.toMatch(/\| \*\*full\*\* \|/);
|
|
151
|
+
expect(full).toMatch(/\| \*\*full\*\* \|/);
|
|
152
|
+
expect(full).not.toMatch(/\| \*\*lite\*\* \|/);
|
|
149
153
|
});
|
|
150
154
|
|
|
151
|
-
it("review instructions remain independent
|
|
155
|
+
it("review instructions remain independent", () => {
|
|
152
156
|
const expected = "PONYTAIL MODE ACTIVE — level: review. Behavior defined by /ponytail-review skill.";
|
|
153
157
|
expect(getPonytailInstructions("review")).toBe(expected);
|
|
154
|
-
expect(getPonytailInstructions("review", { compact: true })).toBe(expected);
|
|
155
158
|
});
|
|
156
159
|
|
|
157
160
|
it("filterSkillBodyForMode keeps rule bullets that contain a colon", () => {
|
|
@@ -184,7 +187,7 @@ describe("ponytail helpers", () => {
|
|
|
184
187
|
});
|
|
185
188
|
|
|
186
189
|
it("getPonytailInstructions falls back when the skill file is missing", () => {
|
|
187
|
-
const instructions = getPonytailInstructions("full"
|
|
190
|
+
const instructions = getPonytailInstructions("full");
|
|
188
191
|
// With the real skill file present this returns the file-based instructions;
|
|
189
192
|
// the fallback path is exercised by deleting the skill file temporarily.
|
|
190
193
|
expect(instructions.length).toBeGreaterThan(0);
|
|
@@ -6,7 +6,7 @@ description: >
|
|
|
6
6
|
minimal. Channels a senior dev who has seen everything: question whether the
|
|
7
7
|
task needs to exist at all (YAGNI), reach for the standard library before
|
|
8
8
|
custom code, native platform features before dependencies, one line before
|
|
9
|
-
fifty. Supports intensity levels: lite, full
|
|
9
|
+
fifty. Supports intensity levels: lite, full. Use on ANY
|
|
10
10
|
coding task: writing, adding, refactoring, fixing, reviewing, or designing
|
|
11
11
|
code, and choosing libraries or dependencies. Also use whenever the user
|
|
12
12
|
says "ponytail", "be lazy", "lazy mode", "simplest solution", "minimal
|
|
@@ -14,7 +14,7 @@ description: >
|
|
|
14
14
|
over-engineering, bloat, boilerplate, or unnecessary dependencies. Do NOT
|
|
15
15
|
use for non-coding requests (general knowledge, prose, translation,
|
|
16
16
|
summaries, recipes).
|
|
17
|
-
argument-hint: "[lite|full
|
|
17
|
+
argument-hint: "[off|lite|full]"
|
|
18
18
|
license: MIT
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -27,8 +27,8 @@ code is the code never written.
|
|
|
27
27
|
## Persistence
|
|
28
28
|
|
|
29
29
|
ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if
|
|
30
|
-
unsure. Off only: "stop ponytail" / "normal mode".
|
|
31
|
-
Switch: `/ponytail lite|full
|
|
30
|
+
unsure. Off only: "stop ponytail" / "normal mode".
|
|
31
|
+
Switch: `/ponytail off|lite|full` (your choice is remembered across sessions).
|
|
32
32
|
|
|
33
33
|
## The ladder
|
|
34
34
|
|
|
@@ -80,13 +80,11 @@ Pattern: `[code] → skipped: [X], add when [Y].`
|
|
|
80
80
|
| Level | What change |
|
|
81
81
|
|-------|------------|
|
|
82
82
|
| **lite** | Build what's asked, but name the lazier alternative in one line. User picks. |
|
|
83
|
-
| **full** | The ladder enforced. Stdlib and native first. Shortest diff, shortest explanation.
|
|
84
|
-
| **ultra** | YAGNI extremist. Deletion before addition. Ship the one-liner and challenge the rest of the requirement in the same breath. |
|
|
83
|
+
| **full** | The ladder enforced. Stdlib and native first. Shortest diff, shortest explanation. |
|
|
85
84
|
|
|
86
85
|
Example: "Add a cache for these API responses."
|
|
87
86
|
- lite: "Done, cache added. FYI: `functools.lru_cache` covers this in one line if you'd rather not own a cache class."
|
|
88
87
|
- full: "`@lru_cache(maxsize=1000)` on the fetch function. Skipped custom cache class, add when lru_cache measurably falls short."
|
|
89
|
-
- 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."
|
|
90
88
|
|
|
91
89
|
## When NOT to be lazy
|
|
92
90
|
|
|
@@ -17,10 +17,10 @@ write flag files, or persist anything.
|
|
|
17
17
|
| Level | Trigger | What change |
|
|
18
18
|
|-------|---------|-------------|
|
|
19
19
|
| **Lite** | `/ponytail lite` | Build what's asked, name the lazier alternative in one line. |
|
|
20
|
-
| **Full** | `/ponytail` | The ladder enforced: YAGNI → stdlib → native → one line → minimum.
|
|
21
|
-
| **
|
|
20
|
+
| **Full** | `/ponytail` or `/ponytail full` | The ladder enforced: YAGNI → stdlib → native → one line → minimum. |
|
|
21
|
+
| **Off** | `/ponytail off` | Ponytail disabled. |
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Your choice is remembered: new sessions (and handoffs) start in the last mode you picked.
|
|
24
24
|
|
|
25
25
|
## Skills
|
|
26
26
|
|
|
@@ -44,20 +44,16 @@ Say "stop ponytail" or "normal mode". Resume anytime with `/ponytail`.
|
|
|
44
44
|
|
|
45
45
|
## Configure Default Mode
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
**Environment variable** (highest priority):
|
|
50
|
-
```bash
|
|
51
|
-
export PONYTAIL_DEFAULT_MODE=ultra
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
**Config file** (`~/.config/ponytail/config.json`, Windows: `%APPDATA%\ponytail\config.json`):
|
|
47
|
+
Off until you pick a mode. Picking one with `/ponytail <mode>` saves it to the
|
|
48
|
+
agent `settings.json` (`~/.selesai/agent/settings.json`):
|
|
55
49
|
```json
|
|
56
|
-
{ "
|
|
50
|
+
{ "ponytailMode": "lite" }
|
|
57
51
|
```
|
|
58
52
|
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
**Environment variable** (overrides settings.json):
|
|
54
|
+
```bash
|
|
55
|
+
export PONYTAIL_DEFAULT_MODE=lite
|
|
56
|
+
```
|
|
61
57
|
|
|
62
58
|
Resolution: env var > config file > `full`.
|
|
63
59
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selesai/code",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.37",
|
|
4
4
|
"description": "Maintained, extension-first Pi coding agent with built-in workflows, subagents, web research, questions, skills, and an enhanced terminal UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|