gentle-pi 0.11.1 → 0.11.3
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 +16 -28
- package/assets/sdd-orchestrator-workflow.md +25 -3
- package/extensions/gentle-ai.ts +167 -146
- package/extensions/startup-banner.ts +4 -6
- package/package.json +1 -1
- package/skills/gentle-ai/SKILL.md +1 -1
- package/tests/runtime-harness.mjs +234 -82
package/README.md
CHANGED
|
@@ -88,9 +88,9 @@ pi
|
|
|
88
88
|
## Quick start
|
|
89
89
|
|
|
90
90
|
```text
|
|
91
|
-
/gentle
|
|
92
|
-
/gentle
|
|
93
|
-
/gentle
|
|
91
|
+
/gentle:status Check package, SDD assets, OpenSpec, and global model config.
|
|
92
|
+
/gentle:doctor Run read-only diagnostics for SDD assets, config, tools, and guards.
|
|
93
|
+
/gentle:sdd-preflight Run or reuse the session SDD preflight explicitly.
|
|
94
94
|
/sdd-init Create or refresh openspec/config.yaml.
|
|
95
95
|
/gentle:models Assign global model/effort routing to SDD/custom agents.
|
|
96
96
|
/gentle:persona Switch between gentleman and neutral persona modes.
|
|
@@ -100,7 +100,7 @@ pi
|
|
|
100
100
|
Typical flow:
|
|
101
101
|
|
|
102
102
|
1. Open Pi in your repo.
|
|
103
|
-
2. Run `/gentle
|
|
103
|
+
2. Run `/gentle:status`.
|
|
104
104
|
3. Run `/sdd-init` once per project, or when test/project capabilities change. This also runs the session SDD preflight.
|
|
105
105
|
4. For a substantial change, ask Pi to use SDD. Natural-language requests are classified by the parent agent, not by brittle runtime regexes.
|
|
106
106
|
5. Review the phase artifacts instead of trusting floating chat context.
|
|
@@ -228,7 +228,7 @@ Engram-only mode is different by design: Engram is working memory and does not m
|
|
|
228
228
|
|
|
229
229
|
## SDD preflight and project files
|
|
230
230
|
|
|
231
|
-
`gentle-pi` does not require SDD agents to be copied into every project. The package ensures global Pi SDD assets exist under the Pi agent home and treats project-local files only as overrides/debug copies. Slash SDD flows such as `/sdd-*`, `/sdd-init`, and the explicit `/gentle
|
|
231
|
+
`gentle-pi` does not require SDD agents to be copied into every project. The package ensures global Pi SDD assets exist under the Pi agent home and treats project-local files only as overrides/debug copies. Slash SDD flows such as `/sdd-*`, `/sdd-init`, and the explicit `/gentle:sdd-preflight` command run a lazy preflight and ask for session-scoped SDD preferences. For natural-language requests, the parent agent decides whether the work should use SDD and must run/reuse `/gentle:sdd-preflight` before continuing.
|
|
232
232
|
|
|
233
233
|
```text
|
|
234
234
|
~/.pi/agent/agents/sdd-*.md
|
|
@@ -246,13 +246,12 @@ The preflight choices are reused for later SDD flows in the same session:
|
|
|
246
246
|
It does **not** overwrite existing global assets unless you explicitly run:
|
|
247
247
|
|
|
248
248
|
```text
|
|
249
|
-
/gentle
|
|
249
|
+
/gentle:install-sdd --force
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
-
Manual preflight
|
|
252
|
+
Manual preflight command:
|
|
253
253
|
|
|
254
254
|
```text
|
|
255
|
-
/gentle-ai:sdd-preflight
|
|
256
255
|
/gentle:sdd-preflight
|
|
257
256
|
```
|
|
258
257
|
|
|
@@ -356,10 +355,12 @@ A project can still override the global default with:
|
|
|
356
355
|
|
|
357
356
|
The modal discovers:
|
|
358
357
|
|
|
359
|
-
- project agents in `.pi/agents
|
|
360
|
-
- user agents in `~/.pi/agent/agents
|
|
358
|
+
- project agents in `.pi/subagents/`, `.pi/agents/`, and `.agents/`;
|
|
359
|
+
- user agents in `~/.pi/agent/subagents/`, `~/.pi/agent/agents/`, and `~/.agents/`;
|
|
361
360
|
- built-in agents from `pi-subagents-j0k3r` when present.
|
|
362
361
|
|
|
362
|
+
When applying routing, project agents write runtime profiles to `.pi/subagents.json`; global and built-in agents write profiles to `~/.pi/agent/subagents.json`.
|
|
363
|
+
|
|
363
364
|
Recommended model/effort shape:
|
|
364
365
|
|
|
365
366
|
| Agent kind | Recommended model | Recommended effort (`thinking`) |
|
|
@@ -400,8 +401,8 @@ Legacy string entries are still accepted and treated as `model`-only config.
|
|
|
400
401
|
|
|
401
402
|
| Command | What it does |
|
|
402
403
|
| -------------------------------- | ------------------------------------------------------------------- |
|
|
403
|
-
| `/gentle
|
|
404
|
-
| `/gentle
|
|
404
|
+
| `/gentle:status` | Shows package, SDD asset, OpenSpec, and global model config status. |
|
|
405
|
+
| `/gentle:doctor` | Runs read-only diagnostics for SDD assets, model/persona config, memory tools, and safety guards. |
|
|
405
406
|
| `/gentle:models` | Opens global model + effort assignment UI. Press `x` to export and `r` to restore saved routing. |
|
|
406
407
|
| `/gentle:persona` | Switches global persona mode, with project override support. |
|
|
407
408
|
| `/gentle:banner` | Configures startup banner rose, text logo, and color preset. |
|
|
@@ -409,14 +410,14 @@ Legacy string entries are still accepted and treated as `model`-only config.
|
|
|
409
410
|
| `/gentle:toggle-text-logo` | Toggles the startup text logo. |
|
|
410
411
|
| `/gentle:banner-color` | Selects a startup banner color preset. |
|
|
411
412
|
| `/sdd-init` | Initializes or refreshes `openspec/config.yaml`. |
|
|
412
|
-
| `/gentle
|
|
413
|
-
| `/gentle
|
|
413
|
+
| `/gentle:install-sdd` | Repairs missing global SDD runtime assets without overwriting files. |
|
|
414
|
+
| `/gentle:install-sdd --force` | Force-refreshes installed global SDD assets. |
|
|
414
415
|
| `/skill-registry:refresh` | Regenerates `.atl/skill-registry.md`. |
|
|
415
416
|
| `/skill-creation` | Creates or updates an LLM-first skill using the packaged `skill-creator` contract and style guide. |
|
|
416
417
|
|
|
417
418
|
Package-owned global SDD runtime assets are also refreshed automatically on session start when `gentle-pi` changes. Project-local `.pi/agents` and `.pi/chains` remain manual overrides and are never overwritten by startup refresh.
|
|
418
419
|
|
|
419
|
-
Startup banner settings are global and default to the current pink rose + text logo. Supported color presets are `pink`, `cyan`, `yellow`, and `green`.
|
|
420
|
+
Startup banner settings are global and default to the current pink rose + text logo. Supported color presets are `pink`, `cyan`, `yellow`, and `green`.
|
|
420
421
|
|
|
421
422
|
Startup flag:
|
|
422
423
|
|
|
@@ -426,19 +427,6 @@ pi --no-skill-registry
|
|
|
426
427
|
|
|
427
428
|
Use it when you want skills available normally but do not want Gentle AI to refresh/watch `.atl/skill-registry.md` on startup. `pi -ns` / `pi --no-skills` also skip the registry startup work because Pi is already disabling skill loading.
|
|
428
429
|
|
|
429
|
-
Compatibility aliases:
|
|
430
|
-
|
|
431
|
-
```text
|
|
432
|
-
/gentle-ai:models
|
|
433
|
-
/gentleman:models
|
|
434
|
-
/gentle-ai:persona
|
|
435
|
-
/gentleman:persona
|
|
436
|
-
/gentle-ai:banner
|
|
437
|
-
/gentle-ai:toggle-rose
|
|
438
|
-
/gentle-ai:toggle-text-logo
|
|
439
|
-
/gentle-ai:banner-color
|
|
440
|
-
```
|
|
441
|
-
|
|
442
430
|
## Included skills
|
|
443
431
|
|
|
444
432
|
- `gentle-ai` — harness discipline for controlled Pi work.
|
|
@@ -55,14 +55,14 @@ Do not guess the active change. If change selection is ambiguous, ask the user a
|
|
|
55
55
|
|
|
56
56
|
## Lazy SDD Preflight
|
|
57
57
|
|
|
58
|
-
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
|
|
58
|
+
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:sdd-preflight` before continuing.
|
|
59
59
|
|
|
60
60
|
**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:
|
|
61
61
|
|
|
62
62
|
1. an injected `## SDD Session Preflight` block, or
|
|
63
63
|
2. an explicit user answer in the current conversation covering all four preflight choices below.
|
|
64
64
|
|
|
65
|
-
If neither exists and `/gentle
|
|
65
|
+
If neither exists and `/gentle: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.
|
|
66
66
|
|
|
67
67
|
The preflight captures:
|
|
68
68
|
|
|
@@ -78,7 +78,7 @@ The package should ensure SDD assets are present as global Pi runtime assets wit
|
|
|
78
78
|
~/.pi/agent/chains/sdd-*.chain.md
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
Manual install commands are recovery/debug paths, not the happy path. `/gentle
|
|
81
|
+
Manual install commands are recovery/debug paths, not the happy path. `/gentle:sdd-preflight` is the explicit preflight command for agent/orchestrator use. If the user explicitly changes SDD preferences later in the same session, follow the new instruction.
|
|
82
82
|
|
|
83
83
|
## Init Guard
|
|
84
84
|
|
|
@@ -132,6 +132,28 @@ skill_resolution
|
|
|
132
132
|
|
|
133
133
|
The parent should synthesize these envelopes, not paste long raw reports unless needed.
|
|
134
134
|
|
|
135
|
+
## Automatic Mode Gatekeeper
|
|
136
|
+
|
|
137
|
+
In `auto` execution mode, the parent/orchestrator is the quality gate between SDD phases. After a delegated phase returns and before launching the next phase, validate that the phase actually reached its objective. This validation is autonomous: do not ask the user on the happy path, but stop and report if the gate catches a real problem.
|
|
138
|
+
|
|
139
|
+
Check every phase result against the Result Contract:
|
|
140
|
+
|
|
141
|
+
- **Contract conformance:** the phase returned `status`, `executive_summary`, `artifacts`, `next_recommended`, `risks`, and `skill_resolution`, and `status` indicates success rather than partial, failed, or blocked.
|
|
142
|
+
- **Artifact existence:** every declared artifact exists and is readable in the active backend. For memory-backed flows, retrieve the topic with the available memory tools; for OpenSpec/file-backed flows, read the declared path. A successful phase with no retrievable artifact fails the gate.
|
|
143
|
+
- **No hallucinated references:** spot-check concrete file paths, symbols, commands, and artifacts the phase claims it created or used. Referenced paths or artifacts that do not resolve fail the gate.
|
|
144
|
+
- **No scope drift:** the output must stay consistent with its inputs and the dependency graph: spec stays within proposal scope, design answers the proposal, tasks cover spec and design, apply implements the tasks, verify checks the implementation against the spec, and sync reflects the verified state before archive.
|
|
145
|
+
- **Routing coherence:** `next_recommended` must follow the SDD dependency graph, and no unaddressed critical risk may be carried silently into the next phase.
|
|
146
|
+
|
|
147
|
+
Use cost-aware validation:
|
|
148
|
+
|
|
149
|
+
- For lower-risk phases (`sdd-explore`, `sdd-spec`, `sdd-tasks`, `sdd-sync`, `sdd-archive`), the parent may validate inline by reading artifacts back and checking claims.
|
|
150
|
+
- For higher-risk phases (`sdd-design`, `sdd-apply`), run fresh-context validation/review before continuing because errors there compound downstream.
|
|
151
|
+
- If an inline gate finds any smell — missing artifact, status mismatch, unresolved path, likely drift, or critical risk — escalate to fresh-context validation before deciding.
|
|
152
|
+
|
|
153
|
+
On gate pass, continue automatically to the next phase. On gate fail, rerun the same phase exactly once with corrective feedback naming the specific failures. Validate the rerun. If it fails again, stop the automatic chain and report the phase, failures from both attempts, and the recommended fix. Never advance to dependent phases on a failed gate.
|
|
154
|
+
|
|
155
|
+
The gatekeeper is additive: it does not relax the Review Workload Guard, Strict TDD Forwarding, native status dependency checks, or mandatory delegation/review rules.
|
|
156
|
+
|
|
135
157
|
## SDD Phase Delegation Mode
|
|
136
158
|
|
|
137
159
|
Launch SDD phase subagents with `subagent_run` `mode: "task"` when the parent needs the phase result to route the next step. Do not use `mode: "background"` for SDD phases that must feed continuation; background completion is a notification/history mechanism, not an orchestration resume guarantee.
|
package/extensions/gentle-ai.ts
CHANGED
|
@@ -1004,14 +1004,16 @@ async function listBuiltinAgentNamesAsync(cwd: string): Promise<Set<string>> {
|
|
|
1004
1004
|
}
|
|
1005
1005
|
|
|
1006
1006
|
function listDiscoverableAgents(cwd: string): AgentEntry[] {
|
|
1007
|
-
const
|
|
1007
|
+
const globalAgentHome = gentlePiAgentHome();
|
|
1008
1008
|
const builtinDirs = builtinAgentDirs(cwd);
|
|
1009
1009
|
const agents = [
|
|
1010
1010
|
...builtinDirs.flatMap((dir) => listAgentsFromDir(dir, "builtin")),
|
|
1011
|
-
...listAgentsFromDir(
|
|
1011
|
+
...listAgentsFromDir(join(globalAgentHome, "agents"), "user"),
|
|
1012
|
+
...listAgentsFromDir(join(globalAgentHome, "subagents"), "user"),
|
|
1012
1013
|
...listAgentsFromDir(join(homedir(), ".agents"), "user"),
|
|
1013
1014
|
...listAgentsFromDir(join(cwd, ".agents"), "project"),
|
|
1014
1015
|
...listAgentsFromDir(join(cwd, ".pi", "agents"), "project"),
|
|
1016
|
+
...listAgentsFromDir(join(cwd, ".pi", "subagents"), "project"),
|
|
1015
1017
|
];
|
|
1016
1018
|
const byName = new Map<string, AgentEntry>();
|
|
1017
1019
|
for (const agent of agents) byName.set(agent.name, agent);
|
|
@@ -1019,17 +1021,19 @@ function listDiscoverableAgents(cwd: string): AgentEntry[] {
|
|
|
1019
1021
|
}
|
|
1020
1022
|
|
|
1021
1023
|
async function listDiscoverableAgentsAsync(cwd: string): Promise<AgentEntry[]> {
|
|
1022
|
-
const
|
|
1024
|
+
const globalAgentHome = gentlePiAgentHome();
|
|
1023
1025
|
const builtinDirs = builtinAgentDirs(cwd);
|
|
1024
1026
|
const agents: AgentEntry[] = [];
|
|
1025
1027
|
for (const dir of builtinDirs) {
|
|
1026
1028
|
agents.push(...(await listAgentsFromDirAsync(dir, "builtin")));
|
|
1027
1029
|
}
|
|
1028
1030
|
const otherDirs: Array<[string, AgentSource]> = [
|
|
1029
|
-
[
|
|
1031
|
+
[join(globalAgentHome, "agents"), "user"],
|
|
1032
|
+
[join(globalAgentHome, "subagents"), "user"],
|
|
1030
1033
|
[join(homedir(), ".agents"), "user"],
|
|
1031
1034
|
[join(cwd, ".agents"), "project"],
|
|
1032
1035
|
[join(cwd, ".pi", "agents"), "project"],
|
|
1036
|
+
[join(cwd, ".pi", "subagents"), "project"],
|
|
1033
1037
|
];
|
|
1034
1038
|
for (const [dir, source] of otherDirs) {
|
|
1035
1039
|
agents.push(...(await listAgentsFromDirAsync(dir, source)));
|
|
@@ -1049,92 +1053,179 @@ function orderDiscoverableAgents(agents: AgentEntry[]): AgentEntry[] {
|
|
|
1049
1053
|
return [...coreFirst, ...rest];
|
|
1050
1054
|
}
|
|
1051
1055
|
|
|
1052
|
-
function projectSettingsPath(cwd: string): string {
|
|
1053
|
-
return join(cwd, ".pi", "settings.json");
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
1056
|
function isClearRoutingEntry(entry: AgentRoutingEntry): boolean {
|
|
1057
1057
|
return entry.model === undefined && entry.thinking === undefined;
|
|
1058
1058
|
}
|
|
1059
1059
|
|
|
1060
|
-
function
|
|
1061
|
-
|
|
1060
|
+
function agentModelProfileConfigPath(cwd: string, source: AgentSource): string {
|
|
1061
|
+
return source === "project"
|
|
1062
|
+
? join(cwd, ".pi", "subagents.json")
|
|
1063
|
+
: join(gentlePiAgentHome(), "subagents.json");
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function modelProfileForRoutingEntry(
|
|
1067
|
+
entry: AgentRoutingEntry | undefined,
|
|
1068
|
+
): Record<string, string> | undefined {
|
|
1069
|
+
if (!entry || isClearRoutingEntry(entry)) return undefined;
|
|
1070
|
+
const profile: Record<string, string> = {};
|
|
1071
|
+
if (entry.model) profile.model = entry.model;
|
|
1072
|
+
if (entry.thinking) profile.effort = entry.thinking;
|
|
1073
|
+
return Object.keys(profile).length > 0 ? profile : undefined;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function updateSubagentModelProfileAtPath(
|
|
1077
|
+
path: string,
|
|
1062
1078
|
name: string,
|
|
1063
1079
|
entry: AgentRoutingEntry | undefined,
|
|
1080
|
+
options: { preserveExisting?: boolean } = {},
|
|
1064
1081
|
): boolean {
|
|
1065
|
-
|
|
1066
|
-
let settings: Record<string, unknown> = {};
|
|
1082
|
+
let config: Record<string, unknown> = {};
|
|
1067
1083
|
if (existsSync(path)) {
|
|
1068
1084
|
try {
|
|
1069
1085
|
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
1070
|
-
if (isRecord(parsed))
|
|
1086
|
+
if (isRecord(parsed)) config = { ...parsed };
|
|
1071
1087
|
} catch {
|
|
1072
|
-
|
|
1088
|
+
config = {};
|
|
1073
1089
|
}
|
|
1074
1090
|
}
|
|
1075
|
-
const
|
|
1076
|
-
? { ...
|
|
1077
|
-
: {};
|
|
1078
|
-
const agentOverrides = isRecord(subagents.agentOverrides)
|
|
1079
|
-
? { ...subagents.agentOverrides }
|
|
1080
|
-
: {};
|
|
1081
|
-
const current = isRecord(agentOverrides[name])
|
|
1082
|
-
? { ...agentOverrides[name] }
|
|
1091
|
+
const modelProfiles = isRecord(config.model_profiles)
|
|
1092
|
+
? { ...config.model_profiles }
|
|
1083
1093
|
: {};
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
subagents.agentOverrides = agentOverrides;
|
|
1092
|
-
else delete subagents.agentOverrides;
|
|
1093
|
-
if (Object.keys(subagents).length > 0) settings.subagents = subagents;
|
|
1094
|
-
else delete settings.subagents;
|
|
1094
|
+
const profile = modelProfileForRoutingEntry(entry);
|
|
1095
|
+
if (profile) {
|
|
1096
|
+
if (options.preserveExisting && isRecord(modelProfiles[name])) return false;
|
|
1097
|
+
modelProfiles[name] = profile;
|
|
1098
|
+
} else delete modelProfiles[name];
|
|
1099
|
+
if (Object.keys(modelProfiles).length > 0) config.model_profiles = modelProfiles;
|
|
1100
|
+
else delete config.model_profiles;
|
|
1095
1101
|
mkdirSync(dirname(path), { recursive: true });
|
|
1096
|
-
writeFileSync(path, `${JSON.stringify(
|
|
1102
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
1097
1103
|
return true;
|
|
1098
1104
|
}
|
|
1099
1105
|
|
|
1100
|
-
async function
|
|
1101
|
-
|
|
1106
|
+
async function updateSubagentModelProfileAtPathAsync(
|
|
1107
|
+
path: string,
|
|
1102
1108
|
name: string,
|
|
1103
1109
|
entry: AgentRoutingEntry | undefined,
|
|
1110
|
+
options: { preserveExisting?: boolean } = {},
|
|
1104
1111
|
): Promise<boolean> {
|
|
1105
|
-
|
|
1106
|
-
let settings: Record<string, unknown> = {};
|
|
1112
|
+
let config: Record<string, unknown> = {};
|
|
1107
1113
|
if (await pathExists(path)) {
|
|
1108
1114
|
try {
|
|
1109
1115
|
const parsed: unknown = JSON.parse(await readFile(path, "utf8"));
|
|
1110
|
-
if (isRecord(parsed))
|
|
1116
|
+
if (isRecord(parsed)) config = { ...parsed };
|
|
1111
1117
|
} catch {
|
|
1112
|
-
|
|
1118
|
+
config = {};
|
|
1113
1119
|
}
|
|
1114
1120
|
}
|
|
1121
|
+
const modelProfiles = isRecord(config.model_profiles)
|
|
1122
|
+
? { ...config.model_profiles }
|
|
1123
|
+
: {};
|
|
1124
|
+
const profile = modelProfileForRoutingEntry(entry);
|
|
1125
|
+
if (profile) {
|
|
1126
|
+
if (options.preserveExisting && isRecord(modelProfiles[name])) return false;
|
|
1127
|
+
modelProfiles[name] = profile;
|
|
1128
|
+
} else delete modelProfiles[name];
|
|
1129
|
+
if (Object.keys(modelProfiles).length > 0) config.model_profiles = modelProfiles;
|
|
1130
|
+
else delete config.model_profiles;
|
|
1131
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1132
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function updateSubagentModelProfile(
|
|
1137
|
+
cwd: string,
|
|
1138
|
+
source: AgentSource,
|
|
1139
|
+
name: string,
|
|
1140
|
+
entry: AgentRoutingEntry | undefined,
|
|
1141
|
+
options: { preserveExisting?: boolean } = {},
|
|
1142
|
+
): boolean {
|
|
1143
|
+
return updateSubagentModelProfileAtPath(
|
|
1144
|
+
agentModelProfileConfigPath(cwd, source),
|
|
1145
|
+
name,
|
|
1146
|
+
entry,
|
|
1147
|
+
options,
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function projectSettingsPath(cwd: string): string {
|
|
1152
|
+
return join(cwd, ".pi", "settings.json");
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function removeLegacyAgentOverridesFromSettings(
|
|
1156
|
+
settingsPath: string,
|
|
1157
|
+
settings: Record<string, unknown>,
|
|
1158
|
+
): void {
|
|
1115
1159
|
const subagents = isRecord(settings.subagents)
|
|
1116
1160
|
? { ...settings.subagents }
|
|
1117
|
-
:
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
: {};
|
|
1121
|
-
const current = isRecord(agentOverrides[name])
|
|
1122
|
-
? { ...agentOverrides[name] }
|
|
1123
|
-
: {};
|
|
1124
|
-
if (entry?.model === undefined) delete current.model;
|
|
1125
|
-
else current.model = entry.model;
|
|
1126
|
-
if (entry?.thinking === undefined) delete current.thinking;
|
|
1127
|
-
else current.thinking = entry.thinking;
|
|
1128
|
-
if (Object.keys(current).length > 0) agentOverrides[name] = current;
|
|
1129
|
-
else delete agentOverrides[name];
|
|
1130
|
-
if (Object.keys(agentOverrides).length > 0)
|
|
1131
|
-
subagents.agentOverrides = agentOverrides;
|
|
1132
|
-
else delete subagents.agentOverrides;
|
|
1161
|
+
: undefined;
|
|
1162
|
+
if (!subagents) return;
|
|
1163
|
+
delete subagents.agentOverrides;
|
|
1133
1164
|
if (Object.keys(subagents).length > 0) settings.subagents = subagents;
|
|
1134
1165
|
else delete settings.subagents;
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1166
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
1167
|
+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function isValidJsonObjectFileOrMissing(path: string): boolean {
|
|
1171
|
+
if (!existsSync(path)) return true;
|
|
1172
|
+
try {
|
|
1173
|
+
return isRecord(JSON.parse(readFileSync(path, "utf8")));
|
|
1174
|
+
} catch {
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function migrateLegacyProjectModelOverrides(cwd: string): number {
|
|
1180
|
+
const settingsPath = projectSettingsPath(cwd);
|
|
1181
|
+
if (!existsSync(settingsPath)) return 0;
|
|
1182
|
+
let settings: Record<string, unknown>;
|
|
1183
|
+
try {
|
|
1184
|
+
const parsed: unknown = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
1185
|
+
if (!isRecord(parsed)) return 0;
|
|
1186
|
+
settings = { ...parsed };
|
|
1187
|
+
} catch {
|
|
1188
|
+
return 0;
|
|
1189
|
+
}
|
|
1190
|
+
const subagents = isRecord(settings.subagents) ? settings.subagents : undefined;
|
|
1191
|
+
const agentOverrides = isRecord(subagents?.agentOverrides)
|
|
1192
|
+
? subagents.agentOverrides
|
|
1193
|
+
: undefined;
|
|
1194
|
+
if (!agentOverrides) return 0;
|
|
1195
|
+
const agentsByName = new Map(listDiscoverableAgents(cwd).map((agent) => [agent.name, agent]));
|
|
1196
|
+
const migratableEntries = Object.entries(agentOverrides)
|
|
1197
|
+
.map(([name, value]) => ({ name, entry: normalizeRoutingEntry(value) }))
|
|
1198
|
+
.filter((item): item is { name: string; entry: AgentRoutingEntry } =>
|
|
1199
|
+
item.entry !== undefined && !isClearRoutingEntry(item.entry),
|
|
1200
|
+
);
|
|
1201
|
+
const targetPaths = new Set(
|
|
1202
|
+
migratableEntries.map(({ name }) =>
|
|
1203
|
+
agentModelProfileConfigPath(cwd, agentsByName.get(name)?.source ?? "project"),
|
|
1204
|
+
),
|
|
1205
|
+
);
|
|
1206
|
+
if (![...targetPaths].every(isValidJsonObjectFileOrMissing)) return 0;
|
|
1207
|
+
let migrated = 0;
|
|
1208
|
+
for (const { name, entry } of migratableEntries) {
|
|
1209
|
+
const source = agentsByName.get(name)?.source ?? "project";
|
|
1210
|
+
if (updateSubagentModelProfile(cwd, source, name, entry, { preserveExisting: true })) migrated += 1;
|
|
1211
|
+
}
|
|
1212
|
+
removeLegacyAgentOverridesFromSettings(settingsPath, settings);
|
|
1213
|
+
return migrated;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
async function updateSubagentModelProfileAsync(
|
|
1217
|
+
cwd: string,
|
|
1218
|
+
source: AgentSource,
|
|
1219
|
+
name: string,
|
|
1220
|
+
entry: AgentRoutingEntry | undefined,
|
|
1221
|
+
options: { preserveExisting?: boolean } = {},
|
|
1222
|
+
): Promise<boolean> {
|
|
1223
|
+
return updateSubagentModelProfileAtPathAsync(
|
|
1224
|
+
agentModelProfileConfigPath(cwd, source),
|
|
1225
|
+
name,
|
|
1226
|
+
entry,
|
|
1227
|
+
options,
|
|
1228
|
+
);
|
|
1138
1229
|
}
|
|
1139
1230
|
|
|
1140
1231
|
export function applyModelConfig(
|
|
@@ -1143,28 +1234,17 @@ export function applyModelConfig(
|
|
|
1143
1234
|
): { updated: number; skipped: number } {
|
|
1144
1235
|
let updated = 0;
|
|
1145
1236
|
let skipped = 0;
|
|
1146
|
-
const builtinNames = listBuiltinAgentNames(cwd);
|
|
1147
1237
|
const seenAgents = new Set<string>();
|
|
1148
1238
|
for (const agent of listDiscoverableAgents(cwd)) {
|
|
1149
1239
|
seenAgents.add(agent.name);
|
|
1150
1240
|
const entry = config[agent.name];
|
|
1151
|
-
if (agent.source === "builtin") {
|
|
1152
|
-
if (entry === undefined) {
|
|
1153
|
-
skipped += 1;
|
|
1154
|
-
continue;
|
|
1155
|
-
}
|
|
1156
|
-
if (updateBuiltinModelOverride(cwd, agent.name, entry)) updated += 1;
|
|
1157
|
-
else skipped += 1;
|
|
1158
|
-
continue;
|
|
1159
|
-
}
|
|
1160
1241
|
if (entry === undefined) {
|
|
1161
1242
|
skipped += 1;
|
|
1162
1243
|
continue;
|
|
1163
1244
|
}
|
|
1164
|
-
if (
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
}
|
|
1245
|
+
if (updateSubagentModelProfile(cwd, agent.source, agent.name, entry)) updated += 1;
|
|
1246
|
+
else skipped += 1;
|
|
1247
|
+
if (agent.source === "builtin") continue;
|
|
1168
1248
|
if (!agent.filePath || !existsSync(agent.filePath)) {
|
|
1169
1249
|
skipped += 1;
|
|
1170
1250
|
continue;
|
|
@@ -1180,7 +1260,7 @@ export function applyModelConfig(
|
|
|
1180
1260
|
}
|
|
1181
1261
|
for (const [name, entry] of Object.entries(config)) {
|
|
1182
1262
|
if (!seenAgents.has(name) && isClearRoutingEntry(entry)) {
|
|
1183
|
-
if (
|
|
1263
|
+
if (updateSubagentModelProfile(cwd, "user", name, entry)) updated += 1;
|
|
1184
1264
|
else skipped += 1;
|
|
1185
1265
|
}
|
|
1186
1266
|
}
|
|
@@ -1193,30 +1273,18 @@ export async function applyModelConfigAsync(
|
|
|
1193
1273
|
): Promise<{ updated: number; skipped: number }> {
|
|
1194
1274
|
let updated = 0;
|
|
1195
1275
|
let skipped = 0;
|
|
1196
|
-
const builtinNames = await listBuiltinAgentNamesAsync(cwd);
|
|
1197
1276
|
const seenAgents = new Set<string>();
|
|
1198
1277
|
for (const agent of await listDiscoverableAgentsAsync(cwd)) {
|
|
1199
1278
|
seenAgents.add(agent.name);
|
|
1200
1279
|
const entry = config[agent.name];
|
|
1201
|
-
if (agent.source === "builtin") {
|
|
1202
|
-
if (entry === undefined) {
|
|
1203
|
-
skipped += 1;
|
|
1204
|
-
continue;
|
|
1205
|
-
}
|
|
1206
|
-
if (await updateBuiltinModelOverrideAsync(cwd, agent.name, entry))
|
|
1207
|
-
updated += 1;
|
|
1208
|
-
else skipped += 1;
|
|
1209
|
-
continue;
|
|
1210
|
-
}
|
|
1211
1280
|
if (entry === undefined) {
|
|
1212
1281
|
skipped += 1;
|
|
1213
1282
|
continue;
|
|
1214
1283
|
}
|
|
1215
|
-
if (
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
}
|
|
1284
|
+
if (await updateSubagentModelProfileAsync(cwd, agent.source, agent.name, entry))
|
|
1285
|
+
updated += 1;
|
|
1286
|
+
else skipped += 1;
|
|
1287
|
+
if (agent.source === "builtin") continue;
|
|
1220
1288
|
if (!agent.filePath || !(await pathExists(agent.filePath))) {
|
|
1221
1289
|
skipped += 1;
|
|
1222
1290
|
continue;
|
|
@@ -1232,7 +1300,7 @@ export async function applyModelConfigAsync(
|
|
|
1232
1300
|
}
|
|
1233
1301
|
for (const [name, entry] of Object.entries(config)) {
|
|
1234
1302
|
if (!seenAgents.has(name) && isClearRoutingEntry(entry)) {
|
|
1235
|
-
if (await
|
|
1303
|
+
if (await updateSubagentModelProfileAsync(cwd, "user", name, entry))
|
|
1236
1304
|
updated += 1;
|
|
1237
1305
|
else skipped += 1;
|
|
1238
1306
|
}
|
|
@@ -1785,6 +1853,7 @@ async function showSddModelPanel(
|
|
|
1785
1853
|
}
|
|
1786
1854
|
|
|
1787
1855
|
async function handleModelsCommand(ctx: ExtensionContext): Promise<void> {
|
|
1856
|
+
migrateLegacyProjectModelOverrides(ctx.cwd);
|
|
1788
1857
|
const savedConfig = await readSavedModelConfigAsync(ctx.cwd);
|
|
1789
1858
|
if (savedConfig.status === "invalid") {
|
|
1790
1859
|
ctx.ui.notify(
|
|
@@ -2078,6 +2147,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2078
2147
|
pi.on("session_start", async (_event, ctx) => {
|
|
2079
2148
|
try {
|
|
2080
2149
|
const installResult = installSddAssets(ctx.cwd, true);
|
|
2150
|
+
migrateLegacyProjectModelOverrides(ctx.cwd);
|
|
2081
2151
|
const modelResult = await applySavedModelConfig(ctx);
|
|
2082
2152
|
if (ctx.hasUI && modelResult.invalidPath) {
|
|
2083
2153
|
ctx.ui.notify(
|
|
@@ -2153,7 +2223,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2153
2223
|
return confirmCommand(event.input.command, ctx);
|
|
2154
2224
|
});
|
|
2155
2225
|
|
|
2156
|
-
pi.registerCommand("gentle
|
|
2226
|
+
pi.registerCommand("gentle:install-sdd", {
|
|
2157
2227
|
description:
|
|
2158
2228
|
"Repair or refresh global Gentle AI SDD subagent and chain assets.",
|
|
2159
2229
|
handler: async (args, ctx) => {
|
|
@@ -2166,7 +2236,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2166
2236
|
},
|
|
2167
2237
|
});
|
|
2168
2238
|
|
|
2169
|
-
pi.registerCommand("gentle
|
|
2239
|
+
pi.registerCommand("gentle:sdd-preflight", {
|
|
2170
2240
|
description:
|
|
2171
2241
|
"Run or reuse the lazy SDD preflight for this Pi session.",
|
|
2172
2242
|
handler: async (_args, ctx) => {
|
|
@@ -2174,13 +2244,6 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2174
2244
|
},
|
|
2175
2245
|
});
|
|
2176
2246
|
|
|
2177
|
-
pi.registerCommand("gentle:sdd-preflight", {
|
|
2178
|
-
description: "Compatibility alias for /gentle-ai:sdd-preflight.",
|
|
2179
|
-
handler: async (_args, ctx) => {
|
|
2180
|
-
await runSddPreflight(ctx);
|
|
2181
|
-
},
|
|
2182
|
-
});
|
|
2183
|
-
|
|
2184
2247
|
const handleSddStatusCommand = (args: string, ctx: ExtensionContext) => {
|
|
2185
2248
|
const parsed = parseSddStatusCommandArgs(args);
|
|
2186
2249
|
const status = resolveSddStatus({
|
|
@@ -2202,13 +2265,6 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2202
2265
|
},
|
|
2203
2266
|
});
|
|
2204
2267
|
|
|
2205
|
-
pi.registerCommand("gentle-ai:sdd-status", {
|
|
2206
|
-
description: "Compatibility alias for /sdd-status.",
|
|
2207
|
-
handler: async (args, ctx) => {
|
|
2208
|
-
handleSddStatusCommand(args, ctx);
|
|
2209
|
-
},
|
|
2210
|
-
});
|
|
2211
|
-
|
|
2212
2268
|
const handleSddContinueCommand = (args: string, ctx: ExtensionContext) => {
|
|
2213
2269
|
const parsed = parseSddStatusCommandArgs(args);
|
|
2214
2270
|
const status = resolveSddStatus({
|
|
@@ -2230,13 +2286,6 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2230
2286
|
},
|
|
2231
2287
|
});
|
|
2232
2288
|
|
|
2233
|
-
pi.registerCommand("gentle-ai:sdd-continue", {
|
|
2234
|
-
description: "Compatibility alias for /sdd-continue.",
|
|
2235
|
-
handler: async (args, ctx) => {
|
|
2236
|
-
handleSddContinueCommand(args, ctx);
|
|
2237
|
-
},
|
|
2238
|
-
});
|
|
2239
|
-
|
|
2240
2289
|
pi.registerCommand("gentle:models", {
|
|
2241
2290
|
description: "Configure global per-agent models for el Gentleman.",
|
|
2242
2291
|
handler: async (_args, ctx) => {
|
|
@@ -2244,20 +2293,6 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2244
2293
|
},
|
|
2245
2294
|
});
|
|
2246
2295
|
|
|
2247
|
-
pi.registerCommand("gentle-ai:models", {
|
|
2248
|
-
description: "Compatibility alias for /gentle:models.",
|
|
2249
|
-
handler: async (_args, ctx) => {
|
|
2250
|
-
await handleModelsCommand(ctx);
|
|
2251
|
-
},
|
|
2252
|
-
});
|
|
2253
|
-
|
|
2254
|
-
pi.registerCommand("gentleman:models", {
|
|
2255
|
-
description: "Compatibility alias for /gentle:models.",
|
|
2256
|
-
handler: async (_args, ctx) => {
|
|
2257
|
-
await handleModelsCommand(ctx);
|
|
2258
|
-
},
|
|
2259
|
-
});
|
|
2260
|
-
|
|
2261
2296
|
pi.registerCommand("gentle:persona", {
|
|
2262
2297
|
description: "Switch el Gentleman persona between gentleman and neutral.",
|
|
2263
2298
|
handler: async (_args, ctx) => {
|
|
@@ -2265,21 +2300,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2265
2300
|
},
|
|
2266
2301
|
});
|
|
2267
2302
|
|
|
2268
|
-
pi.registerCommand("gentle
|
|
2269
|
-
description: "Compatibility alias for /gentle:persona.",
|
|
2270
|
-
handler: async (_args, ctx) => {
|
|
2271
|
-
await handlePersonaCommand(ctx);
|
|
2272
|
-
},
|
|
2273
|
-
});
|
|
2274
|
-
|
|
2275
|
-
pi.registerCommand("gentleman:persona", {
|
|
2276
|
-
description: "Compatibility alias for /gentle:persona.",
|
|
2277
|
-
handler: async (_args, ctx) => {
|
|
2278
|
-
await handlePersonaCommand(ctx);
|
|
2279
|
-
},
|
|
2280
|
-
});
|
|
2281
|
-
|
|
2282
|
-
pi.registerCommand("gentle-ai:doctor", {
|
|
2303
|
+
pi.registerCommand("gentle:doctor", {
|
|
2283
2304
|
description: "Run read-only Gentle AI diagnostics for this Pi workspace.",
|
|
2284
2305
|
handler: async (_args, ctx) => {
|
|
2285
2306
|
const agentsInstalled = existsSync(
|
|
@@ -2311,7 +2332,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2311
2332
|
`${engramActive ? "pass" : "warn"}: Engram memory tools ${engramActive ? "active" : "not active in this session"}`,
|
|
2312
2333
|
];
|
|
2313
2334
|
if (!agentsInstalled || !chainsInstalled) {
|
|
2314
|
-
lines.push("remedy: run /gentle
|
|
2335
|
+
lines.push("remedy: run /gentle:install-sdd --force to refresh global SDD assets intentionally");
|
|
2315
2336
|
}
|
|
2316
2337
|
if (modelConfig.status === "invalid") {
|
|
2317
2338
|
lines.push(`remedy: fix or remove ${modelConfig.path}`);
|
|
@@ -2326,7 +2347,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2326
2347
|
},
|
|
2327
2348
|
});
|
|
2328
2349
|
|
|
2329
|
-
pi.registerCommand("gentle
|
|
2350
|
+
pi.registerCommand("gentle:status", {
|
|
2330
2351
|
description: "Show Gentle AI package status for this project.",
|
|
2331
2352
|
handler: async (_args, ctx) => {
|
|
2332
2353
|
const agentsInstalled = existsSync(
|
|
@@ -2349,7 +2370,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2349
2370
|
`Global SDD chains: ${chainsInstalled ? "installed" : "not installed"}`,
|
|
2350
2371
|
`Global SDD assets stale: ${staleSddAssets} file(s)${
|
|
2351
2372
|
staleSddAssets > 0
|
|
2352
|
-
? " — run /gentle
|
|
2373
|
+
? " — run /gentle:install-sdd --force to refresh intentionally"
|
|
2353
2374
|
: ""
|
|
2354
2375
|
}`,
|
|
2355
2376
|
`Project-local SDD agent overrides: ${localSddAgentOverrides} file(s)${
|
|
@@ -585,12 +585,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
585
585
|
},
|
|
586
586
|
});
|
|
587
587
|
};
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
registerColorCommand(`${prefix}:banner-color`);
|
|
593
|
-
}
|
|
588
|
+
registerBannerCommand("gentle:banner");
|
|
589
|
+
registerToggleCommand("gentle:toggle-rose", "showRose");
|
|
590
|
+
registerToggleCommand("gentle:toggle-text-logo", "showTextLogo");
|
|
591
|
+
registerColorCommand("gentle:banner-color");
|
|
594
592
|
|
|
595
593
|
pi.on("session_start", async (_event, ctx) => {
|
|
596
594
|
if (!ctx.hasUI) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
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",
|
|
@@ -69,4 +69,4 @@ Never request a subagent named `reviewer`; it is an intent, not an installed age
|
|
|
69
69
|
|
|
70
70
|
If multiple rows match, run the narrow set that covers the risk. Example: shell integration that mutates live state should use `review-reliability` plus `review-resilience`, not `review-readability` by default.
|
|
71
71
|
|
|
72
|
-
The package ensures SDD agents and chains are available as global Pi runtime assets. Project-local SDD files are overrides/debug copies only. Use `/gentle
|
|
72
|
+
The package ensures SDD agents and chains are available as global Pi runtime assets. Project-local SDD files are overrides/debug copies only. Use `/gentle:install-sdd --force` only for recovery or intentional global refresh.
|
|
@@ -22,31 +22,37 @@ const EXPECTED_BANNER_COMMANDS = [
|
|
|
22
22
|
"gentle:toggle-rose",
|
|
23
23
|
"gentle:toggle-text-logo",
|
|
24
24
|
"gentle:banner-color",
|
|
25
|
-
"gentle-ai:banner",
|
|
26
|
-
"gentle-ai:toggle-rose",
|
|
27
|
-
"gentle-ai:toggle-text-logo",
|
|
28
|
-
"gentle-ai:banner-color",
|
|
29
25
|
];
|
|
30
26
|
|
|
31
27
|
const EXPECTED_COMMANDS = [
|
|
32
|
-
"gentle
|
|
33
|
-
"gentle-ai:sdd-preflight",
|
|
28
|
+
"gentle:install-sdd",
|
|
34
29
|
"gentle:sdd-preflight",
|
|
35
30
|
"sdd-status",
|
|
36
|
-
"gentle-ai:sdd-status",
|
|
37
31
|
"sdd-continue",
|
|
38
|
-
"gentle-ai:sdd-continue",
|
|
39
32
|
"gentle:models",
|
|
33
|
+
"gentle:persona",
|
|
34
|
+
"gentle:status",
|
|
35
|
+
"gentle:doctor",
|
|
36
|
+
"sdd-init",
|
|
37
|
+
"skill-registry:refresh",
|
|
38
|
+
...EXPECTED_BANNER_COMMANDS,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const FORBIDDEN_COMPAT_COMMANDS = [
|
|
42
|
+
"gentle-ai:install-sdd",
|
|
43
|
+
"gentle-ai:sdd-preflight",
|
|
44
|
+
"gentle-ai:sdd-status",
|
|
45
|
+
"gentle-ai:sdd-continue",
|
|
40
46
|
"gentle-ai:models",
|
|
41
47
|
"gentleman:models",
|
|
42
|
-
"gentle:persona",
|
|
43
48
|
"gentle-ai:persona",
|
|
44
49
|
"gentleman:persona",
|
|
45
50
|
"gentle-ai:status",
|
|
46
51
|
"gentle-ai:doctor",
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
52
|
+
"gentle-ai:banner",
|
|
53
|
+
"gentle-ai:toggle-rose",
|
|
54
|
+
"gentle-ai:toggle-text-logo",
|
|
55
|
+
"gentle-ai:banner-color",
|
|
50
56
|
];
|
|
51
57
|
|
|
52
58
|
function createPi() {
|
|
@@ -161,12 +167,16 @@ async function run() {
|
|
|
161
167
|
process.env.GENTLE_PI_CONFIG_HOME = globalConfigHome;
|
|
162
168
|
process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
|
|
163
169
|
const globalModelsPath = join(globalConfigHome, "models.json");
|
|
170
|
+
const globalSubagentsPath = join(globalAgentHome, "subagents.json");
|
|
164
171
|
const { pi, hooks, commands, flags } = createPi();
|
|
165
172
|
await loadExtensions(pi);
|
|
166
173
|
|
|
167
174
|
for (const name of EXPECTED_COMMANDS) {
|
|
168
175
|
assert.ok(commands.has(name), `missing command ${name}`);
|
|
169
176
|
}
|
|
177
|
+
for (const name of FORBIDDEN_COMPAT_COMMANDS) {
|
|
178
|
+
assert.equal(commands.has(name), false, `compat command should not be registered: ${name}`);
|
|
179
|
+
}
|
|
170
180
|
assert.ok(flags.has("no-skill-registry"), "missing no-skill-registry flag");
|
|
171
181
|
assert.ok(hooks.has("session_start"), "missing session_start hook");
|
|
172
182
|
assert.ok(hooks.has("session_shutdown"), "missing session_shutdown hook");
|
|
@@ -313,7 +323,7 @@ async function run() {
|
|
|
313
323
|
assert.equal(bannerConfig.showRose, false);
|
|
314
324
|
assert.equal(bannerConfig.showTextLogo, true);
|
|
315
325
|
assert.equal(bannerConfig.color, "pink");
|
|
316
|
-
await commands.get("gentle
|
|
326
|
+
await commands.get("gentle:toggle-text-logo").handler("", ctx);
|
|
317
327
|
bannerConfig = JSON.parse(await readFile(join(globalConfigHome, "banner.json"), "utf8"));
|
|
318
328
|
assert.equal(bannerConfig.showTextLogo, false);
|
|
319
329
|
await commands.get("gentle:banner-color").handler("cyan", ctx);
|
|
@@ -500,7 +510,7 @@ async function run() {
|
|
|
500
510
|
assert.equal(ctx.ui.selections.length, 3);
|
|
501
511
|
assert.deepEqual(ctx.ui.selections[1].options, ["openspec"]);
|
|
502
512
|
assert.match(ctx.ui.notifications.at(-1).message, /SDD preflight complete/);
|
|
503
|
-
await commands.get("gentle
|
|
513
|
+
await commands.get("gentle:status").handler("", ctx);
|
|
504
514
|
assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets stale: 0 file\(s\)/);
|
|
505
515
|
assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /install-sdd --force/);
|
|
506
516
|
|
|
@@ -543,7 +553,7 @@ async function run() {
|
|
|
543
553
|
const commandSddCwd = await tempWorkspace();
|
|
544
554
|
try {
|
|
545
555
|
const ctx = createCtx(commandSddCwd, true, "command-session");
|
|
546
|
-
await commands.get("gentle
|
|
556
|
+
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
547
557
|
assert.equal(existsSync(join(commandSddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
548
558
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
549
559
|
assert.equal(ctx.ui.selections.length, 3);
|
|
@@ -622,7 +632,7 @@ async function run() {
|
|
|
622
632
|
try {
|
|
623
633
|
await writeFile(globalModelsPath, "{ invalid json");
|
|
624
634
|
const ctx = createCtx(invalidPreflightCwd, true, "invalid-preflight-session");
|
|
625
|
-
await commands.get("gentle
|
|
635
|
+
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
626
636
|
assert.equal(ctx.ui.notifications.at(-1).level, "warning");
|
|
627
637
|
assert.match(ctx.ui.notifications.at(-1).message, /Model routing skipped:/);
|
|
628
638
|
assert.match(ctx.ui.notifications.at(-1).message, /invalid JSON or not an object/);
|
|
@@ -635,7 +645,7 @@ async function run() {
|
|
|
635
645
|
try {
|
|
636
646
|
pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
|
|
637
647
|
const ctx = createCtx(engramSddCwd, true, "engram-session");
|
|
638
|
-
await commands.get("gentle
|
|
648
|
+
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
639
649
|
assert.deepEqual(ctx.ui.selections[1].options, ["openspec", "engram", "both"]);
|
|
640
650
|
} finally {
|
|
641
651
|
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
@@ -646,7 +656,7 @@ async function run() {
|
|
|
646
656
|
try {
|
|
647
657
|
pi.setActiveTools(["read", "bash", "edit", "write", "engram_mem_save"]);
|
|
648
658
|
const ctx = createCtx(directEngramToolCwd, true, "direct-engram-session");
|
|
649
|
-
await commands.get("gentle
|
|
659
|
+
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
650
660
|
assert.deepEqual(ctx.ui.selections[1].options, ["openspec"]);
|
|
651
661
|
} finally {
|
|
652
662
|
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
@@ -656,7 +666,7 @@ async function run() {
|
|
|
656
666
|
const installCwd = await tempWorkspace();
|
|
657
667
|
try {
|
|
658
668
|
const ctx = createCtx(installCwd, true);
|
|
659
|
-
await commands.get("gentle
|
|
669
|
+
await commands.get("gentle:install-sdd").handler("", ctx);
|
|
660
670
|
assert.match(ctx.ui.notifications.at(-1).message, /Global Gentle AI SDD assets installed/);
|
|
661
671
|
assert.equal(existsSync(join(installCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
662
672
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
@@ -677,17 +687,17 @@ async function run() {
|
|
|
677
687
|
await writeFile(join(staleAssetsCwd, ".pi", "chains", "sdd-full.chain.md"), "stale chain\n");
|
|
678
688
|
await writeFile(join(staleAssetsCwd, ".pi", "gentle-ai", "support", "sdd-status-contract.md"), "stale status contract\n");
|
|
679
689
|
const ctx = createCtx(staleAssetsCwd, true);
|
|
680
|
-
await commands.get("gentle
|
|
690
|
+
await commands.get("gentle:status").handler("", ctx);
|
|
681
691
|
assert.match(ctx.ui.notifications.at(-1).message, /Project-local SDD agent overrides: 2 file\(s\)/);
|
|
682
692
|
assert.match(ctx.ui.notifications.at(-1).message, /local SDD agents shadow package assets/);
|
|
683
|
-
await commands.get("gentle
|
|
693
|
+
await commands.get("gentle:doctor").handler("", ctx);
|
|
684
694
|
assert.match(ctx.ui.notifications.at(-1).message, /el Gentleman doctor/);
|
|
685
695
|
assert.match(ctx.ui.notifications.at(-1).message, /Sensitive-path guard active/);
|
|
686
696
|
pi.setActiveTools([{ name: "engram.mem_save" }]);
|
|
687
|
-
await commands.get("gentle
|
|
697
|
+
await commands.get("gentle:doctor").handler("", ctx);
|
|
688
698
|
assert.match(ctx.ui.notifications.at(-1).message, /Engram memory tools active/);
|
|
689
699
|
pi.setActiveTools([{ name: "engram_mem_save" }]);
|
|
690
|
-
await commands.get("gentle
|
|
700
|
+
await commands.get("gentle:doctor").handler("", ctx);
|
|
691
701
|
assert.match(ctx.ui.notifications.at(-1).message, /Engram memory tools not active in this session/);
|
|
692
702
|
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
693
703
|
} finally {
|
|
@@ -709,7 +719,7 @@ async function run() {
|
|
|
709
719
|
assert.match(ctx.ui.notifications[0].message, /SDD preflight complete/);
|
|
710
720
|
assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
|
|
711
721
|
|
|
712
|
-
await commands.get("gentle
|
|
722
|
+
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
713
723
|
assert.equal(ctx.ui.selections.length, 3, "/sdd-init preflight should be reused by later manual preflight");
|
|
714
724
|
} finally {
|
|
715
725
|
await rm(sddCwd, { recursive: true, force: true });
|
|
@@ -789,20 +799,6 @@ async function run() {
|
|
|
789
799
|
assert.equal(await readFile(globalModelsPath, "utf8"), "{ invalid json");
|
|
790
800
|
assert.equal(legacyCtx.ui.notifications.at(-1).level, "warning");
|
|
791
801
|
assert.match(legacyCtx.ui.notifications.at(-1).message, /cannot open model config/);
|
|
792
|
-
await writeFile(
|
|
793
|
-
join(legacyModelsCwd, ".pi", "settings.json"),
|
|
794
|
-
JSON.stringify(
|
|
795
|
-
{
|
|
796
|
-
subagents: {
|
|
797
|
-
agentOverrides: {
|
|
798
|
-
"sdd-apply": { model: "settings/provider-model", thinking: "high" },
|
|
799
|
-
},
|
|
800
|
-
},
|
|
801
|
-
},
|
|
802
|
-
null,
|
|
803
|
-
2,
|
|
804
|
-
),
|
|
805
|
-
);
|
|
806
802
|
await writeFile(globalModelsPath, JSON.stringify({}, null, 2));
|
|
807
803
|
await hooks.get("session_start")[0]({ reason: "startup" }, legacyCtx);
|
|
808
804
|
const emptyGlobalPreservesAgent = await readFile(
|
|
@@ -810,12 +806,12 @@ async function run() {
|
|
|
810
806
|
"utf8",
|
|
811
807
|
);
|
|
812
808
|
assert.match(emptyGlobalPreservesAgent, /model: global\/provider-model/);
|
|
813
|
-
const
|
|
814
|
-
await readFile(join(legacyModelsCwd, ".pi", "
|
|
809
|
+
const emptyGlobalPreservesProfiles = JSON.parse(
|
|
810
|
+
await readFile(join(legacyModelsCwd, ".pi", "subagents.json"), "utf8"),
|
|
815
811
|
);
|
|
816
812
|
assert.equal(
|
|
817
|
-
|
|
818
|
-
"
|
|
813
|
+
emptyGlobalPreservesProfiles.model_profiles["sdd-apply"].model,
|
|
814
|
+
"global/provider-model",
|
|
819
815
|
);
|
|
820
816
|
await writeFile(
|
|
821
817
|
globalModelsPath,
|
|
@@ -827,12 +823,12 @@ async function run() {
|
|
|
827
823
|
"utf8",
|
|
828
824
|
);
|
|
829
825
|
assert.match(invalidEntryPreservesAgent, /model: global\/provider-model/);
|
|
830
|
-
const
|
|
831
|
-
await readFile(join(legacyModelsCwd, ".pi", "
|
|
826
|
+
const invalidEntryPreservesProfiles = JSON.parse(
|
|
827
|
+
await readFile(join(legacyModelsCwd, ".pi", "subagents.json"), "utf8"),
|
|
832
828
|
);
|
|
833
829
|
assert.equal(
|
|
834
|
-
|
|
835
|
-
"
|
|
830
|
+
invalidEntryPreservesProfiles.model_profiles["sdd-apply"].model,
|
|
831
|
+
"global/provider-model",
|
|
836
832
|
);
|
|
837
833
|
await writeFile(globalModelsPath, JSON.stringify({ "sdd-apply": {} }, null, 2));
|
|
838
834
|
await hooks.get("session_start")[0]({ reason: "startup" }, legacyCtx);
|
|
@@ -841,10 +837,10 @@ async function run() {
|
|
|
841
837
|
"utf8",
|
|
842
838
|
);
|
|
843
839
|
assert.doesNotMatch(explicitInheritClearsAgent, /model:/);
|
|
844
|
-
const
|
|
845
|
-
await readFile(join(legacyModelsCwd, ".pi", "
|
|
840
|
+
const explicitInheritClearsProfiles = JSON.parse(
|
|
841
|
+
await readFile(join(legacyModelsCwd, ".pi", "subagents.json"), "utf8"),
|
|
846
842
|
);
|
|
847
|
-
assert.equal(
|
|
843
|
+
assert.equal(explicitInheritClearsProfiles.model_profiles, undefined);
|
|
848
844
|
} finally {
|
|
849
845
|
await rm(legacyModelsCwd, { recursive: true, force: true });
|
|
850
846
|
await rm(globalModelsPath, { force: true });
|
|
@@ -852,15 +848,12 @@ async function run() {
|
|
|
852
848
|
|
|
853
849
|
const staleSettingsOnlyCwd = await tempWorkspace();
|
|
854
850
|
try {
|
|
855
|
-
await mkdir(join(staleSettingsOnlyCwd, ".pi"), { recursive: true });
|
|
856
851
|
await writeFile(
|
|
857
|
-
|
|
852
|
+
globalSubagentsPath,
|
|
858
853
|
JSON.stringify(
|
|
859
854
|
{
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
worker: { model: "stale/model", thinking: "high" },
|
|
863
|
-
},
|
|
855
|
+
model_profiles: {
|
|
856
|
+
worker: { model: "stale/model", effort: "high" },
|
|
864
857
|
},
|
|
865
858
|
},
|
|
866
859
|
null,
|
|
@@ -869,18 +862,126 @@ async function run() {
|
|
|
869
862
|
);
|
|
870
863
|
await writeFile(globalModelsPath, JSON.stringify({ worker: {} }, null, 2));
|
|
871
864
|
await hooks.get("session_start")[0]({ reason: "startup" }, createCtx(staleSettingsOnlyCwd, true));
|
|
872
|
-
const
|
|
873
|
-
await readFile(
|
|
865
|
+
const staleOnlyClearedProfiles = JSON.parse(
|
|
866
|
+
await readFile(globalSubagentsPath, "utf8"),
|
|
874
867
|
);
|
|
875
|
-
assert.equal(
|
|
868
|
+
assert.equal(staleOnlyClearedProfiles.model_profiles, undefined);
|
|
876
869
|
} finally {
|
|
877
870
|
await rm(staleSettingsOnlyCwd, { recursive: true, force: true });
|
|
878
871
|
await rm(globalModelsPath, { force: true });
|
|
872
|
+
await rm(globalSubagentsPath, { force: true });
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const legacySettingsMigrationCwd = await tempWorkspace();
|
|
876
|
+
try {
|
|
877
|
+
await mkdir(join(legacySettingsMigrationCwd, ".pi", "subagents"), { recursive: true });
|
|
878
|
+
await mkdir(join(globalAgentHome, "subagents"), { recursive: true });
|
|
879
|
+
await writeFile(
|
|
880
|
+
join(legacySettingsMigrationCwd, ".pi", "subagents", "local-worker.md"),
|
|
881
|
+
`---\nname: local-worker\ndescription: Local worker\n---\n`,
|
|
882
|
+
);
|
|
883
|
+
await writeFile(
|
|
884
|
+
join(legacySettingsMigrationCwd, ".pi", "subagents", "local-new.md"),
|
|
885
|
+
`---\nname: local-new\ndescription: Local new worker\n---\n`,
|
|
886
|
+
);
|
|
887
|
+
await writeFile(
|
|
888
|
+
join(globalAgentHome, "subagents", "global-worker.md"),
|
|
889
|
+
`---\nname: global-worker\ndescription: Global worker\n---\n`,
|
|
890
|
+
);
|
|
891
|
+
await writeFile(
|
|
892
|
+
join(globalAgentHome, "subagents", "global-new.md"),
|
|
893
|
+
`---\nname: global-new\ndescription: Global new worker\n---\n`,
|
|
894
|
+
);
|
|
895
|
+
await writeFile(
|
|
896
|
+
join(legacySettingsMigrationCwd, ".pi", "subagents.json"),
|
|
897
|
+
JSON.stringify({ model_profiles: { "local-worker": { model: "existing/local", effort: "medium" } } }, null, 2),
|
|
898
|
+
);
|
|
899
|
+
await writeFile(
|
|
900
|
+
globalSubagentsPath,
|
|
901
|
+
JSON.stringify({ model_profiles: { "global-worker": { model: "existing/global", effort: "medium" } } }, null, 2),
|
|
902
|
+
);
|
|
903
|
+
await writeFile(
|
|
904
|
+
join(legacySettingsMigrationCwd, ".pi", "settings.json"),
|
|
905
|
+
JSON.stringify(
|
|
906
|
+
{
|
|
907
|
+
theme: "keep-me",
|
|
908
|
+
subagents: {
|
|
909
|
+
history: "keep-me-too",
|
|
910
|
+
agentOverrides: {
|
|
911
|
+
"local-worker": {},
|
|
912
|
+
"local-new": { model: "local/new-model", thinking: "minimal" },
|
|
913
|
+
"global-worker": {},
|
|
914
|
+
"global-new": { model: "global/new-model", thinking: "xhigh" },
|
|
915
|
+
"unknown-project": { model: "unknown/project-model", thinking: "low" },
|
|
916
|
+
},
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
null,
|
|
920
|
+
2,
|
|
921
|
+
),
|
|
922
|
+
);
|
|
923
|
+
await hooks.get("session_start")[0]({ reason: "startup" }, createCtx(legacySettingsMigrationCwd, true));
|
|
924
|
+
const migratedProjectProfiles = JSON.parse(
|
|
925
|
+
await readFile(join(legacySettingsMigrationCwd, ".pi", "subagents.json"), "utf8"),
|
|
926
|
+
);
|
|
927
|
+
assert.equal(migratedProjectProfiles.model_profiles["local-worker"].model, "existing/local");
|
|
928
|
+
assert.equal(migratedProjectProfiles.model_profiles["local-worker"].effort, "medium");
|
|
929
|
+
assert.equal(migratedProjectProfiles.model_profiles["local-new"].model, "local/new-model");
|
|
930
|
+
assert.equal(migratedProjectProfiles.model_profiles["local-new"].effort, "minimal");
|
|
931
|
+
assert.equal(migratedProjectProfiles.model_profiles["unknown-project"].model, "unknown/project-model");
|
|
932
|
+
assert.equal(migratedProjectProfiles.model_profiles["unknown-project"].effort, "low");
|
|
933
|
+
const migratedGlobalProfiles = JSON.parse(await readFile(globalSubagentsPath, "utf8"));
|
|
934
|
+
assert.equal(migratedGlobalProfiles.model_profiles["global-worker"].model, "existing/global");
|
|
935
|
+
assert.equal(migratedGlobalProfiles.model_profiles["global-worker"].effort, "medium");
|
|
936
|
+
assert.equal(migratedGlobalProfiles.model_profiles["global-new"].model, "global/new-model");
|
|
937
|
+
assert.equal(migratedGlobalProfiles.model_profiles["global-new"].effort, "xhigh");
|
|
938
|
+
const migratedSettings = JSON.parse(
|
|
939
|
+
await readFile(join(legacySettingsMigrationCwd, ".pi", "settings.json"), "utf8"),
|
|
940
|
+
);
|
|
941
|
+
assert.equal(migratedSettings.theme, "keep-me");
|
|
942
|
+
assert.equal(migratedSettings.subagents.history, "keep-me-too");
|
|
943
|
+
assert.equal(migratedSettings.subagents.agentOverrides, undefined);
|
|
944
|
+
} finally {
|
|
945
|
+
await rm(legacySettingsMigrationCwd, { recursive: true, force: true });
|
|
946
|
+
await rm(globalSubagentsPath, { force: true });
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const invalidMigrationTargetCwd = await tempWorkspace();
|
|
950
|
+
try {
|
|
951
|
+
await mkdir(join(invalidMigrationTargetCwd, ".pi", "subagents"), { recursive: true });
|
|
952
|
+
await writeFile(
|
|
953
|
+
join(invalidMigrationTargetCwd, ".pi", "subagents", "local-bad.md"),
|
|
954
|
+
`---\nname: local-bad\ndescription: Local bad target\n---\n`,
|
|
955
|
+
);
|
|
956
|
+
await mkdir(join(globalAgentHome, "subagents"), { recursive: true });
|
|
957
|
+
await writeFile(
|
|
958
|
+
join(globalAgentHome, "subagents", "global-bad.md"),
|
|
959
|
+
`---\nname: global-bad\ndescription: Global bad target\n---\n`,
|
|
960
|
+
);
|
|
961
|
+
await writeFile(join(invalidMigrationTargetCwd, ".pi", "subagents.json"), "{ invalid json");
|
|
962
|
+
await writeFile(globalSubagentsPath, "{ invalid global json");
|
|
963
|
+
await writeFile(
|
|
964
|
+
join(invalidMigrationTargetCwd, ".pi", "settings.json"),
|
|
965
|
+
JSON.stringify({ subagents: { agentOverrides: { "local-bad": { model: "legacy/model", thinking: "low" }, "global-bad": { model: "legacy/global-model", thinking: "high" } } } }, null, 2),
|
|
966
|
+
);
|
|
967
|
+
await hooks.get("session_start")[0]({ reason: "startup" }, createCtx(invalidMigrationTargetCwd, true));
|
|
968
|
+
assert.equal(await readFile(join(invalidMigrationTargetCwd, ".pi", "subagents.json"), "utf8"), "{ invalid json");
|
|
969
|
+
assert.equal(await readFile(globalSubagentsPath, "utf8"), "{ invalid global json");
|
|
970
|
+
const preservedLegacySettings = JSON.parse(
|
|
971
|
+
await readFile(join(invalidMigrationTargetCwd, ".pi", "settings.json"), "utf8"),
|
|
972
|
+
);
|
|
973
|
+
assert.equal(preservedLegacySettings.subagents.agentOverrides["local-bad"].model, "legacy/model");
|
|
974
|
+
assert.equal(preservedLegacySettings.subagents.agentOverrides["global-bad"].model, "legacy/global-model");
|
|
975
|
+
} finally {
|
|
976
|
+
await rm(invalidMigrationTargetCwd, { recursive: true, force: true });
|
|
977
|
+
await rm(globalSubagentsPath, { force: true });
|
|
879
978
|
}
|
|
880
979
|
|
|
881
980
|
const modelsCwd = await tempWorkspace();
|
|
882
981
|
try {
|
|
883
982
|
await mkdir(join(modelsCwd, ".pi", "agents"), { recursive: true });
|
|
983
|
+
await mkdir(join(modelsCwd, ".pi", "subagents"), { recursive: true });
|
|
984
|
+
await mkdir(join(globalAgentHome, "subagents"), { recursive: true });
|
|
884
985
|
await mkdir(
|
|
885
986
|
join(modelsCwd, ".pi", "npm", "node_modules", "pi-subagents-j0k3r", "agents"),
|
|
886
987
|
{ recursive: true },
|
|
@@ -905,6 +1006,10 @@ async function run() {
|
|
|
905
1006
|
join(modelsCwd, ".pi", "agents", "worker.md"),
|
|
906
1007
|
`---\nname: worker\ndescription: Project worker\nmodel: existing/project-worker\nthinking: high\n---\n`,
|
|
907
1008
|
);
|
|
1009
|
+
await writeFile(
|
|
1010
|
+
join(modelsCwd, ".pi", "subagents", "worker.md"),
|
|
1011
|
+
`---\nname: worker\ndescription: Project subagents worker\nmodel: existing/project-subagent-worker\nthinking: medium\n---\n`,
|
|
1012
|
+
);
|
|
908
1013
|
await writeFile(
|
|
909
1014
|
join(
|
|
910
1015
|
modelsCwd,
|
|
@@ -921,6 +1026,14 @@ async function run() {
|
|
|
921
1026
|
join(modelsCwd, ".pi", "agents", "sdd-apply.md"),
|
|
922
1027
|
`---\nname: sdd-apply\ndescription: Apply phase\n---\n\nbody\n`,
|
|
923
1028
|
);
|
|
1029
|
+
await writeFile(
|
|
1030
|
+
join(modelsCwd, ".pi", "subagents", "project-special.md"),
|
|
1031
|
+
`---\nname: project-special\ndescription: Project subagent dir fixture\n---\n\nbody\n`,
|
|
1032
|
+
);
|
|
1033
|
+
await writeFile(
|
|
1034
|
+
join(globalAgentHome, "subagents", "global-special.md"),
|
|
1035
|
+
`---\nname: global-special\ndescription: Global subagent dir fixture\n---\n\nbody\n`,
|
|
1036
|
+
);
|
|
924
1037
|
for (let i = 0; i < 25; i++) {
|
|
925
1038
|
const name = `large-agent-${String(i).padStart(2, "0")}`;
|
|
926
1039
|
await writeFile(
|
|
@@ -933,13 +1046,11 @@ async function run() {
|
|
|
933
1046
|
`---\nname: evil\u001b]52;c;Zm9v\u0007-agent\ndescription: Escape fixture\n---\n`,
|
|
934
1047
|
);
|
|
935
1048
|
await writeFile(
|
|
936
|
-
join(modelsCwd, ".pi", "
|
|
1049
|
+
join(modelsCwd, ".pi", "subagents.json"),
|
|
937
1050
|
JSON.stringify(
|
|
938
1051
|
{
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
worker: { model: "existing/model", thinking: "high" },
|
|
942
|
-
},
|
|
1052
|
+
model_profiles: {
|
|
1053
|
+
worker: { model: "existing/model", effort: "high" },
|
|
943
1054
|
},
|
|
944
1055
|
},
|
|
945
1056
|
null,
|
|
@@ -948,35 +1059,44 @@ async function run() {
|
|
|
948
1059
|
);
|
|
949
1060
|
await writeFile(globalModelsPath, JSON.stringify({}, null, 2));
|
|
950
1061
|
await hooks.get("session_start")[0]({ reason: "startup" }, createCtx(modelsCwd, true));
|
|
951
|
-
const
|
|
952
|
-
await readFile(join(modelsCwd, ".pi", "
|
|
1062
|
+
const preservedProfiles = JSON.parse(
|
|
1063
|
+
await readFile(join(modelsCwd, ".pi", "subagents.json"), "utf8"),
|
|
953
1064
|
);
|
|
954
1065
|
assert.equal(
|
|
955
|
-
|
|
1066
|
+
preservedProfiles.model_profiles.worker.model,
|
|
956
1067
|
"existing/model",
|
|
957
1068
|
);
|
|
958
|
-
assert.equal(
|
|
959
|
-
preservedSettings.subagents.agentOverrides.worker.thinking,
|
|
960
|
-
"high",
|
|
961
|
-
);
|
|
1069
|
+
assert.equal(preservedProfiles.model_profiles.worker.effort, "high");
|
|
962
1070
|
const preservedProjectWorker = await readFile(
|
|
963
1071
|
join(modelsCwd, ".pi", "agents", "worker.md"),
|
|
964
1072
|
"utf8",
|
|
965
1073
|
);
|
|
966
1074
|
assert.match(preservedProjectWorker, /model: existing\/project-worker/);
|
|
967
1075
|
assert.match(preservedProjectWorker, /thinking: high/);
|
|
1076
|
+
const preservedProjectSubagentWorker = await readFile(
|
|
1077
|
+
join(modelsCwd, ".pi", "subagents", "worker.md"),
|
|
1078
|
+
"utf8",
|
|
1079
|
+
);
|
|
1080
|
+
assert.match(preservedProjectSubagentWorker, /model: existing\/project-subagent-worker/);
|
|
1081
|
+
assert.match(preservedProjectSubagentWorker, /thinking: medium/);
|
|
968
1082
|
await writeFile(globalModelsPath, JSON.stringify({ worker: {} }, null, 2));
|
|
969
1083
|
await hooks.get("session_start")[0]({ reason: "startup" }, createCtx(modelsCwd, true));
|
|
970
|
-
const
|
|
971
|
-
await readFile(join(modelsCwd, ".pi", "
|
|
1084
|
+
const clearedProfiles = JSON.parse(
|
|
1085
|
+
await readFile(join(modelsCwd, ".pi", "subagents.json"), "utf8"),
|
|
972
1086
|
);
|
|
973
|
-
assert.equal(
|
|
974
|
-
const
|
|
1087
|
+
assert.equal(clearedProfiles.model_profiles, undefined);
|
|
1088
|
+
const unchangedProjectWorker = await readFile(
|
|
975
1089
|
join(modelsCwd, ".pi", "agents", "worker.md"),
|
|
976
1090
|
"utf8",
|
|
977
1091
|
);
|
|
978
|
-
assert.
|
|
979
|
-
assert.
|
|
1092
|
+
assert.match(unchangedProjectWorker, /model: existing\/project-worker/);
|
|
1093
|
+
assert.match(unchangedProjectWorker, /thinking: high/);
|
|
1094
|
+
const clearedProjectSubagentWorker = await readFile(
|
|
1095
|
+
join(modelsCwd, ".pi", "subagents", "worker.md"),
|
|
1096
|
+
"utf8",
|
|
1097
|
+
);
|
|
1098
|
+
assert.doesNotMatch(clearedProjectSubagentWorker, /model:/);
|
|
1099
|
+
assert.doesNotMatch(clearedProjectSubagentWorker, /thinking:/);
|
|
980
1100
|
|
|
981
1101
|
await writeFile(
|
|
982
1102
|
globalModelsPath,
|
|
@@ -1059,6 +1179,8 @@ async function run() {
|
|
|
1059
1179
|
"sdd-apply": { model: "openai/gpt-5", thinking: "high" },
|
|
1060
1180
|
worker: { model: "openai/gpt-5-mini", thinking: "low" },
|
|
1061
1181
|
researcher: { model: "openai/gpt-5-mini", thinking: "low" },
|
|
1182
|
+
"project-special": { model: "openai/gpt-5-mini", thinking: "low" },
|
|
1183
|
+
"global-special": { model: "openai/gpt-5-mini", thinking: "low" },
|
|
1062
1184
|
},
|
|
1063
1185
|
});
|
|
1064
1186
|
await commands.get("gentle:models").handler("", ctx);
|
|
@@ -1088,19 +1210,48 @@ async function run() {
|
|
|
1088
1210
|
assert.match(applyAgent, /model: openai\/gpt-5/);
|
|
1089
1211
|
assert.match(applyAgent, /thinking: high/);
|
|
1090
1212
|
|
|
1091
|
-
const
|
|
1092
|
-
await readFile(join(modelsCwd, ".pi", "
|
|
1213
|
+
const projectSubagents = JSON.parse(
|
|
1214
|
+
await readFile(join(modelsCwd, ".pi", "subagents.json"), "utf8"),
|
|
1215
|
+
);
|
|
1216
|
+
assert.equal(
|
|
1217
|
+
projectSubagents.model_profiles["sdd-apply"].model,
|
|
1218
|
+
"openai/gpt-5",
|
|
1219
|
+
);
|
|
1220
|
+
assert.equal(projectSubagents.model_profiles["sdd-apply"].effort, "high");
|
|
1221
|
+
assert.equal(
|
|
1222
|
+
projectSubagents.model_profiles.worker.model,
|
|
1223
|
+
"openai/gpt-5-mini",
|
|
1224
|
+
);
|
|
1225
|
+
assert.equal(projectSubagents.model_profiles.worker.effort, "low");
|
|
1226
|
+
const routedProjectSubagentWorker = await readFile(
|
|
1227
|
+
join(modelsCwd, ".pi", "subagents", "worker.md"),
|
|
1228
|
+
"utf8",
|
|
1229
|
+
);
|
|
1230
|
+
assert.match(routedProjectSubagentWorker, /model: openai\/gpt-5-mini/);
|
|
1231
|
+
assert.match(routedProjectSubagentWorker, /thinking: low/);
|
|
1232
|
+
const shadowedProjectAgentWorker = await readFile(
|
|
1233
|
+
join(modelsCwd, ".pi", "agents", "worker.md"),
|
|
1234
|
+
"utf8",
|
|
1235
|
+
);
|
|
1236
|
+
assert.match(shadowedProjectAgentWorker, /model: existing\/project-worker/);
|
|
1237
|
+
assert.match(shadowedProjectAgentWorker, /thinking: high/);
|
|
1238
|
+
assert.equal(
|
|
1239
|
+
projectSubagents.model_profiles["project-special"].model,
|
|
1240
|
+
"openai/gpt-5-mini",
|
|
1093
1241
|
);
|
|
1242
|
+
assert.equal(projectSubagents.model_profiles["project-special"].effort, "low");
|
|
1243
|
+
const globalSubagents = JSON.parse(await readFile(globalSubagentsPath, "utf8"));
|
|
1094
1244
|
assert.equal(
|
|
1095
|
-
|
|
1245
|
+
globalSubagents.model_profiles.researcher.model,
|
|
1096
1246
|
"openai/gpt-5-mini",
|
|
1097
1247
|
);
|
|
1098
|
-
assert.equal(
|
|
1248
|
+
assert.equal(globalSubagents.model_profiles.researcher.effort, "low");
|
|
1099
1249
|
assert.equal(
|
|
1100
|
-
|
|
1250
|
+
globalSubagents.model_profiles["global-special"].model,
|
|
1101
1251
|
"openai/gpt-5-mini",
|
|
1102
1252
|
);
|
|
1103
|
-
assert.equal(
|
|
1253
|
+
assert.equal(globalSubagents.model_profiles["global-special"].effort, "low");
|
|
1254
|
+
assert.equal(existsSync(join(modelsCwd, ".pi", "settings.json")), false);
|
|
1104
1255
|
|
|
1105
1256
|
const kittyE = "\x1b[101u";
|
|
1106
1257
|
assert.notEqual(kittyE, "e");
|
|
@@ -1196,6 +1347,7 @@ async function run() {
|
|
|
1196
1347
|
} finally {
|
|
1197
1348
|
await rm(modelsCwd, { recursive: true, force: true });
|
|
1198
1349
|
await rm(globalModelsPath, { force: true });
|
|
1350
|
+
await rm(globalSubagentsPath, { force: true });
|
|
1199
1351
|
}
|
|
1200
1352
|
|
|
1201
1353
|
const registryCwd = await tempWorkspace();
|