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 +9 -0
- package/assets/orchestrator.md +2 -0
- package/extensions/gentle-ai.ts +6 -2
- package/extensions/skill-registry.ts +30 -0
- package/package.json +3 -2
- package/tests/runtime-harness.mjs +192 -0
- package/tests/skill-registry.test.ts +14 -0
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
|
package/assets/orchestrator.md
CHANGED
|
@@ -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.
|
package/extensions/gentle-ai.ts
CHANGED
|
@@ -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
|
-
|
|
479
|
+
modelOptions: string[],
|
|
478
480
|
agents: string[],
|
|
479
|
-
|
|
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.
|
|
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
|
+
});
|