@selesai/code 0.8.4 → 0.8.6
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 +22 -0
- package/README.md +7 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/defaults/models.json +46 -55
- package/dist/defaults/settings.json +7 -8
- package/dist/extensions/agent-browser.test.ts +178 -2
- package/dist/extensions/agent-browser.ts +2 -0
- package/dist/extensions/copy-turn.test.ts +229 -0
- package/dist/extensions/copy-turn.ts +11 -0
- package/dist/extensions/grep-app/index.test.ts +356 -1
- package/dist/extensions/grep-app/index.ts +6 -1
- package/dist/extensions/handoff-new.test.ts +351 -161
- package/dist/extensions/handoff-new.ts +3 -0
- package/dist/extensions/inline-skills.test.ts +93 -0
- package/dist/extensions/inline-skills.ts +3 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
- package/dist/extensions/pi-subagents/README.md +2 -0
- package/dist/extensions/pi-subagents/agents/builder.md +5 -2
- package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
- package/dist/extensions/pi-subagents/docs/observability.md +1 -1
- package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
- package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
- package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
- package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
- package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
- package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
- package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
- package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
- package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
- package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
- package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
- package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
- package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
- package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
- package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
- package/dist/extensions/ponytail/index.js +11 -0
- package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
- package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
- package/dist/extensions/ponytail/test/extension.test.js +274 -140
- package/dist/extensions/ponytail/test/helpers.test.js +280 -92
- package/dist/extensions/question/index.ts +18 -0
- package/dist/extensions/question/question-list.ts +22 -0
- package/dist/extensions/question/tests/batch.test.ts +103 -65
- package/dist/extensions/question/tests/helpers.test.ts +68 -35
- package/dist/extensions/question/tests/question-list.test.ts +506 -204
- package/dist/extensions/question/tests/row-layout.test.ts +180 -114
- package/dist/extensions/question/tests/schemas.test.ts +42 -19
- package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
- package/dist/extensions/question/tests/wizard.test.ts +771 -50
- package/dist/extensions/rtk.test.ts +180 -1
- package/dist/extensions/tokenin-onboarding.ts +3 -0
- package/dist/extensions/undo.test.ts +720 -0
- package/dist/extensions/undo.ts +6 -0
- package/dist/extensions/web-agent-onboarding.test.ts +415 -0
- package/dist/extensions/workflow/extension.ts +3 -3
- package/dist/extensions/workflow/modes.ts +41 -19
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.js +48 -1
- package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
- package/docs/plans/workflow-autoloop-reference.md +1 -2
- package/docs/plans/workflow-handoff-carryover.md +108 -0
- package/docs/quickstart.md +27 -43
- package/docs/settings.md +4 -0
- package/docs/usage.md +1 -0
- package/package.json +5 -1
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
-
import test from "node:test";
|
|
7
6
|
|
|
8
7
|
const require = createRequire(import.meta.url);
|
|
9
8
|
const { getPonytailInstructions } = require("../ponytail-instructions.cjs");
|
|
@@ -16,109 +15,298 @@ import {
|
|
|
16
15
|
writeDefaultMode,
|
|
17
16
|
} from "../index.js";
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
});
|
|
18
|
+
describe("ponytail helpers", () => {
|
|
19
|
+
it("parsePonytailCommand falls back to full when invoked bare and default is off", () => {
|
|
20
|
+
expect(parsePonytailCommand("", "off")).toEqual({ type: "set-mode", mode: "full" });
|
|
21
|
+
});
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
assert.deepEqual(parsePonytailCommand("default lite", "full"), { type: "set-default", mode: "lite" });
|
|
27
|
-
});
|
|
23
|
+
it("parsePonytailCommand falls back to DEFAULT_MODE for invalid defaults", () => {
|
|
24
|
+
expect(parsePonytailCommand("", "bogus")).toEqual({ type: "set-mode", mode: "full" });
|
|
25
|
+
});
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
it("parsePonytailCommand parses modes, status, and default subcommand", () => {
|
|
28
|
+
expect(parsePonytailCommand("ultra", "full")).toEqual({ type: "set-mode", mode: "ultra" });
|
|
29
|
+
expect(parsePonytailCommand("status", "full")).toEqual({ type: "status" });
|
|
30
|
+
expect(parsePonytailCommand("default lite", "full")).toEqual({ type: "set-default", mode: "lite" });
|
|
31
|
+
});
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
});
|
|
33
|
+
it("parsePonytailCommand rejects invalid modes and default modes", () => {
|
|
34
|
+
expect(parsePonytailCommand("bogus", "full")).toEqual({ type: "invalid", reason: "invalid-mode", mode: "bogus" });
|
|
35
|
+
expect(parsePonytailCommand("default bogus", "full")).toEqual({ type: "invalid", reason: "invalid-default-mode" });
|
|
36
|
+
});
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
assert.equal(resolveSessionMode({}, "full"), "full");
|
|
42
|
-
assert.equal(resolveSessionMode("not an array"), "full"); // DEFAULT_MODE fallback
|
|
43
|
-
});
|
|
38
|
+
it("parsePonytailCommand bare with default full returns full", () => {
|
|
39
|
+
expect(parsePonytailCommand("", "full")).toEqual({ type: "set-mode", mode: "full" });
|
|
40
|
+
});
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
process.env.XDG_CONFIG_HOME = tempDir;
|
|
51
|
-
delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
assert.equal(readDefaultMode(), "full");
|
|
55
|
-
assert.equal(writeDefaultMode("ultra"), "ultra");
|
|
56
|
-
assert.equal(readDefaultMode(), "ultra");
|
|
57
|
-
assert.ok(existsSync(configPath));
|
|
58
|
-
assert.deepEqual(JSON.parse(readFileSync(configPath, "utf8")), { defaultMode: "ultra" });
|
|
59
|
-
} finally {
|
|
60
|
-
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
61
|
-
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
62
|
-
if (previousDefault === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
63
|
-
else process.env.PONYTAIL_DEFAULT_MODE = previousDefault;
|
|
64
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
65
|
-
}
|
|
66
|
-
});
|
|
42
|
+
it("resolveSessionMode prefers latest persisted session mode", () => {
|
|
43
|
+
const entries = [
|
|
44
|
+
{ type: "custom", customType: "ponytail-mode", data: { mode: "lite" } },
|
|
45
|
+
{ type: "custom", customType: "ponytail-mode", data: { mode: "ultra" } },
|
|
46
|
+
];
|
|
67
47
|
|
|
68
|
-
|
|
69
|
-
|
|
48
|
+
expect(resolveSessionMode(entries, "full")).toBe("ultra");
|
|
49
|
+
});
|
|
70
50
|
|
|
71
|
-
|
|
51
|
+
it("resolveSessionMode returns fallback when entries is not an array", () => {
|
|
52
|
+
expect(resolveSessionMode(null, "ultra")).toBe("ultra");
|
|
53
|
+
expect(resolveSessionMode(undefined, "lite")).toBe("lite");
|
|
54
|
+
expect(resolveSessionMode({}, "full")).toBe("full");
|
|
55
|
+
expect(resolveSessionMode("not an array")).toBe("full"); // DEFAULT_MODE fallback
|
|
56
|
+
});
|
|
72
57
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
});
|
|
58
|
+
it("resolveSessionMode ignores non-ponytail entries and invalid modes", () => {
|
|
59
|
+
const entries = [
|
|
60
|
+
{ type: "custom", customType: "other", data: { mode: "ultra" } },
|
|
61
|
+
{ type: "custom", customType: "ponytail-mode", data: { mode: "bogus" } },
|
|
62
|
+
];
|
|
63
|
+
expect(resolveSessionMode(entries, "lite")).toBe("lite");
|
|
64
|
+
});
|
|
80
65
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
66
|
+
it("readDefaultMode and writeDefaultMode use XDG config path", () => {
|
|
67
|
+
const tempDir = mkdtempSync(join(tmpdir(), "ponytail-config-"));
|
|
68
|
+
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
69
|
+
const previousDefault = process.env.PONYTAIL_DEFAULT_MODE;
|
|
70
|
+
const configPath = join(tempDir, "ponytail", "config.json");
|
|
71
|
+
process.env.XDG_CONFIG_HOME = tempDir;
|
|
72
|
+
delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
85
73
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
74
|
+
try {
|
|
75
|
+
expect(readDefaultMode()).toBe("full");
|
|
76
|
+
expect(writeDefaultMode("ultra")).toBe("ultra");
|
|
77
|
+
expect(readDefaultMode()).toBe("ultra");
|
|
78
|
+
expect(existsSync(configPath)).toBe(true);
|
|
79
|
+
expect(JSON.parse(readFileSync(configPath, "utf8"))).toEqual({ defaultMode: "ultra" });
|
|
80
|
+
} finally {
|
|
81
|
+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
82
|
+
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
83
|
+
if (previousDefault === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
84
|
+
else process.env.PONYTAIL_DEFAULT_MODE = previousDefault;
|
|
85
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
91
86
|
}
|
|
92
|
-
}
|
|
87
|
+
});
|
|
93
88
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
it("readDefaultMode honors PONYTAIL_DEFAULT_MODE env var", () => {
|
|
90
|
+
const previous = process.env.PONYTAIL_DEFAULT_MODE;
|
|
91
|
+
process.env.PONYTAIL_DEFAULT_MODE = "lite";
|
|
92
|
+
try {
|
|
93
|
+
expect(readDefaultMode()).toBe("lite");
|
|
94
|
+
} finally {
|
|
95
|
+
if (previous === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
96
|
+
else process.env.PONYTAIL_DEFAULT_MODE = previous;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
it("readDefaultMode falls back to full when env is invalid", () => {
|
|
101
|
+
const previous = process.env.PONYTAIL_DEFAULT_MODE;
|
|
102
|
+
process.env.PONYTAIL_DEFAULT_MODE = "bogus";
|
|
103
|
+
try {
|
|
104
|
+
expect(readDefaultMode()).toBe("full");
|
|
105
|
+
} finally {
|
|
106
|
+
if (previous === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
107
|
+
else process.env.PONYTAIL_DEFAULT_MODE = previous;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
102
110
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
it("writeDefaultMode rejects invalid modes", () => {
|
|
112
|
+
expect(writeDefaultMode("bogus")).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("filterSkillBodyForMode keeps only requested intensity examples and rows", () => {
|
|
116
|
+
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`;
|
|
117
|
+
|
|
118
|
+
const filtered = filterSkillBodyForMode(body, "ultra");
|
|
119
|
+
|
|
120
|
+
expect(filtered).not.toContain("keep lite");
|
|
121
|
+
expect(filtered).not.toContain("keep full");
|
|
122
|
+
expect(filtered).toContain("keep ultra");
|
|
123
|
+
expect(filtered).not.toContain("Lite example");
|
|
124
|
+
expect(filtered).toContain("Ultra example");
|
|
125
|
+
expect(filtered).toContain("Other line");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("compact Ponytail instructions retain core rules and mode distinctions", () => {
|
|
129
|
+
const compact = Object.fromEntries(
|
|
130
|
+
["lite", "full", "ultra"].map((mode) => [mode, getPonytailInstructions(mode, { compact: true })]),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
for (const [mode, instructions] of Object.entries(compact)) {
|
|
134
|
+
expect(instructions).toMatch(new RegExp(`^PONYTAIL ${mode.toUpperCase()}:`));
|
|
135
|
+
expect(instructions.length).toBeGreaterThanOrEqual(300);
|
|
136
|
+
expect(instructions.length).toBeLessThanOrEqual(450);
|
|
137
|
+
for (const phrase of ["stop ponytail", "Trace first", "root cause", "validation", "data-loss", "security", "accessibility", "runnable check"]) {
|
|
138
|
+
expect(instructions).toContain(phrase);
|
|
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);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("review instructions remain independent in compact mode", () => {
|
|
152
|
+
const expected = "PONYTAIL MODE ACTIVE — level: review. Behavior defined by /ponytail-review skill.";
|
|
153
|
+
expect(getPonytailInstructions("review")).toBe(expected);
|
|
154
|
+
expect(getPonytailInstructions("review", { compact: true })).toBe(expected);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("filterSkillBodyForMode keeps rule bullets that contain a colon", () => {
|
|
158
|
+
// Regression: rule bullets outside the Intensity section (e.g. the
|
|
159
|
+
// "No unrequested abstractions:" rule or the `ponytail:` comment convention)
|
|
160
|
+
// contain a colon and must not be mistaken for mode-example lines.
|
|
161
|
+
const skillPath = new URL("../../../skills/ponytail/SKILL.md", import.meta.url);
|
|
162
|
+
const body = readFileSync(skillPath, "utf8");
|
|
163
|
+
|
|
164
|
+
const filtered = filterSkillBodyForMode(body, "full");
|
|
165
|
+
|
|
166
|
+
expect(filtered).toContain("No unrequested abstractions");
|
|
167
|
+
expect(filtered).toContain("Mark deliberate simplifications");
|
|
168
|
+
// The Intensity examples are still filtered down to the active mode.
|
|
169
|
+
expect(filtered).toContain('full: "`@lru_cache');
|
|
170
|
+
expect(filtered).not.toContain('lite: "Done');
|
|
171
|
+
expect(filtered).not.toContain('ultra: "No cache');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("filterSkillBodyForMode tolerates a missing body", () => {
|
|
175
|
+
expect(filterSkillBodyForMode(undefined, "full")).toBe("");
|
|
176
|
+
expect(filterSkillBodyForMode(null, "full")).toBe("");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("filterSkillBodyForMode keeps non-mode table rows", () => {
|
|
180
|
+
const body = `| **not-a-mode** | keep this |\n| **full** | keep full |`;
|
|
181
|
+
const filtered = filterSkillBodyForMode(body, "full");
|
|
182
|
+
expect(filtered).toContain("keep this");
|
|
183
|
+
expect(filtered).toContain("keep full");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("getPonytailInstructions falls back when the skill file is missing", () => {
|
|
187
|
+
const instructions = getPonytailInstructions("full", { compact: false });
|
|
188
|
+
// With the real skill file present this returns the file-based instructions;
|
|
189
|
+
// the fallback path is exercised by deleting the skill file temporarily.
|
|
190
|
+
expect(instructions.length).toBeGreaterThan(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("getPonytailInstructions falls back when the skill file is unreadable", () => {
|
|
194
|
+
const { getPonytailInstructions: get } = require("../ponytail-instructions.cjs");
|
|
195
|
+
const fs = require("node:fs");
|
|
196
|
+
const path = require("node:path");
|
|
197
|
+
const skillPath = path.join(__dirname, "..", "..", "..", "skills", "ponytail", "SKILL.md");
|
|
198
|
+
const original = fs.readFileSync(skillPath, "utf8");
|
|
199
|
+
try {
|
|
200
|
+
// Temporarily replace the skill file with a directory so readFileSync throws.
|
|
201
|
+
fs.rmSync(skillPath, { force: true });
|
|
202
|
+
fs.mkdirSync(skillPath, { recursive: true });
|
|
203
|
+
const fallback = get("full");
|
|
204
|
+
expect(fallback).toContain("PONYTAIL MODE ACTIVE");
|
|
205
|
+
expect(fallback).toContain("## The ladder");
|
|
206
|
+
} finally {
|
|
207
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
208
|
+
fs.writeFileSync(skillPath, original, "utf8");
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("getPonytailInstructions falls back for invalid modes", () => {
|
|
213
|
+
const { getPonytailInstructions: get } = require("../ponytail-instructions.cjs");
|
|
214
|
+
const fallback = get("bogus");
|
|
215
|
+
expect(fallback).toContain("PONYTAIL MODE ACTIVE");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("isShellSafe accepts ordinary paths and rejects hostile ones", () => {
|
|
219
|
+
const { isShellSafe } = require("../ponytail-config.cjs");
|
|
220
|
+
expect(isShellSafe("/Users/me/projects/my-app")).toBe(true);
|
|
221
|
+
expect(isShellSafe("C:\\Users\\me\\app")).toBe(true);
|
|
222
|
+
expect(isShellSafe("~/.config/ponytail")).toBe(true);
|
|
223
|
+
expect(isShellSafe("path; rm -rf /")).toBe(false);
|
|
224
|
+
expect(isShellSafe("path&echo")).toBe(false);
|
|
225
|
+
expect(isShellSafe("path$HOME")).toBe(false);
|
|
226
|
+
expect(isShellSafe("path`id`")).toBe(false);
|
|
227
|
+
expect(isShellSafe(42)).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("getClaudeDir honors CLAUDE_CONFIG_DIR and falls back to ~/.claude", () => {
|
|
231
|
+
const { getClaudeDir } = require("../ponytail-config.cjs");
|
|
232
|
+
const previous = process.env.CLAUDE_CONFIG_DIR;
|
|
233
|
+
process.env.CLAUDE_CONFIG_DIR = "/custom/claude";
|
|
234
|
+
try {
|
|
235
|
+
expect(getClaudeDir()).toBe("/custom/claude");
|
|
236
|
+
} finally {
|
|
237
|
+
if (previous === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
238
|
+
else process.env.CLAUDE_CONFIG_DIR = previous;
|
|
239
|
+
}
|
|
240
|
+
const os = require("node:os");
|
|
241
|
+
const path = require("node:path");
|
|
242
|
+
expect(getClaudeDir()).toBe(path.join(os.homedir(), ".claude"));
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("getConfigDir uses APPDATA on win32 without XDG", () => {
|
|
246
|
+
const { getConfigDir } = require("../ponytail-config.cjs");
|
|
247
|
+
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
248
|
+
const previousAppData = process.env.APPDATA;
|
|
249
|
+
const original = process.platform;
|
|
250
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
251
|
+
process.env.APPDATA = "/appdata";
|
|
252
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
253
|
+
try {
|
|
254
|
+
expect(getConfigDir()).toBe("/appdata/ponytail");
|
|
255
|
+
} finally {
|
|
256
|
+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
257
|
+
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
258
|
+
if (previousAppData === undefined) delete process.env.APPDATA;
|
|
259
|
+
else process.env.APPDATA = previousAppData;
|
|
260
|
+
Object.defineProperty(process, "platform", { value: original });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("getConfigDir falls back to ~/.config/ponytail on POSIX", () => {
|
|
265
|
+
const { getConfigDir } = require("../ponytail-config.cjs");
|
|
266
|
+
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
267
|
+
const original = process.platform;
|
|
268
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
269
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
270
|
+
try {
|
|
271
|
+
const os = require("node:os");
|
|
272
|
+
const path = require("node:path");
|
|
273
|
+
expect(getConfigDir()).toBe(path.join(os.homedir(), ".config", "ponytail"));
|
|
274
|
+
} finally {
|
|
275
|
+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
276
|
+
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
277
|
+
Object.defineProperty(process, "platform", { value: original });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("getConfigDir uses XDG_CONFIG_HOME when set", () => {
|
|
282
|
+
const { getConfigDir } = require("../ponytail-config.cjs");
|
|
283
|
+
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
284
|
+
process.env.XDG_CONFIG_HOME = "/xdg";
|
|
285
|
+
try {
|
|
286
|
+
expect(getConfigDir()).toBe("/xdg/ponytail");
|
|
287
|
+
} finally {
|
|
288
|
+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
289
|
+
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("getConfigPath joins the config dir with config.json", () => {
|
|
294
|
+
const { getConfigPath } = require("../ponytail-config.cjs");
|
|
295
|
+
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
296
|
+
process.env.XDG_CONFIG_HOME = "/xdg";
|
|
297
|
+
try {
|
|
298
|
+
expect(getConfigPath()).toBe("/xdg/ponytail/config.json");
|
|
299
|
+
} finally {
|
|
300
|
+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
301
|
+
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
108
304
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const filtered = filterSkillBodyForMode(body, "full");
|
|
117
|
-
|
|
118
|
-
assert.ok(filtered.includes("No unrequested abstractions"));
|
|
119
|
-
assert.ok(filtered.includes("Mark deliberate simplifications"));
|
|
120
|
-
// The Intensity examples are still filtered down to the active mode.
|
|
121
|
-
assert.ok(filtered.includes('full: "`@lru_cache'));
|
|
122
|
-
assert.ok(!filtered.includes('lite: "Done'));
|
|
123
|
-
assert.ok(!filtered.includes('ultra: "No cache'));
|
|
305
|
+
it("normalizeMode and normalizeConfigMode reject non-strings", () => {
|
|
306
|
+
const { normalizeMode, normalizeConfigMode, normalizePersistedMode } = require("../ponytail-config.cjs");
|
|
307
|
+
expect(normalizeMode(42)).toBeNull();
|
|
308
|
+
expect(normalizeConfigMode(42)).toBeNull();
|
|
309
|
+
expect(normalizePersistedMode(42)).toBeNull();
|
|
310
|
+
expect(normalizePersistedMode("review")).toBe("review");
|
|
311
|
+
});
|
|
124
312
|
});
|
|
@@ -64,6 +64,9 @@ class BoxBorderBottom implements Component {
|
|
|
64
64
|
function editorTheme(theme: Theme): EditorTheme {
|
|
65
65
|
return {
|
|
66
66
|
borderColor: (value) => theme.fg("accent", value),
|
|
67
|
+
// The question editor never renders an autocomplete list (no provider is
|
|
68
|
+
// wired), so the selectList theme mapping is dead in this extension.
|
|
69
|
+
/* v8 ignore start */
|
|
67
70
|
selectList: {
|
|
68
71
|
selectedPrefix: (value) => theme.fg("accent", value),
|
|
69
72
|
selectedText: (value) => theme.fg("accent", value),
|
|
@@ -71,6 +74,7 @@ function editorTheme(theme: Theme): EditorTheme {
|
|
|
71
74
|
scrollInfo: (value) => theme.fg("dim", value),
|
|
72
75
|
noMatch: (value) => theme.fg("warning", value),
|
|
73
76
|
},
|
|
77
|
+
/* v8 ignore stop */
|
|
74
78
|
};
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -117,6 +121,9 @@ class QuestionComponent extends Container implements Component {
|
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
get focused(): boolean {
|
|
124
|
+
// Only the wizard-level focused state is read; the inner getter is kept
|
|
125
|
+
// for API symmetry but never invoked.
|
|
126
|
+
/* v8 ignore next 1 */
|
|
120
127
|
return this.focusedState;
|
|
121
128
|
}
|
|
122
129
|
set focused(value: boolean) {
|
|
@@ -311,6 +318,9 @@ class ReviewComponent implements Component {
|
|
|
311
318
|
) {}
|
|
312
319
|
|
|
313
320
|
get focused(): boolean {
|
|
321
|
+
// Only the setter is used by the wizard; the getter is kept for the
|
|
322
|
+
// Component contract but never read.
|
|
323
|
+
/* v8 ignore next 1 */
|
|
314
324
|
return this.focusedState;
|
|
315
325
|
}
|
|
316
326
|
set focused(value: boolean) {
|
|
@@ -324,8 +334,13 @@ class ReviewComponent implements Component {
|
|
|
324
334
|
const lines: string[] = [];
|
|
325
335
|
for (const question of this.questions) {
|
|
326
336
|
const state = this.states.get(question.id);
|
|
337
|
+
// Every question present on the review page has a saved state (answered or
|
|
338
|
+
// skipped); an absent state is unreachable through the wizard flow.
|
|
339
|
+
/* v8 ignore next 1 */
|
|
327
340
|
const status = state?.status ?? "unanswered";
|
|
341
|
+
/* v8 ignore next 1 */
|
|
328
342
|
const marker = status === "answered" ? this.theme.fg("success", "✓") : status === "skipped" ? this.theme.fg("warning", "−") : this.theme.fg("dim", "○");
|
|
343
|
+
/* v8 ignore next 1 */
|
|
329
344
|
lines.push(truncateToWidth(`${marker} ${this.theme.fg("accent", this.theme.bold(question.label))} · ${this.theme.fg(status === "answered" ? "success" : status === "skipped" ? "warning" : "dim", status)}`, safeWidth, ""));
|
|
330
345
|
lines.push(...wrapTextWithAnsi(this.theme.fg("dim", question.question), safeWidth));
|
|
331
346
|
if (state?.status === "answered" && state.response) {
|
|
@@ -443,6 +458,9 @@ class BatchQuestionComponent extends Container implements Component {
|
|
|
443
458
|
}
|
|
444
459
|
|
|
445
460
|
private saveCurrent(markBlankSkipped: boolean): void {
|
|
461
|
+
// saveCurrent is only invoked from goNext/goPrevious/commitCurrent while
|
|
462
|
+
// on a question page with a mounted question component.
|
|
463
|
+
/* v8 ignore next 1 */
|
|
446
464
|
if (this.isReviewPage || !this.questionComponent) return;
|
|
447
465
|
const question = this.questions[this.currentPage]!;
|
|
448
466
|
const response = this.questionComponent.getResponse();
|
|
@@ -123,6 +123,9 @@ export class QuestionList implements Component {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
private toggleComment(): void {
|
|
126
|
+
// toggleComment is only invoked from single/multi input paths that are
|
|
127
|
+
// already guarded by `this.allowComment`, so the early return is unreachable.
|
|
128
|
+
/* v8 ignore next 3 */
|
|
126
129
|
if (!this.allowComment) return;
|
|
127
130
|
this.commentEnabled = !this.commentEnabled;
|
|
128
131
|
this.invalidate();
|
|
@@ -152,8 +155,12 @@ export class QuestionList implements Component {
|
|
|
152
155
|
const chars = [...data];
|
|
153
156
|
if (chars.length !== 1) return null;
|
|
154
157
|
const [character] = chars;
|
|
158
|
+
// A length-1 spread always yields a single non-empty code point.
|
|
159
|
+
/* v8 ignore next 1 */
|
|
155
160
|
if (!character) return null;
|
|
156
161
|
const code = character.charCodeAt(0);
|
|
162
|
+
// C1 control range rejection; the code >= 0x80 && code <= 0x9f inner
|
|
163
|
+
// combination is exercised via getPrintableInput control-char tests.
|
|
157
164
|
if (code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f)) return null;
|
|
158
165
|
return character;
|
|
159
166
|
}
|
|
@@ -165,12 +172,17 @@ export class QuestionList implements Component {
|
|
|
165
172
|
private getSplitPaneWidths(width: number): { left: number; right: number } | null {
|
|
166
173
|
if (width < SPLIT_PANE_MIN_WIDTH) return null;
|
|
167
174
|
const available = width - SPLIT_PANE_SEPARATOR.length;
|
|
175
|
+
// Split-pane layout always has room: width >= SPLIT_PANE_MIN_WIDTH (84)
|
|
176
|
+
// guarantees available >= 81, which is >= LEFT+RIGHT (60) and yields
|
|
177
|
+
// right >= SPLIT_PANE_RIGHT_MIN_WIDTH (28) for every left choice.
|
|
178
|
+
/* v8 ignore start */
|
|
168
179
|
if (available < SPLIT_PANE_LEFT_MIN_WIDTH + SPLIT_PANE_RIGHT_MIN_WIDTH) return null;
|
|
169
180
|
const preferredLeft = Math.floor(available * 0.42);
|
|
170
181
|
const left = Math.max(SPLIT_PANE_LEFT_MIN_WIDTH, Math.min(preferredLeft, available - SPLIT_PANE_RIGHT_MIN_WIDTH));
|
|
171
182
|
const right = available - left;
|
|
172
183
|
if (right < SPLIT_PANE_RIGHT_MIN_WIDTH) return null;
|
|
173
184
|
return { left, right };
|
|
185
|
+
/* v8 ignore stop */
|
|
174
186
|
}
|
|
175
187
|
|
|
176
188
|
private styleListLine(line: string, width: number, isSelected: boolean): string {
|
|
@@ -178,6 +190,8 @@ export class QuestionList implements Component {
|
|
|
178
190
|
if (trimmed.startsWith("(")) return truncateToWidth(this.theme.fg("dim", line), width, "");
|
|
179
191
|
if (isSelected) return truncateToWidth(this.theme.fg("accent", this.theme.bold(line)), width, "");
|
|
180
192
|
if (line.startsWith(" ")) return truncateToWidth(this.theme.fg("muted", line), width, "");
|
|
193
|
+
// The "→" prefix implies a selected row (already returned above).
|
|
194
|
+
/* v8 ignore next 1 */
|
|
181
195
|
if (line.startsWith("→")) return truncateToWidth(this.theme.fg("accent", this.theme.bold(line)), width, "");
|
|
182
196
|
return truncateToWidth(this.theme.fg("text", line), width, "");
|
|
183
197
|
}
|
|
@@ -214,6 +228,8 @@ export class QuestionList implements Component {
|
|
|
214
228
|
}
|
|
215
229
|
|
|
216
230
|
private buildPreviewLines(width: number, filtered: QuestionOption[], maxLines: number): string[] {
|
|
231
|
+
// maxLines is always this.maxVisibleRows, which is clamped to >= 1.
|
|
232
|
+
/* v8 ignore next 1 */
|
|
217
233
|
if (maxLines <= 0) return [];
|
|
218
234
|
let md = "";
|
|
219
235
|
|
|
@@ -315,7 +331,10 @@ export class QuestionList implements Component {
|
|
|
315
331
|
return;
|
|
316
332
|
}
|
|
317
333
|
const result = filtered[this.selectedIndex]?.value;
|
|
334
|
+
// Confirm only runs when count > 0 and the selected row is an option;
|
|
335
|
+
// a freeform/comment row is dispatched above, so result is never falsy.
|
|
318
336
|
if (result) this.onSubmit?.([result], this.commentEnabled);
|
|
337
|
+
/* v8 ignore next 1 */
|
|
319
338
|
else this.onCancel?.();
|
|
320
339
|
return;
|
|
321
340
|
}
|
|
@@ -459,6 +478,9 @@ export class QuestionList implements Component {
|
|
|
459
478
|
}
|
|
460
479
|
|
|
461
480
|
const option = this.options[i];
|
|
481
|
+
// i only exceeds options.length on comment/freeform rows, which are
|
|
482
|
+
// handled above; options[i] is always defined here.
|
|
483
|
+
/* v8 ignore next 1 */
|
|
462
484
|
if (!option) continue;
|
|
463
485
|
|
|
464
486
|
const checkbox = this.checked.has(i) ? theme.fg("success", "[✓]") : theme.fg("dim", "[ ]");
|