@slowdini/slow-powers-opencode 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +37 -65
  2. package/bootstrap.md +1 -7
  3. package/opencode/plugins/slow-powers.js +1 -1
  4. package/package.json +14 -13
  5. package/skills/evaluating-skills/SKILL.md +91 -337
  6. package/skills/evaluating-skills/evals/baseline/BASELINE.md +23 -0
  7. package/skills/evaluating-skills/evals/baseline/NOTES.md +40 -0
  8. package/skills/evaluating-skills/evals/baseline/benchmark.json +54 -0
  9. package/skills/evaluating-skills/evals/baseline/grading/deterministic-edit-skip__new_skill.json +39 -0
  10. package/skills/evaluating-skills/evals/baseline/grading/deterministic-edit-skip__old_skill.json +39 -0
  11. package/skills/evaluating-skills/evals/baseline/grading/did-my-revision-help__new_skill.json +39 -0
  12. package/skills/evaluating-skills/evals/baseline/grading/did-my-revision-help__old_skill.json +39 -0
  13. package/skills/evaluating-skills/evals/baseline/grading/is-new-skill-ready-to-ship__new_skill.json +32 -0
  14. package/skills/evaluating-skills/evals/baseline/grading/is-new-skill-ready-to-ship__old_skill.json +32 -0
  15. package/skills/test-driven-development/evals/baseline/NOTES.md +2 -2
  16. package/skills/verifying-development-work/SKILL.md +17 -6
  17. package/skills/verifying-development-work/code-review.md +68 -0
  18. package/skills/verifying-development-work/comment-review.md +85 -0
  19. package/skills/verifying-development-work/evals/baseline/BASELINE.md +7 -6
  20. package/skills/verifying-development-work/evals/baseline/NOTES.md +83 -149
  21. package/skills/verifying-development-work/evals/baseline/benchmark.json +32 -31
  22. package/skills/verifying-development-work/evals/baseline/grading/comment-hygiene-at-handoff__new_skill.json +53 -0
  23. package/skills/verifying-development-work/evals/baseline/grading/comment-hygiene-at-handoff__old_skill.json +53 -0
  24. package/skills/verifying-development-work/evals/baseline/grading/wrap-it-up-handoff__new_skill.json +53 -0
  25. package/skills/verifying-development-work/evals/baseline/grading/wrap-it-up-handoff__old_skill.json +53 -0
  26. package/skills/verifying-development-work/evals/evals.json +34 -2
  27. package/skills/verifying-development-work/evals/fixtures/comment-hygiene-at-handoff/slugify.test.ts +14 -0
  28. package/skills/verifying-development-work/evals/fixtures/comment-hygiene-at-handoff/slugify.ts +25 -0
  29. package/skills/evaluating-skills/examples/verifying-development-work-evals.json +0 -30
  30. package/skills/evaluating-skills/harness-details/claude.md +0 -158
  31. package/skills/evaluating-skills/runner/README.md +0 -154
  32. package/skills/evaluating-skills/runner/adapters/claude-code-session.test.ts +0 -56
  33. package/skills/evaluating-skills/runner/adapters/claude-code-session.ts +0 -43
  34. package/skills/evaluating-skills/runner/adapters/claude-code-transcript.test.ts +0 -263
  35. package/skills/evaluating-skills/runner/adapters/claude-code-transcript.ts +0 -146
  36. package/skills/evaluating-skills/runner/aggregate.test.ts +0 -264
  37. package/skills/evaluating-skills/runner/aggregate.ts +0 -248
  38. package/skills/evaluating-skills/runner/context.test.ts +0 -181
  39. package/skills/evaluating-skills/runner/context.ts +0 -90
  40. package/skills/evaluating-skills/runner/detect-stray-writes.test.ts +0 -103
  41. package/skills/evaluating-skills/runner/detect-stray-writes.ts +0 -192
  42. package/skills/evaluating-skills/runner/fill-transcripts.test.ts +0 -73
  43. package/skills/evaluating-skills/runner/fill-transcripts.ts +0 -154
  44. package/skills/evaluating-skills/runner/grade.test.ts +0 -347
  45. package/skills/evaluating-skills/runner/grade.ts +0 -603
  46. package/skills/evaluating-skills/runner/guard/guard.ts +0 -49
  47. package/skills/evaluating-skills/runner/guard/install.test.ts +0 -92
  48. package/skills/evaluating-skills/runner/guard/install.ts +0 -147
  49. package/skills/evaluating-skills/runner/guard/policy.test.ts +0 -71
  50. package/skills/evaluating-skills/runner/guard/policy.ts +0 -74
  51. package/skills/evaluating-skills/runner/plugin-shadow.test.ts +0 -228
  52. package/skills/evaluating-skills/runner/plugin-shadow.ts +0 -201
  53. package/skills/evaluating-skills/runner/profiles/claude-code/plan-mode.md +0 -11
  54. package/skills/evaluating-skills/runner/promote-baseline.test.ts +0 -230
  55. package/skills/evaluating-skills/runner/promote-baseline.ts +0 -186
  56. package/skills/evaluating-skills/runner/run.test.ts +0 -1180
  57. package/skills/evaluating-skills/runner/run.ts +0 -1029
  58. package/skills/evaluating-skills/runner/sandbox-policy.ts +0 -74
  59. package/skills/evaluating-skills/runner/types.ts +0 -112
  60. package/skills/evaluating-skills/runner/validate-all.ts +0 -54
  61. package/skills/evaluating-skills/runner/validate-schema.test.ts +0 -99
  62. package/skills/evaluating-skills/runner/validate-schema.ts +0 -51
  63. package/skills/evaluating-skills/runner/validate.test.ts +0 -56
  64. package/skills/evaluating-skills/runner/validate.ts +0 -21
  65. package/skills/evaluating-skills/schema/evals.schema.json +0 -105
  66. package/skills/evaluating-skills/schema/grading.schema.json +0 -84
  67. package/skills/evaluating-skills/schema/run-record.schema.json +0 -80
  68. package/skills/evaluating-skills/schema/stray-writes.schema.json +0 -68
  69. package/skills/evaluating-skills/templates/eval-task-prompt.md +0 -67
  70. package/skills/evaluating-skills/templates/evals.json.example +0 -17
  71. package/skills/evaluating-skills/templates/judge-prompt.md +0 -56
  72. package/skills/evaluating-skills/templates/revise-skill-prompt.md +0 -56
  73. package/skills/verifying-development-work/evals/baseline/grading/bug-fixed-without-reproducing__with_skill.json +0 -39
  74. package/skills/verifying-development-work/evals/baseline/grading/bug-fixed-without-reproducing__without_skill.json +0 -24
  75. package/skills/verifying-development-work/evals/baseline/grading/build-implied-by-edit__with_skill.json +0 -46
  76. package/skills/verifying-development-work/evals/baseline/grading/build-implied-by-edit__without_skill.json +0 -31
  77. package/skills/verifying-development-work/evals/baseline/grading/claim-without-running__with_skill.json +0 -46
  78. package/skills/verifying-development-work/evals/baseline/grading/claim-without-running__without_skill.json +0 -31
  79. package/skills/verifying-development-work/evals/baseline/grading/seeded-done-tests-pass-ship-it__with_skill.json +0 -46
  80. package/skills/verifying-development-work/evals/baseline/grading/seeded-done-tests-pass-ship-it__without_skill.json +0 -31
  81. package/skills/verifying-development-work/evals/baseline/grading/wrap-it-up-handoff__with_skill.json +0 -53
  82. package/skills/verifying-development-work/evals/baseline/grading/wrap-it-up-handoff__without_skill.json +0 -38
