@slowdini/slow-powers-opencode 0.1.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.
- package/LICENSE +22 -0
- package/README.md +174 -0
- package/bootstrap.md +16 -0
- package/opencode/plugins/slow-powers.js +86 -0
- package/package.json +66 -0
- package/skills/auditing-slow-powers-usage/SKILL.md +157 -0
- package/skills/auditing-slow-powers-usage/evals/baseline/BASELINE.md +22 -0
- package/skills/auditing-slow-powers-usage/evals/baseline/NOTES.md +72 -0
- package/skills/auditing-slow-powers-usage/evals/baseline/benchmark.json +53 -0
- package/skills/auditing-slow-powers-usage/evals/baseline/grading/audits-blindspot-session__with_skill.json +53 -0
- package/skills/auditing-slow-powers-usage/evals/baseline/grading/audits-blindspot-session__without_skill.json +38 -0
- package/skills/auditing-slow-powers-usage/evals/baseline/grading/audits-completed-session__with_skill.json +53 -0
- package/skills/auditing-slow-powers-usage/evals/baseline/grading/audits-completed-session__without_skill.json +38 -0
- package/skills/auditing-slow-powers-usage/evals/baseline/grading/ordinary-dev-task-no-audit__with_skill.json +17 -0
- package/skills/auditing-slow-powers-usage/evals/baseline/grading/ordinary-dev-task-no-audit__without_skill.json +17 -0
- package/skills/auditing-slow-powers-usage/evals/evals.json +74 -0
- package/skills/auditing-slow-powers-usage/evals/fixtures/audits-blindspot-session/session-summary.md +39 -0
- package/skills/auditing-slow-powers-usage/evals/fixtures/audits-completed-session/session-summary.md +33 -0
- package/skills/evaluating-skills/SKILL.md +448 -0
- package/skills/evaluating-skills/evals/evals.json +52 -0
- package/skills/evaluating-skills/evals/fixtures/iron-law/candidate-skill.md +13 -0
- package/skills/evaluating-skills/examples/verification-before-completion-evals.json +30 -0
- package/skills/evaluating-skills/harness-details/claude.md +135 -0
- package/skills/evaluating-skills/pressure-scenarios.md +163 -0
- package/skills/evaluating-skills/runner/README.md +140 -0
- package/skills/evaluating-skills/runner/adapters/claude-code-transcript.test.ts +263 -0
- package/skills/evaluating-skills/runner/adapters/claude-code-transcript.ts +146 -0
- package/skills/evaluating-skills/runner/aggregate.test.ts +188 -0
- package/skills/evaluating-skills/runner/aggregate.ts +228 -0
- package/skills/evaluating-skills/runner/context.test.ts +181 -0
- package/skills/evaluating-skills/runner/context.ts +90 -0
- package/skills/evaluating-skills/runner/detect-stray-writes.test.ts +103 -0
- package/skills/evaluating-skills/runner/detect-stray-writes.ts +192 -0
- package/skills/evaluating-skills/runner/fill-transcripts.test.ts +73 -0
- package/skills/evaluating-skills/runner/fill-transcripts.ts +154 -0
- package/skills/evaluating-skills/runner/grade.test.ts +347 -0
- package/skills/evaluating-skills/runner/grade.ts +603 -0
- package/skills/evaluating-skills/runner/guard/guard.ts +49 -0
- package/skills/evaluating-skills/runner/guard/install.test.ts +92 -0
- package/skills/evaluating-skills/runner/guard/install.ts +147 -0
- package/skills/evaluating-skills/runner/guard/policy.test.ts +71 -0
- package/skills/evaluating-skills/runner/guard/policy.ts +74 -0
- package/skills/evaluating-skills/runner/promote-baseline.test.ts +230 -0
- package/skills/evaluating-skills/runner/promote-baseline.ts +186 -0
- package/skills/evaluating-skills/runner/run.test.ts +716 -0
- package/skills/evaluating-skills/runner/run.ts +814 -0
- package/skills/evaluating-skills/runner/sandbox-policy.ts +74 -0
- package/skills/evaluating-skills/runner/types.ts +104 -0
- package/skills/evaluating-skills/runner/validate-all.ts +54 -0
- package/skills/evaluating-skills/runner/validate-schema.test.ts +99 -0
- package/skills/evaluating-skills/runner/validate-schema.ts +51 -0
- package/skills/evaluating-skills/runner/validate.test.ts +56 -0
- package/skills/evaluating-skills/runner/validate.ts +21 -0
- package/skills/evaluating-skills/schema/evals.schema.json +105 -0
- package/skills/evaluating-skills/schema/grading.schema.json +84 -0
- package/skills/evaluating-skills/schema/run-record.schema.json +80 -0
- package/skills/evaluating-skills/schema/stray-writes.schema.json +68 -0
- package/skills/evaluating-skills/templates/eval-task-prompt.md +71 -0
- package/skills/evaluating-skills/templates/evals.json.example +17 -0
- package/skills/evaluating-skills/templates/judge-prompt.md +56 -0
- package/skills/evaluating-skills/templates/revise-skill-prompt.md +56 -0
- package/skills/finishing-a-development-branch/SKILL.md +96 -0
- package/skills/finishing-a-development-branch/evals/evals.json +41 -0
- package/skills/finishing-a-development-branch/evals/fixtures/finish/package.json +4 -0
- package/skills/finishing-a-development-branch/evals/fixtures/finish/sum.test.ts +5 -0
- package/skills/hardening-plans/SKILL.md +72 -0
- package/skills/hardening-plans/evals/baseline/BASELINE.md +22 -0
- package/skills/hardening-plans/evals/baseline/NOTES.md +58 -0
- package/skills/hardening-plans/evals/baseline/benchmark.json +54 -0
- package/skills/hardening-plans/evals/baseline/grading/concrete-todo-app-plan__new_skill.json +39 -0
- package/skills/hardening-plans/evals/baseline/grading/concrete-todo-app-plan__old_skill.json +39 -0
- package/skills/hardening-plans/evals/baseline/grading/csv-parser-bug-no-plan__new_skill.json +24 -0
- package/skills/hardening-plans/evals/baseline/grading/csv-parser-bug-no-plan__old_skill.json +24 -0
- package/skills/hardening-plans/evals/baseline/grading/seeded-review-catches-defects__new_skill.json +46 -0
- package/skills/hardening-plans/evals/baseline/grading/seeded-review-catches-defects__old_skill.json +46 -0
- package/skills/hardening-plans/evals/evals.json +114 -0
- package/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/skills/systematic-debugging/SKILL.md +84 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +164 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/evals/baseline/BASELINE.md +22 -0
- package/skills/systematic-debugging/evals/baseline/benchmark.json +51 -0
- package/skills/systematic-debugging/evals/baseline/grading/feature-request-no-debugging__with_skill.json +17 -0
- package/skills/systematic-debugging/evals/baseline/grading/feature-request-no-debugging__without_skill.json +17 -0
- package/skills/systematic-debugging/evals/baseline/grading/null-id-crash-investigate-first__with_skill.json +46 -0
- package/skills/systematic-debugging/evals/baseline/grading/null-id-crash-investigate-first__without_skill.json +31 -0
- package/skills/systematic-debugging/evals/evals.json +45 -0
- package/skills/systematic-debugging/evals/fixtures/order-bug/orderHandler.ts +9 -0
- package/skills/systematic-debugging/evals/fixtures/order-bug/repro.ts +10 -0
- package/skills/systematic-debugging/find-polluter.sh +63 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/systematic-debugging/test-academic.md +14 -0
- package/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/skills/test-driven-development/SKILL.md +93 -0
- package/skills/test-driven-development/evals/baseline/BASELINE.md +22 -0
- package/skills/test-driven-development/evals/baseline/NOTES.md +74 -0
- package/skills/test-driven-development/evals/baseline/benchmark.json +51 -0
- package/skills/test-driven-development/evals/baseline/grading/slugify-under-time-pressure__with_skill.json +53 -0
- package/skills/test-driven-development/evals/baseline/grading/slugify-under-time-pressure__without_skill.json +38 -0
- package/skills/test-driven-development/evals/baseline/grading/tests-after-rubber-stamp__with_skill.json +32 -0
- package/skills/test-driven-development/evals/baseline/grading/tests-after-rubber-stamp__without_skill.json +17 -0
- package/skills/test-driven-development/evals/evals.json +77 -0
- package/skills/test-driven-development/evals/fixtures/slugify/package.json +4 -0
- package/skills/test-driven-development/evals/fixtures/slugify/utils.ts +7 -0
- package/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/skills/using-git-worktrees/SKILL.md +70 -0
- package/skills/using-git-worktrees/evals/evals.json +40 -0
- package/skills/verification-before-completion/SKILL.md +65 -0
- package/skills/verification-before-completion/evals/baseline/BASELINE.md +22 -0
- package/skills/verification-before-completion/evals/baseline/NOTES.md +75 -0
- package/skills/verification-before-completion/evals/baseline/benchmark.json +51 -0
- package/skills/verification-before-completion/evals/baseline/grading/bug-fixed-without-reproducing__with_skill.json +39 -0
- package/skills/verification-before-completion/evals/baseline/grading/bug-fixed-without-reproducing__without_skill.json +24 -0
- package/skills/verification-before-completion/evals/baseline/grading/build-implied-by-edit__with_skill.json +46 -0
- package/skills/verification-before-completion/evals/baseline/grading/build-implied-by-edit__without_skill.json +31 -0
- package/skills/verification-before-completion/evals/baseline/grading/claim-without-running__with_skill.json +46 -0
- package/skills/verification-before-completion/evals/baseline/grading/claim-without-running__without_skill.json +31 -0
- package/skills/verification-before-completion/evals/evals.json +77 -0
- package/skills/verification-before-completion/evals/fixtures/build-implied-by-edit/api.ts +1 -0
- package/skills/verification-before-completion/evals/fixtures/build-implied-by-edit/consumer.ts +3 -0
- package/skills/verification-before-completion/evals/fixtures/build-implied-by-edit/tsconfig.json +23 -0
- package/skills/verification-before-completion/evals/fixtures/claim-without-running/sum.test.ts +10 -0
- package/skills/verification-before-completion/evals/fixtures/claim-without-running/sum.ts +1 -0
- package/skills/writing-skills/SKILL.md +306 -0
- package/skills/writing-skills/evals/evals.json +40 -0
- package/skills/writing-skills/graphviz-conventions.dot +172 -0
- package/skills/writing-skills/persuasion-principles.md +187 -0
- package/skills/writing-skills/scripts/render-graphs.js +181 -0
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
buildDispatchTask,
|
|
14
|
+
cleanupStagedSkills,
|
|
15
|
+
redactSkillFromBootstrap,
|
|
16
|
+
STAGED_SIBLING_MANIFEST,
|
|
17
|
+
STAGED_SKILL_PREFIX,
|
|
18
|
+
stageSiblingSkills,
|
|
19
|
+
stageSkillForCC,
|
|
20
|
+
} from "./run";
|
|
21
|
+
|
|
22
|
+
const FIXTURE_ROOT = join(tmpdir(), `slow-powers-run-test-${process.pid}`);
|
|
23
|
+
|
|
24
|
+
beforeAll(() => {
|
|
25
|
+
mkdirSync(FIXTURE_ROOT, { recursive: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterAll(() => {
|
|
29
|
+
rmSync(FIXTURE_ROOT, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("stageSkillForCC", () => {
|
|
33
|
+
test("writes SKILL.md to <repoRoot>/.claude/skills/<slug>/SKILL.md and returns the slug", () => {
|
|
34
|
+
const repoRoot = join(FIXTURE_ROOT, "stage-basic");
|
|
35
|
+
mkdirSync(repoRoot, { recursive: true });
|
|
36
|
+
const content =
|
|
37
|
+
"---\nname: example\ndescription: example skill\n---\n\nbody\n";
|
|
38
|
+
|
|
39
|
+
const slug = stageSkillForCC({
|
|
40
|
+
content,
|
|
41
|
+
iteration: 3,
|
|
42
|
+
condition: "with_skill",
|
|
43
|
+
skillName: "verification-before-completion",
|
|
44
|
+
repoRoot,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(slug).toBe(
|
|
48
|
+
`${STAGED_SKILL_PREFIX}3-with_skill__verification-before-completion`,
|
|
49
|
+
);
|
|
50
|
+
const stagedPath = join(repoRoot, ".claude", "skills", slug, "SKILL.md");
|
|
51
|
+
expect(existsSync(stagedPath)).toBe(true);
|
|
52
|
+
expect(readFileSync(stagedPath, "utf8")).toBe(content);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("overwrites an existing staged skill at the same slug", () => {
|
|
56
|
+
const repoRoot = join(FIXTURE_ROOT, "stage-overwrite");
|
|
57
|
+
mkdirSync(repoRoot, { recursive: true });
|
|
58
|
+
|
|
59
|
+
stageSkillForCC({
|
|
60
|
+
content: "first",
|
|
61
|
+
iteration: 1,
|
|
62
|
+
condition: "with_skill",
|
|
63
|
+
skillName: "s",
|
|
64
|
+
repoRoot,
|
|
65
|
+
});
|
|
66
|
+
const slug = stageSkillForCC({
|
|
67
|
+
content: "second",
|
|
68
|
+
iteration: 1,
|
|
69
|
+
condition: "with_skill",
|
|
70
|
+
skillName: "s",
|
|
71
|
+
repoRoot,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const stagedPath = join(repoRoot, ".claude", "skills", slug, "SKILL.md");
|
|
75
|
+
expect(readFileSync(stagedPath, "utf8")).toBe("second");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("cleanupStagedSkills", () => {
|
|
80
|
+
test("removes only directories with the staged-skill prefix under .claude/skills", () => {
|
|
81
|
+
const repoRoot = join(FIXTURE_ROOT, "cleanup");
|
|
82
|
+
const skillsDir = join(repoRoot, ".claude", "skills");
|
|
83
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
84
|
+
|
|
85
|
+
const stagedA = join(skillsDir, `${STAGED_SKILL_PREFIX}1-with_skill__foo`);
|
|
86
|
+
const stagedB = join(skillsDir, `${STAGED_SKILL_PREFIX}1-new_skill__bar`);
|
|
87
|
+
const productionLike = join(skillsDir, "user-custom-skill");
|
|
88
|
+
mkdirSync(stagedA, { recursive: true });
|
|
89
|
+
mkdirSync(stagedB, { recursive: true });
|
|
90
|
+
mkdirSync(productionLike, { recursive: true });
|
|
91
|
+
|
|
92
|
+
cleanupStagedSkills(repoRoot);
|
|
93
|
+
|
|
94
|
+
expect(existsSync(stagedA)).toBe(false);
|
|
95
|
+
expect(existsSync(stagedB)).toBe(false);
|
|
96
|
+
expect(existsSync(productionLike)).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("is a no-op when .claude/skills does not exist", () => {
|
|
100
|
+
const repoRoot = join(FIXTURE_ROOT, "cleanup-empty");
|
|
101
|
+
mkdirSync(repoRoot, { recursive: true });
|
|
102
|
+
expect(() => cleanupStagedSkills(repoRoot)).not.toThrow();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("stageSiblingSkills", () => {
|
|
107
|
+
function buildSourceSkills(root: string): string {
|
|
108
|
+
const src = join(root, "src-skills");
|
|
109
|
+
mkdirSync(join(src, "alpha", "evals"), { recursive: true });
|
|
110
|
+
writeFileSync(join(src, "alpha", "SKILL.md"), "alpha content");
|
|
111
|
+
writeFileSync(join(src, "alpha", "helper.md"), "alpha helper");
|
|
112
|
+
writeFileSync(join(src, "alpha", "evals", "evals.json"), "{}");
|
|
113
|
+
mkdirSync(join(src, "beta"), { recursive: true });
|
|
114
|
+
writeFileSync(join(src, "beta", "SKILL.md"), "beta content");
|
|
115
|
+
mkdirSync(join(src, "gamma"), { recursive: true });
|
|
116
|
+
writeFileSync(join(src, "gamma", "SKILL.md"), "gamma content");
|
|
117
|
+
return src;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
test("stages each sibling at .claude/skills/<name>/ with full content minus evals/", () => {
|
|
121
|
+
const root = join(FIXTURE_ROOT, "sibling-basic");
|
|
122
|
+
mkdirSync(root, { recursive: true });
|
|
123
|
+
const src = buildSourceSkills(root);
|
|
124
|
+
|
|
125
|
+
stageSiblingSkills({
|
|
126
|
+
skillUnderTest: "gamma",
|
|
127
|
+
skillsSourceDir: src,
|
|
128
|
+
repoRoot: root,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const skillsDir = join(root, ".claude", "skills");
|
|
132
|
+
expect(readFileSync(join(skillsDir, "alpha", "SKILL.md"), "utf8")).toBe(
|
|
133
|
+
"alpha content",
|
|
134
|
+
);
|
|
135
|
+
expect(readFileSync(join(skillsDir, "alpha", "helper.md"), "utf8")).toBe(
|
|
136
|
+
"alpha helper",
|
|
137
|
+
);
|
|
138
|
+
expect(existsSync(join(skillsDir, "alpha", "evals"))).toBe(false);
|
|
139
|
+
expect(readFileSync(join(skillsDir, "beta", "SKILL.md"), "utf8")).toBe(
|
|
140
|
+
"beta content",
|
|
141
|
+
);
|
|
142
|
+
expect(existsSync(join(skillsDir, "gamma"))).toBe(false);
|
|
143
|
+
|
|
144
|
+
const manifestPath = join(skillsDir, STAGED_SIBLING_MANIFEST);
|
|
145
|
+
expect(existsSync(manifestPath)).toBe(true);
|
|
146
|
+
const written = JSON.parse(readFileSync(manifestPath, "utf8")) as {
|
|
147
|
+
created_entries: Array<{ name: string; preexisting: boolean }>;
|
|
148
|
+
};
|
|
149
|
+
expect(written.created_entries.map((e) => e.name).sort()).toEqual([
|
|
150
|
+
"alpha",
|
|
151
|
+
"beta",
|
|
152
|
+
]);
|
|
153
|
+
for (const e of written.created_entries) {
|
|
154
|
+
expect(e.preexisting).toBe(false);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("backs up colliding pre-existing entries and records them in the manifest", () => {
|
|
159
|
+
const root = join(FIXTURE_ROOT, "sibling-collide");
|
|
160
|
+
mkdirSync(root, { recursive: true });
|
|
161
|
+
const src = buildSourceSkills(root);
|
|
162
|
+
|
|
163
|
+
const skillsDir = join(root, ".claude", "skills");
|
|
164
|
+
mkdirSync(join(skillsDir, "alpha"), { recursive: true });
|
|
165
|
+
writeFileSync(join(skillsDir, "alpha", "SKILL.md"), "USER OWNED");
|
|
166
|
+
|
|
167
|
+
stageSiblingSkills({
|
|
168
|
+
skillUnderTest: "gamma",
|
|
169
|
+
skillsSourceDir: src,
|
|
170
|
+
repoRoot: root,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(readFileSync(join(skillsDir, "alpha", "SKILL.md"), "utf8")).toBe(
|
|
174
|
+
"alpha content",
|
|
175
|
+
);
|
|
176
|
+
const manifest = JSON.parse(
|
|
177
|
+
readFileSync(join(skillsDir, STAGED_SIBLING_MANIFEST), "utf8"),
|
|
178
|
+
) as {
|
|
179
|
+
created_entries: Array<{
|
|
180
|
+
name: string;
|
|
181
|
+
preexisting: boolean;
|
|
182
|
+
backup_path?: string;
|
|
183
|
+
}>;
|
|
184
|
+
};
|
|
185
|
+
const alphaEntry = manifest.created_entries.find((e) => e.name === "alpha");
|
|
186
|
+
expect(alphaEntry).toBeDefined();
|
|
187
|
+
expect(alphaEntry?.preexisting).toBe(true);
|
|
188
|
+
expect(alphaEntry?.backup_path).toBeDefined();
|
|
189
|
+
const backupPath = alphaEntry?.backup_path as string;
|
|
190
|
+
expect(existsSync(backupPath)).toBe(true);
|
|
191
|
+
expect(readFileSync(join(backupPath, "SKILL.md"), "utf8")).toBe(
|
|
192
|
+
"USER OWNED",
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("skips the skill-under-test even if it appears in the source skills dir", () => {
|
|
197
|
+
const root = join(FIXTURE_ROOT, "sibling-skip-under-test");
|
|
198
|
+
mkdirSync(root, { recursive: true });
|
|
199
|
+
const src = buildSourceSkills(root);
|
|
200
|
+
|
|
201
|
+
stageSiblingSkills({
|
|
202
|
+
skillUnderTest: "alpha",
|
|
203
|
+
skillsSourceDir: src,
|
|
204
|
+
repoRoot: root,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const skillsDir = join(root, ".claude", "skills");
|
|
208
|
+
expect(existsSync(join(skillsDir, "alpha"))).toBe(false);
|
|
209
|
+
expect(existsSync(join(skillsDir, "beta"))).toBe(true);
|
|
210
|
+
expect(existsSync(join(skillsDir, "gamma"))).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("cleanupStagedSkills (manifest-aware)", () => {
|
|
215
|
+
test("removes manifest-listed sibling entries and restores backed-up pre-existing content", () => {
|
|
216
|
+
const root = join(FIXTURE_ROOT, "cleanup-restore");
|
|
217
|
+
mkdirSync(root, { recursive: true });
|
|
218
|
+
const src = join(root, "src-skills");
|
|
219
|
+
mkdirSync(join(src, "alpha"), { recursive: true });
|
|
220
|
+
writeFileSync(join(src, "alpha", "SKILL.md"), "new alpha");
|
|
221
|
+
mkdirSync(join(src, "beta"), { recursive: true });
|
|
222
|
+
writeFileSync(join(src, "beta", "SKILL.md"), "new beta");
|
|
223
|
+
|
|
224
|
+
const skillsDir = join(root, ".claude", "skills");
|
|
225
|
+
mkdirSync(join(skillsDir, "alpha"), { recursive: true });
|
|
226
|
+
writeFileSync(join(skillsDir, "alpha", "SKILL.md"), "USER ALPHA");
|
|
227
|
+
|
|
228
|
+
stageSiblingSkills({
|
|
229
|
+
skillUnderTest: "x",
|
|
230
|
+
skillsSourceDir: src,
|
|
231
|
+
repoRoot: root,
|
|
232
|
+
});
|
|
233
|
+
expect(readFileSync(join(skillsDir, "alpha", "SKILL.md"), "utf8")).toBe(
|
|
234
|
+
"new alpha",
|
|
235
|
+
);
|
|
236
|
+
expect(readFileSync(join(skillsDir, "beta", "SKILL.md"), "utf8")).toBe(
|
|
237
|
+
"new beta",
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
cleanupStagedSkills(root);
|
|
241
|
+
|
|
242
|
+
expect(readFileSync(join(skillsDir, "alpha", "SKILL.md"), "utf8")).toBe(
|
|
243
|
+
"USER ALPHA",
|
|
244
|
+
);
|
|
245
|
+
expect(existsSync(join(skillsDir, "beta"))).toBe(false);
|
|
246
|
+
expect(existsSync(join(skillsDir, STAGED_SIBLING_MANIFEST))).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("still sweeps prefix-staged entries when no manifest is present", () => {
|
|
250
|
+
const root = join(FIXTURE_ROOT, "cleanup-legacy");
|
|
251
|
+
const skillsDir = join(root, ".claude", "skills");
|
|
252
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
253
|
+
mkdirSync(join(skillsDir, `${STAGED_SKILL_PREFIX}1-with_skill__foo`), {
|
|
254
|
+
recursive: true,
|
|
255
|
+
});
|
|
256
|
+
mkdirSync(join(skillsDir, "user-custom"), { recursive: true });
|
|
257
|
+
|
|
258
|
+
cleanupStagedSkills(root);
|
|
259
|
+
|
|
260
|
+
expect(
|
|
261
|
+
existsSync(join(skillsDir, `${STAGED_SKILL_PREFIX}1-with_skill__foo`)),
|
|
262
|
+
).toBe(false);
|
|
263
|
+
expect(existsSync(join(skillsDir, "user-custom"))).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("buildDispatchTask bootstrap injection", () => {
|
|
268
|
+
const baseOpts = {
|
|
269
|
+
evalId: "e1",
|
|
270
|
+
condition: "with_skill",
|
|
271
|
+
skillPath: null,
|
|
272
|
+
stagedSkillSlug: "slow-powers-eval-1-with_skill__foo" as string | null,
|
|
273
|
+
userPrompt: "do the thing",
|
|
274
|
+
fixtures: [] as string[],
|
|
275
|
+
outputsDir: "/tmp/out",
|
|
276
|
+
condDir: "/tmp/cond",
|
|
277
|
+
skillName: "foo",
|
|
278
|
+
availableSkills: [] as {
|
|
279
|
+
name: string;
|
|
280
|
+
path: string;
|
|
281
|
+
description: string;
|
|
282
|
+
}[],
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
test("prepends <session-start-context> for claude-code when bootstrapContent is provided", () => {
|
|
286
|
+
const task = buildDispatchTask({
|
|
287
|
+
...baseOpts,
|
|
288
|
+
bootstrapContent: "BOOT-LOADED",
|
|
289
|
+
});
|
|
290
|
+
expect(task.dispatch_prompt.startsWith("<session-start-context>")).toBe(
|
|
291
|
+
true,
|
|
292
|
+
);
|
|
293
|
+
expect(task.dispatch_prompt).toContain("BOOT-LOADED");
|
|
294
|
+
expect(task.dispatch_prompt).toContain("</session-start-context>");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("omits <session-start-context> when bootstrapContent is null and nothing is staged", () => {
|
|
298
|
+
const task = buildDispatchTask({
|
|
299
|
+
...baseOpts,
|
|
300
|
+
bootstrapContent: null,
|
|
301
|
+
});
|
|
302
|
+
expect(task.dispatch_prompt).not.toContain("<session-start-context>");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("emits <session-start-context> with a staged-skills inventory even when bootstrapContent is null", () => {
|
|
306
|
+
const task = buildDispatchTask({
|
|
307
|
+
...baseOpts,
|
|
308
|
+
bootstrapContent: null,
|
|
309
|
+
availableSkills: [
|
|
310
|
+
{ name: "foo", path: "/x/foo/SKILL.md", description: "the foo skill" },
|
|
311
|
+
],
|
|
312
|
+
});
|
|
313
|
+
expect(task.dispatch_prompt).toContain("<session-start-context>");
|
|
314
|
+
expect(task.dispatch_prompt).toContain("staged and discoverable");
|
|
315
|
+
expect(task.dispatch_prompt).toContain("* `foo`");
|
|
316
|
+
expect(task.dispatch_prompt).toContain("*Trigger:* the foo skill");
|
|
317
|
+
// No product framing should appear without a bootstrap file.
|
|
318
|
+
expect(task.dispatch_prompt).not.toContain("loaded at session start");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("staged-skills inventory follows the verbatim bootstrap content when both are present", () => {
|
|
322
|
+
const task = buildDispatchTask({
|
|
323
|
+
...baseOpts,
|
|
324
|
+
bootstrapContent: "BOOT-LOADED",
|
|
325
|
+
availableSkills: [
|
|
326
|
+
{ name: "foo", path: "/x/foo/SKILL.md", description: "the foo skill" },
|
|
327
|
+
],
|
|
328
|
+
});
|
|
329
|
+
const bootIdx = task.dispatch_prompt.indexOf("BOOT-LOADED");
|
|
330
|
+
const invIdx = task.dispatch_prompt.indexOf("staged and discoverable");
|
|
331
|
+
expect(bootIdx).toBeGreaterThan(-1);
|
|
332
|
+
expect(invIdx).toBeGreaterThan(bootIdx);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("sets dispatch_prompt_path to dispatch-prompt.txt under the condition dir", () => {
|
|
336
|
+
const task = buildDispatchTask({
|
|
337
|
+
...baseOpts,
|
|
338
|
+
bootstrapContent: null,
|
|
339
|
+
});
|
|
340
|
+
expect(task.dispatch_prompt_path).toBe("/tmp/cond/dispatch-prompt.txt");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const SAMPLE_DIRECTORY = [
|
|
344
|
+
"## Active Skills Directory",
|
|
345
|
+
"",
|
|
346
|
+
"* **`test-driven-development`**",
|
|
347
|
+
" * *Trigger:* Use whenever implementing code.",
|
|
348
|
+
"* **`systematic-debugging`**",
|
|
349
|
+
" * *Trigger:* Use when debugging.",
|
|
350
|
+
].join("\n");
|
|
351
|
+
|
|
352
|
+
test("redactSkillFromBootstrap removes the skill-under-test's directory entry", () => {
|
|
353
|
+
const redacted = redactSkillFromBootstrap(
|
|
354
|
+
SAMPLE_DIRECTORY,
|
|
355
|
+
"test-driven-development",
|
|
356
|
+
);
|
|
357
|
+
expect(redacted).not.toContain("test-driven-development");
|
|
358
|
+
expect(redacted).not.toContain("Use whenever implementing code.");
|
|
359
|
+
// Sibling entries and the heading survive.
|
|
360
|
+
expect(redacted).toContain("systematic-debugging");
|
|
361
|
+
expect(redacted).toContain("Use when debugging.");
|
|
362
|
+
expect(redacted).toContain("## Active Skills Directory");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("redacts the skill-under-test from bootstrap in the skill-absent condition", () => {
|
|
366
|
+
const withoutSkill = buildDispatchTask({
|
|
367
|
+
...baseOpts,
|
|
368
|
+
condition: "without_skill",
|
|
369
|
+
skillPath: null,
|
|
370
|
+
stagedSkillSlug: null,
|
|
371
|
+
skillName: "test-driven-development",
|
|
372
|
+
bootstrapContent: SAMPLE_DIRECTORY,
|
|
373
|
+
});
|
|
374
|
+
expect(withoutSkill.dispatch_prompt).not.toContain(
|
|
375
|
+
"test-driven-development",
|
|
376
|
+
);
|
|
377
|
+
// A sibling skill named in the same bootstrap is untouched.
|
|
378
|
+
expect(withoutSkill.dispatch_prompt).toContain("systematic-debugging");
|
|
379
|
+
|
|
380
|
+
const withSkill = buildDispatchTask({
|
|
381
|
+
...baseOpts,
|
|
382
|
+
condition: "with_skill",
|
|
383
|
+
skillPath: null,
|
|
384
|
+
stagedSkillSlug: "slow-powers-eval-1-with_skill__test-driven-development",
|
|
385
|
+
skillName: "test-driven-development",
|
|
386
|
+
bootstrapContent: SAMPLE_DIRECTORY,
|
|
387
|
+
});
|
|
388
|
+
expect(withSkill.dispatch_prompt).toContain("test-driven-development");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("references staged slug in skill block for claude-code", () => {
|
|
392
|
+
const task = buildDispatchTask({
|
|
393
|
+
...baseOpts,
|
|
394
|
+
bootstrapContent: "BOOT-LOADED",
|
|
395
|
+
});
|
|
396
|
+
expect(task.dispatch_prompt).toContain(
|
|
397
|
+
"slow-powers-eval-1-with_skill__foo",
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("without-skill condition under realistic env reflects 'this skill removed, others available' rather than 'no skill loaded'", () => {
|
|
402
|
+
const task = buildDispatchTask({
|
|
403
|
+
...baseOpts,
|
|
404
|
+
skillPath: null,
|
|
405
|
+
stagedSkillSlug: null,
|
|
406
|
+
bootstrapContent: "BOOT-LOADED",
|
|
407
|
+
});
|
|
408
|
+
expect(task.dispatch_prompt).not.toContain("No skill is loaded");
|
|
409
|
+
expect(task.dispatch_prompt.toLowerCase()).toContain("not available");
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("without-skill condition without bootstrap (e.g. --no-stage) keeps the legacy 'No skill is loaded' wording", () => {
|
|
413
|
+
const task = buildDispatchTask({
|
|
414
|
+
...baseOpts,
|
|
415
|
+
skillPath: null,
|
|
416
|
+
stagedSkillSlug: null,
|
|
417
|
+
bootstrapContent: null,
|
|
418
|
+
});
|
|
419
|
+
expect(task.dispatch_prompt).toContain("No skill is loaded");
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe("run.ts user-mode end-to-end (--skill-dir, isolated CWD)", () => {
|
|
424
|
+
const RUN_TS = join(import.meta.dir, "run.ts");
|
|
425
|
+
|
|
426
|
+
function setup(name: string): { skillDir: string; cwd: string } {
|
|
427
|
+
const root = join(FIXTURE_ROOT, name);
|
|
428
|
+
const skillDir = join(root, "skill-dir");
|
|
429
|
+
const skillSub = join(skillDir, "mr-review");
|
|
430
|
+
mkdirSync(join(skillSub, "evals"), { recursive: true });
|
|
431
|
+
writeFileSync(
|
|
432
|
+
join(skillSub, "SKILL.md"),
|
|
433
|
+
"---\nname: mr-review\ndescription: review merge requests\n---\n\nbody\n",
|
|
434
|
+
);
|
|
435
|
+
writeFileSync(
|
|
436
|
+
join(skillSub, "evals", "evals.json"),
|
|
437
|
+
JSON.stringify({
|
|
438
|
+
skill_name: "mr-review",
|
|
439
|
+
evals: [
|
|
440
|
+
{ id: "e1", prompt: "review this MR", expected_output: "a review" },
|
|
441
|
+
],
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
444
|
+
const cwd = join(root, "work");
|
|
445
|
+
mkdirSync(cwd, { recursive: true });
|
|
446
|
+
return { skillDir, cwd };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function runCli(args: string[], cwd: string) {
|
|
450
|
+
return Bun.spawnSync(["bun", "run", RUN_TS, ...args], {
|
|
451
|
+
cwd,
|
|
452
|
+
stdout: "pipe",
|
|
453
|
+
stderr: "pipe",
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
test("stages only the skill-under-test and writes workspace under CWD", () => {
|
|
458
|
+
const { skillDir, cwd } = setup("usermode-basic");
|
|
459
|
+
const res = runCli(
|
|
460
|
+
[
|
|
461
|
+
"--skill-dir",
|
|
462
|
+
skillDir,
|
|
463
|
+
"--skill",
|
|
464
|
+
"mr-review",
|
|
465
|
+
"--mode",
|
|
466
|
+
"new-skill",
|
|
467
|
+
"--dry-run",
|
|
468
|
+
],
|
|
469
|
+
cwd,
|
|
470
|
+
);
|
|
471
|
+
expect(res.exitCode).toBe(0);
|
|
472
|
+
|
|
473
|
+
const dispatchJson = join(
|
|
474
|
+
cwd,
|
|
475
|
+
"skills-workspace",
|
|
476
|
+
"mr-review",
|
|
477
|
+
"iteration-1",
|
|
478
|
+
"dispatch.json",
|
|
479
|
+
);
|
|
480
|
+
expect(existsSync(dispatchJson)).toBe(true);
|
|
481
|
+
|
|
482
|
+
const stagedSkillsDir = join(cwd, ".claude", "skills");
|
|
483
|
+
const entries = readdirSync(stagedSkillsDir).filter(
|
|
484
|
+
(e) => e !== STAGED_SIBLING_MANIFEST,
|
|
485
|
+
);
|
|
486
|
+
expect(entries).toEqual(["slow-powers-eval-1-with_skill__mr-review"]);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("dispatch prompt lists only the skill-under-test, no other skills, and no product framing without --bootstrap", () => {
|
|
490
|
+
const { skillDir, cwd } = setup("usermode-prompt");
|
|
491
|
+
const res = runCli(
|
|
492
|
+
[
|
|
493
|
+
"--skill-dir",
|
|
494
|
+
skillDir,
|
|
495
|
+
"--skill",
|
|
496
|
+
"mr-review",
|
|
497
|
+
"--mode",
|
|
498
|
+
"new-skill",
|
|
499
|
+
"--dry-run",
|
|
500
|
+
],
|
|
501
|
+
cwd,
|
|
502
|
+
);
|
|
503
|
+
expect(res.exitCode).toBe(0);
|
|
504
|
+
|
|
505
|
+
const dispatch = JSON.parse(
|
|
506
|
+
readFileSync(
|
|
507
|
+
join(
|
|
508
|
+
cwd,
|
|
509
|
+
"skills-workspace",
|
|
510
|
+
"mr-review",
|
|
511
|
+
"iteration-1",
|
|
512
|
+
"dispatch.json",
|
|
513
|
+
),
|
|
514
|
+
"utf8",
|
|
515
|
+
),
|
|
516
|
+
) as {
|
|
517
|
+
tasks: Array<{
|
|
518
|
+
condition: string;
|
|
519
|
+
dispatch_prompt?: string;
|
|
520
|
+
dispatch_prompt_path: string;
|
|
521
|
+
}>;
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const withSkill = dispatch.tasks.find((t) => t.condition === "with_skill");
|
|
525
|
+
expect(withSkill).toBeDefined();
|
|
526
|
+
// The full prompt is no longer inlined in dispatch.json — it lives in a file.
|
|
527
|
+
expect(withSkill?.dispatch_prompt).toBeUndefined();
|
|
528
|
+
const prompt = readFileSync(withSkill?.dispatch_prompt_path ?? "", "utf8");
|
|
529
|
+
expect(prompt).toContain("<session-start-context>");
|
|
530
|
+
expect(prompt).toContain("* `mr-review`");
|
|
531
|
+
expect(prompt).not.toContain("test-driven-development");
|
|
532
|
+
expect(prompt).not.toContain("writing-skills");
|
|
533
|
+
// No product framing (EXTREMELY-IMPORTANT etc.) without a --bootstrap file.
|
|
534
|
+
expect(prompt).not.toContain("EXTREMELY-IMPORTANT");
|
|
535
|
+
expect(prompt).not.toContain("loaded at session start");
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test("writes each dispatch prompt to a file and drops the inline prompt from dispatch.json", () => {
|
|
539
|
+
const { skillDir, cwd } = setup("usermode-prompt-file");
|
|
540
|
+
const res = runCli(
|
|
541
|
+
[
|
|
542
|
+
"--skill-dir",
|
|
543
|
+
skillDir,
|
|
544
|
+
"--skill",
|
|
545
|
+
"mr-review",
|
|
546
|
+
"--mode",
|
|
547
|
+
"new-skill",
|
|
548
|
+
"--dry-run",
|
|
549
|
+
],
|
|
550
|
+
cwd,
|
|
551
|
+
);
|
|
552
|
+
expect(res.exitCode).toBe(0);
|
|
553
|
+
|
|
554
|
+
const dispatch = JSON.parse(
|
|
555
|
+
readFileSync(
|
|
556
|
+
join(
|
|
557
|
+
cwd,
|
|
558
|
+
"skills-workspace",
|
|
559
|
+
"mr-review",
|
|
560
|
+
"iteration-1",
|
|
561
|
+
"dispatch.json",
|
|
562
|
+
),
|
|
563
|
+
"utf8",
|
|
564
|
+
),
|
|
565
|
+
) as {
|
|
566
|
+
tasks: Array<{ dispatch_prompt?: string; dispatch_prompt_path: string }>;
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
expect(dispatch.tasks.length).toBeGreaterThan(0);
|
|
570
|
+
for (const t of dispatch.tasks) {
|
|
571
|
+
// Nothing inlined; everything goes through the file pointer.
|
|
572
|
+
expect(t.dispatch_prompt).toBeUndefined();
|
|
573
|
+
expect(t.dispatch_prompt_path.endsWith("dispatch-prompt.txt")).toBe(true);
|
|
574
|
+
expect(existsSync(t.dispatch_prompt_path)).toBe(true);
|
|
575
|
+
const contents = readFileSync(t.dispatch_prompt_path, "utf8");
|
|
576
|
+
expect(contents.length).toBeGreaterThan(0);
|
|
577
|
+
expect(contents).toContain("User request:");
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("--guard installs a PreToolUse hook; teardown-guard removes it", () => {
|
|
582
|
+
const { skillDir, cwd } = setup("usermode-guard");
|
|
583
|
+
const settingsPath = join(cwd, ".claude", "settings.local.json");
|
|
584
|
+
|
|
585
|
+
const res = runCli(
|
|
586
|
+
[
|
|
587
|
+
"--skill-dir",
|
|
588
|
+
skillDir,
|
|
589
|
+
"--skill",
|
|
590
|
+
"mr-review",
|
|
591
|
+
"--mode",
|
|
592
|
+
"new-skill",
|
|
593
|
+
"--guard",
|
|
594
|
+
],
|
|
595
|
+
cwd,
|
|
596
|
+
);
|
|
597
|
+
expect(res.exitCode).toBe(0);
|
|
598
|
+
expect(existsSync(settingsPath)).toBe(true);
|
|
599
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
600
|
+
expect(settings.hooks.PreToolUse[0].matcher).toContain("Write");
|
|
601
|
+
|
|
602
|
+
const down = runCli(
|
|
603
|
+
["teardown-guard", "--skill-dir", skillDir, "--skill", "mr-review"],
|
|
604
|
+
cwd,
|
|
605
|
+
);
|
|
606
|
+
expect(down.exitCode).toBe(0);
|
|
607
|
+
expect(existsSync(settingsPath)).toBe(false);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("a normal run does not install a guard", () => {
|
|
611
|
+
const { skillDir, cwd } = setup("usermode-noguard");
|
|
612
|
+
const res = runCli(
|
|
613
|
+
[
|
|
614
|
+
"--skill-dir",
|
|
615
|
+
skillDir,
|
|
616
|
+
"--skill",
|
|
617
|
+
"mr-review",
|
|
618
|
+
"--mode",
|
|
619
|
+
"new-skill",
|
|
620
|
+
"--dry-run",
|
|
621
|
+
],
|
|
622
|
+
cwd,
|
|
623
|
+
);
|
|
624
|
+
expect(res.exitCode).toBe(0);
|
|
625
|
+
expect(existsSync(join(cwd, ".claude", "settings.local.json"))).toBe(false);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("namespaces agent_description per iteration+run and records run_nonce", () => {
|
|
629
|
+
const { skillDir, cwd } = setup("usermode-nonce");
|
|
630
|
+
const res = runCli(
|
|
631
|
+
[
|
|
632
|
+
"--skill-dir",
|
|
633
|
+
skillDir,
|
|
634
|
+
"--skill",
|
|
635
|
+
"mr-review",
|
|
636
|
+
"--mode",
|
|
637
|
+
"new-skill",
|
|
638
|
+
"--dry-run",
|
|
639
|
+
],
|
|
640
|
+
cwd,
|
|
641
|
+
);
|
|
642
|
+
expect(res.exitCode).toBe(0);
|
|
643
|
+
|
|
644
|
+
const iterationDir = join(
|
|
645
|
+
cwd,
|
|
646
|
+
"skills-workspace",
|
|
647
|
+
"mr-review",
|
|
648
|
+
"iteration-1",
|
|
649
|
+
);
|
|
650
|
+
const dispatch = JSON.parse(
|
|
651
|
+
readFileSync(join(iterationDir, "dispatch.json"), "utf8"),
|
|
652
|
+
) as {
|
|
653
|
+
run_nonce: string;
|
|
654
|
+
tasks: Array<{ condition: string; agent_description: string }>;
|
|
655
|
+
};
|
|
656
|
+
expect(typeof dispatch.run_nonce).toBe("string");
|
|
657
|
+
expect(dispatch.run_nonce.length).toBeGreaterThan(0);
|
|
658
|
+
|
|
659
|
+
for (const t of dispatch.tasks) {
|
|
660
|
+
// <eval_id>:<condition>:i<iteration>-<nonce> — unique across iterations
|
|
661
|
+
// and re-runs so fill-transcripts can't cross-match a colliding agent.
|
|
662
|
+
expect(t.agent_description).toMatch(
|
|
663
|
+
new RegExp(`:${t.condition}:i1-${dispatch.run_nonce}$`),
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const conditions = JSON.parse(
|
|
668
|
+
readFileSync(join(iterationDir, "conditions.json"), "utf8"),
|
|
669
|
+
) as { run_nonce?: string };
|
|
670
|
+
expect(conditions.run_nonce).toBe(dispatch.run_nonce);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test("--bootstrap content is prepended verbatim before the staged-skills inventory", () => {
|
|
674
|
+
const { skillDir, cwd } = setup("usermode-bootstrap");
|
|
675
|
+
const bootstrapPath = join(cwd, "my-bootstrap.md");
|
|
676
|
+
writeFileSync(bootstrapPath, "MY CUSTOM EVAL FRAMING");
|
|
677
|
+
const res = runCli(
|
|
678
|
+
[
|
|
679
|
+
"--skill-dir",
|
|
680
|
+
skillDir,
|
|
681
|
+
"--skill",
|
|
682
|
+
"mr-review",
|
|
683
|
+
"--mode",
|
|
684
|
+
"new-skill",
|
|
685
|
+
"--bootstrap",
|
|
686
|
+
bootstrapPath,
|
|
687
|
+
"--dry-run",
|
|
688
|
+
],
|
|
689
|
+
cwd,
|
|
690
|
+
);
|
|
691
|
+
expect(res.exitCode).toBe(0);
|
|
692
|
+
|
|
693
|
+
const dispatch = JSON.parse(
|
|
694
|
+
readFileSync(
|
|
695
|
+
join(
|
|
696
|
+
cwd,
|
|
697
|
+
"skills-workspace",
|
|
698
|
+
"mr-review",
|
|
699
|
+
"iteration-1",
|
|
700
|
+
"dispatch.json",
|
|
701
|
+
),
|
|
702
|
+
"utf8",
|
|
703
|
+
),
|
|
704
|
+
) as {
|
|
705
|
+
tasks: Array<{ condition: string; dispatch_prompt_path: string }>;
|
|
706
|
+
};
|
|
707
|
+
const withSkill = dispatch.tasks.find((t) => t.condition === "with_skill");
|
|
708
|
+
const prompt = withSkill
|
|
709
|
+
? readFileSync(withSkill.dispatch_prompt_path, "utf8")
|
|
710
|
+
: "";
|
|
711
|
+
const bootIdx = prompt.indexOf("MY CUSTOM EVAL FRAMING");
|
|
712
|
+
const invIdx = prompt.indexOf("staged and discoverable");
|
|
713
|
+
expect(bootIdx).toBeGreaterThan(-1);
|
|
714
|
+
expect(invIdx).toBeGreaterThan(bootIdx);
|
|
715
|
+
});
|
|
716
|
+
});
|