gentle-pi 0.1.4 → 0.1.12
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/extensions/gentle-ai.ts +550 -10
- package/package.json +4 -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.
|
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,6 +74,34 @@ const CONFIRM_BASH_PATTERNS: RegExp[] = [
|
|
|
52
74
|
/\bpi\s+remove\b/,
|
|
53
75
|
];
|
|
54
76
|
|
|
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
|
+
|
|
55
105
|
function evaluateCommand(command: string): ToolCallEventResult | undefined {
|
|
56
106
|
for (const pattern of DENIED_BASH_PATTERNS) {
|
|
57
107
|
if (pattern.test(command)) {
|
|
@@ -112,19 +162,436 @@ function installSddAssets(
|
|
|
112
162
|
};
|
|
113
163
|
}
|
|
114
164
|
|
|
165
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
166
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function modelConfigPath(cwd: string): string {
|
|
170
|
+
return join(cwd, ".pi", "gentle-ai", "models.json");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function personaConfigPath(cwd: string): string {
|
|
174
|
+
return join(cwd, ".pi", "gentle-ai", "persona.json");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readPersonaMode(cwd: string): PersonaMode {
|
|
178
|
+
const path = personaConfigPath(cwd);
|
|
179
|
+
if (!existsSync(path)) return "gentleman";
|
|
180
|
+
try {
|
|
181
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
182
|
+
if (!isRecord(parsed)) return "gentleman";
|
|
183
|
+
return parsed.mode === "neutral" ? "neutral" : "gentleman";
|
|
184
|
+
} catch {
|
|
185
|
+
return "gentleman";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function writePersonaMode(cwd: string, mode: PersonaMode): void {
|
|
190
|
+
const path = personaConfigPath(cwd);
|
|
191
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
192
|
+
writeFileSync(path, `${JSON.stringify({ mode }, null, 2)}\n`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function readModelConfig(cwd: string): AgentModelConfig {
|
|
196
|
+
const path = modelConfigPath(cwd);
|
|
197
|
+
if (!existsSync(path)) return {};
|
|
198
|
+
try {
|
|
199
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
200
|
+
if (!isRecord(parsed)) return {};
|
|
201
|
+
const config: AgentModelConfig = {};
|
|
202
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
203
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
204
|
+
config[name] = value.trim();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return config;
|
|
208
|
+
} catch {
|
|
209
|
+
return {};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function writeModelConfig(cwd: string, config: AgentModelConfig): void {
|
|
214
|
+
const path = modelConfigPath(cwd);
|
|
215
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
216
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function updateFrontmatterModel(content: string, model: string | undefined): string {
|
|
220
|
+
if (!content.startsWith("---\n")) return content;
|
|
221
|
+
const endIndex = content.indexOf("\n---", 4);
|
|
222
|
+
if (endIndex === -1) return content;
|
|
223
|
+
const frontmatter = content.slice(4, endIndex);
|
|
224
|
+
const body = content.slice(endIndex);
|
|
225
|
+
const lines = frontmatter.split("\n").filter((line) => !line.startsWith("model:"));
|
|
226
|
+
if (model !== undefined) {
|
|
227
|
+
const descriptionIndex = lines.findIndex((line) => line.startsWith("description:"));
|
|
228
|
+
const insertIndex = descriptionIndex >= 0 ? descriptionIndex + 1 : Math.min(1, lines.length);
|
|
229
|
+
lines.splice(insertIndex, 0, `model: ${model}`);
|
|
230
|
+
}
|
|
231
|
+
return `---\n${lines.join("\n")}${body}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function parseAgentName(filePath: string): string | undefined {
|
|
235
|
+
let content: string;
|
|
236
|
+
try {
|
|
237
|
+
content = readFileSync(filePath, "utf8");
|
|
238
|
+
} catch {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
const name = content.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim();
|
|
242
|
+
if (!name) return undefined;
|
|
243
|
+
const packageName = content.match(/^package:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim();
|
|
244
|
+
return packageName ? `${packageName}.${name}` : name;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function listAgentFilesRecursive(dir: string): string[] {
|
|
248
|
+
if (!existsSync(dir)) return [];
|
|
249
|
+
const files: string[] = [];
|
|
250
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
251
|
+
const path = join(dir, entry.name);
|
|
252
|
+
if (entry.isDirectory()) files.push(...listAgentFilesRecursive(path));
|
|
253
|
+
else if (entry.isFile() && entry.name.endsWith(".md") && !entry.name.endsWith(".chain.md")) files.push(path);
|
|
254
|
+
}
|
|
255
|
+
return files;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function listAgentsFromDir(dir: string, source: AgentSource): AgentEntry[] {
|
|
259
|
+
return listAgentFilesRecursive(dir)
|
|
260
|
+
.map((filePath) => {
|
|
261
|
+
const name = parseAgentName(filePath);
|
|
262
|
+
return name ? { name, source, filePath } : undefined;
|
|
263
|
+
})
|
|
264
|
+
.filter((entry): entry is AgentEntry => entry !== undefined);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function listDiscoverableAgents(cwd: string): AgentEntry[] {
|
|
268
|
+
const builtinDirs = [
|
|
269
|
+
join(PACKAGE_ROOT, "..", "pi-subagents", "agents"),
|
|
270
|
+
join(cwd, ".pi", "npm", "node_modules", "pi-subagents", "agents"),
|
|
271
|
+
join(homedir(), ".local", "lib", "node_modules", "pi-subagents", "agents"),
|
|
272
|
+
];
|
|
273
|
+
const agents = [
|
|
274
|
+
...builtinDirs.flatMap((dir) => listAgentsFromDir(dir, "builtin")),
|
|
275
|
+
...listAgentsFromDir(join(homedir(), ".pi", "agent", "agents"), "user"),
|
|
276
|
+
...listAgentsFromDir(join(homedir(), ".agents"), "user"),
|
|
277
|
+
...listAgentsFromDir(join(cwd, ".agents"), "project"),
|
|
278
|
+
...listAgentsFromDir(join(cwd, ".pi", "agents"), "project"),
|
|
279
|
+
];
|
|
280
|
+
const byName = new Map<string, AgentEntry>();
|
|
281
|
+
for (const agent of agents) byName.set(agent.name, agent);
|
|
282
|
+
const discovered = Array.from(byName.values());
|
|
283
|
+
const sddFirst = SDD_AGENT_NAMES
|
|
284
|
+
.map((name) => discovered.find((agent) => agent.name === name))
|
|
285
|
+
.filter((agent): agent is AgentEntry => agent !== undefined);
|
|
286
|
+
const rest = discovered
|
|
287
|
+
.filter((agent) => !SDD_AGENT_NAMES.includes(agent.name as SddAgentName))
|
|
288
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
289
|
+
return [...sddFirst, ...rest];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function projectSettingsPath(cwd: string): string {
|
|
293
|
+
return join(cwd, ".pi", "settings.json");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function updateBuiltinModelOverride(cwd: string, name: string, model: string | undefined): boolean {
|
|
297
|
+
const path = projectSettingsPath(cwd);
|
|
298
|
+
let settings: Record<string, unknown> = {};
|
|
299
|
+
if (existsSync(path)) {
|
|
300
|
+
try {
|
|
301
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
302
|
+
if (isRecord(parsed)) settings = parsed;
|
|
303
|
+
} catch {
|
|
304
|
+
settings = {};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const subagents = isRecord(settings.subagents) ? { ...settings.subagents } : {};
|
|
308
|
+
const agentOverrides = isRecord(subagents.agentOverrides) ? { ...subagents.agentOverrides } : {};
|
|
309
|
+
const current = isRecord(agentOverrides[name]) ? { ...agentOverrides[name] } : {};
|
|
310
|
+
if (model === undefined) delete current.model;
|
|
311
|
+
else current.model = model;
|
|
312
|
+
if (Object.keys(current).length > 0) agentOverrides[name] = current;
|
|
313
|
+
else delete agentOverrides[name];
|
|
314
|
+
if (Object.keys(agentOverrides).length > 0) subagents.agentOverrides = agentOverrides;
|
|
315
|
+
else delete subagents.agentOverrides;
|
|
316
|
+
if (Object.keys(subagents).length > 0) settings.subagents = subagents;
|
|
317
|
+
else delete settings.subagents;
|
|
318
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
319
|
+
writeFileSync(path, `${JSON.stringify(settings, null, "\t")}\n`);
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function applyModelConfig(cwd: string, config: AgentModelConfig): { updated: number; skipped: number } {
|
|
324
|
+
let updated = 0;
|
|
325
|
+
let skipped = 0;
|
|
326
|
+
for (const agent of listDiscoverableAgents(cwd)) {
|
|
327
|
+
const model = config[agent.name];
|
|
328
|
+
if (agent.source === "builtin") {
|
|
329
|
+
if (updateBuiltinModelOverride(cwd, agent.name, model)) updated += 1;
|
|
330
|
+
else skipped += 1;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (!agent.filePath || !existsSync(agent.filePath)) {
|
|
334
|
+
skipped += 1;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const original = readFileSync(agent.filePath, "utf8");
|
|
338
|
+
const next = updateFrontmatterModel(original, model);
|
|
339
|
+
if (next === original) {
|
|
340
|
+
skipped += 1;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
writeFileSync(agent.filePath, next);
|
|
344
|
+
updated += 1;
|
|
345
|
+
}
|
|
346
|
+
return { updated, skipped };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function describeModelConfig(cwd: string, config: AgentModelConfig): string[] {
|
|
350
|
+
return listDiscoverableAgents(cwd).map((agent) => `${agent.name}: ${config[agent.name] ?? "inherit"}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function getPiModelOptions(ctx: ExtensionContext): Promise<string[]> {
|
|
354
|
+
const models = await ctx.modelRegistry.getAvailable();
|
|
355
|
+
const modelIds = models
|
|
356
|
+
.map((model) => `${model.provider}/${model.id}`)
|
|
357
|
+
.sort((left, right) => left.localeCompare(right));
|
|
358
|
+
return [...MODEL_CONTROL_OPTIONS, ...modelIds];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
interface OverlayComponent {
|
|
362
|
+
render(width: number): string[];
|
|
363
|
+
handleInput(data: string): void;
|
|
364
|
+
invalidate(): void;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
type ModelPanelResult =
|
|
368
|
+
| { type: "save"; config: AgentModelConfig }
|
|
369
|
+
| { type: "custom"; agent: string | "all" }
|
|
370
|
+
| { type: "cancel" };
|
|
371
|
+
|
|
372
|
+
const SET_ALL_AGENTS = "Set all agents";
|
|
373
|
+
|
|
374
|
+
class SddModelPanel implements OverlayComponent {
|
|
375
|
+
private cursor = 0;
|
|
376
|
+
private mode: "agents" | "models" = "agents";
|
|
377
|
+
private selectedRow = SET_ALL_AGENTS;
|
|
378
|
+
private modelCursor = 0;
|
|
379
|
+
private query = "";
|
|
380
|
+
private readonly draft: AgentModelConfig;
|
|
381
|
+
private readonly rows: string[];
|
|
382
|
+
|
|
383
|
+
constructor(
|
|
384
|
+
initialConfig: AgentModelConfig,
|
|
385
|
+
private readonly modelOptions: string[],
|
|
386
|
+
agents: string[],
|
|
387
|
+
private readonly done: (result: ModelPanelResult) => void,
|
|
388
|
+
) {
|
|
389
|
+
this.draft = { ...initialConfig };
|
|
390
|
+
this.rows = [SET_ALL_AGENTS, ...agents];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
invalidate(): void {}
|
|
394
|
+
|
|
395
|
+
handleInput(data: string): void {
|
|
396
|
+
if (this.mode === "models") {
|
|
397
|
+
this.handleModelInput(data);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
this.handleAgentInput(data);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
render(width: number): string[] {
|
|
404
|
+
return this.mode === "models" ? this.renderModelPicker(width) : this.renderAgentList(width);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private handleAgentInput(data: string): void {
|
|
408
|
+
const maxCursor = this.rows.length + 1;
|
|
409
|
+
if (matchesKey(data, "ctrl+c") || matchesKey(data, "escape")) {
|
|
410
|
+
this.done({ type: "cancel" });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (matchesKey(data, "ctrl+s")) {
|
|
414
|
+
this.done({ type: "save", config: this.draft });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
418
|
+
this.cursor = Math.min(maxCursor, this.cursor + 1);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
422
|
+
this.cursor = Math.max(0, this.cursor - 1);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (data === "i") {
|
|
426
|
+
this.applySelection(undefined);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (data === "c") {
|
|
430
|
+
const row = this.rows[this.cursor];
|
|
431
|
+
if (row === SET_ALL_AGENTS) this.done({ type: "custom", agent: "all" });
|
|
432
|
+
else if (row) this.done({ type: "custom", agent: row });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (!matchesKey(data, "return")) return;
|
|
436
|
+
if (this.cursor === this.rows.length) {
|
|
437
|
+
this.done({ type: "save", config: this.draft });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (this.cursor === this.rows.length + 1) {
|
|
441
|
+
this.done({ type: "cancel" });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
this.selectedRow = this.rows[this.cursor] ?? SET_ALL_AGENTS;
|
|
445
|
+
this.mode = "models";
|
|
446
|
+
this.modelCursor = 0;
|
|
447
|
+
this.query = "";
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private handleModelInput(data: string): void {
|
|
451
|
+
const options = this.filteredModelOptions();
|
|
452
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
453
|
+
this.done({ type: "cancel" });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (matchesKey(data, "escape")) {
|
|
457
|
+
this.mode = "agents";
|
|
458
|
+
this.query = "";
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (matchesKey(data, "backspace")) {
|
|
462
|
+
this.query = this.query.slice(0, -1);
|
|
463
|
+
this.modelCursor = Math.min(this.modelCursor, Math.max(0, this.filteredModelOptions().length - 1));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
467
|
+
this.modelCursor = Math.min(Math.max(0, options.length - 1), this.modelCursor + 1);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
471
|
+
this.modelCursor = Math.max(0, this.modelCursor - 1);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (matchesKey(data, "return")) {
|
|
475
|
+
const selected = options[this.modelCursor];
|
|
476
|
+
if (!selected) return;
|
|
477
|
+
if (selected === CUSTOM_MODEL) {
|
|
478
|
+
this.done({ type: "custom", agent: this.selectedRow === SET_ALL_AGENTS ? "all" : this.selectedRow });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (selected === KEEP_CURRENT) {
|
|
482
|
+
this.mode = "agents";
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
this.applySelection(selected === INHERIT_MODEL ? undefined : selected);
|
|
486
|
+
this.mode = "agents";
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
490
|
+
this.query += data;
|
|
491
|
+
this.modelCursor = 0;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private applySelection(model: string | undefined): void {
|
|
496
|
+
const row = this.rows[this.cursor];
|
|
497
|
+
if (row === SET_ALL_AGENTS) {
|
|
498
|
+
for (const name of this.rows.slice(1)) {
|
|
499
|
+
if (model === undefined) delete this.draft[name];
|
|
500
|
+
else this.draft[name] = model;
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (!row) return;
|
|
505
|
+
if (model === undefined) delete this.draft[row];
|
|
506
|
+
else this.draft[row] = model;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private filteredModelOptions(): string[] {
|
|
510
|
+
const query = this.query.trim().toLowerCase();
|
|
511
|
+
if (!query) return this.modelOptions;
|
|
512
|
+
return this.modelOptions.filter((option) => option.toLowerCase().includes(query));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private renderAgentList(width: number): string[] {
|
|
516
|
+
const lines: string[] = [];
|
|
517
|
+
const line = (text = "") => truncateToWidth(text, Math.max(1, width), "…", true);
|
|
518
|
+
lines.push(line("Assign Models to Agents"));
|
|
519
|
+
lines.push("");
|
|
520
|
+
lines.push(line("Current assignments:"));
|
|
521
|
+
lines.push("");
|
|
522
|
+
for (let i = 0; i < this.rows.length; i++) {
|
|
523
|
+
const row = this.rows[i] ?? SET_ALL_AGENTS;
|
|
524
|
+
const focused = i === this.cursor;
|
|
525
|
+
const label = row === SET_ALL_AGENTS ? this.renderSetAllLabel(row) : this.renderAgentLabel(row);
|
|
526
|
+
lines.push(line(`${focused ? "▸" : " "} ${label}`));
|
|
527
|
+
}
|
|
528
|
+
lines.push("");
|
|
529
|
+
lines.push(line(`${this.cursor === this.rows.length ? "▸" : " "} Continue`));
|
|
530
|
+
lines.push(line(`${this.cursor === this.rows.length + 1 ? "▸" : " "} ← Back`));
|
|
531
|
+
lines.push("");
|
|
532
|
+
lines.push(line("j/k: navigate • enter: change model / confirm • i: inherit • c: custom • ctrl+s: save • esc: back"));
|
|
533
|
+
return lines;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private renderModelPicker(width: number): string[] {
|
|
537
|
+
const lines: string[] = [];
|
|
538
|
+
const options = this.filteredModelOptions();
|
|
539
|
+
const line = (text = "") => truncateToWidth(text, Math.max(1, width), "…", true);
|
|
540
|
+
lines.push(line(`Select model for ${this.selectedRow}`));
|
|
541
|
+
lines.push("");
|
|
542
|
+
lines.push(line(`◎ ${this.query || "search..."}`));
|
|
543
|
+
lines.push("");
|
|
544
|
+
const maxVisible = 12;
|
|
545
|
+
const start = Math.max(0, Math.min(this.modelCursor - Math.floor(maxVisible / 2), Math.max(0, options.length - maxVisible)));
|
|
546
|
+
const end = Math.min(options.length, start + maxVisible);
|
|
547
|
+
for (let i = start; i < end; i++) {
|
|
548
|
+
const focused = i === this.modelCursor;
|
|
549
|
+
lines.push(line(`${focused ? "▸" : " "} ${options[i]}`));
|
|
550
|
+
}
|
|
551
|
+
if (options.length === 0) lines.push(line(" No matching models"));
|
|
552
|
+
lines.push("");
|
|
553
|
+
lines.push(line("j/k: navigate • type: search • enter: select • esc: back"));
|
|
554
|
+
return lines;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private renderSetAllLabel(row: string): string {
|
|
558
|
+
const values = this.rows.slice(1).map((name) => this.draft[name] ?? "inherit");
|
|
559
|
+
const first = values[0] ?? "inherit";
|
|
560
|
+
const allSame = values.every((value) => value === first);
|
|
561
|
+
return `${row.padEnd(20)} ${allSame ? first : "mixed"}`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private renderAgentLabel(row: string): string {
|
|
565
|
+
return `${row.padEnd(20)} ${this.draft[row] ?? "inherit"}`;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function showSddModelPanel(ctx: ExtensionContext, config: AgentModelConfig): Promise<ModelPanelResult> {
|
|
570
|
+
const modelOptions = await getPiModelOptions(ctx);
|
|
571
|
+
const agents = listDiscoverableAgents(ctx.cwd).map((agent) => agent.name);
|
|
572
|
+
return ctx.ui.custom<ModelPanelResult>((_tui, _theme, _keybindings, done) => new SddModelPanel(config, modelOptions, agents, done), {
|
|
573
|
+
overlay: true,
|
|
574
|
+
overlayOptions: { anchor: "center", width: "70%", minWidth: 72, maxHeight: "85%" },
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
115
578
|
export default function gentleAi(pi: ExtensionAPI): void {
|
|
116
579
|
pi.on("session_start", (_event, ctx) => {
|
|
117
580
|
const result = installSddAssets(ctx.cwd, false);
|
|
581
|
+
const modelResult = applyModelConfig(ctx.cwd, readModelConfig(ctx.cwd));
|
|
118
582
|
if (ctx.hasUI && (result.agents > 0 || result.chains > 0 || result.support > 0)) {
|
|
119
583
|
ctx.ui.notify(
|
|
120
584
|
`Gentle AI SDD assets auto-installed: ${result.agents} agent(s), ${result.chains} chain(s), ${result.support} support file(s).`,
|
|
121
585
|
"info",
|
|
122
586
|
);
|
|
123
587
|
}
|
|
588
|
+
if (ctx.hasUI && modelResult.updated > 0) {
|
|
589
|
+
ctx.ui.notify(`el Gentleman applied SDD model config to ${modelResult.updated} agent(s).`, "info");
|
|
590
|
+
}
|
|
124
591
|
});
|
|
125
592
|
|
|
126
|
-
pi.on("before_agent_start", (event) => ({
|
|
127
|
-
systemPrompt: `${event.systemPrompt}\n\n${
|
|
593
|
+
pi.on("before_agent_start", (event, ctx) => ({
|
|
594
|
+
systemPrompt: `${event.systemPrompt}\n\n${buildGentlePrompt(readPersonaMode(ctx.cwd))}`,
|
|
128
595
|
}));
|
|
129
596
|
|
|
130
597
|
pi.on("tool_call", (event) => {
|
|
@@ -144,18 +611,91 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
144
611
|
},
|
|
145
612
|
});
|
|
146
613
|
|
|
614
|
+
pi.registerCommand("gentleman:models", {
|
|
615
|
+
description: "Configure per-agent models for el Gentleman.",
|
|
616
|
+
handler: async (_args, ctx) => {
|
|
617
|
+
let config = readModelConfig(ctx.cwd);
|
|
618
|
+
let result = await showSddModelPanel(ctx, config);
|
|
619
|
+
while (result.type === "custom") {
|
|
620
|
+
const current = result.agent === "all" ? "inherit" : (config[result.agent] ?? "inherit");
|
|
621
|
+
const custom = await ctx.ui.input(
|
|
622
|
+
`${result.agent === "all" ? "all agents" : result.agent} custom model id`,
|
|
623
|
+
current === "inherit" ? "provider/model" : current,
|
|
624
|
+
);
|
|
625
|
+
if (custom === undefined) return;
|
|
626
|
+
const trimmed = custom.trim();
|
|
627
|
+
if (trimmed.length > 0) {
|
|
628
|
+
if (result.agent === "all") {
|
|
629
|
+
config = Object.fromEntries(listDiscoverableAgents(ctx.cwd).map((agent) => [agent.name, trimmed]));
|
|
630
|
+
} else {
|
|
631
|
+
config = { ...config, [result.agent]: trimmed };
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
result = await showSddModelPanel(ctx, config);
|
|
635
|
+
}
|
|
636
|
+
if (result.type !== "save") return;
|
|
637
|
+
writeModelConfig(ctx.cwd, result.config);
|
|
638
|
+
const applyResult = applyModelConfig(ctx.cwd, result.config);
|
|
639
|
+
ctx.ui.notify(
|
|
640
|
+
[
|
|
641
|
+
"el Gentleman model config saved.",
|
|
642
|
+
`Config: ${modelConfigPath(ctx.cwd)}`,
|
|
643
|
+
`Agents updated: ${applyResult.updated}`,
|
|
644
|
+
...describeModelConfig(ctx.cwd, result.config),
|
|
645
|
+
].join("\n"),
|
|
646
|
+
"info",
|
|
647
|
+
);
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
pi.registerCommand("gentle-ai:models", {
|
|
652
|
+
description: "Alias for /gentleman:models.",
|
|
653
|
+
handler: async (_args, ctx) => {
|
|
654
|
+
ctx.ui.notify("Use /gentleman:models to configure per-agent models.", "info");
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
pi.registerCommand("gentleman:persona", {
|
|
659
|
+
description: "Switch el Gentleman persona between gentleman and neutral.",
|
|
660
|
+
handler: async (_args, ctx) => {
|
|
661
|
+
const current = readPersonaMode(ctx.cwd);
|
|
662
|
+
const selected = await ctx.ui.select(`el Gentleman persona (current: ${current})`, [...PERSONA_OPTIONS]);
|
|
663
|
+
if (selected !== "gentleman" && selected !== "neutral") return;
|
|
664
|
+
writePersonaMode(ctx.cwd, selected);
|
|
665
|
+
ctx.ui.notify(
|
|
666
|
+
[
|
|
667
|
+
`el Gentleman persona set to: ${selected}`,
|
|
668
|
+
`Config: ${personaConfigPath(ctx.cwd)}`,
|
|
669
|
+
"Run /reload or start a new Pi session for already-injected prompts to refresh.",
|
|
670
|
+
].join("\n"),
|
|
671
|
+
"info",
|
|
672
|
+
);
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
pi.registerCommand("gentle-ai:persona", {
|
|
677
|
+
description: "Alias for /gentleman:persona.",
|
|
678
|
+
handler: async (_args, ctx) => {
|
|
679
|
+
ctx.ui.notify("Use /gentleman:persona to switch between gentleman and neutral personas.", "info");
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
|
|
147
683
|
pi.registerCommand("gentle-ai:status", {
|
|
148
684
|
description: "Show Gentle AI package status for this project.",
|
|
149
685
|
handler: async (_args, ctx) => {
|
|
150
686
|
const agentsInstalled = existsSync(join(ctx.cwd, ".pi", "agents", "sdd-apply.md"));
|
|
151
687
|
const chainsInstalled = existsSync(join(ctx.cwd, ".pi", "chains", "sdd-full.chain.md"));
|
|
152
688
|
const openspecConfigured = existsSync(join(ctx.cwd, "openspec", "config.yaml"));
|
|
689
|
+
const modelConfig = readModelConfig(ctx.cwd);
|
|
153
690
|
ctx.ui.notify(
|
|
154
691
|
[
|
|
155
|
-
"
|
|
692
|
+
"el Gentleman package is active.",
|
|
693
|
+
`Persona: ${readPersonaMode(ctx.cwd)}`,
|
|
156
694
|
`SDD agents: ${agentsInstalled ? "installed" : "not installed"}`,
|
|
157
695
|
`SDD chains: ${chainsInstalled ? "installed" : "not installed"}`,
|
|
158
696
|
`OpenSpec config: ${openspecConfigured ? "present" : "missing"}`,
|
|
697
|
+
`Model config: ${existsSync(modelConfigPath(ctx.cwd)) ? "present" : "missing"}`,
|
|
698
|
+
...describeModelConfig(ctx.cwd, modelConfig),
|
|
159
699
|
].join("\n"),
|
|
160
700
|
"info",
|
|
161
701
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
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",
|
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
"./skills"
|
|
37
37
|
]
|
|
38
38
|
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@earendil-works/pi-tui": "*"
|
|
41
|
+
},
|
|
39
42
|
"peerDependencies": {
|
|
40
43
|
"@earendil-works/pi-coding-agent": "*"
|
|
41
44
|
},
|