@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.
- package/README.md +37 -65
- package/bootstrap.md +1 -7
- package/opencode/plugins/slow-powers.js +1 -1
- package/package.json +14 -13
- package/skills/evaluating-skills/SKILL.md +91 -337
- package/skills/evaluating-skills/evals/baseline/BASELINE.md +23 -0
- package/skills/evaluating-skills/evals/baseline/NOTES.md +40 -0
- package/skills/evaluating-skills/evals/baseline/benchmark.json +54 -0
- package/skills/evaluating-skills/evals/baseline/grading/deterministic-edit-skip__new_skill.json +39 -0
- package/skills/evaluating-skills/evals/baseline/grading/deterministic-edit-skip__old_skill.json +39 -0
- package/skills/evaluating-skills/evals/baseline/grading/did-my-revision-help__new_skill.json +39 -0
- package/skills/evaluating-skills/evals/baseline/grading/did-my-revision-help__old_skill.json +39 -0
- package/skills/evaluating-skills/evals/baseline/grading/is-new-skill-ready-to-ship__new_skill.json +32 -0
- package/skills/evaluating-skills/evals/baseline/grading/is-new-skill-ready-to-ship__old_skill.json +32 -0
- package/skills/test-driven-development/evals/baseline/NOTES.md +2 -2
- package/skills/verifying-development-work/SKILL.md +17 -6
- package/skills/verifying-development-work/code-review.md +68 -0
- package/skills/verifying-development-work/comment-review.md +85 -0
- package/skills/verifying-development-work/evals/baseline/BASELINE.md +7 -6
- package/skills/verifying-development-work/evals/baseline/NOTES.md +83 -149
- package/skills/verifying-development-work/evals/baseline/benchmark.json +32 -31
- package/skills/verifying-development-work/evals/baseline/grading/comment-hygiene-at-handoff__new_skill.json +53 -0
- package/skills/verifying-development-work/evals/baseline/grading/comment-hygiene-at-handoff__old_skill.json +53 -0
- package/skills/verifying-development-work/evals/baseline/grading/wrap-it-up-handoff__new_skill.json +53 -0
- package/skills/verifying-development-work/evals/baseline/grading/wrap-it-up-handoff__old_skill.json +53 -0
- package/skills/verifying-development-work/evals/evals.json +34 -2
- package/skills/verifying-development-work/evals/fixtures/comment-hygiene-at-handoff/slugify.test.ts +14 -0
- package/skills/verifying-development-work/evals/fixtures/comment-hygiene-at-handoff/slugify.ts +25 -0
- package/skills/evaluating-skills/examples/verifying-development-work-evals.json +0 -30
- package/skills/evaluating-skills/harness-details/claude.md +0 -158
- package/skills/evaluating-skills/runner/README.md +0 -154
- package/skills/evaluating-skills/runner/adapters/claude-code-session.test.ts +0 -56
- package/skills/evaluating-skills/runner/adapters/claude-code-session.ts +0 -43
- package/skills/evaluating-skills/runner/adapters/claude-code-transcript.test.ts +0 -263
- package/skills/evaluating-skills/runner/adapters/claude-code-transcript.ts +0 -146
- package/skills/evaluating-skills/runner/aggregate.test.ts +0 -264
- package/skills/evaluating-skills/runner/aggregate.ts +0 -248
- package/skills/evaluating-skills/runner/context.test.ts +0 -181
- package/skills/evaluating-skills/runner/context.ts +0 -90
- package/skills/evaluating-skills/runner/detect-stray-writes.test.ts +0 -103
- package/skills/evaluating-skills/runner/detect-stray-writes.ts +0 -192
- package/skills/evaluating-skills/runner/fill-transcripts.test.ts +0 -73
- package/skills/evaluating-skills/runner/fill-transcripts.ts +0 -154
- package/skills/evaluating-skills/runner/grade.test.ts +0 -347
- package/skills/evaluating-skills/runner/grade.ts +0 -603
- package/skills/evaluating-skills/runner/guard/guard.ts +0 -49
- package/skills/evaluating-skills/runner/guard/install.test.ts +0 -92
- package/skills/evaluating-skills/runner/guard/install.ts +0 -147
- package/skills/evaluating-skills/runner/guard/policy.test.ts +0 -71
- package/skills/evaluating-skills/runner/guard/policy.ts +0 -74
- package/skills/evaluating-skills/runner/plugin-shadow.test.ts +0 -228
- package/skills/evaluating-skills/runner/plugin-shadow.ts +0 -201
- package/skills/evaluating-skills/runner/profiles/claude-code/plan-mode.md +0 -11
- package/skills/evaluating-skills/runner/promote-baseline.test.ts +0 -230
- package/skills/evaluating-skills/runner/promote-baseline.ts +0 -186
- package/skills/evaluating-skills/runner/run.test.ts +0 -1180
- package/skills/evaluating-skills/runner/run.ts +0 -1029
- package/skills/evaluating-skills/runner/sandbox-policy.ts +0 -74
- package/skills/evaluating-skills/runner/types.ts +0 -112
- package/skills/evaluating-skills/runner/validate-all.ts +0 -54
- package/skills/evaluating-skills/runner/validate-schema.test.ts +0 -99
- package/skills/evaluating-skills/runner/validate-schema.ts +0 -51
- package/skills/evaluating-skills/runner/validate.test.ts +0 -56
- package/skills/evaluating-skills/runner/validate.ts +0 -21
- package/skills/evaluating-skills/schema/evals.schema.json +0 -105
- package/skills/evaluating-skills/schema/grading.schema.json +0 -84
- package/skills/evaluating-skills/schema/run-record.schema.json +0 -80
- package/skills/evaluating-skills/schema/stray-writes.schema.json +0 -68
- package/skills/evaluating-skills/templates/eval-task-prompt.md +0 -67
- package/skills/evaluating-skills/templates/evals.json.example +0 -17
- package/skills/evaluating-skills/templates/judge-prompt.md +0 -56
- package/skills/evaluating-skills/templates/revise-skill-prompt.md +0 -56
- package/skills/verifying-development-work/evals/baseline/grading/bug-fixed-without-reproducing__with_skill.json +0 -39
- package/skills/verifying-development-work/evals/baseline/grading/bug-fixed-without-reproducing__without_skill.json +0 -24
- package/skills/verifying-development-work/evals/baseline/grading/build-implied-by-edit__with_skill.json +0 -46
- package/skills/verifying-development-work/evals/baseline/grading/build-implied-by-edit__without_skill.json +0 -31
- package/skills/verifying-development-work/evals/baseline/grading/claim-without-running__with_skill.json +0 -46
- package/skills/verifying-development-work/evals/baseline/grading/claim-without-running__without_skill.json +0 -31
- package/skills/verifying-development-work/evals/baseline/grading/seeded-done-tests-pass-ship-it__with_skill.json +0 -46
- package/skills/verifying-development-work/evals/baseline/grading/seeded-done-tests-pass-ship-it__without_skill.json +0 -31
- package/skills/verifying-development-work/evals/baseline/grading/wrap-it-up-handoff__with_skill.json +0 -53
- package/skills/verifying-development-work/evals/baseline/grading/wrap-it-up-handoff__without_skill.json +0 -38
|
@@ -1,1180 +0,0 @@
|
|
|
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
|
-
registerStagedSkillForCleanup,
|
|
17
|
-
STAGED_SIBLING_MANIFEST,
|
|
18
|
-
STAGED_SKILL_PREFIX,
|
|
19
|
-
selectEvals,
|
|
20
|
-
stageSiblingSkills,
|
|
21
|
-
stageSkillForCC,
|
|
22
|
-
} from "./run";
|
|
23
|
-
import type { Eval } from "./types";
|
|
24
|
-
|
|
25
|
-
const FIXTURE_ROOT = join(tmpdir(), `slow-powers-run-test-${process.pid}`);
|
|
26
|
-
|
|
27
|
-
beforeAll(() => {
|
|
28
|
-
mkdirSync(FIXTURE_ROOT, { recursive: true });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
afterAll(() => {
|
|
32
|
-
rmSync(FIXTURE_ROOT, { recursive: true, force: true });
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe("selectEvals", () => {
|
|
36
|
-
const mkEvals = (...ids: string[]): Eval[] =>
|
|
37
|
-
ids.map((id) => ({ id, prompt: `p-${id}`, expected_output: `o-${id}` }));
|
|
38
|
-
|
|
39
|
-
test("returns the full list unchanged when neither flag is set", () => {
|
|
40
|
-
const evals = mkEvals("a", "b", "c");
|
|
41
|
-
expect(selectEvals(evals, {})).toEqual(evals);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("--only keeps just the named ids, preserving config order", () => {
|
|
45
|
-
const evals = mkEvals("a", "b", "c");
|
|
46
|
-
const got = selectEvals(evals, { only: ["c", "a"] });
|
|
47
|
-
expect(got.map((e) => e.id)).toEqual(["a", "c"]);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("--skip drops the named ids", () => {
|
|
51
|
-
const evals = mkEvals("a", "b", "c");
|
|
52
|
-
const got = selectEvals(evals, { skip: ["b"] });
|
|
53
|
-
expect(got.map((e) => e.id)).toEqual(["a", "c"]);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("throws on an unknown id, listing the unknown and the available ids", () => {
|
|
57
|
-
const evals = mkEvals("a", "b");
|
|
58
|
-
expect(() => selectEvals(evals, { only: ["a", "nope"] })).toThrow(
|
|
59
|
-
/unknown eval id\(s\): nope\. Available ids: a, b/,
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("throws when both --only and --skip are given", () => {
|
|
64
|
-
const evals = mkEvals("a", "b");
|
|
65
|
-
expect(() => selectEvals(evals, { only: ["a"], skip: ["b"] })).toThrow(
|
|
66
|
-
/only one of --only \/ --skip/,
|
|
67
|
-
);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("throws when a flag resolves to an empty id list", () => {
|
|
71
|
-
const evals = mkEvals("a", "b");
|
|
72
|
-
expect(() => selectEvals(evals, { only: [] })).toThrow(
|
|
73
|
-
/at least one eval id/,
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe("stageSkillForCC", () => {
|
|
79
|
-
test("writes SKILL.md to <repoRoot>/.claude/skills/<slug>/SKILL.md and returns the slug", () => {
|
|
80
|
-
const repoRoot = join(FIXTURE_ROOT, "stage-basic");
|
|
81
|
-
mkdirSync(repoRoot, { recursive: true });
|
|
82
|
-
const content =
|
|
83
|
-
"---\nname: example\ndescription: example skill\n---\n\nbody\n";
|
|
84
|
-
|
|
85
|
-
const slug = stageSkillForCC({
|
|
86
|
-
content,
|
|
87
|
-
iteration: 3,
|
|
88
|
-
condition: "with_skill",
|
|
89
|
-
skillName: "verification-before-completion",
|
|
90
|
-
repoRoot,
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
expect(slug).toBe(
|
|
94
|
-
`${STAGED_SKILL_PREFIX}3-with_skill__verification-before-completion`,
|
|
95
|
-
);
|
|
96
|
-
const stagedPath = join(repoRoot, ".claude", "skills", slug, "SKILL.md");
|
|
97
|
-
expect(existsSync(stagedPath)).toBe(true);
|
|
98
|
-
expect(readFileSync(stagedPath, "utf8")).toBe(content);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("overwrites an existing staged skill at the same slug", () => {
|
|
102
|
-
const repoRoot = join(FIXTURE_ROOT, "stage-overwrite");
|
|
103
|
-
mkdirSync(repoRoot, { recursive: true });
|
|
104
|
-
|
|
105
|
-
stageSkillForCC({
|
|
106
|
-
content: "first",
|
|
107
|
-
iteration: 1,
|
|
108
|
-
condition: "with_skill",
|
|
109
|
-
skillName: "s",
|
|
110
|
-
repoRoot,
|
|
111
|
-
});
|
|
112
|
-
const slug = stageSkillForCC({
|
|
113
|
-
content: "second",
|
|
114
|
-
iteration: 1,
|
|
115
|
-
condition: "with_skill",
|
|
116
|
-
skillName: "s",
|
|
117
|
-
repoRoot,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const stagedPath = join(repoRoot, ".claude", "skills", slug, "SKILL.md");
|
|
121
|
-
expect(readFileSync(stagedPath, "utf8")).toBe("second");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("stageNameOverride stages under the verbatim name instead of the eval slug", () => {
|
|
125
|
-
const repoRoot = join(FIXTURE_ROOT, "stage-override");
|
|
126
|
-
mkdirSync(repoRoot, { recursive: true });
|
|
127
|
-
const content =
|
|
128
|
-
"---\nname: example\ndescription: example skill\n---\n\nbody\n";
|
|
129
|
-
|
|
130
|
-
const slug = stageSkillForCC({
|
|
131
|
-
content,
|
|
132
|
-
iteration: 2,
|
|
133
|
-
condition: "with_skill",
|
|
134
|
-
skillName: "verification-before-completion",
|
|
135
|
-
repoRoot,
|
|
136
|
-
stageNameOverride: "verification-before-completion",
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
expect(slug).toBe("verification-before-completion");
|
|
140
|
-
const stagedPath = join(repoRoot, ".claude", "skills", slug, "SKILL.md");
|
|
141
|
-
expect(existsSync(stagedPath)).toBe(true);
|
|
142
|
-
expect(readFileSync(stagedPath, "utf8")).toBe(content);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe("registerStagedSkillForCleanup", () => {
|
|
147
|
-
test("appends the custom dir to the manifest so cleanup removes it", () => {
|
|
148
|
-
const root = join(FIXTURE_ROOT, "register-cleanup");
|
|
149
|
-
const skillsDir = join(root, ".claude", "skills");
|
|
150
|
-
mkdirSync(skillsDir, { recursive: true });
|
|
151
|
-
// A sibling manifest already exists (written by stageSiblingSkills).
|
|
152
|
-
writeFileSync(
|
|
153
|
-
join(skillsDir, STAGED_SIBLING_MANIFEST),
|
|
154
|
-
`${JSON.stringify(
|
|
155
|
-
{
|
|
156
|
-
created_at: "x",
|
|
157
|
-
staged_under_test: "verification-before-completion",
|
|
158
|
-
created_entries: [{ name: "sibling-a", preexisting: false }],
|
|
159
|
-
},
|
|
160
|
-
null,
|
|
161
|
-
2,
|
|
162
|
-
)}\n`,
|
|
163
|
-
);
|
|
164
|
-
const customDir = join(skillsDir, "verification-before-completion");
|
|
165
|
-
mkdirSync(customDir, { recursive: true });
|
|
166
|
-
writeFileSync(join(customDir, "SKILL.md"), "staged");
|
|
167
|
-
|
|
168
|
-
registerStagedSkillForCleanup(root, "verification-before-completion");
|
|
169
|
-
|
|
170
|
-
const manifest = JSON.parse(
|
|
171
|
-
readFileSync(join(skillsDir, STAGED_SIBLING_MANIFEST), "utf8"),
|
|
172
|
-
) as { created_entries: Array<{ name: string }> };
|
|
173
|
-
expect(manifest.created_entries.map((e) => e.name).sort()).toEqual([
|
|
174
|
-
"sibling-a",
|
|
175
|
-
"verification-before-completion",
|
|
176
|
-
]);
|
|
177
|
-
|
|
178
|
-
cleanupStagedSkills(root);
|
|
179
|
-
expect(existsSync(customDir)).toBe(false);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test("is idempotent — registering the same name twice does not duplicate it", () => {
|
|
183
|
-
const root = join(FIXTURE_ROOT, "register-idempotent");
|
|
184
|
-
const skillsDir = join(root, ".claude", "skills");
|
|
185
|
-
mkdirSync(skillsDir, { recursive: true });
|
|
186
|
-
writeFileSync(
|
|
187
|
-
join(skillsDir, STAGED_SIBLING_MANIFEST),
|
|
188
|
-
`${JSON.stringify(
|
|
189
|
-
{
|
|
190
|
-
created_at: "x",
|
|
191
|
-
staged_under_test: "foo",
|
|
192
|
-
created_entries: [],
|
|
193
|
-
},
|
|
194
|
-
null,
|
|
195
|
-
2,
|
|
196
|
-
)}\n`,
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
registerStagedSkillForCleanup(root, "foo-staged");
|
|
200
|
-
registerStagedSkillForCleanup(root, "foo-staged");
|
|
201
|
-
|
|
202
|
-
const manifest = JSON.parse(
|
|
203
|
-
readFileSync(join(skillsDir, STAGED_SIBLING_MANIFEST), "utf8"),
|
|
204
|
-
) as { created_entries: Array<{ name: string }> };
|
|
205
|
-
expect(
|
|
206
|
-
manifest.created_entries.filter((e) => e.name === "foo-staged").length,
|
|
207
|
-
).toBe(1);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
describe("cleanupStagedSkills", () => {
|
|
212
|
-
test("removes only directories with the staged-skill prefix under .claude/skills", () => {
|
|
213
|
-
const repoRoot = join(FIXTURE_ROOT, "cleanup");
|
|
214
|
-
const skillsDir = join(repoRoot, ".claude", "skills");
|
|
215
|
-
mkdirSync(skillsDir, { recursive: true });
|
|
216
|
-
|
|
217
|
-
const stagedA = join(skillsDir, `${STAGED_SKILL_PREFIX}1-with_skill__foo`);
|
|
218
|
-
const stagedB = join(skillsDir, `${STAGED_SKILL_PREFIX}1-new_skill__bar`);
|
|
219
|
-
const productionLike = join(skillsDir, "user-custom-skill");
|
|
220
|
-
mkdirSync(stagedA, { recursive: true });
|
|
221
|
-
mkdirSync(stagedB, { recursive: true });
|
|
222
|
-
mkdirSync(productionLike, { recursive: true });
|
|
223
|
-
|
|
224
|
-
cleanupStagedSkills(repoRoot);
|
|
225
|
-
|
|
226
|
-
expect(existsSync(stagedA)).toBe(false);
|
|
227
|
-
expect(existsSync(stagedB)).toBe(false);
|
|
228
|
-
expect(existsSync(productionLike)).toBe(true);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test("is a no-op when .claude/skills does not exist", () => {
|
|
232
|
-
const repoRoot = join(FIXTURE_ROOT, "cleanup-empty");
|
|
233
|
-
mkdirSync(repoRoot, { recursive: true });
|
|
234
|
-
expect(() => cleanupStagedSkills(repoRoot)).not.toThrow();
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
describe("stageSiblingSkills", () => {
|
|
239
|
-
function buildSourceSkills(root: string): string {
|
|
240
|
-
const src = join(root, "src-skills");
|
|
241
|
-
mkdirSync(join(src, "alpha", "evals"), { recursive: true });
|
|
242
|
-
writeFileSync(join(src, "alpha", "SKILL.md"), "alpha content");
|
|
243
|
-
writeFileSync(join(src, "alpha", "helper.md"), "alpha helper");
|
|
244
|
-
writeFileSync(join(src, "alpha", "evals", "evals.json"), "{}");
|
|
245
|
-
mkdirSync(join(src, "beta"), { recursive: true });
|
|
246
|
-
writeFileSync(join(src, "beta", "SKILL.md"), "beta content");
|
|
247
|
-
mkdirSync(join(src, "gamma"), { recursive: true });
|
|
248
|
-
writeFileSync(join(src, "gamma", "SKILL.md"), "gamma content");
|
|
249
|
-
return src;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
test("stages each sibling at .claude/skills/<name>/ with full content minus evals/", () => {
|
|
253
|
-
const root = join(FIXTURE_ROOT, "sibling-basic");
|
|
254
|
-
mkdirSync(root, { recursive: true });
|
|
255
|
-
const src = buildSourceSkills(root);
|
|
256
|
-
|
|
257
|
-
stageSiblingSkills({
|
|
258
|
-
skillUnderTest: "gamma",
|
|
259
|
-
skillsSourceDir: src,
|
|
260
|
-
repoRoot: root,
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
const skillsDir = join(root, ".claude", "skills");
|
|
264
|
-
expect(readFileSync(join(skillsDir, "alpha", "SKILL.md"), "utf8")).toBe(
|
|
265
|
-
"alpha content",
|
|
266
|
-
);
|
|
267
|
-
expect(readFileSync(join(skillsDir, "alpha", "helper.md"), "utf8")).toBe(
|
|
268
|
-
"alpha helper",
|
|
269
|
-
);
|
|
270
|
-
expect(existsSync(join(skillsDir, "alpha", "evals"))).toBe(false);
|
|
271
|
-
expect(readFileSync(join(skillsDir, "beta", "SKILL.md"), "utf8")).toBe(
|
|
272
|
-
"beta content",
|
|
273
|
-
);
|
|
274
|
-
expect(existsSync(join(skillsDir, "gamma"))).toBe(false);
|
|
275
|
-
|
|
276
|
-
const manifestPath = join(skillsDir, STAGED_SIBLING_MANIFEST);
|
|
277
|
-
expect(existsSync(manifestPath)).toBe(true);
|
|
278
|
-
const written = JSON.parse(readFileSync(manifestPath, "utf8")) as {
|
|
279
|
-
created_entries: Array<{ name: string; preexisting: boolean }>;
|
|
280
|
-
};
|
|
281
|
-
expect(written.created_entries.map((e) => e.name).sort()).toEqual([
|
|
282
|
-
"alpha",
|
|
283
|
-
"beta",
|
|
284
|
-
]);
|
|
285
|
-
for (const e of written.created_entries) {
|
|
286
|
-
expect(e.preexisting).toBe(false);
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test("backs up colliding pre-existing entries and records them in the manifest", () => {
|
|
291
|
-
const root = join(FIXTURE_ROOT, "sibling-collide");
|
|
292
|
-
mkdirSync(root, { recursive: true });
|
|
293
|
-
const src = buildSourceSkills(root);
|
|
294
|
-
|
|
295
|
-
const skillsDir = join(root, ".claude", "skills");
|
|
296
|
-
mkdirSync(join(skillsDir, "alpha"), { recursive: true });
|
|
297
|
-
writeFileSync(join(skillsDir, "alpha", "SKILL.md"), "USER OWNED");
|
|
298
|
-
|
|
299
|
-
stageSiblingSkills({
|
|
300
|
-
skillUnderTest: "gamma",
|
|
301
|
-
skillsSourceDir: src,
|
|
302
|
-
repoRoot: root,
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
expect(readFileSync(join(skillsDir, "alpha", "SKILL.md"), "utf8")).toBe(
|
|
306
|
-
"alpha content",
|
|
307
|
-
);
|
|
308
|
-
const manifest = JSON.parse(
|
|
309
|
-
readFileSync(join(skillsDir, STAGED_SIBLING_MANIFEST), "utf8"),
|
|
310
|
-
) as {
|
|
311
|
-
created_entries: Array<{
|
|
312
|
-
name: string;
|
|
313
|
-
preexisting: boolean;
|
|
314
|
-
backup_path?: string;
|
|
315
|
-
}>;
|
|
316
|
-
};
|
|
317
|
-
const alphaEntry = manifest.created_entries.find((e) => e.name === "alpha");
|
|
318
|
-
expect(alphaEntry).toBeDefined();
|
|
319
|
-
expect(alphaEntry?.preexisting).toBe(true);
|
|
320
|
-
expect(alphaEntry?.backup_path).toBeDefined();
|
|
321
|
-
const backupPath = alphaEntry?.backup_path as string;
|
|
322
|
-
expect(existsSync(backupPath)).toBe(true);
|
|
323
|
-
expect(readFileSync(join(backupPath, "SKILL.md"), "utf8")).toBe(
|
|
324
|
-
"USER OWNED",
|
|
325
|
-
);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
test("skips the skill-under-test even if it appears in the source skills dir", () => {
|
|
329
|
-
const root = join(FIXTURE_ROOT, "sibling-skip-under-test");
|
|
330
|
-
mkdirSync(root, { recursive: true });
|
|
331
|
-
const src = buildSourceSkills(root);
|
|
332
|
-
|
|
333
|
-
stageSiblingSkills({
|
|
334
|
-
skillUnderTest: "alpha",
|
|
335
|
-
skillsSourceDir: src,
|
|
336
|
-
repoRoot: root,
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
const skillsDir = join(root, ".claude", "skills");
|
|
340
|
-
expect(existsSync(join(skillsDir, "alpha"))).toBe(false);
|
|
341
|
-
expect(existsSync(join(skillsDir, "beta"))).toBe(true);
|
|
342
|
-
expect(existsSync(join(skillsDir, "gamma"))).toBe(true);
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
describe("cleanupStagedSkills (manifest-aware)", () => {
|
|
347
|
-
test("removes manifest-listed sibling entries and restores backed-up pre-existing content", () => {
|
|
348
|
-
const root = join(FIXTURE_ROOT, "cleanup-restore");
|
|
349
|
-
mkdirSync(root, { recursive: true });
|
|
350
|
-
const src = join(root, "src-skills");
|
|
351
|
-
mkdirSync(join(src, "alpha"), { recursive: true });
|
|
352
|
-
writeFileSync(join(src, "alpha", "SKILL.md"), "new alpha");
|
|
353
|
-
mkdirSync(join(src, "beta"), { recursive: true });
|
|
354
|
-
writeFileSync(join(src, "beta", "SKILL.md"), "new beta");
|
|
355
|
-
|
|
356
|
-
const skillsDir = join(root, ".claude", "skills");
|
|
357
|
-
mkdirSync(join(skillsDir, "alpha"), { recursive: true });
|
|
358
|
-
writeFileSync(join(skillsDir, "alpha", "SKILL.md"), "USER ALPHA");
|
|
359
|
-
|
|
360
|
-
stageSiblingSkills({
|
|
361
|
-
skillUnderTest: "x",
|
|
362
|
-
skillsSourceDir: src,
|
|
363
|
-
repoRoot: root,
|
|
364
|
-
});
|
|
365
|
-
expect(readFileSync(join(skillsDir, "alpha", "SKILL.md"), "utf8")).toBe(
|
|
366
|
-
"new alpha",
|
|
367
|
-
);
|
|
368
|
-
expect(readFileSync(join(skillsDir, "beta", "SKILL.md"), "utf8")).toBe(
|
|
369
|
-
"new beta",
|
|
370
|
-
);
|
|
371
|
-
|
|
372
|
-
cleanupStagedSkills(root);
|
|
373
|
-
|
|
374
|
-
expect(readFileSync(join(skillsDir, "alpha", "SKILL.md"), "utf8")).toBe(
|
|
375
|
-
"USER ALPHA",
|
|
376
|
-
);
|
|
377
|
-
expect(existsSync(join(skillsDir, "beta"))).toBe(false);
|
|
378
|
-
expect(existsSync(join(skillsDir, STAGED_SIBLING_MANIFEST))).toBe(false);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test("still sweeps prefix-staged entries when no manifest is present", () => {
|
|
382
|
-
const root = join(FIXTURE_ROOT, "cleanup-legacy");
|
|
383
|
-
const skillsDir = join(root, ".claude", "skills");
|
|
384
|
-
mkdirSync(skillsDir, { recursive: true });
|
|
385
|
-
mkdirSync(join(skillsDir, `${STAGED_SKILL_PREFIX}1-with_skill__foo`), {
|
|
386
|
-
recursive: true,
|
|
387
|
-
});
|
|
388
|
-
mkdirSync(join(skillsDir, "user-custom"), { recursive: true });
|
|
389
|
-
|
|
390
|
-
cleanupStagedSkills(root);
|
|
391
|
-
|
|
392
|
-
expect(
|
|
393
|
-
existsSync(join(skillsDir, `${STAGED_SKILL_PREFIX}1-with_skill__foo`)),
|
|
394
|
-
).toBe(false);
|
|
395
|
-
expect(existsSync(join(skillsDir, "user-custom"))).toBe(true);
|
|
396
|
-
});
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
describe("buildDispatchTask bootstrap injection", () => {
|
|
400
|
-
const baseOpts = {
|
|
401
|
-
evalId: "e1",
|
|
402
|
-
condition: "with_skill",
|
|
403
|
-
skillPath: null,
|
|
404
|
-
stagedSkillSlug: "slow-powers-eval-1-with_skill__foo" as string | null,
|
|
405
|
-
userPrompt: "do the thing",
|
|
406
|
-
fixtures: [] as string[],
|
|
407
|
-
outputsDir: "/tmp/out",
|
|
408
|
-
condDir: "/tmp/cond",
|
|
409
|
-
skillName: "foo",
|
|
410
|
-
availableSkills: [] as {
|
|
411
|
-
name: string;
|
|
412
|
-
path: string;
|
|
413
|
-
description: string;
|
|
414
|
-
}[],
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
test("prepends <session-start-context> for claude-code when bootstrapContent is provided", () => {
|
|
418
|
-
const task = buildDispatchTask({
|
|
419
|
-
...baseOpts,
|
|
420
|
-
bootstrapContent: "BOOT-LOADED",
|
|
421
|
-
});
|
|
422
|
-
expect(task.dispatch_prompt.startsWith("<session-start-context>")).toBe(
|
|
423
|
-
true,
|
|
424
|
-
);
|
|
425
|
-
expect(task.dispatch_prompt).toContain("BOOT-LOADED");
|
|
426
|
-
expect(task.dispatch_prompt).toContain("</session-start-context>");
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
test("omits <session-start-context> when bootstrapContent is null and nothing is staged", () => {
|
|
430
|
-
const task = buildDispatchTask({
|
|
431
|
-
...baseOpts,
|
|
432
|
-
bootstrapContent: null,
|
|
433
|
-
});
|
|
434
|
-
expect(task.dispatch_prompt).not.toContain("<session-start-context>");
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
test("emits a harness-native available-skills block (no <session-start-context>) when bootstrapContent is null", () => {
|
|
438
|
-
const task = buildDispatchTask({
|
|
439
|
-
...baseOpts,
|
|
440
|
-
bootstrapContent: null,
|
|
441
|
-
availableSkills: [
|
|
442
|
-
{ name: "foo", path: "/x/foo/SKILL.md", description: "the foo skill" },
|
|
443
|
-
],
|
|
444
|
-
});
|
|
445
|
-
// Without a bootstrap, there is no SessionStart block — only the skills list.
|
|
446
|
-
expect(task.dispatch_prompt).not.toContain("<session-start-context>");
|
|
447
|
-
expect(task.dispatch_prompt).toContain(
|
|
448
|
-
"The following skills are available for use with the Skill tool:",
|
|
449
|
-
);
|
|
450
|
-
expect(task.dispatch_prompt).toContain("- foo: the foo skill");
|
|
451
|
-
// The eval-flavored wording and custom format are gone.
|
|
452
|
-
expect(task.dispatch_prompt).not.toContain("staged and discoverable");
|
|
453
|
-
expect(task.dispatch_prompt).not.toContain("*Trigger:*");
|
|
454
|
-
// No product framing should appear without a bootstrap file.
|
|
455
|
-
expect(task.dispatch_prompt).not.toContain("loaded at session start");
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
test("renders the available-skills block as its own section, outside <session-start-context>, after the verbatim bootstrap", () => {
|
|
459
|
-
const task = buildDispatchTask({
|
|
460
|
-
...baseOpts,
|
|
461
|
-
bootstrapContent: "BOOT-LOADED",
|
|
462
|
-
availableSkills: [
|
|
463
|
-
{ name: "foo", path: "/x/foo/SKILL.md", description: "the foo skill" },
|
|
464
|
-
],
|
|
465
|
-
});
|
|
466
|
-
const prompt = task.dispatch_prompt;
|
|
467
|
-
// The skills list is a separate block, not bundled inside the SessionStart
|
|
468
|
-
// context (which carries bootstrap content only).
|
|
469
|
-
const sscEnd = prompt.indexOf("</session-start-context>");
|
|
470
|
-
const listIdx = prompt.indexOf(
|
|
471
|
-
"The following skills are available for use with the Skill tool:",
|
|
472
|
-
);
|
|
473
|
-
const bootIdx = prompt.indexOf("BOOT-LOADED");
|
|
474
|
-
expect(sscEnd).toBeGreaterThan(-1);
|
|
475
|
-
expect(bootIdx).toBeGreaterThan(-1);
|
|
476
|
-
expect(bootIdx).toBeLessThan(sscEnd);
|
|
477
|
-
expect(listIdx).toBeGreaterThan(sscEnd);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
test("sets dispatch_prompt_path to dispatch-prompt.txt under the condition dir", () => {
|
|
481
|
-
const task = buildDispatchTask({
|
|
482
|
-
...baseOpts,
|
|
483
|
-
bootstrapContent: null,
|
|
484
|
-
});
|
|
485
|
-
expect(task.dispatch_prompt_path).toBe("/tmp/cond/dispatch-prompt.txt");
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
const SAMPLE_DIRECTORY = [
|
|
489
|
-
"## Active Skills Directory",
|
|
490
|
-
"",
|
|
491
|
-
"* **`test-driven-development`**",
|
|
492
|
-
" * *Trigger:* Use whenever implementing code.",
|
|
493
|
-
"* **`systematic-debugging`**",
|
|
494
|
-
" * *Trigger:* Use when debugging.",
|
|
495
|
-
].join("\n");
|
|
496
|
-
|
|
497
|
-
test("redactSkillFromBootstrap removes the skill-under-test's directory entry", () => {
|
|
498
|
-
const redacted = redactSkillFromBootstrap(
|
|
499
|
-
SAMPLE_DIRECTORY,
|
|
500
|
-
"test-driven-development",
|
|
501
|
-
);
|
|
502
|
-
expect(redacted).not.toContain("test-driven-development");
|
|
503
|
-
expect(redacted).not.toContain("Use whenever implementing code.");
|
|
504
|
-
// Sibling entries and the heading survive.
|
|
505
|
-
expect(redacted).toContain("systematic-debugging");
|
|
506
|
-
expect(redacted).toContain("Use when debugging.");
|
|
507
|
-
expect(redacted).toContain("## Active Skills Directory");
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
test("redacts the skill-under-test from bootstrap in the skill-absent condition", () => {
|
|
511
|
-
const withoutSkill = buildDispatchTask({
|
|
512
|
-
...baseOpts,
|
|
513
|
-
condition: "without_skill",
|
|
514
|
-
skillPath: null,
|
|
515
|
-
stagedSkillSlug: null,
|
|
516
|
-
skillName: "test-driven-development",
|
|
517
|
-
bootstrapContent: SAMPLE_DIRECTORY,
|
|
518
|
-
});
|
|
519
|
-
expect(withoutSkill.dispatch_prompt).not.toContain(
|
|
520
|
-
"test-driven-development",
|
|
521
|
-
);
|
|
522
|
-
// A sibling skill named in the same bootstrap is untouched.
|
|
523
|
-
expect(withoutSkill.dispatch_prompt).toContain("systematic-debugging");
|
|
524
|
-
|
|
525
|
-
const withSkill = buildDispatchTask({
|
|
526
|
-
...baseOpts,
|
|
527
|
-
condition: "with_skill",
|
|
528
|
-
skillPath: null,
|
|
529
|
-
stagedSkillSlug: "slow-powers-eval-1-with_skill__test-driven-development",
|
|
530
|
-
skillName: "test-driven-development",
|
|
531
|
-
bootstrapContent: SAMPLE_DIRECTORY,
|
|
532
|
-
});
|
|
533
|
-
expect(withSkill.dispatch_prompt).toContain("test-driven-development");
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
test("names the staged slug for disambiguation without instructing invocation", () => {
|
|
537
|
-
const task = buildDispatchTask({
|
|
538
|
-
...baseOpts,
|
|
539
|
-
bootstrapContent: "BOOT-LOADED",
|
|
540
|
-
});
|
|
541
|
-
// The slug is still surfaced so a deliberate invocation targets the staged
|
|
542
|
-
// version and the meta-check can find it — but we no longer assert a plugin
|
|
543
|
-
// is "loaded" or tell the agent to prefer the slug over the bare name, which
|
|
544
|
-
// invited it to hunt for a global copy (issue #144 global-plugin leakage).
|
|
545
|
-
expect(task.dispatch_prompt).toContain(
|
|
546
|
-
"slow-powers-eval-1-with_skill__foo",
|
|
547
|
-
);
|
|
548
|
-
// ...but the over-promoting invoke imperative (issue #119) is gone, so
|
|
549
|
-
// invocation reflects the skill's own triggering rather than an order.
|
|
550
|
-
expect(task.dispatch_prompt).not.toContain("invoke that slug");
|
|
551
|
-
expect(task.dispatch_prompt).not.toContain("if the skill applies");
|
|
552
|
-
expect(task.dispatch_prompt).not.toContain("under evaluation");
|
|
553
|
-
// ...and the leakage-inviting framing is gone (issue #144): no claim that a
|
|
554
|
-
// plugin is loaded, no "use the slug rather than the bare name" contrast.
|
|
555
|
-
expect(task.dispatch_prompt).not.toContain("plugin loaded");
|
|
556
|
-
expect(task.dispatch_prompt).not.toContain("rather than the bare name");
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
test("without-skill condition under realistic env carries no eval-announcing skill commentary", () => {
|
|
560
|
-
const task = buildDispatchTask({
|
|
561
|
-
...baseOpts,
|
|
562
|
-
skillPath: null,
|
|
563
|
-
stagedSkillSlug: null,
|
|
564
|
-
bootstrapContent: "BOOT-LOADED",
|
|
565
|
-
});
|
|
566
|
-
// The arm stays silent about the absent skill: the available-skills block
|
|
567
|
-
// already omits it, so nothing announces that this is an eval control arm.
|
|
568
|
-
expect(task.dispatch_prompt).not.toContain("No skill is loaded");
|
|
569
|
-
expect(task.dispatch_prompt.toLowerCase()).not.toContain("not available");
|
|
570
|
-
expect(task.dispatch_prompt).not.toContain("under evaluation");
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
test("without-skill condition without bootstrap (e.g. --no-stage) keeps the legacy 'No skill is loaded' wording", () => {
|
|
574
|
-
const task = buildDispatchTask({
|
|
575
|
-
...baseOpts,
|
|
576
|
-
skillPath: null,
|
|
577
|
-
stagedSkillSlug: null,
|
|
578
|
-
bootstrapContent: null,
|
|
579
|
-
});
|
|
580
|
-
expect(task.dispatch_prompt).toContain("No skill is loaded");
|
|
581
|
-
});
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
describe("buildDispatchTask plan-mode injection", () => {
|
|
585
|
-
const baseOpts = {
|
|
586
|
-
evalId: "e1",
|
|
587
|
-
condition: "with_skill",
|
|
588
|
-
skillPath: null,
|
|
589
|
-
stagedSkillSlug: "slow-powers-eval-1-with_skill__foo" as string | null,
|
|
590
|
-
userPrompt: "BUILD-THE-TODO-APP",
|
|
591
|
-
fixtures: [] as string[],
|
|
592
|
-
outputsDir: "/tmp/out",
|
|
593
|
-
condDir: "/tmp/cond",
|
|
594
|
-
skillName: "foo",
|
|
595
|
-
bootstrapContent: null as string | null,
|
|
596
|
-
availableSkills: [
|
|
597
|
-
{ name: "foo", path: "/x/foo/SKILL.md", description: "the foo skill" },
|
|
598
|
-
] as { name: string; path: string; description: string }[],
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
test("omits the plan-mode block when planModeContent is null/absent", () => {
|
|
602
|
-
const task = buildDispatchTask({ ...baseOpts });
|
|
603
|
-
expect(task.dispatch_prompt).not.toContain("<system-reminder>");
|
|
604
|
-
const withNull = buildDispatchTask({ ...baseOpts, planModeContent: null });
|
|
605
|
-
expect(withNull.dispatch_prompt).not.toContain("<system-reminder>");
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
test("injects the rendered plan-mode block when planModeContent is provided", () => {
|
|
609
|
-
const task = buildDispatchTask({
|
|
610
|
-
...baseOpts,
|
|
611
|
-
planModeContent: "Plan mode is active. PLAN-RAIL-MARKER.",
|
|
612
|
-
});
|
|
613
|
-
expect(task.dispatch_prompt).toContain("<system-reminder>");
|
|
614
|
-
expect(task.dispatch_prompt).toContain("PLAN-RAIL-MARKER.");
|
|
615
|
-
expect(task.dispatch_prompt).toContain("</system-reminder>");
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
test("places the plan-mode block after the available-skills block and before the user request", () => {
|
|
619
|
-
const prompt = buildDispatchTask({
|
|
620
|
-
...baseOpts,
|
|
621
|
-
planModeContent: "PLAN-RAIL-MARKER",
|
|
622
|
-
}).dispatch_prompt;
|
|
623
|
-
const skillsIdx = prompt.indexOf(
|
|
624
|
-
"The following skills are available for use with the Skill tool:",
|
|
625
|
-
);
|
|
626
|
-
const planIdx = prompt.indexOf("<system-reminder>");
|
|
627
|
-
const promptIdx = prompt.indexOf("BUILD-THE-TODO-APP");
|
|
628
|
-
expect(skillsIdx).toBeGreaterThan(-1);
|
|
629
|
-
expect(planIdx).toBeGreaterThan(skillsIdx);
|
|
630
|
-
expect(promptIdx).toBeGreaterThan(planIdx);
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
test("injects an identical plan-mode block in the with- and without-skill arms", () => {
|
|
634
|
-
const planModeContent = "Plan mode is active. PLAN-RAIL-MARKER.";
|
|
635
|
-
const rendered =
|
|
636
|
-
"<system-reminder>\nPlan mode is active. PLAN-RAIL-MARKER.\n</system-reminder>";
|
|
637
|
-
const withSkill = buildDispatchTask({
|
|
638
|
-
...baseOpts,
|
|
639
|
-
condition: "with_skill",
|
|
640
|
-
stagedSkillSlug: "slow-powers-eval-1-with_skill__foo",
|
|
641
|
-
planModeContent,
|
|
642
|
-
});
|
|
643
|
-
const withoutSkill = buildDispatchTask({
|
|
644
|
-
...baseOpts,
|
|
645
|
-
condition: "without_skill",
|
|
646
|
-
skillPath: null,
|
|
647
|
-
stagedSkillSlug: null,
|
|
648
|
-
availableSkills: [],
|
|
649
|
-
planModeContent,
|
|
650
|
-
});
|
|
651
|
-
expect(withSkill.dispatch_prompt).toContain(rendered);
|
|
652
|
-
expect(withoutSkill.dispatch_prompt).toContain(rendered);
|
|
653
|
-
});
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
describe("run.ts user-mode end-to-end (--skill-dir, isolated CWD)", () => {
|
|
657
|
-
const RUN_TS = join(import.meta.dir, "run.ts");
|
|
658
|
-
|
|
659
|
-
function setup(
|
|
660
|
-
name: string,
|
|
661
|
-
evals: Eval[] = [
|
|
662
|
-
{ id: "e1", prompt: "review this MR", expected_output: "a review" },
|
|
663
|
-
],
|
|
664
|
-
): { skillDir: string; cwd: string } {
|
|
665
|
-
const root = join(FIXTURE_ROOT, name);
|
|
666
|
-
const skillDir = join(root, "skill-dir");
|
|
667
|
-
const skillSub = join(skillDir, "mr-review");
|
|
668
|
-
mkdirSync(join(skillSub, "evals"), { recursive: true });
|
|
669
|
-
writeFileSync(
|
|
670
|
-
join(skillSub, "SKILL.md"),
|
|
671
|
-
"---\nname: mr-review\ndescription: review merge requests\n---\n\nbody\n",
|
|
672
|
-
);
|
|
673
|
-
writeFileSync(
|
|
674
|
-
join(skillSub, "evals", "evals.json"),
|
|
675
|
-
JSON.stringify({ skill_name: "mr-review", evals }),
|
|
676
|
-
);
|
|
677
|
-
const cwd = join(root, "work");
|
|
678
|
-
mkdirSync(cwd, { recursive: true });
|
|
679
|
-
return { skillDir, cwd };
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
function runCli(args: string[], cwd: string) {
|
|
683
|
-
return Bun.spawnSync(["bun", "run", RUN_TS, ...args], {
|
|
684
|
-
cwd,
|
|
685
|
-
stdout: "pipe",
|
|
686
|
-
stderr: "pipe",
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
test("stages only the skill-under-test and writes workspace under CWD", () => {
|
|
691
|
-
const { skillDir, cwd } = setup("usermode-basic");
|
|
692
|
-
const res = runCli(
|
|
693
|
-
[
|
|
694
|
-
"--skill-dir",
|
|
695
|
-
skillDir,
|
|
696
|
-
"--skill",
|
|
697
|
-
"mr-review",
|
|
698
|
-
"--mode",
|
|
699
|
-
"new-skill",
|
|
700
|
-
"--dry-run",
|
|
701
|
-
],
|
|
702
|
-
cwd,
|
|
703
|
-
);
|
|
704
|
-
expect(res.exitCode).toBe(0);
|
|
705
|
-
|
|
706
|
-
const dispatchJson = join(
|
|
707
|
-
cwd,
|
|
708
|
-
"skills-workspace",
|
|
709
|
-
"mr-review",
|
|
710
|
-
"iteration-1",
|
|
711
|
-
"dispatch.json",
|
|
712
|
-
);
|
|
713
|
-
expect(existsSync(dispatchJson)).toBe(true);
|
|
714
|
-
|
|
715
|
-
const stagedSkillsDir = join(cwd, ".claude", "skills");
|
|
716
|
-
const entries = readdirSync(stagedSkillsDir).filter(
|
|
717
|
-
(e) => e !== STAGED_SIBLING_MANIFEST,
|
|
718
|
-
);
|
|
719
|
-
expect(entries).toEqual(["slow-powers-eval-1-with_skill__mr-review"]);
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
test("--plan-mode injects the resolved profile into every dispatch and records plan_mode in dispatch.json", () => {
|
|
723
|
-
const { skillDir, cwd } = setup("usermode-plan-mode");
|
|
724
|
-
const res = runCli(
|
|
725
|
-
[
|
|
726
|
-
"--skill-dir",
|
|
727
|
-
skillDir,
|
|
728
|
-
"--skill",
|
|
729
|
-
"mr-review",
|
|
730
|
-
"--mode",
|
|
731
|
-
"new-skill",
|
|
732
|
-
"--plan-mode",
|
|
733
|
-
"--dry-run",
|
|
734
|
-
],
|
|
735
|
-
cwd,
|
|
736
|
-
);
|
|
737
|
-
expect(res.exitCode).toBe(0);
|
|
738
|
-
|
|
739
|
-
const iterationDir = join(
|
|
740
|
-
cwd,
|
|
741
|
-
"skills-workspace",
|
|
742
|
-
"mr-review",
|
|
743
|
-
"iteration-1",
|
|
744
|
-
);
|
|
745
|
-
const dispatch = JSON.parse(
|
|
746
|
-
readFileSync(join(iterationDir, "dispatch.json"), "utf8"),
|
|
747
|
-
) as {
|
|
748
|
-
plan_mode: boolean;
|
|
749
|
-
tasks: Array<{ condition: string; dispatch_prompt_path: string }>;
|
|
750
|
-
};
|
|
751
|
-
expect(dispatch.plan_mode).toBe(true);
|
|
752
|
-
|
|
753
|
-
// Both arms carry the same harness-injected plan-mode operating context.
|
|
754
|
-
for (const t of dispatch.tasks) {
|
|
755
|
-
const prompt = readFileSync(t.dispatch_prompt_path, "utf8");
|
|
756
|
-
expect(prompt).toContain("<system-reminder>");
|
|
757
|
-
expect(prompt).toContain("Plan mode is active");
|
|
758
|
-
expect(prompt).toContain("ExitPlanMode");
|
|
759
|
-
}
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
test("without --plan-mode, dispatch.json records plan_mode:false and no plan-mode block is injected", () => {
|
|
763
|
-
const { skillDir, cwd } = setup("usermode-no-plan-mode");
|
|
764
|
-
const res = runCli(
|
|
765
|
-
[
|
|
766
|
-
"--skill-dir",
|
|
767
|
-
skillDir,
|
|
768
|
-
"--skill",
|
|
769
|
-
"mr-review",
|
|
770
|
-
"--mode",
|
|
771
|
-
"new-skill",
|
|
772
|
-
"--dry-run",
|
|
773
|
-
],
|
|
774
|
-
cwd,
|
|
775
|
-
);
|
|
776
|
-
expect(res.exitCode).toBe(0);
|
|
777
|
-
|
|
778
|
-
const iterationDir = join(
|
|
779
|
-
cwd,
|
|
780
|
-
"skills-workspace",
|
|
781
|
-
"mr-review",
|
|
782
|
-
"iteration-1",
|
|
783
|
-
);
|
|
784
|
-
const dispatch = JSON.parse(
|
|
785
|
-
readFileSync(join(iterationDir, "dispatch.json"), "utf8"),
|
|
786
|
-
) as {
|
|
787
|
-
plan_mode: boolean;
|
|
788
|
-
tasks: Array<{ dispatch_prompt_path: string }>;
|
|
789
|
-
};
|
|
790
|
-
expect(dispatch.plan_mode).toBe(false);
|
|
791
|
-
for (const t of dispatch.tasks) {
|
|
792
|
-
const prompt = readFileSync(t.dispatch_prompt_path, "utf8");
|
|
793
|
-
expect(prompt).not.toContain("<system-reminder>");
|
|
794
|
-
}
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
test("--stage-name stages the SUT under the verbatim name, threads it everywhere, and registers it for cleanup", () => {
|
|
798
|
-
const { skillDir, cwd } = setup("usermode-stage-name");
|
|
799
|
-
const res = runCli(
|
|
800
|
-
[
|
|
801
|
-
"--skill-dir",
|
|
802
|
-
skillDir,
|
|
803
|
-
"--skill",
|
|
804
|
-
"mr-review",
|
|
805
|
-
"--mode",
|
|
806
|
-
"new-skill",
|
|
807
|
-
"--stage-name",
|
|
808
|
-
"mr-review",
|
|
809
|
-
"--dry-run",
|
|
810
|
-
],
|
|
811
|
-
cwd,
|
|
812
|
-
);
|
|
813
|
-
expect(res.exitCode).toBe(0);
|
|
814
|
-
|
|
815
|
-
// Staged dir is the natural name, not the conspicuous eval slug.
|
|
816
|
-
const stagedSkillsDir = join(cwd, ".claude", "skills");
|
|
817
|
-
const entries = readdirSync(stagedSkillsDir).filter(
|
|
818
|
-
(e) => e !== STAGED_SIBLING_MANIFEST,
|
|
819
|
-
);
|
|
820
|
-
expect(entries).toEqual(["mr-review"]);
|
|
821
|
-
|
|
822
|
-
const iterationDir = join(
|
|
823
|
-
cwd,
|
|
824
|
-
"skills-workspace",
|
|
825
|
-
"mr-review",
|
|
826
|
-
"iteration-1",
|
|
827
|
-
);
|
|
828
|
-
|
|
829
|
-
// conditions.json carries the natural slug — the grader meta-check reads it.
|
|
830
|
-
const conditions = JSON.parse(
|
|
831
|
-
readFileSync(join(iterationDir, "conditions.json"), "utf8"),
|
|
832
|
-
) as {
|
|
833
|
-
conditions: Array<{ name: string; staged_skill_slug: string | null }>;
|
|
834
|
-
};
|
|
835
|
-
const withSkill = conditions.conditions.find(
|
|
836
|
-
(c) => c.name === "with_skill",
|
|
837
|
-
);
|
|
838
|
-
expect(withSkill?.staged_skill_slug).toBe("mr-review");
|
|
839
|
-
|
|
840
|
-
// The custom dir is registered for cleanup (prefix scan won't catch it).
|
|
841
|
-
const manifest = JSON.parse(
|
|
842
|
-
readFileSync(join(stagedSkillsDir, STAGED_SIBLING_MANIFEST), "utf8"),
|
|
843
|
-
) as { created_entries: Array<{ name: string }> };
|
|
844
|
-
expect(manifest.created_entries.map((e) => e.name)).toContain("mr-review");
|
|
845
|
-
|
|
846
|
-
// The dispatch prompt disambiguates to the natural identifier, not the slug.
|
|
847
|
-
const dispatch = JSON.parse(
|
|
848
|
-
readFileSync(join(iterationDir, "dispatch.json"), "utf8"),
|
|
849
|
-
) as {
|
|
850
|
-
tasks: Array<{ condition: string; dispatch_prompt_path: string }>;
|
|
851
|
-
};
|
|
852
|
-
const task = dispatch.tasks.find((t) => t.condition === "with_skill");
|
|
853
|
-
const prompt = readFileSync(task?.dispatch_prompt_path ?? "", "utf8");
|
|
854
|
-
expect(prompt).toContain("registered under the identifier `mr-review`");
|
|
855
|
-
expect(prompt).not.toContain("slow-powers-eval-");
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
test("--stage-name refuses to clobber a pre-existing same-named dir", () => {
|
|
859
|
-
const { skillDir, cwd } = setup("usermode-stage-name-clobber");
|
|
860
|
-
const preexisting = join(cwd, ".claude", "skills", "my-real-skill");
|
|
861
|
-
mkdirSync(preexisting, { recursive: true });
|
|
862
|
-
writeFileSync(join(preexisting, "SKILL.md"), "USER OWNED");
|
|
863
|
-
|
|
864
|
-
const res = runCli(
|
|
865
|
-
[
|
|
866
|
-
"--skill-dir",
|
|
867
|
-
skillDir,
|
|
868
|
-
"--skill",
|
|
869
|
-
"mr-review",
|
|
870
|
-
"--mode",
|
|
871
|
-
"new-skill",
|
|
872
|
-
"--stage-name",
|
|
873
|
-
"my-real-skill",
|
|
874
|
-
"--dry-run",
|
|
875
|
-
],
|
|
876
|
-
cwd,
|
|
877
|
-
);
|
|
878
|
-
expect(res.exitCode).not.toBe(0);
|
|
879
|
-
expect(readFileSync(join(preexisting, "SKILL.md"), "utf8")).toBe(
|
|
880
|
-
"USER OWNED",
|
|
881
|
-
);
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
test("dispatch prompt lists only the skill-under-test, no other skills, and no product framing without --bootstrap", () => {
|
|
885
|
-
const { skillDir, cwd } = setup("usermode-prompt");
|
|
886
|
-
const res = runCli(
|
|
887
|
-
[
|
|
888
|
-
"--skill-dir",
|
|
889
|
-
skillDir,
|
|
890
|
-
"--skill",
|
|
891
|
-
"mr-review",
|
|
892
|
-
"--mode",
|
|
893
|
-
"new-skill",
|
|
894
|
-
"--dry-run",
|
|
895
|
-
],
|
|
896
|
-
cwd,
|
|
897
|
-
);
|
|
898
|
-
expect(res.exitCode).toBe(0);
|
|
899
|
-
|
|
900
|
-
const dispatch = JSON.parse(
|
|
901
|
-
readFileSync(
|
|
902
|
-
join(
|
|
903
|
-
cwd,
|
|
904
|
-
"skills-workspace",
|
|
905
|
-
"mr-review",
|
|
906
|
-
"iteration-1",
|
|
907
|
-
"dispatch.json",
|
|
908
|
-
),
|
|
909
|
-
"utf8",
|
|
910
|
-
),
|
|
911
|
-
) as {
|
|
912
|
-
tasks: Array<{
|
|
913
|
-
condition: string;
|
|
914
|
-
dispatch_prompt?: string;
|
|
915
|
-
dispatch_prompt_path: string;
|
|
916
|
-
}>;
|
|
917
|
-
};
|
|
918
|
-
|
|
919
|
-
const withSkill = dispatch.tasks.find((t) => t.condition === "with_skill");
|
|
920
|
-
expect(withSkill).toBeDefined();
|
|
921
|
-
// The full prompt is no longer inlined in dispatch.json — it lives in a file.
|
|
922
|
-
expect(withSkill?.dispatch_prompt).toBeUndefined();
|
|
923
|
-
const prompt = readFileSync(withSkill?.dispatch_prompt_path ?? "", "utf8");
|
|
924
|
-
expect(prompt).toContain(
|
|
925
|
-
"The following skills are available for use with the Skill tool:",
|
|
926
|
-
);
|
|
927
|
-
expect(prompt).toContain("- mr-review:");
|
|
928
|
-
expect(prompt).not.toContain("test-driven-development");
|
|
929
|
-
expect(prompt).not.toContain("writing-skills");
|
|
930
|
-
// No product framing (EXTREMELY-IMPORTANT etc.) without a --bootstrap file.
|
|
931
|
-
expect(prompt).not.toContain("EXTREMELY-IMPORTANT");
|
|
932
|
-
expect(prompt).not.toContain("loaded at session start");
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
test("writes each dispatch prompt to a file and drops the inline prompt from dispatch.json", () => {
|
|
936
|
-
const { skillDir, cwd } = setup("usermode-prompt-file");
|
|
937
|
-
const res = runCli(
|
|
938
|
-
[
|
|
939
|
-
"--skill-dir",
|
|
940
|
-
skillDir,
|
|
941
|
-
"--skill",
|
|
942
|
-
"mr-review",
|
|
943
|
-
"--mode",
|
|
944
|
-
"new-skill",
|
|
945
|
-
"--dry-run",
|
|
946
|
-
],
|
|
947
|
-
cwd,
|
|
948
|
-
);
|
|
949
|
-
expect(res.exitCode).toBe(0);
|
|
950
|
-
|
|
951
|
-
const dispatch = JSON.parse(
|
|
952
|
-
readFileSync(
|
|
953
|
-
join(
|
|
954
|
-
cwd,
|
|
955
|
-
"skills-workspace",
|
|
956
|
-
"mr-review",
|
|
957
|
-
"iteration-1",
|
|
958
|
-
"dispatch.json",
|
|
959
|
-
),
|
|
960
|
-
"utf8",
|
|
961
|
-
),
|
|
962
|
-
) as {
|
|
963
|
-
tasks: Array<{ dispatch_prompt?: string; dispatch_prompt_path: string }>;
|
|
964
|
-
};
|
|
965
|
-
|
|
966
|
-
expect(dispatch.tasks.length).toBeGreaterThan(0);
|
|
967
|
-
for (const t of dispatch.tasks) {
|
|
968
|
-
// Nothing inlined; everything goes through the file pointer.
|
|
969
|
-
expect(t.dispatch_prompt).toBeUndefined();
|
|
970
|
-
expect(t.dispatch_prompt_path.endsWith("dispatch-prompt.txt")).toBe(true);
|
|
971
|
-
expect(existsSync(t.dispatch_prompt_path)).toBe(true);
|
|
972
|
-
const contents = readFileSync(t.dispatch_prompt_path, "utf8");
|
|
973
|
-
expect(contents.length).toBeGreaterThan(0);
|
|
974
|
-
expect(contents).toContain("User request:");
|
|
975
|
-
}
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
test("--guard installs a PreToolUse hook; teardown-guard removes it", () => {
|
|
979
|
-
const { skillDir, cwd } = setup("usermode-guard");
|
|
980
|
-
const settingsPath = join(cwd, ".claude", "settings.local.json");
|
|
981
|
-
|
|
982
|
-
const res = runCli(
|
|
983
|
-
[
|
|
984
|
-
"--skill-dir",
|
|
985
|
-
skillDir,
|
|
986
|
-
"--skill",
|
|
987
|
-
"mr-review",
|
|
988
|
-
"--mode",
|
|
989
|
-
"new-skill",
|
|
990
|
-
"--guard",
|
|
991
|
-
],
|
|
992
|
-
cwd,
|
|
993
|
-
);
|
|
994
|
-
expect(res.exitCode).toBe(0);
|
|
995
|
-
expect(existsSync(settingsPath)).toBe(true);
|
|
996
|
-
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
997
|
-
expect(settings.hooks.PreToolUse[0].matcher).toContain("Write");
|
|
998
|
-
|
|
999
|
-
const down = runCli(
|
|
1000
|
-
["teardown-guard", "--skill-dir", skillDir, "--skill", "mr-review"],
|
|
1001
|
-
cwd,
|
|
1002
|
-
);
|
|
1003
|
-
expect(down.exitCode).toBe(0);
|
|
1004
|
-
expect(existsSync(settingsPath)).toBe(false);
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
test("a normal run does not install a guard", () => {
|
|
1008
|
-
const { skillDir, cwd } = setup("usermode-noguard");
|
|
1009
|
-
const res = runCli(
|
|
1010
|
-
[
|
|
1011
|
-
"--skill-dir",
|
|
1012
|
-
skillDir,
|
|
1013
|
-
"--skill",
|
|
1014
|
-
"mr-review",
|
|
1015
|
-
"--mode",
|
|
1016
|
-
"new-skill",
|
|
1017
|
-
"--dry-run",
|
|
1018
|
-
],
|
|
1019
|
-
cwd,
|
|
1020
|
-
);
|
|
1021
|
-
expect(res.exitCode).toBe(0);
|
|
1022
|
-
expect(existsSync(join(cwd, ".claude", "settings.local.json"))).toBe(false);
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
test("namespaces agent_description per iteration+run and records run_nonce", () => {
|
|
1026
|
-
const { skillDir, cwd } = setup("usermode-nonce");
|
|
1027
|
-
const res = runCli(
|
|
1028
|
-
[
|
|
1029
|
-
"--skill-dir",
|
|
1030
|
-
skillDir,
|
|
1031
|
-
"--skill",
|
|
1032
|
-
"mr-review",
|
|
1033
|
-
"--mode",
|
|
1034
|
-
"new-skill",
|
|
1035
|
-
"--dry-run",
|
|
1036
|
-
],
|
|
1037
|
-
cwd,
|
|
1038
|
-
);
|
|
1039
|
-
expect(res.exitCode).toBe(0);
|
|
1040
|
-
|
|
1041
|
-
const iterationDir = join(
|
|
1042
|
-
cwd,
|
|
1043
|
-
"skills-workspace",
|
|
1044
|
-
"mr-review",
|
|
1045
|
-
"iteration-1",
|
|
1046
|
-
);
|
|
1047
|
-
const dispatch = JSON.parse(
|
|
1048
|
-
readFileSync(join(iterationDir, "dispatch.json"), "utf8"),
|
|
1049
|
-
) as {
|
|
1050
|
-
run_nonce: string;
|
|
1051
|
-
tasks: Array<{ condition: string; agent_description: string }>;
|
|
1052
|
-
};
|
|
1053
|
-
expect(typeof dispatch.run_nonce).toBe("string");
|
|
1054
|
-
expect(dispatch.run_nonce.length).toBeGreaterThan(0);
|
|
1055
|
-
|
|
1056
|
-
for (const t of dispatch.tasks) {
|
|
1057
|
-
// <eval_id>:<condition>:i<iteration>-<nonce> — unique across iterations
|
|
1058
|
-
// and re-runs so fill-transcripts can't cross-match a colliding agent.
|
|
1059
|
-
expect(t.agent_description).toMatch(
|
|
1060
|
-
new RegExp(`:${t.condition}:i1-${dispatch.run_nonce}$`),
|
|
1061
|
-
);
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
const conditions = JSON.parse(
|
|
1065
|
-
readFileSync(join(iterationDir, "conditions.json"), "utf8"),
|
|
1066
|
-
) as { run_nonce?: string };
|
|
1067
|
-
expect(conditions.run_nonce).toBe(dispatch.run_nonce);
|
|
1068
|
-
});
|
|
1069
|
-
|
|
1070
|
-
test("--bootstrap content is prepended verbatim before the available-skills block", () => {
|
|
1071
|
-
const { skillDir, cwd } = setup("usermode-bootstrap");
|
|
1072
|
-
const bootstrapPath = join(cwd, "my-bootstrap.md");
|
|
1073
|
-
writeFileSync(bootstrapPath, "MY CUSTOM EVAL FRAMING");
|
|
1074
|
-
const res = runCli(
|
|
1075
|
-
[
|
|
1076
|
-
"--skill-dir",
|
|
1077
|
-
skillDir,
|
|
1078
|
-
"--skill",
|
|
1079
|
-
"mr-review",
|
|
1080
|
-
"--mode",
|
|
1081
|
-
"new-skill",
|
|
1082
|
-
"--bootstrap",
|
|
1083
|
-
bootstrapPath,
|
|
1084
|
-
"--dry-run",
|
|
1085
|
-
],
|
|
1086
|
-
cwd,
|
|
1087
|
-
);
|
|
1088
|
-
expect(res.exitCode).toBe(0);
|
|
1089
|
-
|
|
1090
|
-
const dispatch = JSON.parse(
|
|
1091
|
-
readFileSync(
|
|
1092
|
-
join(
|
|
1093
|
-
cwd,
|
|
1094
|
-
"skills-workspace",
|
|
1095
|
-
"mr-review",
|
|
1096
|
-
"iteration-1",
|
|
1097
|
-
"dispatch.json",
|
|
1098
|
-
),
|
|
1099
|
-
"utf8",
|
|
1100
|
-
),
|
|
1101
|
-
) as {
|
|
1102
|
-
tasks: Array<{ condition: string; dispatch_prompt_path: string }>;
|
|
1103
|
-
};
|
|
1104
|
-
const withSkill = dispatch.tasks.find((t) => t.condition === "with_skill");
|
|
1105
|
-
const prompt = withSkill
|
|
1106
|
-
? readFileSync(withSkill.dispatch_prompt_path, "utf8")
|
|
1107
|
-
: "";
|
|
1108
|
-
const bootIdx = prompt.indexOf("MY CUSTOM EVAL FRAMING");
|
|
1109
|
-
const listIdx = prompt.indexOf(
|
|
1110
|
-
"The following skills are available for use with the Skill tool:",
|
|
1111
|
-
);
|
|
1112
|
-
expect(bootIdx).toBeGreaterThan(-1);
|
|
1113
|
-
expect(listIdx).toBeGreaterThan(bootIdx);
|
|
1114
|
-
});
|
|
1115
|
-
|
|
1116
|
-
test("--only restricts dispatches to the named eval ids", () => {
|
|
1117
|
-
const { skillDir, cwd } = setup("usermode-only", [
|
|
1118
|
-
{ id: "e1", prompt: "review MR 1", expected_output: "a review" },
|
|
1119
|
-
{ id: "e2", prompt: "review MR 2", expected_output: "a review" },
|
|
1120
|
-
]);
|
|
1121
|
-
const res = runCli(
|
|
1122
|
-
[
|
|
1123
|
-
"--skill-dir",
|
|
1124
|
-
skillDir,
|
|
1125
|
-
"--skill",
|
|
1126
|
-
"mr-review",
|
|
1127
|
-
"--mode",
|
|
1128
|
-
"new-skill",
|
|
1129
|
-
"--only",
|
|
1130
|
-
"e1",
|
|
1131
|
-
"--dry-run",
|
|
1132
|
-
],
|
|
1133
|
-
cwd,
|
|
1134
|
-
);
|
|
1135
|
-
expect(res.exitCode).toBe(0);
|
|
1136
|
-
|
|
1137
|
-
const dispatch = JSON.parse(
|
|
1138
|
-
readFileSync(
|
|
1139
|
-
join(
|
|
1140
|
-
cwd,
|
|
1141
|
-
"skills-workspace",
|
|
1142
|
-
"mr-review",
|
|
1143
|
-
"iteration-1",
|
|
1144
|
-
"dispatch.json",
|
|
1145
|
-
),
|
|
1146
|
-
"utf8",
|
|
1147
|
-
),
|
|
1148
|
-
) as { tasks: Array<{ eval_id: string }> };
|
|
1149
|
-
|
|
1150
|
-
expect(dispatch.tasks.map((t) => t.eval_id).sort()).toEqual(["e1", "e1"]);
|
|
1151
|
-
// The "N evals × 2 conditions" line reflects the filtered set.
|
|
1152
|
-
expect(new TextDecoder().decode(res.stdout)).toContain(
|
|
1153
|
-
"1 evals × 2 conditions",
|
|
1154
|
-
);
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
test("--only with an unknown id exits non-zero and names the unknown id", () => {
|
|
1158
|
-
const { skillDir, cwd } = setup("usermode-only-unknown", [
|
|
1159
|
-
{ id: "e1", prompt: "review MR 1", expected_output: "a review" },
|
|
1160
|
-
]);
|
|
1161
|
-
const res = runCli(
|
|
1162
|
-
[
|
|
1163
|
-
"--skill-dir",
|
|
1164
|
-
skillDir,
|
|
1165
|
-
"--skill",
|
|
1166
|
-
"mr-review",
|
|
1167
|
-
"--mode",
|
|
1168
|
-
"new-skill",
|
|
1169
|
-
"--only",
|
|
1170
|
-
"nope",
|
|
1171
|
-
"--dry-run",
|
|
1172
|
-
],
|
|
1173
|
-
cwd,
|
|
1174
|
-
);
|
|
1175
|
-
expect(res.exitCode).not.toBe(0);
|
|
1176
|
-
expect(new TextDecoder().decode(res.stderr)).toContain(
|
|
1177
|
-
"unknown eval id(s): nope",
|
|
1178
|
-
);
|
|
1179
|
-
});
|
|
1180
|
-
});
|