@selesai/code 0.1.2 β 0.1.4
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/dist/agents/architect.md +1 -1
- package/dist/agents/builder.md +1 -1
- package/dist/agents/commentator.md +2 -1
- package/dist/agents/explorer.md +1 -1
- package/dist/agents/recapper.md +2 -1
- package/dist/config.d.ts +9 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +37 -2
- package/dist/config.js.map +1 -1
- package/dist/defaults/models.json +88 -2
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/ponytail-pi-extension/index.js +189 -0
- package/dist/extensions/ponytail-pi-extension/package.json +8 -0
- package/dist/extensions/ponytail-pi-extension/test/extension.test.js +167 -0
- package/dist/extensions/ponytail-pi-extension/test/helpers.test.js +92 -0
- package/dist/extensions/prototype.ts +19 -9
- package/dist/extensions/tokenin-onboarding.ts +183 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -3
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const {
|
|
5
|
+
DEFAULT_MODE,
|
|
6
|
+
getDefaultMode,
|
|
7
|
+
normalizeMode,
|
|
8
|
+
normalizeConfigMode,
|
|
9
|
+
normalizePersistedMode,
|
|
10
|
+
isDeactivationCommand,
|
|
11
|
+
writeDefaultMode,
|
|
12
|
+
} = require("../hooks/ponytail-config.js");
|
|
13
|
+
const { getPonytailInstructions, filterSkillBodyForMode } = require("../hooks/ponytail-instructions.js");
|
|
14
|
+
|
|
15
|
+
export { filterSkillBodyForMode };
|
|
16
|
+
export const readDefaultMode = getDefaultMode;
|
|
17
|
+
|
|
18
|
+
export function resolveSessionMode(entries, fallbackMode = DEFAULT_MODE) {
|
|
19
|
+
const fallback = normalizePersistedMode(fallbackMode) || DEFAULT_MODE;
|
|
20
|
+
if (!Array.isArray(entries)) return fallback;
|
|
21
|
+
|
|
22
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
23
|
+
const entry = entries[i];
|
|
24
|
+
if (entry?.type !== "custom" || entry?.customType !== "ponytail-mode") continue;
|
|
25
|
+
|
|
26
|
+
const mode = normalizePersistedMode(entry?.data?.mode);
|
|
27
|
+
if (mode) return mode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parsePonytailCommand(text, defaultMode = DEFAULT_MODE) {
|
|
34
|
+
const fallback = normalizePersistedMode(defaultMode) || DEFAULT_MODE;
|
|
35
|
+
const normalizedText = String(text || "").trim().toLowerCase();
|
|
36
|
+
|
|
37
|
+
if (!normalizedText) {
|
|
38
|
+
return { type: "set-mode", mode: fallback === "off" ? "full" : fallback };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const [primary, secondary] = normalizedText.split(/\s+/);
|
|
42
|
+
|
|
43
|
+
if (primary === "status") return { type: "status" };
|
|
44
|
+
|
|
45
|
+
if (primary === "default") {
|
|
46
|
+
const mode = normalizeConfigMode(secondary);
|
|
47
|
+
return mode ? { type: "set-default", mode } : { type: "invalid", reason: "invalid-default-mode" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const mode = normalizeMode(primary);
|
|
51
|
+
return mode ? { type: "set-mode", mode } : { type: "invalid", reason: "invalid-mode", mode: primary };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { writeDefaultMode };
|
|
55
|
+
|
|
56
|
+
export default function ponytailExtension(pi) {
|
|
57
|
+
let currentMode = DEFAULT_MODE;
|
|
58
|
+
let configuredDefaultMode = getDefaultMode();
|
|
59
|
+
let isActive = false;
|
|
60
|
+
let lastCtx = null;
|
|
61
|
+
|
|
62
|
+
// -- Status bar --
|
|
63
|
+
function syncStatus(ctx) {
|
|
64
|
+
if (ctx) lastCtx = ctx;
|
|
65
|
+
const c = ctx || lastCtx;
|
|
66
|
+
if (!c?.ui?.setStatus || !c.ui.theme?.fg) return;
|
|
67
|
+
const theme = c.ui.theme;
|
|
68
|
+
if (currentMode === "off") {
|
|
69
|
+
c.ui.setStatus("ponytail", "");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const levelIcons = { lite: "πΏ", full: "β‘", ultra: "π₯" };
|
|
73
|
+
const icon = levelIcons[currentMode] || "";
|
|
74
|
+
const label = currentMode.toUpperCase();
|
|
75
|
+
const indicator = isActive ? theme.fg("accent", "β") : theme.fg("dim", "β");
|
|
76
|
+
c.ui.setStatus("ponytail", indicator + " π΄ " + theme.fg("muted", "ponytail: ") + theme.fg("text", icon + " " + label));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const setMode = (mode, ctx) => {
|
|
80
|
+
const normalized = normalizePersistedMode(mode);
|
|
81
|
+
if (!normalized) return;
|
|
82
|
+
|
|
83
|
+
currentMode = normalized;
|
|
84
|
+
pi.appendEntry("ponytail-mode", { mode: normalized });
|
|
85
|
+
syncStatus(ctx);
|
|
86
|
+
ctx?.ui?.notify?.(`Ponytail mode set to ${normalized}.`, "info");
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const sendAlias = (skillName, args, ctx) => {
|
|
90
|
+
const normalized = String(args || "").trim();
|
|
91
|
+
const message = normalized ? `${skillName} ${normalized}` : skillName;
|
|
92
|
+
|
|
93
|
+
if (ctx?.isIdle?.() === false) {
|
|
94
|
+
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
95
|
+
ctx?.ui?.notify?.(`${skillName} queued as follow-up.`, "info");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pi.sendUserMessage(message);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
pi.registerCommand("ponytail", {
|
|
103
|
+
description: "Set or report Ponytail mode",
|
|
104
|
+
handler: async (args, ctx) => {
|
|
105
|
+
const parsed = parsePonytailCommand(args, configuredDefaultMode);
|
|
106
|
+
|
|
107
|
+
if (parsed.type === "status") {
|
|
108
|
+
ctx?.ui?.notify?.(`Ponytail: current ${currentMode} β’ default ${configuredDefaultMode}`, "info");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (parsed.type === "set-default") {
|
|
113
|
+
const written = writeDefaultMode(parsed.mode);
|
|
114
|
+
if (written) {
|
|
115
|
+
configuredDefaultMode = getDefaultMode();
|
|
116
|
+
const message = configuredDefaultMode === written
|
|
117
|
+
? `Default Ponytail mode set to ${written}.`
|
|
118
|
+
: `Saved default ${written}, but env override keeps default at ${configuredDefaultMode}.`;
|
|
119
|
+
ctx?.ui?.notify?.(message, "info");
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (parsed.type === "set-mode") {
|
|
125
|
+
setMode(parsed.mode, ctx);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
ctx?.ui?.notify?.("Unknown or unsupported /ponytail mode.", "warning");
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
pi.registerCommand("ponytail-review", {
|
|
134
|
+
description: "Run /skill:ponytail-review",
|
|
135
|
+
handler: (_args, ctx) => sendAlias("/skill:ponytail-review", "", ctx),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
pi.registerCommand("ponytail-audit", {
|
|
139
|
+
description: "Run /skill:ponytail-audit",
|
|
140
|
+
handler: (_args, ctx) => sendAlias("/skill:ponytail-audit", "", ctx),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
pi.registerCommand("ponytail-gain", {
|
|
144
|
+
description: "Run /skill:ponytail-gain",
|
|
145
|
+
handler: (_args, ctx) => sendAlias("/skill:ponytail-gain", "", ctx),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
pi.registerCommand("ponytail-debt", {
|
|
149
|
+
description: "Run /skill:ponytail-debt",
|
|
150
|
+
handler: (_args, ctx) => sendAlias("/skill:ponytail-debt", "", ctx),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
pi.registerCommand("ponytail-help", {
|
|
154
|
+
description: "Run /skill:ponytail-help",
|
|
155
|
+
handler: (_args, ctx) => sendAlias("/skill:ponytail-help", "", ctx),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
pi.on("input", async (event) => {
|
|
159
|
+
if (event?.source === "extension") return;
|
|
160
|
+
|
|
161
|
+
const text = String(event?.text || "");
|
|
162
|
+
if (currentMode !== "off" && isDeactivationCommand(text)) {
|
|
163
|
+
setMode("off");
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
168
|
+
const entries = ctx?.sessionManager?.getBranch?.() || ctx?.sessionManager?.getEntries?.() || [];
|
|
169
|
+
configuredDefaultMode = getDefaultMode();
|
|
170
|
+
currentMode = resolveSessionMode(entries, configuredDefaultMode);
|
|
171
|
+
syncStatus(ctx);
|
|
172
|
+
ctx?.ui?.notify?.(`Ponytail loaded: ${currentMode}`, "info");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
176
|
+
isActive = true;
|
|
177
|
+
syncStatus(ctx);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
181
|
+
isActive = false;
|
|
182
|
+
syncStatus(ctx);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
pi.on("before_agent_start", async (event) => {
|
|
186
|
+
if (!currentMode || currentMode === "off") return;
|
|
187
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${getPonytailInstructions(currentMode)}` };
|
|
188
|
+
});
|
|
189
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import ponytailExtension from "../index.js";
|
|
8
|
+
|
|
9
|
+
function createPiHarness() {
|
|
10
|
+
const events = new Map();
|
|
11
|
+
const commands = new Map();
|
|
12
|
+
const appendedEntries = [];
|
|
13
|
+
const sentUserMessages = [];
|
|
14
|
+
|
|
15
|
+
const pi = {
|
|
16
|
+
on(eventName, handler) {
|
|
17
|
+
events.set(eventName, handler);
|
|
18
|
+
},
|
|
19
|
+
registerCommand(name, options) {
|
|
20
|
+
commands.set(name, options);
|
|
21
|
+
},
|
|
22
|
+
appendEntry(customType, data) {
|
|
23
|
+
appendedEntries.push({ customType, data });
|
|
24
|
+
},
|
|
25
|
+
sendUserMessage(text, options) {
|
|
26
|
+
sentUserMessages.push({ text, options });
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
ponytailExtension(pi);
|
|
31
|
+
return { events, commands, appendedEntries, sentUserMessages };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createCommandContext(overrides = {}) {
|
|
35
|
+
return {
|
|
36
|
+
isIdle: () => true,
|
|
37
|
+
sessionManager: { getEntries: () => [] },
|
|
38
|
+
ui: { notify() {} },
|
|
39
|
+
...overrides,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function withTempConfig(fn) {
|
|
44
|
+
const tempConfigHome = mkdtempSync(join(tmpdir(), "ponytail-test-"));
|
|
45
|
+
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
46
|
+
process.env.XDG_CONFIG_HOME = tempConfigHome;
|
|
47
|
+
|
|
48
|
+
return Promise.resolve()
|
|
49
|
+
.then(fn)
|
|
50
|
+
.finally(() => {
|
|
51
|
+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
52
|
+
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
53
|
+
rmSync(tempConfigHome, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test("extension registers Ponytail commands", () => {
|
|
58
|
+
const { commands } = createPiHarness();
|
|
59
|
+
|
|
60
|
+
assert.deepEqual([...commands.keys()].sort(), ["ponytail", "ponytail-audit", "ponytail-debt", "ponytail-gain", "ponytail-help", "ponytail-review"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("/ponytail updates session mode and injects instructions", async () => withTempConfig(async () => {
|
|
64
|
+
const { commands, events, appendedEntries } = createPiHarness();
|
|
65
|
+
const ctx = createCommandContext();
|
|
66
|
+
|
|
67
|
+
await events.get("session_start")({ reason: "startup" }, ctx);
|
|
68
|
+
await commands.get("ponytail").handler("ultra", ctx);
|
|
69
|
+
|
|
70
|
+
assert.deepEqual(appendedEntries.at(-1), {
|
|
71
|
+
customType: "ponytail-mode",
|
|
72
|
+
data: { mode: "ultra" },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
76
|
+
assert.ok(result.systemPrompt.includes("PONYTAIL MODE ACTIVE"));
|
|
77
|
+
assert.ok(result.systemPrompt.includes("ultra"));
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
test("session_start restores latest persisted mode", async () => withTempConfig(async () => {
|
|
81
|
+
const { events } = createPiHarness();
|
|
82
|
+
const ctx = createCommandContext({
|
|
83
|
+
sessionManager: {
|
|
84
|
+
getEntries: () => [
|
|
85
|
+
{ type: "custom", customType: "ponytail-mode", data: { mode: "lite" } },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await events.get("session_start")({ reason: "resume" }, ctx);
|
|
91
|
+
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
92
|
+
|
|
93
|
+
assert.ok(result.systemPrompt.includes("lite"));
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
test("skill alias commands delegate to Pi skill commands", async () => {
|
|
97
|
+
const { commands, sentUserMessages } = createPiHarness();
|
|
98
|
+
const ctx = createCommandContext();
|
|
99
|
+
|
|
100
|
+
await commands.get("ponytail-review").handler("", ctx);
|
|
101
|
+
await commands.get("ponytail-audit").handler("", ctx);
|
|
102
|
+
await commands.get("ponytail-debt").handler("", ctx);
|
|
103
|
+
await commands.get("ponytail-gain").handler("", ctx);
|
|
104
|
+
await commands.get("ponytail-help").handler("", ctx);
|
|
105
|
+
|
|
106
|
+
assert.deepEqual(sentUserMessages.map((entry) => entry.text), [
|
|
107
|
+
"/skill:ponytail-review",
|
|
108
|
+
"/skill:ponytail-audit",
|
|
109
|
+
"/skill:ponytail-debt",
|
|
110
|
+
"/skill:ponytail-gain",
|
|
111
|
+
"/skill:ponytail-help",
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("normal mode disables persistent instructions", async () => withTempConfig(async () => {
|
|
116
|
+
const { commands, events } = createPiHarness();
|
|
117
|
+
const ctx = createCommandContext();
|
|
118
|
+
|
|
119
|
+
await events.get("session_start")({ reason: "startup" }, ctx);
|
|
120
|
+
await commands.get("ponytail").handler("ultra", ctx);
|
|
121
|
+
await events.get("input")({ text: "normal mode", source: "interactive" }, ctx);
|
|
122
|
+
|
|
123
|
+
const disabled = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
124
|
+
assert.equal(disabled, undefined);
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
test("a request mentioning normal mode stays active", async () => withTempConfig(async () => {
|
|
128
|
+
const { commands, events } = createPiHarness();
|
|
129
|
+
const ctx = createCommandContext();
|
|
130
|
+
|
|
131
|
+
await events.get("session_start")({ reason: "startup" }, ctx);
|
|
132
|
+
await commands.get("ponytail").handler("ultra", ctx);
|
|
133
|
+
await events.get("input")({ text: "add a normal mode toggle next to dark mode", source: "interactive" }, ctx);
|
|
134
|
+
|
|
135
|
+
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
136
|
+
assert.match(result.systemPrompt, /PONYTAIL MODE ACTIVE/);
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
test("status bar renders the mode and flips active on agent_start", async () => withTempConfig(async () => {
|
|
140
|
+
const { events } = createPiHarness();
|
|
141
|
+
const statusWrites = [];
|
|
142
|
+
const ctx = createCommandContext({
|
|
143
|
+
sessionManager: { getEntries: () => [{ type: "custom", customType: "ponytail-mode", data: { mode: "ultra" } }] },
|
|
144
|
+
ui: { notify() {}, setStatus: (key, text) => statusWrites.push({ key, text }), theme: { fg: (_color, text) => text } },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await events.get("session_start")({ reason: "resume" }, ctx);
|
|
148
|
+
await events.get("agent_start")({}, ctx);
|
|
149
|
+
|
|
150
|
+
assert.equal(statusWrites.at(-2).key, "ponytail");
|
|
151
|
+
assert.match(statusWrites.at(-2).text, /β.*ULTRA/);
|
|
152
|
+
assert.match(statusWrites.at(-1).text, /β.*ULTRA/);
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
test("status bar stays silent when ui lacks a theme", async () => withTempConfig(async () => {
|
|
156
|
+
const { events } = createPiHarness();
|
|
157
|
+
const calls = [];
|
|
158
|
+
const ctx = createCommandContext({
|
|
159
|
+
sessionManager: { getEntries: () => [{ type: "custom", customType: "ponytail-mode", data: { mode: "ultra" } }] },
|
|
160
|
+
ui: { notify() {}, setStatus: (_key, text) => calls.push(text) }, // setStatus present, theme absent
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await events.get("session_start")({ reason: "resume" }, ctx);
|
|
164
|
+
await events.get("agent_start")({}, ctx);
|
|
165
|
+
|
|
166
|
+
assert.deepEqual(calls, []);
|
|
167
|
+
}));
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
filterSkillBodyForMode,
|
|
9
|
+
parsePonytailCommand,
|
|
10
|
+
readDefaultMode,
|
|
11
|
+
resolveSessionMode,
|
|
12
|
+
writeDefaultMode,
|
|
13
|
+
} from "../index.js";
|
|
14
|
+
|
|
15
|
+
test("parsePonytailCommand falls back to full when invoked bare and default is off", () => {
|
|
16
|
+
assert.deepEqual(parsePonytailCommand("", "off"), { type: "set-mode", mode: "full" });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("parsePonytailCommand parses modes, status, and default subcommand", () => {
|
|
20
|
+
assert.deepEqual(parsePonytailCommand("ultra", "full"), { type: "set-mode", mode: "ultra" });
|
|
21
|
+
assert.deepEqual(parsePonytailCommand("status", "full"), { type: "status" });
|
|
22
|
+
assert.deepEqual(parsePonytailCommand("default lite", "full"), { type: "set-default", mode: "lite" });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("resolveSessionMode prefers latest persisted session mode", () => {
|
|
26
|
+
const entries = [
|
|
27
|
+
{ type: "custom", customType: "ponytail-mode", data: { mode: "lite" } },
|
|
28
|
+
{ type: "custom", customType: "ponytail-mode", data: { mode: "ultra" } },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
assert.equal(resolveSessionMode(entries, "full"), "ultra");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("resolveSessionMode returns fallback when entries is not an array", () => {
|
|
35
|
+
assert.equal(resolveSessionMode(null, "ultra"), "ultra");
|
|
36
|
+
assert.equal(resolveSessionMode(undefined, "lite"), "lite");
|
|
37
|
+
assert.equal(resolveSessionMode({}, "full"), "full");
|
|
38
|
+
assert.equal(resolveSessionMode("not an array"), "full"); // DEFAULT_MODE fallback
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("readDefaultMode and writeDefaultMode use XDG config path", () => {
|
|
42
|
+
const tempDir = mkdtempSync(join(tmpdir(), "ponytail-config-"));
|
|
43
|
+
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
44
|
+
const previousDefault = process.env.PONYTAIL_DEFAULT_MODE;
|
|
45
|
+
const configPath = join(tempDir, "ponytail", "config.json");
|
|
46
|
+
process.env.XDG_CONFIG_HOME = tempDir;
|
|
47
|
+
delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
assert.equal(readDefaultMode(), "full");
|
|
51
|
+
assert.equal(writeDefaultMode("ultra"), "ultra");
|
|
52
|
+
assert.equal(readDefaultMode(), "ultra");
|
|
53
|
+
assert.ok(existsSync(configPath));
|
|
54
|
+
assert.deepEqual(JSON.parse(readFileSync(configPath, "utf8")), { defaultMode: "ultra" });
|
|
55
|
+
} finally {
|
|
56
|
+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
57
|
+
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
58
|
+
if (previousDefault === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
59
|
+
else process.env.PONYTAIL_DEFAULT_MODE = previousDefault;
|
|
60
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("filterSkillBodyForMode keeps only requested intensity examples and rows", () => {
|
|
65
|
+
const body = `---\nname: ponytail\n---\n| **lite** | keep lite |\n| **full** | keep full |\n| **ultra** | keep ultra |\n- lite: Lite example\n- full: Full example\n- ultra: Ultra example\nOther line`;
|
|
66
|
+
|
|
67
|
+
const filtered = filterSkillBodyForMode(body, "ultra");
|
|
68
|
+
|
|
69
|
+
assert.ok(!filtered.includes("keep lite"));
|
|
70
|
+
assert.ok(!filtered.includes("keep full"));
|
|
71
|
+
assert.ok(filtered.includes("keep ultra"));
|
|
72
|
+
assert.ok(!filtered.includes("Lite example"));
|
|
73
|
+
assert.ok(filtered.includes("Ultra example"));
|
|
74
|
+
assert.ok(filtered.includes("Other line"));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("filterSkillBodyForMode keeps rule bullets that contain a colon", () => {
|
|
78
|
+
// Regression: rule bullets outside the Intensity section (e.g. the
|
|
79
|
+
// "No unrequested abstractions:" rule or the `ponytail:` comment convention)
|
|
80
|
+
// contain a colon and must not be mistaken for mode-example lines.
|
|
81
|
+
const skillPath = new URL("../../skills/ponytail/SKILL.md", import.meta.url);
|
|
82
|
+
const body = readFileSync(skillPath, "utf8");
|
|
83
|
+
|
|
84
|
+
const filtered = filterSkillBodyForMode(body, "full");
|
|
85
|
+
|
|
86
|
+
assert.ok(filtered.includes("No unrequested abstractions"));
|
|
87
|
+
assert.ok(filtered.includes("Mark deliberate simplifications"));
|
|
88
|
+
// The Intensity examples are still filtered down to the active mode.
|
|
89
|
+
assert.ok(filtered.includes('full: "`@lru_cache'));
|
|
90
|
+
assert.ok(!filtered.includes('lite: "Done'));
|
|
91
|
+
assert.ok(!filtered.includes('ultra: "No cache'));
|
|
92
|
+
});
|
|
@@ -264,7 +264,7 @@ ${userPrompt}
|
|
|
264
264
|
|
|
265
265
|
The purpose of this phase is NOT to start solving β it is to INTERVIEW the user until the requirements are unambiguous. You are the interviewer: drive the conversation. Ask focused clarifying questions ONE at a time, and adapt each question to what the user reveals. Dig into scope, constraints, success criteria, edge cases, and anything ambiguous. Use your own judgement to decide what still needs clarifying β do not follow a fixed checklist. Provide rich context in your questions so the user understands why each matters.
|
|
266
266
|
|
|
267
|
-
When you and the user have reached shared understanding, write a concise requirements summary to ${artifactDir}/requirements.md. The workflow advances automatically once requirements.md exists.`;
|
|
267
|
+
When you and the user have reached shared understanding, DO NOT write the file yet. First show the user a concise draft of the requirements summary and ask explicitly whether they approve committing it, want to keep grilling, or just remembered something to add. Only once the user explicitly approves, write the approved summary to ${artifactDir}/requirements.md. The workflow advances automatically once requirements.md exists.`;
|
|
268
268
|
case "research":
|
|
269
269
|
return `You are entering the RESEARCH phase. This phase is OPTIONAL.
|
|
270
270
|
|
|
@@ -282,7 +282,9 @@ The workflow advances automatically once research.md exists.`;
|
|
|
282
282
|
|
|
283
283
|
This plan is for the ACTUAL prototype/output the user wants β not a plan for research. Based on ${artifactDir}/requirements.md (and ${artifactDir}/research.md if it contains real research), produce the concrete build plan: what to build, how, in what order, which components, and what the finished prototype looks like.
|
|
284
284
|
|
|
285
|
-
If research was skipped, that is fine β proceed directly. You already understand the user's intent from grilling.
|
|
285
|
+
If research was skipped, that is fine β proceed directly. You already understand the user's intent from grilling.
|
|
286
|
+
|
|
287
|
+
Spawn the ARCHITECT SUB-AGENT to produce the plan. Use the task tool with subagent_type "architect". Craft a tailored prompt from ${artifactDir}/requirements.md and ${artifactDir}/research.md so the architect knows exactly what to plan for THIS task. The architect writes to ${artifactDir}/plan.md.
|
|
286
288
|
|
|
287
289
|
The workflow advances automatically once plan.md exists.`;
|
|
288
290
|
case "reuse":
|
|
@@ -290,7 +292,7 @@ The workflow advances automatically once plan.md exists.`;
|
|
|
290
292
|
|
|
291
293
|
Purpose: explore the existing codebase for reusable components, patterns, and knowledge this prototype should build on, so you do not reinvent what already exists.
|
|
292
294
|
|
|
293
|
-
Spawn an EXPLORER SUB-AGENT to do the exploration. Use the task tool with subagent_type "
|
|
295
|
+
Spawn an EXPLORER SUB-AGENT to do the exploration. Use the task tool with subagent_type "explorer". Do NOT send a generic exploration prompt β CRAFT a specific prompt tailored to THIS task: from ${artifactDir}/requirements.md and ${artifactDir}/plan.md, tell the explorer exactly which areas of the codebase, which patterns, and which dependencies are relevant. For example, if the user wants Tailwind, have the explorer determine how Tailwind is already set up and used here and what conventions to follow. Instruct the explorer to write its findings to ${artifactDir}/reuse.md.
|
|
294
296
|
|
|
295
297
|
When the explorer returns, synthesize its findings into ${artifactDir}/reuse.md: what is reusable, where, and how the prototype should leverage it.
|
|
296
298
|
|
|
@@ -306,15 +308,15 @@ Draw from ALL prior phases:
|
|
|
306
308
|
- ${artifactDir}/plan.md (the build plan)
|
|
307
309
|
- ${artifactDir}/reuse.md (codebase exploration findings)
|
|
308
310
|
|
|
309
|
-
|
|
311
|
+
Spawn the RECAPPER SUB-AGENT to compile the handoff. Use the task tool with subagent_type "recapper". Craft a tailored prompt that points the recapper at all four artifact files and tells it what the prototype is about, so it can write a coherent handoff tailored to this task. The recapper writes to ${artifactDir}/handoff.md.
|
|
310
312
|
|
|
311
313
|
The workflow advances automatically once handoff.md exists.`;
|
|
312
314
|
case "loop":
|
|
313
315
|
return `You are entering the LOOP (orchestration) phase. Your job is to DELEGATE the implementation to sub-agents β do not implement the code yourself.
|
|
314
316
|
|
|
315
317
|
Read ${artifactDir}/plan.md, ${artifactDir}/handoff.md, ${artifactDir}/research.md, and ${artifactDir}/reuse.md. Using that full context, GENERATE YOUR OWN delegation prompts (do not use a static template):
|
|
316
|
-
1. Dispatch ONE builder sub-agent (task tool, subagent_type "
|
|
317
|
-
2. Dispatch ONE commentator sub-agent (
|
|
318
|
+
1. Dispatch ONE builder sub-agent (task tool, subagent_type "builder") with a prompt YOU craft: give it the plan + handoff + any research/reuse context it needs, tailored to this specific task, and instruct it to implement every task in plan.md in order. All code changes go in the workspace, never in ${artifactDir}.
|
|
319
|
+
2. Dispatch ONE commentator sub-agent (task tool, subagent_type "commentator") to review the builder's diff against plan.md. Generate the review prompt YOURSELF based on what matters for this task.
|
|
318
320
|
3. If the commentator reports blocking issues, dispatch the builder again with the issues to fix, then re-run the commentator. Repeat until no issues.
|
|
319
321
|
4. Write ${artifactDir}/loop-complete.md with a one-line summary of the finished plan.
|
|
320
322
|
|
|
@@ -322,11 +324,19 @@ Generate the prompts with full awareness of the task β tailored, not generic.
|
|
|
322
324
|
case "audit":
|
|
323
325
|
return `You are entering the AUDIT phase.
|
|
324
326
|
|
|
325
|
-
Purpose: review the changes made during the loop phase, then audit for over-engineering across the repo.
|
|
327
|
+
Purpose: review the changes made during the loop phase, then audit for over-engineering across the repo β and ACT on findings so no tech debt ships.
|
|
328
|
+
|
|
329
|
+
Two sub-agent rounds:
|
|
330
|
+
|
|
331
|
+
1. Spawn the COMMENTATOR SUB-AGENT (task tool, subagent_type "commentator"). Craft a tailored prompt from the full diff and plan.md so the commentator reviews correctness and plan-adherence (writing ${artifactDir}/review.md), then runs a whole-repo over-engineering audit (writing ${artifactDir}/audit.md). Give the commentator the diff context and tell it which files matter.
|
|
332
|
+
|
|
333
|
+
2. If review.md or audit.md lists any actionable issues, spawn the BUILDER SUB-AGENT (task tool, subagent_type "builder") with a tailored prompt containing the commentator's findings. Instruct the builder to FIX every issue: trim over-engineering, delete dead code, remove unnecessary abstractions, correct plan deviations. All fixes go in the workspace, never in ${artifactDir}. Do not leave findings unresolved β no tech debt carries past this phase.
|
|
334
|
+
|
|
335
|
+
3. After the builder returns, re-dispatch the commentator to confirm the fixes resolved the findings and no new issues appeared. Repeat commentatorβbuilder until the commentator reports a clean review.
|
|
326
336
|
|
|
327
|
-
|
|
337
|
+
4. Update ${artifactDir}/audit.md with the final resolved state (what was found, what the builder fixed, what remains β should be none).
|
|
328
338
|
|
|
329
|
-
The workflow closes automatically once both review.md and audit.md exist.`;
|
|
339
|
+
The workflow closes automatically once both review.md and audit.md exist and the commentator reports no outstanding issues.`;
|
|
330
340
|
default:
|
|
331
341
|
return "";
|
|
332
342
|
}
|