@slowdini/slow-powers-opencode 0.1.3 → 0.1.5
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/README.md +3 -3
- package/bootstrap.md +19 -20
- package/package.json +1 -1
- package/skills/auditing-slow-powers-usage/evals/baseline/NOTES.md +8 -0
- package/skills/auditing-slow-powers-usage/evals/evals.json +2 -2
- package/skills/auditing-slow-powers-usage/evals/fixtures/audits-blindspot-session/session-summary.md +1 -1
- package/skills/evaluating-skills/SKILL.md +6 -4
- package/skills/evaluating-skills/evals/evals.json +1 -1
- package/skills/evaluating-skills/harness-details/claude.md +24 -1
- package/skills/evaluating-skills/runner/README.md +16 -2
- package/skills/evaluating-skills/runner/adapters/claude-code-session.test.ts +56 -0
- package/skills/evaluating-skills/runner/adapters/claude-code-session.ts +43 -0
- package/skills/evaluating-skills/runner/aggregate.test.ts +76 -0
- package/skills/evaluating-skills/runner/aggregate.ts +20 -0
- package/skills/evaluating-skills/runner/plugin-shadow.test.ts +228 -0
- package/skills/evaluating-skills/runner/plugin-shadow.ts +201 -0
- package/skills/evaluating-skills/runner/profiles/claude-code/plan-mode.md +11 -0
- package/skills/evaluating-skills/runner/run.test.ts +488 -24
- package/skills/evaluating-skills/runner/run.ts +281 -66
- package/skills/evaluating-skills/runner/types.ts +8 -0
- package/skills/evaluating-skills/templates/eval-task-prompt.md +3 -7
- package/skills/finishing-a-development-branch/SKILL.md +1 -1
- package/skills/hardening-plans/evals/baseline/NOTES.md +7 -0
- package/skills/hardening-plans/evals/evals.json +0 -19
- package/skills/systematic-debugging/condition-based-waiting.md +10 -11
- package/skills/systematic-debugging/root-cause-tracing.md +31 -33
- package/skills/working-in-isolation/SKILL.md +58 -0
- package/skills/working-in-isolation/evals/baseline/BASELINE.md +22 -0
- package/skills/working-in-isolation/evals/baseline/NOTES.md +67 -0
- package/skills/working-in-isolation/evals/baseline/benchmark.json +51 -0
- package/skills/working-in-isolation/evals/baseline/grading/base-branch-checkout__with_skill.json +46 -0
- package/skills/working-in-isolation/evals/baseline/grading/base-branch-checkout__without_skill.json +31 -0
- package/skills/working-in-isolation/evals/baseline/grading/dirty-tree-worktree__with_skill.json +39 -0
- package/skills/working-in-isolation/evals/baseline/grading/dirty-tree-worktree__without_skill.json +24 -0
- package/skills/working-in-isolation/evals/baseline/grading/feature-branch-in-place__with_skill.json +32 -0
- package/skills/working-in-isolation/evals/baseline/grading/feature-branch-in-place__without_skill.json +17 -0
- package/skills/working-in-isolation/evals/baseline/grading/seeded-on-main-momentum__with_skill.json +39 -0
- package/skills/working-in-isolation/evals/baseline/grading/seeded-on-main-momentum__without_skill.json +24 -0
- package/skills/working-in-isolation/evals/baseline/grading/typo-no-worktree__with_skill.json +32 -0
- package/skills/working-in-isolation/evals/baseline/grading/typo-no-worktree__without_skill.json +17 -0
- package/skills/working-in-isolation/evals/evals.json +87 -0
- package/skills/writing-skills/SKILL.md +179 -195
- package/skills/hardening-plans/evals/baseline/grading/csv-parser-bug-no-plan__new_skill.json +0 -24
- package/skills/hardening-plans/evals/baseline/grading/csv-parser-bug-no-plan__old_skill.json +0 -24
- package/skills/using-git-worktrees/SKILL.md +0 -70
- package/skills/using-git-worktrees/evals/evals.json +0 -40
- package/skills/writing-skills/graphviz-conventions.dot +0 -172
- package/skills/writing-skills/scripts/render-graphs.js +0 -181
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
detectPluginShadows,
|
|
7
|
+
formatShadowBanner,
|
|
8
|
+
resolveConfigDir,
|
|
9
|
+
shadowValidityWarnings,
|
|
10
|
+
} from "./plugin-shadow";
|
|
11
|
+
|
|
12
|
+
const ROOT = join(tmpdir(), `slow-powers-plugin-shadow-test-${process.pid}`);
|
|
13
|
+
|
|
14
|
+
beforeAll(() => mkdirSync(ROOT, { recursive: true }));
|
|
15
|
+
afterAll(() => rmSync(ROOT, { recursive: true, force: true }));
|
|
16
|
+
|
|
17
|
+
let seq = 0;
|
|
18
|
+
function freshDirs() {
|
|
19
|
+
seq += 1;
|
|
20
|
+
const base = join(ROOT, `case-${seq}`);
|
|
21
|
+
const configDir = join(base, "config");
|
|
22
|
+
const cwd = join(base, "cwd");
|
|
23
|
+
mkdirSync(configDir, { recursive: true });
|
|
24
|
+
mkdirSync(cwd, { recursive: true });
|
|
25
|
+
return { configDir, cwd };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeFile(path: string, body: string) {
|
|
29
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
30
|
+
writeFileSync(path, body);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Lay down a plugin's skill folders and return its install path. */
|
|
34
|
+
function installPlugin(
|
|
35
|
+
configDir: string,
|
|
36
|
+
key: string,
|
|
37
|
+
skillNames: string[],
|
|
38
|
+
): string {
|
|
39
|
+
const installPath = join(
|
|
40
|
+
configDir,
|
|
41
|
+
"plugins",
|
|
42
|
+
"cache",
|
|
43
|
+
key.replace("@", "__"),
|
|
44
|
+
);
|
|
45
|
+
for (const name of skillNames) {
|
|
46
|
+
writeFile(
|
|
47
|
+
join(installPath, "skills", name, "SKILL.md"),
|
|
48
|
+
`---\nname: ${name}\ndescription: x\n---\n`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return installPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeInstalledManifest(
|
|
55
|
+
configDir: string,
|
|
56
|
+
entries: Record<string, string>,
|
|
57
|
+
) {
|
|
58
|
+
const plugins: Record<string, Array<{ installPath: string }>> = {};
|
|
59
|
+
for (const [key, installPath] of Object.entries(entries))
|
|
60
|
+
plugins[key] = [{ installPath }];
|
|
61
|
+
writeFile(
|
|
62
|
+
join(configDir, "plugins", "installed_plugins.json"),
|
|
63
|
+
`${JSON.stringify({ version: 2, plugins }, null, 2)}\n`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function writeSettings(path: string, enabledPlugins: Record<string, boolean>) {
|
|
68
|
+
writeFile(path, `${JSON.stringify({ enabledPlugins }, null, 2)}\n`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("resolveConfigDir", () => {
|
|
72
|
+
test("honors CLAUDE_CONFIG_DIR", () => {
|
|
73
|
+
expect(
|
|
74
|
+
resolveConfigDir({
|
|
75
|
+
CLAUDE_CONFIG_DIR: "/custom/cfg",
|
|
76
|
+
} as NodeJS.ProcessEnv),
|
|
77
|
+
).toBe("/custom/cfg");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("defaults to ~/.claude when unset", () => {
|
|
81
|
+
expect(resolveConfigDir({} as NodeJS.ProcessEnv)).toBe(
|
|
82
|
+
join(homedir(), ".claude"),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("detectPluginShadows", () => {
|
|
88
|
+
test("flags a staged skill also provided by an enabled plugin", () => {
|
|
89
|
+
const { configDir, cwd } = freshDirs();
|
|
90
|
+
const ip = installPlugin(configDir, "slow-powers@slowdini", [
|
|
91
|
+
"verification-before-completion",
|
|
92
|
+
"writing-skills",
|
|
93
|
+
]);
|
|
94
|
+
writeInstalledManifest(configDir, { "slow-powers@slowdini": ip });
|
|
95
|
+
writeSettings(join(configDir, "settings.json"), {
|
|
96
|
+
"slow-powers@slowdini": true,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const report = detectPluginShadows({
|
|
100
|
+
configDir,
|
|
101
|
+
cwd,
|
|
102
|
+
stagedSkillNames: ["verification-before-completion"],
|
|
103
|
+
});
|
|
104
|
+
expect(report.shadowed).toHaveLength(1);
|
|
105
|
+
expect(report.shadowed[0]).toMatchObject({
|
|
106
|
+
kind: "plugin",
|
|
107
|
+
plugin: "slow-powers@slowdini",
|
|
108
|
+
skill_name: "verification-before-completion",
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("does not flag a plugin disabled in user settings", () => {
|
|
113
|
+
const { configDir, cwd } = freshDirs();
|
|
114
|
+
const ip = installPlugin(configDir, "slow-powers@slowdini", [
|
|
115
|
+
"verification-before-completion",
|
|
116
|
+
]);
|
|
117
|
+
writeInstalledManifest(configDir, { "slow-powers@slowdini": ip });
|
|
118
|
+
writeSettings(join(configDir, "settings.json"), {
|
|
119
|
+
"slow-powers@slowdini": false,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const report = detectPluginShadows({
|
|
123
|
+
configDir,
|
|
124
|
+
cwd,
|
|
125
|
+
stagedSkillNames: ["verification-before-completion"],
|
|
126
|
+
});
|
|
127
|
+
expect(report.shadowed).toHaveLength(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("project settings disabling a user-enabled plugin suppresses the shadow", () => {
|
|
131
|
+
const { configDir, cwd } = freshDirs();
|
|
132
|
+
const ip = installPlugin(configDir, "slow-powers@slowdini", [
|
|
133
|
+
"verification-before-completion",
|
|
134
|
+
]);
|
|
135
|
+
writeInstalledManifest(configDir, { "slow-powers@slowdini": ip });
|
|
136
|
+
writeSettings(join(configDir, "settings.json"), {
|
|
137
|
+
"slow-powers@slowdini": true,
|
|
138
|
+
});
|
|
139
|
+
// Project scope (cwd/.claude/settings.json) outranks user scope.
|
|
140
|
+
writeSettings(join(cwd, ".claude", "settings.json"), {
|
|
141
|
+
"slow-powers@slowdini": false,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const report = detectPluginShadows({
|
|
145
|
+
configDir,
|
|
146
|
+
cwd,
|
|
147
|
+
stagedSkillNames: ["verification-before-completion"],
|
|
148
|
+
});
|
|
149
|
+
expect(report.shadowed).toHaveLength(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("flags a staged skill also present in the global skills dir", () => {
|
|
153
|
+
const { configDir, cwd } = freshDirs();
|
|
154
|
+
writeFile(
|
|
155
|
+
join(configDir, "skills", "my-skill", "SKILL.md"),
|
|
156
|
+
"---\nname: my-skill\n---\n",
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const report = detectPluginShadows({
|
|
160
|
+
configDir,
|
|
161
|
+
cwd,
|
|
162
|
+
stagedSkillNames: ["my-skill"],
|
|
163
|
+
});
|
|
164
|
+
expect(report.shadowed).toHaveLength(1);
|
|
165
|
+
expect(report.shadowed[0]).toMatchObject({
|
|
166
|
+
kind: "global-skill",
|
|
167
|
+
skill_name: "my-skill",
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("no shadow when staged names match nothing in the environment", () => {
|
|
172
|
+
const { configDir, cwd } = freshDirs();
|
|
173
|
+
const ip = installPlugin(configDir, "p@m", ["other"]);
|
|
174
|
+
writeInstalledManifest(configDir, { "p@m": ip });
|
|
175
|
+
writeSettings(join(configDir, "settings.json"), { "p@m": true });
|
|
176
|
+
|
|
177
|
+
const report = detectPluginShadows({
|
|
178
|
+
configDir,
|
|
179
|
+
cwd,
|
|
180
|
+
stagedSkillNames: ["mine"],
|
|
181
|
+
});
|
|
182
|
+
expect(report.shadowed).toHaveLength(0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("is graceful when the config dir has no plugins or skills", () => {
|
|
186
|
+
const { configDir, cwd } = freshDirs();
|
|
187
|
+
const report = detectPluginShadows({
|
|
188
|
+
configDir,
|
|
189
|
+
cwd,
|
|
190
|
+
stagedSkillNames: ["x"],
|
|
191
|
+
});
|
|
192
|
+
expect(report.shadowed).toHaveLength(0);
|
|
193
|
+
expect(report.config_dir).toBe(configDir);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("warning formatting", () => {
|
|
198
|
+
const report = {
|
|
199
|
+
config_dir: "/x",
|
|
200
|
+
shadowed: [
|
|
201
|
+
{
|
|
202
|
+
kind: "plugin" as const,
|
|
203
|
+
plugin: "slow-powers@slowdini",
|
|
204
|
+
skill_name: "verification-before-completion",
|
|
205
|
+
path: "/p",
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
test("shadowValidityWarnings names the skill, the plugin, and the contamination", () => {
|
|
211
|
+
const warnings = shadowValidityWarnings(report);
|
|
212
|
+
expect(warnings).toHaveLength(1);
|
|
213
|
+
expect(warnings[0]).toContain("verification-before-completion");
|
|
214
|
+
expect(warnings[0]).toContain("slow-powers@slowdini");
|
|
215
|
+
expect(warnings[0]).toMatch(/contaminat/i);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("formatShadowBanner is empty when nothing is shadowed", () => {
|
|
219
|
+
expect(formatShadowBanner({ config_dir: "/x", shadowed: [] })).toBe("");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("formatShadowBanner lists shadowed skills and points at isolation docs", () => {
|
|
223
|
+
const banner = formatShadowBanner(report);
|
|
224
|
+
expect(banner).toContain("verification-before-completion");
|
|
225
|
+
expect(banner).toContain("slow-powers@slowdini");
|
|
226
|
+
expect(banner).toMatch(/isolat/i);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Plugin-shadow detector (Claude Code). The runner stages eval skills into the
|
|
2
|
+
// project-local `.claude/skills/` dir, but eval subagents are dispatched via the
|
|
3
|
+
// Task tool and run in-process — so they ALSO inherit whatever skills the
|
|
4
|
+
// orchestrator session loaded from installed plugins and the global skills dir.
|
|
5
|
+
// When a staged skill name collides with one of those, both copies are
|
|
6
|
+
// discoverable: the with/without comparison is contaminated and the control arm
|
|
7
|
+
// is not truly skill-absent.
|
|
8
|
+
//
|
|
9
|
+
// The runner cannot unload a plugin from a running session (plugins load at
|
|
10
|
+
// session start), so this module only *detects and reports* the overlap. It
|
|
11
|
+
// reads declared settings as a best-effort proxy for what the session loaded —
|
|
12
|
+
// it can't observe the live-loaded set, so a session that changed settings
|
|
13
|
+
// without restarting may differ. Isolation itself is a launch-time concern; see
|
|
14
|
+
// harness-details/claude.md → "Isolating from installed plugins".
|
|
15
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
export type ShadowSource =
|
|
20
|
+
| { kind: "plugin"; plugin: string; skill_name: string; path: string }
|
|
21
|
+
| { kind: "global-skill"; skill_name: string; path: string };
|
|
22
|
+
|
|
23
|
+
export type PluginShadowReport = {
|
|
24
|
+
config_dir: string;
|
|
25
|
+
shadowed: ShadowSource[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ISOLATION_DOC =
|
|
29
|
+
'harness-details/claude.md → "Isolating from installed plugins"';
|
|
30
|
+
|
|
31
|
+
/** The Claude Code config dir: `$CLAUDE_CONFIG_DIR` if set, else `~/.claude`. */
|
|
32
|
+
export function resolveConfigDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
33
|
+
const override = env.CLAUDE_CONFIG_DIR;
|
|
34
|
+
return override?.trim() ? override : join(homedir(), ".claude");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readJsonSafe<T>(path: string): T | null {
|
|
38
|
+
if (!existsSync(path)) return null;
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Settings = { enabledPlugins?: Record<string, boolean> };
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Effective `enabledPlugins` map, honoring Claude Code's settings precedence
|
|
50
|
+
* (local > project > user). User scope lives under the config dir; project and
|
|
51
|
+
* local scope live under `<cwd>/.claude/`. Later sources override earlier keys,
|
|
52
|
+
* so a project-scope `false` correctly masks a user-scope `true`.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveEnabledPlugins(opts: {
|
|
55
|
+
configDir: string;
|
|
56
|
+
cwd: string;
|
|
57
|
+
}): Record<string, boolean> {
|
|
58
|
+
const sources = [
|
|
59
|
+
join(opts.configDir, "settings.json"),
|
|
60
|
+
join(opts.cwd, ".claude", "settings.json"),
|
|
61
|
+
join(opts.cwd, ".claude", "settings.local.json"),
|
|
62
|
+
];
|
|
63
|
+
let merged: Record<string, boolean> = {};
|
|
64
|
+
for (const path of sources) {
|
|
65
|
+
const s = readJsonSafe<Settings>(path);
|
|
66
|
+
if (s?.enabledPlugins) merged = { ...merged, ...s.enabledPlugins };
|
|
67
|
+
}
|
|
68
|
+
return merged;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Names of skill folders (those holding a `SKILL.md`) directly under `dir`. */
|
|
72
|
+
function skillFolderNames(dir: string): Array<{ name: string; path: string }> {
|
|
73
|
+
if (!existsSync(dir)) return [];
|
|
74
|
+
let entries: string[];
|
|
75
|
+
try {
|
|
76
|
+
entries = readdirSync(dir);
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const out: Array<{ name: string; path: string }> = [];
|
|
81
|
+
for (const name of entries) {
|
|
82
|
+
const skillDir = join(dir, name);
|
|
83
|
+
try {
|
|
84
|
+
if (!statSync(skillDir).isDirectory()) continue;
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (existsSync(join(skillDir, "SKILL.md")))
|
|
89
|
+
out.push({ name, path: skillDir });
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type InstalledPlugins = {
|
|
95
|
+
plugins?: Record<string, Array<{ installPath?: string }>>;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Skills exposed by currently-enabled installed plugins. */
|
|
99
|
+
export function listEnabledPluginSkills(opts: {
|
|
100
|
+
configDir: string;
|
|
101
|
+
enabled: Record<string, boolean>;
|
|
102
|
+
}): Array<{ plugin: string; skill_name: string; path: string }> {
|
|
103
|
+
const manifest = readJsonSafe<InstalledPlugins>(
|
|
104
|
+
join(opts.configDir, "plugins", "installed_plugins.json"),
|
|
105
|
+
);
|
|
106
|
+
const out: Array<{ plugin: string; skill_name: string; path: string }> = [];
|
|
107
|
+
if (!manifest?.plugins) return out;
|
|
108
|
+
for (const [key, installs] of Object.entries(manifest.plugins)) {
|
|
109
|
+
if (opts.enabled[key] !== true) continue; // only enabled plugins shadow
|
|
110
|
+
for (const inst of installs ?? []) {
|
|
111
|
+
if (!inst.installPath) continue;
|
|
112
|
+
for (const s of skillFolderNames(join(inst.installPath, "skills")))
|
|
113
|
+
out.push({ plugin: key, skill_name: s.name, path: s.path });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Skills under the global skills dir (`<configDir>/skills`). */
|
|
120
|
+
export function listGlobalSkills(
|
|
121
|
+
configDir: string,
|
|
122
|
+
): Array<{ skill_name: string; path: string }> {
|
|
123
|
+
return skillFolderNames(join(configDir, "skills")).map((s) => ({
|
|
124
|
+
skill_name: s.name,
|
|
125
|
+
path: s.path,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Which of `stagedSkillNames` are also discoverable from enabled plugins or the
|
|
131
|
+
* global skills dir. Matches on the skill folder name (exact).
|
|
132
|
+
*/
|
|
133
|
+
export function detectPluginShadows(opts: {
|
|
134
|
+
configDir: string;
|
|
135
|
+
cwd: string;
|
|
136
|
+
stagedSkillNames: string[];
|
|
137
|
+
}): PluginShadowReport {
|
|
138
|
+
const staged = new Set(opts.stagedSkillNames);
|
|
139
|
+
const enabled = resolveEnabledPlugins({
|
|
140
|
+
configDir: opts.configDir,
|
|
141
|
+
cwd: opts.cwd,
|
|
142
|
+
});
|
|
143
|
+
const shadowed: ShadowSource[] = [];
|
|
144
|
+
|
|
145
|
+
for (const s of listEnabledPluginSkills({
|
|
146
|
+
configDir: opts.configDir,
|
|
147
|
+
enabled,
|
|
148
|
+
}))
|
|
149
|
+
if (staged.has(s.skill_name))
|
|
150
|
+
shadowed.push({
|
|
151
|
+
kind: "plugin",
|
|
152
|
+
plugin: s.plugin,
|
|
153
|
+
skill_name: s.skill_name,
|
|
154
|
+
path: s.path,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
for (const s of listGlobalSkills(opts.configDir))
|
|
158
|
+
if (staged.has(s.skill_name))
|
|
159
|
+
shadowed.push({
|
|
160
|
+
kind: "global-skill",
|
|
161
|
+
skill_name: s.skill_name,
|
|
162
|
+
path: s.path,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return { config_dir: opts.configDir, shadowed };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function sourceLabel(s: ShadowSource): string {
|
|
169
|
+
return s.kind === "plugin"
|
|
170
|
+
? `enabled plugin '${s.plugin}'`
|
|
171
|
+
: "the global skills dir";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** One `validity_warnings` line per shadowed skill (for benchmark.json). */
|
|
175
|
+
export function shadowValidityWarnings(report: PluginShadowReport): string[] {
|
|
176
|
+
return report.shadowed.map(
|
|
177
|
+
(s) =>
|
|
178
|
+
`staged skill '${s.skill_name}' is also provided by ${sourceLabel(s)} — ` +
|
|
179
|
+
`eval subagents could discover both copies, so with/without results may be ` +
|
|
180
|
+
`contaminated. Re-run from an isolated session (see ${ISOLATION_DOC}).`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Build-time banner for the runner. Empty string when nothing is shadowed. */
|
|
185
|
+
export function formatShadowBanner(report: PluginShadowReport): string {
|
|
186
|
+
if (report.shadowed.length === 0) return "";
|
|
187
|
+
const lines = report.shadowed.map(
|
|
188
|
+
(s) => ` • ${s.skill_name} — ${sourceLabel(s)}`,
|
|
189
|
+
);
|
|
190
|
+
return [
|
|
191
|
+
"",
|
|
192
|
+
"⚠ Plugin-shadow warning: skills staged for this eval are ALSO discoverable",
|
|
193
|
+
" from your live environment:",
|
|
194
|
+
...lines,
|
|
195
|
+
" Eval subagents (dispatched via the Task tool) inherit this session's plugins,",
|
|
196
|
+
" so both the staged copy and the installed copy are discoverable — the",
|
|
197
|
+
" with/without comparison may be contaminated and the control arm is not truly",
|
|
198
|
+
" skill-absent. The runner cannot unload a plugin from a running session.",
|
|
199
|
+
` Re-run from an isolated session — see ${ISOLATION_DOC}.`,
|
|
200
|
+
].join("\n");
|
|
201
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Plan mode is active. The user wants to review an approach before any code is written, so you must NOT execute yet: do not make any edits, do not run any non-read-only tool, and do not change configs or system state. The only file you may write is the plan file. This constraint supersedes any other instruction you have received this session.
|
|
2
|
+
|
|
3
|
+
You are operating inside the harness's plan-mode workflow — a fixed, multi-phase procedure. Work through the phases in order:
|
|
4
|
+
|
|
5
|
+
1. **Understand.** Read the relevant code and gather context with read-only tools until you can describe the change concretely. Reuse what already exists rather than proposing new code.
|
|
6
|
+
2. **Design.** Decide the implementation approach and the trade-offs.
|
|
7
|
+
3. **Review.** Re-check the design against the user's request and resolve open questions with the user before finalizing.
|
|
8
|
+
4. **Write the plan.** Build the plan up incrementally in the plan file — this is the one file you are permitted to write. Name the files to change and how to verify the result.
|
|
9
|
+
5. **Hand off.** Call ExitPlanMode to submit the plan for the user's approval.
|
|
10
|
+
|
|
11
|
+
Terminal rail: your turn must end in exactly one of two ways — by asking the user a question, or by calling ExitPlanMode to present the finished plan. Do not stop for any other reason and do not begin implementation until the user has approved the plan. The plan-mode workflow already governs how you research, design, and present the work; stay on this rail through to ExitPlanMode.
|