gentle-pi 0.2.6 → 0.2.7

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 CHANGED
@@ -213,6 +213,7 @@ Behavior:
213
213
 
214
214
  - `.atl/` is added to `.gitignore` when needed;
215
215
  - the registry refreshes on session start;
216
+ - startup refresh is skipped when Pi starts with `--no-skills` / `-ns`, `--no-skill-registry`, or `GENTLE_PI_NO_SKILL_REGISTRY=1`;
216
217
  - `/skill-registry:refresh` forces regeneration;
217
218
  - a best-effort watcher refreshes when skill files change;
218
219
  - `## Compact Rules` wins when present; otherwise the registry extracts compact rules from `## Hard Rules`, `## Critical Rules`, `## Critical Patterns`, `## Voice Rules`, and `## Decision Gates` using bullets, numbered lists, or simple tables.
@@ -285,6 +286,14 @@ Saved at:
285
286
  | `/gentle-ai:install-sdd --force` | Force-refreshes installed SDD assets. |
286
287
  | `/skill-registry:refresh` | Regenerates `.atl/skill-registry.md`. |
287
288
 
289
+ Startup flag:
290
+
291
+ ```text
292
+ pi --no-skill-registry
293
+ ```
294
+
295
+ Use it when you want skills available normally but do not want Gentle AI to refresh/watch `.atl/skill-registry.md` on startup. `pi -ns` / `pi --no-skills` also skip the registry startup work because Pi is already disabling skill loading.
296
+
288
297
  Compatibility aliases:
289
298
 
