fix: exclude generated sf milestones from staging

This commit is contained in:
Mikael Hugo 2026-05-07 04:02:34 +02:00
parent 4f39c3f4c8
commit 88cf545821
3 changed files with 24 additions and 28 deletions

View file

@ -5,10 +5,11 @@
* cleanup, .gitignore bootstrapping, and diagnostics must keep out of commits.
*/
/**
* Lists SF runtime paths that should stay out of user commits.
* Lists SF runtime/generated paths that should stay out of user commits.
*
* Purpose: keep generated state, locks, databases, and continuation files from
* polluting project history while allowing durable planning artifacts to remain trackable.
* Purpose: keep generated state, locks, databases, milestone workspaces, and
* continuation files from polluting project history. Durable plans/specs/ADRs
* are promoted to docs instead of committed from `.sf/milestones`.
*
* Consumer: gitignore.ts for .git/info/exclude bootstrapping and git-service.ts for staging exclusions.
*/
@ -22,6 +23,7 @@ export const SF_RUNTIME_PATTERNS = [
".sf/parallel/",
".sf/reports/",
".sf/runtime/",
".sf/milestones/",
".sf/worktrees/",
".sf/auto.lock",
".sf/metrics.json",
@ -38,6 +40,4 @@ export const SF_RUNTIME_PATTERNS = [
".sf/SELF-FEEDBACK.md",
".sf/repo-meta.json",
".sf/DISCUSSION-MANIFEST.json",
".sf/milestones/**/*-CONTINUE.md",
".sf/milestones/**/continue.md",
];

View file

@ -405,12 +405,10 @@ export class GitServiceImpl {
// in a dedicated commit. This must happen as a separate commit because
// the git reset HEAD step below would otherwise undo the rm --cached.
//
// SAFETY: Only untrack the specific RUNTIME paths (activity/, runtime/,
// auto.lock, etc.) — NOT all of .sf/. If .sf/milestones/ files were
// previously tracked, they stay tracked until the milestone completes
// and the worktree is torn down. This prevents a mid-execution behavioral
// discontinuity where the first half of a milestone has .sf/ artifacts
// committed but the second half doesn't (#1326).
// SAFETY: Only untrack known runtime/generated paths (activity/, runtime/,
// milestones/, auto.lock, etc.) — NOT all of .sf/. Human-authored .sf
// guidance can remain tracked, while generated milestone workspaces are
// promoted to docs before becoming durable project artifacts.
if (!this._runtimeFilesCleanedUp) {
let cleaned = false;
for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
@ -430,12 +428,9 @@ export class GitServiceImpl {
// hashed by git. The old approach of `git add -A` followed by unstaging
// hangs indefinitely on repos with large untracked artifact trees (#1605).
//
// Exclude only RUNTIME paths from staging — not the entire .sf/ directory.
// When .sf/milestones/ files are already tracked in the index (projects
// where .sf/ is not gitignored, or Windows junctions that git sees as
// real directories), they should continue to be committed. Excluding the
// entire .sf/ directory mid-milestone causes silent commit failure where
// the second half of a milestone's artifacts are never committed (#1326).
// Exclude runtime/generated paths from staging — not the entire .sf/
// directory. This keeps deliberate .sf guidance trackable while preventing
// generated milestone workspaces from becoming peer source-of-truth files.
//
// If .sf/ IS in .gitignore (the default for external state projects),
// git add -A already skips it and the exclusions are harmless no-ops.

View file

@ -19,14 +19,15 @@ import { bodyHash as preferencesBodyHash } from "./scaffold-versioning.js";
export { SF_RUNTIME_PATTERNS } from "./git-runtime-patterns.js";
/**
* SF runtime exclusion patterns for repos where .sf/ is a LOCAL DIRECTORY.
* Granular so that durable planning artifacts (.sf/milestones/, .sf/PROJECT.md,
* .sf/DECISIONS.md) remain trackable in git per ADR-001.
* SF runtime/generated exclusion patterns for repos where .sf/ is a LOCAL DIRECTORY.
* Granular so deliberate human-authored guidance such as .sf/PRINCIPLES.md,
* .sf/TASTE.md, and .sf/ANTI-GOALS.md can remain trackable.
*
* NOT used when .sf/ is a symlink symlinks need the blanket SF_SYMLINK_EXCLUSION_PATTERNS
* because git cannot traverse symlinks to match per-file patterns.
*
* Migrated from blanket `.sf` on 2026-05-01 to implement ADR-001.
* Migrated from blanket `.sf` on 2026-05-01; later tightened so
* .sf/milestones is generated runtime state unless promoted to docs.
* Previously migrated out of BASELINE_PATTERNS into .git/info/exclude on 2026-04-29.
*/
const SF_RUNTIME_EXCLUSION_PATTERNS = [
@ -160,7 +161,7 @@ export function hasGitTrackedSfFiles(basePath) {
* all other baseline patterns are still applied normally.
*/
/**
* Write sf-specific runtime exclusion patterns (`.sf`, `.sf-id`, `.bg-shell/`)
* Write sf-specific runtime/generated exclusion patterns (`.sf`, `.sf-id`, `.bg-shell/`)
* to `.git/info/exclude` per-clone, never committed, never causes
* working-tree churn. Idempotent: only writes when something is missing.
*
@ -183,8 +184,8 @@ export function ensureGitInfoExclude(basePath) {
: "";
// Determine whether .sf is a symlink (external state) or a local directory.
// Symlink: git cannot traverse it, so only the blanket .sf pattern works.
// Directory: use granular patterns so .sf/milestones/ and other durable
// planning artifacts can be tracked per ADR-001.
// Directory: use granular patterns so deliberate human-authored .sf guidance
// can be tracked while generated runtime state stays ignored.
const sfIsSymlink = (() => {
const localSf = join(basePath, ".sf");
try {
@ -215,7 +216,7 @@ export function ensureGitInfoExclude(basePath) {
if (missing.length > 0) {
const block = [
"",
"# ── SF runtime exclusion (managed by sf, per-clone) ──",
"# ── SF runtime/generated exclusion (managed by sf, per-clone) ──",
...missing,
"",
].join("\n");
@ -272,9 +273,9 @@ export function ensureGitignore(basePath, options) {
*
* Only removes from the index (`--cached`), never from disk. Idempotent.
*
* Note: These are strictly runtime/ephemeral paths (activity logs, lock files,
* metrics, STATE.md). They are always safe to untrack, even when the project
* intentionally keeps other `.sf/` files (like PROJECT.md, milestones/) in
* Note: These are strictly runtime/generated paths (activity logs, lock files,
* metrics, STATE.md, milestone workspaces). They are always safe to untrack,
* even when the project intentionally keeps human-authored `.sf/` guidance in
* version control.
*/
export function untrackRuntimeFiles(basePath) {