gentle-pi 0.10.9 → 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 +11 -146
- package/assets/sdd-orchestrator-workflow.md +157 -0
- package/extensions/gentle-ai.ts +37 -35
- package/extensions/skill-registry.ts +21 -13
- package/package.json +1 -1
- package/tests/artifact-language.test.ts +15 -0
- package/tests/runtime-harness.mjs +8 -2
- package/tests/sdd-agent-tools.test.ts +70 -0
- package/tests/skill-registry.test.ts +31 -1
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
|
|
|
@@ -188,103 +193,15 @@ stop writes → parent captures git status → selected review lens audits affec
|
|
|
188
193
|
|
|
189
194
|
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.
|
|
190
195
|
|
|
191
|
-
## SDD Workflow
|
|
192
|
-
|
|
193
|
-
SDD phases:
|
|
194
|
-
|
|
195
|
-
```text
|
|
196
|
-
init → explore → proposal → spec → design → tasks → apply → verify → sync → archive
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
Dependency graph:
|
|
200
|
-
|
|
201
|
-
```text
|
|
202
|
-
proposal → spec ─┬→ tasks → apply → verify → sync → archive
|
|
203
|
-
proposal → design ┘
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
`/sdd-status [change]` is the read-only status action for resolving the active change, artifact paths, task progress, dependency readiness, and action context before apply/verify/sync/archive.
|
|
207
|
-
|
|
208
|
-
## Native SDD Dispatcher
|
|
209
|
-
|
|
210
|
-
The user expresses intent; they should not have to administer phases manually. For natural-language SDD requests and `/sdd-continue`, the parent/orchestrator must use the native status engine as the state authority, decide the next phase, and delegate only the phase that status marks ready.
|
|
211
|
-
|
|
212
|
-
Flow:
|
|
213
|
-
|
|
214
|
-
```text
|
|
215
|
-
user intent → preflight/init guard → native status engine → phase decision → subagent gets status JSON + generated instructions → artifact/progress write → status recalculation → continue or stop
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
Rules:
|
|
219
|
-
|
|
220
|
-
- `/sdd-status` is a debug/status command, not the main UX.
|
|
221
|
-
- `/sdd-continue` is the native dispatcher command: resolve status, choose the next ready phase, and carry status/instructions into the subagent prompt.
|
|
222
|
-
- `sdd-apply`, `sdd-verify`, `sdd-sync`, and `sdd-archive` must obey parent-provided native status; they must not reconstruct readiness from prompt inference when status JSON is present.
|
|
223
|
-
- Do not launch a phase when native status marks that dependency `blocked`.
|
|
224
|
-
- `sdd-archive` cannot proceed unless native status says `dependencies.archive` is `ready` or `all_done` — UNLESS the store carve-out is active (`nextRecommended: "resolve-via-engram"`), in which case resolve archive readiness from Engram instead of treating `not_applicable` as a gate failure.
|
|
225
|
-
- **Non-authoritative store carve-out:** when `nextRecommended: "resolve-via-engram"` is set, native status is **not authoritative**. This applies to `artifactStore: engram`, `artifactStore: none`, and `artifactStore: both` when the `openspec/` directory does not exist. For non-authoritative stores: resolve readiness from Engram using the Engram memory tools injected by the memory provider on the change topic keys (`sdd/{change-name}/proposal`, `sdd/{change-name}/spec`, `sdd/{change-name}/design`, `sdd/{change-name}/tasks`, etc.). Do **not** treat `blockedReasons` or `not_applicable` dependency states from the native engine as real blockers when the store carve-out is active.
|
|
226
|
-
|
|
227
|
-
## SDD Status Contract
|
|
228
|
-
|
|
229
|
-
Before `/sdd-continue`, `sdd-apply`, `sdd-verify`, `sdd-sync`, or `sdd-archive`, resolve and carry structured status. Lookup order: parent-provided status, then project override `.pi/gentle-ai/support/sdd-status-contract.md`, then globally installed `~/.pi/agent/gentle-ai/support/sdd-status-contract.md`, then the embedded `sdd-status` prompt contract. Do not use `assets/support/...` as a runtime path; that is only the package source path before installation.
|
|
230
|
-
|
|
231
|
-
Status must include:
|
|
232
|
-
|
|
233
|
-
- active change selection and how it was resolved;
|
|
234
|
-
- artifact store and paths/topics for proposal, specs, design, tasks, apply-progress, verify-report, and sync-report;
|
|
235
|
-
- task progress with exact unchecked `- [ ]` implementation task lines;
|
|
236
|
-
- dependency states for apply, verify, sync, and archive;
|
|
237
|
-
- `actionContext` with mode, workspace root, allowed edit roots, and warnings;
|
|
238
|
-
- next recommended action.
|
|
239
|
-
|
|
240
|
-
Do not guess the active change. If change selection is ambiguous, ask the user and stop. If `actionContext.mode: workspace-planning` and no allowed edit roots are provided, stop before apply/verify/sync/archive and ask for an explicit implementation/edit scope.
|
|
241
|
-
|
|
242
|
-
## Lazy SDD Preflight
|
|
196
|
+
## SDD Workflow (lazy-loaded)
|
|
243
197
|
|
|
244
|
-
|
|
198
|
+
The detailed SDD workflow is intentionally not embedded in this always-on parent prompt. Before handling any `/sdd-*` command, natural-language SDD request, SDD continuation/routing, apply/verify/sync/archive work, or SDD/Judgment-Day phase delegation, read this package asset first:
|
|
245
199
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
1. an injected `## SDD Session Preflight` block, or
|
|
249
|
-
2. an explicit user answer in the current conversation covering all four preflight choices below.
|
|
250
|
-
|
|
251
|
-
If neither exists and `/gentle-ai: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.
|
|
252
|
-
|
|
253
|
-
The preflight captures:
|
|
254
|
-
|
|
255
|
-
- execution mode: `interactive` or `auto`;
|
|
256
|
-
- artifact store: `openspec`, `engram`, or `both` when callable memory tools are available;
|
|
257
|
-
- chained PR strategy: `auto-forecast`, `ask-always`, `single-pr-default`, or `force-chained`;
|
|
258
|
-
- review budget in changed lines.
|
|
259
|
-
|
|
260
|
-
The package should ensure SDD assets are present as global Pi runtime assets without the user needing to remember per-project setup commands. If assets are missing, install them non-destructively into:
|
|
261
|
-
|
|
262
|
-
```text
|
|
263
|
-
~/.pi/agent/agents/sdd-*.md
|
|
264
|
-
~/.pi/agent/chains/sdd-*.chain.md
|
|
265
|
-
```
|
|
200
|
+
`{{GENTLE_PI_SDD_WORKFLOW_PATH}}`
|
|
266
201
|
|
|
267
|
-
|
|
202
|
+
That lazy surface contains the SDD phases, native dispatcher rules, status contract, preflight/init guards, artifact-store policy, execution mode, Strict TDD forwarding, phase result contract, and review workload guard.
|
|
268
203
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
Before any SDD flow, make sure project context exists.
|
|
272
|
-
|
|
273
|
-
In this Pi package, the default local artifact is:
|
|
274
|
-
|
|
275
|
-
```text
|
|
276
|
-
openspec/config.yaml
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
If it is missing, ask the user for the minimal information needed or run `/sdd-init` if available. This init guard runs after the session preflight gate above; project config presence or absence never substitutes for session preflight choices. Do not proceed with a substantial SDD flow while pretending project context, testing capability, or session preflight choices are known.
|
|
280
|
-
|
|
281
|
-
## Artifact Store Policy
|
|
282
|
-
|
|
283
|
-
This package does not provide persistent memory by itself.
|
|
284
|
-
|
|
285
|
-
- Default: `openspec` artifacts in the repo.
|
|
286
|
-
- If a separate memory package is installed and callable, memory/hybrid flows may be used.
|
|
287
|
-
- Never claim memory exists because Gentle AI is installed.
|
|
204
|
+
Hard preflight invariant: `openspec/config.yaml`, existing SDD changes, installed `.pi`/global SDD assets, or a todo named "preflight" are not session preflight. 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 an injected `## SDD Session Preflight` block or an explicit user answer covering the preflight choices.
|
|
288
205
|
|
|
289
206
|
## Memory Contract
|
|
290
207
|
|
|
@@ -325,38 +242,6 @@ Memory lifecycle rule (when Engram exposes lifecycle metadata/tooling):
|
|
|
325
242
|
- When a retrieved memory is marked `needs_review`, surface that stale context to the user and verify it against current evidence before relying on it.
|
|
326
243
|
- Do NOT call the injected Engram review tool with action `mark_reviewed` automatically. Only call `mark_reviewed` after explicit user confirmation or through a dedicated memory maintenance command.
|
|
327
244
|
|
|
328
|
-
## Execution Mode
|
|
329
|
-
|
|
330
|
-
Use the session's SDD preflight choice:
|
|
331
|
-
|
|
332
|
-
- `interactive`: default, pause between major phases and ask whether to continue.
|
|
333
|
-
- `auto`: run phases back-to-back when the user explicitly wants speed and trusts the flow.
|
|
334
|
-
|
|
335
|
-
In interactive mode, between phases:
|
|
336
|
-
|
|
337
|
-
1. show concise phase result;
|
|
338
|
-
2. state next phase;
|
|
339
|
-
3. ask whether to continue or adjust.
|
|
340
|
-
|
|
341
|
-
Interactive approval is phase-scoped. A user response such as "continue", "dale", or "go on" approves only the immediate next phase, not the rest of the SDD pipeline. Do not treat a generated artifact as approved until the user has had a chance to review or explicitly delegate that review.
|
|
342
|
-
|
|
343
|
-
Before `sdd-proposal` in interactive mode, offer the user a proposal question round instead of silently deciding whether the proposal is clear enough. Explain that the questions are meant to improve the PRD/proposal by uncovering business understanding, business rules, implications, impact, edge cases, and product tradeoffs. Prefer 3–5 concrete product questions per round, then summarize the resulting assumptions and ask whether the user wants to correct anything or run a second question round. Cover business/product/PRD decisions: business problem, target users and situations, business rules, product outcome, current-state gap, implications and impact, edge cases, decision gaps, first-slice scope boundaries, non-goals, product constraints, and business tradeoffs. Do not ask about test commands, PR shape, changed-line budget, or other harness mechanics at proposal time unless the user explicitly asks to discuss delivery.
|
|
344
|
-
|
|
345
|
-
## Result Contract
|
|
346
|
-
|
|
347
|
-
Every phase result should include:
|
|
348
|
-
|
|
349
|
-
```text
|
|
350
|
-
status
|
|
351
|
-
executive_summary
|
|
352
|
-
artifacts
|
|
353
|
-
next_recommended
|
|
354
|
-
risks
|
|
355
|
-
skill_resolution
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
The parent should synthesize these envelopes, not paste long raw reports unless needed.
|
|
359
|
-
|
|
360
245
|
## Skill Registry Protocol
|
|
361
246
|
|
|
362
247
|
The parent resolves skills once per session or before first delegation:
|
|
@@ -403,26 +288,6 @@ Common intent hints, not hard routing:
|
|
|
403
288
|
|
|
404
289
|
Keep this lightweight: loading a skill should improve the immediate task, not force extra ceremony.
|
|
405
290
|
|
|
406
|
-
## Strict TDD Forwarding
|
|
407
|
-
|
|
408
|
-
For `sdd-apply` and `sdd-verify`, read `openspec/config.yaml` when present.
|
|
409
|
-
|
|
410
|
-
If it declares strict TDD and a test command, include a non-negotiable instruction in the phase prompt:
|
|
411
|
-
|
|
412
|
-
```text
|
|
413
|
-
STRICT TDD MODE IS ACTIVE. Test runner: <command>. Follow RED, GREEN, TRIANGULATE, REFACTOR. Record evidence.
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
Do not rely on the child agent to discover this independently.
|
|
417
|
-
|
|
418
|
-
## Review Workload Guard
|
|
419
|
-
|
|
420
|
-
After `sdd-tasks` and before `sdd-apply`, inspect the task output for review workload risk.
|
|
421
|
-
|
|
422
|
-
If estimated changed lines exceed 400, chained PRs are recommended, or a decision is needed, pause and ask unless the user already approved a delivery strategy.
|
|
423
|
-
|
|
424
|
-
Automatic mode does not override reviewer burnout protection.
|
|
425
|
-
|
|
426
291
|
## Safety
|
|
427
292
|
|
|
428
293
|
- Never commit unless the user explicitly asks.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# SDD Orchestrator Workflow
|
|
2
|
+
|
|
3
|
+
This is the lazy-loaded SDD workflow surface for el Gentleman on Pi. Read this file before handling `/sdd-*`, natural-language SDD requests, SDD continuation/routing, apply/verify/sync/archive work, or SDD/Judgment-Day phase delegation.
|
|
4
|
+
|
|
5
|
+
## SDD Workflow
|
|
6
|
+
|
|
7
|
+
SDD phases:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
init → explore → proposal → spec → design → tasks → apply → verify → sync → archive
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Dependency graph:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
proposal → spec ─┬→ tasks → apply → verify → sync → archive
|
|
17
|
+
proposal → design ┘
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`/sdd-status [change]` is the read-only status action for resolving the active change, artifact paths, task progress, dependency readiness, and action context before apply/verify/sync/archive.
|
|
21
|
+
|
|
22
|
+
## Native SDD Dispatcher
|
|
23
|
+
|
|
24
|
+
The user expresses intent; they should not have to administer phases manually. For natural-language SDD requests and `/sdd-continue`, the parent/orchestrator must use the native status engine as the state authority, decide the next phase, and delegate only the phase that status marks ready.
|
|
25
|
+
|
|
26
|
+
Flow:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
user intent → preflight/init guard → native status engine → phase decision → subagent gets status JSON + generated instructions → artifact/progress write → status recalculation → continue or stop
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Rules:
|
|
33
|
+
|
|
34
|
+
- `/sdd-status` is a debug/status command, not the main UX.
|
|
35
|
+
- `/sdd-continue` is the native dispatcher command: resolve status, choose the next ready phase, and carry status/instructions into the subagent prompt.
|
|
36
|
+
- `sdd-apply`, `sdd-verify`, `sdd-sync`, and `sdd-archive` must obey parent-provided native status; they must not reconstruct readiness from prompt inference when status JSON is present.
|
|
37
|
+
- Do not launch a phase when native status marks that dependency `blocked`.
|
|
38
|
+
- `sdd-archive` cannot proceed unless native status says `dependencies.archive` is `ready` or `all_done` — UNLESS the store carve-out is active (`nextRecommended: "resolve-via-engram"`), in which case resolve archive readiness from Engram instead of treating `not_applicable` as a gate failure.
|
|
39
|
+
- **Non-authoritative store carve-out:** when `nextRecommended: "resolve-via-engram"` is set, native status is **not authoritative**. This applies to `artifactStore: engram`, `artifactStore: none`, and `artifactStore: both` when the `openspec/` directory does not exist. For non-authoritative stores: resolve readiness from Engram using the Engram memory tools injected by the memory provider on the change topic keys (`sdd/{change-name}/proposal`, `sdd/{change-name}/spec`, `sdd/{change-name}/design`, `sdd/{change-name}/tasks`, etc.). Do **not** treat `blockedReasons` or `not_applicable` dependency states from the native engine as real blockers when the store carve-out is active.
|
|
40
|
+
|
|
41
|
+
## SDD Status Contract
|
|
42
|
+
|
|
43
|
+
Before `/sdd-continue`, `sdd-apply`, `sdd-verify`, `sdd-sync`, or `sdd-archive`, resolve and carry structured status. Lookup order: parent-provided status, then project override `.pi/gentle-ai/support/sdd-status-contract.md`, then globally installed `~/.pi/agent/gentle-ai/support/sdd-status-contract.md`, then the embedded `sdd-status` prompt contract. Do not use `assets/support/...` as a runtime path; that is only the package source path before installation.
|
|
44
|
+
|
|
45
|
+
Status must include:
|
|
46
|
+
|
|
47
|
+
- active change selection and how it was resolved;
|
|
48
|
+
- artifact store and paths/topics for proposal, specs, design, tasks, apply-progress, verify-report, and sync-report;
|
|
49
|
+
- task progress with exact unchecked `- [ ]` implementation task lines;
|
|
50
|
+
- dependency states for apply, verify, sync, and archive;
|
|
51
|
+
- `actionContext` with mode, workspace root, allowed edit roots, and warnings;
|
|
52
|
+
- next recommended action.
|
|
53
|
+
|
|
54
|
+
Do not guess the active change. If change selection is ambiguous, ask the user and stop. If `actionContext.mode: workspace-planning` and no allowed edit roots are provided, stop before apply/verify/sync/archive and ask for an explicit implementation/edit scope.
|
|
55
|
+
|
|
56
|
+
## Lazy SDD Preflight
|
|
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-ai:sdd-preflight` before continuing.
|
|
59
|
+
|
|
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
|
+
|
|
62
|
+
1. an injected `## SDD Session Preflight` block, or
|
|
63
|
+
2. an explicit user answer in the current conversation covering all four preflight choices below.
|
|
64
|
+
|
|
65
|
+
If neither exists and `/gentle-ai: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
|
+
|
|
67
|
+
The preflight captures:
|
|
68
|
+
|
|
69
|
+
- execution mode: `interactive` or `auto`;
|
|
70
|
+
- artifact store: `openspec`, `engram`, or `both` when callable memory tools are available;
|
|
71
|
+
- chained PR strategy: `auto-forecast`, `ask-always`, `single-pr-default`, or `force-chained`;
|
|
72
|
+
- review budget in changed lines.
|
|
73
|
+
|
|
74
|
+
The package should ensure SDD assets are present as global Pi runtime assets without the user needing to remember per-project setup commands. If assets are missing, install them non-destructively into:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
~/.pi/agent/agents/sdd-*.md
|
|
78
|
+
~/.pi/agent/chains/sdd-*.chain.md
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Manual install commands are recovery/debug paths, not the happy path. `/gentle-ai:sdd-preflight` and `/gentle:sdd-preflight` are the explicit preflight commands for agent/orchestrator use. If the user explicitly changes SDD preferences later in the same session, follow the new instruction.
|
|
82
|
+
|
|
83
|
+
## Init Guard
|
|
84
|
+
|
|
85
|
+
Before any SDD flow, make sure project context exists.
|
|
86
|
+
|
|
87
|
+
In this Pi package, the default local artifact is:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
openspec/config.yaml
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If it is missing, ask the user for the minimal information needed or run `/sdd-init` if available. This init guard runs after the session preflight gate above; project config presence or absence never substitutes for session preflight choices. Do not proceed with a substantial SDD flow while pretending project context, testing capability, or session preflight choices are known.
|
|
94
|
+
|
|
95
|
+
## Artifact Store Policy
|
|
96
|
+
|
|
97
|
+
This package does not provide persistent memory by itself.
|
|
98
|
+
|
|
99
|
+
- Default: `openspec` artifacts in the repo.
|
|
100
|
+
- If a separate memory package is installed and callable, memory/hybrid flows may be used.
|
|
101
|
+
- Never claim memory exists because Gentle AI is installed.
|
|
102
|
+
|
|
103
|
+
## Execution Mode
|
|
104
|
+
|
|
105
|
+
Use the session's SDD preflight choice:
|
|
106
|
+
|
|
107
|
+
- `interactive`: default, pause between major phases and ask whether to continue.
|
|
108
|
+
- `auto`: run phases back-to-back when the user explicitly wants speed and trusts the flow.
|
|
109
|
+
|
|
110
|
+
In interactive mode, between phases:
|
|
111
|
+
|
|
112
|
+
1. show concise phase result;
|
|
113
|
+
2. state next phase;
|
|
114
|
+
3. ask whether to continue or adjust.
|
|
115
|
+
|
|
116
|
+
Interactive approval is phase-scoped. A user response such as "continue", "dale", or "go on" approves only the immediate next phase, not the rest of the SDD pipeline. Do not treat a generated artifact as approved until the user has had a chance to review or explicitly delegate that review.
|
|
117
|
+
|
|
118
|
+
Before `sdd-proposal` in interactive mode, offer the user a proposal question round instead of silently deciding whether the proposal is clear enough. Explain that the questions are meant to improve the PRD/proposal by uncovering business understanding, business rules, implications, impact, edge cases, and product tradeoffs. Prefer 3–5 concrete product questions per round, then summarize the resulting assumptions and ask whether the user wants to correct anything or run a second question round. Cover business/product/PRD decisions: business problem, target users and situations, business rules, product outcome, current-state gap, implications and impact, edge cases, decision gaps, first-slice scope boundaries, non-goals, product constraints, and business tradeoffs. Do not ask about test commands, PR shape, changed-line budget, or other harness mechanics at proposal time unless the user explicitly asks to discuss delivery.
|
|
119
|
+
|
|
120
|
+
## Result Contract
|
|
121
|
+
|
|
122
|
+
Every phase result should include:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
status
|
|
126
|
+
executive_summary
|
|
127
|
+
artifacts
|
|
128
|
+
next_recommended
|
|
129
|
+
risks
|
|
130
|
+
skill_resolution
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The parent should synthesize these envelopes, not paste long raw reports unless needed.
|
|
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
|
+
|
|
139
|
+
## Strict TDD Forwarding
|
|
140
|
+
|
|
141
|
+
For `sdd-apply` and `sdd-verify`, read `openspec/config.yaml` when present.
|
|
142
|
+
|
|
143
|
+
If it declares strict TDD and a test command, include a non-negotiable instruction in the phase prompt:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
STRICT TDD MODE IS ACTIVE. Test runner: <command>. Follow RED, GREEN, TRIANGULATE, REFACTOR. Record evidence.
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Do not rely on the child agent to discover this independently.
|
|
150
|
+
|
|
151
|
+
## Review Workload Guard
|
|
152
|
+
|
|
153
|
+
After `sdd-tasks` and before `sdd-apply`, inspect the task output for review workload risk.
|
|
154
|
+
|
|
155
|
+
If estimated changed lines exceed 400, chained PRs are recommended, or a decision is needed, pause and ask unless the user already approved a delivery strategy.
|
|
156
|
+
|
|
157
|
+
Automatic mode does not override reviewer burnout protection.
|
package/extensions/gentle-ai.ts
CHANGED
|
@@ -93,42 +93,41 @@ 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;
|
|
119
|
+
function getSddWorkflowPath(): string {
|
|
120
|
+
return join(ASSETS_DIR, "sdd-orchestrator-workflow.md");
|
|
121
|
+
}
|
|
122
|
+
|
|
126
123
|
function getOrchestratorPrompt(): string {
|
|
127
124
|
if (orchestratorPromptCache === null) {
|
|
128
125
|
orchestratorPromptCache = readFileSync(
|
|
129
126
|
join(ASSETS_DIR, "orchestrator.md"),
|
|
130
127
|
"utf8",
|
|
131
|
-
)
|
|
128
|
+
)
|
|
129
|
+
.replaceAll("{{GENTLE_PI_SDD_WORKFLOW_PATH}}", getSddWorkflowPath())
|
|
130
|
+
.trim();
|
|
132
131
|
}
|
|
133
132
|
return orchestratorPromptCache;
|
|
134
133
|
}
|
|
@@ -2296,7 +2295,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2296
2295
|
join(ctx.cwd, ".atl", "skill-registry.md"),
|
|
2297
2296
|
);
|
|
2298
2297
|
const staleSddAssets = sddGlobalAssetDriftCount();
|
|
2299
|
-
const
|
|
2298
|
+
const localSddAgentOverrides = sddLocalAgentOverrideCount(ctx.cwd);
|
|
2300
2299
|
const modelConfig = await readSavedModelConfigAsync(ctx.cwd);
|
|
2301
2300
|
const engramActive = hasWritableEngramTool(pi);
|
|
2302
2301
|
const lines = [
|
|
@@ -2304,7 +2303,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2304
2303
|
`${agentsInstalled ? "pass" : "fail"}: Global SDD agents ${agentsInstalled ? "installed" : "missing"}`,
|
|
2305
2304
|
`${chainsInstalled ? "pass" : "fail"}: Global SDD chains ${chainsInstalled ? "installed" : "missing"}`,
|
|
2306
2305
|
`${staleSddAssets === 0 ? "pass" : "warn"}: Global SDD asset drift ${staleSddAssets} file(s)`,
|
|
2307
|
-
`${
|
|
2306
|
+
`${localSddAgentOverrides === 0 ? "pass" : "warn"}: Project-local SDD agent overrides ${localSddAgentOverrides} file(s)`,
|
|
2308
2307
|
`${openspecConfigured ? "pass" : "warn"}: OpenSpec config ${openspecConfigured ? "present" : "missing"}`,
|
|
2309
2308
|
`${skillRegistryPresent ? "pass" : "warn"}: Skill registry ${skillRegistryPresent ? "present" : "missing"}`,
|
|
2310
2309
|
`${modelConfig.status === "invalid" ? "fail" : "pass"}: Global model config ${modelConfig.status}`,
|
|
@@ -2317,6 +2316,9 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2317
2316
|
if (modelConfig.status === "invalid") {
|
|
2318
2317
|
lines.push(`remedy: fix or remove ${modelConfig.path}`);
|
|
2319
2318
|
}
|
|
2319
|
+
if (localSddAgentOverrides > 0) {
|
|
2320
|
+
lines.push("remedy: remove project-local SDD agent overrides unless intentionally debugging package assets");
|
|
2321
|
+
}
|
|
2320
2322
|
ctx.ui.notify(
|
|
2321
2323
|
lines.join("\n"),
|
|
2322
2324
|
lines.some((line) => line.startsWith("fail:")) ? "warning" : "info",
|
|
@@ -2337,7 +2339,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2337
2339
|
join(ctx.cwd, "openspec", "config.yaml"),
|
|
2338
2340
|
);
|
|
2339
2341
|
const staleSddAssets = sddGlobalAssetDriftCount();
|
|
2340
|
-
const
|
|
2342
|
+
const localSddAgentOverrides = sddLocalAgentOverrideCount(ctx.cwd);
|
|
2341
2343
|
const modelConfig = await readModelConfigAsync(ctx.cwd);
|
|
2342
2344
|
ctx.ui.notify(
|
|
2343
2345
|
[
|
|
@@ -2350,16 +2352,16 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
2350
2352
|
? " — run /gentle-ai:install-sdd --force to refresh intentionally"
|
|
2351
2353
|
: ""
|
|
2352
2354
|
}`,
|
|
2353
|
-
`Project-local SDD
|
|
2354
|
-
|
|
2355
|
-
? " —
|
|
2355
|
+
`Project-local SDD agent overrides: ${localSddAgentOverrides} file(s)${
|
|
2356
|
+
localSddAgentOverrides > 0
|
|
2357
|
+
? " — local SDD agents shadow package assets; remove them unless intentionally debugging"
|
|
2356
2358
|
: ""
|
|
2357
2359
|
}`,
|
|
2358
2360
|
`OpenSpec config: ${openspecConfigured ? "present" : "missing"}`,
|
|
2359
2361
|
`Global model config: ${existsSync(modelConfigPath(ctx.cwd)) ? "present" : "missing"}`,
|
|
2360
2362
|
...describeModelConfig(ctx.cwd, modelConfig),
|
|
2361
2363
|
].join("\n"),
|
|
2362
|
-
staleSddAssets > 0 ||
|
|
2364
|
+
staleSddAssets > 0 || localSddAgentOverrides > 0 ? "warning" : "info",
|
|
2363
2365
|
);
|
|
2364
2366
|
},
|
|
2365
2367
|
});
|
|
@@ -21,7 +21,7 @@ const EXCLUDE_NAMES = new Set(["_shared", "skill-registry"]);
|
|
|
21
21
|
const EXCLUDE_PREFIXES = ["sdd-"];
|
|
22
22
|
const ATL_IGNORE_ENTRY = ".atl/";
|
|
23
23
|
const WATCH_DEBOUNCE_MS = 500;
|
|
24
|
-
const REGISTRY_SCHEMA_VERSION =
|
|
24
|
+
const REGISTRY_SCHEMA_VERSION = 6;
|
|
25
25
|
const NO_SKILL_REGISTRY_FLAG = "no-skill-registry";
|
|
26
26
|
const NO_SKILL_REGISTRY_ENV = "GENTLE_PI_NO_SKILL_REGISTRY";
|
|
27
27
|
const LEGACY_PROJECT_REGISTRY_REL_PATH = ".pi/extensions/skill-registry.ts";
|
|
@@ -96,23 +96,30 @@ function projectSkillDirs(cwd: string): string[] {
|
|
|
96
96
|
|
|
97
97
|
async function findSkillFiles(root: string): Promise<string[]> {
|
|
98
98
|
if (!(await pathExists(root))) return [];
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
99
106
|
const out: string[] = [];
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
let entries;
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
const skillDir = join(root, entry.name);
|
|
109
|
+
let dirInfo;
|
|
104
110
|
try {
|
|
105
|
-
|
|
111
|
+
dirInfo = await stat(skillDir);
|
|
106
112
|
} catch {
|
|
107
113
|
continue;
|
|
108
114
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
if (!dirInfo.isDirectory()) continue;
|
|
116
|
+
|
|
117
|
+
const candidate = join(skillDir, "SKILL.md");
|
|
118
|
+
try {
|
|
119
|
+
const skillInfo = await stat(candidate);
|
|
120
|
+
if (skillInfo.isFile()) out.push(candidate);
|
|
121
|
+
} catch {
|
|
122
|
+
// Missing or unreadable skill files are ignored; the registry is best-effort.
|
|
116
123
|
}
|
|
117
124
|
}
|
|
118
125
|
return out.sort();
|
|
@@ -512,6 +519,7 @@ async function startSkillRegistryWatcher(
|
|
|
512
519
|
export const __testing = {
|
|
513
520
|
projectSkillDirs,
|
|
514
521
|
userSkillDirs,
|
|
522
|
+
findSkillFiles,
|
|
515
523
|
uniqueExistingDirs,
|
|
516
524
|
dedupeBySkillName,
|
|
517
525
|
scopeForPath,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "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",
|
|
@@ -146,6 +146,21 @@ test("SDD chain assets distinguish interactive gates from auto execution", async
|
|
|
146
146
|
assert.match(fullChain, /must stop at each phase boundary/i);
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
+
test("orchestrator lazy-loads detailed SDD workflow", async () => {
|
|
150
|
+
const orchestrator = await readFile(join(ROOT, "assets/orchestrator.md"), "utf8");
|
|
151
|
+
const workflow = await readFile(join(ROOT, "assets/sdd-orchestrator-workflow.md"), "utf8");
|
|
152
|
+
|
|
153
|
+
assert.match(orchestrator, /## SDD Workflow \(lazy-loaded\)/);
|
|
154
|
+
assert.match(orchestrator, /\{\{GENTLE_PI_SDD_WORKFLOW_PATH\}\}/);
|
|
155
|
+
assert.doesNotMatch(orchestrator, /## Native SDD Dispatcher/);
|
|
156
|
+
assert.match(workflow, /## Native SDD Dispatcher/);
|
|
157
|
+
assert.match(workflow, /## SDD Status Contract/);
|
|
158
|
+
assert.match(workflow, /## Execution Mode/);
|
|
159
|
+
assert.match(workflow, /## Strict TDD Forwarding/);
|
|
160
|
+
assert.match(workflow, /## Review Workload Guard/);
|
|
161
|
+
assert.match(workflow, /## Result Contract/);
|
|
162
|
+
});
|
|
163
|
+
|
|
149
164
|
test("persistent harness prompt assets do not hardcode Spanish SDD artifact copy", async () => {
|
|
150
165
|
const files = [
|
|
151
166
|
...(await collectTextFiles(join(ROOT, "assets"))),
|
|
@@ -203,6 +203,11 @@ async function run() {
|
|
|
203
203
|
assert.doesNotMatch(promptResult.systemPrompt, /default\s*\|\s*sonnet\s*\|\s*Non-SDD general delegation/);
|
|
204
204
|
assert.match(promptResult.systemPrompt, /openspec\/config\.yaml.*not session preflight/s);
|
|
205
205
|
assert.match(promptResult.systemPrompt, /Do not mark SDD preflight complete/);
|
|
206
|
+
assert.match(
|
|
207
|
+
promptResult.systemPrompt,
|
|
208
|
+
new RegExp(`${ROOT.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}.*assets.*sdd-orchestrator-workflow\\.md`),
|
|
209
|
+
);
|
|
210
|
+
assert.doesNotMatch(promptResult.systemPrompt, /\{\{GENTLE_PI_SDD_WORKFLOW_PATH\}\}/);
|
|
206
211
|
await writeFile(
|
|
207
212
|
join(globalConfigHome, "persona.json"),
|
|
208
213
|
'{"mode":"neutral"}\n',
|
|
@@ -668,12 +673,13 @@ async function run() {
|
|
|
668
673
|
await mkdir(join(staleAssetsCwd, ".pi", "gentle-ai", "support"), { recursive: true });
|
|
669
674
|
await writeFile(join(staleAssetsCwd, ".pi", "agents", "sdd-apply.md"), "stale apply\n");
|
|
670
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");
|
|
671
677
|
await writeFile(join(staleAssetsCwd, ".pi", "chains", "sdd-full.chain.md"), "stale chain\n");
|
|
672
678
|
await writeFile(join(staleAssetsCwd, ".pi", "gentle-ai", "support", "sdd-status-contract.md"), "stale status contract\n");
|
|
673
679
|
const ctx = createCtx(staleAssetsCwd, true);
|
|
674
680
|
await commands.get("gentle-ai:status").handler("", ctx);
|
|
675
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Project-local SDD
|
|
676
|
-
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/);
|
|
677
683
|
await commands.get("gentle-ai:doctor").handler("", ctx);
|
|
678
684
|
assert.match(ctx.ui.notifications.at(-1).message, /el Gentleman doctor/);
|
|
679
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
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
@@ -114,6 +114,36 @@ test("uniqueExistingDirs normalizes duplicates and ignores missing roots", async
|
|
|
114
114
|
);
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
test("findSkillFiles scans one skill directory level only", async () => {
|
|
118
|
+
const root = join(tmpdir(), `gentle-pi-shallow-${Date.now()}`);
|
|
119
|
+
const skillPath = join(root, "docs", "SKILL.md");
|
|
120
|
+
const nestedSkillPath = join(root, "fixtures", "nested", "SKILL.md");
|
|
121
|
+
mkdirSync(dirname(skillPath), { recursive: true });
|
|
122
|
+
mkdirSync(dirname(nestedSkillPath), { recursive: true });
|
|
123
|
+
writeFileSync(skillPath, "---\nname: docs\ndescription: Docs.\n---\n");
|
|
124
|
+
writeFileSync(nestedSkillPath, "---\nname: nested\ndescription: Nested fixture.\n---\n");
|
|
125
|
+
|
|
126
|
+
assert.deepEqual(await __testing.findSkillFiles(root), [skillPath]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("findSkillFiles follows symlinked skill directories", async (t) => {
|
|
130
|
+
const root = join(tmpdir(), `gentle-pi-symlink-root-${Date.now()}`);
|
|
131
|
+
const realSkillDir = join(tmpdir(), `gentle-pi-symlink-target-${Date.now()}`);
|
|
132
|
+
const linkedSkillDir = join(root, "linked");
|
|
133
|
+
const skillPath = join(linkedSkillDir, "SKILL.md");
|
|
134
|
+
mkdirSync(root, { recursive: true });
|
|
135
|
+
mkdirSync(realSkillDir, { recursive: true });
|
|
136
|
+
writeFileSync(join(realSkillDir, "SKILL.md"), "---\nname: linked\ndescription: Linked skill.\n---\n");
|
|
137
|
+
try {
|
|
138
|
+
symlinkSync(realSkillDir, linkedSkillDir, "dir");
|
|
139
|
+
} catch (error) {
|
|
140
|
+
t.skip(`symlink creation unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
assert.deepEqual(await __testing.findSkillFiles(root), [skillPath]);
|
|
145
|
+
});
|
|
146
|
+
|
|
117
147
|
test("skill registry watchers close on shutdown", async () => {
|
|
118
148
|
const root = join(tmpdir(), `gentle-pi-watchers-${Date.now()}`);
|
|
119
149
|
const skillPath = join(root, "skills", "docs", "SKILL.md");
|