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 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
  | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
@@ -7,6 +7,7 @@ tools:
7
7
  - grep
8
8
  - glob
9
9
  - write
10
+ - edit
10
11
  - bash
11
12
  - mem_search
12
13
  - mem_get_observation
@@ -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. Prefer `subagent_run` with `mode: "background"` for long independent exploration, implementation, tests, or review, and `mode: "task"` when the parent needs the result before continuing.
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.
@@ -93,33 +93,26 @@ function sddGlobalAssetDriftCount(): number {
93
93
  return stale;
94
94
  }
95
95
 
96
- function sddLocalOverrideDriftCount(cwd: string): number {
97
- let stale = 0;
98
- for (const [assetSubdir, installedSubdir] of [
99
- ["agents", join(".pi", "agents")],
100
- ["chains", join(".pi", "chains")],
101
- ["support", join(".pi", "gentle-ai", "support")],
102
- ] as const) {
103
- const assetDir = join(ASSETS_DIR, assetSubdir);
104
- const installedDir = join(cwd, installedSubdir);
105
- if (!existsSync(assetDir) || !existsSync(installedDir)) continue;
106
- for (const entry of readdirSync(assetDir, { withFileTypes: true })) {
107
- if (!entry.isFile()) continue;
108
- const installedPath = join(installedDir, entry.name);
109
- if (!existsSync(installedPath)) continue;
110
- try {
111
- if (
112
- readFileSync(join(assetDir, entry.name), "utf8") !==
113
- readFileSync(installedPath, "utf8")
114
- ) {
115
- stale += 1;
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 staleLocalOverrides = sddLocalOverrideDriftCount(ctx.cwd);
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
- `${staleLocalOverrides === 0 ? "pass" : "warn"}: Project-local SDD override drift ${staleLocalOverrides} file(s)`,
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 staleLocalOverrides = sddLocalOverrideDriftCount(ctx.cwd);
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 override drift: ${staleLocalOverrides} file(s)${
2360
- staleLocalOverrides > 0
2361
- ? " — run /gentle-ai:install-sdd --force only if you intentionally want to replace local overrides"
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 || staleLocalOverrides > 0 ? "warning" : "info",
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.0",
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 override drift: \d+ file\(s\)/);
681
- assert.match(ctx.ui.notifications.at(-1).message, /gentle-ai:install-sdd --force/);
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
+ });