diff --git a/src/resources/extensions/sf/git-runtime-patterns.js b/src/resources/extensions/sf/git-runtime-patterns.js index fc0e50b3e..11fd6f6d8 100644 --- a/src/resources/extensions/sf/git-runtime-patterns.js +++ b/src/resources/extensions/sf/git-runtime-patterns.js @@ -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", ]; diff --git a/src/resources/extensions/sf/git-service.js b/src/resources/extensions/sf/git-service.js index 296777c14..51f459acb 100644 --- a/src/resources/extensions/sf/git-service.js +++ b/src/resources/extensions/sf/git-service.js @@ -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. diff --git a/src/resources/extensions/sf/gitignore.js b/src/resources/extensions/sf/gitignore.js index 82887b6ad..8fcfb0b01 100644 --- a/src/resources/extensions/sf/gitignore.js +++ b/src/resources/extensions/sf/gitignore.js @@ -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) {