gentle-pi 0.1.4 → 0.1.13
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 +192 -32
- package/assets/gentle-logo-only.png +0 -0
- package/extensions/gentle-ai.ts +576 -21
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,66 +1,226 @@
|
|
|
1
1
|
# el Gentleman
|
|
2
2
|
|
|
3
|
-
`gentle-pi`
|
|
3
|
+
`gentle-pi` installs **el Gentleman** into Pi: a controlled-development harness with a senior architect persona, SDD/OpenSpec phase discipline, strict TDD evidence, subagent orchestration, and review-workload protection.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Install it when you want Pi to stop behaving like a generic chatbot and start acting like a disciplined development harness.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Quick start
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
pi install npm:gentle-pi
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Recommended companion packages:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
pi install
|
|
16
|
+
pi install npm:pi-subagents
|
|
17
|
+
pi install npm:pi-intercom
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then start Pi in your project:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pi
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
On session start, `gentle-pi` automatically installs SDD assets into the project without overwriting local edits.
|
|
27
|
+
|
|
28
|
+
## What you get
|
|
29
|
+
|
|
30
|
+
| Capability | What it does |
|
|
31
|
+
|---|---|
|
|
32
|
+
| el Gentleman identity | Answers as a Pi-specific harness, not a generic assistant. Defaults to the `gentleman` persona and can switch to `neutral`. |
|
|
33
|
+
| SDD/OpenSpec routing | Small work stays inline, context-heavy work delegates, large/risky work uses SDD artifacts. |
|
|
34
|
+
| SDD phase agents | Installs `sdd-init`, `sdd-explore`, `sdd-proposal`, `sdd-spec`, `sdd-design`, `sdd-tasks`, `sdd-apply`, `sdd-verify`, and `sdd-archive`. |
|
|
35
|
+
| Strict TDD support | Preserves RED → GREEN → TRIANGULATE → REFACTOR evidence and verify-time compliance checks. |
|
|
36
|
+
| Review workload guard | Forecasts large diffs and recommends chained PRs or explicit `size:exception`. |
|
|
37
|
+
| Model assignment UI | Opens a modal to assign Pi models to project, user, and built-in agents, with SDD agents shown first. |
|
|
38
|
+
| Foundation skills | Adds PR, issue, chained-PR, comment, docs, work-unit, and Judgment Day skills. |
|
|
39
|
+
| Safety policy | Blocks destructive shell actions unless there is explicit user approval. |
|
|
40
|
+
|
|
41
|
+
## Core commands
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
/gentle-ai:status Show package, SDD asset, OpenSpec, and model config status.
|
|
45
|
+
/gentleman:models Open the per-agent model assignment modal.
|
|
46
|
+
/gentle-ai:models Compatibility alias that points to /gentleman:models.
|
|
47
|
+
/gentleman:persona Switch between gentleman and neutral personas.
|
|
48
|
+
/gentle-ai:persona Compatibility alias that points to /gentleman:persona.
|
|
49
|
+
/sdd-init Bootstrap or refresh openspec/config.yaml.
|
|
50
|
+
/gentle-ai:install-sdd Reinstall SDD assets without overwriting local files.
|
|
51
|
+
/gentle-ai:install-sdd --force
|
|
52
|
+
Force-refresh installed SDD assets.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Switch persona
|
|
56
|
+
|
|
57
|
+
Default persona: `gentleman`.
|
|
58
|
+
|
|
59
|
+
Run:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
/gentleman:persona
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Available modes:
|
|
66
|
+
|
|
67
|
+
| Persona | Behavior |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `gentleman` | Teaching-oriented senior architect persona with Rioplatense Spanish/voseo when the user writes Spanish. |
|
|
70
|
+
| `neutral` | Same senior architect discipline and teaching philosophy, but with warm professional language and no regional expressions. |
|
|
71
|
+
|
|
72
|
+
Saved config:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
.pi/gentle-ai/persona.json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Run `/reload` or start a new Pi session after switching if the current session already injected the previous persona.
|
|
79
|
+
|
|
80
|
+
## Assign models to agents
|
|
81
|
+
|
|
82
|
+
Run:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
/gentleman:models
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This opens a modal similar to Gentle-AI's model picker. It discovers Pi subagents from project, user, and built-in sources, with SDD agents sorted first:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
Assign Models to Agents
|
|
92
|
+
|
|
93
|
+
Current assignments:
|
|
94
|
+
|
|
95
|
+
▸ Set all agents mixed
|
|
96
|
+
sdd-init inherit
|
|
97
|
+
sdd-explore openai-codex/gpt-5.5
|
|
98
|
+
sdd-proposal openai-codex/gpt-5.5
|
|
99
|
+
sdd-spec anthropic/claude-sonnet-4
|
|
100
|
+
sdd-design anthropic/claude-sonnet-4
|
|
101
|
+
sdd-tasks anthropic/claude-sonnet-4
|
|
102
|
+
sdd-apply anthropic/claude-sonnet-4
|
|
103
|
+
sdd-verify google/gemini-3-pro
|
|
104
|
+
sdd-archive inherit
|
|
105
|
+
delegate inherit
|
|
106
|
+
my-custom-agent anthropic/claude-sonnet-4
|
|
107
|
+
|
|
108
|
+
Continue
|
|
109
|
+
← Back
|
|
110
|
+
|
|
111
|
+
j/k: navigate • enter: change model / confirm • i: inherit • c: custom • ctrl+s: save • esc: back
|
|
17
112
|
```
|
|
18
113
|
|
|
19
|
-
|
|
114
|
+
Model choices come from Pi itself via the active model registry. The modal also supports custom model IDs for advanced setups.
|
|
115
|
+
|
|
116
|
+
The modal covers:
|
|
117
|
+
|
|
118
|
+
- project agents from `.pi/agents/` and `.agents/`;
|
|
119
|
+
- user agents from `~/.pi/agent/agents/` and `~/.agents/`;
|
|
120
|
+
- built-in `pi-subagents` agents.
|
|
20
121
|
|
|
21
|
-
|
|
22
|
-
- `extensions/sdd-init.ts` — registers `/sdd-init` to bootstrap `openspec/config.yaml`.
|
|
23
|
-
- `extensions/skill-registry.ts` — registers `/skill-registry:refresh` and maintains `.atl/skill-registry.md` from compact skill rules.
|
|
24
|
-
- `skills/gentle-ai` — compact rules for Gentle AI orchestration.
|
|
25
|
-
- `skills/branch-pr`, `skills/chained-pr`, `skills/work-unit-commits`, `skills/judgment-day`, `skills/cognitive-doc-design`, `skills/comment-writer`, and `skills/issue-creation` — foundation skills ported from Gentle-AI.
|
|
26
|
-
- `prompts/` — reusable prompt templates with Gentle-prefixed names (`/gcl`, `/gis`, `/gpr`, `/gwr`) to avoid collisions with project prompts.
|
|
27
|
-
- `assets/orchestrator.md` — parent-session SDD orchestration contract adapted from Gentle-AI.
|
|
28
|
-
- `assets/support` — strict TDD apply/verify support files copied to `.pi/gentle-ai/support/`.
|
|
29
|
-
- `assets/agents` and `assets/chains` — SDD assets auto-installed into projects that use `pi-subagents`.
|
|
122
|
+
Small recommendation in English:
|
|
30
123
|
|
|
31
|
-
|
|
124
|
+
| Agent kind | Recommended model shape |
|
|
125
|
+
|---|---|
|
|
126
|
+
| Exploration, proposal, archive | Fast and cheap is usually enough. |
|
|
127
|
+
| Spec, design, tasks | Strong reasoning model, because these phases shape the implementation. |
|
|
128
|
+
| Apply | Strong coding model with good tool-use reliability. |
|
|
129
|
+
| Verify / review agents | Strongest fresh-context model you can afford; verification benefits from independence. |
|
|
130
|
+
| Tiny utility agents | Inherit the active/default model unless they become a bottleneck. |
|
|
131
|
+
|
|
132
|
+
Saved config:
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
.pi/gentle-ai/models.json
|
|
136
|
+
```
|
|
32
137
|
|
|
33
|
-
|
|
138
|
+
Applied configuration:
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
.pi/agents/*.md # project/user markdown agents
|
|
142
|
+
.pi/settings.json # project overrides for built-in pi-subagents agents
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Use `Inherit active/default model` to remove an agent override.
|
|
146
|
+
|
|
147
|
+
## Installed project files
|
|
148
|
+
|
|
149
|
+
`gentle-pi` installs these project-local assets on session start:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
.pi/agents/sdd-*.md
|
|
153
|
+
.pi/chains/sdd-*.chain.md
|
|
154
|
+
.pi/gentle-ai/support/strict-tdd.md
|
|
155
|
+
.pi/gentle-ai/support/strict-tdd-verify.md
|
|
156
|
+
```
|
|
34
157
|
|
|
35
|
-
|
|
158
|
+
Existing files are skipped. Your local edits remain unless you explicitly run:
|
|
36
159
|
|
|
37
160
|
```text
|
|
38
|
-
/gentle-ai:status
|
|
39
|
-
/sdd-init
|
|
40
161
|
/gentle-ai:install-sdd --force
|
|
41
162
|
```
|
|
42
163
|
|
|
43
|
-
|
|
164
|
+
## Package contents
|
|
165
|
+
|
|
166
|
+
| Path | Purpose |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `extensions/gentle-ai.ts` | Injects el Gentleman, installs assets, provides commands, applies model config, blocks unsafe shell commands. |
|
|
169
|
+
| `extensions/sdd-init.ts` | Registers `/sdd-init` for OpenSpec project initialization. |
|
|
170
|
+
| `extensions/skill-registry.ts` | Registers `/skill-registry:refresh` and maintains `.atl/skill-registry.md`. |
|
|
171
|
+
| `assets/orchestrator.md` | Parent-session orchestration contract. |
|
|
172
|
+
| `assets/agents/` | SDD phase agents copied into `.pi/agents/`. |
|
|
173
|
+
| `assets/chains/` | SDD chains copied into `.pi/chains/`. |
|
|
174
|
+
| `assets/support/` | Strict TDD apply/verify support docs. |
|
|
175
|
+
| `skills/` | el Gentleman and foundation skills. |
|
|
176
|
+
| `prompts/` | Gentle-prefixed prompt templates: `/gcl`, `/gis`, `/gpr`, `/gwr`. |
|
|
177
|
+
|
|
178
|
+
## Foundation skills
|
|
179
|
+
|
|
180
|
+
Included skills:
|
|
181
|
+
|
|
182
|
+
- `gentle-ai` — el Gentleman harness discipline.
|
|
183
|
+
- `branch-pr` — issue-first PR creation.
|
|
184
|
+
- `chained-pr` — split oversized PRs into reviewable chains.
|
|
185
|
+
- `work-unit-commits` — commit by deliverable work unit.
|
|
186
|
+
- `judgment-day` — blind dual review and re-judgment flow.
|
|
187
|
+
- `cognitive-doc-design` — documentation that reduces reviewer load.
|
|
188
|
+
- `comment-writer` — concise collaboration comments.
|
|
189
|
+
- `issue-creation` — issue-first GitHub workflow.
|
|
44
190
|
|
|
45
|
-
##
|
|
191
|
+
## Memory is separate
|
|
46
192
|
|
|
47
|
-
|
|
193
|
+
This package intentionally does **not** configure persistent memory.
|
|
194
|
+
|
|
195
|
+
Use a separate memory package, for example:
|
|
48
196
|
|
|
49
197
|
```bash
|
|
50
|
-
pi install npm:
|
|
51
|
-
pi install npm:pi-intercom
|
|
198
|
+
pi install npm:gentle-engram
|
|
52
199
|
```
|
|
53
200
|
|
|
54
|
-
|
|
55
|
-
|
|
201
|
+
el Gentleman may mention memory only when a memory package or callable memory tool is actually active.
|
|
202
|
+
|
|
203
|
+
## Local development
|
|
56
204
|
|
|
57
|
-
|
|
205
|
+
From this repo:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
pi install ./pi-packages/gentle-ai
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Validate before publishing:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
node --experimental-strip-types --check pi-packages/gentle-ai/extensions/gentle-ai.ts
|
|
215
|
+
cd pi-packages/gentle-ai
|
|
216
|
+
npm pack --dry-run
|
|
217
|
+
```
|
|
58
218
|
|
|
59
219
|
## Design principles
|
|
60
220
|
|
|
61
221
|
- Concepts before code.
|
|
62
|
-
- Artifacts over floating context.
|
|
63
|
-
- Strict TDD when tests exist
|
|
64
|
-
- One orchestrator, focused subagents.
|
|
222
|
+
- Artifacts over floating chat context.
|
|
223
|
+
- Strict TDD evidence when tests exist.
|
|
224
|
+
- One parent orchestrator, focused subagents.
|
|
65
225
|
- Review workload is a first-class constraint.
|
|
66
|
-
-
|
|
226
|
+
- Human control beats agent momentum.
|
|
Binary file
|
package/extensions/gentle-ai.ts
CHANGED
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import type { ExtensionAPI, ToolCallEventResult } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ExtensionAPI, ExtensionContext, ToolCallEventResult } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
5
7
|
|
|
6
8
|
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
7
9
|
const ASSETS_DIR = join(PACKAGE_ROOT, "assets");
|
|
8
10
|
const ORCHESTRATOR_PROMPT = readFileSync(join(ASSETS_DIR, "orchestrator.md"), "utf8").trim();
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
type PersonaMode = "gentleman" | "neutral";
|
|
13
|
+
|
|
14
|
+
const PERSONA_OPTIONS = ["gentleman", "neutral"] as const;
|
|
15
|
+
|
|
16
|
+
const GENTLEMAN_PERSONA_PROMPT = `Persona:
|
|
17
|
+
- Be direct, technical, and concise.
|
|
18
|
+
- When the user writes Spanish, answer in natural Rioplatense Spanish with voseo.
|
|
19
|
+
- Act as a senior architect and teacher: concepts before code, no shortcuts.
|
|
20
|
+
- Treat AI as a tool directed by the human; never present yourself as a default chatbot.
|
|
21
|
+
- Push back when the user asks for code without enough context or understanding.
|
|
22
|
+
- Correct errors directly, explain why, and show the better path.`;
|
|
23
|
+
|
|
24
|
+
const NEUTRAL_PERSONA_PROMPT = `Persona:
|
|
25
|
+
- Be direct, technical, concise, warm, and professional.
|
|
26
|
+
- Always respond in the same language the user writes in.
|
|
27
|
+
- Do not use slang or regional expressions.
|
|
28
|
+
- Act as a senior architect and teacher: concepts before code, no shortcuts.
|
|
29
|
+
- Treat AI as a tool directed by the human; never present yourself as a default chatbot.
|
|
30
|
+
- Push back when the user asks for code without enough context or understanding.
|
|
31
|
+
- Correct errors directly, explain why, and show the better path.`;
|
|
32
|
+
|
|
33
|
+
function buildGentlePrompt(persona: PersonaMode): string {
|
|
34
|
+
const personaPrompt = persona === "neutral" ? NEUTRAL_PERSONA_PROMPT : GENTLEMAN_PERSONA_PROMPT;
|
|
35
|
+
return `## el Gentleman Identity and Harness
|
|
11
36
|
You are el Gentleman: a Pi-specific coding-agent harness for controlled development work.
|
|
12
37
|
|
|
13
38
|
Identity contract:
|
|
@@ -17,11 +42,7 @@ Identity contract:
|
|
|
17
42
|
- Mention memory only when memory packages or callable memory tools are actually active; never invent persistent memory.
|
|
18
43
|
- Do not claim portability outside the Pi runtime.
|
|
19
44
|
|
|
20
|
-
|
|
21
|
-
- Be direct, technical, and concise.
|
|
22
|
-
- When the user writes Spanish, answer in natural Rioplatense Spanish with voseo.
|
|
23
|
-
- Act as a senior architect and teacher: concepts before code, no shortcuts.
|
|
24
|
-
- Treat AI as a tool directed by the human; never present yourself as a default chatbot.
|
|
45
|
+
${personaPrompt}
|
|
25
46
|
|
|
26
47
|
Harness principles:
|
|
27
48
|
- el Gentleman is not prompt engineering. It is runtime discipline around powerful agents.
|
|
@@ -34,6 +55,7 @@ Harness principles:
|
|
|
34
55
|
- Never claim persistent memory is available because of this package. Memory is provided by separate packages or MCP tools when installed and callable.
|
|
35
56
|
|
|
36
57
|
${ORCHESTRATOR_PROMPT}`;
|
|
58
|
+
}
|
|
37
59
|
|
|
38
60
|
const DENIED_BASH_PATTERNS: RegExp[] = [
|
|
39
61
|
/\brm\s+-rf\s+(?:\/|~|\$HOME|\.\.?)(?:\s|$)/,
|
|
@@ -52,7 +74,35 @@ const CONFIRM_BASH_PATTERNS: RegExp[] = [
|
|
|
52
74
|
/\bpi\s+remove\b/,
|
|
53
75
|
];
|
|
54
76
|
|
|
55
|
-
|
|
77
|
+
const SDD_AGENT_NAMES = [
|
|
78
|
+
"sdd-init",
|
|
79
|
+
"sdd-explore",
|
|
80
|
+
"sdd-proposal",
|
|
81
|
+
"sdd-spec",
|
|
82
|
+
"sdd-design",
|
|
83
|
+
"sdd-tasks",
|
|
84
|
+
"sdd-apply",
|
|
85
|
+
"sdd-verify",
|
|
86
|
+
"sdd-archive",
|
|
87
|
+
] as const;
|
|
88
|
+
|
|
89
|
+
type SddAgentName = (typeof SDD_AGENT_NAMES)[number];
|
|
90
|
+
type AgentModelConfig = Record<string, string>;
|
|
91
|
+
type AgentSource = "project" | "user" | "builtin";
|
|
92
|
+
|
|
93
|
+
interface AgentEntry {
|
|
94
|
+
name: string;
|
|
95
|
+
source: AgentSource;
|
|
96
|
+
filePath?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const KEEP_CURRENT = "Keep current";
|
|
100
|
+
const INHERIT_MODEL = "Inherit active/default model";
|
|
101
|
+
const CUSTOM_MODEL = "Custom model id";
|
|
102
|
+
|
|
103
|
+
const MODEL_CONTROL_OPTIONS = [KEEP_CURRENT, INHERIT_MODEL, CUSTOM_MODEL] as const;
|
|
104
|
+
|
|
105
|
+
function evaluateDeniedCommand(command: string): ToolCallEventResult | undefined {
|
|
56
106
|
for (const pattern of DENIED_BASH_PATTERNS) {
|
|
57
107
|
if (pattern.test(command)) {
|
|
58
108
|
return {
|
|
@@ -61,17 +111,32 @@ function evaluateCommand(command: string): ToolCallEventResult | undefined {
|
|
|
61
111
|
};
|
|
62
112
|
}
|
|
63
113
|
}
|
|
64
|
-
for (const pattern of CONFIRM_BASH_PATTERNS) {
|
|
65
|
-
if (pattern.test(command)) {
|
|
66
|
-
return {
|
|
67
|
-
block: true,
|
|
68
|
-
reason: "Gentle AI safety policy requires explicit user approval before this command.",
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
114
|
return undefined;
|
|
73
115
|
}
|
|
74
116
|
|
|
117
|
+
function commandRequiresConfirmation(command: string): boolean {
|
|
118
|
+
return CONFIRM_BASH_PATTERNS.some((pattern) => pattern.test(command));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function confirmCommand(command: string, ctx: ExtensionContext): Promise<ToolCallEventResult | undefined> {
|
|
122
|
+
const denied = evaluateDeniedCommand(command);
|
|
123
|
+
if (denied) return denied;
|
|
124
|
+
if (!commandRequiresConfirmation(command)) return undefined;
|
|
125
|
+
if (!ctx.hasUI) {
|
|
126
|
+
return {
|
|
127
|
+
block: true,
|
|
128
|
+
reason: "Gentle AI safety policy requires interactive confirmation before this command.",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const preview = truncateToWidth(command.replace(/\s+/g, " ").trim(), 180, "…");
|
|
132
|
+
const approved = await ctx.ui.confirm("Allow guarded command?", preview);
|
|
133
|
+
if (approved) return undefined;
|
|
134
|
+
return {
|
|
135
|
+
block: true,
|
|
136
|
+
reason: "Gentle AI safety policy blocked the command because it was not confirmed.",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
75
140
|
function copyDirectoryFiles(sourceDir: string, targetDir: string, force: boolean): { copied: number; skipped: number } {
|
|
76
141
|
if (!existsSync(sourceDir)) return { copied: 0, skipped: 0 };
|
|
77
142
|
mkdirSync(targetDir, { recursive: true });
|
|
@@ -112,24 +177,441 @@ function installSddAssets(
|
|
|
112
177
|
};
|
|
113
178
|
}
|
|
114
179
|
|
|
180
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
181
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function modelConfigPath(cwd: string): string {
|
|
185
|
+
return join(cwd, ".pi", "gentle-ai", "models.json");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function personaConfigPath(cwd: string): string {
|
|
189
|
+
return join(cwd, ".pi", "gentle-ai", "persona.json");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function readPersonaMode(cwd: string): PersonaMode {
|
|
193
|
+
const path = personaConfigPath(cwd);
|
|
194
|
+
if (!existsSync(path)) return "gentleman";
|
|
195
|
+
try {
|
|
196
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
197
|
+
if (!isRecord(parsed)) return "gentleman";
|
|
198
|
+
return parsed.mode === "neutral" ? "neutral" : "gentleman";
|
|
199
|
+
} catch {
|
|
200
|
+
return "gentleman";
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function writePersonaMode(cwd: string, mode: PersonaMode): void {
|
|
205
|
+
const path = personaConfigPath(cwd);
|
|
206
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
207
|
+
writeFileSync(path, `${JSON.stringify({ mode }, null, 2)}\n`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function readModelConfig(cwd: string): AgentModelConfig {
|
|
211
|
+
const path = modelConfigPath(cwd);
|
|
212
|
+
if (!existsSync(path)) return {};
|
|
213
|
+
try {
|
|
214
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
215
|
+
if (!isRecord(parsed)) return {};
|
|
216
|
+
const config: AgentModelConfig = {};
|
|
217
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
218
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
219
|
+
config[name] = value.trim();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return config;
|
|
223
|
+
} catch {
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function writeModelConfig(cwd: string, config: AgentModelConfig): void {
|
|
229
|
+
const path = modelConfigPath(cwd);
|
|
230
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
231
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function updateFrontmatterModel(content: string, model: string | undefined): string {
|
|
235
|
+
if (!content.startsWith("---\n")) return content;
|
|
236
|
+
const endIndex = content.indexOf("\n---", 4);
|
|
237
|
+
if (endIndex === -1) return content;
|
|
238
|
+
const frontmatter = content.slice(4, endIndex);
|
|
239
|
+
const body = content.slice(endIndex);
|
|
240
|
+
const lines = frontmatter.split("\n").filter((line) => !line.startsWith("model:"));
|
|
241
|
+
if (model !== undefined) {
|
|
242
|
+
const descriptionIndex = lines.findIndex((line) => line.startsWith("description:"));
|
|
243
|
+
const insertIndex = descriptionIndex >= 0 ? descriptionIndex + 1 : Math.min(1, lines.length);
|
|
244
|
+
lines.splice(insertIndex, 0, `model: ${model}`);
|
|
245
|
+
}
|
|
246
|
+
return `---\n${lines.join("\n")}${body}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function parseAgentName(filePath: string): string | undefined {
|
|
250
|
+
let content: string;
|
|
251
|
+
try {
|
|
252
|
+
content = readFileSync(filePath, "utf8");
|
|
253
|
+
} catch {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
const name = content.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim();
|
|
257
|
+
if (!name) return undefined;
|
|
258
|
+
const packageName = content.match(/^package:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim();
|
|
259
|
+
return packageName ? `${packageName}.${name}` : name;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function listAgentFilesRecursive(dir: string): string[] {
|
|
263
|
+
if (!existsSync(dir)) return [];
|
|
264
|
+
const files: string[] = [];
|
|
265
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
266
|
+
const path = join(dir, entry.name);
|
|
267
|
+
if (entry.isDirectory()) files.push(...listAgentFilesRecursive(path));
|
|
268
|
+
else if (entry.isFile() && entry.name.endsWith(".md") && !entry.name.endsWith(".chain.md")) files.push(path);
|
|
269
|
+
}
|
|
270
|
+
return files;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function listAgentsFromDir(dir: string, source: AgentSource): AgentEntry[] {
|
|
274
|
+
return listAgentFilesRecursive(dir)
|
|
275
|
+
.map((filePath) => {
|
|
276
|
+
const name = parseAgentName(filePath);
|
|
277
|
+
return name ? { name, source, filePath } : undefined;
|
|
278
|
+
})
|
|
279
|
+
.filter((entry): entry is AgentEntry => entry !== undefined);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function listDiscoverableAgents(cwd: string): AgentEntry[] {
|
|
283
|
+
const builtinDirs = [
|
|
284
|
+
join(PACKAGE_ROOT, "..", "pi-subagents", "agents"),
|
|
285
|
+
join(cwd, ".pi", "npm", "node_modules", "pi-subagents", "agents"),
|
|
286
|
+
join(homedir(), ".local", "lib", "node_modules", "pi-subagents", "agents"),
|
|
287
|
+
];
|
|
288
|
+
const agents = [
|
|
289
|
+
...builtinDirs.flatMap((dir) => listAgentsFromDir(dir, "builtin")),
|
|
290
|
+
...listAgentsFromDir(join(homedir(), ".pi", "agent", "agents"), "user"),
|
|
291
|
+
...listAgentsFromDir(join(homedir(), ".agents"), "user"),
|
|
292
|
+
...listAgentsFromDir(join(cwd, ".agents"), "project"),
|
|
293
|
+
...listAgentsFromDir(join(cwd, ".pi", "agents"), "project"),
|
|
294
|
+
];
|
|
295
|
+
const byName = new Map<string, AgentEntry>();
|
|
296
|
+
for (const agent of agents) byName.set(agent.name, agent);
|
|
297
|
+
const discovered = Array.from(byName.values());
|
|
298
|
+
const sddFirst = SDD_AGENT_NAMES
|
|
299
|
+
.map((name) => discovered.find((agent) => agent.name === name))
|
|
300
|
+
.filter((agent): agent is AgentEntry => agent !== undefined);
|
|
301
|
+
const rest = discovered
|
|
302
|
+
.filter((agent) => !SDD_AGENT_NAMES.includes(agent.name as SddAgentName))
|
|
303
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
304
|
+
return [...sddFirst, ...rest];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function projectSettingsPath(cwd: string): string {
|
|
308
|
+
return join(cwd, ".pi", "settings.json");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function updateBuiltinModelOverride(cwd: string, name: string, model: string | undefined): boolean {
|
|
312
|
+
const path = projectSettingsPath(cwd);
|
|
313
|
+
let settings: Record<string, unknown> = {};
|
|
314
|
+
if (existsSync(path)) {
|
|
315
|
+
try {
|
|
316
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
317
|
+
if (isRecord(parsed)) settings = parsed;
|
|
318
|
+
} catch {
|
|
319
|
+
settings = {};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const subagents = isRecord(settings.subagents) ? { ...settings.subagents } : {};
|
|
323
|
+
const agentOverrides = isRecord(subagents.agentOverrides) ? { ...subagents.agentOverrides } : {};
|
|
324
|
+
const current = isRecord(agentOverrides[name]) ? { ...agentOverrides[name] } : {};
|
|
325
|
+
if (model === undefined) delete current.model;
|
|
326
|
+
else current.model = model;
|
|
327
|
+
if (Object.keys(current).length > 0) agentOverrides[name] = current;
|
|
328
|
+
else delete agentOverrides[name];
|
|
329
|
+
if (Object.keys(agentOverrides).length > 0) subagents.agentOverrides = agentOverrides;
|
|
330
|
+
else delete subagents.agentOverrides;
|
|
331
|
+
if (Object.keys(subagents).length > 0) settings.subagents = subagents;
|
|
332
|
+
else delete settings.subagents;
|
|
333
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
334
|
+
writeFileSync(path, `${JSON.stringify(settings, null, "\t")}\n`);
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function applyModelConfig(cwd: string, config: AgentModelConfig): { updated: number; skipped: number } {
|
|
339
|
+
let updated = 0;
|
|
340
|
+
let skipped = 0;
|
|
341
|
+
for (const agent of listDiscoverableAgents(cwd)) {
|
|
342
|
+
const model = config[agent.name];
|
|
343
|
+
if (agent.source === "builtin") {
|
|
344
|
+
if (updateBuiltinModelOverride(cwd, agent.name, model)) updated += 1;
|
|
345
|
+
else skipped += 1;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (!agent.filePath || !existsSync(agent.filePath)) {
|
|
349
|
+
skipped += 1;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const original = readFileSync(agent.filePath, "utf8");
|
|
353
|
+
const next = updateFrontmatterModel(original, model);
|
|
354
|
+
if (next === original) {
|
|
355
|
+
skipped += 1;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
writeFileSync(agent.filePath, next);
|
|
359
|
+
updated += 1;
|
|
360
|
+
}
|
|
361
|
+
return { updated, skipped };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function describeModelConfig(cwd: string, config: AgentModelConfig): string[] {
|
|
365
|
+
return listDiscoverableAgents(cwd).map((agent) => `${agent.name}: ${config[agent.name] ?? "inherit"}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function getPiModelOptions(ctx: ExtensionContext): Promise<string[]> {
|
|
369
|
+
const models = await ctx.modelRegistry.getAvailable();
|
|
370
|
+
const modelIds = models
|
|
371
|
+
.map((model) => `${model.provider}/${model.id}`)
|
|
372
|
+
.sort((left, right) => left.localeCompare(right));
|
|
373
|
+
return [...MODEL_CONTROL_OPTIONS, ...modelIds];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
interface OverlayComponent {
|
|
377
|
+
render(width: number): string[];
|
|
378
|
+
handleInput(data: string): void;
|
|
379
|
+
invalidate(): void;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
type ModelPanelResult =
|
|
383
|
+
| { type: "save"; config: AgentModelConfig }
|
|
384
|
+
| { type: "custom"; agent: string | "all" }
|
|
385
|
+
| { type: "cancel" };
|
|
386
|
+
|
|
387
|
+
const SET_ALL_AGENTS = "Set all agents";
|
|
388
|
+
|
|
389
|
+
class SddModelPanel implements OverlayComponent {
|
|
390
|
+
private cursor = 0;
|
|
391
|
+
private mode: "agents" | "models" = "agents";
|
|
392
|
+
private selectedRow = SET_ALL_AGENTS;
|
|
393
|
+
private modelCursor = 0;
|
|
394
|
+
private query = "";
|
|
395
|
+
private readonly draft: AgentModelConfig;
|
|
396
|
+
private readonly rows: string[];
|
|
397
|
+
|
|
398
|
+
constructor(
|
|
399
|
+
initialConfig: AgentModelConfig,
|
|
400
|
+
private readonly modelOptions: string[],
|
|
401
|
+
agents: string[],
|
|
402
|
+
private readonly done: (result: ModelPanelResult) => void,
|
|
403
|
+
) {
|
|
404
|
+
this.draft = { ...initialConfig };
|
|
405
|
+
this.rows = [SET_ALL_AGENTS, ...agents];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
invalidate(): void {}
|
|
409
|
+
|
|
410
|
+
handleInput(data: string): void {
|
|
411
|
+
if (this.mode === "models") {
|
|
412
|
+
this.handleModelInput(data);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
this.handleAgentInput(data);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
render(width: number): string[] {
|
|
419
|
+
return this.mode === "models" ? this.renderModelPicker(width) : this.renderAgentList(width);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private handleAgentInput(data: string): void {
|
|
423
|
+
const maxCursor = this.rows.length + 1;
|
|
424
|
+
if (matchesKey(data, "ctrl+c") || matchesKey(data, "escape")) {
|
|
425
|
+
this.done({ type: "cancel" });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (matchesKey(data, "ctrl+s")) {
|
|
429
|
+
this.done({ type: "save", config: this.draft });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
433
|
+
this.cursor = Math.min(maxCursor, this.cursor + 1);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
437
|
+
this.cursor = Math.max(0, this.cursor - 1);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (data === "i") {
|
|
441
|
+
this.applySelection(undefined);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (data === "c") {
|
|
445
|
+
const row = this.rows[this.cursor];
|
|
446
|
+
if (row === SET_ALL_AGENTS) this.done({ type: "custom", agent: "all" });
|
|
447
|
+
else if (row) this.done({ type: "custom", agent: row });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (!matchesKey(data, "return")) return;
|
|
451
|
+
if (this.cursor === this.rows.length) {
|
|
452
|
+
this.done({ type: "save", config: this.draft });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (this.cursor === this.rows.length + 1) {
|
|
456
|
+
this.done({ type: "cancel" });
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
this.selectedRow = this.rows[this.cursor] ?? SET_ALL_AGENTS;
|
|
460
|
+
this.mode = "models";
|
|
461
|
+
this.modelCursor = 0;
|
|
462
|
+
this.query = "";
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private handleModelInput(data: string): void {
|
|
466
|
+
const options = this.filteredModelOptions();
|
|
467
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
468
|
+
this.done({ type: "cancel" });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (matchesKey(data, "escape")) {
|
|
472
|
+
this.mode = "agents";
|
|
473
|
+
this.query = "";
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (matchesKey(data, "backspace")) {
|
|
477
|
+
this.query = this.query.slice(0, -1);
|
|
478
|
+
this.modelCursor = Math.min(this.modelCursor, Math.max(0, this.filteredModelOptions().length - 1));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
482
|
+
this.modelCursor = Math.min(Math.max(0, options.length - 1), this.modelCursor + 1);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
486
|
+
this.modelCursor = Math.max(0, this.modelCursor - 1);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (matchesKey(data, "return")) {
|
|
490
|
+
const selected = options[this.modelCursor];
|
|
491
|
+
if (!selected) return;
|
|
492
|
+
if (selected === CUSTOM_MODEL) {
|
|
493
|
+
this.done({ type: "custom", agent: this.selectedRow === SET_ALL_AGENTS ? "all" : this.selectedRow });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (selected === KEEP_CURRENT) {
|
|
497
|
+
this.mode = "agents";
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
this.applySelection(selected === INHERIT_MODEL ? undefined : selected);
|
|
501
|
+
this.mode = "agents";
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
505
|
+
this.query += data;
|
|
506
|
+
this.modelCursor = 0;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private applySelection(model: string | undefined): void {
|
|
511
|
+
const row = this.rows[this.cursor];
|
|
512
|
+
if (row === SET_ALL_AGENTS) {
|
|
513
|
+
for (const name of this.rows.slice(1)) {
|
|
514
|
+
if (model === undefined) delete this.draft[name];
|
|
515
|
+
else this.draft[name] = model;
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (!row) return;
|
|
520
|
+
if (model === undefined) delete this.draft[row];
|
|
521
|
+
else this.draft[row] = model;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private filteredModelOptions(): string[] {
|
|
525
|
+
const query = this.query.trim().toLowerCase();
|
|
526
|
+
if (!query) return this.modelOptions;
|
|
527
|
+
return this.modelOptions.filter((option) => option.toLowerCase().includes(query));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private renderAgentList(width: number): string[] {
|
|
531
|
+
const lines: string[] = [];
|
|
532
|
+
const line = (text = "") => truncateToWidth(text, Math.max(1, width), "…", true);
|
|
533
|
+
lines.push(line("Assign Models to Agents"));
|
|
534
|
+
lines.push("");
|
|
535
|
+
lines.push(line("Current assignments:"));
|
|
536
|
+
lines.push("");
|
|
537
|
+
for (let i = 0; i < this.rows.length; i++) {
|
|
538
|
+
const row = this.rows[i] ?? SET_ALL_AGENTS;
|
|
539
|
+
const focused = i === this.cursor;
|
|
540
|
+
const label = row === SET_ALL_AGENTS ? this.renderSetAllLabel(row) : this.renderAgentLabel(row);
|
|
541
|
+
lines.push(line(`${focused ? "▸" : " "} ${label}`));
|
|
542
|
+
}
|
|
543
|
+
lines.push("");
|
|
544
|
+
lines.push(line(`${this.cursor === this.rows.length ? "▸" : " "} Continue`));
|
|
545
|
+
lines.push(line(`${this.cursor === this.rows.length + 1 ? "▸" : " "} ← Back`));
|
|
546
|
+
lines.push("");
|
|
547
|
+
lines.push(line("j/k: navigate • enter: change model / confirm • i: inherit • c: custom • ctrl+s: save • esc: back"));
|
|
548
|
+
return lines;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private renderModelPicker(width: number): string[] {
|
|
552
|
+
const lines: string[] = [];
|
|
553
|
+
const options = this.filteredModelOptions();
|
|
554
|
+
const line = (text = "") => truncateToWidth(text, Math.max(1, width), "…", true);
|
|
555
|
+
lines.push(line(`Select model for ${this.selectedRow}`));
|
|
556
|
+
lines.push("");
|
|
557
|
+
lines.push(line(`◎ ${this.query || "search..."}`));
|
|
558
|
+
lines.push("");
|
|
559
|
+
const maxVisible = 12;
|
|
560
|
+
const start = Math.max(0, Math.min(this.modelCursor - Math.floor(maxVisible / 2), Math.max(0, options.length - maxVisible)));
|
|
561
|
+
const end = Math.min(options.length, start + maxVisible);
|
|
562
|
+
for (let i = start; i < end; i++) {
|
|
563
|
+
const focused = i === this.modelCursor;
|
|
564
|
+
lines.push(line(`${focused ? "▸" : " "} ${options[i]}`));
|
|
565
|
+
}
|
|
566
|
+
if (options.length === 0) lines.push(line(" No matching models"));
|
|
567
|
+
lines.push("");
|
|
568
|
+
lines.push(line("j/k: navigate • type: search • enter: select • esc: back"));
|
|
569
|
+
return lines;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private renderSetAllLabel(row: string): string {
|
|
573
|
+
const values = this.rows.slice(1).map((name) => this.draft[name] ?? "inherit");
|
|
574
|
+
const first = values[0] ?? "inherit";
|
|
575
|
+
const allSame = values.every((value) => value === first);
|
|
576
|
+
return `${row.padEnd(20)} ${allSame ? first : "mixed"}`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private renderAgentLabel(row: string): string {
|
|
580
|
+
return `${row.padEnd(20)} ${this.draft[row] ?? "inherit"}`;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function showSddModelPanel(ctx: ExtensionContext, config: AgentModelConfig): Promise<ModelPanelResult> {
|
|
585
|
+
const modelOptions = await getPiModelOptions(ctx);
|
|
586
|
+
const agents = listDiscoverableAgents(ctx.cwd).map((agent) => agent.name);
|
|
587
|
+
return ctx.ui.custom<ModelPanelResult>((_tui, _theme, _keybindings, done) => new SddModelPanel(config, modelOptions, agents, done), {
|
|
588
|
+
overlay: true,
|
|
589
|
+
overlayOptions: { anchor: "center", width: "70%", minWidth: 72, maxHeight: "85%" },
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
115
593
|
export default function gentleAi(pi: ExtensionAPI): void {
|
|
116
594
|
pi.on("session_start", (_event, ctx) => {
|
|
117
595
|
const result = installSddAssets(ctx.cwd, false);
|
|
596
|
+
const modelResult = applyModelConfig(ctx.cwd, readModelConfig(ctx.cwd));
|
|
118
597
|
if (ctx.hasUI && (result.agents > 0 || result.chains > 0 || result.support > 0)) {
|
|
119
598
|
ctx.ui.notify(
|
|
120
599
|
`Gentle AI SDD assets auto-installed: ${result.agents} agent(s), ${result.chains} chain(s), ${result.support} support file(s).`,
|
|
121
600
|
"info",
|
|
122
601
|
);
|
|
123
602
|
}
|
|
603
|
+
if (ctx.hasUI && modelResult.updated > 0) {
|
|
604
|
+
ctx.ui.notify(`el Gentleman applied SDD model config to ${modelResult.updated} agent(s).`, "info");
|
|
605
|
+
}
|
|
124
606
|
});
|
|
125
607
|
|
|
126
|
-
pi.on("before_agent_start", (event) => ({
|
|
127
|
-
systemPrompt: `${event.systemPrompt}\n\n${
|
|
608
|
+
pi.on("before_agent_start", (event, ctx) => ({
|
|
609
|
+
systemPrompt: `${event.systemPrompt}\n\n${buildGentlePrompt(readPersonaMode(ctx.cwd))}`,
|
|
128
610
|
}));
|
|
129
611
|
|
|
130
|
-
pi.on("tool_call", (event) => {
|
|
612
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
131
613
|
if (event.toolName !== "bash") return undefined;
|
|
132
|
-
return
|
|
614
|
+
return confirmCommand(event.input.command, ctx);
|
|
133
615
|
});
|
|
134
616
|
|
|
135
617
|
pi.registerCommand("gentle-ai:install-sdd", {
|
|
@@ -144,18 +626,91 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
144
626
|
},
|
|
145
627
|
});
|
|
146
628
|
|
|
629
|
+
pi.registerCommand("gentleman:models", {
|
|
630
|
+
description: "Configure per-agent models for el Gentleman.",
|
|
631
|
+
handler: async (_args, ctx) => {
|
|
632
|
+
let config = readModelConfig(ctx.cwd);
|
|
633
|
+
let result = await showSddModelPanel(ctx, config);
|
|
634
|
+
while (result.type === "custom") {
|
|
635
|
+
const current = result.agent === "all" ? "inherit" : (config[result.agent] ?? "inherit");
|
|
636
|
+
const custom = await ctx.ui.input(
|
|
637
|
+
`${result.agent === "all" ? "all agents" : result.agent} custom model id`,
|
|
638
|
+
current === "inherit" ? "provider/model" : current,
|
|
639
|
+
);
|
|
640
|
+
if (custom === undefined) return;
|
|
641
|
+
const trimmed = custom.trim();
|
|
642
|
+
if (trimmed.length > 0) {
|
|
643
|
+
if (result.agent === "all") {
|
|
644
|
+
config = Object.fromEntries(listDiscoverableAgents(ctx.cwd).map((agent) => [agent.name, trimmed]));
|
|
645
|
+
} else {
|
|
646
|
+
config = { ...config, [result.agent]: trimmed };
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
result = await showSddModelPanel(ctx, config);
|
|
650
|
+
}
|
|
651
|
+
if (result.type !== "save") return;
|
|
652
|
+
writeModelConfig(ctx.cwd, result.config);
|
|
653
|
+
const applyResult = applyModelConfig(ctx.cwd, result.config);
|
|
654
|
+
ctx.ui.notify(
|
|
655
|
+
[
|
|
656
|
+
"el Gentleman model config saved.",
|
|
657
|
+
`Config: ${modelConfigPath(ctx.cwd)}`,
|
|
658
|
+
`Agents updated: ${applyResult.updated}`,
|
|
659
|
+
...describeModelConfig(ctx.cwd, result.config),
|
|
660
|
+
].join("\n"),
|
|
661
|
+
"info",
|
|
662
|
+
);
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
pi.registerCommand("gentle-ai:models", {
|
|
667
|
+
description: "Alias for /gentleman:models.",
|
|
668
|
+
handler: async (_args, ctx) => {
|
|
669
|
+
ctx.ui.notify("Use /gentleman:models to configure per-agent models.", "info");
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
pi.registerCommand("gentleman:persona", {
|
|
674
|
+
description: "Switch el Gentleman persona between gentleman and neutral.",
|
|
675
|
+
handler: async (_args, ctx) => {
|
|
676
|
+
const current = readPersonaMode(ctx.cwd);
|
|
677
|
+
const selected = await ctx.ui.select(`el Gentleman persona (current: ${current})`, [...PERSONA_OPTIONS]);
|
|
678
|
+
if (selected !== "gentleman" && selected !== "neutral") return;
|
|
679
|
+
writePersonaMode(ctx.cwd, selected);
|
|
680
|
+
ctx.ui.notify(
|
|
681
|
+
[
|
|
682
|
+
`el Gentleman persona set to: ${selected}`,
|
|
683
|
+
`Config: ${personaConfigPath(ctx.cwd)}`,
|
|
684
|
+
"Run /reload or start a new Pi session for already-injected prompts to refresh.",
|
|
685
|
+
].join("\n"),
|
|
686
|
+
"info",
|
|
687
|
+
);
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
pi.registerCommand("gentle-ai:persona", {
|
|
692
|
+
description: "Alias for /gentleman:persona.",
|
|
693
|
+
handler: async (_args, ctx) => {
|
|
694
|
+
ctx.ui.notify("Use /gentleman:persona to switch between gentleman and neutral personas.", "info");
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
|
|
147
698
|
pi.registerCommand("gentle-ai:status", {
|
|
148
699
|
description: "Show Gentle AI package status for this project.",
|
|
149
700
|
handler: async (_args, ctx) => {
|
|
150
701
|
const agentsInstalled = existsSync(join(ctx.cwd, ".pi", "agents", "sdd-apply.md"));
|
|
151
702
|
const chainsInstalled = existsSync(join(ctx.cwd, ".pi", "chains", "sdd-full.chain.md"));
|
|
152
703
|
const openspecConfigured = existsSync(join(ctx.cwd, "openspec", "config.yaml"));
|
|
704
|
+
const modelConfig = readModelConfig(ctx.cwd);
|
|
153
705
|
ctx.ui.notify(
|
|
154
706
|
[
|
|
155
|
-
"
|
|
707
|
+
"el Gentleman package is active.",
|
|
708
|
+
`Persona: ${readPersonaMode(ctx.cwd)}`,
|
|
156
709
|
`SDD agents: ${agentsInstalled ? "installed" : "not installed"}`,
|
|
157
710
|
`SDD chains: ${chainsInstalled ? "installed" : "not installed"}`,
|
|
158
711
|
`OpenSpec config: ${openspecConfigured ? "present" : "missing"}`,
|
|
712
|
+
`Model config: ${existsSync(modelConfigPath(ctx.cwd)) ? "present" : "missing"}`,
|
|
713
|
+
...describeModelConfig(ctx.cwd, modelConfig),
|
|
159
714
|
].join("\n"),
|
|
160
715
|
"info",
|
|
161
716
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Opinionated el Gentleman harness package for Pi: SDD/OpenSpec workflow, strict TDD guidance, subagent assets, and safety defaults.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"README.md"
|
|
27
27
|
],
|
|
28
28
|
"pi": {
|
|
29
|
+
"image": "https://raw.githubusercontent.com/Gentleman-Programming/gentle-pi/main/pi-packages/gentle-ai/assets/gentle-logo-only.png",
|
|
29
30
|
"extensions": [
|
|
30
31
|
"./extensions"
|
|
31
32
|
],
|
|
@@ -36,6 +37,9 @@
|
|
|
36
37
|
"./skills"
|
|
37
38
|
]
|
|
38
39
|
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@earendil-works/pi-tui": "*"
|
|
42
|
+
},
|
|
39
43
|
"peerDependencies": {
|
|
40
44
|
"@earendil-works/pi-coding-agent": "*"
|
|
41
45
|
},
|