gentle-pi 0.1.18 → 0.1.21
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,73 @@
|
|
|
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, relative } from "node:path";
|
|
9
|
+
type ExtensionAPI = any;
|
|
4
10
|
|
|
5
11
|
const CONFIG_REL_PATH = "openspec/config.yaml";
|
|
12
|
+
const MAX_SCAN_FILES = 20_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 CommandInfo {
|
|
40
|
+
scope: string;
|
|
41
|
+
command: string;
|
|
42
|
+
framework: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface Detection {
|
|
46
|
+
projectName: string;
|
|
47
|
+
stack: string[];
|
|
48
|
+
packageManagers: string[];
|
|
49
|
+
markers: string[];
|
|
50
|
+
evidence: string[];
|
|
51
|
+
testCommand?: string;
|
|
52
|
+
testFramework?: string;
|
|
53
|
+
coverageCommand?: string;
|
|
54
|
+
lintCommand?: string;
|
|
55
|
+
typecheckCommand?: string;
|
|
56
|
+
formatCommand?: string;
|
|
57
|
+
commands: {
|
|
58
|
+
unit: CommandInfo[];
|
|
59
|
+
integration: CommandInfo[];
|
|
60
|
+
e2e: CommandInfo[];
|
|
61
|
+
coverage: CommandInfo[];
|
|
62
|
+
lint: CommandInfo[];
|
|
63
|
+
typecheck: CommandInfo[];
|
|
64
|
+
format: CommandInfo[];
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function yamlString(value: string): string {
|
|
69
|
+
return JSON.stringify(value);
|
|
70
|
+
}
|
|
6
71
|
|
|
7
72
|
function escapeBlockScalar(value: string): string {
|
|
8
73
|
return value
|
|
@@ -11,26 +76,703 @@ function escapeBlockScalar(value: string): string {
|
|
|
11
76
|
.join("\n");
|
|
12
77
|
}
|
|
13
78
|
|
|
14
|
-
function
|
|
79
|
+
function readJson<T>(path: string): T | undefined {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
82
|
+
} catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function hasFile(cwd: string, rel: string): boolean {
|
|
88
|
+
return existsSync(join(cwd, rel));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function walkProject(cwd: string): string[] {
|
|
92
|
+
const out: string[] = [];
|
|
93
|
+
const stack = [cwd];
|
|
94
|
+
while (stack.length > 0 && out.length < MAX_SCAN_FILES) {
|
|
95
|
+
const dir = stack.pop()!;
|
|
96
|
+
let entries;
|
|
97
|
+
try {
|
|
98
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
99
|
+
} catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (out.length >= MAX_SCAN_FILES) break;
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
if (!IGNORED_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (entry.isFile()) out.push(relative(cwd, join(dir, entry.name)));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out.sort();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function deps(pkg: PackageJson | undefined): Set<string> {
|
|
115
|
+
return new Set([
|
|
116
|
+
...Object.keys(pkg?.dependencies ?? {}),
|
|
117
|
+
...Object.keys(pkg?.devDependencies ?? {}),
|
|
118
|
+
...Object.keys(pkg?.peerDependencies ?? {}),
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function detectPackageManagerAt(
|
|
123
|
+
cwd: string,
|
|
124
|
+
relDir: string,
|
|
125
|
+
): string | undefined {
|
|
126
|
+
const base = join(cwd, relDir);
|
|
127
|
+
if (existsSync(join(base, "pnpm-lock.yaml"))) return "pnpm";
|
|
128
|
+
if (existsSync(join(base, "yarn.lock"))) return "yarn";
|
|
129
|
+
if (existsSync(join(base, "bun.lockb")) || existsSync(join(base, "bun.lock")))
|
|
130
|
+
return "bun";
|
|
131
|
+
if (existsSync(join(base, "package-lock.json"))) return "npm";
|
|
132
|
+
if (existsSync(join(base, "package.json"))) return "npm";
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function commandInScope(scope: string, command: string): string {
|
|
137
|
+
return scope === "." ? command : `cd ${scope} && ${command}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function runScript(pm: string | undefined, script: string): string {
|
|
141
|
+
if (pm === "yarn") return `yarn ${script}`;
|
|
142
|
+
if (pm === "bun") return `bun run ${script}`;
|
|
143
|
+
return `${pm ?? "npm"} run ${script}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function scriptCommand(
|
|
147
|
+
pm: string | undefined,
|
|
148
|
+
scripts: Record<string, string> | undefined,
|
|
149
|
+
candidates: string[],
|
|
150
|
+
): { name: string; command: string } | undefined {
|
|
151
|
+
if (!scripts) return undefined;
|
|
152
|
+
for (const name of candidates) {
|
|
153
|
+
if (!scripts[name]) continue;
|
|
154
|
+
const command =
|
|
155
|
+
name === "test" && pm !== "bun"
|
|
156
|
+
? `${pm ?? "npm"} test`
|
|
157
|
+
: runScript(pm, name);
|
|
158
|
+
return { name, command };
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function addUnique(list: CommandInfo[], command: CommandInfo): void {
|
|
164
|
+
if (
|
|
165
|
+
list.some(
|
|
166
|
+
(item) =>
|
|
167
|
+
item.scope === command.scope && item.command === command.command,
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
return;
|
|
171
|
+
list.push(command);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function addMarker(detection: Detection, marker: string): void {
|
|
175
|
+
if (!detection.markers.includes(marker)) detection.markers.push(marker);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function addStack(detection: Detection, stack: string): void {
|
|
179
|
+
if (!detection.stack.includes(stack)) detection.stack.push(stack);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function addEvidence(detection: Detection, evidence: string): void {
|
|
183
|
+
if (!detection.evidence.includes(evidence)) detection.evidence.push(evidence);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface GenericHint {
|
|
187
|
+
marker: string;
|
|
188
|
+
stack: string;
|
|
189
|
+
testCommand?: string;
|
|
190
|
+
framework?: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const GENERIC_HINTS: GenericHint[] = [
|
|
194
|
+
{
|
|
195
|
+
marker: "mix.exs",
|
|
196
|
+
stack: "Elixir",
|
|
197
|
+
testCommand: "mix test",
|
|
198
|
+
framework: "ExUnit",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
marker: "rebar.config",
|
|
202
|
+
stack: "Erlang",
|
|
203
|
+
testCommand: "rebar3 eunit",
|
|
204
|
+
framework: "EUnit",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
marker: "gleam.toml",
|
|
208
|
+
stack: "Gleam",
|
|
209
|
+
testCommand: "gleam test",
|
|
210
|
+
framework: "gleam test",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
marker: "deno.json",
|
|
214
|
+
stack: "Deno",
|
|
215
|
+
testCommand: "deno test",
|
|
216
|
+
framework: "deno test",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
marker: "deno.jsonc",
|
|
220
|
+
stack: "Deno",
|
|
221
|
+
testCommand: "deno test",
|
|
222
|
+
framework: "deno test",
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
marker: "Gemfile",
|
|
226
|
+
stack: "Ruby",
|
|
227
|
+
testCommand: "bundle exec rake test",
|
|
228
|
+
framework: "Ruby test task",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
marker: "composer.json",
|
|
232
|
+
stack: "PHP",
|
|
233
|
+
testCommand: "composer test",
|
|
234
|
+
framework: "Composer test script",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
marker: "pom.xml",
|
|
238
|
+
stack: "Java/Maven",
|
|
239
|
+
testCommand: "mvn test",
|
|
240
|
+
framework: "Maven test",
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
marker: "build.gradle",
|
|
244
|
+
stack: "Java/Gradle",
|
|
245
|
+
testCommand: "./gradlew test",
|
|
246
|
+
framework: "Gradle test",
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
marker: "build.gradle.kts",
|
|
250
|
+
stack: "Java/Kotlin Gradle",
|
|
251
|
+
testCommand: "./gradlew test",
|
|
252
|
+
framework: "Gradle test",
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
marker: "pubspec.yaml",
|
|
256
|
+
stack: "Dart/Flutter",
|
|
257
|
+
testCommand: "dart test",
|
|
258
|
+
framework: "Dart test",
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
marker: "dune-project",
|
|
262
|
+
stack: "OCaml",
|
|
263
|
+
testCommand: "dune runtest",
|
|
264
|
+
framework: "Dune runtest",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
marker: "shard.yml",
|
|
268
|
+
stack: "Crystal",
|
|
269
|
+
testCommand: "crystal spec",
|
|
270
|
+
framework: "Crystal spec",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
marker: "stack.yaml",
|
|
274
|
+
stack: "Haskell",
|
|
275
|
+
testCommand: "stack test",
|
|
276
|
+
framework: "Stack test",
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
function setPrimaryTest(
|
|
281
|
+
detection: Detection,
|
|
282
|
+
command: CommandInfo,
|
|
283
|
+
prefer = false,
|
|
284
|
+
): void {
|
|
285
|
+
if (!detection.testCommand || prefer) {
|
|
286
|
+
detection.testCommand = command.command;
|
|
287
|
+
detection.testFramework = command.framework;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function packageDirs(files: string[]): string[] {
|
|
292
|
+
const dirs = files
|
|
293
|
+
.filter((file) => basename(file) === "package.json")
|
|
294
|
+
.map((file) => (dirname(file) === "." ? "." : dirname(file)));
|
|
295
|
+
return [...new Set(dirs)].sort(
|
|
296
|
+
(a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function filesInScope(files: string[], scope: string): string[] {
|
|
301
|
+
if (scope === ".") return files;
|
|
302
|
+
const prefix = `${scope}/`;
|
|
303
|
+
return files
|
|
304
|
+
.filter((file) => file.startsWith(prefix))
|
|
305
|
+
.map((file) => file.slice(prefix.length));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function detectNodePackage(
|
|
309
|
+
cwd: string,
|
|
310
|
+
files: string[],
|
|
311
|
+
scope: string,
|
|
312
|
+
detection: Detection,
|
|
313
|
+
): void {
|
|
314
|
+
const pkg = readJson<PackageJson>(join(cwd, scope, "package.json"));
|
|
315
|
+
if (!pkg) return;
|
|
316
|
+
const scopedFiles = filesInScope(files, scope);
|
|
317
|
+
const pm = detectPackageManagerAt(cwd, scope);
|
|
318
|
+
if (pm && !detection.packageManagers.includes(pm))
|
|
319
|
+
detection.packageManagers.push(pm);
|
|
320
|
+
addStack(
|
|
321
|
+
detection,
|
|
322
|
+
pkg.type === "module" ? "Node.js/TypeScript ESM" : "Node.js/TypeScript",
|
|
323
|
+
);
|
|
324
|
+
if (pkg.name && detection.projectName === basename(cwd))
|
|
325
|
+
detection.projectName = pkg.name;
|
|
326
|
+
addMarker(
|
|
327
|
+
detection,
|
|
328
|
+
scope === "." ? "package.json" : `${scope}/package.json`,
|
|
329
|
+
);
|
|
330
|
+
if (existsSync(join(cwd, scope, "tsconfig.json")))
|
|
331
|
+
addMarker(detection, `${scope === "." ? "" : `${scope}/`}tsconfig.json`);
|
|
332
|
+
if (pm)
|
|
333
|
+
addMarker(
|
|
334
|
+
detection,
|
|
335
|
+
`${scope === "." ? "" : `${scope}/`}${pm} package manager`,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const allDeps = deps(pkg);
|
|
339
|
+
if (allDeps.has("react")) addStack(detection, "React");
|
|
340
|
+
if (allDeps.has("next")) addStack(detection, "Next.js");
|
|
341
|
+
if (allDeps.has("vue")) addStack(detection, "Vue");
|
|
342
|
+
if (allDeps.has("svelte")) addStack(detection, "Svelte");
|
|
343
|
+
if (allDeps.has("@earendil-works/pi-coding-agent"))
|
|
344
|
+
addStack(detection, "Pi extension package");
|
|
345
|
+
|
|
346
|
+
let unit = scriptCommand(pm, pkg.scripts, [
|
|
347
|
+
"test:run",
|
|
348
|
+
"test",
|
|
349
|
+
"vitest",
|
|
350
|
+
"jest",
|
|
351
|
+
"unit",
|
|
352
|
+
]);
|
|
353
|
+
if (!unit) {
|
|
354
|
+
if (
|
|
355
|
+
allDeps.has("vitest") ||
|
|
356
|
+
scopedFiles.some((file) => /^vitest\.config\./.test(basename(file)))
|
|
357
|
+
) {
|
|
358
|
+
unit = { name: "vitest", command: runScript(pm, "vitest") };
|
|
359
|
+
} else if (
|
|
360
|
+
allDeps.has("jest") ||
|
|
361
|
+
scopedFiles.some((file) => /^jest\.config\./.test(basename(file)))
|
|
362
|
+
) {
|
|
363
|
+
unit = { name: "jest", command: runScript(pm, "jest") };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (unit) {
|
|
367
|
+
const framework =
|
|
368
|
+
allDeps.has("vitest") || /vitest/i.test(unit.command)
|
|
369
|
+
? "Vitest"
|
|
370
|
+
: allDeps.has("jest") || /jest/i.test(unit.command)
|
|
371
|
+
? "Jest"
|
|
372
|
+
: "package script";
|
|
373
|
+
const info = {
|
|
374
|
+
scope,
|
|
375
|
+
command: commandInScope(scope, unit.command),
|
|
376
|
+
framework,
|
|
377
|
+
};
|
|
378
|
+
addUnique(detection.commands.unit, info);
|
|
379
|
+
setPrimaryTest(detection, info);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const integration = scriptCommand(pm, pkg.scripts, [
|
|
383
|
+
"test:integration",
|
|
384
|
+
"integration",
|
|
385
|
+
]);
|
|
386
|
+
if (integration)
|
|
387
|
+
addUnique(detection.commands.integration, {
|
|
388
|
+
scope,
|
|
389
|
+
command: commandInScope(scope, integration.command),
|
|
390
|
+
framework: "package integration script",
|
|
391
|
+
});
|
|
392
|
+
if (allDeps.has("@testing-library/react") || allDeps.has("supertest")) {
|
|
393
|
+
const framework = allDeps.has("supertest")
|
|
394
|
+
? "Supertest"
|
|
395
|
+
: "Testing Library";
|
|
396
|
+
if (unit)
|
|
397
|
+
addUnique(detection.commands.integration, {
|
|
398
|
+
scope,
|
|
399
|
+
command: commandInScope(scope, unit.command),
|
|
400
|
+
framework,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let e2e = scriptCommand(pm, pkg.scripts, [
|
|
405
|
+
"test:e2e",
|
|
406
|
+
"e2e",
|
|
407
|
+
"playwright",
|
|
408
|
+
"cypress",
|
|
409
|
+
]);
|
|
410
|
+
if (!e2e) {
|
|
411
|
+
if (
|
|
412
|
+
allDeps.has("@playwright/test") ||
|
|
413
|
+
allDeps.has("playwright") ||
|
|
414
|
+
scopedFiles.some((file) => /^playwright\.config\./.test(basename(file)))
|
|
415
|
+
) {
|
|
416
|
+
e2e = { name: "playwright", command: "npx playwright test" };
|
|
417
|
+
} else if (
|
|
418
|
+
allDeps.has("cypress") ||
|
|
419
|
+
scopedFiles.some((file) => /^cypress\.config\./.test(basename(file)))
|
|
420
|
+
) {
|
|
421
|
+
e2e = { name: "cypress", command: "npx cypress run" };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (e2e) {
|
|
425
|
+
const framework = /cypress/i.test(e2e.command) ? "Cypress" : "Playwright";
|
|
426
|
+
addUnique(detection.commands.e2e, {
|
|
427
|
+
scope,
|
|
428
|
+
command: commandInScope(scope, e2e.command),
|
|
429
|
+
framework,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const coverage = scriptCommand(pm, pkg.scripts, [
|
|
434
|
+
"test:coverage",
|
|
435
|
+
"coverage",
|
|
436
|
+
]);
|
|
437
|
+
if (coverage)
|
|
438
|
+
addUnique(detection.commands.coverage, {
|
|
439
|
+
scope,
|
|
440
|
+
command: commandInScope(scope, coverage.command),
|
|
441
|
+
framework: "coverage",
|
|
442
|
+
});
|
|
443
|
+
const lint = scriptCommand(pm, pkg.scripts, [
|
|
444
|
+
"lint",
|
|
445
|
+
"lint:check",
|
|
446
|
+
"check:lint",
|
|
447
|
+
]);
|
|
448
|
+
if (lint)
|
|
449
|
+
addUnique(detection.commands.lint, {
|
|
450
|
+
scope,
|
|
451
|
+
command: commandInScope(scope, lint.command),
|
|
452
|
+
framework: "linter",
|
|
453
|
+
});
|
|
454
|
+
const typecheck = scriptCommand(pm, pkg.scripts, [
|
|
455
|
+
"typecheck",
|
|
456
|
+
"type-check",
|
|
457
|
+
"check:types",
|
|
458
|
+
]);
|
|
459
|
+
if (typecheck)
|
|
460
|
+
addUnique(detection.commands.typecheck, {
|
|
461
|
+
scope,
|
|
462
|
+
command: commandInScope(scope, typecheck.command),
|
|
463
|
+
framework: "type checker",
|
|
464
|
+
});
|
|
465
|
+
const format = scriptCommand(pm, pkg.scripts, [
|
|
466
|
+
"format",
|
|
467
|
+
"format:check",
|
|
468
|
+
"fmt",
|
|
469
|
+
"prettier",
|
|
470
|
+
]);
|
|
471
|
+
if (format)
|
|
472
|
+
addUnique(detection.commands.format, {
|
|
473
|
+
scope,
|
|
474
|
+
command: commandInScope(scope, format.command),
|
|
475
|
+
framework: "formatter",
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function detectNode(cwd: string, files: string[], detection: Detection): void {
|
|
480
|
+
const dirs = packageDirs(files);
|
|
481
|
+
if (dirs.length === 0 && files.some((file) => /\.[cm]?[tj]sx?$/.test(file)))
|
|
482
|
+
addStack(detection, "Node.js/TypeScript");
|
|
483
|
+
for (const dir of dirs) detectNodePackage(cwd, files, dir, detection);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function detectGo(cwd: string, files: string[], detection: Detection): void {
|
|
487
|
+
if (!hasFile(cwd, "go.mod")) return;
|
|
488
|
+
addStack(detection, "Go");
|
|
489
|
+
addMarker(detection, "go.mod");
|
|
490
|
+
const info = { scope: ".", command: "go test ./...", framework: "go test" };
|
|
491
|
+
addUnique(detection.commands.unit, info);
|
|
492
|
+
setPrimaryTest(detection, info);
|
|
493
|
+
if (files.some((file) => file.endsWith("_test.go")))
|
|
494
|
+
addUnique(detection.commands.integration, {
|
|
495
|
+
scope: ".",
|
|
496
|
+
command: "go test ./...",
|
|
497
|
+
framework: "Go integration tests where present",
|
|
498
|
+
});
|
|
499
|
+
addUnique(detection.commands.coverage, {
|
|
500
|
+
scope: ".",
|
|
501
|
+
command: "go test -cover ./...",
|
|
502
|
+
framework: "go coverage",
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function detectRust(cwd: string, detection: Detection): void {
|
|
507
|
+
if (!hasFile(cwd, "Cargo.toml")) return;
|
|
508
|
+
addStack(detection, "Rust");
|
|
509
|
+
addMarker(detection, "Cargo.toml");
|
|
510
|
+
const info = { scope: ".", command: "cargo test", framework: "cargo test" };
|
|
511
|
+
addUnique(detection.commands.unit, info);
|
|
512
|
+
setPrimaryTest(detection, info);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function detectPython(
|
|
516
|
+
cwd: string,
|
|
517
|
+
files: string[],
|
|
518
|
+
detection: Detection,
|
|
519
|
+
): void {
|
|
520
|
+
const hasPython =
|
|
521
|
+
hasFile(cwd, "pyproject.toml") ||
|
|
522
|
+
hasFile(cwd, "requirements.txt") ||
|
|
523
|
+
hasFile(cwd, "pytest.ini") ||
|
|
524
|
+
files.some((file) => file.endsWith(".py"));
|
|
525
|
+
if (!hasPython) return;
|
|
526
|
+
addStack(detection, "Python");
|
|
527
|
+
for (const marker of ["pyproject.toml", "requirements.txt", "pytest.ini"]) {
|
|
528
|
+
if (hasFile(cwd, marker)) addMarker(detection, marker);
|
|
529
|
+
}
|
|
530
|
+
if (
|
|
531
|
+
files.some(
|
|
532
|
+
(file) =>
|
|
533
|
+
file.startsWith("tests/") ||
|
|
534
|
+
file.endsWith("_test.py") ||
|
|
535
|
+
basename(file) === "conftest.py",
|
|
536
|
+
)
|
|
537
|
+
) {
|
|
538
|
+
const info = { scope: ".", command: "pytest", framework: "pytest" };
|
|
539
|
+
addUnique(detection.commands.unit, info);
|
|
540
|
+
setPrimaryTest(detection, info);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function detectGenericHints(
|
|
545
|
+
cwd: string,
|
|
546
|
+
files: string[],
|
|
547
|
+
detection: Detection,
|
|
548
|
+
): void {
|
|
549
|
+
for (const hint of GENERIC_HINTS) {
|
|
550
|
+
if (!hasFile(cwd, hint.marker)) continue;
|
|
551
|
+
addStack(detection, hint.stack);
|
|
552
|
+
addMarker(detection, hint.marker);
|
|
553
|
+
addEvidence(
|
|
554
|
+
detection,
|
|
555
|
+
`${hint.stack} manifest detected via ${hint.marker}`,
|
|
556
|
+
);
|
|
557
|
+
if (hint.testCommand && hint.framework) {
|
|
558
|
+
const info = {
|
|
559
|
+
scope: ".",
|
|
560
|
+
command: hint.testCommand,
|
|
561
|
+
framework: hint.framework,
|
|
562
|
+
};
|
|
563
|
+
addUnique(detection.commands.unit, info);
|
|
564
|
+
setPrimaryTest(detection, info);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const testFiles = files.filter(
|
|
569
|
+
(file) =>
|
|
570
|
+
/(^|\/)(test|tests|spec|specs)(\/|$)/i.test(file) ||
|
|
571
|
+
/[._-](test|spec)\.[^/]+$/i.test(file),
|
|
572
|
+
);
|
|
573
|
+
if (testFiles.length > 0) {
|
|
574
|
+
addEvidence(
|
|
575
|
+
detection,
|
|
576
|
+
`Test-like files detected (${testFiles.length}); examples: ${testFiles.slice(0, 5).join(", ")}`,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
if (detection.stack.length === 0)
|
|
580
|
+
addStack(detection, "Unclassified software project");
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function detectMakefile(cwd: string, detection: Detection): void {
|
|
584
|
+
const makefile = ["Makefile", "makefile"].find((file) => hasFile(cwd, file));
|
|
585
|
+
if (!makefile) return;
|
|
586
|
+
addMarker(detection, makefile);
|
|
587
|
+
let content = "";
|
|
588
|
+
try {
|
|
589
|
+
content = readFileSync(join(cwd, makefile), "utf8");
|
|
590
|
+
} catch {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (/^test:/m.test(content)) {
|
|
594
|
+
const info = {
|
|
595
|
+
scope: ".",
|
|
596
|
+
command: "make test",
|
|
597
|
+
framework: "pytest via Makefile",
|
|
598
|
+
};
|
|
599
|
+
addUnique(detection.commands.unit, info);
|
|
600
|
+
setPrimaryTest(detection, info, true);
|
|
601
|
+
}
|
|
602
|
+
if (/^coverage:/m.test(content))
|
|
603
|
+
addUnique(detection.commands.coverage, {
|
|
604
|
+
scope: ".",
|
|
605
|
+
command: "make coverage",
|
|
606
|
+
framework: "coverage",
|
|
607
|
+
});
|
|
608
|
+
if (/^lint:/m.test(content))
|
|
609
|
+
addUnique(detection.commands.lint, {
|
|
610
|
+
scope: ".",
|
|
611
|
+
command: "make lint",
|
|
612
|
+
framework: "linter",
|
|
613
|
+
});
|
|
614
|
+
if (/^(fmt|format):/m.test(content))
|
|
615
|
+
addUnique(detection.commands.format, {
|
|
616
|
+
scope: ".",
|
|
617
|
+
command: /^fmt:/m.test(content) ? "make fmt" : "make format",
|
|
618
|
+
framework: "formatter",
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function detectProject(cwd: string): Detection {
|
|
623
|
+
const files = walkProject(cwd);
|
|
624
|
+
const detection: Detection = {
|
|
625
|
+
projectName: basename(cwd),
|
|
626
|
+
stack: [],
|
|
627
|
+
packageManagers: [],
|
|
628
|
+
markers: [],
|
|
629
|
+
evidence: [],
|
|
630
|
+
commands: {
|
|
631
|
+
unit: [],
|
|
632
|
+
integration: [],
|
|
633
|
+
e2e: [],
|
|
634
|
+
coverage: [],
|
|
635
|
+
lint: [],
|
|
636
|
+
typecheck: [],
|
|
637
|
+
format: [],
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
detectNode(cwd, files, detection);
|
|
641
|
+
detectGo(cwd, files, detection);
|
|
642
|
+
detectRust(cwd, detection);
|
|
643
|
+
detectPython(cwd, files, detection);
|
|
644
|
+
detectGenericHints(cwd, files, detection);
|
|
645
|
+
detectMakefile(cwd, detection);
|
|
646
|
+
if (hasFile(cwd, ".github/workflows")) addMarker(detection, "GitHub Actions");
|
|
647
|
+
detection.coverageCommand = detection.commands.coverage[0]?.command;
|
|
648
|
+
detection.lintCommand = detection.commands.lint[0]?.command;
|
|
649
|
+
detection.typecheckCommand = detection.commands.typecheck[0]?.command;
|
|
650
|
+
detection.formatCommand = detection.commands.format[0]?.command;
|
|
651
|
+
return detection;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function commandSummary(commands: CommandInfo[]): string {
|
|
655
|
+
if (commands.length === 0) return "none";
|
|
656
|
+
return commands
|
|
657
|
+
.map((command) => `${command.framework} (${command.command})`)
|
|
658
|
+
.join("; ");
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function renderContext(detection: Detection): string {
|
|
662
|
+
const lines = [
|
|
663
|
+
`${detection.projectName} is a ${detection.stack.length > 0 ? detection.stack.join(", ") : "software"} project.`,
|
|
664
|
+
`Detected markers: ${detection.markers.length > 0 ? detection.markers.join(", ") : "none"}.`,
|
|
665
|
+
];
|
|
666
|
+
if (detection.packageManagers.length > 0)
|
|
667
|
+
lines.push(`Package managers: ${detection.packageManagers.join(", ")}.`);
|
|
668
|
+
if (detection.evidence.length > 0)
|
|
669
|
+
lines.push(`Additional evidence: ${detection.evidence.join("; ")}.`);
|
|
670
|
+
if (detection.testCommand)
|
|
671
|
+
lines.push(`Primary test command: ${detection.testCommand}.`);
|
|
672
|
+
else
|
|
673
|
+
lines.push(
|
|
674
|
+
"No reliable test runner was detected; verify testing manually before enabling strict TDD.",
|
|
675
|
+
);
|
|
676
|
+
lines.push(`Unit tests: ${commandSummary(detection.commands.unit)}.`);
|
|
677
|
+
lines.push(
|
|
678
|
+
`Integration tests: ${commandSummary(detection.commands.integration)}.`,
|
|
679
|
+
);
|
|
680
|
+
lines.push(`E2E tests: ${commandSummary(detection.commands.e2e)}.`);
|
|
681
|
+
return lines.join("\n");
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function pushCommandList(
|
|
685
|
+
lines: string[],
|
|
686
|
+
indent: string,
|
|
687
|
+
commands: CommandInfo[],
|
|
688
|
+
): void {
|
|
689
|
+
if (commands.length === 0) {
|
|
690
|
+
lines.push(`${indent}[]`);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
for (const command of commands) {
|
|
694
|
+
lines.push(`${indent}- scope: ${yamlString(command.scope)}`);
|
|
695
|
+
lines.push(`${indent} command: ${yamlString(command.command)}`);
|
|
696
|
+
lines.push(`${indent} framework: ${yamlString(command.framework)}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function renderConfig(detection: Detection): string {
|
|
701
|
+
const strictTdd = Boolean(detection.testCommand);
|
|
702
|
+
const testCommand = detection.testCommand ?? "";
|
|
703
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
704
|
+
const context = renderContext(detection);
|
|
705
|
+
const unitLayer = detection.commands.unit
|
|
706
|
+
.map((command) => command.framework)
|
|
707
|
+
.join(", ");
|
|
708
|
+
const integrationLayer = detection.commands.integration
|
|
709
|
+
.map((command) => command.framework)
|
|
710
|
+
.join(", ");
|
|
711
|
+
const e2eLayer = detection.commands.e2e
|
|
712
|
+
.map((command) => command.framework)
|
|
713
|
+
.join(", ");
|
|
15
714
|
const lines = [
|
|
16
715
|
`strict_tdd: ${strictTdd}`,
|
|
17
716
|
"context: |",
|
|
18
|
-
escapeBlockScalar(context
|
|
717
|
+
escapeBlockScalar(context),
|
|
19
718
|
"rules:",
|
|
719
|
+
" proposal:",
|
|
720
|
+
" require_problem_statement: true",
|
|
721
|
+
" spec:",
|
|
722
|
+
" require_acceptance_criteria: true",
|
|
723
|
+
" design:",
|
|
724
|
+
" require_tradeoffs: true",
|
|
725
|
+
" tasks:",
|
|
726
|
+
" protect_review_workload: true",
|
|
20
727
|
" apply:",
|
|
21
|
-
` test_command: ${testCommand}`,
|
|
728
|
+
` test_command: ${yamlString(testCommand)}`,
|
|
729
|
+
" verify:",
|
|
730
|
+
` test_command: ${yamlString(testCommand)}`,
|
|
22
731
|
"testing:",
|
|
732
|
+
` detected: ${yamlString(today)}`,
|
|
23
733
|
" runner:",
|
|
24
|
-
` command: ${testCommand}`,
|
|
25
|
-
""
|
|
734
|
+
` command: ${yamlString(testCommand)}`,
|
|
735
|
+
` framework: ${yamlString(detection.testFramework ?? "")}`,
|
|
736
|
+
" layers:",
|
|
737
|
+
` unit: ${yamlString(unitLayer)}`,
|
|
738
|
+
` integration: ${yamlString(integrationLayer)}`,
|
|
739
|
+
` e2e: ${yamlString(e2eLayer)}`,
|
|
740
|
+
" commands:",
|
|
741
|
+
" unit:",
|
|
26
742
|
];
|
|
743
|
+
pushCommandList(lines, " ", detection.commands.unit);
|
|
744
|
+
lines.push(" integration:");
|
|
745
|
+
pushCommandList(lines, " ", detection.commands.integration);
|
|
746
|
+
lines.push(" e2e:");
|
|
747
|
+
pushCommandList(lines, " ", detection.commands.e2e);
|
|
748
|
+
lines.push(" coverage:");
|
|
749
|
+
lines.push(` command: ${yamlString(detection.coverageCommand ?? "")}`);
|
|
750
|
+
lines.push(" commands:");
|
|
751
|
+
pushCommandList(lines, " ", detection.commands.coverage);
|
|
752
|
+
lines.push("quality:");
|
|
753
|
+
lines.push(` lint: ${yamlString(detection.lintCommand ?? "")}`);
|
|
754
|
+
lines.push(" lint_commands:");
|
|
755
|
+
pushCommandList(lines, " ", detection.commands.lint);
|
|
756
|
+
lines.push(` typecheck: ${yamlString(detection.typecheckCommand ?? "")}`);
|
|
757
|
+
lines.push(" typecheck_commands:");
|
|
758
|
+
pushCommandList(lines, " ", detection.commands.typecheck);
|
|
759
|
+
lines.push(` format: ${yamlString(detection.formatCommand ?? "")}`);
|
|
760
|
+
lines.push(" format_commands:");
|
|
761
|
+
pushCommandList(lines, " ", detection.commands.format);
|
|
762
|
+
lines.push("");
|
|
27
763
|
return lines.join("\n");
|
|
28
764
|
}
|
|
29
765
|
|
|
766
|
+
function ensureOpenSpecDirs(cwd: string): void {
|
|
767
|
+
mkdirSync(join(cwd, "openspec", "specs"), { recursive: true });
|
|
768
|
+
mkdirSync(join(cwd, "openspec", "changes", "archive"), { recursive: true });
|
|
769
|
+
}
|
|
770
|
+
|
|
30
771
|
export default function (pi: ExtensionAPI) {
|
|
31
772
|
pi.registerCommand("sdd-init", {
|
|
32
|
-
description:
|
|
33
|
-
|
|
773
|
+
description:
|
|
774
|
+
"Auto-detect project stack and bootstrap openspec/config.yaml for SDD.",
|
|
775
|
+
handler: async (_args: unknown, ctx: any) => {
|
|
34
776
|
const configPath = join(ctx.cwd, CONFIG_REL_PATH);
|
|
35
777
|
if (existsSync(configPath)) {
|
|
36
778
|
ctx.ui.notify(
|
|
@@ -40,42 +782,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
40
782
|
return;
|
|
41
783
|
}
|
|
42
784
|
|
|
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
|
-
|
|
785
|
+
const detection = detectProject(ctx.cwd);
|
|
786
|
+
ensureOpenSpecDirs(ctx.cwd);
|
|
75
787
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
76
|
-
writeFileSync(configPath, renderConfig(
|
|
788
|
+
writeFileSync(configPath, renderConfig(detection));
|
|
789
|
+
|
|
790
|
+
const testSummary = detection.testCommand
|
|
791
|
+
? `strict TDD enabled with \`${detection.testCommand}\``
|
|
792
|
+
: "strict TDD disabled because no test runner was detected";
|
|
793
|
+
const layerSummary = `unit: ${detection.commands.unit.length}, integration: ${detection.commands.integration.length}, e2e: ${detection.commands.e2e.length}`;
|
|
77
794
|
ctx.ui.notify(
|
|
78
|
-
`Wrote ${CONFIG_REL_PATH}
|
|
795
|
+
`Wrote ${CONFIG_REL_PATH}: detected ${detection.stack.join(", ") || "project"}; ${testSummary}; tests found: ${layerSummary}.`,
|
|
79
796
|
"info",
|
|
80
797
|
);
|
|
81
798
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
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",
|