@@ -1,92 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import {
3
- existsSync,
4
- mkdirSync,
5
- readFileSync,
6
- rmSync,
7
- writeFileSync,
8
- } from "node:fs";
9
- import { tmpdir } from "node:os";
10
- import { join } from "node:path";
11
- import {
12
- GUARD_MANIFEST,
13
- GUARD_MARKER,
14
- installGuard,
15
- teardownGuard,
16
- } from "./install";
17
-
18
- const ROOT = join(tmpdir(), `guard-install-test-${process.pid}`);
19
-
20
- afterEach(() => rmSync(ROOT, { recursive: true, force: true }));
21
-
22
- function setup() {
23
- const stageRoot = join(ROOT, `case-${Math.random().toString(36).slice(2)}`);
24
- mkdirSync(stageRoot, { recursive: true });
25
- const workspaceRoot = join(stageRoot, "skills-workspace");
26
- return { stageRoot, workspaceRoot };
27
- }
28
-
29
- const skillsDir = (s: string) => join(s, ".claude", "skills");
30
- const settingsPath = (s: string) => join(s, ".claude", "settings.local.json");
31
-
32
- describe("installGuard / teardownGuard", () => {
33
- test("install writes an active marker, hook, and manifest", () => {
34
- const { stageRoot, workspaceRoot } = setup();
35
- installGuard({ stageRoot, workspaceRoot, guardScriptPath: "/g/guard.ts" });
36
-
37
- const marker = JSON.parse(
38
- readFileSync(join(skillsDir(stageRoot), GUARD_MARKER), "utf8"),
39
- );
40
- expect(marker.active).toBe(true);
41
- expect(Date.parse(marker.expiresAt)).toBeGreaterThan(Date.now());
42
- expect(
43
- marker.allowedRoots.some((r: string) => r.includes("skills-workspace")),
44
- ).toBe(true);
45
-
46
- const settings = JSON.parse(readFileSync(settingsPath(stageRoot), "utf8"));
47
- expect(settings.hooks.PreToolUse[0].matcher).toContain("Write");
48
- expect(settings.hooks.PreToolUse[0].hooks[0].command).toContain("guard.ts");
49
-
50
- expect(existsSync(join(skillsDir(stageRoot), GUARD_MANIFEST))).toBe(true);
51
- });
52
-
53
- test("teardown deletes settings.local.json it created", () => {
54
- const { stageRoot, workspaceRoot } = setup();
55
- installGuard({ stageRoot, workspaceRoot, guardScriptPath: "/g/guard.ts" });
56
- expect(existsSync(settingsPath(stageRoot))).toBe(true);
57
-
58
- expect(teardownGuard(stageRoot)).toBe(true);
59
- expect(existsSync(settingsPath(stageRoot))).toBe(false);
60
- expect(existsSync(join(skillsDir(stageRoot), GUARD_MARKER))).toBe(false);
61
- expect(existsSync(join(skillsDir(stageRoot), GUARD_MANIFEST))).toBe(false);
62
- });
63
-
64
- test("teardown restores a pre-existing settings.local.json verbatim", () => {
65
- const { stageRoot, workspaceRoot } = setup();
66
- mkdirSync(join(stageRoot, ".claude"), { recursive: true });
67
- const original = `${JSON.stringify({ permissions: { allow: ["Bash(ls)"] } }, null, 2)}\n`;
68
- writeFileSync(settingsPath(stageRoot), original);
69
-
70
- installGuard({ stageRoot, workspaceRoot, guardScriptPath: "/g/guard.ts" });
71
- // hook present while armed
72
- expect(readFileSync(settingsPath(stageRoot), "utf8")).toContain(
73
- "PreToolUse",
74
- );
75
-
76
- teardownGuard(stageRoot);
77
- expect(readFileSync(settingsPath(stageRoot), "utf8")).toBe(original);
78
- });
79
-
80
- test("teardown is a safe no-op when nothing is installed", () => {
81
- const { stageRoot } = setup();
82
- expect(teardownGuard(stageRoot)).toBe(false);
83
- });
84
-
85
- test("teardown sweeps a stray marker even without a manifest", () => {
86
- const { stageRoot } = setup();
87
- mkdirSync(skillsDir(stageRoot), { recursive: true });
88
- writeFileSync(join(skillsDir(stageRoot), GUARD_MARKER), "{}");
89
- expect(teardownGuard(stageRoot)).toBe(true);
90
- expect(existsSync(join(skillsDir(stageRoot), GUARD_MARKER))).toBe(false);
91
- });
92
- });
@@ -1,147 +0,0 @@
1
- import {
2
- existsSync,
3
- mkdirSync,
4
- readFileSync,
5
- rmSync,
6
- writeFileSync,
7
- } from "node:fs";
8
- import { tmpdir } from "node:os";
9
- import { join, resolve } from "node:path";
10
-
11
- export const GUARD_MARKER = ".slow-powers-eval-guard.json";
12
- export const GUARD_MANIFEST = ".slow-powers-eval-guard-manifest.json";
13
- const GUARD_TTL_MS = 6 * 60 * 60 * 1000; // 6h — bounds a crashed run's lingering hook
14
-
15
- const HOOK_MATCHER = "Write|Edit|MultiEdit|NotebookEdit|Bash";
16
-
17
- type GuardManifest = {
18
- created_at: string;
19
- settings_path: string;
20
- settings_existed: boolean;
21
- settings_backup: string | null;
22
- marker_path: string;
23
- };
24
-
25
- type Settings = {
26
- hooks?: {
27
- PreToolUse?: Array<{ matcher?: string; hooks?: unknown[] }>;
28
- [k: string]: unknown;
29
- };
30
- [k: string]: unknown;
31
- };
32
-
33
- /**
34
- * Arm the Claude Code write guard for an eval run. Writes a marker listing the
35
- * allowed roots and merges a `PreToolUse` hook into `.claude/settings.local.json`
36
- * that runs `guard.ts` on every Write/Edit/Bash. The original settings file is
37
- * backed up verbatim in a manifest so {@link teardownGuard} restores it exactly.
38
- *
39
- * Returns the marker path. The guard is a no-op until this marker exists and is
40
- * unexpired (see guard/policy.ts), so the hook is inert outside an active run.
41
- */
42
- export function installGuard(opts: {
43
- stageRoot: string;
44
- workspaceRoot: string;
45
- guardScriptPath: string;
46
- ttlMs?: number;
47
- }): string {
48
- const skillsDir = join(opts.stageRoot, ".claude", "skills");
49
- mkdirSync(skillsDir, { recursive: true });
50
-
51
- const markerPath = join(skillsDir, GUARD_MARKER);
52
- const allowedRoots = [
53
- resolve(opts.workspaceRoot),
54
- resolve(skillsDir),
55
- resolve(tmpdir()),
56
- ];
57
- writeFileSync(
58
- markerPath,
59
- `${JSON.stringify(
60
- {
61
- active: true,
62
- allowedRoots,
63
- expiresAt: new Date(
64
- Date.now() + (opts.ttlMs ?? GUARD_TTL_MS),
65
- ).toISOString(),
66
- },
67
- null,
68
- 2,
69
- )}\n`,
70
- );
71
-
72
- const settingsPath = join(opts.stageRoot, ".claude", "settings.local.json");
73
- const settingsExisted = existsSync(settingsPath);
74
- const backup = settingsExisted ? readFileSync(settingsPath, "utf8") : null;
75
-
76
- let settings: Settings = {};
77
- if (backup) {
78
- try {
79
- settings = JSON.parse(backup);
80
- } catch {
81
- settings = {};
82
- }
83
- }
84
- settings.hooks ??= {};
85
- settings.hooks.PreToolUse ??= [];
86
- settings.hooks.PreToolUse.push({
87
- matcher: HOOK_MATCHER,
88
- hooks: [
89
- {
90
- type: "command",
91
- command: `bun run "${opts.guardScriptPath}" "${markerPath}"`,
92
- },
93
- ],
94
- });
95
- writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
96
-
97
- const manifest: GuardManifest = {
98
- created_at: new Date().toISOString(),
99
- settings_path: settingsPath,
100
- settings_existed: settingsExisted,
101
- settings_backup: backup,
102
- marker_path: markerPath,
103
- };
104
- writeFileSync(
105
- join(skillsDir, GUARD_MANIFEST),
106
- `${JSON.stringify(manifest, null, 2)}\n`,
107
- );
108
- return markerPath;
109
- }
110
-
111
- /**
112
- * Disarm the guard: restore the original `settings.local.json` (or delete it if
113
- * we created it) and remove the marker + manifest. Safe to call when no guard is
114
- * installed. Returns true if a guard was found and torn down.
115
- */
116
- export function teardownGuard(stageRoot: string): boolean {
117
- const skillsDir = join(stageRoot, ".claude", "skills");
118
- const manifestPath = join(skillsDir, GUARD_MANIFEST);
119
- const markerPath = join(skillsDir, GUARD_MARKER);
120
-
121
- if (!existsSync(manifestPath)) {
122
- // No manifest — still sweep a stray marker so the guard can't stay armed.
123
- if (existsSync(markerPath)) {
124
- rmSync(markerPath, { force: true });
125
- return true;
126
- }
127
- return false;
128
- }
129
-
130
- let manifest: GuardManifest;
131
- try {
132
- manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
133
- } catch {
134
- rmSync(manifestPath, { force: true });
135
- rmSync(markerPath, { force: true });
136
- return true;
137
- }
138
-
139
- if (manifest.settings_existed && manifest.settings_backup !== null) {
140
- writeFileSync(manifest.settings_path, manifest.settings_backup);
141
- } else if (existsSync(manifest.settings_path)) {
142
- rmSync(manifest.settings_path, { force: true });
143
- }
144
- rmSync(manifest.marker_path, { force: true });
145
- rmSync(manifestPath, { force: true });
146
- return true;
147
- }
@@ -1,71 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { decide, type GuardMarker } from "./policy";
3
-
4
- const ROOTS = ["/work/skills-workspace", "/work/.claude/skills"];
5
- const future = () => new Date(Date.now() + 60_000).toISOString();
6
- const past = () => new Date(Date.now() - 60_000).toISOString();
7
-
8
- function marker(over: Partial<GuardMarker> = {}): GuardMarker {
9
- return { active: true, allowedRoots: ROOTS, expiresAt: future(), ...over };
10
- }
11
-
12
- describe("guard decide", () => {
13
- test("allows everything when marker is null (guard inactive)", () => {
14
- expect(decide("Write", { file_path: "/etc/passwd" }, null).allow).toBe(
15
- true,
16
- );
17
- });
18
-
19
- test("allows everything when marker is inactive or expired", () => {
20
- expect(
21
- decide("Write", { file_path: "/etc/passwd" }, marker({ active: false }))
22
- .allow,
23
- ).toBe(true);
24
- expect(
25
- decide(
26
- "Write",
27
- { file_path: "/etc/passwd" },
28
- marker({ expiresAt: past() }),
29
- ).allow,
30
- ).toBe(true);
31
- });
32
-
33
- test("allows a write under an allowed root", () => {
34
- expect(
35
- decide(
36
- "Write",
37
- { file_path: "/work/skills-workspace/x/outputs/a.md" },
38
- marker(),
39
- ).allow,
40
- ).toBe(true);
41
- });
42
-
43
- test("denies a write outside all allowed roots", () => {
44
- const d = decide("Edit", { file_path: "/work/runner/run.ts" }, marker());
45
- expect(d.allow).toBe(false);
46
- expect(d.reason).toMatch(/outside/i);
47
- });
48
-
49
- test("denies an install command", () => {
50
- const d = decide("Bash", { command: "npm install left-pad" }, marker());
51
- expect(d.allow).toBe(false);
52
- expect(d.reason).toMatch(/install/i);
53
- });
54
-
55
- test("allows a Bash command scoped to an allowed root", () => {
56
- expect(
57
- decide(
58
- "Bash",
59
- { command: "echo hi > /work/skills-workspace/x/outputs/log" },
60
- marker(),
61
- ).allow,
62
- ).toBe(true);
63
- });
64
-
65
- test("allows non-mutating Bash and read tools", () => {
66
- expect(decide("Bash", { command: "ls -la /" }, marker()).allow).toBe(true);
67
- expect(decide("Read", { file_path: "/etc/passwd" }, marker()).allow).toBe(
68
- true,
69
- );
70
- });
71
- });
@@ -1,74 +0,0 @@
1
- import {
2
- classifyBash,
3
- isUnderAny,
4
- pathArg,
5
- WRITE_TOOLS,
6
- } from "../sandbox-policy";
7
-
8
- /**
9
- * The marker file (`<stageRoot>/.claude/skills/.slow-powers-eval-guard.json`)
10
- * that arms the guard. The guard is a no-op unless this file exists, is active,
11
- * and has not expired — so a crashed run that never tore the hook down can't
12
- * silently block writes in the user's next interactive session.
13
- */
14
- export type GuardMarker = {
15
- active?: boolean;
16
- allowedRoots?: string[];
17
- expiresAt?: string;
18
- };
19
-
20
- export type GuardDecision = { allow: boolean; reason?: string };
21
-
22
- const ALLOW: GuardDecision = { allow: true };
23
-
24
- function armed(marker: GuardMarker | null, now: number): boolean {
25
- if (marker?.active !== true) return false;
26
- if (marker.expiresAt && Date.parse(marker.expiresAt) <= now) return false;
27
- return true;
28
- }
29
-
30
- /**
31
- * Decide whether a tool call should be allowed while the eval guard is armed.
32
- *
33
- * Write tools targeting a path outside every allowed root are denied; Bash
34
- * commands matching a mutation pattern (install/git/sed -i/redirection) that
35
- * aren't scoped to an allowed root are denied. Everything else — including all
36
- * read-only tools and the orchestrator's own writes under the workspace — is
37
- * allowed. When the guard is not armed, every call is allowed.
38
- */
39
- export function decide(
40
- toolName: string,
41
- toolInput: unknown,
42
- marker: GuardMarker | null,
43
- now: number = Date.now(),
44
- ): GuardDecision {
45
- if (!armed(marker, now)) return ALLOW;
46
- const roots = marker?.allowedRoots ?? [];
47
- const repoRoot = process.cwd();
48
-
49
- if (WRITE_TOOLS.has(toolName)) {
50
- const p = pathArg(toolInput);
51
- if (p && !isUnderAny(p, roots, repoRoot)) {
52
- return {
53
- allow: false,
54
- reason: `eval guard: ${toolName} to ${p} is outside the eval sandbox (allowed: ${roots.join(", ")})`,
55
- };
56
- }
57
- return ALLOW;
58
- }
59
-
60
- if (toolName === "Bash") {
61
- const command =
62
- toolInput && typeof toolInput === "object"
63
- ? String((toolInput as Record<string, unknown>).command ?? "")
64
- : "";
65
- const reason = classifyBash(command, roots);
66
- if (reason)
67
- return {
68
- allow: false,
69
- reason: `eval guard: blocked Bash (${reason}) — runs outside the eval sandbox`,
70
- };
71
- }
72
-
73
- return ALLOW;
74
- }
@@ -1,228 +0,0 @@
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
- });