gentle-pi 0.2.8 → 0.3.1
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 +25 -9
- package/assets/orchestrator.md +13 -4
- package/extensions/gentle-ai.ts +269 -82
- package/extensions/sdd-init.ts +8 -0
- package/extensions/skill-registry.ts +120 -77
- package/extensions/startup-banner.ts +231 -102
- package/lib/sdd-preflight.ts +269 -0
- package/package.json +2 -1
- package/scripts/verify-package-files.mjs +1 -0
- package/tests/runtime-harness.mjs +176 -2
- package/tests/skill-registry.test.ts +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"assets/",
|
|
27
27
|
"extensions/",
|
|
28
|
+
"lib/",
|
|
28
29
|
"prompts/",
|
|
29
30
|
"skills/",
|
|
30
31
|
"scripts/",
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
import { dirname, join } from "node:path";
|
|
7
|
+
import { discoverAndLoadExtensions } from "@earendil-works/pi-coding-agent";
|
|
6
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
9
|
|
|
8
10
|
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -15,6 +17,8 @@ const EXTENSIONS = [
|
|
|
15
17
|
|
|
16
18
|
const EXPECTED_COMMANDS = [
|
|
17
19
|
"gentle-ai:install-sdd",
|
|
20
|
+
"gentle-ai:sdd-preflight",
|
|
21
|
+
"gentle:sdd-preflight",
|
|
18
22
|
"gentle:models",
|
|
19
23
|
"gentle-ai:models",
|
|
20
24
|
"gentleman:models",
|
|
@@ -31,6 +35,7 @@ function createPi() {
|
|
|
31
35
|
const commands = new Map();
|
|
32
36
|
const flags = new Map();
|
|
33
37
|
const flagValues = new Map([["no-skill-registry", true]]);
|
|
38
|
+
let activeTools = ["read", "bash", "edit", "write"];
|
|
34
39
|
|
|
35
40
|
const pi = {
|
|
36
41
|
on(name, handler) {
|
|
@@ -53,12 +58,19 @@ function createPi() {
|
|
|
53
58
|
getCommands() {
|
|
54
59
|
return Array.from(commands, ([name, definition]) => ({ name, ...definition }));
|
|
55
60
|
},
|
|
61
|
+
getActiveTools() {
|
|
62
|
+
return activeTools;
|
|
63
|
+
},
|
|
64
|
+
setActiveTools(value) {
|
|
65
|
+
activeTools = value;
|
|
66
|
+
},
|
|
56
67
|
getAllTools() {
|
|
57
68
|
return [
|
|
58
69
|
{ name: "read" },
|
|
59
70
|
{ name: "bash" },
|
|
60
71
|
{ name: "edit" },
|
|
61
72
|
{ name: "write" },
|
|
73
|
+
{ name: "mem_save" },
|
|
62
74
|
];
|
|
63
75
|
},
|
|
64
76
|
};
|
|
@@ -68,15 +80,18 @@ function createPi() {
|
|
|
68
80
|
|
|
69
81
|
function createUi() {
|
|
70
82
|
const notifications = [];
|
|
83
|
+
const selections = [];
|
|
71
84
|
return {
|
|
72
85
|
notifications,
|
|
86
|
+
selections,
|
|
73
87
|
notify(message, level = "info") {
|
|
74
88
|
notifications.push({ message, level });
|
|
75
89
|
},
|
|
76
90
|
async confirm() {
|
|
77
91
|
return false;
|
|
78
92
|
},
|
|
79
|
-
async select(
|
|
93
|
+
async select(label, options) {
|
|
94
|
+
selections.push({ label, options });
|
|
80
95
|
return options[0];
|
|
81
96
|
},
|
|
82
97
|
async input(_label, placeholder) {
|
|
@@ -88,11 +103,19 @@ function createUi() {
|
|
|
88
103
|
};
|
|
89
104
|
}
|
|
90
105
|
|
|
91
|
-
function createCtx(cwd, hasUI = false) {
|
|
106
|
+
function createCtx(cwd, hasUI = false, sessionId = "session-1") {
|
|
92
107
|
return {
|
|
93
108
|
cwd,
|
|
94
109
|
hasUI,
|
|
95
110
|
ui: createUi(),
|
|
111
|
+
sessionManager: {
|
|
112
|
+
getSessionFile() {
|
|
113
|
+
return join(cwd, `${sessionId}.jsonl`);
|
|
114
|
+
},
|
|
115
|
+
getSessionId() {
|
|
116
|
+
return sessionId;
|
|
117
|
+
},
|
|
118
|
+
},
|
|
96
119
|
modelRegistry: {
|
|
97
120
|
async getAvailable() {
|
|
98
121
|
return [];
|
|
@@ -122,9 +145,17 @@ async function run() {
|
|
|
122
145
|
}
|
|
123
146
|
assert.ok(flags.has("no-skill-registry"), "missing no-skill-registry flag");
|
|
124
147
|
assert.ok(hooks.has("session_start"), "missing session_start hook");
|
|
148
|
+
assert.ok(hooks.has("input"), "missing input hook");
|
|
125
149
|
assert.ok(hooks.has("before_agent_start"), "missing before_agent_start hook");
|
|
126
150
|
assert.ok(hooks.has("tool_call"), "missing tool_call hook");
|
|
127
151
|
|
|
152
|
+
const discovered = await discoverAndLoadExtensions(["./extensions"], ROOT);
|
|
153
|
+
assert.deepEqual(
|
|
154
|
+
discovered.errors,
|
|
155
|
+
[],
|
|
156
|
+
"declared extension directory must load without invalid helper modules",
|
|
157
|
+
);
|
|
158
|
+
|
|
128
159
|
const promptCwd = await tempWorkspace();
|
|
129
160
|
try {
|
|
130
161
|
const promptHook = hooks.get("before_agent_start")[0];
|
|
@@ -154,10 +185,146 @@ async function run() {
|
|
|
154
185
|
for (const handler of hooks.get("session_start")) {
|
|
155
186
|
await handler({ reason: "startup" }, createCtx(noUiCwd, false));
|
|
156
187
|
}
|
|
188
|
+
assert.equal(
|
|
189
|
+
existsSync(join(noUiCwd, ".pi", "agents", "sdd-apply.md")),
|
|
190
|
+
false,
|
|
191
|
+
"session_start must not install SDD agents before first SDD intent",
|
|
192
|
+
);
|
|
193
|
+
assert.equal(
|
|
194
|
+
existsSync(join(noUiCwd, ".pi", "chains", "sdd-full.chain.md")),
|
|
195
|
+
false,
|
|
196
|
+
"session_start must not install SDD chains before first SDD intent",
|
|
197
|
+
);
|
|
157
198
|
} finally {
|
|
158
199
|
await rm(noUiCwd, { recursive: true, force: true });
|
|
159
200
|
}
|
|
160
201
|
|
|
202
|
+
const lazySddCwd = await tempWorkspace();
|
|
203
|
+
try {
|
|
204
|
+
await mkdir(join(lazySddCwd, ".pi", "gentle-ai"), { recursive: true });
|
|
205
|
+
await writeFile(
|
|
206
|
+
join(lazySddCwd, ".pi", "gentle-ai", "models.json"),
|
|
207
|
+
JSON.stringify({ "sdd-apply": { model: "openai/gpt-5", thinking: "high" } }, null, 2),
|
|
208
|
+
);
|
|
209
|
+
const ctx = createCtx(lazySddCwd, true);
|
|
210
|
+
const inputHook = hooks.get("input")[0];
|
|
211
|
+
assert.deepEqual(
|
|
212
|
+
await inputHook({ text: "hola, solo mirando", source: "interactive" }, ctx),
|
|
213
|
+
{ action: "continue" },
|
|
214
|
+
);
|
|
215
|
+
assert.deepEqual(
|
|
216
|
+
await inputHook({ text: "what is SDD?", source: "interactive" }, ctx),
|
|
217
|
+
{ action: "continue" },
|
|
218
|
+
);
|
|
219
|
+
assert.deepEqual(
|
|
220
|
+
await inputHook({ text: "what can I do with SDD?", source: "interactive" }, ctx),
|
|
221
|
+
{ action: "continue" },
|
|
222
|
+
);
|
|
223
|
+
assert.deepEqual(
|
|
224
|
+
await inputHook({ text: "how do I use SDD?", source: "interactive" }, ctx),
|
|
225
|
+
{ action: "continue" },
|
|
226
|
+
);
|
|
227
|
+
assert.deepEqual(
|
|
228
|
+
await inputHook({ text: "Can I use SDD?", source: "interactive" }, ctx),
|
|
229
|
+
{ action: "continue" },
|
|
230
|
+
);
|
|
231
|
+
assert.deepEqual(
|
|
232
|
+
await inputHook({ text: "don't use sdd for this", source: "interactive" }, ctx),
|
|
233
|
+
{ action: "continue" },
|
|
234
|
+
);
|
|
235
|
+
assert.deepEqual(
|
|
236
|
+
await inputHook({ text: "sin usar SDD por ahora", source: "interactive" }, ctx),
|
|
237
|
+
{ action: "continue" },
|
|
238
|
+
);
|
|
239
|
+
assert.deepEqual(
|
|
240
|
+
await inputHook({ text: "let's not use SDD for this", source: "interactive" }, ctx),
|
|
241
|
+
{ action: "continue" },
|
|
242
|
+
);
|
|
243
|
+
assert.deepEqual(
|
|
244
|
+
await inputHook({ text: "never use SDD here", source: "interactive" }, ctx),
|
|
245
|
+
{ action: "continue" },
|
|
246
|
+
);
|
|
247
|
+
assert.deepEqual(
|
|
248
|
+
await inputHook({ text: "no quiero usar SDD por ahora", source: "interactive" }, ctx),
|
|
249
|
+
{ action: "continue" },
|
|
250
|
+
);
|
|
251
|
+
assert.deepEqual(
|
|
252
|
+
await inputHook({ text: "I use SDD sometimes", source: "interactive" }, ctx),
|
|
253
|
+
{ action: "continue" },
|
|
254
|
+
);
|
|
255
|
+
assert.deepEqual(
|
|
256
|
+
await inputHook({ text: "I'm using SDD in another repo", source: "interactive" }, ctx),
|
|
257
|
+
{ action: "continue" },
|
|
258
|
+
);
|
|
259
|
+
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
260
|
+
|
|
261
|
+
assert.deepEqual(
|
|
262
|
+
await inputHook({ text: "please use sdd for this change", source: "interactive" }, ctx),
|
|
263
|
+
{ action: "continue" },
|
|
264
|
+
);
|
|
265
|
+
assert.deepEqual(
|
|
266
|
+
await inputHook({ text: "/sdd", source: "interactive" }, ctx),
|
|
267
|
+
{ action: "continue" },
|
|
268
|
+
);
|
|
269
|
+
assert.deepEqual(
|
|
270
|
+
await inputHook({ text: "/sdd plan", source: "interactive" }, ctx),
|
|
271
|
+
{ action: "continue" },
|
|
272
|
+
);
|
|
273
|
+
assert.deepEqual(
|
|
274
|
+
await inputHook({ text: "/sdd:plan", source: "interactive" }, ctx),
|
|
275
|
+
{ action: "continue" },
|
|
276
|
+
);
|
|
277
|
+
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
278
|
+
|
|
279
|
+
assert.deepEqual(
|
|
280
|
+
await inputHook({ text: "/sdd-plan this change", source: "interactive" }, ctx),
|
|
281
|
+
{ action: "continue" },
|
|
282
|
+
);
|
|
283
|
+
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), true);
|
|
284
|
+
assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), true);
|
|
285
|
+
const lazyAppliedAgent = await readFile(
|
|
286
|
+
join(lazySddCwd, ".pi", "agents", "sdd-apply.md"),
|
|
287
|
+
"utf8",
|
|
288
|
+
);
|
|
289
|
+
assert.match(lazyAppliedAgent, /model: openai\/gpt-5/);
|
|
290
|
+
assert.match(lazyAppliedAgent, /thinking: high/);
|
|
291
|
+
assert.equal(ctx.ui.selections.length, 3);
|
|
292
|
+
assert.deepEqual(ctx.ui.selections[1].options, ["openspec"]);
|
|
293
|
+
assert.match(ctx.ui.notifications.at(-1).message, /SDD preflight complete/);
|
|
294
|
+
|
|
295
|
+
await inputHook({ text: "/sdd-plan another change", source: "interactive" }, ctx);
|
|
296
|
+
assert.equal(ctx.ui.selections.length, 3, "preflight should run only once per session");
|
|
297
|
+
const promptHook = hooks.get("before_agent_start")[0];
|
|
298
|
+
const promptResult = promptHook({ systemPrompt: "base" }, ctx);
|
|
299
|
+
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
300
|
+
assert.match(promptResult.systemPrompt, /Execution mode: interactive/);
|
|
301
|
+
} finally {
|
|
302
|
+
await rm(lazySddCwd, { recursive: true, force: true });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const commandSddCwd = await tempWorkspace();
|
|
306
|
+
try {
|
|
307
|
+
const ctx = createCtx(commandSddCwd, true, "command-session");
|
|
308
|
+
await commands.get("gentle-ai:sdd-preflight").handler("", ctx);
|
|
309
|
+
assert.equal(existsSync(join(commandSddCwd, ".pi", "agents", "sdd-apply.md")), true);
|
|
310
|
+
assert.equal(ctx.ui.selections.length, 3);
|
|
311
|
+
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
312
|
+
assert.equal(ctx.ui.selections.length, 3, "manual preflight command should reuse session choices");
|
|
313
|
+
} finally {
|
|
314
|
+
await rm(commandSddCwd, { recursive: true, force: true });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const engramSddCwd = await tempWorkspace();
|
|
318
|
+
try {
|
|
319
|
+
pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
|
|
320
|
+
const ctx = createCtx(engramSddCwd, true, "engram-session");
|
|
321
|
+
await commands.get("gentle-ai:sdd-preflight").handler("", ctx);
|
|
322
|
+
assert.deepEqual(ctx.ui.selections[1].options, ["openspec", "engram", "both"]);
|
|
323
|
+
} finally {
|
|
324
|
+
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
325
|
+
await rm(engramSddCwd, { recursive: true, force: true });
|
|
326
|
+
}
|
|
327
|
+
|
|
161
328
|
const installCwd = await tempWorkspace();
|
|
162
329
|
try {
|
|
163
330
|
const ctx = createCtx(installCwd, true);
|
|
@@ -171,7 +338,14 @@ async function run() {
|
|
|
171
338
|
try {
|
|
172
339
|
const ctx = createCtx(sddCwd, true);
|
|
173
340
|
await commands.get("sdd-init").handler("", ctx);
|
|
341
|
+
assert.equal(existsSync(join(sddCwd, ".pi", "agents", "sdd-apply.md")), true);
|
|
342
|
+
assert.equal(existsSync(join(sddCwd, ".pi", "chains", "sdd-full.chain.md")), true);
|
|
343
|
+
assert.equal(ctx.ui.selections.length, 3);
|
|
344
|
+
assert.match(ctx.ui.notifications[0].message, /SDD preflight complete/);
|
|
174
345
|
assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
|
|
346
|
+
|
|
347
|
+
await commands.get("gentle-ai:sdd-preflight").handler("", ctx);
|
|
348
|
+
assert.equal(ctx.ui.selections.length, 3, "/sdd-init preflight should be reused by later manual preflight");
|
|
175
349
|
} finally {
|
|
176
350
|
await rm(sddCwd, { recursive: true, force: true });
|
|
177
351
|
}
|
|
@@ -105,12 +105,15 @@ test("project-scoped duplicate wins over user duplicate", () => {
|
|
|
105
105
|
assert.equal(chosen.path, projectPath);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
test("uniqueExistingDirs normalizes duplicates and ignores missing roots", () => {
|
|
108
|
+
test("uniqueExistingDirs normalizes duplicates and ignores missing roots", async () => {
|
|
109
109
|
const root = join(tmpdir(), `gentle-pi-existing-${Date.now()}`);
|
|
110
110
|
const existing = join(root, "skills");
|
|
111
111
|
mkdirSync(existing, { recursive: true });
|
|
112
112
|
|
|
113
|
-
assert.deepEqual(
|
|
113
|
+
assert.deepEqual(
|
|
114
|
+
await __testing.uniqueExistingDirs([existing, join(root, "skills/"), join(root, "missing")]),
|
|
115
|
+
[existing],
|
|
116
|
+
);
|
|
114
117
|
});
|
|
115
118
|
|
|
116
119
|
test("startup skip honors no skill registry controls", () => {
|