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.
@@ -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 before first SDD intent",
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 before first SDD intent",
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(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
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")), true);
291
- assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-sync.md")), true);
292
- assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), true);
293
- const lazyAppliedAgent = await readFile(
294
- join(lazySddCwd, ".pi", "agents", "sdd-apply.md"),
295
- "utf8",
296
- );
297
- assert.match(lazyAppliedAgent, /model: openai\/gpt-5/);
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")), true);
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")), true);
337
- assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "chains", "sdd-full.chain.md")), true);
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 assets stale: \d+ file\(s\)/);
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")), true);
407
- assert.equal(existsSync(join(sddCwd, ".pi", "agents", "sdd-sync.md")), true);
408
- assert.equal(existsSync(join(sddCwd, ".pi", "chains", "sdd-full.chain.md")), true);
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("Compact Rules are preferred over fallback sections", () => {
32
- const rules = __testing.extractCompactRulesSection(`## Compact Rules
33
-
34
- - Explicit compact rule.
35
-
36
- ## Hard Rules
37
-
38
- - Hard rule should not be copied.
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.deepEqual(rules, ["Explicit compact rule."]);
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("LLM-first and legacy sections extract bullets, ordered lists, and tables", () => {
45
- const rules = __testing.extractCompactRulesSection(`## Hard Rules
46
-
47
- - Prefer focused tests.
48
-
49
- ## Critical Rules
50
-
51
- 1. Link an approved issue.
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
- ## Voice Rules
55
-
56
- | Rule | Requirement |
57
- |------|-------------|
58
- | Be warm | Sound like a teammate. |
59
-
60
- ## Decision Gates
61
+ ## Hard Rules
61
62
 
62
- | Target | Test pattern |
63
- |---|---|
64
- | File operations | Use t.TempDir(). |
63
+ - Do not copy this rule.
65
64
  `);
66
65
 
67
- assert.deepEqual(rules, [
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
- __testing.extractTriggerDescription("Write comments. Trigger: PR feedback, issue replies."),
79
- "PR feedback, issue replies.",
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("fallback extraction is capped at 15 rules", () => {
85
- const body = `## Hard Rules
86
-
87
- ${Array.from({ length: 16 }, (_, i) => `- Rule ${String(i + 1).padStart(2, "0")}.`).join("\n")}
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", rules: ["User rule."] },
101
- { name: "dup", path: projectPath, description: "project", rules: ["Project rule."] },
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
+ });