From 6248e79a7adeba1c647b4b11287daf4028705c20 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Wed, 29 Apr 2026 14:26:49 +0200 Subject: [PATCH] feat(init): auto-seed PREFERENCES.md with detected verification_commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, every fresh project inherits sf's user-level dogfooding defaults (npm run typecheck:extensions, test:sf-light) — which run sf's own dev scripts against unrelated repos and produce universal false negatives. Hit in dr-repo (Go): T01-VERIFY.json showed all_fail because those npm scripts don't exist there, even though T01's actual work passed verification per its SUMMARY. - ensurePreferences() now calls detectProjectSignals() and embeds the auto-detected commands in the YAML frontmatter on first init. Detection failure is non-fatal — falls back to the bare template. - detectVerificationCommands() Go branch now handles multi-module repos (no root go.mod, only nested ones — common pattern for repos like dr-repo/{dr-agent,portal,gateway,installer,cmd/installer}). Generates a per-module loop instead of running go vet/test from the repo root, which would fail since each subdir is its own Go module. Co-Authored-By: Claude Opus 4.7 --- src/resources/extensions/sf/detection.ts | 26 ++++++++++++++++++++++-- src/resources/extensions/sf/gitignore.ts | 21 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/resources/extensions/sf/detection.ts b/src/resources/extensions/sf/detection.ts index 5079a58f3..3742544be 100644 --- a/src/resources/extensions/sf/detection.ts +++ b/src/resources/extensions/sf/detection.ts @@ -760,8 +760,30 @@ function detectVerificationCommands( if (detectedFiles.includes("go.mod")) { // Limit parallelism: Go's default is GOMAXPROCS which can be very high. - commands.push("go test -parallel 2 ./..."); - commands.push("go vet ./..."); + const rootHasGoMod = existsSync(join(basePath, "go.mod")); + if (rootHasGoMod) { + commands.push("go test -parallel 2 ./..."); + commands.push("go vet ./..."); + } else { + // Multi-module repo (no root go.mod, only nested ones — common in + // monorepos like dr-repo/{dr-agent,portal,gateway,...}). Find each + // module dir and emit a per-module loop so commands work from the + // repo root regardless of which modules exist. + const scanned = scanProjectFiles(basePath); + const moduleDirs = scanned + .filter((f) => f.endsWith("/go.mod") || f === "go.mod") + .map((f) => (f === "go.mod" ? "." : f.slice(0, -"/go.mod".length))) + .filter((d) => d.length > 0 && !d.includes("..")); + if (moduleDirs.length > 0) { + const dirsArg = moduleDirs.map((d) => `"${d}"`).join(" "); + commands.push( + `bash -c 'set -e; for d in ${dirsArg}; do (cd "$d" && go vet ./...); done'`, + ); + commands.push( + `bash -c 'set -e; for d in ${dirsArg}; do (cd "$d" && go test -parallel 2 ./...); done'`, + ); + } + } } if ( diff --git a/src/resources/extensions/sf/gitignore.ts b/src/resources/extensions/sf/gitignore.ts index c57fdb654..58d831856 100644 --- a/src/resources/extensions/sf/gitignore.ts +++ b/src/resources/extensions/sf/gitignore.ts @@ -9,6 +9,8 @@ import { execFileSync } from "node:child_process"; import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; +import { yamlSafeString } from "./commands-prefs-wizard.js"; +import { detectProjectSignals } from "./detection.js"; import { GIT_NO_PROMPT_ENV } from "./git-constants.js"; import { nativeLsFiles, nativeRmCached } from "./native-git-bridge.js"; import { sfRoot } from "./paths.js"; @@ -271,9 +273,26 @@ export function ensurePreferences(basePath: string): boolean { return false; } + // Auto-detect project type and seed verification_commands. Without this, + // projects fall back to the user-level defaults — which point at sf's own + // dev scripts (npm run typecheck:extensions, test:sf-light) and produce + // false negatives on every non-Node project. Detection failure is non-fatal. + let verifySection = ""; + try { + const signals = detectProjectSignals(basePath); + if (signals.verificationCommands.length > 0) { + const lines = signals.verificationCommands.map( + (c) => ` - ${yamlSafeString(c)}`, + ); + verifySection = `verification_commands:\n${lines.join("\n")}\n`; + } + } catch { + // fall through to bare template + } + const template = `--- version: 1 -always_use_skills: [] +${verifySection}always_use_skills: [] prefer_skills: [] avoid_skills: [] skill_rules: []