feat(init): auto-seed PREFERENCES.md with detected verification_commands

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 <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-04-29 14:26:49 +02:00
parent 58b1d7c601
commit 6248e79a7a
2 changed files with 44 additions and 3 deletions

View file

@ -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 (

View file

@ -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: []