290
299
  ```text
@@ -31,6 +31,8 @@ User-facing conversation should stay in the user's language and follow the curre
31
31
 
32
32
  Subagent-facing prompts should be written in English by default, even when the user speaks Spanish. Translate the user's request into concise English before delegation. This keeps token usage lower and gives built-in/project subagents a consistent operating language without changing the user-facing persona.
33
33
 
34
+ Generated artifacts — whether by the parent inline or by subagents — (code, UI copy, comments, identifiers, commit messages, filenames, PR descriptions) default to English, regardless of the user's conversation language. Override only when the user explicitly requests another language for that artifact, or when extending a project whose existing convention is non-English.
35
+
34
36
  Exceptions:
35
37
 
36
38
  - Preserve exact user quotes, UI copy, error messages, filenames, commands, and domain terms in their original language when they are evidence.
@@ -471,15 +471,19 @@ class SddModelPanel implements OverlayComponent {
471
471
  private query = "";
472
472
  private readonly draft: AgentModelConfig;
473
473
  private readonly rows: string[];
474
+ private readonly modelOptions: string[];
475
+ private readonly done: (result: ModelPanelResult) => void;
474
476
 
475
477
  constructor(
476
478
  initialConfig: AgentModelConfig,
477
- private readonly modelOptions: string[],
479
+ modelOptions: string[],
478
480
  agents: string[],
479
- private readonly done: (result: ModelPanelResult) => void,
481
+ done: (result: ModelPanelResult) => void,
480
482
  ) {
481
483
  this.draft = { ...initialConfig };
482
484
  this.rows = [SET_ALL_AGENTS, ...agents];
485
+ this.modelOptions = modelOptions;
486
+ this.done = done;
483
487
  }
484
488
 
485
489
  invalidate(): void {}
@@ -21,6 +21,8 @@ const EXCLUDE_PREFIXES = ["sdd-"];
21
21
  const ATL_IGNORE_ENTRY = ".atl/";
22
22
  const WATCH_DEBOUNCE_MS = 500;
23
23
  const REGISTRY_SCHEMA_VERSION = 4;
24
+ const NO_SKILL_REGISTRY_FLAG = "no-skill-registry";
25
+ const NO_SKILL_REGISTRY_ENV = "GENTLE_PI_NO_SKILL_REGISTRY";
24
26
  const LEGACY_PROJECT_REGISTRY_REL_PATH = ".pi/extensions/skill-registry.ts";
25
27
  const LEGACY_PROJECT_REGISTRY_DISABLED_REL_PATH =
26
28
  ".pi/extensions/skill-registry.ts.disabled";
@@ -410,6 +412,26 @@ function regenerateRegistry(cwd: string, force: boolean): RegenResult {
410
412
 
411
413
  const watchedCwds = new Set<string>();
412
414
 
415
+ function isTruthyEnv(value: string | undefined): boolean {
416
+ return value === "1" || value === "true" || value === "yes" || value === "on";
417
+ }
418
+
419
+ function hasCliArg(args: string[], ...names: string[]): boolean {
420
+ return args.some((arg) => names.includes(arg));
421
+ }
422
+
423
+ function shouldSkipSkillRegistryStartup(
424
+ pi: Pick<ExtensionAPI, "getFlag">,
425
+ argv = process.argv.slice(2),
426
+ env = process.env,
427
+ ): boolean {
428
+ return (
429
+ pi.getFlag(NO_SKILL_REGISTRY_FLAG) === true ||
430
+ isTruthyEnv(env[NO_SKILL_REGISTRY_ENV]) ||
431
+ hasCliArg(argv, "--no-skills", "-ns")
432
+ );
433
+ }
434
+
413
435
  function startSkillRegistryWatcher(cwd: string, notify: (message: string) => void): void {
414
436
  if (watchedCwds.has(cwd)) return;
415
437
  watchedCwds.add(cwd);
@@ -444,10 +466,18 @@ export const __testing = {
444
466
  extractTriggerDescription,
445
467
  uniqueExistingDirs,
446
468
  dedupeBySkillName,
469
+ shouldSkipSkillRegistryStartup,
447
470
  };
448
471
 
449
472
  export default function (pi: ExtensionAPI) {
473
+ pi.registerFlag(NO_SKILL_REGISTRY_FLAG, {
474
+ description: "Skip the Gentle AI skill registry refresh and watcher on startup.",
475
+ type: "boolean",
476
+ default: false,
477
+ });
478
+
450
479
  pi.on("session_start", async (_event, ctx) => {
480
+ if (shouldSkipSkillRegistryStartup(pi)) return;
451
481
  try {
452
482
  ensureAtlIgnored(ctx.cwd);
453
483
  const quarantinedLegacy = quarantineLegacyProjectRegistry(ctx.cwd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
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",
@@ -32,7 +32,8 @@
32
32
  "README.md"
33
33
  ],
34
34
  "scripts": {
35
- "test": "node --experimental-strip-types --test tests/*.test.ts",
35
+ "test": "node --experimental-strip-types --test tests/*.test.ts && pnpm run test:harness",
36
+ "test:harness": "node --experimental-strip-types tests/runtime-harness.mjs",
36
37
  "prepack": "pnpm test && node scripts/verify-package-files.mjs",
37
38
  "prepublishOnly": "pnpm test && node scripts/verify-package-files.mjs"
38
39
  },
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+ import assert from "node:assert/strict";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+
8
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
9
+ const EXTENSIONS = [
10
+ "extensions/gentle-ai.ts",
11
+ "extensions/skill-registry.ts",
12
+ "extensions/sdd-init.ts",
13
+ "extensions/startup-banner.ts",
14
+ ];
15
+
16
+ const EXPECTED_COMMANDS = [
17
+ "gentle-ai:install-sdd",
18
+ "gentle:models",
19
+ "gentle-ai:models",
20
+ "gentleman:models",
21
+ "gentle:persona",
22
+ "gentle-ai:persona",
23
+ "gentleman:persona",
24
+ "gentle-ai:status",
25
+ "sdd-init",
26
+ "skill-registry:refresh",
27
+ ];
28
+
29
+ function createPi() {
30
+ const hooks = new Map();
31
+ const commands = new Map();
32
+ const flags = new Map();
33
+ const flagValues = new Map([["no-skill-registry", true]]);
34
+
35
+ const pi = {
36
+ on(name, handler) {
37
+ const list = hooks.get(name) ?? [];
38
+ list.push(handler);
39
+ hooks.set(name, list);
40
+ },
41
+ registerCommand(name, definition) {
42
+ commands.set(name, definition);
43
+ },
44
+ registerFlag(name, definition) {
45
+ flags.set(name, definition);
46
+ },
47
+ getFlag(name) {
48
+ return flagValues.get(name) ?? false;
49
+ },
50
+ setFlag(name, value) {
51
+ flagValues.set(name, value);
52
+ },
53
+ getCommands() {
54
+ return Array.from(commands, ([name, definition]) => ({ name, ...definition }));
55
+ },
56
+ getAllTools() {
57
+ return [
58
+ { name: "read" },
59
+ { name: "bash" },
60
+ { name: "edit" },
61
+ { name: "write" },
62
+ ];
63
+ },
64
+ };
65
+
66
+ return { pi, hooks, commands, flags };
67
+ }
68
+
69
+ function createUi() {
70
+ const notifications = [];
71
+ return {
72
+ notifications,
73
+ notify(message, level = "info") {
74
+ notifications.push({ message, level });
75
+ },
76
+ async confirm() {
77
+ return false;
78
+ },
79
+ async select(_label, options) {
80
+ return options[0];
81
+ },
82
+ async input(_label, placeholder) {
83
+ return placeholder;
84
+ },
85
+ custom() {
86
+ return Promise.resolve({ type: "cancel" });
87
+ },
88
+ };
89
+ }
90
+
91
+ function createCtx(cwd, hasUI = false) {
92
+ return {
93
+ cwd,
94
+ hasUI,
95
+ ui: createUi(),
96
+ modelRegistry: {
97
+ async getAvailable() {
98
+ return [];
99
+ },
100
+ },
101
+ };
102
+ }
103
+
104
+ async function tempWorkspace() {
105
+ return mkdtemp(join(tmpdir(), "gentle-pi-runtime-"));
106
+ }
107
+
108
+ async function loadExtensions(pi) {
109
+ for (const [index, rel] of EXTENSIONS.entries()) {
110
+ const mod = await import(`${pathToFileURL(join(ROOT, rel)).href}?runtime-harness=${index}`);
111
+ assert.equal(typeof mod.default, "function", `${rel} must export a default function`);
112
+ mod.default(pi);
113
+ }
114
+ }
115
+
116
+ async function run() {
117
+ const { pi, hooks, commands, flags } = createPi();
118
+ await loadExtensions(pi);
119
+
120
+ for (const name of EXPECTED_COMMANDS) {
121
+ assert.ok(commands.has(name), `missing command ${name}`);
122
+ }
123
+ assert.ok(flags.has("no-skill-registry"), "missing no-skill-registry flag");
124
+ assert.ok(hooks.has("session_start"), "missing session_start hook");
125
+ assert.ok(hooks.has("before_agent_start"), "missing before_agent_start hook");
126
+ assert.ok(hooks.has("tool_call"), "missing tool_call hook");
127
+
128
+ const promptCwd = await tempWorkspace();
129
+ try {
130
+ const promptHook = hooks.get("before_agent_start")[0];
131
+ const promptResult = promptHook({ systemPrompt: "base" }, createCtx(promptCwd));
132
+ assert.match(promptResult.systemPrompt, /base/);
133
+ assert.match(promptResult.systemPrompt, /el Gentleman/);
134
+ } finally {
135
+ await rm(promptCwd, { recursive: true, force: true });
136
+ }
137
+
138
+ const toolCwd = await tempWorkspace();
139
+ try {
140
+ const toolHook = hooks.get("tool_call")[0];
141
+ assert.equal(await toolHook({ toolName: "bash", input: { command: "git status" } }, createCtx(toolCwd)), undefined);
142
+ const denied = await toolHook({ toolName: "bash", input: { command: "rm -rf /" } }, createCtx(toolCwd));
143
+ assert.equal(denied.block, true);
144
+ assert.match(denied.reason, /destructive/);
145
+ const needsConfirm = await toolHook({ toolName: "bash", input: { command: "git push" } }, createCtx(toolCwd));
146
+ assert.equal(needsConfirm.block, true);
147
+ assert.match(needsConfirm.reason, /confirmation/);
148
+ } finally {
149
+ await rm(toolCwd, { recursive: true, force: true });
150
+ }
151
+
152
+ const noUiCwd = await tempWorkspace();
153
+ try {
154
+ for (const handler of hooks.get("session_start")) {
155
+ await handler({ reason: "startup" }, createCtx(noUiCwd, false));
156
+ }
157
+ } finally {
158
+ await rm(noUiCwd, { recursive: true, force: true });
159
+ }
160
+
161
+ const installCwd = await tempWorkspace();
162
+ try {
163
+ const ctx = createCtx(installCwd, true);
164
+ await commands.get("gentle-ai:install-sdd").handler("", ctx);
165
+ assert.match(ctx.ui.notifications.at(-1).message, /SDD assets installed/);
166
+ } finally {
167
+ await rm(installCwd, { recursive: true, force: true });
168
+ }
169
+
170
+ const sddCwd = await tempWorkspace();
171
+ try {
172
+ const ctx = createCtx(sddCwd, true);
173
+ await commands.get("sdd-init").handler("", ctx);
174
+ assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
175
+ } finally {
176
+ await rm(sddCwd, { recursive: true, force: true });
177
+ }
178
+
179
+ const registryCwd = await tempWorkspace();
180
+ try {
181
+ const ctx = createCtx(registryCwd, true);
182
+ await commands.get("skill-registry:refresh").handler("", ctx);
183
+ assert.match(ctx.ui.notifications.at(-1).message, /Skill registry:/);
184
+ } finally {
185
+ await rm(registryCwd, { recursive: true, force: true });
186
+ }
187
+ }
188
+
189
+ run().catch((error) => {
190
+ console.error(error);
191
+ process.exitCode = 1;
192
+ });
@@ -112,3 +112,17 @@ test("uniqueExistingDirs normalizes duplicates and ignores missing roots", () =>
112
112
 
113
113
  assert.deepEqual(__testing.uniqueExistingDirs([existing, join(root, "skills/"), join(root, "missing")]), [existing]);
114
114
  });
115
+
116
+ test("startup skip honors no skill registry controls", () => {
117
+ const enabled = { getFlag: () => true };
118
+ const disabled = { getFlag: () => false };
119
+
120
+ assert.equal(__testing.shouldSkipSkillRegistryStartup(enabled, [], {}), true);
121
+ assert.equal(__testing.shouldSkipSkillRegistryStartup(disabled, ["--no-skills"], {}), true);
122
+ assert.equal(__testing.shouldSkipSkillRegistryStartup(disabled, ["-ns"], {}), true);
123
+ assert.equal(
124
+ __testing.shouldSkipSkillRegistryStartup(disabled, [], { GENTLE_PI_NO_SKILL_REGISTRY: "1" }),
125
+ true,
126
+ );
127
+ assert.equal(__testing.shouldSkipSkillRegistryStartup(disabled, [], {}), false);
128
+ });