gentle-pi 0.1.18 → 0.1.19
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.
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sdd-init
|
|
3
3
|
description: Initialize project SDD context, testing capabilities, and skill registry.
|
|
4
|
+
model: openai-codex/gpt-5.3-codex
|
|
4
5
|
tools: read, grep, glob, write, bash
|
|
5
6
|
inheritProjectContext: true
|
|
6
7
|
---
|
|
@@ -8,7 +9,8 @@ inheritProjectContext: true
|
|
|
8
9
|
You are the SDD init executor for Gentle AI.
|
|
9
10
|
|
|
10
11
|
- Inspect the project stack, test runner, conventions, and existing docs.
|
|
11
|
-
-
|
|
12
|
+
- If `openspec/config.yaml` is missing, create it automatically with project context, `strict_tdd`, phase rules, and testing runner details.
|
|
13
|
+
- If `openspec/config.yaml` already exists, read it, summarize the current SDD/testing configuration, and do not block the caller. Update only safe derived context when explicitly necessary; never destructively rewrite user-maintained SDD configuration.
|
|
12
14
|
- Ensure `.atl/skill-registry.md` exists when skill registry data is available, or report that it is missing.
|
|
13
15
|
- Do NOT launch child subagents. Parent/orchestrator owns delegation.
|
|
14
16
|
- Return the standard phase envelope with status, executive_summary, artifacts, next_recommended, risks, and skill_resolution.
|
|
@@ -4,13 +4,15 @@ description: Run the full SDD lifecycle for a change when explicitly approved.
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## sdd-init
|
|
7
|
+
|
|
7
8
|
output: init.md
|
|
8
9
|
outputMode: file-only
|
|
9
10
|
progress: true
|
|
10
11
|
|
|
11
|
-
Initialize
|
|
12
|
+
Initialize SDD context for {task} before any planning or implementation. If `openspec/config.yaml` is missing, inspect the project and create it automatically. If it already exists, read it, refresh only safe derived context when appropriate, and report the current SDD/testing configuration without blocking the chain.
|
|
12
13
|
|
|
13
14
|
## sdd-explore
|
|
15
|
+
|
|
14
16
|
reads: init.md
|
|
15
17
|
output: exploration.md
|
|
16
18
|
outputMode: file-only
|
|
@@ -19,6 +21,7 @@ progress: true
|
|
|
19
21
|
Explore {task}. Identify scope, risks, dependencies, prior art, and whether the change should proceed into proposal.
|
|
20
22
|
|
|
21
23
|
## sdd-proposal
|
|
24
|
+
|
|
22
25
|
reads: exploration.md
|
|
23
26
|
output: proposal.md
|
|
24
27
|
outputMode: file-only
|
|
@@ -27,6 +30,7 @@ progress: true
|
|
|
27
30
|
Create or update the OpenSpec proposal for {task} using the exploration notes and the previous step output.
|
|
28
31
|
|
|
29
32
|
## sdd-spec
|
|
33
|
+
|
|
30
34
|
reads: proposal.md
|
|
31
35
|
output: spec.md
|
|
32
36
|
outputMode: file-only
|
|
@@ -35,6 +39,7 @@ progress: true
|
|
|
35
39
|
Write delta specs for {task} from the approved proposal. Preserve RFC 2119 requirements and Given/When/Then scenarios.
|
|
36
40
|
|
|
37
41
|
## sdd-design
|
|
42
|
+
|
|
38
43
|
reads: proposal.md+spec.md
|
|
39
44
|
output: design.md
|
|
40
45
|
outputMode: file-only
|
|
@@ -43,6 +48,7 @@ progress: true
|
|
|
43
48
|
Design the technical approach for {task} using the proposal, specs, and previous outputs. Call out review and judgment risks.
|
|
44
49
|
|
|
45
50
|
## sdd-tasks
|
|
51
|
+
|
|
46
52
|
reads: proposal.md+spec.md+design.md
|
|
47
53
|
output: tasks.md
|
|
48
54
|
outputMode: file-only
|
|
@@ -51,6 +57,7 @@ progress: true
|
|
|
51
57
|
Create strict-TDD, reviewable implementation tasks for {task}. Include the required Review Workload Forecast guard lines and PR split recommendation.
|
|
52
58
|
|
|
53
59
|
## sdd-apply
|
|
60
|
+
|
|
54
61
|
reads: proposal.md+spec.md+design.md+tasks.md
|
|
55
62
|
output: apply-progress.md
|
|
56
63
|
outputMode: file-only
|
|
@@ -59,6 +66,7 @@ progress: true
|
|
|
59
66
|
Implement only approved tasks for {task}; enforce strict TDD when active and stop before writing if workload decisions are unresolved. Update OpenSpec tasks and apply-progress with evidence.
|
|
60
67
|
|
|
61
68
|
## sdd-verify
|
|
69
|
+
|
|
62
70
|
reads: proposal.md+spec.md+design.md+tasks.md+apply-progress.md
|
|
63
71
|
output: verify-report.md
|
|
64
72
|
outputMode: file-only
|
|
@@ -67,6 +75,7 @@ progress: true
|
|
|
67
75
|
Verify {task} against specs, design, tasks, implementation, apply-progress, strict TDD evidence, assertion quality, and review workload boundaries.
|
|
68
76
|
|
|
69
77
|
## sdd-archive
|
|
78
|
+
|
|
70
79
|
reads: verify-report.md
|
|
71
80
|
output: archive-report.md
|
|
72
81
|
outputMode: file-only
|
|
@@ -3,7 +3,17 @@ name: sdd-plan
|
|
|
3
3
|
description: Plan an SDD change through proposal, spec, design, and tasks.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
## sdd-init
|
|
7
|
+
|
|
8
|
+
output: init.md
|
|
9
|
+
outputMode: file-only
|
|
10
|
+
progress: true
|
|
11
|
+
|
|
12
|
+
Initialize SDD context for {task} before planning. If `openspec/config.yaml` is missing, inspect the project and create it automatically. If it already exists, read it and report the current SDD/testing configuration without blocking the chain.
|
|
13
|
+
|
|
6
14
|
## sdd-proposal
|
|
15
|
+
|
|
16
|
+
reads: init.md
|
|
7
17
|
output: proposal.md
|
|
8
18
|
outputMode: file-only
|
|
9
19
|
progress: true
|
|
@@ -11,6 +21,7 @@ progress: true
|
|
|
11
21
|
Create or update the OpenSpec proposal for {task}. Use prior exploration if it is available in the project artifacts.
|
|
12
22
|
|
|
13
23
|
## sdd-spec
|
|
24
|
+
|
|
14
25
|
reads: proposal.md
|
|
15
26
|
output: spec.md
|
|
16
27
|
outputMode: file-only
|
|
@@ -19,6 +30,7 @@ progress: true
|
|
|
19
30
|
Write delta specs for {task} using the proposal and previous output. Keep requirements and scenarios acceptance-focused.
|
|
20
31
|
|
|
21
32
|
## sdd-design
|
|
33
|
+
|
|
22
34
|
reads: proposal.md+spec.md
|
|
23
35
|
output: design.md
|
|
24
36
|
outputMode: file-only
|
|
@@ -27,6 +39,7 @@ progress: true
|
|
|
27
39
|
Design the technical approach for {task}. Preserve native SDD orchestration intent and identify review/judgment risks.
|
|
28
40
|
|
|
29
41
|
## sdd-tasks
|
|
42
|
+
|
|
30
43
|
reads: proposal.md+spec.md+design.md
|
|
31
44
|
output: tasks.md
|
|
32
45
|
outputMode: file-only
|
|
@@ -3,7 +3,17 @@ name: sdd-verify
|
|
|
3
3
|
description: Apply, verify, and optionally archive an already planned SDD change.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
## sdd-init
|
|
7
|
+
|
|
8
|
+
output: init.md
|
|
9
|
+
outputMode: file-only
|
|
10
|
+
progress: true
|
|
11
|
+
|
|
12
|
+
Initialize SDD context for {task} before apply/verify. If `openspec/config.yaml` is missing, inspect the project and create it automatically. If it already exists, read it and report the current SDD/testing configuration without blocking the chain.
|
|
13
|
+
|
|
6
14
|
## sdd-apply
|
|
15
|
+
|
|
16
|
+
reads: init.md
|
|
7
17
|
output: apply-progress.md
|
|
8
18
|
outputMode: file-only
|
|
9
19
|
progress: true
|
|
@@ -11,7 +21,8 @@ progress: true
|
|
|
11
21
|
Implement pending approved tasks for {task}; update OpenSpec tasks and apply-progress with strict TDD evidence.
|
|
12
22
|
|
|
13
23
|
## sdd-verify
|
|
14
|
-
|
|
24
|
+
|
|
25
|
+
reads: init.md+apply-progress.md
|
|
15
26
|
output: verify-report.md
|
|
16
27
|
outputMode: file-only
|
|
17
28
|
progress: true
|
|
@@ -19,6 +30,7 @@ progress: true
|
|
|
19
30
|
Run focused and full verification for {task} using the apply-progress and project artifacts. Include review/judgment blockers.
|
|
20
31
|
|
|
21
32
|
## sdd-archive
|
|
33
|
+
|
|
22
34
|
reads: verify-report.md
|
|
23
35
|
output: archive-report.md
|
|
24
36
|
outputMode: file-only
|
package/extensions/sdd-init.ts
CHANGED
|
@@ -1,8 +1,62 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { basename, dirname, join } from "node:path";
|
|
9
|
+
type ExtensionAPI = any;
|
|
4
10
|
|
|
5
11
|
const CONFIG_REL_PATH = "openspec/config.yaml";
|
|
12
|
+
const MAX_SCAN_FILES = 5_000;
|
|
13
|
+
const IGNORED_DIRS = new Set([
|
|
14
|
+
".git",
|
|
15
|
+
".hg",
|
|
16
|
+
".svn",
|
|
17
|
+
"node_modules",
|
|
18
|
+
"vendor",
|
|
19
|
+
"dist",
|
|
20
|
+
"build",
|
|
21
|
+
"target",
|
|
22
|
+
"coverage",
|
|
23
|
+
".next",
|
|
24
|
+
".nuxt",
|
|
25
|
+
".turbo",
|
|
26
|
+
".cache",
|
|
27
|
+
"__pycache__",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
interface PackageJson {
|
|
31
|
+
name?: string;
|
|
32
|
+
type?: string;
|
|
33
|
+
scripts?: Record<string, string>;
|
|
34
|
+
dependencies?: Record<string, string>;
|
|
35
|
+
devDependencies?: Record<string, string>;
|
|
36
|
+
peerDependencies?: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Detection {
|
|
40
|
+
projectName: string;
|
|
41
|
+
stack: string[];
|
|
42
|
+
packageManager?: string;
|
|
43
|
+
markers: string[];
|
|
44
|
+
testCommand?: string;
|
|
45
|
+
testFramework?: string;
|
|
46
|
+
coverageCommand?: string;
|
|
47
|
+
lintCommand?: string;
|
|
48
|
+
typecheckCommand?: string;
|
|
49
|
+
formatCommand?: string;
|
|
50
|
+
testLayers: {
|
|
51
|
+
unit?: string;
|
|
52
|
+
integration?: string;
|
|
53
|
+
e2e?: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function yamlString(value: string): string {
|
|
58
|
+
return JSON.stringify(value);
|
|
59
|
+
}
|
|
6
60
|
|
|
7
61
|
function escapeBlockScalar(value: string): string {
|
|
8
62
|
return value
|
|
@@ -11,26 +65,315 @@ function escapeBlockScalar(value: string): string {
|
|
|
11
65
|
.join("\n");
|
|
12
66
|
}
|
|
13
67
|
|
|
14
|
-
function
|
|
68
|
+
function readJson<T>(path: string): T | undefined {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
71
|
+
} catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function hasFile(cwd: string, rel: string): boolean {
|
|
77
|
+
return existsSync(join(cwd, rel));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function walkProject(cwd: string): string[] {
|
|
81
|
+
const out: string[] = [];
|
|
82
|
+
const stack = [cwd];
|
|
83
|
+
while (stack.length > 0 && out.length < MAX_SCAN_FILES) {
|
|
84
|
+
const dir = stack.pop()!;
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
88
|
+
} catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (out.length >= MAX_SCAN_FILES) break;
|
|
93
|
+
if (entry.isDirectory()) {
|
|
94
|
+
if (!IGNORED_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (entry.isFile()) out.push(join(dir, entry.name).slice(cwd.length + 1));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out.sort();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function deps(pkg: PackageJson | undefined): Set<string> {
|
|
104
|
+
return new Set([
|
|
105
|
+
...Object.keys(pkg?.dependencies ?? {}),
|
|
106
|
+
...Object.keys(pkg?.devDependencies ?? {}),
|
|
107
|
+
...Object.keys(pkg?.peerDependencies ?? {}),
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function detectPackageManager(cwd: string): string | undefined {
|
|
112
|
+
if (hasFile(cwd, "pnpm-lock.yaml")) return "pnpm";
|
|
113
|
+
if (hasFile(cwd, "yarn.lock")) return "yarn";
|
|
114
|
+
if (hasFile(cwd, "bun.lockb") || hasFile(cwd, "bun.lock")) return "bun";
|
|
115
|
+
if (hasFile(cwd, "package-lock.json")) return "npm";
|
|
116
|
+
if (hasFile(cwd, "package.json")) return "npm";
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function runScript(pm: string | undefined, script: string): string {
|
|
121
|
+
if (pm === "yarn") return `yarn ${script}`;
|
|
122
|
+
if (pm === "bun") return `bun run ${script}`;
|
|
123
|
+
return `${pm ?? "npm"} run ${script}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function scriptCommand(
|
|
127
|
+
pm: string | undefined,
|
|
128
|
+
scripts: Record<string, string> | undefined,
|
|
129
|
+
candidates: string[],
|
|
130
|
+
): string | undefined {
|
|
131
|
+
if (!scripts) return undefined;
|
|
132
|
+
for (const name of candidates) {
|
|
133
|
+
if (scripts[name])
|
|
134
|
+
return name === "test" && pm !== "bun"
|
|
135
|
+
? `${pm ?? "npm"} test`
|
|
136
|
+
: runScript(pm, name);
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function detectNode(cwd: string, files: string[], detection: Detection): void {
|
|
142
|
+
const pkg = readJson<PackageJson>(join(cwd, "package.json"));
|
|
143
|
+
if (
|
|
144
|
+
!pkg &&
|
|
145
|
+
!files.some(
|
|
146
|
+
(f) =>
|
|
147
|
+
f.endsWith(".ts") ||
|
|
148
|
+
f.endsWith(".tsx") ||
|
|
149
|
+
f.endsWith(".js") ||
|
|
150
|
+
f.endsWith(".jsx"),
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
return;
|
|
154
|
+
const pm = detectPackageManager(cwd);
|
|
155
|
+
detection.packageManager = pm;
|
|
156
|
+
detection.stack.push(
|
|
157
|
+
pkg?.type === "module" ? "Node.js/TypeScript ESM" : "Node.js/TypeScript",
|
|
158
|
+
);
|
|
159
|
+
if (pkg?.name) detection.projectName = pkg.name;
|
|
160
|
+
if (hasFile(cwd, "package.json")) detection.markers.push("package.json");
|
|
161
|
+
if (hasFile(cwd, "tsconfig.json")) detection.markers.push("tsconfig.json");
|
|
162
|
+
if (pm) detection.markers.push(`${pm} package manager`);
|
|
163
|
+
|
|
164
|
+
const allDeps = deps(pkg);
|
|
165
|
+
if (allDeps.has("react")) detection.stack.push("React");
|
|
166
|
+
if (allDeps.has("next")) detection.stack.push("Next.js");
|
|
167
|
+
if (allDeps.has("vue")) detection.stack.push("Vue");
|
|
168
|
+
if (allDeps.has("svelte")) detection.stack.push("Svelte");
|
|
169
|
+
if (allDeps.has("@earendil-works/pi-coding-agent"))
|
|
170
|
+
detection.stack.push("Pi extension package");
|
|
171
|
+
|
|
172
|
+
detection.testCommand = scriptCommand(pm, pkg?.scripts, [
|
|
173
|
+
"test",
|
|
174
|
+
"vitest",
|
|
175
|
+
"jest",
|
|
176
|
+
"unit",
|
|
177
|
+
]);
|
|
178
|
+
if (!detection.testCommand) {
|
|
179
|
+
if (
|
|
180
|
+
allDeps.has("vitest") ||
|
|
181
|
+
files.some((f) => /^vitest\.config\./.test(basename(f)))
|
|
182
|
+
)
|
|
183
|
+
detection.testCommand = runScript(pm, "vitest");
|
|
184
|
+
else if (
|
|
185
|
+
allDeps.has("jest") ||
|
|
186
|
+
files.some((f) => /^jest\.config\./.test(basename(f)))
|
|
187
|
+
)
|
|
188
|
+
detection.testCommand = runScript(pm, "jest");
|
|
189
|
+
}
|
|
190
|
+
if (allDeps.has("vitest")) detection.testFramework = "Vitest";
|
|
191
|
+
else if (allDeps.has("jest")) detection.testFramework = "Jest";
|
|
192
|
+
else if (detection.testCommand) detection.testFramework = "package script";
|
|
193
|
+
|
|
194
|
+
detection.coverageCommand = scriptCommand(pm, pkg?.scripts, [
|
|
195
|
+
"coverage",
|
|
196
|
+
"test:coverage",
|
|
197
|
+
]);
|
|
198
|
+
detection.lintCommand = scriptCommand(pm, pkg?.scripts, [
|
|
199
|
+
"lint",
|
|
200
|
+
"check:lint",
|
|
201
|
+
]);
|
|
202
|
+
detection.typecheckCommand = scriptCommand(pm, pkg?.scripts, [
|
|
203
|
+
"typecheck",
|
|
204
|
+
"type-check",
|
|
205
|
+
"check:types",
|
|
206
|
+
]);
|
|
207
|
+
detection.formatCommand = scriptCommand(pm, pkg?.scripts, [
|
|
208
|
+
"format",
|
|
209
|
+
"fmt",
|
|
210
|
+
"prettier",
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
if (detection.testFramework)
|
|
214
|
+
detection.testLayers.unit = detection.testFramework;
|
|
215
|
+
if (allDeps.has("@testing-library/react") || allDeps.has("supertest"))
|
|
216
|
+
detection.testLayers.integration = "Testing Library / Supertest";
|
|
217
|
+
if (allDeps.has("playwright") || allDeps.has("@playwright/test"))
|
|
218
|
+
detection.testLayers.e2e = "Playwright";
|
|
219
|
+
else if (allDeps.has("cypress")) detection.testLayers.e2e = "Cypress";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function detectGo(cwd: string, files: string[], detection: Detection): void {
|
|
223
|
+
if (!hasFile(cwd, "go.mod")) return;
|
|
224
|
+
detection.stack.push("Go");
|
|
225
|
+
detection.markers.push("go.mod");
|
|
226
|
+
if (!detection.testCommand) detection.testCommand = "go test ./...";
|
|
227
|
+
if (!detection.testFramework) detection.testFramework = "go test";
|
|
228
|
+
detection.testLayers.unit ??= "go test";
|
|
229
|
+
if (files.some((f) => f.endsWith("_test.go")))
|
|
230
|
+
detection.testLayers.integration ??= "Go integration tests where present";
|
|
231
|
+
detection.coverageCommand ??= "go test -cover ./...";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function detectRust(cwd: string, detection: Detection): void {
|
|
235
|
+
if (!hasFile(cwd, "Cargo.toml")) return;
|
|
236
|
+
detection.stack.push("Rust");
|
|
237
|
+
detection.markers.push("Cargo.toml");
|
|
238
|
+
detection.testCommand ??= "cargo test";
|
|
239
|
+
detection.testFramework ??= "cargo test";
|
|
240
|
+
detection.testLayers.unit ??= "cargo test";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function detectPython(
|
|
244
|
+
cwd: string,
|
|
245
|
+
files: string[],
|
|
246
|
+
detection: Detection,
|
|
247
|
+
): void {
|
|
248
|
+
const hasPython =
|
|
249
|
+
hasFile(cwd, "pyproject.toml") ||
|
|
250
|
+
hasFile(cwd, "requirements.txt") ||
|
|
251
|
+
hasFile(cwd, "pytest.ini") ||
|
|
252
|
+
files.some((f) => f.endsWith(".py"));
|
|
253
|
+
if (!hasPython) return;
|
|
254
|
+
detection.stack.push("Python");
|
|
255
|
+
for (const marker of ["pyproject.toml", "requirements.txt", "pytest.ini"]) {
|
|
256
|
+
if (hasFile(cwd, marker)) detection.markers.push(marker);
|
|
257
|
+
}
|
|
258
|
+
if (
|
|
259
|
+
!detection.testCommand &&
|
|
260
|
+
(hasFile(cwd, "pytest.ini") ||
|
|
261
|
+
files.some((f) => f.startsWith("tests/") || f.endsWith("_test.py")))
|
|
262
|
+
) {
|
|
263
|
+
detection.testCommand = "pytest";
|
|
264
|
+
detection.testFramework = "pytest";
|
|
265
|
+
detection.testLayers.unit = "pytest";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function detectMakefile(cwd: string, detection: Detection): void {
|
|
270
|
+
const makefile = ["Makefile", "makefile"].find((f) => hasFile(cwd, f));
|
|
271
|
+
if (!makefile) return;
|
|
272
|
+
detection.markers.push(makefile);
|
|
273
|
+
let content = "";
|
|
274
|
+
try {
|
|
275
|
+
content = readFileSync(join(cwd, makefile), "utf8");
|
|
276
|
+
} catch {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (!detection.testCommand && /^test:/m.test(content))
|
|
280
|
+
detection.testCommand = "make test";
|
|
281
|
+
if (!detection.lintCommand && /^lint:/m.test(content))
|
|
282
|
+
detection.lintCommand = "make lint";
|
|
283
|
+
if (!detection.formatCommand && /^(fmt|format):/m.test(content))
|
|
284
|
+
detection.formatCommand = /^fmt:/m.test(content)
|
|
285
|
+
? "make fmt"
|
|
286
|
+
: "make format";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function detectProject(cwd: string): Detection {
|
|
290
|
+
const files = walkProject(cwd);
|
|
291
|
+
const detection: Detection = {
|
|
292
|
+
projectName: basename(cwd),
|
|
293
|
+
stack: [],
|
|
294
|
+
markers: [],
|
|
295
|
+
testLayers: {},
|
|
296
|
+
};
|
|
297
|
+
detectNode(cwd, files, detection);
|
|
298
|
+
detectGo(cwd, files, detection);
|
|
299
|
+
detectRust(cwd, detection);
|
|
300
|
+
detectPython(cwd, files, detection);
|
|
301
|
+
detectMakefile(cwd, detection);
|
|
302
|
+
if (hasFile(cwd, ".github/workflows"))
|
|
303
|
+
detection.markers.push("GitHub Actions");
|
|
304
|
+
detection.stack = [...new Set(detection.stack)];
|
|
305
|
+
detection.markers = [...new Set(detection.markers)];
|
|
306
|
+
return detection;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function renderContext(detection: Detection): string {
|
|
310
|
+
const lines = [
|
|
311
|
+
`${detection.projectName} is a ${detection.stack.length > 0 ? detection.stack.join(", ") : "software"} project.`,
|
|
312
|
+
`Detected markers: ${detection.markers.length > 0 ? detection.markers.join(", ") : "none"}.`,
|
|
313
|
+
];
|
|
314
|
+
if (detection.packageManager)
|
|
315
|
+
lines.push(`Package manager: ${detection.packageManager}.`);
|
|
316
|
+
if (detection.testCommand)
|
|
317
|
+
lines.push(`Primary test command: ${detection.testCommand}.`);
|
|
318
|
+
else
|
|
319
|
+
lines.push(
|
|
320
|
+
"No reliable test runner was detected; verify testing manually before enabling strict TDD.",
|
|
321
|
+
);
|
|
322
|
+
return lines.join("\n");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function renderConfig(detection: Detection): string {
|
|
326
|
+
const strictTdd = Boolean(detection.testCommand);
|
|
327
|
+
const testCommand = detection.testCommand ?? "";
|
|
328
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
329
|
+
const context = renderContext(detection);
|
|
15
330
|
const lines = [
|
|
16
331
|
`strict_tdd: ${strictTdd}`,
|
|
17
332
|
"context: |",
|
|
18
|
-
escapeBlockScalar(context
|
|
333
|
+
escapeBlockScalar(context),
|
|
19
334
|
"rules:",
|
|
335
|
+
" proposal:",
|
|
336
|
+
" require_problem_statement: true",
|
|
337
|
+
" spec:",
|
|
338
|
+
" require_acceptance_criteria: true",
|
|
339
|
+
" design:",
|
|
340
|
+
" require_tradeoffs: true",
|
|
341
|
+
" tasks:",
|
|
342
|
+
" protect_review_workload: true",
|
|
20
343
|
" apply:",
|
|
21
|
-
` test_command: ${testCommand}`,
|
|
344
|
+
` test_command: ${yamlString(testCommand)}`,
|
|
345
|
+
" verify:",
|
|
346
|
+
` test_command: ${yamlString(testCommand)}`,
|
|
22
347
|
"testing:",
|
|
348
|
+
` detected: ${yamlString(today)}`,
|
|
23
349
|
" runner:",
|
|
24
|
-
` command: ${testCommand}`,
|
|
350
|
+
` command: ${yamlString(testCommand)}`,
|
|
351
|
+
` framework: ${yamlString(detection.testFramework ?? "")}`,
|
|
352
|
+
" layers:",
|
|
353
|
+
` unit: ${yamlString(detection.testLayers.unit ?? "")}`,
|
|
354
|
+
` integration: ${yamlString(detection.testLayers.integration ?? "")}`,
|
|
355
|
+
` e2e: ${yamlString(detection.testLayers.e2e ?? "")}`,
|
|
356
|
+
" coverage:",
|
|
357
|
+
` command: ${yamlString(detection.coverageCommand ?? "")}`,
|
|
358
|
+
"quality:",
|
|
359
|
+
` lint: ${yamlString(detection.lintCommand ?? "")}`,
|
|
360
|
+
` typecheck: ${yamlString(detection.typecheckCommand ?? "")}`,
|
|
361
|
+
` format: ${yamlString(detection.formatCommand ?? "")}`,
|
|
25
362
|
"",
|
|
26
363
|
];
|
|
27
364
|
return lines.join("\n");
|
|
28
365
|
}
|
|
29
366
|
|
|
367
|
+
function ensureOpenSpecDirs(cwd: string): void {
|
|
368
|
+
mkdirSync(join(cwd, "openspec", "specs"), { recursive: true });
|
|
369
|
+
mkdirSync(join(cwd, "openspec", "changes", "archive"), { recursive: true });
|
|
370
|
+
}
|
|
371
|
+
|
|
30
372
|
export default function (pi: ExtensionAPI) {
|
|
31
373
|
pi.registerCommand("sdd-init", {
|
|
32
|
-
description:
|
|
33
|
-
|
|
374
|
+
description:
|
|
375
|
+
"Auto-detect project stack and bootstrap openspec/config.yaml for SDD.",
|
|
376
|
+
handler: async (_args: unknown, ctx: any) => {
|
|
34
377
|
const configPath = join(ctx.cwd, CONFIG_REL_PATH);
|
|
35
378
|
if (existsSync(configPath)) {
|
|
36
379
|
ctx.ui.notify(
|
|
@@ -40,42 +383,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
40
383
|
return;
|
|
41
384
|
}
|
|
42
385
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const TDD_CANCEL = "Cancel";
|
|
46
|
-
const tddChoice = await ctx.ui.select("Enable strict TDD for this project?", [
|
|
47
|
-
TDD_YES,
|
|
48
|
-
TDD_NO,
|
|
49
|
-
TDD_CANCEL,
|
|
50
|
-
]);
|
|
51
|
-
if (!tddChoice || tddChoice === TDD_CANCEL) {
|
|
52
|
-
ctx.ui.notify("sdd-init cancelled.", "info");
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const strictTdd = tddChoice === TDD_YES;
|
|
56
|
-
|
|
57
|
-
const testCommand = await ctx.ui.input(
|
|
58
|
-
"Test command",
|
|
59
|
-
"e.g. npm test, pnpm vitest, cargo test",
|
|
60
|
-
);
|
|
61
|
-
if (!testCommand) {
|
|
62
|
-
ctx.ui.notify("sdd-init cancelled (no test command).", "info");
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const context = await ctx.ui.input(
|
|
67
|
-
"Project context (one paragraph)",
|
|
68
|
-
"Describe the project, stack, and constraints.",
|
|
69
|
-
);
|
|
70
|
-
if (!context) {
|
|
71
|
-
ctx.ui.notify("sdd-init cancelled (no context).", "info");
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
386
|
+
const detection = detectProject(ctx.cwd);
|
|
387
|
+
ensureOpenSpecDirs(ctx.cwd);
|
|
75
388
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
76
|
-
writeFileSync(configPath, renderConfig(
|
|
389
|
+
writeFileSync(configPath, renderConfig(detection));
|
|
390
|
+
|
|
391
|
+
const testSummary = detection.testCommand
|
|
392
|
+
? `strict TDD enabled with \`${detection.testCommand}\``
|
|
393
|
+
: "strict TDD disabled because no test runner was detected";
|
|
77
394
|
ctx.ui.notify(
|
|
78
|
-
`Wrote ${CONFIG_REL_PATH}
|
|
395
|
+
`Wrote ${CONFIG_REL_PATH}: detected ${detection.stack.join(", ") || "project"}; ${testSummary}.`,
|
|
79
396
|
"info",
|
|
80
397
|
);
|
|
81
398
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
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",
|