gentle-pi 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/assets/agents/sdd-init.md +1 -0
- package/assets/orchestrator.md +6 -1
- package/assets/sdd-orchestrator-workflow.md +4 -0
- package/extensions/gentle-ai.ts +30 -34
- package/package.json +1 -1
- package/tests/runtime-harness.mjs +3 -2
- package/tests/sdd-agent-tools.test.ts +70 -0
package/README.md
CHANGED
|
@@ -119,7 +119,7 @@ The goal is not ceremony. The goal is to avoid accidental chaos. Once a task sto
|
|
|
119
119
|
|
|
120
120
|
### Delegation triggers
|
|
121
121
|
|
|
122
|
-
`gentle-pi` keeps the parent session thin and delegates at the narrowest useful point. When the Pi Subagents extension is installed, the preferred runtime is the `subagent_*` tool family because it runs the user's configured project/global subagent definitions and preserves history/background behavior. If those tools are unavailable, the parent should fall back to Pi's native `Agent` tool or another available delegation mechanism. The requirement is delegation; the runtime is capability-dependent.
|
|
122
|
+
`gentle-pi` keeps the parent session thin and delegates at the narrowest useful point. When the Pi Subagents extension is installed, the preferred runtime is the `subagent_*` tool family because it runs the user's configured project/global subagent definitions and preserves history/background behavior. Use waiting/task mode when the parent must consume the result and continue the workflow; use background mode only for independent work where parent continuation is not required. If those tools are unavailable, the parent should fall back to Pi's native `Agent` tool or another available delegation mechanism. The requirement is delegation; the runtime is capability-dependent.
|
|
123
123
|
|
|
124
124
|
| Trigger | Required behavior |
|
|
125
125
|
| --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
package/assets/orchestrator.md
CHANGED
|
@@ -82,7 +82,12 @@ Examples:
|
|
|
82
82
|
- run tests/builds and summarize results;
|
|
83
83
|
- fresh-context review.
|
|
84
84
|
|
|
85
|
-
Use the configured subagent runtime when available. Prefer the `subagent_*` tools (`subagent_run`, status/result helpers) when the Pi Subagents extension is installed, because they run the user's configured project/global subagent definitions and preserve history/background behavior.
|
|
85
|
+
Use the configured subagent runtime when available. Prefer the `subagent_*` tools (`subagent_run`, status/result helpers) when the Pi Subagents extension is installed, because they run the user's configured project/global subagent definitions and preserve history/background behavior.
|
|
86
|
+
|
|
87
|
+
Choose subagent mode by orchestration dependency, not by task length:
|
|
88
|
+
|
|
89
|
+
- Use `mode: "task"` when the parent must consume the result and continue the workflow, including SDD phases, implementation batches, verification, review gates, and any delegated work whose output determines the next action.
|
|
90
|
+
- Use `mode: "background"` only for independent work where automatic parent continuation is not required. Background completion may notify the user and preserve history, but it is not a guarantee that the parent model will resume orchestration.
|
|
86
91
|
|
|
87
92
|
If `subagent_*` tools are unavailable, fall back to Pi's native `Agent` tool or another available delegation mechanism. The delegation trigger remains mandatory; the fallback changes the runtime, not the requirement to delegate. If no delegation mechanism is available, stop the complex work and explain the blocker instead of silently continuing inline.
|
|
88
93
|
|
|
@@ -132,6 +132,10 @@ skill_resolution
|
|
|
132
132
|
|
|
133
133
|
The parent should synthesize these envelopes, not paste long raw reports unless needed.
|
|
134
134
|
|
|
135
|
+
## SDD Phase Delegation Mode
|
|
136
|
+
|
|
137
|
+
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.
|
|
138
|
+
|
|
135
139
|
## Strict TDD Forwarding
|
|
136
140
|
|
|
137
141
|
For `sdd-apply` and `sdd-verify`, read `openspec/config.yaml` when present.
|
package/extensions/gentle-ai.ts
CHANGED
|
@@ -93,33 +93,26 @@ function sddGlobalAssetDriftCount(): number {
|
|
|
93
93
|
return stale;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
} catch {
|
|
118
|
-
stale += 1;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return stale;
|
|
96
|
+
function sddLocalAgentOverrideCount(cwd: string): number {
|
|
97
|
+
const packageSddAgentsDir = join(ASSETS_DIR, "agents");
|
|
98
|
+
const packageSddAgentNames = existsSync(packageSddAgentsDir)
|
|
99
|
+
? new Set(
|
|
100
|
+
readdirSync(packageSddAgentsDir, { withFileTypes: true })
|
|
101
|
+
.filter((entry) => entry.isFile() && /^sdd-.*\.md$/i.test(entry.name))
|
|
102
|
+
.map((entry) => entry.name),
|
|
103
|
+
)
|
|
104
|
+
: new Set<string>();
|
|
105
|
+
let count = 0;
|
|
106
|
+
for (const installedDir of [
|
|
107
|
+
join(cwd, ".pi", "agents"),
|
|
108
|
+
join(cwd, ".pi", "subagents"),
|
|
109
|
+
]) {
|
|
110
|
+
if (!existsSync(installedDir)) continue;
|
|
111
|
+
for (const entry of readdirSync(installedDir, { withFileTypes: true })) {
|
|
112
|
+
if (entry.isFile() && packageSddAgentNames.has(entry.name)) count += 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return count;
|
|
123
116
|
}
|
|
124
117
|
|
|
125
118
|
let orchestratorPromptCache: string | null = null;
|
|
@@ -2302,7 +2295,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2302
2295
|
join(ctx.cwd, ".atl", "skill-registry.md"),
|
|
2303
2296
|
);
|
|
2304
2297
|
const staleSddAssets = sddGlobalAssetDriftCount();
|
|
2305
|
-
const
|
|
2298
|
+
const localSddAgentOverrides = sddLocalAgentOverrideCount(ctx.cwd);
|
|
2306
2299
|
const modelConfig = await readSavedModelConfigAsync(ctx.cwd);
|
|
2307
2300
|
const engramActive = hasWritableEngramTool(pi);
|
|
2308
2301
|
const lines = [
|
|
@@ -2310,7 +2303,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2310
2303
|
`${agentsInstalled ? "pass" : "fail"}: Global SDD agents ${agentsInstalled ? "installed" : "missing"}`,
|
|
2311
2304
|
`${chainsInstalled ? "pass" : "fail"}: Global SDD chains ${chainsInstalled ? "installed" : "missing"}`,
|
|
2312
2305
|
`${staleSddAssets === 0 ? "pass" : "warn"}: Global SDD asset drift ${staleSddAssets} file(s)`,
|
|
2313
|
-
`${
|
|
2306
|
+
`${localSddAgentOverrides === 0 ? "pass" : "warn"}: Project-local SDD agent overrides ${localSddAgentOverrides} file(s)`,
|
|
2314
2307
|
`${openspecConfigured ? "pass" : "warn"}: OpenSpec config ${openspecConfigured ? "present" : "missing"}`,
|
|
2315
2308
|
`${skillRegistryPresent ? "pass" : "warn"}: Skill registry ${skillRegistryPresent ? "present" : "missing"}`,
|
|
2316
2309
|
`${modelConfig.status === "invalid" ? "fail" : "pass"}: Global model config ${modelConfig.status}`,
|
|
@@ -2323,6 +2316,9 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2323
2316
|
if (modelConfig.status === "invalid") {
|
|
2324
2317
|
lines.push(`remedy: fix or remove ${modelConfig.path}`);
|
|
2325
2318
|
}
|
|
2319
|
+
if (localSddAgentOverrides > 0) {
|
|
2320
|
+
lines.push("remedy: remove project-local SDD agent overrides unless intentionally debugging package assets");
|
|
2321
|
+
}
|
|
2326
2322
|
ctx.ui.notify(
|
|
2327
2323
|
lines.join("\n"),
|
|
2328
2324
|
lines.some((line) => line.startsWith("fail:")) ? "warning" : "info",
|
|
@@ -2343,7 +2339,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2343
2339
|
join(ctx.cwd, "openspec", "config.yaml"),
|
|
2344
2340
|
);
|
|
2345
2341
|
const staleSddAssets = sddGlobalAssetDriftCount();
|
|
2346
|
-
const
|
|
2342
|
+
const localSddAgentOverrides = sddLocalAgentOverrideCount(ctx.cwd);
|
|
2347
2343
|
const modelConfig = await readModelConfigAsync(ctx.cwd);
|
|
2348
2344
|
ctx.ui.notify(
|
|
2349
2345
|
[
|
|
@@ -2356,16 +2352,16 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2356
2352
|
? " — run /gentle-ai:install-sdd --force to refresh intentionally"
|
|
2357
2353
|
: ""
|
|
2358
2354
|
}`,
|
|
2359
|
-
`Project-local SDD
|
|
2360
|
-
|
|
2361
|
-
? " —
|
|
2355
|
+
`Project-local SDD agent overrides: ${localSddAgentOverrides} file(s)${
|
|
2356
|
+
localSddAgentOverrides > 0
|
|
2357
|
+
? " — local SDD agents shadow package assets; remove them unless intentionally debugging"
|
|
2362
2358
|
: ""
|
|
2363
2359
|
}`,
|
|
2364
2360
|
`OpenSpec config: ${openspecConfigured ? "present" : "missing"}`,
|
|
2365
2361
|
`Global model config: ${existsSync(modelConfigPath(ctx.cwd)) ? "present" : "missing"}`,
|
|
2366
2362
|
...describeModelConfig(ctx.cwd, modelConfig),
|
|
2367
2363
|
].join("\n"),
|
|
2368
|
-
staleSddAssets > 0 ||
|
|
2364
|
+
staleSddAssets > 0 || localSddAgentOverrides > 0 ? "warning" : "info",
|
|
2369
2365
|
);
|
|
2370
2366
|
},
|
|
2371
2367
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -673,12 +673,13 @@ async function run() {
|
|
|
673
673
|
await mkdir(join(staleAssetsCwd, ".pi", "gentle-ai", "support"), { recursive: true });
|
|
674
674
|
await writeFile(join(staleAssetsCwd, ".pi", "agents", "sdd-apply.md"), "stale apply\n");
|
|
675
675
|
await writeFile(join(staleAssetsCwd, ".pi", "agents", "sdd-spec.md"), "stale spec\n");
|
|
676
|
+
await writeFile(join(staleAssetsCwd, ".pi", "agents", "sdd-custom-debug.md"), "custom debug agent\n");
|
|
676
677
|
await writeFile(join(staleAssetsCwd, ".pi", "chains", "sdd-full.chain.md"), "stale chain\n");
|
|
677
678
|
await writeFile(join(staleAssetsCwd, ".pi", "gentle-ai", "support", "sdd-status-contract.md"), "stale status contract\n");
|
|
678
679
|
const ctx = createCtx(staleAssetsCwd, true);
|
|
679
680
|
await commands.get("gentle-ai:status").handler("", ctx);
|
|
680
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Project-local SDD
|
|
681
|
-
assert.match(ctx.ui.notifications.at(-1).message, /
|
|
681
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Project-local SDD agent overrides: 2 file\(s\)/);
|
|
682
|
+
assert.match(ctx.ui.notifications.at(-1).message, /local SDD agents shadow package assets/);
|
|
682
683
|
await commands.get("gentle-ai:doctor").handler("", ctx);
|
|
683
684
|
assert.match(ctx.ui.notifications.at(-1).message, /el Gentleman doctor/);
|
|
684
685
|
assert.match(ctx.ui.notifications.at(-1).message, /Sensitive-path guard active/);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
const repoRoot = process.cwd();
|
|
7
|
+
const assetsAgentsDir = join(repoRoot, "assets", "agents");
|
|
8
|
+
|
|
9
|
+
function readFrontmatter(path: string): string {
|
|
10
|
+
const text = readFileSync(path, "utf8");
|
|
11
|
+
const match = text.match(/^---\n([\s\S]*?)\n---/);
|
|
12
|
+
assert.ok(match, `${path} must have YAML frontmatter`);
|
|
13
|
+
return match[1];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readTools(path: string): string[] {
|
|
17
|
+
const frontmatter = readFrontmatter(path);
|
|
18
|
+
const lines = frontmatter.split("\n");
|
|
19
|
+
const toolsIndex = lines.findIndex((line) => line === "tools:");
|
|
20
|
+
assert.notEqual(toolsIndex, -1, `${path} must declare tools as a YAML array`);
|
|
21
|
+
|
|
22
|
+
const scalarTools = lines.find((line) => /^tools:\s+/.test(line));
|
|
23
|
+
assert.equal(scalarTools, undefined, `${path} must not declare scalar comma-separated tools`);
|
|
24
|
+
|
|
25
|
+
const tools: string[] = [];
|
|
26
|
+
for (const line of lines.slice(toolsIndex + 1)) {
|
|
27
|
+
if (!line.startsWith(" - ")) break;
|
|
28
|
+
tools.push(line.slice(4).trim());
|
|
29
|
+
}
|
|
30
|
+
assert.ok(tools.length > 0, `${path} must declare at least one tool`);
|
|
31
|
+
return tools;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const requiredToolsByAgent: Record<string, string[]> = {
|
|
35
|
+
"sdd-apply.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
36
|
+
"sdd-archive.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save"],
|
|
37
|
+
"sdd-design.md": ["read", "grep", "glob", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
38
|
+
"sdd-explore.md": ["read", "grep", "glob", "mem_save"],
|
|
39
|
+
"sdd-init.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
40
|
+
"sdd-onboard.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
41
|
+
"sdd-proposal.md": ["read", "grep", "glob", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
42
|
+
"sdd-spec.md": ["read", "grep", "glob", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
43
|
+
"sdd-status.md": ["read", "grep", "glob", "bash", "mem_search", "mem_get_observation"],
|
|
44
|
+
"sdd-sync.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
45
|
+
"sdd-tasks.md": ["read", "grep", "glob", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
46
|
+
"sdd-verify.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save"],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
test("SDD package agents declare role-appropriate tools as YAML arrays", () => {
|
|
50
|
+
for (const [fileName, requiredTools] of Object.entries(requiredToolsByAgent)) {
|
|
51
|
+
const path = join(assetsAgentsDir, fileName);
|
|
52
|
+
assert.ok(existsSync(path), `${fileName} must exist`);
|
|
53
|
+
const tools = readTools(path);
|
|
54
|
+
for (const tool of requiredTools) {
|
|
55
|
+
assert.ok(tools.includes(tool), `${fileName} must include ${tool}`);
|
|
56
|
+
}
|
|
57
|
+
for (const tool of tools) {
|
|
58
|
+
assert.ok(!tool.startsWith("subagent_"), `${fileName} must not allow child subagent tool ${tool}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("project does not ship local SDD agent overrides", () => {
|
|
64
|
+
for (const relativeDir of [join(".pi", "agents"), join(".pi", "subagents")]) {
|
|
65
|
+
const dir = join(repoRoot, relativeDir);
|
|
66
|
+
if (!existsSync(dir)) continue;
|
|
67
|
+
const overrides = readdirSync(dir).filter((entry) => /^sdd-.*\.md$/i.test(entry));
|
|
68
|
+
assert.deepEqual(overrides, [], `${relativeDir} must not shadow package SDD agents`);
|
|
69
|
+
}
|
|
70
|
+
});
|