gentle-pi 0.3.5 → 0.3.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/assets/orchestrator.md +13 -6
- package/lib/sdd-preflight.ts +25 -2
- package/package.json +1 -1
- package/tests/artifact-language.test.ts +91 -0
- package/tests/runtime-harness.mjs +67 -1
package/assets/orchestrator.md
CHANGED
|
@@ -6,16 +6,16 @@ Bind this to the parent Pi session only. Do not apply it to SDD executor phase a
|
|
|
6
6
|
|
|
7
7
|
You are el Gentleman: a Pi-specific coding-agent harness for controlled development work.
|
|
8
8
|
|
|
9
|
-
When the user asks who or what you are, answer
|
|
9
|
+
When the user asks who or what you are, answer with this meaning, translated into the user's language:
|
|
10
10
|
|
|
11
11
|
```text
|
|
12
|
-
|
|
12
|
+
I am el Gentleman: a Pi-specific coding-agent harness for controlled development, with a senior architect persona. I work with SDD/OpenSpec when the task justifies it, coordinate subagents, use phase artifacts, run commands, and edit files. I am not a generic chatbot.
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Rules:
|
|
16
16
|
|
|
17
17
|
- Never introduce yourself as only "your assistant" or "the default assistant".
|
|
18
|
-
- Keep the response in the user's language
|
|
18
|
+
- Keep the response in the user's language and follow the currently selected persona mode.
|
|
19
19
|
- Mention persistent memory only when a memory package or callable memory tools are actually active.
|
|
20
20
|
- Do not claim portability outside the Pi runtime.
|
|
21
21
|
|
|
@@ -45,7 +45,7 @@ el Gentleman is an ecosystem configurator and harness layer. After installation,
|
|
|
45
45
|
|
|
46
46
|
- Small request: do it directly.
|
|
47
47
|
- Substantial feature: suggest SDD organically.
|
|
48
|
-
- User
|
|
48
|
+
- User explicitly asks to use SDD: run the SDD flow.
|
|
49
49
|
- Parent session orchestrates; phase agents execute.
|
|
50
50
|
|
|
51
51
|
Delegation is not optional once complexity appears. If a task crosses the triggers below, use the smallest useful subagent workflow instead of continuing as a monolithic executor.
|
|
@@ -101,7 +101,7 @@ Triggers:
|
|
|
101
101
|
- cross-cutting behavior changes;
|
|
102
102
|
- expected large diff or reviewer burden;
|
|
103
103
|
- need for specs/design/tasks before safe implementation;
|
|
104
|
-
- user explicitly
|
|
104
|
+
- user explicitly asks to use SDD, or invokes `/sdd-new`, `/sdd-ff`, or `/sdd-continue`.
|
|
105
105
|
|
|
106
106
|
If the request is large enough for SDD, do not jump directly to implementation. Calibrate context, create artifacts, and ask for approval at the appropriate gates.
|
|
107
107
|
|
|
@@ -181,6 +181,13 @@ proposal → design ┘
|
|
|
181
181
|
|
|
182
182
|
Do not ask SDD setup questions on session start. The first time the user initiates an SDD process in a Pi session, run the SDD preflight once and keep those choices for the rest of that session. Runtime trigger detection is intentionally deterministic: slash SDD flows and `/sdd-init` run preflight automatically; for natural-language requests, the parent/orchestrator decides semantically whether SDD is needed and must run/reuse `/gentle-ai:sdd-preflight` before continuing.
|
|
183
183
|
|
|
184
|
+
**Hard gate:** `openspec/config.yaml`, existing SDD changes, installed `.pi`/global SDD assets, or a todo named "preflight" are not session preflight. They are project context only. Do not mark SDD preflight complete, start `sdd-init`, launch SDD subagents/chains, or move to explore/proposal/spec/design/tasks until this session has either:
|
|
185
|
+
|
|
186
|
+
1. an injected `## SDD Session Preflight` block, or
|
|
187
|
+
2. an explicit user answer in the current conversation covering all four preflight choices below.
|
|
188
|
+
|
|
189
|
+
If neither exists and `/gentle-ai:sdd-preflight` cannot be invoked from the current context, ask the four choices manually with `ask_user_question` before any SDD phase work. Treat missing Engram availability as a reason to ask/confirm artifact store, not as permission to assume defaults.
|
|
190
|
+
|
|
184
191
|
The preflight captures:
|
|
185
192
|
|
|
186
193
|
- execution mode: `interactive` or `auto`;
|
|
@@ -207,7 +214,7 @@ In this Pi package, the default local artifact is:
|
|
|
207
214
|
openspec/config.yaml
|
|
208
215
|
```
|
|
209
216
|
|
|
210
|
-
If it is missing, ask the user for the minimal information needed or run `/sdd-init` if available. Do not proceed with a substantial SDD flow while pretending project context
|
|
217
|
+
If it is missing, ask the user for the minimal information needed or run `/sdd-init` if available. This init guard runs after the session preflight gate above; project config presence or absence never substitutes for session preflight choices. Do not proceed with a substantial SDD flow while pretending project context, testing capability, or session preflight choices are known.
|
|
211
218
|
|
|
212
219
|
## Artifact Store Policy
|
|
213
220
|
|
package/lib/sdd-preflight.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface SddPreflightPreferences {
|
|
|
25
25
|
chainedPrStrategy: SddChainedPrStrategy;
|
|
26
26
|
reviewBudgetLines: number;
|
|
27
27
|
engramAvailable: boolean;
|
|
28
|
+
prompted: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
interface SddPreflightCallbacks {
|
|
@@ -55,6 +56,7 @@ const DEFAULT_SDD_PREFLIGHT: SddPreflightPreferences = {
|
|
|
55
56
|
chainedPrStrategy: "auto-forecast",
|
|
56
57
|
reviewBudgetLines: 400,
|
|
57
58
|
engramAvailable: false,
|
|
59
|
+
prompted: false,
|
|
58
60
|
};
|
|
59
61
|
|
|
60
62
|
const sddPreflightBySession = new Map<string, SddPreflightPreferences>();
|
|
@@ -122,7 +124,23 @@ export function installSddAssets(
|
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
export function isSddPreflightTrigger(text: string): boolean {
|
|
125
|
-
|
|
127
|
+
const trimmed = text.trim();
|
|
128
|
+
if (/^\/sdd(?:[-:][^\s]*)?(?:\s|$)/i.test(trimmed)) return true;
|
|
129
|
+
if (/[??]\s*$/.test(trimmed)) return false;
|
|
130
|
+
if (
|
|
131
|
+
/\b(?:don't|do\s+not|not\s+use|never\s+use|without\s+using|sin\s+usar|no\s+(?:quiero|queremos|vamos\s+a)?\s*usar)\s+sdd\b/i.test(
|
|
132
|
+
trimmed,
|
|
133
|
+
)
|
|
134
|
+
) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return [
|
|
138
|
+
/^(?:please\s+)?(?:use|run|start)\s+(?:the\s+|an?\s+)?sdd(?:\s+(?:flow|process|workflow|plan))?\b/i,
|
|
139
|
+
/^(?:please\s+)?(?:do|handle|implement)\b.+\b(?:with|using)\s+(?:the\s+|an?\s+)?sdd\b/i,
|
|
140
|
+
/^(?:por\s+favor[\s,]+)?(?:vamos|vayamos)\s+con\s+(?:el\s+)?sdd\b/i,
|
|
141
|
+
/^(?:por\s+favor[\s,]+)?(?:usa|usá|usemos|corre|corré|arranca|arrancá|inicia|iniciá|empeza|empezá)\s+(?:el\s+)?sdd\b/i,
|
|
142
|
+
/^(?:por\s+favor[\s,]+)?(?:hacelo|hazlo|hacerlo)\s+(?:con|usando)\s+(?:el\s+)?sdd\b/i,
|
|
143
|
+
].some((pattern) => pattern.test(trimmed));
|
|
126
144
|
}
|
|
127
145
|
|
|
128
146
|
export function sddPreflightSessionKey(ctx: ExtensionContext): string {
|
|
@@ -209,13 +227,17 @@ async function collectSddPreflightPreferences(
|
|
|
209
227
|
: DEFAULT_SDD_PREFLIGHT.chainedPrStrategy,
|
|
210
228
|
reviewBudgetLines,
|
|
211
229
|
engramAvailable,
|
|
230
|
+
prompted: true,
|
|
212
231
|
};
|
|
213
232
|
}
|
|
214
233
|
|
|
215
234
|
export function renderSddPreflightPrompt(prefs: SddPreflightPreferences): string {
|
|
235
|
+
const sourceLine = prefs.prompted
|
|
236
|
+
? "The user already chose these SDD preferences for this Pi session. Reuse them unless the user explicitly changes them."
|
|
237
|
+
: "No interactive UI was available for SDD preflight, so these default preferences were applied for this Pi session. Ask the user before making delivery decisions that depend on them.";
|
|
216
238
|
return [
|
|
217
239
|
"## SDD Session Preflight",
|
|
218
|
-
|
|
240
|
+
sourceLine,
|
|
219
241
|
`- Execution mode: ${prefs.executionMode}`,
|
|
220
242
|
`- Artifact store: ${prefs.artifactStore}${prefs.engramAvailable ? "" : " (Engram unavailable in this session)"}`,
|
|
221
243
|
`- Chained PR strategy: ${prefs.chainedPrStrategy}`,
|
|
@@ -254,6 +276,7 @@ export async function ensureSddPreflight(
|
|
|
254
276
|
`Artifacts: ${prefs.artifactStore}`,
|
|
255
277
|
`PR chaining: ${prefs.chainedPrStrategy}`,
|
|
256
278
|
`Review budget: ${prefs.reviewBudgetLines} changed lines`,
|
|
279
|
+
`Preference source: ${prefs.prompted ? "user prompt" : "defaults (no interactive UI available)"}`,
|
|
257
280
|
`Global SDD assets ready: ${result.agents} agent(s), ${result.chains} chain(s), ${result.support} support file(s), ${result.skipped} already present.`,
|
|
258
281
|
modelRoutingLine,
|
|
259
282
|
].join("\n"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, extname, join, relative } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
renderSddPreflightPrompt,
|
|
8
|
+
type SddPreflightPreferences,
|
|
9
|
+
} from "../lib/sdd-preflight.ts";
|
|
10
|
+
|
|
11
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
12
|
+
const TEXT_EXTENSIONS = new Set([".md", ".ts", ".mjs", ".json"]);
|
|
13
|
+
|
|
14
|
+
async function collectTextFiles(dir: string): Promise<string[]> {
|
|
15
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
16
|
+
const files: string[] = [];
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const path = join(dir, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
files.push(...(await collectTextFiles(path)));
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (entry.isFile() && TEXT_EXTENSIONS.has(extname(entry.name))) {
|
|
24
|
+
files.push(path);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return files;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SPANISH_PREFLIGHT_COPY = [
|
|
31
|
+
/Antes de continuar con SDD/i,
|
|
32
|
+
/Antes de seguir con SDD/i,
|
|
33
|
+
/una opci[oó]n por grupo/i,
|
|
34
|
+
/usar recomendad[oa]/i,
|
|
35
|
+
/\bRitmo\b/i,
|
|
36
|
+
/\bArtefactos\b/i,
|
|
37
|
+
/\bPreguntarme\b/i,
|
|
38
|
+
/l[ií]neas cambiadas/i,
|
|
39
|
+
/\bhacelo\b/i,
|
|
40
|
+
/\bSoy el Gentleman\b/i,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
test("orchestrator keeps conversation language separate from generated artifact language", async () => {
|
|
44
|
+
const orchestrator = await readFile(join(ROOT, "assets/orchestrator.md"), "utf8");
|
|
45
|
+
|
|
46
|
+
assert.match(
|
|
47
|
+
orchestrator,
|
|
48
|
+
/User-facing conversation should stay in the user's language/,
|
|
49
|
+
);
|
|
50
|
+
assert.match(
|
|
51
|
+
orchestrator,
|
|
52
|
+
/Generated artifacts[\s\S]*default to English, regardless of the user's conversation language/,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("rendered SDD preflight prompt is English artifact copy", () => {
|
|
57
|
+
const prefs: SddPreflightPreferences = {
|
|
58
|
+
executionMode: "interactive",
|
|
59
|
+
artifactStore: "openspec",
|
|
60
|
+
chainedPrStrategy: "ask-always",
|
|
61
|
+
reviewBudgetLines: 400,
|
|
62
|
+
engramAvailable: false,
|
|
63
|
+
prompted: true,
|
|
64
|
+
};
|
|
65
|
+
const prompt = renderSddPreflightPrompt(prefs);
|
|
66
|
+
|
|
67
|
+
assert.match(prompt, /The user already chose these SDD preferences/);
|
|
68
|
+
assert.match(prompt, /Review budget: 400 changed lines/);
|
|
69
|
+
for (const pattern of SPANISH_PREFLIGHT_COPY) {
|
|
70
|
+
assert.doesNotMatch(prompt, pattern);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("persistent harness prompt assets do not hardcode Spanish SDD artifact copy", async () => {
|
|
75
|
+
const files = [
|
|
76
|
+
...(await collectTextFiles(join(ROOT, "assets"))),
|
|
77
|
+
...(await collectTextFiles(join(ROOT, "prompts"))),
|
|
78
|
+
];
|
|
79
|
+
const failures: string[] = [];
|
|
80
|
+
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
const text = await readFile(file, "utf8");
|
|
83
|
+
for (const pattern of SPANISH_PREFLIGHT_COPY) {
|
|
84
|
+
if (pattern.test(text)) {
|
|
85
|
+
failures.push(`${relative(ROOT, file)} matched ${pattern}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
assert.deepEqual(failures, []);
|
|
91
|
+
});
|
|
@@ -177,6 +177,20 @@ async function run() {
|
|
|
177
177
|
const promptResult = await promptHook({ systemPrompt: "base" }, createCtx(promptCwd));
|
|
178
178
|
assert.match(promptResult.systemPrompt, /base/);
|
|
179
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
|
+
await mkdir(join(promptCwd, ".pi", "gentle-ai"), { recursive: true });
|
|
183
|
+
await writeFile(
|
|
184
|
+
join(promptCwd, ".pi", "gentle-ai", "persona.json"),
|
|
185
|
+
'{"mode":"neutral"}\n',
|
|
186
|
+
);
|
|
187
|
+
const neutralPromptResult = await promptHook({ systemPrompt: "base" }, createCtx(promptCwd));
|
|
188
|
+
assert.match(neutralPromptResult.systemPrompt, /Do not use slang or regional expressions/);
|
|
189
|
+
assert.doesNotMatch(
|
|
190
|
+
neutralPromptResult.systemPrompt,
|
|
191
|
+
/When the user writes Spanish, answer in natural Rioplatense Spanish with voseo/,
|
|
192
|
+
"neutral persona prompt must not include unconditional voseo instructions after reload",
|
|
193
|
+
);
|
|
180
194
|
const subagentPromptResult = await promptHook(
|
|
181
195
|
{ agentName: "worker", systemPrompt: "worker base" },
|
|
182
196
|
createCtx(promptCwd),
|
|
@@ -284,10 +298,26 @@ async function run() {
|
|
|
284
298
|
);
|
|
285
299
|
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
286
300
|
|
|
301
|
+
assert.deepEqual(
|
|
302
|
+
await inputHook({ text: "vamos con sdd", source: "interactive" }, ctx),
|
|
303
|
+
{ action: "continue" },
|
|
304
|
+
);
|
|
305
|
+
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
306
|
+
assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
307
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
308
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
309
|
+
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
310
|
+
assert.equal(ctx.ui.selections.length, 3);
|
|
311
|
+
assert.equal(ctx.ui.selections[0].label, "SDD execution mode");
|
|
312
|
+
assert.equal(ctx.ui.selections[1].label, "SDD artifact store");
|
|
313
|
+
assert.deepEqual(ctx.ui.selections[1].options, ["openspec"]);
|
|
314
|
+
assert.equal(ctx.ui.selections[2].label, "SDD PR chaining");
|
|
315
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Preference source: user prompt/);
|
|
287
316
|
assert.deepEqual(
|
|
288
317
|
await inputHook({ text: "please use sdd for this change", source: "interactive" }, ctx),
|
|
289
318
|
{ action: "continue" },
|
|
290
319
|
);
|
|
320
|
+
assert.equal(ctx.ui.selections.length, 3, "natural SDD trigger should reuse session choices");
|
|
291
321
|
assert.deepEqual(
|
|
292
322
|
await inputHook({ text: "/sdd", source: "interactive" }, ctx),
|
|
293
323
|
{ action: "continue" },
|
|
@@ -300,7 +330,7 @@ async function run() {
|
|
|
300
330
|
await inputHook({ text: "/sdd:plan", source: "interactive" }, ctx),
|
|
301
331
|
{ action: "continue" },
|
|
302
332
|
);
|
|
303
|
-
assert.equal(
|
|
333
|
+
assert.equal(ctx.ui.selections.length, 3);
|
|
304
334
|
|
|
305
335
|
assert.deepEqual(
|
|
306
336
|
await inputHook({ text: "/sdd-plan this change", source: "interactive" }, ctx),
|
|
@@ -338,6 +368,22 @@ async function run() {
|
|
|
338
368
|
await rm(globalModelsPath, { force: true });
|
|
339
369
|
}
|
|
340
370
|
|
|
371
|
+
for (const [index, text] of ["/sdd", "/sdd plan", "/sdd:plan", "/sdd-plan this change"].entries()) {
|
|
372
|
+
const slashSddCwd = await tempWorkspace();
|
|
373
|
+
try {
|
|
374
|
+
const ctx = createCtx(slashSddCwd, true, `slash-sdd-session-${index}`);
|
|
375
|
+
const inputHook = hooks.get("input")[0];
|
|
376
|
+
assert.deepEqual(await inputHook({ text, source: "interactive" }, ctx), {
|
|
377
|
+
action: "continue",
|
|
378
|
+
});
|
|
379
|
+
assert.equal(existsSync(join(slashSddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
380
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
381
|
+
assert.equal(ctx.ui.selections.length, 3, `${text} should run canonical preflight`);
|
|
382
|
+
} finally {
|
|
383
|
+
await rm(slashSddCwd, { recursive: true, force: true });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
341
387
|
const commandSddCwd = await tempWorkspace();
|
|
342
388
|
try {
|
|
343
389
|
const ctx = createCtx(commandSddCwd, true, "command-session");
|
|
@@ -396,6 +442,26 @@ async function run() {
|
|
|
396
442
|
await rm(sddAgentGuardCwd, { recursive: true, force: true });
|
|
397
443
|
}
|
|
398
444
|
|
|
445
|
+
const noUiSddAgentCwd = await tempWorkspace();
|
|
446
|
+
try {
|
|
447
|
+
const ctx = createCtx(noUiSddAgentCwd, false, "no-ui-sdd-agent-session");
|
|
448
|
+
const promptHook = hooks.get("before_agent_start")[0];
|
|
449
|
+
const promptResult = await promptHook(
|
|
450
|
+
{
|
|
451
|
+
agentName: "sdd-proposal",
|
|
452
|
+
systemPrompt: "You are the SDD proposal executor for Gentle AI.",
|
|
453
|
+
},
|
|
454
|
+
ctx,
|
|
455
|
+
);
|
|
456
|
+
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
457
|
+
assert.match(promptResult.systemPrompt, /No interactive UI was available/);
|
|
458
|
+
assert.equal(ctx.ui.selections.length, 0);
|
|
459
|
+
assert.equal(existsSync(join(noUiSddAgentCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
460
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
461
|
+
} finally {
|
|
462
|
+
await rm(noUiSddAgentCwd, { recursive: true, force: true });
|
|
463
|
+
}
|
|
464
|
+
|
|
399
465
|
const invalidPreflightCwd = await tempWorkspace();
|
|
400
466
|
try {
|
|
401
467
|
await writeFile(globalModelsPath, "{ invalid json");
|