gentle-pi 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -16
- package/assets/agents/sdd-apply.md +4 -5
- package/assets/agents/sdd-archive.md +127 -9
- package/assets/agents/sdd-design.md +2 -4
- package/assets/agents/sdd-explore.md +2 -4
- package/assets/agents/sdd-init.md +2 -4
- package/assets/agents/sdd-onboard.md +2 -4
- package/assets/agents/sdd-proposal.md +2 -4
- package/assets/agents/sdd-spec.md +143 -9
- package/assets/agents/sdd-sync.md +104 -0
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +4 -5
- package/assets/chains/sdd-full.chain.md +11 -2
- package/assets/chains/sdd-verify.chain.md +11 -2
- package/assets/orchestrator.md +14 -13
- package/extensions/gentle-ai.ts +114 -21
- package/extensions/skill-registry.ts +62 -103
- package/lib/openspec-deltas.ts +156 -0
- package/lib/openspec-guardrails.ts +99 -0
- package/lib/sdd-preflight.ts +11 -5
- package/package.json +1 -1
- package/scripts/verify-package-files.mjs +12 -0
- package/skills/gentle-ai/SKILL.md +1 -1
- package/skills/judgment-day/SKILL.md +1 -1
- package/skills/judgment-day/references/prompts-and-formats.md +6 -6
- package/skills/skill-registry/SKILL.md +51 -0
- package/tests/openspec-deltas.test.ts +209 -0
- package/tests/openspec-guardrails.test.ts +71 -0
- package/tests/runtime-harness.mjs +84 -18
- package/tests/skill-registry.test.ts +93 -55
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
-
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { discoverAndLoadExtensions } from "@earendil-works/pi-coding-agent";
|
|
@@ -138,7 +138,9 @@ async function loadExtensions(pi) {
|
|
|
138
138
|
|
|
139
139
|
async function run() {
|
|
140
140
|
const globalConfigHome = await tempWorkspace();
|
|
141
|
+
const globalAgentHome = await tempWorkspace();
|
|
141
142
|
process.env.GENTLE_PI_CONFIG_HOME = globalConfigHome;
|
|
143
|
+
process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
|
|
142
144
|
const globalModelsPath = join(globalConfigHome, "models.json");
|
|
143
145
|
const { pi, hooks, commands, flags } = createPi();
|
|
144
146
|
await loadExtensions(pi);
|
|
@@ -152,6 +154,16 @@ async function run() {
|
|
|
152
154
|
assert.ok(hooks.has("before_agent_start"), "missing before_agent_start hook");
|
|
153
155
|
assert.ok(hooks.has("tool_call"), "missing tool_call hook");
|
|
154
156
|
|
|
157
|
+
for (const entry of await readdir(join(ROOT, "assets", "agents"))) {
|
|
158
|
+
if (!entry.endsWith(".md")) continue;
|
|
159
|
+
const agentPrompt = await readFile(join(ROOT, "assets", "agents", entry), "utf8");
|
|
160
|
+
assert.doesNotMatch(
|
|
161
|
+
agentPrompt,
|
|
162
|
+
/inheritProjectContext:\s*true/,
|
|
163
|
+
`${entry} must not inherit parent project context by default`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
155
167
|
const discovered = await discoverAndLoadExtensions(["./extensions"], ROOT);
|
|
156
168
|
assert.deepEqual(
|
|
157
169
|
discovered.errors,
|
|
@@ -165,6 +177,11 @@ async function run() {
|
|
|
165
177
|
const promptResult = await promptHook({ systemPrompt: "base" }, createCtx(promptCwd));
|
|
166
178
|
assert.match(promptResult.systemPrompt, /base/);
|
|
167
179
|
assert.match(promptResult.systemPrompt, /el Gentleman/);
|
|
180
|
+
const subagentPromptResult = await promptHook(
|
|
181
|
+
{ agentName: "worker", systemPrompt: "worker base" },
|
|
182
|
+
createCtx(promptCwd),
|
|
183
|
+
);
|
|
184
|
+
assert.equal(subagentPromptResult.systemPrompt, "worker base");
|
|
168
185
|
assert.equal(
|
|
169
186
|
existsSync(join(promptCwd, ".pi", "agents", "sdd-apply.md")),
|
|
170
187
|
false,
|
|
@@ -196,13 +213,15 @@ async function run() {
|
|
|
196
213
|
assert.equal(
|
|
197
214
|
existsSync(join(noUiCwd, ".pi", "agents", "sdd-apply.md")),
|
|
198
215
|
false,
|
|
199
|
-
"session_start must not install SDD agents
|
|
216
|
+
"session_start must not install project-local SDD agents",
|
|
200
217
|
);
|
|
201
218
|
assert.equal(
|
|
202
219
|
existsSync(join(noUiCwd, ".pi", "chains", "sdd-full.chain.md")),
|
|
203
220
|
false,
|
|
204
|
-
"session_start must not install SDD chains
|
|
221
|
+
"session_start must not install project-local SDD chains",
|
|
205
222
|
);
|
|
223
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
224
|
+
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
206
225
|
} finally {
|
|
207
226
|
await rm(noUiCwd, { recursive: true, force: true });
|
|
208
227
|
}
|
|
@@ -287,14 +306,14 @@ async function run() {
|
|
|
287
306
|
await inputHook({ text: "/sdd-plan this change", source: "interactive" }, ctx),
|
|
288
307
|
{ action: "continue" },
|
|
289
308
|
);
|
|
290
|
-
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")),
|
|
291
|
-
assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")),
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
);
|
|
296
|
-
assert.
|
|
297
|
-
assert.
|
|
309
|
+
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
310
|
+
assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
311
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
312
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
313
|
+
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
314
|
+
const lazySettings = JSON.parse(await readFile(join(lazySddCwd, ".pi", "settings.json"), "utf8"));
|
|
315
|
+
assert.equal(lazySettings.subagents.agentOverrides["sdd-apply"].model, "openai/gpt-5");
|
|
316
|
+
assert.equal(lazySettings.subagents.agentOverrides["sdd-apply"].thinking, "high");
|
|
298
317
|
assert.equal(ctx.ui.selections.length, 3);
|
|
299
318
|
assert.deepEqual(ctx.ui.selections[1].options, ["openspec"]);
|
|
300
319
|
assert.match(ctx.ui.notifications.at(-1).message, /SDD preflight complete/);
|
|
@@ -305,6 +324,15 @@ async function run() {
|
|
|
305
324
|
const promptResult = await promptHook({ systemPrompt: "base" }, ctx);
|
|
306
325
|
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
307
326
|
assert.match(promptResult.systemPrompt, /Execution mode: interactive/);
|
|
327
|
+
const workerPromptResult = await promptHook(
|
|
328
|
+
{ agentName: "worker", systemPrompt: "worker base" },
|
|
329
|
+
ctx,
|
|
330
|
+
);
|
|
331
|
+
assert.equal(
|
|
332
|
+
workerPromptResult.systemPrompt,
|
|
333
|
+
"worker base",
|
|
334
|
+
"non-SDD subagents must not receive parent harness or SDD preflight prompts",
|
|
335
|
+
);
|
|
308
336
|
} finally {
|
|
309
337
|
await rm(lazySddCwd, { recursive: true, force: true });
|
|
310
338
|
await rm(globalModelsPath, { force: true });
|
|
@@ -314,7 +342,8 @@ async function run() {
|
|
|
314
342
|
try {
|
|
315
343
|
const ctx = createCtx(commandSddCwd, true, "command-session");
|
|
316
344
|
await commands.get("gentle-ai:sdd-preflight").handler("", ctx);
|
|
317
|
-
assert.equal(existsSync(join(commandSddCwd, ".pi", "agents", "sdd-apply.md")),
|
|
345
|
+
assert.equal(existsSync(join(commandSddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
346
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
318
347
|
assert.equal(ctx.ui.selections.length, 3);
|
|
319
348
|
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
320
349
|
assert.equal(ctx.ui.selections.length, 3, "manual preflight command should reuse session choices");
|
|
@@ -332,13 +361,25 @@ async function run() {
|
|
|
332
361
|
},
|
|
333
362
|
ctx,
|
|
334
363
|
);
|
|
335
|
-
assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "agents", "sdd-apply.md")),
|
|
336
|
-
assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "chains", "sdd-full.chain.md")),
|
|
364
|
+
assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
365
|
+
assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
366
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
367
|
+
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
337
368
|
assert.equal(ctx.ui.selections.length, 3);
|
|
338
369
|
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
370
|
+
assert.doesNotMatch(
|
|
371
|
+
promptResult.systemPrompt,
|
|
372
|
+
/el Gentleman Identity and Harness/,
|
|
373
|
+
"SDD executor startup must not receive the parent orchestrator prompt",
|
|
374
|
+
);
|
|
375
|
+
assert.doesNotMatch(
|
|
376
|
+
promptResult.systemPrompt,
|
|
377
|
+
/Work Routing Ladder/,
|
|
378
|
+
"SDD executor startup must not receive parent routing instructions",
|
|
379
|
+
);
|
|
339
380
|
assert.match(ctx.ui.notifications.at(-1).message, /SDD preflight complete/);
|
|
340
381
|
|
|
341
|
-
await promptHook(
|
|
382
|
+
const reusedPromptResult = await promptHook(
|
|
342
383
|
{
|
|
343
384
|
agentName: "sdd-tasks",
|
|
344
385
|
systemPrompt: "You are the SDD tasks executor for Gentle AI.",
|
|
@@ -346,6 +387,11 @@ async function run() {
|
|
|
346
387
|
ctx,
|
|
347
388
|
);
|
|
348
389
|
assert.equal(ctx.ui.selections.length, 3, "SDD agent guard should reuse session choices");
|
|
390
|
+
assert.doesNotMatch(
|
|
391
|
+
reusedPromptResult.systemPrompt,
|
|
392
|
+
/el Gentleman Identity and Harness/,
|
|
393
|
+
"named SDD executor startup must not receive the parent orchestrator prompt",
|
|
394
|
+
);
|
|
349
395
|
} finally {
|
|
350
396
|
await rm(sddAgentGuardCwd, { recursive: true, force: true });
|
|
351
397
|
}
|
|
@@ -378,17 +424,37 @@ async function run() {
|
|
|
378
424
|
try {
|
|
379
425
|
const ctx = createCtx(installCwd, true);
|
|
380
426
|
await commands.get("gentle-ai:install-sdd").handler("", ctx);
|
|
381
|
-
assert.match(ctx.ui.notifications.at(-1).message, /SDD assets installed/);
|
|
427
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Global Gentle AI SDD assets installed/);
|
|
428
|
+
assert.equal(existsSync(join(installCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
429
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
382
430
|
} finally {
|
|
383
431
|
await rm(installCwd, { recursive: true, force: true });
|
|
384
432
|
}
|
|
385
433
|
|
|
434
|
+
const staleAssetsCwd = await tempWorkspace();
|
|
435
|
+
try {
|
|
436
|
+
await mkdir(join(staleAssetsCwd, ".pi", "agents"), { recursive: true });
|
|
437
|
+
await mkdir(join(staleAssetsCwd, ".pi", "chains"), { recursive: true });
|
|
438
|
+
await writeFile(join(staleAssetsCwd, ".pi", "agents", "sdd-apply.md"), "stale apply\n");
|
|
439
|
+
await writeFile(join(staleAssetsCwd, ".pi", "agents", "sdd-spec.md"), "stale spec\n");
|
|
440
|
+
await writeFile(join(staleAssetsCwd, ".pi", "chains", "sdd-full.chain.md"), "stale chain\n");
|
|
441
|
+
const ctx = createCtx(staleAssetsCwd, true);
|
|
442
|
+
await commands.get("gentle-ai:status").handler("", ctx);
|
|
443
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Project-local SDD override drift: \d+ file\(s\)/);
|
|
444
|
+
assert.match(ctx.ui.notifications.at(-1).message, /gentle-ai:install-sdd --force/);
|
|
445
|
+
} finally {
|
|
446
|
+
await rm(staleAssetsCwd, { recursive: true, force: true });
|
|
447
|
+
}
|
|
448
|
+
|
|
386
449
|
const sddCwd = await tempWorkspace();
|
|
387
450
|
try {
|
|
388
451
|
const ctx = createCtx(sddCwd, true);
|
|
389
452
|
await commands.get("sdd-init").handler("", ctx);
|
|
390
|
-
assert.equal(existsSync(join(sddCwd, ".pi", "agents", "sdd-apply.md")),
|
|
391
|
-
assert.equal(existsSync(join(sddCwd, ".pi", "chains", "sdd-full.chain.md")),
|
|
453
|
+
assert.equal(existsSync(join(sddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
454
|
+
assert.equal(existsSync(join(sddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
455
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
456
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
457
|
+
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
392
458
|
assert.equal(ctx.ui.selections.length, 3);
|
|
393
459
|
assert.match(ctx.ui.notifications[0].message, /SDD preflight complete/);
|
|
394
460
|
assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import { __testing } from "../extensions/skill-registry.ts";
|
|
7
7
|
|
|
@@ -28,68 +28,53 @@ test("project skill dirs include supported workspace roots", () => {
|
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
test("
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
test("registry renders indexed skill paths instead of compact rules", () => {
|
|
32
|
+
const cwd = join(tmpdir(), `gentle-pi-render-${Date.now()}`);
|
|
33
|
+
const skillPath = join(cwd, "skills", "go-testing", "SKILL.md");
|
|
34
|
+
const registry = __testing.renderRegistry(cwd, ["skills"], [
|
|
35
|
+
{
|
|
36
|
+
name: "go-testing",
|
|
37
|
+
path: skillPath,
|
|
38
|
+
description: "Trigger: Go tests. Apply focused testing patterns.",
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
40
41
|
|
|
41
|
-
assert.
|
|
42
|
+
assert.match(registry, /## Skills/);
|
|
43
|
+
assert.match(registry, /\| Skill \| Trigger \/ description \| Scope \| Path \|/);
|
|
44
|
+
assert.match(registry, /## Loading protocol/);
|
|
45
|
+
assert.match(registry, /\| `go-testing` \| Trigger: Go tests\. Apply focused testing patterns\. \| project \|/);
|
|
46
|
+
assert.match(registry, new RegExp(skillPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
47
|
+
assert.doesNotMatch(registry, /Selected skills and compact rules/);
|
|
48
|
+
assert.doesNotMatch(registry, /Project Standards \(auto-resolved\)/);
|
|
49
|
+
assert.doesNotMatch(registry, /Rules:/);
|
|
42
50
|
});
|
|
43
51
|
|
|
44
|
-
test("
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
2. Keep PRs within review budget.
|
|
52
|
+
test("frontmatter parser keeps full multiline descriptions", () => {
|
|
53
|
+
const parsed = __testing.parseFrontmatter(`---
|
|
54
|
+
name: ai-sdk-5
|
|
55
|
+
description: >
|
|
56
|
+
Trigger: AI chat features, Vercel AI SDK 5, streaming UI.
|
|
57
|
+
Use AI SDK 5 patterns and avoid v4 APIs.
|
|
58
|
+
license: Apache-2.0
|
|
59
|
+
---
|
|
53
60
|
|
|
54
|
-
##
|
|
55
|
-
|
|
56
|
-
| Rule | Requirement |
|
|
57
|
-
|------|-------------|
|
|
58
|
-
| Be warm | Sound like a teammate. |
|
|
59
|
-
|
|
60
|
-
## Decision Gates
|
|
61
|
+
## Hard Rules
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|---|---|
|
|
64
|
-
| File operations | Use t.TempDir(). |
|
|
63
|
+
- Do not copy this rule.
|
|
65
64
|
`);
|
|
66
65
|
|
|
67
|
-
assert.
|
|
68
|
-
"Prefer focused tests.",
|
|
69
|
-
"Link an approved issue.",
|
|
70
|
-
"Keep PRs within review budget.",
|
|
71
|
-
"Be warm: Sound like a teammate.",
|
|
72
|
-
"File operations: Use t.TempDir().",
|
|
73
|
-
]);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("description trigger text is extracted when present", () => {
|
|
66
|
+
assert.equal(parsed.name, "ai-sdk-5");
|
|
77
67
|
assert.equal(
|
|
78
|
-
|
|
79
|
-
"
|
|
68
|
+
parsed.description,
|
|
69
|
+
"Trigger: AI chat features, Vercel AI SDK 5, streaming UI. Use AI SDK 5 patterns and avoid v4 APIs.",
|
|
80
70
|
);
|
|
81
|
-
assert.equal(__testing.extractTriggerDescription("No explicit trigger."), "No explicit trigger.");
|
|
82
71
|
});
|
|
83
72
|
|
|
84
|
-
test("
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const rules = __testing.extractCompactRulesSection(body);
|
|
90
|
-
|
|
91
|
-
assert.equal(rules.length, 15);
|
|
92
|
-
assert.equal(rules.at(-1), "Rule 15.");
|
|
73
|
+
test("description normalization preserves trigger and collapses whitespace", () => {
|
|
74
|
+
assert.equal(
|
|
75
|
+
__testing.normalizeSkillDescription("Trigger: PR feedback, issue replies.\nUse maintainer voice."),
|
|
76
|
+
"Trigger: PR feedback, issue replies. Use maintainer voice.",
|
|
77
|
+
);
|
|
93
78
|
});
|
|
94
79
|
|
|
95
80
|
test("project-scoped duplicate wins over user duplicate", () => {
|
|
@@ -97,8 +82,8 @@ test("project-scoped duplicate wins over user duplicate", () => {
|
|
|
97
82
|
const projectPath = join(cwd, ".opencode/skills/dup/SKILL.md");
|
|
98
83
|
const userPath = join(cwd + "-home", ".config/opencode/skills/dup/SKILL.md");
|
|
99
84
|
const entries = [
|
|
100
|
-
{ name: "dup", path: userPath, description: "user"
|
|
101
|
-
{ name: "dup", path: projectPath, description: "project"
|
|
85
|
+
{ name: "dup", path: userPath, description: "user" },
|
|
86
|
+
{ name: "dup", path: projectPath, description: "project" },
|
|
102
87
|
];
|
|
103
88
|
|
|
104
89
|
const [chosen] = __testing.dedupeBySkillName(entries, cwd);
|
|
@@ -129,3 +114,56 @@ test("startup skip honors no skill registry controls", () => {
|
|
|
129
114
|
);
|
|
130
115
|
assert.equal(__testing.shouldSkipSkillRegistryStartup(disabled, [], {}), false);
|
|
131
116
|
});
|
|
117
|
+
|
|
118
|
+
test("scope and markdown cells are represented in registry", () => {
|
|
119
|
+
const cwd = join(tmpdir(), `gentle-pi-scope-${Date.now()}`);
|
|
120
|
+
const projectPath = join(cwd, "skills", "docs", "SKILL.md");
|
|
121
|
+
const userPath = join(tmpdir(), `gentle-pi-home-${Date.now()}`, ".claude", "skills", "docs", "SKILL.md");
|
|
122
|
+
const registry = __testing.renderRegistry(cwd, ["skills"], [
|
|
123
|
+
{ name: "project-docs", path: projectPath, description: "Docs | guides" },
|
|
124
|
+
{ name: "user-docs", path: userPath, description: "" },
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
assert.match(registry, /\| `project-docs` \| Docs \\\| guides \| project \|/);
|
|
128
|
+
assert.match(registry, /\| `user-docs` \| — \| user \|/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("generated registry file indexes skill path and omits body rules", async () => {
|
|
132
|
+
const cwd = join(tmpdir(), `gentle-pi-regenerate-${Date.now()}`);
|
|
133
|
+
const skillPath = join(cwd, "skills", "go-testing", "SKILL.md");
|
|
134
|
+
mkdirSync(dirname(skillPath), { recursive: true });
|
|
135
|
+
writeFileSync(
|
|
136
|
+
skillPath,
|
|
137
|
+
`---
|
|
138
|
+
name: go-testing
|
|
139
|
+
description: "Trigger: Go tests. Apply focused Go testing patterns."
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Hard Rules
|
|
143
|
+
|
|
144
|
+
- Run focused tests before broad tests.
|
|
145
|
+
`,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const dirs = await __testing.uniqueExistingDirs(__testing.projectSkillDirs(cwd));
|
|
149
|
+
assert.ok(dirs.includes(join(cwd, "skills")));
|
|
150
|
+
|
|
151
|
+
const registry = __testing.renderRegistry(cwd, ["skills"], [
|
|
152
|
+
{
|
|
153
|
+
name: "go-testing",
|
|
154
|
+
path: skillPath,
|
|
155
|
+
description: "Trigger: Go tests. Apply focused Go testing patterns.",
|
|
156
|
+
},
|
|
157
|
+
]);
|
|
158
|
+
assert.match(registry, /go-testing/);
|
|
159
|
+
assert.match(registry, /Trigger: Go tests\. Apply focused Go testing patterns\./);
|
|
160
|
+
assert.match(registry, new RegExp(skillPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
161
|
+
assert.doesNotMatch(registry, /Run focused tests before broad tests/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("orchestrator documents path injection protocol", () => {
|
|
165
|
+
const source = readFileSync(join(import.meta.dirname, "..", "assets", "orchestrator.md"), "utf8");
|
|
166
|
+
assert.match(source, /## Skills to load before work/);
|
|
167
|
+
assert.match(source, /paths-injected/);
|
|
168
|
+
assert.doesNotMatch(source, /Use matching compact rules based on code context and task intent/);
|
|
169
|
+
});
|