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
- - Create or update `openspec/config.yaml` with project context, `strict_tdd`, phase rules, and testing runner details.
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 or refresh SDD context for {task}. Detect project context, testing capabilities, strict TDD setting, and skill registry availability.
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
- reads: apply-progress.md
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
@@ -1,8 +1,62 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
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 renderConfig(strictTdd: boolean, testCommand: string, context: string): string {
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.trimEnd()),
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: "Bootstrap openspec/config.yaml for SDD workflow (one-time per project).",
33
- handler: async (_args, ctx) => {
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 TDD_YES = "Yes — tests must run before each change";
44
- const TDD_NO = "No — TDD is opt-in per task";
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(strictTdd, testCommand.trim(), context));
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}. Run /skill-registry:refresh once skills with '## Compact Rules' are available.`,
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.18",
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",