gentle-pi 0.3.9 → 0.3.10

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.
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { watch } from "node:fs";
2
+ import { existsSync, watch } from "node:fs";
3
3
  import {
4
4
  access,
5
5
  mkdir,
@@ -11,6 +11,7 @@ import {
11
11
  } from "node:fs/promises";
12
12
  import { homedir } from "node:os";
13
13
  import { basename, join, normalize, relative, sep } from "node:path";
14
+ import { fileURLToPath } from "node:url";
14
15
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
16
 
16
17
  const REGISTRY_REL_PATH = ".atl/skill-registry.md";
@@ -26,6 +27,13 @@ const NO_SKILL_REGISTRY_ENV = "GENTLE_PI_NO_SKILL_REGISTRY";
26
27
  const LEGACY_PROJECT_REGISTRY_REL_PATH = ".pi/extensions/skill-registry.ts";
27
28
  const LEGACY_PROJECT_REGISTRY_DISABLED_REL_PATH =
28
29
  ".pi/extensions/skill-registry.ts.disabled";
30
+ const SKILL_REGISTRY_EXTENSION_SOURCE_KEY =
31
+ "__gentlePiSkillRegistryExtensionSource";
32
+
33
+ interface SkillRegistryExtensionGlobal {
34
+ [SKILL_REGISTRY_EXTENSION_SOURCE_KEY]?: string;
35
+ }
36
+
29
37
  async function pathExists(path: string): Promise<boolean> {
30
38
  try {
31
39
  await access(path);
@@ -415,6 +423,40 @@ function shouldSkipSkillRegistryStartup(
415
423
  );
416
424
  }
417
425
 
426
+ function normalizeExtensionSource(source: string): string {
427
+ return source.split(/[?#]/, 1)[0];
428
+ }
429
+
430
+ function extensionSourcePath(source: string): string | undefined {
431
+ const cleanSource = normalizeExtensionSource(source);
432
+ if (!cleanSource.startsWith("file:")) return undefined;
433
+ try {
434
+ return comparablePath(fileURLToPath(cleanSource));
435
+ } catch {
436
+ return undefined;
437
+ }
438
+ }
439
+
440
+ function shouldSkipDuplicateExtensionLoad(
441
+ source = import.meta.url,
442
+ cwd = process.cwd(),
443
+ state = globalThis as typeof globalThis & SkillRegistryExtensionGlobal,
444
+ ): boolean {
445
+ const currentPath = extensionSourcePath(source);
446
+ const projectLocalPath = comparablePath(join(cwd, "extensions", "skill-registry.ts"));
447
+ if (currentPath && currentPath !== projectLocalPath && existsSync(projectLocalPath)) {
448
+ return true;
449
+ }
450
+
451
+ const currentSource = currentPath ?? normalizeExtensionSource(source);
452
+ const existingSource = state[SKILL_REGISTRY_EXTENSION_SOURCE_KEY];
453
+ if (!existingSource) {
454
+ state[SKILL_REGISTRY_EXTENSION_SOURCE_KEY] = currentSource;
455
+ return false;
456
+ }
457
+ return existingSource !== currentSource;
458
+ }
459
+
418
460
  async function startSkillRegistryWatcher(
419
461
  cwd: string,
420
462
  notify: (message: string) => void,
@@ -460,9 +502,12 @@ export const __testing = {
460
502
  parseFrontmatter,
461
503
  renderRegistry,
462
504
  shouldSkipSkillRegistryStartup,
505
+ shouldSkipDuplicateExtensionLoad,
463
506
  };
464
507
 
465
508
  export default function (pi: ExtensionAPI) {
509
+ if (shouldSkipDuplicateExtensionLoad()) return;
510
+
466
511
  pi.registerFlag(NO_SKILL_REGISTRY_FLAG, {
467
512
  description: "Skip the Gentle AI skill registry refresh and watcher on startup.",
468
513
  type: "boolean",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -3,6 +3,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import test from "node:test";
6
+ import { pathToFileURL } from "node:url";
6
7
  import { __testing } from "../extensions/skill-registry.ts";
7
8
 
8
9
  test("project skill dirs include supported workspace roots", () => {
@@ -115,6 +116,43 @@ test("startup skip honors no skill registry controls", () => {
115
116
  assert.equal(__testing.shouldSkipSkillRegistryStartup(disabled, [], {}), false);
116
117
  });
117
118
 
119
+ test("duplicate extension load is skipped only across different sources", () => {
120
+ const state = {};
121
+
122
+ assert.equal(
123
+ __testing.shouldSkipDuplicateExtensionLoad("file:///repo/extensions/skill-registry.ts?first", "/workspace", state),
124
+ false,
125
+ );
126
+ assert.equal(
127
+ __testing.shouldSkipDuplicateExtensionLoad("file:///repo/extensions/skill-registry.ts?second", "/workspace", state),
128
+ false,
129
+ );
130
+ assert.equal(
131
+ __testing.shouldSkipDuplicateExtensionLoad("file:///home/.pi/node_modules/gentle-pi/extensions/skill-registry.ts", "/workspace", state),
132
+ true,
133
+ );
134
+ });
135
+
136
+ test("project-local skill registry extension wins over installed package copy", () => {
137
+ const cwd = join(tmpdir(), `gentle-pi-local-extension-${Date.now()}`);
138
+ const localExtension = join(cwd, "extensions", "skill-registry.ts");
139
+ mkdirSync(dirname(localExtension), { recursive: true });
140
+ writeFileSync(localExtension, "");
141
+
142
+ assert.equal(
143
+ __testing.shouldSkipDuplicateExtensionLoad(
144
+ "file:///home/.pi/agent/npm/node_modules/gentle-pi/extensions/skill-registry.ts",
145
+ cwd,
146
+ {},
147
+ ),
148
+ true,
149
+ );
150
+ assert.equal(
151
+ __testing.shouldSkipDuplicateExtensionLoad(pathToFileURL(localExtension).href, cwd, {}),
152
+ false,
153
+ );
154
+ });
155
+
118
156
  test("scope and markdown cells are represented in registry", () => {
119
157
  const cwd = join(tmpdir(), `gentle-pi-scope-${Date.now()}`);
120
158
  const projectPath = join(cwd, "skills", "docs", "SKILL.md");