gentle-pi 0.3.4 → 0.3.6
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 +14 -15
- package/assets/agents/sdd-apply.md +4 -5
- package/assets/agents/sdd-archive.md +2 -3
- 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 +2 -3
- package/assets/agents/sdd-sync.md +2 -3
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +4 -5
- package/assets/orchestrator.md +22 -14
- package/extensions/gentle-ai.ts +83 -27
- package/extensions/skill-registry.ts +62 -103
- package/lib/sdd-preflight.ts +36 -7
- package/package.json +1 -1
- 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/runtime-harness.mjs +125 -22
- package/tests/skill-registry.test.ts +93 -55
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skill-registry
|
|
3
|
+
description: "Trigger: update skills, skill registry, actualizar skills, after skill changes. Index available skills by trigger and path."
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: gentleman-programming
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Activation Contract
|
|
11
|
+
|
|
12
|
+
Use this skill after installing, removing, creating, moving, or renaming skills, or when a delegator needs a fresh skill index.
|
|
13
|
+
|
|
14
|
+
## Hard Rules
|
|
15
|
+
|
|
16
|
+
- The registry is an index, not a compiler or summary. `SKILL.md` remains the source of truth.
|
|
17
|
+
- Do not generate or inject compact rules by default; preserve author intent by passing exact skill paths to subagents.
|
|
18
|
+
- Always write `.atl/skill-registry.md` regardless of SDD persistence mode.
|
|
19
|
+
- Save the registry to Engram as `topic_key: skill-registry` when available, with `capture_prompt: false`.
|
|
20
|
+
- Skip `sdd-*`, `_shared`, and `skill-registry`; deduplicate by skill name, preferring project-level skills over user-level skills.
|
|
21
|
+
- Add `.atl/` to `.gitignore` when possible unless explicitly disabled.
|
|
22
|
+
|
|
23
|
+
## Decision Gates
|
|
24
|
+
|
|
25
|
+
| Situation | Action |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| Same skill exists globally and in project | Keep the project-level skill |
|
|
28
|
+
| Same skill exists in multiple global locations | Keep the first source in scan order |
|
|
29
|
+
| No skills found | Write an empty registry so agents stop searching blindly |
|
|
30
|
+
| Agent will delegate work | Select matching registry rows and pass their `SKILL.md` paths |
|
|
31
|
+
|
|
32
|
+
## Execution Steps
|
|
33
|
+
|
|
34
|
+
1. Scan all known user and project skill directories for `*/SKILL.md`.
|
|
35
|
+
2. Read frontmatter only as needed to extract `name` and `description` trigger text.
|
|
36
|
+
3. Render `.atl/skill-registry.md` with scanned sources, registry contract, skill name, trigger/description, scope, and exact path.
|
|
37
|
+
4. Persist to Engram when available using `title: skill-registry`, `topic_key: skill-registry`, `type: config`, and `capture_prompt: false`.
|
|
38
|
+
5. Return the registry path, skill count, cache status, and whether Engram was updated.
|
|
39
|
+
|
|
40
|
+
## Output Contract
|
|
41
|
+
|
|
42
|
+
Return:
|
|
43
|
+
- Project name and `.atl/skill-registry.md` path.
|
|
44
|
+
- Number of indexed skills.
|
|
45
|
+
- Whether the cache was hit or regenerated.
|
|
46
|
+
- Any skipped or duplicate skills when relevant.
|
|
47
|
+
|
|
48
|
+
## References
|
|
49
|
+
|
|
50
|
+
- `docs/skill-style-guide.md` — how skills should be authored before indexing.
|
|
51
|
+
- `skills/_shared/skill-resolver.md` — how delegators use the index.
|
|
@@ -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,13 @@ 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
|
+
assert.match(promptResult.systemPrompt, /openspec\/config\.yaml.*not session preflight/s);
|
|
181
|
+
assert.match(promptResult.systemPrompt, /Do not mark SDD preflight complete/);
|
|
182
|
+
const subagentPromptResult = await promptHook(
|
|
183
|
+
{ agentName: "worker", systemPrompt: "worker base" },
|
|
184
|
+
createCtx(promptCwd),
|
|
185
|
+
);
|
|
186
|
+
assert.equal(subagentPromptResult.systemPrompt, "worker base");
|
|
168
187
|
assert.equal(
|
|
169
188
|
existsSync(join(promptCwd, ".pi", "agents", "sdd-apply.md")),
|
|
170
189
|
false,
|
|
@@ -196,13 +215,15 @@ async function run() {
|
|
|
196
215
|
assert.equal(
|
|
197
216
|
existsSync(join(noUiCwd, ".pi", "agents", "sdd-apply.md")),
|
|
198
217
|
false,
|
|
199
|
-
"session_start must not install SDD agents
|
|
218
|
+
"session_start must not install project-local SDD agents",
|
|
200
219
|
);
|
|
201
220
|
assert.equal(
|
|
202
221
|
existsSync(join(noUiCwd, ".pi", "chains", "sdd-full.chain.md")),
|
|
203
222
|
false,
|
|
204
|
-
"session_start must not install SDD chains
|
|
223
|
+
"session_start must not install project-local SDD chains",
|
|
205
224
|
);
|
|
225
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
226
|
+
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
206
227
|
} finally {
|
|
207
228
|
await rm(noUiCwd, { recursive: true, force: true });
|
|
208
229
|
}
|
|
@@ -265,10 +286,26 @@ async function run() {
|
|
|
265
286
|
);
|
|
266
287
|
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
267
288
|
|
|
289
|
+
assert.deepEqual(
|
|
290
|
+
await inputHook({ text: "vamos con sdd", source: "interactive" }, ctx),
|
|
291
|
+
{ action: "continue" },
|
|
292
|
+
);
|
|
293
|
+
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
294
|
+
assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
295
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
296
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
297
|
+
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
298
|
+
assert.equal(ctx.ui.selections.length, 3);
|
|
299
|
+
assert.equal(ctx.ui.selections[0].label, "SDD execution mode");
|
|
300
|
+
assert.equal(ctx.ui.selections[1].label, "SDD artifact store");
|
|
301
|
+
assert.deepEqual(ctx.ui.selections[1].options, ["openspec"]);
|
|
302
|
+
assert.equal(ctx.ui.selections[2].label, "SDD PR chaining");
|
|
303
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Preference source: user prompt/);
|
|
268
304
|
assert.deepEqual(
|
|
269
305
|
await inputHook({ text: "please use sdd for this change", source: "interactive" }, ctx),
|
|
270
306
|
{ action: "continue" },
|
|
271
307
|
);
|
|
308
|
+
assert.equal(ctx.ui.selections.length, 3, "natural SDD trigger should reuse session choices");
|
|
272
309
|
assert.deepEqual(
|
|
273
310
|
await inputHook({ text: "/sdd", source: "interactive" }, ctx),
|
|
274
311
|
{ action: "continue" },
|
|
@@ -281,21 +318,20 @@ async function run() {
|
|
|
281
318
|
await inputHook({ text: "/sdd:plan", source: "interactive" }, ctx),
|
|
282
319
|
{ action: "continue" },
|
|
283
320
|
);
|
|
284
|
-
assert.equal(
|
|
321
|
+
assert.equal(ctx.ui.selections.length, 3);
|
|
285
322
|
|
|
286
323
|
assert.deepEqual(
|
|
287
324
|
await inputHook({ text: "/sdd-plan this change", source: "interactive" }, ctx),
|
|
288
325
|
{ action: "continue" },
|
|
289
326
|
);
|
|
290
|
-
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")),
|
|
291
|
-
assert.equal(existsSync(join(lazySddCwd, ".pi", "
|
|
292
|
-
assert.equal(existsSync(join(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
);
|
|
297
|
-
assert.
|
|
298
|
-
assert.match(lazyAppliedAgent, /thinking: high/);
|
|
327
|
+
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
328
|
+
assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
329
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
330
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
331
|
+
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
332
|
+
const lazySettings = JSON.parse(await readFile(join(lazySddCwd, ".pi", "settings.json"), "utf8"));
|
|
333
|
+
assert.equal(lazySettings.subagents.agentOverrides["sdd-apply"].model, "openai/gpt-5");
|
|
334
|
+
assert.equal(lazySettings.subagents.agentOverrides["sdd-apply"].thinking, "high");
|
|
299
335
|
assert.equal(ctx.ui.selections.length, 3);
|
|
300
336
|
assert.deepEqual(ctx.ui.selections[1].options, ["openspec"]);
|
|
301
337
|
assert.match(ctx.ui.notifications.at(-1).message, /SDD preflight complete/);
|
|
@@ -306,16 +342,42 @@ async function run() {
|
|
|
306
342
|
const promptResult = await promptHook({ systemPrompt: "base" }, ctx);
|
|
307
343
|
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
308
344
|
assert.match(promptResult.systemPrompt, /Execution mode: interactive/);
|
|
345
|
+
const workerPromptResult = await promptHook(
|
|
346
|
+
{ agentName: "worker", systemPrompt: "worker base" },
|
|
347
|
+
ctx,
|
|
348
|
+
);
|
|
349
|
+
assert.equal(
|
|
350
|
+
workerPromptResult.systemPrompt,
|
|
351
|
+
"worker base",
|
|
352
|
+
"non-SDD subagents must not receive parent harness or SDD preflight prompts",
|
|
353
|
+
);
|
|
309
354
|
} finally {
|
|
310
355
|
await rm(lazySddCwd, { recursive: true, force: true });
|
|
311
356
|
await rm(globalModelsPath, { force: true });
|
|
312
357
|
}
|
|
313
358
|
|
|
359
|
+
for (const [index, text] of ["/sdd", "/sdd plan", "/sdd:plan", "/sdd-plan this change"].entries()) {
|
|
360
|
+
const slashSddCwd = await tempWorkspace();
|
|
361
|
+
try {
|
|
362
|
+
const ctx = createCtx(slashSddCwd, true, `slash-sdd-session-${index}`);
|
|
363
|
+
const inputHook = hooks.get("input")[0];
|
|
364
|
+
assert.deepEqual(await inputHook({ text, source: "interactive" }, ctx), {
|
|
365
|
+
action: "continue",
|
|
366
|
+
});
|
|
367
|
+
assert.equal(existsSync(join(slashSddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
368
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
369
|
+
assert.equal(ctx.ui.selections.length, 3, `${text} should run canonical preflight`);
|
|
370
|
+
} finally {
|
|
371
|
+
await rm(slashSddCwd, { recursive: true, force: true });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
314
375
|
const commandSddCwd = await tempWorkspace();
|
|
315
376
|
try {
|
|
316
377
|
const ctx = createCtx(commandSddCwd, true, "command-session");
|
|
317
378
|
await commands.get("gentle-ai:sdd-preflight").handler("", ctx);
|
|
318
|
-
assert.equal(existsSync(join(commandSddCwd, ".pi", "agents", "sdd-apply.md")),
|
|
379
|
+
assert.equal(existsSync(join(commandSddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
380
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
319
381
|
assert.equal(ctx.ui.selections.length, 3);
|
|
320
382
|
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
321
383
|
assert.equal(ctx.ui.selections.length, 3, "manual preflight command should reuse session choices");
|
|
@@ -333,13 +395,25 @@ async function run() {
|
|
|
333
395
|
},
|
|
334
396
|
ctx,
|
|
335
397
|
);
|
|
336
|
-
assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "agents", "sdd-apply.md")),
|
|
337
|
-
assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "chains", "sdd-full.chain.md")),
|
|
398
|
+
assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
399
|
+
assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
400
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
401
|
+
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
338
402
|
assert.equal(ctx.ui.selections.length, 3);
|
|
339
403
|
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
404
|
+
assert.doesNotMatch(
|
|
405
|
+
promptResult.systemPrompt,
|
|
406
|
+
/el Gentleman Identity and Harness/,
|
|
407
|
+
"SDD executor startup must not receive the parent orchestrator prompt",
|
|
408
|
+
);
|
|
409
|
+
assert.doesNotMatch(
|
|
410
|
+
promptResult.systemPrompt,
|
|
411
|
+
/Work Routing Ladder/,
|
|
412
|
+
"SDD executor startup must not receive parent routing instructions",
|
|
413
|
+
);
|
|
340
414
|
assert.match(ctx.ui.notifications.at(-1).message, /SDD preflight complete/);
|
|
341
415
|
|
|
342
|
-
await promptHook(
|
|
416
|
+
const reusedPromptResult = await promptHook(
|
|
343
417
|
{
|
|
344
418
|
agentName: "sdd-tasks",
|
|
345
419
|
systemPrompt: "You are the SDD tasks executor for Gentle AI.",
|
|
@@ -347,10 +421,35 @@ async function run() {
|
|
|
347
421
|
ctx,
|
|
348
422
|
);
|
|
349
423
|
assert.equal(ctx.ui.selections.length, 3, "SDD agent guard should reuse session choices");
|
|
424
|
+
assert.doesNotMatch(
|
|
425
|
+
reusedPromptResult.systemPrompt,
|
|
426
|
+
/el Gentleman Identity and Harness/,
|
|
427
|
+
"named SDD executor startup must not receive the parent orchestrator prompt",
|
|
428
|
+
);
|
|
350
429
|
} finally {
|
|
351
430
|
await rm(sddAgentGuardCwd, { recursive: true, force: true });
|
|
352
431
|
}
|
|
353
432
|
|
|
433
|
+
const noUiSddAgentCwd = await tempWorkspace();
|
|
434
|
+
try {
|
|
435
|
+
const ctx = createCtx(noUiSddAgentCwd, false, "no-ui-sdd-agent-session");
|
|
436
|
+
const promptHook = hooks.get("before_agent_start")[0];
|
|
437
|
+
const promptResult = await promptHook(
|
|
438
|
+
{
|
|
439
|
+
agentName: "sdd-proposal",
|
|
440
|
+
systemPrompt: "You are the SDD proposal executor for Gentle AI.",
|
|
441
|
+
},
|
|
442
|
+
ctx,
|
|
443
|
+
);
|
|
444
|
+
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
445
|
+
assert.match(promptResult.systemPrompt, /No interactive UI was available/);
|
|
446
|
+
assert.equal(ctx.ui.selections.length, 0);
|
|
447
|
+
assert.equal(existsSync(join(noUiSddAgentCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
448
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
449
|
+
} finally {
|
|
450
|
+
await rm(noUiSddAgentCwd, { recursive: true, force: true });
|
|
451
|
+
}
|
|
452
|
+
|
|
354
453
|
const invalidPreflightCwd = await tempWorkspace();
|
|
355
454
|
try {
|
|
356
455
|
await writeFile(globalModelsPath, "{ invalid json");
|
|
@@ -379,7 +478,9 @@ async function run() {
|
|
|
379
478
|
try {
|
|
380
479
|
const ctx = createCtx(installCwd, true);
|
|
381
480
|
await commands.get("gentle-ai:install-sdd").handler("", ctx);
|
|
382
|
-
assert.match(ctx.ui.notifications.at(-1).message, /SDD assets installed/);
|
|
481
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Global Gentle AI SDD assets installed/);
|
|
482
|
+
assert.equal(existsSync(join(installCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
483
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
383
484
|
} finally {
|
|
384
485
|
await rm(installCwd, { recursive: true, force: true });
|
|
385
486
|
}
|
|
@@ -393,7 +494,7 @@ async function run() {
|
|
|
393
494
|
await writeFile(join(staleAssetsCwd, ".pi", "chains", "sdd-full.chain.md"), "stale chain\n");
|
|
394
495
|
const ctx = createCtx(staleAssetsCwd, true);
|
|
395
496
|
await commands.get("gentle-ai:status").handler("", ctx);
|
|
396
|
-
assert.match(ctx.ui.notifications.at(-1).message, /SDD
|
|
497
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Project-local SDD override drift: \d+ file\(s\)/);
|
|
397
498
|
assert.match(ctx.ui.notifications.at(-1).message, /gentle-ai:install-sdd --force/);
|
|
398
499
|
} finally {
|
|
399
500
|
await rm(staleAssetsCwd, { recursive: true, force: true });
|
|
@@ -403,9 +504,11 @@ async function run() {
|
|
|
403
504
|
try {
|
|
404
505
|
const ctx = createCtx(sddCwd, true);
|
|
405
506
|
await commands.get("sdd-init").handler("", ctx);
|
|
406
|
-
assert.equal(existsSync(join(sddCwd, ".pi", "agents", "sdd-apply.md")),
|
|
407
|
-
assert.equal(existsSync(join(sddCwd, ".pi", "
|
|
408
|
-
assert.equal(existsSync(join(
|
|
507
|
+
assert.equal(existsSync(join(sddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
508
|
+
assert.equal(existsSync(join(sddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
509
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
510
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
511
|
+
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
409
512
|
assert.equal(ctx.ui.selections.length, 3);
|
|
410
513
|
assert.match(ctx.ui.notifications[0].message, /SDD preflight complete/);
|
|
411
514
|
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
|
+
});
|