Skip to content

fix(pull-sdlc): tracked bootstrap files (Pull-SDLC.ai.ps1, Cleanup-Worktree.ps1, ...) stay permanently modified after self-refresh #135

Description

@MarkMichaelis

Symptom

After running .\Pull-SDLC.ai.ps1 in a consumer repo that already has
Pull-SDLC.ai.ps1 tracked at an older version, git status continues
to show Pull-SDLC.ai.ps1 as modified, forever, with no automatic
path to resolution. The sync PR (chore/sdlc-sync) opens fine but does
not include Pull-SDLC.ai.ps1, so even after that PR merges the
consumer's tracked copy stays at the old SHA.

Reproduced live in D:\Git\GmailNewsClient (May 18, 2026, against
upstream e25eca7 -- the PR #134 merge):

.\Pull-SDLC.ai.ps1
... runs cleanly ...
Existing PR updated: https://github.com/IntelliTect-Samples/GmailNewsClient/pull/317
PS> git status
On branch main
        modified:   Pull-SDLC.ai.ps1

Root cause

  1. Pull-SDLC.ai.ps1 is intentionally NOT in
    `` (see the script header docstring).
  2. Self-refresh writes the new version to the working tree but never
    commits anywhere -- the rewrite happens in the parent main tree, not
    in the .worktrees/sdlc-sync scratch worktree, so the sync commit
    inside the worktree contains the OLD Pull-SDLC.ai.ps1.
  3. After PR fix(pull-sdlc): scratch-state auto-recovery (force-with-lease + autocrlf hash + no silent downgrade) #134's safety tightening in Invoke-MainTreeCleanup, the
    cleanup correctly refuses to git checkout -- Pull-SDLC.ai.ps1
    when HEAD:path != UpstreamRef:path (avoiding the silent
    downgrade). Good. But that means the modified state is now permanent
    until the consumer hand-commits the file.

The consumer is stuck with a permanently-dirty working tree they didn't
cause and can't resolve via Pull-SDLC.ai.ps1 alone. This violates the
"just works" promise of the script.

Affected bootstrap files

Same logic applies to every bootstrap file the user dropped in to get
the script running, currently excluded from upstream-managed paths:

  • Pull-SDLC.ai.ps1 (the most visible / annoying case)
  • Pull-SDLC.ai.Tests.ps1
  • Cleanup-Worktree.ps1
  • Consolidate-Tasks.ps1 and Consolidate-Tasks.Tests.ps1
  • sync-manifest.json

The current Invoke-MainTreeCleanup candidate list (line ~463) names
exactly these files; treating them all as upstream-managed would unify
the rule.

Proposed approach (not for immediate implementation)

Option A -- Promote bootstrap files into ``.
Add Pull-SDLC.ai.ps1 (and siblings above) to the managed-paths list.
The diff-replay inside `.worktrees/sdlc-sync` will then write the new
versions on top of the worktree's `main` baseline, the sync commit
will include them, and the `chore/sdlc-sync` PR will carry them. After
the PR merges, the consumer's tracked `Pull-SDLC.ai.ps1` lands at the
new SHA and `git status` is clean.

Caveats to think through during the dev loop:

  • Self-refresh races with replay: self-refresh writes the new script to
    the parent main tree before the worktree runs. After the
    re-exec, the worktree starts from main (which still has the OLD
    script) and the replay writes the new version, so the order works.
    But the parent tree's working copy still shows Pull-SDLC.ai.ps1
    modified vs HEAD until the PR merges; cleanup should leave it alone
    in that window. Invoke-MainTreeCleanup's post-PR-134 safety check
    (revert only when HEAD == upstream) already handles this -- once
    the sync PR merges, HEAD catches up and the next run cleans up.
  • The pre-flight drift guard (-Force documented in the script
    header) should NOT trip on Pull-SDLC.ai.ps1 self-refresh diffs.
    Either exempt the script itself from the drift check, or compare
    using --no-filters raw bytes (same fix as fix(pull-sdlc): scratch-state auto-recovery (force-with-lease + autocrlf hash + no silent downgrade) #133).
  • Self-refresh needs to keep working for the very first bootstrap run
    in repos where the file isn't tracked yet -- the diff-replay handles
    "file does not exist yet" already, so should be fine.

Option B -- Have the sync commit also stage these files inside the
worktree from the parent tree's working copy.
Less consistent with
how other files flow through ``; rejected unless
Option A turns out to have a blocking edge case.

Secondary observation (probably a separate issue)

The May 18 run also emitted 11x
warning: in the working copy of '<file>', LF will be replaced by CRLF the next time Git touches it
inside the worktree. These come from the diff-replay writing LF-stored
upstream blobs into a CRLF-flagged working tree on Windows. They're
cosmetic but noisy. Consider:

  • Setting core.safecrlf=false for the duration of the replay, or
  • Writing replay files with -NoNewline after manually normalizing,
    or
  • Updating .gitattributes.template to declare these paths as
    text=auto eol=lf so git stops warning.

Out of scope for this issue; mention if a future dev loop touches the
replay writer.

Acceptance criteria

After this issue's fix lands:

  • In any consumer repo where Pull-SDLC.ai.ps1 is tracked at an
    older version, running .\Pull-SDLC.ai.ps1 produces a sync PR
    that includes the updated Pull-SDLC.ai.ps1 (and siblings above).
  • After that PR merges, git status is clean -- zero modified
    files.
  • Self-refresh continues to work for first-time bootstrap (file not
    yet tracked).
  • The pre-flight drift guard does not block on Pull-SDLC.ai.ps1
    having self-refreshed content.
  • All existing Pester tests stay green; new tests cover the
    "bootstrap file in UpstreamManagedPaths" scenarios.

Related

This issue is the last remaining "just works" gap.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions